Chapter 4: Implementing Quaternions

其实很多人物的动画里,只有rotation,没有平移或者scale上的变化。

Most humanoid animations are created using only rotations—no translation or scale is needed. Think about an elbow joint, for example. The natural motion of an elbow only rotates. If you want to translate the elbow through space, you rotate the shoulder. Quaternions encode rotations and they interpolate well.

更多数学上的四元数可以参考这里

四元数的union有三种表示方法:

  • xyzw
  • struct { vec3 vector; float scalar; };
  • float*

代码如下:

struct quat
{union{struct{float x;float y;float z;float w;};struct{vec3 vector;float scalar;};float v[4];};inline quat() :x(0), y(0), z(0), w(1) { }inline quat(float _x, float _y, float _z, float _w) :x(_x), y(_y), z(_z), w(_w) {}
};

Quaternion绕轴旋转角度要除以2的原因

A rotation about an axis by θ can be represented on a sphere as any directed arc whose length is on the plane perpendicular to the rotation axis. Why θ/2? A quaternion can track two full rotations, which is 720 degrees. This makes the period of a quaternion 720 degrees. The period of sin/cos is 360 degrees. Dividing θ by 2 maps the range of a quaternion to the range of sin/cos.

大概意思是四元数的周期是720度,而三角函数的周期是360度,为了合理的映射,需要除以2,更深入的原因我就不知道了。

Angle axis

创建一个函数,根据输入的轴和旋转角返回对应的Quaternion,之前研究过这个,不多说了:

quat angleAxis(float angle, const vec3& axis)
{vec3 norm = normalized(axis);float s = sinf(angle * 0.5f);return quat(norm.x * s,norm.y * s,norm.z * s,cosf(angle * 0.5f));
}

Creating rotations from one vector to another

计算向量A到向量B的四元数时,默认向量都是起点位于原点。重点在于,从向量A旋转到向量B,这个旋转用AxisAngle来表示,其旋转轴就是两个向量的叉乘的结果

To find the axis of rotation, normalize the input vectors. Find the cross product of the input vectors. This is the axis of rotation.

代码如下:

quat fromTo(const vec3& from, const vec3& to){// 保证输入的两个向量是归一化的vec3 f = normalized(from);vec3 t = normalized(to); // 两个向量相同,则不作任何旋转if (f == t) return quat();// If this edge case happens, find the most perpendicular vector between// the two vectors to create a pure quaternion.// 两个向量反向时,相当于只有一个向量,这里会自己取一个新的向量// If they are opposite vectors, the most orthogonal axis of // the from vector can be used to create a pure quaternion// 然后计算叉乘, 得到旋转轴else if (f == t * -1.0f){// 取一个正交向量vec3 ortho = vec3(1, 0, 0);// ???除了说防止ortho和f重合以外,这样做还有啥原因remainif (fabsf(f.y) && fabs(f.z) < fabsf(f.x))      ortho = vec3(0, 0, 1);// 基于from和找的正交基(好像是随便找的)进行叉乘 vec3 axis = normalized(cross(f, ortho));// 注意,两个向量反向时,返回的是Pure Quaternionreturn quat(axis.x, axis.y, axis.z, 0); }}// 算出f到t的半程向量vec3 half = normalized(f + t);// 计算叉乘时,结果向量的方向由f和t组成的平面决定,而具体向量的大小由夹角决定和两个Input的向量的长度决定// 算出此时的旋转轴,这样做结果就直接有sin(θ/2), 就避免了复杂的反三角函数的运算了vec3 axis = cross(f, half);// axis * sin(θ/2)return    quat(axis.x, axis.y, axis.z, dot(f, half));
}

Retrieving quaternion data

这个也比较简单,就是反向从四元数里获取其旋转轴和旋转角度,代码如下:

vec3 getAxis(const quat& quat)
{// quat的xyz其实是sin(θ/2) * axis, // 但归一化之后这个sin(θ/2)就会被消除了return normalized(vec3(quat.x, quat.y, quat.z));
}float getAngle(const quat& quat)
{// 直接取arccos, 乘以2就行了return 2.0f * acosf(quat.w);
}

常规的四元数操作

类比于vec3,四元数也有Component-wise Operations,比如加、减、乘和取负值,代码如下所示:

// 加减操作其实就是简单的四个值的加减
quat operator+(const quat& a, const quat& b)
{return quat(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w);
}quat operator-(const quat& a, const quat& b)
{return quat(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w);
}// 四元数与float的乘积
quat operator*(const quat& a, float b)
{return quat(a.x * b, a.y * b, a.z * b, a.w * b);
}// 四元数取负就是四个值都取负
quat operator-(const quat& q)
{return quat(-q.x, -q.y, -q.z, -q.w);
}

四元数的比较

四元数的比较与vec3的比较不完全相同,因为即使是两个值不同的四元数,也可能代表相同的旋转,因为一个旋转,可以正着转,也可以反着转,最终都能得到相同的结果,只是二者的旋转路径不同罢了,这里判断四元数相等,还是通过判断四个值是否完全相同,这样的component-wise操作来判断的:

// 注意这里用的是四元数的QUAT_EPSILON, 而不是之前的EPSILON
bool operator==(const quat& left, const quat& right) {return (fabsf(left.x - right.x) <= QUAT_EPSILON && fabsf(left.y - right.y) <= QUAT_EPSILON && fabsf(left.z - right.z) <= QUAT_EPSILON && fabsf(left.w - left.w) <= QUAT_EPSILON);
}bool operator!=(const quat& a, const quat& b) {return !(a == b);
}

但它额外提供了一个特殊的函数,用于判断两个四元数是否代表相同的Rotation,代码如下:

bool sameOrientation(const quat& left, const quat& right)
{// 如果两个四元数的xyzw完全相同, 返回truereturn (fabsf(left.x - right.x) <= QUAT_EPSILON && fabsf(left.y - right.y) <= QUAT_EPSILON && fabsf(left.z - right.z) <= QUAT_EPSILON && fabsf(left.w - left.w) <= QUAT_EPSILON)// 或者两个四元数的xyzw均各自互为相反数, 返回true|| (fabsf(left.x + right.x) <= QUAT_EPSILON && fabsf(left.y + right.y) <= QUAT_EPSILON && fabsf(left.z + right.z) <= QUAT_EPSILON && fabsf(left.w + left.w) <= QUAT_EPSILON);
}

注意,绝大多数时候,还是需要用==!=来判断四元数的相等的,because the rotation that a quaternion takes can be changed if the quaternion is inverted.

四元数的点积

向量之间的点积是为了评价向量之间的相似程度,四元数之间的点积也是为了评价四元数之间的相似程度,具体的实现方法也与向量点乘类似,跟vec4的点积是一样的,代码如下:

float dot(const quat& a, const quat& b) {return a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w;
}

四元数的长度和sqrLength

与向量一样,四元数的长度的平方,就是它与自己的点积,四元数的长度,就是其平方根,其实是跟vec4是一样的,代码如下:

float lenSq(const quat& q) {return q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w;
}float len(const quat& q) {float lenSq = q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w;if (lenSq < QUAT_EPSILON) {return 0.0f;}return sqrtf(lenSq);
}

Unit Quaternions

Normalized quaternions represent only a rotation and non-normalized quaternions introduce a skew(误差).

用于表示旋转的四元数,其长度必须为1,而非归一化的四元数,会引起误差,所以动画里用到的四元数,用于旋转时都应该是归一化的,代码很简单,不多说:

void normalize(quat& q) {float lenSq = q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w;if (lenSq < QUAT_EPSILON) {return;}float i_len = 1.0f / sqrtf(lenSq);q.x *= i_len;q.y *= i_len;q.z *= i_len;q.w *= i_len;
}quat normalized(const quat& q) {float lenSq = q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w;if (lenSq < QUAT_EPSILON) {return quat();}float i_len = 1.0f / sqrtf(lenSq);return quat(q.x * i_len,q.y * i_len,q.z * i_len,q.w * i_len);
}

Conjugate(共轭) and Inverse

归一化的四元数的共轭就是它的逆,四元数的共轭会翻转它的旋转轴,代码如下:

// 直接旋转轴取负, 就是其共轭四元数
quat conjugate(const quat& q) {return quat(-q.x, -q.y, -q.z, q.w);
}// 如果已经知道q是归一化的了, 那么可以直接调用conjugate函数, 代替这个函数
quat inverse(const quat& q) {// 归一化的四元数的共轭就是它的逆// 但是q可能不是归一化的, 所以可以先求共轭, 再归一化float lenSq = q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w;if (lenSq < QUAT_EPSILON)return quat();float recip = 1.0f / lenSq;// conjugate / normreturn quat(-q.x * recip, -q.y * recip, -q.z * recip, q.w * recip);
}

不同于矩阵,这里的四元数的逆应该只是单纯的代表旋转的逆向而已(感觉不涉及到矩阵、列向量什么的,因为只有方阵才可以有逆矩阵的)

Multiplying quaternion(不是点积)

四元数的乘法与矩阵的乘法类似,也是从右往左结合的,假设有两个四元数pq,表示为:

可以直接把这两个四元数相乘,利用乘法分配率,得到:

根据ijk = -1,可推断出来i = j = k =ijk,比如ijk = -1两边同时左边乘以-i,最后的结果为:

写成代码:

quat operator*(const quat& Q1, const quat& Q2)
{return quat(Q2.x * Q1.w + Q2.y * Q1.z - Q2.z * Q1.y + Q2.w * Q1.x,-Q2.x * Q1.z + Q2.y * Q1.w + Q2.z * Q1.x + Q2.w * Q1.y,Q2.x * Q1.y - Q2.y * Q1.x + Q2.z * Q1.w + Q2.w * Q1.z,-Q2.x * Q1.x - Q2.y * Q1.y - Q2.z * Q1.z + Q2.w * Q1.w// 三个xyz各自相乘, 取负, 然后加上w的平方);
}

额外注意一下两个四元数相乘的结果,发现得到的w值是由一个正的component和三个负的component组成,有:

而其他三项,都是三个正的component和一个负的component:

再来观察一下,得到的四元数的xyz分量的最后两列,可以先看看之前写的vec3的叉乘公式:

// 叉乘, 好像这里是左手坐标系
vec3 cross(const vec3 &l, const vec3 &r)
{return vec3(l.y * r.z - l.z * r.y,l.z * r.x - l.x * r.z,l.x * r.y - l.y * r.x);
}

可以发现,四元数的相乘,上面的xyz分量后两列的值,其实就是两个四元数的轴向量的叉乘的结果,而前两列的值,比如px*qw + pw * qx就是把二者的w用对方的x来相乘,计算和,对于第四行,也就是计算得到的四元数的w的这行,可以看到,值为两个w的积,减去两个quaternion的vector部分的点乘。

基于这个规律,可以简化两个四元数的乘法的代码:

// 这是另外一种的四元数乘法的函数写法, 但由于涉及到函数调用, 它没有之前的函数直接写结果效率高
quat operator*(const quat& Q1, const quat& Q2)
{quat result;// w部分, 等于Q1.w * Q2.w - dot(Q1.xyz, Q2.xyz)result.scalar = Q2.scalar * Q1.scalar - dot(Q2.vector, Q1.vector);// vector部分, 等于两个部分详解// 第一部分是, Q1.w和Q2.w分别乘以vector部分// 第二部分是, 两个四元数的vector部分的叉乘result.vector = (Q1.vector * Q2.scalar) + (Q2.vector * Q1.scalar)+ cross(Q2.vector, Q1.vector);// 注意是Q2.vector在前(好像是左手坐标系)return result;
}

Transforming vectors

为了将四元数作用于vector,首先要把四元数转换成一个纯四元数(Pure Quaternion),纯四元数的w值为0,vector部分是归一化的,假设得到的纯四元数为v’,则此时的旋转需要在左边乘以四元数,右边乘以四元数的逆,公式如下:

得到的结果仍然是一个纯四元数(w为0),其vector部分包含了旋转轴(The result of this multiplication is a pure quaternion whose vector part contains the rotated vector.) 这里让人费解的是,为什么左边和右边都要乘以四元数和四元数的逆?

这里的解释是,向量左乘q得到的旋转效果,是q本身代表的旋转的两倍,后面再乘以q的逆,才可以把vector带回到正确的范围内(感觉等于没说)。这个公式的推导,Remains,也不是本书讨论的范围

Multiplying by q will rotate the vector twice as much as the rotation of q. Multiplying by q-1 brings the vector back into the expected range. This formula can be simplified further.

这里给一个等价的公式,如下所示,qv代表四元数的vector部分,qs代表标量部分,也就是w:

对应的代码如下:

// 这种写法比算qvq-1更高效
vec3 operator*(const quat& q, const vec3& v)
{return q.vector * 2.0f * dot(q.vector, v)+ v * (q.scalar * q.scalar - dot(q.vector, q.vector)) + cross(q.vector, v) * 2.0f * q.scalar;
}

Interpolating quaternions

四元数插值与vector插值差不多,一般用于动画的关键帧之间的插值,也分为:

  • lerp
  • nlerp
  • slerp
  • nslerp
    后面再细说

Neighborhood

A quaternion represents a rotation, not an orientation. Rotating from one part of a sphere to another can be achieved by one of two rotations. The rotation can take the shortest or the longest arc.

从球面上的A点旋转到B点,可以用两种四元数表示,一种是最短的旋转,一种是最长的旋转(为什么只能是两种,我不是特别清楚),而在四元数的插值过程中,直接计算的Delta Rotaion对应的旋转到底是哪一种,如何选择最短的插值路径,这个问题叫做neighborhooding

具体的选择方法是,计算两个被插值的四元数的点乘,若结果为正,则插值会选择shorted arc的最短路径,否则插值的结果会选择最长的旋转路径。在实际的四元数插值的时候,先要计算两个被插值的四元数的点乘结果的正负号,如果值为正的,那么直接插值就行了,如果值为负的,那么要把任意一个四元数取负,代码参考如下:

quat SampleFunction(const quat& a, const quat& b)
{// 把第二个四元数取负if (dot(a, b) < 0.0f) {b = -b;}return slerp(a, b, 0.5f);
}

You only need to neighborhood quaternions when interpolating between them

四元数的lerp函数

lerp指的是linear interpolation,由于四元数里不是线性的,而是arc的,所以这里设计的函数不叫lerp,叫mix函数,但是本质上还是两个四元数代表的vec4的lerp函数,代码如下:

// 在使用此函数之前, 需要保证
quat mix(const quat& from, const quat& to, float t)
{return from * (1.0f - t) + to * t;// 其实就是vec4的线性组合
}

四元数的nlerp函数

nlerp就是把lerp得到的结果归一化而已,没什么特别的:

quat nlerp(const quat& from, const quat& to, float t)
{return normalized(from + (to - from) * t);
}

四元数的slerp函数

slerp should only be used if consistent velocity is required,绝大多数情况下使用nlerp就行,Depending on the interpolation step size, slerp may end up falling back to nlerp anyway.

要计算两个四元数的球形插值,可以首先计算其delta rotation,然后调整旋转的角度,与起始的rotation算到一起,得到插值的四元数。

How can the angle of a quaternion be adjusted? To adjust the angle of a quaternion, raise it to the desired power. For example, to adjust the quaternion to only rotate halfway, you would raise it to the power of 0.5.

四元数的Power函数
这里的术语叫raise a quaternion to some power,这项操作需要一个四元数被解析为轴向角的方式,假设power对应的指数为t,公式为:

感觉这里叫power很奇怪,因为只是把角度乘以了一个t而已,不知道为啥要叫power,这又不是一个幂指数的操作,相关代码如下:

quat operator^(const quat& q, float f)
{// 解析成轴向角float angle = 2.0f * acosf(q.scalar);vec3 axis = normalized(q.vector);float halfCos = cosf(f * angle * 0.5f);float halfSin = sinf(f * angle * 0.5f);return quat(axis.x * halfSin,axis.y * halfSin,axis.z * halfSin,halfCos);
}

Implementing slerp
注意,当插值的两个四元数非常接近的时候,使用slerp可能会出现预料之外的结果,此时会使用nlerp代替slerp(这一点在vec3类里的slerp函数也是这样的)。

计算slerp时,同样需要确保两个四元数的点乘值为正, This delta quaternion is the interpolation path,代码如下:

quat slerp(const quat& start, const quat& end, float t)
{// 使用点积计算两个四元数的相似程度(应该要保证输入单位四元数吧),其实就是计算两个vec4的点积if (fabsf(dot(start, end)) > 1.0f - QUAT_EPSILON)return nlerp(start, end, t);// 这里的start其实是归一化的四元数, 可以用conjugate代替inverse函数quat delta = inverse(start) * end;// 为啥是这样啊,这样是start * delta. = end了,这是右乘return normalized((delta ^ t) * start);
}

Look rotation

此函数与LookAt函数类似,接受两个参数:

  1. Direction,即看向的方向
  2. Up,即看向的方向的上方方向

根据这两个参数,可以创建对应的四元数,这个四元数会把单位旋转变换到对应的朝向,为了不跟矩阵的LookAt函数弄混,这里的函数叫Look Rotation,准确的来说,是把一个单位旋转,即foward为(0,0,1),up为(0,1,0),right为(1,0,0)代表的Orientation,旋转到目标方向。

注意,我一开始以为只要获取新的orientation的forward,然后直接算fromTo((0,0,1), newForward)就行了,这是不对的,这么算只能保证旋转之后的forward是对的,但是up就不一定对了,只要forward和up对了,right自然也就对了,因为是叉乘得到的。

实际的做法是:

  1. 基于两个参数,计算出新的坐标基,即newRight、newUp和newForward
  2. 计算fromTo((0,0,1), newForward),得到四元数q1,这点不变
  3. 利用vec3 objectUp =q1 * (0,1,0),得到通过第一步计算得到的新的objectUp,此时的objectUp不一定是跟newForward正交的,所以,还要计算计算q2 = fromTo(objectUp, newUp),得到四元数q2
  4. 把两个四元数相乘,得到q1q2就行了

代码如下:

// 输入为目标的orientation, 输入的方式跟设置摄像机的朝向的方式差不多
// 注意这个up是世界坐标系的Up, 只是为了帮助构建正交基, 不代表最终的up
quat lookRotation(const vec3& direction, const vec3& up)
{// 基于输入, 创建目标orientation对应的三个正交基, 也就是三个local轴, 这点跟创建View矩阵差不多vec3 f = normalized(direction); // Object Forwardvec3 u = normalized(up); // Desired Upvec3 r = cross(u, f); // Object Rightu = cross(f, r); // Object Up// deltaRotation只需要算一个forward向量的前后delta就行了// From world forward to object forwardquat worldToObject = fromTo(vec3(0, 0, 1), f);//计算q1// 根据计算的deltaRot, 计算其它的local轴// 算出local upvec3 objectUp = worldToObject * vec3(0, 1, 0);// From object up to desired upquat u2u = fromTo(objectUp, u);//计算q2// Rotate to forward direction first// then twist to correct upquat result = worldToObject * u2u;// Don't forget to normalize the resultreturn normalized(result);
}

四元数与旋转矩阵的互换

四元数转旋转矩阵很简单,只需要用四元数作用于,三个世界坐标系的坐标基向量即可,由于这里的四元数作用于向量,是定义的左乘的*运算符重载,所以代码如下:

mat4 quatToMat4(const quat& q)
{vec3 r = q * vec3(1, 0, 0);vec3 u = q * vec3(0, 1, 0);vec3 f = q * vec3(0, 0, 1);return mat4(r.x, r.y, r.z, 0,u.x, u.y, u.z, 0,f.x, f.y, f.z, 0,0 , 0 , 0 , 1);
}

旋转矩阵转四元数,就看矩阵的3*3的子矩阵部分,由于这一部分既包括了Rotation,也包括了Scale,所以要把每列归一化,代码如下:

quat mat4ToQuat(const mat4& m)
{// 三列的vector都要归一化vec3 up = normalized(vec3(m.up.x, m.up.y,m.up.z));// 只有这里的forward是归一化之后就不变的, 其他的都要重新算, 这样做是为了保证坐标基是正交的vec3 forward = normalized(vec3(m.forward.x, m.forward.y, m.forward.z));// the cross product needs to be used to make sure that the resulting vectors are orthogonal.vec3 right = cross(up, forward);up = cross(forward, right);return lookRotation(forward, up);
}

这里有个问题,难道旋转矩阵里面三列对应的vector,再归一化后的值,它们之间不是正交的吗?会不会存在矩阵,它的旋转矩阵是无效的,比如说它的行列式为0的时候,还有个疑问,是不是正常的Transform矩阵的,那三列的vector都是互相正交的?感觉涉及到很多数学,remain问题


Chapter 5: Implementing Transforms

In this chapter, you will implement a structure that holds position, rotation, and scale data. This structure is a transform. A transform maps from one space to another space.

这里有个疑问,为什么不用4×4矩阵来记录rotation、position和scale数据,而是要用一个transform的结构体来表示呢?

这样做,是为了插值(interpolation),Matrices don't interpolate well, but transform structures do,矩阵之间是很难插值的,尤其是因为4×4的矩阵的左上角的3×3的子矩阵既包含了rotation信息,也包含了scale信息,如果直接进行插值的话,值是不对的,而Transform就把这三个数据分开了,这样更适合使用。

本章的重点:

  • Understand how to combine transforms
  • Convert between transforms and matrices
  • Understand how to apply transforms to points and vectors

创建Transform类

这是个简单的类,代码如下:

// Unity里的Transform还有一些Parent相关的父子引用的数据, 这里的Tranform没有定义这些内容
struct Transform
{vec3 position;quat rotation;vec3 scale;Transform() : position(vec3(0, 0, 0)), rotation(quat(0, 0, 0, 1)),scale(vec3(1, 1, 1)){}Transform(const vec3& p, const quat& r, const vec3& s) :position(p), rotation(r),scale(s) {}
};

Combining transforms

transform的结合顺序跟矩阵一样,也是从右到左,但是这里的结合操作,只会用一个Combine函数来表示,不会重载运算符*

如果说一个Transform单纯只有Rotation或者Scale数据,那么二者的组合只需要把各个数据相乘就可以了,但涉及到Translation,就不一样了,这里的原则是:先算scale,再选rotation,最后算translation。

比如说A和B两个Transform结合,那么得到的新的Transform的Rotation和Scale部分就是两个Transform的各个部分的乘积,但是position就是要算出B在A的rotation和scale作用下的新的pos,再加上a原本的基础pos,代码如下:

// a在左边, b在右边
Transform combine(const Transform& a, const Transform& b)
{Transform out;// scale和rotation直接组合out.scale = a.scale * b.scale;out.rotation = b.rotation * a.rotation;// b相当于是在a的localSpace下的, 其position需要基于a的rotation和scaleout.position = a.rotation * (a.scale * b.position);// 最后加上a的基础positionout.position = a.position + out.position;return out;
}

Inverting transforms

a transform maps from one space into another space

inverse的时候,scale和rotation两个部分直接取逆就可以了,rotation是四元数,直接取四元数的逆,而scale就取每个部分的倒数就可以了,不过要注意当scale为0的时候,其倒数也应该是0。特殊的是position部分的取逆,因为Position是基于rotation和scale的,此时的position也要取负,所以是算出scale,再乘以rotation,代码如下

Transform inverse(const Transform& t)
{Transform inv;inv.rotation = inverse(t.rotation);inv.scale.x = fabs(t.scale.x) < VEC3_EPSILON ? 0.0f : 1.0f / t.scale.x;inv.scale.y = fabs(t.scale.y) < VEC3_EPSILON ? 0.0f : 1.0f / t.scale.y;inv.scale.z = fabs(t.scale.z) <    VEC3_EPSILON ? 0.0f : 1.0f / t.scale.z;// position的inverse需要结合rotation的inversevec3 invTrans = t.position * -1.0f;inv.position = inv.rotation * (inv.scale * invTrans);return inv;
}

Transform的三个组件,是不是与Transform的逆的三个组件,各自互为inverse呢?
从上面写的可以看出来,Scale和Rotation是的,但是两个Position的和却不为Vector.Zero。

计算Transform的逆,可以用于把移动后的物体还原。

Mix transforms

其实就是Tranform的插值,前面也提到过,用Transform而不是矩阵来表示物体的位置、旋转等信息,是为了方便插值。比如说两个关键帧的joint位于不同的Transform,那么之间的帧就需要用到Transform的插值。

不过这里的操作不叫插值,而叫blend或mix,其实就是三个部分分别进行线性插值,代码如下:

Transform mix(const Transform& a,const Transform& b,float t)
{// 保证四元数的neighbourhoodquat bRot = b.rotation;if (dot(a.rotation, bRot) < 0.0f)bRot = -bRot;return Transform(lerp(a.position, b.position, t),nlerp(a.rotation, bRot, t),lerp(a.scale, b.scale, t));
}

Converting transforms to matrices

主要用于Shader传递数据,一般Shader里不会有Transform这个概念,不过也可以在GLSL里定义一个Transform的struct,但是这样不太好,更好的做法是,把transform转化为matrix,然后把它作为uniform传给shader。

步骤很简单,首先找到根据rotation信息,计算出矩阵的三个basis vector,其实跟四元数转旋转矩阵的步骤是一样的,用四元数去分别乘以世界矩阵的坐标基(1,0,0)(0,1,0)(0,0,1)即可。在旋转矩阵的3*3的子矩阵里,第一列是right,第二列是up,第三列是forward(0,0,1),代码如下:

mat4 transformToMat4(const Transform& t)
{// 1. 直接用四元数乘以世界坐标系的三个坐标基vec3 x = t.rotation * vec3(1, 0, 0);vec3 y = t.rotation * vec3(0, 1, 0);vec3 z = t.rotation * vec3(0, 0, 1);// 2. 新的坐标基各自乘以对应scale的值x = x * t.scale.x;y = y * t.scale.y;z = z * t.scale.z;// 3. 位移直接提取就行了, 放到矩阵的第四列vec3 p = t.position;// Create matrixreturn mat4(x.x, x.y, x.z, 0, // X basis (&Scale)y.x, y.y, y.z, 0, // Y basis (&scale)z.x, z.y, z.z, 0, // Z basis (& scale)p.x, p.y, p.z, 1 // Position);
}

Converting matrices into transforms

参考:https://math.stackexchange.com/questions/237369/given-this-transformation-matrix-how-do-i-decompose-it-into-translation-rotati/417813
https://answers.unity.com/questions/402280/how-to-decompose-a-trs-matrix.html

这个函数我在网上看到了不同的写法,所以在写这个功能时总结几点:

  1. 不是所有的4*4矩阵都可以被分解为transforms
  2. translation信息就在矩阵第四列,这点都是一样的
  3. 这个函数可以有多种写法,一种是套公式直接写结果,这样最快,但是不容易理解,另外一种就是带着理解的方法去写代码
  4. 有的函数会有问题,当Scale有值为负值时,它无法提取出正确的Scale向量,但是有点写法没考虑过这个问题

在看书之前,我的思路是这样的:
根据上个过程(Converting transforms to matrices)反推即可,position可以直接提取矩阵的第四列,是最简单的。scale是各个basis归一化后的值,也就是向量的模,归一化后的结果,就是quaterion应用在世界坐标系的三个向量基后的新向量基。这里就是求rotation稍微麻烦一点,可以结合上一章学的LookRotation函数,新的direction为矩阵第三列归一化后的结果,

可以看看几个版本的写法:

Unity的版本
我发现Unity里的版本跟我上面想的是一模一样的:

// 把矩阵m改为一个Transform// 创建Transform的localPosition
Vector3 position = m.GetColumn(3);// 获取第四列作为Position// 创建Transform的localRotation
// 矩阵的第一列是right, 第二列是up, 第三列是forward, 这里是直接取的forward作为direction, up作为up
Quaternion rotation = Quaternion.LookRotation(m.GetColumn(2),m.GetColumn(1)
);// 创建Transform的localScale
Vector3 scale = new Vector3(m.GetColumn(0).magnitude,// 提取每个Basix Vector的模m.GetColumn(1).magnitude,m.GetColumn(2).magnitude
);

书中的写法
回忆一下代表Transform的矩阵,它其实是Scale、Rotation和Translation的三个分矩阵的组合,有:

M = TRS(从右往左结合)

代码如下:

// 把这个矩阵的scale, rotation和position信息提取出来
Transform mat4ToTransform(const mat4& m)
{Transform out;// 取第四列作为posout.position = vec3(m.v[12], m.v[13], m.v[14]);// 第四章写过mat4ToQuat这个函数, 就是把mat的basis vector归一化, 然后重新叉乘得到// 调用quaternion.cpp里的函数, 提取出rotationout.rotation = mat4ToQuat(m);// 算出R// 只取3*3的子矩阵M, M = S*Rmat4 rotScaleMat(m.v[0], m.v[1], m.v[2], 0,       // 第一列的列向量m.v[4], m.v[5], m.v[6], 0,                    // 第二列的列向量m.v[8], m.v[9], m.v[10], 0,                   // 第三列的列向量0, 0, 0, 1);// 计算R^-1mat4 invRotMat = quatToMat4(inverse(out.rotation));// M = S*R => S = M * R^-1mat4 scaleSkewMat = rotScaleMat * invRotMat;out.scale = vec3(scaleSkewMat.v[0],scaleSkewMat.v[5],scaleSkewMat.v[10]);return out;
}

一点疑问
其实书中写的不是M=TRS,而是M=SRT,他写的代码如下,顺序正好跟我是反的,我怀疑是他写错了:

// 把这个矩阵的scale, rotation和position信息提取出来
Transform mat4ToTransform(const mat4& m)
{Transform out;// 取第四列作为posout.position = vec3(m.v[12], m.v[13], m.v[14]);// 第四章写过mat4ToQuat这个函数, 就是把mat的basis vector归一化, 然后重新叉乘得到// 调用quaternion.cpp里的函数, 提取出rotationout.rotation = mat4ToQuat(m);// 算出R// 只取3*3的子矩阵M, M = R*Smat4 rotScaleMat(m.v[0], m.v[1], m.v[2], 0,       // 第一列的列向量m.v[4], m.v[5], m.v[6], 0,                    // 第二列的列向量m.v[8], m.v[9], m.v[10], 0,                   // 第三列的列向量0, 0, 0, 1);// 计算R^-1mat4 invRotMat = quatToMat4(inverse(out.rotation));// 算出R-1// M = R*S => S = R^-1 * Mmat4 scaleSkewMat = rotScaleMat * invRotMat;out.scale = vec3(scaleSkewMat.v[0],scaleSkewMat.v[5],scaleSkewMat.v[10]);return out;
}

这个功能主要是为了让程序更加robust,因为有的文件格式,比如glTF,既可以用Transform的形式存储数据,也可以用矩阵的方式存储数据

Transforming points and vectors

Using a transform to modify points and vectors is like combining two transforms.
将transform应用到点和向量上,跟把matrix应用到点和向量上本质是一样的,transform分为三个部分,应用到点和向量上时,Apply的顺序是: Scale -> Rotation -> Translation

代码如下:

// 感觉像是把b这个点放到a的LocalSpace下
vec3 transformPoint(const Transform& a, const vec3& b)
{vec3 out;out = a.rotation * (a.scale * b);// LocalPosition需要带上LocalScale和LocalRotationout = a.position + out;// 加上基础的Positionreturn out;
}// 注意: transformVector与transformPoint不一样,transformVector没有平移操作,它没有位置这个概念
vec3 transformVector(const Transform& a, const vec3& b)
{vec3 out;out = a.rotation * (a.scale * b);return out;
}

Hands-on C++ Game Animation Programming阅读笔记(三)相关推荐

  1. 信用卡葵花宝典 阅读笔记(三)

    <信用卡葵花宝典>第三篇阅读笔记是关于收单业务的基础知识以及风险管理.银行卡业务从大的概念上可以分为发卡业务和收单业务.收单业务通过为商户提供银行卡支付结算服务来获取商户回佣收入,同时通过 ...

  2. Expert C Programming 阅读笔记(~CH1)

    P4: 好梗! There is one other convention-sometimes we repeat a key point to emphasize it. In addition, ...

  3. Expert C Programming 阅读笔记(CH2)

    P33     Bugs are by far the largest and most successful class of entity, with nearly a million known ...

  4. 有效用例模式阅读笔记三

    第五章 用例 5.1 CompelteSingleGole 不适当的目标,会使编写人员不能确定什么时候一个用例结束,什么时候另一个用例开始. 原因: 太大的用例可能会因细节过多占去涉众的大部分精力: ...

  5. 软件需求模式阅读笔记三

    阅读的章节是基础需求模式和信息模式.从现在开始,到了本书的重点,介绍了多种需求模式. 基础需求模式:其中包括系统间接口需求模式,系统间交互需求模式,技术需求模式,遵从标准需求模式,参考需求需求模式和文 ...

  6. 编程修养 阅读笔记三

    转载:http://blog.csdn.net/haoel/article/details/2872 16.把相同或近乎相同的代码形成函数和宏 --------------------- 有人说,最好 ...

  7. 《掌握需求过程》阅读笔记三

    11月底了,这一本书又结束了,还剩一本就寒假了,这学期太快了.<掌握需求过程>这本书真的挺好的,对课程很有帮助. 第六章,功能性需求,功能性需求指的是: 1.场频功能的规格说明: 2.产品 ...

  8. javascript 高级程序设计(第4版)阅读笔记(三)

    第3章,内容很长,所以更得慢,主要讲的是ECMAScript   es的语言基础:语法.数据类型.基本操作符.流控制语句.理解函数,ECMAScript 的语法很大程度上借鉴了 C 语言和其他类 C  ...

  9. Kaiming He论文阅读笔记三——Simple Siamese Representation Learning

    Kaiming He大神在2021年发表的Exploring Simple Siamese Representation Learning,截至目前已经有963的引用,今天我们就一起来阅读一下这篇自监 ...

最新文章

  1. 飓风“桑迪”路径图的制作
  2. HDU 2955 Robberies
  3. 关于equals与hashcode的重写
  4. linux 错误 ttyname failed: Inappropriate ioctl for device 解决方法
  5. Python + HTMLTestRunner + smtplib 完成测试报告生成及发送测试报告邮件
  6. python 遍历对象_python js对象的遍历
  7. java fieldposition_Java FieldPosition toString()用法及代码示例
  8. Matplotlib随记2
  9. 传统开发被冲击得“七零八落”,云原生时代下开发者要如何自救?
  10. 高等教育中的人工智能市场现状研究分析报告-
  11. 全景图拍摄方式有哪些?全景图拍摄制作流程是什么?
  12. 【日常】我的电影、小说、番剧、歌曲“观看记录清单”
  13. 手机端获取用户详细地理位置(高德地图API)
  14. 寒江独钓前辈的第一个例子的编译运行过程
  15. BIOS,U-BOOT,BootLoader三者的对比
  16. SpringBoot用MultipartFile.transferTo传递相对路径的问题
  17. Python开发的6大优点,让你学到真正的技术!
  18. 论文笔记:基于3D病灶分割和分类多任务学习的新冠肺炎辅助诊断
  19. ActiveMQ的消息重发机制
  20. 辽宁自考计算机及应用,辽宁2010年自考计算机及应用(本科)考试计划

热门文章

  1. windows安全模式_别再用苹果装Windows 因为macOS实在是太好用了
  2. LifeSmart云起局域网直接控制向往背景音乐
  3. 2021年——1024程序员节
  4. 拓扑结构计算机网络结构,计算机网络的常见的七种拓扑结构
  5. java生成图片,可设置背景,文本+公式图片+图片
  6. [ATPG]解读report_nonscan_cells -summary得到的report
  7. WPS Word二级标题自动编号,本来应该是2.1,可是却变成1.3,怎么办?
  8. 2014年沈航817
  9. PCB的分类以及它的制造工艺
  10. ac3168无线网卡驱动下载_英特尔面向Windows 10推出无线网卡驱动程序和图形命令中心应用更新...