Spaces:
Running
Running
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(); |