WebGL 單通道wireframe渲染

如果要把一個對象的線框繪製出來,一般的方法是先繪製實體對象,然後通過gl.LINES的模式再繪製一遍模型,此時模型的線框就會被繪製出來。

gl.LINES的問題

  1. 此方法需要繪製兩遍對象,因此會造成性能的損失。
  2. 使用此種方式繪製線框的時候,深度值偏移是必要的。那是因爲,線條的光柵化過程和多邊形的光柵化過程並不是完全一致的。這就導致繪製一個多邊形的邊和繪製多邊形本身,相同位置的片元,其深度值可能並不一樣。

線段和多邊形的光柵化不完全一致,爲了避免z-fighting,也需要一個深度偏移。
但是,添加一個偏移並不能完美的解決問題。 這將會導致一些本該被隱藏的線段,未被遮擋。

  1. 使用gl.LINES的另外一個問題是,在WebGL中,存在一個bug,就是線寬只能設置爲1。請參考以下文章:
    https://www.jianshu.com/p/cee4ce496e78WebGL 繪製Line的bug(一)

因此本文將會介紹一種方法,可以在一個pass內繪製對象及其線框。

原理

我們知道,一般對象都是由三角形組成的。而要顯示的線框,正好是三角形的邊,如果在繪製的時候,給三角形的邊一個不同的顏色,便可以實現在對象上面繪製線框的效果。
那麼現在的問題是,如何確定三角形的邊呢?

重心座標系

要確定三角形的變,可以使用重心座標系。有關重心座標的的說明,可以參考:
https://zh.wikipedia.org/wiki/%E9%87%8D%E5%BF%83%E5%9D%90%E6%A0%87
對於三角形而言,重心座標可以這樣定義:
三角形所在平面上的任意一點P(笛卡爾座標),可以通過三角形的三個頂點A、B、C(笛卡爾座標)來表示:
P = Ax + By + C * Z,其中(x、y、z)便是重心座標。由此可以看出P點其實是A、B、C點加權之和。
如下圖所示,A點的重心座標是(1,0,0),B點的重心座標是(0,1,0),C點的重心座標是(0,0,1)

重心座標確定三角形的邊

由上面的講解 和圖片展示可以得知,重心座標(x,y,z)中任何一個值爲0的點,都在三角形的邊上。不過在實際的圖形渲染中,邊的寬度不可能是0,而應該是一個大於0的值,所以一般可以指定一個要繪製的線寬width,如果任何一個點的重心座標(x,y,z)中的人一個分量的值小於這個線寬width,可以認爲在邊上。

代碼實現

基於上面說的原理,首先需要定義頂點的重心座標,對於一個三角形來說,可以把三個頂點的中心座標分別指定爲(1,0,0)、(0,1,0)、(0,1,0)即可。而對於一個四邊形,有四個頂點 0,1,2,3,而繪製的時候使用索引 0,1,2, 2,1,3來繪製,此時可以把重心座標定義如下:

  var barycentric = [
        1,0,0,  0,1,0, 0,0, 1,  1,0,0,
   ];

然後,在着頂點色器中定義對應的attribute變量,由於重心座標最終需要傳遞到片元着色器中,所以還需要對應的varying變量:

      attribute vec3 aBarycentric;
    ...
    varying vec3 vBarycentric;
    void main(){
        ...
        vBarycentric = aBarycentric;
    }

然後,在片元着色器中判斷,如果vBarycentric的任意一個分量的值小於指定的寬度值,則顏色爲指定的線框顏色:

if(any(lessThan(vBarycentric, vec3(0.02)))){
          gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
        }
        else{
            gl_FragColor = color;
        }

通過上面代碼,最終繪製的立方體效果如下:


去掉鋸齒

從上面的立方體繪製的效果圖可以看出,線框的鋸齒很嚴重,而且線的寬度不是一致的。這是因爲,之前的判斷是基於三角形表面的,通過光柵化之後,由於線條的角度等原因,最終在屏幕上面的寬度就變得不一致了。

使用fwidth方法

要線寬的判斷基於屏幕,需要使用到一個方法fwidth。該方法需要WebGL 引入一個擴展之後才能使用。該擴展是:OES_standard_derivatives。
首先使用WebGL的getExtension方法獲取該擴展,代碼如下:

gl.getExtension("OES_standard_derivatives");

然後在shader中啓用這個擴展,代碼如下:

#extension GL_OES_standard_derivatives : enable

然後通過fwidth函數,把vBarycentric的值縮放到 vBarycentric在屏幕變化的範圍之內,代碼如下:

 vec3 d = fwidth(vBarycentric);
 vec3 a3 = smoothstep(vec3(0.0), d*2.0, vBarycentric);

fwidth表示一個變量在屏幕空間的x軸變化dFdx + y軸變化dFdx之和。 其中涉及到dFdx、dFdy和fwidth的相關介紹,筆者將會在後續的文章中介紹。
在獲取了基於屏幕像素空間的的重心座標a3之後,變可以通過通過該變量來進行判斷,並繪製出指定寬度的線框:

gl_FragColor.rgb = mix(vec3(0.0,0.0,0.0), vec3(1.0), min(min(a3.x, a3.y), a3.z));

通過改良之後的繪製效果如下,可以看出明細效果改進了很多:


四邊形線框

前面我們看到的都是三角形的線框,有的時候,我們希望獲取四邊形的線框,應該怎麼處理呢? 其實此時,只需要調整下頂點的重心座標即可,在前文中,一個四邊形的四個頂點的重心座標如下:

  var barycentric = [
        1,0,0,  0,1,0, 0,0, 1,  1,0,0,
   ];

如果把四邊形的四個頂點的重心座標調整如下:

  var barycentric = [
       1,0,0,  1,1,0, 0,0, 1,  0,0,0,  //前
    ];

便可以達到效果,最終繪製的效果如下圖所示:


後記

英偉達也提出過繪製線框的解決方案,參考下面鏈接:
https://developer.download.nvidia.com/SDK/10/direct3d/Source/SolidWireframe/Doc/SolidWireframe.pdf
不過該方案是基於Geometry Shader技術來實現的,而該技術在WebGL並未實現,所以該方案不能很好的移植到到WebGL。

如果獲取完整源代碼,請關注公衆號。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章