Cesium在1.46版本中新增了對整個場景的后期處理(Post Processing)功能,包括模型描邊、黑白圖、明亮度調整、夜視效果、環境光遮蔽等。對於這么炫酷的功能,我們絕不猶豫,先去翻一翻它的源碼,掌握它的實現原理。
1 后期處理的原理
后期處理的過程有點類似於照片的PS。生活中拍攝了一張自拍照,看到照片后發現它太暗了,於是我們增加亮度得到了一張新的照片。在增加亮度后發現臉上的痘痘清晰可見,這可不是我們希望的效果,於是再進行一次美膚效果處理。在這之后可能還會進行n次別的操作,直到滿足我們的要求。上述這個過程和三維里面的后期處理流程非常類似:拍的原始照片相當於三維場景中實際渲染得到的效果,在此基礎上進行物體描邊、夜視效果、環境光遮蔽等后期處理,最后渲染到場景中的圖片相當於定版的最終照片。整個過程如下圖所示:
2 Cesium添加后期處理的流程
在介紹Cesium添加后期處理流程之前,首先對用到的相關類進行說明:
PostProcessStage:對應於某個具體的后期處理效果,它的輸入為場景渲染圖或者上一個后期處理的結果圖,輸出結果是一張處理后的圖片。
PostProcessStageComposite:一個集合對象,存儲類型為PostProcessStage或者PostProcessStageComposite的元素。
PostProcessStageLibrary:負責創建具體的后期處理效果,包括Silhouette、Bloom、AmbientOcclusion等,創建返回的結果是PostProcessStageComposite或者PostProcessStage類型。
PostProcessStageCollection:是一個集合類型的類,負責管理和維護放到集合中的元素 ,元素的類型是PostProcessStage或者PostProcessStageComposite。
Cesium中添加后期處理的流程是:首先通過PostProcessStageLibrary創建一個或者多個后處理效果對象,得到多個PostProcessStage或者PostProcessStageComposite,然后將他們加入到PostProcessStageCollection對象中。這樣PostProcessStageCollection對象就會按照加入的順序進行屏幕后期處理,在所有的效果都處理完畢后,執行FXAA,最后繪制到屏幕上。下面對Silhouette實現原理進行介紹,Ambient Occlusion實現原理將會在下一篇文章中單獨進行說明。
3 Silhouette實現原理
3.1 開啟物體描邊功能
silhouette的效果可以理解為物體輪廓、描邊,相當於把物體的外輪廓線勾勒出來。在Cesium中開啟silhouette的代碼和效果如下:
1 var collection = viewer.scene.postProcessStages; 2 var silhouette = collection.add(Cesium.PostProcessStageLibrary.createSilhouetteStage()); 3 silhouette.enabled = true; 4 silhouette.uniforms.color = Cesium.Color.YELLOW;
3.2 js代碼內容
創建Stage的函數是實現功能的關鍵所在,Cesium.PostProcessStageLibrary.createSilhouetteStage()這個函數的具體內容如下:
1 PostProcessStageLibrary.createSilhouetteStage = function() { 2 var silhouetteDepth = new PostProcessStage({ 3 name : 'czm_silhouette_depth', 4 fragmentShader : LinearDepth 5 }); 6 var edgeDetection = new PostProcessStage({ 7 name : 'czm_silhouette_edge_detection', 8 fragmentShader : EdgeDetection, 9 uniforms : { 10 length : 0.25, 11 color : Color.clone(Color.BLACK) 12 } 13 }); 14 var silhouetteGenerateProcess = new PostProcessStageComposite({ 15 name : 'czm_silhouette_generate', 16 stages : [silhouetteDepth, edgeDetection] 17 }); 18 var silhouetteProcess = new PostProcessStage({ 19 name : 'czm_silhouette_color_edges', 20 fragmentShader : Silhouette, 21 uniforms : { 22 silhouetteTexture : silhouetteGenerateProcess.name 23 } 24 }); 25 26 var uniforms = {}; 27 defineProperties(uniforms, { 28 length : { 29 get : function() { 30 return edgeDetection.uniforms.length; 31 }, 32 set : function(value) { 33 edgeDetection.uniforms.length = value; 34 } 35 }, 36 color : { 37 get : function() { 38 return edgeDetection.uniforms.color; 39 }, 40 set : function(value) { 41 edgeDetection.uniforms.color = value; 42 } 43 } 44 }); 45 return new PostProcessStageComposite({ 46 name : 'czm_silhouette', 47 stages : [silhouetteGenerateProcess, silhouetteProcess], 48 inputPreviousStageTexture : false, 49 uniforms : uniforms 50 }); 51 };
通過瀏覽代碼發現,該函數最后的返回結果是PostProcessStageComposite對象,該對象包含了silhouetteGenerateProcess和silhouetteGenerateProcess兩個元素,其中silhouetteGenerateProcess又是一個PostProcessStageComposite類型,包括silhouetteDepth和edgeDetection兩部分。在后期處理過程中真正起作用的是PostProcessStage類型的對象,此處包括silhouetteDepth、silhouetteDepth、silhouetteProcess三個對象,也就是說這三個對象的順序執行實現了物體描邊效果。對於PostProcessStage這種類型的對象,它的輸入值包括一些效果參數和一張輸入照片,頂點着色器沒有什么特殊內容,就是構建一個貼屏幕的四邊形,重點全部在片源着色器中。下面對這三個片源着色器中的代碼進行詳細分析。
3.3 LinearDepth
LinearDepth的代碼如下:
1 uniform sampler2D depthTexture; 2 3 varying vec2 v_textureCoordinates; 4 5 float linearDepth(float depth) 6 { 7 float far = czm_currentFrustum.y; 8 float near = czm_currentFrustum.x; 9 return (2.0 * near) / (far + near - depth * (far - near)); 10 } 11 12 void main(void) 13 { 14 float depth = czm_readDepth(depthTexture, v_textureCoordinates); 15 gl_FragColor = vec4(linearDepth(depth)); 16 }
代碼比較簡單,一共才10多行,目的就是將深度圖中的深度值進行線性拉伸。depthTexture代表場景中的深度圖,v_textureCoordinates代表屏幕采樣點坐標。首先通過czm_readDepth讀取場景中的深度值,然后利用linearDepth函數(該函數通過遠近裁剪面對輸入值做了一個線性變換)進行線性拉伸。其實質是把深度值轉換成視空間下的z值,然后將這個z值除以far,得到一個0-1的值,該值的大小可以反應屏幕像素點在視空間下的z值大小。最后將得到的深度值賦值給gl_FragColor變量,相當於把深度值隱藏在顏色中。這樣就得到了一張經過線性拉伸后的深度圖,用於后面的處理。
3.4 EdgeDetection
EdgeDetection的代碼如下:
1 uniform sampler2D depthTexture; 2 uniform float length; 3 uniform vec4 color; 4 5 varying vec2 v_textureCoordinates; 6 7 void main(void) 8 { 9 float directions[3]; 10 directions[0] = -1.0; 11 directions[1] = 0.0; 12 directions[2] = 1.0; 13 14 float scalars[3]; 15 scalars[0] = 3.0; 16 scalars[1] = 10.0; 17 scalars[2] = 3.0; 18 19 float padx = 1.0 / czm_viewport.z; 20 float pady = 1.0 / czm_viewport.w; 21 22 float horizEdge = 0.0; 23 float vertEdge = 0.0; 24 25 for (int i = 0; i < 3; ++i) { 26 float dir = directions[i]; 27 float scale = scalars[i]; 28 29 horizEdge -= texture2D(depthTexture, v_textureCoordinates + vec2(-padx, dir * pady)).x * scale; 30 horizEdge += texture2D(depthTexture, v_textureCoordinates + vec2(padx, dir * pady)).x * scale; 31 32 vertEdge -= texture2D(depthTexture, v_textureCoordinates + vec2(dir * padx, -pady)).x * scale; 33 vertEdge += texture2D(depthTexture, v_textureCoordinates + vec2(dir * padx, pady)).x * scale; 34 } 35 36 float len = sqrt(horizEdge * horizEdge + vertEdge * vertEdge); 37 float alpha = len > length ? 1.0 : 0.0; 38 gl_FragColor = vec4(color.rgb, alpha); 39 }
通過shader的名字就可以大體猜到這段代碼的作用就是對邊界進行檢測。depthTexture是通過linearDepth拉伸后的深度圖,length是設置的物體邊界長度判斷值,color是設置的邊界顏色,v_textureCoordinates是屏幕采樣點的坐標。在main函數中首先定義了directions和scalars兩個數組。directions代表進行邊界檢測的方向,scalars表示邊界檢測的權重值。padx表示每個像素在x方向上的坐標跨度,pady表示每個像素在y方向上的坐標跨度。horizEdge表示水平方向的邊界值,vertEdge表示豎直方向邊界值。然后就是通過for循環在以該像素為中心的九宮格中計算水平方向的深度差值和垂直方向的深度差值,計算的過程可以用下圖表示:
通過上面這張圖可以清晰的看出,邊界檢測的過程其實是對周圍八個像素點計算z坐標差值,包括水平坐標差值horizEdge和豎直差值vertEdge。通過這兩個值得到總差值len,通過表和len的 大小設置顏色的透明度為1或者0,輸出一張圖。
3.5 Silhouette
Silhouette的代碼如下:
1 uniform sampler2D colorTexture; 2 uniform sampler2D silhouetteTexture; 3 4 varying vec2 v_textureCoordinates; 5 6 void main(void) 7 { 8 vec4 silhouetteColor = texture2D(silhouetteTexture, v_textureCoordinates); 9 gl_FragColor = mix(texture2D(colorTexture, v_textureCoordinates), silhouetteColor, silhouetteColor.a); 10 }
silhouette的代碼非常簡單,其中colorTexture代表原始場景圖,silhouetteTexture是通過EdgeDetection得到的圖。通過silhouetteColor.a進行兩張圖的混合,就可以得到最終的結果。
4 總結
后期處理其實是一個疊加修改的過程,通過不同步驟的加工,最后得到想要的結果。本文所講的物體描邊其實是對整個屏幕中的要素進行邊界檢測,檢測出為邊界的地方就將其顏色改為設定的值。花了大半天時間寫完了,希望對感興趣的同學有所幫助。晚上我要出去玩,玩,玩!!!
PS:Cesium交流可以掃碼加群,期待你的加入!!!