Elysia-Suite's picture
Upload 23 files
e5d943e verified
/**
* 🌿 Ivy's GPU Art Studio
* Tab 2: Fluid Simulation
*
* GPU-accelerated fluid dynamics using compute shaders
* Based on Jos Stam's "Stable Fluids" algorithm
* Enhanced with styles, palettes, and effects!
*/
class FluidRenderer {
constructor() {
this.device = null;
this.context = null;
this.format = null;
// Simulation parameters
this.params = {
style: 0, // 0=classic, 1=ivy, 2=ink, 3=smoke, 4=plasma, 5=watercolor
palette: 0, // 0=ivy, 1=rainbow, 2=fire, 3=ocean, 4=neon, 5=sunset, 6=cosmic, 7=mono
viscosity: 0.1,
diffusion: 0.0001,
force: 100,
curl: 30,
pressure: 0.8,
bloom: true,
vortex: false
};
// Simulation state
this.gridSize = 256;
this.velocityBuffers = [];
this.densityBuffers = [];
this.currentBuffer = 0;
this.input = null;
this.animationLoop = null;
this.isActive = false;
this.time = 0;
// Previous mouse position for velocity
this.prevMouseX = 0.5;
this.prevMouseY = 0.5;
}
async init(device, context, format, canvas) {
this.device = device;
this.context = context;
this.format = format;
this.canvas = canvas;
// Create simulation buffers
this.createBuffers();
// Create pipelines
await this.createPipelines();
// Setup input
this.input = new WebGPUUtils.InputHandler(canvas);
// Animation loop
this.animationLoop = new WebGPUUtils.AnimationLoop((dt, totalTime) => {
this.time = totalTime;
this.simulate(dt);
this.render();
});
}
createBuffers() {
const size = this.gridSize * this.gridSize;
// Double buffering for velocity (vec2) and density (f32)
for (let i = 0; i < 2; i++) {
this.velocityBuffers.push(
this.device.createBuffer({
label: `Velocity Buffer ${i}`,
size: size * 8, // vec2f = 8 bytes
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
})
);
this.densityBuffers.push(
this.device.createBuffer({
label: `Density Buffer ${i}`,
size: size * 16, // vec4f for RGBA = 16 bytes
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
})
);
}
// Uniform buffer
this.uniformBuffer = this.device.createBuffer({
label: "Fluid Uniforms",
size: 64,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
});
// Initialize with zeros
const zeroVelocity = new Float32Array(size * 2);
const zeroDensity = new Float32Array(size * 4);
for (let i = 0; i < 2; i++) {
this.device.queue.writeBuffer(this.velocityBuffers[i], 0, zeroVelocity);
this.device.queue.writeBuffer(this.densityBuffers[i], 0, zeroDensity);
}
}
async createPipelines() {
// Compute shader for simulation
const computeShader = this.device.createShaderModule({
label: "Fluid Compute Shader",
code: this.getComputeShaderCode()
});
// Render shader for display
const renderShader = this.device.createShaderModule({
label: "Fluid Render Shader",
code: this.getRenderShaderCode()
});
// Bind group layouts
this.computeBindGroupLayout = this.device.createBindGroupLayout({
entries: [
{ binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } },
{ binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
{ binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } },
{ binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
{ binding: 4, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } }
]
});
this.renderBindGroupLayout = this.device.createBindGroupLayout({
entries: [
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } },
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } }
]
});
// Compute pipeline
this.computePipeline = this.device.createComputePipeline({
label: "Fluid Compute Pipeline",
layout: this.device.createPipelineLayout({
bindGroupLayouts: [this.computeBindGroupLayout]
}),
compute: {
module: computeShader,
entryPoint: "main"
}
});
// Render pipeline
this.renderPipeline = this.device.createRenderPipeline({
label: "Fluid Render Pipeline",
layout: this.device.createPipelineLayout({
bindGroupLayouts: [this.renderBindGroupLayout]
}),
vertex: {
module: renderShader,
entryPoint: "vertexMain"
},
fragment: {
module: renderShader,
entryPoint: "fragmentMain",
targets: [{ format: this.format }]
},
primitive: {
topology: "triangle-list"
}
});
// Create bind groups
this.updateBindGroups();
}
updateBindGroups() {
const curr = this.currentBuffer;
const next = 1 - curr;
this.computeBindGroup = this.device.createBindGroup({
layout: this.computeBindGroupLayout,
entries: [
{ binding: 0, resource: { buffer: this.uniformBuffer } },
{ binding: 1, resource: { buffer: this.velocityBuffers[curr] } },
{ binding: 2, resource: { buffer: this.velocityBuffers[next] } },
{ binding: 3, resource: { buffer: this.densityBuffers[curr] } },
{ binding: 4, resource: { buffer: this.densityBuffers[next] } }
]
});
this.renderBindGroup = this.device.createBindGroup({
layout: this.renderBindGroupLayout,
entries: [
{ binding: 0, resource: { buffer: this.uniformBuffer } },
{ binding: 1, resource: { buffer: this.velocityBuffers[next] } },
{ binding: 2, resource: { buffer: this.densityBuffers[next] } }
]
});
}
start() {
this.isActive = true;
console.log("🌊 FluidRenderer started!");
this.animationLoop.start();
}
stop() {
this.isActive = false;
console.log("🌊 FluidRenderer stopped");
this.animationLoop.stop();
}
reset() {
const size = this.gridSize * this.gridSize;
const zeroVelocity = new Float32Array(size * 2);
const zeroDensity = new Float32Array(size * 4);
for (let i = 0; i < 2; i++) {
this.device.queue.writeBuffer(this.velocityBuffers[i], 0, zeroVelocity);
this.device.queue.writeBuffer(this.densityBuffers[i], 0, zeroDensity);
}
}
setViscosity(value) {
this.params.viscosity = value;
}
setDiffusion(value) {
this.params.diffusion = value;
}
setForce(value) {
this.params.force = value;
}
setColorMode(mode) {
const modes = { ink: 0, fire: 1, rainbow: 2, smoke: 3, ivy: 4 };
this.params.colorMode = modes[mode] || 0;
}
setStyle(style) {
const styles = { classic: 0, ivy: 1, ink: 2, smoke: 3, plasma: 4, watercolor: 5 };
this.params.style = styles[style] ?? 0;
}
setPalette(palette) {
const palettes = { ivy: 0, rainbow: 1, fire: 2, ocean: 3, neon: 4, sunset: 5, cosmic: 6, monochrome: 7 };
this.params.palette = palettes[palette] ?? 0;
}
setCurl(value) {
this.params.curl = value;
}
setPressure(value) {
this.params.pressure = value;
}
setBloom(enabled) {
this.params.bloom = enabled;
}
setVortex(enabled) {
this.params.vortex = enabled;
}
simulate(dt) {
if (!this.isActive) return;
// Auto-spawn some fluid for visual feedback even without mouse
const autoSpawn = !this.input.isPressed;
let mouseX = this.input.mouseX;
let mouseY = this.input.mouseY;
let isPressed = this.input.isPressed;
// Auto animation when not interacting
if (autoSpawn && this.time > 0) {
// Create swirling patterns automatically
const t = this.time * 0.5;
mouseX = 0.5 + 0.3 * Math.sin(t);
mouseY = 0.5 + 0.3 * Math.cos(t * 0.7);
isPressed = true; // Simulate mouse press for auto-spawn
}
// Calculate mouse velocity
const dx = (mouseX - this.prevMouseX) * this.params.force;
const dy = (mouseY - this.prevMouseY) * this.params.force;
this.prevMouseX = mouseX;
this.prevMouseY = mouseY;
// Update uniforms - expanded for new params
const uniforms = new Float32Array([
this.gridSize, // 0: grid size
dt, // 1: delta time
this.params.viscosity, // 2: viscosity
this.params.diffusion, // 3: diffusion
mouseX, // 4: mouse X
mouseY, // 5: mouse Y
dx, // 6: velocity X
dy, // 7: velocity Y
isPressed ? 1.0 : 0.0, // 8: is mouse pressed
this.params.style, // 9: style
this.params.palette, // 10: palette
this.params.curl, // 11: curl/vorticity
this.params.pressure, // 12: pressure
this.params.bloom ? 1.0 : 0.0, // 13: bloom
this.params.vortex ? 1.0 : 0.0, // 14: vortex
this.time // 15: time
]);
this.device.queue.writeBuffer(this.uniformBuffer, 0, uniforms);
// Update bind groups with current buffer state
this.updateBindGroups();
// Run compute shader
const commandEncoder = this.device.createCommandEncoder();
const computePass = commandEncoder.beginComputePass();
computePass.setPipeline(this.computePipeline);
computePass.setBindGroup(0, this.computeBindGroup);
computePass.dispatchWorkgroups(Math.ceil(this.gridSize / 8), Math.ceil(this.gridSize / 8));
computePass.end();
this.device.queue.submit([commandEncoder.finish()]);
// Swap buffers
this.currentBuffer = 1 - this.currentBuffer;
}
render() {
if (!this.isActive) return;
WebGPUUtils.resizeCanvasToDisplaySize(this.canvas, window.devicePixelRatio);
// IMPORTANT: Create render bind group to read the LATEST buffer (after compute)
const curr = this.currentBuffer;
const renderBindGroup = this.device.createBindGroup({
layout: this.renderBindGroupLayout,
entries: [
{ binding: 0, resource: { buffer: this.uniformBuffer } },
{ binding: 1, resource: { buffer: this.velocityBuffers[curr] } },
{ binding: 2, resource: { buffer: this.densityBuffers[curr] } }
]
});
const commandEncoder = this.device.createCommandEncoder();
const renderPass = commandEncoder.beginRenderPass({
colorAttachments: [
{
view: this.context.getCurrentTexture().createView(),
clearValue: { r: 0, g: 0, b: 0, a: 1 },
loadOp: "clear",
storeOp: "store"
}
]
});
renderPass.setPipeline(this.renderPipeline);
renderPass.setBindGroup(0, renderBindGroup);
renderPass.draw(3);
renderPass.end();
this.device.queue.submit([commandEncoder.finish()]);
}
getComputeShaderCode() {
return /* wgsl */ `
struct Uniforms {
gridSize: f32,
dt: f32,
viscosity: f32,
diffusion: f32,
mouseX: f32,
mouseY: f32,
velX: f32,
velY: f32,
mousePressed: f32,
style: f32,
palette: f32,
curl: f32,
pressure: f32,
doBloom: f32,
doVortex: f32,
time: f32,
}
@group(0) @binding(0) var<uniform> u: Uniforms;
@group(0) @binding(1) var<storage, read> velIn: array<vec2f>;
@group(0) @binding(2) var<storage, read_write> velOut: array<vec2f>;
@group(0) @binding(3) var<storage, read> densIn: array<vec4f>;
@group(0) @binding(4) var<storage, read_write> densOut: array<vec4f>;
fn idx(x: i32, y: i32) -> u32 {
let size = i32(u.gridSize);
let cx = clamp(x, 0, size - 1);
let cy = clamp(y, 0, size - 1);
return u32(cy * size + cx);
}
fn getPaletteColor(t: f32, paletteId: i32) -> vec3f {
let tt = fract(t);
if (paletteId == 0) { // Ivy Green
return vec3f(0.13 * tt + 0.05, 0.77 * tt + 0.2, 0.37 * tt + 0.1);
} else if (paletteId == 1) { // Rainbow
return vec3f(
0.5 + 0.5 * sin(tt * 6.28 + 0.0),
0.5 + 0.5 * sin(tt * 6.28 + 2.094),
0.5 + 0.5 * sin(tt * 6.28 + 4.188)
);
} else if (paletteId == 2) { // Fire
return vec3f(tt, tt * 0.4, tt * 0.1);
} else if (paletteId == 3) { // Ocean
return vec3f(0.1 * tt, 0.4 * tt + 0.1, 0.9 * tt + 0.1);
} else if (paletteId == 4) { // Neon
return vec3f(
0.5 + 0.5 * sin(tt * 12.0),
0.5 + 0.5 * sin(tt * 12.0 + 2.0),
0.5 + 0.5 * sin(tt * 12.0 + 4.0)
);
} else if (paletteId == 5) { // Sunset
return vec3f(0.9 * tt + 0.1, 0.4 * tt, 0.3 * tt + 0.1);
} else if (paletteId == 6) { // Cosmic
return vec3f(0.3 * tt + 0.1, 0.1 * tt + 0.05, 0.8 * tt + 0.2);
} else { // Monochrome
return vec3f(tt * 0.9 + 0.1);
}
}
@compute @workgroup_size(8, 8)
fn main(@builtin(global_invocation_id) gid: vec3u) {
let size = i32(u.gridSize);
let x = i32(gid.x);
let y = i32(gid.y);
if (x >= size || y >= size) {
return;
}
let i = idx(x, y);
let paletteId = i32(u.palette);
// Read previous state
var newVel = velIn[i];
var newDens = densIn[i];
// Get neighbors for diffusion
let vL = velIn[idx(x - 1, y)];
let vR = velIn[idx(x + 1, y)];
let vU = velIn[idx(x, y + 1)];
let vD = velIn[idx(x, y - 1)];
let dL = densIn[idx(x - 1, y)];
let dR = densIn[idx(x + 1, y)];
let dU = densIn[idx(x, y + 1)];
let dD = densIn[idx(x, y - 1)];
// Apply diffusion (controlled by diffusion parameter)
let diffAmount = u.diffusion * 1000.0;
newVel = mix(newVel, (vL + vR + vU + vD) * 0.25, diffAmount);
newDens = mix(newDens, (dL + dR + dU + dD) * 0.25, diffAmount);
// Apply viscosity (dampens velocity)
newVel *= (1.0 - u.viscosity * 0.1);
// Vorticity / curl effect
if (u.doVortex > 0.5) {
let curlAmount = u.curl * 0.0005;
let vortex = (vR.y - vL.y) - (vU.x - vD.x);
newVel += vec2f(-vortex, vortex) * curlAmount;
}
// Add forces from mouse
let fx = f32(x) / f32(size);
let fy = f32(y) / f32(size);
let dist = distance(vec2f(fx, fy), vec2f(u.mouseX, u.mouseY));
let radius = 0.02 + (u.pressure * 0.1); // Pressure affects brush size
if (dist < radius && u.mousePressed > 0.5) {
let strength = 1.0 - dist / radius;
// Force affects velocity strength
let forceMultiplier = u.velX * u.velX + u.velY * u.velY;
newVel += vec2f(u.velX, u.velY) * strength * u.dt * 2.0;
// Add density/color using palette
let colorHue = strength + u.time * 0.1;
let color = getPaletteColor(colorHue, paletteId);
newDens += vec4f(color * strength * 3.0, strength * 3.0);
}
// Apply pressure (affects how much velocity is preserved)
newVel *= u.pressure;
// Decay
newVel *= 0.995;
newDens *= 0.992;
// Boundary conditions
if (x <= 1 || x >= size - 2 || y <= 1 || y >= size - 2) {
newVel *= 0.5;
}
velOut[i] = newVel;
densOut[i] = newDens;
}
`;
}
getRenderShaderCode() {
return /* wgsl */ `
struct Uniforms {
gridSize: f32,
dt: f32,
viscosity: f32,
diffusion: f32,
mouseX: f32,
mouseY: f32,
velX: f32,
velY: f32,
mousePressed: f32,
style: f32,
palette: f32,
curl: f32,
pressure: f32,
doBloom: f32,
doVortex: f32,
time: f32,
}
@group(0) @binding(0) var<uniform> u: Uniforms;
@group(0) @binding(1) var<storage, read> velocity: array<vec2f>;
@group(0) @binding(2) var<storage, read> density: array<vec4f>;
struct VertexOutput {
@builtin(position) position: vec4f,
@location(0) uv: vec2f,
}
@vertex
fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
var pos = array<vec2f, 3>(
vec2f(-1.0, -1.0),
vec2f(3.0, -1.0),
vec2f(-1.0, 3.0)
);
var output: VertexOutput;
output.position = vec4f(pos[vertexIndex], 0.0, 1.0);
output.uv = pos[vertexIndex] * 0.5 + 0.5;
return output;
}
fn getPaletteColor(t: f32, paletteId: i32) -> vec3f {
let tt = fract(t);
if (paletteId == 0) { // Ivy Green
return vec3f(0.1 + 0.2 * tt, 0.5 + 0.5 * tt, 0.2 + 0.3 * tt);
} else if (paletteId == 1) { // Rainbow
return vec3f(
0.5 + 0.5 * cos(6.28318 * (tt + 0.0)),
0.5 + 0.5 * cos(6.28318 * (tt + 0.33)),
0.5 + 0.5 * cos(6.28318 * (tt + 0.67))
);
} else if (paletteId == 2) { // Fire
return vec3f(min(1.0, tt * 2.5), tt * tt, tt * tt * tt * 0.3);
} else if (paletteId == 3) { // Ocean
return vec3f(0.0 + 0.2 * tt, 0.3 + 0.4 * tt, 0.6 + 0.4 * tt);
} else if (paletteId == 4) { // Neon
return vec3f(
0.5 + 0.5 * sin(tt * 12.56),
0.5 + 0.5 * sin(tt * 12.56 + 2.094),
0.5 + 0.5 * sin(tt * 12.56 + 4.188)
);
} else if (paletteId == 5) { // Sunset
return vec3f(0.9 - 0.2 * tt, 0.3 + 0.4 * tt, 0.3 + 0.5 * tt);
} else if (paletteId == 6) { // Cosmic
return vec3f(
0.2 + 0.5 * sin(tt * 6.28),
0.1 + 0.3 * sin(tt * 6.28 + 2.0),
0.5 + 0.5 * sin(tt * 6.28 + 4.0)
);
} else { // Monochrome
return vec3f(tt, tt, tt);
}
}
@fragment
fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
let size = i32(u.gridSize);
let x = i32(input.uv.x * f32(size));
let y = i32(input.uv.y * f32(size));
let i = u32(clamp(y, 0, size - 1) * size + clamp(x, 0, size - 1));
let d = density[i];
let v = velocity[i];
let style = i32(u.style);
let paletteId = i32(u.palette);
let speed = length(v);
let dens = length(d.rgb);
var color = vec3f(0.0);
// Show mouse position as a dot for visual feedback
let mouseDist = distance(input.uv, vec2f(u.mouseX, u.mouseY));
let mouseGlow = smoothstep(0.08, 0.0, mouseDist) * 0.5;
// Style-based rendering
if (style == 0) { // Classic - use density color directly
color = d.rgb;
} else if (style == 1) { // Ivy Flow - organic green tones
let hue = dens * 0.3 + speed * 0.1;
color = getPaletteColor(hue, paletteId);
color *= dens * 1.5;
} else if (style == 2) { // Ink Drop - high contrast
color = getPaletteColor(dens + speed * 0.2, paletteId);
color = pow(color * dens, vec3f(0.8));
} else if (style == 3) { // Smoke - soft gradient
let smoke = smoothstep(0.0, 1.0, dens);
color = mix(vec3f(0.02), getPaletteColor(speed * 0.5, paletteId), smoke);
} else if (style == 4) { // Plasma - vibrant swirls
let plasma = sin(dens * 10.0 + u.time) * 0.5 + 0.5;
color = getPaletteColor(plasma + speed * 0.3, paletteId);
color *= dens * 2.0;
} else { // Watercolor - soft bleeding edges
let wc = smoothstep(0.0, 0.5, dens);
color = getPaletteColor(dens * 0.5 + u.time * 0.05, paletteId) * wc;
color = mix(color, vec3f(1.0), (1.0 - wc) * 0.1);
}
// Velocity-based highlights
color += getPaletteColor(0.8, paletteId) * speed * 0.15;
// Add mouse indicator
color += getPaletteColor(u.time * 0.2, paletteId) * mouseGlow;
// Vortex visualization
if (u.doVortex > 0.5) {
// Approximate curl from velocity
let curlVis = abs(v.x - v.y) * 0.5;
color += vec3f(curlVis * 0.3, curlVis * 0.1, curlVis * 0.4);
}
// Bloom effect
if (u.doBloom > 0.5) {
let bloom = max(0.0, dens - 0.5) * 2.0;
color += color * bloom * 0.5;
color = color / (1.0 + color * 0.3); // Tone mapping
}
return vec4f(color, 1.0);
}
`;
}
}
// Export
window.FluidRenderer = FluidRenderer;