OpenGL学习日记
环境搭建
下载glwe-2.1.0
和glfw-3.3.6.bin.WIN32
属性 配置:所有配置 平台:所有平台
属性 C/C++ 常规 附加包含目录
属性 链接器 常规 附加库目录:
属性 链接器 输入 附加依赖项:
第一个窗口
1
2
3
#define GLEW_STATIC // 之前的是static版本
#include <GL/glew.h>
#include <GLFW/glfw3.h>
初始化glfw
1
2
3
4
5
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); // 3.3版本
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
//glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // Mac才需要
创建GLFW窗口对象
1
2
3
4
5
6
7
GLFWwindow* window = glfwCreateWindow(800, 800, "My OpenGL Game", nullptr, nullptr);
if (window == nullptr) {
std::cout << "打开窗口失败" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window); // 通知GLFW将窗口的上下文设置为当前线程的主上下文
初始化glew
1
2
3
4
5
6
glewExperimental = true;
if (glewInit() != GLEW_OK) {
std::cout << "初始化GLEW失败" << std::endl;
glfwTerminate();
return -1;
}
视口大小
1
2
// GLAPI void GLAPIENTRY glViewport (GLint x, GLint y, GLsizei width, GLsizei height);
glViewport(0, 0, 800, 800);
面剔除
1
2
3
4
5
glEnable(GL_CULL_FACE); // 面剔除
glCullFace(GL_BACK); // 剔除背面
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); // 框线模式
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); // 变回默认模式
渲染循环
1
2
3
4
5
while(!glfwWindowShouldClose(window))
{
glfwSwapBuffers(window); // 交换颜色缓冲
glfwPollEvents(); // 检查有无触发事件、更新窗口状态、调用相应回调
}
退出
1
glfwTerminate();
按键控制:esc键退出
1
2
3
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
glfwSetWindowShouldClose(window, true);
}
清屏
1
2
glClearColor(0.2f, 0.3f, 0.3f, 1.0f); // 状态设置函数
glClear(GL_COLOR_BUFFER_BIT); // 状态使用函数
图形渲染管线
3D坐标转化为屏幕的2D像素,是由OpenGL的图形渲染管线管理的。第一步将3D坐标转换为2D坐标,第二步将2D坐标转变为带颜色的像素。
图形渲染管线可以被划分为几个阶段,每个阶段把前一个阶段的输出作为输入。所有阶段都是高度专门化的,可以并行执行,每个小程序叫做着色器。OpenGL着色器是使用OpenGL着色器语言(GLSL)写成的。
在现代OpenGL中,我们必须定义至少一个顶点着色器和一个片段着色器(因为GPU中没有默认的顶点/片段着色器)。
顶点着色器主要的目的是把3D坐标转为另一种3D坐标,同时顶点着色器允许我们对顶点属性进行一些基本处理。
图元装配阶段将顶点着色器输出的所有顶点作为输入,并所有的点装配成指定图元的形状。
几何着色器把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。
光栅化会把图元映射为最终屏幕上相应的像素,生成供片段着色器使用的片段(Fragment)。在片段着色器运行之前会执行裁切。裁切会丢弃超出视图以外的所有像素,用来提升执行效率。
片段着色器的主要目的是计算一个像素的最终颜色。
Alpha测试和混合阶段。这个阶段检测片段的对应的深度值,用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。这个阶段也会检查alpha值并对物体进行混合。
着色器
顶点着色器
1
2
3
4
5
6
#version 330 core
layout (location = 0) in vec3 aPos;
void main()
{
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
片段着色器
1
2
3
4
5
6
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
着色器源码硬编码
1
2
3
4
5
6
const char *vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
"}\0";
硬编码编译着色器
1
2
3
4
5
6
7
8
9
10
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
// 要编译的着色器对象 字符串数量 源码 NULL
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
把多个着色器合并链接,成为着色器程序
1
2
3
4
5
6
7
8
9
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader); // 附加
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram); // 链接
// 删除顶点着色器和片段着色器,只需要留下着色器程序对象就行了
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
在渲染循环里使用着色器程序
1
glUseProgram(shaderProgram);
顶点
OpenGL仅当3D坐标在3个轴(x、y和z)上都为-1.0到1.0的范围内时才处理它。所有在所谓的标准化设备坐标(Normalized Device Coordinates)范围内的坐标才会最终呈现在屏幕上(在这个范围以外的坐标都不会显示)。
1
2
3
4
5
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
VAO:顶点数组对象 VBO:顶点缓冲对象 EBO:索引缓冲对象
通过ID来管理OpenGL对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 顶点缓冲对象 缓存一大堆CPU来的数据
unsigned int VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO); // 绑定VBO
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 把顶点数组复制到缓冲中
// 缓冲类型 大小 实际数据 显卡数据管理方式
// GL_STATIC_DRAW 数据不会或几乎不会改变。
// GL_DYNAMIC_DRAW 数据会被改变很多。
// GL_STREAM_DRAW 数据每次绘制时都会改变。
// 顶点数组对象 从VBO里重新组装信息
unsigned int VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO); // 绑定VAO
// 索引缓冲对象 允许给顶点标索引
unsigned int EBO;
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); // 绑定EBO
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
如何解释顶点数据
1
2
3
4
5
6
7
8
9
10
/*
顶点属性位置值 layout(location = 0)
3个数一个属性
每个数是一个float
不要标准化
步长为3 * sizeof(float)
偏移量0
*/
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0); // 以顶点属性位置值作为参数,启用顶点属性
绘制图形
1
glDrawArrays(GL_TRIANGLES, 0, 3);
使用索引
使用索引
1
2
3
4
5
6
7
8
9
10
11
float vertices[] = {
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};
unsigned int indices[] = { // 索引从0开始!
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};
绘制图形,现在使用glDrawElements()
表示使用索引。
1
2
// glDrawArrays(GL_TRIANGLES, 0, 3);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
总结
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ..:: 初始化代码 :: ..
// 1. 绑定顶点数组对象
glBindVertexArray(VAO);
// 2. 把顶点数组复制到顶点缓冲中
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 把索引数组复制到索引缓冲中
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 4. 设定顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
[...]
// ..:: 绘制代码(渲染循环中) :: ..
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)
glBindVertexArray(0);
着色器
GLSL
着色器是运行在GPU上的程序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#version version_number
in type in_variable_name;
in type in_variable_name;
out type out_variable_name;
uniform type uniform_name;
int main()
{
// 处理输入并进行一些图形操作
...
// 输出处理过的结果到输出变量
out_variable_name = weird_stuff_we_processed;
}
顶点着色器的输入变量叫做顶点属性,一般至少有16个包含4分量的顶点属性可用。
GLSL中包含C等其它语言大部分的默认基础数据类型:int
、float
、double
、uint
和bool
,GLSL也有两种容器类型,分别是向量(Vector)和矩阵(Matrix)。 GLSL中的向量是一个可以包含有2、3或者4个分量的容器,分量的类型可以是前面默认基础类型的任意一个。vecn
、bvecn
、ivecn
、uvecn
、dvecn
。
顶点着色器应该通过layout
来从顶点数据中直接接收输入。一定要输出gl_Position
。
片段着色器应该用vec4
输出颜色变量。
Uniform
Uniform是一种从CPU中的应用向GPU中的着色器发送的全局数据。
1
uniform vec4 ourColor;
在渲染时将数据发给Uniform
1
2
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUniform4f(vertexColorLocation, 0.0f, 1.0f, 0.0f, 1.0f); // 纯绿色
更多输入和输出
计划将颜色数据加入顶点数据中
1
2
3
4
5
6
float vertices[] = {
// 位置 // 颜色
0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // 右下
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // 左下
0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // 顶部
};
顶点格式
1
2
3
4
5
6
// 位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 颜色属性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3* sizeof(float)));
glEnableVertexAttribArray(1);
顶点着色器
1
2
3
4
5
6
7
8
9
#version 330 core
layout (location = 0) in vec3 aPos; // 位置的属性位置值为 0
layout (location = 1) in vec3 aColor; // 颜色的属性位置值为 1
out vec3 ourColor; // 输出给片段着色器
void main()
{
gl_Position = vec4(aPos, 1.0);
ourColor = aColor;
}
片段着色器
1
2
3
4
5
6
7
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
void main()
{
FragColor = vec4(ourColor, 1.0);
}
我们自己的着色器类
Shader
类
1
2
3
4
5
6
7
8
9
10
11
class Shader
{
public:
unsigned int shaderProgramID;
Shader(const char* vertexPath, const char* fragmentPath);
// ~Shader();
void use();
private:
void checkCompileErrors(unsigned int ID, std::string type);
};
从文件中读取着色器代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
std::ifstream vertexShaderFile, fragmentShaderFile;
// 保证ifstream对象可以抛出异常
vertexShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
fragmentShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
std::string vertexShaderCodeString, fragmentShaderCodeString;
try
{
//打开文件
vertexShaderFile.open(vertexPath);
fragmentShaderFile.open(fragmentPath);
std::stringstream vertexShaderStream, fragmentShaderStream;
// 读取文件到数据流
vertexShaderStream << vertexShaderFile.rdbuf();
fragmentShaderStream << fragmentShaderFile.rdbuf();
// 关闭文件
vertexShaderFile.close();
fragmentShaderFile.close();
// 数据流转string
vertexShaderCodeString = vertexShaderStream.str();
fragmentShaderCodeString = fragmentShaderStream.str();
}
catch (const std::exception&)
{
std::cout << "文件打开失败" << std::endl;
}
const char* vertexShaderCode = vertexShaderCodeString.c_str();
const char* fragmentShaderCode = fragmentShaderCodeString.c_str();
编译着色器代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
unsigned int vertex, fragment;
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &vertexShaderCode, NULL);
glCompileShader(vertex);
checkCompileErrors(vertex, "VERTEX");
fragment = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment, 1, &fragmentShaderCode, NULL);
glCompileShader(fragment);
checkCompileErrors(vertex, "FRAGMENT");
shaderProgramID = glCreateProgram();
glAttachShader(shaderProgramID, vertex);
glAttachShader(shaderProgramID, fragment);
glLinkProgram(shaderProgramID);
checkCompileErrors(vertex, "PROGRAM");
glDeleteShader(vertex);
glDeleteShader(fragment);
编译错误检查
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void Shader::checkCompileErrors(unsigned int ID, std::string type){
int success;
char infoLog[512];
if (type != "PROGRAM") {
glGetShaderiv(ID, GL_COMPILE_STATUS, &success);
if (!success) {
glGetShaderInfoLog(ID, 512, NULL, infoLog);
std::cout << infoLog << std::endl;
}
}
else {
glGetProgramiv(ID, GL_LINK_STATUS, &success);
if (!success) {
glGetProgramInfoLog(ID, 512, NULL, infoLog);
std::cout << infoLog << std::endl;
}
}
}
使用程序
1
2
3
void Shader::use() {
glUseProgram(shaderProgramID);
}
纹理
采样
为了将纹理映射到三角形上,需要指定每个顶点对应的纹理坐标,其他片段进行插值。
纹理环绕方式:
GL_REPEAT
对纹理的默认行为。重复纹理图像。 GL_MIRRORED_REPEAT
和GL_REPEAT一样,但每次重复图片是镜像放置的。 GL_CLAMP_TO_EDGE
纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。 GL_CLAMP_TO_BORDER
超出的坐标为用户指定的边缘颜色。
可以对s
和t
坐标轴分别设置环绕方式。
1
2
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
设置边界颜色
1
2
float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
纹理过滤用于处理纹理坐标向纹理像素的转换(纹理坐标是实数,现在要在图片的对应点进行取样)。可以在放大和缩小时使用不同的取样方式。
1
2
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
离得很远时,应该使用小分辨率的纹理,可以用一系列纹理图像,即多级渐远纹理。glGenerateMipmaps()
会自动生成多级渐远纹理。
GL_NEAREST_MIPMAP_NEAREST
使用最邻近的多级渐远纹理来匹配像素大小,并使用邻近插值进行纹理采样 GL_LINEAR_MIPMAP_NEAREST
使用最邻近的多级渐远纹理级别,并使用线性插值进行采样 GL_NEAREST_MIPMAP_LINEAR
在两个最匹配像素大小的多级渐远纹理之间进行线性插值,使用邻近插值进行采样 GL_LINEAR_MIPMAP_LINEAR
在两个邻近的多级渐远纹理之间使用线性插值,并使用线性插值进行采样
多级渐远纹理只能在缩小时设置,因为纹理放大不会使用更小分辨率的纹理。
1
2
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
加载和生成纹理
stb_image.h
是一个非常流行的单头文件图像加载库。
1
2
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
绑定纹理
1
2
3
4
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture); // 绑定纹理
从文件中加载数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
int width, height, nrChannels; // 宽度 高度 颜色通道个数
unsigned char* data = stbi_load("diamond_block.png", &width, &height, &nrChannels, 0);
if (data)
{
// 纹理目标 多级渐远纹理的级别 OpenGL存储的格式 宽度 高度 总是0 原图的格式 原图的数据类型 图片数据
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); // 生成纹理
glGenerateMipmap(GL_TEXTURE_2D); // 多级渐远纹理
}
else
{
std::cout << "加载纹理失败" << std::endl;
}
stbi_set_flip_vertically_on_load(true); // 图片上下颠倒
stbi_image_free(data); // 释放图像内存
应用纹理
顶点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
float vertices[] = {
// ---- 位置 ---- ---- 颜色 ---- - 纹理坐标 -
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // 右上
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 右下
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // 左下
-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // 左上
};
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
顶点着色器
1
2
3
4
5
6
7
8
9
10
11
12
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aTexCoord;
out vec3 ourColor;
out vec2 texCoord;
void main()
{
gl_Position = vec4(aPos, 1.0);
ourColor = aColor;
texCoord = aTexCoord;
}
片段着色器
1
2
3
4
5
6
7
8
9
#version 330 core
in vec3 ourColor;
in vec2 texCoord;
out vec4 FragColor;
uniform sampler2D ourTexture;
void main()
{
FragColor = (texture(ourTexture, texCoord) + vec4(ourColor, 1.0) * 2) / 3;
}
纹理单元
一个纹理的位置值通常称为一个纹理单元,默认纹理单元是0,默认激活。所以刚才没有使用glUniform()
来赋值。
1
2
glActiveTexture(GL_TEXTURE0); // 在绑定纹理之前先激活纹理单元 这是0号纹理单元
glBindTexture(GL_TEXTURE_2D, texture);
给不同采样器赋不同的纹理单元位置值
1
2
3
4
5
testShader->use();
int textureLocation1 = glGetUniformLocation(testShader->shaderProgramID, "texture1");
glUniform1i(textureLocation1, 0);
int textureLocation2 = glGetUniformLocation(testShader->shaderProgramID, "texture2");
glUniform1i(textureLocation2, 1);
片段着色器,注意alpha通道。
1
2
3
4
5
6
7
8
9
10
#version 330 core
in vec3 ourColor;
in vec2 texCoord;
out vec4 FragColor;
uniform sampler2D texture1;
uniform sampler2D texture2;
void main()
{
FragColor = mix(texture(texture1, texCoord), texture(texture2, texCoord), texture(texture2, texCoord).a * 0.7);
}
变换
向量\(\bar{v}=\left(\begin{array}{}x\\y\\z\end{array}\right)\)
一般使用的向量是4分量的,这样可以简单地进行位移。
\[\left[\begin{array}{} 1&0&0&T_x\\ 0&1&0&T_y\\ 0&0&1&T_z\\ 0&0&0&1 \end{array}\right] \left(\begin{array}{} x\\y\\z\\1 \end{array}\right)= \left(\begin{array}{} x+T_x\\y+T_y\\z+T_z\\1 \end{array}\right)\]$w$坐标可以设为$1$或$0$,表示位置时写$1$,表示方向时写$0$,方向向量就不能位移了。
OpenGL Mathematics的缩写,它是一个只有头文件的库。
1
2
3
#include <glm.hpp>
#include <gtc/matrix_transform.hpp>
#include <gtc/type_ptr.hpp>
生成变换矩阵,变换的顺序与书写的顺序相反。
1
2
3
4
5
6
glm::mat4 mat; // 默认是单位矩阵
// 0.9.9及以上版本 使用
// glm::mat4 trans = glm::mat4(1.0f)
mat = glm::translate(mat, glm::vec3(0.5f, 0.0f, 0.0f)); // 平移 (0.5 0 0)
mat = glm::rotate(mat, glm::radians(60.0f), glm::vec3(0.0, 0.0, 1.0)); // 逆时针旋转
mat = glm::scale(mat, glm::vec3(0.5, 0.5, 0.5f)); // 缩放0.5倍
传递给Uniform
1
2
unsigned int transformLoc = glGetUniformLocation(ourShader.ID, "transform");
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(trans));
顶点着色器
1
2
3
4
5
6
7
8
9
10
11
12
13
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
out vec2 TexCoord;
uniform mat4 transform;
void main()
{
gl_Position = transform * vec4(aPos, 1.0f);
TexCoord = vec2(aTexCoord.x, 1.0 - aTexCoord.y);
}
坐标系统
局部坐标:对象相对于局部原点的坐标。
世界空间坐标:计算对象相对于世界原点的坐标,使用的是模型矩阵。
观察空间坐标:每个坐标都是从摄像机的角度观察的,使用的是观察矩阵。
裁剪坐标:裁剪坐标到-1.0到1.0范围内,使用的是投影矩阵。
最后将裁剪坐标变换为屏幕坐标,使用一个叫做视口变换的过程。首先是透视除法,将它们变换到标准化设备坐标,然后用glViewPort
内部的参数来映射到屏幕坐标。
1
2
3
4
5
6
7
8
9
glm::mat4 modelMat;
// 沿x轴旋转-55度
modelMat = glm::rotate(modelMat, glm::radians(-50.0f), glm::vec3(1.0, 0.0, 0.0));
glm::mat4 viewMat;
// 相机在(0, 0, 3) 相当于所有对象移动到(0, 0, -3)
viewMat = glm::translate(viewMat, glm::vec3(0, 0, -3));
glm::mat4 projectionMat;
// FOV 宽高比 近平面 远平面
projectionMat = glm::perspective(glm::radians(45.0f), screenWidth / screenHeight, 0.1f, 100.0f);
1
gl_Position = projection * view * model * vec4(aPos, 1.0f);
透视投影的示意:
1
2
// FOV 宽高比 近平面 远平面
glm::perspective(glm::radians(45.0f), (float)width/(float)height, 0.1f, 100.0f);
平行投影的示意:
1
2
// 左 右 下 上 近 远
glm::ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f);
OpenGL将深度信息存储于深度缓冲中,接下来启用深度测试。
1
2
3
glEnable(GL_DEPTH_TEST); // 启用深度测试
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清屏时清掉深度缓存
摄像机
摄像机需要一个位置和方向,方向可以用朝向、右向量、上向量来表示。有这些信息可以得到LookAt矩阵,可以很高效地从世界坐标变换到观察空间。
1
glm::lookAt(position, target, up);
摄像机的转动可以用欧拉角来实现,即俯仰Pitch、偏航Yaw、滚转Roll。
鼠标操作
1
2
3
4
// 捕捉光标
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
// 设置鼠标移动的回调函数
glfwSetCursorPosCallback(window, mouse_callback);
天空盒
天空盒需要加载立方体纹理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned int loadSkybox(std::vector<std::string> skyboxPaths) {
unsigned int textureSkybox;
glGenTextures(1, &textureSkybox);
glBindTexture(GL_TEXTURE_CUBE_MAP, textureSkybox);
int width, height, nrChannels;
unsigned char* data;
for (unsigned int i = 0; i < skyboxPaths.size(); i++)
{
data = stbi_load(skyboxPaths[i].c_str(), &width, &height, &nrChannels, 0);
glTexImage2D(
GL_TEXTURE_CUBE_MAP_POSITIVE_X + i,
0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data
);
}
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
return textureSkybox;
}
移除变换矩阵的位移
1
glm::mat4 view = glm::mat4(glm::mat3(camera.GetViewMatrix()));
用于贴图3D立方体的立方体贴图可以使用立方体的位置作为纹理坐标来采样。当立方体处于原点(0, 0, 0)时,它的每一个位置向量都是从原点出发的方向向量。这个方向向量正是获取立方体上特定位置的纹理值所需要的。
顶点着色器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#version 330 core
layout (location = 0) in vec3 aPos;
out vec3 TexCoords;
uniform mat4 projection;
uniform mat4 view;
void main()
{
TexCoords = aPos;
vec4 pos = projection * view * vec4(aPos, 1.0);
gl_Position = pos.xyww;
}
这里令z=w
,做透视除法时总会让结果为1.0,让该点总在最后面,被其他物体遮住。为了通过深度测试,需要修改
1
2
3
glDepthFunc(GL_LEQUAL)
// 渲染代码
glDepthFunc(GL_LESS);
片段着色器
1
2
3
4
5
6
7
8
9
10
11
#version 330 core
out vec4 FragColor;
in vec3 TexCoords;
uniform samplerCube skybox;
void main()
{
FragColor = texture(skybox, TexCoords);
}
实例化渲染
glDrawArrays
和glDrawElements
的渲染是CPU和GPU通信,绘制多个相似物体时应该减少通信次数,使用实例化渲染,调用分别改为glDrawArraysInstanced
和glDrawElementsInstanced
。需要一个额外的参数表示需要渲染的实例个数。
1
2
3
layout (location = 2) in vec3 aOffset;
// ...
gl_Position = projection * view * model * vec4(aPos + aOffset, 1.0f);
1
2
3
4
5
6
7
8
9
10
11
12
unsigned int instanceVBO;
glGenBuffers(1, &instanceVBO);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(glm::vec2) * 32768, &translations[0], GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glEnableVertexAttribArray(2);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
// 在渲染一个新实例的时候更新顶点属性
glVertexAttribDivisor(2, 1);