Three.js 之 19 realistic render 真實渲染
我正在參加「掘金·啟航計劃」
本系列為 Three.js journey 教程學習筆記。包含以下內容
- Three.js 之 1 Animation 動畫
- Three.js 之 2 Camera 相機
- Three.js 之 3 畫布與全屏
- Three.js 之 4 Geometry 幾何體
- Three.js 之 5 debug UI
- Three.js 之 6 Texture 紋理
- Three.js 之 7 Materials 材質
- Three.js 之 8 炫酷的 3D Text
- Three.js 之 9 Light 光
- Three.js 之 10 Shadow 投影
- Three.js 之 11 Haunted House 恐怖鬼屋
- Three.js 之 12 Particles 粒子效果
- Three.js 之 13 Galaxy 銀河效果生成器
- Three.js 之 14 Raycaster 光線投射
- Three.js 之 15 Scroll based animation 基於頁面滾動的動畫
- Three.js 之 16 Physics 物理引擎
- Three.js 之 17 Import Model 匯入模型
- Three.js 之 18 使用 Blender 設計和匯出模型
- Three.js 之 19 realistic render 真實渲染
未完待續
我們上一節最後將漢堡模型匯入到了 Three.js 的場景中了,但是顏色效果很奇怪。為了讓它渲染的更真實,我們需要做一些額外的操作,接下來就一起看看吧~
我們匯入複雜一點模型進行展示,我們匯入之前瞭解過的飛行員頭盔。
模型匯入
```js import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
// ...
/* * Loaders / const gltfLoader = new GLTFLoader()
// ...
/* * Models / gltfLoader.load('../assets/models/FlightHelmet/glTF/FlightHelmet.gltf', (gltf) => { gltf.scene.scale.set(8, 8, 8) gltf.scene.position.set(0, -3.4, 0) gltf.scene.rotation.set(0, Math.PI * 0.5, 0) scene.add(gltf.scene) }) ```
再加一些燈光
js
const directionLight = new THREE.DirectionalLight('#ffffff', 3)
directionLight.position.set(0.25, 3, -2.25)
scene.add(directionLight)
真實渲染優化
接下來我們做一些真實渲染的優化操作
physicallyCorrectLights 物理上正確的光照模式
我們將切換到使用物理正確的照明強度計算。物理正確的照明與基於物理的渲染不是一回事,但是,將兩者同時使用以便給我們一個完整的物理準確的場景是有意義的。
物理正確的照明意味著使用真實世界的物理方程計算光線如何隨著距離光源(衰減)而衰減。這是相當簡單的計算,你可以在任何物理教科書找到這些方程。
另一方面,基於物理的渲染涉及以物理正確的方式計算光線與表面的反應。這些方程要複雜得多。幸運的是,我們不必完全理解原理就可以使用它們!
只需要如下設定
js
renderer.physicallyCorrectLights = true
效果如下 physicallyCorrectLights false | physicallyCorrectLights true --- | --- |
環境貼圖
目前我們只有微弱的平行光,所以可能看不到陰影裡的內容。我們現在加一些環境貼圖看看效果。
我們新增如下效果的環境貼圖
並環境貼圖新增至場景中
```js /* * Loaders / const cubeTextureLoader = new THREE.CubeTextureLoader()
/* * Environment map / const environmentMap = cubeTextureLoader.load([ '../assets/textures/environmentMaps/3/px.jpg', '../assets/textures/environmentMaps/3/nx.jpg', '../assets/textures/environmentMaps/3/py.jpg', '../assets/textures/environmentMaps/3/ny.jpg', '../assets/textures/environmentMaps/3/pz.jpg', '../assets/textures/environmentMaps/3/nz.jpg', ])
scene.background = environmentMap // 將環境貼圖新增至場景中 ```
遍歷模型中的 Mesh,並新增材質的環境貼圖,將環境貼圖強度設定為 2.5
```js const debugObject = { envMapIntensity: 2.5, } /* * Update all materials / const updateAllMaterials = () => { scene.traverse((child) => { if (child instanceof THREE.Mesh && child.material instanceof THREE.MeshStandardMaterial) { console.log(child) child.material.envMap = environmentMap child.material.envMapIntensity = debugObject.envMapIntensity } }) }
gui.add(debugObject, 'envMapIntensity').min(0).max(10).step(0.001) .onChange(updateAllMaterials)
/* * Models / gltfLoader.load('../assets/models/FlightHelmet/glTF/FlightHelmet.gltf', (gltf) => { // ... updateAllMaterials() })
```
無環境貼圖 | 有環境貼圖 --- | --- |
Renderer 渲染器相關擬真優化
物體現在越來越真實,但還是有些不自然,我們需要處理一下顏色相關的部分,可以控制 WebGLRenderer 的一些屬性實現。
outputEncoding 渲染器的輸出編碼
.outputEncoding : number
定義渲染器的輸出編碼。預設為 THREE.LinearEncoding
THREE.LinearEncoding
THREE.sRGBEncoding
THREE.BasicDepthPacking
THREE.RGBADepthPacking
這些常量用於紋理的encoding屬性。
推薦使用的是 sRGBEncoding
js
renderer.outputEncoding = THREE.sRGBEncoding
對比效果
outputEncoding LinearEncoding | outputEncoding sRGBEncoding --- | --- |
可以看出紋理變得更加明亮了,這些紋理也會影響環境貼圖。
另一個可以調整的值是 THREE.GammaEncoding。這種編碼的優點是可以讓你使用一個叫做 gammaFactor 的值,它的作用有點像亮度,但我們不會使用這個。
Gamma 編碼是一種儲存顏色的方法,同時根據人眼敏感度優化儲存明暗值的方式。當我們使用 sRGBEncoding 時,就像使用 GammaEncoding 一樣,預設 gamma 因子為 2.2,這是常用值。
雖然有些人可能認為 GammaEncoding 比 sRGBEncoding 更好,因為我們可以控制更暗或更亮場景的 gamma 因子,但這在物理上看起來並不正確,我們稍後會看到如何以更好的方式管理“亮度”。
相關資料可參考 - http://www.donmccurdy.com/2020/06/17/color-management-in-threejs/ - http://medium.com/game-dev-daily/the-srgb-learning-curve-773b7f68cf7a
Textures encoding
環境貼圖顏色現在是不太正確的。它們看起來是灰色的,並且淡化了。即使效果看起來很不錯,但保留正確的顏色更令人滿意。
原因是我們的渲染器 outputEncoding 是 THREE.sRGBEncoding,而環境貼圖紋理預設是 THREE.LinearEncoding。
規則很簡單。我們可以直接看到的所有紋理,比如地圖,應該是 THREE.sRGBEncoding 作為編碼,所有其他的紋理,比如 normalMap,應該是 THREE.LinearEncoding。
我們可以將 environmentMap 紋理編碼改成 THREE.sRGBEncoding:
js
environmentMap.encoding = THREE.sRGBEncoding
但是模型紋理呢?幸運的是,GLTFLoader 實現了這個規則,從它載入的所有紋理都會自動進行正確的編碼。
對比如下
environmentMap.encoding LinearEncoding | environmentMap.encoding sRGBEncoding --- | --- |
toneMapping 色調對映
.toneMapping : Constant
預設是 NoToneMapping。檢視 Renderer constants 以獲取其它備選項
THREE.NoToneMapping
THREE.LinearToneMapping
THREE.ReinhardToneMapping
THREE.CineonToneMapping
THREE.ACESFilmicToneMapping
這些常量定義了 WebGLRenderer 中 toneMapping 的屬性。這個屬性用於在普通計算機顯示器或者移動裝置螢幕等低動態範圍介質上,模擬、逼近高動態範圍(HDR)效果。
這裡我們先使用 ReinhardToneMapping
toneMapping NoToneMapping | toneMapping ReinhardToneMapping --- | --- |
toneMappingExposure 色調對映的曝光級別
我們還可以更改色調對映曝光。你可以看到我們讓多少光進入,演算法會按照自己的方式處理它。
.toneMappingExposure : Number
色調對映的曝光級別。預設是1
js
renderer.toneMappingExposure = 2.5
toneMappingExposure 1 | toneMappingExposure 2.5 --- | --- |
Anti Aliasing 抗鋸齒
我們仔細觀察,會在某些特定的角度看到階梯狀的鋸齒,通常出現在幾何圖形的邊緣。
這是一個眾所周知的問題。當一個畫素的渲染髮生時,會檢查該畫素中正在渲染什麼幾何圖形,並計算顏色,最後,該顏色出現在螢幕上。但是幾何邊緣通常不會與螢幕畫素的垂直線和水平線完全對齊,這就是為什麼你會看到這個鋸齒狀的階梯。
有很多方法可以解決這個問題,開發人員多年來一直在努力解決這個問題。
一個簡單的解決方案是增加我們渲染的解析度。當調整到正常大小時,每個畫素顏色將自動從渲染的 4 個畫素中取平均值。
這種解決方案稱為超取樣 (SSAA) 或全屏取樣 (FSAA),它是最簡單且更高效的解決方案。不過,這意味著要渲染 4 倍以上的畫素,這可能會導致效能問題。
另一種解決方案稱為多重取樣 (MSAA)。同樣,這個方法是為每個畫素渲染多個值(通常是 4 個),就像超級取樣一樣,但只在幾何體的邊緣上。然後對畫素的值進行平均以獲得最終的畫素值。
最新的 GPU 可以執行這種多重取樣抗鋸齒,Three.js 會自動處理設定。我們只需要在例項化期間將 antialias 屬性更改為 true
js
// Renderer
const renderer = new THREE.WebGLRenderer({
canvas,
antialias: true,
})
鋸齒消失了。放大觀察會更加明顯。
antialias false | antialias true --- | --- |
區域性放大
antialias false | antialias true --- | --- |
使用抗鋸齒會消耗一些資源。畫素比(pixel ratio)大於 1 的螢幕實際上並不需要抗鋸齒。比較好的方法是隻為畫素比大於 2 的螢幕啟用它。我們將在以後的學習中看到如何實現它,以及其他優化。
Shadows 影
最後我們再新增一些光影效果。我們之前在已經學習過了。
首先將 WebGLRenderer 的 shadow 開啟
js
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap
開啟平行光發射投影
js
directionLight.castShadow = true
新增 CameraHelper 觀察一下光照範圍是否將物體全部包裹住
```js const directionalLightCameraHelper = new THREE.CameraHelper(directionLight.shadow.camera) scene.add(directionalLightCameraHelper)
directionLight.shadow.camera.far = 15 directionLight.shadow.mapSize.set(1024, 1024) ```
.mapSize : Vector2
一個 Vector2 定義陰影貼圖的寬度和高度。
較高的值會以計算時間為代價提供更好的陰影質量。值必須是 2 的冪,最大為給定裝置的 WebGLRenderer.capabilities.maxTextureSize
,儘管寬度和高度不必相同(例如,(512, 1024) 是有效的)。 預設值為(512, 512)。
由於我們需要逼真和精確的陰影,並且因為我們只有一盞燈,我們可以將陰影貼圖的大小增加到 1024x1024,而不必擔心幀率下降。
最後,我們可以啟用模型所有網格上的陰影。由於我們已經在 updateAllMaterials 函式中遍歷模型,讓我們簡單地啟用所有 children 的 castShadow 和 receiveShadow
js
/**
* Update all materials
*/
const updateAllMaterials = () => {
scene.traverse((child) => {
if (child instanceof THREE.Mesh && child.material instanceof THREE.MeshStandardMaterial) {
// ...
child.castShadow = true
child.receiveShadow = true
}
})
}
no shadow | shadow --- | --- |
最終微調
現在我們已經準備就緒,我們可以調整值,確保 directionalLight 對應於環境貼圖中的燈光,嘗試其他環境貼圖,測試不同的色調對映,新增動畫等。
這些由你個人決定。花點時間,檢視渲染,環顧四周,因為您需要現實生活中的標記,確保您的螢幕顏色良好,也可以問問周圍的朋友建議,直到一切都正確設定。
這裡我調整了環境貼圖的強度和光照強度,看起來和環境融合會更真實一些
光照調整前 | 光照調整後 --- | --- |
最終對比過程
線上 demo 連結
漢堡模型
還記得上一節我們做的漢堡模型嗎?我們也將它重新渲染一下看看效果。
匯入並渲染後發現表面有一些小條紋,這被稱為 shadow acne(陰影粉刺 or 陰影痤瘡)
這是因為在計算表面是否在陰影中時,出於精確原因,陰影粉刺可能會出現在光滑和平坦的表面上。 這裡發生的事情是漢堡包在它自己的表面上投下了陰影。我們可以使用以下2個屬性解決問題
.bias : Float
用於平面
陰影貼圖偏差,在確定曲面是否在陰影中時,從標準化深度新增或減去多少。 預設值為0.此處非常小的調整(大約0.0001)可能有助於減少陰影中的偽影
.normalBias : Float
用於曲面
定義用於查詢陰影貼圖的位置沿物件法線偏移多少。 預設值為 0。增加此值可用於減少陰影粉刺,尤其是在光線以淺角度照射到幾何體上的大型場景中。 代價是陰影可能會出現扭曲。
我們新增如下程式碼
js
directionLight.shadow.normalBias = 0.05
最終效果如下
線上 demo 連結
與上一節的漢堡對比,就會發現之前的很醜毫無胃口,哈哈哈
小結
本節學習瞭如何讓模型更加真實的渲染。通過新增環境貼圖、renderer 擬真優化等方式完成。通過 physicallyCorrectLights, environmentMap, outputEncoding, textures encoding, toneMapping, toneMappingExposure, antialias, Shadows 等讓渲染顯得更加真實。
讓物體在3D空間中更加真實的渲染非常酷。雖然我們做了很多努力,但有時看上去還是不夠真實,任重道遠。