Appearance
绘制一个自定义立方体
在第一个前端工程搭建好之后,我们可以开始创建一个立方体,来熟悉一个最基本的网格模型的渲染。
- 让我们回忆一下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已经是非常便于程序操作的一种封装,进一步阅读可以参考这里