本篇將透過鏡頭追蹤實作,使得我們能夠建立一個第三人稱是角,捕捉拓海的蹤影。
鏡頭追蹤與飄移基本上綜合前面好幾篇多種原理組成。
鏡頭是網頁特效中重要的戰場。網頁由於CRUD的操作很多,所以很重視操作體驗。而鏡頭的視角又能綜合滑鼠做出多種效果。本篇以「追蹤」以及「飄移」作主軸示例,並且以智慧工廠AGV作為主題進行開發。
自動導引車(Automated guided vehicle)是智慧工廠常見領域。是這樣的:機器人如果能更有效率的在工廠執行任務(裝卸、災害應變、運送),將能增加工廠的效率。
目前已經有無人車的實際應用,其能夠判斷車輛四周環境判斷路徑與速度。而工廠的AGV也能夠透過光學雷達、內建的地圖來移動。不管路徑中有什麼障礙物,自動駕駛車仍能夠找到最佳路徑移動。
而在經過數位巒生之後,我們能夠透過網頁的儀表板觀察或模擬機具的狀況。而這樣的模擬,就需要前端網頁視覺特效的開發。而我們將專注在鏡頭開發中。
本篇使用網路資源引用機具模型,並且實作固定路線。這並不是AGV實際的模樣,但卻是很好的視覺化範本,同時也能提供給大家使用作為網頁作品。
Lerp
提升滑鼠體驗Camera.Target
)鏡頭追蹤
滑鼠飄移
同樣是上個範例,不過我這次放大了工廠的場景。
直接複製程式碼即可使用。
https://codepen.io/umas-sunavan/pen/KKRxaKN?editors=0010
AGV完成之後,我們可以幫鏡頭加上追蹤效果。這使我們在檢查AGV移動狀態時能夠讓鏡頭保持聚焦。
邏輯是這樣的:
首先,每幀偵測鏡頭是否距離車輛過遠。
如果過遠,那找出鏡頭與車輛相減後的向量,也就是下圖中的黃色。
每次車輛移動時,都能夠加上該向量,就可以更新鏡頭位置。
以下為程式碼:
const cameraFollowAvg = () => {
// 先判斷鏡頭跟AVG車輛是否過遠
const length = agv.position.distanceTo(camera.position)
if (length > 20) {
// 接續接下來的程式碼...
}
}
...
function animate() {
...
if (agv) {
// 在animate中每幀執行一次
cameraFollowAvg()
}
}
如果距離過遠,我們就取得攝影機跟車輛的距離。
這距離就成為接下來機具跟攝影機的距離。我們透過該距離找到鏡頭的新位置。
程式碼可以這樣寫:
// 取得鏡頭到AVG車輛的距離
const distance = new THREE.Vector3(0,0,0).subVectors(camera.position, agv.position)
// 將其距離轉成單位向量,使得我們能取得方向
distance.normalize()
// 取得方向之後再乘以固定數值。這麼一來就可以取得固定距離。
distance.multiplyScalar(20)
// 固定距離加在車輛上,就等於鏡頭的距離
distance.add(agv.position)
// 更新鏡頭位置
camera.position.set(...distance.toArray())
// 接續接下來的程式碼...
你可能會察覺:為什麼鏡頭都會往車輛的屁股移動?
這是因為,我們的鏡頭就像「快艇衝浪」一樣,被快艇的繩子牽著走。這使得鏡頭永遠都在物體正後方。
Lerp
提升滑鼠體驗為了避免這個狀況,我們可以平移鏡頭位置,並且固定鏡頭高度,如下:
// 固定鏡頭高度
distance.setY(15)
// 平移鏡頭位置,鏡頭永遠比車輛落差固定水平距離
distance.add(new THREE.Vector3(1,0,2))
// 用比較圓滑的方式位移
camera.position.lerp(distance, 0.1)
// 刪除這行 -> camera.position.set(...distance.toArray())
lerp()
是什麼?我們在第七天「Day7: three.js的一方通行:矢量操作——全面釐清向量與底層特性」有提供解釋。
如此一來,鏡頭就不會貼在車輛的正後方,同時還能維持一定高度。
const cameraLookAtAgv = () => {
// 設定鏡頭看向機具位置
control.target.set(...agv.position.toArray())
// 更新鏡頭控制
control.update()
}
function animate() {
...
if (agv) {
...
cameraLookAtAgv()
}
}
control.target
為什麼能夠使鏡頭看向機具位置?為什麼要透過update
更新鏡頭?我在第六天「Day6: three.js 圓弧的藝術家!弧線的教授!——軌道控制器」有提及,有興趣可以回顧。
反投影是什麼?
反投影unproject
是一個可以將NDC(Normalized Device Coordinate)轉換成three.js世界座標的函式。也就是說,它是用戶裝置螢幕上的位置,通常是滑鼠位置。而且長度為1,且原點在螢幕中心。
上圖中,上下界都是1,中心點在螢幕正中央。所以粉紅色的位置,就是中間正上方,座標為(0,1)。事實上,在Shader也是使用這個方法描述位置的。
無論你的螢幕是1920x1080,還是1024x768,NDC的(0,0)位置永遠代表螢幕正中央那顆像素,(0,1)永遠代表螢幕正中央上方的像素。
總之,只要把滑鼠位置用上述的NDC座標系統來描述位置,那three.js就可以把這個位置轉換成場景世界座標的位置。
const x //滑鼠x位置,範圍為-1~1,中心點為螢幕中央
const y //滑鼠y位置,範圍為-1~1,中心點為螢幕中央
const z //深度,-1為camera的near切面,1為far切面
const dnc = THREE.Vector3(x,y,z)
const worldPosition = dnc.unproject(camera)
// 變數dnc已經轉換成世界座標了
// 同時將回傳dnc自己給worldPosition
Z值的深度是什麼?
Z值描述它與鏡頭的遠近。如果設-1,那就會落在鏡頭的near平面,如果設1,就會落在鏡頭的far平面。
如果x與y設置為(0,1),也就是下圖中粉紅色位置。
而若z設為-1,那麼投影出來的的位置,會在下圖中綠色的點上。
相反的,如果我們把Z設定成1,那就會投影在藍色的點上。
將滑鼠位置轉成NDC
首先,我們透過mousemove
事件抓取到滑鼠位置,以及Canvas畫面大小。
// 宣告一個變數,儲存NDC資料
const mouseOnNdc = new THREE.Vector3(0,0,-1)
// renderer.domElement乃我們的canvas的DOM
renderer.domElement.addEventListener( 'mousemove' ,event => {
// 滑鼠X位置
const mouseX = event.offsetX
// 螢幕寬度
const w = renderer.domElement.width
// 滑鼠Y位置
const mouseY = event.offsetY
// 螢幕高度
const h = renderer.domElement.height
// 給定X與Y,Z留0就好
mouseOnNdc.setX(mouseX/w-0.5)
mouseOnNdc.setY(-mouseY/h+0.5)
})
上面的程式碼中,我們留了mouseOnNdc
變數,使得每幀執行時,可以取得mouseOnNdc
位置
將NDC轉成世界座標
準備好一個三維向量Vector3,其中X為NDC的X位置,Y為NDC的Y位置,Z則是你要投影到的「深度」。
// 宣告一個變數儲存世界座標
let mouseOnWorld = new THREE.Vector3(0,0,-1)
const updateMouseAffectTarget = () => {
// 為了避免可變物件(mutable)影響,使用clone()來使mouseOnNdc數值不會被更動
// clone的結果同時也會回傳給mouseOnFrustumTop
mouseOnWorld = mouseOnNdc.clone().unproject(camera)
}
function animate() {
...
if (agv) {
...
updateMouseAffectTarget()
}
}
如果滑鼠位置的NDC為(0,1),則反投影後會在橘色位置。只要能取得橘色到藍色(即鏡頭位置)的距離向量,就能用來偏移鏡頭的目標(Camera.target
)。
const updateMouseAffectTarget = () => {
mouseOnWorld = mouseOnDnc.clone().unproject(camera)
// 滑鼠在世界座標的位置跟鏡頭位置相減,得到的落差,就能製作偏移
const mouseOnWorldToCamera = new THREE.Vector3().subVectors(
mouseOnWorld,
camera.position)
.normalize();
}
將偏移加到camera.target
const updateMouseAffectTarget = () => {
mouseOnWorld = mouseOnDnc.clone().unproject(camera)
const mouseOnWorldToCamera = new THREE.Vector3().subVectors(
mouseOnWorld,
camera.position)
.normalize();
// 鏡頭目標的新位置即是車輛位置加上偏移
control.target.addVectors(mouseOnWorldToCamera.multiplyScalar(10), agv.position)
control.update()
}
完成之後,我們的滑鼠的位移將可以偏移鏡頭目標
偏移得太快速了,我們可以再透過lerp使他更圓滑。
為了使得位移更加圓滑,我加了兩個參數:
idealTarget
:為理想中移動的目標lerpingTarget
:朝向idealTarget
移動取得idealTarget
位置之後,即更新到control.target
上
// 理想要的鏡頭目標
let idealTarget = new THREE.Vector3(0,0,0)
// 漸變Lerp途中的鏡頭目標
let lerpingTarget = new THREE.Vector3(0,0,0)
const updateMouseAffectTarget = () => {
- control.target.addVectors(mouseOnWorldToCamera.multiplyScalar(10), agv.position)
- control.update()
+ idealTarget.addVectors(mouseOnWorldToCamera.multiplyScalar(10), agv.position)
+ lerpingTarget.lerp(idealTarget,0.1)
+ control.target.set(...lerpingTarget.toArray())
+ control.update()
}
這樣一來,鏡頭飄移跟追蹤就完成了。
https://codepen.io/umas-sunavan/pen/ZEoMMbq?editors=0010
在開發智慧工廠的各種元件時,最常遇到檢視的功能。畫面檢視的功能體驗如果很好,不僅在簡報、作品品質上都會有很大的加分。
本篇使用過去所提到的各種原理實作,希望能夠幫助大家處理各種鏡頭特效。
接下來將介紹WebGL的開發,敬請期待。