Files
slogan/server/scripts/avatar-render/render.js
T
admin a6de9ebd12 Add 'server/' from commit 'e64421295fff83acbb6d6ab3d3b27f3ef8368f00'
git-subtree-dir: server
git-subtree-mainline: c4e617ada7
git-subtree-split: e64421295f
2026-08-04 15:02:35 +08:00

116 lines
3.7 KiB
JavaScript
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.
// 化身 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);
}