Hands-on C++ Game Animation Programming阅读笔记(三)

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重合以外,这样做还有啥原因remain
		 if (fabsf(f.y) && fabs(f.z) < fabsf(f.x))		
			ortho = vec3(0, 0, 1);
		
		// 基于from和找的正交基(好像是随便找的)进行叉乘	
		vec3 axis = normalized(cross(f, ortho));
      	// 注意,两个向量反向时,返回的是Pure Quaternion
		return 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完全相同, 返回true
	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)
	// 或者两个四元数的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 / norm
	return 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 Forward
	vec3 u = normalized(up); // Desired Up
	vec3 r = cross(u, f); // Object Right
	u = cross(f, r); // Object Up
	
	// deltaRotation只需要算一个forward向量的前后delta就行了
	// From world forward to object forward
	quat worldToObject = fromTo(vec3(0, 0, 1), f);//计算q1

	// 根据计算的deltaRot, 计算其它的local轴
	// 算出local up
	vec3 objectUp = worldToObject * vec3(0, 1, 0);
	// From object up to desired up
	quat u2u = fromTo(objectUp, u);//计算q2

	// Rotate to forward direction first
	// then twist to correct up
	quat result = worldToObject * u2u;
	// Don't forget to normalize the result
	return 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和scale
	out.position = a.rotation * (a.scale * b.position);
	// 最后加上a的基础position
	out.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的inverse
	vec3 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)
{
	// 保证四元数的neighbourhood
	quat 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 matrix
	return 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;
	// 取第四列作为pos
	out.position = vec3(m.v[12], m.v[13], m.v[14]);
	// 第四章写过mat4ToQuat这个函数, 就是把mat的basis vector归一化, 然后重新叉乘得到
	// 调用quaternion.cpp里的函数, 提取出rotation
	out.rotation = mat4ToQuat(m);// 算出R
	// 只取3*3的子矩阵M, M = S*R
	mat4 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^-1
	mat4 invRotMat = quatToMat4(inverse(out.rotation));
	// M = S*R => S = M * R^-1
	mat4 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;
	// 取第四列作为pos
	out.position = vec3(m.v[12], m.v[13], m.v[14]);
	// 第四章写过mat4ToQuat这个函数, 就是把mat的basis vector归一化, 然后重新叉乘得到
	// 调用quaternion.cpp里的函数, 提取出rotation
	out.rotation = mat4ToQuat(m);// 算出R
	// 只取3*3的子矩阵M, M = R*S
	mat4 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^-1
	mat4 invRotMat = quatToMat4(inverse(out.rotation));// 算出R-1
	// M = R*S => S = R^-1 * M
	mat4 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和LocalRotation
	out = a.position + out;// 加上基础的Position
	return out;
}

// 注意: transformVector与transformPoint不一样,transformVector没有平移操作,它没有位置这个概念
vec3 transformVector(const Transform& a, const vec3& b) 
{
	vec3 out;
	out = a.rotation * (a.scale * b);
	return out;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Program 3D Games in C++: The #1 Language at Top Game Studios Worldwide C++ remains the key language at many leading game development studios. Since it’s used throughout their enormous code bases, studios use it to maintain and improve their games, and look for it constantly when hiring new developers. Game Programming in C++ is a practical, hands-on approach to programming 3D video games in C++. Modeled on Sanjay Madhav’s game programming courses at USC, it’s fun, easy, practical, hands-on, and complete. Step by step, you’ll learn to use C++ in all facets of real-world game programming, including 2D and 3D graphics, physics, AI, audio, user interfaces, and much more. You’ll hone real-world skills through practical exercises, and deepen your expertise through start-to-finish projects that grow in complexity as you build your skills. Throughout, Madhav pays special attention to demystifying the math that all professional game developers need to know. Set up your C++ development tools quickly, and get started Implement basic 2D graphics, game updates, vectors, and game physics Build more intelligent games with widely used AI algorithms Implement 3D graphics with OpenGL, shaders, matrices, and transformations Integrate and mix audio, including 3D positional audio Detect collisions of objects in a 3D environment Efficiently respond to player input Build user interfaces, including Head-Up Displays (HUDs) Improve graphics quality with anisotropic filtering and deferred shading Load and save levels and binary game data Whether you’re a working developer or a student with prior knowledge of C++ and data structures, Game Programming in C++ will prepare you to solve real problems with C++ in roles throughout the game development lifecycle. You’ll master the language that top studios are hiring for—and that’s a proven route to success. Table of Contents Chapter 1: Game Programming Overview Chapter 2: Game Objects and 2D Graphics Chapter 3: Vectors and Basic Physics Chapter 4: A

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值