[OpenGL] 利用Shader实现复杂地形的渲染

时间:2022-04-14 05:49:59

【更新】我的新博客:www.ryuzhihao.cc

               本文在新博客的链接:点击打开链接

已经好久没写关于OpenGL的博客了。不过昨天晚上,和我一个可爱的小学弟做了一个娱乐程序,也正好用来写一篇博客。

       我们在游戏中经常能见到一座高耸的山,雪线以上是白皑皑的积雪,雪线以下是郁郁葱葱的植被;抑或在某座地狱城探险时,碰见一座被熔岩侵蚀的山谷,在山谷缝隙中是被岩浆烤红的岩石……

       虚拟世界中如此多复杂的地表,我们如何使用OpenGL进行渲染呢。使用一张纹理? 一张与地形契合同时又复杂的纹理图案显然难以搜寻。那么PS得到一张纹理?显然又太过麻烦。

       那么,这篇博客使用OpenGL可编程管线的灵活性,来解决这个问题,去轻松渲染一个复杂的地形图。还是惯例先贴一下我的结果。

我的程序截图:

          [OpenGL] 利用Shader实现复杂地形的渲染

          [OpenGL] 利用Shader实现复杂地形的渲染

          [OpenGL] 利用Shader实现复杂地形的渲染

一、地形数据的处理

       地形数据的来源有很多种方式:Height map、分形、噪声等等。由于这篇博客的重点是纹理的生成,所以这里采用最简单的高度图。上图中地形的三维数据来自如如下的高度图(Height map):

                                                        [OpenGL] 利用Shader实现复杂地形的渲染

        Height map本质是一章灰度图,我们把图片中像素的灰度值作为地形的高度(y),而像素在图片中的位置作为地形的水平坐标(x,y)。然后生成三角网格数据,按照VAO或者VBO进行组织,即可将一章高度图转变为三维地貌。

       将地形数据转换三角网格的代码如下:

// VAO:顶点数组和纹理索引数组
Vector<vec3> m_vertexs;
Vector<vec2> m_textures;

void Terrain::loadTerrain(const String &filename, const String &texName)
{
Image image(filename);

m_vertexs.clear();

for(int i=0; i<image.height()-1; i++) // 0 1 2 |0| 3 4 5 |1|
{
for(int k=0; k<image.width()-1; k++)
{
// 生成顶点数组, 坐标按照三角网格处理 GL_TRIGANLES
vec3 v1(k+0,QColor(image.pixel(k+0,i+0)),i+0);
vec3 v2(k+0,QColor(image.pixel(k+0,i+1)),i+1);
vec3 v3(k+1,QColor(image.pixel(k+1,i+1)),i+1);
vec3 v4(k+1,QColor(image.pixel(k+1,i+0)),i+0);

m_vertexs.push_back(v1);
m_vertexs.push_back(v2);
m_vertexs.push_back(v3);
m_vertexs.push_back(v1);
m_vertexs.push_back(v3);
m_vertexs.push_back(v4);
}
}

// 生成纹理坐标
for(int i=0; i<m_vertexs.size(); i++)
{
vec2 tmp(m_vertexs[i].x()/image.width(),m_vertexs[i].z()/image.height());
m_textures.push_back(tmp);
}
}

二、利用Shader实现纹理混合

       在熔岩地貌的模拟中(截图2),我们使用了下面的两张纹理再着色器中进行渲染:

                                          [OpenGL] 利用Shader实现复杂地形的渲染       [OpenGL] 利用Shader实现复杂地形的渲染

         为了让熔岩出现在山谷部分,而岩石裸露在高处,同时营造出从上而下的灼烧过渡的效果。我们利用简单的插值就可以达到目标。已知高度图的高度数值范围为[0,255],根据某点的高度y坐标来对两张纹理进行融合:

      如果y>140,完全采用岩石纹理;

      如果0<y<140,则将两张纹理进行线性插值,越高岩石所占比例越大,越低岩浆所占比例越大。

      为了实现这一过程,片段shader的代码如下:

uniform sampler2D texture1;
uniform sampler2D texture2;

varying vec3 v_position;
varying vec2 v_texcoord;

void main()
{
if(v_position.y >140)
{
gl_FragColor = texture2D(texture1,v_texcoord);
}
else
{
float alpha = v_position.y/(140); // 比例系数

gl_FragColor = texture2D(texture1,v_texcoord)*alpha
+ texture2D(texture2,v_texcoord)*(1-alpha);
}

}

       借助上述代码,炼狱峡谷渲染的三角网格如下:

             [OpenGL] 利用Shader实现复杂地形的渲染

三、使用蒙版(mask)实现纹理混合

      这里的蒙版,我称之为Mask,就是不知道这样叫标准不标准。

      对于截图1中山顶的积雪,我们并不想让它和前面的方法一样,只出现在某个高度以上。因为现实中的积雪,虽然整体在雪线以上,但是在雪线以下的周围区域,也有少量沿着山脊的过渡积雪。为了实现这样的效果,我们使用第三张纹理来辅助我们的处理。

      我们引入一张黑白纹理(如下图),但是并不将它用来贴图,而是作为一张蒙版,黑色的区域没有积雪,白色的区域将被渲染为积雪区域。

                                                              [OpenGL] 利用Shader实现复杂地形的渲染

        这张纹理,我是直接用PS在前面的高度图上抠出来的,所以积雪区域正好大致分布在山脊处。在我的代码中,我利用蒙版纹理的灰度大小,作为积雪深浅的影响因子,越亮积雪越明显,越浅积雪越薄,这样也能营造出一种自然地过渡效果。

       其片段shader的代码如下:

uniform sampler2D texture1;
uniform sampler2D texture2;
uniform sampler2D mask;

varying vec3 v_position;
varying vec2 v_texcoord;

void main()
{
vec4 color;

if(v_position.y >140) // 高于140为岩石
{
color = texture2D(texture1,v_texcoord);
}
else // 低于140为草地,但是有过渡
{
float alpha = v_position.y/(140);

color = vec4(texture2D(texture1,v_texcoord)*(alpha+0.2)
+ texture2D(texture2,v_texcoord)*(1-alpha));
}

// 积雪区域的处理(借助蒙版)
float alpha = texture2D(mask,v_texcoord).r;
gl_FragColor = color*(1-alpha)+vec4(1.0,1.0,1.0,1.0)*alpha;
}