@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,1090 @@
|
|
|
1
|
+
import { Disposable, Vector3, Matrix4x4, Quaternion } from '@zephyr3d/base';
|
|
2
|
+
import { getDevice } from '../../app/api.js';
|
|
3
|
+
import { updateColliderFromNode } from '../spring/spring_collider.js';
|
|
4
|
+
|
|
5
|
+
/** Simulation step, in seconds. @internal */ const FIXED_SIMULATION_TIME_STEP = 1 / 60;
|
|
6
|
+
/** Largest backlog the accumulator will carry, in seconds. @internal */ const MAX_ACCUMULATED_SIMULATION_TIME = 1 / 20;
|
|
7
|
+
/** Threads per workgroup; one thread owns one strand. @internal */ const DEFAULT_WORKGROUP_SIZE = 64;
|
|
8
|
+
/** Length below which a direction is treated as degenerate. @internal */ const MIN_DISTANCE = 1e-7;
|
|
9
|
+
/** Floats per collider entry, matching the layouts the shader indexes. @internal */ const SPHERE_STRIDE = 4;
|
|
10
|
+
const CAPSULE_STRIDE = 8;
|
|
11
|
+
const PLANE_STRIDE = 8;
|
|
12
|
+
/** Colliders of one kind the buffers make room for. @internal */ const MAX_COLLIDERS = 16;
|
|
13
|
+
/** Solver defaults, from TressFXSimulationSettings. @internal */ const DEFAULT_DAMPING = 0.08;
|
|
14
|
+
const DEFAULT_GLOBAL_STIFFNESS = 0;
|
|
15
|
+
const DEFAULT_GLOBAL_RANGE = 0;
|
|
16
|
+
const DEFAULT_LOCAL_STIFFNESS = 0.9;
|
|
17
|
+
const DEFAULT_LOCAL_ITERATIONS = 2;
|
|
18
|
+
const DEFAULT_FTL_DAMPING = 0.7;
|
|
19
|
+
const DEFAULT_VSP_COEFF = 0.8;
|
|
20
|
+
/**
|
|
21
|
+
* Roughly five gravities. TressFX compares a per-frame second difference
|
|
22
|
+
* against 1.4 in its own units; as an acceleration it survives a unit change.
|
|
23
|
+
* @internal
|
|
24
|
+
*/ const DEFAULT_VSP_ACCEL_THRESHOLD = 50;
|
|
25
|
+
/** Default per-substep speed ceiling, in segment lengths. @internal */ const DEFAULT_MAX_SPEED_FACTOR = 4;
|
|
26
|
+
/**
|
|
27
|
+
* Node motion beyond this many strand lengths in one frame is treated as a
|
|
28
|
+
* teleport and snaps the pose instead of swinging it.
|
|
29
|
+
* @internal
|
|
30
|
+
*/ const TELEPORT_DISTANCE_FACTOR = 4;
|
|
31
|
+
/**
|
|
32
|
+
* Whether GPU hair dynamics can run on a device.
|
|
33
|
+
*
|
|
34
|
+
* @remarks
|
|
35
|
+
* WebGPU only: the solver is a compute pass. Strand rendering carries the same
|
|
36
|
+
* requirement, so there is no configuration where the hair draws but cannot
|
|
37
|
+
* simulate.
|
|
38
|
+
*
|
|
39
|
+
* @param device - Device to test, or the current one when omitted.
|
|
40
|
+
* @returns True when the simulation can run.
|
|
41
|
+
*
|
|
42
|
+
* @public
|
|
43
|
+
*/ function isHairSimulationSupported(device) {
|
|
44
|
+
const resolved = device ?? getDevice();
|
|
45
|
+
return resolved?.type === 'webgpu';
|
|
46
|
+
}
|
|
47
|
+
function clamp(value, min, max) {
|
|
48
|
+
return value < min ? min : value > max ? max : value;
|
|
49
|
+
}
|
|
50
|
+
/** Exact element-wise matrix comparison, for skipping the interpolation. @internal */ function matrixEquals(a, b) {
|
|
51
|
+
for(let i = 0; i < 16; i++){
|
|
52
|
+
if (a[i] !== b[i]) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Strand dynamics on the GPU.
|
|
60
|
+
*
|
|
61
|
+
* @remarks
|
|
62
|
+
* Created by {@link HairNode} when simulation is switched on; it writes the
|
|
63
|
+
* strand point buffer in place, so nothing about the draw call changes.
|
|
64
|
+
*
|
|
65
|
+
* @public
|
|
66
|
+
*/ class GPUHairSimulation extends Disposable {
|
|
67
|
+
/** @internal */ _device;
|
|
68
|
+
/** @internal */ _enabled;
|
|
69
|
+
/** @internal */ _disabledReason;
|
|
70
|
+
/** @internal */ _program;
|
|
71
|
+
/** @internal */ _bindGroup;
|
|
72
|
+
/** @internal Live point positions; the same buffer the vertex shader reads. */ _pointBuffer;
|
|
73
|
+
/** @internal Previous positions, for Verlet integration and motion vectors. */ _prevPointBuffer;
|
|
74
|
+
/** @internal Authored positions, the target the pose pull reaches for. */ _restPointBuffer;
|
|
75
|
+
/**
|
|
76
|
+
* @internal Local positions as of the previously rendered frame, which is what
|
|
77
|
+
* a motion vector needs. Distinct from the Verlet history: that is one substep
|
|
78
|
+
* old, sits in the current frame's local space, and has the length, contact and
|
|
79
|
+
* friction corrections folded into it to encode velocity, so it is not a
|
|
80
|
+
* position anything was ever drawn at.
|
|
81
|
+
*/ _framePointBuffer;
|
|
82
|
+
/** @internal Authored distance between consecutive control points. */ _restLengthBuffer;
|
|
83
|
+
/** @internal */ _sphereBuffer;
|
|
84
|
+
/** @internal */ _capsuleBuffer;
|
|
85
|
+
/** @internal */ _planeBuffer;
|
|
86
|
+
/** @internal */ _strandCount;
|
|
87
|
+
/** @internal */ _workgroupCount;
|
|
88
|
+
/** @internal */ _gravity;
|
|
89
|
+
/** @internal */ _damping;
|
|
90
|
+
/** @internal */ _globalStiffness;
|
|
91
|
+
/** @internal */ _globalRange;
|
|
92
|
+
/** @internal */ _localStiffness;
|
|
93
|
+
/** @internal */ _localIterations;
|
|
94
|
+
/** @internal */ _ftlDamping;
|
|
95
|
+
/** @internal */ _vspCoeff;
|
|
96
|
+
/** @internal */ _vspAccelThreshold;
|
|
97
|
+
/** @internal */ _substeps;
|
|
98
|
+
/** @internal */ _friction;
|
|
99
|
+
/** @internal */ _maxSpeedFactor;
|
|
100
|
+
/** @internal */ _colliders;
|
|
101
|
+
/** @internal */ _timeAccumulator;
|
|
102
|
+
/** @internal Scratch, so stepping allocates nothing. */ _sphereData;
|
|
103
|
+
_capsuleData;
|
|
104
|
+
_planeData;
|
|
105
|
+
_relativeTransform;
|
|
106
|
+
_invWorldMatrix;
|
|
107
|
+
_prevWorldMatrix;
|
|
108
|
+
/** @internal Bound while the node stands still, so those frames upload nothing new. */ _identity;
|
|
109
|
+
_localGravity;
|
|
110
|
+
_scratchVec;
|
|
111
|
+
_scratchVec2;
|
|
112
|
+
/** @internal Endpoints and scratch of the per-substep transform interpolation. */ _scaleFrom;
|
|
113
|
+
_scaleTo;
|
|
114
|
+
_translationFrom;
|
|
115
|
+
_translationTo;
|
|
116
|
+
_rotationFrom;
|
|
117
|
+
_rotationTo;
|
|
118
|
+
_rotationLerp;
|
|
119
|
+
_stepFrom;
|
|
120
|
+
_stepTo;
|
|
121
|
+
/** @internal True until the first step has seen a world matrix. */ _hasPrevWorldMatrix;
|
|
122
|
+
/** @internal Length of the longest strand, for the teleport threshold. */ _maxStrandLength;
|
|
123
|
+
/** @internal Centre of the rest pose, whose world motion detects teleports. */ _restCenter;
|
|
124
|
+
/**
|
|
125
|
+
* @internal Where the rest centre stood in the world last frame and how fast
|
|
126
|
+
* it was moving. The roots are pinned in local space, so their acceleration is
|
|
127
|
+
* the node's, which is what the shock propagation threshold tests.
|
|
128
|
+
*/ _prevRootWorld;
|
|
129
|
+
_prevRootVelocity;
|
|
130
|
+
/** @internal Frames seen so far, capped at 2; acceleration needs three. */ _rootHistoryDepth;
|
|
131
|
+
/**
|
|
132
|
+
* Creates a simulation over an uploaded strand set.
|
|
133
|
+
*
|
|
134
|
+
* @param strands - GPU strand data; its point buffer is written in place.
|
|
135
|
+
* @param source - The control points the strands were built from, used to
|
|
136
|
+
* derive rest lengths and the rest pose.
|
|
137
|
+
* @param options - Tuning.
|
|
138
|
+
*/ constructor(strands, source, options){
|
|
139
|
+
super();
|
|
140
|
+
this._device = getDevice();
|
|
141
|
+
this._enabled = false;
|
|
142
|
+
this._disabledReason = null;
|
|
143
|
+
this._program = null;
|
|
144
|
+
this._bindGroup = null;
|
|
145
|
+
this._pointBuffer = null;
|
|
146
|
+
this._prevPointBuffer = null;
|
|
147
|
+
this._restPointBuffer = null;
|
|
148
|
+
this._framePointBuffer = null;
|
|
149
|
+
this._restLengthBuffer = null;
|
|
150
|
+
this._sphereBuffer = null;
|
|
151
|
+
this._capsuleBuffer = null;
|
|
152
|
+
this._planeBuffer = null;
|
|
153
|
+
this._strandCount = strands.strandCount;
|
|
154
|
+
this._gravity = options?.gravity ? new Vector3(options.gravity) : new Vector3(0, -9.8, 0);
|
|
155
|
+
this._damping = clamp(options?.damping ?? DEFAULT_DAMPING, 0, 1);
|
|
156
|
+
this._globalStiffness = clamp(options?.globalStiffness ?? DEFAULT_GLOBAL_STIFFNESS, 0, 1);
|
|
157
|
+
this._globalRange = clamp(options?.globalRange ?? DEFAULT_GLOBAL_RANGE, 0, 1);
|
|
158
|
+
this._localStiffness = clamp(options?.localStiffness ?? DEFAULT_LOCAL_STIFFNESS, 0, 1);
|
|
159
|
+
this._localIterations = clamp(options?.localIterations ?? DEFAULT_LOCAL_ITERATIONS, 1, 8) | 0;
|
|
160
|
+
this._ftlDamping = clamp(options?.ftlDamping ?? DEFAULT_FTL_DAMPING, 0, 1);
|
|
161
|
+
this._vspCoeff = clamp(options?.vspCoeff ?? DEFAULT_VSP_COEFF, 0, 1);
|
|
162
|
+
this._vspAccelThreshold = Math.max(0, options?.vspAccelThreshold ?? DEFAULT_VSP_ACCEL_THRESHOLD);
|
|
163
|
+
this._substeps = clamp(options?.substeps ?? 1, 1, 8) | 0;
|
|
164
|
+
this._friction = clamp(options?.friction ?? 0.2, 0, 1);
|
|
165
|
+
this._maxSpeedFactor = Math.max(0, options?.maxSpeedFactor ?? DEFAULT_MAX_SPEED_FACTOR);
|
|
166
|
+
this._colliders = options?.colliders ? [
|
|
167
|
+
...options.colliders
|
|
168
|
+
] : [];
|
|
169
|
+
this._timeAccumulator = 0;
|
|
170
|
+
this._sphereData = new Float32Array(MAX_COLLIDERS * SPHERE_STRIDE);
|
|
171
|
+
this._capsuleData = new Float32Array(MAX_COLLIDERS * CAPSULE_STRIDE);
|
|
172
|
+
this._planeData = new Float32Array(MAX_COLLIDERS * PLANE_STRIDE);
|
|
173
|
+
this._relativeTransform = Matrix4x4.identity();
|
|
174
|
+
this._invWorldMatrix = Matrix4x4.identity();
|
|
175
|
+
this._prevWorldMatrix = Matrix4x4.identity();
|
|
176
|
+
this._identity = Matrix4x4.identity();
|
|
177
|
+
this._localGravity = new Vector3();
|
|
178
|
+
this._scratchVec = new Vector3();
|
|
179
|
+
this._scratchVec2 = new Vector3();
|
|
180
|
+
this._scaleFrom = new Vector3();
|
|
181
|
+
this._scaleTo = new Vector3();
|
|
182
|
+
this._translationFrom = new Vector3();
|
|
183
|
+
this._translationTo = new Vector3();
|
|
184
|
+
this._rotationFrom = new Quaternion();
|
|
185
|
+
this._rotationTo = new Quaternion();
|
|
186
|
+
this._rotationLerp = new Quaternion();
|
|
187
|
+
this._stepFrom = Matrix4x4.identity();
|
|
188
|
+
this._stepTo = Matrix4x4.identity();
|
|
189
|
+
this._hasPrevWorldMatrix = false;
|
|
190
|
+
this._maxStrandLength = 0;
|
|
191
|
+
this._restCenter = new Vector3();
|
|
192
|
+
this._prevRootWorld = new Vector3();
|
|
193
|
+
this._prevRootVelocity = new Vector3();
|
|
194
|
+
this._rootHistoryDepth = 0;
|
|
195
|
+
this._workgroupCount = Math.ceil(this._strandCount / DEFAULT_WORKGROUP_SIZE);
|
|
196
|
+
if (!isHairSimulationSupported(this._device)) {
|
|
197
|
+
this._disabledReason = 'GPU hair simulation is disabled: current backend is not WebGPU.';
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (!strands.pointBuffer || !strands.headerBuffer) {
|
|
201
|
+
this._disabledReason = 'GPU hair simulation is disabled: strand data has no GPU buffers.';
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
this._init(strands, source);
|
|
206
|
+
this._enabled = true;
|
|
207
|
+
} catch (err) {
|
|
208
|
+
this._disabledReason = `GPU hair simulation is disabled: ${err}`;
|
|
209
|
+
this._releaseResources();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/** Whether the simulation is running. */ get enabled() {
|
|
213
|
+
return this._enabled;
|
|
214
|
+
}
|
|
215
|
+
/** Why the simulation is not running, when it is not. */ get disabledReason() {
|
|
216
|
+
return this._disabledReason;
|
|
217
|
+
}
|
|
218
|
+
/** World-space gravity. */ get gravity() {
|
|
219
|
+
return this._gravity;
|
|
220
|
+
}
|
|
221
|
+
set gravity(value) {
|
|
222
|
+
this._gravity = new Vector3(value);
|
|
223
|
+
}
|
|
224
|
+
/** Velocity lost per step, in [0, 1]. */ get damping() {
|
|
225
|
+
return this._damping;
|
|
226
|
+
}
|
|
227
|
+
set damping(value) {
|
|
228
|
+
this._damping = clamp(value, 0, 1);
|
|
229
|
+
}
|
|
230
|
+
/** Per-substep ceiling on point travel, in segment lengths. */ get maxSpeedFactor() {
|
|
231
|
+
return this._maxSpeedFactor;
|
|
232
|
+
}
|
|
233
|
+
set maxSpeedFactor(value) {
|
|
234
|
+
this._maxSpeedFactor = Math.max(0, value);
|
|
235
|
+
}
|
|
236
|
+
/** Pull back to the authored pose per fixed step, in [0, 1]. Off by default. */ get globalStiffness() {
|
|
237
|
+
return this._globalStiffness;
|
|
238
|
+
}
|
|
239
|
+
set globalStiffness(value) {
|
|
240
|
+
this._globalStiffness = clamp(value, 0, 1);
|
|
241
|
+
}
|
|
242
|
+
/** Fraction of a strand, from the root, the pose pull acts on, in [0, 1]. */ get globalRange() {
|
|
243
|
+
return this._globalRange;
|
|
244
|
+
}
|
|
245
|
+
set globalRange(value) {
|
|
246
|
+
this._globalRange = clamp(value, 0, 1);
|
|
247
|
+
}
|
|
248
|
+
/** How strongly a strand keeps its authored shape, in [0, 1]. */ get localStiffness() {
|
|
249
|
+
return this._localStiffness;
|
|
250
|
+
}
|
|
251
|
+
set localStiffness(value) {
|
|
252
|
+
this._localStiffness = clamp(value, 0, 1);
|
|
253
|
+
}
|
|
254
|
+
/** Local shape constraint passes per substep. */ get localIterations() {
|
|
255
|
+
return this._localIterations;
|
|
256
|
+
}
|
|
257
|
+
set localIterations(value) {
|
|
258
|
+
this._localIterations = clamp(value, 1, 8) | 0;
|
|
259
|
+
}
|
|
260
|
+
/** Share of a point's length correction fed back into its parent, in [0, 1]. */ get ftlDamping() {
|
|
261
|
+
return this._ftlDamping;
|
|
262
|
+
}
|
|
263
|
+
set ftlDamping(value) {
|
|
264
|
+
this._ftlDamping = clamp(value, 0, 1);
|
|
265
|
+
}
|
|
266
|
+
/** How much of the node's motion a strand is carried along by, in [0, 1]. */ get vspCoeff() {
|
|
267
|
+
return this._vspCoeff;
|
|
268
|
+
}
|
|
269
|
+
set vspCoeff(value) {
|
|
270
|
+
this._vspCoeff = clamp(value, 0, 1);
|
|
271
|
+
}
|
|
272
|
+
/** Node acceleration above which a strand is carried rigidly, in units/s². */ get vspAccelThreshold() {
|
|
273
|
+
return this._vspAccelThreshold;
|
|
274
|
+
}
|
|
275
|
+
set vspAccelThreshold(value) {
|
|
276
|
+
this._vspAccelThreshold = Math.max(0, value);
|
|
277
|
+
}
|
|
278
|
+
/** Integration substeps per fixed step. */ get substeps() {
|
|
279
|
+
return this._substeps;
|
|
280
|
+
}
|
|
281
|
+
set substeps(value) {
|
|
282
|
+
this._substeps = clamp(value, 1, 8) | 0;
|
|
283
|
+
}
|
|
284
|
+
/** Friction on tangential contact motion, in [0, 1]. */ get friction() {
|
|
285
|
+
return this._friction;
|
|
286
|
+
}
|
|
287
|
+
set friction(value) {
|
|
288
|
+
this._friction = clamp(value, 0, 1);
|
|
289
|
+
}
|
|
290
|
+
/** Colliders the strands are pushed out of. */ get colliders() {
|
|
291
|
+
return this._colliders;
|
|
292
|
+
}
|
|
293
|
+
set colliders(value) {
|
|
294
|
+
this._colliders = [
|
|
295
|
+
...value
|
|
296
|
+
];
|
|
297
|
+
}
|
|
298
|
+
/** Verlet history. Not a previous-frame position - see the field remarks. */ get prevPointBuffer() {
|
|
299
|
+
return this._prevPointBuffer;
|
|
300
|
+
}
|
|
301
|
+
/** Local positions as of the previously rendered frame, for motion vectors. */ get framePointBuffer() {
|
|
302
|
+
return this._framePointBuffer;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Discards accumulated motion and returns every strand to its rest pose.
|
|
306
|
+
*
|
|
307
|
+
* @remarks
|
|
308
|
+
* Wanted whenever a node is moved rather than animated: a teleport would
|
|
309
|
+
* otherwise register as one enormous frame of inertia and fling the hair.
|
|
310
|
+
* {@link GPUHairSimulation.update} calls this itself when the groom jumps
|
|
311
|
+
* farther in one frame than a few times its own strand length.
|
|
312
|
+
*/ reset(worldMatrix) {
|
|
313
|
+
if (!this._enabled) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
this._timeAccumulator = 0;
|
|
317
|
+
this._prevWorldMatrix.set(worldMatrix);
|
|
318
|
+
this._hasPrevWorldMatrix = true;
|
|
319
|
+
this._rootHistoryDepth = 0;
|
|
320
|
+
this._resetLivePoints();
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Advances the simulation.
|
|
324
|
+
*
|
|
325
|
+
* @param deltaTime - Seconds since the last update.
|
|
326
|
+
* @param worldMatrix - The drawing node's current world matrix.
|
|
327
|
+
*/ update(deltaTime, worldMatrix) {
|
|
328
|
+
if (!this._enabled) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (!this._hasPrevWorldMatrix) {
|
|
332
|
+
this._prevWorldMatrix.set(worldMatrix);
|
|
333
|
+
this._hasPrevWorldMatrix = true;
|
|
334
|
+
} else if (this._maxStrandLength > 0) {
|
|
335
|
+
// A jump larger than a few times the hair itself is a teleport rather than
|
|
336
|
+
// motion; fed to the solver it would fling the strands. The rest centre
|
|
337
|
+
// stands in for the groom, so a rotation around a distant pivot counts as
|
|
338
|
+
// the displacement it actually produces.
|
|
339
|
+
this._prevWorldMatrix.transformPointAffine(this._restCenter, this._scratchVec);
|
|
340
|
+
worldMatrix.transformPointAffine(this._restCenter, this._scratchVec2);
|
|
341
|
+
const dx = this._scratchVec.x - this._scratchVec2.x;
|
|
342
|
+
const dy = this._scratchVec.y - this._scratchVec2.y;
|
|
343
|
+
const dz = this._scratchVec.z - this._scratchVec2.z;
|
|
344
|
+
const threshold = this._maxStrandLength * TELEPORT_DISTANCE_FACTOR;
|
|
345
|
+
if (dx * dx + dy * dy + dz * dz > threshold * threshold) {
|
|
346
|
+
this.reset(worldMatrix);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// Before anything moves: put aside the points the last frame was drawn with,
|
|
351
|
+
// which is what the material differences against to build a motion vector.
|
|
352
|
+
// Unconditional, and ahead of the early returns below - a frame that takes no
|
|
353
|
+
// step still has to advance the snapshot, or it would report the previous
|
|
354
|
+
// frame's motion a second time and smear rather than sharpen.
|
|
355
|
+
this._snapshotFramePoints();
|
|
356
|
+
const frameDt = clamp(Number(deltaTime) || 0, 0, MAX_ACCUMULATED_SIMULATION_TIME);
|
|
357
|
+
if (frameDt <= 0) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
this._timeAccumulator = Math.min(this._timeAccumulator + frameDt, MAX_ACCUMULATED_SIMULATION_TIME);
|
|
361
|
+
const stepCount = Math.floor((this._timeAccumulator + 1e-8) / FIXED_SIMULATION_TIME_STEP);
|
|
362
|
+
if (stepCount <= 0) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
this._timeAccumulator = Math.max(0, this._timeAccumulator - stepCount * FIXED_SIMULATION_TIME_STEP);
|
|
366
|
+
// Whether the node is being shoved hard enough to suspend strand lag.
|
|
367
|
+
const rigid = this._measureRootAcceleration(worldMatrix, frameDt);
|
|
368
|
+
// Gravity and colliders come into local space from the frame-end transform;
|
|
369
|
+
// they change too slowly for the substep interpolation below to matter.
|
|
370
|
+
this._invWorldMatrix.set(worldMatrix);
|
|
371
|
+
this._invWorldMatrix.inplaceInvert();
|
|
372
|
+
this._invWorldMatrix.transformVectorAffine(this._gravity, this._localGravity);
|
|
373
|
+
this._uploadColliders(this._invWorldMatrix);
|
|
374
|
+
const bindGroup = this._bindGroup;
|
|
375
|
+
const substepTime = FIXED_SIMULATION_TIME_STEP / this._substeps;
|
|
376
|
+
bindGroup.setValue('strandCount', this._strandCount);
|
|
377
|
+
// An exponential decay rather than a linear scale, so halving the substep
|
|
378
|
+
// and applying it twice as often leaves the same velocity behind.
|
|
379
|
+
bindGroup.setValue('dampingDecay', Math.exp(-this._damping * substepTime * 60));
|
|
380
|
+
// The dial is the fraction removed per fixed step; the shader applies it
|
|
381
|
+
// once per substep, so convert it to the fraction that compounds back to it.
|
|
382
|
+
bindGroup.setValue('globalStiffness', 1 - Math.pow(1 - this._globalStiffness, 1 / this._substeps));
|
|
383
|
+
bindGroup.setValue('globalRange', this._globalRange);
|
|
384
|
+
bindGroup.setValue('localStiffness', this._localStiffness);
|
|
385
|
+
bindGroup.setValue('localIterations', this._localIterations);
|
|
386
|
+
bindGroup.setValue('ftlDamping', this._ftlDamping);
|
|
387
|
+
bindGroup.setValue('vspCoeff', rigid ? 1 : this._vspCoeff);
|
|
388
|
+
bindGroup.setValue('friction', this._friction);
|
|
389
|
+
bindGroup.setValue('maxSpeedFactor', this._maxSpeedFactor);
|
|
390
|
+
bindGroup.setValue('gravity', this._localGravity);
|
|
391
|
+
bindGroup.setValue('minDistance', MIN_DISTANCE);
|
|
392
|
+
bindGroup.setValue('deltaTime', substepTime);
|
|
393
|
+
const totalSubsteps = stepCount * this._substeps;
|
|
394
|
+
this._device.pushDeviceStates();
|
|
395
|
+
try {
|
|
396
|
+
this._device.setProgram(this._program);
|
|
397
|
+
this._device.setBindGroup(0, bindGroup);
|
|
398
|
+
if (matrixEquals(this._prevWorldMatrix, worldMatrix)) {
|
|
399
|
+
// The node stood still; nothing to distribute.
|
|
400
|
+
bindGroup.setValue('relativeTransform', this._identity);
|
|
401
|
+
for(let i = 0; i < totalSubsteps; i++){
|
|
402
|
+
this._device.compute(this._workgroupCount, 1, 1);
|
|
403
|
+
}
|
|
404
|
+
} else {
|
|
405
|
+
// The node's frame motion, spread evenly over the substeps. Lumping it
|
|
406
|
+
// into the first one would let a frame-time hitch move roots by more
|
|
407
|
+
// than a segment length in what the solver treats as a single substep.
|
|
408
|
+
this._prevWorldMatrix.decompose(this._scaleFrom, this._rotationFrom, this._translationFrom);
|
|
409
|
+
worldMatrix.decompose(this._scaleTo, this._rotationTo, this._translationTo);
|
|
410
|
+
this._stepFrom.set(this._prevWorldMatrix);
|
|
411
|
+
for(let i = 1; i <= totalSubsteps; i++){
|
|
412
|
+
if (i === totalSubsteps) {
|
|
413
|
+
// The exact endpoint, so decompose round-tripping cannot leave the
|
|
414
|
+
// solver's idea of the transform drifting from the node's.
|
|
415
|
+
this._stepTo.set(worldMatrix);
|
|
416
|
+
} else {
|
|
417
|
+
const t = i / totalSubsteps;
|
|
418
|
+
this._scratchVec.setXYZ(this._scaleFrom.x + (this._scaleTo.x - this._scaleFrom.x) * t, this._scaleFrom.y + (this._scaleTo.y - this._scaleFrom.y) * t, this._scaleFrom.z + (this._scaleTo.z - this._scaleFrom.z) * t);
|
|
419
|
+
this._scratchVec2.setXYZ(this._translationFrom.x + (this._translationTo.x - this._translationFrom.x) * t, this._translationFrom.y + (this._translationTo.y - this._translationFrom.y) * t, this._translationFrom.z + (this._translationTo.z - this._translationFrom.z) * t);
|
|
420
|
+
Quaternion.slerp(this._rotationFrom, this._rotationTo, t, this._rotationLerp);
|
|
421
|
+
this._stepTo.compose(this._scratchVec, this._rotationLerp, this._scratchVec2);
|
|
422
|
+
}
|
|
423
|
+
this._invWorldMatrix.set(this._stepTo);
|
|
424
|
+
this._invWorldMatrix.inplaceInvert();
|
|
425
|
+
Matrix4x4.multiply(this._invWorldMatrix, this._stepFrom, this._relativeTransform);
|
|
426
|
+
bindGroup.setValue('relativeTransform', this._relativeTransform);
|
|
427
|
+
this._device.compute(this._workgroupCount, 1, 1);
|
|
428
|
+
this._stepFrom.set(this._stepTo);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
} finally{
|
|
432
|
+
this._device.popDeviceStates();
|
|
433
|
+
}
|
|
434
|
+
this._prevWorldMatrix.set(worldMatrix);
|
|
435
|
+
}
|
|
436
|
+
onDispose() {
|
|
437
|
+
this._releaseResources();
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Whether the node is accelerating hard enough to suspend strand lag.
|
|
441
|
+
*
|
|
442
|
+
* @remarks
|
|
443
|
+
* Differentiated over the frame intervals actually spanned rather than the
|
|
444
|
+
* fixed step: a second difference of position is only an acceleration when
|
|
445
|
+
* the samples are evenly spaced, and frame times are not.
|
|
446
|
+
* @internal
|
|
447
|
+
*/ _measureRootAcceleration(worldMatrix, frameDt) {
|
|
448
|
+
worldMatrix.transformPointAffine(this._restCenter, this._scratchVec);
|
|
449
|
+
let rigid = false;
|
|
450
|
+
if (this._rootHistoryDepth > 0) {
|
|
451
|
+
const vx = (this._scratchVec.x - this._prevRootWorld.x) / frameDt;
|
|
452
|
+
const vy = (this._scratchVec.y - this._prevRootWorld.y) / frameDt;
|
|
453
|
+
const vz = (this._scratchVec.z - this._prevRootWorld.z) / frameDt;
|
|
454
|
+
if (this._rootHistoryDepth > 1) {
|
|
455
|
+
const ax = (vx - this._prevRootVelocity.x) / frameDt;
|
|
456
|
+
const ay = (vy - this._prevRootVelocity.y) / frameDt;
|
|
457
|
+
const az = (vz - this._prevRootVelocity.z) / frameDt;
|
|
458
|
+
const threshold = this._vspAccelThreshold;
|
|
459
|
+
rigid = ax * ax + ay * ay + az * az > threshold * threshold;
|
|
460
|
+
}
|
|
461
|
+
this._prevRootVelocity.setXYZ(vx, vy, vz);
|
|
462
|
+
}
|
|
463
|
+
this._prevRootWorld.set(this._scratchVec);
|
|
464
|
+
if (this._rootHistoryDepth < 2) {
|
|
465
|
+
this._rootHistoryDepth++;
|
|
466
|
+
}
|
|
467
|
+
return rigid;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Copies the live points into the frame snapshot the material differences
|
|
471
|
+
* against.
|
|
472
|
+
*
|
|
473
|
+
* @remarks
|
|
474
|
+
* A dispatch of its own rather than a stage of the solve, because it has to
|
|
475
|
+
* happen on every frame while the solve only happens on frames that take a
|
|
476
|
+
* step, and because it has to see the points before the first substep touches
|
|
477
|
+
* them.
|
|
478
|
+
* @internal
|
|
479
|
+
*/ _snapshotFramePoints() {
|
|
480
|
+
this._bindGroup.setValue('strandCount', this._strandCount);
|
|
481
|
+
this._bindGroup.setValue('snapshotFrame', 1);
|
|
482
|
+
this._device.pushDeviceStates();
|
|
483
|
+
try {
|
|
484
|
+
this._device.setProgram(this._program);
|
|
485
|
+
this._device.setBindGroup(0, this._bindGroup);
|
|
486
|
+
this._device.compute(this._workgroupCount, 1, 1);
|
|
487
|
+
} finally{
|
|
488
|
+
this._device.popDeviceStates();
|
|
489
|
+
}
|
|
490
|
+
this._bindGroup.setValue('snapshotFrame', 0);
|
|
491
|
+
}
|
|
492
|
+
/** Copies the rest pose over the live points. @internal */ _resetLivePoints() {
|
|
493
|
+
// Rest points are xyz only while the live buffer interleaves width, so this
|
|
494
|
+
// cannot be a straight buffer copy; the shader does it instead.
|
|
495
|
+
this._bindGroup.setValue('strandCount', this._strandCount);
|
|
496
|
+
this._bindGroup.setValue('resetPose', 1);
|
|
497
|
+
this._device.pushDeviceStates();
|
|
498
|
+
try {
|
|
499
|
+
this._device.setProgram(this._program);
|
|
500
|
+
this._device.setBindGroup(0, this._bindGroup);
|
|
501
|
+
this._device.compute(this._workgroupCount, 1, 1);
|
|
502
|
+
} finally{
|
|
503
|
+
this._device.popDeviceStates();
|
|
504
|
+
}
|
|
505
|
+
this._bindGroup.setValue('resetPose', 0);
|
|
506
|
+
}
|
|
507
|
+
/** Builds buffers and the compute pipeline. @internal */ _init(strands, source) {
|
|
508
|
+
const device = this._device;
|
|
509
|
+
const pointCount = strands.pointCount;
|
|
510
|
+
const scale = source.scale ?? 1;
|
|
511
|
+
// Rest pose and rest lengths, in the same units the strand data uploaded in
|
|
512
|
+
// - it applies the scale, so this has to as well or every constraint is
|
|
513
|
+
// wrong by it. The same pass measures the longest strand and the rest
|
|
514
|
+
// centre, which the teleport guard in update() works from.
|
|
515
|
+
const restPoints = new Float32Array(pointCount * 3);
|
|
516
|
+
const restLengths = new Float32Array(pointCount);
|
|
517
|
+
const counts = source.pointCounts;
|
|
518
|
+
let first = 0;
|
|
519
|
+
let maxStrandLength = 0;
|
|
520
|
+
let minX = Infinity;
|
|
521
|
+
let minY = Infinity;
|
|
522
|
+
let minZ = Infinity;
|
|
523
|
+
let maxX = -Infinity;
|
|
524
|
+
let maxY = -Infinity;
|
|
525
|
+
let maxZ = -Infinity;
|
|
526
|
+
for(let s = 0; s < counts.length; s++){
|
|
527
|
+
const count = counts[s];
|
|
528
|
+
let strandLength = 0;
|
|
529
|
+
for(let i = 0; i < count; i++){
|
|
530
|
+
const src = (first + i) * 3;
|
|
531
|
+
const dst = (first + i) * 3;
|
|
532
|
+
const x = source.positions[src] * scale;
|
|
533
|
+
const y = source.positions[src + 1] * scale;
|
|
534
|
+
const z = source.positions[src + 2] * scale;
|
|
535
|
+
restPoints[dst] = x;
|
|
536
|
+
restPoints[dst + 1] = y;
|
|
537
|
+
restPoints[dst + 2] = z;
|
|
538
|
+
minX = x < minX ? x : minX;
|
|
539
|
+
minY = y < minY ? y : minY;
|
|
540
|
+
minZ = z < minZ ? z : minZ;
|
|
541
|
+
maxX = x > maxX ? x : maxX;
|
|
542
|
+
maxY = y > maxY ? y : maxY;
|
|
543
|
+
maxZ = z > maxZ ? z : maxZ;
|
|
544
|
+
if (i > 0) {
|
|
545
|
+
const dx = x - restPoints[dst - 3];
|
|
546
|
+
const dy = y - restPoints[dst - 2];
|
|
547
|
+
const dz = z - restPoints[dst - 1];
|
|
548
|
+
const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
549
|
+
restLengths[first + i] = len;
|
|
550
|
+
strandLength += len;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
if (strandLength > maxStrandLength) {
|
|
554
|
+
maxStrandLength = strandLength;
|
|
555
|
+
}
|
|
556
|
+
first += count;
|
|
557
|
+
}
|
|
558
|
+
this._maxStrandLength = maxStrandLength;
|
|
559
|
+
if (pointCount > 0) {
|
|
560
|
+
this._restCenter.setXYZ((minX + maxX) * 0.5, (minY + maxY) * 0.5, (minZ + maxZ) * 0.5);
|
|
561
|
+
}
|
|
562
|
+
this._pointBuffer = strands.pointBuffer;
|
|
563
|
+
this._restPointBuffer = this._createStorage(restPoints);
|
|
564
|
+
this._prevPointBuffer = this._createStorage(restPoints);
|
|
565
|
+
this._framePointBuffer = this._createStorage(restPoints);
|
|
566
|
+
this._restLengthBuffer = this._createStorage(restLengths);
|
|
567
|
+
this._sphereBuffer = this._createStorage(this._sphereData);
|
|
568
|
+
this._capsuleBuffer = this._createStorage(this._capsuleData);
|
|
569
|
+
this._planeBuffer = this._createStorage(this._planeData);
|
|
570
|
+
this._program = createHairSimulationProgram(device, DEFAULT_WORKGROUP_SIZE);
|
|
571
|
+
if (!this._program) {
|
|
572
|
+
throw new Error('failed to create the compute program');
|
|
573
|
+
}
|
|
574
|
+
this._bindGroup = device.createBindGroup(this._program.bindGroupLayouts[0]);
|
|
575
|
+
this._bindGroup.setBuffer('points', this._pointBuffer);
|
|
576
|
+
this._bindGroup.setBuffer('prevPoints', this._prevPointBuffer);
|
|
577
|
+
this._bindGroup.setBuffer('framePoints', this._framePointBuffer);
|
|
578
|
+
this._bindGroup.setBuffer('restPoints', this._restPointBuffer);
|
|
579
|
+
this._bindGroup.setBuffer('restLengths', this._restLengthBuffer);
|
|
580
|
+
this._bindGroup.setBuffer('headers', strands.headerBuffer);
|
|
581
|
+
this._bindGroup.setBuffer('sphereData', this._sphereBuffer);
|
|
582
|
+
this._bindGroup.setBuffer('capsuleData', this._capsuleBuffer);
|
|
583
|
+
this._bindGroup.setBuffer('planeData', this._planeBuffer);
|
|
584
|
+
this._bindGroup.setValue('strandCount', this._strandCount);
|
|
585
|
+
this._bindGroup.setValue('resetPose', 0);
|
|
586
|
+
this._bindGroup.setValue('relativeTransform', Matrix4x4.identity());
|
|
587
|
+
this._bindGroup.setValue('sphereCount', 0);
|
|
588
|
+
this._bindGroup.setValue('capsuleCount', 0);
|
|
589
|
+
this._bindGroup.setValue('planeCount', 0);
|
|
590
|
+
this._bindGroup.setValue('minDistance', MIN_DISTANCE);
|
|
591
|
+
this._bindGroup.setValue('snapshotFrame', 0);
|
|
592
|
+
this._bindGroup.setValue('maxSpeedFactor', this._maxSpeedFactor);
|
|
593
|
+
this._bindGroup.setValue('globalStiffness', this._globalStiffness);
|
|
594
|
+
this._bindGroup.setValue('globalRange', this._globalRange);
|
|
595
|
+
this._bindGroup.setValue('localStiffness', this._localStiffness);
|
|
596
|
+
this._bindGroup.setValue('localIterations', this._localIterations);
|
|
597
|
+
this._bindGroup.setValue('ftlDamping', this._ftlDamping);
|
|
598
|
+
this._bindGroup.setValue('vspCoeff', this._vspCoeff);
|
|
599
|
+
}
|
|
600
|
+
/** @internal */ _createStorage(data) {
|
|
601
|
+
const buffer = this._device.createBuffer(data.byteLength, {
|
|
602
|
+
usage: 'uniform',
|
|
603
|
+
storage: true,
|
|
604
|
+
dynamic: false,
|
|
605
|
+
managed: false
|
|
606
|
+
});
|
|
607
|
+
if (!buffer) {
|
|
608
|
+
throw new Error('failed to allocate a simulation buffer');
|
|
609
|
+
}
|
|
610
|
+
buffer.bufferSubData(0, data);
|
|
611
|
+
return buffer;
|
|
612
|
+
}
|
|
613
|
+
/** Brings colliders into local space and uploads them. @internal */ _uploadColliders(invWorldMatrix) {
|
|
614
|
+
let spheres = 0;
|
|
615
|
+
let capsules = 0;
|
|
616
|
+
let planes = 0;
|
|
617
|
+
for (const collider of this._colliders){
|
|
618
|
+
if (!collider.enabled) {
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
// Node-tracking colliders are world-space and refreshed here.
|
|
622
|
+
updateColliderFromNode(collider);
|
|
623
|
+
if (collider.type === 'sphere' && spheres < MAX_COLLIDERS) {
|
|
624
|
+
const sphere = collider;
|
|
625
|
+
invWorldMatrix.transformPointAffine(sphere.center, this._scratchVec);
|
|
626
|
+
const base = spheres * SPHERE_STRIDE;
|
|
627
|
+
this._sphereData[base] = this._scratchVec.x;
|
|
628
|
+
this._sphereData[base + 1] = this._scratchVec.y;
|
|
629
|
+
this._sphereData[base + 2] = this._scratchVec.z;
|
|
630
|
+
this._sphereData[base + 3] = sphere.radius * localRadiusScale(invWorldMatrix);
|
|
631
|
+
spheres++;
|
|
632
|
+
} else if (collider.type === 'capsule' && capsules < MAX_COLLIDERS) {
|
|
633
|
+
const capsule = collider;
|
|
634
|
+
invWorldMatrix.transformPointAffine(capsule.start, this._scratchVec);
|
|
635
|
+
invWorldMatrix.transformPointAffine(capsule.end, this._scratchVec2);
|
|
636
|
+
const base = capsules * CAPSULE_STRIDE;
|
|
637
|
+
this._capsuleData[base] = this._scratchVec.x;
|
|
638
|
+
this._capsuleData[base + 1] = this._scratchVec.y;
|
|
639
|
+
this._capsuleData[base + 2] = this._scratchVec.z;
|
|
640
|
+
this._capsuleData[base + 3] = capsule.radius * localRadiusScale(invWorldMatrix);
|
|
641
|
+
this._capsuleData[base + 4] = this._scratchVec2.x;
|
|
642
|
+
this._capsuleData[base + 5] = this._scratchVec2.y;
|
|
643
|
+
this._capsuleData[base + 6] = this._scratchVec2.z;
|
|
644
|
+
this._capsuleData[base + 7] = 0;
|
|
645
|
+
capsules++;
|
|
646
|
+
} else if (collider.type === 'plane' && planes < MAX_COLLIDERS) {
|
|
647
|
+
const plane = collider;
|
|
648
|
+
invWorldMatrix.transformPointAffine(plane.point, this._scratchVec);
|
|
649
|
+
// A normal transforms by the inverse transpose, and the inverse is to hand.
|
|
650
|
+
transformNormal(invWorldMatrix, plane.normal, this._scratchVec2);
|
|
651
|
+
const base = planes * PLANE_STRIDE;
|
|
652
|
+
this._planeData[base] = this._scratchVec.x;
|
|
653
|
+
this._planeData[base + 1] = this._scratchVec.y;
|
|
654
|
+
this._planeData[base + 2] = this._scratchVec.z;
|
|
655
|
+
this._planeData[base + 3] = 0;
|
|
656
|
+
this._planeData[base + 4] = this._scratchVec2.x;
|
|
657
|
+
this._planeData[base + 5] = this._scratchVec2.y;
|
|
658
|
+
this._planeData[base + 6] = this._scratchVec2.z;
|
|
659
|
+
this._planeData[base + 7] = 0;
|
|
660
|
+
planes++;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
this._sphereBuffer.bufferSubData(0, this._sphereData);
|
|
664
|
+
this._capsuleBuffer.bufferSubData(0, this._capsuleData);
|
|
665
|
+
this._planeBuffer.bufferSubData(0, this._planeData);
|
|
666
|
+
this._bindGroup.setValue('sphereCount', spheres);
|
|
667
|
+
this._bindGroup.setValue('capsuleCount', capsules);
|
|
668
|
+
this._bindGroup.setValue('planeCount', planes);
|
|
669
|
+
}
|
|
670
|
+
/** @internal */ _releaseResources() {
|
|
671
|
+
this._enabled = false;
|
|
672
|
+
this._bindGroup?.dispose();
|
|
673
|
+
this._bindGroup = null;
|
|
674
|
+
this._program?.dispose();
|
|
675
|
+
this._program = null;
|
|
676
|
+
// The point buffer belongs to the strand data, not here.
|
|
677
|
+
this._pointBuffer = null;
|
|
678
|
+
this._prevPointBuffer?.dispose();
|
|
679
|
+
this._prevPointBuffer = null;
|
|
680
|
+
this._restPointBuffer?.dispose();
|
|
681
|
+
this._restPointBuffer = null;
|
|
682
|
+
this._framePointBuffer?.dispose();
|
|
683
|
+
this._framePointBuffer = null;
|
|
684
|
+
this._restLengthBuffer?.dispose();
|
|
685
|
+
this._restLengthBuffer = null;
|
|
686
|
+
this._sphereBuffer?.dispose();
|
|
687
|
+
this._sphereBuffer = null;
|
|
688
|
+
this._capsuleBuffer?.dispose();
|
|
689
|
+
this._capsuleBuffer = null;
|
|
690
|
+
this._planeBuffer?.dispose();
|
|
691
|
+
this._planeBuffer = null;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
/** Mean axis scale of a matrix, for converting a radius. @internal */ function localRadiusScale(matrix) {
|
|
695
|
+
const sx = Math.hypot(matrix.m00, matrix.m10, matrix.m20);
|
|
696
|
+
const sy = Math.hypot(matrix.m01, matrix.m11, matrix.m21);
|
|
697
|
+
const sz = Math.hypot(matrix.m02, matrix.m12, matrix.m22);
|
|
698
|
+
return (sx + sy + sz) / 3;
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Transforms a normal by the transpose of the inverse world matrix, which is the
|
|
702
|
+
* inverse-transpose a normal needs.
|
|
703
|
+
* @internal
|
|
704
|
+
*/ function transformNormal(invWorldMatrix, normal, out) {
|
|
705
|
+
out.setXYZ(invWorldMatrix.m00 * normal.x + invWorldMatrix.m10 * normal.y + invWorldMatrix.m20 * normal.z, invWorldMatrix.m01 * normal.x + invWorldMatrix.m11 * normal.y + invWorldMatrix.m21 * normal.z, invWorldMatrix.m02 * normal.x + invWorldMatrix.m12 * normal.y + invWorldMatrix.m22 * normal.z);
|
|
706
|
+
const len = out.magnitude;
|
|
707
|
+
if (len > MIN_DISTANCE) {
|
|
708
|
+
out.scaleBy(1 / len);
|
|
709
|
+
} else {
|
|
710
|
+
out.setXYZ(0, 1, 0);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Builds the solver pass.
|
|
715
|
+
*
|
|
716
|
+
* @remarks
|
|
717
|
+
* TressFX's five simulation stages, run back to back inside one kernel with a
|
|
718
|
+
* thread per strand instead of five dispatches with a thread per vertex. The
|
|
719
|
+
* rearrangement keeps them numerically equivalent: integration, the global shape
|
|
720
|
+
* constraint and shock propagation are per-vertex with no cross-vertex
|
|
721
|
+
* dependency, and the local shape constraint is already a serial walk over a
|
|
722
|
+
* strand in TressFX. Solved in local space, where the bind-pose skinning
|
|
723
|
+
* quaternion is the identity and shock propagation reduces to a blend.
|
|
724
|
+
*
|
|
725
|
+
* Exported for shader-generation tests only.
|
|
726
|
+
* @internal
|
|
727
|
+
*/ function createHairSimulationProgram(device, workgroupSize) {
|
|
728
|
+
const program = device.buildComputeProgram({
|
|
729
|
+
workgroupSize: [
|
|
730
|
+
workgroupSize,
|
|
731
|
+
1,
|
|
732
|
+
1
|
|
733
|
+
],
|
|
734
|
+
compute (pb) {
|
|
735
|
+
// Positions and widths interleave, four floats per point; the solver
|
|
736
|
+
// writes xyz and leaves w alone, which is why they share a buffer.
|
|
737
|
+
this.points = pb.float[0]().storageBuffer(0);
|
|
738
|
+
this.prevPoints = pb.float[0]().storageBuffer(0);
|
|
739
|
+
this.framePoints = pb.float[0]().storageBuffer(0);
|
|
740
|
+
this.restPoints = pb.float[0]().storageBufferReadonly(0);
|
|
741
|
+
this.restLengths = pb.float[0]().storageBufferReadonly(0);
|
|
742
|
+
this.headers = pb.uint[0]().storageBufferReadonly(0);
|
|
743
|
+
this.sphereData = pb.float[0]().storageBufferReadonly(0);
|
|
744
|
+
this.capsuleData = pb.float[0]().storageBufferReadonly(0);
|
|
745
|
+
this.planeData = pb.float[0]().storageBufferReadonly(0);
|
|
746
|
+
this.strandCount = pb.uint().uniform(0);
|
|
747
|
+
this.deltaTime = pb.float().uniform(0);
|
|
748
|
+
this.dampingDecay = pb.float().uniform(0);
|
|
749
|
+
this.globalStiffness = pb.float().uniform(0);
|
|
750
|
+
this.globalRange = pb.float().uniform(0);
|
|
751
|
+
this.localStiffness = pb.float().uniform(0);
|
|
752
|
+
this.localIterations = pb.uint().uniform(0);
|
|
753
|
+
this.ftlDamping = pb.float().uniform(0);
|
|
754
|
+
this.vspCoeff = pb.float().uniform(0);
|
|
755
|
+
this.friction = pb.float().uniform(0);
|
|
756
|
+
this.maxSpeedFactor = pb.float().uniform(0);
|
|
757
|
+
this.gravity = pb.vec3().uniform(0);
|
|
758
|
+
this.relativeTransform = pb.mat4().uniform(0);
|
|
759
|
+
this.sphereCount = pb.uint().uniform(0);
|
|
760
|
+
this.capsuleCount = pb.uint().uniform(0);
|
|
761
|
+
this.planeCount = pb.uint().uniform(0);
|
|
762
|
+
this.minDistance = pb.float().uniform(0);
|
|
763
|
+
this.resetPose = pb.uint().uniform(0);
|
|
764
|
+
this.snapshotFrame = pb.uint().uniform(0);
|
|
765
|
+
// Pushes a point out of every collider. Position only; friction and
|
|
766
|
+
// carrying the history along are the caller's job, since both need the
|
|
767
|
+
// history it is still assembling.
|
|
768
|
+
pb.func('Z_hairResolveContacts', [
|
|
769
|
+
pb.vec3('position')
|
|
770
|
+
], function() {
|
|
771
|
+
this.$l.result = this.position;
|
|
772
|
+
this.$for(pb.uint('i'), 0, this.sphereCount, function() {
|
|
773
|
+
this.$l.base = pb.mul(this.i, 4);
|
|
774
|
+
this.$l.centre = pb.vec3(this.sphereData.at(this.base), this.sphereData.at(pb.add(this.base, 1)), this.sphereData.at(pb.add(this.base, 2)));
|
|
775
|
+
this.$l.radius = this.sphereData.at(pb.add(this.base, 3));
|
|
776
|
+
this.$l.delta = pb.sub(this.result, this.centre);
|
|
777
|
+
this.$l.dist = pb.length(this.delta);
|
|
778
|
+
this.$if(pb.lessThan(this.dist, this.radius), function() {
|
|
779
|
+
this.$l.normal = pb.vec3(0, 1, 0);
|
|
780
|
+
this.$if(pb.greaterThan(this.dist, this.minDistance), function() {
|
|
781
|
+
this.normal = pb.div(this.delta, this.dist);
|
|
782
|
+
});
|
|
783
|
+
this.result = pb.add(this.centre, pb.mul(this.normal, this.radius));
|
|
784
|
+
});
|
|
785
|
+
});
|
|
786
|
+
this.$for(pb.uint('j'), 0, this.capsuleCount, function() {
|
|
787
|
+
this.$l.cbase = pb.mul(this.j, 8);
|
|
788
|
+
this.$l.start = pb.vec3(this.capsuleData.at(this.cbase), this.capsuleData.at(pb.add(this.cbase, 1)), this.capsuleData.at(pb.add(this.cbase, 2)));
|
|
789
|
+
this.$l.cradius = this.capsuleData.at(pb.add(this.cbase, 3));
|
|
790
|
+
this.$l.end = pb.vec3(this.capsuleData.at(pb.add(this.cbase, 4)), this.capsuleData.at(pb.add(this.cbase, 5)), this.capsuleData.at(pb.add(this.cbase, 6)));
|
|
791
|
+
this.$l.axis = pb.sub(this.end, this.start);
|
|
792
|
+
this.$l.axisLenSq = pb.dot(this.axis, this.axis);
|
|
793
|
+
this.$l.t = pb.float(0);
|
|
794
|
+
this.$if(pb.greaterThan(this.axisLenSq, this.minDistance), function() {
|
|
795
|
+
this.t = pb.clamp(pb.div(pb.dot(pb.sub(this.result, this.start), this.axis), this.axisLenSq), 0, 1);
|
|
796
|
+
});
|
|
797
|
+
this.$l.closest = pb.add(this.start, pb.mul(this.axis, this.t));
|
|
798
|
+
this.$l.cdelta = pb.sub(this.result, this.closest);
|
|
799
|
+
this.$l.cdist = pb.length(this.cdelta);
|
|
800
|
+
this.$if(pb.lessThan(this.cdist, this.cradius), function() {
|
|
801
|
+
this.$l.cnormal = pb.vec3(0, 1, 0);
|
|
802
|
+
this.$if(pb.greaterThan(this.cdist, this.minDistance), function() {
|
|
803
|
+
this.cnormal = pb.div(this.cdelta, this.cdist);
|
|
804
|
+
});
|
|
805
|
+
this.result = pb.add(this.closest, pb.mul(this.cnormal, this.cradius));
|
|
806
|
+
});
|
|
807
|
+
});
|
|
808
|
+
this.$for(pb.uint('k'), 0, this.planeCount, function() {
|
|
809
|
+
this.$l.pbase = pb.mul(this.k, 8);
|
|
810
|
+
this.$l.point = pb.vec3(this.planeData.at(this.pbase), this.planeData.at(pb.add(this.pbase, 1)), this.planeData.at(pb.add(this.pbase, 2)));
|
|
811
|
+
this.$l.pnormal = pb.vec3(this.planeData.at(pb.add(this.pbase, 4)), this.planeData.at(pb.add(this.pbase, 5)), this.planeData.at(pb.add(this.pbase, 6)));
|
|
812
|
+
this.$l.signedDist = pb.dot(pb.sub(this.result, this.point), this.pnormal);
|
|
813
|
+
this.$if(pb.lessThan(this.signedDist, 0), function() {
|
|
814
|
+
this.result = pb.sub(this.result, pb.mul(this.pnormal, this.signedDist));
|
|
815
|
+
});
|
|
816
|
+
});
|
|
817
|
+
this.$return(this.result);
|
|
818
|
+
});
|
|
819
|
+
// Accessors for the interleaved point buffer.
|
|
820
|
+
pb.func('Z_hairGetPoint', [
|
|
821
|
+
pb.uint('index')
|
|
822
|
+
], function() {
|
|
823
|
+
this.$l.base = pb.mul(this.index, 4);
|
|
824
|
+
this.$return(pb.vec3(this.points.at(this.base), this.points.at(pb.add(this.base, 1)), this.points.at(pb.add(this.base, 2))));
|
|
825
|
+
});
|
|
826
|
+
pb.func('Z_hairSetPoint', [
|
|
827
|
+
pb.uint('index'),
|
|
828
|
+
pb.vec3('value')
|
|
829
|
+
], function() {
|
|
830
|
+
this.$l.base = pb.mul(this.index, 4);
|
|
831
|
+
this.points.setAt(this.base, this.value.x);
|
|
832
|
+
this.points.setAt(pb.add(this.base, 1), this.value.y);
|
|
833
|
+
this.points.setAt(pb.add(this.base, 2), this.value.z);
|
|
834
|
+
this.$return(pb.float(0));
|
|
835
|
+
});
|
|
836
|
+
pb.func('Z_hairGetRest', [
|
|
837
|
+
pb.uint('index')
|
|
838
|
+
], function() {
|
|
839
|
+
this.$l.base = pb.mul(this.index, 3);
|
|
840
|
+
this.$return(pb.vec3(this.restPoints.at(this.base), this.restPoints.at(pb.add(this.base, 1)), this.restPoints.at(pb.add(this.base, 2))));
|
|
841
|
+
});
|
|
842
|
+
pb.func('Z_hairGetHistory', [
|
|
843
|
+
pb.uint('index')
|
|
844
|
+
], function() {
|
|
845
|
+
this.$l.base = pb.mul(this.index, 3);
|
|
846
|
+
this.$return(pb.vec3(this.prevPoints.at(this.base), this.prevPoints.at(pb.add(this.base, 1)), this.prevPoints.at(pb.add(this.base, 2))));
|
|
847
|
+
});
|
|
848
|
+
pb.func('Z_hairSetHistory', [
|
|
849
|
+
pb.uint('index'),
|
|
850
|
+
pb.vec3('value')
|
|
851
|
+
], function() {
|
|
852
|
+
this.$l.base = pb.mul(this.index, 3);
|
|
853
|
+
this.prevPoints.setAt(this.base, this.value.x);
|
|
854
|
+
this.prevPoints.setAt(pb.add(this.base, 1), this.value.y);
|
|
855
|
+
this.prevPoints.setAt(pb.add(this.base, 2), this.value.z);
|
|
856
|
+
this.$return(pb.float(0));
|
|
857
|
+
});
|
|
858
|
+
pb.func('Z_hairSetFramePoint', [
|
|
859
|
+
pb.uint('index'),
|
|
860
|
+
pb.vec3('value')
|
|
861
|
+
], function() {
|
|
862
|
+
this.$l.base = pb.mul(this.index, 3);
|
|
863
|
+
this.framePoints.setAt(this.base, this.value.x);
|
|
864
|
+
this.framePoints.setAt(pb.add(this.base, 1), this.value.y);
|
|
865
|
+
this.framePoints.setAt(pb.add(this.base, 2), this.value.z);
|
|
866
|
+
this.$return(pb.float(0));
|
|
867
|
+
});
|
|
868
|
+
// Rotates a vector by a quaternion.
|
|
869
|
+
pb.func('Z_hairQuatRotate', [
|
|
870
|
+
pb.vec4('q'),
|
|
871
|
+
pb.vec3('v')
|
|
872
|
+
], function() {
|
|
873
|
+
this.$l.uv = pb.cross(this.q.xyz, this.v);
|
|
874
|
+
this.$l.uuv = pb.cross(this.q.xyz, this.uv);
|
|
875
|
+
this.$return(pb.add(this.v, pb.mul(this.uv, pb.mul(this.q.w, 2)), pb.mul(this.uuv, 2)));
|
|
876
|
+
});
|
|
877
|
+
// Shortest-arc rotation taking unit u onto unit v. Antiparallel inputs
|
|
878
|
+
// leave the axis undefined, so any perpendicular of u will do.
|
|
879
|
+
pb.func('Z_hairQuatFromUnitVectors', [
|
|
880
|
+
pb.vec3('u'),
|
|
881
|
+
pb.vec3('v')
|
|
882
|
+
], function() {
|
|
883
|
+
this.$l.r = pb.add(1, pb.dot(this.u, this.v));
|
|
884
|
+
this.$l.axis = pb.cross(this.u, this.v);
|
|
885
|
+
this.$if(pb.lessThan(this.r, 1e-7), function() {
|
|
886
|
+
this.r = pb.float(0);
|
|
887
|
+
this.axis = this.$choice(pb.greaterThan(pb.abs(this.u.x), pb.abs(this.u.z)), pb.vec3(pb.neg(this.u.y), this.u.x, 0), pb.vec3(0, pb.neg(this.u.z), this.u.y));
|
|
888
|
+
});
|
|
889
|
+
this.$l.q = pb.vec4(this.axis, this.r);
|
|
890
|
+
this.$l.lengthSq = pb.dot(this.q, this.q);
|
|
891
|
+
this.$l.result = pb.vec4(0, 0, 0, 1);
|
|
892
|
+
this.$if(pb.greaterThan(this.lengthSq, 1e-10), function() {
|
|
893
|
+
this.result = pb.mul(this.q, pb.inverseSqrt(this.lengthSq));
|
|
894
|
+
});
|
|
895
|
+
this.$return(this.result);
|
|
896
|
+
});
|
|
897
|
+
// Bounds the velocity a stored history implies, so that one bad substep
|
|
898
|
+
// cannot hand the next a velocity damping will never recover from.
|
|
899
|
+
pb.func('Z_hairClampHistory', [
|
|
900
|
+
pb.uint('index'),
|
|
901
|
+
pb.vec3('position'),
|
|
902
|
+
pb.vec3('history')
|
|
903
|
+
], function() {
|
|
904
|
+
this.$l.result = this.history;
|
|
905
|
+
this.$l.delta = pb.sub(this.position, this.result);
|
|
906
|
+
this.$l.speed = pb.length(this.delta);
|
|
907
|
+
this.$l.limit = pb.mul(this.restLengths.at(this.index), this.maxSpeedFactor);
|
|
908
|
+
this.$if(pb.greaterThan(this.speed, this.limit), function() {
|
|
909
|
+
this.result = pb.sub(this.position, pb.mul(this.delta, pb.div(this.limit, this.speed)));
|
|
910
|
+
});
|
|
911
|
+
this.$return(this.result);
|
|
912
|
+
});
|
|
913
|
+
pb.main(function() {
|
|
914
|
+
this.$l.strand = this.$builtins.globalInvocationId.x;
|
|
915
|
+
this.$if(pb.lessThan(this.strand, this.strandCount), function() {
|
|
916
|
+
this.$l.hbase = pb.mul(this.strand, 4);
|
|
917
|
+
this.$l.firstPoint = this.headers.at(this.hbase);
|
|
918
|
+
this.$l.pointCount = this.headers.at(pb.add(this.hbase, 1));
|
|
919
|
+
this.$if(pb.notEqual(this.snapshotFrame, 0), function() {
|
|
920
|
+
// Copy the points the last frame was drawn with aside, before this
|
|
921
|
+
// frame's substeps overwrite them. Run once per frame from update(),
|
|
922
|
+
// including on frames that take no step - skipping those would leave
|
|
923
|
+
// the snapshot a frame stale and report motion that already happened.
|
|
924
|
+
this.$for(pb.uint('i'), 0, this.pointCount, function() {
|
|
925
|
+
this.$l.idx = pb.add(this.firstPoint, this.i);
|
|
926
|
+
this.Z_hairSetFramePoint(this.idx, this.Z_hairGetPoint(this.idx));
|
|
927
|
+
});
|
|
928
|
+
}).$elseif(pb.notEqual(this.resetPose, 0), function() {
|
|
929
|
+
// Rest into all three, so the strand is still as well as in place
|
|
930
|
+
// and the frame it snaps on reports no motion of its own.
|
|
931
|
+
this.$for(pb.uint('i'), 0, this.pointCount, function() {
|
|
932
|
+
this.$l.idx = pb.add(this.firstPoint, this.i);
|
|
933
|
+
this.$l.rest = this.Z_hairGetRest(this.idx);
|
|
934
|
+
this.Z_hairSetPoint(this.idx, this.rest);
|
|
935
|
+
this.Z_hairSetHistory(this.idx, this.rest);
|
|
936
|
+
this.Z_hairSetFramePoint(this.idx, this.rest);
|
|
937
|
+
});
|
|
938
|
+
}).$else(function() {
|
|
939
|
+
// Two pinned points, not one: one fixes where a strand hangs from but
|
|
940
|
+
// leaves the direction it leaves the scalp in free, so the root end
|
|
941
|
+
// flops and can invert. The second gives every strand the root frame
|
|
942
|
+
// the local shape constraint is defined against. In local space
|
|
943
|
+
// "pinned" means unchanged - node motion rides the transform.
|
|
944
|
+
this.$l.rootRest = this.Z_hairGetRest(this.firstPoint);
|
|
945
|
+
this.Z_hairSetPoint(this.firstPoint, this.rootRest);
|
|
946
|
+
this.Z_hairSetHistory(this.firstPoint, this.rootRest);
|
|
947
|
+
this.$if(pb.greaterThan(this.pointCount, 1), function() {
|
|
948
|
+
this.$l.nextIdx = pb.add(this.firstPoint, 1);
|
|
949
|
+
this.$l.nextRest = this.Z_hairGetRest(this.nextIdx);
|
|
950
|
+
this.Z_hairSetPoint(this.nextIdx, this.nextRest);
|
|
951
|
+
this.Z_hairSetHistory(this.nextIdx, this.nextRest);
|
|
952
|
+
});
|
|
953
|
+
this.$l.dt2 = pb.mul(this.deltaTime, this.deltaTime);
|
|
954
|
+
this.$l.rangeLimit = pb.mul(this.globalRange, pb.float(this.pointCount));
|
|
955
|
+
// Stage 1 - integration, shock propagation, global shape constraint.
|
|
956
|
+
this.$for(pb.uint('i'), 2, this.pointCount, function() {
|
|
957
|
+
this.$l.idx = pb.add(this.firstPoint, this.i);
|
|
958
|
+
this.$l.current = this.Z_hairGetPoint(this.idx);
|
|
959
|
+
this.$l.previous = this.Z_hairGetHistory(this.idx);
|
|
960
|
+
// Both stored positions go through the node's frame-to-frame
|
|
961
|
+
// motion: transforming only one would read that motion as strand
|
|
962
|
+
// velocity and throw the hair ahead of the movement.
|
|
963
|
+
this.$l.lagged = pb.mul(this.relativeTransform, pb.vec4(this.current, 1)).xyz;
|
|
964
|
+
this.$l.laggedPrev = pb.mul(this.relativeTransform, pb.vec4(this.previous, 1)).xyz;
|
|
965
|
+
// Shock propagation. Leaving a point untransformed is exactly
|
|
966
|
+
// carrying it rigidly with the node, so the coefficient blends
|
|
967
|
+
// between the two ends TressFX interpolates: 0 lags fully, 1
|
|
968
|
+
// follows. It stops a fast root outrunning its strand in one step.
|
|
969
|
+
this.current = pb.mix(this.lagged, this.current, this.vspCoeff);
|
|
970
|
+
this.previous = pb.mix(this.laggedPrev, this.previous, this.vspCoeff);
|
|
971
|
+
this.$l.velocity = pb.mul(pb.sub(this.current, this.previous), this.dampingDecay);
|
|
972
|
+
this.$l.next = pb.add(this.current, this.velocity, pb.mul(this.gravity, this.dt2));
|
|
973
|
+
// Pull toward the authored pose, over the range's fraction of the
|
|
974
|
+
// strand. Off by default - anchoring a point to a fixed place is a
|
|
975
|
+
// spring, and the local shape constraint holds styling without it.
|
|
976
|
+
this.$if(pb.and(pb.greaterThan(this.globalStiffness, 0), pb.lessThan(pb.float(this.i), this.rangeLimit)), function() {
|
|
977
|
+
this.$l.rest = this.Z_hairGetRest(this.idx);
|
|
978
|
+
this.next = pb.add(this.next, pb.mul(pb.sub(this.rest, this.next), this.globalStiffness));
|
|
979
|
+
});
|
|
980
|
+
this.Z_hairSetHistory(this.idx, this.current);
|
|
981
|
+
this.Z_hairSetPoint(this.idx, this.next);
|
|
982
|
+
});
|
|
983
|
+
// Stage 2 - local shape constraint, what makes a groom read as hair
|
|
984
|
+
// rather than as a chain. It holds each segment's angle to the one
|
|
985
|
+
// before it in that segment's *current* frame, so a strand keeps its
|
|
986
|
+
// curl while free to hang and swing, and resists bending - which is
|
|
987
|
+
// also what stops a disturbance ringing for seconds. Stiffness is
|
|
988
|
+
// halved and capped below 1 as in TressFX, since the constraint
|
|
989
|
+
// moves both points it touches and would otherwise overshoot.
|
|
990
|
+
this.$l.localStrength = pb.mul(pb.min(this.localStiffness, 0.95), 0.5);
|
|
991
|
+
this.$if(pb.greaterThan(this.pointCount, 2), function() {
|
|
992
|
+
this.$for(pb.uint('iter'), 0, this.localIterations, function() {
|
|
993
|
+
this.$for(pb.uint('i'), 1, pb.sub(this.pointCount, 1), function() {
|
|
994
|
+
this.$l.idx = pb.add(this.firstPoint, this.i);
|
|
995
|
+
this.$l.pos = this.Z_hairGetPoint(this.idx);
|
|
996
|
+
this.$l.posPlus = this.Z_hairGetPoint(pb.add(this.idx, 1));
|
|
997
|
+
this.$l.posMinus = this.Z_hairGetPoint(pb.sub(this.idx, 1));
|
|
998
|
+
this.$l.bind = this.Z_hairGetRest(this.idx);
|
|
999
|
+
this.$l.bindPlus = this.Z_hairGetRest(pb.add(this.idx, 1));
|
|
1000
|
+
this.$l.bindMinus = this.Z_hairGetRest(pb.sub(this.idx, 1));
|
|
1001
|
+
// Where the next point sits if its bend still matches the bind
|
|
1002
|
+
// pose, carried into the previous segment's current direction.
|
|
1003
|
+
this.$l.lastVec = pb.sub(this.pos, this.posMinus);
|
|
1004
|
+
this.$l.bindVec = pb.sub(this.bindPlus, this.bind);
|
|
1005
|
+
this.$l.lastBindVec = pb.sub(this.bind, this.bindMinus);
|
|
1006
|
+
this.$l.rotation = this.Z_hairQuatFromUnitVectors(pb.normalize(this.lastBindVec), pb.normalize(this.lastVec));
|
|
1007
|
+
this.$l.bendTarget = pb.add(this.Z_hairQuatRotate(this.rotation, this.bindVec), this.pos);
|
|
1008
|
+
this.$l.del = pb.mul(pb.sub(this.bendTarget, this.posPlus), this.localStrength);
|
|
1009
|
+
// Shared between both ends, unless one is a pinned root.
|
|
1010
|
+
this.$if(pb.greaterThan(this.i, 1), function() {
|
|
1011
|
+
this.Z_hairSetPoint(this.idx, pb.sub(this.pos, this.del));
|
|
1012
|
+
});
|
|
1013
|
+
this.Z_hairSetPoint(pb.add(this.idx, 1), pb.add(this.posPlus, this.del));
|
|
1014
|
+
});
|
|
1015
|
+
});
|
|
1016
|
+
});
|
|
1017
|
+
// Stage 3 - length constraints, root to tip. Placing each point at
|
|
1018
|
+
// its authored distance from an already-final parent leaves the
|
|
1019
|
+
// strand inextensible in one pass.
|
|
1020
|
+
//
|
|
1021
|
+
// The one place the port leaves TressFX, which sweeps even and odd
|
|
1022
|
+
// segments Jacobi-style because a thread there owns a single vertex
|
|
1023
|
+
// and cannot walk a strand. Jacobi transports tension one segment
|
|
1024
|
+
// per sweep, so a hanging strand stretches until the residual
|
|
1025
|
+
// balances: 22% on a 24-point strand under earth gravity at its two
|
|
1026
|
+
// sweeps, still 4.5% at sixteen. TressFX's own default gravity is
|
|
1027
|
+
// zero, so it never sees this.
|
|
1028
|
+
//
|
|
1029
|
+
// Moving the child alone leaves that correction's momentum
|
|
1030
|
+
// unaccounted for, and unaccounted it pumps the chain until the
|
|
1031
|
+
// groom whips; handing it back to the parent is Mueller's FTL
|
|
1032
|
+
// damping.
|
|
1033
|
+
this.$if(pb.greaterThan(this.pointCount, 2), function() {
|
|
1034
|
+
this.$for(pb.uint('j'), 1, pb.sub(this.pointCount, 1), function() {
|
|
1035
|
+
this.$l.parentIdx = pb.add(this.firstPoint, this.j);
|
|
1036
|
+
this.$l.childIdx = pb.add(this.parentIdx, 1);
|
|
1037
|
+
this.$l.parent = this.Z_hairGetPoint(this.parentIdx);
|
|
1038
|
+
this.$l.predicted = this.Z_hairGetPoint(this.childIdx);
|
|
1039
|
+
// Rest lengths are indexed by the far end of a segment.
|
|
1040
|
+
this.$l.restLength = this.restLengths.at(this.childIdx);
|
|
1041
|
+
this.$l.offset = pb.sub(this.predicted, this.parent);
|
|
1042
|
+
this.$l.dist = pb.length(this.offset);
|
|
1043
|
+
// Coincident with the parent leaves the direction undefined; any
|
|
1044
|
+
// offset of the right length will do.
|
|
1045
|
+
this.$l.projected = pb.add(this.parent, pb.vec3(0, this.restLength, 0));
|
|
1046
|
+
this.$if(pb.greaterThan(this.dist, this.minDistance), function() {
|
|
1047
|
+
this.projected = pb.add(this.parent, pb.mul(pb.div(this.offset, this.dist), this.restLength));
|
|
1048
|
+
});
|
|
1049
|
+
this.Z_hairSetPoint(this.childIdx, this.projected);
|
|
1050
|
+
// The parent takes the reverse momentum, unless it is pinned.
|
|
1051
|
+
this.$if(pb.greaterThan(this.j, 1), function() {
|
|
1052
|
+
this.$l.correction = pb.sub(this.projected, this.predicted);
|
|
1053
|
+
this.Z_hairSetHistory(this.parentIdx, pb.add(this.Z_hairGetHistory(this.parentIdx), pb.mul(this.correction, this.ftlDamping)));
|
|
1054
|
+
});
|
|
1055
|
+
});
|
|
1056
|
+
});
|
|
1057
|
+
// Stage 4 - contacts, and the ceiling on this substep's travel.
|
|
1058
|
+
this.$for(pb.uint('i'), 2, this.pointCount, function() {
|
|
1059
|
+
this.$l.idx = pb.add(this.firstPoint, this.i);
|
|
1060
|
+
this.$l.position = this.Z_hairGetPoint(this.idx);
|
|
1061
|
+
this.$l.history = this.Z_hairGetHistory(this.idx);
|
|
1062
|
+
this.$l.resolved = this.Z_hairResolveContacts(this.position);
|
|
1063
|
+
// A push out is a position fix, not a shove: the history follows
|
|
1064
|
+
// it so the point keeps the velocity it arrived with, and friction
|
|
1065
|
+
// then takes its share of the tangential part.
|
|
1066
|
+
this.$l.push = pb.sub(this.resolved, this.position);
|
|
1067
|
+
this.$l.pushDist = pb.length(this.push);
|
|
1068
|
+
this.history = pb.add(this.history, this.push);
|
|
1069
|
+
this.$if(pb.greaterThan(this.pushDist, this.minDistance), function() {
|
|
1070
|
+
this.$l.normalDir = pb.div(this.push, this.pushDist);
|
|
1071
|
+
this.$l.contactVelocity = pb.sub(this.resolved, this.history);
|
|
1072
|
+
this.$l.tangential = pb.sub(this.contactVelocity, pb.mul(this.normalDir, pb.dot(this.contactVelocity, this.normalDir)));
|
|
1073
|
+
this.history = pb.add(this.history, pb.mul(this.tangential, this.friction));
|
|
1074
|
+
});
|
|
1075
|
+
this.Z_hairSetHistory(this.idx, this.Z_hairClampHistory(this.idx, this.resolved, this.history));
|
|
1076
|
+
this.Z_hairSetPoint(this.idx, this.resolved);
|
|
1077
|
+
});
|
|
1078
|
+
});
|
|
1079
|
+
});
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
});
|
|
1083
|
+
if (program) {
|
|
1084
|
+
program.name = '@GPUHair_Simulate';
|
|
1085
|
+
}
|
|
1086
|
+
return program;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
export { GPUHairSimulation, createHairSimulationProgram, isHairSimulationSupported };
|
|
1090
|
+
//# sourceMappingURL=gpu_hair_simulation.js.map
|