@zephyr3d/scene 0.9.27 → 0.10.0

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 (502) hide show
  1. package/README.md +60 -27
  2. package/dist/animation/animation_bank.js +634 -0
  3. package/dist/animation/animation_bank.js.map +1 -0
  4. package/dist/animation/animation_bank_gpu.js +256 -0
  5. package/dist/animation/animation_bank_gpu.js.map +1 -0
  6. package/dist/animation/animationset.js +54 -0
  7. package/dist/animation/animationset.js.map +1 -1
  8. package/dist/animation/cloth/gpu_cloth_component.js +406 -0
  9. package/dist/animation/cloth/gpu_cloth_component.js.map +1 -0
  10. package/dist/animation/cloth/gpu_cloth_system.js +332 -202
  11. package/dist/animation/cloth/gpu_cloth_system.js.map +1 -1
  12. package/dist/animation/gpu_animation_driver.js +289 -0
  13. package/dist/animation/gpu_animation_driver.js.map +1 -0
  14. package/dist/animation/joint_dynamics/collision.js +112 -1
  15. package/dist/animation/joint_dynamics/collision.js.map +1 -1
  16. package/dist/animation/joint_dynamics/controller.js +6 -0
  17. package/dist/animation/joint_dynamics/controller.js.map +1 -1
  18. package/dist/animation/joint_dynamics/joint_dynamics_system.js +11 -0
  19. package/dist/animation/joint_dynamics/joint_dynamics_system.js.map +1 -1
  20. package/dist/animation/joint_dynamics/solver.js +50 -12
  21. package/dist/animation/joint_dynamics/solver.js.map +1 -1
  22. package/dist/animation/joint_dynamics/types.js.map +1 -1
  23. package/dist/animation/skeleton.js +279 -55
  24. package/dist/animation/skeleton.js.map +1 -1
  25. package/dist/animation/skin_palette_atlas.js +252 -0
  26. package/dist/animation/skin_palette_atlas.js.map +1 -0
  27. package/dist/animation/spring/multi_chain_spring_system.js +298 -38
  28. package/dist/animation/spring/multi_chain_spring_system.js.map +1 -1
  29. package/dist/animation/spring/spring_chain.js +2 -0
  30. package/dist/animation/spring/spring_chain.js.map +1 -1
  31. package/dist/animation/spring/spring_collider.js +159 -21
  32. package/dist/animation/spring/spring_collider.js.map +1 -1
  33. package/dist/animation/spring/spring_particle.js +1 -0
  34. package/dist/animation/spring/spring_particle.js.map +1 -1
  35. package/dist/animation/spring/spring_solver.js +173 -0
  36. package/dist/animation/spring/spring_solver.js.map +1 -0
  37. package/dist/animation/spring/spring_system.js +249 -24
  38. package/dist/animation/spring/spring_system.js.map +1 -1
  39. package/dist/animation/spring_modifier.js +32 -1
  40. package/dist/animation/spring_modifier.js.map +1 -1
  41. package/dist/app/app.js +26 -0
  42. package/dist/app/app.js.map +1 -1
  43. package/dist/app/engine.js +86 -15
  44. package/dist/app/engine.js.map +1 -1
  45. package/dist/app/gamepad.js +191 -0
  46. package/dist/app/gamepad.js.map +1 -0
  47. package/dist/app/inputactions.js +271 -0
  48. package/dist/app/inputactions.js.map +1 -0
  49. package/dist/app/inputmgr.js +331 -22
  50. package/dist/app/inputmgr.js.map +1 -1
  51. package/dist/app/loading.js +138 -0
  52. package/dist/app/loading.js.map +1 -0
  53. package/dist/app/pipelinecache.js +209 -0
  54. package/dist/app/pipelinecache.js.map +1 -0
  55. package/dist/app/runtimescript.js +25 -0
  56. package/dist/app/runtimescript.js.map +1 -1
  57. package/dist/app/scriptingsystem.js +54 -0
  58. package/dist/app/scriptingsystem.js.map +1 -1
  59. package/dist/app/scriptregistry.js +7 -2
  60. package/dist/app/scriptregistry.js.map +1 -1
  61. package/dist/asset/asset_manifest.js +59 -0
  62. package/dist/asset/asset_manifest.js.map +1 -0
  63. package/dist/asset/asset_preload.js +258 -0
  64. package/dist/asset/asset_preload.js.map +1 -0
  65. package/dist/asset/assetmanager.js +117 -65
  66. package/dist/asset/assetmanager.js.map +1 -1
  67. package/dist/asset/loaders/ktx2/basis/LICENSE +201 -0
  68. package/dist/asset/loaders/ktx2/basis/basis_transcoder.js +2 -0
  69. package/dist/asset/loaders/ktx2/basis/basis_transcoder.wasm +0 -0
  70. package/dist/asset/loaders/ktx2/basis_location.js +17 -0
  71. package/dist/asset/loaders/ktx2/basis_location.js.map +1 -0
  72. package/dist/asset/loaders/ktx2/ktx2_loader.js +88 -0
  73. package/dist/asset/loaders/ktx2/ktx2_loader.js.map +1 -0
  74. package/dist/asset/loaders/ktx2/ktx2_targets.js +118 -0
  75. package/dist/asset/loaders/ktx2/ktx2_targets.js.map +1 -0
  76. package/dist/asset/loaders/ktx2/transcoder_pool.js +259 -0
  77. package/dist/asset/loaders/ktx2/transcoder_pool.js.map +1 -0
  78. package/dist/asset/mesh_lod_builder.js +104 -0
  79. package/dist/asset/mesh_lod_builder.js.map +1 -0
  80. package/dist/asset/mesh_settings.js +111 -0
  81. package/dist/asset/mesh_settings.js.map +1 -0
  82. package/dist/asset/meshopt_decoder.js +18 -0
  83. package/dist/asset/meshopt_decoder.js.map +1 -0
  84. package/dist/asset/meshopt_simplifier.js +26 -0
  85. package/dist/asset/meshopt_simplifier.js.map +1 -0
  86. package/dist/asset/model.js +171 -1
  87. package/dist/asset/model.js.map +1 -1
  88. package/dist/asset/texture_settings.js +231 -0
  89. package/dist/asset/texture_settings.js.map +1 -0
  90. package/dist/asset/zmsh_binary.js +131 -0
  91. package/dist/asset/zmsh_binary.js.map +1 -0
  92. package/dist/asset/zsdf_binary.js +111 -0
  93. package/dist/asset/zsdf_binary.js.map +1 -0
  94. package/dist/blitter/bilateralblur.js +1 -2
  95. package/dist/blitter/bilateralblur.js.map +1 -1
  96. package/dist/blitter/gaussianblur.js +1 -6
  97. package/dist/blitter/gaussianblur.js.map +1 -1
  98. package/dist/camera/base.js +31 -10
  99. package/dist/camera/base.js.map +1 -1
  100. package/dist/camera/camera.js +104 -178
  101. package/dist/camera/camera.js.map +1 -1
  102. package/dist/camera/follow.js +342 -0
  103. package/dist/camera/follow.js.map +1 -0
  104. package/dist/camera/fps.js +9 -7
  105. package/dist/camera/fps.js.map +1 -1
  106. package/dist/camera/orbit.js +46 -19
  107. package/dist/camera/orbit.js.map +1 -1
  108. package/dist/index.d.ts +25436 -15990
  109. package/dist/index.js +65 -11
  110. package/dist/index.js.map +1 -1
  111. package/dist/material/blinn.js +3 -0
  112. package/dist/material/blinn.js.map +1 -1
  113. package/dist/material/eye.js +1 -1
  114. package/dist/material/eye.js.map +1 -1
  115. package/dist/material/hair.js +2 -3
  116. package/dist/material/hair.js.map +1 -1
  117. package/dist/material/lambert.js +4 -1
  118. package/dist/material/lambert.js.map +1 -1
  119. package/dist/material/material.js +66 -4
  120. package/dist/material/material.js.map +1 -1
  121. package/dist/material/meshmaterial.js +118 -45
  122. package/dist/material/meshmaterial.js.map +1 -1
  123. package/dist/material/mixins/lightmodel/blinnphong.js +1 -1
  124. package/dist/material/mixins/lightmodel/blinnphong.js.map +1 -1
  125. package/dist/material/mixins/lightmodel/lambert.js +1 -1
  126. package/dist/material/mixins/lightmodel/lambert.js.map +1 -1
  127. package/dist/material/mixins/lightmodel/pbrblueprintmixin.js +34 -61
  128. package/dist/material/mixins/lightmodel/pbrblueprintmixin.js.map +1 -1
  129. package/dist/material/mixins/lightmodel/pbrmetallicroughness.js +16 -2
  130. package/dist/material/mixins/lightmodel/pbrmetallicroughness.js.map +1 -1
  131. package/dist/material/mixins/lightmodel/pbrspecularglossness.js +2 -1
  132. package/dist/material/mixins/lightmodel/pbrspecularglossness.js.map +1 -1
  133. package/dist/material/mixins/lit.js +191 -83
  134. package/dist/material/mixins/lit.js.map +1 -1
  135. package/dist/material/mixins/pbr/common.js +31 -206
  136. package/dist/material/mixins/pbr/common.js.map +1 -1
  137. package/dist/material/mtoon.js +1 -1
  138. package/dist/material/mtoon.js.map +1 -1
  139. package/dist/material/pbrmr.js +3 -0
  140. package/dist/material/pbrmr.js.map +1 -1
  141. package/dist/material/pbrsg.js +3 -0
  142. package/dist/material/pbrsg.js.map +1 -1
  143. package/dist/material/sdf_debug.js +78 -0
  144. package/dist/material/sdf_debug.js.map +1 -0
  145. package/dist/material/shader/helper.js +333 -191
  146. package/dist/material/shader/helper.js.map +1 -1
  147. package/dist/material/shader/terrain_blend.js +204 -43
  148. package/dist/material/shader/terrain_blend.js.map +1 -1
  149. package/dist/material/skin.js +331 -297
  150. package/dist/material/skin.js.map +1 -1
  151. package/dist/material/skinprofile.js +1058 -0
  152. package/dist/material/skinprofile.js.map +1 -0
  153. package/dist/material/sss.js +353 -0
  154. package/dist/material/sss.js.map +1 -0
  155. package/dist/material/sss_kernel.js +265 -0
  156. package/dist/material/sss_kernel.js.map +1 -0
  157. package/dist/material/sssprofile.js +879 -0
  158. package/dist/material/sssprofile.js.map +1 -0
  159. package/dist/material/subsurfaceprofile.js +4 -8
  160. package/dist/material/subsurfaceprofile.js.map +1 -1
  161. package/dist/material/terrain-cm.js +464 -188
  162. package/dist/material/terrain-cm.js.map +1 -1
  163. package/dist/material/unlit.js +3 -0
  164. package/dist/material/unlit.js.map +1 -1
  165. package/dist/material/water.js +1261 -1438
  166. package/dist/material/water.js.map +1 -1
  167. package/dist/node_modules/meshoptimizer/LICENSE +21 -0
  168. package/dist/node_modules/meshoptimizer/meshopt_decoder.js +197 -0
  169. package/dist/node_modules/meshoptimizer/meshopt_decoder.js.map +1 -0
  170. package/dist/node_modules/meshoptimizer/meshopt_simplifier.js +677 -0
  171. package/dist/node_modules/meshoptimizer/meshopt_simplifier.js.map +1 -0
  172. package/dist/physics/character.js +226 -0
  173. package/dist/physics/character.js.map +1 -0
  174. package/dist/physics/collider.js +209 -0
  175. package/dist/physics/collider.js.map +1 -0
  176. package/dist/physics/events.js +79 -0
  177. package/dist/physics/events.js.map +1 -0
  178. package/dist/physics/joint.js +272 -0
  179. package/dist/physics/joint.js.map +1 -0
  180. package/dist/physics/node_physics.js +234 -0
  181. package/dist/physics/node_physics.js.map +1 -0
  182. package/dist/physics/part.js +91 -0
  183. package/dist/physics/part.js.map +1 -0
  184. package/dist/physics/rigid_body.js +379 -0
  185. package/dist/physics/rigid_body.js.map +1 -0
  186. package/dist/physics/vehicle.js +335 -0
  187. package/dist/physics/vehicle.js.map +1 -0
  188. package/dist/physics/world.js +25 -0
  189. package/dist/physics/world.js.map +1 -0
  190. package/dist/posteffect/bloom.js +49 -14
  191. package/dist/posteffect/bloom.js.map +1 -1
  192. package/dist/posteffect/motionblur.js +106 -106
  193. package/dist/posteffect/motionblur.js.map +1 -1
  194. package/dist/posteffect/postsss.js +572 -0
  195. package/dist/posteffect/postsss.js.map +1 -0
  196. package/dist/posteffect/sao.js +2 -6
  197. package/dist/posteffect/sao.js.map +1 -1
  198. package/dist/posteffect/skinsss.js +605 -355
  199. package/dist/posteffect/skinsss.js.map +1 -1
  200. package/dist/posteffect/ssgi.js +91 -162
  201. package/dist/posteffect/ssgi.js.map +1 -1
  202. package/dist/posteffect/ssr.js +3 -11
  203. package/dist/posteffect/ssr.js.map +1 -1
  204. package/dist/posteffect/sss.js +4 -13
  205. package/dist/posteffect/sss.js.map +1 -1
  206. package/dist/posteffect/taa.js +1 -0
  207. package/dist/posteffect/taa.js.map +1 -1
  208. package/dist/render/abuffer_oit.js +3 -2
  209. package/dist/render/abuffer_oit.js.map +1 -1
  210. package/dist/render/clipmap.js +179 -64
  211. package/dist/render/clipmap.js.map +1 -1
  212. package/dist/render/cluster_light.js +958 -224
  213. package/dist/render/cluster_light.js.map +1 -1
  214. package/dist/render/cull_visitor.js +25 -5
  215. package/dist/render/cull_visitor.js.map +1 -1
  216. package/dist/render/debug_views.js +37 -0
  217. package/dist/render/debug_views.js.map +1 -0
  218. package/dist/render/depthpass.js +5 -1
  219. package/dist/render/depthpass.js.map +1 -1
  220. package/dist/render/drawable_mixin.js +53 -6
  221. package/dist/render/drawable_mixin.js.map +1 -1
  222. package/dist/render/dualdepthpeeling_oit.js +5 -5
  223. package/dist/render/dualdepthpeeling_oit.js.map +1 -1
  224. package/dist/render/envlight.js +196 -113
  225. package/dist/render/envlight.js.map +1 -1
  226. package/dist/render/fbm_wavegenerator.js +5 -10
  227. package/dist/render/fbm_wavegenerator.js.map +1 -1
  228. package/dist/render/fft_wavegenerator.js +247 -157
  229. package/dist/render/fft_wavegenerator.js.map +1 -1
  230. package/dist/render/gerstner_wavegenerator.js +16 -38
  231. package/dist/render/gerstner_wavegenerator.js.map +1 -1
  232. package/dist/render/gi/probe_volume.js +1334 -0
  233. package/dist/render/gi/probe_volume.js.map +1 -0
  234. package/dist/render/gi/scene_gi.js +282 -0
  235. package/dist/render/gi/scene_gi.js.map +1 -0
  236. package/dist/render/gi/voxel_atlas.js +198 -0
  237. package/dist/render/gi/voxel_atlas.js.map +1 -0
  238. package/dist/render/gi/voxel_lighting.js +652 -0
  239. package/dist/render/gi/voxel_lighting.js.map +1 -0
  240. package/dist/render/gi/voxel_scene.js +642 -0
  241. package/dist/render/gi/voxel_scene.js.map +1 -0
  242. package/dist/render/gi/voxelize_pass.js +223 -0
  243. package/dist/render/gi/voxelize_pass.js.map +1 -0
  244. package/dist/render/globalbindgroup_allocator.js +3 -2
  245. package/dist/render/globalbindgroup_allocator.js.map +1 -1
  246. package/dist/render/gpu_picking.js +9 -36
  247. package/dist/render/gpu_picking.js.map +1 -1
  248. package/dist/render/instance_culling.js +499 -0
  249. package/dist/render/instance_culling.js.map +1 -0
  250. package/dist/render/lightpass.js +19 -8
  251. package/dist/render/lightpass.js.map +1 -1
  252. package/dist/render/lod.js +46 -0
  253. package/dist/render/lod.js.map +1 -0
  254. package/dist/render/primitive.js +301 -9
  255. package/dist/render/primitive.js.map +1 -1
  256. package/dist/render/primitive_readback.js +209 -0
  257. package/dist/render/primitive_readback.js.map +1 -0
  258. package/dist/render/render_queue.js +162 -11
  259. package/dist/render/render_queue.js.map +1 -1
  260. package/dist/render/renderer.js +10 -7
  261. package/dist/render/renderer.js.map +1 -1
  262. package/dist/render/rendergraph/blackboard.js +14 -1
  263. package/dist/render/rendergraph/blackboard.js.map +1 -1
  264. package/dist/render/rendergraph/executor.js +136 -0
  265. package/dist/render/rendergraph/executor.js.map +1 -1
  266. package/dist/render/rendergraph/forward_plus_builder.js +272 -55
  267. package/dist/render/rendergraph/forward_plus_builder.js.map +1 -1
  268. package/dist/render/rendergraph/frame_graph_context.js +41 -0
  269. package/dist/render/rendergraph/frame_graph_context.js.map +1 -0
  270. package/dist/render/rendergraph/scene_render_context.js +31 -2
  271. package/dist/render/rendergraph/scene_render_context.js.map +1 -1
  272. package/dist/render/rendergraph/types.js.map +1 -1
  273. package/dist/render/renderpass.js +28 -5
  274. package/dist/render/renderpass.js.map +1 -1
  275. package/dist/render/sdf/box_allocator.js +98 -0
  276. package/dist/render/sdf/box_allocator.js.map +1 -0
  277. package/dist/render/sdf/global_sdf.js +865 -0
  278. package/dist/render/sdf/global_sdf.js.map +1 -0
  279. package/dist/render/sdf/scene_sdf.js +562 -0
  280. package/dist/render/sdf/scene_sdf.js.map +1 -0
  281. package/dist/render/sdf/sdf_atlas.js +227 -0
  282. package/dist/render/sdf/sdf_atlas.js.map +1 -0
  283. package/dist/render/sdf/sdf_programs.js +42 -0
  284. package/dist/render/sdf/sdf_programs.js.map +1 -0
  285. package/dist/render/sdf/sdf_view.js +199 -0
  286. package/dist/render/sdf/sdf_view.js.map +1 -0
  287. package/dist/render/sdf/waterline.js +610 -0
  288. package/dist/render/sdf/waterline.js.map +1 -0
  289. package/dist/render/shadow_mask_pass.js +142 -25
  290. package/dist/render/shadow_mask_pass.js.map +1 -1
  291. package/dist/render/shore_wave_generator.js +1193 -0
  292. package/dist/render/shore_wave_generator.js.map +1 -0
  293. package/dist/render/sky.js +516 -86
  294. package/dist/render/sky.js.map +1 -1
  295. package/dist/render/transmission_thickness_pass.js +477 -0
  296. package/dist/render/transmission_thickness_pass.js.map +1 -0
  297. package/dist/render/underwater.js +58 -158
  298. package/dist/render/underwater.js.map +1 -1
  299. package/dist/render/virtualtexture/virtual_texture.js +1214 -0
  300. package/dist/render/virtualtexture/virtual_texture.js.map +1 -0
  301. package/dist/render/volumetric_cloud/cloud_common.js +472 -0
  302. package/dist/render/volumetric_cloud/cloud_common.js.map +1 -0
  303. package/dist/render/volumetric_cloud/cloud_noise.js +189 -0
  304. package/dist/render/volumetric_cloud/cloud_noise.js.map +1 -0
  305. package/dist/render/volumetric_cloud/debug_view.js +131 -0
  306. package/dist/render/volumetric_cloud/debug_view.js.map +1 -0
  307. package/dist/render/volumetric_cloud/resources.js +380 -0
  308. package/dist/render/volumetric_cloud/resources.js.map +1 -0
  309. package/dist/render/volumetric_cloud/sky_cache.js +380 -0
  310. package/dist/render/volumetric_cloud/sky_cache.js.map +1 -0
  311. package/dist/render/volumetric_cloud/volumetric_cloud.js +318 -0
  312. package/dist/render/volumetric_cloud/volumetric_cloud.js.map +1 -0
  313. package/dist/render/water_caustics.js +134 -319
  314. package/dist/render/water_caustics.js.map +1 -1
  315. package/dist/render/water_foam.js +627 -0
  316. package/dist/render/water_foam.js.map +1 -0
  317. package/dist/render/water_interaction.js +1795 -0
  318. package/dist/render/water_interaction.js.map +1 -0
  319. package/dist/render/weightedblended_oit.js +3 -3
  320. package/dist/render/weightedblended_oit.js.map +1 -1
  321. package/dist/scene/basesprite.js +9 -4
  322. package/dist/scene/basesprite.js.map +1 -1
  323. package/dist/scene/batchgroup.js +108 -12
  324. package/dist/scene/batchgroup.js.map +1 -1
  325. package/dist/scene/environment.js +28 -7
  326. package/dist/scene/environment.js.map +1 -1
  327. package/dist/scene/foliage.js +1020 -0
  328. package/dist/scene/foliage.js.map +1 -0
  329. package/dist/scene/foliage_buckets.js +147 -0
  330. package/dist/scene/foliage_buckets.js.map +1 -0
  331. package/dist/scene/foliage_mask.js +358 -0
  332. package/dist/scene/foliage_mask.js.map +1 -0
  333. package/dist/scene/hair.js +15 -4
  334. package/dist/scene/hair.js.map +1 -1
  335. package/dist/scene/light.js +179 -13
  336. package/dist/scene/light.js.map +1 -1
  337. package/dist/scene/mesh.js +327 -45
  338. package/dist/scene/mesh.js.map +1 -1
  339. package/dist/scene/meshdrawable.js +6 -0
  340. package/dist/scene/meshdrawable.js.map +1 -1
  341. package/dist/scene/msdftext.js +9 -3
  342. package/dist/scene/msdftext.js.map +1 -1
  343. package/dist/scene/msdftextsprite.js +9 -3
  344. package/dist/scene/msdftextsprite.js.map +1 -1
  345. package/dist/scene/particlesys.js +8 -2
  346. package/dist/scene/particlesys.js.map +1 -1
  347. package/dist/scene/physics_settings.js +157 -0
  348. package/dist/scene/physics_settings.js.map +1 -0
  349. package/dist/scene/scene.js +190 -44
  350. package/dist/scene/scene.js.map +1 -1
  351. package/dist/scene/scene_node.js +147 -6
  352. package/dist/scene/scene_node.js.map +1 -1
  353. package/dist/scene/terrain-cm/bladegrassmaterial.js +735 -0
  354. package/dist/scene/terrain-cm/bladegrassmaterial.js.map +1 -0
  355. package/dist/scene/terrain-cm/grass.js +881 -42
  356. package/dist/scene/terrain-cm/grass.js.map +1 -1
  357. package/dist/scene/terrain-cm/grass_gpu.js +929 -0
  358. package/dist/scene/terrain-cm/grass_gpu.js.map +1 -0
  359. package/dist/scene/terrain-cm/grassmaterial.js +180 -9
  360. package/dist/scene/terrain-cm/grassmaterial.js.map +1 -1
  361. package/dist/scene/terrain-cm/terrain-cm.js +465 -36
  362. package/dist/scene/terrain-cm/terrain-cm.js.map +1 -1
  363. package/dist/scene/terrain-cm/terrain_gi_albedo.js +257 -0
  364. package/dist/scene/terrain-cm/terrain_gi_albedo.js.map +1 -0
  365. package/dist/scene/terrain-cm/terrain_rvt.js +395 -0
  366. package/dist/scene/terrain-cm/terrain_rvt.js.map +1 -0
  367. package/dist/scene/update_queue.js +54 -0
  368. package/dist/scene/update_queue.js.map +1 -0
  369. package/dist/scene/water.js +633 -268
  370. package/dist/scene/water.js.map +1 -1
  371. package/dist/scene/water_surface_sampler.js +324 -0
  372. package/dist/scene/water_surface_sampler.js.map +1 -0
  373. package/dist/scene/wind.js +317 -0
  374. package/dist/scene/wind.js.map +1 -0
  375. package/dist/shaders/atmosphere.js +388 -166
  376. package/dist/shaders/atmosphere.js.map +1 -1
  377. package/dist/shaders/fog.js +100 -32
  378. package/dist/shaders/fog.js.map +1 -1
  379. package/dist/shaders/gi.js +365 -0
  380. package/dist/shaders/gi.js.map +1 -0
  381. package/dist/shaders/ltc_rect.js +202 -0
  382. package/dist/shaders/ltc_rect.js.map +1 -0
  383. package/dist/shaders/misc.js +1 -10
  384. package/dist/shaders/misc.js.map +1 -1
  385. package/dist/shaders/noise.js +5 -1
  386. package/dist/shaders/noise.js.map +1 -1
  387. package/dist/shaders/probes.js +309 -0
  388. package/dist/shaders/probes.js.map +1 -0
  389. package/dist/shaders/sdf.js +537 -0
  390. package/dist/shaders/sdf.js.map +1 -0
  391. package/dist/shaders/shadow.js +104 -70
  392. package/dist/shaders/shadow.js.map +1 -1
  393. package/dist/shaders/skin_brdf.js +243 -0
  394. package/dist/shaders/skin_brdf.js.map +1 -0
  395. package/dist/shaders/ssgi.js +63 -0
  396. package/dist/shaders/ssgi.js.map +1 -0
  397. package/dist/shaders/ssgi_reprojection.js +118 -0
  398. package/dist/shaders/ssgi_reprojection.js.map +1 -0
  399. package/dist/shaders/ssr.js +6 -159
  400. package/dist/shaders/ssr.js.map +1 -1
  401. package/dist/shaders/temporal.js +38 -27
  402. package/dist/shaders/temporal.js.map +1 -1
  403. package/dist/shaders/water.js +32 -82
  404. package/dist/shaders/water.js.map +1 -1
  405. package/dist/shaders/water_medium.js +7 -21
  406. package/dist/shaders/water_medium.js.map +1 -1
  407. package/dist/shadow/dom.js +110 -19
  408. package/dist/shadow/dom.js.map +1 -1
  409. package/dist/shadow/esm.js +3 -3
  410. package/dist/shadow/esm.js.map +1 -1
  411. package/dist/shadow/pcf_opt.js +15 -48
  412. package/dist/shadow/pcf_opt.js.map +1 -1
  413. package/dist/shadow/pcf_pd.js +15 -53
  414. package/dist/shadow/pcf_pd.js.map +1 -1
  415. package/dist/shadow/pcss.js +34 -9
  416. package/dist/shadow/pcss.js.map +1 -1
  417. package/dist/shadow/shadow_impl.js +3 -8
  418. package/dist/shadow/shadow_impl.js.map +1 -1
  419. package/dist/shadow/shadow_region.js +40 -2
  420. package/dist/shadow/shadow_region.js.map +1 -1
  421. package/dist/shadow/shadowmapper.js +93 -53
  422. package/dist/shadow/shadowmapper.js.map +1 -1
  423. package/dist/shadow/ssm.js +18 -72
  424. package/dist/shadow/ssm.js.map +1 -1
  425. package/dist/shadow/vsm.js +3 -3
  426. package/dist/shadow/vsm.js.map +1 -1
  427. package/dist/shapes/shape.js +44 -1
  428. package/dist/shapes/shape.js.map +1 -1
  429. package/dist/utility/buoyancy.js +287 -0
  430. package/dist/utility/buoyancy.js.map +1 -0
  431. package/dist/utility/physical.js +10 -1
  432. package/dist/utility/physical.js.map +1 -1
  433. package/dist/utility/pmrem.js +17 -43
  434. package/dist/utility/pmrem.js.map +1 -1
  435. package/dist/utility/sdf/bake.js +435 -0
  436. package/dist/utility/sdf/bake.js.map +1 -0
  437. package/dist/utility/sdf/bvh.js +576 -0
  438. package/dist/utility/sdf/bvh.js.map +1 -0
  439. package/dist/utility/serialization/manager.js +34 -6
  440. package/dist/utility/serialization/manager.js.map +1 -1
  441. package/dist/utility/serialization/scene/animation.js +46 -5
  442. package/dist/utility/serialization/scene/animation.js.map +1 -1
  443. package/dist/utility/serialization/scene/batch.js +11 -4
  444. package/dist/utility/serialization/scene/batch.js.map +1 -1
  445. package/dist/utility/serialization/scene/camera.js +87 -254
  446. package/dist/utility/serialization/scene/camera.js.map +1 -1
  447. package/dist/utility/serialization/scene/cloth.js +25 -0
  448. package/dist/utility/serialization/scene/cloth.js.map +1 -0
  449. package/dist/utility/serialization/scene/common.js +14 -6
  450. package/dist/utility/serialization/scene/common.js.map +1 -1
  451. package/dist/utility/serialization/scene/foliage.js +821 -0
  452. package/dist/utility/serialization/scene/foliage.js.map +1 -0
  453. package/dist/utility/serialization/scene/hair.js +11 -4
  454. package/dist/utility/serialization/scene/hair.js.map +1 -1
  455. package/dist/utility/serialization/scene/light.js +115 -7
  456. package/dist/utility/serialization/scene/light.js.map +1 -1
  457. package/dist/utility/serialization/scene/material.js +388 -220
  458. package/dist/utility/serialization/scene/material.js.map +1 -1
  459. package/dist/utility/serialization/scene/mesh.js +69 -4
  460. package/dist/utility/serialization/scene/mesh.js.map +1 -1
  461. package/dist/utility/serialization/scene/node.js +140 -5
  462. package/dist/utility/serialization/scene/node.js.map +1 -1
  463. package/dist/utility/serialization/scene/node_physics.js +1318 -0
  464. package/dist/utility/serialization/scene/node_physics.js.map +1 -0
  465. package/dist/utility/serialization/scene/particle.js +11 -4
  466. package/dist/utility/serialization/scene/particle.js.map +1 -1
  467. package/dist/utility/serialization/scene/physics.js +120 -0
  468. package/dist/utility/serialization/scene/physics.js.map +1 -0
  469. package/dist/utility/serialization/scene/primitive.js +124 -10
  470. package/dist/utility/serialization/scene/primitive.js.map +1 -1
  471. package/dist/utility/serialization/scene/scene.js +1687 -51
  472. package/dist/utility/serialization/scene/scene.js.map +1 -1
  473. package/dist/utility/serialization/scene/sprite.js +11 -4
  474. package/dist/utility/serialization/scene/sprite.js.map +1 -1
  475. package/dist/utility/serialization/scene/terrain.js +163 -33
  476. package/dist/utility/serialization/scene/terrain.js.map +1 -1
  477. package/dist/utility/serialization/scene/text.js +11 -4
  478. package/dist/utility/serialization/scene/text.js.map +1 -1
  479. package/dist/utility/serialization/scene/water.js +1543 -264
  480. package/dist/utility/serialization/scene/water.js.map +1 -1
  481. package/dist/utility/serialization/types.js.map +1 -1
  482. package/dist/utility/shprojector.js +10 -22
  483. package/dist/utility/shprojector.js.map +1 -1
  484. package/dist/utility/textures/foam.js +170 -0
  485. package/dist/utility/textures/foam.js.map +1 -0
  486. package/dist/utility/textures/ggxlut.js +17 -43
  487. package/dist/utility/textures/ggxlut.js.map +1 -1
  488. package/dist/utility/textures/ltcdata.js +4 -4
  489. package/dist/utility/textures/ltcdata.js.map +1 -1
  490. package/dist/utility/textures/ltclut.js +16 -6
  491. package/dist/utility/textures/ltclut.js.map +1 -1
  492. package/dist/utility/unsupported.js +24 -0
  493. package/dist/utility/unsupported.js.map +1 -0
  494. package/dist/values.js +13 -1
  495. package/dist/values.js.map +1 -1
  496. package/package.json +25 -12
  497. package/src/asset/loaders/ktx2/basis/LICENSE +201 -0
  498. package/src/asset/loaders/ktx2/basis/basis_transcoder.js +2 -0
  499. package/src/asset/loaders/ktx2/basis/basis_transcoder.wasm +0 -0
  500. package/src/asset/meshoptimizer/LICENSE +21 -0
  501. package/dist/animation/joint_dynamics/convex_collider.js +0 -320
  502. package/dist/animation/joint_dynamics/convex_collider.js.map +0 -1
@@ -1,9 +1,10 @@
1
1
  import { linearToGamma } from '../shaders/misc.js';
2
2
  import { smoothNoise3D } from '../shaders/noise.js';
3
3
  import { getDevice, getEngine } from '../app/api.js';
4
- import { getDefaultAtmosphereParams, getAerialPerspectiveLut, renderAtmosphereLUTs, getAtmosphereParamsStruct, createTransmittanceLutProgram, createMultiScatteringLutProgram, createSkyViewLutProgram, createAPLutProgram, skyBox, atmosphereLUTRendered, getTransmittanceLut, getSkyViewLut } from '../shaders/atmosphere.js';
5
- import { Matrix4x4, Disposable, DRef, Vector4, Vector2, Vector3, CubeFace, objectKeys, DEPTH_FARTHEST, DEPTH_COMPARE_DEFAULT, DEPTH_COMPARE_FARTHER } from '@zephyr3d/base';
4
+ import { getDefaultAtmosphereParams, getAerialPerspectiveLut, renderAtmosphereLUTs, MIN_OBSERVER_ALTITUDE, getMultiScatteringLut, getAtmosphereParamsStruct, createTransmittanceLutProgram, createMultiScatteringLutProgram, createSkyViewLutProgram, createAPLutProgram, skyBox, rayIntersectSphere, transmittanceToSky, atmosphereLUTRendered, getTransmittanceLut, getSkyViewLut } from '../shaders/atmosphere.js';
5
+ import { Matrix4x4, Disposable, DRef, Vector4, Vector3, Vector2, CubeFace, objectKeys, DEPTH_FARTHEST, DEPTH_COMPARE_DEFAULT, DEPTH_COMPARE_FARTHER } from '@zephyr3d/base';
6
6
  import { Primitive } from './primitive.js';
7
+ import '../node_modules/meshoptimizer/meshopt_simplifier.js';
7
8
  import { BoxShape } from '../shapes/box.js';
8
9
  import '../shapes/capsule.js';
9
10
  import '../shapes/cylinder.js';
@@ -20,14 +21,22 @@ import { uniformSphereSamples, Fog } from '../values.js';
20
21
  import { getDefaultHeightFogParams, MAX_FOG_HEIGHT, getHeightFogParamsStruct, calculateFog } from '../shaders/fog.js';
21
22
  import { drawFullscreenQuad } from './fullscreenquad.js';
22
23
  import { panoramaToCubemap } from '../utility/panorama.js';
24
+ import { VolumetricCloud } from './volumetric_cloud/volumetric_cloud.js';
25
+ import { VolumetricCloudResources } from './volumetric_cloud/resources.js';
26
+ import { drawVolumetricCloudDebug } from './volumetric_cloud/debug_view.js';
27
+ import { VolumetricCloudSkyCache } from './volumetric_cloud/sky_cache.js';
28
+ import { cloudCacheEncode } from './volumetric_cloud/cloud_common.js';
23
29
  import '../camera/camera.js';
24
30
  import { PerspectiveCamera } from '../camera/perspectivecamera.js';
31
+ import '../camera/follow.js';
25
32
 
26
33
  const fogTypeMap = {
27
34
  height_fog: Fog.FOG_TYPE_HEIGHT,
28
35
  none: Fog.FOG_TYPE_NONE
29
36
  };
30
37
  const defaultSkyWorldMatrix = Matrix4x4.identity();
38
+ const SKY_OUTPUT_MAX = 64512 * 0.5;
39
+ /** Volumetric clouds give way to procedural ones this far below the cloud base, in meters */ const VOLUMETRIC_CLOUD_CEILING_MARGIN = 500;
31
40
  /**
32
41
  * The sky renderer
33
42
  * @public
@@ -97,10 +106,29 @@ const defaultSkyWorldMatrix = Matrix4x4.identity();
97
106
  _radianceMapWidth;
98
107
  _atmosphereParams;
99
108
  _atmosphereExposure;
109
+ _lowerHemisphereIsBlack;
110
+ _physicalSunDisk;
111
+ /** Observer local up (planet center to observer) of the current frame, in world space. */ _observerUp;
112
+ /** Observer altitude and local up the IBL was last baked at; altitude < 0 when never baked. */ _bakedObserverAltitude;
113
+ _bakedObserverUp;
114
+ _rebakeAltitudeRatio;
115
+ _rebakeAngle;
100
116
  _fogType;
101
117
  _heightFogParams;
102
118
  _cloudy;
103
119
  _cloudIntensity;
120
+ _cloudMode;
121
+ _volumetricCloud;
122
+ _volumetricCloudResources;
123
+ _volumetricCloudCache;
124
+ /** Volumetric clouds drawn this frame: requested, supported and the observer below the layer */ _volumetricActive;
125
+ _bindgroupSkyVolumetric;
126
+ static _programSkyVolumetric = null;
127
+ static _volumetricAltitudeWarned = false;
128
+ _cloudDebugView;
129
+ _cloudDebugSlice;
130
+ _cloudDebugChannel;
131
+ static _volumetricFallbackWarned = false;
104
132
  _debugAerialPerspective;
105
133
  _wind;
106
134
  _skyboxRotation;
@@ -125,6 +153,7 @@ const defaultSkyWorldMatrix = Matrix4x4.identity();
125
153
  * @internal Ratio converting the fog shader's atmosphere-derived LUT samples from the fixed bake
126
154
  * exposure to the live one (1 in legacy).
127
155
  */ _fogPreExposure;
156
+ /** @internal Cubemap latched in update() for the height fog sky light term. */ _fogSkyLightCubemap;
128
157
  /**
129
158
  * Creates an instance of SkyRenderer
130
159
  */ constructor(){
@@ -147,15 +176,33 @@ const defaultSkyWorldMatrix = Matrix4x4.identity();
147
176
  this._irradianceFrameBuffer = new DRef();
148
177
  this._atmosphereParams = getDefaultAtmosphereParams();
149
178
  this._atmosphereExposure = 1;
179
+ this._lowerHemisphereIsBlack = true;
180
+ this._physicalSunDisk = false;
181
+ this._observerUp = Vector3.axisPY();
182
+ this._bakedObserverAltitude = -1;
183
+ this._bakedObserverUp = Vector3.axisPY();
184
+ this._rebakeAltitudeRatio = 0.05;
185
+ this._rebakeAngle = 0.5;
150
186
  this._debugAerialPerspective = 0;
151
187
  this._fogType = 'height_fog';
152
188
  this._heightFogParams = getDefaultHeightFogParams();
153
189
  this._cloudy = 0.45;
154
190
  this._cloudIntensity = 15;
191
+ this._cloudMode = 'procedural';
192
+ this._volumetricCloud = new VolumetricCloud();
193
+ this._volumetricCloudResources = new VolumetricCloudResources();
194
+ this._volumetricCloudCache = new VolumetricCloudSkyCache();
195
+ this._volumetricActive = false;
196
+ this._bindgroupSkyVolumetric = new DRef();
197
+ this._cloudDebugView = 'none';
198
+ this._cloudDebugSlice = 0;
199
+ this._cloudDebugChannel = 0;
155
200
  this._wind = new Vector2(0, 0);
156
201
  this._skyboxRotation = new Vector3(0, 0, 0);
157
202
  this._skyWorldMatrix = defaultSkyWorldMatrix;
158
- this._lastSunDir = SkyRenderer._getSunDir(null);
203
+ // Copied: without a sun light _getSunDir returns the shared ShaderHelper.defaultSunDir, which
204
+ // update() would otherwise overwrite with the last scene's sun direction.
205
+ this._lastSunDir = new Vector3(SkyRenderer._getSunDir(null));
159
206
  this._lastSunColor = SkyRenderer._getSunColor(null);
160
207
  this._panoramaAsset = '';
161
208
  this._shProjector = new CubemapSHProjector(10000);
@@ -169,6 +216,7 @@ const defaultSkyWorldMatrix = Matrix4x4.identity();
169
216
  this._format = null;
170
217
  this._fogLuminanceScale = 1;
171
218
  this._fogPreExposure = 1;
219
+ this._fogSkyLightCubemap = null;
172
220
  }
173
221
  /** @internal */ getHash(_ctx) {
174
222
  return `${this.skyType}:${this.fogType}`;
@@ -272,7 +320,10 @@ const defaultSkyWorldMatrix = Matrix4x4.identity();
272
320
  this.invalidate();
273
321
  }
274
322
  }
275
- /** Aerial perspective density */ get aerialPerspectiveDistance() {
323
+ /**
324
+ * Distance covered by the aerial perspective LUT, in atmosphere meters (world distance times
325
+ * {@link SkyRenderer.cameraHeightScale}). Beyond it aerial perspective no longer increases.
326
+ */ get aerialPerspectiveDistance() {
276
327
  return this._atmosphereParams.apDistance;
277
328
  }
278
329
  set aerialPerspectiveDistance(val) {
@@ -281,6 +332,40 @@ const defaultSkyWorldMatrix = Matrix4x4.identity();
281
332
  this.invalidate();
282
333
  }
283
334
  }
335
+ /**
336
+ * Whether the environment lighting baked from the scattering sky sees black below the horizon.
337
+ */ get lowerHemisphereIsBlack() {
338
+ return this._lowerHemisphereIsBlack;
339
+ }
340
+ set lowerHemisphereIsBlack(val) {
341
+ if (val !== this._lowerHemisphereIsBlack) {
342
+ this._lowerHemisphereIsBlack = val;
343
+ this.invalidate();
344
+ }
345
+ }
346
+ /**
347
+ * Strength of the aerial perspective: scales the distance the atmosphere is integrated over
348
+ * between the camera and the scene, so distant objects get hazier (>1) or clearer (<1). The sky
349
+ * itself is unaffected.
350
+ */ get aerialPerspectiveViewDistanceScale() {
351
+ return this._atmosphereParams.apViewDistanceScale;
352
+ }
353
+ set aerialPerspectiveViewDistanceScale(val) {
354
+ val = Math.max(0, val);
355
+ if (val !== this._atmosphereParams.apViewDistanceScale) {
356
+ this._atmosphereParams.apViewDistanceScale = val;
357
+ }
358
+ }
359
+ /**
360
+ * Distance from the camera where aerial perspective starts, in the same units as
361
+ * {@link SkyRenderer.aerialPerspectiveDistance} (atmosphere meters). Nearer surfaces get no
362
+ * atmospheric haze.
363
+ */ get aerialPerspectiveStartDepth() {
364
+ return this._atmosphereParams.apStartDepth;
365
+ }
366
+ set aerialPerspectiveStartDepth(val) {
367
+ this._atmosphereParams.apStartDepth = Math.max(0, val);
368
+ }
284
369
  /** Atmosphere exposure */ get atmosphereExposure() {
285
370
  return this._atmosphereExposure;
286
371
  }
@@ -290,7 +375,52 @@ const defaultSkyWorldMatrix = Matrix4x4.identity();
290
375
  this.invalidate();
291
376
  }
292
377
  }
293
- /** Aerial perspective density */ get cameraHeightScale() {
378
+ /**
379
+ * Color of the virtual planet ground of the scattering sky.
380
+ *
381
+ * @remarks
382
+ * Shown below the horizon where no scene geometry covers the sky, lit by the sun through the
383
+ * atmosphere, and bounced back into the sky as part of multiple scattering.
384
+ */ get groundAlbedo() {
385
+ return this._atmosphereParams.groundAlbedo;
386
+ }
387
+ set groundAlbedo(val) {
388
+ if (!val.equalsTo(this._atmosphereParams.groundAlbedo)) {
389
+ this._atmosphereParams.groundAlbedo.set(val);
390
+ this.invalidate();
391
+ }
392
+ }
393
+ /**
394
+ * Relative change of the observer altitude that re-bakes the environment lighting of the
395
+ * scattering sky, e.g. 0.05 re-bakes after climbing or descending 5% (and at least 10 m).
396
+ *
397
+ * @remarks
398
+ * The on-screen sky and aerial perspective follow the camera every frame; only the cached IBL
399
+ * bake, which is expensive, waits for this threshold or {@link SkyRenderer.atmosphereRebakeAngle}.
400
+ */ get atmosphereRebakeAltitudeRatio() {
401
+ return this._rebakeAltitudeRatio;
402
+ }
403
+ set atmosphereRebakeAltitudeRatio(val) {
404
+ this._rebakeAltitudeRatio = Math.max(0, val);
405
+ }
406
+ /**
407
+ * Angle in degrees the local vertical may turn, by travelling across the curved planet, before
408
+ * the environment lighting of the scattering sky is re-baked. 0.5 degrees is about 55 km on Earth.
409
+ */ get atmosphereRebakeAngle() {
410
+ return this._rebakeAngle;
411
+ }
412
+ set atmosphereRebakeAngle(val) {
413
+ this._rebakeAngle = Math.max(0, val);
414
+ }
415
+ /**
416
+ * Atmosphere meters per world unit.
417
+ *
418
+ * @remarks
419
+ * The planet top sits at the world origin with its center straight below.
420
+ * The camera position, scaled by this factor, places the observer in the
421
+ * atmosphere: its altitude, and for large horizontal distances where on the
422
+ * curved planet it stands. Aerial perspective distances are scaled the same way.
423
+ */ get cameraHeightScale() {
294
424
  return this._atmosphereParams.cameraHeightScale;
295
425
  }
296
426
  set cameraHeightScale(val) {
@@ -390,6 +520,38 @@ const defaultSkyWorldMatrix = Matrix4x4.identity();
390
520
  }
391
521
  }
392
522
  /**
523
+ * How much distant height fog takes the color of the environment behind it (0..1).
524
+ */ get heightFogSkyLightStrength() {
525
+ return this._heightFogParams.skyLightStrength;
526
+ }
527
+ set heightFogSkyLightStrength(val) {
528
+ if (val !== this._heightFogParams.skyLightStrength) {
529
+ this._heightFogParams.skyLightStrength = val;
530
+ this.invalidate();
531
+ }
532
+ }
533
+ /**
534
+ * Blurriness of the environment seen through the fog, as a roughness selecting the radiance map
535
+ * mip.
536
+ */ get heightFogSkyLightRoughness() {
537
+ return this._heightFogParams.skyLightRoughness;
538
+ }
539
+ set heightFogSkyLightRoughness(val) {
540
+ if (val !== this._heightFogParams.skyLightRoughness) {
541
+ this._heightFogParams.skyLightRoughness = val;
542
+ this.invalidate();
543
+ }
544
+ }
545
+ /**
546
+ * @internal
547
+ * Cubemap the height fog samples for {@link SkyRenderer.heightFogSkyLightStrength}: the scene's
548
+ * IBL radiance map, or the sky's own radiance map as a placeholder binding when there is none
549
+ * (the fog then sees a zero scale and skips it).
550
+ */ getFogSkyLightCubemap(ctx) {
551
+ const light = ctx.scene.env.light;
552
+ return light.type === 'ibl' && light.radianceMap || this.radianceMap;
553
+ }
554
+ /**
393
555
  * Light density of the sky.
394
556
  *
395
557
  * @remarks
@@ -416,6 +578,98 @@ const defaultSkyWorldMatrix = Matrix4x4.identity();
416
578
  }
417
579
  }
418
580
  /**
581
+ * How the clouds of the scatter sky are drawn.
582
+ *
583
+ * @remarks
584
+ * `volumetric` needs a WebGPU device; on other devices the procedural clouds are drawn instead,
585
+ * see {@link SkyRenderer.effectiveCloudMode}.
586
+ */ get cloudMode() {
587
+ return this._cloudMode;
588
+ }
589
+ set cloudMode(val) {
590
+ if (val !== this._cloudMode) {
591
+ this._cloudMode = val;
592
+ this.invalidate();
593
+ }
594
+ }
595
+ /** The cloud mode actually drawn: `volumetric` falls back to `procedural` off WebGPU. */ get effectiveCloudMode() {
596
+ if (this._cloudMode === 'volumetric' && getDevice().type !== 'webgpu') {
597
+ if (!SkyRenderer._volumetricFallbackWarned) {
598
+ SkyRenderer._volumetricFallbackWarned = true;
599
+ console.warn('Volumetric clouds require WebGPU, drawing procedural clouds instead');
600
+ }
601
+ return 'procedural';
602
+ }
603
+ return this._cloudMode;
604
+ }
605
+ /** Parameters of the volumetric clouds. */ get volumetricCloud() {
606
+ return this._volumetricCloud;
607
+ }
608
+ /**
609
+ * Base shape noise of the volumetric clouds, as a 2D atlas of the slices of a cubic volume,
610
+ * or null to generate it.
611
+ *
612
+ * @remarks
613
+ * An N^3 volume is stored as N slices of N x N texels laid out row by row, left to right, so
614
+ * that the atlas holds width * height = N^3 texels, e.g. 128^3 as 2048 x 1024 (16 x 8 slices).
615
+ * Channels: R Perlin-Worley, GBA Worley fbm of increasing frequency.
616
+ */ get cloudBaseNoise() {
617
+ return this._volumetricCloudResources.baseNoiseSource;
618
+ }
619
+ set cloudBaseNoise(tex) {
620
+ this._volumetricCloudResources.baseNoiseSource = tex;
621
+ this._volumetricCloudCache.invalidate();
622
+ }
623
+ /**
624
+ * Detail noise of the volumetric clouds as a slice atlas (see {@link SkyRenderer.cloudBaseNoise}),
625
+ * or null to generate it. Channels: RGB Worley fbm of increasing frequency.
626
+ */ get cloudDetailNoise() {
627
+ return this._volumetricCloudResources.detailNoiseSource;
628
+ }
629
+ set cloudDetailNoise(tex) {
630
+ this._volumetricCloudResources.detailNoiseSource = tex;
631
+ this._volumetricCloudCache.invalidate();
632
+ }
633
+ /**
634
+ * Weather map (the cloud map of Nubis 2017) of the volumetric clouds, or null to generate it.
635
+ * Channels: R coverage of the connected formation, G coverage of the isolated formation (clouds
636
+ * grow where it is highest; Coverage lowers the threshold, rather than prescribing sky area),
637
+ * B cloud type (0 stratus, 0.5 stratocumulus, 1 cumulus).
638
+ */ get cloudWeatherMap() {
639
+ return this._volumetricCloudResources.weatherMapSource;
640
+ }
641
+ set cloudWeatherMap(tex) {
642
+ this._volumetricCloudResources.weatherMapSource = tex;
643
+ this._volumetricCloudCache.invalidate();
644
+ }
645
+ /** Draws a volumetric cloud resource over the sky, for inspection. */ get cloudDebugView() {
646
+ return this._cloudDebugView;
647
+ }
648
+ set cloudDebugView(val) {
649
+ this._cloudDebugView = val;
650
+ }
651
+ /** @internal Whether {@link SkyRenderer.drawCloudDebugView} has anything to draw this frame. */ get cloudDebugViewActive() {
652
+ return this._cloudDebugView !== 'none' && this._skyType === 'scatter' && this.effectiveCloudMode === 'volumetric';
653
+ }
654
+ /**
655
+ * @internal Draws {@link SkyRenderer.cloudDebugView} over the current render target. Called on
656
+ * the presented image, after tone mapping, so the texels show their stored values.
657
+ */ drawCloudDebugView() {
658
+ drawVolumetricCloudDebug(this._volumetricCloudResources, this._cloudDebugView, this._cloudDebugSlice, this._cloudDebugChannel, this._volumetricCloudCache.densityParams);
659
+ }
660
+ /** Slice, 0 to 1: noise volume depth, density XY depth, or density XZ altitude. */ get cloudDebugSlice() {
661
+ return this._cloudDebugSlice;
662
+ }
663
+ set cloudDebugSlice(val) {
664
+ this._cloudDebugSlice = Math.min(1, Math.max(0, val));
665
+ }
666
+ /** Channel shown by {@link SkyRenderer.cloudDebugView}: 0 RGB, 1-4 R, G, B or A as grey. */ get cloudDebugChannel() {
667
+ return this._cloudDebugChannel;
668
+ }
669
+ set cloudDebugChannel(val) {
670
+ this._cloudDebugChannel = Math.min(4, Math.max(0, Math.round(val)));
671
+ }
672
+ /**
419
673
  * Wind velocity
420
674
  *
421
675
  * @remarks
@@ -556,6 +810,7 @@ const defaultSkyWorldMatrix = Matrix4x4.identity();
556
810
  return this._skyDistantLightLut.get().getColorAttachments()[0];
557
811
  }
558
812
  update(ctx) {
813
+ const wantVolumetric = this._skyType === 'scatter' && this.effectiveCloudMode === 'volumetric';
559
814
  // Latch the fog scales for this frame. The bake path (updateBakedSkyMap) uses the sky camera
560
815
  // whose scene is null, so it cannot read lightingMode itself; both bake and main pass run after
561
816
  // update(), so caching them here keeps them consistent.
@@ -567,15 +822,25 @@ const defaultSkyWorldMatrix = Matrix4x4.identity();
567
822
  // take the plain camera pre-exposure.
568
823
  // Legacy leaves both at 1 so its authored colors are untouched.
569
824
  this._fogPreExposure = SkyRenderer.getBakeToPreExposedScale(ctx);
825
+ this._physicalSunDisk = ctx.scene.lightingMode === 'physical';
826
+ // Sky light for height fog: radiance map samples times the same scale the IBL applies to them.
827
+ const envLight = ctx.scene.env.light;
828
+ const fogSkyLightMap = envLight.type === 'ibl' ? envLight.radianceMap : null;
829
+ // Same scale as ShaderHelper.getEnvLightLuminance(), which cannot be used here: it reads ctx.env,
830
+ // and update() runs before the render graph sets it.
831
+ this._heightFogParams.skyLightScale = fogSkyLightMap ? SkyRenderer.getBakeToPreExposedScale(ctx) * (envLight.strength ?? 0) : 0;
832
+ this._heightFogParams.skyLightMaxLod = fogSkyLightMap ? fogSkyLightMap.mipLevelCount - 1 : 0;
833
+ this._fogSkyLightCubemap = this.getFogSkyLightCubemap(ctx);
570
834
  this._fogLuminanceScale = ctx.scene.lightingMode === 'physical' ? ctx.scene.env.light.intensity * ShaderHelper.getPreExposure(ctx) : 1;
571
835
  const useScatter = this._skyType === 'scatter';
572
836
  if (useScatter || !atmosphereLUTRendered()) {
573
837
  this.renderAtmosphereLUTs(ctx);
574
838
  }
839
+ this._updateVolumetricClouds(ctx, wantVolumetric);
575
840
  const oldSunLight = ctx.sunLight && useScatter ? ctx.sunLight.color : null;
576
841
  const sunDir = SkyRenderer._getSunDir(ctx.sunLight);
577
842
  const sunColor = SkyRenderer._getSunColor(ctx.sunLight);
578
- if (this._skyType === 'scatter' && (this._wind.x !== 0 || this._wind.y !== 0)) {
843
+ if (this._skyType === 'scatter' && !this._volumetricActive && (this._wind.x !== 0 || this._wind.y !== 0)) {
579
844
  this._bakedSkyboxDirty = true;
580
845
  }
581
846
  if (!this._skyDistantLightLut.get()) {
@@ -593,13 +858,23 @@ const defaultSkyWorldMatrix = Matrix4x4.identity();
593
858
  this._lastSunColor.set(sunColor);
594
859
  this._bakedSkyboxDirty = true;
595
860
  }
861
+ // The scattering sky seen from the observer changes as the camera climbs or travels across the
862
+ // planet. Re-baking the IBL every frame would be far too expensive, so it waits for thresholds.
863
+ if (useScatter) {
864
+ const altitude = this._atmosphereParams.observerAltitude;
865
+ const baked = this._bakedObserverAltitude;
866
+ if (baked < 0 || Math.abs(altitude - baked) > Math.max(10, this._rebakeAltitudeRatio * baked) || Vector3.dot(this._observerUp, this._bakedObserverUp) < Math.cos(this._rebakeAngle * Math.PI / 180)) {
867
+ this._bakedObserverAltitude = altitude;
868
+ this._bakedObserverUp.set(this._observerUp);
869
+ this._bakedSkyboxDirty = true;
870
+ }
871
+ }
596
872
  if (this._bakedSkyboxDirty) {
597
873
  this._bakedSkyboxDirty = false;
598
- // updateBakedSkyMap derives the IBL (specular prefilter + irradiance SH) from the fog-free
599
- // cubemap internally, before compositing fog. The distant-light LUT is re-baked here against
600
- // the fogged cubemap to preserve the pre-refactor fog shading appearance.
874
+ // updateBakedSkyMap derives both the IBL and the distant-sky LUT from the fog-free cubemap,
875
+ // before compositing fog. Deriving the distant-sky LUT from the fogged cubemap would feed the
876
+ // fog color back into its own ambient term.
601
877
  this.updateBakedSkyMap(ctx);
602
- this.renderSkyDistantLut(ctx, this._bakedSkyboxTexture.get());
603
878
  }
604
879
  // Atmosphere extinction times cloud occlusion. The cloud term intentionally applies only to
605
880
  // the scene's directional light, not to the sky bake: the clouds in the cubemap are lit by
@@ -614,11 +889,9 @@ const defaultSkyWorldMatrix = Matrix4x4.identity();
614
889
  const cameraY = Math.min(this._heightFogParams.parameter2.y + MAX_FOG_HEIGHT, ctx.camera.getWorldPosition().y);
615
890
  const p = Math.max(-125, Math.min(126, -this._heightFogParams.parameter1.w * (cameraY - this._heightFogParams.parameter2.y)));
616
891
  this._heightFogParams.parameter3.z = this._heightFogParams.parameter2.x * Math.pow(2, p);
617
- // The legacy directional fog term expects a display-relative 0..1 light color. It cannot be
618
- // mixed directly with photometric illuminance; physical fog instead receives its radiance
619
- // from the distant-sky integration below.
620
892
  if (ctx.scene.lightingMode === 'physical') {
621
- this._heightFogParams.lightColor.setXYZ(0, 0, 0);
893
+ const sunIlluminance = ctx.sunLight ? ctx.sunLight.intensity : 0;
894
+ this._heightFogParams.lightColor.set(Vector3.scale(newSunLight, sunIlluminance * ShaderHelper.getPreExposure(ctx) / (4 * Math.PI)));
622
895
  } else {
623
896
  this._heightFogParams.lightColor.set(newSunLight);
624
897
  }
@@ -626,19 +899,78 @@ const defaultSkyWorldMatrix = Matrix4x4.identity();
626
899
  }
627
900
  return oldSunLight;
628
901
  }
902
+ /**
903
+ * Advances the volumetric cloud sky cache, after the atmosphere parameters of the frame are set.
904
+ * The sky cache only holds what an observer below the layer sees; above that the procedural
905
+ * clouds are drawn instead.
906
+ */ _updateVolumetricClouds(ctx, wanted) {
907
+ let active = wanted;
908
+ if (active) {
909
+ const altitude = this._atmosphereParams.observerAltitude;
910
+ if (altitude > this._volumetricCloud.layerBottomAltitude - VOLUMETRIC_CLOUD_CEILING_MARGIN) {
911
+ active = false;
912
+ if (!SkyRenderer._volumetricAltitudeWarned) {
913
+ SkyRenderer._volumetricAltitudeWarned = true;
914
+ console.warn('Volumetric clouds are only drawn below the cloud layer, drawing procedural clouds instead');
915
+ }
916
+ }
917
+ }
918
+ if (active) {
919
+ this._volumetricCloudResources.update(this._volumetricCloud.noiseSeed);
920
+ this._volumetricCloudCache.update(this._volumetricCloud, this._volumetricCloudResources, this._atmosphereParams, ctx.camera.getWorldPosition(), ctx.device.frameInfo.elapsedOverall * 0.001);
921
+ active = !!this._volumetricCloudCache.toMap;
922
+ if (this._volumetricCloudCache.roundCompleted) {
923
+ // The environment lighting follows the clouds once per round
924
+ this._bakedSkyboxDirty = true;
925
+ }
926
+ } else if (this._volumetricActive) {
927
+ // Resumes with a full rebuild rather than a fade from stale maps
928
+ this._volumetricCloudCache.invalidate();
929
+ }
930
+ if (active !== this._volumetricActive) {
931
+ this._volumetricActive = active;
932
+ this._bakedSkyboxDirty = true;
933
+ }
934
+ }
629
935
  renderAtmosphereLUTs(ctx) {
630
936
  this._atmosphereParams.lightDir.set(SkyRenderer._getSunDir(ctx.sunLight));
631
937
  this._atmosphereParams.lightColor.set(SkyRenderer._getSunColor(ctx.sunLight));
632
- // Physical: the sun input is photometric (lux), normalized onto the model's authored reference
633
- // and stored at the fixed PHYSICAL_BAKE_EXPOSURE rather than the live camera exposure. That
634
- // keeps the cached IBL bake exposure-independent while staying inside the environment cubemap's
635
- // limited float range -- raw luminance would overflow it to Inf. Consumers rescale to the live
636
- // exposure via getBakeToPreExposedScale().
637
938
  this._atmosphereParams.lightColor.w *= this._atmosphereExposure * (ctx.scene.lightingMode === 'physical' ? SkyRenderer.PHYSICAL_ATMOSPHERE_LUMINANCE_SCALE * SkyRenderer.PHYSICAL_BAKE_EXPOSURE : 1);
638
939
  this._atmosphereParams.cameraAspect = ctx.camera.getAspect();
639
- this._atmosphereParams.cameraWorldMatrix.set(ctx.camera.worldMatrix);
940
+ this._atmosphereParams.cameraTanHalfFovy = ctx.camera.isPerspective() ? ctx.camera.getTanHalfFovy() : 1;
941
+ const referential = this._updateObserver(ctx.camera.getWorldPosition());
942
+ Matrix4x4.multiply(referential, ctx.camera.worldMatrix, this._atmosphereParams.cameraWorldMatrix);
943
+ referential.transformVectorAffine(this._atmosphereParams.lightDir, this._atmosphereParams.lightDir);
640
944
  renderAtmosphereLUTs(this._atmosphereParams);
641
945
  }
946
+ /**
947
+ * Places the observer for the camera at `cameraPos` and returns the world to observer-local
948
+ * rotation, also stored in the atmosphere params.
949
+ */ _updateObserver(cameraPos) {
950
+ const params = this._atmosphereParams;
951
+ const scale = params.cameraHeightScale;
952
+ const R = params.plantRadius;
953
+ const px = cameraPos.x * scale;
954
+ const py = R + cameraPos.y * scale;
955
+ const pz = cameraPos.z * scale;
956
+ const len = Math.hypot(px, py, pz);
957
+ params.observerAltitude = Math.max(len - R, MIN_OBSERVER_ALTITUDE);
958
+ const up = len > 0 ? new Vector3(px / len, py / len, pz / len) : Vector3.axisPY();
959
+ this._observerUp.set(up);
960
+ // Duff et al. would do, but projecting a fixed axis keeps the identity at the origin.
961
+ const ref = Math.abs(up.x) < 0.999 ? Vector3.axisPX() : Vector3.axisPZ();
962
+ const localX = Vector3.sub(ref, Vector3.scale(up, Vector3.dot(ref, up))).inplaceNormalize();
963
+ const localZ = Vector3.cross(localX, up).inplaceNormalize();
964
+ // Rows of the world to local rotation are the local axes. Matrix4x4 is column-major: its
965
+ // setColXYZW(i, ...) writes the strided elements transformVectorAffine and shader mat * vec
966
+ // treat as row i.
967
+ const m = params.skyViewReferential;
968
+ m.setColXYZW(0, localX.x, localX.y, localX.z, 0);
969
+ m.setColXYZW(1, up.x, up.y, up.z, 0);
970
+ m.setColXYZW(2, localZ.x, localZ.y, localZ.z, 0);
971
+ m.setColXYZW(3, 0, 0, 0, 1);
972
+ return m;
973
+ }
642
974
  renderSkyDistantLut(ctx, skybox) {
643
975
  this._prepareSkyBox(ctx.device);
644
976
  ctx.device.pushDeviceStates();
@@ -680,7 +1012,7 @@ const defaultSkyWorldMatrix = Matrix4x4.identity();
680
1012
  // Direct sunlight is represented by the scene's directional sun light. Keep the analytic
681
1013
  // sun disk out of both diffuse and specular IBL to avoid baking that direct contribution a
682
1014
  // second time. Atmospheric scattering and clouds remain in the cubemap.
683
- this._renderSky(camera, false, false, this._getSkyBakeLuminanceScale(ctx));
1015
+ this._renderSky(camera, false, false, this._getSkyBakeLuminanceScale(ctx), this._lowerHemisphereIsBlack);
684
1016
  }
685
1017
  device.popDeviceStates();
686
1018
  this.renderSkyDistantLut(ctx, tex);
@@ -737,6 +1069,7 @@ const defaultSkyWorldMatrix = Matrix4x4.identity();
737
1069
  }
738
1070
  bindgroup.setTexture('skyDistantLightLut', this._skyDistantLightLut.get().getColorAttachments()[0], fetchSampler('clamp_nearest_nomip'));
739
1071
  bindgroup.setTexture('apLut', getAerialPerspectiveLut(), fetchSampler('clamp_linear_nomip'));
1072
+ bindgroup.setTexture('skyLightCubemap', this._fogSkyLightCubemap ?? this.radianceMap, fetchSampler('clamp_linear'));
740
1073
  bindgroup.setValue('rt', device.getFramebuffer() ? 1 : 0);
741
1074
  bindgroup.setValue('invProjViewMatrix', camera.invViewProjectionMatrix);
742
1075
  bindgroup.setValue('cameraNearFar', new Vector2(camera.getNearPlane(), camera.getFarPlane()));
@@ -745,7 +1078,7 @@ const defaultSkyWorldMatrix = Matrix4x4.identity();
745
1078
  bindgroup.setValue('withAerialPerspective', this.skyType === 'scatter' ? 1 : 0);
746
1079
  bindgroup.setValue('fogType', this.mappedFogType);
747
1080
  bindgroup.setValue('atmosphereParams', this._atmosphereParams);
748
- bindgroup.setValue('heightFogParams', this._getUploadHeightFogParams());
1081
+ bindgroup.setValue('heightFogParams', this.getUploadHeightFogParams());
749
1082
  device.setProgram(fogProgram);
750
1083
  device.setBindGroup(0, bindgroup);
751
1084
  device.setVertexLayout(SkyRenderer._vertexLayout);
@@ -758,7 +1091,7 @@ const defaultSkyWorldMatrix = Matrix4x4.identity();
758
1091
  * physical space when physical lighting is active, plus the pre-exposure the shader applies to its
759
1092
  * atmosphere-derived LUT samples. Legacy returns the stored params untouched so its upload stays
760
1093
  * byte-identical. Stored authored values are never mutated.
761
- */ _getUploadHeightFogParams() {
1094
+ */ getUploadHeightFogParams() {
762
1095
  const scale = this._fogLuminanceScale;
763
1096
  const p = this._heightFogParams;
764
1097
  if (scale === 1 && this._fogPreExposure === 1) {
@@ -853,6 +1186,9 @@ const defaultSkyWorldMatrix = Matrix4x4.identity();
853
1186
  this._irradianceSHFB.dispose();
854
1187
  this._irradianceFrameBuffer.dispose();
855
1188
  this._shProjector.dispose();
1189
+ this._volumetricCloudResources.dispose();
1190
+ this._volumetricCloudCache.dispose();
1191
+ this._bindgroupSkyVolumetric.dispose();
856
1192
  if (this._skyDistantLightLut.get()) {
857
1193
  this._skyDistantLightLut.get().getColorAttachments()[0].dispose();
858
1194
  this._skyDistantLightLut.dispose();
@@ -885,13 +1221,13 @@ const defaultSkyWorldMatrix = Matrix4x4.identity();
885
1221
  });
886
1222
  }
887
1223
  }
888
- /** @internal */ _renderSky(camera, depthTest, includeSunDisk, luminanceScale = 1) {
1224
+ /** @internal */ _renderSky(camera, depthTest, includeSunDisk, luminanceScale = 1, lowerHemisphereBlack = false) {
889
1225
  const device = getDevice();
890
1226
  const savedRenderStates = device.getRenderStates();
891
1227
  this._prepareSkyBox(device);
892
1228
  if (this._skyType === 'scatter') {
893
1229
  // The atmosphere LUTs already hold physical luminance, so only the caller's exposure applies.
894
- this._drawScattering(camera, depthTest, includeSunDisk, luminanceScale);
1230
+ this._drawScattering(camera, depthTest, includeSunDisk, luminanceScale, lowerHemisphereBlack);
895
1231
  } else if (this._skyType === 'skybox' && this.skyboxTexture) {
896
1232
  this._drawSkybox(camera, depthTest, luminanceScale);
897
1233
  } else {
@@ -998,7 +1334,8 @@ const defaultSkyWorldMatrix = Matrix4x4.identity();
998
1334
  * clouds at low coverage barely attenuating the sun, while full cover drops to the
999
1335
  * heavy-overcast floor {@link SkyRenderer.CLOUD_SUN_MIN_TRANSMITTANCE}.
1000
1336
  */ cloudSunTransmittance(_sunDir) {
1001
- if (this._skyType !== 'scatter') {
1337
+ // Volumetric clouds shade the sun per pixel through their shadow instead
1338
+ if (this._skyType !== 'scatter' || this._volumetricActive) {
1002
1339
  return Vector3.one();
1003
1340
  }
1004
1341
  const coverage = Math.min(1, Math.max(0, this._cloudy));
@@ -1021,25 +1358,25 @@ const defaultSkyWorldMatrix = Matrix4x4.identity();
1021
1358
  ];
1022
1359
  function rayleighSc(params, fH) {
1023
1360
  const sigma = new Vector3(RAYLEIGH_SIGMA[0] * 1e-6, RAYLEIGH_SIGMA[1] * 1e-6, RAYLEIGH_SIGMA[2] * 1e-6);
1024
- const rho_h = Math.exp(-fH / params.rayleighScatteringHeight);
1361
+ const rho_h = Math.exp(-Math.max(fH, 0) / params.rayleighScatteringHeight);
1025
1362
  return Vector3.scale(sigma, rho_h);
1026
1363
  }
1027
1364
  function mieSc(params, fH) {
1028
1365
  const sigma = new Vector3(MIE_SIGMA * 1e-6, MIE_SIGMA * 1e-6, MIE_SIGMA * 1e-6);
1029
- const rho_h = Math.exp(-(fH / params.mieScatteringHeight));
1366
+ const rho_h = Math.exp(-Math.max(fH, 0) / params.mieScatteringHeight);
1030
1367
  return Vector3.scale(sigma, rho_h);
1031
1368
  }
1032
1369
  function mieAb(params, fH) {
1033
1370
  const sigma = new Vector3(MIE_ABSORPTION_SIGMA * 1e-6, MIE_ABSORPTION_SIGMA * 1e-6, MIE_ABSORPTION_SIGMA * 1e-6);
1034
- const rho_h = Math.exp(-(fH / params.mieScatteringHeight));
1371
+ const rho_h = Math.exp(-Math.max(fH, 0) / params.mieScatteringHeight);
1035
1372
  return Vector3.scale(sigma, rho_h);
1036
1373
  }
1037
1374
  function ozoneAb(params, fH) {
1038
1375
  const sigma = new Vector3(OZONE_ABSORPTION_SIGMA[0] * 1e-6, OZONE_ABSORPTION_SIGMA[1] * 1e-6, OZONE_ABSORPTION_SIGMA[2] * 1e-6);
1039
- const rho_h = Math.max(0, 1 - Math.abs(fH - params.ozoneCenter) * 0.5 / params.ozoneWidth);
1376
+ const rho_h = Math.max(0, 1 - Math.abs(fH - params.ozoneCenter) / params.ozoneWidth);
1040
1377
  return Vector3.scale(sigma, rho_h);
1041
1378
  }
1042
- const eyePos = new Vector3(0, this._atmosphereParams.plantRadius + this._atmosphereParams.cameraHeightScale, 0);
1379
+ const eyePos = new Vector3(0, this._atmosphereParams.plantRadius + MIN_OBSERVER_ALTITUDE, 0);
1043
1380
  const lightDir = SkyRenderer._getSunDir(sunLight);
1044
1381
  const d = this._rayIntersectSphere(this._atmosphereParams.plantRadius + this._atmosphereParams.atmosphereHeight, eyePos, lightDir);
1045
1382
  if (d < 0) {
@@ -1058,27 +1395,38 @@ const defaultSkyWorldMatrix = Matrix4x4.identity();
1058
1395
  }
1059
1396
  return new Vector3(Math.exp(-sum.x), Math.exp(-sum.y), Math.exp(-sum.z));
1060
1397
  }
1061
- /** @internal */ _drawScattering(camera, depthTest, includeSunDisk, luminanceScale) {
1398
+ /** @internal */ _drawScattering(camera, depthTest, includeSunDisk, luminanceScale, lowerHemisphereBlack) {
1062
1399
  const device = getDevice();
1063
1400
  const tLut = getTransmittanceLut();
1064
1401
  const skyLut = getSkyViewLut();
1065
1402
  //const apLut = ScatteringLut.getAerialPerspectiveLut(alpha, 8000);
1066
- const program = SkyRenderer._programSky.scatter;
1067
- const bindgroup = this._bindgroupSky.scatter.get();
1403
+ const volumetric = this._volumetricActive;
1404
+ const program = volumetric ? SkyRenderer._programSkyVolumetric : SkyRenderer._programSky.scatter;
1405
+ const bindgroup = volumetric ? this._bindgroupSkyVolumetric.get() : this._bindgroupSky.scatter.get();
1406
+ if (volumetric) {
1407
+ const cache = this._volumetricCloudCache;
1408
+ bindgroup.setTexture('cloudFrom', cache.fromMap, fetchSampler('clamp_linear_nomip'));
1409
+ bindgroup.setTexture('cloudTo', cache.toMap, fetchSampler('clamp_linear_nomip'));
1410
+ bindgroup.setValue('cloudBlend', new Vector4(cache.blend, cache.fromDelta, cache.toDelta, 0));
1411
+ }
1068
1412
  bindgroup.setValue('flip', device.getFramebuffer() && device.type === 'webgpu' ? new Vector4(1, -1, 1, 1) : new Vector4(1, 1, 1, 1));
1069
1413
  bindgroup.setValue('params', this._atmosphereParams);
1070
1414
  bindgroup.setValue('viewProjMatrix', camera.viewProjectionMatrix);
1071
1415
  bindgroup.setValue('worldMatrix', this._skyWorldMatrix);
1072
1416
  bindgroup.setValue('cameraPos', camera.getWorldPosition());
1073
1417
  bindgroup.setValue('srgbOut', device.getFramebuffer() ? 0 : 1);
1074
- bindgroup.setValue('includeSunDisk', includeSunDisk ? 1 : 0);
1418
+ bindgroup.setValue('includeSunDisk', includeSunDisk ? this._physicalSunDisk ? 2 : 1 : 0);
1419
+ bindgroup.setValue('lowerHemisphereBlack', lowerHemisphereBlack ? 1 : 0);
1075
1420
  bindgroup.setValue('luminanceScale', luminanceScale);
1076
1421
  bindgroup.setTexture('tLut', tLut, fetchSampler('clamp_linear_nomip'));
1077
1422
  bindgroup.setTexture('skyLut', skyLut, fetchSampler('clamp_linear_nomip'));
1078
- bindgroup.setValue('cloudy', this._cloudy);
1079
- bindgroup.setValue('cloudIntensity', this._cloudIntensity);
1080
- bindgroup.setValue('time', device.frameInfo.elapsedOverall * 0.001);
1081
- bindgroup.setValue('velocity', this._wind);
1423
+ bindgroup.setTexture('msLut', getMultiScatteringLut(), fetchSampler('clamp_linear_nomip'));
1424
+ if (!volumetric) {
1425
+ bindgroup.setValue('cloudy', this._cloudy);
1426
+ bindgroup.setValue('cloudIntensity', this._cloudIntensity);
1427
+ bindgroup.setValue('time', device.frameInfo.elapsedOverall * 0.001);
1428
+ bindgroup.setValue('velocity', this._wind);
1429
+ }
1082
1430
  device.setProgram(program);
1083
1431
  device.setBindGroup(0, bindgroup);
1084
1432
  device.setRenderStates(depthTest ? SkyRenderer._renderStatesSky : SkyRenderer._renderStatesSkyNoDepthTest);
@@ -1182,7 +1530,13 @@ const defaultSkyWorldMatrix = Matrix4x4.identity();
1182
1530
  this._bindgroupSky.image = new DRef(device.createBindGroup(SkyRenderer._programSky.image.bindGroupLayouts[0]));
1183
1531
  }
1184
1532
  if (!SkyRenderer._programSky.scatter) {
1185
- SkyRenderer._programSky.scatter = SkyRenderer._createScatterProgram(device, true);
1533
+ SkyRenderer._programSky.scatter = SkyRenderer._createScatterProgram(device, 'procedural');
1534
+ }
1535
+ if (!SkyRenderer._programSkyVolumetric) {
1536
+ SkyRenderer._programSkyVolumetric = SkyRenderer._createScatterProgram(device, 'volumetric');
1537
+ }
1538
+ if (!this._bindgroupSkyVolumetric.get()) {
1539
+ this._bindgroupSkyVolumetric.set(device.createBindGroup(SkyRenderer._programSkyVolumetric.bindGroupLayouts[0]));
1186
1540
  }
1187
1541
  if (!this._bindgroupSky.scatter) {
1188
1542
  this._bindgroupSky.scatter = new DRef(device.createBindGroup(SkyRenderer._programSky.scatter.bindGroupLayouts[0]));
@@ -1322,6 +1676,7 @@ const defaultSkyWorldMatrix = Matrix4x4.identity();
1322
1676
  }
1323
1677
  this.apLut = pb.tex2D().uniform(0);
1324
1678
  this.skyDistantLightLut = pb.tex2D().uniform(0);
1679
+ this.skyLightCubemap = pb.texCube().uniform(0);
1325
1680
  this.invProjViewMatrix = pb.mat4().uniform(0);
1326
1681
  this.cameraNearFar = pb.vec2().uniform(0);
1327
1682
  this.cameraPosition = pb.vec3().uniform(0);
@@ -1337,7 +1692,7 @@ const defaultSkyWorldMatrix = Matrix4x4.identity();
1337
1692
  this.$l.hPos = pb.mul(this.invProjViewMatrix, this.clipSpacePos);
1338
1693
  this.$l.worldPos = pb.div(this.$l.hPos, this.$l.hPos.w).xyz;
1339
1694
  this.$l.isSky = ShaderHelper.isFarthestDepth(this, this.$l.depthValue);
1340
- this.$l.color = calculateFog(this, this.withAerialPerspective, this.fogType, this.atmosphereParams, this.heightFogParams, this.$inputs.uv, this.isSky, this.cameraPosition, this.worldPos, 0, this.apLut, this.skyDistantLightLut);
1695
+ this.$l.color = calculateFog(this, this.withAerialPerspective, this.fogType, this.atmosphereParams, this.heightFogParams, this.$inputs.uv, this.isSky, this.cameraPosition, this.worldPos, 0, this.apLut, this.skyDistantLightLut, this.skyLightCubemap);
1341
1696
  this.$if(pb.equal(this.srgbOut, 0), function() {
1342
1697
  this.$outputs.outColor = this.color;
1343
1698
  }).$else(function() {
@@ -1371,7 +1726,9 @@ const defaultSkyWorldMatrix = Matrix4x4.identity();
1371
1726
  this.APLutProgram = createAPLutProgram(device);
1372
1727
  }
1373
1728
  }
1374
- static _createScatterProgram(device, cloud) {
1729
+ static _createScatterProgram(device, cloudMode) {
1730
+ const cloud = cloudMode === 'procedural';
1731
+ const volumetric = cloudMode === 'volumetric';
1375
1732
  const program = device.buildRenderProgram({
1376
1733
  vertex (pb) {
1377
1734
  this.$inputs.pos = pb.vec3().attrib('position');
@@ -1389,8 +1746,16 @@ const defaultSkyWorldMatrix = Matrix4x4.identity();
1389
1746
  this.$outputs.outColor = pb.vec4();
1390
1747
  this.tLut = pb.tex2D().uniform(0);
1391
1748
  this.skyLut = pb.tex2D().uniform(0);
1749
+ this.msLut = pb.tex2D().uniform(0);
1392
1750
  this.params = getAtmosphereParamsStruct(pb)().uniform(0);
1393
1751
  this.includeSunDisk = pb.int().uniform(0);
1752
+ this.lowerHemisphereBlack = pb.int().uniform(0);
1753
+ if (volumetric) {
1754
+ this.cloudFrom = pb.tex2D().uniform(0);
1755
+ this.cloudTo = pb.tex2D().uniform(0);
1756
+ // (blend, delta of cloudFrom, delta of cloudTo, 0)
1757
+ this.cloudBlend = pb.vec4().uniform(0);
1758
+ }
1394
1759
  if (cloud) {
1395
1760
  this.cloudy = pb.float().uniform(0);
1396
1761
  this.cloudIntensity = pb.float().uniform(0);
@@ -1399,67 +1764,132 @@ const defaultSkyWorldMatrix = Matrix4x4.identity();
1399
1764
  }
1400
1765
  this.srgbOut = pb.int().uniform(0);
1401
1766
  this.luminanceScale = pb.float().uniform(0);
1767
+ // Six-octave fbm. `footprint` is the pixel size in p units: an octave whose features get close
1768
+ // to it is faded to its mean (0.5) instead of aliasing into speckles, e.g. seen from space.
1402
1769
  pb.func('noise', [
1403
1770
  pb.vec3('p'),
1404
- pb.float('t')
1771
+ pb.float('t'),
1772
+ pb.float('footprint')
1405
1773
  ], function() {
1406
- this.p2 = pb.mul(this.p, 0.25);
1407
- this.f = pb.mul(smoothNoise3D(this, this.p2), 0.5);
1408
- this.p2 = pb.mul(this.p2, 3.02);
1409
- this.p2.y = pb.sub(this.p2.y, pb.mul(this.t, 0.02));
1410
- this.f = pb.add(this.f, pb.mul(smoothNoise3D(this, this.p2), 0.25));
1411
- this.p2 = pb.mul(this.p2, 3.03);
1412
- this.p2.y = pb.add(this.p2.y, pb.mul(this.t, 0.01));
1413
- this.f = pb.add(this.f, pb.mul(smoothNoise3D(this, this.p2), 0.125));
1414
- this.p2 = pb.mul(this.p2, 3.02);
1415
- this.f = pb.add(this.f, pb.mul(smoothNoise3D(this, this.p2), 0.0625));
1416
- this.p2 = pb.mul(this.p2, 3.01);
1417
- this.f = pb.add(this.f, pb.mul(smoothNoise3D(this, this.p2), 0.03125));
1418
- this.p2 = pb.mul(this.p2, 3.01);
1419
- this.f = pb.add(this.f, pb.mul(smoothNoise3D(this, this.p2), 0.015625));
1774
+ this.$l.p2 = pb.mul(this.p, 0.25);
1775
+ this.$l.freq = pb.float(0.25);
1776
+ this.$l.f = pb.float(0);
1777
+ const octaves = [
1778
+ {
1779
+ mul: 1,
1780
+ weight: 0.5,
1781
+ dt: 0
1782
+ },
1783
+ {
1784
+ mul: 3.02,
1785
+ weight: 0.25,
1786
+ dt: -0.02
1787
+ },
1788
+ {
1789
+ mul: 3.03,
1790
+ weight: 0.125,
1791
+ dt: 0.01
1792
+ },
1793
+ {
1794
+ mul: 3.02,
1795
+ weight: 0.0625,
1796
+ dt: 0
1797
+ },
1798
+ {
1799
+ mul: 3.01,
1800
+ weight: 0.03125,
1801
+ dt: 0
1802
+ },
1803
+ {
1804
+ mul: 3.01,
1805
+ weight: 0.015625,
1806
+ dt: 0
1807
+ }
1808
+ ];
1809
+ for (const o of octaves){
1810
+ if (o.mul !== 1) {
1811
+ this.p2 = pb.mul(this.p2, o.mul);
1812
+ this.freq = pb.mul(this.freq, o.mul);
1813
+ }
1814
+ if (o.dt !== 0) {
1815
+ this.p2.y = pb.add(this.p2.y, pb.mul(this.t, o.dt));
1816
+ }
1817
+ this.$l.fade = pb.sub(1, pb.smoothStep(0.25, 0.75, pb.mul(this.footprint, this.freq)));
1818
+ this.f = pb.add(this.f, pb.mul(pb.mix(0.5, smoothNoise3D(this, this.p2), this.fade), o.weight));
1819
+ }
1420
1820
  this.$return(this.f);
1421
1821
  });
1422
1822
  pb.main(function() {
1423
- // Calculate sky color
1424
- this.$l.sunColor = pb.vec4();
1425
- this.$l.skyColor = skyBox(this, this.params, this.sunColor, this.$inputs.worldDirection, pb.float(0.01), this.includeSunDisk, this.tLut, this.skyLut).rgb;
1426
- this.$l.rayDir = pb.normalize(this.$inputs.worldDirection);
1823
+ // Everything below runs in the observer's local frame (up = +Y), see skyViewReferential.
1824
+ this.$l.rayDir = pb.normalize(pb.mul(this.params.skyViewReferential, pb.vec4(this.$inputs.worldDirection, 0)).xyz);
1427
1825
  this.$l.sunDir = this.params.lightDir;
1428
- // ad-hoc
1429
- this.$l.sunIntensity = pb.sqrt(pb.max(0, pb.mul(this.sunDir.y, this.rayDir.y)));
1430
- // compute cloud
1826
+ // Angular size of a pixel, taken here in uniform control flow (WGSL requires it).
1827
+ this.$l.pixelAngle = pb.length(pb.fwidth(this.rayDir));
1828
+ // The sun disk's anti-aliasing wants the pixel pitch itself; the fwidth sum above runs up
1829
+ // to twice that along diagonals, which is fine for fading noise but would blur the rim.
1830
+ this.$l.sunPixelAngle = pb.max(pb.length(pb.dpdx(this.rayDir)), pb.length(pb.dpdy(this.rayDir)));
1831
+ this.$l.sunColor = pb.vec4();
1832
+ this.$l.skyColor = skyBox(this, this.params, this.sunColor, this.rayDir, pb.float(0.01), this.includeSunDisk, this.sunPixelAngle, pb.div(SKY_OUTPUT_MAX, pb.max(this.luminanceScale, 1e-8)), this.tLut, this.skyLut, this.msLut).rgb;
1833
+ // Procedural cloud layer: a spherical shell 3 km above the ground, so it is seen from
1834
+ // below, from above and wrapped around the planet from space. Near the world origin the
1835
+ // shell point matches the flat layer this used to be.
1431
1836
  if (cloud) {
1432
- this.$l.noiseValue = pb.float();
1433
- this.$if(pb.lessThanEqual(this.rayDir.y, 0), function() {
1434
- this.noiseValue = 0;
1435
- }).$else(function() {
1436
- this.$l.tMin = pb.div(3000, this.rayDir.y);
1437
- this.$l.cloudPoint = pb.mul(this.rayDir, this.tMin);
1837
+ this.$l.eyePos = pb.vec3(0, pb.add(this.params.plantRadius, this.params.observerAltitude), 0);
1838
+ this.$l.cloudRadius = pb.add(this.params.plantRadius, 3000);
1839
+ this.$l.tCloud = rayIntersectSphere(this, pb.vec3(0), this.cloudRadius, this.eyePos, this.rayDir);
1840
+ this.$l.tGround = rayIntersectSphere(this, pb.vec3(0), this.params.plantRadius, this.eyePos, this.rayDir);
1841
+ // Below the shell the ray leaves through it unless the ground is in the way; above it the
1842
+ // shell is always hit before the ground.
1843
+ this.$l.hasCloud = pb.and(pb.greaterThan(this.tCloud, 0), pb.or(pb.greaterThanEqual(this.params.observerAltitude, 3000), pb.or(pb.lessThanEqual(this.tGround, 0), pb.lessThan(this.tCloud, this.tGround))));
1844
+ this.$l.noiseValue = pb.float(0);
1845
+ this.$l.cloudColor = pb.vec3(0);
1846
+ this.$l.vfactor = pb.float(0);
1847
+ this.$if(this.hasCloud, function() {
1848
+ this.$l.hitPos = pb.add(this.eyePos, pb.mul(this.rayDir, this.tCloud));
1849
+ this.$l.hitUp = pb.normalize(this.hitPos);
1850
+ // Anchor the clouds to the planet, not to the observer: back to the world-aligned planet
1851
+ // frame (the transpose of the rotation), relative to the planet top at the world origin.
1852
+ this.$l.planetHit = pb.mul(pb.vec4(this.hitPos, 0), this.params.skyViewReferential).xyz;
1853
+ this.$l.cloudPoint = pb.sub(this.planetHit, pb.vec3(0, this.params.plantRadius, 0));
1438
1854
  this.speed = pb.mul(pb.vec3(this.velocity.x, 0, this.velocity.y), this.time);
1439
1855
  this.$l.noiseScale = pb.float(4e-4);
1440
- this.noiseValue = this.noise(pb.mul(pb.add(this.cloudPoint, this.speed), this.noiseScale), this.time);
1856
+ this.noiseValue = this.noise(pb.mul(pb.add(this.cloudPoint, this.speed), this.noiseScale), this.time, pb.mul(this.tCloud, this.pixelAngle, this.noiseScale));
1441
1857
  this.noiseValue = pb.add(this.noiseValue, this.cloudy);
1442
1858
  this.noiseValue = pb.smoothStep(1, pb.add(1, this.cloudy), this.noiseValue);
1859
+ // Ad hoc lighting, generalized from the flat layer: sun elevation at the cloud times
1860
+ // the viewing obliquity, lit from either side. The sun reaches the cloud through the
1861
+ // atmosphere above it (planet shadowed on the night side).
1862
+ this.$l.cosView = pb.abs(pb.dot(this.rayDir, this.hitUp));
1863
+ this.$l.sunIntensity = pb.sqrt(pb.max(0, pb.mul(pb.dot(this.hitUp, this.sunDir), this.cosView)));
1864
+ this.$l.sunAtCloud = transmittanceToSky(this, this.params, this.hitPos, this.sunDir, this.tLut);
1865
+ this.cloudColor = pb.mul(this.params.lightColor.rgb, this.sunAtCloud, this.sunIntensity, pb.mul(this.noiseValue, this.cloudIntensity));
1866
+ // Fade out at grazing angles, where the layer aliases
1867
+ this.vfactor = pb.clamp(pb.div(pb.sub(this.cosView, 0.01), pb.sub(0.03, 0.01)), 0, 1);
1443
1868
  });
1444
- // use sun color as cloud color
1445
- /*
1446
- this.$l.sunColor = pb.mul(
1447
- pb.textureSampleLevel(this.skyLut, viewDirToUV(this, this.sunDir), 0),
1448
- this.sunIntensity
1449
- );
1450
- */ this.$l.cloudColor = pb.mul(this.sunColor.rgb, this.sunIntensity, pb.mul(this.noiseValue, this.cloudIntensity));
1451
- }
1452
- // Compute sky color
1453
- if (cloud) {
1454
- // blend
1455
- this.$l.vfactor = pb.clamp(pb.div(pb.sub(this.rayDir.y, 0.01), pb.sub(0.03, 0.01)), 0, 1);
1456
1869
  this.$l.factor = pb.clamp(pb.mul(this.noiseValue, this.vfactor), 0, 1);
1457
1870
  this.$l.color = pb.mix(this.skyColor, this.cloudColor, this.factor);
1871
+ } else if (volumetric) {
1872
+ // Cached clouds: (luminance, transmittance) over the sky, blended between the two
1873
+ // latest completed maps. Below -delta a map holds no clouds.
1874
+ this.$l.color = this.skyColor;
1875
+ this.$l.elevation = pb.asin(pb.clamp(this.rayDir.y, -1, 1));
1876
+ this.$l.cFrom = pb.vec4(0, 0, 0, 1);
1877
+ this.$l.cTo = pb.vec4(0, 0, 0, 1);
1878
+ this.$if(pb.greaterThanEqual(this.elevation, pb.neg(this.cloudBlend.y)), function() {
1879
+ this.cFrom = pb.textureSampleLevel(this.cloudFrom, cloudCacheEncode(this, this.rayDir, this.cloudBlend.y), 0);
1880
+ });
1881
+ this.$if(pb.greaterThanEqual(this.elevation, pb.neg(this.cloudBlend.z)), function() {
1882
+ this.cTo = pb.textureSampleLevel(this.cloudTo, cloudCacheEncode(this, this.rayDir, this.cloudBlend.z), 0);
1883
+ });
1884
+ this.$l.clouds = pb.mix(this.cFrom, this.cTo, this.cloudBlend.x);
1885
+ this.color = pb.add(pb.mul(this.color, this.clouds.a), this.clouds.rgb);
1458
1886
  } else {
1459
1887
  this.$l.color = this.skyColor;
1460
1888
  }
1461
- // 1 for legacy and for the IBL bake; the camera pre-exposure when drawn on screen.
1462
- this.color = pb.mul(this.color, this.luminanceScale);
1889
+ this.$if(pb.and(pb.notEqual(this.lowerHemisphereBlack, 0), pb.lessThan(this.rayDir.y, 0)), function() {
1890
+ this.color = pb.vec3(0);
1891
+ });
1892
+ this.color = pb.min(pb.mul(this.color, this.luminanceScale), pb.vec3(SKY_OUTPUT_MAX));
1463
1893
  this.$if(pb.equal(this.srgbOut, 0), function() {
1464
1894
  this.$outputs.outColor = pb.vec4(this.color, 1);
1465
1895
  }).$else(function() {
@@ -1468,7 +1898,7 @@ const defaultSkyWorldMatrix = Matrix4x4.identity();
1468
1898
  });
1469
1899
  }
1470
1900
  });
1471
- program.name = cloud ? '@SkyScatterCloud' : '@SkyScatter';
1901
+ program.name = cloud ? '@SkyScatterCloud' : volumetric ? '@SkyScatterVolumetricCloud' : '@SkyScatter';
1472
1902
  return program;
1473
1903
  }
1474
1904
  }