翻頁書紋理:如何在 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 播放,會在 3.6 秒內循環 36 個畫面,全部在 GPU 上完成,不需逐格上傳,也不會改變繪製呼叫。
翻頁書貼圖的運作方式
四個數值定義了這段動畫。Columns 與 rows 描述圖集版面。Frame count 通常是 columns 乘上 rows,也是索引繞回的數值。Frame rate 是索引前進的速度。其餘一切——篩選、環繞模式、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 的某一欄像素會出現在每一列最後一個畫面裡。像素風格強烈的美術請關閉 mipmaps,因為 mip 層級會平均相鄰儲存格,表面離鏡頭幾公尺遠之後動畫就會糊成一團。若目標引擎或著色器強制開啟 mipmaps,請在每個儲存格周圍留一或兩個像素的透明邊,或將 UV 視窗四邊各內縮半個 texel。
| 數值 | 控制項目 |
|---|---|
| Columns | 並排的畫面數。設定單一儲存格的水平 tiling:1 / columns。 |
| Rows | 圖集中的列數。設定垂直 tiling:1 / rows。條狀圖集請使用 1。 |
| Frame count | 索引的畫面總數,通常是 columns x rows。索引在此繞回。 |
| FPS | 索引前進的速度。floor(time * fps) 即得出目前影格。 |
| Wrap mode | Clamp。Repeat 會讓取樣器讀取到圖集的邊緣之外。 |
| Filter mode | Point,並關閉 mipmaps,或將每個儲存格的 UV 內縮半個 texel。 |
Unity 中的翻頁動畫貼圖
有兩條路線,而它們出錯的方式各不相同。路線一是一支自行持有列與欄索引並自動播放的 shader。路線二是由 Animation 視窗對材質的 tiling 與 offset 設定關鍵影格,再由單純的 Lit 或 Unlit shader 負責取樣。路線一較適合必須與遊戲時間保持同步的效果;路線二則較適合需要由 Animator 狀態驅動,且要與場景中其他項目一起在時間軸上顯示的動畫。
無論走哪條路線,都要先設定 sprite sheet 的匯入選項:Wrap Mode 設為 Clamp、Mip Maps 關閉、Filter Mode 設為 Point,且像素藝術不套用壓縮。這三項設定造成的翻頁動畫問題,比 shader 的數學運算還多。
路線 1:帶有列與欄索引的 shader
在 Shader Graph 中,Flipbook 節點接收 Columns、Rows 以及由時間驅動的影格索引,並輸出重新映射後的 UV。在手寫的 shader 中,將 _Columns、_Rows 與 _Fps 公開為屬性,並在 vertex 或 fragment 階段計算儲存格。若動畫必須在多個物件之間保持同步,請改由指令碼的 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 視窗。建立一個剪輯,然後將貼圖的 _ST 向量屬性新增為軌道——它會以 Vector4 的形式出現在材質上。
02
將剪輯的取樣率設為與目標 FPS 相同的數值。若取樣率低於你想要的 FPS,關鍵影格會塌縮到同一個取樣點上,動畫播放速度會比預期更慢。
03
每幀對 offset.y 打一次關鍵影格:若是第一列在上的圖集,值為 1 - (row + 1) / rows,tiling = (1, 1 / rows)。
04
在曲線編輯器中將每個關鍵影格的插值設為 Constant。Linear 會把兩個格子混合在一起,播放時會同時看到兩張影格而產生殘影。
05
確認最後一個關鍵影格不重複第一個影格的值,否則動畫在循環接縫處會多停留一個影格。
06
若多個 renderer 共用同一個 material,請改為逐一對每個 renderer 動畫 MaterialPropertyBlock,而不是讀取 renderer.material;後者在你第一次取用時就會複製該 material,且這個複製品會一直存在直到物件被銷毀。
VRChat 用 Poiyomi flipbook
Poiyomi 著色器的 Flipbook 區段會在 material 本身沿著圖集捲動一個 UV 視窗,因此單一 material 插槽就能讓虛擬人偶網格顯示動畫 sprite。它提供 Rows、Columns 與影格率,另有一種模式讓你自行驅動影格索引,而不是交由著色器時間推進。
請使用單列長條圖,而非格狀圖集。長條圖只給著色器一個軸向可步進,能完全消除垂直接縫。若用格狀圖集,UV 視窗在每一列結尾都必須往下移動一列,而邊界上任何濾波、mip 層級或半 texel 的捨入誤差都會取樣到相鄰列,播放時就會看到一條不同影格的細帶。長條圖也讓影格索引簡化成單純的乘法:影格在 UV 中的寬度是 1 / frameCount,偏移量就是 frame / frameCount。以 8 張 256 x 256 格子的動畫為例,長條圖為 2048 x 256,仍是 2 的次方。
Alpha 在虛擬人偶上比在平面場景中重要得多。若 sprite 邊緣銳利,請使用 Cutout 或 Opaque。Transparent 佇列的 flipbook 不寫入深度,用在虛擬人偶上時,會與身體以及彼此之間產生隨相機角度改變的排序結果。盡可能把動畫表面放在虛擬人偶既有的 material 上——每多一個 material 插槽都會計入你的虛擬人偶效能等級。若影格索引是由帶有同步參數的 Animator 狀態驅動,所有用戶端看到的都是同一張影格;由著色器時間計算出的影格索引則是在本機評估,不同檢視者可能停在循環中的不同位置。
01
將影格匯出成單一水平長條圖。長條圖高度等於格子高度;寬度等於影格數乘以格子寬度。不要在尾端留下多餘的格子。
02
匯入貼圖時使用 Wrap Mode Clamp、關閉 Mip Maps,並開啟 sRGB。只有在長條圖確實帶有 alpha 時才啟用 Alpha Is Transparency。
03
建立 Poiyomi material,指定給要動畫的網格,然後開啟 Flipbook 區段。
04
將 Rows 設為 1,Columns 設為影格數。設定影格率以符合你想要的長度——8 張影格、8 FPS 即為一秒的循環。
05
選擇影格來源。Manual 模式會提供一個影格索引,由 Animator 狀態驅動;時間驅動模式則自行循環。
06
在 Rendering 底下,為邊緣銳利的 sprite 設定 Cutout 或 Opaque。Transparent 僅保留給邊緣柔和的特效,並預期會遇到深度排序的異常狀況。
07
檢查循環:最後一張影格不得與第一張重複,否則 sprite 每次循環都會凍結一個影格。
08
在上傳的虛擬替身中以鏡子測試,分別從近距離和整個執行個體的對側觀察。距離正是 mip 與篩選問題浮現的地方。
Godot 與 Phaser 中的 Flipbook
在 Godot 4 中,2D 路徑是搭配 SpriteFrames 資源的 AnimatedSprite2D。匯入拼圓圖,開啟 SpriteFrames,將 Hframes 與 Vframes 設為行數與列數,為動畫加入影格,然後以每秒影格數設定動畫速度,並讓 Loop 保持開啟。SpriteFrames 會將動畫儲存為一連串影格索引,因此格狀拼圖與單一長條圖都適用,你這一側無須進行任何 UV 運算。
如果你只需要一段動畫,單純的 Sprite2D 較為輕量:設定一次 Hframes 與 Vframes,接著透過 AnimationPlayer 以 step 插值推進整數的 frame 屬性。請勿使用預設的緩動曲線補間 frame——該值是整數索引,對它插值不是向下取整為單一值,就是在兩個格子之間閃爍。
若要在 Godot 中於 3D 表面上製作 flipbook,使用與 Unity 版本相同行列運算的 ShaderMaterial 是最短路徑: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 | 在 shader 中使用欄、列與影格索引,並套用與 Unity 相同的 UV 重映射與垂直翻轉。 |
| Phaser 3 | this.load.spritesheet | frameWidth 與 frameHeight;Phaser 會由左至右、由上至下切分圖集。 |
| Phaser 3 | this.anims.create | 影格來自 anims.generateFrameNumbers,再加上 frameRate 與 repeat: -1 以循環播放。 |
在 3D 中即時翻轉 sprite sheet
本節的可玩場景是 The Cyclops' Island,在 Neta Studio 3D 遊戲建構工具中執行。畫面上的翻頁動畫就是上述同一套索引轉 UV 的運算,在 GPU 上的 Three.js shader 中求值——差別在於它跑在真實場景中的真實網格上,有你可操控的攝影機與會移動的燈光。
這一點比聽起來更重要。在扁平的 Unity 場景中,翻頁動畫通常是正面觀看,接縫滲色與 mip 混色很難察覺。讓攝影機以掠射角繞行動畫表面,問題立刻浮現:影格邊緣出現鄰格的一條帶狀區域、最後一層 mip 將兩個影格平均成模糊,或是 alpha 邊緣從正面看正確、從側面看卻消失。瀏覽器版本讓「製作圖集」與「在表面上看到結果」之間的循環短到不必經過建置步驟就能反覆調整。
Mini World 是較小的場景,用來測試單一圖集——網格數量更少、載入更快。如果你還沒有圖集,Agent Sprite Forge 可以產生影格;兩個場景走的是同一條翻頁動畫路徑,因此在 Mini World 看起來正確的圖集,在 The Cyclops' Island 的表現也相同。
製作自己的圖集
要取得可用的圖集有三條路徑:轉換既有的 GIF、把網格裁成單一橫列,或依文字描述產生影格。三者最後都導向同一個檔案——一張 PNG,每個 cell 大小一致,由左至右、由上至下排列,且圖集寬度為 cell 寬度的整數倍。
轉換 GIF 是隱藏工作量最多的路徑,因為 GIF 並不是一連串完整影格。影格經常只是相對於前一格的局部差異,而延遲是以百分之一秒為單位逐格儲存。翻頁動畫 shader 只接受單一影格率,因此匯出前必須把變動的時間攤平成重複影格。
把網格裁成橫列是機械性的操作:保留你需要的那一列,將高度裁成單一 cell,寬度則維持為影格數乘以 cell 寬度。當網格圖集要當作單一動畫使用時,這是最恰當的做法,因為它移除了垂直軸以及隨之而來的接縫。依描述產生是第三條路徑——Agent Sprite Forge 接受一個動作與影格數,回傳固定 cell 大小的圖集,省去逐一匯出影格後再手動對齊的比對工作。
01
匯出前先將每個 GIF 影格疊合。把每個影格解碼到會承接前一幀的畫布上,否則你匯出的會是差異矩形,而不是完整畫面。
02
標準化時間。選擇最短或中位數的 GIF 延遲,然後在來源影格停留較久之處複製影格,讓圖集以單一固定 FPS 播放。
03
將影格由左至右、由上至下排列,每個格子大小完全相同。讓圖集尺寸是格子尺寸的整數倍——寬度為 9.5 個格子的圖集會永遠取樣到錯誤的 texel。
04
若要裁切成條狀,將高度裁成一個格子,並將寬度維持在影格數乘以格子寬度。如果來源網格有尾端空格,請刪除它。
05
如果目標引擎不允許你停用 mipmap,請在每個格子周圍加上一或兩個像素的透明間隙。否則將 UV 視窗四邊各內縮半個 texel。
06
若要產生而不是轉換,請寫下動作、影格數與面向,讓 Agent Sprite Forge 回傳固定格子的圖集,而不是一次匯出一個影格再手動對齊。
07
在引擎中一次一格地推進影格索引來驗證。如果任何格子的邊緣出現鄰格的細條,問題出在圖集版面或 UV 內縮,而不是 shader。
走進它所屬的世界
sprite 是一個無處可站的角色。最後一步是其他人沒有提供的那一步:把你剛建立好的圖集放進一個有人可以走動的世界——一座小島、一顆行星、一個下雨的街角。這個網站上的每個世界,都是從一段文字描述製成,並發布成一個可以遊玩的頁面。
常見問題
什麼是 Poiyomi flipbook?+
它是 Poiyomi shader 的 Flipbook 區段,會在材質上讓 UV 視窗捲動過 sprite 圖集,使單一材質插槽能在虛擬角色網格上顯示動畫 sprite。你設定 Rows、Columns 與影格率,並讓影格索引依 shader 時間推進,或是由 Animator 狀態驅動。偏好使用單列條狀圖,因為它移除了列與列之間的垂直接縫。
可以直接把 GIF 做成 flipbook 嗎?+
不行。GIF 會儲存每個影格的延遲,而且通常只儲存有變動的像素,而 flipbook shader 預期的是單一紋理與單一固定影格率。請先轉換:將每個影格疊合到前一幀、標準化時間,並把結果寫入固定格子網格。shader 永遠不會看到 GIF。
為什麼我的 flipbook 貼圖會在影格之間滲色? +
取樣器讀到了目前格子範圍之外。請把貼圖的環繞模式設為 Clamp,硬邊風格的美術請關閉 mipmap,並將 UV 範圍每邊內縮半個 texel。距離越遠,滲色越嚴重,因為那正是 mip 鏈開始把相鄰格子平均在一起的地方。
flipbook 該用多少影格率? +
8 到 12 FPS 讀起來像手繪動畫,適合角色 sprite;24 到 30 適合火焰、水流等持續性特效。著色器只有一種速率,所以如果來源素材的節奏不固定,請複製影格來對齊,而不是試著在播放途中改變速度。
我需要用網格,還是只有一列就夠了? +
只要是一段連續播放的動畫,一列就足夠,而且能少掉一個軸向的接縫誤差。網格是給把多段動畫打包進同一張貼圖的圖集用的,由著色器或引擎依 idle、run 或 attack 選擇對應的列。如果一張網格圖集最後只輸出一段動畫,就把它裁成一條。
flipbook 能搭配 Unity URP 的 Lit 著色器使用嗎? +
預設無法直接使用 —— URP Lit 沒有影格索引。請改用 Shader Graph 的 Flipbook 節點,它接受 Columns、Rows 與由時間驅動的索引;或者用定值關鍵影格插值來控制材質 _BaseMap_ST 的 tiling 與 offset。Built-in 的 Standard 與 Unlit 則依相同規則使用 _MainTex_ST。
返回 3D 遊戲建立器
全部 31 個完成的 3D 世界、遊戲與場景都在同一頁 —— 每一個都能實際遊玩。
開啟 sprite 素材庫
此資源庫中的每個素材都是透明、可直接用於遊戲的 PNG,並附有已記錄的提示詞——涵蓋 Sprite sheet resources 與 Free sprite sheets,並依動作、風格與主題加上標籤。可自由瀏覽與下載。