AIRI 舞台 MMD 渲染器实战指南:@proj-airi/stage-ui-mmd 的模型加载、物理动画与口型同步全解析
【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi
本指南以 packages/stage-ui-mmd/README.md 为核心骨架,系统讲解 Project AIRI 舞台上 MMD(MikuMikuDance)渲染器的能力边界、接入方式与底层实现。你将掌握.pmx/.pmd模型与.vmd动作的完整接入流程、物理/IK/append-bone 三套求解器的运行时开关、跨窗口同步的配置存储模型,以及表情、眨眼、视线追踪与 wLipSync 口型同步是如何叠加在 VMD 动画之上的。文中所有配置默认值与调用细节均取自仓库源码,可直接对照验证。
一、包定位:AIRI 舞台上为什么需要第四个渲染器
Project AIRI 的舞台(stage)支持多种模型格式,每个渲染器都是一个独立的包,通过统一场景契约挂载到 packages/stage-ui 的Stage.vue中。@proj-airi/stage-ui-mmd负责其中的 MMD(MikuMikuDance)家族:
- Live2D→ packages/stage-ui-live2d
- VRM→ packages/stage-ui-three
- Spine→ packages/stage-ui-spine
- MMD→
@proj-airi/stage-ui-mmd(本文主题)
从 README 的定位看,该包的目标是与 Live2D、VRM 渲染器达到功能对等(feature parity):不仅"能显示模型",还要具备完整的二次元虚拟形象驱动能力——骨骼物理、IK、动作导入、口型同步、表情、视线追踪。它构建在开源库@moeru/three-mmd之上,复用其 PMX/PMD 加载、VMD 动画构建、IK、append-bone("grant")传播、toon 材质与描边能力;物理部分来自@moeru/three-mmd-physics-ammo,且以懒加载方式引入——Ammo WASM 运行时只在真正挂载了 MMD 模型后才初始化(见 packages/stage-ui-mmd/src/utils/ammo.ts)。
何时使用 / 何时不使用
README 给出了清晰的选型建议:
| 场景 | 选择 |
|---|---|
展示.pmx/.pmd的 MMD 角色 | @proj-airi/stage-ui-mmd |
| 需要物理驱动的次级运动(头发/裙摆)、IK、VMD 播放 | @proj-airi/stage-ui-mmd |
| VRM 虚拟形象 | @proj-airi/stage-ui-three |
| Live2D 模型 | @proj-airi/stage-ui-live2d |
| Spine 动画 | @proj-airi/stage-ui-spine |
| 无 WebGL / WASM 支持的运行环境 | 全部不适用(MMD 物理依赖 Ammo WASM) |
包结构与入口
包的导出面(packages/stage-ui-mmd/src/index.ts)统一暴露了:场景组件MMDScene、composables(动画/表情/视线/眨眼/口型/加载)、常量(动作/情绪/形态槽)、Pinia store、以及ammo、mmd-loader、mmd-preview、mmd-validator、mmd-zip-loader、opfs-loader等工具模块。
二、快速上手:场景组件与"指令式"渲染契约
最小接入示例
README 给出了最核心的用法——从包中导入MMDScene与useMMD,前者是场景组件,后者是运行时配置的 Pinia store:
import { MMDScene, useMMD } from '@proj-airi/stage-ui-mmd'<MMDScene v-model:state="state" :audio-context="audioContext" :model-src="modelUrl" :cursor-position="cursorPosition" :current-audio-source="audioSource" :enable-orbit-controls="true" @error="onError" />Props 全量说明
对照场景组件源码 packages/stage-ui-mmd/src/components/scenes/MMD.vue 的defineProps定义:
| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
audioContext | AudioContext | 未定义 | 拥有currentAudioSource的音频上下文,供口型同步节点使用 |
modelSrc | string | 未定义 | 模型地址:打包 ZIP 或裸.pmx/.pmdURL |
modelId | string | 未定义 | 稳定缓存键(用于 OPFS 缓存与 ZIP 资源解析) |
paused | boolean | false | 暂停渲染循环 |
cursorPosition | { x, y } | 未定义 | 光标位置,用于mouse视线追踪模式 |
currentAudioSource | AudioBufferSourceNode | 未定义 | 当前音频源,接入 wLipSync 节点 |
enableOrbitControls | boolean | false | 是否启用轨道控制器(拖拽旋转视角) |
组件状态通过v-model:state双向绑定,取值为'pending' | 'loading' | 'mounted',对应模型的加载生命周期。
暴露的方法
组件通过defineExpose(MMD.vue)对外暴露:
canvasElement():获取内部 canvas 元素;captureFrame():渲染一帧并返回Promise<Blob | null>(PNG 截图);setEmotion(name, intensity):设置情绪表情(morph 交叉淡入淡出 + 可选的映射手势动作);listMorphs():当前模型暴露的全部 morph 名称;listMotions():当前已注册的全部动作名称。
指令式 vs 声明式:MMD 渲染器的架构选择
一个值得注意的架构细节写在 MMD.vue 的文件头注释中:VRM 渲染器通过 TresJS 声明式驱动,而 MMD 是纯指令式(imperative)的。MMD 运行时需要在一个手写的渲染循环中协调 mixer、IK、grant、physics 的更新顺序,因此组件直接持有WebGLRenderer、PerspectiveCamera、灯光、OrbitControls与逐帧管线,这些 three.js 对象都不经过 Vue 响应式系统,直接在循环中原地修改(如renderer、scene、camera、modelGroup都是模块级let变量)。
关键设置包括:
- 渲染器启用
alpha: true(透明背景)、antialias: true、preserveDrawingBuffer: true(保证captureFrame()能读回最后一帧),输出色彩空间为SRGBColorSpace; - 关闭 tone mapping(
NoToneMapping):MMD 的 toon 材质不是 PBR/HDR 材质,filmic tone mapping 会使其褪色发灰,因此直接输出原始颜色(MMD.vue); - 像素比取
Math.min(devicePixelRatio, 2) * renderScale,renderScale可在设置面板中调节渲染分辨率。
三、模型加载:ZIP 打包与裸 URL 双通道
两种来源的判别
入口函数loadMMDModelFromSource(packages/stage-ui-mmd/src/utils/mmd-loader.ts)先用 ZIP 魔数PK\x03\x04判别输入:
- ZIP 打包格式(常见分发形式:模型 + 纹理打包在一起)→ 走
loadMMDZip,解包为 blob URL,并安装"按 basename 匹配"的纹理解析器; - 裸
.pmx/.pmdURL→ 直接加载,纹理依赖服务器端相对路径解析。
// ZIP local-file-header magic: "PK\x03\x04". function isZip(buffer: ArrayBuffer): boolean { const head = new Uint8Array(buffer, 0, Math.min(4, buffer.byteLength)) return head[0] === 0x50 && head[1] === 0x4B && head[2] === 0x03 && head[3] === 0x04 }ZIP 解包与纹理重定向
loadMMDZip(packages/stage-ui-mmd/src/utils/mmd-zip-loader.ts)内部使用 JSZip 解压,然后:
- 扫描压缩包内所有
.pmx/.pmd文件作为候选模型变体(variants),取第一个作为主模型,detectMMDVariants返回全部变体供上层选择; - 将所有归档条目转为 blob URL;
- 生成
urlModifier纹理重定向函数。
纹理重定向是整个加载链路中最精妙的部分:PMX/PMD 二进制里保存的是相对路径(可能带子目录和反斜杠),而模型是从blob:URL 加载的,其 base 无法解析这些相对路径。解决方式是按 basename 匹配——先规范化路径并解码 URL,再尝试"完整路径后缀匹配"(处理重名罕见情况),最后退化为 basename 查找(mmd-zip-loader.ts)。由于单个模型的纹理 basename 实际基本唯一,这一方案在工程上是可靠的。
该重定向器通过createMMDLoaderContext(packages/stage-ui-mmd/src/composables/mmd/loader.ts)安装到独立的LoadingManager.setURLModifier上。注释里特别记录了一个坑:MMDLoader会以模型 URL 的 base 前缀拼接纹理请求,所以 ZIP 导入时请求会以blob:开头——不能一看到blob:就短路跳过,否则所有 ZIP 纹理都会失效;只有data:URI(内嵌 toon 纹理)直接放行。每个模型使用独立的LoadingManager,避免 URL 重写表在模型之间串扰。
OPFS 缓存与纹理就绪等待
- 当传入
cacheKey(即展示模型的 id)时,加载前先查 OPFS 缓存(utils/opfs-loader.ts),命中则直接使用本地缓存字节;ZIP 模型加载成功后也会回写缓存; waitForTextures选项专为离屏预览设计:MMDLoader解析出网格即 resolve,纹理仍在 LoadingManager 队列中异步加载。实时场景无所谓(一两帧内纹理自然出现),但一次性离屏渲染(如预览图)会捕获到无纹理/透明的帧。开启后通过waitForManagerIdle(4 秒超时兜底,防止挂起)等待队列清空。
生命周期与资源释放
loadMMDModelFromSource返回的ResolvedMMDModel结构为:
interface ResolvedMMDModel { mmd: MMD // 拥有 IK、grant、morph 与可选物理状态的 MMD 运行时 mesh: SkinnedMesh // mmd.mesh 的别名 format: 'pmx' | 'pmd' assets?: MMDLoadedAssets // 仅 ZIP 来源存在 dispose: () => void // 回收加载过程中创建的 blob URL }dispose()只回收 blob URL,不释放网格的 GPU 资源——那部分由场景组件统一管理(disposeModel()中调用disposeMMDObject并最终renderer.dispose()+forceContextLoss(),见 MMD.vue)。
四、运行时核心:mixer → IK → grant → physics 的固定更新顺序
动画管理器
createMMDAnimationManager(packages/stage-ui-mmd/src/composables/mmd/animation-manager.ts)是动画层的核心,职责包括:
- 持有模型的
AnimationMixer与VMD clip 注册表(Map<string, AnimationClip>); - 管理idle 动作与当前动作的切换、交叉淡入淡出(默认 0.4 秒);
- 提供求解器开关:
setPhysicsEnabled/setIKEnabled/setGrantEnabled/setGravity; - 懒加载并安装 Ammo 物理后端。
关键接口与行为:
| 方法 | 行为 |
|---|---|
init(idle?) | 安装 Ammo 物理、设置重力(0, -g, 0)、可选启动循环 idle 动作。并发调用共享同一个初始化 Promise;dispose 后完成回调不会"复活"管理器 |
registerClip(name, clip) | 以名称注册 VMD 派生 clip |
availableClips() | 全部已注册动作名 |
playAction(name, { loop, crossfade }) | 播放已注册动作;one-shot(默认)播放完自动回到 idle,loop 动作保持到被替换;未注册名称返回false并告警 |
setIdleMotion(name) | 将某动作设为循环 idle 基准,one-shot 结束后回到它;未注册返回false |
update(delta) | 以mmd.updateWithMixer(delta, mixer, { grant, ik, physics })驱动求解器 |
playAction的内部细节:LoopOnce的 one-shot 会clampWhenFinished,并在finished事件上挂监听回退到 idle;若没有配置 idle,则把当前动作fadeOut,让骨骼回到静止姿势而不是钳制在最后一帧关键帧上(animation-manager.ts)。
每帧更新顺序(AIRI 层的"叠加优先权")
渲染循环(MMD.vue)严格遵循 README 所述的三阶段顺序:
- three-mmd 运行时:
animation.update(delta)—— 保持 mixer → IK → grant → physics 的固有顺序; - AIRI 自有 morph 层:
emote.update(表情)→blink.update(眨眼)→lipSync.update(口型),确保口型/表情压过 VMD 内的嘴部/表情关键帧; - 视线层:
gaze?.update(...)在求解器之后旋转眼骨/头骨,避免被运行时覆盖。
五、配置中心:useMMD Pinia Store 与跨窗口同步
所有运行时配置(物理/IK/grant 开关、视线追踪、缩放、morph 覆盖、情绪→动作映射)都存放在useMMDstore 中(packages/stage-ui-mmd/src/stores/mmd.ts),通过设置面板编辑。绝大多数字段用useLocalStorageManualReset持久化,从而实现跨窗口同步——桌面应用中设置窗口与舞台窗口是独立的 renderer 进程、各自持有 Pinia store,localStorage 是它们之间同步的载体。
完整配置项与默认值
| 类别 | 配置项 | localStorage 键 | 默认值 |
|---|---|---|---|
| 物理/求解器 | physicsEnabled | settings/mmd/physics-enabled | true |
ikEnabled | settings/mmd/ik-enabled | true | |
grantEnabled | settings/mmd/grant-enabled | true | |
physicsGravity | settings/mmd/physics-gravity | 98 | |
| 视线 | gazeMode | settings/mmd/gaze-mode | 'mouse' |
| 相机 | cameraFov | settings/mmd/camera-fov | 30(度) |
| 灯光 | ambientColor | settings/mmd/ambient-color | '#FFFFFF' |
ambientIntensity | settings/mmd/ambient-intensity | 0.6 | |
directionalColor | settings/mmd/directional-color | '#FFFBF5' | |
directionalIntensity | settings/mmd/directional-intensity | 0.75 | |
directionalPosition | settings/mmd/directional-position | { x: 1, y: 2, z: 2 } | |
| 渲染 | albedoGlow | settings/mmd/albedo-glow | 0.45 |
renderScale | settings/mmd/render-scale | 1 | |
| 动画 | idleMotionName | settings/mmd/idle-motion | '' |
availableMotions | settings/mmd/motions | [] | |
emotionActionMap | settings/mmd/emotion-action-map | EMOTION_ACTION_NAME | |
| Morph | morphOverrides | settings/mmd/morph-overrides | {} |
availableMorphs | settings/mmd/available-morphs | [] | |
| 材质 | availableMaterials | settings/mmd/available-materials | [] |
materialOpacity | settings/mmd/material-opacity | {}(缺省为全不透明) |
几个值得展开的默认值语义(注释来源 mmd.ts):
physicsGravity = 98:MMD 的默认重力是 98(即 9.8 × 10,适配 MMD 的缩放尺度),以世界重力(0, -98, 0)施加。调低让头发/布料更"飘",调高更"垂坠";albedoGlow = 0.45:驱动每个材质的emissiveIntensity,让模型呈现 MMD/动画特有的"扁平发光"观感。0 = 仅受场景光照,1 = 近乎自发光。它配合克制的直射光使用:直射光调中等防止受光面过曝,glow + 环境光保证阴影面不暗(见 MMD.vue);gazeMode = 'mouse':与 VRM 渲染器保持一致的三种视线模式'camera' | 'mouse' | 'none';- 颜色默认值中有一个工程细节:颜色选择器会输出 8 位
#RRGGBBAA,而three.Color拒绝这种格式,因此normalizeHex会剥掉 alpha 通道(MMD.vue)。
VMD 动作的持久化与跨窗口同步
availableMotions只存{ id, name }描述符(localStorage 同步列表),VMD 文件本体存在 IndexedDB(localforage,键前缀mmd-motion-)。原因写得很清楚:blob URL 是窗口作用域的,不能跨窗口共享,所以每个窗口需要自己getMotionFile(id)取回File,再URL.createObjectURL物化自己的对象 URL(mmd.ts)。
Store 提供的动作管理 API:
addMotion(file):导入 VMD 到 IndexedDB 并加入同步列表;同名重复导入会覆盖同一 id,保持列表稳定;getMotionFile(id):按 id 取回持久化的File;removeMotion(id):从列表与 IndexedDB 删除;若删的是当前 idle 则同时清空idleMotionName;clearMotions():清空全部。
导入的 VMD 会在场景侧被syncMotions(MMD.vue)增量注册:跳过已注册名称,从 IndexedDB 取文件 → 建对象 URL →loadMMDAnimationClip解析 →animation.registerClip→finally中 revoke URL。解析后若 clip 的 tracks 为 0,会告警提示 VMD 的骨骼/morph 命名与当前模型不匹配(不同 rig/命名),这也是 AIRI 不硬编码模型名、改用候选名解析的原因之一。
瞬态触发器走 BroadcastChannel
oneShotAction(一次性动作请求)与视图刷新通知使用BroadcastChannel(airi-stores-stage-ui-mmd)而非 localStorage——注释明确指出:这是瞬态触发器,必须可靠到达舞台窗口的动画管理器,而 localStorage 的 storage 事件在 Electron 的多个 BrowserWindow 之间触发不可靠。请求带递增nonce,保证对同一动作的重复请求也能重新触发场景的 watcher(mmd.ts)。
六、情绪、眨眼与视线:让模型"活"起来
统一情绪词表
Emotion枚举(packages/stage-ui-mmd/src/constants/emotions.ts)与 Live2D/Spine 保持一致,共 9 个情绪:happy / sad / angry / think / surprised / awkward / question / curious / neutral。这样Stage.vue的 act 事件总线可以用同一套情绪名驱动任意渲染器。
情绪 → morph 目标映射
每种情绪由一组 morph 权重目标 + 交叉淡入时长组成(packages/stage-ui-mmd/src/constants/morphs.ts):
| 情绪 | morph 影响 | 淡入时长 |
|---|---|---|
happy | smile 0.8 + vowelA 0.2 | 0.4s |
sad | sad 0.75 + troubled 0.3 | 0.4s |
angry | anger 0.8 | 0.3s |
surprised | surprise 0.8 + vowelO 0.4 | 0.15s |
think | troubled 0.5 + serious 0.4 | 0.5s |
awkward | troubled 0.6 + smile 0.2 | 0.4s |
question | troubled 0.4 + surprise 0.2 | 0.4s |
curious | surprise 0.35 + smile 0.25 | 0.4s |
neutral | (无) | 0.6s |
源码注释说明了一个调校经验:主表情权重保持在0.7–0.8 而非 1.0,避免过度的夸张表情(参考了 VRM 在 issue #590 中报告的观感问题);次级嘴部 morph 只加少量形状,防止面部显得扁平。useMMDEmote会把实际权重按intensity缩放。
逻辑 morph 槽与候选名解析
这是 AIRI 处理"MMD 模型命名不统一"的核心机制。MMD(PMX/PMD)模型的顶点 morph 名称是日文(部分西方模型是英文),不同模型完全不同。因此 AIRI 从不硬编码单个字符串,而是定义14 个逻辑槽(morphs.ts):
- 嘴部元音:
vowelA / vowelI / vowelU / vowelE / vowelO(对应あいうえお) - 眼部:
blink / blinkLeft / blinkRight - 表情:
smile / anger / sad / surprise / troubled / serious
每个槽对应一个有序候选名列表,运行时取第一个真实存在的候选(resolveMorphName,见 packages/stage-ui-mmd/src/composables/mmd/morph.ts)。日文名在前,因为绝大多数 MMD 模型都是日文命名;英文候补覆盖少数西方/Blender 导出模型。例如:
vowelA: ['あ', 'a', 'A'], blink: ['まばたき', 'blink', 'Blink'], smile: ['笑い', 'にこり', 'わらい', 'smile', 'Smile'],用户还可以在设置面板用morphOverrides手动覆盖某个槽的绑定 morph(MorphController.override会触发rebind)。MorphController把SkinnedMesh.morphTargetDictionary(名字→索引)与morphTargetInfluences(索引→权重)的索引簿记封装起来,并实时读取influences 数组——缓存它可能在三.js 更新 morph target 时指向过期/分离的数组(morph.ts)。
程序化眨眼
useMMDBlink(packages/stage-ui-mmd/src/composables/mmd/blink.ts)与 VRM 渲染器的useBlink使用相同的时间模型:1–6 秒随机间隔一次眨眼,单次开合 0.2 秒,权重按sin(π·progress)正弦形变化,让眼皮运动平滑而非硬切换。
骨骼级视线追踪
MMD 没有 VRM 那样的 first-classlookAt接口,因此createGazeController(packages/stage-ui-mmd/src/composables/mmd/gaze.ts)直接旋转真实眼骨并给头骨叠加阻尼衰减的同一目标角,形成自然的跟随:
- 眼骨候选:
左目/右目(优先)→ 退化为両目控制骨(因両目由 append/grant 求解器在运行时内部驱动,事后旋转为时已晚,仅作 fallback); - 头骨候选:
頭 / head / Head; - 摆动极限(弧度):眼睛 yaw ±0.35、pitch ±0.25;头部 yaw ±0.2、pitch ±0.12;
- 空闲扫视(saccade):无追踪目标时,以 400–2200ms 随机间隔产生 ±0.4 范围内的小幅随机眼动,模拟真实的眼神游离;
- 平滑采用帧率无关的指数趋近:
rate = 1 - exp(-10 * delta)。
视线目标由场景组件解析(MMD.vue):camera模式把相机方向转到模型局部坐标系,换算成 yaw/pitch 相对摆动极限的比例并 clamp,实现"看向相机";mouse模式把光标位置归一化到画布坐标 [-1,1];none则只做空闲扫视。
七、口型同步:共享 wLipSync 配置到あいうえお
口型同步由useMMDLipSync(packages/stage-ui-mmd/src/composables/mmd/lip-sync.ts)实现,数据流为:
- 从共享包
@proj-airi/model-driver-lipsync创建 wLipSync 音频节点(createWLipSyncNode),使用共享的wlipsyncProfile; - 把
currentAudioSource连接到该节点(source.connect(node)); - 每帧调用
vowelDriver.update(node, delta)得到五个元音权重; - 按
MMD_SLOT_BY_VOWEL映射表写入对应 mouth morph 槽:
const MMD_SLOT_BY_VOWEL: Record<WLipSyncVowel, VowelSlot> = { A: 'vowelA', E: 'vowelE', I: 'vowelI', O: 'vowelO', U: 'vowelU', }Vue 的watch+onCleanup负责生命周期:audioContext变化时重建节点,audioSource变化时重连并断开旧连接(带 try/catch 容忍源已结束的情况)。调用方(MMD.vue)持有 AudioContext 与音频源的生命周期,本 composable 只拥有"源 → wLipSync 节点"的连接。口型写入发生在 MMD 动画 helper之后,保证口型压过 VMD 嘴部关键帧。若模型解析后没有任何元音嘴部 morph 可用,会在控制台告警"lip-sync cannot move the mouth"(MMD.vue)。
八、渲染循环、相机适配与设置面板联动
相机自动取景
frameCamera(MMD.vue)用Box3.setFromObject求模型包围盒,同时适配宽高两个轴(取两者所需距离的较大值 × 1.2),确保肖像或横屏视口下模型完整入镜不裁切,并相应调整 near/far 平面。这与 Spine 渲染器的自动取景行为一致。
设置面板的实时联动
组件对 store 中的每个配置项都挂了 watcher,实现"改设置即生效":
- 视图变换:
scale / rotationY / position→applyTransform; - 求解器:
physicsEnabled / ikEnabled / grantEnabled / physicsGravity→ 直接调用动画管理器对应 setter(gravity 会同时作用于已安装的物理世界); - 灯光:环境光/平行光的颜色与强度;平行光位置深监听;
- 渲染:
cameraFov更新投影矩阵;renderScale重设像素比;albedoGlow重设所有材质自发光;materialOpacity按材质名应用透明度(applyMMDMaterialOpacity,可让用户淡出或隐藏单个部件,availableMaterials会向设置面板发布每个部件的信息); - 动作:
oneShotAction触发一次性播放、availableMotions变化触发增量注册、idleMotionName变化切换 idle。
resetState()会重置全部设置并清空导入的动作,shouldUpdateView()通过 BroadcastChannel 通知所有窗口同步刷新。
九、测试与质量保障
包内配有覆盖关键链路的 vitest 测试(packages/stage-ui-mmd/vitest.config.ts):
- animation-manager.test.ts:动画管理器的注册、播放、idle 回退、求解器开关行为;
- morph.test.ts:逻辑槽候选名解析、覆盖绑定、权重读写;
- morphs.test.ts:形态槽常量与候选表的完整性;
- mmd-zip-loader.test.ts:ZIP 解包、变体检测、URL 重写器;
- mmd-loader.browser.test.ts:浏览器环境下的模型加载。
十、限制与适用环境小结
- 物理依赖Ammo.js(WASM),因此在无 WebGL/WASM 的环境中不能使用(README 明确列出);
- VMD 的骨骼/morph 命名若与模型 rig 不匹配,clip 可能 0 tracks——需要通过设置面板的 morph 覆盖与动作名映射来适配非标准模型;
- AIRI 有意不消费 VMD 的相机运动(
loadMMDAnimationClip注释说明),相机控制交给 OrbitControls 与场景设置; - 当前仓库尚未内置预设 idle VMD:动画管理器在无 idle 动作时仍会推进求解器与物理(MMD.vue),导入 VMD 后即可通过设置面板指定循环 idle。
综上,@proj-airi/stage-ui-mmd不是一个简单的模型查看器,而是一套完整的 MMD 舞台驱动方案:ZIP/裸 URL 双通道加载 + OPFS 缓存、三求解器可切换运行时、跨窗口的配置/动作持久化、以及叠加在 VMD 之上的表情—眨眼—口型—视线四层"活体"驱动。对照 packages/stage-ui-mmd/README.md 与 packages/stage-ui-mmd/src 目录可继续深入每个模块的具体实现。
【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考