incanto 0.25.4 → 0.27.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 +1 -1
- package/dist/3d.d.ts +140 -2
- package/dist/3d.js +5 -5
- package/dist/{create-game-iYptFr-q.js → create-game-C0bPoURs.js} +1 -1
- package/dist/{create-game-DyAPwm0j.js → create-game-DUAtTqID.js} +3 -3
- package/dist/env.d.ts +6 -0
- package/dist/env.js +8 -5
- package/dist/{gameplay-BFzBVqhr.js → gameplay-Ddk13pQ6.js} +229 -2
- package/dist/gameplay.js +1 -1
- package/dist/index.js +1 -1
- package/dist/{physics-3d-ob23XE0Y.js → physics-3d-4mGzOZ2J.js} +2 -2
- package/dist/react.js +1 -1
- package/dist/{register-CmtGqGSS.js → register-DcHXS1MA.js} +426 -17
- package/dist/test.js +4 -4
- package/editor/assets/{agent8-DzaYTNiK.js → agent8-DElIPozC.js} +1 -1
- package/editor/assets/{index-BGtXxzJn.js → index-BrneVqN0.js} +145 -49
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/schemas/scene.schema.json +115 -0
- package/skills/incanto-environment.md +88 -9
- package/skills/incanto-node-reference.md +15 -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
|
@@ -4,7 +4,7 @@ import { t as Rng } from "./rng-DP-SR7eg.js";
|
|
|
4
4
|
import { i as getNodeSchema, l as registerNode } from "./registry-BVJ2HbCn.js";
|
|
5
5
|
import { t as createNoise2D } from "./noise-CGUMx44x.js";
|
|
6
6
|
import { i as applyParticlePreset, r as PARTICLE_PRESET_NAMES, t as ParticleSim } from "./particle-sim-Bw7hB93B.js";
|
|
7
|
-
import { A as Water3D,
|
|
7
|
+
import { A as Water3D, F as PhysicsBody3D, H as colliderFootDrop, I as RigidBody3D, L as StaticBody3D, M as WaterCutout3D, N as Area3D, P as CharacterBody3D, R as Node3D } from "./gameplay-Ddk13pQ6.js";
|
|
8
8
|
import { n as splatWeights, t as buildHeightmap } from "./heightmap-CRK0M4jT.js";
|
|
9
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";
|
|
@@ -898,8 +898,15 @@ function themePatchTransfer(theme) {
|
|
|
898
898
|
}
|
|
899
899
|
//#endregion
|
|
900
900
|
//#region src/3d/nodes/terrain-3d.ts
|
|
901
|
-
/**
|
|
902
|
-
|
|
901
|
+
/**
|
|
902
|
+
* Hard cap on grid segments per side. 128 is the default and the right answer
|
|
903
|
+
* for most maps; 256 (66k vertices) exists for terrain that has to hold
|
|
904
|
+
* features finer than a cell — a 2 m creek bed on a 260 m map needs ~1 m
|
|
905
|
+
* cells or the trench smooths away. The cost is CPU at build time (noise
|
|
906
|
+
* octaves + carving per vertex), paid once per shaping change, so raise it
|
|
907
|
+
* deliberately rather than by default.
|
|
908
|
+
*/
|
|
909
|
+
const MAX_RESOLUTION = 256;
|
|
903
910
|
/**
|
|
904
911
|
* Procedural heightfield terrain with biome texture splatting — seeded
|
|
905
912
|
* simplex octaves displace a PlaneGeometry once on the CPU, per-vertex
|
|
@@ -982,6 +989,14 @@ var Terrain3D = class extends Node3D {
|
|
|
982
989
|
* bed to sit in, and the collider/heightAt/drape all see it. */
|
|
983
990
|
channels = [];
|
|
984
991
|
/**
|
|
992
|
+
* Trenches other NODES cut in this terrain — a `River3D` carving its own bed.
|
|
993
|
+
* Keyed by the node that owns each, so a moving or re-pathed river replaces
|
|
994
|
+
* its cut instead of stacking a new one. Runtime state, never serialized:
|
|
995
|
+
* the scene file says "there is a river here", not "here is the ditch it
|
|
996
|
+
* dug", and re-loading the scene digs it again.
|
|
997
|
+
*/
|
|
998
|
+
externalChannels = /* @__PURE__ */ new Map();
|
|
999
|
+
/**
|
|
985
1000
|
* WET SAND band: `{ y, band? }` darkens + glosses the splat in a noisy,
|
|
986
1001
|
* breathing band just above height `y` (a waterline) — the trace of the
|
|
987
1002
|
* last swash runup. `band` is the max height above `y` that reads wet
|
|
@@ -1061,6 +1076,20 @@ var Terrain3D = class extends Node3D {
|
|
|
1061
1076
|
}
|
|
1062
1077
|
return this._heightmap().heightAt(x - ox, z - oz) + oy;
|
|
1063
1078
|
}
|
|
1079
|
+
/**
|
|
1080
|
+
* @internal Cut (spec) or release (null) a trench another node owns. A
|
|
1081
|
+
* `River3D` calls this to dig its own bed; the heightmap, the collider,
|
|
1082
|
+
* `heightAt`, the drape and the mesh all pick it up on their next rebuild.
|
|
1083
|
+
*/
|
|
1084
|
+
_setExternalChannel(owner, specs) {
|
|
1085
|
+
if (specs === null || specs.length === 0) this.externalChannels.delete(owner);
|
|
1086
|
+
else this.externalChannels.set(owner, specs);
|
|
1087
|
+
}
|
|
1088
|
+
/** Authored trenches plus the ones other nodes cut. */
|
|
1089
|
+
allChannels() {
|
|
1090
|
+
if (this.externalChannels.size === 0) return this.channels;
|
|
1091
|
+
return [...this.channels, ...[...this.externalChannels.values()].flat()];
|
|
1092
|
+
}
|
|
1064
1093
|
/** @internal The pure heightfield (lazily built — physics pulls this). */
|
|
1065
1094
|
_heightmap() {
|
|
1066
1095
|
const segs = this.segments();
|
|
@@ -1074,7 +1103,7 @@ var Terrain3D = class extends Node3D {
|
|
|
1074
1103
|
this.flatThreshold,
|
|
1075
1104
|
this.effectiveIslandEdge(),
|
|
1076
1105
|
this.basins,
|
|
1077
|
-
this.
|
|
1106
|
+
this.allChannels()
|
|
1078
1107
|
]);
|
|
1079
1108
|
if (!this.heightmap || key !== this.heightmapKey) {
|
|
1080
1109
|
this.heightmap = buildHeightmap({
|
|
@@ -1089,7 +1118,7 @@ var Terrain3D = class extends Node3D {
|
|
|
1089
1118
|
flatThreshold: this.flatThreshold,
|
|
1090
1119
|
islandEdge: this.effectiveIslandEdge(),
|
|
1091
1120
|
basins: this.basins,
|
|
1092
|
-
channels: this.
|
|
1121
|
+
channels: this.allChannels()
|
|
1093
1122
|
});
|
|
1094
1123
|
this.heightmapKey = key;
|
|
1095
1124
|
}
|
|
@@ -1124,7 +1153,8 @@ var Terrain3D = class extends Node3D {
|
|
|
1124
1153
|
this.islandEdge,
|
|
1125
1154
|
this.layers,
|
|
1126
1155
|
this.textureBase,
|
|
1127
|
-
this.basins
|
|
1156
|
+
this.basins,
|
|
1157
|
+
this.allChannels()
|
|
1128
1158
|
]);
|
|
1129
1159
|
if (key === this.builtKey) return;
|
|
1130
1160
|
this.builtKey = key;
|
|
@@ -6430,6 +6460,22 @@ const PROFILE_SMOOTH = 3;
|
|
|
6430
6460
|
const SLOPE_GAIN = 4;
|
|
6431
6461
|
const SPEED_MIN = .05;
|
|
6432
6462
|
const SPEED_MAX = 12;
|
|
6463
|
+
/** Taps per side when reading the cross-section (rim height, wetted edge). */
|
|
6464
|
+
const SECTION_TAPS = 14;
|
|
6465
|
+
/** How far out a station looks for the bank that holds it, as a multiple of
|
|
6466
|
+
* its half-width and of its depth. Wider than the water on purpose: a river
|
|
6467
|
+
* running down the middle of a broad channel is held by walls it never
|
|
6468
|
+
* touches, and a scan that stopped at the waterline would call that a spill. */
|
|
6469
|
+
const SCAN_WIDTHS = 3;
|
|
6470
|
+
const SCAN_DEPTHS = 5;
|
|
6471
|
+
/** Thinnest water a reach keeps when the ground refuses to hold any (m). */
|
|
6472
|
+
const MIN_FILM = .04;
|
|
6473
|
+
/** Half-window (stations) the wetted widths are box-smoothed over. */
|
|
6474
|
+
const WIDTH_SMOOTH = 2;
|
|
6475
|
+
/** How far a station looks sideways for the bank that holds its water. */
|
|
6476
|
+
function scanReach(halfWidth, depth) {
|
|
6477
|
+
return Math.max(halfWidth * SCAN_WIDTHS, halfWidth + depth * SCAN_DEPTHS);
|
|
6478
|
+
}
|
|
6433
6479
|
/**
|
|
6434
6480
|
* Smooth the control path and walk it at even arc-length steps. The step is
|
|
6435
6481
|
* nudged so the last sample lands exactly on the mouth — an odd short segment
|
|
@@ -6549,26 +6595,102 @@ function buildRiverRings(opts) {
|
|
|
6549
6595
|
const s = smoothed[i];
|
|
6550
6596
|
profile[i] = i === 0 ? s : Math.min(s, profile[i - 1]);
|
|
6551
6597
|
}
|
|
6598
|
+
const fit = opts.fit === true;
|
|
6599
|
+
const levels = new Array(samples.length);
|
|
6600
|
+
const rims = new Array(samples.length);
|
|
6601
|
+
for (let i = 0; i < samples.length; i++) {
|
|
6602
|
+
const s = samples[i];
|
|
6603
|
+
const wanted = profile[i] + opts.depth;
|
|
6604
|
+
if (!fit) {
|
|
6605
|
+
rims[i] = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY];
|
|
6606
|
+
levels[i] = wanted;
|
|
6607
|
+
continue;
|
|
6608
|
+
}
|
|
6609
|
+
const reach = scanReach(halfWidths[i], opts.depth);
|
|
6610
|
+
const px = -s.tz;
|
|
6611
|
+
const pz = s.tx;
|
|
6612
|
+
let rimL = Number.NEGATIVE_INFINITY;
|
|
6613
|
+
let rimR = Number.NEGATIVE_INFINITY;
|
|
6614
|
+
for (let k = 1; k <= SECTION_TAPS; k++) {
|
|
6615
|
+
const d = k / SECTION_TAPS * reach;
|
|
6616
|
+
rimL = Math.max(rimL, opts.bedAt(s.x + px * d, s.z + pz * d));
|
|
6617
|
+
rimR = Math.max(rimR, opts.bedAt(s.x - px * d, s.z - pz * d));
|
|
6618
|
+
}
|
|
6619
|
+
rims[i] = [rimL, rimR];
|
|
6620
|
+
levels[i] = Math.min(wanted, rimL, rimR);
|
|
6621
|
+
}
|
|
6622
|
+
for (let i = 0; i < levels.length; i++) {
|
|
6623
|
+
let v = levels[i];
|
|
6624
|
+
if (fit) v = Math.max(v, bed[i] + MIN_FILM);
|
|
6625
|
+
if (i > 0) v = Math.min(v, levels[i - 1]);
|
|
6626
|
+
const rim = rims[i];
|
|
6627
|
+
v = Math.min(v, Math.max(Math.min(rim[0], rim[1]), bed[i] + MIN_FILM));
|
|
6628
|
+
levels[i] = v;
|
|
6629
|
+
}
|
|
6630
|
+
const wetted = samples.map((s, i) => {
|
|
6631
|
+
const half = halfWidths[i];
|
|
6632
|
+
if (!fit) return [half, half];
|
|
6633
|
+
const reach = scanReach(half, opts.depth);
|
|
6634
|
+
const level = levels[i];
|
|
6635
|
+
const px = -s.tz;
|
|
6636
|
+
const pz = s.tx;
|
|
6637
|
+
const edge = (side) => {
|
|
6638
|
+
for (let k = 1; k <= SECTION_TAPS; k++) {
|
|
6639
|
+
const d = k / SECTION_TAPS * reach;
|
|
6640
|
+
if (opts.bedAt(s.x + px * d * side, s.z + pz * d * side) >= level) return d;
|
|
6641
|
+
}
|
|
6642
|
+
return -half;
|
|
6643
|
+
};
|
|
6644
|
+
return [edge(1), edge(-1)];
|
|
6645
|
+
});
|
|
6646
|
+
const smoothWetted = wetted.map((_, i) => {
|
|
6647
|
+
let l = 0;
|
|
6648
|
+
let r = 0;
|
|
6649
|
+
let n = 0;
|
|
6650
|
+
for (let k = -2; k <= WIDTH_SMOOTH; k++) {
|
|
6651
|
+
const w = wetted[Math.min(Math.max(i + k, 0), wetted.length - 1)];
|
|
6652
|
+
l += Math.abs(w[0]);
|
|
6653
|
+
r += Math.abs(w[1]);
|
|
6654
|
+
n++;
|
|
6655
|
+
}
|
|
6656
|
+
const raw = wetted[i];
|
|
6657
|
+
return [Math.min(l / n, Math.abs(raw[0])), Math.min(r / n, Math.abs(raw[1]))];
|
|
6658
|
+
});
|
|
6552
6659
|
const meanHalf = halfWidths.reduce((a, b) => a + b, 0) / halfWidths.length || 1;
|
|
6553
6660
|
return samples.map((s, i) => {
|
|
6554
|
-
const surfaceY =
|
|
6661
|
+
const surfaceY = levels[i];
|
|
6555
6662
|
const prev = Math.max(0, i - 1);
|
|
6556
6663
|
const next = Math.min(samples.length - 1, i + 1);
|
|
6557
6664
|
const run = samples[next].along - samples[prev].along;
|
|
6558
|
-
const drop = (
|
|
6665
|
+
const drop = (levels[prev] - levels[next]) / (run || 1);
|
|
6559
6666
|
const slope = Math.max(0, drop);
|
|
6560
6667
|
const halfWidth = halfWidths[i];
|
|
6668
|
+
const wet = smoothWetted[i];
|
|
6561
6669
|
const speed = Math.min(Math.max(opts.flowSpeed * Math.sqrt(meanHalf / halfWidth) * (1 + SLOPE_GAIN * slope), SPEED_MIN), SPEED_MAX);
|
|
6670
|
+
const rawWet = wetted[i];
|
|
6562
6671
|
return {
|
|
6563
6672
|
...s,
|
|
6564
6673
|
halfWidth,
|
|
6674
|
+
halfLeft: Math.max(wet[0], .05),
|
|
6675
|
+
halfRight: Math.max(wet[1], .05),
|
|
6676
|
+
spillLeft: rawWet[0] < 0,
|
|
6677
|
+
spillRight: rawWet[1] < 0,
|
|
6565
6678
|
surfaceY,
|
|
6566
6679
|
slope,
|
|
6567
6680
|
speed
|
|
6568
6681
|
};
|
|
6569
6682
|
});
|
|
6570
6683
|
}
|
|
6571
|
-
/**
|
|
6684
|
+
/**
|
|
6685
|
+
* Ribbon mesh for the stations: level across the channel, draped along it, and
|
|
6686
|
+
* attribute-rich.
|
|
6687
|
+
*
|
|
6688
|
+
* Level across ONLY while the ground holds it. Where a bank has fallen away —
|
|
6689
|
+
* the outside of a bend, a reach traversing a hillside, the lip above a fall —
|
|
6690
|
+
* a level ribbon would hang in the air, so those vertices drop to follow the
|
|
6691
|
+
* ground down instead. Water pouring over a lip is a sheet on the rock; it is
|
|
6692
|
+
* never a slab of blue floating over the grass.
|
|
6693
|
+
*/
|
|
6572
6694
|
function buildRiverGeometry(rings, opts) {
|
|
6573
6695
|
const across = Math.max(1, Math.round(opts.acrossSegments));
|
|
6574
6696
|
const perRing = across + 1;
|
|
@@ -6582,6 +6704,7 @@ function buildRiverGeometry(rings, opts) {
|
|
|
6582
6704
|
const indices = new Uint32Array(quads * 6);
|
|
6583
6705
|
for (let r = 0; r < rings.length; r++) {
|
|
6584
6706
|
const ring = rings[r];
|
|
6707
|
+
const columnCap = Math.max(ring.surfaceY - sampleBed(opts, ring.x, ring.z), MIN_FILM);
|
|
6585
6708
|
const px = -ring.tz;
|
|
6586
6709
|
const pz = ring.tx;
|
|
6587
6710
|
const nLen = Math.hypot(ring.slope, 1);
|
|
@@ -6590,12 +6713,15 @@ function buildRiverGeometry(rings, opts) {
|
|
|
6590
6713
|
const nz = ring.tz * ring.slope / nLen;
|
|
6591
6714
|
for (let c = 0; c <= across; c++) {
|
|
6592
6715
|
const t = c / across * 2 - 1;
|
|
6593
|
-
const offset = t * ring.
|
|
6716
|
+
const offset = t * (t < 0 ? ring.halfRight : ring.halfLeft) * opts.bankOvershoot;
|
|
6594
6717
|
const x = ring.x + px * offset;
|
|
6595
6718
|
const z = ring.z + pz * offset;
|
|
6596
6719
|
const i = r * perRing + c;
|
|
6720
|
+
const bed = sampleBed(opts, x, z);
|
|
6721
|
+
const cap = (t < 0 ? ring.spillRight : ring.spillLeft) ? columnCap + (MIN_FILM - columnCap) * Math.abs(t) : columnCap;
|
|
6722
|
+
const y = Math.min(ring.surfaceY, bed + cap);
|
|
6597
6723
|
positions[i * 3] = x;
|
|
6598
|
-
positions[i * 3 + 1] =
|
|
6724
|
+
positions[i * 3 + 1] = y;
|
|
6599
6725
|
positions[i * 3 + 2] = z;
|
|
6600
6726
|
normals[i * 3] = nx;
|
|
6601
6727
|
normals[i * 3 + 1] = ny;
|
|
@@ -6605,7 +6731,7 @@ function buildRiverGeometry(rings, opts) {
|
|
|
6605
6731
|
flow[i * 2] = ring.tx;
|
|
6606
6732
|
flow[i * 2 + 1] = ring.tz;
|
|
6607
6733
|
river[i * 4] = ring.speed;
|
|
6608
|
-
river[i * 4 + 1] = Math.max(0,
|
|
6734
|
+
river[i * 4 + 1] = Math.max(0, y - bed);
|
|
6609
6735
|
river[i * 4 + 2] = ring.slope;
|
|
6610
6736
|
river[i * 4 + 3] = t * opts.bankOvershoot;
|
|
6611
6737
|
}
|
|
@@ -6689,8 +6815,10 @@ function hitFrom(a, b, t, x, z, capped) {
|
|
|
6689
6815
|
const tl = Math.hypot(tx, tz) || 1;
|
|
6690
6816
|
tx /= tl;
|
|
6691
6817
|
tz /= tl;
|
|
6692
|
-
const
|
|
6693
|
-
const
|
|
6818
|
+
const signed = (x - cx) * -tz + (z - cz) * tx;
|
|
6819
|
+
const lerp = (u, v) => u + (v - u) * t;
|
|
6820
|
+
const halfWidth = signed < 0 ? lerp(a.halfRight, b.halfRight) : lerp(a.halfLeft, b.halfLeft);
|
|
6821
|
+
const across = signed / (halfWidth || 1);
|
|
6694
6822
|
return {
|
|
6695
6823
|
inside: !capped && Math.abs(across) <= 1,
|
|
6696
6824
|
across,
|
|
@@ -6726,7 +6854,7 @@ function detectFalls(rings, opts = {}) {
|
|
|
6726
6854
|
topY: lip.surfaceY,
|
|
6727
6855
|
baseY: base.surfaceY,
|
|
6728
6856
|
drop,
|
|
6729
|
-
halfWidth: base.
|
|
6857
|
+
halfWidth: (base.halfLeft + base.halfRight) / 2,
|
|
6730
6858
|
dirX: base.tx,
|
|
6731
6859
|
dirZ: base.tz,
|
|
6732
6860
|
speed: base.speed
|
|
@@ -6930,6 +7058,15 @@ uniform float uRippleStrength;
|
|
|
6930
7058
|
uniform float uFlowRate;
|
|
6931
7059
|
uniform float uSparkle;
|
|
6932
7060
|
uniform float uFresnelScale;
|
|
7061
|
+
/**
|
|
7062
|
+
* Rocks standing in the current: xz = centre, z = radius, w = how much of the
|
|
7063
|
+
* water they break (0 = drowned and quiet, 1 = the crest stands proud).
|
|
7064
|
+
* The water does not flow AROUND them in any simulated sense — it is told
|
|
7065
|
+
* where they are and paints what a current does when it meets one: a bow
|
|
7066
|
+
* cushion upstream, a broken wake downstream, and a bent surface between.
|
|
7067
|
+
*/
|
|
7068
|
+
uniform int uRockCount;
|
|
7069
|
+
uniform vec4 uRocks[8];
|
|
6933
7070
|
|
|
6934
7071
|
varying vec3 vWorldPosition;
|
|
6935
7072
|
varying vec3 vSurfaceNormal;
|
|
@@ -6970,6 +7107,53 @@ void main() {
|
|
|
6970
7107
|
float across = vRiver.w;
|
|
6971
7108
|
|
|
6972
7109
|
vec2 dir = normalize(vFlow + vec2(1e-5, 0.0));
|
|
7110
|
+
|
|
7111
|
+
// ── rocks in the current ────────────────────────────────────────────────
|
|
7112
|
+
// Three things happen where a stream meets a boulder, and all three are
|
|
7113
|
+
// read off the same geometry: the flow BENDS around it (a doublet, the
|
|
7114
|
+
// textbook potential flow past a cylinder), it piles into a bow cushion on
|
|
7115
|
+
// the upstream face, and it tears into a broken wake behind. Everything is
|
|
7116
|
+
// in flow space, so a rock in a bend behaves like a rock in a straight.
|
|
7117
|
+
vec2 bend = vec2(0.0);
|
|
7118
|
+
float bow = 0.0;
|
|
7119
|
+
float wake = 0.0;
|
|
7120
|
+
float shed = 0.0;
|
|
7121
|
+
for (int i = 0; i < 8; i++) {
|
|
7122
|
+
if (i >= uRockCount) break;
|
|
7123
|
+
vec4 rock = uRocks[i];
|
|
7124
|
+
float R = max(rock.z, 0.02);
|
|
7125
|
+
vec2 rel = vWorldPosition.xz - rock.xy;
|
|
7126
|
+
float d = length(rel);
|
|
7127
|
+
if (d > R * 6.0) continue;
|
|
7128
|
+
float breaks = rock.w;
|
|
7129
|
+
// flow-space: +s downstream of the rock, t across it
|
|
7130
|
+
float s = dot(rel, dir);
|
|
7131
|
+
float t = dot(rel, vec2(-dir.y, dir.x));
|
|
7132
|
+
// the doublet: strength falls as 1/r², and inside the rock it is capped
|
|
7133
|
+
// so the field never explodes at the centre
|
|
7134
|
+
float rr = max(d, R);
|
|
7135
|
+
// the BEND is not whitewater: even a drowned stone turns the sheen
|
|
7136
|
+
// around itself, so it never fades out with how proud the rock stands
|
|
7137
|
+
bend += (rel / rr) * (R * R / (rr * rr)) * max(breaks, 0.55);
|
|
7138
|
+
// BOW: a cushion of piled water on the upstream face, brightest dead
|
|
7139
|
+
// ahead of the stone and gone by its shoulders
|
|
7140
|
+
float upstream = smoothstep(0.0, -R * 1.1, s);
|
|
7141
|
+
bow += upstream * (1.0 - smoothstep(R * 0.9, R * 2.0, d)) *
|
|
7142
|
+
(1.0 - smoothstep(R * 0.7, R * 1.6, abs(t))) * breaks;
|
|
7143
|
+
// WAKE: a V opening downstream, fading over a few radii
|
|
7144
|
+
float len = R * 5.0;
|
|
7145
|
+
float along = clamp(s / len, 0.0, 1.0);
|
|
7146
|
+
float halfWake = R * (0.5 + 1.3 * along);
|
|
7147
|
+
wake += step(0.0, s) * (1.0 - along) * (1.0 - along) *
|
|
7148
|
+
(1.0 - smoothstep(halfWake * 0.45, halfWake, abs(t))) * breaks;
|
|
7149
|
+
// the SHOULDERS shed water fastest — that is where the white tears off
|
|
7150
|
+
shed += (1.0 - smoothstep(R * 0.8, R * 1.7, d)) *
|
|
7151
|
+
smoothstep(R * 0.35, R * 0.95, abs(t)) * step(-R * 0.6, s) * breaks;
|
|
7152
|
+
}
|
|
7153
|
+
// a bent surface, not a bent texture: rotating the frame the ripple normal
|
|
7154
|
+
// is built in is what makes the sheen sweep around a stone
|
|
7155
|
+
dir = normalize(dir + clamp(bend, vec2(-1.2), vec2(1.2)));
|
|
7156
|
+
|
|
6973
7157
|
vec3 dir3 = vec3(dir.x, 0.0, dir.y);
|
|
6974
7158
|
vec3 perp3 = vec3(-dir.y, 0.0, dir.x);
|
|
6975
7159
|
|
|
@@ -7036,6 +7220,17 @@ void main() {
|
|
|
7036
7220
|
float foam = smoothstep(0.74 - 0.45 * drive, 1.02 - 0.24 * drive, streak) * min(drive, 1.0);
|
|
7037
7221
|
// crest froth: the tops of standing waves in a real rapid always carry white
|
|
7038
7222
|
foam = max(foam, smoothstep(0.68, 0.96, ripple.x) * smoothstep(0.4, 1.0, rapids) * uFoam * 0.5);
|
|
7223
|
+
|
|
7224
|
+
// rock whitewater: the cushion is a smooth pile (white but soft-edged), the
|
|
7225
|
+
// wake and the shoulders are TORN, so they ride the advected noise and boil
|
|
7226
|
+
// instead of sitting there as a painted decal
|
|
7227
|
+
float boil = 0.5 + 0.5 * sin(uTime * 3.1 + vFlowUv.x * 2.7);
|
|
7228
|
+
float rockFoam = clamp(bow * 1.6, 0.0, 1.0) * (0.62 + 0.38 * boil);
|
|
7229
|
+
rockFoam = max(rockFoam, clamp(wake * 1.5, 0.0, 1.0) * smoothstep(0.24, 0.72, streak) * 1.3);
|
|
7230
|
+
rockFoam = max(rockFoam, clamp(shed * 1.5, 0.0, 1.0) * smoothstep(0.32, 0.8, streak));
|
|
7231
|
+
// a boulder in a millpond barely whitens; the same boulder in a chute tears
|
|
7232
|
+
foam = max(foam, clamp(rockFoam, 0.0, 1.0) * smoothstep(0.15, 0.9, speed) * uFoam);
|
|
7233
|
+
|
|
7039
7234
|
// froth can never be more present than the water carrying it
|
|
7040
7235
|
foam = clamp(foam, 0.0, 1.0) * smoothstep(0.02, 0.14, column);
|
|
7041
7236
|
|
|
@@ -7132,6 +7327,38 @@ const METERS_PER_ACROSS_SEGMENT = 1.5;
|
|
|
7132
7327
|
* flow over it, wide enough that terrain grain does not fizz the whole river. */
|
|
7133
7328
|
const COLUMN_BLUR_PER_HALF_WIDTH = .35;
|
|
7134
7329
|
const COLUMN_BLUR_MAX = 1.2;
|
|
7330
|
+
/**
|
|
7331
|
+
* Self-carved bed. The cut is sized off the WATER COLUMN, not off the width:
|
|
7332
|
+
* a channel's banks are as steep as its banks are, and a taper scaled to width
|
|
7333
|
+
* turns a wide river into a shallow saucer the water floods far past its edge.
|
|
7334
|
+
*
|
|
7335
|
+
* `depth` is the trench in water columns; `bank` the smoothstep blend beyond
|
|
7336
|
+
* the flat bed, also in columns. The waterline crosses that blend at
|
|
7337
|
+
* {@link CARVE_WATERLINE_T} of its width (smoothstep(t) = 1 − 1/depth), which
|
|
7338
|
+
* is what lets the floor be narrowed so the WATER ends up the authored width.
|
|
7339
|
+
*/
|
|
7340
|
+
const CARVE_DEPTH = 2.2;
|
|
7341
|
+
const CARVE_BANK = 2;
|
|
7342
|
+
const CARVE_WATERLINE_T = .52;
|
|
7343
|
+
/** Never cut the floor away entirely, however steep the bank works out. */
|
|
7344
|
+
const CARVE_FLOOR_MIN = .3;
|
|
7345
|
+
/** A trench thinner than this many terrain cells cannot survive the grid it is
|
|
7346
|
+
* sampled on — it smooths away to nothing and the creek is back on raw ground.
|
|
7347
|
+
* Widening it is the honest failure: better a bed too wide than no bed. */
|
|
7348
|
+
const CARVE_MIN_CELLS = 1.5;
|
|
7349
|
+
/** Deepest the bed may be cut, in water columns — the budget for digging
|
|
7350
|
+
* THROUGH a rise so the water keeps flowing, before it would read as a canyon. */
|
|
7351
|
+
const CARVE_MAX_DEPTH = 7;
|
|
7352
|
+
/** The fall the carved floor keeps even where the ground does not (m per m). */
|
|
7353
|
+
const CARVE_MIN_GRADE = .004;
|
|
7354
|
+
/** Rocks the shader can carry at once (the nearest ones win). */
|
|
7355
|
+
const ROCK_SLOTS = 8;
|
|
7356
|
+
/** How often the tree is re-scanned for things standing in the water (s). */
|
|
7357
|
+
const ROCK_RESCAN = .5;
|
|
7358
|
+
/** A rock stops mattering past this many of its own radii from the camera —
|
|
7359
|
+
* never sooner than ROCK_FADE_MIN, or a pebble would blink out at arm's reach. */
|
|
7360
|
+
const ROCK_FADE_RADII = 42;
|
|
7361
|
+
const ROCK_FADE_MIN = 26;
|
|
7135
7362
|
/** How hard the current drags a body toward the water's own velocity (1/s at
|
|
7136
7363
|
* `flowForce: 1`). Drag, not thrust: nothing ever outruns the river. Tuned
|
|
7137
7364
|
* against CharacterController3D, whose stand-still damping fights it — at this
|
|
@@ -7150,6 +7377,8 @@ const DEFAULT_COLORS = {
|
|
|
7150
7377
|
bank: "#465447"
|
|
7151
7378
|
};
|
|
7152
7379
|
const FOAM_COLOR = "#e9f2ef";
|
|
7380
|
+
/** Per-frame scratch (no allocation in the render path). */
|
|
7381
|
+
const rockCamScratch = new Vector3();
|
|
7153
7382
|
/** Fixed look constants — the props stay intent-level, these carry the craft. */
|
|
7154
7383
|
const LOOK = {
|
|
7155
7384
|
/** Pattern units per meter of flow space. */
|
|
@@ -7205,6 +7434,7 @@ var River3D = class River3D extends Node3D {
|
|
|
7205
7434
|
sunColor: { default: "#fff6e0" },
|
|
7206
7435
|
sunIntensity: { default: 1 },
|
|
7207
7436
|
terrain: { default: "" },
|
|
7437
|
+
carve: { default: true },
|
|
7208
7438
|
flowForce: { default: 1 },
|
|
7209
7439
|
spray: { default: 1 }
|
|
7210
7440
|
};
|
|
@@ -7240,6 +7470,18 @@ var River3D = class River3D extends Node3D {
|
|
|
7240
7470
|
sunIntensity = 1;
|
|
7241
7471
|
/** Drape target path; empty = auto-find the first Terrain3D in the tree. */
|
|
7242
7472
|
terrain = "";
|
|
7473
|
+
/**
|
|
7474
|
+
* Cut the bed. Real water arrives on a hillside and erodes itself a channel;
|
|
7475
|
+
* a ribbon laid on raw ground has nothing holding it and reads as a strip of
|
|
7476
|
+
* blue hovering over the grass. With this on, the river trenches its own
|
|
7477
|
+
* course into the terrain it drapes on — banks the player can walk into, a
|
|
7478
|
+
* collider and `heightAt` that agree with it, and splat that paints the cut.
|
|
7479
|
+
*
|
|
7480
|
+
* Off means you carve it yourself with `Terrain3D.channels` (or the reach is
|
|
7481
|
+
* genuinely a flat flood plain). Either way the surface is FITTED to
|
|
7482
|
+
* whatever ground it finds: it can never stand above its own banks.
|
|
7483
|
+
*/
|
|
7484
|
+
carve = true;
|
|
7243
7485
|
/** How hard the current sweeps bodies downstream (0 = visual only). */
|
|
7244
7486
|
flowForce = 1;
|
|
7245
7487
|
/**
|
|
@@ -7255,6 +7497,11 @@ var River3D = class River3D extends Node3D {
|
|
|
7255
7497
|
sprayKey = "";
|
|
7256
7498
|
courseKey = "";
|
|
7257
7499
|
bedAt = null;
|
|
7500
|
+
/** Whether the last course was fitted to real ground (see buildRiverRings). */
|
|
7501
|
+
fitted = false;
|
|
7502
|
+
/** Everything standing in the current, rescanned on a slow timer. */
|
|
7503
|
+
rocks = [];
|
|
7504
|
+
rockScanAt = -1;
|
|
7258
7505
|
world = [
|
|
7259
7506
|
0,
|
|
7260
7507
|
0,
|
|
@@ -7295,6 +7542,12 @@ var River3D = class River3D extends Node3D {
|
|
|
7295
7542
|
_createObject3D() {
|
|
7296
7543
|
return new Mesh(new BufferGeometry(), this.buildMaterial());
|
|
7297
7544
|
}
|
|
7545
|
+
onReady() {
|
|
7546
|
+
this.ensureCourse();
|
|
7547
|
+
}
|
|
7548
|
+
onExitTree() {
|
|
7549
|
+
findDrapeTerrain(this, this.terrain)?._setExternalChannel(this, null);
|
|
7550
|
+
}
|
|
7298
7551
|
update(dt) {
|
|
7299
7552
|
this.time += dt;
|
|
7300
7553
|
this.ensureCourse();
|
|
@@ -7307,6 +7560,16 @@ var River3D = class River3D extends Node3D {
|
|
|
7307
7560
|
this.rebuildIfNeeded(mesh);
|
|
7308
7561
|
this.syncUniforms(mesh);
|
|
7309
7562
|
}
|
|
7563
|
+
/** Rocks are picked per FRAME (nearest to the eye), scanned on a timer. */
|
|
7564
|
+
_onRender3D(ctx) {
|
|
7565
|
+
if (this.rings.length === 0) return;
|
|
7566
|
+
if (this.rockScanAt < 0 || this.time - this.rockScanAt > ROCK_RESCAN) {
|
|
7567
|
+
this.rockScanAt = this.time;
|
|
7568
|
+
this.scanRocks();
|
|
7569
|
+
}
|
|
7570
|
+
const u = this._ensureObject3D().material.uniforms;
|
|
7571
|
+
this.syncRocks(u, ctx.camera.getWorldPosition(rockCamScratch));
|
|
7572
|
+
}
|
|
7310
7573
|
/**
|
|
7311
7574
|
* Where a WORLD point sits in the channel — the query gameplay scripts run
|
|
7312
7575
|
* (steer a raft, drown a torch, decide a ford is too fast to cross). `null`
|
|
@@ -7359,6 +7622,63 @@ var River3D = class River3D extends Node3D {
|
|
|
7359
7622
|
widthProfile() {
|
|
7360
7623
|
return this.widths.length > 0 ? this.widths : [this.width];
|
|
7361
7624
|
}
|
|
7625
|
+
/**
|
|
7626
|
+
* Cut (or release) this river's trench in the terrain it drapes on.
|
|
7627
|
+
*
|
|
7628
|
+
* Runs at ready as well as on every course rebuild: the heightfield collider
|
|
7629
|
+
* is built before the first render sync, so a bed cut only at draw time
|
|
7630
|
+
* would leave the player walking on ground that is no longer there.
|
|
7631
|
+
*/
|
|
7632
|
+
applyCarve(terrain) {
|
|
7633
|
+
if (!terrain) return;
|
|
7634
|
+
if (!this.carve || this.path.length < 2) {
|
|
7635
|
+
terrain._setExternalChannel(this, null);
|
|
7636
|
+
return;
|
|
7637
|
+
}
|
|
7638
|
+
terrain._setExternalChannel(this, null);
|
|
7639
|
+
const [wx, , wz] = worldXZY(this);
|
|
7640
|
+
const [tx, , tz] = worldXZY(terrain);
|
|
7641
|
+
const widths = this.widthProfile();
|
|
7642
|
+
const hm = terrain._heightmap();
|
|
7643
|
+
const cell = (terrain.size[0] ?? 1) / Math.max(1, hm.segsX);
|
|
7644
|
+
const line = resamplePath(this.path.map((p) => [(p[0] ?? 0) + wx - tx, (p[1] ?? 0) + wz - tz]), Math.max(cell, this.depth));
|
|
7645
|
+
if (line.length < 2) {
|
|
7646
|
+
terrain._setExternalChannel(this, null);
|
|
7647
|
+
return;
|
|
7648
|
+
}
|
|
7649
|
+
const total = line[line.length - 1].along || 1;
|
|
7650
|
+
const ground = line.map((s) => terrain._heightmap().heightAt(s.x, s.z));
|
|
7651
|
+
const baseCut = this.depth * CARVE_DEPTH;
|
|
7652
|
+
const maxCut = this.depth * CARVE_MAX_DEPTH;
|
|
7653
|
+
const floor = new Array(line.length);
|
|
7654
|
+
for (let i = 0; i < line.length; i++) {
|
|
7655
|
+
const wanted = ground[i] - baseCut;
|
|
7656
|
+
if (i === 0) {
|
|
7657
|
+
floor[i] = wanted;
|
|
7658
|
+
continue;
|
|
7659
|
+
}
|
|
7660
|
+
const run = line[i].along - line[i - 1].along;
|
|
7661
|
+
const carried = floor[i - 1] - run * CARVE_MIN_GRADE;
|
|
7662
|
+
floor[i] = Math.max(Math.min(wanted, carried), ground[i] - maxCut);
|
|
7663
|
+
}
|
|
7664
|
+
const specs = [];
|
|
7665
|
+
for (let i = 1; i < line.length; i++) {
|
|
7666
|
+
const a = line[i - 1];
|
|
7667
|
+
const b = line[i];
|
|
7668
|
+
const cut = Math.max((ground[i - 1] + ground[i]) / 2 - (floor[i - 1] + floor[i]) / 2, 0);
|
|
7669
|
+
if (cut <= .001) continue;
|
|
7670
|
+
const half = widthAlong(widths, a.along / total) / 2;
|
|
7671
|
+
const bank = Math.max(this.depth * CARVE_BANK, cell * CARVE_MIN_CELLS);
|
|
7672
|
+
const floorHalf = Math.max(half - bank * CARVE_WATERLINE_T, half * CARVE_FLOOR_MIN, cell * .5);
|
|
7673
|
+
specs.push({
|
|
7674
|
+
path: [[a.x, a.z], [b.x, b.z]],
|
|
7675
|
+
width: floorHalf * 2,
|
|
7676
|
+
depth: cut,
|
|
7677
|
+
taper: bank
|
|
7678
|
+
});
|
|
7679
|
+
}
|
|
7680
|
+
terrain._setExternalChannel(this, specs);
|
|
7681
|
+
}
|
|
7362
7682
|
/** Resolve the drape terrain; an explicit path that misses is a hard error. */
|
|
7363
7683
|
resolveTerrain() {
|
|
7364
7684
|
const terrain = findDrapeTerrain(this, this.terrain);
|
|
@@ -7378,6 +7698,7 @@ var River3D = class River3D extends Node3D {
|
|
|
7378
7698
|
this.widths,
|
|
7379
7699
|
this.depth,
|
|
7380
7700
|
this.flowSpeed,
|
|
7701
|
+
this.carve,
|
|
7381
7702
|
terrain ? terrain.name : "",
|
|
7382
7703
|
world
|
|
7383
7704
|
]);
|
|
@@ -7385,6 +7706,7 @@ var River3D = class River3D extends Node3D {
|
|
|
7385
7706
|
this.courseKey = key;
|
|
7386
7707
|
this.world = world;
|
|
7387
7708
|
this.bedAt = null;
|
|
7709
|
+
this.applyCarve(terrain);
|
|
7388
7710
|
if (this.path.length < 2) {
|
|
7389
7711
|
this.rings = [];
|
|
7390
7712
|
this.falls = [];
|
|
@@ -7392,6 +7714,7 @@ var River3D = class River3D extends Node3D {
|
|
|
7392
7714
|
}
|
|
7393
7715
|
const [wx, wy, wz] = world;
|
|
7394
7716
|
this.bedAt = terrain ? (lx, lz) => terrain.heightAt(lx + wx, lz + wz) - wy : () => -this.depth;
|
|
7717
|
+
this.fitted = terrain !== null;
|
|
7395
7718
|
const widths = this.widthProfile();
|
|
7396
7719
|
const narrow = Math.min(...widths);
|
|
7397
7720
|
this.rings = buildRiverRings({
|
|
@@ -7400,7 +7723,8 @@ var River3D = class River3D extends Node3D {
|
|
|
7400
7723
|
depth: this.depth,
|
|
7401
7724
|
flowSpeed: this.flowSpeed,
|
|
7402
7725
|
step: Math.min(Math.max(narrow * STEP_PER_WIDTH, STEP_MIN), STEP_MAX),
|
|
7403
|
-
bedAt: this.bedAt
|
|
7726
|
+
bedAt: this.bedAt,
|
|
7727
|
+
fit: this.fitted
|
|
7404
7728
|
});
|
|
7405
7729
|
this.falls = detectFalls(this.rings);
|
|
7406
7730
|
return true;
|
|
@@ -7419,7 +7743,7 @@ var River3D = class River3D extends Node3D {
|
|
|
7419
7743
|
const wide = Math.max(...widths);
|
|
7420
7744
|
const data = buildRiverGeometry(this.rings, {
|
|
7421
7745
|
acrossSegments: Math.min(ACROSS_MAX, Math.max(ACROSS_MIN, Math.round(wide / METERS_PER_ACROSS_SEGMENT))),
|
|
7422
|
-
bankOvershoot: BANK_OVERSHOOT,
|
|
7746
|
+
bankOvershoot: this.fitted ? 1 : BANK_OVERSHOOT,
|
|
7423
7747
|
bedBlur: Math.min(narrow / 2 * COLUMN_BLUR_PER_HALF_WIDTH, COLUMN_BLUR_MAX),
|
|
7424
7748
|
bedAt: this.bedAt
|
|
7425
7749
|
});
|
|
@@ -7462,6 +7786,8 @@ var River3D = class River3D extends Node3D {
|
|
|
7462
7786
|
uRippleStrength: { value: LOOK.rippleStrength * this.ripples },
|
|
7463
7787
|
uFlowRate: { value: LOOK.flowRate },
|
|
7464
7788
|
uSparkle: { value: this.ripples },
|
|
7789
|
+
uRockCount: { value: 0 },
|
|
7790
|
+
uRocks: { value: Array.from({ length: ROCK_SLOTS }, () => new Vector4()) },
|
|
7465
7791
|
uFresnelScale: { value: LOOK.fresnelScale },
|
|
7466
7792
|
uStandingAmp: { value: LOOK.standingAmp },
|
|
7467
7793
|
uGlideAmp: { value: LOOK.glideAmp }
|
|
@@ -7472,6 +7798,88 @@ var River3D = class River3D extends Node3D {
|
|
|
7472
7798
|
side: DoubleSide
|
|
7473
7799
|
});
|
|
7474
7800
|
}
|
|
7801
|
+
/**
|
|
7802
|
+
* Find everything standing in the current.
|
|
7803
|
+
*
|
|
7804
|
+
* A boulder in a creek is not a physics problem — it is a thing the water
|
|
7805
|
+
* has to be TOLD about, or the stream runs through it as if it were painted
|
|
7806
|
+
* on. Any mesh whose footprint lands inside the wetted channel and whose top
|
|
7807
|
+
* reaches near the surface counts, `InstancedMesh3D` rows included (a rock
|
|
7808
|
+
* scatter is one node with fifty stones in it).
|
|
7809
|
+
*
|
|
7810
|
+
* Scanned on a slow timer rather than per frame: rocks do not move often,
|
|
7811
|
+
* and a river with a hundred of them still writes only the nearest few.
|
|
7812
|
+
*/
|
|
7813
|
+
scanRocks() {
|
|
7814
|
+
const found = [];
|
|
7815
|
+
const [wx, wy, wz] = this.world;
|
|
7816
|
+
/** Keep a candidate if it actually stands IN the water column. */
|
|
7817
|
+
const consider = (x, z, radius, bottom, top) => {
|
|
7818
|
+
if (radius <= .02) return;
|
|
7819
|
+
const hit = projectToRiver(this.rings, x - wx, z - wz);
|
|
7820
|
+
if (!hit || !hit.inside) return;
|
|
7821
|
+
const surface = hit.surfaceY + wy;
|
|
7822
|
+
if (bottom > surface) return;
|
|
7823
|
+
const bed = surface - this.depth;
|
|
7824
|
+
if (top < bed) return;
|
|
7825
|
+
const breaks = Math.min(Math.max((top - bed) / Math.max(this.depth, .05), .2), 1);
|
|
7826
|
+
found.push({
|
|
7827
|
+
x,
|
|
7828
|
+
z,
|
|
7829
|
+
radius,
|
|
7830
|
+
breaks
|
|
7831
|
+
});
|
|
7832
|
+
};
|
|
7833
|
+
let root = this;
|
|
7834
|
+
while (root.parent) root = root.parent;
|
|
7835
|
+
const walk = (node) => {
|
|
7836
|
+
if (node instanceof InstancedMesh3D) {
|
|
7837
|
+
const [sx, sy, sz] = node.size;
|
|
7838
|
+
const [ox, oy, oz] = worldXZY(node);
|
|
7839
|
+
for (const row of node.transforms) {
|
|
7840
|
+
const scale = row[4] ?? 1;
|
|
7841
|
+
const x = ox + (row[0] ?? 0);
|
|
7842
|
+
const y = oy + (row[1] ?? 0);
|
|
7843
|
+
const z = oz + (row[2] ?? 0);
|
|
7844
|
+
const half = (sy ?? 1) * scale / 2;
|
|
7845
|
+
consider(x, z, Math.max(sx, sz) * scale / 2, y - half, y + half);
|
|
7846
|
+
}
|
|
7847
|
+
} else if (node instanceof MeshInstance3D) {
|
|
7848
|
+
const [sx, sy, sz] = node.size;
|
|
7849
|
+
const [x, y, z] = worldXZY(node);
|
|
7850
|
+
const half = (sy ?? 1) / 2;
|
|
7851
|
+
consider(x, z, Math.max(sx, sz) / 2, y - half, y + half);
|
|
7852
|
+
}
|
|
7853
|
+
for (const child of node.children) walk(child);
|
|
7854
|
+
};
|
|
7855
|
+
walk(root);
|
|
7856
|
+
this.rocks = found;
|
|
7857
|
+
}
|
|
7858
|
+
/** Write the nearest rocks into the shader, faded in by distance. */
|
|
7859
|
+
syncRocks(u, camera) {
|
|
7860
|
+
const slots = u.uRocks?.value;
|
|
7861
|
+
const count = u.uRockCount;
|
|
7862
|
+
if (!slots || !count) return;
|
|
7863
|
+
if (this.rocks.length === 0) {
|
|
7864
|
+
count.value = 0;
|
|
7865
|
+
return;
|
|
7866
|
+
}
|
|
7867
|
+
const near = [...this.rocks].map((r) => ({
|
|
7868
|
+
r,
|
|
7869
|
+
d: (r.x - camera.x) ** 2 + (r.z - camera.z) ** 2
|
|
7870
|
+
})).sort((a, b) => a.d - b.d).slice(0, ROCK_SLOTS);
|
|
7871
|
+
let n = 0;
|
|
7872
|
+
for (const { r, d } of near) {
|
|
7873
|
+
const far = Math.max(r.radius * ROCK_FADE_RADII, ROCK_FADE_MIN);
|
|
7874
|
+
const t = Math.min(Math.sqrt(d) / far, 1);
|
|
7875
|
+
const k = Math.min(Math.max((t - .6) / .4, 0), 1);
|
|
7876
|
+
const fade = 1 - k * k * (3 - 2 * k);
|
|
7877
|
+
if (fade <= .01) continue;
|
|
7878
|
+
slots[n]?.set(r.x, r.z, r.radius, r.breaks * fade);
|
|
7879
|
+
n++;
|
|
7880
|
+
}
|
|
7881
|
+
count.value = n;
|
|
7882
|
+
}
|
|
7475
7883
|
syncUniforms(mesh) {
|
|
7476
7884
|
const u = mesh.material.uniforms;
|
|
7477
7885
|
const set = (key, value) => {
|
|
@@ -10870,6 +11278,7 @@ function registerNodes3D() {
|
|
|
10870
11278
|
registerNode(River3D);
|
|
10871
11279
|
registerNode(Terrain3D);
|
|
10872
11280
|
registerNode(Water3D);
|
|
11281
|
+
registerNode(WaterCutout3D);
|
|
10873
11282
|
registerNode(Foliage3D);
|
|
10874
11283
|
registerNode(Flowers3D);
|
|
10875
11284
|
registerNode(Tree3D);
|