ycb-simready / viewer.js
dogancan's picture
Viewer: start camera facing the soup can's label
bf2ef44 verified
Raw History Blame Contribute Delete
7.65 kB
import * as THREE from 'three';
import { OrbitControls } from './vendor/OrbitControls.js';
const cache=new Map();
export async function loadGeometry(asset){if(cache.has(asset.id))return cache.get(asset.id);const r=await fetch(asset.previewUrl);if(!r.ok)throw new Error('Preview not ready');const bytes=await r.arrayBuffer();const data=JSON.parse(new Uint8Array(bytes)[0]===31?await new Response(new Blob([bytes]).stream().pipeThrough(new DecompressionStream('gzip'))).text():new TextDecoder().decode(bytes));data.assetId=asset.id;cache.set(asset.id,data);if(cache.size>12)cache.delete(cache.keys().next().value);return data}
function makeScene(){const scene=new THREE.Scene();scene.add(new THREE.HemisphereLight(0xffffff,0x7c8490,1.7));for(const [x,y,z,power,shadow] of [[3.74,5.28,-3.73,3.2,true],[1.63,3,3.22,1.4,false]]){const l=new THREE.DirectionalLight(0xffffff,power);l.position.set(x,y,z);if(shadow){l.castShadow=true;l.shadow.mapSize.set(4096,4096);const c=l.shadow.camera;c.left=-4.5;c.right=4.5;c.top=4.5;c.bottom=-4.5;c.near=.5;c.far=16;l.shadow.bias=-.0004;l.shadow.normalBias=.02;l.shadow.radius=3.5;l.shadow.blurSamples=16}scene.add(l)}return scene}
const textureLoader=new THREE.TextureLoader(),textures=new Map();function texture(url){const key=url;if(!textures.has(key)){const t=textureLoader.load(url);t.colorSpace=THREE.SRGBColorSpace;t.anisotropy=4;textures.set(key,t)}return textures.get(key)}let showHulls=false,showCom=false;
function addOverlays(group,data,extent){const hulls=new THREE.Group(),com=new THREE.Group();
for(const h of data.collisions||[]){const g=new THREE.BufferGeometry();g.setAttribute('position',new THREE.Float32BufferAttribute(h.positions,3));g.setIndex(h.indices);g.computeVertexNormals();
const m=new THREE.Mesh(g,new THREE.MeshStandardMaterial({color:0x22c55e,transparent:true,opacity:.3,depthWrite:false,side:THREE.DoubleSide,roughness:.8}));m.renderOrder=2;
const e=new THREE.LineSegments(new THREE.EdgesGeometry(g,15),new THREE.LineBasicMaterial({color:0x15803d,transparent:true,opacity:.9}));e.renderOrder=3;hulls.add(m,e)}
if(data.com){const r=extent*.03,c=new THREE.Vector3(...data.com);
const dot=new THREE.Mesh(new THREE.SphereGeometry(r,24,16),new THREE.MeshBasicMaterial({color:0xe11d48,depthTest:false,transparent:true}));dot.position.copy(c);dot.renderOrder=10;
const pts=[];for(const a of [[1,0,0],[0,1,0],[0,0,1]]){const d=new THREE.Vector3(...a).multiplyScalar(r*3.5);pts.push(c.clone().sub(d),c.clone().add(d))}
const axes=new THREE.LineSegments(new THREE.BufferGeometry().setFromPoints(pts),new THREE.LineBasicMaterial({color:0xe11d48,depthTest:false,transparent:true}));axes.renderOrder=10;com.add(dot,axes)}
group.userData.overlay={hulls,com};group.add(hulls,com);applyOverlays(group)}
function applyOverlays(group){const o=group?.userData.overlay;if(!o)return;o.hulls.visible=showHulls;o.com.visible=showCom;const xray=showHulls||showCom;
group.traverse(m=>{if(m.isMesh&&m.userData.visual){m.material.transparent=xray;m.material.opacity=xray?.28:1;m.material.depthWrite=!xray;m.material.needsUpdate=true}})}
function makeModel(data){const group=new THREE.Group();for(const p of data.meshes){const g=new THREE.BufferGeometry();g.setAttribute('position',new THREE.Float32BufferAttribute(p.positions,3));g.setIndex(p.indices);g.computeVertexNormals();const color=new THREE.Color().setRGB(...p.color);const opts={color,roughness:.55,metalness:.05,side:THREE.DoubleSide};if(p.uvs&&p.texture!=null){g.setAttribute('uv',new THREE.Float32BufferAttribute(p.uvs,2));opts.color=new THREE.Color(1,1,1);opts.map=texture(data.textureUrls[p.texture])}const vm=new THREE.Mesh(g,new THREE.MeshStandardMaterial(opts));vm.userData.visual=true;vm.castShadow=true;vm.receiveShadow=true;group.add(vm)}const box=new THREE.Box3().setFromObject(group),size=box.getSize(new THREE.Vector3()),center=box.getCenter(new THREE.Vector3());const scale=2/Math.max(size.x,size.y,size.z,.0001);group.position.set(-center.x*scale,-box.min.y*scale,-center.z*scale);group.scale.setScalar(scale);addOverlays(group,data,Math.max(size.x,size.y,size.z));return {group,height:size.y*scale}}
function dispose(group){group?.traverse(m=>{m.geometry?.dispose();if(m.material){m.material.map=null;m.material.dispose()}})}
const canvas=document.querySelector('#canvas'),host=document.querySelector('#viewport');const renderer=new THREE.WebGLRenderer({canvas,alpha:true,antialias:true});renderer.setPixelRatio(Math.min(devicePixelRatio,2));renderer.toneMapping=THREE.ACESFilmicToneMapping;renderer.toneMappingExposure=1.25;renderer.shadowMap.enabled=true;renderer.shadowMap.type=THREE.PCFShadowMap;const scene=makeScene(),camera=new THREE.PerspectiveCamera(38,1,.01,100),controls=new OrbitControls(camera,canvas);controls.enableDamping=true;controls.minDistance=.7;controls.maxDistance=10;controls.maxPolarAngle=Math.PI*.54;const grid=new THREE.GridHelper(10,50,0xc6cdd6,0xe1e5ea);grid.material.transparent=true;grid.material.opacity=.4;grid.position.y=-.005;scene.add(grid);const floor=new THREE.Mesh(new THREE.PlaneGeometry(20,20),new THREE.ShadowMaterial({opacity:.34}));floor.rotation.x=-Math.PI/2;floor.receiveShadow=true;scene.add(floor);let current=null,height=2,wire=false,request=0;
function reset(){camera.position.set(-0.76,2.2,-4.33);controls.target.set(0,height*.45,0);controls.update()}function resize(){const w=host.clientWidth,h=host.clientHeight;if(w&&h){renderer.setSize(w,h,false);camera.aspect=w/h;camera.updateProjectionMatrix()}}new ResizeObserver(resize).observe(host);document.querySelector('#reset-view').onclick=reset;document.querySelector('#wireframe').onclick=()=>{wire=!wire;current?.traverse(m=>{if(m.isMesh&&m.userData.visual)m.material.wireframe=wire});document.querySelector('#wireframe').setAttribute('aria-pressed',String(wire));document.querySelector('#wireframe').classList.toggle('active',wire)};renderer.setAnimationLoop(()=>{if(current&&!document.hidden&&!document.querySelector('#gallery').hidden){controls.update();renderer.render(scene,camera)}});
export function clear(){++request;if(current){scene.remove(current);dispose(current);current=null}renderer.clear()}
export async function show(asset){clear();const req=request;const data=await loadGeometry(asset);if(req!==request)return;const m=makeModel(data);current=m.group;height=m.height;current.traverse(o=>{if(o.isMesh&&o.userData.visual)o.material.wireframe=wire});scene.add(current);reset();resize();renderer.render(scene,camera)}
let thumbRenderer;const thumbs=new Map();export async function thumbnail(asset){if(thumbs.has(asset.id))return thumbs.get(asset.id);thumbRenderer??=new THREE.WebGLRenderer({alpha:true,antialias:true,preserveDrawingBuffer:true});thumbRenderer.setSize(320,240);thumbRenderer.toneMapping=THREE.ACESFilmicToneMapping;const data=await loadGeometry(asset),s=makeScene(),m=makeModel(data);s.add(m.group);const c=new THREE.PerspectiveCamera(38,4/3,.01,100);c.position.set(2.8,2.2,3.4);c.lookAt(0,m.height*.45,0);thumbRenderer.render(s,c);const url=thumbRenderer.domElement.toDataURL('image/png');dispose(m.group);thumbs.set(asset.id,url);return url}
for(const [id,label,icon,get,set] of [['toggle-hulls','Show collision hulls','⬢',()=>showHulls,v=>showHulls=v],['toggle-com','Show centre of mass','⊕',()=>showCom,v=>showCom=v]]){const b=document.createElement('button');b.id=id;b.className='icon-btn';b.type='button';b.title=label;b.setAttribute('aria-label',label);b.setAttribute('aria-pressed','false');b.textContent=icon;b.onclick=()=>{set(!get());b.setAttribute('aria-pressed',String(get()));b.classList.toggle('active',get());applyOverlays(current)};document.querySelector('#wireframe').before(b)}