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


    第二章 渲染在哪里开始?

    牢记,按第一章介绍的 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 例子为例

    
    

    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)
    {
        // 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 implements IRenderer
    

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

    constructor(options?: Partial)
    {
        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): 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, 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)

  • 相关阅读:
    LeetCode_哈希表_中等_454.四数相加 II
    自然语言处理应用(二):自然语言推断
    MobTech ShareSDK 后台配置说明
    mapreduce任务优化
    什么软件支持汇总和计算员工业绩
    网络安全学习路线
    D. Edge Weight Assignment(思维 + 构造)(有证明)
    长时间序列遥感数据处理及在全球变化、物候提取、植被变绿与固碳分析、生物量估算与趋势分析等领域中的应用
    【百度智能体】零代码创建职场高情商话术助手智能体
    【K-means聚类算法】实现鸢尾花聚类
  • 原文地址:https://www.cnblogs.com/willian/p/18307490