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