Trees, optimisations, noclip toggle
dkl9

dkl9 commited on 2025-176 23:08:34
Showing 9 changed files, with 125 additions and 62 deletions.

... ...
@@ -22,9 +22,11 @@ class Camera:
22 22
         return self.viewport[0] / self.viewport[1]
23 23
 
24 24
     def view_mat(self):
25
-        f = normalise(self.looking())
26
-        s = normalise(np.cross(f, self.up()))
27
-        u = np.cross(s, f)
25
+        ct, st = np.cos(self.theta), np.sin(self.theta)
26
+        cp, sp = np.cos(self.phi), np.sin(self.phi)
27
+        f = vec([cp * st, sp, cp * ct])
28
+        s = vec([-ct, 0, st])
29
+        u = vec([-sp * st, cp, -sp * ct])
28 30
         m = np.identity(4, dtype=FLOAT)
29 31
         m[:3, :3] = [s, u, -f]
30 32
         m[:3, 3] = [-np.dot(s, self.pos), -np.dot(u, self.pos), np.dot(f, self.pos)]
... ...
@@ -9,7 +9,7 @@ class Entity:
9 9
         self.pos = vec(pos)
10 10
         self.draw_mode = kwargs.get("mode", GL_TRIANGLE_STRIP)
11 11
         self.orient = np.identity(4, dtype=FLOAT)
12
-        self.onclick = lambda s, b, n: print(f"clicked {s}:{n} with button {b}")
12
+        self.onclick = lambda s, b, n, h: print(f"clicked {s}:{n} with button {b}, hand {h}")
13 13
         self.vao = None
14 14
         self.vbos = [[None, 0] for _ in VERTEX_ATTRS]
15 15
 
... ...
@@ -59,8 +59,8 @@ class Entity:
59 59
             [0, 0, 0, 1]
60 60
         ]) @ self.orient
61 61
 
62
-    def click(self, button, face):
63
-        self.onclick(self, button, face)
62
+    def click(self, button, face, hand=1):
63
+        self.onclick(self, button, face, hand)
64 64
 
65 65
     def raycast(self, ro, rd):
66 66
         if self.hud or self.draw_mode == GL_LINES:
... ...
@@ -73,7 +73,6 @@ def update_attr(ent, n, l=None):
73 73
                 l = ent.mat_list()[0]
74 74
             case "normal":
75 75
                 l = ent.norm_list()
76
-                assert all([abs(np.linalg.norm(v) - 1) < 1e-3 for v in l])
77 76
             case "refls":
78 77
                 l = ent.mat_list()[1]
79 78
     if ent.vao:
... ...
@@ -39,9 +39,18 @@ def build_scene():
39 39
     mg.fetch((0, 0, 0))
40 40
     return mg
41 41
 
42
+def in_view(wr, cam):
43
+    rwrc = wr.pos + wr.r / 2 - cam.pos
44
+    close = np.linalg.norm(rwrc) < VIEW_DIST
45
+    ld = cam.looking()
46
+    cv = rwrc + 8 * ld
47
+    frust = np.dot(cv, ld) / np.linalg.norm(cv) > 0.5
48
+    return close and frust
49
+
42 50
 def main():
43 51
     pygame.init()
44
-    pygame.display.set_mode((640, 480), pygame.DOUBLEBUF | pygame.OPENGL | pygame.RESIZABLE)
52
+    pygame.display.set_mode((640, 480), pygame.DOUBLEBUF | pygame.OPENGL | pygame.FULLSCREEN | pygame.SCALED, vsync=1)
53
+    pygame.event.set_allowed([pygame.KEYDOWN, pygame.KEYUP, pygame.MOUSEBUTTONDOWN, pygame.MOUSEBUTTONUP, pygame.MOUSEMOTION, pygame.QUIT, pygame.WINDOWRESIZED, pygame.WINDOWSIZECHANGED])
45 54
     pygame.display.set_caption("Voxels")
46 55
     sp = shaders.compileProgram(
47 56
         shaders.compileShader(VS, GL_VERTEX_SHADER),
... ...
@@ -51,6 +60,8 @@ def main():
51 60
         v[2] = glGetUniformLocation(sp, k)
52 61
     glUseProgram(sp)
53 62
     glEnable(GL_DEPTH_TEST)
63
+    glEnable(GL_BLEND)
64
+    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
54 65
     glClearColor(0.1, 0.1, 0.1, 1.0)
55 66
     glUniform4fv(UNIFORMS["lightPos"][2], 1, vec([5, 30, 10, 100]))
56 67
     mg = build_scene()
... ...
@@ -59,7 +70,10 @@ def main():
59 70
     cam.pos = [4, 8, 4]
60 71
     cam.set_focus(True)
61 72
     while True:
62
-        scene = list(filter(lambda wr: np.linalg.norm(wr.pos - cam.pos) < VIEW_DIST, mg.regions.values()))
73
+        scene = list(filter(lambda wr: in_view(wr, cam), mg.regions.values()))
74
+        for wr in scene:
75
+            if wr.dirty:
76
+                wr.remesh()
63 77
         for ev in pygame.event.get():
64 78
             if ev.type == pygame.QUIT or ev.type == pygame.KEYUP and ev.unicode == "q":
65 79
                 pygame.quit()
... ...
@@ -1,51 +1,16 @@
1 1
 from libs import *
2
-import region
2
+from collections import defaultdict
3
+import world
3 4
 
4
-class MapGen:
5
+class MapGen(world.WorldHolder):
5 6
     def __init__(self):
6
-        self.grads = [{} for _ in range(4)]
7
-        self.regions = {}
8
-
9
-    def fetch(self, pos):
10
-        if pos in self.regions:
11
-            return self.regions[pos]
12
-        else:
13
-            wr = region.WorldRegion()
14
-            wr.pos = vec(pos)
15
-            self.world_natural(wr)
16
-            wr.remesh()
17
-            wr.onclick = lambda s, b, c: self.click_region(s, b, c)
18
-            self.regions[pos] = wr
19
-            return wr
20
-
21
-    def voxel_at(self, pos):
22
-        r = next(iter(self.regions.values())).r
23
-        pr, pf = np.divmod(pos + 0.5, r)
24
-        wr = self.fetch(tuple(int(r * c) for c in pr))
25
-        return wr.grid[*tuple(int(c) for c in pf)]
26
-
27
-    def click_region(self, wr, button, coords):
28
-        match button:
29
-            case 1:
30
-                wr.set_voxel(coords[1])
31
-            case 2:
32
-                print(f"coords {coords}")
33
-            case 3:
34
-                offset = axis2offset(coords[0])
35
-                p = wr.set_voxel(coords[1] + offset, 1)
36
-                if p is not None:
37
-                    rc = wr.pos + p // wr.r * wr.r
38
-                    awr = self.fetch(tuple(int(x) for x in rc))
39
-                    p = p % wr.r
40
-                    if not awr.grid[*p]:
41
-                        awr.set_voxel(p, 1)
42
-        if button % 2 == 1:
43
-            for i in range(len(VERTEX_ATTRS)):
44
-                update_attr(wr, i)
7
+        super().__init__()
8
+        self.grads = [defaultdict(lambda dim=d: defaultdict(lambda: normalise(np.random.normal(size=dim)))) for d in range(4)]
9
+        self.poissons = {}
45 10
 
46 11
     def perlin(self, name, pos, period):
47 12
         dim = pos.shape[0]
48
-        gt = self.grads[dim].setdefault(name, {})
13
+        gt = self.grads[dim][name]
49 14
         corner, fract = np.divmod(pos, period)
50 15
         fract /= period
51 16
         # https://stackoverflow.com/a/58827205
... ...
@@ -53,25 +18,40 @@ class MapGen:
53 18
         dots = []
54 19
         for c in corners:
55 20
             ci = tuple(np.add(c, corner, dtype=int, casting="unsafe"))
56
-            cg = gt.setdefault(ci, normalise(np.random.normal(size=dim)))
21
+            cg = gt[ci]
57 22
             assert abs(np.linalg.norm(cg) - 1) < 1e-3
58 23
             dots.append(np.dot(fract - c, cg))
59 24
         return 2 * smerp(dots, fract)
60 25
 
26
+    def poisson(self, name, pos, r, period):
27
+        dim = pos.shape[0]
28
+        if name in self.poissons:
29
+            gt = self.poissons[name]
30
+        else:
31
+            gt = defaultdict(lambda: np.random.poisson(1 / np.prod(period), np.repeat(r, dim)))
32
+            self.poissons[name] = gt
33
+        return gt[tuple((x // r) * r for x in pos)]
34
+
61 35
     def world_natural(self, wr):
62 36
         n = 0
63 37
         for i in range(wr.r):
64 38
             for j in range(wr.r):
65
-                h = 4 + 4 * self.perlin("height", np.add(wr.pos[[0, 2]], (i, j)), np.array([10, 5])) - wr.pos[1]
66
-                for k in range(min(int(h), wr.r)):
67
-                    wr.grid[i, k, j] = 3
68
-                    n += 1
69
-                if h > 0 and h < wr.r:
39
+                h = 4 + 4 * self.perlin("height", np.add(wr.pos[[0, 2]], (i, j)), np.array([10, 20])) - wr.pos[1]
40
+                if h > 0:
41
+                    wr.grid[i, :min(int(h), wr.r), j] = 3
42
+                if h < wr.r and h > 0:
70 43
                     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:
45
+                        for u in range(1, 6):
46
+                            self.voxel_at(wr.pos + [0, h + u, 0], 5)
47
+                        for u in range(5, 7):
48
+                            for v in range(-7 + u, 8 - u):
49
+                                for w in range(-7 + u + abs(v), 8 - u - abs(v)):
50
+                                    self.voxel_at(wr.pos + [v, h + u, w], 6)
71 51
         return n
72 52
 
73 53
     def world_min(self, wr):
74
-        wr.set_voxel((0, 0, 0), 1)
54
+        wr.grid[0, 0, 0] = 1
75 55
         return 1
76 56
 
77 57
     def world_grid(self, wr):
... ...
@@ -60,6 +60,16 @@ class SquareMesh(entity.Entity):
60 60
                     case a:
61 61
                         return bottom if corner & (2 >> ((a % 4) // 2)) else top
62 62
                 return 
63
+            case 5:
64
+                core = (vec([1.0, 1.0, 0.8, 1.0]), vec([0.4, 1.6, 0.0, 1.0]))
65
+                bark = (vec([0.4, 0.3, 0.0, 1.0]), vec([0.5, 0.5, 0.0, 1.0]))
66
+                match axis:
67
+                    case 2 | -2:
68
+                        return core
69
+                    case _:
70
+                        return bark
71
+            case 6:
72
+                return (vec([0.0, 0.4, 0.0, 0.5]), vec([0.6, 0.4, 0.01, 4.0]))
63 73
 
64 74
     def mat_list(self):
65 75
         cl = np.empty((2, 3 * len(self.faces), 4), dtype=FLOAT)
... ...
@@ -20,9 +20,12 @@ class Player(interactive.InteractiveCamera):
20 20
         self.hud["shl"].draw_mode = GL_LINE_STRIP
21 21
         self.hud["shl"].add_sq(2, 0.96, [0, 0, 0], 2)
22 22
         self.hud["shl"].verts[:] = self.hud["shl"].verts[[0, 1, 3, 2, 0, 3]]
23
+        self.hand = 1
24
+        self.collides = True
23 25
 
24 26
     def update_motion(self, pk, mg):
25 27
         netv = [pk[a] - pk[b] for (a, b) in zip(ARROWS[::2], ARROWS[1::2])]
28
+        if self.collides:
26 29
             for i in range(len(netv)):
27 30
                 for s in range(2):
28 31
                     o = (-1) ** s * self.vdof()[i]
... ...
@@ -36,12 +39,16 @@ class Player(interactive.InteractiveCamera):
36 39
             case pygame.KEYUP:
37 40
                 if ev.unicode == "\x1b":
38 41
                     self.set_focus(False)
42
+                elif "1" <= ev.unicode <= "9":
43
+                    self.hand = int(ev.unicode)
44
+                elif ev.unicode == "n":
45
+                    self.collides = not self.collides
39 46
             case pygame.MOUSEBUTTONUP if ev.button == 1:
40 47
                 self.set_focus(True)
41 48
             case pygame.MOUSEBUTTONDOWN if self.get_focus():
42 49
                 t, n = self.target(scene)
43 50
                 if t:
44
-                    t.click(ev.button, n)
51
+                    t.click(ev.button, n, self.hand)
45 52
 
46 53
     def draw_hud(self, wrs, perf):
47 54
         self.hud["skybox"].pos = self.pos
... ...
@@ -18,6 +18,7 @@ class WorldRegion(mesh.SquareMesh):
18 18
         super().__init__(refls=[0.5, 0.5, 0.5, 32.0])
19 19
         self.grid = np.zeros((size, size, size), dtype=VOXEL_TYPE)
20 20
         self.r = size
21
+        self.dirty = False
21 22
 
22 23
     def set_voxel(self, pos, vt=0):
23 24
         if any(pos < 0) or any(pos >= self.r):
... ...
@@ -72,6 +73,7 @@ class WorldRegion(mesh.SquareMesh):
72 73
                             for _ in range(2):
73 74
                                 self.faces.append(((bp - bc) * (d + 1), p))
74 75
         self.verts = vec(vl)
76
+        self.dirty = False
75 77
 
76 78
     def norm_list(self):
77 79
         return np.astype(np.repeat([axis2offset(a) for (a, _) in self.faces], 3, 0), FLOAT)
... ...
@@ -84,12 +86,10 @@ class WorldRegion(mesh.SquareMesh):
84 86
             for i in range(3):
85 87
                 k = np.arange(self.r + 1) if rd[i] >= 0 else np.arange(self.r + 1, 0, -1)
86 88
                 ids.append([(d, i) for d in (self.pos[i] + k - 0.5 - ro[i]) / rd[i]])
87
-            q, p = None, None
88 89
             for (te, i) in heapq.merge(*ids):
89 90
                 if te < 0 or te < tn or te > tf - 1e-3:
90 91
                     continue
91
-                q, p = p, np.astype(np.round(ro + (te + 1e-4) * rd - self.pos), np.int16)
92
-                assert q is None or np.sum(abs(p - q)) <= 2
92
+                p = np.astype(np.round(ro + (te + 1e-4) * rd - self.pos), np.int16)
93 93
                 if self.grid[*p]:
94 94
                     sa = np.astype((i + 1) * -np.sign(rd[i]), np.int8)
95 95
                     return te, (sa, p)
... ...
@@ -0,0 +1,51 @@
1
+from libs import *
2
+from collections import defaultdict
3
+import region
4
+
5
+class WorldHolder():
6
+    def __init__(self):
7
+        self.regions = {}
8
+
9
+    def fetch(self, pos):
10
+        if pos in self.regions:
11
+            return self.regions[pos]
12
+        else:
13
+            wr = region.WorldRegion()
14
+            wr.pos = vec(pos)
15
+            self.regions[pos] = wr
16
+            self.world_natural(wr)
17
+            wr.remesh()
18
+            wr.onclick = lambda s, b, c, h: self.click_region(s, b, c, h)
19
+            return wr
20
+
21
+    def voxel_at(self, pos, vt=None):
22
+        r = next(iter(self.regions.values())).r
23
+        pr, pf = np.divmod(pos + 0.5, r)
24
+        wr = self.fetch(tuple(int(r * c) for c in pr))
25
+        it = tuple(int(c) for c in pf)
26
+        if vt is None:
27
+            return wr.grid[*it]
28
+        else:
29
+            old = wr.grid[*it]
30
+            wr.grid[*it] = vt
31
+            wr.dirty = True
32
+            return old
33
+
34
+    def click_region(self, wr, button, coords, hand):
35
+        match button:
36
+            case 1:
37
+                wr.set_voxel(coords[1])
38
+            case 2:
39
+                print(f"coords {coords}")
40
+            case 3:
41
+                offset = axis2offset(coords[0])
42
+                p = wr.set_voxel(coords[1] + offset, hand)
43
+                if p is not None:
44
+                    rc = wr.pos + p // wr.r * wr.r
45
+                    awr = self.fetch(tuple(int(x) for x in rc))
46
+                    p = p % wr.r
47
+                    if not awr.grid[*p]:
48
+                        awr.set_voxel(p, hand)
49
+        if button % 2 == 1:
50
+            for i in range(len(VERTEX_ATTRS)):
51
+                update_attr(wr, i)
0 52