當我們在場景空間裡面對物體旋轉時,它到底是怎麼旋轉的呢?又怎麼儲存旋轉資訊呢?這是因為Three.js召喚了替身——歐拉角與四元數!
每當我們單純的對物體旋轉時,就好像在時間暫停的時候召喚出歐拉角或是四元數,它們如此強大,導致我們在開發時,都忘記他們的存在。
現在我們就來介紹歐拉角跟四元數。
上一篇矩陣提升了我們對於形變的運用,讓它更簡單。但是「旋轉」仍然是一個難題。
假如今天物體要旋轉的軸,既不是X軸,也不是Y軸,亦不是Z軸,它就是指著某一個方向作為軸心旋轉,那該如何實作?
我們舉生活的例子,天空中的北極星。
今天要做一個天球,天空是一個穹頂,天軸是北極星,北極星不在x,y或z=0的位置,那該如何旋轉?
這個單純用rotate.x, rotate.y, rotate.z很難滿足的。
說到這裡,你可能有一個解法:
就像這樣:
恭喜你,如果你有想到這個解法,看來你就是歐拉轉世。因為歐拉角就是用類似的概念,去描述所謂的旋轉。
要理解歐拉角,你以想像成三個轉軸所旋轉的角度。
這跟絕對位置不太一樣,歐拉角是相對的概念。
假如你是一個戰鬥機,你往北飛,突然你要往左上方飛,那到底有多麼左上呢?
首先,因為你要往上仰,所以你的飛機頭會拉升,產生俯仰(Pitch)的角度。
接著,因為你往左,所以會有偏航(Yaw)的角度。
最後,因為你往左飛,所以飛機也會往左傾斜,出現滾轉(Roll)。
上面三張圖來自wiki.ogre3d.org
這三個軸向旋轉了一些角度,來完成「多麼」左上方這件事。
「飛機的方向一定是朝北嗎?所以這三個軸心就是X,Y,Z囉?」答案是:不知道。看你怎麼開飛機,不過以three.js來說,是以X,Y,Z這樣沒錯。
你可能會問:「誰開飛機會記得這樣搞啊?」對啊,你飛機像附圖那樣轉的話,你也差不多要墜機了。
歐拉提出的歐拉角,設定三個軸的夾角,描述一個角度。
事實上,所有Mesh中的rotation就是用歐拉角描述的。如果你從three.js觀察它物件的屬性,你會看到物件的旋轉資訊,就是用歐拉角儲存的。
也就是說,Mesh
、Group
、Camera
的旋轉,都是用歐拉角來儲存資訊的,就如同用向量來儲存位置、縮放資訊。你不用刻意實例化一個歐拉角來運用,直接使用物件裡的旋轉資料即可應用歐拉角。
我們看three.js官方文件,可以看到Object3D
(Mesh
, Group
, Camera
的父類別)的rotation即是一個歐拉角物件。你不用刻意用他,因為你前三個章節都一直用歐拉角來存取角度。
歐拉物件幫助我們描述任何角度的旋轉。
歐拉物件也可以互相轉換,並包含幾個實用的函式:
setFromVector3()
:向量轉成歐拉角。setFromQuaternion()
:歐拉角轉四元數。set()
:給定XYZ來轉歐拉角。setFromRotationMatrix()
:矩陣轉歐拉角 。四元數為四維空間的數,可以用在描述三維的旋轉。
這個影片可以深入了解:
https://eater.net/quaternions/video/intro
與歐拉角所表現的參數不同,四元數在應用上的概念是,描述一個向量,再以該軸旋轉一個角度。而這個方法,就不像歐拉角那樣選定三個軸的夾角。
影片節錄自https://eater.net/quaternions/video/intro
而這個方法,就很適合天球的實作。
只要實例化四元數
let quaternion = new THREE.Quaternion()
即可透過這些方法,讓四元數儲存旋轉資料:
setFromAxisAngle
:給定一個方向跟角度,它將依據方向為軸心,旋轉角度setFromEuler
:由歐拉角轉成四元數。這讓任何Mesh都可以轉成四元數setFromRotationMatrix
:由旋轉矩陣轉成四元數set
:由x,y,z轉成四元數此外,還有非常實用的函式:
angleTo
:傳入另一個四元數,它可以得出兩者夾角dot
:計算四維點積。
dot()
這非常好用,它是計算點積的意思,透過點積,可得知兩向量正交為0,而同方向為1,反方向為-1。前提是它們都是單位向量,亦即向量長度為1。拿上一篇的程式碼修改:
https://codepen.io/umas-sunavan/pen/JjvNYwY
import * as THREE from 'three';
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 3, 15)
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild( renderer.domElement );
const geometry = new THREE.BoxGeometry(1,1,1)
const material = new THREE.MeshNormalMaterial({color: 0x0000ff})
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
// 錯誤:依序形變,這樣順序相反會有差別
cube.geometry.translate(5,0,0)
cube.geometry.scale(2,1,1)
function animate() {
// cube.rotation.y += 0.1
requestAnimationFrame( animate );
renderer.render( scene, camera );
}
animate();
先把cube改成sphere,讓他變成圓形。
import * as THREE from 'three';
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 3, 15)
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild( renderer.domElement );
// 改成球體
- const geometry = new THREE.BoxGeometry(1,1,1)
+ const geometry = new THREE.SphereGeometry(1,50,50)// 參數帶入半徑、水平面數、垂直面數
const material = new THREE.MeshNormalMaterial()
//改名為sphere
- const cube = new THREE.Mesh(geometry, material);
- scene.add(cube);
+ const sphere = new THREE.Mesh(geometry, material);
+ scene.add(sphere);
- cube.geometry.translate(5,0,0)
- cube.geometry.scale(2,1,1)
function animate() {
requestAnimationFrame( animate );
renderer.render( scene, camera );
}
animate();
天球是有貼圖的。球的材質必須能夠貼貼圖。我們改一成MeshStandardMaterial,然後建立光源。
// 改成MeshStandardMaterial
- const material = new THREE.MeshNormalMaterial()
+ const material = new THREE.MeshStandardMaterial( { color: 0xffffff}) // 帶入顏色
// 新增環境光
const light = new THREE.AmbientLight(0xffffff,1)
scene.add(light)
我們找到星空的貼圖,貼到球面上。
現在球是全白的,因為我們設定MeshStandardMaterial
為白色。
他除了可以帶入顏色作參數,也可以帶入貼圖作參數。
為了要呈現星空,必須帶入貼圖參數(texture
),並且由於Mesh預設只有單面可以呈現貼圖,背面不呈現。所以我們要在參數中設定球體雙面(內部跟外部)都可以看見貼圖 (side: THREE.DoubleSide
)。
// 匯入材質
const texture = new THREE.TextureLoader().load('/chapter3/free_star_sky_hdri_spherical_map_by_kirriaa_dbw8p0w.jpg')
// 帶入材質,設定內外面
- const material = new THREE.MeshStandardMaterial( { color: 0xffffff, map: texture, side: THREE.DoubleSide})
+ const material = new THREE.MeshStandardMaterial( { map: texture, side: THREE.DoubleSide})
現在,貼圖貼上去了,只是球太小,要放大一下。
+ const geometry = new THREE.SphereGeometry(100,50,50)
- const geometry = new THREE.SphereGeometry(1,50,50)
接著我們的天球就完成了。
為了有更棒的體驗,可以加上OrbitControls
,他讓你可以用滑鼠控制鏡頭。注意,它並不屬於three module本身,它位在/examples/jsm/controls/OrbitControls.js
。這個之後會解釋。
import { OrbitControls } from 'https://unpkg.com/three@latest/examples/jsm/controls/OrbitControls.js';
// 帶入鏡頭跟renderer.domElement實例化它即可
new OrbitControls( camera, renderer.domElement );
現在可以透過滑鼠旋轉鏡頭了。
為了找到北極星,我加了兩個東西:
axesHelper
顯示XYZ軸,防止我頭暈迷失方向。arrowHelper
一個箭頭,幫我指向北極星。// axesHelper
const axesHelper = new THREE.AxesHelper( 5 );
scene.add( axesHelper );
// arrowHelper
const dir = new THREE.Vector3(-2.49, 4.74, -3.01).normalize();
const origin = new THREE.Vector3( 0, 0, 0 );
const length = 10;
const hex = 0xffff00;
const arrowHelper = new THREE.ArrowHelper( dir, origin, length, hex );
scene.add( arrowHelper );
現在有了箭頭,我找到了北極星,北極星方向在-2.49, 4.74, -3.01
的位置:
有了這個北極星的位置,就可以把它當作四元數的軸心,讓天球以北極星為中心旋轉。
// 建立四元數
let quaternion = new THREE.Quaternion()
// 即將旋轉的弧度
let rotation = 0
// 由dir為軸心,rotation為旋轉弧度
quaternion.setFromAxisAngle( dir, rotation );
function animate() {
// 不斷增加弧度
rotation += 0.001
// 更新四元數
quaternion.setFromAxisAngle(dir, rotation)
// 增加的弧度,要更新在天球上
sphere.rotation.setFromQuaternion(quaternion)
...
}
這樣一來,我們不僅知道如何應用歐拉角以及四元數,還因此得到一個天球。
事實上,除了四元數很適合製作天球以外,四維矩陣的函式Matrix4.makeRotationAxis()
也可以給定一個軸跟角度來達成天球喔!
實作這顆天球也引出了其他角色,例如OrbitControls
、MeshStandardMaterial
、texture
。
我們下一篇將繼續介紹OrbitControls
。
https://codepen.io/umas-sunavan/pen/ExLmgGm?editors=1010
四元數真的是很難懂的概念QQ
尤其對於大學沒有修過線代的人而言。(看yt看半天才看懂三成)
BTW,感覺寫得很精采XD
小弟我也有在這一屆寫Three.js的文(不過我是Morden Web組)
希望之後可以多多交流!