diff --git a/docs/components/ExampleEditor.vue b/docs/components/ExampleEditor.vue
index 864c33f..79179e5 100644
--- a/docs/components/ExampleEditor.vue
+++ b/docs/components/ExampleEditor.vue
@@ -7,7 +7,7 @@
...props,
deps: {
...deps,
- usegl: '0.6.0',
+ usegl: '0.7.0',
},
}"
>
diff --git a/docs/examples/post-processing/builtin-bloom/dots.frag b/docs/examples/post-processing/builtin-bloom/dots.frag
new file mode 100644
index 0000000..ec05b23
--- /dev/null
+++ b/docs/examples/post-processing/builtin-bloom/dots.frag
@@ -0,0 +1,31 @@
+in vec2 vUv;
+uniform float uTime;
+
+const vec2 center = vec2(0.5, 0.5);
+const float ringRadius = 0.3;
+const float dotRadius = 0.05;
+
+float circle(vec2 uv, vec2 center, float radius) {
+ float d = length(uv - center);
+ return 1.0 - smoothstep(radius - 0.003, radius + 0.003, d);
+}
+
+void main() {
+ vec3 color = vec3(0.0);
+ float t = - uTime * 0.15;
+
+ float offset = 0.;
+ for (int i = 0; i < 8; i++) {
+ vec2 p = center + vec2(cos(t + offset), sin(t + offset)) * ringRadius;
+ float r = dotRadius * (1.8 - 0.24 * float(i));
+ color += vec3(1., vUv) * circle(vUv, p, r);
+ offset += atan(4. * r, ringRadius);
+ }
+
+ // important !
+ // - the colors need to be in linear space for the bloom calculation to be correct
+ // - there needs to be a final pass to convert linear RGB back to sRGB
+ color = pow(color, vec3(2.2));
+
+ gl_FragColor = vec4(color, 1.0);
+}
diff --git a/docs/examples/post-processing/builtin-bloom/index.md b/docs/examples/post-processing/builtin-bloom/index.md
new file mode 100644
index 0000000..19f906a
--- /dev/null
+++ b/docs/examples/post-processing/builtin-bloom/index.md
@@ -0,0 +1,12 @@
+---
+title: Bloom (builtin)
+---
+
+::: example-editor {deps=tweakpane@^4.0.5}
+
+<<< ./index.ts
+<<< ./dots.frag
+<<< @/snippets/canvas-square/styles.css
+<<< @/snippets/default/index.html
+
+:::
diff --git a/docs/examples/post-processing/builtin-bloom/index.ts b/docs/examples/post-processing/builtin-bloom/index.ts
new file mode 100644
index 0000000..06e1d64
--- /dev/null
+++ b/docs/examples/post-processing/builtin-bloom/index.ts
@@ -0,0 +1,17 @@
+import { useWebGLCanvas, bloom, linearToSRGB } from "usegl";
+import { Pane } from "tweakpane";
+import fragment from "./dots.frag?raw";
+import "./styles.css";
+
+const bloomEffect = bloom();
+
+useWebGLCanvas({
+ canvas: "#glCanvas",
+ fragment,
+ postEffects: [bloomEffect, linearToSRGB()],
+});
+
+// You can dynamically update the uniforms of an effect pass, like any other pass
+const pane = new Pane({ title: "Uniforms" });
+pane.addBinding(bloomEffect.uniforms, "uRadius", { min: 0, max: 1 });
+pane.addBinding(bloomEffect.uniforms, "uMix", { min: 0, max: 1 });
diff --git a/docs/examples/post-processing/multi-pass/blur.frag b/docs/examples/post-processing/multi-pass/blur.frag
index f353232..8300322 100644
--- a/docs/examples/post-processing/multi-pass/blur.frag
+++ b/docs/examples/post-processing/multi-pass/blur.frag
@@ -1,24 +1,39 @@
-uniform sampler2D uImage;
+uniform sampler2D uTexture;
uniform vec2 uResolution;
uniform vec2 uDirection;
+uniform float uRadius;
+
in vec2 vUv;
out vec4 outColor;
-const float weights[5] = float[](0.19638062, 0.29675293, 0.09442139, 0.01037598, 0.00025940);
-const float offsets[5] = float[](0.0, 1.41176471, 3.29411765, 5.17647059, 7.05882353);
+/*
+* Directional Gaussian Blur
+* /!\ Absolutely not optimized, for demonstration purposes only
+*/
void main() {
- vec3 color = texture(uImage, vUv).rgb * weights[0];
- float weightSum = weights[0];
+ float sigma = float(uRadius / 2.);
+ vec2 sampleOffset = uDirection / uResolution;
+
+ // Build normalized Gaussian weights
+ float weights[51];
+ weights[0] = 1.0;
+ float sum = weights[0];
+ for (int i = 1; i <= int(uRadius); ++i) {
+ float x = float(i);
+ float w = exp(-0.5 * (x * x) / (sigma * sigma));
+ weights[i] = w;
+ sum += 2.0 * w;
+ }
+ for (int i = 0; i <= int(uRadius); ++i) {
+ weights[i] /= sum;
+ }
- if (vUv.x < 0.52) {
- for(int i = 1; i < 5; i++) {
- vec2 offset = (offsets[i] / uResolution) * 0.5 * uDirection;
- color += texture(uImage, vUv + offset).rgb * weights[i];
- color += texture(uImage, vUv - offset).rgb * weights[i];
- weightSum += 2.0 * weights[i];
- }
- color /= weightSum;
+ vec3 color = texture(uTexture, vUv).rgb * weights[0];
+ for (int i = 1; i <= int(uRadius); ++i) {
+ float fi = float(i);
+ color += texture(uTexture, vUv + sampleOffset * fi).rgb * weights[i];
+ color += texture(uTexture, vUv - sampleOffset * fi).rgb * weights[i];
}
outColor = vec4(color, 1.0);
diff --git a/docs/examples/post-processing/multi-pass/circles.frag b/docs/examples/post-processing/multi-pass/circles.frag
deleted file mode 100644
index 1f2f710..0000000
--- a/docs/examples/post-processing/multi-pass/circles.frag
+++ /dev/null
@@ -1,19 +0,0 @@
-uniform float uTime;
-in vec2 vUv;
-out vec4 outColor;
-
-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() {
- vec3 color = vec3(0, 0.07, 0.15);
- color += drawCircle(vUv - vec2(.4), .1 * (1. + sin(uTime/2.)/10.), vec3(vUv, 1.));
- color += drawCircle(vUv - vec2(.65, .65), .015 * (1. + sin(uTime/2.-1.5)/4.), vec3(vUv, 1.));
- color += drawCircle(vUv - vec2(.75, .4), .04 * (1. + sin(uTime/2.-1.)/4.), vec3(vUv, 1.));
- outColor = vec4(color, 1.);
-}
diff --git a/docs/examples/post-processing/multi-pass/combine.frag b/docs/examples/post-processing/multi-pass/combine.frag
index a10e272..8ec660b 100644
--- a/docs/examples/post-processing/multi-pass/combine.frag
+++ b/docs/examples/post-processing/multi-pass/combine.frag
@@ -6,34 +6,9 @@ uniform float uMix;
in vec2 vUv;
out vec4 outColor;
-vec2 offset(float octave) {
- vec2 padding = 10.0 / uResolution;
- float octaveFloor = min(1.0, floor(octave / 3.0));
- vec2 offset = vec2(
- -octaveFloor * (0.25 + padding.x),
- -(1.0 - (1.0 / exp2(octave))) - padding.y * octave + octaveFloor * (0.35 + padding.y)
- );
- return offset + 0.5 / uResolution;
-}
-
-vec3 blurredMipmapLevel(float octave) {
- vec2 offset = offset(octave - 1.0);
- return texture(uBloomTexture, vUv / exp2(octave) - offset).rgb;
-}
-
-vec3 bloomColor() {
- return blurredMipmapLevel(1.0) * 0.8
- + blurredMipmapLevel(3.0) * 0.5
- + blurredMipmapLevel(4.0) * 1.2;
-}
-
void main() {
vec4 baseColor = texture(uBaseImage, vUv);
- float baseColorGreyscale = dot(baseColor.rgb, vec3(0.299, 0.587, 0.114));
- float mixFactor = (1.0 - baseColorGreyscale * baseColor.a) * uMix;
-
- vec4 combinedColor = baseColor;
- combinedColor.rgb += bloomColor() * mixFactor;
+ vec4 bloomColor = texture(uBloomTexture, vUv);
- outColor = combinedColor;
+ outColor = max(baseColor, mix(baseColor, bloomColor, uMix));
}
diff --git a/docs/examples/post-processing/multi-pass/dots.frag b/docs/examples/post-processing/multi-pass/dots.frag
new file mode 100644
index 0000000..1860188
--- /dev/null
+++ b/docs/examples/post-processing/multi-pass/dots.frag
@@ -0,0 +1,25 @@
+in vec2 vUv;
+out vec4 outColor;
+
+vec3 drawCircle(vec2 pos, float radius, vec3 color) {
+ return smoothstep(radius * 1.03, radius * .97, (length(pos) - radius)) * color;
+}
+
+void main() {
+ vec2 c = vec2(0.5, 0.5);
+ float R = 0.15;
+ float s60 = 0.86602540378; // sqrt(3)/2
+
+ vec2 pTop = c + vec2(0.0, R);
+ vec2 pBL = c + vec2(-R * s60, -R * 0.5);
+ vec2 pBR = c + vec2( R * s60, -R * 0.5);
+
+ vec3 color = vec3(0.0);
+ float r = 0.10;
+
+ color += drawCircle(vUv - pTop, r, vec3(1.0, 0.0, 0.0));
+ color += drawCircle(vUv - pBL, r, vec3(0.0, 1.0, 0.0));
+ color += drawCircle(vUv - pBR, r, vec3(0.0, 0.0, 1.0));
+
+ outColor = vec4(color, 1.0);
+}
diff --git a/docs/examples/post-processing/multi-pass/index.md b/docs/examples/post-processing/multi-pass/index.md
index 74df97a..58d87e6 100644
--- a/docs/examples/post-processing/multi-pass/index.md
+++ b/docs/examples/post-processing/multi-pass/index.md
@@ -1,15 +1,14 @@
---
-title: "Multi pass (bloom)"
+title: Multi pass
---
::: example-editor {deps=tweakpane@^4.0.5}
<<< ./index.ts
-<<< ./circles.frag
-<<< ./mipmap.frag
+<<< ./dots.frag
<<< ./blur.frag
<<< ./combine.frag
-<<< ./styles.css
+<<< @/snippets/canvas-square/styles.css
<<< @/snippets/default/index.html
:::
diff --git a/docs/examples/post-processing/multi-pass/index.ts b/docs/examples/post-processing/multi-pass/index.ts
index e83c5c5..3bd05f6 100644
--- a/docs/examples/post-processing/multi-pass/index.ts
+++ b/docs/examples/post-processing/multi-pass/index.ts
@@ -1,91 +1,68 @@
import { useEffectPass, useWebGLCanvas, useCompositeEffectPass } from "usegl";
-import fragment from "./circles.frag?raw";
-import mipmapsShader from "./mipmap.frag?raw";
-import blurShader from "./blur.frag?raw";
-import combineShader from "./combine.frag?raw";
+import directionalBlurFragment from "./blur.frag?raw";
+import combineFragment from "./combine.frag?raw";
+import dotsFragment from "./dots.frag?raw";
import { Pane } from "tweakpane";
import "./styles.css";
-const mipmaps = useEffectPass({
- fragment: mipmapsShader,
- uniforms: {
- uThreshold: 0.2,
- },
-});
-
const horizontalBlur = useEffectPass({
- fragment: blurShader,
+ fragment: directionalBlurFragment,
uniforms: {
+ uTexture: ({ inputPass }) => inputPass.target!.texture, // optional, the texture uniform is automatically set
uDirection: [1, 0],
+ uRadius: 30,
},
});
const verticalBlur = useEffectPass({
- fragment: blurShader,
+ fragment: directionalBlurFragment,
uniforms: {
+ uTexture: () => horizontalBlur.target!.texture, // optional, the texture uniform is automatically set
uDirection: [0, 1],
+ uRadius: 30,
},
});
const combine = useEffectPass({
- fragment: combineShader,
+ fragment: combineFragment,
uniforms: {
uBaseImage: ({ inputPass }) => inputPass.target!.texture,
- uBloomTexture: () => verticalBlur.target!.texture,
+ uBloomTexture: ({ previousPass }) => previousPass.target!.texture, // same as () => verticalBlur.target!.texture
uMix: 1,
},
});
-const bloomEffect = useCompositeEffectPass({
- mipmaps,
- horizontalBlur,
- verticalBlur,
- combine,
-});
-
-const vignetteEffect = useEffectPass({
- fragment: /* glsl */ `
- uniform sampler2D uTexture;
- uniform float uSize; // (0.0 - 1.0)
- uniform float uRoundness; // (0.0 = rectangle, 1.0 = round)
- uniform float uStrength; // (0.0 - 1.0)
- varying vec2 vUv;
-
- float vignette() {
- vec2 centered = vUv * 2.0 - 1.0;
- float circDist = length(centered);
- float rectDist = max(abs(centered.x), abs(centered.y));
- float dist = mix(rectDist, circDist, uRoundness);
- return 1. - smoothstep(uSize, uSize * 2., dist) * uStrength;
- }
+const bloomPasses = [horizontalBlur, verticalBlur, combine];
- void main() {
- vec4 color = texture(uTexture, vUv);
- color.rgb *= vignette();
- gl_FragColor = color;
- }
- `,
- uniforms: {
- uStrength: 0.5,
- uSize: 0.6,
- uRoundness: 0.7,
+const bloomUniforms = {
+ get uRadius() {
+ return verticalBlur.uniforms.uRadius;
},
-});
+ set uRadius(value: number) {
+ verticalBlur.uniforms.uRadius = value;
+ horizontalBlur.uniforms.uRadius = value;
+ },
+ get uMix() {
+ return combine.uniforms.uMix;
+ },
+ set uMix(value: number) {
+ combine.uniforms.uMix = value;
+ },
+};
+
+const bloom = useCompositeEffectPass(bloomPasses, bloomUniforms);
useWebGLCanvas({
canvas: "#glCanvas",
- fragment: fragment,
- postEffects: [vignetteEffect, bloomEffect],
+ dpr: 1,
+ fragment: dotsFragment,
+ postEffects: [bloom],
});
const pane = new Pane({ title: "Uniforms" });
-// You can update the uniforms of each individual pass, which will trigger a re-render
-const bloom = pane.addFolder({ title: "Bloom" });
-bloom.addBinding(bloomEffect.passes.mipmaps.uniforms, "uThreshold", { min: 0, max: 1 });
-bloom.addBinding(combine.uniforms, "uMix", { min: 0, max: 1 });
-
-const vignette = pane.addFolder({ title: "Vignette" });
-vignette.addBinding(vignetteEffect.uniforms, "uStrength", { min: 0, max: 1 });
-vignette.addBinding(vignetteEffect.uniforms, "uSize", { min: 0, max: 1 });
-vignette.addBinding(vignetteEffect.uniforms, "uRoundness", { min: 0, max: 1 });
+// Updating the uniforms of the composite pass will update those of the individual passes,
+// which will trigger a re-render
+const bloomFolder = pane.addFolder({ title: "Bloom" });
+bloomFolder.addBinding(bloomUniforms, "uMix", { min: 0, max: 1 });
+bloomFolder.addBinding(bloomUniforms, "uRadius", { min: 0, max: 50 });
diff --git a/docs/examples/post-processing/multi-pass/mipmap.frag b/docs/examples/post-processing/multi-pass/mipmap.frag
deleted file mode 100644
index 6e4509e..0000000
--- a/docs/examples/post-processing/multi-pass/mipmap.frag
+++ /dev/null
@@ -1,41 +0,0 @@
-uniform sampler2D uImage;
-uniform vec2 uResolution;
-uniform float uThreshold;
-
-in vec2 vUv;
-out vec4 outColor;
-
-vec2 offset(float octave) {
- vec2 padding = 10.0 / uResolution;
- float octaveFloor = min(1.0, floor(octave / 3.0));
- return vec2(
- -octaveFloor * (0.25 + padding.x),
- -(1.0 - (1.0 / exp2(octave))) - padding.y * octave + octaveFloor * (0.35 + padding.y)
- );
-}
-
-vec3 mipmapLevel(float octave) {
- float scale = exp2(octave);
- vec2 coord = (vUv + offset(octave - 1.0)) * scale;
-
- if (any(lessThan(coord, vec2(0.0))) || any(greaterThan(coord, vec2(1.0)))) {
- return vec3(0.0);
- }
-
- vec3 color = vec3(0.0);
- int spread = int(scale);
-
- for (int i = 0; i < spread; i++) {
- for (int j = 0; j < spread; j++) {
- vec2 offset = (vec2(i, j) / uResolution) * scale / float(spread);
- vec3 imageColor = texture(uImage, coord + offset).rgb;
- color += max(vec3(0.0), imageColor - vec3(uThreshold));
- }
- }
-
- return color / float(spread * spread);
-}
-
-void main() {
- outColor = vec4(mipmapLevel(1.0) + mipmapLevel(3.0) + mipmapLevel(4.0), 1.0);
-}
diff --git a/docs/examples/post-processing/multi-pass/styles.css b/docs/examples/post-processing/multi-pass/styles.css
deleted file mode 100644
index 30a7c83..0000000
--- a/docs/examples/post-processing/multi-pass/styles.css
+++ /dev/null
@@ -1,19 +0,0 @@
-body {
- margin: 0;
- background: black;
- height: 100svh;
- display: grid;
- place-items: center;
-}
-
-canvas {
- width: min(90svmin, 900px);
- aspect-ratio: 1;
- display: block;
- border-radius: 8px;
- border: 1px solid #555;
-}
-
-.tp-dfwv {
- width: 260px !important;
-}
diff --git a/docs/examples/post-processing/single-pass/index.md b/docs/examples/post-processing/single-pass/index.md
index 1d99537..00e10e8 100644
--- a/docs/examples/post-processing/single-pass/index.md
+++ b/docs/examples/post-processing/single-pass/index.md
@@ -1,5 +1,5 @@
---
-title: Single pass (sepia)
+title: Single pass
---
::: example-editor {deps=tweakpane@^4.0.5}
diff --git a/docs/examples/post-processing/single-pass/index.ts b/docs/examples/post-processing/single-pass/index.ts
index edd52a1..e31986c 100644
--- a/docs/examples/post-processing/single-pass/index.ts
+++ b/docs/examples/post-processing/single-pass/index.ts
@@ -2,20 +2,20 @@ import { useEffectPass, useWebGLCanvas, loadTexture } from "usegl";
import { Pane } from "tweakpane";
import "./styles.css";
-const image = loadTexture("https://picsum.photos/id/323/600/400");
-
-const sepiaEffect = useEffectPass({
+const sepia = useEffectPass({
fragment: /* glsl */ `
uniform sampler2D uTexture; // output of the render pass
uniform float uStrength;
in vec2 vUv;
out vec4 fragColor;
- #define SEPIA_COLOR vec3(1.2, 1.0, 0.7)
-
- vec3 sepia(vec3 color) {
- float grayScale = dot(color, vec3(0.299, 0.587, 0.114));
- return grayScale * SEPIA_COLOR;
+ // sepia filter from the CSS specification : https://drafts.fxtf.org/filter-effects/#sepiaEquivalent
+ vec3 sepia(vec3 c){
+ return c * mat3(
+ 0.393, 0.769, 0.189,
+ 0.349, 0.686, 0.168,
+ 0.272, 0.534, 0.131
+ );
}
void main() {
@@ -25,7 +25,7 @@ const sepiaEffect = useEffectPass({
}
`,
uniforms: {
- uStrength: 0.75,
+ uStrength: 1,
},
});
@@ -41,9 +41,9 @@ const { onAfterRender } = useWebGLCanvas({
}
`,
uniforms: {
- uPicture: image,
+ uPicture: loadTexture("https://picsum.photos/id/323/600/400"),
},
- postEffects: [sepiaEffect],
+ postEffects: [sepia],
});
const renderCount = document.querySelector("#renderCount");
@@ -51,6 +51,6 @@ onAfterRender(() => {
renderCount.textContent = `${Number(renderCount.textContent) + 1}`;
});
-// You can update the uniforms of an effect pass
+// You can dynamically update the uniforms of an effect pass, like any other pass
const pane = new Pane({ title: "Uniforms" });
-pane.addBinding(sepiaEffect.uniforms, "uStrength", { min: 0, max: 1 });
+pane.addBinding(sepia.uniforms, "uStrength", { min: 0, max: 1 });
diff --git a/docs/tsconfig.json b/docs/tsconfig.json
new file mode 100644
index 0000000..c4b3a41
--- /dev/null
+++ b/docs/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "lib": ["ES2020", "DOM"],
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true
+ },
+ "include": ["**/*.ts", "**/*.tsx"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/lib/playground/src/pages/gpgpu/boids (static).astro b/lib/playground/src/pages/gpgpu/boids (static).astro
index cac932c..cddefe9 100644
--- a/lib/playground/src/pages/gpgpu/boids (static).astro
+++ b/lib/playground/src/pages/gpgpu/boids (static).astro
@@ -76,15 +76,19 @@ import Layout from "../../layouts/Layout.astro";
positions.render();
renderPass.render();
- velocities.uniforms.uDeltaTime = 0.8;
- velocities.render();
- positions.uniforms.uDeltaTime = 0.8;
- positions.render();
- renderPass.render();
+ requestAnimationFrame(() => {
+ velocities.uniforms.uDeltaTime = 0.8;
+ velocities.render();
+ positions.uniforms.uDeltaTime = 0.8;
+ positions.render();
+ renderPass.render();
- velocities.render();
- positions.render();
- renderPass.render();
+ requestAnimationFrame(() => {
+ velocities.render();
+ positions.render();
+ renderPass.render();
+ });
+ });
});
diff --git a/lib/playground/src/pages/gpgpu/boids.astro b/lib/playground/src/pages/gpgpu/boids.astro
index ec1ff48..53315b4 100644
--- a/lib/playground/src/pages/gpgpu/boids.astro
+++ b/lib/playground/src/pages/gpgpu/boids.astro
@@ -72,16 +72,14 @@ import Layout from "../../layouts/Layout.astro";
renderPass.onAfterRender(incrementRenderCount);
- renderPass.onCanvasReady(() => {
- useLoop(({ deltaTime }) => {
- velocities.uniforms.uDeltaTime = deltaTime / 500;
- velocities.render();
+ useLoop(({ deltaTime }) => {
+ velocities.uniforms.uDeltaTime = deltaTime / 500;
+ velocities.render();
- positions.uniforms.uDeltaTime = deltaTime / 500;
- positions.render();
+ positions.uniforms.uDeltaTime = deltaTime / 500;
+ positions.render();
- renderPass.render();
- });
+ renderPass.render();
});
diff --git a/lib/playground/src/pages/gpgpu/particles - FBO (static).astro b/lib/playground/src/pages/gpgpu/particles - FBO (static).astro
index 6c6c736..b706a71 100644
--- a/lib/playground/src/pages/gpgpu/particles - FBO (static).astro
+++ b/lib/playground/src/pages/gpgpu/particles - FBO (static).astro
@@ -90,9 +90,11 @@ import Layout from "../../layouts/Layout.astro";
positions.render();
renderPass.render();
- positions.uniforms.uDeltaTime = 1;
- positions.render();
- renderPass.render();
+ requestAnimationFrame(() => {
+ positions.uniforms.uDeltaTime = 1;
+ positions.render();
+ renderPass.render();
+ });
});
diff --git a/lib/playground/src/pages/gpgpu/particles - FBO.astro b/lib/playground/src/pages/gpgpu/particles - FBO.astro
index af2bf74..0e87392 100644
--- a/lib/playground/src/pages/gpgpu/particles - FBO.astro
+++ b/lib/playground/src/pages/gpgpu/particles - FBO.astro
@@ -93,12 +93,10 @@ import Layout from "../../layouts/Layout.astro";
renderPass.onAfterRender(incrementRenderCount);
- renderPass.onCanvasReady(() => {
- useLoop(({ deltaTime }) => {
- positions.uniforms.uDeltaTime = deltaTime / 500;
- positions.render();
- renderPass.render();
- });
+ useLoop(({ deltaTime }) => {
+ positions.uniforms.uDeltaTime = deltaTime / 500;
+ positions.render();
+ renderPass.render();
});
diff --git a/lib/playground/src/pages/post-processing/bloom.astro b/lib/playground/src/pages/post-processing/bloom.astro
index 1d9adc2..e9fd9ce 100644
--- a/lib/playground/src/pages/post-processing/bloom.astro
+++ b/lib/playground/src/pages/post-processing/bloom.astro
@@ -3,7 +3,7 @@ import Layout from "../../layouts/Layout.astro";
---
+
+
+
+
diff --git a/lib/src/effects/bloom/glsl/upsample.frag b/lib/src/effects/bloom/glsl/upsample.frag
index 7e699ec..e689a41 100644
--- a/lib/src/effects/bloom/glsl/upsample.frag
+++ b/lib/src/effects/bloom/glsl/upsample.frag
@@ -30,5 +30,5 @@ void main() {
vec4 currColor = texture(uCurrentTexture, vUv);
- fragColor = max(currColor, mix(currColor, prevColor, uRadius));
+ fragColor = mix(currColor, prevColor, uRadius);
}
diff --git a/lib/src/effects/bloom/index.ts b/lib/src/effects/bloom/index.ts
index b5b8d52..f5c7c07 100644
--- a/lib/src/effects/bloom/index.ts
+++ b/lib/src/effects/bloom/index.ts
@@ -1,7 +1,6 @@
import { useCompositeEffectPass } from "../../hooks/useCompositeEffectPass";
-import { useEffectPass } from "../../hooks/useEffectPass";
+import { floatTargetConfig, useEffectPass } from "../../hooks/useEffectPass";
import type { EffectPass } from "../../types";
-import type { RenderTargetParams } from "../../core/renderTarget";
import downSampleFragment from "./glsl/downsample.frag";
import combineFragment from "./glsl/combine.frag";
import sampleVertex from "./glsl/sample.vert";
@@ -13,13 +12,6 @@ export type BloomParams = {
mix?: number;
};
-const floatTargetConfig: RenderTargetParams = {
- internalFormat: WebGL2RenderingContext.RGBA16F,
- type: WebGL2RenderingContext.HALF_FLOAT,
- minFilter: "linear",
- magFilter: "linear",
-};
-
export function bloom(params: BloomParams = {}) {
const { levels = 8, radius = 0.65, mix = 0.5 } = params;
diff --git a/lib/src/effects/linearToSRGB/index.ts b/lib/src/effects/linearToSRGB/index.ts
deleted file mode 100644
index 23cf501..0000000
--- a/lib/src/effects/linearToSRGB/index.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { useEffectPass } from "../../hooks/useEffectPass";
-import linearToSRGBFragment from "./linearToSRGB.frag";
-
-export function linearToSRGB() {
- return useEffectPass({
- fragment: linearToSRGBFragment,
- });
-}
diff --git a/lib/src/effects/linearToSRGB/linearToSRGB.frag b/lib/src/effects/toneMapping/glsl/_common.glsl
similarity index 66%
rename from lib/src/effects/linearToSRGB/linearToSRGB.frag
rename to lib/src/effects/toneMapping/glsl/_common.glsl
index 0076880..b88d273 100644
--- a/lib/src/effects/linearToSRGB/linearToSRGB.frag
+++ b/lib/src/effects/toneMapping/glsl/_common.glsl
@@ -1,9 +1,11 @@
uniform sampler2D uTexture;
+uniform float uExposure;
+uniform bool uConvertToSRGB;
+
varying vec2 vUv;
-void main() {
- vec4 color = texture(uTexture, vUv);
- gl_FragColor = vec4(
+vec4 linearToSRGB(vec4 color) {
+ return vec4(
mix(
color.rgb * 12.92,
pow(color.rgb, vec3(1.0 / 2.4)) * 1.055 - 0.055,
diff --git a/lib/src/effects/toneMapping/glsl/_main.glsl b/lib/src/effects/toneMapping/glsl/_main.glsl
new file mode 100644
index 0000000..cff4e76
--- /dev/null
+++ b/lib/src/effects/toneMapping/glsl/_main.glsl
@@ -0,0 +1,11 @@
+void main() {
+ vec4 color = texture(uTexture, vUv) * uExposure;
+ color.rgb = toneMapping(color.rgb);
+ color = clamp(color, 0.0, 1.0);
+
+ if (uConvertToSRGB) {
+ color = linearToSRGB(color);
+ }
+
+ gl_FragColor = color;
+}
diff --git a/lib/src/effects/toneMapping/glsl/aces.frag b/lib/src/effects/toneMapping/glsl/aces.frag
new file mode 100644
index 0000000..4ca423b
--- /dev/null
+++ b/lib/src/effects/toneMapping/glsl/aces.frag
@@ -0,0 +1,37 @@
+#include "./_common.glsl"
+
+// sRGB => XYZ => D65_2_D60 => AP1 => RRT_SAT
+const mat3 ACESInputMat = mat3(
+ vec3( 0.59719, 0.07600, 0.02840 ), // transposed from source
+ vec3( 0.35458, 0.90834, 0.13383 ),
+ vec3( 0.04823, 0.01566, 0.83777 )
+);
+
+// ODT_SAT => XYZ => D60_2_D65 => sRGB
+const mat3 ACESOutputMat = mat3(
+ vec3( 1.60475, -0.10208, -0.00327 ), // transposed from source
+ vec3( -0.53108, 1.10813, -0.07276 ),
+ vec3( -0.07367, -0.00605, 1.07602 )
+);
+
+// source: https://github.com/selfshadow/ltc_code/blob/master/webgl/shaders/ltc/ltc_blit.fs
+vec3 RRTAndODTFit( vec3 v ) {
+ vec3 a = v * ( v + 0.0245786 ) - 0.000090537;
+ vec3 b = v * ( 0.983729 * v + 0.4329510 ) + 0.238081;
+ return a / b;
+}
+
+// Three.js implementation of ACES Filmic Tone Mapping
+// source: https://github.com/mrdoob/three.js/blob/7f848acd7dc54062c50fca749211ecea0af8742b/src/renderers/shaders/ShaderChunk/tonemapping_pars_fragment.glsl.js#L46
+
+vec3 toneMapping( vec3 color ) {
+ color /= 0.6;
+
+ color = ACESInputMat * color;
+ color = RRTAndODTFit( color );
+ color = ACESOutputMat * color;
+
+ return color;
+}
+
+#include "./_main.glsl"
diff --git a/lib/src/effects/toneMapping/glsl/agx.frag b/lib/src/effects/toneMapping/glsl/agx.frag
new file mode 100644
index 0000000..8add6a4
--- /dev/null
+++ b/lib/src/effects/toneMapping/glsl/agx.frag
@@ -0,0 +1,81 @@
+#include "./_common.glsl"
+
+// Matrices for rec 2020 <> rec 709 color space conversion
+// matrix provided in row-major order so it has been transposed
+// https://www.itu.int/pub/R-REP-BT.2407-2017
+const mat3 LINEAR_REC2020_TO_LINEAR_SRGB = mat3(
+ vec3( 1.6605, - 0.1246, - 0.0182 ),
+ vec3( - 0.5876, 1.1329, - 0.1006 ),
+ vec3( - 0.0728, - 0.0083, 1.1187 )
+);
+
+const mat3 LINEAR_SRGB_TO_LINEAR_REC2020 = mat3(
+ vec3( 0.6274, 0.0691, 0.0164 ),
+ vec3( 0.3293, 0.9195, 0.0880 ),
+ vec3( 0.0433, 0.0113, 0.8956 )
+);
+
+// https://iolite-engine.com/blog_posts/minimal_agx_implementation
+// Mean error^2: 3.6705141e-06
+vec3 agxDefaultContrastApprox( vec3 x ) {
+ vec3 x2 = x * x;
+ vec3 x4 = x2 * x2;
+
+ return + 15.5 * x4 * x2
+ - 40.14 * x4 * x
+ + 31.96 * x4
+ - 6.868 * x2 * x
+ + 0.4298 * x2
+ + 0.1191 * x
+ - 0.00232;
+}
+
+// AgX constants
+const mat3 AgXInsetMatrix = mat3(
+ vec3( 0.856627153315983, 0.137318972929847, 0.11189821299995 ),
+ vec3( 0.0951212405381588, 0.761241990602591, 0.0767994186031903 ),
+ vec3( 0.0482516061458583, 0.101439036467562, 0.811302368396859 )
+);
+
+// explicit AgXOutsetMatrix generated from Filaments AgXOutsetMatrixInv
+const mat3 AgXOutsetMatrix = mat3(
+ vec3( 1.1271005818144368, - 0.1413297634984383, - 0.14132976349843826 ),
+ vec3( - 0.11060664309660323, 1.157823702216272, - 0.11060664309660294 ),
+ vec3( - 0.016493938717834573, - 0.016493938717834257, 1.2519364065950405 )
+);
+
+// LOG2_MIN = -10.0
+// LOG2_MAX = +6.5
+// MIDDLE_GRAY = 0.18
+const float AgxMinEv = - 12.47393; // log2( pow( 2, LOG2_MIN ) * MIDDLE_GRAY )
+const float AgxMaxEv = 4.026069; // log2( pow( 2, LOG2_MAX ) * MIDDLE_GRAY )
+
+
+// Three.js implementation of AgX Tone Mapping, based on Filament, based on Blender
+// source: https://github.com/mrdoob/three.js/blob/c5e5c609904ff38e701f7cafccbd454d363019c7/src/renderers/shaders/ShaderChunk/tonemapping_pars_fragment.glsl.js#L113
+
+vec3 toneMapping( vec3 color ) {
+ color = LINEAR_SRGB_TO_LINEAR_REC2020 * color;
+ color = AgXInsetMatrix * color;
+
+ // Log2 encoding
+ color = max( color, 1e-10 ); // avoid 0 or negative numbers for log2
+ color = log2( color );
+ color = ( color - AgxMinEv ) / ( AgxMaxEv - AgxMinEv );
+
+ color = clamp( color, 0.0, 1.0 );
+
+ // Apply sigmoid
+ color = agxDefaultContrastApprox( color );
+
+ color = AgXOutsetMatrix * color;
+
+ // Linearize
+ color = pow( max( vec3( 0.0 ), color ), vec3( 2.2 ) );
+
+ color = LINEAR_REC2020_TO_LINEAR_SRGB * color;
+
+ return color;
+}
+
+#include "./_main.glsl"
diff --git a/lib/src/effects/toneMapping/glsl/cineon.frag b/lib/src/effects/toneMapping/glsl/cineon.frag
new file mode 100644
index 0000000..79ec2be
--- /dev/null
+++ b/lib/src/effects/toneMapping/glsl/cineon.frag
@@ -0,0 +1,11 @@
+#include "./_common.glsl"
+
+// filmic operator by Jim Hejl and Richard Burgess-Dawson
+// http://filmicworlds.com/blog/filmic-tonemapping-operators/
+
+vec3 toneMapping( vec3 color ) {
+ color = max( vec3( 0.0 ), color - 0.004 );
+ return pow( ( color * ( 6.2 * color + 0.5 ) ) / ( color * ( 6.2 * color + 1.7 ) + 0.06 ), vec3( 2.2 ) );
+}
+
+#include "./_main.glsl"
diff --git a/lib/src/effects/toneMapping/glsl/hable.frag b/lib/src/effects/toneMapping/glsl/hable.frag
new file mode 100644
index 0000000..2ae3fb3
--- /dev/null
+++ b/lib/src/effects/toneMapping/glsl/hable.frag
@@ -0,0 +1,25 @@
+#include "./_common.glsl"
+
+// Uncharted 2 filmic curve by John Hable
+// http://filmicworlds.com/blog/filmic-tonemapping-operators/
+
+const float A = 0.15;
+const float B = 0.50;
+const float C = 0.10;
+const float D = 0.20;
+const float E = 0.02;
+const float F = 0.30;
+const float W = 11.2;
+const float exposureBias = 2.0;
+
+vec3 uncharted2Tonemap(vec3 x) {
+ return ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F)) - E/F;
+}
+
+vec3 toneMapping(vec3 color) {
+ vec3 mapped = uncharted2Tonemap(exposureBias * color);
+ vec3 whiteScale = 1.0 / uncharted2Tonemap(vec3(W));
+ return mapped * whiteScale;
+}
+
+#include "./_main.glsl"
diff --git a/lib/src/effects/toneMapping/glsl/linear.frag b/lib/src/effects/toneMapping/glsl/linear.frag
new file mode 100644
index 0000000..d83c5ac
--- /dev/null
+++ b/lib/src/effects/toneMapping/glsl/linear.frag
@@ -0,0 +1,7 @@
+#include "./_common.glsl"
+
+vec3 toneMapping(vec3 color) {
+ return color;
+}
+
+#include "./_main.glsl"
diff --git a/lib/src/effects/toneMapping/glsl/neutral.frag b/lib/src/effects/toneMapping/glsl/neutral.frag
new file mode 100644
index 0000000..023372d
--- /dev/null
+++ b/lib/src/effects/toneMapping/glsl/neutral.frag
@@ -0,0 +1,25 @@
+#include "./_common.glsl"
+
+const float startCompression = 0.8 - 0.04;
+const float desaturation = 0.15;
+
+// Khronos PBR Neutral
+// https://modelviewer.dev/examples/tone-mapping
+
+vec3 toneMapping( vec3 color ) {
+ float x = min(color.r, min(color.g, color.b));
+ float offset = x < 0.08 ? x - 6.25 * x * x : 0.04;
+ color -= offset;
+
+ float peak = max(color.r, max(color.g, color.b));
+ if (peak < startCompression) return color;
+
+ float d = 1. - startCompression;
+ float newPeak = 1. - d * d / (peak + d - startCompression);
+ color *= newPeak / peak;
+
+ float g = 1. - 1. / (desaturation * (peak - newPeak) + 1.);
+ return mix(color, newPeak * vec3(1, 1, 1), g);
+}
+
+#include "./_main.glsl"
diff --git a/lib/src/effects/toneMapping/glsl/reinhard.frag b/lib/src/effects/toneMapping/glsl/reinhard.frag
new file mode 100644
index 0000000..da24581
--- /dev/null
+++ b/lib/src/effects/toneMapping/glsl/reinhard.frag
@@ -0,0 +1,17 @@
+#include "./_common.glsl"
+
+uniform float uWhitePoint;
+
+// Reinhard tonemapping extended with white point
+
+vec3 toneMapping(vec3 color) {
+ vec3 mapped = color.rgb / (1.0 + color.rgb);
+
+ if (uWhitePoint > 1.0) {
+ vec3 whiteSq = vec3(uWhitePoint * uWhitePoint);
+ mapped = (color.rgb * (1.0 + color.rgb / whiteSq)) / (1.0 + color.rgb);
+ }
+ return mapped;
+}
+
+#include "./_main.glsl"
diff --git a/lib/src/effects/toneMapping/index.ts b/lib/src/effects/toneMapping/index.ts
new file mode 100644
index 0000000..67d53e5
--- /dev/null
+++ b/lib/src/effects/toneMapping/index.ts
@@ -0,0 +1,79 @@
+import { useEffectPass } from "../../hooks/useEffectPass";
+
+import linearFragment from "./glsl/linear.frag";
+import acesFragment from "./glsl/aces.frag";
+import reinhardFragment from "./glsl/reinhard.frag";
+import hableFragment from "./glsl/hable.frag";
+import neutralFragment from "./glsl/neutral.frag";
+import cineonFragment from "./glsl/cineon.frag";
+import agxFragment from "./glsl/agx.frag";
+
+export type ToneMappingParams = {
+ /**
+ * The exposure level to apply to the image before the tone mapping.
+ * @default 1
+ */
+ exposure?: number;
+ /**
+ * The color space to output the final image in.
+ * @default "sRGB"
+ */
+ outputColorSpace?: "sRGB" | "linear";
+};
+
+function createToneMappingPass(fragment: string, params: ToneMappingParams = {}) {
+ const { exposure = 1, outputColorSpace = "sRGB" } = params;
+ return useEffectPass({
+ fragment,
+ uniforms: {
+ uExposure: exposure,
+ uConvertToSRGB: outputColorSpace === "sRGB",
+ },
+ });
+}
+
+type ReinhardToneMappingParams = ToneMappingParams & {
+ /**
+ * The white point to use for extended Reinhard tone mapping.
+ * A value of 1 disables the extended tone mapping.
+ *
+ * @default 1
+ */
+ whitePoint?: number;
+};
+
+export function reinhardToneMapping(params: ReinhardToneMappingParams = {}) {
+ const { exposure = 1, outputColorSpace = "sRGB", whitePoint = 1 } = params;
+ return useEffectPass({
+ fragment: reinhardFragment,
+ uniforms: {
+ uExposure: exposure,
+ uConvertToSRGB: outputColorSpace === "sRGB",
+ uWhitePoint: whitePoint,
+ },
+ });
+}
+
+export function linearToneMapping(params: ToneMappingParams = {}) {
+ return createToneMappingPass(linearFragment, params);
+}
+
+export function hableToneMapping(params: ToneMappingParams = {}) {
+ return createToneMappingPass(hableFragment, params);
+}
+
+export function acesToneMapping(params: ToneMappingParams = {}) {
+ return createToneMappingPass(acesFragment, params);
+}
+
+export function neutralToneMapping(params: ToneMappingParams = {}) {
+ return createToneMappingPass(neutralFragment, params);
+}
+
+export function cineonToneMapping(params: ToneMappingParams = {}) {
+ return createToneMappingPass(cineonFragment, params);
+}
+
+export function agxToneMapping(params: ToneMappingParams = {}) {
+ return createToneMappingPass(agxFragment, params);
+}
diff --git a/lib/src/hooks/useCompositeEffectPass.ts b/lib/src/hooks/useCompositeEffectPass.ts
index d5e1c18..e448b6d 100644
--- a/lib/src/hooks/useCompositeEffectPass.ts
+++ b/lib/src/hooks/useCompositeEffectPass.ts
@@ -9,8 +9,8 @@ import type {
} from "../types";
export function useCompositeEffectPass>(
- passes: EffectPass[],
- uniforms: U,
+ passes: EffectPass[],
+ uniforms: U = {} as U,
): CompositeEffectPass {
const outputPass = passes.at(-1)!;
diff --git a/lib/src/hooks/useCompositor.ts b/lib/src/hooks/useCompositor.ts
index fa0fbde..c367e6e 100644
--- a/lib/src/hooks/useCompositor.ts
+++ b/lib/src/hooks/useCompositor.ts
@@ -1,6 +1,7 @@
import { createRenderTarget } from "../core/renderTarget";
import { findUniformName } from "../internal/findName";
import type { CompositeEffectPass, EffectPass, RenderPass } from "../types";
+import { floatTargetConfig } from "./useEffectPass";
/**
* The compositor handles the combination of the render pass and the effects:
@@ -18,7 +19,7 @@ export function useCompositor(
gl.getExtension("EXT_color_buffer_float");
if (effects.length > 0 && renderPass.target === null) {
- renderPass.setTarget(createRenderTarget(gl));
+ renderPass.setTarget(createRenderTarget(gl, floatTargetConfig));
}
let previousPass = renderPass;
diff --git a/lib/src/hooks/useEffectPass.ts b/lib/src/hooks/useEffectPass.ts
index 5837d2c..c7b15c1 100644
--- a/lib/src/hooks/useEffectPass.ts
+++ b/lib/src/hooks/useEffectPass.ts
@@ -15,15 +15,15 @@ type EffectPassOptions = Omit, "tar
target?: RenderTargetParams | null;
};
-const effectTargetConfig: RenderTargetParams = {
- minFilter: "nearest",
- magFilter: "nearest",
+export const floatTargetConfig: RenderTargetParams = {
+ internalFormat: WebGL2RenderingContext.RGBA16F,
+ type: WebGL2RenderingContext.HALF_FLOAT,
};
export function useEffectPass(
options: EffectPassOptions,
): EffectPass {
- const { target = effectTargetConfig, resolutionScale = 1 } = options;
+ const { target = floatTargetConfig, resolutionScale = 1 } = options;
const renderPass = useQuadRenderPass(undefined, { ...options, target: null });
diff --git a/lib/src/hooks/useWebGLCanvas.ts b/lib/src/hooks/useWebGLCanvas.ts
index b8a29be..a86cd64 100644
--- a/lib/src/hooks/useWebGLCanvas.ts
+++ b/lib/src/hooks/useWebGLCanvas.ts
@@ -41,8 +41,13 @@ export const useWebGLCanvas = (props: Props) => {
const renderPass = useQuadRenderPass(gl, props);
const compositor = useCompositor(gl, renderPass, postEffects);
+ // don't render before the first resize of the canvas to avoid a glitch
+ let isCanvasResized = false;
+
function render() {
- compositor.render();
+ if (isCanvasResized) {
+ compositor.render();
+ }
}
let requestedRender = false;
@@ -107,13 +112,12 @@ export const useWebGLCanvas = (props: Props) => {
let resizeObserver: ReturnType | null = null;
const [canvasReadyCallbacks, onCanvasReady] = useLifeCycleCallback();
- let isFirstResize = true;
function resizeCanvas(width: number, height: number) {
setSize({ width: width * dpr, height: height * dpr });
- if (isFirstResize) {
+ if (!isCanvasResized) {
for (const callback of canvasReadyCallbacks) callback();
- isFirstResize = false;
+ isCanvasResized = true;
}
}
diff --git a/lib/src/index.ts b/lib/src/index.ts
index e90cd80..598a322 100644
--- a/lib/src/index.ts
+++ b/lib/src/index.ts
@@ -20,4 +20,12 @@ export { usePingPongFBO } from "./hooks/usePingPongFBO";
export { useTransformFeedback } from "./hooks/useTransformFeedback";
export { bloom } from "./effects/bloom";
-export { linearToSRGB } from "./effects/linearToSRGB";
+export {
+ linearToneMapping,
+ reinhardToneMapping,
+ hableToneMapping,
+ acesToneMapping,
+ neutralToneMapping,
+ cineonToneMapping,
+ agxToneMapping,
+} from "./effects/toneMapping";
diff --git a/lib/src/internal/useUniforms.ts b/lib/src/internal/useUniforms.ts
index b45afbb..7d7f216 100644
--- a/lib/src/internal/useUniforms.ts
+++ b/lib/src/internal/useUniforms.ts
@@ -56,6 +56,7 @@ export function useUniforms(uniforms: U) {
if (uniformLocation === -1) return -1;
if (typeof value === "number") return _gl.uniform1f(uniformLocation, value);
+ if (typeof value === "boolean") return _gl.uniform1i(uniformLocation, value ? 1 : 0);
if (value instanceof WebGLTexture) {
if (!textureUnits.has(name)) {
diff --git a/lib/src/types.ts b/lib/src/types.ts
index 7669d57..cb36d88 100644
--- a/lib/src/types.ts
+++ b/lib/src/types.ts
@@ -59,7 +59,7 @@ export type EffectPass> = RenderPass<
export interface CompositeEffectPass>
extends Omit, "fragment" | "vertex"> {
- passes: EffectPass[];
+ passes: EffectPass[];
}
export type DrawMode =
diff --git a/lib/tests/__screenshots__/tonemapping/tonemapping-android.png b/lib/tests/__screenshots__/tonemapping/tonemapping-android.png
new file mode 100644
index 0000000..66586f8
Binary files /dev/null and b/lib/tests/__screenshots__/tonemapping/tonemapping-android.png differ
diff --git a/lib/tests/__screenshots__/tonemapping/tonemapping-chromium.png b/lib/tests/__screenshots__/tonemapping/tonemapping-chromium.png
new file mode 100644
index 0000000..f4b0034
Binary files /dev/null and b/lib/tests/__screenshots__/tonemapping/tonemapping-chromium.png differ
diff --git a/lib/tests/__screenshots__/tonemapping/tonemapping-firefox.png b/lib/tests/__screenshots__/tonemapping/tonemapping-firefox.png
new file mode 100644
index 0000000..a7fb005
Binary files /dev/null and b/lib/tests/__screenshots__/tonemapping/tonemapping-firefox.png differ
diff --git a/lib/tests/__screenshots__/tonemapping/tonemapping-iphone.png b/lib/tests/__screenshots__/tonemapping/tonemapping-iphone.png
new file mode 100644
index 0000000..8871f7c
Binary files /dev/null and b/lib/tests/__screenshots__/tonemapping/tonemapping-iphone.png differ
diff --git a/lib/tests/__screenshots__/tonemapping/tonemapping-safari.png b/lib/tests/__screenshots__/tonemapping/tonemapping-safari.png
new file mode 100644
index 0000000..ce9f1cb
Binary files /dev/null and b/lib/tests/__screenshots__/tonemapping/tonemapping-safari.png differ
diff --git a/lib/tests/screenshots.spec.ts b/lib/tests/screenshots.spec.ts
index b5319ff..1cfdb0d 100644
--- a/lib/tests/screenshots.spec.ts
+++ b/lib/tests/screenshots.spec.ts
@@ -9,8 +9,8 @@ const routesToTest = routes.filter(({ route }) => !ignoreRoutes.has(route));
const expectedRendersByDemo = {
scissor: "2",
video: "2",
- "particles - FBO (static)": "3",
- "boids (static)": "4",
+ "particles - FBO (static)": "2",
+ "boids (static)": "3",
mipmap: /[1-3]/,
texture: /1|2/,
sepia: /1|2/,