~/ ?.log $
返回文章列表
8 min read
更新于 2026年3月6日

我不知道的 Pixi.js(06)— 图形与遮罩的渲染机制

Pixi.js 的 Graphics 类用来绘制矢量图形——矩形、圆形、线条、多边形。但 v8 对 Graphics API 做了一次大幅重构,beginFill()/endFill() 这套老接口被移除了,换成了链式调用 + 独立的 fill()/stroke() 方法。如果…

Pixi.js 的 Graphics 类用来绘制矢量图形——矩形、圆形、线条、多边形。但 v8 对 Graphics API 做了一次大幅重构,beginFill()/endFill() 这套老接口被移除了,换成了链式调用 + 独立的 fill()/stroke() 方法。如果你还在用 v7 的写法,升级后会直接报错。


一、v8 的 Graphics API:链式调用的新风格

先看 v7 和 v8 的对比,感受一下变化幅度:

// v7 — beginFill/endFill 模式
const g = new PIXI.Graphics();
g.beginFill(0xff0000);
g.drawRect(10, 10, 100, 50);
g.endFill();
g.lineStyle(2, 0x000000);
g.drawCircle(200, 200, 40);
// v8 — 链式调用 + 独立 fill/stroke
import { Graphics } from 'pixi.js';

const g = new Graphics();
g.rect(10, 10, 100, 50).fill(0xff0000);
g.circle(200, 200, 40).stroke({ width: 2, color: 0x000000 });

说白了,v8 把”画形状”和”填充/描边”拆成了两步:先用 rect()circle()poly() 等方法定义路径,再用 fill()stroke() 决定如何渲染。这和 Canvas 2D 的 ctx.beginPath()ctx.rect()ctx.fill() 逻辑很像。

v8 支持的路径方法:

g.rect(x, y, width, height);         // 矩形
g.circle(x, y, radius);              // 圆形
g.ellipse(x, y, halfWidth, halfHeight); // 椭圆
g.roundRect(x, y, w, h, radius);     // 圆角矩形
g.poly([x1, y1, x2, y2, ...]);       // 多边形
g.moveTo(x, y).lineTo(x2, y2);       // 线段
g.bezierCurveTo(cx1, cy1, cx2, cy2, x, y); // 三次贝塞尔
g.quadraticCurveTo(cx, cy, x, y);    // 二次贝塞尔

这里有一个很多人会忽略的细节——fill()stroke() 只作用于前面最近的路径。如果想给多个形状用不同颜色,需要在每个路径后分别调用:

g.rect(0, 0, 100, 100).fill('red');
g.rect(120, 0, 100, 100).fill('blue');
// 两个矩形各自填充,互不干扰

二、三角化:矢量路径怎么变成 GPU 能画的东西

GPU 只认三角形。无论是矩形、圆形还是贝塞尔曲线,最终都要拆解成三角形(triangulation)才能送进渲染管线。

矩形最简单——两个三角形拼成一个矩形。圆形复杂一些——Pixi.js 把圆形近似为多边形(默认足够多的边),再对多边形做三角化。贝塞尔曲线更复杂——先用 de Casteljau 算法细分成一系列线段,线段围成区域后再三角化。

换句话说,Graphics 的渲染成本和路径的复杂度正相关。一个简单矩形只要 2 个三角形,一个带大量贝塞尔曲线的复杂路径可能需要上百个三角形。

这才是真正的原因——为什么频繁修改 Graphics 路径会严重影响性能。每次修改路径(调用 clear() 后重新绘制),引擎需要重新做三角化计算,这是一个 CPU 密集型操作。对于静态图形,Pixi.js 会缓存三角化结果;对于每帧都变化的图形(比如动态曲线),三角化开销是无法避免的。

问题的关键在于——如果图形是静态的,考虑用 graphics.cacheAsTexture(true) 把矢量图形转成纹理。转成纹理后,渲染成本和 Sprite 一样,只是一次贴图操作,不再需要每帧三角化。代价是失去矢量的无损缩放能力。

const g = new Graphics();
g.circle(0, 0, 100).fill('red');
g.cacheAsTexture(true); // v8 替代了 v7 的 cacheAsBitmap
// 之后 g 的渲染成本等同于一个 Sprite

三、遮罩:用形状裁切显示区域

遮罩(Mask)控制一个对象只显示特定区域内的内容。Pixi.js 支持两种遮罩实现方式,底层机制完全不同。

方式一:Graphics 遮罩(Stencil Mask)

import { Sprite, Graphics } from 'pixi.js';

const photo = new Sprite(texture);

const circleMask = new Graphics();
circleMask.circle(150, 150, 100).fill(0xffffff);

photo.mask = circleMask;
photo.addChild(circleMask); // 遮罩需要加入场景树

底层实现:引擎先把 circleMask 的形状写入 GPU 的 Stencil Buffer(模板缓冲区),然后渲染 photo 时只画和 Stencil Buffer 匹配的像素区域。遮罩的颜色无所谓,只有形状起作用

方式二:Sprite 遮罩(Alpha Mask)

const maskSprite = new Sprite(gradientTexture);
photo.mask = maskSprite;
photo.addChild(maskSprite);

底层实现:引擎把 photomaskSprite 分别渲染到临时 RenderTarget,然后用 maskSprite 的 alpha 通道作为权重合成。alpha 为 1 的区域完全显示,alpha 为 0 的区域完全透明。这种方式支持渐变遮罩、羽化边缘等 Stencil 做不到的效果。

换句话说,Graphics 遮罩是”硬裁切”(要么显示要么不显示),Sprite 遮罩是”软裁切”(可以半透明过渡)。


四、遮罩的性能差异

两种遮罩的性能特征差距很大:

Stencil Mask(Graphics 遮罩):

  • 不需要额外的 RenderTarget
  • 利用 GPU 的硬件 Stencil 测试,开销很小
  • 适合简单形状(圆形、矩形、多边形)
  • 不支持渐变/半透明边缘

Alpha Mask(Sprite 遮罩):

  • 需要两个额外的 RenderTarget(对象和遮罩各渲染一次)
  • 需要一次全屏合成绘制
  • 适合复杂遮罩效果(渐变、纹理遮罩)
  • 性能开销显著高于 Stencil Mask

如果你只记住一句话:能用 Graphics 遮罩就不用 Sprite 遮罩。简单形状裁切用 Stencil 几乎无性能损失,Sprite 遮罩每个至少增加两次 RenderTarget 切换。


五、实战:动态遮罩和性能优化

一个常见场景是”探照灯效果”——一个圆形遮罩跟随鼠标移动:

const scene = new Sprite(backgroundTexture);
const spotlight = new Graphics();

spotlight.circle(0, 0, 80).fill(0xffffff);
scene.mask = spotlight;
scene.addChild(spotlight);

app.stage.addChild(scene);

app.stage.eventMode = 'static';
app.stage.on('pointermove', (event) => {
  spotlight.position.copyFrom(event.global);
});

这段代码工作正常,但有一个潜在的性能问题——每次移动鼠标时 spotlight 的位置变化会触发 Stencil Buffer 的重新写入。对于简单形状这不是问题,但如果遮罩是复杂多边形,重写 Stencil 的三角化开销会比较大。

优化方案是用 hitArea 配合矩形遮罩替代复杂形状,或者在遮罩不变时用 cacheAsTexture 缓存:

// 如果遮罩形状不变(只是位置变),不需要特殊优化
// Pixi.js 会复用三角化缓存,只更新位置变换矩阵
// 但如果每帧 clear() 重绘遮罩形状,三角化缓存就失效了

总结

v8 的 Graphics API 从 beginFill/endFill 模式切换到了链式调用 + 独立 fill()/stroke() 模式。矢量图形的渲染核心是三角化——路径越复杂,CPU 开销越大。遮罩有两种:Stencil Mask 利用硬件模板缓冲做硬裁切,性能好但不支持渐变;Alpha Mask 用 RenderTarget 做软裁切,效果好但性能贵。


本系列其他文章:

延伸阅读:

share.ts

// 觉得这篇文章有帮助?

// 欢迎分享给更多人

export const  subscribe  =  "/rss.xml" ;