跳至主要内容
版本:v8.x

场景图

每帧,PixiJS 都会更新然后渲染场景图。让我们讨论一下场景图中包含的内容,以及它如何影响您开发项目的流程。如果您之前构建过游戏,这一切听起来应该都非常熟悉,但是如果您来自 HTML 和 DOM,在讨论您可以渲染的具体对象类型之前理解这一点非常有价值。

场景图是一棵树

场景图的根节点是应用程序维护的容器,并通过 app.stage 引用。当您将精灵或其他可渲染对象作为舞台的子项添加时,它将被添加到场景图中,并将被渲染并可交互。PixiJS 容器 也可以有子项,因此,随着您构建更复杂的场景时,您将最终得到一个根植于应用程序舞台的父-子关系树。

(帮助您浏览项目的实用工具是 用于 Chrome 的 Pixi.js 开发者工具插件,借助于该插件,您可以在运行时实时查看和操作场景图!)

父项和子项

当父项移动时,其子项也随之移动。当父项旋转时,其子项也会旋转。隐藏父项,子项也将会被隐藏。如果您有一个由多个精灵组成的游戏对象,您可以将它们收集到容器中,以便将它们视为世界中的单个对象,作为一个整体进行移动和旋转。

每一帧,PixiJS 遍历场景图从根节点往下依次遍历子节点,直至叶节点,以计算出各个对象最终的位置、旋转、可见性、透明度等。如果父节点的 Alpha 设置为 0.5(使透明度为 50%),那么它的所有子节点都会以 50% 的透明度开始。如果子节点随后设置为 Alpha 为 0.5,那么它不会是 50% 透明,它的透明度将为 0.5 x 0.5 = 0.25 Alpha,即 75% 透明。同样,一个对象相对于其父级的位置也是这样的,所以如果父级设置为 X 位置为 50 像素,而子级设置为 X 位置为 100 像素,那么它将在屏幕偏移 150 像素的位置绘制,即 50 + 100。

此处有一个示例。我们将创建一个精灵,每个精灵都是上一个精灵的子级,并为它们的位置、旋转、缩放和 Alpha 动画添加效果。即使各个精灵的属性均设置为相同的值,但父级-子级关系链会放大每个变化

// Create the application helper and add its render target to the page
const app = new Application();
await app.init({ width: 640, height: 360 })
document.body.appendChild(app.canvas);

// Add a container to center our sprite stack on the page
const container = new Container({
x:app.screen.width / 2,
y:app.screen.height / 2
});

app.stage.addChild(container);

// load the texture
await Assets.load('assets/images/sample.png');

// Create the 3 sprites, each a child of the last
const sprites = [];
let parent = container;
for (let i = 0; i < 3; i++) {
let wrapper = new Container();
let sprite = Sprite.from('assets/images/sample.png');
sprite.anchor.set(0.5);
wrapper.addChild(sprite);
parent.addChild(wrapper);
sprites.push(wrapper);
parent = wrapper;
}

// Set all sprite's properties to the same value, animated over time
let elapsed = 0.0;
app.ticker.add((delta) => {
elapsed += delta.deltaTime / 60;
const amount = Math.sin(elapsed);
const scale = 1.0 + 0.25 * amount;
const alpha = 0.75 + 0.25 * amount;
const angle = 40 * amount;
const x = 75 * amount;
for (let i = 0; i < sprites.length; i++) {
const sprite = sprites[i];
sprite.scale.set(scale);
sprite.alpha = alpha;
sprite.angle = angle;
sprite.x = x;
}
});

场景图中任何给定节点的累积平移、旋转、缩放和倾斜都存储在对象的 worldTransform 属性中。同样,累积 Alpha 值存储在 worldAlpha 属性中。

渲染顺序

所以我们有一棵要绘制的树。最先绘制哪一个?

PixiJS 从根节点向下渲染这棵树。在各个级别,将会渲染当前对象,然后按照插入顺序渲染各个子级。所以会先渲染第二个子级,再在第一级上方渲染第三个子级。

看看此示例,其中包含两个父级对象 A 和 D,另在 A 下包含两个子级对象 B 和 C

// Create the application helper and add its render target to the page
const app = new Application();
await app.init({ width: 640, height: 360 })
document.body.appendChild(app.canvas);

// Label showing scene graph hierarchy
const label = new Text({
text:'Scene Graph:\n\napp.stage\n ┗ A\n ┗ B\n ┗ C\n ┗ D',
style:{fill: '#ffffff'},
position: {x: 300, y: 100}
});

app.stage.addChild(label);

// Helper function to create a block of color with a letter
const letters = [];
function addLetter(letter, parent, color, pos) {
const bg = new Sprite(Texture.WHITE);
bg.width = 100;
bg.height = 100;
bg.tint = color;

const text = new Text({
text:letter,
style:{fill: "#ffffff"}
});

text.anchor.set(0.5);
text.position = {x: 50, y: 50};

const container = new Container();
container.position = pos;
container.visible = false;
container.addChild(bg, text);
parent.addChild(container);

letters.push(container);
return container;
}

// Define 4 letters
let a = addLetter('A', app.stage, 0xff0000, {x: 100, y: 100});
let b = addLetter('B', a, 0x00ff00, {x: 20, y: 20});
let c = addLetter('C', a, 0x0000ff, {x: 20, y: 40});
let d = addLetter('D', app.stage, 0xff8800, {x: 140, y: 100});

// Display them over time, in order
let elapsed = 0.0;
app.ticker.add((ticker) => {
elapsed += ticker.deltaTime / 60.0;
if (elapsed >= letters.length) { elapsed = 0.0; }
for (let i = 0; i < letters.length; i ++) {
letters[i].visible = elapsed >= i;
}
});

如果你想重新调整子级对象,你可以使用 setChildIndex()。若要在父级的列表中给定某一点添加子级,请使用 addChildAt()。最后,你可以使用 sortableChildren 选项以及为各个子级设置 zIndex 属性,来为对象的子级启用自动排序。

渲染组

随着你在 PixiJS 中深入研究,你会遇到一项名为渲染组的重要功能。可以将渲染组视为场景图中的专门容器,它们本身就像小型场景图。以下是你需要了解的知识,以便在你的项目中有效使用渲染组。更多信息,请查看 渲染组概览

剔除

如果你制作的项目里有很大一部分场景对象不在屏幕上显示(如横向卷轴游戏),那么你最好对这些对象执行“剔除”。剔除是指评估一个对象(或其子对象!)是否在屏幕上显示,如果没有,则关闭它的渲染。如果你不对屏幕外对象执行剔除,渲染器仍然会绘制这些对象,尽管它们在屏幕上看不到。

PixiJS 不提供内置视口剔除支持,但你可以找到能满足你需求的第三方插件。此外,如果你想构建自己的剔除系统,只需在每一次滴答过程中运行对象,并将不需要绘制的任何对象的 renderable 设置为 false。

局部坐标系与全局坐标系

如果你将一个精灵添加到舞台上,默认情况下它会显示在屏幕的左上角。这是 PixiJS 所使用的全局坐标空间的原点。如果你的所有对象都是舞台的子对象,那么全局坐标系就是你唯一需要考虑的坐标系。但是,一旦你引入容器和子对象,事情就会变得越来越复杂。一个子对象在[50, 100]相对于其父对象向右 50 个像素,向下 100 个像素。

我们将这两个坐标系称为“全局”坐标系和“局部”坐标系。当你在对象上使用 position.set(x, y) 时,你始终相对于对象的父对象在局部坐标系中工作。

问题在于,很多时候你想知道对象的全局位置。例如,如果你想剔除屏幕外的对象以节省渲染时间,你必须知道给定子对象是否在视图矩形之外。

要从局部坐标系转换为全局坐标系,可以使用 toGlobal() 函数。下面是一个示例用法

// Get the global position of an object, relative to the top-left of the screen
let globalPos = obj.toGlobal(new Point(0,0));

此代码段会将 globalPos 设置为子对象的全局坐标系,相对于[0, 0]在全局坐标系中。

全局坐标系与屏幕坐标系

当你的项目与主机操作系统或浏览器配合工作时,就会产生第三个坐标系 - “屏幕”坐标系(也称“视口”坐标系)。屏幕坐标表示相对于 PixiJS 正在渲染到的画布元素的左上角位置。DOM 和原生鼠标点击事件之类的元素在屏幕空间中运行。

现在,在大多数情况下,屏幕空间与世界空间相同。如果画布的大小与创建应用程序时指定的渲染视图的大小相同时,就会出现这种情况。默认情况下,这种情况将发生 - 只需创建一个 800x600 应用窗口并将其添加到 HTML 页面中,它就会保持这种大小。世界坐标中的 100 个像素将等于屏幕空间中的 100 个像素。但是!拉伸要渲染的视图以使其填满屏幕或降低分辨率并为速度进行向上缩放是很常见的。在这种情况中,画布元素的屏幕大小(例如通过 CSS)将发生变化,但底层的渲染视图将不进行更改,从而导致世界坐标和屏幕坐标之间不匹配。