@zephyr3d/scene 0.9.25 → 0.9.27
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/dist/animation/animationset.js +92 -30
- package/dist/animation/animationset.js.map +1 -1
- package/dist/animation/hair/gpu_hair_simulation.js +1090 -0
- package/dist/animation/hair/gpu_hair_simulation.js.map +1 -0
- package/dist/animation/joint_dynamics/controller.js +7 -5
- package/dist/animation/joint_dynamics/controller.js.map +1 -1
- package/dist/animation/joint_dynamics/convex_collider.js +320 -0
- package/dist/animation/joint_dynamics/convex_collider.js.map +1 -0
- package/dist/animation/joint_dynamics/joint_dynamics_system.js +81 -5
- package/dist/animation/joint_dynamics/joint_dynamics_system.js.map +1 -1
- package/dist/animation/morphtrack.js +3 -4
- package/dist/animation/morphtrack.js.map +1 -1
- package/dist/animation/skeleton.js +79 -25
- package/dist/animation/skeleton.js.map +1 -1
- package/dist/animation/spring/multi_chain_spring_system.js +577 -179
- package/dist/animation/spring/multi_chain_spring_system.js.map +1 -1
- package/dist/animation/spring/spring_collider.js +4 -3
- package/dist/animation/spring/spring_collider.js.map +1 -1
- package/dist/animation/spring/spring_node_pose_tracker.js +40 -0
- package/dist/animation/spring/spring_node_pose_tracker.js.map +1 -0
- package/dist/animation/spring/spring_particle.js +2 -0
- package/dist/animation/spring/spring_particle.js.map +1 -1
- package/dist/animation/spring/spring_system.js +11 -3
- package/dist/animation/spring/spring_system.js.map +1 -1
- package/dist/animation/spring_modifier.js +10 -0
- package/dist/animation/spring_modifier.js.map +1 -1
- package/dist/app/engine.js +53 -43
- package/dist/app/engine.js.map +1 -1
- package/dist/app/scriptingsystem.js +119 -6
- package/dist/app/scriptingsystem.js.map +1 -1
- package/dist/app/scriptregistry.js +3 -2
- package/dist/app/scriptregistry.js.map +1 -1
- package/dist/asset/assetmanager.js +284 -67
- package/dist/asset/assetmanager.js.map +1 -1
- package/dist/asset/loaders/zhair/zhair_format.js +288 -0
- package/dist/asset/loaders/zhair/zhair_format.js.map +1 -0
- package/dist/asset/loaders/zhair/zhair_loader.js +221 -0
- package/dist/asset/loaders/zhair/zhair_loader.js.map +1 -0
- package/dist/asset/model.js +184 -69
- package/dist/asset/model.js.map +1 -1
- package/dist/camera/camera.js +268 -44
- package/dist/camera/camera.js.map +1 -1
- package/dist/camera/perspectivecamera.js +3 -1
- package/dist/camera/perspectivecamera.js.map +1 -1
- package/dist/index.d.ts +9044 -5139
- package/dist/index.js +17 -6
- package/dist/index.js.map +1 -1
- package/dist/material/eye.js +932 -0
- package/dist/material/eye.js.map +1 -0
- package/dist/material/hair.js +917 -0
- package/dist/material/hair.js.map +1 -0
- package/dist/material/hairstrand.js +842 -0
- package/dist/material/hairstrand.js.map +1 -0
- package/dist/material/hairstrand_data.js +162 -0
- package/dist/material/hairstrand_data.js.map +1 -0
- package/dist/material/lambert.js +5 -2
- package/dist/material/lambert.js.map +1 -1
- package/dist/material/material.js +48 -10
- package/dist/material/material.js.map +1 -1
- package/dist/material/meshmaterial.js +120 -19
- package/dist/material/meshmaterial.js.map +1 -1
- package/dist/material/mixins/albedocolor.js +1 -1
- package/dist/material/mixins/albedocolor.js.map +1 -1
- package/dist/material/mixins/lightmodel/blinnphong.js +5 -3
- package/dist/material/mixins/lightmodel/blinnphong.js.map +1 -1
- package/dist/material/mixins/lightmodel/lambert.js +5 -4
- package/dist/material/mixins/lightmodel/lambert.js.map +1 -1
- package/dist/material/mixins/lightmodel/pbrblueprintmixin.js +3 -3
- package/dist/material/mixins/lightmodel/pbrblueprintmixin.js.map +1 -1
- package/dist/material/mixins/lightmodel/pbrmetallicroughness.js +5 -5
- package/dist/material/mixins/lightmodel/pbrmetallicroughness.js.map +1 -1
- package/dist/material/mixins/lightmodel/pbrspecularglossness.js +3 -3
- package/dist/material/mixins/lightmodel/pbrspecularglossness.js.map +1 -1
- package/dist/material/mixins/lit.js +106 -19
- package/dist/material/mixins/lit.js.map +1 -1
- package/dist/material/mixins/pbr/common.js +4 -4
- package/dist/material/mixins/pbr/common.js.map +1 -1
- package/dist/material/mixins/texture.js +48 -11
- package/dist/material/mixins/texture.js.map +1 -1
- package/dist/material/mtoon.js +6 -5
- package/dist/material/mtoon.js.map +1 -1
- package/dist/material/pbrblueprint.js +146 -28
- package/dist/material/pbrblueprint.js.map +1 -1
- package/dist/material/pbrblueprint_instance.js +501 -0
- package/dist/material/pbrblueprint_instance.js.map +1 -0
- package/dist/material/pbrmr.js +15 -16
- package/dist/material/pbrmr.js.map +1 -1
- package/dist/material/shader/helper.js +734 -23
- package/dist/material/shader/helper.js.map +1 -1
- package/dist/material/shader/terrain_blend.js +55 -0
- package/dist/material/shader/terrain_blend.js.map +1 -0
- package/dist/material/skin.js +192 -32
- package/dist/material/skin.js.map +1 -1
- package/dist/material/spriteblueprint.js +16 -1
- package/dist/material/spriteblueprint.js.map +1 -1
- package/dist/material/subsurfaceprofile.js +86 -23
- package/dist/material/subsurfaceprofile.js.map +1 -1
- package/dist/material/water.js +2059 -72
- package/dist/material/water.js.map +1 -1
- package/dist/posteffect/bloom.js +167 -7
- package/dist/posteffect/bloom.js.map +1 -1
- package/dist/posteffect/coloradjust.js +2 -1
- package/dist/posteffect/coloradjust.js.map +1 -1
- package/dist/posteffect/compositor.js.map +1 -1
- package/dist/posteffect/fxaa.js +2 -1
- package/dist/posteffect/fxaa.js.map +1 -1
- package/dist/posteffect/grayscale.js +3 -1
- package/dist/posteffect/grayscale.js.map +1 -1
- package/dist/posteffect/motionblur.js +664 -56
- 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/sao.js +46 -44
- package/dist/posteffect/sao.js.map +1 -1
- package/dist/posteffect/skinsss.js +368 -69
- package/dist/posteffect/skinsss.js.map +1 -1
- package/dist/posteffect/ssgi.js +16 -9
- package/dist/posteffect/ssgi.js.map +1 -1
- package/dist/posteffect/ssr.js +3 -0
- package/dist/posteffect/ssr.js.map +1 -1
- package/dist/posteffect/sss.js +121 -36
- package/dist/posteffect/sss.js.map +1 -1
- package/dist/posteffect/taa.js.map +1 -1
- package/dist/posteffect/tonemap.js +44 -1
- package/dist/posteffect/tonemap.js.map +1 -1
- package/dist/render/abuffer_oit.js +8 -2
- package/dist/render/abuffer_oit.js.map +1 -1
- package/dist/render/clipmap.js +156 -5
- package/dist/render/clipmap.js.map +1 -1
- package/dist/render/cull_visitor.js +20 -0
- package/dist/render/cull_visitor.js.map +1 -1
- package/dist/render/depthpass.js +36 -2
- package/dist/render/depthpass.js.map +1 -1
- package/dist/render/drawable_mixin.js +26 -1
- package/dist/render/drawable_mixin.js.map +1 -1
- package/dist/render/dualdepthpeeling_oit.js +3 -2
- package/dist/render/dualdepthpeeling_oit.js.map +1 -1
- package/dist/render/envlight.js +1 -1
- package/dist/render/envlight.js.map +1 -1
- package/dist/render/fbm_wavegenerator.js +6 -3
- package/dist/render/fbm_wavegenerator.js.map +1 -1
- package/dist/render/fft_wavegenerator.js +57 -15
- package/dist/render/fft_wavegenerator.js.map +1 -1
- package/dist/render/gerstner_wavegenerator.js +130 -20
- package/dist/render/gerstner_wavegenerator.js.map +1 -1
- package/dist/render/globalbindgroup_allocator.js +3 -0
- package/dist/render/globalbindgroup_allocator.js.map +1 -1
- package/dist/render/hzb.js +80 -22
- package/dist/render/hzb.js.map +1 -1
- package/dist/render/lightpass.js +6 -3
- package/dist/render/lightpass.js.map +1 -1
- package/dist/render/oit.js +29 -0
- package/dist/render/oit.js.map +1 -0
- package/dist/render/render_queue.js +39 -0
- package/dist/render/render_queue.js.map +1 -1
- package/dist/render/renderer.js +4 -0
- package/dist/render/renderer.js.map +1 -1
- package/dist/render/rendergraph/blackboard.js +1 -0
- package/dist/render/rendergraph/blackboard.js.map +1 -1
- package/dist/render/rendergraph/device_pool_allocator.js.map +1 -1
- package/dist/render/rendergraph/executor.js +2 -0
- package/dist/render/rendergraph/executor.js.map +1 -1
- package/dist/render/rendergraph/forward_plus_builder.js +379 -10
- 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/rendergraph.js.map +1 -1
- package/dist/render/rendergraph/texture_affinity_cache.js +1 -1
- package/dist/render/rendergraph/texture_affinity_cache.js.map +1 -1
- package/dist/render/rendergraph/types.js.map +1 -1
- package/dist/render/shadow_mask_pass.js +73 -8
- package/dist/render/shadow_mask_pass.js.map +1 -1
- package/dist/render/sky.js +166 -126
- package/dist/render/sky.js.map +1 -1
- package/dist/render/underwater.js +624 -0
- package/dist/render/underwater.js.map +1 -0
- package/dist/render/water_caustics.js +1380 -0
- package/dist/render/water_caustics.js.map +1 -0
- package/dist/render/weightedblended_oit.js +3 -2
- package/dist/render/weightedblended_oit.js.map +1 -1
- package/dist/scene/basesprite.js +8 -0
- package/dist/scene/basesprite.js.map +1 -1
- package/dist/scene/batchgroup.js +3 -0
- package/dist/scene/batchgroup.js.map +1 -1
- package/dist/scene/environment.js +4 -1
- package/dist/scene/environment.js.map +1 -1
- package/dist/scene/hair.js +963 -0
- package/dist/scene/hair.js.map +1 -0
- package/dist/scene/light.js +5 -1
- package/dist/scene/light.js.map +1 -1
- package/dist/scene/mesh.js +481 -47
- 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 +3 -0
- package/dist/scene/msdftext.js.map +1 -1
- package/dist/scene/msdftextsprite.js +3 -0
- package/dist/scene/msdftextsprite.js.map +1 -1
- package/dist/scene/particlesys.js +6 -0
- package/dist/scene/particlesys.js.map +1 -1
- package/dist/scene/scene.js +3 -0
- package/dist/scene/scene.js.map +1 -1
- package/dist/scene/scene_node.js +6 -1
- package/dist/scene/scene_node.js.map +1 -1
- package/dist/scene/terrain-cm/grass.js +3 -0
- package/dist/scene/terrain-cm/grass.js.map +1 -1
- package/dist/scene/terrain-cm/grassmaterial.js +3 -0
- package/dist/scene/terrain-cm/grassmaterial.js.map +1 -1
- package/dist/scene/terrain-cm/terrain-cm.js +6 -0
- package/dist/scene/terrain-cm/terrain-cm.js.map +1 -1
- package/dist/scene/terrain-cm/texture_cache.js +309 -0
- package/dist/scene/terrain-cm/texture_cache.js.map +1 -0
- package/dist/scene/water.js +546 -4
- package/dist/scene/water.js.map +1 -1
- package/dist/shaders/atmosphere.js.map +1 -1
- package/dist/shaders/diffusion.js +54 -0
- package/dist/shaders/diffusion.js.map +1 -0
- package/dist/shaders/shadow.js +33 -21
- package/dist/shaders/shadow.js.map +1 -1
- package/dist/shaders/temporal.js +71 -33
- package/dist/shaders/temporal.js.map +1 -1
- package/dist/shaders/water.js +22 -19
- package/dist/shaders/water.js.map +1 -1
- package/dist/shaders/water_medium.js +56 -0
- package/dist/shaders/water_medium.js.map +1 -0
- package/dist/shadow/dom.js +546 -0
- package/dist/shadow/dom.js.map +1 -0
- package/dist/shadow/pcf_opt.js +3 -5
- package/dist/shadow/pcf_opt.js.map +1 -1
- package/dist/shadow/pcf_pd.js +1 -1
- package/dist/shadow/pcf_pd.js.map +1 -1
- package/dist/shadow/receiver_bias.js +41 -0
- package/dist/shadow/receiver_bias.js.map +1 -0
- package/dist/shadow/shader.js +20 -60
- package/dist/shadow/shader.js.map +1 -1
- package/dist/shadow/shadow_impl.js +89 -0
- package/dist/shadow/shadow_impl.js.map +1 -1
- package/dist/shadow/shadow_region.js +1 -1
- package/dist/shadow/shadow_region.js.map +1 -1
- package/dist/shadow/shadowmapper.js +150 -37
- package/dist/shadow/shadowmapper.js.map +1 -1
- package/dist/shapes/shape.js +11 -2
- package/dist/shapes/shape.js.map +1 -1
- package/dist/shapes/sphere.js +50 -9
- package/dist/shapes/sphere.js.map +1 -1
- package/dist/utility/blueprint/common/constants.js +112 -2
- package/dist/utility/blueprint/common/constants.js.map +1 -1
- package/dist/utility/blueprint/material/func.js +142 -49
- package/dist/utility/blueprint/material/func.js.map +1 -1
- package/dist/utility/blueprint/material/ir.js +490 -25
- package/dist/utility/blueprint/material/ir.js.map +1 -1
- package/dist/utility/blueprint/material/texture.js +898 -4
- package/dist/utility/blueprint/material/texture.js.map +1 -1
- package/dist/utility/serialization/manager.js +434 -101
- package/dist/utility/serialization/manager.js.map +1 -1
- package/dist/utility/serialization/scene/animation.js +401 -5
- package/dist/utility/serialization/scene/animation.js.map +1 -1
- package/dist/utility/serialization/scene/batch.js +4 -0
- package/dist/utility/serialization/scene/batch.js.map +1 -1
- package/dist/utility/serialization/scene/camera.js +186 -26
- package/dist/utility/serialization/scene/camera.js.map +1 -1
- package/dist/utility/serialization/scene/common.js +26 -5
- package/dist/utility/serialization/scene/common.js.map +1 -1
- package/dist/utility/serialization/scene/hair.js +1141 -0
- package/dist/utility/serialization/scene/hair.js.map +1 -0
- package/dist/utility/serialization/scene/light.js +84 -4
- package/dist/utility/serialization/scene/light.js.map +1 -1
- package/dist/utility/serialization/scene/material.js +1867 -166
- package/dist/utility/serialization/scene/material.js.map +1 -1
- package/dist/utility/serialization/scene/mesh.js +231 -14
- package/dist/utility/serialization/scene/mesh.js.map +1 -1
- package/dist/utility/serialization/scene/node.js +141 -7
- package/dist/utility/serialization/scene/node.js.map +1 -1
- package/dist/utility/serialization/scene/particle.js +4 -0
- package/dist/utility/serialization/scene/particle.js.map +1 -1
- package/dist/utility/serialization/scene/primitive.js +18 -0
- package/dist/utility/serialization/scene/primitive.js.map +1 -1
- package/dist/utility/serialization/scene/sprite.js +4 -0
- package/dist/utility/serialization/scene/sprite.js.map +1 -1
- package/dist/utility/serialization/scene/terrain.js +4 -0
- package/dist/utility/serialization/scene/terrain.js.map +1 -1
- package/dist/utility/serialization/scene/text.js +4 -0
- package/dist/utility/serialization/scene/text.js.map +1 -1
- package/dist/utility/serialization/scene/water.js +1105 -21
- package/dist/utility/serialization/scene/water.js.map +1 -1
- package/dist/utility/serialization/types.js.map +1 -1
- package/dist/utility/shprojector.js +9 -8
- package/dist/utility/shprojector.js.map +1 -1
- package/dist/values.js +49 -2
- package/dist/values.js.map +1 -1
- package/package.json +3 -3
- package/dist/render/rendergraph/frame_graph_context.js +0 -41
- package/dist/render/rendergraph/frame_graph_context.js.map +0 -1
- package/dist/shaders/ssgi.js +0 -63
- package/dist/shaders/ssgi.js.map +0 -1
- package/dist/shaders/ssgi_reprojection.js +0 -118
- package/dist/shaders/ssgi_reprojection.js.map +0 -1
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
import { ShadowImpl } from './shadow_impl.js';
|
|
2
|
+
import { LIGHT_TYPE_POINT } from '../values.js';
|
|
3
|
+
import { ndcToShadowCoord3, shadowCoordDepthInRange, applyShadowDepthBias } from '../shaders/shadow.js';
|
|
4
|
+
import { computeShadowBias } from './shader.js';
|
|
5
|
+
import { ShaderHelper } from '../material/shader/helper.js';
|
|
6
|
+
import { getDevice } from '../app/api.js';
|
|
7
|
+
import { fetchSampler } from '../utility/misc.js';
|
|
8
|
+
import { Vector4, REVERSE_Z } from '@zephyr3d/base';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Opacity layers, which is three rather than four because the fourth channel of
|
|
12
|
+
* the result texture carries the depth the layers are measured from. Keeping
|
|
13
|
+
* both in one texture is what lets the receiver read the whole technique through
|
|
14
|
+
* the single shadow-map binding the lighting shaders declare.
|
|
15
|
+
* @internal
|
|
16
|
+
*/ const DOM_LAYER_COUNT = 3;
|
|
17
|
+
/** @internal Caster-pass uniform holding the frontmost depth from geometry pass 0. */ const UNIFORM_NAME_DOM_FRONT_DEPTH = 'Z_domFrontDepth';
|
|
18
|
+
/**
|
|
19
|
+
* Transmittance a fully opaque caster is recorded as, since zero has no finite
|
|
20
|
+
* optical depth to blend. Dark enough to read as black, and small enough in the
|
|
21
|
+
* exponent that several opaque layers can stack without saturating half floats.
|
|
22
|
+
* @internal
|
|
23
|
+
*/ const DOM_MIN_TRANSMITTANCE = 0.001;
|
|
24
|
+
/**
|
|
25
|
+
* Deep opacity map shadows.
|
|
26
|
+
*
|
|
27
|
+
* @remarks
|
|
28
|
+
* Hair is the case an ordinary shadow map cannot express. A shadow map answers
|
|
29
|
+
* "is this point behind the nearest caster", which is the right question for a
|
|
30
|
+
* wall and the wrong one for ten thousand strands: light entering a groom is not
|
|
31
|
+
* blocked at its surface, it is attenuated gradually as it passes more and more
|
|
32
|
+
* fibres. A binary test renders that as a hard edge with everything behind it
|
|
33
|
+
* uniformly black.
|
|
34
|
+
*
|
|
35
|
+
* A deep opacity map (Yuksel & Keyser 2008) records the attenuation instead of
|
|
36
|
+
* the boundary. Geometry pass 0 writes the frontmost caster depth `z0` per texel.
|
|
37
|
+
* Pass 1 rasterises the casters again and each fragment adds its optical depth to
|
|
38
|
+
* every layer lying behind it, giving cumulative absorption sampled at three
|
|
39
|
+
* depths measured *from `z0`* rather than from the light. Because the layers
|
|
40
|
+
* follow the hair surface, three of them resolve what a plain opacity shadow map
|
|
41
|
+
* needs dozens of uniformly spaced ones to match.
|
|
42
|
+
*
|
|
43
|
+
* Recording optical depth rather than coverage is what keeps it exact for solid
|
|
44
|
+
* casters as well as thin ones, so a scene does not have to reserve this mode for
|
|
45
|
+
* its hair: see the caster output for why the two differ.
|
|
46
|
+
*
|
|
47
|
+
* WebGPU only, and deliberately so: the strand geometry this exists to shadow is
|
|
48
|
+
* itself WebGPU only, so supporting the WebGL fallbacks - cascade atlases instead
|
|
49
|
+
* of array layers, RGBA8-packed depth instead of float - would double the surface
|
|
50
|
+
* area to serve no material.
|
|
51
|
+
*
|
|
52
|
+
* @internal
|
|
53
|
+
*/ class DOM extends ShadowImpl {
|
|
54
|
+
/** @internal */ _layerDistance;
|
|
55
|
+
/** @internal */ _density;
|
|
56
|
+
/** @internal Width of the sampling kernel in texels; 1 disables filtering. */ _filterSize;
|
|
57
|
+
/** @internal */ _paramsScratch;
|
|
58
|
+
/**
|
|
59
|
+
* @internal
|
|
60
|
+
* Which geometry pass is being rasterised. Lives on the implementation rather
|
|
61
|
+
* than in `implData` because {@link DOM.getShaderHash} has no access to the
|
|
62
|
+
* params, and the hash must separate the two passes or they share a program.
|
|
63
|
+
*/ _geometryPass;
|
|
64
|
+
/**
|
|
65
|
+
* @internal
|
|
66
|
+
* Depth the shadow camera spans, captured when the map is rendered. Needed to
|
|
67
|
+
* express {@link DOM.layerDistance} in world units: the shader compares device
|
|
68
|
+
* depths, and for the orthographic projection a directional light uses those
|
|
69
|
+
* are the world depth divided by exactly this.
|
|
70
|
+
*/ _depthRange;
|
|
71
|
+
constructor(layerDistance = 0.25, density = 1, filterSize = 5){
|
|
72
|
+
super();
|
|
73
|
+
this._layerDistance = layerDistance;
|
|
74
|
+
this._density = density;
|
|
75
|
+
this._filterSize = DOM.clampFilterSize(filterSize);
|
|
76
|
+
this._paramsScratch = new Vector4();
|
|
77
|
+
this._geometryPass = 0;
|
|
78
|
+
this._depthRange = 1;
|
|
79
|
+
}
|
|
80
|
+
/** @internal */ static clampFilterSize(val) {
|
|
81
|
+
return val !== 1 && val !== 3 && val !== 5 && val !== 7 ? 5 : val;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Width of the sampling kernel, in shadow map texels. One of 1, 3, 5 or 7.
|
|
85
|
+
*
|
|
86
|
+
* @remarks
|
|
87
|
+
* The graded transmittance this technique records runs along the light, not
|
|
88
|
+
* across the map, so at the silhouette of a groom - where the change is one of
|
|
89
|
+
* coverage rather than of depth - a single tap is as abrupt as an unfiltered
|
|
90
|
+
* shadow map. This is the kernel that softens that edge, and it is the same
|
|
91
|
+
* knob PCF's kernel size is, with the difference that each tap here costs a
|
|
92
|
+
* layer lookup rather than a depth comparison.
|
|
93
|
+
*
|
|
94
|
+
* Set to 1 to sample once, which is measurably cheaper and visibly harder.
|
|
95
|
+
*/ get filterSize() {
|
|
96
|
+
return this._filterSize;
|
|
97
|
+
}
|
|
98
|
+
set filterSize(val) {
|
|
99
|
+
this._filterSize = DOM.clampFilterSize(val);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Depth the layers span, in world units along the light direction.
|
|
103
|
+
*
|
|
104
|
+
* @remarks
|
|
105
|
+
* Set it to roughly the thickness of the hair. Too small and everything past
|
|
106
|
+
* the outer strands saturates the last layer, which is a hard shadow again;
|
|
107
|
+
* too large and all three layers land inside the first strand, leaving the
|
|
108
|
+
* interior unshadowed.
|
|
109
|
+
*
|
|
110
|
+
* Exact for directional lights, whose orthographic projection makes device
|
|
111
|
+
* depth a linear function of world depth. Spot and point lights project
|
|
112
|
+
* perspectively, so there the value is only accurate near the middle of the
|
|
113
|
+
* shadow camera's range - which is where hair lit by one usually sits.
|
|
114
|
+
*/ get layerDistance() {
|
|
115
|
+
return this._layerDistance;
|
|
116
|
+
}
|
|
117
|
+
set layerDistance(val) {
|
|
118
|
+
this._layerDistance = val > 0 ? val : 0.0001;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Artistic multiplier on the absorption the casters actually recorded.
|
|
122
|
+
*
|
|
123
|
+
* @remarks
|
|
124
|
+
* One reproduces alpha compositing exactly and is the right default. Raise it
|
|
125
|
+
* to deepen the interior of a groom without moving the layers, lower it to let
|
|
126
|
+
* more light through than the geometry strictly would.
|
|
127
|
+
*/ get density() {
|
|
128
|
+
return this._density;
|
|
129
|
+
}
|
|
130
|
+
set density(val) {
|
|
131
|
+
this._density = val < 0 ? 0 : val;
|
|
132
|
+
}
|
|
133
|
+
getType() {
|
|
134
|
+
return 'dom';
|
|
135
|
+
}
|
|
136
|
+
resourceDirty() {
|
|
137
|
+
return this._resourceDirty;
|
|
138
|
+
}
|
|
139
|
+
getShadowMapBorder() {
|
|
140
|
+
// Margin so the kernel's outer taps still land inside the map at the edges of
|
|
141
|
+
// the shadow region, matching what the PCF implementations reserve.
|
|
142
|
+
return this._filterSize;
|
|
143
|
+
}
|
|
144
|
+
getDepthScale() {
|
|
145
|
+
return 1;
|
|
146
|
+
}
|
|
147
|
+
setDepthScale(_val) {}
|
|
148
|
+
/**
|
|
149
|
+
* Layer geometry and density, for both the caster and the receiver. `z` and
|
|
150
|
+
* `w` are unused.
|
|
151
|
+
*
|
|
152
|
+
* @remarks
|
|
153
|
+
* `x` is the layer span converted from world units into the device-depth
|
|
154
|
+
* difference the shaders actually compare, using the range captured when the
|
|
155
|
+
* map was rendered.
|
|
156
|
+
*/ getParams(out) {
|
|
157
|
+
const result = out ?? this._paramsScratch;
|
|
158
|
+
result.setXYZW(this._layerDistance / this._depthRange, this._density, 0, 0);
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
getGeometryPassCount() {
|
|
162
|
+
return 2;
|
|
163
|
+
}
|
|
164
|
+
supportsCascades() {
|
|
165
|
+
// Each split would need its own depth and layer set, and both already share
|
|
166
|
+
// the one texture the receiver can bind.
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
clipsCasterAlpha() {
|
|
170
|
+
// Neither pass wants the clip. Pass 1 integrates the alpha, and clipping is
|
|
171
|
+
// destructive to it in both of the forms the material applies: a dithered
|
|
172
|
+
// caster survives with probability alpha and then contributes the absorption
|
|
173
|
+
// of that same alpha, counting it twice, and a hard cutoff drops the sub-pixel
|
|
174
|
+
// strands that carry most of a groom's transmittance.
|
|
175
|
+
//
|
|
176
|
+
// Pass 0 skips it for a different reason. Its job is to find the frontmost
|
|
177
|
+
// strand, and under dithering the clip makes that choice per-pixel random -
|
|
178
|
+
// the layers would then start at a depth that flickers between strands, and
|
|
179
|
+
// every opacity reading behind it inherits the jitter.
|
|
180
|
+
//
|
|
181
|
+
// The case this gives up is a caster whose alpha is a real mask rather than a
|
|
182
|
+
// coverage value, such as a hair card with cut-out texels: pass 0 will place
|
|
183
|
+
// z0 on the card's surface even where it is fully transparent. Strand
|
|
184
|
+
// geometry, which is what this technique is for, has no such texels.
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
beginGeometryPass(shadowMapParams, pass) {
|
|
188
|
+
const implData = shadowMapParams.implData;
|
|
189
|
+
this._geometryPass = pass;
|
|
190
|
+
// The light camera is finalised by the time the casters are drawn, so this is
|
|
191
|
+
// where the world-to-device depth scale becomes known. It stays valid for the
|
|
192
|
+
// lighting pass that follows, which reads the same converted params back.
|
|
193
|
+
const range = shadowMapParams.cameraParams.y - shadowMapParams.cameraParams.x;
|
|
194
|
+
this._depthRange = range > 1e-6 ? range : 1;
|
|
195
|
+
getDevice().setFramebuffer(pass === 0 ? implData.depthFramebuffer : implData.opacityFramebuffer);
|
|
196
|
+
}
|
|
197
|
+
setCasterRenderStates(_shadowMapParams, stateSet) {
|
|
198
|
+
if (this._geometryPass !== 1) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
// Every strand between z0 and the last layer has to contribute, so the pass
|
|
202
|
+
// must not depth-test against the frontmost surface pass 0 resolved, and must
|
|
203
|
+
// not write depth of its own - its framebuffer has no depth attachment at all.
|
|
204
|
+
// Optical depth sums across the three layer channels; alpha instead carries
|
|
205
|
+
// z0, which every fragment reads from the same texel, so replacing is correct
|
|
206
|
+
// where summing would multiply it by the number of overlapping strands.
|
|
207
|
+
stateSet.useBlendingState().enable(true).setBlendEquation('add', 'add').setBlendFuncRGB('one', 'one').setBlendFuncAlpha('one', 'zero');
|
|
208
|
+
stateSet.useDepthState().enableTest(false).enableWrite(false);
|
|
209
|
+
}
|
|
210
|
+
getShadowMapClearColor(_shadowMapParams) {
|
|
211
|
+
if (this._geometryPass === 1) {
|
|
212
|
+
// No hair over this texel means no absorption. Alpha carries z0, and the
|
|
213
|
+
// sentinel below makes an uncovered texel read as "nothing in front".
|
|
214
|
+
return new Vector4(0, 0, 0, REVERSE_Z ? 0 : 1);
|
|
215
|
+
}
|
|
216
|
+
const farthest = REVERSE_Z ? 0 : 1;
|
|
217
|
+
return new Vector4(farthest, farthest, farthest, 1);
|
|
218
|
+
}
|
|
219
|
+
getShadowMapColorFormat(_shadowMapParams) {
|
|
220
|
+
// Null keeps the framebuffer the caller allocates depth-only. The colour this
|
|
221
|
+
// technique needs is allocated here, against framebuffers of its own, so
|
|
222
|
+
// letting the caller also allocate a full-size colour attachment would waste
|
|
223
|
+
// it outright.
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
getShadowMapDepthFormat(_shadowMapParams) {
|
|
227
|
+
return this.preferredShadowMapDepthFormat();
|
|
228
|
+
}
|
|
229
|
+
useNativeShadowMap(_shadowMapParams) {
|
|
230
|
+
// The receiver needs the actual stored values - a depth to measure offsets
|
|
231
|
+
// from and three sums to interpolate - not the result of a hardware compare.
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
getShadowMap(shadowMapParams) {
|
|
235
|
+
return shadowMapParams.implData.result;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Format for both targets.
|
|
239
|
+
*
|
|
240
|
+
* @remarks
|
|
241
|
+
* Full float wherever the accumulation pass can blend into it, and the reason
|
|
242
|
+
* is the anchor depth, not the absorption sums. `z0` is stored absolute, in
|
|
243
|
+
* units of the shadow camera's whole depth range, and every layer coordinate
|
|
244
|
+
* on both sides of the technique is a difference against it divided by the
|
|
245
|
+
* layer span. At half precision its quantisation step is about 2^-11 of the
|
|
246
|
+
* stored value - roughly `0.0005 * depthRange` in world units - which for a
|
|
247
|
+
* shadow camera spanning a large scene is comparable to the depth of the hair
|
|
248
|
+
* itself. The error is independent per texel, so it does not read as bias but
|
|
249
|
+
* as blotches: patches of a backlit groom whose receivers land in front of
|
|
250
|
+
* their quantised `z0` and come out fully lit. A layer span large enough to
|
|
251
|
+
* drown the noise hides it, but throws away the graded falloff the technique
|
|
252
|
+
* exists for.
|
|
253
|
+
*
|
|
254
|
+
* Half float remains the fallback for devices whose blending cannot target
|
|
255
|
+
* full float; there the practical mitigation is a tight shadow distance.
|
|
256
|
+
* @internal
|
|
257
|
+
*/ colorFormat() {
|
|
258
|
+
const caps = getDevice().getDeviceCaps().textureCaps;
|
|
259
|
+
if (caps.supportFloatColorBuffer && caps.supportFloatBlending) {
|
|
260
|
+
return 'rgba32f';
|
|
261
|
+
}
|
|
262
|
+
return caps.supportHalfFloatColorBuffer ? 'rgba16f' : 'rgba32f';
|
|
263
|
+
}
|
|
264
|
+
doUpdateResources(shadowMapParams) {
|
|
265
|
+
const device = getDevice();
|
|
266
|
+
const sharedDepth = shadowMapParams.shadowMapFramebuffer.getDepthAttachment();
|
|
267
|
+
const width = sharedDepth.width;
|
|
268
|
+
const height = sharedDepth.height;
|
|
269
|
+
const format = this.colorFormat();
|
|
270
|
+
// Everything here is pooled, per frame, the way the blur targets of VSM and
|
|
271
|
+
// ESM are. `implData` cannot cache across frames even though it looks like it
|
|
272
|
+
// could: it lives on a ShadowMapParams that is recycled between every light
|
|
273
|
+
// and cleared at the end of each frame, so holding directly-created textures
|
|
274
|
+
// in it leaks them at whatever rate the scene renders.
|
|
275
|
+
const frontDepth = device.pool.fetchTemporalTexture2D(false, format, width, height, false);
|
|
276
|
+
const result = device.pool.fetchTemporalTexture2D(false, format, width, height, false);
|
|
277
|
+
// Pass 0 borrows the shadow map's own depth attachment, which is otherwise
|
|
278
|
+
// unused - this implementation asks for no colour there - so the hardware
|
|
279
|
+
// depth test resolves the frontmost strand for it. Pass 1 gets no depth,
|
|
280
|
+
// which is both what stops it testing and what frees `frontDepth` to be
|
|
281
|
+
// sampled while `result` is written.
|
|
282
|
+
const depthFramebuffer = device.pool.createTemporalFramebuffer(true, [
|
|
283
|
+
frontDepth
|
|
284
|
+
], sharedDepth);
|
|
285
|
+
const opacityFramebuffer = device.pool.createTemporalFramebuffer(true, [
|
|
286
|
+
result
|
|
287
|
+
], null);
|
|
288
|
+
// The framebuffers hold the references now; these drop the fetch counts so
|
|
289
|
+
// both textures return to the pool when the frame ends.
|
|
290
|
+
device.pool.releaseTexture(frontDepth);
|
|
291
|
+
device.pool.releaseTexture(result);
|
|
292
|
+
shadowMapParams.implData = {
|
|
293
|
+
frontDepth,
|
|
294
|
+
result,
|
|
295
|
+
depthFramebuffer,
|
|
296
|
+
opacityFramebuffer
|
|
297
|
+
};
|
|
298
|
+
shadowMapParams.shadowMap = result;
|
|
299
|
+
shadowMapParams.shadowMapSampler = result.getDefaultSampler(false);
|
|
300
|
+
this._resourceDirty = false;
|
|
301
|
+
}
|
|
302
|
+
postRenderShadowMap(_shadowMapParams) {}
|
|
303
|
+
getShaderHash() {
|
|
304
|
+
// The two geometry passes emit different caster fragments and the shadow pass
|
|
305
|
+
// keys its programs on this hash, so the pass index has to appear in it. The
|
|
306
|
+
// kernel is unrolled into the receiver, so it belongs here too.
|
|
307
|
+
return `${DOM_LAYER_COUNT}_${this._geometryPass}_${this._filterSize}`;
|
|
308
|
+
}
|
|
309
|
+
declareCasterUniforms(scope, _shadowMapParams) {
|
|
310
|
+
if (this._geometryPass !== 1) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const pb = scope.$builder;
|
|
314
|
+
// sampleType before uniform(), matching how the receiver's shadow map is
|
|
315
|
+
// declared: the binding is created from the accumulated descriptor.
|
|
316
|
+
const tex = pb.tex2D();
|
|
317
|
+
tex.sampleType('unfilterable-float');
|
|
318
|
+
scope[UNIFORM_NAME_DOM_FRONT_DEPTH] = tex.uniform(0);
|
|
319
|
+
}
|
|
320
|
+
applyCasterUniforms(bindGroup, shadowMapParams) {
|
|
321
|
+
if (this._geometryPass !== 1) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const implData = shadowMapParams.implData;
|
|
325
|
+
bindGroup.setTexture(UNIFORM_NAME_DOM_FRONT_DEPTH, implData.frontDepth, fetchSampler('clamp_nearest_nomip'));
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* How far a depth lies behind the frontmost strand, in layer-span units.
|
|
329
|
+
*
|
|
330
|
+
* @remarks
|
|
331
|
+
* Both callers measure "deeper into the hair than z0", but which direction that
|
|
332
|
+
* is depends on the depth encoding: under reverse-Z a farther surface has the
|
|
333
|
+
* *smaller* value, so the plain subtraction is negative for every fragment
|
|
334
|
+
* behind the front and the clamp below flattens the whole technique to no
|
|
335
|
+
* shadow at all.
|
|
336
|
+
* @internal
|
|
337
|
+
*/ layerCoord(scope, depth, z0, layerSpan) {
|
|
338
|
+
const pb = scope.$builder;
|
|
339
|
+
const delta = REVERSE_Z ? pb.sub(z0, depth) : pb.sub(depth, z0);
|
|
340
|
+
return pb.div(pb.max(delta, 0), layerSpan);
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Caster output.
|
|
344
|
+
*
|
|
345
|
+
* Pass 0 writes the frontmost depth. Pass 1 writes this fragment's optical depth
|
|
346
|
+
* into the layers behind it, for additive blending to sum, and carries z0
|
|
347
|
+
* through alpha so the receiver can find where the layers start.
|
|
348
|
+
*/ computeShadowMapDepth(shadowMapParams, scope, worldPos, alpha) {
|
|
349
|
+
const pb = scope.$builder;
|
|
350
|
+
const that = this;
|
|
351
|
+
const geometryPass = this._geometryPass;
|
|
352
|
+
// Alpha travels as a parameter. Reaching into the caller's scope for its
|
|
353
|
+
// `outColor` would be reading a local of a different shader function, and the
|
|
354
|
+
// two cases that matter - an opaque caster and a fully transparent one - are
|
|
355
|
+
// the same value there.
|
|
356
|
+
const hasAlpha = geometryPass === 1 && !!alpha;
|
|
357
|
+
const funcName = `lib_domCasterOutput_${geometryPass}_${hasAlpha ? 'a' : 'o'}`;
|
|
358
|
+
const params = hasAlpha ? [
|
|
359
|
+
pb.vec3('worldPos'),
|
|
360
|
+
pb.float('alpha')
|
|
361
|
+
] : [
|
|
362
|
+
pb.vec3('worldPos')
|
|
363
|
+
];
|
|
364
|
+
pb.func(funcName, params, function() {
|
|
365
|
+
this.$l.depth = that.casterDepth(shadowMapParams, this, this.worldPos);
|
|
366
|
+
if (geometryPass === 0) {
|
|
367
|
+
this.$return(pb.vec4(this.depth, 0, 0, 1));
|
|
368
|
+
} else {
|
|
369
|
+
this.$l.z0 = pb.textureLoad(this[UNIFORM_NAME_DOM_FRONT_DEPTH], pb.ivec2(this.$builtins.fragCoord.xy), 0).x;
|
|
370
|
+
this.$l.layerSpan = ShaderHelper.getShadowImplParams(this).x;
|
|
371
|
+
// Distance behind the frontmost strand, in layer-span units. A fragment at
|
|
372
|
+
// or in front of z0 lands at zero and fills every layer.
|
|
373
|
+
this.$l.t = that.layerCoord(this, this.depth, this.z0, this.layerSpan);
|
|
374
|
+
// Layer k's far boundary sits at (k+1)/3 of the span, and a fragment
|
|
375
|
+
// contributes to every layer whose boundary lies behind it. The last
|
|
376
|
+
// layer has no far boundary: it extends to the end of the caster volume,
|
|
377
|
+
// so a fragment deeper than the span still lands in it and the third
|
|
378
|
+
// layer holds the cumulative absorption of *everything* behind z0.
|
|
379
|
+
// Dropping deep fragments instead - step(t, 1) - deletes the hair behind
|
|
380
|
+
// the front slice from the map entirely, and any receiver deeper than
|
|
381
|
+
// the span reads only that slice: wherever it happens to be sparse, the
|
|
382
|
+
// middle of a dense backlit groom comes out fully lit in blotches.
|
|
383
|
+
this.$l.mask = pb.vec3(pb.step(this.t, 1 / 3), pb.step(this.t, 2 / 3), 1);
|
|
384
|
+
// A caster that computes no fragment colour covers fully.
|
|
385
|
+
this.$l.casterAlpha = hasAlpha ? this.alpha : pb.float(1);
|
|
386
|
+
// Optical depth rather than raw coverage, because what has to be additive
|
|
387
|
+
// here is the exponent, not the alpha. Compositing N layers multiplies
|
|
388
|
+
// their transmittances - prod(1 - a) - and blending can only sum, so each
|
|
389
|
+
// fragment contributes -log(1 - a) and the receiver's exp(-sum) reproduces
|
|
390
|
+
// that product exactly.
|
|
391
|
+
//
|
|
392
|
+
// Summing alpha instead makes exp(-sum) an approximation that only holds
|
|
393
|
+
// while alpha is small, which is true of a sub-pixel strand and false of
|
|
394
|
+
// anything solid: a single opaque caster would sum to 1 and shadow to
|
|
395
|
+
// exp(-1), a 37% grey rather than black.
|
|
396
|
+
//
|
|
397
|
+
// The floor bounds what alpha = 1 encodes. It has to be finite to blend,
|
|
398
|
+
// and 0.001 transmittance is already black; it also leaves room for
|
|
399
|
+
// several opaque layers to stack without leaving half precision.
|
|
400
|
+
this.$l.opticalDepth = pb.neg(pb.log(pb.max(pb.sub(1, this.casterAlpha), DOM_MIN_TRANSMITTANCE)));
|
|
401
|
+
this.$return(pb.vec4(pb.mul(this.mask, this.opticalDepth), this.z0));
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
const global = pb.getGlobalScope();
|
|
405
|
+
return hasAlpha ? global[funcName](worldPos, alpha) : global[funcName](worldPos);
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Normalized light-space depth of a caster fragment, in the encoding the
|
|
409
|
+
* receiver reconstructs.
|
|
410
|
+
* @internal
|
|
411
|
+
*/ casterDepth(shadowMapParams, scope, worldPos) {
|
|
412
|
+
const pb = scope.$builder;
|
|
413
|
+
if (shadowMapParams.lightType === LIGHT_TYPE_POINT) {
|
|
414
|
+
const lightSpacePos = pb.mul(ShaderHelper.getLightViewMatrixForShadow(scope), pb.vec4(worldPos, 1));
|
|
415
|
+
return pb.clamp(pb.div(pb.length(lightSpacePos.xyz), ShaderHelper.getLightPositionAndRangeForShadow(scope).w), 0, 1);
|
|
416
|
+
}
|
|
417
|
+
return pb.emulateDepthClamp ? pb.clamp(scope.$inputs.clamppedDepth, 0, 1) : scope.$builtins.fragCoord.z;
|
|
418
|
+
}
|
|
419
|
+
computeShadow(shadowMapParams, scope, shadowVertex, NdotL) {
|
|
420
|
+
const funcName = 'lib_computeShadowDOM';
|
|
421
|
+
const pb = scope.$builder;
|
|
422
|
+
const that = this;
|
|
423
|
+
pb.func(funcName, [
|
|
424
|
+
pb.vec4('shadowVertex'),
|
|
425
|
+
pb.float('NdotL')
|
|
426
|
+
], function() {
|
|
427
|
+
this.$l.shadowCoord = pb.div(this.shadowVertex.xyz, this.shadowVertex.w);
|
|
428
|
+
this.$l.shadowCoord = ndcToShadowCoord3(this, this.shadowCoord.xyz);
|
|
429
|
+
this.$l.inRange = pb.all(pb.bvec2(pb.all(pb.bvec4(pb.greaterThanEqual(this.shadowCoord.x, 0), pb.lessThanEqual(this.shadowCoord.x, 1), pb.greaterThanEqual(this.shadowCoord.y, 0), pb.lessThanEqual(this.shadowCoord.y, 1))), shadowCoordDepthInRange(this, this.shadowCoord.z)));
|
|
430
|
+
this.$l.shadow = pb.float(1);
|
|
431
|
+
this.$if(this.inRange, function() {
|
|
432
|
+
// A single tap needs no bias: it reads the texel the receiver itself
|
|
433
|
+
// wrote, so its own surface sits exactly at z0 and contributes nothing.
|
|
434
|
+
// A kernel breaks that. Neighbouring texels record the frontmost surface
|
|
435
|
+
// at their own position, which on anything sloped is nearer the light
|
|
436
|
+
// than the receiver, and the receiver then reads itself as being behind a
|
|
437
|
+
// caster. On a solid surface that is the full jump from no absorption to
|
|
438
|
+
// all of it, so it shows up as the banding an unbiased shadow map has.
|
|
439
|
+
if (that._filterSize > 1) {
|
|
440
|
+
this.$l.bias = computeShadowBias(shadowMapParams.lightType, this, this.shadowCoord.z, this.NdotL, false);
|
|
441
|
+
this.shadowCoord.z = applyShadowDepthBias(this, this.shadowCoord.z, this.bias, true);
|
|
442
|
+
}
|
|
443
|
+
this.shadow = that.transmittance(this, this.shadowCoord);
|
|
444
|
+
});
|
|
445
|
+
this.$return(this.shadow);
|
|
446
|
+
});
|
|
447
|
+
return pb.getGlobalScope()[funcName](shadowVertex, NdotL);
|
|
448
|
+
}
|
|
449
|
+
computeShadowCSM(shadowMapParams, scope, shadowVertex, NdotL, _split) {
|
|
450
|
+
// Cascades would need a depth and a layer set per split, which no longer fits
|
|
451
|
+
// the one texture the single shadow-map binding allows. Callers keep working
|
|
452
|
+
// by shadowing from the first cascade's map.
|
|
453
|
+
return this.computeShadow(shadowMapParams, scope, shadowVertex, NdotL);
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Fraction of light surviving the hair in front of the receiver, averaged over
|
|
457
|
+
* the filter kernel.
|
|
458
|
+
*
|
|
459
|
+
* @remarks
|
|
460
|
+
* The averaging is over transmittance, one value per tap, rather than over the
|
|
461
|
+
* stored texels. Blending the texels first would interpolate `z0` across the
|
|
462
|
+
* silhouette of the groom, where one side holds a real strand depth and the
|
|
463
|
+
* other the empty-texel sentinel, and the depth halfway between them describes
|
|
464
|
+
* nothing - every layer reading taken against it would be measured from the
|
|
465
|
+
* wrong origin. Averaging the results instead asks a well-posed question of
|
|
466
|
+
* each texel and combines only the answers, which is also what makes this an
|
|
467
|
+
* antialiased edge rather than a blurred one.
|
|
468
|
+
* @internal
|
|
469
|
+
*/ transmittance(scope, shadowCoord) {
|
|
470
|
+
const pb = scope.$builder;
|
|
471
|
+
const that = this;
|
|
472
|
+
const size = this._filterSize;
|
|
473
|
+
const funcName = `lib_domTransmittance_${size}`;
|
|
474
|
+
pb.func(funcName, [
|
|
475
|
+
pb.vec3('shadowCoord')
|
|
476
|
+
], function() {
|
|
477
|
+
if (size <= 1) {
|
|
478
|
+
this.$return(that.tapTransmittance(this, this.shadowCoord.xy, this.shadowCoord.z));
|
|
479
|
+
} else {
|
|
480
|
+
// cameraParams.z carries the shadow map size, and this implementation
|
|
481
|
+
// never splits into cascades, so the map is exactly that wide.
|
|
482
|
+
this.$l.texelSize = pb.div(1, ShaderHelper.getShadowCameraParams(this).z);
|
|
483
|
+
this.$l.sum = pb.float(0);
|
|
484
|
+
this.$l.tapUV = pb.vec2();
|
|
485
|
+
const radius = (size - 1) / 2;
|
|
486
|
+
for(let y = -radius; y <= radius; y++){
|
|
487
|
+
for(let x = -radius; x <= radius; x++){
|
|
488
|
+
this.tapUV = pb.add(this.shadowCoord.xy, pb.mul(pb.vec2(x, y), this.texelSize));
|
|
489
|
+
this.sum = pb.add(this.sum, that.tapTransmittance(this, this.tapUV, this.shadowCoord.z));
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
this.$return(pb.div(this.sum, size * size));
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
return pb.getGlobalScope()[funcName](shadowCoord);
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Transmittance from a single shadow map texel.
|
|
499
|
+
* @internal
|
|
500
|
+
*/ tapTransmittance(scope, uv, receiverDepth) {
|
|
501
|
+
const pb = scope.$builder;
|
|
502
|
+
const that = this;
|
|
503
|
+
const funcName = 'lib_domTap';
|
|
504
|
+
pb.func(funcName, [
|
|
505
|
+
pb.vec2('uv'),
|
|
506
|
+
pb.float('receiverDepth')
|
|
507
|
+
], function() {
|
|
508
|
+
this.$l.texel = pb.textureSampleLevel(ShaderHelper.getShadowMap(this), this.uv, 0);
|
|
509
|
+
this.$l.layers = this.texel.rgb;
|
|
510
|
+
this.$l.z0 = this.texel.a;
|
|
511
|
+
this.$l.layerSpan = ShaderHelper.getShadowImplParams(this).x;
|
|
512
|
+
this.$l.density = ShaderHelper.getShadowImplParams(this).y;
|
|
513
|
+
this.$l.t = that.layerCoord(this, this.receiverDepth, this.z0, this.layerSpan);
|
|
514
|
+
// The layers are cumulative, so opacity at an arbitrary depth is a lerp
|
|
515
|
+
// between the two bracketing layer values. Scaling t by the layer count puts
|
|
516
|
+
// the integer part on the near layer and the fraction between the pair.
|
|
517
|
+
this.$l.scaled = pb.clamp(pb.mul(this.t, DOM_LAYER_COUNT), 0, DOM_LAYER_COUNT);
|
|
518
|
+
this.$l.index = pb.floor(this.scaled);
|
|
519
|
+
this.$l.frac = pb.sub(this.scaled, this.index);
|
|
520
|
+
// Nothing has been traversed before the first layer, so the near value at
|
|
521
|
+
// index 0 is zero rather than layers.x.
|
|
522
|
+
this.$l.near = pb.float(0);
|
|
523
|
+
this.$l.far = this.layers.x;
|
|
524
|
+
this.$if(pb.greaterThanEqual(this.index, 2), function() {
|
|
525
|
+
this.near = this.layers.y;
|
|
526
|
+
this.far = this.layers.z;
|
|
527
|
+
}).$elseif(pb.greaterThanEqual(this.index, 1), function() {
|
|
528
|
+
this.near = this.layers.x;
|
|
529
|
+
this.far = this.layers.y;
|
|
530
|
+
});
|
|
531
|
+
// Optical depth is what accumulates linearly through a medium, so the lerp
|
|
532
|
+
// between two layer readings is meaningful in a way that lerping stored
|
|
533
|
+
// alphas would not have been.
|
|
534
|
+
this.$l.absorption = pb.mix(this.near, this.far, this.frac);
|
|
535
|
+
// Beer-Lambert. With the casters recording -log(1 - alpha) this is exactly
|
|
536
|
+
// prod(1 - alpha) over everything crossed, so a solid caster reaches black
|
|
537
|
+
// and a stack of thin strands reaches the same value alpha compositing them
|
|
538
|
+
// would have.
|
|
539
|
+
this.$return(pb.exp(pb.neg(pb.mul(this.absorption, this.density))));
|
|
540
|
+
});
|
|
541
|
+
return pb.getGlobalScope()[funcName](uv, receiverDepth);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
export { DOM };
|
|
546
|
+
//# sourceMappingURL=dom.js.map
|