動態

詳情 返回 返回

基於three.js的虛擬人陰影渲染優化方案 - 動態 詳情

作者:來自 vivo 互聯網大前端團隊- Su Ning

本文將探討 three.js 中的陰影渲染機制,並分享一些針對性能和效果優化的實用技巧,幫助開發者在不同場景下做出最佳的權衡選擇。

一、前言

在3D網頁應用中,高質量的陰影渲染對於營造場景的真實感至關重要。作為廣泛採用的 WebGL 框架之一,three.js 為開發者提供了多種陰影渲染選項,使得創建生動逼真的光影效果成為可能。然而,實現這些視覺上的增強往往伴隨着性能開銷,尤其在處理複雜場景或運行於低端設備時更為明顯。因此,在確保畫面質量的同時優化陰影渲染,以提升用户體驗和保持流暢性,便成了一個核心挑戰。本文將解析 three.js 中的陰影渲染機制,並提供一系列實用的優化策略,助力開發者在不同應用場景下達成最佳平衡。

二、數字人中使用的陰影

在開發擬我形象的過程中,恰當運用陰影可以顯著增加模型的立體感與真實度。同時,在地面上添加陰影不僅能夠為觀察者提供空間定位的參考點,還能大大增強場景的空間層次感和沉浸體驗。

圖1(全局陰影)

圖2(地面陰影)

接下來,我們將探討全局陰影的優化方法以及地面陰影的具體實施方案。

三、全局陰影的優化

全局陰影的實現主要依賴於 three.js 提供的 shadowMap。只需簡單幾步——在 WebGLRenderer 中啓用 shadowMap 功能、定義產生陰影的光源以及設定哪些物體負責投射或接收陰影——即可輕鬆完成設置。

若僅使用 three.js 默認配置下的陰影設置,雖然操作簡便但效果通常不盡如人意。特別是在針對移動平台進行開發時,考慮到性能限制,我們有必要對 three.js 的陰影特性做進一步研究:

3.1 three.js 的陰影

在 three.js 中,陰影的類型主要有兩種,分別是硬陰影(hard shadows)和軟陰影(soft shadows)。硬陰影的邊緣清晰,常用於模擬光源較小或光源位置靠近物體的場景;軟陰影的邊緣較模糊,更加接近現實中的陰影效果。這兩種陰影效果是通過不同的陰影貼圖(shadow map)類型實現的。以下是常見的陰影類型:

3.1.1 BasicShadowMap(硬陰影)

特性: 這是最基本的陰影類型,計算速度快,性能開銷小,但效果相對簡單。生成的陰影沒有柔和的邊緣,呈現出硬邊界。

用途: 用於性能要求較高但不太關注陰影效果的場景。

圖3(BasicShadowMap)

3.1.2 PCFShadowMap (Percentage-Closer Filtering)(軟陰影)

特性: 默認的陰影類型,邊緣相對柔和。使用了一種簡單的濾波技術來使陰影邊緣變得平滑。

用途: 大多數情況下推薦使用,效果較好,性能開銷也可以接受。

圖4(PCFShadowMap)

3.1.3 PCFSoftShadowMap(軟陰影)

特性: 在 PCFShadowMap 的基礎上,進一步對陰影的柔和度進行了優化,提供更柔和的陰影邊緣效果,但性能開銷會更大。

用途: 用於需要較高質量陰影效果的場景。

圖5(PCFSoftShadowMap)

3.1.4 VSMShadowMap (Variance Shadow Map)(軟陰影)

特性: 使用了方差陰影貼圖算法,能夠生成高質量且無鋸齒的柔和陰影。相比 PCF 技術,它可以產生更加平滑的效果,並且可以避免常見的陰影採樣問題。但該技術可能會產生“光暈”現象。

用途: 適用於高質量陰影場景,特別是需要柔和漸變的陰影效果。

圖6(VSMShadowMap)

從上面的預覽圖可以看出,對於 BasicShadowMap 和 PCFShadowMap,陰影的邊緣有比較多的鋸齒,而對於 PCFSoftShadowMap,除了有更多的性能開銷之外,人物在動的時候邊緣也會有明顯的閃爍的情況出現,而且邊緣模糊半徑過大導致陰影的效果並不明顯。使用 VSMShadowMap 雖然可以得到相對好的效果,但是會出現嚴重的偽影問題,雖然可以通過調整 shadow 的偏置值(bias)來解決,但是過大的 bias 值會使得陰影的深度測試結果偏移過多,導致陰影被錯誤地渲染得過遠,從而產生不自然的視覺效果。

作為一個手機上的H5頁面,除了要保障基礎的視覺效果,還需要優化性能以使其運行在更多的設備上,為了實現一開始向大家展示的效果同時不增加性能的開銷,我們有了下面的優化思路。

3.2 優化思路

要想有一個比較好的陰影效果,首先不能是硬陰影,所以排除了 BasicShadowMap;

由於 PCFSoftShadowMap 對於性能的開銷較大的同時效果提升的也不是很明顯,所以也排除掉;最後由於偽影難以控制,所以我們選擇了基於 PCFShadowMap進行優化。

為了得到更好的陰影邊緣,可以通過提升 shadowMap 的分辨率來優化,但是分辨率的提升勢必會導致性能開銷變大,如何在不提升貼圖分辨率的情況下提升陰影邊緣的質量呢?

我們都知道在不同尺寸的屏幕相同分辨率的情況下,越小的屏幕顯示效果越細膩,DirectionalLight 在生成陰影時,會使用一個正交相機(OrthographicCamera)來確定渲染陰影的區域。這個相機的四個邊界(left、right、top、bottom)定義了陰影貼圖的範圍。通過縮小這些邊界,可以將陰影貼圖的像素更集中於需要渲染陰影的區域,從而提升陰影的清晰度。實際上在虛擬人的場景中,用户的主要注意力都集中在頭部區域,所以只要將陰影相機聚焦在頭部的區域即可,而不需要獲取全局的陰影。

const bias = 1.6 // 設置一個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)  // 設置燈光的照射目標
group.add(target)
mainLight.target = target
mainLight.castShadow = true
 
mainLight.shadow.radius = 2  // 設置陰影邊緣的模糊半徑,這個值並不是越大越好,需要根據實際場景進行微調
const { camera } = mainLight.shadow
camera.far = 5
 
// 陰影相機的默認邊界為上下左右分別為5,將其縮小至各0.5
camera.top = -0.5
camera.bottom = 0.5
camera.left = -0.5
camera.right = 0.5

四、地面陰影的實現

在最開始的動圖(圖2)中,除了臉部的陰影,還有一個地面的陰影,很顯然地面陰影不可能專門打一束光照在腳上獲得,這樣會使得整體的光影顯得很奇怪,那麼地面陰影是怎麼實現的呢。

實際上這裏參考了 

model-viewer (https://github.com/google/model-viewer)

的實現,地面上的陰影實際上是一個方形加上陰影貼圖:

  • 創建一個正交相機,將相機的位置設置在腳下,朝向上方並有一點點傾角,獲取到從地面向上看的圖像;
  • 創建一個材質,並且自定義着色器渲染物體的深度信息,渲染第一步創建的相機的場景的時候將材質賦值給scene.overrideMaterial屬性,這樣場景中所有的物體都會使用這個材質進行渲染;
  • 再創建一個正交相機,用於模糊第一個相機獲取到的圖像;
  • 將模糊後的圖像作為貼圖,應用到地板平面上;
  • 此方案在每幀畫面渲染之前都要再額外先把地面陰影的場景渲染出來,所以會增加額外的性能開銷,由於地面陰影的邊緣經過模糊平滑的處理,所以分辨率並不需要太高,貼圖尺寸設置為64*64即可,有效的控制地面陰影帶來的性能損失。

// 設置陰影渲染目標,作為陰影貼圖
const size = 64
const shadowTarget = new THREE.WebGLRenderTarget(size, size)
const shadowTargetBlur = new THREE.WebGLRenderTarget(size, size)
this.shadowTarget = shadowTarget
this.shadowTargetBlur = shadowTargetBlur
 
// 調整位置
this.position.set(0, -0.05, 0)
this.rotateX(Math.PI / 2)  //旋轉地板與地面平行
 
// 設置陰影相機
const camera = new THREE.OrthographicCamera(-0.75, 0.75, 0.75, -0.75, 0, 0.5)
 
// 設置地面相機的一個傾斜角度
camera.rotateX(Math.PI / 6)
camera.rotateY(Math.PI / 6)
this.add(camera)
this.camera = camera
 
// 設置視覺相機
const visionCamera = new THREE.OrthographicCamera(-0.75, 0.75, 0.75, -0.75, 0, 2)
this.add(visionCamera)
this.visionCamera = visionCamera
 
// 設置深度材質的片段着色器
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 );'
  )
}
 
// 創建地板
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

五、結語

針對全局陰影和地面陰影,我們採取了不同的優化方式:

  • 通過合理選擇陰影的渲染方式、優化陰影相機的視野範圍以及優化陰影貼圖的分辨率,可以在保證性能沒有明顯提升的情況下顯著提升陰影的品質;
  • 通過獲取底部視角的深度信息結合自定義shader來生成地面陰影,對頁面的性能沒有明顯的損耗的同時達到一個比較好的效果。

後續也可以通過在 webview 注入機型信息,通過機型對手機的性能進行分級,調用針對性的渲染方案,可以使頁面在流暢運行的前提下進一步提升畫面的表現。為了實現更好的陰影效果,也可以對 three.js 的陰影相機進行擴展,實現多機位 shadowMap 等能力,在不增加太多負載的情況下進一步提升陰影的效果。

user avatar Leesz 頭像 u_17513518 頭像 leexiaohui1997 頭像 huajianketang 頭像 inslog 頭像 huichangkudelingdai 頭像 databend 頭像 woniuseo 頭像 Z-HarOld 頭像 shanejix 頭像 greasql 頭像 suporka 頭像
點贊 23 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.