前言
PixiJS 8.x 版本进行了架构上的重要更新,优化了渲染和更新机制,简化了动画管理,提升了性能和灵活性。PixiJS 8.x 的渲染和更新机制主要通过以下几个关键组件来实现:Application、Renderer、Ticker、EventSystem 等。这些组件共同作用,自动管理每一帧的更新和渲染循环,让开发者无需手动控制每一帧。下面将分别介绍Application、Renderer、Ticker、EventSystem,以及他们是如何协同工作的,从场景图的更新到渲染他们都做了什么。
1 Application
Application 是 PixiJS 的核心入口,用于创建渲染器、舞台(stage)以及帧更新的基础环境,代码量并不多,整合之后估计只有几十行代码。Application 自动管理了渲染器、更新机制和渲染循环,并内置了更新和渲染的调度功能。
- 初始化渲染器和舞台:在
Application创建时,会初始化Renderer(负责渲染)和Container(根容器)作为stage。 - 启动自动渲染:默认情况下,
Application启动后会自动运行更新和渲染循环。
整个应用渲染的入口在这里,Application 启动后会自动运行更新和渲染循环,但是可以关闭。在创建Application 时可以通过配置参数autoStart来决定是否开启渲染循环。如果关闭渲染循环,就需要主动调用app.render来更新场景图。此外,从该类还可以获取canvas、view、screen对象。整体来说这个application类是非常简洁的。
2 扩展系统Extension
在Pixi中,扩展系统(Extension System) 作为一种新的功能,旨在增强 PixiJS 核心功能的灵活性和可扩展性。通过该系统,开发者可以以插件或扩展的形式,向 PixiJS 添加额外的功能,而无需修改核心代码。这种系统使得 PixiJS 的功能可以更容易地进行定制和扩展,适用于各种自定义需求,比如支持新的渲染效果、添加新的动画、拓展与外部库的集成等。
扩展extension是一个对象,并不是一个类,支持以下几种扩展:
/**
* Collection of valid extension types.
* @memberof extensions
*/
enum ExtensionType
// eslint-disable-next-line @typescript-eslint/indent
{
/** extensions that are registered as Application plugins */
Application = 'application',
/** extensions that are registered as WebGL render pipes */
WebGLPipes = 'webgl-pipes',
/** extensions that are registered as WebGL render pipes adaptors */
WebGLPipesAdaptor = 'webgl-pipes-adaptor',
/** extensions that are registered as WebGL render systems */
WebGLSystem = 'webgl-system',
/** extensions that are registered as WebGPU render pipes */
WebGPUPipes = 'webgpu-pipes',
/** extensions that are registered as WebGPU render pipes adaptors */
WebGPUPipesAdaptor = 'webgpu-pipes-adaptor',
/** extensions that are registered as WebGPU render systems */
WebGPUSystem = 'webgpu-system',
/** extensions that are registered as Canvas render pipes */
CanvasSystem = 'canvas-system',
/** extensions that are registered as Canvas render pipes adaptors */
CanvasPipesAdaptor = 'canvas-pipes-adaptor',
/** extensions that are registered as Canvas render systems */
CanvasPipes = 'canvas-pipes',
/** extensions that combine the other Asset extensions */
Asset = 'asset',
/** extensions that are used to load assets through Assets */
LoadParser = 'load-parser',
/** extensions that are used to resolve asset urls through Assets */
ResolveParser = 'resolve-parser',
/** extensions that are used to handle how urls are cached by Assets */
CacheParser = 'cache-parser',
/** extensions that are used to add/remove available resources from Assets */
DetectionParser = 'detection-parser',
/** extensions that are registered with the MaskEffectManager */
MaskEffect = 'mask-effect',
/** A type of extension for creating a new advanced blend mode */
BlendMode = 'blend-mode',
/** A type of extension that will be used to auto detect a resource type */
TextureSource = 'texture-source',
/** A type of extension that will be used to auto detect an environment */
Environment = 'environment',
/** A type of extension for building and triangulating custom shapes used in graphics. */
ShapeBuilder = 'shape-builder',
/** A type of extension for creating custom batchers used in rendering. */
Batcher = 'batcher',
}
重要属性
/** @ignore */
_addHandlers: {} as Partial<Record<ExtensionType, ExtensionHandler>>,
/** @ignore */
_removeHandlers: {} as Partial<Record<ExtensionType, ExtensionHandler>>,
/** @ignore */
_queue: {} as Partial<Record<ExtensionType, StrictExtensionFormat[]>>,
_queue用于收集扩展类型,及其对应的扩展插件;
_addHandlers用于添加对应扩展类型的hander,当再次添加同类型的扩展时就会调用hander,举例如下:
extensions.handleByList(ExtensionType.Application, Application._plugins);
我们通过extensions.handleByList,第二个参数是数组,来收集该类型对应插件,如果插件已经被注册,那么就会添加到对应数组Application._plugins里面,同时会有一个默认的handler记录在_addHandlers,用于给Application._plugins添加插件 , 如果对应扩展类型插件没有注册,那么当对应类型插件注册时,就会直接调用这个handler直接添加该插件。
添加插件
/**
* Register new extensions with PixiJS.
* @param extensions - The spread of extensions to add to PixiJS.
* @returns {extensions} For chaining.
*/
add(...extensions: Array<ExtensionFormat | any>)
{
// Handle any extensions either passed as class w/ data or as data
extensions.map(normalizeExtension).forEach((ext) =>
{
ext.type.forEach((type) =>
{
const handlers = this._addHandlers;
const queue = this._queue;
if (!handlers[type])
{
queue[type] = queue[type] || [];
queue[type]?.push(ext);
}
else
{
handlers[type]?.(ext);
}
});
});
return this;
},
从这里可以看出,这个extension的一个作用就是给对应类型应用添加插件,而整个渲染管线需要涉及到的环节(扩展类型应用)都会依赖于这些插件进行处理场景图的渲染,这也就是为什么这个extension可以为开发者扩展他们自己的渲染方式。
3 Ticker
Ticker 是PixiJS一个重要组件,用于管理帧循环、动画和更新逻辑,整个Pixi的场景图更新与渲染都是从这里开始。
3.1 基础用法
创建 Ticker 实例
import { Ticker } from 'pixi.js';
const ticker = new Ticker();
ticker.add((deltaTime) => {
console.log(`Delta time: ${deltaTime}`);
// 执行更新逻辑
});
// 开始运行 Ticker
ticker.start();
全局共享 Ticker
Ticker有两个全局共享ticker,shared主要用于渲染和更新。
static get shared(): Ticker
{
if (!Ticker._shared)
{
const shared = Ticker._shared = new Ticker();
shared.autoStart = true;
shared._protected = true;
}
return Ticker._shared;
}
system主要用于驱动视觉动画和渲染的计时器。
static get system(): Ticker
{
if (!Ticker._system)
{
const system = Ticker._system = new Ticker();
system.autoStart = true;
system._protected = true;
}
return Ticker._system;
}
3.2 主要属性和方法
属性
deltaTime
当前帧和上一帧的时间差,以逻辑帧(通常 1/60 秒)为单位。deltaMS
当前帧和上一帧的时间差,以毫秒为单位。speed
用于调节 Ticker 更新速度的属性。默认值为1,调高或调低该值可以改变更新频率:
ticker.speed = 2; // 加快两倍速度
ticker.speed = 0.5; // 减慢到一半速度
started
表示 Ticker 是否正在运行的布尔值。
方法
add(fn, context, priority?)
向 Ticker 添加回调函数。可选参数context设置回调的上下文,priority定义优先级(数值越低,优先级越高)。
ticker.add((deltaTime) => console.log('Tick 1'), null, -10);
ticker.add((deltaTime) => console.log('Tick 2'), null, 10);
add回调函数会在每一个动画帧调用ticker.update的时候,从链表头部开始依次执行场景图的更新任务:
while (listener)
{
listener = listener.emit(this);
}
2. remove(fn)
移除指定回调:
const update = (deltaTime) => console.log('Updating...');
ticker.add(update);
ticker.remove(update);
start()
启动 Ticker。若已经运行,不会重复启动。stop()
停止 Ticker,暂停所有回调执行。
ticker通常是会与TickerListener配合使用,当ticker出发之后,就会查看有哪些监听ticker的监听器,这个监听器就是负责整个Pixi场景图的更新与渲染。
再来看一下ticker是如何进行渲染循环的
constructor()
{
this._head = new TickerListener(null, null, Infinity);
this.deltaMS = 1 / Ticker.targetFPMS;
this.elapsedMS = 1 / Ticker.targetFPMS;
this._tick = (time: number): void =>
{
this._requestId = null;
if (this.started)
{
// Invoke listeners now
this.update(time);
// Listener side effects may have modified ticker state.
if (this.started && this._requestId === null && this._head.next)
{
this._requestId = requestAnimationFrame(this._tick);
}
}
};
}
可以看到,这个ticker是依赖于浏览器的requestAnimationFrame,在每一帧的时候触发渲染循环。
3.3 TickerListener
TickerListener 代表了添加到 Ticker 的一个回调函数对象。通过 Ticker 的 add 方法注册的每个回调实际上都会生成一个对应的 TickerListener 实例。它能够:
- 按优先级控制回调的执行顺序。
- 管理回调的上下文绑定_context。
- 提供开关以启用或禁用特定回调_fn。
我们来看一下这个ticker是如何添加TickerListener 的
/**
* Internally adds the event handler so that it can be sorted by priority.
* Priority allows certain handler (user, AnimatedSprite, Interaction) to be run
* before the rendering.
* @private
* @param listener - Current listener being added.
* @returns This instance of a ticker
*/
private _addListener(listener: TickerListener): this
{
// For attaching to head
let current = this._head.next;
let previous = this._head;
// Add the first item
if (!current)
{
listener.connect(previous);
}
else
{
// Go from highest to lowest priority
while (current)
{
if (listener.priority > current.priority)
{
listener.connect(previous);
break;
}
previous = current;
current = current.next;
}
// Not yet connected
if (!listener.previous)
{
listener.connect(previous);
}
}
this._startIfPossible();
return this;
}
如果当前添加的listener的优先级大于当前绑定的监听器的优先级,那么就会进行listener.connect(previous)
/**
* Connect to the list.
* @param previous - Input node, previous listener
*/
public connect(previous: TickerListener): void
{
this.previous = previous;
if (previous.next)
{
previous.next.previous = this;
}
this.next = previous.next;
previous.next = this;
}
可以发现,这个connect作用就是将需要链接的监听器绑定到当前监听器的previous,并将当前监听器绑定到previous监听器的next,从这里也可以看出,这个TickerListener实际上就是一个链表,当ticker触发之后,就会从链表header开始依次执行监听器绑定的回调函数。
在这之前,当我发现这个Pixi的ticker依赖于requestAnimationFrame的时候,当时就产生一个疑问,Pixi是如何控制场景图的更新是在渲染之前的,其答案就在这个TickerListener链表上面,整个场景图的渲染是在链表的尾部,也就是说,当场景图更新完毕之后,最后才会进行渲染。
4 render
Renderer 是 PixiJS 中的渲染核心。它根据提供的 DisplayObject (如 Container 或 Sprite)构建场景图,执行绘制操作,将图像最终渲染到屏幕上的 <canvas> 或 WebGL 渲染上下文中。
PixiJS 的 Renderer 支持以下两种渲染模式:
- WebGL 渲染模式:默认使用现代 WebGL(如 WebGL 2 或 WebGL 1)进行硬件加速渲染。
- Canvas 渲染模式:作为回退方案,当 WebGL 不可用时启用。
4.1 Renderer 的类型
1. WebGLRenderer
- 默认的渲染器,使用 WebGL 进行硬件加速。
- 适合处理复杂场景和高性能需求。
2. CanvasRenderer
- 在不支持 WebGL 的环境中启用,例如一些旧浏览器。
- 功能受限,但可以保证基本的绘图需求。
4.2 Renderer 的核心功能
1. 渲染方法
renderer.render(displayObject, options?)
Renderer 的主方法,负责将场景树中的 DisplayObject 渲染到屏幕上。
const app = new PIXI.Application();
const sprite = PIXI.Sprite.from('example.png');
app.stage.addChild(sprite);
// 手动渲染
app.renderer.render(app.stage);
- 参数:
displayObject:需要渲染的根对象(通常是Container或Stage)。options:渲染选项,例如是否清理缓冲区或自定义矩阵。
2. 管理渲染管线
其 提供了更灵活的渲染管线支持,允许开发者扩展和自定义渲染行为。
- 中间件:可以通过注册中间件来扩展渲染器的功能。
- 渲染插件:支持添加自定义渲染逻辑,适用于高级用户。
3. 渲染目标
Renderer 支持将渲染内容输出到以下目标:
- 默认 Canvas:渲染到
<canvas>元素,用于直接显示。 - 离屏缓冲:渲染到
RenderTexture,可以用于生成贴图或实现特效。
const renderTexture = PIXI.RenderTexture.create({ width: 256, height: 256 });
renderer.render(sprite, { renderTexture });
4.3 Renderer 的属性
**1. **view
- 关联的 HTML
<canvas>元素,用于渲染输出。 - 你可以将其添加到 DOM 树中。
document.body.appendChild(renderer.view);
**2. width 和 **height
- 渲染器的宽度和高度,影响渲染输出的分辨率。
**3. **backgroundColor
- 设置场景的背景颜色,使用 16 进制表示法。
4.4 Render初始化做了什么事
1、Constructor初始化SystemRunner
SystemRunner是什么呢?我们看一下Pixi对SystemRunner的介绍:
SystemRunner is used internally by the renderers as an efficient way for systems to
be notified about what the renderer is up to during the rendering phase.
翻译过来就是
SystemRunner被渲染器在内部使用,在渲染阶段通知渲染器正在做什么。简单来说就是通过流水线的方式来进行场景图的渲染,其会涉及到很多环节,而这些环节通过SystemRunner来进行管理。
以下是默认的SystemRunner
const defaultRunners = [
'init',
'destroy',
'contextChange',
'resolutionChange',
'reset',
'renderEnd',
'renderStart',
'render',
'update',
'postrender',
'prerender'
] as const;
这些默认SystemRunner会在render的constructor中添加到render的runners中
constructor(config: RendererConfig)
{
super();
this.type = config.type;
this.name = config.name;
this.config = config;
const combinedRunners = [...defaultRunners, ...(this.config.runners ?? [])];
this._addRunners(...combinedRunners);
// Validation check that this environment support `new Function`
this._unsafeEvalCheck();
}
2 render.init
之后会调用render中的init对render进行初始化
/**
* Initialize the renderer.
* @param options - The options to use to create the renderer.
*/
public async init(options: Partial<OPTIONS> = {})
{
const skip = options.skipExtensionImports === true ? true : options.manageImports === false;
await loadEnvironmentExtensions(skip);
this._addSystems(this.config.systems);
this._addPipes(this.config.renderPipes, this.config.renderPipeAdaptors);
// loop through all systems...
for (const systemName in this._systemsHash)
{
const system = this._systemsHash[systemName];
const defaultSystemOptions = (system.constructor as any).defaultOptions;
options = { ...defaultSystemOptions, ...options };
}
options = { ...AbstractRenderer.defaultOptions, ...options };
this._roundPixels = options.roundPixels ? 1 : 0;
// await emits..
for (let i = 0; i < this.runners.init.items.length; i++)
{
await this.runners.init.items[i].init(options);
}
// store options
this._initOptions = options as OPTIONS;
}
loadEnvironmentExtensions用于加载当前环境对应的扩展,
_addSystems用于添加System,默认需要参与的System如下:
const DefaultWebGLSystems = [
...SharedSystems,
GlUboSystem,
GlBackBufferSystem,
GlContextSystem,
GlBufferSystem,
GlTextureSystem,
GlRenderTargetSystem,
GlGeometrySystem,
GlUniformGroupSystem,
GlShaderSystem,
GlEncoderSystem,
GlStateSystem,
GlStencilSystem,
GlColorMaskSystem,
];
看一下render是如何添加的:
/**
* Add a new system to the renderer.
* @param ClassRef - Class reference
* @param name - Property name for system, if not specified
* will use a static `name` property on the class itself. This
* name will be assigned as s property on the Renderer so make
* sure it doesn't collide with properties on Renderer.
* @returns Return instance of renderer
*/
private _addSystem(ClassRef: SystemConstructor, name: string): this
{
const system = new ClassRef(this as unknown as Renderer);
if ((this as any)[name])
{
throw new Error(`Whoops! The name "${name}" is already in use`);
}
(this as any)[name] = system;
this._systemsHash[name] = system;
for (const i in this.runners)
{
this.runners[i].add(system);
}
return this;
}
SystemRunner
public add(item: unknown): this
{
if ((item as any)[this._name])
{
this.remove(item);
this.items.push(item);
}
return this;
}
(item as any)[this._name]这个表示,定义的System是否跟含有到SystemRunner中的字段,
也就是说SystemRunner用于收集那些涉及到该SystemRunner相关的System,这个System是什么呢?
随便看一个GlShaderSystem
/**
* System plugin to the renderer to manage the shaders for WebGL.
* @memberof rendering
*/
export class GlShaderSystem implements ShaderSystem
根据Pixi对GlShaderSystem这个类的描述可以知道,该类作为一个系统插件,帮助渲染器来管理WebGL的着色器。
也就是说整个渲染系统是依赖于SystemRunner运行,不同的SystemRunner都会关联对应的系统插件,当SystemRunner运行任务时,就会分配给对应的系统插件进行。
再看: this._addPipes(this.config.renderPipes, this.config.renderPipeAdaptors);
这段代码的意思就是添加渲染管线,看一下有哪些通用的渲染管线:
export const SharedRenderPipes = [
BlendModePipe,
BatcherPipe,
SpritePipe,
RenderGroupPipe,
AlphaMaskPipe,
StencilMaskPipe,
ColorMaskPipe,
CustomRenderPipe,
GraphicsPipe
];
渲染管线会被挂在到render,而render又会被挂载到每一个系统插件上,这样再系统插件运行任务时,就可以获取到对应的渲染管线,获取对应的管线去进行渲染对应的图元。
private _addPipes(pipes: RendererConfig['renderPipes'], pipeAdaptors: RendererConfig['renderPipeAdaptors']): void
{
const adaptors = pipeAdaptors.reduce((acc, adaptor) =>
{
acc[adaptor.name] = adaptor.value;
return acc;
}, {} as Record<string, any>);
pipes.forEach((pipe) =>
{
const PipeClass = pipe.value;
const name = pipe.name;
const Adaptor = adaptors[name];
// sorry typescript..
(this.renderPipes as any)[name] = new PipeClass(
this as unknown as Renderer,
Adaptor ? new Adaptor() : null
);
});
}
在添加渲染管线时,还会给渲染管线匹配对应的适配器,这个所谓的适配器实际上就是用于指定图元的渲染,比如说GlGraphicsAdaptor,这个GlGraphicsAdaptor就是用于渲染Pixi的graphics数据的。
4.5 render.render
public render(args: RenderOptions | Container, deprecated?: {renderTexture: any}): void
{
let options = args;
if (options instanceof Container)
{
options = { container: options };
if (deprecated)
{
// #if _DEBUG
// eslint-disable-next-line max-len
deprecation(v8_0_0, 'passing a second argument is deprecated, please use render options instead');
// #endif
options.target = deprecated.renderTexture;
}
}
options.target ||= this.view.renderTarget;
// TODO: we should eventually fix events so that it can handle multiple canvas elements
if (options.target === this.view.renderTarget)
{
// TODO get rid of this
this._lastObjectRendered = options.container;
options.clearColor = this.background.colorRgba;
}
if (options.clearColor)
{
const isRGBAArray = Array.isArray(options.clearColor) && options.clearColor.length === 4;
options.clearColor = isRGBAArray ? options.clearColor : Color.shared.setValue(options.clearColor).toArray();
}
if (!options.transform)
{
options.container.updateLocalTransform();
options.transform = options.container.localTransform;
}
this.runners.prerender.emit(options);
this.runners.renderStart.emit(options);
this.runners.render.emit(options);
this.runners.renderEnd.emit(options);
this.runners.postrender.emit(options);
}
上述代码就是WebGLRenderer的渲染方法,可以发现其依赖于runners运行系统,而每种类型的运行系统都有相关的系统插件与之关联,通过系统插件完成最终的渲染行为。
5 Shader与Program
在WebGL中我们知道 一个 Program 必须包含一个顶点着色器和一个片段着色器。 Shader 是 Program 的组件,而 Program 是 Shader 的执行容器。 Shader 被编译后,Program 会将它们链接在一起形成一个完整的 GPU 执行管线。
Pixi中shader的作用如下:
- 编写自定义顶点和片段着色器代码。
- 创建特殊的图形效果,如渐变、光照、纹理操作等。
- 为对象绑定自定义的渲染逻辑。
Pixi中的shader在创建的时候需要绑定Program,这个Program会包含顶点着色器和片元着色器字符串程序代码,这个Program是Pixi用于管理顶点着色器和片元着色器的类,并不是由于WebGL创建的,shader会绑定这个由Pixi生成的Program, 最终会根据shader及其Program通过WebGL来创建Program,并绑定着色器。
6 通过Graphics案例分析从图形创建到图形渲染的整个流程Pixi都做了什么
通过代码示例来分析整个渲染流程,同时回答以下几个问题:
- 渲染数据如何生成?
- buffer数据如何绑定?
- shader如何生成?
- program如何生成?
- WebGL如何渲染?
先上代码:
import { Application, Graphics } from "pixi.js";
(async () => {
const app = new Application();
await app.init({ antialias: true, resizeTo: window });
document.body.appendChild(app.canvas);
const graphics = new Graphics();
// 矩形
graphics.rect(50, 50, 100, 100);
graphics.fill(0xde3249);
// 矩形 + 线型,pixi支持线型
graphics.rect(350, 50, 100, 100);
graphics.fill(0xc34288);
graphics.stroke({ width: 10, color: 0xffbd01 });
// 圆
graphics.circle(100, 250, 50);
graphics.fill(0xde3249, 1);
// 圆 + 线型
graphics.circle(400, 250, 50);
graphics.fill(0xc34288, 1);
graphics.stroke({ width: 10, color: 0xffbd01 });
// 椭圆 + 线型
graphics.ellipse(600, 250, 80, 50);
graphics.fill(0xaa4f08, 1);
graphics.stroke({ width: 2, color: 0xffffff });
// 连续自由绘制
graphics.moveTo(50, 350);
graphics.lineTo(250, 350);
graphics.lineTo(100, 400);
graphics.lineTo(50, 350);
graphics.fill(0xff3300);
graphics.stroke({ width: 4, color: 0xffd900 });
// Draw a rounded rectangle
graphics.roundRect(50, 440, 100, 100, 16);
graphics.fill(0x650a5a, 0.25);
graphics.stroke({ width: 2, color: 0xff00ff });
// 五角星
graphics.star(360, 370, 5, 50);
graphics.fill(0x35cc5a);
graphics.stroke({ width: 2, color: 0xffffff });
// 七角星
graphics.star(280, 510, 7, 50);
graphics.fill(0xffcc5a);
graphics.stroke({ width: 2, color: 0xfffffd });
// 绘制多段线
const path = [600, 370, 700, 460, 780, 420, 730, 570, 590, 520];
graphics.poly(path);
graphics.fill(0x3500fa);
app.stage.addChild(graphics);
})();
显示效果
6.1 分析上述代码流程:
- 创建应用const app = new Application();
- 初始化应用app.init这一步会给创建render,初始化render,并开启ticker渲染循环;
- 创建一个graphics,graphics主要用于渲染基本形状,如线,圆和显示矩形,并对其着色和填充,在这里会一次性创建多个图元;
6.2 渲染流程
要回答这个问题,我们先理清这个渲染路线,首先由WebGLRenderer调用其render触发渲染任务,当执行 this.runners.render.emit(options);时,渲染开始。前面说过runners是代表系统运行任务,当前执行的是render任务,而这个系统运行任务又会交给其绑定的系统插件,而当前的render任务只绑定了一个RenderGroupSystem系统插件,运行render.emit(options)相当于会触发该系统插件的render任务,也就是说RenderGroupSystem调用自身的render进行渲染,其接受的是一个container,这个container是整个场景图的根节点。RenderGroupSystem当触发渲染任务之后,会交任务交给渲染管线,也就是GraphicsPipe这个用于处理Graphics渲染的类,GraphicsPipe会触发execute进行渲染Graphics数据,但是前面说过,渲染管线会绑定适配器,渲染任务同样会交给适配器来进行,这个适配器是在初始化render为渲染管线分配的,这就需要看适配器初始化做了什么。
下面来看适配器GlGraphicsAdaptor具体做了什么:
public init()
{
const uniforms = new UniformGroup({
uColor: { value: new Float32Array([1, 1, 1, 1]), type: 'vec4<f32>' },
uTransformMatrix: { value: new Matrix(), type: 'mat3x3<f32>' },
uRound: { value: 0, type: 'f32' },
});
const maxTextures = getMaxTexturesPerBatch();
const glProgram = compileHighShaderGlProgram({
name: 'graphics',
bits: [
colorBitGl,
generateTextureBatchBitGl(maxTextures),
localUniformBitGl,
roundPixelsBitGl,
]
});
this.shader = new Shader({
glProgram,
resources: {
localUniforms: uniforms,
batchSamplers: getBatchSamplersUniformGroup(maxTextures),
}
});
}
通过GlGraphicsAdaptor的init方法,可以知道,GlGraphicsAdaptor在初始化的时候就会绑定着色器和程序,看一下这个程序glProgram是如何获取的、
export function compileHighShaderGlProgram({ bits, name }: {bits: HighShaderBit[], name: string}): GlProgram
{
return new GlProgram({
name,
...compileHighShaderGl({
template: {
vertex: vertexGlTemplate,
fragment: fragmentGlTemplate,
},
bits: [
globalUniformsBitGl,
...bits,
]
})
});
}
查看compileHighShaderGlProgram这个方法得知,会创建一个GlProgram实例,并同时通过compileHighShaderGl获取program,vertexGlTemplate与fragmentGlTemplate分别是Pixi默认的顶点着色器和片元着色器模板,而bits是什么呢?可以把他理解为钩子,因为在Pixi中这个所有图元都共享同一个着色器模板,但是不同图元肯定会有自己独特的着色器,那么如何处理不同图元需要配不同着色器呢?Pixi在着色器的生成中设计了一种钩子,将整个着色器的生成添加不同钩子,这样不同渲染场景只需要在创建program实例的时候添加匹配自己渲染场景的钩子即可,而这些钩子会被通过字符串匹配,链接的方式融入到着色器的生成中。
鉴于此我们可以回答**‘program如何生成?’这个问题,以及‘shader如何生成?’**这个问题,
从上面GlGraphicsAdaptor的init方法来看,似乎内部自己还创建了一个shader实例,那这个shader是干嘛的呢?看一下Pixi对它的描述:
/**
* The Shader class is an integral part of the PixiJS graphics pipeline.
* Central to rendering in PixiJS are two key elements: A [shader] and a [geometry].
* The shader incorporates a {@link GlProgram} for WebGL or a {@link GpuProgram} for WebGPU,
* instructing the respective technology on how to render the geometry.
*
* The primary goal of the Shader class is to offer a unified interface compatible with both WebGL and WebGPU.
* When constructing a shader, you need to provide both a WebGL program and a WebGPU program due to the distinctions
* between the two rendering engines. If only one is provided, the shader won't function with the omitted renderer.
*
* Both WebGL and WebGPU utilize the same resource object when passed into the shader.
* Post-creation, the shader's interface remains consistent across both WebGL and WebGPU.
* The sole distinction lies in whether a glProgram or a gpuProgram is employed.
*
* Modifying shader uniforms, which can encompass:
* - TextureSampler {@link TextureStyle}
* - TextureSource {@link TextureSource}
* - UniformsGroups {@link UniformGroup}
* @example
*
* const shader = new Shader({
* glProgram: glProgram,
* gpuProgram: gpuProgram,
* resources: {
* uTexture: texture.source,
* uSampler: texture.sampler,
* uColor: [1, 0, 0, 1],
* },
* });
*
* // update the uniforms
* shader.resources.uColor[1] = 1;
* shader.resources.uTexture = texture2.source;
* @class
* @memberof rendering
*/
翻译过来就是着色器包含一个{@link GlProgram}的WebGL或{@link GpuProgram}的WebGPU,指导如何渲染几何图形的相应技术。 Shader类的主要目标是提供一个兼容WebGL和WebGPU的统一接口。
我们回到GlGraphicsAdaptor,最后会调用GlGraphicsAdaptor的execute进行渲染,看一下execute做了什么?
public execute(graphicsPipe: GraphicsPipe, renderable: Graphics): void
{
const context = renderable.context;
const shader = context.customShader || this.shader;
const renderer = graphicsPipe.renderer as WebGLRenderer;
const contextSystem = renderer.graphicsContext;
const {
batcher, instructions,
} = contextSystem.getContextRenderData(context);
// WebGL specific..
shader.groups[0] = renderer.globalUniforms.bindGroup;
renderer.state.set(graphicsPipe.state);
renderer.shader.bind(shader);
renderer.geometry.bind(batcher.geometry, shader.glProgram);
const batches = instructions.instructions as Batch[];
for (let i = 0; i < instructions.instructionSize; i++)
{
const batch = batches[i];
if (batch.size)
{
for (let j = 0; j < batch.textures.count; j++)
{
renderer.texture.bind(batch.textures.textures[j], j);
}
renderer.geometry.draw('triangle-list', batch.size, batch.start);
}
}
}
注意这个context,这个GraphicsContext是什么呢?,看一下
我们可以发现,这下面的instructions不就是我们之前的graphics进行绘图的操作吗,这些操作会通过GraphicsContext记录下来,通过GraphicsPath记录绘制路径,这就是我们进行 graphics.rect是记录的数据。
这些只是原始数据,并不是着色器需要的数据,接着往下看,contextSystem.getContextRenderData(context),
这个做了什么呢?从字面意思上看就是获取渲染数据,查看方法返回结果,
可知,这里会返回WebGL渲染是需要的顶点数据和索引数据,到这里就可以回答**‘渲染数据如何生成’**这个问题。那么可能会疑惑这个渲染数据是什么,其实就是着色器的顶点数据,和顶点索引,因为我们使用Pixi内部定义的图元类进行绘制图元,这个图元类都是需要通过底层的WebGL进行渲染的,也就是说需要进行接口的对接,包括我们如果要使用PixiJS扩展自己的渲染管线,同样需要去适配PixiJS。
再接着往下看 renderer.shader.bind(shader);
注意shader是由GlShaderSystem管理,从字面意思上看就是绑定着色器,看具体做了什么?
/**
* Changes the current shader to the one given in parameter.
* @param shader - the new shader
* @param skipSync - false if the shader should automatically sync its uniforms.
* @returns the glProgram that belongs to the shader.
*/
public bind(shader: Shader, skipSync?: boolean): void
{
this._setProgram(shader.glProgram);
if (skipSync) return;
defaultSyncData.textureCount = 0;
defaultSyncData.blockIndex = 0;
let syncFunction = this._shaderSyncFunctions[shader.glProgram._key];
if (!syncFunction)
{
syncFunction = this._shaderSyncFunctions[shader.glProgram._key] = this._generateShaderSync(shader, this);
}
syncFunction(this._renderer, shader, defaultSyncData);
}
首先会从给出的shader中绑定的program来创建一个WebGL的program,并创建顶点着色器和片元着色器,将顶点着色器和片元着色器绑定给WebGL的program,最终生成的program会被缓存到GlShaderSystem。
再接着看 renderer.geometry.bind(batcher.geometry, shader.glProgram);
这里会给WebGL绑定buffer,同样的,这个绑定的buffer会被GlBufferSystem缓存起来
/**
* Binds geometry so that is can be drawn. Creating a Vao if required
* @param geometry - Instance of geometry to bind.
* @param program - Instance of program to use vao for.
*/
public bind(geometry?: Geometry, program?: GlProgram): void
{
// shader = shader || this.renderer.shader.shader;
const gl = this.gl;
this._activeGeometry = geometry;
const vao = this.getVao(geometry, program);
if (this._activeVao !== vao)
{
this._activeVao = vao;
gl.bindVertexArray(vao);
}
this.updateBuffers();
}
同时还会把顶点数据和索引数据给GlGeometrySystem
再看一下,顶点数据是如何被绑定到buffer的,
/**
* Activate vertex array object.
* @param geometry - Geometry instance.
* @param program - Shader program instance.
*/
protected activateVao(geometry: Geometry, program: GlProgram): void
{
const gl = this._renderer.gl;
const bufferSystem = this._renderer.buffer;
const attributes = geometry.attributes;
if (geometry.indexBuffer)
{
// first update the index buffer if we have one..
bufferSystem.bind(geometry.indexBuffer);
}
let lastBuffer = null;
// add a new one!
for (const j in attributes)
{
const attribute = attributes[j];
const buffer = attribute.buffer;
const glBuffer = bufferSystem.getGlBuffer(buffer);
const programAttrib = program._attributeData[j];
if (programAttrib)
{
if (lastBuffer !== glBuffer)
{
bufferSystem.bind(buffer);
lastBuffer = glBuffer;
}
const location = programAttrib.location;
// TODO introduce state again
// we can optimise this for older devices that have no VAOs
gl.enableVertexAttribArray(location);
const attributeInfo = getAttributeInfoFromFormat(attribute.format);
const type = getGlTypeFromFormat(attribute.format);
if (programAttrib.format?.substring(1, 4) === 'int')
{
gl.vertexAttribIPointer(location,
attributeInfo.size,
type,
attribute.stride,
attribute.offset);
}
else
{
gl.vertexAttribPointer(location,
attributeInfo.size,
type,
attributeInfo.normalised,
attribute.stride,
attribute.offset);
}
if (attribute.instance)
{
// TODO calculate instance count based of this...
if (this.hasInstance)
{
// Can't use truthiness check to determine if divisor is set,
// since 0 is a valid value for divisor
const divisor = attribute.divisor ?? 1;
gl.vertexAttribDivisor(location, divisor);
}
else
{
throw new Error('geometry error, GPU Instancing is not supported on this device');
}
}
}
}
}
GlGeometrySystem的activateVao,激活顶点数据,将顶点数据绑定到buffer,到这里可以回答‘buffer数据如何绑定’这个问题。
一起准备就绪之后,就剩下渲染了,看一下GlGeometrySystem
/**
* Draws the currently bound geometry.
* @param topology - The type primitive to render.
* @param size - The number of elements to be rendered. If not specified, all vertices after the
* starting vertex will be drawn.
* @param start - The starting vertex in the geometry to start drawing from. If not specified,
* drawing will start from the first vertex.
* @param instanceCount - The number of instances of the set of elements to execute. If not specified,
* all instances will be drawn.
*/
public draw(topology?: Topology, size?: number, start?: number, instanceCount?: number): this
{
const { gl } = this._renderer;
const geometry = this._activeGeometry;
const glTopology = topologyToGlMap[geometry.topology || topology];
instanceCount ||= geometry.instanceCount;
if (geometry.indexBuffer)
{
const byteSize = geometry.indexBuffer.data.BYTES_PER_ELEMENT;
const glType = byteSize === 2 ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT;
if (instanceCount > 1)
{
/* eslint-disable max-len */
gl.drawElementsInstanced(glTopology, size || geometry.indexBuffer.data.length, glType, (start || 0) * byteSize, instanceCount);
/* eslint-enable max-len */
}
else
{
/* eslint-disable max-len */
gl.drawElements(glTopology, size || geometry.indexBuffer.data.length, glType, (start || 0) * byteSize);
/* eslint-enable max-len */
}
}
else if (instanceCount > 1)
{
// TODO need a better way to calculate size..
gl.drawArraysInstanced(glTopology, start || 0, size || geometry.getSize(), instanceCount);
}
else
{
gl.drawArrays(glTopology, start || 0, size || geometry.getSize());
}
return this;
}
GlGeometrySystem.draw负责渲染数据,其会调用WebGL的相关API,到这里就可以回答**WebGL如何渲染这个问题**了。
7 总结
本文详细介绍了PiixJS的Application类,Application是整个应用的入口,关联到Pixi的渲染循环和渲染舞台;齐次又消息说明了Pixi的渲染循环Ticker,Ticker通过requestAnimationFrame来管理整个帧循环,并可以自己设置每一帧的时间; 介绍了WebGLRenderer,WebGLRenderer负责舞台stage数据的渲染,并延申到了Pixi的运行系统SystemRunner,系统插件System以及渲染管线;介绍了Pixi着色器shader已经program如何生成的;并通过graphics案例贯穿了整个渲染流程。
整体来说,Pixi的渲染都是依赖于其渲染关系,不论是graphics还是其它几何模型,整个流程应该都是类似的,具体细节不做研究,如果感兴趣的话,可以看一下PixiJS源码。