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