import * as THREE from 'three'; // Scene setup const scene = new THREE.Scene(); const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); // Orthographic for 2D const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); // Simulation parameters const resolution = 512; // Texture resolution const damping = 0.995; // Slightly stronger damping for smoother waves const waveSpeed = 0.55; // Faster waves for dynamic feel // Frame buffers for height and normal maps const heightTarget1 = new THREE.WebGLRenderTarget(resolution, resolution, { type: THREE.FloatType, minFilter: THREE.NearestFilter, magFilter: THREE.NearestFilter, }); const heightTarget2 = new THREE.WebGLRenderTarget(resolution, resolution, { type: THREE.FloatType, minFilter: THREE.NearestFilter, magFilter: THREE.NearestFilter, }); const normalTarget = new THREE.WebGLRenderTarget(resolution, resolution, { minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter, }); // Simulation uniforms const simUniforms = { u_time: { value: 0.0 }, u_resolution: { value: new THREE.Vector2(resolution, resolution) }, u_mouse: { value: new THREE.Vector2(-1, -1) }, u_mouseForce: { value: 0.0 }, u_prevHeight: { value: heightTarget1.texture }, u_damping: { value: damping }, u_waveSpeed: { value: waveSpeed }, }; // Simulation vertex shader const simVertexShader = ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `; // Simulation fragment shader (wave equation with background motion) const simFragmentShader = ` uniform float u_time; uniform vec2 u_resolution; uniform vec2 u_mouse; uniform float u_mouseForce; uniform sampler2D u_prevHeight; uniform float u_damping; uniform float u_waveSpeed; varying vec2 vUv; // Noise for randomness and background motion float random(vec2 st) { return fract(sin(dot(st, vec2(127.1, 311.7))) * 43758.5453123); } float noise(vec2 st) { vec2 i = floor(st); vec2 f = fract(st); float a = random(i); float b = random(i + vec2(1.0, 0.0)); float c = random(i + vec2(0.0, 1.0)); float d = random(i + vec2(1.0, 1.0)); 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; } void main() { vec2 texel = 1.0 / u_resolution; float h = texture2D(u_prevHeight, vUv).r; // Current height float h_prev = texture2D(u_prevHeight, vUv).g; // Previous height // Sample neighbors float h_n = texture2D(u_prevHeight, vUv + vec2(0.0, texel.y)).r; float h_s = texture2D(u_prevHeight, vUv - vec2(0.0, texel.y)).r; float h_e = texture2D(u_prevHeight, vUv + vec2(texel.x, 0.0)).r; float h_w = texture2D(u_prevHeight, vUv - vec2(texel.x, 0.0)).r; // Wave equation float c2 = u_waveSpeed * u_waveSpeed; float newHeight = 2.0 * h - h_prev + c2 * (h_n + h_s + h_e + h_w - 4.0 * h); newHeight *= u_damping; // Mouse force with randomness float dist = length(vUv - u_mouse); if (dist < 0.05 && u_mouseForce > 0.0) { float rand = noise(vUv + u_time) * 0.1 + 0.9; // Random strength variation newHeight += 0.15 * rand * u_mouseForce * exp(-dist * 100.0); } // Background motion (subtle waves) float t = u_time * 2.0; float background = noise(vUv * 2.0 + t) * 0.00002; background += sin(u_time * 0.5 + vUv.x * 5.0) * 0.0000002; // Periodic pulse newHeight += background; gl_FragColor = vec4(newHeight, h, 0.0, 1.0); } `; // Simulation material const simMaterial = new THREE.ShaderMaterial({ uniforms: simUniforms, vertexShader: simVertexShader, fragmentShader: simFragmentShader, }); // Normal map shader (computes normals from height field) const normalVertexShader = ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `; const normalFragmentShader = ` uniform sampler2D u_heightMap; uniform vec2 u_resolution; varying vec2 vUv; void main() { vec2 texel = 1.0 / u_resolution; float h = texture2D(u_heightMap, vUv).r; float h_n = texture2D(u_heightMap, vUv + vec2(0.0, texel.y)).r; float h_s = texture2D(u_heightMap, vUv - vec2(0.0, texel.y)).r; float h_e = texture2D(u_heightMap, vUv + vec2(texel.x, 0.0)).r; float h_w = texture2D(u_heightMap, vUv - vec2(texel.x, 0.0)).r; // Compute normal using height gradients vec3 normal = normalize(vec3(h_w - h_e, h_s - h_n, 0.05)); gl_FragColor = vec4(normal * 0.5 + 0.5, 1.0); // Pack to [0,1] } `; const normalMaterial = new THREE.ShaderMaterial({ uniforms: { u_heightMap: { value: heightTarget1.texture }, u_resolution: { value: new THREE.Vector2(resolution, resolution) }, }, vertexShader: normalVertexShader, fragmentShader: normalFragmentShader, }); // Simulation and normal scenes const simGeometry = new THREE.PlaneGeometry(2, 2); const simMesh = new THREE.Mesh(simGeometry, simMaterial); const simScene = new THREE.Scene(); simScene.add(simMesh); const normalMesh = new THREE.Mesh(simGeometry, normalMaterial); const normalScene = new THREE.Scene(); normalScene.add(normalMesh); // Rendering uniforms const renderUniforms = { u_heightMap: { value: heightTarget1.texture }, u_normalMap: { value: normalTarget.texture }, u_resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }, u_time: { value: 0.0 }, u_lightPos: { value: new THREE.Vector2(0.5, 0.5) }, // Dynamic light position }; // Rendering vertex shader const renderVertexShader = ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `; // Rendering fragment shader (with lighting and glowing effects) const renderFragmentShader = ` uniform sampler2D u_heightMap; uniform sampler2D u_normalMap; uniform vec2 u_resolution; uniform float u_time; uniform vec2 u_lightPos; varying vec2 vUv; // Noise for granularity float random(vec2 st) { return fract(sin(dot(st, vec2(127.1, 311.7))) * 43758.5453123); } float noise(vec2 st) { vec2 i = floor(st); vec2 f = fract(st); float a = random(i); float b = random(i + vec2(1.0, 0.0)); float c = random(i + vec2(0.0, 1.0)); float d = random(i + vec2(1.0, 1.0)); 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; } void main() { float height = texture2D(u_heightMap, vUv).r; vec3 normal = texture2D(u_normalMap, vUv).xyz * 2.0 - 1.0; vec2 p = vUv * u_resolution.xy / min(u_resolution.x, u_resolution.y); // Dynamic lighting vec3 lightDir = normalize(vec3(u_lightPos - vUv, 0.5)); float diffuse = max(dot(normal, lightDir), 0.0); float specular = pow(diffuse, 32.0) * 0.5; // Base color with gradient vec3 baseColor = mix(vec3(0.0, 0.2, 0.5), vec3(0.1, 0.7, 1.0), 0.5 + height * 3.0); // Add granularity float n = noise(p * 15.0 + u_time * 0.2) * 0.1; baseColor += vec3(n); // Glowing wave crests float glow = abs(height) > 0.02 ? sin(height * 100.0 + u_time * 2.0) * 0.3 : 0.0; vec3 glowColor = vec3(0.2, 0.8, 1.0) * glow; vec3 glowColorB = vec3(0.2, 0.8, 1.0); // Combine lighting and effects vec3 color = baseColor * (0.5 + diffuse) + vec3(specular) + glowColor; color = clamp(color, 0.0, 1.0); gl_FragColor = vec4(color, 1); } `; // Rendering material const renderMaterial = new THREE.ShaderMaterial({ uniforms: renderUniforms, vertexShader: renderVertexShader, fragmentShader: renderFragmentShader, }); // Rendering plane const renderGeometry = new THREE.PlaneGeometry(2, 2); const renderMesh = new THREE.Mesh(renderGeometry, renderMaterial); scene.add(renderMesh); // Handle window resize window.addEventListener('resize', () => { renderer.setSize(window.innerWidth, window.innerHeight); renderUniforms.u_resolution.value.set(window.innerWidth, window.innerHeight); }); // Mouse interaction (click and drag) const mouse = new THREE.Vector2(); let isMouseDown = false; window.addEventListener('mousedown', (event) => { isMouseDown = true; updateMouse(event); }); window.addEventListener('mousemove', (event) => { if (isMouseDown) updateMouse(event); }); window.addEventListener('mouseup', () => { isMouseDown = false; simUniforms.u_mouseForce.value = 0.0; }); function updateMouse(event) { mouse.x = event.clientX / window.innerWidth; mouse.y = 1.0 - event.clientY / window.innerHeight; simUniforms.u_mouse.value.set(mouse.x, mouse.y); simUniforms.u_mouseForce.value = 1.0; } // Animation loop const clock = new THREE.Clock(); let currentHeightTarget = heightTarget1; let nextHeightTarget = heightTarget2; function animate() { requestAnimationFrame(animate); const t = clock.getElapsedTime(); simUniforms.u_time.value = t; renderUniforms.u_time.value = t; // Update light position (follows mouse or oscillates) renderUniforms.u_lightPos.value.set( isMouseDown ? mouse.x : 0.5 + Math.sin(t * 0.5) * 0.3, isMouseDown ? mouse.y : 0.5 + Math.cos(t * 0.5) * 0.3 ); // Update simulation simUniforms.u_prevHeight.value = currentHeightTarget.texture; renderer.setRenderTarget(nextHeightTarget); renderer.render(simScene, camera); // Update normal map normalMaterial.uniforms.u_heightMap.value = nextHeightTarget.texture; renderer.setRenderTarget(normalTarget); renderer.render(normalScene, camera); // Render to screen renderUniforms.u_heightMap.value = nextHeightTarget.texture; renderer.setRenderTarget(null); renderer.render(scene, camera); // Swap buffers const temp = currentHeightTarget; currentHeightTarget = nextHeightTarget; nextHeightTarget = temp; } animate();