Blog · · 4 min lezen
Three.js Journey afgerond, en waarom TSL echt enorm krachtig is
Ik heb Three.js Journey afgerond. Wat me het meest is bijgebleven: TSL, de nieuwe Three.js Shading Language. Daarmee had ik mijn 3D-portfolio in no time af.
Ik heb Three.js Journey van Bruno Simon afgerond. De cursus neemt je mee van een eerste draaiende kubus tot physics, eigen shaders en het optimaliseren van je scène. Echt een aanrader als je met 3D op het web aan de slag wilt.
Wat me het meest is bijgebleven is TSL, de nieuwe Three.js Shading Language. Die is echt enorm krachtig. Met een paar kleine stukjes code laat ik hieronder zien waarom.
Wat is TSL?
Een eigen shader schreef je vroeger in GLSL, als strings in je JavaScript. Dat werkt, maar je hebt geen autocomplete, fouten zie je pas als de shader compileert en combineren met de bestaande materialen van Three.js is lastig.
Met TSL schrijf je je shader gewoon in JavaScript, opgebouwd uit nodes. Three.js zet dat om naar WGSL voor WebGPU, of naar GLSL als de browser terugvalt op WebGL2.
Je hoeft ook niet meer de hele shader zelf te schrijven. Een node material heeft inputs zoals positionNode, colorNode en emissiveNode. Je past alleen aan wat je nodig hebt. De rest (licht, schaduw, tone mapping) blijft gewoon werken.
import { MeshStandardNodeMaterial } from 'three/webgpu'
import { color, mix, normalLocal, positionLocal, sin, time, uniform, uv } from 'three/tsl'
const material = new MeshStandardNodeMaterial()
const amplitude = uniform(0.15)
// Position: push every vertex along its normal with a moving sine wave
const wave = sin(positionLocal.y.mul(4).add(time)).mul(amplitude)
material.positionNode = positionLocal.add(normalLocal.mul(wave))
// Colour: blend from deep blue to cyan over the height of the mesh
material.colorNode = mix(color(0x1e3a8a), color(0x22d3ee), uv().y)
// Emission: a soft pink glow that pulses over time
material.emissiveNode = color(0xf472b6).mul(sin(time.mul(2)).mul(0.5).add(0.5))
Met een paar regels golft het oppervlak, loopt de kleur over de hoogte van blauw naar cyaan en pulseert er een roze gloed. Zonder ShaderMaterial of onBeforeCompile, en met alle belichting van MeshStandardNodeMaterial.
Rekenen op de GPU met instancedArray
Waar TSL voor mij echt het verschil maakt is compute. Met instancedArray maak je een storage buffer die op de GPU staat. Een compute shader werkt die buffer elke frame bij en je materiaal leest dezelfde buffer weer uit om te tekenen. De data hoeft dus nooit terug naar de CPU.
Een voorbeeld: 100.000 deeltjes met zwaartekracht, volledig op de GPU.
import { SpriteNodeMaterial, Sprite } from 'three/webgpu'
import { Fn, If, deltaTime, hash, instanceIndex, instancedArray, vec3 } from 'three/tsl'
const count = 100_000
const positions = instancedArray(count, 'vec3')
const velocities = instancedArray(count, 'vec3')
// Runs once on the GPU: scatter the particles in a 20 m cube
const init = Fn(() => {
const position = positions.element(instanceIndex)
position.assign(vec3(hash(instanceIndex), hash(instanceIndex.add(1)), hash(instanceIndex.add(2))).sub(0.5).mul(20))
})().compute(count)
// Runs every frame on the GPU: gravity, and back to the top when a particle falls out
const update = Fn(() => {
const position = positions.element(instanceIndex)
const velocity = velocities.element(instanceIndex)
velocity.addAssign(vec3(0, -9.81, 0).mul(deltaTime))
position.addAssign(velocity.mul(deltaTime))
If(position.y.lessThan(-10), () => {
position.y.assign(10)
velocity.assign(vec3(0))
})
})().compute(count)
// The material reads the same buffer: no copy back to the CPU
const material = new SpriteNodeMaterial()
material.positionNode = positions.toAttribute()
const particles = new Sprite(material)
particles.count = count
scene.add(particles)
await renderer.init()
renderer.compute(init)
renderer.setAnimationLoop(() => {
renderer.compute(update)
renderer.render(scene, camera)
})
Op de CPU loop je hiervoor elke frame door 100.000 posities en stuur je ze daarna weer naar de GPU. Hier doet de videokaart het werk parallel en doet je JavaScript bijna niks. Effecten die eerst te zwaar waren voor de browser, kunnen nu ineens gewoon.
Van cursus naar portfolio
Met deze kennis had ik mijn nieuwe portfolio in no time af. Het is een 3D-universum: je vliegt met een ruimteschip tussen planeten, landt en loopt over het oppervlak naar de onderdelen van mijn werk.
De planeten worden procedureel gegenereerd, in een low-poly stijl met terrassen en harde kleurbanden. De kleuren, de atmosfeer en de gloed zijn allemaal TSL nodes. Omdat alles in JavaScript zit, gebruiken het oppervlak, het landen en het lopen dezelfde bron (geen losse shaderbestanden die uit de pas gaan lopen). En heeft een browser nog geen WebGPU, dan valt Three.js automatisch terug op WebGL2.
Dank aan Bruno Simon voor een top cursus. Wil je met 3D op het web beginnen? Start met Three.js Journey en sla het TSL-gedeelte zeker niet over.
Vlieg zelf door het universum
Bekijk mijn portfolio als 3D-universum, gebouwd met Three.js en TSL.