鏡面特效能夠非常有效的讓畫面更加豐富。不僅讓物件更有真實感,也可以創造出空間感。
上一篇示範過如何做出鏡面反射。而本篇將介紹倒影特效。
上一篇提到鏡面反射的原理,就是在物理裡面藏一個照相機,不斷拍攝四周的畫面,並產生材質貼圖,貼到物體上。倒影特效更加簡單:它只需要一個鏡頭拍出材質,它甚至不用實例化鏡頭,就能直接在材質中透過Shader解決掉。
本篇將透過工廠的倒影模擬,產生更加現實、豐富的畫面。
https://codepen.io/umas-sunavan/pen/MWGBmwK?editors=0011
在範本中,我準備了球形Mesh,作為環境貼圖,並加上了基本的光、渲染器等three.js的基本初始化設定。
我先製作了一個機櫃,機櫃貼圖來源為42U Rack Mount Cabinet Enclosure型號。
將模型讀入場景內。
// device作為閉包外存取模型的變數
let device
(async () => {
const path = 'https://storage.googleapis.com/umas_public_assets/michaelBay/day20/cabinet_mapping.gltf'
const gltf = await new GLTFLoader().loadAsync(path);
cabinet = gltf.scene
cabinet.scale.set(0.5,0.5,0.5)
scene.add(cabinet)
})()
...
function animate() {
...
// 幫機櫃加上旋轉
if (cabinet) {
cabinet.rotation.y +=0.01
}
}
加上去之後,因為機櫃顏色太深了,不適合示例。我把顏色材質貼圖拿掉。
(async () => {
...
// 巢狀遞迴所有子元件
gltf.scene.traverse( object => {
// 找尋Mesh物件(藉此捨棄掉鏡頭、光源等物件)
if (object.isMesh) {
// 將顏色材質圖拿掉
object.material.map = null
// 將法線材質圖拿掉
object.material.normalMap = null
}
})
})()
看起來清晰多了。
Reflector
(鏡面物件)的資源位置如果你在npm安裝three,或是直接用CDN抓取,都沒辦法直接抓取到Reflector
。
因為Reflector
物件並不存在於three
主程式中。事實上,它存在於three.js其中一個資料夾中:
./node_modules/three/examples/jsm/objects/Reflector
https://unpkg.com/three@latest/examples/jsm/objects/Reflector
為什麼會把元件放在examples
裡面呢?因為three.js
有很多社群擴充的元件。這些元件都會被放到examples
資料夾裡面,雖然這些都不在three.js的核心中,也不會出現在官方文件說明當中,但仍十分受用,被應用在很多地方。
Reflector
(鏡面物件)總而言之,將它加到專案中即可。
// 匯入module
import { Reflector } from 'https://unpkg.com/three@latest/examples/jsm/objects/Reflector';
const geometry = new THREE.PlaneGeometry(20, 20, 1, 1)
// 實例化Reflector
let mirror = new Reflector(geometry)
mirror.position.set(0,0,-20)
scene.add(mirror);
我們就可以看到鏡子了。
Reflector
提供很多參數給我們調整鏡面,以下說明:
// 參數物件
let options = {
clipBias: 0.03, // 鏡射多遠的距離
textureWidth: 1024, // 鏡射材質圖解析度
textureHeight: 1024, // 鏡射材質圖解析度
color: 0x889999, // 反射光的濾鏡
recursion: 0 // 反射可以反彈幾次
};
const geometry = new THREE.PlaneGeometry(20, 20, 1, 1)
// 放到Reflector參數中。
let mirror = new Reflector(geometry, options)
// 刪除這行 -> mirror.position.set(0,0,-10)
// 調整其面向
mirror.rotation.x = Math.PI * -0.5
scene.add(mirror);
clipBias
鏡射多遠的距離,數值如果設置太高,會裁掉鏡子裡的機櫃
textureWidth
& textureHeight
材質圖解析度。我們從上一篇得知鏡面反射的原理是材質圖。而這裡也是。解析度越低,取樣的越粗糙。
color
鏡子裡要帶有什麼顏色。你可以用這個參數來調整鏡子本身的顏色。除了調整顏色以外,也併用透明度與混合模式來達到鏡面物體的效果(如古人的銅鏡需保有顏色但又必須有鏡面效果)。
recursion
我特別喜歡這個效果。加上模型中的金屬材質跟粗糙材質,使得元件具有生命力。事實上,你可以看到鏡面中的反射光更加生動。
我們的鏡面就完成了。非常簡單。
當然,依照慣例我們還是會補充更多。
比如說好了,我們如果要把這種鏡面效果當作下圖中地板,那麼不能就像下圖那樣全部反射,這樣很假:
Reflector
的效果非常適合運用在鏡子、反光玻璃等場景下。但如果要讓地板光滑到可以反射機房中的機櫃,那就會有問題了。因為光滑地板並不會完全反射地面上的東西。
如果要做到逼真的地板,那麼就至少需要「淡化」鏡面的效果。方法有兩種:
調整Reflector
透明度,並再疊一個地板,使得地板可以比較「不反光」。
即使使用這個方法,鏡面仍然可以完整的反射物件,沒辦法漸淡。
疊兩個物件時,容易造成「Z Fighting」,必須再處理depth buffer
問題。
MeshReflectorMaterial
是three.js社群中的工程師開發的工具,並沒有列在官方文件,支援比較少。但如果你看完我接著即將撰寫的Shader系列文章,那麼其實你也改得了他做的元件,也不一定全都需要依賴社群。MeshReflectorMaterial
,事後才有人開發純javascript版本(本次示例)。這也是為什麼它叫做MeshReflectorMaterial
,因為這個是react-three-fiber擴充套件中的命名慣例。由於第一點比較簡單粗暴好理解,我們就先跳過它來示例比較少見但好用的MeshReflectorMaterial
目前目前使用Reflector
的效果:
這是接下來使用MeshReflectorMaterial
的效果:
需要準備三項東西:
CodePen工廠場景程式碼
MeshReflectorMaterial.js 腳本
安裝套件postprocessing
CodePen
MeshReflectorMaterial.js 腳本
除此之外,還要準備一份人家寫好的類別,其連結如下:
https://gist.github.com/0beqz/e69378b278e5c336afe0c7ae9b4ed86c
安裝postprocessing
安裝套件即可。
npm i postprocessing
淡化鏡面的原理,就是加上一層漸層。這個漸層中,比較靠鏡面的畫面比較不透明,而比較遠離鏡面的畫面比較透明。
透過這個方式,就可以呈現淡化的鏡面效果。
這個技術的問題在於,當用戶放大畫面時,會看到漸層範圍比較小了。當用戶縮小畫面時,會看到漸層範圍比較大。
重新確認一次,除了需要CodePen的程式碼以外,還需要MeshReflectorMaterial.js 腳本並安裝套件npm i postprocessing
。
程式碼中,我已經新增了工廠的廠房模型room
、四跟柱子column1~4
、很多個機櫃Cabinet
(以row跟column來排列)。
我這邊就不詳細解釋這物件實例化部份的實作,有興趣可以查看程式碼,我這邊就聚焦在漸淡倒影的實作。
這個最花時間,這個不得不說明。我這邊挑幾個重要的說明:
最重要的是minDepthThreshold
以及maxDepthThreshold
。
簡單來說,maxDepthThreshold
代表從多遠的地方開始淡出,而minDepthThreshold
代表到多遠的地方會淡出到沒畫面。
想像有一個透明漸層,100%是不透明,0%是透明。在鏡子裡面,距離鏡子最接近物件其鏡中反射應該是100%不透明,距離鏡子最遠的物件其鏡中反射應該是0%最透明。
minDepthThreshold
就代表鏡中反射最遠方的透明處,到底在0~1區間範圍的哪裡。
至於maxDepthThreshold
則控制最不透明處,從哪裡開始透明。其數值不限於0~1。
blur
鏡面是否需要高斯模糊。若是,那麼高斯模糊的材質解析度為何。([0,0]
代表沒有高斯模糊)
resolution
由於反射的原理是透過材質圖,貼在物件上面。因此需要設定材質圖的解析度。解析度越高效能越差。
reflectorOffset
鏡面跟物體中間是否要留一段距離才開始反射。
// 添加到程式碼
let fadingReflectorOptions = {
mixBlur: 2,
mixStrength: 1.5,
resolution: 2048, // 材質圖的解析度
blur: [0, 0], // 高斯模糊的材質解析度為何
minDepthThreshold: 0.7,// 從多遠的地方開始淡出
maxDepthThreshold: 2, // 到多遠的地方會淡出到沒畫面
depthScale: 2,
depthToBlurRatioBias: 2,
mirror: 0,
distortion: 2,
mixContrast: 2,
reflectorOffset: 0, // 鏡面跟物理中間是否要留一段距離才開始反射
bufferSamples: 8,
}
就如同其他物件一樣,透過傳入形狀geometry
跟材質material
來建立Mesh
物件。
不同的地方在於:Mesh
的材質被置換掉了。
// 腳本可以參考:[https://gist.github.com/0beqz/e69378b278e5c336afe0c7ae9b4ed86c](https://gist.github.com/0beqz/e69378b278e5c336afe0c7ae9b4ed86c)
import MeshReflectorMaterial from './MeshReflectorMaterial.js';
// 透過geometry以及material來建立Mesh物件
const geometry = new THREE.PlaneGeometry(60, 60, 1, 1)
const material = new THREE.MeshBasicMaterial()
const mesh = new THREE.Mesh(geo, mat)
// 將材質置換成MeshReflectorMaterial
mesh.material = new MeshReflectorMaterial(renderer, camera, scene, mesh, fadingReflectorOptions);
scene.add(mesh);
// 旋轉mesh角度以作為地面
mesh.rotateX(Math.PI * -0.5)
要注意的是,必須先實例化一個材質,再置換成MeshReflectorMaterial
。MeshReflectorMaterial
它需要傳入mesh
參數,如果沒有透過一個材質來實例化物件,那就沒辦法順利傳入參數。
由於鏡面是透過材質圖形成的,所以每幀必須不斷更新材質圖。
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
// 每幀更新鏡面材質圖
fadingGround.material.update()
}
做完之後,就可以看到鏡面的地面出來了。
有了地面還不夠,我們可以加上牆壁。首先我們將前面新增地面的程式碼包成函式。
const addFadingMirror = () => {
const geo = new THREE.PlaneGeometry(60, 60, 1, 1)
const mat = new THREE.MeshBasicMaterial()
const mesh = new THREE.Mesh(geo, mat)
mesh.material = new MeshReflectorMaterial(renderer, camera, scene, mesh, fadingReflectorOptions);
scene.add(mesh);
return mesh;
}
const fadingGround = addFadingMirror()
fadingGround.rotateX(Math.PI * -0.5)
接著,製作前後左右的牆壁。
const fadingWallZN = addFadingMirror()
fadingWallZN.translateX(0)
fadingWallZN.translateZ(-29)
const fadingWallZP = addFadingMirror()
fadingWallZP.translateZ(29)
fadingWallZP.rotateY(Math.PI)
const fadingWallXN = addFadingMirror()
fadingWallXN.rotateY(Math.PI * 0.5)
fadingWallXN.translateZ(-29)
const fadingWallXP = addFadingMirror()
fadingWallXP.translateX(29)
fadingWallXP.rotateY(Math.PI * -0.5)
function animate() {
...
fadingWallZN.material.update()
fadingWallZP.material.update()
fadingWallXN.material.update()
fadingWallXP.material.update()
}
這能夠創造出比較科幻的效果。
多虧three.js社群,才可以有這樣的reflector工具源源不絕的產出。事實上,我們是有辦法自製自己的鏡面特效了。過去的幾篇文章裡,我們不僅帶到向量計算、光的原理、陰影原理,這些都是製作特效的重要概念。
最後的九天我將會介紹WebGL的Shader,到時候我們可以釐清WebGL Shader的世界觀。到時候要製作自己的特效將輕而易舉。