Skip to content

<Decal>

A declarative component for Three.js’ DecalGeometry.

<script lang="ts">
import { Canvas } from '@threlte/core'
import { Suspense } from '@threlte/extras'
import { World } from '@threlte/rapier'
import Scene from './Scene.svelte'
import { Pane, Checkbox } from 'svelte-tweakpane-ui'
let controls = $state(false)
let debug = $state(false)
</script>
<Pane
title="Decal"
position="fixed"
>
<Checkbox
label="Controls"
bind:value={controls}
/>
<Checkbox
label="Debug"
bind:value={debug}
/>
</Pane>
<div>
<Canvas>
<World>
<Suspense>
<Scene
{controls}
{debug}
/>
</Suspense>
</World>
</Canvas>
</div>
<style>
div {
height: 100%;
}
</style>
<script lang="ts">
import { type Vector3Tuple, DoubleSide } from 'three'
import { T } from '@threlte/core'
import {
Decal,
TransformControls,
useTexture,
OrbitControls,
VirtualEnvironment,
useSuspense
} from '@threlte/extras'
import { RigidBody as RigidBodyRef } from '@dimforge/rapier3d-compat'
import { Collider, RigidBody } from '@threlte/rapier'
let { controls = false, debug = false } = $props()
const suspend = useSuspense()
const svelteIcon = suspend(useTexture('/icons/svelte.png'))
const threlteIcon = suspend(useTexture('/icons/mstile-150x150.png'))
let bodies = $state<RigidBodyRef[]>([])
let position = $state<Vector3Tuple>([0.5, 0, 0.5])
let current = 0
setInterval(() => {
current += 1
current %= bodies.length
const body = bodies[current]
body?.setLinvel({ x: 0, y: 0, z: 0 }, true)
body?.setAngvel({ x: 0, y: 0, z: 0 }, true)
body?.setTranslation(
{ x: (Math.random() - 0.5) * 0.1, y: 5, z: (Math.random() - 0.5) * 0.1 },
true
)
}, 400)
</script>
<T.PerspectiveCamera
makeDefault
position={[5, 1, 4]}
oncreate={(ref) => ref.lookAt(0, 1, 0)}
>
<OrbitControls
enablePan={false}
enableZoom={false}
enableDamping
target={[0, 1, 0]}
/>
</T.PerspectiveCamera>
<T.DirectionalLight
castShadow
position={[5, 5, 5]}
intensity={1.25}
/>
<T.Mesh receiveShadow>
<Collider
shape={'ball'}
args={[1]}
/>
<T.SphereGeometry args={[1, 256, 128]} />
<T.MeshStandardMaterial roughness={0.1} />
{#if $svelteIcon}
<Decal
{position}
{debug}
>
{#snippet children()}
<T.MeshStandardMaterial
map={$svelteIcon}
transparent
roughness={0.2}
polygonOffset
polygonOffsetFactor={-10}
/>
{#if controls}
<TransformControls
oncreate={(ref) => {
ref.position.fromArray(position)
}}
onchange={(event) => {
if (event.target.object) event.target.object.position.toArray(position)
}}
/>
{/if}
{/snippet}
</Decal>
{/if}
</T.Mesh>
{#each { length: 20 } as _, index (index)}
<RigidBody
bind:rigidBody={bodies[index]}
oncreate={(ref) => {
ref.setTranslation({ x: 0, y: -10 + index, z: 0 }, true)
}}
>
<T.Mesh castShadow>
<Collider
shape="ball"
args={[0.3]}
restitution={0.2}
/>
<T.SphereGeometry args={[0.3, 256, 128]} />
<T.MeshStandardMaterial roughness={0.2} />
<Decal
position={[0.35, 0.35, 0.35]}
rotation={Math.PI / 4}
scale={1}
depthTest
{debug}
>
<T.MeshStandardMaterial
map={$threlteIcon}
transparent
roughness={0.2}
polygonOffset
polygonOffsetFactor={-10}
/>
</Decal>
</T.Mesh>
</RigidBody>
{/each}
{#snippet lightformer(
color: string,
shape: 'circle' | 'plane',
size: number,
position: [number, number, number]
)}
<T.Group {position}>
<T.Mesh oncreate={(ref) => ref.lookAt(0, 0, 0)}>
{#if shape === 'circle'}
<T.CircleGeometry args={[size / 2]} />
{:else}
<T.PlaneGeometry args={[size, size]} />
{/if}
<T.MeshBasicMaterial
{color}
side={DoubleSide}
/>
</T.Mesh>
</T.Group>
{/snippet}
<VirtualEnvironment frames={10}>
{@render lightformer('#FF4F4F', 'plane', 20, [0, 0, -20])}
{@render lightformer('#FFD0CB', 'circle', 5, [0, 5, 0])}
{@render lightformer('#2223FF', 'plane', 8, [-3, 0, 4])}
</VirtualEnvironment>

By default, the decal will use its parent mesh as the decal surface.

The decal projector must intersect the surface to be visible.

If you do not specifiy a rotation the decal projector will look at the parents center point. You can also pass a single number as the rotation, which will spin the decal along its surface.

Scene.svelte
<script lang="ts">
import { T } from '@threlte/core'
import { Decal } from '@threlte/extras'
</script>
<T.Mesh>
<T.SphereGeometry />
<T.MeshStandardMaterial />
<Decal
position={[0, 0, 0]}
rotation={[0, 0, 0]}
scale={1}
>
<!-- Will override the default material if added -->
<T.MeshBasicMaterial
map={texture}
polygonOffset
polygonOffsetFactor={-1}
/>
</Decal>
</mesh>

<Decal> extends < T . Mesh > and supports all its props, snippets, bindings and events.

Props

name
type
required
default
description

depthTest
boolean
no
true

mesh
Mesh
no
An optional parent mesh to project the decal

polygonOffsetFactor
number
no
-10

position
Vector3Tuple
no
Positions the center of the decal projector

rotation
EulerTuple
no
Euler for manual orientation or a single float for closest-vertex-normal orient

scale
Vector3Tuple
no

src
string | Texture
no
A url or texture prop can be used instead of a material override