threlte logo

ScreenQuad

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">
  import ScreenQuadGeometry, { vertexShader } from './ScreenQuadGeometry.svelte'
  import { Environment, OrbitControls, useGltf } from '@threlte/extras'
  import { Scene, Uniform, WebGLRenderTarget } from 'three'
  import { T, useTask, useThrelte } from '@threlte/core'

  const { camera, renderStage, renderer, scene, size } = useThrelte()

  const target = new WebGLRenderTarget(1, 1)

  $effect(() => {
    target.setSize($size.width, $size.height)
  })

  const _scene = new Scene()

  useTask(
    () => {
      const last = renderer.getRenderTarget()
      renderer.setRenderTarget(target)
      renderer.render(scene, camera.current)
      renderer.setRenderTarget(last)
      renderer.render(_scene, camera.current)
    },
    {
      stage: renderStage
    }
  )

  /**
   * put your interesting effects in this shader.
   */
  const fragmentShader = `
		precision highp float;

		uniform sampler2D uScene;
		uniform vec2 uMouse;
		uniform float uRadius;
		varying vec2 vUv;

		void main() {

			vec4 color = vec4(0.0, 0.0, 0.0, 1.0);

			vec2 center = vec2(0.5, 0.5);

			if (length(center - vUv) - uRadius < 0.0) {
				color = texture2D(uScene, vUv);
			}

			gl_FragColor = color;
		}
	`

  const gltf = useGltf('/models/spaceships/Bob.gltf')

  const uScene = new Uniform(target.texture)

  const uRadius = new Uniform(0)

  let time = 0
  useTask((delta) => {
    time += delta
    uRadius.value = 1 - 0.5 * (1 + Math.sin(time))
  })
</script>

<T.PerspectiveCamera
  makeDefault
  position={5}
>
  <OrbitControls />
</T.PerspectiveCamera>

<!-- this geometry is so simple that frustrum culling it would actually be more work -->
<T.Mesh
  frustrumCulled={false}
  attach={_scene}
>
  <T.RawShaderMaterial
    {vertexShader}
    {fragmentShader}
    uniforms.uScene={uScene}
    uniforms.uRadius={uRadius}
  />
  <ScreenQuadGeometry />
</T.Mesh>

{#await gltf then { scene }}
  <T is={scene} />
{/await}

<Environment
  url="/textures/equirectangular/hdr/shanghai_riverside_1k.hdr"
  isBackground
/>
<script
  lang="ts"
  module
>
  // (-1, 3)
  //    |\
  //    | \
  //    |  \
  //    |   \
  //    |    \
  //    |     \
  //    |      \
  //    |       \
  //    |________\
  // (-1, -1)   (3, -1)
  const vertices = new Float32Array([-1, -1, 3, -1, -1, 3])

  export const vertexShader = `
		precision highp float;

		attribute vec2 position;

		varying vec2 vUv;

		void main() {
			vUv = 0.5 * (position + 1.0);
			gl_Position = vec4(position, 0.0, 1.0);
		}
	`

  const center = new Vector3()
</script>

<script lang="ts">
  import type { Props } from '@threlte/core'
  import { BufferAttribute, BufferGeometry, Sphere, Vector3 } from 'three'
  import { T } from '@threlte/core'

  let {
    ref = $bindable(new BufferGeometry()),
    children,
    ...restProps
  }: Props<BufferGeometry> = $props()

  ref.setAttribute('position', new BufferAttribute(vertices, 2))
  ref.boundingSphere = new Sphere().set(center, Infinity)
</script>

<T
  is={ref}
  {...restProps}
>
  {@render children?.({
    ref
  })}
</T>

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 material for a mesh that covers the screen. To do this we need two things:

  1. a webgl render target
  2. a separate scene from the one that threlte provides

A WebGLRenderTarget is created and its size is set up to follow the size of the canvas.

A separate scene is needed so that we can add the screen-quad mesh to it and render it. The idea is that the only thing in this scene will be the screen-quad mesh that has the post-processed scene as its material. This mesh can’t be added to the main scene because it would cause a circular dependency.

So the steps are as follows:

  1. render the “main” scene to the render target.
  2. render the separate scene.

There are some sub-steps inbetween but these are the two important bits that happen in the useTask callback in the <Scene> component.

ScreenQuadGeometry

This component creates a right triangle such that its right angle is positioned in the lower-left of the canvas. The size of the triangle is such that the hypotenuse only touches the top-right corner of the canvas. The triangle is in clip-space so we need a special vertex shader to use it.

The vertex shader string is exported from the <ScreenQuadGeometry> component to be used with either a Three.RawShaderMaterial or Three.ShaderMaterial.

Scene

There are a few notable techniques in the <Scene> component.

Firstly, The screen-quad mesh is not frustrum culled. This is because the geometry is so simple that culling it would actually requare more work. If you culled its geometry, you’d actually be creating more vertices.

This <T.Mesh> uses threlte’s attach prop to add it to the secondary scene instead of the default scene created by threlte.

Secondly, the render task that threlte automatically uses is disabled and a custom render task is used instead. This is explained on the useTask page.

Lastly, the uvs are used in the fragment shader to use as an index into the scene texture. For this reason, the resolution of the canvas is sent into the shader as a uniform. An effect is ran to keep the uniform in sync with the current size of the canvas.