@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.
Files changed (146) hide show
  1. package/ATTRIBUTION.md +40 -0
  2. package/LICENSE +21 -0
  3. package/README.md +125 -0
  4. package/package.json +97 -0
  5. package/src/character/characterRig.js +353 -0
  6. package/src/character/freestyleSwimClip.js +541 -0
  7. package/src/character/index.js +4 -0
  8. package/src/character/modelLoader.js +433 -0
  9. package/src/core/materialRoles.js +438 -0
  10. package/src/core/presetDocuments.js +110 -0
  11. package/src/core/shaderBackend.js +24 -0
  12. package/src/debrisgen/debrisFields.js +544 -0
  13. package/src/debrisgen/debrisGenerator.js +1929 -0
  14. package/src/debrisgen/debrisPalettes.js +71 -0
  15. package/src/debrisgen/debrisPhysics.js +198 -0
  16. package/src/debrisgen/debrisPresets.js +155 -0
  17. package/src/debrisgen/debrisSettings.js +333 -0
  18. package/src/debrisgen/debrisTextures.js +380 -0
  19. package/src/debrisgen/index.js +5 -0
  20. package/src/debug/fieldValues.js +95 -0
  21. package/src/debug/index.js +7 -0
  22. package/src/debug/settingsPanel.js +170 -0
  23. package/src/environment/environmentAmbientProbe.js +160 -0
  24. package/src/environment/environmentMaterialAdapter.js +336 -0
  25. package/src/environment/environmentMaterialClassifier.js +178 -0
  26. package/src/environment/environmentPlanarReflection.js +201 -0
  27. package/src/environment/environmentPresets.js +443 -0
  28. package/src/environment/environmentRigs.js +684 -0
  29. package/src/environment/environmentSettings.js +504 -0
  30. package/src/environment/environmentShaderMaterials.js +318 -0
  31. package/src/environment/environmentSunShadowPass.js +268 -0
  32. package/src/environment/environmentTextureResolver.js +161 -0
  33. package/src/environment/environmentTimeOfDay.js +167 -0
  34. package/src/environment/environmentVertexAo.js +188 -0
  35. package/src/environment/index.js +8 -0
  36. package/src/environment/scanAssetStylize.js +103 -0
  37. package/src/index.js +21 -0
  38. package/src/loaders/index.js +1 -0
  39. package/src/post/index.js +2 -0
  40. package/src/post/postProcessing.js +1087 -0
  41. package/src/rockgen/export/glbExport.js +177 -0
  42. package/src/rockgen/heightfield/heightfieldErosion.js +7 -0
  43. package/src/rockgen/heightfield/heightfieldPatch.js +183 -0
  44. package/src/rockgen/heightfield/stylizedErosionSim.js +356 -0
  45. package/src/rockgen/index.js +14 -0
  46. package/src/rockgen/mesh/meshAttributes.js +374 -0
  47. package/src/rockgen/mesh/meshDocument.js +158 -0
  48. package/src/rockgen/mesh/surfaceNets.js +329 -0
  49. package/src/rockgen/noise/cellularNoise3.js +92 -0
  50. package/src/rockgen/noise/prng.js +62 -0
  51. package/src/rockgen/noise/simplexNoise3.js +89 -0
  52. package/src/rockgen/noise/valueNoise3.js +87 -0
  53. package/src/rockgen/rockDocument.js +255 -0
  54. package/src/rockgen/rockHelpers.js +8 -0
  55. package/src/rockgen/rockgenPresets.js +559 -0
  56. package/src/rockgen/rockgenSettings.js +756 -0
  57. package/src/rockgen/sdf/fieldCompiler.js +544 -0
  58. package/src/rockgen/sdf/sculptEdits.js +66 -0
  59. package/src/rockgen/sdf/sdfModifiers.js +45 -0
  60. package/src/rockgen/sdf/sdfOps.js +37 -0
  61. package/src/rockgen/sdf/sdfPrimitives.js +88 -0
  62. package/src/shaders-tsl/anime.js +963 -0
  63. package/src/shaders-tsl/chunks/character-color.js +64 -0
  64. package/src/shaders-tsl/chunks/character-highlights.js +174 -0
  65. package/src/shaders-tsl/chunks/character-lighting.js +309 -0
  66. package/src/shaders-tsl/chunks/character-material-maps.js +146 -0
  67. package/src/shaders-tsl/chunks/character-roles.js +52 -0
  68. package/src/shaders-tsl/chunks/character-scene-lights.js +270 -0
  69. package/src/shaders-tsl/chunks/character-shadow-color.js +61 -0
  70. package/src/shaders-tsl/chunks/character-skinning.js +140 -0
  71. package/src/shaders-tsl/chunks/environment-color.js +47 -0
  72. package/src/shaders-tsl/chunks/environment-debug.js +79 -0
  73. package/src/shaders-tsl/chunks/environment-lighting.js +260 -0
  74. package/src/shaders-tsl/chunks/environment-sun-shadow.js +87 -0
  75. package/src/shaders-tsl/chunks/foliage-fog.js +60 -0
  76. package/src/shaders-tsl/chunks/pass-depth-color.js +89 -0
  77. package/src/shaders-tsl/chunks/stylized-cloud-shadow.js +70 -0
  78. package/src/shaders-tsl/chunks/water-color.js +190 -0
  79. package/src/shaders-tsl/chunks/water-common.js +124 -0
  80. package/src/shaders-tsl/chunks/water-foam.js +93 -0
  81. package/src/shaders-tsl/chunks/water-lighting.js +116 -0
  82. package/src/shaders-tsl/chunks/water-ripple.js +64 -0
  83. package/src/shaders-tsl/chunks/water-waves.js +100 -0
  84. package/src/shaders-tsl/environment-ao-overlay.js +60 -0
  85. package/src/shaders-tsl/environment.js +680 -0
  86. package/src/shaders-tsl/flower.js +267 -0
  87. package/src/shaders-tsl/grass.js +193 -0
  88. package/src/shaders-tsl/post-composite.js +461 -0
  89. package/src/shaders-tsl/sky.js +175 -0
  90. package/src/shaders-tsl/tree-leaf.js +264 -0
  91. package/src/shaders-tsl/water-breaker.js +423 -0
  92. package/src/shaders-tsl/water-kelp.js +171 -0
  93. package/src/shaders-tsl/water-rain.js +107 -0
  94. package/src/shaders-tsl/water-simulation.js +140 -0
  95. package/src/shaders-tsl/water-splash.js +298 -0
  96. package/src/shaders-tsl/water.js +509 -0
  97. package/src/sky/index.js +2 -0
  98. package/src/sky/stylizedSky.js +341 -0
  99. package/src/toon/characterRenderPasses.js +646 -0
  100. package/src/toon/index.js +4 -0
  101. package/src/toon/settings/alphaSettings.js +170 -0
  102. package/src/toon/settings/averageShadowSettings.js +181 -0
  103. package/src/toon/settings/baseTextureSettings.js +133 -0
  104. package/src/toon/settings/celShadeSettings.js +96 -0
  105. package/src/toon/settings/contactShadowSettings.js +114 -0
  106. package/src/toon/settings/eyeHighlightSettings.js +187 -0
  107. package/src/toon/settings/faceLightingSettings.js +135 -0
  108. package/src/toon/settings/furSettings.js +112 -0
  109. package/src/toon/settings/glitterSettings.js +102 -0
  110. package/src/toon/settings/hairHighlightSettings.js +325 -0
  111. package/src/toon/settings/indirectLightSettings.js +244 -0
  112. package/src/toon/settings/localLightSettings.js +176 -0
  113. package/src/toon/settings/materialMapSettings.js +299 -0
  114. package/src/toon/settings/outlineSettings.js +263 -0
  115. package/src/toon/settings/perspectiveRemovalSettings.js +60 -0
  116. package/src/toon/settings/rimLightSettings.js +268 -0
  117. package/src/toon/settings/sceneShadowSettings.js +147 -0
  118. package/src/toon/settings/selfShadowSettings.js +221 -0
  119. package/src/toon/settings/shadowColorSettings.js +238 -0
  120. package/src/toon/settings/skinToneSettings.js +150 -0
  121. package/src/toon/settings/specularSettings.js +314 -0
  122. package/src/toon/settings/stickerSettings.js +102 -0
  123. package/src/toon/toonMaterialAdapter.js +2235 -0
  124. package/src/toon/toonSettings.js +860 -0
  125. package/src/vegetation/flowerSpecies.js +260 -0
  126. package/src/vegetation/index.js +10 -0
  127. package/src/vegetation/stylizedBush.js +136 -0
  128. package/src/vegetation/stylizedFlower.js +213 -0
  129. package/src/vegetation/stylizedFlowers.js +297 -0
  130. package/src/vegetation/stylizedGrass.js +491 -0
  131. package/src/vegetation/stylizedTree.js +2449 -0
  132. package/src/vegetation/stylizedTreeFoliage.js +691 -0
  133. package/src/vegetation/treeExport.js +300 -0
  134. package/src/vegetation/treeRecipe.js +647 -0
  135. package/src/water/index.js +2 -0
  136. package/src/water/water.js +20 -0
  137. package/src/water/waterBreakerSystem.js +591 -0
  138. package/src/water/waterInteraction.js +193 -0
  139. package/src/water/waterMaterial.js +263 -0
  140. package/src/water/waterRain.js +136 -0
  141. package/src/water/waterRippleSimulation.js +227 -0
  142. package/src/water/waterScenePasses.js +415 -0
  143. package/src/water/waterSettings.js +1138 -0
  144. package/src/water/waterSplashSystem.js +251 -0
  145. package/src/water/waterSurface.js +475 -0
  146. 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
+ }