@call-me-sensei/toonlab 0.1.1
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/ATTRIBUTION.md +40 -0
- package/LICENSE +21 -0
- package/README.md +125 -0
- package/package.json +97 -0
- package/src/character/characterRig.js +353 -0
- package/src/character/freestyleSwimClip.js +541 -0
- package/src/character/index.js +4 -0
- package/src/character/modelLoader.js +433 -0
- package/src/core/materialRoles.js +438 -0
- package/src/core/presetDocuments.js +110 -0
- package/src/core/shaderBackend.js +24 -0
- package/src/debrisgen/debrisFields.js +544 -0
- package/src/debrisgen/debrisGenerator.js +1929 -0
- package/src/debrisgen/debrisPalettes.js +71 -0
- package/src/debrisgen/debrisPhysics.js +198 -0
- package/src/debrisgen/debrisPresets.js +155 -0
- package/src/debrisgen/debrisSettings.js +333 -0
- package/src/debrisgen/debrisTextures.js +380 -0
- package/src/debrisgen/index.js +5 -0
- package/src/debug/fieldValues.js +95 -0
- package/src/debug/index.js +7 -0
- package/src/debug/settingsPanel.js +170 -0
- package/src/environment/environmentAmbientProbe.js +160 -0
- package/src/environment/environmentMaterialAdapter.js +336 -0
- package/src/environment/environmentMaterialClassifier.js +178 -0
- package/src/environment/environmentPlanarReflection.js +201 -0
- package/src/environment/environmentPresets.js +443 -0
- package/src/environment/environmentRigs.js +684 -0
- package/src/environment/environmentSettings.js +504 -0
- package/src/environment/environmentShaderMaterials.js +318 -0
- package/src/environment/environmentSunShadowPass.js +268 -0
- package/src/environment/environmentTextureResolver.js +161 -0
- package/src/environment/environmentTimeOfDay.js +167 -0
- package/src/environment/environmentVertexAo.js +188 -0
- package/src/environment/index.js +8 -0
- package/src/environment/scanAssetStylize.js +103 -0
- package/src/index.js +21 -0
- package/src/loaders/index.js +1 -0
- package/src/post/index.js +2 -0
- package/src/post/postProcessing.js +1087 -0
- package/src/rockgen/export/glbExport.js +177 -0
- package/src/rockgen/heightfield/heightfieldErosion.js +7 -0
- package/src/rockgen/heightfield/heightfieldPatch.js +183 -0
- package/src/rockgen/heightfield/stylizedErosionSim.js +356 -0
- package/src/rockgen/index.js +14 -0
- package/src/rockgen/mesh/meshAttributes.js +374 -0
- package/src/rockgen/mesh/meshDocument.js +158 -0
- package/src/rockgen/mesh/surfaceNets.js +329 -0
- package/src/rockgen/noise/cellularNoise3.js +92 -0
- package/src/rockgen/noise/prng.js +62 -0
- package/src/rockgen/noise/simplexNoise3.js +89 -0
- package/src/rockgen/noise/valueNoise3.js +87 -0
- package/src/rockgen/rockDocument.js +255 -0
- package/src/rockgen/rockHelpers.js +8 -0
- package/src/rockgen/rockgenPresets.js +559 -0
- package/src/rockgen/rockgenSettings.js +756 -0
- package/src/rockgen/sdf/fieldCompiler.js +544 -0
- package/src/rockgen/sdf/sculptEdits.js +66 -0
- package/src/rockgen/sdf/sdfModifiers.js +45 -0
- package/src/rockgen/sdf/sdfOps.js +37 -0
- package/src/rockgen/sdf/sdfPrimitives.js +88 -0
- package/src/shaders-tsl/anime.js +963 -0
- package/src/shaders-tsl/chunks/character-color.js +64 -0
- package/src/shaders-tsl/chunks/character-highlights.js +174 -0
- package/src/shaders-tsl/chunks/character-lighting.js +309 -0
- package/src/shaders-tsl/chunks/character-material-maps.js +146 -0
- package/src/shaders-tsl/chunks/character-roles.js +52 -0
- package/src/shaders-tsl/chunks/character-scene-lights.js +270 -0
- package/src/shaders-tsl/chunks/character-shadow-color.js +61 -0
- package/src/shaders-tsl/chunks/character-skinning.js +140 -0
- package/src/shaders-tsl/chunks/environment-color.js +47 -0
- package/src/shaders-tsl/chunks/environment-debug.js +79 -0
- package/src/shaders-tsl/chunks/environment-lighting.js +260 -0
- package/src/shaders-tsl/chunks/environment-sun-shadow.js +87 -0
- package/src/shaders-tsl/chunks/foliage-fog.js +60 -0
- package/src/shaders-tsl/chunks/pass-depth-color.js +89 -0
- package/src/shaders-tsl/chunks/stylized-cloud-shadow.js +70 -0
- package/src/shaders-tsl/chunks/water-color.js +190 -0
- package/src/shaders-tsl/chunks/water-common.js +124 -0
- package/src/shaders-tsl/chunks/water-foam.js +93 -0
- package/src/shaders-tsl/chunks/water-lighting.js +116 -0
- package/src/shaders-tsl/chunks/water-ripple.js +64 -0
- package/src/shaders-tsl/chunks/water-waves.js +100 -0
- package/src/shaders-tsl/environment-ao-overlay.js +60 -0
- package/src/shaders-tsl/environment.js +680 -0
- package/src/shaders-tsl/flower.js +267 -0
- package/src/shaders-tsl/grass.js +193 -0
- package/src/shaders-tsl/post-composite.js +461 -0
- package/src/shaders-tsl/sky.js +175 -0
- package/src/shaders-tsl/tree-leaf.js +264 -0
- package/src/shaders-tsl/water-breaker.js +423 -0
- package/src/shaders-tsl/water-kelp.js +171 -0
- package/src/shaders-tsl/water-rain.js +107 -0
- package/src/shaders-tsl/water-simulation.js +140 -0
- package/src/shaders-tsl/water-splash.js +298 -0
- package/src/shaders-tsl/water.js +509 -0
- package/src/sky/index.js +2 -0
- package/src/sky/stylizedSky.js +341 -0
- package/src/toon/characterRenderPasses.js +646 -0
- package/src/toon/index.js +4 -0
- package/src/toon/settings/alphaSettings.js +170 -0
- package/src/toon/settings/averageShadowSettings.js +181 -0
- package/src/toon/settings/baseTextureSettings.js +133 -0
- package/src/toon/settings/celShadeSettings.js +96 -0
- package/src/toon/settings/contactShadowSettings.js +114 -0
- package/src/toon/settings/eyeHighlightSettings.js +187 -0
- package/src/toon/settings/faceLightingSettings.js +135 -0
- package/src/toon/settings/furSettings.js +112 -0
- package/src/toon/settings/glitterSettings.js +102 -0
- package/src/toon/settings/hairHighlightSettings.js +325 -0
- package/src/toon/settings/indirectLightSettings.js +244 -0
- package/src/toon/settings/localLightSettings.js +176 -0
- package/src/toon/settings/materialMapSettings.js +299 -0
- package/src/toon/settings/outlineSettings.js +263 -0
- package/src/toon/settings/perspectiveRemovalSettings.js +60 -0
- package/src/toon/settings/rimLightSettings.js +268 -0
- package/src/toon/settings/sceneShadowSettings.js +147 -0
- package/src/toon/settings/selfShadowSettings.js +221 -0
- package/src/toon/settings/shadowColorSettings.js +238 -0
- package/src/toon/settings/skinToneSettings.js +150 -0
- package/src/toon/settings/specularSettings.js +314 -0
- package/src/toon/settings/stickerSettings.js +102 -0
- package/src/toon/toonMaterialAdapter.js +2235 -0
- package/src/toon/toonSettings.js +860 -0
- package/src/vegetation/flowerSpecies.js +260 -0
- package/src/vegetation/index.js +10 -0
- package/src/vegetation/stylizedBush.js +136 -0
- package/src/vegetation/stylizedFlower.js +213 -0
- package/src/vegetation/stylizedFlowers.js +297 -0
- package/src/vegetation/stylizedGrass.js +491 -0
- package/src/vegetation/stylizedTree.js +2449 -0
- package/src/vegetation/stylizedTreeFoliage.js +691 -0
- package/src/vegetation/treeExport.js +300 -0
- package/src/vegetation/treeRecipe.js +647 -0
- package/src/water/index.js +2 -0
- package/src/water/water.js +20 -0
- package/src/water/waterBreakerSystem.js +591 -0
- package/src/water/waterInteraction.js +193 -0
- package/src/water/waterMaterial.js +263 -0
- package/src/water/waterRain.js +136 -0
- package/src/water/waterRippleSimulation.js +227 -0
- package/src/water/waterScenePasses.js +415 -0
- package/src/water/waterSettings.js +1138 -0
- package/src/water/waterSplashSystem.js +251 -0
- package/src/water/waterSurface.js +475 -0
- package/src/water/waterVegetation.js +83 -0
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { float, Fn, If, int, uniform, uniformArray, uv, vec3, vec4 } from 'three/tsl';
|
|
3
|
+
import { NodeMaterial } from 'three/webgpu';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
applyShadowClipAdjust,
|
|
7
|
+
createPassDepthColorMaterial,
|
|
8
|
+
PassBasicNodeMaterial,
|
|
9
|
+
} from '../shaders-tsl/chunks/pass-depth-color.js';
|
|
10
|
+
import {
|
|
11
|
+
environmentSunShadow,
|
|
12
|
+
sampleEnvironmentSunShadow,
|
|
13
|
+
} from '../shaders-tsl/chunks/environment-sun-shadow.js';
|
|
14
|
+
|
|
15
|
+
// Runtime render passes for the character toon shader.
|
|
16
|
+
//
|
|
17
|
+
// applyToonShader() alone produces a complete material; this module adds the
|
|
18
|
+
// per-frame passes that unlock the screen-space and shadow-map features:
|
|
19
|
+
//
|
|
20
|
+
// 1. Scene depth prepass -> depth-texture rim light + contact shadow
|
|
21
|
+
// 2. Character-only orthographic shadow map -> real self shadow
|
|
22
|
+
// 3. Head bone tracking -> head-space face shading
|
|
23
|
+
// 4. Average shadow measurement -> per-character uniform scene shadow
|
|
24
|
+
// 5. Character mask -> character-aware bloom in the post pipeline
|
|
25
|
+
//
|
|
26
|
+
// Every pass is auto-gated: it only renders when at least one registered
|
|
27
|
+
// material actually consumes its output, so leaving this running with the
|
|
28
|
+
// features disabled costs almost nothing.
|
|
29
|
+
//
|
|
30
|
+
// Usage:
|
|
31
|
+
// const passes = createCharacterRenderPasses({ renderer, scene, camera });
|
|
32
|
+
// passes.registerCharacterRoot(modelRoot); // after applyToonShader
|
|
33
|
+
// ...in the render loop, before rendering:
|
|
34
|
+
// passes.update();
|
|
35
|
+
|
|
36
|
+
// Layer used to draw character meshes in isolation (self shadow, mask).
|
|
37
|
+
export const TOON_CHARACTER_LAYER = 15;
|
|
38
|
+
|
|
39
|
+
const MAX_MEASURE_SLOTS = 16;
|
|
40
|
+
|
|
41
|
+
const DEFAULT_SELF_SHADOW_OPTIONS = Object.freeze({
|
|
42
|
+
// 'mainLight' follows the scene's directional light; 'cameraRelative' keeps
|
|
43
|
+
// an art-directed angle relative to the camera so face/neck shadows stay
|
|
44
|
+
// attractive from any viewpoint.
|
|
45
|
+
directionMode: 'mainLight',
|
|
46
|
+
cameraRelativePitch: 30,
|
|
47
|
+
cameraRelativeYaw: 0,
|
|
48
|
+
enabled: true,
|
|
49
|
+
mapSize: 2048,
|
|
50
|
+
// World-space receiver biases, slope-scaled in the shader.
|
|
51
|
+
depthBias: 0.01,
|
|
52
|
+
normalBias: 0.015,
|
|
53
|
+
ndotLFix: true,
|
|
54
|
+
// 0 = 1 tap, 1 = 4-tap PCF, 2 = 9-tap PCF.
|
|
55
|
+
quality: 2,
|
|
56
|
+
// Re-sharpen half-width applied after PCF so soft shadows keep a cel edge.
|
|
57
|
+
sharpen: 0.25,
|
|
58
|
+
// View distance at which the self shadow fades out entirely.
|
|
59
|
+
fadeDistance: 20,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
function toMaterialArray(material) {
|
|
63
|
+
return Array.isArray(material) ? material : [material].filter(Boolean);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isAnimeToonMaterial(mat) {
|
|
67
|
+
return Boolean(mat?.uniforms?.materialRole);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function collectToonMaterials(root) {
|
|
71
|
+
const materials = [];
|
|
72
|
+
root.traverse((obj) => {
|
|
73
|
+
if (!obj.isMesh || !obj.material) return;
|
|
74
|
+
for (const mat of toMaterialArray(obj.material)) {
|
|
75
|
+
if (isAnimeToonMaterial(mat)) materials.push(mat);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
return materials;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function setUniform(materials, name, value) {
|
|
82
|
+
for (const mat of materials) {
|
|
83
|
+
const uniform = mat.uniforms?.[name];
|
|
84
|
+
if (!uniform) continue;
|
|
85
|
+
if (value?.isVector3 && uniform.value?.isVector3) uniform.value.copy(value);
|
|
86
|
+
else if (value?.isVector2 && uniform.value?.isVector2) uniform.value.copy(value);
|
|
87
|
+
else if (value?.isMatrix4 && uniform.value?.isMatrix4) uniform.value.copy(value);
|
|
88
|
+
else uniform.value = value;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function anyUniform(materials, predicate) {
|
|
93
|
+
return materials.some((mat) => predicate(mat.uniforms));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Head bone resolution
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
function looseName(name) {
|
|
101
|
+
return String(name ?? '').replace(/[^a-z0-9-ヿ一-鿿]/gi, '').toLowerCase();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function findHeadBone(root) {
|
|
105
|
+
const candidates = [];
|
|
106
|
+
root.traverse((obj) => {
|
|
107
|
+
if (!obj.isBone && !(obj.isObject3D && /bone/i.test(obj.type))) return;
|
|
108
|
+
const name = looseName(obj.name);
|
|
109
|
+
if (!name) return;
|
|
110
|
+
// Exact conventions first: VRM/Mixamo 'head', Rigify 'DEF-head'/'defhead',
|
|
111
|
+
// MMD '頭'. Reject end/tip/top helper bones.
|
|
112
|
+
if (name === 'head' || name === 'defhead' || name === 'mixamorighead' || name === '頭') {
|
|
113
|
+
candidates.push({ obj, score: 0 });
|
|
114
|
+
} else if (/head/.test(name) && !/(end|tip|top|band|phone|wear|acc)/.test(name)) {
|
|
115
|
+
candidates.push({ obj, score: 1 });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
candidates.sort((a, b) => a.score - b.score);
|
|
119
|
+
return candidates[0]?.obj ?? null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function createHeadTracker(root, { forward = new THREE.Vector3(0, 0, 1), up = new THREE.Vector3(0, 1, 0) } = {}) {
|
|
123
|
+
const headBone = findHeadBone(root);
|
|
124
|
+
if (!headBone) return null;
|
|
125
|
+
|
|
126
|
+
// Calibrate against the pose at registration (rest pose right after load):
|
|
127
|
+
// capture which bone-local directions correspond to the model's world
|
|
128
|
+
// forward/up so animated bone rotations carry them correctly afterwards.
|
|
129
|
+
root.updateMatrixWorld(true);
|
|
130
|
+
const restQuat = headBone.getWorldQuaternion(new THREE.Quaternion()).invert();
|
|
131
|
+
const forwardLocal = forward.clone().applyQuaternion(restQuat).normalize();
|
|
132
|
+
const upLocal = up.clone().applyQuaternion(restQuat).normalize();
|
|
133
|
+
|
|
134
|
+
const worldQuat = new THREE.Quaternion();
|
|
135
|
+
const state = {
|
|
136
|
+
forward: new THREE.Vector3(0, 0, 1),
|
|
137
|
+
position: new THREE.Vector3(),
|
|
138
|
+
up: new THREE.Vector3(0, 1, 0),
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
headBone,
|
|
143
|
+
state,
|
|
144
|
+
update() {
|
|
145
|
+
headBone.getWorldQuaternion(worldQuat);
|
|
146
|
+
headBone.getWorldPosition(state.position);
|
|
147
|
+
state.forward.copy(forwardLocal).applyQuaternion(worldQuat).normalize();
|
|
148
|
+
state.up.copy(upLocal).applyQuaternion(worldQuat).normalize();
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Main factory
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
export function createCharacterRenderPasses({
|
|
158
|
+
renderer,
|
|
159
|
+
scene,
|
|
160
|
+
camera,
|
|
161
|
+
depthPrepass = true,
|
|
162
|
+
selfShadow = {},
|
|
163
|
+
} = {}) {
|
|
164
|
+
if (!renderer || !scene || !camera) {
|
|
165
|
+
throw new Error('createCharacterRenderPasses requires { renderer, scene, camera }.');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const selfShadowOptions = { ...DEFAULT_SELF_SHADOW_OPTIONS, ...(selfShadow === false ? { enabled: false } : selfShadow) };
|
|
169
|
+
|
|
170
|
+
const registered = [];
|
|
171
|
+
let characterMaskEnabled = false;
|
|
172
|
+
|
|
173
|
+
// Pass targets are consumed through float color attachments. Depth buffers
|
|
174
|
+
// still gate rasterization, but shader reads use the color targets.
|
|
175
|
+
|
|
176
|
+
// ---- Scene depth prepass ----
|
|
177
|
+
const drawingBufferSize = renderer.getDrawingBufferSize(new THREE.Vector2());
|
|
178
|
+
const depthTarget = new THREE.WebGLRenderTarget(drawingBufferSize.x, drawingBufferSize.y, {
|
|
179
|
+
depthBuffer: true,
|
|
180
|
+
magFilter: THREE.NearestFilter,
|
|
181
|
+
minFilter: THREE.NearestFilter,
|
|
182
|
+
stencilBuffer: false,
|
|
183
|
+
});
|
|
184
|
+
depthTarget.texture.name = 'ToonScenePrepass.Color';
|
|
185
|
+
depthTarget.depthTexture = new THREE.DepthTexture(drawingBufferSize.x, drawingBufferSize.y);
|
|
186
|
+
depthTarget.depthTexture.name = 'ToonScenePrepass.Depth';
|
|
187
|
+
depthTarget.depthTexture.type = THREE.UnsignedIntType;
|
|
188
|
+
depthTarget.texture.type = THREE.FloatType;
|
|
189
|
+
const depthOverrideMaterial = createPassDepthColorMaterial();
|
|
190
|
+
const depthResolution = new THREE.Vector2(drawingBufferSize.x, drawingBufferSize.y);
|
|
191
|
+
|
|
192
|
+
// ---- Character self-shadow map ----
|
|
193
|
+
const shadowTarget = new THREE.WebGLRenderTarget(selfShadowOptions.mapSize, selfShadowOptions.mapSize, {
|
|
194
|
+
depthBuffer: true,
|
|
195
|
+
magFilter: THREE.NearestFilter,
|
|
196
|
+
minFilter: THREE.NearestFilter,
|
|
197
|
+
stencilBuffer: false,
|
|
198
|
+
});
|
|
199
|
+
shadowTarget.texture.name = 'ToonCharSelfShadow.Color';
|
|
200
|
+
shadowTarget.depthTexture = new THREE.DepthTexture(selfShadowOptions.mapSize, selfShadowOptions.mapSize);
|
|
201
|
+
shadowTarget.depthTexture.name = 'ToonCharSelfShadow.Depth';
|
|
202
|
+
shadowTarget.depthTexture.type = THREE.UnsignedIntType;
|
|
203
|
+
shadowTarget.texture.type = THREE.FloatType;
|
|
204
|
+
const shadowCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.01, 10);
|
|
205
|
+
const shadowMatrix = new THREE.Matrix4();
|
|
206
|
+
const shadowLightDirection = new THREE.Vector3(0, 1, 0);
|
|
207
|
+
|
|
208
|
+
// ---- Average shadow measurement ----
|
|
209
|
+
const measureTarget = new THREE.WebGLRenderTarget(MAX_MEASURE_SLOTS, 1, {
|
|
210
|
+
depthBuffer: false,
|
|
211
|
+
stencilBuffer: false,
|
|
212
|
+
});
|
|
213
|
+
measureTarget.texture.name = 'ToonAverageShadowMeasure';
|
|
214
|
+
measureTarget.texture.minFilter = THREE.NearestFilter;
|
|
215
|
+
measureTarget.texture.magFilter = THREE.NearestFilter;
|
|
216
|
+
const measureScene = new THREE.Scene();
|
|
217
|
+
const measureCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
|
218
|
+
const measureMesh = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), null);
|
|
219
|
+
measureMesh.frustumCulled = false;
|
|
220
|
+
measureScene.add(measureMesh);
|
|
221
|
+
|
|
222
|
+
// Same 9-jitter average over each character bounding sphere, sampled from
|
|
223
|
+
// the shared sun-shadow pass. The output feeds vec2(slot, 0.5) lookups, so
|
|
224
|
+
// RT row orientation is irrelevant.
|
|
225
|
+
const measureJitterOffsets = [
|
|
226
|
+
[0.0, 0.0, 0.0], [0.7, 0.3, 0.0], [-0.7, 0.3, 0.0],
|
|
227
|
+
[0.0, 0.7, 0.5], [0.0, 0.7, -0.5], [0.5, -0.5, 0.5],
|
|
228
|
+
[-0.5, -0.5, -0.5], [0.4, -0.2, -0.6], [-0.4, 1.0, 0.2],
|
|
229
|
+
];
|
|
230
|
+
const measureNodeUniforms = {
|
|
231
|
+
sphereCount: uniform(0, 'int'),
|
|
232
|
+
sphereData: uniformArray(
|
|
233
|
+
Array.from({ length: MAX_MEASURE_SLOTS }, () => new THREE.Vector4()),
|
|
234
|
+
'vec4',
|
|
235
|
+
),
|
|
236
|
+
};
|
|
237
|
+
let measureNodeMaterial = null;
|
|
238
|
+
function ensureMeasureNodeMaterial() {
|
|
239
|
+
if (measureNodeMaterial) return measureNodeMaterial;
|
|
240
|
+
measureNodeMaterial = new NodeMaterial();
|
|
241
|
+
measureNodeMaterial.name = 'ToonAverageShadowMeasureNode';
|
|
242
|
+
measureNodeMaterial.depthTest = false;
|
|
243
|
+
measureNodeMaterial.depthWrite = false;
|
|
244
|
+
measureNodeMaterial.fragmentNode = Fn(() => {
|
|
245
|
+
const slot = int(uv().x.mul(MAX_MEASURE_SLOTS).floor()).toVar();
|
|
246
|
+
const result = float(1.0).toVar();
|
|
247
|
+
If(slot.lessThan(measureNodeUniforms.sphereCount), () => {
|
|
248
|
+
const sphere = measureNodeUniforms.sphereData.element(slot);
|
|
249
|
+
const visibility = float(0.0).toVar();
|
|
250
|
+
for (const [x, y, z] of measureJitterOffsets) {
|
|
251
|
+
visibility.addAssign(sampleEnvironmentSunShadow(
|
|
252
|
+
sphere.xyz.add(vec3(x, y, z).mul(sphere.w).mul(0.6)),
|
|
253
|
+
));
|
|
254
|
+
}
|
|
255
|
+
result.assign(visibility.div(measureJitterOffsets.length));
|
|
256
|
+
});
|
|
257
|
+
return vec4(vec3(result), 1.0);
|
|
258
|
+
})();
|
|
259
|
+
return measureNodeMaterial;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ---- Character mask (for character-aware bloom) ----
|
|
263
|
+
let maskTarget = null;
|
|
264
|
+
|
|
265
|
+
const boundsBox = new THREE.Box3();
|
|
266
|
+
const meshBox = new THREE.Box3();
|
|
267
|
+
const boundsSphere = new THREE.Sphere();
|
|
268
|
+
const tempVector = new THREE.Vector3();
|
|
269
|
+
const cameraQuat = new THREE.Quaternion();
|
|
270
|
+
const visibilityRestore = [];
|
|
271
|
+
|
|
272
|
+
let cachedDirectionalLight = null;
|
|
273
|
+
|
|
274
|
+
function findDirectionalLight() {
|
|
275
|
+
if (cachedDirectionalLight?.parent && cachedDirectionalLight.visible) return cachedDirectionalLight;
|
|
276
|
+
cachedDirectionalLight = null;
|
|
277
|
+
scene.traverse((obj) => {
|
|
278
|
+
if (cachedDirectionalLight) return;
|
|
279
|
+
if (obj.isDirectionalLight && obj.visible) cachedDirectionalLight = obj;
|
|
280
|
+
});
|
|
281
|
+
return cachedDirectionalLight;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function registerCharacterRoot(root, { headBoneForward, headBoneUp, headCenterOffset = 0 } = {}) {
|
|
285
|
+
if (!root) return null;
|
|
286
|
+
const materials = collectToonMaterials(root);
|
|
287
|
+
if (materials.length === 0) return null;
|
|
288
|
+
|
|
289
|
+
// Node materials have the pass subgraphs uniform-gated and read the
|
|
290
|
+
// passes' float color targets, so registration no longer mutates
|
|
291
|
+
// per-material defines.
|
|
292
|
+
|
|
293
|
+
const characterMeshes = [];
|
|
294
|
+
const shadowSwaps = [];
|
|
295
|
+
root.traverse((obj) => {
|
|
296
|
+
// Outline hulls and fur shells are derived geometry: they must not cast
|
|
297
|
+
// into the self-shadow map or appear in the character mask twice.
|
|
298
|
+
if (!obj.isMesh || obj.userData?.isToonOutline || obj.userData?.isToonFurShell) return;
|
|
299
|
+
obj.layers.enable(TOON_CHARACTER_LAYER);
|
|
300
|
+
characterMeshes.push(obj);
|
|
301
|
+
|
|
302
|
+
// Per-mesh depth materials that honor the source cutout so hair cards
|
|
303
|
+
// cast card-shaped shadows instead of solid quads.
|
|
304
|
+
const buildDepthMaterial = (mat) => {
|
|
305
|
+
const cutoff = mat?.uniforms?.aCutoff?.value ?? -1;
|
|
306
|
+
const baseMap = mat?.uniforms?.base?.value ?? null;
|
|
307
|
+
const params = {
|
|
308
|
+
alphaTest: cutoff > 0 ? cutoff : 0,
|
|
309
|
+
map: cutoff > 0 ? baseMap : null,
|
|
310
|
+
side: THREE.DoubleSide,
|
|
311
|
+
};
|
|
312
|
+
return createPassDepthColorMaterial(params);
|
|
313
|
+
};
|
|
314
|
+
const depthMaterial = Array.isArray(obj.material)
|
|
315
|
+
? obj.material.map(buildDepthMaterial)
|
|
316
|
+
: buildDepthMaterial(obj.material);
|
|
317
|
+
// Per-mesh PassBasicNodeMaterial swaps keep MMD-scale skeletons on the
|
|
318
|
+
// same storage-skinning path as the depth swaps.
|
|
319
|
+
const buildMaskMaterial = () => {
|
|
320
|
+
const material = new PassBasicNodeMaterial();
|
|
321
|
+
material.color.set(0xffffff);
|
|
322
|
+
material.side = THREE.DoubleSide;
|
|
323
|
+
return material;
|
|
324
|
+
};
|
|
325
|
+
const maskMaterial = Array.isArray(obj.material) ? obj.material.map(buildMaskMaterial) : buildMaskMaterial();
|
|
326
|
+
shadowSwaps.push({ depthMaterial, maskMaterial, mesh: obj });
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
const headTracker = createHeadTracker(root, {
|
|
330
|
+
forward: headBoneForward ? new THREE.Vector3(...headBoneForward) : undefined,
|
|
331
|
+
up: headBoneUp ? new THREE.Vector3(...headBoneUp) : undefined,
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
const entry = {
|
|
335
|
+
characterMeshes,
|
|
336
|
+
headCenterOffset,
|
|
337
|
+
headTracker,
|
|
338
|
+
materials,
|
|
339
|
+
root,
|
|
340
|
+
shadowSwaps,
|
|
341
|
+
slot: registered.length % MAX_MEASURE_SLOTS,
|
|
342
|
+
};
|
|
343
|
+
registered.push(entry);
|
|
344
|
+
return entry;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function unregisterCharacterRoot(root) {
|
|
348
|
+
const index = registered.findIndex((entry) => entry.root === root);
|
|
349
|
+
if (index >= 0) registered.splice(index, 1);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function computeCharacterBounds() {
|
|
353
|
+
boundsBox.makeEmpty();
|
|
354
|
+
for (const entry of registered) {
|
|
355
|
+
for (const mesh of entry.characterMeshes) {
|
|
356
|
+
if (!mesh.visible) continue;
|
|
357
|
+
// computeBoundingBox on skinned geometry ignores the pose; expand the
|
|
358
|
+
// static box generously instead of paying per-vertex skinning on CPU.
|
|
359
|
+
meshBox.setFromObject(mesh);
|
|
360
|
+
if (!meshBox.isEmpty()) boundsBox.union(meshBox);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (boundsBox.isEmpty()) return false;
|
|
364
|
+
boundsBox.getBoundingSphere(boundsSphere);
|
|
365
|
+
boundsSphere.radius *= 1.15;
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function renderDepthPrepass(anyMaterials) {
|
|
370
|
+
const needed = anyUniform(anyMaterials, (u) => (
|
|
371
|
+
(u.rimLightMode?.value === 1 && u.useRimLight?.value) || (u.contactShadowStrength?.value ?? 0) > 0
|
|
372
|
+
));
|
|
373
|
+
if (!needed) {
|
|
374
|
+
setUniform(anyMaterials, 'sceneDepthReady', false);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Outline hulls and fully transparent meshes must not write prepass depth:
|
|
379
|
+
// the hull would put a rim around itself, and a transparent water plane
|
|
380
|
+
// would occlude the swimmer's rim.
|
|
381
|
+
visibilityRestore.length = 0;
|
|
382
|
+
scene.traverse((obj) => {
|
|
383
|
+
if (!obj.isMesh || !obj.visible) return;
|
|
384
|
+
const materials = toMaterialArray(obj.material);
|
|
385
|
+
// Fur shells discard by noise in their own shader; the override depth
|
|
386
|
+
// material would write them as solid geometry, so keep them out too.
|
|
387
|
+
const skip = obj.userData?.isToonOutline ||
|
|
388
|
+
obj.userData?.isToonFurShell ||
|
|
389
|
+
(materials.length > 0 && materials.every((mat) => mat?.transparent === true));
|
|
390
|
+
if (skip) {
|
|
391
|
+
obj.visible = false;
|
|
392
|
+
visibilityRestore.push(obj);
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
const previousOverride = scene.overrideMaterial;
|
|
397
|
+
const previousTarget = renderer.getRenderTarget();
|
|
398
|
+
scene.overrideMaterial = depthOverrideMaterial;
|
|
399
|
+
renderer.setRenderTarget(depthTarget);
|
|
400
|
+
renderer.clear();
|
|
401
|
+
renderer.render(scene, camera);
|
|
402
|
+
renderer.setRenderTarget(previousTarget);
|
|
403
|
+
scene.overrideMaterial = previousOverride;
|
|
404
|
+
|
|
405
|
+
for (const obj of visibilityRestore) obj.visible = true;
|
|
406
|
+
visibilityRestore.length = 0;
|
|
407
|
+
|
|
408
|
+
setUniform(anyMaterials, 'sceneDepthReady', true);
|
|
409
|
+
setUniform(anyMaterials, 'sceneDepthTexture', depthTarget.texture);
|
|
410
|
+
setUniform(anyMaterials, 'sceneDepthResolution', depthResolution);
|
|
411
|
+
setUniform(anyMaterials, 'cameraNearPlane', camera.near);
|
|
412
|
+
setUniform(anyMaterials, 'cameraFarPlane', camera.far);
|
|
413
|
+
setUniform(anyMaterials, 'cameraProjection11', camera.projectionMatrix.elements[5]);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function renderSelfShadow(anyMaterials) {
|
|
417
|
+
const needed = selfShadowOptions.enabled &&
|
|
418
|
+
anyUniform(anyMaterials, (u) => u.selfShadowSourceMode?.value === 2 && (u.selfShadowStrength?.value ?? 0) >= 0);
|
|
419
|
+
if (!needed || !computeCharacterBounds()) {
|
|
420
|
+
setUniform(anyMaterials, 'charSelfShadowReady', false);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Shadow direction: scene main light, or an art-directed camera-relative
|
|
425
|
+
// angle (classic anime trick — face shadows stay pretty from any view).
|
|
426
|
+
if (selfShadowOptions.directionMode === 'cameraRelative') {
|
|
427
|
+
camera.getWorldQuaternion(cameraQuat);
|
|
428
|
+
shadowLightDirection.set(0, 0, 1)
|
|
429
|
+
.applyAxisAngle(new THREE.Vector3(1, 0, 0), THREE.MathUtils.degToRad(selfShadowOptions.cameraRelativePitch))
|
|
430
|
+
.applyAxisAngle(new THREE.Vector3(0, 1, 0), THREE.MathUtils.degToRad(selfShadowOptions.cameraRelativeYaw))
|
|
431
|
+
.applyQuaternion(cameraQuat)
|
|
432
|
+
.normalize();
|
|
433
|
+
} else {
|
|
434
|
+
const light = findDirectionalLight();
|
|
435
|
+
if (light) {
|
|
436
|
+
shadowLightDirection.copy(light.position).sub(light.target?.position ?? tempVector.set(0, 0, 0)).normalize();
|
|
437
|
+
} else {
|
|
438
|
+
shadowLightDirection.set(0.35, 0.75, 0.55).normalize();
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Tight-fit orthographic camera around only the registered characters:
|
|
443
|
+
// all shadow-map texels are spent on the character, which is what makes a
|
|
444
|
+
// 2k map produce crisp hair-on-face shadows.
|
|
445
|
+
const radius = boundsSphere.radius;
|
|
446
|
+
shadowCamera.left = -radius;
|
|
447
|
+
shadowCamera.right = radius;
|
|
448
|
+
shadowCamera.top = radius;
|
|
449
|
+
shadowCamera.bottom = -radius;
|
|
450
|
+
shadowCamera.near = 0.01;
|
|
451
|
+
shadowCamera.far = radius * 2 + 0.1;
|
|
452
|
+
shadowCamera.position.copy(boundsSphere.center).addScaledVector(shadowLightDirection, radius + 0.05);
|
|
453
|
+
shadowCamera.up.set(0, 1, 0);
|
|
454
|
+
if (Math.abs(shadowLightDirection.y) > 0.99) shadowCamera.up.set(0, 0, 1);
|
|
455
|
+
shadowCamera.lookAt(boundsSphere.center);
|
|
456
|
+
shadowCamera.coordinateSystem = renderer.coordinateSystem;
|
|
457
|
+
shadowCamera.updateProjectionMatrix();
|
|
458
|
+
shadowCamera.updateMatrixWorld(true);
|
|
459
|
+
shadowMatrix.multiplyMatrices(shadowCamera.projectionMatrix, shadowCamera.matrixWorldInverse);
|
|
460
|
+
applyShadowClipAdjust(shadowMatrix, renderer);
|
|
461
|
+
|
|
462
|
+
// Swap character materials to cutout-aware depth materials, render only
|
|
463
|
+
// the character layer, restore.
|
|
464
|
+
const restores = [];
|
|
465
|
+
for (const entry of registered) {
|
|
466
|
+
for (const { depthMaterial, mesh } of entry.shadowSwaps ?? []) {
|
|
467
|
+
restores.push({ material: mesh.material, mesh });
|
|
468
|
+
mesh.material = depthMaterial;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const previousTarget = renderer.getRenderTarget();
|
|
473
|
+
shadowCamera.layers.set(TOON_CHARACTER_LAYER);
|
|
474
|
+
renderer.setRenderTarget(shadowTarget);
|
|
475
|
+
renderer.clear();
|
|
476
|
+
renderer.render(scene, shadowCamera);
|
|
477
|
+
renderer.setRenderTarget(previousTarget);
|
|
478
|
+
|
|
479
|
+
for (const { material, mesh } of restores) mesh.material = material;
|
|
480
|
+
|
|
481
|
+
setUniform(anyMaterials, 'charSelfShadowReady', true);
|
|
482
|
+
setUniform(anyMaterials, 'charSelfShadowMap', shadowTarget.texture);
|
|
483
|
+
setUniform(anyMaterials, 'charSelfShadowMatrix', shadowMatrix);
|
|
484
|
+
setUniform(anyMaterials, 'charSelfShadowLightDirection', shadowLightDirection);
|
|
485
|
+
setUniform(anyMaterials, 'charSelfShadowTexelSize', 1 / selfShadowOptions.mapSize);
|
|
486
|
+
setUniform(anyMaterials, 'charSelfShadowQuality', selfShadowOptions.quality);
|
|
487
|
+
setUniform(anyMaterials, 'charSelfShadowSharpen', selfShadowOptions.sharpen);
|
|
488
|
+
setUniform(anyMaterials, 'charSelfShadowNormalBias', selfShadowOptions.normalBias);
|
|
489
|
+
setUniform(anyMaterials, 'charSelfShadowDepthBias', selfShadowOptions.depthBias);
|
|
490
|
+
setUniform(anyMaterials, 'charSelfShadowFadeDistance', selfShadowOptions.fadeDistance);
|
|
491
|
+
setUniform(anyMaterials, 'charSelfShadowNdotLFix', selfShadowOptions.ndotLFix);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function renderAverageShadowMeasure(anyMaterials) {
|
|
495
|
+
const shadowSourceReady = environmentSunShadow.ready.value === true;
|
|
496
|
+
const needed = shadowSourceReady &&
|
|
497
|
+
anyUniform(anyMaterials, (u) => (u.averageShadowMeasuredBlend?.value ?? 0) > 0);
|
|
498
|
+
if (!needed) {
|
|
499
|
+
setUniform(anyMaterials, 'averageShadowMeasureReady', false);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const sphereData = measureNodeUniforms.sphereData.array;
|
|
504
|
+
let count = 0;
|
|
505
|
+
for (const entry of registered) {
|
|
506
|
+
if (count >= MAX_MEASURE_SLOTS) break;
|
|
507
|
+
meshBox.makeEmpty();
|
|
508
|
+
for (const mesh of entry.characterMeshes) {
|
|
509
|
+
if (mesh.visible) meshBox.expandByObject(mesh);
|
|
510
|
+
}
|
|
511
|
+
if (meshBox.isEmpty()) continue;
|
|
512
|
+
meshBox.getBoundingSphere(boundsSphere);
|
|
513
|
+
entry.slot = count;
|
|
514
|
+
sphereData[count].set(boundsSphere.center.x, boundsSphere.center.y, boundsSphere.center.z, boundsSphere.radius);
|
|
515
|
+
count += 1;
|
|
516
|
+
}
|
|
517
|
+
if (count === 0) {
|
|
518
|
+
setUniform(anyMaterials, 'averageShadowMeasureReady', false);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
measureNodeUniforms.sphereCount.value = count;
|
|
523
|
+
measureMesh.material = ensureMeasureNodeMaterial();
|
|
524
|
+
|
|
525
|
+
const previousTarget = renderer.getRenderTarget();
|
|
526
|
+
renderer.setRenderTarget(measureTarget);
|
|
527
|
+
renderer.render(measureScene, measureCamera);
|
|
528
|
+
renderer.setRenderTarget(previousTarget);
|
|
529
|
+
|
|
530
|
+
for (const entry of registered) {
|
|
531
|
+
setUniform(entry.materials, 'averageShadowMeasureReady', true);
|
|
532
|
+
setUniform(entry.materials, 'averageShadowMeasureTexture', measureTarget.texture);
|
|
533
|
+
setUniform(entry.materials, 'averageShadowMeasureSlot', (entry.slot + 0.5) / MAX_MEASURE_SLOTS);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function renderCharacterMask() {
|
|
538
|
+
if (!characterMaskEnabled || registered.length === 0) return null;
|
|
539
|
+
if (!maskTarget) {
|
|
540
|
+
const size = renderer.getDrawingBufferSize(new THREE.Vector2());
|
|
541
|
+
maskTarget = new THREE.WebGLRenderTarget(Math.max(1, size.x >> 1), Math.max(1, size.y >> 1), {
|
|
542
|
+
depthBuffer: true,
|
|
543
|
+
stencilBuffer: false,
|
|
544
|
+
});
|
|
545
|
+
maskTarget.texture.name = 'ToonCharacterMask';
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const previousTarget = renderer.getRenderTarget();
|
|
549
|
+
const previousOverride = scene.overrideMaterial;
|
|
550
|
+
const previousClearColor = renderer.getClearColor(new THREE.Color());
|
|
551
|
+
const previousClearAlpha = renderer.getClearAlpha();
|
|
552
|
+
const previousBackground = scene.background;
|
|
553
|
+
const previousCameraLayers = camera.layers.mask;
|
|
554
|
+
|
|
555
|
+
scene.background = null;
|
|
556
|
+
// Swap per-mesh mask materials so MMD-scale skeletons keep the
|
|
557
|
+
// storage-skinning path (see registerCharacterRoot).
|
|
558
|
+
const maskSwapRestores = [];
|
|
559
|
+
for (const entry of registered) {
|
|
560
|
+
for (const swap of entry.shadowSwaps) {
|
|
561
|
+
if (!swap.maskMaterial) continue;
|
|
562
|
+
maskSwapRestores.push({ material: swap.mesh.material, mesh: swap.mesh });
|
|
563
|
+
swap.mesh.material = swap.maskMaterial;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
camera.layers.set(TOON_CHARACTER_LAYER);
|
|
567
|
+
renderer.setClearColor(0x000000, 1);
|
|
568
|
+
renderer.setRenderTarget(maskTarget);
|
|
569
|
+
renderer.clear();
|
|
570
|
+
renderer.render(scene, camera);
|
|
571
|
+
|
|
572
|
+
renderer.setRenderTarget(previousTarget);
|
|
573
|
+
renderer.setClearColor(previousClearColor, previousClearAlpha);
|
|
574
|
+
for (const restore of maskSwapRestores) restore.mesh.material = restore.material;
|
|
575
|
+
scene.overrideMaterial = previousOverride;
|
|
576
|
+
scene.background = previousBackground;
|
|
577
|
+
camera.layers.mask = previousCameraLayers;
|
|
578
|
+
return maskTarget.texture;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function updateHeadTracking() {
|
|
582
|
+
for (const entry of registered) {
|
|
583
|
+
if (!entry.headTracker) {
|
|
584
|
+
setUniform(entry.materials, 'headDataReady', false);
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
entry.headTracker.update();
|
|
588
|
+
const { forward, position, up } = entry.headTracker.state;
|
|
589
|
+
if (entry.headCenterOffset) {
|
|
590
|
+
tempVector.copy(position).addScaledVector(up, entry.headCenterOffset);
|
|
591
|
+
} else {
|
|
592
|
+
tempVector.copy(position);
|
|
593
|
+
}
|
|
594
|
+
setUniform(entry.materials, 'headDataReady', true);
|
|
595
|
+
setUniform(entry.materials, 'headPositionWS', tempVector);
|
|
596
|
+
setUniform(entry.materials, 'headForwardWS', forward);
|
|
597
|
+
setUniform(entry.materials, 'headUpWS', up);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function update() {
|
|
602
|
+
if (registered.length === 0) return;
|
|
603
|
+
const anyMaterials = registered.flatMap((entry) => entry.materials);
|
|
604
|
+
|
|
605
|
+
updateHeadTracking();
|
|
606
|
+
if (depthPrepass) renderDepthPrepass(anyMaterials);
|
|
607
|
+
renderSelfShadow(anyMaterials);
|
|
608
|
+
renderAverageShadowMeasure(anyMaterials);
|
|
609
|
+
renderCharacterMask();
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function setSize(width, height, pixelRatio = renderer.getPixelRatio()) {
|
|
613
|
+
const w = Math.max(1, Math.floor(width * pixelRatio));
|
|
614
|
+
const h = Math.max(1, Math.floor(height * pixelRatio));
|
|
615
|
+
depthTarget.setSize(w, h);
|
|
616
|
+
depthResolution.set(w, h);
|
|
617
|
+
if (maskTarget) maskTarget.setSize(Math.max(1, w >> 1), Math.max(1, h >> 1));
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function dispose() {
|
|
621
|
+
depthTarget.dispose();
|
|
622
|
+
shadowTarget.dispose();
|
|
623
|
+
measureTarget.dispose();
|
|
624
|
+
maskTarget?.dispose();
|
|
625
|
+
depthOverrideMaterial.dispose();
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return {
|
|
629
|
+
get characterMaskTexture() {
|
|
630
|
+
return maskTarget?.texture ?? null;
|
|
631
|
+
},
|
|
632
|
+
get registeredCount() {
|
|
633
|
+
return registered.length;
|
|
634
|
+
},
|
|
635
|
+
dispose,
|
|
636
|
+
registerCharacterRoot,
|
|
637
|
+
selfShadowCamera: shadowCamera,
|
|
638
|
+
selfShadowOptions,
|
|
639
|
+
setCharacterMaskEnabled(enabled) {
|
|
640
|
+
characterMaskEnabled = Boolean(enabled);
|
|
641
|
+
},
|
|
642
|
+
setSize,
|
|
643
|
+
unregisterCharacterRoot,
|
|
644
|
+
update,
|
|
645
|
+
};
|
|
646
|
+
}
|