Source: procedural-hong-kong-building/source/procedural_building.blend, node group
build system (592 nodes) on object Cube.001. This document is the reference for the
TypeScript port in src/generator.ts. Coordinates are Blender-style (Z-up, meters);
the Three.js scene rotates the root group -90° around X.
| name | type | default in file |
|---|---|---|
| floor | int | 6 |
| length | int | 7 |
| width | int | 3 |
| AC UNIT | 0-1 | 0.724 |
| Roof Probability | 0-1 | 0.512 |
| Clothline Probability | 0-1 | 0.709 |
| Lights | 0-1 | 0.546 |
| window type | 0-1 | 0.75 (P of steel vs wood) |
| window open amount | 0-1 | 0 |
| curtain close | 0-1 | 0 |
| closed/open store | 0-1 | 0.598 |
| roof on store | 0-1 | 0.594 |
| object on ground | 0-1 | 1.0 |
| store sign | 0-1 | 0.748 |
| object on roof | 0-1 | 0.835 |
| randomise | int | seed |
| deform | int | realize-instances toggle (irrelevant in JS) |
| low poly | int | swap detail for plain shell |
Blender Random Value = pure function of (element id, seed). With ID unlinked it uses
the element index within the current geometry — and geometry gets re-indexed after
every SeparateGeometry/Join, so filters must be applied sequentially on arrays with
positional ids. Two nodes with the same seed correlate. Kinds used:
- bool(p): hash01(id, seed) < p
- int(min,max): uniform integer, inclusive
- float vec: componentwise lerp
- Pick Instance index wraps modulo collection child count. An unlinked Instance Index socket defaults to the implicit element index (NOT constant 0) — guardrails, storefronts, shutters, AC wires, clotheslines etc. cycle variants per point.
Both grids: 1×1 cells, XZ-plane, centered on origin. MeshGrid takes verts = param+1, size = param (validated against evaluated instances with tools/compare_truth.ts):
- Grid B (front/back): (length+1) × (floor+1) vertices, x ∈ ±length/2, z ∈ ±floor/2.
- Grid A (sides): (width+1) × (floor+1) vertices.
Split (identical for both, N = length|width):
- top row (z = floor/2) → guardrails (pick = column index); top-right →
roofcorner - right column (x = N/2) →
corner; bottom-right →ground_corner - bottom row (z = -floor/2) → ground-floor cells
- remainder → window cells, N columns × (floor-1) rows (front/back shifted y-0.5, sides y-0.1)
Mirrored corner variants (pre-transform Rz +90°, scale(-1,1,1)) are used on the side facades; plain ones on front/back. 4 facades × right-column corners = 4 building corners.
Front/back (grid B) picks — sides (grid A) use the same structure with different ranges (in brackets):
wall.001pick int(0,100) [A: int(0,100)] — same value reused forwindow guard.001ROOMS.001pick int(0,80) [A: int(0,101)]- awning
roof.002if bool(RoofProbability), pick int(45,125) [A: int(0,113)] - curtains ×2
CURTAINS.001pick int(0,101) [A: int(0,164)] at (0.079, 0.038, 0.5) and (0.919, 0.038, 0.5); x-scale = randFloat(1, 5·curtainClose) seeds 41 / -(seed 32) - lights
lights.001if bool(Lights), pick int(0,120) [A: int(0,89)] - AC (front/back: only above 2nd row — "ac_remove first floor" z > -(floor+1)/2+2):
if bool(AC UNIT):
ac.001pick int(50,100) [A: int(50,200)] with pre-transform T(0.5, 0, 0.15) plus per-child x-jitter randFloat(-0.175, 0.175, childIdx, seed 0); plusAC WIRE.001child 0. Cells with AC: clotheslinescloth lines.001pick int(0,113) [A: int(0,96)] if bool(Clothline). Cells without AC (concat noAC + belowACcut): if bool(Clothline) then 50/50 bool →cloth lines WITH CLOTHES.001child 0 orcloth lines.001child 0. - window: bool(window type, seed 0) → steel else wood.
Steel:
steel frame.001,steel window top preset.001pick int(0,100), 4 panessteel window.001at x=0.079/0.494/0.506/0.921, y=0.011, z=0.518; outer pair scale(-1,1,1) rot z rand(0, 2·open), inner pair rot z rand(0, -open). Wood: same layout withwood frame.001,wood window top preset.001,wood window.001.
All random seeds = randomise except window-type choice and store old-sign (seed 0) and
curtain scale (seeds 32/41).
Front row cells (id = index in bottom row): always groud_front child 0 and wire
pick int(0,25). Per cell bool(closed/open store):
- open →
storefrontc0,storeinsidec0,lightsgroundpick int(0,25),prop_storepick int(0,25); if bool(store sign):store_signpick int(0,25) andstore_sign_hangingpick int(0,25); if bool(roof on store):store_roof(object); if bool(object on ground):prop_frontpick int(0,25) - closed →
shutterc0,old store_signc0 if bool(store sign, seed 0),prop_groudc0 if bool(object on ground)
Back row: ground_back pick int(0,100), wire pick int(0,25), eletricarea
pick int(0,31). Side bottom rows: groud side wall pick int(0,100), plus prop_groud
pick int(0,54) where bool(object on ground).
- top rows:
guardrail frontc0 (front),guardrail backc0 (back),guardrailsidec0 (both sides); cornersroofcorner(mirrored variant on sides). - roof grid
length×widthfaces at z = floor/2 + 0.1 (centers x=-length/2+0.5+i, y=0.6-width/2+0.5+j):roofcollection (whole, 1 child) every face;roof_proppick int(0,120) on faces passing bool(object on roof, seed 0). - watertank branch exists but its MeshToPoints has no mesh input in the file — dead.
Window-cell content is generated ONCE per grid and reused: front and back facades are
identical; side B filters content instances to facade-local x > -(width+1)/2 + 1.45
(drops column 0 items whose local x-offset < 0.45) and adds a solid side_wall panel
c0 on column-0 cells (raw cell positions, no y-shift).
Facade transforms (then everything T(0, -0.6, floor/2 + 0.05)); mapping validated against ground truth — stores face +Y:
- front (stores, guardrail front): T(0, +width/2 + 0.6, 0) · Rz(180°)
- back (ground_back, wires, electric): T(0, -width/2 + 0.6, 0)
- side A (+x): T(+length/2, 0.6, 0) · Rz(90°) — all windows
- side B (-x): T(-length/2, 0.6, 0) · Rz(270°) — filtered + solid column
Placement matrix order: Final · Facade · Post · T(point)·Rz·S · Pre.
low poly swaps everything for extruded-slab shell geometry (front/back slabs 0.5
thick, flat side grids, roof slab 0.4 with 0.5 overhangs front/back).
public/assets/kit.glb (exported Z-up, export_yup=False): top-level nodes named
COL[<collection>][<childIdx>] (child transforms reset to identity, mirroring
CollectionInfo Reset Children) and OBJ[<name>] for Object Info nodes.
Collection-instance empties are realized recursively at export (ROOMS children embed a
floor_preset collection; store_roof is an empty instancing groud_roof_preset).
public/assets/kit_manifest.json records child order/counts for index wrapping.
Materials are NOT taken from the GLB — the exporter marks the building material
alpha-blended, which breaks depth sorting at grazing angles. src/kit.ts builds
building/floor/glass from public/textures/ (opaque + emissive; floor uses alphaTest
cutout; glass is the only blended material).
Normals: the source objects carry Blender "Auto Smooth" custom split normals, which
bake into the GLB and shade inconsistently per face (mottled walls, mismatched corners).
This is geometry data — a material's normalScale (the normal map) cannot fix it.
tools/export_kit.py drops the custom normals and rebuilds clean angle-based normals
(sharp above ~35°, smooth below) via bmesh, so flat walls shade evenly while hard edges
stay crisp. Fix it at export, not with a runtime computeVertexNormals().
tools/dump_truth.py dumps every evaluated instance (name + world matrix) from the
.blend; tools/compare_truth.ts aggregates positions per collection and diffs against
the TS generator. Deterministic categories (walls, rooms, curtains, rails, corners,
roof tiles, ground rows) must be exact; probabilistic ones differ because the JS hash
is not bit-identical to Blender's.