@woosh/meep-engine 3.60.4 → 3.60.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (379) hide show
  1. package/CHANGELOG.md +2602 -0
  2. package/CONTRIBUTING.md +7 -0
  3. package/MIGRATION_2_TO_3.md +136 -0
  4. package/SECURITY.md +4 -0
  5. package/editor/EditorCameraNavigation.d.ts +2 -1
  6. package/editor/EditorCameraNavigation.d.ts.map +1 -1
  7. package/editor/EditorCameraNavigation.js +51 -57
  8. package/package.json +6 -2
  9. package/src/engine/EngineHarness.d.ts +3 -9
  10. package/src/engine/EngineHarness.d.ts.map +1 -1
  11. package/src/engine/EngineHarness.js +6 -8
  12. package/src/engine/graphics/camera/makeOrbitalCameraController.d.ts +9 -6
  13. package/src/engine/graphics/camera/makeOrbitalCameraController.d.ts.map +1 -1
  14. package/src/engine/graphics/camera/makeOrbitalCameraController.js +44 -71
  15. package/src/engine/graphics/camera/orbital/MomentumAxis.d.ts +64 -0
  16. package/src/engine/graphics/camera/orbital/MomentumAxis.d.ts.map +1 -0
  17. package/src/engine/graphics/camera/orbital/MomentumAxis.js +187 -0
  18. package/src/engine/graphics/camera/orbital/OrbitalCameraRig.d.ts +86 -0
  19. package/src/engine/graphics/camera/orbital/OrbitalCameraRig.d.ts.map +1 -0
  20. package/src/engine/graphics/camera/orbital/OrbitalCameraRig.js +319 -0
  21. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.d.ts +34 -10
  22. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.d.ts.map +1 -1
  23. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.js +116 -30
  24. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.d.ts +1 -7
  25. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.d.ts.map +1 -1
  26. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.js +1 -37
  27. package/src/engine/graphics/ecs/water/Water.d.ts +8 -0
  28. package/src/engine/graphics/ecs/water/Water.d.ts.map +1 -1
  29. package/src/engine/graphics/ecs/water/Water.js +8 -0
  30. package/src/engine/graphics3/WaterSystem.d.ts.map +1 -1
  31. package/src/engine/graphics3/WaterSystem.js +731 -734
  32. package/src/engine/graphics3/decal/DecalTileShader.d.ts +61 -0
  33. package/src/engine/graphics3/decal/DecalTileShader.d.ts.map +1 -0
  34. package/src/engine/graphics3/decal/DecalTileShader.js +138 -0
  35. package/src/engine/graphics3/decal/GPUDecalRenderer.d.ts +10 -4
  36. package/src/engine/graphics3/decal/GPUDecalRenderer.d.ts.map +1 -1
  37. package/src/engine/graphics3/decal/GPUDecalRenderer.js +149 -44
  38. package/src/engine/graphics3/decal/chunk_decal_tile_vertex.d.ts +25 -0
  39. package/src/engine/graphics3/decal/chunk_decal_tile_vertex.d.ts.map +1 -0
  40. package/src/engine/graphics3/decal/chunk_decal_tile_vertex.js +55 -0
  41. package/src/engine/graphics3/decal/graph_gather_decal_tiles.d.ts +22 -0
  42. package/src/engine/graphics3/decal/graph_gather_decal_tiles.d.ts.map +1 -0
  43. package/src/engine/graphics3/decal/graph_gather_decal_tiles.js +70 -0
  44. package/src/engine/graphics3/decal/shader_decal_composite.d.ts +7 -5
  45. package/src/engine/graphics3/decal/shader_decal_composite.d.ts.map +1 -1
  46. package/src/engine/graphics3/decal/shader_decal_composite.js +48 -44
  47. package/src/engine/graphics3/decal/shader_decal_tile_copy.d.ts +12 -0
  48. package/src/engine/graphics3/decal/shader_decal_tile_copy.d.ts.map +1 -0
  49. package/src/engine/graphics3/decal/shader_decal_tile_copy.js +78 -0
  50. package/src/engine/graphics3/decal/shader_gather_decal_tiles.d.ts +11 -0
  51. package/src/engine/graphics3/decal/shader_gather_decal_tiles.d.ts.map +1 -0
  52. package/src/engine/graphics3/decal/shader_gather_decal_tiles.js +80 -0
  53. package/src/engine/input/devices/mouse/MouseEventButtons.d.ts +13 -0
  54. package/src/engine/input/devices/mouse/MouseEventButtons.d.ts.map +1 -0
  55. package/src/engine/input/devices/mouse/MouseEventButtons.js +23 -0
  56. package/src/engine/input/ecs/controllers/KeyboardCameraController.d.ts +4 -0
  57. package/src/engine/input/ecs/controllers/KeyboardCameraController.d.ts.map +1 -1
  58. package/src/engine/input/ecs/controllers/KeyboardCameraController.js +9 -45
  59. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  60. package/src/shade/playground/water_refraction/README.md +5 -6
  61. package/src/shade/playground/water_refraction/index.html +1 -1
  62. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  63. package/src/shade/renderer/Renderer.js +7 -5
  64. package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts +40 -80
  65. package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts.map +1 -1
  66. package/src/shade/renderer/camera/orbital/OrbitalCameraController.js +197 -147
  67. package/src/shade/renderer/fluid/GPUFluidCascade.d.ts +3 -3
  68. package/src/shade/renderer/fluid/GPUFluidCascadeStack.d.ts +1 -1
  69. package/src/shade/renderer/fluid/forcing/GPUFluidForcing.d.ts +6 -6
  70. package/src/shade/renderer/fluid/pressure/GPUFluidPressureSolve.d.ts +1 -1
  71. package/src/shade/renderer/fluid/query/GPUFluidQueries.d.ts +1 -1
  72. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.d.ts +2 -2
  73. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.js +2 -2
  74. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts +2 -1
  75. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts.map +1 -1
  76. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.js +18 -1
  77. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.d.ts +31 -0
  78. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.d.ts.map +1 -0
  79. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.js +343 -0
  80. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.d.ts +17 -0
  81. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.d.ts.map +1 -0
  82. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.js +29 -0
  83. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.d.ts.map +1 -1
  84. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.js +7 -13
  85. package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.d.ts.map +1 -1
  86. package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.js +1 -8
  87. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts +5 -5
  88. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts.map +1 -1
  89. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.js +10 -10
  90. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts +1 -4
  91. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts.map +1 -1
  92. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.js +1 -10
  93. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.d.ts +5 -15
  94. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.d.ts.map +1 -1
  95. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.js +6 -34
  96. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.d.ts.map +1 -1
  97. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.js +1 -7
  98. package/src/shade/renderer/hiz/HZBFootprint.d.ts +14 -0
  99. package/src/shade/renderer/hiz/HZBFootprint.d.ts.map +1 -0
  100. package/src/shade/renderer/hiz/HZBFootprint.js +16 -0
  101. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.d.ts +10 -0
  102. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.d.ts.map +1 -0
  103. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.js +11 -0
  104. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.d.ts +10 -0
  105. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.d.ts.map +1 -0
  106. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.js +11 -0
  107. package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts +0 -7
  108. package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts.map +1 -1
  109. package/src/shade/renderer/hiz/HierarchicalZBuffer.js +405 -350
  110. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.d.ts +12 -0
  111. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.d.ts.map +1 -0
  112. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.js +27 -0
  113. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.d.ts +12 -0
  114. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.d.ts.map +1 -0
  115. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.js +27 -0
  116. package/src/shade/renderer/hiz/hzb_footprint_texels.d.ts +14 -0
  117. package/src/shade/renderer/hiz/hzb_footprint_texels.d.ts.map +1 -0
  118. package/src/shade/renderer/hiz/hzb_footprint_texels.js +34 -0
  119. package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts +0 -5
  120. package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts.map +1 -1
  121. package/src/shade/renderer/hiz/shader_hzb_reduce_any.js +37 -57
  122. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0.d.ts +11 -0
  123. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0.d.ts.map +1 -0
  124. package/src/shade/renderer/hiz/{shader_hzb_reduce_mip0_clip.js → shader_hzb_reduce_mip0.js} +120 -111
  125. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  126. package/src/shade/renderer/material/GPUMaterialManager.d.ts +7 -0
  127. package/src/shade/renderer/material/GPUMaterialManager.d.ts.map +1 -1
  128. package/src/shade/renderer/material/GPUMaterialManager.js +11 -0
  129. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts +12 -12
  130. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts.map +1 -1
  131. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.js +17 -118
  132. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.d.ts.map +1 -1
  133. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.js +7 -2
  134. package/src/shade/renderer/water/GPUWaterRenderer.d.ts +0 -6
  135. package/src/shade/renderer/water/GPUWaterRenderer.d.ts.map +1 -1
  136. package/src/shade/renderer/water/GPUWaterRenderer.js +12 -17
  137. package/src/shade/renderer/water/medium/graph_water_meshlets.d.ts +35 -0
  138. package/src/shade/renderer/water/medium/graph_water_meshlets.d.ts.map +1 -0
  139. package/src/shade/renderer/water/medium/graph_water_meshlets.js +89 -0
  140. package/src/shade/renderer/water/query/GPUWaterQueries.d.ts +1 -1
  141. package/src/shade/renderer/water/shader_water.d.ts.map +1 -1
  142. package/src/shade/renderer/water/shader_water.js +22 -13
  143. package/src/shade/renderer/water/shader_water_avboit.d.ts.map +1 -1
  144. package/src/shade/renderer/water/shader_water_avboit.js +7 -7
  145. package/src/shade/renderer/water/surface/graph_water_tessellation.d.ts +1 -1
  146. package/src/shade/renderer/water/surface/graph_water_tessellation.js +1 -1
  147. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +9 -1
  148. package/editor/REWORK_PLAN_2026_08.md +0 -692
  149. package/src/CODEC_LAYOUT_VERIFICATION.md +0 -419
  150. package/src/CORRECTNESS_HUNT_2026_07_24.md +0 -142
  151. package/src/DUPLICATION_AUDIT_2026_08_07/00_core_reuse_surface.md +0 -738
  152. package/src/DUPLICATION_AUDIT_2026_08_07/A_core_math_geom.md +0 -79
  153. package/src/DUPLICATION_AUDIT_2026_08_07/B_core_data.md +0 -93
  154. package/src/DUPLICATION_AUDIT_2026_08_07/C_gfx_render.md +0 -80
  155. package/src/DUPLICATION_AUDIT_2026_08_07/D_gfx_features.md +0 -164
  156. package/src/DUPLICATION_AUDIT_2026_08_07/E_physics.md +0 -69
  157. package/src/DUPLICATION_AUDIT_2026_08_07/F_sound.md +0 -110
  158. package/src/DUPLICATION_AUDIT_2026_08_07/G_net_save.md +0 -129
  159. package/src/DUPLICATION_AUDIT_2026_08_07/H_engine_misc.md +0 -72
  160. package/src/DUPLICATION_AUDIT_2026_08_07/I_generation_view.md +0 -74
  161. package/src/DUPLICATION_AUDIT_2026_08_07/K_crosscutting.md +0 -169
  162. package/src/DUPLICATION_AUDIT_2026_08_07.md +0 -254
  163. package/src/DUPLICATION_AUDIT_REMEDIATION.md +0 -250
  164. package/src/DUPLICATION_REVIEW_2026_10_24.md +0 -1072
  165. package/src/REVIEW_2026_08_06/adv-core.md +0 -511
  166. package/src/REVIEW_2026_08_06/adv-gfx.md +0 -480
  167. package/src/REVIEW_2026_08_06/adv-view.md +0 -446
  168. package/src/REVIEW_2026_08_06/api-core.md +0 -296
  169. package/src/REVIEW_2026_08_06/api-engine.md +0 -195
  170. package/src/REVIEW_2026_08_06/bvh-phys-sound-net.md +0 -507
  171. package/src/REVIEW_2026_08_06/core-binary.md +0 -348
  172. package/src/REVIEW_2026_08_06/core-collection.md +0 -376
  173. package/src/REVIEW_2026_08_06/core-math.md +0 -221
  174. package/src/REVIEW_2026_08_06/core-process.md +0 -266
  175. package/src/REVIEW_2026_08_06/core-science.md +0 -223
  176. package/src/REVIEW_2026_08_06/engine-ai-nav.md +0 -279
  177. package/src/REVIEW_2026_08_06/engine-animation.md +0 -167
  178. package/src/REVIEW_2026_08_06/engine-asset.md +0 -166
  179. package/src/REVIEW_2026_08_06/engine-ecs.md +0 -920
  180. package/src/REVIEW_2026_08_06/engine-input.md +0 -197
  181. package/src/REVIEW_2026_08_06/engine-misc.md +0 -248
  182. package/src/REVIEW_2026_08_06/engine-terrain.md +0 -52
  183. package/src/REVIEW_2026_08_06/generation.md +0 -176
  184. package/src/REVIEW_2026_08_06/geom-2d.md +0 -434
  185. package/src/REVIEW_2026_08_06/geom-3d.md +0 -117
  186. package/src/REVIEW_2026_08_06/gfx-gi.md +0 -143
  187. package/src/REVIEW_2026_08_06/gfx-render.md +0 -740
  188. package/src/REVIEW_2026_08_06/gfx-texture.md +0 -690
  189. package/src/REVIEW_2026_08_06/prior-criticals.md +0 -49
  190. package/src/REVIEW_2026_08_06/prior-highs.md +0 -138
  191. package/src/REVIEW_2026_08_06/test-gaps.md +0 -364
  192. package/src/REVIEW_2026_08_06/view.md +0 -667
  193. package/src/REVIEW_2026_08_06.md +0 -610
  194. package/src/core/binary/lz4/LZ4_HIGH_COMPRESSION_PLAN_2026_09_14.md +0 -542
  195. package/src/core/bvh2/bvh3/NOTES.md +0 -2
  196. package/src/core/bvh8/NOTES.md +0 -63
  197. package/src/core/cache/wtinylfu/benchmark/CacheWTinylfu.bench.md +0 -243
  198. package/src/core/color/COLOR_FIX_PLAN_2026_08_28.md +0 -417
  199. package/src/core/color/COLOR_REVIEW_2026_08_28.md +0 -779
  200. package/src/core/geom/2d/polygon/TRIANGULATION_DESIGN.md +0 -433
  201. package/src/core/geom/3d/atlas/ATLAS_PORT_PLAN.md +0 -486
  202. package/src/core/geom/3d/atlas/BFF_PLAN.md +0 -174
  203. package/src/core/geom/3d/atlas/REVIEW_02.md +0 -392
  204. package/src/core/geom/3d/gjk/GJK_REVIEW_NOTES.md +0 -146
  205. package/src/core/geom/3d/gjk/NOTES.md +0 -8
  206. package/src/core/geom/3d/topology/TOPO_MESH_RETIREMENT_PLAN.md +0 -135
  207. package/src/core/geom/GEOMETRY_REVIEW_002_API_ISSUES.md +0 -381
  208. package/src/core/geom/GEOMETRY_REVIEW_002_NAMING.md +0 -684
  209. package/src/core/geom/MANIFOLD_COMPARISON_2026_08_01.md +0 -554
  210. package/src/core/model/reactive/evaluation/PREDICATE_APPLICABILITY_PROPOSAL.md +0 -446
  211. package/src/engine/REVIEW_2026_07_03.md +0 -211
  212. package/src/engine/asset/loaders/image/IMAGE_CODEC_REVIEW_2026_08_28.md +0 -815
  213. package/src/engine/control/first-person/DESIGN.md +0 -658
  214. package/src/engine/control/first-person/DESIGN_COLLISION.md +0 -382
  215. package/src/engine/control/first-person/DESIGN_EXTENSIONS.md +0 -563
  216. package/src/engine/control/first-person/TODO.md +0 -160
  217. package/src/engine/ecs/ASYNC_SYSTEMS_PROPOSAL.md +0 -531
  218. package/src/engine/ecs/HIERARCHY_REVIEW.md +0 -717
  219. package/src/engine/ecs/async/WORKER_SYSTEM_PLAN.md +0 -262
  220. package/src/engine/ecs/ik/PLAN.md +0 -179
  221. package/src/engine/ecs/storage/binary/SERIALIZATION_COVERAGE.md +0 -272
  222. package/src/engine/ecs/storage/binary/object-v2/DESIGN.md +0 -199
  223. package/src/engine/ecs/transform/TRANSFORM64_MIGRATION_PLAN_2026_09_04.md +0 -521
  224. package/src/engine/graphics/CONTEXT_LOSS_RECOVERY_PLAN.md +0 -463
  225. package/src/engine/graphics/ecs/camera/pp/NOTES.md +0 -48
  226. package/src/engine/graphics/render/frame_graph/CORRECTNESS_FIXES_2026_09_04.md +0 -659
  227. package/src/engine/graphics/render/frame_graph/FRAME_GRAPH_RECOMMENDATIONS.md +0 -235
  228. package/src/engine/graphics/sh3/path_tracer/REVIEW_001_ACTION_PLAN.md +0 -490
  229. package/src/engine/graphics/texture/sampler/compare/FLIP_IMPLEMENTATION_PLAN.md +0 -466
  230. package/src/engine/input/GAMEPAD_API_PROPOSAL.md +0 -717
  231. package/src/engine/input/ecs/ism/ISM_DESIGN.md +0 -1523
  232. package/src/engine/input/ecs/ism/ISM_IMPLEMENTATION_PLAN.md +0 -627
  233. package/src/engine/navigation/mesh/OVERLAPPING_SURFACES_2026_08_25.md +0 -661
  234. package/src/engine/navigation/mesh/PATHFINDING_PLAN.md +0 -229
  235. package/src/engine/network/CONGESTION_CONTROL.md +0 -87
  236. package/src/engine/network/MESSAGE_TRANSPORT.md +0 -364
  237. package/src/engine/network/SMOOTHNESS_DESIGN.md +0 -400
  238. package/src/engine/physics/cloth/MEASUREMENTS.md +0 -5304
  239. package/src/engine/physics/cloth/PLAN.md +0 -3056
  240. package/src/engine/physics/fluid/GPU_FLUID_CASCADES_PROPOSAL_2026_10_16.md +0 -1382
  241. package/src/engine/physics/fluid/REVIEW_02_PLAN.md +0 -243
  242. package/src/engine/physics/fluid/solver/NOTES.md +0 -46
  243. package/src/engine/sound/SOUND_EMITTER_RETIREMENT_PLAN.md +0 -536
  244. package/src/engine/sound/simulation/IMPLEMENTATION_PLAN.md +0 -432
  245. package/src/engine/sound/simulation/PROBE_PLACEMENT.md +0 -250
  246. package/src/engine/sound/simulation/PROBE_TRANSFER_RESEARCH.md +0 -322
  247. package/src/engine/sound/simulation/REFLECTION_IR_PLAN.md +0 -261
  248. package/src/engine/sound/simulation/VISIBILITY_GRAPH_PLAN.md +0 -121
  249. package/src/engine/sound/sopra/IMPLEMENTATION_PLAN.md +0 -999
  250. package/src/format/image/avif/DECISIONS.md +0 -545
  251. package/src/format/image/jpeg/NOTES.md +0 -3
  252. package/src/format/scene/gltf/MESHOPT_COMPRESSION_PLAN.md +0 -814
  253. package/src/generation/COORDINATES.md +0 -54
  254. package/src/generation/REVIEW_01_ACTION_PLAN.md +0 -628
  255. package/src/generation/foliage/ANNUAL_SHOOTS_BRIEF_2026_10_25.md +0 -220
  256. package/src/generation/foliage/CROWN_FILLING_BRIEF_2026_10_26.md +0 -172
  257. package/src/generation/foliage/CROWN_FULLNESS_BRIEF_2026_10_26.md +0 -86
  258. package/src/generation/foliage/FOLIAGE_DESIGN.md +0 -547
  259. package/src/generation/foliage/FOLIAGE_PLAN.md +0 -198
  260. package/src/generation/foliage/LEAF_ARCHITECTURE_BRIEF_2026_10_26.md +0 -158
  261. package/src/generation/foliage/LOD_REWORK_BRIEF_2026_10_27.md +0 -521
  262. package/src/generation/foliage/REAL_TIME_TREES_BRIEF_2026_10_27.md +0 -86
  263. package/src/generation/foliage/SELF_ORGANIZING_FRAMEWORK_BRIEF_2026_10_26.md +0 -347
  264. package/src/generation/foliage/SHOOT_SYSTEMS_ALL_SPECIES_BRIEF_2026_10_26.md +0 -248
  265. package/src/generation/foliage/SPEEDTREE_COMPARISON_2026_10_24.md +0 -694
  266. package/src/generation/foliage/TWO_FRONT_ENDS_BRIEF_2026_10_28.md +0 -552
  267. package/src/shade/RENDERER_CONTRACT.md +0 -697
  268. package/src/shade/SHADER_F16_PROPOSAL_2026_10_24.md +0 -263
  269. package/src/shade/device/IMMEDIATE_DATA_PLAN_2026_08_28.md +0 -569
  270. package/src/shade/device/PASS_ENCODER_PROPOSAL_2026_08_31.md +0 -755
  271. package/src/shade/device/mock/EXTENSION_PROPOSAL_2026_08_22.md +0 -458
  272. package/src/shade/device/mock/IMPLEMENTATION_PLAN.md +0 -255
  273. package/src/shade/device/mock/REVIEW_2026_08_22.md +0 -527
  274. package/src/shade/device/timing/GPU_PROFILER_PROPOSAL_2026_08_28.md +0 -894
  275. package/src/shade/device/timing/profile/SGPT_FORMAT.md +0 -345
  276. package/src/shade/renderer/NOTES.md +0 -78
  277. package/src/shade/renderer/PLAN.md +0 -8
  278. package/src/shade/renderer/animation/ADDITIVE_BLENDING_PLAN.md +0 -145
  279. package/src/shade/renderer/animation/SKINNING_FRAME_PLAN.md +0 -357
  280. package/src/shade/renderer/atmosphere/NOTES.md +0 -6
  281. package/src/shade/renderer/camera/orbital/MomentumAxis.d.ts +0 -76
  282. package/src/shade/renderer/camera/orbital/MomentumAxis.d.ts.map +0 -1
  283. package/src/shade/renderer/camera/orbital/MomentumAxis.js +0 -164
  284. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.d.ts +0 -45
  285. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.d.ts.map +0 -1
  286. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.js +0 -66
  287. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.d.ts +0 -98
  288. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.d.ts.map +0 -1
  289. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.js +0 -184
  290. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts +0 -175
  291. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts.map +0 -1
  292. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.js +0 -411
  293. package/src/shade/renderer/camera/orbital/README.md +0 -81
  294. package/src/shade/renderer/camera/orbital/index.d.ts +0 -6
  295. package/src/shade/renderer/camera/orbital/index.d.ts.map +0 -1
  296. package/src/shade/renderer/camera/orbital/index.js +0 -9
  297. package/src/shade/renderer/extension/RENDER_EXTENSION_DESIGN.md +0 -964
  298. package/src/shade/renderer/fluid/GPU_FLUID_OCCUPANCY_AND_SOTA_REVIEW_2026_10_26.md +0 -361
  299. package/src/shade/renderer/geometry/bvh/BLAS_REFIT_BATCH_PLAN.md +0 -619
  300. package/src/shade/renderer/geometry/meshlet/COMPACTION_PLAN_2026_09_16.md +0 -674
  301. package/src/shade/renderer/geometry/sdf/NOTES.md +0 -6
  302. package/src/shade/renderer/geometry/virtual/ATTRIBUTE_PLAN_2026_09_18.md +0 -540
  303. package/src/shade/renderer/geometry/virtual/MICRON_COMPARISON_2026_08_20.md +0 -305
  304. package/src/shade/renderer/geometry/virtual/VGEO_FORMAT.md +0 -913
  305. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +0 -787
  306. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_PLAN.md +0 -481
  307. package/src/shade/renderer/geometry/virtual/VK_LOD_CLUSTERS_COMPARISON_2026_08_20.md +0 -301
  308. package/src/shade/renderer/geometry/virtual/WEDGE_PLAN_2026_09_18.md +0 -448
  309. package/src/shade/renderer/geometry/virtual/runtime/CULL_PLAN_2026_09_16.md +0 -385
  310. package/src/shade/renderer/geometry/virtual/runtime/CUT_PLAN_2026_09_14.md +0 -656
  311. package/src/shade/renderer/geometry/virtual/runtime/REVIEW_LEDGER.md +0 -308
  312. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN.md +0 -657
  313. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN_2026_09_14.md +0 -551
  314. package/src/shade/renderer/geometry/virtual/runtime/SHADOW_PLAN_2026_09_28.md +0 -529
  315. package/src/shade/renderer/global_illumination/lpv/NOTES.md +0 -3
  316. package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_CLIPMAP_PROPOSAL_2026_10_27.md +0 -4306
  317. package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_RAYS_VARIABLE_AND_SORTED_SCOPE_2026_10_30.md +0 -410
  318. package/src/shade/renderer/global_illumination/sharc/NOTES.md +0 -8
  319. package/src/shade/renderer/gpu_primitive/bvh/GPU_BVH_BUILD_PLAN.md +0 -1369
  320. package/src/shade/renderer/gpu_primitive/bvh/PHASE1_HANDOVER.md +0 -202
  321. package/src/shade/renderer/gpu_primitive/indirect/NOTES.md +0 -94
  322. package/src/shade/renderer/hiz/NOTES.md +0 -3
  323. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts +0 -8
  324. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts.map +0 -1
  325. package/src/shade/renderer/light/cluster/NOTES.md +0 -11
  326. package/src/shade/renderer/light/cluster/PROPOSAL_ALGORITHM_2026_10_16.md +0 -524
  327. package/src/shade/renderer/light/cluster/PROPOSAL_HIERARCHICAL_2026_10_16.md +0 -1101
  328. package/src/shade/renderer/material/resident/shader_resident_texture_means.d.ts +0 -24
  329. package/src/shade/renderer/material/resident/shader_resident_texture_means.d.ts.map +0 -1
  330. package/src/shade/renderer/material/resident/shader_resident_texture_means.js +0 -120
  331. package/src/shade/renderer/particles/DESIGN.md +0 -1011
  332. package/src/shade/renderer/particles/EMITTER_GLOBAL_STATE_PLAN_2026_10_29.md +0 -280
  333. package/src/shade/renderer/particles/ORIENTATION_PLAN_2026_10_15.md +0 -453
  334. package/src/shade/renderer/path_tracer/PATH_TRACED_MEDIA_PLAN_2026_10_02.md +0 -520
  335. package/src/shade/renderer/postprocess/bloom/NOTES.md +0 -6
  336. package/src/shade/renderer/postprocess/fsr/NOTES.md +0 -4
  337. package/src/shade/renderer/postprocess/gtao/NOTES.md +0 -6
  338. package/src/shade/renderer/postprocess/nss/NOTES.md +0 -332
  339. package/src/shade/renderer/postprocess/perspective/NOTES.md +0 -3
  340. package/src/shade/renderer/postprocess/sscs/NOTES.md +0 -5
  341. package/src/shade/renderer/postprocess/ssr/NOTES.md +0 -14
  342. package/src/shade/renderer/postprocess/ssr/REPROJECTION_PROPOSAL.md +0 -286
  343. package/src/shade/renderer/postprocess/ssr/prefilter/NOTES.md +0 -39
  344. package/src/shade/renderer/postprocess/taa/NOTES.md +0 -11
  345. package/src/shade/renderer/postprocess/upscale-dither/NOTES.md +0 -78
  346. package/src/shade/renderer/postprocess/vrs/NOTES.md +0 -4
  347. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_LEDGER.md +0 -1218
  348. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_PLAN_2026_09_03.md +0 -600
  349. package/src/shade/renderer/restir/reservoir/NOTES.md +0 -6
  350. package/src/shade/renderer/shader/chunk/atmosphere/NOTES.md +0 -6
  351. package/src/shade/renderer/shader/chunk/brdf/FRESNEL_BRIEF_2026_10_16.md +0 -391
  352. package/src/shade/renderer/shader/chunk/color/tonemap/ACES_TONE_MAPPING_BRIEF_2026_10_17.md +0 -153
  353. package/src/shade/renderer/shader/chunk/color/tonemap/HDR_TONE_MAPPING_FORMS_BRIEF_2026_10_16.md +0 -322
  354. package/src/shade/renderer/shader/chunk/color/tonemap/SDR_TONE_MAPPING_BRIEF_2026_10_16.md +0 -247
  355. package/src/shade/renderer/shader/chunk/fsr/NOTES.md +0 -3
  356. package/src/shade/renderer/shader/chunk/fsr/spd/NOTES.md +0 -4
  357. package/src/shade/renderer/shader/chunk/geometry/ray/NOTES.md +0 -4
  358. package/src/shade/renderer/shader/chunk/material/MULTISCATTER.md +0 -159
  359. package/src/shade/renderer/shader/chunk/mboit/NOTES.md +0 -38
  360. package/src/shade/renderer/shader/chunk/spherical_harmonics/l1i/NOTES.md +0 -3
  361. package/src/shade/renderer/shader/chunk/spherical_harmonics/sampling/NOTES.md +0 -8
  362. package/src/shade/renderer/shader/compiler/SHADER_SWITCH_PROPOSAL_2026_10_16.md +0 -927
  363. package/src/shade/renderer/shadow/contact/FINDINGS.md +0 -413
  364. package/src/shade/renderer/shadow/map/csm/NOTES.md +0 -5
  365. package/src/shade/renderer/shadow/ray/NOTES.md +0 -31
  366. package/src/shade/renderer/shadow/sdf/NOTES.md +0 -9
  367. package/src/shade/renderer/shadow/sdf/brixelizer/NOTES.md +0 -5
  368. package/src/shade/renderer/shadow/sdf/cascade/NOTES.md +0 -9
  369. package/src/shade/renderer/shadow/vsm/NOTES.md +0 -12
  370. package/src/shade/renderer/shadow/vsm/PLAN.md +0 -442
  371. package/src/shade/renderer/shadow/vsm/REVIEW_LEDGER.md +0 -43
  372. package/src/shade/renderer/texture/COMPRESSED_TEXTURE_PLAN_2026_09_01.md +0 -2444
  373. package/src/shade/renderer/texture/mipmaps/NOTES.md +0 -3
  374. package/src/shade/renderer/volumetrics/NOTES.md +0 -1329
  375. package/src/shade/renderer/volumetrics/VOLUMETRIC_NOISE_PLAN_2026_10_15.md +0 -336
  376. package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts +0 -23
  377. package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts.map +0 -1
  378. package/src/shade/renderer/water/medium/GPUWaterBodies.js +0 -75
  379. package/src/shade/wgsl/NOTES.md +0 -4
@@ -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.