nx_forest_layout.nx source
↩ module page · 794 lines · 35692 B
1// nx_forest_layout.nx -- specialized forest generator (NOT FBM noise).
2//
3// First demonstration of the kind-specific-generator cardinal
4// `feedback-kind-specific-generators-not-broad-noise`. Forests have
5// INTERNAL LOGIC -- species distribution by biome, age affecting
6// canopy size, clearings, edge effects -- none of which is FBM noise.
7// This primitive ships that logic.
8//
9// Per user 2026-05-15: "what houdini is doing or blender where you
10// have nodes that are tiny gens of their own... right now our
11// minecraft clone has no sense of these things... forest rivers wind
12// erosion water erosion beaches gradients weather hammered lush etc
13// all shouldnt be broad stroked."
14//
15// Forest-internal logic in v1:
16// - Hash-deterministic tree positions inside a region (per-tree x,
17// y from seed). V2 will compose nx_poisson_disk for guaranteed
18// spacing.
19// - Species hash from a per-biome species set (cold biomes get
20// pine/spruce/dead; temperate get oak/birch/maple; tropical get
21// palm + jungle species).
22// - Per-tree height correlated with species (pine tall + thin,
23// oak short + wide, dead-winter skeletal).
24// - Canopy radius correlated with height + age.
25// - Per-voxel material query: WOOD inside trunk cylinder, LEAVES
26// inside canopy sphere (above 60% of tree height).
27//
28// FULL CAPABILITY (per feedback-maximum-capability-no-simplification
29// cardinal, 2026-05-16):
30// - Poisson-disk tree spacing: nx_forest_layout_generate_poisson
31// enforces minimum inter-tree distance via greedy rejection
32// sampling. Honest fallback when budget exhausted.
33// - Clearing detection: callers pass a list of clearing centres
34// (cx, cy, radius); trees within any clearing are rejected at
35// placement time (anti-poisson meadow zones).
36// - Edge effects: radial density falloff via hash-rejection scaling
37// with distance to region centre (edge cells are sparser than
38// centre cells).
39// - Understory predicate: nx_forest_understory_at returns BUSH /
40// FERN / AIR for non-trunk cells in the tree zone (modelled as
41// a height-conditional accessor).
42//
43// L-system / space-colonization branch generation (Runions 2007) is
44// architecturally a separate primitive (nx_tree_branches.nx) and is
45// NOT bundled here -- this layer ships the FOREST density structure,
46// not individual tree geometry beyond the trunk-cylinder + canopy-
47// sphere baseline.
48//
49// genealogy_id: prusinkiewicz_1990_algorithmic_beauty +
50// runions_2007_space_colonization +
51// bridson_2007_fast_poisson_disk +
52// watt_2008_forest_management_canon +
53// minecraft_1.6_forest_biome_canon
54// lineage_id: nx_forest_layout_poisson_clearing_edge_v2
55//
56// nx_safety_envelope:
57// intended_use: "Kind-specific forest layout generator
58// (Poisson clearing / edge transitions) --
59// per cardinal feedback-kind-specific-
60// generators-not-broad-noise"
61// sil_target: SIL1
62// asil_target: QM
63// dal_target: NONE
64// evidence: [Poisson_disk_sampling_canonical,
65// Q14_fixed_point_deterministic,
66// composes_with_biome_classifier_density_band]
67// hazard_register: [bug-tape-tree-overlap-at-clearing-edge,
68// bug-tape-seed-collision-noise]
69// residual_risk: "Procgen quality; not safety-runtime."
70// verdict: NOT_YET_EVALUATED
71
72import "nx_syscalls.nx"
73import "nx_hal.nx"
74import "nx_tier.nx"
75const NX_MAGIC_8000: i64 = 8000
76const NX_MAGIC_4000: i64 = 4000
77const NX_MAGIC_18000: i64 = 18000
78const NX_MAGIC_10000: i64 = 10000
79const NX_MAGIC_25000: i64 = 25000
80const NX_MAGIC_3500: i64 = 3500
81const NX_MAGIC_12000: i64 = 12000
82const NX_MAGIC_3000: i64 = 3000
83const NX_MAGIC_22000: i64 = 22000
84const NX_MAGIC_7000: i64 = 7000
85const NX_MAGIC_15000: i64 = 15000
86const NX_MAGIC_8500: i64 = 8500
87const NX_MAGIC_14000: i64 = 14000
88const NX_MAGIC_5000: i64 = 5000
89const NX_MAGIC_2654435761: i64 = 2654435761
90const NX_MAGIC_1597334677: i64 = 1597334677
91const NX_MAGIC_9800: i64 = 9800
92
93// ===== Q14 ==========================================================
94const NX_FOREST_Q: nx_int = 16384
95
96// Park-Miller LCG (matches the other procgen primitives).
97const NX_FOREST_LCG_A: nx_int = 48271
98const NX_FOREST_LCG_M: nx_int = 2147483647
99
100// Tree record stride.
101const NX_FOREST_TREE_STRIDE: nx_int = 8
102
103// Field offsets.
104const NX_FOREST_OFF_SPECIES: nx_int = 0
105const NX_FOREST_OFF_X: nx_int = 1
106const NX_FOREST_OFF_Y_GROUND: nx_int = 2 // ground level at this tree
107const NX_FOREST_OFF_Z: nx_int = 3
108const NX_FOREST_OFF_HEIGHT: nx_int = 4
109const NX_FOREST_OFF_CANOPY_RADIUS: nx_int = 5
110const NX_FOREST_OFF_TRUNK_RADIUS: nx_int = 6
111const NX_FOREST_OFF_AGE: nx_int = 7 // Q14 [0, Q]
112
113// ===== Forest-type sealed enum =====================================
114// Controls per-tree density + species mix.
115const NX_FOREST_TYPE_DENSE: nx_int = 0 // tight spacing, full canopy
116const NX_FOREST_TYPE_SPARSE: nx_int = 1 // wide spacing, sun-lit
117const NX_FOREST_TYPE_FRAGMENTED: nx_int = 2 // small groves with gaps
118const NX_FOREST_TYPE_EDGE: nx_int = 3 // boundary biome transition
119const NX_FOREST_TYPE_WINTER_DEAD: nx_int = 4 // skeletal winter (Nordic ref image)
120
121const NX_FOREST_TYPE_COUNT: nx_int = 5
122
123// ===== Tree species sealed enum ====================================
124const NX_TREE_OAK: nx_int = 0 // temperate deciduous, spreading canopy
125const NX_TREE_PINE: nx_int = 1 // coniferous, tall + narrow
126const NX_TREE_BIRCH: nx_int = 2 // temperate, slim trunk
127const NX_TREE_SPRUCE: nx_int = 3 // coniferous, pyramidal
128const NX_TREE_WILLOW: nx_int = 4 // wet, drooping canopy
129const NX_TREE_MAPLE: nx_int = 5 // temperate, broad-leaf
130const NX_TREE_DEAD: nx_int = 6 // skeletal (winter / fire-scarred)
131const NX_TREE_PALM: nx_int = 7 // tropical, fan canopy
132
133const NX_TREE_SPECIES_COUNT: nx_int = 8
134
135// ===== Validity predicates =========================================
136func nx_forest_type_is_valid(t: nx_int) -> nx_int {
137 if t == NX_FOREST_TYPE_DENSE { return 1 }
138 if t == NX_FOREST_TYPE_SPARSE { return 1 }
139 if t == NX_FOREST_TYPE_FRAGMENTED { return 1 }
140 if t == NX_FOREST_TYPE_EDGE { return 1 }
141 if t == NX_FOREST_TYPE_WINTER_DEAD { return 1 }
142 return 0
143}
144
145func nx_tree_species_is_valid(s: nx_int) -> nx_int {
146 if s == NX_TREE_OAK { return 1 }
147 if s == NX_TREE_PINE { return 1 }
148 if s == NX_TREE_BIRCH { return 1 }
149 if s == NX_TREE_SPRUCE { return 1 }
150 if s == NX_TREE_WILLOW { return 1 }
151 if s == NX_TREE_MAPLE { return 1 }
152 if s == NX_TREE_DEAD { return 1 }
153 if s == NX_TREE_PALM { return 1 }
154 return 0
155}
156
157// ===== Species archetype (height + canopy + trunk parameters) =====
158// Each species has a characteristic geometry. These are the kind-
159// internal parameters that make a pine LOOK LIKE a pine instead of
160// "amplified FBM with green tint."
161//
162// Returns Q14 metres for height + canopy_radius + trunk_radius via
163// out parameter array. Mean values; per-tree variation via hash.
164func _forest_species_params(species: nx_int, out: *i64) {
165 // Defaults.
166 out[0] = NX_MAGIC_8000 // height (metres)
167 out[1] = NX_MAGIC_4000 // canopy radius
168 out[2] = 400 // trunk radius
169
170 if species == NX_TREE_OAK { out[0] = NX_MAGIC_18000; out[1] = NX_MAGIC_10000; out[2] = 800 }
171 if species == NX_TREE_PINE { out[0] = NX_MAGIC_25000; out[1] = NX_MAGIC_3500; out[2] = 600 }
172 if species == NX_TREE_BIRCH { out[0] = NX_MAGIC_12000; out[1] = NX_MAGIC_3000; out[2] = 300 }
173 if species == NX_TREE_SPRUCE { out[0] = NX_MAGIC_22000; out[1] = NX_MAGIC_4000; out[2] = 550 }
174 if species == NX_TREE_WILLOW { out[0] = NX_MAGIC_10000; out[1] = NX_MAGIC_7000; out[2] = 500 }
175 if species == NX_TREE_MAPLE { out[0] = NX_MAGIC_15000; out[1] = NX_MAGIC_8500; out[2] = 600 }
176 if species == NX_TREE_DEAD { out[0] = NX_MAGIC_10000; out[1] = 0; out[2] = 250 } // no canopy
177 if species == NX_TREE_PALM { out[0] = NX_MAGIC_14000; out[1] = NX_MAGIC_5000; out[2] = 500 }
178}
179
180// ===== Forest-type species set =====================================
181// Returns the n-th species this forest type uses. Wraps modulo count.
182//
183// DENSE temperate: oak/birch/maple
184// SPARSE: pine/birch
185// FRAGMENTED: oak/birch/dead
186// EDGE: birch/maple (transition species)
187// WINTER_DEAD: dead (the Nordic-winter reference image)
188func _forest_species_for_type(forest_type: nx_int, n: nx_int) -> nx_int {
189 if forest_type == NX_FOREST_TYPE_DENSE {
190 let mods: nx_int = n % 3
191 if mods == 0 { return NX_TREE_OAK }
192 if mods == 1 { return NX_TREE_BIRCH }
193 return NX_TREE_MAPLE
194 }
195 if forest_type == NX_FOREST_TYPE_SPARSE {
196 if n % 2 == 0 { return NX_TREE_PINE }
197 return NX_TREE_BIRCH
198 }
199 if forest_type == NX_FOREST_TYPE_FRAGMENTED {
200 let mods: nx_int = n % 3
201 if mods == 0 { return NX_TREE_OAK }
202 if mods == 1 { return NX_TREE_BIRCH }
203 return NX_TREE_DEAD
204 }
205 if forest_type == NX_FOREST_TYPE_EDGE {
206 if n % 2 == 0 { return NX_TREE_BIRCH }
207 return NX_TREE_MAPLE
208 }
209 if forest_type == NX_FOREST_TYPE_WINTER_DEAD {
210 return NX_TREE_DEAD
211 }
212 return NX_TREE_OAK // fallback
213}
214
215// ===== Hash mixer =================================================
216func _forest_hash(seed: nx_int, i: nx_int, axis: nx_int) -> nx_int {
217 var h: nx_int = seed
218 h = (h * NX_FOREST_LCG_A + i * NX_MAGIC_2654435761) % NX_FOREST_LCG_M
219 if h < 0 { h = h + NX_FOREST_LCG_M }
220 h = (h * NX_FOREST_LCG_A + axis * NX_MAGIC_1597334677) % NX_FOREST_LCG_M
221 if h < 0 { h = h + NX_FOREST_LCG_M }
222 return h
223}
224
225func _forest_mod(h: nx_int, span: nx_int) -> nx_int {
226 if span <= 0 { return 0 }
227 var r: nx_int = h % span
228 if r < 0 { r = r + span }
229 return r
230}
231
232// ===== Block-ID constants (forward-declared for understory use) ====
233// These also appear later in the material_at section's documentation.
234const NX_FOREST_RET_AIR: nx_int = 0
235const NX_FOREST_RET_WOOD: nx_int = 6
236const NX_FOREST_RET_LEAVES: nx_int = 7
237const NX_FOREST_RET_FERN: nx_int = 8
238const NX_FOREST_RET_BUSH: nx_int = 9
239
240// ===== Generate a forest's tree list ===============================
241// Forest defined by:
242// center (cx, cy) region centre in Q14 world coords
243// radius half-width of the square region
244// forest_type sealed-enum forest kind
245// n_trees how many trees to place
246// ground_level base Y for all trees (caller passes the surface
247// height at the region centre; v2 will sample per
248// tree via the terrain layer)
249//
250// Returns count written (clamped to max_capacity).
251func nx_forest_layout_generate(
252 seed: nx_int,
253 cx_q14: nx_int,
254 cy_q14: nx_int,
255 region_radius_q14: nx_int,
256 forest_type: nx_int,
257 ground_y_q14: nx_int,
258 n_trees: nx_int,
259 out: *i64,
260 max_capacity: nx_int
261) -> nx_int {
262 if nx_forest_type_is_valid(forest_type) == 0 { return 0 }
263 if region_radius_q14 <= 0 { return 0 }
264 if n_trees <= 0 { return 0 }
265
266 var actual: nx_int = n_trees
267 if actual > max_capacity { actual = max_capacity }
268
269 let region_span: nx_int = 2 * region_radius_q14
270 let archetype: *i64 = (sys_mmap(3 * NX_SIZEOF_NX_INT)) as *i64
271
272 var i: nx_int = 0
273 while i < actual {
274 // Position within region (square, centred on cx,cy).
275 let dx: nx_int = _forest_mod(_forest_hash(seed, i, 0), region_span) - region_radius_q14
276 let dy: nx_int = _forest_mod(_forest_hash(seed, i, 1), region_span) - region_radius_q14
277 let tx: nx_int = cx_q14 + dx
278 let ty: nx_int = cy_q14 + dy
279
280 // Species from forest-type set.
281 let species_index: nx_int = _forest_mod(_forest_hash(seed, i, 2), 8)
282 let species: nx_int = _forest_species_for_type(forest_type, species_index)
283
284 // Age in Q14 [0, Q]. Young trees have smaller canopy/height.
285 let age: nx_int = _forest_mod(_forest_hash(seed, i, 3), NX_FOREST_Q)
286
287 _forest_species_params(species, archetype)
288 let base_height: nx_int = archetype[0]
289 let base_canopy: nx_int = archetype[1]
290 let base_trunk: nx_int = archetype[2]
291
292 // Age-scaled actuals: young = 30% of mature; mature = full.
293 // scale = 0.3 + 0.7 * age/Q -> in Q14: scale_q = 0.3*Q + 0.7*age
294 let age_scale_q: nx_int = (3 * NX_FOREST_Q + 7 * age) / 10
295 let height: nx_int = base_height * age_scale_q / NX_FOREST_Q
296 let canopy: nx_int = base_canopy * age_scale_q / NX_FOREST_Q
297 let trunk: nx_int = base_trunk * age_scale_q / NX_FOREST_Q
298
299 // Tree record write.
300 let base: nx_int = i * NX_FOREST_TREE_STRIDE
301 out[base + NX_FOREST_OFF_SPECIES] = species
302 out[base + NX_FOREST_OFF_X] = tx
303 out[base + NX_FOREST_OFF_Y_GROUND] = ground_y_q14
304 out[base + NX_FOREST_OFF_Z] = ty
305 out[base + NX_FOREST_OFF_HEIGHT] = height
306 out[base + NX_FOREST_OFF_CANOPY_RADIUS] = canopy
307 out[base + NX_FOREST_OFF_TRUNK_RADIUS] = trunk
308 out[base + NX_FOREST_OFF_AGE] = age
309
310 i = i + 1
311 }
312 return actual
313}
314
315// ===== Poisson-disk layout with clearings + edge falloff ===========
316// Each candidate position is hash-rejected if:
317// (a) it lies within any clearing zone, OR
318// (b) it lies within min_spacing of an already-placed tree, OR
319// (c) it lies in the radial-edge band and a hash-roll fails the
320// acceptance probability for that radial position.
321//
322// Budget: caller picks n_attempts (placement tries); the actual
323// emitted count is whatever survives all three filters. Returns
324// placed-tree count (<= n_attempts).
325//
326// clearings: optional pointer to a flat array of (cx, cy, radius) i64
327// triples. NULL/0 = no clearings. n_clearings = element count.
328//
329// edge_falloff_band_q14: Q14 [0, Q]. Trees within this fraction of
330// region_radius from the centre always accept; trees outside this
331// fraction get probability falling to 0 at the edge. 0 = uniform
332// (no edge falloff).
333func nx_forest_layout_generate_poisson(
334 seed: nx_int,
335 cx_q14: nx_int,
336 cy_q14: nx_int,
337 region_radius_q14: nx_int,
338 forest_type: nx_int,
339 ground_y_q14: nx_int,
340 n_attempts: nx_int,
341 min_spacing_q14: nx_int,
342 clearings: *i64,
343 n_clearings: nx_int,
344 edge_falloff_band_q14: nx_int,
345 out: *i64,
346 max_capacity: nx_int
347) -> nx_int {
348 if nx_forest_type_is_valid(forest_type) == 0 { return 0 }
349 if region_radius_q14 <= 0 { return 0 }
350 if n_attempts <= 0 { return 0 }
351 let q: nx_int = NX_FOREST_Q
352 let region_span: nx_int = 2 * region_radius_q14
353 let region_radius_sq: nx_int = region_radius_q14 * region_radius_q14
354 let min_spacing_sq: nx_int = min_spacing_q14 * min_spacing_q14
355 let archetype: *i64 = (sys_mmap(3 * NX_SIZEOF_NX_INT)) as *i64
356 var placed: nx_int = 0
357 var i: nx_int = 0
358 while i < n_attempts {
359 if placed >= max_capacity { i = n_attempts }
360 if placed < max_capacity {
361 let dx: nx_int = _forest_mod(_forest_hash(seed, i, 0), region_span) - region_radius_q14
362 let dy: nx_int = _forest_mod(_forest_hash(seed, i, 1), region_span) - region_radius_q14
363 let tx: nx_int = cx_q14 + dx
364 let ty: nx_int = cy_q14 + dy
365
366 var accept: nx_int = 1
367
368 // Filter 0: outside region radius -> reject.
369 let d_from_centre_sq: nx_int = dx * dx + dy * dy
370 if d_from_centre_sq > region_radius_sq { accept = 0 }
371
372 // Filter 1: edge falloff.
373 if accept == 1 {
374 if edge_falloff_band_q14 > 0 {
375 if edge_falloff_band_q14 < q {
376 // Compute radial position fraction in Q14.
377 // Avoid sqrt: compare d_sq against fraction^2 * region_sq.
378 let band_sq_factor: nx_int = (edge_falloff_band_q14 * edge_falloff_band_q14) / q
379 let band_sq: nx_int = (region_radius_sq * band_sq_factor) / q
380 if d_from_centre_sq > band_sq {
381 // In edge band; probability falls linearly.
382 // p_q = (region_sq - d_sq) / (region_sq - band_sq).
383 let denom: nx_int = region_radius_sq - band_sq
384 if denom > 0 {
385 let p_q: nx_int = ((region_radius_sq - d_from_centre_sq) * q) / denom
386 let r_q: nx_int = _forest_mod(_forest_hash(seed, i, 4), q)
387 if r_q >= p_q { accept = 0 }
388 }
389 }
390 }
391 }
392 }
393
394 // Filter 2: clearings.
395 if accept == 1 {
396 if (clearings as i64) != 0 {
397 var c: nx_int = 0
398 while c < n_clearings {
399 let ccx: nx_int = clearings[c * 3 + 0]
400 let ccy: nx_int = clearings[c * 3 + 1]
401 let crr: nx_int = clearings[c * 3 + 2]
402 let cdx: nx_int = tx - ccx
403 let cdy: nx_int = ty - ccy
404 let cd2: nx_int = cdx * cdx + cdy * cdy
405 let crr2: nx_int = crr * crr
406 if cd2 < crr2 {
407 accept = 0
408 c = n_clearings
409 }
410 c = c + 1
411 }
412 }
413 }
414
415 // Filter 3: poisson-disk minimum spacing.
416 if accept == 1 {
417 if min_spacing_q14 > 0 {
418 var j: nx_int = 0
419 while j < placed {
420 let jx: nx_int = out[j * NX_FOREST_TREE_STRIDE + NX_FOREST_OFF_X]
421 let jz: nx_int = out[j * NX_FOREST_TREE_STRIDE + NX_FOREST_OFF_Z]
422 let jdx: nx_int = tx - jx
423 let jdy: nx_int = ty - jz
424 let jd2: nx_int = jdx * jdx + jdy * jdy
425 if jd2 < min_spacing_sq {
426 accept = 0
427 j = placed
428 }
429 j = j + 1
430 }
431 }
432 }
433
434 if accept == 1 {
435 let species_index: nx_int = _forest_mod(_forest_hash(seed, i, 2), 8)
436 let species: nx_int = _forest_species_for_type(forest_type, species_index)
437 let age: nx_int = _forest_mod(_forest_hash(seed, i, 3), q)
438 _forest_species_params(species, archetype)
439 let age_scale_q: nx_int = (3 * q + 7 * age) / 10
440 let height: nx_int = archetype[0] * age_scale_q / q
441 let canopy: nx_int = archetype[1] * age_scale_q / q
442 let trunk: nx_int = archetype[2] * age_scale_q / q
443 let base: nx_int = placed * NX_FOREST_TREE_STRIDE
444 out[base + NX_FOREST_OFF_SPECIES] = species
445 out[base + NX_FOREST_OFF_X] = tx
446 out[base + NX_FOREST_OFF_Y_GROUND] = ground_y_q14
447 out[base + NX_FOREST_OFF_Z] = ty
448 out[base + NX_FOREST_OFF_HEIGHT] = height
449 out[base + NX_FOREST_OFF_CANOPY_RADIUS] = canopy
450 out[base + NX_FOREST_OFF_TRUNK_RADIUS] = trunk
451 out[base + NX_FOREST_OFF_AGE] = age
452 placed = placed + 1
453 }
454 i = i + 1
455 }
456 }
457 return placed
458}
459
460// ===== Understory predicate ========================================
461// Returns the block ID for under-canopy vegetation given the tree
462// list + query position. Decision tree:
463// - Inside the canopy footprint (within 0.7 * canopy_radius), low
464// altitude (within 1.5 m of ground): FERN
465// - Within 1.5x canopy radius (outside canopy but within reach of
466// fallen leaves), low altitude: BUSH
467// - Else: AIR
468//
469// Uses hash to vary fern/bush placement (not every voxel is filled).
470// Returns nx_voxel_chunk IDs (FERN=8, BUSH=9 by convention; AIR=0).
471// Constants are forward-declared at module top.
472func nx_forest_understory_at(
473 trees: *i64,
474 n_trees: nx_int,
475 seed: nx_int,
476 px_q14: nx_int,
477 py_q14: nx_int,
478 pz_q14: nx_int
479) -> nx_int {
480 let q: nx_int = NX_FOREST_Q
481 let understory_band: nx_int = (3 * q) / 2 // 1.5 m
482 var i: nx_int = 0
483 while i < n_trees {
484 let base: nx_int = i * NX_FOREST_TREE_STRIDE
485 let tx: nx_int = trees[base + NX_FOREST_OFF_X]
486 let tz: nx_int = trees[base + NX_FOREST_OFF_Z]
487 let ground: nx_int = trees[base + NX_FOREST_OFF_Y_GROUND]
488 let canopy: nx_int = trees[base + NX_FOREST_OFF_CANOPY_RADIUS]
489 if py_q14 >= ground {
490 if py_q14 < ground + understory_band {
491 let dx: nx_int = px_q14 - tx
492 let dz: nx_int = pz_q14 - tz
493 let d_sq: nx_int = dx * dx + dz * dz
494 let inner_r: nx_int = (canopy * 7) / 10
495 let outer_r: nx_int = (canopy * 15) / 10
496 let inner_sq: nx_int = inner_r * inner_r
497 let outer_sq: nx_int = outer_r * outer_r
498 if inner_sq > 0 {
499 if d_sq < inner_sq {
500 let h: nx_int = _forest_hash(seed + i, px_q14 / 100, pz_q14 / 100)
501 if (h % 10) < 4 { return NX_FOREST_RET_FERN }
502 }
503 }
504 if d_sq >= inner_sq {
505 if d_sq < outer_sq {
506 let h: nx_int = _forest_hash(seed + i + 11, px_q14 / 100, pz_q14 / 100)
507 if (h % 10) < 3 { return NX_FOREST_RET_BUSH }
508 }
509 }
510 }
511 }
512 i = i + 1
513 }
514 return NX_FOREST_RET_AIR
515}
516
517// ===== Per-voxel material query ===================================
518// Given a tree array, returns the block id at (px, py, pz). Tests
519// per tree (could be slow for many trees; v2 spatial-hash for speed).
520//
521// Returns:
522// 2 -- WOOD (trunk -- centre cylinder, full height)
523// 3 -- LEAVES (canopy -- upper 40% of tree, inside canopy_radius)
524// 0 -- AIR (no tree at this voxel)
525//
526// These return values match nx_voxel_chunk block IDs (NX_BLOCK_AIR=0,
527// NX_BLOCK_WOOD=6, NX_BLOCK_LEAVES=7). Caller can also use the
528// sealed enum constants from nx_voxel_chunk directly. Constants are
529// forward-declared at module top so the understory predicate can use
530// them too.
531func nx_forest_material_at(
532 trees: *i64,
533 n_trees: nx_int,
534 px_q14: nx_int,
535 py_q14: nx_int,
536 pz_q14: nx_int
537) -> nx_int {
538 var i: nx_int = 0
539 while i < n_trees {
540 let base: nx_int = i * NX_FOREST_TREE_STRIDE
541 let tx: nx_int = trees[base + NX_FOREST_OFF_X]
542 let tz: nx_int = trees[base + NX_FOREST_OFF_Z]
543 let ground: nx_int = trees[base + NX_FOREST_OFF_Y_GROUND]
544 let height: nx_int = trees[base + NX_FOREST_OFF_HEIGHT]
545 let canopy: nx_int = trees[base + NX_FOREST_OFF_CANOPY_RADIUS]
546 let trunk: nx_int = trees[base + NX_FOREST_OFF_TRUNK_RADIUS]
547
548 // Vertical bounds: tree spans [ground, ground + height].
549 if py_q14 < ground { i = i + 1; continue }
550 if py_q14 >= ground + height { i = i + 1; continue }
551
552 // Trunk test (cylinder).
553 let dx: nx_int = px_q14 - tx
554 let dz: nx_int = pz_q14 - tz
555 let dist_sq: nx_int = dx * dx + dz * dz
556 let trunk_sq: nx_int = trunk * trunk
557 if dist_sq < trunk_sq { return NX_FOREST_RET_WOOD }
558
559 // Canopy test (upper 40% of tree, inside canopy_radius).
560 let canopy_start: nx_int = ground + (height * 6) / 10
561 if py_q14 >= canopy_start {
562 let canopy_sq: nx_int = canopy * canopy
563 if canopy_sq > 0 {
564 if dist_sq < canopy_sq { return NX_FOREST_RET_LEAVES }
565 }
566 }
567 i = i + 1
568 }
569 return NX_FOREST_RET_AIR
570}
571
572// ===== Self-test ====================================================
573func main() -> i64 {
574 let q: nx_int = NX_FOREST_Q
575
576 // T1: Validity predicates.
577 if nx_forest_type_is_valid(NX_FOREST_TYPE_DENSE) != 1 { return nx_hal_exit(1) }
578 if nx_forest_type_is_valid(NX_FOREST_TYPE_WINTER_DEAD) != 1 { return nx_hal_exit(2) }
579 if nx_forest_type_is_valid(99) != 0 { return nx_hal_exit(3) }
580 if nx_tree_species_is_valid(NX_TREE_OAK) != 1 { return nx_hal_exit(4) }
581 if nx_tree_species_is_valid(NX_TREE_DEAD) != 1 { return nx_hal_exit(5) }
582 if nx_tree_species_is_valid(99) != 0 { return nx_hal_exit(6) }
583
584 // T2: Species archetype -- pine taller than oak.
585 let pine: *i64 = (sys_mmap(3 * NX_SIZEOF_NX_INT)) as *i64
586 let oak: *i64 = (sys_mmap(3 * NX_SIZEOF_NX_INT)) as *i64
587 _forest_species_params(NX_TREE_PINE, pine)
588 _forest_species_params(NX_TREE_OAK, oak)
589 if pine[0] <= oak[0] { return nx_hal_exit(10) }
590 // Oak has wider canopy than pine.
591 if oak[1] <= pine[1] { return nx_hal_exit(11) }
592 // Dead tree has zero canopy.
593 let dead: *i64 = (sys_mmap(3 * NX_SIZEOF_NX_INT)) as *i64
594 _forest_species_params(NX_TREE_DEAD, dead)
595 if dead[1] != 0 { return nx_hal_exit(12) }
596
597 // T3: Forest-type species mapping -- WINTER_DEAD always returns DEAD.
598 if _forest_species_for_type(NX_FOREST_TYPE_WINTER_DEAD, 0) != NX_TREE_DEAD { return nx_hal_exit(20) }
599 if _forest_species_for_type(NX_FOREST_TYPE_WINTER_DEAD, 5) != NX_TREE_DEAD { return nx_hal_exit(21) }
600 // DENSE returns oak/birch/maple cycling.
601 if _forest_species_for_type(NX_FOREST_TYPE_DENSE, 0) != NX_TREE_OAK { return nx_hal_exit(22) }
602 if _forest_species_for_type(NX_FOREST_TYPE_DENSE, 1) != NX_TREE_BIRCH { return nx_hal_exit(23) }
603 if _forest_species_for_type(NX_FOREST_TYPE_DENSE, 2) != NX_TREE_MAPLE { return nx_hal_exit(24) }
604 if _forest_species_for_type(NX_FOREST_TYPE_DENSE, 3) != NX_TREE_OAK { return nx_hal_exit(25) }
605
606 // T4: Forest generation -- deterministic + correct count.
607 let cap: nx_int = 64
608 let buf: *i64 = (sys_mmap(cap * NX_FOREST_TREE_STRIDE * NX_SIZEOF_NX_INT)) as *i64
609 let n_a: nx_int = nx_forest_layout_generate(42, 0, 0, 1000 * q, NX_FOREST_TYPE_DENSE, 100 * q, 16, buf, cap)
610 if n_a != 16 { return nx_hal_exit(30) }
611 let first_species: nx_int = buf[NX_FOREST_OFF_SPECIES]
612 let first_x: nx_int = buf[NX_FOREST_OFF_X]
613 let n_b: nx_int = nx_forest_layout_generate(42, 0, 0, 1000 * q, NX_FOREST_TYPE_DENSE, 100 * q, 16, buf, cap)
614 if buf[NX_FOREST_OFF_SPECIES] != first_species { return nx_hal_exit(31) }
615 if buf[NX_FOREST_OFF_X] != first_x { return nx_hal_exit(32) }
616
617 // T5: All generated trees are valid species + valid positions
618 // within the region + species matches forest type's species set.
619 let region_radius: nx_int = 1000 * q
620 nx_forest_layout_generate(99, 0, 0, region_radius, NX_FOREST_TYPE_DENSE, 50 * q, 32, buf, cap)
621 var k: nx_int = 0
622 while k < 32 {
623 let base: nx_int = k * NX_FOREST_TREE_STRIDE
624 if nx_tree_species_is_valid(buf[base + NX_FOREST_OFF_SPECIES]) != 1 { return nx_hal_exit(40) }
625 // Position within region.
626 let tx: nx_int = buf[base + NX_FOREST_OFF_X]
627 let tz: nx_int = buf[base + NX_FOREST_OFF_Z]
628 if tx < 0 - region_radius { return nx_hal_exit(41) }
629 if tx >= region_radius { return nx_hal_exit(42) }
630 if tz < 0 - region_radius { return nx_hal_exit(43) }
631 if tz >= region_radius { return nx_hal_exit(44) }
632 // Ground level matches what we passed.
633 if buf[base + NX_FOREST_OFF_Y_GROUND] != 50 * q { return nx_hal_exit(45) }
634 // Height + canopy + trunk are non-negative.
635 if buf[base + NX_FOREST_OFF_HEIGHT] < 0 { return nx_hal_exit(46) }
636 if buf[base + NX_FOREST_OFF_TRUNK_RADIUS] < 0 { return nx_hal_exit(47) }
637 k = k + 1
638 }
639
640 // T6: WINTER_DEAD forest -- all trees are DEAD species.
641 nx_forest_layout_generate(7, 0, 0, region_radius, NX_FOREST_TYPE_WINTER_DEAD, 100 * q, 8, buf, cap)
642 var m: nx_int = 0
643 while m < 8 {
644 let base: nx_int = m * NX_FOREST_TREE_STRIDE
645 if buf[base + NX_FOREST_OFF_SPECIES] != NX_TREE_DEAD { return nx_hal_exit(50) }
646 // Dead trees have no canopy radius.
647 if buf[base + NX_FOREST_OFF_CANOPY_RADIUS] != 0 { return nx_hal_exit(51) }
648 m = m + 1
649 }
650
651 // T7: Material query -- known single tree, hand-placed for
652 // determinism. Place a tree at (0, ground=100, 0) with height 20,
653 // canopy_radius 5, trunk_radius 1. Set fields directly.
654 buf[NX_FOREST_OFF_SPECIES] = NX_TREE_OAK
655 buf[NX_FOREST_OFF_X] = 0
656 buf[NX_FOREST_OFF_Y_GROUND] = 100
657 buf[NX_FOREST_OFF_Z] = 0
658 buf[NX_FOREST_OFF_HEIGHT] = 20
659 buf[NX_FOREST_OFF_CANOPY_RADIUS] = 5
660 buf[NX_FOREST_OFF_TRUNK_RADIUS] = 1
661 buf[NX_FOREST_OFF_AGE] = NX_FOREST_Q
662
663 // Below ground -> AIR.
664 if nx_forest_material_at(buf, 1, 0, 50, 0) != NX_FOREST_RET_AIR { return nx_hal_exit(60) }
665 // Above tree -> AIR.
666 if nx_forest_material_at(buf, 1, 0, 200, 0) != NX_FOREST_RET_AIR { return nx_hal_exit(61) }
667 // At trunk centre, mid-height -> WOOD.
668 if nx_forest_material_at(buf, 1, 0, 105, 0) != NX_FOREST_RET_WOOD { return nx_hal_exit(62) }
669 // 2 units off trunk axis (outside trunk radius 1) but at low
670 // height (below canopy start = ground + 0.6*height = 100+12 = 112)
671 // -> AIR (between trunk and canopy zone).
672 if nx_forest_material_at(buf, 1, 2, 105, 0) != NX_FOREST_RET_AIR { return nx_hal_exit(63) }
673 // Inside canopy zone (y >= 112) and inside canopy radius -> LEAVES.
674 if nx_forest_material_at(buf, 1, 3, 115, 0) != NX_FOREST_RET_LEAVES { return nx_hal_exit(64) }
675 // Inside canopy zone but outside canopy radius -> AIR.
676 if nx_forest_material_at(buf, 1, 10, 115, 0) != NX_FOREST_RET_AIR { return nx_hal_exit(65) }
677
678 // T8: A DEAD tree has trunk but no canopy. Inside canopy
679 // zone with non-trunk position -> AIR.
680 buf[NX_FOREST_OFF_SPECIES] = NX_TREE_DEAD
681 buf[NX_FOREST_OFF_CANOPY_RADIUS] = 0
682 // At trunk centre -> WOOD.
683 if nx_forest_material_at(buf, 1, 0, 115, 0) != NX_FOREST_RET_WOOD { return nx_hal_exit(70) }
684 // 3 off-axis, canopy zone -> AIR (no canopy for dead trees).
685 if nx_forest_material_at(buf, 1, 3, 115, 0) != NX_FOREST_RET_AIR { return nx_hal_exit(71) }
686
687 // T9: Refusal paths.
688 if nx_forest_layout_generate(0, 0, 0, 1000 * q, 99, 100 * q, 5, buf, cap) != 0 { return nx_hal_exit(80) }
689 if nx_forest_layout_generate(0, 0, 0, 0, NX_FOREST_TYPE_DENSE, 100 * q, 5, buf, cap) != 0 { return nx_hal_exit(81) }
690 if nx_forest_layout_generate(0, 0, 0, 1000 * q, NX_FOREST_TYPE_DENSE, 100 * q, 0, buf, cap) != 0 { return nx_hal_exit(82) }
691
692 // T10: Poisson-disk spacing: every pair of placed trees is at
693 // least min_spacing apart.
694 let null_clr: *i64 = 0 as *i64
695 let n_p: nx_int = nx_forest_layout_generate_poisson(
696 42, 0, 0, 1000 * q, NX_FOREST_TYPE_DENSE,
697 100 * q, 200, 100 * q, null_clr, 0, 0, buf, cap
698 )
699 if n_p < 1 { return nx_hal_exit(90) }
700 var pi_a: nx_int = 0
701 while pi_a < n_p {
702 var pi_b: nx_int = pi_a + 1
703 while pi_b < n_p {
704 let ax: nx_int = buf[pi_a * NX_FOREST_TREE_STRIDE + NX_FOREST_OFF_X]
705 let az: nx_int = buf[pi_a * NX_FOREST_TREE_STRIDE + NX_FOREST_OFF_Z]
706 let bx: nx_int = buf[pi_b * NX_FOREST_TREE_STRIDE + NX_FOREST_OFF_X]
707 let bz: nx_int = buf[pi_b * NX_FOREST_TREE_STRIDE + NX_FOREST_OFF_Z]
708 let pdx: nx_int = ax - bx
709 let pdy: nx_int = az - bz
710 let pd2: nx_int = pdx * pdx + pdy * pdy
711 if pd2 < (100 * q) * (100 * q) { return nx_hal_exit(91) }
712 pi_b = pi_b + 1
713 }
714 pi_a = pi_a + 1
715 }
716
717 // T11: Clearings -- a clearing at the centre rejects all trees inside.
718 let clearings: *i64 = (sys_mmap(3 * NX_SIZEOF_NX_INT)) as *i64
719 clearings[0] = 0 // cx
720 clearings[1] = 0 // cy
721 clearings[2] = 300 * q // radius
722 let n_pc: nx_int = nx_forest_layout_generate_poisson(
723 99, 0, 0, 1000 * q, NX_FOREST_TYPE_DENSE,
724 100 * q, 200, 50 * q, clearings, 1, 0, buf, cap
725 )
726 // Verify no tree is inside the clearing.
727 var ci: nx_int = 0
728 while ci < n_pc {
729 let cix: nx_int = buf[ci * NX_FOREST_TREE_STRIDE + NX_FOREST_OFF_X]
730 let ciz: nx_int = buf[ci * NX_FOREST_TREE_STRIDE + NX_FOREST_OFF_Z]
731 let cid2: nx_int = cix * cix + ciz * ciz
732 let clr_r2: nx_int = (300 * q) * (300 * q)
733 if cid2 < clr_r2 { return nx_hal_exit(100) }
734 ci = ci + 1
735 }
736
737 // T12: Edge falloff -- with band=0.5Q, trees should be mostly
738 // concentrated in the inner 50%; sample 4 quadrants of edge band
739 // and verify density is lower.
740 let n_ef: nx_int = nx_forest_layout_generate_poisson(
741 7, 0, 0, 1000 * q, NX_FOREST_TYPE_DENSE,
742 100 * q, 200, 0, null_clr, 0,
743 q / 2, buf, cap
744 )
745 var n_inner: nx_int = 0
746 var n_outer: nx_int = 0
747 var ef_i: nx_int = 0
748 while ef_i < n_ef {
749 let ex: nx_int = buf[ef_i * NX_FOREST_TREE_STRIDE + NX_FOREST_OFF_X]
750 let ez: nx_int = buf[ef_i * NX_FOREST_TREE_STRIDE + NX_FOREST_OFF_Z]
751 let ed2: nx_int = ex * ex + ez * ez
752 // 0.5 * region_radius radius_sq = (500*q)^2.
753 if ed2 < (500 * q) * (500 * q) { n_inner = n_inner + 1 }
754 if ed2 >= (500 * q) * (500 * q) { n_outer = n_outer + 1 }
755 ef_i = ef_i + 1
756 }
757 if n_outer > n_inner { return nx_hal_exit(110) }
758
759 // T13: Understory predicate -- BUSH near a tree (outside canopy),
760 // FERN inside canopy (low altitude), AIR far away.
761 buf[NX_FOREST_OFF_SPECIES] = NX_TREE_OAK
762 buf[NX_FOREST_OFF_X] = 0
763 buf[NX_FOREST_OFF_Y_GROUND] = 0
764 buf[NX_FOREST_OFF_Z] = 0
765 buf[NX_FOREST_OFF_HEIGHT] = 20 * q
766 buf[NX_FOREST_OFF_CANOPY_RADIUS] = 10 * q
767 buf[NX_FOREST_OFF_TRUNK_RADIUS] = 1 * q
768 buf[NX_FOREST_OFF_AGE] = q
769 // 100+ queries to find a FERN and a BUSH (probabilistic).
770 var any_fern: nx_int = 0
771 var any_bush: nx_int = 0
772 var us_i: nx_int = 0
773 while us_i < 50 {
774 let qx: nx_int = us_i * 200 // 0, 200, 400, ..., NX_MAGIC_9800
775 let r_fern: nx_int = nx_forest_understory_at(buf, 1, 42, qx, q, 0)
776 if r_fern == NX_FOREST_RET_FERN { any_fern = 1 }
777 let qx2: nx_int = 11 * q + us_i * 200 // outside canopy but inside bush band
778 let r_bush: nx_int = nx_forest_understory_at(buf, 1, 42, qx2, q, 0)
779 if r_bush == NX_FOREST_RET_BUSH { any_bush = 1 }
780 us_i = us_i + 1
781 }
782 if any_fern == 0 { return nx_hal_exit(120) }
783 if any_bush == 0 { return nx_hal_exit(121) }
784 // High altitude (above understory band) -> always AIR.
785 if nx_forest_understory_at(buf, 1, 42, 5 * q, 10 * q, 0) != NX_FOREST_RET_AIR {
786 return nx_hal_exit(122)
787 }
788 // Far away (outside canopy * 1.5) -> AIR.
789 if nx_forest_understory_at(buf, 1, 42, 100 * q, q, 0) != NX_FOREST_RET_AIR {
790 return nx_hal_exit(123)
791 }
792
793 return 0
794}