threlte logo

Outlines

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>

<div>
  <Canvas>
    <Scene />
  </Canvas>
</div>

<style>
  div {
    height: 100%;
  }
</style>
<script lang="ts">
  import { useTask, useThrelte } from '@threlte/core'
  import {
    BlendFunction,
    EffectComposer,
    EffectPass,
    OutlineEffect,
    RenderPass
  } from 'postprocessing'
  import { onMount } from 'svelte'

  export let selectedMesh: THREE.Mesh

  const { scene, renderer, camera, size, autoRender, renderStage } = 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)

  onMount(() => {
    let before = autoRender.current
    autoRender.set(false)
    return () => {
      autoRender.set(before)
    }
  })

  useTask(
    (delta) => {
      composer.render(delta)
    },
    { stage: renderStage, autoInvalidate: false }
  )
</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 the ‘postprocessing’ library
    • Create a new EffectComposer with Threlte’s renderer
    • Then run our own render loop with this new render function, using useTask from threlte, make sure to set autoRender to false
    • Our function setupEffectComposer adds the required RenderPass, and OutlinePass to the EffectComposer, specifically to our Mesh object
    • This function will re-run if selectedMesh changes
  • Animation of the cube is done with svelte/motion in Scene.svelte