refined fluid, starfield added

This commit is contained in:
Anuj K 2025-09-03 09:02:56 +05:30
parent 42e873cda5
commit 23be78a855
2 changed files with 130 additions and 74 deletions

View file

@ -1,16 +1,16 @@
import * as THREE from 'three'; import * as THREE from 'three';
// Lightweight ripple simulation: stores current (R) and previous (G) height, // Enhanced ripple simulation with multiple trailing ripples and lighting
// updates with a damped wave equation + a mouse "splat".
const FluidSimShader = { const FluidSimShader = {
uniforms: { uniforms: {
tPrev: { value: null }, // previous state texture (RG) tPrev: { value: null },
iResolution: { value: new THREE.Vector2() }, // render-target resolution in pixels iResolution: { value: new THREE.Vector2() },
iTime: { value: 0.0 }, iTime: { value: 0.0 },
mouse: { value: new THREE.Vector3(-1, -1, 0.0) }, // x,y in pixels, z=strength mouse: { value: new THREE.Vector3(-1, -1, 0.0) },
dissipation: { value: 0.996 }, // global damping dissipation: { value: 0.950 }, // Slightly more persistent for trails
tension: { value: 0.5 }, // wave speed coefficient tension: { value: 1.5 }, // Higher tension for stronger ripples
radius: { value: 18.0 }, // splat radius in pixels radius: { value: 10.0 }, // Larger splat radius
trailLength: { value: 3 }, // Number of trailing ripples
}, },
vertexShader: ` vertexShader: `
varying vec2 vUv; varying vec2 vUv;
@ -26,62 +26,84 @@ const FluidSimShader = {
uniform sampler2D tPrev; uniform sampler2D tPrev;
uniform vec2 iResolution; uniform vec2 iResolution;
uniform float iTime; uniform float iTime;
uniform vec3 mouse; // mouse.xy in pixels, mouse.z = strength uniform vec3 mouse;
uniform float dissipation; // 0..1 uniform float dissipation;
uniform float tension; // ~0.25..1.0 uniform float tension;
uniform float radius; // pixels uniform float radius;
uniform float trailLength;
// Read RG channels: R = current height, G = previous height
vec2 readRG(vec2 uv) { vec2 readRG(vec2 uv) {
vec4 c = texture2D(tPrev, uv); vec4 c = texture2D(tPrev, uv);
return c.rg; return c.rg;
} }
void main() { void main() {
// Texel size
vec2 texel = 1.0 / iResolution; vec2 texel = 1.0 / iResolution;
// Current and previous heights at this pixel
vec2 currPrev = readRG(vUv); vec2 currPrev = readRG(vUv);
float curr = currPrev.r; float curr = currPrev.r;
float prev = currPrev.g; 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 up = readRG(vUv + vec2(0.0, texel.y)).r;
float down = 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 right = readRG(vUv + vec2( texel.x, 0.0)).r;
float left = 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; 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) { if (mouse.z > 0.0001) {
vec2 uvPx = vUv * iResolution; vec2 uvPx = vUv * iResolution;
vec2 d = uvPx - mouse.xy; vec2 d = uvPx - mouse.xy;
// Gaussian falloff in pixel space float dist = length(d);
float r = radius;
float g = exp(-dot(d, d) / max(1.0, (r * r))); // Create multiple concentric ripples
// Scale by strength; sign controls up/down displacement for (float i = 0.0; i < 4.0; i++) {
next += g * mouse.z * 0.5; 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); 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 = { export const FluidDistortionShader = {
uniforms: { uniforms: {
tDiffuse: { value: null }, // input scene color tDiffuse: { value: null },
tSim: { value: null }, // ripple height texture (R = height) tSim: { value: null },
iResolution: { value: new THREE.Vector2() }, // pixels iResolution: { value: new THREE.Vector2() },
amount: { value: 0.065 }, // UV offset scale amount: { value: 0.12 }, // Stronger base distortion
chromaticAmount: { value: 0.008 } // chromatic aberration strength 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: ` vertexShader: `
varying vec2 vUv; varying vec2 vUv;
@ -99,56 +121,79 @@ export const FluidDistortionShader = {
uniform vec2 iResolution; uniform vec2 iResolution;
uniform float amount; uniform float amount;
uniform float chromaticAmount; uniform float chromaticAmount;
uniform vec3 lightPosition;
uniform float lightIntensity;
uniform vec3 lightColor;
uniform float normalStrength;
uniform float ambientLight;
void main() { void main() {
vec2 texel = 1.0 / iResolution; 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 hC = texture2D(tSim, vUv).r;
float hL = texture2D(tSim, vUv - vec2(texel.x, 0.0)).r; float hL = texture2D(tSim, vUv - vec2(texel.x, 0.0)).r;
float hR = 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 hD = texture2D(tSim, vUv - vec2(0.0, texel.y)).r;
float hU = texture2D(tSim, vUv + vec2(0.0, texel.y)).r; float hU = texture2D(tSim, vUv + vec2(0.0, texel.y)).r;
// Gradient // Calculate gradient and normal
vec2 grad = vec2(hR - hL, hU - hD); 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; 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; vec2 chromaticOffset = grad * chromaticAmount;
// Red channel - offset in gradient direction vec2 uvR = vUv + totalOffset + chromaticOffset;
vec2 uvR = vUv + baseOffset + chromaticOffset; vec2 uvG = vUv + totalOffset;
vec2 uvB = vUv + totalOffset - chromaticOffset;
// Green channel - no additional chromatic offset (center)
vec2 uvG = vUv + baseOffset;
// Blue channel - offset opposite to gradient direction
vec2 uvB = vUv + baseOffset - chromaticOffset;
// Clamp all UVs to avoid sampling outside // Clamp UVs
uvR = clamp(uvR, vec2(0.0), vec2(1.0)); uvR = clamp(uvR, vec2(0.0), vec2(1.0));
uvG = clamp(uvG, vec2(0.0), vec2(1.0)); uvG = clamp(uvG, vec2(0.0), vec2(1.0));
uvB = clamp(uvB, 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 r = texture2D(tDiffuse, uvR).r;
float g = texture2D(tDiffuse, uvG).g; float g = texture2D(tDiffuse, uvG).g;
float b = texture2D(tDiffuse, uvB).b; 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) { export function createFluidSimulation(renderer, dpr = 1) {
const simScene = new THREE.Scene(); const simScene = new THREE.Scene();
const simCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); 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({ const quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), new THREE.ShaderMaterial({
uniforms: THREE.UniformsUtils.clone(FluidSimShader.uniforms), uniforms: THREE.UniformsUtils.clone(FluidSimShader.uniforms),
vertexShader: FluidSimShader.vertexShader, vertexShader: FluidSimShader.vertexShader,
@ -158,12 +203,12 @@ export function createFluidSimulation(renderer, dpr = 1) {
})); }));
simScene.add(quad); simScene.add(quad);
// Create ping-pong targets // Higher precision for better ripple quality
const params = { const params = {
minFilter: THREE.LinearFilter, minFilter: THREE.LinearFilter,
magFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter,
format: THREE.RGBAFormat, format: THREE.RGBAFormat,
type: THREE.UnsignedByteType, // portable and sufficient for this use type: THREE.FloatType, // Use float for better precision
depthBuffer: false, depthBuffer: false,
stencilBuffer: false stencilBuffer: false
}; };
@ -173,40 +218,36 @@ export function createFluidSimulation(renderer, dpr = 1) {
let rtA = new THREE.WebGLRenderTarget(width, height, params); let rtA = new THREE.WebGLRenderTarget(width, height, params);
let rtB = new THREE.WebGLRenderTarget(width, height, params); let rtB = new THREE.WebGLRenderTarget(width, height, params);
// Initialize empty // Initialize
renderer.setRenderTarget(rtA); renderer.setRenderTarget(rtA);
renderer.clear(); renderer.clear();
renderer.setRenderTarget(rtB); renderer.setRenderTarget(rtB);
renderer.clear(); renderer.clear();
renderer.setRenderTarget(null); renderer.setRenderTarget(null);
// Init uniforms
quad.material.uniforms.iResolution.value.set(width, height); quad.material.uniforms.iResolution.value.set(width, height);
quad.material.uniforms.tPrev.value = rtA.texture; quad.material.uniforms.tPrev.value = rtA.texture;
// Swap helper
function swap() { function swap() {
const tmp = rtA; rtA = rtB; rtB = tmp; const tmp = rtA; rtA = rtB; rtB = tmp;
} }
// External API
function update(mouseX, mouseY, strength, timeSec) { function update(mouseX, mouseY, strength, timeSec) {
// Update uniforms
quad.material.uniforms.iTime.value = timeSec; quad.material.uniforms.iTime.value = timeSec;
// Mouse: if offscreen (negative), set strength 0
if (mouseX < 0.0 || mouseY < 0.0) { if (mouseX < 0.0 || mouseY < 0.0) {
quad.material.uniforms.mouse.value.set(-1, -1, 0.0); quad.material.uniforms.mouse.value.set(-1, -1, 0.0);
} else { } 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; quad.material.uniforms.tPrev.value = rtA.texture;
renderer.setRenderTarget(rtB); renderer.setRenderTarget(rtB);
renderer.render(simScene, simCamera); renderer.render(simScene, simCamera);
renderer.setRenderTarget(null); renderer.setRenderTarget(null);
// Next frame reads the freshly written state
swap(); swap();
} }

View file

@ -45,28 +45,36 @@ const turntableSpeed = 0.5;
// Store preloaded models // Store preloaded models
let preloadedModels = {}; let preloadedModels = {};
// Fluid simulation + distortion pass // Enhanced fluid simulation + distortion pass
const dpr = renderer.getPixelRatio ? renderer.getPixelRatio() : Math.min(window.devicePixelRatio || 1, 2); const dpr = renderer.getPixelRatio ? renderer.getPixelRatio() : Math.min(window.devicePixelRatio || 1, 2);
const fluid = createFluidSimulation(renderer, dpr); const fluid = createFluidSimulation(renderer, dpr);
const distortionPass = new ShaderPass(FluidDistortionShader); const distortionPass = new ShaderPass(FluidDistortionShader);
distortionPass.material.uniforms.tSim.value = fluid.getTexture(); distortionPass.material.uniforms.tSim.value = fluid.getTexture();
distortionPass.material.uniforms.iResolution.value.set(window.innerWidth * dpr, window.innerHeight * dpr); distortionPass.material.uniforms.iResolution.value.set(window.innerWidth * dpr, window.innerHeight * dpr);
distortionPass.material.uniforms.amount.value = 0.100; distortionPass.material.uniforms.amount.value = 0.005; // Stronger distortion
distortionPass.material.uniforms.chromaticAmount.value = 0.050; 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); composer.addPass(distortionPass);
// Pointer tracking for both fluid simulation and starfield // Enhanced pointer tracking
const pointer = { const pointer = {
x: -1, x: -1,
y: -1, y: -1,
strength: 0.0, strength: 0.0,
prevX: -1, prevX: -1,
prevY: -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(); const mouse = new THREE.Vector2();
function toSimPixels(e) { function toSimPixels(e) {
@ -80,16 +88,21 @@ renderer.domElement.addEventListener('pointermove', (e) => {
const { x, y } = toSimPixels(e); const { x, y } = toSimPixels(e);
const dx = (pointer.prevX < 0) ? 0 : Math.abs(x - pointer.prevX); const dx = (pointer.prevX < 0) ? 0 : Math.abs(x - pointer.prevX);
const dy = (pointer.prevY < 0) ? 0 : Math.abs(y - pointer.prevY); 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.x = x;
pointer.y = y; pointer.y = y;
pointer.strength = speed * 0.85; pointer.strength = speed * 1.2; // Enhanced strength
pointer.prevX = x; pointer.prevX = x;
pointer.prevY = y; pointer.prevY = y;
// Update mouse coordinates for starfield (NDC: -1 to +1) // Update light position to follow cursor
const rect = renderer.domElement.getBoundingClientRect(); 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.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1; mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
}, { passive: true }); }, { passive: true });
@ -99,12 +112,14 @@ renderer.domElement.addEventListener('pointerleave', () => {
pointer.y = -1; pointer.y = -1;
pointer.strength = 0.0; pointer.strength = 0.0;
// Clear mouse for starfield mouse.x = -999;
mouse.x = -999; // Move off-screen
mouse.y = -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 }); }, { passive: true });
// Initialize first scene after all models are loaded // Initialize first scene
function initializeScene() { function initializeScene() {
console.log('Initializing first scene (bold)'); console.log('Initializing first scene (bold)');
const { model, animMixer } = createModelFromPreloaded('bold', preloadedModels, camera, controls); const { model, animMixer } = createModelFromPreloaded('bold', preloadedModels, camera, controls);
@ -132,7 +147,7 @@ function animate() {
updateTransition(delta, scene); updateTransition(delta, scene);
} }
// Turntable rotation animation // Turntable rotation
if (currentModel) { if (currentModel) {
currentModel.rotation.y += turntableSpeed * delta; currentModel.rotation.y += turntableSpeed * delta;
} }
@ -147,7 +162,7 @@ function animate() {
// Animate stars with cursor interaction // Animate stars with cursor interaction
starfield.animateStars(camera, mouse, delta); starfield.animateStars(camera, mouse, delta);
// Update fluid sim and refresh distortion pass input // Update enhanced fluid sim
const nowSec = performance.now() / 1000; const nowSec = performance.now() / 1000;
fluid.update(pointer.x, pointer.y, pointer.strength, nowSec); fluid.update(pointer.x, pointer.y, pointer.strength, nowSec);
distortionPass.material.uniforms.tSim.value = fluid.getTexture(); distortionPass.material.uniforms.tSim.value = fluid.getTexture();