每個場景都有OrbitControl
,就好像每個賽道都有彎道一樣。要是你沒有Orbitcontrol
,那就像你沒有「圓弧的藝術家」或是「弧線的教授」一樣,沒辦法在場景發揮作用。
身為一個3D場景,用戶最好可以用滑鼠控制鏡頭。如此一來就可以自由的選擇想要看的角度。如果沒辦法用滑鼠跟畫面互動,那就失去了網頁的意義了。
上一篇,我們用到OrbitControl
控制鏡頭,今天我們針對它深入解析。
OrbitControl
是什麼?——它即是Control
的一種,但Control
又是什麼?——是我們對物體的控制,一般來說是控制鏡頭。
OrbitControl
顧名思義,就是一個可以環繞中心點的控制鏡頭方式,所以名字才有Orbit。一般來說OrbitControl
就可以滿足大部分的需求。
事實上還有很多種控制鏡頭的方式,下面介紹:
OrbitControls
:
軌道控制,最常用。你的鏡頭在一個隱形的圓形的軌道中移動,它永遠面向場景中的一個點。預設原點。
ArcballControls
:
弧球控制,比軌道控制難用一點的控制,差在可以360度旋轉鏡頭,使得你的鏡頭水平不平衡。
DragControls
:
用來拖曳場景中的物件,鏡頭不會移動。
FirstPersonControls
& FlyControls
& PointerLockControls
:
第一人稱視角,沒有軌道概念。
TrackballControls
:
跟OrbitControls
很像,可是當用戶把鏡頭繞過最頂端之後,並不會繞過頭,而TrackballControls
則會。亦即:TrackballControls
不會維持正Y軸為上,
TransformControls
:
主要是作為控制物件,而非控制鏡頭的。
它可以控制鏡頭旋轉,但無論怎麼旋轉,鏡頭都看向目標(target
) 本身。target是一個位置,描述著鏡頭所看向的中心點。
所以OrbitControl
主要有兩個東西我們需要注意:
OrbitControl
:它會操控你的鏡頭。
Camera.position
。OrbitControl.target
:鏡頭所看向的目標物件,是一個位置資訊Vecro3
。
OrbitControl
會控制鏡頭面向target
。OrbitControl
為了讓鏡頭面向target
,它會修改camera.lookAt()
。camera.lookAt()
,應當由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提供大家運用
https://codepen.io/umas-sunavan/pen/WNJOxxj
事實上,你可以看到第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();
如此一來,你的鏡頭就在上下升降。
https://codepen.io/umas-sunavan/pen/qBYjNRw?editors=1010
它現在旋轉的是鏡頭的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,中文翻譯是「上下搖攝」。
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不正確
我提供了Codepen給大家看這問題:
用滑鼠移動鏡頭看看,會發現鏡頭跳掉了。
https://codepen.io/umas-sunavan/pen/vYjZKmO?editors=1010
所謂target
不正確,意思是雖然你的鏡頭面向(指lookAt()
)了某個方向,但當你再用滑鼠操作時,它從lookAt()
的方向重新回到target
。你可能會問,lookAt()
不就是看向target
嗎?
其實不然,這是陷阱。
target
問題:那lookAt()
到底是什麼?如果我們追跟溯源,會看到lookAt()
其實是Object3D
的函式。Object3D
就是Mesh
, Group
, Camera
等物件的父類別,指的是:旋轉物體的方向朝向指定的向量。
也就是說,它只負責旋轉物件。
你可能會問:旋轉鏡頭物件不就可以重新指向target
嗎?
target
問題:不應該用lookAt()
旋轉鏡頭?不應該。target其實是orbitControl所儲存一個向量,表示它所應該要看向的目標。而且在滑鼠事件出現之後,它就會自動執行函式OrbitControl.update()
,使得鏡頭可以看向目標。
所以,嚴格來說,我們「應當」要修改target
的位置,使得OrbitControl
成為鏡頭轉向的代理人,處理鏡頭轉向的部分,而不是直接用lookAt()
修改鏡頭面向的地方。簡而言之就是:不要修改camera
的lookAt()
,讓OrbitControl
來處理,我們只要透過設定OrbitControl.target
即可。
為什麼要這樣做呢?
原因藏在它的定義裡頭,你還記得OrbitControl
的意思嗎?就是Orbit就是軌道。不是一般的軌道喔!是繞著某個物體旋轉的軌道,就像行星一樣。
*three.js有些物件有target
(例如DirectionalLight
),有些則無(如RectAreaLight
)。有target
就用,沒target
那用lookAt()
也OK的。
圖片來源:https://www.scientificamerican.com/podcast/episode/jupiter-and-venus-squeeze-earths-orbit/
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()
即可解決前面所提到的錯誤。
https://codepen.io/umas-sunavan/pen/JjvJKJV?editors=1010
lookAt()
來改變鏡頭面向,但此舉並無改變中心點target
。導致用戶操作鏡頭時,OrbitControl
變成預設的中心點0,0。orbitControl.target = car.position.clone()
就能移動中心點,即使在用戶操作鏡頭時,也能從中心點控制。希望以上的整理能夠幫助大家。