openusd-explorer / src /studio.ts
ZIP Space Deployer
Deploy ZIP — 2026-10-26 20:15 UTC
0dfa89f
Raw History Blame Contribute Delete
3.24 kB
import * as THREE from 'three';
import {RoomEnvironment} from 'three/addons/environments/RoomEnvironment.js';
import {bodyMatrices,controllableJoints,homeProgress} from './vendor/kinematics.js';
export const STUDIO_DIRECTION=[1,0.65,1.4] as const;
export type StudioLighting={exposure?:number;environment?:number;hemisphere?:number;key?:number};
/** Shared studio, with optional batch lighting tuned for thumbnails on a pale canvas. */
export function createStudio(renderer:THREE.WebGLRenderer,lighting:StudioLighting={}){
renderer.setClearColor(0,0);
renderer.outputColorSpace=THREE.SRGBColorSpace;
renderer.toneMapping=THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure=lighting.exposure??1.35;
const scene=new THREE.Scene();
const pmrem=new THREE.PMREMGenerator(renderer),room=new RoomEnvironment();
const environment=pmrem.fromScene(room,0.04);
scene.environment=environment.texture;
scene.environmentIntensity=lighting.environment??1;
room.dispose();pmrem.dispose();
scene.add(new THREE.HemisphereLight(0xffffff,0xdce0ef,lighting.hemisphere??2));
const light=new THREE.DirectionalLight(0xffffff,lighting.key??3);
light.position.set(3,5,4);scene.add(light);
return {scene,dispose:()=>environment.dispose()};
}
export function setHomePose(model:any):Record<string,number>{
const values:Record<string,number>={};
for(const joint of controllableJoints(model.articulation))values[joint.id]=homeProgress(joint);
if(model.articulation)for(const [id,matrix] of Object.entries(bodyMatrices(model.articulation,values))){
const body=model.bodies.get(id);
if(body){body.matrixAutoUpdate=false;body.matrix.set(...matrix as number[]);body.matrixWorldNeedsUpdate=true;}
}
model.scene.updateMatrixWorld(true);
return values;
}
export function objectBounds(object:THREE.Object3D){
object.updateMatrixWorld(true);
const box=new THREE.Box3().setFromObject(object);
const size=box.getSize(new THREE.Vector3()),center=box.getCenter(new THREE.Vector3());
if(box.isEmpty()||![...size,...center].every(Number.isFinite))throw new Error('Empty or non-finite model bounds.');
return {box,center,radius:Math.max(size.length()/2,0.01)};
}
/** Fits all eight world bounds corners to an orthographic square, including thin objects. */
export function thumbnailCamera(object:THREE.Object3D,padding=1.12){
const {box,center,radius}=objectBounds(object);
const camera=new THREE.OrthographicCamera(-1,1,1,-1,radius/1000,radius*20);
camera.position.copy(center).add(new THREE.Vector3(...STUDIO_DIRECTION).normalize().multiplyScalar(radius*4));
camera.lookAt(center);camera.updateMatrixWorld(true);
const projected=new THREE.Box3();
for(const x of [box.min.x,box.max.x])for(const y of [box.min.y,box.max.y])for(const z of [box.min.z,box.max.z]){
projected.expandByPoint(new THREE.Vector3(x,y,z).applyMatrix4(camera.matrixWorldInverse));
}
const middle=projected.getCenter(new THREE.Vector3());
const extent=Math.max(projected.max.x-projected.min.x,projected.max.y-projected.min.y,radius/100)*padding/2;
camera.left=middle.x-extent;camera.right=middle.x+extent;
camera.bottom=middle.y-extent;camera.top=middle.y+extent;
camera.updateProjectionMatrix();
return camera;
}