前言
在之前搭建博客的文章里,我写过想把微光放进网页,后面接了一句“待更新”。现在终于可以更新了。
最开始我想用 Inochi2D 做看板娘。不过,我的 2D 模型还没做完,3D 模型倒是已经能在 VRChat 和 Warudo 里使用了。既然如此,为什么不直接把现成的模型用起来呢?于是便有了 StarLite。
如果你正在电脑上读这篇文章,应该已经看到旁边的微光了。他会看向光标,偶尔活动一下,戳他也会有回应。同一套模型组件还接到了 DSH(DeepSeek Harness),可以根据 Agent 的工作状态播放动作和显示气泡。
这篇文章主要记录我想实现什么、各部分如何工作,以及一些静态截图里看不出来的小问题。具体安装和配置会继续更新,请以对应版本的仓库文档为准。
我想要一个什么样的桌宠
现在已经有一些功能十分全面的虚拟角色项目,可以语音对话、调用大模型,甚至观察屏幕和操作游戏。我最初想做的事情要简单得多:让自己的角色在博客或桌面的一角待着,在适当的时候回应一下。
因此,StarLite 的核心可以概括为:把模型已有的动作,映射到一些交互和应用事件上。
比如点一下角色,挥手并打个招呼;Agent 工作时,切换到工作动作;需要我回答时,在气泡里显示选项。平时则安静地站着,有一点呼吸和眨眼。微光只是默认示例,更重要的是其他人也能放进自己的角色。
我希望它轻量、容易上手,而且不要占据太多注意力。气泡本身不调用大模型,博客中的句子也是提前写好的。DSH 里的实时内容来自 DSH,桌宠负责呈现。要让角色说什么、做什么,不需要再养一套推理服务。
当然,“功能简单”和“实现简单”是两回事……
从模型到可以交互的角色
模型、动作和程序是不同的东西
模型本身主要保存几何、贴图、骨骼和表情等信息。有了模型,并不代表程序自动知道怎样让它挥手。
这里采用 VRM,是因为它在 glTF 的基础上约定了人形骨骼、表情、视线和弹簧骨骼等信息。程序可以按“头部”“左上臂”这样的标准映射操作角色,不必为每个模型重新猜一遍骨骼名称。浏览器中的加载和渲染使用 Three.js 和 three-vrm。
用户导入标准人形 VRM 后,不必先制作一整套动作。StarLite 自带一个基础 Idle,提供放松站姿和轻呼吸,再配合眨眼、限角光标跟随。对应效果仍然需要模型正确配置:比如眨眼需要 VRM 的 Blink 表达绑定,眼球跟随需要合适的视线设置。给一个形态键起名叫“眨眼”,不等于已经完成了这些配置。
挥手、击掌之类的具体动作,则需要模型作者自己提供。动作可以内嵌在模型文件中,也可以使用外部 VRMA。微光的那套动作不会自动套在别人身上;不同角色的比例、手指和特殊肢体,仍然需要实际检查。
动作和句子需要一起考虑
如果分别随机选一个动作和一句话,就可能出现身体摔倒、气泡里却在说“做得好”的情况。因此,句库可以按动作绑定:先决定做什么,再从对应句子里选一句。
动作之间也需要衔接。循环、单次动作和静态姿势,处理方式并不一样;把一个静态姿势叫作“待机动画”,并不会让它自己呼吸。运行时可以处理有限的姿态过渡,但不能代替动作制作。模型穿模、权重不合适、手指没有正确映射这些问题,最后还是要回到模型或动作里修改。
项目是怎样组织的
现在主要有两个仓库:
- StarLite:公共核心、模型后端和 Hexo 插件在同一个工作区,按包分开。
- StarLite for DSH:DSH 事件接入和 Windows 桌宠,使用前面的模型运行时。
公共核心负责气泡、句库调度和播放契约;VRM 后端负责渲染、骨骼、表情、物理和动作;Hexo 插件负责配置、资源生成和页面生命周期。早期的 Inochi2D 实现也保留在独立后端里,目前主要完善的是 VRM。
所以,气泡不是 Hexo 独有的功能,DSH 也不需要再写一套模型播放器。修改句子不必碰渲染代码,把模型接到另一个应用时也不需要把博客一起带过去。
一些真正用起来才会发现的问题
切换文章,不应该换一只角色
Volantis 使用 PJAX 切换页面时,不会重新加载整个网页。如果每次切页都重新创建模型,既慢,也可能留下重复的画布和事件监听;但如果只保留角色而不更新上下文,他又会继续说上一篇文章的句子。
现在的做法是保留模型实例,只更新页面对应的句库。首次入场按标签页去重,不会每翻一篇文章就重新挥一次手。加载失败时先隐藏,再自动重试,成功后恢复;失败的角色也不应该挡住正文和链接。
耳朵为什么会突然飞起来
动作、视线和弹簧物理都可能修改骨骼。如果同一根骨骼在不同阶段被重复更新,或者上一帧的物理结果被当成下一帧的基准,就会出现很奇怪的抖动。微光的耳朵确实给这个项目找了不少事情做……
这里需要明确谁负责哪一部分、按什么顺序更新。类似地,正在控制头部的动作和光标跟随,也不能无条件地同时覆盖同一个旋转。看到耳朵异常时,一味调整弹簧力度并不能解决更新顺序的问题。
页面掉帧,也不一定是模型的问题
有一轮测试中,小窗口流畅,最大化之后却持续低帧。最后发现博客的全屏星空背景也在消耗渲染时间。角色和背景共用浏览器的资源,只盯着模型优化,很容易找错地方。
静态银河纹理可以预先生成,再做移动、旋转;流星和闪烁星星则单独更新。模型本身还要控制绘制分辨率,在页面隐藏时暂停动画和物理。角色看起来只占屏幕的一小块,不代表贴图、骨骼和浏览器合成就没有开销,因此目前我还不想给出一个脱离设备和模型的“性能很好”的结论。
给每篇文章写几句话
这次新加了页面专属句库。文章正文和属于这篇文章的句子,可以放在同一份 Markdown 里维护。在开头的 front matter 中直接写:
1 |
|
默认点击和偶发互动都可以触发,按顺序读,读完再从第一句开始;它们使用全站入场动画作为配套动作。这里沿用的是动作,不会因为切换文章重新触发一次自动入场。
如果想单独指定动作或规则,可以改成对象形式:
1 | starlite: |
这表示只在点击时触发,页面两句使用
HighFive,和全站点击句库合并后随机选择;全站候选仍保留各自的动作与句子绑定。HighFive
是微光的动作名,换成自己的模型时也要换成实际存在的名称。默认值还可以统一写在博客
_config.yml 的 starlite.pageDialogue
中,不必每篇重复配置。
这篇文章本身就用了最简写法,可以戳旁边的微光试试。切到其他文章后,他会恢复全站句库。
使用和后续
Hexo 插件的公开测试版安装入口是:
1 | npm install hexo-starlite@alpha |
不过,本文页面使用的是新的候选构建,页面专属句库和默认自动获取微光的流程尚未发布到 npm。公开版本的具体安装、模型导入方式请查看对应版本的 README;本轮验证通过后再同步发布。
程序采用 BSD-2-Clause 许可,微光模型采用单独的模型许可,两者不是一回事。将角色放进网页,意味着浏览器必须能下载运行所需的资源;只能在授权范围内托管,不能把“文件能下载到”理解为可以提取再利用。使用自己的模型,也要确认模型、贴图和动作的公开托管权限。
目前项目仍在 alpha 阶段,主要验收环境是桌面 Chrome 和 Edge,小屏默认不加载模型。我更想继续把导入、性能和交互这些基础部分做好,再根据实际使用反馈补功能。
从三视图、建模,到 VRChat、Warudo,再到现在的网页和桌面,同一个角色又多了一种呈现方式。对我来说,这也是继续完善他的一个理由。
如果你也有自己的角色,可以来试试。遇到问题或者有什么想法,欢迎在评论或仓库里告诉我。