how to create sketches for idam.art
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.
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
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;
}
All optional. Control how the client sets up the scene before calling setup().
| Export | Type | Description |
|---|---|---|
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) |
export const rendererSettings = {
shadowMap: true, // enable shadow mapping
toneMapping: 'ACESFilmic', // tone mapping algorithm
toneMappingExposure: 1.0 // exposure level
};
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 property | Description |
|---|---|
THREE | The Three.js library — use this instead of importing |
root | A THREE.Group — add all your objects here |
scene | The main scene (read-only — don't add objects here) |
camera | The perspective camera |
renderer | The WebGL renderer |
player | The player rig group (contains camera) |
baseUrl | URL prefix for loading assets (e.g. /sketches/001-my-sketch/) |
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();
}
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));
}
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 };
}
texture.colorSpace = THREE.SRGBColorSpace. Without this, colors will look washed out.
ctx.root, never ctx.scene. The client clears root when switching sketches.ctx.THREE — do not import Three.js yourself.ctx.baseUrl for asset paths — not relative paths like ./assets/.update() export instead.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
const floor = new THREE.Mesh(
new THREE.PlaneGeometry(20, 20).rotateX(-Math.PI / 2),
new THREE.MeshStandardMaterial({ color: 0x808080 })
);
root.add(floor);
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);
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);
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);
}
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')
texture.colorSpace = THREE.SRGBColorSpace, your images will look desaturated and dark.
rendererSettings: { shadowMap: true }, you also need to set light.castShadow = true and mesh.receiveShadow = true / mesh.castShadow = true on your objects.
// === 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;
}