NJUPT-CG-OpenGL

由于疫情,南京邮电大学 2021-2022-1计算机图形学课程(课程代号:B0301312C )采用了线上教学模式。
鉴于这门课程的内容有一定难度,我已将课程回放保存并上传 bilibili 弹幕网BV1ib4y1Y74J)。此外,根据上课内容,我上传了在线授课过程中的代码(toulzx/NJUPT-CG-OpenGL)供大家参考。
^本文目前仅在语雀个人博客发布发布,内容为作者 tou 原创,不允许转载。谢谢!

README

声明

本文目前仅在语雀个人博客发布发布,内容为作者 tou 原创,不允许转载。谢谢!

简介

由于疫情,南京邮电大学 2021-2022-1计算机图形学课程(课程代号:B0301312C )采用了线上教学模式。

鉴于这门课程的内容有一定难度,我已将课程回放保存并上传 bilibili 弹幕网BV1ib4y1Y74J)。此外,根据上课内容,我上传了在线授课过程中的代码(toulzx/NJUPT-CG-OpenGL)供大家参考。

如何食用

项目结构

  • 文件夹 Source
    • 文件夹 HelloCG:此项目文件夹
    • 一些文件 .PDFs:功能库配置教程
  • 一些文件 .ZIPs:功能库

查阅历次代码及差异

我按照课程教学顺序,将历次课程代码提交到此仓库下。

test_4 分支的最新一次 commit 内容是本课程的最后一次作业(第 4 次作业),它是基于最后一次课程的代码内容。由于此分支包含了所有课程的代码内容的 commits,因此我将此分支设置为默认分支。

此外,此前另外 3 次作业的代码不在课程教学内容中,因此我额外创建了 test_3test_2test_1 分支,他们是从默认分支的某次课程内容延伸出去的。

你可以点击仓库主页的 commits,这可以阅览历次 commit 的信息, 当你点击 某次 commit 的标题 时,你可查看和上一次 commit 内容的差异,我认为这可能会方便大家查看历次课程代码的差异。

⚠ 注意:这样的做法是我的首次尝试,有些 commits 的差异由于失误并不能正确预览,我会在出现这种情况的 commit 差异页面提供批注。当然,你完全可以不参考 commit 差异,你仍然可以下载运行可使用的的历次课程代码(见下)。

历次课程代码

文件夹目录链接

当你点开任意课程代码对应的 commit 链接后,即可查看相应课程内容对应的代码文件:

下载

你可以通过 Git 直接将项目 clone 到本地,然后借助编辑器内置的 Git 管理工具即可轻松切换历次版本。
如果你看不懂上面这一行我在说什么,你可以拷贝链接到 DownGit 下载。

友情链接

关于教程

我推荐你学习这篇教程:LearnOpenGL,你会发现此课程(特指 2021-2022 学年第 1 学期的线上课程)的授课思路参考了这份教程。

课程笔记 & 代码

一点建议

不要未经思考直接 copy 代码。

首先,这对你学习这门课程没有好处;其次,睿检查作业的时候,会查看你的代码并询问细节。(不要尝试在睿的雷区蹦迪,你真的会后悔的)

Notes

Week 2-1

初始环境配置

你可以参考:【LearnOpenGL】编译和链接 GLEW

配置附加库目录,并使用静态链接的方式引用

你需要下载本课程会使用到的附加库,并把它放在指定位置(你可以直接使用我仓库中上传的库):

在项目属性配置中,将附加库的目录添加进来

step1:

image.png
image.png

step2:

image.png
image.png

step3:
1
2
3
opengl32.lib
glew32s.lib
glfw3.lib

image.png
image.png

注意:
  1. glfw 配置链接器的附加库目录的时候注意根据 Visual Studio 版本选择。
  2. 建议统一使用 32 位。

在测试代码中引用附加库目录

1
2
3
4
5
//GLEW 采用静态编译的方式
#define GLEW_STATIC
#include <GL/glew.h>
//GLFW
#include <GLFW/glfw3.h>

#define GLEW_STATIC:如果存在则优先使用静态库。
#include <GL/glew.h>: 就是这张图中的路径:
image.png
同理 glfw.

当你正确添加了附加库目录后,以上代码段编辑器不会标红报错。

然后你可以尝试使用 DEMO 测试运行:
👉 测试 demo 的代码

测试效果如下:
image.png

重写 Demo

const GLint WIDTH = 800, HEIGHT = 600;
窗口大小定义为常量。
GLint 前缀 GL 表示 OpenGL 支持的数据类型(支持 C、C++ 所有基本数据类型,只不过要增加前缀)
OpenGL 主要使用 int unsignedInt float (显卡用不到 double 类型,资源浪费,我们需要在不损失性能的情况下拥有最快速度)。

glfwInit()
初始化.
函数命名方式有特点 : 小写库名+函数名

glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
属性.
glfw 主副版本号设置:3.3

glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
属性.
窗口用途:画 OpenGL ,使用版本: CORE OpenGL

glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
属性.
使其向前兼容 (Mac 必须写,Win 系统已经默认了)

glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);
属性.
不允许改变窗口大小,会影响投影、透视的情况.

GLFWwindow* window = glfwCreateWindow(WIDTH, HEIGHT, "Learn OpenGL", nullptr, nullptr);
GLFW 固定变量命名方式特点: 大写库名+变量名.
宽、高、标题、全屏、多屏

int screenWidth, screenHeight;
glfwGetFramebufferSize(window, &screenWidth, &screenHeight);
获得显存下实际窗口大小.
屏幕硬件可能导致显示的每 1 个像素的硬件构成不止 1 个,由于屏幕显示直接映射在显存上,这种情况下显存空间大于屏幕显示的空间尺寸.

glfwMakeContextCurrent(window);
设置焦点为当前窗口

glewExperimental = GL_TRUE;
必须设置的参数
怪异的保留变量,函数的命名方式,变量的赋值

glViewport(0, 0, screenWidth, screenHeight);
视口.
左下角坐标、显存空间的宽高,这里明显填满了整个窗口

glfwPollEvents();
获取标志信息.
比如:鼠标、键盘使用信息,都会被标志(Week 2-1 还用不到)

glClearColor (GLclampf red, GLclampf green, GLclampf blue, GLclampf alpha);
初始化一个颜色.
颜色用浮点数表示,如果有 alpha 值就不压缩

glClear(GL_COLOR_BUFFER_BIT);
用此颜色清理缓存区,进行初始化赋值操作(设置背景色)

glfwSwapBuffers(GLFWwindow* window);
双缓存机制.
当传输当前帧时,利用这个时间加载下一帧(创建空间绘制下一帧)

Week 2-2 & Week 3-1

顶点着色器(Vector Shader)

1
2
3
4
5
6
#version 330 core
layout(location = 0) in vec3 position;
void main()
{
gl_Position = vec4(position, 1.0f);
}

#version 330 core
类似 core profile 含义,版本号 3.3

layout(location = 0) in vec3 position;
如何从显存获取并映射变量.
变量 position .
三个浮点数构成的向量 vec3.
从显存硬件区域直接获取 in.

gl_Position = vec4(position, 1.0f);
最终顶点信息.
gl_Position 预保留变量,四维信息.

片元着色器(Fragment Shader)

大部分情况等同 像素着色器 Pixels Shader
有时候 1 个像素被不同着色器覆盖绘制多次,片源的描述更准确。

1
2
3
4
5
6
#version 330 core
out vec4 color;
void main()
{
color = vec4(1.0f, 0.5f, 0.2f, 1.0f); // 使他偏橙色
}

color = vec4(1.0f, 0.5f, 0.2f, 1.0f);
颜色四维.
对每个片源进行颜色赋值.

导入并编译着色器

GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
创建着色器对象.

glShaderSource(vertexShader, 1, &vertexShaderCode, NULL);
把代码存入,传 1 个,代码,起始位置 NULL(整个传).

glCompileShader(vertexShader);
编译.
生成目标代码也在 vertexShader
里面东西很多了:源代码、编译过程状态、失败日志、成功标志位、编译结果.

glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
后缀iv :能够获取的返回参数的数据类型 integer & vertex
COMPILE_STATUS : 这里是编译状态的标志位
success: 存放编译状态信息

创建并链接可执行文件


屏幕坐标系

你可以参考:【LearnOpenGL】坐标系统 | 进入 3D

OpenGL 采用右手坐标系:z 朝外为正,x 右,y 上

image.png

传入数据到显卡

你可以参考:【LearnOpenGL】你好,三角形

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
GLfloat vertices[] =
{ // position
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};

// 创建顶点正面对象(VAO)、顶点缓存对象(VBO)
GLuint VAO, VBO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);

// 绑定 VAO、VBO
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);

// 数据传入显卡
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

// 设置 VAO
// 对应此句:"layout(location = 0) in vec3 position;" 和上述 vertices[]
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);

// 解绑定 VAO、VBO
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);

着色器不接收多余输入
VAO 顶点正面对象:解释数据
VBO 顶点缓存对象:传输数据,说明在显存空间上位置
他们一定成对出现!
绑定,先 a 后 b

glBindBuffer(GL_ARRAY_BUFFER, VBO);
在显卡上确定一段区域作为目的地.

glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
设置标志位 static 表示常读不常写,这样显存会寻找合适位置分配.

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);

  • 位置 0
  • 3 个浮点数: 对应 vec3 (float)
  • vertices[] 起始位取 :(GLvoid*)0
  • 不需要对顶点标准化处理(-1 到 1 之间): GL_FALSE
  • 每次取 3 个

调用着色器

1
2
3
4
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);

glUseProgram(shaderProgram);
绑定着色器

glDrawArrays(GL_TRIANGLES, 0, 3);
position 从 0 开始,画 3 个顶点(两个三角形就是 6 个)

Week 3-2

关于 Visual Studio 的“解决方案资源管理器”

添加文件注意,这里是逻辑添加,不一定对应实际文件夹路径:
image.png

建议先本地建好文件,对应添加进来:
image.png

关于着色器的文件后缀

.fs.vs 只是我们为了方便区分顶点着色器和片源着色器而进行个性化命名的。

事实上,在 Week 2-2 & Week 3-1 课程中,甚至是本次课程 Week 3-2 你就可能注意到,我们对顶点着色器和片源着色器的引用是通过调用这部分的代码的字符串内容:

1
2
const GLchar* vShaderCode = vertexCode.c_str();
const GLchar* fShaderCode = fragmentCode.c_str();

防止头文件重复引用

1
#pragma once

1
2
3
4
#ifndef Shader_h		// Shader.h 是文件名
#define Shader_h

#endif

Week 4-1

1
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)(3*sizeof(GLfloat)));

注意这里是 3*sizeof(GLfloat)
改变各个顶点颜色,要向顶点着色器中新传递颜色信息,并且要传给片元着色器:

1
2
3
4
5
// vertexShader

layout(location = 1) in vec2 vertexColor;

out vec3 ourColor;
1
2
3
4
5
// fragmentShader

in vec3 ourColor;

color = vec4(ourColor, 1.0f);

效果:
image.png
我们设置了三个顶点的颜色,为什么中间的颜色会相互融合?这里涉及到了栅格化的知识:

这个图片可能不是你所期望的那种,因为我们只提供了 3 个颜色,而不是我们现在看到的大调色板。这是在片段着色器中进行的所谓片段插值 (Fragment Interpolation) 的结果。当渲染一个三角形时,光栅化 (Rasterization) 阶段通常会造成比原指定顶点更多的片段。光栅会根据每个片段在三角形形状上所处相对位置决定这些片段的位置。
基于这些位置,它会插值 (Interpolate) 所有片段着色器的输入变量。比如说,我们有一个线段,上面的端点是绿色的,下面的端点是蓝色的。如果一个片段着色器在线段的 70% 的位置运行,它的颜色输入属性就会是一个绿色和蓝色的线性结合;更精确地说就是 30% 蓝 + 70% 绿。
这正是在这个三角形中发生了什么。我们有 3 个顶点,和相应的 3 个颜色,从这个三角形的像素来看它可能包含 50000 左右的片段,片段着色器为这些像素进行插值颜色。如果你仔细看这些颜色就应该能明白了:红首先变成到紫再变为蓝色。片段插值会被应用到片段着色器的所有输入属性上。
^以上内容引用自【LearnOpenGL】着色器

使用两个三角形拼成正方形

矩形的实现通过两个三角形合成.
仔细看坐标其实我们要生成的是正方形,但是展示的结果是长方形,这个跟我们窗口长宽有关。

我们主要修改了这部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 顶点位置集 rectangle
GLfloat vertices[] =
{ // position // color
// first triangle
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f,

// second triangle
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f,
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f
};

记得修改:glDrawArrays(GL_TRIANGLES, 0, 6);,我们使用了 6 个顶点绘制两个三角形了。

Week 5-1

改用点和连接信息构建正方形

你可以参考:【LearnOpenGL】你好,三角形 | 索引缓冲对象(EBO)

单纯用点来构成物体,当物体多样了,点多了,复杂度高
我们使用点和连接信息来构建物体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 定义点和颜色
GLfloat vertices[] =
{ // position // color
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // 右上
0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // 右下
-0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // 左下
-0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // 左上
};

// 定义连接信息
unsigned int indices[] =
{
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};

连接信息使用 EBO(elemental buffer object 元素缓冲对象)生成绑定。

VAO、VBO 一定成对出现
但连接信息 EBO 并不是,随时可变。

glDrawArrays(GL_TRIANGLES, 0, 6);
while 中 此句不再适用了
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
改用此句

配置 SOIL2

你可以参考:OpenGL SOIL2.pdf

根据参考链接做即可,项目中的配置方法和以前差不多,不过此处只需要添加一处附加库目录即可:
image.png

Week 5-2

纹理

纠正一个网络上普遍的错误:
OpenGL 推荐 2^n 的纹理,但不是强制的,它只是有利于生成多层渐进纹理。非 2^n 只是无法生成 mipmap(多层渐进纹理)。

你可以参考:【LearnOpenGL】纹理 | 纹理的环绕方式

glBindTexture(GL_TEXTURE_2D, texture);
二维纹理:广泛被使用,图片

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
S、T、R(维度),对应 x、y、z(轴)

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
当纹理大于物体时候的设置
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
当纹理小于物体时候的设置

1
unsigned char* image = SOIL_load_image("images/T_Reflection_Tiles_D.BMP", &imgWidth, &imgHeight, 0, SOIL_LOAD_RGBA);

读图片.
SOIL_LOAD_RGBA 当最后一个透明度没有也会强制生成不透明(即值为 1)

1
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, imgWidth, imgHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, image);
  • GL_TEXTURE_2D 区域
  • 0
  • 目标区域 RGBA
  • 宽高
  • 边缘部分处理
  • 和读取时候的最后一个参数一致
  • 数据类型
  • 图片

生成后即可解绑定.

glUniform1i(glGetUniformLocation(ourShader.Program, "texture0"), 0);
while 中将纹理图片和纹理坐标对应

uniform sampler2D texture0;
sampler2D 实际上是 int 型

glUniform1i(glGetUniformLocation(ourShader.Program, "texture0"), 0);
这里第二个参数传 0 代表位置即可,不用是变量

ourTextCoord = vec2(textCoord.x, 1 - textCoord.y);
vertexShader中不这么设置,直接传的是上下颠倒的!
因为 OpenGL 要求 y 轴 0.0 坐标是在图片的底部的,但是图片的 y 轴 0.0 坐标通常在顶部

Week 7-1

简单增加 36 个三角形
vertices 中交换两块的位置,看到的颜色会变

深度测试

新加入的颜色信息会覆盖原有的,你的立方体可能背面显示在正面之前了…

glEnable(GL_DEPTH_TEST);
开启深度测试
glDepthFunc(GL_LESS);
当小于再改变

glClear(GL_COLOR_BUFFER_BIT | **GL_DEPTH_BUFFER**);
while 中添加,使不要每次重新绘制

此时编译出来的正面就不是绿色而是红色了

只有深度、模板、颜色每次开启时需要重新初始化(while
其它不用

正方体的平移、旋转和缩放

1
2
3
glm::mat4 transform = glm::mat4(1.0f);
GLuint transLoc = glGetUniformLocation(ourShader.Program, "transform");
glUniformMatrix4fv(transLoc, 1, GL_FALSE, glm::value_ptr(transform));

获得位置、数量、不进行转置、uniform 形式传值

GLSL 两种获得输入的方式:

  • _in__ 来自显存_
  • _uniform__ 来自 CPU_

1
transform = glm::rotate(transform, glm::radians(20.0f)*static_cast<GLfloat>(glfwGetTime()), glm::vec3(1.0f, 1.0f, 1.0f));

绕 (1,1,1) 向量轴旋转。
static_cast<GLfloat>(glfwGetTime())
临时使用的时间系数。

transform = glm::scale(transform, glm::vec3(0.5f, 0.5f, 0.5f));
缩放实际上是针对对角线的点操作

1
2
3
4
5
6
7
8
9
10
// main.cpp

glm::mat4 transform = glm::mat4(1.0f);

transform = glm::translate(transform, glm::vec3(0.0f, 0.4f, 0.0f));
transform = glm::rotate(transform, glm::radians(20.0f) * static_cast<GLfloat>(glfwGetTime()), glm::vec3(1.0f, 1.0f, 1.0f));
transform = glm::scale(transform, glm::vec3(0.5f, 0.5f, 0.5f));

GLuint transLoc = glGetUniformLocation(ourShader.Program, "transform");
glUniformMatrix4fv(transLoc, 1, GL_FALSE, glm::value_ptr(transform));

各个变换的顺序有意义,后写的先执行!!!
将值通过 uniform 形式传送给着色器,这样你可以在着色器使用 uniform mat4 transform 接收。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// vectorShader

out vec3 ourColor;

uniform mat4 transform;


void main()
{

gl_Position = transform * vec4(position, 1.0f);
ourColor = vertexColor;

}

Week 7-2

在透视矩阵加持下,生成真正的正方体

你可以参考:【LearnOpenGL】坐标系统 | 进入 3D

1
2
3
4
5
// main.cpp

glm::mat4 projection = glm::perspective(glm::radians(90.0f), float(screenWidth) / float(screenHeight), 0.1f, 100.0f);

glUniformMatrix4fv(glGetUniformLocation(ourShader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// vectorShader

#version 330 core

layout(location = 0) in vec3 position;
layout(location = 1) in vec2 textCoord;

out vec2 ourTextCoord;

uniform mat4 transform;
uniform mat4 projection;


void main()
{

gl_Position = projection * transform * vec4(position, 1.0f); // 注意顺序
ourTextCoord = vec2(textCoord.x, 1 - textCoord.y);

}

各个变换的顺序有意义,后写的先执行!!!

Week 8-1

Camera.h 构造函数

你可以复习:c++ 带参构造函数与初始化列表

1
2
3
4
5
6
7
8
9
10
11
12
Camera(glm::vec3 nPosition = DEFAULT_POSITION)
:
cameraPosition(nPosition),
worldUp(DEFAULT_WORLD_UP),
yaw(DEFAULT_YAW),
pitch(DEFAULT_PITCH),
movementSpeed(DEFAULT_SPEED),
cameraFront(DEFAULT_CAMERA_FRONT),
zoom(DEFAULT_ZOOM)
{
this->updateCameraVectors();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// 以上使用初始化列表的写法等价于下面这种:

Camera(glm::vec3 nPosition = DEFAULT_POSITION)
{
this->position = nPosition;
this->worldUp = DEFAULT_WORLD_UP;
this->yaw = DEFAULT_YAW;
this->pitch = DEFAULT_PITCH;
this->movementSpeed = DEFAULT_SPEED;
this->cameraFront = DEFAULT_CAMERA_FRONT;
this->zoom = DEFAULT_ZOOM;
this->updateCameraVectors();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// 老师上课时的写法

Camera(glm::vec3 position = glm::vec3(0.0f, 0.0f, 0.0f),
glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f),
GLfloat yaw = DEFAULT_YAW,
GLfloat pitch = DEFAULT_PITCH) :cameraFront(glm::vec3(0.0f, 0.0f, -1.0f)), zoom(DEFAULT_ZOOM), movementSpeed(DEFAULT_SPEED)
{
this->position = position;
this->worldUp = up;
this->yaw = yaw;
this->pitch = pitch;
this->updateCameraVectors();
}

以上这三段代码实现的结果一致。

摄像机观察空间

[

](https://learnopengl-cn.github.io/01%20Getting%20started/09%20Camera/)

你可以参考:【LearnOpenGL】摄像机

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 摄像机坐标
glm::vec3 position;

// 世界坐标系下的一个向上(y)的方向向量,我们会定义它为(0,1,0)
glm::vec3 worldUp;

// front(`Direction` in the pic), camera axis Z, 相机坐标系的 Z 轴
glm::vec3 cameraFront;

// right, camera axis X, 相机坐标系的 X 轴
glm::vec3 cameraRight;

// up, camera axis Y, 相机坐标系的 Y 轴
glm::vec3 cameraUp;
1
2
3
4
5
6
7
8
9
10
11
12
13
	void updateCameraVectors()
{
glm::vec3 front;
front.x = cos(glm::radians(this->pitch)) * cos(glm::radians(this->yaw));
front.y = sin(glm::radians(this->pitch));
front.z = cos(glm::radians(this->pitch)) * sin(glm::radians(this->yaw));

this->cameraFront = glm::normalize(front);
this->cameraRight = glm::normalize(glm::cross(this->cameraFront, this->worldUp));
this->cameraUp = glm::normalize(glm::cross(this->cameraRight, this->cameraFront));
}

};

this->cameraRight = glm::normalize(glm::cross(this->cameraFront, this->worldUp));
和 LearnOpenGL 的教程有点不一样。
是因为我们课程里的 camera front 设置为反方向,教程接下来就用了这种方法

this->worldUp 的引入是因为只有相机坐标系的 z 方向是没办法确定 x 方向的,但是利用叉积的原理,我们并不需要准确的 y 方向,找一个在同一平面的作叉积就得到相同的结果,所以我们自然的选择了世界坐标系下的 y 方向向量。

使用矩阵的好处之一是如果你使用 3 个相互垂直(或非线性)的轴定义了一个坐标空间,你可以用这 3 个轴外加一个平移向量来创建一个矩阵,并且你可以用这个矩阵乘以任何向量来将其变换到那个坐标空间。这正是 LookAt 矩阵所做的,现在我们有了 3 个相互垂直的轴和一个定义摄像机空间的位置坐标,我们可以创建我们自己的 LookAt 矩阵了: > image.png > LookAt=[RxRyRz0UxUyUz0DxDyDz00001]∗[100−Px010−Py001−Pz0001] > 其中 R 是右向量,U 是上向量,D 是方向向量 P 是摄像机位置向量。注意,位置向量是相反的,因为我们最终希望把世界平移到与我们自身移动的相反方向。把这个 LookAt 矩阵作为观察矩阵可以很高效地把所有世界坐标变换到刚刚定义的观察空间。LookAt 矩阵就像它的名字表达的那样:它会创建一个看着 (Look at) 给定目标的观察矩阵。

我们首先将摄像机位置设置为之前定义的 cameraPos。方向是当前的位置加上我们刚刚定义的方向向量。这样能保证无论我们怎么移动,摄像机都会注视着目标方向。让我们摆弄一下这些向量,在按下某些按钮时更新 cameraPos 向量。

1
2
3
4
glm::mat4 GetViewMatrix()
{
return glm::lookAt(this->position, this->position + this->cameraFront, this->cameraUp);
}

键盘控制摄像机视角移动

不同设备帧率不同,可能会导致移动速度不一样。我们利用时间一致,用时间来控制移动速度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

void DoMovement()
{
if (keys[GLFW_KEY_W] || keys[GLFW_KEY_UP])
{
camera.ProcessKeyboard(FORWARD, deltaTime);
}
if (keys[GLFW_KEY_S] || keys[GLFW_KEY_DOWN])
{
camera.ProcessKeyboard(BACKWARD, deltaTime);
}
if (keys[GLFW_KEY_A] || keys[GLFW_KEY_LEFT])
{
camera.ProcessKeyboard(LEFT, deltaTime);
}
if (keys[GLFW_KEY_D] || keys[GLFW_KEY_RIGHT])
{
camera.ProcessKeyboard(RIGHT, deltaTime);
}
}

1
2
3
4
5
6
7
8
9
10
11
12

void ProcessMouseMovement(GLfloat xOffset, GLfloat yOffset)
{
xOffset *= this->mouseSensitivity;
yOffset *= this->mouseSensitivity;

this->yaw += xOffset;
this->pitch += yOffset;

this->updateCameraVectors();

}

如果你现在运行代码,你会发现在窗口第一次获取焦点的时候摄像机会突然跳一下。这个问题产生的原因是,在你的鼠标移动进窗口的那一刻,鼠标回调函数就会被调用,这时候的 xpos 和 ypos 会等于鼠标刚刚进入屏幕的那个位置。这通常是一个距离屏幕中心很远的地方,因而产生一个很大的偏移量,所以就会跳了。我们可以简单的使用一个 bool 变量检验我们是否是第一次获取鼠标输入,如果是,那么我们先把鼠标的初始位置更新为 xpos 和 ypos 值,这样就能解决这个问题;接下来的鼠标移动就会使用刚进入的鼠标位置坐标来计算偏移量了:

1
2
3
4
5
6
7
8
9
10
11
void ProcessMouseMovement(GLfloat xOffset, GLfloat yOffset)
{
xOffset *= this->mouseSensitivity;
yOffset *= this->mouseSensitivity;

this->yaw += xOffset;
this->pitch += yOffset;

this->updateCameraVectors();

}

当滚动鼠标滚轮的时候,yoffset 值代表我们竖直滚动的大小。当 scroll_callback 函数被调用后,我们改变全局变量 fov 变量的内容。因为 45.0f 是默认的视野值,我们将会把缩放级别 (Zoom Level) 限制在 1.0f 到 45.0f。

Week 8-2 & Week 8-3

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
/* 原立方体 */

ourShader.Use();

glm::mat4 transform = glm::mat4(1.0f);
// transform = glm::translate(transform, glm::vec3(0.0f, 0.0f, -2.0f));
transform = glm::rotate(transform, glm::radians(20.0f), glm::vec3(1.0f, 1.0f, 1.0f));
// transform = glm::scale(transform, glm::vec3(0.5f, 0.5f, 0.5f));
glUniformMatrix4fv(glGetUniformLocation(ourShader.Program, "transform"), 1, GL_FALSE, glm::value_ptr(transform));

glm::mat4 projection = glm::perspective(glm::radians(camera.GetZoom()), float(screenWidth) / float(screenHeight), 0.1f, 100.0f);
glUniformMatrix4fv(glGetUniformLocation(ourShader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection));

glm::mat4 view = camera.GetViewMatrix();
glUniformMatrix4fv(glGetUniformLocation(ourShader.Program, "view"), 1, GL_FALSE, glm::value_ptr(view));

//
glUniform3f(glGetUniformLocation(ourShader.Program, "LightPos"), lightPos.x, lightPos.y, lightPos.z);
glUniform3f(glGetUniformLocation(ourShader.Program, "ViewPos"), camera.GetPosition().x, camera.GetPosition().y, camera.GetPosition().z);
glUniform1f(glGetUniformLocation(ourShader.Program, "material.diffuse"), DIFFUSE);
glUniform1f(glGetUniformLocation(ourShader.Program, "material.specular"), SPECULAR);
//

glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
glBindVertexArray(0);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* 光源立方体 */

lightShader.Use();

glm::mat4 transformLight = glm::mat4(1.0f);
lightPos = glm::rotate(lightPos, glm::radians(0.05f), glm::vec3(1.0f, 1.0f, 1.0f));
transformLight = glm::translate(transformLight, lightPos);
transformLight = glm::scale(transformLight, glm::vec3(0.1f, 0.1f, 0.1f));
glUniformMatrix4fv(glGetUniformLocation(lightShader.Program, "transform"), 1, GL_FALSE, glm::value_ptr(transformLight));

glm::mat4 projectionLight = glm::perspective(glm::radians(camera.GetZoom()), float(screenWidth) / float(screenHeight), 0.1f, 100.0f);
glUniformMatrix4fv(glGetUniformLocation(lightShader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projectionLight));

glm::mat4 viewLight = camera.GetViewMatrix();
glUniformMatrix4fv(glGetUniformLocation(lightShader.Program, "view"), 1, GL_FALSE, glm::value_ptr(viewLight));

lightModel.Draw();

除去光照部分的设置,两立方体的设置都是对称的格式!

思考:OpenGL z 轴方向问题

OpenGL 中世界坐标系下,是遵循右手准则的。DX 中才是左手准则。

当我们转换到相机坐标系下后,OpenGl 会对坐标进行归一化处理(归一化设备坐标系 NDC)。这里有一个问题,NDC 是遵循左手准则的。

这在相机坐标系下,对归一化后的 NDC 来说,近的物体(z 轴)坐标值要小于远处的物体。
而我们最开始定义 vertices 的时候,是按照世界坐标系定义的,如果我们没有处理,直接传给 NDC 坐标系,所以这会导致 z 轴设置的前后对称(相反):

1
2
3
4
5
6
in vec3 aPos;

void main()
{
gl_Position = vec4(aPos, 1.0);
}

一般的,解决方法是在将世界坐标系下的坐标传入 NDC 之前,我们会对其用投影矩阵转换输入的顶点:

1
2
3
4
5
6
7
in vec3 aPos;
mat4 modelProjectionMatrix;

void main()
{
gl_Position = modelProjectionMatrix * vec4(aPos, 1.0);
}

参考:

OpenGL Projection Matrix

Note that both xp and yp depend on ze; they are inversely propotional to -ze. In other words, they are both divided by -ze. It is a very first clue to construct GL_PROJECTION matrix. After the eye coordinates are transformed by multiplying GL_PROJECTION matrix, the clip coordinates are still a homogeneous coordinates. It finally becomes the normalized device coordinates (NDC) by divided by the w-component of the clip coordinates. (See more details on OpenGL Transformation.) > > Finally, we found all entries of GL_PROJECTION matrix. The complete projection matrix is; > image.png

stackoverflow | opengl-z-value-why-negative-value-in-front

Of course the red triangle is in front of the blue one, because you don’t use any projection matrix. You forgot to transform the input vertex by the projection matrix before you assign the vertex coordinate to gl_Position. > This causes that the vertices are equal to the normalized device space coordinates. In normalized device space the z-axis points into the viewport and the “projection” is orthographic and not perspective.

简书:OpenGL NDC 左手还是右手

null

CSDN | OpenGL 默认的 Z 轴方向问题

OpenGL 的 NDC 默认是左手坐标系(Z 轴正方向指向屏幕内侧),范围为(-1.0, 1.0);
OpenGL 的空间坐标系使用右手坐标系,Z 轴正方向指向屏幕外侧;
> 上面代码中传入的坐标没有经过变换(MVP),直接赋值作为最终的 NDC 坐标,因此展现了这种情况

GitHub | LearnOpenGL | 坐标系统

为了将坐标从一个坐标系变换到另一个坐标系,我们需要用到几个变换矩阵,最重要的几个分别是模型 (Model)、观察 (View)、投影 (Projection) 三个矩阵。我们的顶点坐标起始于局部空间 (Local Space),在这里它称为局部坐标 (Local Coordinate),它在之后会变为世界坐标 (World Coordinate),观察坐标 (View Coordinate),裁剪坐标 (Clip Coordinate),并最后以屏幕坐标 (Screen Coordinate) 的形式结束。

Exercise

第 1 次作业:矩形颜色交替变换

方法 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* main.cpp */
// 逐帧画图
while (!glfwWindowShouldClose(window))
{
glViewport(0, 0, screenWidth, screenHeight);
glfwPollEvents();
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);

// 使用着色器绘制一个三角形
ourShader.Use();

// 作业 1 颜色交替
float time = glfwGetTime();
float colorValue = ( sin(time) / 2 ) + 0.5;
glUniform1f(glGetUniformLocation(ourShader.Program, "colorValue"), colorValue);

glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0);

glfwSwapBuffers(window);
}

float time = glfwGetTime();
很大的值

float colorValue = ( sin(time) / 2 ) + 0.5;
写法不唯一,只是让 time 值变小,并且为正,使其在 0 到 1 变化(颜色范围边界是 0~1)

glUniform1f(glGetUniformLocation(ourShader.Program, "colorValue"), colorValue);
传送 1f 即 1 个 float

查询 uniform 地址不要求你之前使用过着色器程序,但是更新一个 uniform 之前你必须先使用程序(调用 glUseProgram),因为它是在当前激活的着色器程序中设置 uniform 的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* .fs */
#version 330 core


out vec4 color;


in vec3 vertexColor;

uniform float colorValue;


void main()
{

color = vec4(vertexColor.x,
vertexColor.y + colorValue,
vertexColor.z,
1.0f);

}

uniform float colorValue;
接收传送内容

color = vec4(vertexColor.x, vertexColor.y + colorValue, vertexColor.z, 1.0f);
colorValue 从 0~0.5 变化

此时由于预定义三角形颜色 vertexColor = (1, 0, 0) 红色
color = (1, 0 + colorValue, 0)(1, 0, 0)红色 和 (1, 1, 0) 橙色 之间变化.

方法 2

传 vec4,这样的话原来传的值就被这里新定义的覆盖了

1
glUniform4f(glGetUniformLocation(ourShader.Program, "newVertexColor"), 1.0f, colorValue, 0.0f, 1.0f);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* .fs */
#version 330 core

out vec4 color;

in vec3 vertexColor;

uniform vec4 newVertexColor;


void main()
{

color = vec4(newVertexColor);

}

第 2 次作业:纹理替换

睿的要求:纹理变化常通过纹理平移来实现,所以不只是渐变,还要平移!

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
//// 作业 2 纹理交替变换 ---- BEG

float time = glfwGetTime();
float translateValue = fmodf(time, 10.0f) / 10.0f;

if (((int)time / 10) % 2)
{
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture0);
glUniform1i(glGetUniformLocation(ourShader.Program, "texture0"), 0);

glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture1);
glUniform1i(glGetUniformLocation(ourShader.Program, "texture1"), 1);
} else {
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture0);
glUniform1i(glGetUniformLocation(ourShader.Program, "texture0"), 1);

glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture1);
glUniform1i(glGetUniformLocation(ourShader.Program, "texture1"), 0);
}

glUniform1f(glGetUniformLocation(ourShader.Program, "translateValue"), translateValue);

//// 作业 2 纹理交替变换 ---- END

if 语句的作用是使纹理始终沿一个方向变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* .vs */

#version 330 core

layout(location = 2) in vec2 textCoord;
out vec2 ourTextCoord;
out float ourTranslateValue;

uniform float translateValue;

void main()
{

ourTextCoord = vec2(textCoord.x, 1 - textCoord.y - translateValue);
ourTranslateValue = translateValue;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* .fs */

#version 330 core

in vec2 ourTextCoord;
in float ourTranslateValue;

void main()
{


color = mix(texture(texture0, ourTextCoord), texture(texture1, ourTextCoord), ourTranslateValue);

}

利用 mix() 实现渐变,最后一个参数是线性插值,[0,1] ,极端值分别取首个、第二个纹理。
修改纹理坐标,使纹理移动。
顺便利用这个变量 translateValue,进行线性插值,实现纹理的渐变转换。

第 3 次作业:地月模型

根据本次作业布置的时间,本次作业没有利用摄像机视角,没有利用光源模型。

1
2
newPos = glm::rotate(newPos, glm::radians(0.1f), glm::vec3(1.0f, 1.0f, 0.0f));
transform = glm::translate(transform, glm::vec3(newPos.x, newPos.y, newPos.z - 2));

glm::rotate 的第 3 个参数是根据从坐标原点 (0, 0, 0) 出发的向量的轴旋转的。
由于我们设置了中心正方体的坐标在 (0, 0, -2)(因为此时人也在原点),所以我们需要把坐标在原点设置完自转效果,再通过 glm::rotate 移动到 (0, 0, -2)

⚠ 注意:必须加上这个附加库的头文件使用:
#include <glm/gtx/rotate_vector.hpp>

我重复利用同一个正方体的顶点数据 vertices[],去生成大小位置不一样的两个正方体:

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
28
29
30
31
32
ourShader.Use();


glm::mat4 transform;
glm::mat4 projection = glm::perspective(glm::radians(90.0f), float(screenWidth) / float(screenHeight), 0.1f, 100.0f);

glUniformMatrix4fv(glGetUniformLocation(ourShader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection));

/* 中心正方体(地球) */

transform = glm::mat4(1.0f);
transform = glm::translate(transform, glm::vec3(0.0f, 0.0f, -2.0f));
transform = glm::rotate(transform, glm::radians(20.0f) * static_cast<GLfloat>(glfwGetTime()), glm::vec3(sin(23.5 * PI / 180), cos(23.5 * PI / 180), 0));
transform = glm::scale(transform, glm::vec3(0.5f, 0.5f, 0.5f));
glUniformMatrix4fv(glGetUniformLocation(ourShader.Program, "transform"), 1, GL_FALSE, glm::value_ptr(transform));

glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
glBindVertexArray(0);

/* 旋转正方体(月球) */

transform = glm::mat4(1.0f);
newPos = glm::rotate(newPos, glm::radians(0.05f), glm::vec3(sin(23.5 * PI / 180), cos(23.5 * PI / 180) , 0));
transform = glm::translate(transform, glm::vec3(newPos.x, newPos.y, newPos.z - 2));
transform = glm::rotate(transform, glm::radians(20.0f) * static_cast<GLfloat>(glfwGetTime()), glm::vec3(sin(23.5 * PI / 180), cos(23.5 * PI / 180), 0));
transform = glm::scale(transform, glm::vec3(0.1f, 0.1f, 0.1f));
glUniformMatrix4fv(glGetUniformLocation(ourShader.Program, "transform"), 1, GL_FALSE, glm::value_ptr(transform));

glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
glBindVertexArray(0);

第 4 次作业:实现控制物体的移动

引入物体坐标变量 ObjectPos

DoMovement() 函数针对物体坐标进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 物体上下左右(↑/↓/←/→)移动
if (keys[GLFW_KEY_UP]) {
ObjectPos.y += deltaTime * OBJECT_SPEED;
}
if (keys[GLFW_KEY_DOWN]) {
ObjectPos.y -= deltaTime * OBJECT_SPEED;
}
if (keys[GLFW_KEY_LEFT]) {
ObjectPos.x -= deltaTime * OBJECT_SPEED;
}
if (keys[GLFW_KEY_RIGHT]) {
ObjectPos.x += deltaTime * OBJECT_SPEED;
}

每次循环时应该更新物体的坐标,因为现在它可能在 DoMovement()中被修改了:

1
transform = glm::translate(transform, ObjectPos);
作者

tou

发布于

2021-11-19

更新于

2021-12-10

许可协议

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×