import * as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'; import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'; import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; const container = document.getElementById('app'); const scene = new THREE.Scene(); scene.background = new THREE.Color(0x9bc7d8); scene.fog = new THREE.FogExp2(0x9bc7d8, 0.0048); const camera = new THREE.PerspectiveCamera(55, innerWidth / innerHeight, 0.1, 900); camera.position.set(-22, 18, 42); const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' }); renderer.setPixelRatio(Math.min(devicePixelRatio, 2)); renderer.setSize(innerWidth, innerHeight); renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 0.82; renderer.outputColorSpace = THREE.SRGBColorSpace; container.appendChild(renderer.domElement); const controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.maxPolarAngle = Math.PI * 0.49; controls.minDistance = 10; controls.maxDistance = 150; controls.target.set(0, 1.0, 0); const sun = new THREE.Vector3(-0.35, 0.78, 0.52).normalize(); scene.add(new THREE.HemisphereLight(0xc4efff, 0x173d50, 1.7)); const dir = new THREE.DirectionalLight(0xffe3aa, 2.6); dir.position.copy(sun).multiplyScalar(90); scene.add(dir); const sunMesh = new THREE.Mesh(new THREE.CircleGeometry(7, 64), new THREE.MeshBasicMaterial({ color: 0xffe6ac })); sunMesh.position.set(-75, 58, -135); scene.add(sunMesh); const sky = new THREE.Mesh( new THREE.SphereGeometry(420, 32, 16), new THREE.ShaderMaterial({ side: THREE.BackSide, depthWrite: false, uniforms: { topColor:{value:new THREE.Color(0x4f87ad)}, midColor:{value:new THREE.Color(0x9bc7d8)}, bottomColor:{value:new THREE.Color(0xd8b989)} }, vertexShader: `varying vec3 vWorld; void main(){ vec4 w=modelMatrix*vec4(position,1.0); vWorld=normalize(w.xyz); gl_Position=projectionMatrix*viewMatrix*w; }`, 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.); }` }) ); scene.add(sky); const loader = new THREE.TextureLoader(); function loadNormal(url) { const t = loader.load(url); t.wrapS = t.wrapT = THREE.RepeatWrapping; t.colorSpace = THREE.NoColorSpace; t.anisotropy = 8; return t; } // Official three.js water normals, MIT licensed with three.js examples. Good default for this use case. const normal0 = loadNormal('https://threejs.org/examples/textures/waternormals.jpg'); const normal1 = loadNormal('https://threejs.org/examples/textures/water/Water_1_M_Normal.jpg'); const flatNormal = new THREE.DataTexture(new Uint8Array([128,128,255,255]), 1, 1, THREE.RGBAFormat); flatNormal.needsUpdate = true; flatNormal.wrapS = flatNormal.wrapT = THREE.RepeatWrapping; const params = { shallowColor:'#3ca9bd', midColor:'#12658e', deepColor:'#08365f', foamColor:'#e5eee8', skyReflection:'#9bc7d8', sunColor:'#dbc28c', skyColor:'#9bc7d8', exposure:0.82, fogDensity:0.0048, waveHeight:1.0, waveSpeed:1.0, sharpness:0.72, crossSea:0.75, domainWarp:0.8, smallChop:0.45, breakingLip:0.22, lipThreshold:0.88, splashSpikes:0.18, foamAmount:0.48, foamThreshold:0.80, foamPatchiness:0.82, foamPersistence:0.30, foamDrift:0.12, foamScale:0.075, fresnel:0.58, waterRoughness:0.48, normalStrength:0.52, normalScale0:58, normalScale1:23, sunGlint:0.26, sparkle:0.12, turbidity:0.24, textureStrength:0.0, textureScale:42, uploadedNormalStrength:0.0, bloomStrength:0.08, bloomRadius:0.25, bloomThreshold:0.94, boatSpeed:12, followBoat:false, showBoat:true, showRocks:true }; const U = { uTime:{value:0}, uSunDir:{value:sun}, uCameraPos:{value:camera.position}, 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)}, uWaveHeight:{value:params.waveHeight}, uWaveSpeed:{value:params.waveSpeed}, uSharpness:{value:params.sharpness}, uCrossSea:{value:params.crossSea}, uDomainWarp:{value:params.domainWarp}, uSmallChop:{value:params.smallChop}, uBreakingLip:{value:params.breakingLip}, uLipThreshold:{value:params.lipThreshold}, uSplashSpikes:{value:params.splashSpikes}, uFoamAmount:{value:params.foamAmount}, uFoamThreshold:{value:params.foamThreshold}, uFoamPatchiness:{value:params.foamPatchiness}, uFoamPersistence:{value:params.foamPersistence}, uFoamDrift:{value:params.foamDrift}, uFoamScale:{value:params.foamScale}, 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}, uHazeColor:{value:new THREE.Color(params.skyColor)}, uHazeDensity:{value:params.fogDensity}, uNormal0:{value:normal0}, uNormal1:{value:normal1}, uUserTex:{value:flatNormal}, uTextureStrength:{value:params.textureStrength}, uTextureScale:{value:params.textureScale}, uUploadedNormalStrength:{value:params.uploadedNormalStrength} }; const waterGeo = new THREE.PlaneGeometry(820, 820, 460, 460); waterGeo.rotateX(-Math.PI / 2); const waterMat = new THREE.ShaderMaterial({ uniforms: U, vertexShader: /* glsl */` precision highp float; uniform float uTime,uWaveHeight,uWaveSpeed,uSharpness,uCrossSea,uDomainWarp,uSmallChop,uBreakingLip,uLipThreshold,uSplashSpikes; varying vec3 vWorldPos,vNormal; varying float vFoam,vHeight,vBreak; varying vec2 vUv; struct Wave { vec2 dir; float amp; float freq; float speed; float steep; float phase; float weight; }; float hash(vec2 p){ return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453); } 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; } 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; } 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; } vec3 wave(vec2 sampleXZ, Wave w, inout vec3 T, inout vec3 B, inout float crest, inout float br){ 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); float amp=w.amp*uWaveHeight*w.weight; float sharp=w.steep*uSharpness*w.weight; float qa=amp*sharp; T += vec3(-d.x*d.x*qa*w.freq*s, d.x*amp*w.freq*c, -d.x*d.y*qa*w.freq*s); B += vec3(-d.x*d.y*qa*w.freq*s, d.y*amp*w.freq*c, -d.y*d.y*qa*w.freq*s); crest += smoothstep(.60,1.0,s)*sharp; br += smoothstep(uLipThreshold,1.0,s)*smoothstep(.70,1.05,sharp)*w.weight; return vec3(d.x*qa*c, amp*s, d.y*qa*c); } void main(){ 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.; // Three crossing wave families, not a single marching direction. p += wave(sxz, Wave(vec2( 1.00, 0.15),1.32,.103,.55,.74,0.0,1.00), T,B,crest,br); p += wave(sxz, Wave(vec2( 0.76, 0.65),0.90,.151,.78,.58,1.9,0.90), T,B,crest,br); p += wave(sxz, Wave(vec2( 0.18, 0.98),0.52,.247,1.05,.42,4.7,0.75), T,B,crest,br); p += wave(sxz, Wave(vec2(-0.67, 0.74),0.42,.337,1.30,.34,2.4,uCrossSea), T,B,crest,br); p += wave(sxz, Wave(vec2(-0.98,-0.18),0.28,.517,1.72,.27,5.2,uCrossSea*.8), T,B,crest,br); p += wave(sxz, Wave(vec2( 0.45,-0.89),0.17,.821,2.35,.20,3.1,uCrossSea*.6), T,B,crest,br); float b=clamp(br*.38,0.,1.); vec2 wind=normalize(vec2(.75,.42)); 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; 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; 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.); }`, fragmentShader: /* glsl */` precision highp float; uniform float uTime,uFoamAmount,uFoamThreshold,uFoamPatchiness,uFoamPersistence,uFoamDrift,uFoamScale,uFresnel,uWaterRoughness,uNormalStrength,uNormalScale0,uNormalScale1,uSunGlint,uSparkle,uTurbidity,uHazeDensity,uTextureStrength,uTextureScale,uUploadedNormalStrength; uniform vec3 uSunDir,uCameraPos,uShallow,uMid,uDeep,uFoam,uSky,uSun,uHazeColor; uniform sampler2D uNormal0,uNormal1,uUserTex; varying vec3 vWorldPos,vNormal; varying float vFoam,vHeight,vBreak; varying vec2 vUv; float hash(vec2 p){ return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453); } 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; } 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; } void main(){ vec2 uv0=vWorldPos.xz/uNormalScale0+vec2(uTime*.018,uTime*.006); vec2 uv1=vWorldPos.zx/uNormalScale1+vec2(-uTime*.026,uTime*.014); 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; 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); vec3 V=normalize(uCameraPos-vWorldPos), L=normalize(uSunDir), H=normalize(V+L); float ndv=max(dot(N,V),0.0); float fres=pow(1.-ndv,5.0); float ndl=max(dot(N,L),0.0); 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; float dist=length(uCameraPos.xz-vWorldPos.xz); float depth=smoothstep(8.,180.,dist); float h=smoothstep(-1.2,2.0,vHeight); 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; water=mix(water,water*(.72+user*.55),uTextureStrength); 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); 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); 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; 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; float foam=clamp((fresh+old*.35)*uFoamAmount,0.,1.); color=mix(color,uFoam,foam*.80); float fog=1.-exp(-dist*uHazeDensity); color=mix(color,uHazeColor,clamp(fog,0.,.70)); gl_FragColor=vec4(color,1.); }` }); const water = new THREE.Mesh(waterGeo, waterMat); scene.add(water); 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); } makeRock(-38,-58,7); makeRock(-29,-65,4.5); makeRock(45,-78,8); makeRock(55,-70,3.7); const boat = new THREE.Group(); 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); 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); 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); boat.position.set(13,0,-22); boat.rotation.y=-.55; scene.add(boat); 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()); function setColorUniform(name,value){ U[name].value.set(value); } 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; } 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); } const gui=new GUI({title:'Ocean controls'}); gui.close(); 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); 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'); 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'); 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'); 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)); 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); 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); }); }); 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; } 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); } const keys={}; addEventListener('keydown',e=>keys[e.code]=true); addEventListener('keyup',e=>keys[e.code]=false); const clock=new THREE.Clock(); let elapsed=0; 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(); } animate(); addEventListener('resize',()=>{ camera.aspect=innerWidth/innerHeight; camera.updateProjectionMatrix(); renderer.setSize(innerWidth,innerHeight); composer.setSize(innerWidth,innerHeight); bloom.setSize(innerWidth,innerHeight); });