@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,193 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
const workingPosition = new THREE.Vector3();
|
|
4
|
+
|
|
5
|
+
// Tracks objects interacting with a WaterSurface and converts their motion
|
|
6
|
+
// into ripples, wakes, and splashes:
|
|
7
|
+
// - fast downward crossing of the surface -> entry splash + ripple ring
|
|
8
|
+
// - fast upward exit -> smaller exit splash
|
|
9
|
+
// - horizontal movement while touching the water -> wake ripples + foam
|
|
10
|
+
//
|
|
11
|
+
// Interactor sources can be a THREE.Object3D or a function/object producing a
|
|
12
|
+
// world position, so physics bodies (e.g. rapier) hook in without adapters.
|
|
13
|
+
export class WaterInteractionManager {
|
|
14
|
+
constructor(surface, {
|
|
15
|
+
wakeInterval = 0.07,
|
|
16
|
+
entrySplashSpeed = 1.4,
|
|
17
|
+
exitSplashSpeed = 2.2,
|
|
18
|
+
minWakeSpeed = 0.12,
|
|
19
|
+
} = {}) {
|
|
20
|
+
this.surface = surface;
|
|
21
|
+
this.wakeInterval = wakeInterval;
|
|
22
|
+
this.entrySplashSpeed = entrySplashSpeed;
|
|
23
|
+
this.exitSplashSpeed = exitSplashSpeed;
|
|
24
|
+
this.minWakeSpeed = minWakeSpeed;
|
|
25
|
+
this.interactors = new Map();
|
|
26
|
+
this.nextId = 1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
resolvePosition(source, out) {
|
|
30
|
+
if (typeof source === 'function') {
|
|
31
|
+
const result = source(out);
|
|
32
|
+
if (result && result !== out) out.set(result.x ?? 0, result.y ?? 0, result.z ?? 0);
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
if (source?.isObject3D) return source.getWorldPosition(out);
|
|
36
|
+
if (typeof source?.getPosition === 'function') {
|
|
37
|
+
const result = source.getPosition(out);
|
|
38
|
+
if (result && result !== out) out.set(result.x ?? 0, result.y ?? 0, result.z ?? 0);
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
if (source && Number.isFinite(source.x)) return out.set(source.x, source.y ?? 0, source.z ?? 0);
|
|
42
|
+
return out.set(0, -1e6, 0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Options: radius (m), splashStrength / wakeStrength multipliers, splashes /
|
|
46
|
+
// wakes toggles, onSplash(event) callback.
|
|
47
|
+
add(source, options = {}) {
|
|
48
|
+
const id = options.id ?? `interactor-${this.nextId += 1}`;
|
|
49
|
+
this.interactors.set(id, {
|
|
50
|
+
source,
|
|
51
|
+
radius: options.radius ?? 0.35,
|
|
52
|
+
// Vertical extent above the tracked point. For tall bodies (characters,
|
|
53
|
+
// boats) this makes "touching" mean the waterline passes through the
|
|
54
|
+
// body, so chest-deep wading wakes but a fully submerged body is silent.
|
|
55
|
+
height: options.height ?? 0,
|
|
56
|
+
splashStrength: options.splashStrength ?? 1,
|
|
57
|
+
wakeStrength: options.wakeStrength ?? 1,
|
|
58
|
+
splashes: options.splashes ?? true,
|
|
59
|
+
wakes: options.wakes ?? true,
|
|
60
|
+
onSplash: options.onSplash ?? null,
|
|
61
|
+
previousPosition: new THREE.Vector3(0, -1e6, 0),
|
|
62
|
+
wasTouching: false,
|
|
63
|
+
wasAboveSurface: true,
|
|
64
|
+
wasBelowSurface: false,
|
|
65
|
+
wakeTimer: 0,
|
|
66
|
+
initialized: false,
|
|
67
|
+
});
|
|
68
|
+
return id;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
remove(id) {
|
|
72
|
+
this.interactors.delete(id);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
clear() {
|
|
76
|
+
this.interactors.clear();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
update(delta) {
|
|
80
|
+
if (delta <= 0) return;
|
|
81
|
+
for (const interactor of this.interactors.values()) {
|
|
82
|
+
this.updateInteractor(interactor, delta);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
updateInteractor(interactor, delta) {
|
|
87
|
+
const position = this.resolvePosition(interactor.source, workingPosition);
|
|
88
|
+
if (position.y < -1e5) return;
|
|
89
|
+
|
|
90
|
+
if (!interactor.initialized) {
|
|
91
|
+
interactor.previousPosition.copy(position);
|
|
92
|
+
interactor.initialized = true;
|
|
93
|
+
interactor.wasTouching = false;
|
|
94
|
+
interactor.wasAboveSurface = true;
|
|
95
|
+
interactor.wasBelowSurface = false;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const surface = this.surface;
|
|
100
|
+
const inArea = surface.containsPoint(position.x, position.z);
|
|
101
|
+
const waterHeight = surface.getHeightAt(position.x, position.z);
|
|
102
|
+
const height = typeof interactor.height === 'function' ? interactor.height() : interactor.height;
|
|
103
|
+
const top = position.y + Math.max(height, interactor.radius);
|
|
104
|
+
const bottom = position.y - interactor.radius;
|
|
105
|
+
// Surface effects only happen where the waterline actually passes through
|
|
106
|
+
// the interactor volume. A body running along the seabed (fully below) or
|
|
107
|
+
// flying above leaves the surface untouched.
|
|
108
|
+
const touching = inArea && waterHeight >= bottom - 0.05 && waterHeight <= top + 0.05;
|
|
109
|
+
const aboveSurface = bottom > waterHeight;
|
|
110
|
+
const belowSurface = top < waterHeight;
|
|
111
|
+
|
|
112
|
+
const velocityY = (position.y - interactor.previousPosition.y) / delta;
|
|
113
|
+
const planarSpeed = Math.hypot(
|
|
114
|
+
position.x - interactor.previousPosition.x,
|
|
115
|
+
position.z - interactor.previousPosition.z) / delta;
|
|
116
|
+
|
|
117
|
+
// Crossing detection uses the previous frame's relation to the surface so
|
|
118
|
+
// a fast fall that skips past the waterline band in one step still counts.
|
|
119
|
+
const enteredFromAbove = interactor.wasAboveSurface && (touching || belowSurface);
|
|
120
|
+
const exitedUpward = (interactor.wasTouching || interactor.wasBelowSurface) && aboveSurface;
|
|
121
|
+
|
|
122
|
+
if (inArea && enteredFromAbove && interactor.splashes &&
|
|
123
|
+
velocityY < -this.entrySplashSpeed) {
|
|
124
|
+
const strength = Math.min(2.4, 0.4 + Math.abs(velocityY) * 0.22) *
|
|
125
|
+
interactor.splashStrength * (0.6 + interactor.radius);
|
|
126
|
+
surface.splash(position, { strength, radius: interactor.radius * 2.2 });
|
|
127
|
+
interactor.onSplash?.({ type: 'enter', position: position.clone(), strength });
|
|
128
|
+
} else if (inArea && exitedUpward && interactor.splashes &&
|
|
129
|
+
velocityY > this.exitSplashSpeed) {
|
|
130
|
+
const strength = Math.min(1.4, 0.3 + velocityY * 0.1) * interactor.splashStrength;
|
|
131
|
+
surface.splash(position, { strength: strength * 0.7, radius: interactor.radius * 1.6 });
|
|
132
|
+
interactor.onSplash?.({ type: 'exit', position: position.clone(), strength });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (interactor.wakes && interactor.wasTouching && belowSurface && velocityY < -0.3) {
|
|
136
|
+
surface.addRipple(position, { radius: interactor.radius * 1.4, strength: -0.4 * interactor.wakeStrength });
|
|
137
|
+
} else if (interactor.wakes && interactor.wasBelowSurface && touching && velocityY > 0.3) {
|
|
138
|
+
surface.addRipple(position, { radius: interactor.radius * 1.4, strength: 0.35 * interactor.wakeStrength });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (touching && interactor.wakes) {
|
|
142
|
+
interactor.wakeTimer -= delta;
|
|
143
|
+
if (planarSpeed > this.minWakeSpeed && interactor.wakeTimer <= 0) {
|
|
144
|
+
const speedFactor = Math.min(planarSpeed, 4);
|
|
145
|
+
// A body pushing through water makes a V-wake, not concentric rings:
|
|
146
|
+
// a rising bow push just ahead, and a pair of depressions at the
|
|
147
|
+
// flanks slightly behind. The propagating pair draws the trailing V.
|
|
148
|
+
const stepX = position.x - interactor.previousPosition.x;
|
|
149
|
+
const stepZ = position.z - interactor.previousPosition.z;
|
|
150
|
+
const stepLength = Math.hypot(stepX, stepZ) || 1;
|
|
151
|
+
const dirX = stepX / stepLength;
|
|
152
|
+
const dirZ = stepZ / stepLength;
|
|
153
|
+
const sideX = -dirZ;
|
|
154
|
+
const sideZ = dirX;
|
|
155
|
+
const radius = interactor.radius;
|
|
156
|
+
const wakeStrength = speedFactor * interactor.wakeStrength;
|
|
157
|
+
surface.addRipple({
|
|
158
|
+
x: position.x + dirX * radius * 1.15,
|
|
159
|
+
z: position.z + dirZ * radius * 1.15,
|
|
160
|
+
}, { radius: radius * (0.9 + speedFactor * 0.1), strength: 0.16 * wakeStrength });
|
|
161
|
+
const backX = position.x - dirX * radius * 0.5;
|
|
162
|
+
const backZ = position.z - dirZ * radius * 0.5;
|
|
163
|
+
surface.addRipple({
|
|
164
|
+
x: backX + sideX * radius,
|
|
165
|
+
z: backZ + sideZ * radius,
|
|
166
|
+
}, { radius: radius * 0.85, strength: -0.12 * wakeStrength });
|
|
167
|
+
surface.addRipple({
|
|
168
|
+
x: backX - sideX * radius,
|
|
169
|
+
z: backZ - sideZ * radius,
|
|
170
|
+
}, { radius: radius * 0.85, strength: -0.12 * wakeStrength });
|
|
171
|
+
// Fast movers kick up a little spray off the bow.
|
|
172
|
+
if (planarSpeed > 1.8 && interactor.splashes) {
|
|
173
|
+
surface.sprayAt({
|
|
174
|
+
x: position.x + dirX * radius,
|
|
175
|
+
y: position.y,
|
|
176
|
+
z: position.z + dirZ * radius,
|
|
177
|
+
}, {
|
|
178
|
+
count: Math.round(2 + speedFactor),
|
|
179
|
+
strength: speedFactor * 0.22 * interactor.splashStrength,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
interactor.wakeTimer = this.wakeInterval / Math.max(planarSpeed, 1);
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
interactor.wakeTimer = 0;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
interactor.wasTouching = touching;
|
|
189
|
+
interactor.wasAboveSurface = aboveSurface;
|
|
190
|
+
interactor.wasBelowSurface = belowSurface;
|
|
191
|
+
interactor.previousPosition.copy(position);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
import { createWaterNodeMaterial } from '../shaders-tsl/water.js';
|
|
4
|
+
import {
|
|
5
|
+
buildGerstnerWaves,
|
|
6
|
+
createWaterSettings,
|
|
7
|
+
WATER_DEBUG_MODES,
|
|
8
|
+
WATER_GERSTNER_WAVE_COUNT,
|
|
9
|
+
} from './waterSettings.js';
|
|
10
|
+
|
|
11
|
+
// Quality tiers gate the most expensive fragment features:
|
|
12
|
+
// low — no caustics/sparkles, 2-octave detail noise
|
|
13
|
+
// medium — caustics + sparkles, no chromatic caustics, 3-octave detail
|
|
14
|
+
// high — chromatic caustics, 4-octave detail
|
|
15
|
+
const QUALITY_DEFINES = Object.freeze({
|
|
16
|
+
low: Object.freeze({ WATER_QUALITY: 0, WATER_DETAIL_OCTAVES: 2, WATER_FOAM_OCTAVES: 2 }),
|
|
17
|
+
medium: Object.freeze({ WATER_QUALITY: 1, WATER_DETAIL_OCTAVES: 3, WATER_FOAM_OCTAVES: 3 }),
|
|
18
|
+
high: Object.freeze({ WATER_QUALITY: 2, WATER_DETAIL_OCTAVES: 4, WATER_FOAM_OCTAVES: 3 }),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The named water quality tiers, keyed 'low' | 'medium' | 'high'. Each tier
|
|
23
|
+
* maps to the shader defines it produces:
|
|
24
|
+
* `{ WATER_QUALITY, WATER_DETAIL_OCTAVES, WATER_FOAM_OCTAVES }` — see
|
|
25
|
+
* resolveWaterQualityDefines for the feature gates per WATER_QUALITY level.
|
|
26
|
+
* Frozen; use a custom `{ detailOctaves, foamOctaves, qualityLevel }` quality
|
|
27
|
+
* object to deviate from the named tiers.
|
|
28
|
+
*/
|
|
29
|
+
export const WATER_QUALITY_TIERS = QUALITY_DEFINES;
|
|
30
|
+
|
|
31
|
+
function clampOctaves(value, fallback) {
|
|
32
|
+
const number = Number(value);
|
|
33
|
+
if (!Number.isFinite(number)) return fallback;
|
|
34
|
+
return Math.min(8, Math.max(1, Math.round(number)));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolves a quality request into the water shader defines
|
|
39
|
+
* `{ WATER_QUALITY, WATER_DETAIL_OCTAVES, WATER_FOAM_OCTAVES }`.
|
|
40
|
+
*
|
|
41
|
+
* Accepts either a named tier ('low' | 'medium' | 'high', see
|
|
42
|
+
* WATER_QUALITY_TIERS) or a custom tier object:
|
|
43
|
+
*
|
|
44
|
+
* { detailOctaves: 5, foamOctaves: 4, qualityLevel: 'high' }
|
|
45
|
+
*
|
|
46
|
+
* - `qualityLevel` picks the feature gate (0/'low' = no caustics or sparkles,
|
|
47
|
+
* 1/'medium' = caustics + sparkles, 2/'high' = chromatic caustics) and the
|
|
48
|
+
* octave fallbacks; it defaults to 'high'.
|
|
49
|
+
* - `detailOctaves` / `foamOctaves` override the noise octave counts
|
|
50
|
+
* (clamped to 1..8 integers).
|
|
51
|
+
*
|
|
52
|
+
* Unknown or missing values fall back to the 'high' tier, matching the
|
|
53
|
+
* previous behavior.
|
|
54
|
+
*
|
|
55
|
+
* @param {string|{detailOctaves?: number, foamOctaves?: number, qualityLevel?: string|number}} quality
|
|
56
|
+
* @returns {{WATER_QUALITY: number, WATER_DETAIL_OCTAVES: number, WATER_FOAM_OCTAVES: number}}
|
|
57
|
+
*/
|
|
58
|
+
export function resolveWaterQualityDefines(quality) {
|
|
59
|
+
if (quality && typeof quality === 'object') {
|
|
60
|
+
const level = quality.qualityLevel;
|
|
61
|
+
const numericLevel = Number(level);
|
|
62
|
+
const tier = Number.isFinite(numericLevel)
|
|
63
|
+
? Object.values(QUALITY_DEFINES)
|
|
64
|
+
.find((entry) => entry.WATER_QUALITY === Math.min(2, Math.max(0, Math.round(numericLevel))))
|
|
65
|
+
: QUALITY_DEFINES[String(level ?? 'high').toLowerCase()];
|
|
66
|
+
const base = tier ?? QUALITY_DEFINES.high;
|
|
67
|
+
return Object.freeze({
|
|
68
|
+
WATER_QUALITY: base.WATER_QUALITY,
|
|
69
|
+
WATER_DETAIL_OCTAVES: clampOctaves(quality.detailOctaves, base.WATER_DETAIL_OCTAVES),
|
|
70
|
+
WATER_FOAM_OCTAVES: clampOctaves(quality.foamOctaves, base.WATER_FOAM_OCTAVES),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return QUALITY_DEFINES[String(quality ?? '').toLowerCase()] ?? QUALITY_DEFINES.high;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// createWaterSettings normalizes `quality` to a named tier string, so a
|
|
77
|
+
// custom tier object must be pulled from the raw options before it is lost.
|
|
78
|
+
function requestedWaterQuality(options, settings) {
|
|
79
|
+
const raw = options && typeof options === 'object' ? options.quality : undefined;
|
|
80
|
+
return raw && typeof raw === 'object' ? raw : settings.quality;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Water colors are authored as sRGB values in settings/presets; uniforms are
|
|
84
|
+
// uploaded in linear working space and converted back at the end of the
|
|
85
|
+
// fragment shader, matching the pipeline used by built-in materials.
|
|
86
|
+
function setSrgbColorUniform(uniform, value) {
|
|
87
|
+
if (!uniform) return;
|
|
88
|
+
const [r, g, b] = Array.isArray(value) ? value : [value?.r ?? 1, value?.g ?? 1, value?.b ?? 1];
|
|
89
|
+
if (!uniform.value?.isColor) uniform.value = new THREE.Color();
|
|
90
|
+
uniform.value.setRGB(r, g, b, THREE.SRGBColorSpace);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function setVector2Uniform(uniform, value) {
|
|
94
|
+
if (!uniform) return;
|
|
95
|
+
if (!uniform.value?.isVector2) uniform.value = new THREE.Vector2();
|
|
96
|
+
uniform.value.set(value?.[0] ?? value?.x ?? 1, value?.[1] ?? value?.y ?? 0);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function setDirection3Uniform(uniform, value) {
|
|
100
|
+
if (!uniform) return;
|
|
101
|
+
if (!uniform.value?.isVector3) uniform.value = new THREE.Vector3();
|
|
102
|
+
uniform.value
|
|
103
|
+
.set(value?.[0] ?? value?.x ?? 0, value?.[1] ?? value?.y ?? 1, value?.[2] ?? value?.z ?? 0)
|
|
104
|
+
.normalize();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function writeWaveUniforms(material, waves) {
|
|
108
|
+
const wavesA = material.uniforms.uWavesA.value;
|
|
109
|
+
const wavesB = material.uniforms.uWavesB.value;
|
|
110
|
+
for (let i = 0; i < WATER_GERSTNER_WAVE_COUNT; i += 1) {
|
|
111
|
+
const wave = waves[i];
|
|
112
|
+
wavesA[i].set(wave.dirX, wave.dirZ, wave.omega, wave.waveNumber);
|
|
113
|
+
wavesB[i].set(wave.amplitude, wave.phase, wave.steepness, wave.crestWeight);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function resolveWaterDebugMode(mode) {
|
|
118
|
+
if (typeof mode === 'number' && Number.isFinite(mode)) return Math.max(0, Math.floor(mode));
|
|
119
|
+
return WATER_DEBUG_MODES[String(mode ?? 'off').toLowerCase()] ?? 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function setWaterDebugMode(material, mode) {
|
|
123
|
+
if (material?.uniforms?.uDebugMode) {
|
|
124
|
+
material.uniforms.uDebugMode.value = resolveWaterDebugMode(mode);
|
|
125
|
+
}
|
|
126
|
+
return material;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function applyWaterSettingsToMaterial(material, options = {}) {
|
|
130
|
+
const uniforms = material?.uniforms;
|
|
131
|
+
if (!uniforms) return material;
|
|
132
|
+
const settings = createWaterSettings(options);
|
|
133
|
+
const waves = buildGerstnerWaves(settings);
|
|
134
|
+
|
|
135
|
+
writeWaveUniforms(material, waves);
|
|
136
|
+
|
|
137
|
+
uniforms.uDetailNormalStrength.value = settings.detailNormalStrength;
|
|
138
|
+
uniforms.uDetailScale.value = settings.detailScale;
|
|
139
|
+
setVector2Uniform(uniforms.uFlowDirection, settings.flowDirection);
|
|
140
|
+
uniforms.uFlowSpeed.value = settings.flowSpeed;
|
|
141
|
+
|
|
142
|
+
setSrgbColorUniform(uniforms.uShallowColor, settings.shallowColor);
|
|
143
|
+
setSrgbColorUniform(uniforms.uMidColor, settings.midColor);
|
|
144
|
+
setSrgbColorUniform(uniforms.uDeepColor, settings.deepColor);
|
|
145
|
+
uniforms.uDepthFadeDistance.value = settings.depthFadeDistance;
|
|
146
|
+
uniforms.uDeepFadeDistance.value = settings.deepFadeDistance;
|
|
147
|
+
uniforms.uOpacity.value = settings.opacity;
|
|
148
|
+
uniforms.uRefractionStrength.value = settings.refractionStrength;
|
|
149
|
+
uniforms.uCausticsStrength.value = settings.causticsStrength;
|
|
150
|
+
uniforms.uCausticsScale.value = settings.causticsScale;
|
|
151
|
+
uniforms.uCausticsSpeed.value = settings.causticsSpeed;
|
|
152
|
+
|
|
153
|
+
setSrgbColorUniform(uniforms.uFoamColor, settings.foamColor);
|
|
154
|
+
uniforms.uFoamAmount.value = settings.foamAmount;
|
|
155
|
+
uniforms.uFoamContactDistance.value = settings.foamContactDistance;
|
|
156
|
+
uniforms.uFoamLineSpacing.value = settings.foamLineSpacing;
|
|
157
|
+
uniforms.uFoamNoiseScale.value = settings.foamNoiseScale;
|
|
158
|
+
uniforms.uWhitecapAmount.value = settings.whitecapAmount;
|
|
159
|
+
uniforms.uRippleFoamStrength.value = settings.rippleFoamStrength;
|
|
160
|
+
|
|
161
|
+
setDirection3Uniform(uniforms.uSunDirection, settings.sunDirection);
|
|
162
|
+
setSrgbColorUniform(uniforms.uSunColor, settings.sunColor);
|
|
163
|
+
uniforms.uSpecularStrength.value = settings.specularStrength;
|
|
164
|
+
uniforms.uSpecularShininess.value = settings.specularShininess;
|
|
165
|
+
uniforms.uSpecularStretch.value = settings.specularStretch;
|
|
166
|
+
uniforms.uSparkleStrength.value = settings.sparkleStrength;
|
|
167
|
+
uniforms.uSparkleScale.value = settings.sparkleScale;
|
|
168
|
+
uniforms.uSparkleSpeed.value = settings.sparkleSpeed;
|
|
169
|
+
uniforms.uSunGlowStrength.value = settings.sunGlowStrength;
|
|
170
|
+
uniforms.uSceneShadowStrength.value = settings.sceneShadowStrength;
|
|
171
|
+
uniforms.uFresnelStrength.value = settings.fresnelStrength;
|
|
172
|
+
uniforms.uFresnelPower.value = settings.fresnelPower;
|
|
173
|
+
uniforms.uFresnelBias.value = settings.fresnelBias;
|
|
174
|
+
uniforms.uReflectionSoftness.value = settings.reflectionSoftness;
|
|
175
|
+
setSrgbColorUniform(uniforms.uFresnelColor, settings.fresnelColor);
|
|
176
|
+
setSrgbColorUniform(uniforms.uSkyZenithColor, settings.skyZenithColor);
|
|
177
|
+
setSrgbColorUniform(uniforms.uSkyHorizonColor, settings.skyHorizonColor);
|
|
178
|
+
uniforms.uReflectionStrength.value = settings.reflectionStrength;
|
|
179
|
+
uniforms.uReflectionDistortion.value = settings.reflectionDistortion;
|
|
180
|
+
|
|
181
|
+
uniforms.uRippleHeightScale.value = settings.rippleHeightScale;
|
|
182
|
+
uniforms.uShoalingDepth.value = settings.shoalingDepth;
|
|
183
|
+
uniforms.uShorelineWaves.value = settings.shorelineWaves;
|
|
184
|
+
uniforms.uShorelineRunup.value = settings.shorelineRunup;
|
|
185
|
+
// Total swell energy — normalizes breaker foam and scales swash run-up.
|
|
186
|
+
const waveEnergy = waves.reduce((sum, wave) => sum + Math.abs(wave.amplitude), 0);
|
|
187
|
+
uniforms.uWaveEnergy.value = waveEnergy;
|
|
188
|
+
material.userData.waterWaveEnergy = waveEnergy;
|
|
189
|
+
|
|
190
|
+
const qualityDefines = resolveWaterQualityDefines(requestedWaterQuality(options, settings));
|
|
191
|
+
const currentDefines = material.defines ?? {};
|
|
192
|
+
if (currentDefines.WATER_QUALITY !== qualityDefines.WATER_QUALITY
|
|
193
|
+
|| currentDefines.WATER_DETAIL_OCTAVES !== qualityDefines.WATER_DETAIL_OCTAVES
|
|
194
|
+
|| currentDefines.WATER_FOAM_OCTAVES !== qualityDefines.WATER_FOAM_OCTAVES) {
|
|
195
|
+
material.defines = { ...material.defines, ...qualityDefines };
|
|
196
|
+
material.needsUpdate = true;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
material.userData.waterMaterial = true;
|
|
200
|
+
material.userData.waterSettings = settings;
|
|
201
|
+
material.userData.gerstnerWaves = waves;
|
|
202
|
+
return material;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function createWaterMaterial(options = {}) {
|
|
206
|
+
const settings = createWaterSettings(options);
|
|
207
|
+
const requestedQuality = requestedWaterQuality(options, settings);
|
|
208
|
+
const qualityDefines = resolveWaterQualityDefines(requestedQuality);
|
|
209
|
+
|
|
210
|
+
// Node-backend material: same uniform-name surface, quality defines baked
|
|
211
|
+
// as graph flags (a later quality change needs a new material on this
|
|
212
|
+
// backend). `options.shoaling` is the WATER_SHOALING define analog —
|
|
213
|
+
// WaterSurface passes it when a bed sampler bakes aBedHeight.
|
|
214
|
+
const material = createWaterNodeMaterial({
|
|
215
|
+
waveCount: WATER_GERSTNER_WAVE_COUNT,
|
|
216
|
+
qualityLevel: qualityDefines.WATER_QUALITY,
|
|
217
|
+
detailOctaves: qualityDefines.WATER_DETAIL_OCTAVES,
|
|
218
|
+
foamOctaves: qualityDefines.WATER_FOAM_OCTAVES,
|
|
219
|
+
shoaling: Boolean(options.shoaling),
|
|
220
|
+
});
|
|
221
|
+
return applyWaterSettingsToMaterial(
|
|
222
|
+
material,
|
|
223
|
+
typeof requestedQuality === 'object' ? { ...settings, quality: requestedQuality } : settings,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const invViewProjScratch = new THREE.Matrix4();
|
|
228
|
+
|
|
229
|
+
// Node backends: the water fragment reconstructs world positions with the
|
|
230
|
+
// literal GLSL math — screenUv/rawDepth * 2 - 1 into uInvViewProjMatrix — so
|
|
231
|
+
// the backend differences are composed into the matrix on the CPU
|
|
232
|
+
// (docs/tsl-conventions.md #6): screenUV is top-left-origin on both node
|
|
233
|
+
// backends (y-negate), and WebGPU NDC z is [0,1] (the stored [0,1] window
|
|
234
|
+
// depth IS the WebGPU NDC z, so z' maps 2d-1 back to d). These are the
|
|
235
|
+
// inverses of pass-depth-color's shadowClipAdjust* matrices.
|
|
236
|
+
const ndcAdjustWebGPU = new THREE.Matrix4().set(
|
|
237
|
+
1, 0, 0, 0,
|
|
238
|
+
0, -1, 0, 0,
|
|
239
|
+
0, 0, 0.5, 0.5,
|
|
240
|
+
0, 0, 0, 1,
|
|
241
|
+
);
|
|
242
|
+
const ndcAdjustGL = new THREE.Matrix4().set(
|
|
243
|
+
1, 0, 0, 0,
|
|
244
|
+
0, -1, 0, 0,
|
|
245
|
+
0, 0, 1, 0,
|
|
246
|
+
0, 0, 0, 1,
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
// Per-frame camera/screen uniform refresh. WaterSurface calls this for you;
|
|
250
|
+
// it is exported for projects driving the material directly.
|
|
251
|
+
export function updateWaterMaterialCamera(material, renderer, camera) {
|
|
252
|
+
const uniforms = material?.uniforms;
|
|
253
|
+
if (!uniforms) return material;
|
|
254
|
+
renderer.getDrawingBufferSize(uniforms.uResolution.value);
|
|
255
|
+
uniforms.uCameraNear.value = camera.near;
|
|
256
|
+
uniforms.uCameraFar.value = camera.far;
|
|
257
|
+
invViewProjScratch.multiplyMatrices(camera.matrixWorld, camera.projectionMatrixInverse);
|
|
258
|
+
invViewProjScratch.multiply(
|
|
259
|
+
renderer.coordinateSystem === THREE.WebGPUCoordinateSystem ? ndcAdjustWebGPU : ndcAdjustGL,
|
|
260
|
+
);
|
|
261
|
+
uniforms.uInvViewProjMatrix.value.copy(invViewProjScratch);
|
|
262
|
+
return material;
|
|
263
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
import { createWaterRainNodeMaterial } from '../shaders-tsl/water-rain.js';
|
|
4
|
+
|
|
5
|
+
// Node-backend rain: WGSL has no gl_PointSize, so the streak POINTS become
|
|
6
|
+
// instanced billboard quads (dust-motes pattern; see water-rain.js). Same
|
|
7
|
+
// public API as the classic Points-based WaterRain — the WaterRain
|
|
8
|
+
// constructor returns an instance of this class on the TSL backend.
|
|
9
|
+
class WaterRainNodeMesh extends THREE.Mesh {
|
|
10
|
+
constructor({
|
|
11
|
+
count = 2400,
|
|
12
|
+
areaSize = 30,
|
|
13
|
+
fallHeight = 16,
|
|
14
|
+
speed = 16,
|
|
15
|
+
streakLength = 0.42,
|
|
16
|
+
wind = [2.2, 0.8],
|
|
17
|
+
color = [0.8, 0.88, 0.95],
|
|
18
|
+
opacity = 0.34,
|
|
19
|
+
} = {}) {
|
|
20
|
+
const quad = new THREE.PlaneGeometry(1, 1);
|
|
21
|
+
const geometry = new THREE.InstancedBufferGeometry();
|
|
22
|
+
geometry.index = quad.index;
|
|
23
|
+
geometry.setAttribute('position', quad.attributes.position);
|
|
24
|
+
geometry.setAttribute('uv', quad.attributes.uv);
|
|
25
|
+
const seeds = new Float32Array(count * 4);
|
|
26
|
+
for (let i = 0; i < count; i += 1) {
|
|
27
|
+
seeds[i * 4] = Math.random();
|
|
28
|
+
seeds[i * 4 + 1] = Math.random();
|
|
29
|
+
seeds[i * 4 + 2] = Math.random();
|
|
30
|
+
seeds[i * 4 + 3] = Math.random();
|
|
31
|
+
}
|
|
32
|
+
geometry.setAttribute('aSeed', new THREE.InstancedBufferAttribute(seeds, 4));
|
|
33
|
+
geometry.instanceCount = count;
|
|
34
|
+
geometry.boundingSphere = new THREE.Sphere(new THREE.Vector3(), 1e5);
|
|
35
|
+
|
|
36
|
+
const material = createWaterRainNodeMaterial({
|
|
37
|
+
areaSize, color, fallHeight, opacity, speed, streakLength, wind,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
super(geometry, material);
|
|
41
|
+
this.name = 'WaterRain';
|
|
42
|
+
this.frustumCulled = false;
|
|
43
|
+
this.renderOrder = 40;
|
|
44
|
+
this.count = count;
|
|
45
|
+
this.baseOpacity = opacity;
|
|
46
|
+
this.intensity = 1;
|
|
47
|
+
// Rain is an overlay effect: keep it out of the water's scene passes.
|
|
48
|
+
this.userData.waterExclude = true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
setIntensity(intensity) {
|
|
52
|
+
this.intensity = THREE.MathUtils.clamp(intensity, 0, 1);
|
|
53
|
+
this.visible = this.intensity > 0.005;
|
|
54
|
+
this.material.uniforms.uOpacity.value = this.baseOpacity * (0.4 + 0.6 * this.intensity);
|
|
55
|
+
// Instanced quads: the draw-count scaling happens through instanceCount
|
|
56
|
+
// (setDrawRange would trim the base quad, not the particle pool).
|
|
57
|
+
this.geometry.instanceCount = Math.floor(this.count * this.intensity);
|
|
58
|
+
return this;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
update(delta, camera, renderer, waterLevel = 0) {
|
|
62
|
+
const uniforms = this.material.uniforms;
|
|
63
|
+
uniforms.uTime.value += Math.min(Math.max(delta ?? 0.016, 0), 0.1);
|
|
64
|
+
if (camera) {
|
|
65
|
+
const position = camera.getWorldPosition(new THREE.Vector3());
|
|
66
|
+
uniforms.uCenter.value.set(position.x, waterLevel, position.z);
|
|
67
|
+
}
|
|
68
|
+
if (renderer) {
|
|
69
|
+
uniforms.uPixelRatio.value = renderer.getPixelRatio();
|
|
70
|
+
uniforms.uPointScale.value = (renderer.domElement?.clientHeight || 540) * 0.9;
|
|
71
|
+
}
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
dispose() {
|
|
76
|
+
this.geometry.dispose();
|
|
77
|
+
this.material.dispose();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Procedural rain: GPU-looping streak particles in a volume that follows a
|
|
82
|
+
// center point (usually the camera over the water). No texture assets and no
|
|
83
|
+
// per-frame buffer writes; intensity scales draw count and opacity.
|
|
84
|
+
//
|
|
85
|
+
// const rain = new WaterRain({ count: 2600 });
|
|
86
|
+
// scene.add(rain);
|
|
87
|
+
// rain.setIntensity(0.9);
|
|
88
|
+
// rain.update(delta, camera, renderer, waterLevel); // each frame
|
|
89
|
+
//
|
|
90
|
+
// Pair it with WaterSurface.addRipple(...) dimples for rain-pocked water.
|
|
91
|
+
export class WaterRain extends THREE.Points {
|
|
92
|
+
constructor({
|
|
93
|
+
count = 2400,
|
|
94
|
+
areaSize = 30,
|
|
95
|
+
fallHeight = 16,
|
|
96
|
+
speed = 16,
|
|
97
|
+
streakLength = 0.42,
|
|
98
|
+
wind = [2.2, 0.8],
|
|
99
|
+
color = [0.8, 0.88, 0.95],
|
|
100
|
+
opacity = 0.34,
|
|
101
|
+
} = {}) {
|
|
102
|
+
// Derived constructors may skip super() when they return an object: the
|
|
103
|
+
// instanced-quad implementation keeps the same public API surface, so call
|
|
104
|
+
// sites stay `new WaterRain(...)`.
|
|
105
|
+
return new WaterRainNodeMesh({
|
|
106
|
+
areaSize, color, count, fallHeight, opacity, speed, streakLength, wind,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
setIntensity(intensity) {
|
|
111
|
+
this.intensity = THREE.MathUtils.clamp(intensity, 0, 1);
|
|
112
|
+
this.visible = this.intensity > 0.005;
|
|
113
|
+
this.material.uniforms.uOpacity.value = this.baseOpacity * (0.4 + 0.6 * this.intensity);
|
|
114
|
+
this.geometry.setDrawRange(0, Math.floor(this.count * this.intensity));
|
|
115
|
+
return this;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
update(delta, camera, renderer, waterLevel = 0) {
|
|
119
|
+
const uniforms = this.material.uniforms;
|
|
120
|
+
uniforms.uTime.value += Math.min(Math.max(delta ?? 0.016, 0), 0.1);
|
|
121
|
+
if (camera) {
|
|
122
|
+
const position = camera.getWorldPosition(new THREE.Vector3());
|
|
123
|
+
uniforms.uCenter.value.set(position.x, waterLevel, position.z);
|
|
124
|
+
}
|
|
125
|
+
if (renderer) {
|
|
126
|
+
uniforms.uPixelRatio.value = renderer.getPixelRatio();
|
|
127
|
+
uniforms.uPointScale.value = (renderer.domElement?.clientHeight || 540) * 0.9;
|
|
128
|
+
}
|
|
129
|
+
return this;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
dispose() {
|
|
133
|
+
this.geometry.dispose();
|
|
134
|
+
this.material.dispose();
|
|
135
|
+
}
|
|
136
|
+
}
|