@liveroom-tech/react-immersive
v4.2.2
Published
React-based 3D model viewer for interactive GLB, GLTF, OBJ, FBX, and USDZ assets, with mesh selection, object bindings, and a built-in design-time editor.
Maintainers
Readme
React Immersive — 3D model viewer for React
@liveroom-tech/react-immersive is a React-based 3D model viewer for interactive GLB, GLTF, OBJ, FBX, and USDZ assets. It renders a model with React Three Fiber, lets users click named meshes, shows a built-in side panel for the selected object, and supports per-object actions such as color changes, texture uploads, and visibility toggles.
Website · Documentation · Live editor · npm · GitHub
What It Does
The library currently exports:
import {
BindingBuilder,
material,
ModelViewer,
patchBindings,
patchSceneConfig,
SimpleModelViewer,
useObjectBinding,
useObjectBindingIds,
useObjectBindings,
useSceneConfig,
useViewerActions,
useViewerAnimations,
useViewerCamera,
useViewerConnection,
useViewerEffects,
useViewerHover,
useViewerModel,
useViewerSelection,
MATERIAL_BLENDING_MODES,
MATERIAL_SIDES,
} from "@liveroom-tech/react-immersive";It also exports types for ObjectBinding, ObjectBindingMaterial, ObjectBindingsController, ObjectActionEvent, ObjectTransformSpace, SceneConfig, SceneConfigPatch, SceneConfigController, AnimationControls, CinematicConfig / CinematicWaypoint, and related shapes used throughout this document.
ModelViewer provides:
- GLB/GLTF, OBJ/MTL, FBX, and USDZ model rendering through
@react-three/fiber - camera-driven 3D Tiles streaming through
tilesetUrl, with configurable screen-space error and bounded tile cache - orbit/pan/zoom camera controls via
CameraControls - auto-fits the model on load, and re-fits when the canvas is resized (window resize, device rotation, or a panel opening/closing), toggleable through
refitOnResizeto instead preserve the user's orbit/zoom - optional custom scene lighting through a
lightsprop - optional custom camera configuration through a
cameraprop - optional custom background color through a
backgroundColorprop - optional shadow rendering toggle through a
shadowsprop (on by default) - optional on-screen movement controller through
showMouseControllerand its tuning props - optional canvas-focused keyboard camera navigation through
enableKeyboardNavigation - optional top-right camera orientation control through
showViewGizmo - optional per-object move and rotate gizmo through
moveModeEnabled, with World/Local axis alignment throughobjectTransformSpace - optional custom left object data panel through
customObjectBindingDataPanel - optional custom right scene objects panel through
customSceneObjectsPanel - optional hiding of either built-in panel through
showObjectBindingDataPanelandshowSceneObjectsPanel - optional model export button through
showDownloadButtonanddownloadFilename, opt-in PNG capture throughpreserveDrawingBuffer, plus an independentshowResetButtontoggle for the rest of that action bar - an optional UV Checker toolbar button through
showUvCheckerButton, for inspecting UV scale, stretching, seams, orientation, and missing UV0 coordinates directly inModelViewer - click-to-measure distance tool and a bounding-box dimensions overlay through
showMeasureToolsandmeasurementUnit - an exploded-view slider that slides each bound part outward from the model center to reveal interior/assembly structure through
showExplodeControls - a cinematic auto-camera that glides the camera along authored waypoints (or a zero-config showcase orbit), like a film, through
cinematic, with a play/pause control that yields the moment you touch the camera - a guided-tour control cluster (Previous/Stop/Next) for stepping through annotations, toggleable via
showAnnotationNavigation - optional annotation detail popups on marker hover via
showAnnotationOnHover - PBR, Matcap, and UV Checker renderer modes, with the checker exposing UV stretching, seams, rotation, and missing UV0 data directly on the model
- a
sceneConfigprop accepting the same scene-wide config (lighting, environment, background, post-processing, animations, annotations) authored byBindingBuilder's Scene tab - render-loop and perf tuning through
renderMode,maxDpr,performanceProfile, and compressed-asset decoder options (dracoDecoderPath,ktx2TranscoderPath,meshopt) - WebXR "View in your space" (AR) and "Enter VR" through
enableXR, with tap-to-place, pinch-to-resize, and twist-to-rotate AR placement gestures - mesh selection by object name
- a built-in left side panel for the selected object
- object-specific action buttons driven by
objectBindings - built-in color picking that commits the chosen hex into
objectBindings.style.material.baseColor - built-in texture upload that commits a URL into
objectBindings.style.material.texture.path(blob URL by default, or a durable URL whenonTextureUploadis provided) - per-object
MeshPhysicalMaterialoverrides for metalness, roughness, emissive, normal/bump, AO, displacement, clearcoat, sheen, anisotropy, specular, transmission, thickness, reflectivity, sidedness, and limited blending modes - per-object visibility toggling through
objectBindings.visible - hover and selected-state highlighting
- external selection state control through
selectedObjectandonObjectSelect - hover callbacks through
onObjectHover - model-ready callbacks through
onModelLoaded - model-load error callbacks through
onLoadError - camera change callbacks through
onCameraChange - viewer-ready callbacks through
onViewerReady - action event callbacks through
onAction - animation playback for GLB/GLTF, FBX, and USDZ models with supported embedded animations through
onAnimationsReady - annotation marker callbacks through
onAnnotationsChangeand controlled/uncontrolledactiveAnnotation/onActiveAnnotationChange
BindingBuilder provides:
- a design-time UI for generating starter bindings from GLB, GLTF, OBJ, FBX, and USDZ assets, gated by
licenseKey(see "BindingBuilderlicensing & plan tiers" below) - demo model loading or custom
.glb,.gltf,.obj,.fbx,.usdz, or model ZIP bundle upload - editable binding fields for identity, basics, style, actions, metrics, metadata, and saved camera state
- per-object move and rotate authoring with a geometry-centered pivot, World/Local axes, numeric fields, and restore-to-authored-transform support
- a one-click material preset gallery (wood, metal, chrome, glass, plastic, fabric, ceramic, concrete, etc.)
- undo/redo for both the Object tab's bindings and the Scene tab's config, independently, with
Cmd/Ctrl+Z/Cmd/Ctrl+Shift+Zshortcuts - a Scene tab for the same
sceneConfigshapeModelVieweraccepts (lighting, environment, background, post-processing, animations, annotations, and cinematic camera paths) - a Cinematic sub-tab for authoring a camera path visually, frame the model, capture the current view as a waypoint, reorder/preview/delete waypoints, and toggle loop/auto-play; stored on
sceneConfig.cinematicand replayed byModelViewer'scinematicprop - import a previously exported
objectBindings.jsonorsceneConfig.jsonand merge it back onto the current model/config - live preview using
ModelViewer - export as JSON or TypeScript, bundled into a
.zipwith amaterials/folder automatically when any texture field was filled by file upload, otherwise a plain file - license-tier gating for some editor features (texture maps, environment lighting/backgrounds, wireframe preview, animation configuration, annotations, post-processing)
SimpleModelViewer provides:
- a lightweight GLB/GLTF/OBJ/FBX/USDZ viewer that does not require
objectBindingsand does not require alicenseKey - optional local
.glb, standalone/data-URI.gltf,.obj,.fbx,.usdz, or model ZIP bundle upload mode for ad-hoc inspection without relying on themodelUrlasset - built-in scene objects panel with search and visibility toggles
- a built-in environment/scene settings panel (background color, environment preset, auto-rotate, exposure, ambient + directional light) toggleable via
showSceneSettingsPanel, with each control's initial value seedable via props - click-to-select and fit-to-object focus behavior
- optional background color and model lifecycle callbacks
- a smaller API surface for simple inspection use cases
Why objectBindings Exists
objectBindings solves the gap between a raw GLB/GLTF file and an interactive
product experience. A model file gives you geometry, materials, and mesh node
names. It does not know that "CarBody" is configurable, that "FrontDoor"
should open a panel, or that "Wall_A" should expose metadata and a saved
camera view.
objectBindings is the bridge between those raw mesh names and your app's
domain model. It is a serializable record, keyed by mesh node name, that lets
you:
- declare which meshes are interactive
- carry live render state such as visibility, local transforms, and per-object material overrides
- attach actions like
change-color,change-material, andtoggle-visibility - store metadata and metrics alongside the 3D scene
- save camera framing so object focus can use curated views
Because it is plain data, bindings can be authored by hand, generated through
BindingBuilder, stored in a database, versioned as JSON, and round-tripped
through onObjectBindingsChange so your app state and the 3D scene stay in
sync.
Why It Is Worth The Setup
At first glance, objectBindings can look like extra setup. That is true only
if your goal is to render a model and do nothing else with it.
If the model needs to behave like part of your application, bindings stop being overhead and start being the abstraction that keeps the project maintainable. They give you a single source of truth instead of scattering logic across mesh refs, material mutations, raycasting handlers, and custom UI glue.
The short version:
- without bindings, the model is a visual asset
- with bindings, the model becomes application state
If you only need a viewer, use SimpleModelViewer. If you need selection,
customization, persistence, metadata, object-level actions, or app-driven
behavior, bindings are usually the right tradeoff.
Example Applications Where Bindings Shine
- product configurators for cars, furniture, apparel, appliances, or custom goods
- e-commerce customization flows where meshes map to real purchasable options
- CPQ and sales tools where 3D parts correspond to business rules and pricing
- real estate and interior design tools with clickable rooms, walls, fixtures, and finishes
- industrial and engineering viewers for assemblies, equipment, and maintenance workflows
- training and guided walkthroughs with annotations, steps, and saved object viewpoints
- digital twins and operational dashboards where scene parts map to live assets, metrics, and alerts
- smart building controls where lights, switches, sensors, HVAC units, and doors map to real-time app behavior
The pattern is consistent: if users need to say "this exact part of the model has its own meaning, state, or behavior," object bindings are a strong fit.
Installation
npm install @liveroom-tech/react-immersiveImport the library stylesheet once from your application root (for example,
Next.js app/layout.tsx):
import "@liveroom-tech/react-immersive/styles.css";The styles ship as a static CSS file rather than a runtime-injected <style>
tag, so a strict CSP does not need style-src 'unsafe-inline'. Tailwind is not
required in the consuming app.
Peer dependencies:
react >= 17react-dom >= 17
ModelViewer and BindingBuilder require a licenseKey at runtime (see licenseKey). Get one from the Developer Portal, see Resources below.
Resources
- Developer Portal, sign in, pick a plan, and generate a
licenseKey - Docs, full reference for every component, hook, and prop
- Examples & community repo, public docs source, starter examples, and community resources
- Report a bug / request a feature, for public bug reports, docs issues, and feature requests
- Discussions & Q&A, ask questions, share ideas, and compare approaches
- Private support / security, for license, account, confidential customer, or security-sensitive issues
- Live editor, try
BindingBuilderin the browser without installing anything - npm package
Quick Start
import {
ModelViewer,
useObjectBinding,
useObjectBindings,
useViewerActions,
useViewerAnimations,
useViewerCamera,
useViewerHover,
useViewerModel,
useViewerSelection,
} from "@liveroom-tech/react-immersive";
import { defineObjectBindings } from "@liveroom-tech/react-immersive/utils";
const initialBindings = defineObjectBindings({
CarBody: {
id: "car-body",
modelObjectId: "CarBody",
type: "body",
label: "Car Body",
selectable: true,
hoverable: true,
visible: true,
style: {
material: {
texture: {
path: "/materials/oak-wood.jpg",
},
},
},
metrics: {},
metadata: {
category: "exterior",
},
actions: [
{ id: "change-color", label: "Change Color", type: "command" },
{ id: "change-material", label: "Change Material", type: "command" },
{ id: "toggle-visibility", label: "Toggle Visibility", type: "command" },
{
id: "toggle-body",
label: "Toggle Body",
type: "command",
effects: [{ target: "CarBody", visibility: "toggle" }],
},
],
},
});
export default function Example() {
const bindings = useObjectBindings(initialBindings);
const {
objectBindings,
setObjectBindings,
hiddenObjects,
toggleObjectVisibility,
} = bindings;
const { selectedObjectBinding, handleObjectSelect } = useViewerSelection();
const { hoveredObjectBinding, handleHoveredObject } = useViewerHover();
const camera = useViewerCamera();
const {
cameraState,
resetView,
focusObject,
fitScene,
setCameraTarget,
handleCameraChange,
handleViewerReady,
} = camera;
const {
isLoading,
isReady,
error,
bounds,
objectCount,
handleModelLoaded,
handleLoadError,
} = useViewerModel();
const animations = useViewerAnimations();
const { getActionsForObject, handleAction, runAction } = useViewerActions({
bindings,
camera,
animations,
onAction: (event) => {},
});
const focusedBinding = useObjectBinding(objectBindings, "car-body");
return (
<div style={{ height: "100vh", overflow: "hidden" }}>
<ModelViewer
modelUrl="/model.glb"
licenseKey="your-license-key"
objectBindings={objectBindings}
selectedObject={selectedObjectBinding}
onObjectSelect={handleObjectSelect}
onObjectHover={handleHoveredObject}
onCameraChange={handleCameraChange}
onModelLoaded={handleModelLoaded}
onLoadError={handleLoadError}
onObjectBindingsChange={setObjectBindings}
onViewerReady={handleViewerReady}
onAnimationsReady={animations.handleAnimationsReady}
onAction={handleAction}
/>
<button onClick={() => toggleObjectVisibility("car-body")}>
Toggle Visibility
</button>
<button onClick={() => runAction("car-body", "toggle-body")}>
Run declarative action
</button>
<button onClick={() => resetView()}>Reset view</button>
<button onClick={() => focusObject("car-body")}>Focus Object</button>
<button onClick={() => fitScene()}>Fit scene</button>
<button onClick={() => setCameraTarget([0, 0, 0])}>Target origin</button>
<pre>{JSON.stringify(getActionsForObject("car-body"), null, 2)}</pre>
<pre>{JSON.stringify(cameraState, null, 2)}</pre>
<pre>
{JSON.stringify(
{ isLoading, isReady, error, bounds, objectCount },
null,
2,
)}
</pre>
<pre>{JSON.stringify(hiddenObjects, null, 2)}</pre>
<pre>{JSON.stringify(hoveredObjectBinding, null, 2)}</pre>
<pre>{JSON.stringify(focusedBinding, null, 2)}</pre>
</div>
);
}If you want a simple helper for selection state, the library also exports:
const {
selectedObjectBinding,
setSelectedObjectBinding,
handleObjectSelect,
clearSelection,
} = useViewerSelection();If you want to resolve a binding from an object identifier, the library also exports:
const binding = useObjectBinding(objectBindings, "car-body");useObjectBinding checks, in order:
- the object binding map key
binding.idbinding.modelObjectId
If you want a flat list of every modelObjectId across your bindings, the library also exports:
const ids = useObjectBindingIds(objectBindings);
// → ["CarBody", "WheelFL", "WheelFR", ...]If you want one state owner for binding patches, materials, and visibility, use useObjectBindings:
const {
objectBindings,
setObjectBindings,
updateObjectBindings,
selectObjectBindings,
selectObjectBindingIds,
setMaterial,
setBaseColor,
setTexture,
clearTexture,
copyMaterial,
resetMaterial,
getMaterial,
setTransform,
resetTransform,
updateMetadata,
hiddenObjects,
hiddenObjectIds,
hideObject,
showObject,
toggleObjectVisibility,
isObjectHidden,
clearHiddenObjects,
} = useObjectBindings(initialBindings);
<ModelViewer
modelUrl="/model.glb"
licenseKey="your-license-key"
objectBindings={objectBindings}
onObjectBindingsChange={setObjectBindings}
/>;useObjectBindings owns the state used by the viewer. Every setter uses a functional update, so consecutive material, visibility, and general binding updates in the same tick compose instead of overwriting each other.
Object identifiers accepted by its methods can be:
- the object binding map key
binding.idbinding.modelObjectId
You can also target bindings by a shared group, tag, or object type. Groups
represent one named set, while tags let a binding participate in several sets:
updateObjectBindings({ group: "paint" }, { selectable: false });
setBaseColor({ tag: "exterior" }, "#2563eb");
hideObject({ type: "wheel" });
const exteriorBindings = selectObjectBindings({ tag: "exterior" });
const paintIds = selectObjectBindingIds({ group: "paint" });
setTexture({ group: "screens" }, "/images/dashboard.png");
clearTexture({ tag: "printable" }, "baseColor");
copyMaterial("wheel-front", { group: "wheels" });
setTransform("door-left", {
rotation: [0, Math.PI / 2, 0],
});
resetTransform("door-left");
updateMetadata({ group: "lights" }, { state: "on" });When a parent or store owns the state, use the pure selectBindings and
selectBindingKeys helpers for the same queries, and patchBindings to update
all matches. Import these helpers from
@liveroom-tech/react-immersive/utils when the module must remain usable from a
React Server Component; the main entry continues to re-export them for
backwards compatibility.
The hook returns:
objectBindingsandsetObjectBindingsupdateObjectBindingsfor any binding field or atomic edit batchselectObjectBindingsandselectObjectBindingIdsfor group, tag, or type queriessetMaterial,setBaseColor,setTexture,clearTexture,copyMaterial,resetMaterial, andgetMaterialsetTransformandresetTransformfor persistent local-space model transformsupdateMetadatafor recursively merged application datahiddenObjects: a derived map of hidden binding keyshiddenObjectIds: the currently hidden binding IDshideObject,showObject,toggleObjectVisibilityisObjectHiddenclearHiddenObjects
For scene-wide state, use useSceneConfig:
const {
sceneConfig,
setSceneConfig,
updateSceneConfig,
resetSceneConfig,
lights,
} = useSceneConfig(initialSceneConfig);
const enableUvChecker = () =>
updateSceneConfig({
model: { renderer: "uv-checker" },
lighting: { ambient: { intensity: 0.5 } },
});
lights.setIntensity("bulb-light", 4);
lights.setColor("bulb-light", "#ffd27d");
lights.toggle("bulb-light");
lights.attach("bulb-light", {
objectId: "bulb-filament",
offset: [0, -0.2, 0],
});
<ModelViewer
modelUrl="/model.glb"
licenseKey="your-license-key"
objectBindings={objectBindings}
sceneConfig={sceneConfig}
/>;
<button onClick={enableUvChecker}>Show UV checker</button>;Nested objects merge recursively, while arrays and tuples are replaced. Every
update uses a functional state updater, so consecutive calls compose. Use the
pure patchSceneConfig(sceneConfig, patch) utility when a store or parent owns
the scene state. The namespaced lights controller also provides add,
update, remove, show, hide, toggle, setIntensity, setColor,
attach, and detach. Attached lights follow a bound model object without an
R3F light component. The custom lights prop replaces scene-config lights, so
leave it unset when using this controller.
For high-frequency visual effects, use useViewerEffects. It mutates the live
viewer efficiently, requests demand-rendered frames automatically, and restores
its runtime changes on cleanup. Application code does not need Three.js node or
material access:
const {
addPointLight,
flicker,
handleViewerReady,
overrideEffect,
stopEffect,
} = useViewerEffects();
useEffect(() => {
flicker(
{
materials: [
{
targets: { group: "filaments" },
values: { emissiveIntensity: [0.02, 8] },
},
],
lights: [
{
lights: ["light-left", "light-right"],
intensity: [0, 180],
},
],
},
{ id: "bulbs", synchronized: true, blackoutChance: 0.1 },
);
return () => stopEffect("bulbs");
}, [flicker, stopEffect]);
<ModelViewer
modelUrl="/bulbs.glb"
licenseKey="your-license-key"
objectBindings={objectBindings}
onViewerReady={(viewer) => {
handleViewerReady(viewer);
addPointLight({
id: "light-left",
target: "filament-left",
color: "#ff9a3c",
intensity: 0,
distance: 30,
});
}}
/>;
<button onClick={() => overrideEffect("bulbs", 0, 800)}>Blackout</button>;transition, timeline, pulse, and flicker share material, transform, and
light channels. setMaterial, setTransform, and getWorldPosition cover
immediate runtime work. addPointLight, updatePointLight, and
removePointLight manage lights attached to model objects. resetRuntime stops
active effects, restores snapshotted values, and disposes runtime lights.
If you want to keep hover state in sync with the viewer, the library also exports:
const { hoveredObjectBinding, handleHoveredObject } = useViewerHover();
<ModelViewer
modelUrl="/model.glb"
licenseKey="your-license-key"
objectBindings={objectBindings}
onObjectHover={handleHoveredObject}
/>;useViewerHover returns:
hoveredObjectBinding: the currently hovered binding ornullhandleHoveredObject: a callback you can pass directly toonObjectHover
If you want model lifecycle and scene metadata, the library also exports:
const {
isLoading,
isReady,
error,
bounds,
objectCount,
handleModelLoaded,
handleLoadError,
} = useViewerModel();
<ModelViewer
modelUrl="/model.glb"
licenseKey="your-license-key"
objectBindings={objectBindings}
onModelLoaded={handleModelLoaded}
onLoadError={handleLoadError}
/>;useViewerModel returns:
isLoading:trueuntil the model has loaded or failedisReady:trueafter a successful model loaderror: the most recent load/render error, ornullbounds: the loaded scene bounds asmin,max,center, andsizeobjectCount: the number of mesh objects in the loaded scenehandleModelLoaded: a callback you can pass directly toonModelLoadedhandleLoadError: a callback you can pass directly toonLoadError
If you want to inspect or trigger binding actions programmatically, the library also exports:
const { getActionsForObject, handleAction, runAction } = useViewerActions({
bindings,
scene,
camera,
animations,
onAction: (event) => {},
});
const actions = getActionsForObject("car-body");
await runAction("car-body", "power-on");
<ModelViewer onAction={handleAction} />;useViewerActions returns:
getActionsForObject: resolves the actions configured for a binding by key,binding.id, orbinding.modelObjectIdhandleAction: executes an action event and can be passed directly toModelViewer.onActionrunAction: finds and executes an action, then resolves to its event, ornullif the binding or action cannot be found
Actions can declare ordered effects for object-binding patches, scene patches, visibility, camera controls, and animation controls. Pass the corresponding hook controllers into useViewerActions; omitted optional controllers cause only their effect category to be skipped.
runAction executes declarative effects only. It does not invoke viewer-owned behavior such as opening a picker or running the built-in visibility toggle. For programmatic visibility, call the bindings controller directly or declare a custom action with a visibility effect.
BindingBuilder
BindingBuilder is an exported design-time helper component for authoring objectBindings in the browser.
Basic usage:
import { BindingBuilder } from "@liveroom-tech/react-immersive";
export default function App() {
return <BindingBuilder licenseKey="your-license-key" />;
}BindingBuilder accepts one required prop and the same optional decoder
configuration as the viewers:
type BindingBuilderProps = {
licenseKey: string;
licenseValidationUrl?: string;
dracoDecoderPath?: string | false;
ktx2TranscoderPath?: string | false;
meshopt?: boolean;
project?: BindingBuilderProject | null;
onProjectChange?: (snapshot: BindingBuilderProjectSnapshot, origin: "local" | "remote") => void;
projectChangeDebounceMs?: number;
onModelUpload?: (file: File) => Promise<BindingBuilderProjectModel>;
onEditLeaseChange?: (featureId: string, editing: boolean) => void;
};Pass tilesetUrl to ModelViewer for a processed 3D Tiles asset. The original
modelUrl remains the durable source/fallback URL, but is not downloaded by the
viewer while a tileset is present:
<ModelViewer
modelUrl="https://assets.example.com/source/building.glb"
tilesetUrl="https://assets.example.com/tiles/building/tileset.json"
tilesErrorTarget={8}
tilesCacheSize={800}
licenseKey="your-license-key"
objectBindings={{}}
/>Tiles are a dynamic level-of-detail scene, so automatic mesh binding generation
is intentionally disabled in streamed BindingBuilder projects. Scene settings
remain editable. Per-part editing requires the conversion pipeline to provide a
stable CAD/object catalog that can be mapped to tile feature metadata.
licenseKey is validated the same way as ModelViewer's (see licenseKey below). While the key is being checked, BindingBuilder renders a "Verifying license…" placeholder; if it's invalid, it renders an error message instead of the editor.
Current behavior, Object tab:
- upload a
.glb, standalone/data-URI.gltf, or.zipcontaining a.gltfplus its referenced.binand texture files - traverse renderable mesh nodes and generate starter bindings automatically
- edit binding identity, label, type, status, booleans, style, actions, metrics, metadata, and saved camera state
- move and rotate the selected object from the Move & Rotate panel using a geometry-centered pivot, World/Local axis alignment, or numeric position and degree fields; restore its authored transform with one click
- apply a one-click material preset (wood, metal, chrome, glass, plastic, fabric, ceramic, concrete, etc.) onto the selected object's material
- undo/redo the current bindings (toolbar buttons or
Cmd/Ctrl+Z/Cmd/Ctrl+Shift+Z); rapid edits coalesce into a single undo step - import a previously exported
objectBindings.json, merged onto the current model bymodelObjectId - export the current bindings as JSON or TypeScript, with TypeScript exports wrapped in
defineObjectBindingsto preserve literal IDs, bundled into a.zip(with amaterials/folder) when any texture field was filled by uploading a file, otherwise a plain file
Current behavior, Scene tab:
- configure the same
SceneConfigshapeModelViewer'ssceneConfigprop accepts (lighting, environment, background, ground shadows, wireframe, post-processing, animations, annotations) - undo/redo the scene config independently of the Object tab's bindings history
- import a previously exported
sceneConfig.json, merged by top-level section - export the current scene config as JSON or TypeScript
Preview:
- the selected node previews live in an embedded
ModelViewer - enable Transform gizmo, select a mesh, then use the arrows and plane handles to move it or the rings to rotate it; all changes are saved to
binding.transform
BindingBuilder licensing & plan tiers
Some editor features are gated by the license's plan tier (the server-returned tier, normalized to "free" | "starter" | "growth"):
| Tier | Unlocks |
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| free | All 3 components are available. SimpleModelViewer is fully available, while ModelViewer and BindingBuilder are limited to local development with base color, metalness/roughness sliders, and basic scene settings only |
| starter | + Texture maps, the specular workflow, reflectivity, environment lighting, environment backgrounds, wireframe preview, animation configuration, annotations |
| growth | + Post-processing effects |
A locked feature renders in place (not hidden) with a message naming the required plan, so it stays discoverable while editing.
SimpleModelViewer
SimpleModelViewer is an exported lightweight viewer for cases where you want to load a GLB, GLTF, OBJ, FBX, or USDZ asset, inspect meshes, and toggle visibility without building objectBindings. It can either load a fixed modelUrl or, when enableModelUpload is turned on, let the user drag-and-drop or choose a local model at runtime.
Basic usage:
import { SimpleModelViewer } from "@liveroom-tech/react-immersive";
export default function App() {
return (
<div style={{ height: "100vh" }}>
<SimpleModelViewer modelUrl="/model.glb" />
</div>
);
}SimpleModelViewer does not take a licenseKey, licensing is enforced by ModelViewer and BindingBuilder only.
Sizing note:
SimpleModelViewerfills the width and height of its parent container- give the parent an explicit height (for example
100vh,480px, or a Tailwind class likeh-[100px]) when you want a fixed viewer height
Current behavior:
- discovers renderable meshes directly from the loaded model scene
- renders a right-side scene objects panel with search and visibility toggles
- renders a left-side environment/scene settings panel (background color, environment preset, auto-rotate, exposure, ambient + directional light) toggleable via
showSceneSettingsPanel, with every control seedable via props - optionally replaces
modelUrlwith a drag-and-drop / file-picker flow whenenableModelUploadis enabled - lets users click a mesh or panel row to select and focus it
- fits the full scene on initial load
- uses built-in ambient, directional, and environment lighting
Use SimpleModelViewer when you want a quick inspection viewer.
Use ModelViewer when you need binding-driven styling, actions, metadata, custom panels, exports, or animation integrations.
If you want camera state and imperative camera helpers, the library also exports:
const camera = useViewerCamera({
initialPosition: [0, 2, 8],
initialTarget: [0, 1.2, 0],
});
const {
cameraState,
resetView,
focusObject,
fitScene,
setCameraTarget,
lookAt,
orbitTo,
dollyTo,
savePreset,
goToPreset,
handleCameraChange,
handleViewerReady,
} = camera;
<ModelViewer
modelUrl="/model.glb"
licenseKey="your-license-key"
objectBindings={objectBindings}
onCameraChange={handleCameraChange}
onViewerReady={handleViewerReady}
/>;useViewerCamera returns:
cameraState: the latest cameraposition,target,fov, andzoomresetView: resets the controls to their initial saved statefocusObject: fits the camera to a binding by key,binding.id, orbinding.modelObjectIdfitScene: fits the camera to the whole loaded scenesetCameraTarget: sets the camera target to a given[x, y, z]setCameraState: restores a full camera state (position/target/fov/zoom) previously read fromcameraStatelookAt: moves the position and target togetherorbitTo: moves to azimuth and polar angles in radiansdollyTo: moves to a distance from the current targetsavePreset: captures the current camera under a namegoToPreset: moves to a named camera presethandleCameraChange: a callback you can pass directly toonCameraChangehandleViewerReady: a callback you can pass directly toonViewerReady
Pass initialPosition, initialTarget, and optional named presets to useViewerCamera to configure the first view without writing an onViewerReady wrapper.
If your GLB/GLTF model contains animations, the library also exports:
const {
clips,
currentClip,
isPlaying,
speed,
play,
pause,
stop,
setSpeed,
handleAnimationsReady,
} = useViewerAnimations();
<ModelViewer
modelUrl="/character.glb"
licenseKey="your-license-key"
objectBindings={objectBindings}
onAnimationsReady={handleAnimationsReady}
/>;useViewerAnimations returns:
clips: an array of animation clip names found in the GLB/GLTF fileclipDetails: clip metadata includingsourceNameanddurationin secondscurrentClip: the name of the currently playing clip, ornullisPlaying:truewhile an animation is playing (not paused or stopped)speed: the current playback speed (default1)time: the live playback position of the current clip, in seconds (updates while playing)duration: the length of the current clip, in secondsplay: starts a clip by name; if the same clip is paused, resumes it instead of restartingpause: pauses the currently playing clip at its current timestop: stops the current clip and resets itsetSpeed: changes the playback speed (e.g.0.5for half speed,2for double)seek: scrubs the current clip to an absolute time in seconds (works while playing or paused), clamped to the clip lengthhandleAnimationsReady: a callback you can pass directly toonAnimationsReady
Example with playback controls:
const { clips, currentClip, isPlaying, play, pause, stop, setSpeed } =
useViewerAnimations();
// Play/pause toggle
<button onClick={() => (isPlaying ? pause() : play(currentClip ?? clips[0]))}>
{isPlaying ? "Pause" : "Play"}
</button>
// Stop
<button onClick={stop}>Stop</button>
// Speed slider
<input
type="range"
min="0.1"
max="3"
step="0.1"
defaultValue="1"
onChange={(e) => setSpeed(Number(e.target.value))}
/>
// Clip selector
{clips.map((name) => (
<button key={name} onClick={() => play(name)}>
{name} {currentClip === name && isPlaying ? "▶" : ""}
</button>
))}Public API
The source code currently defines ModelViewer with these props:
type ObjectTransformSpace = "local" | "world";
type Props = {
modelUrl: string;
modelFormat?: "glb" | "gltf" | "obj" | "fbx" | "usdz";
licenseKey: string;
objectBindings: Record<string, ObjectBinding>;
selectedObject?: ObjectBinding | null;
onObjectBindingsChange?: (next: Record<string, ObjectBinding>) => void;
onObjectSelect?: (binding: ObjectBinding | null) => void;
onObjectHover?: (binding: ObjectBinding | null) => void;
onModelLoaded?: (scene: Object3D) => void;
onLoadError?: (error: unknown) => void;
onAction?: (event: ObjectActionEvent) => void;
onHiddenObjectsChange?: (next: Record<string, boolean>) => void;
onCameraChange?: (camera: Camera, controls: CameraControls) => void;
onViewerReady?: (viewer: ViewerReadyState) => void;
onTextureUpload?: (file: File, objectId: string) => Promise<string>;
onAnimationsReady?: (controls: AnimationControls) => void;
onAnnotationsChange?: (annotations: AnnotationMarker[]) => void;
activeAnnotation?: AnnotationMarker | null;
onActiveAnnotationChange?: (annotation: AnnotationMarker | null) => void;
lights?: React.ReactNode;
camera?: React.ComponentProps<typeof Canvas>["camera"];
backgroundColor?: string;
shadows?: boolean;
showObjectBindingDataPanel?: boolean;
customObjectBindingDataPanel?: (
props: CustomObjectBindingDataPanelProps,
) => React.ReactNode;
customSceneObjectsPanel?: (
props: CustomSceneObjectsPanelProps,
) => React.ReactNode;
showSceneObjectsPanel?: boolean;
showDownloadButton?: boolean;
downloadFilename?: string;
showResetButton?: boolean;
showLoadingOverlay?: boolean;
showMouseController?: boolean;
mouseControllerPosition?:
| "bottom-left"
| "bottom-right"
| "top-left"
| "top-right"
| "center"
| "center-bottom"
| "center-top";
mouseControllerOpacity?: number;
moveSensitivity?: number;
zoomSensitivity?: number;
sceneConfig?: SceneConfig;
disableZoom?: boolean;
zoomOnSelected?: boolean;
enableCameraControls?: boolean;
moveModeEnabled?: boolean;
objectTransformSpace?: ObjectTransformSpace;
enableKeyboardNavigation?: boolean;
onAutoFit?: () => Promise<boolean>;
refitOnResize?: boolean;
renderMode?: "always" | "demand";
maxDpr?: number;
preserveDrawingBuffer?: boolean;
performanceProfile?: "auto" | "high" | "low";
dracoDecoderPath?: string | false;
ktx2TranscoderPath?: string | false;
meshopt?: boolean;
showMeasureTools?: boolean;
showUvCheckerButton?: boolean;
showExplodeControls?: boolean;
cinematic?: boolean | CinematicConfig;
measurementUnit?: string;
enableXR?: boolean;
usdzUrl?: string;
arScaleMode?: "normalized" | "real-world";
mobileHandoffUrl?: string;
showAnnotationNavigation?: boolean;
showAnnotationOnHover?: boolean;
showViewGizmo?: boolean;
};modelUrl
URL or public path to a .glb, .gltf, .obj, .fbx, or .usdz model. Hosted GLTF, OBJ, and FBX assets must serve external buffers, MTL files, and textures at their declared relative paths. USDZ dependencies are contained in the USDZ package. Use modelFormat when a signed or extensionless URL cannot be auto-detected.
Example:
modelUrl = "/model.glb";modelFormat
Optional explicit format for signed or extensionless model URLs. Normal URLs
ending in .glb, .gltf, .obj, .fbx, or .usdz are detected automatically.
licenseKey
License key for the library. Required.
licenseValidationUrl
Optional. Points license validation at a different endpoint, for first-party or
self-hosted consoles that proxy validation through their own origin instead of
calling react-immersive.liveroom.dev cross-origin.
This is not a way to self-approve a license. When this prop is set, a successful
response is only accepted if it carries an offlineGrant signed by the React
Immersive license service, verified against the ECDSA public key pinned in this
package — and the resulting environment and tier are read from that signed
grant, not from the surrounding JSON body. An endpoint that returns
{"valid": true} without a valid grant is treated as a broken license server:
the SDK falls back to a previously verified grant if one is cached, and
otherwise fails closed.
In practice the endpoint must be a deployment of the official validation route
configured with LICENSE_GRANT_PRIVATE_KEY.
objectBindings
A map keyed by the model node name. Each key should match a mesh name from the GLB/GLTF file.
objectBindings is the single source of truth for all visual state. Changes to the binding record drive rendering:
visiblecontrols mesh visibilitystyle.material.baseColorsets the mesh color (committed by the built-in color picker on close)style.material.texture.pathsets the mesh texture (committed by the built-in texture upload; usesonTextureUploadfor a durable URL when provided, otherwise a session-scoped blob URL)style.material.*applies per-objectMeshPhysicalMaterialoverrides such as emissive, metalness, roughness, opacity, clearcoat, sheen, anisotropy, specular, transmission, thickness, reflectivity, and sidednesscameraState.positionandcameraState.targetdefine the camera view to use when that object is focused or selected from the viewer UI
Example:
import { defineObjectBindings } from "@liveroom-tech/react-immersive/utils";
const objectBindings = defineObjectBindings({
Object_2: {
id: "obj-2",
modelObjectId: "Object_2",
type: "body",
visible: true,
cameraState: {
position: [2.8, 1.6, 4.2],
target: [0, 0.8, 0],
},
style: {
material: {
baseColor: "#ff0000",
texture: {
path: "/materials/body-finish.jpg",
},
},
},
actions: [
{ id: "change-color", label: "Change Color", type: "command" },
{ id: "change-material", label: "Change Material", type: "command" },
{ id: "toggle-visibility", label: "Toggle Visibility", type: "command" },
],
metrics: {},
metadata: {},
},
});defineObjectBindings validates the complete record while preserving its exact keys and literal values. Use ObjectBindingKey<typeof objectBindings> for the binding-key union and ObjectBindingActionId<typeof objectBindings, "Object_2"> for that binding's action-id union. It returns the input unchanged at runtime.
selectedObject
Optional currently selected object binding, or null when nothing is selected.
If provided, ModelViewer behaves as a controlled component.
If omitted, ModelViewer manages its own selection state internally.
When the selected object has a cameraState with both position and target, the viewer uses that saved camera view for focus/select behavior instead of falling back to fitToBox.
onObjectSelect
Optional callback called when the user clicks a mesh or closes the side panel.
Behavior:
- clicking a mesh calls
onObjectSelect(binding) - closing the panel calls
onObjectSelect(null) - clicking the same mesh again still selects it; selection is not toggled off automatically
- if the binding includes
cameraState.positionandcameraState.target, the viewer moves the camera to that saved view when focusing that object
onObjectHover
Optional callback called when the user hovers a mesh in the model.
Behavior:
- pointer over a mesh calls
onObjectHover(binding) - pointer out calls
onObjectHover(null)
onHiddenObjectsChange
Optional callback fired with the derived hidden-object map whenever the current binding data changes hidden visibility state.
onObjectBindingsChange
Optional callback fired when the viewer's built-in UI updates binding data.
This fires for:
- visibility toggles (
binding.visible) - color picks (
binding.style.material.baseColor), committed when the color picker closes - texture uploads (
binding.style.material.texture.path), committed immediately (viaonTextureUploadwhen provided, otherwise as a blob URL) - texture removal (
binding.style.material.texturecleared) - object movement and rotation (
binding.transform.positionandbinding.transform.rotation)
If you want the viewer and your app state to stay in sync, pass objectBindings from state and wire this callback back into that state setter.
onModelLoaded
Optional callback fired after the GLB/GLTF scene has been loaded.
Current callback shape:
(scene: Object3D) => voidonLoadError
Optional callback fired if the model fails to load or render.
Current callback shape:
(error: unknown) => voidonAction
Optional callback fired when a built-in action button is clicked from the side panel.
Current event shape:
type ObjectActionEvent = {
objectId: string;
action: ObjectBindingAction;
binding?: ObjectBinding;
screenX?: number;
screenY?: number;
};onViewerReady
Optional callback fired once camera controls are available and again when the viewer publishes updated ready-state data (for example after model load, camera changes, or binding changes).
Current callback shape:
(viewer: ViewerReadyState) => voidViewerReadyState includes captureImage, an imperative method for grabbing a PNG snapshot of the canvas:
type ViewerReadyState = {
controls: CameraControls;
scene: Object3D | null;
objectBindings: Record<string, ObjectBinding>;
nodeRefs: Record<string, Object3D>;
captureImage: (options?: CaptureImageOptions) => Promise<string>;
};
type CaptureImageOptions = {
width?: number;
height?: number;
transparent?: boolean;
};captureImage requires the opt-in preserveDrawingBuffer prop, forces a render, and resolves with a data:image/png URL. Pass width/height to render at a resolution other than the canvas's current size, and transparent to hide the configured background so the PNG carries an alpha channel instead. It rejects if called before the viewer is ready or when framebuffer preservation is disabled. The Download PNG menu item is also shown only when this prop is enabled.
<ModelViewer
preserveDrawingBuffer
onViewerReady={(viewer) => {
viewer
.captureImage({ width: 1920, height: 1080, transparent: true })
.then((dataUrl) => {
// upload, preview, etc.
});
}}
/>;Note: requesting a custom width/height briefly resizes the live canvas to render at that resolution before restoring it, which can cause a momentary flicker on screen, fine for an occasional snapshot, not for rapid/looped calls.
If camera, effects, screenshots, or custom setup all need the ready viewer, combine them with useViewerConnection:
const camera = useViewerCamera();
const effects = useViewerEffects();
const connection = useViewerConnection(camera, effects);
<ModelViewer
preserveDrawingBuffer
onViewerReady={connection.handleViewerReady}
/>;
const dataUrl = await connection.captureImage({ width: 1920, height: 1080 });onTextureUpload
Optional async callback for handling texture file uploads. When provided, the viewer calls it instead of creating a blob URL, letting consumers upload to their own storage (S3, Cloudinary, a CDN, etc.) and return a durable URL that survives page reloads.
Current callback shape:
(file: File, objectId: string) => Promise<string>;The returned string is stored in binding.style.material.texture.path via onObjectBindingsChange.
If omitted, the viewer falls back to URL.createObjectURL(file) which produces a short blob URL that works for the current session but is lost on reload.
Example:
<ModelViewer
modelUrl="/model.glb"
licenseKey="your-license-key"
objectBindings={objectBindings}
onObjectBindingsChange={setObjectBindings}
onTextureUpload={async (file, objectId) => {
const formData = new FormData();
formData.append("file", file);
formData.append("objectId", objectId);
const res = await fetch("/api/upload-texture", {
method: "POST",
body: formData,
});
const { url } = await res.json();
return url; // e.g. "https://cdn.example.com/textures/abc123.png"
}}
/>onAnimationsReady
Optional callback fired after the GLB/GLTF model is loaded, providing animation controls. If the model contains no animations, it is still called with an empty clips array and no-op control functions.
Current callback shape:
(controls: AnimationControls) => voidWhere AnimationControls is:
type AnimationPlaybackState = {
currentClip: string | null;
isPlaying: boolean;
speed: number;
time: number; // live position of the current clip, in seconds
duration: number; // length of the current clip, in seconds
};
type AnimationControls = {
clips: string[];
clipDetails?: { sourceName: string; duration: number }[];
play: (clipName: string) => void;
pause: () => void;
stop: () => void;
setSpeed: (speed: number) => void;
seek?: (time: number) => void; // scrub the current clip to an absolute time (seconds)
getState?: () => AnimationPlaybackState;
subscribe?: (listener: (state: AnimationPlaybackState) => void) => () => void;
};Wire this to useViewerAnimations().handleAnimationsReady for the simplest integration.
When sceneConfig.animations.autoplayClip is set, ModelViewer will auto-play
that clip on load and respect per-clip loopMode, speed, displayName, and
soft-delete (hidden) settings.
lights
Optional custom lighting to render inside the scene.
If omitted, ModelViewer uses the library's default light rig.
Example:
function CustomLights() {
return (
<>
<ambientLight intensity={0.5} />
<directionalLight position={[4, 8, 4]} intensity={1.6} castShadow />
<pointLight position={[-3, 3, 2]} intensity={0.8} />
</>
);
}
<ModelViewer
modelUrl="/model.glb"
licenseKey="your-license-key"
objectBindings={objectBindings}
lights={<CustomLights />}
/>;camera
Optional custom camera configuration passed through to the underlying React Three Fiber Canvas.
ModelViewer only sets a default fov: 50, it does not set a default position, so an unset position falls back to React Three Fiber's own Canvas default ([0, 0, 5]). Pass an explicit position for predictable framing.
Example:
<ModelViewer
modelUrl="/model.glb"
licenseKey="your-license-key"
objectBindings={objectBindings}
camera={{
position: [0, 2.2, 7],
fov: 40,
near: 0.1,
far: 1000,
}}
/>Passing an explicit camera counts as choosing the initial view yourself, so the mount-time auto-fit (see onAutoFit) is skipped automatically and your position/fov stick. Provide onAutoFit as well only if you want to run your own fit-to-scene logic instead.
backgroundColor
Optional background color for the viewer canvas.
Example:
<ModelViewer
modelUrl="/model.glb"
licenseKey="your-license-key"
objectBindings={objectBindings}
backgroundColor="#0f172a"
/>shadows
Optional boolean that controls whether the viewer renders shadows.
Default:
true;When true, the canvas renders soft shadows: the default light rig casts a shadow from a directional key light, furniture and decor meshes cast and receive shadows, and ambient occlusion is applied through post-processing. When false, shadow map rendering is disabled entirely on the canvas, so there is no shadow-pass cost (the per-mesh and light castShadow/receiveShadow flags are simply ignored).
Example:
<ModelViewer
modelUrl="/model.glb"
licenseKey="your-license-key"
objectBindings={objectBindings}
shadows={false}
/>Interior models (rooms, dollhouses): a single large mesh usually forms the walls/ceiling enclosure. If that shell cast shadows it would seal the whole interior in darkness when lit from outside, so the viewer automatically detects the enclosure (a mesh spanning most of the scene footprint in both horizontal axes) and excludes it from casting while still letting it receive shadows. Furniture and decor then cast realistic contact shadows onto the floor. See How Rendering Works.
onCameraChange
Optional callback fired whenever the camera controls update the camera.
Current callback shape:
(camera: Camera, controls: CameraControls) => voidshowObjectBindingDataPanel
Optional boolean that controls whether the built-in left object details panel is rendered.
Default:
true;customObjectBindingDataPanel
Optional render prop for replacing the built-in left object details panel.
Current prop shape:
type CustomObjectBindingDataPanelProps = {
isOpen: boolean;
selectedObject: ObjectBinding | null;
currentAction: ObjectActionEvent | null;
onClose: () => void;
onAction: (event: ObjectActionEvent) => void;
};When provided, ModelViewer passes its existing internal handlers into your custom panel. Your panel can call them, wrap them, or ignore them.
customSceneObjectsPanel
Optional render prop for replacing the built-in right scene objects panel.
Current prop shape:
type CustomSceneObjectsPanelProps = {
objectBindings: Record<string, ObjectBinding>;
onAction?: (event: ObjectActionEvent) => void;
onFocus?: (binding: ObjectBinding) => void;
onHover?: (binding: ObjectBinding | null) => void;
};When provided, ModelViewer passes the current bindings plus its built-in action, focus, and hover handlers into your custom panel.
showSceneObjectsPanel
Optional boolean that controls whether the built-in right scene objects panel is rendered.
Default:
true;showDownloadButton
Optional boolean that enables the download split-button (export GLB, export the
model's UV layout, and—when preserveDrawingBuffer is enabled—export a PNG
screenshot).
Default:
true;UV export downloads an editable 2048×2048 SVG UV-island outline when the model
uses one material. For multi-material models it downloads a ZIP containing one
SVG texture template per material. Meshes without a uv attribute are omitted;
when the model has no UV coordinates, the viewer reports that no layout is
available.
The bottom-right action bar renders when at least one of showResetButton, showDownloadButton, or (showAnnotationNavigation with annotations present) is true.
downloadFilename
Optional filename stem for the built-in model export button.
Default:
"model";showResetButton
Optional boolean that shows the "reset camera view" button in the bottom-right action bar.
Default:
true;showLoadingOverlay
Optional boolean controlling whether the built-in loading overlay is shown while the model is loading and the initial camera fit is settling.
Default:
true;showMouseController
Optional boolean that renders an on-screen joystick controller for moving the camera with a mouse or touch, useful on touch devices or kiosk layouts where drag-to-orbit is awkward.
Default:
false;mouseControllerPosition
Optional placement for the on-screen controller.
type MouseControllerPosition =
| "bottom-left"
| "bottom-right"
| "top-left"
| "top-right"
| "center"
| "center-bottom"
| "center-top";mouseControllerOpacity
Optional opacity for the on-screen controller.
Default:
1;moveSensitivity
Optional movement sensitivity for the on-screen controller.
Default:
0.08;zoomSensitivity
Optional zoom sensitivity for the underlying CameraControls zoom interaction (mouse wheel / trackpad / pinch).
Default:
1.0;Example:
<ModelViewer
modelUrl="/model.glb"
licenseKey="your-license-key"
objectBindings={objectBindings}
showMouseController
mouseControllerPosition="bottom-right"
mouseControllerOpacity={0.8}
moveSensitivity={0.1}
zoomSensitivity={1.2}
/>sceneConfig
Optional scene-wide configuration object (SceneConfig) covering model, camera, lighting, wireframe, shadows, environment, background, ground shadows, post-processing, animations, and annotations. It is the same shape authored by BindingBuilder's Scene tab. If omitted, the viewer uses its built-in default scene config.
sceneConfig.animations drives autoplay and per-clip playback settings, and sceneConfig.annotations seeds the annotation markers rendered on the model.
sceneConfig.model.renderer accepts "pbr", "matcap", or "uv-checker".
UV Checker temporarily replaces every mesh material with an unlit numbered test
chart without changing objectBindings or uploaded textures. Switching back
restores the original material references. Meshes without UV0 coordinates appear
magenta and trigger an explanatory notice.
<ModelViewer
{...props}
sceneConfig={{
...sceneConfig,
model: { ...sceneConfig.model, renderer: "uv-checker" },
}}
/>disableZoom
Optional boolean. When true, the viewer never zooms the camera to an object on selection (selection still highlights and fires callbacks).
Default:
false;zoomOnSelected
Optional boolean controlling whether selecting an object zooms/fits the camera to it. Set to false to keep the current camera framing on selection. Zooming is also skipped when disableZoom is true.
Default:
true;Moving and rotating objects
Set moveModeEnabled to show a centered Drei PivotControls gizmo for the
selected bound object. Translation arrows, plane handles, and rotation rings
are available together; scaling is not enabled. While object editing is on,
the camera controls are paused so pointer drags manipulate the object instead
of the camera.
import { useState } from "react";
import {
ModelViewer,
type ObjectBinding,
} from "@liveroom-tech/react-immersive";
export function EditableViewer({
initialBindings,
}: {
initialBindings: Record<string, ObjectBinding>;
}) {
const [objectBindings, setObjectBindings] = useState(initialBindings);
return (
<ModelViewer
modelUrl="/model.glb"
licenseKey="your-license-key"
objectBindings={objectBindings}
onObjectBindingsChange={setObjectBindings}
moveModeEnabled
objectTransformSpace="world"
/>
);
}Click a bound mesh to select it, then drag an arrow to move on one axis, a plane handle to move on two axes, or a ring to rotate. The pivot is placed at the center of the selected object's renderable geometry, so rotation happens around the object itself even when the source model's node origin is elsewhere.
objectTransformSpace accepts:
"world"(default): the gizmo remains aligned with the scene axes"local": the gizmo follows the selected object's current orientation
Axis space changes the direction of the handles; it does not change the
object's position when toggled. The resulting transform.position and
transform.rotation values are always persisted in the object's local space,
with rotation stored in radians.
Passing onObjectBindingsChange makes binding edits controlled, so the parent
must store the returned record as shown above. Without that callback,
ModelViewer keeps edits internally for the mounted viewer. Similarly, when
you pass selectedObject, update it from onObjectSelect; otherwise selection
is managed internally.
enableCameraControls
Optional boolean controlling orbit, pan, and zoom input. Default: true.
Camera controls are temporarily disabled whenever moveModeEnabled is on.
enableKeyboardNavigation
Optional boolean enabling camera shortcuts after the viewer canvas is clicked
or focused. Default: false. Use W/S to move forward/back, A/D to
truck left/right, Space/C to move up/down, and the arrow keys to orbit.
Shift + Up/Down moves forward/back. Keyboard navigation is inactive while
object editing or camera controls are disabled.
showViewGizmo
Optional boolean showing a clickable orientation gizmo in the top-right of the
viewer. Default: false. It indicates the camera's view of the scene axes and
can snap the camera to an axis; it is separate from the selected object's
transform gizmo and is hidden while moveModeEnabled is on.
onAutoFit
Optional async callback fired once the model has loaded and the scene is ready. When provided, the viewer calls it so consumers can run their own fit-to-scene behavior (e.g. an animated fit); if omitted, the viewer falls back to its internal fitScene, unless an explicit camera prop was given, in which case that position is treated as the chosen initial view and fitScene is skipped.
Current callback shape:
() => Promise<boolean>;refitOnResize
Optional boolean controlling whether the camera re-frames the model to fit whenever the canvas is resized, a browser window resize, a device rotation, or a side panel opening/closing (which changes how much width the canvas has). Set to false to preserve the user's current orbit/zoom across resizes; the camera aspect stays correct either way, so the model never distorts, it just isn't re-centered. This only affects re-fits after the initial mount-time auto-fit.
Default:
true;onAnnotationsChange
Optional callback fired when the annotation markers on the model change (added, edited, or removed through the viewer UI).
Current callback shape:
(annotations: AnnotationMarker[]) => voidWhere AnnotationMarker is the SceneAnnotationMarker shape:
type AnnotationMarker = {
id: number;
worldPosition: [number, number, number];
localPosition: [number, number, number];
title: string;
description: string;
};activeAnnotation / onActiveAnnotationChange
Optional controlled state for which annotation is currently open. Pass activeAnnotation to control it from your app, and onActiveAnnotationChange to be notified when the viewer wants to open (AnnotationMarker) or close (null) one. If activeAnnotation is left undefined, the viewer manages this state internally (uncontrolled).
Current shapes:
activeAnnotation?: AnnotationMarker | null;
onActiveAnnotationChange?: (annotation: AnnotationMarker | null) => void;renderMode
Optional render loop mode. "demand" (default) only re-renders the canvas when something changes (camera move, state update, animation frame); "always" runs a continuous render loop.
maxDpr
Optional upper bound for the device pixel ratio used when rendering, so retina/4K displays don't render at full 2–3x cost.
Default:
2;preserveDrawingBuffer
Optional boolean that retains the WebGL framebuffer for PNG screenshots. It is
false by default to avoid imposing framebuffer-retention cost on every frame.
Enable it when using captureImage or the built-in Download PNG action.
Default:
false;performanceProfile
Optional control over how aggressively the viewer trades visual fidelity for a stable WebGL context on constrained GPUs.
performanceProfile?: "auto" | "high" | "low";"auto"(default), applies a reduced profile on handheld/mobile browsers and other low-power touch devices: the postprocessing pipeline is skipped, soft shadows are disabled, and the device pixel ratio is capped. Desktop-class touch devices may still keep the higher-quality path when they advertise plenty of memory. This keeps mobile GPUs within their memory budget so the context isn't lost, which also keeps WebXR (enableXR) usable, since a lost context can't start a session."high", always render at full quality (postprocessing, soft shadows, fullmaxDpr), even on mobile. Use when you know the target devices can handle it."low", always apply the reduced profile, on any device.
Default:
"auto";dracoDecoderPath / ktx2TranscoderPath / meshopt
Optional controls for the compressed-asset decoders used when loading the GLB/GLTF asset.
dracoDecoderPath?: string | false;
ktx2TranscoderPath?: string | false;
meshopt?: boolean;Meshopt is bundled and enabled by default. DRACO and KTX2 are also enabled by
default through immutable, versioned decoder builds hosted on the first-party
assets.react-immersive.liveroom.dev domain. They are fetched lazily only when a model
uses the corresponding compression format.
dracoDecoderPath =
"https://assets.react-immersive.l