翻頁書貼圖:如何在 Unity、Godot 與 VRChat 裡做精靈動畫
做一張自己的翻頁書貼圖
翻頁書貼圖就是在單一表面上,讓精靈表逐格播放。以下說明怎麼在 Unity、Godot 與 VRChat 裡做出一張翻頁書貼圖,並在 3D 裡即時翻動一張真正的精靈表。
翻頁書貼圖就是一張精靈表,由著色器在單一表面上逐格播放。它沒有骨架,也沒有時間軸:精靈表把每一幀放進固定的網格,材質再以固定速率把一個小視窗在網格上移動。8 x 8 的精靈表裝 64 幀,以每秒 12 幀播放,一輪大約 5.3 秒。網格本身不變,貼圖不需重新上傳,整個動畫只花一次貼圖取樣。
本頁涵蓋翻頁書著色器需要的四個數字,以及幀索引如何對應到 UV 座標;Unity 裡驅動翻頁書貼圖的兩種做法;Poiyomi 翻頁書在 VRChat 角色上怎麼設定;Godot 與 Phaser 裡的對應做法;還有怎麼從 GIF 或從一段文字描述做出自己的精靈表。
快速解答
翻頁書貼圖就是當作著色器動畫使用的精靈表,隨時間以列與欄索引。整張精靈表放在一個貼圖插槽裡;著色器每次求值會用經過的時間與幀率算出幀索引,把索引化約成一欄與一列,再把表面的 UV 重新映射到那個儲存格。frame = floor(time * fps) mod frameCount,column = frame mod columns,row = floor(frame / columns)。6 x 6 的精靈表以 10 FPS 播放,36 幀跑完是 3.6 秒,全部在 GPU 上完成,不需逐幀上傳,也不會改變繪製呼叫。
翻頁書貼圖的運作原理
動畫由四個數字定義。欄與列描述精靈表的版面。幀數通常等於欄乘列,也是索引繞回的基準。幀率是索引前進的速度。其餘的一切——濾波、環繞模式、UV 內縮——都是為了讓取樣器不要讀到目前儲存格以外的區域。
索引的數學很短,值得寫出來。給定經過的時間 t:frame = floor(t * fps) mod frameCount,column = frame mod columns,row = floor(frame / columns)。接著 UV 視窗的 tiling = (1 / columns, 1 / rows),offset = (column / columns, row / rows)。垂直方向是最容易讓人踩雷的地方。大多數精靈表製作工具把第 0 列寫在圖片頂端,而 Unity 與 Godot 的 UV 原點在左下角。在 Unity 裡用第一列在最上方的精靈表時,offset.y 要寫成 1 - (row + 1) / rows,否則列的播放順序會反過來,動畫的第一幀會變成精靈表的最後一列。
濾波決定成品乾不乾淨。精靈表的環繞模式設成 Clamp。用 Repeat 的話,貼圖右緣的取樣會讀到左緣,於是第 0 幀的一條像素會出現在每一列最後一幀裡面。硬邊像素圖要把 mipmap 關掉,因為 mip 鏈會把相鄰儲存格平均掉,表面只要離鏡頭幾公尺遠,動畫就糊成一團。如果目標引擎或著色器強制開啟 mipmap,就在每個儲存格四周留一、兩個像素的透明緩衝,或把 UV 視窗四邊各內縮半個 texel。
| 數值 | 控制什麼 |
|---|---|
| 欄 | 並排的幀數。決定單一儲存格的水平 tiling:1 / columns。 |
| 列 | 精靈表有幾列。決定垂直 tiling:1 / rows。單條動畫填 1。 |
| 幀數 | 索引的總幀數,通常等於 columns x rows。索引在這裡繞回。 |
| FPS | 索引前進的速度。floor(time * fps) 就是目前的幀。 |
| 環繞模式 | Clamp。Repeat 會讓取樣器讀過精靈表邊緣。 |
| 濾波模式 | Point,並關閉 mipmap;或把每個儲存格的 UV 內縮半個 texel。 |
Unity 裡的翻頁書貼圖
有兩條路,而它們出錯的方式不一樣。第一條是著色器自己持有列與欄索引並自我播放動畫。第二條是用 Animation 視窗對材質的 tiling 與 offset 打關鍵格,取樣則交給普通的 Lit 或 Unlit 著色器。效果必須跟遊戲時間同步時,走第一條;動畫得由 Animator 狀態驅動、而且要跟場景裡其他東西一起出現在時間軸上時,走第二條。
不論走哪條,先處理精靈表的匯入設定:Wrap Mode 設 Clamp、Mip Maps 關掉、Filter Mode 設 Point,像素圖不要開壓縮。這三個設定造成的翻頁書 bug,比著色器數學還多。
路線 1:在著色器裡處理列與欄索引
在 Shader Graph 裡,Flipbook 節點吃 Columns、Rows 與一個由時間驅動的幀索引,輸出重新映射後的 UV。手寫著色器的話,把 _Columns、_Rows、_Fps 開成屬性,在頂點或片段階段算出儲存格。如果動畫必須跨物件同步,幀索引要用腳本的 float 去推,不要讓每個材質各自取樣 _Time.y,否則同一個物件上的兩張翻頁書會愈跑愈開。
路線 2:用 Animation 視窗驅動 tiling 與 offset
這條路是對貼圖的 _ST 向量做動畫。內建 Standard 與 Unlit 用 _MainTex_ST;URP Lit 與 Unlit 用 _BaseMap_ST。Tiling 固定不動,設 (1, 1 / rows),只有 offset.y 在變。以下步驟就是走這條路。
01
選取材質,開啟 Animation 視窗。建立一個 clip,接著把貼圖的 _ST 向量屬性加成軌道——它在材質上會顯示為 Vector4。
02
把 clip 的取樣率設成跟目標 FPS 一樣的數字。取樣率低於你要的 FPS,關鍵格會塌到同一個取樣點上,動畫播起來比預期慢。
03
每一幀對 offset.y 打一次關鍵格:第一列在最上方的精靈表,值 = 1 - (row + 1) / rows,tiling = (1, 1 / rows)。
04
在曲線編輯器裡把每個關鍵格的插值設成 Constant。Linear 會把兩個儲存格混在一起,兩幀會同時出現,變成殘影。
05
確認最後一格沒有重複第一幀的值,否則動畫在循環接縫處會多停一幀。
06
如果多個 renderer 共用同一份材質,改用每個 renderer 各自的 MaterialPropertyBlock 做動畫,不要讀 renderer.material;第一次碰到它就會複製一份材質,而那份複製品會活到物件被銷毀為止。
VRChat 用的 Poiyomi 翻頁書
Poiyomi 著色器的 Flipbook 區塊會在材質上把 UV 視窗在精靈表上捲動,所以一個材質插槽就能讓角色網格顯示動態精靈。它開放 Rows、Columns 與幀率,另外還有一個模式讓你自行驅動幀索引,而不是任由著色器時間推進。
用單列的長條,不要用網格。長條只給著色器一個軸向可以前進,垂直接縫就整個消失。用網格的話,UV 視窗在每一列結束時必須往下跳一列,而那個邊界上任何濾波、mip 層級或半個 texel 的捨入誤差,都會取樣到隔壁那一列——播起來就是一條顏色不對的細帶。長條還讓幀索引變成單純的乘法:一幀在 UV 裡的寬度是 1 / frameCount,offset 就是 frame / frameCount。8 幀、每格 256 x 256 的動畫,長條是 2048 x 256,維持在 2 的次方。
在角色上,透明通道比在平面場景裡重要得多。精靈是硬邊的話,用 Cutout 或 Opaque。Transparent 佇列的翻頁書不寫深度,在角色身上會跟身體、也跟彼此排序,而排序結果會隨鏡頭角度改變。能的話,把動畫表面放在角色本來就有的材質上——每多一個材質插槽,都會算進角色的效能等級。如果幀索引是由帶同步參數的 Animator 狀態驅動,每個客戶端看到的都是同一幀;由著色器時間算出來的幀索引是在本機求值,不同觀眾可能停在循環裡不同的位置。
01
把每一幀匯出成一條橫向長條。長條高度等於儲存格高度;長條寬度等於幀數乘上儲存格寬度。最後不要多留一格。
02
匯入貼圖時 Wrap Mode 設 Clamp、Mip Maps 關掉、sRGB 開啟。只有在長條真的帶透明通道時,才勾 Alpha Is Transparency。
03
建立 Poiyomi 材質,指定給要動的網格,打開 Flipbook 區塊。
04
Rows 設 1,Columns 設成幀數。幀率設成你要的長度——8 幀搭 8 FPS 就是一秒一輪。
05
選擇幀的來源。Manual 模式會露出一個幀索引給你用 Animator 狀態驅動;由時間驅動的模式則自己循環。
06
在 Rendering 底下,硬邊精靈設 Cutout 或 Opaque。Transparent 只留給軟邊效果,而且要預期深度排序會怪怪的。
07
檢查循環:最後一幀不能是第一幀的複製品,否則每一輪精靈都會凍住一幀。
08
把角色上傳後用鏡子測試,近距離看一次,站到房間另一頭再看一次。距離正是 mip 與濾波問題現形的地方。
Godot 與 Phaser 裡的翻頁書
在 Godot 4 裡,2D 路線是 AnimatedSprite2D 搭配 SpriteFrames 資源。匯入精靈表,打開 SpriteFrames,把 Hframes 與 Vframes 設成欄數與列數,加入動畫的每一幀,然後把動畫速度設成每秒幀數,Loop 保持開啟。SpriteFrames 把動畫存成一串幀索引,所以網格精靈表跟單列長條都能用,你這邊完全不必碰 UV 數學。
只需要一個動畫的話,單純的 Sprite2D 更輕:Hframes 與 Vframes 設一次,然後用 AnimationPlayer 以 step 插值推進整數的 frame 屬性。不要用預設的緩動曲線去補間 frame——它是整數索引,補間下去不是被壓成同一個值,就是在兩個儲存格之間閃爍。
要在 Godot 裡把翻頁書貼到 3D 表面上,用 ShaderMaterial 搭配跟 Unity 版本一樣的列欄數學是最短路徑:uv = UV * vec2(1.0 / cols, 1.0 / rows) + vec2(col / cols, row / rows),列則由幀索引算出來。Godot 的 UV 原點也在左下角,跟 Unity 一樣,所以第一列在最上面的精靈表同樣要做垂直翻轉。
在 Phaser 3 裡,精靈表載入一次、載入時就切好。frameWidth 與 frameHeight 定義儲存格,幀號由左到右、由上到下,動畫用幀號辨識而不是用座標——沒有著色器要寫,也沒有列欄算術可以算錯。
| 引擎 | 物件 | 你要設什麼 |
|---|---|---|
| Godot 4 | AnimatedSprite2D + SpriteFrames | 把 Hframes 與 Vframes 設成精靈表版面,再設定動畫速度(FPS),Loop 保持開啟。 |
| Godot 4 | Sprite2D | Hframes 與 Vframes 設一次,再用 AnimationPlayer 以 step 插值推進整數的 frame 屬性。 |
| Godot 4 | 3D 網格上的 ShaderMaterial | 著色器裡的 columns、rows 與幀索引,UV 重映射與垂直翻轉跟 Unity 相同。 |
| Phaser 3 | this.load.spritesheet | frameWidth 與 frameHeight;Phaser 由左到右、由上到下把精靈表切開。 |
| Phaser 3 | this.anims.create | 用 anims.generateFrameNumbers 取幀,加上 frameRate 與 repeat: -1 做循環。 |
在 3D 裡即時翻動精靈表
本節的可玩場景是 The Cyclops' Island,跑在 Neta Studio 的 3D game builder 裡。畫面上的翻頁書就是前面講的索引對 UV 運算,在 GPU 上的 Three.js 著色器裡求值——差別在於它跑在真實場景裡的真實網格上,有你能操控的鏡頭,還有會動的燈光。
這件事比聽起來重要。平面 Unity 場景裡的翻頁書通常是正面看的,接縫滲色與 mip 混色很難看出來。把鏡頭繞到動畫表面的側面、以掠射角看,問題會立刻現形:幀邊緣出現隔壁儲存格的一條帶、最底層 mip 把兩幀平均成一團模糊,或是某條透明邊緣正面看沒問題、側面看就消失。瀏覽器版本把「做出一張精靈表」到「在表面上看到它」之間的循環縮到可以用不建置就反覆調整。
Mini World 是拿來測試單張精靈表的較小場景——網格數量少、載入時間短。還沒有精靈表的話,Agent Sprite Forge 可以生出每一幀,而兩個場景跑的是同一條翻頁書路徑,所以在 Mini World 裡看起來對的精靈表,在 The Cyclops' Island 裡行為也一樣。
做一張自己的精靈表
要弄出一張能用的精靈表,有三條路:轉換既有的 GIF、把網格裁成單列長條,或從一段文字描述生成幀。三條路都收斂到同一個檔案——一張 PNG,每個儲存格尺寸一致,由左到右、由上到下排好,精靈表寬度是儲存格寬度的整數倍。
轉換 GIF 是隱藏工作量最多的一條路,因為 GIF 不是一連串完整畫面。它的幀經常只是相對前一幀的局部差異,而延遲是以百分之一秒為單位、逐幀儲存的。翻頁書著色器只吃一種幀率,所以變動的時間在匯出前必須攤平成重複幀。
把網格裁成單列長條是機械性的工作:留下你要的那一列,高度裁成一個儲存格,寬度維持幀數乘儲存格寬度。當一張網格精靈表其實只當一個動畫用時,就該這樣做,因為它拿掉了垂直軸,也拿掉了隨之而來的接縫。從描述生成是第三條路——Agent Sprite Forge 吃一個動作與幀數,回傳固定儲存格尺寸的精靈表,省掉逐幀匯出、再手動對齊的手工活。
01
匯出前先把 GIF 的每一幀攤平。把每一幀解到畫布上,並把前一幀的內容帶到下一幀,否則你匯出的會是一堆差異矩形,而不是畫面。
02
把時間正規化。挑最短或中位數的 GIF 延遲,來源停得比較久的地方就複製幀,讓精靈表以單一固定 FPS 播放。
03
把每一幀由左到右、由上到下排好,每個儲存格尺寸完全相同。精靈表尺寸要是儲存格尺寸的精確倍數——寬度是 9.5 格的精靈表,會永遠取樣到錯的 texel。
04
要裁成單列長條的話,高度裁成一個儲存格,寬度維持幀數乘儲存格寬度。來源網格如果最後多留了一格空白,把它刪掉。
05
如果目標引擎不讓你關掉 mipmap,就在每個儲存格四周補一、兩個像素的透明緩衝。否則就把 UV 視窗四邊各內縮半個 texel。
06
要生成而不是轉換的話,把動作、幀數與面向寫清楚,讓 Agent Sprite Forge 回傳固定儲存格的精靈表,而不是逐幀匯出、再手動對齊。
07
驗證方式是在引擎裡一次推進一格幀索引。任何一格在邊緣露出一絲隔壁的內容,問題出在精靈表版面或 UV 內縮,不是著色器。
走進它所屬的那個世界
精靈是一個無處可站的角色。最後一步是別人沒做的那一步:把你剛做好的精靈表放進一個有人能走動的世界——一座小島、一顆行星、一個下雨的街角。這個網站上的每個世界都是從一段文字描述做出來的,發佈成一個可以直接玩的頁面。
常見問題
什麼是 Poiyomi 翻頁書?+
就是 Poiyomi 著色器的 Flipbook 區塊,它在材質上把 UV 視窗在精靈表上捲動,讓一個材質插槽就能在角色網格上顯示動態精靈。你設定 Rows、Columns 與幀率,然後看是讓幀索引由著色器時間推進,或是由 Animator 狀態驅動。建議用單列長條,因為它拿掉了列與列之間的垂直接縫。
GIF 可以直接當翻頁書用嗎?+
不行。GIF 存的是逐幀延遲,而且常常只存有變動的像素;翻頁書著色器要的是一張貼圖與一個固定幀率。得先轉換:把每一幀疊到前一幀上攤平、把時間正規化,再把結果寫進固定儲存格的網格。著色器從頭到尾看不到 GIF。
為什麼我的翻頁書貼圖會在幀之間滲色?+
取樣器讀到目前儲存格視窗以外的區域了。把貼圖環繞模式設成 Clamp,硬邊美術關掉 mipmap,並把 UV 視窗四邊各內縮半個 texel。距離一遠會更嚴重,因為那正是 mip 鏈開始把相鄰儲存格平均在一起的距離。
翻頁書該用多少幀率?+
8 到 12 FPS 讀起來像手繪動畫,適合角色精靈;24 到 30 適合火焰、水這類連續效果。著色器只有一種速率,所以來源時間有變動的話,就用複製幀對齊,不要想在循環中途改變速度。
一定要用網格嗎,單列夠不夠?+
任何以序列播放的東西,單列就夠,而且少掉一個軸向的接縫誤差。網格是給那種把好幾個動畫打包進同一張貼圖的精靈表用的,由著色器或引擎替 idle、run、attack 選一列。如果一張網格精靈表只產出一個動畫,就把它裁成單列長條。
翻頁書能搭配 Unity URP 的 Lit 著色器嗎?+
不能直接用——URP Lit 沒有幀索引。改用帶 Flipbook 節點的 Shader Graph,它吃 Columns、Rows 與由時間驅動的索引;或用 Constant 關鍵格插值去對材質的 _BaseMap_ST tiling 與 offset 做動畫。內建 Standard 與 Unlit 用 _MainTex_ST,規則相同。
回到 3D Game Builder
31 個完成的 3D 世界、遊戲與場景都在同一頁上——每一個都能玩。
打開精靈素材庫
這個素材庫裡的每一項素材,都是帶透明通道、可直接進遊戲的 PNG,並附上生成用的提示詞——精靈表資源與免費精靈表,依動作、風格與主題分類。可自由瀏覽與下載。