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 { Canvas } from '@threlte/core'
import { Mesh } from 'three'
import { Checkbox, Pane } from 'svelte-tweakpane-ui'
import type { Wall } from './types'
const mesh = new Mesh()
let paused = $state(false)
let autoRotate = $state(true)
const walls: Wall[] = [
{
position: [6, 2, 4],
rotation: [0, 0.5 * Math.PI, 0],
size: [7, 4, 1]
},
{
position: [-6, 2, 4],
rotation: [0, 0.5 * Math.PI, 0],
size: [7, 4, 1]
},
{
position: [-4, 2, 0],
rotation: [0, 0, 0],
size: [5, 4, 1]
},
{
position: [4, 2, 0],
rotation: [0, 0, 0],
size: [5, 4, 1]
},
{
position: [-3, 2, 7],
rotation: [0, 0, 0],
size: [7, 4, 1]
},
{
position: [5, 2, 7],
rotation: [0, 0, 0],
size: [3, 4, 1]
},
{
position: [-1, 2, 3.5],
rotation: [0, 0, 0],
size: [10, 4, 1]
}
]
// where is the mesh going?
const positions: Vector3Tuple[] = [
[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]
]
</script>
<Pane
position="fixed"
name="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 = $derived(new RenderPass(scene, $camera))
const outlineEffectOptions: ConstructorParameters<typeof OutlineEffect>[2] = {
blendFunction: BlendFunction.ALPHA,
edgeStrength: 100,
pulseSpeed: 0.0,
visibleEdgeColor: 0xffffff,
hiddenEdgeColor: 0x9900ff,
xRay: true,
blur: true
}
const outlineEffect = $derived.by(() => {
const effect = new OutlineEffect(scene, $camera, outlineEffectOptions)
effect.selection.add(mesh)
return effect
})
const outlineEffectPass = $derived(new EffectPass($camera, outlineEffect))
$effect(() => {
composer.addPass(renderPass)
composer.addPass(outlineEffectPass)
return () => {
composer.removeAllPasses() // always remove all passes anytime the effect needs to rerun
}
})
$effect(() => {
composer.setSize($size.width, $size.height)
})
$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 { 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 = $state(0)
const position = $derived(positions[positionIndex])
const positionTween = Tween.of(() => position, {
duration: 400,
easing: quadInOut
})
let time = $state(0)
const { start, stop } = useTask((delta) => {
time += delta
if (time > 0.5) {
positionIndex = (positionIndex + 1) % positions.length
time = 0
}
})
$effect(() => {
if (play) {
start()
return () => {
stop()
}
}
})
</script>
{#each walls as { position, rotation, size }}
<T.Mesh
{position}
{rotation}
>
<T.BoxGeometry args={size} />
<T.MeshStandardMaterial color="silver" />
</T.Mesh>
{/each}
<Environment url="/textures/equirectangular/hdr/shanghai_riverside_1k.hdr" />
<T.Group position={positionTween.current ?? [0, 0, 0]}>
<T
is={mesh}
castShadow
>
<T.MeshStandardMaterial color="gold" />
<T.BoxGeometry />
</T>
</T.Group>
<T.PerspectiveCamera
makeDefault
position={[0, 6, -10]}
fov={60}
>
<OrbitControls
{autoRotate}
enableDamping
target={[0, 0, 5]}
/>
</T.PerspectiveCamera>
<T.Mesh
scale={100}
rotation.x={-1 * 0.5 * Math.PI}
receiveShadow
>
<T.PlaneGeometry />
<T.MeshStandardMaterial
color="green"
side={DoubleSide}
/>
</T.Mesh>import type { Vector3Tuple } from 'three'
export type Wall = { position: Vector3Tuple; rotation: Vector3Tuple; size: Vector3Tuple }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.