我以为能给站点请个会动的吉祥物,结果来了个 64×64 的方块人

第 2 篇的灶台 3,那道叫 mc-live2d 的菜,今天端上来给你看看。
点菜时我以为端上来的是二次元立绘,结果后厨端上来一个……方块人。还会游泳。


🤯 开场:一次“货不对板”的请客

先说结论:这个吉祥物,我请了两天,换了两次“技术方案”,最后它学会游泳了。

事情要从一个很朴素的愿望说起——我想给博客右下角放一个会动的小人,当吉祥物(就是那种网站角落的看板娘,mascot,站点形象代言人)。

然后我打开搜索框,输入了“Live2D”。

这是我今天犯的第一个错误。

因为我的“吉祥物素材”,是 Minecraft 皮肤——一张 64×64 像素的平面 PNG 贴图(就是那张决定你角色长啥样的方块皮)。

我想要的:会眨眼、会说话、会摆姿势的 Live2D 老婆……不是,吉祥物。

现实是:它是一张像素贴图,而 Live2D 需要的是 .moc3 模型(一种带骨骼绑定的动画模型),两者之间没有自动转换工具。

就像你想请一位 3D 建模师,结果来的是位平面设计师——不是不能合作,但一开始的期望全错位了。

场景还原:
我:我想要会动的二次元立绘!
现实:这是你的 64×64 贴图。
我:那它至少能动吧?
现实:它能……游泳。划水那种。


🐒 踩坑 01:先请来了第一位师傅(minecraft-skin-viewer)

没有 Live2D,那就退而求其次:用 three.js(一个 3D 渲染库)把皮肤贴图直接贴到方块人几何体上——做一个“3D 方块立绘”,这也是 Minecraft 本体干的事。

第一步我选了开源库 minecraft-skin-viewer,它把“方块人 3D 渲染”封装好了:

import { SkinViewer } from "minecraft-skin-viewer";

const viewer = new SkinViewer({
  canvas,               // 挂到哪个画布
  skin: "/skins/styunlen.png",  // 皮肤贴图
  width: 180,
  height: 180,
});

效果:右下角出现一个方块人,能旋转、能呼吸浮动,还挺像回事。

但用了两天,问题来了:

这个库的 API 是“黑盒”——我想让方块人“看鼠标”(头转向鼠标方向),但它的头部部件(head)不是公开 API,我只能靠整身旋转 + 各种 hack(类型全靠 as any 硬撑,懂的人都懂这种痛)。

这就好比:你雇了个员工,但他不给你留工位钥匙——你只能透过窗户指挥他。


💡 灵光 01:两天后,我换了位师傅(skinview3d)

两天后我发现了 skinview3d(同一个生态的另一个库),一个词总结:部件级 API

import { SkinViewer } from "skinview3d";

const viewer = new SkinViewer({ canvas, skin, width: 180, height: 180 });

// 部件级操作:直接转头,不动整身
viewer.playerObject.skin.head.rotation.y = 0.5;  // 头往右转

关键差异:

minecraft-skin-viewer(前任)skinview3d(现任)
头部控制❌ 不公开,只能 hackskin.head 直接操作
类型安全❌ 全靠 as any✅ 全类型,零 hack
动画内置旋转/浮动可插拔动画(IdleAnimation/SwimAnimation)

两天前刚上的库,两天后我把它卸了。 代码重写,但方向对了——从此我有了“眼睛跟随”的基础。


🏊 主角:游泳系统——一个方块人为什么要游泳

吉祥物站右下角不动,很呆。我想让它偶尔游动一下。

需求听起来简单:“随机朝某个方向移动一段距离。”但实现起来全是魔鬼细节:

细节 1:距离要是“屏幕自适应”的

游太短没存在感,游太长可能撞出屏幕。于是:

// 游动距离 = 屏幕对角线 × 随机比例(25% ~ 100%)
const ratio = 0.25 + Math.random() * 0.75;   // 0.25 ~ 1.0
const dist = Math.hypot(vw, vh) * ratio;      // hypot = 对角线长度

手机和显示器都能游得“恰到好处”——游对角线的 1/4 到一整条。

细节 2:撞墙要反弹,而且得预计算

“移动到屏幕边缘”意味着要撞边界反弹。但如果我们用“移动中实时检测碰撞”,会出现卡在边缘抖动的 bug。

解法:游泳前先把整条反弹轨迹算好(simulateSwimPath),然后照着轨迹走:

// 预计算:从当前位置出发,沿 angle 方向游 dist 距离,
// 撞到视口边界就反弹,返回整条轨迹 + 终点
const path = simulateSwimPath(start, angle, dist, bounds);

就像游泳运动员下水前先看好赛道——我们是先算好撞墙路线再出发

细节 3:朝向要和游泳方向同步(这是最骚的)

方块人游动时,应该侧身朝游泳方向,而不是永远脸朝观众。

skinview3d 里,模型的旋转用欧拉角(Euler,绕三个轴转的角度组合)。关键是把旋转顺序改成 YXZ

viewer.playerObject.rotation.order = "YXZ";  // 先侧身,再朝目标方向

这样 yaw(水平转向)能扫过长轴,让方块人侧着身子游——就像真的在划水,而不是平移的纸片人。

细节 4:划水次数要对上时间

游泳动画(SwimAnimation)有固定节奏,屏幕位移的速度要和划水同步——不然会出现“动画在划水,人却平移”的割裂感。

const swimMs = Math.round(dist / SWIM_SPEED);        // 距离 ÷ 速度 = 总时长
viewer.animation.speed = 3 / (swimMs / 1000);        // 让 3 次划水刚好游完总时长

目标:游完全程恰好划 3 下。比考游泳教练证还讲究。


🐒 彩蛋坑:移除“收起”功能,因为它毫无意义

原设计里,看板娘可以点击收起/展开(右下角消失/出现)。

但实际用下来:这个功能毫无意义——谁会主动把吉祥物收起来?它又不挡视线(半透明、右下角)。

所以我把收起功能整个删了,让它永远挂在那。顺带把“拖动”做成全画布拖动——现在你可以在屏幕上随意挪它,它会记住位置(sessionStorage 持久化,SPA 页面切换不丢)。

删除功能有时候比添加功能更正确——砍掉没人用的按钮,是对用户最好的尊重。


🎉 结尾:这个方块人的自我修养

回顾一下,这个“吉祥物”最终变成了:

  • 👀 眼睛跟随:鼠标靠近,头转过来看你(半径 350px)
  • 🌬️ 呼吸:待机时轻微浮动
  • 🏊 游泳:随机 360° 方向,屏幕自适应距离,撞墙反弹,侧身划水
  • 🖱️ 可拖动:全画布拖动,位置记忆

不是我最初想要的 Live2D。但它是我的 MC 皮肤,是方块人的形状,而且它会游泳——这个组合意外地有点可爱。

它教会我的事:技术选型时,“能不能达到理想效果”比“听起来高级”重要一万倍。
我想要的是会动的吉祥物,现实给了我会游泳的方块人——接受现实,然后把它做到最好。


🔮 下篇预告

第 3 篇到这里。吉祥物到位,缓存会自己补货,页面能丝滑切换……

然后,第 4 篇,最黑暗的一章来了。

我给缓存装了自动补货员,自我感觉良好。
直到有一天我发现:货架上摆的全是过期面包,而补货员……根本没被叫醒过。

下一篇:《缓存复仇记:挡在我完美缓存前面的,是那个更傻的缓存》

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
Source: https://github.com/zhaoolee/ChineseBQB
Source: https://github.com/zhaoolee/ChineseBQB
Source: https://github.com/zhaoolee/ChineseBQB
颜文字
Emoji
小恐龙
花!
滑稽大佬
演奏
程序员专属
上一篇