incanto 0.20.0 → 0.22.0
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/dist/2d.js +3 -3
- package/dist/3d.d.ts +68 -0
- package/dist/3d.js +4 -3
- package/dist/{create-game-Ct86zczq.js → create-game-B-M1zune.js} +3 -3
- package/dist/{create-game-pNf3sI-Q.js → create-game-C3uiw80V.js} +6 -3
- package/dist/{duplicate-BINwP0WG.js → duplicate-BgtFrFo4.js} +15 -1
- package/dist/gameplay-Dib5mNFD.js +5425 -0
- package/dist/gameplay.js +1 -1
- package/dist/index.js +3 -3
- package/dist/{particle-sim-CyUU7HVU.js → particle-sim-Bw7hB93B.js} +1 -15
- package/dist/{physics-2d-BiIdl51r.js → physics-2d-DiVFFlH3.js} +1 -1
- package/dist/{physics-3d-DxFDfUey.js → physics-3d-DLPPRNu4.js} +2 -1
- package/dist/react.js +1 -1
- package/dist/{register-BWFgsGra.js → register-CHgVdSbp.js} +150 -3143
- package/dist/{register-Djwckzgx.js → register-CvpSUU3O.js} +2 -1
- package/dist/test.js +7 -7
- package/editor/assets/{agent8-BZX7dF4f.js → agent8-4yMWrtSC.js} +1 -1
- package/editor/assets/{index-CXdpxO_B.js → index-DAzNRDSQ.js} +2988 -2822
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/schemas/scene.schema.json +8 -0
- package/skills/incanto-environment.md +56 -0
- package/skills/incanto-gameplay-behaviors.md +36 -1
- package/skills/incanto-node-reference.md +17 -0
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/village-quest-3d/package.json +1 -1
- package/dist/gameplay-sSqrUcnz.js +0 -1978
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { f as Node } from "./loader-BZqOKfI2.js";
|
|
2
1
|
import { t as registerCoreNodes } from "./register-nObreUQR.js";
|
|
3
2
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
4
3
|
import { t as Rng } from "./rng-DP-SR7eg.js";
|
|
5
4
|
import { i as getNodeSchema, l as registerNode } from "./registry-BVJ2HbCn.js";
|
|
6
5
|
import { t as createNoise2D } from "./noise-CGUMx44x.js";
|
|
7
|
-
import { i as applyParticlePreset,
|
|
6
|
+
import { i as applyParticlePreset, r as PARTICLE_PRESET_NAMES, t as ParticleSim } from "./particle-sim-Bw7hB93B.js";
|
|
7
|
+
import { A as Water3D, B as colliderFootDrop, F as StaticBody3D, I as Node3D, M as CharacterBody3D, N as PhysicsBody3D, P as RigidBody3D, j as Area3D } from "./gameplay-Dib5mNFD.js";
|
|
8
8
|
import { n as splatWeights, t as buildHeightmap } from "./heightmap-CRK0M4jT.js";
|
|
9
|
-
import { AdditiveBlending, AnimationClip, AnimationMixer, Box3, BoxGeometry, BufferAttribute, BufferGeometry, CanvasTexture, CapsuleGeometry, Color, ConeGeometry,
|
|
9
|
+
import { AdditiveBlending, AnimationClip, AnimationMixer, Box3, BoxGeometry, BufferAttribute, BufferGeometry, CanvasTexture, CapsuleGeometry, Color, ConeGeometry, CylinderGeometry, DataTexture, DirectionalLight, DoubleSide, DynamicDrawUsage, Euler, Group, IcosahedronGeometry, ImageBitmapLoader, InstancedBufferAttribute, InstancedMesh, LinearFilter, LoopOnce, LoopRepeat, Matrix4, Mesh, MeshBasicMaterial, MeshDepthMaterial, MeshPhysicalMaterial, MeshStandardMaterial, NearestFilter, NoBlending, NormalBlending, PerspectiveCamera, PlaneGeometry, PointLight, Points, PointsMaterial, Quaternion, QuaternionKeyframeTrack, RGBADepthPacking, RGBAFormat, RepeatWrapping, SRGBColorSpace, ShaderChunk, ShaderMaterial, SphereGeometry, Texture, TextureLoader, UniformsLib, UniformsUtils, Vector3, Vector4, VectorKeyframeTrack } from "three";
|
|
10
10
|
import { clone } from "three/addons/utils/SkeletonUtils.js";
|
|
11
11
|
//#region src/3d/camera-rig.ts
|
|
12
12
|
const QUARTER_PITCH = Math.atan(1 / Math.SQRT2);
|
|
@@ -151,269 +151,6 @@ function movementState(grounded, intensity) {
|
|
|
151
151
|
return "fastRun";
|
|
152
152
|
}
|
|
153
153
|
//#endregion
|
|
154
|
-
//#region src/3d/physics/collider-validate.ts
|
|
155
|
-
/**
|
|
156
|
-
* Hard-validate a 3D collider prop at LOAD time (see the 2D twin for rationale).
|
|
157
|
-
* An EMPTY collider is allowed — a body without a collider is work in progress.
|
|
158
|
-
*/
|
|
159
|
-
function validateCollider3D(collider, nodeName) {
|
|
160
|
-
const shape = collider.shape;
|
|
161
|
-
if (shape === void 0) return;
|
|
162
|
-
if (shape === "box") {
|
|
163
|
-
const size = collider.size;
|
|
164
|
-
if (!Array.isArray(size) || size.length !== 3 || size.some((v) => typeof v !== "number")) throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': box needs "size": [x, y, z].`);
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
if (shape === "sphere") {
|
|
168
|
-
if (typeof collider.radius !== "number") throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': sphere needs "radius".`);
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
if (shape === "capsule") {
|
|
172
|
-
if (typeof collider.radius !== "number" || typeof collider.height !== "number") throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': capsule needs "radius" and "height".`);
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
if (shape === "trimesh") {
|
|
176
|
-
const vertices = collider.vertices;
|
|
177
|
-
const indices = collider.indices;
|
|
178
|
-
if (!Array.isArray(vertices) || vertices.length % 3 !== 0 || !Array.isArray(indices) || indices.length % 3 !== 0) throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': trimesh needs "vertices" (flat xyz floats) and "indices" (triangle triplets).`);
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
if (shape === "heightfield") return;
|
|
182
|
-
throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': "shape" must be 'box', 'sphere', 'capsule', 'trimesh', or 'heightfield', got ${JSON.stringify(shape)}.`);
|
|
183
|
-
}
|
|
184
|
-
//#endregion
|
|
185
|
-
//#region src/3d/nodes/node-3d.ts
|
|
186
|
-
/**
|
|
187
|
-
* Base of every 3D node: a JSON-prop transform backed by a `THREE.Object3D`.
|
|
188
|
-
*
|
|
189
|
-
* Props stay plain JSON (registry/serializer compatible); the renderer layer
|
|
190
|
-
* pushes them onto the backing object once per frame (`_syncObject3D`).
|
|
191
|
-
* Rotation is in DEGREES in JSON — friendlier for agents and humans.
|
|
192
|
-
*/
|
|
193
|
-
var Node3D = class extends Node {
|
|
194
|
-
static typeName = "Node3D";
|
|
195
|
-
static props = {
|
|
196
|
-
position: { default: [
|
|
197
|
-
0,
|
|
198
|
-
0,
|
|
199
|
-
0
|
|
200
|
-
] },
|
|
201
|
-
rotation: { default: [
|
|
202
|
-
0,
|
|
203
|
-
0,
|
|
204
|
-
0
|
|
205
|
-
] },
|
|
206
|
-
static: { default: false },
|
|
207
|
-
scale: { default: [
|
|
208
|
-
1,
|
|
209
|
-
1,
|
|
210
|
-
1
|
|
211
|
-
] },
|
|
212
|
-
visible: { default: true },
|
|
213
|
-
renderOrder: { default: 0 },
|
|
214
|
-
orderGroup: {
|
|
215
|
-
default: "default",
|
|
216
|
-
options: [
|
|
217
|
-
"background",
|
|
218
|
-
"terrain",
|
|
219
|
-
"default",
|
|
220
|
-
"characters",
|
|
221
|
-
"effects",
|
|
222
|
-
"overlay"
|
|
223
|
-
]
|
|
224
|
-
}
|
|
225
|
-
};
|
|
226
|
-
position = [
|
|
227
|
-
0,
|
|
228
|
-
0,
|
|
229
|
-
0
|
|
230
|
-
];
|
|
231
|
-
/** Euler XYZ in degrees. */
|
|
232
|
-
rotation = [
|
|
233
|
-
0,
|
|
234
|
-
0,
|
|
235
|
-
0
|
|
236
|
-
];
|
|
237
|
-
/**
|
|
238
|
-
* This subtree never changes after load: the renderer syncs it ONCE and
|
|
239
|
-
* skips it every frame after (transforms, materials, animations frozen).
|
|
240
|
-
* The per-frame walk is O(nodes) — marking terrain/buildings/decor static
|
|
241
|
-
* removes most of a big scene from it. Set `static = false` to resume
|
|
242
|
-
* live syncing. Do NOT mark animated nodes (Water3D, Particles3D,
|
|
243
|
-
* AnimatedSprite3D, ModelInstance3D with clips) or cameras static.
|
|
244
|
-
*/
|
|
245
|
-
static = false;
|
|
246
|
-
scale = [
|
|
247
|
-
1,
|
|
248
|
-
1,
|
|
249
|
-
1
|
|
250
|
-
];
|
|
251
|
-
visible = true;
|
|
252
|
-
/** Draw-order priority (higher = drawn later / on top). Only affects TRANSPARENT
|
|
253
|
-
* materials — three sorts the transparent pass by renderOrder, then depth.
|
|
254
|
-
* Pairs with named scene constants (e.g. `{"@const": "UI"}`) for sorting tiers. */
|
|
255
|
-
renderOrder = 0;
|
|
256
|
-
/** Named draw-order band; `renderOrder` is the fine offset inside it. */
|
|
257
|
-
orderGroup = "default";
|
|
258
|
-
/** What three actually sorts by: band base + renderOrder. */
|
|
259
|
-
get effectiveRenderOrder() {
|
|
260
|
-
return effectiveOrder(this.orderGroup, this.renderOrder);
|
|
261
|
-
}
|
|
262
|
-
_object3D = null;
|
|
263
|
-
/**
|
|
264
|
-
* @internal Fixed-step render interpolation. A physics body fills these with
|
|
265
|
-
* its previous and current fixed-step LOCAL positions; `_syncObject3D(alpha)`
|
|
266
|
-
* lerps the rendered transform between them so motion is smooth between fixed
|
|
267
|
-
* steps. `null` means "not physics-driven" — render `position` directly.
|
|
268
|
-
*/
|
|
269
|
-
_interpPrev = null;
|
|
270
|
-
_interpCurr = null;
|
|
271
|
-
/** @internal The backing three object (lazily created). */
|
|
272
|
-
_ensureObject3D() {
|
|
273
|
-
if (!this._object3D) {
|
|
274
|
-
this._object3D = this._createObject3D();
|
|
275
|
-
this._object3D.userData.incantoNode = this;
|
|
276
|
-
}
|
|
277
|
-
return this._object3D;
|
|
278
|
-
}
|
|
279
|
-
/** @internal Override point for subclasses (mesh, camera, light…). */
|
|
280
|
-
_createObject3D() {
|
|
281
|
-
return new Object3D();
|
|
282
|
-
}
|
|
283
|
-
/** @internal Push JSON props onto the backing object. Called every frame.
|
|
284
|
-
* `alpha` (0..1) interpolates a physics body between its last two fixed-step
|
|
285
|
-
* positions; non-physics nodes ignore it and render `position` directly. */
|
|
286
|
-
_syncObject3D(alpha = 1) {
|
|
287
|
-
const o = this._ensureObject3D();
|
|
288
|
-
const prev = this._interpPrev;
|
|
289
|
-
const curr = this._interpCurr;
|
|
290
|
-
if (prev && curr) o.position.set((prev[0] ?? 0) + ((curr[0] ?? 0) - (prev[0] ?? 0)) * alpha, (prev[1] ?? 0) + ((curr[1] ?? 0) - (prev[1] ?? 0)) * alpha, (prev[2] ?? 0) + ((curr[2] ?? 0) - (prev[2] ?? 0)) * alpha);
|
|
291
|
-
else o.position.set(this.position[0] ?? 0, this.position[1] ?? 0, this.position[2] ?? 0);
|
|
292
|
-
o.rotation.set(MathUtils.degToRad(this.rotation[0] ?? 0), MathUtils.degToRad(this.rotation[1] ?? 0), MathUtils.degToRad(this.rotation[2] ?? 0));
|
|
293
|
-
o.scale.set(this.scale[0] ?? 1, this.scale[1] ?? 1, this.scale[2] ?? 1);
|
|
294
|
-
o.visible = this.visible;
|
|
295
|
-
o.renderOrder = this.effectiveRenderOrder;
|
|
296
|
-
}
|
|
297
|
-
free() {
|
|
298
|
-
super.free();
|
|
299
|
-
this._object3D?.removeFromParent();
|
|
300
|
-
this._object3D = null;
|
|
301
|
-
this._interpPrev = null;
|
|
302
|
-
this._interpCurr = null;
|
|
303
|
-
}
|
|
304
|
-
};
|
|
305
|
-
//#endregion
|
|
306
|
-
//#region src/3d/nodes/bodies-3d.ts
|
|
307
|
-
/**
|
|
308
|
-
* Shared base for physics-backed 3D nodes. Colliders are NODE PROPS:
|
|
309
|
-
* `{shape:'box', size:[x,y,z]}` · `{shape:'sphere', radius}` ·
|
|
310
|
-
* `{shape:'capsule', radius, height}` (meters, y-up).
|
|
311
|
-
*/
|
|
312
|
-
var PhysicsBody3D = class extends Node3D {
|
|
313
|
-
static props = { collider: { default: {} } };
|
|
314
|
-
/** The unified collision model: every collider participant can emit these. */
|
|
315
|
-
static signals = ["triggerEnter", "triggerExit"];
|
|
316
|
-
_collider = {};
|
|
317
|
-
/** @internal Physics resync flag — raised when `collider` is REPLACED. */
|
|
318
|
-
_colliderChanged = false;
|
|
319
|
-
/**
|
|
320
|
-
* Collider shape prop. Change it by REPLACING the object
|
|
321
|
-
* (`body.collider = { shape: 'box', size: [...] }`) — physics rebuilds the
|
|
322
|
-
* body on replacement; in-place mutation of the old object is not watched.
|
|
323
|
-
*/
|
|
324
|
-
get collider() {
|
|
325
|
-
return this._collider;
|
|
326
|
-
}
|
|
327
|
-
set collider(value) {
|
|
328
|
-
this._collider = value;
|
|
329
|
-
this._colliderChanged = true;
|
|
330
|
-
}
|
|
331
|
-
/** Loader hook: bad collider shapes fail at LOAD, not at physics start. */
|
|
332
|
-
static validateJson(node) {
|
|
333
|
-
const body = node;
|
|
334
|
-
validateCollider3D(body.collider, node.name);
|
|
335
|
-
if (body.collider.shape === "heightfield" && !(body instanceof StaticBody3D)) throw new IncantoError("BAD_FORMAT", `Collider on '${node.name}': heightfield is static terrain — put it on a StaticBody3D (with a Terrain3D child), not a ${body.constructor.typeName}.`, { prop: "collider" });
|
|
336
|
-
}
|
|
337
|
-
/** @internal Set by Physics3D when the body is created. */
|
|
338
|
-
_physics = null;
|
|
339
|
-
};
|
|
340
|
-
/** Immovable collider (ground, walls, platforms). */
|
|
341
|
-
var StaticBody3D = class extends PhysicsBody3D {
|
|
342
|
-
static typeName = "StaticBody3D";
|
|
343
|
-
};
|
|
344
|
-
/** Sensor volume emitting `triggerEnter(other)` / `triggerExit(other)`. */
|
|
345
|
-
var Area3D = class extends PhysicsBody3D {
|
|
346
|
-
static typeName = "Area3D";
|
|
347
|
-
};
|
|
348
|
-
/** Dynamic simulated body. */
|
|
349
|
-
var RigidBody3D = class extends PhysicsBody3D {
|
|
350
|
-
static typeName = "RigidBody3D";
|
|
351
|
-
static props = {
|
|
352
|
-
mass: { default: 1 },
|
|
353
|
-
gravityScale: { default: 1 },
|
|
354
|
-
fixedRotation: { default: false },
|
|
355
|
-
friction: { default: .5 },
|
|
356
|
-
restitution: { default: 0 },
|
|
357
|
-
linearVelocity: { default: [
|
|
358
|
-
0,
|
|
359
|
-
0,
|
|
360
|
-
0
|
|
361
|
-
] }
|
|
362
|
-
};
|
|
363
|
-
mass = 1;
|
|
364
|
-
gravityScale = 1;
|
|
365
|
-
fixedRotation = false;
|
|
366
|
-
friction = .5;
|
|
367
|
-
restitution = 0;
|
|
368
|
-
/** m/s, y-up. Read back every step; write to launch. */
|
|
369
|
-
linearVelocity = [
|
|
370
|
-
0,
|
|
371
|
-
0,
|
|
372
|
-
0
|
|
373
|
-
];
|
|
374
|
-
/** @internal set by Physics3D */
|
|
375
|
-
_physics3d = null;
|
|
376
|
-
/** World-space impulse (kg·m/s) — the floating-capsule controller's verb. */
|
|
377
|
-
applyImpulse(impulse) {
|
|
378
|
-
this._physics3d?.applyImpulse(this, impulse);
|
|
379
|
-
}
|
|
380
|
-
};
|
|
381
|
-
/**
|
|
382
|
-
* Kinematic capsule on Rapier's character controller. Gravity is NOT applied
|
|
383
|
-
* automatically — integrate `velocity` yourself, then `moveAndSlide()` from
|
|
384
|
-
* `fixedUpdate`.
|
|
385
|
-
*/
|
|
386
|
-
var CharacterBody3D = class extends PhysicsBody3D {
|
|
387
|
-
static typeName = "CharacterBody3D";
|
|
388
|
-
static props = {
|
|
389
|
-
velocity: { default: [
|
|
390
|
-
0,
|
|
391
|
-
0,
|
|
392
|
-
0
|
|
393
|
-
] },
|
|
394
|
-
snapToGround: { default: true },
|
|
395
|
-
slopeLimitDeg: { default: 45 }
|
|
396
|
-
};
|
|
397
|
-
/** m/s, y-up (up = +y). */
|
|
398
|
-
velocity = [
|
|
399
|
-
0,
|
|
400
|
-
0,
|
|
401
|
-
0
|
|
402
|
-
];
|
|
403
|
-
snapToGround = true;
|
|
404
|
-
/** Max climbable slope angle. */
|
|
405
|
-
slopeLimitDeg = 45;
|
|
406
|
-
/** @internal Updated by Physics3D.moveAndSlide. */
|
|
407
|
-
_grounded = false;
|
|
408
|
-
moveAndSlide() {
|
|
409
|
-
if (!this._physics) throw new IncantoError("TREE_VIOLATION", this.collider.shape === void 0 ? `CharacterBody3D '${this.name}' has no collider — set a collider prop (physics skips collider-less bodies).` : `CharacterBody3D '${this.name}' has no physics world — await enablePhysics3D(engine) first.`);
|
|
410
|
-
this._physics.moveAndSlide(this);
|
|
411
|
-
}
|
|
412
|
-
isOnFloor() {
|
|
413
|
-
return this._grounded;
|
|
414
|
-
}
|
|
415
|
-
};
|
|
416
|
-
//#endregion
|
|
417
154
|
//#region src/3d/nodes/joint-3d.ts
|
|
418
155
|
/**
|
|
419
156
|
* A physics joint linking its PARENT body to `target` (a node path to the
|
|
@@ -6684,121 +6421,6 @@ function softSprite() {
|
|
|
6684
6421
|
return tex;
|
|
6685
6422
|
}
|
|
6686
6423
|
//#endregion
|
|
6687
|
-
//#region src/3d/water/interaction.ts
|
|
6688
|
-
/**
|
|
6689
|
-
* Water3D character interaction — pure functions, no three, node-env testable.
|
|
6690
|
-
*
|
|
6691
|
-
* The node samples physics bodies each update, detects waterline crossings
|
|
6692
|
-
* (`detectCrossings`), and converts them into ripple impulses in a FIXED-SIZE
|
|
6693
|
-
* ring buffer that mirrors the shader's `uRipples[8]` uniform array
|
|
6694
|
-
* (`pushRipple` / `ageRipples`). `rippleHeight` is the exact ring-wave shape
|
|
6695
|
-
* the vertex shader evaluates — `quality: 'simple'` runs it on the CPU.
|
|
6696
|
-
*/
|
|
6697
|
-
/** Ring-buffer capacity — MUST match `uRipples[8]` in the water shaders. */
|
|
6698
|
-
const WATER_MAX_RIPPLES = 8;
|
|
6699
|
-
/**
|
|
6700
|
-
* Distance from a body's ORIGIN down to its collider's lowest point. Water
|
|
6701
|
-
* interaction adds this to the waterline so a body counts as touching the water
|
|
6702
|
-
* when its FEET dip below the surface — not only when its centre does (a wading
|
|
6703
|
-
* character whose origin rides ~1 m above its feet would otherwise never splash).
|
|
6704
|
-
* 0 for a missing/empty/unsized collider (origin == feet).
|
|
6705
|
-
*/
|
|
6706
|
-
function colliderFootDrop(collider) {
|
|
6707
|
-
if (!collider || typeof collider !== "object") return 0;
|
|
6708
|
-
switch (collider.shape) {
|
|
6709
|
-
case "capsule": return (collider.height ?? 0) / 2 + (collider.radius ?? 0);
|
|
6710
|
-
case "sphere": return collider.radius ?? 0;
|
|
6711
|
-
case "box": return (collider.size?.[1] ?? 0) / 2;
|
|
6712
|
-
default: return 0;
|
|
6713
|
-
}
|
|
6714
|
-
}
|
|
6715
|
-
/**
|
|
6716
|
-
* Foam strength (0..1) for a body ENTERING the water, from its DOWNWARD speed:
|
|
6717
|
-
* a gentle wade-in froths a little; a hard plunge froths fully. Clamped at
|
|
6718
|
-
* `FOAM_FULL_SPEED`.
|
|
6719
|
-
*/
|
|
6720
|
-
function entryFoam(fallSpeed) {
|
|
6721
|
-
return .55 + .45 * (Math.min(Math.max(fallSpeed, 0), 12) / 12);
|
|
6722
|
-
}
|
|
6723
|
-
/**
|
|
6724
|
-
* Diff body positions against the waterline. A body enters when `y` drops
|
|
6725
|
-
* below its waterline and exits when it rises above `waterline + hysteresis`
|
|
6726
|
-
* (the band stops surface-bobbing bodies from flickering entered/exited).
|
|
6727
|
-
* Bodies missing from `samples` (despawned mid-water) are dropped WITHOUT an
|
|
6728
|
-
* `exited` event — their splash position no longer exists.
|
|
6729
|
-
*/
|
|
6730
|
-
function detectCrossings(prev, samples, hysteresis = 0) {
|
|
6731
|
-
const entered = [];
|
|
6732
|
-
const exited = [];
|
|
6733
|
-
const inWater = /* @__PURE__ */ new Set();
|
|
6734
|
-
for (const s of samples) {
|
|
6735
|
-
const wasIn = prev.has(s.id);
|
|
6736
|
-
if (wasIn ? s.y <= s.waterline + hysteresis : s.y < s.waterline) {
|
|
6737
|
-
inWater.add(s.id);
|
|
6738
|
-
if (!wasIn) entered.push(s);
|
|
6739
|
-
} else if (wasIn) exited.push(s);
|
|
6740
|
-
}
|
|
6741
|
-
return {
|
|
6742
|
-
entered,
|
|
6743
|
-
exited,
|
|
6744
|
-
inWater
|
|
6745
|
-
};
|
|
6746
|
-
}
|
|
6747
|
-
/** Append a ripple at age 0; when the buffer is full, evict the OLDEST.
|
|
6748
|
-
* `foam` (0..1) tags how much surface froth the shaders paint around it. */
|
|
6749
|
-
function pushRipple(ripples, x, z, amp, foam = 0, max = 8) {
|
|
6750
|
-
if (ripples.length >= max) {
|
|
6751
|
-
let oldest = 0;
|
|
6752
|
-
for (let i = 1; i < ripples.length; i++) if (ripples[i].age > ripples[oldest].age) oldest = i;
|
|
6753
|
-
ripples.splice(oldest, 1);
|
|
6754
|
-
}
|
|
6755
|
-
ripples.push({
|
|
6756
|
-
x,
|
|
6757
|
-
z,
|
|
6758
|
-
age: 0,
|
|
6759
|
-
amp,
|
|
6760
|
-
foam
|
|
6761
|
-
});
|
|
6762
|
-
}
|
|
6763
|
-
/** Advance ages by `dt` and drop ripples decayed past `maxAge` (in place). */
|
|
6764
|
-
function ageRipples(ripples, dt, maxAge = 3) {
|
|
6765
|
-
for (let i = ripples.length - 1; i >= 0; i--) {
|
|
6766
|
-
const r = ripples[i];
|
|
6767
|
-
r.age += dt;
|
|
6768
|
-
if (r.age >= maxAge) ripples.splice(i, 1);
|
|
6769
|
-
}
|
|
6770
|
-
}
|
|
6771
|
-
/** The ring-wave height contribution of one ripple at world (x, z). */
|
|
6772
|
-
function rippleHeight(x, z, ripple) {
|
|
6773
|
-
const dx = x - ripple.x;
|
|
6774
|
-
const dz = z - ripple.z;
|
|
6775
|
-
const r = Math.sqrt(dx * dx + dz * dz);
|
|
6776
|
-
return ripple.amp * Math.sin(6 * r - 8 * ripple.age) * Math.exp(-1.1 * ripple.age) * Math.exp(-.15 * r * r);
|
|
6777
|
-
}
|
|
6778
|
-
/** Sum of every live ripple's height at world (x, z) — the CPU water path. */
|
|
6779
|
-
function ripplesHeightAt(x, z, ripples) {
|
|
6780
|
-
let h = 0;
|
|
6781
|
-
for (const r of ripples) h += rippleHeight(x, z, r);
|
|
6782
|
-
return h;
|
|
6783
|
-
}
|
|
6784
|
-
/**
|
|
6785
|
-
* Accumulate per-body bob timers for bodies floating IN water; returns the
|
|
6786
|
-
* ids due for a gentle bob impulse this update (timer wraps, cadence keeps).
|
|
6787
|
-
* Timers of bodies that left the water are forgotten.
|
|
6788
|
-
*/
|
|
6789
|
-
function stepBobTimers(timers, inWater, dt, interval) {
|
|
6790
|
-
const due = [];
|
|
6791
|
-
for (const id of timers.keys()) if (!inWater.has(id)) timers.delete(id);
|
|
6792
|
-
for (const id of inWater) {
|
|
6793
|
-
const t = (timers.get(id) ?? 0) + dt;
|
|
6794
|
-
if (t >= interval) {
|
|
6795
|
-
due.push(id);
|
|
6796
|
-
timers.set(id, t - interval);
|
|
6797
|
-
} else timers.set(id, t);
|
|
6798
|
-
}
|
|
6799
|
-
return due;
|
|
6800
|
-
}
|
|
6801
|
-
//#endregion
|
|
6802
6424
|
//#region src/3d/water/river.ts
|
|
6803
6425
|
/** Dense Catmull-Rom taps per control segment before arc-length resampling. */
|
|
6804
6426
|
const SPLINE_TAPS = 24;
|
|
@@ -6910,15 +6532,17 @@ function buildRiverRings(opts) {
|
|
|
6910
6532
|
const raw = bed[i];
|
|
6911
6533
|
descending[i] = i === 0 ? raw : Math.min(raw, descending[i - 1]);
|
|
6912
6534
|
}
|
|
6913
|
-
const
|
|
6535
|
+
const keep = Math.max(.5, opts.depth * 1.5);
|
|
6536
|
+
const smoothed = descending.map((center, i) => {
|
|
6914
6537
|
let sum = 0;
|
|
6915
6538
|
let n = 0;
|
|
6916
6539
|
for (let k = -3; k <= PROFILE_SMOOTH; k++) {
|
|
6917
|
-
const
|
|
6918
|
-
|
|
6540
|
+
const h = descending[Math.min(Math.max(i + k, 0), descending.length - 1)];
|
|
6541
|
+
if (Math.abs(h - center) > keep) continue;
|
|
6542
|
+
sum += h;
|
|
6919
6543
|
n++;
|
|
6920
6544
|
}
|
|
6921
|
-
return sum / n;
|
|
6545
|
+
return n > 0 ? sum / n : center;
|
|
6922
6546
|
});
|
|
6923
6547
|
const profile = new Array(smoothed.length);
|
|
6924
6548
|
for (let i = 0; i < smoothed.length; i++) {
|
|
@@ -7079,6 +6703,39 @@ function hitFrom(a, b, t, x, z, capped) {
|
|
|
7079
6703
|
};
|
|
7080
6704
|
}
|
|
7081
6705
|
/**
|
|
6706
|
+
* Find the waterfalls in a course: maximal runs of steep stations, reported at
|
|
6707
|
+
* the station where the water lands. Callers hang spray on these, and the
|
|
6708
|
+
* shader aerates the curtain from the same grade the detector reads — one
|
|
6709
|
+
* source of truth for "this is falling, not flowing".
|
|
6710
|
+
*/
|
|
6711
|
+
function detectFalls(rings, opts = {}) {
|
|
6712
|
+
const minGrade = opts.minGrade ?? .45;
|
|
6713
|
+
const minDrop = opts.minDrop ?? 1.2;
|
|
6714
|
+
const out = [];
|
|
6715
|
+
let start = -1;
|
|
6716
|
+
for (let i = 0; i <= rings.length; i++) {
|
|
6717
|
+
const steep = i < rings.length && rings[i].slope >= minGrade;
|
|
6718
|
+
if (steep && start < 0) start = i;
|
|
6719
|
+
if (steep || start < 0) continue;
|
|
6720
|
+
const lip = rings[Math.max(0, start - 1)];
|
|
6721
|
+
const base = rings[Math.min(rings.length - 1, i)];
|
|
6722
|
+
const drop = lip.surfaceY - base.surfaceY;
|
|
6723
|
+
if (drop >= minDrop) out.push({
|
|
6724
|
+
x: base.x,
|
|
6725
|
+
z: base.z,
|
|
6726
|
+
topY: lip.surfaceY,
|
|
6727
|
+
baseY: base.surfaceY,
|
|
6728
|
+
drop,
|
|
6729
|
+
halfWidth: base.halfWidth,
|
|
6730
|
+
dirX: base.tx,
|
|
6731
|
+
dirZ: base.tz,
|
|
6732
|
+
speed: base.speed
|
|
6733
|
+
});
|
|
6734
|
+
start = -1;
|
|
6735
|
+
}
|
|
6736
|
+
return out;
|
|
6737
|
+
}
|
|
6738
|
+
/**
|
|
7082
6739
|
* Trace where water would actually run from a point: repeated steepest descent
|
|
7083
6740
|
* with momentum, so the line follows a valley floor instead of rattling between
|
|
7084
6741
|
* its walls. Ends when the ground goes flat (a pool), the trace leaves the play
|
|
@@ -7225,7 +6882,10 @@ void main() {
|
|
|
7225
6882
|
|
|
7226
6883
|
// hydraulic jump: in a chute the wave train stands still in the world (phase
|
|
7227
6884
|
// on the downstream coordinate, NEVER scrolled) and only breathes in time
|
|
7228
|
-
|
|
6885
|
+
// standing waves belong to a RAPID: past ~20° the water has left the bed and
|
|
6886
|
+
// a wave train on a falling curtain reads as a rippling flag
|
|
6887
|
+
float chute = smoothstep(0.035, 0.16, grade) * smoothstep(0.6, 2.4, speed) *
|
|
6888
|
+
(1.0 - smoothstep(0.35, 0.7, grade));
|
|
7229
6889
|
float standing =
|
|
7230
6890
|
sin(aFlowUv.x * 1.9 + sin(uTime * 0.7) * 0.35) * 0.6 +
|
|
7231
6891
|
sin(aFlowUv.x * 3.6 + sin(uTime * 0.9) * 0.5) * 0.4;
|
|
@@ -7329,8 +6989,11 @@ void main() {
|
|
|
7329
6989
|
vec3 ripB = riverRipple(base - vec2(travel * pB, 0.0));
|
|
7330
6990
|
vec3 ripple = mix(ripA, ripB, blend);
|
|
7331
6991
|
|
|
7332
|
-
|
|
7333
|
-
|
|
6992
|
+
// on a curtain the pattern stretches into strands: the same field, sampled
|
|
6993
|
+
// with the downstream axis squeezed hard, is what reads as falling water
|
|
6994
|
+
float streakScale = mix(0.55, 0.16, smoothstep(0.35, 0.9, grade));
|
|
6995
|
+
float streakA = riverStreak(base * streakScale - vec2(travel * pA * streakScale, 0.0));
|
|
6996
|
+
float streakB = riverStreak(base * streakScale - vec2(travel * pB * streakScale, 0.0));
|
|
7334
6997
|
float streak = mix(streakA, streakB, blend);
|
|
7335
6998
|
|
|
7336
6999
|
// ripples flatten out where the water thins to a film over the bed
|
|
@@ -7356,14 +7019,25 @@ void main() {
|
|
|
7356
7019
|
// shallow is RELATIVE to this river: a hand's depth is a riffle in a brook
|
|
7357
7020
|
// and nothing at all in a millrace, so the thresholds ride uMaxDepth
|
|
7358
7021
|
float shallowT = column / max(uMaxDepth, 0.05);
|
|
7359
|
-
|
|
7360
|
-
|
|
7022
|
+
// a riffle needs water to break OVER something — a centimetres-thin film on
|
|
7023
|
+
// a drying bar is a wet rock, and painting it white is what turns the tail of
|
|
7024
|
+
// a creek into a bright sheet
|
|
7025
|
+
float riffle =
|
|
7026
|
+
smoothstep(0.04, 0.13, column) *
|
|
7027
|
+
(1.0 - smoothstep(0.12, 0.42, shallowT)) *
|
|
7028
|
+
smoothstep(0.7, 2.2, speed);
|
|
7029
|
+
// FALLING, not flowing: past ~20° the sheet leaves the bed, shatters and
|
|
7030
|
+
// aerates. Aerated water is a white DIFFUSE body — it stops mirroring the
|
|
7031
|
+
// sky (that is the tell that separates a waterfall from a steep chute).
|
|
7032
|
+
float fall = smoothstep(0.35, 0.9, grade);
|
|
7033
|
+
float drive = clamp(rapids * 1.0 + bankShear * 0.35 + riffle * 0.5 + fall * 1.6, 0.0, 2.0) * uFoam;
|
|
7361
7034
|
// COVERAGE is the thing to be strict about: whitewater is where the surface
|
|
7362
7035
|
// has actually broken, and a river that fizzes everywhere reads as milk
|
|
7363
7036
|
float foam = smoothstep(0.74 - 0.45 * drive, 1.02 - 0.24 * drive, streak) * min(drive, 1.0);
|
|
7364
7037
|
// crest froth: the tops of standing waves in a real rapid always carry white
|
|
7365
7038
|
foam = max(foam, smoothstep(0.68, 0.96, ripple.x) * smoothstep(0.4, 1.0, rapids) * uFoam * 0.5);
|
|
7366
|
-
|
|
7039
|
+
// froth can never be more present than the water carrying it
|
|
7040
|
+
foam = clamp(foam, 0.0, 1.0) * smoothstep(0.02, 0.14, column);
|
|
7367
7041
|
|
|
7368
7042
|
// ── body ────────────────────────────────────────────────────────────────
|
|
7369
7043
|
float depthT = clamp(column / max(uMaxDepth, 0.01), 0.0, 1.0);
|
|
@@ -7413,10 +7087,16 @@ void main() {
|
|
|
7413
7087
|
spec *= 0.45 + 0.55 * glint * uSparkle;
|
|
7414
7088
|
spec *= 1.0 - foam * 0.85; // froth is rough — it kills the mirror
|
|
7415
7089
|
|
|
7090
|
+
fresnel *= 1.0 - 0.85 * fall; // froth scatters; it does not reflect
|
|
7416
7091
|
vec3 color = mix(body, sky, fresnel);
|
|
7417
7092
|
color += uSunColor * spec * 1.6;
|
|
7418
7093
|
color = mix(color, uFoamColor, foam * 0.88);
|
|
7419
7094
|
|
|
7095
|
+
// the LIP reads glassy-dark for a moment before the sheet breaks — the
|
|
7096
|
+
// contrast against the white curtain below is most of the drop's drama
|
|
7097
|
+
float lip = smoothstep(0.12, 0.32, grade) * (1.0 - smoothstep(0.32, 0.55, grade));
|
|
7098
|
+
color = mix(color, color * 0.72, lip * 0.6);
|
|
7099
|
+
|
|
7420
7100
|
// ── edges ───────────────────────────────────────────────────────────────
|
|
7421
7101
|
// the ribbon is built wider than the channel; what makes its outline is the
|
|
7422
7102
|
// water running out, so bars and boulders cut it exactly like the ground does
|
|
@@ -7521,7 +7201,8 @@ var River3D = class River3D extends Node3D {
|
|
|
7521
7201
|
sunColor: { default: "#fff6e0" },
|
|
7522
7202
|
sunIntensity: { default: 1 },
|
|
7523
7203
|
terrain: { default: "" },
|
|
7524
|
-
flowForce: { default: 1 }
|
|
7204
|
+
flowForce: { default: 1 },
|
|
7205
|
+
spray: { default: 1 }
|
|
7525
7206
|
};
|
|
7526
7207
|
/** Centerline control points `[[x, z], …]` in the node's local frame. */
|
|
7527
7208
|
path = [];
|
|
@@ -7557,9 +7238,17 @@ var River3D = class River3D extends Node3D {
|
|
|
7557
7238
|
terrain = "";
|
|
7558
7239
|
/** How hard the current sweeps bodies downstream (0 = visual only). */
|
|
7559
7240
|
flowForce = 1;
|
|
7241
|
+
/**
|
|
7242
|
+
* Mist at the foot of every waterfall the course contains: the node finds
|
|
7243
|
+
* the drops itself (see {@link detectFalls}) and hangs a Particles3D plume
|
|
7244
|
+
* on each, sized by how far the water falls. 0 = off.
|
|
7245
|
+
*/
|
|
7246
|
+
spray = 1;
|
|
7560
7247
|
renderOrder = 1;
|
|
7561
7248
|
time = 0;
|
|
7562
7249
|
rings = [];
|
|
7250
|
+
falls = [];
|
|
7251
|
+
sprayKey = "";
|
|
7563
7252
|
courseKey = "";
|
|
7564
7253
|
bedAt = null;
|
|
7565
7254
|
world = [
|
|
@@ -7585,6 +7274,7 @@ var River3D = class River3D extends Node3D {
|
|
|
7585
7274
|
"foam",
|
|
7586
7275
|
"ripples",
|
|
7587
7276
|
"flowForce",
|
|
7277
|
+
"spray",
|
|
7588
7278
|
"sunIntensity"
|
|
7589
7279
|
]) {
|
|
7590
7280
|
const v = r[key];
|
|
@@ -7604,6 +7294,7 @@ var River3D = class River3D extends Node3D {
|
|
|
7604
7294
|
update(dt) {
|
|
7605
7295
|
this.time += dt;
|
|
7606
7296
|
this.ensureCourse();
|
|
7297
|
+
this.syncSpray();
|
|
7607
7298
|
if (this.flowForce > 0 && this.rings.length > 1) this.sweepBodies(dt);
|
|
7608
7299
|
}
|
|
7609
7300
|
_syncObject3D() {
|
|
@@ -7627,6 +7318,21 @@ var River3D = class River3D extends Node3D {
|
|
|
7627
7318
|
surfaceY: hit.surfaceY + wy
|
|
7628
7319
|
} : null;
|
|
7629
7320
|
}
|
|
7321
|
+
/**
|
|
7322
|
+
* The waterfalls on this course — where the water stops flowing and starts
|
|
7323
|
+
* falling, with the drop height and the plunge point in WORLD coordinates.
|
|
7324
|
+
* Hang your own effects (sound, a rainbow, a hidden cave) on these.
|
|
7325
|
+
*/
|
|
7326
|
+
fallsAt() {
|
|
7327
|
+
this.ensureCourse();
|
|
7328
|
+
const [wx, wy, wz] = this.world;
|
|
7329
|
+
return this.falls.map((f) => ({
|
|
7330
|
+
...f,
|
|
7331
|
+
worldX: f.x + wx,
|
|
7332
|
+
worldY: f.baseY + wy,
|
|
7333
|
+
worldZ: f.z + wz
|
|
7334
|
+
}));
|
|
7335
|
+
}
|
|
7630
7336
|
/** @internal The derived centerline stations (surface, grade, current). */
|
|
7631
7337
|
_rings() {
|
|
7632
7338
|
return this.rings;
|
|
@@ -7677,6 +7383,7 @@ var River3D = class River3D extends Node3D {
|
|
|
7677
7383
|
this.bedAt = null;
|
|
7678
7384
|
if (this.path.length < 2) {
|
|
7679
7385
|
this.rings = [];
|
|
7386
|
+
this.falls = [];
|
|
7680
7387
|
return true;
|
|
7681
7388
|
}
|
|
7682
7389
|
const [wx, wy, wz] = world;
|
|
@@ -7691,6 +7398,7 @@ var River3D = class River3D extends Node3D {
|
|
|
7691
7398
|
step: Math.min(Math.max(narrow * STEP_PER_WIDTH, STEP_MIN), STEP_MAX),
|
|
7692
7399
|
bedAt: this.bedAt
|
|
7693
7400
|
});
|
|
7401
|
+
this.falls = detectFalls(this.rings);
|
|
7694
7402
|
return true;
|
|
7695
7403
|
}
|
|
7696
7404
|
rebuildIfNeeded(mesh) {
|
|
@@ -7790,6 +7498,48 @@ var River3D = class River3D extends Node3D {
|
|
|
7790
7498
|
set("uSparkle", this.ripples);
|
|
7791
7499
|
}
|
|
7792
7500
|
/**
|
|
7501
|
+
* Hang a mist plume on every fall. Runtime children (`__spray*`), rebuilt
|
|
7502
|
+
* only when the course or the dial changes — a plume is a Particles3D, so it
|
|
7503
|
+
* carries its own budget, culling and editor visibility for free.
|
|
7504
|
+
*/
|
|
7505
|
+
syncSpray() {
|
|
7506
|
+
const key = this.spray > 0 ? `${this.spray}|${JSON.stringify(this.falls)}` : "";
|
|
7507
|
+
if (key === this.sprayKey) return;
|
|
7508
|
+
this.sprayKey = key;
|
|
7509
|
+
for (const child of [...this.children]) if (child.name.startsWith("__spray")) child.free();
|
|
7510
|
+
if (this.spray <= 0) return;
|
|
7511
|
+
this.falls.forEach((fall, i) => {
|
|
7512
|
+
const plume = new Particles3D(`__spray${i}`);
|
|
7513
|
+
plume.position = [
|
|
7514
|
+
fall.x + fall.dirX * .6,
|
|
7515
|
+
fall.baseY + .15,
|
|
7516
|
+
fall.z + fall.dirZ * .6
|
|
7517
|
+
];
|
|
7518
|
+
const power = Math.min(1.6, .4 + fall.drop * .12) * this.spray;
|
|
7519
|
+
plume.rate = Math.round(Math.min(46, 10 + fall.drop * 3) * this.spray);
|
|
7520
|
+
plume.maxParticles = 160;
|
|
7521
|
+
plume.lifetime = [1, 2.1];
|
|
7522
|
+
plume.speed = [55 * power, 170 * power];
|
|
7523
|
+
plume.directionDeg = -90;
|
|
7524
|
+
plume.spreadDeg = 58;
|
|
7525
|
+
plume.gravity = [
|
|
7526
|
+
0,
|
|
7527
|
+
-240,
|
|
7528
|
+
0
|
|
7529
|
+
];
|
|
7530
|
+
plume.drag = 1.2;
|
|
7531
|
+
plume.sizeStart = 16 * power;
|
|
7532
|
+
plume.sizeEnd = 80 * power;
|
|
7533
|
+
plume.colorStart = "#ffffff";
|
|
7534
|
+
plume.colorEnd = "#dfe9ec";
|
|
7535
|
+
plume.alphaStart = .2;
|
|
7536
|
+
plume.alphaEnd = 0;
|
|
7537
|
+
plume.blend = "normal";
|
|
7538
|
+
plume.depthTest = true;
|
|
7539
|
+
this.addChild(plume);
|
|
7540
|
+
});
|
|
7541
|
+
}
|
|
7542
|
+
/**
|
|
7793
7543
|
* Drag every body floating in the channel toward the water's own velocity.
|
|
7794
7544
|
* Drag rather than thrust: a raft accelerates until it matches the current
|
|
7795
7545
|
* and then coasts, and a swimmer can still fight across a slow ford.
|
|
@@ -7799,7 +7549,7 @@ var River3D = class River3D extends Node3D {
|
|
|
7799
7549
|
while (top.parent) top = top.parent;
|
|
7800
7550
|
const bodies = this._bodies;
|
|
7801
7551
|
bodies.length = 0;
|
|
7802
|
-
collectBodies
|
|
7552
|
+
collectBodies(top, bodies);
|
|
7803
7553
|
if (bodies.length === 0) return;
|
|
7804
7554
|
const [wx, wy, wz] = this.world;
|
|
7805
7555
|
for (const body of bodies) {
|
|
@@ -7810,7 +7560,7 @@ var River3D = class River3D extends Node3D {
|
|
|
7810
7560
|
const feet = p[1] - colliderFootDrop(body.collider);
|
|
7811
7561
|
if (feet > surfaceY + CURRENT_SURFACE_SLACK) continue;
|
|
7812
7562
|
if (feet < surfaceY - this.depth - CURRENT_UNDER_SLACK) continue;
|
|
7813
|
-
const vel = bodyVelocity
|
|
7563
|
+
const vel = bodyVelocity(body);
|
|
7814
7564
|
if (!vel) continue;
|
|
7815
7565
|
const gain = Math.min(1, this.flowForce * CURRENT_DRAG * dt);
|
|
7816
7566
|
const dvx = (hit.dirX * hit.speed - (vel[0] ?? 0)) * gain;
|
|
@@ -7830,12 +7580,12 @@ var River3D = class River3D extends Node3D {
|
|
|
7830
7580
|
}
|
|
7831
7581
|
};
|
|
7832
7582
|
/** Bodies a current can carry: kinematic characters and dynamic rigid bodies. */
|
|
7833
|
-
function collectBodies
|
|
7583
|
+
function collectBodies(node, out) {
|
|
7834
7584
|
if (node instanceof CharacterBody3D || node instanceof RigidBody3D) out.push(node);
|
|
7835
|
-
for (const child of node.children) collectBodies
|
|
7585
|
+
for (const child of node.children) collectBodies(child, out);
|
|
7836
7586
|
}
|
|
7837
7587
|
/** A body's velocity vector — kinematic `velocity` or dynamic `linearVelocity`. */
|
|
7838
|
-
function bodyVelocity
|
|
7588
|
+
function bodyVelocity(body) {
|
|
7839
7589
|
return body.velocity ?? body.linearVelocity;
|
|
7840
7590
|
}
|
|
7841
7591
|
//#endregion
|
|
@@ -10069,9 +9819,9 @@ function normalizeTint(color) {
|
|
|
10069
9819
|
const max = Math.max(tint.r, tint.g, tint.b);
|
|
10070
9820
|
if (max > 0) tint.multiplyScalar(1 / max);
|
|
10071
9821
|
else tint.setRGB(1, 1, 1);
|
|
10072
|
-
return tint.lerp(WHITE
|
|
9822
|
+
return tint.lerp(WHITE, .18);
|
|
10073
9823
|
}
|
|
10074
|
-
const WHITE
|
|
9824
|
+
const WHITE = new Color(1, 1, 1);
|
|
10075
9825
|
/** Bark tint for a customized trunkColor: keep the hue, normalize the
|
|
10076
9826
|
* brightest channel to 1 — the color map keeps its own brightness. */
|
|
10077
9827
|
function barkTint(color) {
|
|
@@ -11095,2749 +10845,6 @@ void main() {
|
|
|
11095
10845
|
}
|
|
11096
10846
|
`;
|
|
11097
10847
|
//#endregion
|
|
11098
|
-
//#region src/3d/frustum.ts
|
|
11099
|
-
const scratchFrustum = new Frustum();
|
|
11100
|
-
const scratchMatrix = new Matrix4();
|
|
11101
|
-
const scratchSphere = new Sphere();
|
|
11102
|
-
/**
|
|
11103
|
-
* Is a world-space sphere inside the camera's view frustum?
|
|
11104
|
-
*
|
|
11105
|
-
* Used to skip per-frame work for things the camera can't see — e.g. Water3D's
|
|
11106
|
-
* fancy scene pre-pass + reflection cube, which otherwise re-render the whole
|
|
11107
|
-
* scene every frame for ponds that are off-screen. Uses shared scratch objects
|
|
11108
|
-
* (single-threaded engine), so it allocates nothing per call.
|
|
11109
|
-
*/
|
|
11110
|
-
function sphereInCameraFrustum(camera, center, radius) {
|
|
11111
|
-
scratchMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
|
|
11112
|
-
scratchFrustum.setFromProjectionMatrix(scratchMatrix);
|
|
11113
|
-
scratchSphere.center.set(center.x, center.y, center.z);
|
|
11114
|
-
scratchSphere.radius = radius;
|
|
11115
|
-
return scratchFrustum.intersectsSphere(scratchSphere);
|
|
11116
|
-
}
|
|
11117
|
-
//#endregion
|
|
11118
|
-
//#region src/3d/water/lake.ts
|
|
11119
|
-
/**
|
|
11120
|
-
* The Water3D 'simple' quality lake shader — the CHEAP good-looking path.
|
|
11121
|
-
*
|
|
11122
|
-
* 'fancy' water is commercial-grade but pays for it: a CubeCamera reflection
|
|
11123
|
-
* plus a half-res scene depth/color pre-pass (and a depth-completion pass) —
|
|
11124
|
-
* 2–3 extra full-scene renders every frame, which drop a pond to ~30 fps. The
|
|
11125
|
-
* old 'simple' fallback was a plain lit MeshStandardMaterial over CPU-displaced
|
|
11126
|
-
* sine waves: cheap, but flat and lifeless (the "허접" playtest complaint).
|
|
11127
|
-
*
|
|
11128
|
-
* This lake shader keeps the cheap budget — ZERO scene re-renders, no cube map,
|
|
11129
|
-
* no depth texture — and still reads as real water by faking the three cues the
|
|
11130
|
-
* eye actually needs:
|
|
11131
|
-
*
|
|
11132
|
-
* - Fresnel sky reflection: a procedural horizon→zenith SKY TINT (uSkyColor),
|
|
11133
|
-
* Schlick-mixed in by view angle. Steep view = water color, grazing = sky.
|
|
11134
|
-
* No environment to sample — a lake reflects mostly sky anyway.
|
|
11135
|
-
* - Procedural ripple normals: two crossing animated simplex layers perturb the
|
|
11136
|
-
* (CPU-computed) macro normal per pixel — the LIQUID micro-detail, texture-
|
|
11137
|
-
* free, distance-faded (procedural noise has no mips → aliases at range).
|
|
11138
|
-
* - Sun glint: a tight Blinn-Phong spark + soft sheen off the rippled normal,
|
|
11139
|
-
* weighted toward the sun's azimuth path (uSunDirection/Color/Intensity).
|
|
11140
|
-
* - Depth-free body: looking straight down reads the lighter surface tint
|
|
11141
|
-
* (clearer shallows feel), grazing reads the darker deep base; grazing also
|
|
11142
|
-
* reads more opaque/reflective. No depth buffer, all from the view angle.
|
|
11143
|
-
* - Soft edge fade: the rectangular plane's rim fades to zero alpha, so the
|
|
11144
|
-
* surface melts into the shore instead of poking out as a hard rectangle
|
|
11145
|
-
* (the "물 삐져나옴" complaint).
|
|
11146
|
-
*
|
|
11147
|
-
* The CPU still displaces the coarse vertex grid (macro swell + interaction
|
|
11148
|
-
* ring waves) and computes its normals — that path stays deterministic and
|
|
11149
|
-
* headless-testable; this shader only adds the per-pixel look on top.
|
|
11150
|
-
*
|
|
11151
|
-
* Atmosphere stage (marked below): the fragment ends with three's
|
|
11152
|
-
* tonemapping + colorspace + fog chunks in built-in-material order, and both
|
|
11153
|
-
* stages carry the standard fog chunks, so the lake tone-maps, sRGB-encodes
|
|
11154
|
-
* and fogs identically to the fancy water and the terrain.
|
|
11155
|
-
*/
|
|
11156
|
-
const LAKE_VERT = `
|
|
11157
|
-
precision highp float;
|
|
11158
|
-
|
|
11159
|
-
// atmosphere: three's linear fog rides the standard chunks (compiled out
|
|
11160
|
-
// unless the scene declares fog — USE_FOG comes from material.fog)
|
|
11161
|
-
#include <fog_pars_vertex>
|
|
11162
|
-
|
|
11163
|
-
varying vec3 vWorldPosition;
|
|
11164
|
-
varying vec3 vNormalW;
|
|
11165
|
-
|
|
11166
|
-
void main() {
|
|
11167
|
-
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
|
|
11168
|
-
vWorldPosition = modelPosition.xyz;
|
|
11169
|
-
// The CPU computeVertexNormals() macro normal, lifted to world space. Water
|
|
11170
|
-
// is laid axis-aligned on XZ (no parent rotation expected), so the plain
|
|
11171
|
-
// model rotation is exact enough for the fresnel/glint cues.
|
|
11172
|
-
vNormalW = normalize(mat3(modelMatrix) * normal);
|
|
11173
|
-
|
|
11174
|
-
vec4 mvPosition = viewMatrix * modelPosition;
|
|
11175
|
-
gl_Position = projectionMatrix * mvPosition;
|
|
11176
|
-
|
|
11177
|
-
// atmosphere: fog depth from the displaced surface position
|
|
11178
|
-
#include <fog_vertex>
|
|
11179
|
-
}
|
|
11180
|
-
`;
|
|
11181
|
-
const LAKE_FRAG = `
|
|
11182
|
-
precision highp float;
|
|
11183
|
-
|
|
11184
|
-
// atmosphere: standard fog chunk (compiled out without scene fog)
|
|
11185
|
-
#include <fog_pars_fragment>
|
|
11186
|
-
|
|
11187
|
-
uniform float uTime;
|
|
11188
|
-
uniform float uOpacity;
|
|
11189
|
-
uniform vec3 uColor; // deep water base (derived from the single hue)
|
|
11190
|
-
uniform vec3 uSurfaceColor; // shallow/surface tint (the hue itself)
|
|
11191
|
-
uniform vec3 uSkyColor; // fresnel reflection tint — NO cube map
|
|
11192
|
-
uniform vec3 uSunDirection;
|
|
11193
|
-
uniform vec3 uSunColor;
|
|
11194
|
-
uniform float uSunIntensity;
|
|
11195
|
-
uniform float uDetailStrength;
|
|
11196
|
-
uniform float uFresnelPower;
|
|
11197
|
-
uniform vec3 uScale; // half extents (x, _, z) for the edge fade
|
|
11198
|
-
uniform vec3 uWaterCenter;
|
|
11199
|
-
|
|
11200
|
-
// incanto: splash whitewater — the ripple buffer drives expanding surface foam
|
|
11201
|
-
// (entry "풍덩" + wake "물살"); uSplashFoam = the Water3D.splash gate.
|
|
11202
|
-
uniform vec4 uRipples[8];
|
|
11203
|
-
uniform float uRippleFoam[8];
|
|
11204
|
-
uniform float uSplashFoam;
|
|
11205
|
-
uniform vec3 uFoamColor;
|
|
11206
|
-
|
|
11207
|
-
varying vec3 vWorldPosition;
|
|
11208
|
-
varying vec3 vNormalW;
|
|
11209
|
-
|
|
11210
|
-
// Simplex 2D noise (Ian McEwan, Stefan Gustavson — https://github.com/stegu/webgl-noise, MIT)
|
|
11211
|
-
vec3 permute(vec3 x) { return mod(((x * 34.0) + 1.0) * x, 289.0); }
|
|
11212
|
-
|
|
11213
|
-
float snoise(vec2 v) {
|
|
11214
|
-
const vec4 C = vec4(0.211324865405187, 0.366025403784439, -0.577350269189626, 0.024390243902439);
|
|
11215
|
-
vec2 i = floor(v + dot(v, C.yy));
|
|
11216
|
-
vec2 x0 = v - i + dot(i, C.xx);
|
|
11217
|
-
vec2 i1;
|
|
11218
|
-
i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
|
|
11219
|
-
vec4 x12 = x0.xyxy + C.xxzz;
|
|
11220
|
-
x12.xy -= i1;
|
|
11221
|
-
i = mod(i, 289.0);
|
|
11222
|
-
vec3 p = permute(permute(i.y + vec3(0.0, i1.y, 1.0)) + i.x + vec3(0.0, i1.x, 1.0));
|
|
11223
|
-
vec3 m = max(0.5 - vec3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), 0.0);
|
|
11224
|
-
m = m * m;
|
|
11225
|
-
m = m * m;
|
|
11226
|
-
vec3 x = 2.0 * fract(p * C.www) - 1.0;
|
|
11227
|
-
vec3 h = abs(x) - 0.5;
|
|
11228
|
-
vec3 ox = floor(x + 0.5);
|
|
11229
|
-
vec3 a0 = x - ox;
|
|
11230
|
-
m *= 1.79284291400159 - 0.85373472095314 * (a0 * a0 + h * h);
|
|
11231
|
-
vec3 g;
|
|
11232
|
-
g.x = a0.x * x0.x + h.x * x0.y;
|
|
11233
|
-
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
|
|
11234
|
-
return 130.0 * dot(m, g);
|
|
11235
|
-
}
|
|
11236
|
-
|
|
11237
|
-
// finite-difference gradient of the simplex height field
|
|
11238
|
-
vec2 noiseGrad(vec2 p) {
|
|
11239
|
-
const float e = 0.06;
|
|
11240
|
-
float h = snoise(p);
|
|
11241
|
-
return vec2(snoise(p + vec2(e, 0.0)) - h, snoise(p + vec2(0.0, e)) - h) / e;
|
|
11242
|
-
}
|
|
11243
|
-
|
|
11244
|
-
// incanto: splash WHITEWATER. A clean radial disc/ring read as a fake CG stamp;
|
|
11245
|
-
// real foam is irregular clumps that drift with the water and break apart at the
|
|
11246
|
-
// edges. A soft COVERAGE envelope (an expanding patch fading over FOAM_MAX_AGE —
|
|
11247
|
-
// the CPU twin rippleFoamAt, constants MIRRORED) THRESHOLDS a domain-warped
|
|
11248
|
-
// fractal noise: dense coverage → broad froth, thinning coverage raises the
|
|
11249
|
-
// threshold → it erodes into sparse bubbles. Warp + drift = advected, not stamped.
|
|
11250
|
-
float lnoise(vec2 p) { return 0.5 + 0.5 * snoise(p); }
|
|
11251
|
-
float foamFbm(vec2 p) {
|
|
11252
|
-
float v = 0.0, a = 0.5;
|
|
11253
|
-
for (int i = 0; i < 4; i++) { v += a * lnoise(p); p = p * 2.02 + 7.3; a *= 0.5; }
|
|
11254
|
-
return v; // ~0..0.94
|
|
11255
|
-
}
|
|
11256
|
-
|
|
11257
|
-
float rippleFoam(vec2 p) {
|
|
11258
|
-
if (uSplashFoam < 0.5) return 0.0;
|
|
11259
|
-
float cover = 0.0;
|
|
11260
|
-
for (int i = 0; i < 8; i++) {
|
|
11261
|
-
float foam = uRippleFoam[i];
|
|
11262
|
-
if (foam <= 0.0) continue;
|
|
11263
|
-
vec4 r = uRipples[i];
|
|
11264
|
-
float age = r.z;
|
|
11265
|
-
float life = 1.0 - age / 1.6; // FOAM_MAX_AGE
|
|
11266
|
-
if (life <= 0.0) continue;
|
|
11267
|
-
float d = distance(p, r.xy);
|
|
11268
|
-
float radius = 0.4 + 1.15 * age; // FOAM_R0 + FOAM_SPREAD * age
|
|
11269
|
-
cover = max(cover, (1.0 - smoothstep(radius * 0.7, radius, d)) * foam * life);
|
|
11270
|
-
}
|
|
11271
|
-
if (cover <= 0.001) return 0.0;
|
|
11272
|
-
// FINE frequencies (premium foam = many small bubbles, not coarse blobs)
|
|
11273
|
-
vec2 flow = vec2(uTime * 0.05, uTime * -0.04);
|
|
11274
|
-
vec2 warp = vec2(foamFbm(p * 1.1 + flow), foamFbm(p * 1.1 + 4.2 - flow)) - 0.5;
|
|
11275
|
-
float pattern = foamFbm(p * 2.6 + warp * 1.4 + flow);
|
|
11276
|
-
// raised floor + wide band = sparser + softer = subtler/more refined
|
|
11277
|
-
float thresh = mix(0.36, 0.96, 1.0 - cover);
|
|
11278
|
-
float foamHere = smoothstep(thresh, thresh + 0.26, pattern);
|
|
11279
|
-
return clamp(foamHere * (0.4 + 0.5 * cover), 0.0, 1.0);
|
|
11280
|
-
}
|
|
11281
|
-
|
|
11282
|
-
// Two crossing animated ripple layers perturb the macro normal. Each fades out
|
|
11283
|
-
// with distance (procedural noise has no mips — past its range it aliases into
|
|
11284
|
-
// white speckle), so far water relaxes back to the smooth swell.
|
|
11285
|
-
vec3 detailNormal(vec3 n, vec2 worldXZ, float strength, float camDist) {
|
|
11286
|
-
float fadeMid = 1.0 - smoothstep(45.0, 170.0, camDist);
|
|
11287
|
-
float fadeFine = 1.0 - smoothstep(14.0, 70.0, camDist);
|
|
11288
|
-
// Three FINE isotropic ripple fields drifting in crossing directions — a
|
|
11289
|
-
// calm lake's surface is many small wavelets, not a few big swells, so the
|
|
11290
|
-
// frequencies are high (≈1.4 m / ≈0.8 m / ≈0.5 m) and the amplitudes small.
|
|
11291
|
-
// Each gradient is multiplied by its own frequency: noiseGrad returns the
|
|
11292
|
-
// slope in NOISE space, so without this the coarsest layer over-amplifies
|
|
11293
|
-
// into the foil-blob look. Net perturbation stays a gentle few degrees.
|
|
11294
|
-
vec2 dMid = normalize(vec2(0.72, 0.50));
|
|
11295
|
-
vec2 dFine = normalize(vec2(-0.42, 0.91));
|
|
11296
|
-
vec2 dMicro = normalize(vec2(0.34, -0.94));
|
|
11297
|
-
vec2 slope =
|
|
11298
|
-
noiseGrad(worldXZ * 0.70 + dMid * uTime * 0.14) * (0.70 * 0.085 * fadeMid)
|
|
11299
|
-
+ noiseGrad(worldXZ * 1.30 + dFine * uTime * 0.20) * (1.30 * 0.060 * fadeFine)
|
|
11300
|
-
+ noiseGrad(worldXZ * 2.10 - dMicro * uTime * 0.26) * (2.10 * 0.040 * fadeFine);
|
|
11301
|
-
slope *= strength;
|
|
11302
|
-
return normalize(vec3(n.x - slope.x, n.y, n.z - slope.y));
|
|
11303
|
-
}
|
|
11304
|
-
|
|
11305
|
-
void main() {
|
|
11306
|
-
vec3 viewDir = normalize(vWorldPosition - cameraPosition);
|
|
11307
|
-
float camDist = length(vWorldPosition - cameraPosition);
|
|
11308
|
-
|
|
11309
|
-
// Flatten the macro normal with distance so far ripples don't strobe into
|
|
11310
|
-
// sky-reflection sequins against the bright horizon.
|
|
11311
|
-
float geoFlat = 1.0 - 0.85 * smoothstep(140.0, 520.0, camDist);
|
|
11312
|
-
vec3 geoN = normalize(mix(vec3(0.0, 1.0, 0.0), normalize(vNormalW), geoFlat));
|
|
11313
|
-
vec3 normal = detailNormal(geoN, vWorldPosition.xz, uDetailStrength, camDist);
|
|
11314
|
-
|
|
11315
|
-
// Schlick fresnel: 2% normal-incidence floor (water's real F0), 98% at
|
|
11316
|
-
// grazing. Steep view → water color, grazing → sky tint.
|
|
11317
|
-
float fresnel = 0.02 + 0.98 * pow(1.0 - clamp(dot(-viewDir, normal), 0.0, 1.0), uFresnelPower);
|
|
11318
|
-
|
|
11319
|
-
// Depth-free body color from the view angle: straight down sees the lighter
|
|
11320
|
-
// surface (clearer shallows feel), grazing sees the deeper base.
|
|
11321
|
-
float viewUp = clamp(dot(-viewDir, vec3(0.0, 1.0, 0.0)), 0.0, 1.0);
|
|
11322
|
-
vec3 body = mix(uColor, uSurfaceColor, viewUp);
|
|
11323
|
-
|
|
11324
|
-
// Sky reflection WITHOUT a cube map: a soft horizon→zenith gradient sampled
|
|
11325
|
-
// by the reflected ray. The horizon is paler/brighter (atmospheric haze),
|
|
11326
|
-
// the zenith deeper — convincing for a lake, which reflects mostly sky.
|
|
11327
|
-
vec3 refl = reflect(viewDir, normal);
|
|
11328
|
-
float up = clamp(refl.y, 0.0, 1.0);
|
|
11329
|
-
vec3 skyTint = mix(uSkyColor * 1.08, uSkyColor * 0.82, up);
|
|
11330
|
-
|
|
11331
|
-
// cap the sky mix below a full mirror (that reads as white foil — the old
|
|
11332
|
-
// goop look) but high enough that the grazing far field catches believable
|
|
11333
|
-
// sky instead of going flat grey
|
|
11334
|
-
vec3 color = mix(body, skyTint, clamp(fresnel, 0.0, 0.62));
|
|
11335
|
-
|
|
11336
|
-
// Sun glint: a tight spark + soft sheen off the rippled normal, weighted to
|
|
11337
|
-
// the sun's azimuth path so crests sparkle ALONG the light and the rest of
|
|
11338
|
-
// the surface stays calm color (not an all-over glossy speckle).
|
|
11339
|
-
vec3 sunL = normalize(uSunDirection);
|
|
11340
|
-
vec3 halfway = normalize(sunL - viewDir); // -viewDir = surface→camera
|
|
11341
|
-
float specAngle = max(dot(normal, halfway), 0.0);
|
|
11342
|
-
vec2 toFrag = normalize(vWorldPosition.xz - cameraPosition.xz);
|
|
11343
|
-
float sunPath = pow(clamp(dot(toFrag, normalize(sunL.xz)), 0.0, 1.0), 4.0);
|
|
11344
|
-
float glintFade = 1.0 - smoothstep(70.0, 240.0, camDist);
|
|
11345
|
-
float glint = pow(specAngle, 380.0) * (0.25 + 0.9 * sunPath) * glintFade;
|
|
11346
|
-
float sheen = pow(specAngle, 36.0) * 0.05 * sunPath;
|
|
11347
|
-
color += uSunColor * min(uSunIntensity * (glint * 0.7 + sheen), 0.8);
|
|
11348
|
-
|
|
11349
|
-
// splash froth where bodies enter / wade — a PALE foam tint (not pure white,
|
|
11350
|
-
// which floats on top as a jarring decal); the water still reads through
|
|
11351
|
-
float splashFoam = rippleFoam(vWorldPosition.xz);
|
|
11352
|
-
color = mix(color, mix(uFoamColor, vec3(1.0), 0.3), splashFoam * 0.58);
|
|
11353
|
-
|
|
11354
|
-
// Soft edge fade — the rectangular rim melts into the shore. Normalized
|
|
11355
|
-
// distance from the node center to its half extents; the last ~18% fades to
|
|
11356
|
-
// zero alpha (no hard "물 삐져나옴" rectangle).
|
|
11357
|
-
vec2 nrm = (vWorldPosition.xz - uWaterCenter.xz) / vec2(uScale.x, uScale.z);
|
|
11358
|
-
float edge = 1.0 - smoothstep(0.82, 1.0, length(nrm));
|
|
11359
|
-
|
|
11360
|
-
// Grazing angles look more opaque/reflective; straight-down looks clearer.
|
|
11361
|
-
float alpha = mix(uOpacity * 0.7, min(uOpacity + 0.25, 1.0), fresnel);
|
|
11362
|
-
alpha *= edge;
|
|
11363
|
-
// foam reads even looking straight down — lift froth opacity, but only part
|
|
11364
|
-
// way so the disturbed water still shows through (translucent froth)
|
|
11365
|
-
alpha = max(alpha, splashFoam * 0.7 * edge);
|
|
11366
|
-
|
|
11367
|
-
gl_FragColor = vec4(color, alpha);
|
|
11368
|
-
|
|
11369
|
-
// atmosphere: join three's output pipeline — ACES tone map + working→output
|
|
11370
|
-
// colorspace encode + linear fog, in the SAME order as the built-in
|
|
11371
|
-
// materials so the lake fogs/grades identically to the terrain.
|
|
11372
|
-
#include <tonemapping_fragment>
|
|
11373
|
-
#include <colorspace_fragment>
|
|
11374
|
-
#include <fog_fragment>
|
|
11375
|
-
}
|
|
11376
|
-
`;
|
|
11377
|
-
//#endregion
|
|
11378
|
-
//#region src/3d/water/caustics.ts
|
|
11379
|
-
/**
|
|
11380
|
-
* Underwater caustics — the dancing refracted-light pattern the water surface
|
|
11381
|
-
* throws onto everything below it. The engine has no post-processing pipeline,
|
|
11382
|
-
* so the renderer does this as ONE depth-aware composite pass, and ONLY while
|
|
11383
|
-
* the camera is genuinely underwater (so normal above-water rendering — even
|
|
11384
|
-
* standing at the water's edge — is completely untouched):
|
|
11385
|
-
*
|
|
11386
|
-
* 1. render the scene to an offscreen target (color + depth)
|
|
11387
|
-
* 2. draw this fullscreen material, which reconstructs each pixel's WORLD
|
|
11388
|
-
* position from the depth buffer and adds an animated caustic highlight
|
|
11389
|
-
* wherever that world point sits below the water surface
|
|
11390
|
-
*
|
|
11391
|
-
* Because the pattern is keyed to reconstructed world XZ, it STICKS to the
|
|
11392
|
-
* floor/walls/props (it isn't a flat screen overlay) and fades with depth +
|
|
11393
|
-
* view distance so it melts into the underwater fog.
|
|
11394
|
-
*/
|
|
11395
|
-
/** Fullscreen triangle/quad — the vertex stage ignores the camera entirely. */
|
|
11396
|
-
const CAUSTICS_VERT = `
|
|
11397
|
-
varying vec2 vUv;
|
|
11398
|
-
void main() {
|
|
11399
|
-
vUv = uv;
|
|
11400
|
-
gl_Position = vec4(position.xy, 0.0, 1.0);
|
|
11401
|
-
}
|
|
11402
|
-
`;
|
|
11403
|
-
/**
|
|
11404
|
-
* The classic animated caustic pattern (layered moving cells), 0..1 — SHARED
|
|
11405
|
-
* between the underwater composite and Water3D's above-water caustics (the
|
|
11406
|
-
* refracted bottom shimmer), so both views of the same water agree.
|
|
11407
|
-
*/
|
|
11408
|
-
const CAUSTIC_GLSL = `
|
|
11409
|
-
float caustic(vec2 uv, float t) {
|
|
11410
|
-
vec2 p = mod(uv * 6.28318, 6.28318) - 250.0;
|
|
11411
|
-
vec2 i = p;
|
|
11412
|
-
float c = 1.0;
|
|
11413
|
-
float inten = 0.005;
|
|
11414
|
-
for (int n = 0; n < 5; n++) {
|
|
11415
|
-
float ti = t * (1.0 - (3.5 / float(n + 1)));
|
|
11416
|
-
i = p + vec2(cos(ti - i.x) + sin(ti + i.y), sin(ti - i.y) + cos(ti + i.x));
|
|
11417
|
-
c += 1.0 / length(vec2(p.x / (sin(i.x + ti) / inten), p.y / (cos(i.y + ti) / inten)));
|
|
11418
|
-
}
|
|
11419
|
-
c /= 5.0;
|
|
11420
|
-
c = 1.17 - pow(c, 1.4);
|
|
11421
|
-
return clamp(pow(abs(c), 8.0), 0.0, 1.0);
|
|
11422
|
-
}
|
|
11423
|
-
`;
|
|
11424
|
-
const CAUSTICS_FRAG = `
|
|
11425
|
-
precision highp float;
|
|
11426
|
-
varying vec2 vUv;
|
|
11427
|
-
uniform sampler2D tColor;
|
|
11428
|
-
uniform sampler2D tDepth;
|
|
11429
|
-
uniform mat4 uInvViewProj;
|
|
11430
|
-
uniform vec3 uCameraPos;
|
|
11431
|
-
uniform float uWaterLevel;
|
|
11432
|
-
uniform float uTime;
|
|
11433
|
-
uniform vec3 uCausticColor;
|
|
11434
|
-
uniform float uCausticIntensity;
|
|
11435
|
-
uniform float uCausticScale;
|
|
11436
|
-
uniform float uMaxDist;
|
|
11437
|
-
|
|
11438
|
-
${CAUSTIC_GLSL}
|
|
11439
|
-
|
|
11440
|
-
void main() {
|
|
11441
|
-
vec4 color = texture2D(tColor, vUv);
|
|
11442
|
-
float depth = texture2D(tDepth, vUv).x;
|
|
11443
|
-
// depth == 1 is the far plane (sky/background) — nothing to light there
|
|
11444
|
-
if (depth < 1.0) {
|
|
11445
|
-
vec4 ndc = vec4(vUv * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0);
|
|
11446
|
-
vec4 world = uInvViewProj * ndc;
|
|
11447
|
-
world.xyz /= world.w;
|
|
11448
|
-
float below = uWaterLevel - world.y;
|
|
11449
|
-
if (below > 0.0) {
|
|
11450
|
-
float dist = distance(world.xyz, uCameraPos);
|
|
11451
|
-
// caustics — fade with distance (melt into the fog) + a touch with depth
|
|
11452
|
-
float distFade = 1.0 - clamp(dist / uMaxDist, 0.0, 1.0);
|
|
11453
|
-
float depthFade = 1.0 - clamp(below / (uMaxDist * 0.5), 0.0, 0.6);
|
|
11454
|
-
// two octaves at offset speeds = the shimmering interference of real caustics
|
|
11455
|
-
float c =
|
|
11456
|
-
caustic(world.xz * uCausticScale, uTime) * 0.65 +
|
|
11457
|
-
caustic(world.xz * uCausticScale * 1.7 + 30.0, uTime * 0.8) * 0.35;
|
|
11458
|
-
color.rgb += uCausticColor * (c * uCausticIntensity * distFade * distFade * depthFade);
|
|
11459
|
-
}
|
|
11460
|
-
}
|
|
11461
|
-
gl_FragColor = color;
|
|
11462
|
-
}
|
|
11463
|
-
`;
|
|
11464
|
-
/** Build the fullscreen composite mesh (a 2×2 clip-space quad). */
|
|
11465
|
-
function createCausticsQuad() {
|
|
11466
|
-
const material = new ShaderMaterial({
|
|
11467
|
-
vertexShader: CAUSTICS_VERT,
|
|
11468
|
-
fragmentShader: CAUSTICS_FRAG,
|
|
11469
|
-
depthTest: false,
|
|
11470
|
-
depthWrite: false,
|
|
11471
|
-
uniforms: {
|
|
11472
|
-
tColor: { value: null },
|
|
11473
|
-
tDepth: { value: null },
|
|
11474
|
-
uInvViewProj: { value: new Matrix4() },
|
|
11475
|
-
uCameraPos: { value: new Vector3() },
|
|
11476
|
-
uWaterLevel: { value: 0 },
|
|
11477
|
-
uTime: { value: 0 },
|
|
11478
|
-
uCausticColor: { value: new Color("#cdeeff") },
|
|
11479
|
-
uCausticIntensity: { value: .55 },
|
|
11480
|
-
uCausticScale: { value: .32 },
|
|
11481
|
-
uMaxDist: { value: 22 }
|
|
11482
|
-
}
|
|
11483
|
-
});
|
|
11484
|
-
const mesh = new Mesh(new PlaneGeometry(2, 2), material);
|
|
11485
|
-
mesh.frustumCulled = false;
|
|
11486
|
-
return {
|
|
11487
|
-
mesh,
|
|
11488
|
-
uniforms: material.uniforms
|
|
11489
|
-
};
|
|
11490
|
-
}
|
|
11491
|
-
//#endregion
|
|
11492
|
-
//#region src/3d/water/shaders.ts
|
|
11493
|
-
/**
|
|
11494
|
-
* The Water3D 'fancy' shaders — ported VERBATIM from
|
|
11495
|
-
* vibe-starter-3d-environment `src/components/water/shader/water.{vert,frag}.glsl`
|
|
11496
|
-
* (9-iteration simplex-2D FBM displacement with analytic finite-difference
|
|
11497
|
-
* normals and an 85→98% edge-fade ring; trough/surface/peak elevation ramp,
|
|
11498
|
-
* fresnel-mixed cube-map reflection, optional depth-texture shoreline foam
|
|
11499
|
-
* with hard above-water discard). Incanto deviations, each marked inline:
|
|
11500
|
-
*
|
|
11501
|
-
* - `uWaterCenter` (NEW): the source normalized world positions by the raw
|
|
11502
|
-
* scale, which only works for water sitting at the world origin — we
|
|
11503
|
-
* subtract the node's world center so positioned/parented water keeps its
|
|
11504
|
-
* edge fade and color ramp. `uScale` carries HALF extents (x=w/2, y=d/2,
|
|
11505
|
-
* z=d/2) so the shader text stays unchanged.
|
|
11506
|
-
* - `uRipples[8]` + `getRippleHeight` (NEW): character-interaction ring waves
|
|
11507
|
-
* (see ./interaction.ts — constants mirrored there). Displacement AND
|
|
11508
|
-
* normals include them via `getHeight`.
|
|
11509
|
-
* - `uUseEnvironmentMap` gate: the source declared it but never read it; we
|
|
11510
|
-
* multiply it into the fresnel mix so water without a live reflection
|
|
11511
|
-
* (headless, `reflection: false`, first frame) shades flat instead of black.
|
|
11512
|
-
* - Fresnel FIX: the source's `dot(viewDirection, normal)` (camera→surface vs
|
|
11513
|
-
* up normal) is negative at every above-water angle, pinning fresnel at max
|
|
11514
|
-
* — the surface was a constant 3/4 mirror, so the cube map smeared across
|
|
11515
|
-
* everything. We dot the negated view direction (true Schlick shape) and
|
|
11516
|
-
* clamp reflectivity to 0.75 so grazing angles keep some water colour.
|
|
11517
|
-
* - Reflection sampling FIXES: no `.x` flip (only LOADED cube textures need
|
|
11518
|
-
* it; CubeCamera targets sample with three's flipEnvMap=+1 convention), and
|
|
11519
|
-
* rays are folded above the horizon (`max(abs(y), 0.1)`) because the lower
|
|
11520
|
-
* hemisphere of a water-centered cube map is parallax garbage.
|
|
11521
|
-
* - World-space `vNormal` FIX: the source ran the world-space analytic normal
|
|
11522
|
-
* through `normalMatrix` (a VIEW-space transform) and then dotted it against
|
|
11523
|
-
* world-space view/sun vectors — approximately right only for a level
|
|
11524
|
-
* camera. We pass the world normal straight through; fresnel and the new
|
|
11525
|
-
* sun glint stay put when the camera rolls.
|
|
11526
|
-
*
|
|
11527
|
-
* Water v2 additions (each marked `incanto v2` inline):
|
|
11528
|
-
* - Sun specular: tight Blinn-Phong glint + broad soft sheen off the
|
|
11529
|
-
* detail-perturbed normal vs `uSunDirection` — the sparkling light path.
|
|
11530
|
-
* - Animated detail normals: two procedural high-frequency simplex gradient
|
|
11531
|
-
* layers (different scales/directions/speeds, finite-differenced — no
|
|
11532
|
-
* texture asset) perturb the analytic vertex normal, `uDetailStrength`.
|
|
11533
|
-
* - Beer's-law absorption: `exp(-uAbsorption · waterViewDepth)` blends the
|
|
11534
|
-
* refracted scene into the ramp color — clear turquoise shallows, dark
|
|
11535
|
-
* depths. Depth comes from the always-on (when rendered) scene pre-pass,
|
|
11536
|
-
* gated by `uUseSceneDepth` so headless/first-frame shading stays flat.
|
|
11537
|
-
* - Screen-space refraction: the same pre-pass' color target (`uSceneColor`)
|
|
11538
|
-
* sampled with a detail-normal offset — submerged geometry shimmers.
|
|
11539
|
-
*
|
|
11540
|
-
* Atmosphere additions (marked `incanto atmosphere`): the fragment ends with
|
|
11541
|
-
* three's `tonemapping_fragment` + `colorspace_fragment` + `fog_fragment`
|
|
11542
|
-
* chunks (the SAME output stage the built-in materials run), and both stages
|
|
11543
|
-
* carry the standard fog pars/vertex chunks — so ACES tone mapping,
|
|
11544
|
-
* sRGB output and `environment.fog` hit water exactly like the terrain.
|
|
11545
|
-
*/
|
|
11546
|
-
const WATER_VERT = `
|
|
11547
|
-
precision highp float;
|
|
11548
|
-
|
|
11549
|
-
// incanto atmosphere: three's linear fog rides the standard chunks (compiled
|
|
11550
|
-
// out unless the scene declares fog — USE_FOG comes from material.fog)
|
|
11551
|
-
#include <fog_pars_vertex>
|
|
11552
|
-
|
|
11553
|
-
const float WAVE_WORLD_REF = 128.0;
|
|
11554
|
-
|
|
11555
|
-
uniform float uTime;
|
|
11556
|
-
|
|
11557
|
-
uniform float uWavesAmplitude;
|
|
11558
|
-
uniform float uWavesSpeed;
|
|
11559
|
-
uniform float uWavesFrequency;
|
|
11560
|
-
uniform float uWavesPersistence;
|
|
11561
|
-
uniform float uWavesLacunarity;
|
|
11562
|
-
uniform float uWavesIterations;
|
|
11563
|
-
uniform vec3 uScale;
|
|
11564
|
-
// incanto: world-anchor + interaction ripples (x, z, age, amplitude)
|
|
11565
|
-
uniform vec3 uWaterCenter;
|
|
11566
|
-
uniform vec4 uRipples[8];
|
|
11567
|
-
// incanto swell: directional traveling wave trains (0 = off — legacy look).
|
|
11568
|
-
// uSwellDir is a unit XZ vector; amplitude in meters; wavelength in meters.
|
|
11569
|
-
uniform float uSwellAmplitude;
|
|
11570
|
-
uniform vec2 uSwellDir;
|
|
11571
|
-
uniform float uSwellWavelength;
|
|
11572
|
-
// incanto shore: the SWASH SURGE shares the shore-waves dial — a very long
|
|
11573
|
-
// slow bulge marching shoreward; ±9 cm of water height walks the waterline
|
|
11574
|
-
// meters up and down a gentle beach (the lapping runup).
|
|
11575
|
-
uniform float uShoreWaves;
|
|
11576
|
-
// incanto presets: the edge-fade ring exists to melt BIG water into terrain
|
|
11577
|
-
// shores — inside an exact-walled basin (pool/pond) it carves a visible
|
|
11578
|
-
// elliptical cliff instead, so those presets turn it off.
|
|
11579
|
-
uniform float uEdgeFadeOn;
|
|
11580
|
-
|
|
11581
|
-
varying vec3 vNormal;
|
|
11582
|
-
varying vec3 vWorldPosition;
|
|
11583
|
-
varying vec4 vScreenPosition;
|
|
11584
|
-
|
|
11585
|
-
// Simplex 3D Noise
|
|
11586
|
-
// by Ian McEwan, Stefan Gustavson (https://github.com/stegu/webgl-noise)
|
|
11587
|
-
//
|
|
11588
|
-
vec4 permute(vec4 x) {
|
|
11589
|
-
return mod(((x * 34.0) + 1.0) * x, 289.0);
|
|
11590
|
-
}
|
|
11591
|
-
vec4 taylorInvSqrt(vec4 r) {
|
|
11592
|
-
return 1.79284291400159 - 0.85373472095314 * r;
|
|
11593
|
-
}
|
|
11594
|
-
|
|
11595
|
-
// Simplex 2D noise
|
|
11596
|
-
//
|
|
11597
|
-
vec3 permute(vec3 x) {
|
|
11598
|
-
return mod(((x * 34.0) + 1.0) * x, 289.0);
|
|
11599
|
-
}
|
|
11600
|
-
|
|
11601
|
-
float snoise(vec2 v) {
|
|
11602
|
-
const vec4 C = vec4(0.211324865405187, 0.366025403784439, -0.577350269189626, 0.024390243902439);
|
|
11603
|
-
vec2 i = floor(v + dot(v, C.yy));
|
|
11604
|
-
vec2 x0 = v - i + dot(i, C.xx);
|
|
11605
|
-
vec2 i1;
|
|
11606
|
-
i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
|
|
11607
|
-
vec4 x12 = x0.xyxy + C.xxzz;
|
|
11608
|
-
x12.xy -= i1;
|
|
11609
|
-
i = mod(i, 289.0);
|
|
11610
|
-
vec3 p = permute(permute(i.y + vec3(0.0, i1.y, 1.0)) + i.x + vec3(0.0, i1.x, 1.0));
|
|
11611
|
-
vec3 m = max(0.5 - vec3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), 0.0);
|
|
11612
|
-
m = m * m;
|
|
11613
|
-
m = m * m;
|
|
11614
|
-
vec3 x = 2.0 * fract(p * C.www) - 1.0;
|
|
11615
|
-
vec3 h = abs(x) - 0.5;
|
|
11616
|
-
vec3 ox = floor(x + 0.5);
|
|
11617
|
-
vec3 a0 = x - ox;
|
|
11618
|
-
m *= 1.79284291400159 - 0.85373472095314 * (a0 * a0 + h * h);
|
|
11619
|
-
vec3 g;
|
|
11620
|
-
g.x = a0.x * x0.x + h.x * x0.y;
|
|
11621
|
-
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
|
|
11622
|
-
return 130.0 * dot(m, g);
|
|
11623
|
-
}
|
|
11624
|
-
|
|
11625
|
-
// Helper function to calculate elevation at any point
|
|
11626
|
-
float getElevation(float x, float z) {
|
|
11627
|
-
// World-metre wavelength: the source normalized by the water's own size,
|
|
11628
|
-
// which shrinks wavelengths on small pools into storm spikes. A fixed
|
|
11629
|
-
// 128 m reference reproduces the source's look (256 m lake) at ANY size.
|
|
11630
|
-
vec2 pos = vec2(x / WAVE_WORLD_REF, z / WAVE_WORLD_REF);
|
|
11631
|
-
|
|
11632
|
-
float elevation = 0.0;
|
|
11633
|
-
float amplitude = 1.0;
|
|
11634
|
-
float frequency = uWavesFrequency;
|
|
11635
|
-
vec2 p = pos.xy;
|
|
11636
|
-
|
|
11637
|
-
for(float i = 0.0; i < uWavesIterations; i++) {
|
|
11638
|
-
float noiseValue = snoise(p * frequency + uTime * uWavesSpeed);
|
|
11639
|
-
elevation += amplitude * noiseValue;
|
|
11640
|
-
amplitude *= uWavesPersistence;
|
|
11641
|
-
frequency *= uWavesLacunarity;
|
|
11642
|
-
}
|
|
11643
|
-
|
|
11644
|
-
elevation *= uWavesAmplitude;
|
|
11645
|
-
|
|
11646
|
-
return elevation;
|
|
11647
|
-
}
|
|
11648
|
-
|
|
11649
|
-
// incanto: expanding character-interaction rings — constants mirror
|
|
11650
|
-
// RIPPLE_K/W/DECAY/SPREAD in src/3d/water/interaction.ts (keep in sync).
|
|
11651
|
-
float getRippleHeight(vec2 p) {
|
|
11652
|
-
float h = 0.0;
|
|
11653
|
-
for (int i = 0; i < 8; i++) {
|
|
11654
|
-
vec4 r = uRipples[i];
|
|
11655
|
-
if (r.w <= 0.0) continue;
|
|
11656
|
-
float d = distance(p, r.xy);
|
|
11657
|
-
h += r.w * sin(6.0 * d - 8.0 * r.z) * exp(-1.1 * r.z) * exp(-0.15 * d * d);
|
|
11658
|
-
}
|
|
11659
|
-
return h;
|
|
11660
|
-
}
|
|
11661
|
-
|
|
11662
|
-
// incanto swell: one peaked traveling wave train. Deep-water dispersion
|
|
11663
|
-
// (c = sqrt(g/k)) gives each wavelength its REAL speed, so short chop
|
|
11664
|
-
// overtakes long swell exactly like the open sea. The pow-shaped profile
|
|
11665
|
-
// sharpens crests and flattens troughs (a height-only Gerstner stand-in —
|
|
11666
|
-
// no XZ pinch, so the analytic finite-difference normals stay exact).
|
|
11667
|
-
float swellWave(vec2 p, vec2 dir, float lambda, float amp, float sharp) {
|
|
11668
|
-
float k = 6.2831853 / max(lambda, 1.0);
|
|
11669
|
-
float c = sqrt(9.8 / k);
|
|
11670
|
-
float s = 0.5 + 0.5 * sin(dot(p, dir) * k - uTime * c * k);
|
|
11671
|
-
return amp * (pow(s, sharp) * 2.0 - 1.0);
|
|
11672
|
-
}
|
|
11673
|
-
|
|
11674
|
-
// incanto swell: three trains — the primary + two shorter crossing sets
|
|
11675
|
-
// (+25° / −19°) so crest lines stay broken and organic, never a zebra field.
|
|
11676
|
-
float getSwell(vec2 p) {
|
|
11677
|
-
if (uSwellAmplitude <= 0.0) return 0.0;
|
|
11678
|
-
vec2 d1 = uSwellDir;
|
|
11679
|
-
vec2 d2 = normalize(vec2(d1.x * 0.906 - d1.y * 0.423, d1.x * 0.423 + d1.y * 0.906));
|
|
11680
|
-
vec2 d3 = normalize(vec2(d1.x * 0.946 + d1.y * 0.326, -d1.x * 0.326 + d1.y * 0.946));
|
|
11681
|
-
return swellWave(p, d1, uSwellWavelength, uSwellAmplitude * 0.60, 2.6)
|
|
11682
|
-
+ swellWave(p, d2, uSwellWavelength * 0.53, uSwellAmplitude * 0.27, 2.2)
|
|
11683
|
-
+ swellWave(p, d3, uSwellWavelength * 0.31, uSwellAmplitude * 0.16, 1.8);
|
|
11684
|
-
}
|
|
11685
|
-
|
|
11686
|
-
// incanto: waves + swell + ripples — used for displacement AND the normal
|
|
11687
|
-
// samples so every layer catches the light like real waves.
|
|
11688
|
-
// incanto shore: two incommensurate long surges — the waterline breathes in
|
|
11689
|
-
// wave-sets instead of a metronome tide
|
|
11690
|
-
float getSwash(vec2 p) {
|
|
11691
|
-
if (uShoreWaves <= 0.0) return 0.0;
|
|
11692
|
-
float along = dot(p, uSwellDir);
|
|
11693
|
-
return uShoreWaves *
|
|
11694
|
-
(0.06 * sin(along * 0.045 - uTime * 0.42) + 0.045 * sin(along * 0.11 - uTime * 0.71 + 1.3));
|
|
11695
|
-
}
|
|
11696
|
-
|
|
11697
|
-
float getHeight(float x, float z) {
|
|
11698
|
-
return getElevation(x, z) + getSwell(vec2(x, z)) + getSwash(vec2(x, z)) +
|
|
11699
|
-
getRippleHeight(vec2(x, z));
|
|
11700
|
-
}
|
|
11701
|
-
|
|
11702
|
-
void main() {
|
|
11703
|
-
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
|
|
11704
|
-
|
|
11705
|
-
float elevation = getHeight(modelPosition.x, modelPosition.z);
|
|
11706
|
-
modelPosition.y += elevation;
|
|
11707
|
-
|
|
11708
|
-
// Height adjustment to smooth water edges
|
|
11709
|
-
// Lower water height as it approaches scale boundaries
|
|
11710
|
-
// incanto: measured from the node's center, not the world origin
|
|
11711
|
-
vec2 normalizedPos = (modelPosition.xz - uWaterCenter.xz) / vec2(uScale.x, uScale.z);
|
|
11712
|
-
float distanceFromCenter = length(normalizedPos);
|
|
11713
|
-
|
|
11714
|
-
// Gradually lower water height near edges
|
|
11715
|
-
float edgeFadeStart = 0.85; // Start fade from 85% point
|
|
11716
|
-
float edgeFadeEnd = 0.98; // Completely lowered at 98% point
|
|
11717
|
-
|
|
11718
|
-
if (uEdgeFadeOn > 0.5 && distanceFromCenter >= edgeFadeStart) {
|
|
11719
|
-
float fadeAmount = smoothstep(edgeFadeStart, edgeFadeEnd, distanceFromCenter);
|
|
11720
|
-
float heightReduction = fadeAmount * 0.5; // Lower up to 0.5 units maximum
|
|
11721
|
-
modelPosition.y -= heightReduction;
|
|
11722
|
-
}
|
|
11723
|
-
|
|
11724
|
-
// Adjust appropriate eps value according to scale
|
|
11725
|
-
float eps = 0.001 * max(uScale.x, uScale.z);
|
|
11726
|
-
|
|
11727
|
-
// Calculate normal using partial derivatives with scaled eps
|
|
11728
|
-
vec3 tangent = normalize(vec3(eps, getHeight(modelPosition.x + eps, modelPosition.z) - elevation, 0.0));
|
|
11729
|
-
vec3 bitangent = normalize(vec3(0.0, getHeight(modelPosition.x, modelPosition.z + eps) - elevation, eps));
|
|
11730
|
-
vec3 objectNormal = normalize(cross(bitangent, tangent));
|
|
11731
|
-
|
|
11732
|
-
// incanto: tangent/bitangent above are WORLD-space — pass the world normal
|
|
11733
|
-
// straight through (the source's normalMatrix mapped it into VIEW space and
|
|
11734
|
-
// then dotted it against world vectors in the fragment shader).
|
|
11735
|
-
vNormal = objectNormal;
|
|
11736
|
-
vWorldPosition = modelPosition.xyz;
|
|
11737
|
-
|
|
11738
|
-
gl_Position = projectionMatrix * viewMatrix * modelPosition;
|
|
11739
|
-
vScreenPosition = gl_Position;
|
|
11740
|
-
|
|
11741
|
-
// incanto atmosphere: fog depth from the displaced surface position
|
|
11742
|
-
vec4 mvPosition = viewMatrix * modelPosition;
|
|
11743
|
-
#include <fog_vertex>
|
|
11744
|
-
}
|
|
11745
|
-
`;
|
|
11746
|
-
const WATER_FRAG = `
|
|
11747
|
-
precision highp float;
|
|
11748
|
-
|
|
11749
|
-
// incanto atmosphere: standard fog chunk (compiled out without scene fog)
|
|
11750
|
-
#include <fog_pars_fragment>
|
|
11751
|
-
|
|
11752
|
-
uniform float uOpacity;
|
|
11753
|
-
|
|
11754
|
-
uniform vec3 uTroughColor;
|
|
11755
|
-
uniform vec3 uSurfaceColor;
|
|
11756
|
-
uniform vec3 uPeakColor;
|
|
11757
|
-
|
|
11758
|
-
uniform float uPeakThreshold;
|
|
11759
|
-
uniform float uPeakTransition;
|
|
11760
|
-
uniform float uTroughThreshold;
|
|
11761
|
-
uniform float uTroughTransition;
|
|
11762
|
-
|
|
11763
|
-
uniform float uFresnelScale;
|
|
11764
|
-
uniform float uFresnelPower;
|
|
11765
|
-
uniform vec3 uScale;
|
|
11766
|
-
// incanto: world-anchor (the color ramp is relative to the waterline)
|
|
11767
|
-
uniform vec3 uWaterCenter;
|
|
11768
|
-
// incanto: 0 until the first cube render — the source declared but never read it
|
|
11769
|
-
uniform float uUseEnvironmentMap;
|
|
11770
|
-
|
|
11771
|
-
// incanto v2: sun specular (glint + sheen) off the detail-perturbed normal
|
|
11772
|
-
uniform vec3 uSunDirection;
|
|
11773
|
-
uniform vec3 uSunColor;
|
|
11774
|
-
uniform float uSunIntensity;
|
|
11775
|
-
// incanto v2: animated procedural detail-normal strength
|
|
11776
|
-
uniform float uDetailStrength;
|
|
11777
|
-
// incanto v2: Beer's-law absorption (per meter of water along the view ray)
|
|
11778
|
-
uniform float uAbsorption;
|
|
11779
|
-
// incanto v2: scene pre-pass gates — 0 headless / before the first render
|
|
11780
|
-
uniform float uUseSceneDepth;
|
|
11781
|
-
uniform float uUseRefraction;
|
|
11782
|
-
// incanto v2: half-res scene color grab (the refracted underwater view).
|
|
11783
|
-
// grab-clip: Water3D renders this pass clipped at the waterline, so it holds
|
|
11784
|
-
// ONLY underwater geometry — refraction may sample it freely (the halo
|
|
11785
|
-
// guards this freed us from lived here; see _onRender3D). uDepthTexture is
|
|
11786
|
-
// NOT clipped (a depth-only completion pass restores the above-water scene),
|
|
11787
|
-
// so depth always means the true nearest occluder — 'no sample' never gets
|
|
11788
|
-
// mistaken for 'open water' (ref-26's shoreline seam).
|
|
11789
|
-
uniform sampler2D uSceneColor;
|
|
11790
|
-
|
|
11791
|
-
// Uniforms for foam effects
|
|
11792
|
-
uniform float uEnableFoam;
|
|
11793
|
-
uniform vec3 uFoamColor;
|
|
11794
|
-
uniform float uFoamIntensity;
|
|
11795
|
-
uniform float uFoamRange;
|
|
11796
|
-
uniform float uFoamScale;
|
|
11797
|
-
uniform sampler2D uDepthTexture;
|
|
11798
|
-
uniform float uCameraNear;
|
|
11799
|
-
uniform float uCameraFar;
|
|
11800
|
-
uniform float uTime;
|
|
11801
|
-
|
|
11802
|
-
// incanto: splash WHITEWATER — the ripple buffer (also bound in the vertex
|
|
11803
|
-
// shader) drives expanding foam on the surface; uSplashFoam = Water3D.splash.
|
|
11804
|
-
uniform vec4 uRipples[8];
|
|
11805
|
-
uniform float uRippleFoam[8];
|
|
11806
|
-
uniform float uSplashFoam;
|
|
11807
|
-
|
|
11808
|
-
// incanto swell: open-water WHITECAPS — foam breaking on the highest crests,
|
|
11809
|
-
// independent of the shoreline depth mask. uWaveEnvelope is the JS-computed
|
|
11810
|
-
// total wave amplitude (FBM + swell) so the crest mask normalizes by the
|
|
11811
|
-
// actual sea state, not the surface's extent. uSwellAmplitude gates the
|
|
11812
|
-
// swell-only color work (crest banding + wave-face shading).
|
|
11813
|
-
uniform float uWhitecaps;
|
|
11814
|
-
uniform float uWaveEnvelope;
|
|
11815
|
-
uniform float uSwellAmplitude;
|
|
11816
|
-
// incanto shore: BREAKERS — foam fronts marching down the depth gradient
|
|
11817
|
-
// toward the beach. Phase rides the VERTICAL water depth, so the fronts hug
|
|
11818
|
-
// the shore's depth contours automatically and roll shoreward with time.
|
|
11819
|
-
uniform float uShoreWaves;
|
|
11820
|
-
|
|
11821
|
-
// incanto presets: above-water CAUSTICS — the dancing light web on the
|
|
11822
|
-
// submerged bottom, seen THROUGH the surface (the pool/shallow-sea look).
|
|
11823
|
-
// Intensity 0 = off. The pattern fn is shared with the underwater composite.
|
|
11824
|
-
uniform float uCausticsAbove;
|
|
11825
|
-
// incanto presets: per-preset reflection ceiling (the old hard 0.6) — lakes
|
|
11826
|
-
// push toward mirror, pools stay glassy-clear.
|
|
11827
|
-
uniform float uReflectivityMax;
|
|
11828
|
-
|
|
11829
|
-
${CAUSTIC_GLSL}
|
|
11830
|
-
|
|
11831
|
-
// 🎨 Contact surface color customization uniforms
|
|
11832
|
-
uniform vec3 uEdgeColor;
|
|
11833
|
-
uniform float uEdgeIntensity;
|
|
11834
|
-
uniform float uEdgeWidth;
|
|
11835
|
-
|
|
11836
|
-
varying vec3 vNormal;
|
|
11837
|
-
varying vec3 vWorldPosition;
|
|
11838
|
-
varying vec4 vScreenPosition;
|
|
11839
|
-
|
|
11840
|
-
uniform samplerCube uEnvironmentMap;
|
|
11841
|
-
// incanto MIRROR: a planar reflection rendered from the camera mirrored across
|
|
11842
|
-
// the water plane. A water-centered cube map can only carry DISTANT content —
|
|
11843
|
-
// the shoreline trees, the boat, the player standing at the edge are exactly
|
|
11844
|
-
// what a still lake is supposed to show, and only a planar pass has them.
|
|
11845
|
-
// uMirrorMatrix projects a world position into that render's UV, so the sample
|
|
11846
|
-
// is self-consistent no matter where the fragment sits on the surface.
|
|
11847
|
-
uniform sampler2D uMirrorMap;
|
|
11848
|
-
uniform mat4 uMirrorMatrix;
|
|
11849
|
-
uniform float uUseMirror;
|
|
11850
|
-
uniform float uMirrorDistort;
|
|
11851
|
-
|
|
11852
|
-
// Noise function (for foam patterns)
|
|
11853
|
-
float hash(vec2 p) {
|
|
11854
|
-
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
|
|
11855
|
-
}
|
|
11856
|
-
|
|
11857
|
-
float noise(vec2 p) {
|
|
11858
|
-
vec2 i = floor(p);
|
|
11859
|
-
vec2 f = fract(p);
|
|
11860
|
-
f = f * f * (3.0 - 2.0 * f);
|
|
11861
|
-
|
|
11862
|
-
float a = hash(i);
|
|
11863
|
-
float b = hash(i + vec2(1.0, 0.0));
|
|
11864
|
-
float c = hash(i + vec2(0.0, 1.0));
|
|
11865
|
-
float d = hash(i + vec2(1.0, 1.0));
|
|
11866
|
-
|
|
11867
|
-
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
|
|
11868
|
-
}
|
|
11869
|
-
|
|
11870
|
-
// incanto v2: the SAME simplex the vertex waves use (Ian McEwan, Stefan
|
|
11871
|
-
// Gustavson — https://github.com/stegu/webgl-noise), here feeding the
|
|
11872
|
-
// procedural detail normals so the package stays texture-free.
|
|
11873
|
-
vec3 permute(vec3 x) {
|
|
11874
|
-
return mod(((x * 34.0) + 1.0) * x, 289.0);
|
|
11875
|
-
}
|
|
11876
|
-
|
|
11877
|
-
float snoise(vec2 v) {
|
|
11878
|
-
const vec4 C = vec4(0.211324865405187, 0.366025403784439, -0.577350269189626, 0.024390243902439);
|
|
11879
|
-
vec2 i = floor(v + dot(v, C.yy));
|
|
11880
|
-
vec2 x0 = v - i + dot(i, C.xx);
|
|
11881
|
-
vec2 i1;
|
|
11882
|
-
i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
|
|
11883
|
-
vec4 x12 = x0.xyxy + C.xxzz;
|
|
11884
|
-
x12.xy -= i1;
|
|
11885
|
-
i = mod(i, 289.0);
|
|
11886
|
-
vec3 p = permute(permute(i.y + vec3(0.0, i1.y, 1.0)) + i.x + vec3(0.0, i1.x, 1.0));
|
|
11887
|
-
vec3 m = max(0.5 - vec3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), 0.0);
|
|
11888
|
-
m = m * m;
|
|
11889
|
-
m = m * m;
|
|
11890
|
-
vec3 x = 2.0 * fract(p * C.www) - 1.0;
|
|
11891
|
-
vec3 h = abs(x) - 0.5;
|
|
11892
|
-
vec3 ox = floor(x + 0.5);
|
|
11893
|
-
vec3 a0 = x - ox;
|
|
11894
|
-
m *= 1.79284291400159 - 0.85373472095314 * (a0 * a0 + h * h);
|
|
11895
|
-
vec3 g;
|
|
11896
|
-
g.x = a0.x * x0.x + h.x * x0.y;
|
|
11897
|
-
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
|
|
11898
|
-
return 130.0 * dot(m, g);
|
|
11899
|
-
}
|
|
11900
|
-
|
|
11901
|
-
// refinement pass: Beer's law per CHANNEL — red is absorbed first, blue/green
|
|
11902
|
-
// linger, so a sandy bottom turns turquoise long before it turns deep blue
|
|
11903
|
-
// (the wide turquoise→blue gradient of a real tropical shallows). The scalar
|
|
11904
|
-
// uAbsorption prop scales this fixed spectral shape.
|
|
11905
|
-
const vec3 ABSORB_TINT = vec3(2.4, 0.7, 0.5);
|
|
11906
|
-
|
|
11907
|
-
// incanto v2: finite-difference gradient of the simplex height field
|
|
11908
|
-
vec2 noiseGrad(vec2 p) {
|
|
11909
|
-
const float e = 0.06;
|
|
11910
|
-
float h = snoise(p);
|
|
11911
|
-
return vec2(snoise(p + vec2(e, 0.0)) - h, snoise(p + vec2(0.0, e)) - h) / e;
|
|
11912
|
-
}
|
|
11913
|
-
|
|
11914
|
-
// incanto v2: animated detail-normal layers — the high-frequency perturbation
|
|
11915
|
-
// that makes the surface feel LIQUID up close (the vertex FBM is far too
|
|
11916
|
-
// coarse to catch light per-pixel). Per-layer distance fades stay (procedural
|
|
11917
|
-
// noise has no mips — past its range a layer aliases into white speckle).
|
|
11918
|
-
//
|
|
11919
|
-
// refinement pass 3: ANISOTROPIC wind-stretched ripple. The old isotropic
|
|
11920
|
-
// simplex layers perturbed the normal in soft round BLOBS — under the sun
|
|
11921
|
-
// they read as sticky slime / 액체괴물, never as water. Real wind ripple is
|
|
11922
|
-
// (a) stretched ALONG the wind, (b) much finer than swell, and (c) crisper
|
|
11923
|
-
// than smooth lumps. So: the noise domain is compressed along a wind
|
|
11924
|
-
// direction and the two fine layers run in CROSSING directions (±~25° off
|
|
11925
|
-
// the wind). dapple pass (ref-27): full ridge sharpening (1-|n|) at 2.8×
|
|
11926
|
-
// stretch folded the field into LONG PARALLEL CREASES — fabric wrinkles,
|
|
11927
|
-
// not water. Stretch comes down to 1.7×, the ridge transform is BLENDED
|
|
11928
|
-
// ~half toward plain simplex (soft short crests, no continuous fold lines),
|
|
11929
|
-
// the mid layer's frequency rises so features shorten, and the two crossing
|
|
11930
|
-
// layers sit close in weight so no single direction dominates. One coarse
|
|
11931
|
-
// smooth layer keeps long-swell coherence underneath. The line this walks:
|
|
11932
|
-
// PR-J's isotropic goop ←→ the crease field this replaces.
|
|
11933
|
-
// ============ the WATER-FEEL rewrite (the syrup fix) ============
|
|
11934
|
-
// The old detail layers had two viscosity bugs that made every surface read
|
|
11935
|
-
// as 꿀렁꿀렁한 gel instead of water:
|
|
11936
|
-
// 1. WRONG SPEED: each ripple layer drifted ~10× slower than a real wave
|
|
11937
|
-
// of its wavelength. Deep-water dispersion says a λ-meter wave travels
|
|
11938
|
-
// at c = sqrt(gλ/2π) — a 40 cm ripple crosses ITSELF ~2.7× per second.
|
|
11939
|
-
// Slow fine ripples are exactly what honey looks like.
|
|
11940
|
-
// 2. PATTERN SLIDING: a translated noise field reads as one sticker being
|
|
11941
|
-
// dragged (cellophane). Real ripple CHURNS — crests appear, interfere
|
|
11942
|
-
// and die. The fine octaves are therefore INTERFERENCE PAIRS: the same
|
|
11943
|
-
// wavelength read twice in slightly split directions and speeds; their
|
|
11944
|
-
// sum morphs continuously instead of sliding.
|
|
11945
|
-
// Four octaves, each advected at its REAL phase speed (in noise-space,
|
|
11946
|
-
// phase = c/λ per second — short waves flicker fast, the tell of thin
|
|
11947
|
-
// mobile water): λ 6.0 m → 0.51/s · 2.2 m → 0.84/s · 0.9 m → 1.31/s ·
|
|
11948
|
-
// 0.38 m → 2.02/s.
|
|
11949
|
-
const vec2 WIND_DIR = vec2(0.8192, 0.5735);
|
|
11950
|
-
|
|
11951
|
-
vec2 octaveSlope(vec2 p, vec2 dir, float lambda, float t) {
|
|
11952
|
-
// advect ALONG the octave's travel direction at the dispersion speed
|
|
11953
|
-
float phase = sqrt(1.56 / lambda) * t; // c/λ = sqrt(g/(2πλ)) · … ≈ sqrt(1.56/λ)
|
|
11954
|
-
return noiseGrad(p / lambda + dir * phase);
|
|
11955
|
-
}
|
|
11956
|
-
// interference pair: same wavelength, directions split ±14°, speeds split
|
|
11957
|
-
// ±12% — the sum churns (crests are born and die) instead of sliding.
|
|
11958
|
-
vec2 octavePair(vec2 p, vec2 dir, float lambda, float t) {
|
|
11959
|
-
vec2 dA = normalize(dir + 0.25 * vec2(-dir.y, dir.x));
|
|
11960
|
-
vec2 dB = normalize(dir - 0.25 * vec2(-dir.y, dir.x));
|
|
11961
|
-
float phase = sqrt(1.56 / lambda) * t;
|
|
11962
|
-
return noiseGrad(p / lambda + dA * (phase * 1.12))
|
|
11963
|
-
+ noiseGrad(p / lambda * 1.07 + dB * (phase * 0.88) + 17.0);
|
|
11964
|
-
}
|
|
11965
|
-
|
|
11966
|
-
vec3 detailNormal(vec3 n, vec2 worldXZ, float strength, float camDist) {
|
|
11967
|
-
float fadeCoarse = 1.0 - smoothstep(90.0, 420.0, camDist);
|
|
11968
|
-
float fadeMid = 1.0 - smoothstep(40.0, 170.0, camDist);
|
|
11969
|
-
float fadeFine = 1.0 - smoothstep(18.0, 80.0, camDist);
|
|
11970
|
-
float fadeMicro = 1.0 - smoothstep(8.0, 34.0, camDist);
|
|
11971
|
-
vec2 dMid = normalize(vec2(0.62, 0.78));
|
|
11972
|
-
vec2 slope =
|
|
11973
|
-
octaveSlope(worldXZ, WIND_DIR, 6.0, uTime) * (0.085 * fadeCoarse)
|
|
11974
|
-
+ octaveSlope(worldXZ, dMid, 2.2, uTime) * (0.10 * fadeMid)
|
|
11975
|
-
+ octavePair(worldXZ, WIND_DIR, 0.9, uTime) * (0.055 * fadeFine)
|
|
11976
|
-
+ octavePair(worldXZ, dMid, 0.38, uTime) * (0.042 * fadeMicro);
|
|
11977
|
-
slope *= strength * 3.0; // spectrum norm: default strength 0.26 ≈ calm-lively
|
|
11978
|
-
return normalize(vec3(n.x - slope.x, n.y, n.z - slope.y));
|
|
11979
|
-
}
|
|
11980
|
-
|
|
11981
|
-
// The GLOSS normal — what fresnel, the reflection and the refraction offset
|
|
11982
|
-
// read. Only the two long octaves: a mirror the size of a pixel integrates
|
|
11983
|
-
// micro-ripple away, so reflections stay smooth and ROUNDED (the glassy
|
|
11984
|
-
// pool-reference surface); the fast fine churn lives in the sun glitter and
|
|
11985
|
-
// the body shading, never as reflection speckle.
|
|
11986
|
-
vec3 glossNormal(vec3 n, vec2 worldXZ, float strength, float camDist) {
|
|
11987
|
-
float fadeCoarse = 1.0 - smoothstep(90.0, 420.0, camDist);
|
|
11988
|
-
float fadeMid = 1.0 - smoothstep(40.0, 170.0, camDist);
|
|
11989
|
-
vec2 dMid = normalize(vec2(0.62, 0.78));
|
|
11990
|
-
vec2 slope =
|
|
11991
|
-
octaveSlope(worldXZ, WIND_DIR, 6.0, uTime) * (0.085 * fadeCoarse)
|
|
11992
|
-
+ octaveSlope(worldXZ, dMid, 2.2, uTime) * (0.075 * fadeMid);
|
|
11993
|
-
slope *= strength * 3.0;
|
|
11994
|
-
return normalize(vec3(n.x - slope.x, n.y, n.z - slope.y));
|
|
11995
|
-
}
|
|
11996
|
-
|
|
11997
|
-
// Linear depth conversion
|
|
11998
|
-
float linearizeDepth(float depth, float near, float far) {
|
|
11999
|
-
float z = depth * 2.0 - 1.0;
|
|
12000
|
-
return (2.0 * near * far) / (far + near - z * (far - near));
|
|
12001
|
-
}
|
|
12002
|
-
|
|
12003
|
-
// incanto: splash WHITEWATER. The look that read as FAKE was a clean radial
|
|
12004
|
-
// disc/ring × speckle. Real foam is irregular clumps + tendrils that drift with
|
|
12005
|
-
// the water and break apart at the edges. So: a soft COVERAGE envelope (where
|
|
12006
|
-
// foam can be — an expanding patch that fades over FOAM_MAX_AGE; this is the
|
|
12007
|
-
// CPU twin rippleFoamAt, constants MIRRORED) THRESHOLDS a domain-warped
|
|
12008
|
-
// fractal noise. Dense coverage → broad froth; thinning coverage (edges / old)
|
|
12009
|
-
// raises the threshold → only the noise peaks survive → it erodes into sparse
|
|
12010
|
-
// bubbles. The warp + slow drift make it look advected, not stamped.
|
|
12011
|
-
float foamFbm(vec2 p) {
|
|
12012
|
-
float v = 0.0, a = 0.5;
|
|
12013
|
-
for (int i = 0; i < 4; i++) { v += a * noise(p); p = p * 2.02 + 7.3; a *= 0.5; }
|
|
12014
|
-
return v; // ~0..0.94
|
|
12015
|
-
}
|
|
12016
|
-
|
|
12017
|
-
float rippleFoam(vec2 p) {
|
|
12018
|
-
if (uSplashFoam < 0.5) return 0.0;
|
|
12019
|
-
float cover = 0.0;
|
|
12020
|
-
for (int i = 0; i < 8; i++) {
|
|
12021
|
-
float foam = uRippleFoam[i];
|
|
12022
|
-
if (foam <= 0.0) continue;
|
|
12023
|
-
vec4 r = uRipples[i];
|
|
12024
|
-
float age = r.z;
|
|
12025
|
-
float life = 1.0 - age / 1.6; // FOAM_MAX_AGE
|
|
12026
|
-
if (life <= 0.0) continue;
|
|
12027
|
-
float d = distance(p, r.xy);
|
|
12028
|
-
float radius = 0.4 + 1.15 * age; // FOAM_R0 + FOAM_SPREAD * age
|
|
12029
|
-
cover = max(cover, (1.0 - smoothstep(radius * 0.7, radius, d)) * foam * life);
|
|
12030
|
-
}
|
|
12031
|
-
if (cover <= 0.001) return 0.0;
|
|
12032
|
-
// organic clumps: domain-warp the fbm (swirl) + slow drift = advected foam.
|
|
12033
|
-
// FINE frequencies (premium foam is many small bubbles, not coarse blobs).
|
|
12034
|
-
vec2 flow = vec2(uTime * 0.05, uTime * -0.04);
|
|
12035
|
-
vec2 warp = vec2(foamFbm(p * 1.1 + flow), foamFbm(p * 1.1 + 4.2 - flow)) - 0.5;
|
|
12036
|
-
float pattern = foamFbm(p * 2.6 + warp * 1.4 + flow);
|
|
12037
|
-
// erode: high coverage → low threshold (denser foam); low coverage → high
|
|
12038
|
-
// threshold (only the peaks survive → delicate lacy bubbles, soft edges).
|
|
12039
|
-
// Raised floor + wide band = sparser + softer = subtler/more refined.
|
|
12040
|
-
float thresh = mix(0.36, 0.96, 1.0 - cover);
|
|
12041
|
-
float foamHere = smoothstep(thresh, thresh + 0.26, pattern);
|
|
12042
|
-
return clamp(foamHere * (0.4 + 0.5 * cover), 0.0, 1.0);
|
|
12043
|
-
}
|
|
12044
|
-
|
|
12045
|
-
// Enhanced depth-based foam effect calculation
|
|
12046
|
-
float calculateFoam() {
|
|
12047
|
-
// incanto v2: foam rides the always-on scene depth pass — both gates
|
|
12048
|
-
if (uEnableFoam < 0.5 || uUseSceneDepth < 0.5) return 0.0;
|
|
12049
|
-
|
|
12050
|
-
// Convert screen coordinates to texture coordinates
|
|
12051
|
-
vec2 screenUV = vScreenPosition.xy / vScreenPosition.w * 0.5 + 0.5;
|
|
12052
|
-
|
|
12053
|
-
// Sample scene depth value from depth texture
|
|
12054
|
-
float sceneDepth = texture2D(uDepthTexture, screenUV).r;
|
|
12055
|
-
float waterDepth = gl_FragCoord.z;
|
|
12056
|
-
|
|
12057
|
-
// Convert to linear depth
|
|
12058
|
-
float linearSceneDepth = linearizeDepth(sceneDepth, uCameraNear, uCameraFar);
|
|
12059
|
-
float linearWaterDepth = linearizeDepth(waterDepth, uCameraNear, uCameraFar);
|
|
12060
|
-
|
|
12061
|
-
// Calculate depth difference (distance between water and other objects)
|
|
12062
|
-
float depthDiff = abs(linearSceneDepth - linearWaterDepth);
|
|
12063
|
-
|
|
12064
|
-
// Generate foam - stronger in wider range
|
|
12065
|
-
float foamFactor = 1.0 - smoothstep(0.0, uFoamRange, depthDiff);
|
|
12066
|
-
foamFactor = pow(foamFactor, 0.5); // Softer decay
|
|
12067
|
-
|
|
12068
|
-
// Generate strong foam patterns with multi-layer noise
|
|
12069
|
-
vec2 foamUV = vWorldPosition.xz * uFoamScale + uTime * 0.2;
|
|
12070
|
-
float foamNoise = 0.0;
|
|
12071
|
-
|
|
12072
|
-
// Large pattern
|
|
12073
|
-
foamNoise += noise(foamUV) * 0.6;
|
|
12074
|
-
// Medium pattern
|
|
12075
|
-
foamNoise += noise(foamUV * 3.0 + uTime * 0.5) * 0.4;
|
|
12076
|
-
// Fine pattern
|
|
12077
|
-
foamNoise += noise(foamUV * 8.0 + uTime * 1.0) * 0.3;
|
|
12078
|
-
// Micro pattern
|
|
12079
|
-
foamNoise += noise(foamUV * 20.0 + uTime * 1.5) * 0.2;
|
|
12080
|
-
|
|
12081
|
-
// Normalize noise
|
|
12082
|
-
foamNoise /= 1.5;
|
|
12083
|
-
|
|
12084
|
-
// refinement pass: WISPY foam — a wide soft ramp instead of the old hard
|
|
12085
|
-
// contrast boost, and the constant edge band cut way down so shallows read
|
|
12086
|
-
// as noise-broken patches (the reference's channel foam), not a painted rim.
|
|
12087
|
-
float foam = foamFactor * foamNoise * uFoamIntensity;
|
|
12088
|
-
foam = smoothstep(0.12, 0.95, foam);
|
|
12089
|
-
|
|
12090
|
-
// Additional foam layer - edge enhancement (kept faint, noise-broken — a
|
|
12091
|
-
// constant term veiled whole shallow ponds in flat white)
|
|
12092
|
-
float edgeFoam = foamFactor * foamFactor * 0.18 * (0.35 + 0.65 * foamNoise);
|
|
12093
|
-
foam = max(foam, edgeFoam);
|
|
12094
|
-
|
|
12095
|
-
// incanto v2: wave/ripple crests breaking NEAR shores kick up extra foam —
|
|
12096
|
-
// the elevation ramp's peak factor masked by the shoreline depth factor.
|
|
12097
|
-
float crest = smoothstep(uPeakThreshold, uPeakThreshold + uPeakTransition * 2.0,
|
|
12098
|
-
(vWorldPosition.y - uWaterCenter.y) / uScale.y);
|
|
12099
|
-
foam = max(foam, foamFactor * crest * (0.5 + 0.5 * foamNoise));
|
|
12100
|
-
|
|
12101
|
-
return clamp(foam, 0.0, 1.0);
|
|
12102
|
-
}
|
|
12103
|
-
|
|
12104
|
-
// Calculate smooth color blending at contact surfaces
|
|
12105
|
-
float calculateEdgeBlending() {
|
|
12106
|
-
// Contact surface effects are part of foam effects (enableFoam is required)
|
|
12107
|
-
if (uEnableFoam < 0.5 || uUseSceneDepth < 0.5 || uEdgeIntensity <= 0.0) return 0.0;
|
|
12108
|
-
|
|
12109
|
-
vec2 screenUV = vScreenPosition.xy / vScreenPosition.w * 0.5 + 0.5;
|
|
12110
|
-
float sceneDepth = texture2D(uDepthTexture, screenUV).r;
|
|
12111
|
-
float waterDepth = gl_FragCoord.z;
|
|
12112
|
-
|
|
12113
|
-
float linearSceneDepth = linearizeDepth(sceneDepth, uCameraNear, uCameraFar);
|
|
12114
|
-
float linearWaterDepth = linearizeDepth(waterDepth, uCameraNear, uCameraFar);
|
|
12115
|
-
|
|
12116
|
-
float depthDiff = abs(linearSceneDepth - linearWaterDepth);
|
|
12117
|
-
|
|
12118
|
-
// User-defined range for color blending at contact surfaces
|
|
12119
|
-
float blendRange = uFoamRange * uEdgeWidth; // Use user-defined range
|
|
12120
|
-
float blendFactor = 1.0 - smoothstep(0.0, blendRange, depthDiff);
|
|
12121
|
-
|
|
12122
|
-
return blendFactor; // 0.0 (no effect) ~ 1.0 (maximum blending)
|
|
12123
|
-
}
|
|
12124
|
-
|
|
12125
|
-
void main() {
|
|
12126
|
-
// 🔥 Fundamental solution: Handle overlapping parts with objects
|
|
12127
|
-
float depthMask = 1.0; // Default: fully render
|
|
12128
|
-
|
|
12129
|
-
// incanto v2: the scene depth pass is always-on for fancy when rendered —
|
|
12130
|
-
// the mask (and the water view depth for Beer's law below) now ride
|
|
12131
|
-
// uUseSceneDepth instead of the foam toggle.
|
|
12132
|
-
vec2 screenUV = vScreenPosition.xy / vScreenPosition.w * 0.5 + 0.5;
|
|
12133
|
-
float linearWaterDepth = linearizeDepth(gl_FragCoord.z, uCameraNear, uCameraFar);
|
|
12134
|
-
// incanto v2: meters of water along the view ray; -1 = depth unavailable
|
|
12135
|
-
float waterViewDepth = -1.0;
|
|
12136
|
-
// shoreline-seam fix: hoisted — the refraction block reconstructs the scene
|
|
12137
|
-
// sample's world height from it (grab validity, see below)
|
|
12138
|
-
float linearSceneDepth = uCameraFar;
|
|
12139
|
-
|
|
12140
|
-
if (uUseSceneDepth > 0.5) {
|
|
12141
|
-
float sceneDepth = texture2D(uDepthTexture, screenUV).r;
|
|
12142
|
-
linearSceneDepth = linearizeDepth(sceneDepth, uCameraNear, uCameraFar);
|
|
12143
|
-
|
|
12144
|
-
float depthDiff = linearWaterDepth - linearSceneDepth;
|
|
12145
|
-
waterViewDepth = max(-depthDiff, 0.0);
|
|
12146
|
-
|
|
12147
|
-
// waterline dither: on a near-flat beach one half-res depth texel spans a
|
|
12148
|
-
// wide screen block at grazing view, so these thresholds cut staircase
|
|
12149
|
-
// notches along the swash edge — jitter the boundary with world noise and
|
|
12150
|
-
// the seam breaks organic. Two octaves at LOW amplitude: a flat slope
|
|
12151
|
-
// multiplies vertical jitter into horizontal metres, so a single strong
|
|
12152
|
-
// octave carved angular value-noise chunks (the diced-radish waterline bug).
|
|
12153
|
-
float seamJitter =
|
|
12154
|
-
(noise(vWorldPosition.xz * 3.1) * 0.65 + noise(vWorldPosition.xz * 8.7) * 0.35 - 0.5) * 0.035;
|
|
12155
|
-
|
|
12156
|
-
// 🎯 Two-step approach
|
|
12157
|
-
// Step 1: Discard completely overlapping parts
|
|
12158
|
-
float hardDiscardThreshold = 0.1 + seamJitter;
|
|
12159
|
-
if (depthDiff > hardDiscardThreshold) {
|
|
12160
|
-
discard; // Completely remove definitively overlapping pixels
|
|
12161
|
-
}
|
|
12162
|
-
|
|
12163
|
-
// Step 2: Gradual transparency for border parts
|
|
12164
|
-
float softDiscardStart = -0.05 + seamJitter; // Start fade slightly ahead
|
|
12165
|
-
float softDiscardEnd = hardDiscardThreshold;
|
|
12166
|
-
|
|
12167
|
-
if (depthDiff > softDiscardStart) {
|
|
12168
|
-
depthMask = 1.0 - smoothstep(softDiscardStart, softDiscardEnd, depthDiff);
|
|
12169
|
-
// At border: 0 (fully transparent) ~ 1 (fully opaque)
|
|
12170
|
-
}
|
|
12171
|
-
}
|
|
12172
|
-
|
|
12173
|
-
// Calculate vector from camera to the vertex
|
|
12174
|
-
vec3 viewDirection = normalize(vWorldPosition - cameraPosition);
|
|
12175
|
-
// incanto v2: perturb the analytic vertex normal with the animated detail
|
|
12176
|
-
// layers — fresnel, reflection, refraction and the sun glint all read it.
|
|
12177
|
-
// refinement pass: the per-LAYER distance fades live inside detailNormal,
|
|
12178
|
-
// and fresnel/reflection read a distance-SMOOTHED normal — a pixel at range
|
|
12179
|
-
// integrates many ripples, so its fresnel must converge to the swell's, not
|
|
12180
|
-
// flicker per ripple (the old per-pixel fresnel turned the whole mid-field
|
|
12181
|
-
// into white sky-reflection sequins). The sun glint keeps the full detail
|
|
12182
|
-
// normal: distant glitter is real and reads as sparkle, not noise.
|
|
12183
|
-
float camDist = length(vWorldPosition - cameraPosition);
|
|
12184
|
-
vec3 geoNormal = normalize(vNormal);
|
|
12185
|
-
vec3 normal = detailNormal(geoNormal, vWorldPosition.xz, uDetailStrength, camDist);
|
|
12186
|
-
// refinement pass 3: the GEO normal must flatten with distance too — the
|
|
12187
|
-
// per-vertex FBM normal varies per pixel near the horizon (its features go
|
|
12188
|
-
// subpixel), and against the bright sun-side sky every flicker becomes a
|
|
12189
|
-
// white sequin. A pixel at range integrates whole wave trains: its normal
|
|
12190
|
-
// converges to flat-up, so fresnel/reflection/streak read exactly that.
|
|
12191
|
-
float geoFlat = 1.0 - 0.9 * smoothstep(150.0, 600.0, camDist);
|
|
12192
|
-
vec3 geoSmooth = normalize(mix(vec3(0.0, 1.0, 0.0), geoNormal, geoFlat));
|
|
12193
|
-
float fresnelDetail = 1.0 - 0.85 * smoothstep(20.0, 120.0, camDist);
|
|
12194
|
-
// eztree-parity pass: hoisted — fresnel AND the refraction offset read it.
|
|
12195
|
-
// crisp-water fix: NEAR the camera the reflection must read the FULL
|
|
12196
|
-
// ripple spectrum (sky reflections shatter into crisp shards at arm's
|
|
12197
|
-
// length — reading only the smooth octaves rendered soft lava-lamp blobs);
|
|
12198
|
-
// with distance it hands over to the smooth gloss so the far field never
|
|
12199
|
-
// speckles against the HDR sky.
|
|
12200
|
-
vec3 gloss = glossNormal(geoNormal, vWorldPosition.xz, uDetailStrength, camDist);
|
|
12201
|
-
vec3 crisp = mix(normal, gloss, smoothstep(6.0, 26.0, camDist));
|
|
12202
|
-
vec3 fresnelNormal = normalize(mix(geoSmooth, crisp, fresnelDetail));
|
|
12203
|
-
vec3 reflectedDirection = reflect(viewDirection, fresnelNormal);
|
|
12204
|
-
// incanto: the source flipped .x here — correct for cube textures LOADED
|
|
12205
|
-
// from images, but a left/right mirror for CubeCamera render targets
|
|
12206
|
-
// (three samples those with flipEnvMap = +1). Verified live: a marker
|
|
12207
|
-
// object's reflection appeared on the opposite side with the flip.
|
|
12208
|
-
// incanto: fold reflection rays above the horizon — the lower hemisphere
|
|
12209
|
-
// of a water-centered cube map is parallax garbage (the ground under and
|
|
12210
|
-
// around the pool) and painted ink-black wavelet patches at grazing angles;
|
|
12211
|
-
// the +y bias keeps samples off the smeared horizon seam. refinement pass:
|
|
12212
|
-
// 0.1 → 0.15 — the physical sky's brightest haze hugs the horizon, and
|
|
12213
|
-
// grazing water sampled it into a blown-white band; a touch higher samples
|
|
12214
|
-
// believable pale blue instead.
|
|
12215
|
-
reflectedDirection.y = max(abs(reflectedDirection.y), 0.15);
|
|
12216
|
-
|
|
12217
|
-
// Sample environment map to get the reflected color
|
|
12218
|
-
// refinement pass 3: soft-cap the HDR radiance — the physical sky's
|
|
12219
|
-
// horizon haze runs ≫1 and a single bright reflection pixel reads as a
|
|
12220
|
-
// white sequin after ACES; 2.5 still tone-maps near-white for real sky
|
|
12221
|
-
// reflections without letting one texel blow out.
|
|
12222
|
-
vec4 reflectionColor = textureCube(uEnvironmentMap, reflectedDirection);
|
|
12223
|
-
reflectionColor.rgb = min(reflectionColor.rgb, vec3(2.5));
|
|
12224
|
-
|
|
12225
|
-
// incanto MIRROR: where the planar pass has data it REPLACES the cube — it
|
|
12226
|
-
// is the same sky plus everything the cube map is blind to. Rays that leave
|
|
12227
|
-
// the screen have no mirror sample, so the border fade hands those back to
|
|
12228
|
-
// the cube instead of clamping a stretched edge texel across the surface.
|
|
12229
|
-
if (uUseMirror > 0.5) {
|
|
12230
|
-
vec4 mirrorClip = uMirrorMatrix * vec4(vWorldPosition, 1.0);
|
|
12231
|
-
vec2 mirrorUv = mirrorClip.xy / max(mirrorClip.w, 1e-4);
|
|
12232
|
-
// the ripple tilt bends the reflected ray: offset the sample by how far
|
|
12233
|
-
// the wavelet normal leans off flat, damped with distance so the far field
|
|
12234
|
-
// (many wavelets per pixel) stays a clean mirror instead of a jitter field
|
|
12235
|
-
vec2 lean = (fresnelNormal.xz - geoNormal.xz) * uMirrorDistort;
|
|
12236
|
-
mirrorUv += lean * (1.0 - smoothstep(20.0, 90.0, camDist));
|
|
12237
|
-
vec2 edge = smoothstep(vec2(0.0), vec2(0.06), mirrorUv) *
|
|
12238
|
-
(1.0 - smoothstep(vec2(0.94), vec2(1.0), mirrorUv));
|
|
12239
|
-
float valid = edge.x * edge.y * step(0.0, mirrorClip.w);
|
|
12240
|
-
if (valid > 0.0) {
|
|
12241
|
-
vec3 mirrored = min(texture2D(uMirrorMap, mirrorUv).rgb, vec3(2.5));
|
|
12242
|
-
reflectionColor.rgb = mix(reflectionColor.rgb, mirrored, valid);
|
|
12243
|
-
}
|
|
12244
|
-
}
|
|
12245
|
-
|
|
12246
|
-
// Calculate fresnel effect
|
|
12247
|
-
// incanto: the source dotted the camera→surface direction against the
|
|
12248
|
-
// up-facing normal — NEGATIVE at every above-water angle, so the clamp
|
|
12249
|
-
// pinned fresnel at its maximum and the whole surface was a constant-ratio
|
|
12250
|
-
// mirror. Negate it (surface→camera) for real angle-dependent fresnel:
|
|
12251
|
-
// steep view = water color, grazing view = sky mirror.
|
|
12252
|
-
// incanto atmosphere: full Schlick — the F0 floor (2%, water's real normal-
|
|
12253
|
-
// incidence reflectance) keeps a whisper of sky at steep angles while the
|
|
12254
|
-
// default exponent 5 stops the HDR sky from whitewashing the whole surface.
|
|
12255
|
-
float fresnel = uFresnelScale * (0.02 + 0.98 * pow(1.0 - clamp(dot(-viewDirection, fresnelNormal), 0.0, 1.0), uFresnelPower));
|
|
12256
|
-
|
|
12257
|
-
// Calculate scale-normalized height value
|
|
12258
|
-
// incanto: relative to the waterline, not absolute world y
|
|
12259
|
-
float normalizedElevation = (vWorldPosition.y - uWaterCenter.y) / uScale.y;
|
|
12260
|
-
|
|
12261
|
-
// Calculate transition factors using smoothstep
|
|
12262
|
-
float peakFactor = smoothstep(uPeakThreshold - uPeakTransition, uPeakThreshold + uPeakTransition, normalizedElevation);
|
|
12263
|
-
float troughFactor = smoothstep(uTroughThreshold - uTroughTransition, uTroughThreshold + uTroughTransition, normalizedElevation);
|
|
12264
|
-
|
|
12265
|
-
// Mix between trough and surface colors based on trough transition
|
|
12266
|
-
vec3 mixedColor1 = mix(uTroughColor, uSurfaceColor, troughFactor);
|
|
12267
|
-
|
|
12268
|
-
// Mix between surface and peak colors based on peak transition
|
|
12269
|
-
vec3 mixedColor2 = mix(mixedColor1, uPeakColor, peakFactor);
|
|
12270
|
-
|
|
12271
|
-
// incanto swell: the legacy ramp normalizes elevation by the surface's
|
|
12272
|
-
// HALF-EXTENT — on a real ocean plane that's hundreds of meters, so the
|
|
12273
|
-
// ramp never sees a wave. When swell is on, re-read the crest height
|
|
12274
|
-
// against the actual sea state (uWaveEnvelope) and (a) lighten crest tops
|
|
12275
|
-
// toward the peak color, (b) shade the wave FACES with a gentle N·L off
|
|
12276
|
-
// the distance-smoothed geometry normal — the lit/shadow lanes are what
|
|
12277
|
-
// make open-ocean swell READ at any distance. Both gated: swell 0 keeps
|
|
12278
|
-
// the legacy look byte-identical.
|
|
12279
|
-
if (uSwellAmplitude > 0.0) {
|
|
12280
|
-
float crestBand = smoothstep(0.3, 0.9,
|
|
12281
|
-
(vWorldPosition.y - uWaterCenter.y) / max(uWaveEnvelope, 0.001));
|
|
12282
|
-
mixedColor2 = mix(mixedColor2, uPeakColor, crestBand * 0.45);
|
|
12283
|
-
float faceShade = clamp(dot(normalize(mix(vec3(0.0, 1.0, 0.0), normalize(vNormal),
|
|
12284
|
-
1.0 - 0.9 * smoothstep(150.0, 600.0, length(vWorldPosition - cameraPosition)))),
|
|
12285
|
-
normalize(uSunDirection)), 0.0, 1.0);
|
|
12286
|
-
mixedColor2 *= 0.80 + 0.32 * faceShade;
|
|
12287
|
-
}
|
|
12288
|
-
|
|
12289
|
-
// incanto v2: Beer's-law depth absorption + screen-space refraction.
|
|
12290
|
-
// Shallow water transmits the (refracted) scene almost untouched; depth
|
|
12291
|
-
// exponentially absorbs it into the ramp color — clear turquoise edges,
|
|
12292
|
-
// dark depths. Headless / first frame: waterViewDepth stays -1 → flat ramp.
|
|
12293
|
-
vec3 bodyColor = mixedColor2;
|
|
12294
|
-
float bodyOpacity = uOpacity;
|
|
12295
|
-
if (waterViewDepth >= 0.0) {
|
|
12296
|
-
// refinement pass: per-channel transmittance (see ABSORB_TINT) — the
|
|
12297
|
-
// turquoise→deep-blue gradient comes from red dying first, not from a
|
|
12298
|
-
// hand-painted band.
|
|
12299
|
-
vec3 transmittance = exp(-uAbsorption * ABSORB_TINT * waterViewDepth);
|
|
12300
|
-
if (uUseRefraction > 0.5) {
|
|
12301
|
-
// eztree-parity pass: the offset reads ONLY the SMOOTH gloss normal —
|
|
12302
|
-
// the ridge-sharpened detail normal flips sign across every crest line
|
|
12303
|
-
// and combed the submerged scene into jaggy scratch streaks (빗살무늬);
|
|
12304
|
-
// even a 15% share re-speckled bright submerged objects.
|
|
12305
|
-
vec2 refrSlope = crisp.xz;
|
|
12306
|
-
// thickness-scaled parallax: the bottom image's refraction displacement
|
|
12307
|
-
// grows with the water column's depth (thin film ≈ no shift, deep water
|
|
12308
|
-
// ≈ full shimmer) — zero AT an object's contact line, so a partially
|
|
12309
|
-
// submerged object's image stays glued to the surface (the old fixed
|
|
12310
|
-
// contact damp), and strongly reduced in knee-deep ponds, where the old
|
|
12311
|
-
// distance-only offset flung the pale submerged column into a ragged
|
|
12312
|
-
// star splat around its own base (ref-23's waterline blob).
|
|
12313
|
-
float contactDamp = clamp(waterViewDepth * 0.5, 0.0, 1.0);
|
|
12314
|
-
vec2 refractionOffset = refrSlope * contactDamp * (0.35 / max(linearWaterDepth, 4.0));
|
|
12315
|
-
vec2 refractedUV = clamp(screenUV + refractionOffset, vec2(0.001), vec2(0.999));
|
|
12316
|
-
// grab-clip: the grab COLOR holds ONLY underwater geometry (Water3D
|
|
12317
|
-
// clips that pass at the waterline), so there is no above-water color
|
|
12318
|
-
// to bleed and no halo to guard — the PR-K soft reject and PR-L probe
|
|
12319
|
-
// ring (whose boundaries became ref-23's silhouette outlines) are gone.
|
|
12320
|
-
// One cheap sanity tap remains: an offset landing on geometry in FRONT
|
|
12321
|
-
// of this water fragment (the thin waterline band the clip slack keeps,
|
|
12322
|
-
// grazing self-overlaps) snaps back to the straight grab pixel…
|
|
12323
|
-
float refractedSceneDepth =
|
|
12324
|
-
linearizeDepth(texture2D(uDepthTexture, refractedUV).r, uCameraNear, uCameraFar);
|
|
12325
|
-
float offsetReject = smoothstep(0.0, 0.05, linearWaterDepth - refractedSceneDepth);
|
|
12326
|
-
// …or on an ABOVE-water sample: the depth-completion pass restored its
|
|
12327
|
-
// true depth, but the clipped grab has no color there (clear-color
|
|
12328
|
-
// texel) — reconstruct its world height along the view ray (view depth
|
|
12329
|
-
// is linear in ray distance: t = camDist · sceneDepth/waterDepth) and
|
|
12330
|
-
// snap those offsets back too.
|
|
12331
|
-
float refrT = camDist * (refractedSceneDepth / max(linearWaterDepth, 1e-4));
|
|
12332
|
-
offsetReject = max(offsetReject, smoothstep(0.02, 0.25,
|
|
12333
|
-
cameraPosition.y + viewDirection.y * refrT - uWaterCenter.y));
|
|
12334
|
-
refractedUV = mix(refractedUV, screenUV, offsetReject);
|
|
12335
|
-
// mip-biased grab read: the pre-pass target carries mips (see
|
|
12336
|
-
// Water3D._onRender3D) — biasing ~1.75 levels samples a pre-blurred
|
|
12337
|
-
// underwater scene. Real water blurs what it refracts; the crisp
|
|
12338
|
-
// half-res grab pixel-speckled at every silhouette and ripple edge.
|
|
12339
|
-
vec3 sceneColor = texture2D(uSceneColor, refractedUV, 1.75).rgb;
|
|
12340
|
-
// incanto presets: above-water caustics — reconstruct the sampled
|
|
12341
|
-
// bottom's world position along the view ray and light it with the
|
|
12342
|
-
// shared caustic web. Needs a focusing column of water (a centimeters-
|
|
12343
|
-
// deep swash film shows no web) and a close viewer: past ~30m each
|
|
12344
|
-
// pixel integrates many caustic cells and the additive web averages
|
|
12345
|
-
// into a solid white sheet over any flat shallow (aerial sandbar bug).
|
|
12346
|
-
if (uCausticsAbove > 0.0) {
|
|
12347
|
-
vec3 bottomWorld = cameraPosition + viewDirection * refrT;
|
|
12348
|
-
float below = uWaterCenter.y - bottomWorld.y;
|
|
12349
|
-
if (below > 0.05) {
|
|
12350
|
-
float reach = smoothstep(0.05, 0.5, below) * (1.0 - smoothstep(0.2, 9.0, below));
|
|
12351
|
-
float causticFade = 1.0 - smoothstep(28.0, 90.0, camDist);
|
|
12352
|
-
float cw = caustic(bottomWorld.xz * 0.42, uTime * 0.7) * 0.65
|
|
12353
|
-
+ caustic(bottomWorld.xz * 0.72 + 30.0, uTime * 0.55) * 0.35;
|
|
12354
|
-
sceneColor += vec3(0.87, 0.96, 1.0) * (cw * uCausticsAbove * reach * causticFade);
|
|
12355
|
-
}
|
|
12356
|
-
}
|
|
12357
|
-
// opacity keeps its dial under in-shader compositing: it scales how
|
|
12358
|
-
// strongly the water body hides the Beer-attenuated scene behind it —
|
|
12359
|
-
// per channel, so the shallows tint instead of fading uniformly
|
|
12360
|
-
vec3 body = clamp(uOpacity * (1.0 - transmittance) * 1.4, 0.0, 1.0);
|
|
12361
|
-
// refinement pass 3: crystal shallows (photo reference) — where the
|
|
12362
|
-
// water column is thin you SEE THE BOTTOM, ripple-distorted by the
|
|
12363
|
-
// detail normals; the body color barely tints it. Clarity ramps the
|
|
12364
|
-
// refracted scene up to ~88% inside the first ~1.5 m of water path and
|
|
12365
|
-
// hands over to absorption beyond — depth still darkens the bottom.
|
|
12366
|
-
float clarity = 0.88 * (1.0 - smoothstep(0.2, 1.5, waterViewDepth));
|
|
12367
|
-
body *= 1.0 - clarity;
|
|
12368
|
-
// shoreline-seam fallback: when the STRAIGHT sample itself sits above
|
|
12369
|
-
// the waterline (beach rising behind a shoreline crest, a piercing
|
|
12370
|
-
// object's dry half) the grab texel is clear-color, not the occluder —
|
|
12371
|
-
// hand those pixels to the alpha path. The main framebuffer already
|
|
12372
|
-
// shows the occluder behind this fragment, so a translucent ramp tint
|
|
12373
|
-
// over it is the honest shallow-water look; compositing the clipped
|
|
12374
|
-
// grab instead painted ref-26's background-colored band.
|
|
12375
|
-
float sceneT = camDist * (linearSceneDepth / max(linearWaterDepth, 1e-4));
|
|
12376
|
-
float sceneAbove = smoothstep(0.02, 0.25,
|
|
12377
|
-
cameraPosition.y + viewDirection.y * sceneT - uWaterCenter.y);
|
|
12378
|
-
float meanAbsorbed = 1.0 - dot(transmittance, vec3(0.299, 0.587, 0.114));
|
|
12379
|
-
float alphaOpacity = mix(uOpacity * 0.45, max(uOpacity, 0.95), meanAbsorbed);
|
|
12380
|
-
bodyColor = mix(mix(sceneColor, mixedColor2, body), mixedColor2, sceneAbove);
|
|
12381
|
-
// in-shader compositing (opacity 1) for valid grab pixels; alpha blend
|
|
12382
|
-
// over the main framebuffer for above-water-occluder pixels
|
|
12383
|
-
bodyOpacity = mix(1.0, alphaOpacity, sceneAbove);
|
|
12384
|
-
} else {
|
|
12385
|
-
// no grab pass: alpha does the work — clear shallows, opaque depths
|
|
12386
|
-
float meanAbsorbed = 1.0 - dot(transmittance, vec3(0.299, 0.587, 0.114));
|
|
12387
|
-
bodyOpacity = mix(uOpacity * 0.45, max(uOpacity, 0.95), meanAbsorbed);
|
|
12388
|
-
}
|
|
12389
|
-
}
|
|
12390
|
-
|
|
12391
|
-
// Mix the final color with the reflection color
|
|
12392
|
-
// incanto: gated on uUseEnvironmentMap so an unbound cube map reads flat
|
|
12393
|
-
// clamp so grazing angles keep some water colour instead of going pure
|
|
12394
|
-
// mirror (dark scenes otherwise paint the near edge black). refinement
|
|
12395
|
-
// pass: 0.75 → 0.6 — the reference sea stays teal even at range; an HDR
|
|
12396
|
-
// sky at 3/4 mirror washed the whole far field toward white.
|
|
12397
|
-
float reflectivity = clamp(fresnel, 0.0, uReflectivityMax) * uUseEnvironmentMap;
|
|
12398
|
-
vec3 finalColor = mix(bodyColor, reflectionColor.rgb, reflectivity);
|
|
12399
|
-
|
|
12400
|
-
// incanto v2: sun specular off the detail-perturbed normal.
|
|
12401
|
-
// refinement pass 3: glints are SPARSE micro-facets. The old 320-power
|
|
12402
|
-
// glint + everywhere-sheen flashed on half the ripples in view — the whole
|
|
12403
|
-
// sea read as dense white speckle (글로시 슬라임). Now: extreme shininess
|
|
12404
|
-
// (1400) so only facets aligned within a fraction of a degree flash, and
|
|
12405
|
-
// both terms are weighted toward the sun's AZIMUTH path — crests sparkle
|
|
12406
|
-
// along the light path, the rest of the surface stays broad calm color
|
|
12407
|
-
// fields (the reference look). The sheen survives only as a soft halo on
|
|
12408
|
-
// that path, near-zero elsewhere.
|
|
12409
|
-
vec3 sunL = normalize(uSunDirection);
|
|
12410
|
-
vec3 halfway = normalize(sunL - viewDirection); // -viewDirection = surface→camera
|
|
12411
|
-
float specAngle = max(dot(normal, halfway), 0.0);
|
|
12412
|
-
vec2 toFrag = normalize(vWorldPosition.xz - cameraPosition.xz);
|
|
12413
|
-
float sunPath = pow(clamp(dot(toFrag, normalize(sunL.xz)), 0.0, 1.0), 5.0);
|
|
12414
|
-
// micro-facet glints are a NEAR-field phenomenon on screen: past ~200 m a
|
|
12415
|
-
// pixel integrates whole wave trains and per-pixel glints alias back into
|
|
12416
|
-
// the old speckle — fade them out and hand the sun path to a SMOOTH streak
|
|
12417
|
-
// off the distance-smoothed normal (a real sea's far sun path is a soft
|
|
12418
|
-
// continuous band, not glitter).
|
|
12419
|
-
// …and a VIEW-angle phenomenon: looking straight down (the photo-ref
|
|
12420
|
-
// clarity framing) a real surface shows almost no sparkle — glints live at
|
|
12421
|
-
// grazing angles. Weight by a Schlick-like grazing term so steep views
|
|
12422
|
-
// stay calm, clear water instead of speckling.
|
|
12423
|
-
float grazing = pow(1.0 - clamp(dot(-viewDirection, vec3(0.0, 1.0, 0.0)), 0.0, 1.0), 2.0);
|
|
12424
|
-
// water-feel rewrite: NO artificial gate — the micro-ripple spectrum's
|
|
12425
|
-
// fast churn makes each facet align with the sun for only a flash, so the
|
|
12426
|
-
// glitter is naturally granular AND flickers at ripple speed (the old
|
|
12427
|
-
// slow-noise gate was another piece of the syrup feel).
|
|
12428
|
-
float glintFade = 1.0 - smoothstep(60.0, 240.0, camDist);
|
|
12429
|
-
float glint = pow(specAngle, 900.0) * (0.15 + 0.85 * sunPath) * glintFade
|
|
12430
|
-
* (0.1 + 0.9 * grazing);
|
|
12431
|
-
float farAngle = max(dot(fresnelNormal, halfway), 0.0);
|
|
12432
|
-
float streak = pow(farAngle, 240.0) * 0.45 * sunPath * (1.0 - glintFade);
|
|
12433
|
-
float sheen = pow(specAngle, 64.0) * 0.05 * sunPath;
|
|
12434
|
-
finalColor += uSunColor * min(uSunIntensity * (glint + streak + sheen), 0.85);
|
|
12435
|
-
|
|
12436
|
-
// Calculate and apply foam effects (only when enableFoam is true)
|
|
12437
|
-
float foamAmount = 0.0;
|
|
12438
|
-
if (uEnableFoam > 0.5) {
|
|
12439
|
-
foamAmount = calculateFoam();
|
|
12440
|
-
}
|
|
12441
|
-
// incanto shore: traveling BREAKERS. The leading edge is sharp (the
|
|
12442
|
-
// collapsing face), the tail is a long noisy wash that dissolves seaward
|
|
12443
|
-
// of the beach; the whole system lives in the 0.1–3.2 m depth band where
|
|
12444
|
-
// real waves shoal and break. Because phase = f(vertical depth), a front
|
|
12445
|
-
// is always parallel to the local shoreline — bays curve it, points wrap
|
|
12446
|
-
// it, no per-scene wiring.
|
|
12447
|
-
if (uShoreWaves > 0.0 && uUseSceneDepth > 0.5 && waterViewDepth >= 0.0) {
|
|
12448
|
-
float sceneT2 = camDist * (linearSceneDepth / max(linearWaterDepth, 1e-4));
|
|
12449
|
-
float bottomY = cameraPosition.y + viewDirection.y * sceneT2;
|
|
12450
|
-
float vertDepth = max(uWaterCenter.y - bottomY, 0.0);
|
|
12451
|
-
if (vertDepth > 0.02 && vertDepth < 3.4) {
|
|
12452
|
-
// DITHER the depth before phasing: the half-res depth texture
|
|
12453
|
-
// quantizes on gentle slopes, and un-dithered fronts render as hard
|
|
12454
|
-
// parallel terraces with visible pixel columns.
|
|
12455
|
-
float dither = (noise(vWorldPosition.xz * 1.7) - 0.5) * 0.35
|
|
12456
|
-
+ (noise(vWorldPosition.xz * 6.3 + 11.0) - 0.5) * 0.12;
|
|
12457
|
-
float dd = max(vertDepth + dither, 0.0);
|
|
12458
|
-
// ONE front per ~1.3 m of depth (the old 0.53 m stacked 3-4 terraces
|
|
12459
|
-
// in view at once), soft leading edge, long dissolving wash
|
|
12460
|
-
float phase = dd * 0.75 - uTime * 0.4;
|
|
12461
|
-
float f = fract(phase);
|
|
12462
|
-
float front = smoothstep(0.0, 0.14, f) * (1.0 - smoothstep(0.14, 0.85, f));
|
|
12463
|
-
// waves arrive in SETS: a slow alongshore envelope gates whole fronts
|
|
12464
|
-
// in and out so the surf breathes instead of ticking like a metronome
|
|
12465
|
-
float sets = smoothstep(0.2, 0.75, noise(vWorldPosition.xz * 0.045 + uTime * vec2(0.025, 0.014)));
|
|
12466
|
-
float breakup = noise(vWorldPosition.xz * 0.3 + uTime * vec2(0.06, 0.04));
|
|
12467
|
-
float lace = noise(vWorldPosition.xz * 2.2 - uTime * vec2(0.18, 0.1));
|
|
12468
|
-
front *= (0.45 + 0.55 * breakup) * (0.55 + 0.45 * lace) * (0.35 + 0.65 * sets);
|
|
12469
|
-
// no fronts in the centimeters-deep SHEET-FLOW film (an enclosed flat
|
|
12470
|
-
// fizzed solid white from the air) — breakers need ~knee depth to form
|
|
12471
|
-
float zone = smoothstep(3.4, 1.2, vertDepth) * smoothstep(0.12, 0.38, vertDepth);
|
|
12472
|
-
foamAmount = max(foamAmount, clamp(uShoreWaves * front * zone * 1.5, 0.0, 1.0));
|
|
12473
|
-
// waterline LACE — fizzing bubbles where the runup meets sand. On a
|
|
12474
|
-
// near-FLAT bar the 0–30 cm depth band is tens of meters wide, so an
|
|
12475
|
-
// ungated lace carpeted whole flats in white — a slow patch gate keeps
|
|
12476
|
-
// it as drifting foam streaks instead (real tidal-flat scum lines).
|
|
12477
|
-
float lacePatch = smoothstep(0.4, 0.8, noise(vWorldPosition.xz * 0.5 + uTime * vec2(0.06, 0.045)));
|
|
12478
|
-
float lace2 = smoothstep(0.24, 0.02, vertDepth) *
|
|
12479
|
-
(0.35 + 0.65 * noise(vWorldPosition.xz * 3.1 + uTime * vec2(0.5, 0.35)));
|
|
12480
|
-
foamAmount = max(foamAmount, clamp(uShoreWaves * lace2 * lacePatch * 0.4, 0.0, 1.0));
|
|
12481
|
-
}
|
|
12482
|
-
}
|
|
12483
|
-
// incanto swell: whitecaps — foam eats the TOPS of the tallest crests in
|
|
12484
|
-
// open water. The crest mask normalizes elevation by the real wave envelope
|
|
12485
|
-
// (uScale.y is the surface's half-extent — useless as a sea-state yardstick
|
|
12486
|
-
// on a big ocean plane), and a two-octave noise breaks the caps into
|
|
12487
|
-
// patches that live and die with the crests. Distance-faded: far whitecaps
|
|
12488
|
-
// subtend under a pixel and would alias into horizon speckle.
|
|
12489
|
-
if (uWhitecaps > 0.0 && uWaveEnvelope > 0.001) {
|
|
12490
|
-
float crestN = (vWorldPosition.y - uWaterCenter.y) / uWaveEnvelope;
|
|
12491
|
-
float cap = smoothstep(0.5, 0.85, crestN);
|
|
12492
|
-
vec2 capUV = vWorldPosition.xz * 0.3;
|
|
12493
|
-
float capNoise = noise(capUV * 2.2 + uTime * vec2(0.10, 0.06)) * 0.65
|
|
12494
|
-
+ noise(capUV * 6.5 - uTime * vec2(0.05, 0.14)) * 0.35;
|
|
12495
|
-
float capFade = 1.0 - smoothstep(220.0, 640.0, camDist);
|
|
12496
|
-
float whitecap = uWhitecaps * cap * smoothstep(0.42, 0.78, capNoise) * capFade;
|
|
12497
|
-
foamAmount = max(foamAmount, whitecap);
|
|
12498
|
-
}
|
|
12499
|
-
// incanto: splash/wake whitewater rides the ripple buffer, INDEPENDENT of the
|
|
12500
|
-
// depth-foam gate so it works even on shallow ponds / before the first pre-pass
|
|
12501
|
-
float splashFoam = rippleFoam(vWorldPosition.xz);
|
|
12502
|
-
|
|
12503
|
-
// 🎨 Calculate contact surface color blending (part of foam effects)
|
|
12504
|
-
if (uEnableFoam > 0.5 && uEdgeIntensity > 0.0) {
|
|
12505
|
-
float edgeBlend = calculateEdgeBlending();
|
|
12506
|
-
|
|
12507
|
-
// Blend water color with user-defined color at contact surfaces
|
|
12508
|
-
vec3 blendedWaterColor = mix(finalColor, uEdgeColor, edgeBlend * uEdgeIntensity);
|
|
12509
|
-
|
|
12510
|
-
// Slightly darken at contact surfaces (shadow effect)
|
|
12511
|
-
float shadowIntensity = uEdgeIntensity * 0.5; // Link shadow intensity to user settings
|
|
12512
|
-
float shadowFactor = 1.0 - edgeBlend * shadowIntensity;
|
|
12513
|
-
finalColor = blendedWaterColor * shadowFactor;
|
|
12514
|
-
}
|
|
12515
|
-
|
|
12516
|
-
// Apply foam effects (only when foamAmount is greater than 0)
|
|
12517
|
-
// refinement pass: low-contrast blend, no additive brightness kick — foam
|
|
12518
|
-
// is bubbles ON the water, slightly lighter, never glowing white.
|
|
12519
|
-
if (foamAmount > 0.0) {
|
|
12520
|
-
finalColor = mix(finalColor, uFoamColor, foamAmount * 0.55);
|
|
12521
|
-
}
|
|
12522
|
-
// splash froth: pale foam tint (NOT pure white — that floats on top as a
|
|
12523
|
-
// jarring decal), the water still reads through, so a plunge/wake looks like
|
|
12524
|
-
// disturbed water rather than painted-on white
|
|
12525
|
-
if (splashFoam > 0.0) {
|
|
12526
|
-
vec3 froth = mix(uFoamColor, vec3(1.0), 0.3); // pale, not stark white
|
|
12527
|
-
finalColor = mix(finalColor, froth, splashFoam * 0.58); // subtle, water reads through
|
|
12528
|
-
}
|
|
12529
|
-
|
|
12530
|
-
// 🎨 Adjust transparency at contact surfaces (part of foam effects)
|
|
12531
|
-
// incanto v2: starts from bodyOpacity (Beer's-law depth-shaped alpha)
|
|
12532
|
-
float baseOpacity = bodyOpacity;
|
|
12533
|
-
if (uEnableFoam > 0.5 && uEdgeIntensity > 0.0) {
|
|
12534
|
-
float edgeBlend = calculateEdgeBlending();
|
|
12535
|
-
float transparencyEffect = uEdgeIntensity * 0.4; // Maximum 40% transparency increase
|
|
12536
|
-
float edgeTransparency = mix(1.0, 1.0 - transparencyEffect, edgeBlend);
|
|
12537
|
-
baseOpacity = bodyOpacity * edgeTransparency;
|
|
12538
|
-
}
|
|
12539
|
-
|
|
12540
|
-
// Make areas with foam more opaque — shoreline foam firm, splash froth gentler
|
|
12541
|
-
// (a lighter opacity bump keeps the splash translucent, not a solid white cap)
|
|
12542
|
-
float foamOpacity = baseOpacity;
|
|
12543
|
-
if (foamAmount > 0.0) {
|
|
12544
|
-
foamOpacity = mix(baseOpacity, min(baseOpacity + 0.5, 1.0), foamAmount);
|
|
12545
|
-
}
|
|
12546
|
-
if (splashFoam > 0.0) {
|
|
12547
|
-
foamOpacity = max(foamOpacity, mix(baseOpacity, min(baseOpacity + 0.3, 1.0), splashFoam));
|
|
12548
|
-
}
|
|
12549
|
-
|
|
12550
|
-
// 🔥 Final fundamental solution: Apply depth mask
|
|
12551
|
-
float finalOpacity = foamOpacity * depthMask;
|
|
12552
|
-
|
|
12553
|
-
gl_FragColor = vec4(finalColor, finalOpacity);
|
|
12554
|
-
|
|
12555
|
-
// incanto atmosphere: join three's output pipeline — ACES tone mapping +
|
|
12556
|
-
// working→output colorspace encode + linear fog, in the SAME order as the
|
|
12557
|
-
// built-in materials so water fogs/grades identically to the terrain.
|
|
12558
|
-
#include <tonemapping_fragment>
|
|
12559
|
-
#include <colorspace_fragment>
|
|
12560
|
-
#include <fog_fragment>
|
|
12561
|
-
}
|
|
12562
|
-
`;
|
|
12563
|
-
//#endregion
|
|
12564
|
-
//#region src/3d/water/underwater.ts
|
|
12565
|
-
/**
|
|
12566
|
-
* True when the camera EYE sits below the water surface and inside its
|
|
12567
|
-
* (margin-expanded) XZ footprint — i.e. genuinely underwater. The tie at exactly
|
|
12568
|
-
* the surface height counts as above water (no flicker as you break the surface).
|
|
12569
|
-
* The underwater look is deliberately gated to this (not "near" the surface), so
|
|
12570
|
-
* wading with the camera above the water never tints the view. `marginXZ` lets it
|
|
12571
|
-
* engage a touch past the rim so swimming to the edge doesn't pop.
|
|
12572
|
-
*/
|
|
12573
|
-
function isCameraUnderwater(camX, camY, camZ, s, marginXZ = 0) {
|
|
12574
|
-
if (camY >= s.centerY) return false;
|
|
12575
|
-
return Math.abs(camX - s.centerX) <= s.halfW + marginXZ && Math.abs(camZ - s.centerZ) <= s.halfD + marginXZ;
|
|
12576
|
-
}
|
|
12577
|
-
/** Default visibility (fog far, meters) when the prop doesn't specify one. */
|
|
12578
|
-
const DEFAULT_VISIBILITY = 22;
|
|
12579
|
-
const DEFAULT_CAUSTICS = {
|
|
12580
|
-
enabled: true,
|
|
12581
|
-
color: "#cdeeff",
|
|
12582
|
-
intensity: .55,
|
|
12583
|
-
scale: .32,
|
|
12584
|
-
speed: 1
|
|
12585
|
-
};
|
|
12586
|
-
/**
|
|
12587
|
-
* Resolve the `underwater` prop (`false` | `true` | `{color?, visibility?,
|
|
12588
|
-
* caustics?}`) into concrete values. With no explicit color the murk is DERIVED
|
|
12589
|
-
* from the water's own hue — darkened and pulled a touch toward deep blue so
|
|
12590
|
-
* being submerged reads as "inside THIS water", brighter water → brighter murk.
|
|
12591
|
-
*/
|
|
12592
|
-
function resolveUnderwater(prop, waterColor) {
|
|
12593
|
-
if (prop === false) return {
|
|
12594
|
-
enabled: false,
|
|
12595
|
-
color: deriveMurk(waterColor),
|
|
12596
|
-
visibility: DEFAULT_VISIBILITY,
|
|
12597
|
-
caustics: DEFAULT_CAUSTICS
|
|
12598
|
-
};
|
|
12599
|
-
const obj = prop && typeof prop === "object" ? prop : {};
|
|
12600
|
-
return {
|
|
12601
|
-
enabled: true,
|
|
12602
|
-
color: obj.color ?? deriveMurk(waterColor),
|
|
12603
|
-
visibility: obj.visibility ?? DEFAULT_VISIBILITY,
|
|
12604
|
-
caustics: resolveCaustics(obj.caustics)
|
|
12605
|
-
};
|
|
12606
|
-
}
|
|
12607
|
-
function resolveCaustics(prop) {
|
|
12608
|
-
if (prop === false) return {
|
|
12609
|
-
...DEFAULT_CAUSTICS,
|
|
12610
|
-
enabled: false
|
|
12611
|
-
};
|
|
12612
|
-
const obj = prop && typeof prop === "object" ? prop : {};
|
|
12613
|
-
return {
|
|
12614
|
-
enabled: true,
|
|
12615
|
-
color: typeof obj.color === "string" ? obj.color : DEFAULT_CAUSTICS.color,
|
|
12616
|
-
intensity: typeof obj.intensity === "number" ? obj.intensity : DEFAULT_CAUSTICS.intensity,
|
|
12617
|
-
scale: typeof obj.scale === "number" ? obj.scale : DEFAULT_CAUSTICS.scale,
|
|
12618
|
-
speed: typeof obj.speed === "number" ? obj.speed : DEFAULT_CAUSTICS.speed
|
|
12619
|
-
};
|
|
12620
|
-
}
|
|
12621
|
-
/**
|
|
12622
|
-
* Darken a hex color toward a deep blue-green — the look of light absorbed by a
|
|
12623
|
-
* column of water (red dies first, blue lingers). Pure hex math so the parser
|
|
12624
|
-
* stays three-free.
|
|
12625
|
-
*/
|
|
12626
|
-
function deriveMurk(hex) {
|
|
12627
|
-
const { r, g, b } = parseHex(hex);
|
|
12628
|
-
const mix = (c, target, t) => Math.round(c * (1 - t) + target * t);
|
|
12629
|
-
return toHex(Math.round(mix(r, 6, .45) * .7), Math.round(mix(g, 40, .4) * .78), Math.round(mix(b, 70, .35) * .82));
|
|
12630
|
-
}
|
|
12631
|
-
function parseHex(hex) {
|
|
12632
|
-
const h = hex.replace("#", "");
|
|
12633
|
-
const n = h.length === 3 ? h.split("").map((c) => c + c).join("") : h;
|
|
12634
|
-
return {
|
|
12635
|
-
r: Number.parseInt(n.slice(0, 2), 16) || 0,
|
|
12636
|
-
g: Number.parseInt(n.slice(2, 4), 16) || 0,
|
|
12637
|
-
b: Number.parseInt(n.slice(4, 6), 16) || 0
|
|
12638
|
-
};
|
|
12639
|
-
}
|
|
12640
|
-
function toHex(r, g, b) {
|
|
12641
|
-
const c = (v) => Math.max(0, Math.min(255, v)).toString(16).padStart(2, "0");
|
|
12642
|
-
return `#${c(r)}${c(g)}${c(b)}`;
|
|
12643
|
-
}
|
|
12644
|
-
//#endregion
|
|
12645
|
-
//#region src/3d/water/waves.ts
|
|
12646
|
-
/**
|
|
12647
|
-
* CPU twin of the Water3D vertex-shader wave field — pure math, no three.
|
|
12648
|
-
*
|
|
12649
|
-
* `waveElevation` ports the shader's 2D-simplex FBM (`getElevation`) exactly
|
|
12650
|
-
* (same constants, same iteration scheme) so character-interaction code can
|
|
12651
|
-
* sample the waterline a body is crossing WITHOUT reading the GPU back.
|
|
12652
|
-
* `simpleWaveHeight` is the legacy crossed-sines displacement used by
|
|
12653
|
-
* `quality: 'simple'`.
|
|
12654
|
-
*/
|
|
12655
|
-
/** The ported shader's FBM constants (vibe-starter-3d-environment water). */
|
|
12656
|
-
const FANCY_WAVE = {
|
|
12657
|
-
frequency: 6.7,
|
|
12658
|
-
persistence: .3,
|
|
12659
|
-
lacunarity: 2.18,
|
|
12660
|
-
iterations: 9,
|
|
12661
|
-
speed: .5
|
|
12662
|
-
};
|
|
12663
|
-
/** The source shader's reference amplitude… */
|
|
12664
|
-
const REFERENCE_AMPLITUDE = .525;
|
|
12665
|
-
/** …which the legacy `waveHeight` default maps onto (0.08 → 0.525). */
|
|
12666
|
-
const REFERENCE_WAVE_HEIGHT = .08;
|
|
12667
|
-
/** Legacy crossed-sine constants (`quality: 'simple'`). */
|
|
12668
|
-
const SIMPLE_WAVE = {
|
|
12669
|
-
freqX: .7,
|
|
12670
|
-
freqZ: .5,
|
|
12671
|
-
cross: .6
|
|
12672
|
-
};
|
|
12673
|
-
/** GLSL `mod` (sign of divisor, unlike JS `%`). */
|
|
12674
|
-
function glslMod(a, n) {
|
|
12675
|
-
return a - Math.floor(a / n) * n;
|
|
12676
|
-
}
|
|
12677
|
-
function fract(x) {
|
|
12678
|
-
return x - Math.floor(x);
|
|
12679
|
-
}
|
|
12680
|
-
/** GLSL `permute`: mod(((x*34)+1)*x, 289). */
|
|
12681
|
-
function permute(x) {
|
|
12682
|
-
return glslMod((x * 34 + 1) * x, 289);
|
|
12683
|
-
}
|
|
12684
|
-
const C_X = .211324865405187;
|
|
12685
|
-
const C_Y = .366025403784439;
|
|
12686
|
-
const C_Z = -.577350269189626;
|
|
12687
|
-
const C_W = .024390243902439;
|
|
12688
|
-
function snoise2(vx, vy) {
|
|
12689
|
-
let ix = Math.floor(vx + (vx + vy) * C_Y);
|
|
12690
|
-
let iy = Math.floor(vy + (vx + vy) * C_Y);
|
|
12691
|
-
const x0x = vx - ix + (ix + iy) * C_X;
|
|
12692
|
-
const x0y = vy - iy + (ix + iy) * C_X;
|
|
12693
|
-
const i1x = x0x > x0y ? 1 : 0;
|
|
12694
|
-
const i1y = x0x > x0y ? 0 : 1;
|
|
12695
|
-
const x12x = x0x + C_X - i1x;
|
|
12696
|
-
const x12y = x0y + C_X - i1y;
|
|
12697
|
-
const x12z = x0x + C_Z;
|
|
12698
|
-
const x12w = x0y + C_Z;
|
|
12699
|
-
ix = glslMod(ix, 289);
|
|
12700
|
-
iy = glslMod(iy, 289);
|
|
12701
|
-
const p0 = permute(permute(iy) + ix);
|
|
12702
|
-
const p1 = permute(permute(iy + i1y) + ix + i1x);
|
|
12703
|
-
const p2 = permute(permute(iy + 1) + ix + 1);
|
|
12704
|
-
let m0 = Math.max(.5 - (x0x * x0x + x0y * x0y), 0);
|
|
12705
|
-
let m1 = Math.max(.5 - (x12x * x12x + x12y * x12y), 0);
|
|
12706
|
-
let m2 = Math.max(.5 - (x12z * x12z + x12w * x12w), 0);
|
|
12707
|
-
m0 = m0 * m0;
|
|
12708
|
-
m0 = m0 * m0;
|
|
12709
|
-
m1 = m1 * m1;
|
|
12710
|
-
m1 = m1 * m1;
|
|
12711
|
-
m2 = m2 * m2;
|
|
12712
|
-
m2 = m2 * m2;
|
|
12713
|
-
const g0x = 2 * fract(p0 * C_W) - 1;
|
|
12714
|
-
const g1x = 2 * fract(p1 * C_W) - 1;
|
|
12715
|
-
const g2x = 2 * fract(p2 * C_W) - 1;
|
|
12716
|
-
const h0 = Math.abs(g0x) - .5;
|
|
12717
|
-
const h1 = Math.abs(g1x) - .5;
|
|
12718
|
-
const h2 = Math.abs(g2x) - .5;
|
|
12719
|
-
const a0 = g0x - Math.floor(g0x + .5);
|
|
12720
|
-
const a1 = g1x - Math.floor(g1x + .5);
|
|
12721
|
-
const a2 = g2x - Math.floor(g2x + .5);
|
|
12722
|
-
m0 *= 1.79284291400159 - .85373472095314 * (a0 * a0 + h0 * h0);
|
|
12723
|
-
m1 *= 1.79284291400159 - .85373472095314 * (a1 * a1 + h1 * h1);
|
|
12724
|
-
m2 *= 1.79284291400159 - .85373472095314 * (a2 * a2 + h2 * h2);
|
|
12725
|
-
const g0 = a0 * x0x + h0 * x0y;
|
|
12726
|
-
const g1 = a1 * x12x + h1 * x12y;
|
|
12727
|
-
const g2 = a2 * x12z + h2 * x12w;
|
|
12728
|
-
return 130 * (m0 * g0 + m1 * g1 + m2 * g2);
|
|
12729
|
-
}
|
|
12730
|
-
/**
|
|
12731
|
-
* Wave elevation at world (x, z) at `time` — the shader's `getElevation`.
|
|
12732
|
-
* Deterministic; bounded by `amplitude · (1 − persistence^n) / (1 − persistence)`.
|
|
12733
|
-
*/
|
|
12734
|
-
function waveElevation(x, z, time, p) {
|
|
12735
|
-
const norm = Math.max(p.scaleNorm, 1e-6);
|
|
12736
|
-
const px = x / norm;
|
|
12737
|
-
const pz = z / norm;
|
|
12738
|
-
let elevation = 0;
|
|
12739
|
-
let amplitude = 1;
|
|
12740
|
-
let frequency = p.frequency;
|
|
12741
|
-
const phase = time * p.speed;
|
|
12742
|
-
for (let i = 0; i < p.iterations; i++) {
|
|
12743
|
-
elevation += amplitude * snoise2(px * frequency + phase, pz * frequency + phase);
|
|
12744
|
-
amplitude *= p.persistence;
|
|
12745
|
-
frequency *= p.lacunarity;
|
|
12746
|
-
}
|
|
12747
|
-
return elevation * p.amplitude;
|
|
12748
|
-
}
|
|
12749
|
-
/** The legacy two-crossed-sines height at LOCAL (x, z) — `quality: 'simple'`. */
|
|
12750
|
-
function simpleWaveHeight(x, z, time, waveHeight) {
|
|
12751
|
-
return waveHeight * (Math.sin(x * SIMPLE_WAVE.freqX + time) + SIMPLE_WAVE.cross * Math.sin(z * SIMPLE_WAVE.freqZ + time * 1.3));
|
|
12752
|
-
}
|
|
12753
|
-
//#endregion
|
|
12754
|
-
//#region src/3d/nodes/water-3d.ts
|
|
12755
|
-
const WATER_QUALITIES = ["fancy", "simple"];
|
|
12756
|
-
const COLOR_KEYS = [
|
|
12757
|
-
"trough",
|
|
12758
|
-
"surface",
|
|
12759
|
-
"peak"
|
|
12760
|
-
];
|
|
12761
|
-
const WATER_PRESET_NAMES = [
|
|
12762
|
-
"custom",
|
|
12763
|
-
"ocean",
|
|
12764
|
-
"pool",
|
|
12765
|
-
"lake",
|
|
12766
|
-
"pond"
|
|
12767
|
-
];
|
|
12768
|
-
/**
|
|
12769
|
-
* The four curated water types — `preset: 'ocean' | 'pool' | 'lake' | 'pond'`
|
|
12770
|
-
* is THE one-choice dial: each bundle sets every wave/color/clarity/caustics
|
|
12771
|
-
* knob to a hand-tuned sea state. A preset value applies ONLY while the prop
|
|
12772
|
-
* still sits at its schema default (same convention as the env-sun handoff),
|
|
12773
|
-
* so any explicitly authored prop wins — pick the type, then override details
|
|
12774
|
-
* to taste.
|
|
12775
|
-
*
|
|
12776
|
-
* - ocean: traveling swell + whitecaps, wide turquoise→deep-blue absorption
|
|
12777
|
-
* - pool: near-flat crystal water, strong above-water caustics on the floor
|
|
12778
|
-
* - lake: calm mirror — high reflectivity, murkier green-teal depths
|
|
12779
|
-
* - pond: still, mossy, opaque quickly — the garden pond
|
|
12780
|
-
*/
|
|
12781
|
-
const WATER_PRESETS = {
|
|
12782
|
-
ocean: {
|
|
12783
|
-
waveHeight: .045,
|
|
12784
|
-
swell: .34,
|
|
12785
|
-
swellWavelength: 52,
|
|
12786
|
-
whitecaps: .15,
|
|
12787
|
-
absorption: .17,
|
|
12788
|
-
opacity: .85,
|
|
12789
|
-
detailStrength: .17,
|
|
12790
|
-
sunIntensity: .85,
|
|
12791
|
-
colors: {
|
|
12792
|
-
trough: "#175360",
|
|
12793
|
-
surface: "#2f8792",
|
|
12794
|
-
peak: "#9bd8d2"
|
|
12795
|
-
},
|
|
12796
|
-
color: "#2f8792",
|
|
12797
|
-
causticsAbove: .3,
|
|
12798
|
-
shoreWaves: .7,
|
|
12799
|
-
reflectionInterval: 400,
|
|
12800
|
-
_look: {
|
|
12801
|
-
fresnelScale: 1.15,
|
|
12802
|
-
reflectivityMax: .93,
|
|
12803
|
-
foamRangeScale: .35,
|
|
12804
|
-
foamIntensity: .55
|
|
12805
|
-
}
|
|
12806
|
-
},
|
|
12807
|
-
pool: {
|
|
12808
|
-
waveHeight: .006,
|
|
12809
|
-
swell: 0,
|
|
12810
|
-
whitecaps: 0,
|
|
12811
|
-
absorption: .3,
|
|
12812
|
-
opacity: .5,
|
|
12813
|
-
detailStrength: .16,
|
|
12814
|
-
sunIntensity: 1.25,
|
|
12815
|
-
colors: {
|
|
12816
|
-
trough: "#1e8dbe",
|
|
12817
|
-
surface: "#3fb6dc",
|
|
12818
|
-
peak: "#bdeef7"
|
|
12819
|
-
},
|
|
12820
|
-
color: "#3fb6dc",
|
|
12821
|
-
causticsAbove: 1,
|
|
12822
|
-
reflectionInterval: 600,
|
|
12823
|
-
_look: {
|
|
12824
|
-
fresnelScale: .85,
|
|
12825
|
-
reflectivityMax: .5,
|
|
12826
|
-
edgeFade: false
|
|
12827
|
-
}
|
|
12828
|
-
},
|
|
12829
|
-
lake: {
|
|
12830
|
-
waveHeight: .028,
|
|
12831
|
-
swell: .14,
|
|
12832
|
-
swellWavelength: 24,
|
|
12833
|
-
whitecaps: 0,
|
|
12834
|
-
absorption: .5,
|
|
12835
|
-
opacity: .88,
|
|
12836
|
-
detailStrength: .3,
|
|
12837
|
-
colors: {
|
|
12838
|
-
trough: "#12343a",
|
|
12839
|
-
surface: "#25565c",
|
|
12840
|
-
peak: "#7fb2ab"
|
|
12841
|
-
},
|
|
12842
|
-
color: "#25565c",
|
|
12843
|
-
causticsAbove: .15,
|
|
12844
|
-
reflectionInterval: 300,
|
|
12845
|
-
_look: {
|
|
12846
|
-
fresnelScale: 1.25,
|
|
12847
|
-
reflectivityMax: .78
|
|
12848
|
-
}
|
|
12849
|
-
},
|
|
12850
|
-
pond: {
|
|
12851
|
-
waveHeight: .012,
|
|
12852
|
-
swell: 0,
|
|
12853
|
-
whitecaps: 0,
|
|
12854
|
-
absorption: .55,
|
|
12855
|
-
opacity: .88,
|
|
12856
|
-
detailStrength: .2,
|
|
12857
|
-
sunIntensity: .9,
|
|
12858
|
-
colors: {
|
|
12859
|
-
trough: "#24402a",
|
|
12860
|
-
surface: "#436b4d",
|
|
12861
|
-
peak: "#8fae8e"
|
|
12862
|
-
},
|
|
12863
|
-
color: "#436b4d",
|
|
12864
|
-
causticsAbove: 0,
|
|
12865
|
-
reflectionInterval: 800,
|
|
12866
|
-
_look: {
|
|
12867
|
-
fresnelScale: 1.1,
|
|
12868
|
-
reflectivityMax: .68,
|
|
12869
|
-
edgeFade: false
|
|
12870
|
-
}
|
|
12871
|
-
}
|
|
12872
|
-
};
|
|
12873
|
-
/** Refinement-pass palette — teal-leaning sea blues sampled off the reference
|
|
12874
|
-
* calm sea (deep open water ≈ #2b7993, mid teal ≈ #16878a). The old ported
|
|
12875
|
-
* palette (#1f5b8a/#52a0d0/#aee3ff) read as saturated pool blue. */
|
|
12876
|
-
const FANCY_COLORS = {
|
|
12877
|
-
trough: "#135a72",
|
|
12878
|
-
surface: "#2a86a0",
|
|
12879
|
-
peak: "#a5ddd6"
|
|
12880
|
-
};
|
|
12881
|
-
/** Color-ramp constants ported as-is; fresnel RETUNED for the atmosphere
|
|
12882
|
-
* stage: the source's scale 1.5 / power 0.8 made the surface a ~75% mirror at
|
|
12883
|
-
* every angle — invisible against its LDR backdrop, but a milk-white sea once
|
|
12884
|
-
* the reflection cube carries the HDR physical sky. Schlick's real exponent
|
|
12885
|
-
* (5) with unit scale keeps steep views water-colored and grazing views sky. */
|
|
12886
|
-
const FANCY_LOOK = {
|
|
12887
|
-
peakThreshold: .08,
|
|
12888
|
-
peakTransition: .05,
|
|
12889
|
-
troughThreshold: -.01,
|
|
12890
|
-
troughTransition: .15,
|
|
12891
|
-
fresnelScale: 1,
|
|
12892
|
-
fresnelPower: 5
|
|
12893
|
-
};
|
|
12894
|
-
/** Foam/contact constants. Refinement-pass retune: `range` scales with the
|
|
12895
|
-
* water's extent (see foamRangeFor) — SEAS get wide wispy channel foam like
|
|
12896
|
-
* the reference, ponds keep a shoreline KISS (a flat 1.4 m range painted a
|
|
12897
|
-
* 0.45 m-deep pond white wall-to-wall — the exact v2 regression, relearned).
|
|
12898
|
-
* `intensity` drops and the fragment ramp softens — wispy patches, never a
|
|
12899
|
-
* bright band. `edgeIntensity` 2 → 0.6: the contact line is a hint. */
|
|
12900
|
-
const FOAM_LOOK = {
|
|
12901
|
-
color: "#dcebe8",
|
|
12902
|
-
intensity: .32,
|
|
12903
|
-
rangeMin: .45,
|
|
12904
|
-
rangeMax: 1.4,
|
|
12905
|
-
scale: .35,
|
|
12906
|
-
edgeColor: "#ffffff",
|
|
12907
|
-
edgeIntensity: .6,
|
|
12908
|
-
edgeWidth: .5
|
|
12909
|
-
};
|
|
12910
|
-
/** Foam depth-range in meters, scaled by the surface's smaller extent: a
|
|
12911
|
-
* garden pond (≤ ~37 m) keeps the tight 0.45 m contact band, open water
|
|
12912
|
-
* ramps to 1.4 m so foam spreads across whole shallow channels. */
|
|
12913
|
-
function foamRangeFor(w, d) {
|
|
12914
|
-
return Math.min(FOAM_LOOK.rangeMax, Math.max(FOAM_LOOK.rangeMin, Math.min(w, d) * .012));
|
|
12915
|
-
}
|
|
12916
|
-
/** `waveHeight` keeps its role as THE wave dial: the legacy default (0.08)
|
|
12917
|
-
* reproduces the source's reference amplitude (0.525) exactly. */
|
|
12918
|
-
const WAVE_WORLD_REF = 128;
|
|
12919
|
-
const AMPLITUDE_PER_WAVE_HEIGHT = REFERENCE_AMPLITUDE / REFERENCE_WAVE_HEIGHT;
|
|
12920
|
-
/** Cube-map reflection resolution (source: reflectionQuality = 256). */
|
|
12921
|
-
const REFLECTION_RESOLUTION = 256;
|
|
12922
|
-
/** Planar-mirror target size cap (per side) — half the drawing buffer, clamped. */
|
|
12923
|
-
const MIRROR_PASS_MAX = 1024;
|
|
12924
|
-
/** How far a wavelet's tilt slides the mirror sample, in UV per unit of normal lean. */
|
|
12925
|
-
const MIRROR_DISTORT = .06;
|
|
12926
|
-
/** Scene pre-pass (depth for absorption/foam + color grab for refraction):
|
|
12927
|
-
* half the drawing buffer, clamped — ONE extra scene render per frame. */
|
|
12928
|
-
const SCENE_PASS_MAX = 1024;
|
|
12929
|
-
/** Grab-clip tolerance above the waterline (meters) — covers depth-buffer
|
|
12930
|
-
* precision at the clip plane, nothing more. Deliberately NO wave-height
|
|
12931
|
-
* slack: clipping at crest height instead kept each piercing object's
|
|
12932
|
-
* [waterline..crest] slice in the grab, and that pale band re-bled through
|
|
12933
|
-
* the mip-biased read and the ripple-wiggled offsets as silhouette scribbles
|
|
12934
|
-
* — a miniature of the halo disease the clip exists to kill. The cost is
|
|
12935
|
-
* crests refracting a hair short at an object's contact (invisible: the
|
|
12936
|
-
* object's own deeper column fills the gap with near-identical color). */
|
|
12937
|
-
const WATERLINE_CLIP_SLACK = .02;
|
|
12938
|
-
/** Fancy vertex density: the FBM displaces per vertex, so density scales with
|
|
12939
|
-
* extent (the source's formula degenerated to always-256; ours ramps to it). */
|
|
12940
|
-
const FANCY_SEGMENTS_PER_M = 4;
|
|
12941
|
-
const FANCY_SEGMENTS_MIN = 32;
|
|
12942
|
-
const FANCY_SEGMENTS_MAX = 256;
|
|
12943
|
-
/** Simple quality: coarse enough to animate every vertex on the CPU. */
|
|
12944
|
-
const SIMPLE_SEGMENTS = 32;
|
|
12945
|
-
/** Bodies must rise this far above the waterline to 'exit' (no surface flicker). */
|
|
12946
|
-
const SURFACE_HYSTERESIS = .05;
|
|
12947
|
-
/** Entry splash amplitude: base + per-(m/s of |vy|), capped. */
|
|
12948
|
-
const ENTER_AMP_BASE = .08;
|
|
12949
|
-
const ENTER_AMP_PER_VY = .02;
|
|
12950
|
-
const ENTER_AMP_MAX = .18;
|
|
12951
|
-
/** Leaving the water drags a smaller ring out with you. */
|
|
12952
|
-
const EXIT_AMP = .04;
|
|
12953
|
-
/** Bodies floating IN water bob a gentle ring every interval (seconds). */
|
|
12954
|
-
const BOB_INTERVAL = 1.4;
|
|
12955
|
-
const BOB_AMP = .04;
|
|
12956
|
-
/** A body striding through water trails a WAKE: a ripple every this-many metres. */
|
|
12957
|
-
const WAKE_STEP = .45;
|
|
12958
|
-
const WAKE_AMP = .08;
|
|
12959
|
-
/** Splash FOAM strengths (0..1) the shaders paint as surface whitewater.
|
|
12960
|
-
* Entry foam scales with fall speed (see `entryFoam`); these tag the rest. */
|
|
12961
|
-
const EXIT_FOAM = .4;
|
|
12962
|
-
/** Wake foam tracks how fast you stride through the water (the "물살"). */
|
|
12963
|
-
const WAKE_FOAM_BASE = .22;
|
|
12964
|
-
const WAKE_FOAM_PER_SPEED = .09;
|
|
12965
|
-
const WAKE_FOAM_MAX = .85;
|
|
12966
|
-
/**
|
|
12967
|
-
* A water surface on XZ. `quality: 'fancy'` (default) is a commercial-grade
|
|
12968
|
-
* water shader — 9-iteration simplex FBM displacement with analytic normals,
|
|
12969
|
-
* two animated procedural detail-normal layers (`detailStrength`), a sun
|
|
12970
|
-
* specular glint+sheen (`sunDirection`/`sunColor`/`sunIntensity`), Beer's-law
|
|
12971
|
-
* depth `absorption` (clear shallows → dark depths), screen-space
|
|
12972
|
-
* `refraction` of the submerged scene, depth-texture shoreline `foam` (on by
|
|
12973
|
-
* default), a trough/surface/peak color ramp and fresnel-mixed CubeCamera
|
|
12974
|
-
* reflections (re-rendered every `reflectionInterval` ms). `quality:
|
|
12975
|
-
* 'simple'` is the cheap CPU sine-wave material (low-end fallback; also the
|
|
12976
|
-
* deterministic headless path).
|
|
12977
|
-
*
|
|
12978
|
-
* Per-frame GPU budget for fancy (beyond drawing the surface): ONE half-res
|
|
12979
|
-
* scene pre-pass that feeds depth (absorption/foam/soft intersections) AND
|
|
12980
|
-
* the refraction color grab; the cube reflection stays on its `1000 ms`
|
|
12981
|
-
* throttle.
|
|
12982
|
-
*
|
|
12983
|
-
* Character interaction (`interaction: true`): each update the node scans the
|
|
12984
|
-
* tree for CharacterBody3D/RigidBody3D, compares their world y against the
|
|
12985
|
-
* waterline at their xz (wave offset included, CPU twin of the shader), emits
|
|
12986
|
-
* `entered`/`exited` with the body node, and pushes ripple impulses into a
|
|
12987
|
-
* fixed 8-slot ring buffer both shaders turn into expanding ring waves. The
|
|
12988
|
-
* surface has no bottom — anything below the waterline inside the XZ
|
|
12989
|
-
* footprint counts as in-water (pair with `Area3D` for volumetric gameplay).
|
|
12990
|
-
*
|
|
12991
|
-
* GPU-only features (reflection/foam) run through the renderer's
|
|
12992
|
-
* `_onRender3D` hand-off — headless runs simply never invoke it.
|
|
12993
|
-
*/
|
|
12994
|
-
var Water3D = class Water3D extends Node3D {
|
|
12995
|
-
static typeName = "Water3D";
|
|
12996
|
-
static signals = ["entered", "exited"];
|
|
12997
|
-
static props = {
|
|
12998
|
-
size: { default: [40, 40] },
|
|
12999
|
-
color: { default: "#2a6fbe" },
|
|
13000
|
-
opacity: { default: .8 },
|
|
13001
|
-
renderOrder: { default: 1 },
|
|
13002
|
-
waveHeight: { default: .04 },
|
|
13003
|
-
waveSpeed: { default: 1 },
|
|
13004
|
-
quality: {
|
|
13005
|
-
default: "fancy",
|
|
13006
|
-
options: WATER_QUALITIES
|
|
13007
|
-
},
|
|
13008
|
-
colors: { default: {} },
|
|
13009
|
-
reflection: { default: true },
|
|
13010
|
-
reflectionInterval: { default: 1e3 },
|
|
13011
|
-
mirror: { default: false },
|
|
13012
|
-
foam: { default: true },
|
|
13013
|
-
interaction: { default: true },
|
|
13014
|
-
splash: { default: true },
|
|
13015
|
-
sunDirection: { default: [
|
|
13016
|
-
.5,
|
|
13017
|
-
.8,
|
|
13018
|
-
.3
|
|
13019
|
-
] },
|
|
13020
|
-
sunColor: { default: "#fff5d6" },
|
|
13021
|
-
sunIntensity: { default: 1 },
|
|
13022
|
-
detailStrength: { default: .26 },
|
|
13023
|
-
absorption: { default: .15 },
|
|
13024
|
-
refraction: { default: true },
|
|
13025
|
-
underwater: { default: true },
|
|
13026
|
-
swell: { default: 0 },
|
|
13027
|
-
swellDirectionDeg: { default: 0 },
|
|
13028
|
-
swellWavelength: { default: 30 },
|
|
13029
|
-
whitecaps: { default: 0 },
|
|
13030
|
-
preset: {
|
|
13031
|
-
default: "custom",
|
|
13032
|
-
options: WATER_PRESET_NAMES
|
|
13033
|
-
},
|
|
13034
|
-
causticsAbove: { default: 0 },
|
|
13035
|
-
shoreWaves: { default: 0 }
|
|
13036
|
-
};
|
|
13037
|
-
/** [width, depth] in meters (the surface lies on XZ). */
|
|
13038
|
-
size = [40, 40];
|
|
13039
|
-
/** Legacy single hue: simple-quality color; tints the fancy ramp when set. */
|
|
13040
|
-
color = "#2a6fbe";
|
|
13041
|
-
opacity = .8;
|
|
13042
|
-
/** Draw after opaque geometry (overrides Node3D's 0 default). */
|
|
13043
|
-
renderOrder = 1;
|
|
13044
|
-
/** Wave intensity dial, meters-ish. 0 = mirror-flat; 0.04 (default) reads
|
|
13045
|
-
* as a calm sea; 0.08 = the noisier ported-source look. */
|
|
13046
|
-
waveHeight = .04;
|
|
13047
|
-
waveSpeed = 1;
|
|
13048
|
-
/** 'fancy' shader water (default) | 'simple' cheap CPU sine material. */
|
|
13049
|
-
quality = "fancy";
|
|
13050
|
-
/** Fancy ramp overrides: `{trough?, surface?, peak?}` hex strings. */
|
|
13051
|
-
colors = {};
|
|
13052
|
-
/** CubeCamera reflections (fancy only; needs a live renderer). */
|
|
13053
|
-
reflection = true;
|
|
13054
|
-
/**
|
|
13055
|
-
* TRUE planar reflection: the scene re-rendered from the camera mirrored
|
|
13056
|
-
* across this surface, sampled per fragment. A cube map can only carry
|
|
13057
|
-
* distant content — the shoreline trees, the moored boat, the player at the
|
|
13058
|
-
* water's edge only appear in the water with this on. Costs one extra scene
|
|
13059
|
-
* render per frame, so it is off by default; a still lake earns it.
|
|
13060
|
-
*/
|
|
13061
|
-
mirror = false;
|
|
13062
|
-
/** ms between reflection (and foam depth) re-renders. */
|
|
13063
|
-
reflectionInterval = 1e3;
|
|
13064
|
-
/** Depth-texture shoreline foam — rides the always-on fancy scene pass. */
|
|
13065
|
-
foam = true;
|
|
13066
|
-
/** Bodies crossing the surface emit signals and raise ripples. */
|
|
13067
|
-
interaction = true;
|
|
13068
|
-
/** Splash WHITEWATER: the shaders paint expanding surface foam where bodies
|
|
13069
|
-
* interact — a "풍덩" froth bloom on entry (scaled by fall speed) and a foam
|
|
13070
|
-
* trail ("물살") behind a body wading/running through. Needs `interaction`. */
|
|
13071
|
-
splash = true;
|
|
13072
|
-
/** Fixed sun for the specular glint — match the scene's key light. */
|
|
13073
|
-
sunDirection = [
|
|
13074
|
-
.5,
|
|
13075
|
-
.8,
|
|
13076
|
-
.3
|
|
13077
|
-
];
|
|
13078
|
-
/** Sun glint/sheen tint. */
|
|
13079
|
-
sunColor = "#fff5d6";
|
|
13080
|
-
/** Sun glint/sheen strength — 0 disables the specular entirely. */
|
|
13081
|
-
sunIntensity = 1;
|
|
13082
|
-
/** Animated detail-normal strength — the close-up liquid feel (0 = off). */
|
|
13083
|
-
detailStrength = .26;
|
|
13084
|
-
/** Beer's-law absorption per meter of water depth (0 = no depth tinting).
|
|
13085
|
-
* Per-channel under the hood: red dies ~3× faster than blue, so shallows
|
|
13086
|
-
* over a bright bottom read turquoise before fading to deep blue. */
|
|
13087
|
-
absorption = .15;
|
|
13088
|
-
/** Screen-space refraction of the submerged scene (fancy only). */
|
|
13089
|
-
refraction = true;
|
|
13090
|
-
/**
|
|
13091
|
-
* Submerged-camera look: when the camera dips below this surface (inside its
|
|
13092
|
-
* XZ footprint) the scene switches to an underwater fog + tint for that frame
|
|
13093
|
-
* — the murky, limited-visibility feel of being underwater. `true` (default)
|
|
13094
|
-
* derives the murk from `color`; `false` disables it; an object
|
|
13095
|
-
* `{ color?, visibility? }` overrides the fog hue / view distance (meters).
|
|
13096
|
-
*/
|
|
13097
|
-
underwater = true;
|
|
13098
|
-
/**
|
|
13099
|
-
* Directional traveling SWELL amplitude in meters (fancy only; 0 = off, the
|
|
13100
|
-
* legacy isotropic look). Three peaked wave trains march across the surface
|
|
13101
|
-
* at real deep-water speeds — THE open-ocean dial: 0.15 = lively lake chop,
|
|
13102
|
-
* 0.4 = coastal sea, 0.8 = heavy weather. Crests feed the color ramp,
|
|
13103
|
-
* `whitecaps` and the shore foam automatically.
|
|
13104
|
-
*/
|
|
13105
|
-
swell = 0;
|
|
13106
|
-
/** Compass direction the swell TRAVELS toward, degrees (0 = +z, 90 = +x). */
|
|
13107
|
-
swellDirectionDeg = 0;
|
|
13108
|
-
/** Primary swell wavelength in meters (the two crossing trains derive). */
|
|
13109
|
-
swellWavelength = 30;
|
|
13110
|
-
/** Open-water whitecap foam on the tallest crests, 0–1 (fancy only). Works
|
|
13111
|
-
* best with `swell` — the caps ride the swell's crest lines. */
|
|
13112
|
-
whitecaps = 0;
|
|
13113
|
-
/**
|
|
13114
|
-
* THE one-choice water type: 'ocean' | 'pool' | 'lake' | 'pond' bundles
|
|
13115
|
-
* every wave/color/clarity/caustics knob into a hand-tuned sea state
|
|
13116
|
-
* ('custom', the default, changes nothing). A preset value applies ONLY
|
|
13117
|
-
* where a prop still sits at its schema default — set the type first, then
|
|
13118
|
-
* override any detail prop to taste and your value wins.
|
|
13119
|
-
*/
|
|
13120
|
-
preset = "custom";
|
|
13121
|
-
/**
|
|
13122
|
-
* Above-water CAUSTICS intensity (0 = off): the dancing light web on the
|
|
13123
|
-
* submerged bottom, seen THROUGH the surface — the pool / tropical-shallows
|
|
13124
|
-
* look. Needs `refraction` (fancy quality). Presets set it; override freely.
|
|
13125
|
-
*/
|
|
13126
|
-
causticsAbove = 0;
|
|
13127
|
-
/**
|
|
13128
|
-
* Traveling SHORE BREAKERS, 0–1 (fancy + depth pre-pass only): foam fronts
|
|
13129
|
-
* that roll down the depth gradient and break toward every beach — the
|
|
13130
|
-
* fronts follow the shore's depth contours automatically. Presets: ocean
|
|
13131
|
-
* ships 0.7; 0 (default) = off.
|
|
13132
|
-
*/
|
|
13133
|
-
shoreWaves = 0;
|
|
13134
|
-
/** Preset-layered prop read (see {@link WATER_PRESETS}): the preset value
|
|
13135
|
-
* applies only while the prop sits at its schema default. */
|
|
13136
|
-
rp(key) {
|
|
13137
|
-
const bundle = WATER_PRESETS[this.preset];
|
|
13138
|
-
const self = this;
|
|
13139
|
-
if (bundle && key in bundle) {
|
|
13140
|
-
const def = Water3D.props[key]?.default;
|
|
13141
|
-
const cur = self[key];
|
|
13142
|
-
if (typeof cur === "object" && cur !== null ? JSON.stringify(cur) === JSON.stringify(def) : cur === def) return bundle[key];
|
|
13143
|
-
}
|
|
13144
|
-
return self[key];
|
|
13145
|
-
}
|
|
13146
|
-
/** Per-preset internal look constants (fresnel curve, reflection ceiling). */
|
|
13147
|
-
presetLook() {
|
|
13148
|
-
const look = WATER_PRESETS[this.preset]?._look;
|
|
13149
|
-
return {
|
|
13150
|
-
fresnelScale: look?.fresnelScale ?? FANCY_LOOK.fresnelScale,
|
|
13151
|
-
reflectivityMax: look?.reflectivityMax ?? .6,
|
|
13152
|
-
edgeFade: look?.edgeFade ?? true,
|
|
13153
|
-
foamRangeScale: look?.foamRangeScale ?? 1,
|
|
13154
|
-
foamIntensity: look?.foamIntensity ?? FOAM_LOOK.intensity
|
|
13155
|
-
};
|
|
13156
|
-
}
|
|
13157
|
-
/** Loader hook: degenerate planes and bad enums fail at LOAD, not at render. */
|
|
13158
|
-
static validateJson(node) {
|
|
13159
|
-
const w = node;
|
|
13160
|
-
if (!((w.size[0] ?? 0) > 0) || !((w.size[1] ?? 0) > 0)) throw new IncantoError("BAD_FORMAT", `Water3D '${node.name}' size must be positive [width, depth] meters, got ${JSON.stringify(w.size)}.`, { prop: "size" });
|
|
13161
|
-
if (!WATER_QUALITIES.includes(w.quality)) throw new IncantoError("BAD_FORMAT", `Water3D '${node.name}' quality must be one of [${WATER_QUALITIES.join(", ")}], got '${w.quality}'.`, {
|
|
13162
|
-
prop: "quality",
|
|
13163
|
-
validOptions: WATER_QUALITIES
|
|
13164
|
-
});
|
|
13165
|
-
for (const [key, value] of Object.entries(w.colors)) {
|
|
13166
|
-
if (!COLOR_KEYS.includes(key)) throw new IncantoError("BAD_FORMAT", `Water3D '${node.name}' colors has unknown key '${key}'. Valid keys: [${COLOR_KEYS.join(", ")}].`, {
|
|
13167
|
-
prop: "colors",
|
|
13168
|
-
validOptions: COLOR_KEYS
|
|
13169
|
-
});
|
|
13170
|
-
if (typeof value !== "string") throw new IncantoError("BAD_FORMAT", `Water3D '${node.name}' colors.${key} must be a hex color string, got ${JSON.stringify(value)}.`, { prop: "colors" });
|
|
13171
|
-
}
|
|
13172
|
-
if (!(w.reflectionInterval > 0)) throw new IncantoError("BAD_FORMAT", `Water3D '${node.name}' reflectionInterval must be > 0 ms, got ${w.reflectionInterval}.`, { prop: "reflectionInterval" });
|
|
13173
|
-
const sun = w.sunDirection;
|
|
13174
|
-
if (!Array.isArray(sun) || sun.length !== 3 || !sun.every((v) => Number.isFinite(v)) || Math.hypot(sun[0] ?? 0, sun[1] ?? 0, sun[2] ?? 0) === 0) throw new IncantoError("BAD_FORMAT", `Water3D '${node.name}' sunDirection must be a non-zero [x, y, z] vector, got ${JSON.stringify(sun)}.`, { prop: "sunDirection" });
|
|
13175
|
-
if (!(w.sunIntensity >= 0)) throw new IncantoError("BAD_FORMAT", `Water3D '${node.name}' sunIntensity must be >= 0, got ${w.sunIntensity}.`, { prop: "sunIntensity" });
|
|
13176
|
-
if (!(w.detailStrength >= 0)) throw new IncantoError("BAD_FORMAT", `Water3D '${node.name}' detailStrength must be >= 0, got ${w.detailStrength}.`, { prop: "detailStrength" });
|
|
13177
|
-
if (!(w.absorption >= 0)) throw new IncantoError("BAD_FORMAT", `Water3D '${node.name}' absorption must be >= 0 (per-meter Beer's-law constant), got ${w.absorption}.`, { prop: "absorption" });
|
|
13178
|
-
if (!(w.swell >= 0) || !Number.isFinite(w.swell)) throw new IncantoError("BAD_FORMAT", `Water3D '${node.name}' swell must be >= 0 meters (0 = off), got ${JSON.stringify(w.swell)}.`, { prop: "swell" });
|
|
13179
|
-
if (!(w.swellWavelength >= 2)) throw new IncantoError("BAD_FORMAT", `Water3D '${node.name}' swellWavelength must be >= 2 meters, got ${JSON.stringify(w.swellWavelength)}.`, { prop: "swellWavelength" });
|
|
13180
|
-
if (!(w.whitecaps >= 0) || !(w.whitecaps <= 1)) throw new IncantoError("BAD_FORMAT", `Water3D '${node.name}' whitecaps must be in [0, 1], got ${JSON.stringify(w.whitecaps)}.`, { prop: "whitecaps" });
|
|
13181
|
-
if (!WATER_PRESET_NAMES.includes(w.preset)) throw new IncantoError("BAD_FORMAT", `Water3D '${node.name}' preset must be one of [${WATER_PRESET_NAMES.join(", ")}], got '${w.preset}'.`, {
|
|
13182
|
-
prop: "preset",
|
|
13183
|
-
validOptions: WATER_PRESET_NAMES
|
|
13184
|
-
});
|
|
13185
|
-
if (!(w.shoreWaves >= 0) || !(w.shoreWaves <= 1)) throw new IncantoError("BAD_FORMAT", `Water3D '${node.name}' shoreWaves must be in [0, 1] (0 = off), got ${JSON.stringify(w.shoreWaves)}.`, { prop: "shoreWaves" });
|
|
13186
|
-
if (!(w.causticsAbove >= 0) || !(w.causticsAbove <= 3)) throw new IncantoError("BAD_FORMAT", `Water3D '${node.name}' causticsAbove must be in [0, 3] (0 = off), got ${JSON.stringify(w.causticsAbove)}.`, { prop: "causticsAbove" });
|
|
13187
|
-
const uw = w.underwater;
|
|
13188
|
-
if (typeof uw !== "boolean") {
|
|
13189
|
-
if (typeof uw !== "object" || uw === null || Array.isArray(uw)) throw new IncantoError("BAD_FORMAT", `Water3D '${node.name}' underwater must be true, false or an object ({ color?, visibility? }), got ${JSON.stringify(uw)}.`, { prop: "underwater" });
|
|
13190
|
-
const o = uw;
|
|
13191
|
-
if (o.color !== void 0 && typeof o.color !== "string") throw new IncantoError("BAD_FORMAT", `Water3D '${node.name}' underwater.color must be a hex color string, got ${JSON.stringify(o.color)}.`, { prop: "underwater" });
|
|
13192
|
-
if (o.visibility !== void 0 && !(o.visibility > 0)) throw new IncantoError("BAD_FORMAT", `Water3D '${node.name}' underwater.visibility must be > 0 meters, got ${JSON.stringify(o.visibility)}.`, { prop: "underwater" });
|
|
13193
|
-
}
|
|
13194
|
-
}
|
|
13195
|
-
time = 0;
|
|
13196
|
-
surfaceKey = "";
|
|
13197
|
-
/** @internal Live ripple ring buffer (≤8) — tests and tools read this. */
|
|
13198
|
-
_ripples = [];
|
|
13199
|
-
inWater = /* @__PURE__ */ new Set();
|
|
13200
|
-
bobTimers = /* @__PURE__ */ new Map();
|
|
13201
|
-
/** Last position a wake ripple was dropped, per in-water body (for the trail). */
|
|
13202
|
-
wadeAnchors = /* @__PURE__ */ new Map();
|
|
13203
|
-
_scratchA = {
|
|
13204
|
-
x: 0,
|
|
13205
|
-
y: 0,
|
|
13206
|
-
z: 0
|
|
13207
|
-
};
|
|
13208
|
-
_scratchB = {
|
|
13209
|
-
x: 0,
|
|
13210
|
-
y: 0,
|
|
13211
|
-
z: 0
|
|
13212
|
-
};
|
|
13213
|
-
_bodies = [];
|
|
13214
|
-
cubeTarget = null;
|
|
13215
|
-
cubeCamera = null;
|
|
13216
|
-
/** Reflection near plane (max half extent) — recreate the camera on resize. */
|
|
13217
|
-
reflectionNear = 0;
|
|
13218
|
-
lastReflectionAt = 0;
|
|
13219
|
-
/** Per-frame scene pre-pass: depth (absorption/foam) + color grab (refraction). */
|
|
13220
|
-
scenePassTarget = null;
|
|
13221
|
-
mirrorTarget = null;
|
|
13222
|
-
mirrorCamera = new PerspectiveCamera();
|
|
13223
|
-
mirrorMatrix = new Matrix4();
|
|
13224
|
-
_createObject3D() {
|
|
13225
|
-
return new Mesh();
|
|
13226
|
-
}
|
|
13227
|
-
update(dt) {
|
|
13228
|
-
this.time += dt * this.waveSpeed;
|
|
13229
|
-
ageRipples(this._ripples, dt);
|
|
13230
|
-
if (this.interaction) this.stepInteraction(dt);
|
|
13231
|
-
}
|
|
13232
|
-
_syncObject3D() {
|
|
13233
|
-
super._syncObject3D();
|
|
13234
|
-
const mesh = this._ensureObject3D();
|
|
13235
|
-
this.rebuildSurfaceIfNeeded(mesh);
|
|
13236
|
-
if (this.quality === "simple") this.syncSimple(mesh);
|
|
13237
|
-
else this.syncFancy(mesh);
|
|
13238
|
-
}
|
|
13239
|
-
/**
|
|
13240
|
-
* @internal Renderer hand-off (see {@link RenderContext3D}): re-render the
|
|
13241
|
-
* CubeCamera reflection every `reflectionInterval` ms, plus ONE per-frame
|
|
13242
|
-
* half-res scene pre-pass whose depth texture drives Beer's-law absorption,
|
|
13243
|
-
* shoreline foam and soft intersections, and whose color texture is the
|
|
13244
|
-
* refraction grab — one extra render feeds all three.
|
|
13245
|
-
*/
|
|
13246
|
-
_onRender3D(ctx) {
|
|
13247
|
-
const mesh = this._ensureObject3D();
|
|
13248
|
-
const material = mesh.material;
|
|
13249
|
-
ctx.camera.updateWorldMatrix(true, false);
|
|
13250
|
-
ctx.camera.getWorldPosition(underwaterCamScratch);
|
|
13251
|
-
if (this.underwaterAt(underwaterCamScratch.x, underwaterCamScratch.y, underwaterCamScratch.z)) {
|
|
13252
|
-
if (material.isShaderMaterial) {
|
|
13253
|
-
const u = material.uniforms;
|
|
13254
|
-
if (u.uUseSceneDepth) u.uUseSceneDepth.value = 0;
|
|
13255
|
-
if (u.uUseRefraction) u.uUseRefraction.value = 0;
|
|
13256
|
-
if (u.uUseMirror) u.uUseMirror.value = 0;
|
|
13257
|
-
}
|
|
13258
|
-
return;
|
|
13259
|
-
}
|
|
13260
|
-
if (this.quality !== "fancy" || !material.isShaderMaterial) return;
|
|
13261
|
-
const halfDiagonal = Math.hypot((this.size[0] ?? 1) / 2, (this.size[1] ?? 1) / 2);
|
|
13262
|
-
if (!sphereInCameraFrustum(ctx.camera, worldTranslationOf(this), halfDiagonal + 1)) return;
|
|
13263
|
-
const u = material.uniforms;
|
|
13264
|
-
const now = Date.now();
|
|
13265
|
-
if (this.reflection) {
|
|
13266
|
-
if (!this.cubeTarget) this.cubeTarget = new WebGLCubeRenderTarget(REFLECTION_RESOLUTION, {
|
|
13267
|
-
generateMipmaps: true,
|
|
13268
|
-
minFilter: LinearMipmapLinearFilter,
|
|
13269
|
-
type: HalfFloatType
|
|
13270
|
-
});
|
|
13271
|
-
const near = Math.max((this.size[0] ?? 1) / 2, (this.size[1] ?? 1) / 2);
|
|
13272
|
-
if (!this.cubeCamera || this.reflectionNear !== near) {
|
|
13273
|
-
this.reflectionNear = near;
|
|
13274
|
-
this.cubeCamera = new CubeCamera(near, 1e3, this.cubeTarget);
|
|
13275
|
-
this.lastReflectionAt = 0;
|
|
13276
|
-
}
|
|
13277
|
-
if (this.lastReflectionAt === 0 || now - this.lastReflectionAt >= this.rp("reflectionInterval")) {
|
|
13278
|
-
this.lastReflectionAt = now;
|
|
13279
|
-
const center = worldTranslationOf(this);
|
|
13280
|
-
const cam = this.cubeCamera;
|
|
13281
|
-
cam.position.set(center.x, center.y + .1, center.z);
|
|
13282
|
-
const wasVisible = mesh.visible;
|
|
13283
|
-
mesh.visible = false;
|
|
13284
|
-
cam.update(ctx.gl, ctx.scene);
|
|
13285
|
-
mesh.visible = wasVisible;
|
|
13286
|
-
}
|
|
13287
|
-
if (u.uEnvironmentMap && u.uUseEnvironmentMap) {
|
|
13288
|
-
u.uEnvironmentMap.value = this.cubeTarget.texture;
|
|
13289
|
-
u.uUseEnvironmentMap.value = 1;
|
|
13290
|
-
}
|
|
13291
|
-
}
|
|
13292
|
-
if (this.mirror && u.uMirrorMap && u.uMirrorMatrix && u.uUseMirror) {
|
|
13293
|
-
ctx.gl.getDrawingBufferSize(drawingBufferScratch);
|
|
13294
|
-
const mirrorW = Math.max(1, Math.min(Math.round(drawingBufferScratch.x / 2), MIRROR_PASS_MAX));
|
|
13295
|
-
const mirrorH = Math.max(1, Math.min(Math.round(drawingBufferScratch.y / 2), MIRROR_PASS_MAX));
|
|
13296
|
-
if (this.mirrorTarget && (this.mirrorTarget.width !== mirrorW || this.mirrorTarget.height !== mirrorH)) {
|
|
13297
|
-
this.mirrorTarget.dispose();
|
|
13298
|
-
this.mirrorTarget = null;
|
|
13299
|
-
}
|
|
13300
|
-
if (!this.mirrorTarget) this.mirrorTarget = new WebGLRenderTarget(mirrorW, mirrorH, {
|
|
13301
|
-
depthBuffer: true,
|
|
13302
|
-
stencilBuffer: false,
|
|
13303
|
-
type: HalfFloatType
|
|
13304
|
-
});
|
|
13305
|
-
const surfaceY = worldTranslationOf(this).y;
|
|
13306
|
-
const cam = ctx.camera;
|
|
13307
|
-
cam.getWorldPosition(mirrorEyeScratch);
|
|
13308
|
-
cam.getWorldQuaternion(mirrorQuatScratch);
|
|
13309
|
-
mirrorTargetScratch.set(0, 0, -1).applyQuaternion(mirrorQuatScratch).add(mirrorEyeScratch);
|
|
13310
|
-
mirrorUpScratch.set(0, 1, 0).applyQuaternion(mirrorQuatScratch);
|
|
13311
|
-
mirrorEyeScratch.y = 2 * surfaceY - mirrorEyeScratch.y;
|
|
13312
|
-
mirrorTargetScratch.y = 2 * surfaceY - mirrorTargetScratch.y;
|
|
13313
|
-
mirrorUpScratch.y = -mirrorUpScratch.y;
|
|
13314
|
-
const mirrorCam = this.mirrorCamera;
|
|
13315
|
-
mirrorCam.fov = cam.fov ?? 50;
|
|
13316
|
-
mirrorCam.aspect = cam.aspect ?? mirrorW / mirrorH;
|
|
13317
|
-
mirrorCam.near = cam.near ?? .1;
|
|
13318
|
-
mirrorCam.far = cam.far ?? 1e3;
|
|
13319
|
-
mirrorCam.position.copy(mirrorEyeScratch);
|
|
13320
|
-
mirrorCam.up.copy(mirrorUpScratch);
|
|
13321
|
-
mirrorCam.lookAt(mirrorTargetScratch);
|
|
13322
|
-
mirrorCam.updateMatrixWorld(true);
|
|
13323
|
-
mirrorCam.updateProjectionMatrix();
|
|
13324
|
-
this.mirrorMatrix.set(.5, 0, 0, .5, 0, .5, 0, .5, 0, 0, .5, .5, 0, 0, 0, 1);
|
|
13325
|
-
this.mirrorMatrix.multiply(mirrorCam.projectionMatrix);
|
|
13326
|
-
this.mirrorMatrix.multiply(mirrorCam.matrixWorldInverse);
|
|
13327
|
-
const prevMirrorTarget = ctx.gl.getRenderTarget();
|
|
13328
|
-
const prevMirrorClip = ctx.gl.clippingPlanes;
|
|
13329
|
-
const wasMirrorVisible = mesh.visible;
|
|
13330
|
-
mesh.visible = false;
|
|
13331
|
-
mirrorClipPlane.constant = -(surfaceY - WATERLINE_CLIP_SLACK);
|
|
13332
|
-
ctx.gl.clippingPlanes = mirrorClipPlanes;
|
|
13333
|
-
ctx.gl.setRenderTarget(this.mirrorTarget);
|
|
13334
|
-
ctx.gl.clear(true, true, false);
|
|
13335
|
-
ctx.gl.render(ctx.scene, mirrorCam);
|
|
13336
|
-
ctx.gl.setRenderTarget(prevMirrorTarget);
|
|
13337
|
-
ctx.gl.clippingPlanes = prevMirrorClip;
|
|
13338
|
-
mesh.visible = wasMirrorVisible;
|
|
13339
|
-
u.uMirrorMap.value = this.mirrorTarget.texture;
|
|
13340
|
-
u.uMirrorMatrix.value.copy(this.mirrorMatrix);
|
|
13341
|
-
u.uUseMirror.value = 1;
|
|
13342
|
-
} else if (u.uUseMirror) u.uUseMirror.value = 0;
|
|
13343
|
-
ctx.gl.getDrawingBufferSize(drawingBufferScratch);
|
|
13344
|
-
const passW = Math.max(1, Math.min(Math.round(drawingBufferScratch.x / 2), SCENE_PASS_MAX));
|
|
13345
|
-
const passH = Math.max(1, Math.min(Math.round(drawingBufferScratch.y / 2), SCENE_PASS_MAX));
|
|
13346
|
-
if (this.scenePassTarget && (this.scenePassTarget.width !== passW || this.scenePassTarget.height !== passH)) {
|
|
13347
|
-
this.scenePassTarget.depthTexture?.dispose();
|
|
13348
|
-
this.scenePassTarget.dispose();
|
|
13349
|
-
this.scenePassTarget = null;
|
|
13350
|
-
}
|
|
13351
|
-
if (!this.scenePassTarget) {
|
|
13352
|
-
const depthTexture = new DepthTexture(passW, passH);
|
|
13353
|
-
depthTexture.type = FloatType;
|
|
13354
|
-
this.scenePassTarget = new WebGLRenderTarget(passW, passH, {
|
|
13355
|
-
depthTexture,
|
|
13356
|
-
depthBuffer: true,
|
|
13357
|
-
stencilBuffer: false,
|
|
13358
|
-
generateMipmaps: true,
|
|
13359
|
-
minFilter: LinearMipmapLinearFilter
|
|
13360
|
-
});
|
|
13361
|
-
}
|
|
13362
|
-
const prevTarget = ctx.gl.getRenderTarget();
|
|
13363
|
-
const prevAutoClear = ctx.gl.autoClear;
|
|
13364
|
-
const prevClippingPlanes = ctx.gl.clippingPlanes;
|
|
13365
|
-
const wasVisible = mesh.visible;
|
|
13366
|
-
mesh.visible = false;
|
|
13367
|
-
const waterY = worldTranslationOf(this).y;
|
|
13368
|
-
grabClipPlane.constant = waterY + WATERLINE_CLIP_SLACK;
|
|
13369
|
-
ctx.gl.clippingPlanes = grabClipPlanes;
|
|
13370
|
-
ctx.gl.setRenderTarget(this.scenePassTarget);
|
|
13371
|
-
ctx.gl.autoClear = true;
|
|
13372
|
-
ctx.gl.clear(true, true, false);
|
|
13373
|
-
ctx.gl.render(ctx.scene, ctx.camera);
|
|
13374
|
-
depthOnlyClipPlane.constant = -(waterY - WATERLINE_CLIP_SLACK);
|
|
13375
|
-
ctx.gl.clippingPlanes = depthOnlyClipPlanes;
|
|
13376
|
-
const prevOverride = ctx.scene.overrideMaterial;
|
|
13377
|
-
const prevBackground = ctx.scene.background;
|
|
13378
|
-
ctx.scene.overrideMaterial = depthOnlyMaterial;
|
|
13379
|
-
ctx.scene.background = null;
|
|
13380
|
-
ctx.gl.autoClear = false;
|
|
13381
|
-
ctx.gl.render(ctx.scene, ctx.camera);
|
|
13382
|
-
ctx.scene.overrideMaterial = prevOverride;
|
|
13383
|
-
ctx.scene.background = prevBackground;
|
|
13384
|
-
ctx.gl.setRenderTarget(prevTarget);
|
|
13385
|
-
ctx.gl.autoClear = prevAutoClear;
|
|
13386
|
-
ctx.gl.clippingPlanes = prevClippingPlanes;
|
|
13387
|
-
mesh.visible = wasVisible;
|
|
13388
|
-
if (u.uDepthTexture && u.uSceneColor && u.uCameraNear && u.uCameraFar && u.uUseSceneDepth) {
|
|
13389
|
-
u.uDepthTexture.value = this.scenePassTarget.depthTexture;
|
|
13390
|
-
u.uSceneColor.value = this.scenePassTarget.texture;
|
|
13391
|
-
u.uCameraNear.value = ctx.camera.near;
|
|
13392
|
-
u.uCameraFar.value = ctx.camera.far;
|
|
13393
|
-
u.uUseSceneDepth.value = 1;
|
|
13394
|
-
if (u.uUseRefraction) u.uUseRefraction.value = this.refraction ? 1 : 0;
|
|
13395
|
-
if (u.uEnableFoam) u.uEnableFoam.value = this.foam ? 1 : 0;
|
|
13396
|
-
}
|
|
13397
|
-
}
|
|
13398
|
-
/**
|
|
13399
|
-
* Camera-submersion query (the renderer asks every water node each frame): the
|
|
13400
|
-
* resolved underwater look + this surface's Y when the camera EYE is below
|
|
13401
|
-
* THIS surface inside its XZ footprint, else null. Deliberately gated to the
|
|
13402
|
-
* eye being underwater (not merely near the surface) so wading with the camera
|
|
13403
|
-
* above the water never tints the view. Cheap above water (geometric test first).
|
|
13404
|
-
*/
|
|
13405
|
-
underwaterAt(camX, camY, camZ) {
|
|
13406
|
-
if (this.underwater === false) return null;
|
|
13407
|
-
const center = worldTranslationOf(this, this._scratchA);
|
|
13408
|
-
if (!isCameraUnderwater(camX, camY, camZ, {
|
|
13409
|
-
centerX: center.x,
|
|
13410
|
-
centerY: center.y,
|
|
13411
|
-
centerZ: center.z,
|
|
13412
|
-
halfW: (this.size[0] ?? 1) / 2,
|
|
13413
|
-
halfD: (this.size[1] ?? 1) / 2
|
|
13414
|
-
}, UNDERWATER_MARGIN)) return null;
|
|
13415
|
-
const cfg = resolveUnderwater(this.underwater, this.rp("color"));
|
|
13416
|
-
return cfg.enabled ? {
|
|
13417
|
-
...cfg,
|
|
13418
|
-
surfaceY: center.y
|
|
13419
|
-
} : null;
|
|
13420
|
-
}
|
|
13421
|
-
free() {
|
|
13422
|
-
const mesh = this._ensureObject3D();
|
|
13423
|
-
mesh.geometry?.dispose();
|
|
13424
|
-
mesh.material?.dispose?.();
|
|
13425
|
-
this.cubeTarget?.dispose();
|
|
13426
|
-
this.cubeTarget = null;
|
|
13427
|
-
this.cubeCamera = null;
|
|
13428
|
-
this.scenePassTarget?.depthTexture?.dispose();
|
|
13429
|
-
this.scenePassTarget?.dispose();
|
|
13430
|
-
this.scenePassTarget = null;
|
|
13431
|
-
this.mirrorTarget?.dispose();
|
|
13432
|
-
this.mirrorTarget = null;
|
|
13433
|
-
super.free();
|
|
13434
|
-
}
|
|
13435
|
-
/** Swap geometry+material only when a surface-shaping prop changed. */
|
|
13436
|
-
rebuildSurfaceIfNeeded(mesh) {
|
|
13437
|
-
const key = JSON.stringify([this.size, this.quality]);
|
|
13438
|
-
if (key === this.surfaceKey && mesh.geometry.getAttribute("position")) return;
|
|
13439
|
-
this.surfaceKey = key;
|
|
13440
|
-
mesh.geometry?.dispose();
|
|
13441
|
-
mesh.material?.dispose?.();
|
|
13442
|
-
const w = this.size[0] ?? 1;
|
|
13443
|
-
const d = this.size[1] ?? 1;
|
|
13444
|
-
if (this.quality === "simple") {
|
|
13445
|
-
mesh.geometry = buildWaterGeometry(w, d, SIMPLE_SEGMENTS);
|
|
13446
|
-
mesh.material = this.buildLakeMaterial();
|
|
13447
|
-
} else {
|
|
13448
|
-
mesh.geometry = buildWaterGeometry(w, d, Math.min(Math.max(Math.round(Math.max(w, d) * FANCY_SEGMENTS_PER_M), FANCY_SEGMENTS_MIN), FANCY_SEGMENTS_MAX));
|
|
13449
|
-
mesh.material = this.buildFancyMaterial();
|
|
13450
|
-
}
|
|
13451
|
-
}
|
|
13452
|
-
buildFancyMaterial() {
|
|
13453
|
-
return new ShaderMaterial({
|
|
13454
|
-
vertexShader: WATER_VERT,
|
|
13455
|
-
fragmentShader: WATER_FRAG,
|
|
13456
|
-
fog: true,
|
|
13457
|
-
uniforms: {
|
|
13458
|
-
fogColor: { value: new Color("#ffffff") },
|
|
13459
|
-
fogNear: { value: 1 },
|
|
13460
|
-
fogFar: { value: 1e3 },
|
|
13461
|
-
fogDensity: { value: 25e-5 },
|
|
13462
|
-
uTime: { value: 0 },
|
|
13463
|
-
uOpacity: { value: this.rp("opacity") },
|
|
13464
|
-
uEnvironmentMap: { value: null },
|
|
13465
|
-
uUseEnvironmentMap: { value: 0 },
|
|
13466
|
-
uMirrorMap: { value: null },
|
|
13467
|
-
uMirrorMatrix: { value: new Matrix4() },
|
|
13468
|
-
uUseMirror: { value: 0 },
|
|
13469
|
-
uMirrorDistort: { value: MIRROR_DISTORT },
|
|
13470
|
-
uWavesAmplitude: { value: this.rp("waveHeight") * AMPLITUDE_PER_WAVE_HEIGHT },
|
|
13471
|
-
uWavesSpeed: { value: FANCY_WAVE.speed },
|
|
13472
|
-
uWavesFrequency: { value: FANCY_WAVE.frequency },
|
|
13473
|
-
uWavesPersistence: { value: FANCY_WAVE.persistence },
|
|
13474
|
-
uWavesLacunarity: { value: FANCY_WAVE.lacunarity },
|
|
13475
|
-
uWavesIterations: { value: FANCY_WAVE.iterations },
|
|
13476
|
-
uTroughColor: { value: new Color(FANCY_COLORS.trough) },
|
|
13477
|
-
uSurfaceColor: { value: new Color(FANCY_COLORS.surface) },
|
|
13478
|
-
uPeakColor: { value: new Color(FANCY_COLORS.peak) },
|
|
13479
|
-
uPeakThreshold: { value: FANCY_LOOK.peakThreshold },
|
|
13480
|
-
uPeakTransition: { value: FANCY_LOOK.peakTransition },
|
|
13481
|
-
uTroughThreshold: { value: FANCY_LOOK.troughThreshold },
|
|
13482
|
-
uTroughTransition: { value: FANCY_LOOK.troughTransition },
|
|
13483
|
-
uFresnelScale: { value: FANCY_LOOK.fresnelScale },
|
|
13484
|
-
uFresnelPower: { value: FANCY_LOOK.fresnelPower },
|
|
13485
|
-
uScale: { value: new Vector3(1, 1, 1) },
|
|
13486
|
-
uWaterCenter: { value: new Vector3() },
|
|
13487
|
-
uRipples: { value: Array.from({ length: 8 }, () => new Vector4()) },
|
|
13488
|
-
uRippleFoam: { value: Array.from({ length: 8 }, () => 0) },
|
|
13489
|
-
uSplashFoam: { value: this.splash ? 1 : 0 },
|
|
13490
|
-
uSunDirection: { value: new Vector3(.5, .8, .3) },
|
|
13491
|
-
uSunColor: { value: new Color(this.sunColor) },
|
|
13492
|
-
uSunIntensity: { value: this.rp("sunIntensity") },
|
|
13493
|
-
uDetailStrength: { value: this.rp("detailStrength") },
|
|
13494
|
-
uAbsorption: { value: this.rp("absorption") },
|
|
13495
|
-
uUseSceneDepth: { value: 0 },
|
|
13496
|
-
uUseRefraction: { value: 0 },
|
|
13497
|
-
uSceneColor: { value: null },
|
|
13498
|
-
uEnableFoam: { value: 0 },
|
|
13499
|
-
uFoamColor: { value: new Color(FOAM_LOOK.color) },
|
|
13500
|
-
uFoamIntensity: { value: FOAM_LOOK.intensity },
|
|
13501
|
-
uFoamRange: { value: foamRangeFor(this.size[0] ?? 1, this.size[1] ?? 1) },
|
|
13502
|
-
uFoamScale: { value: FOAM_LOOK.scale },
|
|
13503
|
-
uDepthTexture: { value: null },
|
|
13504
|
-
uCameraNear: { value: .1 },
|
|
13505
|
-
uCameraFar: { value: 1e3 },
|
|
13506
|
-
uEdgeColor: { value: new Color(FOAM_LOOK.edgeColor) },
|
|
13507
|
-
uEdgeIntensity: { value: FOAM_LOOK.edgeIntensity },
|
|
13508
|
-
uEdgeWidth: { value: FOAM_LOOK.edgeWidth },
|
|
13509
|
-
uSwellAmplitude: { value: this.rp("swell") },
|
|
13510
|
-
uSwellDir: { value: new Vector2(0, 1) },
|
|
13511
|
-
uSwellWavelength: { value: this.rp("swellWavelength") },
|
|
13512
|
-
uWhitecaps: { value: this.rp("whitecaps") },
|
|
13513
|
-
uWaveEnvelope: { value: 0 },
|
|
13514
|
-
uCausticsAbove: { value: this.rp("causticsAbove") },
|
|
13515
|
-
uShoreWaves: { value: this.rp("shoreWaves") },
|
|
13516
|
-
uReflectivityMax: { value: .6 },
|
|
13517
|
-
uEdgeFadeOn: { value: 1 }
|
|
13518
|
-
},
|
|
13519
|
-
transparent: true,
|
|
13520
|
-
depthTest: true,
|
|
13521
|
-
depthWrite: true,
|
|
13522
|
-
side: DoubleSide
|
|
13523
|
-
});
|
|
13524
|
-
}
|
|
13525
|
-
/** The cheap lake ShaderMaterial — fresnel sky tint + ripple normals + sun
|
|
13526
|
-
* glint + edge fade, NO scene re-renders (see lake.ts). */
|
|
13527
|
-
buildLakeMaterial() {
|
|
13528
|
-
return new ShaderMaterial({
|
|
13529
|
-
vertexShader: LAKE_VERT,
|
|
13530
|
-
fragmentShader: LAKE_FRAG,
|
|
13531
|
-
fog: true,
|
|
13532
|
-
uniforms: {
|
|
13533
|
-
fogColor: { value: new Color("#ffffff") },
|
|
13534
|
-
fogNear: { value: 1 },
|
|
13535
|
-
fogFar: { value: 1e3 },
|
|
13536
|
-
fogDensity: { value: 25e-5 },
|
|
13537
|
-
uTime: { value: 0 },
|
|
13538
|
-
uOpacity: { value: this.rp("opacity") },
|
|
13539
|
-
uColor: { value: new Color(this.rp("color")) },
|
|
13540
|
-
uSurfaceColor: { value: new Color(this.rp("color")) },
|
|
13541
|
-
uSkyColor: { value: new Color(this.rp("color")) },
|
|
13542
|
-
uSunDirection: { value: new Vector3(.5, .8, .3).normalize() },
|
|
13543
|
-
uSunColor: { value: new Color(this.sunColor) },
|
|
13544
|
-
uSunIntensity: { value: this.rp("sunIntensity") },
|
|
13545
|
-
uDetailStrength: { value: this.rp("detailStrength") },
|
|
13546
|
-
uFresnelPower: { value: FANCY_LOOK.fresnelPower },
|
|
13547
|
-
uScale: { value: new Vector3(1, 1, 1) },
|
|
13548
|
-
uWaterCenter: { value: new Vector3() },
|
|
13549
|
-
uRipples: { value: Array.from({ length: 8 }, () => new Vector4()) },
|
|
13550
|
-
uRippleFoam: { value: Array.from({ length: 8 }, () => 0) },
|
|
13551
|
-
uSplashFoam: { value: this.splash ? 1 : 0 },
|
|
13552
|
-
uFoamColor: { value: new Color(FOAM_LOOK.color) }
|
|
13553
|
-
},
|
|
13554
|
-
transparent: true,
|
|
13555
|
-
depthTest: true,
|
|
13556
|
-
depthWrite: false,
|
|
13557
|
-
side: DoubleSide
|
|
13558
|
-
});
|
|
13559
|
-
}
|
|
13560
|
-
syncFancy(mesh) {
|
|
13561
|
-
const u = mesh.material.uniforms;
|
|
13562
|
-
const w = this.size[0] ?? 1;
|
|
13563
|
-
const d = this.size[1] ?? 1;
|
|
13564
|
-
const center = worldTranslationOf(this);
|
|
13565
|
-
if (u.uTime) u.uTime.value = this.time;
|
|
13566
|
-
if (u.uOpacity) u.uOpacity.value = this.rp("opacity");
|
|
13567
|
-
if (u.uWavesAmplitude) u.uWavesAmplitude.value = this.rp("waveHeight") * AMPLITUDE_PER_WAVE_HEIGHT;
|
|
13568
|
-
(u.uScale?.value)?.set(w / 2, d / 2, d / 2);
|
|
13569
|
-
(u.uWaterCenter?.value)?.set(center.x, center.y, center.z);
|
|
13570
|
-
const palette = this.resolvePalette();
|
|
13571
|
-
(u.uTroughColor?.value)?.copy(palette.trough);
|
|
13572
|
-
(u.uSurfaceColor?.value)?.copy(palette.surface);
|
|
13573
|
-
(u.uPeakColor?.value)?.copy(palette.peak);
|
|
13574
|
-
(u.uSunDirection?.value)?.set(this.sunDirection[0] ?? 0, this.sunDirection[1] ?? 1, this.sunDirection[2] ?? 0).normalize();
|
|
13575
|
-
(u.uSunColor?.value)?.set(this.sunColor);
|
|
13576
|
-
if (u.uSunIntensity) u.uSunIntensity.value = this.rp("sunIntensity");
|
|
13577
|
-
if (u.uDetailStrength) u.uDetailStrength.value = this.rp("detailStrength");
|
|
13578
|
-
if (u.uAbsorption) u.uAbsorption.value = this.rp("absorption");
|
|
13579
|
-
if (u.uSwellAmplitude) u.uSwellAmplitude.value = this.rp("swell");
|
|
13580
|
-
if (u.uSwellWavelength) u.uSwellWavelength.value = this.rp("swellWavelength");
|
|
13581
|
-
if (u.uWhitecaps) u.uWhitecaps.value = this.rp("whitecaps");
|
|
13582
|
-
if (u.uCausticsAbove) u.uCausticsAbove.value = this.rp("causticsAbove");
|
|
13583
|
-
if (u.uShoreWaves) u.uShoreWaves.value = this.rp("shoreWaves");
|
|
13584
|
-
const look = this.presetLook();
|
|
13585
|
-
if (u.uFresnelScale) u.uFresnelScale.value = look.fresnelScale;
|
|
13586
|
-
if (u.uReflectivityMax) u.uReflectivityMax.value = look.reflectivityMax;
|
|
13587
|
-
if (u.uEdgeFadeOn) u.uEdgeFadeOn.value = look.edgeFade ? 1 : 0;
|
|
13588
|
-
if (u.uFoamRange) u.uFoamRange.value = foamRangeFor(w, d) * look.foamRangeScale;
|
|
13589
|
-
if (u.uFoamIntensity) u.uFoamIntensity.value = look.foamIntensity;
|
|
13590
|
-
const swellRad = this.swellDirectionDeg * Math.PI / 180;
|
|
13591
|
-
(u.uSwellDir?.value)?.set(Math.sin(swellRad), Math.cos(swellRad));
|
|
13592
|
-
if (u.uWaveEnvelope) u.uWaveEnvelope.value = this.rp("waveHeight") * AMPLITUDE_PER_WAVE_HEIGHT * 1.5 + this.rp("swell") * 1.03;
|
|
13593
|
-
if (!this.reflection && u.uUseEnvironmentMap) u.uUseEnvironmentMap.value = 0;
|
|
13594
|
-
if (!this.foam && u.uEnableFoam) u.uEnableFoam.value = 0;
|
|
13595
|
-
if (!this.refraction && u.uUseRefraction) u.uUseRefraction.value = 0;
|
|
13596
|
-
if (u.uSplashFoam) u.uSplashFoam.value = this.splash ? 1 : 0;
|
|
13597
|
-
const ripples = u.uRipples?.value;
|
|
13598
|
-
const foam = u.uRippleFoam?.value;
|
|
13599
|
-
if (ripples) for (let i = 0; i < ripples.length; i++) {
|
|
13600
|
-
const r = this._ripples[i];
|
|
13601
|
-
if (r) ripples[i].set(r.x, r.z, r.age, r.amp);
|
|
13602
|
-
else ripples[i].set(0, 0, 0, 0);
|
|
13603
|
-
if (foam) foam[i] = r ? r.foam ?? 0 : 0;
|
|
13604
|
-
}
|
|
13605
|
-
}
|
|
13606
|
-
/**
|
|
13607
|
-
* @internal Environment-sky sun hand-off (see {@link SunConsumer3D}): the
|
|
13608
|
-
* sky's sun drives the glint ONLY while the `sunDirection` prop sits at its
|
|
13609
|
-
* schema default — an explicitly authored prop always wins. Runs right
|
|
13610
|
-
* after `_syncObject3D` (which wrote the prop value), overwriting the
|
|
13611
|
-
* uniform for this frame.
|
|
13612
|
-
*/
|
|
13613
|
-
_applySunDirection(dir) {
|
|
13614
|
-
const def = Water3D.props.sunDirection?.default;
|
|
13615
|
-
if (!this.sunDirection.every((v, i) => v === def[i])) return;
|
|
13616
|
-
(this._ensureObject3D().material.uniforms?.uSunDirection?.value)?.set(dir[0], dir[1], dir[2]);
|
|
13617
|
-
}
|
|
13618
|
-
/** The fancy three-color ramp; a customized legacy `color` tints it. */
|
|
13619
|
-
resolvePalette() {
|
|
13620
|
-
const overrides = this.rp("colors");
|
|
13621
|
-
const tint = this.rp("color");
|
|
13622
|
-
const legacyTint = tint !== Water3D.props.color?.default ? tint : null;
|
|
13623
|
-
const surface = overrides.surface ?? legacyTint ?? FANCY_COLORS.surface;
|
|
13624
|
-
paletteScratch.surface.set(surface);
|
|
13625
|
-
if (overrides.trough) paletteScratch.trough.set(overrides.trough);
|
|
13626
|
-
else if (legacyTint) paletteScratch.trough.set(legacyTint).lerp(BLACK, .45);
|
|
13627
|
-
else paletteScratch.trough.set(FANCY_COLORS.trough);
|
|
13628
|
-
if (overrides.peak) paletteScratch.peak.set(overrides.peak);
|
|
13629
|
-
else if (legacyTint) paletteScratch.peak.set(legacyTint).lerp(WHITE, .3);
|
|
13630
|
-
else paletteScratch.peak.set(FANCY_COLORS.peak);
|
|
13631
|
-
return paletteScratch;
|
|
13632
|
-
}
|
|
13633
|
-
syncSimple(mesh) {
|
|
13634
|
-
const mat = mesh.material;
|
|
13635
|
-
mat.opacity = this.rp("opacity");
|
|
13636
|
-
const u = mat.uniforms;
|
|
13637
|
-
const w = this.size[0] ?? 1;
|
|
13638
|
-
const d = this.size[1] ?? 1;
|
|
13639
|
-
const center = worldTranslationOf(this, this._scratchA);
|
|
13640
|
-
if (u.uTime) u.uTime.value = this.time;
|
|
13641
|
-
if (u.uOpacity) u.uOpacity.value = this.rp("opacity");
|
|
13642
|
-
(u.uSurfaceColor?.value)?.set(this.rp("color")).lerp(WHITE, .12);
|
|
13643
|
-
(u.uColor?.value)?.set(this.rp("color")).lerp(BLACK, .4);
|
|
13644
|
-
(u.uSkyColor?.value)?.set(this.rp("color")).lerp(SKY_PALE, .82);
|
|
13645
|
-
(u.uScale?.value)?.set(w / 2, d / 2, d / 2);
|
|
13646
|
-
(u.uWaterCenter?.value)?.set(center.x, center.y, center.z);
|
|
13647
|
-
(u.uSunDirection?.value)?.set(this.sunDirection[0] ?? 0, this.sunDirection[1] ?? 1, this.sunDirection[2] ?? 0).normalize();
|
|
13648
|
-
(u.uSunColor?.value)?.set(this.sunColor);
|
|
13649
|
-
if (u.uSunIntensity) u.uSunIntensity.value = this.rp("sunIntensity");
|
|
13650
|
-
if (u.uDetailStrength) u.uDetailStrength.value = this.rp("detailStrength");
|
|
13651
|
-
if (u.uSplashFoam) u.uSplashFoam.value = this.splash ? 1 : 0;
|
|
13652
|
-
const uRip = u.uRipples?.value;
|
|
13653
|
-
const uFoam = u.uRippleFoam?.value;
|
|
13654
|
-
if (uRip) for (let i = 0; i < uRip.length; i++) {
|
|
13655
|
-
const r = this._ripples[i];
|
|
13656
|
-
if (r) uRip[i].set(r.x, r.z, r.age, r.amp);
|
|
13657
|
-
else uRip[i].set(0, 0, 0, 0);
|
|
13658
|
-
if (uFoam) uFoam[i] = r ? r.foam ?? 0 : 0;
|
|
13659
|
-
}
|
|
13660
|
-
const geo = mesh.geometry;
|
|
13661
|
-
const pos = geo.getAttribute("position");
|
|
13662
|
-
const norm = geo.getAttribute("normal");
|
|
13663
|
-
const t = this.time;
|
|
13664
|
-
const wh = this.rp("waveHeight");
|
|
13665
|
-
const cx = center.x;
|
|
13666
|
-
const cz = center.z;
|
|
13667
|
-
const ripples = this._ripples;
|
|
13668
|
-
const eps = .5;
|
|
13669
|
-
for (let i = 0; i < pos.count; i++) {
|
|
13670
|
-
const x = pos.getX(i);
|
|
13671
|
-
const z = pos.getZ(i);
|
|
13672
|
-
const h = simpleWaveHeight(x, z, t, wh) + ripplesHeightAt(x + cx, z + cz, ripples);
|
|
13673
|
-
pos.setY(i, h);
|
|
13674
|
-
const hX = simpleWaveHeight(x + eps, z, t, wh) + ripplesHeightAt(x + cx + eps, z + cz, ripples);
|
|
13675
|
-
const hZ = simpleWaveHeight(x, z + eps, t, wh) + ripplesHeightAt(x + cx, z + cz + eps, ripples);
|
|
13676
|
-
const nx = (h - hX) / eps;
|
|
13677
|
-
const nz = (h - hZ) / eps;
|
|
13678
|
-
const inv = 1 / Math.hypot(nx, 1, nz);
|
|
13679
|
-
norm.setXYZ(i, nx * inv, inv, nz * inv);
|
|
13680
|
-
}
|
|
13681
|
-
pos.needsUpdate = true;
|
|
13682
|
-
norm.needsUpdate = true;
|
|
13683
|
-
}
|
|
13684
|
-
stepInteraction(dt) {
|
|
13685
|
-
let top = this;
|
|
13686
|
-
while (top.parent) top = top.parent;
|
|
13687
|
-
const bodies = this._bodies;
|
|
13688
|
-
bodies.length = 0;
|
|
13689
|
-
collectBodies(top, bodies);
|
|
13690
|
-
const center = worldTranslationOf(this, this._scratchA);
|
|
13691
|
-
const halfW = (this.size[0] ?? 0) / 2;
|
|
13692
|
-
const halfD = (this.size[1] ?? 0) / 2;
|
|
13693
|
-
const samples = [];
|
|
13694
|
-
for (const body of bodies) {
|
|
13695
|
-
const p = worldTranslationOf(body, this._scratchB);
|
|
13696
|
-
if (Math.abs(p.x - center.x) > halfW || Math.abs(p.z - center.z) > halfD) continue;
|
|
13697
|
-
const footDrop = colliderFootDrop(body.collider);
|
|
13698
|
-
samples.push({
|
|
13699
|
-
id: body,
|
|
13700
|
-
x: p.x,
|
|
13701
|
-
y: p.y,
|
|
13702
|
-
z: p.z,
|
|
13703
|
-
waterline: center.y + this.waveOffsetAt(p.x, p.z, center) + footDrop
|
|
13704
|
-
});
|
|
13705
|
-
}
|
|
13706
|
-
const { entered, exited, inWater } = detectCrossings(this.inWater, samples, SURFACE_HYSTERESIS);
|
|
13707
|
-
this.inWater = inWater;
|
|
13708
|
-
const splashFoam = this.splash;
|
|
13709
|
-
for (const s of entered) {
|
|
13710
|
-
const vy = Math.abs(bodyVelocity(s.id)?.[1] ?? 0);
|
|
13711
|
-
pushRipple(this._ripples, s.x, s.z, entrySplashAmp(s.id), splashFoam ? entryFoam(vy) : 0);
|
|
13712
|
-
this.emit("entered", s.id);
|
|
13713
|
-
}
|
|
13714
|
-
for (const s of exited) {
|
|
13715
|
-
pushRipple(this._ripples, s.x, s.z, EXIT_AMP, splashFoam ? EXIT_FOAM : 0);
|
|
13716
|
-
this.emit("exited", s.id);
|
|
13717
|
-
}
|
|
13718
|
-
for (const body of stepBobTimers(this.bobTimers, inWater, dt, BOB_INTERVAL)) {
|
|
13719
|
-
const p = worldTranslationOf(body, this._scratchB);
|
|
13720
|
-
pushRipple(this._ripples, p.x, p.z, BOB_AMP);
|
|
13721
|
-
}
|
|
13722
|
-
for (const body of inWater) {
|
|
13723
|
-
const p = worldTranslationOf(body, this._scratchB);
|
|
13724
|
-
const a = this.wadeAnchors.get(body);
|
|
13725
|
-
if (!a) {
|
|
13726
|
-
this.wadeAnchors.set(body, {
|
|
13727
|
-
x: p.x,
|
|
13728
|
-
z: p.z
|
|
13729
|
-
});
|
|
13730
|
-
continue;
|
|
13731
|
-
}
|
|
13732
|
-
const dx = p.x - a.x;
|
|
13733
|
-
const dz = p.z - a.z;
|
|
13734
|
-
if (dx * dx + dz * dz >= WAKE_STEP * WAKE_STEP) {
|
|
13735
|
-
const v = bodyVelocity(body);
|
|
13736
|
-
const speed = Math.hypot(v?.[0] ?? 0, v?.[2] ?? 0);
|
|
13737
|
-
const wakeFoam = splashFoam ? Math.min(WAKE_FOAM_BASE + WAKE_FOAM_PER_SPEED * speed, WAKE_FOAM_MAX) : 0;
|
|
13738
|
-
pushRipple(this._ripples, p.x, p.z, WAKE_AMP, wakeFoam);
|
|
13739
|
-
a.x = p.x;
|
|
13740
|
-
a.z = p.z;
|
|
13741
|
-
}
|
|
13742
|
-
}
|
|
13743
|
-
for (const body of this.wadeAnchors.keys()) if (!inWater.has(body)) this.wadeAnchors.delete(body);
|
|
13744
|
-
}
|
|
13745
|
-
/** Base wave height at world (x, z) — the CPU twin of the active shader.
|
|
13746
|
-
* Ripples are excluded on purpose: a splash must not re-trigger itself. */
|
|
13747
|
-
waveOffsetAt(x, z, center) {
|
|
13748
|
-
if (this.quality === "simple") return simpleWaveHeight(x - center.x, z - center.z, this.time, this.rp("waveHeight"));
|
|
13749
|
-
return waveElevation(x, z, this.time, {
|
|
13750
|
-
amplitude: this.rp("waveHeight") * AMPLITUDE_PER_WAVE_HEIGHT,
|
|
13751
|
-
frequency: FANCY_WAVE.frequency,
|
|
13752
|
-
persistence: FANCY_WAVE.persistence,
|
|
13753
|
-
lacunarity: FANCY_WAVE.lacunarity,
|
|
13754
|
-
iterations: FANCY_WAVE.iterations,
|
|
13755
|
-
speed: FANCY_WAVE.speed,
|
|
13756
|
-
scaleNorm: WAVE_WORLD_REF
|
|
13757
|
-
});
|
|
13758
|
-
}
|
|
13759
|
-
};
|
|
13760
|
-
/** Bodies that can splash: kinematic characters and dynamic rigid bodies. */
|
|
13761
|
-
function collectBodies(node, out) {
|
|
13762
|
-
if (node instanceof CharacterBody3D || node instanceof RigidBody3D) out.push(node);
|
|
13763
|
-
for (const child of node.children) collectBodies(child, out);
|
|
13764
|
-
}
|
|
13765
|
-
/**
|
|
13766
|
-
* World position approximation: the sum of `position` props up the Node3D
|
|
13767
|
-
* ancestor chain. Ancestor ROTATION/SCALE are deliberately ignored — water
|
|
13768
|
-
* interaction assumes world-aligned layouts (the overwhelmingly common case);
|
|
13769
|
-
* it runs in update(), before the renderer refreshes any matrixWorld.
|
|
13770
|
-
*/
|
|
13771
|
-
function worldTranslationOf(node, out = {
|
|
13772
|
-
x: 0,
|
|
13773
|
-
y: 0,
|
|
13774
|
-
z: 0
|
|
13775
|
-
}) {
|
|
13776
|
-
let x = 0;
|
|
13777
|
-
let y = 0;
|
|
13778
|
-
let z = 0;
|
|
13779
|
-
let current = node;
|
|
13780
|
-
while (current) {
|
|
13781
|
-
if (current instanceof Node3D) {
|
|
13782
|
-
x += current.position[0] ?? 0;
|
|
13783
|
-
y += current.position[1] ?? 0;
|
|
13784
|
-
z += current.position[2] ?? 0;
|
|
13785
|
-
}
|
|
13786
|
-
current = current.parent;
|
|
13787
|
-
}
|
|
13788
|
-
out.x = x;
|
|
13789
|
-
out.y = y;
|
|
13790
|
-
out.z = z;
|
|
13791
|
-
return out;
|
|
13792
|
-
}
|
|
13793
|
-
/** A body's velocity vector — kinematic `velocity` or dynamic `linearVelocity`. */
|
|
13794
|
-
function bodyVelocity(body) {
|
|
13795
|
-
return body.velocity ?? body.linearVelocity;
|
|
13796
|
-
}
|
|
13797
|
-
/** Entry splash amplitude — faster falls splash harder. */
|
|
13798
|
-
function entrySplashAmp(body) {
|
|
13799
|
-
const vy = Math.abs(bodyVelocity(body)?.[1] ?? 0);
|
|
13800
|
-
return Math.min(ENTER_AMP_BASE + ENTER_AMP_PER_VY * vy, ENTER_AMP_MAX);
|
|
13801
|
-
}
|
|
13802
|
-
function buildWaterGeometry(width, depth, segments) {
|
|
13803
|
-
const geo = new PlaneGeometry(width, depth, segments, segments);
|
|
13804
|
-
geo.rotateX(-Math.PI / 2);
|
|
13805
|
-
return geo;
|
|
13806
|
-
}
|
|
13807
|
-
const BLACK = new Color("#000000");
|
|
13808
|
-
const WHITE = new Color("#ffffff");
|
|
13809
|
-
/** Pale sky-blue the lake's single hue is lifted toward for its fresnel
|
|
13810
|
-
* reflection tint (no cube map to sample — see lake.ts). */
|
|
13811
|
-
const SKY_PALE = new Color("#cfe3f0");
|
|
13812
|
-
const paletteScratch = {
|
|
13813
|
-
trough: new Color(),
|
|
13814
|
-
surface: new Color(),
|
|
13815
|
-
peak: new Color()
|
|
13816
|
-
};
|
|
13817
|
-
const drawingBufferScratch = new Vector2();
|
|
13818
|
-
/** Reused camera world-position scratch for the submersion test (zero alloc). */
|
|
13819
|
-
const underwaterCamScratch = new Vector3();
|
|
13820
|
-
/** XZ slack (m) so the underwater look persists a touch past the water's rim. */
|
|
13821
|
-
const UNDERWATER_MARGIN = 3;
|
|
13822
|
-
/** Keep-below clip plane for the scene pre-pass (constant set per render):
|
|
13823
|
-
* normal (0,−1,0) keeps y < constant — see the grab-clip note in _onRender3D. */
|
|
13824
|
-
const mirrorEyeScratch = new Vector3();
|
|
13825
|
-
const mirrorTargetScratch = new Vector3();
|
|
13826
|
-
const mirrorUpScratch = new Vector3();
|
|
13827
|
-
const mirrorQuatScratch = new Quaternion();
|
|
13828
|
-
/** Mirror pass: keep only what is ABOVE the waterline. */
|
|
13829
|
-
const mirrorClipPlane = new Plane(new Vector3(0, 1, 0), 0);
|
|
13830
|
-
const mirrorClipPlanes = [mirrorClipPlane];
|
|
13831
|
-
const grabClipPlane = new Plane(new Vector3(0, -1, 0), 0);
|
|
13832
|
-
const grabClipPlanes = [grabClipPlane];
|
|
13833
|
-
/** Keep-above twin for the depth-completion pass: normal (0,1,0) keeps
|
|
13834
|
-
* y > −constant — set per render to waterY − slack (overlapping the grab). */
|
|
13835
|
-
const depthOnlyClipPlane = new Plane(new Vector3(0, 1, 0), 0);
|
|
13836
|
-
const depthOnlyClipPlanes = [depthOnlyClipPlane];
|
|
13837
|
-
/** Shared override for the depth-completion pass — writes depth, never color. */
|
|
13838
|
-
const depthOnlyMaterial = new MeshDepthMaterial();
|
|
13839
|
-
depthOnlyMaterial.colorWrite = false;
|
|
13840
|
-
//#endregion
|
|
13841
10848
|
//#region src/3d/register.ts
|
|
13842
10849
|
/**
|
|
13843
10850
|
* Register the 3D node taxonomy (and the core nodes). Call once in your game
|
|
@@ -13875,4 +10882,4 @@ function registerNodes3D() {
|
|
|
13875
10882
|
registerNode(CharacterBody3D);
|
|
13876
10883
|
}
|
|
13877
10884
|
//#endregion
|
|
13878
|
-
export {
|
|
10885
|
+
export { QUARTER_PITCH as A, BoneAttachment3D as C, TERRAIN_THEMES as D, DEFAULT_TERRAIN_TEXTURE_BASE as E, keyboardIntensity as M, movementState as N, terrainThemeLayers as O, rigPose as P, BoneLookAt3D as S, Terrain3D as T, Flowers3D as _, Trail3D as a, CharacterController3D as b, Particles3D as c, DirectionalLight3D as d, OmniLight3D as f, DENSITY_PRESETS as g, Foliage3D as h, Tree3D as i, cameraRelative as j, Joint3D as k, ModelInstance3D as l, MeshInstance3D as m, VOXEL_PALETTE as n, River3D as o, InstancedMesh3D as p, VoxelGrid3D as r, traceDownhillPath as s, registerNodes3D as t, LoftMesh3D as u, resolveFlowerDensity as v, Billboard3D as w, Camera3D as x, FLOWER_VARIETIES as y };
|