@liveroom-tech/react-immersive 5.2.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE +10 -6
  2. package/README.md +106 -3856
  3. package/dist/binding-builder.css +1 -1
  4. package/dist/binding-builder.d.mts +5 -1
  5. package/dist/binding-builder.d.ts +5 -1
  6. package/dist/binding-builder.js +67 -17
  7. package/dist/binding-builder.mjs +1 -1
  8. package/dist/chunk-2JFUMJUP.mjs +1 -0
  9. package/dist/chunk-4JMJALRO.mjs +2 -0
  10. package/dist/chunk-OTSCPJWF.mjs +1 -0
  11. package/dist/chunk-P2JZ5Z7I.mjs +1 -0
  12. package/dist/chunk-UGPR4EPP.mjs +81 -0
  13. package/dist/chunk-V7WZUWJ5.mjs +1 -0
  14. package/dist/chunk-WWUWEPFK.mjs +3 -0
  15. package/dist/hosted.css +1 -1
  16. package/dist/hosted.d.mts +48 -19
  17. package/dist/hosted.d.ts +48 -19
  18. package/dist/hosted.js +66 -16
  19. package/dist/hosted.mjs +1 -1
  20. package/dist/index.css +1 -1
  21. package/dist/index.d.mts +78 -12
  22. package/dist/index.d.ts +78 -12
  23. package/dist/index.js +65 -15
  24. package/dist/index.mjs +1 -1
  25. package/dist/{materialVariants-DnZe9eMC.d.mts → modelFileResolver-Co7j1tdQ.d.mts} +125 -27
  26. package/dist/{materialVariants-DnZe9eMC.d.ts → modelFileResolver-Co7j1tdQ.d.ts} +125 -27
  27. package/dist/modelViewerProps-BibuMND7.d.ts +1988 -0
  28. package/dist/modelViewerProps-Ce4-wb57.d.mts +1988 -0
  29. package/dist/{publicProps-tJuWZgT3.d.ts → publicProps-BaJHCDa_.d.mts} +7 -0
  30. package/dist/{publicProps-tJuWZgT3.d.mts → publicProps-CP18d_mt.d.ts} +7 -0
  31. package/dist/simple-model-viewer.d.mts +2 -4
  32. package/dist/simple-model-viewer.d.ts +2 -4
  33. package/dist/simple-model-viewer.js +3 -3
  34. package/dist/simple-model-viewer.mjs +1 -1
  35. package/dist/utils.d.mts +62 -4
  36. package/dist/utils.d.ts +62 -4
  37. package/dist/utils.js +2 -1
  38. package/dist/utils.mjs +1 -1
  39. package/package.json +2 -1
  40. package/dist/chunk-4NLJSD3S.mjs +0 -1
  41. package/dist/chunk-B7REIGDG.mjs +0 -1
  42. package/dist/chunk-HYFJ5DYJ.mjs +0 -1
  43. package/dist/chunk-QC3NT76G.mjs +0 -3
  44. package/dist/chunk-SEESCGUS.mjs +0 -1
  45. package/dist/chunk-T3M3BQ7H.mjs +0 -32
  46. package/dist/chunk-ZPK67UG7.mjs +0 -1
  47. package/dist/modelInfo-BYebMPdZ.d.mts +0 -43
  48. package/dist/modelInfo-XfUQO7C_.d.ts +0 -43
  49. package/dist/modelViewerProps-CIZ2R_Fv.d.ts +0 -569
  50. package/dist/modelViewerProps-DoRiTZMn.d.mts +0 -569
  51. package/dist/rendererBackend-QwMzISTn.d.mts +0 -903
  52. package/dist/rendererBackend-QwMzISTn.d.ts +0 -903
package/README.md CHANGED
@@ -1,3896 +1,146 @@
1
- # React Immersive — 3D model viewer for React
1
+ # React Immersive — 3D model viewers for React
2
2
 
3
- `@liveroom-tech/react-immersive` is a React-based 3D model viewer for interactive GLB, GLTF, OBJ, FBX, USDZ, STL, and PLY 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.
3
+ `@liveroom-tech/react-immersive` renders GLB/GLTF, OBJ, FBX, USDZ, STL, and PLY
4
+ assets in React. Use its components and hooks for mesh selection, materials,
5
+ visibility, camera control, animations, annotations, and scene authoring.
4
6
 
5
- [Website](https://react-immersive.liveroom.dev) · [Documentation](https://react-immersive.liveroom.dev/docs) · [Live editor](https://react-immersive.liveroom.dev/editor) · [npm](https://www.npmjs.com/package/@liveroom-tech/react-immersive) · [GitHub](https://github.com/Liveroom-Technologies/react-immersive-docs)
7
+ **Documentation baseline: 5.2.0 · React 19.**
6
8
 
7
- ## What It Does
9
+ [Documentation](https://react-immersive.liveroom.dev/docs) ·
10
+ [AI Agent Guide](https://react-immersive.liveroom.dev/docs/agent-guide.md) ·
11
+ [Markdown index](https://react-immersive.liveroom.dev/llms.txt) ·
12
+ [Examples](https://react-immersive.liveroom.dev/docs/examples) ·
13
+ [Public GitHub](https://github.com/Liveroom-Technologies/react-immersive-docs)
8
14
 
9
- The library currently exports:
15
+ ## Choose a component
10
16
 
11
- ```ts
12
- import {
13
- material,
14
- ModelViewer,
15
- patchBindings,
16
- patchSceneConfig,
17
- useObjectBinding,
18
- useObjectBindingIds,
19
- useObjectBindings,
20
- useSceneConfig,
21
- useShareableViewerState,
22
- useViewer,
23
- useViewerActions,
24
- useViewerAnimations,
25
- useViewerCamera,
26
- useViewerConnection,
27
- useViewerEffects,
28
- useViewerHover,
29
- useViewerIsolation,
30
- useViewerModel,
31
- useViewerSelection,
32
- MATERIAL_BLENDING_MODES,
33
- MATERIAL_SIDES,
34
- } from "@liveroom-tech/react-immersive";
35
- import { SimpleModelViewer } from "@liveroom-tech/react-immersive/simple-model-viewer";
36
- import { BindingBuilder } from "@liveroom-tech/react-immersive/binding-builder";
37
- ```
38
-
39
- Each component has its own entry point, so an app downloads only the ones it
40
- imports.
41
-
42
- It also exports types for `ObjectBinding`, `ObjectBindingMaterial`, `ObjectBindingsController`, `ObjectActionEvent`, `ObjectTransformSpace`, `SceneConfig`, `SceneConfigPatch`, `SceneConfigController`, `AnimationControls`, `CinematicConfig` / `CinematicWaypoint`, and related shapes used throughout this document.
43
-
44
- `ModelViewer` provides:
45
-
46
- - GLB/GLTF, OBJ/MTL, FBX, USDZ, STL, and PLY model rendering through `@react-three/fiber`
47
- - camera-driven 3D Tiles streaming through `tilesetUrl`, with configurable screen-space error and bounded tile cache
48
- - orbit/pan/zoom camera controls via `CameraControls`
49
- - 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 `refitOnResize` to instead preserve the user's orbit/zoom
50
- - scene lighting through `sceneConfig.lighting`, with the `sceneLight` helper and a runtime lights controller
51
- - optional custom camera configuration through a `camera` prop
52
- - optional custom background color through a `backgroundColor` prop
53
- - optional shadow rendering toggle through a `shadows` prop (on by default)
54
- - optional on-screen movement controller through `showMouseController` and its tuning props
55
- - optional canvas-focused keyboard camera navigation through `enableKeyboardNavigation`
56
- - keyboard object navigation, screen-reader selection announcements, and reduced motion for visitors who ask for it (see [Accessibility](#accessibility))
57
- - optional top-right camera orientation control through `showViewGizmo`
58
- - optional per-object move and rotate gizmo through `moveModeEnabled`, with World/Local axis alignment through `objectTransformSpace`
59
- - optional custom left object data panel through `customObjectBindingDataPanel`
60
- - optional custom right scene objects panel through `customSceneObjectsPanel`
61
- - your own UI over the 3D view through `overlay`, and react-three-fiber content inside the scene through `unsafe_sceneChildren`
62
- - optional hiding of either built-in panel through `showObjectBindingDataPanel` and `showSceneObjectsPanel`
63
- - optional model export button through `showDownloadButton` and `downloadFilename`, opt-in PNG capture through `preserveDrawingBuffer`, plus an independent `showResetButton` toggle for the rest of that action bar
64
- - an optional UV Checker toolbar button through `showUvCheckerButton`, for inspecting UV scale, stretching, seams, orientation, and missing UV0 coordinates directly in `ModelViewer`
65
- - glTF material variants (`KHR_materials_variants`) as swatch rows, one per option group, through `showMaterialVariants`, selected by `sceneConfig.model.activeVariants`
66
- - distance, angle, polygon area, and point-to-plane measurements with vertex/edge snapping and bounding dimensions through `showMeasureTools` and `measurementUnit`
67
- - an exploded-view slider that slides each bound part outward from the model center to reveal interior/assembly structure through `showExplodeControls`
68
- - isolation that ghosts or hides everything but the objects you focus on, through `isolation` / `onIsolationChange` and `showIsolateControls`
69
- - a section cut that opens the model along an axis, with a draggable plane, through `showSectionTools` and `sceneConfig.section`
70
- - 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
71
- - video export of a cinematic pass or turntable revolution, through the download menu or `captureVideo`, as browser-supported WebM/MP4 or an animated GIF
72
- - `CompareViewer`, which shows two configurations (or two models) with one camera, as a before/after slider or side by side
73
- - opt-in WebGPU rendering through `perf={{ webgpu: true }}`, falling back to WebGL where the browser lacks it
74
- - lazy loading: a viewer starts its 3D view when it's near the screen, and a page of viewers stays within the browser's limit on 3D views, with a `poster` image and an optional "View in 3D" button (`reveal`) until the view is there (see [Lazy loading and posters](#lazy-loading-and-posters))
75
- - a guided-tour control cluster (Previous/Stop/Next) for stepping through annotations, toggleable via `showAnnotationNavigation`
76
- - optional annotation detail popups on marker hover via `showAnnotationOnHover`
77
- - PBR, Matcap, and UV Checker renderer modes, with the checker exposing UV stretching, seams, rotation, and missing UV0 data directly on the model
78
- - a `sceneConfig` prop accepting the same scene-wide config (lighting, environment, background, post-processing, animations, annotations) authored by `BindingBuilder`'s Scene tab
79
- - WebGL context-loss recovery with a reload button and automatic rebuild when the context returns, keeping the camera and the visitor's changes
80
- - render-loop and perf tuning through `renderMode`, `maxDpr`, `performanceProfile`, and compressed-asset decoder options (`dracoDecoderPath`, `ktx2TranscoderPath`, `meshopt`)
81
- - 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
82
- - mesh selection by object name
83
- - a built-in left side panel for the selected object
84
- - object-specific action buttons driven by `objectBindings`
85
- - built-in color picking that commits the chosen hex into `objectBindings.style.material.baseColor`
86
- - built-in texture upload that commits a URL into `objectBindings.style.material.texture.path` (blob URL by default, or a durable URL when `onTextureUpload` is provided)
87
- - per-object `MeshPhysicalMaterial` overrides for metalness, roughness, emissive, normal/bump, AO, displacement, clearcoat, sheen, anisotropy, specular, transmission, thickness, reflectivity, sidedness, and limited blending modes
88
- - per-object visibility toggling through `objectBindings.visible`
89
- - decals: images and text printed on an object's surface through `objectBindings.decals`, placed from the surface point pointer events report
90
- - data-driven looks through `sceneConfig.styleRules`: colour scales from metrics, material overrides by status or metadata, and pulsing alarms
91
- - hover and selected-state highlighting
92
- - external selection state control through `selectedObject` and `onObjectSelect`
93
- - hover callbacks through `onObjectHover`
94
- - double-click, right-click, and touch long-press callbacks through `onObjectDoubleClick` and `onObjectContextMenu`
95
- - model-ready callbacks through `onModelLoaded`
96
- - model-load error callbacks through `onLoadError`
97
- - camera change callbacks through `onCameraChange`
98
- - viewer-ready callbacks through `onViewerReady`
99
- - action event callbacks through `onAction`
100
- - opt-in engagement analytics through `onEngagement`, with `debugEngagement` console logging
101
- - animation playback for GLB/GLTF, FBX, and USDZ models with supported embedded animations through `onAnimationsReady`
102
- - annotation marker callbacks through `onAnnotationsChange` and controlled/uncontrolled `activeAnnotation` / `onActiveAnnotationChange`
103
-
104
- `BindingBuilder` provides:
105
-
106
- - a design-time UI for generating starter bindings from GLB, GLTF, OBJ, FBX, USDZ, STL, and PLY assets, gated by `licenseKey` (see "`BindingBuilder` licensing & plan tiers" below)
107
- - demo model loading or custom `.glb`, `.gltf`, `.obj`, `.fbx`, `.usdz`, `.stl`, `.ply`, or model ZIP bundle upload
108
- - editable binding fields for identity, basics, style, actions, metrics, metadata, and saved camera state
109
- - per-object move and rotate authoring with a geometry-centered pivot, World/Local axes, numeric fields, and restore-to-authored-transform support
110
- - a one-click material preset gallery (wood, metal, chrome, glass, plastic, fabric, ceramic, concrete, etc.)
111
- - undo/redo for both the Object tab's bindings and the Scene tab's config, independently, with `Cmd/Ctrl+Z` / `Cmd/Ctrl+Shift+Z` shortcuts
112
- - a Scene tab for the same `sceneConfig` shape `ModelViewer` accepts (lighting, environment, background, post-processing, animations, annotations, and cinematic camera paths)
113
- - 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.cinematic` and replayed by `ModelViewer`'s `cinematic` prop
114
- - import a previously exported `objectBindings.json` or `sceneConfig.json` and merge it back onto the current model/config
115
- - live preview using `ModelViewer`
116
- - export as JSON or TypeScript, bundled into a `.zip` with a `materials/` folder automatically when any texture field was filled by file upload, otherwise a plain file
117
- - license-tier gating for some editor features (texture maps, environment lighting/backgrounds, wireframe preview, animation configuration, annotations, style rules, post-processing)
118
-
119
- `SimpleModelViewer` provides:
120
-
121
- - a lightweight GLB/GLTF/OBJ/FBX/USDZ/STL/PLY viewer with no bindings or actions that does not require a `licenseKey`
122
- - optional local `.glb`, standalone/data-URI `.gltf`, `.obj`, `.fbx`, `.usdz`, `.stl`, `.ply`, or model ZIP bundle upload mode for ad-hoc inspection without relying on the `modelUrl` asset
123
- - built-in scene objects panel with search and visibility toggles
124
- - 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
125
- - click-to-select and fit-to-object focus behavior
126
- - optional background color and model lifecycle callbacks
127
- - a smaller API surface for simple inspection use cases
128
-
129
- ## Why `objectBindings` Exists
130
-
131
- `objectBindings` solves the gap between a raw GLB/GLTF file and an interactive
132
- product experience. A model file gives you geometry, materials, and mesh node
133
- names. It does not know that `"CarBody"` is configurable, that `"FrontDoor"`
134
- should open a panel, or that `"Wall_A"` should expose metadata and a saved
135
- camera view.
136
-
137
- `objectBindings` is the bridge between those raw mesh names and your app's
138
- domain model. It is a serializable record, keyed by mesh node name, that lets
139
- you:
140
-
141
- - declare which meshes are interactive
142
- - carry live render state such as visibility, local transforms, and per-object material overrides
143
- - attach actions like `change-color`, `change-material`, and `toggle-visibility`
144
- - store metadata and metrics alongside the 3D scene
145
- - save camera framing so object focus can use curated views
146
-
147
- Because it is plain data, bindings can be authored by hand, generated through
148
- `BindingBuilder`, stored in a database, versioned as JSON, and round-tripped
149
- through `onObjectBindingsChange` so your app state and the 3D scene stay in
150
- sync.
151
-
152
- ## Why It Is Worth The Setup
17
+ | Need | Component and import | CSS import | Runtime key prop |
18
+ | --- | --- | --- | --- |
19
+ | Display/inspect a model and toggle mesh visibility | `SimpleModelViewer` from `@liveroom-tech/react-immersive/simple-model-viewer` | `@liveroom-tech/react-immersive/simple-model-viewer.css` | None |
20
+ | Interactive object bindings, materials, actions, tours, camera and app state | `ModelViewer` from `@liveroom-tech/react-immersive` | `@liveroom-tech/react-immersive/styles.css` | Required |
21
+ | Author and export bindings/scene configuration | `BindingBuilder` from `@liveroom-tech/react-immersive/binding-builder` | `@liveroom-tech/react-immersive/binding-builder.css` | Required |
153
22
 
154
- At first glance, `objectBindings` can look like extra setup. That is true only
155
- if your goal is to render a model and do nothing else with it.
23
+ Import each component's stylesheet once. Give the viewer's container an explicit
24
+ height. The package does not require Tailwind in the consuming application.
156
25
 
157
- If the model needs to behave like part of your application, bindings stop being
158
- overhead and start being the abstraction that keeps the project maintainable.
159
- They give you a single source of truth instead of scattering logic across mesh
160
- refs, material mutations, raycasting handlers, and custom UI glue.
26
+ ## Install
161
27
 
162
- The short version:
163
-
164
- - without bindings, the model is a visual asset
165
- - with bindings, the model becomes application state
166
-
167
- If you only need a viewer, use `SimpleModelViewer`. If you need selection,
168
- customization, persistence, metadata, object-level actions, or app-driven
169
- behavior, bindings are usually the right tradeoff.
170
-
171
- ## Example Applications Where Bindings Shine
172
-
173
- - product configurators for cars, furniture, apparel, appliances, or custom goods
174
- - e-commerce customization flows where meshes map to real purchasable options
175
- - CPQ and sales tools where 3D parts correspond to business rules and pricing
176
- - real estate and interior design tools with clickable rooms, walls, fixtures, and finishes
177
- - industrial and engineering viewers for assemblies, equipment, and maintenance workflows
178
- - training and guided walkthroughs with annotations, steps, and saved object viewpoints
179
- - digital twins and operational dashboards where scene parts map to live assets, metrics, and alerts
180
- - smart building controls where lights, switches, sensors, HVAC units, and doors map to real-time app behavior
181
-
182
- The pattern is consistent: if users need to say "this exact part of the model
183
- has its own meaning, state, or behavior," object bindings are a strong fit.
184
-
185
- ## Installation
28
+ In a React 19 application:
186
29
 
187
30
  ```bash
188
- npm install @liveroom-tech/react-immersive three @react-three/fiber
189
- ```
190
-
191
- Import the stylesheet for each component you use, once, from your
192
- application root (for example, Next.js `app/layout.tsx`):
193
-
194
- ```tsx
195
- import "@liveroom-tech/react-immersive/styles.css"; // ModelViewer
196
- import "@liveroom-tech/react-immersive/simple-model-viewer.css"; // SimpleModelViewer
197
- import "@liveroom-tech/react-immersive/binding-builder.css"; // BindingBuilder
198
- ```
199
-
200
- The styles ship as a static CSS file rather than a runtime-injected `<style>`
201
- tag, so a strict CSP does not need `style-src 'unsafe-inline'`. Tailwind is not
202
- required in the consuming app.
203
-
204
- If a component's stylesheet is missing, it renders unstyled, and in
205
- development it logs a console warning naming the import to add.
206
-
207
- Peer dependencies, which your app provides so there's exactly one copy of each:
208
-
209
- - `react` and `react-dom` 19
210
- - `three` 0.184
211
- - `@react-three/fiber` 9
212
-
213
- Everything else, including drei, post-processing, and WebXR support, installs
214
- with the package.
215
-
216
- `ModelViewer` and `BindingBuilder` require a `licenseKey` at runtime (see [`licenseKey`](#licensekey)). Get one from the Developer Portal, see [Resources](#resources) below.
217
-
218
- ## Resources
219
-
220
- - [Developer Portal](https://react-immersive.liveroom.dev), sign in, pick a plan, and generate a `licenseKey`
221
- - [Docs](https://react-immersive.liveroom.dev/docs), full reference for every component, hook, and prop
222
- - [Examples & community repo](https://github.com/liveroom-technologies/react-immersive-docs/examples), public docs source, starter examples, and community resources
223
- - [Report a bug / request a feature](https://github.com/liveroom-technologies/react-immersive-docs/issues), for public bug reports, docs issues, and feature requests
224
- - [Discussions & Q&A](https://github.com/liveroom-technologies/react-immersive-docs/discussions), ask questions, share ideas, and compare approaches
225
- - [Private support / security](mailto:developers@liveroom.xyz?subject=React%20Immersive%20Support), for license, account, confidential customer, or security-sensitive issues
226
- - [Live editor](https://react-immersive.liveroom.dev/editor), try `BindingBuilder` in the browser without installing anything
227
- - [npm package](https://www.npmjs.com/package/@liveroom-tech/react-immersive)
228
-
229
- ## Quick Start
230
-
231
- The shortest working viewer lets `ModelViewer` generate the bindings itself:
232
-
233
- ```tsx
234
- import { ModelViewer } from "@liveroom-tech/react-immersive";
235
-
236
- export default function Example() {
237
- return (
238
- <div style={{ height: "100vh", overflow: "hidden" }}>
239
- <ModelViewer
240
- modelUrl="/model.glb"
241
- licenseKey="your-license-key"
242
- />
243
- </div>
244
- );
245
- }
31
+ npm install @liveroom-tech/react-immersive@5.2.0 three@^0.184.0 @react-three/fiber@^9.6.1
246
32
  ```
247
33
 
248
- Every mesh in the model can then be hovered and selected, and opens the side
249
- panel with Change Color, Change Material, and Toggle Visibility (or, for meshes
250
- whose names contain "light", Toggle Light, Change Light Color, and Set
251
- Brightness). See
252
- [Generating bindings automatically](#generating-bindings-automatically) for how
253
- to keep and refine the generated bindings.
254
-
255
- To control the viewer from your own UI, use `useViewer`. It connects every
256
- viewer hook to `ModelViewer` at once:
257
-
258
- ```tsx
259
- import { ModelViewer, useViewer } from "@liveroom-tech/react-immersive";
260
-
261
- export default function Configurator() {
262
- const viewer = useViewer();
34
+ | Peer | Supported range |
35
+ | --- | --- |
36
+ | `react` | `^19.0.0` |
37
+ | `react-dom` | `^19.0.0` |
38
+ | `three` | `^0.184.0` |
39
+ | `@react-three/fiber` | `^9.6.1` |
263
40
 
264
- return (
265
- <div style={{ height: "100vh", overflow: "hidden" }}>
266
- <ModelViewer
267
- modelUrl="/model.glb"
268
- licenseKey="your-license-key"
269
- {...viewer.props}
270
- />
271
- <button
272
- onClick={() => viewer.bindings.setBaseColor({ group: "paint" }, "#b91c1c")}
273
- >
274
- Paint it red
275
- </button>
276
- <button onClick={() => viewer.camera.resetView()}>Reset view</button>
277
- </div>
278
- );
279
- }
280
- ```
41
+ The complete [Vite](https://react-immersive.liveroom.dev/docs/frameworks/vite)
42
+ and [Next.js App Router](https://react-immersive.liveroom.dev/docs/frameworks/nextjs)
43
+ guides include file locations, client boundaries, sample assets, TypeScript
44
+ configuration and run/build instructions. Standalone starters live in the
45
+ [public examples repository](https://github.com/Liveroom-Technologies/react-immersive-docs/tree/main/examples).
281
46
 
282
- `viewer.bindings`, `viewer.selection`, `viewer.hover`, `viewer.camera`,
283
- `viewer.model`, `viewer.animations`, `viewer.actions`, `viewer.scene`,
284
- `viewer.isolation`, `viewer.effects`, and `viewer.connection` are the results of the hooks they're
285
- named after (`useObjectBindings`, `useViewerSelection`, and so on), so
286
- everything below applies to them. Without a `bindings` option, `useViewer`
287
- generates bindings from the model like `objectBindings="auto"`; pass
288
- `useViewer({ bindings })` to start from your own. It also accepts
289
- `sceneConfig`, `camera` (the `useViewerCamera` options), and `onAction`,
290
- `onObjectSelect`, `onObjectHover`, and `onLoadError` callbacks. `sceneConfig`
291
- is passed to `ModelViewer` only once you've supplied one or changed the scene
292
- through `viewer.scene`, so the default scene behaves as if none were passed.
293
- Props set on `ModelViewer` after the spread take precedence.
47
+ ## First render
294
48
 
295
- The full example below authors bindings by hand and wires up every hook
296
- individually, which is what `useViewer` does for you. Wire them yourself when
297
- state lives elsewhere, such as bindings owned by a store:
49
+ Place your asset at `public/model.glb`. In a Vite React application:
298
50
 
299
51
  ```tsx
300
- import {
301
- ModelViewer,
302
- useObjectBinding,
303
- useObjectBindings,
304
- useViewerActions,
305
- useViewerAnimations,
306
- useViewerCamera,
307
- useViewerHover,
308
- useViewerModel,
309
- useViewerSelection,
310
- } from "@liveroom-tech/react-immersive";
311
- import { defineObjectBindings } from "@liveroom-tech/react-immersive/utils";
312
-
313
- const initialBindings = defineObjectBindings({
314
- CarBody: {
315
- id: "car-body",
316
- modelObjectId: "CarBody",
317
- type: "body",
318
- label: "Car Body",
319
- selectable: true,
320
- hoverable: true,
321
- visible: true,
322
- style: {
323
- material: {
324
- texture: {
325
- path: "/materials/oak-wood.jpg",
326
- },
327
- },
328
- },
329
- metrics: {},
330
- metadata: {
331
- category: "exterior",
332
- },
333
- actions: [
334
- { id: "change-color", label: "Change Color", type: "command" },
335
- { id: "change-material", label: "Change Material", type: "command" },
336
- { id: "toggle-visibility", label: "Toggle Visibility", type: "command" },
337
- {
338
- id: "toggle-body",
339
- label: "Toggle Body",
340
- type: "command",
341
- effects: [{ target: "CarBody", visibility: "toggle" }],
342
- },
343
- ],
344
- },
345
- });
346
-
347
- export default function Example() {
348
- const bindings = useObjectBindings(initialBindings);
349
- const {
350
- objectBindings,
351
- setObjectBindings,
352
- hiddenObjects,
353
- toggleObjectVisibility,
354
- } = bindings;
355
- const { selectedObjectBinding, handleObjectSelect } = useViewerSelection();
356
- const { hoveredObjectBinding, handleHoveredObject } = useViewerHover();
357
- const camera = useViewerCamera();
358
- const {
359
- cameraState,
360
- resetView,
361
- focusObject,
362
- fitScene,
363
- setCameraTarget,
364
- handleCameraChange,
365
- handleViewerReady,
366
- } = camera;
367
- const {
368
- isLoading,
369
- isReady,
370
- error,
371
- bounds,
372
- objectCount,
373
- handleModelLoaded,
374
- handleLoadError,
375
- } = useViewerModel();
376
- const animations = useViewerAnimations();
377
- const { getActionsForObject, handleAction, runAction } = useViewerActions({
378
- bindings,
379
- camera,
380
- animations,
381
- onAction: (event) => {},
382
- });
383
- const focusedBinding = useObjectBinding(objectBindings, "car-body");
52
+ import "@liveroom-tech/react-immersive/simple-model-viewer.css";
53
+ import { SimpleModelViewer } from "@liveroom-tech/react-immersive/simple-model-viewer";
384
54
 
55
+ export default function App() {
385
56
  return (
386
- <div style={{ height: "100vh", overflow: "hidden" }}>
387
- <ModelViewer
57
+ <div style={{ height: "100vh" }}>
58
+ <SimpleModelViewer
388
59
  modelUrl="/model.glb"
389
- licenseKey="your-license-key"
390
- objectBindings={objectBindings}
391
- selectedObject={selectedObjectBinding}
392
- onObjectSelect={handleObjectSelect}
393
- onObjectHover={handleHoveredObject}
394
- onCameraChange={handleCameraChange}
395
- onModelLoaded={handleModelLoaded}
396
- onLoadError={handleLoadError}
397
- onObjectBindingsChange={setObjectBindings}
398
- onViewerReady={handleViewerReady}
399
- onAnimationsReady={animations.handleAnimationsReady}
400
- onAction={handleAction}
60
+ onModelLoaded={(model) => console.log("Loaded", model.objectCount, "objects")}
61
+ onLoadError={(error) => console.error("Model failed to load", error)}
401
62
  />
402
-
403
- <button onClick={() => toggleObjectVisibility("car-body")}>
404
- Toggle Visibility
405
- </button>
406
- <button onClick={() => runAction("car-body", "toggle-body")}>
407
- Run declarative action
408
- </button>
409
- <button onClick={() => resetView()}>Reset view</button>
410
- <button onClick={() => focusObject("car-body")}>Focus Object</button>
411
- <button onClick={() => fitScene()}>Fit scene</button>
412
- <button onClick={() => setCameraTarget([0, 0, 0])}>Target origin</button>
413
- <pre>{JSON.stringify(getActionsForObject("car-body"), null, 2)}</pre>
414
- <pre>{JSON.stringify(cameraState, null, 2)}</pre>
415
- <pre>
416
- {JSON.stringify(
417
- { isLoading, isReady, error, bounds, objectCount },
418
- null,
419
- 2,
420
- )}
421
- </pre>
422
- <pre>{JSON.stringify(hiddenObjects, null, 2)}</pre>
423
- <pre>{JSON.stringify(hoveredObjectBinding, null, 2)}</pre>
424
- <pre>{JSON.stringify(focusedBinding, null, 2)}</pre>
425
63
  </div>
426
64
  );
427
65
  }
428
66
  ```
429
67
 
430
- If you want a simple helper for selection state, the library also exports:
431
-
432
- ```tsx
433
- const {
434
- selectedObjectBinding,
435
- setSelectedObjectBinding,
436
- handleObjectSelect,
437
- clearSelection,
438
- } = useViewerSelection();
439
- ```
440
-
441
- If you want to resolve a binding from an object identifier, the library also exports:
442
-
443
- ```tsx
444
- const binding = useObjectBinding(objectBindings, "car-body");
445
- ```
446
-
447
- `useObjectBinding` checks, in order:
448
-
449
- - the object binding map key
450
- - `binding.id`
451
- - `binding.modelObjectId`
452
-
453
- If you want a flat list of every `modelObjectId` across your bindings, the library also exports:
454
-
455
- ```tsx
456
- const ids = useObjectBindingIds(objectBindings);
457
- // → ["CarBody", "WheelFL", "WheelFR", ...]
458
- ```
459
-
460
- If you want one state owner for binding patches, materials, and visibility, use `useObjectBindings`:
461
-
462
- ```tsx
463
- const {
464
- objectBindings,
465
- setObjectBindings,
466
- updateObjectBindings,
467
- selectObjectBindings,
468
- selectObjectBindingIds,
469
- setMaterial,
470
- setBaseColor,
471
- setTexture,
472
- clearTexture,
473
- copyMaterial,
474
- resetMaterial,
475
- getMaterial,
476
- setTransform,
477
- resetTransform,
478
- updateMetadata,
479
- addDecal,
480
- updateDecal,
481
- removeDecal,
482
- hiddenObjects,
483
- hiddenObjectIds,
484
- hideObject,
485
- showObject,
486
- toggleObjectVisibility,
487
- isObjectHidden,
488
- clearHiddenObjects,
489
- } = useObjectBindings(initialBindings);
490
-
491
- <ModelViewer
492
- modelUrl="/model.glb"
493
- licenseKey="your-license-key"
494
- objectBindings={objectBindings}
495
- onObjectBindingsChange={setObjectBindings}
496
- />;
497
- ```
498
-
499
- `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.
500
-
501
- Object identifiers accepted by its methods can be:
502
-
503
- - the object binding map key
504
- - `binding.id`
505
- - `binding.modelObjectId`
506
-
507
- You can also target bindings by a shared `group`, `tag`, or object `type`. Groups
508
- represent one named set, while tags let a binding participate in several sets:
509
-
510
- ```tsx
511
- updateObjectBindings({ group: "paint" }, { selectable: false });
512
- setBaseColor({ tag: "exterior" }, "#2563eb");
513
- hideObject({ type: "wheel" });
514
-
515
- const exteriorBindings = selectObjectBindings({ tag: "exterior" });
516
- const paintIds = selectObjectBindingIds({ group: "paint" });
517
-
518
- setTexture({ group: "screens" }, "/images/dashboard.png");
519
- clearTexture({ tag: "printable" }, "baseColor");
520
- copyMaterial("wheel-front", { group: "wheels" });
521
-
522
- setTransform("door-left", {
523
- rotation: [0, Math.PI / 2, 0],
524
- });
525
- resetTransform("door-left");
526
- updateMetadata({ group: "lights" }, { state: "on" });
527
- ```
528
-
529
- When a parent or store owns the state, use the pure `selectBindings` and
530
- `selectBindingKeys` helpers for the same queries, and `patchBindings` to update
531
- all matches. Import these helpers from
532
- `@liveroom-tech/react-immersive/utils` when the module must remain usable from a
533
- React Server Component; the main entry continues to re-export them for
534
- backwards compatibility.
535
-
536
- The hook returns:
537
-
538
- - `objectBindings` and `setObjectBindings`
539
- - `updateObjectBindings` for any binding field or atomic edit batch
540
- - `selectObjectBindings` and `selectObjectBindingIds` for group, tag, or type queries
541
- - `setMaterial`, `setBaseColor`, `setTexture`, `clearTexture`, `copyMaterial`, `resetMaterial`, and `getMaterial`
542
- - `setTransform` and `resetTransform` for persistent local-space model transforms
543
- - `updateMetadata` for recursively merged application data
544
- - `addDecal`, `updateDecal`, and `removeDecal` for images and text printed on objects (see [Decals](#decals))
545
- - `hiddenObjects`: a derived map of hidden binding keys
546
- - `hiddenObjectIds`: the currently hidden binding IDs
547
- - `hideObject`, `showObject`, `toggleObjectVisibility`
548
- - `isObjectHidden`
549
- - `clearHiddenObjects`
550
-
551
- For scene-wide state, use `useSceneConfig`:
552
-
553
- ```tsx
554
- const {
555
- sceneConfig,
556
- setSceneConfig,
557
- updateSceneConfig,
558
- resetSceneConfig,
559
- lights,
560
- } = useSceneConfig(initialSceneConfig);
561
-
562
- const enableUvChecker = () =>
563
- updateSceneConfig({
564
- model: { renderer: "uv-checker" },
565
- lighting: { ambient: { intensity: 0.5 } },
566
- });
567
-
568
- lights.setIntensity("bulb-light", 4);
569
- lights.setColor("bulb-light", "#ffd27d");
570
- lights.toggle("bulb-light");
571
- lights.attach("bulb-light", {
572
- objectId: "bulb-filament",
573
- offset: [0, -0.2, 0],
574
- });
575
-
576
- <ModelViewer
577
- modelUrl="/model.glb"
578
- licenseKey="your-license-key"
579
- objectBindings={objectBindings}
580
- sceneConfig={sceneConfig}
581
- />;
582
-
583
- <button onClick={enableUvChecker}>Show UV checker</button>;
584
- ```
585
-
586
- Nested objects merge recursively, while arrays and tuples are replaced. Every
587
- update uses a functional state updater, so consecutive calls compose. Use the
588
- pure `patchSceneConfig(sceneConfig, patch)` utility when a store or parent owns
589
- the scene state. The namespaced `lights` controller also provides `add`,
590
- `update`, `remove`, `show`, `hide`, `toggle`, `setIntensity`, `setColor`,
591
- `attach`, and `detach`. Attached lights follow a bound model object without an
592
- R3F light component.
593
-
594
- For high-frequency visual effects, use `useViewerEffects`. It mutates the live
595
- viewer efficiently, requests demand-rendered frames automatically, and restores
596
- its runtime changes on cleanup. Application code does not need Three.js node or
597
- material access:
598
-
599
- ```tsx
600
- const {
601
- addPointLight,
602
- flicker,
603
- handleViewerReady,
604
- overrideEffect,
605
- stopEffect,
606
- } = useViewerEffects();
607
-
608
- useEffect(() => {
609
- flicker(
610
- {
611
- materials: [
612
- {
613
- targets: { group: "filaments" },
614
- values: { emissiveIntensity: [0.02, 8] },
615
- },
616
- ],
617
- lights: [
618
- {
619
- lights: ["light-left", "light-right"],
620
- intensity: [0, 180],
621
- },
622
- ],
623
- },
624
- { id: "bulbs", synchronized: true, blackoutChance: 0.1 },
625
- );
626
-
627
- return () => stopEffect("bulbs");
628
- }, [flicker, stopEffect]);
629
-
630
- <ModelViewer
631
- modelUrl="/bulbs.glb"
632
- licenseKey="your-license-key"
633
- objectBindings={objectBindings}
634
- onViewerReady={(viewer) => {
635
- handleViewerReady(viewer);
636
- addPointLight({
637
- id: "light-left",
638
- target: "filament-left",
639
- color: "#ff9a3c",
640
- intensity: 0,
641
- distance: 30,
642
- });
643
- }}
644
- />;
645
-
646
- <button onClick={() => overrideEffect("bulbs", 0, 800)}>Blackout</button>;
647
- ```
648
-
649
- `transition`, `timeline`, `pulse`, and `flicker` share material, transform, and
650
- light channels. `setMaterial`, `setTransform`, and `getWorldPosition` cover
651
- immediate runtime work. `addPointLight`, `updatePointLight`, and
652
- `removePointLight` manage lights attached to model objects. `resetRuntime` stops
653
- active effects, restores snapshotted values, and disposes runtime lights.
654
-
655
- If you want to keep hover state in sync with the viewer, the library also exports:
656
-
657
- ```tsx
658
- const { hoveredObjectBinding, handleHoveredObject } = useViewerHover();
659
-
660
- <ModelViewer
661
- modelUrl="/model.glb"
662
- licenseKey="your-license-key"
663
- objectBindings={objectBindings}
664
- onObjectHover={handleHoveredObject}
665
- />;
666
- ```
667
-
668
- `useViewerHover` returns:
669
-
670
- - `hoveredObjectBinding`: the currently hovered binding or `null`
671
- - `handleHoveredObject`: a callback you can pass directly to `onObjectHover`
672
-
673
- If you want model lifecycle and scene metadata, the library also exports:
674
-
675
- ```tsx
676
- const {
677
- isLoading,
678
- isReady,
679
- error,
680
- bounds,
681
- objectCount,
682
- materialVariants,
683
- objects,
684
- handleModelLoaded,
685
- handleObjectCatalog,
686
- handleLoadError,
687
- } = useViewerModel();
688
-
689
- <ModelViewer
690
- modelUrl="/model.glb"
691
- licenseKey="your-license-key"
692
- objectBindings={objectBindings}
693
- onModelLoaded={handleModelLoaded}
694
- onObjectCatalog={handleObjectCatalog}
695
- onLoadError={handleLoadError}
696
- />;
697
- ```
698
-
699
- `useViewerModel` returns:
700
-
701
- - `isLoading`: `true` until the model has loaded or failed
702
- - `isReady`: `true` after a successful model load
703
- - `error`: the most recent load/render error, or `null`
704
- - `bounds`: the loaded scene bounds as `min`, `max`, `center`, and `size`
705
- - `objectCount`: the number of mesh objects in the loaded scene
706
- - `materialVariants`: the names of the model's glTF material variants, or an
707
- empty array (see [`showMaterialVariants`](#showmaterialvariants))
708
- - `objects`: every bindable object in the model as a `ViewerObjectInfo`, once
709
- `handleObjectCatalog` is passed to `onObjectCatalog`
710
- - `handleModelLoaded`: a callback you can pass directly to `onModelLoaded`
711
- - `handleObjectCatalog`: a callback you can pass directly to `onObjectCatalog`
712
- - `handleLoadError`: a callback you can pass directly to `onLoadError`
713
-
714
- If you want to inspect or trigger binding actions programmatically, the library also exports:
715
-
716
- ```tsx
717
- const { getActionsForObject, handleAction, runAction } = useViewerActions({
718
- bindings,
719
- scene,
720
- camera,
721
- animations,
722
- onAction: (event) => {},
723
- });
724
-
725
- const actions = getActionsForObject("car-body");
726
- await runAction("car-body", "power-on");
727
-
728
- <ModelViewer onAction={handleAction} />;
729
- ```
730
-
731
- `useViewerActions` returns:
732
-
733
- - `getActionsForObject`: resolves the actions configured for a binding by key, `binding.id`, or `binding.modelObjectId`
734
- - `handleAction`: executes an action event and can be passed directly to `ModelViewer.onAction`
735
- - `runAction`: finds and executes an action, then resolves to its event, or `null` if the binding or action cannot be found
736
-
737
- 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.
68
+ For Next.js, import CSS in `app/layout.tsx` and use the complete client component
69
+ from the [Next.js guide](https://react-immersive.liveroom.dev/docs/frameworks/nextjs).
70
+ The public [Red Room sample](https://react-immersive.liveroom.dev/red_room.glb)
71
+ is by [LaisLH](https://skfb.ly/6VBJR), licensed under
72
+ [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/); retain attribution.
738
73
 
739
- `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.
74
+ ## Add interaction
740
75
 
741
- ## `BindingBuilder`
742
-
743
- `BindingBuilder` is an exported design-time helper component for authoring `objectBindings` in the browser.
744
-
745
- Basic usage:
76
+ `ModelViewer` generates bindings automatically when `objectBindings` is omitted.
77
+ Use `useViewer` to connect bindings, selection, camera, animations and other
78
+ controllers to one viewer:
746
79
 
747
80
  ```tsx
748
- import "@liveroom-tech/react-immersive/binding-builder.css";
749
- import { BindingBuilder } from "@liveroom-tech/react-immersive/binding-builder";
750
-
751
- export default function App() {
752
- return <BindingBuilder licenseKey="your-license-key" />;
753
- }
754
- ```
755
-
756
- `BindingBuilder` accepts one required prop and the same optional decoder
757
- configuration as the viewers:
758
-
759
- ```ts
760
- type BindingBuilderProps = {
761
- licenseKey: string;
762
- dracoDecoderPath?: string | false;
763
- ktx2TranscoderPath?: string | false;
764
- meshopt?: boolean;
765
- };
766
- ```
767
-
768
- `licenseKey` is validated the same way as `ModelViewer`'s (see [`licenseKey`](#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.
769
-
770
- Current behavior, Object tab:
771
-
772
- - upload a `.glb`, standalone/data-URI `.gltf`, `.obj`, `.fbx`, `.usdz`, `.stl`, or `.ply` file, or a `.zip` bundle (a `.gltf` with its `.bin` and textures, an `.obj` with its MTL and textures, or an `.fbx` with external textures)
773
- - traverse renderable mesh nodes and generate starter bindings automatically
774
- - for an object without texture coordinates (STL, most PLY), the **Material** image map fields are turned off with a note, and the starter actions leave out **Change Material** (see [STL and PLY](#stl-and-ply))
775
- - edit binding identity, label, type, status, booleans, style, actions, metrics, metadata, and saved camera state
776
- - add decals to the selected object from the **Decals** panel: upload an image or add text to place it automatically at the centre of the camera-facing surface, then set its size, rotation, and opacity; **Move** lets you drag the decal across the object's surface. Release to save, or press Escape to cancel. Uploaded images are exported under `materials/` with the bindings
777
- - 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
778
- - apply a one-click material preset (wood, metal, chrome, glass, plastic, fabric, ceramic, concrete, etc.) onto the selected object's material
779
- - undo/redo the current bindings (toolbar buttons or `Cmd/Ctrl+Z` / `Cmd/Ctrl+Shift+Z`); rapid edits coalesce into a single undo step
780
- - import a previously exported `objectBindings.json`, merged onto the current model by `modelObjectId`
781
- - export the current bindings as JSON or TypeScript, with TypeScript exports wrapped in `defineObjectBindings` to preserve literal IDs, bundled into a `.zip` (with a `materials/` folder) when any texture field was filled by uploading a file, otherwise a plain file
782
-
783
- Current behavior, Scene tab:
784
-
785
- - configure the same `SceneConfig` shape `ModelViewer`'s `sceneConfig` prop accepts (lighting, environment, background, ground shadows, wireframe, post-processing, animations, annotations)
786
- - configure auto-rotation and its speed, exported as `sceneConfig.model.autoRotate` and `sceneConfig.model.autoRotateSpeed`
787
- - preview animation clips, several at once and forwards or backwards, and set each clip's loop mode (loop, play once, or ping-pong), speed, and display name
788
- - check changes with **Compare** above the preview: a before/after divider across the preview, with the project as it was opened on the left and as it is now on the right; orbiting either side moves both, and **Pin** on the "Before" label makes the current state the "before" for the next round of changes
789
- - pick which of the model's glTF material variants the scene starts with, one per option group, exported as `sceneConfig.model.activeVariants`
790
- - isolate a node from the Scene Nodes list to see it on its own in the preview while you edit it (not exported)
791
- - set up a section cut (**General → Section Cut**: axis, position, and side to keep), exported as `sceneConfig.section`; the preview has the section toolbar and draggable plane too
792
- - build style rules (**Rules** sub-tab) that restyle objects from their metrics, status, and metadata, with suggestions from the model's bindings, a live match count, and a live preview; exported as `sceneConfig.styleRules`
793
- - position and rotate directional, point, and spot lights with transform gizmos; spot lights also expose angle and penumbra controls
794
- - undo/redo the scene config independently of the Object tab's bindings history
795
- - import a previously exported `sceneConfig.json`, merged by top-level section
796
- - export the current scene config as JSON or TypeScript
797
-
798
- Preview:
799
-
800
- - the selected node previews live in an embedded `ModelViewer`
801
-
802
- Autosave:
803
-
804
- - the session (the model, its object bindings, and the scene settings, with an uploaded model file included) is saved to this browser's IndexedDB, in a database named `react-immersive:binding-builder`, shortly after each edit and when the tab is hidden or closed
805
- - **Restore last session** in the preview's "⋯" menu reopens it; one session is kept per browser
806
- - while there are edits on screen, leaving or reloading the page asks for confirmation
807
- - if the browser refuses to store a session, usually for lack of space, the editor says so; export your work to keep it
808
- - 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`
809
-
810
- ### `BindingBuilder` licensing & plan tiers
811
-
812
- Some editor features are gated by the license's plan tier (the server-returned `tier`, normalized to `"free" | "starter" | "growth"`):
81
+ "use client";
813
82
 
814
- | Tier | Unlocks |
815
- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
816
- | `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 |
817
- | `starter` | + Texture maps, the specular workflow, reflectivity, environment lighting, environment backgrounds, wireframe preview, animation configuration, annotations, style rules, decals |
818
- | `growth` | + Post-processing effects |
819
-
820
- A locked feature renders in place (not hidden) with a message naming the required plan, so it stays discoverable while editing.
821
-
822
- ## `SimpleModelViewer`
823
-
824
- `SimpleModelViewer` is an exported lightweight viewer for cases where you want to load a GLB, GLTF, OBJ, FBX, USDZ, STL, or PLY 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.
825
-
826
- Basic usage:
83
+ import { ModelViewer, useViewer } from "@liveroom-tech/react-immersive";
84
+ import "@liveroom-tech/react-immersive/styles.css";
827
85
 
828
- ```tsx
829
- import "@liveroom-tech/react-immersive/simple-model-viewer.css";
830
- import { SimpleModelViewer } from "@liveroom-tech/react-immersive/simple-model-viewer";
86
+ export function InteractiveViewer({ licenseKey }: { licenseKey: string }) {
87
+ const viewer = useViewer();
831
88
 
832
- export default function App() {
833
89
  return (
834
90
  <div style={{ height: "100vh" }}>
835
- <SimpleModelViewer modelUrl="/model.glb" />
91
+ <ModelViewer modelUrl="/model.glb" licenseKey={licenseKey} {...viewer.props} />
836
92
  </div>
837
93
  );
838
94
  }
839
95
  ```
840
96
 
841
- `SimpleModelViewer` does not take a `licenseKey`, licensing is enforced by `ModelViewer` and `BindingBuilder` only.
842
-
843
- Sizing note:
844
-
845
- - `SimpleModelViewer` fills the width and height of its parent container
846
- - give the parent an explicit height (for example `100vh`, `480px`, or a Tailwind class like `h-[100px]`) when you want a fixed viewer height
847
-
848
- Current behavior:
849
-
850
- - discovers renderable meshes directly from the loaded model scene
851
- - renders a right-side scene objects panel with search and visibility toggles
852
- - 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
853
- - optionally replaces `modelUrl` with a drag-and-drop / file-picker flow when `enableModelUpload` is enabled
854
- - lets users click a mesh or panel row to select and focus it
855
- - fits the full scene on initial load
856
- - uses built-in ambient, directional, and environment lighting
857
-
858
- Use `SimpleModelViewer` when you want a quick inspection viewer.
859
- Use `ModelViewer` when you need binding-driven styling, actions, metadata, custom panels, exports, or animation integrations.
860
-
861
- If you want camera state and imperative camera helpers, the library also exports:
862
-
863
- ```tsx
864
- const camera = useViewerCamera({
865
- initialPosition: [0, 2, 8],
866
- initialTarget: [0, 1.2, 0],
867
- });
868
-
869
- const {
870
- cameraState,
871
- resetView,
872
- focusObject,
873
- fitScene,
874
- setCameraTarget,
875
- lookAt,
876
- orbitTo,
877
- dollyTo,
878
- savePreset,
879
- goToPreset,
880
- handleCameraChange,
881
- handleViewerReady,
882
- } = camera;
883
-
884
- <ModelViewer
885
- modelUrl="/model.glb"
886
- licenseKey="your-license-key"
887
- objectBindings={objectBindings}
888
- onCameraChange={handleCameraChange}
889
- onViewerReady={handleViewerReady}
890
- />;
891
- ```
892
-
893
- `useViewerCamera` returns:
894
-
895
- - `cameraState`: the latest camera `position`, `target`, `fov`, and `zoom`
896
- - `resetView`: resets the controls to their initial saved state
897
- - `focusObject`: fits the camera to a binding by key, `binding.id`, or `binding.modelObjectId`
898
- - `fitScene`: fits the camera to the whole loaded scene
899
- - `setCameraTarget`: sets the camera target to a given `[x, y, z]`
900
- - `setCameraState`: restores a full camera state (`position`/`target`/`fov`/`zoom`) previously read from `cameraState`
901
- - `lookAt`: moves the position and target together
902
- - `orbitTo`: orbits to azimuth and polar angles in degrees (azimuth 0 is the front, 90 the right; polar 0 is straight down, 90 level)
903
- - `dollyTo`: moves to a distance from the current target
904
- - `savePreset`: captures the current camera under a name
905
- - `goToPreset`: moves to a named camera preset
906
- - `handleCameraChange`: a callback you can pass directly to `onCameraChange`
907
- - `handleViewerReady`: a callback you can pass directly to `onViewerReady`
908
-
909
- Pass `initialPosition`, `initialTarget`, and optional named `presets` to `useViewerCamera` to configure the first view without writing an `onViewerReady` wrapper.
910
-
911
- If you want a link that opens the viewer the way it looks now, the library also exports `useShareableViewerState`. It keeps the camera, the hidden objects, and the selection in the page URL's hash (`#view=…`), and puts them back when the link is opened:
912
-
913
- ```tsx
914
- const viewer = useViewer();
915
- const share = useShareableViewerState({
916
- cameraState: viewer.camera.cameraState,
917
- setCameraState: viewer.camera.setCameraState,
918
- objectBindings: viewer.bindings.objectBindings,
919
- onObjectBindingsChange: viewer.bindings.setObjectBindings,
920
- selectedObjectBinding: viewer.selection.selectedObjectBinding,
921
- setSelectedObjectBinding: viewer.selection.setSelectedObjectBinding,
922
- });
923
-
924
- <ModelViewer
925
- modelUrl="/model.glb"
926
- licenseKey="your-license-key"
927
- {...viewer.props}
928
- onViewerReady={(ready) => {
929
- viewer.props.onViewerReady(ready);
930
- void share.restoreFromUrl();
931
- }}
932
- refitOnResize={false}
933
- />;
934
-
935
- <button onClick={() => navigator.clipboard.writeText(share.getShareUrl())}>
936
- Copy link
937
- </button>;
938
- ```
939
-
940
- `useShareableViewerState` takes:
941
-
942
- - `cameraState` and `setCameraState`: from `useViewerCamera` (`viewer.camera`)
943
- - `objectBindings` and `onObjectBindingsChange`: the bindings and their setter; hidden objects are restored by setting their `visible` to `false`
944
- - `selectedObjectBinding` and `setSelectedObjectBinding`: from `useViewerSelection` (`viewer.selection`). `ModelViewer` also needs `selectedObject` (`viewer.props` passes it), or a restored selection won't show
945
- - `paramKey`: the hash key the view is stored under. Default `"view"`
946
- - `debounceMs`: how long after a change the hash is updated, in milliseconds. Default `400`
947
- - `autoSync`: keep the hash up to date as the view changes. Default `true`. With `false`, write it with `syncUrl` or build links with `getShareUrl`
948
-
949
- `useShareableViewerState` returns:
950
-
951
- - `getShareUrl`: the current page URL with the current view in its hash, without changing the address bar
952
- - `syncUrl`: writes the current view into the hash now, with `history.replaceState`, so no history entry is added
953
- - `restoreFromUrl`: applies the view in the hash, if there is one, and resolves `true` once it has applied all of it. Called again, it does nothing until the hash holds a different view, so it's safe to call from `onViewerReady`, which runs often
954
-
955
- Things to know:
956
-
957
- - The link holds the view, not the model. Keep whatever identifies the model, such as a route or query parameter, in the URL too. Objects are stored by `binding.id`, so links keep working while the ids stay the same.
958
- - The hash is only updated after `restoreFromUrl` has been called once, so an incoming link isn't overwritten before it's read. Call it even if you don't restore links; with nothing in the hash it does nothing.
959
- - When the bindings come from the model, as with `useViewer` or `objectBindings="auto"`, they exist only once it has loaded, after the first `onViewerReady`. A link that hides or selects objects then restores its camera at once and waits for them: `onViewerReady` runs again when the bindings arrive, and that call hides and selects them. The hash isn't updated while it waits.
960
- - The camera jumps to the shared view without a transition. `refitOnResize={false}` keeps a later resize from re-framing the model away from it.
961
- - The hook listens for `hashchange`, so pasting a newer link into an open tab applies it.
962
-
963
- See the [Shareable View example](https://react-immersive.liveroom.dev/examples/shareable-view) and the [`useShareableViewerState` docs](https://react-immersive.liveroom.dev/docs/hooks/use-shareable-viewer-state).
964
-
965
- If your GLB/GLTF model contains animations, the library also exports:
966
-
967
- ```tsx
968
- const {
969
- clips,
970
- currentClip,
971
- isPlaying,
972
- speed,
973
- play,
974
- pause,
975
- stop,
976
- setSpeed,
977
- handleAnimationsReady,
978
- } = useViewerAnimations();
979
-
980
- <ModelViewer
981
- modelUrl="/character.glb"
982
- licenseKey="your-license-key"
983
- objectBindings={objectBindings}
984
- onAnimationsReady={handleAnimationsReady}
985
- />;
986
- ```
987
-
988
- `useViewerAnimations` returns:
989
-
990
- - `clips`: an array of animation clip names found in the GLB/GLTF file
991
- - `clipDetails`: clip metadata including `sourceName` and `duration` in seconds
992
- - `currentClip`: the clip played most recently, while it's active, or `null`
993
- - `isPlaying`: `true` while any clip is playing (not paused, finished, or stopped)
994
- - `speed`: the playback speed of `currentClip` (default `1`)
995
- - `time`: the live playback position of `currentClip`, in seconds (updates while playing)
996
- - `duration`: the length of `currentClip`, in seconds
997
- - `active`: every active clip (playing, paused, or finished) with its `status`, `time`, `duration`, `speed`, and `reverse`, most recently played last
998
- - `play(clip, options?)`: starts a clip by name, replacing the clips already playing unless `options.alongside` is set; `options.fade` blends over that many seconds, and `options.reverse` plays it backwards. A paused clip resumes instead of restarting
999
- - `pause(clip?)`: pauses one clip, or every active clip, at its current time
1000
- - `stop(clip?)`: stops one clip, or every active clip, and resets the pose
1001
- - `setSpeed(speed, clip?)`: changes the playback speed (e.g. `0.5` for half speed, `2` for double) of one clip, or of every active clip and those played later without their own speed
1002
- - `seek(time, clip?)`: scrubs one clip, or every active clip, to an absolute time in seconds (works while playing or paused), clamped to the clip length
1003
- - `handleAnimationsReady`: a callback you can pass directly to `onAnimationsReady`
1004
-
1005
- Example with playback controls:
1006
-
1007
- ```tsx
1008
- const { clips, currentClip, isPlaying, play, pause, stop, setSpeed } =
1009
- useViewerAnimations();
1010
-
1011
- // Play/pause toggle
1012
- <button onClick={() => (isPlaying ? pause() : play(currentClip ?? clips[0]))}>
1013
- {isPlaying ? "Pause" : "Play"}
1014
- </button>
1015
-
1016
- // Stop
1017
- <button onClick={() => stop()}>Stop</button>
1018
-
1019
- // Speed slider
1020
- <input
1021
- type="range"
1022
- min="0.1"
1023
- max="3"
1024
- step="0.1"
1025
- defaultValue="1"
1026
- onChange={(e) => setSpeed(Number(e.target.value))}
1027
- />
1028
-
1029
- // Clip selector
1030
- {clips.map((name) => (
1031
- <button key={name} onClick={() => play(name)}>
1032
- {name} {currentClip === name && isPlaying ? "▶" : ""}
1033
- </button>
1034
- ))}
1035
- ```
1036
-
1037
- Several clips can play at once. `play` replaces the clips already playing
1038
- unless you pass `alongside: true`, and the other controls act on one clip when
1039
- you name it, or on every active clip when you don't:
1040
-
1041
- ```tsx
1042
- const animations = useViewerAnimations();
1043
-
1044
- // Open both doors together, then close the left one.
1045
- animations.play("DoorLeftOpen", { alongside: true });
1046
- animations.play("DoorRightOpen", { alongside: true });
1047
- animations.play("DoorLeftOpen", { alongside: true, reverse: true });
1048
-
1049
- // Blend from walking to running over half a second.
1050
- animations.play("Run", { fade: 0.5 });
1051
-
1052
- animations.pause("Wheels"); // one clip
1053
- animations.stop(); // every clip
1054
-
1055
- // What's active, and how far along each clip is
1056
- animations.active.map(({ clip, status, time }) => `${clip}: ${status} at ${time}s`);
1057
- ```
1058
-
1059
- A clip whose `loopMode` is `"once"` holds its last pose when it finishes
1060
- (`status: "finished"`) until it's stopped or played again, so an opened door
1061
- stays open. `"ping-pong"` plays forwards then backwards, repeatedly. Playing a
1062
- clip that's already playing in the same direction leaves it running; playing
1063
- it the other way turns it around where it is.
1064
-
1065
- ## `CompareViewer`
1066
-
1067
- Shows two configurations next to each other with one camera: a before and
1068
- after of a renovation, two paint options, the current part against its
1069
- replacement. Each side is a full [`ModelViewer`](#public-api),
1070
- so a side can differ in anything a viewer takes: `objectBindings`,
1071
- `sceneConfig`, material variants, even a different `modelUrl`.
1072
-
1073
- ```tsx
1074
- import "@liveroom-tech/react-immersive/styles.css";
1075
- import { CompareViewer } from "@liveroom-tech/react-immersive";
1076
-
1077
- <div style={{ height: 600 }}>
1078
- <CompareViewer
1079
- modelUrl="/kitchen.glb"
1080
- licenseKey="your-license-key"
1081
- before={{ label: "Current", objectBindings: currentBindings }}
1082
- after={{ label: "Proposed", objectBindings: proposedBindings }}
1083
- />
1084
- </div>;
1085
- ```
1086
-
1087
- ### Layouts
1088
-
1089
- - **`layout="slider"`** (default) lays the two views over each other. A divider
1090
- splits them: the left of it shows `before`, the right shows `after`. Drag the
1091
- divider, or focus it and use the arrow keys, Page Up/Down, Home, and End. It
1092
- is a slider to screen readers, with each side's label and share.
1093
- - **`layout="side-by-side"`** shows the two views next to each other, each half
1094
- the width.
1095
-
1096
- ```tsx
1097
- <CompareViewer
1098
- modelUrl="/car.glb"
1099
- licenseKey="your-license-key"
1100
- layout="side-by-side"
1101
- before={{ sceneConfig: { ...config, model: { ...config.model, activeVariants: ["Paint: Red"] } } }}
1102
- after={{ sceneConfig: { ...config, model: { ...config.model, activeVariants: ["Paint: Blue"] } } }}
1103
- />
1104
- ```
1105
-
1106
- ### One camera
1107
-
1108
- Orbiting, panning, or zooming either side moves both. The side you last
1109
- grabbed leads and the other copies it every frame, so a fly-in, a zoom to a
1110
- selected object, or a refit on one side shows on both. With two different
1111
- models, the views line up on the same point in space, so the models should
1112
- share a coordinate system.
1113
-
1114
- ### Props
1115
-
1116
- ```ts
1117
- type CompareViewerProps = Partial<ModelViewerProps> & {
1118
- licenseKey: string;
1119
- before: CompareViewerSide; // the left side
1120
- after: CompareViewerSide; // the right side
1121
- layout?: "slider" | "side-by-side"; // default "slider"
1122
- split?: number; // the divider, 0 (left edge) to 1; default 0.5
1123
- onSplitChange?: (split: number) => void;
1124
- };
1125
-
1126
- type CompareViewerSide = Partial<ModelViewerProps> & {
1127
- label?: string; // shown over the side; default "Before" / "After"
1128
- };
1129
- ```
1130
-
1131
- Every other prop is shared by both sides, and a side's own props win.
1132
- Callbacks run per side: give `before.onObjectSelect` and
1133
- `after.onObjectSelect` to tell the sides apart. Both sides start from the
1134
- `minimal` [preset](#presets-and-grouped-props),
1135
- with no panels or buttons, so their interfaces don't overlap; pass `preset`
1136
- or `ui` to change that. Pass `split` to control the divider yourself;
1137
- without it, `CompareViewer` keeps the position.
1138
-
1139
- > Each side has its own WebGL (or [WebGPU](#webgpu)) canvas. The two share one download and parse of the model file, but the GPU holds two copies of its geometry and textures.
1140
-
1141
- `poster`, `loading` and `reveal` work as on
1142
- [`ModelViewer`](#lazy-loading-and-posters). With `reveal="interaction"`, one
1143
- **View in 3D** button starts both sides.
1144
-
1145
- The divider and the default side labels use the
1146
- [`labels`](https://react-immersive.liveroom.dev/docs/reference/labels) keys `compareBefore`, `compareAfter`,
1147
- `compareDivider`, and `compareDividerValue`.
1148
-
1149
- ## Presets and grouped props
1150
-
1151
- `preset` sets `ModelViewer` up for a common kind of page:
1152
-
1153
- | Preset | For | What it sets |
1154
- | --- | --- | --- |
1155
- | `minimal` | Just the model | Hides both panels, the download and reset buttons, tour controls, and the material variant buttons |
1156
- | `showcase` | A product on display | Everything `minimal` does, plus auto-rotation |
1157
- | `configurator` | Customers changing parts | Shows the selected object's panel, the reset button, and the material variant buttons; hides the scene objects panel and download |
1158
- | `inspector` | Checking a model | Every panel, the view gizmo, the measure, UV checker, exploded-view, section-cut, and isolate tools, and keyboard navigation |
1159
-
1160
- `MODEL_VIEWER_PRESETS` lists exactly which props each preset sets.
1161
-
1162
- Related props are also available as groups, which keeps a configured viewer
1163
- readable:
1164
-
1165
- ```tsx
1166
- <ModelViewer
1167
- modelUrl="/car.glb"
1168
- licenseKey="your-license-key"
1169
- preset="configurator"
1170
- ui={{ download: "car.glb" }}
1171
- tools={{ measure: { unit: "cm" } }}
1172
- controls={{ autoRotate: 0.5, zoom: false, limits: { maxDistance: 20 } }}
1173
- perf={{ profile: "low" }}
1174
- xr={{ enabled: true, usdzUrl: "/car.usdz" }}
1175
- />
1176
- ```
1177
-
1178
- | Group | Fields, and the flat prop each one sets |
1179
- | --- | --- |
1180
- | `ui` | `objectPanel` (`showObjectBindingDataPanel`), `sceneObjects` (`showSceneObjectsPanel`), `download` (`showDownloadButton`; a string also sets `downloadFilename`), `reset` (`showResetButton`), `fullscreen` (`showFullscreenButton`), `loadingOverlay` (`showLoadingOverlay`), `viewGizmo` (`showViewGizmo`), `mouseController` (`showMouseController`; `{ position, opacity }` also sets `mouseControllerPosition` and `mouseControllerOpacity`), `annotationNavigation` (`showAnnotationNavigation`), `annotationOnHover` (`showAnnotationOnHover`), `highlightOnHover`, `materialVariants` (`showMaterialVariants`), `objectNavigator` (`showObjectNavigator`) |
1181
- | `tools` | `measure` (`showMeasureTools`; `{ unit }` also sets `measurementUnit`), `uvChecker` (`showUvCheckerButton`), `explode` (`showExplodeControls`), `section` (`showSectionTools`), `isolate` (`showIsolateControls`), `texturePositioning` (`texturePositioning`) |
1182
- | `controls` | `enabled` (`enableCameraControls`), `keyboard` (`enableKeyboardNavigation`), `zoom` (the opposite of `disableZoom`), `zoomOnSelect` (`zoomOnSelected`), `autoRotate` (a number also sets `autoRotateSpeed`), `refitOnResize`, `limits` (`cameraLimits`), `moveSensitivity`, `zoomSensitivity`, `reducedMotion` |
1183
- | `perf` | `renderMode`, `maxDpr`, `profile` (`performanceProfile`), `preserveDrawingBuffer`, `webgpu` |
1184
- | `tiles` | `url` (`tilesetUrl`), `errorTarget` (`tilesErrorTarget`), `cacheSize` (`tilesCacheSize`) |
1185
- | `xr` | `enabled` (`enableXR`), `usdzUrl`, `scaleMode` (`arScaleMode`), `mobileHandoffUrl` |
1186
- | `decoders` | `draco` (`dracoDecoderPath`), `ktx2` (`ktx2TranscoderPath`), `meshopt` |
1187
-
1188
- A preset only fills in defaults: a flat prop overrides it, and a grouped prop
1189
- overrides both. The flat props keep working exactly as before.
1190
-
1191
- ## Public API
1192
-
1193
- The source code currently defines `ModelViewer` with these props:
1194
-
1195
- ```ts
1196
- type Props = {
1197
- preset?: "minimal" | "showcase" | "configurator" | "inspector";
1198
- ui?: ModelViewerUiOptions;
1199
- tools?: ModelViewerToolsOptions;
1200
- controls?: ModelViewerControlsOptions;
1201
- perf?: ModelViewerPerfOptions;
1202
- tiles?: ModelViewerTilesOptions;
1203
- xr?: ModelViewerXrOptions;
1204
- decoders?: ModelViewerDecoderOptions;
1205
- modelUrl: string;
1206
- poster?: string;
1207
- loading?: "lazy" | "eager"; // default "lazy"
1208
- reveal?: "auto" | "interaction"; // default "auto"
1209
- tilesetUrl?: string | null;
1210
- tilesErrorTarget?: number;
1211
- tilesCacheSize?: number;
1212
- onObjectCatalog?: (objects: ViewerObjectInfo[]) => void;
1213
- modelFormat?: "glb" | "gltf" | "obj" | "fbx" | "usdz" | "stl" | "ply";
1214
- licenseKey: string;
1215
- objectBindings?: Record<string, ObjectBinding> | "auto"; // default "auto"
1216
- selectedObject?: ObjectBinding | null;
1217
- panelHoveredObject?: ObjectBinding | null;
1218
- onObjectBindingsChange?: (next: Record<string, ObjectBinding>) => void;
1219
- onObjectSelect?: (binding: ObjectBinding | null) => void;
1220
- onObjectHover?: (binding: ObjectBinding | null) => void;
1221
- onObjectDoubleClick?: (event: ObjectPointerEvent) => boolean | void;
1222
- onObjectContextMenu?: (event: ObjectPointerEvent) => void;
1223
- onModelLoaded?: (model: ViewerModelInfo) => void;
1224
- onLoadError?: (error: unknown) => void;
1225
- onProgress?: (event: ProgressEvent) => void;
1226
- cameraControllerType?: "orbit" | "pointerLock";
1227
- onCameraControllerChange?: (
1228
- controller: "orbit" | "pointerLock",
1229
- ) => void;
1230
- onAction?: (event: ObjectActionEvent) => void;
1231
- onEngagement?: (event: ViewerEngagementEvent) => void | Promise<void>;
1232
- debugEngagement?: boolean;
1233
- onHiddenObjectsChange?: (next: Record<string, boolean>) => void;
1234
- onCameraChange?: (state: ViewerCameraState) => void;
1235
- onViewerReady?: (viewer: ViewerReadyState) => void;
1236
- onTextureUpload?: (file: File, objectId: string) => Promise<string>;
1237
- texturePositioning?: boolean | TexturePositioningControls;
1238
- onAnimationsReady?: (controls: AnimationControls) => void;
1239
- onAnnotationsChange?: (annotations: AnnotationMarker[]) => void;
1240
- activeAnnotation?: AnnotationMarker | null;
1241
- onActiveAnnotationChange?: (annotation: AnnotationMarker | null) => void;
1242
- camera?: ViewerCameraConfig;
1243
- cameraLimits?: CameraLimits;
1244
- backgroundColor?: string;
1245
- shadows?: boolean;
1246
- showObjectBindingDataPanel?: boolean;
1247
- customObjectBindingDataPanel?: (
1248
- props: CustomObjectBindingDataPanelProps,
1249
- ) => React.ReactNode;
1250
- customSceneObjectsPanel?: (
1251
- props: CustomSceneObjectsPanelProps,
1252
- ) => React.ReactNode;
1253
- showSceneObjectsPanel?: boolean;
1254
- showDownloadButton?: boolean;
1255
- downloadFilename?: string;
1256
- showResetButton?: boolean;
1257
- showFullscreenButton?: boolean;
1258
- showLoadingOverlay?: boolean;
1259
- showMouseController?: boolean;
1260
- mouseControllerPosition?:
1261
- | "bottom-left"
1262
- | "bottom-right"
1263
- | "top-left"
1264
- | "top-right"
1265
- | "center"
1266
- | "center-bottom"
1267
- | "center-top";
1268
- mouseControllerOpacity?: number;
1269
- moveSensitivity?: number;
1270
- zoomSensitivity?: number;
1271
- sceneConfig?: SceneConfig;
1272
- disableZoom?: boolean;
1273
- zoomOnSelected?: boolean;
1274
- highlightOnHover?: boolean;
1275
- enableCameraControls?: boolean;
1276
- moveModeEnabled?: boolean;
1277
- objectTransformSpace?: "local" | "world";
1278
- enableKeyboardNavigation?: boolean;
1279
- showObjectNavigator?: boolean;
1280
- reducedMotion?: "user" | "always" | "never";
1281
- onAutoFit?: () => Promise<boolean>;
1282
- refitOnResize?: boolean;
1283
- renderMode?: "always" | "demand";
1284
- maxDpr?: number;
1285
- preserveDrawingBuffer?: boolean;
1286
- webgpu?: boolean;
1287
- performanceProfile?: "auto" | "high" | "low";
1288
- dracoDecoderPath?: string | false;
1289
- ktx2TranscoderPath?: string | false;
1290
- meshopt?: boolean;
1291
- showMeasureTools?: boolean;
1292
- showUvCheckerButton?: boolean;
1293
- showExplodeControls?: boolean;
1294
- showSectionTools?: boolean;
1295
- isolation?: ViewerIsolation | null;
1296
- onIsolationChange?: (isolation: ViewerIsolation | null) => void;
1297
- showIsolateControls?: boolean;
1298
- showMaterialVariants?: boolean;
1299
- cinematic?: boolean | CinematicConfig;
1300
- measurementUnit?: string;
1301
- enableXR?: boolean;
1302
- usdzUrl?: string;
1303
- arScaleMode?: "normalized" | "real-world";
1304
- mobileHandoffUrl?: string;
1305
- showAnnotationNavigation?: boolean;
1306
- showAnnotationOnHover?: boolean;
1307
- onSceneConfigChange?: (config: SceneConfig) => void;
1308
- showViewGizmo?: boolean;
1309
- overlay?: React.ReactNode;
1310
- unsafe_sceneChildren?: React.ReactNode;
1311
- autoRotate?: boolean;
1312
- autoRotateSpeed?: number;
1313
- };
1314
- ```
1315
-
1316
- ### `modelUrl`
1317
-
1318
- URL or public path to a `.glb`, `.gltf`, `.obj`, `.fbx`, `.usdz`, `.stl`, or `.ply` 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.
1319
-
1320
- Example:
1321
-
1322
- ```tsx
1323
- modelUrl = "/model.glb";
1324
- ```
1325
-
1326
- ### `modelFormat`
1327
-
1328
- Optional explicit format for signed or extensionless model URLs. Normal URLs
1329
- ending in `.glb`, `.gltf`, `.obj`, `.fbx`, `.usdz`, `.stl`, or `.ply` are detected automatically.
1330
-
1331
- ### STL and PLY
1332
-
1333
- STL and PLY files hold geometry only: no object names, materials or
1334
- animations.
1335
-
1336
- - **One object per file**, named after the file: `bracket.stl` loads as one
1337
- object keyed `bracket`, which its bindings use. A text STL with several named
1338
- solids loads one object per solid. A `.zip` holding one STL or PLY file also
1339
- works.
1340
- - **Colour:** objects start a matte grey. A PLY file's vertex colours show as
1341
- they are, and picking a colour (or texture) replaces them.
1342
- - **No texture coordinates:** STL files have none and PLY files rarely do, so an
1343
- image can't be mapped onto them. The viewer's **Change Material** says so
1344
- instead of offering an upload, BindingBuilder turns off its image map fields
1345
- for that object, and new bindings leave Change Material out. [Decals](#decals)
1346
- work without texture coordinates. This applies to any object without them, in
1347
- any format.
1348
- - **Units and orientation:** STL files are usually in millimetres with Z up, so a
1349
- part can load lying on its back. Turn it upright with
1350
- [`sceneConfig.model.rotation`](#sceneconfig) (BindingBuilder: **Scene** →
1351
- model rotation, −90° on X), and set [`measurementUnit`](#measurementunit) to
1352
- `"mm"`. In AR, the default `"normalized"` [`arScaleMode`](#arscalemode) fits the
1353
- model to a metre; `"real-world"` would read millimetres as metres.
1354
- - **PLY point clouds** (points with no faces) can't be shown; the viewer
1355
- explains why instead of loading them.
1356
-
1357
- ### 3D Tiles streaming
1358
-
1359
- Use `tilesetUrl` to stream a processed 3D Tiles model instead of downloading
1360
- `modelUrl` as one monolithic asset. `modelUrl` remains required by the component
1361
- API, but `tilesetUrl` is the render source while streaming is enabled.
1362
-
1363
- ```tsx
1364
- <ModelViewer
1365
- modelUrl="/models/campus.glb"
1366
- tilesetUrl="/models/campus/tileset.json"
1367
- tilesErrorTarget={8}
1368
- tilesCacheSize={800}
1369
- onObjectCatalog={(objects) => console.log(objects)}
1370
- {...props}
1371
- />
1372
- ```
1373
-
1374
- - `tilesErrorTarget` controls the screen-space error target. Lower values load
1375
- sharper tiles and use more bandwidth/GPU memory. Default: `8`.
1376
- - `tilesCacheSize` is the maximum number of parsed tiles kept in the client
1377
- cache. Default: `800`.
1378
- - `onObjectCatalog` receives the bindable-object catalog from `objects.json`
1379
- before all referenced tiles have loaded. See [`onObjectCatalog`](#onobjectcatalog).
1380
-
1381
- Bindings, selection, hover, material overrides, transforms, exploded view, and
1382
- scene renderer modes use the same public API in streamed and monolithic modes.
1383
- The tileset and its tile payloads must be browser-accessible, and the optional
1384
- object catalog must sit beside `tileset.json` as `objects.json`.
1385
-
1386
- ### `licenseKey`
1387
-
1388
- License key for the library. Required.
1389
-
1390
- ### `objectBindings`
1391
-
1392
- A map keyed by the model node name, or `"auto"`, the default (see
1393
- [Generating bindings automatically](#generating-bindings-automatically)). Each
1394
- key should match a mesh name from the GLB/GLTF file.
1395
-
1396
- `objectBindings` is the single source of truth for all visual state. Changes to the binding record drive rendering:
1397
-
1398
- - `visible` controls mesh visibility
1399
- - `style.material.baseColor` sets the mesh color (committed by the built-in color picker on close)
1400
- - `style.material.texture.path` sets the mesh texture (committed by the built-in texture upload; uses `onTextureUpload` for a durable URL when provided, otherwise a session-scoped blob URL)
1401
- - `style.material.*` applies per-object `MeshPhysicalMaterial` overrides such as emissive, metalness, roughness, opacity, clearcoat, sheen, anisotropy, specular, transmission, thickness, reflectivity, and sidedness
1402
- - `cameraState.position` and `cameraState.target` define the camera view to use when that object is focused or selected from the viewer UI
1403
-
1404
- Example:
1405
-
1406
- ```ts
1407
- import { defineObjectBindings } from "@liveroom-tech/react-immersive/utils";
1408
-
1409
- const objectBindings = defineObjectBindings({
1410
- Object_2: {
1411
- id: "obj-2",
1412
- modelObjectId: "Object_2",
1413
- type: "body",
1414
- visible: true,
1415
- cameraState: {
1416
- position: [2.8, 1.6, 4.2],
1417
- target: [0, 0.8, 0],
1418
- },
1419
- style: {
1420
- material: {
1421
- baseColor: "#ff0000",
1422
- texture: {
1423
- path: "/materials/body-finish.jpg",
1424
- },
1425
- },
1426
- },
1427
- actions: [
1428
- { id: "change-color", label: "Change Color", type: "command" },
1429
- { id: "change-material", label: "Change Material", type: "command" },
1430
- { id: "toggle-visibility", label: "Toggle Visibility", type: "command" },
1431
- ],
1432
- metrics: {},
1433
- metadata: {},
1434
- },
1435
- });
1436
- ```
1437
-
1438
- `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.
1439
-
1440
- If a binding's `modelObjectId` (or its key) doesn't match any node in the
1441
- loaded model, that binding can never be selected. `ModelViewer` then shows a
1442
- dismissible banner with the number of mismatched bindings, and logs a console
1443
- warning naming them. Log [`onObjectCatalog`](#onobjectcatalog) to see the
1444
- model's node names.
1445
-
1446
- #### Generating bindings automatically
1447
-
1448
- `objectBindings` defaults to `"auto"`: leave it out and `ModelViewer` generates
1449
- a binding for every mesh when the model loads, so you don't need to know the
1450
- node names up front:
1451
-
1452
- ```tsx
1453
- <ModelViewer modelUrl="/model.glb" licenseKey="your-license-key" />
1454
- ```
1455
-
1456
- Each generated binding is keyed by the node name, with a readable `label`
1457
- (`"Wheel_FL"` becomes `"Wheel FL"`), an inferred `type`, a unique `id`, and
1458
- built-in actions: `change-color`, `change-material`, and `toggle-visibility`,
1459
- or `toggle-light`, `change-light-color`, and `set-brightness` for meshes whose
1460
- names contain "light". For a streamed model (`tilesetUrl`), the bindings come
1461
- from the object catalog, so objects are bindable before their tiles load.
1462
-
1463
- The generated bindings are passed to `onObjectBindingsChange` once they exist,
1464
- and again after every edit. To keep them, and to refine them later, store them
1465
- and pass them back:
1466
-
1467
- ```tsx
1468
- const [objectBindings, setObjectBindings] = useState<
1469
- Record<string, ObjectBinding> | "auto"
1470
- >("auto");
1471
-
1472
- <ModelViewer
1473
- modelUrl={modelUrl}
1474
- licenseKey="your-license-key"
1475
- objectBindings={objectBindings}
1476
- onObjectBindingsChange={setObjectBindings}
1477
- />;
1478
- ```
1479
-
1480
- After the first change the viewer is controlled by your state as usual. Set the
1481
- state back to `"auto"` when you switch to a different model. While the prop is
1482
- `"auto"`, bindings the same model already had are kept when it reloads, so
1483
- runtime edits survive; a different `modelUrl` starts from fresh bindings.
1484
-
1485
- The same defaults are available as functions from both entry points (they are
1486
- also what `BindingBuilder` generates):
1487
-
1488
- ```ts
1489
- import {
1490
- buildBindingsFromNodes,
1491
- createDefaultBinding,
1492
- inferType,
1493
- } from "@liveroom-tech/react-immersive/utils";
1494
-
1495
- const bindings = buildBindingsFromNodes(["CarBody", "Wheel_FL"]);
1496
- const door = { ...createDefaultBinding("FrontDoor"), label: "Front door" };
1497
- inferType("Wheel_FL"); // "wheel"
1498
- ```
1499
-
1500
- Every generated action is one `ModelViewer` performs itself, so every
1501
- generated button works without extra setup.
1502
-
1503
- ### `selectedObject`
1504
-
1505
- Optional currently selected object binding, or `null` when nothing is selected.
1506
-
1507
- If provided, `ModelViewer` behaves as a controlled component.
1508
- If omitted, `ModelViewer` manages its own selection state internally.
1509
-
1510
- 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`.
1511
-
1512
- ### `panelHoveredObject`
1513
-
1514
- Optional externally controlled binding to highlight as hovered by a custom
1515
- scene-objects panel. It takes precedence over the built-in panel and canvas
1516
- hover while `highlightOnHover` is enabled. Pass `null` when no panel row is
1517
- hovered.
1518
-
1519
- ### `onObjectSelect`
1520
-
1521
- Optional callback called when the user clicks a mesh or closes the side panel.
1522
-
1523
- Behavior:
1524
-
1525
- - clicking a mesh calls `onObjectSelect(binding)`
1526
- - closing the panel calls `onObjectSelect(null)`
1527
- - clicking the same mesh again still selects it; selection is not toggled off automatically
1528
- - if the binding includes `cameraState.position` and `cameraState.target`, the viewer moves the camera to that saved view when focusing that object
1529
-
1530
- ### `onObjectHover`
1531
-
1532
- Optional callback called when the user hovers a mesh in the model.
1533
-
1534
- Behavior:
1535
-
1536
- - pointer over a mesh calls `onObjectHover(binding)`
1537
- - pointer out calls `onObjectHover(null)`
1538
-
1539
- ### `onObjectDoubleClick`
1540
-
1541
- Optional callback called when the user double-clicks an object.
1542
-
1543
- ```ts
1544
- onObjectDoubleClick?: (event: ObjectPointerEvent) => boolean | void
1545
-
1546
- type ObjectPointerEvent = {
1547
- binding: ObjectBinding;
1548
- clientX: number; // viewport coordinates
1549
- clientY: number;
1550
- pointerType: string; // "mouse" | "pen" | "touch"
1551
- surface?: { position: Vec3; normal: Vec3 }; // where on the object, in its own space
1552
- };
1553
- ```
1554
-
1555
- Return `true` to say you handled it: the viewer then skips its own double-click
1556
- behavior, which moves the camera's orbit point to that spot (or starts texture
1557
- positioning, when `texturePositioning` is on). Return nothing to keep it.
1558
-
1559
- ### `onObjectContextMenu`
1560
-
1561
- Optional callback called when the user right-clicks an object, or long-presses
1562
- it on a touch screen. Use it to open your own menu:
1563
-
1564
- ```tsx
1565
- <ModelViewer
1566
- {...props}
1567
- onObjectContextMenu={({ binding, clientX, clientY }) =>
1568
- openMenu({ for: binding.id, x: clientX, y: clientY })
1569
- }
1570
- />
1571
- ```
1572
-
1573
- It receives the same `ObjectPointerEvent` as `onObjectDoubleClick`, with the
1574
- position where the gesture started, ready for a `position: fixed` menu. A
1575
- right-click fires when the button is released without the pointer moving, so a
1576
- right-drag still pans the camera. A long-press fires after half a second, and
1577
- not if the finger moves or a second finger touches down (a pinch); lifting the
1578
- finger afterwards doesn't select the object. Objects with `selectable: false`
1579
- don't fire it.
1580
-
1581
- ### `onHiddenObjectsChange`
1582
-
1583
- Optional callback fired with the derived hidden-object map whenever the current binding data changes hidden visibility state.
1584
-
1585
- ### `onObjectBindingsChange`
1586
-
1587
- Optional callback fired when the viewer's built-in UI updates binding data.
1588
-
1589
- This fires for:
1590
-
1591
- - visibility toggles (`binding.visible`)
1592
- - color picks (`binding.style.material.baseColor`), committed when the color picker closes
1593
- - texture uploads (`binding.style.material.texture.path`), committed immediately (via `onTextureUpload` when provided, otherwise as a blob URL)
1594
- - texture removal (`binding.style.material.texture` cleared)
1595
- - object movement and rotation (`binding.transform.position` and `binding.transform.rotation`)
1596
-
1597
- 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.
1598
-
1599
- ### `onObjectCatalog`
1600
-
1601
- Optional callback listing every bindable object in the model: each named mesh,
1602
- under the identifier `objectBindings` is keyed by. It's the quickest way to find
1603
- out what your meshes are called:
1604
-
1605
- ```tsx
1606
- <ModelViewer
1607
- modelUrl="/model.glb"
1608
- licenseKey="your-license-key"
1609
- onObjectCatalog={(objects) => console.table(objects)}
1610
- />
1611
- ```
1612
-
1613
- It fires each time a model loads. For a streamed model (`tilesetUrl`) the list
1614
- comes from `objects.json` beside `tileset.json`, before the tiles containing
1615
- those objects have necessarily loaded. Each entry is a `ViewerObjectInfo`:
1616
-
1617
- ```ts
1618
- type ViewerObjectInfo = {
1619
- name: string; // the key to use in objectBindings
1620
- triangles: number;
1621
- minimum: [number, number, number];
1622
- maximum: [number, number, number];
1623
- };
1624
- ```
1625
-
1626
- `minimum` and `maximum` are the object's bounds in the model's own units, in
1627
- Z-up coordinates as in 3D Tiles: a point at glTF `(x, y, z)` is `(x, -z, y)`
1628
- here. They're the same for loaded and streamed models, and don't change when the
1629
- viewer moves or scales the model. To keep the list in state, use
1630
- `useViewerModel` and pass its `handleObjectCatalog`.
1631
-
1632
- ### `onModelLoaded`
1633
-
1634
- Optional callback fired after the model has loaded, with its bounds and mesh
1635
- count:
1636
-
1637
- ```ts
1638
- (model: ViewerModelInfo) => void
1639
-
1640
- type ViewerModelInfo = {
1641
- bounds: { min: Vec3; max: Vec3; center: Vec3; size: Vec3 }; // world space
1642
- objectCount: number; // meshes in the loaded scene
1643
- materialVariants: string[]; // glTF material variant names, if any
1644
- materialVariantGroups: MaterialVariantGroup[]; // the same, as option groups
1645
- };
1646
- ```
1647
-
1648
- For the model's object names, use [`onObjectCatalog`](#onobjectcatalog).
1649
-
1650
- ### `onLoadError`
1651
-
1652
- Optional callback fired if the model fails to load or render.
1653
-
1654
- Current callback shape:
1655
-
1656
- ```ts
1657
- (error: unknown) => void
1658
- ```
1659
-
1660
- Whether or not you pass it, `ModelViewer` shows a message in place of the
1661
- model that explains the likely cause: a missing file (404), refused access
1662
- (401/403), a blocked cross-origin request (CORS), a web page served instead of
1663
- a model, or a file that isn't the expected format. It includes the URL and the
1664
- underlying error. Set `showLoadingOverlay={false}` to show your own instead.
1665
-
1666
- ### `onProgress`
1667
-
1668
- Called with the loader's `ProgressEvent` while a monolithic model is being
1669
- downloaded. When `event.lengthComputable` is true, progress is
1670
- `event.loaded / event.total`; the built-in loading overlay uses the same value.
1671
-
1672
- ```ts
1673
- onProgress?: (event: ProgressEvent) => void
1674
- ```
1675
-
1676
- ### `onAction`
1677
-
1678
- Optional callback fired when a built-in action button is clicked from the side panel.
1679
-
1680
- Current event shape:
1681
-
1682
- ```ts
1683
- type ObjectActionEvent = {
1684
- objectId: string;
1685
- action: ObjectBindingAction;
1686
- binding?: ObjectBinding;
1687
- screenX?: number;
1688
- screenY?: number;
1689
- };
1690
- ```
1691
-
1692
- ### `onEngagement` / `debugEngagement`
1693
-
1694
- Optional interaction analytics. Set `onEngagement` to receive typed
1695
- `ViewerEngagementEvent` objects, or set `debugEngagement` to log them in the
1696
- browser's developer console with the `[react-immersive engagement]` prefix.
1697
- Both are disabled by default. The viewer does not send telemetry or persist
1698
- analytics identifiers; your callback decides where events go.
1699
-
1700
- ```tsx
1701
- import { ModelViewer, type ViewerEngagementEvent } from "@liveroom-tech/react-immersive";
1702
-
1703
- function handleEngagement(event: ViewerEngagementEvent) {
1704
- if (event.type === "part-viewed") {
1705
- console.info("Part viewed:", event.objectKey, event.source);
1706
- }
1707
- if (event.type === "ar-ended") {
1708
- console.info("Time in AR:", event.durationMs);
1709
- }
1710
- // Connect this callback to your application's analytics integration.
1711
- }
1712
-
1713
- <ModelViewer
1714
- modelUrl="/car.glb"
1715
- licenseKey={licenseKey}
1716
- onEngagement={handleEngagement}
1717
- debugEngagement
1718
- />
1719
- ```
1720
-
1721
- Every event includes `sessionId` (an ephemeral ID for this viewer/model),
1722
- `timestamp` (ISO format), `elapsedMs`, `modelUrl`, and `tilesetUrl` (or `null`).
1723
- Durations use a monotonic clock and are expressed in milliseconds. Session
1724
- elapsed time includes loading and background time; it is not an attention metric.
1725
-
1726
- | Event `type` | Details and trigger |
1727
- | --- | --- |
1728
- | `part-viewed` | `objectId`, `objectKey`, optional `label`, and `source` (`"canvas"`, `"panel"`, or `"keyboard"`). Selecting/focusing a different part; reselecting the current part is ignored. |
1729
- | `variant-chosen` | `variant`, `previousVariant` (names, or `null` for the default), and `group` (the option group's name, or `null`). Choosing or clearing a built-in material variant; `previousVariant` is the earlier choice in the same group. |
1730
- | `ar-started` | A confirmed immersive WebXR AR session begins. |
1731
- | `ar-ended` | `durationMs` and `reason` (`"session-ended"` or `"interrupted"`). A confirmed AR session ends, or tracking stops on model change, unmount, or context loss. |
1732
- | `ar-launch-requested` | `channel` (`"quick-look"` or `"mobile-handoff"`). Opening external AR or its QR handoff; this does not claim an AR session started. |
1733
- | `tour-started` | `annotationCount`. Starting the guided tour. |
1734
- | `tour-completed` | `annotationCount` and `durationMs`. Advancing past its final annotation; emitted once per completed tour. |
1735
- | `tour-stopped` | `annotationCount`, `durationMs`, and `reason` (`"stopped"`, `"restarted"`, `"annotations-changed"`, or `"interrupted"`). Leaving a tour before completion. |
1736
-
1737
- Hover, camera motion, renders, and externally supplied selection/variant updates
1738
- do not create engagement events. WebXR duration measures the session interval;
1739
- Quick Look runs outside the page and has no reliable duration callback. Turning
1740
- tracking on during an existing AR session measures from attachment. Callback
1741
- changes do not restart timing, and synchronous or asynchronous callback failures are logged without
1742
- interrupting the viewer. A new model or re-enabling tracking creates a new
1743
- analytics session; WebGL recovery keeps the session ID; context loss ends active intervals
1744
- as interrupted.
1745
-
1746
- ### `onViewerReady`
1747
-
1748
- Optional callback fired once camera controls are available and again when the
1749
- viewer publishes updated ready-state data (for example after model load,
1750
- camera changes, or binding changes).
1751
-
1752
- Current callback shape:
1753
-
1754
- ```ts
1755
- (viewer: ViewerReadyState) => void
1756
- ```
1757
-
1758
- `ViewerReadyState` includes `captureImage`, an imperative method for grabbing a PNG snapshot of the canvas:
1759
-
1760
- ```ts
1761
- type ViewerReadyState = {
1762
- objectBindings: Record<string, ObjectBinding>;
1763
- homeCameraState: { position: Vec3; target: Vec3 } | null;
1764
- captureImage: (options?: CaptureImageOptions) => Promise<string>;
1765
- captureVideo: (options?: CaptureVideoOptions) => Promise<Blob>;
1766
- invalidate: () => void;
1767
- rendererBackend: "webgl" | "webgpu"; // which renderer draws; see `webgpu`
1768
- raw: {
1769
- controls: CameraControls; // camera-controls
1770
- scene: Object3D | null; // three.js
1771
- nodes: Record<string, Object3D>; // model nodes by binding key and name
1772
- };
1773
- };
1774
-
1775
- type CaptureImageOptions = {
1776
- width?: number;
1777
- height?: number;
1778
- transparent?: boolean;
1779
- };
1780
- ```
1781
-
1782
- `raw` holds the live three.js scene, the camera-controls instance, and the
1783
- model's nodes, for integrations the rest of the API doesn't cover. You don't
1784
- need it for anything the hooks and props already do, and its shapes are those
1785
- libraries' own, so they can change when those libraries do.
1786
-
1787
- `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.
1788
-
1789
- ```tsx
1790
- <ModelViewer
1791
- preserveDrawingBuffer
1792
- onViewerReady={(viewer) => {
1793
- viewer
1794
- .captureImage({ width: 1920, height: 1080, transparent: true })
1795
- .then((dataUrl) => {
1796
- // upload, preview, etc.
1797
- });
1798
- }}
1799
- />;
1800
- ```
1801
-
1802
- 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.
1803
-
1804
- If camera, effects, screenshots, or custom setup all need the ready viewer, combine them with `useViewerConnection`:
1805
-
1806
- ```tsx
1807
- const camera = useViewerCamera();
1808
- const effects = useViewerEffects();
1809
- const connection = useViewerConnection(camera, effects);
1810
-
1811
- <ModelViewer
1812
- preserveDrawingBuffer
1813
- onViewerReady={connection.handleViewerReady}
1814
- />;
1815
-
1816
- const dataUrl = await connection.captureImage({ width: 1920, height: 1080 });
1817
- ```
1818
-
1819
- `captureVideo` records one complete cinematic pass or turntable revolution and
1820
- resolves with a video `Blob`. It is also available on `useViewerConnection` and
1821
- `useViewer().connection`. Call it from a button handler after the model has
1822
- finished loading and framing; ready-state callbacks run repeatedly as the camera
1823
- moves and should not start recordings automatically.
1824
-
1825
- ```tsx
1826
- const viewer = useViewer();
1827
- const abort = new AbortController();
1828
-
1829
- <ModelViewer {...viewer.props} cinematic showDownloadButton />;
1830
-
1831
- // Run in an event handler. Upload the Blob or preview it with an object URL.
1832
- const clip = await viewer.connection.captureVideo({
1833
- mode: "turntable", // or "cinematic" to use the enabled camera path
1834
- format: "auto", // prefers WebM; falls back to MP4. "gif" for an animated GIF
1835
- duration: 12, // seconds; defaults to path timing or a 24-second orbit
1836
- fps: 30, // 1–60; actual throughput depends on rendering/encoding
1837
- videoBitsPerSecond: 8_000_000,
1838
- signal: abort.signal,
1839
- });
1840
- // abort.abort() cancels an in-progress export and discards its data.
1841
- ```
1842
-
1843
- The download menu offers **Record turntable video** and, when cinematic is
1844
- enabled, **Record cinematic video**, plus Auto/WebM/MP4/GIF format selection. A
1845
- recording shows progress and a Cancel control. The filename and Blob MIME type
1846
- match the encoded format; an explicitly requested unsupported format rejects
1847
- instead of silently changing formats. With `loop: true`, a cinematic export
1848
- includes the return segment and stops after one cycle.
1849
-
1850
- Recording runs in real time at the canvas's current pixel resolution, includes
1851
- post-processing, and hides editor gizmos. It pauses cinematic playback, blocks
1852
- viewer gestures, then restores the previous camera pose and controls on success,
1853
- cancellation, or failure. Keep the tab visible until recording completes.
1854
- `preserveDrawingBuffer` is unnecessary for video. No audio, HTML overlays, or
1855
- format transcoding is included. The browser must support
1856
- `canvas.captureStream`, and `MediaRecorder` for WebM/MP4; exports reject before framing,
1857
- during XR, during another recording, or for durations outside (0, 300] seconds.
1858
-
1859
- **Animated GIF.** Pass `format: "gif"` (or pick **GIF** in the menu's format
1860
- select) for a GIF that loops forever, for places that don't play video, such as
1861
- email, chat, and some marketplaces:
1862
-
1863
- ```ts
1864
- const gif = await connection.captureVideo({
1865
- mode: "turntable",
1866
- format: "gif",
1867
- gifWidth: 480, // pixels; the height follows the canvas. Default 480
1868
- // fps defaults to 15, and a turntable to an 8-second turn
1869
- });
1870
- ```
1871
-
1872
- A GIF holds 256 colours per frame and stores every frame whole, so it is much
1873
- larger than a video of the same length and shows some banding on smooth
1874
- gradients. Frames are taken from the same stream as a video and encoded as
1875
- they're captured, so a long recording doesn't build up memory. Each frame lasts
1876
- as long as it really showed, so a busy device drops frames rather than playing
1877
- the GIF fast. `videoBitsPerSecond` doesn't apply to GIFs. The encoder
1878
- ([gifenc](https://github.com/mattdesl/gifenc)) loads only when a GIF is made.
1879
-
1880
- ### `onTextureUpload`
1881
-
1882
- 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.
1883
-
1884
- Current callback shape:
1885
-
1886
- ```ts
1887
- (file: File, objectId: string) => Promise<string>;
1888
- ```
1889
-
1890
- The returned string is stored in `binding.style.material.texture.path` via `onObjectBindingsChange`.
1891
-
1892
- 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.
1893
-
1894
- Example:
1895
-
1896
- ```tsx
1897
- <ModelViewer
1898
- modelUrl="/model.glb"
1899
- licenseKey="your-license-key"
1900
- objectBindings={objectBindings}
1901
- onObjectBindingsChange={setObjectBindings}
1902
- onTextureUpload={async (file, objectId) => {
1903
- const formData = new FormData();
1904
- formData.append("file", file);
1905
- formData.append("objectId", objectId);
1906
- const res = await fetch("/api/upload-texture", {
1907
- method: "POST",
1908
- body: formData,
1909
- });
1910
- const { url } = await res.json();
1911
- return url; // e.g. "https://cdn.example.com/textures/abc123.png"
1912
- }}
1913
- />
1914
- ```
1915
-
1916
- ### `texturePositioning`
1917
-
1918
- Turns on texture positioning for objects with an uploaded texture. Off by
1919
- default. When on, the **Change Material** panel gains Offset X/Y and Rotation
1920
- controls, and double-clicking the object puts the viewer in Move/Zoom mode:
1921
- drag to move the texture, scroll to scale it, and click the canvas or
1922
- **Done positioning** to finish.
1923
-
1924
- ```tsx
1925
- <ModelViewer modelUrl="/sofa.glb" licenseKey="your-license-key" texturePositioning />
1926
- ```
1927
-
1928
- Changes are written to the binding's `style.material.texture` (`offset`,
1929
- `rotation` in radians, and `scale`), so they reach `onObjectBindingsChange`
1930
- and your saved bindings like any other edit.
1931
-
1932
- To handle some of it yourself, pass an object instead of `true`. Any field you
1933
- leave out keeps the viewer's own behavior:
1934
-
1935
- ```ts
1936
- type TexturePositioningControls = {
1937
- offset?: [number, number]; // shown in the Offset fields instead of the binding's
1938
- rotationDegrees?: number; // shown in the Rotation field, 0–360
1939
- onOffsetChange?: (objectId: string, axisIndex: 0 | 1, value: number) => void;
1940
- onRotationChange?: (objectId: string, degrees: number) => void;
1941
- isPositioning?: boolean; // shows "Done positioning" instead of the hint
1942
- onEndPositioning?: () => void; // after Done positioning ends the mode
1943
- };
1944
- ```
1945
-
1946
- `onOffsetChange` and `onRotationChange` replace the viewer's writes from the
1947
- panel fields; dragging and scrolling on the canvas still write to the binding.
1948
-
1949
- ---
1950
-
1951
- ### `onAnimationsReady`
1952
-
1953
- 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.
1954
-
1955
- Current callback shape:
1956
-
1957
- ```ts
1958
- (controls: AnimationControls) => void
1959
- ```
1960
-
1961
- Where `AnimationControls` is:
1962
-
1963
- ```ts
1964
- type AnimationPlayOptions = {
1965
- alongside?: boolean; // keep the clips already playing (default false: replace them)
1966
- fade?: number; // seconds to fade in, and to fade out the clips it replaces
1967
- reverse?: boolean; // play backwards, from where the clip is or from its end
1968
- };
1969
-
1970
- type AnimationClipState = {
1971
- clip: string;
1972
- status: "playing" | "paused" | "finished"; // finished: a play-once clip holding its last pose
1973
- time: number; // seconds
1974
- duration: number; // seconds
1975
- speed: number;
1976
- reverse: boolean;
1977
- };
1978
-
1979
- type AnimationPlaybackState = {
1980
- currentClip: string | null; // the clip played most recently, while it's active
1981
- isPlaying: boolean; // whether any clip is playing
1982
- speed: number; // speed, position and length of currentClip
1983
- time: number;
1984
- duration: number;
1985
- active: AnimationClipState[]; // every active clip, most recently played last
1986
- };
1987
-
1988
- // Leave out clipName to act on every active clip.
1989
- type AnimationControls = {
1990
- clips: string[];
1991
- clipDetails?: { sourceName: string; duration: number }[];
1992
- play: (clipName: string, options?: AnimationPlayOptions) => void;
1993
- pause: (clipName?: string) => void;
1994
- stop: (clipName?: string) => void; // stops and resets the pose
1995
- setSpeed: (speed: number, clipName?: string) => void;
1996
- seek?: (time: number, clipName?: string) => void; // absolute time, in seconds
1997
- getState?: () => AnimationPlaybackState;
1998
- subscribe?: (listener: (state: AnimationPlaybackState) => void) => () => void;
1999
- };
2000
- ```
2001
-
2002
- Wire this to `useViewerAnimations().handleAnimationsReady` for the simplest integration.
2003
- When `sceneConfig.animations.autoplayClip` is set, `ModelViewer` will auto-play
2004
- that clip on load and respect per-clip `loopMode` (`"repeat"`, `"once"`, or
2005
- `"ping-pong"`), `speed`, `displayName`, and soft-delete (`hidden`) settings.
2006
-
2007
- ### Lighting
2008
-
2009
- Lighting lives in `sceneConfig.lighting`: an ambient light plus a list of
2010
- directional, point, spot, and hemisphere lights. Without a `sceneConfig`,
2011
- `ModelViewer` uses its default rig (ambient, a hemisphere fill, and a
2012
- shadow-casting key light). To use your own lights, patch the default scene
2013
- config. `sceneLight` fills in every setting you leave out:
2014
-
2015
- ```tsx
2016
- import {
2017
- DEFAULT_SCENE_CONFIG,
2018
- patchSceneConfig,
2019
- sceneLight,
2020
- } from "@liveroom-tech/react-immersive/utils";
2021
-
2022
- const sceneConfig = patchSceneConfig(DEFAULT_SCENE_CONFIG, {
2023
- lighting: {
2024
- ambient: { intensity: 0.5 },
2025
- lights: [
2026
- sceneLight({ id: "key", type: "directional", position: [4, 8, 4], intensity: 1.6, castShadow: true }),
2027
- sceneLight({ id: "fill", type: "point", position: [-3, 3, 2], intensity: 0.8 }),
2028
- ],
2029
- },
2030
- });
2031
-
2032
- <ModelViewer
2033
- modelUrl="/model.glb"
2034
- licenseKey="your-license-key"
2035
- sceneConfig={sceneConfig}
2036
- />;
2037
- ```
2038
-
2039
- To change lights while the app runs, use the lights controller from
2040
- `useViewer` (or `useSceneConfig`):
2041
-
2042
- ```tsx
2043
- const viewer = useViewer({ sceneConfig });
2044
-
2045
- viewer.scene.lights.setIntensity("key", 2.2);
2046
- viewer.scene.lights.toggle("fill");
2047
- viewer.scene.lights.add({ id: "bulb", type: "point", color: "#ffd27d", intensity: 4 });
2048
- viewer.scene.lights.attach("bulb", { objectId: "Lamp", offset: [0, 1.4, 0] });
2049
- ```
2050
-
2051
- An attached light follows the object, offset from the object's origin in its
2052
- local space (the origin is often at its base, not its center). Lighting set up
2053
- in `BindingBuilder`'s Scene tab exports as the same `sceneConfig`.
2054
-
2055
- ### Style rules
2056
-
2057
- `sceneConfig.styleRules` restyles objects from their own data while the app
2058
- runs: a colour for a temperature, grey for a disabled part, a pulsing glow for
2059
- an alarm. Each rule picks objects (`target`), checks their data (`when`), and
2060
- sets a look (`material`, `colorScale`, `pulse`):
2061
-
2062
- ```tsx
2063
- const viewer = useViewer({
2064
- bindings,
2065
- sceneConfig: {
2066
- ...DEFAULT_SCENE_CONFIG,
2067
- styleRules: [
2068
- {
2069
- id: "temperature",
2070
- target: { group: "pumps" },
2071
- colorScale: {
2072
- metric: "temperature",
2073
- stops: [[18, "#3b82f6"], [24, "#22c55e"], [32, "#ef4444"]],
2074
- },
2075
- },
2076
- {
2077
- id: "offline",
2078
- when: { status: "disabled" },
2079
- material: { baseColor: "#9ca3af", opacity: 0.4 },
2080
- },
2081
- {
2082
- id: "alarm",
2083
- when: [{ metric: "temperature", above: 32 }, { metadata: "alarm.acknowledged", equals: false }],
2084
- pulse: { color: "#ff3b30", speed: 1.5 },
2085
- },
2086
- ],
2087
- },
2088
- });
2089
-
2090
- <ModelViewer modelUrl="/plant.glb" licenseKey="your-license-key" {...viewer.props} />;
2091
-
2092
- // Live data: update the bindings and the looks follow.
2093
- viewer.bindings.updateObjectBindings("pump-3", { metrics: { temperature: 34 } });
2094
- viewer.bindings.updateObjectBindings("valve-1", { status: "disabled" });
2095
- ```
2096
-
2097
- - `target` takes the same targets as the binding helpers: an id, a list of
2098
- ids, `{ group }`, `{ tag }`, or `{ type }`. Left out, the rule considers every
2099
- bound object.
2100
- - `when` is a condition, or a list that must all match; left out, the rule
2101
- always applies. Conditions: `{ metric, above?, below? }` (the value must be
2102
- greater than `above` and less than `below`; an object without the metric
2103
- doesn't match), `{ status }` with one status or a list (a binding without
2104
- one counts as `"normal"`), and `{ metadata, equals }`, where dots in the key
2105
- reach into nested metadata.
2106
- - `material` accepts any `style.material` field. `colorScale` sets
2107
- `baseColor` (or `emissive`, with `channel: "emissive"`) from a metric,
2108
- blending between hex colour stops and taking the nearest stop outside them.
2109
- `pulse` makes the glow fade in and out (`true`, or `{ color, speed }` in
2110
- pulses per second).
2111
- - Rules apply in order over each object's own `style.material`, so a later
2112
- rule wins where two set the same field. Hover and selection highlighting
2113
- still show on top.
2114
- - Rules only change how objects look. `objectBindings`, `onObjectBindingsChange`,
2115
- and every callback keep the object's own values, so nothing a rule sets is
2116
- saved with the bindings.
2117
- - The pulse isn't drawn on streamed 3D Tiles models (`tilesetUrl`); the other
2118
- looks are.
2119
-
2120
- BindingBuilder's Scene tab has a **Rules** sub-tab for building these rules
2121
- visually. It suggests your groups, tags, types, metric names, and metadata
2122
- keys, shows how many objects each rule matches right now, and previews the
2123
- result live. Rules export and import with the rest of `sceneConfig`.
2124
-
2125
- `applyStyleRules(bindings, rules)` (from the main entry or `/utils`) returns
2126
- the styled bindings, for example to colour your own list the way the viewer
2127
- does, and `colorScaleValue(stops, value)` the colour a scale gives a value, for
2128
- a legend.
2129
-
2130
- ### `camera`
2131
-
2132
- Optional starting camera: where it is and its lens.
2133
-
2134
- ```ts
2135
- type ViewerCameraConfig = {
2136
- position?: [number, number, number];
2137
- fov?: number; // vertical field of view in degrees; default 50
2138
- near?: number;
2139
- far?: number;
2140
- zoom?: number;
2141
- };
2142
- ```
2143
-
2144
- Without a `position`, the camera starts at `[0, 0, 5]`. Pass one for predictable framing.
2145
-
2146
- Example:
2147
-
2148
- ```tsx
2149
- <ModelViewer
2150
- modelUrl="/model.glb"
2151
- licenseKey="your-license-key"
2152
- objectBindings={objectBindings}
2153
- camera={{
2154
- position: [0, 2.2, 7],
2155
- fov: 40,
2156
- near: 0.1,
2157
- far: 1000,
2158
- }}
2159
- />
2160
- ```
2161
-
2162
- Passing an explicit `camera` counts as choosing the initial view yourself, so the mount-time auto-fit (see [`onAutoFit`](#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.
2163
-
2164
- ### `cameraLimits`
2165
-
2166
- Constrains how far the orbit camera can zoom, tilt, turn, and pan. Angles are
2167
- in degrees.
2168
-
2169
- ```tsx
2170
- <ModelViewer
2171
- modelUrl="/car.glb"
2172
- licenseKey="your-license-key"
2173
- cameraLimits={{
2174
- minDistance: 2,
2175
- maxDistance: 12,
2176
- maxPolarAngle: 85, // stay above the floor
2177
- minAzimuthAngle: -60, // turn at most 60° either side of the front
2178
- maxAzimuthAngle: 60,
2179
- }}
2180
- />
2181
- ```
2182
-
2183
- ```ts
2184
- type CameraLimits = {
2185
- minDistance?: number; // default 0
2186
- maxDistance?: number; // default Infinity
2187
- minPolarAngle?: number; // degrees, default 0
2188
- maxPolarAngle?: number; // degrees, default 180
2189
- minAzimuthAngle?: number; // degrees, default -Infinity
2190
- maxAzimuthAngle?: number; // degrees, default Infinity
2191
- panBoundary?: [
2192
- min: [number, number, number],
2193
- max: [number, number, number],
2194
- ];
2195
- boundaryEnclosesCamera?: boolean; // default false
2196
- boundaryFriction?: number; // default 0
2197
- };
2198
- ```
2199
-
2200
- - **Polar** is the vertical angle: `0` looks straight down from above, `90` is
2201
- level with the target, and `180` looks straight up from below.
2202
- - **Azimuth** is the horizontal angle around the vertical axis: `0` views the
2203
- target from the front (+Z), `90` from its right (+X), and `-90` from its left.
2204
-
2205
- `panBoundary` is an axis-aligned world-space box. These limits apply to the
2206
- orbit controller; pointer-lock mode is not orbiting around the same target.
2207
-
2208
- ### `backgroundColor`
2209
-
2210
- Optional background color for the viewer canvas.
2211
-
2212
- Example:
2213
-
2214
- ```tsx
2215
- <ModelViewer
2216
- modelUrl="/model.glb"
2217
- licenseKey="your-license-key"
2218
- objectBindings={objectBindings}
2219
- backgroundColor="#0f172a"
2220
- />
2221
- ```
2222
-
2223
- ### `shadows`
2224
-
2225
- Optional boolean that controls whether the viewer renders shadows.
2226
-
2227
- Default:
2228
-
2229
- ```ts
2230
- true;
2231
- ```
2232
-
2233
- 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).
2234
-
2235
- Example:
2236
-
2237
- ```tsx
2238
- <ModelViewer
2239
- modelUrl="/model.glb"
2240
- licenseKey="your-license-key"
2241
- objectBindings={objectBindings}
2242
- shadows={false}
2243
- />
2244
- ```
2245
-
2246
- > **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](#how-rendering-works).
2247
-
2248
- ### `onCameraChange`
2249
-
2250
- Optional callback fired whenever the camera moves, with its plain state. It
2251
- fires often while the user orbits, pans, or zooms.
2252
-
2253
- ```ts
2254
- (state: ViewerCameraState) => void
2255
-
2256
- type ViewerCameraState = {
2257
- position: [number, number, number];
2258
- target: [number, number, number];
2259
- fov: number | null;
2260
- zoom: number;
2261
- azimuthAngle?: number; // radians, for setCameraState to restore
2262
- polarAngle?: number; // radians
2263
- };
2264
- ```
2265
-
2266
- It's the same state `useViewerCamera` exposes as `cameraState`, and
2267
- `setCameraState` restores it.
2268
-
2269
- ### `showObjectBindingDataPanel`
2270
-
2271
- Optional boolean that controls whether the built-in left object details panel is rendered.
2272
-
2273
- Default:
2274
-
2275
- ```ts
2276
- true;
2277
- ```
2278
-
2279
- ### `customObjectBindingDataPanel`
2280
-
2281
- Optional render prop for replacing the built-in left object details panel.
2282
-
2283
- Current prop shape:
2284
-
2285
- ```ts
2286
- type CustomObjectBindingDataPanelProps = {
2287
- isOpen: boolean;
2288
- selectedObject: ObjectBinding | null;
2289
- currentAction: ObjectActionEvent | null;
2290
- onClose: () => void;
2291
- onAction: (event: ObjectActionEvent) => void;
2292
- };
2293
- ```
2294
-
2295
- When provided, `ModelViewer` passes its existing internal handlers into your custom panel. Your panel can call them, wrap them, or ignore them.
2296
-
2297
- ### `overlay`
2298
-
2299
- Your own UI over the 3D view: badges, HUDs, buttons, custom panels. It sits
2300
- above the model and below the viewer's toolbars and banners.
2301
-
2302
- ```tsx
2303
- <ModelViewer
2304
- modelUrl="/sofa.glb"
2305
- licenseKey="your-license-key"
2306
- overlay={
2307
- <div style={{ position: "absolute", top: 16, left: 16 }}>
2308
- In stock <button onClick={addToCart}>Add to cart</button>
2309
- </div>
2310
- }
2311
- />
2312
- ```
2313
-
2314
- The layer covers the view but lets pointer events through, so the model stays
2315
- draggable around your elements. Its direct children receive pointer events, so
2316
- position them yourself (for example with `position: "absolute"`) rather than
2317
- wrapping everything in one full-size container.
2318
-
2319
- ### `unsafe_sceneChildren`
2320
-
2321
- react-three-fiber elements rendered inside the scene, for anything the viewer
2322
- can't do itself, such as custom geometry, a helper, or an effect driven by
2323
- `useFrame`:
2324
-
2325
- ```tsx
2326
- import { useFrame } from "@react-three/fiber";
2327
-
2328
- function Marker() {
2329
- const ref = useRef<Mesh>(null);
2330
- useFrame((_, delta) => (ref.current!.rotation.y += delta));
2331
- return (
2332
- <mesh ref={ref} position={[0, 1.5, 0]}>
2333
- <boxGeometry args={[0.3, 0.3, 0.3]} />
2334
- <meshStandardMaterial color="hotpink" />
2335
- </mesh>
2336
- );
2337
- }
2338
-
2339
- <ModelViewer {...props} unsafe_sceneChildren={<Marker />} />;
2340
- ```
2341
-
2342
- They're rendered inside their own `Suspense` boundary, so a child that loads
2343
- assets doesn't hold up the model. Because they work with three.js directly,
2344
- they're outside the viewer's own API: the name says so, and they can break when
2345
- three.js or react-three-fiber change. Prefer the props and hooks for anything
2346
- they cover.
2347
-
2348
- ### `customSceneObjectsPanel`
2349
-
2350
- Optional render prop for replacing the built-in right scene objects panel.
2351
-
2352
- Current prop shape:
2353
-
2354
- ```ts
2355
- type CustomSceneObjectsPanelProps = {
2356
- objectBindings: Record<string, ObjectBinding>;
2357
- onAction?: (event: ObjectActionEvent) => void;
2358
- onFocus?: (binding: ObjectBinding) => void;
2359
- onHover?: (binding: ObjectBinding | null) => void;
2360
- };
2361
- ```
2362
-
2363
- When provided, `ModelViewer` passes the current bindings plus its built-in action, focus, and hover handlers into your custom panel.
2364
-
2365
- ### `showSceneObjectsPanel`
2366
-
2367
- Optional boolean that controls whether the built-in right scene objects panel is rendered.
2368
-
2369
- Default:
2370
-
2371
- ```ts
2372
- true;
2373
- ```
2374
-
2375
- ### `showDownloadButton`
2376
-
2377
- Optional boolean that enables the download split-button (export GLB, export the
2378
- model's UV layout, and—when `preserveDrawingBuffer` is enabled—export a PNG
2379
- screenshot).
2380
-
2381
- Default:
2382
-
2383
- ```ts
2384
- true;
2385
- ```
2386
-
2387
- UV export downloads an editable 2048×2048 SVG UV-island outline when the model
2388
- uses one material. For multi-material models it downloads a ZIP containing one
2389
- SVG texture template per material. Meshes without a `uv` attribute are omitted;
2390
- when the model has no UV coordinates, the viewer reports that no layout is
2391
- available.
2392
-
2393
- The bottom-right action bar renders when at least one of `showResetButton`, `showDownloadButton`, `showFullscreenButton` (when supported), or (`showAnnotationNavigation` with annotations present) is `true`.
2394
-
2395
- ### `downloadFilename`
2396
-
2397
- Optional filename stem for the built-in model export button.
2398
-
2399
- Default:
2400
-
2401
- ```ts
2402
- "model";
2403
- ```
2404
-
2405
- ### `showResetButton`
2406
-
2407
- Optional boolean that shows the "reset camera view" button in the bottom-right action bar.
2408
-
2409
- Default:
2410
-
2411
- ```ts
2412
- true;
2413
- ```
2414
-
2415
- ### `showFullscreenButton`
2416
-
2417
- Optional boolean that adds an enter/exit fullscreen toggle to the bottom-right
2418
- action bar. Its grouped form is `ui.fullscreen`. Default: `false`.
2419
-
2420
- ```tsx
2421
- <ModelViewer
2422
- modelUrl="/model.glb"
2423
- licenseKey={licenseKey}
2424
- ui={{ fullscreen: true }}
2425
- />
2426
- ```
2427
-
2428
- Fullscreen expands the whole viewer, including its panels and overlays. The
2429
- button reflects browser exits such as Escape, and is hidden when fullscreen is
2430
- unavailable. It is temporarily disabled during video recording. Embedded viewers
2431
- need an iframe with `allow="fullscreen"` (or `allowFullScreen` in React). A rejected
2432
- request displays an error beside the button; see the
2433
- [Fullscreen API permissions](https://developer.mozilla.org/en-US/docs/Web/API/Element/requestFullscreen#security).
2434
-
2435
- ### `showLoadingOverlay`
2436
-
2437
- Optional boolean controlling whether the built-in loading overlay is shown while the model is loading and the initial camera fit is settling, and the load-error message if the model fails to load (see [`onLoadError`](#onloaderror)).
2438
-
2439
- Default:
2440
-
2441
- ```ts
2442
- true;
2443
- ```
2444
-
2445
- ### `showMouseController`
2446
-
2447
- 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.
2448
-
2449
- Default:
2450
-
2451
- ```ts
2452
- false;
2453
- ```
2454
-
2455
- ### `mouseControllerPosition`
2456
-
2457
- Optional placement for the on-screen controller.
2458
-
2459
- ```ts
2460
- type MouseControllerPosition =
2461
- | "bottom-left"
2462
- | "bottom-right"
2463
- | "top-left"
2464
- | "top-right"
2465
- | "center"
2466
- | "center-bottom"
2467
- | "center-top";
2468
- ```
2469
-
2470
- ### `mouseControllerOpacity`
2471
-
2472
- Optional opacity for the on-screen controller.
2473
-
2474
- Default:
2475
-
2476
- ```ts
2477
- 1;
2478
- ```
2479
-
2480
- ### `moveSensitivity`
2481
-
2482
- Optional movement sensitivity for the on-screen controller.
2483
-
2484
- Default:
2485
-
2486
- ```ts
2487
- 0.08;
2488
- ```
2489
-
2490
- ### `zoomSensitivity`
2491
-
2492
- Optional zoom sensitivity for the underlying `CameraControls` zoom interaction (mouse wheel / trackpad / pinch).
2493
-
2494
- Default:
2495
-
2496
- ```ts
2497
- 1.0;
2498
- ```
2499
-
2500
- Example:
2501
-
2502
- ```tsx
2503
- <ModelViewer
2504
- modelUrl="/model.glb"
2505
- licenseKey="your-license-key"
2506
- objectBindings={objectBindings}
2507
- showMouseController
2508
- mouseControllerPosition="bottom-right"
2509
- mouseControllerOpacity={0.8}
2510
- moveSensitivity={0.1}
2511
- zoomSensitivity={1.2}
2512
- />
2513
- ```
2514
-
2515
- ### `sceneConfig`
2516
-
2517
- 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.
2518
-
2519
- `sceneConfig.animations` drives autoplay and per-clip playback settings, and `sceneConfig.annotations` seeds the annotation markers rendered on the model.
2520
-
2521
- `sceneConfig.model.renderer` accepts `"pbr"`, `"matcap"`, or `"uv-checker"`.
2522
- UV Checker temporarily replaces every mesh material with an unlit numbered test
2523
- chart without changing `objectBindings` or uploaded textures. Switching back
2524
- restores the original material references. Meshes without UV0 coordinates appear
2525
- magenta and trigger an explanatory notice.
2526
-
2527
- ```tsx
2528
- <ModelViewer
2529
- {...props}
2530
- sceneConfig={{
2531
- ...sceneConfig,
2532
- model: { ...sceneConfig.model, renderer: "uv-checker" },
2533
- }}
2534
- />
2535
- ```
2536
-
2537
- ### `onSceneConfigChange`
2538
-
2539
- Called with an updated `SceneConfig` when viewer-owned scene editing changes
2540
- the configuration. Currently this is used when a light-position or rotation
2541
- gizmo moves a scene-config light. Update controlled `sceneConfig` state with
2542
- the value returned by this callback.
2543
-
2544
- ```ts
2545
- onSceneConfigChange?: (config: SceneConfig) => void
2546
- ```
2547
-
2548
- ### `disableZoom`
2549
-
2550
- Optional boolean. When `true`, the viewer never zooms the camera to an object on selection (selection still highlights and fires callbacks).
2551
-
2552
- Default:
2553
-
2554
- ```ts
2555
- false;
2556
- ```
2557
-
2558
- ### `zoomOnSelected`
2559
-
2560
- 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`.
2561
-
2562
- Default:
2563
-
2564
- ```ts
2565
- true;
2566
- ```
2567
-
2568
- ### `highlightOnHover`
2569
-
2570
- Controls hover highlighting for the canvas, built-in object list, and
2571
- `panelHoveredObject`. Default: `true`. On very large models, viewport hover is
2572
- disabled for performance, while panel-row highlighting remains available.
2573
-
2574
- ### Moving and rotating objects
2575
-
2576
- Set `moveModeEnabled` to show a centered Drei `PivotControls` gizmo for the
2577
- selected bound object. Translation arrows, plane handles, and rotation rings
2578
- are available together; scaling is not enabled. While object editing is on,
2579
- the camera controls are paused so pointer drags manipulate the object instead
2580
- of the camera.
2581
-
2582
- ```tsx
2583
- import { useState } from "react";
2584
- import {
2585
- ModelViewer,
2586
- type ObjectBinding,
2587
- } from "@liveroom-tech/react-immersive";
2588
-
2589
- export function EditableViewer({
2590
- initialBindings,
2591
- }: {
2592
- initialBindings: Record<string, ObjectBinding>;
2593
- }) {
2594
- const [objectBindings, setObjectBindings] = useState(initialBindings);
2595
-
2596
- return (
2597
- <ModelViewer
2598
- modelUrl="/model.glb"
2599
- licenseKey="your-license-key"
2600
- objectBindings={objectBindings}
2601
- onObjectBindingsChange={setObjectBindings}
2602
- moveModeEnabled
2603
- objectTransformSpace="world"
2604
- />
2605
- );
2606
- }
2607
- ```
2608
-
2609
- Click a bound mesh to select it, then drag an arrow to move on one axis, a
2610
- plane handle to move on two axes, or a ring to rotate. The pivot is placed at
2611
- the center of the selected object's renderable geometry, so rotation happens
2612
- around the object itself even when the source model's node origin is elsewhere.
2613
-
2614
- `objectTransformSpace` accepts:
2615
-
2616
- - `"world"` (default): the gizmo remains aligned with the scene axes
2617
- - `"local"`: the gizmo follows the selected object's current orientation
2618
-
2619
- Axis space changes the direction of the handles; it does not change the
2620
- object's position when toggled. The resulting `transform.position` and
2621
- `transform.rotation` values are always persisted in the object's local space,
2622
- with rotation stored in radians.
2623
-
2624
- Passing `onObjectBindingsChange` makes binding edits controlled, so the parent
2625
- must store the returned record as shown above. Without that callback,
2626
- `ModelViewer` keeps edits internally for the mounted viewer. Similarly, when
2627
- you pass `selectedObject`, update it from `onObjectSelect`; otherwise selection
2628
- is managed internally.
2629
-
2630
- ### `enableCameraControls`
2631
-
2632
- Optional boolean controlling orbit, pan, and zoom input. Default: `true`.
2633
- Camera controls are temporarily disabled whenever `moveModeEnabled` is on.
2634
-
2635
- ### `cameraControllerType` / `onCameraControllerChange`
2636
-
2637
- Selects the active camera interaction mode:
2638
-
2639
- - `"orbit"` (default) uses orbit, pan, and zoom controls.
2640
- - `"pointerLock"` captures the pointer for first-person look-around. Leaving
2641
- pointer lock returns the viewer to orbit mode.
2642
-
2643
- The viewer's camera-controller menu can manage this internally. Pass
2644
- `cameraControllerType` to control it from application state and update that
2645
- state from `onCameraControllerChange`.
2646
-
2647
- ```tsx
2648
- const [controller, setController] = useState<"orbit" | "pointerLock">("orbit");
2649
-
2650
- <ModelViewer
2651
- {...props}
2652
- cameraControllerType={controller}
2653
- onCameraControllerChange={setController}
2654
- />
2655
- ```
2656
-
2657
- Auto-rotation pauses while pointer-lock mode is active.
2658
-
2659
- ### `autoRotate` / `autoRotateSpeed`
2660
-
2661
- Enables automatic orbiting and controls its speed. Defaults are `false` and
2662
- `1`. When these props aren't passed, `sceneConfig.model.autoRotate` and
2663
- `sceneConfig.model.autoRotateSpeed` apply, so a scene exported from
2664
- `BindingBuilder` can carry the same behavior. When they are passed, they win.
2665
-
2666
- ```tsx
2667
- <ModelViewer {...props} autoRotate autoRotateSpeed={0.75} />
2668
- ```
2669
-
2670
- Auto-rotate doesn't start for visitors who ask their device to reduce motion.
2671
- See [`reducedMotion`](#reducedmotion).
2672
-
2673
- ### `enableKeyboardNavigation`
2674
-
2675
- Optional boolean enabling camera shortcuts after the viewer canvas is clicked
2676
- or focused. Default: `false`. Use `W`/`S` to move forward/back, `A`/`D` to
2677
- truck left/right, Space/`C` to move up/down, and the arrow keys to orbit.
2678
- `Shift` + Up/Down moves forward/back. Keyboard navigation is inactive while
2679
- object editing or camera controls are disabled. While the canvas has focus, a
2680
- screen reader reads out what its keys do (the `cameraKeyboardHelp` label).
2681
-
2682
- ### Accessibility
2683
-
2684
- The viewer can be used without a mouse, and reads out what is happening:
2685
-
2686
- - **Moving through objects with the keyboard.** Tabbing into the viewer reaches
2687
- a small object list at the top of the canvas, shown only while it has focus.
2688
- The arrow keys (or `Home` and `End`) move through the objects and highlight
2689
- each one in the scene. Typing a letter jumps to an object whose name starts
2690
- with it. `Enter` or Space selects the object, exactly as clicking it would,
2691
- and `Escape` clears the selection. Screen readers hear each object's name and
2692
- its position ("Wheel, 3 of 12"). The list follows the scene objects panel's
2693
- order. It skips hidden objects, and objects an isolation has set aside.
2694
- - **Announcing the selection.** Every change of selection is announced, whether
2695
- it came from a click, the keyboard, the objects panel, or your app ("Wheel
2696
- selected", "Selection cleared").
2697
- - **The scene objects panel.** Each row is a button, so the panel can be used
2698
- from the keyboard too. Focusing a row highlights its object, and the
2699
- visibility buttons name the object they hide or show.
2700
- - **Reduced motion.** See [`reducedMotion`](#reducedmotion) below.
2701
-
2702
- All of this text can be translated with `labels`.
2703
-
2704
- ### `showObjectNavigator`
2705
-
2706
- Optional boolean for the keyboard object list described above. Default: `true`.
2707
- It takes no space until a keyboard user reaches it, so it's on by default. Set it
2708
- to `false` when your app provides its own way to choose objects from the
2709
- keyboard. It is hidden while measuring, since clicks then place points instead
2710
- of selecting. Keyboard selections reach `onEngagement` as `part-viewed` events
2711
- with `source: "keyboard"`.
2712
-
2713
- ```tsx
2714
- <ModelViewer {...props} ui={{ objectNavigator: false }} />
2715
- ```
2716
-
2717
- ### `reducedMotion`
2718
-
2719
- Cuts motion the viewer starts on its own, for visitors who find it hard to
2720
- watch. Default: `"user"`, which follows the visitor's device setting
2721
- (`prefers-reduced-motion`). `"always"` and `"never"` override it. Also
2722
- `controls.reducedMotion`; the type is exported as `ReducedMotionSetting`.
2723
-
2724
- When motion is reduced:
2725
-
2726
- - Auto-rotate doesn't start.
2727
- - A `cinematic` path with `autoPlay` waits for its play button.
2728
- - Camera moves jump straight to where they're going instead of gliding: the
2729
- first framing, focusing a selected object, resetting the view, saved views,
2730
- guided-tour stops, and moves made through `useViewerCamera`.
2731
- - Panels, menus and popups appear in place instead of sliding or fading in.
2732
- Loading spinners keep turning, since they show that something is still
2733
- happening.
2734
-
2735
- Motion the visitor asks for still plays: the cinematic play button, video
2736
- export, and dragging the camera. The viewer doesn't change animations your app
2737
- starts, such as model clips, effects, or style rule pulses. To respect the
2738
- setting there too, check `matchMedia("(prefers-reduced-motion: reduce)")` in
2739
- your app. `SimpleModelViewer` takes the same prop.
2740
-
2741
- ### `showViewGizmo`
2742
-
2743
- Optional boolean showing a clickable orientation gizmo in the top-right of the
2744
- viewer. Default: `false`. It indicates the camera's view of the scene axes and
2745
- can snap the camera to an axis; it is separate from the selected object's
2746
- transform gizmo and is hidden while `moveModeEnabled` is on.
2747
-
2748
- ### `onAutoFit`
2749
-
2750
- 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`](#camera) prop was given, in which case that position is treated as the chosen initial view and `fitScene` is skipped.
2751
-
2752
- Current callback shape:
2753
-
2754
- ```ts
2755
- () => Promise<boolean>;
2756
- ```
2757
-
2758
- ### `refitOnResize`
2759
-
2760
- 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.
2761
-
2762
- While a selected object is framed (see `zoomOnSelected`), a resize frames that object again rather than the whole model, so the object panel opening doesn't undo the zoom. An object with an authored `cameraState` keeps that view. Once the selection is cleared, or something else moves the camera away from the object, resizes frame the whole model again.
2763
-
2764
- Default:
2765
-
2766
- ```ts
2767
- true;
2768
- ```
2769
-
2770
- ### `onAnnotationsChange`
2771
-
2772
- Optional callback fired when the annotation markers on the model change (added, edited, or removed through the viewer UI).
2773
-
2774
- Current callback shape:
2775
-
2776
- ```ts
2777
- (annotations: AnnotationMarker[]) => void
2778
- ```
2779
-
2780
- Where `AnnotationMarker` is the `SceneAnnotationMarker` shape:
2781
-
2782
- ```ts
2783
- type AnnotationMarker = {
2784
- id: number;
2785
- worldPosition: [number, number, number];
2786
- localPosition: [number, number, number];
2787
- title: string;
2788
- description: string;
2789
- };
2790
- ```
2791
-
2792
- ### `activeAnnotation` / `onActiveAnnotationChange`
2793
-
2794
- 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).
2795
-
2796
- Current shapes:
2797
-
2798
- ```ts
2799
- activeAnnotation?: AnnotationMarker | null;
2800
- onActiveAnnotationChange?: (annotation: AnnotationMarker | null) => void;
2801
- ```
2802
-
2803
- ### `renderMode`
2804
-
2805
- 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.
2806
-
2807
- ### `maxDpr`
2808
-
2809
- Optional upper bound for the device pixel ratio used when rendering, so retina/4K displays don't render at full 2–3x cost.
2810
-
2811
- Default:
2812
-
2813
- ```ts
2814
- 2;
2815
- ```
2816
-
2817
- ### `preserveDrawingBuffer`
2818
-
2819
- Optional boolean that retains the WebGL framebuffer for PNG screenshots. It is
2820
- `false` by default to avoid imposing framebuffer-retention cost on every frame.
2821
- Enable it when using `captureImage` or the built-in Download PNG action.
2822
-
2823
- Default:
2824
-
2825
- ```ts
2826
- false;
2827
- ```
2828
-
2829
- ### `webgpu`
2830
-
2831
- Optional boolean. Draws with three.js's WebGPU renderer when the browser has
2832
- WebGPU, and with WebGL otherwise. Default: `false`. Also `perf.webgpu`.
2833
-
2834
- ```tsx
2835
- <ModelViewer {...props} perf={{ webgpu: true }} />
2836
- ```
2837
-
2838
- The viewer asks the browser for a WebGPU adapter once per page before
2839
- creating its canvas. Without one, it uses WebGL and says so once in the
2840
- console. `onViewerReady` reports which renderer is drawing as
2841
- `rendererBackend` (`"webgl"` or `"webgpu"`). The WebGPU renderer is loaded only
2842
- by viewers that use it.
2843
-
2844
- With WebGPU, the model, materials, bindings, hover highlight, picking,
2845
- animations, decals, annotations, measurement, video and GIF export, and tone
2846
- mapping and exposure work as they do with WebGL. Some features are built on
2847
- WebGL and are left out:
2848
-
2849
- - The post-processing effects (outline, bloom, depth of field, and the rest)
2850
- don't draw. Tone mapping and exposure from `sceneConfig.postProcessing` still
2851
- apply, through the renderer.
2852
- - Contact shadows (`sceneConfig.groundShadows` in shadow-catcher mode) don't
2853
- draw.
2854
- - The section cut and the move gizmo (`moveModeEnabled`) aren't available, and
2855
- the console says so if they're asked for.
2856
- - AR and VR sessions in the browser (WebXR) aren't offered. Quick Look on iOS
2857
- and the phone hand-off still work.
2858
- - Measurement lines are one pixel wide.
2859
-
2860
- `captureImage` and the Download PNG action work with WebGPU without
2861
- `preserveDrawingBuffer`. A lost WebGPU device shows the same **Reload viewer**
2862
- message as a lost WebGL context.
2863
-
2864
- ### Lazy loading and posters
2865
-
2866
- `loading`, `poster` and `reveal` decide when a viewer creates its 3D view, and
2867
- what shows in its place until then. They matter most on pages with many
2868
- viewers, such as a product grid: browsers allow a page about 16 WebGL contexts
2869
- and take the oldest away past that.
2870
-
2871
- ```tsx
2872
- <ModelViewer {...props} poster="/chair.webp" />
2873
- <ModelViewer {...props} poster="/chair.webp" reveal="interaction" />
2874
- ```
2875
-
2876
- `loading` is `"lazy"` by default. A lazy viewer:
2877
-
2878
- - starts its 3D view when it comes within about 200px of the screen. One that's
2879
- on the screen when the page loads starts at once.
2880
- - shares the page's live 3D views with the other lazy viewers: at most 12, or 6
2881
- on phones and tablets. Viewers on the screen come first. One scrolled away
2882
- from keeps its view while there's room, so coming back is instant. When the
2883
- room is needed, it's put away, leaving a picture of its last frame, and is
2884
- built again when it comes back near the screen.
2885
- - comes back as it was: the camera, picked variants, light and section-cut
2886
- changes, isolation, the exploded view, the camera controller, the UV checker,
2887
- the annotation filter, and, when the viewer keeps `objectBindings` itself,
2888
- the changes made to them. The selection and open panels don't come back. `onModelLoaded` and
2889
- `onViewerReady` run again, with a new viewer.
2890
- - stops drawing while it's off the screen, and starts again when it's back.
2891
- Browsers already stop drawing in hidden tabs.
2892
-
2893
- `loading="eager"` starts the 3D view at once and keeps it. Eager viewers don't
2894
- count towards the 12. `BindingBuilder`'s preview is eager.
2895
-
2896
- `poster` is an image shown in the viewer's place until its 3D view is drawn,
2897
- then fades out; the loading message shows over it. It's fitted inside the
2898
- viewer. To fill the viewer instead, set the CSS variable
2899
- `--ri-poster-fit: cover` on a parent. A render of the model framed as the
2900
- viewer frames it works best: [`captureImage`](#onviewerready) can make one.
2901
-
2902
- `reveal="interaction"` keeps the poster, with a **View in 3D** button, until the
2903
- visitor presses it, so only the products someone picks use a 3D view. The
2904
- button's text is the [`labels`](https://react-immersive.liveroom.dev/docs/reference/labels)
2905
- key `viewIn3D`. `reveal` is `"auto"` by default: the view starts on its own.
2906
-
2907
- The same three props work on [`SimpleModelViewer`](#simplemodelviewer-public-api)
2908
- and [`CompareViewer`](#compareviewer).
2909
-
2910
- ---
2911
-
2912
- ### `performanceProfile`
2913
-
2914
- Optional control over how aggressively the viewer trades visual fidelity for a stable WebGL context on constrained GPUs.
2915
-
2916
- ```ts
2917
- performanceProfile?: "auto" | "high" | "low";
2918
- ```
2919
-
2920
- - `"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.
2921
- - `"high"`, always render at full quality (postprocessing, soft shadows, full `maxDpr`), even on mobile. Use when you know the target devices can handle it.
2922
- - `"low"`, always apply the reduced profile, on any device.
2923
-
2924
- Default:
2925
-
2926
- ```ts
2927
- "auto";
2928
- ```
2929
-
2930
- If the WebGL context is lost, rendering pauses and the viewer displays a
2931
- **Reload viewer** button. This message appears even with
2932
- `ui={{ loadingOverlay: false }}`. When the browser fires `webglcontextrestored`,
2933
- the viewer automatically rebuilds its canvas and scene; the reload button starts
2934
- the same rebuild without waiting for browser restoration.
2935
-
2936
- Recovery uses the current props and cached model assets, and cancels video
2937
- recording. The rebuilt viewer keeps its camera and the visitor's changes, as a
2938
- lazy viewer does when it comes back (see
2939
- [Lazy loading and posters](#lazy-loading-and-posters)); the selection,
2940
- measurements and playback start over.
2941
- `onSceneLoaded`, `onModelLoaded`, `onAnimationsReady`, and `onViewerReady` run again
2942
- for the rebuilt scene, so integrations receive fresh controls and object refs.
2943
-
2944
- ### `dracoDecoderPath` / `ktx2TranscoderPath` / `meshopt`
2945
-
2946
- Optional controls for the compressed-asset decoders used when loading the GLB/GLTF asset.
2947
-
2948
- ```ts
2949
- dracoDecoderPath?: string | false;
2950
- ktx2TranscoderPath?: string | false;
2951
- meshopt?: boolean;
2952
- ```
2953
-
2954
- Meshopt is bundled and enabled by default. DRACO and KTX2 are also enabled by
2955
- default through immutable, versioned decoder builds hosted on the first-party
2956
- `assets.react-immersive.liveroom.dev` domain. They are fetched lazily only when a model
2957
- uses the corresponding compression format.
2958
-
2959
- ```ts
2960
- dracoDecoderPath =
2961
- "https://assets.react-immersive.liveroom.dev/three-r184/draco/";
2962
- ktx2TranscoderPath =
2963
- "https://assets.react-immersive.liveroom.dev/three-r184/basis/";
2964
- ```
2965
-
2966
- Pass `false` to disable either decoder. For a fully self-hosted or offline
2967
- deployment, version-matched files are included under `dist/decoders`.
2968
-
2969
- Copy those files into your app's public/static directory during deployment:
2970
-
2971
- ```sh
2972
- cp -R node_modules/@liveroom-tech/react-immersive/dist/decoders public/react-immersive-decoders
2973
- ```
2974
-
2975
- Then pass the public URL directories (including the trailing slash):
2976
-
2977
- ```tsx
2978
- <ModelViewer
2979
- dracoDecoderPath="/react-immersive-decoders/draco/"
2980
- ktx2TranscoderPath="/react-immersive-decoders/basis/"
2981
- {...props}
2982
- />
2983
- ```
2984
-
2985
- The same props are available on `SimpleModelViewer` and `BindingBuilder`. When
2986
- using the built-in defaults with a strict CSP, allow the first-party asset
2987
- origin and decoder workers; a typical baseline is
2988
- `connect-src 'self' https://assets.react-immersive.liveroom.dev; worker-src 'self' blob:; script-src 'self' 'wasm-unsafe-eval'`.
2989
- Self-hosted paths only require `'self'`. Older browsers may require their
2990
- WebAssembly fallback policy as well.
2991
-
2992
- ### `showUvCheckerButton`
2993
-
2994
- Optional boolean that shows a UV Checker toggle button over the canvas. Turning it on temporarily replaces the model's materials with the numbered diagnostic texture; turning it off restores the renderer configured by `sceneConfig`. Missing UV0 coordinates are shown in magenta.
2995
-
2996
- ```tsx
2997
- <ModelViewer showUvCheckerButton {...props} />
2998
- ```
2999
-
3000
- Default:
3001
-
3002
- ```ts
3003
- false;
3004
- ```
3005
-
3006
- ### `showMeasureTools`
3007
-
3008
- Optional boolean that shows measurement tools and a toggleable bounding-box
3009
- dimensions overlay. Start measuring, then choose a tool:
3010
-
3011
- - **Distance:** pick points for distances between consecutive picks.
3012
- - **Angle:** pick an arm, the angle's vertex, and the other arm. The readout is
3013
- in degrees, with an arc around the middle pick.
3014
- - **Area:** pick polygon corners in boundary order, then **Finish area**. The
3015
- closed polygon is shaded and labeled in square units. Concave polygons are
3016
- supported; nonplanar, crossing, touching, duplicate, or collinear boundaries
3017
- show an error. Use Undo to correct a pick.
3018
- - **Point to plane:** pick a reference face, then a point. The tool highlights
3019
- the picked triangle and shows the shortest distance to its infinite plane,
3020
- with a perpendicular segment to the projected point.
3021
-
3022
- **No snapping** keeps picks on the surface. **Snap: vertex**, **Snap: edge**,
3023
- and **Snap: auto** snap within 12 CSS pixels to vertices or triangle edges on
3024
- the picked face. Auto prioritizes nearby vertices. Hover markers identify the
3025
- snap target; snapped picks are yellow. Snapping respects world transforms,
3026
- instances, and the current pose of skinned/morph geometry. Mesh triangulation
3027
- edges can also be snap targets.
3028
-
3029
- Undo removes the last pick; Clear removes the current measurement. Switching
3030
- tools clears picks. A new pick after a completed angle, area, or point-to-plane
3031
- measurement starts another measurement. Picks are snapshots in world space;
3032
- model changes, display rotation, and binding transforms clear them. Pause
3033
- animated models before measuring a pose. Hidden objects, decals, and editor
3034
- helpers are excluded from picking.
3035
-
3036
- Default:
3037
-
3038
- ```ts
3039
- false;
3040
- ```
3041
-
3042
- ### `measurementUnit`
3043
-
3044
- Optional unit suffix appended to distance and dimension readouts; area uses its
3045
- squared form (for example `m²`). Angles always use degrees. glTF models are
3046
- authored in meters by spec, so values are not converted; this only changes the
3047
- displayed label.
3048
-
3049
- Default:
3050
-
3051
- ```ts
3052
- "m";
3053
- ```
3054
-
3055
- ### `showExplodeControls`
3056
-
3057
- Optional boolean that shows an exploded-view slider over the canvas. It slides each bound part outward from the model center to reveal interior/assembly structure, then back to reassemble. Best suited to static product/CAD models, the offset writes node positions each frame, so it conflicts with a playing skeletal animation.
3058
-
3059
- Default:
3060
-
3061
- ```ts
3062
- false;
3063
- ```
3064
-
3065
- ### `showMaterialVariants`
3066
-
3067
- Shows swatch buttons, centered at the bottom of the viewer, for switching
3068
- between the model's glTF material variants (the `KHR_materials_variants`
3069
- extension): the colorways, finishes, or trims a product file ships with. Each
3070
- button shows the variant's colour when its material is a plain colour. It
3071
- appears for a glTF/GLB with any variants. Default `true`.
3072
-
3073
- Variants come in **option groups**, one row each. Choosing a variant replaces
3074
- the other choice in its row and keeps the other rows, so a paint colour and a
3075
- wheel style combine. Clicking the chosen variant again returns that row to the
3076
- file's default materials. The viewer works out the groups from the file:
3077
-
3078
- - A name like `"Paint: Red"` or `"Paint / Red"` puts the variant in a row
3079
- labelled **Paint**, with a button labelled **Red**.
3080
- - Otherwise, variants that restyle the same parts share a row, and variants
3081
- that restyle different parts get separate rows. A shoe's `"midnight"`,
3082
- `"beach"` and `"street"` colorways, which all restyle the same parts, are one
3083
- row.
3084
-
3085
- The variants shown are `sceneConfig.model.activeVariants`, at most one from
3086
- each group, which is also how you choose them yourself:
3087
-
3088
- ```tsx
3089
- import { chooseMaterialVariant, useViewer } from "@liveroom-tech/react-immersive";
3090
-
3091
- const viewer = useViewer();
3092
-
3093
- <ModelViewer modelUrl="/car.glb" licenseKey="your-license-key" {...viewer.props} />;
3094
-
3095
- // Once the model has loaded
3096
- viewer.model.materialVariants; // ["Paint: Red", "Paint: Blue", "Wheels: Sport", "Wheels: Classic"]
3097
- viewer.model.materialVariantGroups;
3098
- // [
3099
- // { name: "Paint", variants: [{ name: "Paint: Red", label: "Red", swatch: "#ff0000" }, ...] },
3100
- // { name: "Wheels", variants: [{ name: "Wheels: Sport", label: "Sport" }, ...] },
3101
- // ]
3102
-
3103
- viewer.scene.updateSceneConfig({ model: { activeVariants: ["Paint: Red", "Wheels: Sport"] } });
3104
- viewer.scene.updateSceneConfig({ model: { activeVariants: [] } }); // the defaults
3105
-
3106
- // Your own swatches: swap a choice within its group, keeping the others.
3107
- const active = viewer.scene.sceneConfig.model.activeVariants ?? [];
3108
- viewer.scene.updateSceneConfig({
3109
- model: {
3110
- activeVariants: chooseMaterialVariant(active, "Paint: Blue", viewer.model.materialVariantGroups),
3111
- },
3112
- });
3113
- ```
3114
-
3115
- - A variant sets each object's starting material. `objectBindings` material
3116
- overrides (`style.material`) still apply on top of it, and clearing an
3117
- override returns to the variant's material rather than the file's default.
3118
- - Objects no active variant maps keep their default material, bound or not.
3119
- If two active variants map the same object, the later one in the list wins.
3120
- - A variant's materials and textures load the first time it's chosen; the
3121
- current look stays until they're ready.
3122
- - With `onSceneConfigChange`, a pick goes to your scene config (`useViewer`
3123
- wires this up). Without it, `ModelViewer` keeps the picks itself until
3124
- `sceneConfig.model.activeVariants` or `modelUrl` changes.
3125
- - A name the model doesn't have is skipped with a console warning listing the
3126
- model's variants.
3127
- - An action can switch variants with a scene-config effect:
3128
- `{ sceneConfig: { model: { activeVariants: ["Paint: Red"] } } }`.
3129
- - Swatches use the colour of the material a variant puts on the most vertices.
3130
- A textured material gets no swatch, just its label, so showing the buttons
3131
- never loads textures.
3132
- - Each pick reaches [`onEngagement`](#onengagement--debugengagement) as a
3133
- `variant-chosen` event with its `group`.
3134
- - `onModelLoaded` reports `materialVariants` and `materialVariantGroups`, and
3135
- BindingBuilder's Scene tab (**General**, one select per group) picks the
3136
- variants the exported scene config starts with. OBJ, FBX, USDZ, STL, PLY, and
3137
- streamed 3D Tiles models have no variants.
3138
- - Scene configs saved before option groups, with a single
3139
- `model.materialVariant`, still show that variant.
3140
-
3141
- ### Decals
3142
-
3143
- `binding.decals` prints images and text onto an object's surface: a logo on a
3144
- shirt, a name on a mug, a label on a machine. Each decal is projected onto the
3145
- object and follows it as it moves, and it's saved with the binding like any
3146
- other edit.
3147
-
3148
- ```tsx
3149
- const viewer = useViewer();
3150
-
3151
- <ModelViewer
3152
- modelUrl="/mug.glb"
3153
- licenseKey="your-license-key"
3154
- {...viewer.props}
3155
- // Put the customer's text where they double-click.
3156
- onObjectDoubleClick={({ binding, surface }) => {
3157
- if (!surface) return;
3158
- viewer.bindings.addDecal(binding.id, {
3159
- text: customerName,
3160
- color: "#1e293b",
3161
- ...surface, // position and normal, in the object's own space
3162
- size: 0.06, // width in model units
3163
- });
3164
- return true; // skip the viewer's own double-click behavior
3165
- }}
3166
- />;
3167
-
3168
- const id = viewer.bindings.addDecal("mug", { image: "/logos/acme.png", position, normal, size: 0.05 });
3169
- viewer.bindings.updateDecal("mug", id, { size: 0.08, rotation: Math.PI / 12 });
3170
- viewer.bindings.removeDecal("mug", id);
3171
- ```
3172
-
3173
- ```ts
3174
- type ObjectBindingDecal = {
3175
- id: string;
3176
- image?: string; // a URL or public path
3177
- text?: string; // shown when there's no image
3178
- color?: string; // text colour, default "#ffffff"
3179
- fontFamily?: string; // CSS font family, default "sans-serif"
3180
- fontWeight?: "normal" | "bold"; // default "bold"
3181
- position: [number, number, number]; // on the object, in its own space
3182
- normal: [number, number, number]; // the way the surface faces there
3183
- size: number; // width in model units; the height follows the image or text
3184
- rotation?: number; // radians around the normal, default 0
3185
- opacity?: number; // 0–1, default 1
3186
- };
3187
- ```
3188
-
3189
- - `onObjectDoubleClick` and `onObjectContextMenu` report `surface`: the point
3190
- under the pointer and the way the surface faces, in the object's own space,
3191
- which is exactly what a decal's `position` and `normal` take.
3192
- - `addDecal` returns the decal's id (pass your own `id` to choose it), and
3193
- takes the same targets as the other binding helpers, so
3194
- `addDecal({ group: "panels" }, …)` prints on each. Decals are plain binding
3195
- data: you can also set `decals` directly.
3196
- - A decal wraps onto the surface around its position, reaching about half
3197
- its size in depth, so it follows gentle curves without printing on the far
3198
- side of a thin part.
3199
- - A decal fades with its object when that object is ghosted by isolation or
3200
- made see-through by a style rule, and is cut by a section cut.
3201
- - To let the people using your viewer add their own, give a binding the
3202
- built-in `add-text` and `add-image` actions:
3203
-
3204
- ```ts
3205
- actions: [
3206
- { id: "add-text", label: "Add Text", type: "command" },
3207
- { id: "add-image", label: "Add Image", type: "command" },
3208
- ]
3209
- ```
3210
-
3211
- Each opens a control under its button in the object panel. **Add Text**
3212
- takes the text and a colour; **Add Image** takes an image file (sent through
3213
- `onTextureUpload` when you pass it, so the saved URL lasts; otherwise a
3214
- blob URL for this session). The decal appears automatically at the centre
3215
- of the camera-facing surface, ready to drag, resize, turn, or remove. **Move** enables dragging across
3216
- the object's surface; release to save, or press Escape to cancel. The
3217
- **Position** arrows move a decal in small steps relative to the current
3218
- view, without needing to grab it. Click for a fine adjustment or hold an
3219
- arrow for continuous movement. Each step stays on the nearby visible surface.
3220
- Every change is saved to the
3221
- binding's `decals` and reported through `onObjectBindingsChange`. While a
3222
- decal is in Move mode, a hint shows over the viewer and Escape cancels
3223
- the current drag without removing the centred decal.
3224
- - On a rigged (skinned) mesh a decal bends with the animation: it takes on
3225
- the bone weights of the surface under it and is driven by the same skeleton.
3226
- A click on a posed mesh is stored at the matching point of its rest shape, so
3227
- a decal lands where you clicked whatever pose the model is in. Over a joint
3228
- that bends sharply across few polygons, the decal's outer edge can sit a
3229
- hair off the surface. Decals don't follow morph targets (blend shapes).
3230
- - Decals need the binding's object to be a mesh, and aren't drawn on streamed
3231
- 3D Tiles models.
3232
-
3233
- ### Isolating objects
3234
-
3235
- `isolation` focuses the view on some objects: everything else is ghosted
3236
- (drawn faintly) or hidden, and clicks and hovers pass through to what's
3237
- isolated. Nothing about the bindings changes, and clearing the isolation puts
3238
- everything back. It's view state, like the selection, so it isn't part of
3239
- `sceneConfig`.
3240
-
3241
- ```tsx
3242
- const viewer = useViewer();
3243
-
3244
- <ModelViewer
3245
- modelUrl="/engine.glb"
3246
- licenseKey="your-license-key"
3247
- {...viewer.props}
3248
- tools={{ isolate: true }}
3249
- />;
3250
-
3251
- viewer.isolation.isolate({ group: "gearbox" }); // ghost everything else
3252
- viewer.isolation.isolate(["piston-1", "piston-2"], { mode: "hide" });
3253
- viewer.isolation.clearIsolation();
3254
- ```
3255
-
3256
- ```ts
3257
- type ViewerIsolation = {
3258
- target: ObjectBindingTarget; // an id, a list of ids, { group }, { tag }, or { type }
3259
- mode?: "ghost" | "hide"; // default "ghost"
3260
- ghostOpacity?: number; // 0–1, default 0.12
3261
- };
3262
-
3263
- isolation?: ViewerIsolation | null;
3264
- onIsolationChange?: (isolation: ViewerIsolation | null) => void;
3265
- showIsolateControls?: boolean; // tools.isolate
3266
- ```
3267
-
3268
- - Pass `isolation` (including `null`) to control it, and update it from
3269
- `onIsolationChange`; `useViewer` wires both, as `viewer.isolation`. Leave
3270
- `isolation` out and `ModelViewer` keeps it itself, clearing it when
3271
- `modelUrl` changes. `useViewerIsolation` is the same state on its own.
3272
- - `showIsolateControls` (`tools.isolate`, default `false`) adds an isolate
3273
- button to each row of the scene objects panel (click again to show all) and
3274
- a "Show all" pill at the bottom of the viewer while anything is isolated.
3275
- - An action can isolate with an `isolation` effect:
3276
- `{ isolation: { target: { group: "engine" } } }`, or `{ isolation: null }`
3277
- to show everything again. Pass `isolation` to `useViewerActions` for it
3278
- (`useViewer` does).
3279
- - A target that matches no object isolates nothing, rather than fading out
3280
- the whole model. Ghosting never raises an object's own lower opacity, and
3281
- style rules still apply underneath.
3282
- - `applyIsolation` (main entry and `/utils`) applies an isolation to bindings
3283
- the same way, for rendering your own lists.
3284
-
3285
- ### `showSectionTools`
3286
-
3287
- Shows a section-cut button over the canvas (`tools.section`). It cuts the model
3288
- open along an axis, hiding everything on one side of a plane, to look inside
3289
- assemblies, equipment, and buildings. Default `false`. Not available with
3290
- [`webgpu`](#webgpu).
3291
-
3292
- With the cut on, the toolbar picks the axis (X, Y, or Z), the position along
3293
- it, and which side stays, and a plane appears across the model that you can
3294
- drag. The cut is stored in `sceneConfig.section`, so you can also set it
3295
- yourself, author it in BindingBuilder, or switch it from an action:
3296
-
3297
- ```tsx
3298
- viewer.scene.updateSceneConfig({
3299
- section: { enabled: true, axis: "y", position: 0.4, flip: false }, // a floor-plan cut
3300
- });
3301
- ```
3302
-
3303
- ```ts
3304
- type SceneSectionConfig = {
3305
- enabled: boolean;
3306
- axis: "x" | "y" | "z"; // world axis the cut runs across
3307
- position: number; // 0 at the model's minimum along the axis, 1 at its maximum
3308
- flip: boolean; // keep the higher side instead of the lower one
3309
- };
3310
- ```
3311
-
3312
- - The cut applies to the model only; lights, helpers, and gizmos stay whole.
3313
- It also shows without the toolbar whenever `sceneConfig.section.enabled` is
3314
- set; the draggable plane appears only with `showSectionTools`.
3315
- - While the cut is on, the model's materials render both sides of each
3316
- surface, so a cut part shows its inside surfaces instead of disappearing.
3317
- There are no solid caps over the cut.
3318
- - Clicking and hovering ignore whatever is cut away, so the parts you can see
3319
- behind the cut are the ones you select.
3320
- - Toolbar changes go to `onSceneConfigChange` when you pass it (`useViewer`
3321
- does); otherwise `ModelViewer` keeps them until `sceneConfig.section` or
3322
- `modelUrl` changes.
3323
- - The measuring tool can still pick points on the hidden side of a cut.
3324
-
3325
- Default:
3326
-
3327
- ```ts
3328
- false;
3329
- ```
3330
-
3331
- ### `cinematic`
3332
-
3333
- Optional cinematic auto-camera. The camera glides along a path on its own, like a film, ideal for showing off a gallery, apartment, or product with no user interaction. Renders a play/pause button over the canvas; grabbing the camera (or selecting a part) pauses it.
3334
-
3335
- ```ts
3336
- cinematic?: boolean | CinematicConfig;
3337
-
3338
- type CinematicConfig = {
3339
- waypoints?: CinematicWaypoint[]; // camera keyframes to glide through
3340
- duration?: number; // seconds for one full pass (auto-scales when omitted)
3341
- loop?: boolean; // default true
3342
- autoPlay?: boolean; // start once the model has loaded and framed (default false); waits for the play button under reduced motion
3343
- };
3344
-
3345
- type CinematicWaypoint = {
3346
- position: [number, number, number];
3347
- target: [number, number, number]; // world-space look-at point
3348
- };
3349
- ```
3350
-
3351
- - Boolean `true` (or an object with fewer than two waypoints) gives a zero-config **showcase orbit** around the model's bounding sphere.
3352
- - Two or more `waypoints` author a **walkthrough**: the camera follows a smooth Catmull-Rom spline through them at a constant on-screen speed, looping seamlessly.
3353
- - Waypoints are easiest to capture visually in `BindingBuilder`'s **Cinematic** tab, which stores them on `sceneConfig.cinematic`.
3354
-
3355
- A path can also come from `sceneConfig.cinematic`. The `cinematic` prop overrides it: a boolean toggles the feature (`true` keeps any authored waypoints, `false` forces it off), and an object overrides field-by-field, so you can keep the authored waypoints while overriding, say, `autoPlay` or `loop`.
3356
-
3357
- ```tsx
3358
- // Zero-config showcase orbit
3359
- <ModelViewer cinematic ... />
3360
-
3361
- // Authored walkthrough
3362
- <ModelViewer
3363
- cinematic={{
3364
- waypoints: [
3365
- { position: [4, 2, 6], target: [0, 1, 0] },
3366
- { position: [-3, 2, 4], target: [0, 1, 0] },
3367
- { position: [-5, 2, -2], target: [0, 1, 0] },
3368
- ],
3369
- loop: true,
3370
- autoPlay: true,
3371
- }}
3372
- ...
3373
- />
3374
-
3375
- // Keep the BindingBuilder-authored path, just autoplay it
3376
- <ModelViewer sceneConfig={sceneConfig} cinematic={{ autoPlay: true }} ... />
3377
- ```
3378
-
3379
- Default: disabled.
3380
-
3381
- ### `enableXR`
3382
-
3383
- Optional boolean that enables XR entry points. On WebXR-capable browsers, the viewer shows a **"View in your space"** (AR) and/or **"Enter VR"** button once `navigator.xr.isSessionSupported(...)` resolves `true` for that mode. On iPhone/iPad, WebXR AR is not available, but if you also pass [`usdzUrl`](#usdzUrl), the viewer shows a **"View in AR"** button that launches Apple Quick Look instead.
3384
-
3385
- On entering a session the model is rested on the floor a short distance in front
3386
- of the viewer. By default it is normalized to ~1 m on its longest axis so
3387
- arbitrary uploads and room scenes are framed safely. Set
3388
- [`arScaleMode`](#arscalemode) to `"real-world"` for product AR, which preserves
3389
- the GLB's authored metre scale.
3390
-
3391
- **AR placement gestures**, inside an AR session the viewer now uses an explicit placement flow:
3392
-
3393
- - before placement, a bottom label says **"Move your device over a flat surface"** until a hit-testable surface is found
3394
- - once a surface is found, a reticle appears on it and the label changes to **"Tap to place"**
3395
- - tapping the screen or the label places the model at the reticle
3396
- - after placement, **pinch** (two fingers) resizes the model and **twist** rotates it around its vertical axis without changing its position
3397
- - a bottom **"Relocate"** button lets the user press-and-hold to bring the reticle back; releasing the button places the model at the current reticle position
3398
-
3399
- VR sessions keep the initial framed placement (there are no real surfaces to hit-test against).
3400
-
3401
- > Postprocessing is automatically disabled during an XR session (the effect composer isn't WebXR-aware). Note also that on low-power devices the default [`performanceProfile`](#performanceprofile) of `"auto"` already disables postprocessing and soft shadows, important for keeping the WebGL context alive so AR can start at all.
3402
-
3403
- Default:
3404
-
3405
- ```ts
3406
- false;
3407
- ```
3408
-
3409
- ### `usdzUrl`
3410
-
3411
- Optional `USDZ` URL used as an Apple-device fallback when `enableXR` is `true`.
3412
- On iPhone/iPad, if WebXR AR is unavailable and this prop is provided, the viewer
3413
- shows a **"View in AR"** button that launches Apple Quick Look for the given
3414
- asset.
3415
-
3416
- ```ts
3417
- usdzUrl?: string;
3418
- ```
3419
-
3420
- Notes:
3421
-
3422
- - this should point to a `.usdz` file that Safari can fetch directly
3423
- - Android / WebXR-capable devices ignore this and continue using the normal WebXR AR flow
3424
- - if `usdzUrl` is omitted, Apple handheld devices simply won't get an AR button
3425
-
3426
- ### `arScaleMode`
3427
-
3428
- Controls the initial model size inside WebXR AR and VR sessions.
3429
-
3430
- ```ts
3431
- arScaleMode?: "normalized" | "real-world";
3432
- ```
3433
-
3434
- - `"normalized"` (default) fits the longest model dimension to approximately
3435
- 1 metre. Use this for arbitrary uploads and room-scale scenes.
3436
- - `"real-world"` preserves the GLB's authored scale. glTF units are metres, so
3437
- use this for product models exported at their real dimensions.
3438
-
3439
- Users can still pinch to resize after placement in either mode.
3440
-
3441
- ### `showAnnotationNavigation`
3442
-
3443
- Optional boolean that shows the "Guided Tour" control cluster when the model has at least one annotation: a single "Guided Tour" button that, once started, becomes Previous/Stop/Next controls for stepping through annotations in order.
3444
-
3445
- Default:
3446
-
3447
- ```ts
3448
- true;
3449
- ```
3450
-
3451
- ### `showAnnotationOnHover`
3452
-
3453
- Optional boolean that temporarily opens an annotation's detail popup when the
3454
- pointer hovers over its marker. Moving the pointer away closes the hover popup
3455
- after a short grace period. Clicking a marker still locks its popup open until
3456
- it is closed.
3457
-
3458
- Default:
3459
-
3460
- ```ts
3461
- false;
3462
- ```
3463
-
3464
- ```tsx
3465
- <ModelViewer {...props} showAnnotationOnHover />
3466
- ```
3467
-
3468
- ### Theming the scene objects / object binding panels
3469
-
3470
- The scene objects panel and object binding data panel read their colors,
3471
- borders, shadows, and fonts from CSS custom properties (`--ri-sidepanel-*`
3472
- and `--ri-databinding-*`) instead of hardcoded values, each with the current
3473
- look baked in as a fallback. Override them on a `.ri-viewer` ancestor to
3474
- re-skin those panels:
3475
-
3476
- ```css
3477
- .my-app .ri-viewer {
3478
- --ri-sidepanel-bg: #1a0033;
3479
- --ri-sidepanel-accent: #ff2d75;
3480
- }
3481
- ```
3482
-
3483
- See the full variable list in the [`ModelViewer` docs](https://react-immersive.liveroom.dev/docs/reference/model-viewer#theming).
3484
-
3485
- ## `SimpleModelViewer` Public API
3486
-
3487
- ```ts
3488
- type SimpleModelViewerProps = {
3489
- modelUrl: string;
3490
- poster?: string; // see ModelViewer's poster
3491
- loading?: "lazy" | "eager"; // default "lazy"
3492
- reveal?: "auto" | "interaction"; // default "auto"
3493
- modelFormat?: "glb" | "gltf" | "obj" | "fbx" | "usdz" | "stl" | "ply";
3494
- dracoDecoderPath?: string | false;
3495
- ktx2TranscoderPath?: string | false;
3496
- meshopt?: boolean;
3497
- backgroundColor?: string;
3498
- showSceneObjectsPanel?: boolean;
3499
- showSceneSettingsPanel?: boolean;
3500
- enableModelUpload?: boolean;
3501
- zoomOnSelected?: boolean;
3502
- highlightOnHover?: boolean;
3503
- showLoadingOverlay?: boolean;
3504
- refitOnResize?: boolean;
3505
- reducedMotion?: "user" | "always" | "never"; // see ModelViewer's reducedMotion
3506
- // Initial values for the scene settings (environment) panel
3507
- backgroundEnabled?: boolean;
3508
- autoRotate?: boolean;
3509
- envPreset?: PresetsType; // @react-three/drei environment preset
3510
- exposure?: number;
3511
- ambientIntensity?: number;
3512
- ambientColor?: string;
3513
- directionalIntensity?: number;
3514
- directionalColor?: string;
3515
- onModelLoaded?: (model: ViewerModelInfo) => void;
3516
- onLoadError?: (error: unknown) => void;
3517
- };
3518
- ```
3519
-
3520
- `SimpleModelViewer` does not take a `licenseKey`, licensing is enforced by `ModelViewer` and `BindingBuilder` only.
3521
-
3522
- ### `modelUrl`
3523
-
3524
- URL or public path to the `.glb`, `.gltf`, `.obj`, `.fbx`, `.usdz`, `.stl`, or `.ply` model to inspect. Hosted assets must serve external dependencies at their declared relative paths; USDZ dependencies are packaged internally. Use `modelFormat` when a signed or extensionless URL cannot be auto-detected.
3525
-
3526
- ### `modelFormat`
3527
-
3528
- Optional explicit format for signed or extensionless model URLs. Normal URLs
3529
- ending in a supported extension are detected automatically.
3530
-
3531
- ### `poster` / `loading` / `reveal`
3532
-
3533
- Work as on [`ModelViewer`](#lazy-loading-and-posters), sharing the same
3534
- page-wide count of live 3D views. When a `SimpleModelViewer` is put away, only
3535
- its canvas goes: the panels and their settings stay, and the canvas comes back
3536
- with its camera, hidden objects and selection.
3537
- `onModelLoaded` runs again when it does.
3538
-
3539
- ### `backgroundColor`
3540
-
3541
- Optional canvas background color.
3542
-
3543
- Default:
3544
-
3545
- ```ts
3546
- "#1a1a1a";
3547
- ```
3548
-
3549
- ### `showSceneObjectsPanel`
3550
-
3551
- Optional boolean that controls whether the right-side scene objects panel is rendered.
3552
-
3553
- Default:
3554
-
3555
- ```ts
3556
- true;
3557
- ```
3558
-
3559
- ### `showSceneSettingsPanel`
3560
-
3561
- Optional boolean that controls whether the left-side environment/scene settings panel is rendered. It exposes background color/visibility, the `@react-three/drei` environment preset, auto-rotate, exposure, and ambient/directional light color and intensity.
3562
-
3563
- Default:
3564
-
3565
- ```ts
3566
- true;
3567
- ```
3568
-
3569
- ### Scene settings defaults
3570
-
3571
- The following optional props seed the initial values of the scene settings (environment) panel. Each maps to one control, and the panel stays interactive so the user can still adjust them at runtime. They are initial values (not controlled props): changing one after mount does not override a value the user has since changed in the panel, the exception is `backgroundColor`, which stays in sync live.
3572
-
3573
- | Prop | Type | Panel control | Default |
3574
- | ---------------------- | ------------- | ----------------------- | ----------- |
3575
- | `backgroundEnabled` | `boolean` | Background on/off | `false` |
3576
- | `autoRotate` | `boolean` | Auto Rotate | `false` |
3577
- | `envPreset` | `PresetsType` | Environment preset | `"city"` |
3578
- | `exposure` | `number` | Exposure | `1` |
3579
- | `ambientIntensity` | `number` | Ambient Light Intensity | `0.6` |
3580
- | `ambientColor` | `string` | Ambient Light Color | `"#ffffff"` |
3581
- | `directionalIntensity` | `number` | Direct Light Intensity | `1.2` |
3582
- | `directionalColor` | `string` | Direct Light Color | `"#ffffff"` |
3583
-
3584
- `envPreset` accepts any `@react-three/drei` `PresetsType` (for example `"city"`, `"sunset"`, `"dawn"`, `"night"`, `"warehouse"`, `"forest"`, `"apartment"`, `"studio"`, `"park"`, `"lobby"`).
3585
-
3586
- ```tsx
3587
- <SimpleModelViewer
3588
- modelUrl="/model.glb"
3589
- backgroundEnabled
3590
- envPreset="sunset"
3591
- exposure={1.2}
3592
- ambientIntensity={0.4}
3593
- directionalColor="#fff4e0"
3594
- autoRotate
3595
- />
3596
- ```
3597
-
3598
- ### `zoomOnSelected`
3599
-
3600
- Optional boolean. When `true`, selecting a mesh (via click or the objects panel) zooms/frames the camera on it.
3601
-
3602
- Default:
3603
-
3604
- ```ts
3605
- false;
3606
- ```
3607
-
3608
- ### `highlightOnHover`
3609
-
3610
- Optional boolean. When `true`, hovering a mesh (in the viewport or the objects panel) highlights it.
3611
-
3612
- Default:
3613
-
3614
- ```ts
3615
- false;
3616
- ```
3617
-
3618
- ### `showLoadingOverlay`
3619
-
3620
- Optional boolean controlling whether the built-in loading overlay is shown while the model is loading, uploaded files are being prepared, and the initial camera fit is settling.
3621
-
3622
- Default:
3623
-
3624
- ```ts
3625
- true;
3626
- ```
3627
-
3628
- ### `refitOnResize`
3629
-
3630
- 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 (scene settings / scene objects) opening or closing. 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.
3631
-
3632
- Default:
3633
-
3634
- ```ts
3635
- true;
3636
- ```
3637
-
3638
- ### `enableModelUpload`
3639
-
3640
- Optional boolean that swaps the fixed `modelUrl` workflow for a built-in upload UI. It accepts `.glb`, standalone/data-URI `.gltf`, `.obj`, `.fbx`, `.usdz`, `.stl`, `.ply`, or a `.zip` bundle. ZIP bundles may contain a GLTF with its buffers/textures, an OBJ with its optional MTL/textures, or an FBX with external textures. A USDZ file is already a self-contained package and is uploaded directly.
3641
-
3642
- Default:
3643
-
3644
- ```ts
3645
- false;
3646
- ```
3647
-
3648
- ### `onModelLoaded`
3649
-
3650
- Optional callback fired after the model has loaded, with its bounds and mesh
3651
- count (`(model: ViewerModelInfo) => void`, the same payload as `ModelViewer`'s).
3652
-
3653
- ### `onLoadError`
3654
-
3655
- Optional callback fired if the model fails to load or render.
3656
-
3657
- ## Object Binding Shape
3658
-
3659
- The current source defines these binding types internally:
3660
-
3661
- ```ts
3662
- type ObjectBindingAction = {
3663
- id: string;
3664
- label: string;
3665
- type: "command";
3666
- };
3667
-
3668
- type ObjectBindingCameraState = {
3669
- position?: [number, number, number];
3670
- target?: [number, number, number];
3671
- fov?: number;
3672
- zoom?: number;
3673
- };
3674
-
3675
- type ObjectBindingMaterial = {
3676
- // exported by the package; includes texture/baseColor plus the supported
3677
- // MeshPhysicalMaterial overrides used by the viewer and BindingBuilder
3678
- };
3679
-
3680
- type ObjectBindingStyle = {
3681
- material?: ObjectBindingMaterial;
3682
- };
3683
-
3684
- type ObjectBindingTransform = {
3685
- position?: [number, number, number];
3686
- rotation?: [number, number, number]; // radians
3687
- scale?: [number, number, number];
3688
- };
3689
-
3690
- type ObjectBinding = {
3691
- id: string;
3692
- type?: ObjectBindingType; // any string; suggestions: "body" | "light" | "wheel" | "wall" | "door" | "furniture" | ...; default "other"
3693
- modelObjectId: string;
3694
- label?: string;
3695
- group?: string;
3696
- tags?: string[];
3697
- visible?: boolean;
3698
- selectable?: boolean;
3699
- hoverable?: boolean;
3700
- transform?: ObjectBindingTransform;
3701
- style?: ObjectBindingStyle;
3702
- decals?: ObjectBindingDecal[];
3703
- actions?: ObjectBindingAction[];
3704
- metrics?: Record<string, number>;
3705
- metadata?: Record<string, unknown>;
3706
- cameraState?: ObjectBindingCameraState;
3707
- };
3708
- ```
3709
-
3710
- Notes:
3711
-
3712
- - the `objectBindings` record is keyed by node name, not by `id`
3713
- - `group` and `tags` can be assigned in `BindingBuilder` and used as targets by binding helpers and runtime material or transform effects
3714
- - `visible` is live render state, not just an initial default
3715
- - `transform` is persistent local-space render state; position values use model units and rotation values use radians
3716
- - the transform gizmo is displayed at the center of the object's renderable geometry for intuitive rotation, even if the authored node origin is elsewhere; this does not rewrite the GLB/GLTF origin
3717
- - missing transform fields retain their model-authored values, and removing `transform` restores the authored transform
3718
- - `style.material.baseColor` is read by the renderer and is the committed source of truth for color picks
3719
- - `style.material.texture.path` is read by the renderer and is the committed source of truth for texture uploads (stored as durable URLs via `onTextureUpload` when provided, or session-scoped blob URLs otherwise)
3720
- - if no `texture.path` is provided and no `style.material.baseColor` is set, the viewer falls back to the node's original GLB/GLTF material
3721
- - supported `style.material` fields are applied as per-object `MeshPhysicalMaterial` overrides, including metalness, roughness, emissive, opacity, alpha masking, normal/bump, AO, displacement, clearcoat, sheen, anisotropy, specular, transmission, thickness, attenuation tint/distance, reflectivity, and side selection
3722
- - each `style.material` group also accepts a texture-map slot (`metalnessMap`, `roughnessMap`, `normalMap`, `aoMap`, `clearcoatMap`, etc.); color maps are decoded as sRGB and data maps as linear automatically
3723
- - `BindingBuilder` intentionally omits exact Sketchfab glossiness, cavity, and subsurface-scattering workflows; those require preprocessing or a different material pipeline
3724
- - `cameraState` stores a saved camera view for the object; when both `position` and `target` are set, the viewer uses that view when the object is focused or selected instead of falling back to `fitToBox`
3725
- - `metrics` and `metadata` are displayed in the side panel as nested key/value fields (objects expand into indented child labels)
3726
- - `type` groups and filters bindings (for example `hideObject({ type: "wheel" })`). Any string works: `ObjectBindingType` suggests values for vehicles, rooms, furniture, characters, weapons, environment, and more, but a domain it doesn't cover can use its own (`"bone"`, `"turbine-blade"`). Left out, it counts as `"other"`
3727
-
3728
- ## Supported Built-In Actions
3729
-
3730
- `ModelViewer` handles eight action ids itself: `change-color`,
3731
- `change-material`, `add-text`, `add-image`, `toggle-visibility`,
3732
- `toggle-light`, `change-light-color`, and `set-brightness`. Any other id does something only when you handle it in
3733
- `onAction` (pass `useViewerActions().handleAction` there to run an action's
3734
- declared `effects`). If a binding has such an action and `ModelViewer` has no
3735
- `onAction`, its button does nothing, and the viewer logs a console warning
3736
- naming each one. Every action also reaches `onAction`, after the viewer's own
3737
- handling.
3738
-
3739
- `ACTION_CATALOG` lists the ids `BindingBuilder` offers, and whether the viewer
3740
- or your app handles each one (`handling: "viewer" | "host"`):
3741
-
3742
- ```ts
3743
- import { ACTION_CATALOG, findCatalogAction } from "@liveroom-tech/react-immersive/utils";
3744
-
3745
- findCatalogAction("toggle-light")?.handling; // "viewer"
3746
- ```
3747
-
3748
- ### Implemented behaviors
3749
-
3750
- - `add-text`
3751
- Opens a text box and colour inline beneath the action; the text appears at the centre of the camera-facing surface, ready to drag, resize, turn, or remove. Saved to `binding.decals` (see [Decals](#decals))
3752
- - `add-image`
3753
- Opens an image chooser inline beneath the action; the image is placed and edited the same way. Uses `onTextureUpload` for a durable URL when provided, otherwise a session-scoped blob URL
3754
- - `toggle-visibility`
3755
- Hides or shows the selected mesh by setting `binding.visible` and calling `onObjectBindingsChange`
3756
- - `change-color`
3757
- Opens the color picker inline beneath the action; the chosen hex is applied as a live preview while dragging and committed into `binding.style.material.baseColor` via `onObjectBindingsChange` when the picker closes
3758
- - `change-material`
3759
- Opens the texture upload panel inline beneath the action for JPG, PNG, and WebP files; if `onTextureUpload` is provided, the file is passed to it and the returned URL is stored in `binding.style.material.texture.path`; otherwise a session-scoped blob URL is used as the fallback
3760
- - `toggle-light`
3761
- Turns the object's light off, or back on
3762
- - `change-light-color`
3763
- Opens a color picker inline beneath the action that sets the light's color
3764
- - `set-brightness`
3765
- Opens a slider inline beneath the action that sets the light's brightness, from 0 to 200% of what it was when it opened
3766
-
3767
- What "the object's light" is depends on the scene:
3768
-
3769
- - If scene lights are attached to the object (in `BindingBuilder`'s Lighting
3770
- tab, or with `useSceneConfig().lights.attach`), the actions control those
3771
- lights, which light up the room around it. The changes go to
3772
- `sceneConfig.lighting`: to `onSceneConfigChange` if you pass it (`useViewer`
3773
- does, so `viewer.scene` stays current), otherwise `ModelViewer` keeps them
3774
- itself, over whatever `sceneConfig` it's given.
3775
- - Otherwise they control the object's own glow: its emissive color and strength,
3776
- saved on the binding as `style.material.emissive` and
3777
- `style.material.emissiveIntensity`, like any other edit. This works on any
3778
- light fixture with no setup, but a glow doesn't light up the objects around
3779
- it. Toggling off and back on restores the previous strength.
3780
-
3781
- ## How Rendering Works
3782
-
3783
- At a high level, the current viewer does the following:
3784
-
3785
- 1. creates a Three.js canvas; only `fov: 50` is defaulted, so camera position falls back to React Three Fiber's `Canvas` default (`[0, 0, 5]`) unless `camera.position` is passed
3786
- 2. loads the GLB/GLTF asset with `useGLTF`
3787
- 3. looks up meshes by the keys in `objectBindings` (resolving `modelObjectId` to find the actual model node)
3788
- 4. clones each referenced material so edits stay isolated per object (keyed on a structural signature of the binding-to-mesh mapping, not the `objectBindings` reference, so materials are not recreated on every binding update)
3789
- 5. applies runtime color, texture, and style overrides from `objectBindings` in a `useEffect` (not during render)
3790
- 6. manages a reference-counted texture cache so the same texture URL is decoded once and disposed when no longer referenced; each texture is decoded in the correct color space, color maps (base color/albedo, emissive, sheen color, specular color) as sRGB and data maps (normal, roughness, metalness, AO, displacement, etc.) as linear, and the cache is keyed by URL **and** color space, so the same image can be reused safely as both a color and a data map
3791
- 7. adds hover, selected, and panel-hover emissive highlighting
3792
- 8. creates an `AnimationMixer` for the loaded scene, ticked every frame via `useFrame`, and exposes playback controls through `onAnimationsReady`
3793
- 9. renders a side panel for the selected object and its configured actions
3794
-
3795
- Only objects listed in `objectBindings` are rendered interactively by the current `Model` component.
3796
-
3797
- ### Lighting, shadows, and post-processing
3798
-
3799
- When `shadows` is enabled (the default), the canvas renders soft shadow maps and the viewer applies:
3800
-
3801
- - a default light rig (`Lights`) with ambient + hemisphere fill and a directional key light that casts shadows, sized to the model
3802
- - per-mesh `castShadow`/`receiveShadow`, with one exception: the large enclosing "shell" mesh (walls/ceiling of an interior model) is detected by its bounding-box size and excluded from **casting** so a top-down light still reaches the interior, it continues to **receive** shadows, so furniture casts visible contact shadows onto the floor and walls
3803
- - a post-processing stack (`EffectComposer`) with SSAO ambient occlusion (normal pass enabled), bloom, vignette, and ACES filmic tone mapping
3804
-
3805
- Pass `shadows={false}` to disable shadow rendering entirely, or set your own lights in `sceneConfig.lighting` (see [Lighting](#lighting)) to replace the default rig.
3806
-
3807
- ## UI Behavior
3808
-
3809
- The current component includes its own UI shell:
3810
-
3811
- - a viewer layout that fills its parent container
3812
- - a sliding left side panel about `280px` wide (collapses to half-width on narrow screens so the canvas keeps its other half)
3813
- - a canvas area that fills the remaining width
3814
- - a color picker that expands inline beneath the **Change Color** action
3815
- - a texture upload panel that expands inline beneath the **Change Material** action
3816
- - a right scene objects panel with search and per-object visibility toggles
3817
-
3818
- Because of that, it works best when mounted in a container with an explicit height.
3819
-
3820
- ## Usage Expectations For Models
3821
-
3822
- To use this library successfully, your model asset should follow these expectations:
3823
-
3824
- - the mesh node names must match the keys in `objectBindings` (or be referenced via `modelObjectId`)
3825
- - target nodes should have geometry and a supported Three.js mesh material; imported materials are normalized into the editable PBR pipeline
3826
- - the file should be accessible from the browser at `modelUrl`; hosted GLTF, OBJ, and FBX assets must also serve external dependencies at their declared relative paths
3827
- - OBJ, STL, and PLY do not contain animations; supported FBX and USDZ animation clips are exposed through the same animation controls as glTF
3828
- - OBJ, STL, and PLY have no standard unit metadata, so set an appropriate measurement label/scene scale when real-world dimensions matter
3829
-
3830
- If a node name or material name does not match, that object will not render through the interactive binding flow.
3831
-
3832
- ### Large models
3833
-
3834
- Viewport picking (click/hover) is accelerated by a BVH index built in a
3835
- background worker, so even multi-million-triangle models stay responsive to
3836
- the pointer. Models above roughly **12 million triangles** are too large to
3837
- index without a significant memory cost, and the viewer degrades gracefully
3838
- instead of freezing:
3839
-
3840
- - viewport click/hover picking is disabled (`onObjectSelect` / `onObjectHover`
3841
- do not fire from the canvas); selection through the scene objects panel
3842
- still works
3843
- - a dismissible banner tells the user the model is very large, may render
3844
- slowly on their device, and that selection is available through the panel
3845
- - the loading overlay calls out the extra work, uploading that much geometry
3846
- to the GPU is a single synchronous operation that can block the page for
3847
- tens of seconds in any WebGL viewer
3848
-
3849
- Orbit, zoom, animations, annotations, and the panels keep working. There is no
3850
- viewer-side way to make models of this size feel fast, the per-frame cost is
3851
- inherent, and on mobile devices the decoded geometry alone can exceed the
3852
- tab's memory budget. Decimate such assets at ingestion (to roughly 2–3M
3853
- triangles) instead of shipping the raw export; note that very large
3854
- Draco-compressed files need Blender or native tooling to decimate, the WASM
3855
- builds of `gltf-transform` and `gltfpack` cannot decode them within their 4GB
3856
- address space.
3857
-
3858
- Two related picking notes that apply to models of every size:
3859
-
3860
- - clicks resolve the front-most surface under the pointer; a mesh with
3861
- `selectable: false` does not pass the click through to meshes behind it
3862
- - `SimpleModelViewer` renders on demand, an idle viewer draws no frames, so
3863
- it costs no GPU time or battery until something changes
3864
-
3865
- ## License
3866
-
3867
- Proprietary, see [LICENSE](./LICENSE). Installing the package does not itself grant a right to use it in production; a valid `licenseKey` from the [Developer Portal](https://react-immersive.liveroom.dev) is required, and usage is bound to the terms of your license tier.
3868
-
3869
- ### Rich annotations
3870
-
3871
- `sceneConfig.annotations` supports optional `cameraView` (position, target,
3872
- orbit angles, fov and zoom), `images: [{ src, alt }]`, `links: [{ href, label }]`,
3873
- `category`, and `minDistance` / `maxDistance` in scene units. BindingBuilder's
3874
- Annotations tab authors these fields; new markers use automatic close-up tour
3875
- framing. Explicitly capture a camera view for custom framing. Marker selection
3876
- and guided tours restore saved views when provided. The viewer's category chips
3877
- filter markers and tour steps.
3878
- Use `annotationCategories` (`null` = all, `[]` = none) and
3879
- `onAnnotationCategoriesChange` for controlled filtering, and
3880
- `ui.annotationFilters: false` to hide the built-in chips. Distance limits affect
3881
- markers only, so their content remains available through tours. Images stay at
3882
- their HTTP(S) or relative URLs and are not bundled into exports automatically.
3883
-
3884
- ## Translatable viewer text
3885
-
3886
- Both viewers accept a partial `labels` dictionary. Omitted keys use English; changing labels updates text without reloading the model. Named placeholders such as `{count}` can be reordered in translations.
3887
-
3888
- ```tsx
3889
- <ModelViewer
3890
- licenseKey="your-license-key"
3891
- modelUrl="/car.glb"
3892
- labels={{ guidedTour: "Visite guidée", tapToPlace: "Touchez pour placer", visibleEntries: "{count} éléments visibles" }}
3893
- />
3894
- ```
3895
-
3896
- `ModelViewerLabels`, `ModelViewerLabelKey`, and `DEFAULT_MODEL_VIEWER_LABELS` are exported from the main package, `/simple-model-viewer`, and `/utils`. See the [full catalog](https://react-immersive.liveroom.dev/docs/reference/labels). Annotation content, metadata, and custom panels use your application's translations.
97
+ See [Getting Started](https://react-immersive.liveroom.dev/docs/getting-started)
98
+ for a complete bindings example. Use `onObjectCatalog` or BindingBuilder to
99
+ obtain actual model-node identifiers. `defineObjectBindings` preserves literal
100
+ IDs; widen mutable React state to `Record<string, ObjectBinding>` when passing a
101
+ state setter to `onObjectBindingsChange`.
102
+
103
+ Pure data helpers, including `defineObjectBindings`, `patchBindings`,
104
+ `patchSceneConfig`, and `selectBindings`, are available from the server-safe
105
+ `@liveroom-tech/react-immersive/utils` entry.
106
+
107
+ ## Find the API for your task
108
+
109
+ - [ModelViewer](https://react-immersive.liveroom.dev/docs/reference/model-viewer.md): props, bindings, materials, annotations, exports, XR, performance.
110
+ - [SimpleModelViewer](https://react-immersive.liveroom.dev/docs/reference/simple-model-viewer.md): inspection, uploads, lighting and callbacks.
111
+ - [BindingBuilder](https://react-immersive.liveroom.dev/docs/reference/binding-builder.md): authoring, exports and import workflows.
112
+ - [useViewer](https://react-immersive.liveroom.dev/docs/hooks/use-viewer.md): connect controllers to the viewer.
113
+ - [useObjectBindings](https://react-immersive.liveroom.dev/docs/hooks/use-object-bindings.md): editable state, selectors, materials and visibility.
114
+ - [useViewerCamera](https://react-immersive.liveroom.dev/docs/hooks/use-viewer-camera.md): camera focus, reset and saved views.
115
+ - [useSceneConfig](https://react-immersive.liveroom.dev/docs/hooks/use-scene-config.md): lighting, environment and scene settings.
116
+ - [Types](https://react-immersive.liveroom.dev/docs/reference/types.md) and [labels](https://react-immersive.liveroom.dev/docs/reference/labels.md).
117
+ - [Manifest](https://react-immersive.liveroom.dev/docs/manifest.json): supported version/peers, all page URLs and small API section exports.
118
+ - [Extended package reference](https://react-immersive.liveroom.dev/docs/package-reference.md): detailed examples retained from the former long README.
119
+ - [Troubleshooting](https://react-immersive.liveroom.dev/docs/troubleshooting.md) and [migration notes](https://react-immersive.liveroom.dev/docs/releases.md).
120
+
121
+ ## License and keys
122
+
123
+ This is proprietary software, commercially maintained by Liveroom. The core
124
+ source is private; documentation, starters, issues and discussions are public.
125
+ See the [package license](https://react-immersive.liveroom.dev/LICENSE.txt),
126
+ also included as `LICENSE` in the npm package, and the
127
+ [licensing guide](https://react-immersive.liveroom.dev/docs/licensing).
128
+
129
+ ModelViewer and BindingBuilder require a runtime `licenseKey`. Create one in the
130
+ [console](https://react-immersive.liveroom.dev/console). Local development and
131
+ production domains are governed by the selected plan. SimpleModelViewer has no
132
+ runtime key prop and needs no key or paid plan, including in production.
133
+ The remaining proprietary package terms still apply.
134
+
135
+ `VITE_LICENSE_KEY` and `NEXT_PUBLIC_LICENSE_KEY` are browser-visible values.
136
+ Use a key for the intended registered domain and keep keys out of public
137
+ repositories. Never put account credentials, payment tokens, or server API
138
+ secrets in client-exposed environment variables.
139
+
140
+ ## Support
141
+
142
+ Report reproducible issues with the package version, framework, browser and
143
+ sanitized error text in the
144
+ [public issue tracker](https://github.com/Liveroom-Technologies/react-immersive-docs/issues).
145
+ Send private models, license/account requests and security reports to
146
+ [developers@liveroom.xyz](mailto:developers@liveroom.xyz).