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{ const values:Record={}; 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; }