threlte logo

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:

  1. a WebGL render target.
  2. 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.

  1. save the current render target of the renderer
const lastRenderTarget = renderer.getRenderTarget()
  1. set the render target of the renderer to the fbo
renderer.setRenderTarget(target)
  1. 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.

  1. restore the last render target
renderer.setRenderTarget(last)
  1. 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