-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathvisualize.py
executable file
·385 lines (333 loc) · 13.7 KB
/
visualize.py
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
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
#!/usr/bin/python3
# Copyright (c) 2014 Wladimir J. van der Laan
# Distributed under the MIT/X11 software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
'''
Show Homeworld 2 backgrounds using OpenGL-based visualization.
'''
from OpenGL.extensions import alternate
from OpenGL.GL import *
from OpenGL.GL import shaders
from OpenGL.GL.NV.primitive_restart import *
import ctypes
import glfw
import math
import numpy
import os
import random
import time
from math3d import perspective_matrix
from glfw_platform import GLFWPlatform
from parse_bg import parse_bg, PRIM_TRIANGLE_STRIP, PRIM_TRIANGLES
from transformations import Arcball, quaternion_slerp, random_quaternion, quaternion_multiply, quaternion_about_axis
window = 0
w_width, w_height = 500, 400
f_width, f_height = 500, 400
wireframe_mode = False
quit_flag = False
slow_flag = False
rotation_speed = 1.0
arcball = Arcball()
arcball.active = False
animate = None # autospin
fovy = 45 # field of vision in y - I don't know what original homeworld uses
cur_time = nextframe_time = None
# Options for primitive restart
PRIMITIVE_RESTART_NONE = 0
PRIMITIVE_RESTART_CORE = 1
PRIMITIVE_RESTART_NV = 2
# Primitive restart type supported (filled in in probe_extensions)
primitive_restart_mode = PRIMITIVE_RESTART_NONE
gl_types = {
PRIM_TRIANGLES: GL_TRIANGLES,
PRIM_TRIANGLE_STRIP: GL_TRIANGLE_STRIP
}
PRIMITIVE_RESTART_INDEX = 65535
# extension alternates for GL <2.0
from OpenGL.GL.ARB.vertex_shader import *
from OpenGL.GL.ARB.vertex_buffer_object import *
from OpenGL.GL.ARB.vertex_program import *
glGetAttribLocation = alternate('glGetAttribLocation', glGetAttribLocation, glGetAttribLocationARB)
glEnableVertexAttribArray = alternate('glEnableVertexAttribArray', glEnableVertexAttribArray, glEnableVertexAttribArrayARB)
glDisableVertexAttribArray = alternate('glDisableVertexAttribArray', glDisableVertexAttribArray, glDisableVertexAttribArrayARB)
glVertexAttribPointer = alternate('glVertexAttribPointer', glVertexAttribPointer, glVertexAttribPointerARB)
glGenBuffers = alternate('glGenBuffers', glGenBuffers, glGenBuffersARB)
glBindBuffer = alternate('glBindBuffer', glBindBuffer, glBindBufferARB)
glBufferData = alternate('glBufferData', glBufferData, glBufferDataARB)
# GLFW window hints for wayland
# This needs https://github.com/glfw/glfw/pull/2061
GLFW_WAYLAND_SHELL_LAYER = 0x00026001
ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND = 0
def force_rerender():
'''
Force a re-render on next possible opportunity.
'''
global cur_time, nextframe_time
nextframe_time = cur_time
def error_callback(code, message):
print(f'{code} {message.decode()}')
def window_size_callback(window, w, h):
global w_width, w_height
w_width = w
w_height = h
arcball.place([w/2, h/2], h/2)
def framebuffer_size_callback(window, w, h):
global f_width, f_height
f_width = w
f_height = h
force_rerender()
def draw():
glViewport(0, 0, f_width, f_height)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
# set up matrices
glMatrixMode(GL_PROJECTION)
glLoadMatrixf(perspective_matrix(fovy, f_width/f_height, 1.0, 100.0))
glMatrixMode(GL_MODELVIEW)
glLoadMatrixf(arcball.matrix().T)
# rendering time
if wireframe_mode:
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
else:
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
if primitive_restart_mode == PRIMITIVE_RESTART_CORE:
glEnable(GL_PRIMITIVE_RESTART)
glPrimitiveRestartIndex(PRIMITIVE_RESTART_INDEX)
elif primitive_restart_mode == PRIMITIVE_RESTART_NV:
glEnableClientState(GL_PRIMITIVE_RESTART_NV)
glPrimitiveRestartIndexNV(PRIMITIVE_RESTART_INDEX)
shaders.glUseProgram(background_shader)
glBindBuffer(GL_ARRAY_BUFFER, vbo)
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo)
glEnableVertexAttribArray(vertex_loc)
glEnableVertexAttribArray(color_loc)
prims = 0
for numverts,vertsize,vertdata_offset,facelists in nbgdata:
glVertexAttribPointer(vertex_loc, 4, GL_FLOAT, False, vertsize, ctypes.c_void_p(vertdata_offset))
glVertexAttribPointer(color_loc, 4, GL_BYTE, True, vertsize, ctypes.c_void_p(vertdata_offset+16))
for typ, count, facedata_offset in facelists:
glDrawElements(gl_types[typ], count, GL_UNSIGNED_SHORT, ctypes.c_void_p(facedata_offset))
glDisableVertexAttribArray(vertex_loc)
glDisableVertexAttribArray(color_loc)
glBindBuffer(GL_ARRAY_BUFFER, 0)
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)
shaders.glUseProgram(0)
if primitive_restart_mode == PRIMITIVE_RESTART_CORE:
glDisable(GL_PRIMITIVE_RESTART)
elif primitive_restart_mode == PRIMITIVE_RESTART_NV:
glDisableClientState(GL_PRIMITIVE_RESTART_NV)
def advance_time(deltatime):
global animate
if animate is not None:
# Continue in auto-spin if arcball not active
animate[2] += deltatime * 20.0
arcball._qnow = quaternion_slerp(animate[0], animate[1], animate[2], False)
def key_callback(window, key, scancode, action, mods):
'''
Keyboard: w for wireframe mode
'''
global wireframe_mode, slow_flag, quit_flag
if action == glfw.PRESS and mods == 0:
if key == glfw.KEY_W:
wireframe_mode = not wireframe_mode
if key == glfw.KEY_S:
slow_flag = not slow_flag
elif key == glfw.KEY_ESCAPE:
quit_flag = True
def mouse_button_callback(window, button, action, mods):
global animate
(x, y) = glfw.get_cursor_pos(window)
if button == 0 and mods == 0:
arcball.active = (action == glfw.PRESS)
if arcball.active:
arcball.down([x,y])
animate = None
elif numpy.allclose(arcball._qpre, arcball._qnow): # effectively no animation, save CPU cycles
animate = None
else:
animate = [arcball._qpre, arcball._qnow, 1.0]
force_rerender()
def cursor_pos_callback(window, x, y):
if arcball.active:
arcball.drag([x,y])
force_rerender()
def create_shaders():
global background_shader, vertex_loc, color_loc
VERTEX_SHADER = shaders.compileShader(b"""
#version 120
attribute vec4 inVertex;
attribute vec4 inColor;
void main()
{
gl_Position = gl_ModelViewProjectionMatrix * inVertex;
gl_FrontColor = inColor.abgr;
}
""", GL_VERTEX_SHADER)
FRAGMENT_SHADER = shaders.compileShader(b"""
#version 120
void main()
{
gl_FragColor = gl_Color;
}""", GL_FRAGMENT_SHADER)
background_shader = shaders.compileProgram(VERTEX_SHADER,FRAGMENT_SHADER)
vertex_loc = glGetAttribLocation(background_shader, b"inVertex")
color_loc = glGetAttribLocation(background_shader, b"inColor")
def create_vbos(bgdata):
global ibo,vbo,nbgdata
# Build vertex and index buffers and new bgdata structure
# that has pointers into the vertex and index buffers instead of
# data
allvertdata = []
allfacedata = []
vertdata_ptr = 0
facedata_ptr = 0
nbgdata = []
for numverts,vertsize,vertdata,facelists in bgdata:
vertdata_offset = vertdata_ptr
allvertdata.append(vertdata)
vertdata_ptr += len(vertdata)
nfacelists = []
for typ, count, facedata in facelists:
facedata_offset = facedata_ptr
allfacedata.append(facedata)
facedata_ptr += len(facedata)
nfacelists.append((typ, count, facedata_offset))
nbgdata.append((numverts, vertsize, vertdata_offset, nfacelists))
allvertdata = b''.join(allvertdata)
allfacedata = b''.join(allfacedata)
vbo = glGenBuffers(1)
glBindBuffer(GL_ARRAY_BUFFER, vbo)
glBufferData(GL_ARRAY_BUFFER, len(allvertdata), allvertdata, GL_STATIC_DRAW)
glBindBuffer(GL_ARRAY_BUFFER, 0)
ibo = glGenBuffers(1)
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo)
glBufferData(GL_ELEMENT_ARRAY_BUFFER, len(allfacedata), allfacedata, GL_STATIC_DRAW)
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)
def concatenate_primitives(bgdata):
import struct
PRIMITIVE_RESTART = struct.pack('<H',65535)
bgdata_new = []
for numverts,vertsize,vertdata,facelists in bgdata:
# concatenate triangles, as well as triangle strips, into different buffers
triangles = []
triangle_strip = []
for typ, count, facedata in facelists:
assert(len(facedata) == 2*count)
if typ == PRIM_TRIANGLE_STRIP:
if triangle_strip:
if primitive_restart_mode == PRIMITIVE_RESTART_NONE:
# create two degenerate triangles in between
triangle_strip.append(
triangle_strip[-1][-2:] +
facedata[:2])
else:
triangle_strip.append(PRIMITIVE_RESTART)
triangle_strip.append(facedata)
elif typ == PRIM_TRIANGLES:
triangles.append(facedata)
else:
raise ValueError(f'Unknown primitive type {typ}')
facelists_new = []
if triangle_strip:
joined = b''.join(triangle_strip)
facelists_new.append((PRIM_TRIANGLE_STRIP, len(joined)//2, joined))
if triangles:
joined = b''.join(triangles)
facelists_new.append((PRIM_TRIANGLES, len(joined)//2, joined))
bgdata_new.append((numverts,vertsize,vertdata,facelists_new))
return bgdata_new
def probe_extensions():
global primitive_restart_mode
primitive_restart_mode = PRIMITIVE_RESTART_NONE
if glInitGl31VERSION() and glPrimitiveRestartIndex: # 3.1+
primitive_restart_mode = PRIMITIVE_RESTART_CORE
elif glPrimitiveRestartNV:
primitive_restart_mode = PRIMITIVE_RESTART_NV
else:
print("Warning: Primitive restart not supported, falling back to slow path")
def parse_arguments():
import argparse
parser = argparse.ArgumentParser(description='Homeworld 2 background viewer')
parser.add_argument('filename', metavar='FILENAME.HOD', help='Name of background mesh')
parser.add_argument('--randomize', action='store_true', help='Randomize initial orientation and movement')
parser.add_argument('--slow', action='store_true', help='Start in slow mode')
parser.add_argument('--background', action='store_true', help='Render to desktop background')
return parser.parse_args()
def main():
global animate, quit_flag, slow_flag, cur_time, nextframe_time
args = parse_arguments()
# fetch data
bgdata = parse_bg(args.filename)
# initialization
if not glfw.init():
return
glfw.set_error_callback(error_callback)
# Set as desktop background if requested (only on wayland, for now)
if args.background:
# This doesn't work for GNOME Wayland as it doesn't support the layer shell
# extension. As far as I know, there's no way to do it for GNOME Wayland at all.
# Through XWayland there's the _NET_WM_WINDOW_TYPE hint that can be set to
# _NET_WM_WINDOW_TYPE_DESKTOP, GNOME's display manager "mutter" maps
# this to Meta window type META_WINDOW_DESKTOP but from the Wayland
# side there seems to be no way to set this.
glfw.window_hint(GLFW_WAYLAND_SHELL_LAYER, ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND)
window = glfw.create_window(w_width, w_height, "homeworld2 background: " + os.path.basename(args.filename), None, None)
if not window:
print('Could not create GLFW window')
glfw.terminate()
return
glfw.set_window_size_callback(window, window_size_callback)
glfw.set_framebuffer_size_callback(window, framebuffer_size_callback)
glfw.set_key_callback(window, key_callback)
glfw.set_mouse_button_callback(window, mouse_button_callback)
glfw.set_cursor_pos_callback(window, cursor_pos_callback)
glfw.make_context_current(window)
# Install GLFW platform into PyOpenGL. Installing it here only works
# because of PyOpenGL's late binding for client API functions.
import OpenGL
platform = GLFWPlatform()
platform.install(vars(OpenGL.platform))
probe_extensions()
print(f"Primitive restart mode: {['NONE','CORE','NV'][primitive_restart_mode]}")
create_shaders()
bgdata = concatenate_primitives(bgdata)
create_vbos(bgdata)
window_size_callback(window, *glfw.get_window_size(window))
framebuffer_size_callback(window, *glfw.get_framebuffer_size(window))
last_time = glfw.get_time()
nextframe_time = 0
# random initial rotation, random rotation angle
if args.randomize:
begin = random_quaternion()
ang = random.uniform(0.0, 2.0 * math.pi)
rot = quaternion_about_axis(0.005, [math.sin(ang), math.cos(ang), 0.0])
animate = [begin, quaternion_multiply(rot, begin), 0.0]
advance_time(0.0)
# start in slow mode if requested
slow_flag = args.slow
while not glfw.window_should_close(window) and not quit_flag:
cur_time = glfw.get_time()
if slow_flag:
timescale = 0.005
else:
timescale = 1.0
advance_time((cur_time - last_time) * timescale)
if nextframe_time is not None and cur_time >= nextframe_time:
draw()
glfw.swap_buffers(window)
if animate is not None:
if slow_flag: # if slow, only render once per second
nextframe_time = cur_time + 1.0
else:
nextframe_time = cur_time
else:
nextframe_time = None
last_time = cur_time
if nextframe_time is not None:
if nextframe_time > cur_time:
glfw.wait_events_timeout(nextframe_time - cur_time)
else:
glfw.poll_events()
else:
glfw.wait_events()
if __name__ == '__main__':
main()