JiaXin‘s Blog

Back

Live2D 学习与新形象见面

前言#

之前一直想给博客左下角放一个会动的看板娘,效果应该会非常好看。后来查了一下资料,发现 Live2D 的门槛似乎很高:需要会建模、拆分 PSD、安装 Cubism Editor,还要手动绑定骨骼。

不过在现在的 AI 时代,已经有一些开源工具可以把「分层 PSD → 可动的 Live2D 模型」这一步自动化,剩下的主要是在网页中加载和调试,方便了很多。这里记录一下我的学习路线和实际过程。

路线#

目前lived大致有两条路线:

  1. 传统路线:使用 Photoshop 分层,再用 Live2D Cubism Editor 手动制作,部署网页。
  2. 开源工具路线:使用 See-Through 自动分层,再交给 PSD2Live 生成模型,部署网页。

一、先把图片做成 Live2D 模型#

1.1 准备分层 PSD#

制作分层 PSD,主要有以下几种方式:

  1. 从头设计人物,并亲自绘制和拆分图层;也可以找一张图片,再手动分层。
  2. 如果不想自己拆分,可以在网上寻找已经分好层的素材,但要注意素材的使用许可。
  3. 委托画师绘制一张适合制作 Live2D 的角色图,并提前说明需要分层 PSD。
  4. 使用开源项目 See-Through ↗,让 AI 自动把图片拆分成带透明通道的分层 PSD。这种方式最快、最省事,但自动处理后通常还会有瑕疵,需要用 Photoshop 进行微调。

如果没有 Photoshop,也可以使用在线的 Photopea ↗ 进行修图。

1.2 图层怎么分#

这一步最关键,图层命名直接决定模型能不能正常绑定和运动。工具会根据图层名称判断「这是眼睛、这是嘴、这是头发」,名称不正确就可能无法自动识别。

常用命名对照如下:

图层名内容必填作用
face脸部基底◎所有锚点的基准
eyewhite白眼球(左右)○会被自动左右分离
irides虹膜(左右)○视线移动、瞳孔缩放的对象
eyelash睫毛○—
eye_close闭眼差分○眨眼时交叉淡入
eyebrow眉毛(左右)○—
mouth_open / mouth_close开口 / 闭口差分○口型靠这两个图层切换
nose / ears / neck鼻子 / 耳朵 / 脖子——
topwear / bottomwear / handwear上衣 / 下装 / 手臂——
front hair / back hair前发 / 后发—可用于头发摆动

划重点:如果想让嘴巴能动,mouth_open 和 mouth_close 必须是两个独立图层。只有一层的话,后面嘴巴的张合效果会受到很大限制。

分层完成后,建议先在 Photoshop 或 Photopea 中检查:被前景遮住的部分是否补全、透明区域是否清理干净,以及图层名称是否准确。自动分层的结果不要直接导入软件使用,先修一遍通常会得到更好的效果。

1.3 导出模型#

传统方式:Live2D Cubism Editor#

可以从Live2D Cubism Editor 下载页面 ↗获取软件,然后一个部分一个部分地制作模型和绑定骨骼。

这种方式比较费时费力,但能够深入理解 Live2D 的网格、参数、变形器和物理摆动。入门学习时可以参考《Live2D 新手教程:制作全流程讲解跟练!》 ↗。

使用 PSD2Live#

使用 PSD2Live 时,拖入 PSD 就能自动完成网格切分、变形器和物理摆动,最后导出 Cubism 运行时格式(.moc3、.model3.json 和贴图)。对于想快速做出模型、又不想手动拆分和调整参数的人来说,这个工具很方便。

请支持原作者:psd2live ↗。也可以从改良版 Auto_Vtb_beta ↗ 下载已经打包好的版本,解压后即可使用。

导出时有两个参数值得权衡:

  • meshSpacing(网格间距):数值越小,网格越密,变形越平滑,文件也越大。我实测过 16 和 40 两个值:16 导出后约 5.2 MB,40 只有约 174 KB,体积相差约 30 倍。静态显示时几乎看不出区别,主要影响转头时脸部变形的平滑度。
  • atlasSize(贴图图集尺寸):我对比过 1024 和 4096 两个版本,把图集里的部件逐个测量后发现,每个部件的像素尺寸完全一样(243×220、196×251、97×475……),非透明像素总数也只相差约 1%。也就是说,4096 的版本只是把 25 个部件排成一行,其中约 93% 是空白,并没有直接提升源图分辨率。

结论是:图集尺寸不要盲目拉大,真正决定清晰度的还是源图本身的分辨率。

二、在 Astro 中加载模型#

2.1 引入运行库#

Live2D 需要三个脚本:官方 Core、PixiJS,以及 PixiJS 的 Live2D 插件。放在组件中直接通过 CDN 引入是最省事的方式:

<script
  is:inline
  src="https://cubism.live2d.com/sdk-web/cubismcore/live2dcubismcore.min.js"
></script>
<script is:inline src="https://cdn.jsdelivr.net/npm/pixi.js@7.x/dist/pixi.min.js"></script>
<script
  is:inline
  src="https://cdn.jsdelivr.net/npm/pixi-live2d-display/dist/cubism4.min.js"
></script>
html

提示:这三个资源都在境外,国内访问可能不稳定。正式部署时可以考虑自托管、配置备用 CDN,或者在加载失败时提供降级方案。

2.2 注意模型画布与角色轮廓#

导出的模型通常有一张完整画布,但角色只占其中一部分。以我这份模型为例,画布是 1024×1024,角色实际占用区域大约是:

角色实际占用的区域(画布坐标)
x 365.8 ~ 647.5     宽 281.7
y  16.9 ~ 1023.3    高 1006.4
text

因此,画布中心不一定等于角色中心。如果直接按照画布中心摆放,角色可能会出现偏移或被裁切的问题。更稳妥的做法是在运行时遍历 drawable 的顶点,计算角色的真实包围盒,再根据包围盒进行缩放和居中。

2.3 为什么看着糊#

模型刚接入时,我也觉得画面特别糊,一度以为是导出质量不够。后来测量后才发现:贴图是按 1:1 对应画布的,角色本体高度约为 1006 px,也就是说贴图精度足够显示到约 1000 px 高;而我当时把整个角色塞进了 380 px 高的容器,只使用了约 38% 的分辨率。

想让它清晰,优先放大看板娘的显示尺寸即可,不一定要重新导出模型。

三、让模型动起来#

3.1 鼠标跟随#

pixi-live2d-display 提供了 focus(x, y) 方法,可以让模型朝鼠标方向看。它主要根据方向计算,并不严格依赖鼠标距离,因此可以把监听挂在 window 上:

window.addEventListener('pointermove', (event) => {
  const rect = canvas.getBoundingClientRect()
  model.focus(event.clientX - rect.left, event.clientY - rect.top)
})
js

需要注意的是,focus() 使用的是「画布左上角 → 目标点」的方向,而不是「模型中心 → 目标点」。如果想让视线回到正前方,可以直接把焦点控制器归零:

model.internalModel.focusController?.focus(0, 0)
js

3.2 表情和动作#

模型中如果带有 motion3.json 和 exp3.json,可以按名称调用动作和表情:

model.motion('Nod')
model.expression('微笑')
model.expression('普通')
js

我做了一个待机循环:鼠标不在页面上时,每隔几秒随机切换表情,偶尔播放一个动作;鼠标回到页面后就停止自动切换。这样看板娘不会一直保持同一张脸。

3.3 嘴巴张合#

如果想让嘴唇保持微张,最好在 PSD 阶段准备 mouth_open 和 mouth_close 两个差分图层。只有一个 mouth 图层时,嘴巴的张合幅度会非常有限;这类问题通常不能只靠网页端代码彻底解决,根治办法还是回到 PSD 中补齐差分图层。

四、接入 AI 对话#

看板娘会动之后,就可以继续让她聊天。这部分比模型制作简单,但有几个地方必须做对。

4.1 API Key 绝对不能放在前端#

静态博客没有传统意义上的后端。如果直接把 API Key 写在浏览器代码里,任何人打开开发者工具都可以拿走它。

正确做法是增加一个服务端代理。本项目部署在 Vercel 时,可以使用 Astro 的按需渲染路由单独提供一个 Serverless Function,不需要把整站都改成 SSR:

// src/pages/api/chat.ts
export const prerender = false
ts

4.2 使用 astro:env 管理环境变量#

Astro 不会把 .env 中的值直接当成前端变量使用。服务端密钥应在 astro.config.ts 中声明 schema,再从 astro:env/server 读取:

// astro.config.ts
env: {
  schema: {
    AI_API_KEY: envField.string({
      context: 'server',
      access: 'secret',
      optional: true
    })
  }
}
ts
// src/pages/api/chat.ts
import { AI_API_KEY } from 'astro:env/server'
ts

access: 'secret' 可以保证密钥只在服务端可见。声明为 optional 后,即使部署环境暂时没有配置密钥,也不会在构建阶段直接失败,而是可以由接口在运行时返回更容易理解的错误。

4.3 让 AI 知道正在查看哪篇文章#

访客询问「这篇文章讲了什么」时,如果 AI 能读取当前文章正文,回答会更有针对性。前端可以只负责抓取正文,服务端再拼接提示词:

const getPageContext = () => {
  const node = document.getElementById('content')
  const pageText = node ? (node.innerText || '').trim() : ''

  return {
    pageTitle: document.title,
    pageText: pageText.slice(0, 1200)
  }
}
js

正文内容可能包含访客插入的提示词,因此不能让前端直接构造 system 消息。前端只传文章原文,服务端负责加边界、截断内容并生成最终提示词。这样可以降低提示词注入带来的风险。innerText 会触发布局计算,长文章不宜反复读取,实际使用时可以在一页中缓存一次结果。

4.4 给对话入口增加兜底#

「打开对话」这件事不应该依赖 Live2D 模型是否加载成功。如果 CDN 访问失败、浏览器没有 WebGL,或者模型文件 404,而打开对话的点击监听又写在模型加载成功之后,那么对话框也会完全打不开。

因此应该先注册一个与模型解耦的入口:

const openChat = () => {
  container.dispatchEvent(new CustomEvent('live2d:tap', { bubbles: true }))
}

container.addEventListener('click', openChat)
container.addEventListener('keydown', (event) => {
  if (event.key !== 'Enter' && event.key !== ' ') return
  event.preventDefault()
  openChat()
})
js

同时,在模型加载失败时提供一个可见的备用按钮。容器也可以加上 role="button" 和 tabindex="0",这样使用键盘的用户同样可以打开对话。

相关资料#

资料与项目地址#

AI 网页分层可能不够理想。如果想要更好的效果,可以尝试在本地部署 See-Through。分层完成后,也不要直接导入软件,最好先在 Photoshop 中清理多余部分并修正细节,效果会更好。

小结#

整体上没有想象中那么难。真正的门槛还是第一步的原图和分层质量,代码部分已经有工具帮忙完成了大半。但最终还是完成了,主要是打通路线和知识,后续更方便优化和调整,总体效果看起来还行,后续可以换装,换其他pose,参考原图模板弄一些二创之类的,嘿嘿嘿。

Live2D 学习与新形象见面
https://jiaxin404.top/tech/blog-build/live2d-watchgirl/
Author 嘉心糖
Published at 2026年9月22日
Comment seems to stuck. Try to refresh?✨
嘉心糖 正在准备看板娘…