incanto 0.75.0 → 0.76.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.d.ts +1 -1
- package/dist/2d.js +4 -4
- package/dist/3d.d.ts +121 -3
- package/dist/3d.js +4 -4
- package/dist/{create-game-DnwhJ5nW.js → create-game-BMIPKrQW.js} +5 -5
- package/dist/{create-game-DVUhHs2B.js → create-game-Dai5vfVu.js} +5 -5
- package/dist/{editor-switch-O8mxpgRX.d.ts → editor-switch-Bzt0GzVp.d.ts} +12 -2
- package/dist/{environment-presets-BP77Dl0K.js → environment-presets-e_9YCnGu.js} +348 -36
- package/dist/{gameplay-D-JuGura.js → gameplay-Bfff_beb.js} +1468 -61
- package/dist/gameplay.js +1 -1
- package/dist/index.js +1 -1
- package/dist/{physics-2d-x3bCrQf0.js → physics-2d-Tuf5LAyq.js} +2 -2
- package/dist/{physics-3d-Bz2VPQ6D.js → physics-3d-C5MOujfQ.js} +2 -2
- package/dist/{picking-Buita5T9.js → picking-DIwE48sU.js} +8 -5
- package/dist/react.js +1 -1
- package/dist/{register-DxAbTO2T.js → register-7E3P1gz6.js} +1 -1
- package/dist/{src-DLKVc6Hj.js → src-D823V07k.js} +1 -1
- package/dist/{test-HlVUeCSn.js → test-CVbxnXlv.js} +7 -7
- package/dist/test.js +1 -1
- package/dist/vite.js +33 -4
- package/editor/assets/{agent8-BXlYM9rv.js → agent8-DA-I5qhv.js} +1 -1
- package/editor/assets/{debug-S42XGP4Y.js → debug-99tEPoGm.js} +1 -1
- package/editor/assets/index-Db74LZY-.js +11579 -0
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/schemas/scene.schema.json +30 -2
- package/skills/incanto-3d-character.md +26 -1
- package/skills/incanto-editor.md +12 -2
- package/skills/incanto-environment.md +30 -12
- package/skills/incanto-node-reference.md +6 -1
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/beacon-isle-3d/src/game.scene.json +6 -1
- package/templates-app/molehill-2d/package.json +1 -1
- package/templates-app/platformer-2d/package.json +1 -1
- package/templates-app/star-survivor/package.json +1 -1
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/village-quest-3d/package.json +1 -1
- package/editor/assets/index-DgrgUKno.js +0 -11046
|
@@ -5,10 +5,10 @@ import { t as Rng } from "./rng-CDOMybym.js";
|
|
|
5
5
|
import { c as getNodeSchema, h as didYouMean, p as registerNode } from "./touch-CioTZB-y.js";
|
|
6
6
|
import { t as createNoise2D } from "./noise-D3nPpmFg.js";
|
|
7
7
|
import { i as ParticleSim, n as resolveFrames, o as PARTICLE_PRESET_NAMES, r as validateAnimations, s as applyParticlePreset, t as resolveAnimation } from "./sprite-animation-SQa5gIu2.js";
|
|
8
|
-
import { $ as Water3D, Q as tolerateUnknownAction, V as effectRng, at as PhysicsBody3D, ct as Node3D, ft as colliderFootDrop, ht as
|
|
8
|
+
import { $ as Water3D, Q as tolerateUnknownAction, V as effectRng, at as PhysicsBody3D, ct as Node3D, ft as colliderFootDrop, ht as buildSeaSpectrum, it as CharacterBody3D, mt as seaFftSupported, nt as rejectScale, ot as RigidBody3D, pt as SeaFft, rt as Area3D, st as StaticBody3D, tt as WaterCutout3D, vt as findHealth } from "./gameplay-Bfff_beb.js";
|
|
9
9
|
import { t as checkSheetGrid } from "./sheet-grid-Cea343VO.js";
|
|
10
10
|
import { n as splatWeights, t as buildHeightmap } from "./heightmap-BYgD5Edk.js";
|
|
11
|
-
import { AdditiveBlending, AnimationClip, AnimationMixer, Box3, BoxGeometry, BufferAttribute, BufferGeometry, CanvasTexture, CapsuleGeometry, ClampToEdgeWrapping, Color, ConeGeometry, CylinderGeometry, DataTexture, DirectionalLight, DoubleSide, DynamicDrawUsage, Euler, Group, IcosahedronGeometry, ImageBitmapLoader, InstancedBufferAttribute, InstancedMesh, LinearFilter, LinearMipmapLinearFilter, LoopOnce, LoopRepeat, MathUtils, Matrix4, Mesh, MeshBasicMaterial, MeshDepthMaterial, MeshPhysicalMaterial, MeshStandardMaterial, NearestFilter, NoBlending, NoColorSpace, NormalBlending, PerspectiveCamera, PlaneGeometry, PointLight, Points, PointsMaterial, Quaternion, QuaternionKeyframeTrack, RGBADepthPacking, RGBAFormat, RepeatWrapping, SRGBColorSpace, ShaderChunk, ShaderMaterial, SphereGeometry, SpotLight, Texture, TextureLoader, UniformsLib, UniformsUtils, Vector3, Vector4, VectorKeyframeTrack } from "three";
|
|
11
|
+
import { AdditiveBlending, AnimationClip, AnimationMixer, Box3, BoxGeometry, BufferAttribute, BufferGeometry, CanvasTexture, CapsuleGeometry, ClampToEdgeWrapping, Color, ConeGeometry, CubeCamera, CylinderGeometry, DataTexture, DirectionalLight, DoubleSide, DynamicDrawUsage, Euler, Group, HalfFloatType, IcosahedronGeometry, ImageBitmapLoader, InstancedBufferAttribute, InstancedMesh, LinearFilter, LinearMipmapLinearFilter, LoopOnce, LoopRepeat, MathUtils, Matrix4, Mesh, MeshBasicMaterial, MeshDepthMaterial, MeshPhysicalMaterial, MeshStandardMaterial, NearestFilter, NoBlending, NoColorSpace, NormalBlending, PerspectiveCamera, PlaneGeometry, PointLight, Points, PointsMaterial, Quaternion, QuaternionKeyframeTrack, RGBADepthPacking, RGBAFormat, RepeatWrapping, SRGBColorSpace, ShaderChunk, ShaderMaterial, SphereGeometry, SpotLight, Texture, TextureLoader, UniformsLib, UniformsUtils, Vector3, Vector4, VectorKeyframeTrack, WebGLCubeRenderTarget } from "three";
|
|
12
12
|
import { clone } from "three/addons/utils/SkeletonUtils.js";
|
|
13
13
|
//#region src/3d/textures.ts
|
|
14
14
|
/**
|
|
@@ -2979,6 +2979,40 @@ const MOVEMENT_STATES = [
|
|
|
2979
2979
|
"sneak",
|
|
2980
2980
|
"dash"
|
|
2981
2981
|
];
|
|
2982
|
+
/**
|
|
2983
|
+
* What to play when the scene's `animations` map has no clip for a state.
|
|
2984
|
+
*
|
|
2985
|
+
* A missing entry used to leave whatever was playing BEFORE, which is never
|
|
2986
|
+
* what anyone meant: a character swimming across a bay in a frozen jump pose,
|
|
2987
|
+
* because `swim` was unmapped and the last state was `airborne`. Every state
|
|
2988
|
+
* added after the original five had this — `dash`, `climb`, `sneak`, `crouch`
|
|
2989
|
+
* — and no scene in the repo mapped any of them.
|
|
2990
|
+
*
|
|
2991
|
+
* Each chain ends at `idle`, so any map with one clip in it animates.
|
|
2992
|
+
*/
|
|
2993
|
+
const CLIP_FALLBACK = {
|
|
2994
|
+
idle: [],
|
|
2995
|
+
walk: ["idle"],
|
|
2996
|
+
run: ["walk", "idle"],
|
|
2997
|
+
fastRun: [
|
|
2998
|
+
"run",
|
|
2999
|
+
"walk",
|
|
3000
|
+
"idle"
|
|
3001
|
+
],
|
|
3002
|
+
airborne: ["idle"],
|
|
3003
|
+
swim: ["walk", "idle"],
|
|
3004
|
+
climb: ["walk", "idle"],
|
|
3005
|
+
crouch: ["idle"],
|
|
3006
|
+
sneak: ["walk", "idle"],
|
|
3007
|
+
dash: [
|
|
3008
|
+
"fastRun",
|
|
3009
|
+
"run",
|
|
3010
|
+
"walk",
|
|
3011
|
+
"idle"
|
|
3012
|
+
]
|
|
3013
|
+
};
|
|
3014
|
+
/** How much of a swimmer's head rides clear of the water (m) — see `swimDepth`. */
|
|
3015
|
+
const HEAD_ABOVE_WATER = .15;
|
|
2982
3016
|
/** Air control stands down this long after a wall kick, so the kick lands. */
|
|
2983
3017
|
const WALL_LOCKOUT_SECONDS = .18;
|
|
2984
3018
|
const ACC_DELTA_TIME = 8;
|
|
@@ -3082,10 +3116,20 @@ var CharacterController3D = class extends Node3D {
|
|
|
3082
3116
|
fallGravity: { default: 2.5 },
|
|
3083
3117
|
floatHeight: { default: .01 },
|
|
3084
3118
|
stepHeight: { default: .35 },
|
|
3119
|
+
slopeLimitDeg: {
|
|
3120
|
+
default: 55,
|
|
3121
|
+
range: {
|
|
3122
|
+
min: 0,
|
|
3123
|
+
max: 90
|
|
3124
|
+
}
|
|
3125
|
+
},
|
|
3085
3126
|
/** Swimming pace (m/s) in water too deep to stand in; 0 = no swimming (the body sinks). */
|
|
3086
3127
|
swimSpeed: { default: 1.6 },
|
|
3087
3128
|
/** How far the body's origin rides BELOW the surface while swimming (m). */
|
|
3088
|
-
swimDepth: {
|
|
3129
|
+
swimDepth: {
|
|
3130
|
+
default: null,
|
|
3131
|
+
kinds: ["null", "number"]
|
|
3132
|
+
},
|
|
3089
3133
|
/** Held: swim DOWN at `swimSpeed`. Absent from the input map = no diving. */
|
|
3090
3134
|
diveAction: { default: "dive" },
|
|
3091
3135
|
/** Ladder pace (m/s): push UP into an Area3D in `ladderGroup` and the body climbs it; 0 = no ladders. */
|
|
@@ -3246,8 +3290,18 @@ var CharacterController3D = class extends Node3D {
|
|
|
3246
3290
|
* is what a drowning pit wants and what every scene got before this.
|
|
3247
3291
|
*/
|
|
3248
3292
|
swimSpeed = 1.6;
|
|
3249
|
-
/**
|
|
3250
|
-
|
|
3293
|
+
/**
|
|
3294
|
+
* How far the body's ORIGIN rides below the surface while swimming (m), or
|
|
3295
|
+
* `null` (the default) to take it from the body: deep enough that the head
|
|
3296
|
+
* rides 15 cm clear and the rest is in the water, which is what treading
|
|
3297
|
+
* water looks like.
|
|
3298
|
+
*
|
|
3299
|
+
* The origin is the middle of the capsule, so a fixed number is only ever
|
|
3300
|
+
* right for one character size. The 0.35 m this used to default to left the
|
|
3301
|
+
* head of a standard 1.68 m rig 0.46 m clear of the surface — a person
|
|
3302
|
+
* floating like a boat, chest and shoulders in the air.
|
|
3303
|
+
*/
|
|
3304
|
+
swimDepth = null;
|
|
3251
3305
|
/** Held: swim down at `swimSpeed`. Missing from the input map is fine. */
|
|
3252
3306
|
diveAction = "dive";
|
|
3253
3307
|
/**
|
|
@@ -3303,6 +3357,20 @@ var CharacterController3D = class extends Node3D {
|
|
|
3303
3357
|
* 200 kg character does not move a seesaw a 70 kg crate tips.
|
|
3304
3358
|
*/
|
|
3305
3359
|
stepHeight = .35;
|
|
3360
|
+
/**
|
|
3361
|
+
* The steepest ground this character STANDS on, in degrees (0-90).
|
|
3362
|
+
*
|
|
3363
|
+
* At or under it the ground is ground: the character holds its place with no
|
|
3364
|
+
* slide at all, however long you leave it there. Over it the ground stops
|
|
3365
|
+
* counting as ground — no hover spring, no `grounded`, no jump reset — and
|
|
3366
|
+
* the body slips off the face, which is what a slope limit means in every
|
|
3367
|
+
* engine that has one.
|
|
3368
|
+
*
|
|
3369
|
+
* 55° is a person. A mountain goat is 80; something on wheels is 25. `90`
|
|
3370
|
+
* turns the limit off — which is what this controller did before it had one,
|
|
3371
|
+
* and why it slid down a 7° hillside.
|
|
3372
|
+
*/
|
|
3373
|
+
slopeLimitDeg = 55;
|
|
3306
3374
|
mouseLook = true;
|
|
3307
3375
|
zoomMin = 0;
|
|
3308
3376
|
zoomMax = 0;
|
|
@@ -3499,6 +3567,10 @@ var CharacterController3D = class extends Node3D {
|
|
|
3499
3567
|
/** idle | walk | run | fastRun | airborne | swim — drive animations from this. */
|
|
3500
3568
|
state = "idle";
|
|
3501
3569
|
grounded = false;
|
|
3570
|
+
/** Slope of the ground under the character, degrees — 0 when there is none. */
|
|
3571
|
+
groundSlopeDeg = 0;
|
|
3572
|
+
/** Its outward normal, or null. `[0, 1, 0]` is flat ground. */
|
|
3573
|
+
groundNormal = null;
|
|
3502
3574
|
/** In water too deep to stand in — held at the surface by the swim spring. */
|
|
3503
3575
|
swimming = false;
|
|
3504
3576
|
/** The head is under the surface (diving, or a wave over it). */
|
|
@@ -3528,6 +3600,12 @@ var CharacterController3D = class extends Node3D {
|
|
|
3528
3600
|
jumping = false;
|
|
3529
3601
|
/** Seconds of air control still suppressed after a wall kick. */
|
|
3530
3602
|
wallLockout = 0;
|
|
3603
|
+
/** What the anti-slide has learned it must keep paying to hold a slope. */
|
|
3604
|
+
slideHold = 0;
|
|
3605
|
+
/** Where the body stood last step, for the anti-slide's position error. */
|
|
3606
|
+
slideFrom = null;
|
|
3607
|
+
/** Seconds since the player last asked this character to move. */
|
|
3608
|
+
stillFor = 0;
|
|
3531
3609
|
/** The outward normal of the wall being pressed into this step, if any. */
|
|
3532
3610
|
wallNormal = null;
|
|
3533
3611
|
get body() {
|
|
@@ -3572,6 +3650,8 @@ var CharacterController3D = class extends Node3D {
|
|
|
3572
3650
|
let toi = null;
|
|
3573
3651
|
/** What the probe landed on — the platform, when there is one. */
|
|
3574
3652
|
let ground = null;
|
|
3653
|
+
/** …and which way its surface faces, for the slope limit and the spring. */
|
|
3654
|
+
let normal = null;
|
|
3575
3655
|
for (const [ox, oz] of [
|
|
3576
3656
|
[0, 0],
|
|
3577
3657
|
[radius, 0],
|
|
@@ -3593,20 +3673,42 @@ var CharacterController3D = class extends Node3D {
|
|
|
3593
3673
|
if (toi === null || d < toi) {
|
|
3594
3674
|
toi = d;
|
|
3595
3675
|
ground = hit.node;
|
|
3676
|
+
const n = hit.normal;
|
|
3677
|
+
normal = n ? [
|
|
3678
|
+
n[0] ?? 0,
|
|
3679
|
+
n[1] ?? 1,
|
|
3680
|
+
n[2] ?? 0
|
|
3681
|
+
] : null;
|
|
3596
3682
|
}
|
|
3597
3683
|
if (ox === 0 && oz === 0) break;
|
|
3598
3684
|
}
|
|
3599
3685
|
}
|
|
3600
|
-
|
|
3601
|
-
const
|
|
3602
|
-
|
|
3686
|
+
const upDot = normal ? Math.max(-1, Math.min(1, normal[1] ?? 1)) : 1;
|
|
3687
|
+
const slopeDeg = toi === null ? 0 : Math.acos(Math.max(upDot, 0)) * 180 / Math.PI;
|
|
3688
|
+
this.groundSlopeDeg = slopeDeg;
|
|
3689
|
+
this.groundNormal = toi === null ? null : normal ?? [
|
|
3690
|
+
0,
|
|
3691
|
+
1,
|
|
3692
|
+
0
|
|
3693
|
+
];
|
|
3694
|
+
const standable = slopeDeg <= this.slopeLimitDeg;
|
|
3695
|
+
/** The spring's target: `floatHeight` of clearance PERPENDICULAR to the ground. */
|
|
3696
|
+
const holdDis = floatingDis / Math.max(upDot, .35);
|
|
3697
|
+
this.grounded = toi !== null && toi < holdDis * 2 && standable;
|
|
3698
|
+
if (!this.grounded) {
|
|
3699
|
+
this.slideHold = 0;
|
|
3700
|
+
this.slideFrom = null;
|
|
3701
|
+
}
|
|
3702
|
+
const carry = this.groundCarry(standable ? ground : null, dt);
|
|
3703
|
+
const footed = toi !== null && toi < holdDis * 1.3 && standable && (vel[1] ?? 0) <= .5;
|
|
3603
3704
|
const water = this.swimSpeed > 0 ? this.surfaceAt(pos[0], pos[2]) : null;
|
|
3604
3705
|
const surface = water === null ? null : water[0];
|
|
3605
|
-
const
|
|
3706
|
+
const swimDepth = this.swimDepth ?? Math.max(halfHeight + radius - HEAD_ABOVE_WATER, .05);
|
|
3707
|
+
const waterline = surface === null ? null : surface - swimDepth;
|
|
3606
3708
|
const standY = toi === null ? null : originY - toi + floatingDis + halfHeight;
|
|
3607
3709
|
const swimming = waterline !== null && surface !== null && surface > pos[1] - halfHeight && waterline !== null && (standY === null || waterline > standY) && !(this.jumping && (vel[1] ?? 0) > .5);
|
|
3608
3710
|
this.swimming = swimming;
|
|
3609
|
-
this.submerged = surface !== null && surface > pos[1] + halfHeight;
|
|
3711
|
+
this.submerged = surface !== null && surface > pos[1] + halfHeight + radius;
|
|
3610
3712
|
if (swimming) {
|
|
3611
3713
|
this.jumping = false;
|
|
3612
3714
|
this.rising = false;
|
|
@@ -3757,6 +3859,33 @@ var CharacterController3D = class extends Node3D {
|
|
|
3757
3859
|
0,
|
|
3758
3860
|
-(vel[2] - carry[1]) * DRAG_DAMPING * mass
|
|
3759
3861
|
]);
|
|
3862
|
+
this.stillFor = moving ? 0 : this.stillFor + dt;
|
|
3863
|
+
if (this.grounded && !swimming && !climbing && !onLadder && !this.jumping) {
|
|
3864
|
+
const n = this.groundNormal;
|
|
3865
|
+
const nx = n?.[0] ?? 0;
|
|
3866
|
+
const nz = n?.[2] ?? 0;
|
|
3867
|
+
const sinSlope = Math.hypot(nx, nz);
|
|
3868
|
+
if (sinSlope > .001) {
|
|
3869
|
+
const ux = nx / sinSlope;
|
|
3870
|
+
const uz = nz / sinSlope;
|
|
3871
|
+
const drift = (vel[0] - carry[0]) * ux + (vel[2] - carry[1]) * uz;
|
|
3872
|
+
if (this.stillFor < .1) {
|
|
3873
|
+
this.slideHold = 0;
|
|
3874
|
+
this.slideFrom = null;
|
|
3875
|
+
} else {
|
|
3876
|
+
const from = this.slideFrom;
|
|
3877
|
+
const moved = from === null ? 0 : (pos[0] - from[0]) * ux + (pos[2] - from[1]) * uz;
|
|
3878
|
+
this.slideFrom = [pos[0], pos[2]];
|
|
3879
|
+
this.slideHold = Math.max(-2, Math.min(2, this.slideHold + moved / dt * .5));
|
|
3880
|
+
const hold = drift + this.slideHold;
|
|
3881
|
+
if (hold !== 0) body.applyImpulse([
|
|
3882
|
+
-hold * ux * mass,
|
|
3883
|
+
0,
|
|
3884
|
+
-hold * uz * mass
|
|
3885
|
+
]);
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
}
|
|
3760
3889
|
let springToi = toi;
|
|
3761
3890
|
if (this.stepHeight > 0 && moving && toi !== null && !this.jumping && !swimming && !climbing && (vel[1] ?? 0) <= .5) {
|
|
3762
3891
|
const ahead = radius + .12;
|
|
@@ -3776,7 +3905,7 @@ var CharacterController3D = class extends Node3D {
|
|
|
3776
3905
|
}
|
|
3777
3906
|
}
|
|
3778
3907
|
if (springToi !== null && springToi < floatingDis * 2 && (!this.jumping || mantling) && !swimming && !climbing && !onLadder && (vel[1] ?? 0) < 2) {
|
|
3779
|
-
const spring = SPRING_K * (
|
|
3908
|
+
const spring = SPRING_K * (holdDis - springToi) - vel[1] * SPRING_DAMPING;
|
|
3780
3909
|
body.applyImpulse([
|
|
3781
3910
|
0,
|
|
3782
3911
|
spring * mass,
|
|
@@ -3889,7 +4018,7 @@ var CharacterController3D = class extends Node3D {
|
|
|
3889
4018
|
this.wallLockout = WALL_LOCKOUT_SECONDS;
|
|
3890
4019
|
this.emit("wallJumped", wall);
|
|
3891
4020
|
} else if (wantsJump && !onLadder && !(swimming && this.submerged) && (footed || swimming || this.coyoteLeft > 0 || this.jumpsUsed < this.maxJumps)) {
|
|
3892
|
-
const hopVel = swimming ? Math.max(this.jumpVelocity, Math.sqrt(2 * this.sceneGravity() * (this.
|
|
4021
|
+
const hopVel = swimming ? Math.max(this.jumpVelocity, Math.sqrt(2 * this.sceneGravity() * (this.effectiveSwimDepth() + halfHeight + this.stepHeight + .05))) : 0;
|
|
3893
4022
|
const jumpVel = swimming ? hopVel : this.jumpVelocity * (1 + (this.sprintJumpMultiplier - 1) * intensity);
|
|
3894
4023
|
if (swimming) this.swimHop = true;
|
|
3895
4024
|
body.linearVelocity = [
|
|
@@ -3954,7 +4083,14 @@ var CharacterController3D = class extends Node3D {
|
|
|
3954
4083
|
const next = dashing && !swimming ? "dash" : swimming ? "swim" : this.onLadder || climbing ? "climb" : this.crouching && !airborne ? moving ? "sneak" : "crouch" : movementState(!airborne, moving ? intensity : 0);
|
|
3955
4084
|
if (next !== this.state) {
|
|
3956
4085
|
this.state = next;
|
|
3957
|
-
|
|
4086
|
+
let clip = this.animations[next];
|
|
4087
|
+
if (!clip) for (const alt of CLIP_FALLBACK[next]) {
|
|
4088
|
+
const c = this.animations[alt];
|
|
4089
|
+
if (c) {
|
|
4090
|
+
clip = c;
|
|
4091
|
+
break;
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
3958
4094
|
if (clip && this.skinPath !== "") {
|
|
3959
4095
|
const skin = this.getNodeOrNull(this.skinPath);
|
|
3960
4096
|
if (skin && "animation" in skin) skin.animation = clip;
|
|
@@ -4036,6 +4172,14 @@ var CharacterController3D = class extends Node3D {
|
|
|
4036
4172
|
const g = this.tree?.engine?.scene?.physics?.gravity;
|
|
4037
4173
|
return Math.abs(g?.[1] ?? STANDARD_G) || STANDARD_G;
|
|
4038
4174
|
}
|
|
4175
|
+
/** `swimDepth`, or what it is derived to be for this body (see the prop). */
|
|
4176
|
+
effectiveSwimDepth() {
|
|
4177
|
+
if (this.swimDepth !== null) return this.swimDepth;
|
|
4178
|
+
const body = this.parent;
|
|
4179
|
+
const radius = body?.collider?.radius ?? .32;
|
|
4180
|
+
const halfHeight = (body?.collider?.height ?? 1) / 2;
|
|
4181
|
+
return Math.max(halfHeight + radius - HEAD_ABOVE_WATER, .05);
|
|
4182
|
+
}
|
|
4039
4183
|
surfaceAt(x, z) {
|
|
4040
4184
|
if (this.waterScanIn <= 0) {
|
|
4041
4185
|
this.waters.length = 0;
|
|
@@ -10385,6 +10529,15 @@ uniform float uRippleStrength;
|
|
|
10385
10529
|
uniform float uFlowRate;
|
|
10386
10530
|
uniform float uSparkle;
|
|
10387
10531
|
uniform float uFresnelScale;
|
|
10532
|
+
// the river's own sky cube (river-3d.ts): what the surface mirrors instead of
|
|
10533
|
+
// the painted three-colour gradient, which stays as the headless fallback
|
|
10534
|
+
uniform samplerCube uEnvironmentMap;
|
|
10535
|
+
uniform float uUseEnvironmentMap;
|
|
10536
|
+
// the live ripple layer (fft-gpu.ts), a 6 m patch in the ribbon's own frame
|
|
10537
|
+
uniform float uRippleOn;
|
|
10538
|
+
uniform sampler2D uRippleNormal;
|
|
10539
|
+
uniform float uRipplePatch;
|
|
10540
|
+
uniform float uRippleGain;
|
|
10388
10541
|
/**
|
|
10389
10542
|
* Rocks standing in the current: xz = centre, z = radius, w = how much of the
|
|
10390
10543
|
* water they break (0 = drowned and quiet, 1 = the crest stands proud).
|
|
@@ -10427,6 +10580,32 @@ float riverStreak(vec2 p) {
|
|
|
10427
10580
|
return riverNoise(q) * 0.5 + riverNoise(q * 3.3 + 7.3) * 0.5;
|
|
10428
10581
|
}
|
|
10429
10582
|
|
|
10583
|
+
/** water-that-reads-as-water: the FINE wavelets — the two ripple octaves are
|
|
10584
|
+
* a hand-span and up, so at the range a player actually stands from a creek
|
|
10585
|
+
* the surface tilted in soft blobs and nothing on it could ever face the sun
|
|
10586
|
+
* (the gel read). A third, much shorter field, differenced at its own step
|
|
10587
|
+
* (the ripple's 0.35 step is wider than these wavelets), gives the normal the
|
|
10588
|
+
* facets that glitter and that shatter the reflection; it fades out where a
|
|
10589
|
+
* pixel would integrate it into speckle. Squeezed downstream like the rest. */
|
|
10590
|
+
// Karis' split-sum fit: what a ROUGH surface reflects at this angle — see the
|
|
10591
|
+
// sea-state fresnel note in shaders.ts. A river at a grazing view is a field
|
|
10592
|
+
// of wavelets, not a mirror, and the flat-Schlick sheet of sky it used to wear
|
|
10593
|
+
// is the plastic look this replaces.
|
|
10594
|
+
vec2 envBRDFApprox(float roughness, float NoV) {
|
|
10595
|
+
const vec4 c0 = vec4(-1.0, -0.0275, -0.572, 0.022);
|
|
10596
|
+
const vec4 c1 = vec4(1.0, 0.0425, 1.04, -0.04);
|
|
10597
|
+
vec4 r = roughness * c0 + c1;
|
|
10598
|
+
float a004 = min(r.x * r.x, exp2(-9.28 * NoV)) * r.x + r.y;
|
|
10599
|
+
return vec2(-1.04, 1.04) * a004 + r.zw;
|
|
10600
|
+
}
|
|
10601
|
+
|
|
10602
|
+
vec2 riverFineSlope(vec2 p) {
|
|
10603
|
+
vec2 q = vec2(p.x * 10.7, p.y * 3.6) + 5.7;
|
|
10604
|
+
const float e = 0.08;
|
|
10605
|
+
float h = riverNoise(q);
|
|
10606
|
+
return vec2(riverNoise(q + vec2(e, 0.0)) - h, riverNoise(q + vec2(0.0, e)) - h) / e;
|
|
10607
|
+
}
|
|
10608
|
+
|
|
10430
10609
|
void main() {
|
|
10431
10610
|
float speed = vRiver.x;
|
|
10432
10611
|
float column = vRiver.y;
|
|
@@ -10500,7 +10679,26 @@ void main() {
|
|
|
10500
10679
|
// the gradient is a NOISE slope, not a water slope: clamp it before it tilts
|
|
10501
10680
|
// the normal past a real wavelet (an over-driven normal throws fresnel to its
|
|
10502
10681
|
// maximum everywhere and the river turns into a sheet of white sky)
|
|
10503
|
-
|
|
10682
|
+
float camDist = length(cameraPosition - vWorldPosition);
|
|
10683
|
+
float fineFade = (1.0 - smoothstep(6.0, 34.0, camDist)) * lively;
|
|
10684
|
+
vec2 fine = vec2(0.0);
|
|
10685
|
+
if (fineFade > 0.01) {
|
|
10686
|
+
vec2 fineA = riverFineSlope(base - vec2(travel * pA, 0.0));
|
|
10687
|
+
vec2 fineB = riverFineSlope(base - vec2(travel * pB, 0.0));
|
|
10688
|
+
// under the ripple FFT the noise keeps only the sub-pixel flash
|
|
10689
|
+
fine = mix(fineA, fineB, blend) *
|
|
10690
|
+
(0.028 * fineFade * (0.5 + 0.5 * uSparkle) * (uRippleOn > 0.5 ? 0.45 : 1.0));
|
|
10691
|
+
}
|
|
10692
|
+
// the ripple FFT, carried downstream in the ribbon's metres with the same
|
|
10693
|
+
// dual-phase crossfade the noise uses (a field scrolled at a speed that
|
|
10694
|
+
// varies along the river smears; two half-period copies do not)
|
|
10695
|
+
if (uRippleOn > 0.5) {
|
|
10696
|
+
float travelM = speed / max(uFlowRate, 0.001);
|
|
10697
|
+
vec2 rA = texture2D(uRippleNormal, (vFlowUv - vec2(travelM * pA, 0.0)) / uRipplePatch, -1.0).xy;
|
|
10698
|
+
vec2 rB = texture2D(uRippleNormal, (vFlowUv - vec2(travelM * pB, 0.0)) / uRipplePatch, -1.0).xy;
|
|
10699
|
+
fine += mix(rA, rB, blend) * (uRippleGain * lively);
|
|
10700
|
+
}
|
|
10701
|
+
vec2 slopeXY = clamp(ripple.yz * uRippleStrength * lively + fine, vec2(-0.5), vec2(0.5));
|
|
10504
10702
|
vec3 normal = normalize(vSurfaceNormal - dir3 * slopeXY.x - perp3 * slopeXY.y);
|
|
10505
10703
|
|
|
10506
10704
|
vec3 viewDir = normalize(cameraPosition - vWorldPosition);
|
|
@@ -10604,31 +10802,53 @@ void main() {
|
|
|
10604
10802
|
// eye is near the water — hold its dark tone well past the horizon line
|
|
10605
10803
|
vec3 sky = mix(uBankColor, uHorizonColor, smoothstep(0.05, 0.34, reflectDir.y));
|
|
10606
10804
|
sky = mix(sky, uSkyColor, smoothstep(0.34, 0.85, reflectDir.y));
|
|
10607
|
-
|
|
10608
|
-
|
|
10609
|
-
|
|
10610
|
-
0.
|
|
10611
|
-
|
|
10805
|
+
if (uUseEnvironmentMap > 0.5) {
|
|
10806
|
+
// the REAL sky, from the river's own cube — near-clipped, so the lowest
|
|
10807
|
+
// rays (which meet the bank a few metres off) keep the bank tone
|
|
10808
|
+
vec3 lifted = vec3(reflectDir.x, max(reflectDir.y, 0.06), reflectDir.z);
|
|
10809
|
+
vec3 cube = min(textureCube(uEnvironmentMap, lifted).rgb, vec3(2.5));
|
|
10810
|
+
sky = mix(uBankColor, cube, smoothstep(0.02, 0.2, reflectDir.y));
|
|
10811
|
+
}
|
|
10812
|
+
float rough = clamp(0.045 + 0.28 * smoothstep(8.0, 140.0, camDist), 0.04, 0.35);
|
|
10813
|
+
float NoVf = clamp(dot(normal, viewDir), 0.0, 1.0);
|
|
10814
|
+
float fresnel = uFresnelScale * (0.02 + 0.98 * pow(1.0 - NoVf, 5.0));
|
|
10815
|
+
{
|
|
10816
|
+
vec2 fRough = envBRDFApprox(rough, NoVf);
|
|
10817
|
+
vec2 fSmooth = envBRDFApprox(0.02, NoVf);
|
|
10818
|
+
fresnel *= clamp((0.02 * fRough.x + fRough.y) / max(0.02 * fSmooth.x + fSmooth.y, 1e-3), 0.0, 1.35);
|
|
10819
|
+
}
|
|
10820
|
+
fresnel = clamp(fresnel, 0.0, 0.82);
|
|
10612
10821
|
|
|
10613
10822
|
vec3 sunDir = normalize(uSunDirection);
|
|
10614
10823
|
vec3 halfVec = normalize(sunDir + viewDir);
|
|
10615
|
-
|
|
10616
|
-
//
|
|
10617
|
-
//
|
|
10618
|
-
//
|
|
10619
|
-
//
|
|
10620
|
-
//
|
|
10621
|
-
//
|
|
10622
|
-
//
|
|
10623
|
-
//
|
|
10624
|
-
//
|
|
10625
|
-
float
|
|
10626
|
-
|
|
10627
|
-
|
|
10628
|
-
float
|
|
10629
|
-
|
|
10824
|
+
// ── the sun as a microfacet highlight ──────────────────────────────────
|
|
10825
|
+
// water-that-reads-as-water: the two Blinn-Phong lobes (160 + a whisper of
|
|
10826
|
+
// 34) were a hand-shaped sheen gated by the ripple field, capped at 1.2 —
|
|
10827
|
+
// a running creek under a 40°-high sun showed no sun at all. One GGX lobe
|
|
10828
|
+
// over the wavelet normal: resolved facets near the eye flash (the glitter
|
|
10829
|
+
// a riffle throws), and with distance the roughness grows so the same lobe
|
|
10830
|
+
// widens into the soft sheen a river shows from across the valley. HDR by
|
|
10831
|
+
// design — the sun's reflection is the brightest thing on the water and
|
|
10832
|
+
// the bloom pass reads it. RIVER_SUN_RADIANCE scales the sun against the
|
|
10833
|
+
// unit-ish sky gradient this shader mirrors.
|
|
10834
|
+
const float RIVER_SUN_RADIANCE = 30.0;
|
|
10835
|
+
float NoL = clamp(dot(normal, sunDir), 0.0, 1.0);
|
|
10836
|
+
float NoV = max(dot(normal, viewDir), 1e-3);
|
|
10837
|
+
float NoH = clamp(dot(normal, halfVec), 0.0, 1.0);
|
|
10838
|
+
float LoH = clamp(dot(sunDir, halfVec), 0.0, 1.0);
|
|
10839
|
+
float a2 = rough * rough;
|
|
10840
|
+
float dD = NoH * NoH * (a2 - 1.0) + 1.0;
|
|
10841
|
+
float D = a2 / (3.14159265 * dD * dD);
|
|
10842
|
+
float gV = NoL * sqrt(NoV * NoV * (1.0 - a2) + a2);
|
|
10843
|
+
float gL = NoV * sqrt(NoL * NoL * (1.0 - a2) + a2);
|
|
10844
|
+
float Vis = 0.5 / max(gV + gL, 1e-4);
|
|
10845
|
+
float Fh = 0.02 + 0.98 * pow(1.0 - LoH, 5.0);
|
|
10846
|
+
float spec = D * Vis * Fh * NoL * RIVER_SUN_RADIANCE * uSunIntensity;
|
|
10847
|
+
spec *= 0.5 + 0.5 * uSparkle; // the ripples dial still tunes how much sun the water throws
|
|
10630
10848
|
spec *= 1.0 - foam * 0.85; // froth is rough — it kills the mirror
|
|
10631
|
-
|
|
10849
|
+
// a facet under a pixel aliases into a lone flash: the roughness ramp
|
|
10850
|
+
// widens the lobe out there, the cap catches the last isolated ones
|
|
10851
|
+
spec = min(spec, mix(5.0, 1.2, smoothstep(30.0, 200.0, camDist)));
|
|
10632
10852
|
|
|
10633
10853
|
fresnel *= 1.0 - 0.85 * fall; // froth scatters; it does not reflect
|
|
10634
10854
|
vec3 color = mix(body, sky, fresnel);
|
|
@@ -10728,6 +10948,21 @@ const FOAM_COLOR = "#e9f2ef";
|
|
|
10728
10948
|
/** Per-frame scratch (no allocation in the render path). */
|
|
10729
10949
|
const rockCamScratch = new Vector3();
|
|
10730
10950
|
/** Fixed look constants — the props stay intent-level, these carry the craft. */
|
|
10951
|
+
/** The river's own sky: a slow cube of the world beyond its banks (the sky,
|
|
10952
|
+
* the far hills), refreshed every couple of seconds — what the surface
|
|
10953
|
+
* mirrors instead of a painted three-colour gradient. Near-clipped so the
|
|
10954
|
+
* banks themselves, metres away, never smear across it. */
|
|
10955
|
+
const RIVER_SKY_RESOLUTION = 128;
|
|
10956
|
+
const RIVER_SKY_INTERVAL = 2e3;
|
|
10957
|
+
const RIVER_SKY_NEAR = 60;
|
|
10958
|
+
/** The live ripple layer (see Water3D): a 6 m spectrum patch advected with
|
|
10959
|
+
* the current in the ribbon's own frame. */
|
|
10960
|
+
const RIVER_RIPPLE_SIZE = 128;
|
|
10961
|
+
const RIVER_RIPPLE_PATCH = 6;
|
|
10962
|
+
const RIVER_RIPPLE_FETCH = 150;
|
|
10963
|
+
const RIVER_RIPPLE_SEED = 4242;
|
|
10964
|
+
/** Ripple rms slope per unit of `ripples`. */
|
|
10965
|
+
const RIVER_RIPPLE_SLOPE = .07;
|
|
10731
10966
|
const LOOK = {
|
|
10732
10967
|
/** Pattern units per meter of flow space. */
|
|
10733
10968
|
rippleScale: .85,
|
|
@@ -10870,6 +11105,12 @@ var River3D = class River3D extends Node3D {
|
|
|
10870
11105
|
0
|
|
10871
11106
|
];
|
|
10872
11107
|
_bodies = [];
|
|
11108
|
+
cubeTarget = null;
|
|
11109
|
+
cubeCamera = null;
|
|
11110
|
+
lastSkyAt = 0;
|
|
11111
|
+
rippleFft = null;
|
|
11112
|
+
rippleSpectrum = null;
|
|
11113
|
+
fftUnsupported = false;
|
|
10873
11114
|
/** Loader hook: a malformed river fails at LOAD, not as an invisible ribbon. */
|
|
10874
11115
|
static validateJson(node) {
|
|
10875
11116
|
rejectScale(node, "the flow query and its collider both");
|
|
@@ -10936,8 +11177,68 @@ var River3D = class River3D extends Node3D {
|
|
|
10936
11177
|
this.rockScanAt = this.time;
|
|
10937
11178
|
this.scanRocks();
|
|
10938
11179
|
}
|
|
10939
|
-
const
|
|
11180
|
+
const mesh = this._ensureObject3D();
|
|
11181
|
+
const u = mesh.material.uniforms;
|
|
10940
11182
|
this.syncRocks(u, ctx.camera.getWorldPosition(rockCamScratch));
|
|
11183
|
+
if (typeof ctx.gl?.extensions?.has === "function") {
|
|
11184
|
+
this.syncSky(ctx, mesh, u);
|
|
11185
|
+
this.syncRipple(ctx, u);
|
|
11186
|
+
}
|
|
11187
|
+
}
|
|
11188
|
+
/** The sky cube: the world beyond the banks, from above the channel's middle. */
|
|
11189
|
+
syncSky(ctx, mesh, u) {
|
|
11190
|
+
if (!this.cubeTarget) {
|
|
11191
|
+
this.cubeTarget = new WebGLCubeRenderTarget(RIVER_SKY_RESOLUTION, {
|
|
11192
|
+
generateMipmaps: true,
|
|
11193
|
+
minFilter: LinearMipmapLinearFilter,
|
|
11194
|
+
type: HalfFloatType
|
|
11195
|
+
});
|
|
11196
|
+
this.cubeCamera = new CubeCamera(RIVER_SKY_NEAR, 4e3, this.cubeTarget);
|
|
11197
|
+
this.lastSkyAt = 0;
|
|
11198
|
+
}
|
|
11199
|
+
const now = Date.now();
|
|
11200
|
+
if (this.lastSkyAt === 0 || now - this.lastSkyAt >= RIVER_SKY_INTERVAL) {
|
|
11201
|
+
this.lastSkyAt = now;
|
|
11202
|
+
const b = this.ringBounds();
|
|
11203
|
+
const [wx, wy, wz] = this.world;
|
|
11204
|
+
const cam = this.cubeCamera;
|
|
11205
|
+
cam.position.set(wx + (b.minX + b.maxX) / 2, wy + 3, wz + (b.minZ + b.maxZ) / 2);
|
|
11206
|
+
const wasVisible = mesh.visible;
|
|
11207
|
+
mesh.visible = false;
|
|
11208
|
+
cam.update(ctx.gl, ctx.scene);
|
|
11209
|
+
mesh.visible = wasVisible;
|
|
11210
|
+
}
|
|
11211
|
+
if (u.uEnvironmentMap) u.uEnvironmentMap.value = this.cubeTarget.texture;
|
|
11212
|
+
if (u.uUseEnvironmentMap) u.uUseEnvironmentMap.value = 1;
|
|
11213
|
+
}
|
|
11214
|
+
/** The ripple FFT, advected by the fragment in the ribbon's own frame. */
|
|
11215
|
+
syncRipple(ctx, u) {
|
|
11216
|
+
if (this.fftUnsupported) return;
|
|
11217
|
+
if (!this.rippleFft) {
|
|
11218
|
+
if (!seaFftSupported(ctx.gl)) {
|
|
11219
|
+
this.fftUnsupported = true;
|
|
11220
|
+
return;
|
|
11221
|
+
}
|
|
11222
|
+
this.rippleFft = new SeaFft(RIVER_RIPPLE_SIZE, RIVER_RIPPLE_PATCH);
|
|
11223
|
+
this.rippleSpectrum = buildSeaSpectrum({
|
|
11224
|
+
size: RIVER_RIPPLE_SIZE,
|
|
11225
|
+
patch: RIVER_RIPPLE_PATCH,
|
|
11226
|
+
wind: 3,
|
|
11227
|
+
windDirectionDeg: 90,
|
|
11228
|
+
seed: RIVER_RIPPLE_SEED,
|
|
11229
|
+
choppiness: .3,
|
|
11230
|
+
fetch: RIVER_RIPPLE_FETCH,
|
|
11231
|
+
modes: 0
|
|
11232
|
+
});
|
|
11233
|
+
this.rippleFft.setSpectrum(this.rippleSpectrum);
|
|
11234
|
+
}
|
|
11235
|
+
this.rippleFft.update(ctx.gl, this.time);
|
|
11236
|
+
if (u.uRippleNormal) u.uRippleNormal.value = this.rippleFft.normal;
|
|
11237
|
+
if (u.uRippleOn) u.uRippleOn.value = 1;
|
|
11238
|
+
if (u.uRippleGain && this.rippleSpectrum) {
|
|
11239
|
+
const rms = this.rippleSpectrum.rmsSlope;
|
|
11240
|
+
u.uRippleGain.value = rms > 0 ? RIVER_RIPPLE_SLOPE * this.ripples / rms : 0;
|
|
11241
|
+
}
|
|
10941
11242
|
}
|
|
10942
11243
|
/**
|
|
10943
11244
|
* @internal Water surface height over a world point, or null where this
|
|
@@ -11026,6 +11327,11 @@ var River3D = class River3D extends Node3D {
|
|
|
11026
11327
|
const mesh = this._ensureObject3D();
|
|
11027
11328
|
mesh.geometry?.dispose();
|
|
11028
11329
|
mesh.material?.dispose?.();
|
|
11330
|
+
this.cubeTarget?.dispose();
|
|
11331
|
+
this.cubeTarget = null;
|
|
11332
|
+
this.cubeCamera = null;
|
|
11333
|
+
this.rippleFft?.dispose();
|
|
11334
|
+
this.rippleFft = null;
|
|
11029
11335
|
super.free();
|
|
11030
11336
|
}
|
|
11031
11337
|
widthProfile() {
|
|
@@ -11185,6 +11491,12 @@ var River3D = class River3D extends Node3D {
|
|
|
11185
11491
|
uRockCount: { value: 0 },
|
|
11186
11492
|
uRocks: { value: Array.from({ length: ROCK_SLOTS }, () => new Vector4()) },
|
|
11187
11493
|
uFresnelScale: { value: LOOK.fresnelScale },
|
|
11494
|
+
uEnvironmentMap: { value: null },
|
|
11495
|
+
uUseEnvironmentMap: { value: 0 },
|
|
11496
|
+
uRippleOn: { value: 0 },
|
|
11497
|
+
uRippleNormal: { value: null },
|
|
11498
|
+
uRipplePatch: { value: RIVER_RIPPLE_PATCH },
|
|
11499
|
+
uRippleGain: { value: 0 },
|
|
11188
11500
|
uStandingAmp: { value: LOOK.standingAmp },
|
|
11189
11501
|
uGlideAmp: { value: LOOK.glideAmp }
|
|
11190
11502
|
},
|