[中英] Terrain texture merging
图文教程技术文章技术文库地编场景制作
显示全部 10
2860 1
实名

通过了实名认证的内容创造者

发布于 2021-10-1 19:17:00

您需要 登录 才可以下载或查看,没有账号?注册

x
本帖最后由 雨の日曜日 于 2021-10-1 20:20 编辑

written by:Lele Feng

Write in front

Some time ago, a friend came to me to analyze a terrain shader. For a long time, I mainly looked at the texture merging part inside. At present, the common terrain of unity should be t4m. It is said that only four textures can be packaged, that is, four textures can be painted on the terrain. If there are more, it is not very convenient. The method in this shader can package 9 textures, and then control the mixing by a mixed texture. It feels very clever.

写在前面
前一段时间有个朋友找我分析一个地形的shader,代码很长就主要看了下里面的纹理合并的部分。Unity目前常见的地形应该是T4M的做法,据说是只支持打包4张纹理,也就是说可以在地形上刷4种纹理,多了的话就不太方便了。而这个shader中的方法可以打包9张纹理,然后靠一张混合纹理来控制混合,感觉还挺巧妙的。


method
This shader mainly uses two textures. Naturally, an atlas texture containing 9 terrain textures is called blockmaintex:

方法
这个shader主要会利用两张纹理。一张自然是包含了9种地形纹理的atlas纹理,就称为BlockMainTex吧:

640 8.jpg


And a blendtex for blending textures:

以及一张负责混合纹理的BlendTex:

640.jpg


This texture is the key. Its RG channel stores the index values of the two terrain textures to be mixed at this position, and its B channel stores the mixing coefficients of the two textures. The following is the RGB channel diagram in the above figure:

这张纹理是关键所在,它的RG通道存储了该位置处需要混合的两种地形纹理的索引值,它的B通道存储了这两种纹理的混合系数。下面是上图的RGB通道图:



We can finally get effects similar to the following:

我们最终可以得到类似下面的效果:



It can be seen that we can brush up to 9 different terrain textures with one draw call + two textures.

可以看出来,我们可以用一个draw call+两张纹理刷出至多9种不同的地形纹理。

Index of terrain texture
It can be seen that the key lies in the mixed texture blendtex. Its RG channel stores the index values of the two terrain textures to be mixed at this position, that is, each channel stores an index value. In fact, since blockmaintex packs nine textures according to the ninth house grid, the index is a two-dimensional vector (x, y), that is, pack the two-dimensional (x, y) index value into an 8 bits decimal of 0 ~ 1 (the range of channel values). This mainly depends on the following formula:

地形纹理的索引
都可以看出来关键在于混合纹理BlendTex,它的RG通道存储了该位置处需要混合的两种地形纹理的索引值,即每个通道存储了一个索引值。实际上,由于BlockMainTex是按照九宫格来打包了9种纹理,所以这个索引是一个二维的向量(x,y),也就是说把这个二维(x,y)索引值打包进一个0~1的8 bits小数内(通道值的范围)。这主要是靠下面的公式:

Where, X and Y respectively represent the row and column values corresponding to the index (I always understand the above formula as encoding x into the first four bits and Y into the last four bits).

其中,x和y分别表示在索引对应的行列值(我总是把上面的公式理解成把x编码进了前4个bits,把y编码进了后4个bits)。

The above is the encoding process. The relevant formula of decoding is:

上面是编码的过程,解码的相关公式就是:

Shader代码对应:
float2 encodedIndices = tex2D(_BlendTex, i.uv).xy;

float2 twoVerticalIndices = floor((encodedIndices * 16.0));
float2 twoHorizontalIndices = (floor((encodedIndices * 256.0)) - (16.0 * twoVerticalIndices));

float4 decodedIndices;
decodedIndices.x = twoHorizontalIndices.x;
decodedIndices.y = twoVerticalIndices.x;
decodedIndices.z = twoHorizontalIndices.y;
decodedIndices.w = twoVerticalIndices.y;
decodedIndices = floor(decodedIndices/4)/4;

Decodedindices is the result of dividing the integer index value of 0 ~ 3 by 4, that is, the starting value of this texture in blockmaintex. Take the cherry blossom block in the figure for example. Its corresponding XY value is (0, 8) (because the XY range is 0 ~ 15 and the image index range is 0 ~ 3, it needs to be multiplied by 4), so the color in blendtex is 8 / 256.

decodedIndices就是0~3的整数索引值除以4的结果,即该种纹理在BlockMainTex中的起始值。拿图中樱花那个block举例,它对应的xy值是(0,8)(由于xy的范围是0~15,而图片索引范围是0~3,所以要乘以4),所以在BlendTex中的颜色就是8/256。



Texture sampling
Knowing the indexes of the two terrain textures, it's time to sample them.

纹理采样
知道了两张地形纹理的索引,就该对它们进行采样了。
float2 worldScale = (worldPos.xz * _BlockScale);
float2 worldUv = 0.234375 * frac(worldScale) + 0.0078125; // 0.0078125 ~ 0.2421875, the range of a block

float2 uv0 = worldUv.xy + decodedIndices.xy;
float2 uv1 = worldUv.xy + decodedIndices.zw;

The whole terrain is tiled using the decimal part of the world coordinates of the XZ plane as the sampling coordinates. Since each block actually accounts for only 1 / 4 of the length and width value, it needs to be scaled. In order to prevent problems at the joint, stretch slightly on both sides, i.e. 0.0078125 units (i.e. 1 / 128 units) on each side:

整个地形使用xz平面的世界坐标的小数部分作为采样坐标进行平铺。由于每个block其实只占了1/4的长宽值,所以要进行缩放。为了防止接缝处出现问题,还在两边稍微拉伸了下,即每边拉伸了0.0078125个单位(即1/128个单位):



Treatment of joints
If the above uv0 and uv1 are directly used to sample the texture, there will be obvious problems at the terrain joints:

处理接缝
如果直接使用上面的uv0和uv1对纹理采样,那么在地形接缝处会出现明显的问题:



This is mainly because the texture tiling here is obtained by frac of Worldscale manually, so the partial derivation of texture sampling coordinates is actually discontinuous. Generally, the tiling of a single texture is continuous, and the graphics API and hardware help us deal with the tiling type.

这主要是因为这里的纹理tiling是我们手动对worldScale取frac得到的,这样纹理采样坐标的偏导其实是不连续的,而通常我们使用单张纹理的tiling是连续的,是由图形API和硬件帮我们处理平铺类型的。

The solution is also very simple. We only need to ensure that the partial derivative at the joint is continuous without mutation, which can be solved by tex2d function supporting four parameters. The complete code is as follows:

解决方法也很简单,我们只需要保证在接缝处的偏导连续不突变即可,这可以靠支持4个参数的tex2D函数来解决。完整的代码如下:

float blendRatio = tex2D(_BlendTex, i.uv).z;

float2 worldScale = (worldPos.xz * _BlockScale);
float2 worldUv = 0.234375 * frac(worldScale) + 0.0078125;
float2 dx = clamp(0.234375 * ddx(worldScale), -0.0078125, 0.0078125);
float2 dy = clamp(0.234375 * ddy(worldScale), -0.0078125, 0.0078125);

float2 uv0 = worldUv.xy + decodedIndices.xy;
float2 uv1 = worldUv.xy + decodedIndices.zw;
// Sample the two texture
float4 col0 = tex2D(_BlockMainTex, uv0, dx, dy);
float4 col1 = tex2D(_BlockMainTex, uv1, dx, dy);
// Blend the two textures
float4 col = lerp(col0, col1, blendRatio);

In fact, the DDX and ddy of the lower sampling coordinate Worldscale are calculated manually, which is why each block has to stretch 0.0078125 units to each side before, so that the sampling will not cross the border. When calculating DDX and ddy, the results are also intercepted to (- 0.0078125, 0.0078125), i.e. (1 / 128, - 1 / 128). I guess this is because when the camera is very far from the ground (at this time, the absolute values of DDX and ddy will be relatively large and the texture density will be very large), if the absolute value of DDX or ddy exceeds the tensile value of 0.0078125 (1 / 128), The next block will be sampled at the seam, so you need to use clamp to intercept the range. The following figure shows the difference between before and after the interception range. I need to thank MR.Jim  in the comment area. I only considered positive numbers before, but did not consider negative values, which is incorrect (well, in fact, it is also wrong in an online game...).

其实就是手动算了下采样坐标worldScale的ddx和ddy,这也是为什么之前每个block要向每边拉伸了0.0078125个单位,这样才不会采样越境。上面在算ddx和ddy的时候,还把结果截取到(-0.0078125,0.0078125)即(1/128,-1/128)之间,我猜想这是为了在摄像机距地面非常的远的时候(此时ddx和ddy的绝对值会比较大,纹素密度很大),如果ddx或ddy的绝对值超过了拉伸值0.0078125(1/128),就会在接缝处采样到隔壁的block,所以要在这里使用clamp截取一下范围,下图显示了截取范围前后的区别。在此需要感谢评论区的jim童鞋,我之前只考虑到了正数的情况,没有考虑负值,这是不正确的(额这么说来其实某个上线游戏里也是不对的…)。











本帖被以下画板推荐:

自然选择,前进四!
使用道具 <
小裴天使  发表于 2022-8-16 14:55:46  
2#
厉害
回复 收起回复
使用道具
您需要登录后才可以回帖 登录 | 注册

本版积分规则

快速回复 返回顶部 返回列表