九、OpenGL基础数学知识

Posted by 卢小胖 on 2023-10-10
Estimated Reading Time 10 Minutes
Words 2.8k In Total
Viewed Times

1、向量

1.1 向量概念

向量最基本的定义就是一个方向,每个向量在2D图像中都用一个箭头(x, y)表示。我们在2D图片中展示这些向量,因为这样子会更直观一点。你可以把这些2D向量当做z坐标为0的3D向量。由于向量表示的是方向,起始于何处并不会改变它的值。下图我们可以看到向量v¯和w¯是相等的,尽管他们的起始点不同:

如果用公式表示:

如果在三维空间中,就是(x,y,z)表示,箭头的方向表示向量方向:

一般在OpenGL中用(x,y,z,w)来表示向量,这个w分量在后面会用到

1.2 单位向量

大小为1的向量,就是单位向量,如:

  • (1,0,0)
  • (0,1,0)
  • (0,0,1)

如果一个向量不是单位向量,我们把它变成单位向量,该过程称为归一化,例如

  • (15,0,0) ——> (1,0,0)

归一化的向量只有方向和原来是相同的,大小变成了1

1.3 向量点乘(向量点积)

两个向量a = [a1, a2,…, an]和b = [b1, b2,…, bn]的点积定义为:

a·b=a1b1+a2b2+……+anbn。

两个向量a和bb的数量积称为点乘(又叫内积、点积),计算方式如下图所示

实例如下:

几何意义

两个向量a和b,夹角为θ,点乘计算方式如下

如图所示:

1.4 向量叉乘(向量叉积)

百度百科截取:

向量积,数学中又称外积、叉积,物理中称矢积、叉乘,是一种在向量空间中向量的二元运算。与点积不同,它的运算结果是一个向量而不是一个标量。并且两个向量的叉积与这两个向量和垂直。其应用也十分广泛,通常应用于物理学光学和计算机图形学中。

两个向量a和b的叉积写作a×b(有时也被写成a∧b,避免和字母x混淆)。

a和b向量的向量积的方向与这两个向量所在平面垂直,且遵守右手定则

在OpenGL中可以用来求法向量

1.5 向量取反

对一个向量取反会将其方向逆转。我们在一个向量的每个分量前加负号就可以实现取反了(或者说用-1数乘该向量):

1.6 向量加减

向量的加减可以被定义为是分量的加减,即将一个向量中的每一个分量加上另一个向量的对应分量:

向量v = (4, 2)k = (1, 2)可以直观地表示为:

向量相减也是一样的:

两个向量的相减会得到这两个向量指向位置的差。这在我们想要获取两点的差会非常有用:

1.6 向量与标量运算

标量只是一个数字(或者说是仅有一个分量的向量)。当把一个向量加/减/乘/除一个标量,我们可以简单的把向量的每个分量分别进行该运算。对于加法来说会像这样:

其中的+可以是+,-,·或÷,其中·是乘号。注意-和÷运算时不能颠倒(标量-/÷向量),因为颠倒的运算是没有定义的。

数学中没有向量与标量的运算,但是在代码库中很多都支持这种操作。

2、矩阵

2.1 矩阵概念

简单来说矩阵就是一个矩形的数字、符号或表达式数组。矩阵中每一项叫做矩阵的元素(Element)。下面是一个2×3矩阵的例子:

为什么使用矩阵,简单来说:矩阵可以方便的表示向量在空间的变化,比如:旋转,平移,缩放等操作。

和向量一样,矩阵也有非常漂亮的数学属性。矩阵有几个运算,分别是:矩阵加法、减法和乘法。

2.2 矩阵的加减

矩阵与标量之间的加减定义如下:

标量值要加到矩阵的每一个元素上。矩阵与标量的减法也相似:

矩阵相加:

减法:

矩阵数乘:

2.3 矩阵相乘

如果你学过线性代数,对矩阵乘法应该有一些映像,因为它看起来有一些反人类

矩阵相乘有一些规则:

  • 只有当左侧矩阵的列数与右侧矩阵的行数相等,两个矩阵才能相乘
  • 矩阵相乘不遵守交换律(Commutative),也就是说A⋅B≠B⋅A

我们先看一个两个2×2矩阵相乘的例子:

你是否发现了规则?

没错,记住就行了~
对于一些复杂矩阵,计算很容易出错:

image.png

好在我们是在编程中使用,不用手动算哈哈哈哈

3、矩阵与向量

让我们更深入了解一下向量,它其实就是一个N×1矩阵,如果我们有一个M×N矩阵,我们可以用这个矩阵乘以我们的N×1向量,因为这个矩阵的列数等于向量的行数,所以它们就能相乘。

3.1 单位矩阵

在OpenGL中,由于某些原因我们通常使用4×4的变换矩阵,而其中最重要的原因就是大部分的向量都是4分量的。我们能想到的最简单的变换矩阵就是单位矩阵(Identity Matrix)。单位矩阵是一个除了对角线以外都是0的N×N矩阵。在下式中可以看到,这种变换矩阵使一个向量完全不变:

3.2 缩放

我们先来尝试缩放向量v¯=(3,2)。我们可以把向量沿着x轴缩放0.5,使它的宽度缩小为原来的二分之一;我们将沿着y轴把向量的高度缩放为原来的两倍。我们看看把向量缩放(0.5, 2)倍所获得的s¯是什么样的:

OpenGL在3D空间中操作,每个分量的缩放系数可能都不一样,如果我们把缩放变量表示为(S1,S2,S3)我们可以为任意向量(x,y,z)定义一个缩放矩阵:

第四个缩放向量仍然是1,因为在3D空间中缩放w分量是无意义的。w分量另有其他用途,在后面我们会看到。

3.3 位移

和缩放矩阵一样,在4×4矩阵上有几个特别的位置用来执行特定的操作,对于位移来说它们是第四列最上面的3个值。如果我们把位移向量表示为(Tx,Ty,Tz),我们就能把位移矩阵定义为:

3.4 旋转

首先我们来定义一个向量的旋转到底是什么。2D或3D空间中的旋转用角(Angle)来表示。角可以是角度制或弧度制的,周角是360角度或2 PI弧度。我个人更喜欢用角度,因为它们看起来更直观。

大多数旋转函数需要用弧度制的角,但幸运的是角度制的角也可以很容易地转化为弧度制的:PI约等于3.14159265359。

  • 弧度转角度:角度 = 弧度 * (180.0f / PI)
  • 角度转弧度:弧度 = 角度 * (PI / 180.0f)

旋转矩阵在3D空间中每个单位轴都有不同定义,旋转角度用θ表示:

image.png

利用旋转矩阵我们可以把任意位置向量沿一个单位旋转轴进行旋转。也可以将多个矩阵复合,比如先沿着x轴旋转再沿着y轴旋转。但是这会很快导致一个问题——万向节死锁(Gimbal Lock)。避免万向节死锁的真正解决方案是使用四元数(Quaternion),它不仅更安全,而且计算会更有效率。四元数可能会在后面的教程中讨论。

组合

假设我们有一个顶点(x, y, z),我们希望将其缩放2倍,然后位移(1, 2, 3)个单位。我们需要一个位移和缩放矩阵来完成这些变换。结果的变换矩阵看起来像这样:

注意,当矩阵相乘时我们先写位移再写缩放变换的。矩阵乘法是不遵守交换律的,这意味着它们的顺序很重要。当矩阵相乘时,在最右边的矩阵是第一个与向量相乘的,所以你应该从右向左读这个乘法。建议您在组合矩阵时,先进行缩放操作,然后是旋转,最后才是位移,否则它们会(消极地)互相影响。比如,如果你先位移再缩放,位移的向量也会同样被缩放(译注:比如向某方向移动2米,2米也许会被缩放成1米)!

很重要,主要矩阵相乘顺序!

上述结果为:向量先缩放2倍,然后位移了(1, 2, 3)个单位。

4、OpenGL实践

我们以glm这个库为例,需要的GLM的大多数功能都可以从下面这3个头文件中找到:

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

把一个向量(1, 0, 0)位移(1, 1, 0)个单位(注意,我们把它定义为一个glm::vec4类型的值,齐次坐标设定为1.0):

//定义初始向量
glm::vec4 vec(1.0f, 0.0f, 0.0f, 1.0f);

//定义矩阵
glm::mat4 trans;

//translate这个函数一般用于将物体进行位移,这里将向量(1, 1, 0)与单位矩阵相乘
trans = glm::translate(trans, glm::vec3(1.0f, 1.0f, 0.0f));

//将结果再与最初的向量相乘,来对其进行位移改变
vec = trans * vec;

std::cout << vec.x << vec.y << vec.z << std::endl;

关于GLM库中更多操作,参考:

# 总结GLM库中glm::transform(位移), glm:scale(缩放), glm::rotate(旋转)

将结果矩阵传递给着色器,GLSL里也有一个mat4类型,我们将修改顶点着色器让其接收一个mat4的uniform变量,然后再用矩阵uniform乘以位置向量:

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

传递值给着色器,首先查询uniform变量的地址,然后用有Matrix4fv后缀的glUniform函数把矩阵数据发送给着色器。

unsigned int transformLoc = glGetUniformLocation(ourShader.ID, "transform"); 
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(trans));
  • 第一个参数你现在应该很熟悉了,它是uniform的位置值。
  • 第二个参数告诉OpenGL我们将要发送多少个矩阵,这里是1。
  • 第三个参数询问我们是否希望对我们的矩阵进行转置(Transpose),也就是说交换我们矩阵的行和列。OpenGL开发者通常使用一种内部矩阵布局,叫做列主序(Column-major Ordering)布局。GLM的默认布局就是列主序,所以并不需要转置矩阵,我们填GL_FALSE
  • 最后一个参数是真正的矩阵数据,但是GLM并不是把它们的矩阵储存为OpenGL所希望接受的那种,因此我们要先用GLM的自带的函数value_ptr来变换这些数据。