七原罪中的梅利奧達斯有一招叫做「全反擊」,基本上可以把所有攻擊全部彈回去。招數有夠猛。而我們這次所做的特效也是相當兇猛,基本上用上,整個質感就不一樣了。不僅如此,原理還很簡單。以下就來介紹:
智慧工廠是我認為B2B當中最有趣的一部分,它需要很多領域互相合作。例如:
等相當多領域的應用。
本篇我們將以設備檢視作為主軸,並以此為切入點實作反射特效。
設備檢視是視覺特效相當能發揮的領域。基本上有了模型,我們可以幫它加上貼圖,使得它不僅只是設備的檢視器,還能增加產品質感、用戶體驗,跟競爭對手做出很大的區隔。
我已經準備好程式碼。直接在CodePen上面複製即可。
https://codepen.io/umas-sunavan/pen/dyeKKJw
除了three.js必備的物件以外,我還準備了、平行光、環境光、OrbitControls,以及一顆看得到內部但看不到外部的球體當作背景。都在程式碼裡面。
如果以現實世界來思考這件事情,這是因為光照在物體上,物體反射在鏡面上,鏡面捕捉光之後再反射回到眼中裡面。
如果有多個物件同時照在物體上,不就需要計算很多反射嗎?
但其實不然,發明鏡面反射效果的人很聰明:他讓相機放在物體裡面去捕捉周遭的畫面,來當作材質圖,貼在自己身上。
這個作法有點像是上一篇的陰影。「Day18: three.js 前端3D視覺特效開發實戰——鄉鎮市區GIS系統:陰影製作」我們提到陰影的製作方法。陰影也是把相機先放在光源的位置,然後拍一張照代表深度材質圖:
鏡面反射原理跟陰影類似:把相機先放在光源的位置,拍一張照。但不同的是,相機是藏在物體裡面的,而不是在光裡面。而相機產生的貼圖要貼在物體上,而不是拿來計算陰影。
簡單來說,流程是這樣的:
CubeCamera
) 跟渲染對象WebGLCubeRenderTarget
.texture
),用來儲存拍出來的照片。Mesh.add()
函式WebGLCubeRenderTarget.texture
(但不照到所藏的物體本身)texutre
指定給物理材質的環境貼圖(Material.envMap
)// 使用閉包,以利程式碼同步
(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。
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。這兩個在介紹鏡頭時也介紹到,它就是鏡頭遠方跟近方的上下界。
把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
})
...
})()
完成之後,就可以看到我們的模型。目前看起來什麼反射都沒有。
這是因為,我們周遭只有白光。所以他只反射白光。為了解決這個問題,我們加上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);
但還是沒有出現鏡面效果。為什麼?
是這樣的,理論上粗糙的平面(roughness
為1
的平面)並不會產生反射。反射必須是光滑的平面才行。也因此,需要再加上程式碼。這點是很多人經常忘記的步驟。
(async ()=>{
...
gltf.scene.traverse(object => {
...
// 設定粗糙度為0
object.material.roughness = 0
// 順手加的,非金屬反射效果比較好
object.material.metalness = 0
})
...
})()
這個步驟結束後,鏡面反射就完成了!
https://codepen.io/umas-sunavan/pen/eYrKKKe?editors=0010
雖然作品剛剛完成,但重點是原理,以下介紹:
envMap
?CubeCamera
怎麼照出360度照片的?envMap
是什麼?envMap
就是處理反射的貼圖。
事情是這樣的:鏡頭拍了材質圖,物件要如何把材質圖渲染在自己身上呢?
我們再再再回顧WebGL的原理:WebGL內含program,program內含vertexShader以及fragmentShader,物體中有多少錨點vertexShader就會渲染幾遍。渲染完之後將重要的數值傳給fragmentShader,fragmentShader再運算每一粒像素的RGB顏色。
也就是說,材質圖在傳到fragmentShader之後,fragmentShader會想辦法計算每一粒像素應該要如何計算出鏡面反射效果的RBG顏色。
鏡面反射計算的過程我們可以不用管,它們已經做好了一套。但你需要傳入材質圖使得它們計算鏡面反射得出顏色值。
給定envMap
,就是提供鏡面材質圖以利後續計算。同理,給定normalMap就是提供向量材質以利向量計算,給定roughness就是提供光滑程度計算。
事實上,envMap得傳入六張材質圖,分別代表上下左右前後。當六張圖片都在入完之後,就會計算出mipmap。
mipmap是一系列的不同大小的材質圖片,一旦物體接近螢幕,代表物體得渲染成比較大,就使用解析度較高的圖片。一旦物體遠離螢幕,代表物體得渲染成比較小的像素,就使用比較小的材質圖片。這使得底層能夠快速渲染畫面。
CubeCamera
怎麼照出360度照片的?回顧陰影那篇(Day18: three.js 前端3D視覺特效開發實戰——鄉鎮市區GIS系統:陰影製作),點光PointLightShadow
之所以可以四面八方照出陰影,這是因為它準備六個相機,相機前後左右上下各一面,拼接成360度全景的陰影。
而事實上,CubeCamera
也是同樣的邏輯:它有六個相機,上下左右前後各拍一張,最後拼成材質貼圖,貼到物體上。
全名為High Dynamic Range Image,全景為360度的圖片。只用這種圖片張作貼圖,可以貼出你所需要的環境。基本上,你只需要把它貼到3D場景中的球體,然後放大球體,就可以做出360視角檢視的能力。
但通常會出現魚眼的情況,這使得必須調整鏡頭角度。
透過調整fov可以修正此問題。
// 第一個參數為fov,調低可以調整鏡頭焦距,使得畫面能夠減少形變
const camera = new THREE.PerspectiveCamera(10, window.innerWidth / window.innerHeight, 0.1, 10000);
基本上,Cube
一詞指的就是上下左右前後六面。六面剛好可以組成一個立方體,而立方體的英文可稱作Cube
,CubeCamera
因此得名。
以下物件都是同樣的邏輯:
CubeCamera
:分別負責上下左右前後六面的鏡頭
這就是本文提到的。我們透過六個鏡頭,產生六張材質圖
CubeMap
:分別貼出上下左右前後的UV
除了透過CubeCamera
拍攝以外,也可以預先準備好六面的材質圖。CubeMap
就是指六面代表各方位的材質圖。基本上你可以將HDRI圖片轉成CubeMap
再匯入到場景中。
這個網站提供HDRI轉成CubeMap
的功能。
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 } );