基于three.js的虛擬人陰影渲染優(yōu)化方案
一、前言
在3D網(wǎng)頁應(yīng)用中,高質(zhì)量的陰影渲染對于營造場景的真實感至關(guān)重要。作為廣泛采用的 WebGL 框架之一,three.js 為開發(fā)者提供了多種陰影渲染選項,使得創(chuàng)建生動逼真的光影效果成為可能。然而,實現(xiàn)這些視覺上的增強往往伴隨著性能開銷,尤其在處理復(fù)雜場景或運行于低端設(shè)備時更為明顯。因此,在確保畫面質(zhì)量的同時優(yōu)化陰影渲染,以提升用戶體驗和保持流暢性,便成了一個核心挑戰(zhàn)。本文將解析 three.js 中的陰影渲染機制,并提供一系列實用的優(yōu)化策略,助力開發(fā)者在不同應(yīng)用場景下達成最佳平衡。
二、數(shù)字人中使用的陰影
在開發(fā)擬我形象的過程中,恰當(dāng)運用陰影可以顯著增加模型的立體感與真實度。同時,在地面上添加陰影不僅能夠為觀察者提供空間定位的參考點,還能大大增強場景的空間層次感和沉浸體驗。
- 圖1(全局陰影)
圖2(地面陰影)
接下來,我們將探討全局陰影的優(yōu)化方法以及地面陰影的具體實施方案。
三、全局陰影的優(yōu)化
全局陰影的實現(xiàn)主要依賴于 three.js 提供的 shadowMap。只需簡單幾步——在 WebGLRenderer 中啟用 shadowMap 功能、定義產(chǎn)生陰影的光源以及設(shè)定哪些物體負(fù)責(zé)投射或接收陰影——即可輕松完成設(shè)置。
若僅使用 three.js 默認(rèn)配置下的陰影設(shè)置,雖然操作簡便但效果通常不盡如人意。特別是在針對移動平臺進行開發(fā)時,考慮到性能限制,我們有必要對 three.js 的陰影特性做進一步研究:
3.1 three.js 的陰影
在 three.js 中,陰影的類型主要有兩種,分別是硬陰影(hard shadows)和軟陰影(soft shadows)。硬陰影的邊緣清晰,常用于模擬光源較小或光源位置靠近物體的場景;軟陰影的邊緣較模糊,更加接近現(xiàn)實中的陰影效果。這兩種陰影效果是通過不同的陰影貼圖(shadow map)類型實現(xiàn)的。以下是常見的陰影類型:
3.1.1 BasicShadowMap(硬陰影)
特性: 這是最基本的陰影類型,計算速度快,性能開銷小,但效果相對簡單。生成的陰影沒有柔和的邊緣,呈現(xiàn)出硬邊界。
用途: 用于性能要求較高但不太關(guān)注陰影效果的場景。
圖3(BasicShadowMap)
3.1.2 PCFShadowMap (Percentage-Closer Filtering)(軟陰影)
特性: 默認(rèn)的陰影類型,邊緣相對柔和。使用了一種簡單的濾波技術(shù)來使陰影邊緣變得平滑。
用途: 大多數(shù)情況下推薦使用,效果較好,性能開銷也可以接受。
圖4(PCFShadowMap)
3.1.3 PCFSoftShadowMap(軟陰影)
特性: 在 PCFShadowMap 的基礎(chǔ)上,進一步對陰影的柔和度進行了優(yōu)化,提供更柔和的陰影邊緣效果,但性能開銷會更大。
用途: 用于需要較高質(zhì)量陰影效果的場景。
圖5(PCFSoftShadowMap)
3.1.4 VSMShadowMap (Variance Shadow Map)(軟陰影)
特性: 使用了方差陰影貼圖算法,能夠生成高質(zhì)量且無鋸齒的柔和陰影。相比 PCF 技術(shù),它可以產(chǎn)生更加平滑的效果,并且可以避免常見的陰影采樣問題。但該技術(shù)可能會產(chǎn)生“光暈”現(xiàn)象。
用途: 適用于高質(zhì)量陰影場景,特別是需要柔和漸變的陰影效果。
圖6(VSMShadowMap)
從上面的預(yù)覽圖可以看出,對于 BasicShadowMap 和 PCFShadowMap,陰影的邊緣有比較多的鋸齒,而對于 PCFSoftShadowMap,除了有更多的性能開銷之外,人物在動的時候邊緣也會有明顯的閃爍的情況出現(xiàn),而且邊緣模糊半徑過大導(dǎo)致陰影的效果并不明顯。使用 VSMShadowMap 雖然可以得到相對好的效果,但是會出現(xiàn)嚴(yán)重的偽影問題,雖然可以通過調(diào)整 shadow 的偏置值(bias)來解決,但是過大的 bias 值會使得陰影的深度測試結(jié)果偏移過多,導(dǎo)致陰影被錯誤地渲染得過遠,從而產(chǎn)生不自然的視覺效果。
作為一個手機上的H5頁面,除了要保障基礎(chǔ)的視覺效果,還需要優(yōu)化性能以使其運行在更多的設(shè)備上,為了實現(xiàn)一開始向大家展示的效果同時不增加性能的開銷,我們有了下面的優(yōu)化思路。
3.2 優(yōu)化思路
要想有一個比較好的陰影效果,首先不能是硬陰影,所以排除了 BasicShadowMap;
由于 PCFSoftShadowMap 對于性能的開銷較大的同時效果提升的也不是很明顯,所以也排除掉;最后由于偽影難以控制,所以我們選擇了基于 PCFShadowMap進行優(yōu)化。
為了得到更好的陰影邊緣,可以通過提升 shadowMap 的分辨率來優(yōu)化,但是分辨率的提升勢必會導(dǎo)致性能開銷變大,如何在不提升貼圖分辨率的情況下提升陰影邊緣的質(zhì)量呢?
我們都知道在不同尺寸的屏幕相同分辨率的情況下,越小的屏幕顯示效果越細膩,DirectionalLight 在生成陰影時,會使用一個正交相機(OrthographicCamera)來確定渲染陰影的區(qū)域。這個相機的四個邊界(left、right、top、bottom)定義了陰影貼圖的范圍。通過縮小這些邊界,可以將陰影貼圖的像素更集中于需要渲染陰影的區(qū)域,從而提升陰影的清晰度。實際上在虛擬人的場景中,用戶的主要注意力都集中在頭部區(qū)域,所以只要將陰影相機聚焦在頭部的區(qū)域即可,而不需要獲取全局的陰影。
const bias = 1.6 // 設(shè)置一個y軸的偏置值,使得陰影相機可以正對人臉
const mainLight = new THREE.DirectionalLight(0xf2f7ff)
mainLight.intensity = 1.8
mainLight.position.set(0.3, 0.81 + bias, 2.71)
const target = new THREE.Object3D()
target.position.set(0, bias, 0) // 設(shè)置燈光的照射目標(biāo)
group.add(target)
mainLight.target = target
mainLight.castShadow = true
mainLight.shadow.radius = 2 // 設(shè)置陰影邊緣的模糊半徑,這個值并不是越大越好,需要根據(jù)實際場景進行微調(diào)
const { camera } = mainLight.shadow
camera.far = 5
// 陰影相機的默認(rèn)邊界為上下左右分別為5,將其縮小至各0.5
camera.top = -0.5
camera.bottom = 0.5
camera.left = -0.5
camera.right = 0.5
左右滑動查看完整代碼
四、地面陰影的實現(xiàn)
在最開始的動圖(圖2)中,除了臉部的陰影,還有一個地面的陰影,很顯然地面陰影不可能專門打一束光照在腳上獲得,這樣會使得整體的光影顯得很奇怪,那么地面陰影是怎么實現(xiàn)的呢。
實際上這里參考了
model-viewer (https://github.com/google/model-viewer)
的實現(xiàn),地面上的陰影實際上是一個方形加上陰影貼圖:
- 創(chuàng)建一個正交相機,將相機的位置設(shè)置在腳下,朝向上方并有一點點傾角,獲取到從地面向上看的圖像;
- 創(chuàng)建一個材質(zhì),并且自定義著色器渲染物體的深度信息,渲染第一步創(chuàng)建的相機的場景的時候?qū)⒉馁|(zhì)賦值給scene.overrideMaterial屬性,這樣場景中所有的物體都會使用這個材質(zhì)進行渲染;
- 再創(chuàng)建一個正交相機,用于模糊第一個相機獲取到的圖像;
- 將模糊后的圖像作為貼圖,應(yīng)用到地板平面上;
- 此方案在每幀畫面渲染之前都要再額外先把地面陰影的場景渲染出來,所以會增加額外的性能開銷,由于地面陰影的邊緣經(jīng)過模糊平滑的處理,所以分辨率并不需要太高,貼圖尺寸設(shè)置為64*64即可,有效的控制地面陰影帶來的性能損失。
// 設(shè)置陰影渲染目標(biāo),作為陰影貼圖
const size = 64
const shadowTarget = new THREE.WebGLRenderTarget(size, size)
const shadowTargetBlur = new THREE.WebGLRenderTarget(size, size)
this.shadowTarget = shadowTarget
this.shadowTargetBlur = shadowTargetBlur
// 調(diào)整位置
this.position.set(0, -0.05, 0)
this.rotateX(Math.PI / 2) //旋轉(zhuǎn)地板與地面平行
// 設(shè)置陰影相機
const camera = new THREE.OrthographicCamera(-0.75, 0.75, 0.75, -0.75, 0, 0.5)
// 設(shè)置地面相機的一個傾斜角度
camera.rotateX(Math.PI / 6)
camera.rotateY(Math.PI / 6)
this.add(camera)
this.camera = camera
// 設(shè)置視覺相機
const visionCamera = new THREE.OrthographicCamera(-0.75, 0.75, 0.75, -0.75, 0, 2)
this.add(visionCamera)
this.visionCamera = visionCamera
// 設(shè)置深度材質(zhì)的片段著色器
this.depthMaterial.onBeforeCompile = function (shader) {
shader.fragmentShader = shader.fragmentShader.replace(
'gl_FragColor = vec4( vec3( 1.0 - fragCoordZ ), opacity );',
'gl_FragColor = vec4( vec3( 0.0 ), ( 1.0 - fragCoordZ ) * opacity );'
)
}
// 創(chuàng)建地板
const planeGeometry = new THREE.PlaneGeometry(1.5, 1.5)
const material = new THREE.MeshBasicMaterial({
opacity: 0.3,
transparent: true,
map: shadowTarget.texture,
side: THREE.DoubleSide,
color: 0x666666
})
const plane = new THREE.Mesh(planeGeometry, material)
visionCamera.add(plane)
const blurPlane = new THREE.Mesh(planeGeometry)
blurPlane.visible = false
visionCamera.add(blurPlane)
this.plane = plane
this.blurPlane = blurPlane
左右滑動查看完整代碼
五、結(jié)語
針對全局陰影和地面陰影,我們采取了不同的優(yōu)化方式:
- 通過合理選擇陰影的渲染方式、優(yōu)化陰影相機的視野范圍以及優(yōu)化陰影貼圖的分辨率,可以在保證性能沒有明顯提升的情況下顯著提升陰影的品質(zhì);
- 通過獲取底部視角的深度信息結(jié)合自定義shader來生成地面陰影,對頁面的性能沒有明顯的損耗的同時達到一個比較好的效果。
后續(xù)也可以通過在 webview 注入機型信息,通過機型對手機的性能進行分級,調(diào)用針對性的渲染方案,可以使頁面在流暢運行的前提下進一步提升畫面的表現(xiàn)。為了實現(xiàn)更好的陰影效果,也可以對 three.js 的陰影相機進行擴展,實現(xiàn)多機位 shadowMap 等能力,在不增加太多負(fù)載的情況下進一步提升陰影的效果。