iT邦幫忙

2022 iThome 鐵人賽

DAY 5
0
Software Development

30天成為鍵盤麥可貝:前端視覺特效開發實戰系列 第 5

Day5: The World!砸瓦魯多!歐拉歐拉歐拉!——歐拉角跟四元數

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20220920/2014250542U12mWD7w.jpg

圖片來源

當我們在場景空間裡面對物體旋轉時,它到底是怎麼旋轉的呢?又怎麼儲存旋轉資訊呢?這是因為Three.js召喚了替身——歐拉角與四元數!

它們代替我們在時間暫停下處理旋轉大小事,它是THE WOLRD!

每當我們單純的對物體旋轉時,就好像在時間暫停的時候召喚出歐拉角或是四元數,它們如此強大,導致我們在開發時,都忘記他們的存在。

現在我們就來介紹歐拉角跟四元數。

上一篇矩陣提升了我們對於形變的運用,讓它更簡單。但是「旋轉」仍然是一個難題。

天球問題

假如今天物體要旋轉的軸,既不是X軸,也不是Y軸,亦不是Z軸,它就是指著某一個方向作為軸心旋轉,那該如何實作?

我們舉生活的例子,天空中的北極星。

今天要做一個天球,天空是一個穹頂,天軸是北極星,北極星不在x,y或z=0的位置,那該如何旋轉?

這個單純用rotate.x, rotate.y, rotate.z很難滿足的。

天球

圖片來源

說到這裡,你可能有一個解法:

  1. 準備三個Mesh,一個包一個,呈現樹狀結構
  2. 第一個Mesh負責旋轉X,第二個Mesh負責旋轉Y,第三個Mesh負責旋轉Z。
  3. 最後只保留第三個Mesh呈現物體就好。

就像這樣:

Euler2a.gif

圖片來源

恭喜你,如果你有想到這個解法,看來你就是歐拉轉世。因為歐拉角就是用類似的概念,去描述所謂的旋轉。

要理解歐拉角,你以想像成三個轉軸所旋轉的角度。

這跟絕對位置不太一樣,歐拉角是相對的概念。

假如你是一個戰鬥機,你往北飛,突然你要往左上方飛,那到底有多麼左上呢?

首先,因為你要往上仰,所以你的飛機頭會拉升,產生俯仰(Pitch)的角度。

euler_pitch.gif

接著,因為你往左,所以會有偏航(Yaw)的角度。

euler_yaw.gif

最後,因為你往左飛,所以飛機也會往左傾斜,出現滾轉(Roll)。

euler_roll.gif

上面三張圖來自wiki.ogre3d.org

這三個軸向旋轉了一些角度,來完成「多麼」左上方這件事。

「飛機的方向一定是朝北嗎?所以這三個軸心就是X,Y,Z囉?」答案是:不知道。看你怎麼開飛機,不過以three.js來說,是以X,Y,Z這樣沒錯。

你可能會問:「誰開飛機會記得這樣搞啊?」對啊,你飛機像附圖那樣轉的話,你也差不多要墜機了。

在Three.js應用歐拉角

歐拉提出的歐拉角,設定三個軸的夾角,描述一個角度。

事實上,所有Mesh中的rotation就是用歐拉角描述的。如果你從three.js觀察它物件的屬性,你會看到物件的旋轉資訊,就是用歐拉角儲存的。

也就是說,MeshGroupCamera的旋轉,都是用歐拉角來儲存資訊的,就如同用向量來儲存位置、縮放資訊。你不用刻意實例化一個歐拉角來運用,直接使用物件裡的旋轉資料即可應用歐拉角。

我們看three.js官方文件,可以看到Object3DMesh, Group, Camera的父類別)的rotation即是一個歐拉角物件。你不用刻意用他,因為你前三個章節都一直用歐拉角來存取角度。

https://ithelp.ithome.com.tw/upload/images/20221017/20142505FSLJwvhBhs.png

歐拉物件幫助我們描述任何角度的旋轉。

歐拉物件也可以互相轉換,並包含幾個實用的函式:

  • setFromVector3() :向量轉成歐拉角。
  • setFromQuaternion() :歐拉角轉四元數。
  • set() :給定XYZ來轉歐拉角。
  • setFromRotationMatrix() :矩陣轉歐拉角 。

在Three.js應用四元數

四元數為四維空間的數,可以用在描述三維的旋轉。

這個影片可以深入了解:

https://eater.net/quaternions/video/intro

與歐拉角所表現的參數不同,四元數在應用上的概念是,描述一個向量,再以該軸旋轉一個角度。而這個方法,就不像歐拉角那樣選定三個軸的夾角。

Untitled

影片節錄自https://eater.net/quaternions/video/intro

而這個方法,就很適合天球的實作。

只要實例化四元數

let quaternion = new THREE.Quaternion()

即可透過這些方法,讓四元數儲存旋轉資料:

  • setFromAxisAngle:給定一個方向跟角度,它將依據方向為軸心,旋轉角度
  • setFromEuler:由歐拉角轉成四元數。這讓任何Mesh都可以轉成四元數
  • setFromRotationMatrix:由旋轉矩陣轉成四元數
  • set:由x,y,z轉成四元數

此外,還有非常實用的函式:

  • angleTo:傳入另一個四元數,它可以得出兩者夾角
  • dot:計算四維點積。
    • 順帶一提在三維向量(Vector3)中,dot()這非常好用,它是計算點積的意思,透過點積,可得知兩向量正交為0,而同方向為1,反方向為-1。前提是它們都是單位向量,亦即向量長度為1。

製作天球

拿上一篇的程式碼修改:

CodePen

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();

https://ithelp.ithome.com.tw/upload/images/20220920/20142505p9FmW0SMuf.png

製作天球:上貼圖

天球是有貼圖的。球的材質必須能夠貼貼圖。我們改一成MeshStandardMaterial,然後建立光源。

// 改成MeshStandardMaterial
- const material = new THREE.MeshNormalMaterial()
+ const material = new THREE.MeshStandardMaterial( { color: 0xffffff}) // 帶入顏色
// 新增環境光
const light = new THREE.AmbientLight(0xffffff,1)
scene.add(light)

我們找到星空的貼圖,貼到球面上。

https://ithelp.ithome.com.tw/upload/images/20220920/20142505cRVibaQ5oD.png

現在球是全白的,因為我們設定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)

https://ithelp.ithome.com.tw/upload/images/20221017/20142505rpTP0wGcaM.png
圖片來源

接著我們的天球就完成了。

製作天球:讓滑鼠控制鏡頭(非必要)

為了有更棒的體驗,可以加上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 );

現在可以透過滑鼠旋轉鏡頭了。

Untitled

製作天球:找出北極星(非必要)

為了找到北極星,我加了兩個東西:

  • 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的位置:

Untitled

有了這個北極星的位置,就可以把它當作四元數的軸心,讓天球以北極星為中心旋轉。

// 建立四元數
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)
	...
}

Untitled

這樣一來,我們不僅知道如何應用歐拉角以及四元數,還因此得到一個天球。

事實上,除了四元數很適合製作天球以外,四維矩陣的函式Matrix4.makeRotationAxis() 也可以給定一個軸跟角度來達成天球喔!

Untitled

實作這顆天球也引出了其他角色,例如OrbitControlsMeshStandardMaterialtexture

我們下一篇將繼續介紹OrbitControls

CodePen

天球HDRI來源為CC4.0,作者為kirriaa

https://codepen.io/umas-sunavan/pen/ExLmgGm?editors=1010

參考資料

歐拉角三個角度的解釋

最推薦的四元數介紹互動網站

通俗的介紹四元數

Three.js四元數旋轉

分解四元數


上一篇
Day4: Three.js 什麼!空間被扭曲了?我願稱你為最強——矩陣
下一篇
Day6: three.js 圓弧的藝術家!弧線的教授!——OrbitControl軌道控制器
系列文
30天成為鍵盤麥可貝:前端視覺特效開發實戰31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
Mizok
iT邦新手 3 級 ‧ 2022-09-21 03:10:08

四元數真的是很難懂的概念QQ
尤其對於大學沒有修過線代的人而言。(看yt看半天才看懂三成)

BTW,感覺寫得很精采XD
小弟我也有在這一屆寫Three.js的文(不過我是Morden Web組)
希望之後可以多多交流!

我要留言

立即登入留言