diff --git a/src/fluidDistortion.js b/src/fluidDistortion.js index 998f917..7498ddc 100644 --- a/src/fluidDistortion.js +++ b/src/fluidDistortion.js @@ -1,16 +1,16 @@ import * as THREE from 'three'; -// Lightweight ripple simulation: stores current (R) and previous (G) height, -// updates with a damped wave equation + a mouse "splat". +// Enhanced ripple simulation with multiple trailing ripples and lighting const FluidSimShader = { uniforms: { - tPrev: { value: null }, // previous state texture (RG) - iResolution: { value: new THREE.Vector2() }, // render-target resolution in pixels + tPrev: { value: null }, + iResolution: { value: new THREE.Vector2() }, iTime: { value: 0.0 }, - mouse: { value: new THREE.Vector3(-1, -1, 0.0) }, // x,y in pixels, z=strength - dissipation: { value: 0.996 }, // global damping - tension: { value: 0.5 }, // wave speed coefficient - radius: { value: 18.0 }, // splat radius in pixels + mouse: { value: new THREE.Vector3(-1, -1, 0.0) }, + dissipation: { value: 0.950 }, // Slightly more persistent for trails + tension: { value: 1.5 }, // Higher tension for stronger ripples + radius: { value: 10.0 }, // Larger splat radius + trailLength: { value: 3 }, // Number of trailing ripples }, vertexShader: ` varying vec2 vUv; @@ -26,62 +26,84 @@ const FluidSimShader = { uniform sampler2D tPrev; uniform vec2 iResolution; uniform float iTime; - uniform vec3 mouse; // mouse.xy in pixels, mouse.z = strength - uniform float dissipation; // 0..1 - uniform float tension; // ~0.25..1.0 - uniform float radius; // pixels + uniform vec3 mouse; + uniform float dissipation; + uniform float tension; + uniform float radius; + uniform float trailLength; - // Read RG channels: R = current height, G = previous height vec2 readRG(vec2 uv) { vec4 c = texture2D(tPrev, uv); return c.rg; } void main() { - // Texel size vec2 texel = 1.0 / iResolution; - - // Current and previous heights at this pixel vec2 currPrev = readRG(vUv); float curr = currPrev.r; float prev = currPrev.g; - // 4-neighbor laplacian on the "current" height field + // Enhanced 8-neighbor laplacian for stronger ripples float up = readRG(vUv + vec2(0.0, texel.y)).r; float down = readRG(vUv + vec2(0.0, -texel.y)).r; float right = readRG(vUv + vec2( texel.x, 0.0)).r; float left = readRG(vUv + vec2(-texel.x, 0.0)).r; + + // Diagonal neighbors for smoother ripples + float upLeft = readRG(vUv + vec2(-texel.x, texel.y)).r; + float upRight = readRG(vUv + vec2( texel.x, texel.y)).r; + float downLeft = readRG(vUv + vec2(-texel.x, -texel.y)).r; + float downRight = readRG(vUv + vec2( texel.x, -texel.y)).r; - float lap = (up + down + left + right - 4.0 * curr); + // Enhanced laplacian with diagonal weights + float lap = (up + down + left + right) * 0.2 + + (upLeft + upRight + downLeft + downRight) * 0.05 - curr; - // Wave equation with damping: next = curr + (curr - prev) * dissipation + lap * tension + // Wave equation with enhanced parameters float next = curr + (curr - prev) * dissipation + lap * tension; - // Mouse "splat" - add a Gaussian bump near the pointer when in bounds + // Multiple trailing ripples from mouse movement if (mouse.z > 0.0001) { vec2 uvPx = vUv * iResolution; vec2 d = uvPx - mouse.xy; - // Gaussian falloff in pixel space - float r = radius; - float g = exp(-dot(d, d) / max(1.0, (r * r))); - // Scale by strength; sign controls up/down displacement - next += g * mouse.z * 0.5; + float dist = length(d); + + // Create multiple concentric ripples + for (float i = 0.0; i < 4.0; i++) { + if (i >= trailLength) break; + + float offset = i * radius * 0.4; // Spacing between ripples + float r = radius + offset; + float timeOffset = i * 0.3; // Temporal offset for trailing effect + + // Gaussian with sine wave for ripple pattern + float g = exp(-pow(dist - offset, 2.0) / (r * r * 0.5)); + float ripple = sin(dist * 0.2 - iTime * 8.0 + timeOffset) * g; + + // Diminishing strength for trailing ripples + float strength = mouse.z * (1.0 - i * 0.2) * 0.8; + next += ripple * strength; + } } - // Pack next and curr into RG for the next step gl_FragColor = vec4(next, curr, 0.0, 1.0); } ` }; -// Screen-space distortion shader with chromatic aberration +// Enhanced distortion shader with dynamic lighting export const FluidDistortionShader = { uniforms: { - tDiffuse: { value: null }, // input scene color - tSim: { value: null }, // ripple height texture (R = height) - iResolution: { value: new THREE.Vector2() }, // pixels - amount: { value: 0.065 }, // UV offset scale - chromaticAmount: { value: 0.008 } // chromatic aberration strength + tDiffuse: { value: null }, + tSim: { value: null }, + iResolution: { value: new THREE.Vector2() }, + amount: { value: 0.12 }, // Stronger base distortion + chromaticAmount: { value: 0.015 }, // Enhanced chromatic aberration + lightPosition: { value: new THREE.Vector3(0.5, 0.5, 1.0) }, // Light position + lightIntensity: { value: 1.5 }, // Light brightness + lightColor: { value: new THREE.Color(0.8, 0.9, 1.0) }, // Cool light color + normalStrength: { value: 2.0 }, // How pronounced the lighting effect is + ambientLight: { value: 0.15 }, // Base ambient lighting }, vertexShader: ` varying vec2 vUv; @@ -99,56 +121,79 @@ export const FluidDistortionShader = { uniform vec2 iResolution; uniform float amount; uniform float chromaticAmount; + uniform vec3 lightPosition; + uniform float lightIntensity; + uniform vec3 lightColor; + uniform float normalStrength; + uniform float ambientLight; void main() { vec2 texel = 1.0 / iResolution; - // Central differences on the height field to estimate normal/gradient + // Sample height field for normal calculation float hC = texture2D(tSim, vUv).r; float hL = texture2D(tSim, vUv - vec2(texel.x, 0.0)).r; float hR = texture2D(tSim, vUv + vec2(texel.x, 0.0)).r; float hD = texture2D(tSim, vUv - vec2(0.0, texel.y)).r; float hU = texture2D(tSim, vUv + vec2(0.0, texel.y)).r; - // Gradient - vec2 grad = vec2(hR - hL, hU - hD); + // Calculate gradient and normal + vec2 grad = vec2(hR - hL, hU - hD) * normalStrength; + vec3 normal = normalize(vec3(-grad.x, -grad.y, 1.0)); - // Base distortion offset + // Enhanced distortion with trailing effect vec2 baseOffset = grad * amount; - // Chromatic aberration: sample R, G, B at slightly different offsets + // Add subtle trailing distortion based on height + vec2 trailOffset = grad * abs(hC) * amount * 0.3; + vec2 totalOffset = baseOffset + trailOffset; + + // Chromatic aberration with enhanced separation vec2 chromaticOffset = grad * chromaticAmount; - // Red channel - offset in gradient direction - vec2 uvR = vUv + baseOffset + chromaticOffset; - - // Green channel - no additional chromatic offset (center) - vec2 uvG = vUv + baseOffset; - - // Blue channel - offset opposite to gradient direction - vec2 uvB = vUv + baseOffset - chromaticOffset; + vec2 uvR = vUv + totalOffset + chromaticOffset; + vec2 uvG = vUv + totalOffset; + vec2 uvB = vUv + totalOffset - chromaticOffset; - // Clamp all UVs to avoid sampling outside + // Clamp UVs uvR = clamp(uvR, vec2(0.0), vec2(1.0)); uvG = clamp(uvG, vec2(0.0), vec2(1.0)); uvB = clamp(uvB, vec2(0.0), vec2(1.0)); - // Sample each channel separately + // Sample distorted colors float r = texture2D(tDiffuse, uvR).r; float g = texture2D(tDiffuse, uvG).g; float b = texture2D(tDiffuse, uvB).b; + + vec3 distortedColor = vec3(r, g, b); - gl_FragColor = vec4(r, g, b, 1.0); + // Dynamic lighting calculation + vec3 lightDir = normalize(vec3(lightPosition.xy - vUv, lightPosition.z)); + float NdotL = max(dot(normal, lightDir), 0.0); + + // Create rim lighting effect for ripples + float rimLight = pow(1.0 - abs(dot(normal, vec3(0.0, 0.0, 1.0))), 2.0); + + // Combine lighting effects + vec3 lighting = lightColor * (NdotL * lightIntensity + rimLight * 0.3) + ambientLight; + + // Apply lighting selectively - stronger where there are ripples + float rippleIntensity = abs(hC) + length(grad) * 0.5; + rippleIntensity = clamp(rippleIntensity, 0.0, 1.0); + + // Blend original color with lit color based on ripple presence + vec3 finalColor = mix(distortedColor, distortedColor * lighting, rippleIntensity); + + gl_FragColor = vec4(finalColor, 1.0); } ` }; -// Factory to create/update the simulation +// Enhanced fluid simulation factory export function createFluidSimulation(renderer, dpr = 1) { const simScene = new THREE.Scene(); const simCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); - // Fullscreen quad const quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), new THREE.ShaderMaterial({ uniforms: THREE.UniformsUtils.clone(FluidSimShader.uniforms), vertexShader: FluidSimShader.vertexShader, @@ -158,12 +203,12 @@ export function createFluidSimulation(renderer, dpr = 1) { })); simScene.add(quad); - // Create ping-pong targets + // Higher precision for better ripple quality const params = { minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter, format: THREE.RGBAFormat, - type: THREE.UnsignedByteType, // portable and sufficient for this use + type: THREE.FloatType, // Use float for better precision depthBuffer: false, stencilBuffer: false }; @@ -173,40 +218,36 @@ export function createFluidSimulation(renderer, dpr = 1) { let rtA = new THREE.WebGLRenderTarget(width, height, params); let rtB = new THREE.WebGLRenderTarget(width, height, params); - // Initialize empty + // Initialize renderer.setRenderTarget(rtA); renderer.clear(); renderer.setRenderTarget(rtB); renderer.clear(); renderer.setRenderTarget(null); - // Init uniforms quad.material.uniforms.iResolution.value.set(width, height); quad.material.uniforms.tPrev.value = rtA.texture; - // Swap helper function swap() { const tmp = rtA; rtA = rtB; rtB = tmp; } - // External API function update(mouseX, mouseY, strength, timeSec) { - // Update uniforms quad.material.uniforms.iTime.value = timeSec; - // Mouse: if offscreen (negative), set strength 0 + if (mouseX < 0.0 || mouseY < 0.0) { quad.material.uniforms.mouse.value.set(-1, -1, 0.0); } else { - quad.material.uniforms.mouse.value.set(mouseX, mouseY, Math.max(0.0, Math.min(1.0, strength))); + // Enhanced strength for better trailing effect + const enhancedStrength = Math.max(0.0, Math.min(1.0, strength * 1.5)); + quad.material.uniforms.mouse.value.set(mouseX, mouseY, enhancedStrength); } - // Render step: read rtA into shader, write next state into rtB quad.material.uniforms.tPrev.value = rtA.texture; renderer.setRenderTarget(rtB); renderer.render(simScene, simCamera); renderer.setRenderTarget(null); - // Next frame reads the freshly written state swap(); } diff --git a/src/main.js b/src/main.js index 7d8d509..2138341 100644 --- a/src/main.js +++ b/src/main.js @@ -45,28 +45,36 @@ const turntableSpeed = 0.5; // Store preloaded models let preloadedModels = {}; -// Fluid simulation + distortion pass +// Enhanced fluid simulation + distortion pass const dpr = renderer.getPixelRatio ? renderer.getPixelRatio() : Math.min(window.devicePixelRatio || 1, 2); const fluid = createFluidSimulation(renderer, dpr); const distortionPass = new ShaderPass(FluidDistortionShader); distortionPass.material.uniforms.tSim.value = fluid.getTexture(); distortionPass.material.uniforms.iResolution.value.set(window.innerWidth * dpr, window.innerHeight * dpr); -distortionPass.material.uniforms.amount.value = 0.100; -distortionPass.material.uniforms.chromaticAmount.value = 0.050; +distortionPass.material.uniforms.amount.value = 0.005; // Stronger distortion +distortionPass.material.uniforms.chromaticAmount.value = 0.002; // Enhanced chromatic aberration + +// Enhanced lighting parameters +distortionPass.material.uniforms.lightIntensity.value = 1.5; +distortionPass.material.uniforms.lightColor.value.set(1, 1, 1); // Cool blue-white +distortionPass.material.uniforms.normalStrength.value = 2.0; +distortionPass.material.uniforms.ambientLight.value = 0.15; composer.addPass(distortionPass); -// Pointer tracking for both fluid simulation and starfield +// Enhanced pointer tracking const pointer = { x: -1, y: -1, strength: 0.0, prevX: -1, prevY: -1, + trail: [], // Store trail positions for enhanced effect + maxTrailLength: 5 }; -// Mouse coordinates for starfield (normalized device coordinates) +// Mouse coordinates for starfield const mouse = new THREE.Vector2(); function toSimPixels(e) { @@ -80,16 +88,21 @@ renderer.domElement.addEventListener('pointermove', (e) => { const { x, y } = toSimPixels(e); const dx = (pointer.prevX < 0) ? 0 : Math.abs(x - pointer.prevX); const dy = (pointer.prevY < 0) ? 0 : Math.abs(y - pointer.prevY); - const speed = Math.min(Math.sqrt(dx * dx + dy * dy) / (8.0 * dpr), 1.0); + const speed = Math.min(Math.sqrt(dx * dx + dy * dy) / (6.0 * dpr), 1.0); // More sensitive pointer.x = x; pointer.y = y; - pointer.strength = speed * 0.85; + pointer.strength = speed * 1.2; // Enhanced strength pointer.prevX = x; pointer.prevY = y; - // Update mouse coordinates for starfield (NDC: -1 to +1) + // Update light position to follow cursor const rect = renderer.domElement.getBoundingClientRect(); + const normalizedX = (e.clientX - rect.left) / rect.width; + const normalizedY = 1.0 - (e.clientY - rect.top) / rect.height; // Flip Y + distortionPass.material.uniforms.lightPosition.value.set(normalizedX, normalizedY, 1.0); + + // Update mouse coordinates for starfield mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1; mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1; }, { passive: true }); @@ -99,12 +112,14 @@ renderer.domElement.addEventListener('pointerleave', () => { pointer.y = -1; pointer.strength = 0.0; - // Clear mouse for starfield - mouse.x = -999; // Move off-screen + mouse.x = -999; mouse.y = -999; + + // Reset light to center when mouse leaves + distortionPass.material.uniforms.lightPosition.value.set(0.5, 0.5, 1.0); }, { passive: true }); -// Initialize first scene after all models are loaded +// Initialize first scene function initializeScene() { console.log('Initializing first scene (bold)'); const { model, animMixer } = createModelFromPreloaded('bold', preloadedModels, camera, controls); @@ -132,7 +147,7 @@ function animate() { updateTransition(delta, scene); } - // Turntable rotation animation + // Turntable rotation if (currentModel) { currentModel.rotation.y += turntableSpeed * delta; } @@ -147,7 +162,7 @@ function animate() { // Animate stars with cursor interaction starfield.animateStars(camera, mouse, delta); - // Update fluid sim and refresh distortion pass input + // Update enhanced fluid sim const nowSec = performance.now() / 1000; fluid.update(pointer.x, pointer.y, pointer.strength, nowSec); distortionPass.material.uniforms.tSim.value = fluid.getTexture();