3D對(duì)象描邊章喉,常用于物體的高亮顯示刨裆。本文將詳細(xì)介紹如何在Cesium中實(shí)現(xiàn)該功能:提取物體完整的邊緣聂示,用不同顏色區(qū)分可見和被遮擋部分告唆;根據(jù)法線夾角閾值提取平面邊界棺弊;支持高亮顯示Entity、Primitive和3DTiles等擒悬。(文末附源碼)
1 問題描述
Cesium提供了Silhouette后期處理可以實(shí)現(xiàn)3D對(duì)象描邊模她,但是描邊結(jié)果并不能完全顯示物體的輪廓,如圖:
可以看出所描繪的輪廓存在如下不足:
(1)無法提取被遮擋部分的輪廓懂牧;
(2)未被遮擋部分的輪廓不完整侈净。
熟悉three.js的小伙伴可能已經(jīng)想起了OutlinePass的效果,今天我們就來將這個(gè)效果拿到Cesium中實(shí)現(xiàn)僧凤,不過我們要增加點(diǎn)難度畜侦,把THREE.EdgeGeometry的效果也拿過來,并且不修改幾何體躯保,實(shí)現(xiàn)更加實(shí)用的3D描邊效果旋膳。
2 本文目標(biāo)
Cesium后期處理中同時(shí)實(shí)現(xiàn):
(1)three.js OutlinePass的描邊效果,即提取物體完整的邊界途事,包括被遮擋的部分验懊,同時(shí)可以選擇用不同顏色區(qū)分可見和被遮擋部分擅羞;
(2)three.js EdgeGeometry的描邊效果,即根據(jù)法線夾角閾值提取平面邊界义图。
(3)支持高亮顯示Cesium.Entity减俏、Cesium.Primitive以及Cesium 3D Tiles要素。
3 關(guān)鍵技術(shù)
在開始實(shí)現(xiàn)本文目標(biāo)效果之前歌溉,先介紹一下兩個(gè)關(guān)鍵技術(shù):
(1)Cesium后期處理技術(shù)垄懂;
(2)Cesium多次渲染技術(shù)
3.1 Cesium后期處理
我們先通過幾段代碼了解一下Cesium后期處理開發(fā)流程。
3.1.1 創(chuàng)建一個(gè)基本的后期處理節(jié)點(diǎn)
創(chuàng)建一個(gè)后期處理節(jié)點(diǎn)的JavaScript代碼如下:
其中singleStage.frag為片段著色器代碼文件(直接 import 是因?yàn)樽髡呤褂肕esh-3D Engine的命令行工具作為開發(fā)環(huán)境痛垛,服務(wù)端自動(dòng)轉(zhuǎn)成js文件并返回js代碼)草慧,內(nèi)容如下:
這個(gè)處理節(jié)點(diǎn)沒有做任何加工處理,只是簡單的復(fù)制場景匙头。從中可以看出漫谷,一個(gè)基本的后期處理節(jié)點(diǎn)包含兩大部分: (1) fragmentShader 片段著色器代碼:對(duì)每一幀的渲染結(jié)果進(jìn)行加工處理。簡單解釋一下內(nèi)置變量:
colorTexture 整個(gè)場景的顏色紋理或者前一個(gè)后期處理結(jié)果顏色紋理蹂析;
colorTextureDimensions 顏色紋理尺寸舔示,x為寬度,y為高度电抚;
提示: Cesium會(huì)對(duì)每一個(gè)名為xxx惕稻、類型為Cesium.Texture的uniform變量都增加一個(gè)名為xxxDimensions的uniform變量。用來傳遞紋理尺寸蝙叛。
depthTexture 整個(gè)場景的深度紋理或者前一個(gè)后期處理結(jié)果深度紋理俺祠;
(2) uniforms 向fragmentShader中傳遞外部數(shù)據(jù)〗枇保可傳遞的數(shù)據(jù)類型分為兩大類:
常量類:布爾(Boolean),字符串(String),向量(Cesium.CartesianX)蜘渣,矩陣(Cesium.MatrixX)
注意:字符串類型的uniform值可以是:圖片路徑;前續(xù)節(jié)點(diǎn)名稱肺然,用以訪問節(jié)點(diǎn)的顏色紋理蔫缸。
回調(diào)函數(shù):返回值類型同常量類。在后續(xù)的文章中會(huì)用到這類uniform际起。
3.1.2拾碌、創(chuàng)建多個(gè)后處理節(jié)點(diǎn)
有些效果可能需要多次對(duì)場景進(jìn)行加工、合成才能實(shí)現(xiàn)加叁。Cesium提供PostProcessStageComposite類用來解決此類需求倦沧。示意代碼如下:
參數(shù)inputPreviousStageTexture值解釋:
true:stages中各節(jié)點(diǎn)的colorTexture為其前一節(jié)點(diǎn)的顏色紋理;
false:stages所有節(jié)點(diǎn)的colorTexture相同(不考慮uniforms中自定義colorTexture的情況)它匕。
3.1.3展融、設(shè)置選中對(duì)象
Cesium后期處理兩個(gè)類都提供selected屬性,用來生成選中對(duì)象id查詢紋理豫柬。selected接受的對(duì)象只有一個(gè)要求:包含pickId或者pickIds屬性告希。凡是可以通過Cesium.Scene pick方法拾取到的對(duì)象扑浸,都可以找到對(duì)應(yīng)的pickId,反過來燕偶,如果想要被pick到喝噪,也需要再創(chuàng)建DrawCommand的時(shí)候生成pickId。底層不同對(duì)象構(gòu)建pickId的邏輯差別很大指么,導(dǎo)致獲取pickId的方法也不盡相同酝惧。
Entity和Primitive 從picked.primitive._pickIds查找;
3D Tiles要素 已單體化的要素:picked.pickId;未單體化的瓦片:picked.content._model._pickIds;
3.2伯诬、Cesium多次渲染
后期處理技術(shù)允許我們對(duì)場景渲染結(jié)果進(jìn)行加工處理晚唇,但是有些情況(比如SMAA、MSAA抗鋸齒算法盗似,以及接下來我們要實(shí)現(xiàn)的輪廓提取等)哩陕,我們需要對(duì)場景全部或者部分對(duì)象進(jìn)行臨時(shí)修改、顯隱控制并重新渲染赫舒。Cesium并沒有提供現(xiàn)成的技術(shù)悍及,所以我們需要?jiǎng)邮秩?shí)現(xiàn),這部分涉及Cesium底層渲染技術(shù)接癌,這里不展開介紹心赶,有必要的話可以專門寫一篇來補(bǔ)充,這里列出接口定義和使用示例缺猛。
3.2.1园担、實(shí)現(xiàn)功能
在Cesium主渲染流程完成后,指定后期處理節(jié)點(diǎn)(stage)開始前枯夜,將選中的對(duì)象或者沒有被選中的對(duì)象渲染到緩沖區(qū),然后在該后期處理節(jié)點(diǎn)通過texture屬性獲取當(dāng)前通道的顏色數(shù)據(jù)艰山,通過depthTexture獲取深度數(shù)據(jù)湖雹。
3.2.2、接口定義(TypeScript)
關(guān)鍵參數(shù)說明:
renderType 設(shè)置需要渲染的對(duì)象:
all——當(dāng)前渲染隊(duì)列中的所有繪圖命令
selected——渲染隊(duì)列中被選中的對(duì)象關(guān)聯(lián)的繪圖命令曙搬,只過濾用整個(gè)對(duì)象內(nèi)所有幾何體的所有頂點(diǎn)pickId都相同的情況摔吏,如果將pickId寫入幾何體的頂點(diǎn)則需要手動(dòng)在shader中過濾(通過調(diào)用czm_selected()來判斷是否為選中對(duì)象)
unselected——渲染隊(duì)列中的所有未被選中的對(duì)象關(guān)聯(lián)的繪圖命令
stage 設(shè)置綁定的后期處理節(jié)點(diǎn)。必須綁定后期處理節(jié)點(diǎn)纵装,否則渲染通道將不會(huì)被調(diào)用執(zhí)行渲染工作征讲,也無法獲取depthTexture和exture。
texture 獲取顏色紋理橡娄。請(qǐng)?jiān)趗niform回調(diào)函數(shù)中獲取诗箍,因?yàn)榧y理在后期處理節(jié)點(diǎn)的update被調(diào)用時(shí)才會(huì)生成,提前獲取不到挽唉。
depthTexture 獲取深度紋理滤祖。
shaderRedefine 指定shader重定義方式筷狼,即指示在shader代碼追加到繪圖命令本身的shader之后,如何執(zhí)行繪圖邏輯:
add——并且調(diào)用原始的main函數(shù)匠童,執(zhí)行默認(rèn)繪圖邏輯埂材,追加部分的繪圖邏輯;
replace——不調(diào)用原始的main函數(shù),只執(zhí)行追加部分的繪圖邏輯汤求。
vertexShader 追加的頂點(diǎn)著色器代碼俏险,可選。
fragmentShader 追加的片元著色器代碼扬绪,可選竖独。
內(nèi)置的預(yù)編譯定義(在頂點(diǎn)和片元著色器中都可以訪問):
HAS_NORMAL 指示頂點(diǎn)著色器中存在名為normal的屬性。
HAS_V_NORMAL 指示頂點(diǎn)屬性中不存在為normal的屬性勒奇,但存在名為v_normal的varying變量预鬓。
內(nèi)置的函數(shù)czm_selected:識(shí)別當(dāng)前像元是否為選中對(duì)象。
3.2.3赊颠、使用示例
創(chuàng)建后期渲染通道
片段著色代碼(renderPass.frag):
javascript代碼:
在后期處理節(jié)點(diǎn)中使用
片元著色器代碼(readMaskPass.frag):
javascript代碼:
4格二、實(shí)現(xiàn)過程
準(zhǔn)備這么久,終于到了正文了竣蹦。實(shí)現(xiàn)過程整體分為以下幾步:
(1)渲染整個(gè)場景顶猜,獲得場景顏色和深度;
(2)渲染被選中對(duì)象痘括,將法線保存到顏色紋理长窄,同時(shí)得到該對(duì)象深度紋理;
(3)提取選中對(duì)象輪廓纲菌;
(4)將輪廓疊加到場景挠日。
其中關(guān)鍵的步驟是(2)和(3),而(3)中的Shader代碼幾乎可以直接照搬t(yī)hree.js OutlinePass的代碼翰舌,相對(duì)容易嚣潜;(2)是比較困難,難點(diǎn)在于如何只渲染被選中對(duì)象椅贱,這就用到了上文介紹的Cesium多次渲染技術(shù)了懂算。下面分步詳細(xì)介紹。
4.1庇麦、渲染整個(gè)場景
這一步在創(chuàng)建場景之后Cesium就一直進(jìn)行计技,我們只需要?jiǎng)?chuàng)建好場景即可。
4.2山橄、渲染選中對(duì)象
這一步?jīng)Q定了我們實(shí)現(xiàn)的效果有別與Cesium自帶的描邊算法垮媒,因?yàn)檫吘壸R(shí)別算法原理基本是一樣的,差別在于輸入的場景顏色和深度。
首先為提取外邊界的做準(zhǔn)備涣澡,片元著色器只需要通過czm_selected來判斷是否為被選中對(duì)象(要素)贱呐,如果不是則discard即可;其次我們還需要提取平面邊界入桂,即提取所有由近似處于同一平面的三角面組成的多邊形邊界奄薇,為此我們將法線保存到顏色紋理中。
頂點(diǎn)著色器如下(normalDepth.vert):
片元著色器代碼如下(normalDepth.frag):
創(chuàng)建CesiumRenderPass實(shí)例抗愁,這里不需要外部參數(shù)馁蒂,JavaScript代碼如下:
4.3、提取選中對(duì)象輪廓
我們先來實(shí)現(xiàn)僅提取對(duì)象邊緣的算法蜘腌,不做遮擋檢測(cè)沫屡,所有邊緣使用同一顏色描繪。
這部分的shader代碼直接取自 three.js? OutlinePass 的 getEdgeDetectionMaterial 函數(shù)撮珠,只對(duì)uniform做點(diǎn)處理沮脖。關(guān)鍵代碼如下(singleEdgeColor.frag):
其中c1、c2芯急、c3勺届、c4為當(dāng)前像元按線寬(outlineWidth)偏移一定像元之后采樣到的顏色,顏色的r娶耍、g免姿、b三通道保存的是選中對(duì)象的法線數(shù)據(jù),a通道為場景透明度榕酒,a大于0則是選中對(duì)象內(nèi)部胚膊,a為0表示選中對(duì)象外部。
關(guān)鍵的JavaScript代碼如下:
maskTexture和maskDepthTexture分別是4.2處理結(jié)果的顏色和深度紋理想鹰。
效果如下圖:
這部分和three.js OutlinePass有點(diǎn)區(qū)別,主要在深度讀取和比較方法不同辑舷。關(guān)鍵的shader代碼如下:根據(jù)深度識(shí)別被遮擋部分:
其中深度compareDepth定義如下:
maskDepthTexture為4.2渲染結(jié)果深度紋理肩榕。效果如下圖:
4.5 提取平面邊界
THREE.EdgeGeometry邊緣提取的核心原理是:計(jì)算相鄰兩個(gè)三角面的法線夾角,如果夾角小于給定閾值則認(rèn)為兩者在同一平面惩妇,進(jìn)而刪除公共邊。那么在shader中如何實(shí)現(xiàn)呢筐乳?前文已經(jīng)做好了足夠的準(zhǔn)備了歌殃,我們只需要按照 4.2 和 4.3 比較深度和透明度的方法,再實(shí)現(xiàn)一個(gè)比較頂點(diǎn)法線的方法即可蝙云。核心shader代碼如下:
其中 thresholdAngle 為相鄰三角面法線夾角閾值氓皱,單位為弧度,通過uniform傳入。
我們還需要對(duì) main 函數(shù)進(jìn)行修改波材,主要是5.2中 d 的計(jì)算股淡,有變化:
至此核心的步驟就全部完成了。想要得到上文的各個(gè)效果圖廷区,我們還需將得到的輪廓效果疊加到原始場景中唯灵。
4.6、將輪廓疊加到場景
這里簡單粗暴的將兩張紋理貼圖的顏色做加運(yùn)算隙轻。
其中l(wèi)ineTexture為輪廓提取節(jié)點(diǎn)的顏色紋理埠帕,colorTexture為原始場景的顏色紋理。
5玖绿、應(yīng)用示例
本文大體把實(shí)現(xiàn)流程的關(guān)鍵細(xì)節(jié)列出來敛瓷,最后我們將這些細(xì)節(jié)封裝,以便在項(xiàng)目中應(yīng)用斑匪。再貼一下封裝好的接口定義(createEdgeStage.js):