Viewer SDK v1

One API. Built for humans and AI agents.

Embed a public share, wait for the viewer to become ready, then drive camera, materials, lighting, animation, screenshots, and saved states through one promise-first JavaScript API. Paste code from the Editor's API buttons into the playground below, or browse the complete reference.

For humans

Click an API button in the Editor to copy a ready-to-run snippet, paste it into the playground below, hit RUN, and tweak. The reference is a single scannable table.

For AI agents

Every method, event, and error code is on this page as plain text — no UI to click. A machine-readable mirror lives at /api/llms.txt. The SDK is JSON-only, namespaced, and stable within v1.

Quick Start

Use a public share slug. The SDK creates or binds an iframe, performs a version handshake, and returns a ready viewer object.

Browser ESM
import { VoxoboxViewer } from 'https://voxobox.com/viewer-api/v1/index.js';

const viewer = await VoxoboxViewer.create({
  iframe: '#viewer',
  model: 'cqgyCF76b1kE',
});

await viewer.camera.fit({ target: 'model' });
Runs this flow on the demo sphere.

          

Playground

Paste any SDK code below — including snippets copied from the Editor's API buttons — and hit RUN. The viewer instance is pre-created and exposed as viewer, so you can skip VoxoboxViewer.create() and just write the calls.

How to use the Editor → Playground flow:

  • Open the Editor, configure the menu you want (e.g. Lighting → Environment).
  • Click the menu's API button to copy the snippet.
  • Paste it below and hit RUN — the playground reuses the demo sphere so you see your settings applied immediately.
Ready.

          

Ask AI For Code

Answer these questions, then copy the generated prompt into ChatGPT, Claude, Gemini, or another coding agent. The agent should return Playground-ready Voxobox code using the public SDK only.

Use a public share link or slug. Do not enter your Voxobox password.
Ready.

Recipes By Editor Tab

These groups match the Editor's top-level tabs. Each group combines runnable code with the exact object shapes an AI agent needs when it never opens the Editor. Paste any snippet into the playground above; viewer is already available there.

Scene

Use Scene for camera constraints, camera framing, model rotation, wireframe, node visibility, and selection state.

Scene recipe
await viewer.camera.fit({ target: 'model', duration: 0.45 });
await viewer.camera.lookAt([2, 1.5, 3], [0, 0.5, 0], 0.45);
await viewer.camera.setConstraints({
  enablePan: true,
  minPolarAngle: 0,
  maxPolarAngle: Math.PI,
  minAzimuthAngle: -Infinity,
  maxAzimuthAngle: Infinity,
  minDistance: 0.1,
  maxDistance: 100,
});
await viewer.camera.setFov(45);

await viewer.scene.setRotation(0, 0, 0);
await viewer.scene.rotateBy('y', 15);
await viewer.scene.setWireframe({
  enabled: true,
  color: '#111111',
  opacity: 1,
  materialVisible: true,
});

const nodes = await viewer.nodes.list();
if (nodes[0]?.name) {
  await viewer.nodes.setVisible(nodes[0].name, true);
  await viewer.nodes.focus(nodes[0].name);
}
await viewer.selection.clear();
  • camera.fit accepts { target: 'model' | 'selection' | 'material' | 'node', name?, duration? }.
  • scene.setRotation and scene.rotateBy use degrees.
  • camera.setConstraints takes radians for polar/azimuth limits and world units for distance limits.
Lighting

Lighting covers environment, visible background, individual lights, and ground shadows.

Lighting recipe
await viewer.environment.set({
  color: '#f5f5f5',
  intensity: 1,
  blur: 0,
  rotation: 0,
});
await viewer.environment.setExposure(1);
await viewer.environment.setToneMapping('aces');
await viewer.environment.setFlat('#f5f5f5', 1);

await viewer.background.set({
  color: '#ffffff',
  alpha: 1,
  image: null,
  visible: true,
  useEnvironment: false,
  useAsEnvironment: false,
  intensity: 1,
  blur: 0,
  rotation: 0,
  fileName: null,
});

const existingLights = await viewer.lights.list();
if (existingLights[0]) {
  await viewer.lights.update(0, {
    type: 'directional',
    color: '#ffffff',
    intensity: 1,
    position: [4, 6, 4],
    visible: true,
    falloff: 1,
    angle: 0.6,
    softness: 0.2,
    castShadows: true,
    shadowBias: -0.0002,
    cameraAttached: false,
  });
}

await viewer.lights.setGroundShadow({
  enabled: true,
  mode: 'shadow-catcher',
  intensity: 1,
  borderFade: 0.25,
  height: 0,
  shadowDiffusion: 0.6,
  bakedBlur: 0,
  environmentShadows: false,
  fadeMode: 'model',
});
  • environment.set accepts { hdri?, fileName?, color?, intensity?, blur?, rotation? }.
  • environment.setToneMapping accepts 'linear' | 'reinhard' | 'cineon' | 'aces' | 'agx'.
  • lights.setGroundShadow.mode is 'shadow-catcher' or 'baked-ao'; fadeMode is 'model' or 'circle'.
Materials

Materials use viewer.materials.update(name, channels). Patch only the channel fields you need; use null to clear a texture. UV channel information is read-only in v1 via viewer.materials.get(name).

Material recipe
const materials = await viewer.materials.list();
const targetMaterials = materials.map((material) => material.name);
const channels = {
  albedo: { color: '#ffffff', intensity: 1, texture: null, scale: [1, 1], offset: [0, 0], rotation: 0 },
  metalness: { value: 0, texture: null, scale: [1, 1], offset: [0, 0], rotation: 0 },
  specularF0: { value: 1, texture: null, scale: [1, 1], offset: [0, 0], rotation: 0 },
  roughness: { value: 0.5, texture: null, scale: [1, 1], offset: [0, 0], rotation: 0 },
  normalMap: { mode: 'normal', scale: 1, texture: null, texScale: [1, 1], texOffset: [0, 0], texRotation: 0 },
  displacement: { scale: 0, texture: null, texScale: [1, 1], texOffset: [0, 0], texRotation: 0 },
  anisotropy: { value: 0, rotation: 0, texture: null, swapXY: false, texScale: [1, 1], texOffset: [0, 0], texRotation: 0 },
  sheen: { intensity: 0, color: '#000000', texture: null, texScale: [1, 1], texOffset: [0, 0], texRotation: 0 },
  subsurface: {
    intensity: 0,
    thicknessFactor: 0,
    color: '#ffffff',
    subsurfaceColor: '#ffffff',
    falloffColor: '#ffffff',
    texture: null,
    texScale: [1, 1],
    texOffset: [0, 0],
    texRotation: 0,
    translucency: 0,
    transTexture: null,
    transScale: [1, 1],
    transOffset: [0, 0],
    transRotation: 0,
  },
  clearCoat: {
    value: 0,
    roughness: 0,
    normalScale: 1,
    flipGreen: false,
    coatThickness: 5,
    coatReflectivity: 0.5,
    coatTint: '#ffffff',
    texture: null,
    texScale: [1, 1],
    texOffset: [0, 0],
    texRotation: 0,
    roughnessTexture: null,
    roughnessTexScale: [1, 1],
    roughnessTexOffset: [0, 0],
    roughnessTexRotation: 0,
    normalTexture: null,
    normalTexScale: [1, 1],
    normalTexOffset: [0, 0],
    normalTexRotation: 0,
  },
  ao: { intensity: 1, occludeSpecular: false, texture: null, texScale: [1, 1], texOffset: [0, 0], texRotation: 0 },
  cavity: { intensity: 0, texture: null, texScale: [1, 1], texOffset: [0, 0], texRotation: 0 },
  alphaMap: { intensity: 1, invert: false, texture: null, texScale: [1, 1], texOffset: [0, 0], texRotation: 0 },
  opacity: { enabled: false, mode: 'blending', value: 1, color: '#ffffff', invert: false, texture: null, texScale: [1, 1], texOffset: [0, 0], texRotation: 0 },
  emissive: { color: '#000000', intensity: 0, texture: null, texScale: [1, 1], texOffset: [0, 0], texRotation: 0 },
  doubleSided: true,
};

for (const materialName of targetMaterials) {
  await viewer.materials.update(materialName, channels);
}
  • PBR Maps: albedo, metalness, specularF0.
  • Roughness: use roughness.value for roughness workflow or roughness.glossiness for glossiness workflow.
  • Normal: normalMap.mode is 'normal' or 'bump'.
  • Opacity mode: 'blending' | 'refraction' | 'additive' | 'dithered'.
  • Faces: doubleSided: true for double-sided, false for single-sided.
Post-processing

Post-processing setters all take one plain object. enabled should include the master post-processing state when you mirror the Editor.

Post-processing recipe
await viewer.post.setBloom({ enabled: true, threshold: 0.35, intensity: 0.4, radius: 0.45 });
await viewer.post.setChromatic({ enabled: true, amount: 0.005 });
await viewer.post.setColorBalance({
  enabled: true,
  shadows: [0, 0, 0],
  midtones: [0, 0, 0],
  highlights: [0, 0, 0],
});
await viewer.post.setDOF({ enabled: true, foregroundBlur: 0.5, backgroundBlur: 0.5, transition: 0.35 });
await viewer.post.setGrain({ enabled: true, amount: 0.15, animated: true });
await viewer.post.setSSR({ enabled: true, intensity: 0.25 });
await viewer.post.setSharpness({ enabled: true, amount: 0.2 });
await viewer.post.setSSAO({ enabled: true, radius: 0.05, intensity: 0.35, bias: 0.025 });
await viewer.post.setAntialiasing({ enabled: true, transparentPixels: true });
await viewer.post.setToneMapping({ enabled: true, type: 'linear', exposure: 1, brightness: 0, contrast: 0, saturation: 1 });
await viewer.post.setVignette({
  enabled: true,
  amount: 0.85,
  hardness: 0.45,
  offsetX: 0,
  offsetY: 0,
  scaleX: 0.9,
  scaleY: 0.82,
  inner: 0.08,
  falloff: 0.85,
  darkness: 1,
});
Annotations

Annotations are saved 3D points plus optional camera positions. Use focus to move the camera to a saved annotation view.

Annotations recipe
const annotations = [
  {
    id: 'detail-1',
    position3D: [0, 0.5, 0],
    title: 'Detail',
    body: 'Saved note text',
    color: '#444444',
    eye: [2, 1.5, 3],
    target: [0, 0.5, 0],
  },
];

await viewer.annotations.set(annotations, {
  defaultColor: '#444444',
  transitionSpeed: 0.8,
  dwellTime: 3,
});
await viewer.annotations.add({ id: 'detail-2', position3D: [0.25, 0.5, 0], title: 'Second note' });
await viewer.annotations.update('detail-2', { body: 'Updated note text' });
await viewer.annotations.focus('detail-1', { duration: 0.8 });
Animation

Animation works by clip index or clip name. Always call list() first if you do not know which clips a model contains.

Animation recipe
const clips = await viewer.animation.list();
if (clips[0]) {
  await viewer.animation.play(clips[0].name ?? 0, { loop: true, speed: 1 });
  await viewer.animation.seek(0);
  await viewer.animation.setSpeed(1);
  await viewer.animation.setLoop(true);
  const state = await viewer.animation.getState();
  if (state.playing) await viewer.animation.pause();
  await viewer.animation.resume();
  await viewer.animation.stop();
}
Capture & Diagnostics

These controls are not Editor tabs, but they are useful for agents that need output or runtime checks.

Screenshot and performance recipe
const screenshot = await viewer.screenshot.capture({
  width: 1200,
  height: 900,
  qualityScale: 1,
  format: 'png',
});

const report = await viewer.performance.getReport();
console.log({ screenshot, report });

API Reference

Every method on the v1 SDK, grouped by namespace. Reconciled against /viewer-api/v1/index.js. All methods return a promise and accept/return JSON-serializable values only. Each row has a stable anchor (e.g. #ref-camera-fit) for deep links.

viewer

Top-level methods (snapshots, batching, lifecycle).
SignatureDescription
load(model, options?)Load a different public share slug or https://voxobox.com/s/... share link into the existing viewer. Direct .glb/.gltf URLs are also accepted. Share loads do not restore saved Editor materials by default; pass { restoreMaterials: true } only when you explicitly want that saved material look. Add { restoreScene: true, restoreCamera: true } when restoring the Editor's saved camera/lights/background/post look too.
destroy()Tear down the iframe and reject any pending calls.
getState()Full serializable snapshot (camera, materials, lights, env, post).
applyState(state, options?)Restore a snapshot previously returned by getState.
transaction(operations)Apply a batch of { method, args } atomically. Preferred for agents.
configureVariant(variant)Apply a high-level named variant declared on the share.
createTurntable(options?)Auto-rotate the camera. Options: { speed?, axis?, pauseOnInteraction? }.

viewer.camera

Orbit, framing, FOV, constraints.
SignatureDescription
get()Returns { eye, target } as Vec3s.
lookAt(eye, target, duration?)Move the camera. Each of eye/target is [x, y, z]. Animates if duration > 0.
fit({ target?, name?, duration? })Frame the scene. target: 'model' | 'selection' | 'material' | 'node'; name required for material/node.
setConstraints(options)Limit orbit/zoom range.
setFov(degrees)Set vertical field of view.
getAngles()Current azimuth/polar angles.
reset()Restore the share's default camera.

viewer.scene

Scene-level overlays.
SignatureDescription
setWireframe({ enabled?, color?, opacity?, materialVisible? })Toggle the wireframe overlay and tune its color, opacity, and whether the underlying material stays visible. Passing a boolean is shorthand for { enabled }.
getRotation()Current model rotation in degrees: { x, y, z }.
setRotation(x, y, z)Set absolute model rotation in degrees.
rotateBy(axis, degrees)Increment one axis ('x' | 'y' | 'z') by N degrees. Returns the new rotation.
resetRotation()Snap model rotation back to { x: 0, y: 0, z: 0 }.

viewer.materials

List, edit, swap textures.
SignatureDescription
list()Array of { name, uvCount, channels }.
get(name)Full material descriptor, or null if not found.
update(name, channels)Patch channels: { albedo: { color }, roughness: { value }, normal: { texture } }, etc.
replaceTexture(name, channel, texture)Swap a single texture channel. Pass null to clear.
highlight(name, options?)Briefly highlight a material for UI feedback. Options: { outline?, color?, strength?, thickness?, tintColor?, tintStrength?, tintIntensity?, fillColor?, fillStrength?, fillKeepTexture? }. Set outline: false to disable the edge outline. color, strength, and thickness control the outline. tintColor adds a temporary emissive/material tint. fillColor temporarily replaces the material fill color; by default it removes the albedo texture so the fill is solid. Use fillKeepTexture: true to keep the texture visible. All temporary highlight changes are restored by clearHighlight().
clearHighlight()Clear any active material highlight.

viewer.nodes

Show, hide, focus scene nodes.
SignatureDescription
list()Flat array of scene nodes.
show(name)Show a node.
hide(name)Hide a node.
setVisible(name, visible)Set visibility explicitly.
focus(name, options?)Frame this node with the camera.

viewer.selection

Hover/click selection state.
SignatureDescription
get()Current selection (material, node, point).
clear()Clear the current selection.
pick(options)Programmatically pick at a screen coordinate.

viewer.annotations

Saved 3D notes and camera jumps.
SignatureDescription
list()Return all annotations.
set(annotations, settings?)Replace annotations and optionally update settings.
add(annotation)Add one annotation. Shape: { id?, position3D, title?, body?, color?, eye?, target? }.
update(id, patch)Patch an annotation by id.
remove(id)Remove an annotation by id.
clear()Remove all annotations.
focus(id, options?)Move the camera to the annotation's saved eye/target. Options: { duration? }.
getSettings()Return annotation settings.
setSettings(settings)Patch settings such as defaultColor, transitionSpeed, and dwellTime.

viewer.environment

HDRI, exposure, tone mapping.
SignatureDescription
get()Current environment settings.
set({ hdri?, color?, fileName?, intensity?, blur?, rotation? })Set the environment from an HDRI URL or flat color.
setExposure(value)Linear exposure multiplier.
setToneMapping(mode)One of 'linear' | 'reinhard' | 'cineon' | 'aces' | 'agx'.
setFlat(color, intensity?)Use a flat color instead of an HDRI.

viewer.background

Visible canvas background.
SignatureDescription
set({ color?, alpha?, image?, useEnvironment?, blur?, ... })Solid color, transparent, image, or use the environment as the background.

viewer.lights

Manage scene lights and ground shadow.
SignatureDescription
list()Array of { index, type, position, intensity, color, ... }.
get(index)Full descriptor for a single light.
update(index, patch)Patch any light property. If the index does not exist yet, the viewer creates enough lights to make that index valid, then applies the patch. Use visible: false to hide.
add(descriptor)Add a new light and apply the descriptor. Returns the created light descriptor.
remove(index)Remove a light.
setType(index, type)Convert a light to a different type.
aimAt(index, target)Aim a directional or spot light at a point.
showHelpers(enabled)Show or hide the editor-style light helpers in the viewer.
setGroundShadow({ enabled, mode?, intensity?, borderFade?, height?, shadowDiffusion?, bakedBlur?, environmentShadows?, fadeMode? })Toggle and tune the ground contact shadow. mode: 'shadow-catcher' | 'baked-ao'. fadeMode: 'model' | 'circle'.
getBakedShadow()Return the current baked shadow texture (if any).

viewer.post

Post-processing effects (per-effect setters).
SignatureDescription
set(options)Batch setter. Pass an object such as { bloom: {...}, ssao: {...} }; keys match the dedicated setters.
setSSR(params)Screen-space reflections.
setSSAO(params)Screen-space ambient occlusion.
setDOF(params)Depth of field.
setBloom(params)Bloom.
setGrain(params)Film grain.
setVignette(params)Vignette.
setChromatic(params)Chromatic aberration.
setSharpness(params)Sharpness / contrast-adaptive sharpening.
setToneMapping(params)Tone-mapping curve override on the post pass.
setColorBalance(params)Lift / gamma / gain color balance.
setAntialiasing(params)Antialiasing mode (e.g. FXAA, TAA).

viewer.animation

Playback control.
SignatureDescription
list()Available clips with names and durations.
play(indexOrName, options?)Start a clip by index or name. Options: { loop?, speed? }.
pause()Pause the active clip.
resume()Resume the active clip.
stop()Stop playback and reset time.
seek(seconds)Jump to a time in seconds.
setSpeed(speed)Playback speed multiplier.
setLoop(loop)Toggle looping for the active clip.
getState()Current playback state for all clips.

viewer.screenshot

Capture a PNG.
SignatureDescription
capture({ width?, height?, qualityScale?, format? })Returns { dataUrl, mimeType }. format defaults to 'png'.

viewer.performance

Frame-rate and memory diagnostics.
SignatureDescription
getReport()Snapshot of FPS, draw calls, GPU memory, and other counters.

viewer.events

Subscribe to viewer events.
Method / EventDescription
on(event, callback)Subscribe. Returns an unsubscribe function.
off(event, callback)Unsubscribe a specific callback.
viewer.readyEmitted once after the version handshake completes.
model.progress{ pct } while the model loads.
model.loadedEmitted once the model is fully loaded.
camera.changedEmitted after camera movement settles.
selection.changedEmitted when the hovered/picked material or node changes.
material.changedEmitted after a successful materials.update.
animation.finishedEmitted when a non-looping clip completes.
light.movedEmitted after a light position update.
errorGeneric error channel; payload includes { code, message }.

Errors

Rejected promises carry a stable string code alongside the human message. Agents should branch on code, not message. The codes currently emitted by the SDK and viewer controller:

  • VERSION_MISMATCH — the parent SDK and iframe RPC versions do not match.
  • NOT_READY — the viewer is not ready to accept calls.
  • TIMEOUT — the handshake or an individual call did not complete in time.
  • CANCELLED — a call was rejected because the RPC client was destroyed (typically after viewer.destroy()).
  • NOT_FOUND — referenced node or animation does not exist.
  • INVALID_METHOD — a method name is not part of the v1 contract.
  • INVALID_ARGUMENTS — a method received unsupported arguments.
  • ORIGIN_NOT_ALLOWED — this parent origin is not allowed for the share.
  • RATE_LIMITED — too many calls in a rate-limited window, most commonly screenshots.
  • VIEWER_ERROR — the iframe viewer failed while handling a valid call.

Agent Contract

Prefer high-level methods. Use viewer.transaction, viewer.configureVariant, viewer.camera.fit, viewer.getState, and viewer.applyState first. Drop to per-domain methods only when a task needs precise control.

Inputs and outputs are JSON. No DOM nodes, no Three.js objects, no functions cross the iframe boundary.

Stable surface. Within v1, method names, event names, and error codes do not change. Breaking changes ship as v2 at a new URL.

Discoverability. Fetch /api/llms.txt for the machine-readable mirror of this page, or scrape the API reference tables above — every td.sig cell is a signature.

Agent-safe batch updateserializable JSON only
await viewer.transaction([
  { method: 'materials.update', args: ['Fabric', { albedo: { color: '#334155' } }] },
  { method: 'camera.fit',       args: [{ target: 'material', name: 'Fabric', duration: 0.35 }] }
]);

Examples