LearnOpenGL入门这一节学完之后有一段时间没有继续学习,再加上这一节内容还挺多的,原本的知识遗忘了不少。这一节的最后教程带领读者实现了一个FPS游戏风格的摄像机程序,基本包含第一节所有知识,本文主要分析一下这个程序以复习相关内容。
Table of Contents
程序结构
- main.cpp
- camera.h
- shaders.h
- openglutils.h
- textures.h
- VAOs.h
- v.s:顶点着色器
- f.s:片段着色器
openglutils.h:GLFW与GLAD
主程序开头:
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include "openglutils.h"
其中glad用于查询显卡提供的函数的入口地址以便调用,GLFW用于创建和管理窗口。而openglutils.h是自己实现的,开头如下:
#pragma once
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include "shaders.h"
#include "VAOs.h"
#include "textures.h"
#include "camera.h"
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
extern float time_delta;
extern float FOV;
extern Camera camera;
extern enum CAMERA_MOVEMENT;
int SCR_WIDTH = 800, SCR_HEIGHT = 600;
#define COLOR_BG 1.f, 0.f, 0.f, 1.f
其中glm是一个数学运算库。接下来openglutils.h提供了如下方法:
首先是一些GLFW需要用到的回调方法。frameBufferSizeCallback()的功能是在窗口大小变化时重新设定glViewport()。glViewport()指定视口在窗口中的左下角位置和宽高。将之与GLFW生成的窗口绑定,当窗口大小改变时GLFW就会调用frameBufferSizeCallback()并将新的窗口尺寸传入。剩下两个是鼠标位置和滚轮的回调函数,当窗口接受鼠标位置和滚轮的操作时会调用并将对应的操作传入,而具体的实现是在Camera类中。而键盘按键则不需要绑定,直接调用glfwGetKey(window, GLFW_KEY_XXX)检测是否按下调用对应方法即可。
void frameBufferSizeCallback(GLFWwindow* window, int width, int height)
{
SCR_WIDTH = width, SCR_HEIGHT = height;
glViewport(0, 0, width, height);
}
void cursorPosCallback(GLFWwindow* window, double posx, double posy)
{
camera.processCursorPos(static_cast<float>(posx), static_cast<float>(posy));
}
void scrollCallback(GLFWwindow* window, double offsetx, double offsety)
{
camera.processScroll(static_cast<float>(offsetx), static_cast<float>(offsety));
}
void processInput(GLFWwindow* window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
if ( == GLFW_PRESS)
camera.processKeyboardInput(FORWARD);
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
camera.processKeyboardInput(BACKWARD);
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
camera.processKeyboardInput(LEFT);
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
camera.processKeyboardInput(RIGHT);
}
接下来是程序初始化的方法。具体的看注释吧。
GLFWwindow* init_program()
{
/******** 起手式 ********/
// 初始化
glfwInit();
// 配置:OpenGL主版本号3、次版本号3、核心模式
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// 创建窗口、绑定当前窗口为当前线程的主上下文(OpenGL根据当前的上下文运行)、窗口设置
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, frameBufferSizeCallback);
glfwSetCursorPosCallback(window, cursorPosCallback);
glfwSetScrollCallback(window, scrollCallback);
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
// 初始化GLAD,在这之后才能使用OpenGL的函数
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "初始化GLAD失败!" << std::endl;
glfwTerminate();
}
glEnable(GL_DEPTH_TEST);
return window;
}
每一帧的初始化,在渲染循环的最开始调用。主要包括上一帧渲染用了多久、接受并处理键盘输入(和鼠标不同,没有绑定回调所以需要每帧手动调用一下)、清屏以及清除颜色缓冲以及深度缓冲(glClear()方法可用于清空缓冲区,传入的参数是待清空的缓冲区)。
float time_cur, time_last_cur;
void init_frame(GLFWwindow* window)
{
time_cur = static_cast<float>(glfwGetTime());
time_delta = time_cur - time_last_cur;
time_last_cur = time_cur;
processInput(window);
glClearColor(COLOR_BG);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}
剩下的是一些不重要的工具函数,具体见注释:
// 调用OpenGL的方法获取并在控制台打印当前显卡信息,主要是有时候忘记自己用的是集显还是独显看一下
void showInfos()
{
const GLubyte* graphics_card_brand = glGetString(GL_VENDOR);
const GLubyte* graphics_card_model = glGetString(GL_RENDERER);
const GLubyte* opengl_version = glGetString(GL_VERSION);
const GLubyte* glsl_version = glGetString(GL_SHADING_LANGUAGE_VERSION);
printf("Brand: %s\n", graphics_card_brand);
printf("Product: %s\n", graphics_card_model);
printf("OpenGL Version:%s\n", opengl_version);
printf("GLSL Version : %s\n", glsl_version);
}
/*
* 根据时间输出一些参数用的
* 0-直接输出;
* 1-输出sin;
* 2-输出sin延后1/4相;
* 3-输出sin延后1/2相;
* 4-输出sin延后3/4相;
* 5-输出(sin+1)/2;
* 6-输出(sin延后1/4相+1)/2;
* 7-输出(sin延后1/2相+1)/2;
* 8-输出(sin延后3/4相+1)/2;
*/
float getTimePara(unsigned int para)
{
switch (para)
{
case 0:
return static_cast<float>(glfwGetTime());
case 1:
return static_cast<float>(sin(glfwGetTime()));
case 2:
return static_cast<float>(cos(glfwGetTime()));
case 3:
return static_cast<float>(-sin(glfwGetTime()));
case 4:
return static_cast<float>(-cos(glfwGetTime()));
case 5:
return static_cast<float>((sin(glfwGetTime()) + 1) / 2);
case 6:
return static_cast<float>((cos(glfwGetTime()) + 1) / 2);
case 7:
return static_cast<float>((-sin(glfwGetTime()) + 1) / 2);
case 8:
return static_cast<float>((-cos(glfwGetTime()) + 1) / 2);
default:
std::cout << "参数错误" << std::endl;
return 0.f;
}
}
// 将平移、旋转、缩放三个操作集成到一个变换矩阵中,并传递到shader中
// 两个函数分别是传入向量和分别的参数的版本
glm::mat4 transform;
void setTrans(Shader* shader, const std::string& name, glm::vec3 t, float ra, glm::vec3 r, glm::vec3 s)
{
transform = glm::mat4(1.0f);
transform = glm::translate(transform, t);
// 这里必须指定一个旋转轴,不能都是零
transform = glm::rotate(transform, glm::radians(ra), r);
transform = glm::scale(transform, s);
shader->use();
shader->setMat4Float(name, transform);
}
void setTrans(Shader* shader, const std::string& name, float tx, float ty, float tz, float ra, float rx, float ry, float rz, float sx, float sy, float sz)
{
transform = glm::mat4(1.0f);
transform = glm::translate(transform, glm::vec3(tx, ty, tz));
// 这里必须指定一个旋转轴,不能都是零
transform = glm::rotate(transform, glm::radians(ra), glm::vec3(rx, ry, rz));
transform = glm::scale(transform, glm::vec3(sx, sy, sz));
shader->use();
shader->setMat4Float(name, transform);
}
以上就是openglutils.h的全部内容。
camera.h:摄像机实现
main.cpp的下一步创建了一个Camera类实例:
Camera camera(glm::vec3(0.f, 1.f, 0.f));
Camera类通过移动整个场景中所有物体营造出摄像机在场景中移动的感觉,camera.h的内容如下:
#pragma once
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#define CURSOR_SENSITIVITY 0.1f
#define CAM_SPEED 2.5f
float FOV = 45.0f;
float time_delta;
enum CAMERA_MOVEMENT {
FORWARD,
BACKWARD,
LEFT,
RIGHT
};
class Camera
{
public:
// 初始的相机位置、朝向、上方向
glm::vec3 cam_pos = glm::vec3(0.0f, 0.0f, 3.0f);
glm::vec3 cam_front = glm::vec3(0.0f, 0.0f, -1.0f);
glm::vec3 camera_up = glm::vec3(0.0f, 1.0f, 0.0f);
glm::vec3 world_up = glm::vec3(0.0f, 1.0f, 0.0f);
Camera(glm::vec3 position = glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3 front = glm::vec3(0.0f, 0.0f, -1.0f),glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f))
{
cam_pos = position;
cam_front = front;
world_up = up;
camera_up = world_up;
}
Camera(float posx, float posy, float posz, float frontx, float fronty, float frontz, float upx, float upy, float upz)
{
cam_pos = glm::vec3(posx, posy, posz);
cam_front = glm::vec3(frontx, fronty, frontz);
world_up = glm::vec3(upx, upy, upz);
camera_up = world_up;
}
// 给出相机位置等参数后就可以得出一个变换矩阵,可直接使用glm::lookAt()
// 其第二个参数是目标坐标,当然实际上给个方向即可。
glm::mat4 getViewMatrix()
{
// 话说把lookat放到shader里计算会不会更快
// 不过实际应用中因为接下来还要用而从显卡计算后不再传回来所以不能这么干?
return glm::lookAt(cam_pos, cam_pos + cam_front, world_up);
}
// 根据鼠标键盘等输入调整相机位置和朝向以及FOV
float yaw, pitch, lastposx, lastposy;
bool firstMouse = true;
void processCursorPos(float curposx, float curposy)
{
// 计算俯仰角,注意y轴坐标是从上往下的所以是反过来正的是向下负的是向上
pitch += CURSOR_SENSITIVITY * (lastposy - curposy);
lastposy = curposy;
// 俯仰不能超过90度防止死锁和屏幕翻转
if (pitch > 89.9f)pitch = 89.9f;
if (pitch < -89.9f)pitch = -89.9f;
// 计算偏航角
yaw += CURSOR_SENSITIVITY * (curposx - lastposx);
lastposx = curposx;
if (firstMouse)pitch = 0.f, yaw = -90.f, firstMouse = false;
// 生成新的相机方向,图看文件夹里的那个微信图片
cam_front = glm::normalize(glm::vec3(
cos(glm::radians(yaw)) * cos(glm::radians(pitch)),
sin(glm::radians(pitch)),
sin(glm::radians(yaw)) * cos(glm::radians(pitch))
));
}
float lockcamposy;
void processKeyboardInput(CAMERA_MOVEMENT i)
{
lockcamposy = cam_pos.y;
switch (i)
{
case FORWARD:
cam_pos += CAM_SPEED * time_delta * cam_front;
break;
case BACKWARD:
cam_pos -= CAM_SPEED * time_delta * cam_front;
break;
case LEFT:
cam_pos -= glm::normalize(glm::cross(cam_front, world_up)) * CAM_SPEED * time_delta;
break;
case RIGHT:
cam_pos += glm::normalize(glm::cross(cam_front, world_up)) * CAM_SPEED * time_delta;
break;
}
cam_pos.y = lockcamposy;
}
void processScroll(double xoffset, double yoffset)
{
FOV -= static_cast<float>(yoffset);
if (FOV < 1.f)FOV = 1.f;
if (FOV > 100.f)FOV = 100.f;
}
};
上面的相机方向的计算原理如下,屏幕与x-y平面平行,原本的朝向为z轴负方向(与常规的坐标系相反),鼠标移动对应yaw和pitch,于是新的方向为(cos(pitch),sin(yaw),sin(pitch)),由于相机朝向应为单位向量,所以只需将x-z平面内的两个向量再乘上cos(yaw)即可得到最终模为1的版本:
main.cpp接下来定义了地板和箱子的顶点在其模型坐标系内的坐标、贴图坐标和顶点次序,以及箱子们在世界坐标系的位置:
float vertices_floor[] = {
100.f, 0.f, 100.f, 200.f, 0.f,
-100.f, 0.f, 100.f, 0.f, 0.f,
-100.f, 0.f, -100.f, 0.f, 200.f,
100.f, 0.f, -100.f, 200.f, 200.f
};
unsigned int indices_floor[] = {
0,1,2,0,2,3
};
float vertices_box[] = {
// 下面
-0.5f, -0.5f, 0.5f, 0.3333f, 0.3333f, // 下左下 0
0.5f, -0.5f, 0.5f, 0.6667f, 0.3333f, // 下右下 1
0.5f, -0.5f, -0.5f, 0.6667f, 0.6667f, // 下右上 2
-0.5f, -0.5f, -0.5f, 0.3333f, 0.6667f, // 下左上 3
// 0145构成前面
-0.5f, 0.5f, 0.5f, 0.3333f, 0.f, // 上左下 4
0.5f, 0.5f, 0.5f, 0.6667f, 0.f, // 上右下 5
// 1267构成右侧
0.5f, 0.5f, 0.5f, 1.f, 0.3333f, // 上右下 6
0.5f, 0.5f, -0.5f, 1.f, 0.6667f, // 上右上 7
// 2389构成后侧
0.5f, 0.5f, -0.5f, 0.6667f, 1.f, // 上右上 8
-0.5f, 0.5f, -0.5f, 0.3333f, 1.f, // 上左上 9
// 031011构成左侧
-0.5f, 0.5f, 0.5f, 0.f, 0.3333f, // 上左下 10
-0.5f, 0.5f, -0.5f, 0.f, 0.6667f, // 上左上 11
// 上面
-0.5f, 0.5f, 0.5f, 0.6667f, 0.6667f, // 上左下 12
0.5f, 0.5f, 0.5f, 1.f, 0.6667f, // 上右下 13
0.5f, 0.5f, -0.5f, 1.f, 1.f, // 上右上 14
-0.5f, 0.5f, -0.5f, 0.6667f, 1.f, // 上左上 15
};
unsigned int indices_box[] = {
0, 1, 2, // 下
0, 2, 3,
0, 1, 4, // 前
1, 4, 5,
1, 2, 7, // 右
1, 6, 7,
0, 3, 10,// 左
10,11,3,
2, 3, 9, // 后
2, 8, 9,
12,13,14,// 上
12,14,15
};
glm::vec3 positions_box[] = {
glm::vec3(-5.f, 0.5f, 0.f),
glm::vec3(-8.f, 1.f, 5.f),
glm::vec3(-2.f, 2.f, 6.f),
glm::vec3(5.f, 1.5f, 6.f),
glm::vec3(5.f, 1.5f, 3.f),
glm::vec3(5.f, 1.f, -5.f),
glm::vec3(1.5f, 1.5f, -8.f),
glm::vec3(-4.f, 2.f, -3.f),
glm::vec3(-6.f, 2.f, -7.f)
};
shaders.h:着色器与uniform变量
接下来就是主函数了。首先调用openglutils.h初始化一下,然后初始化一个Shader实例:
int main()
{
GLFWwindow* window = init_program();
Shader s("v.s", "f.s");
shaders.h内容如下,涉及到的东西基本都在注释里了:
#pragma once
#include <glad/glad.h>
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
class Shader
{
public:
unsigned int shader_program_id, shader_vertex_id, shader_fragment_id;
Shader(const char* vertexPath, const char* fragmentPath)
{
// 读入shader文件
std::ifstream shader_file_v, shader_file_f;
shader_file_v.exceptions(std::ifstream::failbit | std::ifstream::badbit);
shader_file_f.exceptions(std::ifstream::failbit | std::ifstream::badbit);
std::string shader_str_v, shader_str_f;
try
{
shader_file_v.open(vertexPath);
shader_file_f.open(fragmentPath);
std::stringstream shader_stream_v, shader_stream_f;
shader_stream_v << shader_file_v.rdbuf();
shader_stream_f << shader_file_f.rdbuf();
shader_file_v.close();
shader_file_f.close();
shader_str_v = shader_stream_v.str();
shader_str_f = shader_stream_f.str();
}
catch (std::ifstream::failure& e)
{
std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ: " << e.what() << std::endl;
}
const char* shader_char_v = shader_str_v.c_str();
const char* shader_char_f = shader_str_f.c_str();
// 创建顶点着色器并得到顶点着色器id
shader_vertex_id = glCreateShader(GL_VERTEX_SHADER);
// 替换源代码为自己的
// 第二个参数为字符串数组中的字符串数
// 最后一个参数为字符串长度,不为NULL的话则截取到指定长度
glShaderSource(shader_vertex_id, 1, &shader_char_v, NULL);
glCompileShader(shader_vertex_id);
checkCompileErrors(shader_vertex_id, "VERTEX");
// 同理创建片段着色器
shader_fragment_id = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(shader_fragment_id, 1, &shader_char_f, NULL);
glCompileShader(shader_fragment_id);
checkCompileErrors(shader_fragment_id, "FRAGMENT");
// 将以上两者组合为着色器程序
shader_program_id = glCreateProgram();
glAttachShader(shader_program_id, shader_vertex_id);
glAttachShader(shader_program_id, shader_fragment_id);
glLinkProgram(shader_program_id);
checkCompileErrors(shader_program_id, "PROGRAM");
glDeleteShader(shader_vertex_id);
glDeleteShader(shader_fragment_id);
}
// 将自身绑定到上下文以供渲染使用,另外glUseProgram(0)意味着不使用shader
void use()
{
glUseProgram(shader_program_id);
}
// uniform设置,在外界调用就可以修改shader中指定内容的值
/*
* 由于OpenGL ES由C语言编写,
* 但是C语言不支持函数的重载,
* 所以会有很多名字相同后缀不同的函数版本存在。
* 其中函数名中包含数字(1、2、3、4)表示接受这个数字个用于更改uniform变量的值,
* i表示32位整形,
* f表示32位浮点型,
* ub表示8位无符号byte,
* ui表示32位无符号整形,
* v表示接受相应的指针类型。
*/
void set1Int(const std::string& name, const int value) const
{
glUniform1i(glGetUniformLocation(shader_program_id, name.c_str()), value);
}
void set1Float(const std::string& name, const float value) const
{
glUniform1f(glGetUniformLocation(shader_program_id, name.c_str()), value);
}
void set4Float(const std::string& name, const float v1, const float v2, const float v3, const float v4) const
{
glUniform4f(glGetUniformLocation(shader_program_id, name.c_str()), v1, v2, v3, v4);
}
void setMat4Float(const std::string& name, const glm::mat4& mat) const
{
glUniformMatrix4fv(glGetUniformLocation(shader_program_id, name.c_str()), 1, GL_FALSE, &mat[0][0]);
}
private:
void checkCompileErrors(unsigned int shader, std::string type)
{
int success;
char infoLog[1024];
if (type == "VERTEX")
{
glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(shader, 1024, NULL, infoLog);
std::cout << "顶点着色器 " << shader_vertex_id << " ERROR\n" << infoLog << "\n-- -------------------------------------------------- - -- " << std::endl;
}
}
if (type == "FRAGMENT")
{
glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(shader, 1024, NULL, infoLog);
std::cout << "片段着色器 " << shader_fragment_id << " ERROR\n" << infoLog << "\n-- -------------------------------------------------- - -- " << std::endl;
}
}
if (type == "PROGRAM")
{
glGetProgramiv(shader, GL_LINK_STATUS, &success);
if (!success)
{
glGetProgramInfoLog(shader, 1024, NULL, infoLog);
std::cout << "着色器程序 " << shader_program_id << "(对应顶点着色器 "<< shader_vertex_id<<" 片段着色器 "<< shader_fragment_id<<") ERROR\n" << infoLog << "\n-- -------------------------------------------------- - -- " << std::endl;
}
}
}
};
VAOs.h:VAO、VBO与EBO
回到main.cpp,接下来配置三个“O”,其中VAO主要是用于管理VBO、EBO,减少glBindBuffer、glEnableVertexAttribArray、glVertexAttribPointer等调用操作,VBO存储顶点并告诉显卡怎么操作这一堆顶点EBO存储顶点索引:
unsigned int vao_[] = { 3,2 };
VAO vao_box(vertices_box, sizeof(vertices_box), indices_box, sizeof(indices_box), vao_, sizeof(vao_), 5, GL_STATIC_DRAW);
VAO vao_floor(vertices_floor, sizeof(vertices_floor), indices_floor, sizeof(indices_floor), vao_, sizeof(vao_), 5, GL_STATIC_DRAW);
VAOs.h内容如下,具体怎么回事看注释:
#pragma once
#include <glad/glad.h>
class VAO
{
public:
unsigned int id_vao = 0, id_vbo = 0, id_ebo = 0;
VAO(const float* vertices_box, const unsigned long long vertices_size,
const unsigned int* attributes_sizes, const unsigned int attributes_num,
const unsigned int stride, const int drawtype)
{
// 生成和绑定VAO
glGenVertexArrays(1, &id_vao);
glBindVertexArray(id_vao);
// VAO中并不保存当前绑定的GL_ARRAY_BUFFER,VBO和vertex attribute的绑定是在glVertexAttribPointer中完成的。
// 申请VBO
glGenBuffers(1, &id_vbo);
// 填充VBO数据
glBindBuffer(GL_ARRAY_BUFFER, id_vbo);
glBufferData(GL_ARRAY_BUFFER, vertices_size, vertices_box, drawtype);
// 指定各字段数据长度,这里让主函数传过来一个数组,数组长度为字段个数,各元素为字段有几个float长度,自动填充函数参数
// 在这个例子中传过来的是{3,2},也就是前三个float一组表示顶点坐标,后两个float一组表示贴图中坐标
for (unsigned int i = 0, j = 0; i < attributes_num; ++i)
{
glVertexAttribPointer(i, attributes_sizes[i], GL_FLOAT, GL_FALSE, stride * sizeof(float), (void*)(j * sizeof(float)));
j += attributes_sizes[i];
glEnableVertexAttribArray(i);
}
// 完事以后解绑就行
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}
// 带ebo的版本
VAO(const float* vertices_box, const unsigned int vertices_size,
const unsigned int* indices_box, const unsigned int indices_size,
const unsigned int* attributes_sizes, const unsigned int attributes_num,
const unsigned int stride, const int drawtype)
{
glGenVertexArrays(1, &id_vao);
glGenBuffers(1, &id_vbo);
glBindVertexArray(id_vao);
glBindBuffer(GL_ARRAY_BUFFER, id_vbo);
glBufferData(GL_ARRAY_BUFFER, vertices_size, vertices_box, drawtype);
glGenBuffers(1, &id_ebo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, id_ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices_size, indices_box, drawtype);
for (unsigned int i = 0, j = 0; i < attributes_num; ++i)
{
glVertexAttribPointer(i, attributes_sizes[i], GL_FLOAT, GL_FALSE, stride * sizeof(float), (void*)(j * sizeof(float)));
j += attributes_sizes[i];
glEnableVertexAttribArray(i);
}
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}
~VAO()
{
glDeleteVertexArrays(1, &id_vao);
glDeleteBuffers(1, &id_vbo);
if (id_ebo)glDeleteBuffers(1, &id_ebo);
}
void use()
{
glBindVertexArray(id_vao);
}
};
textures.h:纹理贴图
接下来配置纹理:
Texture2D tex_box("box.png", GL_REPEAT, GL_REPEAT, GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR);
Texture2D tex_floor("floor.png", GL_REPEAT, GL_REPEAT, GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR);
其中textures.h内容如下,具体怎么回事看注释:
#pragma once
#include <glad/glad.h>
#include <iostream>
/*
* 可能是因为stb_image.h自己内部实现的原因
* 在.cpp中直接包含它会导致重编译
* 必须加一个define
* stb_image.h has its own include guards.
* That's not what defining STB_IMAGE_IMPLEMENTATION is for.
* Defining STB_IMAGE_IMPLEMENTATION tells stb_image.h to include not only declarations,
* but also definitions for its functions and variables into that translation unit.
* If stb_image.h is included into multiple translation units with STB_IMAGE_IMPLEMENTATION defined,
* then all of those translation units will have definitions for stb_image's functions and variables,
* and the One Definition Rule is violated.
*/
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
class Texture2D
{
public:
unsigned int texture_id;
Texture2D(const std::string& path, int wrap_s, int wrap_t, int filter_min, int filter_mag)
{
glGenTextures(1, &texture_id);
glBindTexture(GL_TEXTURE_2D, texture_id);
/* 设置纹理环绕方式,S、T、R分别对应xyz轴,比方说2维纹理就调用两次分别设定了S和T
* GL_REPEAT 对纹理的默认行为。重复纹理图像。
* GL_MIRRORED_REPEAT 和GL_REPEAT一样,但每次重复图片是镜像放置的。
* GL_CLAMP_TO_EDGE 纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。
* GL_CLAMP_TO_BORDER 超出的坐标为用户指定的边缘颜色。
*/
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrap_s);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrap_t);
/* 设置纹理过滤方式,也就是放大时怎么插值
* 可选GL_LINEAR或GL_NEAREST
* 设置多级渐远纹理,也就是较近的纹理和较远的纹理缩放比例不同时怎么插值
* GL_NEAREST_MIPMAP_NEAREST 使用最邻近的多级渐远纹理来匹配像素大小,并使用邻近插值进行纹理采样
* GL_LINEAR_MIPMAP_NEAREST 使用最邻近的多级渐远纹理级别,并使用线性插值进行采样
* GL_NEAREST_MIPMAP_LINEAR 在两个最匹配像素大小的多级渐远纹理之间进行线性插值,使用邻近插值进行采样
* GL_LINEAR_MIPMAP_LINEAR 在两个邻近的多级渐远纹理之间使用线性插值,并使用线性插值进行采样
*/
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter_min);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter_mag);
// 读取图像,stb_image.h将会用图像的宽度、高度和颜色通道的个数填充width, height, nrChannels
int width, height, nrChannels;
// 图片似乎必须得是正方形,不然会出现各种奇怪的畸变和条纹
unsigned char* data = stbi_load(path.c_str(), &width, &height, &nrChannels, 0);
if (data)
{
/*
* 第一个参数指定了纹理目标(Target)。
设置为GL_TEXTURE_2D意味着会生成与当前绑定的纹理对象在同一个目标上的纹理
(任何绑定到GL_TEXTURE_1D和GL_TEXTURE_3D的纹理不会受到影响)。
* 第二个参数为纹理指定多级渐远纹理的级别,如果你希望单独手动设置每个多级渐远纹理的级别的话。
这里我们填0,也就是基本级别。
* 第三个参数告诉OpenGL我们希望把纹理储存为何种格式。
我们的图像只有RGB值,因此我们也把纹理储存为RGB值。
* 第四个和第五个参数设置最终的纹理的宽度和高度。
我们之前加载图像的时候储存了它们,所以我们使用对应的变量。
* 下个参数应该总是被设为0(历史遗留的问题)。
* 第七第八个参数定义了源图的格式和数据类型。
我们使用RGB值加载这个图像,并把它们储存为char(byte)数组,我们将会传入对应值。
* 最后一个参数是真正的图像数据。
*/
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
/*
* 调用glTexImage2D时,当前绑定的纹理对象就会被附加上纹理图像。
* 然而,目前只有基本级别(Base-level)的纹理图像被加载了,
* 如果要使用多级渐远纹理,我们必须手动设置所有不同的图像(不断递增第二个参数)。
* 或者,直接在生成纹理之后调用glGenerateMipmap。
* 这会为当前绑定的纹理自动生成所有需要的多级渐远纹理。
*/
glGenerateMipmap(GL_TEXTURE_2D);
}
else std::cout << "texture " << texture_id << " Failed to load texture" << std::endl;
stbi_image_free(data);
glBindTexture(GL_TEXTURE_2D, 0);
}
void use(const unsigned int i)
{
// OpenGL默认激活纹理单元0,所以如果只用到0的话其实无需主动激活
glActiveTexture(GL_TEXTURE0 + i);
glBindTexture(GL_TEXTURE_2D, texture_id);
}
};
主渲染循环
最后是主要的渲染循环了:
glm::mat4 projection, view;
while (!glfwWindowShouldClose(window))
{
init_frame(window);
// 视角变换矩阵
view = camera.getViewMatrix();
// 透视变换矩阵,不加的话就是垂直地投到屏幕上的
projection = glm::perspective(glm::radians(FOV), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.f);
// 渲一个东西的流程就是选VAO、选纹理、选着色器、修改着色器参数、渲染
// 渲地面
vao_floor.use();
tex_floor.use(0);
s.use();
s.setMat4Float("view", view);
s.setMat4Float("projection", projection);
setTrans(&s, "model", 0.f, 0.f, 0.f, 0.f, 1.f, 0.f, 0.f, 1.f, 1.f, 1.f);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
// 渲方块,因为除了位置变换矩阵以外方块用的都是同样的东西所以后面用了个循环调整model矩阵其它不变
vao_box.use();
tex_box.use(0);
s.use();
s.setMat4Float("view", view);
s.setMat4Float("projection", projection);
setTrans(&s, "model", 0.f, 4.f, 0.f, 50 * getTimePara(0), getTimePara(1), getTimePara(2), getTimePara(3), 1.f, 1.f, 1.f);
glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);
for (unsigned int i = 0; i < 9; ++i)
{
setTrans(&s, "model", positions_box[i], 0.f,glm::vec3( 0.f, 1.f, 0.f), glm::vec3(2* positions_box[i].y, 2 * positions_box[i].y, 2 * positions_box[i].y));
glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);
}
// 检查有没有触发什么事件(比如键盘输入、鼠标移动等)调用对应的回调函数(可以通过回调方法手动设置)
glfwPollEvents();
// 交换缓冲
// 绘制过程有两个缓冲区,先画好,再把画好的交换到前台,避免单一缓冲区呈现出“正在绘制”的效果
glfwSwapBuffers(window);
}
glfwTerminate();
return 0;
}
顶点着色器
顶点着色器如下,开头先说一下版本号,然后前面VBO已经划分好了字段长度这里命名一下就行,也就是第一个字段叫vsin_pos,第二个字段叫vsin_texcoord。接下来是声明要被传到片段着色器中的变量和三个可被修改的uniform变量,最后将模型坐标经过变换传给gl_Position、纹理坐标传给片段着色器即可:
#version 330 core
layout (location = 0) in vec3 vsin_pos;
layout (location = 1) in vec2 vsin_texcoord;
out vec2 vsout_texcoord;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
gl_Position = projection * view * model * vec4(vsin_pos, 1.f);
vsout_texcoord = vsin_texcoord;
}
注意这里的矩阵都是4×4的方阵,vsin_pos也要转成方阵。这是因为三维矩阵不能表示一些变换,如平移变换,因此要升维至4维处理(参考),所以干脆都用四维的。
片段着色器
片段着色器如下,使用GLSL内建的texture函数来采样纹理的颜色,它第一个参数是纹理采样器,第二个参数是对应的纹理坐标,纹理采样器虽然是uniform变量但不需要手动赋值,而是由glBindTexture()自动完成。这个片段着色器的输出就是纹理的(插值)纹理坐标上的(过滤后的)颜色:
#version 330 core
in vec2 vsout_texcoord;
out vec4 fsout_color;
uniform sampler2D cubetex;
void main()
{
fsout_color = texture(cubetex, vsout_texcoord);
}