Created
November 20, 2025 05:12
-
-
Save EncodeTheCode/dad9abfc54b51b713962dc496a7be24c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # glb_player_glfw.py | |
| import time, ctypes, math | |
| import numpy as np | |
| import glfw | |
| from OpenGL.GL import * | |
| from OpenGL.GL.shaders import compileProgram, compileShader | |
| import pyassimp | |
| # ---------------- math helpers ---------------- | |
| def identity4(): return np.eye(4, dtype=np.float32) | |
| def mat4_from_list(l): return np.array(l, dtype=np.float32).reshape((4,4)).T | |
| def translate_matrix(v): | |
| m = identity4(); m[:3,3] = np.array(v,dtype=np.float32); return m | |
| def scale_matrix(s): | |
| m = identity4(); m[0,0],m[1,1],m[2,2] = float(s[0]),float(s[1]),float(s[2]); return m | |
| def quat_to_mat4(q): | |
| x,y,z,w = float(q[0]),float(q[1]),float(q[2]),float(q[3]) | |
| n = x*x+y*y+z*z+w*w | |
| if n < 1e-8: return identity4() | |
| s = 2.0 / n | |
| xx,yy,zz = x*x*s, y*y*s, z*z*s | |
| xy, xz, yz = x*y*s, x*z*s, y*z*s | |
| wx, wy, wz = w*x*s, w*y*s, w*z*s | |
| m = np.array([ | |
| [1.0-(yy+zz), xy - wz, xz + wy, 0.0], | |
| [xy + wz, 1.0-(xx+zz), yz - wx, 0.0], | |
| [xz - wy, yz + wx, 1.0-(xx+yy),0.0], | |
| [0.0, 0.0, 0.0, 1.0] | |
| ], dtype=np.float32) | |
| return m | |
| def slerp(q0, q1, t): | |
| q0 = np.array(q0, dtype=np.float64) | |
| q1 = np.array(q1, dtype=np.float64) | |
| dot = np.dot(q0, q1) | |
| if dot < 0.0: | |
| q1 = -q1; dot = -dot | |
| if dot > 0.9995: | |
| res = q0 + t*(q1 - q0) | |
| res /= np.linalg.norm(res); return res.astype(np.float32) | |
| theta_0 = math.acos(max(-1.0, min(1.0, dot))) | |
| theta = theta_0 * t | |
| q2 = q1 - q0 * dot | |
| q2 /= np.linalg.norm(q2) | |
| res = q0*math.cos(theta) + q2*math.sin(theta) | |
| return res.astype(np.float32) | |
| def lerp(a,b,t): return (1.0-t)*np.array(a,dtype=np.float32) + t*np.array(b,dtype=np.float32) | |
| # ---------------- GLSL (GPU skinning) ---------------- | |
| VERT = """ | |
| #version 330 core | |
| layout(location=0) in vec3 in_pos; | |
| layout(location=1) in vec3 in_nrm; | |
| layout(location=2) in ivec4 in_bids; | |
| layout(location=3) in vec4 in_wts; | |
| uniform mat4 u_proj; | |
| uniform mat4 u_view; | |
| uniform mat4 u_model; | |
| uniform mat4 u_bones[120]; // increase if needed | |
| out vec3 v_n; | |
| void main(){ | |
| mat4 skin = mat4(0.0); | |
| for(int i=0;i<4;i++){ | |
| int id = in_bids[i]; | |
| float w = in_wts[i]; | |
| if(id>=0 && w>0.0) skin += u_bones[id] * w; | |
| } | |
| vec4 p = skin * vec4(in_pos,1.0); | |
| vec3 n = mat3(skin) * in_nrm; | |
| gl_Position = u_proj * u_view * u_model * p; | |
| v_n = normalize(n); | |
| } | |
| """ | |
| FRAG = """ | |
| #version 330 core | |
| in vec3 v_n; | |
| out vec4 o; | |
| void main(){ | |
| float l = max(dot(normalize(v_n), normalize(vec3(0.4,0.6,1.0))), 0.0); | |
| vec3 col = vec3(0.2) + vec3(0.8)*l; | |
| o = vec4(col,1.0); | |
| } | |
| """ | |
| # ---------------- GLB Player class ---------------- | |
| class GLBPlayer: | |
| def __init__(self, glb_path, max_bones=120): | |
| self.scene = pyassimp.load(glb_path) | |
| self.max_bones = max_bones | |
| self._collect_nodes() | |
| self._collect_bones() | |
| self._build_mesh_vbos() | |
| self.program = compileProgram(compileShader(VERT, GL_VERTEX_SHADER), | |
| compileShader(FRAG, GL_FRAGMENT_SHADER)) | |
| self.current = None | |
| self.prev = None | |
| self.anim_time = 0.0 | |
| self.blend_time = 0.0 | |
| self.blend_len = 0.15 | |
| self.bone_matrices = np.array([identity4()]*self.max_bones, dtype=np.float32) | |
| def _collect_nodes(self): | |
| self.nodes = {} | |
| def walk(n): | |
| self.nodes[n.name] = n | |
| for c in n.children: walk(c) | |
| walk(self.scene.rootnode) | |
| def _collect_bones(self): | |
| self.bone_names = [] | |
| self.bone_index = {} | |
| inv = [] | |
| for mesh in self.scene.meshes: | |
| if not getattr(mesh, 'bones', None): continue | |
| for b in mesh.bones: | |
| name = b.name | |
| if name not in self.bone_index: | |
| idx = len(self.bone_names) | |
| self.bone_index[name] = idx | |
| self.bone_names.append(name) | |
| inv.append(mat4_from_list(b.offsetmatrix)) | |
| # pad | |
| while len(inv) < self.max_bones: | |
| inv.append(identity4()) | |
| self.inv_bind = np.stack(inv).astype(np.float32) | |
| # animations | |
| self.anims = [] | |
| for a in getattr(self.scene, 'animations', []): | |
| channels = {} | |
| for ch in a.channels: | |
| channels[ch.nodename] = ch | |
| self.anims.append({'name': a.name or str(len(self.anims)), | |
| 'duration': a.duration if a.duration>0 else 1.0, | |
| 'tps': a.tickspersecond if a.tickspersecond>0 else 25.0, | |
| 'channels': channels}) | |
| if not self.anims: | |
| self.anims.append({'name':'idle','duration':1.0,'tps':25.0,'channels':{}}) | |
| def _build_mesh_vbos(self): | |
| self.meshes = [] | |
| for mesh in self.scene.meshes: | |
| v = np.array(mesh.vertices, dtype=np.float32) | |
| n = np.array(mesh.normals if mesh.normals is not None else [[0,0,1]]*len(v), dtype=np.float32) | |
| count = len(v) | |
| bids = np.full((count,4), -1, dtype=np.int32) | |
| wts = np.zeros((count,4), dtype=np.float32) | |
| if mesh.bones: | |
| for b in mesh.bones: | |
| bid = self.bone_index.get(b.name, -1) | |
| for w in b.weights: | |
| vi, wt = w.vertexid, w.weight | |
| for slot in range(4): | |
| if wts[vi,slot] == 0.0: | |
| bids[vi,slot] = bid | |
| wts[vi,slot] = wt | |
| break | |
| idx = np.array([i for f in mesh.faces for i in f], dtype=np.uint32) | |
| vao = glGenVertexArrays(1); glBindVertexArray(vao) | |
| vbo = glGenBuffers(1); glBindBuffer(GL_ARRAY_BUFFER, vbo) | |
| inter = np.hstack([v, n]).astype(np.float32) | |
| glBufferData(GL_ARRAY_BUFFER, inter.nbytes, inter, GL_STATIC_DRAW) | |
| stride = 6*4 | |
| glEnableVertexAttribArray(0); glVertexAttribPointer(0,3,GL_FLOAT,False,stride,ctypes.c_void_p(0)) | |
| glEnableVertexAttribArray(1); glVertexAttribPointer(1,3,GL_FLOAT,False,stride,ctypes.c_void_p(12)) | |
| bid_buf = glGenBuffers(1); glBindBuffer(GL_ARRAY_BUFFER, bid_buf) | |
| glBufferData(GL_ARRAY_BUFFER, bids.nbytes, bids, GL_STATIC_DRAW) | |
| glEnableVertexAttribArray(2); glVertexAttribIPointer(2,4,GL_INT,4*4,ctypes.c_void_p(0)) | |
| w_buf = glGenBuffers(1); glBindBuffer(GL_ARRAY_BUFFER, w_buf) | |
| glBufferData(GL_ARRAY_BUFFER, wts.nbytes, wts, GL_STATIC_DRAW) | |
| glEnableVertexAttribArray(3); glVertexAttribPointer(3,4,GL_FLOAT,False,4*4,ctypes.c_void_p(0)) | |
| ebo = glGenBuffers(1); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo) | |
| glBufferData(GL_ELEMENT_ARRAY_BUFFER, idx.nbytes, idx, GL_STATIC_DRAW) | |
| glBindVertexArray(0) | |
| self.meshes.append({'vao':vao, 'count':len(idx)}) | |
| # default model matrix | |
| self.model = identity4() | |
| # play by name or index; crossfade from current if present | |
| def play(self, anim, blend=0.15, loop=True): | |
| if isinstance(anim, int): anim = self.anims[anim]['name'] | |
| target = None | |
| for a in self.anims: | |
| if a['name'] == anim: | |
| target = a; break | |
| if target is None: raise ValueError("Animation not found: "+str(anim)) | |
| if self.current is None: | |
| self.current = target; self.anim_time = 0.0; self.loop = loop; self.prev = None; self.blend_time = 0.0 | |
| else: | |
| self.prev = self.current | |
| self.current = target | |
| self.blend_len = blend | |
| self.blend_time = 0.0 | |
| # evaluate a node's local TRS from an animation at tick t | |
| def _eval_channel(self, chan, t): | |
| # return (pos, rot, scl) local | |
| pos = None; rot = None; scl = None | |
| if getattr(chan, 'positionkeys', None): | |
| times = [k[0] for k in chan.positionkeys] | |
| vals = [np.array(k[1],dtype=np.float32) for k in chan.positionkeys] | |
| if t <= times[0]: pos = vals[0] | |
| elif t >= times[-1]: pos = vals[-1] | |
| else: | |
| for i in range(len(times)-1): | |
| if times[i] <= t <= times[i+1]: | |
| tt = (t-times[i])/(times[i+1]-times[i]) | |
| pos = lerp(vals[i], vals[i+1], tt); break | |
| if getattr(chan, 'rotationkeys', None): | |
| times = [k[0] for k in chan.rotationkeys] | |
| vals = [np.array([k[1][0],k[1][1],k[1][2],k[1][3]],dtype=np.float32) for k in chan.rotationkeys] | |
| if t <= times[0]: rot = vals[0] | |
| elif t >= times[-1]: rot = vals[-1] | |
| else: | |
| for i in range(len(times)-1): | |
| if times[i] <= t <= times[i+1]: | |
| tt = (t-times[i])/(times[i+1]-times[i]) | |
| rot = slerp(vals[i], vals[i+1], tt); break | |
| if getattr(chan, 'scalingkeys', None): | |
| times = [k[0] for k in chan.scalingkeys] | |
| vals = [np.array(k[1],dtype=np.float32) for k in chan.scalingkeys] | |
| if t <= times[0]: scl = vals[0] | |
| elif t >= times[-1]: scl = vals[-1] | |
| else: | |
| for i in range(len(times)-1): | |
| if times[i] <= t <= times[i+1]: | |
| tt = (t-times[i])/(times[i+1]-times[i]) | |
| scl = lerp(vals[i], vals[i+1], tt); break | |
| return pos, rot, scl | |
| # compute bone matrices each frame | |
| def update(self, dt): | |
| if self.current is None: return | |
| tps = self.current['tps']; dur = self.current['duration'] | |
| self.anim_time += dt * tps | |
| if self.loop: self.anim_time %= dur | |
| if self.prev: | |
| self.blend_time += dt | |
| if self.blend_time >= self.blend_len: | |
| self.prev = None; self.blend_time = 0.0 | |
| # recursive global transforms | |
| globals_curr = {} | |
| globals_prev = {} | |
| def recurse(node, parent, anim): | |
| # local transform: default from node.transformation unless channel present | |
| local = mat4_from_list(node.transformation) if node.transformation is not None else identity4() | |
| if anim: | |
| ch = anim['channels'].get(node.name) | |
| if ch: | |
| p,r,s = self._eval_channel(ch, self.anim_time) | |
| m = identity4() | |
| if r is not None: m = m.dot(quat_to_mat4(r)) | |
| if p is not None: m = m.dot(translate_matrix(p)) | |
| if s is not None: m = m.dot(scale_matrix(s)) | |
| local = m | |
| global_tf = parent.dot(local) | |
| if node.name in self.bone_index: | |
| globals_curr[self.bone_index[node.name]] = global_tf | |
| for c in node.children: recurse(c, global_tf, anim) | |
| recurse(self.scene.rootnode, identity4(), self.current) | |
| if self.prev: | |
| # use same time for prev for sync; could keep prev_time separately for more control | |
| def recurse_prev(node, parent, anim): | |
| local = mat4_from_list(node.transformation) if node.transformation is not None else identity4() | |
| if anim: | |
| ch = anim['channels'].get(node.name) | |
| if ch: | |
| p,r,s = self._eval_channel(ch, self.anim_time) | |
| m = identity4() | |
| if r is not None: m = m.dot(quat_to_mat4(r)) | |
| if p is not None: m = m.dot(translate_matrix(p)) | |
| if s is not None: m = m.dot(scale_matrix(s)) | |
| local = m | |
| global_tf = parent.dot(local) | |
| if node.name in self.bone_index: | |
| globals_prev[self.bone_index[node.name]] = global_tf | |
| for c in node.children: recurse_prev(c, global_tf, anim) | |
| recurse_prev(self.scene.rootnode, identity4(), self.prev) | |
| else: | |
| globals_prev = globals_curr | |
| # blend per-bone in TRS space for good rotation results | |
| t = (self.blend_time / self.blend_len) if (self.prev and self.blend_len>0) else 1.0 | |
| for i in range(min(len(self.bone_names), self.max_bones)): | |
| g = globals_curr.get(i, identity4()) | |
| gp = globals_prev.get(i, identity4()) | |
| # decompose mats into TRS approx by extracting translation and rotation from mat4 | |
| trans_g = g[:3,3] | |
| trans_gp = gp[:3,3] | |
| # rotation extraction (approx): convert 3x3 to quat via matrix -> quat (fast method) | |
| def mat3_to_quat(m): | |
| trace = m[0,0] + m[1,1] + m[2,2] | |
| if trace > 0: | |
| s = 0.5 / math.sqrt(trace + 1.0) | |
| w = 0.25 / s | |
| x = (m[2,1] - m[1,2]) * s | |
| y = (m[0,2] - m[2,0]) * s | |
| z = (m[1,0] - m[0,1]) * s | |
| else: | |
| if m[0,0] > m[1,1] and m[0,0] > m[2,2]: | |
| s = 2.0 * math.sqrt(1.0 + m[0,0] - m[1,1] - m[2,2]) | |
| w = (m[2,1] - m[1,2]) / s | |
| x = 0.25 * s | |
| y = (m[0,1] + m[1,0]) / s | |
| z = (m[0,2] + m[2,0]) / s | |
| elif m[1,1] > m[2,2]: | |
| s = 2.0 * math.sqrt(1.0 + m[1,1] - m[0,0] - m[2,2]) | |
| w = (m[0,2] - m[2,0]) / s | |
| x = (m[0,1] + m[1,0]) / s | |
| y = 0.25 * s | |
| z = (m[1,2] + m[2,1]) / s | |
| else: | |
| s = 2.0 * math.sqrt(1.0 + m[2,2] - m[0,0] - m[1,1]) | |
| w = (m[1,0] - m[0,1]) / s | |
| x = (m[0,2] + m[2,0]) / s | |
| y = (m[1,2] + m[2,1]) / s | |
| z = 0.25 * s | |
| return np.array([x,y,z,w], dtype=np.float32) | |
| rot_g = mat3_to_quat(g[:3,:3]) | |
| rot_gp = mat3_to_quat(gp[:3,:3]) | |
| s_g = np.array([np.cbrt(np.linalg.det(g[:3,:3]))]*3) if np.linalg.det(g[:3,:3])!=0 else np.array([1,1,1]) | |
| s_gp = np.array([np.cbrt(np.linalg.det(gp[:3,:3]))]*3) if np.linalg.det(gp[:3,:3])!=0 else np.array([1,1,1]) | |
| trans_blend = lerp(trans_gp, trans_g, t) | |
| rot_blend = slerp(rot_gp, rot_g, t) | |
| scl_blend = lerp(s_gp, s_g, t) | |
| # compose local matrix: T * R * S | |
| M = identity4() | |
| M = M.dot(translate_matrix(trans_blend)) | |
| M = M.dot(quat_to_mat4(rot_blend)) | |
| M = M.dot(scale_matrix(scl_blend)) | |
| final = M.dot(self.inv_bind[i]) | |
| self.bone_matrices[i] = final | |
| # pad remaining with identity | |
| if len(self.bone_names) < self.max_bones: | |
| for i in range(len(self.bone_names), self.max_bones): | |
| self.bone_matrices[i] = identity4() | |
| def render(self, proj, view, model=None): | |
| if model is None: model = self.model | |
| glUseProgram(self.program) | |
| loc = glGetUniformLocation | |
| glUniformMatrix4fv(loc(self.program, "u_proj"), 1, GL_FALSE, proj.T.astype(np.float32)) | |
| glUniformMatrix4fv(loc(self.program, "u_view"), 1, GL_FALSE, view.T.astype(np.float32)) | |
| glUniformMatrix4fv(loc(self.program, "u_model"), 1, GL_FALSE, model.T.astype(np.float32)) | |
| locb = glGetUniformLocation(self.program, "u_bones") | |
| glUniformMatrix4fv(locb, self.max_bones, GL_FALSE, self.bone_matrices.reshape(-1).astype(np.float32)) | |
| for m in self.meshes: | |
| glBindVertexArray(m['vao']) | |
| glDrawElements(GL_TRIANGLES, m['count'], GL_UNSIGNED_INT, ctypes.c_void_p(0)) | |
| glBindVertexArray(0) | |
| glUseProgram(0) | |
| # ---------------- Example (GLFW) ---------------- | |
| def perspective(fovy, aspect, near, far): | |
| f = 1.0 / math.tan(math.radians(fovy)/2.0) | |
| m = np.zeros((4,4), dtype=np.float32) | |
| m[0,0] = f / aspect; m[1,1] = f | |
| m[2,2] = (far+near)/(near - far); m[2,3] = (2*far*near)/(near - far) | |
| m[3,2] = -1.0 | |
| return m | |
| def look_at(eye, center, up): | |
| e = np.array(eye); c = np.array(center); u = np.array(up) | |
| f = c - e; f /= np.linalg.norm(f) | |
| s = np.cross(f,u); s /= np.linalg.norm(s) | |
| u2 = np.cross(s,f) | |
| M = identity4() | |
| M[0,:3] = s; M[1,:3] = u2; M[2,:3] = -f | |
| M[:3,3] = -M[:3,:3].dot(e) | |
| return M | |
| if __name__ == "__main__": | |
| if not glfw.init(): raise SystemExit("glfw init failed") | |
| w,h = 1280,720 | |
| glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR,3) | |
| glfw.window_hint(glfw.CONTEXT_VERSION_MINOR,3) | |
| glfw.window_hint(glfw.OPENGL_PROFILE,glfw.OPENGL_CORE_PROFILE) | |
| win = glfw.create_window(w,h,"GLB Viewer", None, None) | |
| glfw.make_context_current(win) | |
| glEnable(GL_DEPTH_TEST) | |
| player = GLBPlayer("model.glb", max_bones=80) # <--- put path to your GLB | |
| print("Animations:", [a['name'] for a in player.anims]) | |
| player.play(0, blend=0.12) # start idle (index 0) | |
| running = False | |
| last = time.time() | |
| def key_cb(window, key, sc, action, mods): | |
| nonlocal running | |
| if key == glfw.KEY_ESCAPE and action == glfw.PRESS: glfw.set_window_should_close(window, True) | |
| if key == glfw.KEY_SPACE and action == glfw.PRESS: | |
| running = not running | |
| try: | |
| if running: | |
| player.play("Run", blend=0.12) | |
| else: | |
| player.play("Idle", blend=0.12) | |
| except: | |
| # fallback indices: 1 -> run, 0 -> idle | |
| if running: player.play(1, blend=0.12) | |
| else: player.play(0, blend=0.12) | |
| glfw.set_key_callback(win, key_cb) | |
| while not glfw.window_should_close(win): | |
| now = time.time(); dt = now - last; last = now | |
| player.update(dt) | |
| glViewport(0,0,w,h); glClearColor(0.15,0.16,0.18,1.0); glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT) | |
| proj = perspective(60.0, w/h, 0.1, 100.0) | |
| view = look_at([0,1.5,4.0], [0,1.0,0], [0,1,0]) | |
| player.render(proj, view) | |
| glfw.swap_buffers(win); glfw.poll_events() | |
| glfw.terminate() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment