VAO(Vertext Array Object),中文是顶点数组对象。之前在《Buffer》一文中,我们介绍了 Cesium 如何创建 VBO 的过程,而 VAO 可以简单的认为是基于 VBO 的一个封装,为顶点属性数组和 VBO 中的顶点数据之间建立了关联。我们来看一下使用示例:
- var indexBuffer = Buffer.createIndexBuffer({
- context: context,
- typedArray: indices,
- usage: BufferUsage.STATIC_DRAW,
- indexDatatype: indexDatatype
- });
- var buffer = Buffer.createVertexBuffer({
- context: context,
- typedArray: typedArray,
- usage: BufferUsage.STATIC_DRAW
- });
- // 属性数组,当前是顶点数据z
- // 因此,该属性有3个分量XYZ
- // 值类型为float,4个字节
- // 因此总共占3 *4= 12字节
- attributes.push({
- index: 0,
- vertexBuffer: buffer,
- componentsPerAttribute: 3,
- componentDatatype: ComponentDatatype.FLOAT,
- offsetInBytes: 0,
- strideInBytes: 3 * 4,
- normalize: false
- });
- // 根据属性数组和顶点索引构建VAO
- var va = new VertexArray({
- context: context,
- attributes: attributes,
- indexBuffer: indexBuffer
- });
如同,创建顶点数据和顶点索引的部分之前已经讲过,然后将顶点数据添加到属性数组中,并最终构建成 VAO,使用方式很简单。
- function VertexArray(options) {
- var vao;
- // 创建VAO
- if (context.vertexArrayObject) {
- vao = context.glCreateVertexArray();
- context.glBindVertexArray(vao);
- bind(gl, vaAttributes, indexBuffer);
- context.glBindVertexArray(null);
- }
- }
- function bind(gl, attributes, indexBuffer) {
- for (vari) {
- var attribute = attributes[i];
- if (attribute.enabled) {
- // 绑定顶点属性
- attribute.vertexAttrib(gl);
- }
- }
- if (defined(indexBuffer)) {
- // 绑定顶点索引
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer._getBuffer());
- }
- }
- attr.vertexAttrib = function(gl) {
- var index = this.index;
- // 之前通过Buffer创建的顶点数据_getBuffer
- gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer._getBuffer());
- // 根据Attribute中的属性值来设置如下参数
- gl.vertexAttribPointer(index, this.componentsPerAttribute, this.componentDatatype, this.normalize, this.strideInBytes, this.offsetInBytes);
- gl.enableVertexAttribArray(index);
- if (this.instanceDivisor > 0) {
- context.glVertexAttribDivisor(index, this.instanceDivisor);
- context._vertexAttribDivisors[index] = this.instanceDivisor;
- context._previousDrawInstanced = true;
- }
- };
指定 DrawCommand 的渲染状态,比如剔除,多边形偏移,深度检测等,通过 RenderState 统一管理:
- function RenderState(renderState) {
- var rs = defaultValue(renderState, {});
- var cull = defaultValue(rs.cull, {});
- var polygonOffset = defaultValue(rs.polygonOffset, {});
- var scissorTest = defaultValue(rs.scissorTest, {});
- var scissorTestRectangle = defaultValue(scissorTest.rectangle, {});
- var depthRange = defaultValue(rs.depthRange, {});
- var depthTest = defaultValue(rs.depthTest, {});
- var colorMask = defaultValue(rs.colorMask, {});
- var blending = defaultValue(rs.blending, {});
- var blendingColor = defaultValue(blending.color, {});
- var stencilTest = defaultValue(rs.stencilTest, {});
- var stencilTestFrontOperation = defaultValue(stencilTest.frontOperation, {});
- var stencilTestBackOperation = defaultValue(stencilTest.backOperation, {});
- var sampleCoverage = defaultValue(rs.sampleCoverage, {});
- }
前面我们讲了 VBO/VAO,Texture,Shader 以及 FBO,终于万事俱备只欠东风了,当我们一切准备就绪,剩下的就是一个字:干。Cesium 中提供了三类 Command:DrawCommand、ClearCommand 以及 ComputeCommand。我们先详细的讲 DrawCommand,同时也是最常用的。
- var colorCommand = new DrawCommand({
- owner: primitive,
- // TRIANGLES
- primitiveType: primitive._primitiveType
- });
- colorCommand.vertexArray = primitive._va;
- colorCommand.renderState = primitive._rs;
- colorCommand.shaderProgram = primitive._sp;
- colorCommand.uniformMap = primitive._uniformMap;
- colorCommand.pass = pass;
如上是 DrawCommand 的创建方式,这里只有两个新的知识点,一个是 owner 属性,记录该 DrawCommand 是谁的菜,另外一个是 pass 属性。这是渲染队列的优先级控制。目前,Pass 的枚举如下,具体内容下面后涉及:
- var Pass = {
- ENVIRONMENT: 0,
- COMPUTE: 1,
- GLOBE: 2,
- GROUND: 3,
- OPAQUE: 4,
- TRANSLUCENT: 5,
- OVERLAY: 6,
- NUMBER_OF_PASSES: 7
- };
创建完的 DrawCommand 会通过 update 函数,加载到 frameState 的 commandlist 队列中,比如 Primitive 中 update 加载 drawcommand 的伪代码:
- Primitive.prototype.update = function(frameState) {
- var commandList = frameState.commandList;
- var passes = frameState.passes;
- if (passes.render) {
- var colorCommand = colorCommands[j];
- commandList.push(colorCommand);
- }
- if (passes.pick) {
- var pickLength = pickCommands.length;
- var pickCommand = pickCommands[k];
- commandList.push(pickCommand);
- }
- }
进入队列后就开始听从安排,随时准备上前线(渲染)。Scene 会先对所有的 commandlist 会排序,Pass 值越小优先渲染,通过 Pass 的枚举可以看到最后渲染的是透明的和 overlay:
- function createPotentiallyVisibleSet(scene) {
- for (var i = 0; i < length; ++i) {
- var command = commandList[i];
- var pass = command.pass;
- // 优先computecommand,通过GPU计算
- if (pass === Pass.COMPUTE) {
- computeList.push(command);
- }
- // overlay最后渲染
- else if (pass === Pass.OVERLAY) {
- overlayList.push(command);
- }
- // 其他command
- else {
- var frustumCommandsList = scene._frustumCommandsList;
- var length = frustumCommandsList.length;
- for (var i = 0; i < length; ++i) {
- var frustumCommands = frustumCommandsList[i];
- frustumCommands.commands[pass][index] = command;
- }
- }
- }
- }
根据渲染优先级排序后,会先渲染环境相关的 command,比如 skybox,大气层等,接着,开始渲染其他 command:
- function executeCommands(scene, passState) {
- // 地球
- var commands = frustumCommands.commands[Pass.GLOBE];
- var length = frustumCommands.indices[Pass.GLOBE];
- for (var j = 0; j < length; ++j) {
- executeCommand(commands[j], scene, context, passState);
- }
- // 球面
- us.updatePass(Pass.GROUND);
- commands = frustumCommands.commands[Pass.GROUND];
- length = frustumCommands.indices[Pass.GROUND];
- for (j = 0; j < length; ++j) {
- executeCommand(commands[j], scene, context, passState);
- }
- // 其他非透明的
- var startPass = Pass.GROUND + 1;
- var endPass = Pass.TRANSLUCENT;
- for (var pass = startPass; pass < endPass; ++pass) {
- us.updatePass(pass);
- commands = frustumCommands.commands[pass];
- length = frustumCommands.indices[pass];
- for (j = 0; j < length; ++j) {
- executeCommand(commands[j], scene, context, passState);
- }
- }
- // 透明的
- us.updatePass(Pass.TRANSLUCENT);
- commands = frustumCommands.commands[Pass.TRANSLUCENT];
- commands.length = frustumCommands.indices[Pass.TRANSLUCENT];
- executeTranslucentCommands(scene, executeCommand, passState, commands);
- // 后面在渲染Overlay
- }
接着,就是对每一个 DrawCommand 的渲染,也就是把之前 VAO,Texture 等等渲染到 FBO 的过程,这一块 Cesium 也封装的比较好,有兴趣的可以看详细代码,这里只讲一个逻辑,太困了。。。
- DrawCommand.prototype.execute = function(context, passState) {
- // Contex开始渲染
- context.draw(this, passState);
- };
- Context.prototype.draw = function(drawCommand, passState) {
- passState = defaultValue(passState, this._defaultPassState);
- var framebuffer = defaultValue(drawCommand._framebuffer, passState.framebuffer);
- // 准备工作
- beginDraw(this, framebuffer, drawCommand, passState);
- // 开始渲染
- continueDraw(this, drawCommand);
- };
- function beginDraw(context, framebuffer, drawCommand, passState) {
- var rs = defaultValue(drawCommand._renderState, context._defaultRenderState);
- // 绑定FBO
- bindFramebuffer(context, framebuffer);
- // 设置渲染状态
- applyRenderState(context, rs, passState, false);
- // 设置ShaderProgram
- var sp = drawCommand._shaderProgram;
- sp._bind();
- }
- function continueDraw(context, drawCommand) {
- // 渲染参数
- var primitiveType = drawCommand._primitiveType;
- var va = drawCommand._vertexArray;
- var offset = drawCommand._offset;
- var count = drawCommand._count;
- var instanceCount = drawCommand.instanceCount;
- // 设置Shader中的参数
- drawCommand._shaderProgram._setUniforms(drawCommand._uniformMap, context._us, context.validateShaderProgram);
- // 绑定VAO数据
- va._bind();
- var indexBuffer = va.indexBuffer;
- // 渲染
- if (defined(indexBuffer)) {
- offset = offset * indexBuffer.bytesPerIndex; // offset in vertices to offset in bytes
- count = defaultValue(count, indexBuffer.numberOfIndices);
- if (instanceCount === 0) {
- context._gl.drawElements(primitiveType, count, indexBuffer.indexDatatype, offset);
- } else {
- context.glDrawElementsInstanced(primitiveType, count, indexBuffer.indexDatatype, offset, instanceCount);
- }
- }
- va._unBind();
- }
ClearCommand 用于清空缓冲区的内容,包括颜色,深度和模板。用户在创建的时候,指定清空的颜色值等属性:
- function Scene(options) {
- // Scene在构造函数中创建了clearCommand
- this._clearColorCommand = new ClearCommand({
- color: new Color(),
- stencil: 0,
- owner: this
- });
- }
然后在渲染中更新队列执行清空指令:
- function updateAndClearFramebuffers(scene, passState, clearColor, picking) {
- var clear = scene._clearColorCommand;
- // 设置想要清空的颜色值,默认为(1,0,0,0,)
- Color.clone(clearColor, clear.color);
- // 通过execute方法,清空当前FBO对应的帧缓冲区
- clear.execute(context, passState);
- }
然后,会根据你设置的颜色,深度,模板值来清空对应的帧缓冲区,代码好多啊,但很容易理解:
- Context.prototype.clear = function(clearCommand, passState) {
- clearCommand = defaultValue(clearCommand, defaultClearCommand);
- passState = defaultValue(passState, this._defaultPassState);
- var gl = this._gl;
- var bitmask = 0;
- var c = clearCommand.color;
- var d = clearCommand.depth;
- var s = clearCommand.stencil;
- if (defined(c)) {
- if (!Color.equals(this._clearColor, c)) {
- Color.clone(c, this._clearColor);
- gl.clearColor(c.red, c.green, c.blue, c.alpha);
- }
- bitmask |= gl.COLOR_BUFFER_BIT;
- }
- if (defined(d)) {
- if (d !== this._clearDepth) {
- this._clearDepth = d;
- gl.clearDepth(d);
- }
- bitmask |= gl.DEPTH_BUFFER_BIT;
- }
- if (defined(s)) {
- if (s !== this._clearStencil) {
- this._clearStencil = s;
- gl.clearStencil(s);
- }
- bitmask |= gl.STENCIL_BUFFER_BIT;
- }
- var rs = defaultValue(clearCommand.renderState, this._defaultRenderState);
- applyRenderState(this, rs, passState, true);
- var framebuffer = defaultValue(clearCommand.framebuffer, passState.framebuffer);
- bindFramebuffer(this, framebuffer);
- gl.clear(bitmask);
- };
ComputeCommand 需要配合 ComputeEngine 一起使用,可以认为是一个特殊的 DrawCommand,它不是为了渲染,而是通过渲染机制,实现 GPU 的计算,通过 Shader 计算结果保存到纹理传出的一个过程,实现在 Web 前端高效的处理大量的数值计算,下面,我们通过学习之前 ImageryLayer 中对墨卡托影像切片动态投影的过程来了解该过程。
首先,创建一个 ComputeCommand,定义这个计算过程前需要准备的内容,以及计算后对计算结果如何处理:
- var computeCommand = new ComputeCommand({
- persists: true,
- owner: this,
- // 执行前计算一下当前网格中插值点经纬度和墨卡托
- // 并构建相关的参数,比如GLSL中的计算逻辑
- // 传入的参数,包括attribute和uniform等
- preExecute: function(command) {
- reprojectToGeographic(command, context, texture, imagery.rectangle);
- },
- // 执行后的结果保存在outputTexture
- postExecute: function(outputTexture) {
- texture.destroy();
- imagery.texture = outputTexture;
- finalizeReprojectTexture(that, context, imagery, outputTexture);
- imagery.releaseReference();
- }
- });
还记得 Pass 中的 Compute 枚举吧,放在第一位,每次 Scene.update 时,发现有 ComputeCommand 都会优先计算,这个逻辑和 DrawCommand 一样,都会在 update 中 push 到 commandlist 中,比如在 ImageryLayer 中,则是在
queueReprojectionCommands 方法完成的,而具体的执行也和 DrawCommand 比较相似,稍微有一些特殊和针对的部分,具体代码如下:
- ComputeCommand.prototype.execute = function(computeEngine) {
- computeEngine.execute(this);
- };
- ComputeEngine.prototype.execute = function(computeCommand) {
- if (defined(computeCommand.preExecute)) {
- // Ready?
- computeCommand.preExecute(computeCommand);
- }
- var outputTexture = computeCommand.outputTexture;
- var width = outputTexture.width;
- var height = outputTexture.height;
- // ComputeEngine是一个全局类,在Scene中可以获取
- // 内部有一个Drawcommand
- // 把ComputeCommand中的参数赋给DrawCommand
- var drawCommand = drawCommandScratch;
- drawCommand.vertexArray = vertexArray;
- drawCommand.renderState = renderState;
- drawCommand.shaderProgram = shaderProgram;
- drawCommand.uniformMap = uniformMap;
- drawCommand.framebuffer = framebuffer;
- // Go!
- drawCommand.execute(context);
- if (defined(computeCommand.postExecute)) {
- // Over~
- computeCommand.postExecute(outputTexture);
- }
- };
Renderer 系列告一段落,并没有涉及很多 WebGL 的语法层面,主要希望大家能对各个模块的作用有一个了解,并在这个了解的基础上,学习一下 Cesium 对 WebGL 渲染引擎的封装技巧。通过这一系列,个人很佩服 Cesium 的开发人员对 OpenGL 渲染引擎的理解,在完成这一系列的过程中,个人受益匪浅,也希望能对各位起到一个分享和帮助。
基于功能的面向函数的接口,封装成基于状态管理的面向对象的封装,方便了我们的使用和管理。但从中我们还是可以看到,WebGL 在某些方面的薄弱,比如实例化和 FBO 的部分功能需要在 WebGL2.0 的规范下才支持,当然对此,我表示乐观,我感受到了 WebGL 标准化的快速发展。
另外,我也想到了用 Three.js 封装 Cesium 渲染引擎的可能,当然我对 Three.js 不了解,但随着不断学习 Cesium。Renderer,我个人并不喜欢这个想法。我觉得在设计和封装上,Renderer 已经很不错了,我们可以借鉴 Three.js 在功能和易用性上的特点,强化 Cesium,而不是全盘否定重新造轮子。而且并不能因为点上的优势而进行面上的推倒,如果对这两个引擎都不了解,最好还是埋头学习少一点高谈阔论。基本功是顿悟不出来的。
来源: http://www.cnblogs.com/fuckgiser/p/6002210.html