voronstl/web/batch_glb_png/render.html

153 lines
4.8 KiB
HTML
Raw Normal View History

2026-02-28 16:33:16 -08:00
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Headless GLB Renderer</title>
<style>
html, body { margin:0; padding:0; background:#fff; overflow:hidden; }
canvas { display:block; }
</style>
<script type="importmap">
{
"imports": {
"three": "./libs/three.module.js",
"three/addons/": "./vendor/three/addons/"
}
}
</script>
</head>
<body>
<script type="module">
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
function qs (k) { return new URLSearchParams(location.search).get(k); }
const glbUrl = qs('glb');
const profileUrl = qs('profile');
window.__RENDER_STATUS__ = { ready:false, error:null, pngDataUrl:null };
if (!glbUrl || !profileUrl) {
window.__RENDER_STATUS__.error = "Missing ?glb=... or ?profile=...";
throw new Error(window.__RENDER_STATUS__.error);
}
const profile = await (await fetch(profileUrl)).json();
// Renderer
const renderer = new THREE.WebGLRenderer({
antialias: true,
preserveDrawingBuffer: true,
alpha: false
});
renderer.setPixelRatio(profile.output?.pixelRatio ?? 1);
renderer.setSize(profile.output?.width ?? 1024, profile.output?.height ?? 768, false);
// Three r152+ color management (safe-guard)
if (THREE.SRGBColorSpace) renderer.outputColorSpace = THREE.SRGBColorSpace;
const bg = profile.scene?.background ?? 0xffffff;
renderer.setClearColor(bg, 1);
document.body.appendChild(renderer.domElement);
// Scene
const scene = new THREE.Scene();
// Camera
const cam = new THREE.PerspectiveCamera(
profile.camera?.fov ?? 50,
(profile.output?.width ?? 1024) / (profile.output?.height ?? 768),
profile.camera?.near ?? 0.1,
profile.camera?.far ?? 1000
);
cam.position.fromArray(profile.camera?.position ?? [0,0,10]);
cam.up.fromArray(profile.camera?.up ?? [0,1,0]);
// We wont use OrbitControls in headless; we just mimic its target by looking at it.
const target = new THREE.Vector3().fromArray(profile.controls?.target ?? [0,0,0]);
cam.lookAt(target);
cam.updateProjectionMatrix();
// Lights
const dir = new THREE.DirectionalLight(0xffffff, profile.lights?.directional?.intensity ?? 2.0);
dir.position.fromArray(profile.lights?.directional?.position ?? [5,5,5]);
scene.add(dir);
const amb = new THREE.AmbientLight(0xffffff, profile.lights?.ambient?.intensity ?? 0.5);
scene.add(amb);
// Load GLB
const loader = new GLTFLoader();
const gltf = await loader.loadAsync(glbUrl);
const root = gltf.scene;
scene.add(root);
// --- Fit-to-frame while keeping the profile's viewing direction ---
const box = new THREE.Box3().setFromObject(root);
const size = box.getSize(new THREE.Vector3());
const center = box.getCenter(new THREE.Vector3());
// Use profile target if present, but still re-center model so target makes sense
const profTarget = new THREE.Vector3().fromArray(profile.controls?.target ?? [0, 0, 0]);
// Shift the model so its center sits at the profile target (usually [0,0,0] or your saved target)
root.position.sub(center).add(profTarget);
// Recompute bounds after shifting
const box2 = new THREE.Box3().setFromObject(root);
const size2 = box2.getSize(new THREE.Vector3());
const center2 = box2.getCenter(new THREE.Vector3());
// Keep the same view direction as the profile camera->target vector
const profCamPos = new THREE.Vector3().fromArray(profile.camera?.position ?? [0, 0, 10]);
const viewDir = profCamPos.clone().sub(profTarget).normalize();
// Compute distance so the whole object fits the camera FOV
const maxDim = Math.max(size2.x, size2.y, size2.z, 1e-6);
const fov = cam.fov * (Math.PI / 180);
const fitPadding = profile.camera?.fitPadding ?? 1.15; // you can add this to JSON later
const distance = (maxDim / 2) / Math.tan(fov / 2) * fitPadding;
// Position camera and aim
cam.position.copy(center2).add(viewDir.multiplyScalar(distance));
cam.lookAt(center2);
cam.updateProjectionMatrix();
// Adjust near/far safely
cam.near = Math.max(0.01, distance / 100);
cam.far = distance * 100;
cam.updateProjectionMatrix();
// Apply renderParams
const wireframe = !!profile.renderParams?.wireframe;
root.traverse((obj) => {
if (obj.isMesh && obj.material) {
// If multi-material
if (Array.isArray(obj.material)) {
obj.material.forEach(m => { m.wireframe = wireframe; m.needsUpdate = true; });
} else {
obj.material.wireframe = wireframe;
obj.material.needsUpdate = true;
}
}
});
// Optional: edges overlay if you later add it (kept off unless you implement lines).
// profile.renderParams.edgeAngle is available; current lab.html uses it to build edges.
renderer.render(scene, cam);
// Give the GPU a beat (helps on some headless setups)
await new Promise(r => setTimeout(r, 50));
window.__RENDER_STATUS__.pngDataUrl = renderer.domElement.toDataURL('image/png');
window.__RENDER_STATUS__.ready = true;
</script>
</body>
</html>