iT邦幫忙

2022 iThome 鐵人賽

DAY 19
1
Software Development

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

Day19: three.js 前端3D鏡面特效開發實戰:梅利奧達斯的全反射!—智慧工廠檢視器

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20221005/20142505PF6Qo7Vjnc.png

圖片來源

七原罪中的梅利奧達斯有一招叫做「全反擊」,基本上可以把所有攻擊全部彈回去。招數有夠猛。而我們這次所做的特效也是相當兇猛,基本上用上,整個質感就不一樣了。不僅如此,原理還很簡單。以下就來介紹:

智慧工廠的特效應用

智慧工廠是我認為B2B當中最有趣的一部分,它需要很多領域互相合作。例如:

  1. 災害模擬、廠房氣體監測
    1. 在廠房空間中如果出現火災、有毒氣體外洩等,其二氧化碳飄逸速度為何?多久會遍布整個工廠?救援設備多久之後可以抵達?
  2. 自駕機具路徑與人工智慧
    1. 使用光學雷達偵測路徑以負責裝卸倉儲貨物的自駕機具,到添購多少台可以得到最佳效率?
  3. 檢查晶片瑕疵以確保產線良率
    1. 要多少鏡頭、用什麼的演算法可以抓出產線中零件的瑕疵,以提高生產線良率?
  4. 廠房倉儲、設備檢視與遠端操作
    1. 如何控制遠端的廠房機具,使得新增、刪除、修改零件得以自動或人工處理?

等相當多領域的應用。

本篇我們將以設備檢視作為主軸,並以此為切入點實作反射特效。

成品

rotate.gif

設備檢視是視覺特效相當能發揮的領域。基本上有了模型,我們可以幫它加上貼圖,使得它不僅只是設備的檢視器,還能增加產品質感、用戶體驗,跟競爭對手做出很大的區隔。

準備程式碼

我已經準備好程式碼。直接在CodePen上面複製即可。

CodePen

https://ithelp.ithome.com.tw/upload/images/20221005/20142505dTK8YNXbVM.png

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

除了three.js必備的物件以外,我還準備了、平行光、環境光、OrbitControls,以及一顆看得到內部但看不到外部的球體當作背景。都在程式碼裡面。

鏡面反射原理

為什麼鏡面會反射?

如果以現實世界來思考這件事情,這是因為光照在物體上,物體反射在鏡面上,鏡面捕捉光之後再反射回到眼中裡面。

https://ithelp.ithome.com.tw/upload/images/20221005/20142505NAhDErYtV8.png

如果有多個物件同時照在物體上,不就需要計算很多反射嗎?

https://ithelp.ithome.com.tw/upload/images/20221005/201425050VFpsTWVn6.png

但其實不然,發明鏡面反射效果的人很聰明:他讓相機放在物體裡面去捕捉周遭的畫面,來當作材質圖,貼在自己身上。

https://ithelp.ithome.com.tw/upload/images/20221005/20142505qOwArdfbfL.png

這個作法有點像是上一篇的陰影。「Day18: three.js 前端3D視覺特效開發實戰——鄉鎮市區GIS系統:陰影製作」我們提到陰影的製作方法。陰影也是把相機先放在光源的位置,然後拍一張照代表深度材質圖:

https://ithelp.ithome.com.tw/upload/images/20221005/20142505QRV6o4GQIW.png

鏡面反射原理跟陰影類似:把相機先放在光源的位置,拍一張照。但不同的是,相機是藏在物體裡面的,而不是在光裡面。而相機產生的貼圖要貼在物體上,而不是拿來計算陰影。

鏡面反射的四大步驟:

簡單來說,流程是這樣的:

  1. 準備一個照相機(CubeCamera) 跟渲染對象WebGLCubeRenderTarget
    • 渲染對象具有材質(.texture),用來儲存拍出來的照片。
  2. 照相機藏在物體中
    • 使用Mesh.add()函式
  3. 每幀拍一次照片,捕捉四周畫面成材質圖WebGLCubeRenderTarget.texture(但不照到所藏的物體本身)
  4. 材質圖貼在你想要的物體上形成鏡面反射
    • 貼的方法,就是把texutre指定給物理材質的環境貼圖(Material.envMap

https://ithelp.ithome.com.tw/upload/images/20221005/20142505crNE6bVdMJ.png

開發鏡面特效

開發鏡面特效:讀取3D模型。

// 使用閉包,以利程式碼同步
(async ()=>{
	// 模型檔案,拜託不要亂call我
	const path = 'https://storage.googleapis.com/umas_public_assets/michaelBay/day19/model/hard_disk_iron.gltf'
	// 實例化3D模型
	const gltf = await new GLTFLoader().loadAsync(path);
	// 將模型加到場景裡面
	scene.add(gltf.scene)
})()

這張圖是我在網路上找的硬碟圖片,我先將它建模成3D。

https://ithelp.ithome.com.tw/upload/images/20221005/20142505LFKNpYZyRg.jpg

圖片來源

https://ithelp.ithome.com.tw/upload/images/20221005/20142505i7McXtJec5.png

開發鏡面特效:準備一個照相機(CubeCamera) 跟渲染對象WebGLCubeRenderTarget

// 宣告照相機
let cubeCamera;
(async ()=>{
	const path = 'https://storage.googleapis.com/umas_public_assets/michaelBay/day19/model/hard_disk_iron.gltf'
	const gltf = await new GLTFLoader().loadAsync(path);
	// 用traverse巢狀遞迴子元件
	gltf.scene.traverse(object => {
		// 撇除非Mesh的物件
		if (!object.isMesh) return
		// 材質圖(嚴格來說是渲染對象)
		const cubeRenderTarget = new THREE.WebGLCubeRenderTarget(256, {
			// 渲染對象縮放設定
			generateMipmaps: true,
			// 渲染對象縮放設定
			minFilter: THREE.LinearMipmapLinearFilter,
		})
		// 實例化照相機,給定near, far,以及材質圖
		cubeCamera = new THREE.CubeCamera(0.1, 1000, cubeRenderTarget)
	})
	scene.add(gltf.scene)
})()

你可以看到有near、far。這兩個在介紹鏡頭時也介紹到,它就是鏡頭遠方跟近方的上下界。

https://ithelp.ithome.com.tw/upload/images/20221005/20142505rafZNIkaFV.png

開發鏡面特效:照相機藏在物體中

把camera裝在物體裡面即可。

(async ()=>{
	...
	gltf.scene.traverse(object => {
		...
		// 把camera裝在物體裡面即可
		object.add(cubeCamera)
	})
...
})()

每幀拍一次照片,捕捉四周畫面成材質圖

如果cubeCamera已經實例化,就每幀更新鏡頭。


function animate() {
	requestAnimationFrame(animate);
	renderer.render(scene, camera);
	// 如果cubeCamera已經實例化,就每幀更新鏡頭
+	if (cubeCamera) {
+		cubeCamera.update( renderer, scene );
+	}
}

材質圖貼在你想要的物體上形成鏡面反射

將材質圖貼在物件材質的envMap身上。何謂envMap?將在文末解釋。

(async ()=>{
	...
	gltf.scene.traverse(object => {
		...
		object.add(cubeCamera)
		// 將材質圖貼在物件材質的envMap身上
		object.material.envMap = cubeRenderTarget.texture
	})
...
})()

完成之後,就可以看到我們的模型。目前看起來什麼反射都沒有。

https://ithelp.ithome.com.tw/upload/images/20221005/20142505V4Mx2hONug.png

這是因為,我們周遭只有白光。所以他只反射白光。為了解決這個問題,我們加上HDRI。

加上HDRI

HDRI我將在文末花時間解釋。簡單來說就是一個環境圖片。現在讀取材質圖,加到場景中。

    const sphereGeometry = new THREE.SphereGeometry(50, 30, 30)
    // 產生紋理
+    const texutre = await new THREE.TextureLoader().loadAsync('https://storage.googleapis.com/umas_public_assets/michaelBay/day19/model/Warehouse-with-lights.jpg')
    // 將紋理貼到材質圖中
+    const sphereMaterial = new THREE.MeshStandardMaterial({ side: THREE.BackSide, color: 0xcceeff , map: texutre})
-    const sphereMaterial = new THREE.MeshStandardMaterial({ side: THREE.BackSide, color: 0xcceeff})
    const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial)
    sphere.position.set(0, 0, 0)
    scene.add(sphere);

Untitled

HDRI圖片來源

但還是沒有出現鏡面效果。為什麼?

去除光滑平面

是這樣的,理論上粗糙的平面(roughness1的平面)並不會產生反射。反射必須是光滑的平面才行。也因此,需要再加上程式碼。這點是很多人經常忘記的步驟。


(async ()=>{
	...
	gltf.scene.traverse(object => {
		...
		// 設定粗糙度為0
		object.material.roughness = 0
		// 順手加的,非金屬反射效果比較好
		object.material.metalness = 0
	})
...
})()
		

這個步驟結束後,鏡面反射就完成了!

成果

Untitled

CodePen

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

雖然作品剛剛完成,但重點是原理,以下介紹:

  1. 什麼是envMap
  2. CubeCamera怎麼照出360度照片的?
  3. 什麼是HDRI?
  4. Cube是什麼意思?

envMap是什麼?

envMap就是處理反射的貼圖。

事情是這樣的:鏡頭拍了材質圖,物件要如何把材質圖渲染在自己身上呢?

我們再再再回顧WebGL的原理:WebGL內含program,program內含vertexShader以及fragmentShader,物體中有多少錨點vertexShader就會渲染幾遍。渲染完之後將重要的數值傳給fragmentShader,fragmentShader再運算每一粒像素的RGB顏色。

也就是說,材質圖在傳到fragmentShader之後,fragmentShader會想辦法計算每一粒像素應該要如何計算出鏡面反射效果的RBG顏色。

鏡面反射計算的過程我們可以不用管,它們已經做好了一套。但你需要傳入材質圖使得它們計算鏡面反射得出顏色值。

給定envMap,就是提供鏡面材質圖以利後續計算。同理,給定normalMap就是提供向量材質以利向量計算,給定roughness就是提供光滑程度計算。

envMap流程

事實上,envMap得傳入六張材質圖,分別代表上下左右前後。當六張圖片都在入完之後,就會計算出mipmap。

mipmap是一系列的不同大小的材質圖片,一旦物體接近螢幕,代表物體得渲染成比較大,就使用解析度較高的圖片。一旦物體遠離螢幕,代表物體得渲染成比較小的像素,就使用比較小的材質圖片。這使得底層能夠快速渲染畫面。

CubeCamera怎麼照出360度照片的?

回顧陰影那篇(Day18: three.js 前端3D視覺特效開發實戰——鄉鎮市區GIS系統:陰影製作),點光PointLightShadow 之所以可以四面八方照出陰影,這是因為它準備六個相機,相機前後左右上下各一面,拼接成360度全景的陰影。

https://ithelp.ithome.com.tw/upload/images/20221005/20142505irkz7drd06.png

而事實上,CubeCamera也是同樣的邏輯:它有六個相機,上下左右前後各拍一張,最後拼成材質貼圖,貼到物體上。

什麼是HDRI

全名為High Dynamic Range Image,全景為360度的圖片。只用這種圖片張作貼圖,可以貼出你所需要的環境。基本上,你只需要把它貼到3D場景中的球體,然後放大球體,就可以做出360視角檢視的能力。

但通常會出現魚眼的情況,這使得必須調整鏡頭角度。

https://ithelp.ithome.com.tw/upload/images/20221005/20142505CDhXTWYsqI.png

透過調整fov可以修正此問題。

// 第一個參數為fov,調低可以調整鏡頭焦距,使得畫面能夠減少形變
const camera = new THREE.PerspectiveCamera(10, window.innerWidth / window.innerHeight, 0.1, 10000);

Untitled

Cube是什麼意思?

基本上,Cube 一詞指的就是上下左右前後六面。六面剛好可以組成一個立方體,而立方體的英文可稱作CubeCubeCamera因此得名。

以下物件都是同樣的邏輯:

  1. CubeCamera:分別負責上下左右前後六面的鏡頭

    這就是本文提到的。我們透過六個鏡頭,產生六張材質圖

  2. CubeMap:分別貼出上下左右前後的UV

    除了透過CubeCamera 拍攝以外,也可以預先準備好六面的材質圖。CubeMap就是指六面代表各方位的材質圖。基本上你可以將HDRI圖片轉成CubeMap再匯入到場景中。

    這個網站提供HDRI轉成CubeMap的功能。

    https://ithelp.ithome.com.tw/upload/images/20221005/20142505yJjT1SFd6w.png

    圖片來源

  3. CubeTexture:分別提供上下左右前後的材質圖

    當我們具有CubeMap之後,可以轉成CubeTexture 材質圖,而這個流程就像我們將圖片轉成材質圖一樣。只是它需要六張圖片。

    const loader = new THREE.CubeTextureLoader();
    loader.setPath( 'textures/cube/pisa/' );
    
    const textureCube = loader.load( [
    	'CubeMap正軸x.png', 'CubeMap負軸x.png',
    	'CubeMap正軸y.png', 'CubeMap負軸y.png',
    	'CubeMap正軸z.png', 'CubeMap負軸z.png'
    ] );
    
    const material = new THREE.MeshBasicMaterial( { color: 0xffffff, envMap: textureCube } );
    

    當然,也可以透過本文所提及的CubeCamera 所產生的六面材質圖:

    
    const cubeRenderTarget = new THREE.WebGLCubeRenderTarget(256, {...})
    const cubeTexture = cubeRenderTarget.texture
    const material = new THREE.MeshBasicMaterial( { color: 0xffffff, envMap: cubeTexture } );
    

上一篇
Day18: three.js GIS系統開發實戰:成為網頁特效的鹿丸!影子模仿術:陰影的終極原理
下一篇
Day20: three.js 前端3D視覺特效開發實戰——智慧工廠:倒影特效
系列文
30天成為鍵盤麥可貝:前端視覺特效開發實戰31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言