@zephyr3d/scene 0.9.24 → 0.9.26
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/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/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 +58 -24
- 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 +178 -67
- package/dist/asset/model.js.map +1 -1
- package/dist/camera/camera.js +193 -21
- 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 +4040 -1290
- package/dist/index.js +16 -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 +338 -300
- 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 +73 -18
- 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 +766 -456
- 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 +3 -1
- 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 +4 -1
- package/dist/posteffect/motionblur.js.map +1 -1
- package/dist/posteffect/posteffect.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 +91 -34
- 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 +45 -39
- package/dist/render/abuffer_oit.js.map +1 -1
- package/dist/render/cluster_light.js +4 -4
- package/dist/render/cluster_light.js.map +1 -1
- package/dist/render/cull_visitor.js +19 -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 +52 -51
- 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 +3 -0
- package/dist/render/fbm_wavegenerator.js.map +1 -1
- package/dist/render/gerstner_wavegenerator.js +3 -0
- 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/oit.js +29 -0
- package/dist/render/oit.js.map +1 -0
- package/dist/render/render_queue.js +3 -0
- package/dist/render/render_queue.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 +61 -1
- package/dist/render/rendergraph/forward_plus_builder.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/weightedblended_oit.js +36 -35
- 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 +273 -249
- 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 +6 -0
- 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/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 +50 -50
- 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 +431 -100
- package/dist/utility/serialization/manager.js.map +1 -1
- package/dist/utility/serialization/scene/animation.js +370 -3
- 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 +133 -1
- 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 +92 -69
- 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 +4 -0
- package/dist/utility/serialization/scene/water.js.map +1 -1
- package/dist/utility/serialization/types.js.map +1 -1
- package/dist/utility/shprojector.js +96 -95
- package/dist/utility/shprojector.js.map +1 -1
- package/dist/values.js +105 -58
- package/dist/values.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,963 @@
|
|
|
1
|
+
import { applyMixins, DRef, Vector3 } from '@zephyr3d/base';
|
|
2
|
+
import { GraphNode } from './graph_node.js';
|
|
3
|
+
import { mixinDrawable } from '../render/drawable_mixin.js';
|
|
4
|
+
import { QUEUE_OPAQUE } from '../values.js';
|
|
5
|
+
import '../render/render_queue.js';
|
|
6
|
+
import { getEngine } from '../app/api.js';
|
|
7
|
+
import { Primitive } from '../render/primitive.js';
|
|
8
|
+
import '@zephyr3d/device';
|
|
9
|
+
import '../material/shader/helper.js';
|
|
10
|
+
import '../material/lambert.js';
|
|
11
|
+
import '../material/blinn.js';
|
|
12
|
+
import '../material/unlit.js';
|
|
13
|
+
import '../material/particle.js';
|
|
14
|
+
import '../material/subsurfaceprofile.js';
|
|
15
|
+
import '../material/skin.js';
|
|
16
|
+
import '../material/eye.js';
|
|
17
|
+
import '../material/meshmaterial.js';
|
|
18
|
+
import '../material/hair.js';
|
|
19
|
+
import { HairStrandMaterial } from '../material/hairstrand.js';
|
|
20
|
+
import { HairStrandData } from '../material/hairstrand_data.js';
|
|
21
|
+
import '../material/grassmaterial.js';
|
|
22
|
+
import '../material/terrain-cm.js';
|
|
23
|
+
import '../material/pbrmr.js';
|
|
24
|
+
import '../material/pbrsg.js';
|
|
25
|
+
import '../material/mtoon.js';
|
|
26
|
+
import '../material/pbrblueprint.js';
|
|
27
|
+
import '../material/sprite.js';
|
|
28
|
+
import '../material/sprite_std.js';
|
|
29
|
+
import '../utility/blueprint/material/texture.js';
|
|
30
|
+
import '../render/rendergraph/forward_plus_builder.js';
|
|
31
|
+
import '../render/lightpass.js';
|
|
32
|
+
import '../render/sky.js';
|
|
33
|
+
import '../render/clipmap.js';
|
|
34
|
+
import '../render/envlight.js';
|
|
35
|
+
import '../render/dualdepthpeeling_oit.js';
|
|
36
|
+
import '../shaders/atmosphere.js';
|
|
37
|
+
import '../render/rendergraph/scene_render_context.js';
|
|
38
|
+
import { isHairSimulationSupported, GPUHairSimulation } from '../animation/hair/gpu_hair_simulation.js';
|
|
39
|
+
import { parseZHair } from '../asset/loaders/zhair/zhair_format.js';
|
|
40
|
+
import { mergeHairStrandSources, loadZHairStrandSources } from '../asset/loaders/zhair/zhair_loader.js';
|
|
41
|
+
import { BoundingBox } from '../utility/bounding_volume.js';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Hair scene node.
|
|
45
|
+
*
|
|
46
|
+
* @remarks
|
|
47
|
+
* Point it at a `.zhair` asset with {@link HairNode.setHairAsset}, or hand it
|
|
48
|
+
* control points directly with {@link HairNode.setStrands}. Everything the draw
|
|
49
|
+
* call needs is derived from them.
|
|
50
|
+
*
|
|
51
|
+
* @public
|
|
52
|
+
*/ class HairNode extends applyMixins(GraphNode, mixinDrawable) {
|
|
53
|
+
/** @internal */ _pickTarget;
|
|
54
|
+
/** @internal */ _material;
|
|
55
|
+
/** @internal */ _primitive;
|
|
56
|
+
/** @internal */ _strands;
|
|
57
|
+
/** @internal Local-space bounds of the control points, width included. */ _bounds;
|
|
58
|
+
/** @internal Length of the longest strand, for padding the bounds while simulating. */ _maxStrandLength;
|
|
59
|
+
/** @internal */ _castShadow;
|
|
60
|
+
/** @internal Kept so the simulation can be rebuilt from the authored pose. */ _source;
|
|
61
|
+
/** @internal Path of the `.zhair` asset, empty when strands were set directly. */ _hairAsset;
|
|
62
|
+
/**
|
|
63
|
+
* @internal The opened asset, kept so decimation can be changed without
|
|
64
|
+
* reading the file again.
|
|
65
|
+
*/ _parsedAsset;
|
|
66
|
+
/** @internal */ _strandStride;
|
|
67
|
+
/** @internal */ _maxStrands;
|
|
68
|
+
/** @internal */ _simulation;
|
|
69
|
+
/** @internal */ _simulationEnabled;
|
|
70
|
+
/**
|
|
71
|
+
* @internal Simulation tuning, owned by the node.
|
|
72
|
+
*
|
|
73
|
+
* Held here rather than on the simulation because the node outlives it: the
|
|
74
|
+
* dials survive the simulation being switched off and on, and can be set
|
|
75
|
+
* before any strands have loaded.
|
|
76
|
+
*/ _simulationOptions;
|
|
77
|
+
/**
|
|
78
|
+
* Creates an empty hair node.
|
|
79
|
+
*
|
|
80
|
+
* @param scene - Scene the node belongs to.
|
|
81
|
+
*/ constructor(scene){
|
|
82
|
+
super(scene);
|
|
83
|
+
this._pickTarget = {
|
|
84
|
+
node: this
|
|
85
|
+
};
|
|
86
|
+
this._material = new DRef(new HairStrandMaterial());
|
|
87
|
+
this._primitive = new DRef();
|
|
88
|
+
this._strands = new DRef();
|
|
89
|
+
this._bounds = null;
|
|
90
|
+
this._maxStrandLength = 0;
|
|
91
|
+
this._castShadow = true;
|
|
92
|
+
this._source = null;
|
|
93
|
+
this._hairAsset = '';
|
|
94
|
+
this._parsedAsset = null;
|
|
95
|
+
this._strandStride = 1;
|
|
96
|
+
this._maxStrands = 0;
|
|
97
|
+
this._simulation = null;
|
|
98
|
+
this._simulationEnabled = false;
|
|
99
|
+
this._simulationOptions = {
|
|
100
|
+
gravity: new Vector3(0, -9.8, 0),
|
|
101
|
+
damping: 0.08,
|
|
102
|
+
globalStiffness: 0,
|
|
103
|
+
globalRange: 0,
|
|
104
|
+
localStiffness: 0.9,
|
|
105
|
+
localIterations: 2,
|
|
106
|
+
ftlDamping: 0.7,
|
|
107
|
+
vspCoeff: 0.8,
|
|
108
|
+
vspAccelThreshold: 50,
|
|
109
|
+
substeps: 1,
|
|
110
|
+
friction: 0.2,
|
|
111
|
+
maxSpeedFactor: 4
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
//
|
|
115
|
+
// Geometry
|
|
116
|
+
//
|
|
117
|
+
/** Path of the `.zhair` asset this node draws, or an empty string. */ get hairAsset() {
|
|
118
|
+
return this._hairAsset;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Loads a `.zhair` asset.
|
|
122
|
+
*
|
|
123
|
+
* @remarks
|
|
124
|
+
* The opened file is kept, so changing {@link HairNode.strandStride} or
|
|
125
|
+
* {@link HairNode.maxStrands} afterwards re-decimates what is already in
|
|
126
|
+
* memory instead of reading it again - and never touches the source archive
|
|
127
|
+
* the asset was converted from.
|
|
128
|
+
*
|
|
129
|
+
* @param path - Asset path, or an empty string to clear the node.
|
|
130
|
+
*/ async setHairAsset(path) {
|
|
131
|
+
const assetId = path?.trim() ?? '';
|
|
132
|
+
if (assetId === this._hairAsset) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
this._hairAsset = assetId;
|
|
136
|
+
this._parsedAsset = null;
|
|
137
|
+
if (!assetId) {
|
|
138
|
+
this.setStrands(null);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
let parsed;
|
|
142
|
+
try {
|
|
143
|
+
const data = await getEngine().resourceManager.assetManager.fetchBinaryData(assetId);
|
|
144
|
+
parsed = parseZHair(data);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
console.error(`Load hair asset failed: ${assetId}: ${err}`);
|
|
147
|
+
this.setStrands(null);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
// A load that was superseded while it was in flight must not overwrite the
|
|
151
|
+
// asset that replaced it.
|
|
152
|
+
if (this._hairAsset !== assetId) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
this._parsedAsset = parsed;
|
|
156
|
+
this._applyAssetDecimation();
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Draw one strand in every `strandStride`.
|
|
160
|
+
*
|
|
161
|
+
* @remarks
|
|
162
|
+
* A stride rather than a leading slice, so the kept strands stay spread over
|
|
163
|
+
* the whole scalp instead of leaving a bald patch. Only meaningful when the
|
|
164
|
+
* strands came from an asset; strands set directly are used as given.
|
|
165
|
+
*/ get strandStride() {
|
|
166
|
+
return this._strandStride;
|
|
167
|
+
}
|
|
168
|
+
set strandStride(val) {
|
|
169
|
+
const v = val < 1 ? 1 : val | 0;
|
|
170
|
+
if (v !== this._strandStride) {
|
|
171
|
+
this._strandStride = v;
|
|
172
|
+
this._applyAssetDecimation();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Upper bound on the number of strands drawn, or 0 for no bound.
|
|
177
|
+
*
|
|
178
|
+
* @remarks
|
|
179
|
+
* Applied on top of {@link HairNode.strandStride} by widening the stride
|
|
180
|
+
* further, so the result stays evenly spread.
|
|
181
|
+
*/ get maxStrands() {
|
|
182
|
+
return this._maxStrands;
|
|
183
|
+
}
|
|
184
|
+
set maxStrands(val) {
|
|
185
|
+
const v = val < 0 ? 0 : val | 0;
|
|
186
|
+
if (v !== this._maxStrands) {
|
|
187
|
+
this._maxStrands = v;
|
|
188
|
+
this._applyAssetDecimation();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** GPU-resident strand geometry, or null when none has been assigned. */ get strands() {
|
|
192
|
+
return this._strands.get();
|
|
193
|
+
}
|
|
194
|
+
/** Number of strands currently drawn. */ get strandCount() {
|
|
195
|
+
return this._strands.get()?.strandCount ?? 0;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Replaces the strands this node draws.
|
|
199
|
+
*
|
|
200
|
+
* @remarks
|
|
201
|
+
* The direct route, for strands built in code rather than loaded from an
|
|
202
|
+
* asset. Uploads the control points, sizes the draw call and recomputes the
|
|
203
|
+
* bounding box; the previous strand data is released and any running
|
|
204
|
+
* simulation is rebuilt, since its rest pose comes from the strands.
|
|
205
|
+
*
|
|
206
|
+
* @param source - Control points and topology, or null to clear the node.
|
|
207
|
+
*/ setStrands(source) {
|
|
208
|
+
this._disposeSimulation();
|
|
209
|
+
this._strands.dispose();
|
|
210
|
+
this._bounds = null;
|
|
211
|
+
this._maxStrandLength = 0;
|
|
212
|
+
this._source = null;
|
|
213
|
+
if (!source) {
|
|
214
|
+
this.material.strands = null;
|
|
215
|
+
this._primitive.dispose();
|
|
216
|
+
this.invalidateBoundingVolume();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const data = new HairStrandData(source);
|
|
220
|
+
this._strands.set(data);
|
|
221
|
+
this._source = source;
|
|
222
|
+
this.material.strands = data;
|
|
223
|
+
const extents = computeStrandExtents(source);
|
|
224
|
+
this._bounds = extents.bounds;
|
|
225
|
+
this._maxStrandLength = extents.maxStrandLength;
|
|
226
|
+
this._ensurePrimitive();
|
|
227
|
+
this._syncDrawRange();
|
|
228
|
+
this.invalidateBoundingVolume();
|
|
229
|
+
if (this._simulationEnabled) {
|
|
230
|
+
this._createSimulation();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
//
|
|
234
|
+
// Shading
|
|
235
|
+
//
|
|
236
|
+
// The material is an implementation detail of this node - see the class
|
|
237
|
+
// remarks - so its controls are surfaced here. Only the ones that do something
|
|
238
|
+
// on the strand path are: the vertex shader emits no texture coordinates, so
|
|
239
|
+
// the material's albedo, shift and occlusion maps have nothing to sample and
|
|
240
|
+
// are deliberately left unreachable.
|
|
241
|
+
//
|
|
242
|
+
/** Base colour of the strands. */ get albedoColor() {
|
|
243
|
+
return this.material.albedoColor;
|
|
244
|
+
}
|
|
245
|
+
set albedoColor(value) {
|
|
246
|
+
this.material.albedoColor = value;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Which scattering model shades the hair.
|
|
250
|
+
*
|
|
251
|
+
* @remarks
|
|
252
|
+
* `kajiya-kay` is the art-directed double lobe; `marschner` derives the lobes
|
|
253
|
+
* from the fibre and is the one that renders back-lighting.
|
|
254
|
+
*/ get shadingModel() {
|
|
255
|
+
return this.material.shadingModel;
|
|
256
|
+
}
|
|
257
|
+
set shadingModel(value) {
|
|
258
|
+
this.material.shadingModel = value;
|
|
259
|
+
}
|
|
260
|
+
/** Colour of the primary, near-white specular lobe. */ get specular1Color() {
|
|
261
|
+
return this.material.specular1Color;
|
|
262
|
+
}
|
|
263
|
+
set specular1Color(value) {
|
|
264
|
+
this.material.specular1Color = value;
|
|
265
|
+
}
|
|
266
|
+
/** Exponent of the primary specular lobe. */ get specular1Power() {
|
|
267
|
+
return this.material.specular1Power;
|
|
268
|
+
}
|
|
269
|
+
set specular1Power(value) {
|
|
270
|
+
this.material.specular1Power = value;
|
|
271
|
+
}
|
|
272
|
+
/** Shift of the primary specular lobe along the strand. */ get specular1Shift() {
|
|
273
|
+
return this.material.specular1Shift;
|
|
274
|
+
}
|
|
275
|
+
set specular1Shift(value) {
|
|
276
|
+
this.material.specular1Shift = value;
|
|
277
|
+
}
|
|
278
|
+
/** Colour of the secondary, tinted specular lobe. */ get specular2Color() {
|
|
279
|
+
return this.material.specular2Color;
|
|
280
|
+
}
|
|
281
|
+
set specular2Color(value) {
|
|
282
|
+
this.material.specular2Color = value;
|
|
283
|
+
}
|
|
284
|
+
/** Exponent of the secondary specular lobe. */ get specular2Power() {
|
|
285
|
+
return this.material.specular2Power;
|
|
286
|
+
}
|
|
287
|
+
set specular2Power(value) {
|
|
288
|
+
this.material.specular2Power = value;
|
|
289
|
+
}
|
|
290
|
+
/** Shift of the secondary specular lobe along the strand. */ get specular2Shift() {
|
|
291
|
+
return this.material.specular2Shift;
|
|
292
|
+
}
|
|
293
|
+
set specular2Shift(value) {
|
|
294
|
+
this.material.specular2Shift = value;
|
|
295
|
+
}
|
|
296
|
+
/** How far diffuse lighting wraps around the fibre. */ get diffuseWrap() {
|
|
297
|
+
return this.material.diffuseWrap;
|
|
298
|
+
}
|
|
299
|
+
set diffuseWrap(value) {
|
|
300
|
+
this.material.diffuseWrap = value;
|
|
301
|
+
}
|
|
302
|
+
/** Colour of light transmitted through the fibre. */ get transmissionColor() {
|
|
303
|
+
return this.material.transmissionColor;
|
|
304
|
+
}
|
|
305
|
+
set transmissionColor(value) {
|
|
306
|
+
this.material.transmissionColor = value;
|
|
307
|
+
}
|
|
308
|
+
/** Strength of back-lit transmission. */ get transmissionIntensity() {
|
|
309
|
+
return this.material.transmissionIntensity;
|
|
310
|
+
}
|
|
311
|
+
set transmissionIntensity(value) {
|
|
312
|
+
this.material.transmissionIntensity = value;
|
|
313
|
+
}
|
|
314
|
+
/** Falloff exponent of back-lit transmission. */ get transmissionPower() {
|
|
315
|
+
return this.material.transmissionPower;
|
|
316
|
+
}
|
|
317
|
+
set transmissionPower(value) {
|
|
318
|
+
this.material.transmissionPower = value;
|
|
319
|
+
}
|
|
320
|
+
/** Colour of multiple scattering within the groom. */ get scatterColor() {
|
|
321
|
+
return this.material.scatterColor;
|
|
322
|
+
}
|
|
323
|
+
set scatterColor(value) {
|
|
324
|
+
this.material.scatterColor = value;
|
|
325
|
+
}
|
|
326
|
+
/** Strength of multiple scattering. */ get scatterIntensity() {
|
|
327
|
+
return this.material.scatterIntensity;
|
|
328
|
+
}
|
|
329
|
+
set scatterIntensity(value) {
|
|
330
|
+
this.material.scatterIntensity = value;
|
|
331
|
+
}
|
|
332
|
+
/** Share of scattering taken as local rather than global. */ get scatterLocal() {
|
|
333
|
+
return this.material.scatterLocal;
|
|
334
|
+
}
|
|
335
|
+
set scatterLocal(value) {
|
|
336
|
+
this.material.scatterLocal = value;
|
|
337
|
+
}
|
|
338
|
+
/** How far scattered light wraps around the fibre. */ get scatterWrap() {
|
|
339
|
+
return this.material.scatterWrap;
|
|
340
|
+
}
|
|
341
|
+
set scatterWrap(value) {
|
|
342
|
+
this.material.scatterWrap = value;
|
|
343
|
+
}
|
|
344
|
+
/** Longitudinal shift of the Marschner lobes. */ get marschnerShift() {
|
|
345
|
+
return this.material.marschnerShift;
|
|
346
|
+
}
|
|
347
|
+
set marschnerShift(value) {
|
|
348
|
+
this.material.marschnerShift = value;
|
|
349
|
+
}
|
|
350
|
+
/** Longitudinal roughness of the Marschner lobes. */ get marschnerRoughness() {
|
|
351
|
+
return this.material.marschnerRoughness;
|
|
352
|
+
}
|
|
353
|
+
set marschnerRoughness(value) {
|
|
354
|
+
this.material.marschnerRoughness = value;
|
|
355
|
+
}
|
|
356
|
+
/** Index of refraction of the fibre. */ get marschnerIOR() {
|
|
357
|
+
return this.material.marschnerIOR;
|
|
358
|
+
}
|
|
359
|
+
set marschnerIOR(value) {
|
|
360
|
+
this.material.marschnerIOR = value;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* How strongly pigmented the fibre is.
|
|
364
|
+
*
|
|
365
|
+
* @remarks
|
|
366
|
+
* A multiplier on the distance transmitted light travels before leaving, so
|
|
367
|
+
* raising it deepens the colour of the TT and TRT paths without touching the
|
|
368
|
+
* white surface reflection.
|
|
369
|
+
*/ get marschnerAbsorption() {
|
|
370
|
+
return this.material.marschnerAbsorption;
|
|
371
|
+
}
|
|
372
|
+
set marschnerAbsorption(value) {
|
|
373
|
+
this.material.marschnerAbsorption = value;
|
|
374
|
+
}
|
|
375
|
+
/** Relative weight of the R, TT and TRT lobes. */ get marschnerLobes() {
|
|
376
|
+
return this.material.marschnerLobes;
|
|
377
|
+
}
|
|
378
|
+
set marschnerLobes(value) {
|
|
379
|
+
this.material.marschnerLobes = value;
|
|
380
|
+
}
|
|
381
|
+
//
|
|
382
|
+
// Strand expansion
|
|
383
|
+
//
|
|
384
|
+
/**
|
|
385
|
+
* Ribbon segments generated per strand.
|
|
386
|
+
*
|
|
387
|
+
* @remarks
|
|
388
|
+
* The main quality dial, and independent of how many control points a strand
|
|
389
|
+
* has: the shader resamples the curve, so the same 30-point strand can be
|
|
390
|
+
* drawn with 12 segments close up and 3 far away.
|
|
391
|
+
*/ get segmentsPerStrand() {
|
|
392
|
+
return this.material.segmentsPerStrand;
|
|
393
|
+
}
|
|
394
|
+
set segmentsPerStrand(val) {
|
|
395
|
+
if (val !== this.material.segmentsPerStrand) {
|
|
396
|
+
this.material.segmentsPerStrand = val;
|
|
397
|
+
this._syncDrawRange();
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
/** Multiplier on the width stored in the strand data. */ get strandWidthScale() {
|
|
401
|
+
return this.material.strandWidthScale;
|
|
402
|
+
}
|
|
403
|
+
set strandWidthScale(value) {
|
|
404
|
+
this.material.strandWidthScale = value;
|
|
405
|
+
}
|
|
406
|
+
/** Lower bound on ribbon width, in world units. */ get minStrandWidth() {
|
|
407
|
+
return this.material.minStrandWidth;
|
|
408
|
+
}
|
|
409
|
+
set minStrandWidth(value) {
|
|
410
|
+
this.material.minStrandWidth = value;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Lower bound on ribbon width, in pixels.
|
|
414
|
+
*
|
|
415
|
+
* @remarks
|
|
416
|
+
* Real hair is thinner than a pixel and rasterises to a flickering dotted
|
|
417
|
+
* line. Widening it to about a pixel and paying the extra coverage back in
|
|
418
|
+
* alpha keeps distant hair fading rather than breaking up.
|
|
419
|
+
*/ get minPixelWidth() {
|
|
420
|
+
return this.material.minPixelWidth;
|
|
421
|
+
}
|
|
422
|
+
set minPixelWidth(value) {
|
|
423
|
+
this.material.minPixelWidth = value;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* How far the shading normal bends across the ribbon, in [0, 1].
|
|
427
|
+
*
|
|
428
|
+
* @remarks
|
|
429
|
+
* At 0 a strand shades as a flat tape, at 1 as a cylinder.
|
|
430
|
+
*/ get strandRoundness() {
|
|
431
|
+
return this.material.strandRoundness;
|
|
432
|
+
}
|
|
433
|
+
set strandRoundness(value) {
|
|
434
|
+
this.material.strandRoundness = value;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Strength of the root-depth ambient occlusion, in [0, 1].
|
|
438
|
+
*
|
|
439
|
+
* @remarks
|
|
440
|
+
* Environment irradiance is not attenuated by anything - a shadow map answers
|
|
441
|
+
* for one light, not for the sky - so without this every strand of a groom
|
|
442
|
+
* receives the full sky and a dense hairstyle is lit as brightly in its
|
|
443
|
+
* interior as on its surface, which reads as a flat silhouette with no volume.
|
|
444
|
+
* Most obvious on pale hair, where there is little pigment to darken the
|
|
445
|
+
* interior instead. Set to 0 to disable.
|
|
446
|
+
*
|
|
447
|
+
* Ambient light only: direct light is already attenuated by the shadow map,
|
|
448
|
+
* and under DOM by a graded transmittance, so it is left alone.
|
|
449
|
+
*/ get rootOcclusion() {
|
|
450
|
+
return this.material.rootOcclusion;
|
|
451
|
+
}
|
|
452
|
+
set rootOcclusion(value) {
|
|
453
|
+
this.material.rootOcclusion = value;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* How far along a strand the root occlusion reaches, in (0, 1].
|
|
457
|
+
*
|
|
458
|
+
* @remarks
|
|
459
|
+
* Short for a groom whose strands leave the scalp quickly, longer for one
|
|
460
|
+
* where they lie against each other most of their length.
|
|
461
|
+
*/ get rootOcclusionRange() {
|
|
462
|
+
return this.material.rootOcclusionRange;
|
|
463
|
+
}
|
|
464
|
+
set rootOcclusionRange(value) {
|
|
465
|
+
this.material.rootOcclusionRange = value;
|
|
466
|
+
}
|
|
467
|
+
/** Drop strands with distance, redistributing their coverage to the survivors. */ get strandLOD() {
|
|
468
|
+
return this.material.strandLOD;
|
|
469
|
+
}
|
|
470
|
+
set strandLOD(value) {
|
|
471
|
+
this.material.strandLOD = value;
|
|
472
|
+
}
|
|
473
|
+
/** Floor on the fraction of strands distance decimation keeps. */ get minStrandLODRatio() {
|
|
474
|
+
return this.material.minStrandLODRatio;
|
|
475
|
+
}
|
|
476
|
+
set minStrandLODRatio(value) {
|
|
477
|
+
this.material.minStrandLODRatio = value;
|
|
478
|
+
}
|
|
479
|
+
//
|
|
480
|
+
// Transparency and shadows
|
|
481
|
+
//
|
|
482
|
+
/**
|
|
483
|
+
* How the strands blend with what is behind them.
|
|
484
|
+
*
|
|
485
|
+
* @remarks
|
|
486
|
+
* The pixel-width floor pays its widening back in alpha, which only means
|
|
487
|
+
* anything once blending is on; left at `none` every strand covers its pixel
|
|
488
|
+
* fully and a dense groom reads as a solid shell.
|
|
489
|
+
*/ get blendMode() {
|
|
490
|
+
return this.material.blendMode;
|
|
491
|
+
}
|
|
492
|
+
set blendMode(value) {
|
|
493
|
+
this.material.blendMode = value;
|
|
494
|
+
}
|
|
495
|
+
/** Alpha below which a fragment is discarded. */ get alphaCutoff() {
|
|
496
|
+
return this.material.alphaCutoff;
|
|
497
|
+
}
|
|
498
|
+
set alphaCutoff(value) {
|
|
499
|
+
this.material.alphaCutoff = value;
|
|
500
|
+
}
|
|
501
|
+
/** Dither the alpha test, so a temporal filter resolves the soft edge. */ get alphaDither() {
|
|
502
|
+
return this.material.alphaDither;
|
|
503
|
+
}
|
|
504
|
+
set alphaDither(value) {
|
|
505
|
+
this.material.alphaDither = value;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Whether a blended groom contributes motion vectors.
|
|
509
|
+
*
|
|
510
|
+
* @remarks
|
|
511
|
+
* Only meaningful with {@link HairNode.blendMode} set to something other than
|
|
512
|
+
* `none`. Blended geometry is otherwise absent from the prepass, so a groom
|
|
513
|
+
* carries the velocity of whatever is behind it and a temporal filter smears
|
|
514
|
+
* it as it swings - the same ghosting the dithered path avoids by being
|
|
515
|
+
* opaque. On by default, because a groom is dense enough to own its pixels
|
|
516
|
+
* and there is no reason to want the ghosting.
|
|
517
|
+
*/ get transparentMotionVector() {
|
|
518
|
+
return this.material.transparentMotionVector;
|
|
519
|
+
}
|
|
520
|
+
set transparentMotionVector(value) {
|
|
521
|
+
this.material.transparentMotionVector = value;
|
|
522
|
+
}
|
|
523
|
+
/** Whether the groom is drawn into shadow maps. */ get castShadow() {
|
|
524
|
+
return this._castShadow;
|
|
525
|
+
}
|
|
526
|
+
set castShadow(val) {
|
|
527
|
+
this._castShadow = !!val;
|
|
528
|
+
}
|
|
529
|
+
/** Let the shadow caster pass respect strand alpha, for softer self-shadowing. */ get transparentShadowCaster() {
|
|
530
|
+
return this.material.transparentShadowCaster;
|
|
531
|
+
}
|
|
532
|
+
set transparentShadowCaster(value) {
|
|
533
|
+
this.material.transparentShadowCaster = value;
|
|
534
|
+
}
|
|
535
|
+
/** Alpha below which a fragment casts no shadow. */ get shadowAlphaCutoff() {
|
|
536
|
+
return this.material.shadowAlphaCutoff;
|
|
537
|
+
}
|
|
538
|
+
set shadowAlphaCutoff(value) {
|
|
539
|
+
this.material.shadowAlphaCutoff = value;
|
|
540
|
+
}
|
|
541
|
+
//
|
|
542
|
+
// Simulation
|
|
543
|
+
//
|
|
544
|
+
/**
|
|
545
|
+
* Whether strand dynamics run on this groom.
|
|
546
|
+
*
|
|
547
|
+
* @remarks
|
|
548
|
+
* WebGPU only. Switching it on with no strands loaded is allowed - the
|
|
549
|
+
* simulation is built when the strands arrive.
|
|
550
|
+
*/ get simulationEnabled() {
|
|
551
|
+
return this._simulationEnabled;
|
|
552
|
+
}
|
|
553
|
+
set simulationEnabled(val) {
|
|
554
|
+
const enabled = !!val;
|
|
555
|
+
if (enabled === this._simulationEnabled) {
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
this._simulationEnabled = enabled;
|
|
559
|
+
if (enabled) {
|
|
560
|
+
this._createSimulation();
|
|
561
|
+
} else {
|
|
562
|
+
this._disposeSimulation();
|
|
563
|
+
// Without this the groom keeps whatever pose it was last simulated into,
|
|
564
|
+
// which reads as a bug rather than as hair that stopped moving.
|
|
565
|
+
if (this._source) {
|
|
566
|
+
this._strands.get()?.dispose();
|
|
567
|
+
this._strands.set(new HairStrandData(this._source));
|
|
568
|
+
this.material.strands = this._strands.get();
|
|
569
|
+
}
|
|
570
|
+
// Back to the tight rest bounds.
|
|
571
|
+
this.invalidateBoundingVolume();
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* The running strand dynamics, or null when they are off or unsupported.
|
|
576
|
+
*
|
|
577
|
+
* @remarks
|
|
578
|
+
* Exposed so a caller can reach past the node's own dials - to set colliders,
|
|
579
|
+
* for one, which have no serialised form here.
|
|
580
|
+
*/ get simulation() {
|
|
581
|
+
return this._simulation;
|
|
582
|
+
}
|
|
583
|
+
/** World-space gravity acting on the strands. */ get gravity() {
|
|
584
|
+
return this._simulationOptions.gravity;
|
|
585
|
+
}
|
|
586
|
+
set gravity(value) {
|
|
587
|
+
this._simulationOptions.gravity = new Vector3(value);
|
|
588
|
+
if (this._simulation) {
|
|
589
|
+
this._simulation.gravity = this._simulationOptions.gravity;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* How strongly strands keep the shape they were authored with, in [0, 1].
|
|
594
|
+
*
|
|
595
|
+
* @remarks
|
|
596
|
+
* The dial to reach for first. It holds the angle each segment makes with the
|
|
597
|
+
* one before it, measured against that segment's current direction rather
|
|
598
|
+
* than a fixed pose, so a groom keeps its curl while remaining free to hang
|
|
599
|
+
* and swing. It is also what damps a groom: without bending resistance a
|
|
600
|
+
* strand carries a disturbance up and down its length for seconds.
|
|
601
|
+
*/ get localStiffness() {
|
|
602
|
+
return this._simulationOptions.localStiffness;
|
|
603
|
+
}
|
|
604
|
+
set localStiffness(value) {
|
|
605
|
+
this._simulationOptions.localStiffness = value;
|
|
606
|
+
if (this._simulation) {
|
|
607
|
+
this._simulation.localStiffness = value;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
/** Local shape constraint passes per substep. */ get localIterations() {
|
|
611
|
+
return this._simulationOptions.localIterations;
|
|
612
|
+
}
|
|
613
|
+
set localIterations(value) {
|
|
614
|
+
this._simulationOptions.localIterations = value;
|
|
615
|
+
if (this._simulation) {
|
|
616
|
+
this._simulation.localIterations = value;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* How strongly strands are pulled back to their authored pose, in [0, 1].
|
|
621
|
+
*
|
|
622
|
+
* @remarks
|
|
623
|
+
* Off by default. It anchors a point to a fixed place rather than to its
|
|
624
|
+
* neighbours, which is a spring, and a groom held by springs reads as one.
|
|
625
|
+
* {@link HairNode.localStiffness} is what holds styling without that; this is
|
|
626
|
+
* for grooms that genuinely should not move far from how they were authored,
|
|
627
|
+
* and wants {@link HairNode.globalRange} set as well.
|
|
628
|
+
*/ get globalStiffness() {
|
|
629
|
+
return this._simulationOptions.globalStiffness;
|
|
630
|
+
}
|
|
631
|
+
set globalStiffness(value) {
|
|
632
|
+
this._simulationOptions.globalStiffness = value;
|
|
633
|
+
if (this._simulation) {
|
|
634
|
+
this._simulation.globalStiffness = value;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
/** Fraction of a strand, from the root, the pose pull acts on, in [0, 1]. */ get globalRange() {
|
|
638
|
+
return this._simulationOptions.globalRange;
|
|
639
|
+
}
|
|
640
|
+
set globalRange(value) {
|
|
641
|
+
this._simulationOptions.globalRange = value;
|
|
642
|
+
if (this._simulation) {
|
|
643
|
+
this._simulation.globalRange = value;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Share of a point's length correction fed back into its parent, in [0, 1].
|
|
648
|
+
*
|
|
649
|
+
* @remarks
|
|
650
|
+
* What keeps the solver from gaining energy as the node moves. Lowering it
|
|
651
|
+
* makes a groom livelier and, past a point, makes it whip; 0 removes the
|
|
652
|
+
* feedback entirely.
|
|
653
|
+
*/ get ftlDamping() {
|
|
654
|
+
return this._simulationOptions.ftlDamping;
|
|
655
|
+
}
|
|
656
|
+
set ftlDamping(value) {
|
|
657
|
+
this._simulationOptions.ftlDamping = value;
|
|
658
|
+
if (this._simulation) {
|
|
659
|
+
this._simulation.ftlDamping = value;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
/** Velocity lost each step, in [0, 1]. */ get damping() {
|
|
663
|
+
return this._simulationOptions.damping;
|
|
664
|
+
}
|
|
665
|
+
set damping(value) {
|
|
666
|
+
this._simulationOptions.damping = value;
|
|
667
|
+
if (this._simulation) {
|
|
668
|
+
this._simulation.damping = value;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
/** Tangential motion removed at a contact, in [0, 1]. */ get friction() {
|
|
672
|
+
return this._simulationOptions.friction;
|
|
673
|
+
}
|
|
674
|
+
set friction(value) {
|
|
675
|
+
this._simulationOptions.friction = value;
|
|
676
|
+
if (this._simulation) {
|
|
677
|
+
this._simulation.friction = value;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* How much of the node's motion strands are carried along by, in [0, 1].
|
|
682
|
+
*
|
|
683
|
+
* @remarks
|
|
684
|
+
* 0 leaves a groom exactly where it was in the world and lets the pinned
|
|
685
|
+
* roots drag it along, which is what produces lag; 1 carries it rigidly with
|
|
686
|
+
* the node, so it never lags. Lower values give a livelier, floppier groom.
|
|
687
|
+
*/ get vspCoeff() {
|
|
688
|
+
return this._simulationOptions.vspCoeff;
|
|
689
|
+
}
|
|
690
|
+
set vspCoeff(value) {
|
|
691
|
+
this._simulationOptions.vspCoeff = value;
|
|
692
|
+
if (this._simulation) {
|
|
693
|
+
this._simulation.vspCoeff = value;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
/** Node acceleration above which strands are carried rigidly, in units/s². */ get vspAccelThreshold() {
|
|
697
|
+
return this._simulationOptions.vspAccelThreshold;
|
|
698
|
+
}
|
|
699
|
+
set vspAccelThreshold(value) {
|
|
700
|
+
this._simulationOptions.vspAccelThreshold = value;
|
|
701
|
+
if (this._simulation) {
|
|
702
|
+
this._simulation.vspAccelThreshold = value;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Per-substep ceiling on how far a point may travel, in segment lengths.
|
|
707
|
+
*
|
|
708
|
+
* @remarks
|
|
709
|
+
* A backstop against a single bad substep, not a dial to shape motion with:
|
|
710
|
+
* at the default it never engages during ordinary movement.
|
|
711
|
+
*/ get maxSpeedFactor() {
|
|
712
|
+
return this._simulationOptions.maxSpeedFactor;
|
|
713
|
+
}
|
|
714
|
+
set maxSpeedFactor(value) {
|
|
715
|
+
this._simulationOptions.maxSpeedFactor = value;
|
|
716
|
+
if (this._simulation) {
|
|
717
|
+
this._simulation.maxSpeedFactor = value;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
/** Integration substeps per fixed step. */ get substeps() {
|
|
721
|
+
return this._simulationOptions.substeps;
|
|
722
|
+
}
|
|
723
|
+
set substeps(value) {
|
|
724
|
+
this._simulationOptions.substeps = value;
|
|
725
|
+
if (this._simulation) {
|
|
726
|
+
this._simulation.substeps = value;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
//
|
|
730
|
+
// Scene node and drawable
|
|
731
|
+
//
|
|
732
|
+
/**
|
|
733
|
+
* {@inheritDoc SceneNode.update}
|
|
734
|
+
*
|
|
735
|
+
* @remarks
|
|
736
|
+
* Drives the strand solver. The node re-queues itself every frame, because a
|
|
737
|
+
* simulation only stands still when it is switched off.
|
|
738
|
+
*/ update(frameId, elapsedInSeconds, deltaInSeconds) {
|
|
739
|
+
super.update(frameId, elapsedInSeconds, deltaInSeconds);
|
|
740
|
+
const simulation = this._simulation;
|
|
741
|
+
if (simulation?.enabled) {
|
|
742
|
+
simulation.update(deltaInSeconds, this.worldMatrix);
|
|
743
|
+
this.scene?.queueUpdateNode(this);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* {@inheritDoc SceneNode.computeBoundingVolume}
|
|
748
|
+
*
|
|
749
|
+
* @remarks
|
|
750
|
+
* Derived on the CPU from the control points, because the ribbons are built in
|
|
751
|
+
* the vertex shader and the engine never sees them. The box is in local space;
|
|
752
|
+
* the base class applies the world matrix.
|
|
753
|
+
*
|
|
754
|
+
* While a simulation runs, the strands swing out of their authored pose, so
|
|
755
|
+
* the rest bounds are padded by the longest strand: the solver keeps every
|
|
756
|
+
* point within its strand's length of the pinned root, which bounds any pose
|
|
757
|
+
* it can produce.
|
|
758
|
+
*/ computeBoundingVolume() {
|
|
759
|
+
if (!this._bounds) {
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
const box = new BoundingBox(this._bounds);
|
|
763
|
+
if (this._simulation) {
|
|
764
|
+
const pad = this._maxStrandLength;
|
|
765
|
+
box.minPoint.setXYZ(box.minPoint.x - pad, box.minPoint.y - pad, box.minPoint.z - pad);
|
|
766
|
+
box.maxPoint.setXYZ(box.maxPoint.x + pad, box.maxPoint.y + pad, box.maxPoint.z + pad);
|
|
767
|
+
}
|
|
768
|
+
return box;
|
|
769
|
+
}
|
|
770
|
+
/** {@inheritDoc SceneNode.isHair} */ isHair() {
|
|
771
|
+
return true;
|
|
772
|
+
}
|
|
773
|
+
/** {@inheritDoc Drawable.getPickTarget} */ getPickTarget() {
|
|
774
|
+
return this._pickTarget;
|
|
775
|
+
}
|
|
776
|
+
/** {@inheritDoc Drawable.getMaterial} */ getMaterial() {
|
|
777
|
+
return this._material.get();
|
|
778
|
+
}
|
|
779
|
+
/** {@inheritDoc Drawable.getPrimitive} */ getPrimitive() {
|
|
780
|
+
return this._primitive.get();
|
|
781
|
+
}
|
|
782
|
+
/** {@inheritDoc Drawable.getMorphData} */ getMorphData() {
|
|
783
|
+
return null;
|
|
784
|
+
}
|
|
785
|
+
/** {@inheritDoc Drawable.getMorphInfo} */ getMorphInfo() {
|
|
786
|
+
return null;
|
|
787
|
+
}
|
|
788
|
+
/** {@inheritDoc Drawable.getSkinInfluenceData} */ getSkinInfluenceData() {
|
|
789
|
+
return null;
|
|
790
|
+
}
|
|
791
|
+
/** {@inheritDoc Drawable.getQueueType} */ getQueueType() {
|
|
792
|
+
return this._material.get()?.getQueueType() ?? QUEUE_OPAQUE;
|
|
793
|
+
}
|
|
794
|
+
/** {@inheritDoc Drawable.isUnlit} */ isUnlit() {
|
|
795
|
+
return !this._material.get()?.supportLighting();
|
|
796
|
+
}
|
|
797
|
+
/** {@inheritDoc Drawable.needSceneColor} */ needSceneColor() {
|
|
798
|
+
return this._material.get()?.needSceneColor() ?? false;
|
|
799
|
+
}
|
|
800
|
+
/** {@inheritDoc Drawable.needSceneDepth} */ needSceneDepth() {
|
|
801
|
+
return this._material.get()?.needSceneDepth() ?? false;
|
|
802
|
+
}
|
|
803
|
+
/** {@inheritDoc Drawable.draw} */ draw(ctx, renderQueue) {
|
|
804
|
+
const material = this._material.get();
|
|
805
|
+
const primitive = this._primitive.get();
|
|
806
|
+
// An empty groom is a legitimate state - a node exists before its asset has
|
|
807
|
+
// finished loading - and the draw range is zero until strands arrive.
|
|
808
|
+
if (material && primitive && material.vertexCount > 0) {
|
|
809
|
+
this.bind(ctx, renderQueue);
|
|
810
|
+
material.draw(primitive, ctx);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
onDispose() {
|
|
814
|
+
super.onDispose();
|
|
815
|
+
this._disposeSimulation();
|
|
816
|
+
this._primitive.dispose();
|
|
817
|
+
this._strands.dispose();
|
|
818
|
+
this._material.dispose();
|
|
819
|
+
this._parsedAsset = null;
|
|
820
|
+
this._source = null;
|
|
821
|
+
this._bounds = null;
|
|
822
|
+
}
|
|
823
|
+
//
|
|
824
|
+
// Internals
|
|
825
|
+
//
|
|
826
|
+
/**
|
|
827
|
+
* The material, which is an implementation detail of this node.
|
|
828
|
+
* @internal
|
|
829
|
+
*/ get material() {
|
|
830
|
+
return this._material.get();
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Re-decimates the opened asset into the strands actually drawn.
|
|
834
|
+
* @internal
|
|
835
|
+
*/ _applyAssetDecimation() {
|
|
836
|
+
const parsed = this._parsedAsset;
|
|
837
|
+
if (!parsed) {
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
// A `.zhair` holds one set per source curve object, and an XGen export
|
|
841
|
+
// routinely carries several spline descriptions for a single hairstyle;
|
|
842
|
+
// merging lets the whole head draw in one call.
|
|
843
|
+
this.setStrands(mergeHairStrandSources(loadZHairStrandSources(parsed, {
|
|
844
|
+
strandStride: this._strandStride,
|
|
845
|
+
maxStrands: this._maxStrands
|
|
846
|
+
})));
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Starts strand dynamics over the current strands.
|
|
850
|
+
* @internal
|
|
851
|
+
*/ _createSimulation() {
|
|
852
|
+
this._disposeSimulation();
|
|
853
|
+
const data = this._strands.get();
|
|
854
|
+
if (!data || !this._source || !isHairSimulationSupported()) {
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
const simulation = new GPUHairSimulation(data, this._source, this._simulationOptions);
|
|
858
|
+
if (simulation.enabled) {
|
|
859
|
+
this._simulation = simulation;
|
|
860
|
+
// Motion vectors have to follow the control points once something is
|
|
861
|
+
// moving them, or a groom swinging under a still head reports no motion
|
|
862
|
+
// and a temporal filter smears it.
|
|
863
|
+
this.material.prevPoints = simulation.framePointBuffer;
|
|
864
|
+
// The bounds widen while a simulation runs - see computeBoundingVolume.
|
|
865
|
+
this.invalidateBoundingVolume();
|
|
866
|
+
this.scene?.queueUpdateNode(this);
|
|
867
|
+
} else {
|
|
868
|
+
console.warn(simulation.disabledReason ?? 'GPU hair simulation could not start');
|
|
869
|
+
simulation.dispose();
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
/** @internal */ _disposeSimulation() {
|
|
873
|
+
this.material.prevPoints = null;
|
|
874
|
+
this._simulation?.dispose();
|
|
875
|
+
this._simulation = null;
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Builds the primitive the draw call is issued against.
|
|
879
|
+
*
|
|
880
|
+
* @remarks
|
|
881
|
+
* No vertex attribute is ever read - the vertex shader derives everything from
|
|
882
|
+
* the vertex index and the storage buffers - but a draw still needs a vertex
|
|
883
|
+
* layout to exist, and one float is the smallest buffer that establishes one.
|
|
884
|
+
* @internal
|
|
885
|
+
*/ _ensurePrimitive() {
|
|
886
|
+
if (this._primitive.get()) {
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
const primitive = new Primitive();
|
|
890
|
+
primitive.createAndSetVertexBuffer('position_f32x3', new Float32Array(3));
|
|
891
|
+
primitive.primitiveType = 'triangle-list';
|
|
892
|
+
this._primitive.set(primitive);
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Points the draw range at however many vertices the material now needs.
|
|
896
|
+
*
|
|
897
|
+
* @remarks
|
|
898
|
+
* Drawing is non-indexed, six vertices per ribbon segment, so the count moves
|
|
899
|
+
* with both the strand count and {@link HairNode.segmentsPerStrand}.
|
|
900
|
+
* @internal
|
|
901
|
+
*/ _syncDrawRange() {
|
|
902
|
+
const primitive = this._primitive.get();
|
|
903
|
+
if (primitive) {
|
|
904
|
+
primitive.indexCount = this.material.vertexCount;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Local-space bounds of a strand source, padded by the widest strand, along
|
|
910
|
+
* with the length of the longest strand.
|
|
911
|
+
*
|
|
912
|
+
* @remarks
|
|
913
|
+
* The ribbon is built around the control point, so half a width would suffice;
|
|
914
|
+
* a full width is used because {@link HairNode.minPixelWidth} widens sub-pixel
|
|
915
|
+
* strands beyond their authored size and the box has to survive that.
|
|
916
|
+
*
|
|
917
|
+
* The strand length is measured here because the same pass already touches
|
|
918
|
+
* every point; it pads the bounds while a simulation runs.
|
|
919
|
+
* @internal
|
|
920
|
+
*/ function computeStrandExtents(source) {
|
|
921
|
+
const scale = source.scale ?? 1;
|
|
922
|
+
const positions = source.positions;
|
|
923
|
+
const box = new BoundingBox();
|
|
924
|
+
box.beginExtend();
|
|
925
|
+
let maxStrandLength = 0;
|
|
926
|
+
let first = 0;
|
|
927
|
+
for(let s = 0; s < source.pointCounts.length; s++){
|
|
928
|
+
const count = source.pointCounts[s];
|
|
929
|
+
let length = 0;
|
|
930
|
+
for(let i = 0; i < count; i++){
|
|
931
|
+
const p = (first + i) * 3;
|
|
932
|
+
const x = positions[p] * scale;
|
|
933
|
+
const y = positions[p + 1] * scale;
|
|
934
|
+
const z = positions[p + 2] * scale;
|
|
935
|
+
box.extend3(x, y, z);
|
|
936
|
+
if (i > 0) {
|
|
937
|
+
length += Math.hypot(x - positions[p - 3] * scale, y - positions[p - 2] * scale, z - positions[p - 1] * scale);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
if (length > maxStrandLength) {
|
|
941
|
+
maxStrandLength = length;
|
|
942
|
+
}
|
|
943
|
+
first += count;
|
|
944
|
+
}
|
|
945
|
+
let widest = source.defaultWidth ?? 0.0001;
|
|
946
|
+
if (source.widths) {
|
|
947
|
+
for(let i = 0; i < source.widths.length; i++){
|
|
948
|
+
if (source.widths[i] > widest) {
|
|
949
|
+
widest = source.widths[i];
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
const pad = widest * scale * (source.widthScale ?? 1);
|
|
954
|
+
box.minPoint.setXYZ(box.minPoint.x - pad, box.minPoint.y - pad, box.minPoint.z - pad);
|
|
955
|
+
box.maxPoint.setXYZ(box.maxPoint.x + pad, box.maxPoint.y + pad, box.maxPoint.z + pad);
|
|
956
|
+
return {
|
|
957
|
+
bounds: box,
|
|
958
|
+
maxStrandLength
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
export { HairNode };
|
|
963
|
+
//# sourceMappingURL=hair.js.map
|