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: |
| ... | ... |
@@ -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 |