@taranjeetsinghh/model-lab
v0.8.0-alpha.1
Published
A React Three Fiber 3D model inspector, animation authoring studio, and production sequence runtime.
Maintainers
Readme
@taranjeetsinghh/model-lab
Full guides, live examples, API reference, and architecture documentation are available in
apps/docs. Runnpm run dev:docsfrom the workspace root.
A React Three Fiber package for inspecting glTF models, authoring immersive camera/model/lighting sequences, and replaying those sequences in production websites.
Installation
npm install @taranjeetsinghh/model-lab three @react-three/fiber @react-three/drei react react-domImport the package stylesheet once in the application that uses the full editor:
import '@taranjeetsinghh/model-lab/styles.css'1. Full authoring studio
import { ModelLabEditor } from '@taranjeetsinghh/model-lab/editor'
import '@taranjeetsinghh/model-lab/styles.css'
export function StudioPage() {
return (
<div style={{ height: '100vh' }}>
<ModelLabEditor
dracoDecoderPath="/draco/"
onConfigChange={(config) => console.log(config)}
onSequenceChange={(sequence) => console.log(sequence)}
/>
</div>
)
}The editor includes local GLB/GLTF loading, scene/material inspection, transform controls, editable model pivots, texture UV controls, camera editing, model animations, bounding boxes, vertex normals, sequence keyframes, screenshots, and JSON export/import.
Imperative editor API
import { useRef } from 'react'
import {
ModelLabEditor,
type ModelLabEditorHandle,
} from '@taranjeetsinghh/model-lab/editor'
export function StudioPage() {
const editor = useRef<ModelLabEditorHandle>(null)
return (
<>
<button onClick={() => editor.current?.fitCamera()}>Fit</button>
<button onClick={() => console.log(editor.current?.getConfig())}>Read config</button>
<ModelLabEditor ref={editor} />
</>
)
}Available methods:
getConfig()getPreset()setConfig(partialConfig)loadFiles(files)openFiles()openFolder()fitCamera()reset()captureScreenshot()setPivot(point, { preserveVisualPosition })centerPivot({ preserveVisualPosition })resetPivot({ preserveVisualPosition })
Model pivot / rotation-axis editing
Some assets are exported with an origin far away from their visible geometry. Model Lab now places the imported asset below a pivot transform, so rotation, scale, transform gizmos, embedded glTF animation, and immersive sequences can all use a corrected axis without modifying geometry data.
Inside the editor, open Pivot / rotation axis and choose one of these workflows:
- Set to model center computes the center of the imported geometry bounds.
- Pick on model lets you click a visible surface or point-cloud particle.
- Pivot point accepts any exact XYZ value in the imported model's local space.
- Use imported origin restores
[0, 0, 0]. - Move pivot to world origin places the currently selected pivot at world position zero.
Keep Keep model visually in place enabled to move the pivot without moving the rendered asset. The same compensation is applied to every existing sequence keyframe.
Programmatic editor control
const editor = useRef<ModelLabEditorHandle>(null)
<button onClick={() => editor.current?.centerPivot()}>
Center rotation axis
</button>
<button
onClick={() =>
editor.current?.setPivot([1.25, 0.4, -2], {
preserveVisualPosition: true,
})
}
>
Use custom pivot
</button>Runtime preset
The pivot is included in the exported viewer JSON:
const config = createViewerConfig({
pivot: {
point: [1.25, 0.4, -2],
visible: false,
},
})
<ModelLabViewer src="/models/machine.glb" config={config} />For a controlled configuration that already has authored keyframes, use setViewerPivot() so the current model pose and sequence positions are compensated:
import { setViewerPivot } from '@taranjeetsinghh/model-lab/pivot'
const nextConfig = setViewerPivot(currentConfig, boundsCenter, {
preserveVisualPosition: true,
})Existing React Three Fiber canvas
import { ModelPivotGroup } from '@taranjeetsinghh/model-lab/pivot'
<ModelPivotGroup
ref={modelRef}
pivot={preset.viewer.pivot.point}
position={preset.viewer.model.position}
rotation={preset.viewer.model.rotation}
scale={preset.viewer.model.scale}
>
<primitive object={gltf.scene} />
</ModelPivotGroup>ModelPivotGroup.rotation uses degrees to match exported Model Lab configuration.
2. Production viewer
Export the complete preset from the editor and pass it directly to the lightweight runtime entry point. The preset contains camera, scene, model transforms, material values, texture UV settings, and the immersive sequence.
import {
ModelLabViewer,
parseViewerPreset,
} from '@taranjeetsinghh/model-lab/viewer'
import galaxyPresetJson from './galaxy-viewer-config.json'
const galaxyPreset = parseViewerPreset(galaxyPresetJson)
export function GalaxyHero() {
return (
<div style={{ height: '100vh' }}>
<ModelLabViewer
src="/models/galaxy.glb"
preset={galaxyPreset}
playSequence
sequenceLoop
controlsWhilePlaying={false}
dracoDecoderPath="/draco/"
/>
</div>
)
}config and sequence remain available as overrides. A sequence passed through the sequence prop takes precedence over the sequence stored in preset.viewer.sequence.
Replacement texture URLs
Textures already inside the GLB/GLTF are reused automatically. A texture uploaded through the editor exists only as a local browser file, so map its exported filename to a deployable URL:
<ModelLabViewer
src="/models/galaxy.glb"
preset={galaxyPreset}
textureUrls={{
'galaxy-color.webp': '/models/textures/galaxy-color.webp',
'galaxy-normal.webp': '/models/textures/galaxy-normal.webp',
}}
onTextureError={(name, error) => {
console.error(`Could not restore ${name}`, error)
}}
/>The runtime restores repeat, offset, center, rotation, wrapping, flipY, and color-space values after the texture loads.
Separate sequence export
ModelLabViewer accepts either a SequenceConfig or the complete 3d-model-lab-sequence/v1 payload:
import sequenceExport from './galaxy-warp.json'
<ModelLabViewer
src="/models/galaxy.glb"
preset={galaxyPreset}
sequence={sequenceExport}
playSequence
/>Scroll-driven sequence
<ModelLabViewer
src="/models/galaxy.glb"
preset={galaxyPreset}
playSequence={false}
sequenceProgress={scrollProgress}
/>sequenceProgress is normalized from 0 to 1. The package maps it onto the authored sequence duration.
Runtime viewer ref
const viewer = useRef<ModelLabViewerHandle>(null)
<ModelLabViewer ref={viewer} src="/models/galaxy.glb" />
viewer.current?.fitCamera()
const png = viewer.current?.captureScreenshot()Available methods:
getModel()getRenderer()getCamera()getPivot()getPivotWorldPosition()getBoundsCenter()fitCamera()captureScreenshot()setPivot(point, { preserveVisualPosition })centerPivot({ preserveVisualPosition })resetPivot({ preserveVisualPosition })
3. Headless sequence player
Use this when the website already has its own React Three Fiber canvas and model loader.
import { OrbitControls, useGLTF } from '@react-three/drei'
import { Canvas } from '@react-three/fiber'
import { useRef } from 'react'
import * as THREE from 'three'
import {
ImmersiveSequencePlayer,
ModelPivotGroup,
type OrbitControlsApi,
} from '@taranjeetsinghh/model-lab/sequence'
import sequencePayload from './galaxy-warp.json'
function Scene() {
const gltf = useGLTF('/models/galaxy.glb')
const modelRef = useRef<THREE.Group>(null)
const controlsRef = useRef<OrbitControlsApi>(null)
return (
<>
<ModelPivotGroup ref={modelRef} pivot={[0, 0, 0]}>
<primitive object={gltf.scene} />
</ModelPivotGroup>
<OrbitControls ref={controlsRef as never} />
<ImmersiveSequencePlayer
sequence={sequencePayload}
modelRef={modelRef}
controlsRef={controlsRef}
play
loop
/>
</>
)
}
export function ExistingCanvasExample() {
return <Canvas><Scene /></Canvas>
}Applying a preset in a custom scene
When an application owns its own loader and canvas, apply only the exported material/texture portion:
import { useEffect } from 'react'
import { useGLTF } from '@react-three/drei'
import { applyMaterialOverrides } from '@taranjeetsinghh/model-lab/viewer'
import preset from './galaxy-viewer-config.json'
function Galaxy() {
const gltf = useGLTF('/models/galaxy.glb')
useEffect(() => {
void applyMaterialOverrides(gltf.scene, preset.materials, {
textureUrls: {
'galaxy-color.webp': '/models/textures/galaxy-color.webp',
},
})
}, [gltf.scene])
return <primitive object={gltf.scene} />
}Clone the loaded scene first when the loader cache is shared by several component instances.
Configuration helpers
import {
createViewerConfig,
createFovWarpSequence,
sampleSequence,
} from '@taranjeetsinghh/model-lab'
const config = createViewerConfig({
camera: { fov: 48, position: [3, 2, 7] },
scene: { background: '#050711', grid: false },
})
const warp = createFovWarpSequence(config)
const frameAtOneSecond = sampleSequence(warp, 1)createViewerConfig() safely merges partial settings with defaults and creates independent arrays/keyframes.
Draco-compressed GLB files
The package exposes dracoDecoderPath. Copy Three.js Draco decoder files into the consuming app's public directory:
node_modules/three/examples/jsm/libs/draco/gltf/*
→ public/draco/*Then use:
<ModelLabViewer dracoDecoderPath="/draco/" ... />The same prop is available on ModelLabEditor.
Main exports
ModelLabEditor
ModelLabViewer
ImmersiveSequencePlayer
createViewerConfig
createFovWarpSequence
captureSnapshot
applySnapshot
sampleSequence
resolveSequence
createExportPayload
createSequencePayload
createModelBundle
DEFAULT_CONFIGAll configuration, sequence, callback, ref, and model-info TypeScript types are exported from the package root.
Package design
React, React DOM, Three.js, React Three Fiber, and Drei are peer dependencies. This keeps the host application in control of those versions and avoids shipping a second copy of Three.js inside the library bundle.
The build exposes focused entry points:
@taranjeetsinghh/model-lab— complete public API.@taranjeetsinghh/model-lab/editor— full authoring studio.@taranjeetsinghh/model-lab/viewer— production viewer and material-preset runtime.@taranjeetsinghh/model-lab/sequence— headless sequence player and sequence utilities.@taranjeetsinghh/model-lab/effects— headless cinematic material-effect driver.
SSR frameworks
WebGL needs a browser. In Next.js or another SSR framework, render the editor/viewer as a client component or load it dynamically with SSR disabled.
Publishing checklist
- Replace the placeholder package scope/name.
- Update author, repository, homepage, and bugs fields in
package.json. - Add screenshots and a hosted demo.
- Run
npm run typecheckandnpm run build. - Run
npm pack --dry-runand inspect the included files. - Publish with public access when using a public npm scope.
Cinematic material effects
ModelLabViewer can animate material properties and texture UV transforms alongside a camera sequence:
<ModelLabViewer
src="/models/galaxy.glb"
sequence={sequence}
playSequence
effect={{
type: 'cosmic-pulse',
intensity: 1.2,
speed: 0.2,
color: '#67e8f9',
}}
/>Supported effect names are cosmic-pulse, texture-vortex, hologram-scan, chrome-breathe, and none. The driver discovers mesh, line, and point-cloud materials; point assets receive animated size, opacity, and color treatments while compatible mesh materials receive roughness, metalness, emissive, wireframe, and UV treatments.
For deterministic scrolling, pass the same normalized progress used by the sequence:
<ModelLabViewer
src="/models/galaxy.glb"
sequence={sequence}
playSequence={false}
sequenceProgress={scrollProgress}
effect={{ type: 'texture-vortex', progress: scrollProgress }}
/>Advanced canvases can import the headless driver from @taranjeetsinghh/model-lab/effects. Inside ModelLabViewer, effects automatically receive render-loop sequence progress when effect.progress is omitted; an explicit progress value still overrides that synchronization for scrolling or scrubbing.
