git-subtree-dir: server git-subtree-mainline:c4e617ada7git-subtree-split:e64421295f
116 lines
3.7 KiB
JavaScript
116 lines
3.7 KiB
JavaScript
// 化身 GLB -> 36 帧旋转 PNG(绕 Y 轴 10° 步进),服务端预渲染。
|
||
// 用法: node render.js --glb <path> --out <dir> [--frames 36] [--size 256x512]
|
||
import { argv } from 'node:process';
|
||
import fs from 'node:fs';
|
||
import path from 'node:path';
|
||
import createGL from 'gl';
|
||
import { PNG } from 'pngjs';
|
||
import * as THREE from 'three';
|
||
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
||
|
||
function parseArgs() {
|
||
const a = {};
|
||
for (let i = 2; i < argv.length; i++) {
|
||
if (argv[i].startsWith('--')) {
|
||
const key = argv[i].slice(2);
|
||
const val = argv[i + 1] !== undefined && !argv[i + 1].startsWith('--') ? argv[++i] : true;
|
||
a[key] = val;
|
||
}
|
||
}
|
||
if (!a.glb || !a.out) {
|
||
console.error('用法: node render.js --glb <path> --out <dir> [--frames 36] [--size 256x512]');
|
||
process.exit(1);
|
||
}
|
||
a.frames = a.frames === true ? 36 : parseInt(a.frames, 10) || 36;
|
||
const [w, h] = (a.size === true ? '256x512' : String(a.size)).split('x').map(Number);
|
||
a.width = w || 256;
|
||
a.height = h || 512;
|
||
return a;
|
||
}
|
||
|
||
const args = parseArgs();
|
||
const { width, height, frames } = args;
|
||
|
||
const gl = createGL(width, height, { preserveDrawingBuffer: true });
|
||
if (!gl) {
|
||
console.error('headless-gl 初始化失败(容器内需 mesa/libglvnd)');
|
||
process.exit(2);
|
||
}
|
||
|
||
const canvas = {
|
||
width,
|
||
height,
|
||
style: {},
|
||
addEventListener: () => {},
|
||
removeEventListener: () => {},
|
||
clientWidth: width,
|
||
clientHeight: height,
|
||
getContext: () => gl,
|
||
};
|
||
|
||
const renderer = new THREE.WebGLRenderer({ canvas, context: gl, antialias: false });
|
||
renderer.setClearColor(0xffffff, 1);
|
||
renderer.setSize(width, height, false);
|
||
|
||
const scene = new THREE.Scene();
|
||
scene.add(new THREE.AmbientLight(0xffffff, 1.1));
|
||
const dirLight = new THREE.DirectionalLight(0xffffff, 1.4);
|
||
dirLight.position.set(3, 6, 4);
|
||
scene.add(dirLight);
|
||
scene.add(new THREE.DirectionalLight(0xffffff, 0.5).translateY(-4).translateX(-3));
|
||
|
||
const camera = new THREE.PerspectiveCamera(35, width / height, 0.1, 100);
|
||
|
||
const loader = new GLTFLoader();
|
||
|
||
const loadGlb = () =>
|
||
new Promise((resolve, reject) => {
|
||
// Buffer 需转成 ArrayBuffer 才能触发 GLB 头解析
|
||
const bin = fs.readFileSync(args.glb);
|
||
const ab = bin.buffer.slice(bin.byteOffset, bin.byteOffset + bin.byteLength);
|
||
loader.parse(ab, '', (gltf) => resolve(gltf.scene), (err) => reject(err));
|
||
});
|
||
|
||
loadGlb()
|
||
.then((object) => {
|
||
scene.add(object);
|
||
render(object);
|
||
})
|
||
.catch((err) => {
|
||
console.error('GLB 解析失败:', err && err.message ? err.message : err);
|
||
process.exit(3);
|
||
});
|
||
|
||
function render(object) {
|
||
// 包围盒 -> 相机半径与观测高度
|
||
const box = new THREE.Box3().setFromObject(object);
|
||
const center = box.getCenter(new THREE.Vector3());
|
||
const size = box.getSize(new THREE.Vector3());
|
||
const radius = Math.max(size.x, size.z) * 1.6 + 0.6;
|
||
const lookY = center.y + size.y * 0.35;
|
||
const cameraY = center.y + size.y * 0.35;
|
||
|
||
fs.mkdirSync(args.out, { recursive: true });
|
||
const pixels = new Uint8Array(width * height * 4);
|
||
const rowSize = width * 4;
|
||
const png = new PNG({ width, height });
|
||
|
||
for (let i = 0; i < frames; i++) {
|
||
const angle = (i / frames) * Math.PI * 2;
|
||
camera.position.set(Math.sin(angle) * radius, cameraY, Math.cos(angle) * radius);
|
||
camera.lookAt(0, lookY, 0);
|
||
renderer.render(scene, camera);
|
||
|
||
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
||
const buf = Buffer.from(pixels.buffer);
|
||
for (let y = 0; y < height; y++) {
|
||
buf.copy(png.data, y * rowSize, (height - 1 - y) * rowSize, (height - y) * rowSize);
|
||
}
|
||
const out = path.join(args.out, `frame_${String(i).padStart(3, '0')}.png`);
|
||
fs.writeFileSync(out, PNG.sync.write(png));
|
||
}
|
||
|
||
console.log(`rendered ${frames} frames -> ${args.out}`);
|
||
process.exit(0);
|
||
}
|