From 44085641b11bd3ba2261b370f17f4a05e345dc82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Roche?= Date: Mon, 20 Jan 2025 15:02:33 +0100 Subject: [PATCH] flowmap --- .vscode/settings.json | 3 + components/animated-gradient/material.ts | 42 ++-- components/animated-gradient/webgl.tsx | 11 +- libs/webgl/components/canvas/webgl.tsx | 5 +- libs/webgl/components/flowmap/index.tsx | 116 ++++------- libs/webgl/utils/blend.ts | 13 ++ libs/webgl/utils/flowmap.ts | 192 ++++++++++++++++++ .../utils/{fluid-simulation.js => fluid.js} | 61 +++--- libs/webgl/utils/program.ts | 6 +- 9 files changed, 318 insertions(+), 131 deletions(-) create mode 100644 libs/webgl/utils/flowmap.ts rename libs/webgl/utils/{fluid-simulation.js => fluid.js} (94%) diff --git a/.vscode/settings.json b/.vscode/settings.json index 50248034..00959cb4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -25,5 +25,8 @@ }, "[javascriptreact]": { "editor.defaultFormatter": "biomejs.biome" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "biomejs.biome" } } diff --git a/components/animated-gradient/material.ts b/components/animated-gradient/material.ts index 15f441c2..68038760 100644 --- a/components/animated-gradient/material.ts +++ b/components/animated-gradient/material.ts @@ -4,7 +4,9 @@ import { Vector2, type WebGLProgramParametersWithUniforms, } from 'three' +import type { Fluid } from '~/libs/webgl/utils/fluid' import { NOISE } from '~/libs/webgl/utils/noise' +import type { Flowmap } from './../../libs/webgl/utils/flowmap' export class AnimatedGradientMaterial extends MeshBasicMaterial { private uniforms: { @@ -18,7 +20,12 @@ export class AnimatedGradientMaterial extends MeshBasicMaterial { uColorsTexture: { value: Texture | null } uOffset: { value: number } uQuantize: { value: number } - uFlowmap: { value: Texture | null } + uFlowmap: + | Flowmap + | Fluid + | { + value: null + } uDpr: { value: number } } @@ -38,12 +45,22 @@ export class AnimatedGradientMaterial extends MeshBasicMaterial { colorFrequency = 0.33, quantize = 0, radial = false, - flowmap = true, - } = {}) { + flowmap, + }: { + frequency?: number + amplitude?: number + colorAmplitude?: number + colorFrequency?: number + quantize?: number + radial?: boolean + flowmap?: Flowmap | Fluid + }) { super({ transparent: true, }) + console.log(flowmap.uniform) + this.uniforms = { uTime: { value: 0 }, uAmplitude: { value: amplitude }, @@ -55,12 +72,15 @@ export class AnimatedGradientMaterial extends MeshBasicMaterial { uColorsTexture: { value: null }, uOffset: { value: radial ? Math.random() * 1000 : 0 }, uQuantize: { value: quantize }, - uFlowmap: { value: null }, + uFlowmap: flowmap?.uniform || { + value: null, + }, uDpr: { value: 1 }, } + this.defines = { USE_RADIAL: radial, - USE_FLOWMAP: flowmap, + USE_FLOWMAP: !!flowmap, USE_UV: true, } @@ -127,7 +147,7 @@ export class AnimatedGradientMaterial extends MeshBasicMaterial { # ifdef USE_FLOWMAP vec4 flow = texture2D(uFlowmap, fragCoord / (uResolution.xy * uDpr)); - flow *= 0.00025; + flow *= 0.0025; screenUV += flow.rg; # endif @@ -158,6 +178,8 @@ export class AnimatedGradientMaterial extends MeshBasicMaterial { alpha = alpha - rand(fragCoord) * 0.05; vec4 diffuseColor = vec4( color, alpha ); + + // diffuseColor = texture2D(uFlowmap, fragCoord / (uResolution.xy * uDpr)); ` ) } @@ -221,12 +243,4 @@ export class AnimatedGradientMaterial extends MeshBasicMaterial { set quantize(value) { this.uniforms.uQuantize.value = value } - - get flowmap() { - return this.uniforms.uFlowmap.value - } - - set flowmap(value) { - this.uniforms.uFlowmap.value = value - } } diff --git a/components/animated-gradient/webgl.tsx b/components/animated-gradient/webgl.tsx index 9953abd9..4a4359f3 100644 --- a/components/animated-gradient/webgl.tsx +++ b/components/animated-gradient/webgl.tsx @@ -67,10 +67,12 @@ export function WebGLAnimatedGradient({ colorFrequency = 0.33, quantize = 0, radial = false, - flowmap = true, + flowmap: hasFlowmap = true, colors = ['#ff0000', '#000000'], speed = 1, }: WebGLAnimatedGradientProps) { + const flowmap = useFlowmap('fluid') + const [material] = useState( () => new AnimatedGradientMaterial({ @@ -80,14 +82,10 @@ export function WebGLAnimatedGradient({ colorFrequency, quantize, radial, - flowmap, + flowmap: hasFlowmap && flowmap, }) ) - useFlowmap((texture) => { - material.flowmap = texture - }) - const gradientTexture = useGradient(colors) useEffect(() => { @@ -129,7 +127,6 @@ export function WebGLAnimatedGradient({ const viewport = useThree((state) => state.viewport) useEffect(() => { - console.log(viewport.dpr) material.dpr = viewport.dpr }, [material, viewport]) diff --git a/libs/webgl/components/canvas/webgl.tsx b/libs/webgl/components/canvas/webgl.tsx index 76f394ea..14dfbabc 100644 --- a/libs/webgl/components/canvas/webgl.tsx +++ b/libs/webgl/components/canvas/webgl.tsx @@ -2,6 +2,7 @@ import { OrthographicCamera } from '@react-three/drei' import { Canvas } from '@react-three/fiber' +import { Suspense } from 'react' import { SheetProvider } from '~/libs/theatre' import { FlowmapProvider } from '../flowmap' import { PostProcessing } from '../postprocessing' @@ -54,7 +55,9 @@ export function WebGLCanvas({ {postprocessing && } - + + + diff --git a/libs/webgl/components/flowmap/index.tsx b/libs/webgl/components/flowmap/index.tsx index 13a56be7..5a2fb5be 100644 --- a/libs/webgl/components/flowmap/index.tsx +++ b/libs/webgl/components/flowmap/index.tsx @@ -1,53 +1,33 @@ import { useFrame, useThree } from '@react-three/fiber' import { types } from '@theatre/core' -import { - createContext, - useCallback, - useContext, - useEffect, - useMemo, - useRef, -} from 'react' -import type { Texture } from 'three' +import { createContext, useContext, useMemo } from 'react' import { useCurrentSheet } from '~/libs/theatre' import { useTheatre } from '~/libs/theatre/hooks/use-theatre' -import FluidSimulation from '~/libs/webgl/utils/fluid-simulation' +import { Flowmap } from '~/libs/webgl/utils/flowmap' +import { Fluid } from '~/libs/webgl/utils/fluid' type FlowmapContextType = { - addCallback: (callback: FlowmapCallback) => void - removeCallback: (callback: FlowmapCallback) => void + fluid: Fluid + flowmap: Flowmap } -type FlowmapCallback = (texture: Texture) => void - export const FlowmapContext = createContext( {} as FlowmapContextType ) -export function useFlowmap(callback: FlowmapCallback) { - const { addCallback, removeCallback } = useContext(FlowmapContext) - - // biome-ignore lint/correctness/useExhaustiveDependencies: callback as deps can trigger infinite re-renders - useEffect(() => { - if (!callback) return +export function useFlowmap(type: 'fluid' | 'flowmap' = 'flowmap') { + const { fluid, flowmap } = useContext(FlowmapContext) - addCallback(callback) - - return () => { - removeCallback(callback) - } - }, [addCallback, removeCallback]) - - return useContext(FlowmapContext) + if (type === 'fluid') return fluid + return flowmap } export function FlowmapProvider({ children }: { children: React.ReactNode }) { const gl = useThree((state) => state.gl) - const fluidSimulation = useMemo( - () => new FluidSimulation({ renderer: gl, size: 128 }), - [gl] - ) + const fluid = useMemo(() => new Fluid(gl, { size: 128 }), [gl]) + + const flowmap = useMemo(() => new Flowmap(gl, { size: 128 }), [gl]) const sheet = useCurrentSheet() @@ -75,57 +55,45 @@ export function FlowmapProvider({ children }: { children: React.ReactNode }) { curl: number radius: number }) => { - fluidSimulation.curlStrength = curl - fluidSimulation.densityDissipation = density - fluidSimulation.velocityDissipation = velocity - fluidSimulation.pressureDissipation = pressure - fluidSimulation.radius = radius + fluid.curlStrength = curl + fluid.densityDissipation = density + fluid.velocityDissipation = velocity + fluid.pressureDissipation = pressure + fluid.radius = radius }, - deps: [fluidSimulation], + deps: [fluid], } ) - // const [texture, setTexture] = useState() - - const textureRef = useRef() - - // const getTexture = useCallback(() => textureRef.current, []) - - const callbacksRefs = useRef([]) - - const addCallback = useCallback((callback: FlowmapCallback) => { - callbacksRefs.current.push(callback) - }, []) - - const removeCallback = useCallback((callback: FlowmapCallback) => { - callbacksRefs.current = callbacksRefs.current.filter( - (ref) => ref !== callback - ) - }, []) - - const update = useCallback(() => { - for (const callback of callbacksRefs.current) { - callback(textureRef.current as unknown as Texture) + useTheatre( + sheet, + 'flowmap', + { + falloff: types.number(0.2, { range: [0, 1], nudgeMultiplier: 0.01 }), + dissipation: types.number(0.98, { range: [0, 1], nudgeMultiplier: 0.01 }), + }, + { + onValuesChange: ({ + falloff, + dissipation, + }: { + falloff: number + dissipation: number + }) => { + flowmap.falloff = falloff + flowmap.dissipation = dissipation + }, + deps: [flowmap], } - }, []) - - useFrame(({ gl }) => { - if (callbacksRefs.current.length === 0) return - - textureRef.current = fluidSimulation.update() - update() + ) - gl.setRenderTarget(null) - gl.clear() + useFrame(() => { + fluid.update() + flowmap.update() }, -10) return ( - + {children} ) diff --git a/libs/webgl/utils/blend.ts b/libs/webgl/utils/blend.ts index abce86d2..20ed7506 100644 --- a/libs/webgl/utils/blend.ts +++ b/libs/webgl/utils/blend.ts @@ -20,4 +20,17 @@ export const BLEND = { return (blendColorDodge(base, blend) * opacity + base * (1.0 - opacity)); } `, + ADD: /* glsl */ ` + float blendAdd(float base, float blend) { + return min(base+blend,1.0); + } + + vec3 blendAdd(vec3 base, vec3 blend) { + return min(base+blend,vec3(1.0)); + } + + vec3 blendAdd(vec3 base, vec3 blend, float opacity) { + return (blendAdd(base, blend) * opacity + base * (1.0 - opacity)); + } + `, } as const diff --git a/libs/webgl/utils/flowmap.ts b/libs/webgl/utils/flowmap.ts new file mode 100644 index 00000000..90072ee7 --- /dev/null +++ b/libs/webgl/utils/flowmap.ts @@ -0,0 +1,192 @@ +// https://github.com/alienkitty/alien.js/blob/main/src/three/utils/Flowmap.js + +import { + HalfFloatType, + NoBlending, + ShaderMaterial, + type Texture, + Vector2, + WebGLRenderTarget, + type WebGLRenderer, +} from 'three' +import Program from './program' + +export class Flowmap { + renderer: WebGLRenderer + uniform = { value: null as Texture | null } + mask = { + read: null as WebGLRenderTarget | null, + write: null as WebGLRenderTarget | null, + + // Helper function to ping pong the render targets and update the uniform + swap: () => { + const temp = this.mask.read + this.mask.read = this.mask.write + this.mask.write = temp + this.uniform.value = this.mask.read?.texture ?? null + }, + } + aspect = 1 + lastMouse = new Vector2() + mouse = new Vector2() + targetMouse = new Vector2() + velocity = new Vector2() + program: Program + material: ShaderMaterial + + constructor( + renderer: WebGLRenderer, + { + size = 128, // default size of the render targets + falloff = 0.3, // size of the stamp, percentage of the size + alpha = 1, // opacity of the stamp + dissipation = 0.98, // affects the speed that the stamp fades. Closer to 1 is slower + } = {} + ) { + this.renderer = renderer + + const options = { + type: HalfFloatType, + depthBuffer: false, + } + + this.mask.read = new WebGLRenderTarget(size, size, options) + this.mask.write = new WebGLRenderTarget(size, size, options) + this.mask.swap() + + this.material = new ShaderMaterial({ + vertexShader, + fragmentShader, + uniforms: { + tMap: this.uniform, + + uFalloff: { value: falloff * 0.5 }, + uAlpha: { value: alpha }, + uDissipation: { value: dissipation }, + + // User needs to update these + uAspect: { value: 1 }, + uMouse: { value: this.mouse }, + uVelocity: { value: this.velocity }, + }, + blending: NoBlending, + depthTest: false, + depthWrite: false, + }) + + this.program = new Program(this.material) + + const isTouchCapable = 'ontouchstart' in window + if (isTouchCapable) { + window.addEventListener('touchstart', this.updateMouse, false) + window.addEventListener('touchmove', this.updateMouse, false) + } else { + window.addEventListener('mousemove', this.updateMouse, false) + } + } + + // @ts-ignore + updateMouse = (e) => { + if (e.changedTouches?.length) { + e.x = e.changedTouches[0].pageX + e.y = e.changedTouches[0].pageY + } + if (e.x === undefined) { + e.x = e.pageX + e.y = e.pageY + } + + const viewportSize = this.renderer.getSize(new Vector2()) + this.aspect = viewportSize.width / viewportSize.height + this.material.uniforms.uAspect.value = this.aspect + + const x = e.x / viewportSize.width + const y = 1 - e.y / viewportSize.height + + this.targetMouse.set(x, y) + } + + update() { + const lastVelocity = this.velocity.length() + + this.velocity + .copy(this.targetMouse.clone().sub(this.mouse)) + .multiplyScalar(100) + + this.mouse.lerp(this.targetMouse, lastVelocity === 0 ? 1 : 0.07) + + if (this.velocity.length() < 1) { + this.targetMouse.set(-1, -1) + this.mouse.set(-1, -1) + this.velocity.set(0, 0) + } + + const oldAutoClear = this.renderer.autoClear + this.renderer.autoClear = false + this.renderer.setRenderTarget(this.mask.write) + + this.program.render(this.renderer) + this.mask.swap() + + this.renderer.autoClear = oldAutoClear + this.renderer.setRenderTarget(null) + } + + set falloff(value) { + this.material.uniforms.uFalloff.value = value + } + + get falloff() { + return this.material.uniforms.uFalloff.value + } + + set dissipation(value) { + this.material.uniforms.uDissipation.value = value + } + + get dissipation() { + return this.material.uniforms.uDissipation.value + } +} + +const vertexShader = /* glsl */ ` + // attribute vec2 uv; + // attribute vec2 position; + + varying vec2 vUv; + + void main() { + vUv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); + } +` + +const fragmentShader = /* glsl */ ` + precision highp float; + + uniform sampler2D tMap; + + uniform float uFalloff; + uniform float uAlpha; + uniform float uDissipation; + + uniform float uAspect; + uniform vec2 uMouse; + uniform vec2 uVelocity; + + varying vec2 vUv; + + void main() { + vec4 color = texture2D(tMap, vUv) * uDissipation; + + vec2 cursor = vUv - uMouse; + cursor.x *= uAspect; + + vec3 stamp = vec3(uVelocity * vec2(1, -1), 1.0 - pow(1.0 - min(1.0, length(uVelocity)), 3.0)); + float falloff = smoothstep(uFalloff, 0.0, length(cursor)) * uAlpha; + + color.rgb = mix(color.rgb, stamp, vec3(falloff)); + + gl_FragColor = vec4(color.rgb, 1.0); + } +` diff --git a/libs/webgl/utils/fluid-simulation.js b/libs/webgl/utils/fluid.js similarity index 94% rename from libs/webgl/utils/fluid-simulation.js rename to libs/webgl/utils/fluid.js index 047852b2..29d619cc 100644 --- a/libs/webgl/utils/fluid-simulation.js +++ b/libs/webgl/utils/fluid.js @@ -1,20 +1,17 @@ -/* eslint-disable */ - -// https://github.com/oframe/ogl/blob/master/examples/post-fluid-distortion.html +// https://github.com/alienkitty/alien.js/blob/main/src/three/utils/Fluid.js import { - FloatType, HalfFloatType, LinearFilter, NearestFilter, RGBAFormat, RGFormat, RedFormat, -} from 'three/src/constants' -import { ShaderMaterial } from 'three/src/materials/ShaderMaterial' -import { Vector2 } from 'three/src/math/Vector2' -import { Vector3 } from 'three/src/math/Vector3' -import { WebGLRenderTarget } from 'three/src/renderers/WebGLRenderTarget' + ShaderMaterial, + Vector2, + Vector3, + WebGLRenderTarget, +} from 'three' import Program from './program' @@ -270,10 +267,11 @@ function createDoubleFBO( depthBuffer, stencilBuffer, }), - swap: () => { + swap: (callback) => { const temp = fbo.read fbo.read = fbo.write fbo.write = temp + callback?.(fbo.write.texture) }, } if (internalFormat) { @@ -355,8 +353,8 @@ function getSupportedFormat(gl, internalFormat, format, type) { } } -export default class FluidSimulation { - constructor({ renderer, size = 128 } = {}) { +export class Fluid { + constructor(renderer, { size = 128 } = {}) { this.renderer = renderer // Resolution of simulation this.simRes = size @@ -406,7 +404,13 @@ export default class FluidSimulation { const filtering = supportLinearFiltering ? LinearFilter : NearestFilter - halfFloat = isWebGL2 ? FloatType : HalfFloatType + halfFloat = isWebGL2 + ? HalfFloatType + : gl.getExtension('OES_texture_half_float').HALF_FLOAT_OES + + this.uniform = { + value: null, + } // Create fluid simulation FBOs this.density = createDoubleFBO(this.dyeRes, this.dyeRes, { @@ -581,23 +585,9 @@ export default class FluidSimulation { this.lastMouse = new Vector2() - window.addEventListener('touchstart', this.onMouseDown.bind(this), false) - window.addEventListener('mousedown', this.onMouseDown.bind(this), false) - window.addEventListener('touchstart', this.updateMouse.bind(this), false) window.addEventListener('touchmove', this.updateMouse.bind(this), false) window.addEventListener('mousemove', this.updateMouse.bind(this), false) - - window.addEventListener('touchend', this.onMouseUp.bind(this), false) - window.addEventListener('mouseup', this.onMouseUp.bind(this), false) - } - - onMouseDown() { - this.mouseDown = true - } - - onMouseUp() { - this.mouseDown = false } updateMouse(e) { @@ -657,12 +647,15 @@ export default class FluidSimulation { this.renderer.setRenderTarget(this.density.write) this.splatProgram.render(this.renderer) - this.density.swap() + this.density.swap((texture) => { + this.uniform.value = texture + }) } - update(clock) { + update() { // Perform all of the fluid simulation renders // No need to clear during sim, saving a number of GL calls. + const oldAutoClear = this.renderer.autoClear this.renderer.autoClear = false // Render all of the inputs since last frame @@ -751,11 +744,15 @@ export default class FluidSimulation { this.renderer.setRenderTarget(this.density.write) this.advectionProgram.render(this.renderer) - this.density.swap() + this.density.swap((texture) => { + this.uniform.value = texture + }) // Set clear back to default - this.renderer.autoClear = true + this.renderer.autoClear = oldAutoClear + this.renderer.setRenderTarget(null) + // this.renderer.clear() - return this.density.read.texture + // return this.density.read.texture } } diff --git a/libs/webgl/utils/program.ts b/libs/webgl/utils/program.ts index de77b01e..7f2f1fdd 100644 --- a/libs/webgl/utils/program.ts +++ b/libs/webgl/utils/program.ts @@ -12,9 +12,9 @@ const camera = new OrthographicCamera(1 / -2, 1 / 2, 1 / 2, 1 / -2, 0.001, 1000) camera.position.z = 1 export default class Program extends Scene { - private material: Material - private mesh: Mesh - private scene: Scene + material: Material + mesh: Mesh + scene: Scene constructor(material: Material) { super()