欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

LayaAir引擎源码阅读:基础渲染部分(1)

程序员文章站 2022-07-12 23:27:57
...

LayaAir引擎源码阅读:基础渲染部分(1)

工程结构

最近在看LayaAir2.2 引擎源码的渲染部分,感觉里面还是比较复杂,所以开博客记录一下自己一些理解,希望有不对的地方大家可以讨论一下

首先对于Laya引擎来说一个简单的绘制基本需要用到Render类,Stage类,Context类,Sprite类。Render类用于不断更新绘制与逻辑,Stage类是主舞台类,用于显示对象,Context类是渲染上下文类,包含了一些基本的绘制方法,Sprite类是显示节点类,它可以用于获取一些点击事件和进行一些绘制。当然事实上Laya引擎的渲染过程远远不止这些内容,我们这篇文章主要讨论的是一些二维矢量图的绘制过程,并不包含3D物体和摄像机的渲染与绘制

渲染结构

Render类调用Stage类的render方法进行绘制,而Stage类的父类是Sprite类,Sprite类是通过调用RenderSprite类的方法进行绘制,当然最底层的方法是属于Context类的。它们的关系如下:
LayaAir引擎源码阅读:基础渲染部分(1)

LayaAir引擎工程中常见的Main.ts的结构如下

class Main {
    constructor() {
        //初始化引擎     
        Laya.init(Browser.clientWidth, Browser.clientHeight, WebGL);
        
        // 设置舞台
        Laya.stage.scaleMode = GameConfig.scaleMode;
        Laya.stage.screenMode = GameConfig.screenMode;
        Laya.stage.alignV = GameConfig.alignV;
        Laya.stage.alignH = GameConfig.alignH;
    }
}
//**启动类
new Main();

从上述代码可以看出我们如果要运行一个小程序,首先需要初始化Laya引擎,然后对舞台进行设置。那么它是如何对一些形状进行绘制的呢?答案就在Laya.init里面,可以看到init函数里面对一些必要元素进行了初始化

Laya.enableWebGLPlus();
CacheManger.beginCheck();
stage = Laya.stage = new Stage();
ILaya.stage = Laya.stage;
Utils.gStage = Laya.stage;
URL.rootPath = URL._basePath = Laya._getUrlPath();
MeshQuadTexture.__int__();
MeshVG.__init__();
MeshTexture.__init__();
Laya.render = new Render(0, 0, Browser.mainCanvas);
render = Laya.render;
Laya.stage.size(width, height);
((<any>window)).stage = Laya.stage;
​
WebGLContext.__init__();
MeshParticle2D.__init__();
ShaderCompile.__init__();
RenderSprite.__init__();
KeyBoardManager.__init__();

Render类

而我们需要注意的是new Render(0, 0, Browser.mainCanvas);这个实例化操作,这里它创建了Render类的实例,而LayaAir引擎的渲染是通过Render类来管理的,它是一个单例,可以直接通过Laya.render 访问,首先看Render类的构造函数:

constructor(width: number, height: number, mainCanv: HTMLCanvas) {
        Render._mainCanvas = mainCanv;
        let source: HTMLCanvasElement = Render._mainCanvas.source as HTMLCanvasElement;
        //创建主画布
        source.id = "layaCanvas";
        source.width = width;
        source.height = height;
        if (Render.isConchApp) {
            document.body.appendChild(source);  // 添加HTMLCanvas
        }this.initRender(Render._mainCanvas, width, height); //初始化渲染器
        window.requestAnimationFrame(loop); // 调用loop函数
        function loop(stamp: number): void {
            ILaya.stage._loop(); // 调用stage类的_loop()函数
            window.requestAnimationFrame(loop);
        }
        ILaya.stage.on("visibilitychange", this, this._onVisibilitychange); // 可见性修改
    }

可以看到构造Render类实际上是做了创建画布,初始化渲染器(initRender),调用loop函数进行某些操作,可见性修改这几个工作。这里的window.requestAnimationFrame() 其实就是告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。所以这里的loop回调函数就是浏览器不断重绘时,传入给浏览器的更新函数,它也是一切更新的源头,不过在看loop函数之前,先了解一下initRender函数帮我们初始化了哪些参数

initRender()

首先它内部定义了一个function 用于获取WebGLContext,names 数组就包含了获取WebGL的绘图上下文的参数

// initRender(canvas: HTMLCanvas, w: number, h: number): boolean
​
function getWebGLContext(canvas: any): WebGLRenderingContext {
        // ... 
        var names: any[] = ["webgl2", "webgl", "experimental-webgl", "webkit-3d", "moz-webgl"];
        // ... 
        try {
            gl = canvas.getContext(names[i], { stencil: Config.isStencil, alpha: Config.isAlpha, antialias: Config.isAntialias, premultipliedAlpha: Config.premultipliedAlpha, preserveDrawingBuffer: Config.preserveDrawingBuffer });//antialias为true,premultipliedAlpha为false,IOS和部分安卓QQ浏览器有黑屏或者白屏底色BUG
        }
    }

常见的创建一个WebGL的上下文模板缓存的代码如下:

var canvas = document.getElementById('canvas1');
var context = canvas.getContext('webgl', { antialias: false, stencil: true });  // canvas.getContext(contextType, contextAttributes);

常见的WebGLContextAttributes如下:
1. Alpha
如果它的值是 true,它提供了一个alpha缓冲区到画布上。默认情况下,它的值是 true
2. depth
如果它的值是true,会得到一个绘图的缓冲区,其中包含至少16位的深度缓冲。默认情况下,它的值是true
3. stencil
如果它的值是true,会得到一个绘图的缓冲区,其中包含至少8位的模板缓存。默认情况下,它的值是false
4. antialias
如果它的值是true,会得到一个绘图缓冲区,执行抗锯齿。默认情况下,它的值是true
5. premultipliedAlpha
如果它的值是true,会得到一个绘图缓冲区,其中包含的颜色与预乘alpha。默认情况下它的值是true
6. preserveDrawingBuffer
如果它的值是true,缓冲区将不会被清零,直到被清除或由作者改写将保留它们的值。默认情况下,它的值是false

之后initRender函数进行了一些初始化:

// initRender(canvas: HTMLCanvas, w: number, h: number): boolean
​
    var gl: WebGLRenderingContext = LayaGL.instance = WebGLContext.mainContext = getWebGLContext(Render._mainCanvas.source); // 获取WebGl渲染上下文if (!gl)
        return false;
​
    LayaGL.instance = gl;
    LayaGL.layaGPUInstance = new LayaGPU(gl, WebGL._isWebGL2);
​
    canvas.size(w, h);  //在ctx之后调用。
    Context.__init__();  // 包含一些绘制上下文框的初始化,lineWidth的初始化等等
    SubmitBase.__init__();  // 渲染提交类的初始化,包含渲染的key值,渲染_renderType值等
​
    var ctx: Context = new Context(); // 新建context实例
    ctx.isMain = true;
    Render._context = ctx;  // 初始化Render类的渲染上下文
    canvas._setContext(ctx);//TODO 现在有个问题是 gl.deleteTexture并没有走WebGLContex封装的
    // 一些初始化
    ShaderDefines2D.__init__();
    Value2D.__init__();
    Shader2D.__init__();
    Buffer2D.__int__(gl); // 缓冲初始化
    BlendMode._init_(gl);

loop()

既然Render类的构造函数调用了stage类的_loop函数,所以需要了解一下这个_loop函数到底在stage类里面干了什么

stage类

LayaAir所有元素都是显示在舞台上的(Laya.stage),舞台是显示游戏元素的平台,在游戏视觉编程里,一切游戏的元素必须添加到舞台才能被显示。因此,舞台也是放置显示对象的最终容器,而所有的渲染过程也是从Laya.stage开始的
Laya.stage中的_loop()函数就是帧循环函数,函数内部调用render函数用于渲染当前context渲染上下文和更新逻辑的,当我们在initRender里初始化了渲染器之后,就可以对Render._context进行渲染,如下:

// stage 类
// ...
_loop(): boolean {
    this._globalRepaintGet = this._globalRepaintSet;
    this._globalRepaintSet = false;
    this.render(Render._context, 0, 0); // 对Render._context的渲染上下文 进行渲染
    return true;
}
Laya.stage的render函数的大致结构为:设置帧率模式,更新参数,渲染,提交
// stage 类
// ...
render(context: Context, x: number, y: number): void {
        if (((<any>window)).conch) {
            this.renderToNative(context, x, y); // 没有设置帧率模式的渲染
            return;
        }
    
        if (this._frameRate === Stage.FRAME_SLEEP) {
            ...... // 获取帧率更新模式
        }// 更新参数,包括切换帧率模式,渲染次数计数等
        ......if (this.renderingEnabled) {
            for (var i: number = 0, n: number = this._scene3Ds.length; i < n; i++)//更新3D场景
                this._scene3Ds[i]._update();
            context.clear();
            super.render(context, x, y);  // 渲染,调用父类Sprite.render来渲染,context是渲染的上下文引用,包含变换,ALPHA,drawTexture,graphics.render,和子节点循环渲染
            Stat._StatRender.renderNotCanvas(context, x, y);
        }
        // 提交并销毁
        if (this.renderingEnabled) {
            Stage.clear(this._bgColor);
            context.flush();
            VectorGraphManager.instance && VectorGraphManager.getInstance().endDispose();
        }
        // 更新逻辑
        this._updateTimers();
    }

其中上述super.render(context, x, y); 比较重要,它是属于stage的父类Sprite类的方法,所以我们还需要看看Sprite类

Sprite类

Sprite 类是基本的显示图形的显示列表节点,它是LayaAir引擎的核心显示类,它可以调用graphics 来绘制图片或者矢量图,操作如下:

this.sp = new Sprite();
Laya.stage.addChild(this.sp);
this.sp.graphics.drawLine(10, 58, 146, 58, "#ff0000", 3); //画直线

graphics类以命令流方式存储绘图显示对象,可以通过类内置cmds属性访问所有命令流,它里面也是调用Context类(一个渲染上下文类,包含了一些渲染绘制方式等等)的一些绘制方法,只不过把其存入了命令流,如下列代码:

drawLine(fromX: number, fromY: number, toX: number, toY: number, lineColor: string, lineWidth: number = 1): DrawLineCmd {
    var offset: number = (lineWidth < 1 || lineWidth % 2 === 0) ? 0 : 0.5;
    return this._saveToCmd(Render._context._drawLine, DrawLineCmd.create.call(this, fromX + offset, fromY + offset, toX + offset, toY + offset, lineColor, lineWidth, 0));
}

可以看到是调用了Context类的drawline方法。
而Spirte类控制渲染的render函数如下:

// Sprite类
// ...
render(ctx: Context, x: number, y: number): void {
        RenderSprite.renders[this._renderType]._fun(this, ctx, x + this._x, y + this._y); // renders[] 数组包含一系列RenderSprite

该函数表示对renders数组(由一系列RenderSprite精灵构成)中的第this._renderType 个RenderSprite精灵进行渲染(调用_fun函数),_fun函数是RenderSprite类的方法,定义了属于不同类型的不同的渲染方式,里面都是通过调用Context类的方法进行绘制的,需要注意的是RenderSprite精灵在Laya.init初始化的时候就已经初始化了,所以这时它的_fun也确定好了,由以下定义:

switch (type) {
    case 0:
        this._fun = this._no;
        return;
        //case SpriteConst.IMAGE: 
        //_fun = this._image;
        //return;
    case SpriteConst.ALPHA:
        this._fun = this._alpha;
        return;
    case SpriteConst.TRANSFORM:
        this._fun = this._transform;  // 该函数调用了 context类的context.transform方法进行transform
        return;
    case SpriteConst.BLEND:
        this._fun = this._blend;
        return;
    case SpriteConst.CANVAS:
        this._fun = this._canvas;
        return;
    case SpriteConst.MASK:
        this._fun = this._mask;
        return;
    case SpriteConst.CLIP:
        this._fun = this._clip;
        return;
    case SpriteConst.STYLE:
        this._fun = this._style;
        return;
    case SpriteConst.GRAPHICS:
        this._fun = this._graphics;
        return;
    case SpriteConst.CHILDS:
        this._fun = this._children;
        return;
    case SpriteConst.CUSTOM:
        this._fun = this._custom;
        return;
    case SpriteConst.TEXTURE:
        this._fun = this._texture;
        return;
        //case SpriteConst.IMAGE | SpriteConst.GRAPHICS: 
        //_fun = this._image2;
        //return;
        //case SpriteConst.IMAGE | SpriteConst.TRANSFORM | SpriteConst.GRAPHICS: 
        //_fun = this._image2;
        //return;
    case SpriteConst.FILTERS:
        this._fun = Filter._filter;
        return;
    case RenderSprite.INIT:
        this._fun = RenderSprite._initRenderFun;
        return;
}

Context类

所以兜兜转转最终是回到了Context类,让我们看看里面常用的一些绘制函数,有drawLine 用于绘制一条线,_drawLines用于绘制一系列线,drawCurves绘制一系列曲线等等,比如下面的_drawLines函数,结构也是比较清晰的,先确定起始点,再确定终点,点和点之间的结构是Path()结构,然后stroke()进行绘制

_drawLine(x: number, y: number, fromX: number, fromY: number, toX: number, toY: number, lineColor: string, lineWidth: number, vid: number): void {
    this.beginPath();  // Path结构
    this.strokeStyle = lineColor;
    this.lineWidth = lineWidth;
    this.moveTo(x + fromX, y + fromY); // 起点
    this.lineTo(x + toX, y + toY); // 终点
    this.stroke();
}

可以看到Laya的一些矢量图的绘制基本都是在context类里面的,所以如果想了解里面如何进行绘制的,可以比较详细的看一下context类

参考链接:
[1] https://blog.csdn.net/weixin_36719607/article/details/91439377.
[2] https://developer.mozilla.org/zh-CN/docs/Web/API/WebGL_API.