anshdadhich commited on
Commit
aa97a34
·
verified ·
1 Parent(s): f0a0de8

Use multidirectional waves official water normals and movable boat

Browse files
Files changed (1) hide show
  1. src/main.js +125 -141
src/main.js CHANGED
@@ -7,38 +7,36 @@ import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
7
  import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
8
 
9
  const container = document.getElementById('app');
10
-
11
  const scene = new THREE.Scene();
12
- scene.background = new THREE.Color(0x86b8cf);
13
- scene.fog = new THREE.FogExp2(0x86b8cf, 0.0065);
14
 
15
  const camera = new THREE.PerspectiveCamera(55, innerWidth / innerHeight, 0.1, 900);
16
- camera.position.set(0, 15, 38);
17
 
18
  const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });
19
  renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
20
  renderer.setSize(innerWidth, innerHeight);
21
  renderer.toneMapping = THREE.ACESFilmicToneMapping;
22
- renderer.toneMappingExposure = 0.86;
23
  renderer.outputColorSpace = THREE.SRGBColorSpace;
24
  container.appendChild(renderer.domElement);
25
 
26
  const controls = new OrbitControls(camera, renderer.domElement);
27
  controls.enableDamping = true;
28
  controls.maxPolarAngle = Math.PI * 0.49;
29
- controls.minDistance = 12;
30
- controls.maxDistance = 140;
31
- controls.target.set(0, 0.5, 0);
32
-
33
- const sun = new THREE.Vector3(-0.42, 0.72, 0.55).normalize();
34
- scene.add(new THREE.HemisphereLight(0xbfeeff, 0x0b4a5a, 1.8));
35
- const dir = new THREE.DirectionalLight(0xffedbd, 3.8);
36
- dir.position.copy(sun).multiplyScalar(80);
37
  scene.add(dir);
38
 
39
- const sunMesh = new THREE.Mesh(new THREE.CircleGeometry(9, 64), new THREE.MeshBasicMaterial({ color: 0xfff0b6 }));
40
- sunMesh.position.set(-72, 55, -120);
41
- sunMesh.lookAt(camera.position);
42
  scene.add(sunMesh);
43
 
44
  const sky = new THREE.Mesh(
@@ -46,159 +44,145 @@ const sky = new THREE.Mesh(
46
  new THREE.ShaderMaterial({
47
  side: THREE.BackSide,
48
  depthWrite: false,
49
- uniforms: {
50
- topColor: { value: new THREE.Color(0x426f91) },
51
- midColor: { value: new THREE.Color(0x86b8cf) },
52
- bottomColor: { value: new THREE.Color(0xcaa77b) }
53
- },
54
- vertexShader: `varying vec3 vWorld; void main(){ vec4 w = modelMatrix * vec4(position, 1.0); vWorld = normalize(w.xyz); gl_Position = projectionMatrix * viewMatrix * w; }`,
55
- fragmentShader: `varying vec3 vWorld; uniform vec3 topColor, midColor, bottomColor; void main(){ float h = clamp(vWorld.y * .5 + .5, 0.0, 1.0); vec3 c = mix(bottomColor, midColor, smoothstep(0.0, .45, h)); c = mix(c, topColor, smoothstep(.35, 1.0, h)); gl_FragColor = vec4(c, 1.0); }`
56
  })
57
  );
58
  scene.add(sky);
59
 
60
- const textureLoader = new THREE.TextureLoader();
61
- const defaultTexture = new THREE.DataTexture(new Uint8Array([128,128,255,255]), 1, 1, THREE.RGBAFormat);
62
- defaultTexture.needsUpdate = true;
63
- defaultTexture.wrapS = defaultTexture.wrapT = THREE.RepeatWrapping;
 
 
 
 
 
 
 
 
 
 
64
 
65
  const params = {
66
- shallowColor: '#2aa6b8', midColor: '#0f5f8f', deepColor: '#062f59', foamColor: '#e5eee8',
67
- skyReflection: '#7fb4ca', sunColor: '#dcc58f', skyColor: '#86b8cf', fogDensity: 0.0065, exposure: 0.86,
68
- waveScale: 1.0, waveSpeed: 1.0, choppiness: 0.78, smallChop: 0.55, crestSharpness: 0.62,
69
- domainWarp: 0.72, breakingLip: 0.34, lipThreshold: 0.86, splashSpikes: 0.25,
70
- foamAmount: 0.55, foamThreshold: 0.74, foamPatchiness: 0.78, foamPersistence: 0.38, foamDrift: 0.16, foamScale: 0.085,
71
- fresnelStrength: 0.38, roughness: 0.72, sunGlint: 0.38, sparkle: 0.22, turbidity: 0.20,
72
- textureStrength: 0.0, textureScale: 42.0, normalTextureStrength: 0.0,
73
- bloomStrength: 0.12, bloomRadius: 0.32, bloomThreshold: 0.93, showBoat: true, showRocks: true
 
74
  };
75
 
76
- const waterUniforms = {
77
- uTime: { value: 0 }, uSunDir: { value: sun }, uCameraPos: { value: camera.position },
78
- uShallow: { value: new THREE.Color(params.shallowColor) }, uMid: { value: new THREE.Color(params.midColor) }, uDeep: { value: new THREE.Color(params.deepColor) },
79
- uFoam: { value: new THREE.Color(params.foamColor) }, uSky: { value: new THREE.Color(params.skyReflection) }, uSun: { value: new THREE.Color(params.sunColor) },
80
- uWaveScale: { value: params.waveScale }, uWaveSpeed: { value: params.waveSpeed }, uChoppiness: { value: params.choppiness }, uSmallChop: { value: params.smallChop },
81
- uCrestSharpness: { value: params.crestSharpness }, uDomainWarp: { value: params.domainWarp }, uBreakingLip: { value: params.breakingLip }, uLipThreshold: { value: params.lipThreshold }, uSplashSpikes: { value: params.splashSpikes },
82
- uFoamAmount: { value: params.foamAmount }, uFoamThreshold: { value: params.foamThreshold }, uFoamPatchiness: { value: params.foamPatchiness }, uFoamPersistence: { value: params.foamPersistence }, uFoamDrift: { value: params.foamDrift }, uFoamScale: { value: params.foamScale },
83
- uFresnelStrength: { value: params.fresnelStrength }, uRoughness: { value: params.roughness }, uSunGlint: { value: params.sunGlint }, uSparkle: { value: params.sparkle }, uTurbidity: { value: params.turbidity },
84
- uHazeColor: { value: new THREE.Color(params.skyColor) }, uHazeDensity: { value: params.fogDensity },
85
- uDetailMap: { value: defaultTexture }, uTextureStrength: { value: params.textureStrength }, uTextureScale: { value: params.textureScale }, uNormalTextureStrength: { value: params.normalTextureStrength }
86
  };
87
 
88
- const waterGeo = new THREE.PlaneGeometry(720, 720, 420, 420);
89
  waterGeo.rotateX(-Math.PI / 2);
90
 
91
  const waterMat = new THREE.ShaderMaterial({
92
- uniforms: waterUniforms,
93
- depthWrite: true,
94
  vertexShader: /* glsl */`
95
  precision highp float;
96
- uniform float uTime, uWaveScale, uWaveSpeed, uChoppiness, uSmallChop, uCrestSharpness, uDomainWarp, uBreakingLip, uLipThreshold, uSplashSpikes;
97
- varying vec3 vWorldPos; varying vec3 vNormal; varying float vFoam; varying float vHeight; varying float vBreak; varying vec2 vUv;
98
- struct Wave { vec2 dir; float steep; float amp; float freq; float speed; float phase; };
99
- float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); }
100
- float noise(vec2 p){ vec2 i=floor(p), f=fract(p); float a=hash(i), b=hash(i+vec2(1,0)), c=hash(i+vec2(0,1)), d=hash(i+vec2(1,1)); vec2 u=f*f*(3.0-2.0*f); return mix(a,b,u.x)+(c-a)*u.y*(1.0-u.x)+(d-b)*u.x*u.y; }
101
- float fbm(vec2 p){ float v=0.0, a=0.5; mat2 m=mat2(1.62,1.18,-1.18,1.62); for(int i=0;i<4;i++){ v += a * noise(p); p = m * p + 13.1; a *= .5; } return v; }
102
- vec2 warpPos(vec2 xz){ vec2 w = vec2(fbm(xz*.018 + uTime*.025), fbm(xz*.021 - uTime*.019)) - .5; vec2 w2 = vec2(sin(xz.y*.037 + uTime*.21), sin(xz.x*.031 - uTime*.17)); return xz + (w*34.0 + w2*7.0) * uDomainWarp; }
103
- vec3 trochoid(vec3 base, vec2 sampleXZ, Wave w, inout vec3 tangent, inout vec3 binormal, inout float crest, inout float breaker){
104
- vec2 d=normalize(w.dir); float f=dot(d, sampleXZ)*w.freq + uTime*w.speed*uWaveSpeed + w.phase; float s=sin(f), c=cos(f);
105
- float sharp = w.steep * uChoppiness; float amp = w.amp * uWaveScale; float qa = sharp * amp;
106
- tangent += vec3(-d.x*d.x*qa*w.freq*s, d.x*amp*w.freq*c, -d.x*d.y*qa*w.freq*s);
107
- binormal += vec3(-d.x*d.y*qa*w.freq*s, d.y*amp*w.freq*c, -d.y*d.y*qa*w.freq*s);
108
- float localCrest = smoothstep(uCrestSharpness, 1.0, s) * sharp;
109
- crest += localCrest;
110
- breaker += smoothstep(uLipThreshold, 1.0, s) * smoothstep(0.72, 1.25, sharp) * w.amp;
111
  return vec3(d.x*qa*c, amp*s, d.y*qa*c);
112
  }
113
  void main(){
114
- vUv=uv; vec3 p=position; vec2 xz=warpPos(position.xz); vec3 tangent=vec3(1,0,0); vec3 binormal=vec3(0,0,1); float crest=0.0; float breaker=0.0;
115
- p += trochoid(position, xz, Wave(vec2( 1.00, 0.18), .70, 1.42, .109, .58, 0.0), tangent, binormal, crest, breaker);
116
- p += trochoid(position, xz, Wave(vec2( .77, 0.63), .54, .92, .167, .81, 1.7), tangent, binormal, crest, breaker);
117
- p += trochoid(position, xz, Wave(vec2( .23, 0.97), .38, .58, .263, 1.13, 4.2), tangent, binormal, crest, breaker);
118
- p += trochoid(position, xz, Wave(vec2(-.61, 0.79), .32, .36, .401, 1.49, 2.6), tangent, binormal, crest, breaker);
119
- p += trochoid(position, xz, Wave(vec2(-.96, 0.28), .25, .23, .619, 1.94, 5.1), tangent, binormal, crest, breaker);
120
- p += trochoid(position, xz, Wave(vec2(-.20,-0.98), .18, .14, .887, 2.61, 3.3), tangent, binormal, crest, breaker);
121
- float b = clamp(breaker * .45, 0.0, 1.0);
122
- vec2 wind = normalize(vec2(.82,.37));
123
- p.xz += wind * b * uBreakingLip * 1.15;
124
- p.y -= b * b * uBreakingLip * 1.35;
125
- float spikeMask = smoothstep(.72, 1.0, fbm(xz*.42 + uTime*.9));
126
- p.y += b * spikeMask * uSplashSpikes * .65;
127
- float chop = sin(dot(xz, vec2(1.61, .27)) + uTime*2.7) * sin(dot(xz, vec2(-.33,1.37)) - uTime*2.15);
128
- p.y += chop * 0.04 * uSmallChop; crest += smoothstep(.62,1.25,chop)*.12*uSmallChop;
129
- vWorldPos=(modelMatrix*vec4(p,1.0)).xyz; vNormal=normalize(cross(binormal,tangent)); vFoam=clamp(crest*.50 + b*.65,0.0,1.0); vBreak=b; vHeight=p.y; gl_Position=projectionMatrix*viewMatrix*vec4(vWorldPos,1.0);
130
  }`,
131
  fragmentShader: /* glsl */`
132
  precision highp float;
133
- uniform float uTime; uniform vec3 uSunDir, uCameraPos, uShallow, uMid, uDeep, uFoam, uSky, uSun, uHazeColor;
134
- uniform float uFoamAmount,uFoamThreshold,uFoamPatchiness,uFoamPersistence,uFoamDrift,uFoamScale,uFresnelStrength,uRoughness,uSunGlint,uSparkle,uTurbidity,uHazeDensity,uTextureStrength,uTextureScale,uNormalTextureStrength;
135
- uniform sampler2D uDetailMap;
136
  varying vec3 vWorldPos,vNormal; varying float vFoam,vHeight,vBreak; varying vec2 vUv;
137
- float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1,311.7)))*43758.5453123); }
138
- float noise(vec2 p){ vec2 i=floor(p), f=fract(p); float a=hash(i), b=hash(i+vec2(1,0)), c=hash(i+vec2(0,1)), d=hash(i+vec2(1,1)); vec2 u=f*f*(3.0-2.0*f); return mix(a,b,u.x)+(c-a)*u.y*(1.0-u.x)+(d-b)*u.x*u.y; }
139
- float fbm(vec2 p){ float v=0., a=.5; mat2 m=mat2(1.62,1.18,-1.18,1.62); for(int i=0;i<5;i++){ v+=a*noise(p); p=m*p+17.7; a*=.5; } return v; }
140
  void main(){
141
- vec2 texUV = vWorldPos.xz / max(uTextureScale, 0.001) + vec2(uTime*.01, -uTime*.006);
142
- vec3 detail = texture2D(uDetailMap, texUV).rgb;
143
- vec3 N=normalize(vNormal);
144
- float n1=fbm(vWorldPos.xz*.105+vec2(uTime*.055,-uTime*.035)); float n2=fbm(vWorldPos.zx*.235+vec2(-uTime*.14,uTime*.10));
145
- vec2 texN = (detail.rg - .5) * uNormalTextureStrength;
146
- N=normalize(N+vec3((n1-.5)*.18 + texN.x, 0.0, (n2-.5)*.18 + texN.y));
147
- vec3 V=normalize(uCameraPos-vWorldPos); vec3 L=normalize(uSunDir); vec3 H=normalize(V+L);
148
- float fresnel=pow(1.0-max(dot(N,V),0.0),3.0); float facingSun=max(dot(reflect(-L,N),V),0.0); float gloss=mix(24.0,120.0,1.0-uRoughness);
149
- float specBroad=pow(facingSun,gloss)*.24*uSunGlint; float specTight=pow(facingSun,gloss*6.5)*.72*uSunGlint;
150
- float sparkleMask=smoothstep(.86,1.0,fbm(vWorldPos.xz*.83+uTime*.62)); float sparkles=pow(max(dot(N,H),0.0),gloss*2.2)*sparkleMask*uSparkle;
151
- float heightBlend=smoothstep(-1.3,1.9,vHeight); float dist=length(uCameraPos.xz-vWorldPos.xz); float depth=smoothstep(10.0,165.0,dist);
152
- vec3 water=mix(uShallow,uMid,depth); water=mix(water,uDeep,smoothstep(58.0,220.0,dist)); water=mix(water,uShallow,heightBlend*(.16+uTurbidity*.35)); water*=.74+heightBlend*.13+uTurbidity*.18;
153
- water = mix(water, water * (0.65 + detail * 0.7), uTextureStrength);
154
- vec3 reflectedSky=mix(uSky,vec3(.80,.68,.48),smoothstep(.60,1.0,dot(V,L))); vec3 color=mix(water,reflectedSky,fresnel*uFresnelStrength);
155
- vec2 wind=normalize(vec2(.82,.37)); vec2 side=vec2(-wind.y,wind.x); vec2 adv=vWorldPos.xz*uFoamScale+wind*uTime*uFoamDrift;
156
- vec2 cell=vec2(fbm(adv*.73+11.7),fbm(adv*1.31-5.2)); vec2 broken=adv+(cell-.5)*(2.0+5.0*uFoamPatchiness);
157
- float patchesA=fbm(broken*1.15), patchesB=fbm(broken*2.65+vec2(4.1,-7.8)), holes=fbm(broken*5.25+vec2(-13.0,2.4));
158
- float torn=smoothstep(uFoamPatchiness,1.0,patchesA+.55*patchesB-.70*holes); float breaking=smoothstep(uFoamThreshold,1.0,vFoam+heightBlend*.12+vBreak*.45);
159
- float fresh=breaking*torn; float streakCoord=dot(vWorldPos.xz,side)*(uFoamScale*8.5); float along=dot(vWorldPos.xz,wind)*(uFoamScale*1.5)-uTime*uFoamDrift*2.0;
160
- float old=smoothstep(.74,.98,fbm(vec2(along,streakCoord)+cell*1.7))*smoothstep(.08,.52,vFoam)*uFoamPersistence;
161
- float foam=clamp((fresh+old*.38)*uFoamAmount,0.0,1.0); color=mix(color,uFoam,foam*.78);
162
- color+=uSun*(specBroad+specTight+sparkles); color+=vec3(.01,.055,.07)*(1.0-depth);
163
- float fog=1.0-exp(-dist*uHazeDensity); color=mix(color,uHazeColor,clamp(fog,0.0,.72)); gl_FragColor=vec4(color,1.0);
164
  }`
165
  });
 
166
 
167
- const water = new THREE.Mesh(waterGeo, waterMat);
168
- scene.add(water);
169
-
170
- const rocks = [];
171
- function makeRock(x, z, s) { const mesh = new THREE.Mesh(new THREE.DodecahedronGeometry(s, 1), new THREE.MeshStandardMaterial({ color: 0x6a5545, roughness: 0.92 })); mesh.scale.set(1.6, 0.75, 1.1); mesh.position.set(x, -0.45, z); mesh.rotation.set(Math.random(), Math.random(), Math.random()); scene.add(mesh); rocks.push(mesh); }
172
  makeRock(-38,-58,7); makeRock(-29,-65,4.5); makeRock(45,-78,8); makeRock(55,-70,3.7);
173
 
174
  const boat = new THREE.Group();
175
- const hull = new THREE.Mesh(new THREE.BoxGeometry(6.5,1.1,2.4), new THREE.MeshStandardMaterial({ color: 0x5b321e, roughness: .75 })); hull.position.y=1.3; boat.add(hull);
176
- const mast = new THREE.Mesh(new THREE.CylinderGeometry(.06,.09,6,10), new THREE.MeshStandardMaterial({ color: 0x3b2216 })); mast.position.y=4.4; boat.add(mast);
177
- const sail = new THREE.Mesh(new THREE.PlaneGeometry(3.2,4.5), new THREE.MeshStandardMaterial({ color: 0xffe7c7, roughness:.85, side:THREE.DoubleSide })); sail.position.set(.75,4.2,0); sail.rotation.y=Math.PI/2; boat.add(sail);
178
- boat.position.set(13,0,-22); boat.rotation.y=-0.55; scene.add(boat);
179
-
180
- const composer = new EffectComposer(renderer); composer.addPass(new RenderPass(scene,camera));
181
- const bloom = new UnrealBloomPass(new THREE.Vector2(innerWidth, innerHeight), params.bloomStrength, params.bloomRadius, params.bloomThreshold); composer.addPass(bloom); composer.addPass(new OutputPass());
182
-
183
- function setColorUniform(name, value){ waterUniforms[name].value.set(value); }
184
- function syncAtmosphere(){ const c=new THREE.Color(params.skyColor); scene.background=c; scene.fog.color.copy(c); scene.fog.density=params.fogDensity; waterUniforms.uHazeColor.value.copy(c); waterUniforms.uHazeDensity.value=params.fogDensity; renderer.toneMappingExposure=params.exposure; }
185
- function bindUniform(folder, key, min, max, step, label, uniform='u'+key[0].toUpperCase()+key.slice(1)){ folder.add(params,key,min,max,step).name(label).onChange(v=>waterUniforms[uniform].value=v); }
186
-
187
- const gui = new GUI({ title: 'Ocean controls' }); gui.close();
188
- const colors=gui.addFolder('Colors / atmosphere');
189
- colors.addColor(params,'shallowColor').name('shallow teal').onChange(v=>setColorUniform('uShallow',v)); colors.addColor(params,'midColor').name('mid blue').onChange(v=>setColorUniform('uMid',v)); colors.addColor(params,'deepColor').name('deep blue').onChange(v=>setColorUniform('uDeep',v)); colors.addColor(params,'foamColor').name('foam off-white').onChange(v=>setColorUniform('uFoam',v)); colors.addColor(params,'skyReflection').name('sky reflection').onChange(v=>setColorUniform('uSky',v)); colors.addColor(params,'sunColor').name('sun tint').onChange(v=>setColorUniform('uSun',v)); colors.addColor(params,'skyColor').name('haze / sky').onChange(syncAtmosphere); colors.add(params,'fogDensity',0,.02,.0001).name('haze density').onChange(syncAtmosphere); colors.add(params,'exposure',.35,1.6,.01).name('exposure').onChange(syncAtmosphere);
190
- const waves=gui.addFolder('Trochoidal waves');
191
- bindUniform(waves,'waveScale',0,2.2,.01,'wave height'); bindUniform(waves,'waveSpeed',0,2.5,.01,'wave speed'); bindUniform(waves,'choppiness',0,1.35,.01,'trochoid sharpness'); bindUniform(waves,'domainWarp',0,1.8,.01,'domain warp / anti-repeat'); bindUniform(waves,'smallChop',0,2,.01,'small chop'); bindUniform(waves,'crestSharpness',.1,.95,.01,'crest trigger'); bindUniform(waves,'breakingLip',0,1.2,.01,'curl/breaking lip'); bindUniform(waves,'lipThreshold',.55,.99,.01,'lip threshold'); bindUniform(waves,'splashSpikes',0,1.2,.01,'spiky splash');
192
- const foamGui=gui.addFolder('Foam');
193
- bindUniform(foamGui,'foamAmount',0,2,.01,'foam amount'); bindUniform(foamGui,'foamThreshold',.2,.98,.01,'break threshold'); bindUniform(foamGui,'foamPatchiness',0,1.3,.01,'patchiness'); bindUniform(foamGui,'foamPersistence',0,1.5,.01,'old foam trails'); bindUniform(foamGui,'foamDrift',0,.8,.01,'foam drift'); bindUniform(foamGui,'foamScale',.015,.22,.001,'foam scale');
194
- const optics=gui.addFolder('Optics / reduce plastic');
195
- bindUniform(optics,'fresnelStrength',0,1.2,.01,'fresnel reflect'); bindUniform(optics,'roughness',.05,1,.01,'roughness'); bindUniform(optics,'sunGlint',0,2,.01,'sun glint'); bindUniform(optics,'sparkle',0,2,.01,'sparkle'); bindUniform(optics,'turbidity',0,1,.01,'turbidity'); bindUniform(optics,'textureStrength',0,1,.01,'texture color strength'); bindUniform(optics,'textureScale',4,150,.5,'texture scale'); bindUniform(optics,'normalTextureStrength',0,1.5,.01,'texture normal strength');
196
- const post=gui.addFolder('Bloom / objects'); post.add(params,'bloomStrength',0,1.5,.01).name('bloom strength').onChange(v=>bloom.strength=v); post.add(params,'bloomRadius',0,1.2,.01).name('bloom radius').onChange(v=>bloom.radius=v); post.add(params,'bloomThreshold',0,1,.01).name('bloom threshold').onChange(v=>bloom.threshold=v); post.add(params,'showBoat').name('show boat').onChange(v=>boat.visible=v); post.add(params,'showRocks').name('show rocks').onChange(v=>rocks.forEach(r=>r.visible=v));
197
-
198
- const uploader = document.createElement('input'); uploader.type='file'; uploader.accept='image/*'; uploader.style.cssText='position:fixed;right:16px;bottom:16px;z-index:5;color:white;background:rgba(4,16,28,.55);padding:10px;border-radius:10px;backdrop-filter:blur(8px);max-width:260px;'; document.body.appendChild(uploader);
199
- uploader.addEventListener('change', e=>{ const file=e.target.files?.[0]; if(!file) return; const url=URL.createObjectURL(file); textureLoader.load(url, tex=>{ tex.wrapS=tex.wrapT=THREE.RepeatWrapping; tex.colorSpace=THREE.SRGBColorSpace; tex.anisotropy=8; waterUniforms.uDetailMap.value=tex; params.textureStrength=Math.max(params.textureStrength,.25); params.normalTextureStrength=Math.max(params.normalTextureStrength,.25); waterUniforms.uTextureStrength.value=params.textureStrength; waterUniforms.uNormalTextureStrength.value=params.normalTextureStrength; URL.revokeObjectURL(url); }); });
200
-
201
- const clock = new THREE.Clock();
202
- function animate(){ requestAnimationFrame(animate); const t=clock.getElapsedTime(); waterUniforms.uTime.value=t; waterUniforms.uCameraPos.value.copy(camera.position); boat.position.y=1.15+Math.sin(t*.78+boat.position.x*.1)*.32+Math.sin(t*1.37)*.10; boat.rotation.z=Math.sin(t*.95)*.045; boat.rotation.x=Math.sin(t*.71)*.035; sunMesh.lookAt(camera.position); controls.update(); composer.render(); }
203
  animate();
204
  addEventListener('resize',()=>{ camera.aspect=innerWidth/innerHeight; camera.updateProjectionMatrix(); renderer.setSize(innerWidth,innerHeight); composer.setSize(innerWidth,innerHeight); bloom.setSize(innerWidth,innerHeight); });
 
7
  import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
8
 
9
  const container = document.getElementById('app');
 
10
  const scene = new THREE.Scene();
11
+ scene.background = new THREE.Color(0x9bc7d8);
12
+ scene.fog = new THREE.FogExp2(0x9bc7d8, 0.0048);
13
 
14
  const camera = new THREE.PerspectiveCamera(55, innerWidth / innerHeight, 0.1, 900);
15
+ camera.position.set(-22, 18, 42);
16
 
17
  const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });
18
  renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
19
  renderer.setSize(innerWidth, innerHeight);
20
  renderer.toneMapping = THREE.ACESFilmicToneMapping;
21
+ renderer.toneMappingExposure = 0.82;
22
  renderer.outputColorSpace = THREE.SRGBColorSpace;
23
  container.appendChild(renderer.domElement);
24
 
25
  const controls = new OrbitControls(camera, renderer.domElement);
26
  controls.enableDamping = true;
27
  controls.maxPolarAngle = Math.PI * 0.49;
28
+ controls.minDistance = 10;
29
+ controls.maxDistance = 150;
30
+ controls.target.set(0, 1.0, 0);
31
+
32
+ const sun = new THREE.Vector3(-0.35, 0.78, 0.52).normalize();
33
+ scene.add(new THREE.HemisphereLight(0xc4efff, 0x173d50, 1.7));
34
+ const dir = new THREE.DirectionalLight(0xffe3aa, 2.6);
35
+ dir.position.copy(sun).multiplyScalar(90);
36
  scene.add(dir);
37
 
38
+ const sunMesh = new THREE.Mesh(new THREE.CircleGeometry(7, 64), new THREE.MeshBasicMaterial({ color: 0xffe6ac }));
39
+ sunMesh.position.set(-75, 58, -135);
 
40
  scene.add(sunMesh);
41
 
42
  const sky = new THREE.Mesh(
 
44
  new THREE.ShaderMaterial({
45
  side: THREE.BackSide,
46
  depthWrite: false,
47
+ uniforms: { topColor:{value:new THREE.Color(0x4f87ad)}, midColor:{value:new THREE.Color(0x9bc7d8)}, bottomColor:{value:new THREE.Color(0xd8b989)} },
48
+ vertexShader: `varying vec3 vWorld; void main(){ vec4 w=modelMatrix*vec4(position,1.0); vWorld=normalize(w.xyz); gl_Position=projectionMatrix*viewMatrix*w; }`,
49
+ fragmentShader: `varying vec3 vWorld; uniform vec3 topColor,midColor,bottomColor; void main(){ float h=clamp(vWorld.y*.5+.5,0.,1.); vec3 c=mix(bottomColor,midColor,smoothstep(0.,.42,h)); c=mix(c,topColor,smoothstep(.35,1.,h)); gl_FragColor=vec4(c,1.); }`
 
 
 
 
50
  })
51
  );
52
  scene.add(sky);
53
 
54
+ const loader = new THREE.TextureLoader();
55
+ function loadNormal(url) {
56
+ const t = loader.load(url);
57
+ t.wrapS = t.wrapT = THREE.RepeatWrapping;
58
+ t.colorSpace = THREE.NoColorSpace;
59
+ t.anisotropy = 8;
60
+ return t;
61
+ }
62
+ // Official three.js water normals, MIT licensed with three.js examples. Good default for this use case.
63
+ const normal0 = loadNormal('https://threejs.org/examples/textures/waternormals.jpg');
64
+ const normal1 = loadNormal('https://threejs.org/examples/textures/water/Water_1_M_Normal.jpg');
65
+ const flatNormal = new THREE.DataTexture(new Uint8Array([128,128,255,255]), 1, 1, THREE.RGBAFormat);
66
+ flatNormal.needsUpdate = true;
67
+ flatNormal.wrapS = flatNormal.wrapT = THREE.RepeatWrapping;
68
 
69
  const params = {
70
+ shallowColor:'#3ca9bd', midColor:'#12658e', deepColor:'#08365f', foamColor:'#e5eee8', skyReflection:'#9bc7d8', sunColor:'#dbc28c', skyColor:'#9bc7d8',
71
+ exposure:0.82, fogDensity:0.0048,
72
+ waveHeight:1.0, waveSpeed:1.0, sharpness:0.72, crossSea:0.75, domainWarp:0.8, smallChop:0.45,
73
+ breakingLip:0.22, lipThreshold:0.88, splashSpikes:0.18,
74
+ foamAmount:0.48, foamThreshold:0.80, foamPatchiness:0.82, foamPersistence:0.30, foamDrift:0.12, foamScale:0.075,
75
+ fresnel:0.58, waterRoughness:0.48, normalStrength:0.52, normalScale0:58, normalScale1:23, sunGlint:0.26, sparkle:0.12, turbidity:0.24,
76
+ textureStrength:0.0, textureScale:42, uploadedNormalStrength:0.0,
77
+ bloomStrength:0.08, bloomRadius:0.25, bloomThreshold:0.94,
78
+ boatSpeed:12, followBoat:false, showBoat:true, showRocks:true
79
  };
80
 
81
+ const U = {
82
+ uTime:{value:0}, uSunDir:{value:sun}, uCameraPos:{value:camera.position},
83
+ uShallow:{value:new THREE.Color(params.shallowColor)}, uMid:{value:new THREE.Color(params.midColor)}, uDeep:{value:new THREE.Color(params.deepColor)}, uFoam:{value:new THREE.Color(params.foamColor)}, uSky:{value:new THREE.Color(params.skyReflection)}, uSun:{value:new THREE.Color(params.sunColor)},
84
+ uWaveHeight:{value:params.waveHeight}, uWaveSpeed:{value:params.waveSpeed}, uSharpness:{value:params.sharpness}, uCrossSea:{value:params.crossSea}, uDomainWarp:{value:params.domainWarp}, uSmallChop:{value:params.smallChop},
85
+ uBreakingLip:{value:params.breakingLip}, uLipThreshold:{value:params.lipThreshold}, uSplashSpikes:{value:params.splashSpikes},
86
+ uFoamAmount:{value:params.foamAmount}, uFoamThreshold:{value:params.foamThreshold}, uFoamPatchiness:{value:params.foamPatchiness}, uFoamPersistence:{value:params.foamPersistence}, uFoamDrift:{value:params.foamDrift}, uFoamScale:{value:params.foamScale},
87
+ uFresnel:{value:params.fresnel}, uWaterRoughness:{value:params.waterRoughness}, uNormalStrength:{value:params.normalStrength}, uNormalScale0:{value:params.normalScale0}, uNormalScale1:{value:params.normalScale1}, uSunGlint:{value:params.sunGlint}, uSparkle:{value:params.sparkle}, uTurbidity:{value:params.turbidity},
88
+ uHazeColor:{value:new THREE.Color(params.skyColor)}, uHazeDensity:{value:params.fogDensity},
89
+ uNormal0:{value:normal0}, uNormal1:{value:normal1}, uUserTex:{value:flatNormal}, uTextureStrength:{value:params.textureStrength}, uTextureScale:{value:params.textureScale}, uUploadedNormalStrength:{value:params.uploadedNormalStrength}
 
90
  };
91
 
92
+ const waterGeo = new THREE.PlaneGeometry(820, 820, 460, 460);
93
  waterGeo.rotateX(-Math.PI / 2);
94
 
95
  const waterMat = new THREE.ShaderMaterial({
96
+ uniforms: U,
 
97
  vertexShader: /* glsl */`
98
  precision highp float;
99
+ uniform float uTime,uWaveHeight,uWaveSpeed,uSharpness,uCrossSea,uDomainWarp,uSmallChop,uBreakingLip,uLipThreshold,uSplashSpikes;
100
+ varying vec3 vWorldPos,vNormal; varying float vFoam,vHeight,vBreak; varying vec2 vUv;
101
+ struct Wave { vec2 dir; float amp; float freq; float speed; float steep; float phase; float weight; };
102
+ float hash(vec2 p){ return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453); }
103
+ float noise(vec2 p){ vec2 i=floor(p),f=fract(p); float a=hash(i),b=hash(i+vec2(1,0)),c=hash(i+vec2(0,1)),d=hash(i+vec2(1,1)); vec2 u=f*f*(3.-2.*f); return mix(a,b,u.x)+(c-a)*u.y*(1.-u.x)+(d-b)*u.x*u.y; }
104
+ float fbm(vec2 p){ float v=0.,a=.5; mat2 m=mat2(1.6,1.2,-1.2,1.6); for(int i=0;i<4;i++){ v+=a*noise(p); p=m*p+11.3; a*=.5; } return v; }
105
+ vec2 warp(vec2 xz){ vec2 large=vec2(fbm(xz*.014+uTime*.018),fbm(xz*.017-uTime*.015))-.5; vec2 slow=vec2(sin(xz.y*.028+uTime*.16),sin(xz.x*.024-uTime*.13)); return xz + (large*48.0 + slow*10.0)*uDomainWarp; }
106
+ vec3 wave(vec2 sampleXZ, Wave w, inout vec3 T, inout vec3 B, inout float crest, inout float br){
107
+ vec2 d=normalize(w.dir); float f=dot(d,sampleXZ)*w.freq + uTime*w.speed*uWaveSpeed + w.phase; float s=sin(f), c=cos(f);
108
+ float amp=w.amp*uWaveHeight*w.weight; float sharp=w.steep*uSharpness*w.weight; float qa=amp*sharp;
109
+ T += vec3(-d.x*d.x*qa*w.freq*s, d.x*amp*w.freq*c, -d.x*d.y*qa*w.freq*s);
110
+ B += vec3(-d.x*d.y*qa*w.freq*s, d.y*amp*w.freq*c, -d.y*d.y*qa*w.freq*s);
111
+ crest += smoothstep(.60,1.0,s)*sharp;
112
+ br += smoothstep(uLipThreshold,1.0,s)*smoothstep(.70,1.05,sharp)*w.weight;
 
113
  return vec3(d.x*qa*c, amp*s, d.y*qa*c);
114
  }
115
  void main(){
116
+ vUv=uv; vec2 sxz=warp(position.xz); vec3 p=position; vec3 T=vec3(1,0,0), B=vec3(0,0,1); float crest=0., br=0.;
117
+ // Three crossing wave families, not a single marching direction.
118
+ p += wave(sxz, Wave(vec2( 1.00, 0.15),1.32,.103,.55,.74,0.0,1.00), T,B,crest,br);
119
+ p += wave(sxz, Wave(vec2( 0.76, 0.65),0.90,.151,.78,.58,1.9,0.90), T,B,crest,br);
120
+ p += wave(sxz, Wave(vec2( 0.18, 0.98),0.52,.247,1.05,.42,4.7,0.75), T,B,crest,br);
121
+ p += wave(sxz, Wave(vec2(-0.67, 0.74),0.42,.337,1.30,.34,2.4,uCrossSea), T,B,crest,br);
122
+ p += wave(sxz, Wave(vec2(-0.98,-0.18),0.28,.517,1.72,.27,5.2,uCrossSea*.8), T,B,crest,br);
123
+ p += wave(sxz, Wave(vec2( 0.45,-0.89),0.17,.821,2.35,.20,3.1,uCrossSea*.6), T,B,crest,br);
124
+ float b=clamp(br*.38,0.,1.); vec2 wind=normalize(vec2(.75,.42));
125
+ p.xz += wind*b*uBreakingLip*.95; p.y -= b*b*uBreakingLip*1.15; p.y += b*smoothstep(.72,1.,fbm(sxz*.45+uTime*.8))*uSplashSpikes*.55;
126
+ float chop=sin(dot(sxz,vec2(1.5,.33))+uTime*2.5)*sin(dot(sxz,vec2(-.48,1.19))-uTime*2.0); p.y += chop*.035*uSmallChop; crest += smoothstep(.65,1.2,chop)*.10*uSmallChop;
127
+ vWorldPos=(modelMatrix*vec4(p,1.)).xyz; vNormal=normalize(cross(B,T)); vFoam=clamp(crest*.46+b*.70,0.,1.); vBreak=b; vHeight=p.y; gl_Position=projectionMatrix*viewMatrix*vec4(vWorldPos,1.);
 
 
 
 
128
  }`,
129
  fragmentShader: /* glsl */`
130
  precision highp float;
131
+ uniform float uTime,uFoamAmount,uFoamThreshold,uFoamPatchiness,uFoamPersistence,uFoamDrift,uFoamScale,uFresnel,uWaterRoughness,uNormalStrength,uNormalScale0,uNormalScale1,uSunGlint,uSparkle,uTurbidity,uHazeDensity,uTextureStrength,uTextureScale,uUploadedNormalStrength;
132
+ uniform vec3 uSunDir,uCameraPos,uShallow,uMid,uDeep,uFoam,uSky,uSun,uHazeColor; uniform sampler2D uNormal0,uNormal1,uUserTex;
 
133
  varying vec3 vWorldPos,vNormal; varying float vFoam,vHeight,vBreak; varying vec2 vUv;
134
+ float hash(vec2 p){ return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453); }
135
+ float noise(vec2 p){ vec2 i=floor(p),f=fract(p); float a=hash(i),b=hash(i+vec2(1,0)),c=hash(i+vec2(0,1)),d=hash(i+vec2(1,1)); vec2 u=f*f*(3.-2.*f); return mix(a,b,u.x)+(c-a)*u.y*(1.-u.x)+(d-b)*u.x*u.y; }
136
+ float fbm(vec2 p){ float v=0.,a=.5; mat2 m=mat2(1.62,1.18,-1.18,1.62); for(int i=0;i<5;i++){ v+=a*noise(p); p=m*p+17.7; a*=.5; } return v; }
137
  void main(){
138
+ vec2 uv0=vWorldPos.xz/uNormalScale0+vec2(uTime*.018,uTime*.006); vec2 uv1=vWorldPos.zx/uNormalScale1+vec2(-uTime*.026,uTime*.014);
139
+ vec3 n0=texture2D(uNormal0,uv0).xyz*2.-1.; vec3 n1=texture2D(uNormal1,uv1).xyz*2.-1.; vec3 user=texture2D(uUserTex,vWorldPos.xz/max(uTextureScale,.001)).xyz;
140
+ vec3 N=normalize(vNormal + vec3(n0.x+n1.x*.55,0.,n0.y+n1.y*.55)*uNormalStrength + vec3(user.r-.5,0.,user.g-.5)*uUploadedNormalStrength);
141
+ vec3 V=normalize(uCameraPos-vWorldPos), L=normalize(uSunDir), H=normalize(V+L);
142
+ float ndv=max(dot(N,V),0.0); float fres=pow(1.-ndv,5.0); float ndl=max(dot(N,L),0.0);
143
+ float gloss=mix(18.,90.,1.-uWaterRoughness); float spec=pow(max(dot(reflect(-L,N),V),0.),gloss)*uSunGlint; float micro=pow(max(dot(N,H),0.),gloss*1.8)*smoothstep(.88,1.,fbm(vWorldPos.xz*.7+uTime*.45))*uSparkle;
144
+ float dist=length(uCameraPos.xz-vWorldPos.xz); float depth=smoothstep(8.,180.,dist); float h=smoothstep(-1.2,2.0,vHeight);
145
+ vec3 water=mix(uShallow,uMid,depth); water=mix(water,uDeep,smoothstep(55.,230.,dist)); water=mix(water,uShallow,h*(.10+uTurbidity*.28)); water*=.70+h*.11+uTurbidity*.20;
146
+ water=mix(water,water*(.72+user*.55),uTextureStrength);
147
+ vec3 skyRefl=mix(uSky,vec3(.78,.67,.48),smoothstep(.55,1.,dot(V,L))); vec3 color=mix(water,skyRefl,fres*uFresnel); color += uSun*(spec+micro)*(0.35+0.65*ndl);
148
+ vec2 wind=normalize(vec2(.75,.42)), side=vec2(-wind.y,wind.x); vec2 adv=vWorldPos.xz*uFoamScale+wind*uTime*uFoamDrift; vec2 cell=vec2(fbm(adv*.71+13.1),fbm(adv*1.27-6.)); vec2 broken=adv+(cell-.5)*(2.2+5.5*uFoamPatchiness);
149
+ float patches=fbm(broken*1.1)+.5*fbm(broken*2.7+4.)-.75*fbm(broken*5.1-8.); float torn=smoothstep(uFoamPatchiness,1.,patches); float breaking=smoothstep(uFoamThreshold,1.,vFoam+h*.1+vBreak*.55); float fresh=breaking*torn;
150
+ float old=smoothstep(.76,.98,fbm(vec2(dot(vWorldPos.xz,wind)*uFoamScale*1.3-uTime*uFoamDrift*2.,dot(vWorldPos.xz,side)*uFoamScale*8.)+cell*1.5))*smoothstep(.08,.48,vFoam)*uFoamPersistence;
151
+ float foam=clamp((fresh+old*.35)*uFoamAmount,0.,1.); color=mix(color,uFoam,foam*.80);
152
+ float fog=1.-exp(-dist*uHazeDensity); color=mix(color,uHazeColor,clamp(fog,0.,.70)); gl_FragColor=vec4(color,1.);
 
 
 
 
 
 
 
 
153
  }`
154
  });
155
+ const water = new THREE.Mesh(waterGeo, waterMat); scene.add(water);
156
 
157
+ const rocks=[]; function makeRock(x,z,s){ const m=new THREE.Mesh(new THREE.DodecahedronGeometry(s,1),new THREE.MeshStandardMaterial({color:0x6a5545,roughness:.92})); m.scale.set(1.6,.75,1.1); m.position.set(x,-.45,z); scene.add(m); rocks.push(m); }
 
 
 
 
158
  makeRock(-38,-58,7); makeRock(-29,-65,4.5); makeRock(45,-78,8); makeRock(55,-70,3.7);
159
 
160
  const boat = new THREE.Group();
161
+ const hull = new THREE.Mesh(new THREE.BoxGeometry(6.5,1.1,2.4), new THREE.MeshStandardMaterial({ color:0x5b321e, roughness:.75 })); hull.position.y=1.3; boat.add(hull);
162
+ const mast = new THREE.Mesh(new THREE.CylinderGeometry(.06,.09,6,10), new THREE.MeshStandardMaterial({ color:0x3b2216 })); mast.position.y=4.4; boat.add(mast);
163
+ const sail = new THREE.Mesh(new THREE.PlaneGeometry(3.2,4.5), new THREE.MeshStandardMaterial({ color:0xffe7c7, roughness:.85, side:THREE.DoubleSide })); sail.position.set(.75,4.2,0); sail.rotation.y=Math.PI/2; boat.add(sail);
164
+ boat.position.set(13,0,-22); boat.rotation.y=-.55; scene.add(boat);
165
+
166
+ const composer=new EffectComposer(renderer); composer.addPass(new RenderPass(scene,camera)); const bloom=new UnrealBloomPass(new THREE.Vector2(innerWidth,innerHeight),params.bloomStrength,params.bloomRadius,params.bloomThreshold); composer.addPass(bloom); composer.addPass(new OutputPass());
167
+
168
+ function setColorUniform(name,value){ U[name].value.set(value); }
169
+ function syncAtmosphere(){ const c=new THREE.Color(params.skyColor); scene.background=c; scene.fog.color.copy(c); scene.fog.density=params.fogDensity; U.uHazeColor.value.copy(c); U.uHazeDensity.value=params.fogDensity; renderer.toneMappingExposure=params.exposure; }
170
+ function bind(folder,key,min,max,step,label,uniform='u'+key[0].toUpperCase()+key.slice(1)){ folder.add(params,key,min,max,step).name(label).onChange(v=>U[uniform].value=v); }
171
+ const gui=new GUI({title:'Ocean controls'}); gui.close();
172
+ const colors=gui.addFolder('Colors / atmosphere'); colors.addColor(params,'shallowColor').onChange(v=>setColorUniform('uShallow',v)); colors.addColor(params,'midColor').onChange(v=>setColorUniform('uMid',v)); colors.addColor(params,'deepColor').onChange(v=>setColorUniform('uDeep',v)); colors.addColor(params,'foamColor').onChange(v=>setColorUniform('uFoam',v)); colors.addColor(params,'skyReflection').onChange(v=>setColorUniform('uSky',v)); colors.addColor(params,'sunColor').onChange(v=>setColorUniform('uSun',v)); colors.addColor(params,'skyColor').onChange(syncAtmosphere); colors.add(params,'fogDensity',0,.02,.0001).onChange(syncAtmosphere); colors.add(params,'exposure',.35,1.6,.01).onChange(syncAtmosphere);
173
+ const waves=gui.addFolder('Waves: multi-direction'); bind(waves,'waveHeight',0,2.2,.01,'wave height'); bind(waves,'waveSpeed',0,2.5,.01,'wave speed'); bind(waves,'sharpness',0,1.35,.01,'trochoid sharpness'); bind(waves,'crossSea',0,1.4,.01,'cross-wave strength'); bind(waves,'domainWarp',0,2,.01,'anti-repeat warp'); bind(waves,'smallChop',0,2,.01,'small chop'); bind(waves,'breakingLip',0,1.2,.01,'curl/breaking lip'); bind(waves,'lipThreshold',.55,.99,.01,'lip threshold'); bind(waves,'splashSpikes',0,1.2,.01,'spiky splash');
174
+ const foam=gui.addFolder('Foam'); bind(foam,'foamAmount',0,2,.01,'amount'); bind(foam,'foamThreshold',.2,.98,.01,'break threshold'); bind(foam,'foamPatchiness',0,1.3,.01,'patchiness'); bind(foam,'foamPersistence',0,1.5,.01,'old trails'); bind(foam,'foamDrift',0,.8,.01,'drift'); bind(foam,'foamScale',.015,.22,.001,'scale');
175
+ const opt=gui.addFolder('Optics / texture'); bind(opt,'fresnel',0,1.4,.01,'fresnel'); bind(opt,'waterRoughness',.05,1,.01,'roughness'); bind(opt,'normalStrength',0,1.5,.01,'water normal strength'); bind(opt,'normalScale0',8,120,.5,'large normal scale'); bind(opt,'normalScale1',4,70,.5,'small normal scale'); bind(opt,'sunGlint',0,2,.01,'sun glint'); bind(opt,'sparkle',0,2,.01,'sparkle'); bind(opt,'turbidity',0,1,.01,'turbidity'); bind(opt,'textureStrength',0,1,.01,'uploaded color strength'); bind(opt,'textureScale',4,150,.5,'uploaded texture scale'); bind(opt,'uploadedNormalStrength',0,1.5,.01,'uploaded normal strength');
176
+ const post=gui.addFolder('Boat / bloom'); post.add(params,'boatSpeed',2,35,.5).name('boat speed'); post.add(params,'followBoat').name('camera follows boat'); post.add(params,'bloomStrength',0,1.5,.01).onChange(v=>bloom.strength=v); post.add(params,'bloomRadius',0,1.2,.01).onChange(v=>bloom.radius=v); post.add(params,'bloomThreshold',0,1,.01).onChange(v=>bloom.threshold=v); post.add(params,'showBoat').onChange(v=>boat.visible=v); post.add(params,'showRocks').onChange(v=>rocks.forEach(r=>r.visible=v));
177
+
178
+ const uploader=document.createElement('input'); uploader.type='file'; uploader.accept='image/*'; uploader.style.cssText='position:fixed;right:16px;bottom:16px;z-index:5;color:white;background:rgba(4,16,28,.55);padding:10px;border-radius:10px;backdrop-filter:blur(8px);max-width:260px;'; document.body.appendChild(uploader);
179
+ uploader.addEventListener('change',e=>{ const file=e.target.files?.[0]; if(!file) return; const url=URL.createObjectURL(file); loader.load(url,tex=>{ tex.wrapS=tex.wrapT=THREE.RepeatWrapping; tex.colorSpace=THREE.SRGBColorSpace; tex.anisotropy=8; U.uUserTex.value=tex; params.textureStrength=Math.max(params.textureStrength,.25); params.uploadedNormalStrength=Math.max(params.uploadedNormalStrength,.18); U.uTextureStrength.value=params.textureStrength; U.uUploadedNormalStrength.value=params.uploadedNormalStrength; URL.revokeObjectURL(url); }); });
180
+
181
+ function cpuWave(x,z,t){ const waves=[[[1,.15],1.32,.103,.55,.74,0,1],[[.76,.65],.90,.151,.78,.58,1.9,.9],[[.18,.98],.52,.247,1.05,.42,4.7,.75],[[-.67,.74],.42,.337,1.30,.34,2.4,params.crossSea],[[-.98,-.18],.28,.517,1.72,.27,5.2,params.crossSea*.8],[[.45,-.89],.17,.821,2.35,.20,3.1,params.crossSea*.6]]; let y=0; for(const w of waves){ const d=w[0],len=Math.hypot(d[0],d[1]); const dx=d[0]/len,dz=d[1]/len; y += w[1]*params.waveHeight*w[6]*Math.sin((dx*x+dz*z)*w[2]+t*w[3]*params.waveSpeed+w[5]); } return y; }
182
+ function updateBoat(t,dt){ const k=keys; const forward=new THREE.Vector3(Math.sin(boat.rotation.y),0,Math.cos(boat.rotation.y)); if(k['KeyA']) boat.rotation.y+=dt*1.4; if(k['KeyD']) boat.rotation.y-=dt*1.4; if(k['KeyW']) boat.position.addScaledVector(forward, params.boatSpeed*dt); if(k['KeyS']) boat.position.addScaledVector(forward, -params.boatSpeed*.65*dt); const x=boat.position.x,z=boat.position.z; const h=cpuWave(x,z,t); const hx=cpuWave(x+1.6,z,t)-cpuWave(x-1.6,z,t); const hz=cpuWave(x,z+1.6,t)-cpuWave(x,z-1.6,t); boat.position.y=1.05+h; boat.rotation.z=THREE.MathUtils.lerp(boat.rotation.z, -hx*.12, .08); boat.rotation.x=THREE.MathUtils.lerp(boat.rotation.x, hz*.10, .08); }
183
+ const keys={}; addEventListener('keydown',e=>keys[e.code]=true); addEventListener('keyup',e=>keys[e.code]=false);
184
+
185
+ const clock=new THREE.Clock(); let elapsed=0;
186
+ function animate(){ requestAnimationFrame(animate); const dt=clock.getDelta(); elapsed+=dt; U.uTime.value=elapsed; U.uCameraPos.value.copy(camera.position); updateBoat(elapsed,dt); if(params.followBoat){ controls.target.lerp(boat.position,.08); } sunMesh.lookAt(camera.position); controls.update(); composer.render(); }
 
 
187
  animate();
188
  addEventListener('resize',()=>{ camera.aspect=innerWidth/innerHeight; camera.updateProjectionMatrix(); renderer.setSize(innerWidth,innerHeight); composer.setSize(innerWidth,innerHeight); bloom.setSize(innerWidth,innerHeight); });