@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,264 @@
|
|
|
1
|
+
// TSL port of src/shaders/treeLeaf.vert.glsl + treeLeaf.frag.glsl (+ the
|
|
2
|
+
// treeLeafDepth cutout depth variant) — world-anchored leaf-cluster cards
|
|
3
|
+
// with the canopy-volume shading normal.
|
|
4
|
+
//
|
|
5
|
+
// The color material exposes userData.createDepthColorVariant() so the sun
|
|
6
|
+
// shadow pass can render leafy cutout shadows with the exact same card
|
|
7
|
+
// vertex placement (the classic path uses mesh.customDepthMaterial instead).
|
|
8
|
+
|
|
9
|
+
import * as THREE from 'three';
|
|
10
|
+
import {
|
|
11
|
+
abs,
|
|
12
|
+
attribute,
|
|
13
|
+
cameraFar,
|
|
14
|
+
cameraNear,
|
|
15
|
+
cameraPosition,
|
|
16
|
+
cameraProjectionMatrix,
|
|
17
|
+
cameraViewMatrix,
|
|
18
|
+
clamp,
|
|
19
|
+
cos,
|
|
20
|
+
cross,
|
|
21
|
+
Discard,
|
|
22
|
+
dot,
|
|
23
|
+
float,
|
|
24
|
+
Fn,
|
|
25
|
+
fract,
|
|
26
|
+
mat2,
|
|
27
|
+
mix,
|
|
28
|
+
modelWorldMatrix,
|
|
29
|
+
mat3,
|
|
30
|
+
normalize,
|
|
31
|
+
positionLocal,
|
|
32
|
+
pow,
|
|
33
|
+
select,
|
|
34
|
+
sin,
|
|
35
|
+
smoothstep,
|
|
36
|
+
texture,
|
|
37
|
+
uniform,
|
|
38
|
+
uv,
|
|
39
|
+
varying,
|
|
40
|
+
vec2,
|
|
41
|
+
vec3,
|
|
42
|
+
vec4,
|
|
43
|
+
viewZToOrthographicDepth,
|
|
44
|
+
viewZToPerspectiveDepth,
|
|
45
|
+
} from 'three/tsl';
|
|
46
|
+
import { NodeMaterial } from 'three/webgpu';
|
|
47
|
+
|
|
48
|
+
import { sampleEnvironmentSunShadow } from './chunks/environment-sun-shadow.js';
|
|
49
|
+
import { stylizedCloudShadow } from './chunks/stylized-cloud-shadow.js';
|
|
50
|
+
import { applyFoliageFog, createFoliageFogUniforms } from './chunks/foliage-fog.js';
|
|
51
|
+
|
|
52
|
+
// Shared card-placement vertex logic (color + depth variants). Returns the
|
|
53
|
+
// clip position; assigns the provided varyings when given.
|
|
54
|
+
function buildLeafVertexNode(u, { vUv, vWorldNormal, vWorldPosition, vTint, vHeightT, vViewZ }) {
|
|
55
|
+
return Fn(() => {
|
|
56
|
+
const aCorner = attribute('aCorner', 'vec2');
|
|
57
|
+
const aShadeNormal = attribute('aShadeNormal', 'vec3');
|
|
58
|
+
const aInfo = attribute('aInfo', 'vec4');
|
|
59
|
+
|
|
60
|
+
vUv.assign(uv());
|
|
61
|
+
if (vTint) vTint.assign(aInfo.z);
|
|
62
|
+
if (vHeightT) vHeightT.assign(aInfo.w);
|
|
63
|
+
if (vWorldNormal) vWorldNormal.assign(normalize(mat3(modelWorldMatrix).mul(aShadeNormal)));
|
|
64
|
+
|
|
65
|
+
const worldCenter = modelWorldMatrix.mul(vec4(positionLocal, 1.0)).toVar();
|
|
66
|
+
|
|
67
|
+
// Coherent canopy sway, stronger toward the crown.
|
|
68
|
+
const phase = u.uTime.mul(u.uWindSpeed).add(worldCenter.x.mul(0.6)).add(worldCenter.z.mul(0.5));
|
|
69
|
+
const sway = sin(phase).mul(0.6).add(sin(phase.mul(1.7).add(1.3)).mul(0.4));
|
|
70
|
+
worldCenter.xz.addAssign(
|
|
71
|
+
normalize(u.uWindDirection.add(vec2(1e-4, 0.0)))
|
|
72
|
+
.mul(sway).mul(u.uWindStrength).mul(aInfo.w.mul(0.75).add(0.25)),
|
|
73
|
+
);
|
|
74
|
+
if (vWorldPosition) vWorldPosition.assign(worldCenter.xyz);
|
|
75
|
+
|
|
76
|
+
// Static random roll per card + light flutter.
|
|
77
|
+
const roll = aInfo.y.mul(6.2831).add(
|
|
78
|
+
sin(u.uTime.mul(fract(aInfo.y.mul(7.31)).add(1.1)).mul(u.uWindSpeed).add(aInfo.y.mul(37.0))).mul(0.09),
|
|
79
|
+
);
|
|
80
|
+
const rollCos = cos(roll);
|
|
81
|
+
const rollSin = sin(roll);
|
|
82
|
+
// GLSL mat2(c, s, -s, c) is column-major; TSL matN() scalars are
|
|
83
|
+
// row-major — transposed argument order (docs/tsl-conventions.md).
|
|
84
|
+
const corner = mat2(rollCos, rollSin.negate(), rollSin, rollCos).mul(aCorner).mul(aInfo.x);
|
|
85
|
+
|
|
86
|
+
// World-anchored cylindrical billboard (cameraPosition is the light
|
|
87
|
+
// during the shadow pass, matching the classic depth-pass behavior).
|
|
88
|
+
const toCamera = cameraPosition.sub(worldCenter.xyz);
|
|
89
|
+
const facing = normalize(mix(
|
|
90
|
+
vec3(toCamera.x, 0.0, toCamera.z).add(vec3(1e-4, 0.0, 0.0)),
|
|
91
|
+
toCamera,
|
|
92
|
+
0.35,
|
|
93
|
+
));
|
|
94
|
+
const cardRight = normalize(cross(vec3(0.0, 1.0, 0.0), facing));
|
|
95
|
+
const cardUp = normalize(cross(facing, cardRight));
|
|
96
|
+
worldCenter.xyz.addAssign(cardRight.mul(corner.x).add(cardUp.mul(corner.y)));
|
|
97
|
+
|
|
98
|
+
const viewPosition = cameraViewMatrix.mul(worldCenter).toVar();
|
|
99
|
+
if (vViewZ) vViewZ.assign(viewPosition.z);
|
|
100
|
+
return cameraProjectionMatrix.mul(viewPosition);
|
|
101
|
+
})();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function createTreeLeafNodeMaterial(settings) {
|
|
105
|
+
const u = {
|
|
106
|
+
uAlphaCutoff: uniform(settings.alphaCutoff ?? 0.5),
|
|
107
|
+
uBacklitStrength: uniform(settings.backlitStrength ?? 0),
|
|
108
|
+
uCloudShadowCoverage: uniform(settings.cloudShadowCoverage ?? 0),
|
|
109
|
+
uCloudShadowScale: uniform(settings.cloudShadowScale ?? 1),
|
|
110
|
+
uCloudShadowStrength: uniform(settings.cloudShadowStrength ?? 0),
|
|
111
|
+
uCloudShadowVelocity: uniform(new THREE.Vector2(...(settings.cloudShadowVelocity ?? [0, 0]))),
|
|
112
|
+
uCrownColor: uniform(new THREE.Color()),
|
|
113
|
+
uLeafMap: texture(settings.leafMap),
|
|
114
|
+
uLitColor: uniform(new THREE.Color()),
|
|
115
|
+
uSceneShadowStrength: uniform(settings.sceneShadowStrength ?? 0),
|
|
116
|
+
uShadowColor: uniform(new THREE.Color()),
|
|
117
|
+
uSkyColor: uniform(new THREE.Color()),
|
|
118
|
+
uSunColor: uniform(new THREE.Color()),
|
|
119
|
+
uSunDirection: uniform(new THREE.Vector3(0.35, 0.75, 0.5).normalize()),
|
|
120
|
+
uTime: uniform(0),
|
|
121
|
+
uWindDirection: uniform(new THREE.Vector2(...(settings.windDirection ?? [1, 0.4]))),
|
|
122
|
+
uWindSpeed: uniform(settings.windSpeed ?? 1),
|
|
123
|
+
uWindStrength: uniform(settings.windStrength ?? 0.06),
|
|
124
|
+
...createFoliageFogUniforms(),
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const material = new NodeMaterial();
|
|
128
|
+
material.name = 'StylizedTreeFoliage';
|
|
129
|
+
// Manual fog (see chunks/foliage-fog.js): the custom billboard vertexNode
|
|
130
|
+
// makes three's built-in node fog read the wrong view depth.
|
|
131
|
+
material.fog = false;
|
|
132
|
+
|
|
133
|
+
const vUv = varying(vec2(), 'vLeafUv');
|
|
134
|
+
const vWorldNormal = varying(vec3(), 'vLeafWorldNormal');
|
|
135
|
+
const vWorldPosition = varying(vec3(), 'vLeafWorldPosition');
|
|
136
|
+
const vTint = varying(float(), 'vLeafTint');
|
|
137
|
+
const vHeightT = varying(float(), 'vLeafHeightT');
|
|
138
|
+
const vViewZ = varying(float(), 'vLeafViewZ');
|
|
139
|
+
|
|
140
|
+
material.vertexNode = buildLeafVertexNode(u, { vHeightT, vTint, vUv, vViewZ, vWorldNormal, vWorldPosition });
|
|
141
|
+
|
|
142
|
+
material.fragmentNode = Fn(() => {
|
|
143
|
+
const sprite = u.uLeafMap.sample(vUv).toVar();
|
|
144
|
+
Discard(sprite.a.lessThan(u.uAlphaCutoff));
|
|
145
|
+
|
|
146
|
+
const normal = normalize(vWorldNormal).toVar();
|
|
147
|
+
const sunDirection = normalize(u.uSunDirection);
|
|
148
|
+
const wrap = dot(normal, sunDirection).mul(0.5).add(0.5).toVar();
|
|
149
|
+
const cloudShadow = stylizedCloudShadow(
|
|
150
|
+
vWorldPosition.xz, u.uTime,
|
|
151
|
+
u.uCloudShadowStrength, u.uCloudShadowCoverage, u.uCloudShadowScale, u.uCloudShadowVelocity,
|
|
152
|
+
);
|
|
153
|
+
const sceneShadow = mix(1.0, sampleEnvironmentSunShadow(vWorldPosition), u.uSceneShadowStrength).toVar();
|
|
154
|
+
const sunVisibility = cloudShadow.mul(sceneShadow).toVar();
|
|
155
|
+
|
|
156
|
+
const litBand = smoothstep(0.38, 0.56, wrap).mul(sunVisibility).toVar();
|
|
157
|
+
const crestBand = smoothstep(0.66, 0.78, wrap).mul(sunVisibility)
|
|
158
|
+
.mul(smoothstep(0.3, 0.8, vHeightT.mul(normal.y.mul(0.5).add(0.5).mul(0.3).add(0.7))));
|
|
159
|
+
|
|
160
|
+
const color = mix(u.uShadowColor, u.uLitColor, litBand).toVar();
|
|
161
|
+
color.assign(mix(color, u.uCrownColor, crestBand));
|
|
162
|
+
color.mulAssign(mix(vec3(1.0), u.uSunColor, litBand.mul(0.35)));
|
|
163
|
+
|
|
164
|
+
// Baked per-leaf luminance + per-card jitter.
|
|
165
|
+
color.mulAssign(sprite.r.mul(0.36).add(0.78));
|
|
166
|
+
color.mulAssign(vTint.mul(0.16).add(0.92));
|
|
167
|
+
|
|
168
|
+
// Sky fill + silhouette rim.
|
|
169
|
+
color.addAssign(u.uSkyColor.mul(normal.y.mul(0.5).add(0.5)).mul(litBand.oneMinus()).mul(0.10));
|
|
170
|
+
const viewDirection = normalize(cameraPosition.sub(vWorldPosition));
|
|
171
|
+
const rim = pow(abs(dot(normal, viewDirection)).oneMinus(), 3.0);
|
|
172
|
+
color.addAssign(u.uSkyColor.mul(rim).mul(0.12).mul(vHeightT.mul(0.6).add(0.4)));
|
|
173
|
+
|
|
174
|
+
// Backlit translucency.
|
|
175
|
+
const backlit = pow(clamp(dot(viewDirection, sunDirection.negate()), 0.0, 1.0), 4.0);
|
|
176
|
+
color.addAssign(
|
|
177
|
+
u.uSunColor.mul(backlit).mul(u.uBacklitStrength)
|
|
178
|
+
.mul(sprite.r.mul(0.7).add(0.3))
|
|
179
|
+
.mul(sunVisibility.mul(0.65).add(0.35)),
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// Occluded crowns read clearly darker.
|
|
183
|
+
color.mulAssign(mix(0.8, 1.0, sceneShadow));
|
|
184
|
+
|
|
185
|
+
// Manual linear scene fog on the true billboarded depth (see foliage-fog).
|
|
186
|
+
color.assign(applyFoliageFog(color, vViewZ, u));
|
|
187
|
+
|
|
188
|
+
return vec4(color, 1.0);
|
|
189
|
+
})();
|
|
190
|
+
|
|
191
|
+
material.uniforms = u;
|
|
192
|
+
|
|
193
|
+
// Native node-renderer shadow casting (tree/rock labs keep three's shadow
|
|
194
|
+
// system — no MMD skeletons there): the shadow pass takes a LOCAL-space
|
|
195
|
+
// position override plus a cutout mask. cameraPosition binds to the light
|
|
196
|
+
// during that pass, matching the classic depth-pass card facing; the world
|
|
197
|
+
// card placement is pulled back to local via the inverse model matrix.
|
|
198
|
+
material.castShadowPositionNode = (() => {
|
|
199
|
+
return Fn(() => {
|
|
200
|
+
const aCorner = attribute('aCorner', 'vec2');
|
|
201
|
+
const aInfo = attribute('aInfo', 'vec4');
|
|
202
|
+
const worldCenter = modelWorldMatrix.mul(vec4(positionLocal, 1.0)).toVar();
|
|
203
|
+
const phase = u.uTime.mul(u.uWindSpeed).add(worldCenter.x.mul(0.6)).add(worldCenter.z.mul(0.5));
|
|
204
|
+
const sway = sin(phase).mul(0.6).add(sin(phase.mul(1.7).add(1.3)).mul(0.4));
|
|
205
|
+
worldCenter.xz.addAssign(
|
|
206
|
+
normalize(u.uWindDirection.add(vec2(1e-4, 0.0)))
|
|
207
|
+
.mul(sway).mul(u.uWindStrength).mul(aInfo.w.mul(0.75).add(0.25)),
|
|
208
|
+
);
|
|
209
|
+
const roll = aInfo.y.mul(6.2831).add(
|
|
210
|
+
sin(u.uTime.mul(fract(aInfo.y.mul(7.31)).add(1.1)).mul(u.uWindSpeed).add(aInfo.y.mul(37.0))).mul(0.09),
|
|
211
|
+
);
|
|
212
|
+
const rollCos = cos(roll);
|
|
213
|
+
const rollSin = sin(roll);
|
|
214
|
+
const corner = mat2(rollCos, rollSin.negate(), rollSin, rollCos).mul(aCorner).mul(aInfo.x);
|
|
215
|
+
const toCamera = cameraPosition.sub(worldCenter.xyz);
|
|
216
|
+
const facing = normalize(mix(
|
|
217
|
+
vec3(toCamera.x, 0.0, toCamera.z).add(vec3(1e-4, 0.0, 0.0)),
|
|
218
|
+
toCamera,
|
|
219
|
+
0.35,
|
|
220
|
+
));
|
|
221
|
+
const cardRight = normalize(cross(vec3(0.0, 1.0, 0.0), facing));
|
|
222
|
+
const cardUp = normalize(cross(facing, cardRight));
|
|
223
|
+
worldCenter.xyz.addAssign(cardRight.mul(corner.x).add(cardUp.mul(corner.y)));
|
|
224
|
+
return modelWorldMatrix.inverse().mul(worldCenter).xyz;
|
|
225
|
+
})();
|
|
226
|
+
})();
|
|
227
|
+
// Cutout in the shadow pass only (the custom fragmentNode bypasses
|
|
228
|
+
// maskNode in the main render).
|
|
229
|
+
material.maskNode = u.uLeafMap.sample(uv()).a.greaterThanEqual(u.uAlphaCutoff);
|
|
230
|
+
|
|
231
|
+
// Leafy cutout depth variant for the Water Lab's custom sun-shadow pass.
|
|
232
|
+
// Native WebGPU shadows consume castShadowPositionNode/maskNode from the
|
|
233
|
+
// source material, but Water Lab swaps in this material and renders a color
|
|
234
|
+
// depth target. Make it self-contained with the same full billboard vertex
|
|
235
|
+
// path, so it does not depend on native-shadow-only override hooks.
|
|
236
|
+
material.userData.createDepthColorVariant = () => {
|
|
237
|
+
const depthMaterial = new NodeMaterial();
|
|
238
|
+
depthMaterial.name = 'StylizedTreeFoliageDepth';
|
|
239
|
+
depthMaterial.side = THREE.DoubleSide;
|
|
240
|
+
depthMaterial.fog = false;
|
|
241
|
+
depthMaterial.isShadowPassMaterial = true;
|
|
242
|
+
|
|
243
|
+
const vDepthUv = varying(vec2(), 'vLeafDepthUv');
|
|
244
|
+
const vDepthViewZ = varying(float(), 'vLeafDepthViewZ');
|
|
245
|
+
depthMaterial.vertexNode = buildLeafVertexNode(u, {
|
|
246
|
+
vUv: vDepthUv,
|
|
247
|
+
vViewZ: vDepthViewZ,
|
|
248
|
+
});
|
|
249
|
+
depthMaterial.fragmentNode = Fn(() => {
|
|
250
|
+
const sprite = u.uLeafMap.sample(vDepthUv).toVar();
|
|
251
|
+
Discard(sprite.a.lessThan(u.uAlphaCutoff));
|
|
252
|
+
const orthographic = cameraProjectionMatrix.element(3).w.equal(1.0);
|
|
253
|
+
const depth01 = select(
|
|
254
|
+
orthographic,
|
|
255
|
+
viewZToOrthographicDepth(vDepthViewZ, cameraNear, cameraFar),
|
|
256
|
+
viewZToPerspectiveDepth(vDepthViewZ, cameraNear, cameraFar),
|
|
257
|
+
).toVar();
|
|
258
|
+
return vec4(vec3(depth01), 1.0);
|
|
259
|
+
})();
|
|
260
|
+
return depthMaterial;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
return material;
|
|
264
|
+
}
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
// TSL port of src/shaders/waterBreaker.vert.glsl + waterBreaker.frag.glsl —
|
|
2
|
+
// the dedicated plunging-breaker shells that ride the dominant Gerstner crest.
|
|
3
|
+
//
|
|
4
|
+
// The shell must read as the SAME water as the open sea, so the fragment runs
|
|
5
|
+
// the surface's shading stack through the shared water chunks, and the
|
|
6
|
+
// factory can adopt the surface material's uniform NODES for the shared
|
|
7
|
+
// lighting/wave uniforms (`shared` option). Sharing node objects across
|
|
8
|
+
// materials is the TSL equivalent of the classic uniforms-by-reference wiring
|
|
9
|
+
// in WaterBreakerSystem.attachWaveUniforms — swapping entries in `.uniforms`
|
|
10
|
+
// after the graph is built would NOT rewire it, so attachWaveUniforms rebuilds
|
|
11
|
+
// the material through this factory instead (before first render).
|
|
12
|
+
//
|
|
13
|
+
// TODO(tsl Phase 7): fog ordering — GLSL fogs after colorspace; scene fog is
|
|
14
|
+
// disabled here until the fog phase resolves the ordering.
|
|
15
|
+
|
|
16
|
+
import * as THREE from 'three';
|
|
17
|
+
import {
|
|
18
|
+
clamp,
|
|
19
|
+
cos,
|
|
20
|
+
cross,
|
|
21
|
+
Discard,
|
|
22
|
+
dot,
|
|
23
|
+
float,
|
|
24
|
+
Fn,
|
|
25
|
+
If,
|
|
26
|
+
length,
|
|
27
|
+
max,
|
|
28
|
+
min,
|
|
29
|
+
mix,
|
|
30
|
+
modelWorldMatrix,
|
|
31
|
+
normalize,
|
|
32
|
+
positionGeometry,
|
|
33
|
+
select,
|
|
34
|
+
sin,
|
|
35
|
+
smoothstep,
|
|
36
|
+
sqrt,
|
|
37
|
+
step,
|
|
38
|
+
texture,
|
|
39
|
+
uniform,
|
|
40
|
+
uniformArray,
|
|
41
|
+
varying,
|
|
42
|
+
vec2,
|
|
43
|
+
vec3,
|
|
44
|
+
vec4,
|
|
45
|
+
attribute,
|
|
46
|
+
cameraPosition,
|
|
47
|
+
cameraProjectionMatrix,
|
|
48
|
+
cameraViewMatrix,
|
|
49
|
+
} from 'three/tsl';
|
|
50
|
+
import { NodeMaterial } from 'three/webgpu';
|
|
51
|
+
|
|
52
|
+
import { waterCombineNormal, waterFbm } from './chunks/water-common.js';
|
|
53
|
+
import { createWaterFoamChunk } from './chunks/water-foam.js';
|
|
54
|
+
import { createWaterLightingChunk } from './chunks/water-lighting.js';
|
|
55
|
+
import { createWaterWavesChunk } from './chunks/water-waves.js';
|
|
56
|
+
import { resolveWaterArrayUniformNode, waterArrayUniformEntry } from './water.js';
|
|
57
|
+
|
|
58
|
+
const PI = Math.PI;
|
|
59
|
+
const PI2 = Math.PI * 2;
|
|
60
|
+
|
|
61
|
+
let placeholderReflection = null;
|
|
62
|
+
function getPlaceholderReflection() {
|
|
63
|
+
if (!placeholderReflection) {
|
|
64
|
+
placeholderReflection = new THREE.DataTexture(
|
|
65
|
+
new Uint8Array([126, 190, 218, 255]), 1, 1, THREE.RGBAFormat);
|
|
66
|
+
placeholderReflection.needsUpdate = true;
|
|
67
|
+
}
|
|
68
|
+
return placeholderReflection;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Uniform names adopted from the owning surface material when `shared` is
|
|
72
|
+
// given — must stay in sync with SHARED_SURFACE_UNIFORMS in
|
|
73
|
+
// waterBreakerSystem.js (which drives the classic by-reference sharing).
|
|
74
|
+
const SHARABLE = [
|
|
75
|
+
'uWavesA', 'uWavesB',
|
|
76
|
+
'uShallowColor', 'uMidColor', 'uDeepColor', 'uFoamColor', 'uFoamNoiseScale',
|
|
77
|
+
'uFoamContactDistance', 'uFoamLineSpacing', 'uWhitecapAmount',
|
|
78
|
+
'uSunDirection', 'uSunColor',
|
|
79
|
+
'uFlowDirection', 'uFlowSpeed', 'uDetailScale', 'uDetailNormalStrength',
|
|
80
|
+
'uSpecularStrength', 'uSpecularShininess', 'uSpecularStretch',
|
|
81
|
+
'uSparkleStrength', 'uSparkleScale', 'uSparkleSpeed', 'uSunGlowStrength',
|
|
82
|
+
'uFresnelStrength', 'uFresnelPower', 'uFresnelBias', 'uFresnelColor',
|
|
83
|
+
'uSkyZenithColor', 'uSkyHorizonColor',
|
|
84
|
+
'uReflectionMap', 'uReflectionMatrix', 'uUseReflectionMap',
|
|
85
|
+
'uReflectionStrength', 'uReflectionDistortion', 'uReflectionSoftness',
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* options.waveCount WATER_WAVE_COUNT (must match the shared wave arrays)
|
|
90
|
+
* options.foamOctaves WATER_FOAM_OCTAVES
|
|
91
|
+
* options.shared uniform-node map of the owning surface material; the
|
|
92
|
+
* SHARABLE subset is adopted by reference.
|
|
93
|
+
* options.previous uniform-node map of a previous breaker material; own
|
|
94
|
+
* (non-shared) uniforms are reused so values persist
|
|
95
|
+
* across the attachWaveUniforms rebuild.
|
|
96
|
+
*/
|
|
97
|
+
export function createWaterBreakerNodeMaterial({
|
|
98
|
+
waveCount,
|
|
99
|
+
foamOctaves = 3,
|
|
100
|
+
shared = null,
|
|
101
|
+
previous = null,
|
|
102
|
+
} = {}) {
|
|
103
|
+
const own = {
|
|
104
|
+
uTime: uniform(0),
|
|
105
|
+
uBreakerAmount: uniform(0),
|
|
106
|
+
uBreakerCurl: uniform(0.8),
|
|
107
|
+
uBreakerScale: uniform(1),
|
|
108
|
+
uBreakerPeel: uniform(1),
|
|
109
|
+
uShoalingDepth: uniform(1.4),
|
|
110
|
+
uShorelineWaves: uniform(0.35),
|
|
111
|
+
uWaveEnergy: uniform(0.3),
|
|
112
|
+
uSetPair: uniform(0),
|
|
113
|
+
uShallowColor: uniform(new THREE.Color()),
|
|
114
|
+
uMidColor: uniform(new THREE.Color()),
|
|
115
|
+
uDeepColor: uniform(new THREE.Color()),
|
|
116
|
+
uFoamColor: uniform(new THREE.Color()),
|
|
117
|
+
uSunDirection: uniform(new THREE.Vector3(0, 1, 0)),
|
|
118
|
+
uSunColor: uniform(new THREE.Color()),
|
|
119
|
+
uFoamNoiseScale: uniform(0.6),
|
|
120
|
+
// The shared foam chunk needs these declared; scene depth is never
|
|
121
|
+
// available on the shell, so the shoreline-foam path stays off.
|
|
122
|
+
uUseSceneDepth: uniform(0),
|
|
123
|
+
uFoamContactDistance: uniform(1),
|
|
124
|
+
uFoamLineSpacing: uniform(1.4),
|
|
125
|
+
uWhitecapAmount: uniform(0.3),
|
|
126
|
+
uFlowDirection: uniform(new THREE.Vector2(1, 0)),
|
|
127
|
+
uFlowSpeed: uniform(0.35),
|
|
128
|
+
uDetailScale: uniform(0.5),
|
|
129
|
+
uDetailNormalStrength: uniform(0.5),
|
|
130
|
+
uSpecularStrength: uniform(0.8),
|
|
131
|
+
uSpecularShininess: uniform(120),
|
|
132
|
+
uSpecularStretch: uniform(0.5),
|
|
133
|
+
uSparkleStrength: uniform(0.5),
|
|
134
|
+
uSparkleScale: uniform(1.2),
|
|
135
|
+
uSparkleSpeed: uniform(1),
|
|
136
|
+
uSunGlowStrength: uniform(0.6),
|
|
137
|
+
uFresnelStrength: uniform(0.6),
|
|
138
|
+
uFresnelPower: uniform(3),
|
|
139
|
+
uFresnelBias: uniform(0.12),
|
|
140
|
+
uFresnelColor: uniform(new THREE.Color(1, 1, 1)),
|
|
141
|
+
uSkyZenithColor: uniform(new THREE.Color(0.35, 0.58, 0.85)),
|
|
142
|
+
uSkyHorizonColor: uniform(new THREE.Color(0.85, 0.92, 1.0)),
|
|
143
|
+
uReflectionMap: texture(getPlaceholderReflection()),
|
|
144
|
+
uReflectionMatrix: uniform(new THREE.Matrix4()),
|
|
145
|
+
uUseReflectionMap: uniform(0),
|
|
146
|
+
uReflectionStrength: uniform(0.55),
|
|
147
|
+
uReflectionDistortion: uniform(0.04),
|
|
148
|
+
uReflectionSoftness: uniform(0.4),
|
|
149
|
+
uWavesA: waterArrayUniformEntry(
|
|
150
|
+
uniformArray(Array.from({ length: waveCount }, () => new THREE.Vector4()), 'vec4'),
|
|
151
|
+
),
|
|
152
|
+
uWavesB: waterArrayUniformEntry(
|
|
153
|
+
uniformArray(Array.from({ length: waveCount }, () => new THREE.Vector4()), 'vec4'),
|
|
154
|
+
),
|
|
155
|
+
};
|
|
156
|
+
if (previous) {
|
|
157
|
+
for (const name of Object.keys(own)) {
|
|
158
|
+
if (previous[name]) own[name] = previous[name];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const u = { ...own };
|
|
162
|
+
if (shared) {
|
|
163
|
+
for (const name of SHARABLE) {
|
|
164
|
+
if (shared[name]) u[name] = shared[name];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const wavesANode = resolveWaterArrayUniformNode(u.uWavesA);
|
|
169
|
+
const wavesBNode = resolveWaterArrayUniformNode(u.uWavesB);
|
|
170
|
+
const waves = createWaterWavesChunk({ wavesA: wavesANode, wavesB: wavesBNode, waveCount });
|
|
171
|
+
const foam = createWaterFoamChunk({ u, foamOctaves });
|
|
172
|
+
const lighting = createWaterLightingChunk({ u });
|
|
173
|
+
|
|
174
|
+
const material = new NodeMaterial();
|
|
175
|
+
material.name = 'WaterBreakers';
|
|
176
|
+
material.lights = false;
|
|
177
|
+
// TODO(tsl Phase 7): fog ordering (see module header).
|
|
178
|
+
material.fog = false;
|
|
179
|
+
material.transparent = true;
|
|
180
|
+
material.depthWrite = true;
|
|
181
|
+
material.side = THREE.DoubleSide;
|
|
182
|
+
|
|
183
|
+
// ---- Varyings ----
|
|
184
|
+
const vWorldPosition = varying(vec3(), 'vBreakerWorldPosition');
|
|
185
|
+
const vNormal = varying(vec3(), 'vBreakerNormal');
|
|
186
|
+
const vRestXZ = varying(vec2(), 'vBreakerRestXZ');
|
|
187
|
+
const vSteep = varying(float(), 'vBreakerSteep');
|
|
188
|
+
const vChop = varying(float(), 'vBreakerChop');
|
|
189
|
+
const vProfile = varying(float(), 'vBreakerProfile');
|
|
190
|
+
const vAlong = varying(float(), 'vBreakerAlong');
|
|
191
|
+
const vPulse = varying(float(), 'vBreakerPulse');
|
|
192
|
+
const vPost = varying(float(), 'vBreakerPost');
|
|
193
|
+
const vCurl = varying(float(), 'vBreakerCurl');
|
|
194
|
+
const vFade = varying(float(), 'vBreakerFade');
|
|
195
|
+
const vFaceHeight = varying(float(), 'vBreakerFaceHeight');
|
|
196
|
+
|
|
197
|
+
// ---- Vertex stage (waterBreaker.vert.glsl) ----
|
|
198
|
+
material.vertexNode = Fn(() => {
|
|
199
|
+
const aDir = attribute('aDir', 'vec2');
|
|
200
|
+
const aInfo = attribute('aInfo', 'vec4');
|
|
201
|
+
const aSlope = attribute('aSlope', 'float');
|
|
202
|
+
|
|
203
|
+
const baseWorld = modelWorldMatrix.mul(vec4(positionGeometry, 1.0)).toVar();
|
|
204
|
+
const along = aInfo.x.toVar();
|
|
205
|
+
const t = aInfo.y.toVar();
|
|
206
|
+
// Depth at the break line, where the collapse criterion is met.
|
|
207
|
+
const lineDepth = max(aInfo.z, 0.05).toVar();
|
|
208
|
+
const endFade = aInfo.w.toVar();
|
|
209
|
+
|
|
210
|
+
const up = vec3(0.0, 1.0, 0.0);
|
|
211
|
+
const dir = normalize(vec3(aDir.x, 1e-5, aDir.y).mul(vec3(1.0, 0.0, 1.0))).toVar();
|
|
212
|
+
|
|
213
|
+
// --- Ride the dominant crest -------------------------------------------
|
|
214
|
+
const wave0A = wavesANode.element(0).toVar();
|
|
215
|
+
const wave0B = wavesBNode.element(0).toVar();
|
|
216
|
+
const k0 = max(wave0A.w, 1e-4).toVar();
|
|
217
|
+
const waveLen = float(PI2).div(k0).toVar();
|
|
218
|
+
const theta = k0.mul(dot(wave0A.xy, baseWorld.xz)).sub(wave0A.z.mul(u.uTime))
|
|
219
|
+
.add(wave0B.y).sub(u.uBreakerPeel.mul(0.4).mul(k0).mul(along)).toVar();
|
|
220
|
+
|
|
221
|
+
// delta: signed progress (in wave cycles, -0.5..0.5) of the nearest
|
|
222
|
+
// dominant crest relative to the break line.
|
|
223
|
+
const delta = float(PI * 0.5).sub(theta).div(PI2).add(0.5).fract().sub(0.5).toVar();
|
|
224
|
+
const facing = clamp(dot(wave0A.xy, dir.xz), 0.3, 1.0).toVar();
|
|
225
|
+
const ride = delta.mul(waveLen).div(facing).toVar();
|
|
226
|
+
const crestBase = baseWorld.xyz.add(dir.mul(ride)).toVar();
|
|
227
|
+
// Water depth under the ridden crest: deeper offshore, shoaling inshore.
|
|
228
|
+
const localDepth = clamp(lineDepth.sub(aSlope.mul(ride)), 0.05, lineDepth.mul(3.0)).toVar();
|
|
229
|
+
|
|
230
|
+
// Set envelope: interference of the beat pair in slots 0/1.
|
|
231
|
+
const wave1A = wavesANode.element(1).toVar();
|
|
232
|
+
const wave1B = wavesBNode.element(1).toVar();
|
|
233
|
+
const thetaBeat = wave1A.w.mul(dot(wave1A.xy, crestBase.xz))
|
|
234
|
+
.sub(wave1A.z.mul(u.uTime)).add(wave1B.y).toVar();
|
|
235
|
+
const pairA = wave0B.x.toVar();
|
|
236
|
+
const pairB = wave1B.x.toVar();
|
|
237
|
+
const interference = sqrt(max(
|
|
238
|
+
pairA.mul(pairA).add(pairB.mul(pairB))
|
|
239
|
+
.add(pairA.mul(pairB).mul(2.0).mul(cos(float(PI * 0.5).sub(thetaBeat)))),
|
|
240
|
+
0.0,
|
|
241
|
+
)).div(max(pairA.add(pairB), 1e-4)).toVar();
|
|
242
|
+
const setEnvelope = mix(1.0, interference, u.uSetPair).toVar();
|
|
243
|
+
const setGate = smoothstep(0.5, 0.78, setEnvelope).toVar();
|
|
244
|
+
|
|
245
|
+
// --- Lifecycle, keyed to WHERE the ridden crest is ----------------------
|
|
246
|
+
const approach = smoothstep(-0.3, -0.03, delta);
|
|
247
|
+
const pulse = approach.mul(smoothstep(0.03, 0.3, delta).oneMinus()).mul(setGate).toVar();
|
|
248
|
+
const curl = u.uBreakerCurl.mul(endFade).mul(setGate)
|
|
249
|
+
.mul(smoothstep(-0.14, -0.02, delta))
|
|
250
|
+
.mul(smoothstep(0.05, 0.16, delta).oneMinus()).toVar();
|
|
251
|
+
const post = smoothstep(0.02, 0.12, delta)
|
|
252
|
+
.mul(smoothstep(0.28, 0.46, delta).oneMinus()).mul(setGate).toVar();
|
|
253
|
+
// Hide the wrap seam where the shell jumps back out to the next crest.
|
|
254
|
+
const travelFade = smoothstep(-0.48, -0.4, delta)
|
|
255
|
+
.mul(smoothstep(0.38, 0.47, delta).oneMinus()).toVar();
|
|
256
|
+
|
|
257
|
+
// --- Shell shaping ----------------------------------------------------
|
|
258
|
+
const amount = clamp(u.uBreakerAmount, 0.0, 1.0).toVar();
|
|
259
|
+
const capDepth = min(lineDepth, localDepth);
|
|
260
|
+
const breakHeight = capDepth.mul(0.9).mul(u.uBreakerScale).mul(amount).mul(endFade).mul(setEnvelope).toVar();
|
|
261
|
+
const H = breakHeight.mul(mix(0.12, 1.0, pulse)).mul(post.mul(0.55).oneMinus()).toVar();
|
|
262
|
+
const R = H.mul(0.52).toVar();
|
|
263
|
+
|
|
264
|
+
// The crest leans shoreward as it steepens and the bore noses forward.
|
|
265
|
+
const crestTop = crestBase.add(up.mul(H)).add(dir.mul(H.mul(pulse.mul(0.3).add(post.mul(0.45))))).toVar();
|
|
266
|
+
const barrelCenter = crestTop.sub(up.mul(R)).toVar();
|
|
267
|
+
const backFoot = crestBase.sub(dir.mul(H.mul(2.1).add(0.35))).toVar();
|
|
268
|
+
|
|
269
|
+
// Shell profile as a pure function of t so the shading normal can come
|
|
270
|
+
// from an exact profile tangent (screen-space derivatives facet the shell).
|
|
271
|
+
const shellPoint = (tParam) => {
|
|
272
|
+
const backT = clamp(tParam.div(0.42), 0.0, 1.0);
|
|
273
|
+
const backEase = backT.mul(backT).mul(backT.mul(-2.0).add(3.0));
|
|
274
|
+
const facePos = mix(backFoot, crestTop, backEase);
|
|
275
|
+
const spiralT = clamp(tParam.sub(0.42).div(0.58), 0.0, 1.0);
|
|
276
|
+
const ang = spiralT.mul(PI).mul(curl.mul(1.18).add(0.5));
|
|
277
|
+
const rad = R.mul(spiralT.mul(0.4).mul(curl).oneMinus());
|
|
278
|
+
const spiralPos = barrelCenter
|
|
279
|
+
.add(up.mul(rad.mul(cos(ang))))
|
|
280
|
+
.add(dir.mul(rad.mul(sin(ang))))
|
|
281
|
+
.add(dir.mul(spiralT.mul(H).mul(0.22).mul(pulse)));
|
|
282
|
+
return mix(facePos, spiralPos, step(0.42, tParam));
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const shellPos = shellPoint(t).toVar();
|
|
286
|
+
|
|
287
|
+
// Smooth analytic normal: profile tangent x along-crest direction.
|
|
288
|
+
const profileTangent = shellPoint(min(t.add(0.02), 1.0)).sub(shellPoint(max(t.sub(0.02), 0.0))).toVar();
|
|
289
|
+
const side = normalize(cross(up, dir));
|
|
290
|
+
const shellNormal = cross(profileTangent, side).toVar();
|
|
291
|
+
vNormal.assign(select(length(shellNormal).greaterThan(1e-6), normalize(shellNormal), up));
|
|
292
|
+
vSteep.assign(smoothstep(0.15, 0.55, pulse.add(post)).mul(smoothstep(0.0, 0.22, t)));
|
|
293
|
+
|
|
294
|
+
// --- Blend into the heightfield ----------------------------------------
|
|
295
|
+
// Same shoaling/cap displacement the water surface applies at the ridden
|
|
296
|
+
// position — including its shallow-water chop filter.
|
|
297
|
+
const range = max(max(u.uShoalingDepth, u.uWaveEnergy.mul(2.2)), 1e-3).toVar();
|
|
298
|
+
const chopWeight = mix(0.15, 1.0, smoothstep(range.mul(0.3), range.mul(1.4), localDepth)).toVar();
|
|
299
|
+
const raw = waves.gerstnerDisplacementFiltered(crestBase.xz, u.uTime, chopWeight).y.toVar();
|
|
300
|
+
const deepFactor = smoothstep(0.0, range, localDepth);
|
|
301
|
+
const rearUp = smoothstep(range.mul(0.45), range.mul(1.5), localDepth).oneMinus()
|
|
302
|
+
.mul(smoothstep(0.05, 0.35, localDepth));
|
|
303
|
+
const shoal = mix(u.uShorelineWaves, 1.0, deepFactor).mul(rearUp.mul(0.3).add(1.0)).toVar();
|
|
304
|
+
const skirtTarget = raw.mul(shoal).toVar();
|
|
305
|
+
const skirtCap = localDepth.mul(0.72).toVar();
|
|
306
|
+
const surfaceY = baseWorld.y.add(max(
|
|
307
|
+
min(skirtTarget, skirtCap).add(max(skirtTarget.sub(skirtCap), 0.0).mul(0.1)),
|
|
308
|
+
localDepth.negate().add(0.04),
|
|
309
|
+
)).toVar();
|
|
310
|
+
|
|
311
|
+
// At full pulse the shell IS the crest, so it anchors at the rest level;
|
|
312
|
+
// while still swelling it rides on top of the rendered crest instead.
|
|
313
|
+
const skirt = smoothstep(0.0, 0.3, t).oneMinus().toVar();
|
|
314
|
+
shellPos.y.addAssign(surfaceY.sub(baseWorld.y).mul(pulse.mul(0.85).oneMinus()).mul(skirt.oneMinus()));
|
|
315
|
+
shellPos.y.assign(mix(shellPos.y, surfaceY, skirt));
|
|
316
|
+
|
|
317
|
+
vProfile.assign(t);
|
|
318
|
+
vAlong.assign(along);
|
|
319
|
+
vPulse.assign(pulse);
|
|
320
|
+
vPost.assign(post);
|
|
321
|
+
vCurl.assign(curl);
|
|
322
|
+
// Appear only once the face has genuinely developed.
|
|
323
|
+
vFade.assign(amount.mul(endFade).mul(travelFade).mul(setGate)
|
|
324
|
+
.mul(smoothstep(0.12, 0.35, pulse.add(post)))
|
|
325
|
+
.mul(smoothstep(0.05, 0.15, H)));
|
|
326
|
+
vFaceHeight.assign(clamp(shellPos.y.sub(surfaceY).div(max(breakHeight, 1e-3)).add(0.15), 0.0, 1.0));
|
|
327
|
+
vWorldPosition.assign(shellPos);
|
|
328
|
+
vRestXZ.assign(crestBase.xz);
|
|
329
|
+
vChop.assign(chopWeight);
|
|
330
|
+
|
|
331
|
+
return cameraProjectionMatrix.mul(cameraViewMatrix).mul(vec4(shellPos, 1.0));
|
|
332
|
+
})();
|
|
333
|
+
|
|
334
|
+
// ---- Fragment stage (waterBreaker.frag.glsl) ----
|
|
335
|
+
material.fragmentNode = Fn(() => {
|
|
336
|
+
Discard(vFade.lessThan(0.01));
|
|
337
|
+
|
|
338
|
+
const toCamera = cameraPosition.sub(vWorldPosition).toVar();
|
|
339
|
+
const viewDir = normalize(toCamera).toVar();
|
|
340
|
+
const viewDistance = length(toCamera).toVar();
|
|
341
|
+
|
|
342
|
+
// Cross-fade from the sea's own per-pixel Gerstner normal to the smooth
|
|
343
|
+
// analytic shell normal by vSteep, so there is no lighting seam.
|
|
344
|
+
const sea = waves.gerstnerNormalFiltered(vRestXZ, u.uTime, vChop);
|
|
345
|
+
const seaNormal = vec3(sea.normal).toVar();
|
|
346
|
+
const crest = float(sea.crest).toVar();
|
|
347
|
+
const normal = normalize(mix(seaNormal, normalize(vNormal), clamp(vSteep, 0.0, 1.0))).toVar();
|
|
348
|
+
// Flip camera-ward so the tunnel interior lights too (DoubleSide mesh).
|
|
349
|
+
If(dot(normal, viewDir).lessThan(0.0), () => {
|
|
350
|
+
normal.assign(normal.negate());
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// Same fbm micro-ripple detail as the open surface, same flow drift.
|
|
354
|
+
const detailFade = smoothstep(16.0, 60.0, viewDistance).oneMinus();
|
|
355
|
+
const detailStrength = u.uDetailNormalStrength.mul(mix(0.2, 1.0, detailFade)).toVar();
|
|
356
|
+
const flowOffset = u.uFlowDirection.mul(u.uTime).mul(u.uFlowSpeed);
|
|
357
|
+
const detailUv = vWorldPosition.xz.mul(u.uDetailScale).add(flowOffset.mul(0.55)).toVar();
|
|
358
|
+
const detailCenter = waterFbm(detailUv, 3).toVar();
|
|
359
|
+
const detailGradient = vec2(
|
|
360
|
+
waterFbm(detailUv.add(vec2(0.11, 0.0)), 3).sub(detailCenter),
|
|
361
|
+
waterFbm(detailUv.add(vec2(0.0, 0.11)), 3).sub(detailCenter),
|
|
362
|
+
).div(0.11).mul(detailStrength).toVar();
|
|
363
|
+
const surfaceNormal = waterCombineNormal(normal, detailGradient).toVar();
|
|
364
|
+
|
|
365
|
+
// Body: a breaking face is a thick massed column of water — it reads
|
|
366
|
+
// DARKER than the flat sea, not lighter.
|
|
367
|
+
const faceH = clamp(vFaceHeight, 0.0, 1.0).toVar();
|
|
368
|
+
const bodyColor = mix(u.uDeepColor, u.uMidColor, smoothstep(0.1, 0.9, faceH).mul(0.65)).toVar();
|
|
369
|
+
bodyColor.mulAssign(mix(1.0, 0.78, vSteep));
|
|
370
|
+
// Sun-through tint only when the camera actually looks at the lip against
|
|
371
|
+
// the sun; squared so a side-lit lip stays in the water palette.
|
|
372
|
+
const lip = smoothstep(0.72, 0.95, vProfile).toVar();
|
|
373
|
+
const backlit = clamp(dot(viewDir.negate(), u.uSunDirection).mul(0.5).add(0.5), 0.0, 1.0).toVar();
|
|
374
|
+
bodyColor.assign(mix(bodyColor, u.uShallowColor, lip.mul(vCurl).mul(backlit).mul(backlit).mul(0.3)));
|
|
375
|
+
|
|
376
|
+
// Identical reflection/fresnel treatment to the open water.
|
|
377
|
+
const fresnel = lighting.fresnelFactor(viewDir, surfaceNormal).toVar();
|
|
378
|
+
const reflection = lighting.reflectionColor(vWorldPosition, surfaceNormal, viewDir).toVar();
|
|
379
|
+
const reflectionMix = clamp(fresnel.mul(u.uReflectionStrength), 0.0, 0.92);
|
|
380
|
+
const color = mix(bodyColor, reflection, reflectionMix).toVar();
|
|
381
|
+
color.addAssign(u.uFresnelColor.mul(fresnel).mul(fresnel).mul(0.35));
|
|
382
|
+
|
|
383
|
+
// Foam layers: tumbling lip edge, face streaks, whitewater, apron, plus
|
|
384
|
+
// the sea's own whitecaps while the shell still lies flat.
|
|
385
|
+
const noiseScale = max(u.uFoamNoiseScale, 0.05).toVar();
|
|
386
|
+
const faceNoise = waterFbm(
|
|
387
|
+
vec2(vAlong.mul(1.7), vProfile.mul(4.0).sub(u.uTime.mul(1.6))).mul(noiseScale), 3,
|
|
388
|
+
).toVar();
|
|
389
|
+
const lipFoam = smoothstep(faceNoise.mul(-0.12).add(0.68), 0.9, vProfile.add(faceNoise.mul(0.08)))
|
|
390
|
+
.mul(vPulse).mul(1.4).toVar();
|
|
391
|
+
const streaks = smoothstep(0.62, 0.85, faceNoise)
|
|
392
|
+
.mul(smoothstep(0.3, 0.7, vProfile)).mul(vPulse).mul(0.7);
|
|
393
|
+
const whitewater = vPost.mul(
|
|
394
|
+
waterFbm(vec2(vAlong.mul(2.3), vProfile.mul(3.0).add(u.uTime.mul(2.2))), 3).mul(0.6).add(0.7),
|
|
395
|
+
);
|
|
396
|
+
const apron = smoothstep(0.88, 0.98, vProfile).mul(faceNoise.mul(0.5).add(0.5)).mul(vPulse);
|
|
397
|
+
const ambientCaps = foam.whitecaps(crest, seaNormal.y, vRestXZ, u.uTime).mul(vSteep.oneMinus());
|
|
398
|
+
const foamRaw = clamp(lipFoam.add(streaks).add(whitewater).add(apron).add(ambientCaps), 0.0, 1.0);
|
|
399
|
+
const foamValue = foam.foamShape(foamRaw, vWorldPosition.xz, u.uTime).toVar();
|
|
400
|
+
|
|
401
|
+
// Same toon glints and sparkles as the open water.
|
|
402
|
+
color.addAssign(lighting.specular(viewDir, surfaceNormal, foamValue.mul(0.75).oneMinus()));
|
|
403
|
+
color.addAssign(
|
|
404
|
+
lighting.sparkles(vWorldPosition.xz, surfaceNormal, viewDir, viewDistance, u.uTime)
|
|
405
|
+
.mul(foamValue.oneMinus()),
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
// Foam overlay, sun-shaded two-tone like the surface foam.
|
|
409
|
+
const foamLight = clamp(dot(surfaceNormal, u.uSunDirection), 0.0, 1.0).mul(0.18).add(0.82);
|
|
410
|
+
color.assign(mix(color, u.uFoamColor.mul(foamLight), foamValue));
|
|
411
|
+
|
|
412
|
+
// Solid water, not glass: only the trailing skirt dissolves into the
|
|
413
|
+
// heightfield it sits on.
|
|
414
|
+
const alpha = vFade.mul(mix(0.97, 1.0, foamValue)).mul(smoothstep(0.02, 0.16, vProfile));
|
|
415
|
+
|
|
416
|
+
return vec4(color, alpha);
|
|
417
|
+
})();
|
|
418
|
+
|
|
419
|
+
material.uniforms = u;
|
|
420
|
+
material.defines = { WATER_WAVE_COUNT: waveCount, WATER_FOAM_OCTAVES: foamOctaves };
|
|
421
|
+
material.userData.isToonNodeMaterial = true;
|
|
422
|
+
return material;
|
|
423
|
+
}
|