iT邦幫忙

2022 iThome 鐵人賽

DAY 21
0
Software Development

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

Day21: three.js 智慧工廠開發實戰:Dejavu! 讓鏡頭跟著拓海一起飄移:鏡頭追蹤、飄移特效

  • 分享至 

  • xImage
  •  

本篇將透過鏡頭追蹤實作,使得我們能夠建立一個第三人稱是角,捕捉拓海的蹤影。

鏡頭追蹤與飄移基本上綜合前面好幾篇多種原理組成。

鏡頭是網頁特效中重要的戰場。網頁由於CRUD的操作很多,所以很重視操作體驗。而鏡頭的視角又能綜合滑鼠做出多種效果。本篇以「追蹤」以及「飄移」作主軸示例,並且以智慧工廠AGV作為主題進行開發。

什麼是AGV?

自動導引車(Automated guided vehicle)是智慧工廠常見領域。是這樣的:機器人如果能更有效率的在工廠執行任務(裝卸、災害應變、運送),將能增加工廠的效率。

目前已經有無人車的實際應用,其能夠判斷車輛四周環境判斷路徑與速度。而工廠的AGV也能夠透過光學雷達、內建的地圖來移動。不管路徑中有什麼障礙物,自動駕駛車仍能夠找到最佳路徑移動。

而在經過數位巒生之後,我們能夠透過網頁的儀表板觀察或模擬機具的狀況。而這樣的模擬,就需要前端網頁視覺特效的開發。而我們將專注在鏡頭開發中。

本篇使用網路資源引用機具模型,並且實作固定路線。這並不是AGV實際的模樣,但卻是很好的視覺化範本,同時也能提供給大家使用作為網頁作品。

本篇內容

  • 準備程式碼
  • 鏡頭追蹤
    1. 偵測鏡頭是否要追蹤物件
    2. 維持鏡頭最佳方向
    3. 透過Lerp提升滑鼠體驗
    4. 鏡頭看向車輛
  • 鏡頭飄移
    1. 反投影原理
    2. 偵測滑鼠位置,「反投影」到世界空間
    3. 修改鏡頭目標位置(Camera.Target

完成品

  • 鏡頭追蹤

    Untitled

  • 滑鼠飄移

    Untitled

準備程式碼

同樣是上個範例,不過我這次放大了工廠的場景。

直接複製程式碼即可使用。

CodePen

https://ithelp.ithome.com.tw/upload/images/20221007/20142505ykrnwhM1kh.png

https://codepen.io/umas-sunavan/pen/KKRxaKN?editors=0010

鏡頭追蹤

AGV完成之後,我們可以幫鏡頭加上追蹤效果。這使我們在檢查AGV移動狀態時能夠讓鏡頭保持聚焦。

偵測鏡頭是否要追蹤物件

邏輯是這樣的:

  1. 首先,每幀偵測鏡頭是否距離車輛過遠。

  2. 如果過遠,那找出鏡頭與車輛相減後的向量,也就是下圖中的黃色。

    https://ithelp.ithome.com.tw/upload/images/20221007/20142505X8yh39lCMS.png

  3. 每次車輛移動時,都能夠加上該向量,就可以更新鏡頭位置。

    https://ithelp.ithome.com.tw/upload/images/20221007/20142505yMKt0IbL5Q.png

以下為程式碼:

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())
// 接續接下來的程式碼...

Untitled

維持鏡頭最佳方向

你可能會察覺:為什麼鏡頭都會往車輛的屁股移動?

這是因為,我們的鏡頭就像「快艇衝浪」一樣,被快艇的繩子牽著走。這使得鏡頭永遠都在物體正後方。

https://ithelp.ithome.com.tw/upload/images/20221007/201425051QIhauJXQl.png

透過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的一方通行:矢量操作——全面釐清向量與底層特性」有提供解釋。

如此一來,鏡頭就不會貼在車輛的正後方,同時還能維持一定高度。

Untitled

鏡頭看向車輛

const cameraLookAtAgv = () => {
	// 設定鏡頭看向機具位置
	control.target.set(...agv.position.toArray())
	// 更新鏡頭控制
	control.update()
}

function animate() {
	...
	if (agv) {
		...
		cameraLookAtAgv()
	}

}

control.target為什麼能夠使鏡頭看向機具位置?為什麼要透過update更新鏡頭?我在第六天「Day6: three.js 圓弧的藝術家!弧線的教授!——軌道控制器」有提及,有興趣可以回顧。

Untitled

鏡頭飄移

反投影原理

  • 反投影是什麼?

    反投影unproject 是一個可以將NDC(Normalized Device Coordinate)轉換成three.js世界座標的函式。也就是說,它是用戶裝置螢幕上的位置,通常是滑鼠位置。而且長度為1,且原點在螢幕中心。

    上圖中,上下界都是1,中心點在螢幕正中央。所以粉紅色的位置,就是中間正上方,座標為(0,1)。事實上,在Shader也是使用這個方法描述位置的。

    無論你的螢幕是1920x1080,還是1024x768,NDC的(0,0)位置永遠代表螢幕正中央那顆像素,(0,1)永遠代表螢幕正中央上方的像素。

    https://ithelp.ithome.com.tw/upload/images/20221007/20142505IuVIGdXbHC.png

    總之,只要把滑鼠位置用上述的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平面。

    https://ithelp.ithome.com.tw/upload/images/20221007/20142505PNGeKS5ARA.png

    https://ithelp.ithome.com.tw/upload/images/20221007/20142505NpZ2Qv8cj7.png

    如果x與y設置為(0,1),也就是下圖中粉紅色位置。

    https://ithelp.ithome.com.tw/upload/images/20221007/20142505zS3XOWd1uY.png

    而若z設為-1,那麼投影出來的的位置,會在下圖中綠色的點上。

    https://ithelp.ithome.com.tw/upload/images/20221007/20142505OKqsbNVRcu.png

    相反的,如果我們把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)。

    https://ithelp.ithome.com.tw/upload/images/20221007/20142505BT9U2sWIz9.png

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

    完成之後,我們的滑鼠的位移將可以偏移鏡頭目標

    Untitled

  • 偏移得太快速了,我們可以再透過lerp使他更圓滑。

    為了使得位移更加圓滑,我加了兩個參數:

    1. idealTarget :為理想中移動的目標
    2. lerpingTarget:朝向idealTarget移動

    取得idealTarget 位置之後,即更新到control.target

    https://ithelp.ithome.com.tw/upload/images/20221007/201425053XE7dy3K9G.png

    // 理想要的鏡頭目標
    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://ithelp.ithome.com.tw/upload/images/20221007/20142505lQ33eZuV03.png

Untitled

CodePen

https://codepen.io/umas-sunavan/pen/ZEoMMbq?editors=0010

小結

在開發智慧工廠的各種元件時,最常遇到檢視的功能。畫面檢視的功能體驗如果很好,不僅在簡報、作品品質上都會有很大的加分。

本篇使用過去所提到的各種原理實作,希望能夠幫助大家處理各種鏡頭特效。

接下來將介紹WebGL的開發,敬請期待。

參考資料

機具模型檔案

unproject z值討論

unproject解釋


上一篇
Day20: three.js 前端3D視覺特效開發實戰——智慧工廠:倒影特效
下一篇
Day22: WebGL Shader—你好啊大哥哥,沒想到你可以到Shader來呢!
系列文
30天成為鍵盤麥可貝:前端視覺特效開發實戰31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言