Unity Compute Shader - 3 更多内核

准备工作

在上一章中,我们了解了每个线程块是按照什么顺序在图片上绘制的,但是我们只使用了一个内核来绘制,在这一章中,我们将使用多个内核来同时在一张图片上绘制图案

  • 首先,我们打开 FirstComputeShader 脚本,修改之前的内核名称,添加一个int类型的变量用于获取传入图片的分辨率,同时要将方法名称改为和内核名称相同,如下:
1
2
3
4
5
6
7
8
9
10
#pragma kernel SolidRed

RWTexture2D<float4> Result;
int texResolution;

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

回到 AssignTexture 脚本中,添加一个string类型变量用来记录我们要使用Compute Shader中的哪个内核名称,接着修改与 CSMain 相关的代码,最后使用 SetInt 方法将图片分辨率传给Compute Shader,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using UnityEngine;

public class AssignTexture : MonoBehaviour
{
...

public string kernelName= "SolidRed";

private void Start()
{
...

DispatchShader(texResolution / 8, texResolution / 8);
}

private void InitShader()
{
...

//获取指定名称内核的索引值
KernelHandle = shader.FindKernel(kernelName);

//将图片的分辨率数据传给Compute Shader
shader.SetInt("texResolution", texResolution);
}
}

再次运行,我们就会看到,Quad上显示为一张红色的图片

回到 FirstComputeShader 脚本,添加一个新的内核为 Solidyellow,并创建与之对应的方法,代码如下:

1
2
3
4
5
6
7
8
9
#pragma kernel Solidyellow

...

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

代码添加完成后,回到unity中,将inspector面板中的 kernelName 变量值改为 Solidyellow,再次运行,Quad就显示为一张纯黄色的图片了

四等分不同颜色的正方形

FirstComputeShader 脚本中添加一个内核,名称为 SplitScreen 这个方法是用来将整个图片等分为4个部分,并为每个部分填上不同的颜色,效果如下:

按照上图思路,首先我们需要计算出当前传入图片分辨率的一半,然后使用Compute Shader中的 step 函数确定4个区域的颜色
step 函数入参为两个浮点型参数,并返回一个布尔值的浮点表示,工作原理如下:

  • step(edge, x)
  • 如果x大于或等于edge,则返回1.0。
  • 如果x小于edge,则返回0.0。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#pragma kernel SplitScreen

...

[numthreads(8, 8, 1)]
void SplitScreen(uint3 id : SV_DispatchThreadID)
{
//首先计算出当前传入图片分辨率的一半
//图片是正方形,所以只需要计算一个边长即可
//这里使用位运算,首先是因为边长一定是正整数,而且位运算比除法更快,性能更好
int halfRes = texResolution >> 1;

//-step(edge, x)
//如果x大于或等于edge,则返回1.0
//如果x小于edge,则返回0.0
//这样就将一张图切割为了4个部分,并未每个部分添加了不同的颜色
Result[id.xy] = float4(step(halfRes, id.x), step(halfRes, id.y), 0, 1);
}

这里就是上一章中介绍的DispatchThreadID中X与Y分别作为step中的第二个参数。例如当DispatchThreadID为(166,50,0)时,step(halfRes, id.x)就是step(128, 166),此时step运算结果为1.0

回到unity中,将inspector面板中的 kernelName 变量值改为 SplitScreen,运行后就可以看到之前示例图的效果了

在图片中间绘制一个黄色的圆形

接下来,我们尝试在图片中央画一个黄色的圆形,效果如下:

根据刚才绘制正方形的经验,我们可以创建一个内核函数,其中如果id.xy在圆上,就返回1,如果不在圆上,就返回0
这里我们要用到HLSL的另一个函数 length,用来判断两个点之间的距离。例如

1
2
float2 v = float2(4, 3);
length(v) = 5;

有了判断距离的方法,我们还需要思考一个问题,在当前图上绘制时,坐标原点是左下角,即左下角位置为(0, 0)。如下图所示

而我们需要绘制的圆形则在图片的中心,也就是我们想将坐标的原点移到图片中心,这样移动的话,图片左下角坐标就变成了(-128,-128),而图片右上角坐标就变成了(127,127),如下图所示

按照这个思路,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#pragma kernel Circle

...

float inCircle(float2 pt, float radius)
{
if (length(pt) < radius)
{
return 1.0;
}
else
{
return 0.0;
}
}

[numthreads(8, 8, 1)]
void Circle(uint3 id : SV_DispatchThreadID)
{
int halfRes = texResolution >> 1;

//这里的"-halfRes"其实是在移动坐标原点
//也就是x和y分别横向、纵向负方向移动了halfRes距离
//也就是将坐标(0, 0)从左下角移动到了图片中间
//现在图片左下角起始坐标为(-128,-128)了
float2 _pt = (float2) ((int2) id.xy - halfRes);

//这里代入inCircle方法,判断图片中心点到(x,y)的距离是否大于半径
//小于等于半径的话,就为1,否则就为0
float _res = inCircle(_pt, 100.0);

//将结果转换为颜色值输出
Result[id.xy] = float4(_res, _res, 0, 1);
}

其中,inCircle 方法就是在判断每组坐标(x,y)是否在圆上,如下图所示,(x1, y1)不在圆上,inCircle 方法返回0,(x2, y2)在圆上,inCircle 方法返回1:

回到unity中,将inspector面板中的 kernelName 变量值改为 Circle,运行后就可以看到之前示例图的效果了

绘制蓝色正方形

接下来,我们要在图片上绘制一个蓝色正方形,如下图所示:

通过观察最终效果图,绘制蓝色正方形与绘制黄色圆形的原理其实相似,都是通过判断一个点是否在当前图形范围内,只不过蓝色正方形是要判断当前一个点跟正方形上下左右四条边的关系,这里还是会用到 step 函数。同时使用一个 float4 类型的变量,用来记录正方形左下角起始点位置和长宽,float4 和C#中的 Rect有些类似,都是存储4个float类型的值,如下图所示:

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#pragma kernel Square

...

float inSquare(float2 pt, float4 _rect)
{
//当前点跟矩形 左面 的边的关系
//点在边左侧为0,右侧为1
float horz_L = step(_rect.x, pt.x);
//当前点跟矩形 右面 的边的关系
float horz_R = step(_rect.x + _rect.z, pt.x);
//其实这里只会出现3中情况,即
//点在矩形左侧 0 - 0,点在矩形上 1 - 0,点在矩形右侧 1 - 1
float horz = horz_L - horz_R;

//当前点与矩形 下面 的边的关系
float vert_B = step(_rect.y, pt.y);
//当前点与矩形 上面 的边的关系
float vert_T = step(_rect.y + _rect.w, pt.y);
//根据上面两个判断获取当前点在垂直方向与矩形的关系
float vert = vert_B - vert_T;

//只有当前点在矩形上,即horz=1和vert=1,相乘才为1,其他时候都是0
return horz * vert;
}

[numthreads(8, 8, 1)]
void Square(uint3 id : SV_DispatchThreadID)
{
float4 _rect = float4(10.0f, 20.0f, 30.0f, 40.0f);

float _res = inSquare((float2) id.xy, _rect);

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

这样,我们就在当前你图片上绘制了一个自定义位置和边长的矩形了,效果如下:

现在,我们通过在图片上绘制一个四等分颜色不同的正方形和图片中间黄色的圆形,引入了 steplength 两个函数的使用方法


相关链接

ComputeShader.SetInt