Skip to content

绘制一个自定义立方体

在第一个前端工程搭建好之后,我们可以开始创建一个立方体,来熟悉一个最基本的网格模型的渲染。

  • 让我们回忆一下RoyNativePC,在onStart函数中,我们创建了场景、相机,在这之后我们调用封装起来的一个函数创建了一个甜甜圈。现在我们把这个封装起来的函数替换掉,看看一个最简单的网格模型如何创建。先上代码。
typescript
createACube(){
    // Check the supplied options and provide defaults for unspecified ones
    const he = new Vec3(0.5, 0.5, 0.5);
    const ws = 1;
    const ls = 1;
    const hs = 1;

    const corners = [
        new Vec3(-he.x, -he.y, he.z),
        new Vec3(he.x, -he.y, he.z),
        new Vec3(he.x, he.y, he.z),
        new Vec3(-he.x, he.y, he.z),
        new Vec3(he.x, -he.y, -he.z),
        new Vec3(-he.x, -he.y, -he.z),
        new Vec3(-he.x, he.y, -he.z),
        new Vec3(he.x, he.y, -he.z)
    ];

    const faceAxes = [
        [0, 1, 3], // FRONT
        [4, 5, 7], // BACK
        [3, 2, 6], // TOP
        [1, 0, 4], // BOTTOM
        [1, 4, 2], // RIGHT
        [5, 0, 6]  // LEFT
    ];

    const faceNormals = [
        [0,  0,  1], // FRONT
        [0,  0, -1], // BACK
        [0,  1,  0], // TOP
        [0, -1,  0], // BOTTOM
        [1,  0,  0], // RIGHT
        [-1,  0,  0]  // LEFT
    ];

    const sides = {
        FRONT: 0,
        BACK: 1,
        TOP: 2,
        BOTTOM: 3,
        RIGHT: 4,
        LEFT: 5
    };

    let vtxCount = ((ws + 1) * (hs + 1) + (ws + 1) * (ls + 1) + (ls + 1) * (hs + 1)) * 2;
    let idxCount = ((ws * ls + ws * hs + hs * ls) * 2) * 2;
    const positions = new Float32Array(vtxCount * 3);
    const normals = new Float32Array(vtxCount * 3);
    const uvs = new Float32Array(vtxCount * 2);
    const uvs1 = new Float32Array(vtxCount * 2);
    const indices = new Uint32Array(idxCount * 3);

    let vtxIndex = 0;
    let idxIndex = 0;
    let vcounter = 0;

    const primitiveUv1Padding = 4.0 / 64;
    const primitiveUv1PaddingScale = 1.0 - primitiveUv1Padding * 2;
    const generateFace = (side:number, uSegments:number, vSegments:number) => {
        const temp1 = new Vec3();
        const temp2 = new Vec3();
        const temp3 = new Vec3();
        const r = new Vec3();

        for (let i = 0; i <= uSegments; i++) {
            for (let j = 0; j <= vSegments; j++) {
                temp1.lerp(corners[faceAxes[side][0]], corners[faceAxes[side][1]], i / uSegments);
                temp2.lerp(corners[faceAxes[side][0]], corners[faceAxes[side][2]], j / vSegments);
                temp3.subtractVectors(temp2, corners[faceAxes[side][0]]);
                r.addVectors(temp1, temp3);
                let u = i / uSegments;
                let v = j / vSegments;

                positions[vtxIndex * 3 + 0] = r.x;
                positions[vtxIndex * 3 + 1] = r.y;
                positions[vtxIndex * 3 + 2] = r.z;
                normals[vtxIndex * 3 + 0] = faceNormals[side][0];
                normals[vtxIndex * 3 + 1] = faceNormals[side][1];
                normals[vtxIndex * 3 + 2] = faceNormals[side][2];
                uvs[vtxIndex * 2 + 0] = u;
                uvs[vtxIndex * 2 + 1] = 1 - v;

                // pack as 3x2. 1/3 will be empty, but it's either that or stretched pixels
                // TODO: generate non-rectangular lightMaps, so we could use space without stretching
                u = u * primitiveUv1PaddingScale + primitiveUv1Padding;
                v = v * primitiveUv1PaddingScale + primitiveUv1Padding;
                u /= 3;
                v /= 3;

                u += (side % 3) / 3;
                v += Math.floor(side / 3) / 3;
                uvs1[vtxIndex * 2 + 0] = u;
                uvs1[vtxIndex * 2 + 1] = 1 - v;

                vtxIndex++;

                if ((i < uSegments) && (j < vSegments)) {
                    indices[idxIndex * 3 + 0] = vcounter + vSegments + 1;
                    indices[idxIndex * 3 + 1] = vcounter + 1;
                    indices[idxIndex * 3 + 2] = vcounter;
                    idxIndex++;

                    indices[idxIndex * 3 + 0] = vcounter + vSegments + 1;
                    indices[idxIndex * 3 + 1] = vcounter + vSegments + 2;
                    indices[idxIndex * 3 + 2] = vcounter + 1;
                    idxIndex++;
                }

                vcounter++;
            }
        }
    };

    generateFace(sides.FRONT, ws, hs);
    generateFace(sides.BACK, ws, hs);
    generateFace(sides.TOP, ws, ls);
    generateFace(sides.BOTTOM, ws, ls);
    generateFace(sides.RIGHT, ls, hs);
    generateFace(sides.LEFT, ls, hs);

    const options:any = {
        normals: normals,
        uvs: uvs,
        uvs1: uvs1,
        indices: indices
    };

    if (vtxIndex != vtxCount || idxIndex != idxCount)
        throw new Error();

    // ----------------------

    var royMesh:RoyMesh = RoyRoot.get().createMesh() as RoyMesh;
    var royMeshProvider:RoyMeshProvider = new RoyMeshProvider();
    royMesh.provider = royMeshProvider;
    royMesh.setVertexBufferAttribute(RoyEngine.VertexAttribute.POSITION, RoyEngine.VertexBuffer$AttributeType.FLOAT3);
    royMeshProvider.addVertexBufferData(positions)
        
    let meshAABB:AABB = new AABB();
    let tmpVec:Vec3 = new Vec3();
    let calAABB:boolean = false;

    royMesh.indexBufferDataType = RoyEngine.IndexBuffer$IndexType.UINT;
    royMeshProvider.setIndexBufferData(indices);

    for (let index = 0; index < indices.length; index ++)
    {
        let idxIndex:number = indices[index];
        tmpVec.set(positions[idxIndex * 3 + 0], positions[idxIndex * 3 + 1], positions[idxIndex * 3 + 2]);
        meshAABB.mergeVec3(tmpVec);
    }
    calAABB = true;
        
    royMesh.setVertexBufferAttribute(RoyEngine.VertexAttribute.NORMAL, RoyEngine.VertexBuffer$AttributeType.FLOAT3);
    royMesh.setVertexBufferAttribute(RoyEngine.VertexAttribute.UV0, RoyEngine.VertexBuffer$AttributeType.FLOAT2);
    royMesh.setVertexBufferAttribute(RoyEngine.VertexAttribute.UV1, RoyEngine.VertexBuffer$AttributeType.FLOAT2);

    royMeshProvider.addVertexBufferData(normals);
    royMeshProvider.addVertexBufferData(uvs);
    royMeshProvider.addVertexBufferData(uvs1);
    royMesh.syncToBuffer();
    
    if (!calAABB)
    {
        for (let index = 0; index < positions.length / 3; index ++)
        {
            tmpVec.set(positions[index * 3 + 0], positions[index * 3 + 1], positions[index * 3 + 2]);
            meshAABB.mergeVec3(tmpVec);
        }
    }

    royMesh.setAxisAlignBoundBox(meshAABB);

    // ----------------------

    let royMaterial:RoyMaterialInstance = RoyRoot.get().royMatTemplateMgr.createMatInstance(MatGltfLitOpaque.MatName);

    let sceneNode = this.m_scene.createSceneNode("TestMeshNode");
    this.m_scene.rootSceneNode.addChild(sceneNode);

    let meshComp:RoyMeshComponent = sceneNode.createComponent(RoyMeshComponent);
    meshComp.setMesh(royMesh, 1);
    meshComp.setMaterial(0, royMaterial);
    meshComp.setGeometryRange(0, RoyEngine.RenderableManager$PrimitiveType.TRIANGLES, 0, royMesh.getIndexBufferData().length);
}

上面的函数比较长,解释一下:

  • 主线是位于函数的末端,包括创建资源部分和创建运行时部分
    • 创建资源部分:包括royMesh(模型)、royMaterial(材质)
    • 创建运行时部分:创建sceneNode挂到场景根节点上,从sceneNode上创建royMeshComponent,并且把资源(royMesh和royMaterial)设置给royMeshComponent
    • 这样就完成了组合,运行时部分就是告诉引擎,用这个材质去显示这个模型,同时在告诉引擎一些参数如何显示,还有没有额外要求。
  • 从函数的开头,一直到接近函数末尾分割线,都是在手动创建一个mesh的过程:
    • 主要操作:创建顶点部分、创建索引部分、计算包围盒,然后同步给royMesh
    • 顶点部分:类型是Float32Array,包括Position、Normal、UV是最为常见的顶点属性方式,上述代码生成了这些数据
    • 索引部分:类型是Uint16Array或者Uint32Array,描述了顶点部分的顺序,形成了图元(图元往往是三角面)。
    • 包围盒计算:根据索引merge顶点,寻找最大的边界即是包围盒。
  • 在实际项目中,往往不会像上面那样来创建程序化的模型,一般是两种形态:
    • 利用第三方建模工具,如3DMax做好网格模型导出通用格式并加载进来,在加载器中把数据和roymesh对应起来
    • 利用几何引擎来生成程序化的模型,参数化建模,把几何引擎产出的数据和roymesh对应起来。几何引擎的引入可以参考libs/roysolid,它是对occt的一个再封装。

以上,从源头解释了RoyMesh的创建过程,虽然初学者会觉得RoyMesh复杂,但其实RoyMesh的数据是要送给GPU的,是GPU要求的模型结构就是这么复杂,RoyMesh已经是非常便于程序操作的一种封装,进一步阅读可以参考这里