CSS2DRenderer Overlay
This example shows how to run an additional Three.js renderer in parallel with
Threlte’s <Canvas>
, while still leveraging Threlte’s built-in elements.
Specifically, we’ll run
CSS2DRenderer
to add flat labels to objects in a three-dimensional scene.
This example can be easily adapted to use CSS3DRenderer instead, if you want the elements to live “inside” the scene, rather than flat across the surface.
<script lang="ts">
import { Canvas } from '@threlte/core'
import Scene from './Scene.svelte'
</script>
<div id="css-renderer-target" />
<div id="main">
<Canvas>
<Scene />
</Canvas>
</div>
<style>
div#main {
height: 100%;
}
#css-renderer-target {
left: 0;
position: absolute;
pointer-events: none;
top: 0;
}
</style>
<script>
import { T } from '@threlte/core'
// A normal html svelte component, no relation the Threlte
export let label = ''
let count = 0
function click() {
count++
}
</script>
<button on:click={click}>
{label} - {count}
</button>
<style>
button {
margin-left: 0.75rem;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
pointer-events: auto;
background-image: linear-gradient(#5e6cf4 0%, #1338db 100%);
}
</style>
<script>
import { T } from '@threlte/core'
import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js'
export let pointerEvents = false
let element
</script>
<div
bind:this={element}
style:pointer-events={pointerEvents ? 'auto' : 'none !important'}
style:will-change="transform"
>
<slot />
</div>
{#if element}
<T
{...$$restProps}
is={CSS2DObject}
args={[element]}
>
<slot
name="three"
ref={CSS2DObject}
/>
</T>
{/if}
<script lang="ts">
import { T, useTask, useThrelte } from '@threlte/core'
import { OrbitControls } from '@threlte/extras'
import { CSS2DRenderer } from 'three/addons/renderers/CSS2DRenderer.js'
import CounterLabel from './CounterLabel.svelte'
import CssObject from './CssObject.svelte'
const { scene, size, autoRenderTask, camera } = useThrelte()
// Set up the CSS2DRenderer to run in a div placed atop the <Canvas>
const element = document.querySelector('#css-renderer-target') as HTMLElement
const cssRenderer = new CSS2DRenderer({ element })
$: cssRenderer.setSize($size.width, $size.height)
// We are running two renderers, and don't want to run
// updateMatrixWorld twice; tell the renderers that we'll handle
// it manually.
// https://threejs.org/docs/#api/en/core/Object3D.updateWorldMatrix
scene.matrixWorldAutoUpdate = false
// To update the matrices *once* per frame, we'll use a task that is added
// right before the autoRenderTask. This way, we can be sure that the
// matrices are updated before the renderers run.
useTask(
() => {
scene.updateMatrixWorld()
},
{ before: autoRenderTask }
)
// The CSS2DRenderer needs to be updated after the autoRenderTask, so we
// add a task that runs after it.
useTask(
() => {
// Update the DOM
cssRenderer.render(scene, camera.current)
},
{
after: autoRenderTask,
autoInvalidate: false
}
)
</script>
<T.PerspectiveCamera
makeDefault
position={[5, 5, 5]}
>
<OrbitControls enableDamping />
</T.PerspectiveCamera>
<T.DirectionalLight position={[0, 10, 10]} />
<T.Mesh position.y={1}>
<T.BoxGeometry args={[2, 2, 2]} />
<T.MeshStandardMaterial color="#F64F6F" />
</T.Mesh>
<CssObject
position={[-1, 2, 1]}
center={[0, 0.5]}
>
<CounterLabel label="Hello" />
<T.Mesh slot="three">
<T.SphereGeometry args={[0.25]} />
<T.MeshStandardMaterial color="#4F6FF6" />
</T.Mesh>
</CssObject>
<CssObject
position={[1, 2, 1]}
center={[0, 0.5]}
>
<CounterLabel label="CSS" />
<T.Mesh slot="three">
<T.SphereGeometry args={[0.25]} />
<T.MeshStandardMaterial color="#6FF64F" />
</T.Mesh>
</CssObject>
<CssObject
position={[1, 2, -1]}
center={[0, 0.5]}
>
<CounterLabel label="Renderer" />
<T.Mesh slot="three">
<T.SphereGeometry args={[0.25]} />
<T.MeshStandardMaterial color="#F64F6F" />
</T.Mesh>
</CssObject>
How does it work?
In this scene, we run two renderers - the default one provided by Threlte, and a
new CSS2DRenderer which we initialize manually. Threlte’s renderer runs on a
canvas element as usual, while our new renderer runs in a <div>
with absolute
positioning on top of it.
The render loop
To integrate the a new renderer into svelte’s loop, we call it inside a task
added right after Threlte’s
autoRenderTask
. For
details on how to use the Threlte Task Scheduling System, see the
documentation.
By default, each renderer traverses the scene and updates every object. We can
set
scene.matrixWorldAutoUpdate
to false and manually call scene.updateMatrixWorld()
each tick in order to
avoid duplicating the work, since we’re running two renderers. To do that, we’re
adding a task that runs right before Threlte’s autoRenderTask
.
<script>
const { scene, size, autoRenderTask, camera } = useThrelte()
// Set up the CSS2DRenderer to run in a div placed atop the <Canvas>
const element = document.querySelector('#css-renderer-target') as HTMLElement
const cssRenderer = new CSS2DRenderer({ element })
$: cssRenderer.setSize($size.width, $size.height)
// We are running two renderers, and don't want to run
// updateMatrixWorld twice; tell the renderers that we'll handle
// it manually.
// https://threejs.org/docs/#api/en/core/Object3D.updateWorldMatrix
scene.matrixWorldAutoUpdate = false
// To update the matrices *once* per frame, we'll use a task that is added
// right before the autoRenderTask. This way, we can be sure that the
// matrices are updated before the renderers run.
useTask(
() => {
scene.updateMatrixWorld()
},
{ before: autoRenderTask }
)
// The CSS2DRenderer needs to be updated after the autoRenderTask, so we
// add a task that runs after it.
useTask(
() => {
// Update the DOM
cssRenderer.render(scene, camera.current)
},
{
after: autoRenderTask,
autoInvalidate: false
}
)
</script>
Setting up CssObject
The other integral part is a component that accepts DOM contents in the default
slot and places them in the scene and renders them with the ThreeJS
CSS2DRenderer
:
<script>
import { T } from '@threlte/core'
import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js'
export let pointerEvents = false
let element
</script>
<div
bind:this={element}
style:pointer-events={pointerEvents ? 'auto' : 'none !important'}
style:will-change="transform"
>
<slot />
</div>
{#if element}
<T
{...$$restProps}
is={CSS2DObject}
args={[element]}
/>
{/if}
This component renders children into a div, and allows nested Threlte components
via the three
slot. It passes all other properties through, letting us use it
like so:
<CssObject
position={[-1, 2, 1]}
center={[-0.2, 0.5]}
>
<CounterLabel label="Hello" />
<T.Mesh slot="three">
<T.SphereGeometry args={[0.25]} />
<T.MeshStandardMaterial color="#4F6FF6" />
</T.Mesh>
</CssObject>
where <CounterLabel>
is a normal Svelte component outside Threlte’s control,
but the mesh is a component inside the scene hooked in with slot="three"
.