voronstl/web/batch_glb_png/render.html

153 lines
4.8 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>