Writing a sketch · 4 of 7

A Three.js scene

One scene.js that exports a function. three itself is supplied.

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.