存储多边形网格体
在渲染方面,您始终可以直接在程序的源代码中定义网格,但这是有限的。如果每次要渲染新模型的图像时都需要重新编译程序,那将是不切实际的。为了完整起见,我们仍将在本章中研究该选项。通常,模型数据永远不会存储在程序的代码中,而是存储在单独的文件中。大多数 3D 渲染程序都希望场景文件作为输入,读取该文件的内容,其中包含有关摄像机、灯光和场景中包含的几何体的信息,并将此信息传递给最终渲染该场景图像的引擎。在本章中,我们将了解多边形网格如何以三种最常见的文件格式存储:RIB(RenderMan)、OBJ 和 FBX。最后,我们将编写格式以方便将几何图形导入到我们的程序中。
在 C++ 中
如上所述,可以直接在程序的源代码中定义网格数据,尽管这不仅不切实际,而且通常也应该避免。它减慢了编译过程,并且可以在很大程度上增加程序文件(可执行文件)。但是,当您编写原型并且不想处理实现场景文件导入器时总是会遇到的复杂情况时,这可能会很方便。
我们已经在上一章中研究了这个代码。您需要做的就是声明一个人脸索引数组,其大小需要提前知道(第 1 行和第 2 行)。您还需要一个顶点索引数组(第 3 行),当然还有顶点数组本身(顶点在 3D 空间中的位置 - 第 4 行)。请注意,这两个数组的大小可以直接从人脸索引数组计算。顶点索引数组的大小可以通过将人脸索引数组的每个元素相加来计算(在立方体的情况下为 6 乘以 4 - 第 9 行)。顶点数组的大小可以通过找出人脸索引数组中的最大值加 1 来计算(不要忘记 C++ 中的所有数组都是从 0 开始的 - 第 15 行)。
uint32_t numFaces = 6;
int faceIndex[numFaces] = {4, 4, 4, 4, 4, 4};
int vertexIndex[24] = {0, 1, 2, 3, 0, 4, 5, 1, 1, 5, 6, 2, 0, 3, 7, 4, 5, 4, 7, 6, 2, 6, 7, 3};
Vec3f verts[8] = {{-1,1,1},{1,1,1},{1,1,-1},{-1,1,-1},{-1,-1,1},{1,-1,1},{1,-1,-1},{-1,-1,-1}};
Vec2f uvs[24] = {{0.375, 0},{0.625, 0} ... {0.125, 0.25}};
// first compute how many vertices we expect
uint32_t numVertices = 0;
for (int i = 0; i < numFaces; ++i) {
numVertices += faceIndex[i];
}
// find max size of the vertex array
uint32_t maxVertexIndex = 0;
for (int i = 0; i < numVertices; ++i) {
if (maxVertexIndex < vertexIndex[i]) maxVertexIndex = vertexIndex[i];
}
maxVertexIndex += 1;
// loop other all faces
int offset = 0;
for (int i = 0; i < numFaces; ++i) {
std::cerr << "Face: " << i << " has " << faceIndex[i] << " vertices\n";
for (int j = 0; j < faceIndex[i]; ++j) {
int vertIndex = vertexIndex[offset + j];
std::cerr << j << " vertex index: " << vertIndex << " pos: " << verts[verIndex] << "\n";
}
offset += faceIndex[i];
}RIB(渲染曼)
RIB 代表 RenderMan Interface Bytestream(RIB 文件的扩展名为 .rib)。它是皮克斯用来将场景数据传递给其渲染器的格式(目前在生产中使用的渲染器称为 RIS。在它之前是 Prman,许多著名的皮克斯电影都是用它制作的:《玩具总动员》、《海底总动员》、《怪兽公司》、《瓦力》等。RIB 文件可以以二进制或 ASCII 格式存储。这是我们的立方体在 RIB 文件中的样子:
PointsPolygons [4 4 4 4 4 4] [0 1 3 2 2 3 5 4 4 5 7 6 6 7 1 0 1 7 5 3 6 0 2 4] "P" [-0.5 -0.5 0.5 0.5 -0.5 0.5 -0.5 0.5 0.5 0.5 0.5 0.5 -0.5 0.5 -0.5 0.5 0.5 -0.5 -0.5 -0.5 -0.5 0.5 -0.5 -0.5] "facevarying normal N" [0 0 1 0 0 1 0 0 1 0 0 1 0 1 0 0 1 0 0 1 0 0 1 0 0 0 -1 0 0 -1 0 0 -1 0 0 -1 0 -1 0 0 -1 0 0 -1 0 0 -1 0 1 0 0 1 0 0 1 0 0 1 0 0 -1 0 0 -1 0 0 -1 0 0 -1 0 0] "facevarying float s" [0.375 0.625 0.625 0.375 0.375 0.625 0.625 0.375 0.375 0.625 0.625 0.375 0.375 0.625 0.625 0.375 0.625 0.875 0.875 0.625 0.125 0.375 0.375 0.125] "facevarying float t" [0 0 0.25 0.25 0.25 0.25 0.5 0.5 0.5 0.5 0.75 0.75 0.75 0.75 1 1 0 0 0.25 0.25 0 0 0.25 0.25] "constant string primtype" ["mesh"]我们已经在上一章中讨论过这种格式。声明以请求开头。第一个数组是人脸索引数组。它后面是顶点索引数组。后面的每个数组都定义了一个原始变量。第一个“P”是必需的(它包含网格顶点的位置)。所有其他(在本例中为 N 和 st)都是可选的。PointsPolygons
图 1:立方体 uv 的布局。
请注意,在这种格式中,法线和纹理坐标数组的大小与顶点索引数组相同。数组中的法线和纹理坐标数等于所有面的顶点数之和。对于立方体,此数字为 24(立方体中有 6 个面,每个面有 4 个顶点)。不过,对于纹理坐标,您可以通过查看图 1 看到,那么只需要 14 个坐标(这取决于您布局立方体的 uv 的方式。您也可以在 uv 空间中拆分面,在这种情况下,您需要 24 个纹理坐标)。声明 24 个纹理坐标而不是仅 14 个并不是最佳选择,尽管声明网格每个面的每个顶点的 uv 坐标使我们不必使用纹理坐标索引数组。检查 OBJ 或 FBX 文件格式,查看纹理或普通索引数组的示例。
OBJ 格式
OBJ 文件格式是一种非常古老但常见的文件格式,最初由 Wavefront 开发(OBJ 文件的扩展名为 .obj)。与 RIB 接口或其他格式(如 FBX)不同,OBJ 仅存储几何数据(不存储其他场景数据,如灯光或摄像机)。许多不同的几何体类型和几何体数据都可以以这种格式存储(例如有关对象材质的信息),不过,在本课的上下文中,我们将仅研究如何在 OBJ 文件中定义多边形网格体。下面是我们的立方体在这种格式下的样子:
This file uses centimeters as units for non-parametric coordinates.
v -0.500000 -0.500000 0.500000
v 0.500000 -0.500000 0.500000
v -0.500000 0.500000 0.500000
v 0.500000 0.500000 0.500000
v -0.500000 0.500000 -0.500000
v 0.500000 0.500000 -0.500000
v -0.500000 -0.500000 -0.500000
v 0.500000 -0.500000 -0.500000
vt 0.375000 0.000000
vt 0.625000 0.000000
vt 0.375000 0.250000
vt 0.625000 0.250000
vt 0.375000 0.500000
vt 0.625000 0.500000
vt 0.375000 0.750000
vt 0.625000 0.750000
vt 0.375000 1.000000
vt 0.625000 1.000000
vt 0.875000 0.000000
vt 0.875000 0.250000
vt 0.125000 0.000000
vt 0.125000 0.250000
vn 0.000000 0.000000 1.000000
vn 0.000000 0.000000 1.000000
vn 0.000000 0.000000 1.000000
vn 0.000000 0.000000 1.000000
vn 0.000000 1.000000 0.000000
vn 0.000000 1.000000 0.000000
vn 0.000000 1.000000 0.000000
vn 0.000000 1.000000 0.000000
vn 0.000000 0.000000 -1.000000
vn 0.000000 0.000000 -1.000000
vn 0.000000 0.000000 -1.000000
vn 0.000000 0.000000 -1.000000
vn 0.000000 -1.000000 0.000000
vn 0.000000 -1.000000 0.000000
vn 0.000000 -1.000000 0.000000
vn 0.000000 -1.000000 0.000000
vn 1.000000 0.000000 0.000000
vn 1.000000 0.000000 0.000000
vn 1.000000 0.000000 0.000000
vn 1.000000 0.000000 0.000000
vn -1.000000 0.000000 0.000000
vn -1.000000 0.000000 0.000000
vn -1.000000 0.000000 0.000000
vn -1.000000 0.000000 0.000000
f 1/1/1 2/2/2 4/4/3 3/3/4
f 3/3/5 4/4/6 6/6/7 5/5/8
f 5/5/9 6/6/10 8/8/11 7/7/12
f 7/7/13 8/8/14 2/10/15 1/9/16
f 2/2/17 8/11/18 6/12/19 4/4/20
f 7/13/21 1/1/22 3/3/23 5/14/24文件中的每一行都以一个字母开头,该字母表示以下数字所表示的数据类型。如果该行以字母开头,则接下来的 3 个数字声明顶点的位置。网格中顶点的索引由其在文件中的位置定义。如果该行以字母开头,则接下来的 2 个数字定义纹理坐标。这些字母表示接下来的 3 个数字定义了法线。最后,该字母定义了人脸声明。它后面是至少 3 组,每组三个数字,用“/”分隔。第一个数字定义顶点数组中顶点的索引(v)。第二个数字定义纹理坐标数组中顶点纹理坐标的索引 (vt) 。第三个数字定义了法线数组中顶点法线的索引(vn)。在 OBJ 文件格式中,数组是从 1 开始的(数组中的第一个元素具有索引 1)。在上面的示例中,立方体的第一个面由顶点数组中的第一个、第二个、第四个和第三个顶点定义(1/1/1 2/2/2 4/4/3 3/3/4)。面有 4 个顶点。该面的第一个顶点使用纹理坐标数组中的第一个纹理坐标,第二个顶点使用数组中的第二个纹理坐标,第三个顶点使用第四个纹理坐标,依此类推 (1/1/1 2/2/2 4/4/3 3/3/4) 。同样,对于法线,第一个顶点使用法线数组中的第一个法线,第二个顶点使用第二个法线,第三个顶点使用第三个法线,依此类推(1/1/1 2/2/2 4/4/3 3/3/4vvtvnf)
请注意,在这种格式(以及 FBX 格式)中,纹理坐标数组中顶点的索引是面定义的一部分。只有 14 个纹理坐标导出到 OBJ 文件,但对于立方体的每个顶点,我们现在需要定义该数组中顶点的索引。同样的事情也适用于法线。
FBX的
FBX 是 Autodesk 专有的文件格式(FBX 文件的扩展名为 .fbx)。它用于促进 Autodesk 开发的应用程序(例如 Maya 和 3DSMax)之间的场景数据传输。它不是一种开放的文件格式,并且该格式的规范并不正式存在,但如果以 ASCII 存储,则可以查看 FBX 文件的内容。FBX 文件格式作为 3D 应用程序之间的互换文件格式非常流行,可以描述构成场景的大多数元素(几何体、灯光、材质、相机、材质分配、图层等)。这是我们的立方体在 FBX 中的样子:
; FBX 7.4.0 project file
; Copyright (C) 1997-2010 Autodesk Inc. and/or its licensors.
; All rights reserved.
; ----------------------------------------------------
Objects: {
Geometry: 140208557167936, "Geometry::", "Mesh" {
Vertices: *24 {
a: -0.5,-0.5,0.5,0.5,-0.5,0.5,-0.5,0.5,0.5,0.5,0.5,0.5,-0.5,0.5,-0.5,0.5,0.5,-0.5,-0.5,-0.5,-0.5,0.5,-0.5,-0.5
}
PolygonVertexIndex: *24 {
a: 0,1,3,-3,2,3,5,-5,4,5,7,-7,6,7,1,-1,1,7,5,-4,6,0,2,-5
}
Edges: *12 {
a: 0,2,6,10,3,1,7,5,11,9,15,13
}
GeometryVersion: 124
LayerElementNormal: 0 {
Normals: *72 {
a: 0,0,1,0,0,1,0,0,1,0,0,1,0,1,0,0,1,0,0,1,0,0,1,0,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,-1,0,0,-1,0,0,-1,0,0,-1,0,1,0,0,1,0,0,1,0,0,1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0
}
NormalsW: *24 {
a: 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
}
}
LayerElementUV: 0 {
UV: *28 {
a: 0.375,0,0.625,0,0.375,0.25,0.625,0.25,0.375,0.5,0.625,0.5,0.375,0.75,0.625,0.75,0.375,1,0.625,1,0.875,0,0.875,0.25,0.125,0,0.125,0.25
}
UVIndex: *24 {
a: 0,1,3,2,2,3,5,4,4,5,7,6,6,7,9,8,1,10,11,3,12,0,2,13
}
}
}
}在FBX格式中,人脸索引和顶点索引数组组合在一个数组中。如果您查看上面示例中调用的数组,您会注意到它包含一些负值。这些负值用于指示面的末端。例如,在上面的示例中,第一个面有 4 个顶点(0、1、3、-3)。要找到第四个顶点的索引,只需将索引值乘以 -1 并从结果中删除 1。例如:PolygonVertexIndex
-3 * - 1 - 1 = 2因此,第一个面由顶点数组中索引为 0、1、3 和 2 的顶点组成。第二个面也有 4 个顶点。它们在顶点数组中的索引是 2、3、5 和 4 (-5 * -1 - 1 = 4),依此类推。您需要解析整个数组才能知道网格包含多少个面。PolygonVertexIndex
其他格式,如 USD 和 glTF 呢?
glTF 格式在视频游戏行业尤为常见。它是一种轻量级格式,其规格由 Khronos Group 开发和维护。我们现在不会过多地讨论它,但将来会写一个关于它的教训。与FBX一样,它是一种相当多价/通用的格式,支持几何体、纹理、相机和光存储/定义。它有 ASCII 和二进制形式。
其他更新的形式正在出现,例如美元。USD 代表通用场景描述(一个雄心勃勃的目标)。该格式最初由皮克斯开发,以满足其内部制作需求(2013 年之前)。USD 不是一种格式,因为它带有您可以遵循的规范来读取和写入与 USD 兼容的文件。当我们谈论 USD 时,我们更指皮克斯在 2016 年作为开源项目发布的代码。该代码提供了框架以及一些辅助工具,允许您读取和写入 USD 文件,以及将以 USD 格式存储的数据公开给渲染器(通过名为 Hydra 的接口)。USD 代码库相当令人生畏,USD 也是一种相当复杂的格式。它的主要目标是简化来自各种来源的数据的聚合,并通过一种称为组合的机制将它们组合成场景数据。它支持许多功能,例如分层、参数覆盖、类继承、变体、有效负载等等。这是一种相当复杂和压倒性的格式,我们将在以后的课程中写到。与此同时,如果您有兴趣开始阅读有关该格式的更多信息