之前就一直有寫博客的想法,別人也建議寫一寫,但一直沒有動手寫,自己想了一下原因,就一個字:懶、懶、懶。為了改掉這個毛病,決定從今天開始寫博客了,一方面對自己掌握的知識做一個梳理,另一方面和大家做一個交流,更能深化對問題的理解。廢話好像有點多,好了,各位乘客,收起小桌板,系好安全帶,要發車嘍。
Cesium作為一個開源的webgl三維地球渲染引擎,具備很多的基礎功能和高級功能。之前已經有很多文章對Cesium做了相關的介紹以及如何使用API等等,我想和大家分享的是Cesium一些功能的底層實現。作為一個源碼研究愛好者,希望能將底層優秀代碼和大家分享。我們不是代碼的生產者,我們只是代碼世界的搬運工,哈哈。聽說Cesium最近集成了平面剪裁功能,我們趕緊去看一看。
一 Cesium平面裁剪效果
Cesium裁剪模型的效果如下:
這就是Cesium中根據一個平面對模型進行裁剪的效果,看上去很神奇。除了可以對單個模型進行裁剪,還支持對3D Tiles模型、地形進行裁剪,裁剪面可以定義成單個面也可以設置成多個面。
二 Cesium平面裁剪調用
在Cesium中添加模型以及對模型進行裁剪非常簡單好用,只需下面幾行代碼就可以實現:
1 var modelEntityClippingPlanes;//定義的裁剪平面集合 2 function loadModel(url) { 3 var clippingPlanes = [ 4 new Cesium.ClippingPlane(new Cesium.Cartesian3(0.0, 0.0, -1.0), -100.0) 5 ];//裁剪平面數組 6 7 modelEntityClippingPlanes = new Cesium.ClippingPlaneCollection({ 8 planes : clippingPlanes, 9 edgeWidth : viewModel.edgeStylingEnabled ? 1.0 : 0.0 10 }); 11 //更新裁剪平面的位置 12 function updateClippingPlanes() { 13 return modelEntityClippingPlanes; 14 } 15 //添加要裁剪的飛機模型,並設置裁剪平面 16 var position = Cesium.Cartesian3.fromDegrees(-123.0744619, 44.0503706, 100.0); 17 var heading = Cesium.Math.toRadians(135.0); 18 var pitch = 0.0; 19 var roll = 0.0; 20 var hpr = new Cesium.HeadingPitchRoll(heading, pitch, roll); 21 var orientation = Cesium.Transforms.headingPitchRollQuaternion(position, hpr); 22 var entity = viewer.entities.add({ 23 name : url, 24 position : position, 25 orientation : orientation, 26 model : { 27 uri : url, 28 scale : 8, 29 minimumPixelSize : 100.0, 30 clippingPlanes : new Cesium.CallbackProperty(updateClippingPlanes, false)//重要,設置裁剪平面的地方 31 } 32 }); 33 34 viewer.trackedEntity = entity; 35 //將繪制的裁剪平面繪制到場景中 36 for (var i = 0; i < clippingPlanes.length; ++i) { 37 var plane = clippingPlanes[i]; 38 var planeEntity = viewer.entities.add({ 39 position : position, 40 plane : { 41 dimensions : new Cesium.Cartesian2(300.0, 300.0), 42 material : Cesium.Color.WHITE.withAlpha(0.1), 43 plane : new Cesium.CallbackProperty(createPlaneUpdateFunction(plane, Cesium.Matrix4.IDENTITY), false), 44 outline : true, 45 outlineColor : Cesium.Color.WHITE 46 } 47 }); 48 49 planeEntities.push(planeEntity); 50 } 51 }
三 實現原理剖析
通過分析Cesium源碼發現裁剪的實現是在片源着色器中,在視空間坐標系下通過判斷模型與裁剪位置構成向量與裁剪平面法向量點乘的正負來判斷片源是否剔除。如果點乘為正,說明兩個向量的夾角小於90度,在裁剪面要顯示的一側,保留,否則剔除。通過下面這張圖應該能更容易理解一點。
其中,綠色為裁剪平面,O點為裁剪平面的位置點,OA是裁剪平面的法向量,B點為模型的某個頂點,通過判斷向量OA與OB點乘的結果就可以判斷模型頂點是否需要剔除。下面分析一下Cesium中代碼的實現。Cesium通過在繪制Model的片源着色器代碼中追加一段代碼實現平面裁剪,追加后的代碼如下:
1 precision highp float; 2 varying vec3 v_normal; 3 varying vec2 v_texcoord0; 4 uniform sampler2D u_diffuse; 5 uniform vec4 u_specular; 6 uniform float u_shininess; 7 void gltf_clip_main() { 8 vec3 normal = normalize(v_normal); 9 vec4 color = vec4(0., 0., 0., 0.); 10 vec4 diffuse = vec4(0., 0., 0., 1.); 11 vec4 specular; 12 diffuse = texture2D(u_diffuse, v_texcoord0); 13 specular = u_specular; 14 diffuse.xyz *= max(dot(normal,vec3(0.,0.,1.)), 0.); 15 color.xyz += diffuse.xyz; 16 color = vec4(color.rgb * diffuse.a, diffuse.a); 17 gl_FragColor = color; 18 } 19 vec4 getClippingPlane(sampler2D packedClippingPlanes, int clippingPlaneNumber, mat4 transform) 20 { 21 int pixY = clippingPlaneNumber / 1; 22 int pixX = clippingPlaneNumber - (pixY * 1); 23 float u = (float(pixX) + 0.5) * 1.0; 24 float v = (float(pixY) + 0.5) * 0.5; 25 vec4 plane = texture2D(packedClippingPlanes, vec2(u, v)); 26 return czm_transformPlane(plane, transform); 27 } 28 29 float clip(vec4 fragCoord, sampler2D clippingPlanes, mat4 clippingPlanesMatrix) 30 { 31 bool clipped = true; 32 vec4 position = czm_windowToEyeCoordinates(fragCoord); 33 vec3 clipNormal = vec3(0.0); 34 vec3 clipPosition = vec3(0.0); 35 float clipAmount = 0.0; 36 float pixelWidth = czm_metersPerPixel(position); 37 for (int i = 0; i < 1; ++i) 38 { 39 vec4 clippingPlane = getClippingPlane(clippingPlanes, i, clippingPlanesMatrix); 40 clipNormal = clippingPlane.xyz; 41 clipPosition = -clippingPlane.w * clipNormal; 42 float amount = dot(clipNormal, (position.xyz - clipPosition)) / pixelWidth; 43 clipAmount = max(amount, clipAmount); 44 clipped = clipped && (amount <= 0.0); 45 } 46 if (clipped) 47 { 48 discard; 49 } 50 return clipAmount; 51 } 52 53 uniform sampler2D gltf_clippingPlanes; 54 uniform mat4 gltf_clippingPlanesMatrix; 55 uniform vec4 gltf_clippingPlanesEdgeStyle; 56 void main() 57 { 58 gltf_clip_main(); 59 float clipDistance = clip(gl_FragCoord, gltf_clippingPlanes, gltf_clippingPlanesMatrix); 60 vec4 clippingPlanesEdgeColor = vec4(1.0); 61 clippingPlanesEdgeColor.rgb = gltf_clippingPlanesEdgeStyle.rgb; 62 float clippingPlanesEdgeWidth = gltf_clippingPlanesEdgeStyle.a; 63 if (clipDistance > 0.0 && clipDistance < clippingPlanesEdgeWidth) 64 { 65 gl_FragColor = clippingPlanesEdgeColor; 66 } 67 }
其中,gltf_clip_main函數中的內容是沒有追加平面裁剪之前片源着色器中的main函數中的代碼,主要是負責繪制模型本身。我們看到和平面裁剪相關的uniform變量有gltf_clippingPlanes、gltf_clippingPlanesMatrix、gltf_clippingPlanesEdgeStyle三個,其中gltf_clippingPlanes是sampler2D類型,將所有的裁剪平面的position、normal放到一張圖片中;gltf_clippingPlanesMatrix變量是將平面從世界坐標轉換到視空間下的變換矩陣;gltf_clippingPlanesEdgeStyle存儲了裁剪的樣式信息,其中gltf_clippingPlanesEdgeStyle.rgb存儲了裁剪銜接處模型的顏色,gltf_clippingPlanesEdgeStyle.a存儲了裁剪邊界處的像素寬度。
在調用gltf_clip_main函數后,通過clip函數實現裁剪,並在像素沒有剔除的情況下返回該片源與裁剪平面的像素距離。clip函數是整個裁剪功能實現的關鍵所在,我們將精力重點放在clip這個函數上。通過czm_windowToEyeCoordinates這個Cesium自帶函數計算當前片源在視空間下的三維坐標position,然后通過czm_metersPerPixel這個函數計算視空間下position這個位置每個像素代表的空間長度。接下來就是通過一個for循環計算每個裁剪平面對該像素的影響。我們來分析一下for循環中的內部代碼。首先通過getClippingPlane這個函數計算出在視空間下的平面坐標,clipNormal表示平面的法線,clipPosition代表平面的位置,然后position.xyz - clipPosition計算出了模型頂點和平面位置之間的向量,此處暫記為向量m,dot(clipNormal, (position.xyz - clipPosition))得到該向量和平面法線的點乘結果,由於clipNormal為單位向量,所以dot(clipNormal, (position.xyz - clipPosition))的結果就是向量m在法線方向上的投影長度,用這個長度除以pixelWidth轉換為像素,記為amount。clipAmount取每次平面計算結果的最大值,對於單個的平面裁剪當amount < 0時,將該片源剔除,對於多個平面,通過clipped && (amount <= 0.0)進行判斷,最后在沒剔除的情況下返回clipAmount,這就是clip函數的所有內容。
通過clip函數計算出了clipDistance(模型頂點和平面的像素距離),最后就是設置裁剪處的顏色gl_FragColor = clippingPlanesEdgeColor。好了,這就是模型平面裁剪的所有內容了。
四 總結
模型的平面裁剪都是在片源着色器中完成的,空間位置的計算都是在視空間下進行。視空間在一些GPU效果實現中發揮着很大作用,很多計算都是在視空間下進行的。第一篇博客,語言組織,頁面布局都沒有經驗,不足之處請大家諒解,哈哈。睡覺,睡覺,睡覺!!!
PS:Cesium交流可以掃碼加群,期待你的加入!!!