PixiJS编程指南之Container

567 阅读9分钟

先简单说一下PixiJS的渲染,PixiJS有一个场景图的概念,这个场景图的结构就类似于树形结构,PixiJS从树的根节点从上往下渲染,当然,PixiJS允许多个树的存在,在树的每一层,渲染当前对象,然后按插入顺序渲染每个子对象。因此,第二个子项渲染在第一个子项之上,第三个子项渲染在第二个子项之上。

当父节点移动时,其子节点也会移动。当父节点旋转时,其子节点也会旋转。隐藏父节点,子节点也将被隐藏。如果游戏对象由多个 sprite 组成,则可以将它们收集到一个容器下,以将它们视为世界中的单个对象,作为一个对象移动和旋转。

每一帧,PixiJS 都会从根开始贯穿场景图,从根到所有子对象再到叶子,以计算每个对象的最终位置、旋转、可见性、透明度等。如果父项的 Alpha 设置为 0.5(使其 50% 透明),则其所有子项也将从 50% 透明开始。如果将子项设置为 0.5 alpha,则它不会是 50% 透明,而是 0.5 x 0.5 = 0.25 alpha,即 75% 透明。同样,对象的位置是相对于其父对象的位置,因此,如果父对象设置为 50 像素的 x 位置,而子对象设置为 100 像素的 x 位置,则它将以 150 像素或 50 + 100 的屏幕偏移量绘制。

那么这个Container是什么呢?其实Container就是树形结构的节点,其作为一个容器存在,除了作为容器之外,Container还有一些其它用途。

Container常见属性

属性描述
positionX 和 Y 位置以像素为单位,并更改对象相对于其父对象的位置,也可直接用作object.x
/ object.y
rotation旋转以弧度为单位指定,并顺时针旋转对象 (0.0 - 2 * Math.PI)
angle角度是旋转的别名,以度而不是弧度 (0.0 - 360.0) 指定
pivot对象旋转所围绕的点(以像素为单位) - 还设置子对象的原点
alpha从 0.0(完全透明)到 1.0(完全不透明)的不透明度
scale比例指定为百分比,其中 1.0 表示 100% 或实际大小,并且可以为 x 轴和 y 轴单独设置
skewSkew 在 x 和 y 上变换对象,类似于 CSS skew() 函数,并以弧度指定
visible对象是否可见(作为布尔值)
renderable对象是否应该被渲染——当为false时,对象仍然会被更新,但不会被渲染,比较特殊,它不会影响子对象

示例程序

import { Application, Assets, Container, Sprite } from 'pixi.js';

(async () =>
{
    // 创建应用
    const app = new Application();

    // 初始化应用
    await app.init({ background: '#1099bb', resizeTo: window });

    // 将canvas添加到 文档中
    // 这里可以在new Application()时通过view自己绑定canvas,如果没有显示绑定,那么pixi内部会自己创建一个canvas
    document.body.appendChild(app.canvas);

    // 创建一个容器
    const container = new Container();

    app.stage.addChild(container);

    // 加载一个纹理 Assets后续会介绍
    const texture = await Assets.load('https://pixijs.com/assets/bunny.png');

    // 创建一个5 x 5 纹理阵列
    for (let i = 0; i < 25; i++)
    {
        const bunny = new Sprite(texture);

        bunny.x = (i % 5) * 40;
        bunny.y = Math.floor(i / 5) * 40;
        container.addChild(bunny);
    }

    // 将容器移动到中心
    container.x = app.screen.width / 2;
    container.y = app.screen.height / 2;

    // 设置容器锚点,也就是几何变换的基点,不设置的话,默认以container左上角为原点旋转
    //  该显示对象在其局部空间中的旋转、缩放和倾斜中心。“位置”是“枢轴”在父节点的局部空间中的投影。
    container.pivot.x = container.width / 2;
    container.pivot.y = container.height / 2;

    // 监听动画,pixi的ticker在每一帧都会调用其回调函数
    app.ticker.add((time) =>
    {
        // 旋转容器
        container.rotation -= 0.01 * time.deltaTime;
    });
})();

作为遮罩使用

container除了作为容器之外,还可以作为遮罩使用。

import { Application, Assets, Container, Sprite, Graphics } from "pixi.js";

(async () => {
  const app = new Application();

  await app.init({ antialias: true, resizeTo: window });

  document.body.appendChild(app.canvas);

  app.stage.eventMode = "static";

  await Assets.load(["https://pixijs.com/assets/panda.png"]);

  const container = new Container();

  container.x = app.screen.width / 2;
  container.y = app.screen.height / 2;

  const panda = Sprite.from("https://pixijs.com/assets/panda.png");

  panda.anchor.set(0.5);

  container.addChild(panda);

  app.stage.addChild(container);

  const thing = new Graphics();

  app.stage.addChild(thing);

  thing.x = app.screen.width / 2;
  thing.y = app.screen.height / 2;

  container.mask = thing;

  app.ticker.add(() => {
    thing.clear();
    thing.moveTo(-120 / 2, -100 / 2);
    thing.lineTo(120 / 2, -100 / 2);
    thing.lineTo(120 / 2, 100 / 2);
    thing.lineTo(-120 / 2, 100 / 2);
    //  thing.moveTo(-120, -100);
    //  thing.lineTo(120, -100);
    //  thing.lineTo(120, 100);
    //  thing.lineTo(-120, 100);
    thing.fill({ color: 0x8bc5ff, alpha: 0.4 });
  });
})();

240 x 200矩形显示区域

120 x 100矩形显示区域

这里会有一个疑问就是container.mask = thing设置遮罩之后,为什么还要进行app.stage.addChild(thing)。

原因解析:

  1. 遮罩对象本身也是舞台上的一部分:
    • 在 PixiJS 中,任何作为遮罩使用的对象(例如 GraphicsSpriteContainer 等)本质上都是一个 可渲染的对象。为了使这个遮罩对象参与渲染,它需要像其他图形或精灵一样被添加到舞台上,才能进入渲染队列,影响最终的显示结果。
    • 即使遮罩本身并不显示在最终的图像中(例如,你可能希望隐藏遮罩本身,只显示它对目标对象的影响),它仍然需要被添加到舞台上,以便 PixiJS 的渲染引擎能正确地进行遮罩操作。
  2. 渲染顺序和遮罩的实际应用:
    • 当你为 Container 设置一个遮罩时,实际上是将该遮罩作为渲染的一部分添加到渲染管线中。在 PixiJS 中,遮罩的运作依赖于其在渲染队列中的位置,即使它本身不可见,它也需要参与渲染过程,才能与目标容器正确地配合。
    • 将遮罩对象添加到舞台后,PixiJS 会计算遮罩和目标对象之间的交互关系,然后将该遮罩效果应用到目标容器或精灵上。
  3. 遮罩的物理存在:
    • 即使遮罩本身不会显示出来(例如你可能不希望它呈现颜色),它仍然是一个 图形对象,它的作用是通过其形状或区域来影响被遮罩对象的显示。
    • 因此,遮罩需要参与整个渲染流程,必须放置在场景中的一个可渲染的图形对象位置(即舞台中),否则,渲染系统无法“看到”它,也就无法将它应用到目标容器或精灵上。

遮罩原理

PixiJS 中的遮罩(mask)机制实际上是一种通过 裁剪 操作来控制图形或精灵的显示区域。通过设置遮罩,PixiJS 允许你限定目标对象显示的区域,只渲染在遮罩区域内的部分,而遮罩之外的部分将会被 “裁剪”“隐藏”。遮罩对象的原理基于 GPU 的 stencil bufferalpha test,通常会通过一个 遮罩图形(如矩形、圆形、路径等) 来定义该区域。

原理:

  1. 遮罩图形
    • 遮罩本身是一个 图形对象(通常是 GraphicsSprite),它定义了一个形状或区域,决定了目标图形的可见部分。比如,圆形遮罩会显示圆形区域内的目标内容,超出圆形的部分会被裁剪。
  2. 遮罩与目标对象的关系
    • 通过将 mask 属性设置为某个图形对象,目标对象会被限制在该遮罩的形状范围内。具体来说,目标对象的渲染仅在遮罩图形定义的区域内可见,超出部分将被隐藏。
  3. 如何工作
    • 在渲染过程中,PixiJS 使用 stencil bufferalpha test 来实现遮罩效果。具体来说,目标对象的像素被渲染到一个临时缓冲区,然后与遮罩图形进行比对。只有那些在遮罩图形定义区域内的像素才会最终呈现出来。
  4. 工作流程
    • 目标对象渲染:首先,PixiJS 渲染目标对象(例如精灵、图形等),它会计算该对象的位置、形状和纹理等信息。
    • 遮罩图形渲染:随后,遮罩图形也会被渲染,但它通常不会显示出来,或者即使显示,它的颜色和透明度也是不影响结果的。遮罩的作用主要在于影响后续的渲染步骤。
    • 对比操作:目标对象的像素与遮罩图形进行比较。PixiJS 使用像素级别的比对机制,确定哪些部分应该被保留,哪些部分应该被裁剪。
    • 最终显示:最后,经过遮罩裁剪的目标对象的可见区域会被渲染到画布上,而被遮挡的部分将不会被显示出来。

遮罩的实现方法

PixiJS 中实现遮罩的方法主要依赖以下两种技术:

1. Stencil Buffer(模板缓冲)

  • PixiJS 通常使用 stencil buffer 来实现遮罩。模板缓冲区是一个用于 像素级别控制 渲染的缓冲区。它允许开发者根据模板测试来控制哪些像素可以被写入帧缓冲区。

  • 使用 Graphics(或者其他类型的对象,如 Sprite)作为遮罩时,模板缓冲区会记录遮罩区域的位置和形状。在之后的渲染中,只有那些位于遮罩区域内的像素才会通过模板测试,最终被绘制到屏幕上。

    具体流程:

  • 渲染遮罩图形(通常是一个透明的形状)。此时,模板缓冲区记录下遮罩图形的位置。

  • 在渲染目标对象时,只有那些处于遮罩区域内的像素会被写入帧缓冲区。

  • 最终屏幕上只显示目标对象在遮罩区域内的部分。

2. Alpha Test(透明度测试)

  • 另一种实现遮罩的方式是 alpha test,它通过测试目标对象像素的透明度来决定是否显示该像素。一般情况下,透明度小于某个阈值的像素会被丢弃。

  • 当使用 Graphics 作为遮罩时,通常会设置一个 alpha 值来作为判断标准。如果目标对象的像素透明度小于这个阈值,那么该像素将被丢弃。

    具体流程:

  • 遮罩图形会被设置为具有非透明的区域,并且可能使用 alpha 值为透明或不透明。

  • 渲染目标对象时,PixiJS 会根据像素的透明度判断是否保留该像素。如果目标像素的透明度大于遮罩图形的 alpha 阈值,那么该像素将被渲染,否则将被丢弃。

  • PixiJS 中的遮罩 是通过利用 stencil bufferalpha test 技术来实现的,目的是通过指定一个区域来限制目标对象的渲染范围。
  • 遮罩对象本身可以是任何形状或对象,可以是 GraphicsSprite 等类型,并且它通常需要添加到舞台上才能正确参与渲染。
  • 遮罩机制可以让你精确控制目标对象显示的区域,能够实现各种复杂的图形效果。