Basic scene graph.

This commit is contained in:
Roz K 2022-12-31 19:16:44 +01:00
parent 9fff666ebf
commit 6485da7d67
Signed by: roz
GPG Key ID: 51FBF4E483E1C822
8 changed files with 327 additions and 184 deletions

View File

@ -13,24 +13,26 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import time
from math import pi, tau, dist from math import pi, tau, dist
from ctypes import Structure
from engine import * from engine import *
from game import math from game import math
from game import triangles from game.time import Time
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.events import Events from game.events import Events
from game.mouse import Mouse from game.mouse import Mouse
from game.keyboard import Keyboard 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_hfov = pi * 0.25
proj_ratio = 16.0 / 9.0 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_direction = math.vec3_normalize((1.0, 0.0, 0.5))
sun_power = 1.0 sun_power = 1.0
def main(): def update_camera(time, mouse, camera, environment):
print("Generating terrain...") camera_yaw = mouse.drag[0] * 0.001
gen_begin = time.thread_time() camera_pitch = mouse.drag[1] * 0.001 + pi * 0.25
generated = Generator(256) camera_distance = mouse.wheel * 20.0
gen_end = time.thread_time() camera.set_view(camera_yaw, camera_pitch, camera_distance)
print("Done: ", round(gen_end - gen_begin, 2), "seconds") 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...") print("Initializing...")
display = create_display(b'RK Island - Drag to rotate, wheel to zoom, q to quit', 1600, 900) display = create_display(b'RK Island - Drag to rotate, wheel to zoom, q to quit', 1600, 900)
events = Events(display) events = Events(display)
mouse = Mouse(events, wheel = 60, wheel_min = 20) mouse = Mouse(events, wheel = 60, wheel_min = 20)
keyboard = Keyboard(events) keyboard = Keyboard(events)
render_initialize(__debug__) render_initialize(__debug__)
terrain_shader = Shader('terrain', 'common')
tiles_shader = Shader('terrain', 'common')
tests_shader = Shader('tests', 'common') tests_shader = Shader('tests', 'common')
sky_shader = Shader('sky') 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, TEXTURE_FORMAT_FLOAT_32, 256, 256, 0, TEXTURE_FLAG_MIN_LINEAR | TEXTURE_FLAG_MAG_LINEAR,
generated.packed_heights) generated.packed_heights)
normalmap = create_texture( normalmap = Texture(
TEXTURE_FORMAT_RGB10_A2, 256, 256, 0, TEXTURE_FLAG_MIN_LINEAR | TEXTURE_FLAG_MAG_LINEAR, TEXTURE_FORMAT_RGB10_A2, 256, 256, 0, TEXTURE_FLAG_MIN_LINEAR | TEXTURE_FLAG_MAG_LINEAR,
generated.packed_normals) generated.packed_normals)
@ -77,7 +101,7 @@ def main():
rock_model = archive.get_model('rock') rock_model = archive.get_model('rock')
mud_model = archive.get_model('mud') mud_model = archive.get_model('mud')
lava_model = archive.get_model('lava') 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 #TODO: generator & for real
vc = generated.volcano_c vc = generated.volcano_c
@ -108,7 +132,7 @@ def main():
model = mud_model model = mud_model
else: else:
model = rock_model 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_texture = archive.get_texture('tests')
tests_vertices = archive.get_vertices('tests') tests_vertices = archive.get_vertices('tests')
@ -118,153 +142,75 @@ def main():
tests_batch = Batch(tests_vertices, 3, 3, tests_batch = Batch(tests_vertices, 3, 3,
translation = PARAM_FORMAT_VEC3_FLOAT, translation = PARAM_FORMAT_VEC3_FLOAT,
orientation = PARAM_FORMAT_MAT3_INT10 | PARAM_FORMAT_NORMALIZE) orientation = PARAM_FORMAT_MAT3_INT10 | PARAM_FORMAT_NORMALIZE)
blob_spawn_translation = vec3(-100.0, -500.0, 0.0) 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_forward = math.vec3_normalize((sun_direction[0], sun_direction[1], 0.0))
blob_right = math.vec3_cross(blob_forward, math.vec3_up) blob_right = math.vec3_cross(blob_forward, math.vec3_up)
blob_id = blob_model.spawn(tests_batch, blob_spawn_orientation = mat3(vec3(*blob_right), vec3(*blob_forward), vec3_up)
blob_spawn_translation, 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_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_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_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') 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() scene = SceneNode(
camera.set_projection(proj_hfov, proj_ratio, proj_near_z, proj_far_z) FuncNode(update_camera, (mouse, camera, environment)),
environment = Environment() TextureNode((tiles_texture, heightmap, normalmap),
ShaderNode(tiles_shader,
blob_translation = tests_batch.translation[blob_id] InputNode(tiles_shader, camera),
cube_translation = tests_batch.translation[cube_id] InputNode(tiles_shader, environment),
cube_orientation = tests_batch.orientation[cube_id] DrawNode(tiles_batch)
clouds_orientation = tests_batch.orientation[clouds_id] )
),
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") print("Running... Ctrl+c to quit")
start_time = time.monotonic() time = Time()
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
try: try:
while True: while not keyboard.quit:
current_time = time.monotonic() - start_time
frame_begin = time.thread_time()
events.update() events.update()
if keyboard.quit:
break
begin_frame() begin_frame()
time.update()
camera_distance = mouse.wheel * 20.0 scene.draw(time)
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()
end_frame() end_frame()
swap_buffers(display) 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: except KeyboardInterrupt:
pass 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...") print("Quitting...")
del tests_batch del tests_batch
del terrain_batch del tiles_batch
destroy_texture(sea_polar_textures) del sky_triangles
destroy_texture(sea_detail_texture) del sea_polar_textures
destroy_texture(heightmap) del sea_detail_texture
destroy_texture(normalmap) del heightmap
destroy_triangles(sky_triangles) del normalmap
archive.destroy() archive.destroy()
del terrain_shader del tiles_shader
del tests_shader del tests_shader
del sky_shader del sky_shader
render_terminate() render_terminate()

29
game/inputs.py Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
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)

View File

@ -20,6 +20,7 @@ from pathlib import Path
import png import png
import engine import engine
from game.texture import Texture
VERTEX_SIZE = 20 VERTEX_SIZE = 20
VERTEX_FORMAT = engine.vertex_format( VERTEX_FORMAT = engine.vertex_format(
@ -133,7 +134,7 @@ class TextureData:
_write_blob(file, self.pixels) _write_blob(file, self.pixels)
def create_texture(data): 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: class VerticesData:
__slots__ = 'name', 'vertices', 'indices' __slots__ = 'name', 'vertices', 'indices'
@ -208,8 +209,6 @@ class Archive:
def destroy(self): def destroy(self):
for vertices in self.vertices_db.values(): for vertices in self.vertices_db.values():
engine.destroy_vertices(vertices) engine.destroy_vertices(vertices)
for texture in self.textures_db.values():
engine.destroy_texture(texture)
self.textures_db.clear() self.textures_db.clear()
self.vertices_db.clear() self.vertices_db.clear()
self.models_db.clear() self.models_db.clear()

95
game/scene.py Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
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)

View File

@ -17,10 +17,10 @@ from itertools import product
from math import tau, cos, sin, copysign from math import tau, cos, sin, copysign
from array import array from array import array
from engine import (create_texture, from engine import TEXTURE_FORMAT_RGB10_A2, TEXTURE_FORMAT_TYPECODE, TEXTURE_FLAG_MIN_LINEAR, TEXTURE_FLAG_MAG_LINEAR
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.math import vec3_scale, vec3_direction, vec3_cross, vec3_normal_rgb10a2
from game.texture import Texture
from game.resources import load_png from game.resources import load_png
_format = TEXTURE_FORMAT_RGB10_A2 _format = TEXTURE_FORMAT_RGB10_A2
@ -51,7 +51,7 @@ def load_polar_textures(paths, waves_height = 0.008):
_width, _height, normals = load_texture(path) _width, _height, normals = load_texture(path)
assert _width == width and _height == height assert _width == width and _height == height
data.extend(list(map(_conv, normals))) 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): def load_detail_texture(path, scale = 0.5, waves_height = 0.002):
width, height, data = load_png(path) 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])) return vec3_scale(n, copysign(1.0, n[2]))
normals = list(map(normal, product(range(height), range(width)), data)) normals = list(map(normal, product(range(height), range(width)), data))
data = array(_typecode, list(map(_conv, normals))) data = array(_typecode, list(map(_conv, normals)))
return create_texture(_format, width, height, 0, _flags, data) return Texture(_format, width, height, 0, _flags, data)

31
game/texture.py Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
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)

29
game/time.py Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
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

View File

@ -16,9 +16,23 @@
from itertools import chain from itertools import chain
from array import array from array import array
from math import cos, sin from math import cos, sin
from engine import create_triangles, draw_triangles, destroy_triangles
# TODO: with FOV class Triangles:
def sky_triangles(vsubdivs, distance, projection_ratio): __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 assert vsubdivs > 0
vertices = [] vertices = []
hsubdivs = round(vsubdivs * projection_ratio) hsubdivs = round(vsubdivs * projection_ratio)
@ -47,4 +61,4 @@ def sky_triangles(vsubdivs, distance, projection_ratio):
x2 += stepx x2 += stepx
y1 = y2 y1 = y2
y2 += stepy y2 += stepy
return array('f', chain.from_iterable(vertices)) super().__init__(array('f', chain.from_iterable(vertices)))