跳转到内容

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

除非另有说明,否则本页上的所有示例源代码均归属公共领域。
华夏公益教科书