code wiki / (root) / nx_forest_layout.nx

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}