变换矩阵

获取示例代码,本文代码在分支chapter5中。


在介绍本文的代码之前,先要了解一个概念:矩阵。学过线性代数的朋友应该都知道矩阵相当于是一个二维数组,有自己的运算规则。下面就通过几个例子简单了解一下矩阵的特性。

3X3矩阵的加法 从图中可以看出3X3矩阵就像是一个3X3的表格,每个单元格中填写一个数。它的加法就是把两个矩阵对应位置的元素加起来放在结果矩阵对应的位置上。那么如果相加的两个矩阵尺寸不一样怎么办?答案是无法运算。矩阵的加减要求两边的矩阵必须尺寸相等。

下面是减法的运算,和加法相似,很好理解。

接下来是乘法。

乘法稍微有些复杂,所以我用符号来代替数值,方便观察规律。我们看结果的第一行第一列aj + bm + cp,它就是左边的矩阵第一行和右边的矩阵第一列逐个相乘再相加的结果。

再看结果的第二行第一列aj + bm + cp,它就是左边的矩阵第二行和右边的矩阵第一列逐个相乘再相加的结果。

差不多已经可以总结出规律了,结果矩阵的第n行第m列的结果就是左边的矩阵第n行和右边的矩阵第m列逐个相乘再相加的结果。读者可以看看其他的值是不是这样计算出来的。

最后说一下除法,矩阵的除法有些特殊,比如说B/A,可以换算成B*inv(A)。inv(A)是A的逆矩阵,因为不是所有矩阵都有逆矩阵,所以除法在矩阵计算中并不总是可用。逆矩阵的求解比较复杂,这里暂时就不解释了。本文目前也没有用到求逆矩阵的地方。如果读者感兴趣的话,可以自行百度或者翻一翻以前的线性代数课本。

变换矩阵

说完了矩阵,那么什么是变换矩阵呢?在图形绘制过程中,有三种变换,分别是平移,缩放,旋转。如果我们想要用代码表示一个3D环境中的变换需要几个变量呢,首先要有平移tx, ty, tz,然后是缩放sx, sy, sz,最后是旋转rx, ry, rz。在渲染的时候把这些变量附加到原始的位置数据上就可以实现变换了。这种方式虽然可行但不够好,尤其是在GPU上这种方式产生的运算负担远大于使用矩阵。

attribute vec4 position;
attribute vec4 color;

uniform float elapsedTime;
uniform mat4 transform;

varying vec4 fragColor;

void main(void) {
    fragColor = color;
    gl_Position = transform * position;
}

这是本文代码例子中的Vertex Shader,新增了一个uniform uniform mat4 transform;mat4这个类型前文有提到过,4X4的矩阵。它是Shader内置的类型,支持直接加减乘等操作。使用矩阵会产生更少的运算指令,GPU可以更好的优化运算过程。那么应该怎么使用呢?接下来我就一一介绍每一种变换矩阵。

平移矩阵

假设有一个点(1, 2, 3),经过大小为(1, 2, 3)的平移,最终必定会平移到(1+1, 2+2, 3+3)的位置。使用矩阵计算如下。

这里补充一点,如果左边的矩阵的列数等于右边的矩阵的行数,它们就可以相乘,结果矩阵的行数等于左边矩阵的行数,列数等于右边矩阵的列数。

平移矩阵就是一个4X4的单位矩阵的第4行的前三个元素用tx,ty,tz填充之后的矩阵。下面就是一个单位矩阵。

使用GLKMatrix4 translateMatrix = GLKMatrix4MakeTranslation(tx, ty, tz);可以得到一个平移矩阵。GLKMatrix4MakeTranslation位于GLKit中。

缩放矩阵

缩放矩阵的三个缩放元素sx,sy,sz,分布在从左到右的对角线上,矩阵相乘后位置的x,y,z分别乘以了sx,sy,sz,从而实现了缩放。

代码实现如下。

GLKMatrix4 scaleMatrix = GLKMatrix4MakeScale(sx, sy, sz);

旋转矩阵

旋转矩阵相比于上面两个略微有些复杂,旋转包含两个重要元素,旋转的角度,绕什么轴旋转。具体原理可以参考三维空间中的旋转:旋转矩阵、欧拉角。代码实现如下。

GLKMatrix4 rotateMatrix = GLKMatrix4MakeRotation(M_PI/2 , 0.0, 0.0, 1.0);

M_PI/2是弧度,0.0,0.0,1.0是旋转轴的向量。

综合三个矩阵

现在我们得到了三个矩阵,接下来就是把它们相乘。

self.transformMatrix = GLKMatrix4Multiply(translateMatrix, rotateMatrix);
self.transformMatrix = GLKMatrix4Multiply(self.transformMatrix, scaleMatrix);

注意相乘的顺序translateMatrix * rotateMatrix * scaleMatrix,这样可以保证先缩放再旋转,最后再平移。如果先平移再缩放,点的位置已经改变,缩放出来的结果自然就不对了。

代码实现

最后回到本文的代码实现中来,我把chapter4的代码整理了一下,公用的东西移到了基类GLBaseViewController里,这样可以更加专注于要重点介绍的知识点。目前ViewController.m中代码如下。

//
//  ViewController.m
//  OpenGLESDemo
//
//  Created by wangyang on 15/8/28.
//  Copyright (c) 2015年 wangyang. All rights reserved.
//

#import "ViewController.h"

@interface ViewController ()
@property (assign, nonatomic) GLKMatrix4 transformMatrix;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.transformMatrix = GLKMatrix4Identity;
}

#pragma mark - Update Delegate

- (void)update {
    [super update];
    float varyingFactor = sin(self.elapsedTime);
    GLKMatrix4 scaleMatrix = GLKMatrix4MakeScale(varyingFactor, varyingFactor, 1.0);
    GLKMatrix4 rotateMatrix = GLKMatrix4MakeRotation(varyingFactor , 0.0, 0.0, 1.0);
    GLKMatrix4 translateMatrix = GLKMatrix4MakeTranslation(varyingFactor, 0.0, 0.0);
    // transformMatrix = translateMatrix * rotateMatrix * scaleMatrix
    // 矩阵会按照从右到左的顺序应用到position上。也就是先缩放(scale),再旋转(rotate),最后平移(translate)
    // 如果这个顺序反过来,就完全不同了。从线性代数角度来讲,就是矩阵A乘以矩阵B不等于矩阵B乘以矩阵A。
    self.transformMatrix = GLKMatrix4Multiply(translateMatrix, rotateMatrix);
    self.transformMatrix = GLKMatrix4Multiply(self.transformMatrix, scaleMatrix);
}

- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
    [super glkView:view drawInRect:rect];
    
    GLuint transformUniformLocation = glGetUniformLocation(self.shaderProgram, "transform");
    glUniformMatrix4fv(transformUniformLocation, 1, 0, self.transformMatrix.m);
    [self drawTriangle];
}


#pragma mark - Draw Many Things
- (void)drawTriangle {
    static GLfloat triangleData[36] = {
        0,      0.5f,  0,  1,  0,  0, // x, y, z, r, g, b,每一行存储一个点的信息,位置和颜色
        -0.5f,  0.0f,  0,  0,  1,  0,
        0.5f,   0.0f,  0,  0,  0,  1,
        0,      -0.5f,  0,  1,  0,  0,
        -0.5f,  0.0f,  0,  0,  1,  0,
        0.5f,   0.0f,  0,  0,  0,  1,
    };
    [self bindAttribs:triangleData];
    glDrawArrays(GL_TRIANGLES, 0, 6);
}

@end

Vertex Shader

attribute vec4 position;
attribute vec4 color;

uniform float elapsedTime;
uniform mat4 transform;

varying vec4 fragColor;

void main(void) {
    fragColor = color;
    gl_Position = transform * position;
}

代码中每一次update计算新的变换矩阵,渲染时把值传递给Vertex Shader的uniform mat4 transform,Vertex Shader把原始位置和transform相乘,得出新的位置。注意,因为transform是mat4,所以给uniform赋值时使用的是glUniformMatrix4fv 。代码效果如下。

本篇主要介绍了什么是变换矩阵,如何使用变换矩阵以及怎样和Vertex Shader配合。下一篇就要开始介绍3D渲染最基本的技术,透视投影矩阵。

Updated: