One folder, one scene.js that exports a function, and a visual.json
beside it if there are controls.
my-scene/
scene.js
visual.json
There is no canonical way to export a Three.js scene, so Sialk defines one and holds it frozen, the way it holds the audio contract. Sialk supplies three itself, along with the canvas, the renderer, the loop and resizing. Your module builds the scene and says what changes each frame. It never creates a renderer, never runs a loop, and never fetches anything from a CDN, so a show does not depend on the venue's wifi.
The smallest one that works
import * as THREE from 'three';
export default function (stage) {
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(55, stage.width / stage.height, 0.1, 100);
camera.position.z = 6;
const mesh = new THREE.Mesh(
new THREE.IcosahedronGeometry(1.5, 2),
new THREE.MeshStandardMaterial({ color: 0xd8c6b1, flatShading: true }),
);
scene.add(mesh, new THREE.PointLight(0xffffff, 40, 0, 1).translateY(5));
return {
scene,
camera,
update({ audio, transport }) {
mesh.scale.setScalar(1 + audio.bass * 0.6);
mesh.rotation.y = transport.elapsed * 0.3;
},
};
}
Drag the folder on. That is the whole process.
What the function is given, and what it returns
stage carries THREE, renderer, canvas, width, height and sialk.
You rarely need any of it beyond the size for the camera's aspect; it is there
for post-processing and for anything that must know the renderer.
Return an object:
| Field | Required | What it does |
|---|---|---|
scene, camera |
yes, unless render is given |
What Sialk draws each frame |
update(frame) |
no | Called every frame before the draw. frame is { audio, transport, parameters, width, height } |
resize(width, height) |
no | Called when the layer changes size. Without it, a perspective camera's aspect is kept for you |
render(frame) |
no | Take over drawing, for an EffectComposer built on stage.renderer |
dispose() |
no | Called if the scene stops after an error |
The function may be async if a model has to load first. The layer stays
transparent until it resolves.
Making it react
frame.audio and frame.transport are the audio contract,
the same objects a p5 sketch reads on window.sialk, mutated in place once per
frame. Read them inside update, never cache them.
update({ audio, transport }) {
mesh.scale.setScalar(1 + audio.bass * 0.6 + audio.onBeat * 0.2);
material.emissiveIntensity = 1 - audio.beatPhase; // falls in time with the music
light.intensity = 30 + audio.level * 140;
for (let i = 0; i < 64; i++) bar(i, 0.25 + audio.spectrum[i] * 5);
}
Controls the performer can play
Declare them in visual.json, in the same folder, under parameters, in the
shape declaring parameters describes. Every type
is allowed, a number needs min and max, and everything wants a label.
They arrive on frame.parameters under their own names, as themselves:
{
"parameters": {
"speed": { "type": "number", "min": 0, "max": 3, "default": 1, "label": "Speed" },
"glow": { "type": "colour", "default": "#d9a437", "label": "Glow" }
}
}
update({ transport, parameters }) {
mesh.rotation.y = transport.elapsed * 0.3 * parameters.speed;
material.emissive.set(parameters.glow);
}
Addons
Import them from three/addons/, exactly as three's own documentation spells
them. Sialk ships three's examples/jsm under that name.
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
A model file sits in your folder and loads by a relative path: the folder is
the root of the scene's own origin, so new GLTFLoader().loadAsync('./horse.glb')
finds it. The WebGPU renderer and the TSL node material addons are not
supplied; the layer is a WebGL2 canvas.
What is different from a page
Every tutorial does three things your module must not: create a
WebGLRenderer, call setAnimationLoop or requestAnimationFrame, and append
a canvas to the page. Sialk does all three, once, and a scene that does them
again draws twice. The three you import is the copy Sialk ships, currently
r185; a CDN address in a scene is refused before it loads.
The layer is transparent where the scene does not paint, so whatever is beneath
it in the stack shows through. Set scene.background if you want it opaque.
Cost
Three.js cares about count, not resolution. A quarter of a million points
is slow at every size; four times the pixels are nearly free. Prefer one
InstancedMesh to a thousand meshes, and keep the draw calls low. This runs
at 4K/60 in front of a room.
Check it before the show
npx @sialk/agent check my-scene reads the module and reports what Sialk
would not play as intended: a missing default export, an import from
anywhere but three, a renderer or a loop of your own, a field the contract
does not have. Zero problems is the floor; running it in Sialk is the proof.