简介
有时候我们在游戏,会看到类似这样裸露的土地、沼泽之类的:

这是游戏《自然之力》的地图,在草地上会有“粘土”、“黑土”这样裸露的区域,玩家能够在这片区域挖土,不同的材料可以制作不同的道具。
那这个是怎么做的呢?
- 最粗暴的办法,就是画一个这样的纹理,贴到地形上
- 还有一种就是使用几种不同的纹理对地形进行混合贴图
比如,unity中刷地形的时候,就是使用第二种办法,事先加载好纹理,再使用“刷子”在地形上画对应的纹理贴图。

这节我们使用代码来实现这样的效果。
原理
原理很简单,就是取不同的纹理贴图的颜色,使用一张混合贴图将这些颜色进行混合。后面我们结合代码来讲具体的过程。

实现
实现好的效果是这样的。

首先我们写一个多重纹理类来保存我们需要使用到的纹理:


接着,我们在片段着色器中计算混合的纹理颜色:

我们想要实现的效果是,在一片绿油油的草地上,有裸露的土地和道理,那么地形的整体背景应该是“草”,对应混合贴图 blendMap 中的黑色背景。 为啥是黑色背景,而不是透明背景? 因为黑色背景 r=g=b=0 这样,我们计算的时候就比较方便了,因为任何颜色值乘以0还是0,这样就很容易过滤掉其他的颜色。
接下来我们逐行代码解释:
1// pass_textureCoordinates 为整个地形块的纹理坐标
2// 对混合贴图 blendMap 采样也使用这个纹理坐标,因为 混合贴图代表着最终地形的贴图是啥样的
3vec4 blendMapColor = texture(blendMap, pass_textureCoordinates);
1// 过滤掉混合贴图中的其他颜色,主要是为了贴地形的背景纹理
2float backTextureAmount = 1 - (blendMapColor.r + blendMapColor.g + blendMapColor.b);
看在ps中使用颜色拾取器,查看混合贴图中每个像素的颜色,可以发现这个像素要么是纯色的,要么是两种颜色的混合。注意,这里的像素值是0-255,在片段着色器中是0-1,就是除以了255归一化了而已。


根据这个,我们可以发现,混合贴图中任意一点的像素r,g,b通道的值加起来不会超过255(在片段着色器中不会超过1)。
那么backTextureAmount计算的就是除去混合贴图中某点像素值的r,g,b通道值,用这个差值作为地形的背景混合因子。
这也是接下来代码做的事情。
1vec2 tileCoords = pass_textureCoordinates * 40.0f;
2vec4 backgroundTextureColor = texture(backgroundTexture, tileCoords) * backTextureAmount;
是不是很奇怪,为啥这里把地形的纹理采样坐标pass_textureCoordinates乘以40。因为我们的地形贴图很小啊,地形很大啊,如果使用地形的采样坐标去采样贴图的画,相当于把一张小贴图拉到地形那么大,那么你会看到什么呢?
对啊,那就糊了啊。

但是你可能会说,那乘以40不就超过1了嘛,纹理采样坐标不是0-1嘛。
对啊,你说滴对。
但是,你忘了之前有个文章说到纹理环绕模式了吗? 超过1的纹理坐标会根据加载纹理时设置的纹理环绕模式进行采样的。
你看,乘以40之后采样的纹理很清晰。

1vec4 rTextureColor = texture(rTexture,tileCoords) * blendMapColor.r;
2vec4 gTextureColor = texture(gTexture,tileCoords) * blendMapColor.g;
3vec4 bTextureColor = texture(bTexture,tileCoords) * blendMapColor.b;
4
5vec4 totalColor = backgroundTextureColor + rTextureColor + gTextureColor + bTextureColor;
接下来,我们分别使用 混合地图中的 每个通道的颜色值作为混合因子,使用地形贴图 比如 泥土、道路贴图 作为对应的采样贴图,进行颜色计算。
最后将几种颜色加起来作为最终的输出颜色。
由于这里用到了5张纹理,1张混合贴图+4张地形贴图,所以,我们在渲染的时候,需要开启4个纹理单元:

最后,我们在生成地形时指定对应的贴图纹理即可。

最后结果,前面已经看到了。
好了,结束。
其实很简单有没有。
欢迎大家关注我的公众号【OpenGL编程】。
分享3D编程的心得、算法,包括:OpenGL、Shader编程、WebGL(Three.js)等,一起走进3D编程的世界。