您當前的位置:首頁 > 攝影

如何使用 Oasis 快速實現描邊效果

作者:由 螞蟻 RichLab 前端團隊 發表于 攝影時間:2022-04-08

前言

描邊可以分為

外描邊、內描邊、後處理描邊

外描邊

只顯示整個模型的外輪廓,模型的內部細節是無法描邊的,比如眼睛,眉毛這些內部元素;

內描邊

不僅可以顯示外輪廓,還能夠顯示眼睛、眉毛、腰帶這種內部輪廓;

後處理描邊

其實是檢測顏色的突變,無論模型是否有凹凸細節,只要有顏色發生變化,都會被當作輪廓繪製。

本文介紹兩種描邊方法:一種是先渲染想要描邊的模型,然後沿法線撐大模型,將描邊效果(如純黑)作為該模型的材質再渲染一遍,其中可以利用模版測試和正面剔除來達到第二遍的pass只渲染描邊;另一種是影象後處理,利用2D圖形演算法針對整幅畫面進行邊緣檢測。在 Oasis 中,都有相應的元件能夠方便快速地實現。

使用多渲染器實現內外描邊

引擎截至v0.6還不支援 Shader 多 Pass,所以本文介紹透過多渲染器的方式來達到同樣的目的

在 Oasis 中 3D 模型的渲染可以用渲染器元件來實現,渲染器元件由網格和材質組成,

網格

負責儲存頂點、法線等元資料,

材質

負責畫素著色。我們一般不需要手動設定渲染器,透過載入 glTF 模型 會自帶渲染器。當我們渲染描邊的時候,因為多個渲染器元件可以共享同一個網格或者材質,所以我們只需要新增一個渲染器元件,網格從 glTF 裡面獲取,材質參考自定義材質教程建立一個

純色的描邊材質

就行。

新增渲染器的程式碼如下:

const

renderers

MeshRenderer

[]

=

[];

// 從 glTF 中獲取所有渲染器元件

rootEntity

getComponentsIncludeChildren

MeshRenderer

renderers

);

// 新增描邊渲染器元件

renderers

forEach

((

renderer

=>

{

const

entity

=

renderer

entity

// 新增渲染器元件

const

borderRenderer

=

entity

addComponent

MeshRenderer

);

// 網格 - 共享網格資料

borderRenderer

mesh

=

renderer

mesh

// 設定描邊材質,具體的會在下文中介紹

borderRenderer

setMaterial

borderMaterial

);

});

外描邊

外描邊可以參考模板測試教程來實現,根本原理就是透過比較

模版緩衝區

的值來決定是否渲染該片元。

1. 填入模板

在 Oasis 中操作渲染狀態只需要對材質的 renderState 賦值即可。按照上面的思路,我們先開啟原渲染器的模板狀態,並強制寫入 1,因為 canvas 的預設模版值為 0 ,所以第一次渲染,保證了渲染的畫素位置模版值為1,其餘的地方為0 :

const material = renderer。getBorderMaterial();

const stencilState = material。renderState。stencilState;

// 開啟模版測試

stencilState。enabled = true;

// 設定參考值為 1

stencilState。referenceValue = 1;

// 透過時將“參考值”寫入模版緩衝。因為預設“總是透過”,所以強制寫入1

stencilState。passOperationFront = StencilOperation。Replace;

如下圖的黑色部分表示模版值為1,白色部分為預設0:

如何使用 Oasis 快速實現描邊效果

2. 撐大

接下來,我們需要將模型的所有頂點沿著法線方向撐大,可以參考自定義材質教程的寫法,在

頂點著色器

中對

gl_Position

進行縮放:

const

vertex

=

`

attribute

vec3

POSITION

attribute

vec3

NORMAL

uniform

float

u_width

uniform

mat4

u_MVPMat

uniform

mat4

u_modelMat

uniform

mat4

u_viewMat

uniform

mat4

u_projMat

uniform

mat4

u_normalMat

void

main

()

{

vec4

mPosition

=

u_modelMat

*

vec4

POSITION

1。0

);

// 得到世界座標系下的歸一化法線向量

vec3

mNormal

=

normalize

mat3

u_normalMat

*

NORMAL

);

// 將模型位置沿著法線方向縮放 u_width

mPosition

xyz

+=

mNormal

*

u_width

gl_Position

=

u_projMat

*

u_viewMat

*

mPosition

}

`

描邊材質只需要在片元著色器中設定純色即可:

const

fragment

=

`

uniform

vec3

u_color

void

main

(){

gl_FragColor

=

vec4

u_color

1

);

}

`

建立完描邊材質後,我們將它繫結到新增的渲染器元件上,我們會發現縮放的渲染器元件生效了,但是會擋在原來的渲染器前面,如下圖:

如何使用 Oasis 快速實現描邊效果

這是因為我們還沒有對描邊材質配置

模版狀態

,這時候生效的是深度測試,因為撐大後的模型離螢幕更近,所以覆蓋了原來的渲染器。

3. 模板測試

第一個渲染器已經將 1 填入了模板緩衝,我們可以在渲染撐大的模型的時候,只渲染那些不等於1的畫素,即將模板測試的比較函式設定為

不相等

如下圖所示,利用模板測試只渲染了撐大的部分,即描邊部分:

如何使用 Oasis 快速實現描邊效果

描邊材質的模板測試程式碼如下:

// 保證最後再渲染描邊材質

material

renderQueueType

=

RenderQueueType

Transparent

+

1

// 雙面渲染防止一些破面

material

renderState

rasterState

cullMode

=

CullMode

Off

const

stencilState

=

material

renderState

stencilState

// 開啟模版測試

stencilState

enabled

=

true

// 設定比較值

stencilState

referenceValue

=

1

// 透過條件:referenceValue != 模板緩衝值

stencilState

compareFunctionFront

=

CompareFunction

NotEqual

stencilState

compareFunctionBack

=

CompareFunction

NotEqual

最終達到的外描邊效果:

如何使用 Oasis 快速實現描邊效果

如何使用 Oasis 快速實現描邊效果

內描邊

因為模板測試只能渲染撐大的那一部分,所以無法顯示內輪廓,比如腰帶等等,我們一般使用第二遍 shader pass 採用正面剔除的方法來實現內描邊。

1. 渲染正面

正常渲染模型,不需要設定任何模版狀態或者深度狀態。

2. 撐大

撐大部分的程式碼還是和前面一樣,沿著法線縮放,參考前面即可。

3. 渲染背面

// 保證最後渲染描邊材質

material。renderQueueType = RenderQueueType。Transparent + 1;

// 剔除正面,只渲染背面

material。renderState。rasterState。cullMode = CullMode。Front;

渲染撐大後的背面,能夠保證撐大的模型不會因為深度測試擋住本來的模型,也不會因為模板測試只渲染模型的外輪廓,內描邊效果如下圖,可以看到,模型的內部輪廓也顯示了:

如何使用 Oasis 快速實現描邊效果

如何使用 Oasis 快速實現描邊效果

使用後處理實現描邊

在前言裡面提到過,除了使用

多渲染器方案

實現內外描邊效果外,還可以使用

後處理方案

對影象進行邊緣檢測。

後處理的優點:

在某些場景下(如模型面數非常多)能夠提高效能。

能夠整體處理描邊效果,解決多模型描邊重合的問題。

能夠解決模型撐大後破面的問題(如正方體撐大後因為法線

拐角超過90度

,會導致破面)。

1. 渲染離屏紋理

Oasis 使用 RenderTarget 來渲染離屏紋理,即不渲染到螢幕,而是渲染到一張紋理上,然後利用這張離屏紋理來實現各種後處理特效,比如描邊。

const { width, height } = engine。canvas;

const renderColorTexture = new RenderColorTexture(engine, width, height);

const renderTarget = new RenderTarget(engine, width, height, renderColorTexture);

2.新增指令碼

RenderTarget 是過程式,我們需要利用指令碼的生命週期將整個渲染管線串起來。

我們可以給相機建立一個指令碼,在渲染場景前設定 renderTarget ,代表我們想將場景渲染到紋理上;在渲染後,我們將 renderTarget 清空,並設定了和全屏平面一樣的 Layer1,代表我們想將離屏紋理繪製到螢幕上,並進行描邊後處理:

class

PostScript

extends

Script

{

renderTarget

RenderTarget

onBeginRender

camera

Camera

void

{

// 繪製場景到紋理上

camera

renderTarget

=

this

renderTarget

camera

cullingMask

=

Layer

Layer0

}

onEndRender

camera

Camera

void

{

camera

renderTarget

=

null

// 繪製到螢幕上,並在screenMaterial裡面進行描邊後處理

camera

cullingMask

=

Layer

Layer1

// 再繪製一遍,只繪製全屏紋理

camera

render

();

}

}

// 建立全屏 Plane, 用來實現全屏後處理特效。

const

screen

=

rootEntity

createChild

“screen”

));

const

screenRenderer

=

screen

addComponent

MeshRenderer

);

screen

layer

=

Layer

Layer1

screenRenderer

mesh

=

PrimitiveMesh

createPlane

engine

2

2

);

// 自定義材質,shader 裡面執行邊緣檢測演算法。

const

material

=

this

getScreenMaterial

engine

);

screenRenderer

setMaterial

material

);

const

renderColorTexture

=

new

RenderColorTexture

engine

width

height

);

const

renderTarget

=

new

RenderTarget

engine

width

height

renderColorTexture

);

// 傳入預設管線得到的離屏紋理。

material

shaderData

setTexture

“u_texture”

renderColorTexture

);

// 新增指令碼

const

screenRenderer

=

screen

addComponent

MeshRenderer

);

screenRenderer

renderTarget

=

renderTarget

可以看到,我們建立了一個掩碼為 Layer1 的平面, 只用來渲染一個

全屏平面

,如果直接渲染離屏紋理,看到的效果就是場景直接渲染在螢幕上,但是我們想要描邊效果,就需要對這張紋理做一點演算法處理。

3. sobel 邊緣檢測

邊緣檢測的方法很多,這裡用 sobel 卷積因子來舉例。我們把後處理中的每一個畫素亮度分別進行縱向、橫向的卷積運算,即每個畫素的周邊9個畫素亮度分別乘以下圖這些權重:

如何使用 Oasis 快速實現描邊效果

G_x

如何使用 Oasis 快速實現描邊效果

G_y

黃色和紅色為分別對應的權重,可以看到,如果一個畫素的周邊訊號(如亮度)變化越陡峭,則卷積結果越大,我們可以利用這個特性來檢測邊緣,因為邊緣那幾個畫素的亮度變化都是比較陡峭的。

如何使用 Oasis 快速實現描邊效果

4. shader 程式碼

基於 sobel 的邊緣檢測演算法,我們可以透過離屏紋理,繪製描邊效果,shader 程式碼如下:

// 描邊顏色

uniform

vec3

u_color

// 離屏紋理

uniform

sampler2D

u_texture

// 紋素大小

uniform

vec2

u_texSize

varying

vec2

v_uv

// 對應畫素的亮度

float

luminance

vec4

color

{

return

0。2125

*

color

r

+

0。7154

*

color

g

+

0。0721

*

color

b

}

// 前面提到的 sobel 卷積運算,得到陡峭程度。

float

sobel

()

{

// sobel 卷積因子

float

Gx

9

=

float

[](

-

1。0

0。0

1。0

-

2。0

0。0

2。0

-

1。0

0。0

1。0

);

float

Gy

9

=

float

[](

-

1。0

-

2。0

-

1。0

0。0

0。0

0。0

1。0

2。0

1。0

);

float

texColor

float

edgeX

=

0。0

float

edgeY

=

0。0

vec2

uv

9

];

// 周邊的9個畫素的紋理座標。

uv

0

=

v_uv

+

u_texSize

xy

*

vec2

-

1

-

1

);

uv

1

=

v_uv

+

u_texSize

xy

*

vec2

0

-

1

);

uv

2

=

v_uv

+

u_texSize

xy

*

vec2

1

-

1

);

uv

3

=

v_uv

+

u_texSize

xy

*

vec2

-

1

0

);

uv

4

=

v_uv

+

u_texSize

xy

*

vec2

0

0

);

uv

5

=

v_uv

+

u_texSize

xy

*

vec2

1

0

);

uv

6

=

v_uv

+

u_texSize

xy

*

vec2

-

1

1

);

uv

7

=

v_uv

+

u_texSize

xy

*

vec2

0

1

);

uv

8

=

v_uv

+

u_texSize

xy

*

vec2

1

1

);

for

int

i

=

0

i

<

9

i

++

{

// 計算下對應紋素的亮度

texColor

=

luminance

texture2D

u_texture

uv

i

]));

// 橫向卷積

edgeX

+=

texColor

*

Gx

i

];

// 縱向卷積

edgeY

+=

texColor

*

Gy

i

];

}

return

abs

edgeX

+

abs

edgeY

);

}

void

main

(){

float

sobelFactor

=

sobel

();

// sobel 越大,說明陡峭程度越大,越接近描邊顏色。

gl_FragColor

=

mix

texture2D

u_texture

v_uv

),

vec4

u_color

1。0

),

sobelFactor

);

}

得到渲染結果如下:

如何使用 Oasis 快速實現描邊效果

結語

本篇所有相關程式碼和效果可以透過 Playground 檢視。

截至 Oasis 0。6,Oasis 還沒有封裝相應的全屏後處理元件和 shader 多 pass 能力,使用者需要建立多個渲染器,或者建立離屏紋理、指令碼、全屏Plane ,稍微有點不方便,我們在未來會封裝相應的能力,使使用者能夠專注於演算法實現,簡化流程步驟。

描邊實現還有很多方法和細節,比如鏡頭空間的

描邊粗細

,描邊的

鋸齒

問題,描邊的

破面

問題等等,一般可以針對具體的應用場景選擇適合的方法進行實現或者定製。

最後

為了給廣大的 Oasis 引擎使用者提供更高效、更便捷的工作流,編輯器對外開放是我們 Oasis 團隊的核心重點之一。為了更好的服務大家,我們準備了一份關於 “互動圖形引擎調研” 的問卷 (

幸運使用者將獲得我們準備的精美獎品

),期待您的反饋~

標簽: 描邊  渲染  uv  渲染器  材質