PixiJS原始碼分析系列:第二章 渲染在哪裡開始?

池中物王二狗發表於2024-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, 這是一個高效能事件庫

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, 瀏覽器控制檯會輸出

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)

相關文章