@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.
- package/CHANGELOG.md +2621 -0
- package/CONTRIBUTING.md +7 -0
- package/MIGRATION_2_TO_3.md +136 -0
- package/SECURITY.md +4 -0
- package/editor/EditorCameraNavigation.d.ts +2 -1
- package/editor/EditorCameraNavigation.d.ts.map +1 -1
- package/editor/EditorCameraNavigation.js +51 -57
- package/package.json +6 -2
- package/src/engine/EngineHarness.d.ts +3 -9
- package/src/engine/EngineHarness.d.ts.map +1 -1
- package/src/engine/EngineHarness.js +6 -8
- package/src/engine/graphics/camera/makeOrbitalCameraController.d.ts +9 -6
- package/src/engine/graphics/camera/makeOrbitalCameraController.d.ts.map +1 -1
- package/src/engine/graphics/camera/makeOrbitalCameraController.js +44 -71
- package/src/engine/graphics/camera/orbital/MomentumAxis.d.ts +64 -0
- package/src/engine/graphics/camera/orbital/MomentumAxis.d.ts.map +1 -0
- package/src/engine/graphics/camera/orbital/MomentumAxis.js +187 -0
- package/src/engine/graphics/camera/orbital/OrbitalCameraRig.d.ts +86 -0
- package/src/engine/graphics/camera/orbital/OrbitalCameraRig.d.ts.map +1 -0
- package/src/engine/graphics/camera/orbital/OrbitalCameraRig.js +319 -0
- package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.d.ts +34 -10
- package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.d.ts.map +1 -1
- package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.js +116 -30
- package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.d.ts +1 -7
- package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.js +1 -37
- package/src/engine/graphics/ecs/water/Water.d.ts +8 -0
- package/src/engine/graphics/ecs/water/Water.d.ts.map +1 -1
- package/src/engine/graphics/ecs/water/Water.js +8 -0
- package/src/engine/graphics3/WaterSystem.d.ts.map +1 -1
- package/src/engine/graphics3/WaterSystem.js +731 -734
- package/src/engine/graphics3/decal/DecalTileShader.d.ts +61 -0
- package/src/engine/graphics3/decal/DecalTileShader.d.ts.map +1 -0
- package/src/engine/graphics3/decal/DecalTileShader.js +138 -0
- package/src/engine/graphics3/decal/GPUDecalRenderer.d.ts +10 -4
- package/src/engine/graphics3/decal/GPUDecalRenderer.d.ts.map +1 -1
- package/src/engine/graphics3/decal/GPUDecalRenderer.js +149 -44
- package/src/engine/graphics3/decal/chunk_decal_tile_vertex.d.ts +25 -0
- package/src/engine/graphics3/decal/chunk_decal_tile_vertex.d.ts.map +1 -0
- package/src/engine/graphics3/decal/chunk_decal_tile_vertex.js +55 -0
- package/src/engine/graphics3/decal/graph_gather_decal_tiles.d.ts +22 -0
- package/src/engine/graphics3/decal/graph_gather_decal_tiles.d.ts.map +1 -0
- package/src/engine/graphics3/decal/graph_gather_decal_tiles.js +70 -0
- package/src/engine/graphics3/decal/shader_decal_composite.d.ts +7 -5
- package/src/engine/graphics3/decal/shader_decal_composite.d.ts.map +1 -1
- package/src/engine/graphics3/decal/shader_decal_composite.js +48 -44
- package/src/engine/graphics3/decal/shader_decal_tile_copy.d.ts +12 -0
- package/src/engine/graphics3/decal/shader_decal_tile_copy.d.ts.map +1 -0
- package/src/engine/graphics3/decal/shader_decal_tile_copy.js +78 -0
- package/src/engine/graphics3/decal/shader_gather_decal_tiles.d.ts +11 -0
- package/src/engine/graphics3/decal/shader_gather_decal_tiles.d.ts.map +1 -0
- package/src/engine/graphics3/decal/shader_gather_decal_tiles.js +80 -0
- package/src/engine/input/devices/mouse/MouseEventButtons.d.ts +13 -0
- package/src/engine/input/devices/mouse/MouseEventButtons.d.ts.map +1 -0
- package/src/engine/input/devices/mouse/MouseEventButtons.js +23 -0
- package/src/engine/input/ecs/controllers/KeyboardCameraController.d.ts +4 -0
- package/src/engine/input/ecs/controllers/KeyboardCameraController.d.ts.map +1 -1
- package/src/engine/input/ecs/controllers/KeyboardCameraController.js +9 -45
- package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
- package/src/shade/playground/water_refraction/README.md +5 -6
- package/src/shade/playground/water_refraction/index.html +1 -1
- package/src/shade/renderer/Renderer.d.ts +11 -0
- package/src/shade/renderer/Renderer.d.ts.map +1 -1
- package/src/shade/renderer/Renderer.js +50 -6
- package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts +40 -80
- package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts.map +1 -1
- package/src/shade/renderer/camera/orbital/OrbitalCameraController.js +197 -147
- package/src/shade/renderer/fluid/GPUFluidCascade.d.ts +3 -3
- package/src/shade/renderer/fluid/GPUFluidCascadeStack.d.ts +1 -1
- package/src/shade/renderer/fluid/forcing/GPUFluidForcing.d.ts +6 -6
- package/src/shade/renderer/fluid/pressure/GPUFluidPressureSolve.d.ts +1 -1
- package/src/shade/renderer/fluid/query/GPUFluidQueries.d.ts +1 -1
- package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.d.ts +28 -45
- package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.d.ts.map +1 -1
- package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.js +111 -119
- package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts +22 -3
- package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts.map +1 -1
- package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.js +84 -3
- package/src/shade/renderer/geometry/virtual/proxy/vgeo_proxy_triangle_limit.d.ts +19 -0
- package/src/shade/renderer/geometry/virtual/proxy/vgeo_proxy_triangle_limit.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/proxy/vgeo_proxy_triangle_limit.js +25 -0
- package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.d.ts +31 -0
- package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.js +343 -0
- package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.d.ts +15 -0
- package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.js +38 -0
- package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.d.ts +17 -0
- package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.js +29 -0
- package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.js +7 -13
- package/src/shade/renderer/geometry/virtual/sdf/VGeoSDFProxies.d.ts +28 -59
- package/src/shade/renderer/geometry/virtual/sdf/VGeoSDFProxies.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/sdf/VGeoSDFProxies.js +84 -789
- package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.d.ts.map +1 -1
- package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.js +1 -8
- package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts +7 -6
- package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts.map +1 -1
- package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.js +12 -11
- package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/chunk_mesh_brick_instance.d.ts +2 -2
- package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/chunk_mesh_brick_instance.js +2 -2
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts +1 -4
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts.map +1 -1
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.js +1 -10
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.d.ts +5 -15
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.d.ts.map +1 -1
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.js +6 -34
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.d.ts.map +1 -1
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.js +1 -7
- package/src/shade/renderer/hiz/HZBFootprint.d.ts +14 -0
- package/src/shade/renderer/hiz/HZBFootprint.d.ts.map +1 -0
- package/src/shade/renderer/hiz/HZBFootprint.js +16 -0
- package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.d.ts +10 -0
- package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.d.ts.map +1 -0
- package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.js +11 -0
- package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.d.ts +10 -0
- package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.d.ts.map +1 -0
- package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.js +11 -0
- package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts +0 -7
- package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts.map +1 -1
- package/src/shade/renderer/hiz/HierarchicalZBuffer.js +405 -350
- package/src/shade/renderer/hiz/chunk_hzb_footprint_x.d.ts +12 -0
- package/src/shade/renderer/hiz/chunk_hzb_footprint_x.d.ts.map +1 -0
- package/src/shade/renderer/hiz/chunk_hzb_footprint_x.js +27 -0
- package/src/shade/renderer/hiz/chunk_hzb_footprint_y.d.ts +12 -0
- package/src/shade/renderer/hiz/chunk_hzb_footprint_y.d.ts.map +1 -0
- package/src/shade/renderer/hiz/chunk_hzb_footprint_y.js +27 -0
- package/src/shade/renderer/hiz/hzb_footprint_texels.d.ts +14 -0
- package/src/shade/renderer/hiz/hzb_footprint_texels.d.ts.map +1 -0
- package/src/shade/renderer/hiz/hzb_footprint_texels.js +34 -0
- package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts +0 -5
- package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts.map +1 -1
- package/src/shade/renderer/hiz/shader_hzb_reduce_any.js +37 -57
- package/src/shade/renderer/hiz/shader_hzb_reduce_mip0.d.ts +11 -0
- package/src/shade/renderer/hiz/shader_hzb_reduce_mip0.d.ts.map +1 -0
- package/src/shade/renderer/hiz/{shader_hzb_reduce_mip0_clip.js → shader_hzb_reduce_mip0.js} +120 -111
- package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
- package/src/shade/renderer/material/GPUMaterialManager.d.ts +11 -1
- package/src/shade/renderer/material/GPUMaterialManager.d.ts.map +1 -1
- package/src/shade/renderer/material/GPUMaterialManager.js +16 -8
- package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts +12 -12
- package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts.map +1 -1
- package/src/shade/renderer/material/resident/GPUResidentMaterialContext.js +17 -118
- package/src/shade/renderer/postprocess/gtao/GTAO.d.ts +7 -3
- package/src/shade/renderer/postprocess/gtao/GTAO.d.ts.map +1 -1
- package/src/shade/renderer/postprocess/gtao/GTAO.js +26 -13
- package/src/shade/renderer/postprocess/gtao/ao_reproject_shader.d.ts +14 -0
- package/src/shade/renderer/postprocess/gtao/ao_reproject_shader.d.ts.map +1 -1
- package/src/shade/renderer/postprocess/gtao/ao_reproject_shader.js +196 -68
- package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao.d.ts.map +1 -1
- package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao.js +42 -28
- package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao_with_irradiance.d.ts.map +1 -1
- package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao_with_irradiance.js +429 -414
- package/src/shade/renderer/postprocess/gtao/shader_gtao_spatial_denoise.d.ts +16 -0
- package/src/shade/renderer/postprocess/gtao/shader_gtao_spatial_denoise.d.ts.map +1 -0
- package/src/shade/renderer/postprocess/gtao/shader_gtao_spatial_denoise.js +156 -0
- package/src/shade/renderer/scene/GPUSceneContext.d.ts +2 -2
- package/src/shade/renderer/scene/GPUSceneContext.js +5 -5
- package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.d.ts.map +1 -1
- package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.js +7 -2
- package/src/shade/renderer/water/GPUWaterRenderer.d.ts +0 -6
- package/src/shade/renderer/water/GPUWaterRenderer.d.ts.map +1 -1
- package/src/shade/renderer/water/GPUWaterRenderer.js +12 -17
- package/src/shade/renderer/water/medium/graph_water_meshlets.d.ts +35 -0
- package/src/shade/renderer/water/medium/graph_water_meshlets.d.ts.map +1 -0
- package/src/shade/renderer/water/medium/graph_water_meshlets.js +89 -0
- package/src/shade/renderer/water/query/GPUWaterQueries.d.ts +1 -1
- package/src/shade/renderer/water/shader_water.d.ts.map +1 -1
- package/src/shade/renderer/water/shader_water.js +22 -13
- package/src/shade/renderer/water/shader_water_avboit.d.ts.map +1 -1
- package/src/shade/renderer/water/shader_water_avboit.js +7 -7
- package/src/shade/renderer/water/surface/graph_water_tessellation.d.ts +1 -1
- package/src/shade/renderer/water/surface/graph_water_tessellation.js +1 -1
- package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +9 -1
- package/editor/REWORK_PLAN_2026_08.md +0 -692
- package/src/CODEC_LAYOUT_VERIFICATION.md +0 -419
- package/src/CORRECTNESS_HUNT_2026_07_24.md +0 -142
- package/src/DUPLICATION_AUDIT_2026_08_07/00_core_reuse_surface.md +0 -738
- package/src/DUPLICATION_AUDIT_2026_08_07/A_core_math_geom.md +0 -79
- package/src/DUPLICATION_AUDIT_2026_08_07/B_core_data.md +0 -93
- package/src/DUPLICATION_AUDIT_2026_08_07/C_gfx_render.md +0 -80
- package/src/DUPLICATION_AUDIT_2026_08_07/D_gfx_features.md +0 -164
- package/src/DUPLICATION_AUDIT_2026_08_07/E_physics.md +0 -69
- package/src/DUPLICATION_AUDIT_2026_08_07/F_sound.md +0 -110
- package/src/DUPLICATION_AUDIT_2026_08_07/G_net_save.md +0 -129
- package/src/DUPLICATION_AUDIT_2026_08_07/H_engine_misc.md +0 -72
- package/src/DUPLICATION_AUDIT_2026_08_07/I_generation_view.md +0 -74
- package/src/DUPLICATION_AUDIT_2026_08_07/K_crosscutting.md +0 -169
- package/src/DUPLICATION_AUDIT_2026_08_07.md +0 -254
- package/src/DUPLICATION_AUDIT_REMEDIATION.md +0 -250
- package/src/DUPLICATION_REVIEW_2026_10_24.md +0 -1072
- package/src/REVIEW_2026_08_06/adv-core.md +0 -511
- package/src/REVIEW_2026_08_06/adv-gfx.md +0 -480
- package/src/REVIEW_2026_08_06/adv-view.md +0 -446
- package/src/REVIEW_2026_08_06/api-core.md +0 -296
- package/src/REVIEW_2026_08_06/api-engine.md +0 -195
- package/src/REVIEW_2026_08_06/bvh-phys-sound-net.md +0 -507
- package/src/REVIEW_2026_08_06/core-binary.md +0 -348
- package/src/REVIEW_2026_08_06/core-collection.md +0 -376
- package/src/REVIEW_2026_08_06/core-math.md +0 -221
- package/src/REVIEW_2026_08_06/core-process.md +0 -266
- package/src/REVIEW_2026_08_06/core-science.md +0 -223
- package/src/REVIEW_2026_08_06/engine-ai-nav.md +0 -279
- package/src/REVIEW_2026_08_06/engine-animation.md +0 -167
- package/src/REVIEW_2026_08_06/engine-asset.md +0 -166
- package/src/REVIEW_2026_08_06/engine-ecs.md +0 -920
- package/src/REVIEW_2026_08_06/engine-input.md +0 -197
- package/src/REVIEW_2026_08_06/engine-misc.md +0 -248
- package/src/REVIEW_2026_08_06/engine-terrain.md +0 -52
- package/src/REVIEW_2026_08_06/generation.md +0 -176
- package/src/REVIEW_2026_08_06/geom-2d.md +0 -434
- package/src/REVIEW_2026_08_06/geom-3d.md +0 -117
- package/src/REVIEW_2026_08_06/gfx-gi.md +0 -143
- package/src/REVIEW_2026_08_06/gfx-render.md +0 -740
- package/src/REVIEW_2026_08_06/gfx-texture.md +0 -690
- package/src/REVIEW_2026_08_06/prior-criticals.md +0 -49
- package/src/REVIEW_2026_08_06/prior-highs.md +0 -138
- package/src/REVIEW_2026_08_06/test-gaps.md +0 -364
- package/src/REVIEW_2026_08_06/view.md +0 -667
- package/src/REVIEW_2026_08_06.md +0 -610
- package/src/core/binary/lz4/LZ4_HIGH_COMPRESSION_PLAN_2026_09_14.md +0 -542
- package/src/core/bvh2/bvh3/NOTES.md +0 -2
- package/src/core/bvh8/NOTES.md +0 -63
- package/src/core/cache/wtinylfu/benchmark/CacheWTinylfu.bench.md +0 -243
- package/src/core/color/COLOR_FIX_PLAN_2026_08_28.md +0 -417
- package/src/core/color/COLOR_REVIEW_2026_08_28.md +0 -779
- package/src/core/geom/2d/polygon/TRIANGULATION_DESIGN.md +0 -433
- package/src/core/geom/3d/atlas/ATLAS_PORT_PLAN.md +0 -486
- package/src/core/geom/3d/atlas/BFF_PLAN.md +0 -174
- package/src/core/geom/3d/atlas/REVIEW_02.md +0 -392
- package/src/core/geom/3d/gjk/GJK_REVIEW_NOTES.md +0 -146
- package/src/core/geom/3d/gjk/NOTES.md +0 -8
- package/src/core/geom/3d/topology/TOPO_MESH_RETIREMENT_PLAN.md +0 -135
- package/src/core/geom/GEOMETRY_REVIEW_002_API_ISSUES.md +0 -381
- package/src/core/geom/GEOMETRY_REVIEW_002_NAMING.md +0 -684
- package/src/core/geom/MANIFOLD_COMPARISON_2026_08_01.md +0 -554
- package/src/core/model/reactive/evaluation/PREDICATE_APPLICABILITY_PROPOSAL.md +0 -446
- package/src/engine/REVIEW_2026_07_03.md +0 -211
- package/src/engine/asset/loaders/image/IMAGE_CODEC_REVIEW_2026_08_28.md +0 -815
- package/src/engine/control/first-person/DESIGN.md +0 -658
- package/src/engine/control/first-person/DESIGN_COLLISION.md +0 -382
- package/src/engine/control/first-person/DESIGN_EXTENSIONS.md +0 -563
- package/src/engine/control/first-person/TODO.md +0 -160
- package/src/engine/ecs/ASYNC_SYSTEMS_PROPOSAL.md +0 -531
- package/src/engine/ecs/HIERARCHY_REVIEW.md +0 -717
- package/src/engine/ecs/async/WORKER_SYSTEM_PLAN.md +0 -262
- package/src/engine/ecs/ik/PLAN.md +0 -179
- package/src/engine/ecs/storage/binary/SERIALIZATION_COVERAGE.md +0 -272
- package/src/engine/ecs/storage/binary/object-v2/DESIGN.md +0 -199
- package/src/engine/ecs/transform/TRANSFORM64_MIGRATION_PLAN_2026_09_04.md +0 -521
- package/src/engine/graphics/CONTEXT_LOSS_RECOVERY_PLAN.md +0 -463
- package/src/engine/graphics/ecs/camera/pp/NOTES.md +0 -48
- package/src/engine/graphics/render/frame_graph/CORRECTNESS_FIXES_2026_09_04.md +0 -659
- package/src/engine/graphics/render/frame_graph/FRAME_GRAPH_RECOMMENDATIONS.md +0 -235
- package/src/engine/graphics/sh3/path_tracer/REVIEW_001_ACTION_PLAN.md +0 -490
- package/src/engine/graphics/texture/sampler/compare/FLIP_IMPLEMENTATION_PLAN.md +0 -466
- package/src/engine/input/GAMEPAD_API_PROPOSAL.md +0 -717
- package/src/engine/input/ecs/ism/ISM_DESIGN.md +0 -1523
- package/src/engine/input/ecs/ism/ISM_IMPLEMENTATION_PLAN.md +0 -627
- package/src/engine/navigation/mesh/OVERLAPPING_SURFACES_2026_08_25.md +0 -661
- package/src/engine/navigation/mesh/PATHFINDING_PLAN.md +0 -229
- package/src/engine/network/CONGESTION_CONTROL.md +0 -87
- package/src/engine/network/MESSAGE_TRANSPORT.md +0 -364
- package/src/engine/network/SMOOTHNESS_DESIGN.md +0 -400
- package/src/engine/physics/cloth/MEASUREMENTS.md +0 -5304
- package/src/engine/physics/cloth/PLAN.md +0 -3056
- package/src/engine/physics/fluid/GPU_FLUID_CASCADES_PROPOSAL_2026_10_16.md +0 -1382
- package/src/engine/physics/fluid/REVIEW_02_PLAN.md +0 -243
- package/src/engine/physics/fluid/solver/NOTES.md +0 -46
- package/src/engine/sound/SOUND_EMITTER_RETIREMENT_PLAN.md +0 -536
- package/src/engine/sound/simulation/IMPLEMENTATION_PLAN.md +0 -432
- package/src/engine/sound/simulation/PROBE_PLACEMENT.md +0 -250
- package/src/engine/sound/simulation/PROBE_TRANSFER_RESEARCH.md +0 -322
- package/src/engine/sound/simulation/REFLECTION_IR_PLAN.md +0 -261
- package/src/engine/sound/simulation/VISIBILITY_GRAPH_PLAN.md +0 -121
- package/src/engine/sound/sopra/IMPLEMENTATION_PLAN.md +0 -999
- package/src/format/image/avif/DECISIONS.md +0 -545
- package/src/format/image/jpeg/NOTES.md +0 -3
- package/src/format/scene/gltf/MESHOPT_COMPRESSION_PLAN.md +0 -814
- package/src/generation/COORDINATES.md +0 -54
- package/src/generation/REVIEW_01_ACTION_PLAN.md +0 -628
- package/src/generation/foliage/ANNUAL_SHOOTS_BRIEF_2026_10_25.md +0 -220
- package/src/generation/foliage/CROWN_FILLING_BRIEF_2026_10_26.md +0 -172
- package/src/generation/foliage/CROWN_FULLNESS_BRIEF_2026_10_26.md +0 -86
- package/src/generation/foliage/FOLIAGE_DESIGN.md +0 -547
- package/src/generation/foliage/FOLIAGE_PLAN.md +0 -198
- package/src/generation/foliage/LEAF_ARCHITECTURE_BRIEF_2026_10_26.md +0 -158
- package/src/generation/foliage/LOD_REWORK_BRIEF_2026_10_27.md +0 -521
- package/src/generation/foliage/REAL_TIME_TREES_BRIEF_2026_10_27.md +0 -86
- package/src/generation/foliage/SELF_ORGANIZING_FRAMEWORK_BRIEF_2026_10_26.md +0 -347
- package/src/generation/foliage/SHOOT_SYSTEMS_ALL_SPECIES_BRIEF_2026_10_26.md +0 -248
- package/src/generation/foliage/SPEEDTREE_COMPARISON_2026_10_24.md +0 -694
- package/src/generation/foliage/TWO_FRONT_ENDS_BRIEF_2026_10_28.md +0 -552
- package/src/shade/RENDERER_CONTRACT.md +0 -697
- package/src/shade/SHADER_F16_PROPOSAL_2026_10_24.md +0 -263
- package/src/shade/device/IMMEDIATE_DATA_PLAN_2026_08_28.md +0 -569
- package/src/shade/device/PASS_ENCODER_PROPOSAL_2026_08_31.md +0 -755
- package/src/shade/device/mock/EXTENSION_PROPOSAL_2026_08_22.md +0 -458
- package/src/shade/device/mock/IMPLEMENTATION_PLAN.md +0 -255
- package/src/shade/device/mock/REVIEW_2026_08_22.md +0 -527
- package/src/shade/device/timing/GPU_PROFILER_PROPOSAL_2026_08_28.md +0 -894
- package/src/shade/device/timing/profile/SGPT_FORMAT.md +0 -345
- package/src/shade/renderer/NOTES.md +0 -78
- package/src/shade/renderer/PLAN.md +0 -8
- package/src/shade/renderer/animation/ADDITIVE_BLENDING_PLAN.md +0 -145
- package/src/shade/renderer/animation/SKINNING_FRAME_PLAN.md +0 -357
- package/src/shade/renderer/atmosphere/NOTES.md +0 -6
- package/src/shade/renderer/camera/orbital/MomentumAxis.d.ts +0 -76
- package/src/shade/renderer/camera/orbital/MomentumAxis.d.ts.map +0 -1
- package/src/shade/renderer/camera/orbital/MomentumAxis.js +0 -164
- package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.d.ts +0 -45
- package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.d.ts.map +0 -1
- package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.js +0 -66
- package/src/shade/renderer/camera/orbital/OrbitalCameraInput.d.ts +0 -98
- package/src/shade/renderer/camera/orbital/OrbitalCameraInput.d.ts.map +0 -1
- package/src/shade/renderer/camera/orbital/OrbitalCameraInput.js +0 -184
- package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts +0 -175
- package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts.map +0 -1
- package/src/shade/renderer/camera/orbital/OrbitalCameraRig.js +0 -411
- package/src/shade/renderer/camera/orbital/README.md +0 -81
- package/src/shade/renderer/camera/orbital/index.d.ts +0 -6
- package/src/shade/renderer/camera/orbital/index.d.ts.map +0 -1
- package/src/shade/renderer/camera/orbital/index.js +0 -9
- package/src/shade/renderer/extension/RENDER_EXTENSION_DESIGN.md +0 -964
- package/src/shade/renderer/fluid/GPU_FLUID_OCCUPANCY_AND_SOTA_REVIEW_2026_10_26.md +0 -361
- package/src/shade/renderer/geometry/bvh/BLAS_REFIT_BATCH_PLAN.md +0 -619
- package/src/shade/renderer/geometry/meshlet/COMPACTION_PLAN_2026_09_16.md +0 -674
- package/src/shade/renderer/geometry/sdf/NOTES.md +0 -6
- package/src/shade/renderer/geometry/virtual/ATTRIBUTE_PLAN_2026_09_18.md +0 -540
- package/src/shade/renderer/geometry/virtual/MICRON_COMPARISON_2026_08_20.md +0 -305
- package/src/shade/renderer/geometry/virtual/VGEO_FORMAT.md +0 -913
- package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +0 -787
- package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_PLAN.md +0 -481
- package/src/shade/renderer/geometry/virtual/VK_LOD_CLUSTERS_COMPARISON_2026_08_20.md +0 -301
- package/src/shade/renderer/geometry/virtual/WEDGE_PLAN_2026_09_18.md +0 -448
- package/src/shade/renderer/geometry/virtual/runtime/CULL_PLAN_2026_09_16.md +0 -385
- package/src/shade/renderer/geometry/virtual/runtime/CUT_PLAN_2026_09_14.md +0 -656
- package/src/shade/renderer/geometry/virtual/runtime/REVIEW_LEDGER.md +0 -308
- package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN.md +0 -657
- package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN_2026_09_14.md +0 -551
- package/src/shade/renderer/geometry/virtual/runtime/SHADOW_PLAN_2026_09_28.md +0 -529
- package/src/shade/renderer/geometry/virtual/sdf/VGEO_SDF.d.ts +0 -47
- package/src/shade/renderer/geometry/virtual/sdf/VGEO_SDF.d.ts.map +0 -1
- package/src/shade/renderer/geometry/virtual/sdf/VGEO_SDF.js +0 -80
- package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_collect.d.ts +0 -16
- package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_collect.d.ts.map +0 -1
- package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_collect.js +0 -67
- package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_decode.d.ts +0 -17
- package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_decode.d.ts.map +0 -1
- package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_decode.js +0 -145
- package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_traverse.d.ts +0 -12
- package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_traverse.d.ts.map +0 -1
- package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_traverse.js +0 -105
- package/src/shade/renderer/global_illumination/lpv/NOTES.md +0 -3
- package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_CLIPMAP_PROPOSAL_2026_10_27.md +0 -4306
- package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_RAYS_VARIABLE_AND_SORTED_SCOPE_2026_10_30.md +0 -410
- package/src/shade/renderer/global_illumination/sharc/NOTES.md +0 -8
- package/src/shade/renderer/gpu_primitive/bvh/GPU_BVH_BUILD_PLAN.md +0 -1369
- package/src/shade/renderer/gpu_primitive/bvh/PHASE1_HANDOVER.md +0 -202
- package/src/shade/renderer/gpu_primitive/indirect/NOTES.md +0 -94
- package/src/shade/renderer/hiz/NOTES.md +0 -3
- package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts +0 -8
- package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts.map +0 -1
- package/src/shade/renderer/light/cluster/NOTES.md +0 -11
- package/src/shade/renderer/light/cluster/PROPOSAL_ALGORITHM_2026_10_16.md +0 -524
- package/src/shade/renderer/light/cluster/PROPOSAL_HIERARCHICAL_2026_10_16.md +0 -1101
- package/src/shade/renderer/material/resident/shader_resident_texture_means.d.ts +0 -24
- package/src/shade/renderer/material/resident/shader_resident_texture_means.d.ts.map +0 -1
- package/src/shade/renderer/material/resident/shader_resident_texture_means.js +0 -120
- package/src/shade/renderer/particles/DESIGN.md +0 -1011
- package/src/shade/renderer/particles/EMITTER_GLOBAL_STATE_PLAN_2026_10_29.md +0 -280
- package/src/shade/renderer/particles/ORIENTATION_PLAN_2026_10_15.md +0 -453
- package/src/shade/renderer/path_tracer/PATH_TRACED_MEDIA_PLAN_2026_10_02.md +0 -520
- package/src/shade/renderer/postprocess/bloom/NOTES.md +0 -6
- package/src/shade/renderer/postprocess/fsr/NOTES.md +0 -4
- package/src/shade/renderer/postprocess/gtao/NOTES.md +0 -6
- package/src/shade/renderer/postprocess/nss/NOTES.md +0 -332
- package/src/shade/renderer/postprocess/perspective/NOTES.md +0 -3
- package/src/shade/renderer/postprocess/sscs/NOTES.md +0 -5
- package/src/shade/renderer/postprocess/ssr/NOTES.md +0 -14
- package/src/shade/renderer/postprocess/ssr/REPROJECTION_PROPOSAL.md +0 -286
- package/src/shade/renderer/postprocess/ssr/prefilter/NOTES.md +0 -39
- package/src/shade/renderer/postprocess/taa/NOTES.md +0 -11
- package/src/shade/renderer/postprocess/upscale-dither/NOTES.md +0 -78
- package/src/shade/renderer/postprocess/vrs/NOTES.md +0 -4
- package/src/shade/renderer/rasterize/native/avboit/AVBOIT_LEDGER.md +0 -1218
- package/src/shade/renderer/rasterize/native/avboit/AVBOIT_PLAN_2026_09_03.md +0 -600
- package/src/shade/renderer/restir/reservoir/NOTES.md +0 -6
- package/src/shade/renderer/shader/chunk/atmosphere/NOTES.md +0 -6
- package/src/shade/renderer/shader/chunk/brdf/FRESNEL_BRIEF_2026_10_16.md +0 -391
- package/src/shade/renderer/shader/chunk/color/tonemap/ACES_TONE_MAPPING_BRIEF_2026_10_17.md +0 -153
- package/src/shade/renderer/shader/chunk/color/tonemap/HDR_TONE_MAPPING_FORMS_BRIEF_2026_10_16.md +0 -322
- package/src/shade/renderer/shader/chunk/color/tonemap/SDR_TONE_MAPPING_BRIEF_2026_10_16.md +0 -247
- package/src/shade/renderer/shader/chunk/fsr/NOTES.md +0 -3
- package/src/shade/renderer/shader/chunk/fsr/spd/NOTES.md +0 -4
- package/src/shade/renderer/shader/chunk/geometry/ray/NOTES.md +0 -4
- package/src/shade/renderer/shader/chunk/material/MULTISCATTER.md +0 -159
- package/src/shade/renderer/shader/chunk/mboit/NOTES.md +0 -38
- package/src/shade/renderer/shader/chunk/spherical_harmonics/l1i/NOTES.md +0 -3
- package/src/shade/renderer/shader/chunk/spherical_harmonics/sampling/NOTES.md +0 -8
- package/src/shade/renderer/shader/compiler/SHADER_SWITCH_PROPOSAL_2026_10_16.md +0 -927
- package/src/shade/renderer/shadow/contact/FINDINGS.md +0 -413
- package/src/shade/renderer/shadow/map/csm/NOTES.md +0 -5
- package/src/shade/renderer/shadow/ray/NOTES.md +0 -31
- package/src/shade/renderer/shadow/sdf/NOTES.md +0 -9
- package/src/shade/renderer/shadow/sdf/brixelizer/NOTES.md +0 -5
- package/src/shade/renderer/shadow/sdf/cascade/NOTES.md +0 -9
- package/src/shade/renderer/shadow/vsm/NOTES.md +0 -12
- package/src/shade/renderer/shadow/vsm/PLAN.md +0 -442
- package/src/shade/renderer/shadow/vsm/REVIEW_LEDGER.md +0 -43
- package/src/shade/renderer/texture/COMPRESSED_TEXTURE_PLAN_2026_09_01.md +0 -2444
- package/src/shade/renderer/texture/mipmaps/NOTES.md +0 -3
- package/src/shade/renderer/volumetrics/NOTES.md +0 -1329
- package/src/shade/renderer/volumetrics/VOLUMETRIC_NOISE_PLAN_2026_10_15.md +0 -336
- package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts +0 -23
- package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts.map +0 -1
- package/src/shade/renderer/water/medium/GPUWaterBodies.js +0 -75
- 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.
|