ComputeShader基础用法系列之二

上一节我们介绍了如何让一个最简单的ComputeShader跑起来并且使我们看到效果。接下来我们看看如何向ComputeShader中传值以及如何回读到CPU。

ComputeShader类提供了一些传值方法,基础类型如SetFloat,SetInt,SetVector,SetMatrix,集合如SetMatrixArray,SetFloats,SetInts,SetBuffer等。我们具体看一下怎么使用。

在我们上一节compute文件基础上,在RWTexture2D<float4> Result;下声明一个float4 color;如下:

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

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    Result[id.xy] = color;
}

然后再C#中SetVector,如下:

public ComputeShader compute;
    // Start is called before the first frame update
    public Renderer renderer;

    public Color color;
    private int kernel;
    void Start()
    {
        kernel = compute.FindKernel("CSMain");
        RenderTexture rt = new RenderTexture(512,512,32);
        rt.enableRandomWrite = true;
        rt.Create();
        compute.SetTexture(kernel,"Result",rt);
        renderer.material.SetTexture("_BaseMap",rt);
    }

    // Update is called once per frame
    void Update()
    {
        compute.SetVector("color",color);
        compute.Dispatch(kernel,64,64,1);
    }

当我们不断的在CPU传入不同的颜色值时,贴图的颜色就会发生变化:

 

在举个传float的例子,我们加入一个时间变量,在Compute文件中加入time属性,返回结果修改为如下:

Result[id.xy] = color + sin(time).xxxx;

c#中:

compute.SetFloat("time",Time.time);

纹理就会随着时间不断变化颜色:

 

 

 如果我们要传一个集合呢,比如说我们要做一个颜色列表,让GPU根据线程ID取不同的颜色值,可以如下(color可以隐式转换为vec4,但是color数组不行,需要手动转换成vec4数组):

compute.SetVectorArray("colors",colorVectors);

compute文件中如下:

#pragma kernel CSMain
RWTexture2D<float4> Result;
float4 colors[10];
float time;
[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    int colorIndex = id.x%10;
    Result[id.xy] = colors[colorIndex] + sin(time).xxxx;
}

最后结果如下:

 

 

 为了进行下面的内容,我们先做一个例子,通过ComputeShader进行物体的Transform设置,这里面涉及到传入数据到GPU以及回读到CPU。

Transform这种结构数据需要使用StructuredBuffer类型进行传递,因为需要读写,所以我们用RWStructuredBuffer。但是我们会发现ComputeShader类只给了设置数据的方法,但是没有返回数据到CPU的方法。这时我们就需要使用ComputeBuffer进行数据传递和回读。

c#代码如下:

using UnityEngine;

public class ComputeShaderTest : MonoBehaviour
{
    struct InstanceData
    {
        public Vector3 position;
        public Vector3 rotation;
        public Vector3 scale;
    }
    public ComputeShader compute;
    public GameObject prefab;
    public int count = 32;
    // Start is called before the first frame update
    private int kernel;
    void Start()
    {
        kernel = compute.FindKernel("CSMain");
        ComputeBuffer cb = new ComputeBuffer(count,36);
        var instanceData = new InstanceData[count];
        cb.SetData(instanceData);
        compute.SetBuffer(kernel,"data",cb);
        compute.Dispatch(kernel,4,1,1);
        cb.GetData(instanceData);
        for (int i = 0; i < count; i++)
        {
            var go = GameObject.Instantiate(prefab);
            go.transform.position = instanceData[i].position;
            go.transform.localEulerAngles = instanceData[i].rotation;
            go.transform.localScale = instanceData[i].scale;
        }
        cb.Release();
    }
}

compute文件如下:

// Each #kernel tells which function to compile; you can have many kernels
#pragma kernel CSMain
struct InstanceData{
    float3 position;
    float3 rotation;
    float3 scale;
};
RWStructuredBuffer<InstanceData> data;
[numthreads(8,1,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    int idx = id.x;
    data[idx].position = float3(id.x,0,0);
    data[idx].rotation = float3(id.x,0,0);
    data[idx].scale = float3(idx,idx,idx);
}

最后效果如图:

 

 可以看到我们通过ComputeBuffer的SetData和GetData传递和回读数据,需要注意的是ComputeBuffer是有大小限制的,大小为不能超过2048的、4的倍数的一个自然数。

 我们最后看一下传递float数组的例子,根据一个float数组划线:

c#代码如下:

using UnityEngine;

public class ComputeShaderTest : MonoBehaviour
{
    public ComputeShader compute;
    // Start is called before the first frame update
    public Renderer renderer;
    private int kernel;
    void Start()
    {
        float[] fArray = new float[]
        {
           1, 1, 0, 1, 0, 0, 0, 1,
        };
        kernel = compute.FindKernel("CSMain");
        RenderTexture rt = new RenderTexture(512,512,32);
        rt.enableRandomWrite = true;
        rt.Create();
        compute.SetTexture(kernel,"Result",rt);
        compute.SetFloats("fValues",fArray);
        compute.Dispatch(kernel,64,64,1);
        renderer.material.SetTexture("_BaseMap",rt);
    }
}

compute代码如下:

#pragma kernel CSMain
RWTexture2D<float4> Result;
float fValues[8];

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    uint idx = id.x;
    float colorFactor = fValues[idx%8];
    Result[id.xy] = float4(colorFactor,colorFactor, colorFactor, 0);
}

结果如下:

 

 我们看到computeshader的结果并没有根据我们传入的结果进行分布,而是隔了8个像素画一次。那么问题出在了哪里呢?因为内存对齐!

我们设置float数组必须满足内存对齐16字节,因为GPU运算单元是4-component的,是基于向量运算的(即float4),所以我们传入的float数组(长度为8)到GPU的时候,只有第1个和第5个元素会被当做float值,其他元素都当做补齐元素了,这样就导致了数据丢失。而最后的结果之所以是8个像素一条线,是因为我们再compute shader中声明的float数组是8长度的。但是我们只向GPU传入了2长度。所以剩下的会被自动补0。那么我们就必须修改CPU传入数组为:

float[] fArray = new float[]
        {
           1, 0,0,0,
           1,0,0,0,
           0,0,0,0,
           1,0,0,0,
           0,0,0,0,
           0,0,0,0,
           0,0,0,0,
           1,0,0,0
        };

结果如下:

 

 这样结果就正确了。接下来我们会学习基于ComputeShader的一些用法和注意的问题。

posted @ 2020-09-30 17:43  syb7384  阅读(1826)  评论(0编辑  收藏  举报