threlte logo
@threlte/extras

<Decal>

webgl <Decal> works with WebGL and is available from the @threlte/extras export. webgpu <Decal> works with WebGPU and is available from the @threlte/extras/webgpu export.

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 gravity={[0, 0, 0]}>
      <Suspense>
        <Scene
          {controls}
          {debug}
        />
      </Suspense>
    </World>
  </Canvas>
</div>

<style>
  div {
    height: 100%;
  }
</style>
<script lang="ts">
  import { type Vector3Tuple, DoubleSide, Vector3 } 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 { Attractor, 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])

  const vec3 = new Vector3()
</script>

<T.PerspectiveCamera
  makeDefault
  position={[5, 1, 4]}
>
  <OrbitControls
    enablePan={false}
    enableZoom={false}
    enableDamping
  />
</T.PerspectiveCamera>

<T.DirectionalLight
  castShadow
  position={[5, 5, 5]}
  intensity={1.25}
/>

<Attractor />

<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 }, index (index)}
  <RigidBody
    bind:rigidBody={bodies[index]}
    oncreate={(ref) => {
      vec3.randomDirection()
      ref.setTranslation(vec3, 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.

Examples

Basic example

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>

Component Signature

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

Props

name
type
required
default
description

debug
boolean
no
false
Shows a wireframe box and axes at the projector's position, rotation and scale.

depthTest
boolean
no
true
Whether the decal material tests against the depth buffer.

mesh
Mesh
no
the parent mesh
The mesh to project onto.

polygonOffsetFactor
number
no
-10
The material's polygon offset factor. Negative values pull the decal toward the camera to prevent z-fighting with the target mesh.

position
Vector3Tuple
no
[0, 0, 0]
The center of the projector, in the target mesh's local space.

rotation
EulerTuple | number
no
The projector's orientation. A tuple sets it directly. Otherwise the projector faces along the normal of the mesh vertex closest to position, and a number spins it around that normal by that many radians.

scale
Vector3Tuple | number
no
1
The size of the projector box. A number sets all three sides.

src
Texture | string
no
The image to project, as a URL to load or a texture. Becomes the decal material's map. Leave it out and pass a material as a child instead to fully control the decal's appearance.

Bindings

name
type
description

ref
Mesh
The decal mesh.