【Vector3】2021-r126版three.js中Vector3类的源码学习

【Vector3】2021-r126版three.js中Vector3类的源码学习


写在最前面,threejs中向量的各种运算,并不完全是严格按照数学意义来定的,更多是作为一种工具去使用,例如数学中向量并没有所谓除法运算,但Vector3的方法中是有“除法”的,总之,会用这个工具操纵向量达成目标即可。

three.js中有关vector3的文档地址:threejs官方文档中的Vector3类
线代的几何意义视频(讲的生动有趣建议看看):线性代数的本质(几何意义)Essence of linear algebra

1、构造函数

先看构造函数:

function Vector3(x, y, z) {
			if (x === void 0) {
				x = 0;
			}
			if (y === void 0) {
				y = 0;
			}
			if (z === void 0) {
				z = 0;
			}
			this.x = x;
			this.y = y;
			this.z = z;
		}

可见一个向量(或称三维空间中的一个点)仅包含3个属性,即x,y,z,传入要按照xyz的顺序来。

threejs提供了很多便利的点与方向的计算方法,下面分成几类来学习。

2、方法

2.1 加减乘除(数值运算)

2.1.1 加法运算(平移)

方法用途
.add ( v : Vector3 ) : this将传入的向量v和这个向量相加。
.addScalar ( s : Float ) : this将传入的标量s和这个向量的x值、y值以及z值相加。
.addScaledVector ( v : Vector3, s : Float ) : this将所传入的v与s相乘所得的乘积和这个向量相加。
.addVectors ( a : Vector3, b : Vector3 ) : this将该向量设置为a + b。

下面分析源码时,如果不是特殊的传入参数,则不作解释,传参类型在表格中有给出。

从上到下依次看源码,可以看到,加法在代码层面都蛮简单的。
(补充:唯一要注意的是,源码add显示可以传入两个参数,文档对add的解释却是仅可传入一个参数,这里抛出的warn给出了答案,应该是过去的版本支持两个参数吧。)

_proto.add = function add(v, w) {
			if (w !== undefined) {
				console.warn('THREE.Vector3: .add() now only accepts one argument. Use .addVectors( a, b ) instead.');
				return this.addVectors(v, w);
			}
			this.x += v.x;
			this.y += v.y;
			this.z += v.z;
			return this;
		};
_proto.addScalar = function addScalar(s) {
			this.x += s;
			this.y += s;
			this.z += s;
			return this;
		};
_proto.addVectors = function addVectors(a, b) {
			this.x = a.x + b.x;
			this.y = a.y + b.y;
			this.z = a.z + b.z;
			return this;
		};
_proto.addScaledVector = function addScaledVector(v, s) {
			this.x += v.x * s;
			this.y += v.y * s;
			this.z += v.z * s;
			return this;
		};

下面稍微分析下向量加法的几何意义,如下图所示,黄向量+红向量,其实就等于黄向量的点,朝着浅粉这个方向,移动红向量的长度,也就是说,向量的加法其实是点朝着某个向量的方向移动而已。

通常向量都是由原点出发的,但在空间中,它可以在任何位置出现,表达的是同一个方向,因此,这里黄+红向量,等价于将红向量移动至粉向量处,然后黄向量的点沿粉向量移动一段距离。

而黄色方向+粉色方向,等价于深粉方向,也就是两次平移操作等价于一个平移操作,这就是向量加法。

因此,在思考一个点该朝哪个方向移动时,可以基于坐标轴原点来思考,再将其与点相加,即可让该点朝着该方向移动。(加上两倍的向量意为朝着向量方向移动两倍距离)
在这里插入图片描述

2.1.2 减法运算(平移)

方法用途
.sub ( v : Vector3 ) : this从该向量减去向量v。
.subScalar ( s : Float ) : this从该向量的x、y和z中减去标量s。
.subVectors ( a : Vector3, b : Vector3 ) : this将该向量设置为a - b。

跟加法如出一辙,看源码。其实还是平移,只不过是向指定向量的反方向平移,不多赘述。

_proto.sub = function sub(v, w) {
			if (w !== undefined) {
				console.warn('THREE.Vector3: .sub() now only accepts one argument. Use .subVectors( a, b ) instead.');
				return this.subVectors(v, w);
			}
			this.x -= v.x;
			this.y -= v.y;
			this.z -= v.z;
			return this;
		};
		_proto.subScalar = function subScalar(s) {
			this.x -= s;
			this.y -= s;
			this.z -= s;
			return this;
		};
		_proto.subVectors = function subVectors(a, b) {
			this.x = a.x - b.x;
			this.y = a.y - b.y;
			this.z = a.z - b.z;
			return this;
		};

2.1.3 "乘法"运算(缩放)

方法用途
.multiply ( v : Vector3 ) : this将该向量与所传入的向量v进行相乘。
.multiplyScalar ( s : Float ) : this将该向量与所传入的标量s进行相乘。
.multiplyVectors ( a : Vector3, b : Vector3 ) : this按照分量顺序,将该向量设置为和a * b相等。

先看multiply的源码,可见multiply方法定义的向量相乘,实际上仅是数值相乘,而该方法等价于将一个纯缩放矩阵作用于向量(如果各个缩放尺度一致,则为统一缩放,否则模型将会变形)。

	_proto.multiply = function multiply(v, w) {
			if (w !== undefined) {
				console.warn('THREE.Vector3: .multiply() now only accepts one argument. Use .multiplyVectors( a, b ) instead.');
				return this.multiplyVectors(v, w);
			}
			this.x *= v.x;
			this.y *= v.y;
			this.z *= v.z;
			return this;
		};

接下来看multiplyScalar 的源码,由于是乘以统一的标量,因此,该方法等价于将一个统一缩放矩阵作用于向量

_proto.multiplyScalar = function multiplyScalar(scalar) {
			this.x *= scalar;
			this.y *= scalar;
			this.z *= scalar;
			return this;
		};

接下来是multiplyVectors的源码:

_proto.multiplyVectors = function multiplyVectors(a, b) {
	this.x = a.x * b.x;
	this.y = a.y * b.y;
	this.z = a.z * b.z;
	return this;
};

道理差不多的,只不过不是作用于this,而是传入两个向量做计算,可以视为将b作用于a,对a进行了缩放,或将a作用于b,对b进行了缩放。

补充:这里贴一下缩放矩阵,方便理解,如下图,斜对角线上为各个维度的缩放尺度,当三个数相等时,模型将会作统一缩放,即形状不变,只是变大了,否则模型将会变形,变长,变扁扁之类的。
在这里插入图片描述

2.1.4 "除法"运算(缩放)

(注:与上文乘法类似,如果对模型整体应用该方法,同样会导致变形)

方法用途
.divide ( v : Vector3 ) : this将该向量除以向量v。
.divideScalar ( s : Float ) : this通将该向量除以标量s。如果传入的s = 0,则向量将被设置为( 0, 0, 0 )

看源码,跟乘法差不多,只是少了一个将this设置为两向量相除而已,不多赘述。

_proto.divide = function divide(v) {
	this.x /= v.x;
	this.y /= v.y;
	this.z /= v.z;
	return this;
};
_proto.divideScalar = function divideScalar(scalar) {
	return this.multiplyScalar(1 / scalar);
};

2.2 各种变换

2.2.1 轴角applyAxisAngle(仅旋转)

方法用途
.applyAxisAngle ( axis : Vector3, angle : Float ) : this将轴和角度所指定的旋转应用到该向量上。

(注:变换包括平移,旋转,缩放,切变;轴角,欧拉角与四元数仅表示旋转)

下面一个个看源码,轴角法,即,以一个向量定义旋转轴,再加一个旋转角度来定义旋转,比如,如果用(0,1,0)(即y轴)来表示旋转轴,那么就是绕着y轴的旋转。
从源码可以看出,轴角无法直接作用于点,它需要转换为四元数后,使用四元数来完成旋转,这里用到了基于轴角转换四元数的方法(setFromAxisAngle),以后会另开一篇博文介绍四元数的方法,这里只要知道这是为了转换为四元数即可(四元数或矩阵可以直接作用于点)。

_proto.applyAxisAngle = function applyAxisAngle(axis, angle) {
			return this.applyQuaternion(_quaternion.setFromAxisAngle(axis, angle));
		};

2.2.2 欧拉角applyEuler(仅旋转)

方法用途
.applyEuler ( euler : Euler ) : this通过将Euler(欧拉)对象转换为Quaternion(四元数)并应用, 将欧拉变换应用到这一向量上。

而欧拉角意为,任意一个旋转,都可拆分为绕三根主轴(模型的x,y,z轴)的旋转,旋转角度包括3个,可称为偏航,俯仰,与滚动,这样的称呼是基于飞机模型来定的,因为是一种较为形象的描述方法,现在也比较常用了。
从源码中可以看出,欧拉角也无法直接作用于点,同样需要转换为四元数,只不过转换所使用的方法为基于欧拉角(setFromEuler)。

_proto.applyEuler = function applyEuler(euler) {
			if (!(euler && euler.isEuler)) {
				console.error('THREE.Vector3: .applyEuler() now expects an Euler rotation rather than a Vector3 and order.');
			}
			return this.applyQuaternion(_quaternion.setFromEuler(euler));
		};

补充:上面那些方法的传参都是3维向量类或浮点数,就没用多做说明,而applyEuler 有所改变,传入为euler(欧拉角),欧拉角是一个新的类(threejs文档中的euler类),专门用于表示欧拉角,有空另起一篇文章讲下。

2.2.3 轴角与欧拉角的对比

欧拉角与轴角的区别在于,轴角法不会遇到万向节死锁问题,储存需要4个浮点值(3个数表示轴,1个表示角),欧拉角的储存仅需3个浮点数(但在threejs里,由于还需要指定该欧拉角的旋转顺序(注:由于向量乘法不满足交换律,所以旋转矩阵乘法的顺序会影响旋转的结果),因此还要多存个字符串),此外,欧拉角会面临万向节死锁问题(关于万向节死锁,详见如何脱离数学推导理解欧拉角与万向节死锁),以及,欧拉角与轴角均无法直接作用于点,都需要转换为四元数或旋转矩阵才能作用到点上。

2.2.4 四元数applyQuaternion(仅旋转)

方法用途
.applyQuaternion ( quaternion : Quaternion ) : this将Quaternion变换应用到该向量。

接下来讲下四元数,四元数是加速旋转计算的方法,要在数学层面或者几何意义上去理解它有一定的困难,这里放一个生动形象的科普视频,感兴趣可以看下四元数的可视化

放下《游戏引擎架构》关于四元数的一段原文,可以看到这里提到,传统基于矩阵的旋转计算存在3个问题:
1、占用储存空间。 2、性能相对较低。 3、难以计算插值。
因此引入了四元数作为其余旋转方法的代替,并且也是目前最流行的方法。
在这里插入图片描述
此外,四元数还可以用于避免基于欧拉角旋转导致的万向节死锁问题,然而我们还是可以使用欧拉角的,只不过在计算时,不是将欧拉角转换为三个旋转矩阵再作用于向量,而是将欧拉角转换为四元数再作用于向量。

2.2.5 用四元数取代矩阵解决欧拉角万向节死锁

这里要再强调一下,基于欧拉角导致万向节死锁,本质上是基于三个连续相乘的旋转矩阵导致的死锁,也就是用了三个表示绕三根主轴旋转的旋转矩阵,作用到一个向量上,这样的计算方式会导致出现死锁。然而,用欧拉角计算出四元数,再把四元数作用在向量上,这样是不会导致死锁的,因为这个本质等于,按照给出的欧拉角计算出了旋转轴,然后按照一根旋转轴来进行了旋转,因此,这就并不是将一个旋转拆分为3个旋转来运算,就不会出现死锁现象。

但是!

这里看一下欧拉角转四元数中,判断旋转顺序的源码,可以发现,可转四元数的顺序不包括重复的,也就是类似XYX这样的顺序,因此如果要实现诸如XYX,ZYZ这样的运动,首先这玩意不能转四元数,因此只能用三个旋转矩阵来计算,而这种情况下,还是要面临万向节死锁!

因此,实际上万向节死锁是无法完全消灭的,总要去面对它。
更多内容可见我关于万向节死锁的文章。

switch (order) {
				case 'XYZ':
					this._x = s1 * c2 * c3 + c1 * s2 * s3;
					this._y = c1 * s2 * c3 - s1 * c2 * s3;
					this._z = c1 * c2 * s3 + s1 * s2 * c3;
					this._w = c1 * c2 * c3 - s1 * s2 * s3;
					break;
				case 'YXZ':
					this._x = s1 * c2 * c3 + c1 * s2 * s3;
					this._y = c1 * s2 * c3 - s1 * c2 * s3;
					this._z = c1 * c2 * s3 - s1 * s2 * c3;
					this._w = c1 * c2 * c3 + s1 * s2 * s3;
					break:
				case 'ZXY':
					this._x = s1 * c2 * c3 - c1 * s2 * s3;
					this._y = c1 * s2 * c3 + s1 * c2 * s3;
					this._z = c1 * c2 * s3 + s1 * s2 * c3;
					this._w = c1 * c2 * c3 - s1 * s2 * s3;
					break;
				case 'ZYX':
					this._x = s1 * c2 * c3 - c1 * s2 * s3;
					this._y = c1 * s2 * c3 + s1 * c2 * s3;
					this._z = c1 * c2 * s3 - s1 * s2 * c3;
					this._w = c1 * c2 * c3 + s1 * s2 * s3;
					break;
				case 'YZX':
					this._x = s1 * c2 * c3 + c1 * s2 * s3;
					this._y = c1 * s2 * c3 + s1 * c2 * s3;
					this._z = c1 * c2 * s3 - s1 * s2 * c3;
					this._w = c1 * c2 * c3 - s1 * s2 * s3;
					break;
				case 'XZY':
					this._x = s1 * c2 * c3 - c1 * s2 * s3;
					this._y = c1 * s2 * c3 - s1 * c2 * s3;
					this._z = c1 * c2 * s3 + s1 * s2 * c3;
					this._w = c1 * c2 * c3 + s1 * s2 * s3;
					break;
				default:
					console.warn('THREE.Quaternion: .setFromEuler() encountered an unknown order: ' + order);
			}

2.2.6 3x3矩阵与4x4矩阵(涵盖多种变换)

方法用途
.applyMatrix3 ( m : Matrix3 ) : this将所传入的v与s相乘所得的乘积和这个向量相加。
.applyMatrix4 ( m : Matrix4 ) : this将该向量乘以四阶矩阵m(第四个维度隐式地为1),并按视角划分。
.transformDirection ( m : Matrix4 ) : this通过传入的矩阵(m的左上角3 x 3子矩阵)变换向量的方向, 并将结果进行normalizes(归一化)。

对比:除了平移之外,3x3矩阵可表示其余任意一种变换,也可将多组变换通过乘法进行拼接,但受限于矩阵运算的方法,3x3矩阵不能表示平移,具体分析可看《游戏引擎架构》Jason Gregory的第四章,而4x4矩阵理论上可表示任意一种变换,包括平移,旋转,缩放及/或切变,一组变换串接成的某目标变换的4x4矩阵称为仿射矩阵,且任何一个仿射矩阵均可由一组表示纯平移,纯旋转,纯缩放及/或切变,通过乘法串接形成,因此,虽然一个仿射矩阵很难直接想象出来,但拆分为基础变换的组合相对简单。由于相比3x3矩阵而言,4x4矩阵可表示任意一种(包括平移)的变换,因此在游戏开发中,4x4矩阵通常更为常用,但在无位移的情况下,也因情况而定。)

补充:具体是矩阵x向量,还是向量x矩阵,这个顺序是按照矩阵由列还是行向量组成而定的,一般常用的是由列向量组成矩阵,也即向量在矩阵的右侧,但如果某引擎核心是基于行向量,则基础变换矩阵的写法也应有所调整,在threejs库中是基于列向量的。

此外,最先接触向量的最先变换,而在threejs内,向量在矩阵的右边,因此,变换A,变换B,变换C的乘法顺序,应该是CBA*向量。

综上所述,与基于角度的旋转不同,矩阵既可以表示旋转,也可以表示平移,缩放,切变等,且可以通过一个个基础变换,通过乘法拼接而成,Vector3给定了基于3x3矩阵或基于4x4矩阵的变换方法,也即,可以首先通过串连计算出仿射矩阵或3x3变换矩阵,再通过调用方法applyMatrix3或applyMatrix4,将其应用于向量上做某种变换。

接下来看下3x3矩阵(applyMatrix3)的源码,这里传参是三维矩阵类,先放下官方文档关于这个类的地址threejs文档中的Matrix3类,后面有空再写篇文章读下源码。
elements是三维矩阵中的元素,是一个Array数组,这里是按照列优先的顺序来储存的,通过读源码可知,函数意为给点在左边乘以一个3x3的线性变换矩阵,且矩阵由列向量组成。

_proto.applyMatrix3 = function applyMatrix3(m) {
			var x = this.x, y = this.y, z = this.z;
			var e = m.elements;
			this.x = e[0] * x + e[3] * y + e[6] * z;
			this.y = e[1] * x + e[4] * y + e[7] * z;
			this.z = e[2] * x + e[5] * y + e[8] * z;
			return this;
		};

4x4矩阵(applyMatrix4 )也是一样的,只不过由于调用该方法的this是一个3维向量,因此在计算时需要临时添加第4个维度w,在运算后,再将4维坐标转3维,下面是《游戏引擎架构》里关于4维坐标转3维的方法,这个转换过程被隐藏在applyMatrix4内部了
在这里插入图片描述

_proto.applyMatrix4 = function applyMatrix4(m) {
			var x = this.x,
					y = this.y,
					z = this.z;
			var e = m.elements;
			var w = 1 / (e[3] * x + e[7] * y + e[11] * z + e[15]);
			this.x = (e[0] * x + e[4] * y + e[8] * z + e[12]) * w;
			this.y = (e[1] * x + e[5] * y + e[9] * z + e[13]) * w;
			this.z = (e[2] * x + e[6] * y + e[10] * z + e[14]) * w;
			return this;
		};

为了便于理解,这里把applyMatrix4矩阵与向量相乘的表达式贴在下面,可以看到,为了与4x4矩阵相乘,右边向量需要加一个维度w,且w值为1(其实这里有两种w的设置方法,一种是w设为1,另一种是设为0,若右边所乘为纯方向向量,则这里要把w设置为0,但applyMatrix4方法内是设置为1的,也即applyMatrix4方法所处理的三维向量为包括方向与长度的向量,而不是纯方向向量),在源码中计算的var w,实际上就是上面图片所示,将计算后的4位点转换为3维点时,除以w的一个操作,源码中是乘,因为计算w时已经被1所除了。
在这里插入图片描述
最后看下transformDirection 的源码,参考上图4x4矩阵的下标,可知该方法意为,传入一4x4变换矩阵m,但仅取左上角的3x3矩阵,并将其应用于向量this,可以理解为,该方法消除了变换中的平移操作,而仅对向量做了平移之外的变换。

_proto.transformDirection = function transformDirection(m) {
			// input: THREE.Matrix4 affine matrix
			// vector interpreted as a direction
			var x = this.x,
					y = this.y,
					z = this.z;
			var e = m.elements;
			this.x = e[0] * x + e[4] * y + e[8] * z;
			this.y = e[1] * x + e[5] * y + e[9] * z;
			this.z = e[2] * x + e[6] * y + e[10] * z;
			return this.normalize();
		};

2.2.7 法线矩阵

方法用途
.applyNormalMatrix ( m : Matrix3 ) : this将该向量乘以法线矩阵m并将结果归一化。

看下源码:

_proto.applyNormalMatrix = function applyNormalMatrix(m) {
			return this.applyMatrix3(m).normalize();
		};

2.3 基于某种方式设置向量属性(xyz值)

2.3.1 基于笛卡尔坐标系设置xyz值

方法用途
.set ( x : Float, y : Float, z : Float ) : this设置该向量的x、y 和 z 分量。
.setX ( x : Float ) : this将向量中的x值替换为x。
.setY ( y : Float ) : this将向量中的y值替换为y。
.setZ ( z : Float ) : this将向量中的z值替换为z。
.setComponent ( index : Integer, value : Float ) : null若index为 0 则设置 x 值为 value。若index为 1 则设置 y 值为 value。若index为 2 则设置 z 值为 value。
.setScalar ( scalar : Float ) : this将该向量的x、y和z值同时设置为等于传入的scalar。

看下源码,理解上应该没有难度,不多赘述。

_proto.set = function set(x, y, z) {
	if (z === undefined) z = this.z; // sprite.scale.set(x,y)
	this.x = x;
	this.y = y;
	this.z = z;
	return this;
};
_proto.setX = function setX(x) {
	this.x = x;
	return this;
};
_proto.setY = function setY(y) {
	this.y = y;
	return this;
};
_proto.setZ = function setZ(z) {
	this.z = z;
	return this;
};
_proto.setScalar = function setScalar(scalar) {
	this.x = scalar;
	this.y = scalar;
	this.z = scalar;
	return this;
};

2.3.2 基于圆柱或球坐标系设置xyz值

方法用途
.setFromCylindrical ( c : Cylindrical ) : this从圆柱坐标c中设置该向量。
.setFromCylindricalCoords ( radius : Float, theta : Float, y : Float ) : this从圆柱坐标中的radius、theta和y设置该向量。
.setFromSpherical ( s : Spherical ) : this从球坐标s中设置该向量。
.setFromSphericalCoords ( radius : Float, phi : Float, theta : Float ) : this从球坐标中的radius、phi和theta设置该向量。

在2.3.1中介绍了基于直角笛卡尔坐标系直接修改向量xyz值的方法,在这一节中,将额外拓展两个新的坐标系,即基于圆柱,与球坐标系来修改点的xyz值。下面贴下《游戏引擎架构》中关于这两种常用坐标系的讲解:
在这里插入图片描述
了解圆柱与球坐标系如何表示一个点后,下面看setFromCylindrical和setFromCylindricalCoords 的源码(基于圆柱坐标系):

_proto.setFromCylindrical = function setFromCylindrical(c) {
	return this.setFromCylindricalCoords(c.radius, c.theta, c.y);
};
_proto.setFromCylindricalCoords = function setFromCylindricalCoords(radius, theta, y) {
	this.x = radius * Math.sin(theta);
	this.y = y;
	this.z = radius * Math.cos(theta);
	return this;
};

这两个方法的区别是,setFromCylindrical基于圆柱坐标系类所创造的点来修改向量的xyz值,而setFromCylindricalCoords是通过直接指明圆柱坐标系下某点的三个值,来修改向量的xyz值,实际上,通过代码也可看出,两者是一个意思。
但是建议使用setFromCylindrical方法,因为通过Cylindrical(圆柱坐标)类来构建的点,会有一些好用的方法,例如将xyz点转为圆柱坐标系下的点等等。

下面看下setFromSpherical 与setFromSphericalCoords (球坐标系)的源码:
这俩方法跟上面圆柱坐标系的两种方法差不多,都是一个基于值,一个基于类,同样建议使用基于类的方法(当然,如果考虑到占用空间的问题,在确实没必要的情况下,使用基于值也是完全可以的,两种方法本质上并无区别)。

_proto.setFromSpherical = function setFromSpherical(s) {
			return this.setFromSphericalCoords(s.radius, s.phi, s.theta);
		};

_proto.setFromSphericalCoords = function setFromSphericalCoords(radius, phi, theta) {
		var sinPhiRadius = Math.sin(phi) * radius;
		this.x = sinPhiRadius * Math.sin(theta);
		this.y = Math.cos(phi) * radius;
		this.z = sinPhiRadius * Math.cos(theta);
		return this;
};

补充:三种坐标系各有利弊,其实作用都是为了将一个向量(点)移动到另一个位置,其中,圆柱与球坐标系,适合控制点沿着曲线运动,例如,通过调整圆柱坐标系中点的角度与y值,可使一点呈螺旋状逐渐上升或下降,而调整球面坐标系中垂直于xz平面的角度,可使点做类似太阳东升西落的运动,而这些“圆弧”运动,用笛卡尔坐标系是很难控制的。

因此,在不同场景下,选用合适的坐标系是很有价值的。

2.3.3 基于矩阵设置xyz值

【建议在看这一小节前,先看下2.3.4小节,理解下提取出变换矩阵中一列的用意】

方法用途
.setFromMatrixColumn ( matrix : Matrix4, index : Integer ) : this从传入的四阶矩阵matrix由index指定的列中, 设置该向量的x值、y值和z值。
.setFromMatrix3Column ( matrix : Matrix3, index : Integer ) : this从matrix的索引列 设置此向量的x,y和z分量。
.setFromMatrixPosition ( m : Matrix4 ) : this从变换矩阵(transformation matrix)m中, 设置该向量为其中与位置相关的元素。
.setFromMatrixScale ( m : Matrix4 ) : this从变换矩阵(transformation matrix)m中, 设置该向量为其中与缩放相关的元素。

接下来看下setFromMatrixPosition 的源码,即基于4x4矩阵中与位移相关数值来设置点的xyz值,传入参数是一个4x4矩阵。

_proto.setFromMatrixPosition = function setFromMatrixPosition(m) {
			var e = m.elements;
			this.x = e[12];
			this.y = e[13];
			this.z = e[14];
			return this;
		};

先讲下何为“矩阵中与位移相关的值”,如下图示。下面展示了两种不同的变换矩阵作用于点的方法,上面是点乘矩阵,下面是矩阵乘点,在部分引擎中使用的是上面的规则,而在threejs中,则是使用下面的规则。(补充:如果矩阵由列向量组成,则应使用矩阵乘点,如果矩阵由行向量组成,则应使用点乘矩阵,如不理解也不碍事,会用即可)
在这里插入图片描述
在这里插入图片描述
通过给向量与矩阵加一个维度,可以让矩阵也具有使点平移的功能,而与平移有关的数值,根据不同的相乘顺序,在4x4矩阵上的位置也不同,(补充一句,3x3矩阵无法进行平移,只有4x4矩阵里有平移),观察图片可知,在threejs的规则中,平移的3个数值在4x4矩阵的右侧。
该方法的大概作用可以描述为,仅取某仿射矩阵中的平移效果,而不作其他变换,例如一个矩阵可使模型旋转,放大后按某向量进行平移,则使用该方法可消除其他效果而仅作平移。

接下来看下setFromMatrixColumn的源码,其中fromArray仅看源码(它也是Vector3的方法,会在其他小节讲到),可知其本意是从一个数组中某个位置开始连续的取得3个数值,并将三个数值作为xyz值赋值给调用它的Vector3向量。

但是,fromArray在此处的用处,是从一个矩阵中取得一列的前3个维度的值(因为threejs所基于的矩阵均由列向量组成,因此,setFromMatrixColumn函数的含义是将某矩阵中某列所表示的那个向量作用于this向量),取前3个维度,因为组成4x4矩阵的列向量的第4个维度w,并不需要(也无法)作用于只有xyz三个维度的Vector3向量。

因此,简单来说就是,setFromMatrixColumn的作用是,从一个4x4矩阵中,按照index指示的列号,取得该列的前3个维度值,并将其分别赋值给this的xyz值。

_proto.setFromMatrixColumn = function setFromMatrixColumn(m, index) {
			return this.fromArray(m.elements, index * 4);
		};

下面看下setFromMatrix3Column 的源码,它其实就是setFromMatrixColumn 方法的3x3矩阵版

_proto.setFromMatrix3Column = function setFromMatrix3Column(m, index) {
			return this.fromArray(m.elements, index * 3);
		};

这里再来回顾下threejs中储存4x4矩阵元素时,各个元素在数组中的下标,如下图,矩阵中第二行第三个元素,在实际储存的elements数组中为e(9),即储存在下标为9的位置。
在这里插入图片描述
以及threejs中储存3x3矩阵元素时,各个元素在数组中的下标,如下图。
在这里插入图片描述
因此,举例, this.setFromMatrixColumn(m, 0)的意思是,从e(0)开始,取得e(0)赋值给this.x,取得e(1)赋值给this.y,以及取得e(2)赋值给this.z,而setFromMatrixColumn(m, 1)的意思是,从e(4)开始,取得e(4)赋值给this.x,取得e(5)赋值给this.y,以及取得e(6)赋值给this.z,以此类推,这也是为什么index 要乘以4的原因。(3x3矩阵不再赘述,跟4x4是一个道理。)

接下来看下setFromMatrixScale 的源码,其中length()为获得向量的模,也就是原点到向量点的线段长度。

_proto.setFromMatrixScale = function setFromMatrixScale(m) {
			var sx = this.setFromMatrixColumn(m, 0).length();
			var sy = this.setFromMatrixColumn(m, 1).length();
			var sz = this.setFromMatrixColumn(m, 2).length();
			this.x = sx;
			this.y = sy;
			this.z = sz;
			return this;
		};

setFromMatrixScale 所传入的参数是一个4x4矩阵,this.setFromMatrixColumn(m, 0).length()意为,在将4x4矩阵m的某一列所表示的向量赋值给this后,计算新生成的向量this的模长。分析至此,setFromMatrixScale方法的含义也很好理解了,它会将传入的4x4矩阵中,前3列且前3维度的向量提取出来,分别计算模长后,赋值给this的x,y,z值,也即,提取出缩放相关的数值,并将其赋值给向量。

补充:纯缩放矩阵如下图所示,如果传入setFromMatrixColumn的矩阵为这样的纯缩放矩阵,则提取出的this向量,与另一向量做“乘法”运算(multiply方法)等价于将该纯缩放矩阵作用于另一向量。
在这里插入图片描述

2.3.4 基于数组形式的矩阵设置xyz值

方法用途
.fromArray ( array : Array, offset : Integer ) : this设置向量中的x值为array[ offset + 0 ],y值为array[ offset + 1 ], z值为array[ offset + 2 ]。

看下fromArray 源码:

_proto.fromArray = function fromArray(array, offset) {
			if (offset === void 0) {
				offset = 0;
			}
			this.x = array[offset];
			this.y = array[offset + 1];
			this.z = array[offset + 2];
			return this;
		};

这里要明确一点,矩阵实际储存是用数组的,下图为3x3矩阵与4x4矩阵各个位置的元素在元素数组中的下标,而fromArray方法可以从offset所指示的下标开始,连续获取3个数值,对比下图可知,该方法可以获取到3x3或者4x4数组中的一个3维列向量,也就是可以单独提取出某个变换矩阵中,某一个基的变换,将其赋值给某向量后,即意为将该向量移动至这个基。

简单来说就是,变换矩阵的3个列向量,其实等于目标空间的三个基在原空间的投影,原空间的点可以利用这3个基组成的变换矩阵,来变换到目标空间,而提取出一个基之后,赋值给某变换矩阵,则可对目标空间做出修改(变换),例如,如果一个变换矩阵是统一缩放,那提取出其中一列之后,与原空间的另外两个基向量组成一个新的变换矩阵,则该新变换矩阵可仅对提取出的那一列所对应的维度进行缩放。

其余应用环境可根据需求来考虑,如对上面一段无法理解,可去看下本文开头提到的线性变换本质的视频。
在这里插入图片描述
在这里插入图片描述

2.3.5 基于BufferAttribute类设置xyz值

方法用途
.fromBufferAttribute ( attribute : BufferAttribute, index : Integer ) : this从attribute中设置向量的x值、y值和z值。

先简单介绍下BufferAttribute类,该类储存了BufferGeometry定义的几何/几何体中的数据,包括顶点,顶点颜色等,其中顶点按顺序,索引从0开始,因此,fromBufferAttribute方法意为,获取某几何/几何体中由索引指定的某顶点的xyz值,并将其赋值给this。

再来看下源码,其中attribute.getX方法为,获取由index索引指定的某顶点的x值。

要注意的是,该方法仅仅是取出来了顶点的数值,并不是取到了几何/几何体中的一个顶点对象,因为顶点是以数值的形式储存在一维数组里的,并不是对象数组。

_proto.fromBufferAttribute = function fromBufferAttribute(attribute, index, offset) {
			if (offset !== undefined) {
				console.warn('THREE.Vector3: offset has been removed from .fromBufferAttribute().');
			}
			this.x = attribute.getX(index);
			this.y = attribute.getY(index);
			this.z = attribute.getZ(index);
			return this;
		};

2.3.6 基于限定范围设置xyz值(圈在范围内)

方法用途
.clamp ( min : Vector3, max : Vector3 ) : this如果该向量的x值、y值或z值大于限制范围内最大x值、y值或z值,则该值将会被所对应的值取代。如果该向量的x值、y值或z值小于限制范围内最小x值、y值或z值,则该值将会被所对应的值取代。
.clampLength ( min : Float, max : Float ) : this如果向量长度大于最大值,则它将会被最大值所取代。如果向量长度小于最小值,则它将会被最小值所取代。
.clampScalar ( min : Float, max : Float ) : this如果该向量的x值、y值或z值大于最大值,则它们将被最大值所取代。如果该向量的x值、y值或z值小于最小值,则它们将被最小值所取代。
.max ( v : Vector3 ) : this如果该向量的x值、y值或z值小于所传入v的x值、y值或z值, 则将该值替换为对应的最大值。
.min ( v : Vector3 ) : this如果该向量的x值、y值或z值大于所传入v的x值、y值或z值, 则将该值替换为对应的最小值。

下面看下clamp 源码,注意传入的min与max都是Vector3类,且默认传入的第一个向量代表min,第二个代表max,所以注意不要传反。此外,Math.max返回传入的两个数值中较大的值,Math.min返回较小值,Math.max(min.x, Math.min(max.x, this.x))这个写法大意为,在对比最小值时,取相对较大的值,在对比最大值时,取相对较小的那个,以此来将点限定在由min和max向量指定的范围内。

_proto.clamp = function clamp(min, max) {
	// assumes min < max, componentwise
	this.x = Math.max(min.x, Math.min(max.x, this.x));
	this.y = Math.max(min.y, Math.min(max.y, this.y));
	this.z = Math.max(min.z, Math.min(max.z, this.z));
	return this;
};

下面看下clampLength源码,这个是基于向量长度(模)来限制的,传入的min与max是浮点型数值,此外,divideScalar(length || 1)的作用是将this向量转换为单位向量,与normalize()方法的作用是一样的,这里不用normalize应该是因为,由于对比min,max,与模长的大小必须要计算一次模长,而调用normalize的话,内部又计算一次模长,虽然代码会简单,但实际多运行了几步,因此没有使用normalize。

_proto.clampLength = function clampLength(min, max) {
			var length = this.length();
			return this.divideScalar(length || 1).multiplyScalar(Math.max(min, Math.min(max, length)));
		};

下面看下clampScalar 的源码,这个方法等价于给clamp方法传入两个各个维度值相等的向量,只不过传入两个向量还要使用构建函数,因此,在限定范围为两个三维值相等的向量时,可使用clampScalar 方法来减少一点计算。

_proto.clampScalar = function clampScalar(minVal, maxVal) {
			this.x = Math.max(minVal, Math.min(maxVal, this.x));
			this.y = Math.max(minVal, Math.min(maxVal, this.y));
			this.z = Math.max(minVal, Math.min(maxVal, this.z));
			return this;
		};

接下来看max和min的源码,跟上面将点限制在范围内不同,这俩是类似于设置“下限”,如min方法意为,若this的各个分量小于v的对应分量,则修改为v的分量,而max方法相反。

因此,经min或max方法运算过的向量,各个分量均小于,或大于传入的v向量对应的分量。

_proto.min = function min(v) {
			this.x = Math.min(this.x, v.x);
			this.y = Math.min(this.y, v.y);
			this.z = Math.min(this.z, v.z);
			return this;
		};
_proto.max = function max(v) {
			this.x = Math.max(this.x, v.x);
			this.y = Math.max(this.y, v.y);
			this.z = Math.max(this.z, v.z);
			return this;
		};

举个直观点的例子,如下图(参考2维),若min点为图中黑色圆圈处,则向量允许出现的范围为红色区域,超出该区域的点将被min函数移动至区域边缘,该小节其他方法也可用此方法理解,就不一一画图了(3维即限定在一块立体空间内)。
在这里插入图片描述

2.3.7 基于取整操作设置xyz值

方法用途
.ceil () : this将该向量x分量、 y分量以及z分量向上取整为最接近的整数。
.floor () : this向量的分量向下取整为最接近的整数值。
.round () : this向量中的分量四舍五入取整为最接近的整数值。
.roundToZero () : this向量中的分量朝向0取整数(若分量为负数则向上取整,若为正数则向下取整)。

ceil ,floor ,与round 的源码贴在下面,因为比较简单,不作详细分析。

这里只举个例子方便理解:

4.3向上取整是5,向下取整是4,四舍五入是4。
-4.3向上取整是-4,向下取整是-5,四舍五入是-4。

_proto.floor = function floor() {
			this.x = Math.floor(this.x);
			this.y = Math.floor(this.y);
			this.z = Math.floor(this.z);
			return this;
		};
_proto.ceil = function ceil() {
			this.x = Math.ceil(this.x);
			this.y = Math.ceil(this.y);
			this.z = Math.ceil(this.z);
			return this;
		};
_proto.round = function round() {
			this.x = Math.round(this.x);
			this.y = Math.round(this.y);
			this.z = Math.round(this.z);
			return this;
		};

接下来看下roundToZero 源码,此为向原点取整。

_proto.roundToZero = function roundToZero() {
			this.x = this.x < 0 ? Math.ceil(this.x) : Math.floor(this.x);
			this.y = this.y < 0 ? Math.ceil(this.y) : Math.floor(this.y);
			this.z = this.z < 0 ? Math.ceil(this.z) : Math.floor(this.z);
			return this;
		};

如下图所示,红色区域为取整前四个点组成的区域,如果对四个点使用向原点取整,则新生成的点组成的区域会在原区域内部。(图示为2维,扩展成3维即在原立体空间内)
在这里插入图片描述
但是,如果对四个点使用向下取整,则在点的分量存在负值时,新生成的点组成的区域将会超出原区域,如下图所示。
在这里插入图片描述
上面两张图仅用作加深理解,并不是说向原点取整只可应用于这一种情况,还是要具体情况具体分析。

2.3.8 基于叉积设置xyz值

方法用途
.cross ( v : Vector3 ) : this将该向量设置为它本身与传入的v的叉积(cross product)。
.crossVectors ( a : Vector3, b : Vector3 ) : this将该向量设置为传入的a与b的叉积(cross product)。

先介绍下叉积,叉积得到的是一个向量,该向量垂直于求叉积的两个向量表示的平面集(因为向量可随意移动,所以两个向量可以表示一组平行的平面),也就是法向量,该法向量的方向遵循右手定则,如下图所示,红色即为叉积的方向。
在这里插入图片描述
接下来看下crossVectors 的源码,源码内基于叉积公式计算法向量的xyz值,并将其赋值给了this,叉积公式如下所示。该方法意为将一向量设置为两个传入向量所表示的平面集的法向量。
在这里插入图片描述

_proto.crossVectors = function crossVectors(a, b) {
			var ax = a.x,
					ay = a.y,
					az = a.z;
			var bx = b.x,
					by = b.y,
					bz = b.z;
			this.x = ay * bz - az * by;
			this.y = az * bx - ax * bz;
			this.z = ax * by - ay * bx;
			return this;
		};

接下来看cross 的源码,该方法意为将this向量设置为它与传入向量所表示的平面集的法向量,且该方法是调用crossVectors 来实现的。

_proto.cross = function cross(v, w) {
	if (w !== undefined) {
		console.warn('THREE.Vector3: .cross() now only accepts one argument. Use .crossVectors( a, b ) instead.');
		return this.crossVectors(v, w);
	}
	return this.crossVectors(this, v);
};

2.3.9 基于伪随机值设置xyz值

方法用途
.random () : this将此向量的每个分量设置为介于0和1之间的伪随机值(不包括1)。

看下源码,就是随机一个点,该点的各个分量在0-1之间(不包括1是因为Math.random()返回的伪随机数是含0不含1的)。

_proto.random = function random() {
			this.x = Math.random();
			this.y = Math.random();
			this.z = Math.random();
			return this;
		};

2.3.10 在原向量方向的基础上改变向量长度(统一缩放)

方法用途
.setLength ( l : Float ) : this将该向量的方向设置为和原向量相同,但是长度(length)为l。

看下setLength 源码,其中normalize的作用是将向量转化为单位向量(即长度/模为1),multiplyScalar是上文有讲过的执行统一缩放的“乘法”运算,对单位向量执行尺度为length的统一缩放,可将向量的模变为length,推导如下:
在这里插入图片描述
综上所述,setLength 方法也可作为缩放使用,但并不限于此。

_proto.setLength = function setLength(length) {
			return this.normalize().multiplyScalar(length);
		};

2.3.10 设置为反向量

方法用途
.negate () : this向量取反,即: x = -x, y = -y , z = -z。

看下negate 源码,显然就是把this设置为反向量,如果对整个模型应用该方法,则模型会围着原点做翻转,即关于原点对称。

_proto.negate = function negate() {
			this.x = -this.x;
			this.y = -this.y;
			this.z = -this.z;
			return this;
		};

2.4 仅计算并返回数值,并不修改向量属性

2.4.1 计算向量与另一个向量间的角度

方法用途
.angleTo ( v : Vector3 ) : Float以弧度返回该向量与向量v之间的角度。

先看angleTo 源码。该方法意为,求两3维向量夹角,夹角可以这样理解,将垂直于这两个向量的向量当做旋转轴,两个向量围绕这根轴旋转,从一个向量旋转到另一个向量的角度,即为所求。

这里贴下求3维向量夹角cos值的公式:
在这里插入图片描述

_proto.angleTo = function angleTo(v) {
			var denominator = Math.sqrt(this.lengthSq() * v.lengthSq());
			if (denominator === 0) return Math.PI / 2;
			var theta = this.dot(v) / denominator; // clamp, to handle numerical problems
			return Math.acos(MathUtils.clamp(theta, -1, 1));
		};

看回源码,denominator 即为公式中分母的部分,lengthSq是求调用该函数的向量的模长的平方,所以还要再加个Math.sqrt开方,但实际上Vector3类给了我们直接求模长的方法length,而这里不适用length,因为我们要求两个向量的模长,如果对两个向量使用length方法,则内部会计算两次Math.sqrt+两次lengthSq,通过下面源码中的方法,则可以少计算一次Math.sqrt。

此外,denominator为0时,意为调用该函数的this向量,与传入的v向量之中存在0向量(即模为0的向量),而在数学上并未严格定义0向量与其余向量的夹角,在angleTo中,该角度被定义为90度(该函数返回的是弧度,因此是Math.PI / 2),注意π是180度。

dot为求this与传参的点积,对照上面公式可知,theta即为所求向量夹角的cos值。 因此,最后还要调用acos将其转换为角度。此外,MathUtils.clamp这个函数为判断theta是否在-1与1之间,如果 传参theta在最大值和最小值之间,则返回 theta的值,如果 theta 比最小值小,则返回 min 值,如果比最大值大,则返回 max 值。

2.4.2 计算向量与另一个向量间的距离

方法用途
.distanceTo ( v : Vector3 ) : Float计算该向量到所传入的v间的距离。
.manhattanDistanceTo ( v : Vector3 ) : Float计算该向量到所传入的v之间的曼哈顿距离(Manhattan distance)。
.distanceToSquared ( v : Vector3 ) : Float计算该向量到传入的v的平方距离。 如果你只是将该距离和另一个距离进行比较,则应当比较的是距离的平方, 因为它的计算效率会更高一些。

看下distanceToSquared 的源码,该方法是求两个点间距离的平方,其公式如下图:
在这里插入图片描述
推导方法如下(画的一般,意会一下):
在这里插入图片描述
理解公式后,distanceToSquared 的源码也很好理解了,不多赘述。

_proto.distanceToSquared = function distanceToSquared(v) {
			var dx = this.x - v.x,
					dy = this.y - v.y,
					dz = this.z - v.z;
			return dx * dx + dy * dy + dz * dz;
		};

接下来看distanceTo 的源码,可见它其实就是求开了方的distanceToSquared ,也即两点间的距离。这里给出了未开方,与开方的版本,意为在某些需求下提高计算效率,如表格中“用途”一栏所写,在仅比较距离长短时,无需开方也可满足需求,因此使用省略开方的方法可提高计算效率。

_proto.distanceTo = function distanceTo(v) {
			return Math.sqrt(this.distanceToSquared(v));
		};

接下来看manhattanDistanceTo 的源码,该方法为计算两向量间的曼哈顿距离,曼哈顿距离为两个点在标准坐标系上的绝对轴距的总和,看源码应该可以很好理解它的意思。(注:Math.abs为求绝对值)

_proto.manhattanDistanceTo = function manhattanDistanceTo(v) {
			return Math.abs(this.x - v.x) + Math.abs(this.y - v.y) + Math.abs(this.z - v.z);
		};

除此之外,threejs没有给出的方法还有切比雪夫距离,若以原点为基准,距离原点欧式距离为1的点可构成一个球,与原点曼哈顿距离为1的点可构成一个变长为根号二的旋转45度的立方体,而与原点切比雪夫距离为1的点可构成一个变长为2的立方体。

这三种不同的距离计算方式,通常具有不同的用途,根据需求挑选合适的方式即可。

2.4.3 计算向量的长度

方法用途
.length () : Float计算从(0, 0, 0) 到 (x, y, z)的欧几里得长度 (Euclidean length,即直线长度)
.manhattanLength () : Float计算该向量的曼哈顿长度(Manhattan length)。
.lengthSq () : Float计算从(0, 0, 0)到(x, y, z)的欧几里得长度 (Euclidean length,即直线长度)的平方。 如果你正在比较向量的长度,应当比较的是长度的平方,因为它的计算效率更高一些。

【2.4.3跟2.4.2差不多,可将长度理解为求调用函数的this点到原点间的距离。】

先看length与lengthSq 的源码,前者为求模,后者为求模的平方(即不开方)。跟2.4.2节类似,不开方的版本可用来在某些情况下减少开方的计算量。

	_proto.length = function length() {
			return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
		};
	_proto.lengthSq = function lengthSq() {
			return this.x * this.x + this.y * this.y + this.z * this.z;
};

接下来看manhattanLength 的源码,跟2.4.2节求原点与this的曼哈顿距离是一样的,不多赘述。

_proto.manhattanLength = function manhattanLength() {
			return Math.abs(this.x) + Math.abs(this.y) + Math.abs(this.z);
		};

2.4.4 计算两向量的点积

方法用途
.dot ( v : Vector3 ) : Float计算该vector和所传入v的点积(dot product)。

先介绍下点积,点积求出的是一个浮点数,其正负可代表两向量夹角是否大于90度,小于则正,大于则负,计算方法为各分量相乘再相加。

_proto.dot = function dot(v) {
			return this.x * v.x + this.y * v.y + this.z * v.z;
		}; // TODO lengthSquared?

也许可用来判断两模型是否背对,等等。

2.4.5 获取向量的属性(xyz值)

方法用途
.getComponent ( index : Integer ) : Float如果index值为0返回x值。如果index值为1返回y值。如果index值为2返回z值。
.toArray ( array : Array, offset : Integer ) : Array返回一个数组[x, y ,z],或者将x、y和z复制到所传入的array中。

先看下getComponent 源码,其实就是通过索引获取xyz值,完全可以this.x来获取。

_proto.getComponent = function getComponent(index) {
			switch (index) {
				case 0:
					return this.x;
				case 1:
					return this.y;
				case 2:
					return this.z;
				default:
					throw new Error('index is out of range: ' + index);
			}
		};

接下来看下toArray 源码。

该方法有两种使用途径,一种是直接调用,则会返回一个储存了this的xyz值的一维数组,其中包含三个元素,即xyz值。第二种是传入一个一维数组,并指示(或不指示则默认偏移为0)偏移量,则会从偏移量指示的下标开始,使用this的xyz值来修改传入数值的值。

其中,由于几何/几何体顶点的储存方法为一维数组,因此第二种使用方法也可理解为,(在传入数组为模型顶点的一维数组时)将模型中的一个顶点,移动至this向量所在处。

_proto.toArray = function toArray(array, offset) {
			if (array === void 0) {
				array = [];
			}
			if (offset === void 0) {
				offset = 0;
			}
			array[offset] = this.x;
			array[offset + 1] = this.y;
			array[offset + 2] = this.z;
			return array;
		};

2.5 其他

2.5.1 拷贝与克隆

方法用途
.clone () : Vector3返回一个新的Vector3,其具有和当前这个向量相同的x、y和z。
.copy ( v : Vector3 ) : this将所传入Vector3的x、y和z属性复制给这一Vector3。

先看clone 的源码,返回的是一个新的实例对象,而该对象与this的属性相同,类似于在this处新生成了一个向量。

_proto.clone = function clone() {
			return new this.constructor(this.x, this.y, this.z);
		};

再看copy的源码,可以看到并没有新的对象生成,仅仅是把传入的向量v的属性赋值给了this,类似于将this移动到了v处。

_proto.copy = function copy(v) {
			this.x = v.x;
			this.y = v.y;
			this.z = v.z;
			return this;
		};

2.5.2 判断相等性

方法用途
.equals ( v : Vector3 ) : Boolean检查该向量和v的严格相等性。

看下equals 源码:

_proto.equals = function equals(v) {
			return v.x === this.x && v.y === this.y && v.z === this.z;
		};

这里借用下其他人对两个等号与三个等号的定义:两个等号与三个等号
在这里插入图片描述
当this向量与传入向量严格相等时,返回true,否则返回false。

2.5.3 插值

方法用途
.lerp ( v : Vector3, alpha : Float ) : this在该向量与传入的向量v之间的线性插值,alpha是沿着线的长度的百分比 —— alpha = 0 时表示的是当前向量,alpha = 1 时表示的是所传入的向量v。
.lerpVectors ( v1 : Vector3, v2 : Vector3, alpha : Float ) : this将此向量设置为在v1和v2之间进行线性插值的向量, 其中alpha为两个向量之间连线的长度的百分比 —— alpha = 0 时表示的是v1,alpha = 1 时表示的是v2。

先介绍下何为插值,插值就是求中间值,通常用来计算从某点运动到另一点时,中间的过度点,可以以此来制作动画效果。

接下来看下lerp源码,应用该方法计算动画的过度路径,则模型的运动路径为沿着this向量到传入的v向量间的直线,因此也叫线性插值。其中,alpha为两点间的百分比,也可理解为模型运动到下一点的位置(或步长),起始点为this,目标点为传入的v,如果传入alpha为0.1,则朝着v走至this与v间距离的十分之一处。

(注意:该方法会修改this的值哦)

_proto.lerp = function lerp(v, alpha) {
			this.x += (v.x - this.x) * alpha;
			this.y += (v.y - this.y) * alpha;
			this.z += (v.z - this.z) * alpha;
			return this;
		};

接下来看下lerpVectors的源码,该方法与lerp的区别是,起始点不再是this,而是传入两个向量,一个代表起始点,一个代表目标点,要注意的是,默认v1为起始点,因此不要传反。其余与lerp相同,不再赘述。

_proto.lerpVectors = function lerpVectors(v1, v2, alpha) {
			this.x = v1.x + (v2.x - v1.x) * alpha;
			this.y = v1.y + (v2.y - v1.y) * alpha;
			this.z = v1.z + (v2.z - v1.z) * alpha;
			return this;
		};

补充:除了线性插值外还有球面插值,然而球面插值需要基于四元数来计算,因此球面插值的方法不在Vector3类中。

2.5.4 投影

方法用途
.project ( camera : Camera ) : this将向量从世界空间投影到相机的归一化设备坐标(NDC)空间中。
.projectOnPlane ( planeNormal : Vector3 ) : this通过从该向量中减去投影到平面法线上的向量,可以将该向量投影到平面上。
.projectOnVector ( v : Vector3 ) : this投影该向量到向量v上。
.unproject ( camera : Camera ) : this将该向量从相机的归一化设备坐标(NDC)空间投影到世界空间。

首先明确下投影,即投影到由变换矩阵(仿射矩阵)定义的空间内,其中,3x3变换矩阵与4x4仿射矩阵左上角的3x3部分,它们的列向量为目标空间的基,将该矩阵作用于某向量,则可使该向量由原空间映射到目标空间。

看下源码

_proto.project = function project(camera) {
			return this.applyMatrix4(camera.matrixWorldInverse).applyMatrix4(camera.projectionMatrix);
		};
_proto.unproject = function unproject(camera) {
			return this.applyMatrix4(camera.projectionMatrixInverse).applyMatrix4(camera.matrixWorld);
		};
_proto.projectOnVector = function projectOnVector(v) {
			var denominator = v.lengthSq();
			if (denominator === 0) return this.set(0, 0, 0);
			var scalar = v.dot(this) / denominator;
			return this.copy(v).multiplyScalar(scalar);
		};

		_proto.projectOnPlane = function projectOnPlane(planeNormal) {
			_vector.copy(this).projectOnVector(planeNormal);

			return this.sub(_vector);
		};

2.5.5 反射

方法用途
.reflect ( normal : Vector3 ) : this将此向量反射出与法线正交的平面。 假定法线具有单位长度。
_proto.reflect = function reflect(normal) {
			// reflect incident vector off plane orthogonal to normal
			// normal is assumed to have unit length
			return this.sub(_vector.copy(normal).multiplyScalar(2 * this.dot(normal)));
		};

3、方法的应用场景分析

缩放矩阵:如果各个维度缩放尺度不一样,则会形变,因此通常对模型仅使用尺度一致的缩放矩阵,这种三个轴缩放因子相等的情况,叫统一缩放
【备注下要写啥:】
可视化一下各个变换作用在模型上的效果
【注:这部分应该会在以后单开一节Vector3类的使用学习了,放在这里内容实在太多了】
以及!部分较难懂的方法的应用,已经简要的写在各个方法对应的小节里了,可暂时作为参考。

补充:里面投影,反射,以及应用法线矩阵这三块内容(共6个方法)没有讲解,因为我也还没有特别明白,一知半解下容易误人子弟,因此暂时跳过,以后会再补充。

【待续…】

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值