Voxobox Viewer SDK v1 Canonical docs: https://voxobox.com/api/ SDK URL: https://voxobox.com/viewer-api/v1/index.js Core rules - Import with: import { VoxoboxViewer } from 'https://voxobox.com/viewer-api/v1/index.js'; - Create with: const viewer = await VoxoboxViewer.create({ iframe: '#viewer', model: 'cqgyCF76b1kE' }); - VoxoboxViewer.create exposes the created instance as window.viewer by default, so DevTools can run: console.log(await viewer.performance.getReport()); - All methods return promises. - Inputs and outputs are JSON-serializable. Do not pass DOM nodes, Three.js objects, or functions. - Vec3 values are [x, y, z]. Colors are CSS hex strings. Texture fields are URL strings or null to clear. - Texture rotation fields are radians. Scene/model rotation fields are degrees. - On-demand rendering: the viewer redraws only when something changes; every SDK method that alters the scene triggers a frame for you, so a still scene idles at near-zero GPU. Never add your own render loop. Avoid continuous motion (auto-rotate, looping animation, animated grain) unless explicitly requested — it forces a redraw every frame and keeps the GPU/fans busy. - Prefer small, cheap assets. For surface detail like bump, generate a small procedural texture (tiny canvas-drawn noise, or a low-res data texture) instead of loading a large image map; reuse one small texture across materials where possible. Quick start await viewer.camera.fit({ target: 'model', duration: 0.45 }); AI prompt builder for users The API page includes an Ask AI For Code builder under the Playground. Ask the user these questions, then return Playground-ready code: 1. What Voxobox model should the AI use? Accept a public Voxobox share link or model slug only. 2. What do you want the scene to look like? 3. Any material, color, or texture requirements? 4. Any camera, lighting, post-processing, annotation, or animation notes? When a share link is provided, normalize it to the slug. Example: https://voxobox.com/s/KzKAdNmK602X becomes KzKAdNmK602X. Use official docs: https://voxobox.com/api/ Use agent reference: https://voxobox.com/api/llms.txt Use public SDK: https://voxobox.com/viewer-api/v1/index.js Return copy-pasteable JavaScript that runs in the Voxobox API Playground, the model/share slug the code expects, assumptions, and a short visual QA checklist. Rules: use only the public Voxobox Viewer SDK; assume a ready viewer object exists in the Playground; if a model slug is available, returned JavaScript must begin with await viewer.load('MODEL_SLUG'); before applying scene changes; do not use private internals or editor-only helpers; inspect runtime lists before updating indexed resources; for lights, call await viewer.lights.list() and only update indexes that exist, or call await viewer.lights.add(...) and then refresh the list before updating; for lightweight or embed-friendly scenes, include await viewer.quality.set({ mode: 'low' });, avoid live shadows where possible, disable heavy post effects that are not essential, and finish with console.log(await viewer.performance.getReport()); so the user can inspect the result; make visual changes obvious without washing out the product; for moods like sunset or misty, prefer warm background color, warm key/fill lights, exposure/tone mapping, soft shadows, bloom, depth of field, vignette, and grain; keep the environment mostly neutral unless the user explicitly asks to tint the whole model; do not ask for or use passwords; if upload or account access is required, ask the user to upload the model first or provide a share link. Recipes by Editor tab Scene Use Scene for camera, model rotation, wireframe, nodes, and selection. viewer.camera.get() -> { eye, target } viewer.camera.lookAt(eyeVec3, targetVec3, duration?) viewer.camera.fit({ target: 'model' | 'selection' | 'material' | 'node', name?, duration? }) viewer.camera.setConstraints({ enablePan, minPolarAngle, maxPolarAngle, minAzimuthAngle, maxAzimuthAngle, minDistance, maxDistance }) viewer.camera.setFov(degrees) viewer.camera.getAngles() viewer.camera.reset() viewer.scene.getRotation() viewer.scene.setRotation(xDegrees, yDegrees, zDegrees) viewer.scene.rotateBy('x' | 'y' | 'z', degrees) viewer.scene.resetRotation() viewer.scene.setWireframe({ enabled, color, opacity, materialVisible }) viewer.nodes.list() viewer.nodes.show(name) viewer.nodes.hide(name) viewer.nodes.setVisible(name, visible) viewer.nodes.setShadow(name, { cast?, receive? }) viewer.nodes.focus(name, options?) viewer.selection.get() viewer.selection.clear() viewer.selection.pick(options) Lighting Use Lighting for environment, background, lights, and ground shadows. viewer.environment.get() viewer.environment.set({ hdri?, fileName?, color?, intensity?, blur?, rotation? }) viewer.environment.setExposure(value) viewer.environment.setToneMapping('linear' | 'reinhard' | 'cineon' | 'aces' | 'agx') viewer.environment.setFlat(color, intensity?) viewer.background.set({ color?, alpha?, image?, visible?, useEnvironment?, useAsEnvironment?, intensity?, blur?, rotation?, fileName? }) viewer.lights.list() viewer.lights.get(index) viewer.lights.update(index, { type?, color?, intensity?, position?, visible?, falloff?, angle?, softness?, castShadows?, shadowBias?, cameraAttached? }) viewer.lights.add(descriptor?) viewer.lights.update auto-creates missing light slots up to the requested index, so code may update index 3 even when only indexes 0-2 exist. viewer.lights.add returns the created light descriptor. viewer.lights.remove(index) viewer.lights.setType(index, type) viewer.lights.aimAt(index, targetVec3) viewer.lights.showHelpers(enabled) viewer.lights.setGroundShadow({ enabled, mode: 'shadow-catcher' | 'baked-ao', intensity, borderFade, height, shadowDiffusion, bakedBlur, environmentShadows, fadeMode: 'model' | 'circle' }) mode 'shadow-catcher' is the live, soft contact shadow (PCF); mode 'baked-ao' captures that same shadow once into a static texture (cheaper to display) — bakedBlur (0+) softens the baked texture. fadeMode defaults to 'circle'. The baked texture is anchored to the model, so it tracks the model and matches the live catcher. viewer.lights.getBakedShadow() Materials Use Materials for PBR maps, alpha mask, AO, anisotropy, cavity, clear coat, displacement, emission, faces, normal, opacity, roughness, sheen, and subsurface. viewer.materials.list() -> [{ name, uvCount, channels }] viewer.materials.get(name) viewer.materials.update(name, channels) viewer.materials.replaceTexture(name, channel, textureUrlOrNull) viewer.materials.highlight(name, options?) -> briefly highlight a material. options: { outline?: boolean, color?: CSS color string, strength?: number, thickness?: number, tintColor?: CSS color string, tintStrength?: number, tintIntensity?: number, fillColor?: CSS color string, fillStrength?: number, fillKeepTexture?: boolean }; set outline:false to disable edge outline; color/strength/thickness control the outline; tintColor adds a temporary emissive/material tint; fillColor temporarily replaces the material fill color and removes the albedo texture by default so the fill is solid; fillKeepTexture:true keeps the texture visible. clearHighlight() restores temporary highlight changes. viewer.materials.clearHighlight() UV Channel is read-only in v1: inspect (await viewer.materials.get(name)).uvCount. Material channel schema albedo: { color, intensity, texture, scale: [u, v], offset: [u, v], rotation } metalness: { value, texture, scale: [u, v], offset: [u, v], rotation } specularF0: { value, texture, scale: [u, v], offset: [u, v], rotation } roughness: { value?, glossiness?, texture, scale: [u, v], offset: [u, v], rotation } normalMap: { mode: 'normal' | 'bump', scale, texture, texScale: [u, v], texOffset: [u, v], texRotation } displacement: { scale, texture, texScale: [u, v], texOffset: [u, v], texRotation } anisotropy: { value, rotation, texture, swapXY, texScale: [u, v], texOffset: [u, v], texRotation } sheen: { intensity, color, texture, texScale: [u, v], texOffset: [u, v], texRotation } subsurface: { intensity, thicknessFactor, color, subsurfaceColor, falloffColor, texture, texScale: [u, v], texOffset: [u, v], texRotation, translucency, transTexture, transScale: [u, v], transOffset: [u, v], transRotation } clearCoat: { value, roughness, normalScale, flipGreen, coatThickness, coatReflectivity, coatTint, texture, texScale: [u, v], texOffset: [u, v], texRotation, roughnessTexture, roughnessTexScale: [u, v], roughnessTexOffset: [u, v], roughnessTexRotation, normalTexture, normalTexScale: [u, v], normalTexOffset: [u, v], normalTexRotation } ao: { intensity, occludeSpecular, texture, texScale: [u, v], texOffset: [u, v], texRotation } cavity: { intensity, texture, texScale: [u, v], texOffset: [u, v], texRotation } alphaMap: { intensity, invert, texture, texScale: [u, v], texOffset: [u, v], texRotation } opacity: { enabled, mode: 'blending' | 'refraction' | 'additive' | 'dithered', value, color, invert, texture, texScale: [u, v], texOffset: [u, v], texRotation } emissive: { color, intensity, texture, texScale: [u, v], texOffset: [u, v], texRotation } doubleSided: boolean Material example const names = (await viewer.materials.list()).map((material) => material.name); for (const name of names) { await viewer.materials.update(name, { albedo: { color: '#ffffff', intensity: 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 }, opacity: { enabled: false, mode: 'blending', value: 1, color: '#ffffff', invert: false, texture: null }, doubleSided: true }); } Post-processing viewer.post.set(options) batches effect keys such as { bloom, ssao, dof }. viewer.post.setBloom({ enabled, threshold, intensity, radius }) viewer.post.setChromatic({ enabled, amount }) viewer.post.setColorBalance({ enabled, shadows: [r, g, b], midtones: [r, g, b], highlights: [r, g, b] }) viewer.post.setDOF({ enabled, foregroundBlur, backgroundBlur, transition }) viewer.post.setGrain({ enabled, amount, animated }) viewer.post.setSSR({ enabled, intensity }) viewer.post.setSharpness({ enabled, amount }) viewer.post.setSSAO({ enabled, radius, intensity, bias }) viewer.post.setGTAO(params) viewer.post.setAntialiasing({ enabled, transparentPixels }) viewer.post.setToneMapping({ enabled, type, exposure, brightness, contrast, saturation }) viewer.post.setVignette({ enabled, amount, hardness, offsetX, offsetY, scaleX, scaleY, inner, falloff, darkness }) Annotations Annotation shape: { id, position3D: Vec3, title, body, color, eye: Vec3, target: Vec3 } Annotation settings: { defaultColor, transitionSpeed, dwellTime } viewer.annotations.list() viewer.annotations.set(annotations, settings?) viewer.annotations.add(annotation) viewer.annotations.update(id, patch) viewer.annotations.remove(id) viewer.annotations.clear() viewer.annotations.focus(id, { duration? }) viewer.annotations.getSettings() viewer.annotations.setSettings(settings) Animation Clip shape: { index, name, duration } viewer.animation.list() viewer.animation.play(indexOrName, { loop?, speed? }) viewer.animation.pause() viewer.animation.resume() viewer.animation.stop() viewer.animation.seek(seconds) viewer.animation.setSpeed(speed) viewer.animation.setLoop(loop) viewer.animation.getState() Capture and diagnostics viewer.screenshot.capture({ width?, height?, qualityScale?, format?: 'png' }) -> { dataUrl, mimeType } viewer.performance.getReport() Quality Default quality mode is auto: devicePixelRatio capped at 2. viewer.quality.get() viewer.quality.set({ mode: 'auto' | 'low' | 'balanced' | 'high' }) viewer.quality.set({ pixelRatio }) viewer.quality.set({ maxFps }) Modes map to render pixel ratios and active render caps: low = pixelRatio 1 + 30fps, balanced = pixelRatio 1.5 + 45fps, high = pixelRatio 2 + uncapped, auto = devicePixelRatio capped at 2 + uncapped. Custom pixelRatio is clamped from 0.5 to 2. Custom maxFps is clamped from 1 to 120; null or 0 removes the cap. Top-level methods viewer.load(model, options?) viewer.load accepts a public share slug, a https://voxobox.com/s/... share link, or a direct .glb/.gltf URL. For share links/slugs, it resolves the public CDN model URL and saved material settings before loading. By default it restores saved materials only, not the Editor's saved camera/lights/background/post look, so Playground code owns the final scene. Pass { restoreScene: true, restoreCamera: true } only when explicitly reusing the saved Editor look. viewer.destroy() viewer.getState() viewer.applyState(state, options?) viewer.transaction([{ method, args }]) viewer.configureVariant(variant) viewer.createTurntable({ speed?, axis?, pauseOnInteraction? }) Events viewer.events.on(event, callback) returns an unsubscribe function. viewer.events.off(event, callback) Event names: viewer.ready, model.progress, model.loaded, camera.changed, selection.changed, material.changed, animation.finished, light.moved, error. Error codes VERSION_MISMATCH, NOT_READY, NOT_FOUND, INVALID_METHOD, INVALID_ARGUMENTS, ORIGIN_NOT_ALLOWED, RATE_LIMITED, TIMEOUT, CANCELLED, VIEWER_ERROR. Batch example const [material] = await viewer.materials.list(); if (material) { await viewer.transaction([ { method: 'materials.update', args: [material.name, { albedo: { color: '#334155' } }] }, { method: 'camera.fit', args: [{ target: 'material', name: material.name, duration: 0.35 }] } ]); }