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 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
A mesh is created in App.svelte
and passed into both Scene.svelte
and CustomRenderer.svelte
.
Scene
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
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.