Material looks and shaders
Looks in one line (rim, dissolve, hologram, wind...) and node-graph shaders.
Most looks a game wants are one line; the API reference ("Material looks") has every field.
The default path: a look in one line
import { Transform } from '@onceworlds/engine';
import { Material, Mesh } from '@onceworlds/engine/modules';
world.spawn([Transform(), ...Mesh.sphere(0.5, Material.pulse('#ffd23f'))]); // a pickup that glows and breathes
world.spawn([Transform(), ...Mesh.box(1, Material.rim('#2d6cdf', { kind: 'toon' }))]); // a cartoon hero with a bright edge
| Look | For |
|---|---|
Material.rim(color) |
Heroes, selected or highlighted things, ghosts: a glow at the edges in the emissive colour (white unless you set it). |
Material.dissolve(color) |
Spawning in and dying out: tween opacity from 1 to 0 and it burns away with a glowing edge (emissive). It stays solid. |
Material.hologram(color) |
Holograms, force fields, ghosts, UI in the world: see-through scan lines that flicker. |
Material.triplanar(color, { map }) |
Rocks, cliffs, walls, ground made of boxes: the texture is laid from the world, no stretching, no UVs. |
Material.foliage(color) |
Leaves, grass, bushes, flags: sways in the wind, more at the top. |
Material.pulse(color) |
Pickups, buttons, warnings, power-ups: the glow breathes. |
Each mixes with kind: 'toon' (cartoon bands) or 'unlit', and keeps instancing: a thousand coins with the same look are one
draw. effectStrength and effectSpeed (1 is the look's own) make it more or less and faster or slower.
Per entity, change only color, opacity, emissive and emissiveIntensity. Those are each entity's own and cost nothing
to animate (a rim that turns red when hit: set emissive). Any other field makes a different material.
Your own shading: Material.shader
When no look fits (lava rock, a force field with your own pattern, a pulsing crystal), write a node graph. It is lit by the scene's
sun, lights and shadows like a standard material (or lit: 'toon' / 'unlit'), and runs on WebGPU and on WebGL2 phones alike.
import { t, Transform } from '@onceworlds/engine';
import { Material, Mesh } from '@onceworlds/engine/modules';
const Lava = Material.shader({
uniforms: { speed: t.f32(1), hot: t.color('#ff6a1a') },
build: ({ TSL, u, position, time }) => {
const n = TSL.mx_noise_float(position.mul(0.9).add(TSL.vec3(0, time.mul(u.speed), 0))).mul(0.5).add(0.5);
return { color: TSL.vec3(0.08, 0.05, 0.04), emissive: u.hot.rgb.mul(n.pow(3).mul(6)) };
},
});
world.spawn([Transform(), ...Mesh.sphere(1, Lava({ speed: 2 }))]);
buildgets three.js's node library asTSL(TSL.sin,TSL.mix,TSL.smoothstep,TSL.mx_noise_float...), the uniforms asu, this draw'scolor(vec4) andemissive,map,uv,position(world),localPosition,normalandtime. It returns any ofcolor,opacity,emissive,roughness,metalness,offset(moves vertices: waves, wobble) andalphaTest.- Nodes are built with methods (
a.mul(b).add(c)), not*and+. A colour uniform is a vec4: useu.hot.rgb. - Animate with
timeinsidebuild, never by changing a uniform every frame: every different uniform value is another material (compiled once, kept while used). Per-entity variation goes throughcolor,opacityandemissive, whichbuildreads. - A
buildthat throws is reported once and the material draws magenta: look for that colour. Material.custom(WGSL) is the older, lower-level path: unlit and drawn one entity at a time. PreferMaterial.shader.
Mistakes
| Don't | Do |
|---|---|
Material.toon('#4fb36b', { effect: 'rim' }) (no emissive colour: the rim is black) |
Material.rim('#4fb36b', { kind: 'toon' }) |
Fade a Material.dissolve with transparent: true |
Leave it solid: opacity is how much is left |
| Set a shader uniform in a system every frame | Use time in build, or color / emissive per entity |
Lava({ sped: 2 }) |
The error lists the uniforms the shader has and how to declare one |
Material.hologram on a big wall the camera sits behind |
Holograms are see-through and drawn after the rest: keep them small and few |