从零开始手撸WebGL3D引擎4:Mesh的封装

本篇讲述mini3d.js对静态模型的封装,目前支持三角形列,使用或不使用索引,
暂不支持子模型。涉及到的类有Mesh, VertexBuffer, IndexBuffer, VertexFormat,以及预定义顶点语义VertexSemantic

总体结构

一个Mesh由一个VertexBuffer和零或一个IndexBuffer构成。模型所有的顶点的属性数据采用交错数组的方式存放在VertexBuffer中。即:

[position1, normal1, uv1,  position2, normal2, uv2, ...., position N, normal N, uvN]

这种方式。目前顶点的所有属性统一采用Float32格式。这是为了简化采取的妥协。
如果属性的格式不一致,则需要分开放到不同的数组中。目前不支持多数组(即多个vbo)。现在的设计是个极简的设计,可以支持绝大多数情况。未来如果要支持更多的功能,则可能修改这个设计,例如如果想支持动态修改模型属性并重新提交到显卡,为了避免大量数据的重复提交,可能把不变的数据和需要动态修改的数据分开,放到两个数组(vbo)中。
上面顶点具体使用哪些属性,如何排列,则是通过VertexFormat定义。VertexFormat由一组VertexSemantic构成,semantic是一个字符串,除了在mini3d.VertexSemantic中预定义的属性,还可使用任何自定义的属性字符串。定义semantic一方面是为了描述顶点属性的格式,另一方面是为了和Vertex Shader的输入attribute进行关联。
至于IndexBuffer是可选的,当手创建简单的2D面片,例如UI时,可以不使用索引。Mesh会根据是否提供IndexBuffer使用不同的draw call。Index的格式根据传入的索引数据的数量自动选择Uint8Uint16。mini3d.js没有处理索引数量超过65536的情况。

VertexFormat 顶点格式

要使用WebGL进行渲染,必先提供数据。模型由顶点构成,顶点除了坐标这个最基本的数据外,还可能有很多其他的数据,例如法线、切线、纹理坐标、颜色等等。除了这些常用的数据,还可能有自定义数据。因为Shader具有极高的灵活性,除了完成常用的渲染算法外,还可能采用各种独一无二的渲染方式,甚至将逻辑置于其中。我们的目标是能支持任意类型的顶点数据,方便程序进行扩展。由于顶点数据是和Vertex Shader的attribute对应的,所以还需要管理好这个对应关系。VertexFormat类如下:

class VertexFormat{
     
    constructor(){
     
        this.attribs = [];
        this.attribSizeMap = {
     };
        this._vertexSize = 0;
    }

    addAttrib(attribSemantic, size){
            
        this.attribs.push(attribSemantic); 
        this.attribSizeMap[attribSemantic] = size;        
    }

    getVertexSize(){
     
        if(this._vertexSize === 0){
               
            for(let i=0; i<this.attribs.length; ++i){
     
                let semantic = this.attribs[i];
                this._vertexSize += this.attribSizeMap[semantic];
            }
        }        
        return this._vertexSize;
    }
}

其内部使用this.attribs数组存放顶点属性的语义(semantic),semantic是一个字符串,mini3d.js预定义了一些:

let VertexSemantic = {
         
    POSITION : 'position',        
    NORMAL : 'normal',
    TANGENT : 'tangent',
    COLOR : 'color',
    UV0 : 'uv0',
    UV1 : 'uv1',  
    UV2 : 'uv2', 
    UV3 : 'uv3'
}

也可以填写自定义的。所谓语义,就是词语的含义,借用到这儿表示这个顶点属性的含义,他是表示的位置,还是表示的法线,或者是一个自定义的含义。this.attribs是一个数组,因此同时也能表示这些语义的顺序,最终VertexBuffer生成提交到vbo的顶点数据时,会按照这个顺序排列顶点属性。这个顺序重要吗?也许不重要,但是提供了一个可能性吧。以前有说法某某显卡,采用某种顺序提交顶点会有优化。

addAttrib(attribSemantic, size)方法添加一个属性语义,同时指定了这个属性的成员数量。上面说过,Mesh的顶点属性统一采用Float32类型,以便把他们打包到一个VBO中。而这儿的size就是指定某属性用几个Float32表示。例如:

format.addAttrib(mini3d.VertexSemantic.POSITION, 3);

表示添加一个属性表示顶点坐标,使用3个Float32。

getVertexSize()方法会返回整个顶点所有属性的Float32总数。

VertexBuffer类

VertexBuffer由Mesh内部使用。主要的设计考虑是分别设置各个属性的数据,然后根据顶点格式进行_compile得到javascript的数组,然后创建一个Flaot32Array并上传到vbo中。
所以核心是_compile函数和upload函数。

设置顶点属性数据

setData(semantic, data)为某个属性设置所有的顶点的数据。也就是说一个模型中的所有顶点的某类数据是放在一个javascript数组中设置进来的。这么做对于手工创建模型比较方便,对于载入模型是否方便要看模型文件的格式。如果有问题也会提供逐顶点设置数据的接口。注意的是,虽然各个属性的数据是分开多个数组设置的,但是最终提交到VBO的是一个交错数组。在_compile这个内部函数中会生成这个数组。

提交数据到显存

unpload()方法首先调用_compile()生成所有顶点数据的交错数组,然后创建一个Float32Array并且上传到this._vbo中。之后会释放这个交错数组。

绘制时绑定属性数据到shader

这是对gl.vertexAttribPointergl.enableVertexAttribArray的封装。顶点shader需要知道每个attribute的数据从何而来,在绘制前会绑定VBO,但是属性在VBO中的具体位置和size都需要指定。bindAttrib(shader)方法封装了这个功能。之所以传入shader,是要使用shader获取到attriube的location。

let location = shader.getAttributeLocation(semantic);

获取location同样使用了semantic。所以正如上文说到semantic起到联系VertexBuffer和Shader的作用。由于我们还没封装材质,所以这地方暂时用shader。

IndexBuffer

目前基本的简化就是图元只使用gl.TRIANGLES。
setData(data)方法用于接受索引数据。
upload()方法会根据索引数量生成Uint8Array或Uint16Array

Mesh接口

Mesh只是提供了一个面向使用着的接口,主要的功能都在VertexBuffer和IndexBuffer中。

创建

constructor(vertexFormat)构造函数中传入一个VertexFormat实例,内部会创建一个VertexBuffer。

设置顶点数据

setVertexData(semantic, data) 内部调用了VertexBuffer的setData方法。

设置三角形索引数据

setTriangles(data)如果调用则会创建出IndexBuffer,并调用其setData方法。

上传数据到显卡

upload()方法内部调用了VertexBuffer和IndexBuffer的upload。

绘制

render(shader){
     
        gl.bindBuffer(gl.ARRAY_BUFFER, this._vertexBuffer.vbo);
        
        this._vertexBuffer.bindAttrib(shader);
                  
        if(this._indexBuffer){
     
            gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this._indexBuffer.vbo);
            gl.drawElements(this._indexBuffer.mode, this._indexBuffer.indexCount, this._indexBuffer.type, 0);
            gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
        } else {
     
            gl.drawArrays(gl.TRIANGLES, 0, this._vertexBuffer.vertexCount);
        }
        
        this._vertexBuffer.unbindAttrib(shader);

        gl.bindBuffer(gl.ARRAY_BUFFER, null);
    }

执行WebGL的bindBuffer,然后调用VertexBuffer的bindAttrib(shader),之后执行draw call。

关于子模型

子模型是未来可能要支持的功能,模型之所以要切分为子模型,是为了采用不同的材质。这个还是挺常见的。实现方式,正常来说,是通过索引中不同的分段来指定子模型使用的顶点。毕竟子模型是模型的一部分,子模型之间会共享顶点,因此Vertex Buffer还是用同一个。当然这样做子模型顶点就不能使用不同的顶点属性了,必须提供顶点属性的并集,但是这应该不常见。

你可能感兴趣的:(从零开始手撸webGL3D引擎,webgl)