WebGL 单通道wireframe渲染
如果要把一个对象的线框绘制出来,一般的方法是先绘制实体对象,然后通过gl.LINES的模式再绘制一遍模型,此时模型的线框就会被绘制出来。
gl.LINES的问题
- 此方法需要绘制两遍对象,因此会造成性能的损失。
- 使用此种方式绘制线框的时候,深度值偏移是必要的。那是因为,线条的光栅化过程和多边形的光栅化过程并不是完全一致的。这就导致绘制一个多边形的边和绘制多边形本身,相同位置的片元,其深度值可能并不一样。
线段和多边形的光栅化不完全一致,为了避免z-fighting,也需要一个深度偏移。
但是,添加一个偏移并不能完美的解决问题。 这将会导致一些本该被隐藏的线段,未被遮挡。
- 使用gl.LINES的另外一个问题是,在WebGL中,存在一个bug,就是线宽只能设置为1。请参考以下文章:
https://www.jianshu.com/p/cee... 绘制Line的bug(一)
因此本文将会介绍一种方法,可以在一个pass内绘制对象及其线框。
原理
我们知道,一般对象都是由三角形组成的。而要显示的线框,正好是三角形的边,如果在绘制的时候,给三角形的边一个不同的颜色,便可以实现在对象上面绘制线框的效果。
那么现在的问题是,如何确定三角形的边呢?
重心坐标系
要确定三角形的变,可以使用重心坐标系。有关重心坐标的的说明,可以参考:
https://zh.wikipedia.org/wiki...
对于三角形而言,重心坐标可以这样定义:
三角形所在平面上的任意一点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。
如果获取完整源代码,请关注公众号。