第二章 渲染在哪里开始?
牢记,按第一章介绍的 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 例子为例
javascript
<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 行 可以看到它是一个抽象类
ts
export abstract class DisplayObject extends utils.EventEmitter<DisplayObjectEvents>
以下显示对象都继承实现了这个抽象类
ts
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 行
ts
ticker.add(this.render, this, UPDATE_PRIORITY.LOW); // 在ticker 内添加了 render() 回调
只要 ticker 开启,就会调用 Application 实例的 render 方法
/packages/app/src/Application.ts
第 70 - 90 行 构造函数与 render 方法
ts
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
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 判断返回的
渲染器类型分为三类:
ts
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
ts
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 行:
ts
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 方法:
ts
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 方法:
ts
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 模式:
js
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();
或用于处理多次调用相同函数
js
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 对象
ts
render(displayObject: IRenderableObject, options?: IRendererRenderOptions): void
{
this.objectRenderer.render(displayObject, options);
}
可以看到调用的是 ObjectRendererSystem 系统的 render 方法
/packages/core/src/render/ObjectRendererSystem.ts
第 49 - 125 行:
ts
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 函数
ts
/**
* 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 行:
ts
displayObject.render(renderer);
// apply transform..
renderer.batch.currentRenderer.flush();
等到 displayObject.render(renderer);
显示对像树遍历收集完渲染数据后才 flush 推到 GPU
进入 /packages/core/batch/src/BatchRenderer.ts
找到 flush 第 625 - 646 行:
ts
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 行
ts
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 渲染了,学不动了...
注:转载请注明出处博客园:王二狗Sheldon池中物 (willian12345@126.com)