573 lines
16 KiB
TypeScript
573 lines
16 KiB
TypeScript
"use client";
|
|
|
|
import { useFBO } from "@react-three/drei";
|
|
import { Canvas, useFrame, useThree } from "@react-three/fiber";
|
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
import * as THREE from "three";
|
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
interface UniformMap {
|
|
[key: string]: { value: unknown };
|
|
}
|
|
|
|
interface ShaderBackgroundProps {
|
|
vertexShader?: string;
|
|
fragmentShader?: string;
|
|
uniforms?: UniformMap;
|
|
className?: string;
|
|
color?: string;
|
|
}
|
|
|
|
const defaultVertexShader = `
|
|
varying vec2 vUv;
|
|
void main() {
|
|
vUv = uv;
|
|
gl_Position = vec4(position, 1.0);
|
|
}
|
|
`;
|
|
|
|
const defaultFragmentShader = `
|
|
precision highp float;
|
|
|
|
varying vec2 vUv;
|
|
uniform float u_time;
|
|
uniform vec3 u_resolution;
|
|
uniform sampler2D u_fluid;
|
|
uniform float u_distortionAmount;
|
|
uniform vec3 u_color;
|
|
|
|
#define time u_time
|
|
|
|
const float arrow_density = 4.5;
|
|
const float arrow_length = .45;
|
|
|
|
const int iterationTime1 = 20;
|
|
const int iterationTime2 = 20;
|
|
const int vector_field_mode = 0;
|
|
const float scale = 6.;
|
|
|
|
const float velocity_x = 0.1;
|
|
const float velocity_y = 0.2;
|
|
|
|
const float mode_2_speed = 2.5;
|
|
const float mode_1_detail = 200.;
|
|
const float mode_1_twist = 50.;
|
|
|
|
const bool isArraw = true;
|
|
|
|
const vec3 luma = vec3(0.2126, 0.7152, 0.0722);
|
|
|
|
float f(in vec2 p)
|
|
{
|
|
return tanh(p.x+sin(p.y+time*velocity_x)) * sin(p.y*p.x*0.1+time*velocity_y);
|
|
}
|
|
|
|
struct Field {
|
|
vec2 vel;
|
|
vec2 pos;
|
|
};
|
|
|
|
Field field(in vec2 p,in int mode)
|
|
{
|
|
Field field;
|
|
if(mode == 0){
|
|
vec2 ep = vec2(0.05,0.);
|
|
vec2 rz= vec2(0);
|
|
for( int i=0; i<iterationTime1; i++ )
|
|
{
|
|
float t0 = f(p);
|
|
float t1 = f(p + ep.xy);
|
|
float t2 = f(p + ep.yx);
|
|
vec2 g = vec2((t1-t0), (t2-t0))/ep.xx;
|
|
vec2 t = vec2(-g.y,g.x);
|
|
|
|
p += (mode_1_twist*0.01)*t + g*(1./mode_1_detail);
|
|
p.x = p.x + sin( time*mode_2_speed/10.)/10.;
|
|
p.y = p.y + cos(time*mode_2_speed/10.)/10.;
|
|
rz= g;
|
|
}
|
|
field.vel = rz;
|
|
return field;
|
|
}
|
|
|
|
if(mode == 1){
|
|
vec2 ep = vec2(0.05,0.);
|
|
vec2 rz= vec2(0);
|
|
for( int i=0; i<iterationTime1; i++ )
|
|
{
|
|
float t0 = f(p);
|
|
float t1 = f(p + ep.xy);
|
|
float t2 = f(p + ep.yx);
|
|
vec2 g = vec2((t1-t0), (t2-t0))/ep.xx;
|
|
vec2 t = vec2(-g.y,g.x);
|
|
|
|
p += (mode_1_twist*0.01)*t + g*(1./mode_1_detail);
|
|
p.x = p.x + sin( time*mode_2_speed/10.)/10.;
|
|
p.y = p.y + cos(time*mode_2_speed/10.)/10.;
|
|
rz= g;
|
|
}
|
|
|
|
field.vel = rz;
|
|
for(int i=1; i<iterationTime2; i++){
|
|
p.x+=0.3/float(i)*sin(float(i)*3.*p.y+time*mode_2_speed) + 0.5;
|
|
p.y+=0.3/float(i)*cos(float(i)*3.*p.x + time*mode_2_speed) + 0.5;
|
|
}
|
|
field.pos = p;
|
|
return field;
|
|
}
|
|
|
|
return field;
|
|
}
|
|
|
|
float segm(in vec2 p, in vec2 a, in vec2 b)
|
|
{
|
|
vec2 pa = p - a;
|
|
vec2 ba = b - a;
|
|
float h = clamp(dot(pa,ba)/dot(ba,ba), 0., 1.);
|
|
return length(pa - ba*h)*20.*arrow_density;
|
|
}
|
|
|
|
float fieldviz(in vec2 p,in int mode)
|
|
{
|
|
vec2 ip = floor(p*arrow_density)/arrow_density + .5/arrow_density;
|
|
vec2 t = field(ip,mode).vel;
|
|
float m = min(0.1,pow(length(t),0.5)*(arrow_length/arrow_density));
|
|
vec2 b = normalize(t)*m;
|
|
float rz = segm(p, ip, ip+b);
|
|
vec2 prp = (vec2(-b.y,b.x));
|
|
rz = min(rz,segm(p, ip+b, ip+b*0.65+prp*0.3));
|
|
return clamp(min(rz,segm(p, ip+b, ip+b*0.65-prp*0.3)),0.,1.);
|
|
}
|
|
|
|
vec3 getRGB(in Field fld,in int mode){
|
|
|
|
if(mode == 0){
|
|
vec2 p = fld.vel;
|
|
float magnitude = length(p);
|
|
float angle = atan(p.y, p.x);
|
|
float pulse = 0.5 + 0.5 * sin(angle * 2.0 + time * 0.75);
|
|
float glow = smoothstep(0.0, 1.2, magnitude) * pow(pulse, 2.0);
|
|
vec3 base = u_color * 0.03;
|
|
vec3 accent = u_color * 0.9;
|
|
return clamp(base + accent * glow, 0.0, 1.0);
|
|
}
|
|
|
|
if(mode == 1){
|
|
vec2 p = fld.pos;
|
|
float ripple = sin(p.x * 1.5 + time * 0.4) * 0.5 + 0.5;
|
|
float swirl = cos(p.y * 1.5 - time * 0.6) * 0.5 + 0.5;
|
|
vec3 base = u_color * 0.02;
|
|
vec3 accent = u_color * 0.8;
|
|
return clamp(base + accent * pow(ripple * swirl, 1.5), 0.0, 1.0);
|
|
}
|
|
|
|
return vec3(0.0);
|
|
}
|
|
|
|
void mainImage( out vec4 fragColor, in vec2 fragCoord )
|
|
{
|
|
vec2 p = fragCoord.xy / u_resolution.xy-0.5 ;
|
|
p.x *= u_resolution.x/u_resolution.y;
|
|
p *= scale;
|
|
|
|
vec3 col;
|
|
int vector_mode = 0;
|
|
Field fld = field(p,vector_mode);
|
|
col = getRGB(fld,vector_mode);
|
|
col = mix(col, col * 1.1, 0.25);
|
|
col = pow(col, vec3(1.2));
|
|
|
|
fragColor = vec4(col,1.0);
|
|
}
|
|
|
|
void main() {
|
|
vec2 fluid = texture2D(u_fluid, vUv).xy;
|
|
vec2 warpedUv = vUv + fluid * (0.5 * u_distortionAmount);
|
|
vec2 warpedFragCoord = warpedUv * u_resolution.xy;
|
|
|
|
vec4 col = vec4(0.0);
|
|
mainImage(col, warpedFragCoord);
|
|
|
|
gl_FragColor = col;
|
|
}
|
|
`;
|
|
|
|
const simulationVertexShader = `
|
|
varying vec2 vUv;
|
|
void main() {
|
|
vUv = uv;
|
|
gl_Position = vec4(position, 1.0);
|
|
}
|
|
`;
|
|
|
|
const fluidFragmentShader = `
|
|
uniform float u_time;
|
|
uniform vec2 u_resolution;
|
|
uniform vec4 u_mouse;
|
|
uniform int u_frame;
|
|
uniform sampler2D iPreviousFrame;
|
|
uniform float uBrushSize;
|
|
uniform float uBrushStrength;
|
|
uniform float uFluidDecay;
|
|
uniform float uTrailLength;
|
|
uniform float uStopDecay;
|
|
varying vec2 vUv;
|
|
|
|
vec2 ur, U;
|
|
|
|
float ln(vec2 p, vec2 a, vec2 b) {
|
|
return length(p-a-(b-a)*clamp(dot(p-a,b-a)/dot(b-a,b-a),0.,1.));
|
|
}
|
|
|
|
vec4 t(vec2 v, int a, int b) {
|
|
return texture2D(iPreviousFrame, fract((v+vec2(float(a),float(b)))/ur));
|
|
}
|
|
|
|
vec4 t(vec2 v) {
|
|
return texture2D(iPreviousFrame, fract(v/ur));
|
|
}
|
|
|
|
float area(vec2 a, vec2 b, vec2 c) {
|
|
float A = length(b-c), B = length(c-a), C = length(a-b), s = 0.5*(A+B+C);
|
|
return sqrt(s*(s-A)*(s-B)*(s-C));
|
|
}
|
|
|
|
void main() {
|
|
U = vUv * u_resolution;
|
|
ur = u_resolution.xy;
|
|
|
|
if (u_frame < 1) {
|
|
float w = 0.5+sin(0.2*U.x)*0.5;
|
|
float q = length(U-0.5*ur);
|
|
gl_FragColor = vec4(0.1*exp(-0.001*q*q),0,0,w);
|
|
} else {
|
|
vec2 v = U,
|
|
A = v + vec2( 1, 1),
|
|
B = v + vec2( 1,-1),
|
|
C = v + vec2(-1, 1),
|
|
D = v + vec2(-1,-1);
|
|
|
|
for (int i = 0; i < 8; i++) {
|
|
v -= t(v).xy;
|
|
A -= t(A).xy;
|
|
B -= t(B).xy;
|
|
C -= t(C).xy;
|
|
D -= t(D).xy;
|
|
}
|
|
|
|
vec4 me = t(v);
|
|
vec4 n = t(v, 0, 1),
|
|
e = t(v, 1, 0),
|
|
s = t(v, 0, -1),
|
|
w = t(v, -1, 0);
|
|
vec4 ne = .25*(n+e+s+w);
|
|
me = mix(t(v), ne, vec4(0.15,0.15,0.95,0.));
|
|
me.z = me.z - 0.01*((area(A,B,C)+area(B,C,D))-4.);
|
|
|
|
vec4 pr = vec4(e.z,w.z,n.z,s.z);
|
|
me.xy = me.xy + 100.*vec2(pr.x-pr.y, pr.z-pr.w)/ur;
|
|
|
|
me.xy *= uFluidDecay;
|
|
me.z *= uTrailLength;
|
|
|
|
if (u_mouse.z > 0.0) {
|
|
vec2 mousePos = u_mouse.xy;
|
|
vec2 mousePrev = u_mouse.zw;
|
|
vec2 mouseVel = mousePos - mousePrev;
|
|
float velMagnitude = length(mouseVel);
|
|
float q = ln(U, mousePos, mousePrev);
|
|
vec2 m = mousePos - mousePrev;
|
|
float l = length(m);
|
|
if (l > 0.0) m = min(l, 10.0) * m / l;
|
|
|
|
float brushSizeFactor = 1e-4 / uBrushSize;
|
|
float strengthFactor = 0.03 * uBrushStrength;
|
|
|
|
float falloff = exp(-brushSizeFactor*q*q*q);
|
|
falloff = pow(falloff, 0.5);
|
|
|
|
me.xyw += strengthFactor * falloff * vec3(m, 10.);
|
|
|
|
if (velMagnitude < 2.0) {
|
|
float distToCursor = length(U - mousePos);
|
|
float influence = exp(-distToCursor * 0.01);
|
|
float cursorDecay = mix(1.0, uStopDecay, influence);
|
|
me.xy *= cursorDecay;
|
|
me.z *= cursorDecay;
|
|
}
|
|
}
|
|
|
|
gl_FragColor = clamp(me, -0.4, 0.4);
|
|
}
|
|
}
|
|
`;
|
|
|
|
const config = {
|
|
brushSize: 12.0,
|
|
brushStrength: 0.45,
|
|
fluidDecay: 0.95,
|
|
trailLength: 0.85,
|
|
stopDecay: 0.85,
|
|
distortionAmount: 1.5,
|
|
lerpFactor: 0.15,
|
|
};
|
|
|
|
interface SimulationProps {
|
|
vertexShader: string;
|
|
fragmentShader: string;
|
|
uniforms: UniformMap;
|
|
color: string;
|
|
}
|
|
|
|
const ShaderPlane = ({
|
|
vertexShader,
|
|
fragmentShader,
|
|
uniforms,
|
|
color,
|
|
}: SimulationProps) => {
|
|
const { size, gl } = useThree();
|
|
|
|
const fluidMaterial = useRef<THREE.ShaderMaterial | null>(null);
|
|
const displayMaterial = useRef<THREE.ShaderMaterial | null>(null);
|
|
const fluidMeshRef = useRef<THREE.Mesh | null>(null);
|
|
const displayMeshRef = useRef<THREE.Mesh | null>(null);
|
|
|
|
const fluidTarget1 = useFBO(size.width, size.height, {
|
|
minFilter: THREE.LinearFilter,
|
|
magFilter: THREE.LinearFilter,
|
|
format: THREE.RGBAFormat,
|
|
type: THREE.FloatType,
|
|
});
|
|
const fluidTarget2 = useFBO(size.width, size.height, {
|
|
minFilter: THREE.LinearFilter,
|
|
magFilter: THREE.LinearFilter,
|
|
format: THREE.RGBAFormat,
|
|
type: THREE.FloatType,
|
|
});
|
|
|
|
const currentFluidTarget = useRef(fluidTarget1);
|
|
const previousFluidTarget = useRef(fluidTarget2);
|
|
const frameCount = useRef(0);
|
|
|
|
const mouse = useRef({ x: 0, y: 0, prevX: 0, prevY: 0 });
|
|
const targetMouse = useRef({ x: 0, y: 0 });
|
|
const smoothMouse = useRef({ x: 0, y: 0 });
|
|
|
|
useEffect(() => {
|
|
const halfX = size.width / 2;
|
|
const halfY = size.height / 2;
|
|
|
|
mouse.current = { x: halfX, y: halfY, prevX: halfX, prevY: halfY };
|
|
targetMouse.current = { x: halfX, y: halfY };
|
|
smoothMouse.current = { x: halfX, y: halfY };
|
|
}, [size]);
|
|
|
|
useEffect(() => {
|
|
const { domElement } = gl;
|
|
|
|
const handleMouseMove = (event: MouseEvent) => {
|
|
const rect = domElement.getBoundingClientRect();
|
|
targetMouse.current.x = event.clientX - rect.left;
|
|
targetMouse.current.y = rect.height - (event.clientY - rect.top);
|
|
};
|
|
|
|
const handleMouseLeave = () => {};
|
|
|
|
domElement.addEventListener("mousemove", handleMouseMove);
|
|
domElement.addEventListener("mouseleave", handleMouseLeave);
|
|
|
|
return () => {
|
|
domElement.removeEventListener("mousemove", handleMouseMove);
|
|
domElement.removeEventListener("mouseleave", handleMouseLeave);
|
|
};
|
|
}, [gl]);
|
|
|
|
const fluidUniforms = useMemo(
|
|
() => ({
|
|
u_time: { value: 0 },
|
|
u_resolution: { value: new THREE.Vector2(size.width, size.height) },
|
|
u_mouse: { value: new THREE.Vector4(0, 0, 0, 0) },
|
|
u_frame: { value: 0 },
|
|
iPreviousFrame: { value: null },
|
|
uBrushSize: { value: config.brushSize },
|
|
uBrushStrength: { value: config.brushStrength },
|
|
uFluidDecay: { value: config.fluidDecay },
|
|
uTrailLength: { value: config.trailLength },
|
|
uStopDecay: { value: config.stopDecay },
|
|
}),
|
|
[size.width, size.height],
|
|
);
|
|
|
|
const displayUniforms = useMemo(
|
|
() => ({
|
|
u_time: { value: 0 },
|
|
u_resolution: { value: new THREE.Vector3(size.width, size.height, 1) },
|
|
u_fluid: { value: null },
|
|
u_distortionAmount: { value: config.distortionAmount },
|
|
u_color: { value: new THREE.Color(color) },
|
|
...uniforms,
|
|
}),
|
|
[size.width, size.height, uniforms, color],
|
|
);
|
|
|
|
useEffect(() => {
|
|
frameCount.current = 0;
|
|
fluidTarget1.setSize(size.width, size.height);
|
|
fluidTarget2.setSize(size.width, size.height);
|
|
|
|
if (fluidMaterial.current) {
|
|
const resolution = fluidMaterial.current.uniforms.u_resolution
|
|
.value as THREE.Vector2;
|
|
resolution.set(size.width, size.height);
|
|
}
|
|
|
|
if (displayMaterial.current) {
|
|
const resolution = displayMaterial.current.uniforms.u_resolution
|
|
.value as THREE.Vector3;
|
|
resolution.set(size.width, size.height, 1);
|
|
}
|
|
}, [size, fluidTarget1, fluidTarget2]);
|
|
|
|
useEffect(() => {
|
|
if (displayMaterial.current) {
|
|
displayMaterial.current.uniforms.u_fluid.value =
|
|
currentFluidTarget.current.texture;
|
|
}
|
|
}, []);
|
|
|
|
useFrame(({ gl, clock, camera }) => {
|
|
const time = clock.getElapsedTime();
|
|
|
|
mouse.current.prevX = smoothMouse.current.x;
|
|
mouse.current.prevY = smoothMouse.current.y;
|
|
|
|
smoothMouse.current.x +=
|
|
(targetMouse.current.x - smoothMouse.current.x) * config.lerpFactor;
|
|
smoothMouse.current.y +=
|
|
(targetMouse.current.y - smoothMouse.current.y) * config.lerpFactor;
|
|
|
|
mouse.current.x = smoothMouse.current.x;
|
|
mouse.current.y = smoothMouse.current.y;
|
|
|
|
if (fluidMaterial.current) {
|
|
fluidMaterial.current.uniforms.u_time.value = time;
|
|
fluidMaterial.current.uniforms.u_frame.value = frameCount.current;
|
|
fluidMaterial.current.uniforms.u_mouse.value.set(
|
|
mouse.current.x,
|
|
mouse.current.y,
|
|
mouse.current.prevX,
|
|
mouse.current.prevY,
|
|
);
|
|
fluidMaterial.current.uniforms.u_resolution.value.set(
|
|
size.width,
|
|
size.height,
|
|
);
|
|
}
|
|
|
|
if (fluidMaterial.current && fluidMeshRef.current) {
|
|
fluidMaterial.current.uniforms.iPreviousFrame.value =
|
|
previousFluidTarget.current.texture;
|
|
gl.setRenderTarget(currentFluidTarget.current);
|
|
gl.render(fluidMeshRef.current, camera);
|
|
}
|
|
|
|
if (displayMaterial.current) {
|
|
displayMaterial.current.uniforms.u_time.value = time;
|
|
displayMaterial.current.uniforms.u_resolution.value.set(
|
|
size.width,
|
|
size.height,
|
|
1,
|
|
);
|
|
displayMaterial.current.uniforms.u_fluid.value =
|
|
currentFluidTarget.current.texture;
|
|
}
|
|
|
|
if (displayMaterial.current && displayMeshRef.current) {
|
|
gl.setRenderTarget(null);
|
|
gl.render(displayMeshRef.current, camera);
|
|
}
|
|
|
|
const temp = currentFluidTarget.current;
|
|
currentFluidTarget.current = previousFluidTarget.current;
|
|
previousFluidTarget.current = temp;
|
|
|
|
frameCount.current += 1;
|
|
});
|
|
|
|
const fluidPlane = useMemo(
|
|
() => (
|
|
<mesh ref={fluidMeshRef}>
|
|
<planeGeometry args={[2, 2]} />
|
|
<shaderMaterial
|
|
ref={fluidMaterial}
|
|
uniforms={fluidUniforms}
|
|
vertexShader={simulationVertexShader}
|
|
fragmentShader={fluidFragmentShader}
|
|
/>
|
|
</mesh>
|
|
),
|
|
[fluidUniforms],
|
|
);
|
|
|
|
const displayPlane = useMemo(
|
|
() => (
|
|
<mesh ref={displayMeshRef}>
|
|
<planeGeometry args={[2, 2]} />
|
|
<shaderMaterial
|
|
ref={displayMaterial}
|
|
uniforms={displayUniforms}
|
|
vertexShader={vertexShader}
|
|
fragmentShader={fragmentShader}
|
|
side={THREE.FrontSide}
|
|
depthTest={false}
|
|
depthWrite={false}
|
|
/>
|
|
</mesh>
|
|
),
|
|
[displayUniforms, fragmentShader, vertexShader],
|
|
);
|
|
|
|
return (
|
|
<>
|
|
{fluidPlane}
|
|
{displayPlane}
|
|
</>
|
|
);
|
|
};
|
|
|
|
const Shader7 = ({
|
|
vertexShader = defaultVertexShader,
|
|
fragmentShader = defaultFragmentShader,
|
|
uniforms = {},
|
|
className,
|
|
color = "#bbffcc",
|
|
}: ShaderBackgroundProps) => {
|
|
const [simKey, setSimKey] = useState(0);
|
|
|
|
useEffect(() => {
|
|
const handleResize = () => setSimKey((prev) => prev + 1);
|
|
window.addEventListener("resize", handleResize);
|
|
return () => window.removeEventListener("resize", handleResize);
|
|
}, []);
|
|
|
|
return (
|
|
<section
|
|
className={cn(className, "absolute inset-0 h-screen w-full")}
|
|
color={color}
|
|
>
|
|
<Canvas>
|
|
<ShaderPlane
|
|
key={simKey}
|
|
vertexShader={vertexShader}
|
|
fragmentShader={fragmentShader}
|
|
uniforms={uniforms}
|
|
color={color}
|
|
/>
|
|
</Canvas>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
export { Shader7 };
|