// 程序化生成简化人体化身 GLB(脸型/体型/肤色三参数组合)。 // 用法: node gen-templates.js --out [--faces 6] [--bodies 6] [--skins 5] // 索引对齐 MatchTemplates:face 0 起、body/skin 1 起;已存在的 GLB 跳过(幂等)。 import { argv } from 'node:process'; import fs from 'node:fs'; import path from 'node:path'; import * as THREE from 'three'; import { GLTFExporter } from 'three/addons/exporters/GLTFExporter.js'; // Node 无 FileReader,GLTFExporter 二进制导出需要它(onloadend 回调风格) if (typeof globalThis.FileReader === 'undefined') { globalThis.FileReader = class { constructor() { this.result = null; } set onloadend(fn) { this._onloadend = fn; } get onloadend() { return this._onloadend; } readAsArrayBuffer(blob) { blob.arrayBuffer().then((buf) => { this.result = buf; this._onloadend && this._onloadend(); }); } }; } function parseArgs() { const a = { faces: 6, bodies: 6, skins: 5 }; 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; } } a.out = a.out || '.'; return a; } // 5 档肤色(PBR base color) const SKIN_TONES = ['#FDE4CF', '#F3C39B', '#D99B6C', '#A96B3F', '#6B4226']; // 脸型变体:发型形状/颜色差异 const FACE_HAIR = [ { style: 'short', color: '#2B2B2B' }, // 短发 { style: 'fringe', color: '#4A3728' }, // 刘海长发 { style: 'bob', color: '#8C5A2B' }, // 波波头 ]; // 体型变体:宽度/高度比例 const BODY_VARIANTS = [ { name: 'slim', width: 0.85, height: 1.0 }, { name: 'normal', width: 1.0, height: 1.0 }, { name: 'broad', width: 1.18, height: 0.96 }, ]; function buildAvatar(faceIdx, bodyIdx, skinIdx) { const face = FACE_HAIR[faceIdx % FACE_HAIR.length]; const body = BODY_VARIANTS[bodyIdx % BODY_VARIANTS.length]; const skin = SKIN_TONES[skinIdx % SKIN_TONES.length]; const root = new THREE.Group(); const skinMat = new THREE.MeshStandardMaterial({ color: skin, roughness: 0.7 }); const hairMat = new THREE.MeshStandardMaterial({ color: face.color, roughness: 0.85 }); const clothMat = new THREE.MeshStandardMaterial({ color: '#3D5A80', roughness: 0.8 }); const W = body.width; const H = body.height; // 躯干:胶囊体 const torso = new THREE.Mesh(new THREE.CapsuleGeometry(0.22 * W, 0.5, 8, 16), clothMat); torso.position.y = 1.12 * H; root.add(torso); // 头:球体 const head = new THREE.Mesh(new THREE.SphereGeometry(0.16 * W, 24, 18), skinMat); head.position.y = 1.68 * H; root.add(head); // 发型:覆盖头顶的半球壳(按脸型变体) const hair = new THREE.Mesh(new THREE.SphereGeometry(0.175 * W, 24, 12, 0, Math.PI * 2, 0, Math.PI * 0.52), hairMat); hair.position.y = 1.68 * H + 0.03; root.add(hair); if (face.style === 'fringe') { const fringe = new THREE.Mesh(new THREE.BoxGeometry(0.16 * W, 0.05, 0.2 * W), hairMat); fringe.position.y = 1.7 * H; fringe.position.z = -0.12 * W; fringe.rotation.x = -0.25; root.add(fringe); } else if (face.style === 'bob') { const back = new THREE.Mesh(new THREE.BoxGeometry(0.3 * W, 0.3, 0.06), hairMat); back.position.y = 1.52 * H; back.position.z = 0.14 * W; root.add(back); } // 上肢 for (const side of [-1, 1]) { const arm = new THREE.Mesh(new THREE.CapsuleGeometry(0.075 * W, 0.42, 6, 10), skinMat); arm.position.set(side * 0.32 * W, 1.32 * H, 0); arm.rotation.z = side * 0.06; root.add(arm); } // 下肢 for (const side of [-1, 1]) { const leg = new THREE.Mesh(new THREE.CapsuleGeometry(0.1 * W, 0.62, 6, 10), clothMat); leg.position.set(side * 0.12 * W, 0.55 * H, 0); root.add(leg); } return root; } const args = parseArgs(); fs.mkdirSync(args.out, { recursive: true }); const exporter = new GLTFExporter(); let generated = 0; for (let f = 0; f < args.faces; f++) { for (let b = 1; b <= args.bodies; b++) { for (let s = 1; s <= args.skins; s++) { const name = `avatar_f${f}_b${b}_s${s}.glb`; const outPath = path.join(args.out, name); if (fs.existsSync(outPath)) continue; const scene = buildAvatar(f, b, s); const buf = await new Promise((resolve, reject) => { exporter.parse( scene, (result) => resolve(Buffer.from(result)), (err) => reject(err), { binary: true } ); }); fs.writeFileSync(outPath, buf); generated++; } } } console.log(`generated ${generated} GLB (skipped existing) -> ${args.out}`);