Transformation in OpenGL

Transformation in OpenGL

实际在 OpenGL 中的变换的一些记录,尤其是与 「GAMES101」Transformation 推导中的一些区别。其中旋转、平移等常见变换以及视图变换均是相同的。这里主要讨论正交投影和透视投影,以及法线变换矩阵的推导。

NDC 坐标 (Normalized Device Coordinates, 标准化设备坐标)

为了得到与 glm 中相同的结果,我们得先了解一下 NDC 坐标。在 OpenGL 中,标准化设备坐标是一个 $x, y, z$ 值在 $[-1, 1]$ 的一小段空间。需要注意的是,NDC 坐标是左手坐标系
Test NDC
其中绿色三角形 $z$ 值为 $-0.5$,红色为 $0.5$,可以看出其为左手坐标系。

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>

const int WIDTH = 1920;
const int HEIGHT = 1080;

int main() {
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow *window =
glfwCreateWindow(WIDTH, HEIGHT, "NDC test", nullptr, nullptr);
if (!window) {
std::cerr << "failed to create window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
if (!gladLoadGL()) {
std::cerr << "failed to load glad" << std::endl;
}
glfwSetFramebufferSizeCallback(
window, [](GLFWwindow *, int w, int h) { glViewport(0, 0, w, h); });
glEnable(GL_DEPTH_TEST);
GLuint vao, vbo;
glGenVertexArrays(1, &vao);
glGenBuffers(1, &vbo);
glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
GLfloat vertices[] {
-0.5f, 0.0f, 0.0f,
0.5f, 0.0f, 0.0f,
0.0f, 0.5f, 0.0f
};
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat),
reinterpret_cast<void *>(0));
glEnableVertexAttribArray(0);
const char *vsSource = R"(
#version 430 core
layout (location = 0) in vec3 pos;
uniform float offset;

void main() {
gl_Position = vec4(pos + vec3(offset / 5, offset / 10, offset), 1.0);
}
)";
const char *fsSource = R"(
#version 430 core
out vec4 color;
uniform float offset;

void main() {
color = vec4(0.5 + offset, 0.5 - offset, 0.0, 1.0);
}
)";
GLuint vs = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vs, 1, &vsSource, nullptr);
glCompileShader(vs);
GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fs, 1, &fsSource, nullptr);
glCompileShader(fs);
GLuint pg = glCreateProgram();
glAttachShader(pg, vs);
glAttachShader(pg, fs);
glLinkProgram(pg);
glDeleteShader(vs);
glDeleteShader(fs);
glUseProgram(pg);
while (!glfwWindowShouldClose(window)) {
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, GL_TRUE);
GLint loc = glGetUniformLocation(pg, "offset");
glUniform1f(loc, -0.5f);
glDrawArrays(GL_TRIANGLES, 0, 3);
glUniform1f(loc, 0.5f);
glDrawArrays(GL_TRIANGLES, 0, 3);
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwTerminate();
}

Orthographic projection (正交投影)

将 $[l, r] \times [b, t] \times \color{red}{[-n, -f]}$ (视野范围) 映射到 $[-1, 1] ^ 3$ 标准 (canonical) 立方体(映射到 NDC 坐标)。

注:与 GAMES101 推导不同,这里我们用 $-n, -f$ 表示近平面和远平面的 $z$ 值。

先平移,将中心移到原点,再缩放到 $[-1, 1] ^ 3$,缩放的同时将 $z$ 值取相反数以变换为 NDC 中的左手坐标系

$$\begin{aligned} M_{ortho} &= \begin{bmatrix} \frac{2}{r - l} & 0 & 0 & 0\\ 0 & \frac{2}{t - b} & 0 & 0\\ 0 & 0 & \color{red}{-\frac{2}{f - n}} & 0\\ 0 & 0 & 0 & 1 \end{bmatrix}\begin{bmatrix} 1 & 0 & 0 & -\frac{r + l}{2}\\ 0 & 1 & 0 & -\frac{t + b}{2}\\ 0 & 0 & 1 & -\frac{n + f}{2}\\ 0 & 0 & 0 & 1 \end{bmatrix}\\ &= \begin{bmatrix} \frac{2}{r - l} & 0 & 0 & -\frac{r + l}{r - l}\\ 0 & \frac{2}{t - b} & 0 & -\frac{t + b}{t - b}\\ 0 & 0 & -\frac{2}{f - n} & -\frac{f + n}{f - n}\\ 0 & 0 & 0 & 1 \end{bmatrix} \end{aligned}$$

与 glm 中一致

1
2
3
4
5
6
7
8
9
10
11
12
template<typename T>
GLM_FUNC_QUALIFIER mat<4, 4, T, defaultp> orthoRH_NO(
T left, T right, T bottom, T top, T zNear, T zFar) {
mat<4, 4, T, defaultp> Result(1);
Result[0][0] = static_cast<T>(2) / (right - left);
Result[1][1] = static_cast<T>(2) / (top - bottom);
Result[2][2] = - static_cast<T>(2) / (zFar - zNear);
Result[3][0] = - (right + left) / (right - left);
Result[3][1] = - (top + bottom) / (top - bottom);
Result[3][2] = - (zFar + zNear) / (zFar - zNear);
return Result;
}

Perspective projection (透视投影)

齐次坐标:$(x, y, z, 1), (kx, ky, kz, k \neq 0), (xz, yz, z ^ 2, z \neq 0)$ 都是同一个点

Fig. 7.13 from Fundamentals of Computer Graphics, 4th Edition

基本思路:将远平面挤压成和近平面同大小,再正交投影;近平面不会变,远平面中心点不会变

Persp to Ortho

从侧面 ($x$) 看,上图右侧 $-z$ 方向即为相机看向的方向,由相似三角形可得

$$y’ = \color{red}{\left|\frac{n}{z}\right|}y$$

同理

$$x’ = \color{red}{\left|\frac{n}{z}\right|}x$$

所以

$$\begin{bmatrix} x\\y\\z\\1 \end{bmatrix} \Rightarrow \begin{bmatrix} \color{red}{-nx/z}\\\color{red}{-ny/z}\\\text{unknown}\\1 \end{bmatrix} = ^ {\times \color{red}{-z}}\begin{bmatrix} nx\\ny\\\text{still unknown}\\\color{red}{-z} \end{bmatrix}$$

即求 $M_{persp \rightarrow ortho}$ 满足

$$M_{persp \rightarrow ortho} \begin{bmatrix} x\\y\\z\\1 \end{bmatrix}=\begin{bmatrix} nx\\ny\\\text{unknown}\\z \end{bmatrix}$$

于是有

$$M_{persp \rightarrow ortho}=\begin{bmatrix} n & 0 & 0 & 0\\ 0 & n & 0 & 0\\ ? & ? & ? & ?\\ 0 & 0 & \color{red}{-1} & 0 \end{bmatrix}$$

注意到任何一个近平面上的点不变,任何一个远平面上的点 $z$ 不会发生变化


利用近平面不变
用 $n$ 替换上面坐标变换中的 $z$,即取进平面上点 $(x, y, -n, 1)$,有

$$\begin{bmatrix} x\\y\\-n\\1 \end{bmatrix} \Rightarrow \begin{bmatrix} x\\y\\-n\\1 \end{bmatrix} = ^ {\times (z=n)}\begin{bmatrix} nx\\ny\\-n^2\\n \end{bmatrix}$$

对于 $M_{persp \rightarrow ortho}$ 的第三行必为 $(0, 0, A, B)$,有

$$\begin{bmatrix} 0 & 0 & A & B \end{bmatrix}\begin{bmatrix} x\\y\\-n\\1 \end{bmatrix}=-n^2$$

即有

$$-An + B = -n ^ 2$$


利用远平面 $z$ 不变
取点 $(0, 0, -f, 1) ^ T$,即有

$$\begin{bmatrix} 0\\0\\-f\\1 \end{bmatrix} \Rightarrow \begin{bmatrix} 0\\0\\-f\\1 \end{bmatrix} = \begin{bmatrix} 0\\0\\-f^2\\f \end{bmatrix}$$

于是

$$\begin{bmatrix} 0 & 0 & A & B \end{bmatrix}\begin{bmatrix} 0\\0\\-f\\1 \end{bmatrix}=-f^2$$

即有

$$-Af + B = -f ^ 2$$


于是

$$\begin{cases} -An + B = -n ^ 2\\ -Af + B = -f ^ 2 \end{cases}$$

所以

$$A = n + f, B = nf$$

$$M_{persp \rightarrow ortho}=\begin{bmatrix} n & 0 & 0 & 0\\ 0 & n & 0 & 0\\ 0 & 0 & n + f & nf\\ 0 & 0 & -1 & 0 \end{bmatrix}$$

最后

$$M_{persp} = M_{ortho}M_{persp \rightarrow ortho} = \begin{bmatrix} \frac{2n}{r - l} & 0 & \frac{r + l}{r - l} & 0\\ 0 & \frac{2n}{t - b} & \frac{t + b}{t - b} & 0\\ 0 & 0 & -\frac{f + n}{f - n} & -\frac{2nf}{f - n}\\ 0 & 0 & -1 & 0 \end{bmatrix}$$

透视投影中视锥的定义 (假设 $l = -r, b = -t$)

  • aspect ratio 宽高比
  • field-of-view fov 垂直可视角度

$$\tan \frac{fovY}{2} = \frac{t}{\left|n\right|}$$ $$aspect = \frac{r}{t}$$

故用 fov 和 aspect ratio 替换可以得到

$$M_{persp} = \begin{bmatrix} \frac{1}{\tan(\text{fov} / 2)\cdot \text{aspect}} & 0 & 0 & 0\\ 0 & \frac{1}{\tan(\text{fov} / 2)} & 0 & 0 \\ 0 & 0 & - \frac{f + n}{f - n} & -\frac{2fn}{f - n}\\ 0 & 0 & -1 & 0 \end{bmatrix}$$

对比 glm 代码是一致的

1
2
3
4
5
6
7
8
9
10
11
12
13
template<typename T>
GLM_FUNC_QUALIFIER mat<4, 4, T, defaultp> perspectiveRH_NO(
T fovy, T aspect, T zNear, T zFar) {
T const tanHalfFovy = tan(fovy / static_cast<T>(2));

mat<4, 4, T, defaultp> Result(static_cast<T>(0));
Result[0][0] = static_cast<T>(1) / (aspect * tanHalfFovy);
Result[1][1] = static_cast<T>(1) / (tanHalfFovy);
Result[2][2] = - (zFar + zNear) / (zFar - zNear);
Result[2][3] = - static_cast<T>(1);
Result[3][2] = - (static_cast<T>(2) * zFar * zNear) / (zFar - zNear);
return Result;
}

法线变换矩阵 (The Normal Transformation Matrix)

在实现光照时,我们需要将法线变换到观察空间/世界空间,但是在不均匀缩放等情况下,直接对法线变换是错误的,这里就需要法线变换矩阵了。
设 $\boldsymbol{T}$ 为切向量,$\boldsymbol{N}$ 为法向量。设切向量的变换矩阵为 $M$,变换后的切向量为 $\boldsymbol{T’} = M\boldsymbol{T}$,设正确的法线变换矩阵为 $G$,使得 $\boldsymbol{N’} = G\boldsymbol{N}$,变换后的法线与切线仍然是垂直的,于是有

$$\boldsymbol{N’} \cdot \boldsymbol{T’} = (G \boldsymbol{N}) \cdot (M \boldsymbol{T}) = (G\boldsymbol{N})^T(M \boldsymbol{T}) = \boldsymbol N ^ T G^T M \boldsymbol T = \boldsymbol N \cdot \boldsymbol T = 0$$

故有

$$G^TM = I \Rightarrow G = (M^{-1})^T$$

# ,

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×