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