litematica-render 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- litematic/__init__.py +34 -0
- litematic/parse/__init__.py +1 -0
- litematic/parse/errors.py +13 -0
- litematic/parse/model.py +225 -0
- litematic/parse/nbt.py +167 -0
- litematic/parse/parser.py +317 -0
- litematic/render/__init__.py +134 -0
- litematic/render/errors.py +9 -0
- litematic/render/pack/__init__.py +7 -0
- litematic/render/pack/blockstate.py +111 -0
- litematic/render/pack/compile.py +423 -0
- litematic/render/pack/elements.py +421 -0
- litematic/render/pack/entities.py +181 -0
- litematic/render/pack/extract.py +243 -0
- litematic/render/pack/loader.py +186 -0
- litematic/render/pack/resolve.py +344 -0
- litematic/render/pack/stack.py +83 -0
- litematic/render/pack/texture.py +347 -0
- litematic/render/png.py +54 -0
- litematic/render/quads.py +106 -0
- litematic/render/scene/__init__.py +6 -0
- litematic/render/scene/isometric.py +579 -0
- litematic/render/scene/painter.py +395 -0
- litematic/render/shapes/__init__.py +6 -0
- litematic/render/shapes/boxes.py +290 -0
- litematic/render/shapes/families.py +357 -0
- litematica_render-0.1.0.dist-info/METADATA +52 -0
- litematica_render-0.1.0.dist-info/RECORD +30 -0
- litematica_render-0.1.0.dist-info/WHEEL +4 -0
- litematica_render-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
"""Scene composition: quad expansion, yaw remap, camera filtering, AO."""
|
|
2
|
+
|
|
3
|
+
from typing import NamedTuple
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from litematic.parse.model import Litematic
|
|
8
|
+
from litematic.render.errors import RenderError
|
|
9
|
+
from litematic.render.pack.resolve import Resolver
|
|
10
|
+
from litematic.render.quads import (
|
|
11
|
+
AO_LEVELS,
|
|
12
|
+
CULL_NONE,
|
|
13
|
+
TIER_DOWN,
|
|
14
|
+
TIER_EAST,
|
|
15
|
+
TIER_NORTH,
|
|
16
|
+
TIER_SOUTH,
|
|
17
|
+
TIER_UP,
|
|
18
|
+
TIER_WEST,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
_AIR_NAMES = {"minecraft:air", "minecraft:cave_air", "minecraft:void_air"}
|
|
22
|
+
_WATER_FAMILY = {"minecraft:water", "minecraft:bubble_column"}
|
|
23
|
+
_Y_SLAB = 64
|
|
24
|
+
_CAMERA_TIERS = (TIER_UP, TIER_SOUTH, TIER_EAST)
|
|
25
|
+
|
|
26
|
+
# Face normal vectors per tier in block space; AO samples sit one step out
|
|
27
|
+
# along the normal and one step along each tangent sign.
|
|
28
|
+
_TIER_NORMALS = np.zeros((6, 3), np.int64)
|
|
29
|
+
_TIER_NORMALS[TIER_DOWN] = (0, -1, 0)
|
|
30
|
+
_TIER_NORMALS[TIER_UP] = (0, 1, 0)
|
|
31
|
+
_TIER_NORMALS[TIER_NORTH] = (0, 0, -1)
|
|
32
|
+
_TIER_NORMALS[TIER_SOUTH] = (0, 0, 1)
|
|
33
|
+
_TIER_NORMALS[TIER_WEST] = (-1, 0, 0)
|
|
34
|
+
_TIER_NORMALS[TIER_EAST] = (1, 0, 0)
|
|
35
|
+
_TIER_OF_NORMAL = {tuple(row): tier for tier, row in enumerate(_TIER_NORMALS)}
|
|
36
|
+
|
|
37
|
+
# Per tier: normal axis then the two tangent axes for AO corner sampling.
|
|
38
|
+
_TIER_AXES = np.zeros((6, 3), np.int64)
|
|
39
|
+
_TIER_AXES[TIER_DOWN] = (1, 0, 2)
|
|
40
|
+
_TIER_AXES[TIER_UP] = (1, 0, 2)
|
|
41
|
+
_TIER_AXES[TIER_NORTH] = (2, 0, 1)
|
|
42
|
+
_TIER_AXES[TIER_SOUTH] = (2, 0, 1)
|
|
43
|
+
_TIER_AXES[TIER_WEST] = (0, 1, 2)
|
|
44
|
+
_TIER_AXES[TIER_EAST] = (0, 1, 2)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class _Scene(NamedTuple):
|
|
48
|
+
"""Camera-facing quads after yaw rotation and vertex AO, scene coords."""
|
|
49
|
+
|
|
50
|
+
x: np.ndarray # int32 per-quad block x
|
|
51
|
+
y: np.ndarray # int32 per-quad block y
|
|
52
|
+
z: np.ndarray # int32 per-quad block z
|
|
53
|
+
quad_vtx: np.ndarray # (Q, 4, 3) f32 block-local, yaw rotated
|
|
54
|
+
quad_uv: np.ndarray # (Q, 4, 2) f32 texel coords, never rotated
|
|
55
|
+
quad_ao: np.ndarray # (Q, 4) f32 per-vertex AO multipliers
|
|
56
|
+
quad_shade: np.ndarray # (Q,) u1 tier, limited to up/south/east
|
|
57
|
+
quad_dbl: np.ndarray # (Q,) bool double-sided flag
|
|
58
|
+
texkey_ids: np.ndarray # (Q,) i32 index into texkeys
|
|
59
|
+
texkeys: dict[str, np.ndarray] # unique texkey -> (16, 16, 4) array
|
|
60
|
+
span_w: int # canvas width in pixels = 2t * span_w at scale t
|
|
61
|
+
span_h: int # canvas height in pixels = t * span_h at scale t
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _yaw_rot(
|
|
65
|
+
x: np.ndarray, z: np.ndarray, yaw: int, w: int, d: int
|
|
66
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
67
|
+
if yaw == 1:
|
|
68
|
+
return z, (w - 1) - x
|
|
69
|
+
if yaw == 2:
|
|
70
|
+
return (w - 1) - x, (d - 1) - z
|
|
71
|
+
if yaw == 3:
|
|
72
|
+
return (d - 1) - z, x
|
|
73
|
+
return x, z
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _yaw_tier_map(yaw: int) -> np.ndarray:
|
|
77
|
+
"""Remap the six face tiers through the yaw rotation formula."""
|
|
78
|
+
out = np.empty(6, np.uint8)
|
|
79
|
+
for tier in range(6):
|
|
80
|
+
nx, ny, nz = (int(v) for v in _TIER_NORMALS[tier])
|
|
81
|
+
if yaw == 1:
|
|
82
|
+
rx, rz = nz, -nx
|
|
83
|
+
elif yaw == 2:
|
|
84
|
+
rx, rz = -nx, -nz
|
|
85
|
+
elif yaw == 3:
|
|
86
|
+
rx, rz = -nz, nx
|
|
87
|
+
else:
|
|
88
|
+
rx, rz = nx, nz
|
|
89
|
+
out[tier] = _TIER_OF_NORMAL[(rx, ny, rz)]
|
|
90
|
+
return out
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
_YAW_TIER_MAPS = tuple(_yaw_tier_map(yaw) for yaw in range(4))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _horizontal_cull_dirs(
|
|
97
|
+
yaw: int,
|
|
98
|
+
) -> tuple[tuple[int, int], tuple[int, int]]:
|
|
99
|
+
"""World-space neighbor steps whose faces face the camera after yaw."""
|
|
100
|
+
dirs: list[tuple[int, int]] = []
|
|
101
|
+
for tier in (TIER_EAST, TIER_SOUTH):
|
|
102
|
+
nx, _, nz = (int(v) for v in _TIER_NORMALS[tier])
|
|
103
|
+
for _ in range((4 - yaw) % 4):
|
|
104
|
+
nx, nz = nz, -nx
|
|
105
|
+
dirs.append((nx, nz))
|
|
106
|
+
return dirs[0], dirs[1]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class _Luts(NamedTuple):
|
|
110
|
+
"""Per-palette-entry lookups for culling and quad expansion."""
|
|
111
|
+
|
|
112
|
+
is_air: np.ndarray # bool
|
|
113
|
+
occludes: np.ndarray # bool
|
|
114
|
+
pool: np.ndarray | None # (P,) QUAD_DTYPE, None when every entry is air
|
|
115
|
+
offsets: np.ndarray # (E + 1,) i8 start of each entry's rows in pool
|
|
116
|
+
texkey_ids: np.ndarray # (P,) i32 per-pool-row texkey id
|
|
117
|
+
cull_class: np.ndarray # (P,) i32 pair class, negative entries never pair
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class _Arena(NamedTuple):
|
|
121
|
+
"""Preallocated scene columns filled slab by slab."""
|
|
122
|
+
|
|
123
|
+
x: np.ndarray # (K,) i32 world block x per quad
|
|
124
|
+
y: np.ndarray # (K,) i32 world block y per quad
|
|
125
|
+
z: np.ndarray # (K,) i32 world block z per quad
|
|
126
|
+
vtx: np.ndarray # (K, 4, 3) f32 block-local, yaw rotated
|
|
127
|
+
uv: np.ndarray # (K, 4, 2) f32
|
|
128
|
+
ao: np.ndarray # (K, 4) f32
|
|
129
|
+
shade: np.ndarray # (K,) u1 camera-facing tier
|
|
130
|
+
dbl: np.ndarray # (K,) bool
|
|
131
|
+
texkey: np.ndarray # (K,) i32
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _new_arena(capacity: int) -> _Arena:
|
|
135
|
+
"""Allocate the exact scene columns for the counted quad total."""
|
|
136
|
+
return _Arena(
|
|
137
|
+
np.zeros(capacity, np.int32),
|
|
138
|
+
np.zeros(capacity, np.int32),
|
|
139
|
+
np.zeros(capacity, np.int32),
|
|
140
|
+
np.zeros((capacity, 4, 3), np.float32),
|
|
141
|
+
np.zeros((capacity, 4, 2), np.float32),
|
|
142
|
+
np.zeros((capacity, 4), np.float32),
|
|
143
|
+
np.zeros(capacity, np.uint8),
|
|
144
|
+
np.zeros(capacity, bool),
|
|
145
|
+
np.zeros(capacity, np.int32),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _compose(schem: Litematic, yaw: int, textures: Resolver) -> _Scene:
|
|
150
|
+
"""Collect visible blocks from all regions into a quad scene."""
|
|
151
|
+
try:
|
|
152
|
+
return _compose_inner(schem, yaw, textures)
|
|
153
|
+
except RenderError:
|
|
154
|
+
raise
|
|
155
|
+
except Exception as exc:
|
|
156
|
+
raise RenderError(f"compose: scene composition failed: {exc}") from exc
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _compose_inner(schem: Litematic, yaw: int, textures: Resolver) -> _Scene:
|
|
160
|
+
"""Expand every region's visible cells into yawed, filtered quads.
|
|
161
|
+
|
|
162
|
+
Pass A stashes per-slab visible cells and counts the exact camera-facing
|
|
163
|
+
quad total; pass B expands each slab straight into the preallocated
|
|
164
|
+
arena through a rolling occlusion window, so no intermediate list of
|
|
165
|
+
slab pieces is ever held.
|
|
166
|
+
"""
|
|
167
|
+
key_ids: dict[str, int] = {}
|
|
168
|
+
key_arrays: dict[str, np.ndarray] = {}
|
|
169
|
+
tier_map = _YAW_TIER_MAPS[yaw]
|
|
170
|
+
dir_a, dir_b = _horizontal_cull_dirs(yaw)
|
|
171
|
+
plan = []
|
|
172
|
+
capacity = 0
|
|
173
|
+
for name in sorted(schem.regions):
|
|
174
|
+
region = schem.regions[name]
|
|
175
|
+
luts = _palette_luts(region.palette, textures, key_ids, key_arrays)
|
|
176
|
+
if luts.pool is None:
|
|
177
|
+
continue
|
|
178
|
+
cam_rows = _camera_row_counts(luts, tier_map)
|
|
179
|
+
stash = list(_visible_slabs(region, luts, dir_a, dir_b))
|
|
180
|
+
capacity += sum(
|
|
181
|
+
int(cam_rows[picked.astype(np.int64)].sum())
|
|
182
|
+
for (*_, picked) in stash
|
|
183
|
+
)
|
|
184
|
+
plan.append((region, luts, stash))
|
|
185
|
+
if capacity == 0:
|
|
186
|
+
return _empty_scene()
|
|
187
|
+
arena = _new_arena(capacity)
|
|
188
|
+
written = 0
|
|
189
|
+
for region, luts, stash in plan:
|
|
190
|
+
px, py, pz = region.position.x, region.position.y, region.position.z
|
|
191
|
+
for y0, y1, lx, ly, lz, picked in stash:
|
|
192
|
+
written += _expand_slab_into(
|
|
193
|
+
arena, written, y0, y1, lx, ly, lz, picked,
|
|
194
|
+
luts, region, tier_map, yaw, (px, py, pz),
|
|
195
|
+
)
|
|
196
|
+
stash.clear()
|
|
197
|
+
x = arena.x[:written]
|
|
198
|
+
y = arena.y[:written]
|
|
199
|
+
z = arena.z[:written]
|
|
200
|
+
x = x - int(x.min())
|
|
201
|
+
y = y - int(y.min())
|
|
202
|
+
z = z - int(z.min())
|
|
203
|
+
rx, rz = _yaw_rot(x, z, yaw, int(x.max()) + 1, int(z.max()) + 1)
|
|
204
|
+
span_w = int(rx.max()) + int(rz.max()) + 2
|
|
205
|
+
span_h = int(rx.max()) + int(rz.max()) + 2 * int(y.max()) + 4
|
|
206
|
+
# Copy so the capacity-sized arena is not pinned by views.
|
|
207
|
+
return _Scene(
|
|
208
|
+
rx.astype(np.int32),
|
|
209
|
+
y.astype(np.int32),
|
|
210
|
+
rz.astype(np.int32),
|
|
211
|
+
arena.vtx[:written].copy(),
|
|
212
|
+
arena.uv[:written].copy(),
|
|
213
|
+
arena.ao[:written].copy(),
|
|
214
|
+
arena.shade[:written].copy(),
|
|
215
|
+
arena.dbl[:written].copy(),
|
|
216
|
+
arena.texkey[:written].copy(),
|
|
217
|
+
dict(key_arrays),
|
|
218
|
+
span_w,
|
|
219
|
+
span_h,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _camera_row_counts(luts: _Luts, tier_map: np.ndarray) -> np.ndarray:
|
|
224
|
+
"""Camera-facing pool-row count per palette entry under this yaw."""
|
|
225
|
+
pool = luts.pool
|
|
226
|
+
if pool is None:
|
|
227
|
+
return np.zeros(len(luts.offsets) - 1, np.int64)
|
|
228
|
+
counts = np.zeros(len(luts.offsets) - 1, np.int64)
|
|
229
|
+
for i in range(len(counts)):
|
|
230
|
+
rows = pool[luts.offsets[i] : luts.offsets[i + 1]]
|
|
231
|
+
counts[i] = int(np.isin(tier_map[rows["shade"]], _CAMERA_TIERS).sum())
|
|
232
|
+
return counts
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _empty_scene() -> _Scene:
|
|
236
|
+
"""Return the zero-quad scene with the v1 default canvas spans."""
|
|
237
|
+
return _Scene(
|
|
238
|
+
np.zeros(0, np.int32),
|
|
239
|
+
np.zeros(0, np.int32),
|
|
240
|
+
np.zeros(0, np.int32),
|
|
241
|
+
np.zeros((0, 4, 3), np.float32),
|
|
242
|
+
np.zeros((0, 4, 2), np.float32),
|
|
243
|
+
np.zeros((0, 4), np.float32),
|
|
244
|
+
np.zeros(0, np.uint8),
|
|
245
|
+
np.zeros(0, bool),
|
|
246
|
+
np.zeros(0, np.int32),
|
|
247
|
+
{},
|
|
248
|
+
2,
|
|
249
|
+
4,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _cull_classes(palette) -> np.ndarray:
|
|
254
|
+
"""Pair class per palette entry; negative entries never pair.
|
|
255
|
+
|
|
256
|
+
Same block name pairs (glass, ice, slime, water levels); the water
|
|
257
|
+
family pairs across names; leaves never pair (cutout leaves default).
|
|
258
|
+
"""
|
|
259
|
+
classes = np.full(len(palette), -1, np.int32)
|
|
260
|
+
ids: dict[str, int] = {}
|
|
261
|
+
for i, state in enumerate(palette):
|
|
262
|
+
if state.name in _AIR_NAMES:
|
|
263
|
+
continue
|
|
264
|
+
if state.name.partition(":")[2].endswith("leaves"):
|
|
265
|
+
continue
|
|
266
|
+
if state.name in _WATER_FAMILY:
|
|
267
|
+
classes[i] = 0
|
|
268
|
+
continue
|
|
269
|
+
key = state.name
|
|
270
|
+
if key not in ids:
|
|
271
|
+
ids[key] = len(ids) + 1
|
|
272
|
+
classes[i] = ids[key]
|
|
273
|
+
return classes
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _palette_luts(
|
|
277
|
+
palette, textures: Resolver, key_ids: dict[str, int], key_arrays: dict
|
|
278
|
+
) -> _Luts:
|
|
279
|
+
"""Resolve every palette entry to quads, occl flags, and texkey ids."""
|
|
280
|
+
count = len(palette)
|
|
281
|
+
is_air = np.zeros(count, bool)
|
|
282
|
+
occludes = np.zeros(count, bool)
|
|
283
|
+
cull_class = _cull_classes(palette)
|
|
284
|
+
quads_pieces: list[np.ndarray] = []
|
|
285
|
+
id_pieces: list[np.ndarray] = []
|
|
286
|
+
lengths = np.zeros(count + 1, np.int64)
|
|
287
|
+
for i, state in enumerate(palette):
|
|
288
|
+
if state.name in _AIR_NAMES:
|
|
289
|
+
is_air[i] = True
|
|
290
|
+
continue
|
|
291
|
+
res = textures.resolve(state)
|
|
292
|
+
occludes[i] = res.occludes
|
|
293
|
+
quads_pieces.append(res.quads)
|
|
294
|
+
id_pieces.append(_texkey_ids(res.quads, textures, key_ids, key_arrays))
|
|
295
|
+
lengths[i + 1] = len(res.quads)
|
|
296
|
+
if not quads_pieces:
|
|
297
|
+
return _Luts(
|
|
298
|
+
is_air, occludes, None, np.cumsum(lengths), np.zeros(0, np.int32),
|
|
299
|
+
cull_class,
|
|
300
|
+
)
|
|
301
|
+
pool = (
|
|
302
|
+
np.concatenate(quads_pieces) if len(quads_pieces) > 1 else quads_pieces[0]
|
|
303
|
+
)
|
|
304
|
+
ids = np.concatenate(id_pieces) if len(id_pieces) > 1 else id_pieces[0]
|
|
305
|
+
return _Luts(is_air, occludes, pool, np.cumsum(lengths), ids, cull_class)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _texkey_ids(
|
|
309
|
+
quads: np.ndarray,
|
|
310
|
+
textures: Resolver,
|
|
311
|
+
key_ids: dict[str, int],
|
|
312
|
+
key_arrays: dict,
|
|
313
|
+
) -> np.ndarray:
|
|
314
|
+
"""Map a quad array's texkeys to shared scene ids, registering arrays."""
|
|
315
|
+
unique, inverse = np.unique(quads["texkey"], return_inverse=True)
|
|
316
|
+
keys = [str(key) for key in unique]
|
|
317
|
+
fresh = [key for key in keys if key not in key_ids]
|
|
318
|
+
arrays = textures.texkey_arrays(fresh)
|
|
319
|
+
for key in fresh:
|
|
320
|
+
key_ids[key] = len(key_ids)
|
|
321
|
+
key_arrays[key] = arrays[key]
|
|
322
|
+
lut = np.array([key_ids[key] for key in keys], np.int32)
|
|
323
|
+
return lut[inverse.reshape(-1)]
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _occlusion_window(
|
|
327
|
+
widx: np.ndarray, occludes: np.ndarray, wy0: int
|
|
328
|
+
) -> tuple[np.ndarray, tuple[int, int, int], int]:
|
|
329
|
+
"""False-padded flat occlusion grid over the window's index rows.
|
|
330
|
+
|
|
331
|
+
AO and cullface lookups reach at most one row beyond the slab, so the
|
|
332
|
+
window (rows [wy0, wy1), unpacked on demand) never needs the rest of
|
|
333
|
+
the region; region-edge rows fall to the False padding exactly like
|
|
334
|
+
the vanilla out-of-bounds air.
|
|
335
|
+
"""
|
|
336
|
+
sx, win_h, sz = widx.shape
|
|
337
|
+
padded = np.zeros((sx + 2, win_h + 2, sz + 2), bool)
|
|
338
|
+
padded[1 : sx + 1, 1 : win_h + 1, 1 : sz + 1] = occludes[widx]
|
|
339
|
+
strides = ((win_h + 2) * (sz + 2), sz + 2, 1)
|
|
340
|
+
return padded.ravel(), strides, wy0
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _visible_slabs(
|
|
344
|
+
region,
|
|
345
|
+
luts: _Luts,
|
|
346
|
+
dir_a: tuple[int, int],
|
|
347
|
+
dir_b: tuple[int, int],
|
|
348
|
+
):
|
|
349
|
+
"""Yield each slab's row span plus its visible cells for the arena."""
|
|
350
|
+
sx, sy, sz = region.size
|
|
351
|
+
is_air, occludes = luts.is_air, luts.occludes
|
|
352
|
+
for y0 in range(0, sy, _Y_SLAB):
|
|
353
|
+
y1 = min(y0 + _Y_SLAB, sy)
|
|
354
|
+
body = region.slab_indices(y0, y1)
|
|
355
|
+
present = ~is_air[body]
|
|
356
|
+
occ_body = occludes[body]
|
|
357
|
+
upper = occludes[body][:, 1:, :]
|
|
358
|
+
if y1 == sy:
|
|
359
|
+
next_row = np.zeros((sx, 1, sz), bool)
|
|
360
|
+
else:
|
|
361
|
+
# One extra row past the slab edge; a single-row unpack keeps
|
|
362
|
+
# the visibility pass from paying for the next slab's body.
|
|
363
|
+
next_row = occludes[region.slab_indices(y1, y1 + 1)]
|
|
364
|
+
upper = np.concatenate([upper, next_row], axis=1)
|
|
365
|
+
occ_a = _shifted(occ_body, *dir_a)
|
|
366
|
+
occ_b = _shifted(occ_body, *dir_b)
|
|
367
|
+
visible = present & ~(upper & occ_a & occ_b)
|
|
368
|
+
if not visible.any():
|
|
369
|
+
continue
|
|
370
|
+
lx, ly, lz = np.nonzero(visible)
|
|
371
|
+
yield y0, y1, lx, ly + y0, lz, body[visible]
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _shifted(grid: np.ndarray, dx: int, dz: int) -> np.ndarray:
|
|
375
|
+
"""Neighbor occlusion one unit step along (dx, dz); outside is False."""
|
|
376
|
+
out = np.zeros_like(grid)
|
|
377
|
+
if dx > 0:
|
|
378
|
+
out[:-1, :, :] = grid[1:, :, :]
|
|
379
|
+
elif dx < 0:
|
|
380
|
+
out[1:, :, :] = grid[:-1, :, :]
|
|
381
|
+
elif dz > 0:
|
|
382
|
+
out[:, :, :-1] = grid[:, :, 1:]
|
|
383
|
+
elif dz < 0:
|
|
384
|
+
out[:, :, 1:] = grid[:, :, :-1]
|
|
385
|
+
return out
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _expand_slab_into(
|
|
389
|
+
arena: _Arena,
|
|
390
|
+
offset: int,
|
|
391
|
+
y0: int,
|
|
392
|
+
y1: int,
|
|
393
|
+
lx: np.ndarray,
|
|
394
|
+
ly: np.ndarray,
|
|
395
|
+
lz: np.ndarray,
|
|
396
|
+
picked: np.ndarray,
|
|
397
|
+
luts: _Luts,
|
|
398
|
+
region,
|
|
399
|
+
tier_map: np.ndarray,
|
|
400
|
+
yaw: int,
|
|
401
|
+
position: tuple[int, int, int],
|
|
402
|
+
) -> int:
|
|
403
|
+
"""Expand one stashed slab into the arena; return the quads written."""
|
|
404
|
+
picked = picked.astype(np.int64)
|
|
405
|
+
pool = luts.pool
|
|
406
|
+
if pool is None:
|
|
407
|
+
return 0
|
|
408
|
+
counts = luts.offsets[picked + 1] - luts.offsets[picked]
|
|
409
|
+
total = int(counts.sum())
|
|
410
|
+
if total == 0:
|
|
411
|
+
return 0
|
|
412
|
+
cell_of = np.repeat(
|
|
413
|
+
np.arange(len(picked), dtype=np.int32), counts
|
|
414
|
+
)
|
|
415
|
+
starts = np.cumsum(counts) - counts
|
|
416
|
+
within = np.arange(total, dtype=np.int64) - np.repeat(starts, counts)
|
|
417
|
+
pool_idx = np.repeat(luts.offsets[picked], counts) + within
|
|
418
|
+
# Structured rows carry a 256-byte texkey, so gather fields and chunk.
|
|
419
|
+
model_shade = pool["shade"][pool_idx]
|
|
420
|
+
keep = np.isin(tier_map[model_shade], _CAMERA_TIERS)
|
|
421
|
+
kept_at = np.flatnonzero(keep)
|
|
422
|
+
pool_rows = pool_idx[kept_at]
|
|
423
|
+
del pool_idx, keep
|
|
424
|
+
model_shade = model_shade[kept_at]
|
|
425
|
+
cell_of = cell_of[kept_at]
|
|
426
|
+
new_tier = tier_map[model_shade]
|
|
427
|
+
wy0 = max(y0 - 1, 0)
|
|
428
|
+
widx = region.slab_indices(wy0, min(y1 + 1, region.size.y))
|
|
429
|
+
occ = _occlusion_window(widx, luts.occludes, wy0)
|
|
430
|
+
px, py, pz = position
|
|
431
|
+
written = 0
|
|
432
|
+
chunk = 1 << 20
|
|
433
|
+
for start in range(0, len(pool_rows), chunk):
|
|
434
|
+
stop = min(start + chunk, len(pool_rows))
|
|
435
|
+
rows_c = pool_rows[start:stop]
|
|
436
|
+
cell_of_c = cell_of[start:stop]
|
|
437
|
+
cells = np.stack(
|
|
438
|
+
[lx[cell_of_c], ly[cell_of_c], lz[cell_of_c]], axis=1
|
|
439
|
+
)
|
|
440
|
+
alive = _cull_alive(
|
|
441
|
+
pool["cull"][rows_c],
|
|
442
|
+
cells,
|
|
443
|
+
picked[cell_of_c],
|
|
444
|
+
luts.cull_class,
|
|
445
|
+
occ,
|
|
446
|
+
widx,
|
|
447
|
+
wy0,
|
|
448
|
+
region.size.y,
|
|
449
|
+
)
|
|
450
|
+
cells = cells[alive]
|
|
451
|
+
rows = rows_c[alive]
|
|
452
|
+
vtx = pool["vtx"][rows]
|
|
453
|
+
dbl = pool["dbl"][rows]
|
|
454
|
+
end = offset + written + len(cells)
|
|
455
|
+
arena.x[offset + written : end] = cells[:, 0] + px
|
|
456
|
+
arena.y[offset + written : end] = cells[:, 1] + py
|
|
457
|
+
arena.z[offset + written : end] = cells[:, 2] + pz
|
|
458
|
+
arena.vtx[offset + written : end] = _rotate_vertices(vtx, yaw)
|
|
459
|
+
arena.uv[offset + written : end] = pool["uv"][rows]
|
|
460
|
+
arena.ao[offset + written : end] = _vertex_ao(
|
|
461
|
+
vtx, model_shade[start:stop][alive], dbl, cells, *occ
|
|
462
|
+
)
|
|
463
|
+
arena.shade[offset + written : end] = new_tier[start:stop][alive]
|
|
464
|
+
arena.dbl[offset + written : end] = dbl
|
|
465
|
+
arena.texkey[offset + written : end] = luts.texkey_ids[rows]
|
|
466
|
+
written += len(cells)
|
|
467
|
+
return written
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _cull_alive(
|
|
471
|
+
cull: np.ndarray,
|
|
472
|
+
cells: np.ndarray,
|
|
473
|
+
cell_palette: np.ndarray,
|
|
474
|
+
cull_class: np.ndarray,
|
|
475
|
+
occ: tuple[np.ndarray, tuple[int, int, int], int],
|
|
476
|
+
widx: np.ndarray,
|
|
477
|
+
wy0: int,
|
|
478
|
+
sy: int,
|
|
479
|
+
) -> np.ndarray:
|
|
480
|
+
"""Alive mask after neighbor culling; no-cullface rows always alive."""
|
|
481
|
+
mask = np.ones(len(cull), bool)
|
|
482
|
+
rows = np.nonzero(cull != CULL_NONE)[0]
|
|
483
|
+
if len(rows) == 0:
|
|
484
|
+
return mask
|
|
485
|
+
nb = cells[rows] + _TIER_NORMALS[cull[rows].astype(np.int64)]
|
|
486
|
+
sx, _, sz = widx.shape
|
|
487
|
+
valid = (
|
|
488
|
+
(nb[..., 0] >= 0) & (nb[..., 0] < sx)
|
|
489
|
+
& (nb[..., 1] >= 0) & (nb[..., 1] < sy)
|
|
490
|
+
& (nb[..., 2] >= 0) & (nb[..., 2] < sz)
|
|
491
|
+
)
|
|
492
|
+
nb_clipped = np.stack(
|
|
493
|
+
[
|
|
494
|
+
np.clip(nb[..., 0], 0, sx - 1),
|
|
495
|
+
np.clip(nb[..., 1], 0, sy - 1),
|
|
496
|
+
np.clip(nb[..., 2], 0, sz - 1),
|
|
497
|
+
],
|
|
498
|
+
axis=1,
|
|
499
|
+
)
|
|
500
|
+
flat, (st_x, st_y, st_z), win_y0 = occ
|
|
501
|
+
win_h = st_x // st_y - 2
|
|
502
|
+
nb_pad = np.stack(
|
|
503
|
+
[
|
|
504
|
+
np.clip(nb[..., 0], -1, sx + 1),
|
|
505
|
+
np.clip(nb[..., 1], wy0 - 1, wy0 + win_h),
|
|
506
|
+
np.clip(nb[..., 2], -1, sz + 1),
|
|
507
|
+
],
|
|
508
|
+
axis=1,
|
|
509
|
+
)
|
|
510
|
+
occluded = flat[
|
|
511
|
+
(nb_pad[..., 0] + 1) * st_x
|
|
512
|
+
+ (nb_pad[..., 1] + 1 - win_y0) * st_y
|
|
513
|
+
+ (nb_pad[..., 2] + 1) * st_z
|
|
514
|
+
]
|
|
515
|
+
neighbor_pi = widx[
|
|
516
|
+
nb_clipped[..., 0],
|
|
517
|
+
nb_clipped[..., 1] - wy0,
|
|
518
|
+
nb_clipped[..., 2],
|
|
519
|
+
]
|
|
520
|
+
own = cull_class[cell_palette[rows]]
|
|
521
|
+
pair = (
|
|
522
|
+
valid
|
|
523
|
+
& (own >= 0)
|
|
524
|
+
& (own == cull_class[neighbor_pi])
|
|
525
|
+
)
|
|
526
|
+
mask[rows] = ~(occluded | pair)
|
|
527
|
+
return mask
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def _rotate_vertices(vtx: np.ndarray, yaw: int) -> np.ndarray:
|
|
531
|
+
"""Rotate block-local vertices by the yaw formula, uv left untouched."""
|
|
532
|
+
out = vtx
|
|
533
|
+
for _ in range(yaw % 4):
|
|
534
|
+
x, y, z = out[..., 0], out[..., 1], out[..., 2]
|
|
535
|
+
out = np.stack([z, y, 16.0 - x], axis=-1)
|
|
536
|
+
return np.asarray(out, dtype=np.float32)
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _vertex_ao(
|
|
540
|
+
vtx: np.ndarray,
|
|
541
|
+
shade: np.ndarray,
|
|
542
|
+
dbl: np.ndarray,
|
|
543
|
+
cells: np.ndarray,
|
|
544
|
+
flat: np.ndarray,
|
|
545
|
+
strides: tuple[int, int, int],
|
|
546
|
+
y_base: int = 0,
|
|
547
|
+
) -> np.ndarray:
|
|
548
|
+
"""Per-vertex AO multipliers from the padded flat grid; vtx is pre-yaw."""
|
|
549
|
+
tier = shade.astype(np.int32)
|
|
550
|
+
axes = _TIER_AXES[tier]
|
|
551
|
+
normals = _TIER_NORMALS[tier]
|
|
552
|
+
st_x, st_y, st_z = strides
|
|
553
|
+
steps = np.array([st_x, st_y, st_z], np.int32)
|
|
554
|
+
t1 = steps[axes[:, 1]]
|
|
555
|
+
t2 = steps[axes[:, 2]]
|
|
556
|
+
pick1 = axes[:, 1][:, None, None]
|
|
557
|
+
pick2 = axes[:, 2][:, None, None]
|
|
558
|
+
# A vertex exactly on the block center plane (coord 8.0) signs to 0 and
|
|
559
|
+
# samples the straight-out neighbor; the shape table keeps coords off 8.
|
|
560
|
+
s1 = np.sign(np.take_along_axis(vtx, pick1, axis=2)[:, :, 0] - 8.0)
|
|
561
|
+
s2 = np.sign(np.take_along_axis(vtx, pick2, axis=2)[:, :, 0] - 8.0)
|
|
562
|
+
s1 = s1.astype(np.int32)
|
|
563
|
+
s2 = s2.astype(np.int32)
|
|
564
|
+
cells32 = cells.astype(np.int32)
|
|
565
|
+
base = (
|
|
566
|
+
(cells32[:, 0] + 1) * np.int32(st_x)
|
|
567
|
+
+ (cells32[:, 1] + 1 - y_base) * np.int32(st_y)
|
|
568
|
+
+ (cells32[:, 2] + 1) * np.int32(st_z)
|
|
569
|
+
+ normals[:, 0] * np.int32(st_x)
|
|
570
|
+
+ normals[:, 1] * np.int32(st_y)
|
|
571
|
+
+ normals[:, 2] * np.int32(st_z)
|
|
572
|
+
)[:, None]
|
|
573
|
+
side1 = flat[base + s1 * t1[:, None]].astype(np.int32)
|
|
574
|
+
side2 = flat[base + s2 * t2[:, None]].astype(np.int32)
|
|
575
|
+
corner = flat[base + s1 * t1[:, None] + s2 * t2[:, None]].astype(np.int32)
|
|
576
|
+
level = np.where(side1 & side2, 0, 3 - (side1 + side2 + corner))
|
|
577
|
+
ao = np.asarray(AO_LEVELS, np.float32)[level]
|
|
578
|
+
ao[dbl] = 1.0
|
|
579
|
+
return ao
|