机缘巧合下,遇到了地形渲染上常见的接缝问题,常见类型有如下几种:

  • 图集+双线性插值导致采样结果错误
  • Mipmap跳变,导致两个像素采样得到的值跳变
  • 采样器Point模式采样精度不足,导致在纹素临界处跳变
  • 不同LOD Mesh导致的模型接缝

以上几点在 不帅的 地形5种常见接缝的修复方案 中均有解决方案,而ProjectS遇到的问题,严格来说也可以归类于第二点,不过原因却是有些耐人寻味,请看下文

临时写法造成的意外

出于快速验证方案的目的,我直接通过if-else进行纹理采样,逻辑也很简单,根据不同的index采样不同的纹理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
float4 SampleTexture2D(int index, float2 uv)
{
float2 finalUV = TRANSFORM_TEX(uv, _GrassTex);
if (index == 0)
{
return SAMPLE_TEXTURE2D(_GroundTex, sampler_GroundTex, finalUV);
}
if (index == 1)
{
return SAMPLE_TEXTURE2D(_GrassTex, sampler_GrassTex, finalUV);
}
if (index == 2)
{
return SAMPLE_TEXTURE2D(_RockTex, sampler_RockTex, finalUV);
}
return float4(1, 1, 1, 1);
}

到游戏中猛一看也是没啥问题,但是当我在场景漫游的时候,发现较远处的地形纹理接缝处有问题,如下

基于Index Map采样Material在镜头距离较远的时候两个Material交界处会有走样,非常像常见的mipmap走样

可能并不是特别直观,也不能确定究竟是不是mipmap问题

那么我们把地形纹理都替换成特殊的mipmap检测纹理,在renderdoc看下

非常显眼的亮青色线条具体内容在放大之后查看发现就是mipmap0的纹理,更进一步验证了我们的采样

这就很奇怪了,我确保了所有sampler的tiling offset都是相同,按理说不会出现mipmap走样才对

我甚至一度怀疑是我UV算错了,但通过RenderDoc逐像素debug发现,任意两个像素对应的uv都是连续的

那么为什么会出现mipmap0的线条呢

Mipmap原理

导数,偏导数

我先带大伙梦回大学

  1. 导数(Derivative):
    • 导数是描述函数在某一点的变化率或斜率的概念。对于一元函数,导数表示函数在某一点的瞬时变化率;对于多元函数,导数表示函数在某一点沿着某个方向的变化率。
    • 一元函数 f(x) 在点 x 处的导数可以表示为 f’(x),表示函数 f 在 x 处的变化率。导数可以用极限来定义:f’(x) = lim(h->0) [f(x + h) - f(x)] / h。
    • 导数可以用于求函数的极值点、切线方程、曲线凹凸性等问题。
  2. 偏导数(Partial Derivative):
    • 偏导数是多元函数中的概念,用于描述函数在某一点沿着某个坐标轴的变化率。对于多元函数 f(x, y),其关于某个变量的偏导数表示在其他变量保持不变的情况下,函数沿着某个坐标轴的变化率。
    • 偏导数可以用 ∂f/∂x 或 f_x 表示,表示函数 f 对 x 的偏导数(也可以叫对x求导数);类似地,可以表示函数对 y 的偏导数 ∂f/∂y 或 f_y。
    • 偏导数可以用于求多元函数的梯度、方向导数等问题。

那么显然易见的,屏幕空间两个像素之间的纹素坐标变化量做对数函数得到的就是mipmap的等级,导数越大,mipmap 级别越高(mipmap 尺寸越小)

一图以蔽之

其中D就是最终求得的Mipmap等级

代码示例就是

1
2
3
4
5
6
7
// https://microsoft.github.io/DirectX-Specs/d3d/archive/D3D11_3_FunctionalSpec.htm#7.18.11%20LOD%20Calculations
float MipmapLevelIsotropic(float2 uv, float2 resolution)
{
float2 dx = ddx(uv) * resolution;
float2 dy = ddy(uv) * resolution;
return log2(max(length(dx), length(dy)));
}

那么目前为止看上去也没有什么问题,因为我们使用的是SAMPLER_TEXTURE2D这个API,驱动层会自动帮我们计算Mipmap等级

GPU上的ddx,ddy与分支运行原理

来自 shader-derivative-functions 上的一段话

Derivatives computation is based on the parallel execution on the GPU’s hardware of multiple instances of a shader. Scalar operations are executed with a SIMD (Single Instruction Multiple Data) architecture on registers containing a vector of 4 values for a block of 2×2 pixels. This means that at every step of execution, the shader instances belonging to each 2×2 block are synchronized making derivative computation fast and easy to implement in hardware, being a simple subtraction of values contained in the same register.

But what happens in the case of a conditional branch? In this case, if not all of the threads in a core take the same branch, there is a divergence in the code execution. In the image below an example of divergence is shown: a conditional branch execution in a GPU core with 8 shader instances. Three instances take the first branch (yellow). During the yellow branch execution the other 5 instances are inactive (an execution bitmask is used to activate/deactivate execution). After the yellow branch, the execution mask is inverted and the blue branch is executed by the remaining 5 instances.

In addition to the efficiency and performance loss of the branch, the divergence is breaking the synchronization between the pixels in a block making derivatives operations undefined. This is a problem for texture sampling which needs derivatives for mipmap level selection, anisotropic filtering, etc. When facing such a problem, a shader compiler could flatten the branch (thus avoiding it) or try to rearrange the code moving texture reads outside of the branch control flow. This problem can be avoided by using explicit derivatives or mipmap level when sampling a texture.

GPU分支执行示意图

大意为,GPU以2x2的pixel进行PS处理,所以可以计算ddx和ddy,但如果存在分支,会导致导数计算的不确定性

例如有1,2,3,4,相邻的四个像素,其中1,2在分支A执行,3,4在分支B执行,此时想要获取1,3之间的导数,数值则是不确定的,也就因此造成了本篇文章的问题

解决方案

首先自然是不能有分支,即使我们使用SAMPLE_TEXTURE2D_LOD来手动通过ddx,ddy计算mipmap等级也不行,因为说到底这个事情只是把驱动层做的事情变成手动做,本质问题依旧存在

实现方式也很简单,通过一个一位数组记录到textureArray中的索引,直接映射过去采样即可

https://www.lfzxb.top/ghost-recon-wildlands-terrain-technology-and-tools/中的示例图

参考

shader-derivative-functions

地形5种常见接缝的修复方案