博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Cesium原理篇:6 Render模块(5: VAO&RenderState&Command)
阅读量:7010 次
发布时间:2019-06-28

本文共 12377 字,大约阅读时间需要 41 分钟。

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

转载地址:http://vqttl.baihongyu.com/

你可能感兴趣的文章
如何重命名DB- How to Rename DB_NAME with NID?
查看>>
一种真正意义上的Session劫持
查看>>
session 登录中的管理
查看>>
升值加薪的人中为什么么有你???
查看>>
MySQL数据表所有操作命令
查看>>
Linux 之 iptables
查看>>
更改CloudStack中KVM平台的Windows虚拟机默认磁盘类型为VirtIO
查看>>
Tomcat启动脚本
查看>>
Exchange Server 2013 公网发布疑难解答
查看>>
mysql修改密码
查看>>
linux下的静态库和动态库分析
查看>>
2.1 Latches--锁存器 和 FlipFlops--触发器 part1
查看>>
fio 命令入门到跑路(千万不能在系统所在的分区测试硬盘性能)
查看>>
zabbix自动报警邮件正文变成附件问题解决
查看>>
第一篇博客
查看>>
豆瓣阿北:用户价值大于产品体验,通过产品做运营
查看>>
单播(unicast)、组播(multicast)、广播(broadcast)的区别
查看>>
我的友情链接
查看>>
利用clonezilla克隆、还原CentOS整个系统
查看>>
解决127.0.0.1 localhost 劫持问题
查看>>