-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrender.py
More file actions
324 lines (266 loc) · 11.3 KB
/
Copy pathrender.py
File metadata and controls
324 lines (266 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
# Copyright © 2025 Brandon Namgoong.
# Licensed under the GNU General Public License v3.0.
# Made for ICS3U1.
"""
Module containing all functions regarding rendering using OpenGL,
and creating and interacting with offsite, hidden OpenGL contexts via glfw.
Some code has been adapted from kickstart ideas from GPT 4o. (OpenGL is horrifying for beginners)
"""
from OpenGL.GL import *
from OpenGL.GLU import *
import numpy as np
import glfw
#-----------------------------------------------------------------------------
# globals
#-----------------------------------------------------------------------------
# screen dimensions
__screenWidth = 0 # placeholder
__screenHeight = 0 # placeholder
# all window hints needed during init except hiding
__windowHints: dict = {
glfw.CONTEXT_VERSION_MAJOR: 2, # OpenGL version 2.1 because 3.0+ overcomplicates things and only 3.2 or 2.1 available on macOS
glfw.CONTEXT_VERSION_MINOR: 1,
glfw.RED_BITS: 8,
glfw.GREEN_BITS: 8,
glfw.BLUE_BITS: 8,
glfw.ALPHA_BITS: 8
}
__boxScale: float = 1 / 120 # box is approx 1/60 of screen dimensions
# OpenGL capabilities to be enabled
__glCapabilities: list = [
GL_NORMALIZE, GL_CULL_FACE, GL_DEPTH_TEST, # normalize vectors, do face culling, enable depth clipping
GL_LIGHTING, GL_COLOR_MATERIAL, # enable lighting and colored materials
GL_LIGHT0, GL_LIGHT1 # enable lights 0 and 1
]
# lighting
__light0Pos: list[float] = [-10.0, -10.0, 0.0, 1.0] # behind left light for shapes
__light1Pos: list[float] = [0.0, 0.0, 10.0, 0.0] # direct light towards top of shapes
__lightDiffuse: list[float] = lambda strength : [strength, strength, strength, 1.0]
#-----------------------------------------------------------------------------
# objects
#-----------------------------------------------------------------------------
__window: glfw._GLFWwindow = None # placeholder for when window is created
#-----------------------------------------------------------------------------
# private functions
#-----------------------------------------------------------------------------
# Calculates and returns a matrix representing the normal vector of the specified face
def __getNormal(face: tuple[int, ...], vertexArray: list[tuple[float, float, float]]) -> tuple[float]:
netNormal = np.array([0.0, 0.0, 0.0]) # start with empty vector
# run through and summate every pair
for i in range(len(face)):
netNormal += np.cross(
np.array(vertexArray[face[(i + 1) % len(face)]]) - np.array(vertexArray[face[i]]), # current vector line
np.array(vertexArray[face[(i + 2) % len(face)]]) - np.array(vertexArray[face[(i + 1) % len(face)]]) # vector line ahead
)
return tuple((netNormal / np.linalg.norm(netNormal)).tolist())
# Renders the given concave face using tessellation as a continous, filled polygon
def __tessellate(face: tuple[int, ...], vertexArray: list[tuple[float, float, float]]):
# setup
tess: GLUtesselator = gluNewTess()
gluTessCallback(tess, GLU_TESS_BEGIN, glBegin)
gluTessCallback(tess, GLU_TESS_END, glEnd)
gluTessCallback(tess, GLU_TESS_VERTEX, glVertex3dv)
gluTessCallback(tess, GLU_TESS_COMBINE, lambda coordinates : coordinates)
gluTessBeginPolygon(tess, None)
gluTessBeginContour(tess)
# tessellate
for vertexIndex in face:
coord = (GLdouble * 3)(*vertexArray[vertexIndex]) # "convert to C-style arrays required by GLU" apparently
gluTessVertex(tess, coord, coord)
# end
gluTessEndContour(tess)
gluTessEndPolygon(tess)
gluDeleteTess(tess)
#-----------------------------------------------------------------------------
# public functions
#-----------------------------------------------------------------------------
def init(
width: int, height: int,
offsetY: int = 0,
isHidden: bool = True
):
"""
Initializes an offsite OpenGL context with the specified window width and height.
`offsetY` is an optional parameter that moves the entire viewport +Y units.
By default `isHidden` is set to `True`, and should be kept that way.
If the window must be visible, set `isHidden` to `False`.
Should be called just once to initialize glfw and OpenGL.
"""
# first initialize glfw
glfw.init()
# configure window settings
if isHidden:
glfw.window_hint(glfw.VISIBLE, glfw.FALSE) # hide window
for key in __windowHints.keys():
glfw.window_hint(key, __windowHints[key])
# create and set window
global __window ; __window = glfw.create_window(width, height, "Hidden OpenGL context", None, None)
glfw.make_context_current(__window) # ensure correct OpenGL context
# initialize OpenGL
glMatrixMode(GL_PROJECTION) # load projection matrix stack
glLoadIdentity() # flush projection matrix
scaledWidth = width * __boxScale
scaledHeight = height * __boxScale
glOrtho( # set orthographic projection to box
-scaledWidth, scaledWidth,
-scaledHeight + offsetY, scaledHeight + offsetY,
-50, 50 # arbitrary depth
)
glMatrixMode(GL_MODELVIEW) # load modelview matrix stack
glLoadIdentity() # flush modelview matrix
# enable capabilities
for cap in __glCapabilities: glEnable(cap)
glCullFace(GL_BACK) # culling set to back-face
glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE)
# store screen dimensions
global __screenWidth, __screenHeight ; __screenWidth, __screenHeight = width, height
def setupScene(xtheta: float = -45.0):
"""
Sets up the scene with a worldview rotation and lights.
Must be called periodically per loop pre-rendering, after a `flush()`.
`xtheta` specifies the angle in degrees about the origin after translation.
By default it is 45º clockwise for isometric projection.
"""
# set a worldview tilt
glRotated(xtheta, 1, 0, 0)
# setup lights
glLightfv(GL_LIGHT0, GL_POSITION, __light0Pos) # LIGHT0
glLightfv(GL_LIGHT0, GL_DIFFUSE, __lightDiffuse(0.5))
glLightfv(GL_LIGHT1, GL_POSITION, __light1Pos) # LIGHT1
glLightfv(GL_LIGHT1, GL_DIFFUSE, __lightDiffuse(1.0))
def drawWireframe(
vertexArray: list[tuple[float, float, float]], lineArray: list[tuple[int, int]],
vertexCol: tuple[int, int, int], lineCol: tuple[int, int, int],
vertexDiameter: int = 8, lineWidth: int = 2,
dx: float = 0.0, dy: float = 0.0, dz: float = 0.0,
theta: float = 0.0
):
"""
Renders the given set of vertices as a wireframe of vertices and lines.
`vertexArray` specifies the set of vertices and `lineArray` specifies the set of lines.
`vertexCol` and `lineCol` values range from `0` to `255`.
The default vertex diameter is 8 pixels and the default line width is 2 pixels.
Translate using projection units `dx` in +X, `dy` in +Y, and `dz` in +Z.
`theta` intrinsically rotates in degrees about z-axis.
"""
glPushMatrix() # saves current stack
# apply transformations
glTranslated(dx, dy, dz)
glRotated(theta, 0, 0, 1)
# line rendering FIRST
glColor3d(
lineCol[0] / 255, lineCol[1] / 255, lineCol[2] / 255 # normalize to 0 - 1 range
)
glLineWidth(lineWidth)
glBegin(GL_LINES)
for line in lineArray: # draw each line
for vertexIndex in line:
glVertex3dv(vertexArray[vertexIndex])
glEnd()
# vertex rendering
glColor3d(
vertexCol[0] / 255, vertexCol[1] / 255, vertexCol[2] / 255
)
glPointSize(vertexDiameter)
glBegin(GL_POINTS)
for vertex in vertexArray: # draw each vertex
glVertex3dv(vertex)
glEnd()
glPopMatrix() # restores previous stack
def drawPolygon(
vertexArray: list[tuple[float, float, float]],
baseArray: list[tuple[int, ...]], quadArray: list[tuple[int, int, int, int]],
isConvex: bool,
avgColor: tuple[int, int, int],
dx: float = 0.0, dy: float = 0.0, dz: float = 0.0,
theta: float = 0.0
):
"""
Renders the given set of faces as a continuous, filled polygon.
`vertexArray` specifies the set of vertices,
`baseArray` specifies the set of base faces, and `quadArray` specifies the set of quad faces.
This rendering involves simple shading with respect to each face.
`isConvex` specifies whether the base polygon is convex or not.
`avgColor` specifies the base color that shading will use to make certain faces darker or lighter.
Its values range from `0` to `255`.
Translate using projection units `dx` in +X, `dy` in +Y, and `dz` in +Z.
`theta` intrinsically rotates in degrees about z-axis.
"""
glPushMatrix() # saves current stack
# apply transformations
glTranslated(dx, dy, dz)
glRotated(theta, 0, 0, 1)
# set color
glColor3d(
avgColor[0] / 255, avgColor[1] / 255, avgColor[2] / 255
)
# render bases
if isConvex:
for face in baseArray:
glBegin(GL_POLYGON)
glNormal3dv(
__getNormal(face, vertexArray) # define the normal for each face
)
for vertexIndex in face:
glVertex3dv(vertexArray[vertexIndex])
glEnd()
else: # tessellation time
for face in baseArray:
glNormal3dv(
__getNormal(face, vertexArray)
)
__tessellate(face, vertexArray)
# render quads
glBegin(GL_QUADS)
for face in quadArray:
glNormal3dv(
__getNormal(face, vertexArray)
)
for vertexIndex in face:
glVertex3dv(vertexArray[vertexIndex])
glEnd()
glPopMatrix() # restores previous stack
def getMatrix(
dx: float, dy: float, dz: float,
theta: float
) -> list:
"Calculates and returns the modelview matrix for the given transformations."
glPushMatrix() # saves current stack
# apply transformations
glTranslated(dx, dy, dz)
glRotated(theta, 0, 0, 1)
mat = glGetDoublev(GL_MODELVIEW_MATRIX) # get matrix
glPopMatrix() # restores previous stack
return mat
def toBytes():
"Returns the OpenGL thread frame matrix as raw byte data."
glPixelStorei(GL_PACK_ALIGNMENT, 1) # set pixel storage mode
glReadBuffer(GL_FRONT)
glFinish()
# get pixel array
return np.frombuffer(
buffer = glReadPixels(0, 0, __screenWidth, __screenHeight, GL_RGBA, GL_UNSIGNED_BYTE),
dtype = np.uint8
).reshape( # reshape into proper shape
(__screenHeight, __screenWidth, 4)
).tobytes()
def reset():
"""
Sets transparent background and clears OpenGL buffers and matrix stack.
Must be called periodically at the start of loops.
"""
glfw.make_context_current(__window) # ensure correct OpenGL context
glViewport(0, 0, __screenWidth, __screenHeight)
glClearColor(0.0, 0.0, 0.0, 0.0)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT) # clear buffers
glLoadIdentity() # load identity matrix
def finish():
"""
Signals the OpenGL context that rendering of the current frame (or iteration of loop) is complete.
Must be called periodically in loops after all render commands.
"""
glfw.swap_buffers(__window) # rotate back buffer to front buffer
def end():
"Ends the current glfw OpenGL context."
glfw.destroy_window(__window)
glfw.terminate()