在绘制面的时候可能会遇到需要绘制带有“洞”的多边形,比如下面这种情况:
传统前端绘制方法
在传统的前端绘制技术中(如 SVG、Canvas 2D),可以通过设置填充方式(fill-rule)来方便的实现“洞”的效果,这种填充方式称作“evenodd”,翻译过来叫做“奇偶填充”。
下图展示了奇偶填充和普通填充在填充效果上的区别:
图片来源:http://www.enfocus.com/manuals/ReferenceGuide/PP/13/zhCN/zh-cn/common/ppr/concept/c_aa1141812.html
所谓奇偶填充就是从多边形的外层开始算,奇数边内部填充,偶数边内部不填充。
WebGL 实现
在 WebGL 中绘制面需要借助一个三角形分解的库来实现,这个库会根据面的坐标串生成坐标索引,WebGL 才能根据索引绘制出一个面来。如果三角形分解库支持传入“洞”坐标,那么可以直接利用这样的接口实现。比如常见的earcut的前端js库(https://github.com/mapbox/earcut)就能支持单独传入“洞”参数。
earcut(vertices[, holes, dimensions = 2])
问题
虽然这样做可以实现洞的效果,但前提是开发者需要知道给定的一系列闭合坐标串中哪些是外轮廓,哪些是“洞”。这无疑会增加判断成本,且牺牲了通用性。有没有可能像 SVG 或者 Canvas 2d 那样自动根据轮廓的相交关系实现奇偶填充呢?答案是可以,需要用到模板检测(Stencil Test)。
在前面的文章中,我介绍过模板测试的基本概念和使用方法。这里我们要利用 stencilOp 方法将模板缓冲区的内容做取反处理:
stencilOp(gl.INVERT, gl.INVERT, gl.INVERT)
如果只有一个面,或者多个面互不相交,那么所有区域就会做一次取反操作,这时我们定下这个值,模板检测时只有等于这个值的时候才允许绘制。这样所有面都可以绘制出来。如果其中一个区域位于另一个区域内部,则这个区域会有两次取反操作,那么这个区域就不会被绘制出来。从而实现“洞”的效果。
核心代码如下:
// 模板绘制 gl.stencilFunc(gl.ALWAYS, 0, 0xff); // 取反操作 gl.stencilFunc(gl.INVERT, gl.INVERT, gl.INVERT); gl.stencilMask(0xff); gl.colorMask(false, false, false, false); drawAreas(); // 绘制真正的面 // 0 取反一次是 0xff,那么只有等于 0xff 的情况才通过测试 gl.stencilFunc(gl.EQUAL, 0xff, 0xff); gl.stencilMask(0); gl.colorMask(true, true, true, true); drawAreas();
如果存在多个洞的话,使用模版裁剪好,还是用耳切计算三角形?
使用哪种方案和洞的个数无关。只和文中提到的是否知道在给定的一系列坐标串中,哪些是外轮廓,哪些是洞。