feat(models): 发布 v0.2.0 —— GLB/VRM 主力格式、自动缩略图与分析、模型审核与骨骼预览
需求 1:系统确定以 GLB / VRM 为模型主力格式
- 服务端新增支持库:
- model-inspector.ts:解析 GLB/VRM 的面数、顶点、材质、贴图、形变目标、
**骨骼树(名称/层级/是否蒙皮/humanoid 映射/子节点数)**、humanoid 覆盖率与可驱动性
- model-thumbnail.ts:**纯 TS 软件光栅化**(z-buffer + Lambert + 完整蒙皮矩阵)
生成 512×512 PNG 缩略图,零原生依赖、不装 headless-gl
- vrm-writer.ts:GLB → VRM 1.0(写入 VRMC_vrm 扩展与 humanoid 映射)
- gltf-packer.ts:glTF(.gltf + .bin + 贴图)→ 单文件 GLB
- humanoid 候选表与 Godot 客户端 humanoid_map.gd **逐字对齐**,并有单测断言两边一致
(服务端判定的"可驱动" = 客户端实际能否驱动)
需求 2:模型管理
- .glb / .vrm / .gltf **直接导入**(不重编码,GLB 含 VRM 扩展会自动识别为 VRM)
- .pmx / .fbx 服务端转换:assimp → .glb →(失败)→ .gltf 后自行打包成 GLB
→(MODEL_CONVERT_FORMAT=vrm 时)补 VRMC_vrm 产出 VRM 1.0;每步 assimp info 回读校验
- .zip **自动搜索包内可用模型**:.vrm → .glb → .gltf(直接导入)优先,
其次 .pmx → .fbx(转换);同优先级内按"指名字 → 目录最浅 → 文件最大"
- 入库后自动分析(models.stats)与生成缩略图(models.thumbnail_asset_id),
失败均不阻断入库;管理端列表显示小缩略图、预览放开到 vrm/glb/gltf
需求 3:审核功能
- 上传的模型状态**一律 pending**(不再自动发布、不再自动设默认)
- 审核界面新增模型面板:体积、面数、顶点、材质、贴图、骨骼、形变目标等指标,
humanoid 覆盖进度、可驱动性结论(缺哪些必需骨骼)、warnings,以及
**骨骼详细预览**(层级缩进、蒙皮/Humanoid 徽章、子节点数、搜索保留祖先链、
仅看蒙皮、限高滚动 + 只渲染前 400 行)
- 审核通过可带 setPrimary=true 一并设为默认模型(此时才写源码种子);
POST /api/models/:id/primary 仅允许 published(否则 409)
- scripts/import-default-model.mjs 改为"上传 → 审核通过 + 设默认"流程
其它:
- models 新增 stats 列(迁移 AddModelStats1789460000000,已在真实库执行)
- 契约升至 rest-api/data-models/model-formats 0.4.0、database 0.3.0(另两库已同步)
- 版本号 0.1.0 → 0.2.0(package.json ×2 + lockfile + project.yaml)
- 文档:README.txt / DSH.md / project.yaml / tasks 005·007·010 全面更新;
顶层开发书更新为 v1.4
验证:168 项单测通过(10 文件);契约 6 项与 29 条路由一致;typecheck/build 通过;
真实端到端:MMD 压缩包 → GLB + 分析(面数 9071/骨骼 238/humanoid 0-22) + 缩略图(15KB PNG);
GLB 直接导入 sha256 与源文件一致;审核通过后 defaultModelId 由 VRM 变为该模型
This commit is contained in:
@@ -81,12 +81,14 @@ export class AppConfig {
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩包转换的目标格式。
|
||||
* `fbx` 优先(按需求),但 Assimp 的 FBX 导出器对带蒙皮的大骨架模型会写坏文件,
|
||||
* 因此服务端在导出后回读校验,失败会自动回退 GLB 并记录 conversionNote。
|
||||
* 需要转换的模型(PMX / FBX)交付成什么格式。
|
||||
*
|
||||
* 系统以 **GLB / VRM 为主力**:GLB 是两个客户端都能原生加载的通用格式,
|
||||
* VRM 在其上多了 humanoid / 表情 / springBone 语义。默认 `glb`;
|
||||
* 设为 `vrm` 时会在 GLB 之上补 `VRMC_vrm` 扩展,humanoid 骨骼不全则回退 GLB。
|
||||
*/
|
||||
get modelConvertFormat(): 'fbx' | 'glb' {
|
||||
return (this.config.get<string>('modelConvertFormat') ?? 'fbx') === 'glb' ? 'glb' : 'fbx';
|
||||
get modelConvertFormat(): 'glb' | 'vrm' {
|
||||
return (this.config.get<string>('modelConvertFormat') ?? 'glb') === 'vrm' ? 'vrm' : 'glb';
|
||||
}
|
||||
|
||||
/** 压缩包里优先选用的 PMX 文件名(大小写不敏感);找不到时按"目录最浅、文件最大"挑 */
|
||||
@@ -191,7 +193,7 @@ export const configuration = () => {
|
||||
adminDir: process.env.ADMIN_DIR ?? resolve(process.cwd(), '..', 'admin'),
|
||||
uploadMaxBytes: Number(process.env.UPLOAD_MAX_BYTES ?? 512 * 1024 * 1024),
|
||||
assimpBin: process.env.ASSIMP_BIN ?? 'assimp',
|
||||
modelConvertFormat: process.env.MODEL_CONVERT_FORMAT ?? 'fbx',
|
||||
modelConvertFormat: process.env.MODEL_CONVERT_FORMAT ?? 'glb',
|
||||
modelConvertPmx: process.env.MODEL_CONVERT_PMX ?? 'Hatsune Miku.pmx',
|
||||
modelConvertTimeoutMs: Number(process.env.MODEL_CONVERT_TIMEOUT_MS ?? 180_000),
|
||||
defaultModelId: process.env.DEFAULT_MODEL_ID ?? '',
|
||||
|
||||
@@ -9,12 +9,12 @@ import { join } from 'node:path';
|
||||
* 未设置该变量时回落到这里的常量)。
|
||||
*/
|
||||
export const CONTRACT_VERSIONS: Readonly<Record<string, string>> = Object.freeze({
|
||||
'rest-api': '0.3.0',
|
||||
'rest-api': '0.4.0',
|
||||
'websocket-keypoints': '0.1.0',
|
||||
'data-models': '0.3.0',
|
||||
'data-models': '0.4.0',
|
||||
'skeleton-topology': '0.1.0',
|
||||
'model-formats': '0.3.0',
|
||||
database: '0.2.0',
|
||||
'model-formats': '0.4.0',
|
||||
database: '0.3.0',
|
||||
});
|
||||
|
||||
/** 解析 `contracts/VERSION`(`key: value` 每行一条,`#` 起注释) */
|
||||
|
||||
@@ -36,15 +36,15 @@ export interface DefaultModelSeed {
|
||||
updatedAt: string | null;
|
||||
}
|
||||
|
||||
/** 由 Hatsune Miku.pmx → GLB 转换得到,已设为默认模型 */
|
||||
/** 由 手工指定 转换得到,已设为默认模型 */
|
||||
export const DEFAULT_MODEL_SEED: DefaultModelSeed = {
|
||||
id: 20,
|
||||
name: "初音未来 Hatsune Miku",
|
||||
format: "glb",
|
||||
sourceSha256: "d1113cf074e19d3a5b4e7025973604c615d6e038ad222070e4748a805ac3c73f",
|
||||
fileSha256: "696c90c7e8514b759f764312d41f9b9069027e62f3aaa2d83ce2c3e475945028",
|
||||
convertedFrom: "Hatsune Miku.pmx → GLB",
|
||||
updatedAt: "2026-09-15T11:34:23.976Z",
|
||||
id: null,
|
||||
name: "",
|
||||
format: "",
|
||||
sourceSha256: "",
|
||||
fileSha256: "",
|
||||
convertedFrom: null,
|
||||
updatedAt: null,
|
||||
};
|
||||
|
||||
/** 该种子是否指向一个真实模型 */
|
||||
|
||||
Reference in New Issue
Block a user