《Unity着色器圣经》10.0.4 | Buffers.

目录索引

译文

在某些情况下,需要同时处理多个数据,例如粒子开发、后处理、光线跟踪功能、模拟等。它们的特点是计算单元产生大量的图形负载。然而,对我们有利的是,我们可以在程序中使用两种相关的数据类型来加快向内存缓冲区读取和写入值的速度:ComputeBuffer和StructuredBuffer。

正如它的名字所提到的,ComputeBuffer对应于一个缓冲区,我们可以从C#脚本中创建并填充一个值列表。

StructuredBuffer本质上是相同的,只是我们在计算着色器中声明了它。

// ------------ C#
struct Properties
{
    Vector3 vertices;
    Vector3 normals;
    Vector4 tangents;
}
Properties[] m_meshProp;
ComputeBuffer m_meshBuffer;

// ------------ Compute Shader
struct Properties
{
    float3 vertices;
    float3 normals;
    float4 tangents;
};
StructuredBuffer<Properties> meshProp;

我们将在我们的项目中创建一个新的计算着色器来了解其实现,我们将其称为USB_Compute_buffer。同样,我们将创建一个新的C#脚本,称为USBComputeBuffer。

之前,在第4.1.6节中,我们创建了一个名为“circle”的简单方法,用于在四边形中复制圆。在本节中,我们将执行相同的练习,不同之处在于我们将使用以前为此目的创建的脚本。

在研究了Unity中的部分计算着色器集成后,我们可以推断USBComputeBuffer将通过SetFloat和SetTexture函数处理数据配置。

同样,我们将配置稍后将通过ComputeBuffer.SetBuffer函数发送到计算着色器中预定义值列表的数据。

我们将首先声明与第4.1.6节中效果相关的公共变量。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class USBComputeBuffer : MonoBehaviour
{
    public ComputeShader m_shader;

    [Range(0.0f, 0.5f)] public float m_radius = 0.5f;
    [Range(0.0f, 1.0f)] public float m_center = 0.5f;
    [Range(0.0f, 0.5f)] public float m_smooth = 0.01f;
    public Color m_mainColor = new Color();

    private RenderTexture m_mainTex;
    private int m_texSize = 128;
    private Renderer m_rend;
    ...
}

如果我们注意上面的例子,我们会注意到已经定义了与我们之前用于生成圆相同的属性(m_radius、m_center和m_smooth)。此外,还为其创建了一个颜色属性。

这些变量可以使用ComputeShader单独发送到计算着色器。SetFloat函数或创建一个缓冲区,其中包含我们要分配的值的完整列表。

对于练习,我们声明一个结构和一个缓冲区,该缓冲区与我们将在着色器中使用的值列表相关联。

public class USBComputeBuffer : MonoBehaviour
{
    public ComputeShader m_shader;

    [Range(0.0f, 0.5f)] public float m_radius = 0.5f;
    [Range(0.0f, 1.0f)] public float m_center = 0.5f;
    [Range(0.0f, 0.5f)] public float m_smooth = 0.01f;
    public Color m_mainColor = new Color();

    private RenderTexture m_mainTex;
    private int m_texSize = 128;
    private Renderer m_rend;

    // declare a struct with the list of values
    struct Circle
    {
        public float radius;
        public float center;
        public float smooth;
    }

    // declare an array type Circle to access each variable
    Circle[] m_circle;

    // declare a buffer of type ComputeBuffer
    ComputeBuffer m_buffer;
    ...
}

在“structCircle”中,我们已经声明了稍后将在ComputeShader中使用的变量;通过“m_circle”,我们将访问每个实例。

考虑到练习的性质,我们可以推断全局变量的值将被分配给结构中定义的值。此时,ComputeBuffer变得相关,因为一旦程序用值填充了列表,我们就必须将数据复制到缓冲区,并最终将其传递给着色器。

在执行这样的过程之前,我们将首先创建纹理。为此,我们声明了一个新方法,称之为“CreateShaderEx”。该方法将包含第10.0.2节中描述的纹理定义算法。

void Start()
{
    CreateShaderTex();
}

void CreateShaderTex()
{
    // first, we create the texture
    m_mainTex = new RenderTexture(m_texSize, m_texSize, 0, RenderTextureFormat.ARGB32);
    m_mainTex.enableRandomWrite = true;
    m_mainTex.Create();

    // then we access to the mesh renderer
    m_rend = GetComponent<Renderer>();
    m_rend.enabled = true;
}

接下来,我们声明一个新函数,我们将在“Update”方法中使用该函数,仅用于研究目的。

void Update()
{
    SetShaderTex();
}

void SetShaderTex()
{
    // write the code here...
}

在继续之前,我们将转到USB_compute_buffer,因为在从USBComputeBuffer获取数据之前,我们必须配置其结构。我们将添加“circle”函数,然后在CSMain内核中定义其值。

#pragma kernel CSMain
RWTexture2D<float4> Result;

// declare de method
float CircleShape (float2 p, float center, float radius, float smooth)
{
    float c = length(p - center);
    return smoothstep(c - smooth, c + smooth, radius);
}

[numthreads(128, 1, 1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    uint width;
    uint height;
    Result.GetDimensions(width, height);
    float2 uv = float2((id.xy + 0.5) / float2(width, height));
    // initialize the values to zero
    float c = CircleShape(uv, 0, 0, 0);

    Result[id.xy] = float4(c, c, c, 1);
}


在上一个练习中,我们定义了“CircleShape”方法,该方法等于第4.1.6节中详细介绍的“circle”函数。在CSMain内核中,这样的函数已经初始化,其值设置为“零”。因此,默认情况下,输出对应于黑色。

值得注意的是,在“x”中,操作的线程数等于128,这主要是由于两个因素造成的:

变量“m_texSize”的纹理大小等于128。

我们只需要一个维度就可以遍历之前定义的“m_circle”列表。

接下来,我们将定义缓冲区,该缓冲区将包含正确操作CircleShape方法所需的变量。

#pragma kernel CSMain
RWTexture2D<float4> Result;
float4 MainColor;

// declare the array of values
struct Circle
{
    float radius;
    float center;
    float smooth;
};

// declare the buffer
StructuredBuffer<Circle> CircleBuffer;

// declare the function
float CircleShape (float2 p, float center, float radius, float smooth)
{
    float c = length(p - center);
    return smoothstep(c - smooth, c + smooth, radius);
}

[numthreads(128, 1, 1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    uint width;
    uint height;
    Result.GetDimensions(width, height);
    float2 uv = float2((id.xy + 0.5) / float2(width, height));

    // we access to the array values
    float center = CircleBuffer[id.x].center;
    float radius = CircleBuffer[id.x].radius;
    float smooth = CircleBuffer[id.x].smooth;

    // we use the value as arguments in the function
    float c = CircleShape(uv, center, radius, smooth);

    Result[id.xy] = float4(c, c, c, 1);
}


与USBComputeBuffer一样,它在一个名为Circle的结构中指定了一个标量列表。这些变量将数量和数据类型与C#脚本中声明的变量相匹配。

随后,我们声明了一个名为CircleBuffer的StructuredBuffer。缓冲区将处理存储我们从USBComputeBuffer发送的值。

只需要完成SetShaderEx函数的操作并将其发送到计算着色器。要做到这一点,我们必须返回USBComputeBuffer。

void SetShaderTex()
{
    uint threadGroupSizeX;
    m_shader.GetKernelThreadGroupSizes(0, out threadGroupSizeX, out _, out _);
    int size = (int)threadGroupSizeX;
    m_circle = new Circle[size];
    ...
}

该示例通过声明一个名为threadGroupSizeX的“无符号整数”类型的变量来开始。这是由于名为GetKernelThreadGroupSizes的Void类型函数,该函数接受已在内核中配置的线程组,即,上述变量将接收值128。

// compute shader
[numthreads(128, 1, 1)]

// C#
GetKernelThreadGroupSizes(kernel, 128, 1, 1);

最后,将这样一个值添加到列表“m_circle”中,我们将其用作缓冲区的“data”。接下来,我们将把公共变量分配给列表中声明的变量。我们可以简单地初始化一个循环,并将值分别传递给每个变量来实现这一点。

void SetShaderTex()
{
    uint threadGroupSizeX;
    m_shader.GetKernelThreadGroupSizes(0, out threadGroupSizeX, out _, out _);
    int size = (int)threadGroupSizeX;
    m_circle = new Circle[size];

    for (int i = 0; i < size; i++)
    {
        Circle circle = m_circle[i];
        circle.radius = m_radius;
        circle.center = m_center;
        circle.smooth = m_smooth;
        m_circle[i] = circle;
    }
    ...
}

一旦我们将信息存储在列表中,我们就可以声明一个新的ComputeBuffer,配置信息,然后将数据发送到Compute Shader。

for (int i = 0; i < size; i++)
{
    Circle circle = m_circle[i];
    circle.radius = m_radius;
    circle.center = m_center;
    circle.smooth = m_smooth;
    m_circle[i] = circle;
}

int stride = 12;
m_buffer = new ComputeBuffer(m_circle, stride, ComputeBufferType.Default);
m_buffer.SetData(m_circle);
m_shader.SetBuffer(0, "CircleBuffer", m_buffer);
...

默认情况下,ComputeBuffer的构造函数包含三个参数:缓冲区中元素的数量、元素的大小以及我们正在创建的缓冲区的类型。

正如我们在上面的例子中看到的,我们使用存储在变量“m_circle”中的数据作为第一个参数。第二个(步长)等于我们正在传递的标量的维数乘以float变量的字节。

struct Circle
{
    float radius; // One - dimensional float - 4 bytes
    float center; // One - dimensional float - 4 bytes
    float smooth; // One - dimensional float - 4 bytes
};

int stride = (1 + 1 + 1) * 4


The type of buffer we are using in the third argument corresponds to the StructuredBuffer of type Circle that we declared earlier in the Compute Shader.

StructuredBuffer<Circle> CircleBuffer;


通过SetData函数将数据传递到缓冲区后,我们发送信息通过SetBuffer函数传递到StructuredBuffer“CircleBuffer”。

最后,我们必须配置纹理,并将颜色“m_mainColor”传递给在计算着色器中找到的四维向量“mainColor”。

在过程结束时,当缓冲区将不再使用时,我们可以调用“Release或Dispose”函数,该函数手动释放缓冲区。

void SetShaderTex()
{
    ...
    int stride = 12;
    m_buffer = new ComputeBuffer(m_circle, stride, ComputeBufferType.Default);
    m_buffer.SetData(m_circle);
    m_shader.SetBuffer(0, "CircleBuffer", m_buffer);

    m_shader.SetTexture(0, "Result", m_mainTex);
    m_shader.SetVector("MainColor", m_mainColor);
    m_rend.material.SetTexture("_MainTex", m_mainTex);

    m_shader.Dispatch(0, m_texSize, m_texSize, 1);
    m_buffer.Release();
}

原文对照

There are some cases where it will be necessary to process multiple data simultaneously, e.g., particle development, post-processing, ray tracing functions, simulations, and more. These are characterized by the computational units’ extensive graphics load they generate. However, to our benefit, there are two associated data types that we can use in our program to speed up the reading and writing of values to the memory buffer: ComputeBuffer and StructuredBuffer.


As its name mentions, ComputeBuffer corresponds to a buffer, which we can create and fill with a list of values from our C# script.


A StructuredBuffer is essentially the same, except that we declare it in the Compute Shader.

// ------------ C#
struct Properties
{
    Vector3 vertices;
    Vector3 normals;
    Vector4 tangents;
}
Properties[] m_meshProp;
ComputeBuffer m_meshBuffer;

// ------------ Compute Shader
struct Properties
{
    float3 vertices;
    float3 normals;
    float4 tangents;
};
StructuredBuffer<Properties> meshProp;

We will create a new Compute Shader in our project to understand its implementation, which we will call USB_compute_buffer. In the same way, we will create a new C# script which we will call USBComputeBuffer.
Previously, in section 4.1.6, we created a simple method called “circle,” which we used to reproduce a circle in a Quad. In this section, we will perform the same exercise with the difference that we will use the scripts previously created for this purpose.


Having studied part of the Compute Shader integration in Unity, we might deduce that USBComputeBuffer will handle the data configuration through the SetFloat and SetTexture functions.


In the same way, we will configure data that we will send later to the predefined list of values in the Compute Shader through the ComputeBuffer.SetBuffer function.


We will start by declaring the public variables associated with the effect in section 4.1.6.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class USBComputeBuffer : MonoBehaviour
{
    public ComputeShader m_shader;

    [Range(0.0f, 0.5f)] public float m_radius = 0.5f;
    [Range(0.0f, 1.0f)] public float m_center = 0.5f;
    [Range(0.0f, 0.5f)] public float m_smooth = 0.01f;
    public Color m_mainColor = new Color();

    private RenderTexture m_mainTex;
    private int m_texSize = 128;
    private Renderer m_rend;
    ...
}

If we pay attention to the example above, we will notice that the same properties (m_radiusm_center, and m_smooth) have been defined that we used previously for the generation of a circle. In addition, a color property has been created for it.


Such variables can be sent individually to the Compute Shader using the ComputeShader. SetFloat function or create a buffer containing the complete list of values we want to assign.


For the exercise, we declare a structure and a buffer associated with the list of values we will use in the shader.

public class USBComputeBuffer : MonoBehaviour
{
    public ComputeShader m_shader;

    [Range(0.0f, 0.5f)] public float m_radius = 0.5f;
    [Range(0.0f, 1.0f)] public float m_center = 0.5f;
    [Range(0.0f, 0.5f)] public float m_smooth = 0.01f;
    public Color m_mainColor = new Color();

    private RenderTexture m_mainTex;
    private int m_texSize = 128;
    private Renderer m_rend;

    // declare a struct with the list of values
    struct Circle
    {
        public float radius;
        public float center;
        public float smooth;
    }

    // declare an array type Circle to access each variable
    Circle[] m_circle;

    // declare a buffer of type ComputeBuffer
    ComputeBuffer m_buffer;
    ...
}

Inside the “struct Circle,” we have declared the variables we will use later in the ComputeShader; through “m_circle,” we will access each instance.


Given the exercise nature, we can deduce that the values of the global variables will be assigned to those defined in the struct. At this point, the ComputeBuffer becomes relevant since, once the program has filled the list with values, we must copy the data to the buffer and finally pass it to the shader.


We will start by creating the texture before performing such a process. To do so, we declare a new method which we will call “CreateShaderTex.” The method will contain the algorithm described in section 10.0.2 for the texture definition.

void Start()
{
    CreateShaderTex();
}

void CreateShaderTex()
{
    // first, we create the texture
    m_mainTex = new RenderTexture(m_texSize, m_texSize, 0, RenderTextureFormat.ARGB32);
    m_mainTex.enableRandomWrite = true;
    m_mainTex.Create();

    // then we access to the mesh renderer
    m_rend = GetComponent<Renderer>();
    m_rend.enabled = true;
}

Next, we declare a new function, which we will use in the “Update” method for study purposes only.

void Update()
{
    SetShaderTex();
}

void SetShaderTex()
{
    // write the code here...
}

Before continuing, we will go to USB_compute_buffer, since we must configure its structure before bringing the data from USBComputeBuffer. We will add the “circle” function and then define its values in the CSMain Kernel.

#pragma kernel CSMain
RWTexture2D<float4> Result;

// declare de method
float CircleShape (float2 p, float center, float radius, float smooth)
{
    float c = length(p - center);
    return smoothstep(c - smooth, c + smooth, radius);
}

[numthreads(128, 1, 1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    uint width;
    uint height;
    Result.GetDimensions(width, height);
    float2 uv = float2((id.xy + 0.5) / float2(width, height));
    // initialize the values to zero
    float c = CircleShape(uv, 0, 0, 0);

    Result[id.xy] = float4(c, c, c, 1);
}


In the previous exercise, we defined the “CircleShape” method, which is equal to the “circle” function detailed in section 4.1.6. Within the CSMain Kernel, such a function has been initialized with its values set to “zero.” Consequently, the output corresponds to a black color by default.


Noteworthy that the number of threads for the operation is equal to 128 in “x,” this is mainly due to two factors:

  1. The texture size of the variable “m_texSize” is equal to 128.
  2. We only need one dimension to traverse the previously defined “m_circle” list.

Next, we will define the buffer that will contain the variables necessary for the correct operation of the CircleShape method.

#pragma kernel CSMain
RWTexture2D<float4> Result;
float4 MainColor;

// declare the array of values
struct Circle
{
    float radius;
    float center;
    float smooth;
};

// declare the buffer
StructuredBuffer<Circle> CircleBuffer;

// declare the function
float CircleShape (float2 p, float center, float radius, float smooth)
{
    float c = length(p - center);
    return smoothstep(c - smooth, c + smooth, radius);
}

[numthreads(128, 1, 1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    uint width;
    uint height;
    Result.GetDimensions(width, height);
    float2 uv = float2((id.xy + 0.5) / float2(width, height));

    // we access to the array values
    float center = CircleBuffer[id.x].center;
    float radius = CircleBuffer[id.x].radius;
    float smooth = CircleBuffer[id.x].smooth;

    // we use the value as arguments in the function
    float c = CircleShape(uv, center, radius, smooth);

    Result[id.xy] = float4(c, c, c, 1);
}


As in USBComputeBuffer, it has specified a list of scalars inside a struct called Circle. These variables match quantity and data type with those declared in the C# script.


Subsequently, we have declared a StructuredBuffer called CircleBuffer. The buffer will handle storing the values we send from USBComputeBuffer.


It would only be necessary to complete the SetShaderTex function’s operations and send them to the Compute Shader. To do this, we must return to USBComputeBuffer.

void SetShaderTex()
{
    uint threadGroupSizeX;
    m_shader.GetKernelThreadGroupSizes(0, out threadGroupSizeX, out _, out _);
    int size = (int)threadGroupSizeX;
    m_circle = new Circle[size];
    ...
}


The example has started the exercise by declaring a variable of type “unsigned integer” called threadGroupSizeX. It is due to the Void type function called GetKernelThreadGroupSizes, which takes the group of threads that have been configured in the Kernel, i.e., the variable mentioned above will receive the value 128.

// compute shader
[numthreads(128, 1, 1)]

// C#
GetKernelThreadGroupSizes(kernel, 128, 1, 1);


Finally, such a value has been added to the list “m_circle,” which we will use as “data” for the buffer. Next, we will assign the public variables to those declared within the list. We can simply initialize a loop and pass the values to each variable separately to do this.

void SetShaderTex()
{
    uint threadGroupSizeX;
    m_shader.GetKernelThreadGroupSizes(0, out threadGroupSizeX, out _, out _);
    int size = (int)threadGroupSizeX;
    m_circle = new Circle[size];

    for (int i = 0; i < size; i++)
    {
        Circle circle = m_circle[i];
        circle.radius = m_radius;
        circle.center = m_center;
        circle.smooth = m_smooth;
        m_circle[i] = circle;
    }
    ...
}


Once we store the information in the list, we can declare a new ComputeBuffer, configure the information, and then send the data to the Compute Shader.

for (int i = 0; i < size; i++)
{
    Circle circle = m_circle[i];
    circle.radius = m_radius;
    circle.center = m_center;
    circle.smooth = m_smooth;
    m_circle[i] = circle;
}

int stride = 12;
m_buffer = new ComputeBuffer(m_circle, stride, ComputeBufferType.Default);
m_buffer.SetData(m_circle);
m_shader.SetBuffer(0, "CircleBuffer", m_buffer);
...


By default, the constructor of the ComputeBuffer contains three arguments: the number of elements in the buffer, the size of the elements, and the type of buffer we are creating.


As we can see in the example above, we use the data stored in the variable “m_circle” as the first argument. The second one (stride) is equal to the number of dimensions of those scalars we are passing times the floating variable’s bytes.

struct Circle
{
    float radius; // One - dimensional float - 4 bytes
    float center; // One - dimensional float - 4 bytes
    float smooth; // One - dimensional float - 4 bytes
};

int stride = (1 + 1 + 1) * 4


The type of buffer we are using in the third argument corresponds to the StructuredBuffer of type Circle that we declared earlier in the Compute Shader.

StructuredBuffer<Circle> CircleBuffer;


After passing the data to the buffer through the SetData function, we send the information
to the StructuredBuffer “CircleBuffer” through the SetBuffer function.


Finally, we must configure the texture and pass the color “m_mainColor” to the four-dimensional vector “MainColor” found in the Compute Shader.


At the end of the process, when the buffer will no longer be used, we can call the “Release or Dispose” function, which manually releases the buffer.

void SetShaderTex()
{
    ...
    int stride = 12;
    m_buffer = new ComputeBuffer(m_circle, stride, ComputeBufferType.Default);
    m_buffer.SetData(m_circle);
    m_shader.SetBuffer(0, "CircleBuffer", m_buffer);

    m_shader.SetTexture(0, "Result", m_mainTex);
    m_shader.SetVector("MainColor", m_mainColor);
    m_rend.material.SetTexture("_MainTex", m_mainTex);

    m_shader.Dispatch(0, m_texSize, m_texSize, 1);
    m_buffer.Release();
}
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容