From 487b17c55b573c1ec3b8493c3f3e12d7d5d25186 Mon Sep 17 00:00:00 2001 From: Quantum Date: Fri, 24 Aug 2018 23:21:28 -0400 Subject: [PATCH] Converted models to VBO. Note that this actually makes things slower... at least until we move to shaders. --- punyverse/entity.py | 24 +++--- punyverse/glgeom.py | 6 +- punyverse/model.py | 199 ++++++++++++++++++++++++++----------------- punyverse/world.json | 2 +- 4 files changed, 139 insertions(+), 92 deletions(-) diff --git a/punyverse/entity.py b/punyverse/entity.py index d261bd8..809cc57 100644 --- a/punyverse/entity.py +++ b/punyverse/entity.py @@ -6,7 +6,7 @@ from pyglet.gl import * from six.moves import range from punyverse.glgeom import compile, glMatrix, glRestore, belt, Sphere, Disk -from punyverse.model import model_list, load_model +from punyverse.model import load_model, WavefrontVBO from punyverse.orbit import KeplerOrbit from punyverse.texture import get_best_texture, load_clouds @@ -35,9 +35,9 @@ class Entity(object): class Asteroid(Entity): - def __init__(self, asteroid_id, location, direction): + def __init__(self, model, location, direction): super(Asteroid, self).__init__('Asteroid', location, direction=direction) - self.asteroid_id = asteroid_id + self.model = model def update(self): super(Asteroid, self).update() @@ -47,8 +47,8 @@ class Asteroid(Entity): self.rotation = rx + 1, ry + 1, rz + 1 def draw(self, options): - with glMatrix(self.location, self.rotation), glRestore(GL_CURRENT_BIT): - glCallList(self.asteroid_id) + with glMatrix(self.location, self.rotation): + self.model.draw() class AsteroidManager(object): @@ -60,7 +60,7 @@ class AsteroidManager(object): __nonzero__ = __bool__ def load(self, file): - self.asteroids.append(model_list(load_model(file), 5, 5, 5, (0, 0, 0))) + self.asteroids.append(WavefrontVBO(load_model(file), 5, 5, 5, (0, 0, 0))) def new(self, location, direction): return Asteroid(random.choice(self.asteroids), location, direction) @@ -85,8 +85,8 @@ class Belt(Entity): if not isinstance(models, list): models = [models] - objects = [model_list(load_model(model), info.get('sx', scale), info.get('sy', scale), - info.get('sz', scale), (0, 0, 0)) for model in models] + objects = [WavefrontVBO(load_model(model), info.get('sx', scale), info.get('sy', scale), + info.get('sz', scale), (0, 0, 0)) for model in models] self.belt_id = compile(belt, radius, cross, objects, count) self.rotation_angle = 360.0 / rotation if rotation else 0 @@ -373,9 +373,9 @@ class ModelBody(Body): super(ModelBody, self).__init__(name, world, info, parent) scale = info.get('scale', 1) - self.object_id = model_list(load_model(info['model']), info.get('sx', scale), info.get('sy', scale), - info.get('sz', scale), (0, 0, 0)) + self.vbo = WavefrontVBO(load_model(info['model']), info.get('sx', scale), info.get('sy', scale), + info.get('sz', scale), (0, 0, 0)) def _draw(self, options): - with glMatrix(self.location, self.rotation), glRestore(GL_CURRENT_BIT): - glCallList(self.object_id) + with glMatrix(self.location, self.rotation): + self.vbo.draw() diff --git a/punyverse/glgeom.py b/punyverse/glgeom.py index b060d2d..b2a343d 100644 --- a/punyverse/glgeom.py +++ b/punyverse/glgeom.py @@ -1,5 +1,5 @@ from array import array -from ctypes import c_int, c_float, byref, cast, POINTER, c_uint +from ctypes import c_int, c_float, byref, cast, POINTER, c_uint, c_short, c_ushort from math import * from random import random, gauss, choice @@ -84,6 +84,8 @@ def array_to_ctypes(arr): 'f': c_float, 'i': c_int, 'I': c_uint, + 'h': c_short, + 'H': c_ushort, }[arr.typecode])) @@ -219,7 +221,7 @@ def belt(radius, cross, object, count): if scale < 0: scale = 1 glScalef(scale, scale, scale) - glCallList(choice(object)) + choice(object).draw() def progress_bar(x, y, width, height, filled): diff --git a/punyverse/model.py b/punyverse/model.py index f7b1632..1231850 100644 --- a/punyverse/model.py +++ b/punyverse/model.py @@ -2,13 +2,14 @@ import bz2 import gzip import os import zipfile -from uuid import uuid4 +from collections import defaultdict import six from pyglet.gl import * # noinspection PyUnresolvedReferences from six.moves import range, zip +from punyverse.glgeom import array_to_gl_buffer, glRestoreClient, glMatrix, glRestore from punyverse.texture import load_texture @@ -46,15 +47,11 @@ class Material(object): class Group(object): - __slots__ = ('name', 'material', 'faces') + __slots__ = ('material', 'faces') - def __init__(self, name=None): - if name is None: - self.name = str(uuid4()) - else: - self.name = name - self.material = None - self.faces = [] + def __init__(self, material=None, faces=None): + self.material = material + self.faces = faces or [] class WavefrontObject(object): @@ -98,15 +95,13 @@ class WavefrontObject(object): def texture(self, words): l = len(words) - x, y, z = 0, 0, 0 + u, v = 0, 0 if l >= 2: - x = float(words[1]) + u = float(words[1]) if l >= 3: # OBJ origin is at upper left, OpenGL origin is at lower left - y = 1 - float(words[2]) - if l >= 4: - z = float(words[3]) - self.textures.append((x, y, z)) + v = 1 - float(words[2]) + self.textures.append((u, v)) def face(self, words): l = len(words) @@ -153,7 +148,7 @@ class WavefrontObject(object): print("Warning: no group") def group(self, words): - group = Group(words[1].decode('utf-8')) + group = Group() self.groups.append(group) self.current_group = group @@ -201,78 +196,128 @@ def load_model(path): return WavefrontObject(path) -def model_list(model, sx=1, sy=1, sz=1, rotation=(0, 0, 0)): - for m, text in six.iteritems(model.materials): - if text.texture: - load_texture(os.path.join(model.root, text.texture)) +class ModelVBO(object): + __slots__ = ('has_normal', 'has_texture', 'data_buf', 'index_buf', 'offset_type', 'vertex_count') - display = glGenLists(1) + def draw(self): + with glRestoreClient(GL_CLIENT_VERTEX_ARRAY_BIT): + stride = (3 + self.has_normal * 3 + self.has_texture * 2) * 4 - glNewList(display, GL_COMPILE) - glPushMatrix() - glPushAttrib(GL_CURRENT_BIT) + glBindBuffer(GL_ARRAY_BUFFER, self.data_buf) + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, self.index_buf) - pitch, yaw, roll = rotation - glPushAttrib(GL_TRANSFORM_BIT) - glRotatef(pitch, 1, 0, 0) - glRotatef(yaw, 0, 1, 0) - glRotatef(roll, 0, 0, 1) - glPopAttrib() + glEnableClientState(GL_VERTEX_ARRAY) + glVertexPointer(3, GL_FLOAT, stride, 0) + if self.has_normal: + glEnableClientState(GL_NORMAL_ARRAY) + glNormalPointer(GL_FLOAT, stride, 3 * 4) + if self.has_texture: + glEnableClientState(GL_TEXTURE_COORD_ARRAY) + glTexCoordPointer(3, GL_FLOAT, stride, (6 if self.has_normal else 3) * 4) - vertices = model.vertices - textures = model.textures - normals = model.normals + glDrawElements(GL_TRIANGLES, self.vertex_count, self.offset_type, 0) + glBindBuffer(GL_ARRAY_BUFFER, 0) + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0) - for g in model.groups: - material = g.material - tex_id = load_texture(os.path.join(model.root, material.texture)) if (material and material.texture) else 0 +class WavefrontVBO(object): + def __init__(self, model, sx=1, sy=1, sz=1, rotation=(0, 0, 0)): + self._tex_cache = {} + self.vbos = [] + self.rotation = rotation + self.scale = (sx, sy, sz) - if tex_id: - glEnable(GL_TEXTURE_2D) - glBindTexture(GL_TEXTURE_2D, tex_id) - else: - glBindTexture(GL_TEXTURE_2D, 0) - glDisable(GL_TEXTURE_2D) + for m, material in six.iteritems(model.materials): + if material.texture and material.texture not in self._tex_cache: + self._tex_cache[material.texture] = load_texture(os.path.join(model.root, material.texture)) - if material: - fv4 = GLfloat * 4 + vertices = model.vertices + textures = model.textures + normals = model.normals - if material.Ka: - kx, ky, kz = material.Ka - glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, fv4(kx, ky, kz, 1)) - if material.Kd: - kx, ky, kz = material.Kd - glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, fv4(kx, ky, kz, 1)) - if material.Ks: - kx, ky, kz = material.Ks - glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, fv4(kx, ky, kz, 1)) - glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, material.shininess) + for group in self.merge_groups(model): + self.vbos.append((group.material, self.process_group(group, vertices, normals, textures))) - def point(f, n, max_texture=len(textures)): - normal = f.norms[n] - if normal is not None: - glNormal3f(*normals[normal]) - texture = f.texs[n] - if texture is not None and texture < max_texture: - glTexCoord2f(*textures[texture][:2]) - x, y, z = vertices[f.verts[n]] - glVertex3f(x * sx, y * sy, z * sz) + def draw(self, fv4=GLfloat * 4): + with glMatrix(rotation=self.rotation), glRestore(GL_TEXTURE_BIT | GL_ENABLE_BIT): + for mat, vbo in self.vbos: + tex_id = self._tex_cache[mat.texture] if mat and mat.texture else 0 - glBegin(GL_TRIANGLES) - for f in g.faces: - for a, b in zip(range(1, f.size), range(2, f.size)): - point(f, 0) - point(f, a) - point(f, b) - glEnd() + if tex_id: + glEnable(GL_TEXTURE_2D) + glBindTexture(GL_TEXTURE_2D, tex_id) + else: + glBindTexture(GL_TEXTURE_2D, 0) + glDisable(GL_TEXTURE_2D) - if tex_id: - glBindTexture(GL_TEXTURE_2D, 0) - glDisable(GL_TEXTURE_2D) + if mat: + if mat.Ka: + kx, ky, kz = mat.Ka + glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, fv4(kx, ky, kz, 1)) + if mat.Kd: + kx, ky, kz = mat.Kd + glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, fv4(kx, ky, kz, 1)) + if mat.Ks: + kx, ky, kz = mat.Ks + glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, fv4(kx, ky, kz, 1)) + glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, mat.shininess) - glPopAttrib() - glPopMatrix() + vbo.draw() - glEndList() - return display + def merge_groups(self, model): + by_mat = defaultdict(list) + for g in model.groups: + if g.faces: + by_mat[g.material].append(g) + + groups = [] + for mat, gs in six.iteritems(by_mat): + faces = [] + for g in gs: + faces += g.faces + groups.append(Group(mat, faces)) + return groups + + def process_group(self, group, vertices, normals, textures): + sx, sy, sz = self.scale + max_texture = len(textures) + has_texture = bool(textures) and any(any(n is not None for n in f.texs) for f in group.faces) + has_normal = bool(normals) and any(any(n is not None for n in f.norms) for f in group.faces) + buffer = [] + indices = [] + offsets = {} + + for f in group.faces: + verts = [] + for v, n, t in zip(f.verts, f.norms, f.texs): + # Blender defines texture coordinates on faces even without textures. + if t is not None and t >= max_texture: + t = None + if (v, n, t) in offsets: + verts.append(offsets[v, n, t]) + else: + index = len(offsets) + verts.append(index) + x, y, z = vertices[v] + item = [sx * x, sy * y, sz * z] + if has_normal: + item += [0, 0, 0] if n is None else list(normals[n]) + if has_texture: + item += [0, 0] if t is None else list(textures[t]) + offsets[v, n, t] = index + buffer += item + + for a, b in zip(verts[1:], verts[2:]): + indices += [verts[0], a, b] + + result = ModelVBO() + result.has_normal = has_normal + result.has_texture = has_texture + result.offset_type = GL_UNSIGNED_SHORT if len(offsets) < 65536 else GL_UNSIGNED_INT + result.data_buf = array_to_gl_buffer(buffer, 'f') + result.index_buf = array_to_gl_buffer(indices, { + GL_UNSIGNED_SHORT: 'H', + GL_UNSIGNED_INT: 'I', + }[result.offset_type]) + result.vertex_count = len(indices) + return result diff --git a/punyverse/world.json b/punyverse/world.json index b3a2b5b..6772c11 100644 --- a/punyverse/world.json +++ b/punyverse/world.json @@ -291,7 +291,7 @@ "radius": "2.362 * AU", "cross": 1000, "scale": 30, - "count": 1024, + "count": 256, "rotation": 114536500 } },