WIP sources and TODOs.

This commit is contained in:
2022-08-28 05:09:52 +02:00
parent 28ba30ab53
commit 94a150307b
52 changed files with 28435 additions and 0 deletions

0
game/__init__.py Normal file
View File

60
game/batch.py Normal file
View File

@ -0,0 +1,60 @@
# 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 array import array
from engine import (
INSTANCE_FLAG_SPAWNED, BATCH_MAX_SIZE, BATCH_ORIENTATION_FORMAT_NONE, create_batch, draw_batch, destroy_batch)
class Batch:
__slots__ = '_batch', 'max_size', 'flags', 'texlevels', 'meshes', 'translations', 'orientations'
def __init__(self, max_size, translation_format, orientation_format):
assert max_size <= BATCH_MAX_SIZE
self._batch = create_batch(max_size, translation_format, orientation_format)
self.max_size = max_size
self.flags = array('B')
self.texlevels = array('H')
self.meshes = array('I')
self.translations = array('f')
if orientation_format != BATCH_ORIENTATION_FORMAT_NONE:
self.orientations = array('f')
else:
self.orientations = None
def __del__(self):
destroy_batch(self._batch)
def append(self, flags, texlevel, mesh, translation, orientation):
assert len(translation) == 3
assert orientation is None or len(orientation) == 3
index = len(self.flags)
assert index < self.max_size
self.flags.append(flags | INSTANCE_FLAG_SPAWNED)
self.texlevels.append(texlevel)
self.meshes.append(mesh)
self.translations.extend(translation)
if self.orientations is not None:
self.orientations.extend(orientation)
return index
def set_translation(self, index, translation):
self.translations[index * 3 : index * 3 + 3] = translation
def set_orientation(self, index, orientation):
self.orientations[index * 3 : index * 3 + 3] = orientation
def draw(self):
draw_batch(self._batch, self.flags, self.texlevels, self.meshes, self.translations, self.orientations)

88
game/environment.py Normal file
View File

@ -0,0 +1,88 @@
# 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 math import radians, cos
from engine import *
from game.math import vec3_add, vec3_sub, vec3_scale, vec3_mul, vec3_dot
def _angles(start, end):
cmin = round(cos(radians(start)), 6)
cmax = round(cos(radians(end)), 6)
return (cmin, cmax, 1.0 / (cmax - cmin))
def _floats(a, b):
return (a, b - a)
def _colors(a, b):
return (a, vec3_sub(b, a))
_light_color = (
(_angles(180.0, 0.0), _colors((1.0, 1.0, 1.0), (1.0, 1.0, 1.0))),
)
_horizon_color = (
(_angles(180.0, 0.0), _colors((0.75, 0.75, 1.0), (0.75, 0.75, 1.0))),
)
_sky_color = (
(_angles(180.0, 0.0), _colors((0.0, 0.0, 0.5), (0.0, 0.0, 0.5))),
)
_sun_color = (
(_angles(180.0, 0.0), _colors((8.0, 8.0, 4.0), (8.0, 8.0, 4.0))),
)
_light_power = (
(_angles(180.0, 90.0), _floats(0.0, 0.0)),
(_angles( 90.0, 0.0), _floats(0.0, 1.0))
)
def _resolve(ranges, c):
for (cmin, cmax, crng), ab in ranges:
if c >= cmin and c <= cmax:
return ((c - cmin) * crng, ab)
return None
def _resolve_float(ranges, c):
w, (a, b) = _resolve(ranges, c)
return a + (b * w)
def _resolve_color(ranges, c):
w, (a, b) = _resolve(ranges, c)
return vec3_add(a, vec3_scale(b, w))
def resolve_inputs():
light_direction = resolve_input(b'u_light_direction')
light_color = resolve_input(b'u_light_color')
horizon_color = resolve_input(b'u_horizon_color')
sky_color = resolve_input(b'u_sky_color')
sun_color = resolve_input(b'u_sun_color')
return (light_direction, light_color, horizon_color, sky_color, sun_color)
def from_sun(sun_direction, sun_power):
c = vec3_dot(sun_direction, vec3_up)
light_power = _resolve_float(_light_power, c) * sun_power
light_color = vec3_scale(_resolve_color(_light_color, c), sun_power) # vec3_scale(_resolve_color(_light_color, c), light_power)
horizon_color = vec3_mul(_resolve_color(_horizon_color, c), light_color)
sky_color = vec3_mul(_resolve_color(_sky_color, c), light_color)
sun_color = _resolve_color(_sun_color, c)
return (vec3(sun_direction), vec3(light_color), vec3(horizon_color), vec3(sky_color), vec3(sun_color))
_modes = (INPUT_VIEW_ORIENTATION, INPUT_IDENTITY, INPUT_IDENTITY, INPUT_IDENTITY, INPUT_IDENTITY)
def set_inputs(inputs, values):
list(map(set_input_vec3, inputs, values, _modes))

251
game/game.py Normal file
View File

@ -0,0 +1,251 @@
# 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/>.
import time
from math import pi, tau, dist
from engine import *
from game import math, resources, batch, triangles, generator, environment, sea
def main():
print("Generating terrain...")
gen_begin = time.process_time()
generated = generator.Generator(256)
gen_end = time.process_time()
print("Done: ", round(gen_end - gen_begin, 2), "seconds")
print("Initializing...")
window = initialize(b'RK Island')
terrain_shader = load_shader(b'game/shaders/terrain')
sky_shader = load_shader(b'game/shaders/sky')
print("Loading resources...")
select_shader(terrain_shader)
archive = resources.RuntimeArchive.load('data/rk_island.rkar')
tiles_texture = archive.get_texture('tiles')
tiles_vertices = archive.get_vertices('tiles')
water = archive.get_model('water')
sand = archive.get_model('sand')
grass = archive.get_model('grass')
forest = archive.get_model('forest')
rock = archive.get_model('rock')
mud = archive.get_model('mud')
lava = archive.get_model('lava')
heightmap = create_texture(1, b'u_height_sampler',
TEXTURE_FORMAT_32F, 256, 256, 0, TEXTURE_FLAG_MIN_LINEAR | TEXTURE_FLAG_MAG_LINEAR,
generated.packed_heights)
normalmap = create_texture(2, b'u_normal_sampler',
TEXTURE_FORMAT_RGB10_A2, 256, 256, 0, TEXTURE_FLAG_MIN_LINEAR | TEXTURE_FLAG_MAG_LINEAR,
generated.packed_normals)
select_vertices(tiles_vertices)
terrain_batch = batch.Batch(
generated.size ** 2, BATCH_TRANSLATION_FORMAT_SHORT, BATCH_ORIENTATION_FORMAT_NONE)
unselect_vertices(tiles_vertices)
terrain_environment_inputs = environment.resolve_inputs()
tests_texture = archive.get_texture('tests')
tests_vertices = archive.get_vertices('tests')
blob = archive.get_model('blob')
cube = archive.get_model('cube')
clouds = archive.get_model('clouds')
select_vertices(tests_vertices)
tests_batch = batch.Batch(3, BATCH_TRANSLATION_FORMAT_FLOAT, BATCH_ORIENTATION_FORMAT_FLOAT)
unselect_vertices(tests_vertices)
unselect_shader(terrain_shader)
#TODO: generator & for real
print("Building tiles...")
vc = generated.volcano_c
vr = generated.volcano_r
for my, mx in generated.map_coords:
vd = dist((mx + 0.5, my + 0.5), vc)
nx, ny, nz, h = generated.unpack(my, mx)
r = generated.rivers[my * generated.size + mx]
if h == 0.0:
continue
if r > 0.0:
model = water
elif h < 2.0:
model = sand
elif h < 180:
if nz > 0.9:
if ny < -0.01 and nz > 0.93:
model = forest
else:
model = grass
else:
model = rock
elif vd < vr - 3.0 and nz > 0.999:
model = lava
elif vd < vr + 2.0:
model = mud
elif vd < vr + 6.0 and nz < 0.67:
model = mud
else:
model = rock
model.spawn(terrain_batch, (float(((mx - 128) * 8) + 4), float(((127 - my) * 8) + 4), 0.0), None)
blob_translation = vec3((-100.0, -500.0, 0.0))
cube_translation = vec3((100.0, -500.0, 0.0))
cube_orientation = vec3(vec3_forward)
clouds_orientation = vec3(vec3_forward)
blob_id = blob.spawn(tests_batch, blob_translation)
cube_id = cube.spawn(tests_batch, cube_translation, cube_orientation)
clouds_id = clouds.spawn(tests_batch, (0.0, 0.0, 32.0), clouds_orientation)
proj_hfov = pi * 0.25
proj_ratio = 16.0 / 9.0
proj_near_z = 8.0
proj_far_z = 3000.0
select_shader(sky_shader)
sky_environment_inputs = environment.resolve_inputs()
sea_phase = resolve_input(b'u_sea_phase')
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))
unselect_shader(sky_shader)
sun_direction = vec3(math.vec3_normalize((1.0, 0.0, 1.0)))
sun_power = 1.0
camera = vec3((0.0, -1200.0, 500.0))
lookat = vec3((0.0, 500.0, -500.0))
start_time = time.monotonic()
current_time = 0.0
up = vec3(vec3_up)
_rotation = mat3()
_camera = vec3()
_lookat = vec3()
_blob_translation = vec3()
_cube_translation = vec3()
_cube_orientation = vec3()
_clouds_orientation = vec3()
print("Running...")
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:
for x in range(10000):
current_time = time.monotonic() - start_time
begin_frame()
frame_begin = time.thread_time()
mat3_rotation(_rotation, up, (current_time * 0.05) % tau)
mat3_mul_vec3(_camera, _rotation, camera)
mat3_mul_vec3(_lookat, _rotation, lookat)
set_view(_camera, _lookat)
set_projection(proj_hfov, proj_ratio, proj_near_z, proj_far_z)
mat3_rotation(_rotation, up, (current_time * 0.21) % tau)
mat3_mul_vec3(_blob_translation, _rotation, blob_translation)
tests_batch.set_translation(blob_id, _blob_translation)
tests_batch.set_orientation(blob_id, vec3(math.vec3_normalize((sun_direction[0], sun_direction[1], 0.0))))
mat3_mul_vec3(_cube_translation, _rotation, cube_translation)
tests_batch.set_translation(cube_id, _cube_translation)
mat3_rotation(_rotation, up, (current_time * 0.43) % tau)
mat3_mul_vec3(_cube_orientation, _rotation, cube_orientation)
tests_batch.set_orientation(cube_id, _cube_orientation)
mat3_rotation(_rotation, up, (current_time * -0.037) % tau)
mat3_mul_vec3(_clouds_orientation, _rotation, clouds_orientation)
tests_batch.set_orientation(clouds_id, _clouds_orientation)
environment_values = environment.from_sun(sun_direction, sun_power)
select_shader(terrain_shader)
select_texture(heightmap)
select_texture(normalmap)
environment.set_inputs(terrain_environment_inputs, environment_values)
select_texture(tiles_texture)
select_vertices(tiles_vertices)
draw_begin = time.thread_time()
terrain_batch.draw()
draw_end = time.thread_time()
unselect_vertices(tiles_vertices)
unselect_texture(tiles_texture)
select_texture(tests_texture)
select_vertices(tests_vertices)
tests_batch.draw()
unselect_vertices(tests_vertices)
unselect_texture(tests_texture)
unselect_texture(normalmap)
unselect_texture(heightmap)
unselect_shader(terrain_shader)
select_shader(sky_shader)
environment.set_inputs(sky_environment_inputs, environment_values)
set_input_float(sea_phase, (current_time * 0.023) % 1.0)
select_texture(sea_polar_textures)
select_texture(sea_detail_texture)
draw_triangles(sky_triangles)
unselect_texture(sea_detail_texture)
unselect_texture(sea_polar_textures)
unselect_shader(sky_shader)
frame_end = time.thread_time()
end_frame()
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
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
# camera = vec3((0.0, -1200.0, 500.0))
# lookat = vec3((0.0, 500.0, -500.0))
# for x in range(10000)
# current_time = 0
# Draw * 10000 : min = 0.11 , max = 1.52 , avg = 0.18 ms
# Draw * 10000 : min = 0.12 , max = 1.74 , avg = 0.18 ms
# Draw * 10000 : min = 0.12 , max = 1.92 , avg = 0.18 ms
print("Quitting...")
del tests_batch
del terrain_batch
destroy_texture(sea_polar_textures)
destroy_texture(sea_detail_texture)
destroy_texture(heightmap)
destroy_triangles(sky_triangles)
archive.destroy()
destroy_shader(terrain_shader)
destroy_shader(sky_shader)
terminate()

223
game/generator.py Normal file
View File

@ -0,0 +1,223 @@
# 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 os import urandom
from operator import mul
from itertools import product, combinations, chain, starmap, repeat
from math import prod, sqrt, floor, ceil, dist, hypot, pi, cos, sin
from random import seed as set_seed, random, uniform, triangular, sample
from array import array
from struct import unpack
from game.math import vec3_normal_rgb10a2
class Generator:
def __init__(self, size):
seed = unpack('I', urandom(4))[0] # 666 # 2749145609 #
print("Seed =", seed)
set_seed(seed)
self.size = size
self.map_coords = tuple(product(range(size), repeat = 2))
self.field_coords = tuple(starmap(lambda y, x: (y + 0.5, x + 0.5), self.map_coords))
self.generate_field()
self.generate_heights()
self.generate_rivers()
self.smooth_heights()
self.generate_normals()
self.smooth_normals()
self.pack()
def weighted_samples(self, radius):
samples = []
smin = floor(radius)
tw = 0.0
for y, x in product(range(-smin, smin + 1), repeat = 2):
w = 1.0 - hypot(y, x) / radius
if w > 0.0:
tw += w
samples.append([y, x, w])
iw = 1.0 / tw
for sample in samples:
sample[2] *= iw
return samples
def generate_field(self):
half = self.size // 2
vhalf = (half, half)
rmin = self.size / 16
rmax = self.size / 8
vrad = self.size / 32
cones = [(vhalf, vrad, 0.2)]
for _ in range(180):
r = uniform(rmin, rmax)
d = uniform(r, half + r)
a = uniform(-pi, pi)
p = d / half
cones.append(((d * cos(a) + half, d * sin(a) + half), r, p))
def height(_position):
def influence(_center, _radius, _power):
d = dist(_position, _center)
return ((_radius ** 2.0) / (d ** 2.0) if d > _radius else (d ** 2.0) / (_radius ** 2.0)) * _power
return sum(starmap(influence, cones))
heights = list(map(height, self.field_coords))
heights = list(map(mul, heights, repeat(1.0 / max(heights))))
self.volcano_c = vhalf
self.volcano_r = vrad
self.cones = cones
self.heights = heights
def generate_heights(self):
size = self.size
half = size // 2
vhalf = (half, half)
heights = self.heights
def shape(_position, _height):
d = dist(vhalf, _position) / half
return max(0.0, _height - d) ** 2.5
heights = list(map(shape, self.field_coords, heights))
vy, vx = self.volcano_c
vrad = self.volcano_r
vmin = ceil(vrad)
vmax = vrad - 2.0
addrs = []
hmin = 1.0
for y, x in product(range(-vmin, vmin), repeat = 2):
if hypot(y + 0.5, x + 0.5) < vmax:
addr = (y + vy) * size + (x + vx)
addrs.append(addr)
hmin = min(hmin, heights[addr])
hmin -= 8.0 / 255.0
for addr in addrs:
heights[addr] = hmin
self.heights = list(map(mul, heights, repeat(255.0 / max(heights))))
def generate_rivers(self):
size = self.size
heights = self.heights
cw = 1.0 / sqrt(2.0)
offsets = (
# y x w y x w y x w
(-1, -1, cw), (-1, 0, 1.0), (-1, 1, cw),
( 0, -1, 1.0), ( 0, 1, 1.0),
( 1, -1, cw), ( 1, 0, 1.0), ( 1, 1, cw))
def river(_ry, _rx):
path = []
mins = []
minh = heights[_ry * size + _rx]
flowing = True
while flowing:
flowing = False
path.append((_ry, _rx))
mins.append(minh)
rh = heights[_ry * size + _rx]
if rh == 0.0:
break
for by, bx in reversed(path):
ns = []
for oy, ox, w in offsets:
ny = (_ry + oy) % size
nx = (_rx + ox) % size
if (ny, nx) not in path:
nh = heights[ny * size + nx]
ns.append(((nh - rh) * w, min(minh, nh), ny, nx))
if ns:
_, minh, _ry, _rx = min(ns)
flowing = True
break
return [(rx, ry, rh) for (rx, ry), rh in zip(path, mins)]
def center(_cy, _cx):
return (heights[_cy * size + _cx], _cy, _cx)
centers = sorted(
[center(round(cy) % size, round(cx) % size) for (cy, cx), _, _ in self.cones[1:]],
reverse = True)
sources = [(sy, sx) for _, sy, sx in centers[:32]]
paths = []
for sy, sx in sources:
paths.append(river(sy, sx))
rivers = [0.0 for _ in range(size ** 2)]
path = max(paths, key = len)
for ry, rx, minh in path:
minh = max(0.0, minh - 1.0)
addr = ry * size + rx
heights[addr] = minh
for oy, ox, _ in offsets:
heights[(ry + oy) * size + (rx + ox)] = minh
rivers[addr] = 1.0
self.rivers = rivers
def smooth_heights(self):
size = self.size
heights = self.heights
samples = self.weighted_samples(2.0)
def smooth(_y, _x):
return sum(heights[((_y + dy) % size) * size + ((_x + dx) % size)] * w for dy, dx, w in samples)
heights = list(starmap(smooth, self.map_coords))
self.heights = list(map(mul, heights, repeat(255.0 / max(heights))))
def generate_normals(self):
size = self.size
heights = self.heights
def normal(_y, _x):
height = heights[_y * size + _x]
def direction(_dy, _dx):
dz = (heights[((_y - _dy) % size) * size + ((_x + _dx) % size)] - height) / 8.0
di = 1.0 / sqrt(_dx ** 2.0 + _dy ** 2.0 + dz ** 2.0) #OPTIM: dx² + dy² = 1.0
return (_dx * di, _dy * di, dz * di)
hx, hy, hz = direction(0, 1)
vx, vy, vz = direction(1, 0)
return (hy * vz - hz * vy, hz * vx - hx * vz, hx * vy - hy * vx)
self.normals = list(starmap(normal, self.map_coords))
def smooth_normals(self):
size = self.size
normals = self.normals
samples = self.weighted_samples(2.0)
def smooth(_y, _x):
tx = ty = tz = 0.0
for dy, dx, w in samples:
nx, ny, nz = normals[((_y + dy) % size) * size + ((_x + dx) % size)]
tx += nx * w
ty += ny * w
tz += nz * w
di = 1.0 / sqrt(tx ** 2.0 + ty ** 2.0 + tz ** 2.0)
return (tx * di, ty * di, tz * di)
self.normals = list(starmap(smooth, self.map_coords))
def pack(self):
self.packed_heights = array('f', self.heights)
self.packed_normals = array('I', tuple(map(vec3_normal_rgb10a2, self.normals)))
def unpack(self, y, x):
addr = y * self.size + x
nx, ny, nz = self.normals[addr]
return (nx, ny, nz, self.heights[addr])

71
game/math.py Normal file
View File

@ -0,0 +1,71 @@
# 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 math import hypot
def vec3_add(a, b):
return (a[0] + b[0], a[1] + b[1], a[2] + b[2])
def vec3_sub(a, b):
return (a[0] - b[0], a[1] - b[1], a[2] - b[2])
def vec3_normalize(v):
l = hypot(*v)
return (v[0] / l, v[1] / l, v[2] / l)
def vec3_direction(a, b):
v = (b[0] - a[0], b[1] - a[1], b[2] - a[2])
l = hypot(*v)
return (v[0] / l, v[1] / l, v[2] / l)
def vec3_scale(v, s):
return (v[0] * s, v[1] * s, v[2] * s)
def vec3_mul(a, b):
return (a[0] * b[0], a[1] * b[1], a[2] * b[2])
def vec3_dot(a, b):
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
def vec3_cross(a, b):
return (a[1] * b[2] - a[2] * b[1], -(a[0] * b[2] - a[2] * b[0]), a[0] * b[1] - a[1] * b[0])
# Texture formats
def float_s8(f):
return round((f ** (1.0 / 2.2)) * 255.0) #TODO: more accurate
def vec3_srgb8a8(v):
p = 1.0 / 2.2 #TODO: more accurate
return (round((v[0] ** p) * 255.0), round((v[1] ** p) * 255.0), round((v[2] ** p) * 255.0), 255)
def vec3_rgba8(v):
return (round(v[0] * 255.0), round(v[1] * 255.0), round(v[2] * 255.0), 255)
def vec3_normal_rgba8(n):
l = max(abs(n[0]), abs(n[1]), abs(n[2]))
return (
round((0.5 + (n[0] / l) * 0.5) * 255.0),
round((0.5 + (n[1] / l) * 0.5) * 255.0),
round((n[2] / l) * 255.0), 0)
def vec3_normal_rgb10a2(n):
l = max(abs(n[0]), abs(n[1]), abs(n[2]))
return \
round((0.5 + (n[0] / l) * 0.5) * 1023.0) | \
round((0.5 + (n[1] / l) * 0.5) * 1023.0) << 10 | \
round((n[2] / l) * 1023.0) << 20
# Vertex formats

271
game/obj2rkar.py Normal file
View File

@ -0,0 +1,271 @@
# 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/>.
import sys
import struct
from array import array
from pathlib import Path
from itertools import starmap, chain, repeat
from engine import *
from game.math import float_s8, vec3_srgb8a8
from game.resources import load_png, TextureData, VerticesData, ModelData, Archive
_texture_flags = TEXTURE_FLAG_MIPMAPS | TEXTURE_FLAG_MIN_LINEAR | TEXTURE_FLAG_MAG_NEAREST
class ObjArchive(Archive):
def __init__(self):
super().__init__()
def import_hacks(self, path):
print("Importing hacks", path)
hacks = {}
for line in open(path):
data = line.split()
if not data:
continue
name = data.pop(0)
if name == '#':
continue
assert len(data) == 4 and data[0] == '=' and data[2] == 'mesh'
alias = data[1]
mesh = data[3]
assert name not in hacks.keys()
hacks[name] = (alias, mesh)
return hacks
def import_mtl(self, mtlpath):
def load_value(x):
x = round(float(x), 6)
if x == -0.0:
x = 0.0
return x
size = None
def load_texture(pngpath, nchannels):
width, height, pixels = load_png(pngpath)
assert pixels.typecode == 'B'
assert (size is None or size == (width, height))
assert len(pixels) == width * height * nchannels
return ((width, height), pixels)
print("Importing mtl", mtlpath)
texture = array('B')
texlevels = {}
name = ''
texlevel = 0
color = (0, 0, 0, 0)
metallic = 0
specular = 0
roughness = 0
bump = 0
def finish(_texture, _color, _metallic, _specular, _roughness, _bump):
width, height = size
if not isinstance(_color, array):
_color = array('B', _color * (width * height))
_texture.extend(_color)
if not isinstance(_metallic, array):
_metallic = repeat(_metallic, width * height)
if not isinstance(_specular, array):
_specular = repeat(_specular, width * height)
if not isinstance(_roughness, array):
_roughness = repeat(_roughness, width * height)
if not isinstance(_bump, array):
_bump = repeat(_bump, width * height)
_texture.extend(chain.from_iterable(zip(_metallic, _specular, _roughness, _bump)))
for line in open(mtlpath):
data = line.split()
if not data:
continue
code = data.pop(0)
if code == '#':
continue
elif code == 'newmtl':
if name:
if size is None:
size = (1, 1)
finish(texture, color, metallic, specular, roughness, bump)
texlevels[name] = texlevel
texlevel += 2
name = data[0]
print("Importing material", name)
color = (0, 0, 0, 0)
metallic = 0
specular = 0
roughness = 0
bump = 0
elif code == 'Kd': # color
color = vec3_srgb8a8(tuple(map(load_value, data)))
assert len(color) == 4
elif code == 'Ni': # ior -> metallic
metallic = float_s8(load_value(data[0]))
elif code == 'Ks': # specular
specular = float_s8(load_value(data[0]))
elif code == 'Ns': # roughness
roughness = float_s8(1.0 - load_value(data[0]) / 1000.0)
elif code == 'map_Kd':
pngpath = mtlpath.parent / data[0]
print("Importing color texture", pngpath)
size, color = load_texture(pngpath, 4)
pngpath = pngpath.parent / (pngpath.stem + '_b.png')
if pngpath.exists():
print("Importing bump channel", pngpath)
size, bump = load_texture(pngpath, 1)
elif code == 'map_Ni':
pngpath = mtlpath.parent / data[0]
print("Importing metallic channel", pngpath)
size, metallic = load_texture(pngpath, 1)
elif code == 'map_Ks':
pngpath = mtlpath.parent / data[0]
print("Importing specular channel", pngpath)
size, specular = load_texture(pngpath, 1)
elif code == 'map_Ns':
pngpath = mtlpath.parent / data[0]
print("Importing roughness channel", pngpath)
size, roughness = load_texture(pngpath, 1)
if name:
if size is None:
size = (1, 1)
finish(texture, color, metallic, specular, roughness, bump)
texlevels[name] = texlevel
texlevel += 2
name = str(mtlpath.stem)
assert texlevel < 255
assert name not in self.textures_db.keys()
print("Storing texture", name)
width, height = size
self.textures_db[name] = TextureData(
name, TEXTURE_FORMAT_SRGB8_A8, width, height, texlevel, _texture_flags, texture)
return texlevels
def import_obj(self, objpath):
def load_coord(x):
x = round(float(x), 6)
if x == -0.0:
x = 0.0
return x
def load_index(x):
return int(x) - 1
print("Importing obj", objpath)
texlevels = {}
name = ''
texlevel = 0
mesh = []
positions = []
normals = []
texcoords = []
objects = {}
for line in open(objpath):
data = line.split()
if not data:
continue
code = data.pop(0)
if code == '#':
continue
elif code == 'mtllib':
texlevels = self.import_mtl(objpath.parent / data[0])
elif code == 'o':
if name:
assert mesh
objects[name] = (texlevel, mesh)
name = data[0]
print("Importing object", name)
mesh = []
elif code == 'usemtl':
texlevel = texlevels[data[0]]
elif code == 'v':
position = tuple(map(load_coord, data))
assert len(position) == 3
positions.append(position)
elif code == 'vn':
normal = tuple(map(load_coord, data))
assert len(normal) == 3
normals.append(normal)
elif code == 'vt':
texcoord = tuple(map(load_coord, data))
assert len(texcoord) == 2
texcoords.append((texcoord[0], 1.0 - texcoord[1]))
elif code == 'f':
indices = tuple(map(lambda x: tuple(map(load_index, x.split('/'))), data))
assert len(indices) == 3
assert len(indices[0]) == 3
assert len(indices[1]) == 3
assert len(indices[2]) == 3
triangle = tuple(map(lambda x: positions[x[0]] + normals[x[2]] + texcoords[x[1]], indices))
assert len(triangle) == 3 and len(set(triangle)) == 3
mesh.append(triangle)
if name:
assert mesh
objects[name] = (texlevel, mesh)
vertices = set()
for _, mesh in objects.values():
vertices |= frozenset(chain.from_iterable(mesh))
vertices = tuple(vertices)
indices = []
models = {}
for name, (texlevel, mesh) in sorted(objects.items()):
if name[0] == '_':
print(name, ": texlevel =", texlevel)
models[name] = (texlevel, -1, -1)
else:
offset = len(indices)
assert offset < 65536
indices.extend(map(vertices.index, chain.from_iterable(mesh)))
count = len(indices) - offset
assert count % 3 == 0
count //= 3
assert count < 65536
print(name, ": texlevel =", texlevel, "offset =", offset, "count =", count)
models[name] = (texlevel, offset, count)
name = str(objpath.stem)
assert name not in self.vertices_db.keys()
#TODO: move to math
def pack_10(_x):
return round(_x * (512.0 if _x < 0.0 else 511.0)) & 1023
def pack_vertex(_px, _py, _pz, _nx, _ny, _nz, _s, _t):
n = (pack_10(_nx) << 20) | (pack_10(_ny) << 10) | (pack_10(_nz) << 0)
s = max(0, min(65535, round(_s * 65535.0)))
t = max(0, min(65535, round(_t * 65535.0)))
return struct.pack('fffIHH', _px, _py, _pz, n, s, t)
self.vertices_db[name] = VerticesData(name,
array('B', b''.join(starmap(pack_vertex, vertices))),
array('H', indices))
path = objpath.parent / (objpath.stem + '.hacks')
if path.exists():
hacks = self.import_hacks(path)
for name, (alias, mesh) in hacks.items():
print("Hacking", name, "from", alias, "with mesh", mesh)
assert name not in models.keys() and alias in models.keys() and mesh in models.keys()
texlevel, _, _ = models[alias]
_, offset, count = models[mesh]
models[name] = (texlevel, offset, count)
for name, (texlevel, offset, count) in sorted(models.items()):
if name[0] != '_':
print("Storing", name)
assert name not in self.models_db.keys()
self.models_db[name] = ModelData(
name, INSTANCE_FLAG_VISIBLE, texlevel, offset | count << 16)
if __name__ == '__main__':
if len(sys.argv) < 3:
print("Usage: python3 -m tools.obj2rkar output.rkar input.obj ...")
sys.exit(2)
importer = ObjArchive()
for argv in sys.argv[2:]:
objpath = Path(argv)
importer.import_obj(objpath)
outpath = Path(sys.argv[1])
print("Exporting", outpath)
importer.save(outpath)
print("Done.")

292
game/resources.py Normal file
View File

@ -0,0 +1,292 @@
# 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/>.
import struct
from array import array
from pathlib import Path
import png
import engine
VERTEX_SIZE = 20
VERTEX_FORMAT = engine.vertex_format(
engine.VERTEX_FORMAT_VEC3_FLOAT,
engine.VERTEX_FORMAT_VEC3_INT10,
engine.VERTEX_FORMAT_VEC2_USHORT)
def load_png(path):
width, height, data, _ = png.Reader(filename = path).read_flat()
return (width, height, data)
def _read_magic(file, magic):
assert magic
if file.read(len(magic)) != magic:
raise RuntimeError("Archive magic mismatch!", magic)
def _write_magic(file, magic):
assert magic
size = file.write(magic)
assert size == len(magic)
def _read_struct(file, format):
assert format
size = struct.calcsize(format)
assert size
data = file.read(size)
assert len(data) == size
_read_magic(file, b'RK')
return struct.unpack(format, data)
def _write_struct(file, format, elems):
assert format
data = struct.pack(format, *elems)
size = file.write(data)
assert size == len(data)
_write_magic(file, b'RK')
def _read_string(file):
length, = _read_struct(file, 'B')
assert length
data = file.read(length)
assert len(data) == length
_read_magic(file, b'RK')
return str(data, encoding='ascii')
def _write_string(file, string):
data = bytes(string, encoding='ascii')
assert data and len(data) < 256
_write_struct(file, 'B', (len(data),))
size = file.write(data)
assert size == len(data)
_write_magic(file, b'RK')
def _read_array(file, format, length):
assert format
data = array(format)
data.fromfile(file, length)
assert len(data) == length
_read_magic(file, b'RK')
return data
def _write_array(file, array):
assert array
array.tofile(file)
_write_magic(file, b'RK')
def _read_blob(file):
typecode, length = _read_struct(file, 'II')
typecode = chr(typecode)
assert typecode, length
data = array(typecode)
data.fromfile(file, length)
assert len(data) == length
_read_magic(file, b'RK')
return data
def _write_blob(file, array):
assert array
_write_struct(file, 'II', (ord(array.typecode), len(array)))
array.tofile(file)
_write_magic(file, b'RK')
class TextureData:
__slots__ = 'name', 'format', 'width', 'height', 'nlevels', 'flags', 'pixels'
def __init__(self, name, format, width, height, nlevels, flags, pixels):
assert pixels.typecode == engine.TEXTURE_FORMAT_TYPECODE[format]
assert len(pixels) == width * height * max(1, nlevels) * engine.TEXTURE_FORMAT_NELEMS[format]
self.name = name
self.format = format
self.width = width
self.height = height
self.nlevels = nlevels
self.flags = flags
self.pixels = pixels
@classmethod
def from_archive(cls, file):
_read_magic(file, b'TX')
name = _read_string(file)
format, width, height, nlevels, flags = _read_struct(file, 'IIIII')
pixels = _read_blob(file)
assert pixels.typecode == engine.TEXTURE_FORMAT_TYPECODE[format]
assert len(pixels) == width * height * max(1, nlevels) * engine.TEXTURE_FORMAT_NELEMS[format]
return cls(name, format, width, height, nlevels, flags, pixels)
def to_archive(self, file):
_write_magic(file, b'TX')
_write_string(file, self.name)
_write_struct(file, 'IIIII', (self.format, self.width, self.height, self.nlevels, self.flags))
_write_blob(file, self.pixels)
def create_texture(slot, input, data):
return engine.create_texture(
slot, input, data.format, data.width, data.height, data.nlevels, data.flags, data.pixels)
class VerticesData:
__slots__ = 'name', 'vertices', 'indices'
def __init__(self, name, vertices, indices):
if len(vertices) % VERTEX_SIZE != 0:
raise RuntimeError("Vertex format mismatch!")
self.name = name
self.vertices = vertices
self.indices = indices
@classmethod
def from_archive(cls, file):
_read_magic(file, b'VT')
name = _read_string(file)
nvertices, nindices = _read_struct(file, 'II')
vertices = _read_array(file, 'B', nvertices * VERTEX_SIZE)
indices = _read_array(file, 'H', nindices)
return cls(name, vertices, indices)
def to_archive(self, file):
_write_magic(file, b'VT')
_write_string(file, self.name)
_write_struct(file, 'II', (len(self.vertices) // VERTEX_SIZE, len(self.indices)))
_write_array(file, self.vertices)
_write_array(file, self.indices)
def create_vertices(data):
return engine.create_vertices(VERTEX_FORMAT, len(data.vertices) // VERTEX_SIZE, data.vertices, data.indices)
class ModelData:
__slots__ = 'name', 'flags', 'texlevel', 'mesh'
def __init__(self, name, flags, texlevel, mesh):
self.name = name
self.flags = flags
self.texlevel = texlevel
self.mesh = mesh
@classmethod
def from_archive(cls, file):
_read_magic(file, b'MD')
name = _read_string(file)
flags, texlevel, mesh = _read_struct(file, 'BHI')
return ModelData(name, flags, texlevel, mesh)
def to_archive(self, file):
_write_magic(file, b'MD')
_write_string(file, self.name)
_write_struct(file, 'BHI', (self.flags, self.texlevel, self.mesh))
class Model:
__slots__ = 'flags', 'texlevel', 'mesh'
def __init__(self, flags, texlevel, mesh):
self.flags = flags
self.texlevel = texlevel
self.mesh = mesh
def spawn(self, batch, translation = engine.vec3_zero, orientation = engine.vec3_forward):
return batch.append(self.flags, self.texlevel, self.mesh, translation, orientation)
def create_model(data):
return Model(data.flags, data.texlevel, data.mesh)
class Archive:
__slots__ = 'textures_db', 'vertices_db', 'models_db'
def __init__(self, textures_db = None, vertices_db = None, models_db = None):
self.textures_db = textures_db or {}
self.vertices_db = vertices_db or {}
self.models_db = models_db or {}
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()
def get_texture(self, name):
return self.textures_db[name]
def get_vertices(self, name):
return self.vertices_db[name]
def get_model(self, name):
return self.models_db[name]
@classmethod
def _new_texture(cls, data):
return data
@classmethod
def _new_vertices(cls, data):
return data
@classmethod
def _new_model(cls, data):
return data
@classmethod
def from_archive(cls, file):
textures_db = {}
vertices_db = {}
models_db = {}
_read_magic(file, b'RKAR')
ntextures, nvertices, nmodels = _read_struct(file, 'III')
for _ in range(ntextures):
data = TextureData.from_archive(file)
textures_db[data.name] = cls._new_texture(data)
for _ in range(nvertices):
data = VerticesData.from_archive(file)
vertices_db[data.name] = cls._new_vertices(data)
for _ in range(nmodels):
data = ModelData.from_archive(file)
models_db[data.name] = cls._new_model(data)
return cls(textures_db, vertices_db, models_db)
@classmethod
def load(cls, filename):
file = open(Path(filename), 'rb')
archive = cls.from_archive(file)
file.close()
return archive
def to_archive(self, file):
_write_magic(file, b'RKAR')
_write_struct(file, 'III', (len(self.textures_db), len(self.vertices_db), len(self.models_db)))
for _, data in self.textures_db.items():
data.to_archive(file)
for _, data in self.vertices_db.items():
data.to_archive(file)
for _, data in self.models_db.items():
data.to_archive(file)
def save(self, filename):
file = open(Path(filename), 'wb')
self.to_archive(file)
file.close()
class RuntimeArchive(Archive):
@classmethod
def _new_texture(cls, data):
return create_texture(0, b'u_texture_sampler', data)
@classmethod
def _new_vertices(cls, data):
return create_vertices(data)
@classmethod
def _new_model(cls, data):
return create_model(data)

71
game/sea.py Normal file
View File

@ -0,0 +1,71 @@
# 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 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 game.math import vec3_scale, vec3_direction, vec3_cross, vec3_normal_rgb10a2
from game.resources import load_png
_format = TEXTURE_FORMAT_RGB10_A2
_typecode = TEXTURE_FORMAT_TYPECODE[TEXTURE_FORMAT_RGB10_A2]
_conv = vec3_normal_rgb10a2
_flags = TEXTURE_FLAG_MIN_LINEAR | TEXTURE_FLAG_MAG_LINEAR
def load_polar_textures(paths, waves_height = 0.008):
def load_texture(_path):
width, height, data = load_png(_path)
assert data.typecode == 'H'
assert len(data) == width * height
def polar(_y, _x, _h):
d = 1.0 + (_y / height)
a = (_x / width) * tau
return (d * sin(a), d * cos(a), (_h / 65535.0) * waves_height)
def normal(_pos, _h):
y, x = _pos
o = polar(y, x, _h)
n = vec3_cross(
vec3_direction(o, polar(y, x + 1, data[y * width + ((x + 1) % width)])),
vec3_direction(o, polar(y + 1, x, data[((y + 1) % height) * width + x])))
return vec3_scale(n, copysign(1.0, n[2]))
return (width, height, list(map(normal, product(range(height), range(width)), data)))
width, height, normals = load_texture(paths[0])
data = array(_typecode, list(map(_conv, normals)))
for path in paths[1:]:
_width, _height, normals = load_texture(path)
assert _width == width and _height == height
data.extend(list(map(_conv, normals)))
return create_texture(0, b'u_sea_polar_sampler', _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)
assert data.typecode == 'H'
assert len(data) == width * height
def coord(_y, _x, _h):
return ((_x / width) * scale, (_y / height) * scale, (_h / 65535.0) * waves_height)
def normal(_pos, _h):
y, x = _pos
o = coord(y, x, _h)
n = vec3_cross(
vec3_direction(o, coord(y, x + 1, data[y * width + ((x + 1) % width)])),
vec3_direction(o, coord(y + 1, x, data[((y + 1) % height) * width + x])))
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(1, b'u_sea_detail_sampler', _format, width, height, 0, _flags, data)

View File

@ -0,0 +1,90 @@
// 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/>.
#version 320 es
precision highp float;
in vec3 v_position;
uniform mat4 u_view_km; // world space -> view space, unit = km
uniform vec3 u_light_direction; // view space (-direction_x, -direction_y, -direction_z)
uniform vec3 u_light_color;
uniform vec3 u_horizon_color;
uniform vec3 u_sky_color;
uniform vec3 u_sun_color;
uniform float u_sea_phase;
#define u_right u_view_km[0].xyz
#define u_forward u_view_km[1].xyz
#define u_up u_view_km[2].xyz
#define u_origin u_view_km[3].xyz
uniform highp sampler2DArray u_sea_polar_sampler;
uniform highp sampler2D u_sea_detail_sampler;
const float c_sea_radius = 637.1;
const float c_sea_radius_sq = c_sea_radius * c_sea_radius;
const float c_sky_radius = c_sea_radius + 10.0;
const vec3 c_normal_scale = vec3(2.0, 2.0, 1.0);
const vec3 c_normal_shift = vec3(-1.0, -1.0, 0.0);
const float c_detail_scale = 2.0;
layout(location = 0) out vec4 o_color;
vec3 sky(in vec3 ray_direction) {
float d = max(0.0, dot(ray_direction, u_light_direction));
return mix(u_horizon_color, u_sky_color, max(0.0, dot(ray_direction, u_up))) + u_sun_color * pow(d, 1000.0);
}
void main(void) {
vec3 direction = normalize(v_position);
vec3 earth_center = u_origin - u_up * c_sea_radius;
float p_dist = dot(direction, earth_center);
vec3 pc = earth_center - direction * p_dist;
if (p_dist <= 0.0 || dot(pc, pc) >= c_sea_radius_sq) {
// sky
o_color = vec4(sky(direction), 1.0);
} else {
// sea
vec3 sea_position = direction * (p_dist - sqrt(c_sea_radius_sq - dot(pc, pc))) - u_origin;
vec3 sea_direction = normalize(sea_position);
//TODO: vec2
float s = dot(u_forward, sea_direction);
if (dot(u_right, sea_direction) > 0.0) {
// [1.0 -1.0] -> [0.0 0.5]
s = (1.0 - s) * 0.25;
} else {
// [-1.0 1.0] -> [0.5 1.0] -> [0.0 0.5] + 0.5
s = (1.0 + s) * 0.25 + 0.5;
}
float t = sqrt(length(sea_position)); //TODO: more accurate
vec3 sea_polar1 = normalize(
c_normal_shift + texture(u_sea_polar_sampler, vec3(s, t + u_sea_phase, 0.0)).xyz * c_normal_scale);
vec3 sea_polar2 = normalize(
c_normal_shift + texture(u_sea_polar_sampler, vec3(s, t - u_sea_phase, 1.0)).xyz * c_normal_scale);
//TODO: vec2
s = (u_sea_phase + dot(sea_position, u_right)) * c_detail_scale;
t = (u_sea_phase + dot(sea_position, u_forward)) * c_detail_scale;
vec3 sea_detail = normalize(c_normal_shift + texture(u_sea_detail_sampler, vec2(s, t)).xyz * c_normal_scale);
//TODO: better blending, with earth normal
vec4 normal = u_view_km * vec4(normalize(sea_polar1 + sea_polar2 + sea_detail), 0.0);
float d = max(0.0, dot(normal.xyz, u_light_direction));
s = pow(max(0.0, dot(normal.xyz, normalize(u_light_direction - direction))), 500.0) * step(0.0, d);
o_color = vec4(
u_sky_color * d + //TODO: sea color
u_light_color * s +
sky(reflect(direction, normal.xyz)), 1.0);
}
}

View File

@ -0,0 +1,28 @@
// 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/>.
#version 320 es
precision highp float;
layout(location = 0) in vec3 a_position; // view space
uniform mat4 u_projection; // view space -> screen space
out vec3 v_position; // view space
void main(void) {
v_position = a_position;
gl_Position = u_projection * vec4(a_position, 1.0);
}

View File

@ -0,0 +1,56 @@
// 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/>.
#version 320 es
precision highp float;
in vec3 v_position; // view space
in vec3 v_normal; // view space
in vec4 v_terrain_normal; // view space (x, y, z, weight)
in vec4 v_texcoord; // texture space (s, t, pixel_level, material_level)
#define v_weight v_terrain_normal.w
uniform vec3 u_light_direction; // view space (-direction_x, -direction_y, -direction_z)
uniform vec3 u_light_color; // (color.r * power, color.g * power, color.b * power)
uniform highp sampler2DArray u_texture_sampler;
layout(location = 0) out vec4 o_color;
void main(void) {
vec4 pixel = texture(u_texture_sampler, v_texcoord.stp);
vec4 material = texture(u_texture_sampler, v_texcoord.stq);
#define m_metallic material.x
#define m_specular material.y
#define m_roughness material.z
vec3 normal = normalize(v_normal);
vec3 eye_dir = -normalize(v_position);
float d = dot(normal, u_light_direction);
float halfd = 0.5 + d * 0.5;
float td = dot(normalize(v_terrain_normal.xyz), u_light_direction);
float diffuse = halfd * mix(halfd, 0.5 + td * 0.5, v_weight) * (1.0 - m_metallic);
float s = max(0.0, dot(normal, normalize(u_light_direction + eye_dir)));
float shininess = 1.0 + pow(1.0 - m_roughness, 2.0) * 999.0;
float stepd = step(0.0, d);
float specular = pow(s, shininess) * mix(stepd, stepd * max(0.0, td), v_weight);
vec3 color = pixel.rgb * u_light_color;
o_color = vec4(
vec3(
color * diffuse +
color * (specular * m_metallic) +
u_light_color * (specular * m_specular)),
pixel.a);
}

View File

@ -0,0 +1,63 @@
// 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/>.
#version 320 es
precision highp float;
layout(location = 0) in vec3 a_position; // model space
layout(location = 1) in vec3 a_normal; // model space
layout(location = 2) in vec2 a_texcoord; // texture space
layout(location = 3) in vec4 i_translation; // per mesh, model space -> world space (x, y, z, texlevel)
layout(location = 4) in vec3 i_orientation; // per mesh, model space -> world space
#define i_texlevel i_translation.w
uniform mat4 u_view; // world space -> view space
uniform mat4 u_projection; // view space -> screen space
uniform highp sampler2D u_height_sampler;
uniform highp sampler2D u_normal_sampler;
const vec3 c_normal_scale = vec3(2.0, 2.0, 1.0);
const vec3 c_normal_shift = vec3(-1.0, -1.0, 0.0);
const vec2 c_terrain_scale = vec2(1.0 / 2048.0, -1.0 / 2048.0);
const vec2 c_terrain_shift = vec2(0.5, 0.5);
const float c_weight_scale = 1.0 / 64.f;
const vec3 c_world_forward = vec3(0.0, 1.0, 0.0);
const vec3 c_world_up = vec3(0.0, 0.0, 1.0);
out vec3 v_position; // view space
out vec3 v_normal; // view space
out vec4 v_terrain_normal; // view space (x, y, z, weigth)
out vec4 v_texcoord; // texture space (s, t, pixel_level, material_level)
void main(void) {
vec3 orientation = normalize(i_orientation);
mat3 rotation = mat3(cross(orientation, c_world_up), orientation, c_world_up);
vec4 world_position = vec4(i_translation.xyz + rotation * a_position, 1.0);
float weight = max(0.0, 1.0 - world_position.z * c_weight_scale);
vec3 world_normal = rotation * normalize(a_normal);
vec2 terrain_coords = c_terrain_shift + world_position.xy * c_terrain_scale;
world_position.z += texture(u_height_sampler, terrain_coords).r;
vec4 view_position = u_view * world_position;
vec3 terrain_normal = normalize(c_normal_shift + texture(u_normal_sampler, terrain_coords).rgb * c_normal_scale);
world_normal = mat3(cross(c_world_forward, terrain_normal), c_world_forward, terrain_normal) * world_normal;
v_position = view_position.xyz;
v_normal = (u_view * vec4(world_normal, 0.0)).xyz;
v_terrain_normal = vec4((u_view * vec4(terrain_normal, 0.0)).xyz, weight);
v_texcoord = vec4(a_texcoord, i_texlevel, i_texlevel + 1.0);
gl_Position = u_projection * view_position;
}

50
game/triangles.py Normal file
View File

@ -0,0 +1,50 @@
# 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 itertools import chain
from array import array
from math import cos, sin
# 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))