Add basic multipass support.
[bsnes:xml-shaders.git] / reference / shaderview.py
1 #!/usr/bin/python
2 import sys, time
3 import os
4 import os.path
5 import OpenGL
6 from OpenGL.GL import *
7 from OpenGL.GLUT import *
8 from OpenGL.GL import framebufferobjects
9 import texreader
10 import shaderreader
11
12
13 BASEDIR = os.path.abspath(os.path.dirname(__file__))
14
15
16 def load_textures():
17         res = []
18
19         texturePath = os.path.join(BASEDIR, "test-patterns")
20         for name in os.listdir(texturePath):
21                 if not name.endswith(".ppm"):
22                         continue
23
24                 with open(os.path.join(texturePath, name), "r") as handle:
25                         data = handle.read()
26
27                 try:
28                         width, height, pixels = texreader.parse_ppm(data)
29                 except Exception, e:
30                         print >> sys.stderr, "Could not read texture from %r: %s" % (
31                                         name, e)
32                         continue
33
34                 res.append((os.path.basename(name), width, height, pixels))
35
36         return res
37
38
39 def scale_integer(windowW, windowH, inputW, inputH):
40         """
41         Calculates the image as the largest integer multiple of raw frame size.
42         """
43         scaleX = windowW / inputW
44         scaleY = windowH / inputH
45
46         if scaleX > 0 and scaleY > 0:
47                 scale = min(scaleX, scaleY)
48                 finalW = scale * inputW
49                 finalH = scale * inputH
50         else:
51                 if scaleX == 0:
52                         finalW = windowW
53                 else:
54                         finalW = inputW
55
56                 if scaleY == 0:
57                         finalH = windowH
58                 else:
59                         finalH = inputH
60
61         return finalW, finalH
62
63
64 def scale_aspect(windowW, windowH, inputW, inputH):
65         """
66         Calculates the image as the largest multiple of the raw frame size.
67         """
68         scale = min(
69                         float(windowW) / float(inputW),
70                         float(windowH) / float(inputH),
71                 )
72
73         return inputW * scale, inputH * scale
74
75
76 def scale_max(windowW, windowH, inputW, inputH):
77         """
78         Stretch the image to the largest possible size, regardless of aspect.
79         """
80         return windowW, windowH
81
82
83 SCALE_METHODS = [
84                 ("Integer", scale_integer),
85                 ("Aspect-correct", scale_aspect),
86                 ("Maximized", scale_max),
87         ]
88
89
90 class Texture(object):
91
92         def __init__(self, width, height, pixels=None):
93                 self.textureID = glGenTextures(1)
94                 self.width = width
95                 self.height = height
96
97                 glBindTexture(GL_TEXTURE_2D, self.textureID)
98
99                 # We assume the GL implementation can handle non-power-of-two textures,
100                 # so we use the image dimensions directly as texture dimensions.
101                 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height,
102                                 0, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV, pixels)
103
104                 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER)
105                 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER)
106
107         def __del__(self):
108                 glDeleteTextures([self.textureID])
109
110
111 class GLUTDemo(object):
112
113         def __init__(self, shaderFile, textures):
114                 self.start = time.clock()
115                 self.framecount = 0
116
117                 glutInitDisplayMode(GLUT_RGBA)
118
119                 glutInitWindowSize(640, 480)
120                 self.window = glutCreateWindow("XML Shader Demo - Esc to exit")
121                 self.windowW = None
122                 self.windowH = None
123
124                 glutDisplayFunc(self._draw_scene)
125                 glutIdleFunc(self._handle_idle)
126                 glutReshapeFunc(self._handle_resize)
127                 glutKeyboardFunc(self._handle_key)
128
129                 glClearColor(0.0, 0.0, 0.0, 0.0)
130                 glEnable(GL_TEXTURE_2D)
131
132                 with open(shaderFile, "r") as handle:
133                         self.shaderPasses = shaderreader.parse_shader(handle.read())
134
135                 self.textures = textures
136                 textureMenu = glutCreateMenu(self._set_texture)
137                 for index, (filename, _, _, _) in enumerate(self.textures):
138                         glutAddMenuEntry(filename, index)
139
140                 self.inputTexture = None
141                 self._set_texture(0)
142
143                 self.framebufferID = framebufferobjects.glGenFramebuffers(1)
144                 self.framebufferTexture1, self.framebufferTexture2 = \
145                                 glGenTextures(2)
146
147                 scaleMenu = glutCreateMenu(self._set_scale_method)
148                 for index, (desc, _) in enumerate(SCALE_METHODS):
149                         glutAddMenuEntry(desc, index)
150
151                 self._set_scale_method(0)
152
153                 self.masterMenu = glutCreateMenu(lambda _: 0)
154                 glutAddSubMenu("Test pattern", textureMenu)
155                 glutAddSubMenu("Scale method", scaleMenu)
156                 glutAttachMenu(GLUT_RIGHT_BUTTON)
157
158         def _set_texture(self, textureIndex):
159
160                 _, inputW, inputH, pixels = self.textures[textureIndex]
161
162                 self.inputTexture = Texture(inputW, inputH, pixels)
163
164                 # According to the OpenGL docs, a glutCreateMenu callback doesn't need
165                 # to return anything. PyOpenGL seems to think differently.
166                 return 0
167
168         def _set_scale_method(self, index):
169                 self.scaleMethod = SCALE_METHODS[index][1]
170
171                 # According to the OpenGL docs, a glutCreateMenu callback doesn't need
172                 # to return anything. PyOpenGL seems to think differently.
173                 return 0
174
175         def _setUniform(self, program, name, x, y):
176                 loc = glGetUniformLocation(program, name)
177
178                 if loc < 0:
179                         return
180
181                 glUniform2f(loc, x, y)
182
183         def _render_pass(self, shaderPass, fromTexture, finalW, finalH):
184
185                 # How big should the output of this pass be?
186                 outputW, outputH = shaderPass.calculateFramebufferSize(
187                                 fromTexture.width, fromTexture.height, finalW, finalH,
188                         )
189
190                 # If this pass doesn't specify a size, default to the input.
191                 if outputW is None: outputW = fromTexture.width
192                 if outputH is None: outputH = fromTexture.height
193
194                 # Make a texture exactly that big.
195                 toTexture = Texture(outputW, outputH)
196
197                 # Configure OpenGL to render to the texture.
198                 framebufferobjects.glBindFramebuffer(
199                                 framebufferobjects.GL_FRAMEBUFFER,
200                                 self.framebufferID,
201                         )
202                 framebufferobjects.glFramebufferTexture2D(
203                                 framebufferobjects.GL_FRAMEBUFFER,
204                                 framebufferobjects.GL_COLOR_ATTACHMENT0,
205                                 GL_TEXTURE_2D,
206                                 toTexture.textureID,
207                                 0,
208                         )
209
210                 # Are we all set?
211                 framebufferobjects.glCheckFramebufferStatus(
212                                 framebufferobjects.GL_FRAMEBUFFER,
213                         )
214
215                 # Let's do our rendering pass.
216                 glUseProgram(shaderPass.programID)
217
218                 self._setUniform(shaderPass.programID, 'rubyInputSize',
219                                 fromTexture.width, fromTexture.height)
220                 self._setUniform(shaderPass.programID, 'rubyOutputSize',
221                                 toTexture.width, toTexture.height)
222                 self._setUniform(shaderPass.programID, 'rubyTextureSize',
223                                 fromTexture.width, fromTexture.height)
224                 loc = glGetUniformLocation(shaderPass.programID, 'rubyFrameCount')
225                 if loc >= 0:
226                         glUniform1i(loc, self.framecount)
227
228                 glBindTexture(GL_TEXTURE_2D, fromTexture.textureID)
229
230                 if shaderPass.filterMethod == shaderreader.ATTR_FILTER_LINEAR:
231                         glTexParameter(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
232                         glTexParameter(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
233                 else:
234                         glTexParameter(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
235                         glTexParameter(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
236
237                 glViewport(0, 0, int(outputW), int(outputH))
238                 glMatrixMode(GL_PROJECTION)
239                 glLoadIdentity()
240                 # A framebufferobject will always have the underlying texture's (0,0)
241                 # at the lower-left, so align our coordinate system to match.
242                 glOrtho(0, outputW, 0, outputH, -1, 1)
243
244                 glClear(GL_COLOR_BUFFER_BIT)
245
246                 glBegin(GL_QUADS)                   # Start drawing a 4 sided polygon
247
248                 glTexCoord2f(0,   0  ); glVertex(0,       0,     )
249                 glTexCoord2f(1.0, 0  ); glVertex(outputW, 0,     )
250                 glTexCoord2f(1.0, 1.0); glVertex(outputW, outputH)
251                 glTexCoord2f(0,   1.0); glVertex(0,       outputH)
252
253                 glEnd()                             # We are done with the polygon
254
255                 # Detach all the things we set up.
256                 glUseProgram(0)
257                 framebufferobjects.glBindFramebuffer(
258                                 framebufferobjects.GL_FRAMEBUFFER,
259                                 0,
260                         )
261
262                 return toTexture
263
264         def _draw_scene(self):
265                 if None in (self.inputTexture, self.windowW, self.windowH):
266                         return
267
268                 finalW, finalH = self.scaleMethod(
269                                 self.windowW, self.windowH,
270                                 self.inputTexture.width, self.inputTexture.height,
271                         )
272
273                 finalX = (self.windowW - finalW) / 2
274                 finalY = (self.windowH - finalH) / 2
275
276                 fromTexture = self.inputTexture
277                 for shaderPass in self.shaderPasses:
278                         toTexture = self._render_pass(shaderPass, fromTexture,
279                                         finalW, finalH)
280                         fromTexture = toTexture
281
282
283                 glViewport(0, 0, self.windowW, self.windowH)
284                 glMatrixMode(GL_PROJECTION)
285                 glLoadIdentity()
286                 # After all the shader passes, the (0,0) corner of the resulting
287                 # texture is supposed to appear at the top-left, so let's set our
288                 # coordinate system appropriately.
289                 glOrtho(0, self.windowW, self.windowH, 0, -1, 1)
290
291                 glClear(GL_COLOR_BUFFER_BIT)
292
293                 glBindTexture(GL_TEXTURE_2D, toTexture.textureID)
294                 glTexParameter(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
295                 glTexParameter(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
296
297                 glBegin(GL_QUADS)                   # Start drawing a 4 sided polygon
298
299                 glTexCoord(0,   0  ); glVertex(finalX,        finalY,      )
300                 glTexCoord(1.0, 0  ); glVertex(finalX+finalW, finalY,      )
301                 glTexCoord(1.0, 1.0); glVertex(finalX+finalW, finalY+finalH)
302                 glTexCoord(0,   1.0); glVertex(finalX,        finalY+finalH)
303
304                 glEnd()                             # We are done with the polygon
305
306                 glFlush()
307
308         def _handle_key(self, key, x, y):
309                 if key == '\x1b': # Escape
310                         sys.exit(0)
311
312         def _handle_resize(self, width, height):
313                 self.windowW = max(width, 1)
314                 self.windowH = max(height, 1)
315
316         def _handle_idle(self):
317                 now = time.clock()
318                 if now > self.start + 1:
319                         fps = self.framecount / (now - self.start)
320                         sys.stdout.write("FPS: %0.1f\r" % fps)
321                         sys.stdout.flush()
322                         self.framecount = 0
323                         self.start = now
324
325                 glutPostWindowRedisplay(self.window)
326                 self.framecount += 1
327
328 if __name__ == "__main__":
329         argv = glutInit(sys.argv)
330
331         if len(argv) != 2:
332                 print >> sys.stderr, "Usage: %s /path/to/shaderfile" % (argv[0],)
333                 sys.exit(1)
334
335         shaderFile = argv[1]
336         textures = load_textures()
337
338         if not textures:
339                 print >> sys.stderr, "Can't find any textures to display."
340                 sys.exit(1)
341
342         demo = GLUTDemo(shaderFile, textures)
343
344         glutMainLoop()