作业要求:
- 材料见附件 TerrainEngine 文件夹。
- 摄像机坐标系与全局坐标系之间的变换;
- 海面的波浪效果;
- 地形的读取、绘制及纹理贴图;
- 天空和地形的倒影效果。
建立统一管理所有渲染元素的TerrainEngine
对象,它包括几方面:天空盒的VAO、VBO等,地形网格的zyMesh
对象(zyMesh
见前面的作业),以及所有对应的纹理,着色器等。如下所示:
class TerrainEngine {
private:
GLuint skyBox_VAO, skyBox_VBO;
float cloud_speed = 0.01;
GLuint skyBox_Textures[5];
Shader skyBox_Shader;
Shader water_Shader;
float water_speed = 0.3, water_alpha = 0.56, water_scale = 0.3;
GLuint water_Texture;
zyMesh land;
GLuint land_Texture, detail_Texture;
public:
glm::vec3 skyboxSize = glm::vec3(500.0f, 210.0f, 500.0f);
static const GLsizei skyBox_verts_num = 36, skyBox_attrib_stride = 5;
static const float cubeVertices[skyBox_attrib_stride * skyBox_verts_num];
glm::mat4 skyBox_model;=
glm::vec3 landSize= glm::vec3( 30.0f, 7.0f, 30.0f);
glm::mat4 land_model;
Shader land_Shader;
... ...// 函数成员
};
天空盒
作业提供了5张图作为立方盒的贴图(底部除外),但由于这5张图与底部的水波纹理图分辨率不一致,不能使用OpenGL提供的Skybox功能。而且后者也是静态的,无法模拟要求的水波效果。所以我们手动使用5个2D纹理,并将它们映射到立方体的内面上。
立方体参数
手动设置立方体每个顶点的位置坐标和纹理坐标
const float TerrainEngine::cubeVertices [] = {
// positions // texture Coords
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, // back
0.5f, -0.5f, -0.5f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 1.0f, // right
0.5f, 0.5f, -0.5f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 0.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 1.0f, 0.0f, // front
0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f,
-0.5f, 0.5f, 0.5f, 1.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f, // left
-0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 1.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 1.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, // top
0.5f, 0.5f, -0.5f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 1.0f, 1.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f, // bottom: water
0.5f, -0.5f, -0.5f, 1.0f, 1.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
};
并在构造函数中手动设置立方体的VAO、VBO,如下所示:
TerrainEngine::TerrainEngine(std::string skybox_vs, std::string skybox_fs, std::string water_vs, std::string water_fs,\
std::string land_vs, std::string land_fs): \
skyBox_Textures{0}, skyBox_Shader(skybox_vs.c_str(), skybox_fs.c_str()), water_Shader(water_vs.c_str(), water_fs.c_str()),\
land_Shader(land_vs.c_str(), land_fs.c_str()) {
glGenVertexArrays(1, &skyBox_VAO);
glGenBuffers(1, &skyBox_VBO);
glBindVertexArray(skyBox_VAO);
glBindBuffer(GL_ARRAY_BUFFER, skyBox_VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(cubeVertices), cubeVertices, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);// position
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, skyBox_attrib_stride * sizeof(float), (void*)0);
glEnableVertexAttribArray(1);// texture coords
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, skyBox_attrib_stride * sizeof(float), (void*)(3 * sizeof(float)));
// unbind VBO and VAO
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
... ...
}
立方体纹理
对于天空盒前、后、左、右、上的五张纹理图,加载方式同C-4作业,借助stb_image
库。但需要注意,为了消除天空盒相邻面的贴图在拼接时产生的不自然的边缘
需要将纹理的WRAP模式参数设置为GL_CLAMP_TO_EDGE
,以将颜色拓展到边缘。
void TerrainEngine::load_textures(std::vector<std::string> skyboxFiles, std::string waterFiles,\
std::string landFiles, std::string detailFiles, std::string heightMapFiles) {
assert(skyboxFiles.size() == 5);
for (unsigned i = 0; i < skyboxFiles.size(); i++) {
skyBox_Textures[i] = load_single_texture(skyboxFiles[i].c_str(), GL_CLAMP_TO_EDGE);
assert(skyBox_Textures[i] != 0);
}
... ...
}
unsigned int load_single_texture(char const * path, GLuint WRAP_MODE)
{
unsigned int textureID;
glGenTextures(1, &textureID);
int width, height, nrComponents;
unsigned char *data = stbi_load(path, &width, &height, &nrComponents, 0);
if (data) {
GLenum format;
if (nrComponents == 1)
format = GL_RED;
else if (nrComponents == 3)
format = GL_RGB;
else if (nrComponents == 4)
format = GL_RGBA;
glBindTexture(GL_TEXTURE_2D, textureID);
glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, WRAP_MODE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, WRAP_MODE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
stbi_image_free(data);
} else {
std::cout << "Texture failed to load at path: " << path << std::endl;
stbi_image_free(data);
}
return textureID;
}
渲染
因为有5张不同的纹理,所以需要将立方体分5次渲染,每次渲染前都要激活相应的纹理单元并绑定纹理,如下所示:
void TerrainEngine::draw_skybox(glm::mat4 const & model, glm::mat4 const & view, glm::mat4 const & proj, float deltaTime) {
static float x_shift = 0, y_shift = 0;
x_shift += deltaTime;// * cloud_speed;
y_shift += deltaTime;// * cloud_speed * 0.8;
glm::vec3 transVec = glm::vec3(cloud_speed) * glm::vec3(cos(x_shift), 0.0, cos(y_shift));
skyBox_Shader.use();
skyBox_Shader.setMat4("model", glm::translate(model, transVec));
skyBox_Shader.setMat4("view", view);
skyBox_Shader.setMat4("projection", proj);
skyBox_Shader.setInt("texture", 0);
glBindVertexArray(skyBox_VAO);
for (unsigned i = 0; i < 5; i++) {
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, skyBox_Textures[i]);
glDrawArrays(GL_TRIANGLES, i*6, 6);
glBindTexture(GL_TEXTURE_2D, 0);//unbind texture
}
glBindVertexArray(0);//unbind VAO
}
特别地,为了造成天空中的云彩在飘动的“错觉”,在每一帧中设置适当的x和y方向的偏移量x_shift
和y_shift
,加到顶点着色器的model
矩阵中,以使世界空间有小幅度的平移。看上去就像是天空的云在移动。
水波
水波的渲染有两个要求,一方面是造成水波在移动的感觉,另一方面是天空和地形在水波里的倒影绘制。
水波纹理
水波的纹理贴图同上理加载,只不过需要将WRAP模式参数设置成GL_REPEAT
,以得到视觉上“连绵不断”的水波。
void TerrainEngine::load_textures(std::vector<std::string> skyboxFiles, std::string waterFiles,\
std::string landFiles, std::string detailFiles, std::string heightMapFiles) {
... ...
water_Texture = load_single_texture(waterFiles.c_str(), GL_REPEAT);
assert(water_Texture != 0);
... ...
}
倒影绘制
只需要将天空盒skybox内的五个面和地形,在y轴上反转方向,重新画一次即可。
void TerrainEngine::draw_water(glm::mat4 const & view, glm::mat4 const & proj, Camera const & camera, float deltaTime) {
const static glm::mat4 mirror_y({
{1, 0, 0, 0},
{0, -1, 0, 0},
{0, 0, 1, 0},
{0, 0, 0, 1}
});// y轴取为相反(镜面对称)
const static glm::mat4 mirror_y_skybox_model = mirror_y * skyBox_model;
const static glm::mat4 mirror_y_land_model = mirror_y * land_model;
draw_skybox(mirror_y_skybox_model, view, proj);// 在y轴的相反方向再画一次天空盒
draw_land(mirror_y_land_model, view, proj, false);// draw a mirrored terrain, y of "world up" should be -1
... ...
}
水波移动
这个与前述的天空背景移动的原理略有不同。在每一帧中计算x和y方向(实际上是x和z方向)的偏移量x_shift
和y_shift
,通过水波速度water_speed
调整一下大小,然后传给顶点着色器中,修改相应的纹理坐标。所以实际上每个顶点获取的颜色值在每帧中是不一样的,从而看似底部的水面在“移动”。如下所示,
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
out vec2 TexCoord;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
uniform float xShift;
uniform float yShift;
void main() {
gl_Position = projection * view * model * vec4(aPos, 1.0f);
// 将texture重复50倍
vec2 scaledCoord = 50.0f * aTexCoord;
TexCoord = vec2(scaledCoord.x + xShift, scaledCoord.y + yShift);
}
因为天空和地形“倒影”的存在,它们在此前已经被绘制了,所以水波的绘制需要禁用深度缓冲的写入,以免“倒影”的片段被渲染管线丢弃。同时启用混合(Blending),并设置混合函数为GL_ONE_MINUS_SRC_ALPHA
,使得水面以下的部分同时展现水波和天空、地形的纹理。
void TerrainEngine::draw_water(glm::mat4 const & view, glm::mat4 const & proj, Camera const & camera, float deltaTime) {
... ...
static float x_shift = 0, y_shift = 0;
x_shift += deltaTime * water_speed;
y_shift += deltaTime * water_speed * 0.8;
glDepthMask(GL_FALSE);//使用只读的(Read-only)深度缓冲(禁用深度缓冲的写入)
glEnable(GL_BLEND);//启用混合(Blending)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
water_Shader.use();
water_Shader.setMat4("model", skyBox_model);
water_Shader.setMat4("view", view);
water_Shader.setMat4("projection", proj);
water_Shader.setFloat("xShift", water_scale * sin(x_shift));
water_Shader.setFloat("yShift", water_scale * sin(y_shift));
water_Shader.setFloat("water_alpha", water_alpha);
water_Shader.setInt("texture", 0);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, water_Texture);
glBindVertexArray(skyBox_VAO);//注意water用的VAO也是跟skybox一起的
glDrawArrays(GL_TRIANGLES, 5 * 6, 6);
glBindVertexArray(0);
glDisable(GL_BLEND);
glDepthMask(GL_TRUE);
}
地形
注意到只给了一张bmp形式的高度图,首先需要从二维的图中读取(x,z)点对应的高度y,然后对这张二维的散点数据进行三角网格化,然后再用之前作业开发的zyMesh
类设置相应的渲染选项。三步走如下图所示。
|
|
|
读取高度
首先利用stb_image
库读入bmp图到字符序列raw_data_char
中,然后按顺序解析其中的每一个数,得到每个(row
,col
)位置的归一化高度(float)((int)raw_data_char[row * height + col]) / max_height
。将每个读到的点都当成最后网格中的一个顶点,按obj格式的文件输出。
std::string TerrainEngine::load_height_map(std::string & hmapFiles) {
float max_height = 255.0f;
int width, height, nChannels;
unsigned char * raw_data_char = stbi_load(hmapFiles.c_str(), &width, &height, &nChannels, 1);
if (raw_data_char == NULL) {printf("Error: invalid heightmap!\n");}
float max_x = (float)width, max_y = (float)height;
std::vector<float> land_pos;
std::vector<unsigned int> land_indices;
std::string objName = "../resource/land.obj";
FILE * fid = fopen(objName.c_str(), "w+");
unsigned tri_cnt = 0;
for (int row = 0; row < height; row++){
for (int col = 0; col < width; col++){
land_pos.push_back((float)row / max_y);
land_pos.push_back((float)((int)raw_data_char[row * height + col]) / max_height);
land_pos.push_back((float)col / max_x);
fprintf(fid, "v %f %f %f\n", land_pos[3*tri_cnt], land_pos[3*tri_cnt+1], land_pos[3*tri_cnt+2]);
tri_cnt++;
}
}
... ...
}
三角网格化
将一个nx*ny的规则长方形划分成三角形,显然将每个小长方形沿对角线切开即可。如下图所示,可以程式化、有规律地进行剖分。
具体的代码如下所示,但务必注意三角形面的朝向。
std::string TerrainEngine::load_height_map(std::string & hmapFiles) {
... ...
tri_cnt = 0;
for (int row = 0; row < height-1; row++){
for (int col = 0; col < width-1; col++){
// 第一个三角形
land_indices.push_back(row *width + col);
land_indices.push_back(row *width + col+1);
land_indices.push_back((row+1)*width + col+1);
fprintf(fid, "f %u %u %u\n", land_indices[3*tri_cnt ]+1, land_indices[3*tri_cnt+1]+1, land_indices[3*tri_cnt+2]+1);
tri_cnt++;
// 第二个三角形:务必注意面的朝向!!!
land_indices.push_back((row+1)*width + col+1);
land_indices.push_back((row+1)*width + col);
land_indices.push_back(row *width + col);
fprintf(fid, "f %u %u %u\n", land_indices[3*tri_cnt ]+1, land_indices[3*tri_cnt+1]+1, land_indices[3*tri_cnt+2]+1);
tri_cnt++;
}
}
fclose(fid);
return objName;
}
绘制三角网格
经过前一步的三角网格化并输出为obj文件,此时可以利用之前网格系列作业的zyMesh
类进行网格的加载。同时设置每个顶点对应的纹理坐标,这里需要注意使顶点高度确实与从纹理图上看起来的“高度”相匹配。
void TerrainEngine::load_textures(std::vector<std::string> skyboxFiles, std::string waterFiles,\
std::string landFiles, std::string detailFiles, std::string heightMapFiles) {
... ...
// 读取灰度图处理地形
land = zyMesh(load_height_map(heightMapFiles), false, false);//先不要setupMesh,等纹理坐标计算完之后再一起setup
// 设置mesh顶点数据的纹理坐标
for (size_t iv = 0; iv < land.vertexList.size(); iv++){
land.vertexData[iv].TexCoords.x = land.vertexData[iv].Position.z;
land.vertexData[iv].TexCoords.y = land.vertexData[iv].Position.x;
}
land.setupMesh();
}
作业中有两张纹理用于地形的绘制,粗糙一点的terrain-texture3.bmp和精细一点的detail.bmp,为了能在距离拉近时看到较精细的纹理,距离拉远时主要看到更粗一点的纹理,同时对两种纹理进行采样并叠加。
void TerrainEngine::draw_land(glm::mat4 const & model, glm::mat4 const & view, glm::mat4 const & proj, bool isUp) {
land_Shader.use();
land_Shader.setMat4("model", model);
land_Shader.setMat4("view", view);
land_Shader.setMat4("projection", proj);
glBindVertexArray(land.VAO);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, land_Texture);
land_Shader.setInt("land_Texture", 0);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, detail_Texture);
land_Shader.setInt("detail_Texture", 1);
land_Shader.setFloat("detail_scale", 30.0);
land_Shader.setBool("isUp", isUp);
// draw mesh
glDrawElements(GL_TRIANGLES, land.indices.size(), GL_UNSIGNED_INT, 0);
glBindVertexArray(0);//draw完之后解绑防止误用
}
因为浸没在水下的部分陆地不要渲染出来,所以着色器需特别设置,丢弃掉在可见平面下的东西。如下的片段采样器代码所示。bool型的uniform变量isUp
,在渲染水面之上的部分时设置为true,而渲染水面之下的部分时设置为false。
// vertex shader
#version 460 core
layout (location = 0) in vec3 aPosition;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoord;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
out vec2 TexCoord;
out float y_val;
void main() {
gl_Position = projection * view * model * vec4(aPosition, 1.0f);
y_val = (model * vec4(aPosition, 1.0f)).y;//世界空间的y坐标。用来给片段着色器丢掉处在可见平面下的东西
TexCoord = aTexCoord;
}
// fragment shader
#version 460 core
out vec4 FragColor;
in vec2 TexCoord;
in float y_val;
uniform sampler2D land_Texture;
uniform sampler2D detail_Texture;
uniform float detail_scale;//决定了repeat有多密 这个数越大,detail的纹理越密
uniform bool isUp;
void main(){
if ((isUp==true && y_val<0.0) || (isUp==false && y_val>0.0))
discard;
vec4 macro = texture(land_Texture, TexCoord);
vec4 micro = texture2D(detail_Texture, detail_scale * TexCoord);
FragColor = macro + micro - 0.5f;
}
效果图
|
|
|
|