跳轉到內容

Cg 程式設計/Unity/計算顏色直方圖

來自 Wikibooks,開放世界中的開放書籍
一張貓的圖片。
上面貓的圖片的顏色直方圖,橫軸表示 RGB 值,縱軸表示這些值的頻率。

本教程展示瞭如何使用 Unity 中的計算著色器來計算影像的顏色直方圖。特別是,它展示瞭如何使用 原子 函式,這樣多個執行緒(即對計算著色器函式的多次呼叫)就可以訪問相同的記憶體位置。它還展示瞭如何使用計算緩衝區。如果您不熟悉 Unity 中的計算著色器,您應該先閱讀 “計算影像效果”部分。請注意,計算著色器在 macOS 上不受支援。

通用顏色直方圖計算

[編輯 | 編輯原始碼]

影像的 RGB 顏色直方圖 是一個條形圖,它顯示了對於紅色、綠色和藍色通道的每個值,影像中有多少畫素具有該值。例如,有多少畫素的紅色值為 0,有多少畫素的綠色值為 0,等等。對於 8 位的顏色解析度,紅色、綠色和藍色通道有 256 個可能的值(0 到 255);因此,RGB 顏色直方圖指定了 3 × 256 = 768 個數字。如果也包含 alpha 通道,則 RGBA 顏色直方圖包含 4 × 256 = 1024 個數字。

要計算這樣的 RGBA 顏色直方圖,程式首先將直方圖的 1024 個數字初始化為 0。然後它檢視影像的每個畫素,並根據畫素的特定紅色、綠色、藍色和 alpha 值將直方圖中的四個數字(加 1)遞增。由於對每個畫素都執行相同的操作,因此這個問題很容易並行化,除非兩個不同畫素的兩個不同執行緒可能嘗試在同一時間遞增直方圖的同一個數字,這會導致稱為 競爭條件 的問題。如果遞增直方圖中某個數字的操作是 原子 操作,即如果它不能被其他執行緒中斷,則可以避免這些問題。這就是我們在本教程的計算著色器中使用的內容。

總體概述:呼叫計算著色器

[編輯 | 編輯原始碼]

在本教程中,我們從呼叫計算著色器的 C# 指令碼開始,因為它提供了更大的畫面。請注意,我們計算任何紋理影像的顏色直方圖;不僅針對像 “計算影像效果”部分 中那樣針對相機檢視。因此,您可以將此指令碼附加到任何 GameObject

using UnityEngine;

public class histogramScript : MonoBehaviour {

   public ComputeShader shader;
   public Texture2D inputTexture;   
   public uint[] histogramData;
   
   ComputeBuffer histogramBuffer;
   int handleMain;
   int handleInitialize;
   
   void Start () 
   {
      if (null == shader || null == inputTexture) 
      {
         Debug.Log("Shader or input texture missing.");
         return;
      }
         
      handleInitialize = shader.FindKernel("HistogramInitialize");
      handleMain = shader.FindKernel("HistogramMain");
      histogramBuffer = new ComputeBuffer(256, sizeof(uint) * 4);
      histogramData = new uint[256 * 4];
      
      if (handleInitialize < 0 || handleMain < 0 || 
         null == histogramBuffer || null == histogramData) 
      {
         Debug.Log("Initialization failed.");
         return;
      }

      shader.SetTexture(handleMain, "InputTexture", inputTexture);
      shader.SetBuffer(handleMain, "HistogramBuffer", histogramBuffer);
      shader.SetBuffer(handleInitialize, "HistogramBuffer", histogramBuffer);
   }
	
   void OnDestroy() 
   {
      if (null != histogramBuffer) 
      {
         histogramBuffer.Release();
         histogramBuffer = null;
      }
   }   
   
   void Update()
   {
      if (null == shader || null == inputTexture || 
         0 > handleInitialize || 0 > handleMain ||
         null == histogramBuffer || null == histogramData) 
      {
         Debug.Log("Cannot compute histogram");
         return;
      }
         
      shader.Dispatch(handleInitialize, 256 / 64, 1, 1);
         // divided by 64 in x because of [numthreads(64,1,1)] in the compute shader code
      shader.Dispatch(handleMain, (inputTexture.width + 7) / 8, (inputTexture.height + 7) / 8, 1);
         // divided by 8 in x and y because of [numthreads(8,8,1)] in the compute shader code
        
      histogramBuffer.GetData(histogramData);
   }
}

該指令碼定義了三個公共變數:public ComputeShader shader,它必須設定為下面顯示的計算著色器;public Texture2D inputTexture,它必須設定為要計算直方圖的紋理;以及 public uint[] histogramData,指令碼將其設定為一個包含 1024 個無符號整數的計算直方圖陣列。

三個私有變數是:ComputeBuffer histogramBuffer,它包含與 histogramData 相同的資料,但可以被計算著色器訪問;int handleMainint handleInitialize 是兩個計算著色器函式的索引,用於處理所有畫素的主處理和 1024 個直方圖數字的初始化。

Start() 函式使用 ComputeShader.FindKernel() 設定兩個控制代碼,並建立 histogramBuffer 計算緩衝區和 histogramData 陣列。雖然計算緩衝區是作為包含 256 個元素的陣列建立的,每個元素包含 4 個無符號整數,但 histogramData 是作為包含 1024 個無符號整數的陣列建立的。這個差異並不重要,因為這兩個陣列的記憶體佈局是相同的。當然,histogramData 也可以定義為包含 256 個結構的陣列,每個結構包含 4 個無符號整數。Start() 函式的其餘部分執行錯誤檢查,並將紋理和計算緩衝區設定為每個計算著色器函式的相應統一變數,以便它們可以訪問它們。

OnDestroy() 函式只是釋放計算緩衝區,因為與之相關的硬體資源不會被垃圾回收器自動釋放。

Update() 函式執行一些錯誤檢查,然後呼叫計算著色器函式來初始化 histogramBuffer 和計算著色器函式來處理所有畫素。對於初始化,我們使用 4 (= 256 / 64) 個 64 × 1 × 1 執行緒的執行緒組來初始化計算緩衝區的 256 個元素。對於畫素的主處理,我們使用 8 × 8 × 1 執行緒的執行緒組,並透過將紋理影像的尺寸除以 8 來計算執行緒組的數量。新增 7 是為了確保如果尺寸不能被 8 整除,我們不會缺少一個執行緒組。最後,Update() 函式呼叫 histogramBuffer.GetData(histogramData); 將資料從計算緩衝區複製到 histogramData 中的 Unity 陣列;請注意,這兩個資料結構必須具有相同的記憶體佈局,此呼叫才能正常工作。

在每一幀結束時,計算出的顏色直方圖將可用於公共變數 histogramData 中;因此,您可以在執行程式時在Inspector Window中檢視它。

計算著色器細節

[編輯 | 編輯原始碼]

在這種情況下,計算著色器包含兩個計算著色器函式,一個用於初始化,另一個用於處理紋理的紋素的主處理。因此,它還包含兩個 #pragma kernel 指令,以及兩個 [numthreads()] 指令。

#pragma kernel HistogramInitialize
#pragma kernel HistogramMain

Texture2D<float4> InputTexture; // input texture

struct histStruct {
   uint4 color;
};
RWStructuredBuffer<histStruct> HistogramBuffer;

[numthreads(64,1,1)]
void HistogramInitialize(uint3 id : SV_DispatchThreadID) 
{
   HistogramBuffer[id.x].color = uint4(0, 0, 0, 0);
}

[numthreads(8,8,1)]
void HistogramMain (uint3 id : SV_DispatchThreadID) 
{
   uint4 col = uint4(255.0 * InputTexture[id.xy]);

   InterlockedAdd(HistogramBuffer[col.r].color.r, 1);
   InterlockedAdd(HistogramBuffer[col.g].color.g, 1); 
   InterlockedAdd(HistogramBuffer[col.b].color.b, 1); 
   InterlockedAdd(HistogramBuffer[col.a].color.a, 1); 
}

與往常一樣,您可以透過在Project Window中點選Create,然後選擇Shader > Compute Shader來建立一個計算著色器。然後,您應該將程式碼複製並貼上到新檔案中。

前兩行 #pragma kernel HistogramInitialize#pragma kernel HistogramMain 指定了兩個可以從指令碼中使用 ComputeShader.Dispatch() 函式呼叫的計算著色器函式(“核心”)。

Texture2D<float4> InputTexture; 指定了一個名為 InputTexture 的只讀 2D RGBA 紋理的統一變數。

struct histStruct { uint4 color; }; 定義了一個只有 一個成員的小結構:一個名為 color 的 4D 無符號整數向量。color.r 用於計算具有特定值的紅色畫素(根據陣列中的位置);類似地,color.gcolor.bcolor.a 分別用於綠色、藍色和 alpha 通道。

然後,結構 histStruct 用於 RWStructuredBuffer<histStruct> HistogramBuffer; 來定義一個讀寫結構化緩衝區,該緩衝區表示 C# 指令碼中的計算緩衝區 histogramBuffer。記憶體佈局匹配,因為 RWStructuredBuffer 的元素型別為 histStruct,它包含 4 個無符號整數。

函式 HistogramInitialize() 使用 64 × 1 × 1 尺寸的執行緒組,這意味著引數 uint3 id : SV_DispatchThreadIDuint3(0, 0, 0) 執行到 uint3(255, 0, 0),因為我們使用 4 個執行緒組。因此,該函式可以使用 id.x 在初始化所有元素為 0 時索引 HistogramBuffer 的 256 個元素。

函式 HistogramMain() 使用 8 × 8 × 1 尺寸的執行緒組。因為我們根據紋理大小設定執行緒組的數量,所以該函式可以使用引數 uint3 id : SV_DispatchThreadID 透過 InputTexture[id.xy] 訪問紋理的紋素。因為 RGBA 值被讀取為 0.0 到 1.0 之間的浮點值,所以它們乘以 255.0 並透過將它們轉換為 uint4 col 變數中的無符號整數而向下取整。然後,col 中的 RGBA 值用於索引 HistogramBuffer 以遞增緩衝區中的計數器變數,即 HistogramBuffer[col.r].color.r 用於紅色值,HistogramBuffer[col.g].color.g 用於綠色值,等等。

為了遞增計數器變數,程式碼使用了函式 InterlockedAdd(),該函式將變數作為第一個引數,將整數作為第二個引數。在我們的案例中,第二個引數為 1,因為我們遞增了 1。InterlockedAdd() 是 HLSL 計算著色器中的原子函式之一;也就是說,GPU 確保由於多個執行緒嘗試在同一時間遞增同一個變數而產生的任何競爭條件都被避免。HLSL 中有幾個 原子函式;請注意,它們都只適用於整數或無符號整數。

如果您想觀察競爭條件的影響,您可以用類似以下程式碼替換對原子函式 InterlockedAdd() 的呼叫

   HistogramBuffer[col.r].color.r += 1; 
   // WARNING: THIS CREATES RACE CONDITIONS!

在大多數 GPU 上,這將不是一個原子操作,因此,當您執行此程式碼時通常會出現競態條件,從而導致結果不確定。您可能能夠在 **檢查器視窗** 中觀察到 histogramData 陣列中的值由於這些競態條件而發生了一些隨機變化。

您已經完成了本教程!您所學到的一些內容包括

  • 什麼是顏色直方圖以及如何計算它們。
  • 如何在 C# 指令碼中建立和使用 Unity 的計算緩衝區,以及如何在計算著色器中定義相應的讀/寫結構化緩衝區。
  • 如何在同一個計算著色器中定義和使用多個計算著色器函式。
  • 如何在計算著色器中使用原子函式。

進一步閱讀

[編輯 | 編輯原始碼]

如果您想了解更多

< Cg Programming/Unity

除非另有說明,否則本頁上的所有示例原始碼均歸屬公共領域。
華夏公益教科書