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