首页/文章列表/文章详情

PixiJS源码分析系列:第二章 渲染在哪里开始?

编程知识2682024-07-17评论

第二章 渲染在哪里开始?

牢记,按第一章介绍的 npm start 启动本地调式环境才可进行调式

如果是 example 文件夹内的例子还需要 serve . 开启本地静态服务器

image

上一章介绍了 PixiJS 源码调式环境的安装,以及基本的调试方法。本章要研究一下它是如何渲染的

渲染大致步骤:

  1. 注册渲染器 renderer

  2. TickerPlugin 的 ticker 会自动开启并调用注册的回调函数 'TickerListener'

  3. 'TickerListener' 回调内调用 Application render 方法

  4. Application render 方法会调用渲染器 this.renderer.render(this.stage) 并传入 stage

  5. stage 是即是显示对像又是容器,所以只要渲染器开始调用 stage 的 render 方法,就会渲染 stage 下的所有子对象从而实现整颗显示对象树的渲染

还是以 example/simple.html 例子为例

<script type="text/javascript">const app = new PIXI.Application({ width: 800, height: 600 }); document.body.appendChild(app.view); const sprite = PIXI.Sprite.from('logo.png'); sprite.x = 100; sprite.y = 100; sprite.anchor.set(0.5); sprite.rotation = Math.PI / 4; app.stage.addChild(sprite); app.ticker.add(() => { sprite.rotation += 0.01; }); </script>

sprite 是 Sprite 对象的实例, Sprite 实例继承自: Container -> DisplayObject -> EventEmitter

朔源至最顶层是 EventEmitter, 这是一个高性能事件库

EventEmitterhttps://github.com/primus/eventemitter3

至于为何它是高性能的,后面章节会顺便分析一下这个库

我们暂时不用去管这个 EventEmitter, 把它当做一个简单的事件收发库就行

先关注一下 DisplayObject,想要在画布中渲染,它必须得继承自 DisplayObject /packages/display/src/DisplayObject.ts

所有 DisplayObject 都继承自 EventEmitter, 可以监听事件, 触发事件

DisplayObject.ts 源码 210 行 可以看到它是一个抽象类

export abstract class DisplayObject extends utils.EventEmitter<DisplayObjectEvents>

以下显示对象都继承实现了这个抽象类

PIXI.ContainerPIXI.Graphics PIXI.Sprite PIXI.Text PIXI.BitmapText PIXI.TilingSprite PIXI.AnimatedSpritePIXI.Mesh PIXI.NineSlicePlanePIXI.SimpleMesh PIXI.SimplePlane PIXI.SimpleRope 

DisplayObject 有一个叫 render 的抽你方法需要子类实现

abstract render(renderer: Renderer): void;

render 方法就是各子类显示对像需要自己去实现绘制自己的方法

回到 example/simple.html 文件

app.stage 就是 Application 类的 stage 属性,它是一个 Container 对象,继承自 DisplayObject

stage 可以看作就是一棵显示对象树,而最顶层就是渲染方法就是 Application 的 render 方法

Application 实例化时它自身公开的 render 方法就被 TickerPlugin 插件的 init 方法调用了

/packages/ticker/TickerPlugin.ts 源码 68 行

ticker.add(this.render, this, UPDATE_PRIORITY.LOW); // 在ticker 内添加了 render() 回调

只要 ticker 开启,就会调用 Application 实例的 render 方法

/packages/app/src/Application.ts 第 70 - 90 行 构造函数与 render 方法

constructor(options?: Partial<IApplicationOptions>){ // The default options options = Object.assign({ forceCanvas: false, }, options); this.renderer = autoDetectRenderer<VIEW>(options); // console.log('hello', 88888); // install plugins here Application._plugins.forEach((plugin) => { plugin.init.call(this, options); });}/** Render the current stage. */public render(): void{ this.renderer.render(this.stage);}

this.renderer 就是渲染器,把 this.stage 整个传到渲染器内渲染

往 stage 内添加子显示对象其实就是往一个 Container 内添加子显示对象,当然由于 Container 继承自 DisplayObject,所以 Container 也需要实现自己的 render 方法

/packages/display/src/Container.ts

render(renderer: Renderer): void{ // 检测是否需要渲染 if (!this.visible || this.worldAlpha <= 0 || !this.renderable) { return; } // 如果是特殊的对象需要特殊的渲染逻辑 if (this._mask || this.filters?.length) { this.renderAdvanced(renderer); } else if (this.cullable) { this._renderWithCulling(renderer); } else { this._render(renderer); for (let i = 0, j = this.children.length; i < j; ++i) { this.children[i].render(renderer); } }}

这个 render 方法很简单,它接受一个 renderer 调用自己的 _render 后再遍历子显示对象调用子显示对象公开的 render 方法

就是一个显示对象树,从顶层开始调用往树了枝叶遍历调用 render 从而实现显示对象树的渲染

有一点需要注意,render 方法内显示它如果是一个 mask 遮罩或自带 filters 滤镜,那么需要调用更高极的渲染方法 renderAdvanced 或 _renderWithCulling,否则它先自己 this._render(renderer);

Container 本身自己的 _render 是空的,意味着它本身不会被渲染,只会被子显示对象渲染,但是继承实现它的子类,比如 Sprite,会去实现自己的 _render 方法覆盖实现渲染

renderer 渲染器

渲染器从哪里来的?

进入渲染器看看

渲染器是由 Application 类的构造函数内 autoDetectRenderer 判断返回的

渲染器类型分为三类:

export enum RENDERER_TYPE{ /** * Unknown render type. * @default 0 */ UNKNOWN, /** * WebGL render type. * @default 1 */ WEBGL, /** * Canvas render type. * @default 2 */ CANVAS,}

我们找到 StartupSystem.ts 文件内的 defaultOptions 对象,将 hello 设为 true

static defaultOptions: StartupSystemOptions = { /** * {@link PIXI.IRendererOptions.hello} * @default false * @memberof PIXI.settings.RENDER_OPTIONS */ hello: true,};

本地服务器下打开 example/simple.html, 浏览器控制台会输出

image

图 2-1

由输出的 PixiJS 7.3.2 - WebGL 2 可知,现在使用的是 WebGL 2

Renderer 类就是我们现在用到的渲染器 /packages/core/src/Renderer.ts

进入到 Renderer.ts 文件可以看到此类继承自 SystemManager 并实现了 IRenderer 接口

export class Renderer extends SystemManager<Renderer> implements IRenderer

进入构造函数:
/packages/core/src/Renderer.ts 第 292 - 364 行:

constructor(options?: Partial<IRendererOptions>){ super(); // Add the default render options options = Object.assign({}, settings.RENDER_OPTIONS, options); this.gl = null; this.CONTEXT_UID = 0; this.globalUniforms = new UniformGroup({ projectionMatrix: new Matrix(), }, true); const systemConfig = { runners: [ 'init', 'destroy', 'contextChange', 'resolutionChange', 'reset', 'update', 'postrender', 'prerender', 'resize' ], systems: Renderer.__systems, priority: [ '_view', 'textureGenerator', 'background', '_plugin', 'startup', // low level WebGL systems 'context', 'state', 'texture', 'buffer', 'geometry', 'framebuffer', 'transformFeedback', // high level pixi specific rendering 'mask', 'scissor', 'stencil', 'projection', 'textureGC', 'filter', 'renderTexture', 'batch', 'objectRenderer', '_multisample' ], }; this.setup(systemConfig); if ('useContextAlpha' in options) { if (process.env.DEBUG) { // eslint-disable-next-line max-len deprecation('7.0.0', 'options.useContextAlpha is deprecated, use options.premultipliedAlpha and options.backgroundAlpha instead'); } options.premultipliedAlpha = options.useContextAlpha && options.useContextAlpha !== 'notMultiplied'; options.backgroundAlpha = options.useContextAlpha === false ? 1 : options.backgroundAlpha; } this._plugin.rendererPlugins = Renderer.__plugins; this.options = options as IRendererOptions; this.startup.run(this.options);}

Renderer 类内有一堆的 runners, plugins, systems

runners 即所谓的 signal '信号', 可以理解为 生命周期+状态变更时就会触发

plugins 即为 Renderer 所专门使用的插件

systems 即为 Renderer 所使用的系统,它由各个系统组合形成了渲染器 Renderer,以一辆车举例,'系统'可以理解组成车子的各个子系统,比如空调系统,油路系统,传动系统 等等

在构造函数中调用的this.setup(systemConfig)就是安装渲染函数所需要用到的系统,它来自/packages/core/system/SystemManager.ts

进入 SystemManager.ts 找到 setup 方法:

setup(config: ISystemConfig<R>): void{ this.addRunners(...config.runners); // Remove keys that aren't available const priority = (config.priority ?? []).filter((key) => config.systems[key]); // Order the systems by priority const orderByPriority = [ ...priority, ...Object.keys(config.systems) .filter((key) => !priority.includes(key)) ]; for (const i of orderByPriority) { this.addSystem(config.systems[i], i); } console.log('看看runners里是什么:',this.runners)}

可以看到,创建了很多个 Runner 对象存储在 this.runners 内

在 setup 函数最后一行打印看看 runners 里存了些啥

image

图 2-3

可以看到各个 Runner 对象的 items 里保存了所有的 system 当 Runner 被调用时,也即触发调用 items 内系统

找到 addSystem 方法:

addSystem(ClassRef: ISystemConstructor<R>, name: string): this{ const system = new ClassRef(this as any as R); 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); } /** * Fired after rendering finishes. * @event PIXI.Renderer#postrender */ /** * Fired before rendering starts. * @event PIXI.Renderer#prerender */ /** * Fired when the WebGL context is set. * @event PIXI.Renderer#context * @param {WebGLRenderingContext} gl - WebGL context. */ return this;}

(this as any)[name] = system; 这一句就把 实例化后的 const system = new ClassRef(this as any as R); '系统' 按名称赋值到了 this 也即 Renderer 实例属性上了

所以通过 this.setup 后, 构造函数最后的 this.startup 属性 (StartupSystem) 可以访问,因为此时已经存在

根据注释,StartupSystem 就是用于负责初始化渲染器的,这是一切渲染的开始...

StartupSystem 的 run 方法 /packages/core/startup/StartupSystem.ts

第 56 - 69 行

run(options: StartupSystemOptions): void{ const { renderer } = this; console.log(renderer.runners.init) renderer.runners.init.emit(renderer.options); if (options.hello) { // eslint-disable-next-line no-console console.log(`PixiJS ${process.env.VERSION} - ${renderer.rendererLogId} - https://pixijs.com`); } renderer.resize(renderer.screen.width, renderer.screen.height);}

第 58 行输出 console.log(renderer.runners.init) 看看名为 init 的 Runner 属性 items 内有 6 个系统需要触发 emit

image

图 2-3

再看看 Runner 类 /packages/core/runner/Runner.ts

根据注释:Runner是一种高性能且简单的信号替代方案。最适合在事件以高频率分配给许多对象的情况下使用(比如每帧!)

注释中举的例子已经很清晰的说明了 Runner 的使用场景了

Runner 类似 Signal 模式:

import { Runner } from '@pixi/runner';const myObject = { loaded: new Runner('loaded'),};const listener = { loaded: function() { // Do something when loaded }};myObject.loaded.add(listener);myObject.loaded.emit();

或用于处理多次调用相同函数

import { Runner } from '@pixi/runner';const myGame = { update: new Runner('update'),};const gameObject = { update: function(time) { // Update my gamey state },};myGame.update.add(gameObject);myGame.update.emit(time);

Signal 和 观察者模式 之间的主要区别在于实现方式和使用场景。观察者模式通常涉及一个主题(Subject)和多个观察者(Observers),主题维护观察者列表并在状态变化时通知观察者。

观察者模式更加结构化,观察者需要显式地注册和注销,而且通常是一对多的关系。

相比之下,Signal 更加简单和灵活,它通常用于处理单个事件或消息的订阅和分发。

Signal 不需要维护观察者列表,而是直接将事件发送给所有订阅者。

Signal 更加轻量级,适用于简单的事件处理场景,而观察者模式更适合需要更多结构和控制的情况。

renderer 的 render 函数

渲染器 Renderer 类内调用的 render 是名为 objectRenderer 的 ObjectRendererSystem 对象

render(displayObject: IRenderableObject, options?: IRendererRenderOptions): void{ this.objectRenderer.render(displayObject, options);}

可以看到调用的是 ObjectRendererSystem 系统的 render 方法

/packages/core/src/render/ObjectRendererSystem.ts 第 49 - 125 行:

render(displayObject: IRenderableObject, options?: IRendererRenderOptions): void{ const renderer = this.renderer; let renderTexture: RenderTexture; let clear: boolean; let transform: Matrix; let skipUpdateTransform: boolean; if (options) { renderTexture = options.renderTexture; clear = options.clear; transform = options.transform; skipUpdateTransform = options.skipUpdateTransform; } // can be handy to know! this.renderingToScreen = !renderTexture; renderer.runners.prerender.emit(); renderer.emit('prerender'); // apply a transform at a GPU level renderer.projection.transform = transform; // no point rendering if our context has been blown up! if (renderer.context.isLost) { return; } if (!renderTexture) { this.lastObjectRendered = displayObject; } if (!skipUpdateTransform) { // update the scene graph const cacheParent = displayObject.enableTempParent(); displayObject.updateTransform(); displayObject.disableTempParent(cacheParent); // displayObject.hitArea = //TODO add a temp hit area } renderer.renderTexture.bind(renderTexture); renderer.batch.currentRenderer.start(); if (clear ?? renderer.background.clearBeforeRender) { renderer.renderTexture.clear(); } displayObject.render(renderer); // apply transform.. renderer.batch.currentRenderer.flush(); if (renderTexture) { if (options.blit) { renderer.framebuffer.blit(); } renderTexture.baseTexture.update(); } renderer.runners.postrender.emit(); // reset transform after render renderer.projection.transform = null; renderer.emit('postrender');}

displayObject.updateTransform(); 这一句,会遍历显示对象树,计算所有显示对象的 localTransform 和 worldTransform ,这对于正常渲染元素的样子与位置至关重要

displayObject.render(renderer); 这一句,也就是传进来的 stage 对象,遍历子显示对象的 render 并将渲染器传入。

最终会调用到 Sprite 内的 _render 方法就是我们加入到 stage 的 'logo.png'

/packages/sprite/src/Sprite.ts 的第 369 - 375 行

image

图 2-4

batch 就是 BatchSystem 的实例

batch 的当前渲染器 ExtensionType.RendererPlugin

再调用 batch 渲染器的 render(this) 将 this 即当前 Sprite 对象传入

batch 批处理渲染器

batch 渲染器定义 /packages/core/batch/src/BatchRenderer.ts

由 BatchRenderer.ts 定义的 extension 可知它是一个 ExtensionType.RendererPlugin类型的扩展插件

在源码最后一行extensions.add(BatchRenderer); 可知,它默认就被安装(实例化)到了 Renderer 上

正是由于默认被实例化安装了,所以才能在 图 2-5 Sprite.ts 的 _render 函数中调用 renderer.plugins[this.pluginName].render(this);

让我们看看 BatchRenderer.ts 的 render 函数

/** * Buffers the"batchable" object. It need not be rendered immediately. * @param {PIXI.DisplayObject} element - the element to render when * using this renderer */render(element: IBatchableElement): void{ if (!element._texture.valid) { return; } if (this._vertexCount + (element.vertexData.length / 2) > this.size) { this.flush(); } this._vertexCount += element.vertexData.length / 2; this._indexCount += element.indices.length; this._bufferedTextures[this._bufferSize] = element._texture.baseTexture; this._bufferedElements[this._bufferSize++] = element;}

可以看到,这个 render 并不是立即渲染,而是将渲染数据缓存起来,等到渲染的时候再进行渲染。

由这个类的注释信息可知,它的作用是先缓存需要渲染的 texture 数据,等待将 多个 texture 信息直接提交到GPU进行批量渲染, 以减少 draw 次数提高性能

在这个 render 函数最后一行加一个 debugger 看看

image
image

图 2-5

/packages/core/src/render/ObjectRendererSystem.ts 的 render 函数, 也就是第 104 - 107 行:

displayObject.render(renderer); // apply transform..renderer.batch.currentRenderer.flush();

等到displayObject.render(renderer); 显示对像树遍历收集完渲染数据后才 flush 推到 GPU

进入/packages/core/batch/src/BatchRenderer.ts 找到 flush 第 625 - 646 行:

flush(): void{ if (this._vertexCount === 0) { return; } this._attributeBuffer = this.getAttributeBuffer(this._vertexCount); this._indexBuffer = this.getIndexBuffer(this._indexCount); this._aIndex = 0; this._iIndex = 0; this._dcIndex = 0; this.buildTexturesAndDrawCalls(); this.updateGeometry(); this.drawBatches(); // reset elements buffer for the next flush this._bufferSize = 0; this._vertexCount = 0; this._indexCount = 0;}

至此 flush() 函数,才是真正调用 webgl 处

_attributeBuffer 是一个 ViewableBuffer 的实例对象

而随后的 this.buildTexturesAndDrawCalls(); 会调用 buildTexturesAndDrawCalls -> buildDrawCalls -> packInterleavedGeometry

/packages/core/batch/src/BatchRenderer.ts 766 - 800 行

packInterleavedGeometry(element: IBatchableElement, attributeBuffer: ViewableBuffer, indexBuffer: Uint16Array, aIndex: number, iIndex: number): void{ const { uint32View, float32View, } = attributeBuffer; const packedVertices = aIndex / this.vertexSize; const uvs = element.uvs; const indicies = element.indices; const vertexData = element.vertexData; const textureId = element._texture.baseTexture._batchLocation; const alpha = Math.min(element.worldAlpha, 1.0); const argb = Color.shared .setValue(element._tintRGB) .toPremultiplied(alpha, element._texture.baseTexture.alphaMode > 0); // lets not worry about tint! for now.. for (let i = 0; i < vertexData.length; i += 2) { float32View[aIndex++] = vertexData[i]; float32View[aIndex++] = vertexData[i + 1]; float32View[aIndex++] = uvs[i]; float32View[aIndex++] = uvs[i + 1]; uint32View[aIndex++] = argb; float32View[aIndex++] = textureId; } for (let i = 0; i < indicies.length; i++) { indexBuffer[iIndex++] = packedVertices + indicies[i]; }}

packInterleavedGeometry 内会将 element.vertexData 顶点数据, uvs, argb 等信息存入 attributeBuffer

indexBuffer 是用来存储 sprite 渲染时所需的顶点索引的缓冲区。

在渲染 sprite 时,引擎需要知道如何连接顶点以形成正确的形状,而这些连接顶点的顺序就是通过 _indexBuffer 中的数据来定义的。

每三个索引对应一个顶点,通过这些索引,引擎可以正确地连接顶点以渲染出 sprite 的形状。

如果你把 indexBuffer 打印出来可以看到有 12 个值, WebGL 绘制几何体都是由三角形组成的

矩形由2个三角形组成

let vertices = [0.5, 0.5, 0.0,-0.5, 0.5, 0.0,-0.5, -0.5, 0.0, // 第一个三角形-0.5, -0.5, 0.0,0.5, -0.5, 0.0,0.5, 0.5, 0.0, // 第二个三角形]; // 矩形

有一条边是公共,这个时候可以索引缓冲区对象减少冗余的数据

索引缓冲对象全称是 Index Buffer Object(IBO),通过索引的方式复用已有的数据。

顶点位置数据只需要 4 个就足够了,公共数据使用索引代替。

const vertices = [0.5, 0.5, 0.0, // 第 1 个顶点-0.5, 0.5, 0.0, // 第 2 个顶点-0.5, -0.5, 0.0, // 第 3 个顶点0.5, -0.5, 0.0, // 第 4 个顶点]; // 矩形

绘制模式为 gl.TRIANGLES 时,两个三角形是独立的,索引数据如下:

const indexData = [
0, 1, 2, // 对应顶点位置数据中 1、2、3 顶点的索引
0, 2, 3, // 对应顶点位置数据中 1、3、4 顶点的索引
]

这就是为什么Sprite.ts 类中 const indices = new Uint16Array([0, 1, 2, 0, 2, 3]);如此定义的原因

相关知识可参考:https://segmentfault.com/a/1190000041144928

接下来是 this.updateGeometry(); 简单来说它它会创建几何模型 和 shader

最后调用 this.drawBatches() 内调用 gl.drawElements() 将前面缓存整理好的 buffer 绘制到 GPU

不管是 Canvas context 还是 WebGL 都是非对象的过程式的调用,PixiJS 的 Renderer 封装了这些操作,让开发者更专注于业务逻辑。

将过程式的调用封装成对象

WebGL 想要渲染,原理:

顶点着色器 + 片段着色器, 顶点着色器确定顶点位置,片段着色器确定每个片元的像素颜色

组成的着色程序 program 后通过 gl.drawArrays 或 gl.drawElements 运行一个着色方法对绘制到 GPU 上

我们采取先整体再细节的方式阅读源码,WebGL 具体渲染挺复杂的,暂时可以略过,如果有兴趣可以参考 WebGL 教程

这是一个很好的 WebGL 教程 https://webglfundamentals.org/webgl/lessons/zh_cn/webgl-fundamentals.html

本章小节

本章通过分析 webgl 渲染器,顺带看了部分 PixiJS 的 system/SystemManager"系统设计", 咋一看确实很复杂

优秀的设计时分值得借鉴,完全可以运用到自己的项目或组件库内

我对 webgl 了解的十分粗浅但借助 debugger 还是可以一步一步分析出逻辑走向,道阻且长啊

最新的 PixiJS 已经支持 WebGPU 渲染了,学不动了...


注:转载请注明出处博客园:王二狗Sheldon池中物 (willian12345@126.com)

神弓

博客园

这个人很懒...

用户评论 (0)

发表评论

captcha