Published on

PixiJS 源码揭秘 - 1. 初识PixiJS源码

Authors
  • avatar
    Name
    青雲
    Twitter

引言

PixiJS 是一个开源的2D渲染库,由Goodboy Digital于2013年首次发布。该库旨在提供高性能的图形渲染能力,同时简化开发者的使用门槛。PixiJS 支持 WebGL 和 HTML5 Canvas,可以在设备上自动选择最佳的渲染方式,确保其在多种设备和平台上都能获得最佳的性能表现。

PixiJS 的历史

发展历程

  1. 初创期(2013)
    • Goodboy Digital 发布 PixiJS 的第一个版本,目的是为开发者提供一个简单、高效的2D渲染库。
    • 支持 WebGL 和 Canvas 双后备机制,自动选择最佳渲染方式。
  2. 快速增长期(2014-2017)
    • PixiJS 的社区迅速扩大,吸引了大量开发者和贡献者。
    • 持续添加各类新功能,如高级滤镜、纹理管理、事件系统等。
  3. 稳定成熟期(2018-至今):
    • 在社区和贡献者的共同努力下,PixiJS 变得更加稳定和功能完备。
    • 引入了 WebGPU 支持等前沿技术,进一步提升渲染性能和可扩展性。

核心特性

PixiJS 在2D渲染领域中具有许多核心特性,使其成为开发高性能、互动性强的Web内容和游戏的理想选择。

  1. 高性能的2D渲染器:可支持成千上万个精灵对象、复杂的图形绘制以及动画效果,而性能表现依然出色。
  2. 自动兼容 WebGL 和 Canvas:PixiJS 采用自动回退机制,优先使用 WebGL 渲染以获得最佳性能,但在不支持 WebGL 的设备或浏览器上,它会自动回退到 Canvas 渲染
  3. 易于使用的API:PixiJS 拥有一个简洁且强大的API,使得开发者可以轻松上手并快速创建2D图形应用。
  4. 丰富的交互能力:提供了强大的事件系统,使得创建交互式应用变得非常简单。开发者可以方便地监听和处理鼠标、触摸等用户输入事件,从而实现丰富的交互效果。
  5. 灵活的扩展性:插件机制,允许开发者根据需要扩展其功能。

应用领域

PixiJS 在游戏开发和交互式 Web 内容创作中具有广泛的应用场景,包括但不限于:

  • 2D 游戏开发:由于其高效的渲染性能和丰富的动画功能,PixiJS 被广泛应用于各类2D游戏的开发。
  • 数据可视化:利用 PixiJS 简单创建高性能的图表和数据可视化工具,广泛应用于金融、医疗等领域。
  • 互动广告和媒体:通过 PixiJS 构建互动性强、视觉效果炫酷的广告和多媒体内容。

多年以来,PixiJS 已成为2D图形渲染领域的领导者,被众多知名公司和项目所采用,如谷歌的创意广告、微软的游戏开发、幕布的思维导图等。

写作初衷与背景

写作目的

本系列文章旨在深入解析 PixiJS 的源码实现,通过系统化的分析和讲解,帮助大家深刻理解其核心技术和设计思想。具体目的如下:

  1. 掌握实际开发技巧:提供详尽的代码示例和实战案例,便于读者将其运用到实际项目中。
  2. 提升性能优化能力:学习 PixiJS 的性能优化策略与方法,提高项目开发的效率和质量。
  3. 渲染引擎原理剖析:深度探究 2D 渲染引擎的核心原理,涵盖 WebGL 与 Canvas 的工作机制、渲染管线的构建、批处理技术等。
  4. 理解 PixiJS 的设计原理:通过对源码的剖析,深入掌握 PixiJS 的整体架构以及各个模块的设计原理。
  5. 扩展开发视野:了解前沿技术在实际项目中的应用,拓展开发者的技术视野。

阅读准备

预期读者:

  1. Web开发者:具备一定 Web 开发经验,想了解和掌握高性能 2D 图形渲染技术。
  2. 游戏开发者:希望深入理解PixiJS 及其在游戏开发中的应用,以提升游戏性能和开发效率。
  3. 技术爱好者和研究者:对图形渲染技术有浓厚兴趣,想深入学习 PixiJS 内部实现和设计思想。

知识基础:

  1. JavaScript基础:熟悉 JavaScript 语言及常用开发工具。
  2. HTML5知识:了解 HTML5 和 Canvas 的基本概念和使用方法。
  3. 前端开发经验:有一定的前端开发经验,了解常用的Web开发技术和框架。

概览PixiJS的架构

在理解PixiJS的具体实现之前,我们需要对其整体架构有一个概览性的认知。PixiJS 架构设计清晰,模块划分明确。下面将从核心概念和主要组成部分两个方面进行介绍。

核心概念

渲染循环(Render Loop)

PixiJS 与静态网页不同,它是一个动态更新的系统,持续不断地更新和重绘自身。这个过程被称为渲染循环(Render Loop)。

在PixiJS项目中,大部分工作都是在这个更新和渲染的循环中完成。你编写代码更新场景中的对象,而PixiJS负责将这些对象渲染到屏幕上。

在每一帧的渲染循环中发生的事情,主要有三个步骤:

  1. 运行 Ticker 回调

渲染循环的第一步是计算自上一帧以来经过的时间,并调用 Application 对象的 Ticker 回调函数。Ticker 是一个计时器系统,它的回调函数负责更新场景中的元素,例如动画、物体移动等。它会根据时间增量(delta time)更新每个对象的状态。

  1. 更新场景图(Scene Graph)

PixiJS的场景图是一种树状结构,包含了所有待绘制的对象,如 Sprites、文本等。每个对象都有自己的位置、旋转和缩放属性,这些对象在场景图中按层次关系排列。

在更新阶段,PixiJS需要遍历整个场景图,重新计算每个对象的位置和状态,包括以下步骤:

  • 更新变换属性:每个对象的位移、旋转和缩放属性需要根据父子关系进行更新。
  • 计算边界框:计算每个对象的包围盒,用于确定它们的渲染区域。
  • 更新渲染属性:更新每个对象的渲染属性,例如透明度、混合模式等。
  1. 渲染场景图

一旦场景图中的所有对象都更新完毕,PixiJS就会开始绘制它们。渲染过程从场景图的根节点(通常是app.stage)开始,依次渲染每个对象及其子对象。PixiJS会遍历整个树状结构,绘制其中所有的对象。

需要注意的是,PixiJS默认不会对场景图中的对象进行剪裁优化(culling)。也就是说,即使对象在屏幕外部,PixiJS也会尝试绘制它们。因此,开发者需要自行禁用那些不可见的对象以优化性能。

场景图(Scene Graph)

在每一帧中,PixiJS会更新并渲染场景图。

场景图是一棵树

场景图的根节点是由应用维护的一个容器,这个容器被引用为 app.stage。当你将一个 Text 或者其他可渲染对象添加为 stage 的子节点时,它们就被添加到了场景图中,并且会被渲染和交互。PixiJS的容器(Containers)也可以包含子节点,因此,当你构建更复杂的场景时,会形成一个以 app.stage 为根的父子关系树。

父子关系

在场景图中,父节点移动时,它的所有子节点也会随之移动。当父节点旋转时,子节点也会旋转。隐藏父节点时,子节点也会被隐藏。如果你的一个对象是由多个 sprites 组成的,你可以将它们放在一个容器下,将它们作为一个整体进行移动和旋转。

在每一帧中,PixiJS 会从根节点开始,遍历整个场景图,计算每个对象的最终位置、旋转、可见性、透明度等属性。例如,如果一个父节点的透明度(alpha)设置为0.5,它的所有子节点起始透明度也是0.5。如果一个子节点的透明度设置为0.5,它的最终透明度将是0.5 * 0.5 = 0.25,即75%的透明度。同样,一个对象的位置是相对于它的父节点的,如果父节点的 x 位置是50像素,而子节点的 x 位置是100像素,那么这个子节点将绘制在屏幕上150像素的位置,即50 + 100。

渲染顺序

场景图中的对象是按照从根节点到叶节点的顺序渲染的。在每一层级中,当前对象首先被渲染,然后依次渲染其子对象,子对象的渲染顺序与其插入顺序一致。因此,第二个子对象会被绘制在第一个子对象之上,第三个子对象会被绘制在第二个子对象之上。

本地与全局坐标

如果你将一个 sprite 添加到舞台,默认情况下它会显示在屏幕的左上角,这是 PixiJS 使用的全局坐标空间的原点。如果所有对象都是舞台的子对象,你只需要关心全局坐标。但引入容器和子对象后,情况变得复杂。一个子对象的位置是相对于它的父对象而言的。

全局与屏幕坐标

当你的项目与操作系统或浏览器交互时,还需要考虑第三种坐标系统——“屏幕”坐标(又称为“视口”坐标)。屏幕坐标表示相对于PixiJS渲染到的canvas元素的左上角的位置。

在很多情况下,屏幕坐标与世界坐标是等价的,即在canvas元素的尺寸与创建Application时指定的渲染视图尺寸相同时。例如,如果你创建了一个800x600的应用窗口,并将其添加到HTML页面中,默认情况下,100像素的世界坐标等于100像素的屏幕坐标。

但是,当渲染视图被拉伸以填满屏幕,或以较低分辨率进行渲染并缩放时,情况就会变得复杂。此时canvas元素的屏幕尺寸会改变(例如通过CSS),但基础渲染视图的尺寸不会改变,从而导致世界坐标与屏幕坐标之间的不匹配。

渲染组(Render Groups)

PixiJS v8引入了一个强大的功能:渲染组。渲染组可以看作是场景图中的专门容器,它们本身就像是小型的场景图。

渲染组是什么?

渲染组本质上是PixiJS认为的独立场景图的容器。当你将场景的一部分分配给渲染组时,这意味着你在告诉 PixiJS 将这些对象作为一个单元进行管理。这个管理包括监控变化,并为该组准备一组专门的渲染指令。这是一个强大的工具,可以优化你的渲染过程。

为什么要使用渲染组?

使用渲染组的主要优势在于它们的优化能力。渲染组允许将某些计算(如变换、颜色调整和透明度调整)卸载到GPU 上进行处理。这意味着诸如移动或调整渲染组的操作可以以最小的 CPU 开销完成,从而使应用程序更具性能效率。

虽然渲染组很强大,但使用太多反而可能降低性能。目标是找到优化渲染和避免系统负担过重之间的平衡。在使用渲染组时,请务必进行性能分析。大多数时候你并不需要使用它们。

主要组成部分

PixiJS的架构由多个核心组件组成,各自负责不同的功能区域。

Renderer

渲染器是 PixiJS 系统的核心部分,负责显示场景图并将其绘制到屏幕上。PixiJS 能够自动确定是否使用 WebGPU 或 WebGL 渲染器,根据具体平台和浏览器的支持情况选择最佳渲染方式。

Container

容器作为构造场景图的基础对象,采用了一种树状结构来组织各种可渲染对象,如精灵、图形和文本。容器与场景图的概念紧密相关,为组织和管理不同层次的可视元素提供了一个高效的框架。

Assets

资源系统提供了一组高效的工具,用于异步加载外部资源,包括图像、音频文件等。此系统使得资源的管理变得更加简单和高效,促进了媒体内容的重复使用和优化

Ticker

Ticker 是一种基于时间的周期性回调机制,它提供了一个稳定的循环,用于驱动游戏或应用的更新逻辑。开发者可以利用一个或多个 Ticker 实例来分离和管理不同的逻辑更新需求。

Application

Application 类是一个便捷的封装体,将 Loader(加载器)、Ticker 和 Renderer(渲染器)集成为一个易于操作的接口。这个辅助类非常适合于快速启动项目、原型设计以及构建较为简单的应用。

Events

PixiJS 提供了完整的基于指针的事件系统,支持对象的点击、触发悬停等交互操作。这套事件系统极大地简化了交互性应用和游戏开发的复杂度。

Accessibility

为了确保你的应用或游戏能够被更广泛的用户群体所访问,PixiJS 引入了一整套工具以支持键盘操作和屏幕阅读器的兼容性,从而提升应用的总体可访问性。

源码目录结构分析

PixiJS的源码目录结构层次清晰,并按照功能模块进行了划分。

主要目录结构

src目录是PixiJS源码的核心部分,主要分为以下几个模块,每个模块负责不同的功能:

src
├─ accessibility        // 无障碍功能的实现
├─ advanced-blend-modes // 高级混合模式功能
├─ app                  // 应用程序相关内容
├─ assets               // 资源管理和加载
├─ color                // 颜色相关实用工具
├─ compressed-textures  // 压缩纹理处理
├─ culling              // 剪枝相关功能
├─ environment          // 运行环境检测和适配
├─ events               // 事件管理系统
├─ extensions           // 扩展管理功能
├─ filters              // 图像滤镜
├─ math-extras          // 数学扩展工具
├─ maths                // 数学相关功能
├─ prepare              // 资源预处理
├─ rendering            // 渲染系统
├─ scene                // 场景管理和显示对象
├─ spritesheet          // 精灵图相关功能
├─ ticker               // 帧调度系统
├─ unsafe-eval          // 非安全代码评估的处理
├─ utils                // 工具函数
├─ bundle.advanced-blend-modes.ts  // 高级混合模式的打包文件
├─ bundle.browser.ts                 // 浏览器环境的打包文件
├─ bundle.math-extras.ts             // 数学扩展的打包文件
├─ bundle.unsafe-eval.ts             // 不安全代码评估的打包文件
├─ bundle.webworker.ts               // Web Worker 环境的打包文件
└─ index.ts                           // 入口文件

模块详解

accessibility 无障碍功能

accessibility目录包含了与无障碍功能相关的实现,确保PixiJS应用对所有用户友好,包括那些使用辅助技术的用户。

  1. AccessibilityMixins.d.ts:类型定义。
  2. AccessibilitySystem.ts:核心逻辑。
  3. accessibilityTarget.ts:无障碍目标对象的处理。
  4. init.ts:初始化。

advanced-blend-modes 高级混合模式

advanced-blend-modes目录包含实现各种高级混合模式的代码,这些混合模式用于控制如何将图像渲染到屏幕上。

  1. 各种混合模式文件,如ColorBlend.ts、HardLightBlend.ts等。
  2. init.ts:初始化。

app 应用程序相关内容

app目录包含与PixiJS应用程序相关的内容,主要是Application类的实现和插件管理。

  1. Application.ts:Application类的实现,负责应用程序的核心逻辑。
  2. ApplicationMixins.d.ts:Application的类型定义。
  3. ResizePlugin.tsTickerPlugin.ts:两种常见插件的实现。
  4. init.ts:应用程序的初始化。

assets 资源管理和加载

assets目录负责资源的加载和管理,包括纹理、音频等。

  1. cache:资源缓存的实现。
  2. detections:资源格式检测。
  3. loader:资源加载器及其解析器。
  4. resolver:资源解析相关功能。
  5. utils:资源管理的实用工具。

rendering 渲染系统

rendering目录包含PixiJS渲染系统的核心代码,该系统负责将场景图渲染到屏幕上。

  1. batcher:批处理系统。
  2. high-shader:高级着色器。
  3. mask:蒙版处理。
  4. renderers:不同类型的渲染器实现。
  5. shared:共享的渲染工具和系统。

scene 场景管理和显示对象

scene目录管理所有的显示对象和容器,包括精灵、图形、文本等。

  1. container:容器及其相关工具。
  2. graphics:图形绘制。
  3. mesh:网格处理。
  4. sprite:精灵处理。
  5. text:文本渲染。

快速定位功能实现和相关类

  1. 按功能模块查找:如果了解要查找的功能对应的模块,可以直接在相应的目录中查找。例如,需要处理精灵相关的内容,可以直接查看src/scene/sprite目录。
  2. 使用文件名中的关键词:目录中的文件名通常包含了它们实现的具体功能,例如需要查找高级混合模式的实现,可以先找到advanced-blend-modes目录,再查找具体的混合模式文件如ColorBlend.ts
  3. 利用init文件:很多模块都有init.ts文件,这些文件通常用于模块的初始化,可以作为了解模块整体功能的起点。
  4. 参考类型定义文件:类型定义文件(如*.d.ts)通常包含各类和接口的定义,可以帮助理解各类的结构和用法。
  5. 使用IDE的搜索和导航功能,可以迅速在相关模块和类之间跳转,方便地进行源码阅读和调试。

源码调试

首先要设置开发环境:

  1. 克隆仓库
https://github.com/pixijs/pixijs.git
cd pixijs
  1. 安装依赖
npm install
  1. 启动开发环境
npm run start

image.png

接下来就可以调试代码、运行示例和测试用例,以便更好地理解源码。

使用示例

让我们通过一个简单的示例来展示如何使用 PixiJS 构建一个基本的应用程序,并解析其渲染流程。首先,我们需要安装 PixiJS 依赖:

npm install pixi.js

安装完成后,我们在项目中引入所需模块并创建一个简单的应用程序,

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

(async () => {
  // 创建一个新的应用程序
  const app = new Application();

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

  // 将应用程序的画布添加到文档 body 中
  document.body.appendChild(app.canvas);

  // 加载小兔子图片
  const texture = await Assets.load('https://pixijs.com/assets/bunny.png');

  // 创建一个小兔子精灵
  const bunny = new Sprite(texture);

  // 将精灵的锚点设置到中心
  bunny.anchor.set(0.5);

  // 将精灵移动到屏幕中心
  bunny.x = app.screen.width / 2;
  bunny.y = app.screen.height / 2;

  app.stage.addChild(bunny);

  // 监听动画更新
  app.ticker.add((time) => {
    // 为了好玩,每帧旋转一下小兔子
    bunny.rotation += 0.1 * time.deltaTime;
  });
})();

20240827225002_rec_.gif

在这个示例中,PixiJS的渲染流程如下:

  1. 初始化应用程序: 创建一个Application实例,并初始化它。这一步会配置应用程序的背景颜色及画布的大小等。
  2. 加载资源: 使用Assets模块加载图像资源。当资源加载完成时,执行回调函数创建纹理。
  3. 创建精灵: 使用加载的纹理创建精灵对象,并设置其初始位置和其他属性。
  4. 添加到舞台: 将精灵对象添加到舞台上,使其参与渲染。
  5. 更新和渲染: 每帧调用Ticker的回调函数,更新精灵的状态(如旋转),确保帧独立的变换,然后渲染器会自动根据最新状态重新绘制舞台。

通过以上步骤,我们成功创建了一个简单的PixiJS应用,并理解了其基本的渲染流程。

PixiJS 官网提供了一个Playground,里面包含了一些简单的示例。你可以在Playground中编写自己的 demo 并进行调试,是一个非常方便的学习和实验工具。

结语

本篇文章介绍了 PixiJS 的发展历程、核心特性、核心概念、源码目录结构、源码调试方法以及使用示例等相关内容,后续文章将在此基础上进一步深入解析 PixiJS 的源码。