Big code cleanup and better arrow keys
dkl9

dkl9 commited on 2025-179 22:38:38
Showing 13 changed files, with 315 additions and 192 deletions.

... ...
@@ -29,7 +29,8 @@ class Camera:
29 29
         u = vec([-sp * st, cp, -sp * ct])
30 30
         m = np.identity(4, dtype=FLOAT)
31 31
         m[:3, :3] = [s, u, -f]
32
-        m[:3, 3] = [-np.dot(s, self.pos), -np.dot(u, self.pos), np.dot(f, self.pos)]
32
+        p = self.pos
33
+        m[:3, 3] = [-s @ p, -u @ p, f @ p]
33 34
         return m
34 35
 
35 36
     def persp_mat(self):
... ...
@@ -45,12 +46,13 @@ class Camera:
45 46
         ])
46 47
 
47 48
     def mvp(self, ent):
48
-        if ent.hud:
49 49
         am = np.identity(4, dtype=FLOAT)
50
+        vp = np.identity(4, dtype=FLOAT)
51
+        if ent.hud:
50 52
             am[0, 0] = 1 / self.aspect()
51
-            return ent.translate() @ am @ ent.orient
52 53
         else:
53
-            return self.persp_mat() @ self.view_mat() @ ent.translate() @ ent.orient
54
+            vp = self.persp_mat() @ self.view_mat()
55
+        return vp @ ent.translate() @ am @ ent.orient
54 56
 
55 57
     def draw(self, ent):
56 58
         if not ent.verts.shape[0]:
... ...
@@ -0,0 +1,60 @@
1
+from libs import *
2
+from OpenGL.GL import shaders
3
+import os
4
+
5
+VS = f"""
6
+#version 120
7
+{"\n".join([f"attribute {a};" for a in VERTEX_ATTRS])}
8
+{"\n".join([f"varying {a}F;" for a in VERTEX_ATTRS])}
9
+{"\n".join([
10
+    f"uniform {v[1]} {k};" for (k, v) in UNIFORMS.items() if v[0] == "vert"
11
+])}
12
+void main() {{
13
+    gl_Position = MVP * vec4(position, 1.0);
14
+{"\n".join([f"    {a.split()[1]}F = {a.split()[1]};" for a in VERTEX_ATTRS])}
15
+}}
16
+"""
17
+
18
+FS = f"""
19
+#version 120
20
+{"\n".join([f"varying {a}F;" for a in VERTEX_ATTRS])}
21
+{"\n".join([
22
+    f"uniform {v[1]} {k};" for (k, v) in UNIFORMS.items() if v[0] == "frag"
23
+])}
24
+void main() {{
25
+    float ambient = reflsF.x;
26
+    vec3 lightDir = normalize(lightPos.xyz - positionF);
27
+    float diffuse = reflsF.y * lightPos.w *
28
+        dot(normalF, lightDir) / pow(distance(lightPos.xyz, positionF), 2);
29
+    float specular = reflsF.z * lightPos.w * pow(max(dot(
30
+        normalize(camPos - positionF), reflect(-lightDir, normalF)
31
+    ), 0.0), reflsF.w);
32
+    gl_FragColor = vec4((ambient + diffuse + specular) * colF.xyz, colF.w);
33
+}}
34
+"""
35
+
36
+def init_display():
37
+    pygame.init()
38
+    full = os.environ.get("FULLSCREEN")
39
+    pygame.display.set_mode((640, 480),
40
+        pygame.DOUBLEBUF | pygame.OPENGL |
41
+        (pygame.FULLSCREEN | pygame.SCALED if full else pygame.RESIZABLE),
42
+    vsync=1)
43
+    pygame.event.set_allowed([
44
+        pygame.KEYDOWN, pygame.KEYUP,
45
+        pygame.MOUSEBUTTONDOWN, pygame.MOUSEBUTTONUP, pygame.MOUSEMOTION,
46
+        pygame.QUIT, pygame.WINDOWRESIZED, pygame.WINDOWSIZECHANGED
47
+    ])
48
+    pygame.display.set_caption("Voxels")
49
+    sp = shaders.compileProgram(
50
+        shaders.compileShader(VS, GL_VERTEX_SHADER),
51
+        shaders.compileShader(FS, GL_FRAGMENT_SHADER)
52
+    )
53
+    for (k, v) in UNIFORMS.items():
54
+        v[2] = glGetUniformLocation(sp, k)
55
+    glUseProgram(sp)
56
+    glEnable(GL_DEPTH_TEST)
57
+    glEnable(GL_BLEND)
58
+    glEnable(GL_CULL_FACE)
59
+    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
60
+    glClearColor(0.1, 0.1, 0.1, 1.0)
... ...
@@ -1,15 +1,25 @@
1 1
 from libs import *
2
+from raycast import intersect_tri
3
+import types
4
+
5
+draw_modes = types.SimpleNamespace()
6
+draw_modes.TRI_STRIP = GL_TRIANGLE_STRIP
7
+draw_modes.TRI_FAN = GL_TRIANGLE_FAN
8
+draw_modes.TRIS = GL_TRIANGLES
9
+draw_modes.LINE_STRIP = GL_LINE_STRIP
10
+draw_modes.LINES = GL_LINES
2 11
 
3 12
 class Entity:
4 13
     def __init__(self, verts, col, pos, **kwargs):
5 14
         self.verts = vec(verts)
6 15
         self.hud = kwargs.get("hud", False)
7 16
         self.col = vec(col)
8
-        self.refls = vec(kwargs.get("refls", [1.0, 0.0, 0.0, 1.0] if self.hud else [0.5, 0.5, 0.0, 32.0]))
17
+        refl = [1.0, 0.0, 0.0, 1.0] if self.hud else [0.5, 0.5, 0.0, 32.0]
18
+        self.refls = vec(kwargs.get("refls", refl))
9 19
         self.pos = vec(pos)
10 20
         self.draw_mode = kwargs.get("mode", GL_TRIANGLE_STRIP)
11 21
         self.orient = np.identity(4, dtype=FLOAT)
12
-        self.onclick = lambda s, b, n, h: print(f"clicked {s}:{n} with button {b}, hand {h}")
22
+        self.onclick = lambda s, b, n, h: print(f"{h}:{b} on {s}:{n}")
13 23
         self.vao = None
14 24
         self.vbos = [[None, 0] for _ in VERTEX_ATTRS]
15 25
 
... ...
@@ -18,19 +28,20 @@ class Entity:
18 28
 
19 29
     def norm_list(self):
20 30
         l = []
21
-        if self.draw_mode == GL_TRIANGLE_STRIP or self.draw_mode == GL_TRIANGLE_FAN:
31
+        match self.draw_mode:
32
+            case draw_modes.TRI_STRIP | draw_modes.TRI_FAN:
22 33
                 l.append(norm_tri(self.verts[:3]))
23 34
                 for i in range(1, len(self.verts) - 1):
24 35
                     l.append(norm_tri(self.verts[i - 1:i + 2]))
25 36
                 l.append(norm_tri(self.verts[-3:]))
26
-        elif self.draw_mode == GL_TRIANGLES:
37
+            case draw_modes.TRIS:
27 38
                 for i in range(0, len(self.verts), 3):
28 39
                     for _ in range(3):
29 40
                         l.append(norm_tri(self.verts[i:i + 3]))
30
-        elif self.draw_mode == GL_LINE_STRIP or self.draw_mode == GL_LINES:
41
+            case draw_modes.LINE_STRIP | draw_modes.LINES:
31 42
                 for _ in range(len(self.verts)):
32 43
                     l.append([1, 0, 0])
33
-        else:
44
+            case _:
34 45
                 raise NotImplementedError
35 46
         return (self.orient[:3, :3] @ vec(l).T).T
36 47
 
... ...
@@ -66,14 +77,17 @@ class Entity:
66 77
         if self.hud or self.draw_mode == GL_LINES:
67 78
             return
68 79
         elif self.draw_mode == GL_TRIANGLE_STRIP:
69
-            tl = ((i, self.verts[i:i + 3]) for i in range(len(self.verts) - 2))
80
+            ir = range(len(self.verts) - 2)
81
+            tl = ((i, self.verts[i:i + 3]) for i in ir)
70 82
         elif self.draw_mode == GL_TRIANGLES:
71
-            tl = ((i, self.verts[3 * i:3 * (i + 1)]) for i in range(len(self.verts) // 3))
83
+            ir = range(len(self.verts) // 3)
84
+            tl = ((i, self.verts[3 * i:3 * (i + 1)]) for i in ir)
72 85
         else:
73 86
             raise NotImplementedError
74 87
         d, n = None, None
75 88
         for (i, t) in tl:
76
-            r = intersect_tri(ro, rd, np.dot(t, self.orient[:3, :3].T) + self.pos)
89
+            t = np.dot(t, self.orient[:3, :3].T) + self.pos
90
+            r = intersect_tri(ro, rd, t)
77 91
             if r and (d is None or r < d):
78 92
                 d, n = r, i
79 93
         return d, n
... ...
@@ -0,0 +1,58 @@
1
+from libs import *
2
+import entity
3
+import mesh
4
+
5
+def crosshair():
6
+    return entity.Entity(
7
+        [[-0.03, 0, 0], [0.03, 0, 0], [0, -0.03, 0], [0, 0.03, 0]],
8
+        [1.0, 1.0, 1.0, 1.0], [0.0, 0.0, 0.0], mode=GL_LINES, hud=True
9
+    )
10
+
11
+def fps():
12
+    return entity.Entity(
13
+        [[0, 0, 0], [0, -0.1, 0], [0.1, 0, 0], [0.1, -0.1, 0]],
14
+        YELLOW, [-1.0, 1.0, 0.0], mode=GL_TRIANGLE_STRIP, hud=True
15
+    )
16
+
17
+def hand():
18
+    h = mesh.SquareMesh(hud=True)
19
+    h.add_sq(1, 1, [0.5, 0, 0], 1)
20
+    h.add_sq(2, 1, [0, 0.5, 0], 1)
21
+    h.add_sq(3, 1, [0, 0, 0.5], 1)
22
+    h.pos = [0.8, -0.8, 0.0]
23
+    h.rotate(-1, [0, 1, 0])
24
+    h.rotate(0.1, [1, 0, 0])
25
+    h.orient[:3, :3] *= 0.2
26
+    return h
27
+
28
+def selection():
29
+    s = mesh.SquareMesh()
30
+    s.draw_mode = GL_LINE_STRIP
31
+    s.add_sq(-2, 0.96, [0, 0, 0], 2)
32
+    s.verts[:] = s.verts[[0, 1, 3, 2, 0, 3]]
33
+    return s
34
+
35
+def skybox():
36
+    sb = mesh.SquareMesh()
37
+    offset = [0, 0, VIEW_DIST]
38
+    for d in range(3):
39
+        roll_vec3(offset, 1)
40
+        for s in range(2):
41
+            sf = (-1) ** s
42
+            sb.add_sq((d + 1) * sf, 2 * VIEW_DIST, sf * vec(offset), 2)
43
+            i = 6 * (2 * d + s) + 1
44
+            for t in range(2):
45
+                vp = np.s_[i + 3 * t:i + 3 * t + 2]
46
+                sb.verts[vp] = sb.verts[vp][::-1]
47
+    return sb
48
+
49
+def sun():
50
+    s = entity.Entity(
51
+        [[0.0, 0.0, 0.0]], [1.0, 1.0, 0.9, 1.0],
52
+        normalise(LIGHT_POS) * VIEW_DIST, mode=GL_TRIANGLE_FAN
53
+    )
54
+    cv = []
55
+    for k in np.arange(0, 1.01, 0.05):
56
+        cv.append([np.cos(2 * np.pi * k), 0, np.sin(2 * np.pi * k)])
57
+    s.verts = np.concatenate((s.verts, np.multiply(3, cv)), dtype=FLOAT)
58
+    return s
... ...
@@ -1,6 +1,6 @@
1 1
 from libs import *
2 2
 import camera
3
-import entity
3
+import huds
4 4
 
5 5
 def reshape(w, h):
6 6
     glViewport(0, 0, w, h)
... ...
@@ -10,39 +10,40 @@ class InteractiveCamera(camera.Camera):
10 10
     def __init__(self, **kwargs):
11 11
         super().__init__(**kwargs)
12 12
         self.sensitivity = kwargs.get("sensitivity", 0.005)
13
-        self.rv = vec([0, 0, 0])
13
+        self.vaa_vel = vec([0, 0, 0])
14 14
         self.speed = kwargs.get("speed", 0.05)
15 15
         self.hud = {
16
-            "crosshair": entity.Entity(
17
-                [[-0.03, 0.0, 0.0], [0.03, 0.0, 0.0], [0.0, -0.03, 0.0], [0.0, 0.03, 0.0]],
18
-                [1.0, 1.0, 1.0, 1.0], [0.0, 0.0, 0.0], mode=GL_LINES, hud=True
19
-            )
16
+            "crosshair": huds.crosshair()
20 17
         }
21 18
 
22
-    def set_focus(self, f): 
23
-        pygame.event.set_grab(f)
24
-        pygame.mouse.set_visible(not f)
19
+    def set_focus(self, focus): 
20
+        pygame.event.set_grab(focus)
21
+        pygame.mouse.set_visible(not focus)
25 22
         pygame.mouse.set_pos([self.viewport[0] / 2, self.viewport[1] / 2])
26
-        self.hud["crosshair"].pos[0] = 2 * (not f)
23
+        self.hud["crosshair"].pos[0] = 2 * (not focus)
27 24
 
28 25
     def get_focus(self):
29 26
         return not pygame.mouse.get_visible()
30 27
 
31 28
     def mouse_move(self, dx, dy):
32 29
         self.theta = (self.theta - self.sensitivity * dx) % (2 * np.pi)
33
-        self.phi = min(np.pi / 2, max(-np.pi / 2, self.phi + self.sensitivity * dy))
30
+        self.phi += self.sensitivity * dy
31
+        self.phi = min(np.pi / 2, max(-np.pi / 2, self.phi))
34 32
                 
35
-    def update_motion(self, pk):
36
-        self.rv = vec([pk[a] - pk[b] for (a, b) in zip(ARROWS[::2], ARROWS[1::2])])
33
+    def update_motion(self, pressed, mg=None):
34
+        diffs = [0, 0, 0]
35
+        for i in range(3):
36
+            diffs[i] = pressed[ARROWS[2 * i]] - pressed[ARROWS[2 * i + 1]]
37
+        self.vaa_vel = vec(diffs)
37 38
 
38
-    def vdof(self):
39
+    def vaa_mat(self):
39 40
         l = self.looking()
40 41
         l[1] = 0
41 42
         u = self.up()
42 43
         return vec([normalise(np.cross(l, u)), u, normalise(l)])
43 44
 
44 45
     def update_pos(self):
45
-        self.pos += self.speed * self.vdof() @ self.rv.T
46
+        self.pos += self.speed * self.vaa_mat() @ self.vaa_vel.T
46 47
 
47 48
     def handle_event(self, ev, mg):
48 49
         match ev.type:
... ...
@@ -53,6 +54,6 @@ class InteractiveCamera(camera.Camera):
53 54
             case pygame.WINDOWSIZECHANGED | pygame.WINDOWRESIZED:
54 55
                 self.viewport = reshape(ev.x, ev.y)
55 56
 
56
-    def draw_hud(self, wrs, perf):
57
+    def draw_hud(self, regions, perf):
57 58
         for ent in self.hud.values():
58 59
             self.draw(ent)
... ...
@@ -6,13 +6,23 @@ import pygame
6 6
 FLOAT = np.float32
7 7
 VOXEL_TYPE = np.uint8
8 8
 VERTEX_ATTRS = ["vec3 position", "vec4 col", "vec3 normal", "vec4 refls"]
9
-UNIFORMS = {"MVP": ["vert", "mat4", None], "lightPos": ["frag", "vec4", None], "camPos": ["frag", "vec3", None]}
9
+UNIFORMS = {
10
+    "MVP": ["vert", "mat4", None],
11
+    "lightPos": ["frag", "vec4", None],
12
+    "camPos": ["frag", "vec3", None]
13
+}
10 14
 
11 15
 LIGHT_POS = [5, 30, 10]
16
+REGION_SIZE = 8
12 17
 GRAVITY = 1
13 18
 VIEW_DIST = 30
14 19
 FPS = 60
15
-ARROWS = [pygame.K_RIGHT, pygame.K_LEFT, pygame.K_SPACE, pygame.K_LSHIFT, pygame.K_UP, pygame.K_DOWN]
20
+ARROWS = [
21
+    ["RIGHT", "e", "d"], ["LEFT", "a"],
22
+    ["SPACE"], ["LSHIFT"],
23
+    ["UP", "COMMA", "w"], ["DOWN", "o", "s"],
24
+]
25
+ARROWS = [[getattr(pygame, f"K_{n}") for n in opts] for opts in ARROWS]
16 26
 
17 27
 def vec(l):
18 28
     return np.array(l, dtype=FLOAT)
... ...
@@ -32,34 +42,21 @@ def roll_vec3(l, k):
32 42
         case 2:
33 43
             l[:] = [l[1], l[2], l[0]]
34 44
 
35
-def axis2offset(a, x=1):
36
-    o = [0, 0, x * np.sign(a)]
45
+def axis2offset(a, mag=1):
46
+    o = [0, 0, mag * np.sign(a)]
37 47
     roll_vec3(o, abs(a))
38 48
     return o
39 49
 
40
-def smerp(ab, w):
41
-    assert all([abs(x) < 2 for x in ab])
42
-    for i in range(w.shape[0]):
43
-        ab = [a + w[i] ** 2 * (3 - 2 * w[i]) * (b - a) for (a, b) in zip(ab[::2], ab[1::2])]
44
-    return ab[0]
50
+def smerp(corners, weights):
51
+    for w in weights:
52
+        t = w ** 2 * (3 - 2 * w)
53
+        pairs = zip(corners[::2], corners[1::2])
54
+        corners = [a + t * (b - a) for (a, b) in pairs]
55
+    return corners[0]
45 56
 
46
-# https://iquilezles.org/articles/intersectors/
47
-def intersect_tri(ro, rd, tv):
48
-    e1 = tv[1] - tv[0]
49
-    e2 = tv[2] - tv[0]
50
-    to = ro - tv[0]
51
-    n = np.cross(e1, e2)
52
-    q = np.cross(to, rd)
53
-    d = 1.0 / np.dot(rd, n)
54
-    u = d * np.dot(-q, e2)
55
-    v = d * np.dot(q, e1)
56
-    t = d * np.dot(-n, to)
57
-    if u > 0.0 and v > 0.0 and u + v < 1.0 and t > 1e-3:
58
-        return t
59
-
60
-def norm_tri(tv):
61
-    assert tv.shape[0] == 3
62
-    return normalise(np.cross(tv[2] - tv[0], tv[1] - tv[0]))
57
+def norm_tri(verts):
58
+    assert verts.shape[0] == 3
59
+    return normalise(np.cross(verts[2] - verts[0], verts[1] - verts[0]))
63 60
 
64 61
 def update_attr(ent, n, l=None):
65 62
     if isinstance(n, int):
... ...
@@ -1,45 +1,14 @@
1
-from OpenGL.GL import shaders
2 1
 from libs import *
2
+from display import init_display
3 3
 from entity import Entity
4 4
 from mapgen import MapGen
5 5
 from mesh import SquareMesh
6 6
 from player import Player
7 7
 from region import WorldRegion
8
-import os
9
-
10
-VS = f"""
11
-#version 120
12
-{"\n".join([f"attribute {a};" for a in VERTEX_ATTRS])}
13
-{"\n".join([f"varying {a}F;" for a in VERTEX_ATTRS])}
14
-{"\n".join([f"uniform {v[1]} {k};" for (k, v) in UNIFORMS.items() if v[0] == "vert"])}
15
-void main() {{
16
-    gl_Position = MVP * vec4(position, 1.0);
17
-{"\n".join([f"    {a.split()[1]}F = {a.split()[1]};" for a in VERTEX_ATTRS])}
18
-}}
19
-"""
20
-
21
-FS = f"""
22
-#version 120
23
-{"\n".join([f"varying {a}F;" for a in VERTEX_ATTRS])}
24
-{"\n".join([f"uniform {v[1]} {k};" for (k, v) in UNIFORMS.items() if v[0] == "frag"])}
25
-void main() {{
26
-    float ambient = reflsF.x;
27
-    vec3 lightDir = normalize(lightPos.xyz - positionF);
28
-    float lambert = dot(normalF, lightDir);
29
-    float diffuse = reflsF.y * lightPos.w * lambert / pow(distance(lightPos.xyz, positionF), 2);
30
-    float specular = reflsF.z * lightPos.w * pow(max(dot(normalize(camPos - positionF), reflect(-lightDir, normalF)), 0.0), reflsF.w);
31
-    gl_FragColor = vec4((ambient + diffuse + specular) * colF.xyz, colF.w);
32
-}}
33
-"""
34 8
 
35 9
 def debug_mode(mg, cam):
36 10
     pass
37 11
 
38
-def build_scene():
39
-    mg = MapGen()
40
-    mg.fetch((0, 0, 0))
41
-    return mg
42
-
43 12
 def in_view(wr, cam):
44 13
     rwrc = wr.pos + wr.r / 2 - cam.pos
45 14
     close = np.linalg.norm(rwrc) < VIEW_DIST
... ...
@@ -49,23 +18,9 @@ def in_view(wr, cam):
49 18
     return close and frust
50 19
 
51 20
 def main():
52
-    pygame.init()
53
-    pygame.display.set_mode((640, 480), pygame.DOUBLEBUF | pygame.OPENGL | (pygame.FULLSCREEN | pygame.SCALED if os.environ.get("FULLSCREEN") else pygame.RESIZABLE), vsync=1)
54
-    pygame.event.set_allowed([pygame.KEYDOWN, pygame.KEYUP, pygame.MOUSEBUTTONDOWN, pygame.MOUSEBUTTONUP, pygame.MOUSEMOTION, pygame.QUIT, pygame.WINDOWRESIZED, pygame.WINDOWSIZECHANGED])
55
-    pygame.display.set_caption("Voxels")
56
-    sp = shaders.compileProgram(
57
-        shaders.compileShader(VS, GL_VERTEX_SHADER),
58
-        shaders.compileShader(FS, GL_FRAGMENT_SHADER)
59
-    )
60
-    for (k, v) in UNIFORMS.items():
61
-        v[2] = glGetUniformLocation(sp, k)
62
-    glUseProgram(sp)
63
-    glEnable(GL_DEPTH_TEST)
64
-    glEnable(GL_BLEND)
65
-    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
66
-    glClearColor(0.1, 0.1, 0.1, 1.0)
21
+    init_display()
67 22
     glUniform4fv(UNIFORMS["lightPos"][2], 1, vec([*LIGHT_POS, 100]))
68
-    mg = build_scene()
23
+    mg = MapGen()
69 24
     clock = pygame.time.Clock()
70 25
     cam = Player(speed=6/FPS)
71 26
     cam.pos = [4, 8, 4]
... ...
@@ -76,10 +31,16 @@ def main():
76 31
             if wr.dirty:
77 32
                 wr.remesh()
78 33
         for ev in pygame.event.get():
79
-            if ev.type == pygame.QUIT or ev.type == pygame.KEYUP and ev.unicode == "q":
34
+            match ev.type:
35
+                case pygame.QUIT:
36
+                    pygame.quit()
37
+                    return
38
+                case pygame.KEYUP:
39
+                    match ev.unicode:
40
+                        case "q":
80 41
                             pygame.quit()
81 42
                             return
82
-            elif ev.type == pygame.KEYUP and ev.unicode == "d":
43
+                        case "d":
83 44
                             debug_mode(mg, cam)
84 45
             cam.handle_event(ev, mg, scene)
85 46
         cam.update_motion(pygame.key.get_pressed(), mg)
... ...
@@ -5,7 +5,11 @@ import world
5 5
 class MapGen(world.WorldHolder):
6 6
     def __init__(self):
7 7
         super().__init__()
8
-        self.grads = [defaultdict(lambda dim=d: defaultdict(lambda: normalise(np.random.normal(size=dim)))) for d in range(4)]
8
+        self.grads = []
9
+        for d in range(4):
10
+            self.grads.append(defaultdict(lambda dim=d:
11
+                defaultdict(lambda: normalise(np.random.normal(size=dim)))
12
+            ))
9 13
         self.poissons = {}
10 14
 
11 15
     def perlin(self, name, pos, period):
... ...
@@ -18,9 +22,7 @@ class MapGen(world.WorldHolder):
18 22
         dots = []
19 23
         for c in corners:
20 24
             ci = tuple(np.add(c, corner, dtype=int, casting="unsafe"))
21
-            cg = gt[ci]
22
-            assert abs(np.linalg.norm(cg) - 1) < 1e-3
23
-            dots.append(np.dot(fract - c, cg))
25
+            dots.append(np.dot(fract - c, gt[ci]))
24 26
         return 2 * smerp(dots, fract)
25 27
 
26 28
     def poisson(self, name, pos, r, period):
... ...
@@ -28,7 +30,9 @@ class MapGen(world.WorldHolder):
28 30
         if name in self.poissons:
29 31
             gt = self.poissons[name]
30 32
         else:
31
-            gt = defaultdict(lambda: np.random.poisson(1 / np.prod(period), np.repeat(r, dim)))
33
+            gt = defaultdict(lambda: np.random.poisson(
34
+                1 / np.prod(period), np.repeat(r, dim)
35
+            ))
32 36
             self.poissons[name] = gt
33 37
         return gt[tuple((x // r) * r for x in pos)]
34 38
 
... ...
@@ -36,17 +40,24 @@ class MapGen(world.WorldHolder):
36 40
         n = 0
37 41
         for i in range(wr.r):
38 42
             for j in range(wr.r):
39
-                h = 4 + 4 * self.perlin("height", np.add(wr.pos[[0, 2]], (i, j)), np.array([10, 20])) - wr.pos[1]
43
+                h = 4 + 4 * self.perlin(
44
+                    "height",
45
+                    np.add(wr.pos[[0, 2]], (i, j)),
46
+                    np.array([10, 20])
47
+                ) - wr.pos[1]
40 48
                 if h > 0:
41 49
                     wr.grid[i, :min(int(h), wr.r), j] = 3
42 50
                 if h < wr.r and h > 0:
43 51
                     wr.grid[i, int(h), j] = 4
44
-                    if 2 <= i < wr.r - 2 and 2 <= j < wr.r - 2 and self.poisson("trees", wr.pos[[0, 2]], wr.r, (6, 6))[i, j] >= 1:
52
+                    if 2 <= i < wr.r - 2 and 2 <= j < wr.r - 2 and self.poisson(
53
+                            "trees", wr.pos[[0, 2]], wr.r, (6, 6)
54
+                        )[i, j] >= 1:
45 55
                         for u in range(1, 6):
46 56
                             self.voxel_at(wr.pos + [0, h + u, 0], 5)
47 57
                         for u in range(5, 7):
48 58
                             for v in range(-7 + u, 8 - u):
49
-                                for w in range(-7 + u + abs(v), 8 - u - abs(v)):
59
+                                b = 7 - u - abs(v)
60
+                                for w in range(-b, b + 1):
50 61
                                     self.voxel_at(wr.pos + [v, h + u, w], 6)
51 62
         return n
52 63
 
... ...
@@ -29,14 +29,18 @@ class SquareMesh(entity.Entity):
29 29
         self.verts = np.delete(self.verts, np.s_[3 * ind:3 * (ind + 2)], 0)
30 30
         del self.faces[ind:ind + 2]
31 31
 
32
-    def cube_mat(self, fd, axis, corner):
33
-        if not isinstance(fd, int):
34
-            fd = self.grid[*fd]
35
-        match fd:
32
+    def cube_mat(self, face_desc, axis, corner):
33
+        if not isinstance(face_desc, int):
34
+            face_desc = self.grid[*face_desc]
35
+        face_orient = ((axis % 4) // 2)
36
+        match face_desc:
36 37
             case 0:
37 38
                 return (vec([0.0, 0.0, 0.0, 0.0]), vec([1.0, 0.0, 0.0, 1.0]))
38 39
             case 1:
39
-                return (vec([*np.add(0.5, axis2offset(axis, 0.5)), 1.0]), vec([1.0, 0.0, 0.1, 128.0]))
40
+                return (
41
+                    vec([*np.add(0.5, axis2offset(axis, 0.5)), 1.0]),
42
+                    vec([1.0, 0.0, 0.1, 128.0])
43
+                )
40 44
             case 2:
41 45
                 top = (vec([0.2, 0.2, 1.0, 1.0]), vec([1.0, 0.0, 0.0, 1.0]))
42 46
                 bottom = (vec([0.0, 0.0, 0.2, 1.0]), vec([1.0, 0.0, 0.0, 1.0]))
... ...
@@ -46,7 +50,7 @@ class SquareMesh(entity.Entity):
46 50
                     case -2:
47 51
                         return bottom
48 52
                     case a:
49
-                        return bottom if corner & (1 << ((a % 4) // 2)) else top
53
+                        return bottom if corner & (2 >> face_orient) else top
50 54
             case 3:
51 55
                 return (vec([0.7, 0.7, 0.8, 1.0]), vec([0.5, 0.5, 0.01, 2.0]))
52 56
             case 4:
... ...
@@ -58,7 +62,7 @@ class SquareMesh(entity.Entity):
58 62
                     case -2:
59 63
                         return bottom
60 64
                     case a:
61
-                        return bottom if corner & (1 << ((a % 4) // 2)) else top
65
+                        return bottom if corner & (1 << face_orient) else top
62 66
                 return 
63 67
             case 5:
64 68
                 core = (vec([1.0, 1.0, 0.8, 1.0]), vec([0.4, 1.6, 0.0, 1.0]))
... ...
@@ -75,6 +79,7 @@ class SquareMesh(entity.Entity):
75 79
         cl = np.empty((2, 3 * len(self.faces), 4), dtype=FLOAT)
76 80
         for s in range(len(self.faces) // 2):
77 81
             axis, fd = self.faces[2 * s]
78
-            for (t, c) in enumerate([0, 1, 2, 3, 2, 1] if (True or axis > 0) else [1, 2, 3, 2, 1, 0]):
79
-                cl[0][6 * s + t], cl[1][6 * s + t] = self.cube_mat(fd, axis, c)
82
+            for (t, c) in enumerate([0, 1, 2, 3, 2, 1]):
83
+                i = 6 * s + t
84
+                cl[0][i], cl[1][i] = self.cube_mat(fd, axis, c)
80 85
         return cl
... ...
@@ -1,61 +1,39 @@
1 1
 from libs import *
2 2
 import interactive
3
-import entity
4
-import mesh
3
+import huds
5 4
 
6 5
 class Player(interactive.InteractiveCamera):
7 6
     def __init__(self, **kwargs):
8 7
         super().__init__(**kwargs)
9
-        self.hud["fps"] = entity.Entity(
10
-            [[0.0, 0.0, 0.0], [0.1, 0.0, 0.0], [0.0, -0.1, 0.0], [0.1, -0.1, 0.0]],
11
-            YELLOW, [-1.0, 1.0, 0.0], mode=GL_TRIANGLE_STRIP, hud=True
12
-        )
13
-        self.hud["skybox"] = mesh.SquareMesh()
14
-        offset = [0, 0, VIEW_DIST]
15
-        for d in range(3):
16
-            roll_vec3(offset, 1)
17
-            for s in range(2):
18
-                self.hud["skybox"].add_sq((d + 1) * (-1) ** s, 2 * VIEW_DIST, (-1) ** s * vec(offset), 2)
19
-        self.hud["sun"] = entity.Entity(
20
-            [[0.0, 0.0, 0.0], *[[2 * np.cos(2 * np.pi * k), 0, 2 * np.sin(2 * np.pi * k)] for k in np.arange(0, 1.01, 0.05)]],
21
-            [1.0, 1.0, 0.8, 1.0], normalise(LIGHT_POS) * VIEW_DIST, mode=GL_TRIANGLE_FAN
22
-        )
23
-        h = mesh.SquareMesh(hud=True)
24
-        self.hud["hand"] = h
25
-        h.add_sq(1, 1, [0.5, 0, 0], 1)
26
-        h.add_sq(2, 1, [0, 0.5, 0], 1)
27
-        h.add_sq(3, 1, [0, 0, 0.5], 1)
28
-        h.pos = [0.8, -0.8, 0.5]
29
-        h.rotate(2, [0, 1, 0])
30
-        h.rotate(-0.1, [1, 0, 0])
31
-        h.orient[:3, :3] *= 0.2
32
-        self.hud["shl"] = mesh.SquareMesh()
33
-        self.hud["shl"].draw_mode = GL_LINE_STRIP
34
-        self.hud["shl"].add_sq(-2, 0.96, [0, 0, 0], 2)
35
-        self.hud["shl"].verts[:] = self.hud["shl"].verts[[0, 1, 3, 2, 0, 3]]
8
+        self.hud["fps"] = huds.fps()
9
+        self.hud["skybox"] = huds.skybox()
10
+        self.hud["sun"] = huds.sun()
11
+        self.hud["hand"] = huds.hand()
12
+        self.hud["shl"] = huds.selection()
36 13
         self.hand = 1
37 14
         self.collides = True
38 15
         self.falls = True
39 16
 
40
-    def update_motion(self, pk, mg):
41
-        netv = [pk[a] - pk[b] for (a, b) in zip(ARROWS[::2], ARROWS[1::2])]
42
-        if self.collides:
43
-            for i in range(len(netv)):
17
+    def update_motion(self, pressed, mg):
18
+        netv = [0, 0, 0]
19
+        for i in range(3):
44 20
             for s in range(2):
45
-                    o = (-1) ** s * self.vdof()[i]
46
-                    va = mg.voxel_at(self.pos + o)
47
-                    if va:
48
-                        netv[i] = (max if s else min)(netv[i], 0)
21
+                sf = (-1) ** s
22
+                cp = self.pos + sf * self.vaa_mat()[i]
23
+                if self.collides and mg.voxel_at(cp):
24
+                    pass
49 25
                 elif self.falls and i == 1 and s == 1:
50
-                        netv[i] = self.rv[i] - GRAVITY / FPS
51
-        self.rv = vec(netv)
26
+                    netv[i] = self.vaa_vel[i] - GRAVITY / FPS
27
+                else:
28
+                    netv[i] += sf * any(pressed[j] for j in ARROWS[2 * i + s])
29
+        self.vaa_vel = vec(netv)
52 30
 
53
-    def set_hand(self, nh):
54
-        nh = int(nh)
55
-        self.hand = nh
56
-        self.hud["hand"].faces = [(a, self.hand) for (a, _) in self.hud["hand"].faces]
31
+    def set_hand(self, new_hand):
32
+        self.hand = int(new_hand)
33
+        hg = self.hud["hand"]
34
+        hg.faces = [(a, self.hand) for (a, _) in hg.faces]
57 35
         for i in range(1, 4, 2):
58
-            update_attr(self.hud["hand"], i)
36
+            update_attr(hg, i)
59 37
 
60 38
     def handle_event(self, ev, mg, scene):
61 39
         super().handle_event(ev, mg)
... ...
@@ -83,10 +61,18 @@ class Player(interactive.InteractiveCamera):
83 61
         c, ap = self.target(wrs)
84 62
         if c:
85 63
             self.hud["shl"].pos = c.pos + ap[1] + axis2offset(ap[0], 0.52)
86
-            self.hud["shl"].orient[:3, :3] = np.sign(ap[0]) * np.roll(np.identity(3), abs(ap[0]) + 1, 0)
64
+            self.hud["shl"].orient[:3, :3] = np.sign(ap[0]) * np.roll(
65
+                np.identity(3), abs(ap[0]) + 1, 0
66
+            )
87 67
         else:
88 68
             self.hud["shl"].pos = vec([0, 0, 0])
89
-        self.hud["fps"].col = GREEN if 0.9 < perf < 1.1 else YELLOW if perf > 0.5 else RED
69
+        if 0.9 < perf < 1.1:
70
+            perf_col = GREEN
71
+        elif perf > 0.5:
72
+            perf_col = YELLOW
73
+        else:
74
+            perf_col = RED
75
+        self.hud["fps"].col = perf_col
90 76
         update_attr(self.hud["fps"], "col")
91 77
         for ent in self.hud.values():
92 78
             self.draw(ent)
... ...
@@ -0,0 +1,26 @@
1
+from libs import *
2
+
3
+# https://iquilezles.org/articles/intersectors/
4
+def intersect_tri(ro, rd, verts):
5
+    e1 = verts[1] - verts[0]
6
+    e2 = verts[2] - verts[0]
7
+    to = ro - verts[0]
8
+    n = np.cross(e1, e2)
9
+    q = np.cross(to, rd)
10
+    d = 1.0 / np.dot(rd, n)
11
+    u = d * np.dot(-q, e2)
12
+    v = d * np.dot(q, e1)
13
+    t = d * np.dot(-n, to)
14
+    if u > 0.0 and v > 0.0 and u + v < 1.0 and t > 1e-3:
15
+        return t
16
+
17
+def intersect_box(ro, rd, c, s):
18
+    m = 1 / rd
19
+    n = m * (ro - c)
20
+    k = np.abs(m) * s
21
+    tn = np.max(-n - k)
22
+    tf = np.min(-n + k)
23
+    if tf > tn and tf > 0:
24
+        return tn, tf
25
+    else:
26
+        return None, None
... ...
@@ -1,36 +1,28 @@
1 1
 from libs import *
2
+from raycast import intersect_box
2 3
 import heapq
3 4
 import mesh
4 5
 
5
-def intersect_box(ro, rd, c, s):
6
-    m = 1 / rd
7
-    n = m * (ro - c)
8
-    k = np.abs(m) * s
9
-    tn = np.max(-n - k)
10
-    tf = np.min(-n + k)
11
-    if tf > tn and tf > 0:
12
-        return tn, tf
13
-    else:
14
-        return None, None
15
-
16 6
 class WorldRegion(mesh.SquareMesh):
17
-    def __init__(self, size=8):
7
+    def __init__(self, size=REGION_SIZE):
18 8
         super().__init__(refls=[0.5, 0.5, 0.5, 32.0])
19 9
         self.grid = np.zeros((size, size, size), dtype=VOXEL_TYPE)
20 10
         self.r = size
21 11
         self.dirty = False
22 12
 
23
-    def set_voxel(self, pos, vt=0):
13
+    def set_voxel(self, pos, voxel_type=0):
24 14
         if any(pos < 0) or any(pos >= self.r):
25 15
             return pos
26
-        self.grid[*pos] = vt
27
-        if vt == 0:
16
+        self.grid[*pos] = voxel_type
17
+        if voxel_type == 0:
28 18
             self.remesh()
29 19
         else:
30
-            bp = bool(vt)
20
+            bp = bool(voxel_type)
31 21
             for d in range(3):
32 22
                 for s in range(2):
33
-                    if pos[d] == self.r - 1 and s == 0 or pos[d] == 0 and s == 1:
23
+                    oob = pos[d] == self.r - 1 and s == 0
24
+                    oob |= s == 0 or pos[d] == 0 and s == 1
25
+                    if oob:
34 26
                         continue
35 27
                     a = (d + 1) * (-1) ** s
36 28
                     o = np.array(axis2offset(a), dtype=np.int64)
... ...
@@ -76,7 +68,9 @@ class WorldRegion(mesh.SquareMesh):
76 68
         self.dirty = False
77 69
 
78 70
     def norm_list(self):
79
-        return np.astype(np.repeat([axis2offset(a) for (a, _) in self.faces], 3, 0), FLOAT)
71
+        return np.astype(np.repeat([
72
+            axis2offset(a) for (a, _) in self.faces
73
+        ], 3, 0), FLOAT)
80 74
 
81 75
     def raycast(self, ro, rd):
82 76
         vr = np.repeat(self.r, 3)
... ...
@@ -84,12 +78,17 @@ class WorldRegion(mesh.SquareMesh):
84 78
         if tn is not None:
85 79
             ids = []
86 80
             for i in range(3):
87
-                k = np.arange(self.r + 1) if rd[i] >= 0 else np.arange(self.r + 1, 0, -1)
88
-                ids.append([(d, i) for d in (self.pos[i] + k - 0.5 - ro[i]) / rd[i]])
81
+                if rd[i] >= 0:
82
+                    k = np.arange(self.r + 1)
83
+                else:
84
+                    k = np.arange(self.r + 1, 0, -1)
85
+                dists = (self.pos[i] + k - 0.5 - ro[i]) / rd[i]
86
+                ids.append([(d, i) for d in dists])
89 87
             for (te, i) in heapq.merge(*ids):
90 88
                 if te < 0 or te < tn or te > tf - 1e-3:
91 89
                     continue
92
-                p = np.astype(np.round(ro + (te + 1e-4) * rd - self.pos), np.int16)
90
+                p = ro + (te + 1e-4) * rd - self.pos
91
+                p = np.astype(np.round(p), np.int16)
93 92
                 if self.grid[*p]:
94 93
                     sa = np.astype((i + 1) * -np.sign(rd[i]), np.int8)
95 94
                     return te, (sa, p)
... ...
@@ -18,16 +18,19 @@ class WorldHolder():
18 18
             wr.onclick = (lambda s, b, c, h: self.click_region(s, b, c, h))
19 19
             return wr
20 20
 
21
-    def voxel_at(self, pos, vt=None):
21
+    def voxel_at(self, pos, voxel_type=None):
22
+        try:
22 23
             r = next(iter(self.regions.values())).r
24
+        except StopIteration:
25
+            r = REGION_SIZE
23 26
         pr, pf = np.divmod(pos + 0.5, r)
24 27
         wr = self.fetch(tuple(int(r * c) for c in pr))
25 28
         it = tuple(int(c) for c in pf)
26
-        if vt is None:
29
+        if voxel_type is None:
27 30
             return wr.grid[*it]
28 31
         else:
29 32
             old = wr.grid[*it]
30
-            wr.grid[*it] = vt
33
+            wr.grid[*it] = voxel_type
31 34
             wr.dirty = True
32 35
             return old
33 36
 
34 37