之前一直对图形学的mesh心存疑惑,请教师兄,做了个小demo。
这篇博客使用灵活的可编程管线,基于网格和高度图渲染地形。
思路:先用网格生成算法生成网格,这篇博客生成的是地形是在XZ平面上的,初始的Y值设置为0。在shader里面获取高度图(说到底还是灰度图)的RGB值,修改网格的Y值为正相关于高度图的R值。片段着色器设定阈值,比较它和Y值的大小,高于这个值,纹理都是山;Y值低于阈值,将两张纹理混合,生成火山的效果,低的越多熔岩效果越多。
一、效果图
1、纯火山的效果,阈值设的和高度值差很多
2、混合效果
二、地形数据
本来有个想法就是直接用Qt的Qimage类逐像素地读取出这个高度图,然后push到自己的网格里面,直接生成具有高度的网格数据,这样不用在着色器里面修改高度值了。不过其实这里有个坑,我网上找的图片都是上面图片的jpg格式,其实他本来应该是png格式,被强转了,导致load加载出来的图片是null,后来经过一顿操作,美图秀秀直接转格式(这个不同于改后缀名,强改后缀名没有把里面数据的存放方式改掉,但是美图秀秀保存时候选成png格式却可以)。不过有些图片强转之后确实也可以load加载出来,原因应该是那些图片的像素存放比较有特点,转换前和转换后两种图片格式里面数据的存放方式本来就差不多,当然也就可以load。
最后还是用文章开头讲的思路,在着色器里面修改Y值。
三、 网格算法
两个嵌套for循环生成,没啥神奇的地方。
生成顶点数组的代码(pos+texcoord):
//row_num表示网格行数,col_num表示网格列数
int row_num=200,col_num=200;
std::vector<float> p;
float x=-50,z=-50;
for(int i=0;i<row_num;i++) {
x=-5;
for(int j=0;j<col_num;j++) {
p.push_back(x);
p.push_back(0);
p.push_back(z);
p.push_back(1.0f/col_num*j);
p.push_back(1-i*1.0f/row_num);
x+=0.1;
}
z+=0.1;
}
生成索引数组的代码(绘制的图元是GL_TRIANGLES):
std::vector<unsigned int> indicess;
for(int i=1;i<row_num;i++) {
for(int j=1;j<col_num;j++) {
indicess.push_back((i-1)*col_num+j-1);
indicess.push_back((i-1)*col_num+j);
indicess.push_back(i*col_num+j-1);
indicess.push_back(i*col_num+j-1);
indicess.push_back((i-1)*col_num+j);
indicess.push_back(i*col_num+j);
}
}
四、代码
main.cpp:
...
//创建openGL的窗体代码
...
//将顶点数组(包含了texcoord)和索引数组发到GPU上
...
//加载纹理,创建shader并连接到程序上
unsigned int tex1=loadTexture("rock.jpg");
unsigned int tex2=loadTexture("water_meitu_5.jpg");
unsigned int dep=loadTexture("heightmap.png");
Shader shader("normal.vs","normal.fs");
shader.use();
shader.setInt("dep",0);
shader.setInt("tex1",1);
shader.setInt("tex2",2);
...
//render代码块
glm::mat4 Model,View,Projection;
while (!glfwWindowShouldClose(window))
{
float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
processInput(window);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
shader.use();
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, dep);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, tex1);
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, tex2);
Projection=glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
View=camera.GetViewMatrix();
shader.setMat4("projection", Projection);
shader.setMat4("view", View);
glBindVertexArray(VAO);
Model=glm::mat4(1.0f);
Model=glm::translate(Model, glm::vec3(0.0f, 0.0f, -2.0f));
shader.setMat4("model", Model);
glDrawElements(GL_TRIANGLES,(row_num-1)*(col_num-1)*6, GL_UNSIGNED_INT, 0);
glfwSwapBuffers(window);
glfwPollEvents();
}
顶点着色器代码:
#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec2 texcoord;
uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;
uniform sampler2D tex1;
uniform sampler2D tex2;
uniform sampler2D dep;
out vec2 coord;
out vec3 n_p;
void main()
{
float height=texture(dep,texcoord).r;
vec3 new_pos=vec3(position.x,height*5,position.z);
gl_Position = projection*view*model*vec4(new_pos, 1.0f);
n_p=new_pos;
coord=texcoord;
}
片段着色器代码:
#version 330 core
out vec4 FragColor;
in vec2 coord;
uniform sampler2D tex1;
uniform sampler2D tex2;
uniform sampler2D dep;
in vec3 n_p;
void main()
{
int m=2;
if(n_p.y>m)
{
FragColor=texture2D(tex1,coord);
}
else
{
float alpha=n_p.y/(m); // 比例系数
FragColor=texture2D(tex1,coord)*alpha+texture2D(tex2,coord)*(1-alpha);
}
}
五、问题与解决手法
问题:边缘的网格被拉伸的很夸张。
原因:当采样纹理边缘的时候,OpenGL在边界值和下一个重复的纹理的值之间进行插值运算,而且之前纹理的过滤方式设置的是GL_REPEAT。
手法:纹理过滤方式改为GL_CLAMP_TO_EDGE。
结果变正常了:
有一张高度图就能直接render出地形,真刺激!
六、纹理下载
纹理下载链接 提取码:d4dy