← Back to list

Unity筆刷技術的突破與實踐洞察:從SetPixels到ComputeShader

在 Unity 開發筆刷系統的過程中,我發現網路上對於「筆刷原理」的深度討論相當稀少。許多開發者習慣直接使用引擎內建工具,很少去探究背後的底層機制。事實上,唯有真正掌握筆刷的運作原理,才能突破現有工具的限制,打造完全符合自身需求的自訂筆刷。這也是我撰寫這篇文章的初衷 — —…

AkilarLiao · 2025-09-01 04:42 · 1 claps · 8.8 min read
#unity #game-development #shaders #gpu #compute-shader
Open on Medium ↗
Wiki topics: OPS · LLMOps & Inference 🎮 · Gaming

Unity筆刷技術的突破與實踐洞察:從SetPixels到ComputeShader

在 Unity 開發筆刷系統的過程中,我發現網路上對於「筆刷原理」的深度討論相當稀少。許多開發者習慣直接使用引擎內建工具,很少去探究背後的底層機制。事實上,唯有真正掌握筆刷的運作原理,才能突破現有工具的限制,打造完全符合自身需求的自訂筆刷。這也是我撰寫這篇文章的初衷 — — 希望分享我的技術探索與實踐心得,幫助更多人理解並實現屬於自己的筆刷系統。

SetPixels32筆刷:從CPU搬資料到GPU

先從最簡單的SetPixels32說起。相信有做過戰爭迷霧經驗的人應該對他很熟悉,或許有些人會認為使用Graphics.CopyTexture效能更好,但因為筆刷通常需要支援任意大小,這類方法其實並不適合。

下面這段程式碼,就是典型的 SetPixels32 實作:遍歷筆刷覆蓋範圍內的每個像素,根據距離與權重計算顏色,最後一次性 Apply 到 GPU。

var destColor = new Color32(0, 0, 0, 0);
for (int rowIndex = startRowIndex; rowIndex<=endRowIndex;
    ++rowIndex)
{
    for (int columnIndex = startColumnIndex;
        columnIndex<=endColumnIndex; ++columnIndex)
    {
        Vector2 gridCenter = new Vector2(
            worldMin.x + columnIndex * gridSize.x+
            halfGridSize.x,
            worldMin.y + rowIndex * gridSize.y+
            halfGridSize.y);

        int index = columnIndex + rowIndex * mapWidth;
        float sqrDistance = (brushCenter - gridCenter).
            sqrMagnitude;

        sqrDistance = Mathf.Clamp(sqrDistance, sqrhalfBrushInnerSize, sqrhalfBrushSize);
        float weightRatio = 1.0f - (sqrDistance - sqrhalfBrushInnerSize) / 
            (sqrhalfBrushSize - sqrhalfBrushInnerSize);

        destColor = editColorBuffer[index];

        ProcessPixelColor(weightRatio, speed * 0.033f, isAdd, sqrDistance, sqrhalfBrushSize,
            in mouseDelta, ref destColor);

        editColorBuffer[index] = destColor;
    }
}
editTexture.SetPixels32(editColorBuffer);
editTexture.Apply();

這種做法的原理很簡單:就是用 CPU 逐像素處理整個筆刷區塊,處理完後再一次把整張貼圖資料送回 GPU。這會造成大量的 CPU → GPU 資料傳輸,貼圖越大或同時處理多張時,效能瓶頸就會非常明顯。對於需要即時回饋、手感細膩的筆刷操作來說,這種方法往往無法滿足需求。

Fragment Shader 筆刷:運算搬到 GPU

在瞭解 SetPixels32 的效能瓶頸後,另一個筆刷優化方式,就是將像素處理的工作從 CPU 搬到 GPU,也就是利用 Fragment Shader 來實現筆刷效果,這樣可以大幅減輕 CPU 傳資料到GPU的負擔,讓每個像素的運算都由 GPU 高效並行處理。

下面這段 Shader 代碼,就是將筆刷範圍內像素的權重與顏色計算,全部交由 Fragment Shader 處理:

bool GetBltTextureColorAndBrushWeight(in VertexOutput input, 
    out half brushWeight, out half4 bltTextureColor)
{
    half2 dsetUV = input.screenPosition.xy / input.screenPosition.w;
    bltTextureColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex,
        input.screenPosition.xy / input.screenPosition.w);
    uint rowIndex = _TextureArrayIndex / 2;
    uint columnIndex = _TextureArrayIndex % 2;

    float2 wordPosition2D = float2((columnIndex + dsetUV.x) * _BrushInfo.z,
        (rowIndex + dsetUV.y) * _BrushInfo.w);

    float distance = length(wordPosition2D - _BrushTransform.xy);
    if (distance > _BrushTransform.w)
        return false;

    distance = clamp(distance, _BrushTransform.z, _BrushTransform.w);
    brushWeight = (1.0 - (distance - _BrushTransform.z) / _BrushInfo.y) * 
        unity_DeltaTime.z * _BrushInfo.x * sc_weightApplyRatio;
    return true;
}

這段程式主要做了兩件事:

  • 根據螢幕座標計算當前像素在世界空間的位置,進而判斷它是否落在筆刷半徑範圍內。
  • 若在範圍內,計算權重(brushWeight),用於決定筆刷對該像素的影響程度。

優點

  • 完全在 GPU 處理:不再需要將像素資料從 CPU 傳回 GPU,效能大幅提升,特別適合高解析度或多重筆刷操作。
  • 即時反應:能提供更即時、更細膩的筆刷手感。

缺點

  • 需要遍歷整張 RenderTexture:即使筆刷只覆蓋一小塊範圍,Fragment Shader 仍然會處理整張貼圖,造成不必要的運算浪費。
  • Input/Output RenderTarget 限制:在處理時,輸入和輸出 RenderTarget 不能是同一張貼圖,所以每次繪製都必須先將原本的內容拷貝(Blt)到另一張 RenderTexture,再進行處理,這會增加一次額外的 GPU Pass。
  • 多重目標更複雜:如果要同時對 TextureArray 進行筆刷操作,管理和效率問題會更加明顯。

PS:Unity官方的Terrain的筆刷也是採用這種方式,可以參考這個連結:

[embed][com.unity.terrain-tools/Shaders/PaintTexture.shader at master ·… Mirrored from UPM, not affiliated with Unity Technologies.] 📦 The Terrain Tools package adds additional Terrain…github.com](https://github.com/needle-mirror/com.unity.terrain-tools/blob/master/Shaders/PaintTexture.shader)

Compute Shader 筆刷:OnePass 高效處理,TextureArray 也難不倒

在理解了 Fragment Shader 在 Input/Output RenderTarget 上的限制後,我們進一步發揮 Compute Shader 的特性,有效解決了這些瓶頸:

  1. Input 可以等於 Output 直接在同一張 RenderTexture 上進行讀寫,無需額外 Blit 拷貝,大幅減少資源消耗與延遲。
  2. 只需處理筆刷範圍像素 不必遍歷整張 RenderTexture,而是精準針對筆刷影響範圍進行運算,效能提升顯著。
  3. 完整支援 TextureArray 可以直接對 TextureArray 進行讀寫,輕鬆應對多層貼圖的高階需求,彈性與擴展性更高。

這些優勢讓 Compute Shader 筆刷在效能與功能上,都遠超傳統 Fragment Shader 筆刷,實現真正的 OnePass 高效繪製。

以下這段 Compute Shader 代碼正好展現了這些特點:

  • Input 就是 Output:直接在目標貼圖上進行讀寫。
  • 只處理局部範圍:僅針對筆刷覆蓋的 Rect 區域進行運算。
[numthreads(8, 8, 1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{   
    if ((id.x >= _BrushPixelsColumnCount) ||
        (id.y >= _BrushPixelsRowCount))
        return;

    TextureIndex textureIndex;
    half brushWeight;    
    if (!GetDestPixelWeight(id.xy, textureIndex, brushWeight))
        return;

    SetEditTextureValue(textureIndex, half4(_TintColor,
        saturate(GetEditTextureValue(textureIndex).a + brushWeight)));
}

如上所示,Compute Shader 只針對筆刷影響範圍(由 _BrushPixelsColumnCount_BrushPixelsRowCount 控制)進行運算,並且直接在目標貼圖(可能是 TextureArray)上讀寫,充分發揮 OnePass 與高效的優勢。其中,TextureIndex 指的是 TextureArray 的索引。

這邊再整理一下三個方案比較:

SetPixels32 筆刷

  • 優點:實作簡單,容易理解。
  • 缺點:大量 CPU→GPU 資料傳輸,效能瓶頸明顯。

Fragment Shader 筆刷

  • 優點:運算全部交給 GPU,效能提升,即時反應佳。
  • 缺點:必須遍歷整張貼圖,運算浪費;Input/Output RenderTarget 有限制。

Compute Shader 筆刷

  • 優點:只處理筆刷影響範圍,Input 就是 Output,支援 TextureArray,效能與彈性最佳。
  • 缺點:實作較複雜,需要理解 GPU 並行運算。

在這三種方案的比較中,Compute Shader 筆刷展現的並不只是效能上的優勢,而是一種更 乾淨、直接、優雅 的解法。 它讓筆刷不再是資料搬運與反覆拷貝,而是 GPU 在畫布上即時創作的過程。

結論:這樣的轉變,帶來的不是單純的「更快」,而是一種全新的體驗:

  • 美術能感受到筆刷如同真實畫筆般的即時回饋
  • 工程師能以更簡潔的邏輯,實現複雜的筆刷需求
  • 專案開發則獲得一個能應對大場景、多層貼圖的高效解法

메타데이터
post_id
09be5a2b185c
slug
unity筆刷技術的突破與實踐洞察-從setpixels到computeshader-09be5a2b185c
url
https://medium.com/@pioneering_catawba_cheetah_996/unity%E7%AD%86%E5%88%B7%E6%8A%80%E8%A1%93%E7%9A%84%E7%AA%81%E7%A0%B4%E8%88%87%E5%AF%A6%E8%B8%90%E6%B4%9E%E5%AF%9F-%E5%BE%9Esetpixels%E5%88%B0computeshader-09be5a2b185c
canonical_url
https://medium.com/@pioneering_catawba_cheetah_996/unity%E7%AD%86%E5%88%B7%E6%8A%80%E8%A1%93%E7%9A%84%E7%AA%81%E7%A0%B4%E8%88%87%E5%AF%A6%E8%B8%90%E6%B4%9E%E5%AF%9F-%E5%BE%9Esetpixels%E5%88%B0computeshader-09be5a2b185c
author_url
https://medium.com/@pioneering_catawba_cheetah_996
status
ok
fetched_at
2026-06-26 08:21:59