LayaAir引擎源码阅读:基础渲染部分(1)
工程结构
最近在看LayaAir2.2 引擎源码的渲染部分,感觉里面还是比较复杂,所以开博客记录一下自己一些理解,希望有不对的地方大家可以讨论一下
首先对于Laya引擎来说一个简单的绘制基本需要用到Render类,Stage类,Context类,Sprite类。Render类用于不断更新绘制与逻辑,Stage类是主舞台类,用于显示对象,Context类是渲染上下文类,包含了一些基本的绘制方法,Sprite类是显示节点类,它可以用于获取一些点击事件和进行一些绘制。当然事实上Laya引擎的渲染过程远远不止这些内容,我们这篇文章主要讨论的是一些二维矢量图的绘制过程,并不包含3D物体和摄像机的渲染与绘制
渲染结构
Render类调用Stage类的render方法进行绘制,而Stage类的父类是Sprite类,Sprite类是通过调用RenderSprite类的方法进行绘制,当然最底层的方法是属于Context类的。它们的关系如下:
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.
上一篇: 30行JavaFX程序大赛结果
下一篇: 探秘Java虚拟机——内存管理与垃圾回收