voxel-survey.md
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# Voxel accuracy survey A repeatable procedure -- runnable by an agent or a human -- for finding map objects the DRAMATIC_SHAPE mod voxelizes into the wrong shape (a bed extruded to wall height, a table merged into the wall, stairs lying flat) and for fixing them so each object gets the 3D shape it depicts. The loop is: **survey → diagnose → pin → re-survey → spot-check**. ## 1. Survey a location `mods/DRAMATIC_SHAPE/tests/voxel_survey.lua` teleports to a map, walks a list of stand points and screenshots each one flat (the 2D ground truth) and at every voxel camera pitch, with tilt-shift forced off (it would blur exactly the edges under inspection). Levels are set through `Pipelines.setLevel`, so the run never writes the player's options. ```powershell $env:POKEPORT_DRIVER = "mods/DRAMATIC_SHAPE/tests/voxel_survey.lua" $env:SHOT_DIR = "<absolute scratch dir, must exist>" $env:SURVEY_MAP = "REDS_HOUSE_2F" $env:SURVEY_SPOTS = "3,4,up@centre; 2,6,left@bed; 5,2,right@stairs" $env:POKEPORT_SPEED = "4" # optional; the driver scales its waits & lovec.exe . ``` - `SURVEY_SPOTS` is `x,y[,facing][@label]` in cell coordinates, `;` separated. Pick the room centre plus one spot near each object of interest, so every object is seen close up and from more than one parallax. - `SURVEY_LEVELS` defaults to `1,2,3` (15/35/50 degrees). All three matter: tall-object errors scream at 35/50, thin-object errors (a prop with no body) only show near top-down at 15. - The run quits by itself and names shots `<map>_<label>_<flat|v15|v35|v50>.png`. ## 2. Diagnose against the flat shot The `_flat` shot is the authority for what each object IS. Compare every voxel shot against it and record, per object: cells, what it looks like, what it should look like. The recurring failure modes: | symptom | cause | fix class | | --- | --- | --- | | furniture as a wall-height box | detector defaults solid tiles to `wall` | `bed` / `table` / `desk` | | furniture towering 3-6 blocks | flood-fill merged it into the wall region, region consensus adopted the tall height | pin it; pinned tiles leave the region | | walkable art lying flat (stairs, mats) | walkable cells resolve to `ground` | `stair_*`, or leave (mats ARE flat) | | prop as a solid box wrapped in its art | background flood could not reach around it | `billboard` (forced per-pixel prop) | | top-down drawing standing upright | art depicts a surface, class folds it | any `top`-art class (`bed`, `ledge`) | | a house as a cube wearing its own elevation | one drawing packs roof, facade and slopes, and the volume path folds all three upright | a `buildings` template (see below) | To identify tiles: the map's `blocks` list in `data/generated/maps.lua` indexes `tileset.blocks` (0-based; each block is 4x4 tile ids over 2x2 cells), and the atlas is the tileset's `image` PNG, 16 tiles per row. Zoom it with a grid to read ids: ```python from PIL import Image, ImageDraw im = Image.open("assets/generated/tilesets/<atlas>.png").convert("RGB") z = 8; big = im.resize((im.width*z, im.height*z), Image.NEAREST) d = ImageDraw.Draw(big) for t in range(im.width//8 * im.height//8): x, y = (t % 16)*8*z, (t//16)*8*z d.rectangle([x, y, x+8*z-1, y+8*z-1], outline=(255,0,0)) d.text((x+3, y+2), str(t), fill=(255,0,255)) big.save("<scratch>/atlas_ids.png") ``` ## 3. Pin shapes in the profile `mods/DRAMATIC_SHAPE/data/voxel_heights.lua` maps tile ids to classes per tileset id; a pinned tile bypasses detection entirely. The classes: - `bed` (h7, art on top) -- anything drawn from above that lies low. Also the fallback for a drawing that depicts furniture WITH someone on it: a standee would shred it (see the shade note below) and an upright fold duplicates them onto the box's top and front, but a slab draws the whole thing exactly once. - `counter` (h8) -- a service counter: half a cell, one 8px band, so the drawn front panel stands up and the counter top stays on top. Shorter than `table` on purpose; a 12px counter reads as a wall stub. - `table` (h12) / `desk` (h24) -- boxes; the south face folds the drawing upright, flanks wear the front stack darkened, and the top keeps the drawn surface (a meal drawn on the tabletop stays on the tabletop). - `billboard` (10px) / `prop` (5px) / `stool` (5px) / `cutout` (1px) -- standing per-pixel cutouts: TVs, plants, monitors, stools, vases. Segmented by the art's BLACK OUTLINE: background is the shades touching the cluster's edge, flooded in from around it; the outline and everything it encloses survives, paint whites included -- so a white vase cuts cleanly out of a grey tabletop. Solid pixels then split into connected components, each standing on its own feet in the row it is drawn in (two stacked stools stay two stools; nothing floats), and a prop drawn directly above a pinned box stands ON that box (the monitor on its desk, the vase on the table). Touching drawings that must stay separate objects go in different pools. `stool` additionally seats characters: standing on its walkable cell lifts them to its 8px class height. **Check the drawing has a floor margin before reaching for these.** The background is read off the shades touching the cluster's rim, so a drawing that runs edge to edge sees its own body colours flood: the Pokemon Center bench loses 307 of its 420 interior pixels that way and comes out a bare outline. Histogram the rim against the interior first -- if all three non-black shades appear on the rim, no standee pool will work and the object wants a box or a slab instead. - `relief` (h3) -- a prop drawn from above (a game console on the floor): the drawing stays flat and only the pixels inside its outline extrude, art on the top face. - `bookcase` (h32) -- a free-standing shelf drawn tall, not deep: each rank collapses onto a one-cell-deep box at its full drawn height, back rows become hidden floor, and a trim row above that cannot be pinned (shared with other furniture) is adopted as the cap. Pin the book rows and base; leave the shared trim unpinned. - `stair_e` / `stair_w` -- a rising flight of four steps climbing toward the named side, for stairs leading UP. - `stair_down_e` / `stair_down_w` -- a sunken stairwell descending toward the named side, for stairs leading DOWN. Read the drawn railing to pick the side: its high end is where the player enters at floor level. - `wall` -- pin the wall band (and its windows) when de-merging furniture would otherwise leave the auto-detected wall patchy. A whole building is not a tile pin. Its drawing packs several 3D facings at once -- roof from above, facade face-on, ends as diagonal silhouettes -- and no single class covers that, so buildings go in the profile's `buildings` list instead, as a BAND TABLE over the drawing's rows (`mods/DRAMATIC_SHAPE/lib/Buildings.lua`; the pipeline is `mods/DRAMATIC_SHAPE/assets/docs/buidling_to_voxel/sprite_to_voxel_methodology.md`). A template is matched by its exact tile grid, which `mods/DRAMATIC_SHAPE/assets/docs/buildings/` catalogues per building along with every map that places it, so one entry covers all of them. Author only what needs a human to read the drawing -- which rows are roof, how the roof's depth maps onto them, the slab, the eave, an awning band -- because the silhouette, the taper rate (the slope), the eave height and every window are measured off the pixels. Verify a new template against `mods/DRAMATIC_SHAPE/tools/building_voxels.py`, which builds the same model offline and renders isometric previews: the voxel and shell counts it prints must match the runtime's (the mod's `Buildings.stats()`), and it asserts the intent -- symmetric profile, constant taper rate, nothing poking through the roof, every wall column covered. Heights live in the same file; a cell is 16x16 px, a "block" of height is 8. The voxelization must be pixel perfect: every standee and relief voxel carries exactly its source texel, and a segmentation that eats or keeps the wrong pixels (a cut-off monitor, a slab of tabletop in the cutout) is a bug -- fix the pin set or the segmentation, don't accept it. ## 4. Re-survey and compare Re-run step 1 into a fresh `SHOT_DIR` and put before/after side by side. Check every object at every pitch, not just the one you fixed -- pins change region shapes, so neighbours can shift. ## 5. Spot-check the blast radius - Every map sharing the tileset id inherits the pins (grep `tileset = "<id>"` in `data/generated/maps.lua`) -- survey at least one. - A change to the shared analysis (anything in `lib/Structures.lua` rather than the data file) affects every map of that kind; survey one unrelated busy interior (e.g. `OAKS_LAB`) to prove nothing regressed. - `luajit mods/DRAMATIC_SHAPE/tests/dramatic_shape_test.lua` for the headless invariants. ## Gameplay is out of bounds Shape pins are purely presentational. Collision, warps and triggers read the same data they always did -- a stairwell cell is still the walkable warp cell it was when it was flat. If a fix seems to need a collision change, it is the wrong fix.