TSL writes compute shaders too (Compute and Workgroups's compute pipelines, without the bind groups). instancedArray() creates a storage buffer node, Fn() defines the kernel, instanceIndex is the invocation's index, and .compute(count) makes a dispatchable node. BookNest runs its sale on the GPU:
import { WebGPURenderer } from 'three/webgpu';
import { Fn, If, instancedArray, instanceIndex } from 'three/tsl';
import { BOOKS } from './bookshelf.js';
const renderer = new WebGPURenderer();
await renderer.init();
const prices = instancedArray(new Float32Array(BOOKS.map((book) => book.price)));
const ratings = instancedArray(new Float32Array(BOOKS.map((book) => book.rating)));
const sale = Fn(() => { // one invocation per book
const price = prices.element(instanceIndex);
If(ratings.element(instanceIndex).lessThan(4.5), () => {
price.assign(price.mul(0.8)); // 20% off
});
})().compute(BOOKS.length);
renderer.compute(sale);
const result = new Float32Array(await renderer.getArrayBufferAsync(prices.value));
console.log(Array.from(result, (price) => price.toFixed(2)).join(' '));
window.__done = true;Output
14.99 31.60 24.00 15.00 16.20 17.04
The three books rated under 4.5 lost 20%. getArrayBufferAsync() waits for the GPU, so read back only what the CPU needs; particle positions in an instancedArray(count, 'vec3') can feed a material's positionNode without leaving the GPU. The WebGL 2 backend emulates compute with transform feedback, without workgroup memory or atomics.
<!doctype html>
<style>
body { margin: 0; background: #f5f1e8; font: 12px system-ui, sans-serif; color: #263238; }
canvas { display: block; max-width: 100%; }
.ui { padding: 4px 8px; }
pre { margin: 4px 8px; white-space: pre-wrap; }
</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>
<div class="ui"><button id="run">Run the sale again</button> (each run takes another 20% off books rated under 4.5)</div>
<pre id="out"></pre>
<script type="module">
import * as THREE from 'three/webgpu';
import { Fn, If, instancedArray, instanceIndex } from 'three/tsl';
const BOOKS = [
{ title: 'Harbor', rating: 4.6, price: 14.99, color: '#1f5f8b' }, { title: 'Deep Web', rating: 4.3, price: 29.99, color: '#5b3f99' },
{ title: 'Saffron', rating: 4.8, price: 24.5, color: '#e09a10' }, { title: 'Summits', rating: 4.1, price: 12.99, color: '#3f7d3a' },
{ title: 'Clockmaker', rating: 4.5, price: 18.0, color: '#b5452f' }, { title: 'Gardens', rating: 4.4, price: 21.0, color: '#2a9d8f' },
];
const renderer = new THREE.WebGPURenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(600, 240);
document.body.prepend(renderer.domElement);
await renderer.init();
// Storage buffers on the GPU, and a kernel with one invocation per book
const prices = instancedArray(new Float32Array(BOOKS.map((b) => b.price)));
const ratings = instancedArray(new Float32Array(BOOKS.map((b) => b.rating)));
const sale = Fn(() => {
const price = prices.element(instanceIndex);
If(ratings.element(instanceIndex).lessThan(4.5), () => {
price.assign(price.mul(0.8)); // 20% off
});
})().compute(BOOKS.length);
const scene = new THREE.Scene();
scene.background = new THREE.Color('#fbf8f2');
const camera = new THREE.PerspectiveCamera(30, 600 / 240, 0.1, 50);
camera.position.set(0, 1.3, 6.5);
camera.lookAt(0, 0.8, 0);
scene.add(new THREE.HemisphereLight('#ffffff', '#8d6e63', 1.5));
const sun = new THREE.DirectionalLight('#ffffff', 2);
sun.position.set(2, 3, 4);
scene.add(sun);
const scale = 0.06; // metres per dollar
const bars = BOOKS.map((book, i) => {
const before = new THREE.Mesh(new THREE.BoxGeometry(0.28, 1, 0.28), new THREE.MeshStandardMaterial({ color: '#d8d0c0' }));
const after = new THREE.Mesh(new THREE.BoxGeometry(0.28, 1, 0.28), new THREE.MeshStandardMaterial({ color: book.color }));
before.position.x = (i - 2.5) * 0.8 - 0.15;
after.position.x = (i - 2.5) * 0.8 + 0.15;
before.scale.y = book.price * scale;
before.position.y = before.scale.y / 2;
scene.add(before, after);
return after;
});
async function runSale() {
renderer.compute(sale);
// Reading back waits for the GPU: only fetch what the CPU needs
const result = new Float32Array(await renderer.getArrayBufferAsync(prices.value));
result.forEach((p, i) => { bars[i].scale.y = p * scale; bars[i].position.y = p * scale / 2; });
document.getElementById('out').textContent = BOOKS.map((b, i) =>
`${b.title} (${b.rating}): $${b.price.toFixed(2)} → $${result[i].toFixed(2)}`).join(' · ');
}
document.getElementById('run').onclick = runSale;
await runSale();
renderer.setAnimationLoop(() => renderer.render(scene, camera));
</script>