2130 字
11 分钟
给博客加 Live2D 看板娘:少女前线 + 流萤 多模型自由切换(全本地化,无外部 CDN)

给博客加一个 Live2D 看板娘,支持少女前线全角色 + 流萤 Firefly 自由切换,切换模型时对话内容也跟着换。全部资源本地化(不依赖 jsdelivr / raw.githubusercontent,国内直连可用),只有桌面端才加载,移动端零开销。

致谢:三个 Live2D 项目源头#

本方案由三个开源项目组合而成,先致谢:

  1. stevenjoezhang/live2d-widget —— 看板娘主体框架。新版(npm live2d-widgets 系)自带 cubism2 + cubism5 双内核:老版 .moc 模型和 Cubism4/5 的 .moc3 模型都能加载,这是把两个不同世代的模型塞进同一个看板娘的关键。
  2. nova1751/live2d-api —— 少女前线全部角色的 Cubism2 模型(95 式、HK416、UMP9、M4、AK12、AN-94、G36C 等 20+ 角色,含多套皮肤/破损形态)。原项目走 jsdelivr CDN,国内不稳,本文把它本地化。
  3. Iskongkongyo/Firefly-live2d —— 流萤(崩坏:星穹铁道)Cubism4 模型,含唱歌、表情、语音、对话消息框。原项目是 PixiJS 独立加载器,本文只取它的模型文件(model/)喂给 live2d-widget 的 cubism5 内核。

三个项目均按各自 LICENSE 使用;模型版权归原厂商(少女前线 / 米哈游),本文仅作个人博客装饰用途。

原理:为什么能一个看板娘装两个世代的模型#

  • live2d-widget 新版加载模型时先 fetch 模型的 index.json,看 Version 字段或有没有 FileReferences 自动分流:
    • Version 2 / 老格式(model.moc + textures + motions)→ 用 cubism2 内核(live2d.min.js + chunk/index.js)
    • Version 3 / 有 FileReferences(.moc3 + model3.json)→ 用 cubism5 内核(live2dcubismcore.min.js + chunk/index2.js)
  • 少女前线模型就是老格式 → cubism2 内核;流萤是 Cubism4(.moc3)→ 把它 model3.json 复制成 index.json 放流萤目录,自动走 cubism5 内核。
  • 模型列表写在 waifu-tips.json 的 models 数组里,每项 paths 是该模型全部形态(切衣服用),message 是它专属的开场白——switch-model 工具循环模型的同时把 message 也带上,对话随模型切换。

博客目录结构(public/live2d-widget/)#

public/live2d-widget/
├─ waifu-tips.js # 看板娘逻辑(新版 ESM)
├─ waifu-tips.json # 对话内容 + 模型列表(重点配置)
├─ waifu.css # 样式(含右下角定位)
├─ live2d.min.js # cubism2 内核
├─ cubismcore/
│ └─ live2dcubismcore.min.js # cubism5 内核(来自 Firefly 项目)
├─ chunk/
│ ├─ index.js # cubism2 辅助模块(ESM chunk)
│ └─ index2.js # cubism5 内核模块(ESM chunk,AppDelegate)
└─ model/
├─ <少女前线角色>/... # 从 live2d-api 全量拷贝(20+ 角色)
└─ firefly/ # 从 Firefly-live2d 的 model/ 拷贝
├─ index.json # = Firefly.model3.json 复制改名(关键!)
├─ FileReferences_Moc_0.moc3
├─ textures/ motions/ physics/ sounds/ expressions/

步骤#

1. 下载三个项目#

Terminal window
# live2d-widget:只要 dist/ 下的产物
# dist/waifu-tips.js dist/waifu-tips.json dist/waifu.css
# dist/live2d.min.js dist/chunk/index.js dist/chunk/index2.js
# live2d-api:只要 model/ 目录(全量角色)
# Firefly-live2d:只要 model/ 目录 + load/live2dcubismcore.min.js

GitHub 直链下载:https://raw.githubusercontent.com/<owner>/<repo>/<branch>/<path>,或 codeload.github.com/<owner>/<repo>/zip/refs/heads/<branch> 下整包再挑文件。

2. 组装到博客 public/#

把上述文件按上面的目录结构放进 public/live2d-widget/。注意两点:

  • 流萤的 index.json:把 Firefly.model3.json 原样复制一份改名为 index.json。live2d-widget 固定 fetch <模型路径>/index.json,看到 Version: 3 + FileReferences 才走 cubism5 内核。
  • GFL 模型不用改:每个形态目录(如 HK416/HK416_805/normal/)自带的 index.json 就是老格式,直接可用。

3. 写 waifu-tips.json(模型列表 + 对话迁移)#

保留 stevenjoezhang 默认的 message / seasons(点按、拍照、节日对话模板),替换 models 数组为本地模型。每个模型条目:

{
"name": "95type",
"paths": [
"/live2d-widget/model/95type/95type_405/normal/index.json",
"/live2d-widget/model/95type/95type_405/destroy/index.json",
"/live2d-widget/model/95type/95type_3702/normal/index.json",
"/live2d-widget/model/95type/95type_3702/destroy/index.json"
],
"message": "少女前线 Girls Frontline~ 95type"
}
  • paths 必须是绝对路径(以 / 开头),因为页面可能挂在 /posts/xxx/ 这类子路径,相对路径会解析错。
  • paths 里多个形态 = switch-texture 切衣服的候选。
  • message 是该模型的专属开场白——切到它时自动显示(对话随模型迁移)。少女前线的 message 直接用 live2d-api 的 model_list.json 里同款文案,流萤的自定义。

批量生成:读 live2d-api 的 model_list.json(models 数组每组路径列表 + messages 平行数组),展开成上面的格式,再追加流萤条目即可。

4. 页面注入(Layout.astro)#

在站点全局布局末尾加一段内联脚本,桌面端才加载(screen.width >= 768),避免移动端下载 200MB+ 模型:

<script is:inline>
if (screen.width >= 768) {
(function () {
const live2d_path = "/live2d-widget/";
function loadExternalResource(url, type) {
return new Promise((resolve, reject) => {
let tag;
if (type === "css") {
tag = document.createElement("link");
tag.rel = "stylesheet"; tag.href = url;
} else if (type === "js") {
tag = document.createElement("script");
tag.type = "module"; tag.src = url;
}
if (tag) { tag.onload = () => resolve(url); tag.onerror = () => reject(url); document.head.appendChild(tag); }
});
}
(async () => {
// 避免跨域图片问题(拍屏功能用)
const OriginalImage = window.Image;
window.Image = function (...args) {
const img = new OriginalImage(...args);
img.crossOrigin = "anonymous";
return img;
};
window.Image.prototype = OriginalImage.prototype;
await Promise.all([
loadExternalResource(live2d_path + "waifu.css", "css"),
loadExternalResource(live2d_path + "waifu-tips.js", "js"),
]);
initWidget({
waifuPath: live2d_path + "waifu-tips.json",
cubism2Path: live2d_path + "live2d.min.js",
cubism5Path: live2d_path + "cubismcore/live2dcubismcore.min.js",
tools: ["hitokoto", "switch-model", "switch-texture", "photo", "info", "quit"],
logLevel: "warn",
drag: false,
});
})();
})();
}
</script>

与官方 autoload.js 的差异:live2d_path 指向本地 /live2d-widget/;cubism2Path/cubism5Path 全部本地化(官方默认 cubism5Path 指向 cubism.live2d.com,国内不一定通);去掉了 asteroids 工具(它动态加载 jsdelivr 上的脚本,国内不稳)。若想保留可以自行把 asteroids.js 也本地化。

5. 验证#

Terminal window
# 部署后逐个验证资源可达
$d='https://<你的域名>.pages.dev'
foreach($u in @("$d/live2d-widget/waifu-tips.json","$d/live2d-widget/waifu-tips.js","$d/live2d-widget/live2d.min.js","$d/live2d-widget/chunk/index2.js","$d/live2d-widget/model/firefly/index.json")){
try { $r=Invoke-WebRequest $u -UseBasicParsing -TimeoutSec 20; "$($r.StatusCode) $u" } catch { "ERR $u" }
}
# 页面确认注入
(Invoke-WebRequest "$d/" -UseBasicParsing).Content.Contains('live2d-widget')

浏览器 Ctrl+F5 强刷首页,右下角出现看板娘;点工具栏的「切换模型」图标逐个换角色,观察模型与开场白一起变。

坑(全是实测踩出来的)#

  1. chunk/index.js 和 chunk/index2.js 必须都在——cubism2 用 index.js,cubism5 用 index2.js,缺一个对应内核加载即报错。
  2. cubism5 内核文件来自 Firefly 项目的 load/live2dcubismcore.min.js,别用 live2d-widget 官方 autoload 里那个 cubism.live2d.com 地址(国内可达性差)。
  3. waifu-tips.js 是 ESM module(tag.type = "module"),它内部 import("./chunk/index2.js") 是相对路径——所以 chunk 目录必须和 waifu-tips.js 同级,不能改层级。
  4. paths 用绝对路径,否则子路径页面 404。
  5. 移动端必须跳过加载——22 模型 125 形态约 235MB,全塞给手机 = 灾难。screen.width >= 768 门控。
  6. Pages 单文件 25MB 上限:本方案最大文件是流萤纹理 png(5MB),GFL 纹理 1-5MB,全部安全;如果以后塞进超大模型注意这个限制。
  7. 类型检查:astro check 0 errors 再部署;is:inline 脚本不会被 Astro 打包处理,直接原样输出。

效果#

  • 一个看板娘位置(右下角),switch-model 循环 22 个模型 / 125 个形态
  • 少女前线全角色(Cubism2)+ 流萤(Cubism4)自由切换,模型与对话同步迁移
  • 全部资源本地托管在博客自己的 Pages 域名下,国内直连,无外部 CDN 依赖
  • 桌面端才加载;页面缓存友好(静态文件,CF 边缘缓存)

更新记录(2026-09-17)#

角色专属对话(220 句)#

早期版本只有一套全局对话模板,切角色只是模型变、台词不变。新版给 waifu-tips.json 的每个模型加了 dialog 字段,每个角色一整套对话:

{
"name": "HK416",
"paths": ["/live2d-widget/model/HK416/HK416_805/index.json"],
"message": ["HK416 报道!指挥官,今天有什么任务?"],
"dialog": {
"default": ["哼,本小姐今天心情不错。", "指挥官,别盯着我看啦!"],
"tapBody": ["喂喂!谁让你随便碰的!", "再碰的话我可要生气了!"],
"hoverBody": ["看什么看,没见过美女吗?"],
"welcome": "哼,这不是回来了吗?本小姐等你很久了。"
}
}
  • default:定时随机闲聊(4 句)
  • tapBody:点击模型(3 句)
  • hoverBody:鼠标悬停(2 句)
  • welcome:欢迎语(1 句)

22 个角色 × 10 句 = 220 句。实现上在 waifu-tips.js 注入了一个 q(key) 读取器:按 localStorage.modelId 取当前模型的 dialog[key],没有则回退全局 message[key]。这样日常随机、点击、悬停都会说该角色的台词。

注意优先级:切换模型的 message 显示 4 秒(priority 10),期间会压住点击/悬停对话(9/8)——这是正常的,等切换语播完就轮到角色日常对话。

截图修复(via 浏览器不再卡死)#

photo 工具原来用 canvas.toDataURL() 生成超大 data URL 塞进 <a download>——via 等手机浏览器处理超长 data URL 会内存暴涨直接卡死。改为标准 canvas.toBlob() + URL.createObjectURL():

e.toBlob((t) => {
if (!t) return;
const u = document.createElement("a");
const r = URL.createObjectURL(t);
u.style.display = "none";
u.href = r;
u.download = "live2d-photo.png";
document.body.appendChild(u);
u.click();
document.body.removeChild(u);
setTimeout(() => URL.revokeObjectURL(r), 1e3);
});

Blob 由浏览器原生管理,不产生超长字符串,下载后 1 秒回收 URL。

其他细节#

  • 模型位置:右下角 → 左下角(#waifu { left: 0 }),工具菜单移到模型右侧(right: -35px),不再遮挡模型。
  • 隐藏/唤出(移动端):#waifu-toggle 需要 z-index: 2147483648 高于 #waifu(2147483647)——否则 waifu 背景透明但 z-index 高会拦截点击,手机上点橙色按钮没反应。移动端 toggle 常露 28px 可点,隐藏模型后露出全部 60px。
  • 对话框位置:#waifu-tips 的 margin-top 从 -30px 改为 -70px,避免遮住角色的枪械模型。
给博客加 Live2D 看板娘:少女前线 + 流萤 多模型自由切换(全本地化,无外部 CDN)
https://tsangasuna.pages.dev/posts/live2d-kanban/
作者
TsangAsuna
发布于
2026-09-16
许可协议
CC BY-NC-SA 4.0

分享文章

生成精美分享图或复制链接,与更多人分享本文。

继续阅读

沿着主题读

基于共同的标签与分类

换条路线

从其他文章中稳定抽取