01
WebGL Fluid Simulation
플루이드 커서 글로우
마우스를 움직여보세요
↑ 데모 안에서 마우스를 움직이면 실제 효과가 반응합니다 (WebGL2 필요)
커서가 지나간 자리에 부드럽게 빛나는 분홍 연기가 피어올라 향처럼 말려 흩어집니다. Pavel Dobryakov의 WebGL 유체 시뮬레이션(MIT)을 헤더용으로 이식한 것으로, 실제 물리(속도장·압력·와도) 위에 마우스 이동량을 힘으로 뿌려 만듭니다.
- 포인터 이동량을 속도장에 splat(분사)해 유체를 밀고, 염료(dye)에 고정색 #ffb1ee를 얹어 빛나는 궤적을 남깁니다.
- 그래프 캔버스가 포인터 이벤트를 가지므로, 커서는 window 레벨에서 추적해 이 캔버스 박스로 매핑합니다.
- WebGL2가 없으면 조용히 아무것도 그리지 않고, prefers-reduced-motion이면 실행하지 않습니다(접근성).
header.tsx — 사용법
// 1) 아래 fluid-cursor.tsx 파일을 프로젝트에 넣습니다.
// 2) 헤더 위에 얹기만 하면 됩니다 — pointer-events-none이라 클릭을
// 가로채지 않고, 캔버스가 부모를 가득 채웁니다. WebGL2 + 마우스 이동에만 반응.
import { FluidCursor } from "@/components/fluid-cursor";
export function Header() {
return (
<header className="relative overflow-hidden">
{/* 헤더 콘텐츠 */}
<h1>Artoke</h1>
{/* 커서를 따라 흐르는 빛 — 맨 위 레이어, 비인터랙티브 */}
<div aria-hidden className="pointer-events-none absolute inset-0">
<FluidCursor />
</div>
</header>
);
}fluid-cursor.tsx — 전체 컴포넌트 (복사해서 그대로 사용)
"use client";
import { type RefObject, useEffect, useRef } from "react";
/**
* Glowing cursor smoke — the getdesign.md hero cursor effect: a WebGL fluid
* simulation (Pavel Dobryakov's MIT WebGL-Fluid-Simulation, as ported by
* reactbits "SplashCursor") that emits softly glowing pink smoke along the
* pointer's path; it curls, drifts and dissipates like incense.
*
* Config matches getdesign's instance verbatim: SIM 128, DYE 1440, density
* dissipation 3.5, velocity dissipation 2, pressure 0.1 ×20 iters, curl 3,
* splat radius 0.2, force 6000, shading on, transparent — and their custom
* fixed colour #ffb1ee (rainbow mode off).
*
* The graph canvas above owns pointer events, so pointers are tracked at the
* window level and mapped into this canvas's box. Requires WebGL2; quietly
* renders nothing without it. Honours prefers-reduced-motion by not running.
*/
// --- getdesign's exact config ----------------------------------------------
const SIM_RESOLUTION = 128;
const DYE_RESOLUTION = 1440;
const DENSITY_DISSIPATION = 3.5;
const VELOCITY_DISSIPATION = 2;
const PRESSURE = 0.1;
const PRESSURE_ITERATIONS = 20;
const CURL = 3;
const SPLAT_RADIUS = 0.2;
const SPLAT_FORCE = 6000;
/** #ffb1ee scaled by 0.15, the sim's dye-brightness factor. */
const DYE_COLOR = { r: 1.0 * 0.15, g: 0.694 * 0.15, b: 0.933 * 0.15 };
const BASE_VERT = `
precision highp float;
attribute vec2 aPosition;
varying vec2 vUv;
varying vec2 vL;
varying vec2 vR;
varying vec2 vT;
varying vec2 vB;
uniform vec2 texelSize;
void main () {
vUv = aPosition * 0.5 + 0.5;
vL = vUv - vec2(texelSize.x, 0.0);
vR = vUv + vec2(texelSize.x, 0.0);
vT = vUv + vec2(0.0, texelSize.y);
vB = vUv - vec2(0.0, texelSize.y);
gl_Position = vec4(aPosition, 0.0, 1.0);
}`;
const CLEAR_FRAG = `
precision mediump float;
precision mediump sampler2D;
varying highp vec2 vUv;
uniform sampler2D uTexture;
uniform float value;
void main () { gl_FragColor = value * texture2D(uTexture, vUv); }`;
const SPLAT_FRAG = `
precision highp float;
precision highp sampler2D;
varying vec2 vUv;
uniform sampler2D uTarget;
uniform float aspectRatio;
uniform vec3 color;
uniform vec2 point;
uniform float radius;
void main () {
vec2 p = vUv - point.xy;
p.x *= aspectRatio;
vec3 splat = exp(-dot(p, p) / radius) * color;
vec3 base = texture2D(uTarget, vUv).xyz;
gl_FragColor = vec4(base + splat, 1.0);
}`;
const ADVECTION_FRAG = `
precision highp float;
precision highp sampler2D;
varying vec2 vUv;
uniform sampler2D uVelocity;
uniform sampler2D uSource;
uniform vec2 texelSize;
uniform float dt;
uniform float dissipation;
void main () {
vec2 coord = vUv - dt * texture2D(uVelocity, vUv).xy * texelSize;
vec4 result = texture2D(uSource, coord);
float decay = 1.0 + dissipation * dt;
gl_FragColor = result / decay;
}`;
const DIVERGENCE_FRAG = `
precision mediump float;
precision mediump sampler2D;
varying highp vec2 vUv;
varying highp vec2 vL;
varying highp vec2 vR;
varying highp vec2 vT;
varying highp vec2 vB;
uniform sampler2D uVelocity;
void main () {
float L = texture2D(uVelocity, vL).x;
float R = texture2D(uVelocity, vR).x;
float T = texture2D(uVelocity, vT).y;
float B = texture2D(uVelocity, vB).y;
vec2 C = texture2D(uVelocity, vUv).xy;
if (vL.x < 0.0) { L = -C.x; }
if (vR.x > 1.0) { R = -C.x; }
if (vT.y > 1.0) { T = -C.y; }
if (vB.y < 0.0) { B = -C.y; }
float div = 0.5 * (R - L + T - B);
gl_FragColor = vec4(div, 0.0, 0.0, 1.0);
}`;
const CURL_FRAG = `
precision mediump float;
precision mediump sampler2D;
varying highp vec2 vUv;
varying highp vec2 vL;
varying highp vec2 vR;
varying highp vec2 vT;
varying highp vec2 vB;
uniform sampler2D uVelocity;
void main () {
float L = texture2D(uVelocity, vL).y;
float R = texture2D(uVelocity, vR).y;
float T = texture2D(uVelocity, vT).x;
float B = texture2D(uVelocity, vB).x;
float vorticity = R - L - T + B;
gl_FragColor = vec4(0.5 * vorticity, 0.0, 0.0, 1.0);
}`;
const VORTICITY_FRAG = `
precision highp float;
precision highp sampler2D;
varying vec2 vUv;
varying vec2 vL;
varying vec2 vR;
varying vec2 vT;
varying vec2 vB;
uniform sampler2D uVelocity;
uniform sampler2D uCurl;
uniform float curl;
uniform float dt;
void main () {
float L = texture2D(uCurl, vL).x;
float R = texture2D(uCurl, vR).x;
float T = texture2D(uCurl, vT).x;
float B = texture2D(uCurl, vB).x;
float C = texture2D(uCurl, vUv).x;
vec2 force = 0.5 * vec2(abs(T) - abs(B), abs(R) - abs(L));
force /= length(force) + 0.0001;
force *= curl * C;
force.y *= -1.0;
vec2 velocity = texture2D(uVelocity, vUv).xy;
velocity += force * dt;
velocity = min(max(velocity, -1000.0), 1000.0);
gl_FragColor = vec4(velocity, 0.0, 1.0);
}`;
const PRESSURE_FRAG = `
precision mediump float;
precision mediump sampler2D;
varying highp vec2 vUv;
varying highp vec2 vL;
varying highp vec2 vR;
varying highp vec2 vT;
varying highp vec2 vB;
uniform sampler2D uPressure;
uniform sampler2D uDivergence;
void main () {
float L = texture2D(uPressure, vL).x;
float R = texture2D(uPressure, vR).x;
float T = texture2D(uPressure, vT).x;
float B = texture2D(uPressure, vB).x;
float divergence = texture2D(uDivergence, vUv).x;
float pressure = (L + R + B + T - divergence) * 0.25;
gl_FragColor = vec4(pressure, 0.0, 0.0, 1.0);
}`;
const GRADIENT_SUBTRACT_FRAG = `
precision mediump float;
precision mediump sampler2D;
varying highp vec2 vUv;
varying highp vec2 vL;
varying highp vec2 vR;
varying highp vec2 vT;
varying highp vec2 vB;
uniform sampler2D uPressure;
uniform sampler2D uVelocity;
void main () {
float L = texture2D(uPressure, vL).x;
float R = texture2D(uPressure, vR).x;
float T = texture2D(uPressure, vT).x;
float B = texture2D(uPressure, vB).x;
vec2 velocity = texture2D(uVelocity, vUv).xy;
velocity.xy -= vec2(R - L, T - B);
gl_FragColor = vec4(velocity, 0.0, 1.0);
}`;
// Display with shading + alpha from brightness (TRANSPARENT mode).
const DISPLAY_FRAG = `
precision highp float;
precision highp sampler2D;
varying vec2 vUv;
varying vec2 vL;
varying vec2 vR;
varying vec2 vT;
varying vec2 vB;
uniform sampler2D uTexture;
uniform vec2 texelSize;
void main () {
vec3 c = texture2D(uTexture, vUv).rgb;
vec3 lc = texture2D(uTexture, vL).rgb;
vec3 rc = texture2D(uTexture, vR).rgb;
vec3 tc = texture2D(uTexture, vT).rgb;
vec3 bc = texture2D(uTexture, vB).rgb;
float dx = length(rc) - length(lc);
float dy = length(tc) - length(bc);
vec3 n = normalize(vec3(dx, dy, length(texelSize)));
float diffuse = clamp(dot(n, vec3(0.0, 0.0, 1.0)) + 0.7, 0.7, 1.0);
c *= diffuse;
float a = max(c.r, max(c.g, c.b));
gl_FragColor = vec4(c, a);
}`;
interface FBO {
texture: WebGLTexture;
fbo: WebGLFramebuffer;
width: number;
height: number;
texelSizeX: number;
texelSizeY: number;
attach(id: number): number;
}
interface DoubleFBO {
width: number;
height: number;
texelSizeX: number;
texelSizeY: number;
read: FBO;
write: FBO;
swap(): void;
}
/** Live-tunable params for the effects showcase. Defaults = getdesign's hero. */
export interface FluidConfig {
color: string; // hex dye colour
force: number; // splat push strength
curl: number; // vorticity / swirl
dissipation: number; // how fast the dye fades
}
export const FLUID_DEFAULTS: FluidConfig = {
color: "#ffb1ee",
force: SPLAT_FORCE,
curl: CURL,
dissipation: DENSITY_DISSIPATION,
};
/** hex → rgb 0..1, then scaled by the sim's 0.15 dye-brightness factor. */
function dyeFromHex(hex: string): [number, number, number] {
const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
const n = m ? parseInt(m[1], 16) : 0xffb1ee;
return [((n >> 16) / 255) * 0.15, (((n >> 8) & 255) / 255) * 0.15, ((n & 255) / 255) * 0.15];
}
export function FluidCursor({
configRef,
}: {
configRef?: RefObject<Partial<FluidConfig>>;
}) {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
const gl = canvas.getContext("webgl2", {
alpha: true,
depth: false,
stencil: false,
antialias: false,
preserveDrawingBuffer: false,
}) as WebGL2RenderingContext | null;
if (!gl) return;
gl.getExtension("EXT_color_buffer_float");
const supportLinear = !!gl.getExtension("OES_texture_float_linear");
const filtering = supportLinear ? gl.LINEAR : gl.NEAREST;
// --- tiny GL helpers ---------------------------------------------------
function compileShader(type: number, src: string): WebGLShader | null {
const sh = gl!.createShader(type);
if (!sh) return null;
gl!.shaderSource(sh, src);
gl!.compileShader(sh);
if (!gl!.getShaderParameter(sh, gl!.COMPILE_STATUS)) {
// eslint-disable-next-line no-console
console.error("[fluid] shader:", gl!.getShaderInfoLog(sh));
return null;
}
return sh;
}
const vert = compileShader(gl.VERTEX_SHADER, BASE_VERT);
if (!vert) return;
interface Prog {
program: WebGLProgram;
uniforms: Record<string, WebGLUniformLocation>;
}
function makeProgram(fragSrc: string): Prog | null {
const frag = compileShader(gl!.FRAGMENT_SHADER, fragSrc);
if (!frag) return null;
const program = gl!.createProgram();
if (!program) return null;
gl!.attachShader(program, vert!);
gl!.attachShader(program, frag);
gl!.linkProgram(program);
if (!gl!.getProgramParameter(program, gl!.LINK_STATUS)) return null;
const uniforms: Record<string, WebGLUniformLocation> = {};
const count = gl!.getProgramParameter(program, gl!.ACTIVE_UNIFORMS);
for (let i = 0; i < count; i++) {
const info = gl!.getActiveUniform(program, i);
if (!info) continue;
const loc = gl!.getUniformLocation(program, info.name);
if (loc) uniforms[info.name] = loc;
}
return { program, uniforms };
}
const clearProg = makeProgram(CLEAR_FRAG);
const splatProg = makeProgram(SPLAT_FRAG);
const advectionProg = makeProgram(ADVECTION_FRAG);
const divergenceProg = makeProgram(DIVERGENCE_FRAG);
const curlProg = makeProgram(CURL_FRAG);
const vorticityProg = makeProgram(VORTICITY_FRAG);
const pressureProg = makeProgram(PRESSURE_FRAG);
const gradientProg = makeProgram(GRADIENT_SUBTRACT_FRAG);
const displayProg = makeProgram(DISPLAY_FRAG);
if (
!clearProg || !splatProg || !advectionProg || !divergenceProg ||
!curlProg || !vorticityProg || !pressureProg || !gradientProg ||
!displayProg
)
return;
// Fullscreen quad
const quad = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, quad);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([-1, -1, -1, 1, 1, 1, 1, -1]),
gl.STATIC_DRAW,
);
const elems = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elems);
gl.bufferData(
gl.ELEMENT_ARRAY_BUFFER,
new Uint16Array([0, 1, 2, 0, 2, 3]),
gl.STATIC_DRAW,
);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(0);
function blit(target: FBO | null, doClear = false) {
if (target == null) {
gl!.viewport(0, 0, gl!.drawingBufferWidth, gl!.drawingBufferHeight);
gl!.bindFramebuffer(gl!.FRAMEBUFFER, null);
} else {
gl!.viewport(0, 0, target.width, target.height);
gl!.bindFramebuffer(gl!.FRAMEBUFFER, target.fbo);
}
if (doClear) {
gl!.clearColor(0, 0, 0, 0);
gl!.clear(gl!.COLOR_BUFFER_BIT);
}
gl!.drawElements(gl!.TRIANGLES, 6, gl!.UNSIGNED_SHORT, 0);
}
function createFBO(
w: number,
h: number,
internalFormat: number,
format: number,
type: number,
filter: number,
): FBO {
gl!.activeTexture(gl!.TEXTURE0);
const texture = gl!.createTexture()!;
gl!.bindTexture(gl!.TEXTURE_2D, texture);
gl!.texParameteri(gl!.TEXTURE_2D, gl!.TEXTURE_MIN_FILTER, filter);
gl!.texParameteri(gl!.TEXTURE_2D, gl!.TEXTURE_MAG_FILTER, filter);
gl!.texParameteri(gl!.TEXTURE_2D, gl!.TEXTURE_WRAP_S, gl!.CLAMP_TO_EDGE);
gl!.texParameteri(gl!.TEXTURE_2D, gl!.TEXTURE_WRAP_T, gl!.CLAMP_TO_EDGE);
gl!.texImage2D(
gl!.TEXTURE_2D, 0, internalFormat, w, h, 0, format, type, null,
);
const fbo = gl!.createFramebuffer()!;
gl!.bindFramebuffer(gl!.FRAMEBUFFER, fbo);
gl!.framebufferTexture2D(
gl!.FRAMEBUFFER, gl!.COLOR_ATTACHMENT0, gl!.TEXTURE_2D, texture, 0,
);
gl!.viewport(0, 0, w, h);
gl!.clearColor(0, 0, 0, 0);
gl!.clear(gl!.COLOR_BUFFER_BIT);
return {
texture, fbo, width: w, height: h,
texelSizeX: 1 / w, texelSizeY: 1 / h,
attach(id: number) {
gl!.activeTexture(gl!.TEXTURE0 + id);
gl!.bindTexture(gl!.TEXTURE_2D, texture);
return id;
},
};
}
function createDoubleFBO(
w: number, h: number,
internalFormat: number, format: number, type: number, filter: number,
): DoubleFBO {
let fbo1 = createFBO(w, h, internalFormat, format, type, filter);
let fbo2 = createFBO(w, h, internalFormat, format, type, filter);
return {
width: w, height: h, texelSizeX: 1 / w, texelSizeY: 1 / h,
get read() { return fbo1; },
set read(v) { fbo1 = v; },
get write() { return fbo2; },
set write(v) { fbo2 = v; },
swap() { const t = fbo1; fbo1 = fbo2; fbo2 = t; },
} as unknown as DoubleFBO;
}
function getResolution(resolution: number) {
let aspect = gl!.drawingBufferWidth / gl!.drawingBufferHeight;
if (aspect < 1) aspect = 1 / aspect;
const min = Math.round(resolution);
const max = Math.round(resolution * aspect);
return gl!.drawingBufferWidth > gl!.drawingBufferHeight
? { width: max, height: min }
: { width: min, height: max };
}
let dye: DoubleFBO;
let velocity: DoubleFBO;
let divergence: FBO;
let curl: FBO;
let pressure: DoubleFBO;
function initFramebuffers() {
const simRes = getResolution(SIM_RESOLUTION);
const dyeRes = getResolution(DYE_RESOLUTION);
const texType = gl!.HALF_FLOAT;
dye = createDoubleFBO(
dyeRes.width, dyeRes.height, gl!.RGBA16F, gl!.RGBA, texType, filtering,
);
velocity = createDoubleFBO(
simRes.width, simRes.height, gl!.RG16F, gl!.RG, texType, filtering,
);
divergence = createFBO(
simRes.width, simRes.height, gl!.R16F, gl!.RED, texType, gl!.NEAREST,
);
curl = createFBO(
simRes.width, simRes.height, gl!.R16F, gl!.RED, texType, gl!.NEAREST,
);
pressure = createDoubleFBO(
simRes.width, simRes.height, gl!.R16F, gl!.RED, texType, gl!.NEAREST,
);
}
function resizeCanvas(): boolean {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const w = Math.max(1, Math.floor(canvas!.clientWidth * dpr));
const h = Math.max(1, Math.floor(canvas!.clientHeight * dpr));
if (canvas!.width !== w || canvas!.height !== h) {
canvas!.width = w;
canvas!.height = h;
return true;
}
return false;
}
resizeCanvas();
initFramebuffers();
// --- pointer -----------------------------------------------------------
const pointer = {
texcoordX: 0, texcoordY: 0,
prevTexcoordX: 0, prevTexcoordY: 0,
deltaX: 0, deltaY: 0,
moved: false, started: false,
};
function updatePointer(clientX: number, clientY: number) {
const rect = canvas!.getBoundingClientRect();
if (rect.width <= 0 || rect.height <= 0) return;
const x = (clientX - rect.left) / rect.width;
const y = 1 - (clientY - rect.top) / rect.height;
if (x < 0 || x > 1 || y < 0 || y > 1) { pointer.started = false; return; }
if (!pointer.started) {
pointer.started = true;
pointer.texcoordX = x; pointer.texcoordY = y;
}
pointer.prevTexcoordX = pointer.texcoordX;
pointer.prevTexcoordY = pointer.texcoordY;
pointer.texcoordX = x;
pointer.texcoordY = y;
pointer.deltaX = (x - pointer.prevTexcoordX) * (canvas!.width / canvas!.height);
pointer.deltaY = y - pointer.prevTexcoordY;
if (Math.abs(pointer.deltaX) > 0 || Math.abs(pointer.deltaY) > 0)
pointer.moved = true;
}
const onPointerMove = (e: PointerEvent) => updatePointer(e.clientX, e.clientY);
const onTouchMove = (e: TouchEvent) => {
const t = e.touches[0];
if (t) updatePointer(t.clientX, t.clientY);
};
window.addEventListener("pointermove", onPointerMove, { passive: true });
window.addEventListener("touchmove", onTouchMove, { passive: true });
// --- sim steps ---------------------------------------------------------
function correctRadius(radius: number) {
const aspect = canvas!.width / canvas!.height;
return aspect > 1 ? radius * aspect : radius;
}
function splat(x: number, y: number, dx: number, dy: number) {
gl!.useProgram(splatProg!.program);
gl!.uniform1i(splatProg!.uniforms.uTarget, velocity.read.attach(0));
gl!.uniform1f(splatProg!.uniforms.aspectRatio, canvas!.width / canvas!.height);
gl!.uniform2f(splatProg!.uniforms.point, x, y);
gl!.uniform3f(splatProg!.uniforms.color, dx, dy, 0);
gl!.uniform1f(
splatProg!.uniforms.radius,
correctRadius(SPLAT_RADIUS / 100),
);
blit(velocity.write);
velocity.swap();
gl!.uniform1i(splatProg!.uniforms.uTarget, dye.read.attach(0));
const [dr, dg, db] = dyeFromHex(
configRef?.current?.color ?? FLUID_DEFAULTS.color,
);
gl!.uniform3f(splatProg!.uniforms.color, dr, dg, db);
blit(dye.write);
dye.swap();
}
function step(dt: number) {
gl!.disable(gl!.BLEND);
gl!.useProgram(curlProg!.program);
gl!.uniform2f(
curlProg!.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY,
);
gl!.uniform1i(curlProg!.uniforms.uVelocity, velocity.read.attach(0));
blit(curl);
gl!.useProgram(vorticityProg!.program);
gl!.uniform2f(
vorticityProg!.uniforms.texelSize,
velocity.texelSizeX, velocity.texelSizeY,
);
gl!.uniform1i(vorticityProg!.uniforms.uVelocity, velocity.read.attach(0));
gl!.uniform1i(vorticityProg!.uniforms.uCurl, curl.attach(1));
gl!.uniform1f(vorticityProg!.uniforms.curl, configRef?.current?.curl ?? CURL);
gl!.uniform1f(vorticityProg!.uniforms.dt, dt);
blit(velocity.write);
velocity.swap();
gl!.useProgram(divergenceProg!.program);
gl!.uniform2f(
divergenceProg!.uniforms.texelSize,
velocity.texelSizeX, velocity.texelSizeY,
);
gl!.uniform1i(divergenceProg!.uniforms.uVelocity, velocity.read.attach(0));
blit(divergence);
gl!.useProgram(clearProg!.program);
gl!.uniform1i(clearProg!.uniforms.uTexture, pressure.read.attach(0));
gl!.uniform1f(clearProg!.uniforms.value, PRESSURE);
blit(pressure.write);
pressure.swap();
gl!.useProgram(pressureProg!.program);
gl!.uniform2f(
pressureProg!.uniforms.texelSize,
velocity.texelSizeX, velocity.texelSizeY,
);
gl!.uniform1i(pressureProg!.uniforms.uDivergence, divergence.attach(0));
for (let i = 0; i < PRESSURE_ITERATIONS; i++) {
gl!.uniform1i(pressureProg!.uniforms.uPressure, pressure.read.attach(1));
blit(pressure.write);
pressure.swap();
}
gl!.useProgram(gradientProg!.program);
gl!.uniform2f(
gradientProg!.uniforms.texelSize,
velocity.texelSizeX, velocity.texelSizeY,
);
gl!.uniform1i(gradientProg!.uniforms.uPressure, pressure.read.attach(0));
gl!.uniform1i(gradientProg!.uniforms.uVelocity, velocity.read.attach(1));
blit(velocity.write);
velocity.swap();
gl!.useProgram(advectionProg!.program);
gl!.uniform2f(
advectionProg!.uniforms.texelSize,
velocity.texelSizeX, velocity.texelSizeY,
);
gl!.uniform1i(advectionProg!.uniforms.uVelocity, velocity.read.attach(0));
gl!.uniform1i(advectionProg!.uniforms.uSource, velocity.read.attach(0));
gl!.uniform1f(advectionProg!.uniforms.dt, dt);
gl!.uniform1f(advectionProg!.uniforms.dissipation, VELOCITY_DISSIPATION);
blit(velocity.write);
velocity.swap();
gl!.uniform1i(advectionProg!.uniforms.uVelocity, velocity.read.attach(0));
gl!.uniform1i(advectionProg!.uniforms.uSource, dye.read.attach(1));
gl!.uniform1f(
advectionProg!.uniforms.dissipation,
configRef?.current?.dissipation ?? DENSITY_DISSIPATION,
);
blit(dye.write);
dye.swap();
}
function render() {
gl!.blendFunc(gl!.ONE, gl!.ONE_MINUS_SRC_ALPHA);
gl!.enable(gl!.BLEND);
gl!.useProgram(displayProg!.program);
gl!.uniform2f(
displayProg!.uniforms.texelSize,
1 / canvas!.width, 1 / canvas!.height,
);
gl!.uniform1i(displayProg!.uniforms.uTexture, dye.read.attach(0));
blit(null, true);
}
// --- frame loop --------------------------------------------------------
let raf = 0;
let lastTime = performance.now();
function frame() {
raf = requestAnimationFrame(frame);
const now = performance.now();
let dt = (now - lastTime) / 1000;
dt = Math.min(dt, 0.016666);
lastTime = now;
if (resizeCanvas()) initFramebuffers();
if (pointer.moved) {
pointer.moved = false;
const force = configRef?.current?.force ?? SPLAT_FORCE;
splat(
pointer.texcoordX, pointer.texcoordY,
pointer.deltaX * force, pointer.deltaY * force,
);
}
step(dt);
render();
}
raf = requestAnimationFrame(frame);
const ro = new ResizeObserver(() => {
if (resizeCanvas()) initFramebuffers();
});
ro.observe(canvas);
return () => {
cancelAnimationFrame(raf);
ro.disconnect();
window.removeEventListener("pointermove", onPointerMove);
window.removeEventListener("touchmove", onTouchMove);
};
}, []);
return <canvas ref={canvasRef} className="size-full" aria-hidden />;
}
플루이드 시뮬레이션은 Pavel Dobryakov의 WebGL-Fluid-Simulation(MIT), reactbits SplashCursor 이식을 기반으로 합니다.
← 이펙트 전체 보기