pixijs 的填充渲染错误,如何处理?

大家好,我是前端西瓜哥。

经常用 pixijs 的小伙伴或许注意到了,在 pixijs 下绘制多边形并使用填充,在很多情况下渲染是正确的,如果多边形是自交的,填充就会有问题。

这里使用的 pixi.js 版本为 7.4.2

我们写一段 pixijs 代码,绘制一个自交多边形。

ini 复制代码
import * as PIXI from'pixi.js';

const app = new PIXI.Application({
background: '#eee',
antialias: true,
});

const polygonPoints = [
  { x: 200, y: 27 },
  { x: 200, y: 200 },
  { x: 233, y: 110 },
  { x: 176, y: 181 },
  { x: 98, y: 138 },
  { x: 95, y: 68 },
  { x: 130, y: 27 },
];

const polygon = new PIXI.Graphics()
  .beginFill(0xff0000, 0.3)
  .lineStyle(2, 0x000000)
  .drawPolygon(polygonPoints);

app.stage.addChild(polygon);

document.body.appendChild(app.view);

渲染结果:

我们来看看是什么会导致这个原因。

在线示例

这里提供一个在线示例,读者可以修改多边形的点,观测不同三角剖分的在各种场景下的区别。

codesandbox.io/p/sandbox/2...

ear cutting 三角剖分

首先需要理解 pixijs 的图形是怎么渲染的?

pixijs 是使用 WebGL 进行渲染的,WebGL 渲染的常见方式是绘制三角形,大量的三角形进行组合,便绘制出了图形。

对于 pixijs,其图形的填充和描边,分别都需要 通过三角剖分为顶点,并设置颜色信息,然后提供给 WebGL 调用绘制。

三角剖分(triangulation)是一种将图形转为多个三角形表达的算法,是计算机图形学中的经典课题。比如矩形可以用两个三角形表达,圆形可以用一圈的三角形表达。

pixijs 选择了使用 earcut 的第三方库来队多边形进行三角剖分。

github.com/mapbox/earc...

earcut 库实现了一种改进的耳切(ear cutting)算法。

所谓耳切算法,就是遍历多边形上的点不断地将突出的三角形 "耳朵" 切掉,直到切完整个多边形,这些切出来的三角形就是我们想要的最终结果。

如下图,连续的点 2、3、4 构成的三角形不会包含其他的点,把它作为 "耳朵" 切割出来。

earcut 库的优点是快且小(压缩后 3KB),因为牺牲了质量,对于 pixijs 这种追求高性能高速度的渲染引擎,是合适的选择。

但问题在于 ear cutting 算法限制很大,只支持简单多边形,像是自交的多边形,渲染结果可能不符合预期,这便是 pixijs 的填充效果错误的原因。

简单多边形效果是正确的。

scss 复制代码
const polygon = [  { x: 200, y: 27 },  { x: 180, y: 100 },  { x: 233, y: 110 },  { x: 176, y: 181 },  { x: 98, y: 138 },  { x: 95, y: 68 },  { x: 130, y: 27 },];

earcut(polygon.map((p) => [p.x, p.y]).flat(), [], 2);
// 结果如下,三个一组构成三角形
// [
//   5, 6, 0, 
//   1, 2, 3, 
//   3, 4, 5, 
//   5, 0, 1, 
//   1, 3, 5
// ]

但如果多边形自交了,渲染明显会相当怪异,不符合预期。

一般我们需要 even-odd 或 non-zero。

即使不用这两种填充规则,用另外的填充规则,比如 figma 的将所有封闭区域都填充,ear cutting 这种把内容填充到多边形外也是很奇怪的

scss 复制代码
const polygon = [  { x: 200, y: 27 },  { x: 200, y: 200 },  { x: 233, y: 110 },  { x: 176, y: 181 },  { x: 98, y: 138 },  { x: 95, y: 68 },  { x: 130, y: 27 },];

earcut(polygon.map((p) => [p.x, p.y]).flat(), [], 2);
// 结果如下,三个一组构成三角形
// [
//   5, 6, 0,
//   2, 3, 4,
//   4, 5, 0,
//   2, 4, 0
// ]

tess 三角剖分

为此我们需要换个合适的,能处理复杂情况的算法。

我们可以看看 pixijs 的一个第三方库 @pixi-essentials/svg,这个库的作用是将 svg 转为 pixijs 的图形进行渲染。

github.com/ShukantPal/...

所以需要保持 svg 原有的正确填充效果,这个库使用了 libtess 来做三角剖分。

github.com/brendankenn...

libtess 是一个多边形镶嵌(polygon tesselation)库,从 OpenGL Utility Library(GLU)移植过来,用 javascript 重写。

对于前面用到的示例,我们换成 libtess 来试试。

因为是基于 GLU 迁移过来的,所以 API 挺复杂的,出于易用性,对 libtess 做了一层封装。

下面是 libtess 的示例中一个封装好的方法。

scss 复制代码
import * as libtess from'libtess';

const tessy = (function initTesselator() {
// function called for each vertex of tesselator output
function vertexCallback(data, polyVertArray) {
    polyVertArray[polyVertArray.length] = data[0];
    polyVertArray[polyVertArray.length] = data[1];
  }
function begincallback(type) {
    if (type !== libtess.primitiveType.GL_TRIANGLES) {
      console.log('expected TRIANGLES but got type: ' + type);
    }
  }
function errorcallback(errno) {
    console.log('error callback');
    console.log('error number: ' + errno);
  }
// callback for when segments intersect and must be split
function combinecallback(coords, data, weight) {
    // console.log('combine callback');
    return [coords[0], coords[1], coords[2]];
  }
function edgeCallback(flag) {
    // don't really care about the flag, but need no-strip/no-fan behavior
    // console.log('edge flag: ' + flag);
  }

var tessy = new libtess.GluTesselator();
// tessy.gluTessProperty(libtess.gluEnum.GLU_TESS_WINDING_RULE, libtess.windingRule.GLU_TESS_WINDING_POSITIVE);
  tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_VERTEX_DATA, vertexCallback);
  tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_BEGIN, begincallback);
  tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_ERROR, errorcallback);
  tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_COMBINE, combinecallback);
  tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_EDGE_FLAG, edgeCallback);

return tessy;
})();

function triangulate(contours) {
// libtess will take 3d verts and flatten to a plane for tesselation
// since only doing 2d tesselation here, provide z=1 normal to skip
// iterating over verts only to get the same answer.
// comment out to test normal-generation code
  tessy.gluTessNormal(0, 0, 1);

var triangleVerts = [];
  tessy.gluTessBeginPolygon(triangleVerts);

for (var i = 0; i < contours.length; i++) {
    tessy.gluTessBeginContour();
    var contour = contours[i];
    for (var j = 0; j < contour.length; j += 2) {
      var coords = [contour[j], contour[j + 1], 0];
      tessy.gluTessVertex(coords, coords);
    }
    tessy.gluTessEndContour();
  }

// finish polygon (and time triangulation process)
  tessy.gluTessEndPolygon();

return triangleVerts;
}

下面是简单多边形的效果,这里还对比了 earcut 的效果。

scss 复制代码
const arr = polygon.map((p) => [p.x, p.y]).flat();
triangulate([arr]);
// 返回的不是索引,是图形的点位置,
// 两个为一个点,三个点为一个三角形
// [
//   130, 27,
//   98, 138,
//   95, 68,
//
//   98, 138,
//   130, 27,
//   176, 181,
//
//   176, 181,
//   130, 27,
//   180, 100,
//
//   180, 100,
//   130, 27,
//   200, 27,
//
//   233, 110,
//   176, 181,
//   180, 100
// ]

然后是自交多边形,这次渲染对了。

除了 libtess 库,还有其他的第三方库也迁移了 GLU tesselator。有:

具体不同库的效果谁更好我没有研究过,但这里暂且列出一些以供读者选择和测试。

pixijs 上如何改造?

下面我们看如何给 pixijs 用上这个新的三角剖分算法。

说实在的,pixijs 的改造方式算不上很优雅,但还是可以改的。

pixijs 暴露了 graphicsUtils 对象,该对象下放置了不同图形的三角化逻辑。

css 复制代码
declare const graphicsUtils: {
  // 多边形
  buildPoly: IShapeBuildCommand;
  buildCircle: IShapeBuildCommand;
  buildRectangle: IShapeBuildCommand;
  buildRoundedRectangle: IShapeBuildCommand;
  buildLine: typeof buildLine;
  // ...
};

我们需要改一下 graphicsUtils.buildPoly.triangulate,设置为我们的新的 triangulate 方法。

这里用的是 tess2 库。

ini 复制代码
import { graphicsUtils } from'pixi.js';
import * as Tess2 from'tess2';

// 重写原来的 buildPoly.triangulate 方法
graphicsUtils.buildPoly.triangulate = triangulate;

function triangulate(graphicsData, graphicsGeometry) {
let points = graphicsData.points;
const holes = graphicsData.holes;
const verts = graphicsGeometry.points;
const indices = graphicsGeometry.indices;

if (points.length >= 6) {
    const holeArray = [];
    for (let i = 0; i < holes.length; i++) {
      const hole = holes[i];

      holeArray.push(points.length / 2);
      points = points.concat(hole.points);
    }

    // Tesselate
    const res = Tess2.tesselate({
      contours: [points],
      windingRule: Tess2.WINDING_ODD,
      elementType: Tess2.POLYGONS,
      polySize: 3,
      vertexSize: 2,
    });

    if (!res.elements.length) {
      return;
    }

    const vrt = res.vertices;
    const elm = res.elements;

    const vertPos = verts.length / 2;

    for (var i = 0; i < elm.length; i++) {
      indices.push(elm[i] + vertPos);
    }

    for (let i = 0; i < vrt.length; i++) {
      verts.push(vrt[i]);
    }
  }
}

这样就用上了 tess2 的三角剖切,复杂的多边形也能正确渲染了。

这次填充渲染就对了。

如果你想要多种多边形三角剖分都同时存在,可能需要考虑继承一下 Graphics 类,然后在 renderer 的时候设置好 buildPoly.triangulate

ini 复制代码
class DefGraphics extends PIXI.Graphics {
  render(renderer) {
    PIXI.graphicsUtils.buildPoly.triangulate = defTriangulator;
    super.render(renderer);
  }
}

class TessGraphics extends PIXI.Graphics {
  render(renderer) {
    PIXI.graphicsUtils.buildPoly.triangulate = triangulate;
    super.render(renderer);
  }
}

在线示例:

codesandbox.io/p/sandbox/n...

最后

pixijs 为了提高渲染速度,在质量上做了妥协,如果你的项目对质量有要求,就需要进行特殊的调整,就像这里的三角剖分的算法一样,希望对你有所帮助。

我是前端西瓜哥,关注我,学习更多图形渲染知识。


相关阅读,

PixiJS 源码解读:绘制矩形的渲染过程讲解

用 Pixi.js 写 WebGL

相关推荐
贵沫末2 分钟前
React——基础
前端·react.js·前端框架
aklry14 分钟前
uniapp三步完成一维码的生成
前端·vue.js
Rubin9321 分钟前
判断元素在可视区域?用于滚动加载,数据埋点等
前端
爱学习的茄子22 分钟前
AI驱动的单词学习应用:从图片识别到语音合成的完整实现
前端·深度学习·react.js
用户38022585982422 分钟前
使用three.js实现3D地球
前端·three.js
程序无bug24 分钟前
Spring 面向切面编程AOP 详细讲解
java·前端
zhanshuo24 分钟前
鸿蒙UI开发全解:JS与Java双引擎实战指南
前端·javascript·harmonyos
撰卢1 小时前
如何提高网站加载速度速度
前端·javascript·css·html
10年前端老司机1 小时前
在React项目中如何封装一个可扩展,复用性强的组件
前端·javascript·react.js
Struggler2811 小时前
解决setTimeout/setInterval计时不准确问题的方案
前端