← back to idam

sketch guide

how to create sketches for idam.art

Overview

Each sketch is an ES module (sketch.js) that exports a few functions and constants. The idam client dynamically imports your module and calls your setup() function, passing in Three.js and the scene context. You build your 3D world, and idam handles the renderer, camera, navigation, multiplayer, and VR.

File Structure

A sketch is either a single sketch.js file, or a zip containing:

sketch.js           // required — your module
assets/             // optional — images, textures, models
  photo-1.jpg
  photo-2.jpg
  texture.png

Minimal Example

export const startPosition = { x: 0, y: 1.6, z: 5 };
export const background = 0x111111;

export function setup(ctx) {
    const { THREE, root } = ctx;

    const light = new THREE.DirectionalLight(0xffffff, 1.0);
    light.position.set(5, 10, 5);
    root.add(light);

    const cube = new THREE.Mesh(
        new THREE.BoxGeometry(1, 1, 1),
        new THREE.MeshStandardMaterial({ color: 0x00ff88 })
    );
    root.add(cube);

    return { cube };
}

export function dispose(state) { }

export function update(delta, ctx, state) {
    state.cube.rotation.y += 0.5 * delta;
}

Exported Constants

All optional. Control how the client sets up the scene before calling setup().

ExportTypeDescription
startPosition { x, y, z } Where the player spawns. Default: { x: 0, y: 1.6, z: 3 }
background number Scene background color as hex. Example: 0xf5f5f5
hideDefaultEnvironment boolean Hide the default floor/grid. Default: true
rendererSettings object Renderer overrides (see below)

rendererSettings

export const rendererSettings = {
    shadowMap: true,                  // enable shadow mapping
    toneMapping: 'ACESFilmic',        // tone mapping algorithm
    toneMappingExposure: 1.0         // exposure level
};

Lifecycle Functions

setup(ctx) — required

Called once when the sketch loads. Build your scene here. Add everything to ctx.root.

Return an object with any state you need in update() or dispose().

ctx propertyDescription
THREEThe Three.js library — use this instead of importing
rootA THREE.Group — add all your objects here
sceneThe main scene (read-only — don't add objects here)
cameraThe perspective camera
rendererThe WebGL renderer
playerThe player rig group (contains camera)
baseUrlURL prefix for loading assets (e.g. /sketches/001-my-sketch/)

dispose(state) — required (can be a no-op)

Called when switching away from this sketch. The client automatically disposes everything in ctx.root — geometries, materials, and textures are all cleaned up for you.

Only use this to clean up non-Three.js resources: intervals, audio, event listeners, WebSocket connections, etc.

export function dispose(state) {
    clearInterval(state.myTimer);
    state.audioContext.close();
}

update(delta, ctx, state) — optional

Called every frame. Use for animations. delta is seconds since last frame. state is whatever setup() returned.

export function update(delta, ctx, state) {
    state.cube.rotation.y += 0.5 * delta;
    state.particles.position.y = Math.sin(ctx.THREE.MathUtils.degToRad(Date.now() * 0.1));
}

Loading Assets

Use ctx.baseUrl to build paths to your assets. Never use relative paths.

export function setup(ctx) {
    const { THREE, root, baseUrl } = ctx;

    const loader = new THREE.TextureLoader();
    const texture = loader.load(baseUrl + 'assets/my-photo.jpg');
    texture.colorSpace = THREE.SRGBColorSpace;  // important!

    const painting = new THREE.Mesh(
        new THREE.PlaneGeometry(2, 1.5),
        new THREE.MeshStandardMaterial({ map: texture })
    );
    painting.position.set(0, 1.5, -3);
    root.add(painting);

    return { painting };
}
Always set colorSpace. For any texture that represents a color image (photos, paintings, diffuse maps), set texture.colorSpace = THREE.SRGBColorSpace. Without this, colors will look washed out.

Key Rules

  1. Add everything to ctx.root, never ctx.scene. The client clears root when switching sketches.
  2. Use ctx.THREE — do not import Three.js yourself.
  3. Use ctx.baseUrl for asset paths — not relative paths like ./assets/.
  4. Do not create a renderer, scene, or camera. The client provides these.
  5. Do not create an animation loop. Use the update() export instead.
  6. Do not import VRButton or NavigationController. The client handles VR and navigation.

Uploading

Upload via the admin panel or curl. For a sketch with assets, zip them together:

# zip your sketch
zip -r my-sketch.zip sketch.js assets/

# upload
curl -X POST \
  -H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
  -F "title=My Sketch" \
  -F "description=A cool 3D scene" \
  -F "files=@my-sketch.zip" \
  https://idam.art/api/sketches/upload

Common Recipes

Floor

const floor = new THREE.Mesh(
    new THREE.PlaneGeometry(20, 20).rotateX(-Math.PI / 2),
    new THREE.MeshStandardMaterial({ color: 0x808080 })
);
root.add(floor);

Text Label (Canvas Texture)

const canvas = document.createElement('canvas');
const ctx2d = canvas.getContext('2d');
canvas.width = 512; canvas.height = 128;
ctx2d.fillStyle = 'white';
ctx2d.font = '48px sans-serif';
ctx2d.fillText('Hello World', 10, 80);
const tex = new THREE.CanvasTexture(canvas);
const label = new THREE.Mesh(
    new THREE.PlaneGeometry(2, 0.5),
    new THREE.MeshBasicMaterial({ map: tex, transparent: true })
);
root.add(label);

Particle System

const positions = new Float32Array(1000 * 3);
for (let i = 0; i < 1000 * 3; i++) {
    positions[i] = (Math.random() - 0.5) * 20;
}
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const particles = new THREE.Points(geo,
    new THREE.PointsMaterial({ color: 0xffffff, size: 0.1 })
);
root.add(particles);

Gallery Wall with Framed Images

function createFramedImage(THREE, root, baseUrl, imagePath, x, y, z) {
    const loader = new THREE.TextureLoader();
    const tex = loader.load(baseUrl + imagePath);
    tex.colorSpace = THREE.SRGBColorSpace;

    // image plane
    const img = new THREE.Mesh(
        new THREE.PlaneGeometry(1.2, 0.9),
        new THREE.MeshStandardMaterial({ map: tex })
    );
    img.position.set(x, y, z);
    root.add(img);

    // frame
    const frame = new THREE.Mesh(
        new THREE.BoxGeometry(1.3, 1.0, 0.03),
        new THREE.MeshStandardMaterial({ color: 0x222222 })
    );
    frame.position.set(x, y, z - 0.02);
    root.add(frame);
}

Gotchas

Phone photos display sideways. Three.js TextureLoader does not apply EXIF orientation metadata. Photos taken on phones will silently render rotated. Pre-process images before uploading:
from PIL import Image, ImageOps
img = ImageOps.exif_transpose(Image.open('photo.jpg'))
img.save('photo.jpg')
Don't forget colorSpace. Without texture.colorSpace = THREE.SRGBColorSpace, your images will look desaturated and dark.
Shadows need setup. If you use rendererSettings: { shadowMap: true }, you also need to set light.castShadow = true and mesh.receiveShadow = true / mesh.castShadow = true on your objects.

Full Template

// === EXPORTED CONSTANTS (all optional) ===
export const startPosition = { x: 0, y: 1.6, z: 5 };
export const background = 0xf5f5f5;
export const hideDefaultEnvironment = true;
export const rendererSettings = {
    shadowMap: true,
    toneMapping: 'ACESFilmic',
    toneMappingExposure: 1.0
};

// === SETUP (required) ===
export function setup(ctx) {
    const { THREE, root, camera, renderer, player, baseUrl } = ctx;

    // lighting
    const light = new THREE.DirectionalLight(0xffffff, 1.0);
    light.position.set(5, 10, 5);
    root.add(light);
    root.add(new THREE.AmbientLight(0x404040));

    // floor
    const floor = new THREE.Mesh(
        new THREE.PlaneGeometry(20, 20).rotateX(-Math.PI / 2),
        new THREE.MeshStandardMaterial({ color: 0x808080 })
    );
    root.add(floor);

    // load a texture
    const loader = new THREE.TextureLoader();
    const texture = loader.load(baseUrl + 'assets/my-image.jpg');
    texture.colorSpace = THREE.SRGBColorSpace;

    const painting = new THREE.Mesh(
        new THREE.PlaneGeometry(2, 1.5),
        new THREE.MeshStandardMaterial({ map: texture })
    );
    painting.position.set(0, 1.5, -3);
    root.add(painting);

    return { painting };
}

// === DISPOSE (required, can be a no-op) ===
export function dispose(state) {
    // clean up non-Three.js resources only
    // geometries, materials, textures in root are auto-disposed
}

// === UPDATE (optional) ===
export function update(delta, ctx, state) {
    state.painting.rotation.y += 0.3 * delta;
}