sindicate 0.1.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/README.md +20 -0
- package/docs/contracts/README.md +22 -0
- package/package.json +38 -0
- package/src/core/decalField.js +151 -0
- package/src/core/quality.js +109 -0
- package/src/core/utils.js +48 -0
- package/src/index.js +31 -0
- package/src/systems/shatter.js +94 -0
- package/src/vendor/FBXLoader.js +4586 -0
- package/src/world/bvhWorker.js +85 -0
- package/src/world/collision.js +174 -0
- package/src/world/grass.js +347 -0
- package/src/world/water.js +227 -0
- package/src/world/waterNoise.js +47 -0
- package/src/world/weatherUniforms.js +24 -0
- package/src/world/wind.js +40 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// Low-poly water with SEE-THROUGH depth: techniques ported from Braffolk's
|
|
2
|
+
// fable5-world-demo WaterMaterial (same three.js/TSL stack), restyled for our faceted
|
|
3
|
+
// Synty look. The surface refracts the actual scene behind it (viewportSharedTexture)
|
|
4
|
+
// with per-channel Beer–Lambert absorption by water-column thickness, and reflects the
|
|
5
|
+
// sky-phase colours by fresnel — so riverbeds and wading legs show through the shallows,
|
|
6
|
+
// deep water goes murky, and the water tracks dawn/dusk/night/moor-dread for free.
|
|
7
|
+
// Kept from ours: the gentle faceted swell, per-vertex aDepth (foam + inside-gating),
|
|
8
|
+
// the pond's rClip sprawl guard, the dive-under-terrain shoreline geometry.
|
|
9
|
+
// Their hard-won lessons honoured: fresnel weights use a FLATTENED normal (a tilted
|
|
10
|
+
// per-facet normal saturates (1−cosθ)^5 into a white sheet); refraction uv is
|
|
11
|
+
// depth-VALIDATED so foreground geometry never smears into the water.
|
|
12
|
+
import * as THREE from 'three/webgpu';
|
|
13
|
+
import {
|
|
14
|
+
Fn, attribute, positionLocal, positionWorld, positionView, time, sin, vec2, vec3, float,
|
|
15
|
+
normalize, cross, dot, clamp, mix, max, smoothstep, dFdx, dFdy, uv, exp, reflect,
|
|
16
|
+
cameraPosition, cameraNear, cameraFar, screenUV, viewportSharedTexture, viewportDepthTexture,
|
|
17
|
+
perspectiveDepthToViewZ, fract, abs, texture,
|
|
18
|
+
} from 'three/tsl';
|
|
19
|
+
import { uSkyCol, uHorizonCol } from './weatherUniforms.js';
|
|
20
|
+
import { waterNoiseTexture } from './waterNoise.js';
|
|
21
|
+
|
|
22
|
+
const C = (hex) => { const c = new THREE.Color(hex); return vec3(c.r, c.g, c.b); };
|
|
23
|
+
|
|
24
|
+
// UK water absorption per metre (r dies first → cool green-blue depths, murky by ~2.5m)
|
|
25
|
+
const SIGMA = vec3(0.55, 0.24, 0.17);
|
|
26
|
+
|
|
27
|
+
// Scene-behind-the-water, refracted and absorbed. n = the faceted surface normal;
|
|
28
|
+
// murk = in-scatter density (rivers run denser — their plain beds give refraction
|
|
29
|
+
// little to show until the riverbed-texturing pass lands).
|
|
30
|
+
// Returns { col, vDepth }: the transmitted colour and the vertical water column (for foam).
|
|
31
|
+
const refraction = (n, murk = vec3(0.10, 0.16, 0.17)) => {
|
|
32
|
+
const toCam = cameraPosition.sub(positionWorld);
|
|
33
|
+
const dist = toCam.length();
|
|
34
|
+
const viewDirY = toCam.y.div(dist.max(1e-4));
|
|
35
|
+
const fragZ = positionView.z; // negative
|
|
36
|
+
// ripple/facet-driven refraction offset, shrinking with distance
|
|
37
|
+
const refrK = clamp(float(9).div(dist.max(1)), 0.04, 1).mul(0.05);
|
|
38
|
+
const ruv = screenUV.add(n.xz.mul(refrK));
|
|
39
|
+
const zR = perspectiveDepthToViewZ(viewportDepthTexture(ruv).x, cameraNear, cameraFar);
|
|
40
|
+
// refracted sample landing on geometry IN FRONT of the water (a wader, a bridge post)
|
|
41
|
+
// must fall back to the straight uv, or the foreground smears into the surface
|
|
42
|
+
const leaked = zR.greaterThan(fragZ.add(0.02));
|
|
43
|
+
const uvF = mix(ruv, screenUV, leaked.select(float(1), float(0)));
|
|
44
|
+
const zS = mix(zR, perspectiveDepthToViewZ(viewportDepthTexture(screenUV).x, cameraNear, cameraFar), leaked.select(float(1), float(0)));
|
|
45
|
+
const thick = fragZ.sub(zS).max(0); // metres of water along the view ray
|
|
46
|
+
const sceneCol = viewportSharedTexture(uvF).rgb;
|
|
47
|
+
const absorb = thick.mul(1.3);
|
|
48
|
+
const T = vec3(exp(absorb.mul(-0.55)), exp(absorb.mul(-0.24)), exp(absorb.mul(-0.17)));
|
|
49
|
+
// turbidity in-scatter follows the sky phase → murk brightens by day, dims at night
|
|
50
|
+
const inscat = uSkyCol.mul(murk);
|
|
51
|
+
const col = sceneCol.mul(T).add(inscat.mul(vec3(1, 1, 1).sub(T)));
|
|
52
|
+
const vDepth = thick.mul(viewDirY.abs().max(0.06)); // vertical column (shore feather/foam)
|
|
53
|
+
return { col, vDepth, viewDir: toCam.div(dist.max(1e-4)) };
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Sky-phase reflection along the reflected ray, reproducing the dome's horizon→sky ramp.
|
|
57
|
+
// Darkened ×0.78: real water reflects darker than the sky (micro-roughness), and a full-
|
|
58
|
+
// brightness midday sky whites out narrow streams seen at grazing angles.
|
|
59
|
+
const skyReflection = (viewDir, n) => {
|
|
60
|
+
const rdir = reflect(viewDir.negate(), n);
|
|
61
|
+
return mix(uHorizonCol, uSkyCol, smoothstep(0.0, 0.55, rdir.y.max(0.035))).mul(0.78);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Fresnel on a HARD-flattened normal (braffolk's lesson: reflectance must follow the
|
|
65
|
+
// MEAN surface or big low-poly facet tilts saturate (1−cosθ)^5 into an ice sheet) and
|
|
66
|
+
// capped so the refracted bed always survives underneath. The facets still sparkle —
|
|
67
|
+
// the FULL normal shapes the reflected sky colour + shimmer, just not the mix weight.
|
|
68
|
+
const fresnel = (viewDir, n) => {
|
|
69
|
+
const nF = normalize(vec3(n.x.mul(0.25), n.y, n.z.mul(0.25)));
|
|
70
|
+
const cosT = clamp(dot(viewDir, nF), 0.0, 1.0);
|
|
71
|
+
return float(0.02).add(cosT.oneMinus().pow(5).mul(0.98)).mul(0.5);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export function waterMaterial({
|
|
75
|
+
foam = 0xeef8f6, shallow = 0x3f96a8, deep = 0x114f78, maxDepth = 2.0,
|
|
76
|
+
foamDepth = 0.18, rClip = 21, waveHeight = 0.16, waveLen = 9, waveSpeed = 0.7,
|
|
77
|
+
refract = true, // false = the cheap baked-depth ramp (the SEA: too big to pay viewport copies for water you can't see into)
|
|
78
|
+
} = {}) {
|
|
79
|
+
const mat = new THREE.MeshBasicNodeMaterial({ transparent: true, depthWrite: true });
|
|
80
|
+
const FM = C(foam), SH = C(shallow), DP = C(deep);
|
|
81
|
+
|
|
82
|
+
// rClip may be a number (one body per material — the sea) or 'attribute': lakes bake their
|
|
83
|
+
// radius per-vertex (aRClip) so EVERY lake shares ONE material. Each distinct refracting
|
|
84
|
+
// material compiles its own viewportSharedTexture sampling, and each ViewportTexture node
|
|
85
|
+
// pays its own copyFramebufferToTexture per frame — N lakes on one material is 1 copy, not N.
|
|
86
|
+
const rc = rClip === 'attribute' ? attribute('aRClip', 'float') : float(rClip);
|
|
87
|
+
|
|
88
|
+
const waveAt = Fn(([p]) => {
|
|
89
|
+
const t = time.mul(waveSpeed), k = 6.2832 / waveLen;
|
|
90
|
+
return sin(p.x.mul(k).add(p.y.mul(k * 0.6)).add(t))
|
|
91
|
+
.add(sin(p.y.mul(k * 0.9).sub(p.x.mul(k * 0.3)).sub(t.mul(0.85))).mul(0.7))
|
|
92
|
+
.mul(waveHeight);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// foam wherever the water ends — shallow waterline (depth→0) OR the clip radius — both wavy.
|
|
96
|
+
const foamFull = Fn(([d, pondD]) => {
|
|
97
|
+
const t = time.mul(waveSpeed);
|
|
98
|
+
const wav = sin(positionWorld.x.mul(0.5).add(t)).add(sin(positionWorld.z.mul(0.6).sub(t.mul(0.7)))).mul(0.5);
|
|
99
|
+
const edge = float(foamDepth).add(wav.mul(foamDepth * 0.4));
|
|
100
|
+
const depthFoam = smoothstep(edge, edge.mul(0.15), d);
|
|
101
|
+
const radiusFoam = smoothstep(rc.sub(2.0), rc.sub(0.8), pondD.add(wav.mul(0.5)));
|
|
102
|
+
return max(depthFoam, radiusFoam);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
mat.positionNode = Fn(() => {
|
|
106
|
+
const p = positionLocal;
|
|
107
|
+
// ADD the wave to the baked local Z (shore verts are baked BELOW the terrain so the
|
|
108
|
+
// sheet dives under dry ground — the waterline is the natural surface intersection)
|
|
109
|
+
return vec3(p.x, p.y, p.z.add(waveAt(p.xy)));
|
|
110
|
+
})();
|
|
111
|
+
|
|
112
|
+
// Build the expensive nodes ONCE and reference them from both color and opacity —
|
|
113
|
+
// separate calls would duplicate the viewport-texture sampling in the compiled shader.
|
|
114
|
+
const nShared = normalize(cross(dFdx(positionWorld), dFdy(positionWorld))); // flat per-facet normal
|
|
115
|
+
const viewShared = cameraPosition.sub(positionWorld);
|
|
116
|
+
const viewDirShared = viewShared.div(viewShared.length().max(1e-4));
|
|
117
|
+
const R = refract ? refraction(nShared) : null;
|
|
118
|
+
const skyShared = skyReflection(viewDirShared, nShared);
|
|
119
|
+
const fresShared = fresnel(viewDirShared, nShared);
|
|
120
|
+
|
|
121
|
+
mat.colorNode = Fn(() => {
|
|
122
|
+
const wp = positionWorld;
|
|
123
|
+
const t = time.mul(waveSpeed);
|
|
124
|
+
const shimmer = sin(wp.x.mul(0.7).add(t)).mul(sin(wp.z.mul(0.8).sub(t))).mul(0.05).add(1);
|
|
125
|
+
const d = attribute('aDepth', 'float');
|
|
126
|
+
const base = refract ? R.col : mix(SH, DP, smoothstep(0, maxDepth, d)); // sea: baked ramp
|
|
127
|
+
const water = mix(base, skyShared, fresShared).mul(shimmer);
|
|
128
|
+
const col = mix(water, FM, foamFull(d, positionLocal.xy.length())); // foam at every edge
|
|
129
|
+
// darken the WHOLE surface (base + foam, not just the sky reflection) with the sky's brightness,
|
|
130
|
+
// so water doesn't glow white at dusk/night. uSkyCol goes near-black at night → lit floors at 0.22.
|
|
131
|
+
const lit = clamp(uSkyCol.r.add(uSkyCol.g).add(uSkyCol.b).mul(0.55), 0.22, 1.0);
|
|
132
|
+
return col.mul(lit);
|
|
133
|
+
})();
|
|
134
|
+
|
|
135
|
+
mat.opacityNode = Fn(() => {
|
|
136
|
+
const d = attribute('aDepth', 'float'), pondD = positionLocal.xy.length();
|
|
137
|
+
// per-pixel shoreline feather (mm-deep water fades over the bed) gated by the baked
|
|
138
|
+
// inside test + the pond's sprawl clip
|
|
139
|
+
const inside = smoothstep(0.0, 0.05, d).mul(smoothstep(rc, rc.sub(0.5), pondD));
|
|
140
|
+
const feather = refract ? smoothstep(0.004, 0.06, R.vDepth) : smoothstep(0.0, 0.4, d).mul(0.92);
|
|
141
|
+
return clamp(mix(feather.mul(0.94), float(1), foamFull(d, pondD)).mul(inside), 0, 1);
|
|
142
|
+
})();
|
|
143
|
+
return mat;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Flowing river water: a ribbon mesh (built in zones.js) at world Y, with baked per-vertex
|
|
147
|
+
// aDepth + downstream UVs. Same see-through refraction + sky fresnel as the lakes; the
|
|
148
|
+
// downstream streak modulates the surface so the current still reads.
|
|
149
|
+
export function riverMaterial({
|
|
150
|
+
foam = 0xeef8f6, shallow = 0x4494a4, deep = 0x11557a, maxDepth = 1.3,
|
|
151
|
+
foamDepth = 0.2, flowSpeed = 0.5,
|
|
152
|
+
refract = true,
|
|
153
|
+
} = {}) {
|
|
154
|
+
const mat = new THREE.MeshBasicNodeMaterial({ transparent: true, depthWrite: true, side: THREE.DoubleSide });
|
|
155
|
+
const FM = C(foam), SH = C(shallow), DP = C(deep);
|
|
156
|
+
|
|
157
|
+
mat.positionNode = Fn(() => {
|
|
158
|
+
const p = positionLocal;
|
|
159
|
+
const sw = sin(p.x.mul(0.6).add(time.mul(0.8))).add(sin(p.z.mul(0.7).sub(time.mul(0.6)))).mul(0.05);
|
|
160
|
+
return vec3(p.x, p.y.add(sw), p.z); // gentle swell on the world-space ribbon
|
|
161
|
+
})();
|
|
162
|
+
|
|
163
|
+
const RIVER_MURK = vec3(0.22, 0.30, 0.32); // denser than lakes — gives the stream a visible body
|
|
164
|
+
|
|
165
|
+
// ---- flowmap ripples (braffolk WaterMaterial 132-151, faithful port) ----------------
|
|
166
|
+
// Two phases half a cycle apart, crossfaded by w2 so each phase's snap-back is hidden
|
|
167
|
+
// (the classic no-sliding-texture flowmap). Gradients come pre-derived from the baked
|
|
168
|
+
// noise texture, so a ripple layer costs ONE fetch. Flow = per-vertex downstream aFlow.
|
|
169
|
+
const noiseTex = waterNoiseTexture();
|
|
170
|
+
const PERIOD = 24; // world metres per noise tile
|
|
171
|
+
const flowV = attribute('aFlow', 'vec2');
|
|
172
|
+
const spd = flowV.length();
|
|
173
|
+
const CYC = 0.45;
|
|
174
|
+
const ph1 = fract(time.mul(CYC));
|
|
175
|
+
const ph2 = fract(time.mul(CYC).add(0.5));
|
|
176
|
+
const w2 = abs(ph1.sub(0.5)).mul(2);
|
|
177
|
+
const vel = flowV.mul(1.9).add(vec2(0.045, 0.03)); // rivers stream; the constant is the breeze fallback
|
|
178
|
+
const gradAt = (s, off) => texture(noiseTex, positionWorld.xz.sub(off).div(s * PERIOD)).zw.sub(0.5).div(0.02 * s);
|
|
179
|
+
const offA = vel.mul(ph1.div(CYC));
|
|
180
|
+
const offB = vel.mul(ph2.div(CYC)).add(vec2(3.71, 1.13));
|
|
181
|
+
const rippleLayer = (off) => gradAt(0.9, off).add(gradAt(3.4, off.mul(0.62)).mul(0.5));
|
|
182
|
+
const grad = mix(rippleLayer(offA), rippleLayer(offB), w2);
|
|
183
|
+
const rippleAmp = float(0.007).add(spd.mul(0.028)); // their tuned amp — bigger saturates fresnel ("white sheet")
|
|
184
|
+
const slope = grad.mul(rippleAmp);
|
|
185
|
+
const facetN = normalize(cross(dFdx(positionWorld), dFdy(positionWorld)));
|
|
186
|
+
const nShared = normalize(facetN.add(vec3(slope.x.negate(), 0, slope.y.negate()))); // ripples UNDER the facet silhouette
|
|
187
|
+
|
|
188
|
+
const viewShared = cameraPosition.sub(positionWorld);
|
|
189
|
+
const viewDirShared = viewShared.div(viewShared.length().max(1e-4));
|
|
190
|
+
const R = refract ? refraction(nShared, RIVER_MURK) : null; // ripple normal wobbles the refraction downstream
|
|
191
|
+
const skyShared = skyReflection(viewDirShared, nShared);
|
|
192
|
+
const fresShared = fresnel(viewDirShared, nShared);
|
|
193
|
+
|
|
194
|
+
// ---- foam (braffolk 283-308): two decorrelated advected scales MULTIPLY into clumpy
|
|
195
|
+
// patches (a single thresholded fbm slides as coherent stripes); variance-renormalised
|
|
196
|
+
// crossfade so coverage doesn't pulse at blend midpoints. Shore foam keys on the water
|
|
197
|
+
// column; RAPIDS foam keys on the baked downstream DROP (the stepped monotonic profile
|
|
198
|
+
// gives real rapids exactly where the water level steps down).
|
|
199
|
+
const dAttr = attribute('aDepth', 'float');
|
|
200
|
+
const foamUv = (off, s) => positionWorld.xz.sub(off).div(s * PERIOD);
|
|
201
|
+
const fA = texture(noiseTex, foamUv(offA, 0.55)).y;
|
|
202
|
+
const fB = texture(noiseTex, foamUv(offB.mul(1.13), 0.55)).y;
|
|
203
|
+
const dA = texture(noiseTex, foamUv(offA.mul(0.6), 0.21)).y;
|
|
204
|
+
const dB = texture(noiseTex, foamUv(offB.mul(0.71), 0.21)).y;
|
|
205
|
+
const varNorm = w2.mul(w2).add(w2.oneMinus().mul(w2.oneMinus())).sqrt();
|
|
206
|
+
const fblend = mix(fA, fB, w2).sub(0.5).div(varNorm).add(0.5);
|
|
207
|
+
const fDetail = mix(dA, dB, w2).sub(0.5).div(varNorm).add(0.5);
|
|
208
|
+
const foamPat = smoothstep(0.42, 0.85, fblend.mul(0.62).add(fDetail.mul(0.38)));
|
|
209
|
+
const shoreCol = refract ? R.vDepth : dAttr; // per-pixel column on high, baked depth on the ramp path
|
|
210
|
+
const shoreFoam = smoothstep(0.16, 0.03, shoreCol).mul(0.42).mul(smoothstep(0.6, 0.25, dAttr));
|
|
211
|
+
const rapidFoam = smoothstep(0.025, 0.09, attribute('aDrop', 'float')).mul(0.8);
|
|
212
|
+
const foamAmt = clamp(shoreFoam.add(rapidFoam), 0, 1).mul(foamPat).clamp(0, 0.68);
|
|
213
|
+
|
|
214
|
+
mat.colorNode = Fn(() => {
|
|
215
|
+
const base = refract ? R.col : mix(SH, DP, smoothstep(0, maxDepth, dAttr));
|
|
216
|
+
const water = mix(base, skyShared, fresShared);
|
|
217
|
+
return mix(water, FM, foamAmt);
|
|
218
|
+
})();
|
|
219
|
+
|
|
220
|
+
mat.opacityNode = Fn(() => {
|
|
221
|
+
const inside = smoothstep(0.0, 0.05, dAttr); // dry dive-skirt verts stay clipped
|
|
222
|
+
const feather = refract ? smoothstep(0.004, 0.06, R.vDepth) : smoothstep(0.0, 0.25, dAttr).mul(0.9);
|
|
223
|
+
const body = smoothstep(0.1, 0.7, dAttr).mul(0.55); // the channel core always reads as WATER, not damp ground
|
|
224
|
+
return clamp(max(feather.mul(0.94), body).add(foamAmt.mul(0.6)).mul(inside), 0, 1);
|
|
225
|
+
})();
|
|
226
|
+
return mat;
|
|
227
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// CPU-baked tileable water noise — braffolk's NoiseBake, boot-time edition (~10ms).
|
|
2
|
+
// One 256² RGBA float texture: r = fbm, g = decorrelated fbm (foam), b/a = d(fbm)/du,dv
|
|
3
|
+
// packed as gradient*0.02 + 0.5. Ripple layers and foam patterns each cost ONE fetch.
|
|
4
|
+
import * as THREE from 'three/webgpu';
|
|
5
|
+
|
|
6
|
+
let tex = null;
|
|
7
|
+
|
|
8
|
+
export function waterNoiseTexture() {
|
|
9
|
+
if (tex) return tex;
|
|
10
|
+
const N = 256;
|
|
11
|
+
const data = new Float32Array(N * N * 4);
|
|
12
|
+
// periodic value-noise lattices (deterministic LCG)
|
|
13
|
+
const lat = (n, seed) => {
|
|
14
|
+
const g = new Float32Array(n * n);
|
|
15
|
+
let s = seed >>> 0;
|
|
16
|
+
for (let i = 0; i < n * n; i++) { s = (s * 1664525 + 1013904223) >>> 0; g[i] = s / 4294967296; }
|
|
17
|
+
return g;
|
|
18
|
+
};
|
|
19
|
+
const L1 = lat(8, 1234567), L2 = lat(16, 424242), L3 = lat(32, 87654321), L4 = lat(8, 5550123);
|
|
20
|
+
const sample = (L, n, u, v) => { // periodic smooth bilinear
|
|
21
|
+
u = ((u % 1) + 1) % 1; v = ((v % 1) + 1) % 1;
|
|
22
|
+
const x = u * n, y = v * n;
|
|
23
|
+
const x0 = Math.floor(x) % n, y0 = Math.floor(y) % n, x1 = (x0 + 1) % n, y1 = (y0 + 1) % n;
|
|
24
|
+
let fx = x - Math.floor(x), fy = y - Math.floor(y);
|
|
25
|
+
fx = fx * fx * (3 - 2 * fx); fy = fy * fy * (3 - 2 * fy);
|
|
26
|
+
const a = L[y0 * n + x0], b = L[y0 * n + x1], c = L[y1 * n + x0], d = L[y1 * n + x1];
|
|
27
|
+
return a + (b - a) * fx + (c - a) * fy + (a - b - c + d) * fx * fy;
|
|
28
|
+
};
|
|
29
|
+
const fbmAt = (u, v) => sample(L1, 8, u, v) * 0.55 + sample(L2, 16, u, v) * 0.3 + sample(L3, 32, u, v) * 0.15;
|
|
30
|
+
const fbm2At = (u, v) => sample(L4, 8, u + 0.37, v + 0.61) * 0.6 + sample(L3, 32, u + 0.11, v + 0.83) * 0.4;
|
|
31
|
+
const e = 1 / N;
|
|
32
|
+
for (let j = 0; j < N; j++) {
|
|
33
|
+
for (let i = 0; i < N; i++) {
|
|
34
|
+
const u = i / N, v = j / N, o = (j * N + i) * 4;
|
|
35
|
+
data[o] = fbmAt(u, v);
|
|
36
|
+
data[o + 1] = fbm2At(u, v);
|
|
37
|
+
data[o + 2] = (fbmAt(u + e, v) - fbmAt(u - e, v)) / (2 * e) * 0.02 + 0.5; // d/du per uv-unit, packed
|
|
38
|
+
data[o + 3] = (fbmAt(u, v + e) - fbmAt(u, v - e)) / (2 * e) * 0.02 + 0.5;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
tex = new THREE.DataTexture(data, N, N, THREE.RGBAFormat, THREE.FloatType);
|
|
42
|
+
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
|
|
43
|
+
tex.minFilter = THREE.LinearFilter;
|
|
44
|
+
tex.magFilter = THREE.LinearFilter;
|
|
45
|
+
tex.needsUpdate = true;
|
|
46
|
+
return tex;
|
|
47
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Shared weather→ground uniforms. Module-level singletons so the terrain, grass, and Weather all
|
|
2
|
+
// reference the SAME nodes regardless of build order (terrain/grass build before/after Weather).
|
|
3
|
+
// Weather sets the values each frame (eased, so the ground soaks/dries and snow builds/melts gradually).
|
|
4
|
+
import * as THREE from 'three/webgpu';
|
|
5
|
+
import { uniform } from 'three/tsl';
|
|
6
|
+
|
|
7
|
+
export const uWet = uniform(0); // 0 dry … 1 soaked — rain darkens the ground
|
|
8
|
+
export const uCover = uniform(0); // 0 bare … 1 snow-covered — snow whitens up-facing ground + grass tips
|
|
9
|
+
export const uDayness = uniform(1); // 0 night … 1 day (sky.js writes it) — dims UNLIT baked-albedo materials (impostors) so they stop glowing at night
|
|
10
|
+
export const uSkyCol = uniform(new THREE.Color(0x7fb2e5)); // current sky-phase zenith colour (sky.js writes)
|
|
11
|
+
export const uHorizonCol = uniform(new THREE.Color(0xdfe9d8)); // current horizon colour — water reflections track day/dusk/night for free
|
|
12
|
+
export const uSunDir = uniform(new THREE.Vector3(0.3, 0.8, 0.3)); // current key light direction (sun by day, moon by night) — grass backlight etc.
|
|
13
|
+
export const uRain = uniform(0); // 0 dry … 1 downpour — the rain FALLING RIGHT NOW.
|
|
14
|
+
// Not the same thing as uWet, and the difference is the whole point:
|
|
15
|
+
// the ground stays dark and slick for a good while after a storm
|
|
16
|
+
// passes, but the moment the rain stops, drops stop landing on it.
|
|
17
|
+
// Splashes read off this; puddles and sheen read off uWet.
|
|
18
|
+
export const uRainT = uniform(0); // seconds, running while it rains — the ground's clock, so the rings
|
|
19
|
+
// spreading on the puddles beat in time with the rain actually falling.
|
|
20
|
+
// (Weather had a uTime of its own, but it was private to the rain
|
|
21
|
+
// particles; the ground could not see it, so the ground could not move.)
|
|
22
|
+
export const uCloudiness = uniform(0); // 0 fair … 1 storm deck (weather.js writes; LEADS the rain/snow so the sky thickens BEFORE precipitation and clears after)
|
|
23
|
+
export const uWindDir = uniform(new THREE.Vector2(1, 0)); // unit heading the wind blows TOWARD, on the ground plane (XZ). world/wind.js writes it each frame.
|
|
24
|
+
export const uWindStr = uniform(0.4); // 0 calm … ~1 gusting — the global wind strength. Grass sway + rain slant scale off it; the CPU tumbleweeds read wind.vx/vz directly.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// GLOBAL WIND — one slowly-wandering heading and a gusting strength, ticked once a frame. Everything
|
|
2
|
+
// that should lean on the weather reads from HERE instead of rolling its own: the tumbleweeds roll on it
|
|
3
|
+
// (CPU: wind.vx/vz), and the grass sway + rain slant read it in their shaders (via uWindDir / uWindStr).
|
|
4
|
+
// `angle` is the heading the wind blows TOWARD (radians on the ground plane, atan2(z, x)). Deterministic
|
|
5
|
+
// layered sines drive the drift + gusts — smooth, no per-frame RNG jerks, and identical after a reload.
|
|
6
|
+
import { uWindDir, uWindStr } from './weatherUniforms.js';
|
|
7
|
+
|
|
8
|
+
export class Wind {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.t = 0;
|
|
11
|
+
this.baseAngle = 0.7; // the PREVAILING heading (fixed ~SE-ish); the wind only veers gently around it
|
|
12
|
+
this.angle = this.baseAngle;
|
|
13
|
+
this.base = 1.6; // mean speed (m/s) for the CPU consumers (tumbleweeds) — a light breeze, not a gale (Nick: they rolled too fast)
|
|
14
|
+
this.gust = 0.5; // 0..1 current gust factor
|
|
15
|
+
this.speed = this.base;
|
|
16
|
+
this.x = Math.cos(this.angle); this.z = Math.sin(this.angle); // unit heading
|
|
17
|
+
this.vx = this.x * this.speed; this.vz = this.z * this.speed; // velocity, m/s
|
|
18
|
+
this._publish();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
update(dt) {
|
|
22
|
+
this.t += dt;
|
|
23
|
+
// VEER, don't circle. Set the heading DIRECTLY as prevailing ± a bounded oscillation (~±27°); the old
|
|
24
|
+
// code INTEGRATED sines (angle += …·dt), whose low-frequency term summed to several radians and swept
|
|
25
|
+
// the wind the whole way round the compass over time (Nick: "pushed things in a 360, weird").
|
|
26
|
+
this.angle = this.baseAngle + Math.sin(this.t * 0.021) * 0.33 + Math.sin(this.t * 0.057 + 1.3) * 0.14;
|
|
27
|
+
// gusts swell and lull between ~0.12 and ~1.05
|
|
28
|
+
this.gust = Math.max(0.12, 0.55 + 0.5 * Math.sin(this.t * 0.21) * Math.sin(this.t * 0.083 + 1.0));
|
|
29
|
+
this.speed = this.base * (0.5 + 0.85 * this.gust);
|
|
30
|
+
this.x = Math.cos(this.angle); this.z = Math.sin(this.angle);
|
|
31
|
+
this.vx = this.x * this.speed; this.vz = this.z * this.speed;
|
|
32
|
+
this._publish();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// hand the current wind to the shared shader uniforms (grass sway, rain slant, and whatever else opts in)
|
|
36
|
+
_publish() {
|
|
37
|
+
uWindDir.value.set(this.x, this.z);
|
|
38
|
+
uWindStr.value = this.gust;
|
|
39
|
+
}
|
|
40
|
+
}
|