VAO

       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 ( var i = 0; i < attributes.length; ++i) {
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;
}
};

RenderState

       指定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, {});
}

Drawcommand

       前面我们讲了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

       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

       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,而不是全盘否定重新造轮子。而且并不能因为点上的优势而进行面上的推倒,如果对这两个引擎都不了解,最好还是埋头学习少一点高谈阔论。基本功是顿悟不出来的。

Cesium原理篇:6 Render模块(5: VAO&RenderState&Command)的更多相关文章

  1. Cesium原理篇:7最长的一帧之Entity(下)

    上一篇,我们介绍了当我们添加一个Entity时,通过Graphics封装其对应参数,通过EntityCollection.Add方法,将EntityCollection的Entity传递到DataSo ...

  2. Cesium原理篇:5最长的一帧之影像

    如果把地球比做一个人,地形就相当于这个人的骨骼,而影像就相当于这个人的外表了.之前的几个系列,我们全面的介绍了Cesium的地形内容,详见: Cesium原理篇:1最长的一帧之渲染调度 Cesium原 ...

  3. Cesium原理篇:3最长的一帧之地形(2:高度图)

           这一篇,接着上一篇,内容集中在高度图方式构建地球网格的细节方面.        此时,Globe对每一个切片(GlobeSurfaceTile)创建对应的TileTerrain类,用来维 ...

  4. Cesium原理篇:6 Renderer模块(1: Buffer)

    刚刚结束完地球切片的渲染调度后,打算介绍一下目前大家都很关注的3D Tiles方面的内容,但发现要讲3D Tiles,或者充分理解它,需要对DataSource,Primitive要有基础,而这要求对 ...

  5. Cesium原理篇:6 Renderer模块(1: Buffer)【转】

    https://www.bbsmax.com/A/n2d9P1Q5Dv/ 刚刚结束完地球切片的渲染调度后,打算介绍一下目前大家都很关注的3D Tiles方面的内容,但发现要讲3D Tiles,或者充分 ...

  6. Cesium原理篇:GroundPrimitive

    今天来看看GroundPrimitive,选择GroundPrimitive有三个目的:1 了解GroundPrimitive和Primitive的区别和关系 2 createGeometry的特殊处 ...

  7. Cesium原理篇:GroundPrimitive【转】

    今天来看看GroundPrimitive,选择GroundPrimitive有三个目的:1 了解GroundPrimitive和Primitive的区别和关系 2 createGeometry的特殊处 ...

  8. Cesium原理篇:6 Render模块(3: Shader)

    在介绍Renderer的第一篇,我就提到WebGL1.0对应的是OpenGL ES2.0,也就是可编程渲染管线.之所以单独强调这一点,算是为本篇埋下一个伏笔.通过前两篇,我们介绍了VBO和Textur ...

  9. Cesium原理篇:6 Render模块(6: Instance实例化)

    最近研究Cesium的实例化,尽管该技术需要在WebGL2.0,也就是OpenGL ES3.0才支持.调试源码的时候眼前一亮,发现VAO和glDrawBuffers都不是WebGL1.0的标准函数,都 ...

随机推荐

  1. GiuHub 使用

    一 Mac 能不能连接安卓手机 1 USB数据线  设置 > 通用 > 开发人员选项 > USB调试 > 选择"相机PTP模式"  连接后,手机中的照片和视 ...

  2. Android ANR 分析解决方法

    一:什么是ANR ANR:Application Not Responding,即应用无响应 二:ANR的类型 ANR一般有三种类型: 1. KeyDispatchTimeout(5 seconds) ...

  3. wpf之mvvm基类

    当我们用MVVM设计模式的时候要实现INotifyPropertyChanged,每次都要实现这个接口比较麻烦,所以基类的作用就体现出来了.代码如下:   1 2 3 4 5 6 7 8 9 10 1 ...

  4. DBImport v3.3 中文版发布:数据库数据互导及文档生成工具(IT人员必备)

    前言: 好久没写文了, 距离上一篇文章是3个月前的事了,虽然工作很忙,主要还是缺少写作的内容和激情,所以没怎么动手. 之前有一个来月不断面试不同层次来应聘的人员,很有想写文的冲动,后来还是忍住了. 估 ...

  5. 用 maven filter 管理不同环境的配置文件

    使用 maven profile 一个项目可以部署在不同的环境当中,maven 的 profile 针对不同的环境指定各自的编译方法.在 pom.xml 的 profile 中,可以根据不同的环境定制 ...

  6. JS获取剪贴板图片之后的格式选择与压缩问题

    前言 某年某月的某一天,突然发现博客服务器上上传的图片都比较大,一些很小的截图都有几百kb,本来服务器带宽就慢,不优化一下说不过去. 问题细述 特别说明:本文代码因为只是用于我自己后台写markdow ...

  7. ASP.NET MVC 5 -从控制器访问数据模型

    在本节中,您将创建一个新的MoviesController类,并在这个Controller类里编写代码来取得电影数据,并使用视图模板将数据展示在浏览器里. 在开始下一步前,先Build一下应用程序(生 ...

  8. MySQL中的全文索引

    之前曾经发表了一篇关于SQL Server全文索引的文章.现在将MySQL全文索引的配置过程记录一下. Step1:创建Student表 CREATE TABLE `student` ( `id` I ...

  9. Qt And MFC Mouse Over Tips

    Qt鼠标提示分析说明 关于鼠标停留在控件上面,显示提示内容的方法. 对于Qt来说, Qt的某一个控件类, 如果属于GUI的, 那么这个控件类会有一个setToolTip(QString text)的方 ...

  10. 重温 w3cshool css3

    border-radius: 2em 1em 4em / 0.5em 3em;  兼容性IE9+.Firefox 4+.Chrome.Safari 5+ 以及 Opera 支持 border-radi ...