Outlines
This example implements the outline postprocessing pass, based on this Vanilla Three.js example.
An outlined cube loops through a maze, with a different outline color when the object is hidden.
<script lang="ts"> import CustomRenderer from './CustomRenderer.svelte' import Scene from './Scene.svelte' import type { Vector3Tuple } from 'three' import type { Wall } from './types' import { Canvas } from '@threlte/core' import { Checkbox, Pane } from 'svelte-tweakpane-ui' import { Mesh, Shape } from 'three'
const mesh = new Mesh()
let paused = $state(false) let autoRotate = $state(false)
const walls: Wall[] = [ { height: 3, shape: new Shape() .moveTo(3.5, -4.5) .lineTo(3.5, -3.5) .lineTo(5.5, -3.5) .lineTo(5.5, -0.5) .lineTo(-2.5, -0.5) .lineTo(-2.5, 0.5) .lineTo(5.5, 0.5) .lineTo(5.5, 3.5) .lineTo(-0.5, 3.5) .lineTo(-0.5, 4.5) .lineTo(6.5, 4.5) .lineTo(6.5, -4.5) }, { height: 3, shape: new Shape() .moveTo(-6.5, -4.5) .lineTo(-6.5, 4.5) .lineTo(-3.5, 4.5) .lineTo(-3.5, 3.5) .lineTo(-5.5, 3.5) .lineTo(-5.5, -3.5) .lineTo(0.5, -3.5) .lineTo(0.5, -4.5) } ]
// where is the mesh going? const positions: Vector3Tuple[] = [ [2, -2, 0], [-4, -2, 0], [-4, 2, 0], [-2, 2, 0], [-2, 6, 0], [-8, 6, 0], [-8, -6, 0], [2, -6, 0] ]</script>
<Pane position="fixed" title="outline effect"> <Checkbox bind:value={paused} label="paused" /> <Checkbox bind:value={autoRotate} label="auto rotate camera" /></Pane>
<Canvas> <Scene play={!paused} {autoRotate} {mesh} {walls} {positions} /> <CustomRenderer {mesh} /></Canvas><script lang="ts"> import type { Mesh } from 'three' import { useTask, useThrelte } from '@threlte/core' import { BlendFunction, EffectComposer, EffectPass, OutlineEffect, RenderPass } from 'postprocessing'
type Props = { mesh: Mesh }
let { mesh }: Props = $props()
const { scene, renderer, camera, size, autoRender, renderStage } = useThrelte()
const composer = new EffectComposer(renderer)
const renderPass = new RenderPass(scene) composer.addPass(renderPass)
$effect(() => { composer.setSize($size.width, $size.height) })
export const outlineEffectOptions: ConstructorParameters<typeof OutlineEffect>[2] = { blendFunction: BlendFunction.ALPHA, edgeStrength: 100, pulseSpeed: 0.0, xRay: true, blur: true }
const outlineEffect = new OutlineEffect(scene, undefined, outlineEffectOptions) $effect(() => { outlineEffect.selection.add(mesh) return () => { outlineEffect.selection.clear() } })
const outlineEffectPass = new EffectPass(undefined, outlineEffect) composer.addPass(outlineEffectPass)
$effect(() => { renderPass.mainCamera = $camera outlineEffect.mainCamera = $camera outlineEffectPass.mainCamera = $camera })
$effect(() => { return () => { composer.removeAllPasses() outlineEffectPass.dispose() renderPass.dispose() composer.dispose() } })
$effect(() => { const last = autoRender.current autoRender.set(false) return () => { autoRender.set(last) } })
useTask( (delta) => { composer.render(delta) }, { stage: renderStage, autoInvalidate: false } )</script><script lang="ts"> import type { ExtrudeGeometryOptions, Mesh, Vector3Tuple } from 'three' import type { Wall } from './types' import { DoubleSide } from 'three' import { Environment, OrbitControls } from '@threlte/extras' import { T, useTask } from '@threlte/core' import { Tween } from 'svelte/motion' import { quadInOut } from 'svelte/easing'
type Props = { autoRotate?: boolean mesh: Mesh play?: boolean positions?: Vector3Tuple[] walls?: Wall[] }
let { autoRotate = true, mesh, positions = [], play = true, walls = [] }: Props = $props()
let positionIndex = 0 const positionTween = new Tween(positions[positionIndex], { duration: 400, easing: quadInOut }) let time = 0
// if `positions` changes, restart $effect(() => { positions positionIndex = 0 positionTween.set(positions[positionIndex], { duration: 0 }) time = 0 })
const { start, stop } = useTask((delta) => { time += delta if (time > 0.5) { positionIndex += 1 positionIndex %= positions.length positionTween.set(positions[positionIndex]) time = 0 } })
$effect(() => { if (play) { start() } return () => { stop() } })
const extrudeOptions: ExtrudeGeometryOptions = { bevelEnabled: false }</script>
<Environment url="/textures/equirectangular/hdr/shanghai_riverside_1k.hdr" />
<T.OrthographicCamera makeDefault position={[10, 10, 10]} zoom={50}> <OrbitControls {autoRotate} enableDamping /></T.OrthographicCamera>
<T.Group rotation.x={-1 * 0.5 * Math.PI}> {#each walls as { height, shape }} <T.Mesh scale.z={height}> <T.ExtrudeGeometry args={[shape, extrudeOptions]} /> <T.MeshStandardMaterial color="silver" /> </T.Mesh> {/each}
<T.Group position={positionTween.current ?? positions[0] ?? [0, 0, 0]}> <T is={mesh}> <T.MeshStandardMaterial color="gold" /> <T.BoxGeometry /> </T> </T.Group>
<T.Mesh scale={100} position.z={-1.01} > <T.PlaneGeometry /> <T.MeshStandardMaterial color="green" side={DoubleSide} /> </T.Mesh></T.Group>import type { Shape } from 'three'
export type Wall = { shape: Shape height: number}How it works
Section titled “How it works”A mesh is created in App.svelte and passed into both Scene.svelte and CustomRenderer.svelte.
The scene is responsible for setting up the walls, floor, and attaching a geometry and material to the mesh while the custom renderer adds the mesh to the outline effects selection set.
CustomRenderer
Section titled “CustomRenderer”Both passes that are added to the composer rely on the camera from the threlte context so they can are derived anytime the camera changes. When either of the passes updates, the composer’s passes are reset and the updated passes are added to the composer.