Three.js
Three.js and uno UI can draw to the same canvas by sharing a WebGPU device and context. The order of the draw calls determines whether the UI appears in front of or behind the scene.
The examples below assume you are familiar with the basics of uno UI covered in the First Layout section.
Shared setup
Use WebGPURenderer from three/webgpu. Pass it the canvas, device, and context used by uno UI.
import ResourcesWebGPU from '@uno.build/ui/ResourcesWebGPU'
import UI from '@uno.build/ui/UI'
import * as THREE from 'three/webgpu'
const canvas = document.querySelector('canvas')
const resources = await ResourcesWebGPU.create({ canvas })
const { ui } = await UI.create({ resources })
const { device, context } = resources
const renderer = new THREE.WebGPURenderer({ canvas, device, context })
renderer.outputColorSpace = THREE.LinearSRGBColorSpace
renderer.setClearColor(0xffffff, 1)
await renderer.init()Control the draw order
Clear the color at the start of each frame, then preserve it in subsequent draws.
uno UI: load_op
The load_op option of ui.draw() controls what happens to the existing pixels before drawing the UI:
| Value | Effect |
|---|---|
'load' (default) | Keeps existing pixels. Used when calling ui.draw() without options. |
'clear' | Clears the target to transparent black. |
load_op does not choose which layer is in front. The draw order does that. Using 'clear' after drawing the scene erases the scene; using 'load' keeps it visible wherever the UI is transparent.
Three.js: autoClearColor
renderer.autoClearColor controls whether Three.js clears the color before rendering the scene:
| Value | Effect |
|---|---|
true (default) | Clears the existing color. |
false | Preserves the existing color. |
Keep renderer.autoClear at its default value of true. Changing only autoClearColor lets Three.js continue clearing depth and stencil for each frame.
UI in front of the scene
Draw the scene first, then draw the UI:
renderer.autoClearColor = true
requestAnimationFrame(function render(time) {
cube.rotation.x = time / 2000
cube.rotation.y = time / 1000
renderer.render(scene, camera)
ui.draw()
requestAnimationFrame(render)
})Three.js clears to white and draws the cube. uno UI preserves that image and draws the panel over it.
UI behind the scene
Draw the UI first with load_op: 'clear', then render the scene with autoClearColor = false:
renderer.autoClearColor = false
// uno UI now paints the background, since Three.js will not clear the color.
ui.root.style('backgroundColor', '#ffffff')
ui.update()
requestAnimationFrame(function render(time) {
cube.rotation.x = time / 2000
cube.rotation.y = time / 1000
ui.draw({ load_op: 'clear' })
renderer.render(scene, camera)
requestAnimationFrame(render)
})uno UI clears the canvas and draws the white background and panel. Three.js keeps those pixels and draws the cube over them. Leaving autoClearColor = true would erase the UI before drawing the cube.
Keep scene.background at its default value of null when uno UI supplies the background. A background texture or skybox drawn by Three.js can cover the UI even when color clearing is disabled.
UI behind and in front
For separate background and foreground layouts, create two UI instances using the same resources:
const { ui: background_ui } = await UI.create({ resources })
const { ui: foreground_ui } = await UI.create({ resources })Apply the layout and viewport setup to each UI, then draw the background UI, the scene, and the foreground UI in that order:
renderer.autoClearColor = false
requestAnimationFrame(function render(time) {
cube.rotation.x = time / 2000
cube.rotation.y = time / 1000
background_ui.draw({ load_op: 'clear' })
renderer.render(scene, camera)
foreground_ui.draw()
requestAnimationFrame(render)
})The background UI starts the frame, Three.js draws over it, and the foreground UI draws last. Keep the foreground layout transparent wherever the scene should remain visible.
Why LinearSRGBColorSpace is required
Keep renderer.outputColorSpace = THREE.LinearSRGBColorSpace for this direct canvas composition, especially when drawing UI behind the scene.
By default, Three.js uses SRGBColorSpace. WebGPURenderer renders the scene into an intermediate texture, then converts its colors in a fullscreen output pass. That texture does not contain the UI already drawn to the canvas, so the output pass overwrites it. autoClearColor = false prevents clearing, but does not prevent this fullscreen draw.
LinearSRGBColorSpace matches Three.js's working color space. With the default NoToneMapping, Three.js skips the intermediate conversion pass and draws directly onto the canvas, preserving the background UI when autoClearColor = false. Enabling tone mapping can introduce the intermediate pass again. See Three.js output conversion.
This setup omits the usual linear-to-sRGB output conversion, so the scene's colors may appear darker than with standard sRGB output. Keeping that conversion requires coordinating the final composition between Three.js and uno UI.
Demo
This demo uses two UI instances: one for the background and one for the foreground. The foreground UI reuses the React component from the React demo.