一、行主序、列主序
概念參考行主序 列主序
以線性代數中描述的矩陣為標準,行主序就是依次按行存儲,而列主序就是依次按列存儲。在threeJS中:
var A = new THREE.Matrix4();
A.set(1, 2, 3, 4,
5, 6, 7, 8,
9, 10, 11, 12,
13, 14, 15, 16);
console.log(A);
var B = new THREE.Matrix4();
B.set(16, 15, 14, 13,
12, 11, 10, 9,
8, 7, 6, 5,
4, 3, 2, 1);
console.log(B);
var C = new THREE.Matrix4();
C.multiplyMatrices (A, B);
console.log(C);
其運行結果為:
在網上找一個在線矩陣計算器,比如http://www.yunsuan.info/matrixcomputations/solvematrixmultiplication.html
相對應的計算結果如下:
因此可以認為,threejs矩陣內部儲存形式為列主序,表達和描述的仍然是線性代數中行主序,set()函數就是以行主序接受矩陣參數的。
二、如何根據變換設計自己的矩陣
概念性的東西,可以參考
線性代數筆記三 線性變換和矩陣乘法
圖形學筆記一 仿射變換和齊次坐標
1.向量或點的縮放平移等操作
這部分比較好處理,例子可以參考
three.js 之 Matrix
2.坐標系的轉化
馮樂樂講MVP的例子也很好,可以參考
UnityShader精要筆記二 數學基礎
核心思路就是以世界坐標為中轉,應用坐標的變換等價于基變換。
比如模型坐標系轉世界坐標系,就是模型空間任意一點計算出其在世界坐標系的位置。即模型每個點動了,整個模型也動了。
而世界坐標系轉觀察坐標系,則是先用觀察坐標系轉世界坐標系之后求逆,這樣快速運算。所以說,世界坐標系是用來中轉的,世界中心點不會動。
三、THREEJS封裝的矩陣API
1.平移
var vector = new THREE.Vector3(20, 20, 0);
var matrix = new THREE.Matrix4();
matrix.makeTranslation(10, 40, 0);
vector.applyMatrix4(matrix);
2.旋轉
matrix.makeRotationX(angle);
matrix.makeRotationY(angle);
matrix.makeRotationZ(angle);
matrix.makeRotationAxis(axis, angle);
matrix.makeRotationFromEuler(euler);
matrix.makeRotationFromQuaternion(quaternion);
前三個方法分別代表的是繞X、Y、Z三個軸旋轉,無需贅述。
第四個方法是前三個方法的整合版,第一個參數表示的是代表xyz的THREE.Vector3,第二個參數是旋轉的弧度。下面兩行代碼是等價的:
matrix.makeRotationX(Math.PI);
matrix.makeRotationAxis(new THREE.Vector3(1, 0, 0), Math.PI);
第五個方法表示圍繞x、y和z軸的旋轉,這是表示旋轉最常用的方式;第六個方法是一種基于軸和角度表示旋轉的替代方法。
關于旋轉,可以參考
Cocos 3.x 四元數 rotateAroundLocal
Three.js歐拉對象Euler和四元數Quaternion
構造函數:Euler(x,y,z,order)
參數xyz分別表示繞xyz軸旋轉的角度值,角度單位是弧度。參數order表示旋轉順序,默認值XYZ,也可以設置為YXZ、YZX等值
// 創建一個歐拉對象,表示繞著xyz軸分別旋轉45度,0度,90度
var Euler = new THREE.Euler( Math.PI/4,0, Math.PI/2);
四元數的方法.setFromAxisAngle(axis, angle)通過旋轉軸axis和旋轉角度angle設置四元數數據,也就是x、y、z和w四個分量。
var quaternion = new THREE.Quaternion();
// 旋轉軸new THREE.Vector3(0,1,0)
// 旋轉角度Math.PI/2
quaternion.setFromAxisAngle(new THREE.Vector3(0,1,0),Math.PI/2)
console.log('查看四元數結構',quaternion);
四元數乘法.multiply()
對象的一個旋轉可以用一個四元數表示,兩次連續旋轉可以理解為兩次旋轉對應的四元數對象進行乘法運算。
// 四元數q1、q2分別表示一個旋轉,兩個四元數進行乘法運算,相乘結果保存在q2中
// 在q1表示的旋轉基礎在進行q2表示的旋轉操作
q1.quaternion.multiply( q2 );
歐拉、四元數和矩陣轉化
歐拉對象、四元數對象和旋轉矩陣可以相關轉化,都可以表示旋轉變換。
//通過矩陣對象Matrix4的.makeRotationFromQuaternion(q)方法可以把四元數轉化對應的矩陣對象。
Matrix4.makeRotationFromQuaternion(q)
//通過歐拉對象設置四元數對象
quaternion.setFromEuler(Euler)
//四元數轉化為歐拉對象
Euler.setFromQuaternion(quaternion)
Object3D對象角度屬性.rotation的值是歐拉對象Euler,四元數屬性.quaternion的值是四元數對象Quaternion。
執行Object3D對象旋轉方法,會同時改變對象的角度屬性和四元數屬性。四元數屬性和位置.position、縮放屬性.scale一樣會轉化為對象的本地矩陣屬性.matrix,本地矩陣屬性值包含了旋轉矩陣、縮放矩陣、平移矩陣。
Object3D對象角度屬性.rotation和四元數屬性.quaternion是相互關聯的一個改變會同時改變另一個。
// 一個網格模型對象,基類是Object3D
var mesh = new THREE.Mesh()
// 繞z軸旋轉
mesh.rotateZ(Math.PI)
console.log('查看角度屬性rotation',mesh.rotation);
console.log('查看四元數屬性quaternion',mesh.quaternion);
3.compose
//使用make系列的方法操作
Object3D.applyMatrix(new THREE.Matrix4().makeScale(2,1,1));
Object3D.applyMatrix(new THREE.Matrix4().makeTranslation(0,4,0));
Object3D.applyMatrix(new THREE.Matrix4().makeRotationZ(Math.PI/6));
//使用compose方法操作
var matrix = new THREE.Matrix4();
var trans = new THREE.Vector3(0,4,0);
var rotat = new THREE.Quaternion().setFromEuler(new THREE.Euler(0,0,Math.PI/6));
var scale = new THREE.Vector3(2,1,1);
Object3D.applyMatrix4(matrix.compose(trans, rotat, scale)); //效果同上
就是compose的逆過程。隨便舉個例子。
var matrix = new THREE.Matrix4().set(1,2,3,4,2,3,4,5,3,4,5,6,4,5,6,7);
var trans = new THREE.Vector3();
var rotat = new THREE.Quaternion();
var scale = new THREE.Vector3();
matrix.decompose(trans, rotat, scale);
//返回Vector3 {x: 4, y: 5, z: 6} 因為是隨便寫的,所以只有平移變量不需計算就可以看出來的
console.log(trans);
//返回Quaternion {_x: 0.05565363763555474, _y: -0.11863820054057297
//, _z: 0.051265314875937947, _w: 0.7955271896092125}
console.log(rotat);
//返回Vector3 {x: 3.7416573867739413, y: 5.385164807134504, z: 7.0710678118654755}
console.log(scale);
如何通過矩陣設置Object3D對象位置呢,參考108 THREE.JS 使用矩陣對3D對象進行位置設置
//最后先將模型移動到中心位置
var inverseM = new THREE.Matrix4();
inverseM.getInverse(centerM);
matrix.multiply(inverseM);
//將矩陣賦值給模型
cube.matrix = matrix;
//使用矩陣更新模型的信息
cube.matrix.decompose(cube.position, cube.quaternion, cube.scale);
還有個注意點,就是threejs的decompose是X軸方向的負縮放,有可能與其它3D庫的matrix.decompose不同(比如是Z軸方向的負縮放)
測試的matrix數據如下:
rawData
(16) [5.053215498074303e-16, -1, 6.123233995736772e-17, 0, -1, -5.053215498074303e-16, -1.2246467991473532e-16, 0, -1.2246467991473535e-16, 6.123233995736766e-17, 1, 0, 1282.7820737078252, 3307.4240662137445, 3.128811373451107e-13, 1]
4.相乘
之前用過的matrix.multiplyMatrices(matrixA, matrixB),表示 將矩陣設置為matrixA * matrixB的結果。
threejs矩陣還有前乘和后乘的區別,也很容易混淆。
在threeJS中矩陣的后乘方法為multiply():
var A = new THREE.Matrix4();
A.set(1, 2, 3, 4,
5, 6, 7, 8,
9, 10, 11, 12,
13, 14, 15, 16);
var B = new THREE.Matrix4();
B.set(16, 15, 14, 13,
12, 11, 10, 9,
8, 7, 6, 5,
4, 3, 2, 1);
A.multiply(B);
console.log(A);
console.log(B);
其運行結果為:
表明A.multiply(B)
不會改變B的值,會改變A的值,相當于A=A*B
,即后乘方法multiply()的結果就是把傳入的參數放自己后面去乘。
反過來,使用前乘方法A.premultiply(B);
,結果就是B?A
5.逆矩陣
let m4 = new THREE.Matrix4();
m4.elements = [10, 8, 3, 0, 15, 7, 2, 0, 10, 6, 1, 0, 0, 0, 0, 1];
let m3 = new THREE.Matrix4();
//執行這行,會將m4的逆矩陣設置給m3
m3.getInverse(m4);
6.例子
參考Three.js中的矩陣,做出如圖的旋轉效果:
var box_geometry = new THREE.BoxGeometry();
var sphere_geometry = new THREE.SphereGeometry(0.5, 32, 32);
var cylinder_geometry = new THREE.CylinderGeometry(0.1, 0.1, 0.5);
var material = new THREE.MeshLambertMaterial({color: new THREE.Color(0.9, 0.55, 0.4)});
var box = new THREE.Mesh(box_geometry, material);
var sphere = new THREE.Mesh(sphere_geometry, material);
var cylinder = new THREE.Mesh(cylinder_geometry, material);
scene.add(box);
scene.add(sphere);
scene.add(cylinder);
box.matrixAutoUpdate = false;
sphere.matrixAutoUpdate = false;
cylinder.matrixAutoUpdate = false;
var sphere_matrix = new THREE.Matrix4().makeTranslation(0.0, 1.0, 0.0);
sphere_matrix.multiply(new THREE.Matrix4().makeRotationZ(-Math.PI * 0.25));
sphere.applyMatrix(sphere_matrix);
var cylinder_matrix = sphere_matrix.clone();
cylinder_matrix.multiply(new THREE.Matrix4().makeTranslation(0.0, 0.75, 0.0));
cylinder.applyMatrix(cylinder_matrix);
注意這個例子中只給了部分代碼,由于使用的是MeshLambertMaterial,需要添加光照才能看到幾何體,當然也可以換其它material:
// 環境光
const ambientLight = new THREE.AmbientLight(0xffffff, 1); // 創建環境光
this.scene.add(ambientLight); // 將環境光添加到場景
如果因為threejs版本問題,applyMatrix報undefined,改成applyMatrix4即可。
也可以參考這個例子,確認一下multiply的用法,上面有提到結論:
表明
A.multiply(B)
不會改變B的值,會改變A的值,相當于A=A*B
var sphere_matrix = new THREE.Matrix4().makeTranslation(0.0, 1.0, 0.0);
sphere.applyMatrix(sphere_matrix);
var cylinder_matrix = sphere_matrix.clone();
cylinder_matrix.multiply(new THREE.Matrix4().makeTranslation(0.0, 0.75, 0.0));
cylinder.applyMatrix(cylinder_matrix);
這段代碼和之前的功能是一樣的:
var box = new THREE.Mesh(box_geometry, material);
var sphere = new THREE.Mesh(sphere_geometry, material);
sphere.position.y += 1;
var cylinder = new THREE.Mesh(cylinder_geometry, material);
cylinder.position.y += 1.75;
scene.add(box);
scene.add(sphere);
scene.add(cylinder);
可以看出,cylinder_matrix使用multiply,在之前的平移矩陣上,再移動了0.75
但是,如果矩陣中還有其他值,使用乘法,都會改變:
let ttt = new THREE.Matrix4().set(1,2,3,4,2,3,4,5,3,4,5,6,4,5,6,7);
ttt.multiply(new THREE.Matrix4().makeTranslation(10,20,30));
console.log("ttt:",ttt.elements);
let ttt2 = new THREE.Matrix4().set(1,2,3,4,2,3,4,5,3,4,5,6,4,5,6,7);
ttt2.premultiply(new THREE.Matrix4().makeTranslation(10,20,30));
console.log("ttt2:",ttt2.elements);
結果:
ttt: (16) [1, 2, 3, 4, 2, 3, 4, 5, 3, 4, 5, 6, 144, 205, 266, 327]
ttt2: (16) [41, 82, 123, 4, 52, 103, 154, 5, 63, 124, 185, 6, 74, 145, 216, 7]
spher: (16) [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1] cylinder: (16) [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1.75, 0, 1]
7.Object3D.matrix和matrixWorld
仍然以UnityShader精要筆記二 數學基礎中MVP的例子:
就是有個奶牛叫妞妞,她有自己的坐標空間即模型空間,在這個空間里,她的鼻子坐標是(0,2,4),最后如何顯示在屏幕上呢?首先,轉化為齊次坐標(0,2,4,1)。頂點變換的第一步就是將頂點坐標從模型空間變換到世界空間,這個變換通常叫做模型變換(model transform)。根據Transform的信息,妞妞進行了(2,2,2)的縮放,(0,150,0)的旋轉以及(5,0,25)的平移。根據之前的知識,要先縮放再旋轉再平移:
我們使用代碼來驗證一下:
var geometry = new THREE.BoxGeometry(1, 1, 1);
var material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
var cube = new THREE.Mesh(geometry, material);
cube.scale.set(2, 2, 2);
cube.rotateY(150 * Math.PI / 180);
cube.position.set(5, 0, 25);
var vec = new THREE.Vector4(0, 2, 4, 1);
vec.applyMatrix4(cube.matrix);
console.log("vec:", vec);
現在打印出來的是0,2,4,1 這是因為matrix并沒有立即生效,可以手動調用cube.updateMatrix()
,關于更新的問題后面再說,現在先換一個打印方式:
function animate() {
requestAnimationFrame(animate);
// cube.rotation.x += 0.01;
// cube.rotation.y += 0.01;
renderer.render(scene, camera);
var vec = new THREE.Vector4(0, 2, 4, 1);
vec.applyMatrix4(cube.matrix);
console.log("vec:", vec);
}
//animate();
打印結果與例子中計算結果一致:vec: Vector4 {x: 9, y: 4, z: 18.07179676972449, w: 1}
這也說明,cube.matrix是由模型坐標系轉向其父容器世界坐標系的。現在繼續做測試,把cube再添加一個父容器:
var geometry = new THREE.BoxGeometry(1, 1, 1);
var material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
var cube = new THREE.Mesh(geometry, material);
cube.scale.set(2, 2, 2);
cube.rotateY(150 * Math.PI / 180);
cube.position.set(5, 0, 25);
var vec = new THREE.Vector4(0, 2, 4, 1);
vec.applyMatrix4(cube.matrix);
var cubeParent = new THREE.Object3D();
cubeParent.position.set(3, 0, 0);
cubeParent.add(cube);
scene.add(cubeParent);
然后打印的地方,把cube.matrix和cube.matrixWorld都打印:
function animate() {
requestAnimationFrame(animate);
// cube.rotation.x += 0.01;
// cube.rotation.y += 0.01;
renderer.render(scene, camera);
var vec = new THREE.Vector4(0, 2, 4, 1);
vec.applyMatrix4(cube.matrix);
console.log("vec:", vec);
console.log("matrix:", cube.matrix);
console.log("matrixWorld:", cube.matrixWorld);
}
顯然,能看出cube.matrixWorld是把嵌套的父容器也考慮進去,一步到位,直接轉到世界坐標系。
cube.modelViewMatrix
表示對象相對于相機坐標系的變換。也就是matrixWorld左乘相機的matrixWorldInverse。
但是,我打印一下,發現這個值不對:
那沒辦法,我們自己用矩陣乘法計算:
camera.rotateX(30 * Math.PI / 180);
camera.position.set(0, 10, -10);
function animate() {
requestAnimationFrame(animate);
// cube.rotation.x += 0.01;
// cube.rotation.y += 0.01;
renderer.render(scene, camera);
var vec = new THREE.Vector4(0, 2, 4, 1);
vec.applyMatrix4(cube.matrix);
console.log("vec:", vec);
console.log("matrix:", cube.matrix);
console.log("matrixWorld:", cube.matrixWorld);
var vec2 = new THREE.Vector4(0, 2, 4, 1);
let m = cube.matrixWorld.clone();
m.premultiply(camera.matrixWorldInverse);
vec2.applyMatrix4(m);
console.log("vec2:", vec2);
回到我們的農場游戲。現在我們需要把妞妞的鼻子從世界空間變換到觀察空間中。為此我們需要知道世界坐標系下攝像機的變換信息。這同樣可以通過攝像機面板中的Transform組件得到:(1,1,1)的縮放,(30,0,0)的旋轉,(0,10,-10)的平移。
可以看到結果與例子中的Z值是相反的,這是因為unity用的左手坐標系,而threejs是右手坐標系。
注,這里用的是premultiply,可以參考之前的結論:
表明A.multiply(B)不會改變B的值,會改變A的值,相當于A=A*B
8.camera相關的matrix
攝像機Cameras 有兩個額外的四維矩陣:
- Camera.matrixWorldInverse: 視圖矩陣 - 攝像機世界坐標變換的逆矩陣。
- Camera.projectionMatrix: 投影矩陣 - 表示將場景中的信息投影到裁剪空間。
9.makeScale會清理其它數據
let ttt = new THREE.Matrix4().set(1,2,3,4,2,3,4,5,3,4,5,6,4,5,6,7);
ttt.makeScale(2,2,2);
console.log("ttt:",ttt.elements);
let ttt2 = new THREE.Matrix4().set(1,2,3,4,2,3,4,5,3,4,5,6,4,5,6,7);
ttt2.makeTranslation(1,2,3);
console.log("ttt2:",ttt2.elements);
let ttt3 = new THREE.Matrix4().set(1,2,3,4,2,3,4,5,3,4,5,6,4,5,6,7);
ttt3.compose(new THREE.Vector3(2,2,2),new THREE.Quaternion(),new THREE.Vector3(1,2,3));
console.log("ttt3:",ttt3.elements);
結果如下:
ttt: (16) [2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1]
ttt2: (16) [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 2, 3, 1]
ttt3: (16) [1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 3, 0, 2, 2, 2, 1]
也就是說,如果不想清理之前的數據,可以使用compose進行一次性轉換。
10.更多的API
四、THREEJS來更新對象的變換
參考
three.js 之 Matrix
學習ThreeJS 04 更新機制
1.更改對象的位置,四元數,和伸縮屬性,three.js 會根據這些屬性重新計算對象的矩陣:
object.position.copy(start_position);
object.quaternion.copy(quaternion);
默認情況下,matrixAutoUpdate 屬性是設置為 true 的,矩陣會自動重新計算(如果它們已添加到場景中,或者是已添加到場景中的另一個對象的子節點)。
var object1 = new THREE.Object3D();
var object2 = new THREE.Object3D();
object1.add( object2 );
//object1 和 object2 會自動更新它們的矩陣
scene.add( object1 );
如果對象是靜態的,或者你希望自己手動控制什么時候重新計算,可以通過將屬性設置為 false 來獲取更好的性能。
object.matrixAutoUpdate = false
同時在改變任何屬性之后,手動更新矩陣:
object.updateMatrix();
2.直接修改對象的矩陣
object.matrix.setRotationFromQuaternion(quaternion);
object.matrix.setPosition(start_position);
object.matrixAutoUpdate = false;
注意在這種情況下 matrixAutoUpdate
必須設置成 false
。并且你要確定不要調用 updateMatrix
方法。調用 updateMatrix
會阻斷對矩陣的手動更改,會根據位置、伸縮等屬性重新計算矩陣。
3.matrixWorldNeedsUpdate
參考https://sogrey.top/Three.js-start/cores/#Object3D
matrixWorldNeedsUpdate : Boolean
當這個屬性設置了之后,它將計算在那一幀中的matrixWorld,并將這個值重置為false。默認值為false。
五、窗口 resize事件更新
- 發生場景:當窗口大小發生變化時,會出現局部空白區域。
- 解決方法:重新獲取瀏覽器窗口新的寬高尺寸,然后通過新的寬高尺寸更新相機Camera和渲染器WebGLRenderer的參數。
- 要注意一下,Three.js自適應渲染不一定就是窗口變化,本質上還是你要渲染的區域寬高尺寸變化了;更進一步變化是視圖矩陣.matrixWorldInverse和投影矩陣.projectionMatrix的變化。
// onresize 事件會在窗口被調整大小時發生
window.οnresize=function(){
// 重置渲染器輸出畫布canvas尺寸
renderer.setSize(window.innerWidth,window.innerHeight);
// 全屏情況下:設置觀察范圍長寬比aspect為窗口寬高比
camera.aspect = window.innerWidth/window.innerHeight;
// 渲染器執行render方法的時候會讀取相機對象的投影矩陣屬性projectionMatrix
// 但是不會每渲染一幀,就通過相機的屬性計算投影矩陣(節約計算資源)
// 如果相機的一些屬性發生了變化,需要執行updateProjectionMatrix ()方法更新相機的投影矩陣
camera.updateProjectionMatrix ();
};