@woosh/meep-engine 3.60.4 → 3.60.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (419) hide show
  1. package/CHANGELOG.md +2621 -0
  2. package/CONTRIBUTING.md +7 -0
  3. package/MIGRATION_2_TO_3.md +136 -0
  4. package/SECURITY.md +4 -0
  5. package/editor/EditorCameraNavigation.d.ts +2 -1
  6. package/editor/EditorCameraNavigation.d.ts.map +1 -1
  7. package/editor/EditorCameraNavigation.js +51 -57
  8. package/package.json +6 -2
  9. package/src/engine/EngineHarness.d.ts +3 -9
  10. package/src/engine/EngineHarness.d.ts.map +1 -1
  11. package/src/engine/EngineHarness.js +6 -8
  12. package/src/engine/graphics/camera/makeOrbitalCameraController.d.ts +9 -6
  13. package/src/engine/graphics/camera/makeOrbitalCameraController.d.ts.map +1 -1
  14. package/src/engine/graphics/camera/makeOrbitalCameraController.js +44 -71
  15. package/src/engine/graphics/camera/orbital/MomentumAxis.d.ts +64 -0
  16. package/src/engine/graphics/camera/orbital/MomentumAxis.d.ts.map +1 -0
  17. package/src/engine/graphics/camera/orbital/MomentumAxis.js +187 -0
  18. package/src/engine/graphics/camera/orbital/OrbitalCameraRig.d.ts +86 -0
  19. package/src/engine/graphics/camera/orbital/OrbitalCameraRig.d.ts.map +1 -0
  20. package/src/engine/graphics/camera/orbital/OrbitalCameraRig.js +319 -0
  21. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.d.ts +34 -10
  22. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.d.ts.map +1 -1
  23. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.js +116 -30
  24. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.d.ts +1 -7
  25. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.d.ts.map +1 -1
  26. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.js +1 -37
  27. package/src/engine/graphics/ecs/water/Water.d.ts +8 -0
  28. package/src/engine/graphics/ecs/water/Water.d.ts.map +1 -1
  29. package/src/engine/graphics/ecs/water/Water.js +8 -0
  30. package/src/engine/graphics3/WaterSystem.d.ts.map +1 -1
  31. package/src/engine/graphics3/WaterSystem.js +731 -734
  32. package/src/engine/graphics3/decal/DecalTileShader.d.ts +61 -0
  33. package/src/engine/graphics3/decal/DecalTileShader.d.ts.map +1 -0
  34. package/src/engine/graphics3/decal/DecalTileShader.js +138 -0
  35. package/src/engine/graphics3/decal/GPUDecalRenderer.d.ts +10 -4
  36. package/src/engine/graphics3/decal/GPUDecalRenderer.d.ts.map +1 -1
  37. package/src/engine/graphics3/decal/GPUDecalRenderer.js +149 -44
  38. package/src/engine/graphics3/decal/chunk_decal_tile_vertex.d.ts +25 -0
  39. package/src/engine/graphics3/decal/chunk_decal_tile_vertex.d.ts.map +1 -0
  40. package/src/engine/graphics3/decal/chunk_decal_tile_vertex.js +55 -0
  41. package/src/engine/graphics3/decal/graph_gather_decal_tiles.d.ts +22 -0
  42. package/src/engine/graphics3/decal/graph_gather_decal_tiles.d.ts.map +1 -0
  43. package/src/engine/graphics3/decal/graph_gather_decal_tiles.js +70 -0
  44. package/src/engine/graphics3/decal/shader_decal_composite.d.ts +7 -5
  45. package/src/engine/graphics3/decal/shader_decal_composite.d.ts.map +1 -1
  46. package/src/engine/graphics3/decal/shader_decal_composite.js +48 -44
  47. package/src/engine/graphics3/decal/shader_decal_tile_copy.d.ts +12 -0
  48. package/src/engine/graphics3/decal/shader_decal_tile_copy.d.ts.map +1 -0
  49. package/src/engine/graphics3/decal/shader_decal_tile_copy.js +78 -0
  50. package/src/engine/graphics3/decal/shader_gather_decal_tiles.d.ts +11 -0
  51. package/src/engine/graphics3/decal/shader_gather_decal_tiles.d.ts.map +1 -0
  52. package/src/engine/graphics3/decal/shader_gather_decal_tiles.js +80 -0
  53. package/src/engine/input/devices/mouse/MouseEventButtons.d.ts +13 -0
  54. package/src/engine/input/devices/mouse/MouseEventButtons.d.ts.map +1 -0
  55. package/src/engine/input/devices/mouse/MouseEventButtons.js +23 -0
  56. package/src/engine/input/ecs/controllers/KeyboardCameraController.d.ts +4 -0
  57. package/src/engine/input/ecs/controllers/KeyboardCameraController.d.ts.map +1 -1
  58. package/src/engine/input/ecs/controllers/KeyboardCameraController.js +9 -45
  59. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  60. package/src/shade/playground/water_refraction/README.md +5 -6
  61. package/src/shade/playground/water_refraction/index.html +1 -1
  62. package/src/shade/renderer/Renderer.d.ts +11 -0
  63. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  64. package/src/shade/renderer/Renderer.js +50 -6
  65. package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts +40 -80
  66. package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts.map +1 -1
  67. package/src/shade/renderer/camera/orbital/OrbitalCameraController.js +197 -147
  68. package/src/shade/renderer/fluid/GPUFluidCascade.d.ts +3 -3
  69. package/src/shade/renderer/fluid/GPUFluidCascadeStack.d.ts +1 -1
  70. package/src/shade/renderer/fluid/forcing/GPUFluidForcing.d.ts +6 -6
  71. package/src/shade/renderer/fluid/pressure/GPUFluidPressureSolve.d.ts +1 -1
  72. package/src/shade/renderer/fluid/query/GPUFluidQueries.d.ts +1 -1
  73. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.d.ts +28 -45
  74. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.d.ts.map +1 -1
  75. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.js +111 -119
  76. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts +22 -3
  77. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts.map +1 -1
  78. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.js +84 -3
  79. package/src/shade/renderer/geometry/virtual/proxy/vgeo_proxy_triangle_limit.d.ts +19 -0
  80. package/src/shade/renderer/geometry/virtual/proxy/vgeo_proxy_triangle_limit.d.ts.map +1 -0
  81. package/src/shade/renderer/geometry/virtual/proxy/vgeo_proxy_triangle_limit.js +25 -0
  82. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.d.ts +31 -0
  83. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.d.ts.map +1 -0
  84. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.js +343 -0
  85. package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.d.ts +15 -0
  86. package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.d.ts.map +1 -1
  87. package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.js +38 -0
  88. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.d.ts +17 -0
  89. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.d.ts.map +1 -0
  90. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.js +29 -0
  91. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.d.ts.map +1 -1
  92. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.js +7 -13
  93. package/src/shade/renderer/geometry/virtual/sdf/VGeoSDFProxies.d.ts +28 -59
  94. package/src/shade/renderer/geometry/virtual/sdf/VGeoSDFProxies.d.ts.map +1 -1
  95. package/src/shade/renderer/geometry/virtual/sdf/VGeoSDFProxies.js +84 -789
  96. package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.d.ts.map +1 -1
  97. package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.js +1 -8
  98. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts +7 -6
  99. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts.map +1 -1
  100. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.js +12 -11
  101. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/chunk_mesh_brick_instance.d.ts +2 -2
  102. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/chunk_mesh_brick_instance.js +2 -2
  103. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts +1 -4
  104. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts.map +1 -1
  105. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.js +1 -10
  106. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.d.ts +5 -15
  107. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.d.ts.map +1 -1
  108. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.js +6 -34
  109. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.d.ts.map +1 -1
  110. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.js +1 -7
  111. package/src/shade/renderer/hiz/HZBFootprint.d.ts +14 -0
  112. package/src/shade/renderer/hiz/HZBFootprint.d.ts.map +1 -0
  113. package/src/shade/renderer/hiz/HZBFootprint.js +16 -0
  114. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.d.ts +10 -0
  115. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.d.ts.map +1 -0
  116. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.js +11 -0
  117. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.d.ts +10 -0
  118. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.d.ts.map +1 -0
  119. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.js +11 -0
  120. package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts +0 -7
  121. package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts.map +1 -1
  122. package/src/shade/renderer/hiz/HierarchicalZBuffer.js +405 -350
  123. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.d.ts +12 -0
  124. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.d.ts.map +1 -0
  125. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.js +27 -0
  126. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.d.ts +12 -0
  127. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.d.ts.map +1 -0
  128. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.js +27 -0
  129. package/src/shade/renderer/hiz/hzb_footprint_texels.d.ts +14 -0
  130. package/src/shade/renderer/hiz/hzb_footprint_texels.d.ts.map +1 -0
  131. package/src/shade/renderer/hiz/hzb_footprint_texels.js +34 -0
  132. package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts +0 -5
  133. package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts.map +1 -1
  134. package/src/shade/renderer/hiz/shader_hzb_reduce_any.js +37 -57
  135. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0.d.ts +11 -0
  136. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0.d.ts.map +1 -0
  137. package/src/shade/renderer/hiz/{shader_hzb_reduce_mip0_clip.js → shader_hzb_reduce_mip0.js} +120 -111
  138. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  139. package/src/shade/renderer/material/GPUMaterialManager.d.ts +11 -1
  140. package/src/shade/renderer/material/GPUMaterialManager.d.ts.map +1 -1
  141. package/src/shade/renderer/material/GPUMaterialManager.js +16 -8
  142. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts +12 -12
  143. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts.map +1 -1
  144. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.js +17 -118
  145. package/src/shade/renderer/postprocess/gtao/GTAO.d.ts +7 -3
  146. package/src/shade/renderer/postprocess/gtao/GTAO.d.ts.map +1 -1
  147. package/src/shade/renderer/postprocess/gtao/GTAO.js +26 -13
  148. package/src/shade/renderer/postprocess/gtao/ao_reproject_shader.d.ts +14 -0
  149. package/src/shade/renderer/postprocess/gtao/ao_reproject_shader.d.ts.map +1 -1
  150. package/src/shade/renderer/postprocess/gtao/ao_reproject_shader.js +196 -68
  151. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao.d.ts.map +1 -1
  152. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao.js +42 -28
  153. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao_with_irradiance.d.ts.map +1 -1
  154. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao_with_irradiance.js +429 -414
  155. package/src/shade/renderer/postprocess/gtao/shader_gtao_spatial_denoise.d.ts +16 -0
  156. package/src/shade/renderer/postprocess/gtao/shader_gtao_spatial_denoise.d.ts.map +1 -0
  157. package/src/shade/renderer/postprocess/gtao/shader_gtao_spatial_denoise.js +156 -0
  158. package/src/shade/renderer/scene/GPUSceneContext.d.ts +2 -2
  159. package/src/shade/renderer/scene/GPUSceneContext.js +5 -5
  160. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.d.ts.map +1 -1
  161. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.js +7 -2
  162. package/src/shade/renderer/water/GPUWaterRenderer.d.ts +0 -6
  163. package/src/shade/renderer/water/GPUWaterRenderer.d.ts.map +1 -1
  164. package/src/shade/renderer/water/GPUWaterRenderer.js +12 -17
  165. package/src/shade/renderer/water/medium/graph_water_meshlets.d.ts +35 -0
  166. package/src/shade/renderer/water/medium/graph_water_meshlets.d.ts.map +1 -0
  167. package/src/shade/renderer/water/medium/graph_water_meshlets.js +89 -0
  168. package/src/shade/renderer/water/query/GPUWaterQueries.d.ts +1 -1
  169. package/src/shade/renderer/water/shader_water.d.ts.map +1 -1
  170. package/src/shade/renderer/water/shader_water.js +22 -13
  171. package/src/shade/renderer/water/shader_water_avboit.d.ts.map +1 -1
  172. package/src/shade/renderer/water/shader_water_avboit.js +7 -7
  173. package/src/shade/renderer/water/surface/graph_water_tessellation.d.ts +1 -1
  174. package/src/shade/renderer/water/surface/graph_water_tessellation.js +1 -1
  175. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +9 -1
  176. package/editor/REWORK_PLAN_2026_08.md +0 -692
  177. package/src/CODEC_LAYOUT_VERIFICATION.md +0 -419
  178. package/src/CORRECTNESS_HUNT_2026_07_24.md +0 -142
  179. package/src/DUPLICATION_AUDIT_2026_08_07/00_core_reuse_surface.md +0 -738
  180. package/src/DUPLICATION_AUDIT_2026_08_07/A_core_math_geom.md +0 -79
  181. package/src/DUPLICATION_AUDIT_2026_08_07/B_core_data.md +0 -93
  182. package/src/DUPLICATION_AUDIT_2026_08_07/C_gfx_render.md +0 -80
  183. package/src/DUPLICATION_AUDIT_2026_08_07/D_gfx_features.md +0 -164
  184. package/src/DUPLICATION_AUDIT_2026_08_07/E_physics.md +0 -69
  185. package/src/DUPLICATION_AUDIT_2026_08_07/F_sound.md +0 -110
  186. package/src/DUPLICATION_AUDIT_2026_08_07/G_net_save.md +0 -129
  187. package/src/DUPLICATION_AUDIT_2026_08_07/H_engine_misc.md +0 -72
  188. package/src/DUPLICATION_AUDIT_2026_08_07/I_generation_view.md +0 -74
  189. package/src/DUPLICATION_AUDIT_2026_08_07/K_crosscutting.md +0 -169
  190. package/src/DUPLICATION_AUDIT_2026_08_07.md +0 -254
  191. package/src/DUPLICATION_AUDIT_REMEDIATION.md +0 -250
  192. package/src/DUPLICATION_REVIEW_2026_10_24.md +0 -1072
  193. package/src/REVIEW_2026_08_06/adv-core.md +0 -511
  194. package/src/REVIEW_2026_08_06/adv-gfx.md +0 -480
  195. package/src/REVIEW_2026_08_06/adv-view.md +0 -446
  196. package/src/REVIEW_2026_08_06/api-core.md +0 -296
  197. package/src/REVIEW_2026_08_06/api-engine.md +0 -195
  198. package/src/REVIEW_2026_08_06/bvh-phys-sound-net.md +0 -507
  199. package/src/REVIEW_2026_08_06/core-binary.md +0 -348
  200. package/src/REVIEW_2026_08_06/core-collection.md +0 -376
  201. package/src/REVIEW_2026_08_06/core-math.md +0 -221
  202. package/src/REVIEW_2026_08_06/core-process.md +0 -266
  203. package/src/REVIEW_2026_08_06/core-science.md +0 -223
  204. package/src/REVIEW_2026_08_06/engine-ai-nav.md +0 -279
  205. package/src/REVIEW_2026_08_06/engine-animation.md +0 -167
  206. package/src/REVIEW_2026_08_06/engine-asset.md +0 -166
  207. package/src/REVIEW_2026_08_06/engine-ecs.md +0 -920
  208. package/src/REVIEW_2026_08_06/engine-input.md +0 -197
  209. package/src/REVIEW_2026_08_06/engine-misc.md +0 -248
  210. package/src/REVIEW_2026_08_06/engine-terrain.md +0 -52
  211. package/src/REVIEW_2026_08_06/generation.md +0 -176
  212. package/src/REVIEW_2026_08_06/geom-2d.md +0 -434
  213. package/src/REVIEW_2026_08_06/geom-3d.md +0 -117
  214. package/src/REVIEW_2026_08_06/gfx-gi.md +0 -143
  215. package/src/REVIEW_2026_08_06/gfx-render.md +0 -740
  216. package/src/REVIEW_2026_08_06/gfx-texture.md +0 -690
  217. package/src/REVIEW_2026_08_06/prior-criticals.md +0 -49
  218. package/src/REVIEW_2026_08_06/prior-highs.md +0 -138
  219. package/src/REVIEW_2026_08_06/test-gaps.md +0 -364
  220. package/src/REVIEW_2026_08_06/view.md +0 -667
  221. package/src/REVIEW_2026_08_06.md +0 -610
  222. package/src/core/binary/lz4/LZ4_HIGH_COMPRESSION_PLAN_2026_09_14.md +0 -542
  223. package/src/core/bvh2/bvh3/NOTES.md +0 -2
  224. package/src/core/bvh8/NOTES.md +0 -63
  225. package/src/core/cache/wtinylfu/benchmark/CacheWTinylfu.bench.md +0 -243
  226. package/src/core/color/COLOR_FIX_PLAN_2026_08_28.md +0 -417
  227. package/src/core/color/COLOR_REVIEW_2026_08_28.md +0 -779
  228. package/src/core/geom/2d/polygon/TRIANGULATION_DESIGN.md +0 -433
  229. package/src/core/geom/3d/atlas/ATLAS_PORT_PLAN.md +0 -486
  230. package/src/core/geom/3d/atlas/BFF_PLAN.md +0 -174
  231. package/src/core/geom/3d/atlas/REVIEW_02.md +0 -392
  232. package/src/core/geom/3d/gjk/GJK_REVIEW_NOTES.md +0 -146
  233. package/src/core/geom/3d/gjk/NOTES.md +0 -8
  234. package/src/core/geom/3d/topology/TOPO_MESH_RETIREMENT_PLAN.md +0 -135
  235. package/src/core/geom/GEOMETRY_REVIEW_002_API_ISSUES.md +0 -381
  236. package/src/core/geom/GEOMETRY_REVIEW_002_NAMING.md +0 -684
  237. package/src/core/geom/MANIFOLD_COMPARISON_2026_08_01.md +0 -554
  238. package/src/core/model/reactive/evaluation/PREDICATE_APPLICABILITY_PROPOSAL.md +0 -446
  239. package/src/engine/REVIEW_2026_07_03.md +0 -211
  240. package/src/engine/asset/loaders/image/IMAGE_CODEC_REVIEW_2026_08_28.md +0 -815
  241. package/src/engine/control/first-person/DESIGN.md +0 -658
  242. package/src/engine/control/first-person/DESIGN_COLLISION.md +0 -382
  243. package/src/engine/control/first-person/DESIGN_EXTENSIONS.md +0 -563
  244. package/src/engine/control/first-person/TODO.md +0 -160
  245. package/src/engine/ecs/ASYNC_SYSTEMS_PROPOSAL.md +0 -531
  246. package/src/engine/ecs/HIERARCHY_REVIEW.md +0 -717
  247. package/src/engine/ecs/async/WORKER_SYSTEM_PLAN.md +0 -262
  248. package/src/engine/ecs/ik/PLAN.md +0 -179
  249. package/src/engine/ecs/storage/binary/SERIALIZATION_COVERAGE.md +0 -272
  250. package/src/engine/ecs/storage/binary/object-v2/DESIGN.md +0 -199
  251. package/src/engine/ecs/transform/TRANSFORM64_MIGRATION_PLAN_2026_09_04.md +0 -521
  252. package/src/engine/graphics/CONTEXT_LOSS_RECOVERY_PLAN.md +0 -463
  253. package/src/engine/graphics/ecs/camera/pp/NOTES.md +0 -48
  254. package/src/engine/graphics/render/frame_graph/CORRECTNESS_FIXES_2026_09_04.md +0 -659
  255. package/src/engine/graphics/render/frame_graph/FRAME_GRAPH_RECOMMENDATIONS.md +0 -235
  256. package/src/engine/graphics/sh3/path_tracer/REVIEW_001_ACTION_PLAN.md +0 -490
  257. package/src/engine/graphics/texture/sampler/compare/FLIP_IMPLEMENTATION_PLAN.md +0 -466
  258. package/src/engine/input/GAMEPAD_API_PROPOSAL.md +0 -717
  259. package/src/engine/input/ecs/ism/ISM_DESIGN.md +0 -1523
  260. package/src/engine/input/ecs/ism/ISM_IMPLEMENTATION_PLAN.md +0 -627
  261. package/src/engine/navigation/mesh/OVERLAPPING_SURFACES_2026_08_25.md +0 -661
  262. package/src/engine/navigation/mesh/PATHFINDING_PLAN.md +0 -229
  263. package/src/engine/network/CONGESTION_CONTROL.md +0 -87
  264. package/src/engine/network/MESSAGE_TRANSPORT.md +0 -364
  265. package/src/engine/network/SMOOTHNESS_DESIGN.md +0 -400
  266. package/src/engine/physics/cloth/MEASUREMENTS.md +0 -5304
  267. package/src/engine/physics/cloth/PLAN.md +0 -3056
  268. package/src/engine/physics/fluid/GPU_FLUID_CASCADES_PROPOSAL_2026_10_16.md +0 -1382
  269. package/src/engine/physics/fluid/REVIEW_02_PLAN.md +0 -243
  270. package/src/engine/physics/fluid/solver/NOTES.md +0 -46
  271. package/src/engine/sound/SOUND_EMITTER_RETIREMENT_PLAN.md +0 -536
  272. package/src/engine/sound/simulation/IMPLEMENTATION_PLAN.md +0 -432
  273. package/src/engine/sound/simulation/PROBE_PLACEMENT.md +0 -250
  274. package/src/engine/sound/simulation/PROBE_TRANSFER_RESEARCH.md +0 -322
  275. package/src/engine/sound/simulation/REFLECTION_IR_PLAN.md +0 -261
  276. package/src/engine/sound/simulation/VISIBILITY_GRAPH_PLAN.md +0 -121
  277. package/src/engine/sound/sopra/IMPLEMENTATION_PLAN.md +0 -999
  278. package/src/format/image/avif/DECISIONS.md +0 -545
  279. package/src/format/image/jpeg/NOTES.md +0 -3
  280. package/src/format/scene/gltf/MESHOPT_COMPRESSION_PLAN.md +0 -814
  281. package/src/generation/COORDINATES.md +0 -54
  282. package/src/generation/REVIEW_01_ACTION_PLAN.md +0 -628
  283. package/src/generation/foliage/ANNUAL_SHOOTS_BRIEF_2026_10_25.md +0 -220
  284. package/src/generation/foliage/CROWN_FILLING_BRIEF_2026_10_26.md +0 -172
  285. package/src/generation/foliage/CROWN_FULLNESS_BRIEF_2026_10_26.md +0 -86
  286. package/src/generation/foliage/FOLIAGE_DESIGN.md +0 -547
  287. package/src/generation/foliage/FOLIAGE_PLAN.md +0 -198
  288. package/src/generation/foliage/LEAF_ARCHITECTURE_BRIEF_2026_10_26.md +0 -158
  289. package/src/generation/foliage/LOD_REWORK_BRIEF_2026_10_27.md +0 -521
  290. package/src/generation/foliage/REAL_TIME_TREES_BRIEF_2026_10_27.md +0 -86
  291. package/src/generation/foliage/SELF_ORGANIZING_FRAMEWORK_BRIEF_2026_10_26.md +0 -347
  292. package/src/generation/foliage/SHOOT_SYSTEMS_ALL_SPECIES_BRIEF_2026_10_26.md +0 -248
  293. package/src/generation/foliage/SPEEDTREE_COMPARISON_2026_10_24.md +0 -694
  294. package/src/generation/foliage/TWO_FRONT_ENDS_BRIEF_2026_10_28.md +0 -552
  295. package/src/shade/RENDERER_CONTRACT.md +0 -697
  296. package/src/shade/SHADER_F16_PROPOSAL_2026_10_24.md +0 -263
  297. package/src/shade/device/IMMEDIATE_DATA_PLAN_2026_08_28.md +0 -569
  298. package/src/shade/device/PASS_ENCODER_PROPOSAL_2026_08_31.md +0 -755
  299. package/src/shade/device/mock/EXTENSION_PROPOSAL_2026_08_22.md +0 -458
  300. package/src/shade/device/mock/IMPLEMENTATION_PLAN.md +0 -255
  301. package/src/shade/device/mock/REVIEW_2026_08_22.md +0 -527
  302. package/src/shade/device/timing/GPU_PROFILER_PROPOSAL_2026_08_28.md +0 -894
  303. package/src/shade/device/timing/profile/SGPT_FORMAT.md +0 -345
  304. package/src/shade/renderer/NOTES.md +0 -78
  305. package/src/shade/renderer/PLAN.md +0 -8
  306. package/src/shade/renderer/animation/ADDITIVE_BLENDING_PLAN.md +0 -145
  307. package/src/shade/renderer/animation/SKINNING_FRAME_PLAN.md +0 -357
  308. package/src/shade/renderer/atmosphere/NOTES.md +0 -6
  309. package/src/shade/renderer/camera/orbital/MomentumAxis.d.ts +0 -76
  310. package/src/shade/renderer/camera/orbital/MomentumAxis.d.ts.map +0 -1
  311. package/src/shade/renderer/camera/orbital/MomentumAxis.js +0 -164
  312. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.d.ts +0 -45
  313. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.d.ts.map +0 -1
  314. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.js +0 -66
  315. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.d.ts +0 -98
  316. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.d.ts.map +0 -1
  317. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.js +0 -184
  318. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts +0 -175
  319. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts.map +0 -1
  320. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.js +0 -411
  321. package/src/shade/renderer/camera/orbital/README.md +0 -81
  322. package/src/shade/renderer/camera/orbital/index.d.ts +0 -6
  323. package/src/shade/renderer/camera/orbital/index.d.ts.map +0 -1
  324. package/src/shade/renderer/camera/orbital/index.js +0 -9
  325. package/src/shade/renderer/extension/RENDER_EXTENSION_DESIGN.md +0 -964
  326. package/src/shade/renderer/fluid/GPU_FLUID_OCCUPANCY_AND_SOTA_REVIEW_2026_10_26.md +0 -361
  327. package/src/shade/renderer/geometry/bvh/BLAS_REFIT_BATCH_PLAN.md +0 -619
  328. package/src/shade/renderer/geometry/meshlet/COMPACTION_PLAN_2026_09_16.md +0 -674
  329. package/src/shade/renderer/geometry/sdf/NOTES.md +0 -6
  330. package/src/shade/renderer/geometry/virtual/ATTRIBUTE_PLAN_2026_09_18.md +0 -540
  331. package/src/shade/renderer/geometry/virtual/MICRON_COMPARISON_2026_08_20.md +0 -305
  332. package/src/shade/renderer/geometry/virtual/VGEO_FORMAT.md +0 -913
  333. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +0 -787
  334. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_PLAN.md +0 -481
  335. package/src/shade/renderer/geometry/virtual/VK_LOD_CLUSTERS_COMPARISON_2026_08_20.md +0 -301
  336. package/src/shade/renderer/geometry/virtual/WEDGE_PLAN_2026_09_18.md +0 -448
  337. package/src/shade/renderer/geometry/virtual/runtime/CULL_PLAN_2026_09_16.md +0 -385
  338. package/src/shade/renderer/geometry/virtual/runtime/CUT_PLAN_2026_09_14.md +0 -656
  339. package/src/shade/renderer/geometry/virtual/runtime/REVIEW_LEDGER.md +0 -308
  340. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN.md +0 -657
  341. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN_2026_09_14.md +0 -551
  342. package/src/shade/renderer/geometry/virtual/runtime/SHADOW_PLAN_2026_09_28.md +0 -529
  343. package/src/shade/renderer/geometry/virtual/sdf/VGEO_SDF.d.ts +0 -47
  344. package/src/shade/renderer/geometry/virtual/sdf/VGEO_SDF.d.ts.map +0 -1
  345. package/src/shade/renderer/geometry/virtual/sdf/VGEO_SDF.js +0 -80
  346. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_collect.d.ts +0 -16
  347. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_collect.d.ts.map +0 -1
  348. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_collect.js +0 -67
  349. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_decode.d.ts +0 -17
  350. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_decode.d.ts.map +0 -1
  351. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_decode.js +0 -145
  352. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_traverse.d.ts +0 -12
  353. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_traverse.d.ts.map +0 -1
  354. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_traverse.js +0 -105
  355. package/src/shade/renderer/global_illumination/lpv/NOTES.md +0 -3
  356. package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_CLIPMAP_PROPOSAL_2026_10_27.md +0 -4306
  357. package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_RAYS_VARIABLE_AND_SORTED_SCOPE_2026_10_30.md +0 -410
  358. package/src/shade/renderer/global_illumination/sharc/NOTES.md +0 -8
  359. package/src/shade/renderer/gpu_primitive/bvh/GPU_BVH_BUILD_PLAN.md +0 -1369
  360. package/src/shade/renderer/gpu_primitive/bvh/PHASE1_HANDOVER.md +0 -202
  361. package/src/shade/renderer/gpu_primitive/indirect/NOTES.md +0 -94
  362. package/src/shade/renderer/hiz/NOTES.md +0 -3
  363. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts +0 -8
  364. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts.map +0 -1
  365. package/src/shade/renderer/light/cluster/NOTES.md +0 -11
  366. package/src/shade/renderer/light/cluster/PROPOSAL_ALGORITHM_2026_10_16.md +0 -524
  367. package/src/shade/renderer/light/cluster/PROPOSAL_HIERARCHICAL_2026_10_16.md +0 -1101
  368. package/src/shade/renderer/material/resident/shader_resident_texture_means.d.ts +0 -24
  369. package/src/shade/renderer/material/resident/shader_resident_texture_means.d.ts.map +0 -1
  370. package/src/shade/renderer/material/resident/shader_resident_texture_means.js +0 -120
  371. package/src/shade/renderer/particles/DESIGN.md +0 -1011
  372. package/src/shade/renderer/particles/EMITTER_GLOBAL_STATE_PLAN_2026_10_29.md +0 -280
  373. package/src/shade/renderer/particles/ORIENTATION_PLAN_2026_10_15.md +0 -453
  374. package/src/shade/renderer/path_tracer/PATH_TRACED_MEDIA_PLAN_2026_10_02.md +0 -520
  375. package/src/shade/renderer/postprocess/bloom/NOTES.md +0 -6
  376. package/src/shade/renderer/postprocess/fsr/NOTES.md +0 -4
  377. package/src/shade/renderer/postprocess/gtao/NOTES.md +0 -6
  378. package/src/shade/renderer/postprocess/nss/NOTES.md +0 -332
  379. package/src/shade/renderer/postprocess/perspective/NOTES.md +0 -3
  380. package/src/shade/renderer/postprocess/sscs/NOTES.md +0 -5
  381. package/src/shade/renderer/postprocess/ssr/NOTES.md +0 -14
  382. package/src/shade/renderer/postprocess/ssr/REPROJECTION_PROPOSAL.md +0 -286
  383. package/src/shade/renderer/postprocess/ssr/prefilter/NOTES.md +0 -39
  384. package/src/shade/renderer/postprocess/taa/NOTES.md +0 -11
  385. package/src/shade/renderer/postprocess/upscale-dither/NOTES.md +0 -78
  386. package/src/shade/renderer/postprocess/vrs/NOTES.md +0 -4
  387. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_LEDGER.md +0 -1218
  388. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_PLAN_2026_09_03.md +0 -600
  389. package/src/shade/renderer/restir/reservoir/NOTES.md +0 -6
  390. package/src/shade/renderer/shader/chunk/atmosphere/NOTES.md +0 -6
  391. package/src/shade/renderer/shader/chunk/brdf/FRESNEL_BRIEF_2026_10_16.md +0 -391
  392. package/src/shade/renderer/shader/chunk/color/tonemap/ACES_TONE_MAPPING_BRIEF_2026_10_17.md +0 -153
  393. package/src/shade/renderer/shader/chunk/color/tonemap/HDR_TONE_MAPPING_FORMS_BRIEF_2026_10_16.md +0 -322
  394. package/src/shade/renderer/shader/chunk/color/tonemap/SDR_TONE_MAPPING_BRIEF_2026_10_16.md +0 -247
  395. package/src/shade/renderer/shader/chunk/fsr/NOTES.md +0 -3
  396. package/src/shade/renderer/shader/chunk/fsr/spd/NOTES.md +0 -4
  397. package/src/shade/renderer/shader/chunk/geometry/ray/NOTES.md +0 -4
  398. package/src/shade/renderer/shader/chunk/material/MULTISCATTER.md +0 -159
  399. package/src/shade/renderer/shader/chunk/mboit/NOTES.md +0 -38
  400. package/src/shade/renderer/shader/chunk/spherical_harmonics/l1i/NOTES.md +0 -3
  401. package/src/shade/renderer/shader/chunk/spherical_harmonics/sampling/NOTES.md +0 -8
  402. package/src/shade/renderer/shader/compiler/SHADER_SWITCH_PROPOSAL_2026_10_16.md +0 -927
  403. package/src/shade/renderer/shadow/contact/FINDINGS.md +0 -413
  404. package/src/shade/renderer/shadow/map/csm/NOTES.md +0 -5
  405. package/src/shade/renderer/shadow/ray/NOTES.md +0 -31
  406. package/src/shade/renderer/shadow/sdf/NOTES.md +0 -9
  407. package/src/shade/renderer/shadow/sdf/brixelizer/NOTES.md +0 -5
  408. package/src/shade/renderer/shadow/sdf/cascade/NOTES.md +0 -9
  409. package/src/shade/renderer/shadow/vsm/NOTES.md +0 -12
  410. package/src/shade/renderer/shadow/vsm/PLAN.md +0 -442
  411. package/src/shade/renderer/shadow/vsm/REVIEW_LEDGER.md +0 -43
  412. package/src/shade/renderer/texture/COMPRESSED_TEXTURE_PLAN_2026_09_01.md +0 -2444
  413. package/src/shade/renderer/texture/mipmaps/NOTES.md +0 -3
  414. package/src/shade/renderer/volumetrics/NOTES.md +0 -1329
  415. package/src/shade/renderer/volumetrics/VOLUMETRIC_NOISE_PLAN_2026_10_15.md +0 -336
  416. package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts +0 -23
  417. package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts.map +0 -1
  418. package/src/shade/renderer/water/medium/GPUWaterBodies.js +0 -75
  419. package/src/shade/wgsl/NOTES.md +0 -4
@@ -1,391 +0,0 @@
1
- # Fresnel for the direct lobe and the split-sum LUT: a brief (2026-10-16)
2
-
3
- **Status: decided and built.** Alex chose Schlick, in the form Unreal's rasterizer uses; it landed in dbdb1cdb1
4
- (§7). The sections before §7 are the brief as it was written, with the shading it describes as current. Every number
5
- here was measured; how, and where the scripts are, is in §8.
6
-
7
- Shade evaluates one material with two Fresnels. The direct lobe (`chunk_BRDF_GGX`) uses `F_Hauber`; the split-sum LUT
8
- `tSplitSum`, read by every indirect specular path and by `re_direct_multiscatter`, encodes `f0 · A + f90 · B` with
9
- Schlick's form at the fourth power. `MULTISCATTER.md` records three consequences: a dielectric's indirect specular is up
10
- to 0.068 of a uniform sky above its direct, a white metal's direct path reaches only 0.93, and Turquin's eq. 16
11
- overshoots the Heitz 2016 random walk partly because Hauber's dip compounds on every bounce.
12
-
13
- ## 1. Recommendation
14
-
15
- **Use Schlick's `(1 − VoH)⁵` in the direct lobe, and regenerate the split-sum LUT with the same weight in both
16
- channels.** Keep the LUT, a 64² rg16float texture read with one fetch, and replace its contents. In §5 this is
17
- pairing E.
18
-
19
- - **All three inconsistencies close.** Direct and indirect agree to the LUT's precision: the worst gap in the f64
20
- model goes from 0.071 to 0.000. A white metal's direct furnace goes from 0.929 to 1.000. At roughness 0.99, eq. 16
21
- against a random walk using the same Fresnel goes from +4.7% to 0.0% for a white metal and from +5.7% to +1.0% for
22
- silver.
23
- - **It costs one multiply per light.** Per vec3, it is 13 DXIL float operations against Hauber's 12, with no
24
- transcendental. The LUT fetch and the per-pixel compensation are unchanged.
25
- - **Against exact Fresnel it ties with Hauber.**
26
- - Dielectrics: the two are within 0.0033 of each other at every angle.
27
- - Five measured metals: mean |ΔF| 0.0139 against Hauber's 0.0148. Each wins on different metals (§3).
28
- - It is far more accurate than what the indirect path uses now. The shipped x⁴ LUT is up to +0.079 in albedo at
29
- IOR 1.5 (up to +29%).
30
- - **What it changes visually** (§6):
31
- - Dielectric reflections at grazing view darken toward the exact value: −12% to −34% of a sphere rim's linear
32
- luminance. At roughness 0.26, N·V 0.26, the indirect albedo goes from 0.288 to 0.2225, and exact is 0.229.
33
- - Metal spheres move by less than 2.3% at the rim.
34
- - A back-lit white-metal or silver rim brightens by about 1%.
35
- - **It is the white-tint case of F82-tint**, the metal Fresnel of OpenPBR and Adobe's Standard Material. If a metal
36
- edge tint is ever added to the material, it becomes a third LUT channel on top of this, not a rewrite (§4).
37
-
38
- **What would reverse it: preferring the grazing dip of high-index grey metals over silver, gold, copper and the perfect
39
- conductor.** For aluminium and iron at low roughness, Hauber is 0.03 to 0.06 closer in albedo; chromium, nickel and
40
- titanium follow the same physics. For silver, Hauber is 0.06 worse.
41
-
42
- If that look matters, Hauber's consistent form is pairing B (§5): it adds an `Ess` channel to the LUT. That closes the
43
- gap, but the white furnace stays at 0.93 and the walk overshoot at +4.7%.
44
-
45
- ## 2. Why the two paths disagree: Hauber's dip forces a choice
46
-
47
- At `f90 = 1`, which every caller passes, `F_Hauber = f0 (1 − x⁴) + x⁵` with `x = 1 − VoH`. At `F0 = 1` it is
48
- `1 − VoH x⁴`, which dips to 0.918 at VoH = 0.2.
49
-
50
- Turquin's compensation needs `Ess`, the albedo with `F ≡ 1`, and Shade reads it as `A + B`. A two-channel LUT can hold
51
- Hauber's split or `A + B = Ess`, but not both:
52
-
53
- - **Before 933feac74**, the asset held Hauber's split exactly. That commit's own check found A within 4.9e-4 of the
54
- `1 − f⁴` term and B within 4.8e-4 of the `f⁵` term. Its `A + B` was therefore the dipped white albedo. 933feac74 read
55
- that shortfall (up to −8.2%) as a stale asset.
56
- - **933feac74 moved B to x⁴.** Now `A + B = Ess`, but `f0 · A + B` is Schlick at the fourth power, not Hauber, and the
57
- indirect path has used a different Fresnel since. The furnace's 0.068 dielectric gap is that difference:
58
- `(1 − f0)x⁴ − (x − f0)x⁴ = VoH x⁴`, up to 0.082.
59
-
60
- **The f64 model of the pre-933feac74 pairing gives no gap and a white furnace of 1.000, but for the wrong reason.**
61
- `Kms = (1 − A − B) / (A + B)` counts the dip as missing multiple scattering, so it stays positive even as roughness
62
- goes to 0.
63
-
64
- **Any Fresnel that is linear in F0, with `F(1, μ) ≡ 1`, removes the choice.** `f0 · A + f90 · B` is then its split,
65
- and `A + B` is its `Ess`. Schlick at any exponent qualifies; Hauber does not.
66
-
67
- ## 3. Hauber's approximation, re-examined
68
-
69
- ### Source
70
-
71
- Niklas Hauber, "Finding Alternatives to Schlick's Approximation of the Fresnel Equations", photometric.io, 2024-06-27
72
- (the URL in `chunk_F_Hauber`). He found it by GPU symbolic regression, with parameters R0 and 1 − cos θ:
73
-
74
- R(θ) = R0 + (1 − cos θ − R0)(1 − cos θ)⁴
75
-
76
- **What he fitted it to:**
77
-
78
- - 4,096 points, θ uniform "from 0 to π" as written.
79
- - 40% at IOR 1.5, and 40% at an IOR uniform in [1.34, 1.7].
80
- - 20% from seven complex IORs: 0.3+3i, 0.15+4i, 0.3+3.35i, 1.4+7i, 1.84+3.73i, 2.1+4i, 2.9+3.1i.
81
-
82
- So the fit covers both dielectrics and conductors, mostly dielectrics, and most of its metals have a real part large
83
- enough to dip.
84
-
85
- **What he reports:** 34% lower MSE than Schlick, essentially the same shape for dielectrics, almost 50% better on his
86
- metals, with the warning that another dataset would differ. He also reports 5 CUDA instructions against 6. He offers a
87
- shape blend, `R0 + (1 − R0 − s cos θ)(1 − cos θ)⁴`, where `s = 1` is his form and `s = 0` is Schlick at the fourth
88
- power. The post does not discuss F0 → 1.
89
-
90
- ### Shade's form
91
-
92
- `mix(f0, f90 − VoH, x⁴)` is `f0 + (f90 − VoH − f0) x⁴`. At `f90 = 1` this is Hauber's formula exactly, and it equals
93
- Schlick x⁵ minus `F0 · VoH x⁴`: the dip is proportional to F0, so it is deepest at F0 = 1. The `f90` is
94
- Shade's own generalisation; the post has none. The dip term does not scale with it, so at `f90 = 0` the form is
95
- negative (`−VoH x⁴`). Every caller passes 1, so that never runs.
96
-
97
- **His claim reproduces on his dataset:** 35.5% lower MSE (θ in [0, π/2]; the same with |cos θ| over [0, π]), 1.6% on
98
- dielectrics, 49% on metals. So the formula is read correctly.
99
-
100
- ### Against exact Fresnel
101
-
102
- **Dielectrics** use the exact unpolarised Fresnel. Error over θ uniform in [0°, 90°]; "albedo" is the GGX lobe's
103
- directional albedo at the 25 furnace points (roughness 0.26–0.99, N·V 0.10–0.99):
104
-
105
- | IOR 1.5 (F0 0.04) | max \|ΔF\| | mean \|ΔF\| | ΔF_avg | worst Δalbedo | worst relative Δalbedo |
106
- |---|---|---|---|---|---|
107
- | Hauber | 0.033 at 85° | 0.0093 | −0.0068 | −0.0185 | −21.5% |
108
- | Schlick x⁵ | 0.036 at 85° | 0.0092 | −0.0061 | +0.0194 | −20.2% |
109
- | Schlick x⁴ (the shipped LUT) | 0.103 at 82° | 0.0214 | +0.0122 | +0.0785 | +28.7% |
110
-
111
- The worst absolute and the worst relative errors fall at different points. For Hauber and Schlick x⁵ the worst
112
- relative error is at mid angles, where the albedo is small and both read low (0.054 against 0.069 at roughness 0.51,
113
- N·V 0.51).
114
-
115
- Over IOR 1.3–2.0, the worst case for each:
116
-
117
- | | max \|ΔF\| | mean \|ΔF\| | worst \|Δalbedo\| | worst relative |
118
- |---|---|---|---|---|
119
- | Hauber | 0.067 | 0.0119 | 0.047 | −21.5% |
120
- | Schlick x⁵ | 0.068 | 0.0121 | 0.048 | −20.2% |
121
- | Schlick x⁴ | 0.142 | 0.0313 | 0.109 | +54.1% (IOR 1.3) |
122
-
123
- For a dielectric, Hauber is Schlick x⁵ minus `F0 · VoH x⁴`, at most 0.0033 at F0 0.04.
124
-
125
- **Conductors** use measured n and k: Johnson & Christy 1972 for gold, silver and copper; Johnson & Christy 1974 for
126
- iron; McPeak et al. 2015 for aluminium (Rakić 1995 has 13 rows in the visible, and its dip agrees to 0.012). The full
127
- complex Fresnel equations are integrated over 380–780 nm against D65 and the CIE 1931 2° observer (the CIE's own
128
- 1 nm tables, taken at 5 nm), then projected to linear sRGB. That gives gold F0 (1.038, 0.729, 0.365) and aluminium
129
- (0.906, 0.916, 0.923). Each candidate is fed the exact curve's own F0; gold's red is left above 1.
130
-
131
- Worst channel per metal. The Δalbedo column is the worst over the 25 lobes:
132
-
133
- | metal | Hauber max \|ΔF\| | Schlick x⁵ max \|ΔF\| | Hauber Δalbedo | Schlick x⁵ Δalbedo | measured dip below F0 |
134
- |---|---|---|---|---|---|
135
- | silver | 0.078 | 0.007 | −0.065 | +0.005 | 0.3% |
136
- | gold | 0.062 | 0.033 | −0.051 | +0.029 | 4.5% (red) |
137
- | copper | 0.055 | 0.043 | −0.044 | +0.033 | 1.3% (red) |
138
- | aluminium | 0.049 | 0.109 | −0.026 | +0.082 | 7.1% (red) |
139
- | iron | 0.141 | 0.178 | +0.103 | +0.135 | 4.7% (red) |
140
- | mean of the 15 channels' mean \|ΔF\| | 0.0148 | 0.0139 | | | |
141
-
142
- Over all five metals, Hauber's MSE is 22% below Schlick's. That comes from aluminium (85% lower) and iron (47%). On
143
- gold it is 5× Schlick's, on silver 212×, on copper about equal.
144
-
145
- **As F0 → 1**, Hauber's minimum over angle falls further below F0:
146
-
147
- | F0 | 0.5 | 0.8 | 0.9 | 0.95 | 0.98 | 1.0 |
148
- |---|---|---|---|---|---|---|
149
- | Hauber, minimum | −0.5% | −3.4% | −5.4% | −6.7% | −7.6% | −8.2% (0.918 at VoH 0.20) |
150
-
151
- Schlick at either exponent never falls below F0, and is exactly 1 at F0 = 1. Only a perfect conductor has F0 = 1, and
152
- it has no dip. Silver, the nearest real metal (F0 0.98), dips 0.3%.
153
-
154
- ### The dip belongs to n, not to F0
155
-
156
- Fitting Hauber's shape parameter `s` to each measured channel by least squares:
157
-
158
- | | silver | gold | copper | aluminium | iron |
159
- |---|---|---|---|---|---|
160
- | best `s` (R, G, B) | 0.09, 0.09, 0.05 | 0.38, 0.24, 0.58 | 0.34, 0.86, 0.88 | 1.15, 0.88, 0.64 | 2.24, 2.21, 1.96 |
161
- | F0 (R, G, B) | 0.99, 0.98, 0.98 | 1.04, 0.73, 0.36 | 0.93, 0.62, 0.52 | 0.91, 0.92, 0.92 | 0.53, 0.51, 0.49 |
162
-
163
- Silver and aluminium have nearly the same F0 and need opposite shapes. The dip comes from the real part of the index
164
- (Lazányi's correction has its slope at 2n), and the G-buffer carries only F0.
165
-
166
- **So no function of F0 alone can carry the dip.** Hauber's fit applies the average dip of its seven metals to every
167
- F0, including F0 = 1. It is right for aluminium-like metals and wrong for silver-like ones.
168
-
169
- ## 4. The state of the art in pure functions
170
-
171
- ### Fresnel
172
-
173
- DXIL is from Chrome's own Tint and DXC on the RTX 4090. Float operations per vec3 are counted, with the input loads
174
- excluded.
175
-
176
- | form | inputs | DXIL float ops (div, sqrt, exp/log) | five metals: worst max \|ΔF\| / mean \|ΔF\| | dielectric IOR 1.5 max \|ΔF\| | F0 = 1 |
177
- |---|---|---|---|---|---|
178
- | Schlick 1994, x⁵ (multiplies) | F0 | 13 (0) | 0.178 / 0.0139 | 0.036 | 1 |
179
- | Schlick x⁵ as `chunk_F_Schlick` (`pow`) | F0 | 13 (log + exp) | same | same | 1 |
180
- | Schlick x⁴ (the shipped LUT) | F0 | 12 (0) | 0.213 / 0.0186 | 0.103 | 1 |
181
- | Hauber 2024 (shipped direct) | F0 | 12 (0) | 0.141 / 0.0148 | 0.033 | 0.918 |
182
- | Lazányi–Schlick 2005 | n, k → F0, a, α | 25 (log + exp) | needs n | — | — |
183
- | F82-tint: Hoffman 2019, Adobe ASM, OpenPBR | F0 + edge tint | 40 (3 div) | 0.020 / 0.0026 (tint from data) | — | 1 at tint 1 |
184
- | Gulbrandsen 2014, (r, g) → n, k | F0 + edge tint | 109 (15 div, 3 sqrt) | exact up to its approximate conductor formula | — | singular, clamped to 0.99 |
185
- | exact dielectric (`F_dielectric`) | IOR | 28, scalar (4 div, 1 sqrt), branch | — | 0 | — |
186
- | exact conductor | n, k | 85 (6 div, 6 sqrt) | 0 | — | — |
187
-
188
- Notes on the sources (the literature summaries are with the scripts, §8):
189
-
190
- - **Schlick 1994** was not read first-hand: the only reachable copy is an image-only scan. Its formula is as Hoffman
191
- 2019 (eq. 1) and OpenPBR give it.
192
-
193
- - **Lazányi & Szirmay-Kalos 2005** (WSCG): subtract `a μ (1 − μ)^α` from Schlick. `a = 2n` comes from the slope at
194
- grazing incidence; α is fitted at μ = 0.15. Their stated relative errors: Cu 1.9–4.9%, Au 5.0–5.4%, Ag 4.4–9.0%,
195
- Al 2.0–17.5%. The inputs are n and k.
196
- - **Gulbrandsen 2014** (JCGT 3(4)): maps reflectivity and edge tint to n, k, then evaluates PBRT's approximate
197
- conductor formula. Standard Surface uses it; MaterialX feeds the result to its full conductor Fresnel.
198
- - **Hoffman 2019 and 2023, Kutz et al. 2021/2023 (Adobe Standard Material), OpenPBR 1.1.1:**
199
- `F = Schlick(μ) − b μ (1 − μ)⁶`, with `b = Schlick(1/7)(1 − tint)/(μ̄(1 − μ̄)⁶)` and μ̄ = 1/7, so that F(82°) =
200
- tint · Schlick(82°). It is linear in F0, F90 and b, so a split-sum LUT needs one more channel, `∫ μ(1 − μ)⁶`.
201
- The OpenPBR companion paper gives its hemispherical average as `F0 + (1 − F0)/21 − b/126`. Unreal 5.4 has the
202
- Adobe F82 tint only in its Substrate path tracer (disabled by default); its rasterizer runs Schlick.
203
- - **Filament and Frostbite** use Schlick with a shadowed F90. Belcour et al. 2020 needs a tabulated basis.
204
- Vernooij 2024 fits the Lazányi form per metal. No F0-only metal approximation newer than Hauber's was found.
205
-
206
- ### Split-sum (DFG) fits
207
-
208
- **Keep the LUT.** It is one fetch that both paths already pay, since the compensation reads it anyway. It is also an
209
- order of magnitude more accurate than the best published fit, which costs 26 to 89 float operations.
210
-
211
- Each fit was measured against the f64 x⁵ split of Shade's lobe (GGX, height-correlated Smith, α = r²). The points are
212
- a 32² grid of N·V ≥ 0.05 and roughness, which falls midway between the 64-wide LUT's texel centres, its worst place to
213
- interpolate. DXIL is counted as above.
214
-
215
- | fit | fitted to | DXIL float ops | white (F0 1) albedo: max \|ΔE\| / mean | F0 0.04: max \|ΔE\| |
216
- |---|---|---|---|---|
217
- | **the LUT, regenerated at x⁵** | Shade's lobe | **1 sample** | **0.0027 (0.3%) / 0.0002** | **0.0009** |
218
- | Karis 2014 `EnvBRDFApprox` (`DFGApprox`) | UE4's G, "adapted" | 16 (1 exp) | 0.342 (−43%) / 0.106 | 0.083 |
219
- | Narkowicz 2014, polynomial | uncorrelated Smith, α = (1 − gloss)⁴ | 36 (1 sqrt) | 0.221 (−28%) / 0.019 | 0.080 |
220
- | Narkowicz 2014, Lazarov form | same | — | 0.315 (−39%) / 0.041 | 0.080 |
221
- | Pesce & Iwanicki 2015 | not stated; roughness = √r, the best of three readings | 15 (1 sqrt, 1 exp) | 0.126 (−15%) / 0.028 | 0.135 |
222
- | UE 5.4 analytic energy fit ("WIP") | height-correlated table | 26 (2 div, 3 log, 3 exp) | 0.058 (+6.5%) / 0.0064 | 0.015 |
223
- | MaterialX `mx_ggx_dir_albedo_analytic` (2022) | height-correlated, Schlick | 89 (2 div) | 0.084 (−8.4%) / 0.0060 | 0.025 |
224
- | Kulla & Conty 2017, E only | height-correlated | 38 (div, sqrt, log, exp) | 0.011 (3.5%) / 0.0022 | — |
225
- | Sforza & Pellacini 2022, E only | uncorrelated Smith | — | 0.226 (−28%) / 0.016 | — |
226
-
227
- Notes on the table:
228
-
229
- - **Worst cases.** Most fits are worst at grazing (N·V 0.08) and rough. The LUT's three columns below N·V 0.05 reach
230
- 0.019.
231
- - **Fdez-Agüera 2019 is not a fit.** It reads Epic's LUT, and its contribution is a multiple-scattering form, with
232
- `Favg = F0 + (1 − F0)/21` for Schlick.
233
- - **Kulla & Conty keep tables.** Their slides favour tables over fits. The E-only fits can stand in for Turquin's
234
- `Ess`, but not for the split.
235
- - **Turquin lists analytic fits as future work.** His conclusion names good analytic fits for all the smooth tables as
236
- future work, to save texture fetches in real time. No published fit reaches the LUT's accuracy for a
237
- height-correlated lobe.
238
- - **Engines moved from fits to LUTs in 2025.** three.js dropped Karis's fit for a height-correlated LUT (PR #32054,
239
- October 2025). Godot's Forward+ renderer moved to a LUT in March 2025; its mobile renderers keep Karis's fit.
240
- Filament, EEVEE and Unity HDRP use LUTs only.
241
- - **One Shade pass still uses a fit:** `ssr_combine_shader.js` weights by `DFGApprox`, which is 0.34 (43%) below Shade's
242
- white albedo at grazing, rough. That is already reported (2026-10-03) and separate from this brief.
243
-
244
- ## 5. The pairings against the three inconsistencies
245
-
246
- Measured in an f64 model of Shade's two paths under a uniform sky, at the 25 furnace points, for white, a 0.04
247
- dielectric and the five measured metals:
248
-
249
- - direct = `E_F(F0) · (1 + F0 Kms)`
250
- - indirect = `(F0 A + B)(1 + F0 Kms)`
251
-
252
- The model reproduces the real passes' furnace sweep (`white_furnace.spec.js`'s helpers, run through the emulator):
253
- white direct 0.9301 against 0.9302, dielectric 0.2198 against 0.2196, gap 0.0682 against 0.0684.
254
-
255
- The walk is the shared bench's port of Heitz 2016, 4M paths per point. Each Fresnel candidate and the exact Fresnel
256
- are applied at every bounce, and the conductors are walked per wavelength (10 nm) before projection.
257
-
258
- | pairing | worst indirect − direct | white direct, min | eq. 16 vs its own walk, r 0.99: white / silver | LUT |
259
- |---|---|---|---|---|
260
- | A. shipped: Hauber direct, LUT x⁴ | +0.071 (white), +0.068 (dielectric) | 0.929 | +4.7% / +5.7% | as is |
261
- | B. Hauber direct, LUT (1 − x⁴, x⁵) + an `Ess` channel | 0.000 | 0.929 | +4.7% / +5.7% | 3 channels |
262
- | C. Hauber direct, LUT (1 − x⁴, x⁵), `Kms` from A + B (pre-933feac74) | 0.000 | 1.000, by booking the dip as multiple scattering | — | regenerate |
263
- | D. Schlick x⁴ in both | 0.000 | 1.000 | 0.0% / +0.9% | as is |
264
- | **E. Schlick x⁵ in both** | **0.000** | **1.000** | **0.0% / +1.0%** | **regenerate** |
265
-
266
- **D needs no new asset, but it moves the direct path to the least accurate form.** For a dielectric, it puts the
267
- +29% albedo error of today's indirect path into the direct one, and it is worse on every metal (§3).
268
-
269
- **E is the shipped LUT's two channels, regenerated.**
270
-
271
- **Against the walk with exact Fresnel**, the worst over N·V and channels, at roughness 0.26:
272
-
273
- | | white | silver | gold | copper | aluminium | iron | dielectric IOR 1.5 |
274
- |---|---|---|---|---|---|---|---|
275
- | Hauber (A's, B's direct) | −7.1% | −6.9% | −8.0% | −5.4% | +3.4% | +17.3% | −22% |
276
- | Schlick x⁵ (E) | 0.0% | +0.5% | −4.1% | +4.3% | +10.7% | +23.4% | −21% |
277
- | Schlick x⁴ (A's indirect, D) | 0.0% | +0.6% | +6.4% | +9.2% | +11.5% | +28.9% | +25% |
278
-
279
- The dielectric's relative errors are on small values. At N·V 0.51 they are 0.0687 (x⁵) and 0.0962 (x⁴) against an
280
- exact 0.0869. At N·V 0.26 they are 0.2225 and 0.2879 against 0.2294.
281
-
282
- **At roughness 0.99, the Fresnel no longer decides; Turquin's `Fms = F0` does.** Against the exact walk, eq. 16 is
283
- +21.7% for iron, +17.2% for copper and +23.8% in gold's blue under every Fresnel. That is the compensation's own
284
- saturation error, which `MULTISCATTER.md` already ledgers.
285
-
286
- **Hauber's dip compounding is the part E removes.** Against walks with their own Fresnel at roughness 0.99, eq. 16's
287
- mean overshoot is:
288
-
289
- | | white | silver | aluminium | gold | copper | iron |
290
- |---|---|---|---|---|---|---|
291
- | Hauber | +0.026 | +0.029 | +0.036 | +0.024 | +0.031 | +0.025 |
292
- | Schlick x⁵ | 0.000 | +0.004 | +0.016 | +0.010 | +0.020 | +0.021 |
293
-
294
- Against the exact walk the totals move the other way, by 0.003 to 0.008 for the metals: Hauber's dip had been
295
- offsetting part of `Fms = F0`'s overshoot.
296
-
297
- ## 6. The visual change
298
-
299
- Rendered on the RTX 4090 with the multiscatter bench's sphere sweep: seven roughnesses (0.1–1.0) by seven materials
300
- (gold, copper, aluminium, iron and silver at their table F0, a white metal at F0 1, and a black dielectric), IBL, TAA,
301
- EV −1.
302
-
303
- Two lightings:
304
-
305
- - the bench's sun, which comes from the camera's side, so VoH is about 0.9 on every sphere;
306
- - the same sun from behind the spheres, so their rims see VoH ≈ 0.27, where Hauber's dip lives.
307
-
308
- No engine file was edited. The "after" page's prelude rewrites `F_Hauber`'s body to Schlick x⁵ in the one module that
309
- declares it, and replaces `split_sum.bin`'s texels as they are fetched. The replacement is the shipped texels plus the
310
- f64 change of weight, `∫ VoH x⁴`, so only the Fresnel differs, not the integration. A repeat of the shipped arm
311
- differed by at most 0.27% (front) and 0.09% (back).
312
-
313
- Change in mean linear luminance, after against before, over the outer rim (0.75–0.92 of the radius):
314
-
315
- | | 0.1 | 0.25 | 0.4 | 0.55 | 0.7 | 0.85 | 1.0 |
316
- |---|---|---|---|---|---|---|---|
317
- | black dielectric, front | −34.1% | −32.8% | −27.6% | −21.4% | −18.4% | −14.9% | −12.3% |
318
- | black dielectric, back | −33.7% | −17.3% | −7.5% | −11.6% | −17.2% | −15.6% | −13.4% |
319
- | iron, front | −2.3% | −2.2% | −1.6% | −0.9% | −0.6% | −0.4% | −0.3% |
320
- | copper, front | −1.5% | −1.4% | −1.0% | −0.5% | −0.3% | −0.2% | −0.1% |
321
- | white metal, back | 0.0% | +0.9% | +1.3% | +0.8% | +0.2% | 0.0% | 0.0% |
322
- | silver, back | −0.1% | +0.6% | +1.0% | +0.5% | +0.1% | 0.0% | 0.0% |
323
-
324
- Over whole spheres, metals move by at most 0.4% and the black dielectric by 2–8%.
325
-
326
- **Where the eye sees it is the dielectric's Fresnel rim under the sky.** It loses the x⁴ LUT's excess and lands near
327
- exact: 0.2225 against 0.2294 at roughness 0.26, N·V 0.26, where it was 0.288.
328
-
329
- Swapping only the direct Fresnel to x⁴ (pairing D) brightens the back-lit black dielectric's rim by up to 28%. The
330
- images are with the scripts (§8).
331
-
332
- ## 7. As built (dbdb1cdb1)
333
-
334
- **The form is standard Schlick.** `F_Schlick(F0, F90, VoH)` computes `Fc = (1 − VoH)^5` by multiplies, then returns `F90 Fc + (1 − Fc) F0`, and the split-sum lookup reads a LUT integrated with the same weight. `chunk_F_Schlick` now has that form; its `pow(x, 5.0)` had compiled to a log and an exp.
335
-
336
- **Every `F_Hauber` caller moved to it,** and `chunk_F_Hauber` is deleted:
337
-
338
- - `chunk_BRDF_GGX`;
339
- - `graph_lpv_shade.js`;
340
- - the path tracer: `chunk_render_trace_path.js` ×2 and `chunk_sample_material_data.js`;
341
- - the forward fragment;
342
- - the view Fresnel of `shader_avboit_splat.js` and `shader_oit_generate_moments.js`.
343
-
344
- **`chunk_integrate_brdf` weights `(1 − x⁵, x⁵)`, and `split_sum.bin` was regenerated once on the RTX 4090.** It
345
- followed the 16cca4c69 procedure: 2^18 samples in 1024-sample blocks through rg32float, f16 encoded on the CPU, then
346
- the chunk put back at 1024 samples. Checked against the old asset:
347
-
348
- - its A + B moved by at most one f16 ulp anywhere;
349
- - against the old asset plus the f64 change of weight, it is within 5.8e-4 everywhere.
350
-
351
- **Pinned red then green:**
352
-
353
- - `chunk_BRDF_GGX.spec.js` (new): a perfect conductor's lobe is D V alone, and the lobe's Fresnel is Schlick's fifth
354
- power.
355
- - `split_sum_asset.spec.js`: weight x⁵, and the generator itself held to the same integral.
356
- - `white_furnace.spec.js`: the dip's slack goes from four checks.
357
- - A white metal's direct light reads 0.997 – 1.000 of the sky.
358
- - A pure material's direct and indirect specular agree to 0.0031.
359
- - A white dielectric's totals agree to 0.0010.
360
- - The metalness blend holds to 1.3e-5.
361
- - `chunk_F_Schlick.spec.js` (new): pins the form.
362
-
363
- **The diffuse coupling of 835cba76b now balances exactly.** It keeps 1 − the LUT's dielectric albedo, which is now the
364
- direct lobe's own, so a white dielectric's direct light reads 1.000 – 1.001 of the sky (it was 0.932 – 1.000).
365
-
366
- **Device checks:**
367
-
368
- - Every shader that links a changed chunk compiles on the 4090: 19 unique modules, found through the reverse import
369
- closure. The lightmap bake pair fails on a struct member already missing on master (a224bc59c1).
370
- - The sphere sweep, rendered from a detached master worktree and from the change, moves as §6 predicted, to 0.1
371
- point.
372
-
373
- **Not part of this:** `ssr_combine_shader.js` weights by Karis's fit, not the LUT (§4).
374
-
375
- ## 8. Method and reproduction
376
-
377
- The scripts, data and renders are in the side-session notes folder
378
- `local_9644ea11-c179-40dd-a485-620475f78a45/fresnel-brief/`, outside the repo:
379
-
380
- - `dfg_lib.mjs`: the lobe integrated over microfacet normals, with GGX's CDF as the variable, so that narrow lobes
381
- resolve. It is held to `split_sum_asset.spec.js`'s light-direction quadrature and to the shipped texels to about
382
- 1e-4.
383
- - `fresnel_lib.mjs`: exact dielectric and complex conductor Fresnel, and the spectral projection.
384
- - `optical_data.json`: n and k, CIE 1931 and D65, each with its source URL.
385
- - `study_dielectric.mjs`, `study_metal.mjs`, `hauber_claim.mjs`, `study_dfg.mjs`, `study_dfg_e.mjs`,
386
- `pesce_mapping.mjs`, `combos_study.mjs` and `model_check.mjs`, each with its `.txt` output.
387
- - `dfg_fits.mjs`: every published fit, with its constants.
388
- - `lit_fresnel.md` and `lit_dfg.md`: the literature, read from the primary sources, with URLs and sections.
389
- - `walk_candidates.mjs`: the shared bench's Heitz port, with every Fresnel. Run with `analyse_walk.mjs`.
390
- - `alu/`: the dump page and counter.
391
- - `render_drive.mjs`, the sphere page and `sphere_diff.py`, with the renders.
@@ -1,153 +0,0 @@
1
- # ACES tone mapping, Unreal's default look: a brief (2026-10-17)
2
-
3
- **Status: built.** `ShadeToneMappingMode.ACES` is the default. It is added beside GT7, PBR Neutral and NONE, and
4
- replaces none of them. Alex asked for a close match to Unreal Engine's current default look, on SDR and HDR alike,
5
- as fixed-function tone mapping: no settings and no LUTs.
6
-
7
- ## 1. What it is
8
-
9
- - **SDR is Unreal's "Filmic" tone mapper at its default settings.** It evaluates UE 5.4's sRGB LUT colour-grading and filmic tone-mapping pipeline in `chunk_tonemap_ue_filmic` per pixel where UE bakes it into a 32³ LUT:
10
- 1. ACEScg from UE's default sRGB working space;
11
- 2. Expand Gamut at 1.0;
12
- 3. the colour corrections at their defaults, which only clamp at zero;
13
- 4. Blue Correction at 0.6;
14
- 5. the curve at Slope 0.88, Toe 0.55, Shoulder 0.26, Black Clip 0 and White Clip 0.04 (the default post-process settings);
15
- 6. the inverse blue correction, then Rec.709 clipped to [0, 1].
16
- - **HDR is what Unreal 5.8 does with Filmic on a display past 100 cd/m²:** the SDR picture, taken back to the scene
17
- through the inverse ACES 2.0 output transform for a 100 cd/m² display, and rendered again with the ACES 2.0 output
18
- transform for this display's headroom, both limited to Rec.709. The ACES 2.0 part is OpenColorIO 2.6.0's
19
- implementation, in `chunk_aces2_output_transform`, with three limits on its inverse (D5). Its per-display parameters
20
- and gamut tables are built on the CPU in `aces2_transform_block.js` and owned by `TonemapAces2Tables`.
21
-
22
- | | SDR | HDR, 1000 cd/m² | HDR, 2000 cd/m² | HDR, 4000 cd/m² |
23
- |---|---|---|---|---|
24
- | exposed 0.18 shows at, × SDR white | 0.180 | 0.257 | 0.287 | 0.313 |
25
- | white reaches, × SDR white | 1.0 at +6 EV | 4.90 | 9.62 | 18.64 |
26
-
27
- HDR columns are 1015, 2030 and 4060 cd/m² displays, whose headroom over the 203 cd/m² paper white puts ACES 2.0 at
28
- 500, 1000 and 2000 cd/m². The values are OpenColorIO's.
29
-
30
- Mid-grey rises with the display's peak. That is ACES 2.0's design, and Unreal 5.8 keeps it: 10 cd/m² at a 100 cd/m²
31
- display, plus 14% of that for every doubling of the peak. A display with no headroom shows the SDR picture exactly.
32
-
33
- ## 2. Sources
34
-
35
- - **UE 5.4 documentation:** "Color Grading and the Filmic Tonemapper in Unreal Engine", Epic Games.
36
- - **UE after 5.4, release notes** (Epic, verified):
37
- - 5.6 adds ACES 2.0 for HDR as an option.
38
- - 5.8 adds "support for ACES 2.0 in SDR", moves ACES into a secondary LUT, and drops the HDR UI paper white from 300
39
- to 203 cd/m² to match BT.2408.
40
- - 5.8 offers Filmic (default) and Standard ACES tone-mapping methods.
41
- - **UE 5.8's behaviour, Epic staff** (Yutaro Sawada, forums.unrealengine.com/t/tonemappingmethod/2747218, 2026-07-30,
42
- in Japanese):
43
- - ACES 2.0 is now the default HDR version.
44
- - Filmic, the default, aims at one look on SDR and HDR. Past 100 cd/m² it is "applied, converted back to linear
45
- through the inverse ACES 2.0, then output again through ACES 2.0 at the target luminance".
46
- - The scene colour multiplier applies to Standard ACES only.
47
- - The peak comes from the hardware, and mid and minimum luminance derive from it.
48
- - **ACES 2.0:**
49
- - ampas/aces-core, Lib.Academy.OutputTransform.ctl and Lib.Academy.Tonescale.ctl;
50
- - OpenColorIO v2.6.0, ops/fixedfunction/ACES2/Transform.cpp, FixedFunctionOpCPU.cpp and transforms/builtins/ACES.cpp,
51
- which this ports.
52
- - The glue between Filmic and ACES 2.0 (§3, D2) is inferred from the staff description.
53
-
54
- The state of the art elsewhere, for context:
55
-
56
- - Godot defaults to Linear, three.js to none, and Unity HDRP to none; Filament defaults to a legacy ACES fit.
57
- - Blender added ACES 1.3 and 2.0 views in 5.0, with AgX the default since 4.0.
58
- - No real-time engine but Unreal ships ACES 2.0 by default.
59
-
60
- ## 3. Decisions
61
-
62
- - **D1. SDR is Filmic exactly, Unreal's pass excepted.** Left out because they are not tone mapping:
63
- - vignette (UE's default intensity 0.4), grain, sharpening and bloom;
64
- - the LUT itself: UE interpolates a 32³ table, where this evaluates the function;
65
- - the LUT's input range: UE's log encoding stops at 43.7, so it shades anything brighter as 43.7. This does not,
66
- which moves saturated highlights past 43.7 by a few codes.
67
- - **D2. HDR, the glue I inferred:**
68
- - The inverse runs at 100 cd/m² with Rec.709 limiting primaries, because Filmic's output is a Rec.709 SDR picture.
69
- - The forward runs at 100 × the display's peak over the paper white, also limited to Rec.709. So both transforms
70
- take SDR white for 100 cd/m², and SDR white lands on the paper white, as in every other mode.
71
- - Rec.709 rather than the canvas's Display P3, because the review measured what P3 does: just past the paper white
72
- (203.01 cd/m²) saturated colours jumped up to 67 codes from the SDR picture, 213 of 512 by more than 2. Limited
73
- to the SDR picture's own gamut, the HDR picture becomes the SDR one as the headroom shrinks to none. HDR gives up
74
- colours outside Rec.709, which the SDR picture never had either.
75
- - A display at or under the paper white shows the SDR picture, Unreal's "past 100 cd/m²" rule in this engine's
76
- units.
77
- - Unreal sets the HDR paper-white level in nits and runs the forward transform at the display's absolute
78
- peak. If its paper white scales the SDR picture onto the paper white, which its default of 203, BT.2408 and ours
79
- suggest, the two agree.
80
- - **D3. No scene colour multiplier.** UE applies its 1.5 to Standard ACES only.
81
- - **D4. Tables are built on the CPU, per display.** ACES 2.0 needs a reach table, a hue table and a gamut-cusp
82
- table with an upper-hull gamma per hue. They are found by searches no pixel can afford.
83
- - Built in 7–13 ms in JS, on the first HDR frame and then on a change of peak, not every frame and not with the
84
- renderer.
85
- - Bound as one 15 KB storage buffer: two blocks of 1,893 floats.
86
- - These are the algorithm's own data, not a grading LUT.
87
- - **D5. The inverse is limited where OCIO's is ill-conditioned.** Filmic clips channels onto the Rec.709 cube's
88
- surface, and ACES 2.0's estimate of the gamut boundary lies a hair inside that surface. So a bright tinted highlight,
89
- SDR (0.981, 1, 1) say, sits on or past the boundary, where OCIO's inverse extrapolates. The review measured it as a
90
- jump of 0.24 × SDR white between neighbouring scene values on a smooth ramp, which a picture shows as speckle or a
91
- ring. Three limits, only in the inverse, and only where it is ill-conditioned:
92
- - colourfulness past the estimated boundary is clamped to it, so it comes back as the reach boundary, which the
93
- forward transform takes back to the boundary;
94
- - the decompression's gain is held to 5. Below the last unit of J it stays under 4.9 for 3,000 Filmic outputs, and
95
- within that unit it reaches 600 to 11,700, which multiplies float32 rounding by as much;
96
- - over that last unit of J the decompression fades to none, so a near-white colour meets the neutral that white,
97
- past J_max, is sent to.
98
-
99
- A colour whose SDR picture is not in that region is untouched and matches OCIO. Two more departures are where OCIO
100
- gives NaN: the gamut boundary's power takes a base clamped at zero, and an achromatic response at or under zero
101
- tone-maps to zero, which is where OCIO's `max(0, NaN)` lands it.
102
-
103
- ## 4. Measured
104
-
105
- **Against the references**, in `shader_tonemap_aces.spec.js`, under the WGSL emulator, in Display P3:
106
-
107
- - SDR is within the dither of a float64 Python reference of the Filmic curve and grading pipeline, at 29 colours from black to 1000.
108
- - HDR is within 1e-4 (encoded) of OpenColorIO 2.6.0 at four display peaks: its builtin inverse at 100 cd/m², then its
109
- ACES 2.0 fixed function limited to Rec.709 between its builtins' clamps.
110
- - The exceptions are three bright saturated colours whose SDR picture is near white, where D5's limits apply, and
111
- a grey at +4 EV on 8120 cd/m², where OCIO's float32 inverse drifts by more than that. These four are held to a
112
- float64 Python port with the same limits instead.
113
- - A tinted highlight's HDR picture moves continuously: the largest step along a ramp shrinks when the interval is cut
114
- ten times finer. Without D5's clamp and gain limit it does not, nor with P3 limiting at 203.01 cd/m² is an HDR
115
- display within 2 codes of SDR; both tests failed before the change.
116
- - Mutation testing: 110 mutants by an adversarial reviewer, then the survivors and every limit in D5 again. Three
117
- survive, each unreachable or under the tolerance (the inverse's nd ≥ 1 branch, which the clamp makes unreachable;
118
- the gain ≥ 1 escape; a looser cusp-search tolerance).
119
- - The JS table builder matches a float64 Python port to 6e-8.
120
- - That Python port matches OCIO's builtins to 2e-5, forward and inverse, at 100, 500 and 1000 cd/m².
121
-
122
- **On the device** (RTX 4090, headless Chrome, volumetrics_froxel, 1258×646, exposure fixed). OKLab lightness and HSV
123
- saturation are on the displayed frame:
124
-
125
- | view | mode | L mean | L p05 | L p95 | saturation |
126
- |---|---|---|---|---|---|
127
- | overview | ACES | 0.434 | 0.141 | 0.655 | 0.323 |
128
- | overview | GT7 | 0.445 | 0.180 | 0.635 | 0.281 |
129
- | broadside | ACES | 0.512 | 0.224 | 0.868 | 0.263 |
130
- | broadside | GT7 | 0.522 | 0.258 | 0.891 | 0.241 |
131
- | volume edge | ACES | 0.472 | 0.256 | 0.725 | 0.342 |
132
- | volume edge | GT7 | 0.480 | 0.288 | 0.699 | 0.300 |
133
-
134
- - Mid-grey is where GT7 puts it, 0.18 in and 0.18 out.
135
- - The mid-tone contrast is 1.36 against GT7's 1.05. Shadows sit deeper and highlights brighter, and saturation rises
136
- 12–15%.
137
-
138
- The pass's cost, three interleaved rounds of 24 frames:
139
-
140
- | pass | ACES | GT7 |
141
- |---|---|---|
142
- | SDR | 12–13 µs | 14–21 µs |
143
- | HDR | 55–102 µs | 16–33 µs |
144
-
145
- The HDR pass was forced through `matchMedia` and ran on the device with no validation errors and no non-finite output.
146
- It has not been looked at on an HDR display.
147
-
148
- ## 5. Open, measured
149
-
150
- - **The HDR pass costs three to four times GT7's**: two ACES 2.0 transforms a pixel, 55–102 µs at 0.8 MP on a 4090.
151
- Unreal pays this once into a LUT, which was ruled out here.
152
- - **The glue to Filmic is inferred** (D2, D5). Unreal evaluates this composite into a LUT, whose interpolation smooths
153
- what D5 limits.