赞
踩
高斯模糊的原理已有很多文章介绍。实践上讲,就是对图像做一个卷积(通俗讲,就是采样相邻的几个像素并乘算上对应的权重)。我这里想采用的卷积核是5×5的(数据来自于这里的讨论):
{
0.01, 0.02, 0.04, 0.02, 0.01,
0.02, 0.04, 0.08, 0.04, 0.02,
0.04, 0.08, 0.16, 0.08, 0.04,
0.02, 0.04, 0.08, 0.04, 0.02,
0.01, 0.02, 0.04, 0.02, 0.01
};
现在,我想尝试在UE的材质节点中进行高斯模糊的操作。
在UE材质中所面临的一个问题就是——
采样操作需要重复多次,但是材质节点中并没有 循环 的操作,如果重复创建25个节点会比较麻烦且难以维护。
一种解决方法是借助外部的shader文件。(可以参考A Guide to Creating Your Own Shaders in Unreal Engine - EmptyQ)
而我不想借助外部的shader文件,我希望只使用材质节点完成。
开始我是这样想的:
材质最终还是会转换为HLSL代码。转换后的代码可以在材质编辑器中查看:

因此,每个材质节点都会以某种形式转换为HLSL代码。那采样的节点自然也会转换为对应的HLSL代码。所以,我可以使用文本差异工具,来比较出添加采样节点后HLSL代码增加的部分,而这部分就是纹理采样的代码。
而 Custom节点 可以插入HLSL代码,因此,知道了采样的HLSL代码是什么,就可以在代码中循环了。
不过后来发现,其实在 Custom节点官方文档 里的范例就是展示做相同的事,而且代码表达上更为优雅:(不过其中有个问题值得探究,见文末【延伸探究:TexSampler参数哪里来的】)

因此,接下来我将仿照这里的写法。
我的Custom的节点代码如下:
float kernel[25] = { 0.01, 0.02, 0.04, 0.02, 0.01, 0.02, 0.04, 0.08, 0.04, 0.02, 0.04, 0.08, 0.16, 0.08, 0.04, 0.02, 0.04, 0.08, 0.04, 0.02, 0.01, 0.02, 0.04, 0.02, 0.01 }; float3 result = float3(0,0,0); float step = range/5; for(int x=0;x<5;x++) for(int y=0;y<5;y++) { float2 uv = UV; uv.x+=step*(x-2); uv.y+=step*(y-2); float3 color = Texture2DSample(Tex, TexSampler, uv); result += color*kernel[x*5+y]; } return result;
其中step是采样时邻居的距离。
节点连接如下:

效果:

可以看到,基础效果是实现了。
不过一个很明显的瑕疵是:边界感。
这种瑕疵随着模糊范围的升高而越来越明显:

不难想到,这种瑕疵是因为采样的Mip等级太高所致的:可以想象,当采样的“邻居”相距很远的时候,也意味一个颜色的影响的距离会变得很远,而Mip等级高时颜色的变化频率也会更高,因此颜色的突变也会影响很远。
那么,Mip等级多少合适呢?
不难想到,它由step决定:
step是
1
512
\frac{1}{512}
5121时,正好一步采样1个像素,那么Mip等级就是0。step是
1
256
\frac{1}{256}
2561时,那么一次采样跨越了2个像素,那么Mip等级就是1step是
1
128
\frac{1}{128}
1281时,那么一次采样跨越了4个像素,那么Mip等级就是2step。Mip等级就是以2为底取“一步采样的像素数”的对数。即:
M
i
p
等
级
=
log
2
(
图
像
分
辨
率
×
s
t
e
p
)
Mip等级=\log_2(图像分辨率×step)
Mip等级=log2(图像分辨率×step)
接下来实践一下。
首先,计算mip等级。
然后,采样的函数由 Texture2DSample 变成 Texture2DSampleLevel,这将可以添加一个参数来指定Mip等级:
float kernel[25] = { 0.01, 0.02, 0.04, 0.02, 0.01, 0.02, 0.04, 0.08, 0.04, 0.02, 0.04, 0.08, 0.16, 0.08, 0.04, 0.02, 0.04, 0.08, 0.04, 0.02, 0.01, 0.02, 0.04, 0.02, 0.01 }; float3 result = float3(0,0,0); float step = range/5; float mip = log2(TextureSize*step); for(int x=0;x<5;x++) for(int y=0;y<5;y++) { float2 uv = UV; uv.x+=step*(x-2); uv.y+=step*(y-2); float3 color = Texture2DSampleLevel(Tex, TexSampler, uv, mip); result += color*kernel[x*5+y]; } return result;
纹理尺寸暂时硬编码为512:

效果:

可以看到之前的瑕疵已经没有了。
毕竟,上面的方式采样了多次,效率是相对较低的。
其实,如果不强求“高斯模糊”的方式,直接降低Mip的方式也是可以达成 “模糊” 的目标的。
首先为了最好的效果,可以将图像的Mip生成方式变为Blur:

然后,将采样的Mip计算方式变为绝对值,而此值由参数决定:

测试效果:

对比下来,高斯模糊(左)相对于降低Mip的方式(右)更柔和,变化更细腻,毕竟Mip降低时的分辨率是较低的。而降低Mip的方式的优点是效率高。

在 Custom节点官方文档 里的范例,以及本文的Custom节点的代码中,可以看到代码中的TexSampler变量并没有在输入的列表中,那么它是哪里来的呢?

我想在这里深入探究一下。
首先,Custome节点的C++类是UMaterialExpressionCustom。
而UMaterialExpressionCustom::Compile函数在 MaterialExpressions.cpp中定义:
int32 UMaterialExpressionCustom::Compile(class FMaterialCompiler* Compiler, int32 OutputIndex)
{
...
return Compiler->CustomExpression(this, CompiledInputs);
}
可以看到它最后调用了 Compiler(FMaterialCompiler) 的CustomExpression函数。
FHLSLMaterialTranslator是实际的FMaterialCompiler。而在它的CustomExpression函数实现中终于看到了相关的逻辑:
int32 FHLSLMaterialTranslator::CustomExpression( class UMaterialExpressionCustom* Custom, TArray<int32>& CompiledInputs ) { ... // Add call to implementation function FString CodeChunk = FString::Printf(TEXT("CustomExpression%d(Parameters"), CustomExpressionIndex); for( int32 i = 0; i < CompiledInputs.Num(); i++ ) { // skip over unnamed inputs if( Custom->Inputs[i].InputName.IsNone() ) { continue; } FString ParamCode = GetParameterCode(CompiledInputs[i]); EMaterialValueType ParamType = GetParameterType(CompiledInputs[i]); CodeChunk += TEXT(","); CodeChunk += *ParamCode; if (ParamType == MCT_Texture2D || ParamType == MCT_TextureCube || ParamType == MCT_Texture2DArray || ParamType == MCT_TextureExternal || ParamType == MCT_VolumeTexture) { CodeChunk += TEXT(","); CodeChunk += *ParamCode; CodeChunk += TEXT("Sampler"); } } CodeChunk += TEXT(")"); ResultIdx = AddCodeChunk( OutputType, *CodeChunk ); return ResultIdx; }
可以看到,它在处理所有的输入的时候,如果发现类型是MCT_Texture2D等纹理,会自动再补上一个同名且后接"Sampler"的参数。
这下就明白它的逻辑了:
如果Custom节点的输入中有名为XXX的纹理,那么在编译时就会自动加上一个名为XXXSampler的输入,因此可以在Custom节点的代码中直接使用这个Sampler作为采样函数的参数。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。