nodes/world.pyΒΆ
Part of Q1K3.
1"""World / Map system for Q1K3 port.
2
3Mirrors upstream `map.js`:
4
5- Map is a 128^3 grid of voxel cells (xz=32 units, y=16 units).
6- Block list = list of axis-aligned cuboids (each is a static MeshInstance3D
7 + a write into the bool collision array).
8- ``block_at(p)`` and ``block_at_box(min, max)`` query the bitmap for fast AABB
9 collision.
10
11Each block's appearance is one ``MeshInstance3D`` with ``Mesh.cube()`` and a
12``Material(albedo_map=ndarray)`` from `nodes.textures`.
13"""
14
15from __future__ import annotations
16
17import numpy as np
18
19from simvx.core import Material, MeshInstance3D, Node3D, Vec3
20
21from . import meshes, textures
22
23GRID_X = 128
24GRID_Y = 128
25GRID_Z = 128
26
27# Upstream: each block coord uses xz=32, y=16 unit scaling.
28CELL_X = 32
29CELL_Y = 16
30CELL_Z = 32
31
32
33class MapData:
34 """Static collision bitmap for the current map.
35
36 The ``cm`` array is a ``(GRID_X * GRID_Y * GRID_Z)`` boolean grid; True
37 means "this cell is solid". Indexed as ``cm[(z*GRID_Y+y)*GRID_X+x]``.
38 """
39
40 def __init__(self) -> None:
41 self.cm = np.zeros(GRID_X * GRID_Y * GRID_Z, dtype=bool)
42
43 def add_block(self, x: int, y: int, z: int, sx: int, sy: int, sz: int) -> None:
44 """Mark a (sx, sy, sz) sized region starting at (x, y, z) as solid."""
45 for cz in range(z, z + sz):
46 for cy in range(y, y + sy):
47 for cx in range(x, x + sx):
48 if 0 <= cx < GRID_X and 0 <= cy < GRID_Y and 0 <= cz < GRID_Z:
49 self.cm[(cz * GRID_Y + cy) * GRID_X + cx] = True
50
51 def block_at(self, p: Vec3) -> bool:
52 """Test the cell that contains world-space point *p*."""
53 x = int(p.x) >> 5
54 y = int(p.y) >> 4
55 z = int(p.z) >> 5
56 if not (0 <= x < GRID_X and 0 <= y < GRID_Y and 0 <= z < GRID_Z):
57 return False
58 return bool(self.cm[(z * GRID_Y + y) * GRID_X + x])
59
60 def block_at_box(self, bmin: Vec3, bmax: Vec3) -> bool:
61 """Test if any cell in the AABB ``[bmin, bmax]`` is solid."""
62 x0 = max(0, int(bmin.x) >> 5)
63 x1 = min(GRID_X - 1, int(bmax.x) >> 5)
64 y0 = max(0, int(bmin.y) >> 4)
65 y1 = min(GRID_Y - 1, int(bmax.y) >> 4)
66 z0 = max(0, int(bmin.z) >> 5)
67 z1 = min(GRID_Z - 1, int(bmax.z) >> 5)
68 if x0 > x1 or y0 > y1 or z0 > z1:
69 return False
70 for cz in range(z0, z1 + 1):
71 zoff = cz * GRID_Y * GRID_X
72 for cy in range(y0, y1 + 1):
73 yoff = zoff + cy * GRID_X
74 for cx in range(x0, x1 + 1):
75 if self.cm[yoff + cx]:
76 return True
77 return False
78
79
80# ---------------------------------------------------------------------------
81# Block factory: spawn a MeshInstance3D for one cuboid and register it on
82# MapData simultaneously.
83# ---------------------------------------------------------------------------
84
85# Material cache so identical textures share one Material/upload.
86_MATERIAL_CACHE: dict[int, Material] = {}
87
88
89def _material_for(tex_id: int) -> Material:
90 if tex_id not in _MATERIAL_CACHE:
91 img = textures.get(tex_id)
92 # albedo_map takes an RGBA ndarray directly, not only a file path.
93 _MATERIAL_CACHE[tex_id] = Material(albedo_map=img, roughness=0.85, metallic=0.0)
94 return _MATERIAL_CACHE[tex_id]
95
96
97def add_block_node(
98 parent: Node3D,
99 map_data: MapData,
100 x: int,
101 y: int,
102 z: int,
103 sx: int,
104 sy: int,
105 sz: int,
106 tex_id: int,
107) -> MeshInstance3D:
108 """Spawn a textured cube for the given cell range AND register it as
109 collision in *map_data*."""
110 map_data.add_block(x, y, z, sx, sy, sz)
111 # World-space size and centre
112 wsx = sx * CELL_X
113 wsy = sy * CELL_Y
114 wsz = sz * CELL_Z
115 cx = x * CELL_X + wsx / 2
116 cy = y * CELL_Y + wsy / 2
117 cz = z * CELL_Z + wsz / 2
118 inst = MeshInstance3D(
119 mesh=meshes.cube(),
120 material=_material_for(tex_id),
121 position=(cx, cy, cz),
122 scale=(wsx, wsy, wsz),
123 )
124 parent.add_child(inst)
125 return inst