threlte logo
Postprocessing

Outline

Implements the Outline postprocessing pass. Vanilla threejs example here

An outlined cube loops through a maze, with a different outline color when the object is hidden.

<script lang="ts">
  import { Canvas } from '@threlte/core'
  import Scene from './Scene.svelte'
</script>

<Canvas>
  <Scene />
</Canvas>
<script lang="ts">
  import { useThrelte, useRender } from '@threlte/core'
  import {
    EffectComposer,
    EffectPass,
    RenderPass,
    OutlineEffect,
    BlendFunction
  } from 'postprocessing'

  export let selectedMesh: THREE.Mesh

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

  const composer = new EffectComposer(renderer)
  
  const setupEffectComposer = (camera: THREE.Camera, selectedMesh: THREE.Mesh) => {
    composer.removeAllPasses()
    composer.addPass(new RenderPass(scene, camera))

    const outlineEffect = new OutlineEffect(scene, camera, {
      blendFunction: BlendFunction.ALPHA,
      edgeStrength: 100,
      pulseSpeed: 0.0,
      visibleEdgeColor: 0xffffff,
      hiddenEdgeColor: 0x9900ff,
      xRay: true,
      blur: true
    })
    if (selectedMesh !== undefined) {
      outlineEffect.selection.add(selectedMesh)
    }
    composer.addPass(new EffectPass(camera, outlineEffect))
  }

  $: setupEffectComposer($camera, selectedMesh)
  $: composer.setSize($size.width, $size.height)

  useRender((_, delta) => {
    composer.render(delta)
  })
</script>
<script>
	import { T } from '@threlte/core'
</script>

<T.Mesh position={[6, 2, 4]} rotation.y={Math.PI / 2}>
	<T.MeshStandardMaterial color="silver" />
	<T.BoxGeometry args={[7, 4, 1]} />
</T.Mesh>
<T.Mesh position={[-6, 2, 4]} rotation.y={Math.PI / 2}>
	<T.MeshStandardMaterial color="silver" />
	<T.BoxGeometry args={[7, 4, 1]} />
</T.Mesh>

<T.Mesh position={[-4, 2, 0]}>
	<T.MeshStandardMaterial color="silver" />
	<T.BoxGeometry args={[5, 4, 1]} />
</T.Mesh>
<T.Mesh position={[4, 2, 0]}>
	<T.MeshStandardMaterial color="silver" />
	<T.BoxGeometry args={[5, 4, 1]} />
</T.Mesh>
<T.Mesh position={[-3, 2, 7]}>
	<T.MeshStandardMaterial color="silver" />
	<T.BoxGeometry args={[7, 4, 1]} />
</T.Mesh>
<T.Mesh position={[5, 2, 7]}>
	<T.MeshStandardMaterial color="silver" />
	<T.BoxGeometry args={[3, 4, 1]} />
</T.Mesh>
<T.Mesh position={[-1, 2, 3.5]}>
	<T.MeshStandardMaterial color="silver" />
	<T.BoxGeometry args={[10, 4, 1]} />
</T.Mesh>
<script lang="ts">
	import { onMount } from 'svelte'
	import { quadInOut } from 'svelte/easing'
	import { tweened } from 'svelte/motion'

  import { T } from '@threlte/core'
  import { OrbitControls, Grid } from '@threlte/extras'

  import Maze from './Maze.svelte'
  import CustomRenderer from './CustomRenderer.svelte'

  const route = [
		[0, 1, -3],
		[0, 1, 1.5],
		[4.7, 1, 1.5],
		[4.7, 1, 5],
		[2, 1, 5],
		[2, 1, 9],
		[8, 1, 9],
		[8, 1, -3]
	]
	let routeIndex = 0
	let cubePosition = tweened(route[routeIndex], {
		duration: 400,
		easing: quadInOut,
	})
	let outlinedCube: THREE.Mesh

	onMount(() => {
		const interval = setInterval(nextCubePosition, 500)

		return () => {
			clearInterval(interval)
		}
	})

	const nextCubePosition = () => {
		if (routeIndex < route.length - 1) {
			routeIndex++
		} else {
			routeIndex = 0
		}
		cubePosition.set(route[routeIndex])
	}
</script>

<Maze />

<T.Mesh position={$cubePosition} bind:ref={outlinedCube}>
  <T.MeshToonMaterial color="gold" />
  <T.BoxGeometry />
</T.Mesh>

<CustomRenderer selectedMesh={outlinedCube} />

<T.PerspectiveCamera
  makeDefault
  position={[0, 6, -10]}
  fov={15}
  zoom={0.2}
>
  <OrbitControls enableZoom={true} enableDamping target={[0, 0, 5]}/>
</T.PerspectiveCamera>

<T.DirectionalLight
  intensity={0.8}
  position.x={5}
  position.y={10}
/>
<T.AmbientLight intensity={0.2} />

<Grid
  gridSize={18}
  position={[0, -0.001, 5]}
  cellColor="#ffffff"
  sectionColor="#ffffff"
  sectionThickness={0}
  fadeDistance={25}
/>

How it works

  • in Scene.svelte
    • bind the mesh we want to outline, and pass it as prop selectedMesh to CustomRenderer component
  • Postprocessing is performed within CustomRenderer component
    • we use ‘postprocessing’ library from the pmndrs team
    • call EffectComposer on threlte’s render function, to return a new render function
    • then run our own render loop with this new render function, using useRender from threlte
    • our function setupEffectComposer adds the required RenderPass, and OutlinePass to the composer, specifically to our mesh
    • this function will re-run if selectedMesh changes (not done in this example currently)
  • animation of the cube is done with svelte/motion in Scene.svelte