AIRI 舞台 MMD 渲染器实战指南:@proj-airi/stage-ui-mmd 的模型加载、物理动画与口型同步全解析
2026/9/12 17:21:38 网站建设 项目流程

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、以及ammommd-loadermmd-previewmmd-validatormmd-zip-loaderopfs-loader等工具模块。

二、快速上手:场景组件与"指令式"渲染契约

最小接入示例

README 给出了最核心的用法——从包中导入MMDSceneuseMMD,前者是场景组件,后者是运行时配置的 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类型默认值说明
audioContextAudioContext未定义拥有currentAudioSource的音频上下文,供口型同步节点使用
modelSrcstring未定义模型地址:打包 ZIP 或裸.pmx/.pmdURL
modelIdstring未定义稳定缓存键(用于 OPFS 缓存与 ZIP 资源解析)
pausedbooleanfalse暂停渲染循环
cursorPosition{ x, y }未定义光标位置,用于mouse视线追踪模式
currentAudioSourceAudioBufferSourceNode未定义当前音频源,接入 wLipSync 节点
enableOrbitControlsbooleanfalse是否启用轨道控制器(拖拽旋转视角)

组件状态通过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 的更新顺序,因此组件直接持有WebGLRendererPerspectiveCamera、灯光、OrbitControls与逐帧管线,这些 three.js 对象都不经过 Vue 响应式系统,直接在循环中原地修改(如rendererscenecameramodelGroup都是模块级let变量)。

关键设置包括:

  • 渲染器启用alpha: true(透明背景)、antialias: truepreserveDrawingBuffer: true(保证captureFrame()能读回最后一帧),输出色彩空间为SRGBColorSpace
  • 关闭 tone mappingNoToneMapping):MMD 的 toon 材质不是 PBR/HDR 材质,filmic tone mapping 会使其褪色发灰,因此直接输出原始颜色(MMD.vue);
  • 像素比取Math.min(devicePixelRatio, 2) * renderScalerenderScale可在设置面板中调节渲染分辨率。

三、模型加载: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 解压,然后:

  1. 扫描压缩包内所有.pmx/.pmd文件作为候选模型变体(variants),取第一个作为主模型,detectMMDVariants返回全部变体供上层选择;
  2. 所有归档条目转为 blob URL;
  3. 生成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)是动画层的核心,职责包括:

  • 持有模型的AnimationMixerVMD 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 所述的三阶段顺序:

  1. three-mmd 运行时animation.update(delta)—— 保持 mixer → IK → grant → physics 的固有顺序;
  2. AIRI 自有 morph 层emote.update(表情)→blink.update(眨眼)→lipSync.update(口型),确保口型/表情压过 VMD 内的嘴部/表情关键帧
  3. 视线层gaze?.update(...)在求解器之后旋转眼骨/头骨,避免被运行时覆盖。

五、配置中心:useMMD Pinia Store 与跨窗口同步

所有运行时配置(物理/IK/grant 开关、视线追踪、缩放、morph 覆盖、情绪→动作映射)都存放在useMMDstore 中(packages/stage-ui-mmd/src/stores/mmd.ts),通过设置面板编辑。绝大多数字段用useLocalStorageManualReset持久化,从而实现跨窗口同步——桌面应用中设置窗口与舞台窗口是独立的 renderer 进程、各自持有 Pinia store,localStorage 是它们之间同步的载体。

完整配置项与默认值

类别配置项localStorage 键默认值
物理/求解器physicsEnabledsettings/mmd/physics-enabledtrue
ikEnabledsettings/mmd/ik-enabledtrue
grantEnabledsettings/mmd/grant-enabledtrue
physicsGravitysettings/mmd/physics-gravity98
视线gazeModesettings/mmd/gaze-mode'mouse'
相机cameraFovsettings/mmd/camera-fov30(度)
灯光ambientColorsettings/mmd/ambient-color'#FFFFFF'
ambientIntensitysettings/mmd/ambient-intensity0.6
directionalColorsettings/mmd/directional-color'#FFFBF5'
directionalIntensitysettings/mmd/directional-intensity0.75
directionalPositionsettings/mmd/directional-position{ x: 1, y: 2, z: 2 }
渲染albedoGlowsettings/mmd/albedo-glow0.45
renderScalesettings/mmd/render-scale1
动画idleMotionNamesettings/mmd/idle-motion''
availableMotionssettings/mmd/motions[]
emotionActionMapsettings/mmd/emotion-action-mapEMOTION_ACTION_NAME
MorphmorphOverridessettings/mmd/morph-overrides{}
availableMorphssettings/mmd/available-morphs[]
材质availableMaterialssettings/mmd/available-materials[]
materialOpacitysettings/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.registerClipfinally中 revoke URL。解析后若 clip 的 tracks 为 0,会告警提示 VMD 的骨骼/morph 命名与当前模型不匹配(不同 rig/命名),这也是 AIRI 不硬编码模型名、改用候选名解析的原因之一。

瞬态触发器走 BroadcastChannel

oneShotAction(一次性动作请求)与视图刷新通知使用BroadcastChannelairi-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 影响淡入时长
happysmile 0.8 + vowelA 0.20.4s
sadsad 0.75 + troubled 0.30.4s
angryanger 0.80.3s
surprisedsurprise 0.8 + vowelO 0.40.15s
thinktroubled 0.5 + serious 0.40.5s
awkwardtroubled 0.6 + smile 0.20.4s
questiontroubled 0.4 + surprise 0.20.4s
curioussurprise 0.35 + smile 0.250.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)。MorphControllerSkinnedMesh.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)实现,数据流为:

  1. 从共享包@proj-airi/model-driver-lipsync创建 wLipSync 音频节点(createWLipSyncNode),使用共享的wlipsyncProfile
  2. currentAudioSource连接到该节点(source.connect(node));
  3. 每帧调用vowelDriver.update(node, delta)得到五个元音权重;
  4. 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 / positionapplyTransform
  • 求解器: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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询