找回密码
 立即注册
查看: 423|回复: 0

[译文]Unreal Engine 4 使用HLSL自定义着色器(Custom Shaders)教程(下)

[复制链接]
发表于 2021-4-7 09:33 | 显示全部楼层 |阅读模式
本文是《Unreal Engine 4 自定义着色器(Custom Shaders)教程》的下半部分,上半部分请见:
实现高斯模糊(Gaussian Blur)

和前面教程中的卡通描边一样,高斯模糊的实现也需要使用卷积运算。最终输出结果是一个核(kernel)中的所有像素的平均值。
在传统的矩形模糊(Box Blur)中,每个像素的权重相同,这就容易导致“横向模糊”问题;高斯模糊中 ,像素离中心的距离越远它的权重就越小,从而消除了“横向模糊”问题。这也使得中心像素更加重要了。




使用材质节点实现卷积运算并不是理想的方法,因为不同大小的核需要采样的像素数量不同,比如:5×5的核需要采样25个像素,而10×10的核需要采样100个像素!此时,你的节点图会看起来像一碗意大利面。
这时候自定义节点就隆重登场了。使用自定义节点,我们可以用一个for循环来实现对核中每个像素的采样。首先我们需要设定一个参数来控制采样半径。
创建半径参数

回到材质编辑器创建一个标量参数(ScalarParameter)并命名为Radius,将其默认值设为1.




这个Radius将决定图像的模糊程度。
接下来为Gaussian Blur创建一个新的输入引脚并命名为Radius,然后添加一个Round节点并如下图所示链接:


Round节点的作用是确保核的各维度永远是整数。
好!开始写代码了!因为我们需要分别从纵横两个方向进行高斯处理,所以最好把这个计算放到函数中。
当使用自定义节点时,我们无法使用标准方式定义函数。这是因为编译器会把你的代码直接复制粘贴到一个函数中。我们无法将一个函数定义到另一个函数里,否则将产生错误。
幸运的是,我们可以利用这个复制粘贴行为创建全局函数。
创建全局函数

如上文所述,编译器就是简单粗暴地把自定义节点中的文本复制粘贴到一个函数里。举个例子:我们的自定义节点是下面的内容:
  1. return 1;
复制代码
那么编译器会把它直接复制到CustomExpressionX函数中,甚至连缩进都不改一改!
  1. MaterialFloat3 CustomExpression0(FMaterialPixelParaeters Parameters)
  2. {
  3. return 1;
  4. }
复制代码
那么我们看一下如果我们使用下面的代码会出现什么情况
  1. return 1;
  2. }
  3. float MyGlobalVariable;
  4. int MyGlobalFunction(int x)
  5. {
  6.     return x;
复制代码
然后生成地HLSL代码就变成了:
  1. MaterialFloat3 CustomExpression0(FMaterialPixelParameters Parameters)
  2. {
  3.     return 1;
  4. }
  5. float MyGlobalVariable;
  6. int MyGlobalFunction(int x)
  7. {
  8.     return x;
  9. }
复制代码
如你所见,MyGlobalVariable和MyGlobalFunction()不隶属于任何函数,因此它们是全局的,我们可以在任何地方调用他们。
注:输入的代码中没有最后的大括号,因为编译器会在末尾插入一个},如果你非要写个}在代码里,那么最后就会因为有两个}而报错了。
好,现在我们利用这个小技巧实现高斯函数吧。
创建高斯函数

简化版的一维高斯函数:




用钟形曲线来表示它,如果输入值在-1到1之间,那么输出值会在0到1之间。


本教程中,我们会把高斯函数放到独立的节点中,创建一个新的自定义节点并将其命名为Global
然后,将下面的代码填到里面:
  1. return 1;
  2. }
  3. float Calculate1DGaussian(float x)
  4. {
  5.     return exp(-0.5 * pow(3.141 * (x), 2));
复制代码
Calculate1DGaussian()就是简化版的一维高斯函数的代码实现。
为了使该函数可用,我们得在材质节点图中的某处调用Global一下才行。最简单的办法就是让Global和第一个节点相乘一下。这可以确保我们在自定义节点中调用全局函数之前,它们已经被定义了。
首先,将GlobalOutput Type设为CMOT Float 4,之所以这样做是因为我们得让它乘以同样是float4SceneTexture


接下来,如下图所示链接各个节点:


点击应用编译材质。现在,任何后续节点都可以使用定义在Global的中的函数了。
多像素采样

打开Gaussian.usf使用下面的代码替换现有内容:
  1. static const int SceneTextureId = 14;
  2. float2 TexelSize = View.ViewSizeAndInvSize.zw;
  3. float2 UV = GetDefaultSceneTextureUV(Parameters, SceneTextureId);
  4. float3 PixelSum = float3(0, 0, 0);
  5. float WeightSum = 0;
复制代码
解释一下各个变量的意义:
    SceneTextureId: 保存要抽样的场景纹理index。这样我们就不用把index写死到函数调用中了。本例中index对应为Post Process Input 0TexelSize: 保存纹理影像元件(Texel)的大小。用于转换成UV空间中的偏移量。UV: 当前像素的UV坐标。PixelSum: 用于累加核中每个像素的颜色值。WeightSum: 用于累加核中每个像素的权重值。
接下来,我们需要创建2个for循环。一个用于垂直方向一个用于水平方向。将下方代码添加到变量列表下面:
  1. for (int x = -Radius; x <= Radius; x++)
  2. {
  3.     for (int y = -Radius; y <= Radius; y++)
  4.     {
  5.     }
  6. }
复制代码
上面的代码会创建一个以当前像素为中心的网格。其范围是2r + 1,例如:radius是2,那么范围就是(2 * 2 + 1) by (2 * 2 + 1) 即5×5。
接下来,我们需要累加像素的颜色值和权重值。将下面的代码插入到最内层的for 循环中:
  1. float2 Offset = UV + float2(x, y) * TexelSize;
  2. float3 PixelColor = SceneTextureLookup(Offset, SceneTextureId, 0).rgb;
  3. float Weight = Calculate1DGaussian(x / Radius) * Calculate1DGaussian(y / Radius);
  4. PixelSum += PixelColor * Weight;
  5. WeightSum += Weight;
复制代码
解释一下上面的代码:
    计算采样像素的相对偏移量并将其转换到UV空间。使用上面的偏移量对场景纹理进行抽样(本例中是Post Process Input 0)。对被采样像素计算权重。要进行2D高斯运算,我们之际上只需要将2个1D高斯运算乘到一起。之所以要除以Radius,是因为简化版的高斯运算的输入值为-1到1.这个除运算实际上就是对输入值进行标准化。将加权后的颜色累加到PixelSum。把权重累加到WeightSum。
最后,我们需要计算加权平均值。把下面的代码加到文件末尾(for循环的外面):
  1. return PixelSum / WeightSum;
复制代码
这就是高斯模糊的全部了!关闭Gaussian.usf并回到材质编辑器。点击应用并关闭PP_GaussianBlur。使用PPI_Blur测试一下不同的模糊半径。




局限性

尽管自定义节点非常强大,但是也有它的缺点。本例中我们将列举自定义节点的一些局限性和缺点。
渲染访问

渲染管线的很多部分都不允许自定义节点访问。其中包含光照信息和运动向量。这一点和使用向前渲染(forward rendering)略有不同。
引擎版本匹配

在一个版本中编写的HLSL节点不能够确保可以在其它版本中运行。如文中提到的,4.19前我们使用TextureCoordinate获取UV,而4.19以后我们使用GetDefaultSceneTextureUV()。
优化

以下是Epic官方关于优化的一段描述:
自定义节点会阻止常量叠算(Constant folding)并可能产生较内置节点更多的指令调用。常量叠算是UE4中降低着色器指令调用的一种优化方式。 例如:一个表达式链Time >Sin >Mul by parameter > Add,将会被UE4塌陷成一个指令 这是可能的,因为所有的表达式输入(Time, parameter)对于本次draw call来说是一个常量,它们并不随像素而改变。然而UE4无法在自定义节点中折叠它们,这样就无法达到和内置节点同样的效率。 所以最佳实践就是,只有当你的已有的节点实在无法满足你的需要时再使用自定义节点。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-15 08:27 , Processed in 0.096510 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表