iT邦幫忙

2022 iThome 鐵人賽

DAY 6
0
Software Development

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

Day6: three.js 圓弧的藝術家!弧線的教授!——OrbitControl軌道控制器

  • 分享至 

  • xImage
  •  

賽馬娘 弧線的教授 圓弧的藝術家 軌道控制器

圖片來源

每個場景都有OrbitControl,就好像每個賽道都有彎道一樣。要是你沒有Orbitcontrol,那就像你沒有「圓弧的藝術家」或是「弧線的教授」一樣,沒辦法在場景發揮作用。

身為一個3D場景,用戶最好可以用滑鼠控制鏡頭。如此一來就可以自由的選擇想要看的角度。如果沒辦法用滑鼠跟畫面互動,那就失去了網頁的意義了。

上一篇,我們用到OrbitControl控制鏡頭,今天我們針對它深入解析。

Control

OrbitControl是什麼?——它即是Control的一種,但Control又是什麼?——是我們對物體的控制,一般來說是控制鏡頭。

OrbitControl顧名思義,就是一個可以環繞中心點的控制鏡頭方式,所以名字才有Orbit。一般來說OrbitControl就可以滿足大部分的需求。

事實上還有很多種控制鏡頭的方式,下面介紹:

  • OrbitControls

    軌道控制,最常用。你的鏡頭在一個隱形的圓形的軌道中移動,它永遠面向場景中的一個點。預設原點。

  • ArcballControls

    弧球控制,比軌道控制難用一點的控制,差在可以360度旋轉鏡頭,使得你的鏡頭水平不平衡。

  • DragControls

    用來拖曳場景中的物件,鏡頭不會移動。

  • FirstPersonControls & FlyControls & PointerLockControls

    第一人稱視角,沒有軌道概念。

  • TrackballControls

    OrbitControls很像,可是當用戶把鏡頭繞過最頂端之後,並不會繞過頭,而TrackballControls則會。亦即:TrackballControls不會維持正Y軸為上,

  • TransformControls

    主要是作為控制物件,而非控制鏡頭的。

OrbitControl控制「目標」跟「鏡頭」

它可以控制鏡頭旋轉,但無論怎麼旋轉,鏡頭都看向目標(target) 本身。target是一個位置,描述著鏡頭所看向的中心點。

OrbitControl target rotate orbit

所以OrbitControl主要有兩個東西我們需要注意:

  1. OrbitControl:它會操控你的鏡頭。
    1. 你會修改自己的鏡頭位置,它也會。控制鏡頭為至的方式就是修改Camera.position
  2. OrbitControl.target:鏡頭所看向的目標物件,是一個位置資訊Vecro3
    1. OrbitControl會控制鏡頭面向target
    2. OrbitControl為了讓鏡頭面向target,它會修改camera.lookAt()
    3. 你不應該直接修改camera.lookAt(),應當由OrbitControl處理。
    4. 可是,OrbitControl不會在每幀渲染時自動控制,得用OrbitControl.update()更新。

好,我知道很複雜,看code最方便。我們看code就好:

實作

直接從上一篇的codePen拿來用

https://codepen.io/umas-sunavan/pen/ExLmgGm

首先,畫面旋轉好暈喔,先把旋轉關掉,並把不必要的東西拔掉,像是箭頭。

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

- // 建立四元數
- 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)
	requestAnimationFrame( animate );
	renderer.render( scene, camera );

}
animate();

以上移除了34~54除了48行外的程式碼。

實作:加上地球做為目標物件(非必要)

加上一顆地球方便我們看結果。其作法跟上篇天球的做法雷同。只是鏡頭活在天球內部、地球外部。

const earthGeometry = new THREE.SphereGeometry(5,50,50)
// 匯入材質
const earthTexture = new THREE.TextureLoader().load('2k_earth_daymap.jpeg')
// 帶入材質,設定內外面
const earthMaterial = new THREE.MeshStandardMaterial( { map: earthTexture, side: THREE.DoubleSide})
const earth = new THREE.Mesh(earthGeometry, earthMaterial);
scene.add(earth);

對了,我還修改了天球的名稱,這段我就不秀出來了。

我所使用的材質圖來源在此。並準備codePen提供大家運用

CodePen

https://codepen.io/umas-sunavan/pen/WNJOxxj

地球材質圖來源

實作:鏡頭上下移動 Pedestal Up/Down

事實上,你可以看到第15行我們其實就已經設定了鏡頭的位置。

// 已經存在的鏡頭位置設定
camera.position.set(0, 10, 15)

如果要讓鏡頭移動,可以在animate中不斷更新值

// 宣告旋轉變數
let rotation = 0

function animate() {
// 每幀更新旋轉變數
	rotation += 0.05
// 更新到位置
	camera.position.set(0,10 + Math.cos(rotation),15) // Math.cos的結果會在1~-1之間移動
	...
}
animate();

如此一來,你的鏡頭就在上下升降。

Pedestal Up, Pedestal Down, earth, camera

CodePen

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

實作:上下搖攝 Tilt Up/Down

它現在旋轉的是鏡頭的position,你也可以移動鏡頭所面對方向。

你可能有在官方文件看到lookAt()函式,它顧名思義就是旋轉鏡頭的方向,朝向所想的地方,於是這樣寫:

// 建立一個向量,以儲存鏡頭方向
const cameraLookAt = new THREE.Vector3(0,0,0)
let rotation = 0

function animate() {
	rotation += 0.05
-	camera.position.set(0,10 + Math.cos(rotation),15)
+	// 變化該向量
+	cameraLookAt.set(0,0 + Math.cos(rotation),0)
+	// 看向該向量
+	camera.lookAt(cameraLookAt)
	...
}

這樣也行。你的鏡頭會一直點頭,術語叫Tilt,中文翻譯是「上下搖攝」。

Tilt Up, Tilt Down, earth, camera

CodePen

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

你也可以讓鏡頭靜靜的指向某方,例如(10,10,10)。而不是一直移動:


// 建立一個向量,以儲存鏡頭方向
    const cameraLookAt = new THREE.Vector3(0,0,0)
-   let rotation = 0
    // 移動到animate()之外
    cameraLookAt.set(10,0,0)
    // 移動到animate()之外
    camera.lookAt(cameraLookAt)
    function animate() {
-       // 每幀更新旋轉變數
-       rotation += 0.05
-       // 變化該向量
-       cameraLookAt.set(0,0 + Math.cos(rotation),0)
-       // 看向該向量
-       camera.lookAt(cameraLookAt)
    }

當你玩一玩會發現一個問題:為什麼當滑鼠重新控制鏡頭時,會跳一下?

target, bug, lookAt, earth, camera

這就遇到一個問題了:target不正確

Target不正確問題

我提供了Codepen給大家看這問題:

CodePen

用滑鼠移動鏡頭看看,會發現鏡頭跳掉了。

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

所謂target不正確,意思是雖然你的鏡頭面向(指lookAt())了某個方向,但當你再用滑鼠操作時,它從lookAt()的方向重新回到target。你可能會問,lookAt()不就是看向target嗎?

其實不然,這是陷阱。

target問題:那lookAt() 到底是什麼?

如果我們追跟溯源,會看到lookAt()其實是Object3D的函式。Object3D就是Mesh, Group, Camera等物件的父類別,指的是:旋轉物體的方向朝向指定的向量。

也就是說,它只負責旋轉物件。

https://ithelp.ithome.com.tw/upload/images/20220921/20142505TSUSR3q4X7.png

你可能會問:旋轉鏡頭物件不就可以重新指向target嗎?

target問題:不應該用lookAt()旋轉鏡頭?

不應該。target其實是orbitControl所儲存一個向量,表示它所應該要看向的目標。而且在滑鼠事件出現之後,它就會自動執行函式OrbitControl.update() ,使得鏡頭可以看向目標。

所以,嚴格來說,我們「應當」要修改target的位置,使得OrbitControl成為鏡頭轉向的代理人,處理鏡頭轉向的部分,而不是直接用lookAt()修改鏡頭面向的地方。簡而言之就是:不要修改cameralookAt(),讓OrbitControl來處理,我們只要透過設定OrbitControl.target即可。

為什麼要這樣做呢?

原因藏在它的定義裡頭,你還記得OrbitControl的意思嗎?就是Orbit就是軌道。不是一般的軌道喔!是繞著某個物體旋轉的軌道,就像行星一樣。

*three.js有些物件有target(例如DirectionalLight),有些則無(如RectAreaLight)。有target就用,沒target那用lookAt()也OK的。

target, orbit, orbitcontrols, earth, camera

圖片來源:https://www.scientificamerican.com/podcast/episode/jupiter-and-venus-squeeze-earths-orbit/

target問題:OrbitControl的本質

Orbit就是繞著某個中心旋轉的軌道,OrbitControl就是繞著中心(target)旋轉的鏡頭,你不讓他朝向中心,那它要繞著誰旋轉?

所以我們總不能搶人家的飯碗,那是它存在的定義啊!

所以說,當我們需要修改位置時,可以執行orbitControl.update() 或者car.position.clone() ,這兩個都可以讓OrbitControl更新鏡頭位置。

在程式上,先將OrbitControls 儲存於一個變數。

- new OrbitControls( camera, renderer.domElement );
+ const control = new OrbitControls( camera, renderer.domElement );

接著,我們把lookAt() 等邏輯移除,改用control.target

- // 建立一個向量,以儲存鏡頭方向
- const cameraLookAt = new THREE.Vector3(0,0,0)
- cameraLookAt.set(10,0,0)
- camera.lookAt(cameraLookAt)
+ // 改用這個方法來控制鏡頭的方向
+ control.target.set(10,0,0)
+ control.update()

即可解決前面所提到的錯誤。

target, bug, lookAt, earth, camera

CodePen

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

總結:target跟lookAt的差異

  • 我們常誤用lookAt()來改變鏡頭面向,但此舉並無改變中心點target。導致用戶操作鏡頭時,OrbitControl變成預設的中心點0,0。
  • 改使用 orbitControl.target = car.position.clone() 就能移動中心點,即使在用戶操作鏡頭時,也能從中心點控制。

希望以上的整理能夠幫助大家。


上一篇
Day5: The World!砸瓦魯多!歐拉歐拉歐拉!——歐拉角跟四元數
下一篇
Day7: three.js的一方通行:矢量操作——全面釐清向量與底層特性
系列文
30天成為鍵盤麥可貝:前端視覺特效開發實戰31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言