Java软光栅渲染器-空间变换

空间变换

目标

对于所有3D图形引擎来说,将一组向量从一个坐标空间变换到另一个坐标空间的操作是最常用的和最基本的操作之一。例如,一个模型的顶点数据通常以对象空间的坐标形式保存,而在渲染该模型前,顶点坐标必须变换到虚拟相机空间才能被正确处理。本章主要介绍不同笛卡尔坐标系之间的线性变换,包括简单的比例变换、平移变换和旋转变换。

《3D游戏与计算机图形学中的数学方法》,第4章 坐标变换

本文将要实现下列变换:

  • 实现比例变换
  • 实现旋转变换
  • 实现平移变换
  • 实现齐次坐标
  • 实现法向量变换
  • 表达形式的互相转换

实现

比例变换

对模型中任意顶点 v,分别沿x、y、z轴分别按比例缩放,只需要把 v 的各个分量与x、y、z相乘即可。

Vector3f v = new Vector3f(a, b, c);
Vector3f scale = new Vector3f(x, y, z);
v.multLocal(scale);

结果:(ax, by, cz)。

用3x3矩阵来描述这个旋转变换,需要把单位矩阵的对角线元数设为x、y、z。

           |x  0  0|
M(scale) = |0  y  0|
           |0  0  z|

矩阵 M(scale) 乘以列向量 v(a, b, c) ,可以使 v 按比例缩放。

               |x  0  0| |a|
M(scale) * v = |0  y  0|*|b|
               |0  0  z| |c|

               |ax+b0+c0|   |ax|
M(scale) * v = |0x+by+c0| = |by|
               |0x+0y+cz|   |cz|

对于4x4矩阵,把矩阵左上角的3x3矩阵当做旋转矩阵,第4列元数保持为 [0 0 0 1]即可。

           |x  0  0  0|
M(scale) = |0  y  0  0|
           |0  0  z  0|
           |0  0  0  1|

Matrix3f实现:

/**
 * 比例变换矩阵
 * @param v
 * @return
 */
public Matrix3f fromScale(Vector3f v) {
    m00 = v.x; m01 = 0;   m02 = 0;
    m10 = 0;   m11 = v.y; m12 = 0;
    m20 = 0;   m21 = 0;   m22 = v.z;
    return this;
}

/**
 * 比例变换矩阵
 * @param v
 * @return
 */
public Matrix3f fromScale(float x, float y, float z) {
    m00 = x; m01 = 0; m02 = 0;
    m10 = 0; m11 = y; m12 = 0;
    m20 = 0; m21 = 0; m22 = z;
    return this;
}

Matrix4f实现:

/**
 * 4*4矩阵的左上角3个Vector3f,表示缩放矩阵。
 * @param v
 * @return
 */
public Matrix4f fromScale(Vector3f v) {
    m00 = v.x; m01 = 0;   m02 = 0;   m03 = 0;
    m10 = 0;   m11 = v.y; m12 = 0;   m13 = 0;
    m20 = 0;   m21 = 0;   m22 = v.z; m23 = 0;
    m30 = 0;   m31 = 0;   m32 = 0;   m33 = 1;
    return this;
}

/**
 * 4*4矩阵的左上角3个Vector3f,表示缩放矩阵。
 * @param x
 * @param y
 * @param z
 * @return
 */
public Matrix4f fromScale(float x, float y, float z) {
    m00 = x; m01 = 0; m02 = 0; m03 = 0;
    m10 = 0; m11 = y; m12 = 0; m13 = 0;
    m20 = 0; m21 = 0; m22 = z; m23 = 0;
    m30 = 0; m31 = 0; m32 = 0; m33 = 1;
    return this;
}

旋转变换

在二维空间(平面)中旋转 θ 角,可以用2x2矩阵描述。

|cos(θ) -sin(θ)|
|sin(θ)  cos(θ)|

把它“扩展”到三维空间,可以认为这是绕Z轴旋转 θ 角。矩阵的第三行和第三列元数与单位矩阵保持一致,这可保证向量在旋转的过程中 z 坐标保持不变。

        |cos(θ) -sin(θ)   0|
Rz(θ) = |sin(θ)  cos(θ)   0|
        |    0       0    1|

按同样的方式,可以分别得到绕X轴和Y轴旋转的矩阵。

        |  1      0        1 |
Rx(θ) = |  0  cos(θ)  -sin(θ)|
        |  0  sin(θ)   cos(θ)|

        | cos(θ)  0  sin(θ)|
Ry(θ) = |     0   1      0 |
        |-sin(θ)  0  cos(θ)|

Matrix3f实现:

/**
 * 绕x轴旋转
 * @param angle
 * @return
 */
public Matrix3f fromRotateX(float angle) {
    float sin = (float) Math.sin(angle);
    float cos = (float) Math.cos(angle);

    m00 = 1; m11 = 0;   m12 = 0;
    m10 = 0; m11 = cos; m12 = -sin;
    m20 = 0; m21 = sin; m22 = cos;

    return this;
}

/**
 * 绕y轴旋转
 * @param angle
 * @return
 */
public Matrix3f fromRotateY(float angle) {
    float sin = (float) Math.sin(angle);
    float cos = (float) Math.cos(angle);

    m00 = cos;  m11 = 0; m12 = sin;
    m10 = 0;    m11 = 1; m12 = 0;
    m20 = -sin; m21 = 0; m22 = cos;

    return this;
}

/**
 * 绕z轴旋转
 * @param angle
 * @return
 */
public Matrix3f fromRotateZ(float angle) {
    float sin = (float) Math.sin(angle);
    float cos = (float) Math.cos(angle);

    m00 = cos; m11 = -sin; m12 = 0;
    m10 = sin; m11 = cos;  m12 = 0;
    m20 = 0;   m21 = 0;    m22 = 1;

    return this;
}

欧拉角

对于欧拉角旋转来说,可以把这三个矩阵相乘,就能得到结果。需要注意的是,α、β、γ都不能超过90°,取值范围都是[-π/2, π/2]

R(α, β, γ) = Rz(γ) * Ry(β) * Rx(α);

Matrix3f实现:

/**
 * 欧拉角旋转
 * @param xAngle
 * @param yAngle
 * @param zAngle
 * @return
 */
public Matrix3f fromRotate(float xAngle, float yAngle, float zAngle) {
    Matrix3f rotateX = new Matrix3f().fromRotateX(xAngle);
    Matrix3f rotateY = new Matrix3f().fromRotateX(yAngle);
    Matrix3f rotateZ = new Matrix3f().fromRotateX(zAngle);

    Matrix3f result = rotateZ.mult(rotateY).mult(rotateX);
    return result;
}

注意:这个计算方法的性能很差,可以直接根据矩阵乘法规则算出结果矩阵,然后化简。

轴角对

将向量 P 绕任意轴旋转 θ 角,若旋转轴用单位向量 A,则向量 P 可以分解为分别与向量 A 平行和垂直的分量。由于与向量 A 平行的分量,也就是向量 P 在向量 A 上的投影,在旋转过程中保持不变,因此,问题简化为向量 P 中与向量 A 垂直的分量的旋转问题。

因为向量 A 为单位向量,向量 P 在向量 A 上的投影可简单表示为:

proj P = (A . P)A

向量 P 中与向量 A 垂直的分量的表达式为:

perp P = P-(A . P)A

将垂直分量 perp P 绕向量 A 旋转后,再加上平行分量 proj P,所得的向量和为向量 P 的旋转结果。

垂直分量 perp P 绕向量 A 旋转 θ 角后的向量可表示为:

perp P = [P-(A . P)A]cosθ + (A×P)sinθ

再加上平行于旋转轴 A 的分量 proj P,可以得到向量 P 绕向量 A 旋转后的结果 P':

P' = Pcosθ + (A×P)sinθ + A(A . P)(1-cosθ)

然后,再把(AxP)和A(A.P)换成等效矩阵,就可求出旋转矩阵。

     |1  0  0|        | 0  -z   y|        |xx  xy  xz|
P' = |0  1  0|Pcosθ + | x   0  -x|Psinθ + |xy  yy  yz|P(1-cosθ)
     |0  0  1|        |-y   x   0|        |xz  yz  zz|

我在算了两页纸,没有算出来。矩阵真叫人头晕。

令c=cosθ和s=sinθ,可得旋转矩阵RA(θ),该矩阵将任意向量 P 绕任意旋转轴 A 旋转 θ 角。

        | (1-c)xx + c   (1-c)xy - sz  (1-c)xz + sy |
RA(θ) = | (1-c)xy + sz  (1-c)yy + c   (1-c)yz - sx |
        | (1-c)xz - sy  (1-c)yz + sx  (1-c)zz + c  |

Matrix3f实现如下:

/**
 * 轴角对旋转矩阵
 * @param v
 * @param angle
 * @return
 */
public Matrix3f fromAxisAngle(Vector3f v, float angle) {
    return fromAxisAngle(v.x, v.y, v.z, angle);
}

/**
 * 轴角对旋转矩阵。
 * @param vx
 * @param vy
 * @param vz
 * @param angle
 * @return
 */
public Matrix3f fromAxisAngle(float vx, float vy, float vz, float angle) {
    float length = vx * vx + vy * vy + vz * vz;

    if (length == 0) {
        zero();
        return this;
    }

    // 先把向量规范化
    if (Math.abs(length - 1.0) > 0.0001) {
        length = (float) (1.0 / Math.sqrt(length));
        vx *= length;
        vy *= length;
        vz *= length;
    }

    float sin = (float) Math.sin(angle);
    float cos = (float) Math.cos(angle);

    // 节省5次减法运算
    float _1_minus_cos = 1f - cos;

    // 节省3次乘法运算
    float xSin = vx * sin;
    float ySin = vy * sin;
    float zSin = vz * sin;

    // 节省6次乘法运算
    float xyM = vx * vy * _1_minus_cos;
    float xzM = vx * vz * _1_minus_cos;
    float yzM = vy * vz * _1_minus_cos;

    m00 = vx * vx * _1_minus_cos + cos;
    m01 = xyM - zSin;
    m02 = xzM + ySin;
    m10 = xyM + zSin;
    m11 = vy * vy * _1_minus_cos + cos;
    m12 = yzM - xSin;
    m20 = xzM - ySin;
    m21 = yzM + xSin;
    m22 = vz * vz * _1_minus_cos + cos;

    return this;
}

平移变换

以上仅仅介绍了对三维向量进行3x3矩阵的变换,通过多个单一变换矩阵的乘积,可以将一系列单一变换矩阵合并成一个3x3变换矩阵。另外一种重要变换还未介绍,该变换就是平移变换

坐标系在空间中的平移变换可以通过加上一个偏移向量实现,平移变换而不影响坐标轴的方向和比例,而且也不能表示成3x3矩阵的形式。考虑将一点 P 从一个坐标系变换到另一个坐标系中的一般表达式如下所示:

P' = MP + T

其中,矩阵M是一个可逆的3x3矩阵,矩阵T是一个3D平移向量。在该表达式中要执行两类矩阵运算(比例变换和旋转变换),最终得到一个繁琐的表达式:

P' = M2(M1P + T1) + T2
   = (M2M1)P + M2T1 + T2

如果要将n个变换合并,需要继续计算每一步中的 MnMn-1 和 MnTn-1 + Tn 的值。

齐次坐标

幸运的是,存在一个紧凑简便的数学表达式,可以表示上面的变换。在该表达式中,向量被扩展成了一个四维齐次坐标的表达形式,变换矩阵也成了4x4矩阵。通过给一个3D点 P 增加第四个坐标,可以将其变成四维的,第四个坐标称为 w 坐标,w = 1

构造一个4x4变换矩阵与上面的3x3矩阵M和3D平移变量T对应,如下所示:

    |m00 m01 m02 : Tx|
    |m10 m11 m12 : Ty|
F = |m20 m21 M22 : Tz|
    |------------:---|
    | 0   0   0  : 1 |

用该矩阵乘以向量 对向量的x、y、z轴坐标的变换结果与上面的两个变换等效。可以用4x4向量的第四列来定义平移变换。

/**
 * 设置平移变换
 * @param t
 */
public void setTranslation(Vector3f t) {
    m03 = t.x;
    m13 = t.y;
    m23 = t.z;
}

回顾上一章实现的4x4矩阵和3D向量的乘法,结果等同于先用3x3矩阵旋转、缩放3D向量,然后再加上

/**
 * 4x4矩阵与Vector3f向量相乘,结果保存在一个新的store对象中。
 * 
 * @param vec
 * @param store
 * @return
 */
public Vector3f mult(Vector3f vec, Vector3f store) {
    if (store == null) {
        store = new Vector3f();
    }

    float vx = vec.x, vy = vec.y, vz = vec.z;
    store.x = m00 * vx + m01 * vy + m02 * vz + m03;
    store.y = m10 * vx + m11 * vy + m12 * vz + m13;
    store.z = m20 * vx + m21 * vy + m22 * vz + m23;

    return store;
}

点和方向

在三维空间中,表示一个点的向量和表示方向的向量是不同的。在平移变换中,表示点的向量要发生改变,而表示方向的向量保持不变。

为了使用与点向量变换相同的4x4变换矩阵进行方向的变换,可以将方向向量的坐标扩展成四维的,并令坐标w等于0,这将消除矩阵 F 的第4列队方向向量的影响,只剩下左上角3x3矩阵影响方向向量的变换。

用齐次坐标表示的两个点向量 P 和 Q,它们的 w 坐标等于1,因此它们的差表示的方向向量 P-Q 的 w 坐标等于 0,这个结果很有用,因为方向向量 P-Q 表示从 P 指向 Q 的方向,不应受平移变换的影响。

法向量变换

使法向量正常旋转的旋转矩阵应为 顶点旋转矩阵逆矩阵转置矩阵,必须如此变换的向量称为 共变相量,而以矩阵M为变换矩阵的普通方式进行变换的向量称为 逆变向量

计算方法:先取其左上角的3x3旋转矩阵,再求其逆矩阵,再求转置矩阵即可。

Matrix4f worldMat = ..;// 4x4变换矩阵
Matrix3f normalMat = new Matrix3f(); // 法向量旋转矩阵

worldMat.toRotateMatrix(normalMat);
normalMat.inserseLocal();
normalMat.transposeLocal();

如果矩阵M为 正交矩阵,那么 M的逆矩阵 = M的转置矩阵 ,此时使法向量正常旋转的矩阵就等于矩阵M本身。这时为了计算法向量的变换矩阵的 求逆矩阵转置矩阵 的操作可以避免。

矩阵M 乘以 矩阵M的转置 等于 单位矩阵I,就说明这是一个正交矩阵。

Matrix4f worldMat = ..;// 4x4变换矩阵
Matrix3f mat1 = new Matrix3f();// 旋转矩阵
Matrix3f mat2 = new Matrix3f();// 转置矩阵

worldMat.toRotateMatrix(mat1);
mat2 = rotateMat.transpose();
if (mat1.mult(mat2).isIdentity() ) {
    // 说明是正交矩阵
}

法向量变换矩阵的推导过程就不写了。

表达形式的互相转换

四元数转矩阵

四元数和3x3矩阵都表示轴角对旋转。若旋转轴为单位向量,旋转角为 θ,根据前面的内容可知,四元数的各个分量与 cos(θ/2)、sin(θ/2) 有关,而3x3矩阵与 cos(θ)、sin(θ)有关。使用三角函数的倍角公式,就可以对它们进行互相转换。

Quaternion转矩阵的代码实现如下:

/**
 * 四元数转3x3旋转矩阵
 * @param result
 * @return
 */
public Matrix3f toRotationMatrix(Matrix3f result) {

    if (result == null)
        result = new Matrix3f();

    // 先计算2x、2y、2z的值,可以节省多次乘法运算。
    float _2x = x * 2;
    float _2y = y * 2;
    float _2z = z * 2;
    float _2xx = x * _2x;
    float _2xy = x * _2y;
    float _2xz = x * _2z;
    float _2xw = w * _2x;
    float _2yy = y * _2y;
    float _2yz = y * _2z;
    float _2yw = w * _2y;
    float _2zz = z * _2z;
    float _2zw = w * _2z;

    result.m00 = 1 - (_2yy + _2zz);
    result.m01 = (_2xy - _2zw);
    result.m02 = (_2xz + _2yw);
    result.m10 = (_2xy + _2zw);
    result.m11 = 1 - (_2xx + _2zz);
    result.m12 = (_2yz - _2xw);
    result.m20 = (_2xz - _2yw);
    result.m21 = (_2yz + _2xw);
    result.m22 = 1 - (_2xx + _2yy);

    return result;
}

四元数转4x4矩阵,要稍微复杂一些。先消除矩阵中的比例变换,等计算完之后再复原。

/**
 * 四元数转4x4矩阵
 * @param result
 * @return
 */
public Matrix4f toRotationMatrix(Matrix4f result) {

    Vector3f originalScale = new Vector3f();

    // 保存矩阵原来的比例变换
    result.toScaleVector(originalScale);
    result.setScale(1, 1, 1);

    // 先计算2x、2y、2z的值,可以节省多次乘法运算。
    float _2x = x * 2;
    float _2y = y * 2;
    float _sz = z * 2;
    float _2xx = x * _2x;
    float _2xy = x * _2y;
    float _2xz = x * _sz;
    float _2xw = w * _2x;
    float _2yy = y * _2y;
    float _2yz = y * _sz;
    float _2yw = w * _2y;
    float _2zz = z * _sz;
    float _2zw = w * _sz;

    result.m00 = 1 - (_2yy + _2zz);
    result.m01 = (_2xy - _2zw);
    result.m02 = (_2xz + _2yw);
    result.m10 = (_2xy + _2zw);
    result.m11 = 1 - (_2xx + _2zz);
    result.m12 = (_2yz - _2xw);
    result.m20 = (_2xz - _2yw);
    result.m21 = (_2yz + _2xw);
    result.m22 = 1 - (_2xx + _2yy);

    // 恢复矩阵的比例变换
    result.setScale(originalScale);

    return result;
}

用三角函数的半角公式,也能把3x3矩阵转成四元数。需要注意的是,3x3矩阵可能是多个变换矩阵的乘积,它即可以表示旋转变换,也可以表示比例(缩放)变换。在转换成四元数之前,需要先把比例变换对矩阵的影响消除掉。具体的做法,是先把3x3矩阵中的每一列向量规范化,然后再转换四元数。

这个推导的过程有些复杂,我直接复制了jMonkeyEngine中Quaternion类的实现:

/**
 * 旋转矩阵转四元数
 * @param m00
 * @param m01
 * @param m02
 * @param m10
 * @param m11
 * @param m12
 * @param m20
 * @param m21
 * @param m22
 * @return
 */
public Quaternion fromRotationMatrix(float m00, float m01, float m02,
        float m10, float m11, float m12, float m20, float m21, float m22) {
    // 先把矩阵的3个列向量规范化,避免缩放矩阵对四元数旋转的影响。
    float lengthSquared = m00 * m00 + m10 * m10 + m20 * m20;
    if (lengthSquared != 1f && lengthSquared != 0f) {
        lengthSquared = (float) (1.0 / Math.sqrt(lengthSquared));
        m00 *= lengthSquared;
        m10 *= lengthSquared;
        m20 *= lengthSquared;
    }
    lengthSquared = m01 * m01 + m11 * m11 + m21 * m21;
    if (lengthSquared != 1f && lengthSquared != 0f) {
        lengthSquared = (float) (1.0 / Math.sqrt(lengthSquared));
        m01 *= lengthSquared;
        m11 *= lengthSquared;
        m21 *= lengthSquared;
    }
    lengthSquared = m02 * m02 + m12 * m12 + m22 * m22;
    if (lengthSquared != 1f && lengthSquared != 0f) {
        lengthSquared = (float) (1.0 / Math.sqrt(lengthSquared));
        m02 *= lengthSquared;
        m12 *= lengthSquared;
        m22 *= lengthSquared;
    }

    // Use the Graphics Gems code, from 
    // ftp://ftp.cis.upenn.edu/pub/graphics/shoemake/quatut.ps.Z
    // *NOT* the "Matrix and Quaternions FAQ", which has errors!

    // the trace is the sum of the diagonal elements; see
    // http://mathworld.wolfram.com/MatrixTrace.html
    float t = m00 + m11 + m22;

    // we protect the division by s by ensuring that s>=1
    if (t >= 0) { // |w| >= .5
        float s = (float) Math.sqrt(t + 1); // |s|>=1 ...
        w = 0.5f * s;
        s = 0.5f / s;                 // so this division isn't bad
        x = (m21 - m12) * s;
        y = (m02 - m20) * s;
        z = (m10 - m01) * s;
    } else if ((m00 > m11) && (m00 > m22)) {
        float s = (float) Math.sqrt(1.0f + m00 - m11 - m22); // |s|>=1
        x = s * 0.5f; // |x| >= .5
        s = 0.5f / s;
        y = (m10 + m01) * s;
        z = (m02 + m20) * s;
        w = (m21 - m12) * s;
    } else if (m11 > m22) {
        float s = (float) Math.sqrt(1.0f + m11 - m00 - m22); // |s|>=1
        y = s * 0.5f; // |y| >= .5
        s = 0.5f / s;
        x = (m10 + m01) * s;
        z = (m21 + m12) * s;
        w = (m02 - m20) * s;
    } else {
        float s = (float) Math.sqrt(1.0f + m22 - m00 - m11); // |s|>=1
        z = s * 0.5f; // |z| >= .5
        s = 0.5f / s;
        x = (m02 + m20) * s;
        y = (m21 + m12) * s;
        w = (m10 - m01) * s;
    }
    return this;
}

三种变换的4x4矩阵表示

三种变换指的是平移变换、旋转变换、比例变换。现在我们可以在Matrix4f中实现这三种变换了。

平移变换

平移变换最简单,把矩阵第4列设为x、y、z即可。

/**
 * 设置平移变换
 * 
 * @param x
 * @param y
 * @param z
 */
public void setTranslation(float x, float y, float z) {
    m03 = x;
    m13 = y;
    m23 = z;
}

/**
 * 设置平移变换
 *
 * @param translation
 */
public void setTranslation(Vector3f translation) {
    m03 = translation.x;
    m13 = translation.y;
    m23 = translation.z;
}

/**
 * 获得平移变换向量
 * @return
 */
public Vector3f toTranslationVector() {
    return new Vector3f(m03, m13, m23);
}

/**
 * 获得平移变换向量
 * @param vector
 */
public void toTranslationVector(Vector3f vector) {
    vector.set(m03, m13, m23);
}

比例变换

在进行比例变换时,应该先清除原矩阵左上角3x3矩阵中的比例变换。具体做法依然是先规范化3个列向量,然后做一个乘法即可。

/**
 * 设置比例变换
 * 
 * @param x
 * @param y
 * @param z
 */
public void setScale(float x, float y, float z) {
    Vector3f tmp = new Vector3f();

    tmp.set(m00, m10, m20);
    tmp.normalizeLocal().multLocal(x);
    m00 = tmp.x;
    m10 = tmp.y;
    m20 = tmp.z;

    tmp.set(m01, m11, m21);
    tmp.normalizeLocal().multLocal(y);
    m01 = tmp.x;
    m11 = tmp.y;
    m21 = tmp.z;

    tmp.set(m02, m12, m22);
    tmp.normalizeLocal().multLocal(z);
    m02 = tmp.x;
    m12 = tmp.y;
    m22 = tmp.z;
}

/**
 * 设置比例变换
 * 
 * @param scale
 */
public void setScale(Vector3f scale) {
    this.setScale(scale.x, scale.y, scale.z);
}

获得比例变换,是通过对矩阵前3列分别求模来得到的。

/**
 * 设置比例变换向量
 * 
 * @param the
 */
public void toScaleVector(Vector3f vector) {
    float scaleX = (float) Math.sqrt(m00 * m00 + m10 * m10 + m20 * m20);
    float scaleY = (float) Math.sqrt(m01 * m01 + m11 * m11 + m21 * m21);
    float scaleZ = (float) Math.sqrt(m02 * m02 + m12 * m12 + m22 * m22);
    vector.set(scaleX, scaleY, scaleZ);
}

/**
 * 获得比例变换向量
 * 
 * @return
 */
public Vector3f toScaleVector() {
    Vector3f result = new Vector3f();
    this.toScaleVector(result);
    return result;
}

旋转变换

先把四元数转成3x3矩阵,再把这个矩阵复制到4x4矩阵的左上角。

/**
 * 设置旋转变换
 * @param quat
 */
public void setRotationQuaternion(Quaternion quat) {
    quat.toRotationMatrix(this);
}

获得旋转变换的3x3矩阵

/**
 * 获得旋转矩阵
 * @return
 */
public Matrix3f toRotationMatrix() {
    return new Matrix3f(m00, m01, m02, m10, m11, m12, m20, m21, m22);
}

/**
 * 获得旋转矩阵
 * @param mat
 */
public void toRotationMatrix(Matrix3f mat) {
    mat.m00 = m00;
    mat.m01 = m01;
    mat.m02 = m02;
    mat.m10 = m10;
    mat.m11 = m11;
    mat.m12 = m12;
    mat.m20 = m20;
    mat.m21 = m21;
    mat.m22 = m22;
}

获得四元数,先转成3x3矩阵,再把3x3矩阵转成四元数。

/**
 * 旋转矩阵转为四元数
 * @return
 */
public Quaternion toRotationQuat() {
    Quaternion quat = new Quaternion();
    quat.fromRotationMatrix(toRotationMatrix());
    return quat;
}

/**
 * 旋转矩阵转为四元数
 * @param q
 */
public void toRotationQuat(Quaternion q) {
    q.fromRotationMatrix(toRotationMatrix());
}

模型的空间变换

终于算写到这里了。

学习jMonkeyEngine,定义一个Transform类,用来记录物体的三种空间变换。

Transform的定义

package net.jmecn.math;

/**
 * 空间变换
 * @author yanmaoyuan
 *
 */
public class Transform {

    private Vector3f scale;// 比例变换
    private Quaternion rot;// 旋转变换
    private Vector3f translation;// 平移变换

    /**
     * 初始化空间变换
     */
    public Transform() {
        scale = new Vector3f(1, 1, 1);
        rot = new Quaternion(0, 0, 0, 1);
        translation = new Vector3f(0, 0, 0);
    }

    /**
     * 单位化
     */
    public void loadIdentity() {
        scale.set(1, 1, 1);
        rot.set(0, 0, 0, 1);
        translation.set(0, 0, 0);
    }
}

与矩阵的互相转化

这个类最重要的作用,是提供一个统一的空间变换方法,将三种变换与4x4矩阵互相转化。

/**
 * 三种变换转为4x4矩阵
 * @return
 */
public Matrix4f toTransformMatrix() {
    Matrix4f trans = new Matrix4f();
    trans.setTranslation(translation);
    trans.setRotationQuaternion(rot);
    trans.setScale(scale);
    return trans;
}

/**
 * 4x4矩阵转为三种变换
 * @param mat
 */
public void fromTransformMatrix(Matrix4f mat) {
    translation.set(mat.toTranslationVector());
    rot.set(mat.toRotationQuat());
    scale.set(mat.toScaleVector());
}

逆变换

通过计算逆矩阵,还可以求空间变换的逆。

/**
 * 求空间变换的逆
 * @return
 */
public Transform invert() {
    Transform t = new Transform();
    t.fromTransformMatrix(toTransformMatrix().invertLocal());
    return t;
}

变换顶点

作为“空间变换”,它当然也可以变换顶点。

/**
 * 对空间进行空间变换
 * @param in
 * @param store
 * @return
 */
public Vector3f transformVector(final Vector3f in, Vector3f store){
    if (store == null)
        store = new Vector3f();

    store.set(in);
    // 先缩放
    store.multLocal(scale);
    // 再旋转
    rot.mult(store, store);
    // 再平移
    store.addLocal(translation);
    return store;
}

同样也提供逆操作。

/**
 * 对顶点进行逆变换
 * @param in
 * @param store
 * @return
 */
public Vector3f transformInverseVector(final Vector3f in, Vector3f store){
    if (store == null)
        store = new Vector3f();

    // 先负平移
    in.subtract(translation, store);
    // 然后负旋转
    rot.inverse().mult(store, store);
    // 然后负缩放
    store.divideLocal(scale);

    return store;
}

合并变换

就如矩阵可以通过乘法来合并多个空间变换,Transform也有这个功能。关于它的计算方式,可以参考前文“平移变换”部分的内容。

坐标系在空间中的平移变换可以通过加上一个偏移向量实现,平移变换而不影响坐标轴的方向和比例,而且也不能表示成3x3矩阵的形式。考虑将一点 P 从一个坐标系变换到另一个坐标系中的一般表达式如下所示:

P' = MP + T

其中,矩阵M是一个可逆的3x3矩阵,矩阵T是一个3D平移向量。在该表达式中要执行两类矩阵运算(比例变换和旋转变换),最终得到一个繁琐的表达式:

P' = M2(M1P + T1) + T2
   = (M2M1)P + M2T1 + T2

在这个类中,比例变换和旋转变换都不是通过3x3矩阵来表示的,但计算的顺序并没有区别:先比例变换、再旋转变换、再平移变换。

代码实现如下:

/**
 * 根据“父”空间变换来计算当前空间变换。
 * 
 * @param parent
 * @return
 */
public Transform combineWithParent(Transform parent) {
    scale.multLocal(parent.scale);
    parent.rot.mult(rot, rot);

    translation.multLocal(parent.scale);// 缩放
    parent.rot.multLocal(translation)   // 旋转
        .addLocal(parent.translation);  // 平移
    return this;
}

这个方法与使用矩阵乘法的结果是等价的。

插值

既然Vector3f和Quaternion都可以插值,那么Transform当然也可以插值。

/**
 * 在两个空间变换之间插值
 * @param t1
 * @param t2
 * @param delta
 */
public void interpolateTransforms(Transform t1, Transform t2, float delta) {
    this.rot.slerp(t1.rot,t2.rot,delta);
    this.translation.interpolateLocal(t1.translation,t2.translation,delta);
    this.scale.interpolateLocal(t1.scale,t2.scale,delta);
}

其他

除了上述方法,剩下的主要是各种方便的构造方法。

/**
 * 初始化空间变换
 */
public Transform(Vector3f translation, Quaternion rot){
    this.translation.set(translation);
    this.rot.set(rot);
}

public Transform(Vector3f translation, Quaternion rot, Vector3f scale){
    this(translation, rot);
    this.scale.set(scale);
}

public Transform(Vector3f translation){
    this(translation, Quaternion.IDENTITY);
}

public Transform(Quaternion rot){
    this(Vector3f.ZERO, rot);
}

public Transform(){
    this(Vector3f.ZERO, Quaternion.IDENTITY);
}

以及各种getter、setter

public Transform set(Transform matrixQuat) {
    this.translation.set(matrixQuat.translation);
    this.rot.set(matrixQuat.rot);
    this.scale.set(matrixQuat.scale);
    return this;
}

public Transform setTranslation(Vector3f trans) {
    this.translation.set(trans);
    return this;
}

public Transform setTranslation(float x,float y, float z) {
    translation.set(x,y,z);
    return this;
}

public Vector3f getTranslation(Vector3f trans) {
    if (trans==null)
        trans=new Vector3f();
    trans.set(this.translation);
    return trans;
}

public Vector3f getTranslation() {
    return translation;
}

public Transform setScale(Vector3f scale) {
    this.scale.set(scale);
    return this;
}

public Transform setScale(float x, float y, float z) {
    scale.set(x,y,z);
    return this;
}

public Transform setScale(float scale) {
    this.scale.set(scale, scale, scale);
    return this;
}

public Vector3f getScale(Vector3f scale) {
    if (scale==null)
        scale=new Vector3f();
    scale.set(this.scale);
    return scale;
}

public Vector3f getScale() {
    return scale;
}

public Transform setRotation(Quaternion rot) {
    this.rot.set(rot);
    return this;
}

public Quaternion getRotation(Quaternion quat) {
    if (quat==null)
        quat=new Quaternion();
    quat.set(rot);
    return quat;
}

public Quaternion getRotation() {
    return rot;
}

总结

这部分真难。。如果没有jMonkeyEngine的源代码和两本专业书的支持,我恐怕很难写完。

不过,数学库总算是完成了。