diff --git a/lib/playground/src/pages/post-processing/bloom.astro b/lib/playground/src/pages/post-processing/bloom.astro index 3e5ea2b..3544f80 100644 --- a/lib/playground/src/pages/post-processing/bloom.astro +++ b/lib/playground/src/pages/post-processing/bloom.astro @@ -3,37 +3,48 @@ import Layout from "../../layouts/Layout.astro"; --- - - diff --git a/lib/playground/src/shaders/bloom.ts b/lib/playground/src/shaders/bloom.ts index e4f2825..c6262a4 100644 --- a/lib/playground/src/shaders/bloom.ts +++ b/lib/playground/src/shaders/bloom.ts @@ -14,15 +14,27 @@ export const vertex = /*glsl*/ ` export const fragment = /*glsl*/ ` in vec2 vUv; + float sdCircle(vec2 p, float r) { + return length(p) - r; + } + + vec3 drawCircle(vec2 pos, float radius, vec3 color) { + return smoothstep(radius * 1.01, radius * .99, sdCircle(pos, radius)) * color; + } + void main() { - vec2 uv = gl_PointCoord.xy; - gl_FragColor.rgba = mix(vec4(0.), vec4(vUv*1.2, 1., 1.), smoothstep(0.51, 0.49, length(uv - 0.5))) ; + vec3 color = vec3(0, 0.07, 0.15); + color += drawCircle(vUv - vec2(.4), .1, vec3(vUv, 1.)); + color += drawCircle(vUv - vec2(.65, .65), .02, vec3(vUv, 1.)); + color += drawCircle(vUv - vec2(.75, .4), .04, vec3(vUv, 1.)); + gl_FragColor.rgba = vec4(color, 1.); } `; export const mipmapsShader = /*glsl*/ ` uniform sampler2D u_image; uniform vec2 u_resolution; +uniform float u_threshold; in vec2 vUv; out vec4 outColor; @@ -57,7 +69,8 @@ vec3 mipmapLevel(float octave) { for (int i = 0; i < spread; i++) { for (int j = 0; j < spread; j++) { vec2 off = (vec2(i, j) / u_resolution.xy + vec2(0.0) / u_resolution.xy) * scale / float(spread); - color += texture(u_image, coord + off).rgb; + vec3 imageColor = texture(u_image, coord + off).rgb; + color += max(vec3(0.0), imageColor.rgb - vec3(u_threshold)); weights += 1.0; } @@ -132,6 +145,7 @@ void main() { export const combineShader = /* glsl */ ` uniform sampler2D u_image; uniform sampler2D u_bloomTexture; +uniform float u_mix; uniform vec2 u_resolution; in vec2 vUv; out vec4 outColor; @@ -188,8 +202,8 @@ void main() { outColor = baseColor; float baseColorGreyscale = dot(baseColor.rgb, vec3(0.299, 0.587, 0.114)); + float mixFactor = (bloomColor.a - baseColorGreyscale * baseColor.a) * u_mix; - - outColor = mix(baseColor, bloomColor, bloomColor.a - baseColorGreyscale * baseColor.a); + outColor = mix(baseColor, baseColor + bloomColor, mixFactor); } `; diff --git a/lib/src/hooks/useCompositeEffectPass.ts b/lib/src/hooks/useCompositeEffectPass.ts new file mode 100644 index 0000000..81ddd13 --- /dev/null +++ b/lib/src/hooks/useCompositeEffectPass.ts @@ -0,0 +1,64 @@ +import { createRenderTarget } from "../core/renderTarget"; +import { useLifeCycleCallback } from "../internal/useLifeCycleCallback"; +import type { + CompositeEffectPass, + EffectPass, + RenderCallback, + RenderTarget, + UpdatedCallback, +} from "../types"; + +export function useCompositeEffectPass

>>( + passes: P, +): CompositeEffectPass

{ + const effectPasses = Object.values(passes); + const outputPass = effectPasses.at(-1)!; + + const [beforeRenderCallbacks, onBeforeRender] = useLifeCycleCallback>(); + const [afterRenderCallbacks, onAfterRender] = useLifeCycleCallback>(); + const [onUpdatedCallbacks, onUpdated] = useLifeCycleCallback>(); + + function render() { + for (const callback of beforeRenderCallbacks) callback({ uniforms: {} }); + for (const pass of effectPasses) pass.render(); + for (const callback of afterRenderCallbacks) callback({ uniforms: {} }); + } + + function initialize(gl: WebGL2RenderingContext) { + for (const [index, pass] of effectPasses.entries()) { + pass.initialize(gl); + + if (index < effectPasses.length - 1) { + pass.setTarget(createRenderTarget(gl)); + } + + pass.onUpdated((newUniforms, oldUniforms) => { + for (const callback of onUpdatedCallbacks) { + callback(newUniforms, oldUniforms); + } + }); + } + } + + function setSize(size: { width: number; height: number }) { + for (const pass of effectPasses) { + pass.setSize(size); + } + } + + function setTarget(target: RenderTarget | null) { + outputPass.setTarget(target); + } + + return { + target: outputPass.target, + passes, + onBeforeRender, + onAfterRender, + onUpdated, + initialize, + render, + setSize, + setTarget, + }; +} diff --git a/lib/src/hooks/useCompositor.ts b/lib/src/hooks/useCompositor.ts index 820e315..b59722a 100644 --- a/lib/src/hooks/useCompositor.ts +++ b/lib/src/hooks/useCompositor.ts @@ -1,26 +1,44 @@ import { createRenderTarget } from "../core/renderTarget"; -import type { CompositePostEffect, EffectUniforms, PostEffect, RenderPass } from "../types"; import { findUniformName } from "../internal/findName"; +import type { CompositeEffectPass, EffectPass, RenderPass } from "../types"; /** - * The compositor handles the combination of the render pass and the post effects: - * - create the render targets for intermediate passes - * - provide each post effect with its inputPass to use in uniforms + * The compositor handles the combination of the render pass and the effects: + * - initialize the gl context and create render targets for all effects + * - provide each effect with its previousPass and inputPass to use in uniforms * - fill the texture uniforms with the previous pass if they are not provided in uniforms + * - detect the first texture uniform of each effect and, if it has no value provided, fill it with the previous pass * - render all passes in the correct order */ -export function useCompositor( +export function useCompositor( gl: WebGL2RenderingContext, - renderPass: RenderPass, - effects: Array, + renderPass: RenderPass, + effects: Array | CompositeEffectPass>>>, ) { - const flatEffects = effects.flat(); + if (effects.length > 0 && renderPass.target === null) { + renderPass.setTarget(createRenderTarget(gl)); + } + + let previousPass = renderPass; - createRenderTargets(flatEffects, renderPass, gl); - setInputPasses(renderPass, effects); - autofillTextureUniforms(flatEffects, renderPass); + for (const [index, effect] of effects.entries()) { + effect.initialize(gl); + effect.setTarget(index === effects.length - 1 ? null : createRenderTarget(gl)); + + if (isCompositeEffectPass(effect)) { + const inputPass = previousPass; + for (const effectPass of Object.values(effect.passes)) { + const previousPassRef = previousPass; + setupEffectPass(effectPass, previousPassRef, inputPass); + previousPass = effectPass; + } + } else { + setupEffectPass(effect, previousPass); + previousPass = effect; + } + } - const allPasses = [renderPass, ...flatEffects]; + const allPasses = [renderPass, ...effects]; function render() { for (const pass of allPasses) { @@ -37,72 +55,32 @@ export function useCompositor( return { render, setSize, allPasses }; } -/** - * Initialize the gl context of the effect passes and create the render targets - */ -function createRenderTargets( - flatEffects: PostEffect[], - renderPass: RenderPass, - gl: WebGL2RenderingContext, -) { - if (flatEffects.length > 0 && renderPass.target === null) { - renderPass.setTarget(createRenderTarget(gl)); - } - - for (const [index, effect] of flatEffects.entries()) { - effect.initialize(gl); - effect.setTarget(index === flatEffects.length - 1 ? null : createRenderTarget(gl)); - } +function isCompositeEffectPass( + effect: EffectPass | CompositeEffectPass, +): effect is CompositeEffectPass { + return typeof (effect as CompositeEffectPass).passes === "object"; } -/** - * Provide each effect with its "inputPass": - * - if the effect has only one pass, the inputPass is the pass preceeding this effect - * - if the effect has multiple passes, the inputPass is the pass preceeding the first pass of the effect - */ -function setInputPasses( - renderPass: RenderPass, - effects: (PostEffect | CompositePostEffect)[], +function setupEffectPass( + effect: EffectPass, + previousPass: EffectPass | RenderPass, + inputPass?: EffectPass | RenderPass, ) { - let previousPass = renderPass; - - for (const effect of effects) { - if (Array.isArray(effect)) { - const inputPass = previousPass; - for (const effectPass of effect) { - setTextureUniforms(effectPass, inputPass); - } - previousPass = effect.at(-1)!; - } else { - setTextureUniforms(effect, previousPass); - previousPass = effect; - } - } -} - -function setTextureUniforms(effect: PostEffect, inputPass: RenderPass) { + // provide the previousPass and inputPass to the uniforms functions for (const uniformName of Object.keys(effect.uniforms)) { const uniformValue = effect.uniforms[uniformName]; if (typeof uniformValue === "function") { - effect.uniforms[uniformName] = () => uniformValue({ inputPass }); + effect.uniforms[uniformName] = () => uniformValue({ previousPass, inputPass }); } } -} -/** - * For each effect pass, find the first texture uniform that has no value provided, - * and fill it with the previous pass. - */ -function autofillTextureUniforms(flatEffects: PostEffect[], renderPass: RenderPass) { - for (const [index, effect] of flatEffects.entries()) { - const textureUniformName = - findUniformName(effect.fragment, "image") || - findUniformName(effect.fragment, "texture") || - findUniformName(effect.fragment, "pass"); + // detect the first texture uniform and, if it has no texture provided, fill it with the previous pass + const textureUniformName = + findUniformName(effect.fragment, "image") || + findUniformName(effect.fragment, "texture") || + findUniformName(effect.fragment, "pass"); - if (textureUniformName && effect.uniforms[textureUniformName] === undefined) { - effect.uniforms[textureUniformName] = () => - (index > 0 ? flatEffects[index - 1] : renderPass).target?.texture; - } + if (textureUniformName && effect.uniforms[textureUniformName] === undefined) { + effect.uniforms[textureUniformName] = () => previousPass.target?.texture; } } diff --git a/lib/src/hooks/useEffectPass.ts b/lib/src/hooks/useEffectPass.ts index ec7578e..ed5641d 100644 --- a/lib/src/hooks/useEffectPass.ts +++ b/lib/src/hooks/useEffectPass.ts @@ -1,9 +1,9 @@ -import type { EffectUniforms, RenderPass } from "../types"; +import type { EffectPass, EffectUniforms } from "../types"; import type { QuadPassOptions } from "./useQuadRenderPass"; import { useQuadRenderPass } from "./useQuadRenderPass"; export function useEffectPass( options: QuadPassOptions, -): RenderPass { +): EffectPass { return useQuadRenderPass(undefined, options); } diff --git a/lib/src/hooks/useWebGLCanvas.ts b/lib/src/hooks/useWebGLCanvas.ts index 7dd833d..cdb664a 100644 --- a/lib/src/hooks/useWebGLCanvas.ts +++ b/lib/src/hooks/useWebGLCanvas.ts @@ -1,5 +1,5 @@ import { useResizeObserver } from "./useResizeObserver"; -import type { CompositePostEffect, PostEffect, Uniforms } from "../types"; +import type { CompositeEffectPass, EffectPass, Uniforms } from "../types"; import { useWebGLContext } from "./useWebGLContext"; import type { QuadPassOptions } from "./useQuadRenderPass"; import { useQuadRenderPass } from "./useQuadRenderPass"; @@ -12,7 +12,7 @@ interface Props extends UseLoopOptions, QuadPassOptions { canvas: HTMLCanvasElement | OffscreenCanvas | string; webglOptions?: WebGLContextAttributes; dpr?: number; - postEffects?: Array; + postEffects?: Array | CompositeEffectPass>; renderMode?: "manual" | "auto"; colorSpace?: PredefinedColorSpace; } diff --git a/lib/src/index.ts b/lib/src/index.ts index 4d7f689..13fdd3e 100644 --- a/lib/src/index.ts +++ b/lib/src/index.ts @@ -7,6 +7,7 @@ export { fillTexture, loadTexture, loadVideoTexture } from "./core/texture"; export { useCompositor } from "./hooks/useCompositor"; export { useEffectPass } from "./hooks/useEffectPass"; +export { useCompositeEffectPass } from "./hooks/useCompositeEffectPass"; export { useQuadRenderPass } from "./hooks/useQuadRenderPass"; export { useRenderPass } from "./hooks/useRenderPass"; export { useWebGLCanvas } from "./hooks/useWebGLCanvas"; diff --git a/lib/src/types.ts b/lib/src/types.ts index 350d661..a102e1e 100644 --- a/lib/src/types.ts +++ b/lib/src/types.ts @@ -12,10 +12,14 @@ export type EffectUniforms = Record< | UniformValue | ((passes: { /** - * - in an effect with only one pass, the inputPass is the pass rendered before this effect + * - in an effect with only one pass, the inputPass is the pass rendered before this effect * - in an effect with multiple passes, the inputPass is the pass rendered before the first pass of the effect */ inputPass: RenderPass; + /** + * pass rendered immediately before this effect + */ + previousPass: RenderPass; }) => UniformValue) >; @@ -51,10 +55,13 @@ export interface RenderPass> extends initialize: (gl: WebGL2RenderingContext) => void; } -export type CompositeEffect = RenderPass[]; +export interface EffectPass> extends RenderPass {} -export type PostEffect = RenderPass; -export type CompositePostEffect = PostEffect[]; +export interface CompositeEffectPass< + P extends Record> = Record>, +> extends Omit { + passes: P; +} export type DrawMode = | "POINTS" @@ -69,9 +76,11 @@ export interface Resizable { setSize: ({ width, height }: { width: number; height: number }) => void; } -export type RenderCallback = (args: Readonly<{ uniforms: U }>) => void; +export type RenderCallback> = ( + args: Readonly<{ uniforms: U }>, +) => void; -export type UpdatedCallback = ( +export type UpdatedCallback> = ( uniforms: Readonly, oldUniforms: Readonly, ) => void; diff --git a/lib/tests/__screenshots__/bloom/bloom-android.png b/lib/tests/__screenshots__/bloom/bloom-android.png index 5d14a1d..036369f 100644 --- a/lib/tests/__screenshots__/bloom/bloom-android.png +++ b/lib/tests/__screenshots__/bloom/bloom-android.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b3ec56b45e9fb2c4b4f3d4f0637bbe6553f6fd6632347764bcd88dd3e246e3c -size 45281 +oid sha256:679112107847463eaad04c98aa6d74a6f8d74ae5cd51f071f02fdd0380a1424a +size 35702 diff --git a/lib/tests/__screenshots__/bloom/bloom-chromium.png b/lib/tests/__screenshots__/bloom/bloom-chromium.png index 4e05196..9d7407a 100644 --- a/lib/tests/__screenshots__/bloom/bloom-chromium.png +++ b/lib/tests/__screenshots__/bloom/bloom-chromium.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d68fffc9b136bf4a2d03fb74aef18e123f2096507084f50bfdce599a24093a6b -size 67550 +oid sha256:d8e6cba623bc0fbeda8475554b0f388f379b2227aa2410db22da8881be382120 +size 51842 diff --git a/lib/tests/__screenshots__/bloom/bloom-firefox.png b/lib/tests/__screenshots__/bloom/bloom-firefox.png index 6b90738..114bb62 100644 --- a/lib/tests/__screenshots__/bloom/bloom-firefox.png +++ b/lib/tests/__screenshots__/bloom/bloom-firefox.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:725d706a88bdfbb0df7c62aa7ff153986b95da933490318f3734d9e175b6a0da -size 94148 +oid sha256:4f938184ed90d8e6c86e8b59af74c1200955c181a4819fe06f8b18555337560a +size 79547 diff --git a/lib/tests/__screenshots__/bloom/bloom-iphone.png b/lib/tests/__screenshots__/bloom/bloom-iphone.png index 205c74e..7ad7943 100644 --- a/lib/tests/__screenshots__/bloom/bloom-iphone.png +++ b/lib/tests/__screenshots__/bloom/bloom-iphone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:751161448f1b2b311860295a17a6b7c0aaa93783c732e99bcb4a97ec5af69275 -size 47564 +oid sha256:01de390e08ea95a9586ab5d8b60d5838ab99f6e0422e18607d8a14e212923bdf +size 35909 diff --git a/lib/tests/__screenshots__/bloom/bloom-safari.png b/lib/tests/__screenshots__/bloom/bloom-safari.png index 804ac74..c820c0e 100644 --- a/lib/tests/__screenshots__/bloom/bloom-safari.png +++ b/lib/tests/__screenshots__/bloom/bloom-safari.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b40ec546f50b99ece4c9c80151d70b212bac3db6220fc8105d618a0ca92db15b -size 64058 +oid sha256:4a94ee99d0fa423c52d0d5d619f22998337563d8dfedf4e908d985807130c099 +size 48205