第二章 渲染在哪里开始?
牢记,按第一章介绍的 npm start 启动本地调式环境才可进行调式
如果是 example 文件夹内的例子还需要 serve . 开启本地静态服务器
上一章介绍了 PixiJS 源码调式环境的安装,以及基本的调试方法。本章要研究一下它是如何渲染的
渲染大致步骤:
- 注册渲染器 renderer
- TickerPlugin 的 ticker 会自动开启并调用注册的回调函数 'TickerListener'
- 'TickerListener' 回调内调用 Application render 方法
- Application render 方法会调用渲染器 this.renderer.render(this.stage) 并传入 stage
- 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, 这是一个高性能事件库
EventEmitter https://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.Container PIXI.Graphics PIXI.Sprite PIXI.Text PIXI.BitmapText PIXI.TilingSprite PIXI.AnimatedSprite PIXI.Mesh PIXI.NineSlicePlane PIXI.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, 浏览器控制台会输出
图 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 里存了些啥
图 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
图 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 行
图 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 看看
图 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]);
如此定义的原因
接下来是 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 渲染了,学不动了...