diff --git a/punyverse/camera.py b/punyverse/camera.py index 18a6d64..f50a3a6 100644 --- a/punyverse/camera.py +++ b/punyverse/camera.py @@ -1,5 +1,8 @@ from math import sin, cos, radians, hypot +from punyverse.glgeom import Matrix4f +from punyverse.utils import cached_property + class Camera(object): def __init__(self, x=0, y=0, z=0, pitch=0, yaw=0, roll=0): @@ -20,6 +23,8 @@ class Camera(object): self.y += dy * speed self.z += dz * speed + self.view_matrix = None + def mouse_move(self, dx, dy): if self.pitch > 90 or self.pitch < -90: dx = -dx @@ -36,6 +41,8 @@ class Camera(object): elif self.pitch > 180: self.pitch -= 360 + self.view_matrix = None + def direction(self): m = cos(radians(self.pitch)) @@ -46,14 +53,21 @@ class Camera(object): def update(self, dt, move): if self.roll_left: - self.roll += 4 * dt * 10 - if self.roll: self.roll -= 4 * dt * 10 + self.view_matrix = None + if self.roll_right: + self.roll += 4 * dt * 10 + self.view_matrix = None if move: self.move(self.speed * 10 * dt) def reset_roll(self): self.roll = 0 + self.view_matrix = None def distance(self, x, y, z): return hypot(hypot(x - self.x, y - self.y), z - self.z) + + @cached_property + def view_matrix(self): + return Matrix4f.from_angles((self.x, self.y, self.z), (self.pitch, self.yaw, self.roll), view=True) diff --git a/punyverse/entity.py b/punyverse/entity.py index eed5300..0b340df 100644 --- a/punyverse/entity.py +++ b/punyverse/entity.py @@ -5,10 +5,11 @@ from pyglet.gl import * # noinspection PyUnresolvedReferences from six.moves import range -from punyverse.glgeom import compile, glMatrix, glRestore, belt, Sphere, Disk, OrbitVBO +from punyverse.glgeom import compile, glMatrix, glRestore, belt, Sphere, Disk, OrbitVBO, Matrix4f from punyverse.model import load_model, WavefrontVBO from punyverse.orbit import KeplerOrbit from punyverse.texture import get_best_texture, load_clouds +from punyverse.utils import cached_property G = 6.67384e-11 # Gravitation Constant @@ -16,13 +17,19 @@ G = 6.67384e-11 # Gravitation Constant class Entity(object): background = False - def __init__(self, name, location, rotation=(0, 0, 0), direction=(0, 0, 0)): + def __init__(self, world, name, location, rotation=(0, 0, 0), direction=(0, 0, 0)): + self.world = world self.name = name self.location = location self.rotation = rotation self.direction = direction + @cached_property + def mv_matrix(self): + return self.world.view_matrix() * Matrix4f.from_angles(self.location, self.rotation) + def update(self): + self.mv_matrix = None x, y, z = self.location dx, dy, dz = self.direction self.location = x + dx, y + dy, z + dz @@ -35,24 +42,25 @@ class Entity(object): class Asteroid(Entity): - def __init__(self, model, location, direction): - super(Asteroid, self).__init__('Asteroid', location, direction=direction) + def __init__(self, world, model, location, direction): + super(Asteroid, self).__init__(world, 'Asteroid', location, direction=direction) self.model = model def update(self): super(Asteroid, self).update() - rx, ry, rz = self.rotation # Increment all axis to 'spin' self.rotation = rx + 1, ry + 1, rz + 1 def draw(self, options): - with glMatrix(self.location, self.rotation): + with glMatrix(): + glLoadMatrixf(self.mv_matrix.as_gl()) self.model.draw() class AsteroidManager(object): - def __init__(self): + def __init__(self, world): + self.world = world self.asteroids = [] def __bool__(self): @@ -60,16 +68,14 @@ class AsteroidManager(object): __nonzero__ = __bool__ def load(self, file): - self.asteroids.append(WavefrontVBO(load_model(file), 5, 5, 5, (0, 0, 0))) + self.asteroids.append(WavefrontVBO(load_model(file), 5, 5, 5)) def new(self, location, direction): - return Asteroid(random.choice(self.asteroids), location, direction) + return Asteroid(self.world, random.choice(self.asteroids), location, direction) class Belt(Entity): def __init__(self, name, world, info): - self.world = world - x = world.evaluate(info.get('x', 0)) y = world.evaluate(info.get('y', 0)) z = world.evaluate(info.get('z', 0)) @@ -86,12 +92,12 @@ class Belt(Entity): models = [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] + info.get('sz', scale)) for model in models] self.belt_id = compile(belt, radius, cross, objects, count) self.rotation_angle = 360.0 / rotation if rotation else 0 - super(Belt, self).__init__(name, (x, y, z), (inclination, longitude, argument)) + super(Belt, self).__init__(world, name, (x, y, z), (inclination, longitude, argument)) def update(self): super(Belt, self).update() @@ -99,7 +105,8 @@ class Belt(Entity): self.rotation = pitch, self.world.tick * self.rotation_angle % 360, roll def draw(self, options): - with glMatrix(self.location, self.rotation), glRestore(GL_CURRENT_BIT): + with glMatrix(), glRestore(GL_CURRENT_BIT): + glLoadMatrixf(self.mv_matrix.as_gl()) glCallList(self.belt_id) @@ -111,8 +118,7 @@ class Sky(Entity): yaw = world.evaluate(info.get('yaw', 0)) roll = world.evaluate(info.get('roll', 0)) - super(Sky, self).__init__('Sky', (0, 0, 0), (pitch, yaw, roll)) - self.world = world + super(Sky, self).__init__(world, 'Sky', (0, 0, 0), (pitch, yaw, roll)) self.texture = get_best_texture(info['texture']) division = info.get('division', 30) @@ -120,7 +126,9 @@ class Sky(Entity): def draw(self, options): cam = self.world.cam - with glMatrix((-cam.x, -cam.y, -cam.z), self.rotation), glRestore(GL_TEXTURE_BIT | GL_ENABLE_BIT): + with glMatrix(), glRestore(GL_TEXTURE_BIT | GL_ENABLE_BIT): + matrix = self.world.view_matrix() * Matrix4f.from_angles((-cam.x, -cam.y, -cam.z), self.rotation) + glLoadMatrixf(matrix.as_gl()) glEnable(GL_CULL_FACE) glEnable(GL_TEXTURE_2D) glDisable(GL_LIGHTING) @@ -132,7 +140,6 @@ class Sky(Entity): class Body(Entity): def __init__(self, name, world, info, parent=None): - self.world = world self.parent = parent self.satellites = [] @@ -151,7 +158,7 @@ class Body(Entity): self.orbit_blend = orbit_distance / 4 self.orbit_opaque = orbit_distance - super(Body, self).__init__(name, (x, y, z), (pitch, yaw, roll)) + super(Body, self).__init__(world, name, (x, y, z), (pitch, yaw, roll)) self.initial_roll = roll self.orbit = None @@ -180,17 +187,23 @@ class Body(Entity): self.orbit_vbo = None self.orbit_cache = None + @cached_property + def orbit_matrix(self): + return self.world.view_matrix() * Matrix4f.from_angles(self.location) + def update(self): super(Body, self).update() - pitch, yaw, roll = self.rotation - roll = (self.initial_roll + self.world.tick * self.rotation_angle) % 360 - self.rotation = pitch, yaw, roll + if self.rotation_angle: + pitch, yaw, roll = self.rotation + roll = (self.initial_roll + self.world.tick * self.rotation_angle) % 360 + self.rotation = pitch, yaw, roll if self.orbit: px, py, pz = self.parent.location x, z, y = self.orbit.orbit(self.world.tick * self.orbit_speed % 360) self.location = (x + px, y + py, z + pz) + self.orbit_matrix = None for satellite in self.satellites: satellite.update() @@ -212,7 +225,9 @@ class Body(Entity): return self.orbit_vbo def _draw_orbits(self, distance): - with glMatrix(self.parent.location), glRestore(GL_ENABLE_BIT | GL_LINE_BIT | GL_CURRENT_BIT): + with glMatrix(), glRestore(GL_ENABLE_BIT | GL_LINE_BIT | GL_CURRENT_BIT): + glLoadMatrixf(self.parent.orbit_matrix.as_gl()) + glDisable(GL_LIGHTING) solid = distance < self.parent.orbit_opaque glColor4f(1, 1, 1, 1 if solid else (1 - (distance - self.parent.orbit_opaque) / self.parent.orbit_blend)) @@ -284,7 +299,8 @@ class SphericalBody(Body): self.ring = Disk(distance, distance + size, 30) def _draw_sphere(self, fv4=GLfloat * 4): - with glMatrix(self.location, self.rotation), glRestore(GL_LIGHTING_BIT | GL_ENABLE_BIT | GL_TEXTURE_BIT): + with glMatrix(), glRestore(GL_LIGHTING_BIT | GL_ENABLE_BIT | GL_TEXTURE_BIT): + glLoadMatrixf(self.mv_matrix.as_gl()) glEnable(GL_CULL_FACE) glCullFace(GL_BACK) @@ -301,14 +317,11 @@ class SphericalBody(Body): self.sphere.draw() - def _draw_atmosphere(self, glMatrixBuffer=GLfloat * 16): - with glMatrix(self.location), glRestore(GL_ENABLE_BIT | GL_CURRENT_BIT | GL_TEXTURE_BIT): - matrix = glMatrixBuffer() - glGetFloatv(GL_MODELVIEW_MATRIX, matrix) - matrix[0: 3] = [1, 0, 0] - matrix[4: 7] = [0, 1, 0] - matrix[8:11] = [0, 0, 1] - glLoadMatrixf(matrix) + def _draw_atmosphere(self): + with glMatrix(), glRestore(GL_ENABLE_BIT | GL_CURRENT_BIT | GL_TEXTURE_BIT): + mv = self.mv_matrix.matrix + matrix = Matrix4f([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, mv[12], mv[13], mv[14], 1]) + glLoadMatrixf(matrix.as_gl()) glDisable(GL_LIGHTING) glEnable(GL_TEXTURE_2D) @@ -319,7 +332,8 @@ class SphericalBody(Body): self.atmosphere.draw() def _draw_clouds(self): - with glMatrix(self.location, self.rotation), glRestore(GL_ENABLE_BIT | GL_TEXTURE_BIT): + with glMatrix(), glRestore(GL_ENABLE_BIT | GL_TEXTURE_BIT): + glLoadMatrixf(self.mv_matrix.as_gl()) glEnable(GL_BLEND) glEnable(GL_ALPHA_TEST) glEnable(GL_CULL_FACE) @@ -331,7 +345,8 @@ class SphericalBody(Body): self.clouds.draw() def _draw_rings(self): - with glMatrix(self.location, self.ring_rotation), glRestore(GL_ENABLE_BIT | GL_TEXTURE_BIT): + with glMatrix(), glRestore(GL_ENABLE_BIT | GL_TEXTURE_BIT): + glLoadMatrixf(self.mv_matrix.as_gl()) glDisable(GL_LIGHTING) glEnable(GL_TEXTURE_2D) glEnable(GL_BLEND) @@ -365,8 +380,9 @@ class ModelBody(Body): scale = info.get('scale', 1) self.vbo = WavefrontVBO(load_model(info['model']), info.get('sx', scale), info.get('sy', scale), - info.get('sz', scale), (0, 0, 0)) + info.get('sz', scale)) def _draw(self, options): - with glMatrix(self.location, self.rotation): + with glMatrix(): + glLoadMatrixf(self.mv_matrix.as_gl()) self.vbo.draw() diff --git a/punyverse/glgeom.py b/punyverse/glgeom.py index b6f5b11..127e9fd 100644 --- a/punyverse/glgeom.py +++ b/punyverse/glgeom.py @@ -99,6 +99,59 @@ def array_to_gl_buffer(buffer, array_type='f'): return vbo.value +class Matrix4f(object): + def __init__(self, matrix): + self.matrix = array('f', matrix) + assert len(self.matrix) == 16 + + @classmethod + def from_angles(cls, location=(0, 0, 0), rotation=(0, 0, 0), view=False): + m = [0] * 16 + x, y, z = location + pitch, yaw, roll = rotation + sp, sy, sr = sin(radians(pitch)), sin(radians(yaw)), sin(radians(roll)) + cp, cy, cr = cos(radians(pitch)), cos(radians(yaw)), cos(radians(roll)) + + m[0x0] = cy * cr + m[0x1] = sp * sy * cr + cp * sr + m[0x2] = sp * sr - cp * sy * cr + m[0x3] = 0 + m[0x4] = -cy * sr + m[0x5] = cp * cr - sp * sy * sr + m[0x6] = cp * sy * sr + sp * cr + m[0x7] = 0 + m[0x8] = sy + m[0x9] = -sp * cy + m[0xA] = cp * cy + m[0xB] = 0 + if view: + m[0xC] = m[0x0] * -x + m[0x4] * -y + m[0x8] * -z + m[0xD] = m[0x1] * -x + m[0x5] * -y + m[0x9] * -z + m[0xE] = m[0x2] * -x + m[0x6] * -y + m[0xA] * -z + else: + m[0xC] = x + m[0xD] = y + m[0xE] = z + m[0xF] = 1 + return cls(m) + + def as_gl(self): + return array_to_ctypes(self.matrix) + + @property + def bytes(self): + return self.matrix.itemsize * 16 + + def __mul__(self, other): + if not isinstance(other, Matrix4f): + return NotImplemented + + rows = ((0, 4, 8, 12), (1, 5, 9, 13), (2, 6, 10, 14), (3, 7, 11, 15)) + cols = ((0, 1, 2, 3), (4, 5, 6, 7), (8, 9, 10, 11), (12, 13, 14, 15)) + a, b = self.matrix, other.matrix + return type(self)(sum(a[i] * b[j] for i, j in zip(r, c)) for c in cols for r in rows) + + def compile(pointer, *args, **kwargs): display = glGenLists(1) glNewList(display, GL_COMPILE) diff --git a/punyverse/model.py b/punyverse/model.py index 1231850..9465bfa 100644 --- a/punyverse/model.py +++ b/punyverse/model.py @@ -9,7 +9,7 @@ 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.glgeom import array_to_gl_buffer, glRestoreClient, glRestore from punyverse.texture import load_texture @@ -221,10 +221,9 @@ class ModelVBO(object): class WavefrontVBO(object): - def __init__(self, model, sx=1, sy=1, sz=1, rotation=(0, 0, 0)): + def __init__(self, model, sx=1, sy=1, sz=1): self._tex_cache = {} self.vbos = [] - self.rotation = rotation self.scale = (sx, sy, sz) for m, material in six.iteritems(model.materials): @@ -239,7 +238,7 @@ class WavefrontVBO(object): self.vbos.append((group.material, self.process_group(group, vertices, normals, textures))) def draw(self, fv4=GLfloat * 4): - with glMatrix(rotation=self.rotation), glRestore(GL_TEXTURE_BIT | GL_ENABLE_BIT): + with 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 diff --git a/punyverse/ui.py b/punyverse/ui.py index a8e8135..f699e0e 100644 --- a/punyverse/ui.py +++ b/punyverse/ui.py @@ -236,14 +236,10 @@ class Punyverse(pyglet.window.Window): def on_draw(self): glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) - glLoadIdentity() + glLoadMatrixf(self.world.view_matrix().as_gl()) c = self.world.cam x, y, z = c.x, c.y, c.z - glRotatef(c.pitch, 1, 0, 0) - glRotatef(c.yaw, 0, 1, 0) - glRotatef(c.roll, 0, 0, 1) - glTranslatef(-x, -y, -z) glEnable(GL_LIGHTING) world = self.world diff --git a/punyverse/utils.py b/punyverse/utils.py new file mode 100644 index 0000000..8a2f4a2 --- /dev/null +++ b/punyverse/utils.py @@ -0,0 +1,18 @@ +class cached_property(object): + def __init__(self, func, name=None): + self.func = func + self.name = name or func.__name__ + self.__doc__ = getattr(func, '__doc__') + + def __get__(self, instance, owner=None): + if instance is None: + return self + result = instance.__dict__[self.name] = self.func(instance) + return result + + def __set__(self, instance, value): + if value is None: + if self.name in instance.__dict__: + del instance.__dict__[self.name] + else: + instance.__dict__[self.name] = value diff --git a/punyverse/world.py b/punyverse/world.py index 10173f5..f0c5e41 100644 --- a/punyverse/world.py +++ b/punyverse/world.py @@ -23,7 +23,7 @@ class World(object): self.z = None self.tick_length = 0 self.tick = 0 - self.asteroids = AsteroidManager() + self.asteroids = AsteroidManager(self) self.cam = Camera() self.callback = callback @@ -143,3 +143,6 @@ class World(object): c.move(c.speed * 12 * dt) else: self._time_accumulate += delta + + def view_matrix(self): + return self.cam.view_matrix