欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-03-26 10:36:29
...

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


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

地形纹理的索引

关键在于混合纹理BlendTex,它的RG通道存储了该位置处需要混合的两种地形纹理的索引值,即每个通道存储了一个索引值。实际上,由于BlockMainTex是按照九宫格来打包了9种纹理,所以这个索引是一个二维的向量(x,y),也就是说把这个二维(x,y)索引值打包进一个0~1的8 bits小数内(通道值的范围)。这主要是靠下面的公式:
#游戏unity-VR场景漫游#shader之地形纹理合并
其中,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之地形纹理合并
如果直接使用上面的uv0和uv1对纹理采样,那么在地形接缝处会出现明显的问题:
#游戏unity-VR场景漫游#shader之地形纹理合并
这主要是因为这里的纹理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截取一下范围。