尝试在UE的材质节点中进行高斯模糊

0. 高斯模糊基础

高斯模糊的原理已有很多文章介绍。实践上讲,就是对图像做一个卷积(通俗讲,就是采样相邻的几个像素并乘算上对应的权重)。我这里想采用的卷积核是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的材质节点中进行高斯模糊的操作。

1. 面临的问题和思路

在UE材质中所面临的一个问题就是——
采样操作需要重复多次,但是材质节点中并没有 循环 的操作,如果重复创建25个节点会比较麻烦且难以维护。

一种解决方法是借助外部的shader文件。(可以参考A Guide to Creating Your Own Shaders in Unreal Engine - EmptyQ

而我不想借助外部的shader文件,我希望只使用材质节点完成。


开始我是这样想的:
材质最终还是会转换为HLSL代码。转换后的代码可以在材质编辑器中查看:
在这里插入图片描述
因此,每个材质节点都会以某种形式转换为HLSL代码。那采样的节点自然也会转换为对应的HLSL代码。所以,我可以使用文本差异工具,来比较出添加采样节点后HLSL代码增加的部分,而这部分就是纹理采样的代码。

Custom节点 可以插入HLSL代码,因此,知道了采样的HLSL代码是什么,就可以在代码中循环了。


不过后来发现,其实在 Custom节点官方文档 里的范例就是展示做相同的事,而且代码表达上更为优雅:(不过其中有个问题值得探究,见文末【延伸探究:TexSampler参数哪里来的】)

在这里插入图片描述
因此,接下来我将仿照这里的写法。

2. 实现

我的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是采样时邻居的距离。

节点连接如下:
在这里插入图片描述
效果:
在这里插入图片描述
可以看到,基础效果是实现了。
不过一个很明显的瑕疵是:边界感。

3. 降低采样的Mip来避免瑕疵

这种瑕疵随着模糊范围的升高而越来越明显:
在这里插入图片描述
不难想到,这种瑕疵是因为采样的Mip等级太高所致的:可以想象,当采样的“邻居”相距很远的时候,也意味一个颜色的影响的距离会变得很远,而Mip等级高时颜色的变化频率也会更高,因此颜色的突变也会影响很远。


那么,Mip等级多少合适呢?
不难想到,它由step决定:

  • 假设图像分辨率512×512。那么step 1 512 \frac{1}{512} 5121时,正好一步采样1个像素,那么Mip等级就是0
  • 假设step 1 256 \frac{1}{256} 2561时,那么一次采样跨越了2个像素,那么Mip等级就是1
  • 假设step 1 128 \frac{1}{128} 1281时,那么一次采样跨越了4个像素,那么Mip等级就是2
  • 也就是说:一步采样的像素数=图像分辨率×step。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:
在这里插入图片描述
效果:
在这里插入图片描述
可以看到之前的瑕疵已经没有了。

4*. 测试直接降低Mip的效果

毕竟,上面的方式采样了多次,效率是相对较低的。

其实,如果不强求“高斯模糊”的方式,直接降低Mip的方式也是可以达成 “模糊” 的目标的。

首先为了最好的效果,可以将图像的Mip生成方式变为Blur:
在这里插入图片描述
然后,将采样的Mip计算方式变为绝对值,而此值由参数决定:
在这里插入图片描述
测试效果:
在这里插入图片描述
对比下来,高斯模糊(左)相对于降低Mip的方式(右)更柔和,变化更细腻,毕竟Mip降低时的分辨率是较低的。而降低Mip的方式的优点是效率高。
在这里插入图片描述

延伸探究:TexSampler参数哪里来的

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作为采样函数的参数。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值