Skip to content

Commit

Permalink
feat: add useCompositeEffectPass hook to have more control over multi…
Browse files Browse the repository at this point in the history
… pass effects
  • Loading branch information
jsulpis committed Jan 16, 2025
1 parent 2b7bf2d commit 5d18c72
Show file tree
Hide file tree
Showing 13 changed files with 180 additions and 128 deletions.
54 changes: 20 additions & 34 deletions lib/playground/src/pages/post-processing/bloom.astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,48 @@ import Layout from "../../layouts/Layout.astro";
---

<script>
import { useEffectPass, useWebGLCanvas } from "usegl";
import { fragment, vertex, mipmapsShader, blurShader, combineShader } from "../../shaders/bloom";
import { useEffectPass, useWebGLCanvas, useCompositeEffectPass } from "usegl";
import { fragment, mipmapsShader, blurShader, combineShader } from "../../shaders/bloom";
import { incrementRenderCount } from "../../components/renderCount";

const mipmapsPass = useEffectPass({
const mipmaps = useEffectPass({
fragment: mipmapsShader,
uniforms: {
u_threshold: 0.2,
},
});

const horizontalBlurPass = useEffectPass({
const horizontalBlur = useEffectPass({
fragment: blurShader,
uniforms: {
u_direction: [1, 0],
},
});

const verticalBlurPass = useEffectPass({
const verticalBlur = useEffectPass({
fragment: blurShader,
uniforms: {
u_direction: [0, 1],
},
});

const combinePass = useEffectPass({
const combine = useEffectPass({
fragment: combineShader,
uniforms: {
u_image: ({ inputPass }) => inputPass.target!.texture,
u_bloomTexture: () => verticalBlurPass.target!.texture,
u_bloomTexture: () => verticalBlur.target!.texture,
u_mix: 0,
},
});

const bloomEffect = [mipmapsPass, horizontalBlurPass, verticalBlurPass, combinePass];
const bloomEffect = useCompositeEffectPass({
mipmaps,
horizontalBlur,
verticalBlur,
combine,
});

bloomEffect.passes.combine.uniforms.u_mix = 1;

const vignetteEffect = useEffectPass({
fragment: /* glsl */ `
Expand All @@ -60,40 +71,15 @@ import Layout from "../../layouts/Layout.astro";
`,
});

const { gl, onAfterRender } = useWebGLCanvas({
const { onAfterRender } = useWebGLCanvas({
canvas: "#glCanvas",
fragment: fragment,
vertex: vertex,
attributes: {
a_position: {
data: [
[0.1, 0.5],
[-0.3, 0],
[0.2, -0.1],
].flat(),
size: 2,
},
a_size: {
data: [100, 300, 200],
size: 1,
},
},
postEffects: [vignetteEffect, bloomEffect],
});

gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
gl.clearColor(0, 0.07, 0.15, 1);

onAfterRender(incrementRenderCount);
</script>

<Layout title="Bloom">
<canvas id="glCanvas"></canvas>
</Layout>

<style>
canvas {
aspect-ratio: 3 / 2;
}
</style>
24 changes: 19 additions & 5 deletions lib/playground/src/shaders/bloom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
`;
64 changes: 64 additions & 0 deletions lib/src/hooks/useCompositeEffectPass.ts
Original file line number Diff line number Diff line change
@@ -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<P extends Record<string, EffectPass<any>>>(
passes: P,
): CompositeEffectPass<P> {
const effectPasses = Object.values(passes);
const outputPass = effectPasses.at(-1)!;

const [beforeRenderCallbacks, onBeforeRender] = useLifeCycleCallback<RenderCallback<any>>();
const [afterRenderCallbacks, onAfterRender] = useLifeCycleCallback<RenderCallback<any>>();
const [onUpdatedCallbacks, onUpdated] = useLifeCycleCallback<UpdatedCallback<any>>();

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,
};
}
116 changes: 47 additions & 69 deletions lib/src/hooks/useCompositor.ts
Original file line number Diff line number Diff line change
@@ -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<U extends EffectUniforms>(
export function useCompositor(
gl: WebGL2RenderingContext,
renderPass: RenderPass<U>,
effects: Array<PostEffect | CompositePostEffect>,
renderPass: RenderPass<any>,
effects: Array<EffectPass<any> | CompositeEffectPass<Record<string, EffectPass<any>>>>,
) {
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) {
Expand All @@ -37,72 +55,32 @@ export function useCompositor<U extends EffectUniforms>(
return { render, setSize, allPasses };
}

/**
* Initialize the gl context of the effect passes and create the render targets
*/
function createRenderTargets(
flatEffects: PostEffect[],
renderPass: RenderPass<any>,
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<any>,
): effect is CompositeEffectPass<any> {
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<any>,
effects: (PostEffect | CompositePostEffect)[],
function setupEffectPass(
effect: EffectPass<any>,
previousPass: EffectPass<any> | RenderPass<any>,
inputPass?: EffectPass<any> | RenderPass<any>,
) {
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<any>) {
// 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<any>) {
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;
}
}
4 changes: 2 additions & 2 deletions lib/src/hooks/useEffectPass.ts
Original file line number Diff line number Diff line change
@@ -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<U extends EffectUniforms>(
options: QuadPassOptions<U>,
): RenderPass<U> {
): EffectPass<U> {
return useQuadRenderPass(undefined, options);
}
4 changes: 2 additions & 2 deletions lib/src/hooks/useWebGLCanvas.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -12,7 +12,7 @@ interface Props<U extends Uniforms> extends UseLoopOptions, QuadPassOptions<U> {
canvas: HTMLCanvasElement | OffscreenCanvas | string;
webglOptions?: WebGLContextAttributes;
dpr?: number;
postEffects?: Array<PostEffect | CompositePostEffect>;
postEffects?: Array<EffectPass<any> | CompositeEffectPass<any>>;
renderMode?: "manual" | "auto";
colorSpace?: PredefinedColorSpace;
}
Expand Down
Loading

0 comments on commit 5d18c72

Please sign in to comment.