@woosh/meep-engine 3.60.4 → 3.60.6

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