OpenGL 3D图形数学基础

96
StarryThrone
0.3 2017.11.22 18:09* 字数 8007

1 3D图形仿射变换

OpenGL中模型的平移、选择和缩放操作都可以通过一个仿射矩阵和该模型的所有顶点列向量相乘获得。iOS中GLKit提供矩阵的初始化和计算方法,不必手写各个分量,但是深入理解其原理对学习3D图形变换很有帮助。

1.1 单位矩阵(Identity Matrix)

单位矩阵的行列数一定相等,单位矩阵和任意模型顶点相乘的结果不会改变模型的位置。可以通过常量GLKMatrix4Identity获得。

1.2 平移矩阵(Translation Matrices)

前文说过4*4转换矩阵最后一列表示新坐标系的原点坐标,OpenGL中平移一个模型可以看做是将模型自生的坐标系平移,再将模型从旧坐标系转换到新坐标系的过程。

OpenGL中通常使用齐次坐标w分量为1的四维向量表示空间中的一个点,当其w分量不为1.0时,其平移效果会在转换矩阵的基础上产生缩放效果。另外使用三维向量或者w分量为0的四维向量表示方向向量。转换矩阵平移三维模型的计算过程如下。可以通过函数GLKMatrix4MakeTranslation(float tx, float ty, float tz)获得。

1.3 旋转矩阵(Rotation Matrices)

1.3.2 基础旋转仿射矩阵

对于旋转矩阵,首先考虑模型绕x轴简单旋转,旋转后模型的x轴坐标不会变化。前文已经说明,4阶齐次旋转矩阵的前三列代表新坐标系轴的三个单位基向量。

按上图对坐标系绕x轴旋转的旋转矩阵可以如下表示,GLKit中可以使用函数直接获取旋转矩阵GLKMatrix4MakeXRotation(float radians)

同理可以推出绕y轴和绕z轴旋转的矩阵,GLKit中可以使用函数直接获取旋转矩阵GLKMatrix4MakeYRotation(float radians)、GLKMatrix4MakeZRotation(float radians)

当一个模型分别实现绕x,y,z轴旋转后,也就实现了绕经过原点的任意向量旋转,GLKit内部代码会根据在三个轴上的旋转角度构建出类似下图的转换矩阵。注意因为物体坐标系相对世界坐标系x-y-z旋转顺序时,将最后的物体坐标系向世界坐标系转换时需要逆序进行,因此矩阵反序相乘。

在GLKit中可以通过函数GLKMatrix4MakeRotation(float radians, float x, float y, float z)获得对应矩阵。

1.3.2 绕任意轴旋转的仿射矩阵

在空间中将向量v绕单位向量n旋转-θ得到向量v‘,其旋转过程和其各个方向分量如下图所示。

由于前文已经说过仿射矩阵的前三列为三个单位基向量的方向,因此可以写出绕任意轴旋转的仿射矩阵如下。在GLKit中使用函数GLKMatrix4MakeRotation(float radians, float x, float y, float z)获得相应矩阵,该函数内部会对x-y-z进行标准化处理。

1.4 缩放矩阵(Scale Matrices)和镜像矩阵

1.4.1 基础缩放矩阵

沿坐标轴缩放矩阵比较简单,只需将缩放后新的基向量直接组合成为仿射矩阵即可,但需注意其x-y-z轴上不等比缩放时会使模型发生变形。可使用函数GLKMatrix4MakeScale( float sx, float sy, float sz)获得。另外令缩放因子为-1则得到镜像变化的放射矩阵。

1.4.2 沿任意方向缩放矩阵

对于空间中的想量v沿着单位向量n的方向以k为缩放因子进行模型缩放,如下图所示。注意使用n的相反向量得到的是相同结果。

因此可以得出其仿射矩阵如下,在GLKit中并未为此类缩放提供函数。另外此处令缩放因子k=-1,则得到绕任意轴取镜像的放射矩阵。

2 3D中的方位与角位移

物体的方位描述的是物体的朝向,方位与方向不同,向量有方向但是没有方位。

数学中不能使用绝对值值来描述物体的位置,只能描述其相对于参考系的位置,同样方位是通过相对已知方位的旋转来描述的,旋转的量称为角位移。主要有矩阵形式、欧拉角和四元数三种描述方式。

2.1 矩阵形式

上一节已经说明了旋转物体的仿射矩阵,取其前三行三列组成的三阶方阵可以表示模型的方位。

使用矩阵时需注意矩阵蠕变现象,即当输入错误的数据,或者浮点运算产生的累积误差使得矩阵不再正交。此时需要矩阵正交化来消除误差,重新得到一个尽可能和原矩阵相同的正交矩阵。

2.1.1 正交矩阵

当方阵M和它的转置MT的乘积等于单位矩阵的时,方阵M称为正交矩阵。即M的转置等于M的逆。

在空间几何中,M是一个3×3阶方阵,使用r1,r2,r3分别表示3×3矩阵的1至3列,其中r1,r2,r3分别代表空间中的3个基向量,只要满足每个向量都是单位向量,同时任意两个向量垂直,此时M就可以称为正交矩阵。对于OpenGL中所有的旋转矩阵都满足上述两个条件,因此都为正交矩阵。

2.1.2 矩阵正交化

斯密特正交化的思想是通过对每一列减去它平行于已经处理过列的部分。对于3×3阶方阵M,r1,r2,r3分别表示3×3矩阵的1至3列,正交化计算如下。

此时得到的新的基向量已经相互垂直,但是其并不是单位向量,此时还需对它们分别进行标准化操作。但是施密特正交化是有偏差的,因为其r1总不能得到校准。一种改进的方法是旋转一个小的因子k,每次只减去投影的k倍,通过多次迭代最终得到一个正交矩阵,其计算过程如下。

2.1.3 使用矩阵形式的优点

可以立即进行向量旋转:矩阵形式和四元数可以直接对向量进行运算得到旋转后的向量坐标,欧拉角不能。
矩阵形式被图形API使用:程序中保存方位可以使用欧拉角和四元数,但是OpenGL内部执行旋转时必须为其提供矩阵形式。
多个角位移连接:当对个坐标系转换时可以先合成一个转换矩阵,最后再对坐标进行转换。
矩阵的逆:使用逆矩阵可以撤销原来的操作,由于旋转矩阵是正交的,因此其逆矩阵等于其转置矩阵。

2.1.4 使用矩阵形式的缺点

矩阵占用了更多的内存:保存一个方位,矩阵需要使用9个数,欧拉角只是用3个数,四元数只使用4个数。
难于理解:矩阵不能直观表示物体的方位。
矩阵可能是病态的:矩阵会发生矩阵蠕变现象。
矩阵无法插值:给定两个矩阵,无法在其中进行曲面线性插值。

2.2 欧拉角

欧拉角的基本思想是3D物体的旋转分解为绕3个相互垂直轴的三个旋转组成的序列,这组描述三个角度及旋转信息的数称为欧拉角。

欧拉角思想并未指定旋转顺序和旋转轴,只要求两个连续的旋转轴必须垂直,因此旋转轴有多种组合。另外欧拉角还分静态定义和动态定义,静态定义指其旋转轴为世界坐标系,动态定义指其旋转轴为模型坐标系,静态定义不会发生万向节死锁(万向节死锁稍后解释),动态定义会。这里通过经典力学中使用的动态zxz规范做图形说明。

上图中蓝色部分xyz为世界坐标系,红色表示的XYZ为模型坐标系,N为XYO平面和xyO平面的交线。模型坐标系变化过程为,首先绕Z轴旋转α度,再绕X轴旋转β度,最后绕Z轴旋转γ变换得到。

但是对于3D图形旋转而言,通常使用的是Y-X-Z,即首先进行yaw(偏航),再进行倾覆(pitch),最后翻滚(roll)的顺序进行。如下图所示。
2.2.1 万向节死锁

欧拉角有一个致命且无法消除的缺点,就是万向节死锁。即当第一次旋转的轴和第三次旋转的轴相对于世界坐标系重合时,欧拉角会丢失一个自由度,第一次和第三次相对于物体坐标系都绕同一个轴旋转。在上图中,假如第二次pitch的角度为90°,此时第一次yaw的角度和第三次roll的角度产生的都是滚筒旋转。为了避免这个问题,通常在第二次旋转为90°时,第一次强制旋转为0°,让所有的roll效果由第三次旋转实现。

2.2.2 欧拉角的优点

易于使用:欧拉角形象的表示出模型的朝向。
使用内存最少:欧拉角保存一个位置只需要使用三个数。
任意三个数都是合法的:任意三个角度都能找出一个对应的方位。

2.2.3 欧拉角的缺点

别名问题:同一个方位可以有无穷多个欧拉角组合,别名问题有三个原因。
第一,将一个角度加上360°后,方位不会改变,但是数值改变了。
第二,三个角度之间强耦合性,这使得如(yaw 0, pitch 135°, roll 0)和(yaw 180°, pitch 45°, roll 180°)表示同一个方位。

为了保证任意欧拉角都有唯一表示,通常对偏航、倾覆和翻滚的角度做出限制,令yaw和roll在180°到-180°之间,令pitch在90°到-90°之间。

第三,由于万向节死锁使得(yaw α, pitch 90°, roll β)和(yaw η, pitch 90°, roll θ)欧拉角组合,只要满足α-β等于η-θ,这两个欧拉角表示同一个方位。为了解决这个问题,规定只要当pitch为±90°时,roll恒等于0。

插值问题:引发插值困难的因素有三个。
第一,例如在300°和10°之间插值会绕优弧插值,并不是最短路径,此问题可以通过限制欧拉角解决。

第二,在使用限制欧拉角后,对于170°和-170°之间插值,仍会绕优弧行走,解决办法是将其角度差α映射到-180°到180°之间。公式为wrap(α) = α - 360((α+180) / 360)。其中空号内除法得到的是整数。

第三,当发生万向节死锁时,无法在两个欧拉角之间进行插值,并且该问题无法解决。只能通过四元数的方式表示方位角实现球面插值。

2.3 四元数

2.3.1 定义

一个四元数包含一个标量分量和一个3D向量分量,有两种表示方式。

2.3.2 起源

四元数起源于复数,复数对(a,b)定义了复数P(a+bi),其共轭复数定义为P’(a-bi)。复数的模定义为||P|| = square(P*P'),||P|| = square(a^2 + b^2)。将复数的实部作为二维笛卡尔坐标系的横轴,虚部作为其纵轴,构建一个复平面,任意复数都可以表示为该平面上的点。此时引入新的复数q,通过复数乘法能够表示平面上的旋转。

爱尔兰数学家William Hamilton一直致力于将复数从2D扩展到3D,最后他扩展了复数系统,定义复数P(w, xi, yj, zk)。其中i、j、k关系如下。该复数可以用四元数M[w, (x, y, z)]表示。类似于复数能够旋转2维向量,四元数能旋转3D向量。

2.3.3 四元数和轴-角对

3D中任意角位移都能表示为绕单一轴旋转,这种描述方式称为轴-角描述法,绕轴n绕α的角位移可以用轴角对(n, α)表示。四元数能够被解释为角位移的的轴角对方式。即上述轴角对可以用下面的四元数表示。

2.3.4 负四元数

四元数求负即将其各个分量分别取负,在空间几何中四元数M和-M表示相同的角位移。

2.3.5 单位四元数

数学上的单位四元数为M [1, 0],任何四元数乘以单位四元数都等于自身。在空间几何中还有单位四元数N [-1, 0],表示不发生任何角位移。

2.3.6 四元数的模

对于任意四元数,其求模公式如下。将表示角位移的四元数带人公式中可得其模为1。

2.3.7 四元数的共轭和逆

四元数的共轭表示为q*,将四元数的向量部分取负即可得到。四元数的逆表示为q-1。q-1 = q的共轭除以q的模。因此对于单位四元数,其共轭和逆四元数是相等的。

2.3.8 四元数的乘法(叉乘)

四元数的叉乘公式如下,需注意的是四元数的乘法满足结合律,不满足交换律。另外由乘法公式可以推导四元数乘积的模等于模的乘积。

2.3.9 四元数旋转空间向量

对于空间任意向量p,将其绕单位向量n旋转θ得到向量p‘,引入角轴对四元数q [cos(θ/2), nsin(θ/2)]。p’ = qpq-1,可以通过乘法展开,并将p'和使用旋转矩阵方式得到的结果比较从而证明该等式成立。实际上,该方法的时间成本和将四元数转换为旋转矩阵后再旋转向量的时间成本相当。该公式推导可参考文章

四元数的乘法可以连接多个角位移,当p分别经过轴角对a和b旋转,其旋转公式表示如下。

2.3.10 四元数的差

四元数的差被定义为一个方位到另一个方位的角位移,给定方位a和b,从a经过角位移d变化到b可用公式表示为ad = b。此时可计算出d = a-1b

2.3.11 四元数的点乘

四元数的点乘运算方式类似于向量的点乘,其公式如下。其几何意义可以表示为四元数a和b的点乘绝对值越大,它们代表的角位移越相似。

2.3.12 四元数的对数、指数和标量乘运算
2.3.13 四元数的幂运算

当四元数q的指数从0变化为1时,其值从 [1, 0] 变化为q。这里需注意当q代表的角位移角度为30°时,q2代表60°,q4代表的是240°,(q4)1/2表示的是120°,可以看出(q4)1/2不等于q2,因为数学上关于指数运算的代数公式对于四元数的求幂运算都不适用。

2.3.14 四元数的插值-slerp

欧拉角和一般线性插值都不能用于空间球面插值,四元数的slerp(Spherical Linear Interpolation)可以用于空间球面平滑插值。两个标量之间的一般线性插值可以如下定义。
d = a1 - a0; a~t~ = a0 + td
同样的对于四元数的插值也可以定义如下:
lerp(a0, a1, t) = a0 + td
计算两个值的差:上文讲过计算四元数q0到q1的差可以由角位移d = q0-1q1得出。
计算差的一部分:四元数的幂运算可以将四元数等分,dt = dt
在初值上加上偏移量:通过四元数的乘法可以组合角位移。
因此,得出四元数的插值公式为:slerp(q0, q1, t) = q0 (q0-1 q1)t

在实际应用中,可以通过空间几何推导出四元数插值一般公式。对于向量v0,旋转w可以得到向量v1,对于任意0到1之间的时间点t,其旋转后的向量表示为vt。

首先可以得到等式:vt = k0v0 + k1v1
根据上图由空间几何可以推导出k1 = sintw / sinw
将等式表示为vt = k1v1 + k0v0可以出导出k0 = sin(1-t)w / sinw
最后可以推导出vt = (sin(1-t)w / sinw) v0 + (sintw / sinw) v1
将该公式推导至四元数,因此四元数的插值一般公式可以表示为slerp(q0, q1, t) = (sin(1-t)w / sinw) q0 + (sintw / sinw) q1
这里需要注意,当q0和q1点乘为负时,其角位移方向为优弧表示的方向,因此需要对其中一个取负,另外当q0和q1非常接近时sinw的值会引起不必要的计算误差,此时需使用四元数插值的理论公式。

2.3.15 四元数的插值-squad

slerp提供了在两个方位之间插值,squad允许在一个方位序列中进行平滑插值,方位序列由一组四元数控制点构成。

此外,还需要引入辅助四元数序列si,将它作为临时控制点,为了为每个控制点都扩展一个对应的临时控制点,控制点前后分别增加虚拟控制点q0 = q1和qn+1 = qn。

引入插值变量h,当h从0变化到1时,squad描述了qi到qi+1之间的曲线,整条插值曲线分别用如下公式获得。此处并未深入讨论Squad的细节,如需了解,参考书籍Quaternions, Interpolation and Animation

2.3.16 四元数的优点

平滑插值:slerp和squad提供了方位间的平滑插值。
快速连接和角位移求逆:四元数叉乘能够将角位移序列转换为单个角位移,比矩阵连接更快。单位四元数的逆可以通过共轭四元数得到,相较于正交矩阵通过转置求逆更快。
可以和矩阵快速互相转换
内存占用较小:四元数存储一个方位只需要使用四个数。

2.3.17 四元数的缺点

四元数可能不合法:坏数据的输入或者浮点数的累积误差都可能引起四元数不合法,可用四元数标准化解决这个问题,即将四元数除以它的模,确保其为单位大小。

四元数对于空间方位描述有着不可替代的作用,在GLKit中可以使用GLKQuaternion相关方法对四元数进行运算。另外,正如当使用欧拉角去描述物体旋转,即便最后将欧拉角转换为矩阵形式,但是在欧拉角过程中容易发生万向节死锁,但是改用四元数变不会发生同样的事情。

3 方位表达方式之间的互相转换

前文已经说过,OpenGL的底层是通过矩阵变化来旋转物体的,因此无聊我们用的是哪一种形式表达刚体的方位,最终都需要转化为矩阵形式。

3.1 从欧拉角到仿射旋转矩阵

欧拉角转化到矩阵比较简单,上文旋转矩阵中已经分别介绍了绕各个轴单独的旋转矩阵,只需按规定欧拉角的旋转顺序将各个矩阵相乘即可得到,如x-y-z旋转顺序动态欧拉角对应的模型坐标系向世界坐标系转化的仿射旋转矩阵表示如下。从世界坐标系向模型坐标系转换的仿射矩阵使用相反的乘法,其结果为下述矩阵的转置矩阵。

3.2 从仿射旋转矩阵到欧拉角

根据上一小结中的旋转矩阵,可以直接使用反三角函数求出三个欧拉角,得出θ = arcSin(m33),Ψ = arcTan(-m12 / m11),Φ = arcTan(-m23 / m33),当θ = ±90°时,上述4个分量都为0,此时计算出θ = arcSin(m33),令Ψ = 0,在通过矩阵计算出Φ的值。

3.3 从四元数到矩阵

前文已经得到绕任意轴旋转的矩阵表达式如下。

表示绕任意轴旋转的四元数q [w, x, y, z]如下。

此时通过数学上的一些技巧可以将矩阵中各个元素用四元数分量表示出来。对于对角线上的元素如m11,可以通过下面的推导公式求出:

对于非对角线的元素如m12,可以通过以下推导获得。

因此最后可以写出用四元数转换为的旋转仿射矩阵表示如下:

3.4 从矩阵到四元数

根据上述公式,各个元素之间代数运算可得到下述关系,此处w-x-y-z中,通过判断四个分量中最大的值如w2,再解平方根确定一个分量w,再根据其余等式计算剩余分量,这样会有两个不同的解,但是它们代表了相同的角位移。

3.5 从欧拉角到四元数

将绕x-y-z轴旋转的欧拉角分别用p-h-b表示,则其对应的四元数可以如下表示。

假定欧拉角为y-x-z的动态旋转顺序,则表示物体坐标系在世界坐标系的其整个角位移的四元数q = bph可以表示为如下公式。

3.6 从四元数到欧拉角

可以通过上面公式解方程求出p-h-b的值,但是通常利用欧拉角转换到矩阵,四元数转换到矩阵的公式联合求解出分别绕x-y-z轴动态旋转的欧拉角。注意,不同旋转顺序其欧拉角有不同的解。

4 理解空间变换

4.1 连接矩阵变换级模型坐标系向世界坐标系转换

一个向量经过多个连续的空间变换可以通过矩阵相乘合成一个变换,然后右乘列向量。但是这里需要注意的是,矩阵乘法的顺序非常重要,当基于模型坐标系做动态变化时,矩阵的乘法顺序和变化顺序相同,当基于世界坐标系做静态变化时,矩阵的乘法顺序和变化顺序相反。例如基于世界坐标系绕y轴旋转一个模型,然后再将其平移(4,10,-20)的代码应该如下表示。

GLKVector4 inputVector;
GLKMatrix4 rotationMatrix = GLKMatrix4MakeRotation(M_PI_2, 0, 1, 0);
GLKMatrix4 translationMatrix = GLKMatrix4MakeTranslation(4, 10, -20);
GLKMatrix4 compositeMatrix = GLKMatrix4Multiply(translationMatrix, rotationMatrix);
GLKVector4 outputVector = GLKMatrix4MultiplyVector4(compositeMatrix, inputVector);

4.2 模型坐标系向视图坐标系转换

在OpenGL中,模型首先被转化到世界坐标系中,再将其转化到视图坐标系中。默认情况下透视投影中的观察点坐标为世界坐标系的原点,空头的朝向为Z轴负方向。然而,在一般的投影空间中,观察者被认为在z轴的正无穷远处,这样便能观察到整个视图空间的模型。

将模型从模型空间转换到视图空间的转换成为模型-视图转换(model-view transform),对应的矩阵成为模型视图矩阵(model-view matrix)。
由模型-世界转换(model-world transform)以及世界-视图转换(world-view transform)合成。在OpenGL中,world-view matrix右乘model-world matrix得到model-view matrix。

4.3 观察矩阵(Lookat Matrix)

可以通过观察者矩阵(Lookat Matrix)来构建world-view matrix,通过世界坐标系的y轴方向向量(up vector),相机聚焦的中心(center,用向量表示为center vector),相机的位置(eye,可以表示为向量eye vector)可以确定视图坐标系。将世界坐标系中的点转化到视图坐标系中的仿射矩阵推导过程如下。

点再线上的投影可以用过表示点的向量和表示线的单位向量点乘获得,因此世界坐标系原点位置相对于视图坐标系可以如下计算,即可获得将需要的仿射矩阵,这里需要注意仿射矩阵不是正交矩阵。需要计算从视图坐标系向世界坐标系转化的仿射矩阵时可以在求得3*3阶旋转矩阵后,将眼睛对于世界坐标系的位置带入旋转矩阵即可。

在GLKit中可以调用如下函数构造该仿射矩阵。

GLKMatrix4MakeLookAt(float eyeX, float eyeY, float eyeZ,
                                                  float centerX, float centerY, float centerZ,
                                                  float upX, float upY, float upZ)

4.4 投影变换(Projection Transformations)

将模型转换到视图空间后OpenGL将会开始投影变换操作,此时将会决定模型的可见范围,同时建立剪切面。

4.4.1 正交投影(Orthographic Projection)

在正交投影中,模型将被垂直投影到屏幕上,即无论观察者距离物体的距离有多远,它投影后的大小都相同。正交投影通常用于二维图形和文字的处理。

正交矩阵
构建好的正交矩阵如下图所示

GLKit中使用如下函数构建正交矩阵

GLKMatrix4 GLKMatrix4MakeOrtho(float left, float right,
                                                 float bottom, float top,
                                                 float nearZ, float farZ)
4.4.2 透视投影(Perspective Projection)**

透视投影的特点是对于同样尺寸的模型,其距离观察者的距离越远,它被投影到屏幕上的尺寸越小。

透视矩阵(Perspective Matrices)
透视矩阵又被称为平截椎体矩阵(frustum matrix),在OpenGL中通过指定其近矩形平面的四个方向距离中心的距离,以及4个剪切平面(及平截椎体除去近、远剪切面)的坐标可以直接构造如下透视矩阵。

GLKit中使用如下函数构建透视矩阵

GLKMatrix4MakeFrustum(float left, float right,
                                                   float bottom, float top,
                                                   float nearZ, float farZ)

另外,还可以通过指定垂直方向上的可视角度(以pi为单位),指定水平和垂直宽度比,以及远近剪切面的距离直接构建对称的透视矩阵

GLKMatrix4 GLKMatrix4MakePerspective(float fovyRadians, float aspect, float nearZ, float farZ)

5 插值、直线、曲线和样条曲线(Interpolation, Lines, Curves, and Splines)

5.1 线性插值

线性插值:尽管对于矩阵的线性插值结果并没有实际的意义,但是角度、位置和坐标常使用线性插值,在OpenGLSL中提供内置函数vec4 mix(vec4 A, vec4 B, float t);

5.2 曲线

OpenGL中曲线类型为贝塞尔曲线,它由一个起点一个终点以及其中一系列控制点组成。由起点A,终点C,控制点B描绘出贝塞尔曲线如下图。

当t从0变化到1时,对于P点,其满足下列公式:
D = A + t(B - A)......E = B + t(C - B)......P = D + t(E -D)
带入公式最终得到:
P = A + 2t(B - A) + t2(C - 2B + A)

在OpenGLSL中可以使用多个线性插值函数绘制2次贝塞尔曲线:

vec4 quadratic_bezier(vec4 A, vec4 B, vec4 C, float t) {
  vec4 D = mix(A, B, t); 
  vec4 E = mix(B, C, t);
  vec4 P = mix(D, E, t); return P;
}

由起点A,终点D,控制点BC描绘的贝塞尔曲线如下图。

当t从0变化到1时,对于P点,其满足下列公式:
E = A + t(B - A)......F = B + t(C - B)......G = C + t(D - C)
H = E + t(F - E).......I = F + t(G - F )......P = H + t(I - H)

在OpenGLSL中可以使用多个线性插值函数绘制3次贝塞尔曲线:

vec4 cubic_bezier(vec4 A, vec4 B, vec4 C, vec4 D, float t) {
  vec4 E = mix(A, B, t);      //E=A+t(B-A) 
  vec4 F = mix(B, C, t);      //F=B+t(C-B)
  vec4 G = mix(C, D, t);      //G=C+t(D-C)
  vec4 H = mix(E, F, t);      //H=E+t(F-E) 
  vec4 I = mix(F, G, t);      //I=F+t(G-F)
  vec4 P = mix(H, I, t);      //P=H+t(I-H)
  return P;
}

可以改写为:

vec4 cubic_bezier(vec4 A, vec4 B, vec4 C, vec4 D, float t) {
  vec4 E = mix(A, B, t); 
  vec4 F = mix(B, C, t); 
  vec4 G = mix(C, D, t);
  return quadratic_bezier(E, F, G, t); 
}

因此,更高阶的贝塞尔曲线都可以按照这样的方式构建出,但是通常高于包含4个点的时曲线的描绘就不再使用贝塞尔曲线了,而使用样条曲线。含有5个点的贝塞尔曲线构建方式如下:

vec4 quintic_bezier(vec4 A, vec4 B, vec4 C, vec4 D, vec4 E, float t) {
  vec4 F = mix(A, B, t); 
  vec4 G = mix(B, C, t); 
  vec4 H = mix(C, D, t); 
  vec4 I = mix(D, E, t);
  return cubic_bezier(F, G, H, I, t); 
}

5.3 样条曲线(Splines)

样条曲线是由多个小的曲线(如贝塞尔曲线)组成,每个曲线称为片段(segment),片段两头的控制点称为焊点(welds),片段内部的点称为节点(knots),焊点一定被相邻片段共享,有时节点也会被相邻片段共享。

一个由三个贝塞尔曲线组成的B类立方样条曲线(cubic B-spline)(ABCD)-(DEFG)-(GHIJ)如下图所示。注意这里CDE共线,FGH共线,其中D、G都为各自线段的中点。

当t从0分别变化到1、2、3时,P分别变化到D、G和J。其样条曲线生成函数可以如下构建。使用改方程时,匀速变化的t会得到匀速变化的p,反之p将不会匀速插值。

vec4 cubic_bspline_10(vec4 CP[10], float t) {
  if (t <= 0.0) {
    return CP[0];
  }
  if (t >= 1.0) {
    return CP[9];
  }

  float f = t * 3.0;
  int i = int(floor(f));   //  取整数
  float s = fract(f);      //  取小数

  vec4 A = CP[i * 3]; 
  vec4 B = CP[i * 3 + 1]; 
  vec4 C = CP[i * 3 + 2]; 
  vec4 D = CP[i * 3 + 3];
  return cubic_bezier(A, B, C, D, s); 
}

如果一个函数具有连续的一阶导数(first derivative)则被称为C1连续,如果它具有连续的二阶导数(second derivative)称为C2连续。贝塞尔曲线同是C1和C2连续,为了保证焊点位置是连续的,每个焊点出发到下一个节点的方向和t变化速率都必须同上一个节点到该焊点的对应方向和速率(焊点为两个相邻节点的中点)相同。满足上述条件样条函数也满足C1和C2连续。这样的样条曲线也称为三次埃米尔特样条插值(cubic Hermite spline)简称为(cspline)。

OpenGL
Web note ad 1