fullscreenquad
This example demonstrates a well-known technique for doing simple postprocessing utilizing a “screen-quad”.
<script lang="ts">
import Scene from './Scene.svelte'
import { Canvas } from '@threlte/core'
</script>
<Canvas autoRender={false}>
<Scene />
</Canvas>
<script
lang="ts"
module
>
const vertexShader = `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 1.0);
}
`
</script>
<script lang="ts">
import { Environment, OrbitControls, useFBO, useGltf } from '@threlte/extras'
import { FullScreenQuad } from 'three/examples/jsm/postprocessing/Pass.js'
import { ShaderMaterial, Uniform } from 'three'
import { T, useTask, useThrelte } from '@threlte/core'
const { camera, renderStage, renderer, scene } = useThrelte()
const target = useFBO()
/**
* put your interesting effects in this shader.
*/
const fragmentShader = `
uniform sampler2D uScene;
uniform float uTime;
varying vec2 vUv;
void main() {
gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
vec2 center = vec2(0.5, 0.5);
float radius = 1.0 - 0.5 * (1.0 + sin(uTime));
if (length(center - vUv) - radius < 0.0) {
gl_FragColor = texture2D(uScene, vUv);
}
}
`
const gltf = useGltf('/models/spaceships/Bob.gltf')
const uScene = new Uniform(target.texture)
const uTime = new Uniform(0)
useTask((delta) => {
uTime.value += delta
})
const material = new ShaderMaterial({
fragmentShader,
uniforms: {
uScene,
uTime
},
vertexShader
})
const quad = new FullScreenQuad(material)
// not using the <T> component so we need to clean up after ourselves
$effect(() => {
return () => {
quad.dispose()
material.dispose()
}
})
useTask(
() => {
const last = renderer.getRenderTarget()
renderer.setRenderTarget(target)
renderer.render(scene, camera.current)
renderer.setRenderTarget(last)
quad.render(renderer)
},
{
stage: renderStage
}
)
</script>
<T.PerspectiveCamera
makeDefault
position={5}
>
<OrbitControls />
</T.PerspectiveCamera>
{#await gltf then { scene }}
<T is={scene} />
{/await}
<Environment
url="/textures/equirectangular/hdr/shanghai_riverside_1k.hdr"
isBackground
/>
Overview
The basic idea of postprocessing is to draw the scene to a frame-buffer that can then be sent into another shader. The output of this shader is used as the texture or as part of a material for a mesh that covers the screen. To do this we need two things:
- a WebGL render target.
- a mesh that covers the screen
Render Target
Threlte’s useFBO gives us a WebGLRenderTarget that automatically resizes when the size of the canvas updates.
FullScreenQuad
Three provides a FullScreenQuad class that both covers the screen and provides all the necessary buffer attributes for use in shaders.
FullScreenQuad itself isn’t a mesh but uses one under the hood when rendering.
Rendering
There is a specific order to how the scene and the quad should be rendered.
- save the current render target of the renderer
const lastRenderTarget = renderer.getRenderTarget()
- set the render target of the renderer to the fbo
renderer.setRenderTarget(target)
- render the main scene using the main camera
const { camera } = useThrelte()
renderer.render(scene, camera.current)
Threlte’s camera is a currentWritable so we can use its .current property to get the instance. Its important to use the current property to avoid having to read from the store every frame.
- restore the last render target
renderer.setRenderTarget(last)
- Render the quad
quad.render(renderer)
FullScreenQuad.render accepts a renderer and renders to whatever its current render target is. It uses a private camera when renderering.
Resources
- three.js’s Pass class includes the implementation for FullScreenQuad