A-6:Terrain Engine


作业要求:

  1. 材料见附件 TerrainEngine 文件夹。
  2. 摄像机坐标系与全局坐标系之间的变换;
  3. 海面的波浪效果;
  4. 地形的读取、绘制及纹理贴图;
  5. 天空和地形的倒影效果。

建立统一管理所有渲染元素的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_shifty_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_shifty_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类设置相应的渲染选项。三步走如下图所示。

step1: 像素图
step2: 散点图
step3: 网格图

读取高度

首先利用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;
}

效果图

全局场景
局部纹理
底部平视
顶部俯视
更多动态效果请见录屏。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
cd C:\Program Files\FlightGear fgfs --fg-root=C:\Program Files\FlightGear\data --aircraft=ufo --in-air --fdm=null --telnet=5501 --telnet=5502 --telnet=5503 --disable-ai-traffic --disable-real-weather-fetch --disable-random-objects --disable-terrasync --disable-clouds --disable-sound --disable-panel --disable-hud --disable-specular-highlight --timeofday=noon --prop:/sim/rendering/multi-sample-buffers=1 --prop:/sim/rendering/multi-samples=2 --prop:/sim/rendering/draw-mask-clouds=false --prop:/sim/rendering/draw-mask-terrain=true --prop:/sim/rendering/draw-mask-objects=true --prop:/sim/rendering/draw-mask-lights=true --prop:/sim/rendering/draw-mask-internal=true --prop:/sim/rendering/draw-mask-cockpit=true --prop:/sim/rendering/draw-mask-effects=true --prop:/sim/rendering/draw-mask-overlay=true --prop:/sim/rendering/draw-mask-world=true --prop:/sim/rendering/draw-mask-panel=true --prop:/sim/rendering/draw-mask-vr=true --prop:/sim/rendering/draw-mask-2d=true --prop:/sim/rendering/draw-mask-3d=true --prop:/sim/rendering/draw-mask-sky=true --prop:/sim/rendering/draw-mask-shadows=true --prop:/sim/rendering/draw-mask-cabin=true --prop:/sim/rendering/draw-mask-weather=true --prop:/sim/rendering/draw-mask-stereo=true --prop:/sim/rendering/draw-mask-internal-cockpit=true --prop:/sim/rendering/draw-mask-internal-windows=true --prop:/sim/rendering/draw-mask-internal-instruments=true --prop:/sim/rendering/draw-mask-internal-overlay=true --prop:/sim/rendering/draw-mask-internal-effects=true --prop:/sim/rendering/draw-mask-internal-lights=true --prop:/sim/rendering/draw-mask-internal-world=true --prop:/sim/rendering/draw-mask-internal-panel=true --prop:/sim/rendering/draw-mask-internal-3d=true --prop:/sim/rendering/draw-mask-internal-sky=true --prop:/sim/rendering/draw-mask-internal-cabin=true --prop:/sim/rendering/draw-mask-internal-weather=true --prop:/sim/rendering/draw-mask-internal-stereo=true --prop:/sim/rendering/draw-mask-internal-shadow=true --prop:/sim/rendering/draw-mask-internal-stall=true --prop:/sim/rendering/draw-mask-internal-aoa=true --prop:/sim/rendering/draw-mask-internal-thermal=false --prop:/sim/rendering/draw-mask-internal-ice=false --prop:/sim/rendering/draw-mask-internal-glass=true --prop:/sim/rendering/draw-mask-internal-dead=true --prop:/sim/rendering/draw-mask-internal-reflection=true程序显示错误unknown command-line option: enable-hud-2d怎么解决
05-10
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zongy17

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值