Skip to content

Latest commit

 

History

History
1512 lines (719 loc) · 40.5 KB

《闫令琪-现代图形学入门》学习笔记.md

File metadata and controls

1512 lines (719 loc) · 40.5 KB

《闫令琪-现代计算机图形学入门》学习笔记

本文用于作为本人图形学学习笔记。

以《闫令琪——现代计算机图形学入门》视频教程为主,以书籍《基于webgl自顶向下方法(第七版)》为辅。


闫令琪——现代计算机图形学入门

课程视频地址:https://www.bilibili.com/video/BV1X7411F744?p=1

视频配套推荐阅读的虎书:http://index-of.es/z0ro-Repository-2/Cyber/01%20-%20Computer%20Science/Fundamentals%20Of%20Computer%20Graphics%20-%20Peter%20Shirley,%20Steve%20Marschner.pdf


《基于webgl自顶向下方法(第七版)》

书籍购买地址:http://product.dangdang.com/23933108.html


本文的书写顺序说明:

我会先书写闫令琪的视频课程学习笔记,之后会补充上《基于webgl自顶向下方法(第七版)》书籍学习笔记。

例如第一部分 几何对象和变换,我会先写视频笔记,然后再补充上书籍笔记。


补充说明:

  1. 在闫令琪的视频课程中,每一节课程中出现了大量英文单词,即使这些单词和图形学并无关联,但是也一并记录下来,就当背单词吧。
  2. 除了原本教程,我还会在每一节结尾处,添加上 Three.js 中对应的知识点。

第1节:图形学概述

计算机图形学?

计算机图形学 对应的英文为:Computer Graphics,简称 CG。


在传统概念中,计算机图形学与计算机视觉的区分(不同点):

计算机图形学

数学模型 转化为一张图片的过程,称为 计算机图形学。例如 3D渲染。

由数学模型转化为另外一种数学模型,依旧归类为计算机图形学。

  1. 3D模型就是一种数据模型
  2. 3D渲染就属于 计算机图形学
  3. 一些二维数据模型也属于计算机图形学
  4. 所谓 “转化过程” 即 渲染过程,包含计算机显示器的 栅格化(光栅化) 过程

计算机视觉

把一张图片通过 “猜测、推断” 转为为一种 数学模型的过程,称为 计算机视觉。例如人脸识别。

把一张图片转化为另外一张图片,称为计算机图像处理


实际现状

随着 AR/MR/VR/CR/XR 的技术发展,计算机图形学与计算机视觉边界越来越模糊。

例如 XR(视觉混合增强),既包含图形学又包含计算机视觉技术。


第2节:向量与线性代数

swift:敏捷的

brutal:残酷的

linear:直线的,线形的,线性的

algebra:代数,代数学

linear algebra:线性代数

course:课程

dot:点

cross:交叉、相交、穿越

product:除了 产品之外,还有一个意思是 乘积、积


向量 Vector

原教程中使用的是 Vectors

向量的基本特征:

所谓向量,即有方向的一条线段。

用形象化的描述,即 一个带有箭头的线段。

在物理学上称为 矢量,在图形学上称为 向量。

关于向量的一些基础知识点:

  1. 假设有一个向量 a ,那么通常会在 a 顶部添加一个箭头 → 符号用来表示这是一个向量。

    由于暂时没找到如何打出 a 上添加箭头的符号,你只能脑补一下

    本文以 a(→) 这种形式来代替

    在一些书籍中,还可以使用小写且加粗的 a 来表示这是一个向量

    为了打字方便,实际中我们可能就简单使用 “向量a” 这样的称呼和书写方式。

  2. 假设向量a的起始点为A,结束点为B,那么可以使用 AB(顶部添加一个箭头→)来表示这个向量的方向。

    AB 顶部添加一个箭头 → 这个画面依然需要你脑补出来

    向量方向的值就是 B - A ,也就是说 AB(→) = B - A

  3. 向量具有方向(direction)和长度(length)

    甚至很多时候,向量本身就只是用来表示某个方向

  4. 向量并不需要关心绝对的起始位置

    只要保证 开始点A 和 结束点B 的相对位置不变即可


向量的长度:

在表达一个向量的长度时,使用 ||a(→)|| 这种形式。

有一些教程中,会将向量的长度称呼为 向量的模


单位向量:

假定我们将向量的某个长度规定为 1 ,则这个长度就是 1 个单位向量。

我们使用在 a 的顶部添加一个 ^ 的符号来表示这是单位向量。

由于无法正常打出这个符号,所以我们这里暂时使用 a(^) 来代替

计算单位向量的值公式为:a(^) = a(→) / ||a(→)||

在我们描述一个单位向量时,只关心它的方向,不关心它的长度(因为长度固定为 1)。


向量的相加

向量求和

向量a + 向量b 的计算,就是向量求和(向量相加)。

向量求和可以通过 2 种形象化转化:

  1. 平行四边形法则:向量a 和 向量b 同时放在一个起始点,此时构成一个平行四边形,这个平行四边形的对角线就是两个向量求和后的向量。

  2. 三角形法则:向量b 的起始点位于向量a 的结束点,此时构成一个三角形,这个三角形的第3条边就是这两个向量求和后的向量。

    三角形法则相对于平行四边形法则,更适用于多条向量求和。

    因为只需要让每一条向量不断地首尾相连,最终第一条向量的起始点和最后一条向量的结束点构成的向量,即这些向量求和的最终结果。


列向量与行向量

在不同的体系中,记录向量的方式不同。

例如一个向量的坐标为 x,y,那么列向量的表示方式为:

A = (
     x
     y
    )

而行向量的表示方法为:

A(T) = (x, y)

这里的 T 应该是书写在 A 的右上角,表示 转置

暗含的意思就是:将列向量转化为行向量

“转置” 中的 “置” 和 重置中的置 含义相同。


而向量的长度,即 ||A|| = 开根号( x平方 + y平方 )

Math.sqrt( Math.pow(x,2) + Math.pow(y,2))


向量的乘法

向量的乘法一共有 2 种:点乘 和 叉乘

点乘(dot Product)

假设向量a 的长度为 ||a||、向量b 的长度为||b||,2个向量之间的夹角为 θ,那么 向量的点乘公式为:

a(→) · b(→) = ||a|| * ||b|| * cosθ


点乘的最终结果为一个数字(值)。


这个公式可以演化为:

cosθ = a(→) · b(→) / (||a|| * ||b||)

也就是说,可以通过向量的点乘,反向计算出两个向量之间的夹角。


假设2个向量长度都是1,那么上面公式可以简化为:

cosθ = a(→) · b(→)

点乘又被称为内积、点积。


点乘的特性:

更改2个向量的前后顺序,并不会影响最终结果。

说明:为了简化书写,向量不再使用 a(→),而是直接使用 a 表示。

a · b = b · a

a · ( b + c ) = a · b + a · c

(ka) · b = a · (kb) = k(a · b)


点乘的数据计算:

假设在二维坐标系中,向量a的坐标为(xa,ya)、向量b的坐标为 (xb,yb),那么:

a · b = xa * xb + ya * yb

假设在三维坐标系中,向量a的坐标为(xa,ya,za)、向量b的坐标为 (xb,yb,zb),那么:

a · b = xa * xb + ya * yb + za * zb


点乘的几个重要作用:

  1. 计算出2个向量之间的夹角

  2. 计算出一个向量在另外一个向量上的投影

    假设我们计算出2个向量之间加的夹角 θ ,那么向量b 在向量a 上的投影,实际上就是将 向量b 作为一个三角形的最长斜边,通过 cosθ * b 即可得到投影长度。

  3. cosθ * b 得到的是 b 在 a 上的投影,那 sinθ * b 又是什么呢?

    答:sinθ * b 得到的是 2 个向量之间最大的距离

    也就是说,我们可以得出 2 个向量之间,究竟有多接近。

    对于物体表面光反射而言,两个向量越接近,看到光反射的内容越多,表现出来的是越亮。


点乘还可以判断2个向量 前与后的关系

  1. 假定两个向量的点乘结果 大于0,则表示这 2 个向量的方向是基本相同的。

    这里的方向基本相同是指他们相对于 水平轴而言

    你可以简单得把 水平轴 理解为 向上或向下

  2. 假定两个向量的点乘结果 小于0,则表示这 2 个向量的方向是基本相反的。

  3. 假定两个向量的点乘结果 等于0,则表示这 2 个向量的方向在一条直线上,完全相同或完全相反。


补充说明:上述文字中 方向基本相反 和 方向基本相同,他们包含的意思为:

  1. 方向基本相反:随着向量的逐渐延伸,两个向量的距离会有越来越远
  2. 方向基本相同:随着向量的逐渐延伸,两个向量的距离也会逐渐变远,但是不会像方向基本相反那样剧烈变大。

叉乘(cross product)

假设有向量a 和 向量b,叉乘的作用就是计算出同时垂直于向量a,向量b 的向量c。

向量c 垂直于 向量a和向量b 组成的平面

前面我们使用 · 来表示点乘,这里我们使用 x 来表示叉乘


向量的叉乘是区分计算顺序的。也就是说 a x b 不等同于 b x a。

点乘是不在乎顺序的,但是叉乘是在乎顺序的。


叉乘又被称为 叉积。

叉乘的最终结果为一个向量。


右手螺旋法则

向量c 同时垂直于 向量a 和向量b,那如何判断出向量c的方向呢?

此时就要采用右手螺旋法则,假设现在是 a x b,那么:

  1. 向量a 和 向量b 构成一个平面

  2. 弯曲除大拇指外的其他四指,让四指弯曲的方向等同于 向量a 旋转到向量b 的方向

    四指弯曲的方向,只可能是顺时针或逆时针

    最终右手的姿态可能为:大拇指朝上的点赞 或者 大拇指朝下的鄙视

  3. 此时 大拇指的方向,即向量c 的方向


a x b 得到的向量c,和 b x a 得到的向量c,他们的方向一定是相反的。


假设一个向量叉乘自己,那得到的是什么?

答:一个长度为 0 的向量

请注意,尽管长度为 0 ,但是它依然是一个向量,而不是一个数字 0。


叉乘的作用

叉乘主要有2个重要作用:

  1. 判断左和右
  2. 判断内和外

判断左还是右?

假设向量a 和 向量b 构成一个平面,那么我们可以通过肉眼观察,向量b旋转到向量a 是顺时针还是逆时针。

如果是顺时针,则向量b 在向量a 的左侧。

如果是逆时针,则向量b 在向量a 的右侧。


但是在数学上如何计算出向量b 位于向量a 的哪一侧呢?

答:计算向量a 叉乘 向量b,得到的结果 z 如果是正数,则 b 在 a 的左侧。反之则 b 在 a 的右侧。


判断内还是外?

假设有 3 个向量 a b c 首尾相连构成了一个三角形,此时有一个点 p,如何计算出 点p 是在三角形内还是三角形外?

答:依次分别计算出 a x ap、b x bp、c x cp,然后检验这 3 个叉乘结果是否都为正 或 为负。

假设都为正,则表明 点p 都位于 3 个向量的右侧

假设都为负,则表明 点p 都位于 3 个向量的左侧

若三个叉乘结果都为正 或 都为负,那么就表明 点p 位于三角形内部。

若出现有的叉乘结果为正,有的为负,那么就表明 点p 位于三角形外部。


假设点p 实际上是三角形的 3 个顶点,或者是 3 条边上的某个点,那么此时究竟该判断 p 为三角形内还是外?

答:这类情况下,并没有标准答案,完全由我们个人来做决定。

你可以说点p 位于三角形内,也可以说点p 位于三角形外部,自己做决定即可。


叉乘的应用:

叉乘可以让我们通过 2 个向量快速构建出 3D 3维坐标系。

在实际的 3D 渲染中,叉乘还用于光栅化,让我们可以计算出某个点位于三角形内还是外。


矩阵(matrice)

使用 m x n 的结构来表达某些转化方式。

即 m 行 n 列

通常用于表达某种 旋转,平移,缩放。


行矩阵: 假设一个矩阵是 1 行 n 列,这种矩阵就被称为 行矩阵。

方阵: 假设一个矩阵的 行和列数量相同,这种矩阵被称为 方阵。

对角阵: 假设一个矩阵只有在 对角线上的位置才有值(非0),其他位置值都为 0,这种矩阵被称为对角阵。

正交矩阵: 假设一个矩阵转置后的矩阵 和 这个矩阵的逆矩阵相同,这种矩阵被称为 正交矩阵。

关于 矩阵转置、逆矩阵 我们会在后面课程中提到。


矩阵的乘积

简单来说,就是 2 个矩阵之间的相乘。

但并不是任何 2 个矩阵都可以进行相乘,他们必须满足:前一个矩阵的 列数 等于 下一个矩阵的行数。


假设一个矩阵是 m x n,另外一个矩阵是 n x p,那么这两个矩阵就可以相乘。

因为前一个矩阵列数为 n,下一个矩阵行数为 n,因此他们满足相乘要求。


矩阵(m x n) 与另外一个矩阵(n x p) 相乘后的结果是什么?

答:得到一个 m 行 p 列 的矩阵。

也就是 m x p,n 似乎是被消掉了


快速算出矩阵相乘结果中的某个位置的值

假设2个矩阵相乘的结果矩阵中,想知道某个具体位置的值,例如 1 行 2 列 的值,快速计算方法为:

第1个矩阵的 1 行的值 与 第2 个矩阵的 2列的值,进行一个点乘计算,得到的值就是我们想要的值。

这一块不理解无所谓,并不重要。


数学概念补充说明:

交换律:将 a x b 改为 b x a,这就叫交换

分配律也叫结合律:

  1. 将 (a x b) x c 改为 a x (b x c)
  2. 将 a x (b + c) 改为 ab + ac

矩阵乘积不具备交换律,也就是说通常情况下 矩阵a 乘以 矩阵b 都不会等于 矩阵b 乘以 矩阵a。

只有在某些特殊情况下才会相等,但绝大多数时候都是不会相等的。


但是矩阵乘积具备分配律,假设有 3 个矩阵彼此相乘,例如 (a x b) x c 等同于 a x (b x c)。

不可以更改矩阵的交换顺序,但可以更改矩阵的相乘的先后顺序。

同样 a (b + c) 等同于 ab + ac。


Three.js 相关知识点

二维向量:Vector2

三维向量:Vector3

四维向量:Vector4

向量的点积:Vector2.dot( v )、Vector3.dot( v)

向量的叉积:Vector2.cross( v)、Vector3.cross( v )


三维矩阵:Matrix3 ( 3 行 3 列)

四维矩阵:Matrix4 ( 4 行 4 列)

三维矩阵相乘:

  1. Matrix3.multiply( m ):将当前矩阵乘以矩阵 m

  2. Matrix3.premultiply( m ):将矩阵m 乘以当前矩阵

    我们知道 矩阵不具备 交换律,交换相乘的前后顺序是会影响最终结果的

三维矩阵转置:Matrix3.transpose()

由于三维矩阵本身就是 3 行 3 列,转置后依然是 3 行 3 列,行和列发生了对调。转置的结果只是将位置上的数字发生了对调。

提前剧透一下:对于旋转矩阵而言,转置之后得到的矩阵实际上就是这个旋转矩阵的 逆矩阵。


第3节:变换(Transfromation)

transformation:变换、变化

shear:斜切、倾斜


课程中提到了一个和图形学相关,但与 当前无关的技术名词:逆运动学

逆运动学是决定要达成所需要的姿势所要设置的关节可活动对象的参数的过程。


为什么要学习变换?

因为 3D 渲染的过程中,几乎各个过程中都需要使用到各种变换。


先从 2D 的变换入手。

二维变换包括:

  1. 旋转(rotation)

    在传统的图形学中,当描述 “旋转多少度”时,都是指 逆时针 旋转。

    并且旋转的中心点都是 坐标轴(0,0) 的位置,也就是物体的 左下角

    但是在 three.js 中,旋转的中心位于 “物体中心” 位置。

  2. 缩放(scale)

  3. 斜切(shear)

    斜切 也可以称为 切变

    就是水平或垂直方向上,某一个方向不变,另外一个方向发生偏移。

  4. 平移


二维变换遇到的困境:

当平移与与旋转、缩放、斜切想结合时,无法使用 2 x 2 矩阵来统一描述变换矩阵。


为什么不可以描述?

因为二维转换中坐标缺失 “方向” 的概念。

这里说的 “方向” 实际上就是指 向量,向量可以表示方向。

比如一个二维坐标 (x,y),和一个二维向量(x,y),他们在表述的时候是一模一样,这样就造成我们无法在记录坐标位置的同时还能够记录该坐标要运动的方向。


因此需要通过增加一维的方式来统一矩阵。

这是由于二维平移中,是缺少 “方向” 这个概念。

也就是使用 3 x 3 的矩阵来描述二维的变换。

这种使用 N + 1 维来描述 N 维 的形式,被称为 "齐次坐标"。


增加的1维的表现形式:点坐标增加维度为 1、向量增加维度为 0

准确严谨的定义为:(x,y,w),而 w 通常我们认为值为 0 或 1。

原本一个坐标是 (x,y),那么齐次坐标中就变为 (x,y,1)。

原本一个向量是 (x,y),那么齐次坐标中就变为 (x,y,0)。

这样我们就很容易区分出坐标和向量了。


这样我们就可以自由,充足去计算点和向量之间的运算了。


点坐标和 向量之间的互动关系:

实际上就是他们增加的那一个维度值之间的计算。

  1. 向量 + 向量 = 向量

    0 + 0 = 0 -> 0 代表向量

    同理 向量(0) - 向量(0) = 向量(0)

  2. 点坐标 - 点坐标 = 向量

    1 - 1 = 0 -> 0 代表向量

  3. 点坐标 + 向量 = 点

    1 + 0 = 1 -> 1 代表点坐标

  4. 点坐标 + 点坐标 = ??

    1 +1 = 2 -> ??

    在之前的齐次坐标定义中,增加的维度的值只能是 0 和 1,那 点+点 新增维度等于 2 这种情况下,又该如何去理解结果呢?

    我们知道,严谨定义齐次坐标实际上是 (x,y,w),此时 w = 2,那么上面矩阵可以变化为:

    (x/w, y/w, w/w),也就是 (x/2,y/2,1)。

    这个变化的前提是 w 不可以等于 0

    此时 w 即为 符合齐次坐标规范的 1。

    我们可以看出,点 + 点 实际上得到的还是点,这个点 是这 2 个相加点的 中点。


再次重复一遍:点 增加的维度值为 1、向量增加的维度值为 0

记住这个结论后,再去记忆 点 与 向量的加减就很容易了。


齐次坐标的意义:

齐次坐标是一种提升空间维度来解决低纬度问题的方式。

假设要解决一个二维空间的问题,例如一个平面,你可以把它想象成这个平面是三维空间中某个物体的一个面。

通过使用三维空间的变换方式,去解决这个二维平面的各种变换。


齐次坐标还可以解决二维空间中其他根本无解的问题。

例如:在二维空间中,两条平行线是永远不可能相交的。

但是在三维 透视 视觉空间中,例如一条长长的铁轨,铁轨两侧是平行的,但是在视觉上看,远处的铁轨视线最终是会相交于一点的。

在正交相机中,铁轨两侧永远是不会相交的


同理,按照齐次坐标理论,可以使用四维空间坐标体系来解决三维空间的变换(转换)。


齐次坐标造成变换矩阵的统一性:

使用三维空间,也就是 3 x 3 的矩阵可以描述一次二维空间的变换,假设二维空间需要多个不同的转换,例如平移,缩放,旋转。那么相当于:平移矩阵 x 缩放矩阵 x 旋转矩阵 x 二维平面坐标。

在第二节中我们提到过矩阵乘积的特性:

  1. 矩阵乘积不符合 置换律
  2. 矩阵乘积符合 结合律

那么上述的那些矩阵相乘,实际上可以表述为:

(平移矩阵 x 缩放矩阵 x 旋转矩阵) x 二维平面坐标

由于 平移、缩放、旋转 矩阵都是 3 x 3,那么意味着它们的乘积结果依然是 3 x 3 的矩阵。

也就是说无论多少次平移,旋转,缩放,最终结果依然是一个 3 x 3 的矩阵。


因此,结论就是:一个 3 x 3 的矩阵可以表示出二维空间所有可能的变换。

请注意,这个 3 x 3 的矩阵前 2 列的结尾都是 0,说明前 2 列都是向量,而第三列结尾是 1,表示一个坐标。

换句话说,前 2 列的向量用来表示在 水平 与 垂直上的旋转和缩放。这 2 列中第1 个数表示 x 轴旋转角度,第 2 个数表示 y 轴旋转角度。

第 3 列的点坐标用来表明 平移。第3列第一个数是用于表示 x 轴上的平移量,第2个数表示 y轴上的平移量。

由于前 2 列为方向(向量),最后维度值为 0,第 3 列为平移(坐标),最后维度值为 1,因此我们可以得出结论:这个 3 x 3 的矩阵最后一行 永远是 0 0 1

由于最后一行永远是 0 0 1,所以个别时候储存一个 3 x 3 的变换矩阵时会省略掉最后一行。也就是说使用一个 2 x 3 的矩阵的形式来储存这个 3 x 3 矩阵。

同理可以推断出,任何一个 4 x 4 的矩阵都可以表示出一个三维空间的所有可能的变换。

这样我们在记录变换时,就可以以统一的 矩阵 格式来记录所有可能发生的变化。

当然可以通过逆向计算,来倒推、还原之前的变换。


二维空间变化的矩阵公式:

具体的推导过程我们不再过多阐述,只说结论。

transformations

在没有引入齐次坐标概念之前,我们计算一个物体 旋转、缩放、平移的方式为:旋转矩阵(2x2) · 缩放矩阵(2x2) + 平移矩阵(2x2)

引入齐次坐标后,我们将上述计算方式统一成一种 3 x 3 的矩阵形式。

唯一的代价就是之前是几个 2 x 2,而现在是一个 3 x 3,似乎增加了计算的复杂度。

但是这种统一矩阵带来的好处非常多,多到可以完全忽略这个代价。


矩阵书写的顺序:

矩阵乘积不符合 变换率(先后顺序不可改变),并且矩阵运算按照约定是从右向左计算的,因此当我们在书写 旋转,缩放,平移 矩阵时,通常顺序为:

平移矩阵、缩放矩阵、旋转矩阵、点坐标

通常情况下,缩放矩阵 和 旋转矩阵 的顺序是可以互相调换的。


逆变换:

简单来说,就是把某个矩阵计算结果,反向操作,恢复成之前的矩阵。


非原点的2维旋转:

假设我们现在要对一个平面进行旋转,但是旋转中心并非默认的原点,而是其他一个点坐标。

这里默认左下角为原点,当前平面左下角并不在原点。

那么,我们理论上需要做的步骤为:

  1. 假定一个平移矩阵 T(c),先将平面移动到原点

  2. 进行旋转矩阵 R(a) 操作

  3. 将旋转结果再以 T(c) 的相反值 T(-c) 再操作一次

    至此,我们就得到了 非原点旋转 的最终结果。

也就是说,整个矩阵变换过程为:T(c) · R(a) · T(-c)

我们假定 T(-c) 就是 T(c) 的逆矩阵


3维变换:

上面我们讲述的全部是 二维 平面的转换,在讲解 3D 变换时,实际上就是将 3维 空间点坐标(x,y,z) 再增加上一个维度 w,w 的值也是 0 或 1,用于表示和区分 这是一个 3维点坐标 还是 3维向量(方向)。

3D点坐标:(x,y,z,1)

3D向量:(x,y,z,0)

也就是说,使用一个 4 x 4 的矩阵来表示 3 维中的 点或向量

实际上就是引入了 4 维空间来解决 3 维空间的变换


假设 w 的值不为 0 也不为 1,我们依然可以像处理 二维平面那样,将 x,y,z 都除以 w。

在这个 4 x 4 矩阵中,前 3 列分别代表水平,垂直,前后的旋转与缩放,第 4 列代表 平移。

因此前 3 列最后一项值均为 0,也就是说,在这个 4 x 4 矩阵中,最后一行一定是:0 0 0 1

这里面的很多概念和值的意义,和 2 维齐次坐标 矩阵中的含义是完全相同的。

例如依然是 先旋转,缩放,再平移

特别强调:列优先、行优先

所谓列优先:是指按照一列一列的书写顺序。

所谓行优先:是指按照一行一行的书写顺序。

请注意,书写顺序并不会影响和改变 矩阵采用的 列阵式 形式。

在闫令琪的这套视频教程中,以及本文上面讲述中,矩阵全部采用的是列阵式,也就是说是一列表示一个维度。

但是在 Three.js 中通过 .set() 方法修改矩阵时,按照行优先,即一行一行的顺序来设置元素的值。

Three.js之所以这样做,纯粹是为了书写方便,实际上在three.js内部依然采用列阵式存储。

比如当使用 .elements 获得元素时,返回结果就是按照 列优先 的顺序来返回的。


矩阵转置:

列阵式 和 行阵式 之间是可以互相转化的,这个转换的过程就被称为:“矩阵转置”。


矩阵转置中存在一个特殊情况:

对于旋转矩阵而言,转置前和转置后的两个矩阵实际上是彼此的 逆矩阵。

矩阵转置对应的是 Three.js 中 Matrix3 的 .transpose() 方法。


正交矩阵:

假设一个矩阵转置之后的矩阵和自己的逆矩阵完全相同,我们称这种矩阵为 “正交矩阵”。

旋转操作的矩阵就符合 正交矩阵 的定义,因此旋转矩阵 就是 正交矩阵。


本文主要以 二维空间平面的转换为基础,讲解变换、齐次坐标。

接下来,才会开始真正进入 3D 空间变换。


第4节:视图/投影变换

viewing:观测

projection:投影

orthographic:正交

perspective:透视


在国内的教程中,通常将 视图变换、投影变换 分开来讲,认为这是 2 个独立的变换。

闫令琪在他的教程中,将以上 2 种变换可以统称为:观测变换

这个 “观测” 为闫令琪个人的习惯用词,在有些书籍中使用的是 “观察”。


对于3D中的 平移、缩放、旋转而言,旋转最难。


绕某轴旋转

假设我们说 绕某个轴旋转,实际情况就是对应这个轴的坐标不会发生变化,发生变化的是另外两个轴上的坐标。

  1. 绕 x 轴旋转,即 x 坐标不变,y和z 坐标发生变化
  2. 绕 y 轴旋转,即 y 坐标不变,x和z 坐标发生变化
  3. 绕 z 轴旋转,即 z 坐标不变,x和y 坐标发生变化

旋转对应的矩阵乘积的复杂之处:

按照普通的惯性视为,我们应该将 3 个轴上的旋转矩阵依次相乘,即 x · y · z。

我们需要严格要求按照某种顺序来做叉乘。

例如 x 先和 y 进行乘积,那么 x 旋转到 y,通过右手螺旋法则可以知道,最终乘积结果为 正 z 轴。

假设是 y 先和 x 进行乘积,那么 y 旋转到 x,通过右手螺旋法则可以知道 ,最终乘积结果为 负 z 轴。

结论:旋转轴的先后顺序会影响最终结果。


以飞机为例:

  1. 左右平移被称为:偏航

    以与机身上下垂直的轴 进行旋转

  2. 左右旋转被称为:滚转

    以机身为轴 进行旋转

  3. 上下变化被称为:仰俯

    以与机身水平垂直的轴 进行旋转


四元数

主要用于处理 3D 空间中物体旋转。

四元数即使用 4 个数字来表示出物体的 3D 旋转。

在 Three.js 中,对应的是 Quaternion,构造函数 4 个数字依次为 x、y、z、w。


四元数与矩阵:

四元数 理论只适用于 3D 空间中的旋转,对于 四维空间 该理论就不适用了。

对于矩阵而言,矩阵转换理论适用于任何维度的空间。


四元数的使用现状:

四元数只适用于 3D 空间中的旋转,相对 3D 空间中的旋转矩阵而言,更加简洁快速。

但是,对于其他操作 例如 平移、缩放而言,在多种变换情况下 矩阵和四元数同时使用 可能会带来理解、计算上的不便,因此很多人推崇任何时候都使用矩阵。

四元数在四维空间中的失效,也是其中一个原因。


因此,我们在日常开发中,尽量使用矩阵,而不是四元数。


如何拍出一张好照片?

  1. 第一步,要选好拍照内容,例如先让被拍摄的人站好位置。
  2. 第二步,确定好相机的位置和拍摄角度,相机位置决定了相机拍摄的画面范围。
  3. 第三部,摁下快门,完成最终的整体拍摄。

以上拍照片的过程,实际上和 3D 渲染观测视图的过程是相似的。

渲染 3D 当前视图的流程是:

  1. 确定好物体的变换(旋转,缩放,平移)

    这里的变换还包暗含局部坐标和世界坐标的转换

  2. 设置好镜头的位置、明确相机 .up 的方向(即相机哪个方向为上,默认为 y 轴朝上)

    相机的 .up 属性为一个方向,方向即向量,对于方向而言,向量的长度是不在乎的

    例如相机默认的 .up 的值是 Vector3(0,1,0),此时 Vector3(0,1,0) 和 Vector(0,2,0) 含义相同,都表示 y 轴为上。

  3. 最终整体变换(透视或正交变换)


物体之间的运动是相对的。

也就是说假设物体不变移动镜头 和 镜头不变移动物体,最终呈现的效果是相同的。


在闫令琪的本节视频课程中,他讲述的是这样一个事情:

  1. 我们假定相机位置永远在原点,也就是 (0,0,0) 的位置,如果相机不在原点则通过矩阵变形,让相机变换到原点。
  2. 当相机是位置固定后,3D 空间中的其他物体则可根据刚才相机的变换矩阵,作出对应的变换。
  3. 这样所谓相机的位置变换,变成了 相机不变,物体在变换。
  4. 这样做的目的是为了便于计算,也就是说只需要不断计算物体的变换即可。

透视与正交

透视即和人眼睛相似,即远小近大。

铁轨两侧在远处会相交

正交则无论远近,物体投影大小不发生变化。

铁轨两侧永远平行


透视相机的视椎是一个四凌锥,而正交相机的视椎是一个长方体。


正交投影在实际中的转换过程:

  1. 在 3D 空间中定义一个长方体(左右上下远近)

    一个点 z 值越大则表明距离我们越近,z 值越小则表明距离我们越远。

  2. 平移该长方体,将该长方体的中心移动到原点

  3. 将该长方体变换(映射)为一个立方体

    该立方体的 x,y,z 坐标范围为 -1 至 1

  4. 至此,完成正交投影完成

    在最终渲染之前,还会将结果重新变换,让物体并不会出现因压缩而变形


透视投影在实际中的转换过程:

透视投影的视椎是一个椎体

  1. 我们假定距离我们最近的面 是永远固定不变的。

  2. 把远平面硬生生压缩成和近平面相同的尺寸

    假设相机位置距离近面为 n,则压缩后的 y 坐标为 (n/z)y

  3. 此时变成了 “正交投影”

    视椎由原来的椎体变成了长方体

  4. ...

    后面的推导过程实在是理解不了,暂时就不去理解了。


实际上正交和透视计算过程,是无需我们关心的,因此这个具体推导和公式,不再进行深入学习了。


《基于webgl自顶向下方法(第七版)》学习笔记之:几何对象和变换

对于视频笔记中相同的知识点,不再说重复叙述,下面叙述的是对视频笔记中的补充。

scalar:标量

euclid:欧几里得(古希腊数学家),通常表示欧几里得几何,即传统几何。

在一些日常语境中,欧几里得几何指二维空间中的平面几何。三维空间的几何通常直接称呼为立体几何。

affine space:仿射空间

normal:法向量

constructive solid geometry,简称 CSG:构造实体几何,也就是将不同几何体通过相互并、交等操作来构建新的几何对象。

gimbal lock:万向节死锁


向量的补充:

可以增加或减少向量的长度,但是向量的方向并不会发生改变。


零向量:

假设向量a 与 向量b 长度相同、方向相反,那么此时 a + b 得到的结果为一个长度为 0 的向量,这个向量被称为 "零向量",属于特殊向量。

我们可以把它标记为 0。

请注意,零向量长度为零,且没有方向。


向量的几何定义和存储类型的差异说明:

在opengl(webgl)中使用vec2、vec3、vec4来“定义”二维,三维,四维中的向量。

在three.js中使用Vector2、Vector3、Vector4来“定义”二维,三维,四维中的向量。

请注意上面中的 “定义” 是加了引号的,因为上面提到的这些,它们的类型并不是几何类型,而是存储类型


我们以 Three.js 的 Vector3 为例来加以说明。

在 Vector3 的官方文档中写道:

  1. 该类表示的是一个三维向量。

    一个向量需要有起始点和结束点,实际上我们假定默认起始点都为 (0,0,0),而( x, y ,z ) 指向量的结束点。

  2. 该类还可以表示一个三维点的坐标,即 ( x, y ,z )。

  3. 该类还可以表示为一个有顺序的、3个为一组的数组组合。

    此时 x,y,z 的意义即这组数组中的 3 个数字

    因此,甚至某些时候你都可以使用 vector3 来定义和存储 颜色 RGB 3 个值


结论再次强调一遍:

Vector2、Vector3、Vector4 他们只是某种存储类型,而并非几何类型。


与坐标无关的几何

在我们传统印象中,一定是先有坐标系,然后才会在坐标系上标记出几何体。

因为我们需要明确表示出几何体在坐标系中的坐标、长度信息。

现在我们试想一下,假设现实世界中有一个 2厘米 x 2厘米的纸片平面。

虽然纸片有厚度,但是这里我们假定纸片就是一个二维平面。

我们可以把该纸片放入很多坐标系中。

例如 水平x轴,垂直y轴,或者是 其他坐标系中。

坐标系可以有很多种,但是无论哪种坐标系,纸片会发生任何变化吗?

答案是没有,因为纸片永远是 2厘米 x 2厘米。


如果没有坐标系,当我们对纸片周围其他物体进行描述时,我们可能是这样的语句:

  1. 在纸片的左侧...
  2. 在纸片的右侧...

顺着这个无坐标系的思维,我们继续往下,看能有什么有用的结论。

。。。实际上得出的结论在视频教程中都有提到过。


先补充一个名词解释:仿射空间

仿射空间:

仿射空间就是没有原点的向量空间。

实际上,你可以完全忽略仿射空间的概念,把仿射空间理解成可以做点和向量数学运算的 3D 空间即可。

在现实世界中我们可能会认为一个点减去另外一个点,得到的应该还是一个点,但是在仿射空间中,结果为一个向量。

因为在齐次坐标中,点的最后一个维度值为 1, 两个点相减,1 - 1 = 0 ,最后一个维度值为 0,即表示一个向量。

关于点和向量之间的相互加减结果,可参考回顾本文视频课程笔记中的相关部分。


也就是说,在仿射空间中,我们可以对不同坐标系,矩阵转换进行相加,相减,相乘,求逆等等。

实际上 Three.js 中的 3D 空间就是仿射空间。


刚体变换:

在 3D 空间中,对一个物体进行 旋转、平移,只能修改物体的位置和方向,但是物体的形状和体积没有发生变化,此时我们称这种变换为 “刚体变换”。

同理,当对物体进行缩放、斜切等操作,会修改物体的形状和体积,这类操作就不属于 刚体变换。

至于叫什么,暂时没有查到。非刚体变换?


在严令琪的视频课程中,变换的不动点都是原点,即左下角(0,0)。但是在 webgl 和 three.js 中,变换的不动点在物体的中心。


在Three.js 中,Uniform 类是用于存储 着色器全局变量。


万向节死锁:

在使用欧拉角旋转时,容易引起 万向节死锁。

在很多教程中,都直接称呼为 “万向锁”

万向节死锁可以简单理解为以下场景:

假设有一个人在北极正中心,那么此刻他抬头无论看向哪个方向,他“头顶”始终是北极。

相当于此时这个人的天空一直处于北极,相当于 向上的轴 “迷失方向” 了。

使用四元数,则不存在万向节死锁的问题。


webgl中的lookAt()与three.js中的lookAt()

在webgl中,我们设定相机看向某个位置,需要3个要素:

  1. 相机的位置,这个被称为 视点

    也就是眼睛的位置

  2. 相机看向的位置,这个被称为参考点

  3. 相机向上的朝向

也就是说,在webgl 中,执行 lookat 的代码为:

lookAt(eye,at,up)
//lookAt() 方法返回一个四维矩阵(mat4)实例

特别强调,并不是只有相机才可以拥有 lookAt() 方法,其他 3D 对象也可以拥有。


但是请记住,在 three.js 中 .lookAt() 函数参数并非如此。

在 three.js 中 .lookAt() 的参数实际上只是上述中的 at

在 three.js 中绝大多数对象的基类为 Object3D,它提供了 .lookAt() 函数。

无论是场景、相机、光、还是物体(网格),他们都是继承于 Object3D。


three.js中 lookAt()的参数:

参数有2种:

  1. 表明朝向目标 at 的位置坐标分量 x, y, z
  2. 表明朝向目标 at 的向量 Vector3 实例
xxx.lookAt(x,y,z)
//或
xxx.lookAt(vector3)

//请注意,three.js 中 .lookAt() 并不会返回一个四维矩阵,而是什么也不返回
//在 JS 中一个函数什么也不返回,其实相当于返回 null

three.js中lookAt()相应的3个知识点:

  1. 获取或设置当前位置坐标,对应修改属性为 .position

  2. 设置参考点位置的,对应方法 .lookAt()

    并没有获取参考点位置的属性

  3. 获取或设置向上朝向,对应修改属性名为 .up

虽然在使用中,我们调用的是 Object3D 实例中的 .lookAt() 方法,但是在 Object3D 内部,实际上经过处理后再执行 Matrix4 的 .lookAt() 。

因为在齐次坐标理论中,需要使用 N+1 维来解决 N 维的变换,由于 Object3D 为三维对象,所以自然需要四维矩阵 Matrix4 来做处理。


不同类型的对象.lookAt()的差异:

在 Object3D.js .lookAt() 方法源码中我们可以看到:

// _m1 = new Matrix4()

lookAt(){
    ...
    
    if ( this.isCamera || this.isLight ) {
        _m1.lookAt( _position, _target, this.up );
    } else {
        _m1.lookAt( _target, _position, this.up );
    }
    
    ...
}

也就是说,对于相机和光而言,lookAt 指 让它们看向目标。

而对于其他对象,例如场景、网格等,lookAt 指 让他们看向 “相机和光”。


第5节:光栅化(三角形)

rasterization:光栅化

triangles:三角形