文章

OpenGL学习日记

OpenGL学习日记

环境搭建

下载glwe-2.1.0glfw-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等其它语言大部分的默认基础数据类型:intfloatdoubleuintbool,GLSL也有两种容器类型,分别是向量(Vector)和矩阵(Matrix)。 GLSL中的向量是一个可以包含有2、3或者4个分量的容器,分量的类型可以是前面默认基础类型的任意一个。vecnbvecnivecnuvecndvecn

顶点着色器应该通过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 超出的坐标为用户指定的边缘颜色。

可以对st坐标轴分别设置环绕方式。

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);
}

实例化渲染

glDrawArraysglDrawElements的渲染是CPU和GPU通信,绘制多个相似物体时应该减少通信次数,使用实例化渲染,调用分别改为glDrawArraysInstancedglDrawElementsInstanced。需要一个额外的参数表示需要渲染的实例个数。

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);
本文由作者按照 CC BY 4.0 进行授权