Unity Shader 关于tex2D中 dx dy 的猜想

问题

Unity Shader-地形纹理合并文章里面有提到一个黑线问题. 其实在OpenGL ES3.0以后直接用Texture2DArray就可以避免了. 不过我又犯贱研究了一下为何在Atlas里面会有这个问题和解决方案.

里面内容涉及还是挺多的,资料查了很多 但是没有找到很官方的解释. 所以我只能说一下我的猜想具体是否正确 目前以我所知的知识还无法考证☹️

为何有黑线

这个问题还不好一下说清楚,所以要分几步来说明一下

UV采样

在纹理的设置中分别对应了三种采样方式:

  • Point点采样-无滤波
  • Billnear双线性插值
  • Trillinear三线性插值.

那这些到底是什么意思呢?

3D游戏编程大师技巧 下册 P560 9.6基本采样理论中专门说明了这个问题.

我这里就简单说下我的理解, 重复说一遍没啥意义.大神已经给解释的很清楚.

Point

点采样方式,文章中首先举出的例子就是 一维纹理的采样.

比如有一个一维纹理.分别要采样到一个1x4的mesh上和一个1x14的mesh上

两者的采样率分辨为 8/4 =2 和 8/14 = 0.57 . 根据采样率映射到的 1x14上的采样点如下图. 再向下取整. 从而得到最终的像素点.

Billnear 双线性插值

比如上面的例子 对于样本12 7.41 直接使用7号位置的像素值 显然不准确. 所以不如

最终采样结果 = 7号样本的59%(1-0.41) + 8号样本的41%

这样采样来的准确.

同理将此逻辑推广到二维坐标系下就可以同时根据4个像素点进行采样渲染. 逻辑如图:

补充:

Link



Trillinear 三线性插值

这里涉及到的原理有些深奥了. 我自己也没太弄明白. 具体的可以参考

说下我目前有可能理解到的部分:

  • 走样问题是肯定会出现的,使用Mipmapping进行滤波可以延缓其出现的时间
  • 三线性插值是在 双线性插值插值的基础上 同时参考了多张Mipmapping

纹理走样如图(未开Mip):

再谈黑线问题

知道了UV如何采样,再回过头来很黑线问题. 黑线问题应该是因为在边界处采样时候 GPU “用错了” Mipmapping导致的.

那这又引出另外一个问题,GPU如何知道该用哪张Mipmapping去采样呢?

ddx ddy

恩,引出本文主题之前 还有一个概念需要交代一下就是. 现在显卡进行采样时候 都会同时取出相邻的四个fragment. 看到的资料是说 因为这么做很容易.

所以在fragment shader中我们可以通过 ddx(a),ddy(a) 取得周围临近fragment和当前fragment的差值.

注意一点对 ddx,ddy的理解就是.

这个是在取周围fragment的数据,至于这个数据是uv还是normal还是pos那是你自己需要关心的. 这个函数不会做处理.

因为取得的这个相差值所在的两个点距离很小,所以我们也近似的认为ddx(a),ddy(a)取得是a数据在当前fragment的导数(或者是叫偏导数?)

ddx ddy的用处

那这东西取出来有什么用呢?

  • 确定采样时候使用哪个 Mipmapping
  • 在fragment中求的法线
  • 图片锐化

求Mipmapping后面再讲,先说取法线问题.

查考An introduction to shader derivative functions这篇文章即可. 注意看他的那个3D模型

图片锐化问题 可以看:

HLSL ddx / ddy

tex2D dx dy

再返回头来看黑线问题, Unity Shader-地形纹理合并文章中是使用的

四参数tex2D函数进行的采样

1
2
float4 col0 = tex2D(_BlockMainTex, uv0, dx, dy);
float4 col1 = tex2D(_BlockMainTex, uv1, dx, dy);

双参数的函数根据MipMap的LOD实现原理中所提到的是可以拆解为

1
2
3
4
5
6
tex2D(sampler2D tex, float4 uv)
{
float lod = CalcLod(ddx(uv), ddy(uv));
uv.w= lod;
return tex2Dlod(tex, uv);
}

所以四参数的据我猜想应该可以理解为

1
2
3
4
5
6
tex2D(sampler2D tex, float4 uv, float2 dx, float2 dy)
{
float lod = CalcLod(dx, dy);
uv.w= lod;
return tex2Dlod(tex, uv);
}

也就是指定好dx,dy 无需再用 ddx 和 ddy 去计算了.

Atlas Texture导致的偏导错误

说完上面这一坨理论知识,终于可以说文章的重点了.🙃

那在Atlas Texture中的黑线是怎么来的呢

引用Unity Shader-地形纹理合并文章中的话

如果直接使用上面的uv0和uv1对纹理采样,那么在地形接缝处会出现明显的问题:
这主要是因为这里的纹理tiling是我们手动对worldScale取frac得到的,这样纹理采样坐标的偏导其实是不连续的,而通常我们使用单张纹理的tiling是连续的,是由图形API和硬件帮我们处理平铺类型的。

同时参考文章 What are screen space derivatives and when would I use them

在单张纹理时候,将Wrap Mode选为 Repeat . 遇到uv超过1时候

比如横向排列的一组uv坐标为

(0,0.3),(0,0.7),(0,0.9),(0,1.1),(0,1.5)

GPU是知道如何处理uv采样的,因为是Repeat模式,所以会采样到当前纹理左侧部分的像素.

但是在Atlas Texture中 Wrap Mode只能选择Clamp. 并且对于(0,1.1)会首先frac掉整数,把当前点的uv变为0.1

假设

A (0,0.7) B (0,0.9) C (0,1.1)

是三个连续的片元. 那

A-B = 0.7 - 0.9 = -0.2
B-C = 0.9 - 0.1 = 0.8

B-C此处的变化率应该仍旧是 -0.2. 而-0.2 我们假设对应的是最高等级的纹理. 但是0.8 就可能对应的是经过Mipmapping压缩过的纹理了.所以在这个点上的片元就出现了

使用错Mipmapping的问题 应该使用大纹理时候使用了小纹理.

而小纹理由于尺寸缩小,所以边界采样时候就发生了越界. 也就是看到的黑线

验证

使用一张1024的纹理,中间32x32大小区域为绿色,其余区域均为黑色. 纹理的Wrap Mode选为Clamp. 相机贴近地表 看向远处.

构建一个100x100的Quad平铺,Quad纹理取值范围为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var uvMin = (512 - pixelValue) / 1024f;
var uvMax = (512 + pixelValue) / 1024f;

for (var xIndex = 0; xIndex < xNum; xIndex++)
{
for (var yIndex = 0; yIndex < yNum; yIndex++)
{
...
uv[startIndex] = new Vector2(uvMin, uvMin);
uv[startIndex + 1] = new Vector2(uvMax, uvMin);
uv[startIndex + 2] = new Vector2(uvMin, uvMax);
uv[startIndex + 3] = new Vector2(uvMax, uvMax);
...
}
}

GUI中拖动滑块调整pixelValue的取值范围

Mid Off:

Mid Off

Mid On:

Mid On

可以看出 在打开了Mipmapping后,地表过早的出现了黑线,并且随着地图渐远黑线越发的明显

解决方案

Unity Shader-地形纹理合并 文中给出的解决方案是分两步

首先是每个纹理周围留出1个素文(texel)的拉伸,然后把dx,dy clamp到+- 0.0078125 也就是其文章中使用的1个素文范围内.

我不是很理解这么做的目的,因为这样确实可以”解决”裂缝的问题,但是解决的思路其实和 直接关闭 Mipmapping 没有任何区别. 因为Clamp到一个素文宽度 会导致CalcLod时候总选择最大的那张纹理.

我自己也尝试写了一下那个Shader,不过因为我是单张纹理Clmap的,不需要那么复杂的计算uv所以直接

1
2
3
4
5
6
7
8
9
fixed4 frag(v2f i) : SV_Target 
{
//0.0009765625
float2 dx = clamp(ddx(i.uv), -0.0009765625, 0.0009765625);
float2 dy = clamp(ddy(i.uv), -0.0009765625, 0.0009765625);
fixed4 c = tex2D(_MainTex,i.uv,dx,dy);

return fixed4(c.rgb, 1.0);
}

得到的结果和我关闭Mipmapping以后的结果一样.

疑惑

因为我Terrain准备直接使用Texture2DArray去做,所以也就不打算再深入研究下去了. 有几个地方还是没有弄清楚

ddx(a),ddy(a) 查到的资料部分显示 其实现的方式是直接从临近的fragment中取得值,也就是不是通过计算得到的偏导数.

我尝试了一下直接写ddx(1.23f),按说这样写应该是直接报错的. 但是我是可以编译过去.转换后的代码类似于:

vec4(vec4(1.23, 1.23, 0.0, 0.0)).xy

但确实是实现时候没有调用dFdy(a),dFdx(a). (如果ddx(uv)等变量时候 是会转化为dFdy和dFdx的)

所以不知道ddx ddy 是否真的是从周围fragment中”取” 数据, 而不是在计算.

另外一个没有搞懂的就是Unity Shader-地形纹理合并文章中clamp一下dx,dy的和直接关闭Mipmapping的区别.


补充说明

后来又研究了一下Mip问题 有一些收获

MIP Banding问题

关于 Billnear 和 Trillinear 还涉及到一个 MIP Banding问题. 相关资料:

Will “brilinear” filtering persist?
amd_radeon_hd_6800_reloaded

MIP Banding现象:

去除后效果:

解释理解了大概,应该和之前猜想的一样. 因为双线性插值没有在多个Mip之间混,所以当出现Mip切换时候 会有明显”条带”现象. 但是三线性插值因为参考了多找Mip.所以不会出现这种现象.

同时Will “brilinear” filtering persist?这篇文章也提到了 走样的原因. 和之前理解的也应该一样

If the next larger MIP texture is applied, texture shimmering (so called aliasing) would appear, since the texture would be scanned already too coarse meshed with four samples per color value. Here the first disadvantage occurs, if you filter from only one MIP map: The “Level of Detail” has to be determined in such a way, that in any case aliasing is avoided, cause no one would (of course) accept texture shimmering. Distributed over the polygon, the pixels get more or less texture details depending upon that.

Brilinear 伪三线性插值

之前Brilinear将这个词理解为双线性插值应该是不准确的. 目前Unity中的Brilinear应该已经是伪三线性插值了.

经典双线性插值

三线性插值

Brilinear伪三线性插值

Will “brilinear” filtering persist?

“Brilinear” filtering likewise interpolates textures to suppress MIP banding. The tri-band is significantly reduced though. Within broad ranges solely bilinear filtering is used. Please keep in mind that “brilinear” is just an artificial word without any official use. “Pseudo-trilinear” sounds more technical in the first instance but it describes the filter just as insufficient. The effect shall be illustrated with images by imitating colored MIP maps with a paint application.

Link

  • Have you tried the brilinear cheat?

  • I am not familiar with this. Can you explain?

  • You’re not doing your own texture filtering in the shader, so it’s not really applicable. But this is an ancient NVidia trick to save bandwidth when doing trilinear interpolation. Basically, instead of always sampling 2 MIPs and blending between them, you only sample 2 MIPs for like the middle 33% between two MIPmaps and do a fast blend into and out of that region. When you’re less than 33% away from a MIPmap, you just clamp to the result that MIPmap gives you and don’t even sample the other MIP. Results in lower texture filtering quality (blurring/aliasing), but saves bandwidth.

坚持原创技术分享,您的支持将鼓励我继续创作!