UE4 材质 UV膨胀技术
自动和改进的UV膨胀材质
在处理带有不透明遮罩的纹理时,将边缘像素放大或延伸到背景中可能很重要。对于蒙版材质,这可以防止在低等级的mipmap中的边缘渗出现象。
为什么会有出现这个情况呢?
主要是mipmap是根据DDX,和DDY进行判断的,这两个函数的作用就是用来判断在这个像素的某一个参数值,在屏幕相邻的像素点中是一个什么差值。用来判断使用mipmap的等级,实际的情况还没有看,不过差不多就是这个道理:如果我的mipmap等级很高,也就是距离的非常的远,那么对于蒙版来说,这会导致采样的蒙版贴图的边缘会出现插值,也就是对于蒙版的边缘,会有一些黑边的出现。
对于动态mesh的绘制效果,膨胀的位置纹理贴图(" positional texture maps ")将允许跨越UV边界进行无缝的绘制。位置纹理贴图对膨胀非常敏感,因此为位置贴图正确的执行膨胀非常重要。
在这篇文章中,我们将研究一种为静态网格创建位置纹理贴图的方法,并在unreal engine 4使用像素着色器扩展它们。我们使用一种 slope extrapolation方法,以更精确地重建边缘附近的像素,就好像它们是从边缘开始的一样。
首先,让我们进入到为什么需要这个技术的场景当中。
与16像素和64像素半径的膨胀相比,上图的第一行第一个图像显示了未膨胀时的纹理。第二行显示应用到蒙版的材质,并演示较低的mipmap在不使用膨胀时背景出现渗出的情况。膨胀版将保持其外观,并且不会在远处出现黑色轮廓。
除了上述的mip-map问题外,如果不使用膨胀,*别的mip也会出现问题。在将多边形渲染到纹理贴图时,多边形的边缘倾向于以一定角度横切像素,从而使像素以阶梯状图案跨越多边形的边缘。这些像素将是白色或黑色,因此在纹理分辨率下可以看到台阶。
在实时渲染中,这叫做锯齿。暴力的解决方案是以更高的分辨率渲染,然后将图像采样到所需的分辨率。在许多情况下,这是可行的,但是由于纹理过滤的工作方式,边缘伪影仍然可以悄悄地进入,并且我们可能没有足够高的分辨率的原始素材。膨胀通过向外扩展边缘上的像素来解决问题,这样它们可以混合到其他颜色相似的像素而不是纹理背景颜色。
大多数图像编辑程序(PS)都具有可以处理膨胀的插件,并且大多数插件在处理颜色信息时效果很好,而在处理位置或法线时效果不佳。 对于特殊情况,我们需要动态创建展开的UV图,并在该过程中自动对其进行膨胀。
关于展开UV,可以参考
https://papalqi.cn/index.php/archives/521/
结果是,一个膨胀器比Photoshop的xNormal更好地处理位置数据和法线数据,并且速度更快。
拥有一个展开的位置贴图意味着您可以在静态网格上执行光线跟踪类型操作,例如通过在世界上进行光线追踪将spheremask绘制到网格上。
这是一些基本膨胀器的代码。它在指定的距离内搜索8个相邻的正方形图案。注意,在变量“offset”中,上下左右在对角线偏移之前。这只是一个简单的方式来确保更短的距离的坐标值能够率先被使用。
float texelsize = 1 / TextureSize;
float2 offsets[8] = {float2(-1,0), float2(1,0), float2(0,1), float2(0,-1), float2(-1,1), float2(1,1), float2(1,-1), float2(-1,-1)};
float3 sample = Tex.SampleLevel(TexSampler, UV, 0);
if(sample.x != 0 || sample.y != 0 || sample.z != 0) return sample;
int i, j = 0;
while(i < MaxSteps)
{
i++;
while (j < 8)
{
float2 curUV = UV + offsets[j] * texelsize * i;
float3 offsetsample = Tex.SampleLevel(TexSampler, curUV, 0);
if(offsetsample.x != 0 || offsetsample.y != 0 || offsetsample.z != 0) return offsetsample;
j++;
}
}
return MinSample;
这在某些情况下是可行的,但是这个简单方法的问题是它只是将边缘像素从原来的位置向外拉伸。这意味着边缘上的任何过滤的纹理都将被插值到错误的值,因为相邻像素不会倾向于实际位置的方向:
为了解决这个问题,我们可以在代码中引入一些斜率的数学技巧。这意味着每当代码找到一个可能的像素进行采样时,而不是仅仅使用该值,它就会检查另一个稍微有点偏移的像素,并获取这两个像素之间的差异。使用两个采样点之间的距离,可以建立一个斜率,然后使用该斜率将采样向外外推到实际写入的像素。
在上图中,您可以想象蓝色阴影多边形是某个高度贴图或位置纹理的侧剖面图。绿点是我们理想中想要膨胀到像素点。如果使用简单的膨胀,直接从最近找到的邻居(上面的蓝点)复制值。这会导致红线是平的,并且不会继续保持原始数据的斜率。通过采取一个额外的偏移采样(上面的橙色点),可以计算斜率,最终值可以是原始数据的连续斜率。
若要将其添加到上述着色器,只需要几行。这发生在最后一个if语句的内部,该语句检查找到的邻居是否不等于0。
float2 projectUV = curUV + offsets[i] * texelsize * j * 0.25;
float3 direction = Tex.SampleLevel(TexSampler, projectUV, 0);
float3 delta = offsetsample - direction;
MinSample = offsetsample + delta * 4;
无论当前偏移距离是多少,这只会将另一个样本推出0.25倍。选择此值是为了避免跳过太远的前方,并且可能读取所需目标像素另一侧的黑色值。这可以通过另一个迭代检查来找到理想的新样本位置,但在我的测试中,这很好,因为我在uv布局上没有任何非常薄的部分。注意delta乘以4,这是考虑到偏移量是0.25倍的长度。
现在让我们快速看看这到底是如何改善膨胀效果的。一个只有8个样本方向的标准膨胀看起来质量不是很高,通过控制参数,你会很快开始看到8个样本方向随着距离增加出现一条硬线的形成。斜率外推完全解决了这个问题,甚至完美地连接了此隔离的UV环内部的UV:
在上面的图像中很难看到,但中心峰值的平滑实际上并不能使它在使用此位置贴图进行渲染时工作得更好。它是原始彩色像素之外数据的斜率和曲率的延续。在下面的例子中,我增加了图像的对比度,并在感兴趣的区域指向箭头。请注意,在左侧,值在原始标注栏的任一侧停止增加,这会导致某种可见的夹点,而在右侧,整个区域是连续的。
膨胀代码。请记住,此代码可能不适用于所有类型的UV布局,并且可以通过对相邻的偏移查找代码添加进一步的检查来轻松改进。通过将采样变量更改为float4并使用alpha而不是rgb进行0检查,可以对其进行一些修改以使用常规颜色纹理。采用曲率代替斜率作为输入,可以得到更精确的结果。
//////////////// UV Positional Dilation ///////////////////////////
//** Tex **// Input Texture Object storing Volume Data
//** UV **// Input float2 for UVs
//** TextureSize **// Resolution of render target
//** MaxSteps **// Pixel Radius to search
float texelsize = 1 / TextureSize;
float mindist = 10000000;
float2 offsets[8] = {float2(-1,0), float2(1,0), float2(0,1), float2(0,-1), float2(-1,1), float2(1,1), float2(1,-1), float2(-1,-1)};
float3 sample = Tex.SampleLevel(TexSampler, UV, 0);
float3 curminsample = sample;
if(sample.x == 0 && sample.y == 0 && sample.z == 0)
{
int i = 0;
while(i < MaxSteps)
{
i++;
int j = 0;
while (j < 8)
{
float2 curUV = UV + offsets[j] * texelsize * i;
float3 offsetsample = Tex.SampleLevel(TexSampler, curUV, 0);
if(offsetsample.x != 0 || offsetsample.y != 0 || offsetsample.z != 0)
{
float curdist = length(UV - curUV);
if (curdist < mindist)
{
float2 projectUV = curUV + offsets[j] * texelsize * i * 0.25;
float3 direction = Tex.SampleLevel(TexSampler, projectUV, 0);
mindist = curdist;
if(direction.x != 0 || direction.y != 0 || direction.z != 0)
{
float3 delta = offsetsample - direction;
curminsample = offsetsample + delta * 4
}
else
{
curminsample = offsetsample;
}
}
}
j++;
}
}
}
return curminsample;
推荐阅读