桌寵換成會動的骨架,開心那一格反而最看不出來

格莉奇OS 右下角的桌寵在 9 月 11 日凌晨換成 glitch-2d 的原生 2D 骨架,會呼吸、眨眼,講話時嘴型跟著音量走。可是量下去,開心是四個表情裡最弱的,缺的是畫出來的眼睛。那天量的數字後來還被自己推翻了一次,尺開錯了。

舊靜態圖與新 2D 骨架的四個表情對照,上排是舊圖,下排是骨架

先講結論:桌寵換成會動的以後,開心那一格是四個表情裡臉上變化最小的,缺的是畫出來的眼睛。

那天還量出另一個結論:開心跟當機都輸給原本那張不會動的圖。那個結論後來作廢了,因為那把尺本身是錯的,過程寫在最後一節。

站是格莉奇OS,我的角色格莉奇那個開在瀏覽器裡的桌面,右下角常駐的那位就是桌寵。要不要把她從「幾張表情差分圖」換成「一副會動的骨架」,是我拍的板。骨架的執行期、把它掛上站的程式、量測腳本,都是 AI 寫的。

凌晨,她變成活的

第一步是把骨架搬上站。9 月 11 日凌晨那筆 commit 把她從換圖改成活的:站上的 glitch2d/ 是從 glitch-2d 抄過來的執行期副本,四支 engine 模組加一份 rig.json,貼圖轉成 WebP,8 張從 PNG 的 8.2 MB 壓到 0.7 MB。

canvas 直接掛在桌面的 DOM 裡,沒有包 iframe。桌寵頭上的對話泡、點下去開聊天的命中區、當機時的抖動與色偏,都是站上原本就有的 CSS 與事件,隔一層 iframe 就得全部重做。

退路有三層,每一層都實測過。WebGL 建不起來就換 Canvas 2D。失敗的 context 會把 canvas 的型別鎖死,所以要先換掉節點,站上顯示的是 mountPet() 回傳的那一張。rig.json 或貼圖載不到,桌寵就維持原本的靜態立繪。

她是常駐的,所以畫面壓在 32fps,捲出視窗或切到別的分頁就不畫。

早上起來,她縮水了

凌晨掛上去的時候,canvas 只寫了 height:100%,等於把她塞進 #pet 容器裡。那個容器桌機 46vh、手機 34vh,兩邊都把她縮小了,桌機上她只剩舊版高度的 27%。這條跟骨架沒關係,是版型自己改壞的。

舊版有兩套做法。桌機讓圖用原始尺寸溢出容器,做出「超大半身、腳在畫面外」的效果;手機才用 min(62vh,520px) 收成小全身。

修法是讓 canvas 照同一套走。--pet-figure 定她的身高,canvas 依她佔畫布的比例反推高度,#pet 的寬度固定成舊圖的寬度,命中區與對話泡那些百分比才不會跟著跑。位移是量出來的:拿她胸前那塊 ERROR 布標,對到新舊兩版的中心重合。

用 Playwright 量(1440x900 與 390x844,動畫停掉):

項目 舊 新
桌機:頂端 417 415
桌機:螢幕上可見高 483 485
桌機:右緣距視窗右 150 151
桌機:ERROR 布標寬 70 71
桌機:布標中心 x 1088 1087
手機:頂端 483 483
手機:可見高 361 361
手機:右緣距視窗右 0 0

旁邊的黑洞先生(站上另一個角色)兩個斷點都沒被影響,跟舊版相同。

剩下的差是姿勢:舊圖是舉手揮手,手臂會多佔一點寬度。

版型對齊了,才開始量表情

版型對齊之後,新舊兩版才能拿同一塊臉來比。量法是:同一個視窗、同一塊臉的矩形、把動畫停掉,算每個表情跟平常臉的平均像素差。

第一次量出來,骨架的開心是 2,發呆是 3。

那時候用的是 engine 內建的 happy 與 curious。那兩組是給角色展示頁用的微表情,那裡臉佔滿整個畫面。happy 只讓嘴角彎 3 像素、眉毛抬 2 像素,原始碼的註解自己就這樣寫。桌寵只有 220 像素高,這種幅度跟平常臉分不出來。

所以另開一組參數。這副骨架能改變臉的只有四件事,全部用上:

那天再量一次,開心跟當機還是輸

改完跟舊的靜態圖擺在一起,當天量到這八個數字:

表情 舊靜態圖 新骨架
開心 31.5 22.2
發呆 20.8 23.4
睡眠 30.1 24.1
當機 42.0 30.9

發呆已經超過舊圖,睡眠接近,開心跟當機還差一截。

這張表隔天就作廢了,理由在最後一節。這裡照當天的樣子留著,因為當天是照著它往下做的。

差在哪,看封面那張對照。舊圖的開心是一對閉起來的笑眼加一張大笑的嘴,骨架只能把 eyeOpen 壓到 0.3 瞇起來,形狀不對。舊圖的當機是兩顆螺旋眼,骨架只能閉眼加張嘴,抖動與色偏交給 CSS,螺旋是參數生不出來的。

缺的都是畫出來的眼睛。四件事都用上了,剩下的是素材問題:臉部圖集要加虹膜與眼線的差分,rig.json 加對應部件,再跑一次同步腳本 sync_glitch2d.py --write。這是改角色素材,要我先看過,所以當天只寫進 NEXT.md,等下一輪。

一張死圖可以把笑眼直接畫出來,骨架得先有那張素材才擺得出來。

按鈕在開始播的瞬間彈回去

換骨架還弄壞了一顆按鈕。「聽我自我介紹」那顆按鈕改由骨架播音檔之後,一按下去就立刻彈回未播放的狀態。

原因在 speak() 內部會先 stop() 一次做重置,那一發 stopped 事件送到站上,按鈕就以為已經播完了。改成播出來以後的停止才算數。另外把 handle 掛上 window.petRig,方便在主控台除錯,並加一個唯讀的 mouthOpen 當驗收指標。

實測:靜止時 mouthOpen 是 0,播放中最大 0.808、平均 0.319,嘴巴那塊的像素變化量從 2.65 上升到 5.45,再按一次會停。

那天還沒做完的兩件事

點她會揮手,可是那是骨架內建的 wave(),手臂前後擺動。舊素材 pet-plain.webp 那張是舉起手、手掌張開,兩種是不同的動作。要做成舊圖那樣,得另外產一張「舉起來的袖子加張開的手掌」,做成可切換的部件,那是 glitch-2d 那邊的工作。

另一件:站上其他地方的她還是舊畫風。大頭照、九宮格貼圖、心情日記的配圖,都還是原本那套。桌寵換成新骨架之後,同一個角色在同一個站上有兩種畫風並存,還沒處理。

後記:9 月 12 日的現況

這篇寫完之後,上一節那兩件補了一件,正文中段缺的眼睛也補了。

掌心朝外的手做出來了。旋轉變不出掌心,所以袖子換成拿掉手的版本,手獨立成兩個部件放在同一個 slot,手臂擺到 0.55 的時候硬切換,任何時刻只有一隻手。切換點落在手臂還在途中的低角度。逐格看得出來手在那一格變了,動起來察不察覺得到,還沒有人判定過。手畫在袖子底下,腕端延長一段藏進袖口,看起來是從袖子裡伸出來的。

畫出來的笑眼與螺旋眼也做出來了,摳進 face-features 的空白區,開心與當機自動套用,站上的 motion.js 也接了對照表。

畫風並存那件還沒動。大頭照、九宮格貼圖、黑洞先生、日記配圖都還是舊畫風,桌寵自己的靜態退路圖 pet-plain.webp 也是,加上正面掌心是生成素材,嚴格說現在是三種畫風在同一個站上。

那兩格追上了嗎?用 glitch-2d 新加的 tools/check-expression.mjs 量,取逐幀最強。下面這組是為了寫這篇,在 9 月 12 日的 HEAD 上重跑的;README 裡那組是更早的取景留下來的,沒有照抄。

表情 只靠參數 加上畫出來的眼睛
happy 14.1 15.0
error 59.9 63.0

有進步,幅度不大,記錄裡的原話就是「確實有幫助但幅度不大」。開心多了 6%,當機多了 5%。

正文那張表為什麼作廢,理由比數字本身重要:會動的東西不能用時間平均去量。零均值的抖動平均之後會互相抵消。當機那一格每一幀都被呼叫端隨機抖動,逐幀最強 63.0,時間平均只有 43.1,平均低估了 32%;靜態的三個只低估 7% 到 13%。

正文那八個數字就是用時間平均算的,而且沒有留下腳本,重跑不了。所以「有沒有輸給舊的靜態圖」現在答不出來,要重新立一次基準才行。

這把新尺量到的另一件事:開心 15.0,發呆 34.1。四個表情裡最弱的是開心,這一條在兩把尺底下都成立。

還有一件是推上線之後才發現的。揮手那隻手在揮動途中會離開袖口,最遠差 102 像素。本機七條檢查全綠、定格圖正確、逐格也對,還是推上去了;是我在真的站上看她動才看出來的,9 月 12 日早上修掉。

漏掉的原因是那七條量的是切邊、可見度、袖根破口,沒有一條在量「手跟袖子有沒有連在一起」,而定格圖抓不到揮動中途。第八條檢查補上去了:逐幀比對袖子的腕關節與當下那隻手的腕點,超過 4 像素就失敗,現在全程 0.0。

這件跟表情那件是同一個形狀:量錯對象的檢查,綠得再乾脆也擋不住東西。

去開機看看,右下角點她一下。