zoukankan      html  css  js  c++  java
  • cesium 原理 之 command拼接

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

  • 相关阅读:
    location 匹配规则
    nginx+keepalived 高可用方案
    Nginx 静态文件服务
    web服务器-nginx优化
    Oracle 修改字符集(AL32UTF8 转换成UTF8字符集)
    xshell复制粘贴
    关于mysql中的DDL,DML,DQL和DCL
    LVS实现Kubernetes集群高可用
    k8s实践(一):Centos7.6部署k8s(v1.14.2)集群
    Centos7.6部署k8s v1.16.4高可用集群(主备模式)
  • 原文地址:https://www.cnblogs.com/yanan-boke/p/9140519.html
Copyright © 2011-2022 走看看