在3D物体中,所有的几何变换,几何检测,动画,渲染着色都要基于三角网格进行,所以三角网格的基本原理和优化策略都是是非常重要的。
一、三角网格的存储结构
三角形网格是为了模拟一个物体连续的体积表面,三角形网格具有表示简单和操作简单的特性而成为事实上表示物体平滑表面的标准,其它多边形网格都可以简化为三角形网格,
三角形需要表现三角形网格的顶点、边、面的信息。
1.索引三角网格
索引三角网格是三角网格的标准存储形式。
索引三角网格的一般定义:
struct Vertex
{
// 顶点
Vector3 p;
// UV纹理,用于渲染
float u,v;
// 顶点法向量,用于计算光照,求取遇到问题可能需要面拆分
Vector3 normal;
// 工具变量,用于对顶点操作的标记
int mark;
}
struct Triangle
{
// 记录的是索引
struct VertIndex
{
// 存储一个索引,比存储vector3少得多
int index;
// 焊接和顶点拆分时候,顶点中的uv会失效,所以在三角形中也备份一份;且三角形上的拷贝直接从三角形取得UV坐标即可
float u, v;
}
// 三角形的三个顶点
VertIndex v[3];
// 面法向量
Vector3 normal;
// 三角形所属空间物体的那个部分
int part;
// 材质列表的索引
int material;
// 工具变量,用于对三角形操作的标记
int mark;
}
struct Material
{
// 贴图纹理的名字
char diffuseTextureName[256];
// 标记,用于Material的操作标记
int mark;
}
struct Part
{
// 三角形所属物体中的部分的名字
char name[256];
// 标记
int mark;
}
// 一个三角网格定义了一个物体平滑的空间表面,或者多个物体组合为一个物体的空间表面
class EditTriMesh
{
public:
private:
// 顶点分配的个数,顶点需要预分配一些空间给顶点的面拆分,点拆分使用,避免每次都new一个顶点出来
// 预分配的大小为: vAlloc =
vCount
* 4 / 3 + 10;
int vAlloc;
// 顶点个数
int vCount;
// 顶点列表
Vertex *vList;
// 三角形分配的个数,三角形需要预分配一些空间给面拆分,点拆分使用,避免每次都new一个面结构体出来
// tAlloc =
tCount
* 4 / 3 + 10;
int tAlloc;
int tCount;
Triangle *tList;
// 材质纹理的列表
int mCount;
Material *mList;
// 所属物体部分的列表
int pCount;
Part *pList;
}
这种简单的索引三角网格,并没有给出三角形的邻接信息,每次获取邻接信息都要遍历整个网格的三角形列表。另一种反映网格边邻接信息的存储方式是,
将三角形用边的列表来定义,顶点中维护一个共用该顶点的所有边的索引,这样通过定位顶点,可以在常数时间内找到和该点相关的边和三角形。
2.三角带网格
三角带存储,因为顶点顺序就包含了三角形索引信息,所以不需要存储三角形的顶点索引,且提交的顶点数比顶点索引存储更少,但是只在一些平台上使用,例如PS平台上。
三角带是用t + 2s个顶点,存储了t个三角形,s是三角带的个数,所以尽量减少三角带数量,用退化三角形连接多个三角带是通用的做法,多个三角带存在主要是表现一个物体有时候不能用一个三角带表示完,用退化连接主要是建立多个三角带是需要较长时间的。因为三角带中的三角形提交顺序是一个顺时针交替一个逆时针的,所以因为绕序问题需要四个退化的三角形,或者在指明退化三角形不用渲染情况下用2个退化三角形。
3.索引三角网格较三角带网格使用广泛
索引三角网格,给图形卡提交的三角网格物体的顶点数,几乎一个三角形只需要提交一个顶点,和三角带网格一样,但是索引三角网格比较复杂,因为
三角形需要维护
顶点的索引信息,增删查改都比较复杂,而且需要比较多的空间,不过因为并不是所有的平台都支持三角网格,所以
索引三角网格是比较通用而普遍的定义三角网格的存储格式
。
4.三角网格的优化
索引三角网格和三角带网格都有显卡顶点缓存优化策略,可以使得三角网格提交的一个三角形少于一个顶点。
这个优化是显卡硬件层面的优化策略,工作原理是API传输给显卡先会缓存显卡中的顶点缓存,去哈希查询下如果显卡中存在该顶点那么"命中"不用发送告诉显卡使用显卡顶点缓存的xx位置的顶点即可,
如果没有那么“脱靶”传送顶点给显卡。
想要更好的使用显卡顶点缓存优化渲染,那么需要对三角网格优化排序下,使得共用顶点的三角形放置在相邻的位置,这些三角形连续发送时候就可以提高顶点的命中率。
二、三角网格存储的额外信息
纹理坐标用于渲染,顶点法向量用于光照计算,表面法向量用于相交检测或背面消隐。
1.纹理坐标
对于用纹理着色的多边形,每个顶点都要存放纹理坐标用这些坐标索引纹理图从而为相应像素着色,
一个三角形表面需要三个UV坐标指明纹理位置进行定位映射,顶点进行变换缩放时候纹理坐标不用进行变换,
真正渲染着色的时候需要对三角形中的每个像素进行纹理坐标插值算法求得该像素的纹理坐标,将纹理坐标的像素拷贝到空间点上。
2.表面和顶点法向量
表面法向量作用:
1)计算光照;
2)进行背面剔除;
3)模拟粒子在表面的“弹跳"效果;
4)通过只考虑正面而加速碰撞检测。
表面法向量可能保存于三角形级或顶点级,或者两者都有。
对于三角形级法向量用三角形的边向量叉乘就可以了,假设三角形是顺时针列出的。
如果是顶点级的法向量(计算光照),可以求得共用该顶点的三角形,求取三角形的法向量(假设三角形是顺时针列举出来的),然后进行平均即可。但是会遇到"公告板"问题,共用一个顶点的三角形是两个相反的三角形;或者会遇到立方体边缘着色时候用Gouraud着色导致的边缘没有剧烈变化而模糊的问题;
这两种情况下都可以通过平面拆分来获得,使得他们不是共享该顶点的面,因此平均后就没有问题了,如果没有剧烈变化的平面也没有面拆分,会导致平面法向量接近的获得较多的”发言权“,但是因为三角表面本来就是一种近似,所以不会导致什么问题。
计算光照时候顶点的颜色可以直接计算出来,三角形中的其它点的颜色也需要插值算法来计算,这样的计算量消耗是巨大的,所以比较少使用实时动态计算光照。
3.光照值
光照值用于沿表面的插值,典型的方法是用Gouraud着色。有些时候,顶点处保存顶点法向量,用于渲染时候动态计算光照值。但另一些情况下我们需要自己指定光照值。
三、三角网格的重要操作
同拓扑的三角网格,也就是位置不同,大小不同,伸缩不同,但是网格形状是一致的网格。
有一种重要的网格就是封闭网格,物体表面的网格基本是封闭网格,导致网格不封闭的异常原因有:
1)孤立的顶点。
2)重复的顶点。
3)退化的三角形。
4)开放边,仅是一个三角形使用。
5)超过两个三角形共享的边。
6)重复面。
根据应用的不同,上面异常导致的问题,可能是大错误,或者是小错误,这个时候需要进行三角形网格纠正;或者无关紧要,那么不需要进行网格修复。
1.逐片操作:渲染提交和顶点变换
三角形网格的一系列基本操作都是逐三角形或逐点应用基本操作的结果,例如
渲染时需对逐个三角形进行提交;转换时如变换和缩放时需对逐个顶点进行操作。三角形渲染和顶点变换是对三角网格进行最多最广泛的操作。
2.基本优化操作:点焊接,面拆分
(1)点焊接
对于在误差范围内的点进行焊接,保留一个顶点,删除一个顶点。这样使得顶点减少了,那么需要存储的内存变少了;对三角网格的逐片操作(例如变换和渲染)速度会加快;且三角网格中几何相邻的边在逻辑上也是相邻的。
点焊接的基本操作:
1)去掉三角网格中孤立的点。
2)当两个顶点均来自于细长的三角形,那么直接焊接可能会产生退化三角形,这个时候应该去掉整个细长的面。
3 ) 焊接时候,如果不是细长的三角形,那么只简单的去掉一个顶点(例如去掉高序数的或者去掉低序数的)。
点焊接不是网格削减,即不用大规模去掉三角形,而是尽量保持网格的外形和精度。
点焊接会导致一个问题,即
被焊接顶点中的顶点法向量,纹理坐标消失了,会导致不连续,因此这个时候权衡下使用三角形中保存的顶点法向量和纹理坐标,还是使用焊接到的顶点的法向量或纹理坐标即可。
点焊机算法是O(n^2)的,因为查找需要n搜索,调整三角形列表和顶点列表也是n搜索遍历。但是加以思考就可以找到O(n)算法效率,具体需要分析下已有的算法。
(2)面拆分
面拆分的作用,在"公告板”中,或者立方体变化剧烈的情况下,为了正确的计算顶点法向量。或者其它类似情况下需要进行面拆分。
面拆分是点焊接相反的算法,使得顶点增加了,面增加了,使得拓扑间断了。不过这也正是我们想要的,就是逻辑间断的地方拓扑(几何)也是间断的。一般情况下面拆分了几何并没有间断位置还是那个位置,大小也是一样的大小,只是我们保持了两份数据,当做两个顶点来使用。
面拆分时候,产生新的顶点,它的顶点法向量,UV纹理坐标,可以来自于三角形中或者就是原来的顶点拷贝一份。
3.高级优化操作:边坍塌网格削减,点拆分LOD渐进式网格
(1)边坍塌网格削减
边坍塌是将边缩减为顶点的方法,与之对应的是顶点拆分。边坍塌使边上 的两个顶点变为一个,共享该边的两个三角形消失。
边坍塌常用于网格削减,边坍塌可以有效的减少顶点和三角形的数量。
这样使得摄像机远处的地面,建筑,物体,人物变得模糊。
网格削减是将顶点和三角形较多的网格简化为顶点和三角形较少的网格,并且要求网格外观和主要顶点尽量保持不变。使用
边坍塌就可以实现网格削减的方法,但是选择需要坍塌的边相对耗时(甚至需要进行离线计算), 但是看使用的启发算法和网格削减精度来决定,边坍塌本身的网格操作是很简单的。
边坍塌舍弃边上的两个顶点,产生新的顶点,它的顶点法向量,UV纹理坐标,可以来自于三角形中或者就是原来某个顶点的一份拷贝。
(2)点拆分LOD渐进式网格
Hope的论文讲述了如何用
点拆分来反演边坍塌的过程,用此反演方法生成的网格称为渐进式(LOD)网格
。
大概是要先存放边坍塌过程中,选择的坍塌边信息,顶点信息。然后根据当前的网格形态和坍塌过程中的信息,通过相反的算法输入坍塌信息(从上到下,或者从下到上的顺序),将点拆分,逐步的构建反演的三角网格,来实现LOD网格。
这样使得摄像机近处的地面,建筑,物体,人物变得细腻而逼真。
点拆分产生新的顶点,它的顶点法向量,UV纹理坐标,可以来自于三角形中或者就是原来某个顶点的一份
拷贝,更准确的是来自于边坍塌过程中产生的顶点信息中。
四、索引三角网格代码分析
1.预分配内存和整理内存(非频繁申请和释放),提高索引三角网格的性能
预分配内存大小是: nAllocCount = int( nCount * 4 / 3 + 10 );
vList = (Vertex *)::realloc(vList,
nAllocCount
* sizeof(*vList));
线性空间,对网格操作时候不是真正的删掉内存,而是挪动内存:
realloc函数的应用, memset函数,memcpy函数,memmove函数的应用。
// 没有真正释放内存,而是使用了memmove移动顶点位置
memmove(&vList[vertexIndex], &vList[vertexIndex+1], (vCount - vertexIndex) * sizeof(*vList));
2.每次删除顶点或者材质索引或部分索引时候都要重新整理三角形列表
//---------------------------------------------------------------------------
// EditTriMesh::deleteVertex
//
// Deletes one vertex from the vertex list. This will fixup vertex
// indices in the triangles, and also delete triangles that referenced
// that vertex
void EditTriMesh::deleteVertex(int vertexIndex) {
// Check index. Warn in debug build, don't crash release
if ((vertexIndex < 0) || (vertexIndex >= vertexCount())) {
assert(false);
return;
}
// Scan triangle list and fixup vertex indices
for (int i = 0 ; i < triCount() ; ++i) {
Tri *t = &tri(i);
// Assume it won't get deleted
t->mark = 0;
// Fixup vertex indices on this face
for (int j = 0 ; j < 3 ; ++j) {
// Do we need to delete it?
// 需要删掉的顶点,很多时候不止一个三角形,如果是删掉的那么也没有fixup的必要了的
if (t->v[j].index == vertexIndex) {
t->mark = 1;
break;
}
// Fixup vertex index?
// 不是要删除的,因为删除了该顶点,后面的顶点都要相应的减1了,否则会不准确或失效
if (t->v[j].index > vertexIndex) {
--t->v[j].index;
}
}
}
// Delete the vertex from the vertex array
--vCount;
// vertexIndex是从0开始的顶点索引,--vCount已经将个数减了vCount - vertexIndex得到正确移动的个数
// 没有真正释放内存,而是使用了memmove移动顶点位置
memmove(&vList[vertexIndex], &vList[vertexIndex+1], (vCount - vertexIndex) * sizeof(*vList));
// Delete the triangles that used it
deleteMarkedTris(1);
}
//---------------------------------------------------------------------------
// EditTriMesh::setVertexCount
//
// Set the vertex count. If the list is grown, the new vertices at the end
// are initialized with default values. If the list is shrunk, any invalid
// faces are deleted.
void EditTriMesh::setVertexCount(int vc)
{
assert(vc >= 0);
// Make sure we had enough allocated coming in
assert(vCount <= vAlloc);
// Check if growing or shrinking the list
if (vc > vCount) {
// Check if we need to allocate more
if (vc > vAlloc) {
// We need to grow the list. Figure out the
// new count. We don't want to constantly be
// allocating memory every time a single vertex
// is added, but yet we don't want to allocate
// too much memory and be wasteful. The system
// shown below seems to be a good compromise.
vAlloc = vc * 4 / 3 + 10; // 预分配内存大小
vList = (Vertex *)::realloc(vList, vAlloc * sizeof(*vList));
// Check for out of memory. You may need more
// robust error handling...
if (vList == NULL) {
ABORT("Out of memory");
}
}
// Initilaize the new vertices
while (vCount < vc) {
vList[vCount].setDefaults();
++vCount;
}
} else if (vc < vCount) {
// Shrinking the list. Go through
// and mark invalid faces for deletion
// 删除大于指定索引顶点的
for (int i = 0 ; i < triCount() ; ++i) {
Tri *t = &tri(i);
if (
(t->v[0].index >= vc) ||
(t->v[1].index >= vc) ||
(t->v[2].index >= vc)
) {
// Mark it for deletion
t->mark = 1;
} else {
// It's OK
t->mark = 0;
}
}
// Delete the marked triangles
deleteMarkedTris(1);
// Set the new count. Any extra memory is
// wasted for now...
vCount = vc;
}
}
void EditTriMesh::deleteMarkedTris(int mark) {
// Scan triangle list, and move triangles forward to
// suck up the "holes" left by deleted triangles
int destTriIndex = 0;
for (int i = 0 ; i < triCount() ; ++i) {
const Tri *t = &tri(i);
// Is it staying?
if (t->mark != mark) {
// 下标不相同的,挪动到前面来,并没有真正的删除内存,这个是为了性能考虑很好的做法
if (destTriIndex != i) {
tri(destTriIndex) = *t;
}
++destTriIndex;
}
}
// Set new triangle count
setTriCount(destTriIndex);
}
//---------------------------------------------------------------------------
// EditTriMesh::deleteTri
//
// Deletes one triangle from the triangle list.
void EditTriMesh::deleteTri(int triIndex) {
// Check index. Warn in debug build, don't crash release
if ((triIndex < 0) || (triIndex >= triCount())) {
assert(false);
return;
}
// Delete it
--tCount;
// 因为是线性容器,三角形级别的只需要考虑自己就可以了,直接挪动后面的到前面来
memmove(&tList[triIndex], &tList[triIndex+1], (tCount - triIndex) * sizeof(*tList));
}
3.删掉退化的三角形
// Return true if we are degenerate (any two vertex indices are the same)
// 判断是退化的
bool EditTriMesh::Tri::isDegenerate() const {
return
(v[0].index == v[1].index) ||
(v[1].index == v[2].index) ||
(v[0].index == v[2].index);
}
// 遍历三角网格将退化的删掉
void EditTriMesh::deleteDegenerateTris() {
// Scan triangle list, marking the bad ones
for (int i = 0 ; i < triCount() ; ++i) {
Tri *t = &tri(i);
// Is it bogus?
if (t->isDegenerate()) {
// Mark it to be whacked
t->mark = 1;
} else {
// Keep it
t->mark = 0;
}
}
// Delete the bad triangles that we found
deleteMarkedTris(1);
}
4.点复制放到顶点的末尾用于三角形中的面拆分
//---------------------------------------------------------------------------
// EditTriMesh::dupVertex
//
// Add a duplicate of a vertex to the end of the list.
//
// Notice that we do NOT make the mistake of coding this simply as:
//
// return
addVertex(vertex(srcVertexIndex)) // 不使用容器里面的元素引用,因为容器会改变导致元素失效
//
// Because the vertices may shift in memory as the lists are reallocated,
// and our reference will be invalid.
int EditTriMesh::dupVertex(int srcVertexIndex) {
// Fetch index of the new one we will add
int r = vCount;
// Check if we have to allocate
if (vCount >= vAlloc) {
// Need to actually allocate memory - use the other function
// to do the hard work
setVertexCount(vCount+1);
} else {
// No need for heavy duty work - we can
// do it quickly here
++vCount;
// No need to default - we are about to assign it
}
// Make the copy
vList[r] = vList[srcVertexIndex];
// Return index of new vertex
return r;
}
5.通过三角形链表整理材质链表
// 因为删掉三角形时候,没有删掉顶点,材质,或者部分的列表,其实也是需要逆向处理下的,不处理也不会有什么问题。
// 只是顶点,材质,部分的列表需要定期重新整理下
//---------------------------------------------------------------------------
// EditTriMesh::deleteUnusedMaterials
//
// Scan list of materials and delete any that are not used by any triangles
//
// This method may seem a little more complicated, but it operates
// in linear time with respect to the number of triangles.
// Other methods will run in quadratic time or worse.
// 通过当前的三角形,逆向的整理一遍材质链表,材质链表有删除,三角形也需要重新整理一遍,且材质链表也要重新排序一遍
void EditTriMesh::deleteUnusedMaterials() {
int i;
// Assume all materials will be unused
markAllMaterials(0);
// Scan triangle list and mark referenced materials
for (i = 0 ; i < triCount() ; ++i) {
material(tri(i).material).mark = 1;
}
// OK, figure out how many materials there will be,
// and where they will go int he new material list,
// after the unused ones are removed
int newMaterialCount = 0;
for (i = 0 ; i < materialCount() ; ++i) {
Material *m = &material(i);
// Was it used?
if (m->mark == 0) {
// No - mark it to be whacked
m->mark = -1;
} else {
// Yes - it will occupy the next slot in the
// new material list
m->mark = newMaterialCount;
++newMaterialCount;
}
}
// Check if nothing got deleted, then don't bother with the
// rest of this
if (newMaterialCount == materialCount()) {
return;
}
// Fixup indices in the face list
// 因为内部有材质需要删除,那么原来的三角形中的材质索引就要修正了
// 当前使用的材质索引都通过mList[materialIndex].mark索引了一遍,通过mList.mark赋值给三角形,mList并没有改变。
// 但是后面mList会重新排序,所以提前使用的mList[materialIndex].mark是正确的。
for (i = 0 ; i < triCount() ; ++i) {
Tri *t = &tri(i);
t->material = material(t->material).mark;
}
// Remove the empty spaces from the material list
int destMaterialIndex = 0;
for (i = 0 ; i < materialCount() ; ++i) {
const Material *m = &material(i);
// This one staying?
// 将mList从新排序整理一遍,挪开空的到链表末尾
if (m->mark != -1) {
assert(m->mark == destMaterialIndex);
if (i != destMaterialIndex) {
material(destMaterialIndex) = *m;
}
++destMaterialIndex;
}
}
assert(destMaterialIndex == newMaterialCount);
// Set the new count. We don't call the function to
// do this, since it will scan for triangles that use the
// whacked entries. We already took care of that.
mCount = newMaterialCount;
}
6.从当前三角网格中构建指定部分的三角网格
// part部分是在三角形网格下的,属于人物的head, hand, body,foot类型,对三角形小集合进行了分类。而所有part的三角形小集// 合组成了整个三角形网格。如果有需要也是可以对部分中指定的三角形小集合生成一个单独的网格对象的。
// Extract parts
// 提取部分,提取每个部分到独立的网格中,每个网格精确的含有一个部分
// 传入一个以部分为索引的网格列表,对当前三角列表提取属于这个部分(目标网格对象)的三角形列表,对这些三角形列表中的材质顶点三角形
// 都重新构建一份赋值给目标网格对象,部分就是原三角形列表中的部分。
void extractParts(EditTriMesh *meshes);
// 传入一个指定部分指定材质,在当前三角形列表中筛选出三角形列表2,对三角形列表2中的顶点、三角形重新构建一份,
// 赋值给目标网格列表,目标网格的材质就是指定的材质,部分就是指定的部分。
void extractOnePartOneMaterial(int partIndex, int materialIndex, EditTriMesh *result);
//---------------------------------------------------------------------------
// EditTriMesh::extractParts
//
// Extract each part into a seperate mesh. Each resulting mesh will
// have exactly one part
void EditTriMesh::extractParts(EditTriMesh *meshes) {
// !SPEED! This function will run in O(partCount * triCount).
// We could optimize it somewhat by having the triangles sorted by
// part. However, any real optimization would be considerably
// more complicated. Let's just keep it simple.
// Scan through each part
for (int partIndex = 0 ; partIndex < partCount() ; ++partIndex) {
// Get shortcut to destination mesh
// 一个网格对象里面有关于party的数据,这些部分用于划分多个网格组成的链表
// 对于每个party的网格里面的元素进行操作
EditTriMesh *dMesh = &meshes[partIndex];
// Mark all vertices and materials, assuming they will
// not be used by this part
// 这个网格对象中的所有顶点和材质数据都标记空,假设该部分的网格都不使用这些
markAllVertices(-1);
markAllMaterials(-1);
// Setup the destination part mesh with a single part
// 对目标网格清空
dMesh->empty();
// 对目标网格设置一个party,多余的该目标网格内部的三角形都会被清理
dMesh->setPartCount(1);
// 目标网格的第零个party数据,用当前大网格的party顺序索引上的部分元素赋值
dMesh->part(0) = part(partIndex);
// Convert face list, simultaneously building material and
// vertex list
// 遍历当前大网格,选取party partIndex和当前的party相关的三角形,转换表面列表,同时建立材质和三角形链表
for (int faceIndex = 0 ; faceIndex < triCount() ; ++faceIndex) {
// Fetch shortcut, make sure it belongs to this
// part
Tri *tPtr = &tri(faceIndex);
if (tPtr->part != partIndex) {
continue;
}
// Make a copy
Tri t = *tPtr;
// Remap material index
Material *m = &material(t.material);
// 前面已经设置了小于,且同一个party或者不同party之间没有被之前的使用过,那么拷贝一个到目标网格
if (m->mark < 0) {
m->mark = dMesh->addMaterial(*m);
}
// 目标网格中的材质是之前那个party的mark,或者自身party前元素的mark,或者刚拷贝标记的mark.
t.material = m->mark;
// Remap vertices
for (int j = 0 ; j < 3 ; ++j) {
Vertex *v = &vertex(t.v[j].index);
// 三角形中的所有顶点,如果没有被使用过,那么拷贝一个顶点建立顶点列表且赋予mark值,该mark值是目标网格中的索引
if (v->mark < 0) {
v->mark = dMesh->addVertex(*v);
}
// 当前三角形顶点的索引赋予mark值,因为该mark来自于一个party中的顶点列表中的索引,所以多个party中的mark是很有可能重复的。
// 这里party中的三角形的顶点索引是可能乱序出现问题的?虽然t是拷贝的,但是v->mark的值是会重复的。
// 且因为t.v[j].index是可重复的,所以会导致问题。如果部分之间t.v[j].index是不会重复的,且就算部分之间重复
// 但是每个部分开始都用了markAllVertices(-1),v->mark都是本部分新生成的顶点的索引,故不会有问题。
t.v[j].index = v->mark;
}
// Add the face
t.part = 0;
dMesh->addTri(t);
}
}
}
void EditTriMesh::extractOnePartOneMaterial(int partIndex, int materialIndex, EditTriMesh *result) {
// Mark all vertices, assuming they will not be used
markAllVertices(-1);
// Setup the destination mesh with a single part and material
result->empty();
result->setPartCount(1);
result->part(0) = part(partIndex);
result->setMaterialCount(1);
result->material(0) = material(materialIndex);
// Convert face list, simultaneously building vertex list
for (int faceIndex = 0 ; faceIndex < triCount() ; ++faceIndex) {
// Fetch shortcut, make sure it belongs to this
// part and uses this material
Tri *tPtr = &tri(faceIndex);
if (tPtr->part != partIndex) {
continue;
}
if (tPtr->material != materialIndex) {
continue;
}
// Make a copy
Tri t = *tPtr;
// Remap vertices
for (int j = 0 ; j < 3 ; ++j) {
Vertex *v = &vertex(t.v[j].index);
// 因为v->mark都是新生成的result中的索引,所以t.v[j].index = v->mark是新的顶点中的索引,是没有问题的
// 能够得到一个指定部分指定材质的三角形网格列表
if (v->mark < 0) {
v->mark = result->addVertex(*v);
}
t.v[j].index = v->mark;
}
// Add the face
t.part = 0;
t.material = 0;
result->addTri(t);
}
}
7.面拆分,所有的三角形面都拆分为独立的
void detachAllFaces();
//---------------------------------------------------------------------------
// EditTriMesh::detachAllFaces
//
// Detach all the faces from one another. This creates a new vertex list,
// with each vertex only used by one triangle. Simultaneously, unused
// vertices are removed.
void EditTriMesh::detachAllFaces() {
// Check if we don't have any faces, then bail now.
// This saves us a crash with a spurrious "out of memory"
if (triCount() < 1) {
return;
}
// Figure out how many triangles we'll have
int newVertexCount = triCount() * 3;
// Allocate a new vertex list
Vertex *newVertexList = (Vertex *)::malloc(newVertexCount * sizeof(Vertex));
// Check for out of memory. You may need more
// robust error handling...
if (newVertexList == NULL) {
ABORT("Out of memory");
}
// Scan the triangle list and fill it in
for (int i = 0 ; i < triCount() ; ++i) {
Tri *t = &tri(i);
// Process the three vertices on this face
for (int j = 0 ; j < 3 ; ++j) {
// Get source and destination vertex indices
// 根据原来三角形列表信息,构造新的顶点
int sIndex = t->v[j].index;
int dIndex = i*3 + j;
Vertex *dPtr = &newVertexList[dIndex];
// Copy the vertex
*dPtr = vertex(sIndex);
// Go ahead and fill in UV and normal now. It can't hurt
// 面拆分后,顶点的u、v、normal用三角形保存顶点的;也是三角形顶点索引中存在uv的含义。
dPtr->u = t->v[j].u;
dPtr->v = t->v[j].v;
dPtr->normal = t->normal;
// Set new vertex index
// 三角形还是不改变,只是三角形的顶点索引变为了新的
// 三角形中的材质还是不变,部分也不变
t->v[j].index = dIndex;
}
}
// Free the old vertex list
::free(vList);
// Install the new one
vList = newVertexList;
vCount = newVertexCount;
vAlloc = newVertexCount;
}
8.顶点变换,物体三角网格上的顶点都要进行变换
// Transform all the vertices
void transformVertices(const Matrix4x3 &m);
//---------------------------------------------------------------------------
// EditTriMesh::transformVertices
//
// Transform all the vertices. We could transform the surface normals,
// but they may not even be valid, anyway. If you need them, compute them.
void EditTriMesh::transformVertices(const Matrix4x3 &m) {
// 变换所有顶点,只有顶点的向量改变,uv和normal没有改变,顶点法向量失效了需要重新计算
for (int i = 0 ; i < vertexCount() ; ++i) {
vertex(i).p *= m;
}
}
9.求顶点的法向量,平均共享三角形的面法向量
// Compute vertex level surface normals. This
// automatically computes the triangle level
// surface normals
void computeVertexNormals();
//---------------------------------------------------------------------------
// EditTriMesh::computeTriNormals
//
// Compute vertex level surface normals. This automatically computes the
// triangle level surface normals
void EditTriMesh::computeVertexNormals() {
int i;
// First, make sure triangle level surface normals are up-to-date
// 就是最简单的求每个三角形的法向量
computeTriNormals();
// Zero out vertex normals
for (i = 0 ; i < vertexCount() ; ++i) {
vertex(i).normal.zero();
}
// Sum in the triangle normals into the vertex normals
// that are used by the triangle
for (i = 0 ; i < triCount() ; ++i) {
const Tri *t = &tri(i);
// 不同的i下,也就是不同的三角形会共享t->v[j].index顶点,因此顶点的normal是所有共享该顶点的面的法向量的和
for (int j = 0 ; j < 3 ; ++j) {
vertex(t->v[j].index).normal += t->normal;
}
}
// Now "average" the vertex surface normals, by normalizing them
for (i = 0 ; i < vertexCount() ; ++i) {
// 单位向量的平均是加起来再单位化
vertex(i).normal.normalize();
}
}
10.优化三角形顶点或材质
// Re-order the vertex list, in the order that they
// are used by the faces. This can improve cache
// performace and vertex caching by increasing the
// locality of reference. This function can also remove
// unused vertices simultaneously
// 按照三角形的次序,来重新排列顶点的次序,前提是要求三角形是连续给出的,才能有效的进行显存命中优化
void optimizeVertexOrder(bool removeUnusedVertices = true);
// Sort triangles by material. This is VERY important
// for effecient rendering
// 将三角形按照材质来排序,对于提高纹理渲染的速度很有作用;但是这样顶点优化可能因为三角形次序改变了,那么会受到影响
// 大多数情况下纹理相近的,也是顶点相近的,所以不会受到太大的影响;先优化材质,在优化顶点比较好,因为先优化顶点因为三角形次序
// 提交渲染时候是根据三角形列表的,改变了提交三角形列表导致并不能将连续的顶点一起提交。
void sortTrisByMaterial();
//---------------------------------------------------------------------------
// EditTriMesh::optimizeVertexOrder
//
// Re-order the vertex list, in the order that they are used by the faces.
// This can improve cache performace and vertex caching by increasing the
// locality of reference.
//
// If removeUnusedVertices is true, then any unused vertices are discarded.
// Otherwise, they are retained at the end of the vertex list. Normally
// you will want to discard them, which is why we default the paramater to
// true.
void EditTriMesh::optimizeVertexOrder(bool removeUnusedVertices) {
int i;
// Mark all vertices with a very high mark, which assumes
// that they will not be used
for (i = 0 ; i < vertexCount() ; ++i) {
vertex(i).mark = vertexCount();
}
// Scan the face list, and figure out where the vertices
// will end up in the new, ordered list. At the same time,
// we remap the indices in the triangles according to this
// new ordering.
int usedVertexCount = 0;
for (i = 0 ; i < triCount() ; ++i) {
Tri *t = &tri(i);
// Process each of the three vertices on this triangle
for (int j = 0 ; j < 3 ; ++j) {
// Get shortcut to the vertex used
Vertex *v = &vertex(t->v[j].index);
// Has it been used already?
if (v->mark == vertexCount()) {
// We're the first triangle to use
// this one. Assign the vertex to
// the next slot in the new vertex
// list
v->mark = usedVertexCount;
++usedVertexCount;
}
// Remap the vertex index
// 这里重新索引并不准确的,需要对顶点按照三角形遍历出来的次序重新排列
t->v[j].index = v->mark;
}
}
// Re-sort the vertex list. This puts the used vertices
// in order where they go, and moves all the unused vertices
// to the end (in no particular order, since qsort is not
// a stable sort)
qsort(vList, vertexCount(), sizeof(Vertex), vertexCompareByMark);
// Did they want to discard the unused guys?
if (removeUnusedVertices) {
// Yep - chop off the unused vertices by slamming
// the vertex count. We don't call the function to
// set the vertex count here, since it will scan
// the triangle list for any triangle that use those
// vertices. But we already know that all of the
// vertices we are deleting are unused
vCount = usedVertexCount;
}
}
//---------------------------------------------------------------------------
// EditTriMesh::sortTrisByMaterial
//
// Sort triangles by material. This is VERY important for effecient
// rendering
void EditTriMesh::sortTrisByMaterial() {
// Put the current index into the "mark" field so we can
// have a stable sort
for (int i = 0 ; i < triCount() ; ++i) {
tri(i).mark = i;
}
// Use qsort
qsort(tList, triCount(), sizeof(Tri), triCompareByMaterial);
}
//渲染前要重新计算下表面法向量和顶点法向量(因为顶点变换会失效),用于计算背面剔除,相交检测和光照等。
// 渲染优化,这里仅仅重新计算了顶点的法向量用于光照,因为容易失效
void optimizeForRendering();
// Do all of the optimizations and prepare the model
// for fast rendering under *most* rendering systems,
// with proper lighting.
void EditTriMesh::optimizeForRendering() {
computeVertexNormals();
}
11.拷贝三角形列表UV到顶点列表UV,在UV上进行点拆分
// Ensure that the vertex UVs are correct, possibly
// duplicating vertices if necessary
// 拷贝三角形列表中保存的顶点索引UV到顶点列表的顶点UV中去,进行了点拆分。
// 没有拷贝UV的直接从三角形顶点索引拷贝过去,有拷贝了的顶点的UV和三角形顶点索引的UV一样那么不用管
// 有了拷贝现有的顶点UV和三角形顶点索引UV的又不一样,那么从顶点列表中寻找一个顶点位置和法向量一样的,
// 如果该顶点没有被标记使用,或者UV和三角形的顶点索引一样,那么找到将三角形顶点索引填入新的返回。
// 如果全部遍历了一遍顶点列表,还是没有找到则生成一个新的顶点插入到顶点列表中,并赋值给三角形顶点索引。
// 这样UV不一样的顶点,将会生成多个,也就是从UV上面对顶点进行了拆分,每个顶点一个UV是渲染着色的需要,三角网格也无法避免。
void copyUvsIntoVertices();
//---------------------------------------------------------------------------
// EditTriMesh::copyUvsIntoVertices
//
// Ensure that the vertex UVs are correct, possibly duplicating
// vertices if necessary
void EditTriMesh::copyUvsIntoVertices() {
// Mark all vertices indicating thet their UV's are invalid
markAllVertices(0);
// Scan the faces, and shove in the UV's into the vertices
for (int triIndex = 0 ; triIndex < triCount() ; ++triIndex) {
Tri *triPtr = &tri(triIndex);
for (int i = 0 ; i < 3 ; ++i) {
// Locate vertex
int vIndex = triPtr->v[i].index;
Vertex *vPtr = &vertex(vIndex);
// Have we filled in the UVs for this vertex yet?
if (vPtr->mark == 0) {
// Nope. Shove them in
vPtr->u = triPtr->v[i].u;
vPtr->v = triPtr->v[i].v;
// Mark UV's as valid, and keep going
vPtr->mark = 1;
continue;
}
// UV's have already been filled in by another face.
// Did that face have the same UV's as me?
if (
(vPtr->u == triPtr->v[i].u) &&
(vPtr->v == triPtr->v[i].v)
) {
// Yep - no need to change anything
continue;
}
// OK, we can't use this vertex - somebody else already has
// it "claimed" with different UV's. First, we'll search
// for another vertex with the same position. Yikes -
// this requires a linear search through the vertex list.
// Luckily, this should not happen the majority of the time.
bool foundOne = false;
for (int newIndex = 0 ; newIndex < vertexCount() ; ++newIndex) {
Vertex *newPtr = &vertex(newIndex);
// Is the position and normal correct?
if (
(newPtr->p != vPtr->p) ||
(newPtr->normal != vPtr->normal)
) {
continue;
}
// OK, this vertex is geometrically correct.
// Has anybody filled in the UV's yet?
if (newPtr->mark == 0) {
// We can claim this one.
newPtr->mark = 1;
newPtr->u = triPtr->v[i].u;
newPtr->v = triPtr->v[i].v;
// Remap vertex index
triPtr->v[i].index = newIndex;
// No need to keep looking
foundOne = true;
break;
}
// Already claimed by somebody else, so we can't change
// them. but are they correct, already anyway?
if (
(newPtr->u == triPtr->v[i].u) &&
(newPtr->v == triPtr->v[i].v)
) {
// Yep - no need to change anything. Just remap the
// vertex index
triPtr->v[i].index = newIndex;
// No need to keep looking
foundOne = true;
break;
}
// No good - keep looking
}
// Did we find a vertex?
if (!foundOne) {
// Nope, we'll have to create a new one
Vertex newVertex = *vPtr;
newVertex.mark = 1;
newVertex.u = triPtr->v[i].u;
newVertex.v = triPtr->v[i].v;
triPtr->v[i].index = addVertex(newVertex);
}
}
}
}
12.从文本文件中读取和构建三角网格数据结构
// part部分是在三角形网格下的,属于人物的head, hand, body,foot类型,对三角形小集合进行了分类。而所有part的三角形小集合组成了整个
// 三角形网格。如果有需要也是可以对部分中指定的三角形小集合生成一个单独的网格对象的。
// Import/Export S3D format
// 从文本文件中读取三角网格的信息,使用非常广泛
bool importS3d(const char *filename, char *returnErrMsg);
//---------------------------------------------------------------------------
// EditTriMesh::importS3d
//
// Load up an S3D file. Returns true on success. If failure, returns
// false and puts an error message into returnErrMsg
bool EditTriMesh::importS3d(const char *filename, char *returnErrMsg) {
int i;
// Try to open up the file
// 以文本方式读文件
FILE *f = fopen(filename, "rt");
if (f == NULL) {
strcpy(returnErrMsg, "Can't open file");
failed:
empty();
if (f != NULL) {
fclose(f);
}
return false;
}
// Read and check version
if (!skipLine(f)) {
corrupt:
strcpy(returnErrMsg, "File is corrupt");
goto failed;
}
int version;
// 1.读入一个整型,fscanf遇到空格或者换行都会终止
// fscanf(fd,"%ld,%ld,%ld,%c,%lf\n",&dev,&offset,&length,&ch,&ts))可以解析csv文件
// fscanf遇到空格或者换行会终止,那么文件指针的移动也是移动到空格和换行的位置,扫描时候有\n说明一行读完了
if (fscanf(f, "%d\n", &version) != 1) {
goto corrupt;
}
if (version != 103) {
sprintf(returnErrMsg, "File is version %d - only version 103 supported", version);
goto failed;
}
// Read header
// 跳过空白的一行
if (!skipLine(f)) {
goto corrupt;
}
int numTextures, numTris, numVerts, numParts, numFrames, numLights, numCameras;
// 2.fscanf返回值成功时是参数的个数
if (fscanf(f, "%d , %d , %d , %d , %d , %d , %d\n", &numTextures, &numTris, &numVerts, &numParts, &numFrames, &numLights, &numCameras) != 7) {
goto corrupt;
}
// Allocate lists
setMaterialCount(numTextures);
setTriCount(numTris);
setVertexCount(numVerts);
setPartCount(numParts);
// Read part list. the only number we care about
// is the triangle count, which we'll temporarily
// stach into the mark field
// 跳过空白的一行
if (!skipLine(f)) {
goto corrupt;
}
int firstVert = 0, firstTri = 0;
// 3.关于部分组织的系列数据,有numParts行
for (i = 0 ; i < numParts ; ++i) {
Part *p = &part(i);
int partFirstVert, partNumVerts, partFirstTri, partNumTris;
if (fscanf(f, "%d , %d , %d , %d , \"%[^\"]\"\n", &partFirstVert, &partNumVerts, &partFirstTri, &partNumTris, p->name) != 5) {
sprintf(returnErrMsg, "Corrupt at part %d", i);
goto failed;
}
// 需要连续的顶点个数和三角形个数
if (firstVert != partFirstVert || firstTri != partFirstTri) {
sprintf(returnErrMsg, "Part vertex/tri mismatch detected at part %d", i);
goto failed;
}
// 部分中,三角形个数的量,仅保存在这里以及p->name
p->mark = partNumTris;
// 顶点和三角形的开始索引,在此累加,用于校验顶点和三角形个数相等
firstVert += partNumVerts;
firstTri += partNumTris;
}
// 总体的顶点个数和三角形个数要求相等
if (firstVert != numVerts || firstTri != numTris) {
strcpy(returnErrMsg, "Part vertex/tri mismatch detected at end of part list");
goto failed;
}
// Read textures.
// 跳过空白的一行
if (!skipLine(f)) {
goto corrupt;
}
// 4.对连续材质数据的读取,一个材质一行
for (i = 0 ; i < numTextures ; ++i) {
Material *m = &material(i);
// Fetch line of text
if (fgets(m->diffuseTextureName, sizeof(m->diffuseTextureName), f) != m->diffuseTextureName) {
sprintf(returnErrMsg, "Corrupt reading texture %d", i);
goto failed;
}
// Styrip off newline, which fgets leaves.
// Wouldn't it have been nice if the stdio
// functions would just have a function to read a line
// WITHOUT the newline character. What a pain...
// strchr 查找m->diffuseTextureName中首次出现'\n'的位置,没有找到为NULL
// fgets不会像gets那样自动地去掉结尾的\n,所以程序中手动将\n位置处的值变为\0,代表输入的结束
char *nl = strchr(m->diffuseTextureName, '\n');
if (nl != NULL) {
*nl = '\0';
}
}
// Read triangles a part at a time
// 跳过空白的一行
if (!skipLine(f)) {
goto corrupt;
}
// 5.对连续部分下的三角形索引信息的获取,三角形的材质索引,顶点索引,uv坐标的获取
// 每个部分下有 part(partIndex).mark个三角形,三角形个数必须连续累加
int whiteTextureIndex = -1;
int destTriIndex = 0;
for (int partIndex = 0 ; partIndex < numParts ; ++partIndex) {
// Read all triangles in this part
for (int i = 0 ; i < part(partIndex).mark ; ++i) {
// get shortcut to destination triangle
Tri *t = &tri(destTriIndex);
// Slam part number
t->part = partIndex;
// Parse values from file
// 扫描一行,同时跳过\n
if (fscanf(f, "%d , %d , %f , %f , %d , %f , %f , %d , %f , %f\n",
&t->material,
&t->v[0].index, &t->v[0].u, &t->v[0].v,
&t->v[1].index, &t->v[1].u, &t->v[1].v,
&t->v[2].index, &t->v[2].u, &t->v[2].v
) != 10) {
sprintf(returnErrMsg, "Corrupt reading triangle %d (%d of part %d)", destTriIndex, i, partIndex);
goto failed;
}
// Check for untextured triangle
// 材质索引小于0,那么赋予一个白色
if (t->material < 0) {
if (whiteTextureIndex < 0) {
Material whiteMaterial;
strcpy(whiteMaterial.diffuseTextureName, "White");
whiteTextureIndex = addMaterial(whiteMaterial);
}
t->material = whiteTextureIndex;
}
// Scale UV's to 0...1 range
// UV的值是[0,256]这里需要除法,转换为[0,1],一个三角形需要三个顶点的UV纹理坐标,才能映射纹理的一个区域
// 可能UV纹理坐标系和顶点坐标系不一致,需要翻转贴图或者翻转坐标系处理下
t->v[0].u /= 256.0f;
t->v[0].v /= 256.0f;
t->v[1].u /= 256.0f;
t->v[1].v /= 256.0f;
t->v[2].u /= 256.0f;
t->v[2].v /= 256.0f;
// Next triangle, please
++destTriIndex;
}
}
assert(destTriIndex == triCount());
// Read vertices
// 跳过空白的一行
if (!skipLine(f)) {
goto corrupt;
}
// 6.扫描顶点,一个顶点里面存放了顶点的一个向量,该向量作为一行
for (i = 0 ; i < numVerts ; ++i) {
Vertex *v = &vertex(i);
if (fscanf(f, "%f , %f , %f\n", &v->p.x, &v->p.y, &v->p.z) != 3) {
sprintf(returnErrMsg, "Corrupt reading vertex %d", i);
goto failed;
}
}
// OK, we don't need anything from the rest of the file. Close file.
fclose(f);
f = NULL;
// Check for structural errors in the mesh
if (!validityCheck(returnErrMsg)) {
goto failed;
}
// OK!
return true;
}
13.三角网格的渲染-直接得到顶点和三角形索引列表进行DrawIndexedPrimitiveUP渲染
struct RenderVertex {
Vector3 p; // position
Vector3 n; // normal
float u,v; // texture mapping coordinate
};
struct RenderTri {
unsigned short index[3];
};
class TriMesh {
public:
TriMesh();
~TriMesh();
// Memory allocation
void allocateMemory(int nVertexCount, int nTriCount);
void freeMemory();
// Mesh accessesors
int getVertexCount() const { return vertexCount; }
RenderVertex *getVertexList() const { return vertexList; }
int getTriCount() const { return triCount; }
RenderTri *getTriList() const { return triList; }
// Rendering. This will use the current 3D context.
void render() const;
// Bounding box
void computeBoundingBox();
const AABB3 &getBoundingBox() const { return boundingBox; }
// Conversion to/from an "edit" mesh. Note that this class
// doesn't know anything about parts or materials, so the
// conversion is not an exact translation.
void fromEditMesh(const EditTriMesh &mesh);
void toEditMesh(EditTriMesh &mesh) const;
protected:
// Mesh data
int vertexCount;
RenderVertex *vertexList;
int triCount;
RenderTri *triList;
// Axially aligned bounding box. You must call computeBoundingBox()
// to update this if you modify the vertex list directly
AABB3 boundingBox;
};
//---------------------------------------------------------------------------
// TriMesh::fromEditMesh
//
// Convert an EditTriMesh to a TriMesh. Note that this function may need
// to make many logical changes to the mesh, such as ordering of vertices.
// Vertices may need to be duplictaed to place UV's at the vertex level.
// Unused vertices are discarded and the vertex list order is optimized.
// However, the actual mesh geometry will not be modified as far as number
// of faces, vertex positions, vertex normals, etc.
//
// Also, since TriMesh doesn't have any notion of parts or materials,
// that information is lost.
//
// The input mesh is not modified.
void TriMesh::fromEditMesh(const EditTriMesh &mesh) {
int i;
// Make a copy of the mesh
EditTriMesh tempMesh(mesh);
// Make sure UV's are perperly set at the vertex level
tempMesh.copyUvsIntoVertices();
// Optimize the order of the vertices for best cache performance.
// This also discards unused vertices
tempMesh.optimizeVertexOrder();
// Allocate memory
allocateMemory(tempMesh.vertexCount(), tempMesh.triCount());
// Make sure we have something
if (triCount < 1) {
return;
}
// Convert vertices
for (i = 0 ; i < vertexCount ; ++i) {
const EditTriMesh::Vertex *s = &tempMesh.vertex(i);
RenderVertex *d = &vertexList[i];
d->p = s->p;
d->n = s->normal;
d->u = s->u;
d->v = s->v;
}
// Convert faces
for (i = 0 ; i < triCount ; ++i) {
const EditTriMesh::Tri *s = &tempMesh.tri(i);
RenderTri *d = &triList[i];
d->index[0] = s->v[0].index;
d->index[1] = s->v[1].index;
d->index[2] = s->v[2].index;
}
// Make sure bounds are computed
computeBoundingBox();
}
void TriMesh::render() const {
gRenderer.renderTriMesh(vertexList, vertexCount, triList, triCount);
}
void Renderer::renderTriMesh(const RenderVertex *vertexList, int vertexCount, const RenderTri *triList, int triCount) {
HRESULT result;
// Make sure we have something to render
if (!checkMesh(vertexList, vertexCount, triList, triCount)) {
return;
}
// Make sure we have a device
if (pD3DDevice == NULL) {
assert(false);
return;
}
// Enable lighting, if user has enabled it
setD3DRenderState(D3DRS_LIGHTING, lightEnable);
// Set the vertex shader using a flexible vertex format
result = pD3DDevice->SetVertexShader(D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_TEX1);
assert(SUCCEEDED(result));
// Render it using "user pointer" data
result = pD3DDevice->DrawIndexedPrimitiveUP(
D3DPT_TRIANGLELIST,
0,
vertexCount,
triCount,
triList,// 顶点的索引数据
D3DFMT_INDEX16,//单个的格式
vertexList,
sizeof(vertexList[0])
);
assert(SUCCEEDED(result));
}