第二章 渲染在哪裡開始?
牢記,按第一章介紹的 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]);
如此定義的原因
相關知識可參考: 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)