• v0.2.5 eebccd55b4

    zijunchiang released this 2026-09-16 18:55:25 +08:00 | 22 commits to main since this release

    OpenDance Studio -- Web 管理服务端 v0.2.5

    MMD 材质/着色对齐: 修复 PMX 转换后"颜色/贴图看起来不对"。契约不变
    (rest-api / data-models / model-formats 0.4.0、database 0.3.0)。

    诊断(本地真 Assimp 6.0.4 + 样本逐项核对)

    PMX 的着色模型(卡通阶梯 + 球面贴图 + 平坦环境光)与 glTF PBR 不是一回事。
    样本 【奥黛塔】_by_原神.zip(39 个材质)核对结果:

    项目 结论
    双面(不剔除背面) PMX 里 36 个材质有该标志; 导出的 GLB 里 doubleSided 一个都没有 -> 头发/裙摆/睫毛这类薄片被单面剔除, 这是"贴图/颜色不对"的主因
    specular / shininess PMX 为 (0,0,0) / 1.0; Assimp 写出 KHR_materials_specular(黑色)与 KHR_materials_volume 等与 MMD 无关的扩展
    环境光 / toon MMD 的平坦环境光与 toon 阶梯在 glTF core 里没有对应项
    球面贴图(SPH/SPA) 该模型 15 项; glTF core 无槽位
    accessor.min/max Assimp 写坏(实测 min = 2.2250738585072014e-308), Godot 用它算包围盒

    本次改动

    1. 新增 models/pmx-materials.ts: 只读 PMX 材质段(布局与权威库 mmd-parser 一致,
      注意贴图索引宽度是 1 字节), 拿到 diffuse / specular / shininess / ambient /
      drawFlag / 球面贴图 / toon / faceCount;
    2. 新增 models/glb-materials.ts: 转换后按索引把 PMX 参数补回 GLB ——
      doubleSided(按 drawFlag bit0)、metallicFactor=0、
      roughnessFactor=clamp(1-shininess/100, 0.5, 1)、baseColorFactor x diffuse(含 alpha);
      清掉 KHR_materials_specular / volume / transmission / ior / emissive_strength;
      修正全部浮点 accessor 的 min/max;
    3. 新增 MODEL_SHADING(默认 pbr, 可选 unlit): unlit 给每个材质写
      KHR_materials_unlit, 颜色 = 贴图 x baseColorFactor、与场景光照无关,
      最接近 MMD 的平涂观感(three.js 与 Godot 都支持);
    4. 能力边界(不假装保留): 球面贴图与 toon 阶梯无法用 glTF core 表达, 数量写进
      模型的 conversion_note, 审核面板可见;
    5. 新增 9 项测试; 测试规模 15 文件 / 220 项。

    关于"增加支持的库进行转换"

    换一个 PMX->glTF 转换器不能解决这件事: 球面贴图(matcap)与 toon 阶梯是
    视点相关/光照相关的着色, 无法烘进 baseColorTexture; glTF 2.0 core 也没有这两个
    槽位。行业做法只有三条: ①按 glTF 能力补齐(本次做的双面/PBR 规范化),
    ②交付 KHR_materials_unlit 的平涂颜色(本次的 MODEL_SHADING=unlit),
    ③只在支持 MToon 的查看器里用 VRM(VRMC_materials_mtoon 有 matcapTexture)——
    但 Godot 客户端不认 MToon, 所以服务端仍以 GLB 为准。

    升级后需要重新导入模型

    材质修正在转换时生效:

    • 重新上传该 PMX 压缩包(或在管理端删除后重传);
    • reanalyze-models.mjs 不会重转材质, 它只刷新分析与缩略图。

    想用平涂颜色: 在 deploy/.env 里加 MODEL_SHADING=unlit 后重跑 deploy/install.sh
    并重新导入模型。

    校验

    opendance-web-v0.2.5.tar.gz sha256: c1988dd4e9fceff48c8ae368e50adc0d6ba1275aa7f7abe643d1bbab7ddb3c4f

    Downloads
  • v0.2.4 ae8a68e98f

    zijunchiang released this 2026-09-16 04:02:35 +08:00 | 23 commits to main since this release

    OpenDance Studio -- Web 管理服务端 v0.2.4

    修复「导入 PMX 后模型显示为白模、脸部颜色丢失」。 契约与 v0.2.3 相同
    (rest-api / data-models / model-formats 0.4.0、database 0.3.0)。

    定位结论:GLB 数据没问题,问题在缩略图渲染器

    本地装真 Assimp 6.0.4 完整复现该模型(【奥黛塔】_by_原神.zip)后逐层核对:

    检查项 结论
    转换 正常: 23.7 MB GLB、643 骨骼、93,350 面、1.2 s
    贴图内嵌 18 张贴图与压缩包原图逐字节一致
    材质 -> 贴图对应 用权威 PMX 解析器比对, 39 个材质完全一致(不是映射错)
    UV 采样 正确: 脸部材质取到的就是皮肤色 (246,221,216)
    缩略图渲染器 它刻意不解码 baseColorTexture, 只用 baseColorFactor 上色; 而 MMD 模型的 baseColorFactor 基本都是白色 -> 缩略图必然全白

    管理端 3D 预览(three.js)本来就有贴图, 白模只出现在列表页与审核面板的缩略图。

    本次改动

    1. model-thumbnail.ts 内置极简 PNG 解码器(zlib inflate + 逐行反滤波, 8 位非隔行,
      colorType 0/2/3/4/6), 按 baseColorFactor x 贴图 逐像素采样(透视校正 UV);
      解码失败 / 非 PNG / 隔行扫描时回退纯色, 绝不抛错;
    2. 按 alphaMode 处理透明度: OPAQUE 忽略贴图 alpha(MMD 语义)、MASK 按
      alphaCutoff 丢弃、BLEND 做 source-over;
    3. 取景收紧(FRAMING 1.6 -> 1.3), 40px 列表缩略图主体更清楚;
    4. 新增 6 项测试(含"白模"回归: baseColorFactor 全白 + 红贴图必须输出偏红);
      测试规模 14 文件 / 211 项。

    关于"要不要默认转 VRM"

    不必, 也解决不了这个问题:VRM 就是 GLB + humanoid 元数据, 材质管线与 GLB
    完全相同(同样只有 baseColorTexture, 没有 MMD 的球面/卡通贴图槽位);
    而该模型 humanoid 映射为 0/22, 转 VRM 也会按设计回退交付 GLB。
    真正让贴图显示出来的是本次对渲染端的修复。这个模型本来也不用球面贴图
    (diffuse 全白、无 sphere), 所以核心 GLB 没有"丢贴图"。

    升级后请重新生成缩略图

    已有模型的缩略图是旧渲染器生成的, 升级后执行一次(v0.2.3 起服务端已带该脚本):

    cd /opt/opendance/web-server
    node scripts/reanalyze-models.mjs --url http://127.0.0.1:3000 --force
    

    只重算单个: --model-id 22; 先看清单: --dry-run。

    校验

    opendance-web-v0.2.4.tar.gz sha256: 138f31220afe5455cda187ca64c6b45624b35e4865eecf6af4d86e0f3284399c

    Downloads
  • v0.2.3 8e8cc0475b

    zijunchiang released this 2026-09-16 03:30:12 +08:00 | 24 commits to main since this release

    OpenDance Studio -- Web 管理服务端 v0.2.3

    修复导入 PMX 模型返回 500,以及 GBK 压缩包丢贴图。 契约与 v0.2.2 相同
    (rest-api / data-models / model-formats 0.4.0、database 0.3.0)。

    根因一:500 Internal server error

    样本 【奥黛塔】_by_原神.zip(643 根骨骼、93,350 面)转换本身成功
    (GLB 23.7MB、1.2s),但分析结果里的骨骼树按骨骼逐条展开,JSON 实测 70,980 字节,
    而 models.stats 是 TEXT(上限 65,535)。MySQL / MariaDB 严格模式直接拒绝写入
    (ER_DATA_TOO_LONG / 1406), 而这一步没有兜底 -> 未捕获 -> 500。

    根因二:18 张贴图只内嵌了 17 张

    tex/髮.png 的 GBK 字节是 F3 8C。文件名解码先试 Shift-JIS, 而 WHATWG 的
    shift_jis 把未定义码位映射成私用区不可见字符(不是 U+FFFD), 于是被判为
    「解码成功」, 永不回退 GBK; 贴图名对不上就丢图。

    本次改动

    1. models.stats -> MEDIUMTEXT(实体 mediumtext + transformer 保留 JSON 语义;
      不用 simple-json 以免实体元数据与库列型不一致), 新增迁移
      AddModelStatsMediumText1789550000000(install.sh 会自动执行);
    2. 新增 shrinkStatsForStorage() 兜底: 即使库没迁移, 也只截断骨骼树
      (父在前, 保留祖先链), 面数 / 骨骼数 / humanoid 覆盖 / 可驱动性 / warnings
      全部保留; 服务层只在 1406 / DATA_TOO_LONG 时裁剪重试一次, 其它 DB 错误照旧抛出;
    3. 压缩包文件名解码把「私用区字符」也算解错信号并回退 GBK —— 实测该模型贴图
      由 17/18 变为 18/18 全部内嵌;
    4. 新增 12 项测试; 测试规模 13 文件 / 205 项。

    已经在跑旧版本的服务器

    不改代码也能立刻解除 500(把列扩到 16MB 即可, 旧代码写入的数据不变):

    ALTER TABLE models MODIFY stats MEDIUMTEXT NULL;
    

    执行的数据库与账号就是 deploy/.env 里的 DB_NAME / DB_USER:

    mysql -h 127.0.0.1 -u "$DB_USER" -p "$DB_NAME" \
      -e "ALTER TABLE models MODIFY stats MEDIUMTEXT NULL;"
    

    改完直接重新导入该压缩包即可(无需重启服务)。
    想同时拿到「18/18 贴图」与后续兜底, 再部署 v0.2.3:

    cd /root
    curl -LO https://gitea.kasugano.cn/zijunchiang/opendance-web/releases/download/v0.2.3/opendance-web-v0.2.3.tar.gz
    curl -LO https://gitea.kasugano.cn/zijunchiang/opendance-web/releases/download/v0.2.3/SHA256SUMS
    sha256sum -c SHA256SUMS
    tar -xzf opendance-web-v0.2.3.tar.gz && cd opendance-web-v0.2.3
    bash deploy/install.sh          # 自动执行 AddModelStatsMediumText 迁移并重启服务
    

    迁移完成后 GET /health 的 database.migrationsPending 应为 0。

    校验

    opendance-web-v0.2.3.tar.gz sha256: d8aefad75baa8080e47c72a95b7990ca15af04d9e58c8492dea5c0ae5c0d3a2d

    Downloads
  • v0.2.2 afed67e175

    zijunchiang released this 2026-09-16 02:56:50 +08:00 | 25 commits to main since this release

    OpenDance Studio -- Web 管理服务端 v0.2.2

    修复「上传 PMX 压缩包报 Assimp 缺失」的报错与部署体验。 功能与契约与 v0.2.1 相同
    (rest-api / data-models / model-formats 0.4.0、database 0.3.0)。

    修复的问题

    服务器没装 Assimp 时, 上传 .pmx / .fbx(或含它们的压缩包)只得到两条:

    GLB 直出:导出失败(ENOENT):spawn assimp ENOENT
    glTF 直出:导出失败(ENOENT):spawn assimp ENOENT
    

    用户看不出「缺什么、怎么装」, 也不知道只上传 .glb / .vrm 其实不需要它。

    本次改动

    1. 新增 src/common/assimp.ts: 按 ASSIMP_BIN -> PATH -> /usr/bin、/usr/local/bin
      等常见目录定位可执行文件(只认文件, 不认同名目录), 转换前先预检; 找不到就一次性
      给出各发行版安装命令、查过的路径与替代办法, 不再打两条 ENOENT;
      动作预处理(FBX / glTF -> BVH)复用同一文案。
    2. 新增 deploy/assimp-lib.sh: install.sh 按发行版自动安装
      (apt-get / dnf / yum / zypper / apk / pacman); 装在非常规目录时把绝对路径
      写入 deploy/.env 的 ASSIMP_BIN; 装不上只告警不中止, 可用 SKIP_ASSIMP_INSTALL=1 跳过。
      Debian / Ubuntu 只装 assimp-utils(不写死 libassimp5/6, 由发行版决定)。
      check-access.sh 自检增加 Assimp 一行。
    3. /health 增加 assimp.path; 管理端「概览 -> 服务健康」显示 Assimp 状态, 不可用时提示。
    4. 新增 13 项测试 test/assimp-locate.spec.ts; 测试规模 12 文件 / 193 项。

    已经在跑 v0.2.0 / v0.2.1 的服务器

    不用重新部署, 装一次 Assimp 即可, 服务无需重启:

    # Debian / Ubuntu(让 apt 自己选 libassimp5 或 6)
    apt-get update && apt-get install -y assimp-utils
    # RHEL / CentOS / Rocky / Fedora
    # dnf install -y assimp
    
    assimp version                      # 能打印版本即成功
    curl -fsS http://127.0.0.1:3000/health   # 其中 assimp.available 应为 true
    

    装好后重新上传该压缩包即可。也可运行 v0.2.2 的 bash deploy/install.sh,
    它会自动检查/安装 Assimp 并把结论写进安装日志。

    校验

    opendance-web-v0.2.2.tar.gz sha256: 0a42313fb31624647cd2fe4dce77c69ef8941c60066522d05a6dbd7ed66a61af

    Downloads
  • v0.2.1 e9de740d66

    zijunchiang released this 2026-09-15 22:27:16 +08:00 | 27 commits to main since this release

    OpenDance Studio -- Web 管理服务端 v0.2.1

    v0.2.0 的补丁版: 修复首次部署必然失败的问题。功能与契约与 v0.2.0 相同
    (rest-api / data-models / model-formats 0.4.0、database 0.3.0)。

    修复的问题

    现象(在干净服务器上按 README 部署 v0.2.0 时):

    ./deploy/.env: line 53: Miku.pmx: command not found
    [install] 第 53 行执行失败(退出码 127)
    

    根因: deploy/.env.example 里 MODEL_CONVERT_PMX=Hatsune Miku.pmx 的值含空格却没加引号。
    install.sh 用 set -a; . deploy/.env 载入配置时会真的执行这个文件, 这一行被切成
    「赋值 + 命令」, Miku.pmx 被当成命令执行。

    本次改动

    1. 模板改为 MODEL_CONVERT_PMX="Hatsune Miku.pmx"(占位符值同样加引号);
    2. 新增 deploy/env-lib.sh 语法预检: install.sh 与 ctl.sh 在 source 之前检查,
      报错直接指出哪一行、原文、改法; $( ) 与反引号会建议改用单引号
      (双引号里的它们仍会被执行);
    3. install.sh 的失败提示补上出错文件名(.env 被 source 时行号属于 .env,
      只报行号无法定位);
    4. 新增 12 项回归测试 web-server/test/env-file.spec.ts, 锁住「模板可被 shell 原样载入」;
    5. 文档补充 .env 写法约定与 FAQ; 测试规模 11 文件 / 180 项。

    已经部署到一半(v0.2.0)怎么继续

    不必重新下载, 在解压目录里给这一行加上引号再重跑安装:

    sed -i 's|^MODEL_CONVERT_PMX=Hatsune Miku.pmx$|MODEL_CONVERT_PMX="Hatsune Miku.pmx"|' deploy/.env
    bash deploy/install.sh
    

    校验

    curl -LO https://gitea.kasugano.cn/zijunchiang/opendance-web/releases/download/v0.2.1/SHA256SUMS
    sha256sum -c SHA256SUMS   # 期望 OK
    

    opendance-web-v0.2.1.tar.gz sha256: f574c82f4c9f7ac705ebd1099510bd863010b7f2d5d43fac43800b705ee9eaf0

    Downloads
  • v0.2.0 b7748a50eb

    zijunchiang released this 2026-09-15 21:53:29 +08:00 | 42 commits to main since this release

    OpenDance Studio — Web 管理服务端 v0.2.0

    三端开发顺序中的阶段一交付物:NestJS + TypeORM + 现有 MySQL 实例。

    v0.2.0 主要变更

    1. GLB / VRM 确定为模型主力格式,服务端与客户端分别落实支持库;
    2. 模型管理:.glb/.vrm 直接导入;.pmx/.fbx 服务端优先转 GLB、其次 VRM;
      压缩包自动搜索包内可用模型;入库自动生成缩略图与模型分析;
    3. 模型审核:上传一律 pending,审核界面含骨骼详细预览(评分基础);
    4. 新增历史模型回填(POST /api/models/:id/reanalyze + scripts/reanalyze-models.mjs);
    5. 契约升至 rest-api/data-models/model-formats 0.4.0、database 0.3.0。

    1. 模型格式策略

    上传 服务端行为
    .glb / .vrm / .gltf 直接导入,不重编码(GLB 含 VRM 扩展会自动识别为 VRM)
    .pmx / .fbx 转换:assimp → .glb →(失败)→ .gltf 后自行打包成 GLB →(MODEL_CONVERT_FORMAT=vrm 时)补 VRMC_vrm 扩展产出 VRM 1.0
    .zip 自动搜索包内可用模型:.vrm → .glb → .gltf(直接导入)优先,其次 .pmx → .fbx(转换)。同优先级内:指名字 → 目录最浅 → 文件最大

    每一步都用 assimp info 回读校验(Assimp 会写出损坏文件),贴图内嵌进 GLB;
    压缩包原件作为 model-archive 资产留存溯源。

    客户端支持库:Windows 客户端用 Godot 内置 GLTFDocument 加载 glTF/GLB/VRM;
    服务端的 humanoid 候选表与客户端 humanoid_map.gd 逐字对齐,并有单测断言两边一致。

    2. 自动缩略图与模型分析

    • 缩略图(model-thumbnail.ts):纯 TypeScript 软件光栅化(z-buffer + Lambert +
      完整蒙皮矩阵),零原生依赖。512×512 PNG 写入 models.thumbnail_asset_id。
    • 模型分析(model-inspector.ts):面数 / 顶点 / 网格 / 材质 / 贴图 / 形变目标 /
      动画 / 节点 / 骨骼与骨骼树 / humanoid 覆盖 / 可驱动性,写入 models.stats。
    • 两者失败都不阻断入库。

    3. 模型审核(本次重点)

    • 上传的模型状态一律 pending:不再自动发布、不再自动设默认。
    • 审核界面新增模型面板:体积、格式、容器、VRM 版本、面数/顶点/网格/材质/贴图/
      骨骼/节点/形变目标/动画、humanoid 覆盖进度、可驱动性结论(缺哪些必需骨骼)、
      warnings,以及骨骼详细预览(层级缩进、蒙皮/humanoid 徽章、子节点数、
      搜索保留祖先链、仅看蒙皮、限高滚动 + 只渲染前 400 行)。
    • 通过审核时可勾选**「同时设为全局默认模型」**(setPrimary=true);单独设置用
      POST /api/models/:id/primary(仅 published,否则 409)。

    为什么骨骼是审核重点:客户端按骨骼名映射 humanoid 规范(22 根,16 根为驱动
    33 点所必需)。实测日文 MMD 骨名的 PMX 转 GLB 后覆盖 0/22 → 无法驱动;
    某 VRM 只有 7/22;Bip001* 命名的 FBX 转 GLB 后 17/22。审核时就能拦下来。

    4. 历史模型回填(升级必做一次)

    升级后旧模型还没有分析与预览图:

    cd /opt/opendance/web-server
    node scripts/reanalyze-models.mjs --url http://127.0.0.1:3000            # 回填全部缺 stats 的
    node scripts/reanalyze-models.mjs --url http://127.0.0.1:3000 --dry-run  # 只列清单
    node scripts/reanalyze-models.mjs --url http://127.0.0.1:3000 --force    # 全部重算
    

    实测(真实 21 MB VRM / 290 骨骼):回填出面数 53552、顶点 44662、骨骼 290、
    humanoid 7/22,并重建 512×512 缩略图。
    文件在存储中丢失时接口返回 409 并说明原因(不会用全 0 结果覆盖已有数据);
    StorageService.persistBuffer 在 sha256 去重命中但文件不存在时会自愈补写
    (避免换过 STORAGE_DIR、或只恢复数据库后出现"静默全 0 分析")。

    5. 实测数据

    上传 MMD 压缩包(含 Hatsune Miku.pmx)
      → HTTP 201 / 0.86s / format=glb / status=pending
      → conversionNote = Hatsune Miku.pmx → GLB,已内嵌 2 张贴图
      → 分析:vertices=7155 triangles=9071 bones=238 humanoid=0/22 drivable=false
      → 缩略图:512×512 PNG / 15,043 字节(目视确认为带蒙皮的 T-pose 角色图)
    
    直接上传 GLB
      → HTTP 201 / 0.36s / conversionNote = 直接导入(GLB,未做重编码)
      → 入库文件 sha256 与源文件完全一致(证明没有重编码)
    
    审核
      → POST /api/reviews/model/22/approve { setPrimary: true } → 200
      → status=published、isPrimary=true;源码种子写入 sourceSha256=72eb6882…
      → 重建后 GET /api/catalog/export 的 defaultModelId 由 VRM 变为 22
    
    缩略图性能(真实 10MB VRM / 83 关节 / 5307 顶点):60–121ms(中位 79ms)
    
    契约校验 ✓ rest-api/data-models/model-formats 0.4.0、database 0.3.0;路由 30 条一致
    类型检查 ✓ tsc --noEmit 通过(strict)
    单元测试 ✓ 168 / 168 通过(10 个文件)
    生产构建 ✓ nest build + vite build 通过
    

    6. 部署更新

    cd /root
    curl -LO https://gitea.kasugano.cn/zijunchiang/opendance-web/releases/download/v0.2.0/opendance-web-v0.2.0.tar.gz
    curl -LO https://gitea.kasugano.cn/zijunchiang/opendance-web/releases/download/v0.2.0/SHA256SUMS
    sha256sum -c SHA256SUMS
    cp /opt/opendance/deploy/.env /root/opendance-env.bak.$(date +%s)      # 先备份现网配置
    tar -xzf opendance-web-v0.2.0.tar.gz && cd opendance-web-v0.2.0
    cp /opt/opendance/deploy/.env deploy/.env
    grep -q '^ADMIN_PORT=' deploy/.env || echo 'ADMIN_PORT=3001' >> deploy/.env
    grep -q '^HOST=' deploy/.env || echo 'HOST=0.0.0.0' >> deploy/.env
    grep -q '^MODEL_CONVERT_FORMAT=' deploy/.env || echo 'MODEL_CONVERT_FORMAT=glb' >> deploy/.env
    bash deploy/install.sh        # 会执行新迁移 AddModelStats
    # 回填历史模型(一次即可)
    cd /opt/opendance/web-server && node scripts/reanalyze-models.mjs --url http://127.0.0.1:3000
    

    前置:PMX/FBX 转换需要 Assimp(apt-get install -y assimp-utils libassimp6);
    .glb/.vrm 直接导入不需要 Assimp。

    已知事项

    • 缩略图是纯色块着色的 T-pose 轮廓图,用于列表页快速辨识;精细预览由管理端
      three.js 实时渲染(已放开 vrm/glb/gltf,FBX 仍不支持)。
    • PMX → GLB 会丢失物理、IK、toon/sphere 贴图、面部表情等 MMD 专有特性,
      属格式能力差异(见 contracts/model-formats.md §1.1)。
    • 模型授权信息需部署方核实后再通过审核。
    Downloads
  • v0.1.0 dbd9155b40

    zijunchiang released this 2026-09-14 19:02:20 +08:00 | 63 commits to main since this release

    OpenDance Studio — Web 管理服务端 v0.1.0

    三端开发顺序中的阶段一交付物:NestJS + TypeORM + 现有 MySQL 实例。

    本次修订(三条需求)

    1. 模型上传支持压缩包自动转换:上传 MMD 模型包 .zip → 服务端解压选 .pmx
      → Assimp 转 FBX(导出后回读校验,失败自动回退 GLB 并把贴图内嵌)
      → 入库;转换成功后设为默认模型并把身份写进源码;
    2. 首次启动的 .env 引导流程:deploy/init-env.sh 交互式逐项填写、校验、
      可做连通性自检,非交互场景绝不挂住;
    3. 默认监听 0.0.0.0(代码、两份 .env 模板、安装脚本与引导脚本四处对齐并有测试守护)。
      之前的双端口分离、VMD 支持、不使用 Docker / 不部署反代等变更继续有效。

    1. 模型压缩包自动转换(.zip)

    POST /api/models 的 file 现在也接受 .zip(MMD 模型包):

    .zip ─▶ ZIP 读取器 ─▶ 选主模型(.pmx) ─▶ Assimp 导出 FBX ─▶ 回读校验 ─┬─通过─▶ 入库 FBX
            (STORE/DEFLATE,   (指名字/最浅/最大)                      └─失败─▶ 导出 GLB
             zip-slip 防护)                                                    (贴图内嵌) ─▶ 入库 GLB
    
    • 回读校验是必须的:Assimp 的 FBX 导出器对带蒙皮的大骨架模型会写出损坏文件。
      实测本包(714 骨骼的 PMX):导出退出码 11、产物回读报
      FBX-DOM no Objects dictionary found。只看退出码会把坏文件入库。
    • 回退结果:Hatsune Miku.pmx → GLB,1,152,464 字节,2 张贴图已内嵌,238 关节,
      1 网格 / 240 节点 / 3 材质。转换耗时 < 1s(含解压与回读校验)。
    • 贴图内嵌:Assimp 在 GLB 里把贴图写成外部 URI(还带 \ 反斜杠),
      服务端把压缩包内的贴图搬进 GLB 的 BIN chunk,保证单文件可加载(否则是白模)。
    • 溯源:压缩包原件存为 model-archive 资产并记入 models.source_asset_id;
      转换说明写入 models.conversion_note。
    • 安全:解压前逐条校验路径(拒绝绝对路径与 ..)、拒绝加密条目、
      只支持 STORE/DEFLATE,不支持的方式明确报错。
    • 失败语义:包内无 .pmx、Assimp 不可用、两种格式都失败 → HTTP 422,不产生记录。
    • 顺带修掉了 multipart 中文文件名乱码(模å_… → 模型_…)。

    默认模型写进源码

    转换成功且 autoDefault 不为 false(默认)时:

    1. 把该模型置为 is_primary=1 + status=published;
    2. 把身份写进 web-server/src/config/default-model.ts(名称 / 格式 / 产物 sha256 / 转换来源)。

    GET /api/catalog/export 的 defaultModelId 解析顺序:

    0. DEFAULT_MODEL_ID 环境变量(存在且已发布)
    1. 源码种子 DEFAULT_MODEL_SEED(按产物 sha256 匹配,换库/重建库仍然有效)
    2. 已发布且 is_primary 的 VRM
    3. 任意已发布 VRM
    4. id 最小的已发布模型
    

    实测:库中存在 2 个 VRM 模型时,defaultModelId 解析为 16(初音未来 Hatsune Miku / glb)。

    运维可用 CLI 完成同样的事(在 systemd 沙箱外,因此能写源码):

    cd /opt/opendance/web-server
    node scripts/import-default-model.mjs 模型.zip --url http://127.0.0.1:3000 \
      --name "初音未来 Hatsune Miku" --author "..." --license "..." \
      --source-name "..." --source-url "https://..."
    # 或只补写源码:node scripts/import-default-model.mjs --write-source-only --model-id 16
    

    2. 首次启动的 .env 引导

    bash deploy/install.sh          # 缺 deploy/.env 时自动进入引导
    bash deploy/init-env.sh         # 也可以单独跑
    bash deploy/init-env.sh --check # 只校验不改动
    bash deploy/init-env.sh --force # 已有 .env 也重新问一遍
    

    逐项询问 DB_HOST / DB_PORT / DB_USER / DB_PASS / DB_NAME / PORT / ADMIN_PORT / HOST,
    回车保留默认;口令不回显、写盘不加引号;校验必填与端口范围(1..65535);
    可用 Node + mysql2 做数据库连通性自检(口令只经环境变量传给子进程,失败仅告警);
    只替换这 8 个键、保留模板注释,写完 chmod 600 并回读校验;结尾打印摘要(口令 ***)。

    非交互场景绝不挂住:-y/--yes/--non-interactive、
    OPENDANCE_NONINTERACTIVE=1、stdin 为 /dev/null 或关闭时只校验不提问;
    所有读取都带超时,EOF/超时立即退出。

    3. 默认监听 0.0.0.0

    代码默认值、deploy/.env.example、web-server/.env.example、install.sh 与
    init-env.sh 五处一致为 0.0.0.0,并有单元测试守护(含"两份模板都写着
    HOST=0.0.0.0"的断言)。反代与后端同机时可改为 127.0.0.1,
    管理端还可用 ADMIN_HOST 单独限制来源。

    交付与验收

    契约校验    ✓ rest-api 0.3.0 / data-models 0.3.0 / model-formats 0.3.0 / database 0.2.0;路由 29 条一致
    类型检查    ✓ tsc --noEmit 通过(strict)
    单元测试    ✓ 125 / 125 通过(7 个文件)
    生产构建    ✓ nest build + vite build 通过
    压缩包转换  ✓ 真实 MMD 包 → HTTP 201 / 0.89s / format=glb / isPrimary=true / status=published
    默认模型    ✓ 源码种子生效,defaultModelId 从 VRM 回落到 #16
    GLB 可用性  ✓ 下载 1,152,464 字节;1 mesh / 240 nodes / 1 skin(238 joints) / 2 张内嵌贴图
    .env 引导   ✓ 交互与非交互两条路径均实测;管道交互可写值且保留注释
    打包完整性  ✓ 产物含 ZIP 读取器、转换模块、迁移、引导脚本与 CLI
    

    部署更新步骤

    curl -LO https://gitea.kasugano.cn/zijunchiang/opendance-web/releases/download/v0.1.0/opendance-web-v0.1.0.tar.gz
    tar -xzf opendance-web-v0.1.0.tar.gz && cd opendance-web-v0.1.0
    cp /opt/opendance/deploy/.env deploy/.env
    grep -q '^ADMIN_PORT=' deploy/.env || echo 'ADMIN_PORT=3001' >> deploy/.env
    grep -q '^HOST=' deploy/.env || echo 'HOST=0.0.0.0' >> deploy/.env
    bash deploy/install.sh          # 会执行新的迁移 AddModelConversionFields
    

    前置:转换功能需要服务器能调用 Assimp(ASSIMP_BIN,默认 assimp)。
    未安装时压缩包上传会返回 422 并提示安装;apt install assimp-utils libassimp6 即可。

    附:模型格式在本系统中的实际能力(实测)

    原生可加载的只有 glTF 系:Windows 客户端(Godot GLTFDocument)与管理端预览
    (three.js GLTFLoader + VRMLoaderPlugin)都只认 GLB / glTF / VRM;
    FBX 与 PMX 两个客户端都不能直接加载(管理端对 fbx 明确给"无法解析"提示)。

    转换方向 结果(Assimp 6.0.4 实测)
    PMX → GLB ✅ 可用(本项目通路,贴图会内嵌)
    PMX → glTF ✅ 可用
    PMX → FBX ❌ 失败(退出码 11,产物回读报 FBX-DOM no Objects dictionary found)
    VRM → GLB ✅ 可用(17 MB→17 MB,822 ms)
    VRM → FBX ✅ 可用(33 MB,1030 ms)
    FBX → GLB ✅ 可用(21 MB,1847 ms)
    GLB → FBX ⚠️ 视模型而定;本项目来源(PMX)的 GLB 必失败

    转换不是无损的(实测):

    • PMX → GLB 丢失:物理刚体/关节、IK 链、付与/D 骨、卡通(toon)与球面(sphere)贴图、
      轮廓线参数、顶点色、多套 UV;骨骼 714 → 蒙皮用 238;面部表情(morph) 0 个
      (源 PMX 里 まばたき/ウィンク/にこり 等表情确实存在);材质 2 → 3。
    • VRM → GLB 丢失:extensionsUsed 里的 VRM 消失 → humanoid 骨骼语义、
      expression 绑定、spring bone、meta(许可/作者)、first-person 设置全部丢失;
      92 个 blend shape 作为 glTF morph target 保留,但 VRM 语义名丢失。
      → 系统不再把它识别为 VRM(vrmVersion 变 null,VRM 优先规则不再适用)。
    • GLB → VRM 做不到:需要 VRM 专用工具写入 humanoid/expression/springBone/meta,
      Assimp 不会写 VRM 扩展。本系统没有这个方向的实现。
    • FBX → GLB:几何/蒙皮/形态目标保留;若 FBX 引用外部贴图,单文件上传时贴图缺失
      (实测 #17 转换后 images=0);材质被近似拆分(1 → 7)。
    • 任何目标格式都无法承载 PMX 的物理与卡通渲染参数。

    更关键的是骨骼命名:模型能被驱动的前提是骨骼名能映射到 humanoid 规范
    (客户端 humanoid_map.gd 的候选名以英文/VRoid/3ds Max Biped 为主)。实测覆盖率:

    模型 humanoid 覆盖 能否驱动(16 根必需骨)
    PMX→GLB(Hatsune Miku,日文 MMD 骨名) 0 / 22 ❌ 不能
    VRM #18(中文骨名) 7 / 22 ❌ 不能
    FBX #17 → GLB(Bip001 命名) 17 / 22 ⚠️ 只缺左右小腿

    结论:格式对了不等于能用,还要有 humanoid 骨骼命名(或补一张映射表)。

    已知设计取舍

    • 默认模型种子以上传压缩包的 sha256 为匹配键:实测 Assimp 的 GLB 导出不是字节级
      确定的(同一份 zip 连续转换三次得到三个不同 sha256,大小一致、BIN chunk 有十几字节浮动),
      用产物 sha256 当键会失效。产物 sha256 仍写入 fileSha256 字段但仅作记录。

    已知事项

    • 模型压缩包里的授权信息:本仓库内置的默认模型(初音未来 Hatsune Miku,Boku Wa Luka)
      压缩包内没有许可文件,且疑似游戏提取资源。已按"未标注、对外使用前请确认授权"
      记入 license 字段;对外发布前请核实并更新来源五元组。
    • 未内置管理端登录认证,请在反代层加 Basic Auth / IP 白名单,或只在内网暴露管理端端口。
    • Assimp 的 FBX 导出器对复杂蒙皮模型不可用(本项目已自动回退 GLB);
      若必须得到 FBX,需要 Blender + mmd_tools 之类的专用转换器,当前未纳入。
    Downloads