@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,227 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
import { createWaterSimulationNodeMaterial } from '../shaders-tsl/water-simulation.js';
|
|
4
|
+
|
|
5
|
+
export const WATER_SIM_MAX_IMPULSES = 16;
|
|
6
|
+
|
|
7
|
+
const FIXED_TIMESTEP = 1 / 120;
|
|
8
|
+
const MAX_SUBSTEPS_PER_FRAME = 6;
|
|
9
|
+
|
|
10
|
+
// GPU ping-pong heightfield for interactive ripples (character wakes, splash
|
|
11
|
+
// rings, rain, projectiles). State texture: R = height, G = velocity,
|
|
12
|
+
// B = foam energy. The simulated window can follow a moving target via
|
|
13
|
+
// setCenter(); the reprojection shift is texel-exact so ripples stay put in
|
|
14
|
+
// world space while the window travels.
|
|
15
|
+
export class WaterRippleSimulation {
|
|
16
|
+
constructor({
|
|
17
|
+
resolution = 256,
|
|
18
|
+
worldWidth = 20,
|
|
19
|
+
worldDepth = 20,
|
|
20
|
+
centerX = 0,
|
|
21
|
+
centerZ = 0,
|
|
22
|
+
} = {}) {
|
|
23
|
+
this.resolution = Math.max(16, Math.floor(resolution));
|
|
24
|
+
this.worldWidth = Math.max(0.1, worldWidth);
|
|
25
|
+
this.worldDepth = Math.max(0.1, worldDepth);
|
|
26
|
+
this.centerX = centerX;
|
|
27
|
+
this.centerZ = centerZ;
|
|
28
|
+
|
|
29
|
+
this.parameters = {
|
|
30
|
+
rippleDamping: 0.985,
|
|
31
|
+
ripplePropagation: 11,
|
|
32
|
+
rippleFoamDecay: 0.94,
|
|
33
|
+
rippleFoamGain: 2.4,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
this.targets = [this.createTarget(), this.createTarget()];
|
|
37
|
+
this.readIndex = 0;
|
|
38
|
+
this.initialized = false;
|
|
39
|
+
this.timeAccumulator = 0;
|
|
40
|
+
this.pendingImpulses = [];
|
|
41
|
+
this.pendingShiftTexels = new THREE.Vector2(0, 0);
|
|
42
|
+
|
|
43
|
+
// Same fullscreen ping-pong contract and uniform-name surface as the
|
|
44
|
+
// retired ShaderMaterial path; the update loop writes `.value`s unchanged.
|
|
45
|
+
this.material = createWaterSimulationNodeMaterial({
|
|
46
|
+
maxImpulses: WATER_SIM_MAX_IMPULSES,
|
|
47
|
+
resolution: this.resolution,
|
|
48
|
+
});
|
|
49
|
+
this.material.uniforms.uDamping.value = this.parameters.rippleDamping;
|
|
50
|
+
this.material.uniforms.uFoamDecay.value = this.parameters.rippleFoamDecay;
|
|
51
|
+
this.material.uniforms.uFoamGain.value = this.parameters.rippleFoamGain;
|
|
52
|
+
this.material.uniforms.uRegionWorldSize.value.set(this.worldWidth, this.worldDepth);
|
|
53
|
+
|
|
54
|
+
this.fullscreenScene = new THREE.Scene();
|
|
55
|
+
this.fullscreenCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
|
56
|
+
const quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), this.material);
|
|
57
|
+
quad.frustumCulled = false;
|
|
58
|
+
this.fullscreenScene.add(quad);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
createTarget() {
|
|
62
|
+
const target = new THREE.WebGLRenderTarget(this.resolution, this.resolution, {
|
|
63
|
+
type: THREE.HalfFloatType,
|
|
64
|
+
format: THREE.RGBAFormat,
|
|
65
|
+
minFilter: THREE.LinearFilter,
|
|
66
|
+
magFilter: THREE.LinearFilter,
|
|
67
|
+
wrapS: THREE.ClampToEdgeWrapping,
|
|
68
|
+
wrapT: THREE.ClampToEdgeWrapping,
|
|
69
|
+
depthBuffer: false,
|
|
70
|
+
stencilBuffer: false,
|
|
71
|
+
generateMipmaps: false,
|
|
72
|
+
});
|
|
73
|
+
target.texture.name = 'waterRippleState';
|
|
74
|
+
return target;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get texture() {
|
|
78
|
+
return this.targets[this.readIndex].texture;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
get texelSize() {
|
|
82
|
+
return this.material.uniforms.uTexel.value;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// (centerX, centerZ, halfWidth, halfDepth) for the surface shader.
|
|
86
|
+
getRegion(out = new THREE.Vector4()) {
|
|
87
|
+
return out.set(this.centerX, this.centerZ, this.worldWidth * 0.5, this.worldDepth * 0.5);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
setParameters(parameters = {}) {
|
|
91
|
+
for (const key of Object.keys(this.parameters)) {
|
|
92
|
+
if (Number.isFinite(parameters[key])) this.parameters[key] = parameters[key];
|
|
93
|
+
}
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Moves the simulated window; existing ripples remain fixed in world space.
|
|
98
|
+
setCenter(x, z) {
|
|
99
|
+
const texelWorldX = this.worldWidth / this.resolution;
|
|
100
|
+
const texelWorldZ = this.worldDepth / this.resolution;
|
|
101
|
+
const quantizedX = Math.round(x / texelWorldX) * texelWorldX;
|
|
102
|
+
const quantizedZ = Math.round(z / texelWorldZ) * texelWorldZ;
|
|
103
|
+
this.pendingShiftTexels.x += (quantizedX - this.centerX) / texelWorldX;
|
|
104
|
+
this.pendingShiftTexels.y += (quantizedZ - this.centerZ) / texelWorldZ;
|
|
105
|
+
this.centerX = quantizedX;
|
|
106
|
+
this.centerZ = quantizedZ;
|
|
107
|
+
return this;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
containsPoint(worldX, worldZ, margin = 0) {
|
|
111
|
+
return Math.abs(worldX - this.centerX) <= this.worldWidth * 0.5 - margin &&
|
|
112
|
+
Math.abs(worldZ - this.centerZ) <= this.worldDepth * 0.5 - margin;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Queues a gaussian impulse at a world position. strength is a vertical
|
|
116
|
+
// velocity kick in m/s; negative values push the surface down.
|
|
117
|
+
addImpulse(worldX, worldZ, { radius = 0.35, strength = 0.6 } = {}) {
|
|
118
|
+
if (!this.containsPoint(worldX, worldZ)) return this;
|
|
119
|
+
if (this.pendingImpulses.length >= WATER_SIM_MAX_IMPULSES * 4) return this;
|
|
120
|
+
this.pendingImpulses.push({ worldX, worldZ, radius, strength });
|
|
121
|
+
return this;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Queues a ring of impulses, used by splashes for an outward-running wave.
|
|
125
|
+
addRingImpulse(worldX, worldZ, { radius = 0.5, strength = 0.6, points = 6 } = {}) {
|
|
126
|
+
for (let i = 0; i < points; i += 1) {
|
|
127
|
+
const angle = (i / points) * Math.PI * 2;
|
|
128
|
+
this.addImpulse(
|
|
129
|
+
worldX + Math.cos(angle) * radius,
|
|
130
|
+
worldZ + Math.sin(angle) * radius,
|
|
131
|
+
{ radius: radius * 0.8, strength: strength / points * 2.4 },
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
return this;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
writeImpulseUniforms() {
|
|
138
|
+
const impulses = this.material.uniforms.uImpulses.value;
|
|
139
|
+
const count = Math.min(this.pendingImpulses.length, WATER_SIM_MAX_IMPULSES);
|
|
140
|
+
for (let i = 0; i < count; i += 1) {
|
|
141
|
+
const impulse = this.pendingImpulses[i];
|
|
142
|
+
impulses[i].set(
|
|
143
|
+
(impulse.worldX - this.centerX) / this.worldWidth + 0.5,
|
|
144
|
+
(impulse.worldZ - this.centerZ) / this.worldDepth + 0.5,
|
|
145
|
+
Math.max(impulse.radius, 1e-3),
|
|
146
|
+
impulse.strength,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
this.material.uniforms.uImpulseCount.value = count;
|
|
150
|
+
this.pendingImpulses.splice(0, count);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
clearTargets(renderer) {
|
|
154
|
+
const previousTarget = renderer.getRenderTarget();
|
|
155
|
+
const previousClearColor = renderer.getClearColor(new THREE.Color());
|
|
156
|
+
const previousClearAlpha = renderer.getClearAlpha();
|
|
157
|
+
renderer.setClearColor(0x000000, 0);
|
|
158
|
+
for (const target of this.targets) {
|
|
159
|
+
renderer.setRenderTarget(target);
|
|
160
|
+
renderer.clear(true, false, false);
|
|
161
|
+
}
|
|
162
|
+
renderer.setRenderTarget(previousTarget);
|
|
163
|
+
renderer.setClearColor(previousClearColor, previousClearAlpha);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
step(renderer) {
|
|
167
|
+
const uniforms = this.material.uniforms;
|
|
168
|
+
uniforms.uPrevState.value = this.targets[this.readIndex].texture;
|
|
169
|
+
const writeTarget = this.targets[1 - this.readIndex];
|
|
170
|
+
renderer.setRenderTarget(writeTarget);
|
|
171
|
+
renderer.render(this.fullscreenScene, this.fullscreenCamera);
|
|
172
|
+
this.readIndex = 1 - this.readIndex;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
update(renderer, delta) {
|
|
176
|
+
if (!this.initialized) {
|
|
177
|
+
this.clearTargets(renderer);
|
|
178
|
+
this.initialized = true;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
this.timeAccumulator += Math.min(Math.max(delta, 0), 0.1);
|
|
182
|
+
let substeps = Math.floor(this.timeAccumulator / FIXED_TIMESTEP);
|
|
183
|
+
if (substeps <= 0 && this.pendingImpulses.length === 0) return;
|
|
184
|
+
substeps = Math.min(substeps, MAX_SUBSTEPS_PER_FRAME);
|
|
185
|
+
this.timeAccumulator = Math.max(
|
|
186
|
+
0, Math.min(this.timeAccumulator - substeps * FIXED_TIMESTEP, FIXED_TIMESTEP));
|
|
187
|
+
|
|
188
|
+
const uniforms = this.material.uniforms;
|
|
189
|
+
const texelWorld = this.worldWidth / this.resolution;
|
|
190
|
+
// Explicit heightfield stability (CFL): c * dt / dx must stay under ~0.7.
|
|
191
|
+
const maxPropagationSpeed = (texelWorld * 0.7) / FIXED_TIMESTEP;
|
|
192
|
+
const speed = Math.min(this.parameters.ripplePropagation, maxPropagationSpeed);
|
|
193
|
+
uniforms.uPropagation.value = (speed / texelWorld) * (speed / texelWorld);
|
|
194
|
+
uniforms.uDamping.value = this.parameters.rippleDamping;
|
|
195
|
+
uniforms.uFoamDecay.value = this.parameters.rippleFoamDecay;
|
|
196
|
+
uniforms.uFoamGain.value = this.parameters.rippleFoamGain;
|
|
197
|
+
uniforms.uDelta.value = FIXED_TIMESTEP;
|
|
198
|
+
uniforms.uRegionWorldSize.value.set(this.worldWidth, this.worldDepth);
|
|
199
|
+
|
|
200
|
+
const previousTarget = renderer.getRenderTarget();
|
|
201
|
+
const previousXrEnabled = renderer.xr.enabled;
|
|
202
|
+
renderer.xr.enabled = false;
|
|
203
|
+
try {
|
|
204
|
+
for (let i = 0; i < Math.max(substeps, this.pendingImpulses.length > 0 ? 1 : 0); i += 1) {
|
|
205
|
+
// Region shift and queued impulses apply on the first substep only.
|
|
206
|
+
if (i === 0) {
|
|
207
|
+
uniforms.uRegionShiftTexels.value.copy(this.pendingShiftTexels);
|
|
208
|
+
this.pendingShiftTexels.set(0, 0);
|
|
209
|
+
this.writeImpulseUniforms();
|
|
210
|
+
} else {
|
|
211
|
+
uniforms.uRegionShiftTexels.value.set(0, 0);
|
|
212
|
+
uniforms.uImpulseCount.value = 0;
|
|
213
|
+
}
|
|
214
|
+
this.step(renderer);
|
|
215
|
+
}
|
|
216
|
+
} finally {
|
|
217
|
+
renderer.setRenderTarget(previousTarget);
|
|
218
|
+
renderer.xr.enabled = previousXrEnabled;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
dispose() {
|
|
223
|
+
for (const target of this.targets) target.dispose();
|
|
224
|
+
this.material.dispose();
|
|
225
|
+
this.fullscreenScene.traverse((object) => object.geometry?.dispose?.());
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
import { createPassDepthColorMaterial } from '../shaders-tsl/chunks/pass-depth-color.js';
|
|
4
|
+
|
|
5
|
+
// Render passes that feed the water material:
|
|
6
|
+
// grab pass — scene color + depth in one render (refraction, absorption,
|
|
7
|
+
// shoreline foam, caustics)
|
|
8
|
+
// reflection — planar mirror render with oblique near-plane clipping
|
|
9
|
+
//
|
|
10
|
+
// Exclusion flags on object.userData:
|
|
11
|
+
// waterExclude — hidden from both passes
|
|
12
|
+
// waterReflectionExclude — hidden from the reflection pass only
|
|
13
|
+
// skipWaterReflection — legacy alias of waterReflectionExclude
|
|
14
|
+
//
|
|
15
|
+
// Node backends (docs/tsl-conventions.md):
|
|
16
|
+
// - The grab target keeps its texture linear (working space): rendering into
|
|
17
|
+
// a user render target skips the renderer's output color transform, so the
|
|
18
|
+
// texels are exactly the linear values the water shader's own output path
|
|
19
|
+
// feeds the final screen pass — refraction stays an exact pass-through.
|
|
20
|
+
// - TSL shaders cannot portably sample depth ATTACHMENTS (WGSL/GLSL builders
|
|
21
|
+
// type them differently), so scene depth comes from a second render into a
|
|
22
|
+
// float COLOR target with swapped depth materials (pass-depth-color.js),
|
|
23
|
+
// following environmentSunShadowPass's swap-cache lessons (no per-frame
|
|
24
|
+
// allocations, cached swap arrays, visibility kept in step).
|
|
25
|
+
// - Node-backend RTs are written top-down: the projective reflection matrix
|
|
26
|
+
// gets a CPU-side uv y-flip, and the oblique near-plane clip uses the
|
|
27
|
+
// coordinate-system-aware construction from three's own ReflectorNode.
|
|
28
|
+
|
|
29
|
+
const BIAS_MATRIX = new THREE.Matrix4().set(
|
|
30
|
+
0.5, 0, 0, 0.5,
|
|
31
|
+
0, 0.5, 0, 0.5,
|
|
32
|
+
0, 0, 0.5, 0.5,
|
|
33
|
+
0, 0, 0, 1,
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// uv' = (u, 1 - v): the node backends write render targets top-down, so the
|
|
37
|
+
// GL-convention projective uv needs a y-flip (composed after BIAS_MATRIX).
|
|
38
|
+
const FLIP_Y_UV_MATRIX = new THREE.Matrix4().set(
|
|
39
|
+
1, 0, 0, 0,
|
|
40
|
+
0, -1, 0, 1,
|
|
41
|
+
0, 0, 1, 0,
|
|
42
|
+
0, 0, 0, 1,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
function hideFlagged(root, predicate) {
|
|
46
|
+
const hidden = [];
|
|
47
|
+
root.traverse((object) => {
|
|
48
|
+
if (!object.visible || !predicate(object)) return;
|
|
49
|
+
object.visible = false;
|
|
50
|
+
hidden.push(object);
|
|
51
|
+
});
|
|
52
|
+
return () => {
|
|
53
|
+
for (const object of hidden) object.visible = true;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class WaterScenePasses {
|
|
58
|
+
constructor({
|
|
59
|
+
sceneColor = true,
|
|
60
|
+
reflection = true,
|
|
61
|
+
sceneColorScale = 1,
|
|
62
|
+
reflectionScale = 0.5,
|
|
63
|
+
clipBias = 0.02,
|
|
64
|
+
} = {}) {
|
|
65
|
+
this.sceneColorEnabled = sceneColor;
|
|
66
|
+
this.reflectionEnabled = reflection;
|
|
67
|
+
this.sceneColorScale = sceneColorScale;
|
|
68
|
+
this.reflectionScale = reflectionScale;
|
|
69
|
+
this.clipBias = clipBias;
|
|
70
|
+
|
|
71
|
+
this.grabTarget = null;
|
|
72
|
+
this.reflectionTarget = null;
|
|
73
|
+
this.reflectionCamera = new THREE.PerspectiveCamera();
|
|
74
|
+
this.reflectionMatrix = new THREE.Matrix4();
|
|
75
|
+
this.reflectionValid = false;
|
|
76
|
+
this.grabValid = false;
|
|
77
|
+
|
|
78
|
+
// Scene-depth pass state for the TSL renderer path.
|
|
79
|
+
this.depthTarget = null;
|
|
80
|
+
this.depthMaterialCache = new WeakMap();
|
|
81
|
+
// Swapped material (or material ARRAY) per mesh, keyed by the source
|
|
82
|
+
// material identity: handing the renderer a fresh array every frame
|
|
83
|
+
// churns its render-object cache (see environmentSunShadowPass).
|
|
84
|
+
this.depthSwapCache = new WeakMap();
|
|
85
|
+
this.depthMaterialRestores = [];
|
|
86
|
+
this.depthVisibilityRestores = [];
|
|
87
|
+
|
|
88
|
+
this.scratch = {
|
|
89
|
+
drawingBufferSize: new THREE.Vector2(),
|
|
90
|
+
cameraPosition: new THREE.Vector3(),
|
|
91
|
+
cameraDirection: new THREE.Vector3(),
|
|
92
|
+
target: new THREE.Vector3(),
|
|
93
|
+
plane: new THREE.Plane(),
|
|
94
|
+
clipPlane: new THREE.Vector4(),
|
|
95
|
+
q: new THREE.Vector4(),
|
|
96
|
+
waterWorldPosition: new THREE.Vector3(),
|
|
97
|
+
clearColor: new THREE.Color(),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
ensureGrabTarget(renderer) {
|
|
102
|
+
renderer.getDrawingBufferSize(this.scratch.drawingBufferSize);
|
|
103
|
+
const width = Math.max(1, Math.floor(this.scratch.drawingBufferSize.x * this.sceneColorScale));
|
|
104
|
+
const height = Math.max(1, Math.floor(this.scratch.drawingBufferSize.y * this.sceneColorScale));
|
|
105
|
+
if (!this.grabTarget) {
|
|
106
|
+
this.grabTarget = new THREE.WebGLRenderTarget(width, height, {
|
|
107
|
+
depthBuffer: true,
|
|
108
|
+
stencilBuffer: false,
|
|
109
|
+
generateMipmaps: false,
|
|
110
|
+
minFilter: THREE.LinearFilter,
|
|
111
|
+
magFilter: THREE.LinearFilter,
|
|
112
|
+
});
|
|
113
|
+
// Working-space (linear) texels — see the node-backend notes above.
|
|
114
|
+
} else if (this.grabTarget.width !== width || this.grabTarget.height !== height) {
|
|
115
|
+
this.grabTarget.setSize(width, height);
|
|
116
|
+
}
|
|
117
|
+
if (!this.depthTarget) {
|
|
118
|
+
this.depthTarget = new THREE.WebGLRenderTarget(width, height, {
|
|
119
|
+
depthBuffer: true,
|
|
120
|
+
stencilBuffer: false,
|
|
121
|
+
generateMipmaps: false,
|
|
122
|
+
minFilter: THREE.NearestFilter,
|
|
123
|
+
magFilter: THREE.NearestFilter,
|
|
124
|
+
});
|
|
125
|
+
this.depthTarget.texture.name = 'WaterScenePasses.Depth';
|
|
126
|
+
this.depthTarget.texture.type = THREE.FloatType;
|
|
127
|
+
} else if (this.depthTarget.width !== width || this.depthTarget.height !== height) {
|
|
128
|
+
this.depthTarget.setSize(width, height);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
ensureReflectionTarget(renderer) {
|
|
133
|
+
renderer.getDrawingBufferSize(this.scratch.drawingBufferSize);
|
|
134
|
+
const width = Math.max(1, Math.floor(this.scratch.drawingBufferSize.x * this.reflectionScale));
|
|
135
|
+
const height = Math.max(1, Math.floor(this.scratch.drawingBufferSize.y * this.reflectionScale));
|
|
136
|
+
if (!this.reflectionTarget) {
|
|
137
|
+
this.reflectionTarget = new THREE.WebGLRenderTarget(width, height, {
|
|
138
|
+
depthBuffer: true,
|
|
139
|
+
stencilBuffer: false,
|
|
140
|
+
generateMipmaps: false,
|
|
141
|
+
minFilter: THREE.LinearFilter,
|
|
142
|
+
magFilter: THREE.LinearFilter,
|
|
143
|
+
samples: 4,
|
|
144
|
+
});
|
|
145
|
+
// Keep the texture linear (see the grab-target notes).
|
|
146
|
+
} else if (this.reflectionTarget.width !== width || this.reflectionTarget.height !== height) {
|
|
147
|
+
this.reflectionTarget.setSize(width, height);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Depth replacement for one material, cached by source identity. Camera-view
|
|
152
|
+
// depth pass: keep the source's side (unlike the shadow pass's flip) and
|
|
153
|
+
// match the depth buffer's participation rules (visible + depthWrite).
|
|
154
|
+
depthMaterialFor(material) {
|
|
155
|
+
let depthMaterial = this.depthMaterialCache.get(material);
|
|
156
|
+
if (!depthMaterial) {
|
|
157
|
+
if (typeof material?.userData?.createDepthColorVariant === 'function') {
|
|
158
|
+
// Materials with custom vertex displacement (tree foliage cards,
|
|
159
|
+
// kelp blades) provide their own cutout depth variant.
|
|
160
|
+
depthMaterial = material.userData.createDepthColorVariant();
|
|
161
|
+
} else {
|
|
162
|
+
const cutoff = material?.uniforms?.aCutoff?.value ??
|
|
163
|
+
material?.uniforms?.alphaCutoff?.value ??
|
|
164
|
+
(material?.alphaTest > 0 ? material.alphaTest : -1);
|
|
165
|
+
const map = material?.uniforms?.base?.value ??
|
|
166
|
+
material?.uniforms?.baseMap?.value ??
|
|
167
|
+
material?.map ?? null;
|
|
168
|
+
depthMaterial = createPassDepthColorMaterial({
|
|
169
|
+
alphaTest: cutoff > 0 ? cutoff : 0,
|
|
170
|
+
map: cutoff > 0 ? map : null,
|
|
171
|
+
side: material?.side ?? THREE.FrontSide,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
// Depth-as-color must never be fogged: with scene.fog set, the node
|
|
175
|
+
// pipeline's per-material fog mix would blend distant depth values
|
|
176
|
+
// toward the fog color (the pass also nulls scene.fog — see below).
|
|
177
|
+
depthMaterial.fog = false;
|
|
178
|
+
this.depthMaterialCache.set(material, depthMaterial);
|
|
179
|
+
}
|
|
180
|
+
depthMaterial.visible = material?.visible !== false && material?.depthWrite !== false;
|
|
181
|
+
return depthMaterial;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Node backends only: render linear window depth into the float color
|
|
185
|
+
// target with swapped materials. Runs inside renderGrabPass's hide window
|
|
186
|
+
// (waterMesh + waterExclude objects are already invisible).
|
|
187
|
+
renderDepthPass(renderer, scene, camera) {
|
|
188
|
+
const materialRestores = this.depthMaterialRestores;
|
|
189
|
+
const visibilityRestores = this.depthVisibilityRestores;
|
|
190
|
+
materialRestores.length = 0;
|
|
191
|
+
visibilityRestores.length = 0;
|
|
192
|
+
|
|
193
|
+
scene.traverse((obj) => {
|
|
194
|
+
if (!obj.visible) return;
|
|
195
|
+
const renderable = obj.isMesh || obj.isPoints || obj.isLine || obj.isSprite;
|
|
196
|
+
if (!renderable) return;
|
|
197
|
+
// Points/lines/sprites would draw their color into the depth target;
|
|
198
|
+
// outline shells hug the base mesh depth.
|
|
199
|
+
if (!obj.isMesh || obj.userData?.isToonOutline || obj.userData?.isToonFurShell) {
|
|
200
|
+
obj.visible = false;
|
|
201
|
+
visibilityRestores.push(obj);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const sourceMaterials = Array.isArray(obj.material) ? obj.material : [obj.material];
|
|
205
|
+
if (!sourceMaterials.some((mat) => mat && mat.visible !== false && mat.depthWrite !== false)) {
|
|
206
|
+
obj.visible = false;
|
|
207
|
+
visibilityRestores.push(obj);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
// Instanced custom-attribute geometry without its own depth variant
|
|
211
|
+
// (e.g. grass blades) would collapse to its base quads under the
|
|
212
|
+
// generic swap — the ground behind still writes depth, so skip them.
|
|
213
|
+
const primary = sourceMaterials.find(Boolean);
|
|
214
|
+
if (
|
|
215
|
+
obj.geometry?.isInstancedBufferGeometry &&
|
|
216
|
+
!obj.isInstancedMesh &&
|
|
217
|
+
typeof primary?.userData?.createDepthColorVariant !== 'function'
|
|
218
|
+
) {
|
|
219
|
+
obj.visible = false;
|
|
220
|
+
visibilityRestores.push(obj);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
materialRestores.push({ material: obj.material, mesh: obj });
|
|
224
|
+
let swap = this.depthSwapCache.get(obj);
|
|
225
|
+
if (!swap || swap.source !== obj.material) {
|
|
226
|
+
swap = {
|
|
227
|
+
source: obj.material,
|
|
228
|
+
swapped: Array.isArray(obj.material)
|
|
229
|
+
? obj.material.map((mat) => this.depthMaterialFor(mat))
|
|
230
|
+
: this.depthMaterialFor(obj.material),
|
|
231
|
+
};
|
|
232
|
+
this.depthSwapCache.set(obj, swap);
|
|
233
|
+
} else if (Array.isArray(swap.swapped)) {
|
|
234
|
+
// Keep per-group visibility in step.
|
|
235
|
+
for (let i = 0; i < swap.source.length; i += 1) this.depthMaterialFor(swap.source[i]);
|
|
236
|
+
} else {
|
|
237
|
+
this.depthMaterialFor(swap.source);
|
|
238
|
+
}
|
|
239
|
+
obj.material = swap.swapped;
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const previousBackground = scene.background;
|
|
243
|
+
// Scene fog would blend the encoded depth toward the fog color on any
|
|
244
|
+
// depth variant that keeps material.fog on (e.g. foliage-provided
|
|
245
|
+
// variants).
|
|
246
|
+
const previousFog = scene.fog;
|
|
247
|
+
const previousClearColor = renderer.getClearColor(this.scratch.clearColor);
|
|
248
|
+
const previousClearAlpha = renderer.getClearAlpha();
|
|
249
|
+
scene.background = null;
|
|
250
|
+
scene.fog = null;
|
|
251
|
+
renderer.setRenderTarget(this.depthTarget);
|
|
252
|
+
// Depth clears to 1 (far plane / sky) like the classic depth buffer.
|
|
253
|
+
renderer.setClearColor(0xffffff, 1);
|
|
254
|
+
renderer.clear();
|
|
255
|
+
renderer.render(scene, camera);
|
|
256
|
+
scene.background = previousBackground;
|
|
257
|
+
scene.fog = previousFog;
|
|
258
|
+
renderer.setClearColor(previousClearColor, previousClearAlpha);
|
|
259
|
+
|
|
260
|
+
for (const { material, mesh } of materialRestores) mesh.material = material;
|
|
261
|
+
for (const obj of visibilityRestores) obj.visible = true;
|
|
262
|
+
materialRestores.length = 0;
|
|
263
|
+
visibilityRestores.length = 0;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
renderGrabPass(renderer, scene, camera, waterMesh) {
|
|
267
|
+
this.ensureGrabTarget(renderer);
|
|
268
|
+
const restoreFlagged = hideFlagged(scene, (object) => object.userData?.waterExclude);
|
|
269
|
+
const waterWasVisible = waterMesh.visible;
|
|
270
|
+
waterMesh.visible = false;
|
|
271
|
+
const previousTarget = renderer.getRenderTarget();
|
|
272
|
+
const previousXr = renderer.xr.enabled;
|
|
273
|
+
const previousShadowAutoUpdate = renderer.shadowMap.autoUpdate;
|
|
274
|
+
try {
|
|
275
|
+
renderer.xr.enabled = false;
|
|
276
|
+
renderer.shadowMap.autoUpdate = false;
|
|
277
|
+
renderer.setRenderTarget(this.grabTarget);
|
|
278
|
+
renderer.clear();
|
|
279
|
+
renderer.render(scene, camera);
|
|
280
|
+
this.renderDepthPass(renderer, scene, camera);
|
|
281
|
+
this.grabValid = true;
|
|
282
|
+
} finally {
|
|
283
|
+
renderer.setRenderTarget(previousTarget);
|
|
284
|
+
renderer.xr.enabled = previousXr;
|
|
285
|
+
renderer.shadowMap.autoUpdate = previousShadowAutoUpdate;
|
|
286
|
+
waterMesh.visible = waterWasVisible;
|
|
287
|
+
restoreFlagged();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
updateReflectionCamera(renderer, camera, waterY) {
|
|
292
|
+
const { cameraPosition, cameraDirection, target } = this.scratch;
|
|
293
|
+
camera.getWorldPosition(cameraPosition);
|
|
294
|
+
if (cameraPosition.y <= waterY + 1e-4) return false;
|
|
295
|
+
|
|
296
|
+
camera.getWorldDirection(cameraDirection);
|
|
297
|
+
target.copy(cameraPosition).add(cameraDirection);
|
|
298
|
+
|
|
299
|
+
const reflectionCamera = this.reflectionCamera;
|
|
300
|
+
// Prevent the node renderer from rebuilding (and wiping) the oblique
|
|
301
|
+
// projection: Renderer.render forces updateProjectionMatrix when the
|
|
302
|
+
// camera's coordinate system mismatches.
|
|
303
|
+
reflectionCamera.coordinateSystem = renderer.coordinateSystem;
|
|
304
|
+
reflectionCamera.position.set(cameraPosition.x, 2 * waterY - cameraPosition.y, cameraPosition.z);
|
|
305
|
+
reflectionCamera.up.copy(camera.up);
|
|
306
|
+
reflectionCamera.up.y *= -1;
|
|
307
|
+
reflectionCamera.lookAt(target.x, 2 * waterY - target.y, target.z);
|
|
308
|
+
reflectionCamera.near = camera.near;
|
|
309
|
+
reflectionCamera.far = camera.far;
|
|
310
|
+
reflectionCamera.projectionMatrix.copy(camera.projectionMatrix);
|
|
311
|
+
reflectionCamera.updateMatrixWorld();
|
|
312
|
+
reflectionCamera.matrixWorldInverse.copy(reflectionCamera.matrixWorld).invert();
|
|
313
|
+
|
|
314
|
+
// Oblique near-plane clipping (Lengyel), same construction as
|
|
315
|
+
// THREE.Reflector: geometry below the water plane never leaks into the
|
|
316
|
+
// mirrored image. The node backends use ReflectorNode's coordinate-
|
|
317
|
+
// system-aware variant of the same construction.
|
|
318
|
+
const { plane, clipPlane, q } = this.scratch;
|
|
319
|
+
plane.setFromNormalAndCoplanarPoint(
|
|
320
|
+
new THREE.Vector3(0, 1, 0),
|
|
321
|
+
new THREE.Vector3(0, waterY, 0));
|
|
322
|
+
plane.applyMatrix4(reflectionCamera.matrixWorldInverse);
|
|
323
|
+
clipPlane.set(plane.normal.x, plane.normal.y, plane.normal.z, plane.constant);
|
|
324
|
+
|
|
325
|
+
const projectionMatrix = reflectionCamera.projectionMatrix;
|
|
326
|
+
q.x = (Math.sign(clipPlane.x) + projectionMatrix.elements[8]) / projectionMatrix.elements[0];
|
|
327
|
+
q.y = (Math.sign(clipPlane.y) + projectionMatrix.elements[9]) / projectionMatrix.elements[5];
|
|
328
|
+
q.z = -1.0;
|
|
329
|
+
q.w = (1.0 + projectionMatrix.elements[10]) / projectionMatrix.elements[14];
|
|
330
|
+
clipPlane.multiplyScalar(1.0 / clipPlane.dot(q));
|
|
331
|
+
projectionMatrix.elements[2] = clipPlane.x;
|
|
332
|
+
projectionMatrix.elements[6] = clipPlane.y;
|
|
333
|
+
projectionMatrix.elements[10] = renderer.coordinateSystem === THREE.WebGPUCoordinateSystem
|
|
334
|
+
? clipPlane.z - this.clipBias
|
|
335
|
+
: clipPlane.z + 1.0 - this.clipBias;
|
|
336
|
+
projectionMatrix.elements[14] = clipPlane.w;
|
|
337
|
+
|
|
338
|
+
this.reflectionMatrix
|
|
339
|
+
.copy(BIAS_MATRIX)
|
|
340
|
+
.multiply(reflectionCamera.projectionMatrix)
|
|
341
|
+
.multiply(reflectionCamera.matrixWorldInverse);
|
|
342
|
+
// Node-backend RTs are written top-down: flip the projective v.
|
|
343
|
+
this.reflectionMatrix.premultiply(FLIP_Y_UV_MATRIX);
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
renderReflectionPass(renderer, scene, camera, waterMesh) {
|
|
348
|
+
this.ensureReflectionTarget(renderer);
|
|
349
|
+
this.scratch.waterWorldPosition.setFromMatrixPosition(waterMesh.matrixWorld);
|
|
350
|
+
if (!this.updateReflectionCamera(renderer, camera, this.scratch.waterWorldPosition.y)) {
|
|
351
|
+
this.reflectionValid = false;
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const restoreFlagged = hideFlagged(scene, (object) => object.userData?.waterExclude ||
|
|
356
|
+
object.userData?.waterReflectionExclude ||
|
|
357
|
+
object.userData?.skipWaterReflection);
|
|
358
|
+
const waterWasVisible = waterMesh.visible;
|
|
359
|
+
waterMesh.visible = false;
|
|
360
|
+
const previousTarget = renderer.getRenderTarget();
|
|
361
|
+
const previousXr = renderer.xr.enabled;
|
|
362
|
+
const previousShadowAutoUpdate = renderer.shadowMap.autoUpdate;
|
|
363
|
+
try {
|
|
364
|
+
renderer.xr.enabled = false;
|
|
365
|
+
renderer.shadowMap.autoUpdate = false;
|
|
366
|
+
renderer.setRenderTarget(this.reflectionTarget);
|
|
367
|
+
renderer.clear();
|
|
368
|
+
renderer.render(scene, this.reflectionCamera);
|
|
369
|
+
this.reflectionValid = true;
|
|
370
|
+
} finally {
|
|
371
|
+
renderer.setRenderTarget(previousTarget);
|
|
372
|
+
renderer.xr.enabled = previousXr;
|
|
373
|
+
renderer.shadowMap.autoUpdate = previousShadowAutoUpdate;
|
|
374
|
+
waterMesh.visible = waterWasVisible;
|
|
375
|
+
restoreFlagged();
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
render(renderer, scene, camera, waterMesh) {
|
|
380
|
+
if (this.sceneColorEnabled) this.renderGrabPass(renderer, scene, camera, waterMesh);
|
|
381
|
+
else this.grabValid = false;
|
|
382
|
+
if (this.reflectionEnabled) this.renderReflectionPass(renderer, scene, camera, waterMesh);
|
|
383
|
+
else this.reflectionValid = false;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
bindToMaterial(material) {
|
|
387
|
+
const uniforms = material?.uniforms;
|
|
388
|
+
if (!uniforms) return;
|
|
389
|
+
if (this.grabValid && this.grabTarget) {
|
|
390
|
+
uniforms.uSceneColor.value = this.grabTarget.texture;
|
|
391
|
+
uniforms.uSceneDepth.value = this.depthTarget.texture;
|
|
392
|
+
uniforms.uUseSceneColor.value = 1;
|
|
393
|
+
uniforms.uUseSceneDepth.value = 1;
|
|
394
|
+
} else {
|
|
395
|
+
uniforms.uUseSceneColor.value = 0;
|
|
396
|
+
uniforms.uUseSceneDepth.value = 0;
|
|
397
|
+
}
|
|
398
|
+
if (this.reflectionValid && this.reflectionTarget) {
|
|
399
|
+
uniforms.uReflectionMap.value = this.reflectionTarget.texture;
|
|
400
|
+
uniforms.uReflectionMatrix.value.copy(this.reflectionMatrix);
|
|
401
|
+
uniforms.uUseReflectionMap.value = 1;
|
|
402
|
+
} else {
|
|
403
|
+
uniforms.uUseReflectionMap.value = 0;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
dispose() {
|
|
408
|
+
this.grabTarget?.dispose();
|
|
409
|
+
this.reflectionTarget?.dispose();
|
|
410
|
+
this.depthTarget?.dispose();
|
|
411
|
+
this.grabTarget = null;
|
|
412
|
+
this.reflectionTarget = null;
|
|
413
|
+
this.depthTarget = null;
|
|
414
|
+
}
|
|
415
|
+
}
|