Use multidirectional waves official water normals and movable boat
Browse files- 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(
|
| 13 |
-
scene.fog = new THREE.FogExp2(
|
| 14 |
|
| 15 |
const camera = new THREE.PerspectiveCamera(55, innerWidth / innerHeight, 0.1, 900);
|
| 16 |
-
camera.position.set(
|
| 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.
|
| 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 =
|
| 30 |
-
controls.maxDistance =
|
| 31 |
-
controls.target.set(0,
|
| 32 |
-
|
| 33 |
-
const sun = new THREE.Vector3(-0.
|
| 34 |
-
scene.add(new THREE.HemisphereLight(
|
| 35 |
-
const dir = new THREE.DirectionalLight(
|
| 36 |
-
dir.position.copy(sun).multiplyScalar(
|
| 37 |
scene.add(dir);
|
| 38 |
|
| 39 |
-
const sunMesh = new THREE.Mesh(new THREE.CircleGeometry(
|
| 40 |
-
sunMesh.position.set(-
|
| 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 |
-
|
| 51 |
-
|
| 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
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
const params = {
|
| 66 |
-
shallowColor:
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
foamAmount:
|
| 71 |
-
|
| 72 |
-
textureStrength:
|
| 73 |
-
bloomStrength:
|
|
|
|
| 74 |
};
|
| 75 |
|
| 76 |
-
const
|
| 77 |
-
uTime:
|
| 78 |
-
uShallow:
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
uDetailMap: { value: defaultTexture }, uTextureStrength: { value: params.textureStrength }, uTextureScale: { value: params.textureScale }, uNormalTextureStrength: { value: params.normalTextureStrength }
|
| 86 |
};
|
| 87 |
|
| 88 |
-
const waterGeo = new THREE.PlaneGeometry(
|
| 89 |
waterGeo.rotateX(-Math.PI / 2);
|
| 90 |
|
| 91 |
const waterMat = new THREE.ShaderMaterial({
|
| 92 |
-
uniforms:
|
| 93 |
-
depthWrite: true,
|
| 94 |
vertexShader: /* glsl */`
|
| 95 |
precision highp float;
|
| 96 |
-
uniform float uTime,
|
| 97 |
-
varying vec3 vWorldPos
|
| 98 |
-
struct Wave { vec2 dir; float
|
| 99 |
-
float hash(vec2 p){ return fract(sin(dot(p,
|
| 100 |
-
float noise(vec2 p){ vec2 i=floor(p),
|
| 101 |
-
float fbm(vec2 p){ float v=0.
|
| 102 |
-
vec2
|
| 103 |
-
vec3
|
| 104 |
-
vec2 d=normalize(w.dir); float f=dot(d,
|
| 105 |
-
float
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 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;
|
| 115 |
-
|
| 116 |
-
p +=
|
| 117 |
-
p +=
|
| 118 |
-
p +=
|
| 119 |
-
p +=
|
| 120 |
-
p +=
|
| 121 |
-
|
| 122 |
-
vec2 wind
|
| 123 |
-
p.xz += wind
|
| 124 |
-
p.y
|
| 125 |
-
|
| 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
|
| 134 |
-
uniform
|
| 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,
|
| 138 |
-
float noise(vec2 p){ vec2 i=floor(p),
|
| 139 |
-
float fbm(vec2 p){ float v=0.,
|
| 140 |
void main(){
|
| 141 |
-
vec2
|
| 142 |
-
vec3
|
| 143 |
-
vec3 N=normalize(vNormal);
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 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
|
| 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:
|
| 176 |
-
const mast = new THREE.Mesh(new THREE.CylinderGeometry(.06,.09,6,10), new THREE.MeshStandardMaterial({ color:
|
| 177 |
-
const sail = new THREE.Mesh(new THREE.PlaneGeometry(3.2,4.5), new THREE.MeshStandardMaterial({ color:
|
| 178 |
-
boat.position.set(13,0,-22); boat.rotation.y=-
|
| 179 |
-
|
| 180 |
-
const composer
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
function
|
| 184 |
-
function
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
const gui
|
| 188 |
-
const
|
| 189 |
-
|
| 190 |
-
const
|
| 191 |
-
|
| 192 |
-
const
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 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); });
|