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