@zephyr3d/scene 0.9.26 → 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.
- package/README.md +60 -27
- package/dist/animation/animation_bank.js +634 -0
- package/dist/animation/animation_bank.js.map +1 -0
- package/dist/animation/animation_bank_gpu.js +256 -0
- package/dist/animation/animation_bank_gpu.js.map +1 -0
- package/dist/animation/animationset.js +146 -30
- package/dist/animation/animationset.js.map +1 -1
- package/dist/animation/cloth/gpu_cloth_component.js +406 -0
- package/dist/animation/cloth/gpu_cloth_component.js.map +1 -0
- package/dist/animation/cloth/gpu_cloth_system.js +332 -202
- package/dist/animation/cloth/gpu_cloth_system.js.map +1 -1
- package/dist/animation/gpu_animation_driver.js +289 -0
- package/dist/animation/gpu_animation_driver.js.map +1 -0
- package/dist/animation/joint_dynamics/collision.js +112 -1
- package/dist/animation/joint_dynamics/collision.js.map +1 -1
- package/dist/animation/joint_dynamics/controller.js +6 -0
- package/dist/animation/joint_dynamics/controller.js.map +1 -1
- package/dist/animation/joint_dynamics/joint_dynamics_system.js +11 -0
- package/dist/animation/joint_dynamics/joint_dynamics_system.js.map +1 -1
- package/dist/animation/joint_dynamics/solver.js +50 -12
- package/dist/animation/joint_dynamics/solver.js.map +1 -1
- package/dist/animation/joint_dynamics/types.js.map +1 -1
- package/dist/animation/skeleton.js +300 -56
- package/dist/animation/skeleton.js.map +1 -1
- package/dist/animation/skin_palette_atlas.js +252 -0
- package/dist/animation/skin_palette_atlas.js.map +1 -0
- package/dist/animation/spring/multi_chain_spring_system.js +298 -38
- package/dist/animation/spring/multi_chain_spring_system.js.map +1 -1
- package/dist/animation/spring/spring_chain.js +2 -0
- package/dist/animation/spring/spring_chain.js.map +1 -1
- package/dist/animation/spring/spring_collider.js +159 -21
- package/dist/animation/spring/spring_collider.js.map +1 -1
- package/dist/animation/spring/spring_particle.js +1 -0
- package/dist/animation/spring/spring_particle.js.map +1 -1
- package/dist/animation/spring/spring_solver.js +173 -0
- package/dist/animation/spring/spring_solver.js.map +1 -0
- package/dist/animation/spring/spring_system.js +249 -24
- package/dist/animation/spring/spring_system.js.map +1 -1
- package/dist/animation/spring_modifier.js +32 -1
- package/dist/animation/spring_modifier.js.map +1 -1
- package/dist/app/app.js +26 -0
- package/dist/app/app.js.map +1 -1
- package/dist/app/engine.js +86 -15
- package/dist/app/engine.js.map +1 -1
- package/dist/app/gamepad.js +191 -0
- package/dist/app/gamepad.js.map +1 -0
- package/dist/app/inputactions.js +271 -0
- package/dist/app/inputactions.js.map +1 -0
- package/dist/app/inputmgr.js +331 -22
- package/dist/app/inputmgr.js.map +1 -1
- package/dist/app/loading.js +138 -0
- package/dist/app/loading.js.map +1 -0
- package/dist/app/pipelinecache.js +209 -0
- package/dist/app/pipelinecache.js.map +1 -0
- package/dist/app/runtimescript.js +25 -0
- package/dist/app/runtimescript.js.map +1 -1
- package/dist/app/scriptingsystem.js +54 -0
- package/dist/app/scriptingsystem.js.map +1 -1
- package/dist/app/scriptregistry.js +7 -2
- package/dist/app/scriptregistry.js.map +1 -1
- package/dist/asset/asset_manifest.js +59 -0
- package/dist/asset/asset_manifest.js.map +1 -0
- package/dist/asset/asset_preload.js +258 -0
- package/dist/asset/asset_preload.js.map +1 -0
- package/dist/asset/assetmanager.js +117 -65
- package/dist/asset/assetmanager.js.map +1 -1
- package/dist/asset/loaders/ktx2/basis/LICENSE +201 -0
- package/dist/asset/loaders/ktx2/basis/basis_transcoder.js +2 -0
- package/dist/asset/loaders/ktx2/basis/basis_transcoder.wasm +0 -0
- package/dist/asset/loaders/ktx2/basis_location.js +17 -0
- package/dist/asset/loaders/ktx2/basis_location.js.map +1 -0
- package/dist/asset/loaders/ktx2/ktx2_loader.js +88 -0
- package/dist/asset/loaders/ktx2/ktx2_loader.js.map +1 -0
- package/dist/asset/loaders/ktx2/ktx2_targets.js +118 -0
- package/dist/asset/loaders/ktx2/ktx2_targets.js.map +1 -0
- package/dist/asset/loaders/ktx2/transcoder_pool.js +259 -0
- package/dist/asset/loaders/ktx2/transcoder_pool.js.map +1 -0
- package/dist/asset/mesh_lod_builder.js +104 -0
- package/dist/asset/mesh_lod_builder.js.map +1 -0
- package/dist/asset/mesh_settings.js +111 -0
- package/dist/asset/mesh_settings.js.map +1 -0
- package/dist/asset/meshopt_decoder.js +18 -0
- package/dist/asset/meshopt_decoder.js.map +1 -0
- package/dist/asset/meshopt_simplifier.js +26 -0
- package/dist/asset/meshopt_simplifier.js.map +1 -0
- package/dist/asset/model.js +177 -3
- package/dist/asset/model.js.map +1 -1
- package/dist/asset/texture_settings.js +231 -0
- package/dist/asset/texture_settings.js.map +1 -0
- package/dist/asset/zmsh_binary.js +131 -0
- package/dist/asset/zmsh_binary.js.map +1 -0
- package/dist/asset/zsdf_binary.js +111 -0
- package/dist/asset/zsdf_binary.js.map +1 -0
- package/dist/blitter/bilateralblur.js +1 -2
- package/dist/blitter/bilateralblur.js.map +1 -1
- package/dist/blitter/gaussianblur.js +1 -6
- package/dist/blitter/gaussianblur.js.map +1 -1
- package/dist/camera/base.js +31 -10
- package/dist/camera/base.js.map +1 -1
- package/dist/camera/camera.js +178 -200
- package/dist/camera/camera.js.map +1 -1
- package/dist/camera/follow.js +342 -0
- package/dist/camera/follow.js.map +1 -0
- package/dist/camera/fps.js +9 -7
- package/dist/camera/fps.js.map +1 -1
- package/dist/camera/orbit.js +46 -19
- package/dist/camera/orbit.js.map +1 -1
- package/dist/index.d.ts +30475 -19865
- package/dist/index.js +67 -12
- package/dist/index.js.map +1 -1
- package/dist/material/blinn.js +3 -0
- package/dist/material/blinn.js.map +1 -1
- package/dist/material/eye.js +1 -1
- package/dist/material/eye.js.map +1 -1
- package/dist/material/hair.js +2 -3
- package/dist/material/hair.js.map +1 -1
- package/dist/material/hairstrand.js +235 -235
- package/dist/material/hairstrand.js.map +1 -1
- package/dist/material/lambert.js +4 -1
- package/dist/material/lambert.js.map +1 -1
- package/dist/material/material.js +362 -300
- package/dist/material/material.js.map +1 -1
- package/dist/material/meshmaterial.js +118 -45
- package/dist/material/meshmaterial.js.map +1 -1
- package/dist/material/mixins/lightmodel/blinnphong.js +1 -1
- package/dist/material/mixins/lightmodel/blinnphong.js.map +1 -1
- package/dist/material/mixins/lightmodel/lambert.js +1 -1
- package/dist/material/mixins/lightmodel/lambert.js.map +1 -1
- package/dist/material/mixins/lightmodel/pbrblueprintmixin.js +34 -61
- package/dist/material/mixins/lightmodel/pbrblueprintmixin.js.map +1 -1
- package/dist/material/mixins/lightmodel/pbrmetallicroughness.js +16 -2
- package/dist/material/mixins/lightmodel/pbrmetallicroughness.js.map +1 -1
- package/dist/material/mixins/lightmodel/pbrspecularglossness.js +2 -1
- package/dist/material/mixins/lightmodel/pbrspecularglossness.js.map +1 -1
- package/dist/material/mixins/lit.js +223 -83
- package/dist/material/mixins/lit.js.map +1 -1
- package/dist/material/mixins/pbr/common.js +31 -206
- package/dist/material/mixins/pbr/common.js.map +1 -1
- package/dist/material/mtoon.js +1 -1
- package/dist/material/mtoon.js.map +1 -1
- package/dist/material/pbrmr.js +3 -0
- package/dist/material/pbrmr.js.map +1 -1
- package/dist/material/pbrsg.js +3 -0
- package/dist/material/pbrsg.js.map +1 -1
- package/dist/material/sdf_debug.js +78 -0
- package/dist/material/sdf_debug.js.map +1 -0
- package/dist/material/shader/helper.js +733 -190
- package/dist/material/shader/helper.js.map +1 -1
- package/dist/material/shader/terrain_blend.js +204 -43
- package/dist/material/shader/terrain_blend.js.map +1 -1
- package/dist/material/skin.js +331 -297
- package/dist/material/skin.js.map +1 -1
- package/dist/material/skinprofile.js +1058 -0
- package/dist/material/skinprofile.js.map +1 -0
- package/dist/material/sss.js +353 -0
- package/dist/material/sss.js.map +1 -0
- package/dist/material/sss_kernel.js +265 -0
- package/dist/material/sss_kernel.js.map +1 -0
- package/dist/material/sssprofile.js +879 -0
- package/dist/material/sssprofile.js.map +1 -0
- package/dist/material/subsurfaceprofile.js +4 -8
- package/dist/material/subsurfaceprofile.js.map +1 -1
- package/dist/material/terrain-cm.js +464 -188
- package/dist/material/terrain-cm.js.map +1 -1
- package/dist/material/unlit.js +3 -0
- package/dist/material/unlit.js.map +1 -1
- package/dist/material/water.js +2009 -201
- package/dist/material/water.js.map +1 -1
- package/dist/node_modules/meshoptimizer/LICENSE +21 -0
- package/dist/node_modules/meshoptimizer/meshopt_decoder.js +197 -0
- package/dist/node_modules/meshoptimizer/meshopt_decoder.js.map +1 -0
- package/dist/node_modules/meshoptimizer/meshopt_simplifier.js +677 -0
- package/dist/node_modules/meshoptimizer/meshopt_simplifier.js.map +1 -0
- package/dist/physics/character.js +226 -0
- package/dist/physics/character.js.map +1 -0
- package/dist/physics/collider.js +209 -0
- package/dist/physics/collider.js.map +1 -0
- package/dist/physics/events.js +79 -0
- package/dist/physics/events.js.map +1 -0
- package/dist/physics/joint.js +272 -0
- package/dist/physics/joint.js.map +1 -0
- package/dist/physics/node_physics.js +234 -0
- package/dist/physics/node_physics.js.map +1 -0
- package/dist/physics/part.js +91 -0
- package/dist/physics/part.js.map +1 -0
- package/dist/physics/rigid_body.js +379 -0
- package/dist/physics/rigid_body.js.map +1 -0
- package/dist/physics/vehicle.js +335 -0
- package/dist/physics/vehicle.js.map +1 -0
- package/dist/physics/world.js +25 -0
- package/dist/physics/world.js.map +1 -0
- package/dist/posteffect/bloom.js +49 -14
- package/dist/posteffect/bloom.js.map +1 -1
- package/dist/posteffect/motionblur.js +668 -63
- package/dist/posteffect/motionblur.js.map +1 -1
- package/dist/posteffect/posteffect.js +12 -1
- package/dist/posteffect/posteffect.js.map +1 -1
- package/dist/posteffect/postsss.js +572 -0
- package/dist/posteffect/postsss.js.map +1 -0
- package/dist/posteffect/sao.js +48 -50
- package/dist/posteffect/sao.js.map +1 -1
- package/dist/posteffect/skinsss.js +605 -355
- package/dist/posteffect/skinsss.js.map +1 -1
- package/dist/posteffect/ssgi.js +91 -162
- package/dist/posteffect/ssgi.js.map +1 -1
- package/dist/posteffect/ssr.js +3 -11
- package/dist/posteffect/ssr.js.map +1 -1
- package/dist/posteffect/sss.js +34 -15
- package/dist/posteffect/sss.js.map +1 -1
- package/dist/posteffect/taa.js +1 -0
- package/dist/posteffect/taa.js.map +1 -1
- package/dist/render/abuffer_oit.js +40 -39
- package/dist/render/abuffer_oit.js.map +1 -1
- package/dist/render/clipmap.js +328 -62
- package/dist/render/clipmap.js.map +1 -1
- package/dist/render/cluster_light.js +958 -224
- package/dist/render/cluster_light.js.map +1 -1
- package/dist/render/cull_visitor.js +26 -5
- package/dist/render/cull_visitor.js.map +1 -1
- package/dist/render/debug_views.js +37 -0
- package/dist/render/debug_views.js.map +1 -0
- package/dist/render/depthpass.js +5 -1
- package/dist/render/depthpass.js.map +1 -1
- package/dist/render/drawable_mixin.js +53 -6
- package/dist/render/drawable_mixin.js.map +1 -1
- package/dist/render/dualdepthpeeling_oit.js +53 -53
- package/dist/render/dualdepthpeeling_oit.js.map +1 -1
- package/dist/render/envlight.js +196 -113
- package/dist/render/envlight.js.map +1 -1
- package/dist/render/fbm_wavegenerator.js +8 -13
- package/dist/render/fbm_wavegenerator.js.map +1 -1
- package/dist/render/fft_wavegenerator.js +268 -136
- package/dist/render/fft_wavegenerator.js.map +1 -1
- package/dist/render/gerstner_wavegenerator.js +113 -28
- package/dist/render/gerstner_wavegenerator.js.map +1 -1
- package/dist/render/gi/probe_volume.js +1334 -0
- package/dist/render/gi/probe_volume.js.map +1 -0
- package/dist/render/gi/scene_gi.js +282 -0
- package/dist/render/gi/scene_gi.js.map +1 -0
- package/dist/render/gi/voxel_atlas.js +198 -0
- package/dist/render/gi/voxel_atlas.js.map +1 -0
- package/dist/render/gi/voxel_lighting.js +652 -0
- package/dist/render/gi/voxel_lighting.js.map +1 -0
- package/dist/render/gi/voxel_scene.js +642 -0
- package/dist/render/gi/voxel_scene.js.map +1 -0
- package/dist/render/gi/voxelize_pass.js +223 -0
- package/dist/render/gi/voxelize_pass.js.map +1 -0
- package/dist/render/globalbindgroup_allocator.js +3 -2
- package/dist/render/globalbindgroup_allocator.js.map +1 -1
- package/dist/render/gpu_picking.js +9 -36
- package/dist/render/gpu_picking.js.map +1 -1
- package/dist/render/hzb.js +80 -22
- package/dist/render/hzb.js.map +1 -1
- package/dist/render/instance_culling.js +499 -0
- package/dist/render/instance_culling.js.map +1 -0
- package/dist/render/lightpass.js +23 -9
- package/dist/render/lightpass.js.map +1 -1
- package/dist/render/lod.js +46 -0
- package/dist/render/lod.js.map +1 -0
- package/dist/render/oit.js +11 -11
- package/dist/render/oit.js.map +1 -1
- package/dist/render/primitive.js +301 -9
- package/dist/render/primitive.js.map +1 -1
- package/dist/render/primitive_readback.js +209 -0
- package/dist/render/primitive_readback.js.map +1 -0
- package/dist/render/render_queue.js +198 -11
- package/dist/render/render_queue.js.map +1 -1
- package/dist/render/renderer.js +14 -7
- package/dist/render/renderer.js.map +1 -1
- package/dist/render/rendergraph/blackboard.js +15 -1
- package/dist/render/rendergraph/blackboard.js.map +1 -1
- package/dist/render/rendergraph/executor.js +136 -0
- package/dist/render/rendergraph/executor.js.map +1 -1
- package/dist/render/rendergraph/forward_plus_builder.js +585 -59
- package/dist/render/rendergraph/forward_plus_builder.js.map +1 -1
- package/dist/render/rendergraph/frame_resource_requirements.js +1 -0
- package/dist/render/rendergraph/frame_resource_requirements.js.map +1 -1
- package/dist/render/rendergraph/history_resources.js +8 -1
- package/dist/render/rendergraph/history_resources.js.map +1 -1
- package/dist/render/rendergraph/scene_render_context.js +31 -2
- package/dist/render/rendergraph/scene_render_context.js.map +1 -1
- package/dist/render/rendergraph/types.js.map +1 -1
- package/dist/render/renderpass.js +28 -5
- package/dist/render/renderpass.js.map +1 -1
- package/dist/render/sdf/box_allocator.js +98 -0
- package/dist/render/sdf/box_allocator.js.map +1 -0
- package/dist/render/sdf/global_sdf.js +865 -0
- package/dist/render/sdf/global_sdf.js.map +1 -0
- package/dist/render/sdf/scene_sdf.js +562 -0
- package/dist/render/sdf/scene_sdf.js.map +1 -0
- package/dist/render/sdf/sdf_atlas.js +227 -0
- package/dist/render/sdf/sdf_atlas.js.map +1 -0
- package/dist/render/sdf/sdf_programs.js +42 -0
- package/dist/render/sdf/sdf_programs.js.map +1 -0
- package/dist/render/sdf/sdf_view.js +199 -0
- package/dist/render/sdf/sdf_view.js.map +1 -0
- package/dist/render/sdf/waterline.js +610 -0
- package/dist/render/sdf/waterline.js.map +1 -0
- package/dist/render/shadow_mask_pass.js +142 -25
- package/dist/render/shadow_mask_pass.js.map +1 -1
- package/dist/render/shore_wave_generator.js +1193 -0
- package/dist/render/shore_wave_generator.js.map +1 -0
- package/dist/render/sky.js +516 -86
- package/dist/render/sky.js.map +1 -1
- package/dist/render/transmission_thickness_pass.js +477 -0
- package/dist/render/transmission_thickness_pass.js.map +1 -0
- package/dist/render/underwater.js +524 -0
- package/dist/render/underwater.js.map +1 -0
- package/dist/render/virtualtexture/virtual_texture.js +1214 -0
- package/dist/render/virtualtexture/virtual_texture.js.map +1 -0
- package/dist/render/volumetric_cloud/cloud_common.js +472 -0
- package/dist/render/volumetric_cloud/cloud_common.js.map +1 -0
- package/dist/render/volumetric_cloud/cloud_noise.js +189 -0
- package/dist/render/volumetric_cloud/cloud_noise.js.map +1 -0
- package/dist/render/volumetric_cloud/debug_view.js +131 -0
- package/dist/render/volumetric_cloud/debug_view.js.map +1 -0
- package/dist/render/volumetric_cloud/resources.js +380 -0
- package/dist/render/volumetric_cloud/resources.js.map +1 -0
- package/dist/render/volumetric_cloud/sky_cache.js +380 -0
- package/dist/render/volumetric_cloud/sky_cache.js.map +1 -0
- package/dist/render/volumetric_cloud/volumetric_cloud.js +318 -0
- package/dist/render/volumetric_cloud/volumetric_cloud.js.map +1 -0
- package/dist/render/water_caustics.js +1195 -0
- package/dist/render/water_caustics.js.map +1 -0
- package/dist/render/water_foam.js +627 -0
- package/dist/render/water_foam.js.map +1 -0
- package/dist/render/water_interaction.js +1795 -0
- package/dist/render/water_interaction.js.map +1 -0
- package/dist/render/weightedblended_oit.js +36 -36
- package/dist/render/weightedblended_oit.js.map +1 -1
- package/dist/scene/basesprite.js +9 -4
- package/dist/scene/basesprite.js.map +1 -1
- package/dist/scene/batchgroup.js +108 -12
- package/dist/scene/batchgroup.js.map +1 -1
- package/dist/scene/environment.js +28 -7
- package/dist/scene/environment.js.map +1 -1
- package/dist/scene/foliage.js +1020 -0
- package/dist/scene/foliage.js.map +1 -0
- package/dist/scene/foliage_buckets.js +147 -0
- package/dist/scene/foliage_buckets.js.map +1 -0
- package/dist/scene/foliage_mask.js +358 -0
- package/dist/scene/foliage_mask.js.map +1 -0
- package/dist/scene/hair.js +15 -4
- package/dist/scene/hair.js.map +1 -1
- package/dist/scene/light.js +179 -13
- package/dist/scene/light.js.map +1 -1
- package/dist/scene/mesh.js +327 -45
- package/dist/scene/mesh.js.map +1 -1
- package/dist/scene/meshdrawable.js +6 -0
- package/dist/scene/meshdrawable.js.map +1 -1
- package/dist/scene/msdftext.js +9 -3
- package/dist/scene/msdftext.js.map +1 -1
- package/dist/scene/msdftextsprite.js +9 -3
- package/dist/scene/msdftextsprite.js.map +1 -1
- package/dist/scene/particlesys.js +8 -2
- package/dist/scene/particlesys.js.map +1 -1
- package/dist/scene/physics_settings.js +157 -0
- package/dist/scene/physics_settings.js.map +1 -0
- package/dist/scene/scene.js +190 -44
- package/dist/scene/scene.js.map +1 -1
- package/dist/scene/scene_node.js +147 -6
- package/dist/scene/scene_node.js.map +1 -1
- package/dist/scene/terrain-cm/bladegrassmaterial.js +735 -0
- package/dist/scene/terrain-cm/bladegrassmaterial.js.map +1 -0
- package/dist/scene/terrain-cm/grass.js +881 -42
- package/dist/scene/terrain-cm/grass.js.map +1 -1
- package/dist/scene/terrain-cm/grass_gpu.js +929 -0
- package/dist/scene/terrain-cm/grass_gpu.js.map +1 -0
- package/dist/scene/terrain-cm/grassmaterial.js +180 -9
- package/dist/scene/terrain-cm/grassmaterial.js.map +1 -1
- package/dist/scene/terrain-cm/terrain-cm.js +465 -36
- package/dist/scene/terrain-cm/terrain-cm.js.map +1 -1
- package/dist/scene/terrain-cm/terrain_gi_albedo.js +257 -0
- package/dist/scene/terrain-cm/terrain_gi_albedo.js.map +1 -0
- package/dist/scene/terrain-cm/terrain_rvt.js +395 -0
- package/dist/scene/terrain-cm/terrain_rvt.js.map +1 -0
- package/dist/scene/update_queue.js +54 -0
- package/dist/scene/update_queue.js.map +1 -0
- package/dist/scene/water.js +997 -96
- package/dist/scene/water.js.map +1 -1
- package/dist/scene/water_surface_sampler.js +324 -0
- package/dist/scene/water_surface_sampler.js.map +1 -0
- package/dist/scene/wind.js +317 -0
- package/dist/scene/wind.js.map +1 -0
- package/dist/shaders/atmosphere.js +388 -166
- package/dist/shaders/atmosphere.js.map +1 -1
- package/dist/shaders/fog.js +100 -32
- package/dist/shaders/fog.js.map +1 -1
- package/dist/shaders/gi.js +365 -0
- package/dist/shaders/gi.js.map +1 -0
- package/dist/shaders/ltc_rect.js +202 -0
- package/dist/shaders/ltc_rect.js.map +1 -0
- package/dist/shaders/misc.js +1 -10
- package/dist/shaders/misc.js.map +1 -1
- package/dist/shaders/noise.js +5 -1
- package/dist/shaders/noise.js.map +1 -1
- package/dist/shaders/probes.js +309 -0
- package/dist/shaders/probes.js.map +1 -0
- package/dist/shaders/sdf.js +537 -0
- package/dist/shaders/sdf.js.map +1 -0
- package/dist/shaders/shadow.js +104 -70
- package/dist/shaders/shadow.js.map +1 -1
- package/dist/shaders/skin_brdf.js +243 -0
- package/dist/shaders/skin_brdf.js.map +1 -0
- package/dist/shaders/ssr.js +6 -159
- package/dist/shaders/ssr.js.map +1 -1
- package/dist/shaders/temporal.js +38 -27
- package/dist/shaders/temporal.js.map +1 -1
- package/dist/shaders/water.js +40 -87
- package/dist/shaders/water.js.map +1 -1
- package/dist/shaders/water_medium.js +42 -0
- package/dist/shaders/water_medium.js.map +1 -0
- package/dist/shadow/dom.js +110 -19
- package/dist/shadow/dom.js.map +1 -1
- package/dist/shadow/esm.js +3 -3
- package/dist/shadow/esm.js.map +1 -1
- package/dist/shadow/pcf_opt.js +15 -48
- package/dist/shadow/pcf_opt.js.map +1 -1
- package/dist/shadow/pcf_pd.js +15 -53
- package/dist/shadow/pcf_pd.js.map +1 -1
- package/dist/shadow/pcss.js +34 -9
- package/dist/shadow/pcss.js.map +1 -1
- package/dist/shadow/shadow_impl.js +3 -8
- package/dist/shadow/shadow_impl.js.map +1 -1
- package/dist/shadow/shadow_region.js +87 -49
- package/dist/shadow/shadow_region.js.map +1 -1
- package/dist/shadow/shadowmapper.js +93 -53
- package/dist/shadow/shadowmapper.js.map +1 -1
- package/dist/shadow/ssm.js +18 -72
- package/dist/shadow/ssm.js.map +1 -1
- package/dist/shadow/vsm.js +3 -3
- package/dist/shadow/vsm.js.map +1 -1
- package/dist/shapes/shape.js +44 -1
- package/dist/shapes/shape.js.map +1 -1
- package/dist/utility/buoyancy.js +287 -0
- package/dist/utility/buoyancy.js.map +1 -0
- package/dist/utility/physical.js +10 -1
- package/dist/utility/physical.js.map +1 -1
- package/dist/utility/pmrem.js +17 -43
- package/dist/utility/pmrem.js.map +1 -1
- package/dist/utility/sdf/bake.js +435 -0
- package/dist/utility/sdf/bake.js.map +1 -0
- package/dist/utility/sdf/bvh.js +576 -0
- package/dist/utility/sdf/bvh.js.map +1 -0
- package/dist/utility/serialization/manager.js +36 -6
- package/dist/utility/serialization/manager.js.map +1 -1
- package/dist/utility/serialization/scene/animation.js +77 -7
- package/dist/utility/serialization/scene/animation.js.map +1 -1
- package/dist/utility/serialization/scene/batch.js +11 -4
- package/dist/utility/serialization/scene/batch.js.map +1 -1
- package/dist/utility/serialization/scene/camera.js +140 -279
- package/dist/utility/serialization/scene/camera.js.map +1 -1
- package/dist/utility/serialization/scene/cloth.js +25 -0
- package/dist/utility/serialization/scene/cloth.js.map +1 -0
- package/dist/utility/serialization/scene/common.js +14 -6
- package/dist/utility/serialization/scene/common.js.map +1 -1
- package/dist/utility/serialization/scene/foliage.js +821 -0
- package/dist/utility/serialization/scene/foliage.js.map +1 -0
- package/dist/utility/serialization/scene/hair.js +11 -4
- package/dist/utility/serialization/scene/hair.js.map +1 -1
- package/dist/utility/serialization/scene/light.js +115 -7
- package/dist/utility/serialization/scene/light.js.map +1 -1
- package/dist/utility/serialization/scene/material.js +388 -220
- package/dist/utility/serialization/scene/material.js.map +1 -1
- package/dist/utility/serialization/scene/mesh.js +69 -4
- package/dist/utility/serialization/scene/mesh.js.map +1 -1
- package/dist/utility/serialization/scene/node.js +140 -5
- package/dist/utility/serialization/scene/node.js.map +1 -1
- package/dist/utility/serialization/scene/node_physics.js +1318 -0
- package/dist/utility/serialization/scene/node_physics.js.map +1 -0
- package/dist/utility/serialization/scene/particle.js +11 -4
- package/dist/utility/serialization/scene/particle.js.map +1 -1
- package/dist/utility/serialization/scene/physics.js +120 -0
- package/dist/utility/serialization/scene/physics.js.map +1 -0
- package/dist/utility/serialization/scene/primitive.js +124 -10
- package/dist/utility/serialization/scene/primitive.js.map +1 -1
- package/dist/utility/serialization/scene/scene.js +1687 -51
- package/dist/utility/serialization/scene/scene.js.map +1 -1
- package/dist/utility/serialization/scene/sprite.js +11 -4
- package/dist/utility/serialization/scene/sprite.js.map +1 -1
- package/dist/utility/serialization/scene/terrain.js +163 -33
- package/dist/utility/serialization/scene/terrain.js.map +1 -1
- package/dist/utility/serialization/scene/text.js +11 -4
- package/dist/utility/serialization/scene/text.js.map +1 -1
- package/dist/utility/serialization/scene/water.js +2473 -114
- package/dist/utility/serialization/scene/water.js.map +1 -1
- package/dist/utility/serialization/types.js.map +1 -1
- package/dist/utility/shprojector.js +97 -109
- package/dist/utility/shprojector.js.map +1 -1
- package/dist/utility/textures/foam.js +170 -0
- package/dist/utility/textures/foam.js.map +1 -0
- package/dist/utility/textures/ggxlut.js +17 -43
- package/dist/utility/textures/ggxlut.js.map +1 -1
- package/dist/utility/textures/ltcdata.js +4 -4
- package/dist/utility/textures/ltcdata.js.map +1 -1
- package/dist/utility/textures/ltclut.js +16 -6
- package/dist/utility/textures/ltclut.js.map +1 -1
- package/dist/utility/unsupported.js +24 -0
- package/dist/utility/unsupported.js.map +1 -0
- package/dist/values.js +13 -1
- package/dist/values.js.map +1 -1
- package/package.json +25 -12
- package/src/asset/loaders/ktx2/basis/LICENSE +201 -0
- package/src/asset/loaders/ktx2/basis/basis_transcoder.js +2 -0
- package/src/asset/loaders/ktx2/basis/basis_transcoder.wasm +0 -0
- package/src/asset/meshoptimizer/LICENSE +21 -0
|
@@ -1,41 +1,427 @@
|
|
|
1
1
|
import { Vector4, DEPTH_CLEAR_VALUE } from '@zephyr3d/base';
|
|
2
|
-
import { MAX_SHADOW_MASK_LIGHTS } from '../values.js';
|
|
2
|
+
import { MAX_GLOBAL_LIGHTS, MAX_SHADOW_MASK_LIGHTS } from '../values.js';
|
|
3
|
+
import { PBStructTypeInfo, PBArrayTypeInfo, PBPrimitiveTypeInfo, PBPrimitiveType } from '@zephyr3d/device';
|
|
3
4
|
import { ShaderHelper } from '../material/shader/helper.js';
|
|
4
5
|
import { getDevice } from '../app/api.js';
|
|
5
6
|
|
|
7
|
+
/** Name of the index pass's per-light view-space bounding spheres. */ const UNIFORM_NAME_LIGHT_SPHERES = 'lightSpheres';
|
|
8
|
+
/**
|
|
9
|
+
* vec4s per light in the index pass's sphere buffer: the bounding sphere, then the
|
|
10
|
+
* light's brightness in x.
|
|
11
|
+
*/ const SPHERE_STRIDE = 2;
|
|
12
|
+
/** Target size in pixels of a cluster tile on screen. */ const TILE_SIZE_PX = 64;
|
|
13
|
+
/** Bound on the tile count along each screen axis, which caps the index pass cost at high resolution. */ const MAX_TILES_PER_AXIS = 64;
|
|
14
|
+
/** Number of depth slices. */ const TILE_COUNT_Z = 32;
|
|
15
|
+
/**
|
|
16
|
+
* View depth in meters where the exponential slices of a perspective camera begin.
|
|
17
|
+
* Everything nearer shares slice 0: exponential slicing would otherwise spend a quarter
|
|
18
|
+
* of the slices on the first meter in front of a 0.1 m near plane, where little is lit.
|
|
19
|
+
*/ const NEAR_SLICE_METERS = 1;
|
|
20
|
+
/** Clusters per workgroup of the WebGPU index pass, and lights per shared batch. */ const INDEX_GROUP_SIZE = 64;
|
|
21
|
+
/**
|
|
22
|
+
* Average list length per cluster the WebGPU light list starts out sized for, once there
|
|
23
|
+
* are more lights than this (with fewer, every cluster fits all of them). It then grows
|
|
24
|
+
* to what the clusters asked for, read back from the GPU.
|
|
25
|
+
*/ const LIST_LIGHTS_PER_CLUSTER = 64;
|
|
26
|
+
/** Bound on the entries of the WebGPU light list: 64 MB. */ const MAX_LIST_ENTRIES = 1 << 24;
|
|
27
|
+
/**
|
|
28
|
+
* Buckets of the per-cluster histogram of light scores that picks which lights a
|
|
29
|
+
* cluster keeps when its list is cut. Each spans a factor of 4 in score.
|
|
30
|
+
*/ const SCORE_BUCKETS = 32;
|
|
31
|
+
/**
|
|
32
|
+
* Declares the uniforms and helper functions shared by the index pass programs.
|
|
33
|
+
*/ function declareIndexPassScope(pb, scope, sphereStride, storage) {
|
|
34
|
+
(function() {
|
|
35
|
+
this.invProjMatrix = pb.mat4().uniform(0);
|
|
36
|
+
this.sizeParam = pb.vec4().uniform(0);
|
|
37
|
+
this.countParam = pb.ivec4().uniform(0);
|
|
38
|
+
this.orthoProj = pb.int().uniform(0);
|
|
39
|
+
this.sliceParam = pb.vec4().uniform(0);
|
|
40
|
+
// Already in view space: transformed once per light on the CPU rather than once
|
|
41
|
+
// per light in each of the clusters. See writeLight for the layout.
|
|
42
|
+
this[UNIFORM_NAME_LIGHT_SPHERES] = storage ? pb.vec4[0]().storageBufferReadonly(0) : pb.vec4[(ShaderHelper.getMaxClusterLights() + 1) * sphereStride]().uniformBuffer(0);
|
|
43
|
+
pb.func('lineIntersectionToZPlane', [
|
|
44
|
+
pb.vec3('a'),
|
|
45
|
+
pb.vec3('b'),
|
|
46
|
+
pb.float('zDistance')
|
|
47
|
+
], function() {
|
|
48
|
+
this.$l.normal = pb.vec3(0, 0, 1);
|
|
49
|
+
this.$l.ab = pb.sub(this.b, this.a);
|
|
50
|
+
this.$l.t = pb.div(pb.sub(this.zDistance, pb.dot(this.normal, this.a)), pb.dot(this.normal, this.ab));
|
|
51
|
+
this.$return(pb.add(this.a, pb.mul(this.t, this.ab)));
|
|
52
|
+
});
|
|
53
|
+
// View depth where perspective slice k begins: slices before the first exponential
|
|
54
|
+
// one form the near slice, which begins at the near plane.
|
|
55
|
+
pb.func('sliceDepth', [
|
|
56
|
+
pb.float('k')
|
|
57
|
+
], function() {
|
|
58
|
+
this.$if(pb.lessThan(this.k, this.sliceParam.y), function() {
|
|
59
|
+
this.$return(this.sizeParam.z);
|
|
60
|
+
});
|
|
61
|
+
this.$return(pb.mul(this.sliceParam.x, pb.pow(pb.div(this.sizeParam.w, this.sliceParam.x), pb.div(pb.sub(this.k, this.sliceParam.y), this.sliceParam.z))));
|
|
62
|
+
});
|
|
63
|
+
pb.func('clipToView', [
|
|
64
|
+
pb.vec4('clip')
|
|
65
|
+
], function() {
|
|
66
|
+
this.$l.view = pb.mul(this.invProjMatrix, this.clip);
|
|
67
|
+
this.$return(pb.div(this.view, this.view.w));
|
|
68
|
+
});
|
|
69
|
+
pb.func('screenToView', [
|
|
70
|
+
pb.vec4('screen')
|
|
71
|
+
], function() {
|
|
72
|
+
this.$l.texCoord = pb.div(this.screen.xy, this.sizeParam.xy);
|
|
73
|
+
this.$l.clip = pb.vec4(pb.sub(pb.mul(pb.vec2(this.texCoord.x, pb.sub(1, this.texCoord.y)), 2), pb.vec2(1)), this.screen.z, this.screen.w);
|
|
74
|
+
this.$return(this.clipToView(this.clip));
|
|
75
|
+
});
|
|
76
|
+
// Squared distance from a point to the box, zero inside it.
|
|
77
|
+
pb.func('aabbDistSq', [
|
|
78
|
+
pb.vec3('p'),
|
|
79
|
+
pb.vec3('aabbMin'),
|
|
80
|
+
pb.vec3('aabbMax')
|
|
81
|
+
], function() {
|
|
82
|
+
this.$l.d = pb.max(pb.max(pb.sub(this.aabbMin, this.p), pb.sub(this.p, this.aabbMax)), pb.vec3(0));
|
|
83
|
+
this.$return(pb.dot(this.d, this.d));
|
|
84
|
+
});
|
|
85
|
+
// Radius 0 marks an unbounded light that no global slot took: it reaches every cluster.
|
|
86
|
+
pb.func('sphereReachesAABB', [
|
|
87
|
+
pb.vec4('sphere'),
|
|
88
|
+
pb.float('distSq')
|
|
89
|
+
], function() {
|
|
90
|
+
this.$return(pb.or(pb.equal(this.sphere.w, 0), pb.lessThanEqual(this.distSq, pb.mul(this.sphere.w, this.sphere.w))));
|
|
91
|
+
});
|
|
92
|
+
// Whether the sphere lies entirely outside one of the tile frustum's side planes
|
|
93
|
+
// (inward unit normals through the eye). The box of a far slice is much wider than
|
|
94
|
+
// the frustum it bounds, and this rejects the lights reaching only its corners.
|
|
95
|
+
pb.func('sphereOutsideTile', [
|
|
96
|
+
pb.vec4('sphere'),
|
|
97
|
+
pb.vec3('left'),
|
|
98
|
+
pb.vec3('right'),
|
|
99
|
+
pb.vec3('bottom'),
|
|
100
|
+
pb.vec3('top')
|
|
101
|
+
], function() {
|
|
102
|
+
this.$l.r = pb.neg(this.sphere.w);
|
|
103
|
+
this.$return(pb.and(pb.greaterThan(this.sphere.w, 0), pb.or(pb.or(pb.lessThan(pb.dot(this.left, this.sphere.xyz), this.r), pb.lessThan(pb.dot(this.right, this.sphere.xyz), this.r)), pb.or(pb.lessThan(pb.dot(this.bottom, this.sphere.xyz), this.r), pb.lessThan(pb.dot(this.top, this.sphere.xyz), this.r)))));
|
|
104
|
+
});
|
|
105
|
+
}).call(scope);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Emits the view-space bounds of cluster `tileIndex` (an int): locals `aabbMin`/`aabbMax`
|
|
109
|
+
* and the inward side-plane normals `planeLeft`/`planeRight`/`planeBottom`/`planeTop`.
|
|
110
|
+
*/ function emitClusterBounds(pb, tileIndex) {
|
|
111
|
+
this.$l.tileIndex = tileIndex;
|
|
112
|
+
this.$l.tileSize = pb.div(this.sizeParam.xy, pb.vec2(this.countParam.xy));
|
|
113
|
+
this.$l.zIndex = pb.div(this.tileIndex, pb.mul(this.countParam.x, this.countParam.y));
|
|
114
|
+
this.$l.yIndex = pb.div(pb.sub(this.tileIndex, pb.mul(this.zIndex, this.countParam.x, this.countParam.y)), this.countParam.x);
|
|
115
|
+
this.$l.xIndex = pb.sub(this.tileIndex, pb.add(pb.mul(this.zIndex, this.countParam.x, this.countParam.y), pb.mul(this.yIndex, this.countParam.x)));
|
|
116
|
+
this.$l.maxPoint_sS = pb.vec4(pb.mul(pb.vec2(pb.float(pb.add(this.xIndex, 1)), pb.float(pb.add(this.yIndex, 1))), this.tileSize), 0.0, 1.0);
|
|
117
|
+
this.$l.minPoint_sS = pb.vec4(pb.mul(pb.vec2(pb.float(this.xIndex), pb.float(this.yIndex)), this.tileSize), 0.0, 1.0);
|
|
118
|
+
this.$l.maxPoint_vS = this.screenToView(this.maxPoint_sS).xyz;
|
|
119
|
+
this.$l.minPoint_vS = this.screenToView(this.minPoint_sS).xyz;
|
|
120
|
+
this.$l.sliceNear = pb.div(pb.float(this.zIndex), pb.float(this.countParam.z));
|
|
121
|
+
this.$l.sliceFar = pb.div(pb.add(pb.float(this.zIndex), 1), pb.float(this.countParam.z));
|
|
122
|
+
this.$l.aabbMin = pb.vec3();
|
|
123
|
+
this.$l.aabbMax = pb.vec3();
|
|
124
|
+
// Zero normals never reject: an orthographic tile is exactly its box.
|
|
125
|
+
this.$l.planeLeft = pb.vec3(0);
|
|
126
|
+
this.$l.planeRight = pb.vec3(0);
|
|
127
|
+
this.$l.planeBottom = pb.vec3(0);
|
|
128
|
+
this.$l.planeTop = pb.vec3(0);
|
|
129
|
+
this.$if(pb.notEqual(this.orthoProj, 0), function() {
|
|
130
|
+
// Orthographic: view rays are parallel, so a tile's view-space x/y extent does not
|
|
131
|
+
// depend on depth, and slices are linear because near may be zero or negative.
|
|
132
|
+
this.$l.tileNear = pb.neg(pb.mix(this.sizeParam.z, this.sizeParam.w, this.sliceNear));
|
|
133
|
+
this.$l.tileFar = pb.neg(pb.mix(this.sizeParam.z, this.sizeParam.w, this.sliceFar));
|
|
134
|
+
this.aabbMin = pb.vec3(pb.min(this.minPoint_vS.xy, this.maxPoint_vS.xy), this.tileFar);
|
|
135
|
+
this.aabbMax = pb.vec3(pb.max(this.minPoint_vS.xy, this.maxPoint_vS.xy), this.tileNear);
|
|
136
|
+
}).$else(function() {
|
|
137
|
+
this.$l.tileNear = pb.neg(this.sliceDepth(pb.float(this.zIndex)));
|
|
138
|
+
this.$l.tileFar = pb.neg(this.sliceDepth(pb.add(pb.float(this.zIndex), 1)));
|
|
139
|
+
this.$l.eyePos = pb.vec3(0);
|
|
140
|
+
this.$l.minPointNear = this.lineIntersectionToZPlane(this.eyePos, this.minPoint_vS, this.tileNear);
|
|
141
|
+
this.$l.minPointFar = this.lineIntersectionToZPlane(this.eyePos, this.minPoint_vS, this.tileFar);
|
|
142
|
+
this.$l.maxPointNear = this.lineIntersectionToZPlane(this.eyePos, this.maxPoint_vS, this.tileNear);
|
|
143
|
+
this.$l.maxPointFar = this.lineIntersectionToZPlane(this.eyePos, this.maxPoint_vS, this.tileFar);
|
|
144
|
+
this.aabbMin = pb.min(pb.min(this.minPointNear, this.minPointFar), pb.min(this.maxPointNear, this.maxPointFar));
|
|
145
|
+
this.aabbMax = pb.max(pb.max(this.minPointNear, this.minPointFar), pb.max(this.maxPointNear, this.maxPointFar));
|
|
146
|
+
// Side planes through the eye and the tile edges at z = tileNear (< 0), with
|
|
147
|
+
// normals facing into the tile.
|
|
148
|
+
this.$l.lo = pb.min(this.minPointNear.xy, this.maxPointNear.xy);
|
|
149
|
+
this.$l.hi = pb.max(this.minPointNear.xy, this.maxPointNear.xy);
|
|
150
|
+
this.planeLeft = pb.normalize(pb.vec3(pb.neg(this.tileNear), 0, this.lo.x));
|
|
151
|
+
this.planeRight = pb.normalize(pb.vec3(this.tileNear, 0, pb.neg(this.hi.x)));
|
|
152
|
+
this.planeBottom = pb.normalize(pb.vec3(0, pb.neg(this.tileNear), this.lo.y));
|
|
153
|
+
this.planeTop = pb.normalize(pb.vec3(0, this.tileNear, pb.neg(this.hi.y)));
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Emits the state of a cluster keeping its 16 strongest lights. Needs the bounds from
|
|
158
|
+
* emitClusterBounds.
|
|
159
|
+
*/ function emitSelectionInit(pb) {
|
|
160
|
+
this.$l.n = pb.int(0);
|
|
161
|
+
this.$l.lightIndex = [
|
|
162
|
+
pb.uint(0),
|
|
163
|
+
pb.uint(0),
|
|
164
|
+
pb.uint(0),
|
|
165
|
+
pb.uint(0),
|
|
166
|
+
pb.uint(0),
|
|
167
|
+
pb.uint(0),
|
|
168
|
+
pb.uint(0),
|
|
169
|
+
pb.uint(0),
|
|
170
|
+
pb.uint(0),
|
|
171
|
+
pb.uint(0),
|
|
172
|
+
pb.uint(0),
|
|
173
|
+
pb.uint(0),
|
|
174
|
+
pb.uint(0),
|
|
175
|
+
pb.uint(0),
|
|
176
|
+
pb.uint(0),
|
|
177
|
+
pb.uint(0)
|
|
178
|
+
];
|
|
179
|
+
this.$l.lightScore = [
|
|
180
|
+
pb.float(0),
|
|
181
|
+
pb.float(0),
|
|
182
|
+
pb.float(0),
|
|
183
|
+
pb.float(0),
|
|
184
|
+
pb.float(0),
|
|
185
|
+
pb.float(0),
|
|
186
|
+
pb.float(0),
|
|
187
|
+
pb.float(0),
|
|
188
|
+
pb.float(0),
|
|
189
|
+
pb.float(0),
|
|
190
|
+
pb.float(0),
|
|
191
|
+
pb.float(0),
|
|
192
|
+
pb.float(0),
|
|
193
|
+
pb.float(0),
|
|
194
|
+
pb.float(0),
|
|
195
|
+
pb.float(0)
|
|
196
|
+
];
|
|
197
|
+
// Once all slots are taken, the weakest one is replaced by any light scoring
|
|
198
|
+
// higher, so a crowded cluster keeps the lights that matter most to it rather
|
|
199
|
+
// than whichever happen to come first in the buffer.
|
|
200
|
+
this.$l.minSlot = pb.int(0);
|
|
201
|
+
this.$l.minScore = pb.float(0);
|
|
202
|
+
emitScoreSetup.call(this, pb);
|
|
203
|
+
}
|
|
204
|
+
/** Emits the locals emitLightScore needs. Needs the bounds from emitClusterBounds. */ function emitScoreSetup(pb) {
|
|
205
|
+
// Half the cluster diagonal: the irradiance estimate is taken at the cluster
|
|
206
|
+
// center but not closer than this, so a light inside the cluster stays finite.
|
|
207
|
+
this.$l.halfDiagSq = pb.mul(pb.dot(pb.sub(this.aabbMax, this.aabbMin), pb.sub(this.aabbMax, this.aabbMin)), 0.25);
|
|
208
|
+
this.$l.center = pb.mul(pb.add(this.aabbMin, this.aabbMax), 0.5);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Emits the local `score` of the light in `lightPos`, which emitLightReaches found to
|
|
212
|
+
* reach the cluster: its brightness times the lit shader's range window at the nearest
|
|
213
|
+
* point of the cluster, over the squared distance to the cluster center. Unbounded
|
|
214
|
+
* lights score highest.
|
|
215
|
+
*/ function emitLightScore(pb, brightness) {
|
|
216
|
+
this.$l.score = pb.float(3.0e38);
|
|
217
|
+
this.$if(pb.greaterThan(this.lightPos.w, 0), function() {
|
|
218
|
+
this.$l.f = pb.clamp(pb.sub(1, pb.div(this.distSq, pb.mul(this.lightPos.w, this.lightPos.w))), 0, 1);
|
|
219
|
+
this.$l.toCenter = pb.sub(this.center, this.lightPos.xyz);
|
|
220
|
+
this.score = pb.div(pb.mul(brightness, this.f, this.f), pb.max(pb.dot(this.toCenter, this.toCenter), pb.max(this.halfDiagSq, 1e-6)));
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Emits the test of the light at buffer index `index` (a uint) with view-space bounding
|
|
225
|
+
* sphere `sphere` and brightness `brightness` against the cluster, keeping it when it
|
|
226
|
+
* reaches the cluster and is among its 16 strongest. Must be emitted inside a loop over
|
|
227
|
+
* the lights: rejecting a light continues it.
|
|
228
|
+
*/ /**
|
|
229
|
+
* Emits whether the light whose view-space bounding sphere is in the local `lightPos`
|
|
230
|
+
* reaches the cluster, declaring the local `distSq` (its squared distance to the box).
|
|
231
|
+
* Needs the bounds from emitClusterBounds.
|
|
232
|
+
*/ function emitLightReaches(pb) {
|
|
233
|
+
this.$l.distSq = this.aabbDistSq(this.lightPos.xyz, this.aabbMin, this.aabbMax);
|
|
234
|
+
// Negative radius: shaded through a global slot, never through the clusters.
|
|
235
|
+
return pb.and(pb.greaterThanEqual(this.lightPos.w, 0), pb.and(this.sphereReachesAABB(this.lightPos, this.distSq), pb.not(this.sphereOutsideTile(this.lightPos, this.planeLeft, this.planeRight, this.planeBottom, this.planeTop))));
|
|
236
|
+
}
|
|
237
|
+
function emitConsiderLight(pb, index, sphere, brightness) {
|
|
238
|
+
this.$l.lightPos = sphere;
|
|
239
|
+
this.$if(emitLightReaches.call(this, pb), function() {
|
|
240
|
+
emitLightScore.call(this, pb, brightness);
|
|
241
|
+
this.$if(pb.lessThan(this.n, 16), function() {
|
|
242
|
+
this.lightIndex.setAt(this.n, index);
|
|
243
|
+
this.lightScore.setAt(this.n, this.score);
|
|
244
|
+
this.n = pb.add(this.n, 1);
|
|
245
|
+
}).$elseif(pb.greaterThan(this.score, this.minScore), function() {
|
|
246
|
+
this.lightIndex.setAt(this.minSlot, index);
|
|
247
|
+
this.lightScore.setAt(this.minSlot, this.score);
|
|
248
|
+
}).$else(function() {
|
|
249
|
+
this.$continue();
|
|
250
|
+
});
|
|
251
|
+
this.$if(pb.equal(this.n, 16), function() {
|
|
252
|
+
this.minSlot = 0;
|
|
253
|
+
this.minScore = this.lightScore[0];
|
|
254
|
+
this.$for(pb.int('k'), 1, 16, function() {
|
|
255
|
+
this.$if(pb.lessThan(this.lightScore.at(this.k), this.minScore), function() {
|
|
256
|
+
this.minSlot = this.k;
|
|
257
|
+
this.minScore = this.lightScore.at(this.k);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
/** Emits the packing of the selected light indices, one byte each, front to back. */ function emitPackSelection(pb) {
|
|
264
|
+
this.$l.r = pb.add(pb.sal(this.lightIndex[0], 24), pb.sal(this.lightIndex[1], 16), pb.sal(this.lightIndex[2], 8), this.lightIndex[3]);
|
|
265
|
+
this.$l.g = pb.add(pb.sal(this.lightIndex[4], 24), pb.sal(this.lightIndex[5], 16), pb.sal(this.lightIndex[6], 8), this.lightIndex[7]);
|
|
266
|
+
this.$l.b = pb.add(pb.sal(this.lightIndex[8], 24), pb.sal(this.lightIndex[9], 16), pb.sal(this.lightIndex[10], 8), this.lightIndex[11]);
|
|
267
|
+
this.$l.a = pb.add(pb.sal(this.lightIndex[12], 24), pb.sal(this.lightIndex[13], 16), pb.sal(this.lightIndex[14], 8), this.lightIndex[15]);
|
|
268
|
+
return pb.uvec4(this.r, this.g, this.b, this.a);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* The strength a light is ranked by: its peak color channel times its intensity, times
|
|
272
|
+
* the area for rect lights, whose intensity is per unit area.
|
|
273
|
+
*/ function lightBrightness(light) {
|
|
274
|
+
const c = light.diffuseAndIntensity;
|
|
275
|
+
const brightness = Math.max(c.x, c.y, c.z) * c.w;
|
|
276
|
+
return light.isRectLight() ? brightness * light.width * light.height : brightness;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Writes the world-space sphere bounding the region `light` lights into `out` (center,
|
|
280
|
+
* radius). The range sphere, except for spot lights: the smallest sphere around the cone
|
|
281
|
+
* sector they light (the cone clipped by the range sphere), which for a half-angle up to
|
|
282
|
+
* 45 degrees passes through the apex and the rim, and beyond that is centered on the
|
|
283
|
+
* rim's plane. Radius <= 0 for unbounded lights.
|
|
284
|
+
*/ function getLightBounds(light, out) {
|
|
285
|
+
const posRange = light.positionAndRange;
|
|
286
|
+
out[0] = posRange.x;
|
|
287
|
+
out[1] = posRange.y;
|
|
288
|
+
out[2] = posRange.z;
|
|
289
|
+
out[3] = posRange.w;
|
|
290
|
+
if (posRange.w > 0 && light.isSpotLight()) {
|
|
291
|
+
const dirCutoff = light.directionAndCutoff;
|
|
292
|
+
const cosAngle = Math.min(dirCutoff.w, 1);
|
|
293
|
+
if (cosAngle > 0) {
|
|
294
|
+
const len = Math.hypot(dirCutoff.x, dirCutoff.y, dirCutoff.z) || 1;
|
|
295
|
+
let dist;
|
|
296
|
+
if (cosAngle >= Math.SQRT1_2) {
|
|
297
|
+
dist = posRange.w / (2 * cosAngle);
|
|
298
|
+
out[3] = dist;
|
|
299
|
+
} else {
|
|
300
|
+
dist = posRange.w * cosAngle;
|
|
301
|
+
out[3] = posRange.w * Math.sqrt(1 - cosAngle * cosAngle);
|
|
302
|
+
}
|
|
303
|
+
out[0] += dirCutoff.x / len * dist;
|
|
304
|
+
out[1] += dirCutoff.y / len * dist;
|
|
305
|
+
out[2] += dirCutoff.z / len * dist;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const _bounds = new Float32Array(4);
|
|
6
310
|
class ClusteredLight {
|
|
7
311
|
/** Emit the shadow-mask overflow warning only once per session. */ static _warnedShadowMaskOverflow = false;
|
|
312
|
+
/** Instances returned by {@link ClusteredLight.release}, reused to keep their GPU resources. */ static _pool = [];
|
|
313
|
+
/**
|
|
314
|
+
* Fetches an instance from the pool, creating one if it is empty.
|
|
315
|
+
*
|
|
316
|
+
* @param width - Width in device pixels of the target it will be built for.
|
|
317
|
+
* @param height - Height in device pixels of that target.
|
|
318
|
+
*
|
|
319
|
+
* @remarks
|
|
320
|
+
* The GPU work reading an instance's buffers may still be pending when the render
|
|
321
|
+
* code is done with it, so release it only after the render graph has executed.
|
|
322
|
+
*/ static acquire(width, height) {
|
|
323
|
+
// Prefer an instance already sized for this target, so alternating targets do not
|
|
324
|
+
// rebuild the index texture every frame.
|
|
325
|
+
const x = ClusteredLight.tileCount(width);
|
|
326
|
+
const y = ClusteredLight.tileCount(height);
|
|
327
|
+
const index = this._pool.findIndex((cl)=>cl._tileCountX === x && cl._tileCountY === y);
|
|
328
|
+
if (index >= 0) {
|
|
329
|
+
return this._pool.splice(index, 1)[0];
|
|
330
|
+
}
|
|
331
|
+
return this._pool.pop() ?? new ClusteredLight();
|
|
332
|
+
}
|
|
333
|
+
/** Tile count along a screen axis of `size` pixels. */ static tileCount(size) {
|
|
334
|
+
return Math.min(Math.max(Math.ceil(size / TILE_SIZE_PX), 1), MAX_TILES_PER_AXIS);
|
|
335
|
+
}
|
|
336
|
+
/** Returns an instance obtained from {@link ClusteredLight.acquire} to the pool. */ static release(cl) {
|
|
337
|
+
this._pool.push(cl);
|
|
338
|
+
}
|
|
8
339
|
_tileCountX;
|
|
9
340
|
_tileCountY;
|
|
10
341
|
_tileCountZ;
|
|
11
342
|
_lights;
|
|
343
|
+
/** View-space bounding sphere of each light in `_lights`, for culling. See writeLight. */ _lightSpheres;
|
|
344
|
+
/** Buffer indices of the lights shaded outside the clusters, zero-terminated. */ _globalLights;
|
|
12
345
|
_lightIndexTexture;
|
|
13
346
|
_lightIndexFramebuffer;
|
|
14
347
|
_lightIndexProgram;
|
|
15
348
|
_bindGroup;
|
|
349
|
+
/** WebGPU only: the pass counting each cluster's lights ahead of the index pass. */ _lightCountProgram;
|
|
350
|
+
_countBindGroup;
|
|
351
|
+
/** WebGPU only: list entries the clusters asked for, as last read back from the GPU. */ _requestedListEntries;
|
|
352
|
+
_readbackPending;
|
|
353
|
+
_readback;
|
|
354
|
+
_warnedListOverflow;
|
|
16
355
|
_lightIndexVertexLayout;
|
|
356
|
+
/** Not released by disposing the layout, so kept to be disposed along with it. */ _lightIndexVertexBuffer;
|
|
17
357
|
_lightIndexRenderStates;
|
|
18
358
|
_lightBuffer;
|
|
359
|
+
_lightSphereBuffer;
|
|
360
|
+
/** Lights the WebGPU storage buffers above hold, slot 0 included. */ _storageLightCapacity;
|
|
361
|
+
/**
|
|
362
|
+
* WebGPU only: per cluster, the offset of its list in `_lightListBuffer` and its
|
|
363
|
+
* length, two uints each.
|
|
364
|
+
*/ _clusterGridBuffer;
|
|
365
|
+
/** WebGPU only: light buffer indices of every cluster's lights, one list after another. */ _lightListBuffer;
|
|
366
|
+
_lightListCapacity;
|
|
367
|
+
/** WebGPU only: the atomic allocating list room, zeroed before each build. */ _listCounterBuffer;
|
|
368
|
+
/**
|
|
369
|
+
* WebGPU only: mappable copy of `_listCounterBuffer` for readBackListRequest, kept
|
|
370
|
+
* across reads instead of a staging buffer being made for each one.
|
|
371
|
+
*/ _listCounterReadBuffer;
|
|
372
|
+
_listCounterZero;
|
|
373
|
+
_lightIndexTexSize;
|
|
19
374
|
_sizeParam;
|
|
20
375
|
_countParam;
|
|
21
376
|
_clusterParam;
|
|
377
|
+
/** Perspective depth slicing: split depth, first exponential slice, exponential slice count. */ _sliceParam;
|
|
22
378
|
_numShadowLights;
|
|
379
|
+
_orthographic;
|
|
380
|
+
/** Scratch list of the lights competing for the slots after the mask-backed ones. */ _rest;
|
|
381
|
+
/** Scratch for prioritize(). */ _sortScores;
|
|
382
|
+
_sortOrder;
|
|
383
|
+
_sortScratch;
|
|
23
384
|
constructor(){
|
|
24
|
-
this._tileCountX =
|
|
25
|
-
this._tileCountY =
|
|
26
|
-
this._tileCountZ =
|
|
27
|
-
|
|
385
|
+
this._tileCountX = 0;
|
|
386
|
+
this._tileCountY = 0;
|
|
387
|
+
this._tileCountZ = TILE_COUNT_Z;
|
|
388
|
+
// Grown to fit the frame's lights, see ensureLightArrayCapacity.
|
|
389
|
+
this._lights = new Float32Array(16 * 64);
|
|
390
|
+
this._lightSpheres = new Float32Array(4 * SPHERE_STRIDE * 64);
|
|
391
|
+
this._globalLights = new Int32Array(MAX_GLOBAL_LIGHTS);
|
|
28
392
|
this._lightIndexTexture = null;
|
|
29
393
|
this._lightIndexFramebuffer = null;
|
|
30
394
|
this._lightIndexProgram = null;
|
|
31
395
|
this._lightBuffer = null;
|
|
396
|
+
this._lightSphereBuffer = null;
|
|
397
|
+
this._storageLightCapacity = 0;
|
|
398
|
+
this._clusterGridBuffer = null;
|
|
399
|
+
this._lightListBuffer = null;
|
|
400
|
+
this._lightListCapacity = 0;
|
|
401
|
+
this._listCounterBuffer = null;
|
|
402
|
+
this._listCounterReadBuffer = null;
|
|
403
|
+
this._listCounterZero = new Uint32Array(4);
|
|
404
|
+
this._lightIndexTexSize = new Int32Array(2);
|
|
32
405
|
this._bindGroup = null;
|
|
406
|
+
this._lightCountProgram = null;
|
|
407
|
+
this._countBindGroup = null;
|
|
408
|
+
this._requestedListEntries = 0;
|
|
409
|
+
this._readbackPending = false;
|
|
410
|
+
this._readback = new Uint8Array(16);
|
|
411
|
+
this._warnedListOverflow = false;
|
|
33
412
|
this._lightIndexVertexLayout = null;
|
|
413
|
+
this._lightIndexVertexBuffer = null;
|
|
34
414
|
this._lightIndexRenderStates = null;
|
|
35
415
|
this._sizeParam = new Vector4();
|
|
36
416
|
this._countParam = new Int32Array(4);
|
|
37
417
|
this._clusterParam = new Vector4();
|
|
418
|
+
this._sliceParam = new Vector4();
|
|
38
419
|
this._numShadowLights = 0;
|
|
420
|
+
this._orthographic = false;
|
|
421
|
+
this._rest = [];
|
|
422
|
+
this._sortScores = new Float64Array(64);
|
|
423
|
+
this._sortOrder = new Uint32Array(64);
|
|
424
|
+
this._sortScratch = [];
|
|
39
425
|
}
|
|
40
426
|
get lightBuffer() {
|
|
41
427
|
return this._lightBuffer;
|
|
@@ -47,37 +433,48 @@ class ClusteredLight {
|
|
|
47
433
|
*/ get numShadowLights() {
|
|
48
434
|
return this._numShadowLights;
|
|
49
435
|
}
|
|
436
|
+
/**
|
|
437
|
+
* Whether the clusters were built for an orthographic camera, which slices depth linearly
|
|
438
|
+
* rather than exponentially. The lit shader reads it to compute the matching slice.
|
|
439
|
+
*/ get orthographic() {
|
|
440
|
+
return this._orthographic;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Buffer indices of the lights every fragment shades regardless of its cluster
|
|
444
|
+
* (directional lights, up to {@link MAX_GLOBAL_LIGHTS}), zero-terminated. Kept out of
|
|
445
|
+
* the clusters so they do not take one of the few slots in each.
|
|
446
|
+
*/ get globalLights() {
|
|
447
|
+
return this._globalLights;
|
|
448
|
+
}
|
|
50
449
|
get clusterParam() {
|
|
51
450
|
return this._clusterParam;
|
|
52
451
|
}
|
|
53
452
|
get countParam() {
|
|
54
453
|
return this._countParam;
|
|
55
454
|
}
|
|
56
|
-
get lightIndexTexture() {
|
|
455
|
+
/** The per-cluster light slots, except on WebGPU where the clusters hold lists. */ get lightIndexTexture() {
|
|
57
456
|
return this._lightIndexTexture;
|
|
58
457
|
}
|
|
458
|
+
get lightIndexTexSize() {
|
|
459
|
+
return this._lightIndexTexSize;
|
|
460
|
+
}
|
|
461
|
+
/** See {@link ShaderHelper.getClusterGrid}. WebGPU only. */ get clusterGridBuffer() {
|
|
462
|
+
return this._clusterGridBuffer;
|
|
463
|
+
}
|
|
464
|
+
/** See {@link ShaderHelper.getClusterLightList}. WebGPU only. */ get lightListBuffer() {
|
|
465
|
+
return this._lightListBuffer;
|
|
466
|
+
}
|
|
59
467
|
createVertexLayout(device, textureWidth, textureHeight) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
vertices[i * 3 + 1] = 2 * (iy + 0.5) / textureHeight - 1;
|
|
68
|
-
vertices[i * 3 + 2] = i;
|
|
69
|
-
}
|
|
70
|
-
vb = device.createVertexBuffer('position_f32x3', vertices);
|
|
71
|
-
} else {
|
|
72
|
-
const vertices = new Float32Array(this._tileCountX * this._tileCountY * this._tileCountZ * 2);
|
|
73
|
-
for(let i = 0; i < vertices.length; i++){
|
|
74
|
-
const ix = i % textureWidth;
|
|
75
|
-
const iy = Math.floor(i / textureWidth);
|
|
76
|
-
vertices[i * 2 + 0] = 2 * (ix + 0.5) / textureWidth - 1;
|
|
77
|
-
vertices[i * 2 + 1] = 2 * (iy + 0.5) / textureHeight - 1;
|
|
78
|
-
}
|
|
79
|
-
vb = device.createVertexBuffer('position_f32x2', vertices);
|
|
468
|
+
const numClusters = this._tileCountX * this._tileCountY * this._tileCountZ;
|
|
469
|
+
const vertices = new Float32Array(numClusters * 2);
|
|
470
|
+
for(let i = 0; i < numClusters; i++){
|
|
471
|
+
const ix = i % textureWidth;
|
|
472
|
+
const iy = Math.floor(i / textureWidth);
|
|
473
|
+
vertices[i * 2 + 0] = 2 * (ix + 0.5) / textureWidth - 1;
|
|
474
|
+
vertices[i * 2 + 1] = 2 * (iy + 0.5) / textureHeight - 1;
|
|
80
475
|
}
|
|
476
|
+
const vb = device.createVertexBuffer('position_f32x2', vertices);
|
|
477
|
+
this._lightIndexVertexBuffer = vb;
|
|
81
478
|
this._lightIndexVertexLayout = device.createVertexLayout({
|
|
82
479
|
vertexBuffers: [
|
|
83
480
|
{
|
|
@@ -86,218 +483,416 @@ class ClusteredLight {
|
|
|
86
483
|
]
|
|
87
484
|
});
|
|
88
485
|
}
|
|
486
|
+
/** Sizes the grid for a target, dropping the resources built for another grid. */ resizeGrid(width, height) {
|
|
487
|
+
const x = ClusteredLight.tileCount(width);
|
|
488
|
+
const y = ClusteredLight.tileCount(height);
|
|
489
|
+
if (x === this._tileCountX && y === this._tileCountY) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
this._tileCountX = x;
|
|
493
|
+
this._tileCountY = y;
|
|
494
|
+
this._lightIndexFramebuffer?.dispose();
|
|
495
|
+
this._lightIndexFramebuffer = null;
|
|
496
|
+
this._lightIndexTexture?.dispose();
|
|
497
|
+
this._lightIndexTexture = null;
|
|
498
|
+
this._lightIndexVertexLayout?.dispose();
|
|
499
|
+
this._lightIndexVertexLayout = null;
|
|
500
|
+
this._lightIndexVertexBuffer?.dispose();
|
|
501
|
+
this._lightIndexVertexBuffer = null;
|
|
502
|
+
this._clusterGridBuffer?.dispose();
|
|
503
|
+
this._clusterGridBuffer = null;
|
|
504
|
+
this._lightListBuffer?.dispose();
|
|
505
|
+
this._lightListBuffer = null;
|
|
506
|
+
this._lightListCapacity = 0;
|
|
507
|
+
}
|
|
89
508
|
createRenderState(device) {
|
|
90
509
|
this._lightIndexRenderStates = device.createRenderStateSet();
|
|
91
510
|
this._lightIndexRenderStates.useDepthState().enableTest(false).enableWrite(false);
|
|
92
511
|
this._lightIndexRenderStates.useRasterizerState().setCullMode('none');
|
|
93
512
|
}
|
|
94
513
|
createProgram(device) {
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
this
|
|
103
|
-
this.
|
|
104
|
-
this
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
pb.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
this.$l.
|
|
125
|
-
this.$l.
|
|
126
|
-
this.$
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
pb.vec4('sphere'),
|
|
130
|
-
pb.vec3('aabbMin'),
|
|
131
|
-
pb.vec3('aabbMax')
|
|
132
|
-
], function() {
|
|
133
|
-
this.$l.dmin = pb.float(0);
|
|
134
|
-
this.$if(pb.lessThanEqual(this.sphere.w, 0), function() {
|
|
135
|
-
this.$return(true);
|
|
514
|
+
const sphereStride = SPHERE_STRIDE;
|
|
515
|
+
if (device.type === 'webgpu') {
|
|
516
|
+
// Declares what both passes use and returns the batched light loop: runs `body`
|
|
517
|
+
// for each light of each batch the workgroup loads, with the light's index in
|
|
518
|
+
// `lightId`, its sphere in `lightPos` and its brightness in `brightness`, for the
|
|
519
|
+
// threads where `test` holds. Also emits the cluster bounds and the locals below.
|
|
520
|
+
const setup = function() {
|
|
521
|
+
declareIndexPassScope(pb0, this, sphereStride, true);
|
|
522
|
+
this.clusterGrid = pb0.uint[0]().storageBuffer(0);
|
|
523
|
+
this.listCounter = pb0.atomic_uint[0]().storageBuffer(0);
|
|
524
|
+
// One batch of lights, loaded by the whole workgroup and then tested by each of
|
|
525
|
+
// its clusters, instead of every cluster reading every light on its own.
|
|
526
|
+
this.batchSpheres = pb0.vec4[INDEX_GROUP_SIZE]().workgroup();
|
|
527
|
+
this.batchBrightness = pb0.float[INDEX_GROUP_SIZE]().workgroup();
|
|
528
|
+
};
|
|
529
|
+
let pb0 = null;
|
|
530
|
+
const emitMainSetup = function(pb) {
|
|
531
|
+
this.$l.numClusters = pb.mul(this.countParam.x, this.countParam.y, this.countParam.z);
|
|
532
|
+
this.$l.cluster = pb.int(this.$builtins.globalInvocationId.x);
|
|
533
|
+
// Threads past the last cluster still take part in loading the batches.
|
|
534
|
+
this.$l.valid = pb.lessThan(this.cluster, this.numClusters);
|
|
535
|
+
emitClusterBounds.call(this, pb, pb.min(this.cluster, pb.sub(this.numClusters, 1)));
|
|
536
|
+
this.$l.cell = pb.mul(pb.uint(pb.min(this.cluster, pb.sub(this.numClusters, 1))), 2);
|
|
537
|
+
this.$l.local = pb.uint(this.$builtins.localInvocationId.x);
|
|
538
|
+
this.$l.numLights = pb.uint(pb.sub(this.countParam.w, 1));
|
|
539
|
+
this.$l.numBatches = pb.div(pb.add(this.numLights, INDEX_GROUP_SIZE - 1), INDEX_GROUP_SIZE);
|
|
540
|
+
};
|
|
541
|
+
const forEachBatchedLight = function(pb, test, body) {
|
|
542
|
+
this.$for(pb.uint('b'), 0, this.numBatches, function() {
|
|
543
|
+
this.$l.base = pb.add(pb.mul(this.b, INDEX_GROUP_SIZE), 1);
|
|
544
|
+
this.$l.li = pb.add(this.base, this.local);
|
|
545
|
+
this.$if(pb.lessThan(this.li, pb.uint(this.countParam.w)), function() {
|
|
546
|
+
this.batchSpheres.setAt(this.local, this[UNIFORM_NAME_LIGHT_SPHERES].at(pb.mul(this.li, 2)));
|
|
547
|
+
this.batchBrightness.setAt(this.local, this[UNIFORM_NAME_LIGHT_SPHERES].at(pb.add(pb.mul(this.li, 2), 1)).x);
|
|
136
548
|
});
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
this.$l.
|
|
143
|
-
this.
|
|
549
|
+
pb.workgroupBarrier();
|
|
550
|
+
this.$if(test.call(this), function() {
|
|
551
|
+
this.$l.batchCount = pb.min(pb.sub(pb.uint(this.countParam.w), this.base), INDEX_GROUP_SIZE);
|
|
552
|
+
this.$for(pb.uint('j'), 0, this.batchCount, function() {
|
|
553
|
+
this.$l.lightId = pb.add(this.base, this.j);
|
|
554
|
+
this.$l.lightPos = this.batchSpheres.at(this.j);
|
|
555
|
+
this.$l.brightness = this.batchBrightness.at(this.j);
|
|
556
|
+
body.call(this);
|
|
144
557
|
});
|
|
145
558
|
});
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
});
|
|
149
|
-
this.$return(false);
|
|
559
|
+
// The next batch overwrites the one still being read.
|
|
560
|
+
pb.workgroupBarrier();
|
|
150
561
|
});
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
562
|
+
};
|
|
563
|
+
// Count pass: how many lights reach each cluster, and in total. The total decides
|
|
564
|
+
// in the write pass whether the lists fit, which no single pass could know before
|
|
565
|
+
// every cluster had allocated its list.
|
|
566
|
+
this._lightCountProgram = device.buildComputeProgram({
|
|
567
|
+
label: 'ClusteredLightCount',
|
|
568
|
+
workgroupSize: [
|
|
569
|
+
INDEX_GROUP_SIZE,
|
|
570
|
+
1,
|
|
571
|
+
1
|
|
572
|
+
],
|
|
573
|
+
compute (pb) {
|
|
574
|
+
pb0 = pb;
|
|
575
|
+
setup.call(this);
|
|
576
|
+
pb.main(function() {
|
|
577
|
+
emitMainSetup.call(this, pb);
|
|
578
|
+
this.$l.listCount = pb.uint(0);
|
|
579
|
+
forEachBatchedLight.call(this, pb, ()=>this.valid, function() {
|
|
580
|
+
this.$if(emitLightReaches.call(this, pb), function() {
|
|
581
|
+
this.listCount = pb.add(this.listCount, 1);
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
this.$if(this.valid, function() {
|
|
585
|
+
this.clusterGrid.setAt(pb.add(this.cell, 1), this.listCount);
|
|
586
|
+
pb.atomicAdd(this.listCounter.at(1), this.listCount);
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
this._lightCountProgram.name = '@ClusteredLight_Count';
|
|
592
|
+
this._countBindGroup = device.createBindGroup(this._lightCountProgram.bindGroupLayouts[0]);
|
|
593
|
+
// Write pass: allocates each cluster's list and fills it.
|
|
594
|
+
this._lightIndexProgram = device.buildComputeProgram({
|
|
595
|
+
label: 'ClusteredLightIndex',
|
|
596
|
+
workgroupSize: [
|
|
597
|
+
INDEX_GROUP_SIZE,
|
|
598
|
+
1,
|
|
599
|
+
1
|
|
600
|
+
],
|
|
601
|
+
compute (pb) {
|
|
602
|
+
pb0 = pb;
|
|
603
|
+
setup.call(this);
|
|
604
|
+
this.listCapacity = pb.uint().uniform(0);
|
|
605
|
+
this.lightList = pb.uint[0]().storageBuffer(0);
|
|
606
|
+
// The bucket of a score, higher for stronger lights.
|
|
607
|
+
pb.func('scoreBucket', [
|
|
608
|
+
pb.float('score')
|
|
609
|
+
], function() {
|
|
610
|
+
this.$return(pb.uint(pb.clamp(pb.add(pb.floor(pb.mul(pb.log2(pb.max(this.score, 1e-30)), 0.5)), SCORE_BUCKETS / 2), 0, SCORE_BUCKETS - 1)));
|
|
611
|
+
});
|
|
612
|
+
pb.main(function() {
|
|
613
|
+
emitMainSetup.call(this, pb);
|
|
614
|
+
emitScoreSetup.call(this, pb);
|
|
615
|
+
this.$l.need = pb.uint(0);
|
|
616
|
+
this.$if(this.valid, function() {
|
|
617
|
+
this.need = this.clusterGrid.at(pb.add(this.cell, 1));
|
|
618
|
+
});
|
|
619
|
+
// When the lists do not all fit, every cluster gives up the same share of its
|
|
620
|
+
// lights, so the sum fits and none is left empty. The margin absorbs the
|
|
621
|
+
// rounding of the float scale.
|
|
622
|
+
this.$l.total = pb.atomicLoad(this.listCounter.at(1));
|
|
623
|
+
this.$l.allowed = this.need;
|
|
624
|
+
this.$if(pb.greaterThan(this.total, this.listCapacity), function() {
|
|
625
|
+
this.allowed = pb.uint(pb.floor(pb.mul(pb.float(this.need), pb.div(pb.float(this.listCapacity), pb.float(this.total)), 0.999)));
|
|
626
|
+
});
|
|
627
|
+
this.$l.truncated = pb.lessThan(this.allowed, this.need);
|
|
628
|
+
// A cut list keeps the strongest lights for this cluster: the buckets above
|
|
629
|
+
// `cutBucket` whole, and of that bucket only the first `cutRoom`.
|
|
630
|
+
this.$l.hist = pb.uint[SCORE_BUCKETS]();
|
|
631
|
+
this.$for(pb.int('h'), 0, SCORE_BUCKETS, function() {
|
|
632
|
+
this.hist.setAt(this.h, 0);
|
|
633
|
+
});
|
|
634
|
+
forEachBatchedLight.call(this, pb, ()=>pb.and(this.valid, this.truncated), function() {
|
|
635
|
+
this.$if(emitLightReaches.call(this, pb), function() {
|
|
636
|
+
emitLightScore.call(this, pb, this.brightness);
|
|
637
|
+
this.$l.bucket = this.scoreBucket(this.score);
|
|
638
|
+
this.hist.setAt(this.bucket, pb.add(this.hist.at(this.bucket), 1));
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
this.$l.cutBucket = pb.int(-1);
|
|
642
|
+
this.$l.cutRoom = pb.uint(0);
|
|
643
|
+
this.$if(this.truncated, function() {
|
|
644
|
+
this.$l.kept = pb.uint(0);
|
|
645
|
+
this.$for(pb.int('h'), SCORE_BUCKETS - 1, 0, false, true, function() {
|
|
646
|
+
this.$if(pb.greaterThan(pb.add(this.kept, this.hist.at(this.h)), this.allowed), function() {
|
|
647
|
+
this.cutBucket = this.h;
|
|
648
|
+
this.cutRoom = pb.sub(this.allowed, this.kept);
|
|
649
|
+
this.$break();
|
|
650
|
+
});
|
|
651
|
+
this.kept = pb.add(this.kept, this.hist.at(this.h));
|
|
652
|
+
});
|
|
182
653
|
});
|
|
183
|
-
this.$
|
|
184
|
-
|
|
654
|
+
this.$l.listOffset = pb.uint(0);
|
|
655
|
+
this.$if(pb.and(this.valid, pb.greaterThan(this.allowed, 0)), function() {
|
|
656
|
+
this.listOffset = pb.atomicAdd(this.listCounter.at(0), this.allowed);
|
|
657
|
+
// Only rounding can leave the sum above the capacity; trim the last lists.
|
|
658
|
+
this.allowed = pb.select(pb.uint(0), pb.min(this.allowed, pb.sub(this.listCapacity, this.listOffset)), pb.lessThan(this.listOffset, this.listCapacity));
|
|
659
|
+
});
|
|
660
|
+
this.$l.written = pb.uint(0);
|
|
661
|
+
forEachBatchedLight.call(this, pb, ()=>pb.and(this.valid, pb.lessThan(this.written, this.allowed)), function() {
|
|
662
|
+
this.$if(pb.greaterThanEqual(this.written, this.allowed), function() {
|
|
185
663
|
this.$break();
|
|
186
664
|
});
|
|
187
|
-
this.$
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
this
|
|
193
|
-
|
|
194
|
-
this.
|
|
195
|
-
this
|
|
665
|
+
this.$if(emitLightReaches.call(this, pb), function() {
|
|
666
|
+
this.$l.keep = true;
|
|
667
|
+
this.$if(this.truncated, function() {
|
|
668
|
+
emitLightScore.call(this, pb, this.brightness);
|
|
669
|
+
this.$l.bucket = pb.int(this.scoreBucket(this.score));
|
|
670
|
+
this.keep = pb.greaterThan(this.bucket, this.cutBucket);
|
|
671
|
+
this.$if(pb.and(pb.equal(this.bucket, this.cutBucket), pb.greaterThan(this.cutRoom, 0)), function() {
|
|
672
|
+
this.keep = true;
|
|
673
|
+
this.cutRoom = pb.sub(this.cutRoom, 1);
|
|
196
674
|
});
|
|
197
675
|
});
|
|
198
|
-
this.$if(
|
|
199
|
-
this
|
|
676
|
+
this.$if(this.keep, function() {
|
|
677
|
+
this.lightList.setAt(pb.add(this.listOffset, this.written), this.lightId);
|
|
678
|
+
this.written = pb.add(this.written, 1);
|
|
200
679
|
});
|
|
201
680
|
});
|
|
202
681
|
});
|
|
203
|
-
this.$
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
pb.uint(0),
|
|
222
|
-
pb.uint(0),
|
|
223
|
-
pb.uint(0),
|
|
224
|
-
pb.uint(0)
|
|
225
|
-
];
|
|
682
|
+
this.$if(this.valid, function() {
|
|
683
|
+
this.clusterGrid.setAt(this.cell, this.listOffset);
|
|
684
|
+
this.clusterGrid.setAt(pb.add(this.cell, 1), this.written);
|
|
685
|
+
});
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
} else {
|
|
690
|
+
this._lightIndexProgram = device.buildRenderProgram({
|
|
691
|
+
vertex (pb) {
|
|
692
|
+
this.$inputs.pos = pb.vec2().attrib('position');
|
|
693
|
+
this.$outputs.value = pb.uvec4();
|
|
694
|
+
declareIndexPassScope(pb, this, sphereStride, false);
|
|
695
|
+
pb.main(function() {
|
|
696
|
+
this.$builtins.pointSize = 1;
|
|
697
|
+
this.$builtins.position = pb.vec4(this.$inputs.pos.xy, 0, 1);
|
|
698
|
+
emitClusterBounds.call(this, pb, pb.int(this.$builtins.vertexIndex));
|
|
699
|
+
emitSelectionInit.call(this, pb);
|
|
226
700
|
this.$for(pb.uint('i'), 1, pb.uint(this.countParam.w), function() {
|
|
227
|
-
this
|
|
228
|
-
this.$l.lightPos = pb.mul(this.viewMatrix, pb.vec4(this.light.xyz, 1));
|
|
229
|
-
this.$l.lightPos.w = this.light.w;
|
|
230
|
-
this.$if(this.sphereIntersectsAABB(this.lightPos, this.aabbMin, this.aabbMax), function() {
|
|
231
|
-
this.lightIndex.setAt(this.n, this.i);
|
|
232
|
-
this.n = pb.add(this.n, 1);
|
|
233
|
-
this.$if(pb.equal(this.n, 16), function() {
|
|
234
|
-
this.$break();
|
|
235
|
-
});
|
|
236
|
-
});
|
|
701
|
+
emitConsiderLight.call(this, pb, this.i, this[UNIFORM_NAME_LIGHT_SPHERES].at(pb.mul(this.i, sphereStride)), this[UNIFORM_NAME_LIGHT_SPHERES].at(pb.add(pb.mul(this.i, sphereStride), 1)).x);
|
|
237
702
|
});
|
|
238
|
-
this.$
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
this.$outputs.color = this.$inputs.value;
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
});
|
|
703
|
+
this.$outputs.value = emitPackSelection.call(this, pb);
|
|
704
|
+
});
|
|
705
|
+
},
|
|
706
|
+
fragment (pb) {
|
|
707
|
+
this.$outputs.color = pb.uvec4();
|
|
708
|
+
pb.main(function() {
|
|
709
|
+
this.$outputs.color = this.$inputs.value;
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
}
|
|
253
714
|
this._lightIndexProgram.name = '@ClusteredLight_Index';
|
|
254
715
|
this._bindGroup = device.createBindGroup(this._lightIndexProgram.bindGroupLayouts[0]);
|
|
716
|
+
if (!ShaderHelper.usesClusterLightLists()) {
|
|
717
|
+
this._lightSphereBuffer?.dispose();
|
|
718
|
+
const sphereBufferType = this._lightIndexProgram.getBindingInfo(UNIFORM_NAME_LIGHT_SPHERES).type;
|
|
719
|
+
this._lightSphereBuffer = device.createStructuredBuffer(sphereBufferType, {
|
|
720
|
+
usage: 'uniform'
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Grows the WebGPU light and sphere storage buffers to hold `count` entries (slot 0
|
|
726
|
+
* included). The contents are rewritten every frame, so nothing is copied.
|
|
727
|
+
*/ ensureStorageLightCapacity(device, count) {
|
|
728
|
+
if (this._storageLightCapacity >= count && this._lightBuffer && this._lightSphereBuffer) {
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
const capacity = Math.max(64, 1 << Math.ceil(Math.log2(count)));
|
|
255
732
|
this._lightBuffer?.dispose();
|
|
256
|
-
|
|
733
|
+
this._lightSphereBuffer?.dispose();
|
|
734
|
+
this._lightBuffer = device.createBuffer(capacity * 64, {
|
|
735
|
+
usage: 'uniform',
|
|
736
|
+
storage: true
|
|
737
|
+
});
|
|
738
|
+
this._lightSphereBuffer = device.createBuffer(capacity * 16 * SPHERE_STRIDE, {
|
|
739
|
+
usage: 'uniform',
|
|
740
|
+
storage: true
|
|
741
|
+
});
|
|
742
|
+
this._storageLightCapacity = capacity;
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Makes the WebGPU cluster grid and light list buffers fit the current grid and
|
|
746
|
+
* `numLights` lights. The list grows but is not shrunk.
|
|
747
|
+
*/ ensureListBuffers(device, numLights) {
|
|
748
|
+
const numClusters = this._tileCountX * this._tileCountY * this._tileCountZ;
|
|
749
|
+
if (!this._clusterGridBuffer) {
|
|
750
|
+
this._clusterGridBuffer = device.createBuffer(numClusters * 8, {
|
|
751
|
+
usage: 'uniform',
|
|
752
|
+
storage: true
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
const capacity = Math.min(Math.max(numClusters * Math.min(Math.max(numLights, 1), LIST_LIGHTS_PER_CLUSTER), // With some headroom, as moving lights change what the clusters ask for.
|
|
756
|
+
Math.ceil(this._requestedListEntries * 1.25)), MAX_LIST_ENTRIES);
|
|
757
|
+
if (!this._lightListBuffer || this._lightListCapacity < capacity) {
|
|
758
|
+
this._lightListBuffer?.dispose();
|
|
759
|
+
this._lightListBuffer = device.createBuffer(capacity * 4, {
|
|
760
|
+
usage: 'uniform',
|
|
761
|
+
storage: true
|
|
762
|
+
});
|
|
763
|
+
this._lightListCapacity = capacity;
|
|
764
|
+
}
|
|
765
|
+
if (!this._listCounterBuffer) {
|
|
766
|
+
this._listCounterBuffer = device.createBuffer(16, {
|
|
767
|
+
usage: 'uniform',
|
|
768
|
+
storage: true
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
if (!this._listCounterReadBuffer) {
|
|
772
|
+
this._listCounterReadBuffer = device.createBuffer(16, {
|
|
773
|
+
usage: 'read'
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Reads back how many list entries the clusters asked for, so the next builds can
|
|
779
|
+
* size the list to fit. One read is in flight at a time.
|
|
780
|
+
*/ readBackListRequest() {
|
|
781
|
+
if (this._readbackPending) {
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
this._readbackPending = true;
|
|
785
|
+
// The read buffer is copied into only while no read is pending, so a copy never
|
|
786
|
+
// lands in it while it is being mapped.
|
|
787
|
+
getDevice().copyBuffer(this._listCounterBuffer, this._listCounterReadBuffer, 0, 0, 16);
|
|
788
|
+
this._listCounterReadBuffer.getBufferSubData(this._readback, 0, 16).then(()=>{
|
|
789
|
+
this._readbackPending = false;
|
|
790
|
+
const requested = new Uint32Array(this._readback.buffer, 0, 4)[1];
|
|
791
|
+
this._requestedListEntries = requested;
|
|
792
|
+
if (requested > MAX_LIST_ENTRIES && !this._warnedListOverflow) {
|
|
793
|
+
this._warnedListOverflow = true;
|
|
794
|
+
console.warn(`ClusteredLight: the light clusters ask for ${requested} list entries, above the ` + `${MAX_LIST_ENTRIES} budget; crowded clusters keep only their strongest lights. ` + `Giving lights a finite range usually cuts this down by orders of magnitude.`);
|
|
795
|
+
}
|
|
796
|
+
}, ()=>{
|
|
797
|
+
this._readbackPending = false;
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
/** Grows the CPU-side light arrays to hold `count` entries (slot 0 included). */ ensureLightArrayCapacity(count) {
|
|
801
|
+
const stride = SPHERE_STRIDE;
|
|
802
|
+
if (this._lights.length >= count * 16) {
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
const capacity = 1 << Math.ceil(Math.log2(count));
|
|
806
|
+
this._lights = new Float32Array(capacity * 16);
|
|
807
|
+
this._lightSpheres = new Float32Array(capacity * 4 * stride);
|
|
808
|
+
}
|
|
809
|
+
/** Creates the uniform light buffer read by the lit shaders, laid out as ShaderHelper declares it. */ createLightBuffer(device) {
|
|
810
|
+
const lightBufferType = new PBStructTypeInfo('ClusteredLightBuffer', 'std140', [
|
|
811
|
+
{
|
|
812
|
+
name: ShaderHelper.getLightBufferUniformName(),
|
|
813
|
+
type: new PBArrayTypeInfo(new PBPrimitiveTypeInfo(PBPrimitiveType.F32VEC4), (ShaderHelper.getMaxClusterLights() + 1) * 4)
|
|
814
|
+
}
|
|
815
|
+
]);
|
|
257
816
|
this._lightBuffer = device.createStructuredBuffer(lightBufferType, {
|
|
258
817
|
usage: 'uniform'
|
|
259
818
|
});
|
|
260
819
|
}
|
|
261
820
|
createLightIndexTexture(device) {
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
const
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
if (textureWidth * textureHeight !== this._tileCountX * this._tileCountY * this._tileCountZ) {
|
|
268
|
-
throw new Error('Internal error');
|
|
269
|
-
}
|
|
270
|
-
this._lightIndexTexture = device.createTexture2D(device.type === 'webgl' ? 'rgba32f' : 'rgba32ui', textureWidth, textureHeight, {
|
|
821
|
+
// One texel per cluster in row-major order; the tail of the last row is unused.
|
|
822
|
+
const numClusters = this._tileCountX * this._tileCountY * this._tileCountZ;
|
|
823
|
+
const textureWidth = Math.min(1 << Math.ceil(Math.log2(Math.ceil(Math.sqrt(numClusters)))), device.getDeviceCaps().textureCaps.maxTextureSize);
|
|
824
|
+
const textureHeight = Math.ceil(numClusters / textureWidth);
|
|
825
|
+
this._lightIndexTexture = device.createTexture2D('rgba32ui', textureWidth, textureHeight, {
|
|
271
826
|
mipmapping: false
|
|
272
827
|
});
|
|
273
828
|
this._lightIndexTexture.name = 'ClusterLightIndex';
|
|
829
|
+
this._lightIndexTexSize[0] = textureWidth;
|
|
830
|
+
this._lightIndexTexSize[1] = textureHeight;
|
|
274
831
|
this._lightIndexFramebuffer?.dispose();
|
|
275
832
|
this._lightIndexFramebuffer = device.createFrameBuffer([
|
|
276
833
|
this._lightIndexTexture
|
|
277
834
|
], null);
|
|
278
835
|
}
|
|
279
|
-
|
|
280
|
-
|
|
836
|
+
/**
|
|
837
|
+
* Builds the per-cluster light lists for a camera.
|
|
838
|
+
*
|
|
839
|
+
* @param width - Width in device pixels of the target the lit pass will draw into.
|
|
840
|
+
* @param height - Height in device pixels of that target.
|
|
841
|
+
*
|
|
842
|
+
* @remarks
|
|
843
|
+
* The lit shader maps `fragCoord` to a tile with `width`/`height`, so they must be the
|
|
844
|
+
* size of the target it actually renders to, not whatever viewport is current here.
|
|
845
|
+
*/ calculateLightIndex(camera, renderQueue, width, height, screenSpaceShadowMask = camera.screenSpaceShadowMask, preExposure = 1) {
|
|
846
|
+
const numLights = this.getVisibleLights(camera, renderQueue, screenSpaceShadowMask, preExposure);
|
|
281
847
|
const device = getDevice();
|
|
282
|
-
|
|
848
|
+
const compute = ShaderHelper.usesClusterLightLists();
|
|
849
|
+
this.resizeGrid(width, height);
|
|
850
|
+
if (compute) {
|
|
851
|
+
this.ensureStorageLightCapacity(device, numLights + 1);
|
|
852
|
+
this.ensureListBuffers(device, numLights);
|
|
853
|
+
} else if (!this._lightIndexTexture) {
|
|
283
854
|
this.createLightIndexTexture(device);
|
|
284
855
|
}
|
|
285
856
|
if (!this._lightIndexProgram) {
|
|
286
857
|
this.createProgram(device);
|
|
287
858
|
}
|
|
288
|
-
if (!this.
|
|
859
|
+
if (!compute && !this._lightBuffer) {
|
|
860
|
+
this.createLightBuffer(device);
|
|
861
|
+
}
|
|
862
|
+
if (!compute && !this._lightIndexVertexLayout) {
|
|
289
863
|
this.createVertexLayout(device, this._lightIndexTexture.width, this._lightIndexTexture.height);
|
|
290
864
|
}
|
|
291
|
-
if (!this._lightIndexRenderStates) {
|
|
865
|
+
if (!compute && !this._lightIndexRenderStates) {
|
|
292
866
|
this.createRenderState(device);
|
|
293
867
|
}
|
|
294
|
-
const
|
|
295
|
-
const
|
|
296
|
-
const
|
|
297
|
-
const
|
|
298
|
-
|
|
868
|
+
const vw = width;
|
|
869
|
+
const vh = height;
|
|
870
|
+
const near = camera.getNearPlane();
|
|
871
|
+
const far = camera.getFarPlane();
|
|
872
|
+
// The lit shader computes slice = t * scale + bias, where t is log2(view depth) for
|
|
873
|
+
// perspective (exponential slices) and the view depth itself for orthographic (linear
|
|
874
|
+
// slices: an orthographic near plane may be zero or negative, where log2 is undefined).
|
|
875
|
+
this._orthographic = !camera.isPerspective();
|
|
876
|
+
let scale;
|
|
877
|
+
let bias;
|
|
878
|
+
if (this._orthographic) {
|
|
879
|
+
scale = this._tileCountZ / (far - near);
|
|
880
|
+
bias = -near * scale;
|
|
881
|
+
} else {
|
|
882
|
+
// A near slice pays off only when it spans more than one regular exponential slice.
|
|
883
|
+
const split = NEAR_SLICE_METERS / (camera.scene?.metersPerUnit ?? 1);
|
|
884
|
+
const nearSlice = split > near * Math.pow(far / near, 1 / this._tileCountZ) && split < far;
|
|
885
|
+
const expStart = nearSlice ? split : near;
|
|
886
|
+
const firstExpSlice = nearSlice ? 1 : 0;
|
|
887
|
+
const numExpSlices = this._tileCountZ - firstExpSlice;
|
|
888
|
+
scale = numExpSlices / Math.log2(far / expStart);
|
|
889
|
+
// Depths before the split land below firstExpSlice, which the lit shader's clamp
|
|
890
|
+
// and integer conversion take to slice 0.
|
|
891
|
+
bias = firstExpSlice - Math.log2(expStart) * scale;
|
|
892
|
+
this._sliceParam.setXYZW(expStart, firstExpSlice, numExpSlices, 0);
|
|
893
|
+
}
|
|
299
894
|
this._clusterParam.setXYZW(vw, vh, scale, bias);
|
|
300
|
-
this._sizeParam.setXYZW(vw, vh,
|
|
895
|
+
this._sizeParam.setXYZW(vw, vh, near, far);
|
|
301
896
|
this._countParam[0] = this._tileCountX;
|
|
302
897
|
this._countParam[1] = this._tileCountY;
|
|
303
898
|
this._countParam[2] = this._tileCountZ;
|
|
@@ -305,36 +900,170 @@ class ClusteredLight {
|
|
|
305
900
|
// Keep it valid even when there are no unshadowed punctual lights so clustered
|
|
306
901
|
// shading math stays well-defined for emissive and environment lighting paths.
|
|
307
902
|
this._countParam[3] = numLights + 1;
|
|
903
|
+
const numClusters = this._tileCountX * this._tileCountY * this._tileCountZ;
|
|
308
904
|
device.pushDeviceStates();
|
|
309
|
-
|
|
310
|
-
|
|
905
|
+
if (!compute) {
|
|
906
|
+
device.setFramebuffer(this._lightIndexFramebuffer);
|
|
907
|
+
// setFramebuffer may no-op, so reset viewport and scissor explicitly.
|
|
908
|
+
device.setViewport(null);
|
|
909
|
+
device.setScissor(null);
|
|
910
|
+
}
|
|
911
|
+
// The compute pass writes every cluster itself, empty ones included.
|
|
912
|
+
if (compute || numLights > 0) {
|
|
913
|
+
if (this._lightSphereBuffer.disposed) {
|
|
914
|
+
this._lightSphereBuffer.reload();
|
|
915
|
+
}
|
|
916
|
+
// Slot 0 is never read, so upload only up to the last light.
|
|
311
917
|
if (this._lightBuffer.disposed) {
|
|
312
918
|
this._lightBuffer.reload();
|
|
313
919
|
}
|
|
314
|
-
this._lightBuffer.bufferSubData(0, this._lights);
|
|
920
|
+
this._lightBuffer.bufferSubData(0, this._lights, 0, (numLights + 1) * 16);
|
|
921
|
+
this._lightSphereBuffer.bufferSubData(0, this._lightSpheres, 0, (numLights + 1) * 4 * SPHERE_STRIDE);
|
|
315
922
|
this._bindGroup.setValue('invProjMatrix', camera.getInvProjectionMatrix());
|
|
316
|
-
this._bindGroup.setValue('viewMatrix', camera.viewMatrix);
|
|
317
923
|
this._bindGroup.setValue('sizeParam', this._sizeParam);
|
|
318
924
|
this._bindGroup.setValue('countParam', this._countParam);
|
|
319
|
-
this._bindGroup.
|
|
925
|
+
this._bindGroup.setValue('orthoProj', this._orthographic ? 1 : 0);
|
|
926
|
+
this._bindGroup.setValue('sliceParam', this._sliceParam);
|
|
927
|
+
this._bindGroup.setBuffer(UNIFORM_NAME_LIGHT_SPHERES, this._lightSphereBuffer);
|
|
320
928
|
device.setProgram(this._lightIndexProgram);
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
929
|
+
if (compute) {
|
|
930
|
+
const numGroups = Math.ceil(numClusters / INDEX_GROUP_SIZE);
|
|
931
|
+
this._listCounterBuffer.bufferSubData(0, this._listCounterZero);
|
|
932
|
+
const countBindGroup = this._countBindGroup;
|
|
933
|
+
countBindGroup.setValue('invProjMatrix', camera.getInvProjectionMatrix());
|
|
934
|
+
countBindGroup.setValue('sizeParam', this._sizeParam);
|
|
935
|
+
countBindGroup.setValue('countParam', this._countParam);
|
|
936
|
+
countBindGroup.setValue('orthoProj', this._orthographic ? 1 : 0);
|
|
937
|
+
countBindGroup.setValue('sliceParam', this._sliceParam);
|
|
938
|
+
countBindGroup.setBuffer(UNIFORM_NAME_LIGHT_SPHERES, this._lightSphereBuffer);
|
|
939
|
+
countBindGroup.setBuffer('clusterGrid', this._clusterGridBuffer);
|
|
940
|
+
countBindGroup.setBuffer('listCounter', this._listCounterBuffer);
|
|
941
|
+
device.setProgram(this._lightCountProgram);
|
|
942
|
+
device.setBindGroup(0, countBindGroup);
|
|
943
|
+
device.compute(numGroups, 1, 1);
|
|
944
|
+
this._bindGroup.setValue('listCapacity', this._lightListCapacity);
|
|
945
|
+
this._bindGroup.setBuffer('clusterGrid', this._clusterGridBuffer);
|
|
946
|
+
this._bindGroup.setBuffer('lightList', this._lightListBuffer);
|
|
947
|
+
this._bindGroup.setBuffer('listCounter', this._listCounterBuffer);
|
|
948
|
+
device.setProgram(this._lightIndexProgram);
|
|
949
|
+
device.setBindGroup(0, this._bindGroup);
|
|
950
|
+
device.compute(numGroups, 1, 1);
|
|
951
|
+
this.readBackListRequest();
|
|
952
|
+
} else {
|
|
953
|
+
device.setVertexLayout(this._lightIndexVertexLayout);
|
|
954
|
+
device.setBindGroup(0, this._bindGroup);
|
|
955
|
+
const savedRS = device.getRenderStates();
|
|
956
|
+
device.setRenderStates(this._lightIndexRenderStates);
|
|
957
|
+
device.draw('point-list', 0, numClusters);
|
|
958
|
+
device.setRenderStates(savedRS);
|
|
959
|
+
}
|
|
327
960
|
} else {
|
|
328
961
|
device.clearFrameBuffer(new Vector4(0, 0, 0, 0), DEPTH_CLEAR_VALUE, 0);
|
|
329
962
|
}
|
|
330
963
|
device.popDeviceStates();
|
|
331
964
|
}
|
|
332
|
-
|
|
965
|
+
/**
|
|
966
|
+
* Orders `lights` by estimated contribution at the camera, most important first.
|
|
967
|
+
*
|
|
968
|
+
* @remarks
|
|
969
|
+
* Both caps drop lights by buffer index - the global one truncates the tail, and
|
|
970
|
+
* a full cluster keeps its lowest 16 indices - so without this whichever lights the
|
|
971
|
+
* scene traversal happened to reach last are the ones that vanish. The score is
|
|
972
|
+
* `brightness * r^2 / (r^2 + d^2)`, with `r` the radius of the sphere bounding the
|
|
973
|
+
* lit region and `d` the camera's distance to it: 0 from anywhere inside it, falling
|
|
974
|
+
* off with distance beyond it, and larger for lights that reach further. The sphere is
|
|
975
|
+
* getLightBounds', so a spot light pointing away from the camera ranks by where it
|
|
976
|
+
* shines rather than where it stands, and the brightness is lightBrightness', so a
|
|
977
|
+
* rect light counts its whole area. Directional lights have no range and light
|
|
978
|
+
* everything, so they always rank first.
|
|
979
|
+
*/ prioritize(lights, eye) {
|
|
980
|
+
if (lights.length < 2) {
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
// Scores and a permutation in reused typed arrays, so a frame allocates nothing.
|
|
984
|
+
const n = lights.length;
|
|
985
|
+
if (this._sortScores.length < n) {
|
|
986
|
+
const capacity = 1 << Math.ceil(Math.log2(n));
|
|
987
|
+
this._sortScores = new Float64Array(capacity);
|
|
988
|
+
this._sortOrder = new Uint32Array(capacity);
|
|
989
|
+
}
|
|
990
|
+
const scores = this._sortScores;
|
|
991
|
+
const order = this._sortOrder.subarray(0, n);
|
|
992
|
+
const sorted = this._sortScratch;
|
|
993
|
+
sorted.length = n;
|
|
994
|
+
for(let i = 0; i < n; i++){
|
|
995
|
+
const light = lights[i];
|
|
996
|
+
getLightBounds(light, _bounds);
|
|
997
|
+
const range = _bounds[3];
|
|
998
|
+
let score;
|
|
999
|
+
if (light.isDirectionLight() || range <= 0) {
|
|
1000
|
+
score = Infinity;
|
|
1001
|
+
} else {
|
|
1002
|
+
const dx = _bounds[0] - eye.x;
|
|
1003
|
+
const dy = _bounds[1] - eye.y;
|
|
1004
|
+
const dz = _bounds[2] - eye.z;
|
|
1005
|
+
const d = Math.max(Math.sqrt(dx * dx + dy * dy + dz * dz) - range, 0);
|
|
1006
|
+
const range2 = range * range;
|
|
1007
|
+
score = lightBrightness(light) * range2 / (range2 + d * d);
|
|
1008
|
+
}
|
|
1009
|
+
scores[i] = score;
|
|
1010
|
+
order[i] = i;
|
|
1011
|
+
sorted[i] = light;
|
|
1012
|
+
}
|
|
1013
|
+
order.sort(this._compareScores);
|
|
1014
|
+
for(let i = 0; i < n; i++){
|
|
1015
|
+
lights[i] = sorted[order[i]];
|
|
1016
|
+
}
|
|
1017
|
+
sorted.length = 0;
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Orders buffer positions by descending score. Ties go to the lower position, which
|
|
1021
|
+
* keeps equally important lights in traversal order frame to frame. Compared rather
|
|
1022
|
+
* than subtracted: two directional lights would give Infinity - Infinity.
|
|
1023
|
+
*/ _compareScores = (a, b)=>{
|
|
1024
|
+
const sa = this._sortScores[a];
|
|
1025
|
+
const sb = this._sortScores[b];
|
|
1026
|
+
return sa > sb ? -1 : sa < sb ? 1 : a - b;
|
|
1027
|
+
};
|
|
1028
|
+
getVisibleLights(camera, renderQueue, useShadowMask, preExposure) {
|
|
1029
|
+
this.ensureLightArrayCapacity(Math.min(renderQueue.shadowedLights.length + renderQueue.unshadowedLights.length, ShaderHelper.getMaxClusterLights()) + 1);
|
|
1030
|
+
const lights = this._lights;
|
|
1031
|
+
const view = camera.viewMatrix;
|
|
1032
|
+
const spheres = this._lightSpheres;
|
|
1033
|
+
const sphereStride = SPHERE_STRIDE;
|
|
1034
|
+
const globalLights = this._globalLights;
|
|
1035
|
+
globalLights.fill(0);
|
|
1036
|
+
let numGlobal = 0;
|
|
333
1037
|
const writeLight = (light, slot)=>{
|
|
334
1038
|
const offset = slot * 16;
|
|
335
1039
|
const colorIntensity = light.diffuseAndIntensity;
|
|
336
|
-
|
|
337
|
-
|
|
1040
|
+
const posRange = light.positionAndRange;
|
|
1041
|
+
const dirCutoff = light.directionAndCutoff;
|
|
1042
|
+
lights.set(posRange, offset);
|
|
1043
|
+
// Culling sphere: radius < 0 = shaded through a global slot, skipped by the index
|
|
1044
|
+
// pass; 0 = unbounded, in every cluster; otherwise it bounds the lit region.
|
|
1045
|
+
getLightBounds(light, _bounds);
|
|
1046
|
+
const x = _bounds[0];
|
|
1047
|
+
const y = _bounds[1];
|
|
1048
|
+
const z = _bounds[2];
|
|
1049
|
+
let radius = _bounds[3];
|
|
1050
|
+
if (radius <= 0) {
|
|
1051
|
+
if (numGlobal < MAX_GLOBAL_LIGHTS) {
|
|
1052
|
+
globalLights[numGlobal++] = slot;
|
|
1053
|
+
radius = -1;
|
|
1054
|
+
} else {
|
|
1055
|
+
radius = 0;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
// The view matrix is column-major.
|
|
1059
|
+
const s = slot * 4 * sphereStride;
|
|
1060
|
+
spheres[s + 0] = view[0] * x + view[4] * y + view[8] * z + view[12];
|
|
1061
|
+
spheres[s + 1] = view[1] * x + view[5] * y + view[9] * z + view[13];
|
|
1062
|
+
spheres[s + 2] = view[2] * x + view[6] * y + view[10] * z + view[14];
|
|
1063
|
+
spheres[s + 3] = radius;
|
|
1064
|
+
// Ranks the lights competing for a crowded cluster; same measure as prioritize().
|
|
1065
|
+
spheres[s + 4] = lightBrightness(light);
|
|
1066
|
+
lights.set(dirCutoff, offset + 4);
|
|
338
1067
|
// Only the intensity carries the camera pre-exposure; the color stays as authored. The
|
|
339
1068
|
// light's own cached vector must not be mutated because it is shared across cameras.
|
|
340
1069
|
lights[offset + 8] = colorIntensity.x;
|
|
@@ -357,27 +1086,32 @@ class ClusteredLight {
|
|
|
357
1086
|
}
|
|
358
1087
|
slot = numShadow;
|
|
359
1088
|
}
|
|
360
|
-
//
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
1089
|
+
// Everything without a mask slot fills the region after the mask-backed shadow
|
|
1090
|
+
// lights: the unshadowed lights, plus shadow-casting lights beyond the mask
|
|
1091
|
+
// capacity (MAX_SHADOW_MASK_LIGHTS). Rather than dropping the latter silently they
|
|
1092
|
+
// are still lit here - without a mask sample, i.e. degraded to no shadow. Both
|
|
1093
|
+
// compete for the remaining slots on equal terms, most important first.
|
|
1094
|
+
const rest = this._rest;
|
|
1095
|
+
rest.length = 0;
|
|
1096
|
+
for (const light of renderQueue.unshadowedLights){
|
|
1097
|
+
rest.push(light);
|
|
364
1098
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
}
|
|
1099
|
+
const overflow = useShadowMask ? renderQueue.shadowedLights.length - numShadow : 0;
|
|
1100
|
+
for(let k = 0; k < overflow; k++){
|
|
1101
|
+
rest.push(renderQueue.shadowedLights[numShadow + k]);
|
|
1102
|
+
}
|
|
1103
|
+
this.prioritize(rest, camera.getWorldPosition());
|
|
1104
|
+
const numRest = Math.min(rest.length, ShaderHelper.getMaxClusterLights() - slot);
|
|
1105
|
+
for(let j = 0; j < numRest; j++){
|
|
1106
|
+
writeLight(rest[j], slot + j + 1);
|
|
1107
|
+
}
|
|
1108
|
+
slot += numRest;
|
|
1109
|
+
if (overflow > 0 && !ClusteredLight._warnedShadowMaskOverflow) {
|
|
1110
|
+
ClusteredLight._warnedShadowMaskOverflow = true;
|
|
1111
|
+
const dropped = rest.length - numRest;
|
|
1112
|
+
console.warn(`ClusteredLight: ${renderQueue.shadowedLights.length} shadow-casting lights exceed the ` + `screen-space shadow mask capacity (${MAX_SHADOW_MASK_LIGHTS}); ${overflow} light(s) are ` + `lit without shadows${dropped > 0 ? `, and the ${dropped} least important light(s) dropped` : ''}.`);
|
|
380
1113
|
}
|
|
1114
|
+
rest.length = 0;
|
|
381
1115
|
this._numShadowLights = numShadow;
|
|
382
1116
|
return slot;
|
|
383
1117
|
}
|