From 6485da7d678dec4ccb5c7d5191732bbb66f137ce Mon Sep 17 00:00:00 2001 From: Roz K Date: Sat, 31 Dec 2022 19:16:44 +0100 Subject: [PATCH] Basic scene graph. --- game/game.py | 238 ++++++++++++++++++---------------------------- game/inputs.py | 29 ++++++ game/resources.py | 5 +- game/scene.py | 95 ++++++++++++++++++ game/sea.py | 8 +- game/texture.py | 31 ++++++ game/time.py | 29 ++++++ game/triangles.py | 76 +++++++++------ 8 files changed, 327 insertions(+), 184 deletions(-) create mode 100644 game/inputs.py create mode 100644 game/scene.py create mode 100644 game/texture.py create mode 100644 game/time.py diff --git a/game/game.py b/game/game.py index ad30abe..7abc63f 100644 --- a/game/game.py +++ b/game/game.py @@ -13,24 +13,26 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import time from math import pi, tau, dist -from ctypes import Structure from engine import * from game import math -from game import triangles -from game import sea -from game.shader import Shader -from game.generator import Generator -from game.resources import RuntimeArchive -from game.batch import Batch -from game.camera import Camera -from game.environment import Environment +from game.time import Time from game.events import Events from game.mouse import Mouse from game.keyboard import Keyboard +from game.generator import Generator +from game.resources import RuntimeArchive +from game.texture import Texture +from game.shader import Shader +from game.camera import Camera +from game.environment import Environment +from game.inputs import InputFloat +from game.batch import Batch +from game.scene import SceneNode, TextureNode, ShaderNode, InputNode, DrawNode, FuncNode +from game.triangles import SkyTriangles +from game import sea proj_hfov = pi * 0.25 proj_ratio = 16.0 / 9.0 @@ -40,27 +42,49 @@ proj_far_z = 3000.0 sun_direction = math.vec3_normalize((1.0, 0.0, 0.5)) sun_power = 1.0 -def main(): - print("Generating terrain...") - gen_begin = time.thread_time() - generated = Generator(256) - gen_end = time.thread_time() - print("Done: ", round(gen_end - gen_begin, 2), "seconds") +def update_camera(time, mouse, camera, environment): + camera_yaw = mouse.drag[0] * 0.001 + camera_pitch = mouse.drag[1] * 0.001 + pi * 0.25 + camera_distance = mouse.wheel * 20.0 + camera.set_view(camera_yaw, camera_pitch, camera_distance) + environment.from_sun(camera.view, sun_direction, sun_power) +def update_tests(time, blob_translation, blob_spawn_translation, cube_translation, cube_spawn_translation, cube_orientation, clouds_orientation): + rotation = mat3() + mat3_rotation(rotation, vec3_up, (time.current * 0.21) % tau) + mat3_mul_vec3(blob_translation, rotation, blob_spawn_translation) + mat3_mul_vec3(cube_translation, rotation, cube_spawn_translation) + mat3_rotation(cube_orientation, vec3_up, (time.current * 0.43) % tau) + mat3_rotation(clouds_orientation, vec3_up, (time.current * -0.037) % tau) + +def update_sea(time, camera, sea_phase): + camera.to_km() + sea_phase.update((time.current * 0.023) % 1.0) + +def main(): print("Initializing...") display = create_display(b'RK Island - Drag to rotate, wheel to zoom, q to quit', 1600, 900) events = Events(display) mouse = Mouse(events, wheel = 60, wheel_min = 20) keyboard = Keyboard(events) render_initialize(__debug__) - terrain_shader = Shader('terrain', 'common') + + tiles_shader = Shader('terrain', 'common') tests_shader = Shader('tests', 'common') sky_shader = Shader('sky') - heightmap = create_texture( + camera = Camera() + camera.set_projection(proj_hfov, proj_ratio, proj_near_z, proj_far_z) + environment = Environment() + sea_phase = InputFloat(sky_shader.u_sea_phase, 0.0) + + print("Generating terrain...") + generated = Generator(256) + + heightmap = Texture( TEXTURE_FORMAT_FLOAT_32, 256, 256, 0, TEXTURE_FLAG_MIN_LINEAR | TEXTURE_FLAG_MAG_LINEAR, generated.packed_heights) - normalmap = create_texture( + normalmap = Texture( TEXTURE_FORMAT_RGB10_A2, 256, 256, 0, TEXTURE_FLAG_MIN_LINEAR | TEXTURE_FLAG_MAG_LINEAR, generated.packed_normals) @@ -77,7 +101,7 @@ def main(): rock_model = archive.get_model('rock') mud_model = archive.get_model('mud') lava_model = archive.get_model('lava') - terrain_batch = Batch(tiles_vertices, generated.size ** 2, 8, translation = PARAM_FORMAT_VEC3_SHORT) + tiles_batch = Batch(tiles_vertices, generated.size ** 2, 8, translation = PARAM_FORMAT_VEC3_SHORT) #TODO: generator & for real vc = generated.volcano_c @@ -108,7 +132,7 @@ def main(): model = mud_model else: model = rock_model - model.spawn(terrain_batch, vec3(float(((mx - 128) * 8) + 4), float(((127 - my) * 8) + 4), 0.0)) + model.spawn(tiles_batch, vec3(float(((mx - 128) * 8) + 4), float(((127 - my) * 8) + 4), 0.0)) tests_texture = archive.get_texture('tests') tests_vertices = archive.get_vertices('tests') @@ -118,153 +142,75 @@ def main(): tests_batch = Batch(tests_vertices, 3, 3, translation = PARAM_FORMAT_VEC3_FLOAT, orientation = PARAM_FORMAT_MAT3_INT10 | PARAM_FORMAT_NORMALIZE) + blob_spawn_translation = vec3(-100.0, -500.0, 0.0) - cube_spawn_translation = vec3(100.0, -500.0, 0.0) blob_forward = math.vec3_normalize((sun_direction[0], sun_direction[1], 0.0)) blob_right = math.vec3_cross(blob_forward, math.vec3_up) - blob_id = blob_model.spawn(tests_batch, - blob_spawn_translation, mat3(vec3(*blob_right), vec3(*blob_forward), vec3_up)) + blob_spawn_orientation = mat3(vec3(*blob_right), vec3(*blob_forward), vec3_up) + blob_id = blob_model.spawn(tests_batch, blob_spawn_translation, blob_spawn_orientation) + blob_translation = tests_batch.translation[blob_id] + cube_spawn_translation = vec3(100.0, -500.0, 0.0) cube_id = cube_model.spawn(tests_batch, cube_spawn_translation, mat3_identity) + cube_translation = tests_batch.translation[cube_id] + cube_orientation = tests_batch.orientation[cube_id] clouds_id = clouds_model.spawn(tests_batch, vec3(0.0, 0.0, 32.0), mat3_identity) + clouds_orientation = tests_batch.orientation[clouds_id] sea_polar_textures = sea.load_polar_textures(('data/sea_bump1.png', 'data/sea_bump2.png')) sea_detail_texture = sea.load_detail_texture('data/sea_bump.png') - sky_triangles = create_triangles(triangles.sky_triangles(64, proj_far_z - 0.1, proj_ratio)) + sky_triangles = SkyTriangles(64, proj_far_z - 0.1, proj_ratio) - camera = Camera() - camera.set_projection(proj_hfov, proj_ratio, proj_near_z, proj_far_z) - environment = Environment() - - blob_translation = tests_batch.translation[blob_id] - cube_translation = tests_batch.translation[cube_id] - cube_orientation = tests_batch.orientation[cube_id] - clouds_orientation = tests_batch.orientation[clouds_id] + scene = SceneNode( + FuncNode(update_camera, (mouse, camera, environment)), + TextureNode((tiles_texture, heightmap, normalmap), + ShaderNode(tiles_shader, + InputNode(tiles_shader, camera), + InputNode(tiles_shader, environment), + DrawNode(tiles_batch) + ) + ), + FuncNode(update_tests, (blob_translation, blob_spawn_translation, cube_translation, cube_spawn_translation, cube_orientation, clouds_orientation)), + TextureNode((tests_texture, heightmap, normalmap), + ShaderNode(tests_shader, + InputNode(tests_shader, camera), + InputNode(tests_shader, environment), + DrawNode(tests_batch) + ) + ), + FuncNode(update_sea, (camera, sea_phase)), + TextureNode((sea_polar_textures, sea_detail_texture), + ShaderNode(sky_shader, + InputNode(sky_shader, camera), + InputNode(sky_shader, environment), + InputNode(sky_shader, sea_phase), + DrawNode(sky_triangles) + ) + ) + ) print("Running... Ctrl+c to quit") - start_time = time.monotonic() - current_time = 0.0 - frame = 0 - frame_min = 10000.0 - frame_max = 0.0 - frame_avg = 0.0 - draw_min = 10000.0 - draw_max = 0.0 - draw_avg = 0.0 - perf_count = 0 + time = Time() try: - while True: - current_time = time.monotonic() - start_time - frame_begin = time.thread_time() - + while not keyboard.quit: events.update() - if keyboard.quit: - break - begin_frame() - - camera_distance = mouse.wheel * 20.0 - camera_yaw = mouse.drag[0] * 0.001 - camera_pitch = mouse.drag[1] * 0.001 + pi * 0.25 - camera.set_view(camera_yaw, camera_pitch, camera_distance) - environment.from_sun(camera.view, sun_direction, sun_power) - - terrain_shader.select() - camera.set_inputs(terrain_shader) - environment.set_inputs(terrain_shader) - select_texture(0, tiles_texture) - select_texture(1, heightmap) - select_texture(2, normalmap) - draw_begin = time.thread_time() - terrain_batch.draw() - draw_end = time.thread_time() - unselect_texture(0, tiles_texture) - unselect_texture(1, heightmap) - unselect_texture(2, normalmap) - terrain_shader.unselect() - - rotation = mat3() - mat3_rotation(rotation, vec3_up, (current_time * 0.21) % tau) - mat3_mul_vec3(blob_translation, rotation, blob_spawn_translation) - mat3_mul_vec3(cube_translation, rotation, cube_spawn_translation) - mat3_rotation(cube_orientation, vec3_up, (current_time * 0.43) % tau) - mat3_rotation(clouds_orientation, vec3_up, (current_time * -0.037) % tau) - - tests_shader.select() - camera.set_inputs(tests_shader) - environment.set_inputs(tests_shader) - select_texture(0, tests_texture) - select_texture(1, heightmap) - select_texture(2, normalmap) - tests_batch.draw() - unselect_texture(0, tests_texture) - unselect_texture(1, heightmap) - unselect_texture(2, normalmap) - tests_shader.unselect() - - camera.to_km() - - sky_shader.select() - camera.set_inputs(sky_shader) - environment.set_inputs(sky_shader) - set_input_float(sky_shader.u_sea_phase, (current_time * 0.023) % 1.0) - select_texture(0, sea_polar_textures) - select_texture(1, sea_detail_texture) - draw_triangles(sky_triangles) - unselect_texture(0, sea_polar_textures) - unselect_texture(1, sea_detail_texture) - sky_shader.unselect() - - frame_end = time.thread_time() + time.update() + scene.draw(time) end_frame() swap_buffers(display) - - if frame > 0: - draw_ms = draw_end - draw_begin - draw_min = min(draw_min, draw_ms) - draw_max = max(draw_max, draw_ms) - draw_avg += draw_ms - frame_ms = frame_end - frame_begin - frame_min = min(frame_min, frame_ms) - frame_max = max(frame_max, frame_ms) - frame_avg += frame_ms - perf_count += 1 - frame += 1 - except KeyboardInterrupt: pass - print("\rDraw *", perf_count, - ": min =", round(draw_min * 1000.0, 2), - ", max =", round(draw_max * 1000.0, 2), - ", avg =", round((draw_avg / perf_count) * 1000.0, 2), "ms") - - print("\rFrame *", perf_count, - ": min =", round(frame_min * 1000.0, 2), - ", max =", round(frame_max * 1000.0, 2), - ", avg =", round((frame_avg / perf_count) * 1000.0, 2), "ms") - - # seed 666 - # for _ in range(10000): - # current_time = 0 # time.monotonic() - start_time - - # Draw * 9999 : min = 0.14 , max = 0.43 , avg = 0.19 ms - # Draw * 9999 : min = 0.14 , max = 0.35 , avg = 0.19 ms - # Draw * 9999 : min = 0.13 , max = 0.44 , avg = 0.18 ms - - # Frame * 9999 : min = 0.21 , max = 0.7 , avg = 0.33 ms - # Frame * 9999 : min = 0.2 , max = 0.54 , avg = 0.31 ms - # Frame * 9999 : min = 0.19 , max = 0.6 , avg = 0.29 ms - print("Quitting...") del tests_batch - del terrain_batch - destroy_texture(sea_polar_textures) - destroy_texture(sea_detail_texture) - destroy_texture(heightmap) - destroy_texture(normalmap) - destroy_triangles(sky_triangles) + del tiles_batch + del sky_triangles + del sea_polar_textures + del sea_detail_texture + del heightmap + del normalmap archive.destroy() - del terrain_shader + del tiles_shader del tests_shader del sky_shader render_terminate() diff --git a/game/inputs.py b/game/inputs.py new file mode 100644 index 0000000..b4616d3 --- /dev/null +++ b/game/inputs.py @@ -0,0 +1,29 @@ +# Copyright (C) 2022 RozK +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from engine import set_input_float + +class InputFloat: + __slots__ = '_input', 'value' + + def __init__(self, input, value): + self._input = input + self.value = value + + def update(self, value): + self.value = value + + def set_inputs(self, shader): + set_input_float(self._input, self.value) diff --git a/game/resources.py b/game/resources.py index 2982503..98848f6 100644 --- a/game/resources.py +++ b/game/resources.py @@ -20,6 +20,7 @@ from pathlib import Path import png import engine +from game.texture import Texture VERTEX_SIZE = 20 VERTEX_FORMAT = engine.vertex_format( @@ -133,7 +134,7 @@ class TextureData: _write_blob(file, self.pixels) def create_texture(data): - return engine.create_texture(data.format, data.width, data.height, data.nlevels, data.flags, data.pixels) + return Texture(data.format, data.width, data.height, data.nlevels, data.flags, data.pixels) class VerticesData: __slots__ = 'name', 'vertices', 'indices' @@ -208,8 +209,6 @@ class Archive: def destroy(self): for vertices in self.vertices_db.values(): engine.destroy_vertices(vertices) - for texture in self.textures_db.values(): - engine.destroy_texture(texture) self.textures_db.clear() self.vertices_db.clear() self.models_db.clear() diff --git a/game/scene.py b/game/scene.py new file mode 100644 index 0000000..a52354c --- /dev/null +++ b/game/scene.py @@ -0,0 +1,95 @@ +# Copyright (C) 2022 RozK +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class Node: + __slots__ = '_subnodes' + + def __init__(self, *subnodes): + self._subnodes = subnodes + + def draw(self, time): + for subnode in self._subnodes: + subnode.draw(time) + +class SceneNode(Node): + __slots__ = '_subnodes' + + def __init__(self, *subnodes): + Node.__init__(self, *subnodes) + self._subnodes = subnodes + + def draw(self, time): + Node.draw(self, time) + +class TextureNode(Node): + __slots__ = '_textures' + + def __init__(self, textures, *subnodes): + Node.__init__(self, *subnodes) + self._textures = textures + + def draw(self, time): + for slot, texture in enumerate(self._textures): + texture.select(slot) + Node.draw(self, time) + for slot, texture in enumerate(self._textures): + texture.unselect(slot) + +class ShaderNode(Node): + __slots__ = '_shader' + + def __init__(self, shader, *subnodes): + Node.__init__(self, *subnodes) + self._shader = shader + + def draw(self, time): + self._shader.select() + Node.draw(self, time) + self._shader.unselect() + +class InputNode(Node): + __slots__ = '_shader', '_input' + + def __init__(self, shader, input, *subnodes): + Node.__init__(self, *subnodes) + self._shader = shader + self._input = input + + def draw(self, time): + self._input.set_inputs(self._shader) + Node.draw(self, time) + +class DrawNode(Node): + __slots__ = '_drawable' + + def __init__(self, drawable, *subnodes): + Node.__init__(self, *subnodes) + self._drawable = drawable + + def draw(self, time): + self._drawable.draw() + Node.draw(self, time) + +class FuncNode(Node): + __slots__ = '_func', '_params' + + def __init__(self, func, params, *subnodes): + Node.__init__(self, *subnodes) + self._func = func + self._params = params + + def draw(self, time): + self._func(time, *self._params) + Node.draw(self, time) diff --git a/game/sea.py b/game/sea.py index 8815b4b..bfed466 100644 --- a/game/sea.py +++ b/game/sea.py @@ -17,10 +17,10 @@ from itertools import product from math import tau, cos, sin, copysign from array import array -from engine import (create_texture, - TEXTURE_FORMAT_RGB10_A2, TEXTURE_FORMAT_TYPECODE, TEXTURE_FLAG_MIN_LINEAR, TEXTURE_FLAG_MAG_LINEAR) +from engine import TEXTURE_FORMAT_RGB10_A2, TEXTURE_FORMAT_TYPECODE, TEXTURE_FLAG_MIN_LINEAR, TEXTURE_FLAG_MAG_LINEAR from game.math import vec3_scale, vec3_direction, vec3_cross, vec3_normal_rgb10a2 +from game.texture import Texture from game.resources import load_png _format = TEXTURE_FORMAT_RGB10_A2 @@ -51,7 +51,7 @@ def load_polar_textures(paths, waves_height = 0.008): _width, _height, normals = load_texture(path) assert _width == width and _height == height data.extend(list(map(_conv, normals))) - return create_texture(_format, width, height, len(paths), _flags, data) + return Texture(_format, width, height, len(paths), _flags, data) def load_detail_texture(path, scale = 0.5, waves_height = 0.002): width, height, data = load_png(path) @@ -68,4 +68,4 @@ def load_detail_texture(path, scale = 0.5, waves_height = 0.002): return vec3_scale(n, copysign(1.0, n[2])) normals = list(map(normal, product(range(height), range(width)), data)) data = array(_typecode, list(map(_conv, normals))) - return create_texture(_format, width, height, 0, _flags, data) + return Texture(_format, width, height, 0, _flags, data) diff --git a/game/texture.py b/game/texture.py new file mode 100644 index 0000000..19a507c --- /dev/null +++ b/game/texture.py @@ -0,0 +1,31 @@ +# Copyright (C) 2022 RozK +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from engine import create_texture, select_texture, unselect_texture, destroy_texture + +class Texture: + __slots__ = '_texture' + + def __init__(self, format, width, height, nlevels, flags, pixels): + self._texture = create_texture(format, width, height, nlevels, flags, pixels) + + def __del__(self): + destroy_texture(self._texture) + + def select(self, slot): + select_texture(slot, self._texture) + + def unselect(self, slot): + unselect_texture(slot, self._texture) diff --git a/game/time.py b/game/time.py new file mode 100644 index 0000000..e2bc2bb --- /dev/null +++ b/game/time.py @@ -0,0 +1,29 @@ +# Copyright (C) 2022 RozK +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from time import monotonic + +class Time: + __slots__ = '_start', 'current', 'delta' + + def __init__(self): + self._start = monotonic() + self.current = 0.0 + self.delta = 0.0 + + def update(self): + current = monotonic() - self._start + self.delta = current - self.current + self.current = current diff --git a/game/triangles.py b/game/triangles.py index 82c3c96..a5838db 100644 --- a/game/triangles.py +++ b/game/triangles.py @@ -16,35 +16,49 @@ from itertools import chain from array import array from math import cos, sin +from engine import create_triangles, draw_triangles, destroy_triangles -# TODO: with FOV -def sky_triangles(vsubdivs, distance, projection_ratio): - assert vsubdivs > 0 - vertices = [] - hsubdivs = round(vsubdivs * projection_ratio) - z = -distance - width = distance * projection_ratio - height = distance - startx = width * -0.5 - starty = height * -0.5 - stepx = width / hsubdivs - stepy = height / vsubdivs - y1 = starty - y2 = y1 + stepy - for sy in range(vsubdivs): - x1 = startx - x2 = x1 + stepx - for sx in range(hsubdivs): - a = (x1, y2, z) - b = (x2, y1, z) - vertices.append((x1, y1, z)) - vertices.append(b) - vertices.append(a) - vertices.append((x2, y2, z)) - vertices.append(a) - vertices.append(b) - x1 = x2 - x2 += stepx - y1 = y2 - y2 += stepy - return array('f', chain.from_iterable(vertices)) +class Triangles: + __slots__ = '_triangles' + + def __init__(self, triangles): + self._triangles = create_triangles(triangles) + + def __del__(self): + destroy_triangles(self._triangles) + + def draw(self): + draw_triangles(self._triangles) + +class SkyTriangles(Triangles): + # TODO: with FOV + def __init__(self, vsubdivs, distance, projection_ratio): + assert vsubdivs > 0 + vertices = [] + hsubdivs = round(vsubdivs * projection_ratio) + z = -distance + width = distance * projection_ratio + height = distance + startx = width * -0.5 + starty = height * -0.5 + stepx = width / hsubdivs + stepy = height / vsubdivs + y1 = starty + y2 = y1 + stepy + for sy in range(vsubdivs): + x1 = startx + x2 = x1 + stepx + for sx in range(hsubdivs): + a = (x1, y2, z) + b = (x2, y1, z) + vertices.append((x1, y1, z)) + vertices.append(b) + vertices.append(a) + vertices.append((x2, y2, z)) + vertices.append(a) + vertices.append(b) + x1 = x2 + x2 += stepx + y1 = y2 + y2 += stepy + super().__init__(array('f', chain.from_iterable(vertices)))