#游戏unity-VR场景漫游#shader之地形纹理合并

一般情况下,在游戏中只是单纯的用单一纹理来平铺地面,看的时间长了,不可避免的会出现眩晕感和疲惫感,还会降低游戏的逼真性。所以,有的时候我们需要用有限的纹理融合在一起,形成多种新的不同的纹理图片。这就是地形纹理合并。


这个shader主要会利用两张纹理。一张自然是包含了9种地形纹理的atlas纹理,就称为BlockMainTex
#游戏unity-VR场景漫游#shader之地形纹理合并_第1张图片
以及一张负责混合纹理的BlendTex:
#游戏unity-VR场景漫游#shader之地形纹理合并_第2张图片
这张纹理是关键所在,它的RG通道存储了该位置处需要混合的两种地形纹理的索引值,它的B通道存储了这两种纹理的混合系数。
最终可以得到类似下面的效果:
#游戏unity-VR场景漫游#shader之地形纹理合并_第3张图片

地形纹理的索引

关键在于混合纹理BlendTex,它的RG通道存储了该位置处需要混合的两种地形纹理的索引值,即每个通道存储了一个索引值。实际上,由于BlockMainTex是按照九宫格来打包了9种纹理,所以这个索引是一个二维的向量(x,y),也就是说把这个二维(x,y)索引值打包进一个0~1的8 bits小数内(通道值的范围)。这主要是靠下面的公式:
这里写图片描述
其中,x和y分别表示在索引对应的行列值(我总是把上面的公式理解成把x编码进了前4个bits,把y编码进了后4个bits)。
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就是0~3的整数索引值除以4的结果,即该种纹理在BlockMainTex中的起始值。拿图中樱花那个block举例,它对应的xy值是(0,8)(由于xy的范围是0~15,而图片索引范围是0~3,所以要乘以4),所以在BlendTex中的颜色就是8/256。

纹理采样

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;

整个地形使用xz平面的世界坐标的小数部分作为采样坐标进行平铺。由于每个block其实只占了1/4的长宽值,所以要进行缩放。为了防止接缝处出现问题,还在两边稍微拉伸了下,即每边拉伸了0.0078125个单位(即1/128个单位):
#游戏unity-VR场景漫游#shader之地形纹理合并_第4张图片
如果直接使用上面的uv0和uv1对纹理采样,那么在地形接缝处会出现明显的问题:
#游戏unity-VR场景漫游#shader之地形纹理合并_第5张图片
这主要是因为这里的纹理tiling是我们手动对worldScale取frac得到的,这样纹理采样坐标的偏导其实是不连续的,而通常我们使用单张纹理的tiling是连续的,是由图形API和硬件帮我们处理平铺类型的。

解决方法也很简单,我们只需要保证在接缝处的偏导连续不突变即可,这可以靠支持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);

其实就是手动算了下采样坐标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截取一下范围。

你可能感兴趣的:(游戏,unity,shader)