@woosh/meep-engine 3.60.4 → 3.60.5

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.
Files changed (379) hide show
  1. package/CHANGELOG.md +2602 -0
  2. package/CONTRIBUTING.md +7 -0
  3. package/MIGRATION_2_TO_3.md +136 -0
  4. package/SECURITY.md +4 -0
  5. package/editor/EditorCameraNavigation.d.ts +2 -1
  6. package/editor/EditorCameraNavigation.d.ts.map +1 -1
  7. package/editor/EditorCameraNavigation.js +51 -57
  8. package/package.json +6 -2
  9. package/src/engine/EngineHarness.d.ts +3 -9
  10. package/src/engine/EngineHarness.d.ts.map +1 -1
  11. package/src/engine/EngineHarness.js +6 -8
  12. package/src/engine/graphics/camera/makeOrbitalCameraController.d.ts +9 -6
  13. package/src/engine/graphics/camera/makeOrbitalCameraController.d.ts.map +1 -1
  14. package/src/engine/graphics/camera/makeOrbitalCameraController.js +44 -71
  15. package/src/engine/graphics/camera/orbital/MomentumAxis.d.ts +64 -0
  16. package/src/engine/graphics/camera/orbital/MomentumAxis.d.ts.map +1 -0
  17. package/src/engine/graphics/camera/orbital/MomentumAxis.js +187 -0
  18. package/src/engine/graphics/camera/orbital/OrbitalCameraRig.d.ts +86 -0
  19. package/src/engine/graphics/camera/orbital/OrbitalCameraRig.d.ts.map +1 -0
  20. package/src/engine/graphics/camera/orbital/OrbitalCameraRig.js +319 -0
  21. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.d.ts +34 -10
  22. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.d.ts.map +1 -1
  23. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.js +116 -30
  24. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.d.ts +1 -7
  25. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.d.ts.map +1 -1
  26. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.js +1 -37
  27. package/src/engine/graphics/ecs/water/Water.d.ts +8 -0
  28. package/src/engine/graphics/ecs/water/Water.d.ts.map +1 -1
  29. package/src/engine/graphics/ecs/water/Water.js +8 -0
  30. package/src/engine/graphics3/WaterSystem.d.ts.map +1 -1
  31. package/src/engine/graphics3/WaterSystem.js +731 -734
  32. package/src/engine/graphics3/decal/DecalTileShader.d.ts +61 -0
  33. package/src/engine/graphics3/decal/DecalTileShader.d.ts.map +1 -0
  34. package/src/engine/graphics3/decal/DecalTileShader.js +138 -0
  35. package/src/engine/graphics3/decal/GPUDecalRenderer.d.ts +10 -4
  36. package/src/engine/graphics3/decal/GPUDecalRenderer.d.ts.map +1 -1
  37. package/src/engine/graphics3/decal/GPUDecalRenderer.js +149 -44
  38. package/src/engine/graphics3/decal/chunk_decal_tile_vertex.d.ts +25 -0
  39. package/src/engine/graphics3/decal/chunk_decal_tile_vertex.d.ts.map +1 -0
  40. package/src/engine/graphics3/decal/chunk_decal_tile_vertex.js +55 -0
  41. package/src/engine/graphics3/decal/graph_gather_decal_tiles.d.ts +22 -0
  42. package/src/engine/graphics3/decal/graph_gather_decal_tiles.d.ts.map +1 -0
  43. package/src/engine/graphics3/decal/graph_gather_decal_tiles.js +70 -0
  44. package/src/engine/graphics3/decal/shader_decal_composite.d.ts +7 -5
  45. package/src/engine/graphics3/decal/shader_decal_composite.d.ts.map +1 -1
  46. package/src/engine/graphics3/decal/shader_decal_composite.js +48 -44
  47. package/src/engine/graphics3/decal/shader_decal_tile_copy.d.ts +12 -0
  48. package/src/engine/graphics3/decal/shader_decal_tile_copy.d.ts.map +1 -0
  49. package/src/engine/graphics3/decal/shader_decal_tile_copy.js +78 -0
  50. package/src/engine/graphics3/decal/shader_gather_decal_tiles.d.ts +11 -0
  51. package/src/engine/graphics3/decal/shader_gather_decal_tiles.d.ts.map +1 -0
  52. package/src/engine/graphics3/decal/shader_gather_decal_tiles.js +80 -0
  53. package/src/engine/input/devices/mouse/MouseEventButtons.d.ts +13 -0
  54. package/src/engine/input/devices/mouse/MouseEventButtons.d.ts.map +1 -0
  55. package/src/engine/input/devices/mouse/MouseEventButtons.js +23 -0
  56. package/src/engine/input/ecs/controllers/KeyboardCameraController.d.ts +4 -0
  57. package/src/engine/input/ecs/controllers/KeyboardCameraController.d.ts.map +1 -1
  58. package/src/engine/input/ecs/controllers/KeyboardCameraController.js +9 -45
  59. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  60. package/src/shade/playground/water_refraction/README.md +5 -6
  61. package/src/shade/playground/water_refraction/index.html +1 -1
  62. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  63. package/src/shade/renderer/Renderer.js +7 -5
  64. package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts +40 -80
  65. package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts.map +1 -1
  66. package/src/shade/renderer/camera/orbital/OrbitalCameraController.js +197 -147
  67. package/src/shade/renderer/fluid/GPUFluidCascade.d.ts +3 -3
  68. package/src/shade/renderer/fluid/GPUFluidCascadeStack.d.ts +1 -1
  69. package/src/shade/renderer/fluid/forcing/GPUFluidForcing.d.ts +6 -6
  70. package/src/shade/renderer/fluid/pressure/GPUFluidPressureSolve.d.ts +1 -1
  71. package/src/shade/renderer/fluid/query/GPUFluidQueries.d.ts +1 -1
  72. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.d.ts +2 -2
  73. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.js +2 -2
  74. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts +2 -1
  75. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts.map +1 -1
  76. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.js +18 -1
  77. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.d.ts +31 -0
  78. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.d.ts.map +1 -0
  79. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.js +343 -0
  80. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.d.ts +17 -0
  81. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.d.ts.map +1 -0
  82. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.js +29 -0
  83. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.d.ts.map +1 -1
  84. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.js +7 -13
  85. package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.d.ts.map +1 -1
  86. package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.js +1 -8
  87. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts +5 -5
  88. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts.map +1 -1
  89. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.js +10 -10
  90. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts +1 -4
  91. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts.map +1 -1
  92. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.js +1 -10
  93. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.d.ts +5 -15
  94. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.d.ts.map +1 -1
  95. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.js +6 -34
  96. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.d.ts.map +1 -1
  97. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.js +1 -7
  98. package/src/shade/renderer/hiz/HZBFootprint.d.ts +14 -0
  99. package/src/shade/renderer/hiz/HZBFootprint.d.ts.map +1 -0
  100. package/src/shade/renderer/hiz/HZBFootprint.js +16 -0
  101. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.d.ts +10 -0
  102. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.d.ts.map +1 -0
  103. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.js +11 -0
  104. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.d.ts +10 -0
  105. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.d.ts.map +1 -0
  106. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.js +11 -0
  107. package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts +0 -7
  108. package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts.map +1 -1
  109. package/src/shade/renderer/hiz/HierarchicalZBuffer.js +405 -350
  110. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.d.ts +12 -0
  111. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.d.ts.map +1 -0
  112. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.js +27 -0
  113. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.d.ts +12 -0
  114. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.d.ts.map +1 -0
  115. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.js +27 -0
  116. package/src/shade/renderer/hiz/hzb_footprint_texels.d.ts +14 -0
  117. package/src/shade/renderer/hiz/hzb_footprint_texels.d.ts.map +1 -0
  118. package/src/shade/renderer/hiz/hzb_footprint_texels.js +34 -0
  119. package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts +0 -5
  120. package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts.map +1 -1
  121. package/src/shade/renderer/hiz/shader_hzb_reduce_any.js +37 -57
  122. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0.d.ts +11 -0
  123. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0.d.ts.map +1 -0
  124. package/src/shade/renderer/hiz/{shader_hzb_reduce_mip0_clip.js → shader_hzb_reduce_mip0.js} +120 -111
  125. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  126. package/src/shade/renderer/material/GPUMaterialManager.d.ts +7 -0
  127. package/src/shade/renderer/material/GPUMaterialManager.d.ts.map +1 -1
  128. package/src/shade/renderer/material/GPUMaterialManager.js +11 -0
  129. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts +12 -12
  130. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts.map +1 -1
  131. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.js +17 -118
  132. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.d.ts.map +1 -1
  133. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.js +7 -2
  134. package/src/shade/renderer/water/GPUWaterRenderer.d.ts +0 -6
  135. package/src/shade/renderer/water/GPUWaterRenderer.d.ts.map +1 -1
  136. package/src/shade/renderer/water/GPUWaterRenderer.js +12 -17
  137. package/src/shade/renderer/water/medium/graph_water_meshlets.d.ts +35 -0
  138. package/src/shade/renderer/water/medium/graph_water_meshlets.d.ts.map +1 -0
  139. package/src/shade/renderer/water/medium/graph_water_meshlets.js +89 -0
  140. package/src/shade/renderer/water/query/GPUWaterQueries.d.ts +1 -1
  141. package/src/shade/renderer/water/shader_water.d.ts.map +1 -1
  142. package/src/shade/renderer/water/shader_water.js +22 -13
  143. package/src/shade/renderer/water/shader_water_avboit.d.ts.map +1 -1
  144. package/src/shade/renderer/water/shader_water_avboit.js +7 -7
  145. package/src/shade/renderer/water/surface/graph_water_tessellation.d.ts +1 -1
  146. package/src/shade/renderer/water/surface/graph_water_tessellation.js +1 -1
  147. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +9 -1
  148. package/editor/REWORK_PLAN_2026_08.md +0 -692
  149. package/src/CODEC_LAYOUT_VERIFICATION.md +0 -419
  150. package/src/CORRECTNESS_HUNT_2026_07_24.md +0 -142
  151. package/src/DUPLICATION_AUDIT_2026_08_07/00_core_reuse_surface.md +0 -738
  152. package/src/DUPLICATION_AUDIT_2026_08_07/A_core_math_geom.md +0 -79
  153. package/src/DUPLICATION_AUDIT_2026_08_07/B_core_data.md +0 -93
  154. package/src/DUPLICATION_AUDIT_2026_08_07/C_gfx_render.md +0 -80
  155. package/src/DUPLICATION_AUDIT_2026_08_07/D_gfx_features.md +0 -164
  156. package/src/DUPLICATION_AUDIT_2026_08_07/E_physics.md +0 -69
  157. package/src/DUPLICATION_AUDIT_2026_08_07/F_sound.md +0 -110
  158. package/src/DUPLICATION_AUDIT_2026_08_07/G_net_save.md +0 -129
  159. package/src/DUPLICATION_AUDIT_2026_08_07/H_engine_misc.md +0 -72
  160. package/src/DUPLICATION_AUDIT_2026_08_07/I_generation_view.md +0 -74
  161. package/src/DUPLICATION_AUDIT_2026_08_07/K_crosscutting.md +0 -169
  162. package/src/DUPLICATION_AUDIT_2026_08_07.md +0 -254
  163. package/src/DUPLICATION_AUDIT_REMEDIATION.md +0 -250
  164. package/src/DUPLICATION_REVIEW_2026_10_24.md +0 -1072
  165. package/src/REVIEW_2026_08_06/adv-core.md +0 -511
  166. package/src/REVIEW_2026_08_06/adv-gfx.md +0 -480
  167. package/src/REVIEW_2026_08_06/adv-view.md +0 -446
  168. package/src/REVIEW_2026_08_06/api-core.md +0 -296
  169. package/src/REVIEW_2026_08_06/api-engine.md +0 -195
  170. package/src/REVIEW_2026_08_06/bvh-phys-sound-net.md +0 -507
  171. package/src/REVIEW_2026_08_06/core-binary.md +0 -348
  172. package/src/REVIEW_2026_08_06/core-collection.md +0 -376
  173. package/src/REVIEW_2026_08_06/core-math.md +0 -221
  174. package/src/REVIEW_2026_08_06/core-process.md +0 -266
  175. package/src/REVIEW_2026_08_06/core-science.md +0 -223
  176. package/src/REVIEW_2026_08_06/engine-ai-nav.md +0 -279
  177. package/src/REVIEW_2026_08_06/engine-animation.md +0 -167
  178. package/src/REVIEW_2026_08_06/engine-asset.md +0 -166
  179. package/src/REVIEW_2026_08_06/engine-ecs.md +0 -920
  180. package/src/REVIEW_2026_08_06/engine-input.md +0 -197
  181. package/src/REVIEW_2026_08_06/engine-misc.md +0 -248
  182. package/src/REVIEW_2026_08_06/engine-terrain.md +0 -52
  183. package/src/REVIEW_2026_08_06/generation.md +0 -176
  184. package/src/REVIEW_2026_08_06/geom-2d.md +0 -434
  185. package/src/REVIEW_2026_08_06/geom-3d.md +0 -117
  186. package/src/REVIEW_2026_08_06/gfx-gi.md +0 -143
  187. package/src/REVIEW_2026_08_06/gfx-render.md +0 -740
  188. package/src/REVIEW_2026_08_06/gfx-texture.md +0 -690
  189. package/src/REVIEW_2026_08_06/prior-criticals.md +0 -49
  190. package/src/REVIEW_2026_08_06/prior-highs.md +0 -138
  191. package/src/REVIEW_2026_08_06/test-gaps.md +0 -364
  192. package/src/REVIEW_2026_08_06/view.md +0 -667
  193. package/src/REVIEW_2026_08_06.md +0 -610
  194. package/src/core/binary/lz4/LZ4_HIGH_COMPRESSION_PLAN_2026_09_14.md +0 -542
  195. package/src/core/bvh2/bvh3/NOTES.md +0 -2
  196. package/src/core/bvh8/NOTES.md +0 -63
  197. package/src/core/cache/wtinylfu/benchmark/CacheWTinylfu.bench.md +0 -243
  198. package/src/core/color/COLOR_FIX_PLAN_2026_08_28.md +0 -417
  199. package/src/core/color/COLOR_REVIEW_2026_08_28.md +0 -779
  200. package/src/core/geom/2d/polygon/TRIANGULATION_DESIGN.md +0 -433
  201. package/src/core/geom/3d/atlas/ATLAS_PORT_PLAN.md +0 -486
  202. package/src/core/geom/3d/atlas/BFF_PLAN.md +0 -174
  203. package/src/core/geom/3d/atlas/REVIEW_02.md +0 -392
  204. package/src/core/geom/3d/gjk/GJK_REVIEW_NOTES.md +0 -146
  205. package/src/core/geom/3d/gjk/NOTES.md +0 -8
  206. package/src/core/geom/3d/topology/TOPO_MESH_RETIREMENT_PLAN.md +0 -135
  207. package/src/core/geom/GEOMETRY_REVIEW_002_API_ISSUES.md +0 -381
  208. package/src/core/geom/GEOMETRY_REVIEW_002_NAMING.md +0 -684
  209. package/src/core/geom/MANIFOLD_COMPARISON_2026_08_01.md +0 -554
  210. package/src/core/model/reactive/evaluation/PREDICATE_APPLICABILITY_PROPOSAL.md +0 -446
  211. package/src/engine/REVIEW_2026_07_03.md +0 -211
  212. package/src/engine/asset/loaders/image/IMAGE_CODEC_REVIEW_2026_08_28.md +0 -815
  213. package/src/engine/control/first-person/DESIGN.md +0 -658
  214. package/src/engine/control/first-person/DESIGN_COLLISION.md +0 -382
  215. package/src/engine/control/first-person/DESIGN_EXTENSIONS.md +0 -563
  216. package/src/engine/control/first-person/TODO.md +0 -160
  217. package/src/engine/ecs/ASYNC_SYSTEMS_PROPOSAL.md +0 -531
  218. package/src/engine/ecs/HIERARCHY_REVIEW.md +0 -717
  219. package/src/engine/ecs/async/WORKER_SYSTEM_PLAN.md +0 -262
  220. package/src/engine/ecs/ik/PLAN.md +0 -179
  221. package/src/engine/ecs/storage/binary/SERIALIZATION_COVERAGE.md +0 -272
  222. package/src/engine/ecs/storage/binary/object-v2/DESIGN.md +0 -199
  223. package/src/engine/ecs/transform/TRANSFORM64_MIGRATION_PLAN_2026_09_04.md +0 -521
  224. package/src/engine/graphics/CONTEXT_LOSS_RECOVERY_PLAN.md +0 -463
  225. package/src/engine/graphics/ecs/camera/pp/NOTES.md +0 -48
  226. package/src/engine/graphics/render/frame_graph/CORRECTNESS_FIXES_2026_09_04.md +0 -659
  227. package/src/engine/graphics/render/frame_graph/FRAME_GRAPH_RECOMMENDATIONS.md +0 -235
  228. package/src/engine/graphics/sh3/path_tracer/REVIEW_001_ACTION_PLAN.md +0 -490
  229. package/src/engine/graphics/texture/sampler/compare/FLIP_IMPLEMENTATION_PLAN.md +0 -466
  230. package/src/engine/input/GAMEPAD_API_PROPOSAL.md +0 -717
  231. package/src/engine/input/ecs/ism/ISM_DESIGN.md +0 -1523
  232. package/src/engine/input/ecs/ism/ISM_IMPLEMENTATION_PLAN.md +0 -627
  233. package/src/engine/navigation/mesh/OVERLAPPING_SURFACES_2026_08_25.md +0 -661
  234. package/src/engine/navigation/mesh/PATHFINDING_PLAN.md +0 -229
  235. package/src/engine/network/CONGESTION_CONTROL.md +0 -87
  236. package/src/engine/network/MESSAGE_TRANSPORT.md +0 -364
  237. package/src/engine/network/SMOOTHNESS_DESIGN.md +0 -400
  238. package/src/engine/physics/cloth/MEASUREMENTS.md +0 -5304
  239. package/src/engine/physics/cloth/PLAN.md +0 -3056
  240. package/src/engine/physics/fluid/GPU_FLUID_CASCADES_PROPOSAL_2026_10_16.md +0 -1382
  241. package/src/engine/physics/fluid/REVIEW_02_PLAN.md +0 -243
  242. package/src/engine/physics/fluid/solver/NOTES.md +0 -46
  243. package/src/engine/sound/SOUND_EMITTER_RETIREMENT_PLAN.md +0 -536
  244. package/src/engine/sound/simulation/IMPLEMENTATION_PLAN.md +0 -432
  245. package/src/engine/sound/simulation/PROBE_PLACEMENT.md +0 -250
  246. package/src/engine/sound/simulation/PROBE_TRANSFER_RESEARCH.md +0 -322
  247. package/src/engine/sound/simulation/REFLECTION_IR_PLAN.md +0 -261
  248. package/src/engine/sound/simulation/VISIBILITY_GRAPH_PLAN.md +0 -121
  249. package/src/engine/sound/sopra/IMPLEMENTATION_PLAN.md +0 -999
  250. package/src/format/image/avif/DECISIONS.md +0 -545
  251. package/src/format/image/jpeg/NOTES.md +0 -3
  252. package/src/format/scene/gltf/MESHOPT_COMPRESSION_PLAN.md +0 -814
  253. package/src/generation/COORDINATES.md +0 -54
  254. package/src/generation/REVIEW_01_ACTION_PLAN.md +0 -628
  255. package/src/generation/foliage/ANNUAL_SHOOTS_BRIEF_2026_10_25.md +0 -220
  256. package/src/generation/foliage/CROWN_FILLING_BRIEF_2026_10_26.md +0 -172
  257. package/src/generation/foliage/CROWN_FULLNESS_BRIEF_2026_10_26.md +0 -86
  258. package/src/generation/foliage/FOLIAGE_DESIGN.md +0 -547
  259. package/src/generation/foliage/FOLIAGE_PLAN.md +0 -198
  260. package/src/generation/foliage/LEAF_ARCHITECTURE_BRIEF_2026_10_26.md +0 -158
  261. package/src/generation/foliage/LOD_REWORK_BRIEF_2026_10_27.md +0 -521
  262. package/src/generation/foliage/REAL_TIME_TREES_BRIEF_2026_10_27.md +0 -86
  263. package/src/generation/foliage/SELF_ORGANIZING_FRAMEWORK_BRIEF_2026_10_26.md +0 -347
  264. package/src/generation/foliage/SHOOT_SYSTEMS_ALL_SPECIES_BRIEF_2026_10_26.md +0 -248
  265. package/src/generation/foliage/SPEEDTREE_COMPARISON_2026_10_24.md +0 -694
  266. package/src/generation/foliage/TWO_FRONT_ENDS_BRIEF_2026_10_28.md +0 -552
  267. package/src/shade/RENDERER_CONTRACT.md +0 -697
  268. package/src/shade/SHADER_F16_PROPOSAL_2026_10_24.md +0 -263
  269. package/src/shade/device/IMMEDIATE_DATA_PLAN_2026_08_28.md +0 -569
  270. package/src/shade/device/PASS_ENCODER_PROPOSAL_2026_08_31.md +0 -755
  271. package/src/shade/device/mock/EXTENSION_PROPOSAL_2026_08_22.md +0 -458
  272. package/src/shade/device/mock/IMPLEMENTATION_PLAN.md +0 -255
  273. package/src/shade/device/mock/REVIEW_2026_08_22.md +0 -527
  274. package/src/shade/device/timing/GPU_PROFILER_PROPOSAL_2026_08_28.md +0 -894
  275. package/src/shade/device/timing/profile/SGPT_FORMAT.md +0 -345
  276. package/src/shade/renderer/NOTES.md +0 -78
  277. package/src/shade/renderer/PLAN.md +0 -8
  278. package/src/shade/renderer/animation/ADDITIVE_BLENDING_PLAN.md +0 -145
  279. package/src/shade/renderer/animation/SKINNING_FRAME_PLAN.md +0 -357
  280. package/src/shade/renderer/atmosphere/NOTES.md +0 -6
  281. package/src/shade/renderer/camera/orbital/MomentumAxis.d.ts +0 -76
  282. package/src/shade/renderer/camera/orbital/MomentumAxis.d.ts.map +0 -1
  283. package/src/shade/renderer/camera/orbital/MomentumAxis.js +0 -164
  284. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.d.ts +0 -45
  285. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.d.ts.map +0 -1
  286. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.js +0 -66
  287. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.d.ts +0 -98
  288. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.d.ts.map +0 -1
  289. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.js +0 -184
  290. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts +0 -175
  291. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts.map +0 -1
  292. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.js +0 -411
  293. package/src/shade/renderer/camera/orbital/README.md +0 -81
  294. package/src/shade/renderer/camera/orbital/index.d.ts +0 -6
  295. package/src/shade/renderer/camera/orbital/index.d.ts.map +0 -1
  296. package/src/shade/renderer/camera/orbital/index.js +0 -9
  297. package/src/shade/renderer/extension/RENDER_EXTENSION_DESIGN.md +0 -964
  298. package/src/shade/renderer/fluid/GPU_FLUID_OCCUPANCY_AND_SOTA_REVIEW_2026_10_26.md +0 -361
  299. package/src/shade/renderer/geometry/bvh/BLAS_REFIT_BATCH_PLAN.md +0 -619
  300. package/src/shade/renderer/geometry/meshlet/COMPACTION_PLAN_2026_09_16.md +0 -674
  301. package/src/shade/renderer/geometry/sdf/NOTES.md +0 -6
  302. package/src/shade/renderer/geometry/virtual/ATTRIBUTE_PLAN_2026_09_18.md +0 -540
  303. package/src/shade/renderer/geometry/virtual/MICRON_COMPARISON_2026_08_20.md +0 -305
  304. package/src/shade/renderer/geometry/virtual/VGEO_FORMAT.md +0 -913
  305. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +0 -787
  306. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_PLAN.md +0 -481
  307. package/src/shade/renderer/geometry/virtual/VK_LOD_CLUSTERS_COMPARISON_2026_08_20.md +0 -301
  308. package/src/shade/renderer/geometry/virtual/WEDGE_PLAN_2026_09_18.md +0 -448
  309. package/src/shade/renderer/geometry/virtual/runtime/CULL_PLAN_2026_09_16.md +0 -385
  310. package/src/shade/renderer/geometry/virtual/runtime/CUT_PLAN_2026_09_14.md +0 -656
  311. package/src/shade/renderer/geometry/virtual/runtime/REVIEW_LEDGER.md +0 -308
  312. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN.md +0 -657
  313. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN_2026_09_14.md +0 -551
  314. package/src/shade/renderer/geometry/virtual/runtime/SHADOW_PLAN_2026_09_28.md +0 -529
  315. package/src/shade/renderer/global_illumination/lpv/NOTES.md +0 -3
  316. package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_CLIPMAP_PROPOSAL_2026_10_27.md +0 -4306
  317. package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_RAYS_VARIABLE_AND_SORTED_SCOPE_2026_10_30.md +0 -410
  318. package/src/shade/renderer/global_illumination/sharc/NOTES.md +0 -8
  319. package/src/shade/renderer/gpu_primitive/bvh/GPU_BVH_BUILD_PLAN.md +0 -1369
  320. package/src/shade/renderer/gpu_primitive/bvh/PHASE1_HANDOVER.md +0 -202
  321. package/src/shade/renderer/gpu_primitive/indirect/NOTES.md +0 -94
  322. package/src/shade/renderer/hiz/NOTES.md +0 -3
  323. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts +0 -8
  324. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts.map +0 -1
  325. package/src/shade/renderer/light/cluster/NOTES.md +0 -11
  326. package/src/shade/renderer/light/cluster/PROPOSAL_ALGORITHM_2026_10_16.md +0 -524
  327. package/src/shade/renderer/light/cluster/PROPOSAL_HIERARCHICAL_2026_10_16.md +0 -1101
  328. package/src/shade/renderer/material/resident/shader_resident_texture_means.d.ts +0 -24
  329. package/src/shade/renderer/material/resident/shader_resident_texture_means.d.ts.map +0 -1
  330. package/src/shade/renderer/material/resident/shader_resident_texture_means.js +0 -120
  331. package/src/shade/renderer/particles/DESIGN.md +0 -1011
  332. package/src/shade/renderer/particles/EMITTER_GLOBAL_STATE_PLAN_2026_10_29.md +0 -280
  333. package/src/shade/renderer/particles/ORIENTATION_PLAN_2026_10_15.md +0 -453
  334. package/src/shade/renderer/path_tracer/PATH_TRACED_MEDIA_PLAN_2026_10_02.md +0 -520
  335. package/src/shade/renderer/postprocess/bloom/NOTES.md +0 -6
  336. package/src/shade/renderer/postprocess/fsr/NOTES.md +0 -4
  337. package/src/shade/renderer/postprocess/gtao/NOTES.md +0 -6
  338. package/src/shade/renderer/postprocess/nss/NOTES.md +0 -332
  339. package/src/shade/renderer/postprocess/perspective/NOTES.md +0 -3
  340. package/src/shade/renderer/postprocess/sscs/NOTES.md +0 -5
  341. package/src/shade/renderer/postprocess/ssr/NOTES.md +0 -14
  342. package/src/shade/renderer/postprocess/ssr/REPROJECTION_PROPOSAL.md +0 -286
  343. package/src/shade/renderer/postprocess/ssr/prefilter/NOTES.md +0 -39
  344. package/src/shade/renderer/postprocess/taa/NOTES.md +0 -11
  345. package/src/shade/renderer/postprocess/upscale-dither/NOTES.md +0 -78
  346. package/src/shade/renderer/postprocess/vrs/NOTES.md +0 -4
  347. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_LEDGER.md +0 -1218
  348. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_PLAN_2026_09_03.md +0 -600
  349. package/src/shade/renderer/restir/reservoir/NOTES.md +0 -6
  350. package/src/shade/renderer/shader/chunk/atmosphere/NOTES.md +0 -6
  351. package/src/shade/renderer/shader/chunk/brdf/FRESNEL_BRIEF_2026_10_16.md +0 -391
  352. package/src/shade/renderer/shader/chunk/color/tonemap/ACES_TONE_MAPPING_BRIEF_2026_10_17.md +0 -153
  353. package/src/shade/renderer/shader/chunk/color/tonemap/HDR_TONE_MAPPING_FORMS_BRIEF_2026_10_16.md +0 -322
  354. package/src/shade/renderer/shader/chunk/color/tonemap/SDR_TONE_MAPPING_BRIEF_2026_10_16.md +0 -247
  355. package/src/shade/renderer/shader/chunk/fsr/NOTES.md +0 -3
  356. package/src/shade/renderer/shader/chunk/fsr/spd/NOTES.md +0 -4
  357. package/src/shade/renderer/shader/chunk/geometry/ray/NOTES.md +0 -4
  358. package/src/shade/renderer/shader/chunk/material/MULTISCATTER.md +0 -159
  359. package/src/shade/renderer/shader/chunk/mboit/NOTES.md +0 -38
  360. package/src/shade/renderer/shader/chunk/spherical_harmonics/l1i/NOTES.md +0 -3
  361. package/src/shade/renderer/shader/chunk/spherical_harmonics/sampling/NOTES.md +0 -8
  362. package/src/shade/renderer/shader/compiler/SHADER_SWITCH_PROPOSAL_2026_10_16.md +0 -927
  363. package/src/shade/renderer/shadow/contact/FINDINGS.md +0 -413
  364. package/src/shade/renderer/shadow/map/csm/NOTES.md +0 -5
  365. package/src/shade/renderer/shadow/ray/NOTES.md +0 -31
  366. package/src/shade/renderer/shadow/sdf/NOTES.md +0 -9
  367. package/src/shade/renderer/shadow/sdf/brixelizer/NOTES.md +0 -5
  368. package/src/shade/renderer/shadow/sdf/cascade/NOTES.md +0 -9
  369. package/src/shade/renderer/shadow/vsm/NOTES.md +0 -12
  370. package/src/shade/renderer/shadow/vsm/PLAN.md +0 -442
  371. package/src/shade/renderer/shadow/vsm/REVIEW_LEDGER.md +0 -43
  372. package/src/shade/renderer/texture/COMPRESSED_TEXTURE_PLAN_2026_09_01.md +0 -2444
  373. package/src/shade/renderer/texture/mipmaps/NOTES.md +0 -3
  374. package/src/shade/renderer/volumetrics/NOTES.md +0 -1329
  375. package/src/shade/renderer/volumetrics/VOLUMETRIC_NOISE_PLAN_2026_10_15.md +0 -336
  376. package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts +0 -23
  377. package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts.map +0 -1
  378. package/src/shade/renderer/water/medium/GPUWaterBodies.js +0 -75
  379. package/src/shade/wgsl/NOTES.md +0 -4
package/CHANGELOG.md ADDED
@@ -0,0 +1,2602 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ ### Virtual geometry: proxy meshes, offline
6
+
7
+ - **Added:** `vgeo_to_proxy_mesh(reader, triangle_limit)` (`geometry/virtual/proxy/`) turns an open
8
+ `VGeoContainerReader` into an ordinary `MeshletGeometry` of at most `triangle_limit` triangles, with no device, for
9
+ systems that want a plain mesh of a VGEO asset, such as card capture. Nothing is simplified: it cuts the
10
+ DAG from its roots, always refining the group whose parents carry the largest error, and stops at the
11
+ first refinement over the limit. Error stays uniform and the cut is watertight. The cut's clusters are
12
+ the meshlets, as stored, with the mesh path's BVH. Pages install as the descent reaches them. On Zorah's
13
+ `SM_Courtyard_Ceiling_A1_1` (17.6M triangles, 689 MiB), 100,000 triangles took 0.44 s and read 27 MiB,
14
+ and 1,000 took 23 ms. A limit below the roots' triangle count gets the roots.
15
+ - **Changed:** `vgeo_runtime_meshlet_batch` writes its metadata rows through the new
16
+ `vgeo_meshlet_metadata_write`, which the proxy shares.
17
+
18
+ ### Probe clipmap GI: no resident materials for a raster capture; a hit no card sees is black
19
+
20
+ - **Fixed:** the probe clipmap GI made every material of the scene resident each frame, and a scene past
21
+ the resident table's 819 material ids (ids are never reused) threw "material id … exceeds material
22
+ limit" from `GPUProbeClipmapGI.graph_update` every frame. The raster card capture, the default, reads
23
+ the scene's own material table and now makes nothing resident. A traced capture
24
+ (`MeshCardCaptureMode.Trace`, and templates with no raster surface) makes resident only the materials
25
+ it traces, just before it traces them; the 819 limit still bounds those, the path tracer and the
26
+ bakers.
27
+ - **Changed:** a probe ray's hit that no card sees, or on a row with no card set (virtual geometry, a
28
+ pair still capturing), brings back no light, as in UE. It took the material's colour factor × its
29
+ textures' means. The colour factor alone, from the scene's table, was measured first and made Sponza's
30
+ bounce-lit gallery 60% brighter on screen; black makes it 27% darker than the means (mean luma 29.3 →
31
+ 21.5) and the sunlit nave 1% darker. Virtual geometry has no cards yet, so its surfaces bounce no light.
32
+ - **Breaking:** `GPUResidentMaterialContext.buffer_texture_means`, `onMaterialRepacked` and
33
+ `shader_resident_texture_means.js` are gone. Card recapture listens to the new
34
+ `GPUMaterialManager.onMaterialRestaged`. `GPUResidentMaterialContext.commit()` writes what
35
+ `obtain_material` made resident without taking every material the manager holds, as `update()` does.
36
+ `GPUProbeClipmap.graph_update` takes no `materials`.
37
+
38
+ ### HZB mip 0: a footprint fixed per axis, and shadow cascades the HZB halves exactly
39
+
40
+ - **Faster:** the HZB's first reduction reads a footprint of a constant 2 or 3 source texels per axis,
41
+ picked when the viewport changes (`hzb footprint x` / `hzb footprint y`, four variants), instead of
42
+ looping over bounds computed per thread. Output is unchanged. In `water-bodies`, against the old loop
43
+ at the same sizes in one process: the shadow cascades' 6 reductions 2.45 → 1.24 ms a frame on an AMD
44
+ RDNA 2 iGPU (0.505×) and 0.78× on an RTX 4090; the main view's 0.36× and 0.75×.
45
+ - **Changed:** directional cascades default to 1536 each (were 1740, then 1440). Half of 1536 is a mip 0
46
+ the HZB takes unpadded, so cascades reduce 2×2 and their pyramid is two 4-mip SPD passes and one
47
+ single-mip pass, where 1740 and 1440 took 3×3 and five single-mip passes. About the same texels in
48
+ all (7.08M against 7.17M): the first cascade 12% coarser across, the others 7% finer. With both
49
+ changes the HZB chain in `water-bodies` went ~5.2 → ~1.85 ms a frame on the iGPU, 0.142 → 0.086 ms on
50
+ the 4090.
51
+ - **Breaking:** one mip 0 shader reduces a region of its source for a camera (the whole viewport) and for
52
+ an atlas slot: `shader_hzb_reduce_mip0` moved from `shader_hzb_reduce_any.js` to
53
+ `shader_hzb_reduce_mip0.js` and takes a clip region; `shader_hzb_reduce_mip0_clip.js` is gone, and so
54
+ is its span label, `HZB / reduce mip0 with clip`. `HierarchicalZBuffer.setViewportSize` refuses a side
55
+ of 0.
56
+
57
+ ### One orbital camera: the harness, the editor and Shade's playgrounds share a rig
58
+
59
+ - **Changed:** every orbital camera is now an `OrbitalCameraRig` (`src/engine/graphics/camera/orbital/`) moving a
60
+ `TopDownCameraController`: a drag moves the camera as it happens, letting go throws it on with the drag's recent
61
+ velocity (half life 0.1 s, the same distance at any frame rate), grabbing again stops every throw, and a wheel notch
62
+ zooms by a fixed proportion. The rig keeps velocities, not a pose, so a writer that adds to the controller composes
63
+ with a throw and one that sets it outright wins. It subscribes to nothing: `makeOrbitalCameraController`,
64
+ `EditorCameraNavigation` and Shade's `OrbitalCameraController` each feed it their own pointer.
65
+ - **Fixed:** the harness camera (`makeOrbitalCameraController`) zoomed by one world unit a notch, so 0.5% at distance
66
+ 200 and 50% at distance 2; it now zooms by the same proportion at any distance. Its pitch had no limit, so a long
67
+ drag turned the camera over and under the ground; it stops at straight down and straight up.
68
+ - **Fixed:** `KeyboardCameraController` moved the target along the camera's own forward, so at the harness's default
69
+ pitch of 1.4 W sent it 98.5% into the ground, and a diagonal travelled √2 too far. It now moves the target across
70
+ the ground along the view's heading (`top_down_truck_on_ground`), at full speed at any pitch.
71
+ - **Fixed:** the harness registers `ScriptSystem` when it builds the orbital camera. Nothing registered it before, so
72
+ `KeyboardCameraController`'s script never ran in a harness scene and W/A/S/D did nothing. Every `Script` in a
73
+ harness scene now runs, `Tween`'s and `playAnimationTrack`'s included.
74
+ - **Fixed:** `TopDownCameraController.setFromEyeAndTarget` reported about half of all views turned over: pitch past
75
+ the vertical, rolled half a turn. Same rotation, but an orbit clamped it, so the first drag snapped the camera to
76
+ straight down. It now always reports pitch within ±π/2, and no roll for an up of +Y.
77
+ - **Changed:** the harness orbits on the primary button (and touch) and pans on the secondary, the other way round
78
+ from before. The editor's orbit turns the world with the hand, the opposite horizontal direction from before.
79
+ Shade's `OrbitalCameraController` zooms on the vertical wheel only; a sideways swipe no longer zooms.
80
+ - **Added:** `top_down_truck_in_view` and `top_down_truck_on_ground` beside `TopDownCameraController.pan`, and
81
+ `MouseEventButtons`, the bits of `MouseEvent.buttons`.
82
+ - **Breaking:** removed `rotate_from_view` and `TopDownCameraController.rotate`; Shade's `OrbitalCameraRig`,
83
+ `OrbitalCameraInput`, `MomentumAxis`, `OrbitalCameraConfig` (`ORBITAL_CAMERA_FEEL_DEFAULTS`,
84
+ `createOrbitalCameraConfig`, `createPrecisionProfile`) and the `camera/orbital/index.js` barrel; Shade's
85
+ `OrbitalCameraController` lost its third constructor argument, `flyTo` and `pan`. `MomentumAxis` and
86
+ `OrbitalCameraRig` live on in `src/engine/graphics/camera/orbital/` with new interfaces.
87
+ `EngineHarness.buildOrbitalCameraController` and `makeOrbitalCameraController` no longer take `sensitivity`.
88
+ `computeTopDownTransform` moved from `TopDownCameraControllerSystem.js` to `TopDownCameraController.js`.
89
+
90
+ ### Water: cheaper optics, and how to author a body's area
91
+
92
+ - **Changed:** the water optics pass takes transmission and radiance away from water from its clear, not from a
93
+ full-screen draw; its background copy writes only the background. Output is bit-identical; on an integrated GPU
94
+ the pass is about 0.22 ms cheaper at 1280 × 720.
95
+ - **Changed:** the optics pass no longer evaluates the pixel surface the receivers pass already shaded. The receivers
96
+ leave it in `Water trace metadata`, which is now `rgba32uint` (control path, octahedral normal, slope variance and
97
+ coverage, body). Water is about 1.35 ms cheaper a frame on an integrated GPU at 1280 × 720. The normal carries
98
+ 16-bit octahedral precision, so output is not bit-identical.
99
+ - **Removed:** water debug view 6 (Newton residual), and the residual the pixel probe returned as `radiance[3]`.
100
+ - **Documented:** a body's geometry is its area. Author it with the fewest triangles that cover it: every internal
101
+ edge splits the lattice cells it crosses and keeps 8 × 8 patches from spanning it. Subdividing a flat area adds
102
+ nothing to the waves, which the renderer tessellates itself; see `Water`.
103
+
104
+ ### `Trail3D`: the fade is seen as authored, and each trail can shape it
105
+
106
+ - **Fixed:** a trail's alpha is the opacity of the tube as seen. The tube is drawn from both sides, so every line of
107
+ sight crosses two faces, and both used to carry the full alpha: `color.a = 0.5` covered 0.75, and the linear fade
108
+ held near solid for most of the trail's life before collapsing at its tail. Each face now carries half the optical
109
+ depth (`1 - √(1 - α)`), so the two cover exactly `α`. Trails authored against the old look will read thinner.
110
+ - **Added:** `Trail3D.fade`, an `AnimationCurve` of opacity over a knot's age relative to `maxAge`, multiplied into
111
+ `color.a`. Every trail starts on `TRAIL3D_FADE_LINEAR`, one shared, frozen, linear curve; assign your own to
112
+ reshape a trail's fade (`TRAIL3D_FADE_LINEAR.clone()` is an editable copy). It round-trips through
113
+ `toJSON`/`fromJSON` and takes part in `equals`.
114
+ - **Changed:** `TubeXFixedPhysicsSimulator.update(tube, max_age, timeDelta, alpha, fade)` takes the curve, and
115
+ `alpha` has no default any more.
116
+
117
+ ### `SegmentedResourceBarView`: crisp notches with fixed meanings, stacked values, a ghost that trails, and cells
118
+
119
+ - **Changed:** notches are DOM elements on whole device pixels at any `devicePixelRatio` and under a CSS scale, not
120
+ canvas strokes. Each carries its tier's `class_name`; `segmented-resource-bar.css` (beside the view, import it from
121
+ a style entry point) styles them through `--segmented-bar-*` custom properties.
122
+ - **Changed:** notch tiers have fixed meanings. `tiers` takes `SegmentTier`s (`unit`, `class_name`, `width_px`,
123
+ `min_spacing_px`); a notch belongs to the coarsest tier whose unit divides it, and a tier too tight on screen drops
124
+ out, finest first. Without `tiers`, `SEGMENT_TIERS_DEFAULT` marks every 10, 100 and 1000.
125
+ - **Fixed:** `values` stack end to end, their total clamped to `max`; each fill used to start at the left edge.
126
+ - **Fixed:** the ghost trails a drop: it holds for `ghost_delay_ms` (450), then eases down over `ghost_duration_ms`
127
+ (350), and jumps up on a rise. Under `prefers-reduced-motion` it holds, then snaps. The model is `GhostTrail`.
128
+ - **Added:** `cells: { count, group }` draws separate cells with identical gaps, one partly filled cell allowed.
129
+ - **Added:** the layout as pure functions: `segment_tick_positions`, `segment_notch_place`, `segment_cell_layout`.
130
+ - **Deprecated:** `RESOURCE_BAR_SEGMENTS` and `SegmentDefinition`; the view no longer reads them.
131
+
132
+ ### The daily driver: graph trees draw their species' shoot prototypes, and are the default
133
+
134
+ - **Changed:** `generate_tree` grows by the generator graph unless asked otherwise (`growth`), a preset's name
135
+ included. Ask for `TreeGrowth.Botanical` for trees seen up close. A graph tree draws its species' shoot prototypes on
136
+ its carriers (`tree_shoot_instances`):
137
+ - each is scaled to its carrier by a step of the error ladder (√2), and turned about the vertical;
138
+ - the wood any LOD draws is grafted onto the carrier, which takes the prototype's girth, growth and units;
139
+ - every leaf and twig is the prototype's cuts, the instance's steps finer than the tree's error, from the species'
140
+ atlas.
141
+
142
+ It fits the cost ceiling at 0.2-3.6 s a tree once its species is cached (botanical 1.4-11.9 s, same run), at the
143
+ botanical tree's crown density within -0.03 to +0.12 (TWO_FRONT_ENDS_BRIEF_2026_10_28.md, G3).
144
+ - **Changed:** without a `cache`, every call shares `TREE_SPECIES_CACHE`. A species' materials and prototypes are
145
+ made once in a program; `clear()` lets them go. A caller's own species goes through `generate_tree({ species,
146
+ growth })`; a `TreeSpeciesConfig` passed alone is grown by the generator graph.
147
+ - **Added:** three skeleton records:
148
+ - `TreeSkeleton.branch_instance`: the leaf collector leaves an instance's branches to its prototype;
149
+ - `segment_carried_leaf_area` and `segment_carried_reach`: what a segment carries without holding it, which the
150
+ wind's mechanics load it with.
151
+
152
+ `TreeAsset.side_area`, `shoot_prototypes` and `shoot_instances` record what a tree grew and what draws it.
153
+ `tree_lod_plan` takes `cards` and `tree_grown_side_area` takes `carried`, for leaves a tree draws but doesn't
154
+ hold.
155
+ - **Added:**
156
+ - `tree_skeleton_floor_girth`: no branch is thinner than what grows from it.
157
+ - `tree_skeleton_copy_branch` copies one branch through a placement, its girth scaled with it.
158
+ - `tree_skeleton_extend_branch` is the shoot simulation's carrier extension, now shared.
159
+ - `tree_cluster_hang` is `tree_clusters`' hang and skin, now shared.
160
+ - **Changed:** `tree_render_cost` reads `TreeAsset.side_area`.
161
+ - **Changed:** `tree_skeleton_compute_segment_mechanics` reports each segment's `leaf_area`.
162
+ - **Removed:** the plan to space graph carriers by their prototypes' sleeves (G2). The graph tree already has the
163
+ botanical tree's framework and density.
164
+
165
+ ### Shoot prototypes: a species' shoot systems grown once, cut and baked into one atlas
166
+
167
+ - **Added:** `tree_shoot_prototypes`, cached by `TreeSpeciesCache.prototypes` (TWO_FRONT_ENDS_BRIEF_2026_10_28.md,
168
+ G1). Each prototype is a shoot system grown in place on a carrier of the species' graph framework, and kept where it
169
+ grew, under the framework's crown frame. There are as few as reach every carrier length, without a gap, within the
170
+ leaves' own size spread: 6 to 27 per species. All of them are cut at every error from 1 cm to where they draw no
171
+ wood, and drawn with a few tiles of one atlas.
172
+ - **Added:** `tree_skeleton_grow_shoots_on` grows the shoot systems of chosen carriers only.
173
+ `tree_skeleton_copy_branches` copies branches with their nodes, segments, units, scars and dead flags into another
174
+ skeleton. `tree_cluster_atlas` is the cut, representatives and bake that `generate_tree` ran inline, now shared.
175
+ `foliage_scale_spread` and `TREE_LOD_ERROR_STEP` are the representatives' and the LOD plan's own, now shared.
176
+ - **Fixed:** `tree_clusters` hung every whole-tree cut from branch 0's root. A skeleton holding several trees now
177
+ hangs each one's cut from its own root.
178
+ - **Performance:** `tree_cluster_representatives` scored every candidate against every cut at every pick. It now
179
+ counts in each candidate's size window with a Fenwick tree a LOD, and sums unlikeness only for tied candidates. The
180
+ picks are bit-identical. The pine's representatives went from 46.4 s to 6.3 s.
181
+
182
+ ### Two growth front ends: a tree grown by its generator graph alone
183
+
184
+ - **Added:** `generate_tree`'s `growth` (`TreeGrowth.Graph` or `TreeGrowth.Botanical`, the default). A graph tree is
185
+ the framework of the generator graph alone (`tree_skeleton_grow_framework`, new). A botanical tree is that framework
186
+ with every shoot system grown on it (`tree_skeleton_grow`, as before). Both are drawn by the same pipeline and fit
187
+ the same cost gate. Graph trees are sparse, carrying only their framework's leaves (side density 0.21-0.38 against
188
+ the botanical trees' 0.25-0.71), until their cards come from a botanical prototype
189
+ (`TWO_FRONT_ENDS_BRIEF_2026_10_28.md`, G1-G3).
190
+ - **Added:** `make_tree_species_preset`'s third argument `growth`. The pine grown by its graph has a framework of its
191
+ own (stem, whorl boughs, inter-whorl branches, twigs), as the botanical pine grows from its seed.
192
+ - **Added:** `TreeSpeciesCache`: passed to `generate_tree` as `cache`, it makes a bark material once per
193
+ `bark_material` and a leaf atlas once per `foliage` and `foliage_material`, when a tree first asks for one, and shares
194
+ them between the trees that do. Each tree clones the leaf material and tints its own. A tree drawn with the caller's
195
+ own materials makes none.
196
+ - **Changed:** both front ends end on one crown frame law (`tree_skeleton_fit_crown_frame`): the box of every living
197
+ branch a shoot rule grows on or grows. The botanical tree's frame now includes its last season's growth.
198
+ - **Changed:** a tree's bark texture is its species' and no longer changes with its seed: every bark has the same
199
+ noise (`make_bark_material`'s own seed). The leaf atlas never did.
200
+
201
+ ### No cluster card draws its tile at a size the tree's leaves do not grow to
202
+
203
+ - **Fixed:** `tree_cluster_representatives` drew a cut with any tile, magnified without bound, when the texture
204
+ ceiling could not hold a cover within the leaves' spread at `FoliageTileLayout.legible_texel`. A tile's twigs then
205
+ drew as boughs: the pine's LOD0 cuts were magnified up to 10.8x (seed 60557), the spruce's 15.6x, and shrunk down
206
+ to 0.02x on the shrub. LOD0's texel now keeps halving past the legible texel until every cut of every LOD is drawn
207
+ within the spread: pine 6.4 mm, spruce 4.0, shrub 4.8-9.6, oak 2.83 (was 1.41, with 2-8 cuts past the spread). Trees
208
+ whose cover fitted at or before the legible texel (willow, birch, maple, palm, fern) are drawn as before. Crown
209
+ density holds within .03 at every LOD; the atlases shrink (pine 1038 x 1005 to 625 x 650).
210
+
211
+ ### Bark moves with its rig: no step at a joint in the wind
212
+
213
+ - **Fixed:** a bark ring is weighted to the joint whose span it is in, alone; a child's first ring, and its wood before
214
+ its own first joint, are carried as its parent's bark carries its base. The weights blended a ring's joint with the
215
+ joint above by `t_norm` along the whole branch, so the rings either side of a pivot carried different shares of the
216
+ joint above: in the wind the bark stepped there (the palm's trunk at 5.85 m: [0.70 J1, 0.30 J0] below,
217
+ [0.70 J2, 0.30 J1] above, 7.6 cm apart in the reference wind). The rig placed its joints as turning whole spans
218
+ about their pivots (`tree_skeleton_assign_skin_joints`); the bark now moves as it was placed. Leaves and cluster
219
+ cards, which take the bark's weights where they hang, follow it.
220
+
221
+ ### A cluster card lies where what it draws spreads, as sharp as the texture ceiling affords
222
+
223
+ - **Changed:** `tree_cluster_cards(skeleton, attachments, leaves, clusters)` fits each card's plane to what it draws,
224
+ its leaves' cards and its twigs' bands each by their area, not to where its leaves attach. A palm's leaves attach
225
+ along a rachis arching in a vertical plane while they spread to either side of it: its cards stood upright, and its
226
+ crown seen from below was .37 dense against .70 drawn leaf by leaf; now .69.
227
+ - **Changed:** `tree_cluster_representatives(skeleton, attachments, leaves, lods, fine_texel, legible_texel, budget)`
228
+ draws LOD0 at the finest texel the texture ceiling affords for its whole cover: the leaf material's own
229
+ (`FoliageTileLayout.texel`), coarsened by halves while the cover does not fit, never past
230
+ `FoliageTileLayout.legible_texel` (was `cluster_texel`). The palm's fronds are drawn at 13.2 mm where they were at
231
+ 49.6 mm, the maple's leaves at 2.1 mm where they were at 8.5.
232
+ - **Changed:** `tree_lod_plan` counts a cluster card per cluster without building the cards.
233
+
234
+ ### A few cluster tiles draw every cut, sharp, in one atlas per tree
235
+
236
+ - **Added:** `mesh/tree_cluster_representatives.js`: the fewest cuts whose tiles draw every cut of every LOD at a
237
+ size the tree's own leaves grow to (a cut magnifies its representative's leaves by their size ratio, held within
238
+ the 10th to 90th percentile of `attachments.scales` about the median), the least unlike each cut (leaf coverage,
239
+ where it hangs) where several do, no more than the tree's texture ceiling holds. A cut draws its representative on
240
+ its own plane, as large as it draws (its leaves' and twigs' area, so the crown keeps its leaf area), hanging where it
241
+ hangs, turned to run out its way.
242
+ - **Added:** `FoliageTileLayout.cluster_texel`, the texel a cluster card draws the leaf at: its outline's finest
243
+ feature four texels across, its margin's teeth aside (`LeafGeometry.outline`, `FoliageTileLayout.outline`), a
244
+ needle two texels across its width. A coarser LOD's representative is baked where the LOD before it is drawn to a
245
+ pixel, its error, or the leaf's texel if finer.
246
+ - **Added:** `TREE_RENDER_COST_CEILING.texture_density`, 240,667 bytes of foliage texture per m^2 (the shrub's leaf
247
+ atlas at 6aff5265e, the 640 KiB every preset's was); `tree_render_cost`'s `texture_bytes` and `texture_density`
248
+ count each map of every LOD's foliage material once. Every preset fits it.
249
+ - **Added:** `material/tree_cluster_press.js`, `material/tree_cluster_tiles.js` and
250
+ `material/tree_cluster_atlas_pack.js`, the bake's pressing, tile layout and packing, which the choice measures too.
251
+ Every card presses its own copy of its leaves, so a leaf on cuts of two LODs lies flat on each.
252
+ - **Changed:** every LOD of a tree draws its foliage with one atlas (`TreeLODAsset.foliage_material` is the same
253
+ material on all of them). `tree_cluster_bake` takes a texel per card; `tree_mesh_build_clusters` takes each
254
+ card's placement `{rep, frames}` and draws the representative's tile through it.
255
+
256
+ ### The nested clipmap placement is shared
257
+
258
+ - **Added:** `ClipmapCascadePlacement` (`core/geom/clipmap/`): the windows of a stack of nested cascades around one
259
+ anchor, placed coarsest first by the hysteretic rule, re-placed whole on a jump, each child clamped inside its parent.
260
+ The corner step, the hysteresis and the nesting margin are options, and `clipmap_cascade_nest_resolution_min(step,
261
+ margin)` is the smallest side that nests. The fluid's placement, generalised for the probe clipmap's cascades.
262
+ - **Changed:** `FluidCascadePlacement` is a `ClipmapCascadePlacement` with the fluid's step, hysteresis and margin. Its
263
+ placement is unchanged.
264
+ - **Breaking:** `FluidCascadePlacementResult` is `ClipmapCascadePlacementResult`. `FLUID_CASCADE_NEST_RESOLUTION_MIN` is
265
+ gone: the floor is `clipmap_cascade_nest_resolution_min`.
266
+
267
+ ### Each LOD carries its own rig, on the wood it draws
268
+
269
+ - **Added:** `tree_skeleton_assign_skin_joints(skeleton, species, options)`: `options.tolerance` in place of
270
+ `deflection_tolerance`; `options.rings`, the wood an LOD draws, puts joints only on it, no further out than its
271
+ bark; `options.within`, a finer rig's joints, places joints only where it has one, so a coarser LOD's joints are a
272
+ subset of the finer's. Its result carries `joint_rotation` and `node_joints`, the binding it wrote.
273
+ - **Added:** `tree_skeleton_bind_skin_joints(skeleton, skin_info)` binds a derived rig back onto its skeleton.
274
+ - **Added:** `asset/tree_lod_plan.js`: each LOD's error and rig, the finest error at which the tree fits its ceiling
275
+ in triangles and in joints; the rig's tolerance is the larger of `deflection_tolerance` and the error. A tree that
276
+ fits no ceiling at any error is drawn without wood.
277
+ - **Changed:** each `TreeLODAsset` has its own `skin_info` and `wind_rig`, and its bark and cluster cards are
278
+ skinned to them. `TreeAsset.skin_info` is the rig of all the wood as grown, which `attachments` are bound to;
279
+ `TreeAsset.wind_rig` is LOD0's.
280
+ - **Changed:** `tree_render_cost`'s `joints` and `joint_density` are per LOD; every LOD's rig is held to
281
+ `TREE_RENDER_COST_CEILING.joint_density`, and every preset fits it.
282
+ - **Removed:** `asset/tree_lod_errors.js`: `tree_lod_errors` is `tree_lod_plan`, whose LODs carry their rig;
283
+ `tree_triangles_at_error` moves with it.
284
+
285
+ ### Each LOD's leaves are cluster cards baked from what the tree grew
286
+
287
+ - **Added:** each LOD draws the twigs and leaves its wood leaves out as cluster cards, one per cut
288
+ (`mesh/tree_clusters.js`): an undrawn branch on drawn wood with all it carries, or a drawn branch's undrawn tip; a
289
+ leaf on drawn wood joins its branch's nearest cut. A card lies in the plane its leaves and twigs spread in, facing
290
+ away from the crown (`mesh/tree_cluster_cards.js`), and is a 6-triangle hull skinned as its cut hangs
291
+ (`mesh/tree_mesh_build_clusters.js`).
292
+ - **Added:** `material/tree_cluster_bake.js`: each card's tile drawn orthographically from the leaves and twigs the
293
+ tree grew, at a texel equal to the LOD's error. Each leaf is pressed onto its card, turned about where it attaches,
294
+ so it covers there what it covers facing it; twigs are bands their length by their diameter. Albedo, normal (in the
295
+ card's frame) and ORM, one atlas per LOD, each card cut to its tile's alpha.
296
+ - **Added:** `asset/tree_lod_errors.js`: each LOD's error, the finest a half-octave apart from
297
+ `TREE_LOD_ERROR_FINEST` (1 cm, never finer) at which the tree's bark and cards fit `TREE_RENDER_COST_CEILING`'s
298
+ triangles at that LOD, never finer than the LOD before. `generate_tree` draws each LOD to it.
299
+ - **Added:** `tree_grown_side_area(skeleton, leaves)`: the side area of what the tree grew, its wood's nodes and every
300
+ leaf's card, whatever an LOD draws; `tree_render_cost` measures against it. `TREE_RENDER_COST_CEILING` is
301
+ re-derived under it at 6aff5265e: 761.2, 412.3 and 96.2 triangles and 16.89 joints per m^2.
302
+ - **Added:** `tri2_rasterize_barycentric` (`core/geom/2d/triangle`): the texels a triangle covers, with their
303
+ barycentric weights, by the top-left rule. The crown opacity measure draws with it.
304
+ - **Added:** `foliage_material_from_samplers(albedo, normal, orm, name)`, the foliage material over given maps.
305
+ - **Changed:** `TreeLODAsset.foliage_material` is the LOD's own, its cluster atlas, built with its geometry;
306
+ `TreeAsset.foliage_material` stays the leaf material the bakes draw from. `configure_lazy_geometries`'s foliage
307
+ builder returns `{geometry, material}` or `null`. `TreeLODAsset.config` is the LOD's config, at its error.
308
+ - **Changed:** `tree_mesh_build_foliage(skeleton, attachments, config, cutout, out)` draws every leaf and takes no
309
+ LOD: it is the leaf reference the bakes and measures draw from.
310
+ - **Changed:** `FoliageAttachmentSet.ranks` is `attach_t`, where along its carrier each card attaches.
311
+ - **Changed:** `foliage_atlas_cutout_fit_tile` and `foliage_atlas_cutout_fit_alpha` take a tile's height apart from its
312
+ width.
313
+ - **Removed:** `TreeLODConfig.foliage_keep_ratio`, `foliage_area_compensation_exponent` and `simplify_cards`: no LOD
314
+ thins its leaves.
315
+
316
+ ### Each LOD's bark is the tree at a geometric error
317
+
318
+ - **Added:** `TreeLODConfig.error`, metres: wood thinner than it is not drawn as a tube, nor anything it carries, and
319
+ its spine ends where it thins to it; the bark's rings, and each ring's sides, are as few as keep the surface it draws
320
+ (spine, radius, lobes, flutes, ovalization, collar bosses, knots and the weld's flare) within it, half along the
321
+ branch and half around it. `TreeLODConfig.createDefaultChain(count, error)` doubles it per level from
322
+ `TREE_LOD_ERROR_FINEST` (1 cm, chosen until each tree derives its own).
323
+ - **Added:** `polyline_simplify_by_error` (`core/geom/polyline`): Douglas-Peucker over any sequence, the caller
324
+ measuring the error.
325
+ - **Added:** `tree_mesh_compute_branch_rings`'s rings carry `start`, the vertex their ring begins at.
326
+ - **Changed:** a foliage card finds the bark rings it sits between by where it attaches along its carrier, and sits
327
+ exactly there; a card on wood the LOD does not draw stands on its skeleton.
328
+ - **Removed:** `TreeLODConfig.radial_scale`, `longitudinal_scale`, `max_branch_level`, `min_branch_radius`;
329
+ `BranchGeneratorConfig.radial_segments_min`, `radial_segments_max`.
330
+
331
+ ### What a tree costs to draw
332
+
333
+ - **Added:** `analysis/tree_render_cost.js`: a generated tree's triangles at every LOD and its wind joints, per square
334
+ metre it shows from the side (`tree_envelope_side_area`, the mean side area of its drawn envelope), and
335
+ `TREE_RENDER_COST_CEILING`, the densest of the presets at 6aff5265e, the last real-time generator.
336
+
337
+ ### The pine grows from its seed, and every shoot keeps out of older wood
338
+
339
+ - **Added:** `ShootGrowthConfig.age`, `leader_onset`, `leader_height_scale` and `leader_unit(height)`: a tree
340
+ grows for its age, and a carrier that stops before then grows on from its tip, written onto it. The stem grows by
341
+ height (Kellomäki & Kurttio 1991, Eq. 6).
342
+ - **Added:** `ShootGrowthConfig.apical_control`: Borchert-Honda sharing of the light the shoots receive (Palubicki et
343
+ al. 2009), scaling each new unit's length. 0.5 changes nothing.
344
+ - **Added:** `ShootWoodHash`: no shoot grows through older shoot wood it does not join, and a shoot whose line thicker
345
+ wood has grown into dies, as does a lateral whose tip the wood its mother grew since has closed over. No shoot grows
346
+ into the ground. A framework branch its parent's thickening has grown over is dead.
347
+ - **Added:** `shoot_survival(growth, light, gross)`, a new shoot's chance in its light.
348
+ - **Changed:** a lateral of a generator that bears no leaves (no foliage rule) does not grow: a shoot pays for its
349
+ offspring with its leaves.
350
+ - **Added:** `analysis/tree_leaf_area_targets.js`: measured whole-tree leaf area against DBH per preset species
351
+ (Forrester et al. 2017, Martin et al. 1998, Gómez-García & Pérez-Rodríguez 2015, Repola 2009) and FIA crown ratings.
352
+ - **Changed:** the pine preset grows from a seed leader: the generator graph lays the seed and the roots, and one shoot
353
+ rule grows the stem and everything on it from Kellomäki & Kurttio's (1991) shoot demography, 30 seasons.
354
+ - **Changed:** in full light every shoot the buds grow is kept: Rb at saturation is the gross yield of the unit a shoot
355
+ grew on. `ShootGrowthConfig.vigour` is removed.
356
+ - **Changed:** shoots turn towards where the light field grows brighter, not away from the crown's centre; zigzag kinks
357
+ a unit off its axis's course instead of turning the course; plagiotropism and straightness turn shoots as they turn
358
+ the graph's spines, the stem excepted.
359
+ - **Changed:** `ShootSystemRule.target` is a generator per mother order and `mean_unit_length` a length per order;
360
+ `shoot_expected_yield(rule, order, length)` takes the unit's order and length.
361
+ - **Removed:** `ShootGrowthConfig.vigour`; `shoot_outward3`.
362
+
363
+ ### Deprecated re-exports are gone
364
+
365
+ - **Breaking:** every re-export kept only so an old name or an old import path would keep working is removed: 209
366
+ modules that did nothing but re-export, and 13 re-exports inside modules that stay. Import each binding from
367
+ where it is defined; the table below lists every one. Nothing behaves differently: each removed name was the same
368
+ object as its replacement, except `fabsf`, which was `Math.abs`.
369
+ - **Untagged ones too:** fifteen of them never carried `@deprecated` but said in a comment that they were kept for an
370
+ old import path (`engine/graphics/FrameThrottle.js`, `shade/renderer/camera/OrbitalCameraController.js`, the
371
+ GTAO constants on `fragment_shader_gtao.js`, …). They went with the rest.
372
+ - **Kept:** aliases that name a default implementation (`seededRandom`, `mix`, `fresnel_Schlick`,
373
+ `string_compute_similarity`, `v3_morton_encode`, `Queue`) and re-exports a module offers by design
374
+ (`DOF2_PRESETS`, `ParticleConstants`' `PARTICLE_VM_INSTRUCTION_WORDS`, `PhysicsSystem`'s `BodyKind` and
375
+ `RigidBodyFlags`) are not compatibility shims and stay. So does `hsv2rgb_float`, a deprecated wrapper with its own
376
+ return shape rather than a re-export, and the deprecated method aliases on classes (`Vector3#readFromArray`,
377
+ `EntityManager#attachDataSet`, …), which are not module exports.
378
+ - **Specs:** the three specs that only pinned alias identity are deleted, and so is `lsb_32.spec.js`, whose cases
379
+ `ctz32.spec.js` already makes. Specs written against an old name are renamed
380
+ to the binding they test, and the cases that only re-ran an existing case under the old name are dropped. The
381
+ colour cases that pinned a behaviour through the old paths now sit in each function's own spec.
382
+
383
+ <details><summary>Every removed re-export and its replacement</summary>
384
+
385
+ | Removed | Import instead |
386
+ |---|---|
387
+ | `aabb_build_frustum` from `src/core/geom/3d/aabb/aabb3_build_frustum.js` | `aabb3_build_frustum` from `src/core/geom/3d/aabb/aabb3_build_frustum.js` |
388
+ | `aabb2_array_combine` from `src/core/geom/2d/aabb/aabb2_array_combine.js` | `aabb2_union_array` from `src/core/geom/2d/aabb/aabb2_union_array.js` |
389
+ | `aabb2_compute_area` from `src/core/geom/2d/aabb/aabb2_compute_area.js` | `aabb2_area` from `src/core/geom/2d/aabb/aabb2_area.js` |
390
+ | `aabb2_compute_center_from_multiple` from `src/core/geom/2d/aabb/aabb2_compute_center_from_multiple.js` | `aabb2_center_from_aabb2_buffer` from `src/core/geom/2d/aabb/aabb2_center_from_aabb2_buffer.js` |
391
+ | `aabb2_compute_overlap` from `src/core/geom/2d/aabb/aabb2_compute_overlap.js` | `aabb2_compute_intersection` from `src/core/geom/2d/aabb/aabb2_compute_intersection.js` |
392
+ | `aabb2_from_v2_array` from `src/core/geom/2d/aabb/aabb2_from_v2_array.js` | `aabb2_from_v2_buffer` from `src/core/geom/2d/aabb/aabb2_from_v2_buffer.js` |
393
+ | `aabb2_intersects_point` from `src/core/geom/2d/aabb/aabb2_intersects_point.js` | `aabb2_contains_point` from `src/core/geom/2d/aabb/aabb2_contains_point.js` |
394
+ | `aabb2_overlap_exists` from `src/core/geom/2d/aabb/aabb2_overlap_exists.js` | `aabb2_intersects_aabb2` from `src/core/geom/2d/aabb/aabb2_intersects_aabb2.js` |
395
+ | `aabb3_array_intersects_frustum_degree` from `src/core/geom/3d/aabb/aabb3_array_intersects_frustum_degree.js` | `aabb3_classify_against_frustum_array` from `src/core/geom/3d/aabb/aabb3_classify_against_frustum_array.js` |
396
+ | `aabb3_box_surface_area_2` from `src/core/geom/3d/aabb/aabb3_box_surface_area_2.js` | `aabb3_half_surface_area` from `src/core/geom/3d/aabb/aabb3_half_surface_area.js` |
397
+ | `aabb3_combined_surface_area` from `src/core/geom/3d/aabb/aabb3_combined_surface_area.js` | `aabb3_union_half_surface_area` from `src/core/geom/3d/aabb/aabb3_union_half_surface_area.js` |
398
+ | `aabb3_compute_half_surface_area` from `src/core/geom/3d/aabb/aabb3_compute_half_surface_area.js` | `aabb3_half_surface_area_extents` from `src/core/geom/3d/aabb/aabb3_half_surface_area_extents.js` |
399
+ | `aabb3_compute_surface_area` from `src/core/geom/3d/aabb/aabb3_compute_surface_area.js` | `aabb3_surface_area` from `src/core/geom/3d/aabb/aabb3_surface_area.js` |
400
+ | `aabb3_detailed_volume_intersection` from `src/core/geom/3d/aabb/aabb3_detailed_volume_intersection.js` | `aabb3_compute_clipping_volume_intersection` from `src/core/geom/3d/aabb/aabb3_compute_clipping_volume_intersection.js` |
401
+ | `aabb3_estimate_projected_area2` from `src/core/geom/3d/aabb/aabb3_estimate_projected_area2.js` | `aabb3_estimate_projected_area` from `src/core/geom/3d/aabb/aabb3_estimate_projected_area.js` |
402
+ | `aabb3_intersects_frustum_degree` from `src/core/geom/3d/aabb/aabb3_intersects_frustum_degree.js` | `aabb3_classify_against_frustum` from `src/core/geom/3d/aabb/aabb3_classify_against_frustum.js` |
403
+ | `aabb3_intersects_line_segment` from `src/core/geom/3d/aabb/aabb3_intersects_line_segment.js` | `aabb3_intersects_segment3` from `src/core/geom/3d/aabb/aabb3_intersects_segment3.js` |
404
+ | `aabb3_near_distance_to_intersection_ray_segment` from `src/core/geom/3d/aabb/aabb3_near_distance_to_intersection_ray_segment.js` | `aabb3_ray_segment_entry_distance` from `src/core/geom/3d/aabb/aabb3_ray_segment_entry_distance.js` |
405
+ | `aabb3_nearest_point_on_surface` from `src/core/geom/3d/aabb/aabb3_nearest_point_on_surface.js` | `aabb3_closest_point_on_surface` from `src/core/geom/3d/aabb/aabb3_closest_point_on_surface.js` |
406
+ | `aabb3_project_to_2d` from `src/core/geom/3d/aabb/aabb3_compute_projected_area.js` | `aabb3_projected_area` from `src/core/geom/3d/aabb/aabb3_projected_area.js` |
407
+ | `aabb3_raycast` from `src/core/geom/3d/aabb/aabb3_raycast.js` | `aabb3_raycast_hit_point` from `src/core/geom/3d/aabb/aabb3_raycast_hit_point.js` |
408
+ | `allocate_m4` from `src/core/geom/3d/mat4/allocate_m4.js` | `m4_allocate` from `src/core/geom/3d/mat4/m4_allocate.js` |
409
+ | `AnimationGraphSystem3` from `src/engine/graphics3/AnimationGraphSystem3.js` | `AnimationGraphSystem` from `src/engine/graphics3/AnimationGraphSystem.js` |
410
+ | `AnimationSystem3` from `src/engine/graphics3/AnimationSystem3.js` | `AnimationSystem` from `src/engine/graphics3/AnimationSystem.js` |
411
+ | `apply_mat4_transform_to_direction_v3_array` from `src/core/geom/3d/mat4/apply_mat4_transform_to_direction_v3_array.js` | `m4_transform_direction_v3_buffer` from `src/core/geom/3d/mat4/m4_transform_direction_v3_buffer.js` |
412
+ | `apply_mat4_transform_to_v3_array` from `src/core/geom/3d/mat4/apply_mat4_transform_to_v3_array.js` | `m4_transform_v3_buffer` from `src/core/geom/3d/mat4/m4_transform_v3_buffer.js` |
413
+ | `bt_face_get_attached_faces` from `src/core/geom/3d/topology/struct/binary/query/bt_face_get_attached_faces.js` | `bt_face_get_vertex_adjacent_faces` from `src/core/geom/3d/topology/struct/binary/query/bt_face_get_vertex_adjacent_faces.js` |
414
+ | `bt_face_get_neighbour_faces` from `src/core/geom/3d/topology/struct/binary/query/bt_face_get_neighbour_faces.js` | `bt_face_get_edge_adjacent_faces` from `src/core/geom/3d/topology/struct/binary/query/bt_face_get_edge_adjacent_faces.js` |
415
+ | `bt_faces_shared_loop` from `src/core/geom/3d/topology/struct/binary/query/bt_faces_shared_loop.js` | `bt_face_get_shared_loop` from `src/core/geom/3d/topology/struct/binary/query/bt_face_get_shared_loop.js` |
416
+ | `bt_kill_only_vert` from `src/core/geom/3d/topology/struct/binary/io/vertex/bt_kill_only_vert.js` | `bt_vertex_kill_only` from `src/core/geom/3d/topology/struct/binary/io/vertex/bt_vertex_kill_only.js` |
417
+ | `bt_merge_verts_by_distance` from `src/core/geom/3d/topology/struct/binary/io/vertex/bt_merge_verts_by_distance.js` | `bt_merge_vertices_by_distance` from `src/core/geom/3d/topology/struct/binary/io/vertex/bt_merge_vertices_by_distance.js` |
418
+ | `bt_mesh_build_surface_triangle_bvh` from `src/core/geom/3d/topology/struct/binary/query/bt_mesh_surface_ray_parity.js` | `bt_mesh_build_face_bvh` from `src/core/geom/3d/topology/struct/binary/query/bt_mesh_build_face_bvh.js` |
419
+ | `bt_mesh_compute_vertex_quadratics` from `src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_vertex_quadratics.js` | `bt_mesh_compute_vertex_quadrics` from `src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_vertex_quadrics.js` |
420
+ | `bt_mesh_count_edge_faces` from `src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.js` | `bt_edge_count_faces` from `src/core/geom/3d/topology/struct/binary/query/bt_edge_count_faces.js` |
421
+ | `bt_mesh_face_poke` from `src/core/geom/3d/topology/struct/binary/io/face/bt_face_poke.js` | `bt_face_poke` from `src/core/geom/3d/topology/struct/binary/io/face/bt_face_poke.js` |
422
+ | `bt_query_edge_is_boundary` from `src/core/geom/3d/topology/struct/binary/query/bt_query_edge_is_boundary.js` | `bt_edge_is_boundary` from `src/core/geom/3d/topology/struct/binary/query/bt_edge_is_boundary.js` |
423
+ | `bt_vert_fuse_duplicate_edges` from `src/core/geom/3d/topology/struct/binary/io/vertex/bt_vert_fuse_duplicate_edges.js` | `bt_vertex_fuse_duplicate_edges` from `src/core/geom/3d/topology/struct/binary/io/vertex/bt_vertex_fuse_duplicate_edges.js` |
424
+ | `bt_vert_kill` from `src/core/geom/3d/topology/struct/binary/io/vertex/bt_vert_kill.js` | `bt_vertex_kill` from `src/core/geom/3d/topology/struct/binary/io/vertex/bt_vertex_kill.js` |
425
+ | `CameraSystem3` from `src/engine/graphics3/CameraSystem3.js` | `CameraSystem` from `src/engine/graphics3/CameraSystem.js` |
426
+ | `chunk_sample_shadowmap_atlas_for_light` from `src/shade/renderer/shadow/map/shader/chunk_shadowmap_get_light_visibility.js` | `chunk_sample_shadowmap_atlas_for_light` from `src/shade/renderer/shadow/map/shader/chunk_sample_shadowmap_atlas_for_light.js` |
427
+ | `chunk_shade_standard_material_direct_heavy` from `src/shade/renderer/deferred/chunk_shade_standard_material_direct_heavy.js` | `chunk_shade_standard_material_direct` from `src/shade/renderer/deferred/chunk_shade_standard_material_direct.js` |
428
+ | `circle_compute_circle_intersection` from `src/core/geom/2d/circle/circle_compute_circle_intersection.js` | `circle_compute_intersection` from `src/core/geom/2d/circle/circle_compute_intersection.js` |
429
+ | `circle_compute_circle_penetration` from `src/core/geom/2d/circle/circle_compute_circle_penetration.js` | `circle_compute_penetration` from `src/core/geom/2d/circle/circle_compute_penetration.js` |
430
+ | `circle_intersects_point` from `src/core/geom/2d/circle/circle_intersects_point.js` | `circle_contains_point` from `src/core/geom/2d/circle/circle_contains_point.js` |
431
+ | `clipping_volume_matrix4_project` from `src/core/geom/3d/frustum/clipping_volume_matrix4_project.js` | `frustum3_matrix4_project_n` from `src/core/geom/3d/frustum/frustum3_matrix4_project_n.js` |
432
+ | `compose_matrix4_array` from `src/core/geom/3d/mat4/compose_matrix4_array.js` | `m4_compose` from `src/core/geom/3d/mat4/m4_compose.js` |
433
+ | `compute_aabb_from_points` from `src/core/geom/3d/aabb/compute_aabb_from_points.js` | `aabb_nd_from_points` from `src/core/geom/3d/aabb/aabb_nd_from_points.js` |
434
+ | `compute_bounding_cone_of_2_cones` from `src/core/geom/3d/cone/compute_bounding_cone_of_2_cones.js` | `cone_compute_bounding_of_2_cones` from `src/core/geom/3d/cone/cone_compute_bounding_of_2_cones.js` |
435
+ | `compute_bounding_sphere_of_2_spheres` from `src/core/geom/3d/sphere/compute_bounding_sphere_of_2_spheres.js` | `sphere_compute_bounding_of_2_spheres` from `src/core/geom/3d/sphere/sphere_compute_bounding_of_2_spheres.js` |
436
+ | `compute_circle_bounding_box` from `src/core/geom/3d/compute_circle_bounding_box.js` | `circle3_compute_bounding_box` from `src/core/geom/3d/circle3_compute_bounding_box.js` |
437
+ | `compute_polygon_area_2d` from `src/core/geom/2d/compute_polygon_area_2d.js` | `polygon2_signed_area` from `src/core/geom/2d/polygon/polygon2_signed_area.js` |
438
+ | `compute_tetrahedron_quality` from `src/core/geom/3d/tetrahedra/compute_tetrahedron_quality.js` | `tetrahedral_mesh_compute_tet_quality` from `src/core/geom/3d/tetrahedra/tetrahedral_mesh_compute_tet_quality.js` |
439
+ | `compute_tetrahedron_volume` from `src/core/geom/3d/tetrahedra/compute_tetrahedron_volume.js` | `tetrahedral_mesh_compute_tet_volume` from `src/core/geom/3d/tetrahedra/tetrahedral_mesh_compute_tet_volume.js` |
440
+ | `compute_triangle_area_3d` from `src/core/geom/3d/triangle/compute_triangle_area_3d.js` | `tri3_area` from `src/core/geom/3d/triangle/tri3_area.js` |
441
+ | `compute_triangle_group_aabb3` from `src/core/geom/3d/aabb/compute_triangle_group_aabb3.js` | `aabb3_from_triangle_group` from `src/core/geom/3d/aabb/aabb3_from_triangle_group.js` |
442
+ | `compute_triangle_group_aabb3` from `src/engine/graphics/sh3/path_tracer/geometry/compute_triangle_group_aabb3.js` | `aabb3_from_triangle_group` from `src/core/geom/3d/aabb/aabb3_from_triangle_group.js` |
443
+ | `compute_triangle_normal` from `src/core/geom/3d/triangle/compute_triangle_normal.js` | `tri3_compute_normal_v3_objects` from `src/core/geom/3d/triangle/tri3_compute_normal_v3_objects.js` |
444
+ | `computeBoundingSphereOfSpheres` from `src/core/geom/packing/computeBoundingSphereOfSpheres.js` | `sphere_compute_bounding_of_spheres` from `src/core/geom/packing/sphere_compute_bounding_of_spheres.js` |
445
+ | `computeConeBoundingBox` from `src/core/geom/3d/cone/computeConeBoundingBox.js` | `cone_compute_bounding_box` from `src/core/geom/3d/cone/cone_compute_bounding_box.js` |
446
+ | `computeConePlaneSide` from `src/core/geom/3d/cone/computeConePlaneSide.js` | `cone_compute_plane_side` from `src/core/geom/3d/cone/cone_compute_plane_side.js` |
447
+ | `computeTriangleClosestPointToPointBarycentric` from `src/core/geom/3d/triangle/computeTriangleClosestPointToPointBarycentric.js` | `tri3_closest_point_barycentric` from `src/core/geom/3d/triangle/tri3_closest_point_barycentric.js` |
448
+ | `computeTriangleRayIntersection` from `src/core/geom/3d/triangle/computeTriangleRayIntersection.js` | `tri3_ray_intersection` from `src/core/geom/3d/triangle/tri3_ray_intersection.js` |
449
+ | `computeTriangleRayIntersectionBarycentric` from `src/core/geom/3d/triangle/computeTriangleRayIntersectionBarycentric.js` | `tri3_ray_intersection_barycentric` from `src/core/geom/3d/triangle/tri3_ray_intersection_barycentric.js` |
450
+ | `computeTriangleRayIntersectionBarycentricEdge` from `src/core/geom/3d/triangle/computeTriangleRayIntersectionBarycentricEdge.js` | `tri3_ray_intersection_barycentric_edge` from `src/core/geom/3d/triangle/tri3_ray_intersection_barycentric_edge.js` |
451
+ | `computeTriangleRayIntersectionBarycentricGeometry` from `src/core/geom/3d/triangle/computeTriangleRayIntersectionBarycentricGeometry.js` | `tri3_ray_intersection_indexed` from `src/core/geom/3d/triangle/tri3_ray_intersection_indexed.js` |
452
+ | `convertPathToURL` from `src/engine/network/convertPathToURL.js` | `convertPathToURL` from `src/core/path/convertPathToURL.js` |
453
+ | `costByBestShortSide` from `src/core/geom/packing/max-rect/cost/costByBestShortSide.js` | `cost_by_best_short_side` from `src/core/geom/packing/max-rect/cost/cost_by_best_short_side.js` |
454
+ | `costByRemainingArea` from `src/core/geom/packing/max-rect/cost/costByRemainingArea.js` | `cost_by_remaining_area` from `src/core/geom/packing/max-rect/cost/cost_by_remaining_area.js` |
455
+ | `crc32` from `src/core/binary/hash/crc32.js` | `crc32` from `src/core/math/hash/crc32.js` |
456
+ | `crc32` from `src/format/image/png/crc32.js` | `crc32` from `src/core/math/hash/crc32.js` |
457
+ | `cutArea` from `src/core/geom/packing/max-rect/cutArea.js` | `cut_area` from `src/core/geom/packing/max-rect/cut_area.js` |
458
+ | `DebugDrawSystem3` from `src/engine/graphics3/DebugDrawSystem3.js` | `DebugDrawSystem` from `src/engine/graphics3/DebugDrawSystem.js` |
459
+ | `DecalSystem3` from `src/engine/graphics3/DecalSystem3.js` | `DecalSystem` from `src/engine/graphics3/DecalSystem.js` |
460
+ | `decompose_matrix_4_array` from `src/core/geom/3d/mat4/decompose_matrix_4_array.js` | `m4_decompose` from `src/core/geom/3d/mat4/m4_decompose.js` |
461
+ | `editor/view/ecs/components/DatGuiController.js` (default) | `src/view/controller/dat/DatGuiController.js` (default) |
462
+ | `encode_unit_to_hemioct` from `src/core/geom/3d/normal/hemioct/encode_unit3_hemioct.js` | `encode_unit_to_hemioct` from `src/core/geom/3d/normal/hemioct/encode_unit_to_hemioct.js` |
463
+ | `eulerAnglesFromMatrix` from `src/core/geom/3d/mat4/eulerAnglesFromMatrix.js` | `m4_euler_angles` from `src/core/geom/3d/mat4/m4_euler_angles.js` |
464
+ | `fabsf` from `src/core/math/fabsf.js` | `Math.abs` |
465
+ | `fill_in_a_cavity` from `src/core/geom/3d/tetrahedra/delaunay/fill_in_a_cavity.js` | `tetrahedral_mesh_fill_cavity` from `src/core/geom/3d/tetrahedra/delaunay/tetrahedral_mesh_fill_cavity.js` |
466
+ | `findBestContainer` from `src/core/geom/packing/max-rect/findBestContainer.js` | `find_best_container` from `src/core/geom/packing/max-rect/find_best_container.js` |
467
+ | `fixed_convex_hull_humus` from `src/core/geom/2d/convex-hull/fixed_convex_hull_humus.js` | `convex_hull_fixed_count_2d_bruteforce` from `src/core/geom/2d/convex-hull/convex_hull_fixed_count_2d_bruteforce.js` |
468
+ | `fixed_convex_hull_relaxation` from `src/core/geom/2d/convex-hull/fixed_convex_hull_relaxation.js` | `convex_hull_fixed_count_2d_relaxation` from `src/core/geom/2d/convex-hull/convex_hull_fixed_count_2d_relaxation.js` |
469
+ | `FogOfWarSystem3` from `src/engine/graphics3/FogOfWarSystem3.js` | `FogOfWarSystem` from `src/engine/graphics3/FogOfWarSystem.js` |
470
+ | `frameThrottle` from `src/engine/graphics/FrameThrottle.js` | `frameThrottle` from `src/core/function/frameThrottle.js` |
471
+ | `frustum_matrix4_project` from `src/core/geom/3d/frustum/frustum_matrix4_project.js` | `frustum3_matrix4_project` from `src/core/geom/3d/frustum/frustum3_matrix4_project.js` |
472
+ | `frustum3_computeNearestPointToPoint` from `src/core/geom/3d/frustum/frustum3_computeNearestPointToPoint.js` | `frustum3_compute_closest_point` from `src/core/geom/3d/frustum/frustum3_compute_closest_point.js` |
473
+ | `GraphicsEngine3` from `src/engine/graphics3/GraphicsEngine3.js` | `GraphicsEngine` from `src/engine/graphics3/GraphicsEngine.js` |
474
+ | `GTAO_DEPTH_MIP_LEVELS, GTAO_DEPTH_MIP_SAMPLING_OFFSET, GTAO_EFFECT_FALLOFF_RANGE, GTAO_EFFECT_RADIUS` from `src/shade/renderer/postprocess/gtao/fragment_shader_gtao.js` | the same names from `src/shade/renderer/postprocess/gtao/GTAO_CONSTANTS.js` |
475
+ | `hex2rgb` from `src/core/color/hex/hex2rgb.js` | `hex_to_rgb` from `src/core/color/hex/hex_to_rgb.js` |
476
+ | `hexahedron_from_aabb` from `src/core/geom/3d/frustum/hexahedron_from_aabb.js` | `frustum3_from_aabb` from `src/core/geom/3d/frustum/frustum3_from_aabb.js` |
477
+ | `HighlightExtension` from `src/engine/graphics3/HighlightSystem.js` | `HighlightExtension` from `src/engine/graphics3/HighlightOutlineSystem.js` |
478
+ | `HighlightSystem` from `src/engine/graphics3/HighlightSystem.js` | `SGMeshHighlightSystem` from `src/engine/graphics/ecs/mesh-v2/aggregate/SGMeshHighlightSystem.js` |
479
+ | `HighlightSystem3` from `src/engine/graphics3/HighlightSystem3.js` | `SGMeshHighlightSystem` from `src/engine/graphics/ecs/mesh-v2/aggregate/SGMeshHighlightSystem.js` |
480
+ | `hpe_to_xyz` from `src/core/color/hunt/hpe_to_xyz.js` | `hpe_to_xyz` from `src/core/color/xyz/hpe_to_xyz.js` |
481
+ | `hsv2rgb` from `src/core/color/hsv/hsv2rgb.js` | `hsv_to_rgb_uint8` from `src/core/color/hsv/hsv_to_rgb_uint8.js` |
482
+ | `int2rgb` from `src/core/color/int2rgb.js` | `int_to_rgb` from `src/core/color/int_to_rgb.js` |
483
+ | `intersect_ray_2d` from `src/core/geom/2d/intersect_ray_2d.js` | `ray2_ray2_compute_intersection_array_2d` from `src/core/geom/2d/line/ray2_ray2_compute_intersection_array_2d.js` |
484
+ | `is_point_within_planes` from `src/core/geom/3d/plane/is_point_within_planes.js` | `plane3_contains_point_within_set` from `src/core/geom/3d/plane/plane3_contains_point_within_set.js` |
485
+ | `isImageBitmap` from `src/engine/graphics/texture/isImageBitmap.js` | `isImageBitmap` from `src/core/dom/isImageBitmap.js` |
486
+ | `LightSystem3` from `src/engine/graphics3/LightSystem3.js` | `LightSystem` from `src/engine/graphics3/LightSystem.js` |
487
+ | `line_segment_compute_line_segment_intersection_2d` from `src/core/geom/2d/line/line_segment_compute_line_segment_intersection_2d.js` | `segment2_segment_compute_intersection_v2_2d` from `src/core/geom/2d/line/segment2_segment_compute_intersection_v2_2d.js` |
488
+ | `line_segment_compute_line_segment_intersection_array_2d` from `src/core/geom/2d/line/line_segment_compute_line_segment_intersection_array_2d.js` | `segment2_segment_compute_intersection_array_2d` from `src/core/geom/2d/line/segment2_segment_compute_intersection_array_2d.js` |
489
+ | `line_segment_compute_line_segment_intersection_vectors_2d` from `src/core/geom/2d/line/line_segment_compute_line_segment_intersection_vectors_2d.js` | `segment2_segment_compute_intersection_v2_objects_2d` from `src/core/geom/2d/line/segment2_segment_compute_intersection_v2_objects_2d.js` |
490
+ | `line_segment_compute_line_segment_intersection_vectors_array_2d` from `src/core/geom/2d/line/line_segment_compute_line_segment_intersection_vectors_array_2d.js` | `segment2_segment_compute_intersection_to_new_array_2d` from `src/core/geom/2d/line/segment2_segment_compute_intersection_to_new_array_2d.js` |
491
+ | `line_segment_intersection_fraction_2d` from `src/core/geom/2d/line/line_segment_intersection_fraction_2d.js` | `line2_segment_intersection_fraction_2d` from `src/core/geom/2d/line/line2_segment_intersection_fraction_2d.js` |
492
+ | `line_segment_line_segment_intersection_exists_2d` from `src/core/geom/2d/line/line_segment_line_segment_intersection_exists_2d.js` | `segment2_intersects_segment2_2d` from `src/core/geom/2d/line/segment2_intersects_segment2_2d.js` |
493
+ | `line2_distance_to_point_sqr` from `src/core/geom/2d/line/line2_distance_to_point_sqr.js` | `line2_compute_segment_point_distance_sqr` from `src/core/geom/2d/line/line2_compute_segment_point_distance_sqr.js` |
494
+ | `line3_closest_points_segment_segment` from `src/core/geom/3d/line/line3_closest_points_segment_segment.js` | `line3_compute_segment_segment_closest_points` from `src/core/geom/3d/line/line3_compute_segment_segment_closest_points.js` |
495
+ | `line3_compute_nearest_point_to_point` from `src/core/geom/3d/line/line3_compute_nearest_point_to_point.js` | `line3_compute_segment_closest_point` from `src/core/geom/3d/line/line3_compute_segment_closest_point.js` |
496
+ | `line3_compute_segment_nearest_point_to_aabb3_t` from `src/core/geom/3d/line/line3_compute_segment_nearest_point_to_aabb3_t.js` | `line3_compute_segment_closest_point_to_aabb3_t` from `src/core/geom/3d/line/line3_compute_segment_closest_point_to_aabb3_t.js` |
497
+ | `line3_compute_segment_nearest_point_to_point_t` from `src/core/geom/3d/line/line3_compute_segment_nearest_point_to_point_t.js` | `line3_compute_segment_closest_point_t` from `src/core/geom/3d/line/line3_compute_segment_closest_point_t.js` |
498
+ | `LOW_MID_HZ, MID_HIGH_HZ` from `src/engine/sound/simulation/render/reverbImpulseResponse.js` | `LOW_MID_HZ, MID_HIGH_HZ` from `src/engine/sound/simulation/render/reverb_band_tails.js` |
499
+ | `lsb_32` from `src/core/binary/lsb_32.js` | `ctz32` from `src/core/binary/ctz32.js` |
500
+ | `m3_cm_from_translation` from `src/core/geom/mat3/m3_cm_from_translation.js` | `m3_cm_make_translation` from `src/core/geom/mat3/m3_cm_make_translation.js` |
501
+ | `m3_cm_invert` from `src/core/geom/mat3/m3_cm_invert.js` | `m3_invert` from `src/core/geom/mat3/m3_invert.js` |
502
+ | `m3_make_rotation_TBN` from `src/core/geom/mat3/m3_make_rotation.js` | `m3_make_rotation_TBN` from `src/core/geom/mat3/m3_make_rotation_TBN.js` |
503
+ | `m3_multiply_vec3` from `src/core/geom/mat3/m3_multiply_vec3.js` | `m3_scale_columns_by_vec3` from `src/core/geom/mat3/m3_scale_columns_by_vec3.js` |
504
+ | `m4_compute_normal_matrix3` from `src/core/geom/3d/mat4/m4_compute_normal_matrix3.js` | `m4_normal_matrix3` from `src/core/geom/3d/mat4/m4_normal_matrix3.js` |
505
+ | `make_justified_point_grid` from `src/core/geom/3d/util/make_justified_point_grid.js` | `aabb3_emit_point_grid` from `src/core/geom/3d/util/aabb3_emit_point_grid.js` |
506
+ | `MeshSystem3` from `src/engine/graphics3/MeshSystem3.js` | `MeshSystem` from `src/engine/graphics3/MeshSystem.js` |
507
+ | `mortonEncode_LUT` from `src/core/geom/3d/morton/mortonEncode_LUT.js` | `v3_morton_encode_lut` from `src/core/geom/3d/morton/v3_morton_encode_lut.js` |
508
+ | `mortonEncode_magicbits` from `src/core/geom/3d/morton/mortonEncode_magicbits.js` | `v3_morton_encode_magicbits` from `src/core/geom/3d/morton/v3_morton_encode_magicbits.js` |
509
+ | `NodeGraphCamera` from `editor/view/node-graph/NodeGraphView.js` | `NodeGraphCamera` from `editor/view/node-graph/NodeGraphCamera.js` |
510
+ | `octahedral_uv_to_direction` from `src/shade/renderer/light/environment/octahedral_uv_to_direction.js` | `octahedral_z_pole_uv_to_direction` from `src/shade/renderer/light/environment/octahedral_z_pole_uv_to_direction.js` |
511
+ | `optional_equality` from `src/core/model/object/optional_equality.js` | `objectsEqual` from `src/core/function/objectsEqual.js` |
512
+ | `OrbitalCameraController` from `src/shade/renderer/camera/OrbitalCameraController.js` | `OrbitalCameraController` from `src/shade/renderer/camera/orbital/OrbitalCameraController.js` |
513
+ | `orientation3_array` from `src/core/geom/2d/convex-hull/orientation3_array.js` | `orient2d_indexed` from `src/core/geom/2d/convex-hull/orient2d_indexed.js` |
514
+ | `packMaxRectangles` from `src/core/geom/packing/max-rect/packMaxRectangles.js` | `pack_max_rectangles` from `src/core/geom/packing/max-rect/pack_max_rectangles.js` |
515
+ | `packOneBox` from `src/core/geom/packing/max-rect/packOneBox.js` | `pack_one_box` from `src/core/geom/packing/max-rect/pack_one_box.js` |
516
+ | `ParticipatingMediaSystem3` from `src/engine/graphics3/ParticipatingMediaSystem3.js` | `ParticipatingMediaSystem` from `src/engine/graphics3/ParticipatingMediaSystem.js` |
517
+ | `PARTICLE_GRAPH_ERROR, PARTICLE_GRAPH_INFO, PARTICLE_GRAPH_WARNING` from `src/shade/renderer/particles/graph/validate_particle_graph.js` | the same names from `src/shade/renderer/particles/graph/particle_graph_diagnostic.js` |
518
+ | `PathDisplaySystem3` from `src/engine/graphics3/PathDisplaySystem3.js` | `PathDisplaySystem` from `src/engine/graphics3/PathDisplaySystem.js` |
519
+ | `PickingSystem3` from `src/engine/graphics3/PickingSystem3.js` | `PickingSystem` from `src/engine/graphics3/PickingSystem.js` |
520
+ | `plane3_compute_convex_3_plane_intersection` from `src/core/geom/3d/plane/plane3_compute_convex_3_plane_intersection.js` | `plane3_compute_3_plane_intersection` from `src/core/geom/3d/plane/plane3_compute_3_plane_intersection.js` |
521
+ | `plane3_compute_plane_intersection` from `src/core/geom/3d/plane/plane3_compute_plane_intersection.js` | `plane3_compute_intersection_objects` from `src/core/geom/3d/plane/plane3_compute_intersection_objects.js` |
522
+ | `plane3_intersect_plane` from `src/core/geom/3d/plane/plane3_intersect_plane.js` | `plane3_compute_intersection` from `src/core/geom/3d/plane/plane3_compute_intersection.js` |
523
+ | `plane3_three_compute_convex_3_plane_intersection` from `src/core/geom/3d/plane/plane3_three_compute_convex_3_plane_intersection.js` | `plane3_compute_3_plane_intersection_from_three` from `src/core/geom/3d/plane/plane3_compute_3_plane_intersection_from_three.js` |
524
+ | `PlaneGeometry` from `src/shade/renderer/geometry/BoxGeometry.js` | `PlaneGeometry` from `src/shade/renderer/geometry/PlaneGeometry.js` |
525
+ | `Preloader` from `src/engine/asset/preloader/AssetPreloader.js` | `AssetPreloader` from `src/engine/asset/preloader/AssetPreloader.js` |
526
+ | `qt_query_data_nearest_to_point` from `src/core/geom/2d/quad-tree/qt_query_data_nearest_to_point.js` | `qt_query_closest_to_point` from `src/core/geom/2d/quad-tree/qt_query_closest_to_point.js` |
527
+ | `quat_decode_from_uint32` from `src/core/geom/3d/quaternion/quat_decode_from_uint32.js` | `quat3_decode_from_uint32` from `src/core/geom/3d/quaternion/quat3_decode_from_uint32.js` |
528
+ | `quat_encode_to_uint32` from `src/core/geom/3d/quaternion/quat_encode_to_uint32.js` | `quat3_encode_to_uint32` from `src/core/geom/3d/quaternion/quat3_encode_to_uint32.js` |
529
+ | `quat3_createFromAxisAngle` from `src/core/geom/3d/quaternion/quat3_createFromAxisAngle.js` | `quat3_from_axis_angle` from `src/core/geom/3d/quaternion/quat3_from_axis_angle.js` |
530
+ | `randomPointInBox` from `src/core/geom/random/randomPointInBox.js` | `random_point_in_box` from `src/core/geom/random/random_point_in_box.js` |
531
+ | `randomPointInCircle` from `src/core/geom/random/randomPointInCircle.js` | `random_point_in_circle` from `src/core/geom/random/random_point_in_circle.js` |
532
+ | `randomPointInPoint` from `src/core/geom/random/randomPointInPoint.js` | `random_point_in_point` from `src/core/geom/random/random_point_in_point.js` |
533
+ | `randomPointInSphere` from `src/core/geom/random/randomPointInSphere.js` | `random_point_in_sphere` from `src/core/geom/random/random_point_in_sphere.js` |
534
+ | `randomPointOnBox` from `src/core/geom/random/randomPointOnBox.js` | `random_point_on_box` from `src/core/geom/random/random_point_on_box.js` |
535
+ | `randomPointOnSphere` from `src/core/geom/random/randomPointOnSphere.js` | `random_point_on_sphere` from `src/core/geom/random/random_point_on_sphere.js` |
536
+ | `ray3_array_compose` from `src/core/geom/3d/ray/ray3_array_compose.js` | `ray3_compose_array` from `src/core/geom/3d/ray/ray3_compose_array.js` |
537
+ | `ray3_compute_nearest_point_to_point` from `src/core/geom/3d/ray/ray3_compute_nearest_point_to_point.js` | `ray3_compute_closest_point` from `src/core/geom/3d/ray/ray3_compute_closest_point.js` |
538
+ | `ray3_distance_to_point` from `src/core/geom/3d/ray/ray3_distance_to_point.js` | `ray3_compute_point_projection_t` from `src/core/geom/3d/ray/ray3_compute_point_projection_t.js` |
539
+ | `ray3_shift_origin_along_direction` from `src/core/geom/3d/ray/ray3_shift_origin_along_direction.js` | `ray3_shift_origin_along_direction_array` from `src/core/geom/3d/ray/ray3_shift_origin_along_direction_array.js` |
540
+ | `rayTriangleIntersection` from `src/core/geom/3d/triangle/rayTriangleIntersection.js` | `tri3_ray_intersection_v3_objects` from `src/core/geom/3d/triangle/tri3_ray_intersection_v3_objects.js` |
541
+ | `read_three_planes_to_array` from `src/core/geom/3d/frustum/read_three_planes_to_array.js` | `frustum3_planes_to_flat_from_three` from `src/core/geom/3d/frustum/frustum3_planes_to_flat_from_three.js` |
542
+ | `rectangle_to_aabb` from `src/core/geom/2d/rectangle_to_aabb.js` | `aabb2_from_rectangle` from `src/core/geom/2d/aabb/aabb2_from_rectangle.js` |
543
+ | `removeRedundantBoxes` from `src/core/geom/packing/max-rect/removeRedundantBoxes.js` | `remove_redundant_boxes` from `src/core/geom/packing/max-rect/remove_redundant_boxes.js` |
544
+ | `removeRedundantBoxesArray` from `src/core/geom/packing/max-rect/removeRedundantBoxesArray.js` | `remove_redundant_boxes_from_array` from `src/core/geom/packing/max-rect/remove_redundant_boxes_from_array.js` |
545
+ | `rgb2hex` from `src/core/color/hex/rgb2hex.js` | `rgb_to_hex` from `src/core/color/hex/rgb_to_hex.js` |
546
+ | `rgb2hsv` from `src/core/color/hsv/rgb2hsv.js` | `rgb_to_hsv` from `src/core/color/hsv/rgb_to_hsv.js` |
547
+ | `rgb2uint24` from `src/core/color/rgb2uint24.js` | `rgb_to_uint24` from `src/core/color/rgb_to_uint24.js` |
548
+ | `rgb2uint32` from `src/core/color/rgb2uint32.js` | `rgb_to_uint32` from `src/core/color/rgb_to_uint32.js` |
549
+ | `sh_compute_K` from `src/core/geom/3d/sphere/harmonics/sh_compute_K.js` | `sh_normalization_factor` from `src/core/geom/3d/sphere/harmonics/sh_normalization_factor.js` |
550
+ | `sh3_rotate_bl` from `src/core/geom/3d/sphere/harmonics/sh3_rotate_bl.js` | `sh3_rotate` from `src/core/geom/3d/sphere/harmonics/sh3_rotate.js` |
551
+ | `sh3_sample_irradiance_by_direction` from `src/shade/renderer/global_illumination/lpv/lpv_visualise_probes.js` | `sh3_sample_irradiance_by_direction` from `src/core/geom/3d/sphere/harmonics/sh3_sample_irradiance_by_direction.js` |
552
+ | `ShadedGeometrySystem3` from `src/engine/graphics3/ShadedGeometrySystem3.js` | `ShadedGeometrySystem` from `src/engine/graphics3/ShadedGeometrySystem.js` |
553
+ | `shader_cull_lights_frustum` from `src/shade/renderer/light/cluster/cull/shader_cull_lights_frustum.js` | `shader_cull_point_lights_frustum` from `src/shade/renderer/light/cluster/cull/shader_cull_lights_frustum.js` |
554
+ | `shape_mesh_from_geometry` from `src/core/geom/3d/shape/shape_mesh_from_geometry.js` | `shape3_mesh_from_geometry` from `src/core/geom/3d/shape/shape3_mesh_from_geometry.js` |
555
+ | `shape3d_voxelize_to_grid` from `src/core/geom/3d/shape/util/shape3d_voxelize_to_grid.js` | `shape3_voxelize_to_grid` from `src/core/geom/3d/shape/util/shape3_voxelize_to_grid.js` |
556
+ | `slice_frustum_linear_to_points` from `src/core/geom/3d/frustum/slice_frustum_linear_to_points.js` | `frustum3_slice_linear_to_points` from `src/core/geom/3d/frustum/frustum3_slice_linear_to_points.js` |
557
+ | `sphere_array_intersects_point` from `src/core/geom/3d/sphere/sphere_array_intersects_point.js` | `sphere_packed_contains_point` from `src/core/geom/3d/sphere/sphere_packed_contains_point.js` |
558
+ | `sphere_array_intersects_ray` from `src/core/geom/3d/sphere/sphere_array_intersects_ray.js` | `sphere_packed_intersects_ray` from `src/core/geom/3d/sphere/sphere_packed_intersects_ray.js` |
559
+ | `sphere_intersects_point` from `src/core/geom/3d/sphere/sphere_intersects_point.js` | `sphere_contains_point` from `src/core/geom/3d/sphere/sphere_contains_point.js` |
560
+ | `sphere_matrix4_project` from `src/core/geom/3d/sphere/sphere_matrix4_transform.js` | `sphere_matrix4_transform` from `src/core/geom/3d/sphere/sphere_matrix4_transform.js` |
561
+ | `sphere_projected_sphere_radius_sqr` from `src/core/geom/3d/sphere/sphere_projected_sphere_radius_sqr.js` | `sphere_projected_radius_sqr` from `src/core/geom/3d/sphere/sphere_projected_radius_sqr.js` |
562
+ | `sphere_radius_sqr_from_v3_array_transformed` from `src/core/geom/3d/sphere/sphere_radius_sqr_from_v3_array_transformed.js` | `sphere_radius_sqr_from_v3_buffer_transformed` from `src/core/geom/3d/sphere/sphere_radius_sqr_from_v3_buffer_transformed.js` |
563
+ | `spherical_from_cartesian` from `src/core/geom/3d/sphere/spherical_from_cartesian.js` | `sphere_spherical_from_cartesian` from `src/core/geom/3d/sphere/sphere_spherical_from_cartesian.js` |
564
+ | `spline_bezier2_2d` from `src/core/geom/2d/spline_bezier2_2d.js` | `spline2_quadratic_bezier` from `src/core/geom/2d/spline2_quadratic_bezier.js` |
565
+ | `SplitView` from `editor/view/v2/SplitView.js` | `SplitView` from `src/view/layout/SplitView.js` |
566
+ | `src/core/binary/hash/xxh3_common.js` (every export) | `src/core/math/hash/xxh3_common.js` |
567
+ | `string_compute_byte_size` from `src/core/primitives/strings/string_compute_byte_size.js` | `utf8_encoded_length` from `src/core/binary/utf8/utf8_encoded_length.js` |
568
+ | `TerrainSystem3` from `src/engine/graphics3/TerrainSystem3.js` | `TerrainSystem` from `src/engine/graphics3/TerrainSystem.js` |
569
+ | `tetrahedral_mesh_tet_get_neighbours` from `src/core/geom/3d/tetrahedra/tetrahedral_mesh_tet_get_neighbours.js` | `tetrahedral_mesh_get_neighbours` from `src/core/geom/3d/tetrahedra/tetrahedral_mesh_get_neighbours.js` |
570
+ | `TooltipComponentSystem3` from `src/engine/graphics3/TooltipComponentSystem3.js` | `TooltipComponentSystem` from `src/engine/graphics3/TooltipComponentSystem.js` |
571
+ | `Trail3DSystem3` from `src/engine/graphics3/Trail3DSystem3.js` | `Trail3DSystem` from `src/engine/graphics3/Trail3DSystem.js` |
572
+ | `triangle_compute_plane_side` from `src/core/geom/3d/triangle/triangle_compute_plane_side.js` | `tri3_compute_plane_side` from `src/core/geom/3d/triangle/tri3_compute_plane_side.js` |
573
+ | `triangle_compute_signed_volume` from `src/core/geom/3d/triangle/triangle_compute_signed_volume.js` | `tri3_compute_signed_volume` from `src/core/geom/3d/triangle/tri3_compute_signed_volume.js` |
574
+ | `triangle_intersects_clipping_volume` from `src/core/geom/3d/triangle/triangle_intersects_clipping_volume.js` | `tri3_intersects_clipping_volume` from `src/core/geom/3d/triangle/tri3_intersects_clipping_volume.js` |
575
+ | `triangle_mesh_compute_signed_volume` from `src/core/geom/3d/triangle/triangle_mesh_compute_signed_volume.js` | `tri3_mesh_compute_signed_volume` from `src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.js` |
576
+ | `triangle2d_compute_area` from `src/core/geom/2d/triangle2d_compute_area.js` | `tri2_signed_area` from `src/core/geom/2d/triangle/tri2_signed_area.js` |
577
+ | `triangle2d_contains_point` from `src/core/geom/2d/triangle2d_contains_point.js` | `tri2_contains_point` from `src/core/geom/2d/triangle/tri2_contains_point.js` |
578
+ | `triangle2d_get_barycentric` from `src/core/geom/2d/triangle2d_get_barycentric.js` | `tri2_compute_barycentric` from `src/core/geom/2d/triangle/tri2_compute_barycentric.js` |
579
+ | `triangle3_incenter` from `src/core/geom/3d/triangle/triangle3_incenter.js` | `tri3_incenter` from `src/core/geom/3d/triangle/tri3_incenter.js` |
580
+ | `v3_angle_atan2_between` from `src/core/geom/vec3/v3_angle_atan2_between.js` | `v3_angle_stable` from `src/core/geom/vec3/v3_angle_stable.js` |
581
+ | `v3_angle_between` from `src/core/geom/vec3/v3_angle_between.js` | `v3_angle` from `src/core/geom/vec3/v3_angle.js` |
582
+ | `v3_angle_cos_between` from `src/core/geom/vec3/v3_angle_cos_between.js` | `v3_cos_angle` from `src/core/geom/vec3/v3_cos_angle.js` |
583
+ | `v3_array_add_in_place` from `src/core/geom/vec3/v3_array_add_in_place.js` | `v3_add` from `src/core/geom/vec3/v3_add.js` |
584
+ | `v3_array_centroid` from `src/core/geom/vec3/v3_array_centroid.js` | `v3_centroid_v3_buffer` from `src/core/geom/vec3/v3_centroid_v3_buffer.js` |
585
+ | `v3_array_copy` from `src/core/geom/vec3/v3_array_copy.js` | `v3_copy_array` from `src/core/geom/vec3/v3_copy_array.js` |
586
+ | `v3_array_displace_in_direction` from `src/core/geom/vec3/v3_array_displace_in_direction.js` | `v3_displace_in_direction_to_array` from `src/core/geom/vec3/v3_displace_in_direction_to_array.js` |
587
+ | `v3_array_equals` from `src/core/geom/vec3/v3_array_equals.js` | `v3_equals_array` from `src/core/geom/vec3/v3_equals_array.js` |
588
+ | `v3_array_immediate_add` from `src/core/geom/vec3/v3_array_immediate_add.js` | `v3_add_array` from `src/core/geom/vec3/v3_add_array.js` |
589
+ | `v3_array_matrix4_rotate` from `src/core/geom/vec3/v3_array_matrix4_rotate.js` | `v3_matrix4_rotate_array` from `src/core/geom/vec3/v3_matrix4_rotate_array.js` |
590
+ | `v3_array_normalize` from `src/core/geom/vec3/v3_array_normalize.js` | `v3_normalize_array` from `src/core/geom/vec3/v3_normalize_array.js` |
591
+ | `v3_array_scale` from `src/core/geom/vec3/v3_array_scale.js` | `v3_scale_array` from `src/core/geom/vec3/v3_scale_array.js` |
592
+ | `v3_compute_interior_angle` from `src/core/geom/vec3/v3_compute_interior_angle.js` | `v3_interior_angle` from `src/core/geom/vec3/v3_interior_angle.js` |
593
+ | `v3_compute_triangle_normal` from `src/core/geom/3d/triangle/v3_compute_triangle_normal.js` | `tri3_compute_normal` from `src/core/geom/3d/triangle/tri3_compute_normal.js` |
594
+ | `v3_dot_array_array` from `src/core/geom/vec3/v3_dot_array_array.js` | `v3_dot_array` from `src/core/geom/vec3/v3_dot_array.js` |
595
+ | `v3_matrix4_rotate` from `src/core/geom/vec3/v3_matrix4_rotate.js` | `v3_matrix4_rotate_unit` from `src/core/geom/vec3/v3_matrix4_rotate_unit.js` |
596
+ | `v3_multiply_scalar` from `src/core/geom/vec3/v3_multiply_scalar.js` | `v3_scale` from `src/core/geom/vec3/v3_scale.js` |
597
+ | `v3_quat3_apply` from `src/core/geom/vec3/v3_quat3_apply.js` | `v3_quaternion_apply` from `src/core/geom/vec3/v3_quaternion_apply.js` |
598
+ | `v3_quat3_apply_inverse` from `src/core/geom/vec3/v3_quat3_apply_inverse.js` | `v3_quaternion_apply_inverse` from `src/core/geom/vec3/v3_quaternion_apply_inverse.js` |
599
+ | `v3_triple_cross_product` from `src/core/geom/vec3/v3_triple_cross_product.js` | `v3_triple_cross` from `src/core/geom/vec3/v3_triple_cross.js` |
600
+ | `v4_multiply_mat4` from `src/core/geom/vec4/v4_multiply_mat4.js` | `v4_matrix4_multiply` from `src/core/geom/vec4/v4_matrix4_multiply.js` |
601
+ | `vector_copy` from `src/core/geom/vec/vector_copy.js` | `vector_copy_offset` from `src/core/geom/vec/vector_copy_offset.js` |
602
+ | `vector_scale_array` from `src/core/geom/vec/vector_scale_array.js` | `vector_scale_offset` from `src/core/geom/vec/vector_scale_offset.js` |
603
+ | `VolumetricLightMapSystem3` from `src/engine/graphics3/VolumetricLightMapSystem3.js` | `VolumetricLightMapSystem` from `src/engine/graphics3/VolumetricLightMapSystem.js` |
604
+ | `WaterSystem3` from `src/engine/graphics3/WaterSystem3.js` | `WaterSystem` from `src/engine/graphics3/WaterSystem.js` |
605
+ | `XXH128Hash` from `src/core/binary/hash/XXH128Hash.js` | `XXH128Hash` from `src/core/math/hash/XXH128Hash.js` |
606
+ | `xxh3_128` from `src/core/binary/hash/xxh3_128.js` | `xxh3_128` from `src/core/math/hash/xxh3_128.js` |
607
+ | `xxh3_64` from `src/core/binary/hash/xxh3_64.js` | `xxh3_64` from `src/core/math/hash/xxh3_64.js` |
608
+ | `XXH3_SECRET` from `src/core/binary/hash/XXH3_SECRET.js` | `XXH3_SECRET` from `src/core/math/hash/XXH3_SECRET.js` |
609
+ | `xyz_to_hpe` from `src/core/color/hunt/xyz_to_hpe.js` | `xyz_to_hpe` from `src/core/color/xyz/xyz_to_hpe.js` |
610
+
611
+ </details>
612
+
613
+ ### Every tile draws the species' own leaf, and a card is one leaf
614
+
615
+ - **Added:** `LeafArchitecture` (`FoliageMaterialConfig.leaf`), a species' leaf as floras describe it, in the terms
616
+ of Ellis et al. (2009), *Manual of Leaf Architecture*. It has `LeafApexShape`, `LeafBaseShape` and
617
+ `LeafLobation`, and covers:
618
+ - blade size, length/width ratio and where the blade is widest;
619
+ - apex and base angles and shapes;
620
+ - pinnate or palmate lobes and sinus depth;
621
+ - teeth per centimetre, their depth and order;
622
+ - secondary veins, petiole, and a compound leaf's leaflets and rachis.
623
+ Oak, birch, willow, maple and hazel cite their floras (Flora of North America, Flora of China, BSBI, Critchfield
624
+ 1971), and the oak's outline is fitted to Boratyński et al. (2008). See `LEAF_ARCHITECTURE_BRIEF_2026_10_26.md`.
625
+ - **Added:** `leaf_geometry`, `needle_shoot_geometry`, `foliage_tile_layout` and `foliage_tile_raster`, which draw a
626
+ tile from the architecture. Also `polygon2_rasterize_coverage` (core): scanline coverage of contours by the nonzero
627
+ rule.
628
+ - **Added:** `FoliageTileContent`. A tile draws one leaf, or a stretch of needle shoot with the needles the shoot bears
629
+ there, shared among the card's planes.
630
+ - **Changed:** a broadleaf card is one leaf at its own node on its shoot, at the leaf's true size, leaving the shoot
631
+ at the rule's insertion angle. Short shoots' leaves sit at their tip (`FoliageConfig.leaves_per_short_shoot`). The
632
+ broadleaf presets draw `CutoutHull` cards.
633
+ - **Changed:** card size, anchor and atlas resolution come from the tile layout. The resolution draws the leaf's
634
+ finest feature four texels across and its stems two.
635
+ - **Changed:** `make_foliage_material(species, out_cutout)` and `foliage_atlas_cutout(species, out)` take the species.
636
+ `foliage_unit_card_spans` takes the leaves a card draws. `foliage_cards_per_metre`, `foliage_card_leaf_area` and
637
+ `foliage_leaf_area_per_metre` take the tile layout or a reach.
638
+ - **Removed:**
639
+ - `FoliageMaterialConfig.leaf_shape`, `leaflets_per_cluster`, `leaf_arrangement` (the rosette),
640
+ `short_shoot_leaflets`, `resolution`, `stem_anchor_u` and `stem_anchor_v`;
641
+ - `FoliageConfig.card_width` and `card_length`;
642
+ - `foliage_leaf_silhouette.js` and `foliage_atlas_tiles.js`;
643
+ - `foliage_card_leaf_count`.
644
+
645
+ ### Every woody species grows its twigs, and its crown fills by light
646
+
647
+ - **Added:** `ShootGrowthConfig` (`TreeSpeciesConfig.shoot_growth`): leader growth, and the net shoots a shoot leaves
648
+ a year as a function of its light, from 1 at the compensation light to `vigour` at saturation. Spruce, pine and
649
+ birch are sourced; oak, maple, willow and the shrub borrow the curve's shape. The shrub's full-sun vigour is its
650
+ gross yield from Grisafi et al. (2024), so only shade sheds its shoots.
651
+ - **Added:** `ShootBudZone`, per-order bud zones of a `ShootSystemRule` (long, sylleptic and short shoots). Pine,
652
+ spruce, birch, maple, willow and the shrub get shoot rules from their sources (see
653
+ `SHOOT_SYSTEMS_ALL_SPECIES_BRIEF_2026_10_26.md`).
654
+ - **Changed:** `tree_skeleton_grow_shoots` grows every carrier's twigs in one season loop under a voxel light field
655
+ (Beer-Lambert from a standard overcast sky). Shoots are shed to their light's net yield, the wood thickens each
656
+ season with what it carries, and a shoot the wood grows into dies (`CROWN_FILLING_BRIEF_2026_10_26.md`).
657
+ - **Changed:** a unit carries as many cards as its leaves fill, not one scaled card. Short shoots draw rosette tiles
658
+ of their own.
659
+ - **Added:** `TreeSkeleton.branch_unit_offset`, `branch_unit_count`, `unit_start_t` and `branch_unit_start_t`: every
660
+ annual unit of a branch grown by them.
661
+ - **Removed:** `TreeSkeleton.branch_current_growth_t` (use `branch_unit_start_t(b, 0)`); `ShootSystemRule.years`,
662
+ `vigour` and `carrier_unit_length` (the light and the leader growth set them); `shoot_demography_grow`;
663
+ `foliage_current_shoot_card_scale`.
664
+
665
+ ### Wind seats keep the joints that move them alike
666
+
667
+ - **Changed:** where more than four skinning joints meet at a branch seat or card, the four kept are those that leave
668
+ the point closest to where all of them would carry it in the reference wind, and a dropped joint folds into the
669
+ kept one that carries the point most alike: each joint between two turns the point by its rotation times its lever.
670
+ `interpolate_ring_skin_weights4` takes the joints' motion (`tree_skeleton_read_joint_motion`) and the point, in
671
+ place of `joint_parents`.
672
+ - **Added:** `TreeSkeleton.joint_count`, `joint_vertex`, `joint_rotation` and `set_joints`, written by
673
+ `tree_skeleton_assign_skin_joints`: where each joint pivots and how far it turns in the reference wind.
674
+
675
+ ### Annual shoots: the oak's leaves sit on this year's twigs
676
+
677
+ - **Added:** `TreeSpeciesConfig.shoot_rules` (`ShootSystemRule`). It grows the twig system on a carrier
678
+ generator year by year (`tree_skeleton_grow_shoots`), from the oak shoot demography of Buck-Sorlin & Bell
679
+ (Forestry 73(1), 2000; Bot. J. Linn. Soc. 127, 1998):
680
+ - Each living axis extends its terminal bud every year.
681
+ - Last year's unit bursts its 4 subapical buds (29% each) and its median buds (4.5% each).
682
+ - Unit lengths are drawn from their distributions, shorter with branching order.
683
+ - Each year keeps `vigour / 2.65` of its new shoots, highest-order and shortest first. `vigour` is the knob for
684
+ how full the periphery grows (1 holds it steady).
685
+ - Each axis is one branch with one segment per year. Twig radii follow the paper's Table 7, by length and age. No
686
+ twig grows through framework wood.
687
+ See `ANNUAL_SHOOTS_BRIEF_2026_10_25.md`.
688
+ - **Added:** `TreeSkeleton.branch_current_growth_t`, where a branch's current-year unit starts (-1 if not recorded).
689
+ A carrier that records it carries one card on that unit, scaled to its length, in place of the density rule.
690
+ Each card draws its own maturity from its branch.
691
+ - **Added:** `FoliageMaterialConfig.leaf_arrangement`. The new `"rosette"` crowds a shoot's leaves at its tip, as
692
+ an oak's are. `"rachis"` is the existing arrangement and the default.
693
+ - **Changed:** the oak preset grows shoots on generators 1, 2 and 4, with 0.17 m `TriStarCluster` cards where its
694
+ cards were a median 0.90 m. Its secondary branches are 1.4x longer, 12 to a bough where there were 8. At seed 1337 it has 2,261
695
+ cards (was 842), 5,703 wind joints (447) and 9,860 skeleton nodes (766), with LAI 3.2 over its crown's top
696
+ silhouette (was 4.0). It is more see-through: side opacity 0.49 against 0.64. Generation takes 441 ms against 291 ms.
697
+ - **Changed:** each segment bends under its own drag and weight, acting at L / 6, as well as under what it carries. A
698
+ one-unit twig had zero compliance before. `TreeWindConfig.elastic_modulus` is refitted from 3.33e7 to 4.55e7, so
699
+ the oak's median primary bough keeps its sway (ratio 0.999 over 12 seeds). Maple boughs move 1.12x, pine 1.14x and
700
+ birch 1.04x.
701
+ - **Changed:** `TreeWindConfig.max_joints` defaults to 16,384, a chosen number above every preset's skeleton node
702
+ count, the oak's included.
703
+ - **Changed:** `tree_skeleton_compute_branch_frequency` and `tree_skeleton_compute_segment_compliance` are exported.
704
+
705
+ ### Leaf cards count in how far a bend reaches
706
+
707
+ - **Fixed:** `TreeSegmentMechanics.reach` counted only the skeleton a segment carries, though a leaf card hangs up to a
708
+ metre past its anchor (oak: median card 0.90 m). A leafy segment now reaches half its length plus its card, and a
709
+ segment carrying a leafy one adds that card to the distance, so joint placement sees how far twig rotation moves the
710
+ leaves. Counts at 1 cm barely move: oak 447 -> 447, pine 517 -> 525, fern 26 -> 26.
711
+
712
+ ### Wind joints sway in their branch's mode
713
+
714
+ - **Fixed:** with the derived joint count the oak's leaves shook. Each joint's natural frequency was the
715
+ single-degree-of-freedom estimate of its own span, so cutting a branch into short spans made every piece stiffer:
716
+ the same 12 bough hinges rang at 5.0 rad/s with 32 joints, 10.4 at 1 cm and 12.7 with a joint on every node, and
717
+ the oak's twig joints at a median 4.4 Hz (max 19 Hz), each fluttering at its own rate.
718
+ - **Now:** every joint on a branch takes the branch's first-mode frequency, the same estimate taken for the whole
719
+ branch about its root with everything it carries (`tree_skeleton_compute_branch_frequency`). Joints already
720
+ shared their branch's phase, so a branch now sways as one piece however many joints the rig cuts it into.
721
+ - **Oak at 1 cm:** trunk 0.27 Hz, boughs a median 0.89 Hz (p90 1.5), twigs 1.7 Hz (p90 2.6, max 6.2). The bough
722
+ hinges are back at 5.0 rad/s at any joint count.
723
+ - **Playground.** Stepped at 1/240 s in the default wind, with every joint's deflection read back from the GPU,
724
+ the oak's joints swayed at a median 3.2–3.8 Hz (p90 5.9–6.5, max 12.5–18.9) before and sway at 1.3–1.5 Hz
725
+ (p90 2.4–2.7, max 4.6–9.9) now, at the same amplitude (median 0.0034–0.0040 rad), over two runs each.
726
+ - **Breaking:** `tree_skeleton_compute_joint_mechanics` no longer takes `joint_parents`.
727
+ - **Specs:** a uniform stem's frequency scales as r / L^2, as an Euler-Bernoulli cantilever's; a stem's joints all
728
+ take its frequency; on the oak every joint on a branch shares one frequency and phase, and a branch's frequency is
729
+ the same at 32 joints and with a joint on every node. The last two failed against the per-span frequencies. A
730
+ mutant that leaves a branch's laterals out of what it carries survives them.
731
+
732
+ ### Wind joints: the tree decides how many, `max_joints` is a safety net
733
+
734
+ - **Changed:** `tree_skeleton_assign_skin_joints` derives the number of wind joints from the tree instead of filling a
735
+ budget of 32. A joint goes wherever the rig would otherwise hide visible sway in the 6.5 m/s wind the wood is fitted
736
+ to (`TREE_WIND_REFERENCE_SPEED`).
737
+ - **Branch without a joint.** Its bending is lost, so it gets a joint when that bending would move what it carries
738
+ by more than the tolerance.
739
+ - **Within a joint's span.** The joint rotates the whole span about one pivot, so a new joint starts where the error
740
+ of doing that would pass the tolerance.
741
+ - **Tolerance.** `TreeWindConfig.deflection_tolerance`, a distance in metres (default 0.01, a chosen number). A tall
742
+ tree's bends move more, so it takes more joints than a small one. Bends are taken as the rig shows them, saturated
743
+ at its bend limit (`tree_wind_bend_limit`, now shared with `build_tree_wind_rig`).
744
+ - **Counts (seed 1337).** Oak 447, pine 517, birch 185, willow 514, maple 311, spruce 416, palm 65, shrub 46 and
745
+ fern 26, up from 32 each. With 10x stiffer wood the oak takes 187, and with 100x it takes 1, which leaves no joint
746
+ that moves. Dead stubs and other wood that barely bends get none.
747
+ - **New:** `TreeWindConfig.deflection_tolerance` (m). The oak takes 234 joints at 0.04 and 609 at 0.0025, and
748
+ `Infinity` gives no wind joints.
749
+ - **Changed:** `TreeWindConfig.max_joints` defaults to 4,096 and only caps the derived count. Past it, the joints that
750
+ keep the least sway are dropped. 4,096 is a chosen number above every preset's skeleton node count. Values above
751
+ 65,536 are clamped, since joint indices are 16-bit.
752
+ - **New:** `TreeSegmentMechanics.reach` (distance to the farthest element a segment carries) and
753
+ `tree_skeleton_compute_segment_compliance`.
754
+ - **Changed:** where a child branch's base ring or a leaf card sits on a frustum whose two rings hold more than 4
755
+ joints between them, `interpolate_ring_skin_weights4` now gives each joint past the fourth to the kept joint
756
+ nearest it in the joint tree, instead of spreading its weight over all four. It takes the joint tree as a new
757
+ argument (`tree_skeleton_read_joint_parents`).
758
+ - **Measured.** In a steady 6.5 m/s pose, the widest seam on the bark spec's five presets went from 1.05 to 0.74 mm,
759
+ and on the oak's leaf cards from 0.81 to 0.39 mm.
760
+ - **Why it happens.** Skinning has 4 weights per vertex, so no layout can seat every branch exactly at every depth.
761
+ This happened at 32 joints too (the fern dropped up to 14% of a weight at 4 seats); the derived count makes it
762
+ more common (spruce at 73 of 1,341 seats).
763
+ - A branch's joint compliances still sum to its own bend however many joints it gets, so `elastic_modulus` keeps its
764
+ fit.
765
+ - **Specs:** the bark and foliage seam specs bound the gap in metres, by `deflection_tolerance`, in a steady
766
+ `TREE_WIND_REFERENCE_SPEED` pose, instead of asking for exactly matching weights. Skinning has 4 weights per vertex,
767
+ and a branch base or leaf card seated where more than 4 joints meet cannot carry them all; the 32-joint budget hid
768
+ that on all but the fern.
769
+
770
+ ### Seasons: leaves fall and wilt with senescence, and card tints can brighten
771
+
772
+ - **New:** leaf drop. With `FoliageMaterialConfig.senescence`, `foliage_collect_attachments` sheds each card once
773
+ senescence passes its own threshold (`foliage_abscission_threshold`).
774
+ - **First leaves.** A leaf waits for its own atlas tile. The pigment model ages each tile at its own pace
775
+ (`foliage_variant_senescence`, now shared with `make_foliage_material`), and `foliage_variant_abscission_onset`
776
+ solves for the point where a tile's exposed lamina has lost its chlorophyll. That point runs from 0.66 for mature
777
+ tiles to 0.82 for mid-maturity ones.
778
+ - **Order.** After its tile's onset, outer and apical leaves go first; the threshold is ordered by sun exposure and
779
+ position along the shoot.
780
+ - **End state.** The crown is bare at 1. On maple (seed 5), 33% of cards are left at 0.9 and none at 0.97.
781
+ - **New:** wilt. Each attached card carries `dryness`, which rises from its tile's onset to its own fall. A drying
782
+ card goes towards a leaf with no turgor: it rolls shut (fold towards 90°), its arch flattens, and its tip hangs
783
+ (`camber_droop` towards 1).
784
+ - **Card types.** Only the folding cards wilt: `FoldedVCard`, `RibbedFrond` and `CutoutHull`. `CrossCard` and
785
+ `TriStarCluster` cards have no fold to close.
786
+ - **About to fall.** A card falls before it reaches the limit. One about to fall is close to shut, a sliver with
787
+ near-zero area. That is measured and valid: no non-finite positions, normals or tangents.
788
+ - **Changed:** the wind rig sees leaf drop. The joint mechanics drop the sail of shed leaves, judging the expected leaf
789
+ at each segment by the same rule, so maple's summed joint compliance falls by more than 20% when it is bare.
790
+ - **Fixed:** card tints were clamped to 1 before the `unorm8` vertex colours, so they could only darken. About half
791
+ of all tint channels sat above 1 and were clipped.
792
+ - **Now:** `foliage_collect_attachments` computes each card's tint (`tints`) and the largest channel
793
+ (`tint_scale`, about 1.09–1.11 across presets). Vertex colours carry `tint / tint_scale`, and `generate_tree`
794
+ sets the synthesized foliage material's `diffuse_color` to `tint_scale`.
795
+ - **Custom material:** a caller's own foliage material is left alone, and its tints stay clamped.
796
+ - **New:** `FoliageAttachmentSet.dryness`, `tints` and `tint_scale`. The attachment arrays are now views trimmed to
797
+ the cards that remain.
798
+
799
+ ### Plagiotropic shoots: layered foliage tiers, no buds under a bough, straight branches
800
+
801
+ - **New:** `BranchGeneratorConfig.plagiotropism` [0, 1], how dorsiventral a lateral shoot is. It has three effects:
802
+ - **Spine.** The shoot's spine is pulled towards the horizontal.
803
+ - **Bud plane.** Its children's buds are fanned into the plane of its tangent and the horizontal across it.
804
+ - **Ventral knockout.** No bud is emitted into the downward cone of half-angle `plagiotropism * pi / 2`. This covers
805
+ the raw bud, and also its heading after fanning and after any turn for clearance. A bud with no admissible heading
806
+ does not break. There is no separate knockout setting. Contact projection can still deflect a child's first
807
+ segment afterwards (at most 1% of children in the specs).
808
+ - **Presets.** Only species whose branches are plagiotropic in their architectural model set it. The source is
809
+ Hallé, Oldeman & Tomlinson's architectural models, as listed by Pfisterer (2000, *Mitt. Biol. Bundesanst.* 370) and Millet, Bouchard & Édelin (1999, *Écoscience* 6: 187–203).
810
+ - **Spruce** (*Picea*, Massart's model: tiers of plagiotropic branches) sets 0.7 on its laterals. Averaged over
811
+ seeds 1, 7 and 1337, its pads flatten (vertical over horizontal spread of a bough's cards, 0.42 → 0.25) and its
812
+ crown gets gaps between tiers (empty height bands, 0.27 → 0.41).
813
+ - **Pine, maple, oak and birch** (*Pinus*, *Acer*, *Quercus*, *Betula*) grow to Rauh's model: their branches are
814
+ orthotropic and built like the trunk, so they set none. Pine's tiers come from its annual whorls.
815
+ - **Replaces.** It replaces an undocumented emission rule that biased every lateral's buds upward and off its
816
+ underside, keyed on the child's gravitropism. Removing that rule put more twigs under the broadleaf boughs
817
+ (12 seeds):
818
+ - oak: twigs more than 30° below horizontal 1.4% → 3.8%, mean heading y 0.398 → 0.327;
819
+ - birch: 0.6% → 1.2%.
820
+ That bias had no botanical basis. Rauh-model branches are orthotropic and branch radially, and where a positional
821
+ bias is reported for them it is slight *hypotony*, laterals favoured on the lower side (Millet et al. 1999, for
822
+ aspen and white ash), never an upward one. Silver birch's twigs hang.
823
+ - **New:** `BranchGeneratorConfig.straightness` [0, 1], a pull back towards the heading the branch was given.
824
+ - **Heading.** That is the bud's heading, or for the fork that continues a split, the heading the split gave it.
825
+ - **Measured:** with `gnarl_strength` 0.6 on oak's boughs, the mean start-to-tip heading change falls from 72° to
826
+ 54° at 1.
827
+ - No preset sets it. The apical upturn stays `tip_upcurl`.
828
+
829
+ ### Foliage cards are cut to their leaves, and as many as the shoot needs
830
+
831
+ - **New:** `FoliageAtlasCutout`, the shape of the synthesized foliage atlas. For each tile it holds a convex hull
832
+ that contains the centre of every texel the tile draws, and the tile's leaf coverage. `make_foliage_material(config,
833
+ seed, out_cutout)` fills it while it synthesizes the atlas (the textures are byte-identical to before), and
834
+ `foliage_atlas_cutout(config)` computes it from the alpha alone. `generate_tree` keeps it as
835
+ `TreeAsset.foliage_cutout`.
836
+ - **Breaking:** `FoliageCardType.OctagonalCutoutHull` is now `CutoutHull`.
837
+ - **Before:** a fixed octagon, the same for every leaf.
838
+ - **Now:** each card draws its tile's fitted hull, folded about the midrib and arched along its length. This is
839
+ passed to `tree_mesh_build_foliage(skeleton, attachments, config, lod_config, cutout, out)`; `cutout` comes before
840
+ `out`, which is a breaking change. Without a cutout, the cards cover whole tiles.
841
+ - **How much is cut:** needle tiles to 0.34 of the tile, against 0.63 for the octagon; palmate tiles to 0.63, about
842
+ the same.
843
+ - **Breaking:** `FoliageSpawnRule.count` and `terminal_count` are gone. A carrier's cards now follow from its leafy
844
+ length: enough leaf that a line of sight through the shoot's foliage sleeve meets one leaf on average.
845
+ - **Formula:** `pi sin(insertion_angle) (1 - stem_anchor_v) / (card_width * scale * coverage * planes)` cards per
846
+ metre.
847
+ - **Rounding:** fractional counts are rounded with a per-branch dither, so short shoots carry their share on
848
+ average, instead of a fixed count per carrier (5–13 in the presets).
849
+ - **Leafy span:** it starts at `attach_min`, honoured up to 0.92, for the cards and the wind sail alike.
850
+ - **Per preset at seed 1337,** 5b96446cc vs now (with all of Tier 1): oak 1013 → 842, pine 1584 → 694, spruce
851
+ 1296 → 1191, maple 847 → 439, willow 682 → 1455, palm 84 → 237.
852
+ - **Wind:** the joint mechanics take the same leaf area as the foliage's sail (within 0–3.5% on every preset).
853
+ - **Changed:** `TreeWindConfig.elastic_modulus` is refitted from 2.42e8 to 3.33e7.
854
+ - **Why:** the foliage sail is now the derived leaf area, not whole cards. Measured over 12 seeds, that alone cut
855
+ the oak's median bough hinge compliance to 0.138 of its tuned value.
856
+ - **Oak:** the refit restores it (ratio 0.9998). The same modulus also sets natural frequency, so the median bough
857
+ hinge frequency falls from 4.2 Hz to 2.1 Hz, and oak's trunk base from 2.4 Hz to 0.95 Hz.
858
+ - **The other presets:** one modulus cannot hold every preset's old sway, because the old sail differed from the
859
+ new one by a different factor in each. Median bough compliance, now over before: pine 0.18, spruce 0.33, fern
860
+ 0.33, maple 0.39, birch 0.88, shrub 1.17, willow 2.93, palm 4.16. Trunk frequencies fell about 2.5x across the
861
+ presets.
862
+ - **New:** `FoliageAttachmentSet.carriers`, the skeleton branch that carries each card.
863
+ - **New:** `convex_hull_fixed_count_2d_relaxation(…, bounds)`. Given a `[x0, y0, x1, y1]` box, it never removes an
864
+ edge whose removal would put a vertex outside the box. When no edge can be removed, it returns the remaining count
865
+ and writes nothing.
866
+ - **Breaking:** `compute_foliage_variant_atlas_layout` moved to `material/compute_foliage_variant_atlas_layout.js`.
867
+ `make_foliage_material.js` no longer exports it; the package index still does.
868
+
869
+ ### Dead stubs and knots grow from the tree's history
870
+
871
+ - **New:** `BranchGeneratorConfig.self_pruning`, how readily a species sheds branches that die in the crown's shade.
872
+ A crown rule's bare stretch below its living children (its `attach_min`) is where it once grew children too, at
873
+ the same nodes: the same spacing and phyllotaxis continued down the stem, so a conifer's are whorls. Each such
874
+ branch died the smaller the earlier it died, and keeps `(1 - self_pruning) - age` of its length as a dead stub,
875
+ age running from 0 just below the living children to 1 at the stem's base. Once that no longer clears the bark it
876
+ has been grown over and leaves a scar. No preset authors a number of stubs: oak keeps one dead limb under its crown,
877
+ maple a dead pair, pine and spruce a few thin dead branches, birch and willow none, and a palm's dropped fronds ring
878
+ its trunk. A stub is shaped as the base of the branch it was (shape envelopes at its position along that whole
879
+ branch, per-step amounts from its segmentation), so it bends like a living branch's base instead of carrying a
880
+ whole branch's tip curl in its short length.
881
+ - **New:** `TreeSkeleton.branch_dead` and branch scars (`scar_count`, `scar_branch`, `scar_data`, `add_scar`). A dead
882
+ branch grows nothing, carries no leaves, conducts no sap (no pipe-model area) and ends in a flat break. Bark knots
883
+ are now those scars: the collar the branch had (`parent_collar_bulge`) outliving it as a callus rim, sized by the
884
+ branch when it died.
885
+ - **Breaking:** `BranchGeneratorConfig.knot_count`, `knot_strength` and `apex_height` are gone, with the authored
886
+ lower-stub generators.
887
+ - **Changed:** children on a stem's flare crests are no thicker than `R sin(pi / N)`, so N of them fit side by side.
888
+ Palm and fern fronds no longer kink their stem (`parent_kink` 0).
889
+
890
+ ### Tree generation grows from a generator graph, not a fixed list of levels
891
+
892
+ - **Breaking:** `TreeSpeciesConfig.levels` is gone. A tree is now `branches: BranchGeneratorConfig[]` (how a branch
893
+ is shaped; `branches[0]` is the trunk) joined by `branch_rules: BranchSpawnRule[]` (which generator spawns which,
894
+ where along the parent, how many, with what phyllotaxis and insertion angle). Each rule has `source < target`, so the
895
+ graph is acyclic. One generator can spawn several kinds of children, and several can spawn the same kind. The
896
+ order rules are listed in does not change the tree. Overriding `branches` clears both rule lists unless they are
897
+ given with it, since rules index generators.
898
+ - **Breaking:** `BranchLevelConfig` is replaced by `BranchGeneratorConfig`. Its emission fields (`child_count`,
899
+ `bare_length_ratio`, `phyllotaxis_mode`, `whorl_count`, `azimuth_jitter`, `insertion_angle*`, `length_profile`)
900
+ moved to `BranchSpawnRule` (`count`, `attach_min`/`attach_max`, …).
901
+ - **Breaking:** `FoliageConfig` keeps only the card geometry. Which generators carry leaves, and how densely, is
902
+ `TreeSpeciesConfig.foliage_rules: FoliageSpawnRule[]`. `enabled`, `attach_min_level`, `cards_per_branch`,
903
+ `terminal_rosette_count`, `bare_ratio`, `phyllotaxis_mode`, `whorl_count` and `insertion_angle` are gone; a bare
904
+ tree has no foliage rules. `foliage: false` now fails an assertion.
905
+ - **Breaking:** `tree_skeleton_assign_skin_joints(skeleton, species)` takes the species and reads its
906
+ `wind.max_joints`. `TreeSkeleton.add_branch` takes the generator index after the level, and `TreeSkeleton` gains
907
+ `branch_generator`.
908
+ - **New:** `BranchGeneratorConfig.root`: structural surface roots that descend onto the ground plane and creep along it,
909
+ add no pipe-model area to their stem, and never get a wind joint. `PhyllotaxisMode.FlareLobes` plants them on the
910
+ crests of the trunk's buttress lobes.
911
+ - **New:** `BranchGeneratorConfig.excurrent` (the conifer leader habit, which was inferred from a whorled trunk),
912
+ `BranchSpawnRule.parent_kink` (replaces "whorls never kink the stem") and
913
+ `crown_envelope` (children below or outside the crown are not shrunk or pulled in by it).
914
+ - **Changed:** Oak, Pine, Birch, Willow, Maple and Spruce have surface roots, one per flare lobe, that sink into the
915
+ ground about halfway out. Oak also has epicormic sprouts that
916
+ carry leaves; Pine and Spruce have inter-whorl branches that feed the same twigs as the whorl boughs. Every
917
+ preset's trees change for the same seed.
918
+
919
+ ### Tree wind joints get their stiffness from beam mechanics
920
+
921
+ - **Fixed:** a joint's sway no longer depends on its branching level. Bare stubs, which carry no sail, swayed like
922
+ leafy boughs (2.7 times the stiffest bough's compliance on the oak). Bent as one cantilever, every stub now sways
923
+ less than every primary bough on all six woody presets, 3,300-17,700 times less at the median.
924
+ - **New:** `tree_skeleton_compute_segment_mechanics` and `tree_skeleton_compute_joint_mechanics`. Each skeleton
925
+ segment is an Euler-Bernoulli beam element with the drag of its bark (C_d 1.2) and leaf cards (C_d 0.4) and the
926
+ mass of green wood and fresh leaves; a lateral loads the parent segment it hangs from below its attachment. A
927
+ joint's compliance is the drag moment integrated over the flexibility `dx / (E I)` of the span it rotates; its
928
+ natural frequency is the single-degree-of-freedom `sqrt(K / J)` of that span under its own weight. Surface roots
929
+ carry no load. Pinned against the analytic cantilever: `L^3 / r^3` scaling, and a joint at 0.375 L carrying
930
+ 0.2441 of the tip slope under a uniform load.
931
+ - **Changed:** wind joints go to what the wind moves. Primary-bough hinges and mid-bough joints compete for the
932
+ budget ranked by the bending they would govern, then secondary hinges likewise; a stub gets a joint only once
933
+ every primary bough has its own. The oak spends 23 joints on boughs and none on stubs.
934
+ - **Changed:** `chunk_foliage_wind_step` drags with the dynamic pressure: deflection `0.5 * compliance * v^2`. It
935
+ was `compliance * |v| * (0.5 + 0.5 |v|)`, whose linear term has no physical basis (3x the deflection at 0.5 m/s).
936
+ - **Breaking:** `TreeWindConfig.trunk_compliance`, `branch_compliance` and `natural_frequency` are gone, replaced by
937
+ one `elastic_modulus` (2.42e8 Pa effective), fitted so the oak's boughs keep their previous sway in a 6.5 m/s
938
+ wind. The oak's trunk base then rings near 1.0 Hz and its boughs near 2.1 Hz.
939
+ - **Breaking:** `tree_skeleton_assign_skin_joints` returns `joint_compliance` and `joint_natural_frequency` in
940
+ place of `joint_t_norms`, `joint_radii` and `joint_lengths`. A joint whose span holds no segment is rigid
941
+ (compliance 0). `build_tree_wind_rig` returns `null` for a single joint: joint 0 is the anchored trunk base.
942
+
943
+ ### ACES, Unreal Engine's look, is the default tone mapping
944
+
945
+ - **Added:** `ShadeToneMappingMode.ACES`, and **Breaking:** it is the renderer's default, where GT7 was. Every
946
+ default look changes. Set `renderer.tone_mapping_mode = ShadeToneMappingMode.GT7` to keep the previous one.
947
+ - SDR is Unreal's "Filmic" tone mapper at its default settings, with Expand Gamut
948
+ and Blue Correction around it, per pixel where UE bakes a LUT. An exposed 0.18 shows at 0.18 of SDR white, as
949
+ with GT7, at a mid-tone contrast of 1.36 against GT7's 1.05. Shadows are deeper, highlights brighter, and
950
+ saturation 12–15% higher on the volumetrics playground.
951
+ - HDR is what Unreal 5.8 does with Filmic past 100 cd/m². The SDR picture goes back through the inverse ACES 2.0
952
+ output transform for an SDR display, then out through ACES 2.0 for the display's headroom over the paper white.
953
+ Both are limited to Rec.709, the SDR picture's gamut, so as the headroom shrinks to none the HDR picture becomes
954
+ the SDR one. Mid-grey rises with the peak, as ACES 2.0 is designed to: 0.26 of SDR white at 1000 cd/m².
955
+ - The inverse is limited where OpenColorIO's is ill-conditioned. Its decompression is clamped at the estimated
956
+ gamut boundary, held to a gain of 5, and faded out over the last unit of J before white. Without these limits, a
957
+ bright tinted highlight, whose SDR picture Filmic clips onto the gamut's surface, jumps by up to a quarter of SDR
958
+ white between neighbouring scene values.
959
+ - **New:** `TonemapAces2Tables`, the HDR pass's two ACES 2.0 output transforms. They are built on the first HDR frame
960
+ and rebuilt only when the display's peak changes (7–13 ms).
961
+ - **New:** `aces2_transform_block_write`, OpenColorIO 2.6.0's ACES 2.0 parameter and gamut-table setup, and the WGSL
962
+ `aces2_forward` / `aces2_inverse` that read it. Outside the inverse's limits they match OCIO to 1e-4, encoded.
963
+ - **New:** `PAPER_WHITE_NITS` is exported to JS from `chunk_PAPER_WHITE_NITS.js`, which writes it into WGSL.
964
+ - **Cost:** the SDR pass takes 12–13 µs at 1258×646 on an RTX 4090, against GT7's 14–21. The HDR pass takes 55–102 µs,
965
+ against GT7's 16–33.
966
+ - `ACES_TONE_MAPPING_BRIEF_2026_10_17.md`, beside the tone mapping chunks, has the sources, the decisions and the
967
+ measurements.
968
+
969
+ ### Environment maps carry an octahedral skirt, and are filtered in hardware
970
+
971
+ - **Breaking:** an environment map (`scene.lights.environment`) must carry a one-texel octahedral skirt:
972
+ the octahedral square in the interior `size - 2` texels, each texel of the outer ring a copy of the
973
+ interior texel the octahedral fold puts beside it. A map without one, or one too small to keep an
974
+ interior at its last filtered level (under 48 texels), fails an assertion in `GPULightCollection` when it
975
+ is uploaded. A constant map already has its skirt. Octahedral `.hdr` files converted before this
976
+ change have none: re-convert them from their equirectangular sources.
977
+ - **Breaking:** `resample_equirectangular_to_octahedral` and `resample_cube_to_octahedral` write the skirt,
978
+ and take a third argument `skirt` (default `true`); `output_resolution` is the side with the skirt.
979
+ `tools/environment/convert_equirectangular_to_octahedral.mjs` writes it too; `--no-skirt` writes the
980
+ square across the whole map.
981
+ - **Breaking:** `sample_environment_map` and `sample_environment_color` take a sampler after the texture.
982
+ Every shader that reads the environment binds `sEnvironment` (`ENVIRONMENT_MAP_SAMPLER_RESOURCE`, in
983
+ `SHADING_LIGHT_RESOURCE_GROUP`), and `tEnvironment` is declared filterable (`"float"`).
984
+ - **Faster:** `sample_environment_map` is two hardware bilinear taps, one per level, where it was eight
985
+ loads and eight octahedral wraps. Radiance plus irradiance over 1920×1080: 88 → 17 µs on an RTX 4090,
986
+ 15.4 → 1.3 ms on a 2-CU RDNA2 iGPU. `sample_environment_color` and the prefilter's 196 taps a texel
987
+ are one tap each. The split-sum error against the SH-exponential playground's quadrature reference is
988
+ unchanged to four digits at every roughness.
989
+ - **New:** `sampler2d_octahedral_skirt_fill`, `octahedral_skirt_matches`, `octahedral_skirt_source_texel`
990
+ (CPU), and the WGSL `texture_octahedral_skirt_uv` and `texture_octahedral_skirt_texel_uv`.
991
+ - **Changed:** the prefilter (`shader_convolve_reflection_map`) and the sky irradiance map write their levels
992
+ with the skirt; the SH-exponential encoder's cell pass integrates the interior only.
993
+
994
+ ### The froxel media build keeps a volume in the rows at its top and bottom edges
995
+
996
+ - **Fixed:** `shader_volumetrics_build_participating_media` turned its froxels' Y planes to face out, so its
997
+ box test kept a volume only in the rows of froxels the volume spanned from top to bottom. A volume lost up
998
+ to a row of fog at its top and bottom edges, and one less than a row tall (16 px) never drew at all. The
999
+ build now takes its planes from `volumetrics_froxel_planes`, as the bin does. The playground's picture
1000
+ stays inside its noise floor at all 11 of its viewpoints, and on a 2-CU RDNA2 iGPU the build costs what
1001
+ it did (new/old 0.998).
1002
+
1003
+ ### The froxel media build walks only the volumes its workgroup can see
1004
+
1005
+ - **New:** `shader_volumetrics_bin_participating_media`. Before the participating-media build it tests every
1006
+ volume against each build workgroup's slab of froxels, and the build walks only its group's list, in row
1007
+ order. The media written is unchanged (bit for bit under the emulator). On a 2-CU RDNA2 iGPU at 1898×982 the
1008
+ build plus the bin costs 4.85 ms against 7.78 with the playground's six volumes, and 6.10 against 207 ms
1009
+ with 256 more volumes no froxel reaches.
1010
+ - **New:** `VOLUMETRICS_MEDIA_GROUP_VOLUME_BUDGET` (32), the most listed volumes a froxel walks. Past it the
1011
+ group's highest-indexed volumes are dropped. The walk had no budget before.
1012
+ - **New:** `chunk_volumetrics_media_group_list` and `VOLUMETRICS_MEDIA_WORKGROUP_RESOLUTION`, which the
1013
+ build's `@workgroup_size` now reads.
1014
+ - **New:** `shader_volumetrics_split_participating_media`, run before the bin. It splits the volumes between a
1015
+ near field (any part nearer than `froxel_far`) and a far field (any part beyond), both when a volume is
1016
+ across the plane, and drops what lies outside the grid's sides or behind its near plane. The bin
1017
+ reads only the near field: on the iGPU it stays at 0.35 ms with 1,024 volumes the camera cannot see,
1018
+ against 3.54 ms, for 0.01–0.02 ms of split. Both fields are `VOLUMETRICS_MEDIA_VOLUME_LIST`s.
1019
+ - **New:** the aerial pass's far field walks the far-field list instead of every volume. Its sums vary by rounding
1020
+ from frame to frame, since the list is unordered. On the iGPU, with `froxel_far` at 40 m and 1,024 volumes
1021
+ the camera cannot see, the aerial pass costs 5.9 ms against 56. At the default `froxel_far` it costs 1.13
1022
+ against 10.7.
1023
+
1024
+ ### A wrapped shape answers about its own point, however deeply it is nested
1025
+
1026
+ - **Fixed:** `TransformedShape3D` and `PosedShape3D` gave wrong distances, gradients, `contains_point`
1027
+ answers and volume samples when a union between them held more wrappers. Every instance shared one
1028
+ scratch point, so the union's first child wrote over the point its siblings had still to be asked
1029
+ about. A wrapped union of two wrapped unit spheres answered 1.024 for a distance of 0.118. Each instance
1030
+ now holds its own arrays. A flat `TransformedShape3D` distance costs what it did (34–36 ns against
1031
+ 38–40 ns, node, one wrapper class per process), `support` costs what it did, and
1032
+ `compute_bounding_box` no longer copies the box it guarded.
1033
+ - **Fixed:** `UnionShape3D#signed_distance_gradient_at_point` answered about the wrong point when one of
1034
+ its children's distances sampled a gradient of its own: the sampler's probe was shared too. It now
1035
+ probes in its caller's `result`.
1036
+ - **Fixed:** `UnionShape3D#compute_bounding_box` rounded its children's boxes to float32 and allocated
1037
+ an array per call. `raycast` on `TransformedShape3D`, `PosedShape3D` and `UnionShape3D` allocated a
1038
+ ray per call. Each instance now keeps its own.
1039
+ - **Fixed:** `PosedShape3D` had no `signed_distance_gradient_at_point`, so a `TransformedShape3D`
1040
+ wrapping one threw on every distance query. It answers the shape's gradient, turned into the world by
1041
+ the pose.
1042
+
1043
+ ### The scene's wind reaches cloth, water, volumetrics and particles
1044
+
1045
+ The GPU wind fluid's readers (S3, S5, S6): every consumer of wind now reads one scene wind, the GPU
1046
+ fluid where a domain exists and the far field of the global effectors elsewhere.
1047
+
1048
+ - **New:** `GPUFluidSystem#request(min…, max…, nx, ny, nz, target, target_offset)` and `#status`, the
1049
+ GPU wind read back to the CPU as a lattice of `(x, y, z, 0)` m/s: in order, at most 4 batches in
1050
+ flight, drops reported, headless answered from the far field. On the RTX 4090 a lattice lands a
1051
+ median 2 frames after it is asked for. `GPUFluidQueries`, `FLUID_QUERY_DATABASE_SPEC`,
1052
+ `shader_fluid_query_gather`.
1053
+ - **New:** `ClothGPUFluidWind`. Cloth reads the scene's wind by default: `ClothGPUFluidWind` while the
1054
+ scene has an enabled `GPUFluidComponent`, else `ClothFluidWind` while the far field is not still,
1055
+ else no wind. An assigned `ClothSystem#wind`, `null` included, wins.
1056
+ - **Breaking:** `ClothAmbientWind` is removed. Its gust is `GlobalFluidEffector#gust`, on the scene
1057
+ clock. Cloth's worker/inline parity holds for cloths registered on the same tick and under a wind
1058
+ that does not change over time (cloth `MEASUREMENTS.md` §69).
1059
+ - **Breaking:** `ClothFluidWind` takes the `FluidEffectorsSystem` and loses `ambient`; outside the CPU
1060
+ fields it answers the far field.
1061
+ - **Breaking:** `GlobalFluidEffector#apply` is removed; `FluidSimulator#step` refuses a global (dev
1062
+ assert). Bring a field to the far field with `fluid_apply_atmosphere`.
1063
+ - **Breaking:** `WaterWaves.windSpeed` and `windDirection` are removed. A body's waves follow the
1064
+ scene's wind 10 m above its origin, low-passed over 20 s on the GPU; weights re-derive only past a
1065
+ 5% / 3° deadband, and a turn starts only past 3°. `WaterWaves` binary v2; the v1→v2 upgrader drops
1066
+ the wind. The wave row is re-laid (`shore_reflection` its own field, a GPU-written `wind` and
1067
+ `wind_seeded`, `derived_for` a vec4). Water animates its bands only while an enabled
1068
+ `GPUFluidComponent` exists or the far field is not still.
1069
+ - **Breaking:** `SceneVolumetrics.wind` is removed. Participating media scroll with the scene's far
1070
+ field (`GPUFluidSystem#far_field` × `noise_wind_factor` + `noise_velocity`, folded on the host;
1071
+ still air without a `GPUFluidSystem`). Where the scene's fluid departs from the far field, every
1072
+ pass that evaluates the noise samples the published wind field there and carries the noise by the
1073
+ departure as a flow map (period 2.5 s); nothing is read back. A scroll writes each moved row's seven
1074
+ phase words (`SceneVolumetrics.noise_version`, `invalidate_noise()`) instead of repacking the
1075
+ table: `GPUSceneVolumetrics.update` went from 41.5–46.5 µs and 72.3 KB a frame to 2.6–2.7 µs and
1076
+ nothing per volume, at 64 volumes. `ParticipatingMediaSystem` allocates nothing per frame.
1077
+ - **New:** `ParticipatingMediaVolume.noise_wind_factor` and `noise_flow_phase`, in the row's padding
1078
+ (still 240 B); `participating_media_noise_multiplier_in_wind`. Every reader of the volumes' noise
1079
+ (the froxel build, both path tracers, Brick4, the lightmap viz bakes, the LPV trace) binds the scene's
1080
+ wind field. The froxel build costs 15.4 → 19.4 µs with a disturbed domain, and in uniform air what it
1081
+ did (RTX 4090, 1280×720, camera inside the fog): the flow map and the cascades' read are linked only
1082
+ where the scene's wind field has cascades.
1083
+ - **New:** `WIND_FIELD_UNIFORM`, the shader switch for a wind field with no cascades, decided by
1084
+ `chunk_wind_field_sample` (the far field) and `chunk_participating_media_noise` (the scroll alone). The
1085
+ froxel build sets it from `scene.wind.cascade_count`; a pass that does not reads the field as before.
1086
+ Linked for it, the froxel build in uniform air reads the old shader's time to within 0.1 µs, 1.3–1.5 µs
1087
+ under the variant for the field as it is.
1088
+ - **Fixed:** `ShaderSwitchSet#with` allocated 595 B a call that changed a value, though the set it
1089
+ returned already existed; asked again, it allocates nothing.
1090
+ - **Fixed:** the volumetrics noise scrolled upwind; it is carried downwind.
1091
+ - **New:** `SAMPLE_WIND` (opcode 62), the particle VM's read of the scene's wind at a world position in
1092
+ m/s, and the node library's `wind` node. Every particle VM pass binds the scene's wind field (one
1093
+ uniform, one 3-D texture, a sampler, no storage buffer); `execute_particle_vm` takes `sample_wind`;
1094
+ `ParticleReferenceSimulation.wind`.
1095
+ - **Fixed:** a wide-register particle frame that registered a pre-warmed emitter failed frame-graph
1096
+ validation.
1097
+ - **Fixed:** `GPUTypedTable#set_record` allocated 278.8 B a call and `#set_fields` built ~216 B of
1098
+ assert messages a call in dev builds; the run search's assert message 132 B a run. All now 0.
1099
+ - **New:** `GPUTypedTable#add_run_filled`.
1100
+ - Playgrounds: the bonfire burns in a gust (`?wind=0&gust=0` for the old still night); the
1101
+ water-surface playground's four per-body winds are one far field with a gust and a domain on the
1102
+ current body.
1103
+
1104
+ ### The GPU fluid on entities: effectors, obstacles and forcing
1105
+
1106
+ S4 of the GPU wind fluid: a scene gets the GPU wind by giving an entity a `GPUFluidComponent`; its
1107
+ effectors and obstacles reach the cascades through GPU rows, a per-frame LBVH and brick queries.
1108
+
1109
+ - **New:** `GPUFluidComponent` (+ v0 adapter) and `GPUFluidSystem(graphics, scene)`: steps the domain
1110
+ at the fixed step (at most 2 ticks a frame, the excess counted), keeps fluid rows from
1111
+ `FluidEffectorsComponent`s and `FluidObstacle` tuples, publishes `GPUSceneContext#wind`, and offers
1112
+ `far_field(out)`. The lowest enabled entity id is the domain. `EngineHarness.buildTerrain`
1113
+ registers it with water. `GPUFluidComponent.vorticity_confinement` drives a curl pass and its
1114
+ confinement term.
1115
+ - **New:** `GPUFluidForcing` (`shade/renderer/fluid/forcing/`): the fluid database
1116
+ (`FLUID_DATABASE_SPEC`), a per-frame LBVH per resolution a stepping cascade needs (one dispatch at
1117
+ ≤ 1,024 leaves; above, rebuilt on a membership change or every 16th frame, refit otherwise), per-tick
1118
+ cooperative brick queries, and the forcing each cascade step reads: solids, pinned faces at the wall's
1119
+ velocity, effector deposits. `graph_bvh_refit_lbvh`, `shader_bvh_refit_prepare`.
1120
+ - **New:** `ImpulseFluidEffector#spread` (a cone half-angle: 0 the old push, π an omnidirectional
1121
+ burst) and `VortexFluidEffector`, with adapters.
1122
+ - **New:** the effector and obstacle records: `FLUID_EFFECTOR_RECORD_STRUCT` (48 B),
1123
+ `FluidEffectorRecordKind`, `AbstractFluidEffector#write_gpu_record` / `has_gpu_record`,
1124
+ `fluid_record_apply`, `fluid_effector_record_deposit` / `_resolve` and their chunks;
1125
+ `FLUID_OBSTACLE_RECORD_STRUCT` (80 B), `fluid_obstacle_write_gpu_shape` / `_motion`,
1126
+ `fluid_obstacle_record_signed_distance` / `_wall_velocity` / `_extent`,
1127
+ `chunk_fluid_obstacle_record`; `shape_record_pack_bounding_box`,
1128
+ `chunk_shape_record_signed_distance`, `chunk_shape_record_local_bounds`, `chunk_shape_record_kind`.
1129
+ - **Breaking:** `ImpulseFluidEffector#apply` and `WakeFluidEffector#apply` deposit through their GPU
1130
+ records, so the CPU and GPU fluids deposit the same momentum. The kernel floor is two cells and
1131
+ keeps momentum: a sub-cell push carried 13–77× the continuum's momentum before, 0.98–1.08 now. A wake
1132
+ carries `|v|·v·strength·πR²·dt`; its peak on the segment is `v·strength·L/(L/6 + 2R/15)`, not
1133
+ `v·strength`. `dt ≤ 0` deposits nothing.
1134
+ - **Changed:** `WakeFluidEffector#advance(dt)` measures the tick's velocity.
1135
+ - **Changed:** `v3_clamp_to_cone` allocates nothing (it boxed 85 B a call), and its slerp takes the
1136
+ angle by atan2; `v3_clamp_to_cone_array`.
1137
+ - **Changed:** `SoftwareGPUDevice`'s indirect dispatch records carry `indirect_buffer` and
1138
+ `indirect_offset`.
1139
+ - On the RTX 4090, at the defaults (four cascades of 64×32×64), a scene of a gusting global, impulses, a
1140
+ vortex, a wake and a moving box costs the fluid 220 µs on a tick without a coarse step and 396 µs with
1141
+ one.
1142
+
1143
+ ### The GPU fluid's cascades, and the wind every GPU consumer reads
1144
+
1145
+ S2 of the GPU wind fluid: a stack of nested, scrolling, two-way coupled cascades, and the published
1146
+ wind field they write. On the RTX 4090 a tick at the defaults (four cascades of 64×32×64) costs
1147
+ 144 µs, or 292 µs on a tick that also steps a coarse cascade.
1148
+
1149
+ - **New:** `GPUFluidCascadeStack` (`shade/renderer/fluid/`): 1–8 nested cascades around an anchor,
1150
+ each twice its child's cell size. Placement goes through `ClipmapWindowSet`, a child stays the
1151
+ published band inside its parent, and a jump re-places the whole stack; a placement's exposed
1152
+ region is filled from the parent in one dispatch. Two-way coupling: each step reads its parent
1153
+ through a per-step ghost shell, relaxes toward it in a WRF band, and is restricted back into it by
1154
+ face means, divergence-free. Ruler sub-cycling steps cascade 0 every tick and at most one other.
1155
+ Every step meets the far field through the atmosphere identity, under a deviation clamp.
1156
+ `record_publish` writes the stack into the scene's wind field.
1157
+ - **New:** `FluidCascadePlacement`, `fluid_cascade_ruler`, `fluid_cascade_window_place`;
1158
+ `ClipmapWindowSet#clamp_corner`.
1159
+ - **New:** `GPUSceneWind` (`shade/renderer/wind/`), on every `GPUSceneContext` as `context.wind`: an
1160
+ `rgba16float` `GPUTileAtlas3D` with a tile per cascade and the uniform table `WIND_FIELD_STRUCT`.
1161
+ Producer API `set_cascades`, `set_cascade_window`, `set_cascade_decay`, `set_far_velocity`,
1162
+ `update`. With no producer it is still air.
1163
+ - **New:** `chunk_wind_field_sample` and `wind_field_add_resources`: `wind_field_sample(p)`, in m/s,
1164
+ for any GPU consumer: the finest cascade through the atmosphere identity, blended live across a
1165
+ 5-cell edge band toward its parent or the far field, and the far field outside every cascade. One
1166
+ uniform, one 3-D texture and a sampler; no storage buffer.
1167
+ - **New:** `FluidWindPublish`, `shader_fluid_wind_publish`, `chunk_fluid_wind_publish_cell`: a
1168
+ cascade's cell-centred deviation from its step's far field into its tile. A tile stores the
1169
+ deviation, so uniform air reads back exactly through the 4090's truncating f16 store.
1170
+ - **Changed:** `GPUFluidCascade` takes `FluidAtmosphere` records instead of a far velocity, and a
1171
+ parent; `FluidMacPasses` gains `record_ghosts`, and `GPUFluidPressureSolve` takes the ghost
1172
+ pressure.
1173
+
1174
+ ### The GPU fluid's solver core: one cascade window
1175
+
1176
+ The first stage of the GPU wind fluid (`engine/physics/fluid/GPU_FLUID_CASCADES_PROPOSAL_2026_10_16.md`,
1177
+ S1): one cascade window stepped on the GPU, every kernel a WGSL twin of the CPU fluid's kernels or of
1178
+ a CPU model, and held to them. On the RTX 4090 a step costs 115 µs at 64×32×64 and 245 µs at
1179
+ 128×32×128.
1180
+
1181
+ - **New:** `GPUFluidCascade` (`shade/renderer/fluid/`), one cascade window and the step that advances
1182
+ it, recorded into a FrameGraph as one pass: MacCormack advection, divergence, the pressure solve and
1183
+ the gradient. The velocity starts at the far field; outside the window, velocity is the far field and
1184
+ pressure is 0.
1185
+ - **New:** `GPUFluidPressureSolve` (`shade/renderer/fluid/pressure/`), a window's pressure solve:
1186
+ geometric-multigrid V-cycles with the finite-volume edge weight and a colour-masked LDS block
1187
+ smoother at ω = 1, 3L − 2 dispatches a V-cycle (10 at 64×32×64) plus L weight dispatches a solve, or
1188
+ red-black SOR one colour a dispatch. `record_v_cycles` / `record_sor` record it as its own pass,
1189
+ `encode_v_cycles` / `encode_sor` into a caller's. Non-finite pressure and RHS are read as 0 and
1190
+ counted. V-cycles reach the same residual ~3× faster than SOR; two a step is the default.
1191
+ - **New:** `FluidMacPasses` and `FluidMacVelocitySet` (`shade/renderer/fluid/mac/`): the MAC passes of
1192
+ a step (`shader_fluid_mac_advect_predict`, `_advect_correct`, `_divergence`, `_gradient`, with
1193
+ `FLUID_MAC_STEP_STRUCT`) on `r32float` face textures.
1194
+ - **New:** `chunk_v3_mac_*`, the MAC grid's per-face and per-cell maths in WGSL (pin rule, divergence,
1195
+ gradient subtraction, clipped trace, 4-tap carriers, manual f32 trilinear taps through seams,
1196
+ Selle's limiter, MacCormack predict and correct), twins of the CPU's `v3_mac_*` kernels.
1197
+ - **New:** the V-cycle's per-cell WGSL: `chunk_fluid_mg_word`, `chunk_fluid_mg_level0_word`,
1198
+ `chunk_fluid_mg_coarse_word`, `chunk_fluid_mg_relax`, `chunk_fluid_mg_restrict`,
1199
+ `chunk_fluid_mg_prolong`, and the Dirichlet seam `chunk_fluid_pressure_ghost`.
1200
+ - **New:** `chunk_subgroup_atomic_add`, a lane's count added with one atomic per subgroup.
1201
+
1202
+ ### The CPU fluid meets one far field
1203
+
1204
+ Every global effector used to act on every field on its own, in whatever order the fields gathered
1205
+ them. They are now folded into one far field once a tick, which the CPU fluid meets through one exact
1206
+ step, and which the GPU fluid will read too.
1207
+
1208
+ - **New:** `GlobalFluidEffector` gains `gust`, `gust_period`, the state `gust_phase`
1209
+ (Float64Array(3), turns), and `compute_gust(out)`; `advance(dt)` advances the phases. It is
1210
+ `ClothAmbientWind`'s gust term for term, and editing the period never jumps.
1211
+ - **New:** `FluidEffectorsSystem` folds every global into one far field once a tick;
1212
+ `read_atmosphere(out, offset)` returns a `FluidAtmosphere` record without allocating.
1213
+ - **New:** `fluid_atmosphere_to_grid`, `fluid_apply_atmosphere`, `FluidAtmosphere`,
1214
+ `FluidFieldAtmosphere`; `FluidField#atmosphere` holds the field's last instant, `clear()` resets it
1215
+ and `equals()` compares it.
1216
+ - **Breaking:** `FluidSystem` and `WorkerFluidSystem` no longer apply each `GlobalFluidEffector` on its
1217
+ own. They bring every field to the folded far field through the atmosphere identity, so globals act
1218
+ as one whatever their order, and the gust is carried without drag filtering. Only the first fold
1219
+ seeds the far field at the globals' wind; a global added later brings the air up to its wind at its
1220
+ drag, and when the last global goes the far field holds. Calm is authored as a global with wind 0
1221
+ and a drag; a scene that never had a global is still air.
1222
+ - **Breaking:** the worker protocol's `FluidCommand.FIELD_IDS` is replaced by `FluidCommand.FIELDS`,
1223
+ per-field records of `FLUID_COMMAND_FIELD_WORDS` (id plus atmosphere) in a `FluidCommandField`
1224
+ layout.
1225
+ - **Changed:** `GlobalFluidEffector#apply` is only the direct `FluidSimulator#step` path, and it has
1226
+ no gust.
1227
+ - **Fixed:** saving a scene with fluid effectors threw. `FluidEffectorsComponent` and the Global,
1228
+ Impulse and Wake effectors have a `typeName` and v0 serialization adapters, and a load resumes the
1229
+ gust where it was.
1230
+
1231
+ ### Fluid effectors live once a tick, in their own system
1232
+
1233
+ A wake advances its trail when it is synced to its entity, and every fluid system synced effectors
1234
+ itself, so a scene with two fluid systems advanced every wake twice a tick and the second applied
1235
+ no wake at all.
1236
+
1237
+ - **Breaking:** `FluidSystem` and `WorkerFluidSystem` require a `FluidEffectorsSystem`; register it
1238
+ with them (first, if the `EntityManager` is already running). Without one, startup fails in a
1239
+ development build.
1240
+ - **New:** `FluidEffectorsSystem` syncs every effector on an entity with a `Transform64` and calls
1241
+ `advance(dt)` on every effector, exactly once per fixed step and before any fluid system applies
1242
+ them, whatever order the systems were registered in.
1243
+ - **New:** `AbstractFluidEffector#advance(dt)`, a per-tick hook for state an effector evolves on its
1244
+ own; a no-op by default.
1245
+
1246
+ ### One clipmap module for the shadow map, the fluid and the cascades to come
1247
+
1248
+ - **New:** `core/geom/clipmap/`: `quantize_hysteretic`, `ClipmapWindowSet` (integer corners per
1249
+ level; `place`, `contains`, `storage_cell`, `decode`; no allocation per call), `window_scroll_regions`
1250
+ (what a scroll exposes, as disjoint boxes) and `clipmap_select_finest`.
1251
+ - **New:** WGSL chunks `chunk_clipmap_storage_coord`, `chunk_clipmap_contains` and
1252
+ `chunk_clipmap_select_finest` (the finest level holding a point, with an edge-band weight), and
1253
+ `CLIPMAP_LEVEL_STRUCT` / `CLIPMAP_LEVEL_ARRAY`.
1254
+ - **Changed:** `VSMPageManager` keeps its windows in a `ClipmapWindowSet` and evicts through
1255
+ `window_scroll_regions`; `VirtualShadowMap`'s depth range and `fluid_reanchor_field` snap through
1256
+ `quantize_hysteretic`. Window placement, eviction order, the depth range and the fluid anchor are
1257
+ unchanged to the bit.
1258
+ - **Breaking:** `shader_debug_clipmap` draws any clipmap of cascades from a `CLIPMAP_DEBUG_STRUCT`
1259
+ uniform named `clipmap` (a colour per level, the edge band, cell lines). It no longer reads
1260
+ `clipmap_info` or `gBufferOcclusion`.
1261
+ - **Removed:** `GPUClipMap3D`, `shader_update_clipmap_cascade`, `chunk_clip_cascade_coord_encode` /
1262
+ `chunk_clip_cascade_decode` (`chunk_clip_cascade_wrap_coord.js`) and `GPURadianceField`
1263
+ (`RadianceField.js`). Use the clipmap module.
1264
+
1265
+ ### The 3-D tile atlas has a tile length per axis, and the shaders share its placement
1266
+
1267
+ - **Breaking:** `GPUTileAtlas3D`'s `content_size` is `[x, y, z]` (a cube is `[n, n, n]`); its
1268
+ `content_size` and `tile_size` getters return frozen `[x, y, z]` arrays.
1269
+ `tile_atlas_bordered_block` takes `content_size` as `[x, y, z]`, and
1270
+ `tile_atlas_widened_footprint(slots_x, slots_y, tile_size_x, tile_size_y, max_dimension, out)`
1271
+ replaces `(slots_x, slots_y, axis_limit, out)`.
1272
+ - **Changed:** a full atlas widens the side narrower in texels (x on a tie, each side capped at its
1273
+ own limit); with cubic tiles this is the old rule.
1274
+ - **Removed:** `chunk_sdf_slot_index_to_texture_offset`; use
1275
+ `chunk_tile_atlas_slot_origin(slot, slots.xy, tile_size)`.
1276
+ - **New:** `chunk_tile_atlas_slot_origin`, `GPUTileAtlas3D.slot_origin` on the device; the
1277
+ volumetrics noise tap, the geometry SDF build and its three readers use it.
1278
+ - **New:** `chunk_texture3d_trilinear_corners` (with `_load`, `_corner_aligned`, `_cell_centred`,
1279
+ `chunk_texture3d_trilinear_interpolate` and `TEXTURE3D_TRILINEAR_CORNERS_STRUCT`): a manual
1280
+ trilinear tap with exact f32 weights whose eight corners also feed a limiter. The geometry SDF tap
1281
+ is now its corner-aligned form.
1282
+ - **New:** `chunk_texel_coordinate_to_uv3` and `chunk_uv3_to_texel_coordinate`, cell-centred.
1283
+
1284
+ ### Shared SDF and effector kernels, in JS and WGSL
1285
+
1286
+ - **New:** `chunk_sdf3_box`, `chunk_sdf3_sphere`, `chunk_sdf3_capsule` and `chunk_sdf3_cylinder`, the
1287
+ WGSL twins of `sdf3_box`, `sdf3_sphere`, `sdf3_capsule` and `sdf3_cylinder`, with the same frames
1288
+ and parameters. `aabb3_signed_distance_to_point` now calls `sdf3_box`.
1289
+ - **New:** `falloff_quadratic`, `v3_clamp_to_cone`, `rigid_velocity_at_point` and
1290
+ `vortex_swirl(out, out_offset, x, position, axis, segment_length, radius, strength)` (a
1291
+ `segment_length` of 0 is a point vortex about the axis line), each with a WGSL chunk of the same
1292
+ name. The cone clamp's twins compose the same slerp and agree at the antipode and next to the axis.
1293
+ - **New:** `chunk_v3_slerp` and `chunk_v3_angle_stable`, the twins of `v3_slerp` and
1294
+ `v3_angle_stable`, branch for branch. The general branch takes its angle by atan2, which holds f32
1295
+ parity next to the antipode where acos does not.
1296
+ - **New:** `chunk_point_closest_on_line`, the closest point on a segment; `point_distance_to_line`
1297
+ now measures to it.
1298
+ - **Fixed:** `point_distance_to_line` returned NaN for a segment of no length; it now returns the
1299
+ distance to that point.
1300
+ - To reach WGSL's `point_distance_above_plane`, an `sdf3_plane` is packed as `vec4(n, −offset)`.
1301
+
1302
+ ### A physics shape as a core record
1303
+
1304
+ The kind-and-four-parameters record cloth kept for its colliders is now core's, so the GPU fluid's
1305
+ obstacles can pack the same shapes.
1306
+
1307
+ - **New:** `core/geom/3d/shape/record/`: `ShapeRecordKind` (cloth's five analytic kinds, then
1308
+ `ConvexHull`), the record layout, `shape_record_count` and `shape_record_pack(record, offset, pose,
1309
+ pose_offset, planes, plane_index, shape, element)`. A shape is a run of records: a rigid
1310
+ `TransformedShape3D` packs its subject with each pose composed, a `UnionShape3D` its children
1311
+ depth first, and a hull of up to 16 planes its planes into a caller's plane array. A mesh, a
1312
+ heightmap, a wrapper that scales, shears or mirrors, and any union holding one, pack nothing.
1313
+ - **New:** `shape_record_local_bounds` and `shape_record_signed_distance(record, offset, planes, x,
1314
+ y, z)` / `_gradient_array`, the CPU field of a record (a hull: exact inside, conservative outside).
1315
+ - **New:** `m4_decompose_rigid_array`, the narrowphase's rigid-matrix test, moved to core;
1316
+ `rigid_subject_pose` calls it.
1317
+ - **Changed:** cloth's collider record composes the core record. Its slots, its accepted kinds and
1318
+ its results are unchanged to the bit.
1319
+
1320
+ ### Shared GPU chunks: an append, a box overlap, a cooperative BVH query, halo tiles
1321
+
1322
+ - **New:** `subgroup_append_slot(count, want, capacity)` (`chunk_subgroup_append_slot`): claims a
1323
+ lane's slot in an append-only list with one atomic per subgroup. Past the capacity the count keeps
1324
+ rising and the lane gets `SUBGROUP_APPEND_NONE` (0xFFFFFFFF).
1325
+ - **New:** `collection_u32_append(&list, want, value, capacity)` (`chunk_collection_u32_append`) for
1326
+ a `COLLECTION_U32_STRUCT_WRITE` list.
1327
+ - **Changed:** the virtual shadow map's mover list appends through it; what it lists is unchanged.
1328
+ - **New:** `chunk_aabb3_intersects_aabb3`, closed like the CPU `aabb3_intersects_aabb3`: boxes that
1329
+ share a face, an edge or a corner intersect.
1330
+ - **New:** `chunk_bvh_query_aabb_cooperative`: every leaf of a BVH whose box overlaps one query box,
1331
+ found by a 64-invocation workgroup a level at a time and returned as payloads in ascending order.
1332
+ At most 256 frontier nodes, 51 levels and 128 candidates; what does not fit is counted. The
1333
+ candidates are ordered with `chunk_sort_bitonic_comparator`. The shader defines
1334
+ `bvh_query_aabb_node` and `bvh_query_aabb_accept_leaf`.
1335
+ - **New:** `chunk_grid_tile_halo_f32` and `chunk_grid_tile_halo_vec4`: an 8×8×4 tile of a toroidally
1336
+ stored 3-D grid and its one-cell halo, loaded into workgroup memory by 256 invocations, any number
1337
+ of tiles per kernel. The shader defines the fetches inside and outside the window.
1338
+ - **New:** `graph_draw_texture_3d_slice_as_rgba({graph, source, resolution, axis, slice, mip_level,
1339
+ scale, bias, output_format})` draws one slice of a 3-D texture as texel × scale + bias per channel,
1340
+ so signed fields such as velocity can be viewed.
1341
+
1342
+ ### Readback runs, interleaved sampling, a singleton without allocation, and a teleport without motion
1343
+
1344
+ - **New:** `GPUDatabaseReadback.request_run(table, first_row, row_count, byte_offset, byte_size,
1345
+ target, target_offset)` reads the same byte range of consecutive rows under one handle and one
1346
+ status, all or nothing; a run of whole records is one copy per page rather than one per row.
1347
+ `request` is a run of one.
1348
+ - **New:** `scs3d_sample_linear3_strided(out, data, stride, channel_offset, res_x, res_y, res_z,
1349
+ position, position_offset)` samples three interleaved channels where a readback landed them, with
1350
+ the planar form's corners, weights and clamping.
1351
+ - **New:** `EntityComponentDataset#getAnyComponentInstance(type)`: the instance held by the lowest
1352
+ entity id, or null, without allocating (0 B and 14–36 ns against `getAnyComponent`'s 24–41 B and
1353
+ 99–124 ns, 10,000 entities). `getAnyComponent` answers from the same record.
1354
+ - **Fixed:** a `Transform64` placed with `TRANSFORM64_EVENT_PLACED` reported its whole teleport as
1355
+ one frame's motion on the GPU, to TAA and to anything reading `global − prev_global`.
1356
+ `GPUSceneRows` now seeds the placed row's `prev_global` to its new pose.
1357
+
1358
+ ### Tone mapping
1359
+
1360
+ - **Added:** `ShadeToneMappingMode.PBR_NEUTRAL`, Khronos PBR Neutral on every output. SDR runs the specification:
1361
+ base colours under 0.8 come out as they are, less a 0.04 offset, and hues never shift. HDR puts the display's
1362
+ peak, over the paper white, where the specification has 1, so below the compression start an HDR display shows
1363
+ what an SDR display shows. GT7 stays the default.
1364
+ - **Breaking:** the WGSL `tonemap_khronos_pbr(color)` is `tonemap_khronos_pbr(color, peak)`. A peak of 1 is the
1365
+ specification, as before.
1366
+ - **Breaking:** the unwired ACES and AgX chunks are gone. Neither has a form that serves an SDR and an HDR display with
1367
+ the same mid-tones (`HDR_TONE_MAPPING_FORMS_BRIEF_2026_10_16.md`). Removed from `shade/renderer/shader/chunk/color/`:
1368
+ `tonemap/chunk_tonemap_aces.js`, `sRGB/chunk_RRTAndODTFit.js` (its fit), `tonemap/chunk_tonemap_agx.js`, and
1369
+ `tonemap/chunk_tonemap_pbr_neutral.js`, a duplicate of `chunk_tonemap_khronos_pbr` that mixed toward 1 instead of
1370
+ the compressed peak. The CPU `core/color/tonemap/tonemap_aces.js` stays.
1371
+ - **Breaking:** the paper white is a convention of every tone mapping mode, not of GT7:
1372
+ `shade/renderer/shader/chunk/color/tonemap/gt7/chunk_GT7_PAPER_WHITE_NITS.js` is now
1373
+ `shade/renderer/shader/chunk/color/tonemap/chunk_PAPER_WHITE_NITS.js`, and its WGSL constant `GT7_PAPER_WHITE_NITS`
1374
+ is `PAPER_WHITE_NITS`. The value is unchanged, 250 cd/m².
1375
+
1376
+ ### A skinned mesh's normals turn with it, however alike they were bound
1377
+
1378
+ `MeshletBatch#compress` collapsed any attribute all of a meshlet's vertices shared, normals and
1379
+ tangents included, and the skinning pass leaves a collapsed slot alone. A flat cloth sheet, every bind
1380
+ normal up, was lit in its bind pose whatever shape it took: a corner folded over on top came out dark,
1381
+ and the drape was shaded only by its shadows.
1382
+
1383
+ - **Fixed:** a meshlet that carries skin weights keeps a normal and a tangent per vertex. A skinned
1384
+ `MeshletGeometry` serialized before this keeps its collapsed slots until it is built again.
1385
+
1386
+ ### What travels is what goes through the action executor
1387
+
1388
+ The host compared every replicated component of every networked entity at the end of each step, to
1389
+ find what its simulation had written directly: 7–8 ms a tick for 20,000 entities with nothing
1390
+ changing, a cost in the size of the world, where the stack is for worlds of millions of objects. It
1391
+ is gone, and the session now finds no change by itself.
1392
+
1393
+ - **Breaking:** a replicated component the host writes directly (physics, a controller) reaches the
1394
+ clients only if its writer reports it: `dataset.sendEvent(entity, "net_mutate_component",
1395
+ { component_type })`, once per write. A write nobody reports stays on the host.
1396
+ - **Breaking:** the `publish_changes` option is gone.
1397
+ - **Changed:** on the host, `net_mutate_component` without `new_state` records the component as it
1398
+ stands instead of executing it: the host's state is no longer overwritten with the wire's quantised
1399
+ image (a `Transform64` rotation lost precision every tick it was reported).
1400
+ - **Changed:** a host's per-peer AUTH_STATE and a client's record of what it predicted walk the
1401
+ entities each peer owns, not every networked entity. Host, 20,000 entities, nothing changing:
1402
+ 14–15 ms a tick → 0.01–0.02 ms. Ownership is set when a `NetworkIdentity` attaches, or by the join;
1403
+ `owner_peer_id` and `network_id` are not written afterwards.
1404
+ - The assumptions and the measured costs: `engine/network/README.md`, "What the session assumes".
1405
+
1406
+ ### A network message can be any size, and a world of any size can be joined
1407
+
1408
+ A client could not join a large world: messages past 64 KiB were dropped silently, so 2,990
1409
+ one-component entities could never be joined, and a join past ~302 KB threw every tick. One connection
1410
+ layer now carries every message, engine or user, up to `max_message_bytes` (1 MiB): numbered packets
1411
+ acknowledged by ranges, loss detection and probes after QUIC, paced reliable lanes. A tick's traffic to a
1412
+ peer leaves in as few datagrams as fit — 16 owned entities went from 17 datagrams a tick to 1. Design:
1413
+ `engine/network/MESSAGE_TRANSPORT.md`.
1414
+
1415
+ - **Breaking:** the wire protocol is new and versioned; a peer on an earlier build cannot connect.
1416
+ - **Breaking:** `Channel`, `ReliableCommandPipeline` and `transport/fragments/*` are gone, replaced
1417
+ by `transport/connection/Connection`. `NetworkPeer.channel_for` is `connection_for`.
1418
+ - **Breaking:** `NetworkPeer` takes a required `clock` (ms) and `tick_period_ms`, and the connection
1419
+ options `send_rate_bytes_per_second`, `max_message_bytes`, `receive_window_bytes` and
1420
+ `max_queued_bytes`. Its `send_*` methods queue; `send_queued()` and `flush_peer(id)` send, and
1421
+ `end_tick` calls `send_queued()` last. `NetworkSession` wires all of this.
1422
+ - **Breaking:** `send_reliable_command` carries up to `max_message_bytes` (was 1,178 B), paced.
1423
+ - **Changed:** `Transport.max_datagram_bytes` (1,200; WebRTC 1,150; WebTransport its own).
1424
+ `LoopbackTransport` is no longer `reliable`: `drop_next` loses packets.
1425
+ - **New:** a join is paged, paced, and catches up with what changed while it went out;
1426
+ `NetworkSession.onSynced` fires once the client's world is whole. 20,000 entities join, including
1427
+ while the world grows by 1,000 a tick.
1428
+ - **New:** `NetworkPeer.onFrameTooLarge`: an action-stream frame larger than any message is given up
1429
+ and said so, instead of throwing every tick.
1430
+ - **New:** a connection that stops being whole (`onProtocolError`) is handled as a dropped transport:
1431
+ the host puts the peer in grace, the client resumes on a new connection. One stray datagram used to
1432
+ wedge a connection for good.
1433
+ - **Fixed:** `NodeUDPTransport` drops datagrams from anyone but its remote.
1434
+
1435
+ ### shade: a below-floor failure names everything the hardware lacks
1436
+
1437
+ Hardware below the device floor was told only that it was "below the minimum this game requires": the
1438
+ check stopped at the first gap, and what it found went into `detail`, which nothing showed. An `Engine`
1439
+ application showed less still — a "Graphics device lost" overlay advising a reload, for hardware a
1440
+ reload cannot help.
1441
+
1442
+ - **New:** `SHADE_DEVICE_FLOOR` (`shade/device`) — the required features and limits, one definition
1443
+ that both the check and the device request read. `shade_device_floor_unmet(adapter)` lists every
1444
+ feature and limit an adapter falls short of, with the value it offers, as phrases a person can read.
1445
+ - **Changed:** `ShadeDeviceFailure.below_floor(unmet)` takes that list (was one `detail` string). Its
1446
+ `message` names every gap — "This graphics hardware does not meet this application's minimum
1447
+ requirements: feature 'float32-blendable' is not supported; limit maxStorageBuffersPerShaderStage
1448
+ is 8, at least 10 is required." — and `detail` is the list alone.
1449
+ - **Changed:** no failure message calls the application a game; the engine also runs CAD tools and
1450
+ product visualizers.
1451
+ - **Fixed:** `Renderer.initialize()` rejects with a `ShadeDeviceFailure` in the two cases that escaped
1452
+ it: a browser without WebGPU (was a plain `Error`), and a refused device request
1453
+ (`device_request_failed`, whose message now carries what the browser said).
1454
+ - **Fixed:** `Engine` shows a failed start as "Graphics unavailable" with the failure's own message.
1455
+
1456
+ ### A positioned sound is placed when it or the listener moves, not every frame
1457
+
1458
+ A playing 3D instance wrote its distance gain and its panner position to Web Audio every frame, moved or
1459
+ not; a crowd of one-shots standing where they were played made that most of the audio system's frame.
1460
+
1461
+ - **Breaking:** an `EventInstance` reads its `position` when it starts, after `moved()`, and when the
1462
+ listener moves — no longer every frame. The array is still held by reference, so whoever writes it
1463
+ calls `instance.moved()`; a write nobody reports is not heard. Give each instance an array of its own:
1464
+ instances sharing one are all wherever it was last written.
1465
+ - **Breaking:** `SopraEngine.setListener(position)` is the listener's move report. Every 3D instance
1466
+ re-attenuates on the next `update` after a call, and none does between calls, so after writing into
1467
+ the same array, call it again. A caller that already sets it every frame sees no difference.
1468
+ - **New:** `EventInstance.moved()`, and `SopraEngine.listenerVersion`, which `setListener` advances.
1469
+ - **Changed:** `EventInstance.update(now)` reads the engine's listener itself, with the version that
1470
+ goes with it; the `listenerPosition` argument is gone.
1471
+ - **Changed:** `AudioEmitterSystem`, `LiveEmitterSet` and `AudioEventTriggerSystem` report moves from
1472
+ `TRANSFORM64_EVENT_CHANGE`, the listener's included, as the live set already refit its broadphase. A
1473
+ transform written without `t64_announce_change` moves no sound, as it moves no rendered row.
1474
+ - **Fixed:** `TopDownCameraControllerSystem` announces the transform it writes, when the pose changed.
1475
+ Without it a `SoundListener` on that camera never moved, so every panned sound was directed from where
1476
+ the camera stood when it linked.
1477
+ - **Fixed:** `PathFollowingSystem` announces the moves it writes. Without it, nothing that follows a
1478
+ transform by its announcement saw a path follower move — its sound included, from this change on.
1479
+
1480
+ ### The transient pool keeps what it is ordered by on its records
1481
+
1482
+ - **Changed:** a `TransientResourcePool` record carries `size`, read once as its resource enters the
1483
+ pool, and a subclass caches whatever else its `compare` and `find` read in the new
1484
+ `describeRecord(record)` hook. `GPUBufferAllocator` caches `usage`.
1485
+ - **Breaking:** `buffer_find` and `buffer_compare` read a record's `size` and `usage`, not its
1486
+ resource's; a pool of your own built on them caches both, as `GPUBufferAllocator.describeRecord` does.
1487
+ - **New:** `array_remove_at` and `array_insert_at` (`core/collection/array`): `splice` by one element in
1488
+ place, without the array `splice` returns. The pool edits its free list with them.
1489
+
1490
+ ### A look rotation allocates nothing
1491
+
1492
+ - **New:** `quat3_look_rotation(out, out_offset, fx, fy, fz, ux, uy, uz)` (`core/geom/3d/quaternion`),
1493
+ worked in locals. `Quaternion._lookRotation` and `t64_look_rotation` answer through it, with the same
1494
+ results but the sign of a zero lane; `t64_look_rotation` no longer allocates. Its inputs must be finite
1495
+ where assertions are kept.
1496
+
1497
+ ### sopra: events carry their own effects, and effect parameters move live
1498
+
1499
+ A cue could not be filtered per play: effects existed only on buses, and a bus effect's parameters
1500
+ could not be moved once built — the nodes were private, and the only way in was to know that a
1501
+ one-effect bus's input happens to be its effect. Nor could an effect follow a sopra parameter the way a
1502
+ bus volume can, or hold a value on an emitter whose voice is rebuilt each time it is promoted. And adding
1503
+ a bus to the tree reset every fader the player had moved.
1504
+
1505
+ - **New:** `EventDescription.effects` — insert effects of the event's own, the same types a bus takes.
1506
+ Every instance builds its own copy, pre-fader (`voices → effects → instance gain → 3D chain → bus`),
1507
+ so one play can be filtered, compressed or shaped apart from every other. Serialized (JSON and
1508
+ binary), and part of `equals`/`hash`/`clone`.
1509
+ - **New:** `EventInstance.setEffectParam(index, name, value)` / `approachEffectParam(index, name,
1510
+ value, timeConstant)` / `getEffectParam` — move one instance's copy, from the moment it exists (while
1511
+ its buffers load included).
1512
+ - **New:** `SopraEngine.setBusEffectParam` / `approachBusEffectParam` / `getBusEffectParam`, over
1513
+ `BusGraph.setEffectParam` / `approachEffectParam` / `getEffectParam` — the same two verbs as the
1514
+ fader's `setVolume` / `approachVolume`. `set` moves it now; `approach` glides there exponentially
1515
+ (`setTargetAtTime`). Either way in-flight automation is cancelled and held first, so a value sent every
1516
+ frame glides on from where the last one had got to. The getters return where the parameter was sent,
1517
+ not where a glide is. An effect is addressed by its index in the definition's `effects`, a parameter
1518
+ by name.
1519
+ - **New:** `SopraEngine.bindParameterToBusEffectParam(name, busId, effectIndex, paramName, curve,
1520
+ timeConstant = 0)` — `bindParameterToBusVolume`'s counterpart. The curve maps the parameter's value to
1521
+ the effect parameter's; above 0, `timeConstant` makes each change a glide. The address is checked at the
1522
+ bind (throws naming what is missing). The parameter's value takes hold at once, without a glide — at
1523
+ the bind, or when it is first defined or set. The binding is a standing rule: `setBuses` builds every
1524
+ tree that has its target at the bound value (with `keepVolumes` or without; one binding that fails to
1525
+ apply does not stop the rest, and is thrown after), and over a tree that lacks the target it does
1526
+ nothing until a tree has it again. Binding the same parameter to the same target again replaces it.
1527
+ - **New:** `AudioEmitter.effectOverrides` (`EffectParamOverrides`, `sopra/runtime`) — the emitter's own
1528
+ values for its event's effect parameters: `set` / `approach` / `get` / `has` / `remove` / `clear`.
1529
+ `AudioEmitterSystem` attaches them at link (`checkAttach` / `attach(event, onChange)` / `detach`): each
1530
+ write is then checked against the event — an address it lacks, or a non-finite value, throws at the
1531
+ write in every build and is not kept, and ones written before the link are checked by the link, which
1532
+ throws before anything plays — and moves the current instance, on both routes. One owner at a time, so
1533
+ one entity per `AudioEmitter` (a second link throws). Every new
1534
+ instance is built at the overrides, at once, never as a glide from the definition, so a managed emitter
1535
+ demoted and promoted again keeps them; `LiveEmitterSet.add` takes them as a new last argument and puts
1536
+ them on each promotion. Live state: not serialized, not part of `equals`/`hash`.
1537
+ - **New:** `EffectChain.hasParam(index, name)` and `BusGraph.hasEffectParam(busId, index, name)` — ask
1538
+ whether an address exists, without throwing; `effectParamIndex(effects, index, name)` (`EffectChain.js`)
1539
+ resolves one against a list of effect definitions, or throws naming what it has.
1540
+ - **New:** `AbstractAudioEffect.parameters` — each effect class names, once, the parameters its built
1541
+ effect can move: `EqEffect` `frequency`, `Q`, `gainDb`; `CompressorEffect` `threshold`, `knee`,
1542
+ `ratio`, `attack`, `release`; `ReverbEffect` none. Each is the definition field it starts from.
1543
+ `build` returns their `AudioParam`s as `params`, in that order (`BuiltAudioEffect`); a mismatch is
1544
+ refused at build, naming the class. A subclass written before this inherits an empty list and builds
1545
+ and chains as before. `EffectChain` (`sopra/runtime`) is the one place a list of effects is wired and
1546
+ its parameters moved, for buses and instances alike; `BusGraph` builds its inserts through it.
1547
+ - **New:** `cancelAndHold(param, time)` (`sopra/util/fadeOutAndStop.js`) — the engine's one
1548
+ cancel-and-hold, with the Firefox fallback; `rampGain`, `BusGraph.setVolume` and `EffectChain` share
1549
+ it. `BusGraph.setVolume`'s fallback now anchors at the current value before setting, as `rampGain`'s
1550
+ always did (no audible change).
1551
+ - **New:** `setBuses(definitions, {keepVolumes: true})` — buses present in both trees keep their faders
1552
+ (moved as `setBusVolume` moves them, so an engaged duck carries over); new buses start at their
1553
+ definition. The default is unchanged: every bus starts at its defined gain.
1554
+ - **Changed (format):** `EventDescription` binary format 0 → 1 (effects appended). Its record is embedded
1555
+ untagged by `AudioEmitter` and `AudioEventTrigger`, so they move 0 → 1 with it. All three are read from
1556
+ version 0 by upgraders (`EventDescriptionSerializationUpgrader_0_1`, `AudioEmitterSerializationUpgrader_0_1`,
1557
+ `AudioEventTriggerSerializationUpgrader_0_1`), registered by `populateSopraSerializationRegistry` and
1558
+ `populateEngineSerializationRegistry`. They take the registry, because the record's root clip is a
1559
+ polymorphic tree that can only be measured by reading it.
1560
+ - **Changed:** the `BusGraph.buses` record holds `effects: EffectChain|null` in place of `chain`, the
1561
+ flat node list `dispose` used.
1562
+
1563
+ ### A replay reconciliation keeps every predicted entity's input
1564
+
1565
+ - **Fixed:** `ServerAuthoritativeClient` (`NetworkSessionReconcile.Replay`) rewound to the end of
1566
+ `server_frame - 1`, applied the one entity's AUTH_STATE, and replayed from `server_frame + 1` — so frame
1567
+ `server_frame`'s input came off every other predicted entity and never went back. A client owning two
1568
+ entities had the first-reconciled one wrong on every tick while input ran, falling up to 242 steps
1569
+ behind; it caught up only once input stopped. It now rewinds to the END of `server_frame`, the point
1570
+ the AUTH_STATE describes, overwrites there, and replays what follows. The default `Correct` mode was
1571
+ never affected.
1572
+ - **Fixed:** an applied AUTH_STATE is kept per `network_id` and applied again, at the end of its frame,
1573
+ by any later replay that crosses it. Before, a lossy link delivering one entity's AUTH_STATE for an
1574
+ older frame after another's newer one replayed the second entity back onto its misprediction.
1575
+ A kept correction belongs to one entity on one frame numbering: it is dropped once its id is freed
1576
+ or handed to another entity, and `NetworkSession` forgets them all when an INITIAL_SYNC replaces the
1577
+ world and seeks the clock.
1578
+ - **Fixed:** action-stream records arriving for a frame the client had already predicted erased the
1579
+ client's own records for that frame, so the next rewind across it could not undo that input and the
1580
+ replay applied it twice. They are now written after what the frame holds. This is every client's
1581
+ receive path, in either reconcile mode; only `Replay` rewinds across it.
1582
+ - **New:** `ServerAuthoritativeClient.forget_timeline()` — drops the kept corrections and the per-entity
1583
+ reconcile watermark, for a caller that replaces the frame numbering itself.
1584
+ - **New:** `ActionLog.resume_frame(frame)` and `FrameBufferRing.resume(frame)` — open a frame to write
1585
+ after its committed records; a frame not in the ring opens empty. `FrameBufferRing.open` now zeroes the
1586
+ claimed slot's length until `commit`.
1587
+ - **Changed:** an `onApplyAuthState` handler must leave `buffer.position` past what it read: those bytes
1588
+ are the kept correction. A replay that crosses its frame fires the handler again, with a buffer holding
1589
+ just those bytes from position 0.
1590
+ - **Changed:** `replayed_arrived_count` counts only records above `server_frame`. Records at
1591
+ `server_frame` are never undone now, so they are never reapplied.
1592
+
1593
+ ### A stopped network session gives its engine back
1594
+
1595
+ - **Fixed:** `NetworkSession.stop()` left the `NetworkSystem` that `start()` added in the engine, so the
1596
+ next session on the same engine — a rematch without a page reload — threw "Another instance of system
1597
+ 'NetworkSystem' is already registered" from `start()`. `stop()` now removes it: it is out of the
1598
+ engine's loop when `stop()` returns, and the `network_id`s of the entities left in the world are
1599
+ released. The `InterpolationSystem` stays, whoever added it; the session withdraws only its own source.
1600
+ - **Changed:** `stop()` returns a promise, resolved once the engine has shut the system down. Awaiting
1601
+ it is optional.
1602
+ - **Changed:** `start()` on a stopped session throws. The next match is a new `NetworkSession`: a
1603
+ stopped one still holds its old peers, and restarted it failed every host tick sending to them.
1604
+
1605
+ ### A clip plays whatever else its nodes carry
1606
+
1607
+ - **Fixed:** a clip keying a node that is neither a skin's joint nor above a mesh — a leaf bone an
1608
+ exporter left out of the skin, a socket nothing is attached to yet, an animated empty — never
1609
+ played on the GPU, and nothing said so: it waited for a row no system held, and none of its other
1610
+ nodes moved either. Broken in 3.44.0, when `MeshSystem` stopped holding a row for every node of a
1611
+ model. `AnimationSystem` and `AnimationGraphSystem` now hold every node their entity's
1612
+ `AnimationClips` key, with the chain above it, until the clips go (`hold_clip_targets` /
1613
+ `release_clip_targets` in `register_clips.js`).
1614
+ - **Fixed:** a rig built in code, driven by `AnimationSystem` or `AnimationGraphSystem` with no
1615
+ other system placing rows, threw "the scene names no dataset" once there was a device. The drivers
1616
+ bind the scene to their dataset at startup, as every system placing rows does.
1617
+
1618
+ ### Cloth reuses core, and core gains what cloth had kept to itself
1619
+
1620
+ Cloth had grown private copies of geometry, graph and numeric code the engine already had, and
1621
+ general-purpose code nothing else could reach. The copies now call core, and the general pieces
1622
+ live there.
1623
+
1624
+ - **New:** rigid poses, translation and rotation as separate operands, no allocation —
1625
+ `pose3_apply_point_array`, `pose3_apply_inverse_point_array`, `pose3_to_local_array`,
1626
+ `pose3_compose_array` (`core/geom/3d/pose`). Cloth, frame FABRIK and the box, hull and capsule
1627
+ narrowphase use them.
1628
+ - **New:** `quat3_multiply_conjugate_left_array` (a*⊗b), `quat3_multiply_conjugate_right_array`
1629
+ (a⊗b*), `quat3_clamp_angle_array` (same axis, at most the given angle, exactly).
1630
+ - **New:** `v3_normalize_array_checked`, `v3_reject_array`, `v3_distance_array`,
1631
+ `tri3_compute_normal_area_weighted_array`, `aabb3_array_round_outward_f32`, `min2_exact` /
1632
+ `max2_exact` (`Math.min` / `Math.max` without the call), `array_sort_insertion_ascending`,
1633
+ `matrix_symmetric_3x3_solve_ldlt` (packed `[m00, m11, m22, m01, m02, m12]`).
1634
+ - **New:** `csr_build_incidence`, `csr_graph_color_greedy`, `csr_graph_validate_coloring`
1635
+ (`core/graph/csr`). Cloth colours over them: a 64×64 sheet's colouring went from 134 ms to
1636
+ 3.5 ms, same colours.
1637
+ - **New:** `parent_entity_model_of` (`engine/ecs/parent`), `t64_read_pose_array`
1638
+ (`engine/ecs/transform`), `fluid_sample_velocity_world_mps` and `fluid_sample_domain_bounds`
1639
+ (`engine/physics/fluid/ecs`); `FAT_LINEAR` is exported from `compute_fat_world_aabb.js`.
1640
+ - **Fixed:** a cloth frame spinning faster than its per-step limit turned exactly that limit; it
1641
+ turned less, three quarters of it at a half turn.
1642
+ - **Fixed:** `ClothFluidWind` placed the field/ambient boundary half a cell off the field's cells,
1643
+ toward +: `origin` is cell (0, 0, 0)'s centre, not its corner.
1644
+ - **Fixed:** a cloth collider's leaf in the broadphase is rounded outward to float32 exactly,
1645
+ rather than padded by two ulps of its magnitude.
1646
+ - **Changed:** a vertex whose Hessian is not positive definite now keeps its position in a build
1647
+ without asserts, rather than taking a step from a failed factorisation. With asserts it throws, as
1648
+ before.
1649
+
1650
+ ### The cloth builders refuse a scaled rig
1651
+
1652
+ - **Fixed:** `cloth_proxy_from_joints` and `cloth_proxy_from_mesh` refuse a rig with a rest scale
1653
+ (beyond 1e-4) on any node above a particle, up to the model's root, and name it. Cloth simulates
1654
+ and writes back rigid poses and the GPU scales what lies under a scaled node, so such a garment
1655
+ was drawn at another size than the one simulated: a 10 cm chain under a joint scaled by 2 was
1656
+ simulated at 0.1 and drawn at 0.2, and its colliders met the smaller one. A scale on the entity
1657
+ that places the model is not seen at build time and is not checked.
1658
+
1659
+ ### A cloth collider that stands still costs nothing to keep
1660
+
1661
+ - **Breaking:** `ClothColliderFlags.Static` is removed. A collider whose pose has not changed since
1662
+ the last step is no longer refit (1.43 → 0.42 µs per collider per step), which is what the flag
1663
+ saved, so there is nothing left to promise. A collider saved with the bit still loads, and is swept
1664
+ when it moves rather than left behind. The `flags` word and its binary format are unchanged.
1665
+
1666
+ ### Clips are components; animation needs no model system
1667
+
1668
+ `AnimationSystem` and `AnimationGraphSystem` could reach a model's clips only through
1669
+ `MeshSystem.instance_of(entity).clips`, so a rig built in code could not animate. The clips are now a
1670
+ component on the entity that plays them, and the drivers read nothing else.
1671
+
1672
+ - **New:** `AnimationClips` (`engine/graphics3/animation`), the clips an entity can play:
1673
+ `ShadeAnimationClip`s whose channels key entities, looked up by name. It goes beside the `Animation`
1674
+ or `AnimationGraphController`. Not saved, as `MeshSkin` is not; replaced, never edited.
1675
+ - **New:** a model `MeshSystem` loads carries an `AnimationClips` on its root, its clips re-addressed
1676
+ to the placement's entities, empty when the model has none. It comes and goes with the model.
1677
+ - **New:** a rig built in code animates: `Transform64`, `AnimationClips` and an `Animation` or
1678
+ `AnimationGraphController` on the root; `TransformAttachment` and `GPUStateAuthority` on each joint a
1679
+ clip keys, and `ParentEntity(root)` where IK, `ContactProbe` or cloth should see the clips.
1680
+ - **Breaking:** `AnimationSystem` and `AnimationGraphSystem` take `(graphics, scene)` and play the
1681
+ `AnimationClips` on their entity. They no longer depend on `SGMesh` or hold a `MeshSystem`.
1682
+ - **Breaking:** `PrefabInstance.clips` is removed; read the root's
1683
+ `getComponent(root, AnimationClips).clips`.
1684
+ - **Breaking:** `register_instance_clips.js` is `register_clips.js`:
1685
+ `register_instance_clips(context, instance, slots)` is `register_clips(context, clips, slots)`,
1686
+ `unregister_instance_clips` is `unregister_clips`, `InstanceClipRegistration` is
1687
+ `ClipRegistration`. `ClipListPlayer.bind(entity, ecd, clips)` takes the clips, and
1688
+ `ClipListPlayer.instance` is `clips`.
1689
+
1690
+ Migrating: pass the drivers the scene the `MeshSystem` was given, in place of the `MeshSystem`.
1691
+ That is one argument at each known call site: queep-3-arena `src/app/main.ts:458`,
1692
+ `new AnimationSystem3(graphics, meshes)` → `new AnimationSystem3(graphics, scene)`; old-circle
1693
+ `packages/client/src/render/scene.mjs:75`, `new AnimationSystem(engine.graphics, this.meshSystem)` →
1694
+ `new AnimationSystem(engine.graphics, this.scene)`.
1695
+
1696
+ ### Skins are components; MeshSystem only loads
1697
+
1698
+ `MeshSystem` is a shortcut: an `SGMesh` naming a file becomes the model under its entity when the
1699
+ file has loaded. It had also become the only place a model worked — it registered skins, held a row
1700
+ for every entity of a model and kept them current — so a skinned mesh built in code could not be
1701
+ drawn deformed without faking a loaded asset. All of that now lives in components read by their own
1702
+ systems, the same for a loaded model and a mesh built in code.
1703
+
1704
+ - **New:** `MeshSkin` (`engine/graphics/ecs/mesh-v2`), the skin of the mesh on its entity: its joints
1705
+ as entities, in the order the geometry's joint indices address them, and their inverse binds. Not
1706
+ saved, as `ShadedGeometry` is not; replaced, never edited.
1707
+ - **New:** `MeshSkinSystem` registers every `MeshSkin` with the animation manager and binds its mesh,
1708
+ again after a renderer restart. Meshes bound to one skeleton the same way (a body and its hair)
1709
+ share one skin on the GPU. Add it beside `ShadedGeometrySystem`.
1710
+ - **New:** a primitive, and a skin's joint, holds the rows of everything above it, followed as they
1711
+ move (`RowChains`), so it composes on the GPU under a node a clip drives. A clip keying a node above
1712
+ a rigid mesh (a head under an animated tag) now binds without a model system holding every row.
1713
+ - **New:** `GPUAnimationManager.register_skin(joints, inverse_bind_matrices)` answers a second
1714
+ registration of the same joint entities under the same inverse binds with the first one's id;
1715
+ `bind_skin_mesh` / `unbind_skin_mesh` bind meshes one at a time, once the skin's joints are named.
1716
+ - **Fixed:** a mesh bound to a skin before its clone existed kept the skin's id after the skin was
1717
+ unregistered: the next skin to take the id deformed it, and binding it to its own was refused.
1718
+ - **Breaking: a skinned model deforms only where `MeshSkinSystem` runs.** `MeshSystem` no longer
1719
+ registers skins or holds rows. `MeshSystem.animation_of`, `PrefabInstance.skins`,
1720
+ `InstanceAnimationRegistration` and `register_instance_animation` / `unregister_instance_animation`
1721
+ are removed; `register_instance_animation.js` is `register_instance_clips.js`.
1722
+ - **Breaking:** `GPUAnimationManager.register_skin(skin)` is `register_skin(joints,
1723
+ inverse_bind_matrices)` plus `bind_skin_mesh` per mesh, and `unregister_skin` on a skin it issued
1724
+ releases one registration.
1725
+ - **Breaking:** `ClothRig.skin` is the entity whose `MeshSkin` the proxy indexes (-1 until named), not
1726
+ an ordinal; `ClothRig.from(proxy, skin)`. `ClothSystem.models`, `WorkerClothSystem.models` and
1727
+ `ClothWorld.models` are removed. A `ClothRig` saved as JSON with `skin: 0` now names entity 0.
1728
+ - **Breaking:** a primitive under a parent holds the parent's row and is written as its offset under
1729
+ it, not as a world pose under the scene root.
1730
+ - **Breaking:** `prefab_place` answers `skins` (`{entity, skin_id}[]`, or null) instead of `animation`.
1731
+
1732
+ ### Water surface: spectral bands and wave particles
1733
+
1734
+ `WATER_SURFACE_SIMULATION_PLAN_2026_10_06.md`, `WATER_SURFACE_DISTANCE_LOD_PROPOSAL_2026_10_06.md`. The placeholder
1735
+ simulation is replaced by a surface that works from a cup to an ocean, entirely on the GPU, and costs what the view
1736
+ shows: a body out of view costs one culling thread a frame.
1737
+
1738
+ - **New:** every body's ambient waves follow from its wind and its fetch. Eighteen spectral bands, 7.8 mm to 1 km,
1739
+ shared by every body and animated once a frame (128² FFT tiles in two fused workgroup passes, ~21 µs on an RTX 4090;
1740
+ nothing while calm or paused), are weighted per body by a fetch-limited JONSWAP sea projected onto them on the GPU:
1741
+ the band projection keeps the published spectrum's height variance within 1.5%, and a 10 m/s wind over 10 km raises
1742
+ the half-metre sea of the JONSWAP growth law. Fetch is the chord of the ellipse in the body's box along the wind, so a
1743
+ long narrow lake is long only along its length. Crests travel downwind and are pinched as a Gerstner wave's are, with
1744
+ a fold clamp. Waves too short for the cells they are drawn on are filtered into slope variance for the BRDF rather
1745
+ than aliased. The spectrum evaluates only the bands a body in view can draw, and only when time moved.
1746
+ - **New:** wave particles (Yuksel et al. 2007, with packet kernels after Jeschke & Wojtan 2017).
1747
+ `WaterSystem.splash` rings travel at their group speed, spread over their widening front, damp by viscosity and
1748
+ surface film (a raindrop's ripple is gone in a fraction of a second, a big splash crosses a bay), split as their
1749
+ fronts widen, and reflect off the body's authored outline, islands included. Update, emission, binning into a
1750
+ 16-level hash and conservative per-body bounds all run on the GPU, dispatched from GPU counts.
1751
+ - **New:** the surface is drawn from a distance-LOD lattice per body (CDLOD, Strugar 2010): square cells fixed to the
1752
+ water in its metric frame, their size from the distance to the camera, odd vertices morphing onto the coarser grid
1753
+ so levels meet without cracks, cut exactly by the authored triangles so outlines, islands and sloped rest heights
1754
+ stay the mesh's own. It replaces the screen-space lattice, which coarsened a whole body to keep displaced points on
1755
+ screen and drew the sea flat with the camera among the waves (×166 at 9 m over a 20 m/s ocean). Displacement no
1756
+ longer decides how many vertices there are. A quadrant wholly inside a triangle draws as one indexed 9 × 9 patch;
1757
+ cells a triangle cuts are clipped once, in a compute pass, and drawn from their stored polygon; a body too far for
1758
+ any of its waves draws its authored triangles. Measured on an RTX 4090 at 1058 × 746 for that ocean from 9 m:
1759
+ 180,000 cells in patches, 176,000 interior and 146,000 boundary cells; the two surface passes 0.82 + 1.21 ms and
1760
+ the frame's GPU time 4.1 ms, against 2.03 + 2.42 and 6.4 ms with every cell drawn as its own 6 or 15 vertices.
1761
+ - **New:** a culling pass in front of the simulation, one thread a body against the frame's view with a prior bound on
1762
+ its waves (twice its bands' envelopes, its particles' last bounds, its queued splashes). The spectrum, wave
1763
+ particles, bounds and tessellation then run over what it keeps. A hidden body is paused — its wave particles stand
1764
+ still, its wind turn holds — unless `Water.simulateWhenHidden`. 4,000 hidden bodies add under 2 µs (they cost
1765
+ 0.37 ms before); 1,000 sub-pixel ponds in view cost their triangles (+0.45 ms) instead of 4,608 vertices each per
1766
+ pass (+340 ms).
1767
+ - **New:** the short waves are shaded per pixel. A band too short for the cells it is drawn on but long enough for the
1768
+ pixel tilts the pixel's normal; only what neither resolves becomes roughness. Before, a band had to span sixteen
1769
+ cells to be drawn whole, so at 4 px cells anything under ~64 px a wavelength turned to roughness, even up close.
1770
+ The ocean from 9 m now shows at 4 px cells the chop it showed only with 1 px cells; on an RTX 4090 that costs
1771
+ ~0.25–0.3 ms a surface pass.
1772
+ - **Fixed:** splash rings went missing: a fresh ring sometimes never showed, and rings winked out and came back with
1773
+ no new splash, more and more as a pool filled with reflections. The particles were there all along; the hash's crowd
1774
+ thinning hid whole fronts by a lottery, and counted every packet of a front an outline had broken as a front of its
1775
+ own, so a few reflected rings made a crowd. In a 7 × 4.5 m pool with a stone every 0.9 s (the water-splash example,
1776
+ seeded, stepped at 60 Hz on an RTX 4090), 47 of 66 rings were drawn at under half their height in their first second
1777
+ — one not at all for two seconds — 38 vanished and 26 came back, and the surface drawn missed 88% (rms) of what the
1778
+ packets make, while no point read more than 49 of the surface sampler's 64 entries. Now a run of a front's
1779
+ neighbouring packets is one entry a hash cell, drawing exactly the surface its packets sum to, mirror images and gaps
1780
+ included, so a point reads about one entry a front that reaches it. Where a point would still read more than 48, its
1781
+ flattest runs are doomed: they fade over a third of a second and die. A front that would take a point past the
1782
+ sampler's 64 is cut at once, whole, and dies. The steepest are always kept, and nothing doomed comes back. The same
1783
+ pool: every ring drawn whole from its landing, none vanishing or returning, the surface drawn the packets' to f32,
1784
+ never past 64 entries a point; a ring is doomed some 11 s after it lands, at a steepness of about 0.012 (0.1 when
1785
+ fresh). The cup under 195 drops a second stays within the 64 too. On an RTX 4090, the water playground stepped at
1786
+ 60 Hz: the particle passes cost ~25 µs a frame more on its ocean (12,500 particles) and ~8 µs on its lake, and the
1787
+ water as a whole ~110 and ~80 µs less, the surface passes reading one entry a run where they read every packet; the
1788
+ cup costs the same. The pool costs ~13 µs more in the particle passes and ~67 µs more in the surface passes, which
1789
+ now draw the rings that were hidden. ~10 MB more at the default capacity.
1790
+ - **Fixed:** a wave packet that falls below the drawing threshold fades out over a third of a second instead of
1791
+ vanishing.
1792
+ - **Fixed:** a splash's ring snapped to a different shape under a second after it landed, when its particles first
1793
+ split. Their packets had straight crests, which tile a front only along its tangent, so the split redrew the ring
1794
+ (by 64% of its peak). Packets now curve around their front's centre, so a front draws the same ring at any particle
1795
+ count.
1796
+ - **New:** `Water.simulateWhenHidden` (default off). The binary adapter is at version 5; the v4→v5 upgrader gives old
1797
+ saves the default.
1798
+ - **New:** `WaterWaves.upwindFetch`, the open water beyond the mesh (an open sea states the distance to its far
1799
+ shore), and `WaterWaves.shoreReflection`, the outline's amplitude reflection for wave particles (default 0.5).
1800
+ The binary adapter is at version 1; the v0→v1 upgrader gives old saves no upwind fetch and the default
1801
+ reflection.
1802
+ - **Changed:** `WaterWaves.windDirection` is an angle in the body's local xz that turns with the body and is not
1803
+ skewed by its scale. A change of direction turns the waves over four seconds; a wind that keeps turning is
1804
+ followed a turn behind.
1805
+ - **Removed:** the placeholder simulation: `GPUWaterWaveSimulation`, `shader_water_wave_batch`,
1806
+ `shader_water_wave_domain` and `chunk_water_wave` — the 512² per-body wave-equation grid and its
1807
+ terrain-depth wave speed. Waves no longer slow in shallows: deep water is assumed.
1808
+ - **New:** `GPUTypedTableDescriptor.chunk_read_field(name, rw)` reads one field from a database bound read+write.
1809
+ - **Fixed** (WGSL emulator): an element read out of a constructed `array<T, N>(…)` keeps its type, so a swizzle or
1810
+ member read on it compiles (it read garbage, or NaN); an array sized by a named constant starts as that many
1811
+ zeros (it started empty); `textureStore` takes the array-layer form.
1812
+ - **Fixed** (WGSL emulator): a shift of an i32 keeps its sign. WGSL makes every shift amount a u32, and the emulator
1813
+ sent any shift with a u32 operand through its unsigned helpers: `>>` filled with zeros where WGSL sign-extends
1814
+ (`-1 >> 1u` was 2147483647, not -1), and `<<` handed a scalar back unsigned (`-1 << 1u` was 4294967294), in scalar,
1815
+ vector and compound forms alike.
1816
+ - **New:** the `water_surface` playground: a cup, a puddle, a lake and an ocean in one scene, with wind, rain,
1817
+ click-to-splash and per-pass GPU timings.
1818
+
1819
+ ### Saving skips a component flagged transient
1820
+
1821
+ - **Fixed:** a component carrying `COMPONENT_SERIALIZATION_TRANSIENT_FIELD` is left out of a save in a
1822
+ dataset where no entity has a `SerializationMetadata`. `BinaryBufferSerializer` gave up on the
1823
+ missing type before it read the component's own flag, so the component was written, and a load
1824
+ made a second copy of one that is rebuilt at runtime.
1825
+
1826
+ ### Cloth over animated joints: a hold, not a bit
1827
+
1828
+ A garment's joints and the clips that key them both want a joint's offset, and the two sides flipped
1829
+ one `GPUStateAuthority` bit without each other. The bit was also never what stopped a clip: no GPU
1830
+ pass reads it.
1831
+
1832
+ - **Fixed:** a cape simulates under a clip that keys its bones. A bound clip wrote the joint's row
1833
+ on the GPU every frame, after the CPU's flush, so cloth's write-back never reached the screen
1834
+ there. The clip's bindings for a joint cloth holds are now muted.
1835
+ - **Fixed:** a joint whose clip was unregistered while cloth held it is no longer frozen once the
1836
+ garment goes. A clip that starts while cloth holds a joint no longer cuts off cloth's write-back,
1837
+ and one that stops no longer writes its last pose over cloth's offset. A rig re-seeded while
1838
+ seeded gives its joints back when it goes, and `ClothWorld#reseed` gives them back at once, so a
1839
+ rig removed or refused before its next seeding leaves no joint held.
1840
+ - **Fixed:** a joint a hold has let go of with a clip still bound shows what the exact evaluation
1841
+ says. A clip faded out over it resolved to the rest from before the hold, or to the holder's pose
1842
+ when the clip bound, rather than the offset the holder left. And what the holder wrote just before
1843
+ letting go never reached the row, so the axes no clip keys kept its previous pose. Neither did an
1844
+ offset announced in the frame a clip first binds the joint.
1845
+ `GPUSceneRows#mark_handover` is new: the one write of an offset past the bit, as the GPU takes it.
1846
+ - **New:** `GPUStateAuthority#held`, `gpu_authority_hold`, `gpu_authority_unhold`,
1847
+ `gpu_authority_held` and `GPU_AUTHORITY_EVENT_HOLD_CHANGE`: a CPU writer holds a component against
1848
+ the GPU. `gpu_authority_grant` does not set a held bit, and the GPU writer takes the component back
1849
+ when the hold ends. `t64_evaluate_world` and `T64WorldCache` answer a held level with its offset
1850
+ (`t64_evaluate_local`), as the GPU shows it.
1851
+ - **Breaking:** `ClothInstance#revoked_joints` is `held_joints`, and holds every joint the rig
1852
+ drives.
1853
+
1854
+ ### IK: goals on bones, frame FABRIK, limits and contacts from physics
1855
+
1856
+ The ECS IK is rebuilt on `engine/ecs/ik/PLAN.md`. A goal sits on the bone it moves, the tree is the
1857
+ `TransformAttachment` hierarchy, joint limits are the physics `Joint` on each bone, and ground contact
1858
+ comes from a physics cast.
1859
+
1860
+ - **New:** `IKGoal` on the effector bone - `target` and `chain_root` entities, `position_weight`,
1861
+ `rotation_weight` - solved by `IKGoalSystem`: frame FABRIK over the goals whose paths share a bone,
1862
+ and the analytic two-bone solve for a lone path of three bones, whose knee takes its hinge from the
1863
+ physics `Joint` on it. IK holds the bones it poses (`gpu_authority_hold`), so a clip keying them is
1864
+ muted and the GPU row, `t64_evaluate_world`, sockets and cloth anchors read IK's offset; each
1865
+ tick's pose starts from the clips, never from that offset. A goal goes when its target or a bone of
1866
+ its path is destroyed, and gives its bones back as they were authored.
1867
+ - **New:** `frame_fabrik_solve` over a `FrameFabrikProblem`: frame FABRIK on typed arrays, allocating
1868
+ nothing per solve. Every solved joint is a frame, so a branch keeps its children's offsets. Weight 0
1869
+ gives the pose no goal gives, the pose is continuous in the weight, and the solve stops on joint
1870
+ movement.
1871
+ - **New:** `two_bone_path_solve` and `joint_hinge_mid_axis` (the knee's `mid_axis` from its
1872
+ `Joint`, its bend side from asymmetric limits), `frame_fabrik_limit_from_joint`, and in core
1873
+ `swing_twist_clamp_array` and `swing_twist_compose_array`: IK keeps each bone inside the box a
1874
+ `Joint` with `swingTwist` allows.
1875
+ - **New:** `ContactProbe` and `ContactProbeSystem` (physics): a ray, or a sphere cast, from a
1876
+ reference entity, placing the probe's entity at the hit, turned as the reference and tilted to the
1877
+ surface, and passing through the character's own colliders. A foot's target, a decal, an AI.
1878
+ - **Changed:** `swing_twist_error` and `swing_twist_error_array` moved to
1879
+ `core/geom/3d/quaternion/swing_twist_error.js`; `solve_constraints.js` no longer exports them.
1880
+ - **Changed:** `AnimationSystem` and `AnimationGraphSystem` declare that they write `Animation` and
1881
+ `AnimationGraphController`, so a system reading them (IK) runs after them.
1882
+ - **Breaking:** `InverseKinematics`, `IKConstraint`, `InverseKinematicsSystem`, `IKProblem`,
1883
+ `IKSolver`, the `"2BIK"` and `"1BSA"` solvers (`TwoBoneInverseKinematicsSolver`,
1884
+ `OneBoneSurfaceAlignmentSolver`), `ik_probe_terrain_contact`, `ik_bone_chain`,
1885
+ `ik_write_local_rotation`, `InverseKinematicsSerializationAdapter` and its upgrader are gone, and
1886
+ the engine's serialization registry no longer registers them. **A save containing an
1887
+ `InverseKinematics` component now fails to load entirely: the binary deserializer cannot skip an
1888
+ unknown type.**
1889
+
1890
+ ### Two-bone IK lands the effector on the target
1891
+
1892
+ The two-joint solver (`two_joint_ik`, behind the ECS foot IK) missed reachable targets.
1893
+
1894
+ - **Fixed:** a target off the plane the limb bends in is reached. The hip's two corrections were
1895
+ composed in the wrong order, as the listing in the reference the solver follows composes them;
1896
+ on a 0.92 m leg, targets 11 and 23 cm away were missed by 1.6 and 14 mm.
1897
+ - **Fixed:** a target within a centimetre of full reach is reached, and one beyond it straightens
1898
+ the limb toward it. The reach was clamped 0.01 short of the bones' span, so a foot set down at
1899
+ the edge of its reach stopped above the ground.
1900
+ - **Fixed:** a target on the far side of the root from the effector - an arm hanging down reaching
1901
+ straight up - is reached. The swing's axis was the cross product of two rays along one line, so
1902
+ the limb bent to the right reach and never turned, a metre off on a 0.92 m leg.
1903
+ - **Fixed:** the mid joint bends about its own hinge, whichever way it faces and however its frame
1904
+ is oriented. The solver took the bend axis from the mid joint's local -Z, which is right only for
1905
+ a rig whose -Z lies in the bend plane on the knee's side: a knee bending toward +Z missed a target
1906
+ 10 cm up by 119 mm, and bone frames yawed 30 and 90 degrees missed by 8 and 59 mm. A knee animated
1907
+ a little past straight is brought round to the side its hinge bends it (it missed by 18 mm), and
1908
+ a hinge that is not square to the bones is solved exactly.
1909
+ - **Breaking:** `two_joint_ik(a, b, c, t, mid_axis, a_gr, b_gr, a_lr, b_lr)`. `eps` is gone (it
1910
+ followed the target), and `mid_axis` takes its place: the mid joint's hinge in its own frame, of
1911
+ unit length, about which a positive turn straightens the joint - ozz-animation's convention.
1912
+
1913
+ ### Screen-space reflections
1914
+
1915
+ - **New:** `Renderer.ssr.tile_denoising_enabled` opts into 8×8 coverage tiles with a two-tile
1916
+ halo. Stationary, constant radiance without ray support, uncertainty or changing history can
1917
+ skip spatial filtering and temporal neighborhood statistics. Reprojection validation and
1918
+ history age still run. It defaults to `false`; profile the scene and device before enabling it.
1919
+ - **Fixed:** the reflection lab uploads changes to mirror roughness. Its spatial measurement,
1920
+ and the variance lab's capture and reference, recognize the tiled denoising passes.
1921
+
1922
+ ### Shadow casting
1923
+
1924
+ - **Changed:** shadow casters are drawn by their front faces, as the main view draws them, in every
1925
+ shadow type — cascades, virtual shadow maps, spot and point lights. Back-face casting on top of the
1926
+ shadow pass's rasterizer slope bias had left every shadow a texel or so short of where its caster
1927
+ meets the ground. A one-sided surface now casts from the side it shows.
1928
+ - **Changed:** a point light's receiver depth bias is a world offset toward the light of a texel of
1929
+ its map at the receiver, scaled by the surface's slope, in place of a flat 1e-4 of its range, which
1930
+ front-face casters speckled through.
1931
+
1932
+ ### Virtual shadow maps
1933
+
1934
+ A directional light's shadow from a clipmap of pages drawn on demand
1935
+ (`shade/renderer/shadow/vsm/PLAN.md`). Cascades stay the default.
1936
+
1937
+ - **New:** `Renderer.feature_virtual_shadow_maps`. Each shadow-casting sun gets 12 levels of 64×64
1938
+ pages of 128 texels around the camera, from a 1 cm finest texel, in a pool of 30×30 slots in the
1939
+ shadow atlas. Pages are drawn as the main view's pixels ask for them and as moving casters make
1940
+ them stale, at most `Renderer.shadow_virtual_page_budget` a frame (default 8, shared between suns).
1941
+ Covers for pixels with nothing resident above them come first, then stale and missing pages by
1942
+ the importance of the pixels asking × frames waited. A pixel's importance is its weight under the
1943
+ automatic exposure's metering mask, now a shared chunk (`chunk_screen_importance`): full over most
1944
+ of the screen, less toward its edges. `shadow_virtual_lod_bias` and `shadow_virtual_first_texel_size`
1945
+ tune the levels. Every reader of the sun's shadow — surface lighting, volumetrics, water — samples
1946
+ it through the light database it already binds.
1947
+ - **New:** casters that moved are found on the GPU, by what changed rather than what they are: a
1948
+ mesh row whose transform, world box, geometry or material changed since the last frame, and a
1949
+ skinned mesh whose joints' skin matrices did. The pages under them are marked stale in a compute
1950
+ pass, and the marks wait on the GPU until a readback takes them.
1951
+ - **New:** the playground `src/shade/playground/vsm/`: a courtyard and a crowd, the cascades for an
1952
+ A/B, overlays for the serving level, the resolution deficit and page edges, an inset of the pool,
1953
+ a clipmap panel, and `verify()`, which checks the GPU's page table against the CPU's.
1954
+ - **New:** `VTPhysicalSlots`, the slot bookkeeping `VTPhysicalCache` now extends;
1955
+ `GPUAnimationManager#get_skin_joint_count`; `SKINNING_BINDING_STRUCT.skin_matrix_count`.
1956
+ - **Fixed:** a receiver past a shadow map's far plane read shadowed against a texel nothing was drawn
1957
+ into. It now reads lit there, and shadowed by anything drawn.
1958
+ - **Fixed:** the GPU database's WGSL serde reads and writes `i32` and `vec2i`–`vec4i` fields, and
1959
+ `read_wgsl_type_value` returns negative signed vector components as negatives.
1960
+ - **Breaking:** `shadow/map/csm/CSM_SHADOWMAP_STRUCT.js` is
1961
+ `shadow/map/DIRECTIONAL_SHADOWMAP_STRUCT.js`, with a `virtual_map` field.
1962
+ - **Removed:** the earlier virtual shadow map prototype, which nothing reached:
1963
+ - `GPUViewShadowMaps`, `VirtualShadowmapRenderer`, `GPUVirtualShadowTextureResidence`;
1964
+ - `vsm_tile_allocation_pipeline`, `vsm_residence_pipeline`, `vms_pipeline_compute_coarse_bounds`,
1965
+ `render/vms_render_tiles`;
1966
+ - `decode_tile_address`, `debug_inspect_coarse_bounds_buffer`, `debug_inspect_residence_buffer`;
1967
+ - `VSM_UNIFORMS_STRUCT`, `VSM_RESIDENCE_WORKGROUP_TILE_RESOLUTION`;
1968
+ - `light/GPUViewLightContext`.
1969
+
1970
+ ### Water surface and optics
1971
+
1972
+ Stages of `WATER_OPTICS_PLAN_2026_10_02.md`.
1973
+
1974
+ - **New:** water is a real mesh. A primitive GPU wave simulation (a damped linear wave equation on a
1975
+ 512² grid over each body, wave speed √(g·d) from the terrain depth, so waves slow in shallows and
1976
+ reflect from the shore) writes a vertex buffer behind a surface interface the production simulation
1977
+ will replace. `WaterWaves` (wind speed and direction) drives it; a body
1978
+ without one is calm. `WaterSystem.splash(entity, position, radius, strength)` kicks the surface.
1979
+ - **Removed:** the analytic optical normal and the shore wave shading: the surface's geometry supplies
1980
+ its normals. `Water.opticalNormalStrength`, `opticalNormalScale`, `waveSpeed`, `waveAmplitude`
1981
+ and `waveFrequency` no longer reach the renderer.
1982
+ - **Breaking:** `Water` is physical authoring data: `level`, `ior` (default 1.333), `roughness`
1983
+ (default 0.02, formerly `reflectionRoughness`) and `medium`, a `ParticipatingMedium` — the same
1984
+ description the path tracer, fog and glass read. `color`, `shoreColor`, `shoreDepthTransition`,
1985
+ `scattering`, `extinction` and the optical-normal and wave fields are gone. The binary adapter is at
1986
+ version 3; the v2→v3 upgrader keeps level, IOR and roughness and gives old levels the default
1987
+ medium in place of their uncalibrated colours.
1988
+ - **New:** water media tools in `shade/renderer/medium/water/`: presets (`water_medium_preset`, from
1989
+ swimming pool to turbid river), a bio-optical model from chlorophyll, CDOM and minerals, Jerlov
1990
+ types, an inversion from a target deep colour and clarity distance, and the spectral-to-RGB fit
1991
+ they all end in. The default is clear open ocean, chlorophyll 0.3 mg/m³.
1992
+ `participating_medium_to_json` / `serialize_participating_medium` store a medium.
1993
+ - **New:** the water's colour is light, not paint: the sun, refracted and shadowed by the cascaded
1994
+ shadow map, and the sky the active GI mode reports, sent back up the refracted view ray by the water
1995
+ column with every order of scattering — Lee's shallow-water model with QAA's deep-water reflectance,
1996
+ which takes the medium's phase functions through its backscattering. Against a Monte Carlo slab
1997
+ oracle it is within 0.82–1.49 in deep water across the presets; turbid water reads brown, clear water
1998
+ blue. Light leaving the water crosses the surface by exact Fresnel and the n² law, so a
1999
+ floor seen through water reads about half as bright as it would dry. Reflections that miss on
2000
+ screen fall back to IBL or Brick4, whichever the renderer runs, as SSR's do.
2001
+ `GPUWaterRenderer.settings.inscatter_segments` (default 4) sets the steps of the in-scatter.
2002
+ - **Fixed:** the WGSL emulator read a component of `refract`'s result as an absent property, so
2003
+ `refract(...).y` was NaN on the CPU and a number on every device.
2004
+ - **New:** what is seen through water was lit through it. Surface shading stays unaware of water;
2005
+ the water optics pass rebuilds each submerged receiver's irradiance dry and under the water — the
2006
+ sun refracted and attenuated by Gordon's diffuse attenuation, shadowed, the GI mode's sky through
2007
+ the diffuse Fresnel transmittance — and scales the receiver by the ratio, so floors darken and lose
2008
+ red with depth. It costs nothing outside water-covered pixels. A lamp's light and emission are
2009
+ scaled by the same ratio, and lit particles under the surface get no downwelling yet.
2010
+ - **Changed:** a water surface is its mesh and nothing else: the water passes read no height map and no
2011
+ terrain, so bodies of any number and scale — an ocean, a mountain lake, a cup on a table — draw the
2012
+ same way. Shore water fades in by its depth over the scene behind it, and where nothing is seen
2013
+ through the water it reads as deep, to where its clearest channel keeps a thousandth.
2014
+
2015
+ ### Participating media in the path tracer
2016
+
2017
+ The path tracer and the bakes that trace with it carry light through participating media — one
2018
+ volume system for water, fog, clouds and glass (`PATH_TRACED_MEDIA_PLAN_2026_10_02.md`). A scene
2019
+ with no medium and no participating-media volume renders as it did.
2020
+
2021
+ - **New:** `ParticipatingMedium` (`shade/renderer/medium/`) describes what fills a region: RGB
2022
+ absorption in 1/m and up to four scattering lobes, each with its own phase function
2023
+ (`PhaseHenyeyGreenstein`, `PhaseCornetteShanks`, `PhaseRayleigh`, `PhaseDraine`,
2024
+ `PhaseWaterMolecular`, `PhaseFournierForand`, `PhaseJendersieDeon`). `StandardShadeMaterial.medium`
2025
+ names the medium inside a closed surface; the resident material table the ray passes read carries
2026
+ its slot (`INDIRECT_MATERIAL_STRUCT.medium`, in former padding) and a media table beside it.
2027
+ - **New:** a material with a medium is a refracting interface into it, with probability
2028
+ `transmission_factor`: exact Fresnel at the ratio of the two sides' `ior_factor`, total internal
2029
+ reflection, and the (n_from / n_to)² that radiance carries across. Media nest and overlap by
2030
+ `ParticipatingMedium.priority` — water inside a glass inside a tank, water modelled into the wall
2031
+ holding it. A path starting inside a medium, an underwater camera or probe, finds it. Without a
2032
+ medium, transmission stays thin-walled.
2033
+ - **New:** inside media and the scene's participating-media volumes, paths scatter and are absorbed
2034
+ (free flight against per-channel majorants, hero-channel spectral MIS over RGB), light is sampled
2035
+ from every scattering point, and shadow rays are attenuated by what they cross, an index-matched
2036
+ (`ior_factor` 1) boundary included. `playground/path_traced_media/` holds the device checks:
2037
+ Beer-Lambert through a slab and through glass with every internal reflection summed, an
2038
+ underwater camera, white furnaces and shadows through media, each against its closed form.
2039
+ - **New:** light reaches a surface or a scattering point through one smooth refracting interface —
2040
+ the sun on the bottom of a pool, a lamp behind a window of water — by manifold next-event
2041
+ estimation (Hanika et al. 2015): Newton steps on Snell's law find where the light refracts, and the
2042
+ beam's spreading and the surface's curvature (its interpolated normals) weigh it, so waves throw
2043
+ caustics. Every light type; at most 8 steps and 16 ray casts a connection and 4 connections a
2044
+ path. A step that overshoots — a low sun seen from under water — is shortened; a mesh between the
2045
+ surface and the interface (an index-matched box in the water) is passed; far from the origin the
2046
+ solve settles for what f32 resolves. Through flat water it meets the closed form to 0.1% for the sun
2047
+ from 30 to 85 degrees and for a lamp overhead. A surface under water facing away from a low sun is
2048
+ lit by its refracted light, and a surface inside a medium reflects by the ratio of its index to the
2049
+ medium's. `path_traced_media`'s scenes take 18 and 15 ms a 512×512 frame on an RTX 4090.
2050
+ - **Not yet:** light through two refracting interfaces — through a glass sphere, in and out — still
2051
+ reaches nothing behind them directly, nor light through a normal-mapped surface as its normal map
2052
+ bends it: connections walk the vertex normals. Where waves overlap enough that several points
2053
+ refract the same light to the same receiver, the one found stands for all. Past a path's fourth
2054
+ connection, light behind an interface is lost to it: an estimated 18–22% of the sunlight scattered
2055
+ in a milky sphere, 31–55% in deep water of albedo 0.9–0.99. A spot's cone, and a surface in air lit
2056
+ through water by a light in it, are judged along the straight line. Interfaces are smooth whatever
2057
+ the material's roughness, and alpha on a medium-bounding surface is ignored. `serialize_scene`
2058
+ keeps a material's `ior_factor` but not its `medium`, nor the scene's participating-media
2059
+ volumes: a saved scene loads with its water and glass thin-walled. Every loop a path runs
2060
+ has a fixed limit, so no thread can
2061
+ hold its group: 32 scattering events, 16 interface events (the first 4 about the interpolated
2062
+ normal, the rest about the triangle's, which lets out light a tessellated sphere would otherwise
2063
+ trap by total internal reflection near its outline), 8 pass-throughs, 64 collisions and 64 volume
2064
+ pieces a flight — 32 separate volumes on one ray, past which the rest of it is taken as one piece
2065
+ and soon cut. What a limit drops is light: 0.1% at the outline of a 5,000-triangle glass sphere
2066
+ in a white furnace, 0.5% of a scattering glass sphere (0.7% at its outline).
2067
+ - **Fixed:** the path tracer's specular bounce weighed what it reflects by `F termV / (termV +
2068
+ termL)`, which drops the NoL·NoV term of the VNDF weight G2 / G1: a smooth surface's reflections
2069
+ carried half their light, a rough one's somewhat less. A black glass tile reflecting an emitter
2070
+ read 0.020 against Fresnel's 0.040; it reads 0.03998.
2071
+ - **Fixed:** a participating-media volume scaled to nothing on an axis put every point of the world
2072
+ inside it (the first in the scene) or became a copy of the volume before it; it is left out.
2073
+ - **Faster:** the path tracer's light selection walks only the pages lights occupy — they are
2074
+ packed from row 0 — instead of every page the light tables could hold, twice per light sample.
2075
+ In `path_traced_media`'s scene without media, 7.0 → 4.8 ms a 256×256 tile on an RTX 4090.
2076
+ - **Breaking:** a shader importing `chunk_render_trace_path` binds the resident media table as the
2077
+ uniform `media` (`MEDIA_TABLE_STRUCT`, `GPUResidentMaterialContext.buffer_media`) beside
2078
+ `materials`.
2079
+ - **New WGSL:** `F_dielectric` (exact unpolarized Fresnel with total internal reflection),
2080
+ `sample_henyey_greenstein_cos`, `sample_draine`, and the Fournier-Forand phase function with its
2081
+ sampler and `fournier_forand_parameters_from_backscatter`. `sample_draine_cos` is deprecated: its
2082
+ closed form loses the distribution for small g and large alpha (CDF error 0.09 at g = 0.1,
2083
+ alpha = 250; total below g = 0.003). `select_light_importance` takes a zero normal for a point with no
2084
+ surface, inside a medium, and weighs every light by its irradiance there alone.
2085
+ - **Breaking:** a scene's participating-media volumes are rows of its scene database
2086
+ (`participating_media_volumes`), which the froxel volumetrics read through
2087
+ `chunk_participating_media_volumes_iterate`, so the ray passes can read them with no binding of their
2088
+ own. `GPUSceneVolumetrics` takes that table in its constructor and no longer has `table` or
2089
+ `metadata_buffer`; the froxel media build and far-field scattering passes bind `scene_database`
2090
+ instead of `scene_volumes` and `scene_volumetrics_metadata`.
2091
+
2092
+ ### Shadows: the cascade count is a runtime setting
2093
+
2094
+ `Renderer.shadow_cascade_count` (default 3) sets how many cascades a directional light's shadow map is
2095
+ split into, and may change at any time. A change rebuilds every directional map with the new count in
2096
+ the same frame; for the two frames its caster range takes to be measured again, a rebuilt map's near
2097
+ planes are its view slices' own, so casters between the light and a slice can drop out of it. The count
2098
+ does not change a cascade's resolution — 1536 for every cascade, see "HZB mip 0" above — and every cascade
2099
+ is a rect in the shared shadow atlas, so a count the atlas cannot fit leaves the light unshadowed
2100
+ (with a console error each frame) rather than shrinking anything.
2101
+
2102
+ - **Faster:** a shaded pixel reads a directional light's cascades one at a time, as it tests them,
2103
+ instead of copying all of them out of the light database first. Measured on an RTX 4090 (Chrome,
2104
+ D3D12) at 1898×926, `volumetrics_froxel`'s "Volume edge" view, eight paired rounds at three
2105
+ cascades: deferred shading 217 → 80 µs, volumetrics lighting 63 → 52 µs — about 0.15 ms of a
2106
+ 2.5 ms frame — with the shadow maps and the picture unchanged.
2107
+ - **Fixed:** turning shadows off now frees the shadow records along with the maps, so no
2108
+ `shadow_directional` record names cascade rows that have been given back.
2109
+ - **Breaking:** `SHADOWMAP_CSM_CASCADE_COUNT` is gone (`SHADOWMAP_CSM_DEFAULT_CASCADE_COUNT` is the
2110
+ default). In the light database, `shadow_directional` is now a range — `{first_cascade,
2111
+ cascade_count}` — over a new `shadow_cascade` table, one row per cascade holding its atlas rect, its
2112
+ projection and its caster range (`CSM_CASCADE_STRUCT`); the `shadow_csm_caster_range` table and
2113
+ `CSM_CASTER_RANGE_STRUCT` are gone, and `CSM_CASTER_RANGE_UNKNOWN` moved to `CSM_CASCADE_STRUCT.js`.
2114
+ WGSL calling `shadowmap_csm_compute_cascade_blended` passes the database and the range instead of a
2115
+ cascade array, and gets back `shadow_cascade` rows. `DirectionalLightShadowmap` takes its cascade
2116
+ count in its constructor and names its rows by `first_cascade` (was `caster_row`);
2117
+ `GPUSceneShadowmapContext.caster_range_table` is `cascade_table`.
2118
+
2119
+ ### The CPU particle system is removed
2120
+
2121
+ `ParticleEffect` + `GPUParticleEmitterSystem` is meep's particle system. The CPU one it replaced —
2122
+ the `ParticleEmitter` component, `ParticleEmitterSystem` and the `particular` engine under them,
2123
+ deprecated since 3.22.0 and 3.30.0 respectively — is gone, with nothing left to read its data.
2124
+
2125
+ - **Breaking: a save or level holding a `ParticleEmitter` no longer loads.** Its adapter and
2126
+ upgrader are unregistered, and the binary format cannot skip a type it has no adapter for, so the
2127
+ whole load fails with `No serialization adapter found for 'ParticleEmitter'` rather than dropping
2128
+ the component. Re-author such content as `ParticleEffect` before upgrading.
2129
+ - **Removed:** `ParticleEmitter` and all of `engine/graphics/particles/particular/`;
2130
+ `ParticleEmitterSystem`, its `ParticleExtension`, the `ParticleEmitterSystem3` alias and the
2131
+ `GPUParticularRenderer` that drew it; `shader_particle_render.js`, the blended billboard pipeline
2132
+ only that renderer used — so, as was already true of the GPU particles, nothing draws particles
2133
+ under `ShadeTransparencyMode.MBOIT` or with transparency off; and the `Sprite` component with
2134
+ `SpriteSystemPE` (deprecated in 3.33.0), whose only renderer was a one-particle emitter.
2135
+ - **Removed from `engine/animation`:** `shutdownParticleEmitter` and
2136
+ `stopEmitterAndNotifyOnceFinished`, so `stopEntityAndNotifyWhenStopped` waits on trails and audio
2137
+ only; `removeEntityGenericEffect` and `removeEntityWithMeshParticlesEffect`, which spawned legacy
2138
+ smoke puffs; and `removeEntityWithEffect`'s `emitter` option — it still shrinks the entity away and
2139
+ plays the sound, and spawns nothing.
2140
+ - **Moved, not removed**, since nothing about them is particular to particles:
2141
+ `distribute_points_on_indexed_triangles` to `engine/graphics/geometry/`, `lut_to_sampler` to
2142
+ `engine/graphics/texture/sampler/`, `MovingBoundingBox` to `core/geom/3d/aabb/`, and the
2143
+ `RGB_LUT_INFERNO`, `RGB_LUT_TURBO`, `RGB_LUT_VIRIDIS` and `RGBA_LUT_HEATMAP_IR` colour maps to
2144
+ `core/math/lookup/`. `ParameterLookupTable` already lived in `core/math/lookup/`; the re-exports at
2145
+ its old particle path are gone.
2146
+ - **Editor:** both editor shells (`editor/prototypeEditorShell.js` and the scene editor) register
2147
+ `GPUParticleEmitterSystem` in place of the CPU system. The "Particle Emitter" template is now
2148
+ "Particle Effect" and places the particle editor's starter plume, and `ParticleEffect` entities
2149
+ get the positioned marker and the entity-list icon. The legacy emitter's inspectors, its
2150
+ emission-shape symbol and `ParticleEmitterRebuildProcess` are gone. There is no dedicated
2151
+ `ParticleEffect` inspector yet; the reflection inspector shows its scalar fields.
2152
+
2153
+ Migrating: register `new GPUParticleEmitterSystem(graphics, scene, assetManager)` — unlike the CPU
2154
+ system it takes the Shade scene its emitters are placed in — and author effects as `ParticleEffect`
2155
+ (`shade/renderer/particles/DESIGN.md`). An emitter with several layers becomes one entity per layer,
2156
+ since an effect has one texture and one rate; a one-shot (`emissionImmediate`) is
2157
+ `GPUParticleEmitterSystem#burst(entity, count)`.
2158
+
2159
+ ### Networking: smooth by default
2160
+
2161
+ A game that wires `NetworkSession` the obvious way now renders smoothly at any display rate. Drawn at
2162
+ 144 Hz on a 60 Hz step, the previous tree left 58% of drawn frames of a predicted player — and of
2163
+ everything on the host — not moving at all, then jumping a step; now no view stalls or doubles a
2164
+ frame on a clean link, with packets arriving the way a browser delivers them. The design is in
2165
+ `engine/network/SMOOTHNESS_DESIGN.md`, the summary in the network README's "Smooth by default".
2166
+
2167
+ - **Breaking: `NetworkSession.tick(dt)` is deprecated and does nothing** (it warns once). The session
2168
+ runs on the `EntityManager`'s fixed step — `entity_manager.update(dt)`, which the engine loop
2169
+ already calls, drives it. Remove the call; code that drove a session without an engine loop must
2170
+ call `entity_manager.update(dt)` instead. `tick_rate_hz`, when given, now sets the engine's
2171
+ `fixedUpdateStepSize`; omitted, the session uses the engine's step.
2172
+ - **Behaviour change: what arrives from the network is applied as the next fixed step opens**, not
2173
+ when the transport delivers it. A browser delivers packets whenever its event loop gets to them —
2174
+ usually between a frame's render blend and its draw — and applying them there drew most client
2175
+ frames unblended. Code (tests, mostly) that delivers packets and then reads the result must run a
2176
+ step of the receiving engine in between. A transport's lost connection is reported in order after
2177
+ the packets that preceded it. `connect()` for a peer id first takes whatever the id's previous
2178
+ transport still held, so a reconnect is never refused by a drop that has not been read yet.
2179
+ - **Behaviour change: the session's timers run on the engine's frame time** — the render playout,
2180
+ idle reaping and the grace window all advance with the `dt` given to `entity_manager.update`, not
2181
+ with `performance.now()`. A paused engine pauses them; an engine driven by hand drives them.
2182
+ - **Replication says how each component is drawn, and how a client corrects it**, with two methods
2183
+ and no optional parameters: `replicate_interpolated(C, interpoland, correction)` blends `C` with
2184
+ the given `Interpoland`, its record layout and a Linear blend; `replicate_discrete(C, correction)`
2185
+ shows each state as it lands. `correction` is a `Correction` (`network/correction`): how the
2186
+ difference between the host's state and the prediction is compared and carried into the present.
2187
+ Given: `HOST_VALUE_CORRECTION` (take the host's value, for what a client does not predict),
2188
+ `POSE_CORRECTION` and `VELOCITY_CORRECTION` (`network/adapters`); a game writes its own for its
2189
+ own components. For `Transform64`, `replicate_transform64(session)` in `network/adapters` is
2190
+ `replicate_interpolated(Transform64, POSE_INTERPOLAND, POSE_CORRECTION)`. `replicate()` is
2191
+ deprecated: it keeps its meaning (`replicate(C)` discrete, `replicate(C, adapter)` blended over the
2192
+ component's replication layout), corrects by the host's value, and warns once. **Breaking:** a component blended
2193
+ between ticks, by either, must have `copy(other)` — its simulated value is held with it while a
2194
+ blend is drawn over it; held through the wire's layout, it came back rounded every step. The
2195
+ interpolands of one session's components must have distinct type ids; `start()` checks.
2196
+ - **Behaviour change: networked entities are interpolated without being asked.** The session adds an
2197
+ `InterpolationSystem` if the engine has none and attaches (or adapts) an `Interpolated` on every
2198
+ networked entity, by role: the local fixed-step timeline on the host and for what a client owns,
2199
+ the network's render-delayed playout for the rest.
2200
+ - **Behaviour change: an interpolated entity is simulated at the fixed step.** `InterpolationSystem`
2201
+ now records and restores by itself, for every `Interpolated` entity, not only physics bodies: when a
2202
+ step opens, every value a render blend displaced is put back — and so is anything written over the
2203
+ blend since. A write between steps is a write to a drawn pose. To change the simulation between
2204
+ steps, set `Interpolated.snap` (a teleport: shown at once, and blending resumes from it —
2205
+ `PhysicsSystem.setPose` does this), or call `InterpolationSystem.restore()` (or
2206
+ `session.normalize_if_dirty()`) first and write the truth. A render-rate read of an interpolated
2207
+ `Transform64` returns the drawn pose. This was already the rule for interpolated physics bodies;
2208
+ it is now the rule for every interpolated entity, and it is what keeps a render-phase writer
2209
+ (`ClingToTerrainSystem` on a blended pose) from leaking the blend's lag into the simulation.
2210
+ `PhysicsSystem.interpolationLog` is deprecated and ignored (warns once); remove the assignment.
2211
+ - **Breaking: the blend fraction is passed in an array.** `BinaryInterpolationAdapter.interpolate(
2212
+ out_buffer, source, first_offset, second_offset, blend, blend_offset)` reads it at
2213
+ `blend[blend_offset]`, and `InterpolationLog.interpolate` takes the same `blend, blend_offset` pair
2214
+ where it took `t`. As a number it crossed both calls boxed, 16 bytes a body every frame. An adapter
2215
+ of your own starts with `const t = blend[blend_offset];`.
2216
+ - **Behaviour change: clients reconcile by comparison** (`reconcile: 'correct'`, default): the host's
2217
+ state for frame S against the prediction for S; a component that differs is carried into the
2218
+ present by its `Correction` — a pose by its offset, turn in the body's own frame and scale ratio,
2219
+ hidden by a decaying render offset. Pass `reconcile: 'replay'` for the previous rewind + replay. A
2220
+ client now predicts with each sampled input as the host will apply it, after the wire's rounding.
2221
+ New: `correction_count`, `correction_skipped_count`, `onCorrection(entity, frame, component_class,
2222
+ magnitude)`.
2223
+ - **Behaviour change: a client sends no input until its host has said which world the client is
2224
+ in.** Each `connect()` holds the client's action stream until the host answers: a
2225
+ `RESUME_ACCEPT` releases all of it, input predicted while cut off included, as before; an
2226
+ `INITIAL_SYNC` drops everything predicted before it, and the client's input starts at the frame
2227
+ the sync sets its counter to — on that connection and on any later one that resumes the same
2228
+ world. A host that resynced a client used to apply the input the client had predicted, while cut
2229
+ off, on the world the snapshot replaced — late and all at once, carrying the player on from where
2230
+ the snapshot had just put it — and a predicting client's first input, sampled before its first
2231
+ sync on a world of its own, cost it a correction as it joined. The wire is unchanged. See the
2232
+ network README's "Connecting, and coming back".
2233
+ - **Fixed: a peer could make the host send it the whole world every tick.** The host answered every
2234
+ `RESUME_HELLO`, and answered one claiming a session it held no record of — any connected peer can
2235
+ send one — with a fresh `INITIAL_SYNC`. A small packet each tick kept the host serializing and
2236
+ sending its world each tick: with 500 entities, some 600 times the bytes the peer sent. The host
2237
+ now answers a connection's `RESUME_HELLO` once and ignores any later one on it (ignores, not
2238
+ kicks: a datagram can arrive twice), and sends a connection its `INITIAL_SYNC`, copies against
2239
+ loss included, once — when it takes the connection for a fresh peer, as it binds it or after
2240
+ refusing its claim to resume — however many `RESUME_HELLO`s follow. So a client readmitted past
2241
+ its grace gets the two copies a new one does, not the two more a reject used to add; and a
2242
+ reconnect that lands on a connection the host still holds, rather than on a new one, is not sent
2243
+ the world again. The wire is unchanged.
2244
+ - **Fixed: a peer could make the host re-send a fragmented message hundreds of times over.** A
2245
+ `NACK_FRAGMENT` re-sent a chunk as often as it named it, and may name one 255 times; the host
2246
+ answers three NACKs per message. With 500 entities, NACKs for the two `INITIAL_SYNC`s cost the host
2247
+ about 340 times the bytes the peer sent, some 55 serialized worlds per connection. A NACK now
2248
+ re-sends each chunk of the message at most once, and nothing for an index the message does not
2249
+ have, so repair costs at most three copies of a message the host chose to send. The other way
2250
+ round, a receiver asked for every chunk a fragment claimed its message had: one empty fragment
2251
+ claiming 255 drew a NACK for 254 each round, some fifty times its size. A NACK now asks for no more
2252
+ chunks than its message has delivered; a message that has lost more than that is asked for the
2253
+ rest over the following rounds. A chunk that is really lost is still asked for and re-sent. The
2254
+ wire is unchanged.
2255
+ - **Behaviour change: the host publishes what its simulation changed** at the end of each step
2256
+ (`publish_changes: true`, default) and applies late input in the current frame instead of rolling
2257
+ back (`late_input: 'apply'`, default; `'rollback'` restores it). Publishing never writes the wire's
2258
+ quantised value back into the host's state, and sends nothing an action already carried.
2259
+ - **Breaking: peer ids are `[0, 252]`.** 253 is now `SENDER_RECORDED`, the provenance of a record
2260
+ written by `SimActionExecutor.record()` — state published, not applied — which a rollback replay
2261
+ carries over unchanged instead of dropping.
2262
+ - New: `System.openFixedStep()` / `closeFixedStep()` — phases bracketing each fixed step, for a system
2263
+ that frames the step (the interpolation system settles, the network opens and closes its frame);
2264
+ `NetworkSystem`'s `driver` (`open_step`, `close_step`, `frame`) for driving a bare `NetworkPeer` on
2265
+ the engine's clock; `EntityManager.fixedStepTimeScale`; `InterpolationSystem.settle()`,
2266
+ `restore()`, `correct()`, `allocateSourceId()`; `InterpolationLog.rewrite()`,
2267
+ `latest_tick_at_or_before()`, `ensure_ticks()`; `ServerAuthoritativeServer.begin_tick()` /
2268
+ `end_tick()`, `open_frame`, `onBeforeTickClose`, `late_input`; `ServerAuthoritativeClient.begin_tick()`
2269
+ / `end_tick()`, `auto_reconcile`, `reconcile()`; `SimActionExecutor.record()`,
2270
+ `on_component_written`; `SmoothingState.absorb_body_orientation_correction()`,
2271
+ `render_body_orientation()`, `is_settled()`, time-paced `decay(entity, dt)`; `PredictionHistory`;
2272
+ `DeferredTransport`; `LoopbackTransport.deliver_next()`; `NetworkPeer.hold_outbound()` /
2273
+ `release_outbound()`.
2274
+ - Wire: a new `FRAME_MARKER` packet (type 13) replaces the client's empty per-tick keep-alive. Older
2275
+ peers drop it as an unknown type; without it the host reads an input-less client's buffer depth
2276
+ as zero and asks it to run fast.
2277
+ - Fixed: network writes to `Transform64` are announced (mesh placement follows only announced
2278
+ transforms) — the network announces every in-place write through the component's replication
2279
+ adapter, `BinaryClassSerializationAdapter.announce(dataset, entity)` (new; nothing by default), and
2280
+ `TransformReplicationAdapter` announces a Transform64; a custom Transform64 wire adapter has to as
2281
+ well; a record is never sent back to the peer it came from (a client echoed the host's
2282
+ state back to it every tick, and the host applied it for client-owned entities); the playout no
2283
+ longer starts one frame behind the stream; the network timeline holds across frames that carried
2284
+ no change instead of showing the newest state early; an entity spawned during a fixed step is
2285
+ interpolated from its birth tick (GAP-036); the interpolation system runs before pose readers such
2286
+ as the camera;
2287
+ an authoritative state arriving after a newer one no longer corrects the present a second time;
2288
+ a resync no longer compares against predictions made for the world it replaced; a change the
2289
+ simulation made after an action wrote the same component in that step is published; the
2290
+ interpolation truth is held losslessly (not through a wire adapter's rounding); an entity id
2291
+ reused within the log's history does not blend from the previous owner's records; a log grown past
2292
+ its logical capacity keeps every tick; a scale difference is reconciled; the prediction comparison
2293
+ tolerates Float32 rounding at any distance from the origin; an action whose `apply` throws leaves
2294
+ neither a half-written record nor a half-applied write, and a listener that throws on a write's
2295
+ announcement cannot leave the frame half-written; a host input that fails to apply is reported and
2296
+ skipped instead of leaving the frame open, and a `begin_tick` that fails part-way closes the frame
2297
+ it opened; a client that reconnects after its host's grace has run out is resynced once — the
2298
+ `RESUME_REJECT` that can trail the host's snapshot no longer throws away the token the snapshot
2299
+ issued (a link lost before the next copy arrived then cost the client its session) or reports a
2300
+ second reconnection, and the copies of an `INITIAL_SYNC` the host sends against loss are no
2301
+ longer applied over a world the client is already predicting on (a correction just after every
2302
+ join and resync).
2303
+
2304
+ - **Virtual geometry: a `deflate` page codec**, id `2` on the frame header — one bare raw DEFLATE
2305
+ stream per page through native `CompressionStream`/`DecompressionStream`, no zlib or gzip
2306
+ wrapper. It stores a container in fewer bytes than LZ4 — 1.369x against 1.295x over the pages of
2307
+ a 240×120 torus — for about four and a half times the decode, 126 MB/s of decoded output against
2308
+ 562. `format_version` and `min_reader_version` are unchanged: a codec is a per-asset capability
2309
+ gate, and a reader without a decoder for one refuses the asset rather than misreading it.
2310
+ - **Breaking default:** `VGeoBuildOptions.codec` and `VGeoWriteOptions.codec` are now
2311
+ `VGEO_CODEC_DEFLATE`, so `vgeo_build` and `gltf_to_vgeo` write deflate unless told otherwise. A
2312
+ build happens once and a fetch happens forever, so the denser side of the trade is what goes in
2313
+ unasked. **A container converted from here on opens only in a reader carrying the deflate
2314
+ decoder** — older readers refuse it cleanly rather than misreading it, but they do refuse it. Pass
2315
+ `VGEO_CODEC_LZ4` or `--codec lz4` for a container whose pages arrive during play, where that
2316
+ decode is a page's arrival cost rather than a build's. `codec_level` applies to LZ4 only and now
2317
+ does nothing under the default codec.
2318
+ - **Async API change:** the platform offers its zlib only as a stream, so the virtual-geometry
2319
+ functions that touch a page blob now return promises: `vgeo_build`, `vgeo_write_container`,
2320
+ `vgeo_validate_container`, `vgeo_read_page`, `vgeo_encode_frame_blob` and
2321
+ `vgeo_decode_frame_blob`. Await them. `VGeoContainerReader`, `VGeoPageFetcherCore` and the page
2322
+ worker were already asynchronous and are unchanged at their own boundaries.
2323
+ - New in `core/binary/compression`: `compress_bytes` (the mirror of `decompress_bytes`),
2324
+ `decompress_bytes_into` (decompresses into a buffer the caller owns and refuses a stream the
2325
+ moment it would write past it — deflate reaches 1032:1, so checking a length afterwards is an
2326
+ attacker-chosen allocation) and `deflate_decompress_bound`.
2327
+ - Replace the `pako` dependency with native `DecompressionStream` (Node >= 24 and browsers with
2328
+ shipping WebGPU support).
2329
+ - **Async API change:** `PNGReader.parse()`, `decodeChunk()` and `decodePixels()`, `png_inflate`,
2330
+ `png_chunk_decode_iTXt`, `png_chunk_decode_zTXt`, USD's `decode_png`, `ktx2_read`, and
2331
+ `texel_data_from_ktx2` now return promises. Await their results and handle failures as rejections.
2332
+ Keep input bytes unchanged and do not reuse a PNG reader until its operation settles. The image
2333
+ worker and glTF loader retain their existing asynchronous interfaces.
2334
+ - PNG pixels retain their bad-Adler32 compatibility fallback. Compressed PNG metadata and KTX2
2335
+ ZLIB levels remain strict. Native decoding also rejects trailing compressed-stream data and
2336
+ gzip data supplied where the file format requires zlib.
2337
+
2338
+ ### Trails draw inside the transparency pipeline
2339
+
2340
+ A `Trail3D` used to be blended over the finished frame after transparency had resolved, tested
2341
+ against the opaque scene alone — so a trail behind a transparent card, a glass pane or a cloud of
2342
+ smoke was drawn on top of it. It is now drawn through AVBOIT beside the transparent meshes and the
2343
+ GPU particles, and composites against both by depth, order-independently. Design and verification:
2344
+ the AVBOIT ledger, entry 22.
2345
+
2346
+ - **Behaviour change: trails composite against transparency.** A trail is hidden and tinted by the
2347
+ transparent surfaces and particles in front of it and hides what is behind it, within AVBOIT's own
2348
+ limits (two surfaces in one depth slice blend rather than sort; a trail thinner than a voxel of the
2349
+ 1/8-resolution volume hides what is behind it by only about the share of the voxel it covers —
2350
+ steadily, as it moves, not in blocks: its splat is multisampled and weighted by coverage, AVBOIT
2351
+ ledger entry 24). Opacity is unchanged — both faces
2352
+ of the tube still draw. Trails now also take the volumetric fog at their depth, which they did not
2353
+ before. Fog of war covers them as it did.
2354
+ - **Behaviour change: what is drawn at `AfterTransparency` now lands over trails.** Trails used to be
2355
+ the last content extension there; they are resolved before that phase now, so path display, the
2356
+ selection highlight and the deprecated CPU `ParticleEmitterSystem` draw over a trail whatever its
2357
+ depth, where they used to draw under it. The GPU particles, being in the pipeline with it, sort
2358
+ against it properly.
2359
+ - **Behaviour change: trails are not drawn under `ShadeTransparencyMode.MBOIT`**, nor with
2360
+ transparency off — the same as the GPU particles. MBOIT is the legacy mode.
2361
+ - **Cost:** a frame with a live trail runs the transparency pipeline even when nothing else in it is
2362
+ transparent — its fixed passes (occupancy, warp, integration, resolve) where there used to be one
2363
+ blended pass; of the order of 0.3 ms at 720p on light content (AVBOIT ledger, entry 17). A trail
2364
+ also shares the pipeline's 128 depth slices with every transparent surface on screen, as an emitter
2365
+ does: a long one spanning a deep range makes the slices coarser for all of them while it is in
2366
+ view. One outside the view frustum costs nothing of either.
2367
+ - **Deprecated: `TrailExtension`** (`engine/graphics3/Trail3DSystem.js`) draws nothing and warns once
2368
+ when constructed. It was the system's own `AfterTransparency` extension; nothing needs to register
2369
+ it. Kept so that imports of it still link; the removal candidate for the next major.
2370
+ - New: `Renderer.add_transparency_channel(channel)`, `remove_transparency_channel(channel)` and
2371
+ `transparency_channel_count()`, and the same three on `GraphicsEngine`, which holds channels across
2372
+ a restart as it holds extensions. A channel is an `AVBOITSideChannel` — what an extension cannot be:
2373
+ something drawn inside the transparency pipeline rather than over it. Registering one twice keeps
2374
+ one. It may implement the new, optional `avboit_is_empty()`, and is left out of any frame in which it
2375
+ says so; under MBOIT or with transparency off, channels are not consulted at all.
2376
+ - New: `DynamicMeshAVBOITChannel` (`shade/renderer/dynamic/`) draws a `DynamicMeshBatch` as such a
2377
+ channel — straight alpha, unlit, fogged, depth-tested against the opaque scene and never written. It
2378
+ owns the batch's GPU buffers: `release(geometry)` for a mesh gone for good, `destroy()` for all.
2379
+ - Fixed, on the way: dynamic-mesh GPU buffers are dropped and made again when the device changes. A
2380
+ `GraphicsEngine` stopped and started under systems that keep running — an `Engine` with an external
2381
+ `EntityManager` — used to go on writing and binding path display's, debug draw's and trails'
2382
+ buffers made on the old device: a validation error that loses the frame's commands, on every frame
2383
+ that had one of them to draw.
2384
+ - Internal: `GPUDynamicMeshRenderer`'s buffers and upload are `GPUDynamicMeshResidencyTable` now, and
2385
+ its vertex pull is `chunk_dynamic_mesh_vertex`; the particle side channel's coverage clamp,
2386
+ contribution routing, box depth interval and slice marking are shared chunks. These moves change
2387
+ nothing on screen for path display, debug draw or the particles.
2388
+
2389
+ ### Rendering
2390
+
2391
+ - **Fixed: AVBOIT striped transparent surfaces tilted away from the camera.** Over another transparent
2392
+ layer such a surface occluded itself through the neighbouring voxel columns, by an amount that depended on
2393
+ where each pixel sat in its voxel: an 8-pixel sawtooth, from about 30° at 1080p, 3–5 sRGB codes on the
2394
+ device at 45–60°. The lookup now looks a voxel's width of the fragment's own depth change further toward
2395
+ the camera (`avboit_footprint_bias`, capped at the coarsest slicing's bias); a tilted surface is not
2396
+ occluded by what lies within that band in front of it, the depth the volume cannot tell from the surface
2397
+ itself. AVBOIT ledger entry 26.
2398
+ - **Breaking (transparency channels):** a channel whose fragments splat into the volume adds
2399
+ `avboit_footprint_bias` to its lookup: `avboit_sample_transmittance` and
2400
+ `avboit_integral_depth_coordinate` take a `footprint_bias`, `0` for a fragment that writes nothing into
2401
+ the volume.
2402
+ - **New:** the AVBOIT warp tries virtual resolutions between octaves, so the occupied slices fill more of
2403
+ the budget: 1.28–1.43× finer depth slices on average where the warp halves, 1.68× in the bonfire, for
2404
+ about a microsecond per resolution tried. `Renderer.avboit.warp_steps_per_octave`, 4 by default; 1
2405
+ halves as before. AVBOIT ledger entries 27 and 29.
2406
+ - **Faster:** the AVBOIT warp build is one workgroup instead of one invocation, with subgroup reductions
2407
+ and a subgroup scan for the LUT: 3–9 µs on an RTX 4090 where it took 22.5–136 µs (317 µs with
2408
+ `warp_steps_per_octave` 4 over fragmented occupancy), bit-exact. AVBOIT ledger entry 28.
2409
+ - **Fixed: a soft particle standing on a floor occluded what was behind it while drawn faded.** The AVBOIT
2410
+ splat read the soft fade at its voxel's top-left pixel; on a floor at a distance that pixel is a good part
2411
+ of the fade range farther away, and the volume recorded up to 2.4× the coverage the draw used. It now
2412
+ averages four reads over the voxel's footprint.
2413
+ - **Fixed: a view with nothing opaque to draw never cleared its depth or visibility buffer.** A scene
2414
+ with no opaque and no alpha-tested geometry — an empty one, one holding only transparent surfaces,
2415
+ one whose last mesh was just removed — drew over whatever the renderer's own targets last held,
2416
+ since they outlive the frame. A removed mesh went on hiding what had stood behind it; solid dynamic
2417
+ meshes (`GPUDynamicMeshRenderer.opaque`) failed the depth test against their own depth from two
2418
+ frames earlier and vanished from the third frame on. Shadow maps rasterize the same way: a shadow
2419
+ view with nothing to draw left its rectangle of the atlas as it found it, and a point light's cube
2420
+ faces as the texture pool handed them over. Such a view now costs one clear, where its first draw
2421
+ would have cleared.
2422
+ - **Breaking: `GPUDynamicMeshRenderer.graph_draw` returns `{ color, depth }`**, not the colour handle.
2423
+ An extension publishes both: `scene.color = drawn.color; view.depth = drawn.depth;`
2424
+ (RENDERER_CONTRACT §6). With `opaque` set the pass writes the scene depth, and when only the colour
2425
+ came back, every later reader of `ViewTextures.depth` read a version the draw had superseded —
2426
+ depth of field, motion blur, the NSS upscaler, the next extension. A development build refused
2427
+ such a frame at compile. With `opaque` left false the depth comes back as it went in, and
2428
+ publishing it changes nothing.
2429
+ - **Faster: a `DynamicMesh` draw costs a fraction of what it did on the CPU.** Each draw used to take
2430
+ a transient uniform buffer, a copy and a bind group of its own every frame — twice over for a trail,
2431
+ whose AVBOIT splat and draw each made their own. The draws' constants now go up as a table: one
2432
+ transient buffer and one write per 1,024 draws, sized to one of five classes so that a mesh count
2433
+ changing every frame does not keep making buffers, and read by both AVBOIT passes. A draw names its
2434
+ row by its `first_instance` and binds its geometry's group, which lasts until the geometry outgrows
2435
+ its buffers. Every row is still written every frame. Measured with 1,500 animated meshes at
2436
+ 1280×720 on an RTX 4090, stripped of asserts, paired, CPU per frame: 4.3–4.6 ms to 1.3–1.4 opaque,
2437
+ 4.1–4.9 to 1.3 blended, 9.8–11.4 to 2.0–2.5 through AVBOIT; 5,000 blended meshes, 20 ms to 2.2–2.4;
2438
+ 50, about the same (0.82–1.00 of before). The pictures are bit-identical. No change in the GPU's
2439
+ time on the passes could be seen past the per-run noise of its clocks.
2440
+ - **Breaking, for anything that builds its own pipeline on `DYNAMIC_MESH_RESOURCES` or draws a
2441
+ `GPUDynamicMeshResidency` itself:** the resources are two groups, `DYNAMIC_MESH_BIND_GROUP_DRAWS`
2442
+ (`camera`, and `draws`, the table) and `DYNAMIC_MESH_BIND_GROUP_GEOMETRY` (`vertices`,
2443
+ `indices`). `dynamic_mesh_vertex` and `dynamic_mesh_avboit_vertex` take the draw's row after the
2444
+ vertex index — pass them `@builtin(instance_index)`. The AVBOIT splat's and draw's own groups moved
2445
+ up one: `DYNAMIC_MESH_AVBOIT_SPLAT_BIND_GROUP_PASS` is 2, `DYNAMIC_MESH_AVBOIT_SPLAT_BIND_GROUP_VOLUME`
2446
+ 3, `DYNAMIC_MESH_AVBOIT_DRAW_BIND_GROUP_PASS` 2. `GPUDynamicMeshResidency.create_bindings` is gone,
2447
+ and its `draw` takes the draw's row where it took the bind groups. `GPUDynamicMeshDrawList` writes
2448
+ and draws a frame's meshes.
2449
+ - New: `GraphicsContext.setPipelineBindGroup`, which sets one group of a pipeline's layout and leaves
2450
+ the others.
2451
+ - `SoftwareGPURenderPassEncoder` records where a direct draw starts: `first_vertex` and
2452
+ `first_instance` for `draw`; `first_index`, `base_vertex` and `first_instance` for
2453
+ `drawIndexed`. It used to drop them.
2454
+
2455
+ ## 4.0.0
2456
+
2457
+ **The legacy `SoundEmitter` audio path is retired.** sopra had already been the only renderer for a
2458
+ while; what remained was a data model and the translation layer that fed it. Both are gone, and the
2459
+ native `AudioEmitter` path is the one the engine registers, serializes, edits and converts into.
2460
+
2461
+ ### Removed
2462
+
2463
+ - **`SoundEmitterSystem`** and `SoundEmitterComponentContext`, **`SoundControllerSystem`**,
2464
+ `loadSoundTrackAsset`, and the editor's `SoundEmitterSymbolicDisplay`.
2465
+ - **`SoundEmitterChannels`** — replaced by `SopraDefaultBus`, whose ids are identical
2466
+ (`effects`/`music`/`ambient`, plus `master`). The deprecated `get/setChannelVolume` proxies go with
2467
+ the system; they were 1-line forwards to `sopra.getBusVolume`/`setBusVolume`, which is now the
2468
+ place to read and write a channel's volume.
2469
+ - **`SoundEmitter.fadeOutAllTracks` / `stopAllTracks`**, `SoundTrack.on.ended`, the live `time`
2470
+ seek, `setVolumeOverTime`, and both throwing `nodes` getters.
2471
+ - **`createSound({track})`** — the legacy `SoundTrack` input. Pass `{url}` or `{event}`.
2472
+
2473
+ ### Moved
2474
+
2475
+ - `SoundEmitter`, `SoundTrack` and `SoundController` — with the v2 adapter and both upgraders — from
2476
+ `engine/sound/ecs/emitter/` and `engine/sound/ecs/` to **`engine/sound/sopra/legacy/`**. They are
2477
+ now frozen, data-only readers: no system links them and nothing plays them. They stay registered in
2478
+ `populateEngineSerializationRegistry` so that saves written before this release still deserialize.
2479
+
2480
+ ### Added
2481
+
2482
+ - **`Renderer.transparency_mode`** and `ShadeTransparencyMode` — selects between the moment-based OIT
2483
+ (`MBOIT`, the previous default) and the new adaptive voxel-based OIT (`AVBOIT`, Drobot SIGGRAPH 2025, now
2484
+ the default) in
2485
+ `shade/renderer/rasterize/native/avboit/`: extinction splatted into a 1/8-resolution voxel volume whose
2486
+ depth axis is adaptively packed onto 128 slices, integrated into a 3D transmittance lookup. Depth-complexity
2487
+ invariant, filters the background per channel for transmissive materials (opt-in through
2488
+ `Renderer.avboit.transmission_tint_from_base_color`), needs no `float32-blendable`. Plan and ledger sit
2489
+ next to the code.
2490
+ - **`AudioEventTrigger`** + `AudioEventTriggerSystem` — plays authored events in response to entity
2491
+ events, the native replacement for `SoundController`.
2492
+ - **`AudioEmitter.on.ended`** — fires when a direct one-shot finishes on its own, so entity builders
2493
+ can react to a sound ending without reaching into the system.
2494
+ - **`convertLegacySoundComponents(dataset, {knownBusIds})`** in `engine/sound/sopra/legacy/` — turns
2495
+ loaded `SoundEmitter`/`SoundController` components into `AudioEmitter`/`AudioEventTrigger`.
2496
+ - `AudioEmitter` and `AudioEventTrigger` serialization, registered in the engine registry (which now
2497
+ also populates the sopra and acoustic definition types).
2498
+ - An `AudioEmitter` symbolic display and positioned marker in the editor.
2499
+ - A graceful-shutdown handler for `AudioEmitter` in `hideEntityGracefully`, and
2500
+ `stopAudioEmitterAndNotifyOnceFinished` in `AnimatedActions`.
2501
+
2502
+ ### Renamed
2503
+
2504
+ - **The transition-era `3` suffix is dropped from every graphics system and from the engine facade.**
2505
+ It marked the 2.x → 3.x renderer port and disambiguated against 2.x classes that no longer exist,
2506
+ so it had stopped distinguishing anything. `GraphicsEngine3` → `GraphicsEngine`; and
2507
+ `AnimationGraphSystem3`, `AnimationSystem3`, `CameraSystem3`, `DebugDrawSystem3`, `DecalSystem3`,
2508
+ `FogOfWarSystem3`, `HighlightSystem3`, `LightSystem3`, `MeshSystem3`, `ParticipatingMediaSystem3`,
2509
+ `ParticleEmitterSystem3`, `PathDisplaySystem3`, `PickingSystem3`, `ShadedGeometrySystem3`,
2510
+ `TerrainSystem3`, `TooltipComponentSystem3`, `Trail3DSystem3`, `VolumetricLightMapSystem3` and
2511
+ `WaterSystem3` each lose theirs. Files move with the classes (`MeshSystem3.js` → `MeshSystem.js`).
2512
+ - **Nothing breaks in this release.** Every old module still exists and exports the old name as a
2513
+ deprecated alias *of the same class object*, so `instanceof` and `EntityManager.getSystem` keep
2514
+ working whichever spelling a caller uses. The aliases are the removal candidate for the next major.
2515
+ - The directory is **still `engine/graphics3/`**. Dropping its `3` would mean merging into
2516
+ `engine/graphics/`, which is a different and still-live tree; that is a separate change.
2517
+ - `graphics3/TerrainSystem` and `ecs/terrain/ecs/TerrainSystem` now share a class name. They are
2518
+ unrelated classes in unrelated modules and nothing imports both, but code that wants both has to
2519
+ alias one.
2520
+
2521
+ ### Migrating
2522
+
2523
+ 1. Register `AudioEmitterSystem` and `AudioEventTriggerSystem` in place of `SoundEmitterSystem` and
2524
+ `SoundControllerSystem`. Keep `SoundListenerSystem` — it is untouched by this change.
2525
+ `AudioEmitterSystem` is now the single owner of the per-frame sopra tick and listener pose.
2526
+ 2. Call `convertLegacySoundComponents(dataset, { knownBusIds: Object.values(SopraDefaultBus) })`
2527
+ after deserializing a save or scene. Without it, loaded legacy components sit inert.
2528
+ 3. Author new content as `AudioEmitter` (an `EventDescription` per emitter) or `AudioEventTrigger`
2529
+ (rules of `{ startEvent, stopEvent, event }`). A random pick is a `RandomContainerAudioClip`,
2530
+ looping is the clip's loop flag, per-sound volume is `gainDb`, the channel is `busId`.
2531
+ 4. Swap `SoundEmitterChannels` imports for `SopraDefaultBus`, and channel-volume calls for
2532
+ `getSystem(AudioEmitterSystem).sopra.getBusVolume`/`setBusVolume`.
2533
+ 5. Drop the `3` from graphics system and `GraphicsEngine` imports (see **Renamed**). Optional in
2534
+ this release — the old spellings still resolve — but they are deprecated.
2535
+
2536
+ ## 3.1.0
2537
+
2538
+ **meep renders on WebGPU. three.js is gone.**
2539
+
2540
+ The renderer is **Shade** — GPU-driven, meshlet-clustered, visibility-buffer deferred, clustered
2541
+ lighting. See [the renderer contract](src/shade/RENDERER_CONTRACT.md).
2542
+
2543
+ Upgrading from 2.x is a breaking change with no compatibility layer. See
2544
+ [MIGRATION_2_TO_3.md](MIGRATION_2_TO_3.md).
2545
+
2546
+ ### Requirements
2547
+
2548
+ - **WebGPU**, with `indirect-first-instance`, `float32-blendable`, ≥10 storage buffers per shader
2549
+ stage and ≥32 colour-attachment bytes per sample. Below that the renderer refuses to start with a
2550
+ message rather than degrading.
2551
+ - No WebGL fallback, no backend abstraction, no degradation tiers.
2552
+
2553
+ ### Removed
2554
+
2555
+ - **three.js**, and the peer dependency on it. `@types/three` with it.
2556
+ - The WebGL renderer and everything only it reached: the forward+ stack, the compositor, frame
2557
+ buffers and post effects, shadow maps, impostor bakers, three materials, the legacy fog of war and
2558
+ glTF loader, the WebGL minimap, `WebGLRendererPool`, `WebGLContextMonitor`.
2559
+ - **v0/v1 mesh pipelines** — `Renderable`/`RenderSystem`, `Mesh`/`MeshSystem` — and
2560
+ `MicronRenderPlugin`.
2561
+ - **`AnimationSystem`, `AnimationGraphSystem`, the `AnimationGraph` component**, and the ribbon trail
2562
+ path (`Trail2DSystem`, `RibbonX`).
2563
+ - **`LightSystem`, `CameraSystem`**, the legacy `ParticleEmitterSystem`.
2564
+ - meep's **lightmap, LPG and LPV bake pipelines** and the material-injection layer under them.
2565
+ - **DDS and DRACO** support.
2566
+ - The **three-material apparatus** — `engine/asset/loaders/material`, `engine/graphics/material`.
2567
+ - **`TopoMesh`** and the topology structures around it.
2568
+ - The **attachment socket API** — `Attachment`, `AttachmentSystem`, `AttachmentBinding` and its two
2569
+ subclasses, `AttachmentSockets`/`AttachmentSocket`/`BoneAttachmentSocket`/`AttachmentSocketType`,
2570
+ `AttachmentSocketsAssetLoader` and `GameAssetType.AttachmentSockets`. It predated
2571
+ `TransformAttachment` and duplicated it for the non-bone case. The editor's socket gizmo goes with
2572
+ it.
2573
+ - The **`Timer` component and `TimerSystem`**, along with `Timer.createTimeoutPromise`. Superseded by
2574
+ `BehaviorSystem`: `DelayBehavior` waits, `ActionBehavior` fires, `DieBehavior` cleans up.
2575
+ `createTimer` keeps its signature and is built on those.
2576
+ - **`gl-matrix`** as a dependency.
2577
+ - Declaration files are no longer tracked or shipped in the source tree. `npm run generate-types`
2578
+ emits them into `publish/src`.
2579
+
2580
+ ### Changed
2581
+
2582
+ - `GraphicsEngine` → **`GraphicsEngine3`**. The facade is narrow: no `layers`, no
2583
+ `getMaterialManager()`, no renderer camera object.
2584
+ - `Mesh` (v1) → **`SGMesh`**. Level binaries carrying the old component were converted; `Mesh.opacity`
2585
+ does not come across.
2586
+ - A model file expands into **entities** — `ShadedGeometry` on the nodes that draw — rather than
2587
+ living as a tree inside a component.
2588
+ - `Renderer.add_pass` / `remove_pass` → **render extensions**: a declared phase, declared ordering,
2589
+ and typed per-frame records.
2590
+ - Bone and socket positions come from **`query_entity_node_world_pose`**, not from a skeleton.
2591
+ - Picking is **async and batched** (`PickingQuery`); there is no synchronous entry point.
2592
+ - `stats.js` is used by its own name rather than through three's copy.
2593
+ - The renderer selection switch is gone — there is one renderer.
2594
+
2595
+ ### Added
2596
+
2597
+ - **Shade**, vendored into `src/shade/`: meshlet geometry, GPU scene database, GPU animation and
2598
+ skinning, clustered lighting, ReSTIR DI, brick4 volumetric GI, virtual geometry (`.vgeo`), virtual
2599
+ textures, TAA/NSS upscaling, dynamic resolution scaling, a path tracer.
2600
+ - A **validating software GPU device** (`src/shade/device/mock/`) and a WGSL interpreter, so shader
2601
+ and pipeline behaviour is testable in node without a GPU.
2602
+ - **`RENDERER_CONTRACT.md`** — the renderer's public surface, written down for the first time.