@call-me-sensei/toonlab 0.4.21 → 0.4.23
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/NPM-LIBRARY.md +87 -27
- package/README.md +11 -10
- package/agents/PROMPTS.md +17 -3
- package/agents/README.md +13 -3
- package/agents/claude/CLAUDE.md +3 -0
- package/agents/codex/AGENTS.md +112 -159
- package/agents/cursor/toonlab.mdc +7 -3
- package/agents/examples/game-foundation.mjs +104 -0
- package/agents/references/game-lifecycle.md +57 -0
- package/agents/references/runtime-entry-points.md +26 -0
- package/agents/references/style-bundles.md +4 -1
- package/agents/skills/claude/asset-sourcing/SKILL.md +7 -4
- package/agents/skills/claude/environment/SKILL.md +4 -0
- package/agents/skills/claude/game-dev/SKILL.md +63 -87
- package/agents/skills/claude/lighting/SKILL.md +36 -0
- package/agents/skills/claude/post-processing/SKILL.md +8 -3
- package/agents/skills/claude/rock-ground-shaders/SKILL.md +10 -15
- package/agents/skills/claude/rockgen/SKILL.md +52 -21
- package/agents/skills/claude/scene-style-application/SKILL.md +4 -0
- package/agents/skills/claude/style-presets/SKILL.md +11 -8
- package/agents/skills/claude/toon-shading/SKILL.md +4 -0
- package/agents/skills/claude/vegetation-sky/SKILL.md +13 -7
- package/agents/skills/claude/visual-verification/SKILL.md +45 -119
- package/agents/skills/claude/water/SKILL.md +6 -2
- package/agents/skills/codex/asset-sourcing/SKILL.md +7 -4
- package/agents/skills/codex/environment/SKILL.md +4 -0
- package/agents/skills/codex/game-dev/SKILL.md +63 -87
- package/agents/skills/codex/lighting/SKILL.md +36 -0
- package/agents/skills/codex/post-processing/SKILL.md +8 -3
- package/agents/skills/codex/rock-ground-shaders/SKILL.md +10 -15
- package/agents/skills/codex/rockgen/SKILL.md +52 -21
- package/agents/skills/codex/scene-style-application/SKILL.md +4 -0
- package/agents/skills/codex/style-presets/SKILL.md +11 -8
- package/agents/skills/codex/toon-shading/SKILL.md +4 -0
- package/agents/skills/codex/vegetation-sky/SKILL.md +13 -7
- package/agents/skills/codex/visual-verification/SKILL.md +45 -119
- package/agents/skills/codex/water/SKILL.md +6 -2
- package/database/apply-one-catalog-seed.mjs +63 -0
- package/database/providers.mjs +1 -0
- package/database/seeds/catalog/0005_2026-08-c8-first12.sql +1101 -0
- package/database/seeds/catalog/0006_2026-09-c8-first100.sql +14245 -0
- package/database/seeds/catalog/0007_2026-09-c8-first100-primary-model.sql +14845 -0
- package/mcp/lab-management.mjs +11 -8
- package/mcp/public-catalog.mjs +0 -16
- package/mcp/tree-lab-contract.mjs +2381 -0
- package/mcp/vite-plugin.mjs +5 -1
- package/package.json +29 -18
- package/src/catalog/officialCatalog.js +1 -0
- package/src/catalog/officialCatalogAssetRuntime.js +98 -1
- package/src/catalog/officialCatalogLod.js +160 -16
- package/src/catalog/officialCatalogPlacement.js +52 -12
- package/src/catalog/officialCatalogProvider.js +79 -8
- package/src/catalog/officialCatalogRockPackage.js +172 -0
- package/src/cloud/cloudReprojection.js +40 -7
- package/src/cloud/cloudVolume.js +44 -7
- package/src/cloud/noise/baseShapeVolume.js +8 -6
- package/src/cloud/noise/cirrusMap.js +2 -1
- package/src/cloud/noise/curlNoise.js +2 -1
- package/src/cloud/noise/erosionVolume.js +2 -1
- package/src/cloud/noise/textureCache.js +77 -0
- package/src/cloud/noise/weatherMap.js +2 -1
- package/src/core/sha256.js +83 -0
- package/src/ground-shader/groundShaderMaterial.js +12 -3
- package/src/rock-shader/rockMaterial.js +205 -28
- package/src/rock-shader/rockShaderRuntime.js +17 -1
- package/src/rock-shader/rockShaderSettings.js +16 -0
- package/src/rockgen/index.js +9 -0
- package/src/rockgen/lod/index.js +1 -0
- package/src/rockgen/lod/rockDenseFieldPolicy.js +143 -0
- package/src/rockgen/rockDocument.js +899 -35
- package/src/rockgen/surface/c7GeologySurface.js +39 -9
- package/src/rockgen/surface/naturalRockSurface.js +1948 -0
- package/src/shaders-tsl/chunks/projected-water-caustics.js +14 -2
- package/src/shaders-tsl/chunks/water-foam.js +6 -6
- package/src/shaders-tsl/chunks/water-lighting.js +28 -5
- package/src/shaders-tsl/chunks/water-shore-state.js +5 -2
- package/src/shaders-tsl/chunks/water-waves.js +10 -5
- package/src/shaders-tsl/flower.js +7 -4
- package/src/shaders-tsl/grass.js +4 -3
- package/src/shaders-tsl/water-shore-state-simulation.js +3 -2
- package/src/shaders-tsl/water.js +209 -41
- package/src/sky/skySystem.js +30 -7
- package/src/toon/settings/perspectiveRemovalSettings.js +1 -1
- package/src/toon/toonSettings.js +74 -0
- package/src/vegetation/stylizedFlower.js +6 -0
- package/src/version.js +1 -1
- package/src/water/waterDetailSpectrum.js +91 -0
- package/src/water/waterDynamics.js +422 -0
- package/src/water/waterFoamParticles.js +196 -0
- package/src/water/waterFoamTexture.js +57 -0
- package/src/water/waterHydrodynamics.js +209 -0
- package/src/water/waterMaterial.js +1 -0
- package/src/water/waterRenderExtension.js +107 -0
- package/src/water/waterScenePasses.js +14 -6
- package/src/water/waterSettings.js +53 -26
- package/src/water/waterShoreMaterial.js +38 -14
- package/src/water/waterSpectralOcean.js +137 -0
- package/src/water/waterStageSettings.js +19 -1
- package/src/water/waterSurface.js +62 -18
- package/src/water/waterUnderwaterAtmosphere.js +30 -3
- package/types/catalog/officialCatalog.d.ts +1 -0
- package/types/catalog/officialCatalogPlacement.d.ts +4 -0
- package/types/catalog/officialCatalogRockPackage.d.ts +68 -0
- package/types/cloud/noise/baseShapeVolume.d.ts +1 -1
- package/types/cloud/noise/textureCache.d.ts +12 -0
- package/types/core/sha256.d.ts +2 -0
- package/types/index.d.ts +30 -0
- package/types/rock-shader/rockMaterial.d.ts +3 -0
- package/types/rock-shader/rockShaderSettings.d.ts +2 -0
- package/types/rockgen/index.d.ts +1 -0
- package/types/rockgen/lod/index.d.ts +1 -0
- package/types/rockgen/lod/rockDenseFieldPolicy.d.ts +181 -0
- package/types/rockgen/rockDocument.d.ts +654 -71
- package/types/rockgen/surface/naturalRockSurface.d.ts +100 -0
- package/types/shaders-tsl/chunks/projected-water-caustics.d.ts +7 -1
- package/types/shaders-tsl/chunks/water-lighting.d.ts +4 -3
- package/types/shaders-tsl/water.d.ts +2 -1
- package/types/version.d.ts +1 -1
- package/types/water/waterDetailSpectrum.d.ts +13 -0
- package/types/water/waterDynamics.d.ts +89 -0
- package/types/water/waterFoamParticles.d.ts +32 -0
- package/types/water/waterFoamTexture.d.ts +1 -0
- package/types/water/waterHydrodynamics.d.ts +49 -0
- package/types/water/waterRenderExtension.d.ts +16 -0
- package/types/water/waterScenePasses.d.ts +1 -1
- package/types/water/waterSettings.d.ts +1 -0
- package/types/water/waterSpectralOcean.d.ts +17 -0
- package/types/water/waterStageSettings.d.ts +1 -1
- package/types/water/waterUnderwaterAtmosphere.d.ts +19 -4
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { MeshBasicNodeMaterial } from 'three/webgpu';
|
|
3
|
+
import { attribute, clamp, dot, length, max, mix, normalWorld, positionWorld, smoothstep, texture, uniform, uv } from 'three/tsl';
|
|
4
|
+
import { getWaterFoamTexture } from './waterFoamTexture.js';
|
|
5
|
+
import { sampleEnvironmentSunShadow } from '../shaders-tsl/chunks/environment-sun-shadow.js';
|
|
6
|
+
import { sampleEnvironmentCloudShadow } from '../sky/cloudShadow.js';
|
|
7
|
+
|
|
8
|
+
// Small floating foam rafts, each resolving several bubbles. They translate
|
|
9
|
+
// with solved flow but do not stretch into polygons with a texture mapping.
|
|
10
|
+
export class WaterFoamParticles {
|
|
11
|
+
constructor(dynamics, count=12000) {
|
|
12
|
+
this.dynamics=dynamics;this.count=count;this.seed=6297;
|
|
13
|
+
this.points=new Float32Array(count*6); // x,z,age,lifetime,size,rotation
|
|
14
|
+
this.thickness=new Float32Array(count);
|
|
15
|
+
for(let i=0;i<count;i++)this.points[i*6+2]=-1;
|
|
16
|
+
this.alpha=new Float32Array(count);
|
|
17
|
+
const seeds=new Float32Array(count);
|
|
18
|
+
for(let i=0;i<count;i++)seeds[i]=this.random()*8;
|
|
19
|
+
// A rounded cap, not a flat card: its base touches the water and its
|
|
20
|
+
// crown gives the aerated lip a centimetre-scale silhouette and shading.
|
|
21
|
+
const geometry=new THREE.PlaneGeometry(1,1,4,4);geometry.rotateX(-Math.PI/2);
|
|
22
|
+
const position=geometry.attributes.position;
|
|
23
|
+
for(let i=0;i<position.count;i++) {
|
|
24
|
+
const r2=4*(position.getX(i)**2+position.getZ(i)**2);
|
|
25
|
+
position.setY(i,Math.max(0,1-r2)**.7);
|
|
26
|
+
}
|
|
27
|
+
geometry.computeVertexNormals();
|
|
28
|
+
geometry.setAttribute('aFoamAlpha',new THREE.InstancedBufferAttribute(this.alpha,1).setUsage(THREE.DynamicDrawUsage));
|
|
29
|
+
geometry.setAttribute('aFoamSeed',new THREE.InstancedBufferAttribute(seeds,1));
|
|
30
|
+
this.color=uniform(new THREE.Color());
|
|
31
|
+
this.sun=uniform(new THREE.Vector3(0,1,0));
|
|
32
|
+
const material=new MeshBasicNodeMaterial({transparent:true,depthWrite:false,side:THREE.DoubleSide});
|
|
33
|
+
const bubble=texture(getWaterFoamTexture()).sample(uv().mul(.25));
|
|
34
|
+
const seed=attribute('aFoamSeed','float');
|
|
35
|
+
const shape=texture(getWaterFoamTexture()).sample(uv().mul(.3).add(seed)).b;
|
|
36
|
+
const radius=shape.mul(.22).add(.22);
|
|
37
|
+
const disk=smoothstep(radius.sub(.035),radius.add(.015),length(uv().sub(.5))).oneMinus();
|
|
38
|
+
const visibility=sampleEnvironmentSunShadow(positionWorld).mul(sampleEnvironmentCloudShadow(positionWorld,1));
|
|
39
|
+
const diffuse=max(dot(normalWorld,this.sun),0).mul(.38).add(.62);
|
|
40
|
+
material.colorNode=this.color.mul(bubble.r).mul(mix(.6,1,visibility)).mul(diffuse);
|
|
41
|
+
material.opacityNode=clamp(attribute('aFoamAlpha','float').mul(disk).mul(mix(.15,1,bubble.g)),0,1);
|
|
42
|
+
this.mesh=new THREE.InstancedMesh(geometry,material,count);
|
|
43
|
+
this.mesh.name='FloatingSwashFoam';this.mesh.frustumCulled=false;this.mesh.renderOrder=3;
|
|
44
|
+
this.mesh.userData.waterExclude=true;this.mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
|
|
45
|
+
dynamics.surface.add(this.mesh);
|
|
46
|
+
this.focus=new THREE.Vector3();this.sample={};this.alive=0;
|
|
47
|
+
this.frontState=new Float32Array(dynamics.columns*4);
|
|
48
|
+
this.previousEdges=new Float32Array(dynamics.columns);
|
|
49
|
+
this.frontReady=false;
|
|
50
|
+
this.frontTexture=new THREE.DataTexture(this.frontState,dynamics.columns,1,THREE.RGBAFormat,THREE.FloatType);
|
|
51
|
+
this.frontTexture.minFilter=this.frontTexture.magFilter=THREE.LinearFilter;
|
|
52
|
+
this.frontTexture.generateMipmaps=false;
|
|
53
|
+
}
|
|
54
|
+
random(){this.seed=(Math.imul(this.seed,1664525)+1013904223)>>>0;return (this.seed+.5)/4294967296;}
|
|
55
|
+
concentration(x,z) {
|
|
56
|
+
const d=this.dynamics;
|
|
57
|
+
return d.hydro.sampleArray(d.hydro.foam,(x-d.minX)/d.dx,(z-d.minZ)/d.dz);
|
|
58
|
+
}
|
|
59
|
+
edgeAt(x) {
|
|
60
|
+
const d=this.dynamics;
|
|
61
|
+
if(d.options.shorelineAxis!=='z')return d.shoreEdgeAt(x);
|
|
62
|
+
const gx=THREE.MathUtils.clamp((x-d.minX)/d.dx,0,d.columns-1),i=Math.min(d.columns-2,Math.floor(gx));
|
|
63
|
+
return THREE.MathUtils.lerp(d.shoreContour[i],d.shoreContour[i+1],gx-i);
|
|
64
|
+
}
|
|
65
|
+
sampleFront(x,component,previous=false) {
|
|
66
|
+
const d=this.dynamics,gx=THREE.MathUtils.clamp((x-d.minX)/d.dx,0,d.columns-1),i=Math.min(d.columns-2,Math.floor(gx));
|
|
67
|
+
const a=previous?this.previousEdges[i]:this.frontState[i*4+component];
|
|
68
|
+
const b=previous?this.previousEdges[i+1]:this.frontState[(i+1)*4+component];
|
|
69
|
+
return THREE.MathUtils.lerp(a,b,gx-i);
|
|
70
|
+
}
|
|
71
|
+
updateFront(dt) {
|
|
72
|
+
const d=this.dynamics;
|
|
73
|
+
if(d.options.shorelineAxis!=='z')return;
|
|
74
|
+
for(let i=0;i<d.columns;i++) {
|
|
75
|
+
const x=d.minX+i*d.dx,edge=this.edgeAt(x),j=i*4;
|
|
76
|
+
const previous=this.frontReady?this.frontState[j]:edge;
|
|
77
|
+
this.previousEdges[i]=previous;
|
|
78
|
+
if(dt>0&&this.frontReady) {
|
|
79
|
+
const speed=THREE.MathUtils.clamp((edge-previous)/dt,-5,5);
|
|
80
|
+
this.frontState[j+1]=THREE.MathUtils.lerp(this.frontState[j+1],speed,1-Math.exp(-dt/.18));
|
|
81
|
+
}
|
|
82
|
+
this.frontState[j]=edge;
|
|
83
|
+
const concentration=Math.max(this.concentration(x,edge-.025),this.concentration(x,edge-.15),this.concentration(x,edge-.3));
|
|
84
|
+
// Remember existing aeration through the drain. This is a visual
|
|
85
|
+
// contact-line reservoir; it neither creates water nor refills a calm
|
|
86
|
+
// shore. Its contribution fades and is thinner during retreat.
|
|
87
|
+
this.frontState[j+2]=Math.max(concentration,this.frontState[j+2]*Math.exp(-dt/5));
|
|
88
|
+
this.frontState[j+3]+=d.sample(x,edge-.2,this.sample).velocityZ*dt;
|
|
89
|
+
}
|
|
90
|
+
this.frontReady=true;this.frontTexture.needsUpdate=true;
|
|
91
|
+
d.surface.material.uniforms.uFoamFrontState.value=this.frontTexture;
|
|
92
|
+
d.surface.material.uniforms.uUseFoamFrontState.value=1;
|
|
93
|
+
}
|
|
94
|
+
retreatAt(x) {return this.frontReady?THREE.MathUtils.smoothstep(-this.sampleFront(x,1),.02,.18):0;}
|
|
95
|
+
patchAt(x,edge) {
|
|
96
|
+
// Smooth packet-scale variation, transported with the changing front.
|
|
97
|
+
// The aeration field still decides whether any foam can exist here.
|
|
98
|
+
return .5+.5*Math.sin(x*1.31+edge*.27)*Math.sin(x*.47-edge*.19+2.1);
|
|
99
|
+
}
|
|
100
|
+
networkAt(x,z) {
|
|
101
|
+
const {data,width:n}=getWaterFoamTexture().image;
|
|
102
|
+
const flow=this.frontReady?this.sampleFront(x,3):0;
|
|
103
|
+
const gx=((x/2.4%1)+1)%1*n,gz=(((z-flow)/2.4%1)+1)%1*n;
|
|
104
|
+
const ix=Math.floor(gx),iz=Math.floor(gz),tx=gx-ix,tz=gz-iz;
|
|
105
|
+
const at=(a,b)=>data[((b%n)*n+a%n)*4+3]/255;
|
|
106
|
+
return (at(ix,iz)*(1-tx)+at(ix+1,iz)*tx)*(1-tz)+(at(ix,iz+1)*(1-tx)+at(ix+1,iz+1)*tx)*tz;
|
|
107
|
+
}
|
|
108
|
+
availableFoam(x,z) {
|
|
109
|
+
const current=this.concentration(x,z),edge=this.edgeAt(x);
|
|
110
|
+
if(!this.frontReady||edge==null)return current;
|
|
111
|
+
return Math.max(current,this.sampleFront(x,2)*this.retreatAt(x)*Math.exp(-Math.max(0,edge-z)/.4));
|
|
112
|
+
}
|
|
113
|
+
update(dt) {
|
|
114
|
+
const d=this.dynamics,s=d.surface,settings=s.renderedSettings??s.settings;
|
|
115
|
+
this.updateFront(dt);
|
|
116
|
+
if(typeof s.followTarget==='function')s.followTarget(this.focus);
|
|
117
|
+
const cx=THREE.MathUtils.clamp(this.focus.x,d.minX+12,d.minX+s.width-12);
|
|
118
|
+
const cz=(this.edgeAt(cx)??0)-1.5;
|
|
119
|
+
const gain=settings.foamAmount*settings.swashFoamAmount;
|
|
120
|
+
this.color.value.setRGB(...settings.foamColor,THREE.SRGBColorSpace);
|
|
121
|
+
this.sun.value.set(...settings.sunDirection).normalize();
|
|
122
|
+
const matrices=this.mesh.instanceMatrix.array;
|
|
123
|
+
let budget=dt>0?Math.min(this.count,Math.ceil(dt*18000)):0,alive=0,packetRemaining=0,packetX=cx,packetZ=cz;
|
|
124
|
+
let packetRim=true,packetPatch=1,packetSpread=.035;
|
|
125
|
+
for(let i=0;i<this.count;i++) {
|
|
126
|
+
const j=i*6;
|
|
127
|
+
let x=this.points[j],z=this.points[j+1],age=this.points[j+2];
|
|
128
|
+
if(age>=0&&dt>0) {
|
|
129
|
+
const oldX=x,oldZ=z,oldEdge=this.frontReady?this.sampleFront(x,0,true):null;
|
|
130
|
+
const flow=d.sample(x,z,this.sample);
|
|
131
|
+
x+=flow.velocityX*dt;z+=flow.velocityZ*dt;age+=dt;
|
|
132
|
+
if(oldEdge!=null&&oldEdge-oldZ>=0&&oldEdge-oldZ<.35) {
|
|
133
|
+
const newEdge=this.edgeAt(x),edgeDelta=newEdge-this.sampleFront(oldX,0,true);
|
|
134
|
+
// The narrow rim remains with the receding contact line. Foam
|
|
135
|
+
// farther into the sheet continues to follow the bulk current.
|
|
136
|
+
if(edgeDelta<0)z=oldZ+edgeDelta;
|
|
137
|
+
}
|
|
138
|
+
if(age>this.points[j+3]||Math.abs(x-cx)>13||Math.abs(z-cz)>5)age=-1;
|
|
139
|
+
}
|
|
140
|
+
if(age<0&&budget>0&&gain>0) {
|
|
141
|
+
budget--;
|
|
142
|
+
if(packetRemaining--<=0){
|
|
143
|
+
packetRemaining=4+Math.floor(this.random()*9);
|
|
144
|
+
packetX=cx+(this.random()-.5)*23.6;
|
|
145
|
+
const front=this.edgeAt(packetX);
|
|
146
|
+
packetPatch=front==null?1:this.patchAt(packetX,front);
|
|
147
|
+
packetRim=front==null||this.random()<.42;
|
|
148
|
+
const drain=1-.5*this.retreatAt(packetX);
|
|
149
|
+
const width=(.18-.11*packetPatch)*drain;
|
|
150
|
+
const distance=packetRim ? .018+Math.min(.65,-Math.log(this.random())*width)
|
|
151
|
+
: .35+this.random()**1.25*(.6+1.65*packetPatch)*drain;
|
|
152
|
+
packetZ=front==null?cz+(this.random()-.5)*7.6:front-distance;
|
|
153
|
+
packetSpread=packetRim?.035:.25+.45*packetPatch;
|
|
154
|
+
}
|
|
155
|
+
x=packetX+(this.random()-.5)*.24;
|
|
156
|
+
const localFront=this.edgeAt(x),packetFront=this.edgeAt(packetX);
|
|
157
|
+
z=packetZ+(localFront!=null&&packetFront!=null?localFront-packetFront:0)+(this.random()-.5)*packetSpread;
|
|
158
|
+
const column=d.sample(x,z,this.sample),foam=this.availableFoam(x,z),retreat=this.retreatAt(x);
|
|
159
|
+
const patchDensity=localFront==null?1:(packetRim?.65:.3)+(packetRim?.35:.7)*packetPatch;
|
|
160
|
+
const network=localFront==null||packetRim?1:.25+.75*this.networkAt(x,z);
|
|
161
|
+
if(column.wet&&column.depth<.3&&foam>(retreat>.1?.008:.04)&&this.random()<(1-.6*retreat)*patchDensity*network) {
|
|
162
|
+
age=0;this.points[j+3]=2.5+this.random()*3;
|
|
163
|
+
const lip=localFront!=null&&localFront-z<.35;
|
|
164
|
+
this.points[j+4]=lip?.055+(.055+.13*packetPatch)*this.random():.035+this.random()*.1;
|
|
165
|
+
this.thickness[i]=lip?.006+.025*packetPatch**1.5*(.5+.5*this.random()):.0015+this.random()*.005;
|
|
166
|
+
this.points[j+5]=lip?this.random()*Math.PI*2:Math.atan2(column.velocityX,column.velocityZ)+(this.random()-.5);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
this.points[j]=x;this.points[j+1]=z;this.points[j+2]=age;
|
|
170
|
+
if(age<0){this.alpha[i]=0;continue;}
|
|
171
|
+
const column=d.sample(x,z,this.sample),foam=this.availableFoam(x,z);
|
|
172
|
+
if(!column.wet||foam<.008){this.points[j+2]=-1;this.alpha[i]=0;continue;}
|
|
173
|
+
const life=this.points[j+3];
|
|
174
|
+
const fade=THREE.MathUtils.smoothstep(age,0,.18)*(1-THREE.MathUtils.smoothstep(age,life-.6,life));
|
|
175
|
+
const edge=(1-THREE.MathUtils.smoothstep(Math.abs(x-cx),10,12))*(1-THREE.MathUtils.smoothstep(Math.abs(z-cz),3,4));
|
|
176
|
+
this.alpha[i]=fade*edge*(1-.6*this.retreatAt(x))*THREE.MathUtils.clamp(foam*5*gain,0,1);
|
|
177
|
+
const size=this.points[j+4],angle=this.points[j+5],c=Math.cos(angle),sn=Math.sin(angle),k=i*16;
|
|
178
|
+
const distance=(this.edgeAt(x)??z)-z,filament=distance>.35;
|
|
179
|
+
const sx=size*(filament?.45:1),sz=size*(filament?1.8:1);
|
|
180
|
+
const nx=THREE.MathUtils.clamp(Math.round((x-d.minX)/d.dx),0,d.columns-1);
|
|
181
|
+
const nz=THREE.MathUtils.clamp(Math.round((z-d.minZ)/d.dz),0,d.rows-1),ni=(nz*d.columns+nx)*3;
|
|
182
|
+
const gx=-d.normals[ni]/Math.max(d.normals[ni+1],.1),gz=-d.normals[ni+2]/Math.max(d.normals[ni+1],.1);
|
|
183
|
+
const height=this.thickness[i]*(1-.65*this.retreatAt(x))*(.5+.5*fade);
|
|
184
|
+
matrices[k]=c*sx;matrices[k+2]=-sn*sx;matrices[k+5]=height;matrices[k+8]=sn*sz;matrices[k+10]=c*sz;
|
|
185
|
+
matrices[k+1]=(gx*c-gz*sn)*sx;matrices[k+9]=(gx*sn+gz*c)*sz;
|
|
186
|
+
matrices[k+12]=x-d.x;matrices[k+13]=column.height-d.level+.0015;matrices[k+14]=z-d.z;matrices[k+15]=1;
|
|
187
|
+
alive++;
|
|
188
|
+
}
|
|
189
|
+
this.alive=alive;
|
|
190
|
+
this.mesh.visible=gain>0;
|
|
191
|
+
this.mesh.geometry.attributes.aFoamAlpha.needsUpdate=true;this.mesh.instanceMatrix.needsUpdate=true;
|
|
192
|
+
s.material.uniforms.uFoamParticleRegion.value.set(cx,cz,12,4);
|
|
193
|
+
s.material.uniforms.uFoamParticleCoverage.value=gain>0?Math.min(1,alive/4500):0;
|
|
194
|
+
}
|
|
195
|
+
dispose(){this.dynamics.surface.material.uniforms.uUseFoamFrontState.value=0;this.frontTexture.dispose();this.mesh.removeFromParent();this.mesh.geometry.dispose();this.mesh.material.dispose();}
|
|
196
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
let cached;
|
|
4
|
+
// Packed bubble membranes and separated small bubbles. Generated once;
|
|
5
|
+
// this is surface detail, not glowing spray points or a scrolling photograph.
|
|
6
|
+
export function getWaterFoamTexture() {
|
|
7
|
+
if (cached) return cached;
|
|
8
|
+
const n=512, cells=32, data=new Uint8Array(n*n*4);
|
|
9
|
+
const hash=(x,y,salt=0)=>{const v=Math.sin(x*127.1+y*311.7+salt*91.3)*43758.5453;return v-Math.floor(v);};
|
|
10
|
+
const wrap=v=>(v%cells+cells)%cells;
|
|
11
|
+
const smooth=(a,b,v)=>{const t=Math.max(0,Math.min(1,(v-a)/(b-a)));return t*t*(3-2*t);};
|
|
12
|
+
const noise=(x,y)=>{
|
|
13
|
+
const ix=Math.floor(x),iy=Math.floor(y),tx=smooth(0,1,x-ix),ty=smooth(0,1,y-iy);
|
|
14
|
+
const h=(a,b)=>hash((a%8+8)%8,(b%8+8)%8,9);
|
|
15
|
+
return (h(ix,iy)*(1-tx)+h(ix+1,iy)*tx)*(1-ty)+(h(ix,iy+1)*(1-tx)+h(ix+1,iy+1)*tx)*ty;
|
|
16
|
+
};
|
|
17
|
+
for(let y=0;y<n;y++)for(let x=0;x<n;x++) {
|
|
18
|
+
const px=x/n*cells,py=y/n*cells,ix=Math.floor(px),iy=Math.floor(py);
|
|
19
|
+
let d1=Infinity,d2=Infinity,radius=.4;
|
|
20
|
+
for(let dy=-1;dy<=1;dy++)for(let dx=-1;dx<=1;dx++) {
|
|
21
|
+
const sx=ix+dx,sy=iy+dy,hx=wrap(sx),hy=wrap(sy);
|
|
22
|
+
const ox=sx+.15+.7*hash(hx,hy),oy=sy+.15+.7*hash(hx,hy,1);
|
|
23
|
+
const d=Math.hypot(px-ox,py-oy);
|
|
24
|
+
if(d<d1){d2=d1;d1=d;radius=.23+.18*hash(hx,hy,2);}else if(d<d2)d2=d;
|
|
25
|
+
}
|
|
26
|
+
const rim=1-smooth(.025,.11,d2-d1);
|
|
27
|
+
const disk=1-smooth(radius-.04,radius+.025,d1);
|
|
28
|
+
const i=(y*n+x)*4;
|
|
29
|
+
data[i]=Math.round(255*(.68+.32*rim));
|
|
30
|
+
data[i+1]=Math.round(255*disk);
|
|
31
|
+
data[i+2]=Math.round(255*noise(x/n*8,y/n*8));
|
|
32
|
+
// A second, coarser cellular field stores only narrow shared borders.
|
|
33
|
+
// These are the connected foam strands around clear-water pockets.
|
|
34
|
+
// Two periodic shears curve the cell walls without collapsing their
|
|
35
|
+
// area. Straight Voronoi bisectors otherwise read as cracked ice.
|
|
36
|
+
const mx=x/n*6+.20*Math.sin(y/n*Math.PI*6)+.07*Math.sin(y/n*Math.PI*14+1.2);
|
|
37
|
+
const my=y/n*6+.20*Math.sin(mx/6*Math.PI*6+.8)+.08*Math.sin(mx/6*Math.PI*10);
|
|
38
|
+
const bx=Math.floor(mx),by=Math.floor(my);
|
|
39
|
+
let m1=Infinity,m2=Infinity;
|
|
40
|
+
for(let dy=-1;dy<=1;dy++)for(let dx=-1;dx<=1;dx++) {
|
|
41
|
+
const sx=bx+dx,sy=by+dy,hx=(sx%6+6)%6,hy=(sy%6+6)%6;
|
|
42
|
+
const ox=sx+.15+.7*hash(hx,hy,31),oy=sy+.15+.7*hash(hx,hy,37);
|
|
43
|
+
const distance=Math.hypot(mx-ox,my-oy);
|
|
44
|
+
if(distance<m1){m2=m1;m1=distance;}else if(distance<m2)m2=distance;
|
|
45
|
+
}
|
|
46
|
+
const gap=m2-m1,width=.045+.11*noise(x/n*8,y/n*8);
|
|
47
|
+
const core=1-smooth(.012,.04,gap);
|
|
48
|
+
const fringe=(1-smooth(.014,width,gap))*(.45+.55*disk);
|
|
49
|
+
data[i+3]=Math.round(255*Math.max(core,fringe));
|
|
50
|
+
}
|
|
51
|
+
cached=new THREE.DataTexture(data,n,n,THREE.RGBAFormat);
|
|
52
|
+
cached.name='WaterFoamBubbles';
|
|
53
|
+
cached.wrapS=cached.wrapT=THREE.RepeatWrapping;
|
|
54
|
+
cached.minFilter=THREE.LinearMipmapLinearFilter;cached.magFilter=THREE.LinearFilter;
|
|
55
|
+
cached.generateMipmaps=true;cached.needsUpdate=true;
|
|
56
|
+
return cached;
|
|
57
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// Conservative finite-volume Saint-Venant solver for finite authored water.
|
|
2
|
+
// Hydrostatic reconstruction + HLL flux (Audusse et al., 2004).
|
|
3
|
+
// h [m], qx/qz [m²/s]; no authored run-up curve or hidden water source.
|
|
4
|
+
const G = 9.81;
|
|
5
|
+
const DRY = 1e-5;
|
|
6
|
+
export class WaterHydrodynamics {
|
|
7
|
+
constructor({ columns = 96, rows = 96, width = 60, depth = 60, centerX = 0, centerZ = 0, waterLevel = 0, bedHeight = () => -2, boundary = null, friction = 0.018 } = {}) {
|
|
8
|
+
if (!Number.isInteger(columns) || !Number.isInteger(rows) || columns < 3 || rows < 3 || !Number.isFinite(width) || !Number.isFinite(depth) || width <= 0 || depth <= 0)
|
|
9
|
+
throw new RangeError('Invalid water grid');
|
|
10
|
+
Object.assign(this, { columns, rows, width, depth, centerX, centerZ, waterLevel, boundary, friction });
|
|
11
|
+
this.dx = width / (columns - 1);
|
|
12
|
+
this.dz = depth / (rows - 1);
|
|
13
|
+
this.minX = centerX - width / 2;
|
|
14
|
+
this.minZ = centerZ - depth / 2;
|
|
15
|
+
this.length = columns * rows;
|
|
16
|
+
for (const key of ['bed', 'h', 'qx', 'qz', 'dh', 'dqx', 'dqz', 'foam', 'nextFoam', 'dFoamMass', 'moisture']) {
|
|
17
|
+
this[key] = new Float64Array(this.length);
|
|
18
|
+
}
|
|
19
|
+
for (let z = 0; z < rows; z++)
|
|
20
|
+
for (let x = 0; x < columns; x++) {
|
|
21
|
+
const i = z * columns + x;
|
|
22
|
+
this.bed[i] = bedHeight(this.minX + x * this.dx, this.minZ + z * this.dz);
|
|
23
|
+
if (!Number.isFinite(this.bed[i]))
|
|
24
|
+
throw new TypeError('Non-finite water bed');
|
|
25
|
+
this.h[i] = Math.max(0, waterLevel - this.bed[i]);
|
|
26
|
+
}
|
|
27
|
+
this.time = 0;
|
|
28
|
+
this.boundaryVolume = 0;
|
|
29
|
+
this.roundoffVolume = 0;
|
|
30
|
+
this.initialVolume = this.volume();
|
|
31
|
+
this.steps = 0;
|
|
32
|
+
this.foamLifetime = 4;
|
|
33
|
+
this.foamResidueLifetime = 10;
|
|
34
|
+
this.ghost = { h: 0, qx: 0, qz: 0, bed: 0 };
|
|
35
|
+
}
|
|
36
|
+
volume() { return this.h.reduce((a, h) => a + h, 0) * this.dx * this.dz; }
|
|
37
|
+
diagnostics() {
|
|
38
|
+
return { time: this.time, steps: this.steps, volume: this.volume(), boundaryVolume: this.boundaryVolume,
|
|
39
|
+
massError: this.volume() - this.initialVolume - this.boundaryVolume - this.roundoffVolume,
|
|
40
|
+
roundoffVolume: this.roundoffVolume, minDepth: this.h.reduce((a, h) => Math.min(a, h), Infinity) };
|
|
41
|
+
}
|
|
42
|
+
// A momentum impulse moves water without injecting mass. Direction is
|
|
43
|
+
// radial for a splash; wakes can supply an explicit horizontal direction.
|
|
44
|
+
impulse(x, z, strength = 0.2, radius = 0.8, direction = null) {
|
|
45
|
+
const r = Math.max(radius, Math.min(this.dx, this.dz));
|
|
46
|
+
for (let iz = Math.max(0, Math.floor((z - r * 3 - this.minZ) / this.dz)); iz <= Math.min(this.rows - 1, Math.ceil((z + r * 3 - this.minZ) / this.dz)); iz++) {
|
|
47
|
+
for (let ix = Math.max(0, Math.floor((x - r * 3 - this.minX) / this.dx)); ix <= Math.min(this.columns - 1, Math.ceil((x + r * 3 - this.minX) / this.dx)); ix++) {
|
|
48
|
+
const i = iz * this.columns + ix, rx = this.minX + ix * this.dx - x, rz = this.minZ + iz * this.dz - z;
|
|
49
|
+
const d = Math.hypot(rx, rz), weight = Math.exp(-d * d / (2 * r * r)) * strength;
|
|
50
|
+
this.qx[i] += this.h[i] * weight * (direction?.[0] ?? rx / Math.max(d, r * 0.1));
|
|
51
|
+
this.qz[i] += this.h[i] * weight * (direction?.[1] ?? rz / Math.max(d, r * 0.1));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
face(left, right, axis, dt) {
|
|
56
|
+
const { h, qx, qz, bed, dh, dqx, dqz } = this;
|
|
57
|
+
const ghost = this.ghost;
|
|
58
|
+
const hL = left < 0 ? ghost.h : h[left], hR = right < 0 ? ghost.h : h[right];
|
|
59
|
+
const bL = left < 0 ? ghost.bed : bed[left], bR = right < 0 ? ghost.bed : bed[right];
|
|
60
|
+
const uL = hL > DRY ? (left < 0 ? ghost.qx : qx[left]) / hL : 0;
|
|
61
|
+
const vL = hL > DRY ? (left < 0 ? ghost.qz : qz[left]) / hL : 0;
|
|
62
|
+
const uR = hR > DRY ? (right < 0 ? ghost.qx : qx[right]) / hR : 0;
|
|
63
|
+
const vR = hR > DRY ? (right < 0 ? ghost.qz : qz[right]) / hR : 0;
|
|
64
|
+
const crest = Math.max(bL, bR);
|
|
65
|
+
const a = Math.max(0, hL + bL - crest), b = Math.max(0, hR + bR - crest);
|
|
66
|
+
const normalL = axis === 0 ? uL : vL, normalR = axis === 0 ? uR : vR;
|
|
67
|
+
// Two-sided HLL wave speeds retain an advancing bore more clearly
|
|
68
|
+
// than a single maximum-speed viscosity in every direction.
|
|
69
|
+
const cL = Math.sqrt(G*a), cR = Math.sqrt(G*b);
|
|
70
|
+
const sL = Math.min(0, normalL-cL, normalR-cR, a<=DRY ? normalR-2*cR : Infinity);
|
|
71
|
+
const sR = Math.max(0, normalL+cL, normalR+cR, b<=DRY ? normalL+2*cL : -Infinity);
|
|
72
|
+
const width = Math.max(sR-sL, 1e-8);
|
|
73
|
+
const flux = (fL,fR,delta) => (sR*fL-sL*fR+sL*sR*delta)/width;
|
|
74
|
+
const mass = flux(a*normalL,b*normalR,b-a);
|
|
75
|
+
// Transport aeration with the very same water flux. Sampling a
|
|
76
|
+
// concentration backwards into dry cells diluted the leading swash
|
|
77
|
+
// foam to zero even while aerated water was entering those cells.
|
|
78
|
+
const foamFlux = mass * (mass >= 0 ? (left < 0 ? 0 : this.foam[left]) : (right < 0 ? 0 : this.foam[right]));
|
|
79
|
+
const fx = flux(a*normalL*uL+(axis===0?G*a*a*0.5:0), b*normalR*uR+(axis===0?G*b*b*0.5:0),b*uR-a*uL);
|
|
80
|
+
const fz = flux(a*normalL*vL+(axis===1?G*a*a*0.5:0), b*normalR*vR+(axis===1?G*b*b*0.5:0),b*vR-a*vL);
|
|
81
|
+
const scale = dt / (axis === 0 ? this.dx : this.dz);
|
|
82
|
+
if (left >= 0) {
|
|
83
|
+
dh[left] -= scale * mass;
|
|
84
|
+
this.dFoamMass[left] -= scale * foamFlux;
|
|
85
|
+
dqx[left] -= scale * (fx + (axis === 0 ? G * (hL * hL - a * a) * 0.5 : 0));
|
|
86
|
+
dqz[left] -= scale * (fz + (axis === 1 ? G * (hL * hL - a * a) * 0.5 : 0));
|
|
87
|
+
}
|
|
88
|
+
else
|
|
89
|
+
this.boundaryVolume += mass * dt * (axis === 0 ? this.dz : this.dx);
|
|
90
|
+
if (right >= 0) {
|
|
91
|
+
dh[right] += scale * mass;
|
|
92
|
+
this.dFoamMass[right] += scale * foamFlux;
|
|
93
|
+
dqx[right] += scale * (fx + (axis === 0 ? G * (hR * hR - b * b) * 0.5 : 0));
|
|
94
|
+
dqz[right] += scale * (fz + (axis === 1 ? G * (hR * hR - b * b) * 0.5 : 0));
|
|
95
|
+
}
|
|
96
|
+
else
|
|
97
|
+
this.boundaryVolume -= mass * dt * (axis === 0 ? this.dz : this.dx);
|
|
98
|
+
}
|
|
99
|
+
boundaryFace(i, side, axis, low, dt) {
|
|
100
|
+
const { ghost } = this;
|
|
101
|
+
ghost.h = this.h[i];
|
|
102
|
+
ghost.qx = this.qx[i];
|
|
103
|
+
ghost.qz = this.qz[i];
|
|
104
|
+
ghost.bed = this.bed[i];
|
|
105
|
+
const ix = i % this.columns, iz = Math.floor(i / this.columns);
|
|
106
|
+
const handled = this.boundary?.(side, this.minX + ix * this.dx, this.minZ + iz * this.dz, this.time, ghost);
|
|
107
|
+
if (!handled) {
|
|
108
|
+
if (axis === 0)
|
|
109
|
+
ghost.qx *= -1;
|
|
110
|
+
else
|
|
111
|
+
ghost.qz *= -1;
|
|
112
|
+
}
|
|
113
|
+
ghost.h = Math.max(0, ghost.h);
|
|
114
|
+
this.face(low ? -1 : i, low ? i : -1, axis, dt);
|
|
115
|
+
}
|
|
116
|
+
advance(duration) {
|
|
117
|
+
let remaining = Math.max(0, duration);
|
|
118
|
+
while (remaining > 1e-9) {
|
|
119
|
+
let rate = 1;
|
|
120
|
+
for (let i = 0; i < this.length; i++) {
|
|
121
|
+
const h = this.h[i], c = Math.sqrt(G * h);
|
|
122
|
+
rate = Math.max(rate, ((h > DRY ? Math.abs(this.qx[i]) / h : 0) + c) / this.dx + ((h > DRY ? Math.abs(this.qz[i]) / h : 0) + c) / this.dz);
|
|
123
|
+
}
|
|
124
|
+
// Unsplit 2D CFL; include a conservative cap for external forcing.
|
|
125
|
+
const dt = Math.min(remaining, 0.32 / rate, 1 / 120);
|
|
126
|
+
this.step(dt);
|
|
127
|
+
remaining -= dt;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
step(dt) {
|
|
131
|
+
this.dh.fill(0);
|
|
132
|
+
this.dqx.fill(0);
|
|
133
|
+
this.dqz.fill(0);
|
|
134
|
+
this.dFoamMass.fill(0);
|
|
135
|
+
const nx = this.columns, nz = this.rows;
|
|
136
|
+
for (let z = 0; z < nz; z++) {
|
|
137
|
+
for (let x = 0; x < nx - 1; x++)
|
|
138
|
+
this.face(z * nx + x, z * nx + x + 1, 0, dt);
|
|
139
|
+
this.boundaryFace(z * nx, 'west', 0, true, dt);
|
|
140
|
+
this.boundaryFace(z * nx + nx - 1, 'east', 0, false, dt);
|
|
141
|
+
}
|
|
142
|
+
for (let x = 0; x < nx; x++) {
|
|
143
|
+
for (let z = 0; z < nz - 1; z++)
|
|
144
|
+
this.face(z * nx + x, (z + 1) * nx + x, 1, dt);
|
|
145
|
+
this.boundaryFace(x, 'south', 1, true, dt);
|
|
146
|
+
this.boundaryFace((nz - 1) * nx + x, 'north', 1, false, dt);
|
|
147
|
+
}
|
|
148
|
+
for (let i = 0; i < this.length; i++) {
|
|
149
|
+
const next = this.h[i] + this.dh[i];
|
|
150
|
+
const foamMass = this.h[i] * this.foam[i] + this.dFoamMass[i];
|
|
151
|
+
this.nextFoam[i] = next > DRY ? Math.max(0,Math.min(1,foamMass/next)) : this.foam[i];
|
|
152
|
+
if (!Number.isFinite(next) || next < -1e-7)
|
|
153
|
+
throw new Error(`Unstable water depth at ${i}: ${next}`);
|
|
154
|
+
if (next < 0)
|
|
155
|
+
this.roundoffVolume -= next * this.dx * this.dz;
|
|
156
|
+
this.h[i] = Math.max(next, 0);
|
|
157
|
+
this.qx[i] += this.dqx[i];
|
|
158
|
+
this.qz[i] += this.dqz[i];
|
|
159
|
+
if (this.h[i] <= DRY) {
|
|
160
|
+
this.qx[i] = 0;
|
|
161
|
+
this.qz[i] = 0;
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
// Semi-implicit Manning bed friction removes energy, never water.
|
|
165
|
+
const damping = 1 + dt * G * this.friction ** 2 * Math.hypot(this.qx[i], this.qz[i]) / Math.max(this.h[i] ** (7 / 3), 1e-7);
|
|
166
|
+
this.qx[i] /= damping;
|
|
167
|
+
this.qz[i] /= damping;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Foam is a passive visual tracer advected by the solved velocity.
|
|
171
|
+
// Only converging, moving shallow water generates new aeration.
|
|
172
|
+
for (let z = 0; z < nz; z++)
|
|
173
|
+
for (let x = 0; x < nx; x++) {
|
|
174
|
+
const i = z * nx + x, h = this.h[i], u = h > DRY ? this.qx[i] / h : 0, v = h > DRY ? this.qz[i] / h : 0;
|
|
175
|
+
const left = i - (x > 0 ? 1 : 0), right = i + (x < nx - 1 ? 1 : 0), down = i - (z > 0 ? nx : 0), up = i + (z < nz - 1 ? nx : 0);
|
|
176
|
+
const du = ((this.h[right] > DRY ? this.qx[right] / this.h[right] : 0) - (this.h[left] > DRY ? this.qx[left] / this.h[left] : 0)) / (2 * this.dx);
|
|
177
|
+
const dv = ((this.h[up] > DRY ? this.qz[up] / this.h[up] : 0) - (this.h[down] > DRY ? this.qz[down] / this.h[down] : 0)) / (2 * this.dz);
|
|
178
|
+
// Ordinary orbital convergence is not breaking: without a
|
|
179
|
+
// shallow-flow gate every incoming swell filled with foam.
|
|
180
|
+
// Aerate rapid, converging bores and wakes, then let their
|
|
181
|
+
// tracer travel into slower backwash and decay there.
|
|
182
|
+
const speed = Math.hypot(u, v);
|
|
183
|
+
const froude = speed / Math.sqrt(G * Math.max(h, 0.025));
|
|
184
|
+
const breaking = Math.max(0, Math.min(1, (froude - 0.45) / 0.45));
|
|
185
|
+
const source = Math.max(0, -du - dv - 0.15) * breaking * Math.min(1, speed) * Math.min(1, h / 0.025);
|
|
186
|
+
const transported = this.nextFoam[i];
|
|
187
|
+
const lifetime = h > 0.002 ? this.foamLifetime : this.foamResidueLifetime;
|
|
188
|
+
this.nextFoam[i] = Math.min(1, transported * Math.exp(-dt / Math.max(lifetime, 0.25)) + source * dt * 1.5);
|
|
189
|
+
this.moisture[i] = h > 0.001 ? 1 : this.moisture[i] * Math.exp(-dt / 16);
|
|
190
|
+
}
|
|
191
|
+
[this.foam, this.nextFoam] = [this.nextFoam, this.foam];
|
|
192
|
+
this.time += dt;
|
|
193
|
+
this.steps++;
|
|
194
|
+
}
|
|
195
|
+
sampleArray(array, x, z) {
|
|
196
|
+
x = Math.max(0, Math.min(this.columns - 1, x));
|
|
197
|
+
z = Math.max(0, Math.min(this.rows - 1, z));
|
|
198
|
+
const ix = Math.min(this.columns - 2, Math.floor(x)), iz = Math.min(this.rows - 2, Math.floor(z)), tx = x - ix, tz = z - iz, i = iz * this.columns + ix;
|
|
199
|
+
return (array[i] * (1 - tx) + array[i + 1] * tx) * (1 - tz) + (array[i + this.columns] * (1 - tx) + array[i + this.columns + 1] * tx) * tz;
|
|
200
|
+
}
|
|
201
|
+
sample(x, z, out = {}) {
|
|
202
|
+
const gx = (x - this.minX) / this.dx, gz = (z - this.minZ) / this.dz;
|
|
203
|
+
out.depth = this.sampleArray(this.h, gx, gz);
|
|
204
|
+
out.height = this.sampleArray(this.bed, gx, gz) + out.depth;
|
|
205
|
+
out.velocityX = out.depth > DRY ? this.sampleArray(this.qx, gx, gz) / out.depth : 0;
|
|
206
|
+
out.velocityZ = out.depth > DRY ? this.sampleArray(this.qz, gx, gz) / out.depth : 0;
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -229,6 +229,7 @@ export function createWaterMaterial(options = {}) {
|
|
|
229
229
|
detailOctaves: qualityDefines.WATER_DETAIL_OCTAVES,
|
|
230
230
|
foamOctaves: qualityDefines.WATER_FOAM_OCTAVES,
|
|
231
231
|
shoaling: Boolean(options.shoaling),
|
|
232
|
+
dynamics: Boolean(options.dynamics),
|
|
232
233
|
});
|
|
233
234
|
return applyWaterSettingsToMaterial(
|
|
234
235
|
material,
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
// Visual continuation around a finite coastal solve. Boundary vertices are
|
|
4
|
+
// shared exactly; incoming wave phases continue offshore as a heightfield.
|
|
5
|
+
// This adds distant context, not extra simulated volume or interactive cells.
|
|
6
|
+
export class WaterRenderExtension {
|
|
7
|
+
constructor(dynamics, extent) {
|
|
8
|
+
this.dynamics = dynamics;
|
|
9
|
+
const d = dynamics;
|
|
10
|
+
const axis = (center, width, count) => {
|
|
11
|
+
const low = center - width / 2, high = center + width / 2;
|
|
12
|
+
const inner = Array.from({length:count},(_,i)=>low+i*width/(count-1));
|
|
13
|
+
const distances = [1, 3, 7, 15, 31, 63, 127, extent / 2];
|
|
14
|
+
return [...distances.map(v=>low-v).reverse(), ...inner, ...distances.map(v=>high+v)];
|
|
15
|
+
};
|
|
16
|
+
this.xs = axis(d.x, d.surface.width, d.columns);
|
|
17
|
+
this.zs = axis(d.z, d.surface.depth, d.rows);
|
|
18
|
+
const nx=this.xs.length, nz=this.zs.length;
|
|
19
|
+
const positions=[], indices=[];
|
|
20
|
+
for(const z of this.zs)for(const x of this.xs) positions.push(x-d.x,0,z-d.z);
|
|
21
|
+
for(let z=0;z<nz-1;z++)for(let x=0;x<nx-1;x++) {
|
|
22
|
+
// Omit every face already covered by the physical grid.
|
|
23
|
+
if(x>=8&&x<8+d.columns-1&&z>=8&&z<8+d.rows-1) continue;
|
|
24
|
+
const a=z*nx+x,b=a+1,c=a+nx,e=c+1;
|
|
25
|
+
indices.push(a,c,b,b,c,e);
|
|
26
|
+
}
|
|
27
|
+
this.geometry = new THREE.BufferGeometry();
|
|
28
|
+
this.geometry.setAttribute('position',new THREE.Float32BufferAttribute(positions,3));
|
|
29
|
+
this.geometry.setIndex(indices);
|
|
30
|
+
this.geometry.computeVertexNormals();
|
|
31
|
+
this.states = new Float32Array(nx*nz*4);
|
|
32
|
+
this.normals = new Float32Array(nx*nz*3);
|
|
33
|
+
this.wetDistances = new Float32Array(nx*nz);
|
|
34
|
+
this.geometry.setAttribute('aWaterState',new THREE.BufferAttribute(this.states,4).setUsage(THREE.DynamicDrawUsage));
|
|
35
|
+
this.geometry.setAttribute('aWaterNormal',new THREE.BufferAttribute(this.normals,3).setUsage(THREE.DynamicDrawUsage));
|
|
36
|
+
this.geometry.setAttribute('aWaterWetDistance',new THREE.BufferAttribute(this.wetDistances,1).setUsage(THREE.DynamicDrawUsage));
|
|
37
|
+
this.mesh = new THREE.Mesh(this.geometry,d.surface.material);
|
|
38
|
+
this.mesh.name = 'WaterLabCoastalContinuation';
|
|
39
|
+
this.mesh.frustumCulled = false;
|
|
40
|
+
this.mesh.userData.waterExclude = true;
|
|
41
|
+
d.surface.add(this.mesh);
|
|
42
|
+
this.sample = {};
|
|
43
|
+
}
|
|
44
|
+
incoming(x,z,stepX=0,stepZ=0) {
|
|
45
|
+
const d=this.dynamics;
|
|
46
|
+
let h=0;
|
|
47
|
+
for(const w of d.incidentWaves ?? []) {
|
|
48
|
+
const frequency=w.waveNumber*(Math.abs(w.dirX)*stepX+Math.abs(w.dirZ)*stepZ);
|
|
49
|
+
const resolved=1-THREE.MathUtils.smoothstep(frequency,Math.PI*.45,Math.PI*.95);
|
|
50
|
+
h+=resolved*w.amplitude*Math.sin(w.waveNumber*(w.dirX*x+w.dirZ*z)-w.omega*d.time+w.phase);
|
|
51
|
+
}
|
|
52
|
+
return h;
|
|
53
|
+
}
|
|
54
|
+
update() {
|
|
55
|
+
const d=this.dynamics,nx=this.xs.length,nz=this.zs.length;
|
|
56
|
+
this.mesh.material=d.surface.material;
|
|
57
|
+
for(let z=0;z<nz;z++)for(let x=0;x<nx;x++) {
|
|
58
|
+
const wx=this.xs[x],wz=this.zs[z],i=z*nx+x;
|
|
59
|
+
const cx=THREE.MathUtils.clamp((wx-d.minX)/d.dx,0,d.columns-1),ix=Math.min(d.columns-2,Math.floor(cx));
|
|
60
|
+
this.wetDistances[i]=THREE.MathUtils.lerp(d.shoreContour[ix],d.shoreContour[ix+1],cx-ix)-wz;
|
|
61
|
+
if(x>=8&&x<8+d.columns&&z>=8&&z<8+d.rows) {
|
|
62
|
+
const source=((z-8)*d.columns+x-8)*4;
|
|
63
|
+
this.states.set(d.state.subarray(source,source+4),i*4);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const bx=THREE.MathUtils.clamp(wx,d.minX,d.minX+d.surface.width);
|
|
67
|
+
const bz=THREE.MathUtils.clamp(wz,d.minZ,d.minZ+d.surface.depth);
|
|
68
|
+
const distance=Math.hypot(wx-bx,wz-bz);
|
|
69
|
+
const boundary=d.sample(bx,bz,this.sample);
|
|
70
|
+
const bed=d.bed(wx,wz), restDepth=Math.max(0,d.level-bed);
|
|
71
|
+
const stepX=(this.xs[Math.min(nx-1,x+1)]-this.xs[Math.max(0,x-1)])/2;
|
|
72
|
+
const stepZ=(this.zs[Math.min(nz-1,z+1)]-this.zs[Math.max(0,z-1)])/2;
|
|
73
|
+
const incoming=this.incoming(wx,wz,stepX,stepZ);
|
|
74
|
+
// Match the solved boundary exactly, then smoothly remove its local
|
|
75
|
+
// correction as incoming waves extend into the distant context.
|
|
76
|
+
const correction=(boundary.height-d.level-this.incoming(bx,bz))*Math.exp(-((distance/18)**2));
|
|
77
|
+
const allowance=0.35*restDepth;
|
|
78
|
+
const farHeight=d.level+(allowance>1e-5?allowance*Math.tanh((incoming+correction)/allowance):0);
|
|
79
|
+
// Continue the measured shore profile alongshore. Fading it to a
|
|
80
|
+
// rest-depth wave offshore formula on dry sand cut the run-up off at
|
|
81
|
+
// the two side boundaries. Only deep context tends to that formula.
|
|
82
|
+
const blend=THREE.MathUtils.smoothstep(distance,0,8)*THREE.MathUtils.smoothstep(restDepth,.5,2.5);
|
|
83
|
+
const height=THREE.MathUtils.lerp(boundary.height,farHeight,blend);
|
|
84
|
+
this.states[i*4]=Math.max(bed,height);
|
|
85
|
+
this.states[i*4+1]=Math.max(0,height-bed);
|
|
86
|
+
this.states[i*4+2]=boundary.velocityX;
|
|
87
|
+
this.states[i*4+3]=boundary.velocityZ;
|
|
88
|
+
}
|
|
89
|
+
for(let z=0;z<nz;z++)for(let x=0;x<nx;x++) {
|
|
90
|
+
const i=z*nx+x, left=Math.max(0,x-1),right=Math.min(nx-1,x+1),down=Math.max(0,z-1),up=Math.min(nz-1,z+1);
|
|
91
|
+
const gx=(this.states[(z*nx+right)*4]-this.states[(z*nx+left)*4])/(this.xs[right]-this.xs[left]);
|
|
92
|
+
const gz=(this.states[(up*nx+x)*4]-this.states[(down*nx+x)*4])/(this.zs[up]-this.zs[down]);
|
|
93
|
+
const inv=1/Math.hypot(gx,1,gz);
|
|
94
|
+
this.normals[i*3]=-gx*inv;this.normals[i*3+1]=inv;this.normals[i*3+2]=-gz*inv;
|
|
95
|
+
// At the seam also match the original displayed normal, so there is
|
|
96
|
+
// no abrupt reflection line despite different outer tessellation.
|
|
97
|
+
if(x>=8&&x<8+d.columns&&z>=8&&z<8+d.rows) {
|
|
98
|
+
const source=((z-8)*d.columns+x-8)*3;
|
|
99
|
+
this.normals.set(d.normals.subarray(source,source+3),i*3);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
this.geometry.attributes.aWaterState.needsUpdate=true;
|
|
103
|
+
this.geometry.attributes.aWaterNormal.needsUpdate=true;
|
|
104
|
+
this.geometry.attributes.aWaterWetDistance.needsUpdate=true;
|
|
105
|
+
}
|
|
106
|
+
dispose() {this.mesh.removeFromParent();this.geometry.dispose();}
|
|
107
|
+
}
|
|
@@ -408,7 +408,14 @@ export class WaterScenePasses {
|
|
|
408
408
|
this.ensureGrabTarget(renderer);
|
|
409
409
|
this.scratch.waterWorldPosition.setFromMatrixPosition(waterMesh.matrixWorld);
|
|
410
410
|
camera.getWorldPosition(this.scratch.cameraPosition);
|
|
411
|
-
const
|
|
411
|
+
const cameraBelowUniform = waterMesh.material?.uniforms?.uCameraBelow;
|
|
412
|
+
// Standalone meshes using WaterScenePasses may not run WaterSurface's
|
|
413
|
+
// column update; retain their original rest-plane fallback.
|
|
414
|
+
const hasSurfaceColumn = typeof waterMesh.getHeightAt === 'function'
|
|
415
|
+
&& typeof waterMesh.containsPoint === 'function';
|
|
416
|
+
const cameraBelow = hasSurfaceColumn && cameraBelowUniform
|
|
417
|
+
? cameraBelowUniform.value > 0.5
|
|
418
|
+
: this.scratch.cameraPosition.y < this.scratch.waterWorldPosition.y;
|
|
412
419
|
// Above water the grab sees the submerged scene. Below water it becomes a
|
|
413
420
|
// same-pose transmission camera clipped to the air side of the surface,
|
|
414
421
|
// so clouds and above-water objects are genuinely visible through it.
|
|
@@ -459,13 +466,13 @@ export class WaterScenePasses {
|
|
|
459
466
|
}
|
|
460
467
|
}
|
|
461
468
|
|
|
462
|
-
applyWaterClipPlane(renderer, camera, waterY) {
|
|
469
|
+
applyWaterClipPlane(renderer, camera, waterY, side = 1) {
|
|
463
470
|
// Oblique near-plane clipping (Lengyel), matching THREE.Reflector's
|
|
464
471
|
// coordinate-system-aware construction. The positive side of this plane
|
|
465
472
|
// is the air volume, even when the virtual camera itself is underwater.
|
|
466
473
|
const { plane, clipPlane, q } = this.scratch;
|
|
467
|
-
plane.normal.set(0,
|
|
468
|
-
plane.constant = -waterY;
|
|
474
|
+
plane.normal.set(0, side, 0);
|
|
475
|
+
plane.constant = -waterY * side;
|
|
469
476
|
plane.applyMatrix4(camera.matrixWorldInverse);
|
|
470
477
|
clipPlane.set(plane.normal.x, plane.normal.y, plane.normal.z, plane.constant);
|
|
471
478
|
|
|
@@ -508,7 +515,8 @@ export class WaterScenePasses {
|
|
|
508
515
|
updateReflectionCamera(renderer, camera, waterY) {
|
|
509
516
|
const { cameraPosition, cameraDirection, target } = this.scratch;
|
|
510
517
|
camera.getWorldPosition(cameraPosition);
|
|
511
|
-
if (cameraPosition.y
|
|
518
|
+
if (Math.abs(cameraPosition.y - waterY) < 1e-4) return false;
|
|
519
|
+
const reflectionSide = cameraPosition.y < waterY ? -1 : 1;
|
|
512
520
|
|
|
513
521
|
camera.getWorldDirection(cameraDirection);
|
|
514
522
|
target.copy(cameraPosition).add(cameraDirection);
|
|
@@ -528,7 +536,7 @@ export class WaterScenePasses {
|
|
|
528
536
|
reflectionCamera.updateMatrixWorld();
|
|
529
537
|
reflectionCamera.matrixWorldInverse.copy(reflectionCamera.matrixWorld).invert();
|
|
530
538
|
|
|
531
|
-
this.applyWaterClipPlane(renderer, reflectionCamera, waterY);
|
|
539
|
+
this.applyWaterClipPlane(renderer, reflectionCamera, waterY, reflectionSide);
|
|
532
540
|
|
|
533
541
|
this.reflectionMatrix
|
|
534
542
|
.copy(BIAS_MATRIX)
|