WebGPURenderer

Switching from WebGLRenderer to WebGPURenderer

Most scenes switch by changing one import and one constructor: built-in materials become node materials automatically, and the scene graph, lights, shadows, textures and raycasting are shared code. BookNest's bookcase needs only the renderer and a PMREMGenerator from three/webgpu:

BookNest's bookcase drawn by WebGPURendererHTML
<script src="three-map.js"></script>
<script type="module">
import { WebGPURenderer, PMREMGenerator } from 'three/webgpu';
import * as BN from './bookshelf.js';
const renderer = new WebGPURenderer({ antialias: true });
await renderer.init();                                  // adapter, device, backend
const stage = BN.createStage({ width: 640, height: 240, renderer });
const bookcase = BN.createBookcase();
stage.scene.add(bookcase);
BN.lightBookcase(stage, PMREMGenerator);                // the node-based PMREM
BN.enablePicking(stage, bookcase.children.slice(1));   // raycasting is renderer-independent
BN.renderFrames(renderer, stage.scene, stage.camera);
</script>
Browser output of Listing 5.46
Browser output of 46

The picture matches the WebGL renders of The 3D Bookshelf; renderer.info.render counted 53 draw calls and 1,037 triangles, one more of each than WebGLRenderer's 52 and 1,036. What does not carry over is anything tied to GLSL or WebGL objects:

What changes when a WebGLRenderer page moves to WebGPURenderer
WebGLRenderer WebGPURenderer (r186)
ShaderMaterial, onBeforeCompile Node materials and TSL (ShaderMaterial and TSL)
EffectComposer and its passes RenderPipeline (named PostProcessing before r183) with pass nodes
PMREMGenerator from three PMREMGenerator from three/webgpu
PCFSoftShadowMap Removed in r186 for both renderers; PCFShadowMap is soft since r182
The whole bookcase on WebGPURenderer: one import, one constructor and a node-based PMREMHTMLLive
<!doctype html>
<style>
  body { margin: 0; background: #f5f1e8; font: 13px system-ui, sans-serif; color: #263238; }
  canvas { display: block; max-width: 100%; }
  p { margin: 4px 8px; }
</style>
<script type="importmap">
{ "imports": {
  "three": "https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.webgpu.js",
  "three/webgpu": "https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.webgpu.js",
  "three/tsl": "https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.tsl.js",
  "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.186.1/examples/jsm/" } }
</script>
<p id="info"></p>
<script type="module">
// The switch: import from three/webgpu and construct a WebGPURenderer. Everything else is shared.
import * as THREE from 'three/webgpu';
import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';

const BOOKS = [
  { title: 'The Quiet Harbor', author: 'Elena Marsh', color: '#1f5f8b', rating: 4.6, pages: 312 },
  { title: 'Patterns of the Deep Web', author: 'Ravi Menon', color: '#5b3f99', rating: 4.3, pages: 428 },
  { title: 'Salt and Saffron', author: 'Leila Haddad', color: '#e09a10', rating: 4.8, pages: 256 },
  { title: 'Small Steps to Big Summits', author: 'Tom Okafor', color: '#3f7d3a', rating: 4.1, pages: 198 },
  { title: "The Clockmaker's Paradox", author: 'Iris Vale', color: '#b5452f', rating: 4.5, pages: 344 },
  { title: 'Gardens in Glass', author: 'June Park', color: '#2a9d8f', rating: 4.4, pages: 176 },
];

// --- Book model: pages, two boards and a curved spine, merged with one group per part ---
const thickness = (book) => 0.05 + book.pages * 0.0003;          // metres, from the page count
function bookGeometry(book, height, depth = 0.62) {
  const t = thickness(book), b = 0.012, y = height / 2, z = -depth / 2;
  const box = (w, h, d, x, zz = z) => new THREE.BoxGeometry(w, h, d).translate(x, y, zz);
  const spine = new THREE.CylinderGeometry(t / 2, t / 2, height, 24, 1, true, -Math.PI / 2, Math.PI)
    .scale(1, 1, 0.5).translate(0, y, 0);
  return mergeGeometries([box(t - 2 * b, height - 0.03, depth - 0.02, 0, z + 0.01),
    box(b, height, depth, (b - t) / 2), box(b, height, depth, (t - b) / 2), spine], true);
}
function paint(book, width, height, draw) {
  const canvas = Object.assign(document.createElement('canvas'), { width, height });
  const g = canvas.getContext('2d');
  g.fillStyle = book.color;  g.fillRect(0, 0, width, height);
  g.fillStyle = '#ffffff';
  draw(g);
  const texture = new THREE.CanvasTexture(canvas);
  texture.colorSpace = THREE.SRGBColorSpace;
  return texture;
}
const coverArt = (book) => paint(book, 360, 520, (g) => {
  g.font = 'bold 50px Georgia, serif';
  book.title.split(' ').forEach((word, i) => g.fillText(word, 28, 84 + i * 56, 304));
  g.font = 'italic 28px Georgia, serif';
  g.fillText(`${book.author}, ${book.rating} / 5`, 28, 460);
});
const spineArt = (book) => paint(book, 128, 640, (g) => {
  g.rotate(Math.PI / 2);
  g.textBaseline = 'middle';
  g.font = 'bold 44px Georgia, serif';
  g.fillText(book.title, 24, -64, 440);
  g.font = '28px Georgia, serif';
  g.fillText(book.author.split(' ').at(-1), 490, -64, 130);
});
function createSpineBook(book, height = 0.9) {
  const material = (options) => new THREE.MeshStandardMaterial({ roughness: 0.6, ...options });
  const mesh = new THREE.Mesh(bookGeometry(book, height), [material({ color: '#f3ecdc' }),
    material({ color: book.color }), material({ map: coverArt(book) }), material({ map: spineArt(book) })]);
  return Object.assign(mesh, { name: book.title, userData: { book } });
}

// --- The arrangement ---
function createBookcase() {
  const bookcase = new THREE.Group();
  const plank = new THREE.Mesh(new THREE.BoxGeometry(3.5, 0.06, 0.8), new THREE.MeshStandardMaterial({ color: '#8d6e63' }));
  plank.name = 'plank';
  plank.position.set(0, -0.52, 0);
  bookcase.add(plank);
  const top = BOOKS.reduce((best, book) => (book.rating > best.rating ? book : best));
  let x = -0.85;
  BOOKS.forEach((book, i) => {
    const mesh = createSpineBook(book, 0.84 + 0.05 * (i % 3));   // three trim heights
    if (book === top) {
      mesh.position.set(0.3, -0.49, 0.05);
      mesh.rotation.y = -Math.PI / 2 + 0.35;                     // cover forward, turned a little
    } else {
      mesh.position.set(x + thickness(book) / 2, -0.49, 0.3);    // running x plus half a thickness
      x += thickness(book) + 0.005;
    }
    bookcase.add(mesh);
  });
  return bookcase;
}

// --- Stage and lighting: sun and hemisphere, RoomEnvironment fill, Neutral tone mapping, shadows ---
function createStage(width, height, renderer = new THREE.WebGLRenderer({ antialias: true })) {
  renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
  renderer.setSize(width, height);
  document.body.prepend(renderer.domElement);
  const scene = new THREE.Scene();
  scene.background = new THREE.Color('#eceff1');
  const camera = new THREE.PerspectiveCamera(35, width / height, 0.1, 50);
  scene.add(new THREE.HemisphereLight('#ffffff', '#8d6e63', 1));
  const sun = new THREE.DirectionalLight('#ffffff', 2.5);
  sun.name = 'sun';
  scene.add(sun);
  return { renderer, scene, camera };
}
function lightBookcase({ renderer, scene, camera }, PMREM = THREE.PMREMGenerator) {
  renderer.toneMapping = THREE.NeutralToneMapping;
  const pmrem = new PMREM(renderer);
  scene.environment = pmrem.fromScene(new RoomEnvironment(), 0.04).texture;
  scene.environmentIntensity = 0.5;
  pmrem.dispose();
  const wall = new THREE.Mesh(new THREE.PlaneGeometry(12, 6), new THREE.MeshStandardMaterial({ color: '#eceff1' }));
  wall.name = 'wall';
  wall.position.set(0, 1, -0.45);
  scene.add(wall);
  renderer.shadowMap.enabled = true;
  const sun = scene.getObjectByName('sun');
  sun.castShadow = true;
  sun.shadow.mapSize.set(1024, 1024);
  sun.shadow.normalBias = 0.02;
  Object.assign(sun.shadow.camera, { left: -2, right: 2, top: 1.5, bottom: -1.5 });
  scene.traverse((o) => { if (o.isMesh) o.castShadow = o.receiveShadow = true; });
  sun.position.set(-1.5, 3, 4);                        // light from the left, shadows fall right
  camera.position.set(0.15, 0.45, 2.75);
  camera.lookAt(-0.1, -0.05, 0);
}

const renderer = new THREE.WebGPURenderer({ antialias: true });
await renderer.init();                                 // adapter, device, backend
const stage = createStage(600, 270, renderer);
const bookcase = createBookcase();
stage.scene.add(bookcase);
lightBookcase(stage, THREE.PMREMGenerator);           // PMREMGenerator from three/webgpu

// Raycasting is renderer-independent: hover highlights work unchanged
const books = bookcase.children.slice(1), raycaster = new THREE.Raycaster();
let hovered = null;
renderer.domElement.addEventListener('pointermove', (e) => {
  const r = renderer.domElement.getBoundingClientRect();
  raycaster.setFromCamera(new THREE.Vector2((e.clientX - r.left) / r.width * 2 - 1, 1 - (e.clientY - r.top) / r.height * 2), stage.camera);
  const mesh = raycaster.intersectObjects(books)[0]?.object ?? null;
  if (hovered) hovered.material.forEach((m) => m.emissive.set(0));
  if (mesh) mesh.material.forEach((m) => m.emissive.set('#303030'));
  hovered = mesh;
});

let frames = 0;
renderer.setAnimationLoop(() => {
  renderer.render(stage.scene, stage.camera);
  if (++frames % 30 === 1) {
    const { drawCalls, triangles } = renderer.info.render;
    document.getElementById('info').textContent =
      `${renderer.backend.isWebGPUBackend ? 'WebGPU' : 'WebGL 2'} backend · ${drawCalls} draw calls · ${triangles} triangles · hover a book`;
  }
});
</script>