@liveroom-tech/react-immersive 1.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.
- package/LICENSE +20 -0
- package/README.md +1730 -0
- package/dist/index.d.mts +672 -0
- package/dist/index.d.ts +672 -0
- package/dist/index.js +15912 -0
- package/dist/index.mjs +16007 -0
- package/package.json +83 -0
package/README.md
ADDED
|
@@ -0,0 +1,1730 @@
|
|
|
1
|
+
# `@liveroom-tech/react-immersive`
|
|
2
|
+
|
|
3
|
+
`@liveroom-tech/react-immersive` is a React-based 3D model viewer for interactive GLB/GLTF 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.
|
|
4
|
+
|
|
5
|
+
## What It Does
|
|
6
|
+
|
|
7
|
+
The library currently exports:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import {
|
|
11
|
+
BindingBuilder,
|
|
12
|
+
ModelViewer,
|
|
13
|
+
SimpleModelViewer,
|
|
14
|
+
useObjectBinding,
|
|
15
|
+
useObjectBindingIds,
|
|
16
|
+
useObjectVisibility,
|
|
17
|
+
useViewerActions,
|
|
18
|
+
useViewerAnimations,
|
|
19
|
+
useViewerCamera,
|
|
20
|
+
useViewerHover,
|
|
21
|
+
useViewerModel,
|
|
22
|
+
useViewerSelection,
|
|
23
|
+
MATERIAL_BLENDING_MODES,
|
|
24
|
+
MATERIAL_SIDES,
|
|
25
|
+
} from "@liveroom-tech/react-immersive";
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
It also exports types for `ObjectBinding`, `ObjectBindingMaterial`, `ObjectActionEvent`, `SceneConfig`, `AnimationControls`, and related shapes used throughout this document.
|
|
29
|
+
|
|
30
|
+
`ModelViewer` provides:
|
|
31
|
+
|
|
32
|
+
- GLB/GLTF model rendering through `@react-three/fiber` and `@react-three/drei`
|
|
33
|
+
- orbit/pan/zoom camera controls via `CameraControls`
|
|
34
|
+
- 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
|
|
35
|
+
- optional custom scene lighting through a `lights` prop
|
|
36
|
+
- optional custom camera configuration through a `camera` prop
|
|
37
|
+
- optional custom background color through a `backgroundColor` prop
|
|
38
|
+
- optional shadow rendering toggle through a `shadows` prop (on by default)
|
|
39
|
+
- optional on-screen movement controller through `showMouseController` and its tuning props
|
|
40
|
+
- optional custom left object data panel through `customObjectBindingDataPanel`
|
|
41
|
+
- optional custom right scene objects panel through `customSceneObjectsPanel`
|
|
42
|
+
- optional hiding of either built-in panel through `showObjectBindingDataPanel` and `showSceneObjectsPanel`
|
|
43
|
+
- optional model export button through `showDownloadButton` and `downloadFilename`, plus independent `showResetButton` / `showDownloadButtons` toggles for the rest of that action bar
|
|
44
|
+
- click-to-measure distance tool and a bounding-box dimensions overlay through `showMeasureTools` and `measurementUnit`
|
|
45
|
+
- a guided-tour control cluster (Previous/Stop/Next) for stepping through annotations, toggleable via `showAnnotationNavigation`
|
|
46
|
+
- a `sceneConfig` prop accepting the same scene-wide config (lighting, environment, background, post-processing, animations, annotations) authored by `BindingBuilder`'s Scene tab
|
|
47
|
+
- render-loop and perf tuning through `renderMode`, `maxDpr`, `performanceProfile`, and compressed-asset decoder options (`dracoDecoderPath`, `ktx2TranscoderPath`, `meshopt`)
|
|
48
|
+
- 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
|
|
49
|
+
- mesh selection by object name
|
|
50
|
+
- a built-in left side panel for the selected object
|
|
51
|
+
- object-specific action buttons driven by `objectBindings`
|
|
52
|
+
- built-in color picking that commits the chosen hex into `objectBindings.style.material.baseColor`
|
|
53
|
+
- 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)
|
|
54
|
+
- per-object `MeshPhysicalMaterial` overrides for metalness, roughness, emissive, normal/bump, AO, displacement, clearcoat, sheen, anisotropy, specular, transmission, thickness, reflectivity, sidedness, and limited blending modes
|
|
55
|
+
- per-object visibility toggling through `objectBindings.visible`
|
|
56
|
+
- hover and selected-state highlighting
|
|
57
|
+
- external selection state control through `selectedObject` and `onObjectSelect`
|
|
58
|
+
- hover callbacks through `onObjectHover`
|
|
59
|
+
- model-ready callbacks through `onModelLoaded`
|
|
60
|
+
- model-load error callbacks through `onLoadError`
|
|
61
|
+
- camera change callbacks through `onCameraChange`
|
|
62
|
+
- viewer-ready callbacks through `onViewerReady`
|
|
63
|
+
- action event callbacks through `onAction`
|
|
64
|
+
- animation playback for GLB/GLTF models with embedded animations through `onAnimationsReady`
|
|
65
|
+
- annotation marker callbacks through `onAnnotationsChange` and controlled/uncontrolled `activeAnnotation` / `onActiveAnnotationChange`
|
|
66
|
+
|
|
67
|
+
`BindingBuilder` provides:
|
|
68
|
+
|
|
69
|
+
- a design-time UI for generating starter bindings from GLB/GLTF assets, gated by `licenseKey` (see "`BindingBuilder` licensing & plan tiers" below)
|
|
70
|
+
- demo model loading or custom `.glb`, `.gltf`, or zipped GLTF upload
|
|
71
|
+
- editable binding fields for identity, basics, style, actions, metrics, metadata, and saved camera state
|
|
72
|
+
- a one-click material preset gallery (wood, metal, chrome, glass, plastic, fabric, ceramic, concrete, etc.)
|
|
73
|
+
- 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
|
|
74
|
+
- a Scene tab for the same `sceneConfig` shape `ModelViewer` accepts (lighting, environment, background, post-processing, animations, annotations)
|
|
75
|
+
- import a previously exported `objectBindings.json` or `sceneConfig.json` and merge it back onto the current model/config
|
|
76
|
+
- live preview using `ModelViewer`
|
|
77
|
+
- 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
|
|
78
|
+
- license-tier gating for some editor features (texture maps, environment lighting/backgrounds, wireframe preview, animation configuration, annotations, post-processing)
|
|
79
|
+
|
|
80
|
+
`SimpleModelViewer` provides:
|
|
81
|
+
|
|
82
|
+
- a lightweight GLB/GLTF viewer that does not require `objectBindings` and does not require a `licenseKey`
|
|
83
|
+
- optional local `.glb`, standalone/data-URI `.gltf`, or zipped GLTF upload mode for ad-hoc inspection without relying on the `modelUrl` asset
|
|
84
|
+
- built-in scene objects panel with search and visibility toggles
|
|
85
|
+
- 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
|
|
86
|
+
- click-to-select and fit-to-object focus behavior
|
|
87
|
+
- optional background color and model lifecycle callbacks
|
|
88
|
+
- a smaller API surface for simple inspection use cases
|
|
89
|
+
|
|
90
|
+
## Why `objectBindings` Exists
|
|
91
|
+
|
|
92
|
+
`objectBindings` solves the gap between a raw GLB/GLTF file and an interactive
|
|
93
|
+
product experience. A model file gives you geometry, materials, and mesh node
|
|
94
|
+
names. It does not know that `"CarBody"` is configurable, that `"FrontDoor"`
|
|
95
|
+
should open a panel, or that `"Wall_A"` should expose metadata and a saved
|
|
96
|
+
camera view.
|
|
97
|
+
|
|
98
|
+
`objectBindings` is the bridge between those raw mesh names and your app's
|
|
99
|
+
domain model. It is a serializable record, keyed by mesh node name, that lets
|
|
100
|
+
you:
|
|
101
|
+
|
|
102
|
+
- declare which meshes are interactive
|
|
103
|
+
- carry live render state such as visibility and per-object material overrides
|
|
104
|
+
- attach actions like `change-color`, `change-material`, and `toggle-visibility`
|
|
105
|
+
- store metadata and metrics alongside the 3D scene
|
|
106
|
+
- save camera framing so object focus can use curated views
|
|
107
|
+
|
|
108
|
+
Because it is plain data, bindings can be authored by hand, generated through
|
|
109
|
+
`BindingBuilder`, stored in a database, versioned as JSON, and round-tripped
|
|
110
|
+
through `onObjectBindingsChange` so your app state and the 3D scene stay in
|
|
111
|
+
sync.
|
|
112
|
+
|
|
113
|
+
## Why It Is Worth The Setup
|
|
114
|
+
|
|
115
|
+
At first glance, `objectBindings` can look like extra setup. That is true only
|
|
116
|
+
if your goal is to render a model and do nothing else with it.
|
|
117
|
+
|
|
118
|
+
If the model needs to behave like part of your application, bindings stop being
|
|
119
|
+
overhead and start being the abstraction that keeps the project maintainable.
|
|
120
|
+
They give you a single source of truth instead of scattering logic across mesh
|
|
121
|
+
refs, material mutations, raycasting handlers, and custom UI glue.
|
|
122
|
+
|
|
123
|
+
The short version:
|
|
124
|
+
|
|
125
|
+
- without bindings, the model is a visual asset
|
|
126
|
+
- with bindings, the model becomes application state
|
|
127
|
+
|
|
128
|
+
If you only need a viewer, use `SimpleModelViewer`. If you need selection,
|
|
129
|
+
customization, persistence, metadata, object-level actions, or app-driven
|
|
130
|
+
behavior, bindings are usually the right tradeoff.
|
|
131
|
+
|
|
132
|
+
## Example Applications Where Bindings Shine
|
|
133
|
+
|
|
134
|
+
- product configurators for cars, furniture, apparel, appliances, or custom goods
|
|
135
|
+
- e-commerce customization flows where meshes map to real purchasable options
|
|
136
|
+
- CPQ and sales tools where 3D parts correspond to business rules and pricing
|
|
137
|
+
- real estate and interior design tools with clickable rooms, walls, fixtures, and finishes
|
|
138
|
+
- industrial and engineering viewers for assemblies, equipment, and maintenance workflows
|
|
139
|
+
- training and guided walkthroughs with annotations, steps, and saved object viewpoints
|
|
140
|
+
- digital twins and operational dashboards where scene parts map to live assets, metrics, and alerts
|
|
141
|
+
- smart building controls where lights, switches, sensors, HVAC units, and doors map to real-time app behavior
|
|
142
|
+
|
|
143
|
+
The pattern is consistent: if users need to say "this exact part of the model
|
|
144
|
+
has its own meaning, state, or behavior," object bindings are a strong fit.
|
|
145
|
+
|
|
146
|
+
## Installation
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
npm install @liveroom-tech/react-immersive
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The component ships with its own internal UI styles through a CSS import. Tailwind is not required in the consuming app.
|
|
153
|
+
|
|
154
|
+
Peer dependencies:
|
|
155
|
+
|
|
156
|
+
- `react >= 17`
|
|
157
|
+
- `react-dom >= 17`
|
|
158
|
+
|
|
159
|
+
`ModelViewer` and `BindingBuilder` require a `licenseKey` at runtime (see [`licenseKey`](#licensekey)). Get one from the Developer Portal — see [Resources](#resources) below.
|
|
160
|
+
|
|
161
|
+
## Resources
|
|
162
|
+
|
|
163
|
+
- [Developer Portal](https://react-immersive.liveroom.dev) — sign in, pick a plan, and generate a `licenseKey`
|
|
164
|
+
- [Docs](https://react-immersive.liveroom.dev/docs) — full reference for every component, hook, and prop
|
|
165
|
+
- [Examples & community repo](https://github.com/liveroom-technologies/react-immersive) — public docs source, starter examples, and community resources
|
|
166
|
+
- [Report a bug / request a feature](https://github.com/liveroom-technologies/react-immersive-docs/issues) — for public bug reports, docs issues, and feature requests
|
|
167
|
+
- [Discussions & Q&A](https://github.com/liveroom-technologies/react-immersive-docs/discussions) — ask questions, share ideas, and compare approaches
|
|
168
|
+
- [Private support / security](mailto:developers@liveroom.xyz?subject=React%20Immersive%20Support) — for license, account, confidential customer, or security-sensitive issues
|
|
169
|
+
- [Live editor](https://react-immersive.liveroom.dev/editor) — try `BindingBuilder` in the browser without installing anything
|
|
170
|
+
- [npm package](https://www.npmjs.com/package/@liveroom-tech/react-immersive)
|
|
171
|
+
|
|
172
|
+
## Quick Start
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
import { useState } from "react";
|
|
176
|
+
import {
|
|
177
|
+
ModelViewer,
|
|
178
|
+
useObjectBinding,
|
|
179
|
+
useObjectVisibility,
|
|
180
|
+
useViewerActions,
|
|
181
|
+
useViewerAnimations,
|
|
182
|
+
useViewerCamera,
|
|
183
|
+
useViewerHover,
|
|
184
|
+
useViewerModel,
|
|
185
|
+
useViewerSelection,
|
|
186
|
+
} from "@liveroom-tech/react-immersive";
|
|
187
|
+
|
|
188
|
+
const initialBindings = {
|
|
189
|
+
CarBody: {
|
|
190
|
+
id: "car-body",
|
|
191
|
+
modelObjectId: "CarBody",
|
|
192
|
+
type: "body",
|
|
193
|
+
label: "Car Body",
|
|
194
|
+
selectable: true,
|
|
195
|
+
hoverable: true,
|
|
196
|
+
visible: true,
|
|
197
|
+
style: {
|
|
198
|
+
material: {
|
|
199
|
+
texture: {
|
|
200
|
+
path: "/materials/oak-wood.jpg",
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
metrics: {},
|
|
205
|
+
metadata: {
|
|
206
|
+
category: "exterior",
|
|
207
|
+
},
|
|
208
|
+
actions: [
|
|
209
|
+
{ id: "change-color", label: "Change Color", type: "command" },
|
|
210
|
+
{ id: "change-material", label: "Change Material", type: "command" },
|
|
211
|
+
{ id: "toggle-visibility", label: "Toggle Visibility", type: "command" },
|
|
212
|
+
],
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
export default function Example() {
|
|
217
|
+
const [objectBindings, setObjectBindings] = useState(initialBindings);
|
|
218
|
+
const { selectedObjectBinding, handleObjectSelect } = useViewerSelection();
|
|
219
|
+
const { hoveredObjectBinding, handleHoveredObject } = useViewerHover();
|
|
220
|
+
const {
|
|
221
|
+
cameraState,
|
|
222
|
+
resetView,
|
|
223
|
+
focusObject,
|
|
224
|
+
fitScene,
|
|
225
|
+
setCameraTarget,
|
|
226
|
+
handleCameraChange,
|
|
227
|
+
handleViewerReady,
|
|
228
|
+
} = useViewerCamera();
|
|
229
|
+
const {
|
|
230
|
+
isLoading,
|
|
231
|
+
isReady,
|
|
232
|
+
error,
|
|
233
|
+
bounds,
|
|
234
|
+
objectCount,
|
|
235
|
+
handleModelLoaded,
|
|
236
|
+
handleLoadError,
|
|
237
|
+
} = useViewerModel();
|
|
238
|
+
const { hiddenObjects, toggleObjectVisibility } = useObjectVisibility(
|
|
239
|
+
objectBindings,
|
|
240
|
+
setObjectBindings,
|
|
241
|
+
);
|
|
242
|
+
const { getActionsForObject, runAction } = useViewerActions(objectBindings, {
|
|
243
|
+
onAction: (event) => {},
|
|
244
|
+
onObjectBindingsChange: setObjectBindings,
|
|
245
|
+
});
|
|
246
|
+
const focusedBinding = useObjectBinding(objectBindings, "car-body");
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
<div style={{ height: "100vh", overflow: "hidden" }}>
|
|
250
|
+
<ModelViewer
|
|
251
|
+
modelUrl="/model.glb"
|
|
252
|
+
licenseKey="your-license-key"
|
|
253
|
+
objectBindings={objectBindings}
|
|
254
|
+
selectedObject={selectedObjectBinding}
|
|
255
|
+
onObjectSelect={handleObjectSelect}
|
|
256
|
+
onObjectHover={handleHoveredObject}
|
|
257
|
+
onCameraChange={handleCameraChange}
|
|
258
|
+
onModelLoaded={handleModelLoaded}
|
|
259
|
+
onLoadError={handleLoadError}
|
|
260
|
+
onObjectBindingsChange={setObjectBindings}
|
|
261
|
+
onViewerReady={handleViewerReady}
|
|
262
|
+
onAction={(event) => {}}
|
|
263
|
+
/>
|
|
264
|
+
|
|
265
|
+
<button onClick={() => toggleObjectVisibility("car-body")}>
|
|
266
|
+
Toggle Visibility
|
|
267
|
+
</button>
|
|
268
|
+
<button onClick={() => runAction("car-body", "toggle-visibility")}>
|
|
269
|
+
Run visibility action
|
|
270
|
+
</button>
|
|
271
|
+
<button onClick={() => resetView()}>Reset view</button>
|
|
272
|
+
<button onClick={() => focusObject("car-body")}>Focus Object</button>
|
|
273
|
+
<button onClick={() => fitScene()}>Fit scene</button>
|
|
274
|
+
<button onClick={() => setCameraTarget([0, 0, 0])}>Target origin</button>
|
|
275
|
+
<pre>{JSON.stringify(getActionsForObject("car-body"), null, 2)}</pre>
|
|
276
|
+
<pre>{JSON.stringify(cameraState, null, 2)}</pre>
|
|
277
|
+
<pre>
|
|
278
|
+
{JSON.stringify(
|
|
279
|
+
{ isLoading, isReady, error, bounds, objectCount },
|
|
280
|
+
null,
|
|
281
|
+
2,
|
|
282
|
+
)}
|
|
283
|
+
</pre>
|
|
284
|
+
<pre>{JSON.stringify(hiddenObjects, null, 2)}</pre>
|
|
285
|
+
<pre>{JSON.stringify(hoveredObjectBinding, null, 2)}</pre>
|
|
286
|
+
<pre>{JSON.stringify(focusedBinding, null, 2)}</pre>
|
|
287
|
+
</div>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
If you want a simple helper for selection state, the library also exports:
|
|
293
|
+
|
|
294
|
+
```tsx
|
|
295
|
+
const {
|
|
296
|
+
selectedObjectBinding,
|
|
297
|
+
setSelectedObjectBinding,
|
|
298
|
+
handleObjectSelect,
|
|
299
|
+
clearSelection,
|
|
300
|
+
} = useViewerSelection();
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
If you want to resolve a binding from an object identifier, the library also exports:
|
|
304
|
+
|
|
305
|
+
```tsx
|
|
306
|
+
const binding = useObjectBinding(objectBindings, "car-body");
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
`useObjectBinding` checks, in order:
|
|
310
|
+
|
|
311
|
+
- the object binding map key
|
|
312
|
+
- `binding.id`
|
|
313
|
+
- `binding.modelObjectId`
|
|
314
|
+
|
|
315
|
+
If you want a flat list of every `modelObjectId` across your bindings, the library also exports:
|
|
316
|
+
|
|
317
|
+
```tsx
|
|
318
|
+
const ids = useObjectBindingIds(objectBindings);
|
|
319
|
+
// → ["CarBody", "WheelFL", "WheelFR", ...]
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
If you want to control object visibility outside the viewer, the library also exports:
|
|
323
|
+
|
|
324
|
+
```tsx
|
|
325
|
+
const [objectBindings, setObjectBindings] = useState(initialBindings);
|
|
326
|
+
|
|
327
|
+
const {
|
|
328
|
+
hiddenObjects,
|
|
329
|
+
hiddenObjectIds,
|
|
330
|
+
hideObject,
|
|
331
|
+
showObject,
|
|
332
|
+
toggleObjectVisibility,
|
|
333
|
+
isObjectHidden,
|
|
334
|
+
clearHiddenObjects,
|
|
335
|
+
} = useObjectVisibility(objectBindings, setObjectBindings);
|
|
336
|
+
|
|
337
|
+
<ModelViewer
|
|
338
|
+
modelUrl="/model.glb"
|
|
339
|
+
licenseKey="your-license-key"
|
|
340
|
+
objectBindings={objectBindings}
|
|
341
|
+
onObjectBindingsChange={setObjectBindings}
|
|
342
|
+
/>;
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
`useObjectVisibility` is a stateless utility hook. It does not own its own copy of `objectBindings` — the consumer owns the state and passes both the current value and its setter. The hook's methods (`hideObject`, `showObject`, etc.) call the setter directly, so there is no risk of the hook and the viewer drifting out of sync.
|
|
346
|
+
|
|
347
|
+
It accepts either:
|
|
348
|
+
|
|
349
|
+
- the object binding map key
|
|
350
|
+
- `binding.id`
|
|
351
|
+
- `binding.modelObjectId`
|
|
352
|
+
|
|
353
|
+
and returns:
|
|
354
|
+
|
|
355
|
+
- `hiddenObjects`: a derived map of hidden binding keys
|
|
356
|
+
- `hiddenObjectIds`: the currently hidden binding IDs
|
|
357
|
+
- `hideObject`, `showObject`, `toggleObjectVisibility`
|
|
358
|
+
- `isObjectHidden`
|
|
359
|
+
- `clearHiddenObjects`
|
|
360
|
+
|
|
361
|
+
If you want to keep hover state in sync with the viewer, the library also exports:
|
|
362
|
+
|
|
363
|
+
```tsx
|
|
364
|
+
const { hoveredObjectBinding, handleHoveredObject } = useViewerHover();
|
|
365
|
+
|
|
366
|
+
<ModelViewer
|
|
367
|
+
modelUrl="/model.glb"
|
|
368
|
+
licenseKey="your-license-key"
|
|
369
|
+
objectBindings={objectBindings}
|
|
370
|
+
onObjectHover={handleHoveredObject}
|
|
371
|
+
/>;
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
`useViewerHover` returns:
|
|
375
|
+
|
|
376
|
+
- `hoveredObjectBinding`: the currently hovered binding or `null`
|
|
377
|
+
- `handleHoveredObject`: a callback you can pass directly to `onObjectHover`
|
|
378
|
+
|
|
379
|
+
If you want model lifecycle and scene metadata, the library also exports:
|
|
380
|
+
|
|
381
|
+
```tsx
|
|
382
|
+
const {
|
|
383
|
+
isLoading,
|
|
384
|
+
isReady,
|
|
385
|
+
error,
|
|
386
|
+
bounds,
|
|
387
|
+
objectCount,
|
|
388
|
+
handleModelLoaded,
|
|
389
|
+
handleLoadError,
|
|
390
|
+
} = useViewerModel();
|
|
391
|
+
|
|
392
|
+
<ModelViewer
|
|
393
|
+
modelUrl="/model.glb"
|
|
394
|
+
licenseKey="your-license-key"
|
|
395
|
+
objectBindings={objectBindings}
|
|
396
|
+
onModelLoaded={handleModelLoaded}
|
|
397
|
+
onLoadError={handleLoadError}
|
|
398
|
+
/>;
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
`useViewerModel` returns:
|
|
402
|
+
|
|
403
|
+
- `isLoading`: `true` until the model has loaded or failed
|
|
404
|
+
- `isReady`: `true` after a successful model load
|
|
405
|
+
- `error`: the most recent load/render error, or `null`
|
|
406
|
+
- `bounds`: the loaded scene bounds as `min`, `max`, `center`, and `size`
|
|
407
|
+
- `objectCount`: the number of mesh objects in the loaded scene
|
|
408
|
+
- `handleModelLoaded`: a callback you can pass directly to `onModelLoaded`
|
|
409
|
+
- `handleLoadError`: a callback you can pass directly to `onLoadError`
|
|
410
|
+
|
|
411
|
+
If you want to inspect or trigger binding actions programmatically, the library also exports:
|
|
412
|
+
|
|
413
|
+
```tsx
|
|
414
|
+
const { getActionsForObject, runAction } = useViewerActions(objectBindings, {
|
|
415
|
+
onAction: (event) => {},
|
|
416
|
+
onObjectBindingsChange: setObjectBindings,
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
const actions = getActionsForObject("car-body");
|
|
420
|
+
runAction("car-body", "toggle-visibility");
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
`useViewerActions` returns:
|
|
424
|
+
|
|
425
|
+
- `getActionsForObject`: resolves the actions configured for a binding by key, `binding.id`, or `binding.modelObjectId`
|
|
426
|
+
- `runAction`: dispatches an `ObjectActionEvent` and returns it, or `null` if the binding or action cannot be found
|
|
427
|
+
|
|
428
|
+
Current note:
|
|
429
|
+
|
|
430
|
+
- visibility-style actions (`toggle-visibility`) update binding data through `onObjectBindingsChange`
|
|
431
|
+
- color/material actions still rely on `ModelViewer`'s built-in UI popovers, so `runAction` dispatches those events but does not open the internal color picker or texture upload popover by itself
|
|
432
|
+
|
|
433
|
+
## `BindingBuilder`
|
|
434
|
+
|
|
435
|
+
`BindingBuilder` is an exported design-time helper component for authoring `objectBindings` in the browser.
|
|
436
|
+
|
|
437
|
+
Basic usage:
|
|
438
|
+
|
|
439
|
+
```tsx
|
|
440
|
+
import { BindingBuilder } from "@liveroom-tech/react-immersive";
|
|
441
|
+
|
|
442
|
+
export default function App() {
|
|
443
|
+
return <BindingBuilder licenseKey="your-license-key" />;
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
`BindingBuilder` accepts one required prop:
|
|
448
|
+
|
|
449
|
+
```ts
|
|
450
|
+
type BindingBuilderProps = {
|
|
451
|
+
licenseKey: string;
|
|
452
|
+
};
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
`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.
|
|
456
|
+
|
|
457
|
+
Current behavior — Object tab:
|
|
458
|
+
|
|
459
|
+
- load the bundled demo model or upload a `.glb`, standalone/data-URI `.gltf`, or `.zip` containing a `.gltf` plus its referenced `.bin` and texture files
|
|
460
|
+
- traverse renderable mesh nodes and generate starter bindings automatically
|
|
461
|
+
- edit binding identity, label, type, status, booleans, style, actions, metrics, metadata, and saved camera state
|
|
462
|
+
- apply a one-click material preset (wood, metal, chrome, glass, plastic, fabric, ceramic, concrete, etc.) onto the selected object's material
|
|
463
|
+
- undo/redo the current bindings (toolbar buttons or `Cmd/Ctrl+Z` / `Cmd/Ctrl+Shift+Z`); rapid edits coalesce into a single undo step
|
|
464
|
+
- import a previously exported `objectBindings.json`, merged onto the current model by `modelObjectId`
|
|
465
|
+
- export the current bindings as JSON or TypeScript — bundled into a `.zip` (with a `materials/` folder) when any texture field was filled by uploading a file, otherwise a plain file
|
|
466
|
+
|
|
467
|
+
Current behavior — Scene tab:
|
|
468
|
+
|
|
469
|
+
- configure the same `SceneConfig` shape `ModelViewer`'s `sceneConfig` prop accepts (lighting, environment, background, ground shadows, wireframe, post-processing, animations, annotations)
|
|
470
|
+
- undo/redo the scene config independently of the Object tab's bindings history
|
|
471
|
+
- import a previously exported `sceneConfig.json`, merged by top-level section
|
|
472
|
+
- export the current scene config as JSON or TypeScript
|
|
473
|
+
|
|
474
|
+
Preview:
|
|
475
|
+
|
|
476
|
+
- the selected node previews live in an embedded `ModelViewer`
|
|
477
|
+
|
|
478
|
+
### `BindingBuilder` licensing & plan tiers
|
|
479
|
+
|
|
480
|
+
Some editor features are gated by the license's plan tier (the server-returned `tier`, normalized to `"free" | "starter" | "growth"`):
|
|
481
|
+
|
|
482
|
+
| Tier | Unlocks |
|
|
483
|
+
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
484
|
+
| `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 |
|
|
485
|
+
| `starter` | + Texture maps, the specular workflow, reflectivity, environment lighting, environment backgrounds, wireframe preview, animation configuration, annotations |
|
|
486
|
+
| `growth` | + Post-processing effects |
|
|
487
|
+
|
|
488
|
+
A locked feature renders in place (not hidden) with a message naming the required plan, so it stays discoverable while editing.
|
|
489
|
+
|
|
490
|
+
## `SimpleModelViewer`
|
|
491
|
+
|
|
492
|
+
`SimpleModelViewer` is an exported lightweight viewer for cases where you want to load a GLB/GLTF 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.
|
|
493
|
+
|
|
494
|
+
Basic usage:
|
|
495
|
+
|
|
496
|
+
```tsx
|
|
497
|
+
import { SimpleModelViewer } from "@liveroom-tech/react-immersive";
|
|
498
|
+
|
|
499
|
+
export default function App() {
|
|
500
|
+
return (
|
|
501
|
+
<div style={{ height: "100vh" }}>
|
|
502
|
+
<SimpleModelViewer modelUrl="/model.glb" />
|
|
503
|
+
</div>
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
`SimpleModelViewer` does not take a `licenseKey` — licensing is enforced by `ModelViewer` and `BindingBuilder` only.
|
|
509
|
+
|
|
510
|
+
Sizing note:
|
|
511
|
+
|
|
512
|
+
- `SimpleModelViewer` fills the width and height of its parent container
|
|
513
|
+
- give the parent an explicit height (for example `100vh`, `480px`, or a Tailwind class like `h-[100px]`) when you want a fixed viewer height
|
|
514
|
+
|
|
515
|
+
Current behavior:
|
|
516
|
+
|
|
517
|
+
- discovers renderable meshes directly from the loaded GLB/GLTF scene
|
|
518
|
+
- renders a right-side scene objects panel with search and visibility toggles
|
|
519
|
+
- 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
|
|
520
|
+
- optionally replaces `modelUrl` with a drag-and-drop / file-picker flow when `enableModelUpload` is enabled
|
|
521
|
+
- lets users click a mesh or panel row to select and focus it
|
|
522
|
+
- fits the full scene on initial load
|
|
523
|
+
- uses built-in ambient, directional, and environment lighting
|
|
524
|
+
|
|
525
|
+
Use `SimpleModelViewer` when you want a quick inspection viewer.
|
|
526
|
+
Use `ModelViewer` when you need binding-driven styling, actions, metadata, custom panels, exports, or animation integrations.
|
|
527
|
+
|
|
528
|
+
If you want camera state and imperative camera helpers, the library also exports:
|
|
529
|
+
|
|
530
|
+
```tsx
|
|
531
|
+
const {
|
|
532
|
+
cameraState,
|
|
533
|
+
resetView,
|
|
534
|
+
focusObject,
|
|
535
|
+
fitScene,
|
|
536
|
+
setCameraTarget,
|
|
537
|
+
handleCameraChange,
|
|
538
|
+
handleViewerReady,
|
|
539
|
+
} = useViewerCamera();
|
|
540
|
+
|
|
541
|
+
<ModelViewer
|
|
542
|
+
modelUrl="/model.glb"
|
|
543
|
+
licenseKey="your-license-key"
|
|
544
|
+
objectBindings={objectBindings}
|
|
545
|
+
onCameraChange={handleCameraChange}
|
|
546
|
+
onViewerReady={handleViewerReady}
|
|
547
|
+
/>;
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
`useViewerCamera` returns:
|
|
551
|
+
|
|
552
|
+
- `cameraState`: the latest camera `position`, `target`, `fov`, and `zoom`
|
|
553
|
+
- `resetView`: resets the controls to their initial saved state
|
|
554
|
+
- `focusObject`: fits the camera to a binding by key, `binding.id`, or `binding.modelObjectId`
|
|
555
|
+
- `fitScene`: fits the camera to the whole loaded scene
|
|
556
|
+
- `setCameraTarget`: sets the camera target to a given `[x, y, z]`
|
|
557
|
+
- `setCameraState`: restores a full camera state (`position`/`target`/`fov`/`zoom`) previously read from `cameraState`
|
|
558
|
+
- `handleCameraChange`: a callback you can pass directly to `onCameraChange`
|
|
559
|
+
- `handleViewerReady`: a callback you can pass directly to `onViewerReady`
|
|
560
|
+
|
|
561
|
+
If your GLB/GLTF model contains animations, the library also exports:
|
|
562
|
+
|
|
563
|
+
```tsx
|
|
564
|
+
const {
|
|
565
|
+
clips,
|
|
566
|
+
currentClip,
|
|
567
|
+
isPlaying,
|
|
568
|
+
speed,
|
|
569
|
+
play,
|
|
570
|
+
pause,
|
|
571
|
+
stop,
|
|
572
|
+
setSpeed,
|
|
573
|
+
handleAnimationsReady,
|
|
574
|
+
} = useViewerAnimations();
|
|
575
|
+
|
|
576
|
+
<ModelViewer
|
|
577
|
+
modelUrl="/character.glb"
|
|
578
|
+
licenseKey="your-license-key"
|
|
579
|
+
objectBindings={objectBindings}
|
|
580
|
+
onAnimationsReady={handleAnimationsReady}
|
|
581
|
+
/>;
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
`useViewerAnimations` returns:
|
|
585
|
+
|
|
586
|
+
- `clips`: an array of animation clip names found in the GLB/GLTF file
|
|
587
|
+
- `clipDetails`: clip metadata including `sourceName` and `duration` in seconds
|
|
588
|
+
- `currentClip`: the name of the currently playing clip, or `null`
|
|
589
|
+
- `isPlaying`: `true` while an animation is playing (not paused or stopped)
|
|
590
|
+
- `speed`: the current playback speed (default `1`)
|
|
591
|
+
- `time`: the live playback position of the current clip, in seconds (updates while playing)
|
|
592
|
+
- `duration`: the length of the current clip, in seconds
|
|
593
|
+
- `play`: starts a clip by name; if the same clip is paused, resumes it instead of restarting
|
|
594
|
+
- `pause`: pauses the currently playing clip at its current time
|
|
595
|
+
- `stop`: stops the current clip and resets it
|
|
596
|
+
- `setSpeed`: changes the playback speed (e.g. `0.5` for half speed, `2` for double)
|
|
597
|
+
- `seek`: scrubs the current clip to an absolute time in seconds (works while playing or paused), clamped to the clip length
|
|
598
|
+
- `handleAnimationsReady`: a callback you can pass directly to `onAnimationsReady`
|
|
599
|
+
|
|
600
|
+
Example with playback controls:
|
|
601
|
+
|
|
602
|
+
```tsx
|
|
603
|
+
const { clips, currentClip, isPlaying, play, pause, stop, setSpeed } =
|
|
604
|
+
useViewerAnimations();
|
|
605
|
+
|
|
606
|
+
// Play/pause toggle
|
|
607
|
+
<button onClick={() => (isPlaying ? pause() : play(currentClip ?? clips[0]))}>
|
|
608
|
+
{isPlaying ? "Pause" : "Play"}
|
|
609
|
+
</button>
|
|
610
|
+
|
|
611
|
+
// Stop
|
|
612
|
+
<button onClick={stop}>Stop</button>
|
|
613
|
+
|
|
614
|
+
// Speed slider
|
|
615
|
+
<input
|
|
616
|
+
type="range"
|
|
617
|
+
min="0.1"
|
|
618
|
+
max="3"
|
|
619
|
+
step="0.1"
|
|
620
|
+
defaultValue="1"
|
|
621
|
+
onChange={(e) => setSpeed(Number(e.target.value))}
|
|
622
|
+
/>
|
|
623
|
+
|
|
624
|
+
// Clip selector
|
|
625
|
+
{clips.map((name) => (
|
|
626
|
+
<button key={name} onClick={() => play(name)}>
|
|
627
|
+
{name} {currentClip === name && isPlaying ? "▶" : ""}
|
|
628
|
+
</button>
|
|
629
|
+
))}
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
## Public API
|
|
633
|
+
|
|
634
|
+
The source code currently defines `ModelViewer` with these props:
|
|
635
|
+
|
|
636
|
+
```ts
|
|
637
|
+
type Props = {
|
|
638
|
+
modelUrl: string;
|
|
639
|
+
licenseKey: string;
|
|
640
|
+
objectBindings: Record<string, ObjectBinding>;
|
|
641
|
+
selectedObject?: ObjectBinding | null;
|
|
642
|
+
onObjectBindingsChange?: (next: Record<string, ObjectBinding>) => void;
|
|
643
|
+
onObjectSelect?: (binding: ObjectBinding | null) => void;
|
|
644
|
+
onObjectHover?: (binding: ObjectBinding | null) => void;
|
|
645
|
+
onModelLoaded?: (scene: Object3D) => void;
|
|
646
|
+
onLoadError?: (error: unknown) => void;
|
|
647
|
+
onAction?: (event: ObjectActionEvent) => void;
|
|
648
|
+
onHiddenObjectsChange?: (next: Record<string, boolean>) => void;
|
|
649
|
+
onCameraChange?: (camera: Camera, controls: CameraControls) => void;
|
|
650
|
+
onViewerReady?: (viewer: ViewerReadyState) => void;
|
|
651
|
+
onTextureUpload?: (file: File, objectId: string) => Promise<string>;
|
|
652
|
+
onAnimationsReady?: (controls: AnimationControls) => void;
|
|
653
|
+
onAnnotationsChange?: (annotations: AnnotationMarker[]) => void;
|
|
654
|
+
activeAnnotation?: AnnotationMarker | null;
|
|
655
|
+
onActiveAnnotationChange?: (annotation: AnnotationMarker | null) => void;
|
|
656
|
+
lights?: React.ReactNode;
|
|
657
|
+
camera?: React.ComponentProps<typeof Canvas>["camera"];
|
|
658
|
+
backgroundColor?: string;
|
|
659
|
+
shadows?: boolean;
|
|
660
|
+
showObjectBindingDataPanel?: boolean;
|
|
661
|
+
customObjectBindingDataPanel?: (
|
|
662
|
+
props: CustomObjectBindingDataPanelProps,
|
|
663
|
+
) => React.ReactNode;
|
|
664
|
+
customSceneObjectsPanel?: (
|
|
665
|
+
props: CustomSceneObjectsPanelProps,
|
|
666
|
+
) => React.ReactNode;
|
|
667
|
+
showSceneObjectsPanel?: boolean;
|
|
668
|
+
showDownloadButton?: boolean;
|
|
669
|
+
downloadFilename?: string;
|
|
670
|
+
showResetButton?: boolean;
|
|
671
|
+
showDownloadButtons?: boolean;
|
|
672
|
+
showLoadingOverlay?: boolean;
|
|
673
|
+
showMouseController?: boolean;
|
|
674
|
+
mouseControllerPosition?:
|
|
675
|
+
| "bottom-left"
|
|
676
|
+
| "bottom-right"
|
|
677
|
+
| "top-left"
|
|
678
|
+
| "top-right"
|
|
679
|
+
| "center"
|
|
680
|
+
| "center-bottom"
|
|
681
|
+
| "center-top";
|
|
682
|
+
mouseControllerOpacity?: number;
|
|
683
|
+
moveSensitivity?: number;
|
|
684
|
+
zoomSensitivity?: number;
|
|
685
|
+
sceneConfig?: SceneConfig;
|
|
686
|
+
disableZoom?: boolean;
|
|
687
|
+
zoomOnSelected?: boolean;
|
|
688
|
+
onAutoFit?: () => Promise<boolean>;
|
|
689
|
+
refitOnResize?: boolean;
|
|
690
|
+
renderMode?: "always" | "demand";
|
|
691
|
+
maxDpr?: number;
|
|
692
|
+
performanceProfile?: "auto" | "high" | "low";
|
|
693
|
+
dracoDecoderPath?: string | false;
|
|
694
|
+
ktx2TranscoderPath?: string | false;
|
|
695
|
+
meshopt?: boolean;
|
|
696
|
+
showMeasureTools?: boolean;
|
|
697
|
+
measurementUnit?: string;
|
|
698
|
+
enableXR?: boolean;
|
|
699
|
+
usdzUrl?: string;
|
|
700
|
+
showAnnotationNavigation?: boolean;
|
|
701
|
+
};
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
### `modelUrl`
|
|
705
|
+
|
|
706
|
+
URL or public path to the `.glb` or `.gltf` model. For `.gltf` URLs with external `.bin` or texture files, those files must be hosted at the relative paths declared in the `.gltf`.
|
|
707
|
+
|
|
708
|
+
Example:
|
|
709
|
+
|
|
710
|
+
```tsx
|
|
711
|
+
modelUrl = "/model.glb";
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
### `licenseKey`
|
|
715
|
+
|
|
716
|
+
License key for the library. Required.
|
|
717
|
+
|
|
718
|
+
### `objectBindings`
|
|
719
|
+
|
|
720
|
+
A map keyed by the model node name. Each key should match a mesh name from the GLB/GLTF file.
|
|
721
|
+
|
|
722
|
+
`objectBindings` is the single source of truth for all visual state. Changes to the binding record drive rendering:
|
|
723
|
+
|
|
724
|
+
- `visible` controls mesh visibility
|
|
725
|
+
- `style.material.baseColor` sets the mesh color (committed by the built-in color picker on close)
|
|
726
|
+
- `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)
|
|
727
|
+
- `style.material.*` applies per-object `MeshPhysicalMaterial` overrides such as emissive, metalness, roughness, opacity, clearcoat, sheen, anisotropy, specular, transmission, thickness, reflectivity, and sidedness
|
|
728
|
+
- `cameraState.position` and `cameraState.target` define the camera view to use when that object is focused or selected from the viewer UI
|
|
729
|
+
|
|
730
|
+
Example:
|
|
731
|
+
|
|
732
|
+
```ts
|
|
733
|
+
const objectBindings = {
|
|
734
|
+
Object_2: {
|
|
735
|
+
id: "obj-2",
|
|
736
|
+
modelObjectId: "Object_2",
|
|
737
|
+
type: "body",
|
|
738
|
+
visible: true,
|
|
739
|
+
cameraState: {
|
|
740
|
+
position: [2.8, 1.6, 4.2],
|
|
741
|
+
target: [0, 0.8, 0],
|
|
742
|
+
},
|
|
743
|
+
style: {
|
|
744
|
+
material: {
|
|
745
|
+
baseColor: "#ff0000",
|
|
746
|
+
texture: {
|
|
747
|
+
path: "/materials/body-finish.jpg",
|
|
748
|
+
},
|
|
749
|
+
},
|
|
750
|
+
},
|
|
751
|
+
actions: [
|
|
752
|
+
{ id: "change-color", label: "Change Color", type: "command" },
|
|
753
|
+
{ id: "change-material", label: "Change Material", type: "command" },
|
|
754
|
+
{ id: "toggle-visibility", label: "Toggle Visibility", type: "command" },
|
|
755
|
+
],
|
|
756
|
+
metrics: {},
|
|
757
|
+
metadata: {},
|
|
758
|
+
},
|
|
759
|
+
};
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
### `selectedObject`
|
|
763
|
+
|
|
764
|
+
Optional currently selected object binding, or `null` when nothing is selected.
|
|
765
|
+
|
|
766
|
+
If provided, `ModelViewer` behaves as a controlled component.
|
|
767
|
+
If omitted, `ModelViewer` manages its own selection state internally.
|
|
768
|
+
|
|
769
|
+
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`.
|
|
770
|
+
|
|
771
|
+
### `onObjectSelect`
|
|
772
|
+
|
|
773
|
+
Optional callback called when the user clicks a mesh or closes the side panel.
|
|
774
|
+
|
|
775
|
+
Behavior:
|
|
776
|
+
|
|
777
|
+
- clicking a mesh calls `onObjectSelect(binding)`
|
|
778
|
+
- closing the panel calls `onObjectSelect(null)`
|
|
779
|
+
- clicking the same mesh again still selects it; selection is not toggled off automatically
|
|
780
|
+
- if the binding includes `cameraState.position` and `cameraState.target`, the viewer moves the camera to that saved view when focusing that object
|
|
781
|
+
|
|
782
|
+
### `onObjectHover`
|
|
783
|
+
|
|
784
|
+
Optional callback called when the user hovers a mesh in the model.
|
|
785
|
+
|
|
786
|
+
Behavior:
|
|
787
|
+
|
|
788
|
+
- pointer over a mesh calls `onObjectHover(binding)`
|
|
789
|
+
- pointer out calls `onObjectHover(null)`
|
|
790
|
+
|
|
791
|
+
### `onHiddenObjectsChange`
|
|
792
|
+
|
|
793
|
+
Optional callback fired with the derived hidden-object map whenever the current binding data changes hidden visibility state.
|
|
794
|
+
|
|
795
|
+
### `onObjectBindingsChange`
|
|
796
|
+
|
|
797
|
+
Optional callback fired when the viewer's built-in UI updates binding data.
|
|
798
|
+
|
|
799
|
+
This fires for:
|
|
800
|
+
|
|
801
|
+
- visibility toggles (`binding.visible`)
|
|
802
|
+
- color picks (`binding.style.material.baseColor`) — committed when the color picker closes
|
|
803
|
+
- texture uploads (`binding.style.material.texture.path`) — committed immediately (via `onTextureUpload` when provided, otherwise as a blob URL)
|
|
804
|
+
- texture removal (`binding.style.material.texture` cleared)
|
|
805
|
+
|
|
806
|
+
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.
|
|
807
|
+
|
|
808
|
+
### `onModelLoaded`
|
|
809
|
+
|
|
810
|
+
Optional callback fired after the GLB/GLTF scene has been loaded.
|
|
811
|
+
|
|
812
|
+
Current callback shape:
|
|
813
|
+
|
|
814
|
+
```ts
|
|
815
|
+
(scene: Object3D) => void
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
### `onLoadError`
|
|
819
|
+
|
|
820
|
+
Optional callback fired if the model fails to load or render.
|
|
821
|
+
|
|
822
|
+
Current callback shape:
|
|
823
|
+
|
|
824
|
+
```ts
|
|
825
|
+
(error: unknown) => void
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
### `onAction`
|
|
829
|
+
|
|
830
|
+
Optional callback fired when a built-in action button is clicked from the side panel.
|
|
831
|
+
|
|
832
|
+
Current event shape:
|
|
833
|
+
|
|
834
|
+
```ts
|
|
835
|
+
type ObjectActionEvent = {
|
|
836
|
+
objectId: string;
|
|
837
|
+
action: ObjectBindingAction;
|
|
838
|
+
binding?: ObjectBinding;
|
|
839
|
+
screenX?: number;
|
|
840
|
+
screenY?: number;
|
|
841
|
+
};
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
### `onViewerReady`
|
|
845
|
+
|
|
846
|
+
Optional callback fired once camera controls are available and again when the
|
|
847
|
+
viewer publishes updated ready-state data (for example after model load,
|
|
848
|
+
camera changes, or binding changes).
|
|
849
|
+
|
|
850
|
+
Current callback shape:
|
|
851
|
+
|
|
852
|
+
```ts
|
|
853
|
+
(viewer: ViewerReadyState) => void
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
`ViewerReadyState` includes `captureImage`, an imperative method for grabbing a PNG snapshot of the canvas:
|
|
857
|
+
|
|
858
|
+
```ts
|
|
859
|
+
type ViewerReadyState = {
|
|
860
|
+
controls: CameraControls;
|
|
861
|
+
scene: Object3D | null;
|
|
862
|
+
objectBindings: Record<string, ObjectBinding>;
|
|
863
|
+
nodeRefs: Record<string, Object3D>;
|
|
864
|
+
captureImage: (options?: CaptureImageOptions) => Promise<string>;
|
|
865
|
+
};
|
|
866
|
+
|
|
867
|
+
type CaptureImageOptions = {
|
|
868
|
+
width?: number;
|
|
869
|
+
height?: number;
|
|
870
|
+
transparent?: boolean;
|
|
871
|
+
};
|
|
872
|
+
```
|
|
873
|
+
|
|
874
|
+
`captureImage` 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.
|
|
875
|
+
|
|
876
|
+
```tsx
|
|
877
|
+
onViewerReady={(viewer) => {
|
|
878
|
+
viewer
|
|
879
|
+
.captureImage({ width: 1920, height: 1080, transparent: true })
|
|
880
|
+
.then((dataUrl) => {
|
|
881
|
+
// upload, preview, etc.
|
|
882
|
+
});
|
|
883
|
+
}}
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
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.
|
|
887
|
+
|
|
888
|
+
If you're also using `useViewerCamera`'s `handleViewerReady`, call both from your own `onViewerReady` to get camera helpers and `captureImage` together:
|
|
889
|
+
|
|
890
|
+
```tsx
|
|
891
|
+
onViewerReady={(viewer) => {
|
|
892
|
+
handleViewerReady(viewer);
|
|
893
|
+
captureImageRef.current = viewer.captureImage;
|
|
894
|
+
}}
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
### `onTextureUpload`
|
|
898
|
+
|
|
899
|
+
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.
|
|
900
|
+
|
|
901
|
+
Current callback shape:
|
|
902
|
+
|
|
903
|
+
```ts
|
|
904
|
+
(file: File, objectId: string) => Promise<string>;
|
|
905
|
+
```
|
|
906
|
+
|
|
907
|
+
The returned string is stored in `binding.style.material.texture.path` via `onObjectBindingsChange`.
|
|
908
|
+
|
|
909
|
+
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.
|
|
910
|
+
|
|
911
|
+
Example:
|
|
912
|
+
|
|
913
|
+
```tsx
|
|
914
|
+
<ModelViewer
|
|
915
|
+
modelUrl="/model.glb"
|
|
916
|
+
licenseKey="your-license-key"
|
|
917
|
+
objectBindings={objectBindings}
|
|
918
|
+
onObjectBindingsChange={setObjectBindings}
|
|
919
|
+
onTextureUpload={async (file, objectId) => {
|
|
920
|
+
const formData = new FormData();
|
|
921
|
+
formData.append("file", file);
|
|
922
|
+
formData.append("objectId", objectId);
|
|
923
|
+
const res = await fetch("/api/upload-texture", {
|
|
924
|
+
method: "POST",
|
|
925
|
+
body: formData,
|
|
926
|
+
});
|
|
927
|
+
const { url } = await res.json();
|
|
928
|
+
return url; // e.g. "https://cdn.example.com/textures/abc123.png"
|
|
929
|
+
}}
|
|
930
|
+
/>
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
### `onAnimationsReady`
|
|
934
|
+
|
|
935
|
+
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.
|
|
936
|
+
|
|
937
|
+
Current callback shape:
|
|
938
|
+
|
|
939
|
+
```ts
|
|
940
|
+
(controls: AnimationControls) => void
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
Where `AnimationControls` is:
|
|
944
|
+
|
|
945
|
+
```ts
|
|
946
|
+
type AnimationPlaybackState = {
|
|
947
|
+
currentClip: string | null;
|
|
948
|
+
isPlaying: boolean;
|
|
949
|
+
speed: number;
|
|
950
|
+
time: number; // live position of the current clip, in seconds
|
|
951
|
+
duration: number; // length of the current clip, in seconds
|
|
952
|
+
};
|
|
953
|
+
|
|
954
|
+
type AnimationControls = {
|
|
955
|
+
clips: string[];
|
|
956
|
+
clipDetails?: { sourceName: string; duration: number }[];
|
|
957
|
+
play: (clipName: string) => void;
|
|
958
|
+
pause: () => void;
|
|
959
|
+
stop: () => void;
|
|
960
|
+
setSpeed: (speed: number) => void;
|
|
961
|
+
seek?: (time: number) => void; // scrub the current clip to an absolute time (seconds)
|
|
962
|
+
getState?: () => AnimationPlaybackState;
|
|
963
|
+
subscribe?: (listener: (state: AnimationPlaybackState) => void) => () => void;
|
|
964
|
+
};
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
Wire this to `useViewerAnimations().handleAnimationsReady` for the simplest integration.
|
|
968
|
+
When `sceneConfig.animations.autoplayClip` is set, `ModelViewer` will auto-play
|
|
969
|
+
that clip on load and respect per-clip `loopMode`, `speed`, `displayName`, and
|
|
970
|
+
soft-delete (`hidden`) settings.
|
|
971
|
+
|
|
972
|
+
### `lights`
|
|
973
|
+
|
|
974
|
+
Optional custom lighting to render inside the scene.
|
|
975
|
+
|
|
976
|
+
If omitted, `ModelViewer` uses the library's default light rig.
|
|
977
|
+
|
|
978
|
+
Example:
|
|
979
|
+
|
|
980
|
+
```tsx
|
|
981
|
+
function CustomLights() {
|
|
982
|
+
return (
|
|
983
|
+
<>
|
|
984
|
+
<ambientLight intensity={0.5} />
|
|
985
|
+
<directionalLight position={[4, 8, 4]} intensity={1.6} castShadow />
|
|
986
|
+
<pointLight position={[-3, 3, 2]} intensity={0.8} />
|
|
987
|
+
</>
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
<ModelViewer
|
|
992
|
+
modelUrl="/model.glb"
|
|
993
|
+
licenseKey="your-license-key"
|
|
994
|
+
objectBindings={objectBindings}
|
|
995
|
+
lights={<CustomLights />}
|
|
996
|
+
/>;
|
|
997
|
+
```
|
|
998
|
+
|
|
999
|
+
### `camera`
|
|
1000
|
+
|
|
1001
|
+
Optional custom camera configuration passed through to the underlying React Three Fiber `Canvas`.
|
|
1002
|
+
|
|
1003
|
+
`ModelViewer` only sets a default `fov: 50` — it does not set a default position, so an unset position falls back to React Three Fiber's own `Canvas` default (`[0, 0, 5]`). Pass an explicit `position` for predictable framing.
|
|
1004
|
+
|
|
1005
|
+
Example:
|
|
1006
|
+
|
|
1007
|
+
```tsx
|
|
1008
|
+
<ModelViewer
|
|
1009
|
+
modelUrl="/model.glb"
|
|
1010
|
+
licenseKey="your-license-key"
|
|
1011
|
+
objectBindings={objectBindings}
|
|
1012
|
+
camera={{
|
|
1013
|
+
position: [0, 2.2, 7],
|
|
1014
|
+
fov: 40,
|
|
1015
|
+
near: 0.1,
|
|
1016
|
+
far: 1000,
|
|
1017
|
+
}}
|
|
1018
|
+
/>
|
|
1019
|
+
```
|
|
1020
|
+
|
|
1021
|
+
### `backgroundColor`
|
|
1022
|
+
|
|
1023
|
+
Optional background color for the viewer canvas.
|
|
1024
|
+
|
|
1025
|
+
Example:
|
|
1026
|
+
|
|
1027
|
+
```tsx
|
|
1028
|
+
<ModelViewer
|
|
1029
|
+
modelUrl="/model.glb"
|
|
1030
|
+
licenseKey="your-license-key"
|
|
1031
|
+
objectBindings={objectBindings}
|
|
1032
|
+
backgroundColor="#0f172a"
|
|
1033
|
+
/>
|
|
1034
|
+
```
|
|
1035
|
+
|
|
1036
|
+
### `shadows`
|
|
1037
|
+
|
|
1038
|
+
Optional boolean that controls whether the viewer renders shadows.
|
|
1039
|
+
|
|
1040
|
+
Default:
|
|
1041
|
+
|
|
1042
|
+
```ts
|
|
1043
|
+
true;
|
|
1044
|
+
```
|
|
1045
|
+
|
|
1046
|
+
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).
|
|
1047
|
+
|
|
1048
|
+
Example:
|
|
1049
|
+
|
|
1050
|
+
```tsx
|
|
1051
|
+
<ModelViewer
|
|
1052
|
+
modelUrl="/model.glb"
|
|
1053
|
+
licenseKey="your-license-key"
|
|
1054
|
+
objectBindings={objectBindings}
|
|
1055
|
+
shadows={false}
|
|
1056
|
+
/>
|
|
1057
|
+
```
|
|
1058
|
+
|
|
1059
|
+
> **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).
|
|
1060
|
+
|
|
1061
|
+
### `onCameraChange`
|
|
1062
|
+
|
|
1063
|
+
Optional callback fired whenever the camera controls update the camera.
|
|
1064
|
+
|
|
1065
|
+
Current callback shape:
|
|
1066
|
+
|
|
1067
|
+
```ts
|
|
1068
|
+
(camera: Camera, controls: CameraControls) => void
|
|
1069
|
+
```
|
|
1070
|
+
|
|
1071
|
+
### `showObjectBindingDataPanel`
|
|
1072
|
+
|
|
1073
|
+
Optional boolean that controls whether the built-in left object details panel is rendered.
|
|
1074
|
+
|
|
1075
|
+
Default:
|
|
1076
|
+
|
|
1077
|
+
```ts
|
|
1078
|
+
true;
|
|
1079
|
+
```
|
|
1080
|
+
|
|
1081
|
+
### `customObjectBindingDataPanel`
|
|
1082
|
+
|
|
1083
|
+
Optional render prop for replacing the built-in left object details panel.
|
|
1084
|
+
|
|
1085
|
+
Current prop shape:
|
|
1086
|
+
|
|
1087
|
+
```ts
|
|
1088
|
+
type CustomObjectBindingDataPanelProps = {
|
|
1089
|
+
isOpen: boolean;
|
|
1090
|
+
selectedObject: ObjectBinding | null;
|
|
1091
|
+
currentAction: ObjectActionEvent | null;
|
|
1092
|
+
onClose: () => void;
|
|
1093
|
+
onAction: (event: ObjectActionEvent) => void;
|
|
1094
|
+
};
|
|
1095
|
+
```
|
|
1096
|
+
|
|
1097
|
+
When provided, `ModelViewer` passes its existing internal handlers into your custom panel. Your panel can call them, wrap them, or ignore them.
|
|
1098
|
+
|
|
1099
|
+
### `customSceneObjectsPanel`
|
|
1100
|
+
|
|
1101
|
+
Optional render prop for replacing the built-in right scene objects panel.
|
|
1102
|
+
|
|
1103
|
+
Current prop shape:
|
|
1104
|
+
|
|
1105
|
+
```ts
|
|
1106
|
+
type CustomSceneObjectsPanelProps = {
|
|
1107
|
+
objectBindings: Record<string, ObjectBinding>;
|
|
1108
|
+
onAction?: (event: ObjectActionEvent) => void;
|
|
1109
|
+
onFocus?: (binding: ObjectBinding) => void;
|
|
1110
|
+
onHover?: (binding: ObjectBinding | null) => void;
|
|
1111
|
+
};
|
|
1112
|
+
```
|
|
1113
|
+
|
|
1114
|
+
When provided, `ModelViewer` passes the current bindings plus its built-in action, focus, and hover handlers into your custom panel.
|
|
1115
|
+
|
|
1116
|
+
### `showSceneObjectsPanel`
|
|
1117
|
+
|
|
1118
|
+
Optional boolean that controls whether the built-in right scene objects panel is rendered.
|
|
1119
|
+
|
|
1120
|
+
Default:
|
|
1121
|
+
|
|
1122
|
+
```ts
|
|
1123
|
+
true;
|
|
1124
|
+
```
|
|
1125
|
+
|
|
1126
|
+
### `showDownloadButton`
|
|
1127
|
+
|
|
1128
|
+
Optional boolean that enables the download split-button (export GLB / export PNG screenshot).
|
|
1129
|
+
|
|
1130
|
+
Default:
|
|
1131
|
+
|
|
1132
|
+
```ts
|
|
1133
|
+
true;
|
|
1134
|
+
```
|
|
1135
|
+
|
|
1136
|
+
The bottom-right action bar only renders at all when at least one of `showResetButton`, `showDownloadButtons`, or (`showAnnotationNavigation` with annotations present) is `true`. Within that bar, the download button itself requires **both** `showDownloadButtons` and `showDownloadButton` to be `true`.
|
|
1137
|
+
|
|
1138
|
+
### `downloadFilename`
|
|
1139
|
+
|
|
1140
|
+
Optional filename stem for the built-in model export button.
|
|
1141
|
+
|
|
1142
|
+
Default:
|
|
1143
|
+
|
|
1144
|
+
```ts
|
|
1145
|
+
"model";
|
|
1146
|
+
```
|
|
1147
|
+
|
|
1148
|
+
### `showResetButton`
|
|
1149
|
+
|
|
1150
|
+
Optional boolean that shows the "reset camera view" button in the bottom-right action bar.
|
|
1151
|
+
|
|
1152
|
+
Default:
|
|
1153
|
+
|
|
1154
|
+
```ts
|
|
1155
|
+
true;
|
|
1156
|
+
```
|
|
1157
|
+
|
|
1158
|
+
### `showDownloadButtons`
|
|
1159
|
+
|
|
1160
|
+
Optional boolean — master toggle for the download split-button area in the bottom-right action bar. Set to `false` to hide it regardless of `showDownloadButton`.
|
|
1161
|
+
|
|
1162
|
+
Default:
|
|
1163
|
+
|
|
1164
|
+
```ts
|
|
1165
|
+
true;
|
|
1166
|
+
```
|
|
1167
|
+
|
|
1168
|
+
### `showLoadingOverlay`
|
|
1169
|
+
|
|
1170
|
+
Optional boolean controlling whether the built-in loading overlay is shown while the model is loading and the initial camera fit is settling.
|
|
1171
|
+
|
|
1172
|
+
Default:
|
|
1173
|
+
|
|
1174
|
+
```ts
|
|
1175
|
+
true;
|
|
1176
|
+
```
|
|
1177
|
+
|
|
1178
|
+
### `showMouseController`
|
|
1179
|
+
|
|
1180
|
+
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.
|
|
1181
|
+
|
|
1182
|
+
Default:
|
|
1183
|
+
|
|
1184
|
+
```ts
|
|
1185
|
+
false;
|
|
1186
|
+
```
|
|
1187
|
+
|
|
1188
|
+
### `mouseControllerPosition`
|
|
1189
|
+
|
|
1190
|
+
Optional placement for the on-screen controller.
|
|
1191
|
+
|
|
1192
|
+
```ts
|
|
1193
|
+
type MouseControllerPosition =
|
|
1194
|
+
| "bottom-left"
|
|
1195
|
+
| "bottom-right"
|
|
1196
|
+
| "top-left"
|
|
1197
|
+
| "top-right"
|
|
1198
|
+
| "center"
|
|
1199
|
+
| "center-bottom"
|
|
1200
|
+
| "center-top";
|
|
1201
|
+
```
|
|
1202
|
+
|
|
1203
|
+
### `mouseControllerOpacity`
|
|
1204
|
+
|
|
1205
|
+
Optional opacity for the on-screen controller.
|
|
1206
|
+
|
|
1207
|
+
Default:
|
|
1208
|
+
|
|
1209
|
+
```ts
|
|
1210
|
+
1;
|
|
1211
|
+
```
|
|
1212
|
+
|
|
1213
|
+
### `moveSensitivity`
|
|
1214
|
+
|
|
1215
|
+
Optional movement sensitivity for the on-screen controller.
|
|
1216
|
+
|
|
1217
|
+
Default:
|
|
1218
|
+
|
|
1219
|
+
```ts
|
|
1220
|
+
0.08;
|
|
1221
|
+
```
|
|
1222
|
+
|
|
1223
|
+
### `zoomSensitivity`
|
|
1224
|
+
|
|
1225
|
+
Optional zoom sensitivity for the underlying `CameraControls` zoom interaction (mouse wheel / trackpad / pinch).
|
|
1226
|
+
|
|
1227
|
+
Default:
|
|
1228
|
+
|
|
1229
|
+
```ts
|
|
1230
|
+
1.0;
|
|
1231
|
+
```
|
|
1232
|
+
|
|
1233
|
+
Example:
|
|
1234
|
+
|
|
1235
|
+
```tsx
|
|
1236
|
+
<ModelViewer
|
|
1237
|
+
modelUrl="/model.glb"
|
|
1238
|
+
licenseKey="your-license-key"
|
|
1239
|
+
objectBindings={objectBindings}
|
|
1240
|
+
showMouseController
|
|
1241
|
+
mouseControllerPosition="bottom-right"
|
|
1242
|
+
mouseControllerOpacity={0.8}
|
|
1243
|
+
moveSensitivity={0.1}
|
|
1244
|
+
zoomSensitivity={1.2}
|
|
1245
|
+
/>
|
|
1246
|
+
```
|
|
1247
|
+
|
|
1248
|
+
### `sceneConfig`
|
|
1249
|
+
|
|
1250
|
+
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.
|
|
1251
|
+
|
|
1252
|
+
`sceneConfig.animations` drives autoplay and per-clip playback settings, and `sceneConfig.annotations` seeds the annotation markers rendered on the model.
|
|
1253
|
+
|
|
1254
|
+
### `disableZoom`
|
|
1255
|
+
|
|
1256
|
+
Optional boolean. When `true`, the viewer never zooms the camera to an object on selection (selection still highlights and fires callbacks).
|
|
1257
|
+
|
|
1258
|
+
Default:
|
|
1259
|
+
|
|
1260
|
+
```ts
|
|
1261
|
+
false;
|
|
1262
|
+
```
|
|
1263
|
+
|
|
1264
|
+
### `zoomOnSelected`
|
|
1265
|
+
|
|
1266
|
+
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`.
|
|
1267
|
+
|
|
1268
|
+
Default:
|
|
1269
|
+
|
|
1270
|
+
```ts
|
|
1271
|
+
true;
|
|
1272
|
+
```
|
|
1273
|
+
|
|
1274
|
+
### `onAutoFit`
|
|
1275
|
+
|
|
1276
|
+
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`.
|
|
1277
|
+
|
|
1278
|
+
Current callback shape:
|
|
1279
|
+
|
|
1280
|
+
```ts
|
|
1281
|
+
() => Promise<boolean>;
|
|
1282
|
+
```
|
|
1283
|
+
|
|
1284
|
+
### `refitOnResize`
|
|
1285
|
+
|
|
1286
|
+
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.
|
|
1287
|
+
|
|
1288
|
+
Default:
|
|
1289
|
+
|
|
1290
|
+
```ts
|
|
1291
|
+
true;
|
|
1292
|
+
```
|
|
1293
|
+
|
|
1294
|
+
### `onAnnotationsChange`
|
|
1295
|
+
|
|
1296
|
+
Optional callback fired when the annotation markers on the model change (added, edited, or removed through the viewer UI).
|
|
1297
|
+
|
|
1298
|
+
Current callback shape:
|
|
1299
|
+
|
|
1300
|
+
```ts
|
|
1301
|
+
(annotations: AnnotationMarker[]) => void
|
|
1302
|
+
```
|
|
1303
|
+
|
|
1304
|
+
Where `AnnotationMarker` is the `SceneAnnotationMarker` shape:
|
|
1305
|
+
|
|
1306
|
+
```ts
|
|
1307
|
+
type AnnotationMarker = {
|
|
1308
|
+
id: number;
|
|
1309
|
+
worldPosition: [number, number, number];
|
|
1310
|
+
localPosition: [number, number, number];
|
|
1311
|
+
title: string;
|
|
1312
|
+
description: string;
|
|
1313
|
+
};
|
|
1314
|
+
```
|
|
1315
|
+
|
|
1316
|
+
### `activeAnnotation` / `onActiveAnnotationChange`
|
|
1317
|
+
|
|
1318
|
+
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).
|
|
1319
|
+
|
|
1320
|
+
Current shapes:
|
|
1321
|
+
|
|
1322
|
+
```ts
|
|
1323
|
+
activeAnnotation?: AnnotationMarker | null;
|
|
1324
|
+
onActiveAnnotationChange?: (annotation: AnnotationMarker | null) => void;
|
|
1325
|
+
```
|
|
1326
|
+
|
|
1327
|
+
### `renderMode`
|
|
1328
|
+
|
|
1329
|
+
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.
|
|
1330
|
+
|
|
1331
|
+
### `maxDpr`
|
|
1332
|
+
|
|
1333
|
+
Optional upper bound for the device pixel ratio used when rendering, so retina/4K displays don't render at full 2–3x cost.
|
|
1334
|
+
|
|
1335
|
+
Default:
|
|
1336
|
+
|
|
1337
|
+
```ts
|
|
1338
|
+
2;
|
|
1339
|
+
```
|
|
1340
|
+
|
|
1341
|
+
### `performanceProfile`
|
|
1342
|
+
|
|
1343
|
+
Optional control over how aggressively the viewer trades visual fidelity for a stable WebGL context on constrained GPUs.
|
|
1344
|
+
|
|
1345
|
+
```ts
|
|
1346
|
+
performanceProfile?: "auto" | "high" | "low";
|
|
1347
|
+
```
|
|
1348
|
+
|
|
1349
|
+
- `"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.
|
|
1350
|
+
- `"high"` — always render at full quality (postprocessing, soft shadows, full `maxDpr`), even on mobile. Use when you know the target devices can handle it.
|
|
1351
|
+
- `"low"` — always apply the reduced profile, on any device.
|
|
1352
|
+
|
|
1353
|
+
Default:
|
|
1354
|
+
|
|
1355
|
+
```ts
|
|
1356
|
+
"auto";
|
|
1357
|
+
```
|
|
1358
|
+
|
|
1359
|
+
### `dracoDecoderPath` / `ktx2TranscoderPath` / `meshopt`
|
|
1360
|
+
|
|
1361
|
+
Optional controls for the compressed-asset decoders used when loading the GLB/GLTF asset.
|
|
1362
|
+
|
|
1363
|
+
```ts
|
|
1364
|
+
dracoDecoderPath?: string | false;
|
|
1365
|
+
ktx2TranscoderPath?: string | false;
|
|
1366
|
+
meshopt?: boolean;
|
|
1367
|
+
```
|
|
1368
|
+
|
|
1369
|
+
DRACO, Meshopt, and KTX2 decoding are enabled by default via hosted decoder/transcoder bundles, fetched lazily only when the model needs them. Pass a path to self-host the decoder/transcoder files, or `false` to disable that decoder.
|
|
1370
|
+
|
|
1371
|
+
### `showMeasureTools`
|
|
1372
|
+
|
|
1373
|
+
Optional boolean that shows a click-to-measure toolbar over the canvas: click two points on the model for a distance readout, plus a toggleable bounding-box dimensions overlay.
|
|
1374
|
+
|
|
1375
|
+
Default:
|
|
1376
|
+
|
|
1377
|
+
```ts
|
|
1378
|
+
false;
|
|
1379
|
+
```
|
|
1380
|
+
|
|
1381
|
+
### `measurementUnit`
|
|
1382
|
+
|
|
1383
|
+
Optional unit suffix appended to measurement readouts. glTF models are authored in meters by spec, so values are not converted — this only changes the displayed label.
|
|
1384
|
+
|
|
1385
|
+
Default:
|
|
1386
|
+
|
|
1387
|
+
```ts
|
|
1388
|
+
"m";
|
|
1389
|
+
```
|
|
1390
|
+
|
|
1391
|
+
### `enableXR`
|
|
1392
|
+
|
|
1393
|
+
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.
|
|
1394
|
+
|
|
1395
|
+
On entering a session the model is normalized (scaled to ~1 m on its longest axis) and rested on the floor a short distance in front of the viewer, so large/real-scale scenes are framed instead of engulfing the viewer.
|
|
1396
|
+
|
|
1397
|
+
**AR placement gestures** — inside an AR session the viewer now uses an explicit placement flow:
|
|
1398
|
+
|
|
1399
|
+
- before placement, a bottom label says **"Move your device over a flat surface"** until a hit-testable surface is found
|
|
1400
|
+
- once a surface is found, a reticle appears on it and the label changes to **"Tap to place"**
|
|
1401
|
+
- tapping the screen or the label places the model at the reticle
|
|
1402
|
+
- after placement, **pinch** (two fingers) resizes the model and **twist** rotates it around its vertical axis without changing its position
|
|
1403
|
+
- 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
|
|
1404
|
+
|
|
1405
|
+
VR sessions keep the initial framed placement (there are no real surfaces to hit-test against).
|
|
1406
|
+
|
|
1407
|
+
> 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.
|
|
1408
|
+
|
|
1409
|
+
Default:
|
|
1410
|
+
|
|
1411
|
+
```ts
|
|
1412
|
+
false;
|
|
1413
|
+
```
|
|
1414
|
+
|
|
1415
|
+
### `usdzUrl`
|
|
1416
|
+
|
|
1417
|
+
Optional `USDZ` URL used as an Apple-device fallback when `enableXR` is `true`.
|
|
1418
|
+
On iPhone/iPad, if WebXR AR is unavailable and this prop is provided, the viewer
|
|
1419
|
+
shows a **"View in AR"** button that launches Apple Quick Look for the given
|
|
1420
|
+
asset.
|
|
1421
|
+
|
|
1422
|
+
```ts
|
|
1423
|
+
usdzUrl?: string;
|
|
1424
|
+
```
|
|
1425
|
+
|
|
1426
|
+
Notes:
|
|
1427
|
+
|
|
1428
|
+
- this should point to a `.usdz` file that Safari can fetch directly
|
|
1429
|
+
- Android / WebXR-capable devices ignore this and continue using the normal WebXR AR flow
|
|
1430
|
+
- if `usdzUrl` is omitted, Apple handheld devices simply won't get an AR button
|
|
1431
|
+
|
|
1432
|
+
### `showAnnotationNavigation`
|
|
1433
|
+
|
|
1434
|
+
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.
|
|
1435
|
+
|
|
1436
|
+
Default:
|
|
1437
|
+
|
|
1438
|
+
```ts
|
|
1439
|
+
true;
|
|
1440
|
+
```
|
|
1441
|
+
|
|
1442
|
+
### Theming the scene objects / object binding panels
|
|
1443
|
+
|
|
1444
|
+
The scene objects panel and object binding data panel read their colors,
|
|
1445
|
+
borders, shadows, and fonts from CSS custom properties (`--ri-sidepanel-*`
|
|
1446
|
+
and `--ri-databinding-*`) instead of hardcoded values, each with the current
|
|
1447
|
+
look baked in as a fallback. Override them on a `.ri-viewer` ancestor to
|
|
1448
|
+
re-skin those panels:
|
|
1449
|
+
|
|
1450
|
+
```css
|
|
1451
|
+
.my-app .ri-viewer {
|
|
1452
|
+
--ri-sidepanel-bg: #1a0033;
|
|
1453
|
+
--ri-sidepanel-accent: #ff2d75;
|
|
1454
|
+
}
|
|
1455
|
+
```
|
|
1456
|
+
|
|
1457
|
+
See the full variable list in the [`ModelViewer` docs](https://react-immersive.liveroom.dev/docs/reference/model-viewer#theming).
|
|
1458
|
+
|
|
1459
|
+
## `SimpleModelViewer` Public API
|
|
1460
|
+
|
|
1461
|
+
```ts
|
|
1462
|
+
type SimpleModelViewerProps = {
|
|
1463
|
+
modelUrl: string;
|
|
1464
|
+
backgroundColor?: string;
|
|
1465
|
+
showSceneObjectsPanel?: boolean;
|
|
1466
|
+
showSceneSettingsPanel?: boolean;
|
|
1467
|
+
enableModelUpload?: boolean;
|
|
1468
|
+
zoomOnSelected?: boolean;
|
|
1469
|
+
highlightOnHover?: boolean;
|
|
1470
|
+
showLoadingOverlay?: boolean;
|
|
1471
|
+
refitOnResize?: boolean;
|
|
1472
|
+
// Initial values for the scene settings (environment) panel
|
|
1473
|
+
backgroundEnabled?: boolean;
|
|
1474
|
+
autoRotate?: boolean;
|
|
1475
|
+
envPreset?: PresetsType; // @react-three/drei environment preset
|
|
1476
|
+
exposure?: number;
|
|
1477
|
+
ambientIntensity?: number;
|
|
1478
|
+
ambientColor?: string;
|
|
1479
|
+
directionalIntensity?: number;
|
|
1480
|
+
directionalColor?: string;
|
|
1481
|
+
onModelLoaded?: (scene: Object3D) => void;
|
|
1482
|
+
onLoadError?: (error: unknown) => void;
|
|
1483
|
+
};
|
|
1484
|
+
```
|
|
1485
|
+
|
|
1486
|
+
`SimpleModelViewer` does not take a `licenseKey` — licensing is enforced by `ModelViewer` and `BindingBuilder` only.
|
|
1487
|
+
|
|
1488
|
+
### `modelUrl`
|
|
1489
|
+
|
|
1490
|
+
URL or public path to the `.glb` or `.gltf` model to inspect. For `.gltf` URLs with external `.bin` or texture files, those files must be hosted at the relative paths declared in the `.gltf`.
|
|
1491
|
+
|
|
1492
|
+
### `backgroundColor`
|
|
1493
|
+
|
|
1494
|
+
Optional canvas background color.
|
|
1495
|
+
|
|
1496
|
+
Default:
|
|
1497
|
+
|
|
1498
|
+
```ts
|
|
1499
|
+
"#1a1a1a";
|
|
1500
|
+
```
|
|
1501
|
+
|
|
1502
|
+
### `showSceneObjectsPanel`
|
|
1503
|
+
|
|
1504
|
+
Optional boolean that controls whether the right-side scene objects panel is rendered.
|
|
1505
|
+
|
|
1506
|
+
Default:
|
|
1507
|
+
|
|
1508
|
+
```ts
|
|
1509
|
+
true;
|
|
1510
|
+
```
|
|
1511
|
+
|
|
1512
|
+
### `showSceneSettingsPanel`
|
|
1513
|
+
|
|
1514
|
+
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.
|
|
1515
|
+
|
|
1516
|
+
Default:
|
|
1517
|
+
|
|
1518
|
+
```ts
|
|
1519
|
+
true;
|
|
1520
|
+
```
|
|
1521
|
+
|
|
1522
|
+
### Scene settings defaults
|
|
1523
|
+
|
|
1524
|
+
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.
|
|
1525
|
+
|
|
1526
|
+
| Prop | Type | Panel control | Default |
|
|
1527
|
+
| ---------------------- | ------------- | ----------------------- | ----------- |
|
|
1528
|
+
| `backgroundEnabled` | `boolean` | Background on/off | `false` |
|
|
1529
|
+
| `autoRotate` | `boolean` | Auto Rotate | `false` |
|
|
1530
|
+
| `envPreset` | `PresetsType` | Environment preset | `"city"` |
|
|
1531
|
+
| `exposure` | `number` | Exposure | `1` |
|
|
1532
|
+
| `ambientIntensity` | `number` | Ambient Light Intensity | `0.6` |
|
|
1533
|
+
| `ambientColor` | `string` | Ambient Light Color | `"#ffffff"` |
|
|
1534
|
+
| `directionalIntensity` | `number` | Direct Light Intensity | `1.2` |
|
|
1535
|
+
| `directionalColor` | `string` | Direct Light Color | `"#ffffff"` |
|
|
1536
|
+
|
|
1537
|
+
`envPreset` accepts any `@react-three/drei` `PresetsType` (for example `"city"`, `"sunset"`, `"dawn"`, `"night"`, `"warehouse"`, `"forest"`, `"apartment"`, `"studio"`, `"park"`, `"lobby"`).
|
|
1538
|
+
|
|
1539
|
+
```tsx
|
|
1540
|
+
<SimpleModelViewer
|
|
1541
|
+
modelUrl="/model.glb"
|
|
1542
|
+
backgroundEnabled
|
|
1543
|
+
envPreset="sunset"
|
|
1544
|
+
exposure={1.2}
|
|
1545
|
+
ambientIntensity={0.4}
|
|
1546
|
+
directionalColor="#fff4e0"
|
|
1547
|
+
autoRotate
|
|
1548
|
+
/>
|
|
1549
|
+
```
|
|
1550
|
+
|
|
1551
|
+
### `zoomOnSelected`
|
|
1552
|
+
|
|
1553
|
+
Optional boolean. When `true`, selecting a mesh (via click or the objects panel) zooms/frames the camera on it.
|
|
1554
|
+
|
|
1555
|
+
Default:
|
|
1556
|
+
|
|
1557
|
+
```ts
|
|
1558
|
+
false;
|
|
1559
|
+
```
|
|
1560
|
+
|
|
1561
|
+
### `highlightOnHover`
|
|
1562
|
+
|
|
1563
|
+
Optional boolean. When `true`, hovering a mesh (in the viewport or the objects panel) highlights it.
|
|
1564
|
+
|
|
1565
|
+
Default:
|
|
1566
|
+
|
|
1567
|
+
```ts
|
|
1568
|
+
false;
|
|
1569
|
+
```
|
|
1570
|
+
|
|
1571
|
+
### `showLoadingOverlay`
|
|
1572
|
+
|
|
1573
|
+
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.
|
|
1574
|
+
|
|
1575
|
+
Default:
|
|
1576
|
+
|
|
1577
|
+
```ts
|
|
1578
|
+
true;
|
|
1579
|
+
```
|
|
1580
|
+
|
|
1581
|
+
### `refitOnResize`
|
|
1582
|
+
|
|
1583
|
+
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.
|
|
1584
|
+
|
|
1585
|
+
Default:
|
|
1586
|
+
|
|
1587
|
+
```ts
|
|
1588
|
+
true;
|
|
1589
|
+
```
|
|
1590
|
+
|
|
1591
|
+
### `enableModelUpload`
|
|
1592
|
+
|
|
1593
|
+
Optional boolean that swaps the fixed `modelUrl` workflow for a built-in upload UI. When enabled, the viewer starts with a drag-and-drop / file-picker empty state and loads the uploaded `.glb`, standalone/data-URI `.gltf`, or `.zip` containing a `.gltf` plus its referenced `.bin` and texture files instead of the asset passed via `modelUrl`.
|
|
1594
|
+
|
|
1595
|
+
Default:
|
|
1596
|
+
|
|
1597
|
+
```ts
|
|
1598
|
+
false;
|
|
1599
|
+
```
|
|
1600
|
+
|
|
1601
|
+
### `onModelLoaded`
|
|
1602
|
+
|
|
1603
|
+
Optional callback fired after the GLB/GLTF scene has loaded.
|
|
1604
|
+
|
|
1605
|
+
### `onLoadError`
|
|
1606
|
+
|
|
1607
|
+
Optional callback fired if the model fails to load or render.
|
|
1608
|
+
|
|
1609
|
+
## Object Binding Shape
|
|
1610
|
+
|
|
1611
|
+
The current source defines these binding types internally:
|
|
1612
|
+
|
|
1613
|
+
```ts
|
|
1614
|
+
type ObjectBindingAction = {
|
|
1615
|
+
id: string;
|
|
1616
|
+
label: string;
|
|
1617
|
+
type: "command";
|
|
1618
|
+
};
|
|
1619
|
+
|
|
1620
|
+
type ObjectBindingCameraState = {
|
|
1621
|
+
position?: [number, number, number];
|
|
1622
|
+
target?: [number, number, number];
|
|
1623
|
+
fov?: number;
|
|
1624
|
+
zoom?: number;
|
|
1625
|
+
};
|
|
1626
|
+
|
|
1627
|
+
type ObjectBindingMaterial = {
|
|
1628
|
+
// exported by the package; includes texture/baseColor plus the supported
|
|
1629
|
+
// MeshPhysicalMaterial overrides used by the viewer and BindingBuilder
|
|
1630
|
+
};
|
|
1631
|
+
|
|
1632
|
+
type ObjectBindingStyle = {
|
|
1633
|
+
material?: ObjectBindingMaterial;
|
|
1634
|
+
};
|
|
1635
|
+
|
|
1636
|
+
type ObjectBinding = {
|
|
1637
|
+
id: string;
|
|
1638
|
+
type: ObjectBindingType; // "body" | "light" | "wheel" | "glass" | "interior" | "floor" | "wall" | "door" | "furniture" | "decor" | "electronics" | "other" | ...
|
|
1639
|
+
modelObjectId: string;
|
|
1640
|
+
label?: string;
|
|
1641
|
+
visible?: boolean;
|
|
1642
|
+
selectable?: boolean;
|
|
1643
|
+
hoverable?: boolean;
|
|
1644
|
+
style?: ObjectBindingStyle;
|
|
1645
|
+
actions?: ObjectBindingAction[];
|
|
1646
|
+
metrics?: Record<string, number>;
|
|
1647
|
+
metadata?: Record<string, unknown>;
|
|
1648
|
+
cameraState?: ObjectBindingCameraState;
|
|
1649
|
+
};
|
|
1650
|
+
```
|
|
1651
|
+
|
|
1652
|
+
Notes:
|
|
1653
|
+
|
|
1654
|
+
- the `objectBindings` record is keyed by node name, not by `id`
|
|
1655
|
+
- `visible` is live render state, not just an initial default
|
|
1656
|
+
- `style.material.baseColor` is read by the renderer and is the committed source of truth for color picks
|
|
1657
|
+
- `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)
|
|
1658
|
+
- 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
|
|
1659
|
+
- 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
|
|
1660
|
+
- 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
|
|
1661
|
+
- `BindingBuilder` intentionally omits exact Sketchfab glossiness, cavity, and subsurface-scattering workflows; those require preprocessing or a different material pipeline
|
|
1662
|
+
- `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`
|
|
1663
|
+
- `metrics` and `metadata` are displayed in the side panel JSON preview
|
|
1664
|
+
- `ObjectBindingType` is a large union covering vehicles, rooms, furniture, characters, weapons, environment, and more
|
|
1665
|
+
|
|
1666
|
+
## Supported Built-In Actions
|
|
1667
|
+
|
|
1668
|
+
Action handling is currently hardcoded in `ModelViewer`.
|
|
1669
|
+
|
|
1670
|
+
### Implemented behaviors
|
|
1671
|
+
|
|
1672
|
+
- `toggle-visibility`
|
|
1673
|
+
Hides or shows the selected mesh by setting `binding.visible` and calling `onObjectBindingsChange`
|
|
1674
|
+
- `change-color`
|
|
1675
|
+
Opens the color picker; the chosen hex is applied as a live preview while dragging and committed into `binding.style.material.baseColor` via `onObjectBindingsChange` when the picker closes
|
|
1676
|
+
- `change-material`
|
|
1677
|
+
Opens the texture upload popup 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
|
|
1678
|
+
|
|
1679
|
+
## How Rendering Works
|
|
1680
|
+
|
|
1681
|
+
At a high level, the current viewer does the following:
|
|
1682
|
+
|
|
1683
|
+
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
|
|
1684
|
+
2. loads the GLB/GLTF asset with `useGLTF`
|
|
1685
|
+
3. looks up meshes by the keys in `objectBindings` (resolving `modelObjectId` to find the actual model node)
|
|
1686
|
+
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)
|
|
1687
|
+
5. applies runtime color, texture, and style overrides from `objectBindings` in a `useEffect` (not during render)
|
|
1688
|
+
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
|
|
1689
|
+
7. adds hover, selected, and panel-hover emissive highlighting
|
|
1690
|
+
8. creates an `AnimationMixer` for the loaded scene, ticked every frame via `useFrame`, and exposes playback controls through `onAnimationsReady`
|
|
1691
|
+
9. renders a side panel for the selected object and its configured actions
|
|
1692
|
+
|
|
1693
|
+
Only objects listed in `objectBindings` are rendered interactively by the current `Model` component.
|
|
1694
|
+
|
|
1695
|
+
### Lighting, shadows, and post-processing
|
|
1696
|
+
|
|
1697
|
+
When `shadows` is enabled (the default), the canvas renders soft shadow maps and the viewer applies:
|
|
1698
|
+
|
|
1699
|
+
- a default light rig (`Lights`) with ambient + hemisphere fill and a directional key light that casts shadows, sized to the model
|
|
1700
|
+
- 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
|
|
1701
|
+
- a post-processing stack (`EffectComposer`) with SSAO ambient occlusion (normal pass enabled), bloom, vignette, and ACES filmic tone mapping
|
|
1702
|
+
|
|
1703
|
+
Pass `shadows={false}` to disable shadow rendering entirely, or pass a custom `lights` rig to replace the default lighting.
|
|
1704
|
+
|
|
1705
|
+
## UI Behavior
|
|
1706
|
+
|
|
1707
|
+
The current component includes its own UI shell:
|
|
1708
|
+
|
|
1709
|
+
- a viewer layout that fills its parent container
|
|
1710
|
+
- a sliding left side panel about `280px` wide
|
|
1711
|
+
- a canvas area that fills the remaining width
|
|
1712
|
+
- a floating color picker popover
|
|
1713
|
+
- a floating texture upload popover
|
|
1714
|
+
- a right scene objects panel with search and per-object visibility toggles
|
|
1715
|
+
|
|
1716
|
+
Because of that, it works best when mounted in a container with an explicit height.
|
|
1717
|
+
|
|
1718
|
+
## Usage Expectations For Models
|
|
1719
|
+
|
|
1720
|
+
To use this library successfully, your GLB/GLTF asset should follow these expectations:
|
|
1721
|
+
|
|
1722
|
+
- the mesh node names must match the keys in `objectBindings` (or be referenced via `modelObjectId`)
|
|
1723
|
+
- target nodes should have geometry and a `MeshStandardMaterial`
|
|
1724
|
+
- the file should be accessible from the browser at `modelUrl`; hosted `.gltf` files must also serve external `.bin` and texture files at their declared relative paths
|
|
1725
|
+
|
|
1726
|
+
If a node name or material name does not match, that object will not render through the interactive binding flow.
|
|
1727
|
+
|
|
1728
|
+
## License
|
|
1729
|
+
|
|
1730
|
+
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.
|