木匣子

Web/Game/Programming/Life etc.

浅析 Pixi.js 渲染流程

Phaser 是一个非常流行的 html5 游戏开发引擎,而它使用的渲染层是由 Pixi.js 提供。Pixi 是一个相当完善的基于 WebGL 的 2D 渲染层,对不支持 WebGL 浏览器可采用 Canvas 垫背(2D WebGL renderer with canvas fallback)。

由于 WebGL 是 OpenGL ES 2.0 的子集,而 OpenGL ES 2.0 又是 OpenGL 2.x 的子集,所以我觉得从 WebGL 入手理解游戏引擎的渲染层如何工作是个比较容易的方向;可以忽略很多高极特性,去关注最本质的部分。

我很好奇 Pixi 是如何把 Shader、Texture、FrameBuffer 等 OpenGL 元素用 javascript 组织到一起的,于是下载了它的源码做了简单的分析。

以下是 example 1 - Basic/index.html 的节选:

// create a new instance of a pixi stage
var stage = new PIXI.Stage(0x66FF99);

// create a renderer instance
var renderer = PIXI.autoDetectRenderer(400, 300);

// add the renderer view element to the DOM
document.body.appendChild(renderer.view);

requestAnimFrame(animate);

// create a texture from an image path
var texture = PIXI.Texture.fromImage("bunny.png");

// create a new Sprite using the texture
var bunny = new PIXI.Sprite(texture);

// center the sprites anchor point
bunny.anchor.x = 0.5;
bunny.anchor.y = 0.5;

// move the sprite to the center of the screen
bunny.position.x = 200;
bunny.position.y = 150;

stage.addChild(bunny);

function animate() {
  // just for fun, let's rotate mr rabbit a little
  bunny.rotation += 0.1;

  // render the stage
  renderer.render(stage);

  requestAnimFrame(animate);
}

requestAnimFrame(animate);

Main Loop

Pixi 工作的大致流程是:

  1. 创建一个 Renderer 渲染器,并获取其 view(一个 canvas 对象)添加到网页中。(或者指定一个 canvas 对象作为其 view);

  2. 在 MainLoop 中调用 Renderer.render() 方法并传入一个 DisplayObject 作为根节点,开始渲染工作。

Scene Graph

Pixi 的场景树是模仿 Flash 的 DisplayObject 架构。这点很有趣,因为我到看了好几个 html5 引擎(例如 e-gret engine)都以此来吸纳 Flash 开发者。仔细读完这个场景树模型,觉得它确实比 cocos2d 那套要简单一些,通用性也更广。

Sprite

精灵可能是一个游戏中最重要的元素,所以对 Sprite 的显示过程,我精读了与其相关的绝大部分代码,并用 Dia 画了个图方便理解。

pixi_sprite.png

EventTarget

由于在网页上加载外部图像资源是异步的。所以 Pixi 引入了事件机制,用于通知图像加载情况。

记得以前刚学 Flash 开发的时候,用外部资源创建 Sprite 的时候,直接取它的 width 和 height 总是得到 0 百思不得其解,后来才意识到这是一个「异步」操作。

BaseTexture

BaseTexture 引用一个 HTMLImageElement 对象,它可以是单图,也可以是一个图集。是 gl.Texture 的数据来源。

Texture

Texture 引用一个 BaseTexture 对象(Texture.emptyTexture 例外),并保存 UV 顶点数据用于渲染。 UV 决定了 Texture 显示 BaseTexture 的具体部分(使用图集中的子图)。

Sprite

Sprite 引用一个 Texture 用于展示其外观。渲染时提交自身的坐标、颜色等信息以及 Texture 的 UV 信息,同时还会记录 Sprite 使用的 Shader 。

Rendering

Pixi 的渲染工作主要由 Renderer 完成,WebGLRenderer 由数个部分组件,这里我们主要关注 WebGLShaderManager 和 WebGLSpriteBatch 。

pixi_renderer.png

对渲染工作比较直观的理解就是从场景树的根节点开始,以 zIndex 为序坐小到大进行深度优先遍历,对每个结点执行渲染操作。由后至前(画家算法)把整个场景绘制一遍,如此往复(CanvasRenderer 的做法)。

即使每一个 Sprite 只有 4个顶点要处理,如果有 1000 个 Sprite 则要执行 1000 个 WebGL Call ;这样的渲染流程会让 WebGL 花费大量的时间在 Shader 及上下文参数的切换上。实际上这 1000 个操作如用使用的是同一个 Shader ,可以将其合并为一个 WebGL Call 即可完成这部分渲染工作。

以上的优化方法即是 WebGLSpriteBatch 的作为。WebGLRenderer 遍历场景树的时候并不是直接将结点绘制出来,而是一个收集信息的过程。在开始渲染时,WebGLSpriteBatch 开辟一块很大数组的数组。然后经过每个结点时,把结点的顶点信息存入该数组。遍历后场景树后,分析其中可以合并的渲染批次,然后分别 render 到 framebuffer 中。

Cocos2d-x 3.0 提出的 RenderCommand/CommandQueue 也是基于类似的优化技巧。

在 Renderer 执行渲染操作之前,会执行 Shader 及上下文的切换,还有创建及更新帖图数据等操作,这里会涉及到具体 Shader 和 Texture 的组织操作。

好了 Pixi 暂时分析到这里吧。现在对 WebGL 的应用有了更宏观层面的理解了。