目的:
模型当前的面数过多,在游戏上跑,性能会有较大问题
方法:
方法一:美术制作各种级别的模型
优点:模型精细度可控
缺点:素材可能变大并且美术需要消耗大量人力制作
方法二:程序自动支持各种级别的模型
优点:不需要美术消耗资源制作,时间大量节省
缺点:精细度不一定能完美解决,可能每个模型需要调整不同的减面系数
LOD是Level Of Details的缩写即多层次细节 LOD就是为了支持当物体远离观察者或者物体的重要程度不同,位置不同,速度不同或者视角相关的参数不同需要减少渲染3D模型的复杂度
Mesh Decimation技术
第一种为顶点聚类(vertex clustering)
第二种为增量式简化(Increment decimation)。
顶点聚类(vertex clustering)
顶点聚类原理大致分为以下几步:
1. 生成聚类(Cluster Generation)
2.计算表现因子(Computing a representative)
3.生成网格(Mesh Generation)
4.改变拓扑结构(Topology Changes)
边坍缩(edge collapse)
边坍缩(edge collapse)是实现增量式简化(Increment decimation)的一种思路,使用三角边坍缩的方法来进行网格简化,将两个顶点合并成一个顶点,如图所示。
Automatic LOD效果(分别是lod0,lod1,lod2的效果)
实现代码
GetWorldVertices(gameObject)会遍历模型上所有的顶点,对于SkinnedMesh会带入骨骼的旋转和权重值进行计算,返回物体所有的顶点在世界坐标系的位置。
meshUniqueVertices.BuildData(m_meshOriginal, aVerticesWorld)会重新创建一组Mesh顶点数据和一组三角形面片数据,包含本地坐标系位置和世界坐标系位置,因为Unity的Mesh数据结构里面,所有的顶点保存在一个顶点列表里面,三角形面片列表只会拥有顶点的索引,所以对于位置相同的顶点,三角形面片列表会拥有顶点列表里面相同的索引。
AddVertices(m_meshUniqueVertices.ListVertices, m_meshUniqueVertices.ListVerticesWorld, m_meshUniqueVertices.ListBoneWeights)会使用重新创建的顶点数据列表创建对应的Vertex对象数组,并保存到m_listVertices列表里面。
Vertex对象记录了一个顶点在网格简化过程中所需要的相关信息,如坐标位置,相邻的Vertex顶点,相邻的三角形对象,边收缩代价(Edge Collapse Cost),坍缩目标点,优化排序的顶点等等
优化顶点排序
二分优化排序,把大的值尽量放在左边,他并不能完全保证排序顺序(实际上只排了一半),但起码大的排在前面的概率会大
而且不需要0(n)以上来排序,只需要0(logn)
HeapValue是获取具体的边坍塌代价
void HeapSortUp(int k)
{
int k2;
while (HeapValue(k) < HeapValue((k2 = (k - 1) / 2)))
{
Vertex tmp = m_listHeap[k];
m_listHeap[k] = m_listHeap[k2];
m_listHeap[k].m_nHeapSpot = k;
m_listHeap[k2] = tmp;
m_listHeap[k2].m_nHeapSpot = k2;
k = k2;
}
}
创建对应的Triangle对象数组
AddFaceListSubMesh(nSubMesh, m_meshUniqueVertices.SubmeshesFaceList[nSubMesh].m_listIndices, anIndices, av2Mapping)会使用重新创建的三角形面片列表创建对应的Triangle对象数组,并保存到m_aListTriangles列表里面。主要是获取每个三角面所包含的三个顶点,并计算法线(用于计算曲率)。同时,将该三角面加入顶点的三角面列表,并将每个顶点加入另外两个顶点的邻居列表中去。Triangle对象包含了Vertex对象的引用列表,三角形面法线信息用于计算坍缩代价,UV信息等。
坍塌计算
ComputeAllEdgeCollapseCosts(strProgressDisplayObjectName, gameObject.transform, aRelevanceSpheres, progress)会遍历网格中的所有Vertex顶点,对于每一个Vertex顶点,计算所有相邻顶点的边坍缩代价(Edge Collapse Cost),保存边坍缩代价最小的值和坍缩目标点(Collapse Vertex),并且把计算后的Vertex顶点添加到堆里面,按照边坍缩代价进行从小到大排序
一条边是否要坍塌
它的边长与曲率值的乘积
-通过顶点间的向量之差的叉乘得到法线方向。
-比较两个面的法线的点积得到坍塌边uv的曲率值
-然后用两点距离×点乘的值得到坍塌的代价
坍缩代价
-边的长度,在模型简化过程中,小的细节应该优先被移除,所以边长短的应该优先被移除
-点周围的曲率变化,理论上曲率变化越小的顶点,所处的区域越平坦,应该优先被移除
边坍缩处理阶段
循环地从堆里弹出边坍缩代价最小的顶点,并且调用Collapse(Vertex u, Vertex v, bool bRecompute, Transform transform, RelevanceSphere[] aRelevanceSpheres)对顶点进行边坍缩处理
具体方式是获取坍缩代价最小的顶点u及其坍缩目标顶点v
遍历u的三角面列表
如果包含v就删除该三角面,否则将u替换为v
其中替换方案:把老顶点的临点赋值给新临点,删除老顶点的临点和老顶点
然后重新计算u的所有邻居点的坍缩代价和坍缩目标并更新堆的顺序
直到所有顶点全部处理完成
顶点间坍塌顺序的区别
参考资料
https://zhuanlan.zhihu.com/p/51944864?utm_source=qq&utm_medium=social&utm_oi=827612570348310528
unity运行时执行
三种模式 Screen Coverage:
-对象在屏幕中的大小决定
-Camera Distance:摄像机远近决定
-Just Change:强制切换LOD
CameraDistance
先判定相机距离
-Vector3 v3WorldCenter = transform.TransformPoint(m_localCenter.x, m_localCenter.y, m_localCenter.z);
-//计算自身坐标空间到摄像机的距离
-fDistanceToCamera = Vector3.Distance(v3WorldCenter, currentCamera.transform.position);
判断距离与每个lod切换条件
-if (fDistanceToCamera < m_listLODLevels[nLODLevel + 1].m_fMaxCameraDistance)
Screen Coverage
先判定对象占据屏幕的大小(特别注意的也需要看子对象是否比父级对象占据的上下左右最大)
Vector3 v3World = i == 0 ? GetComponent().bounds.min : GetComponent().bounds.max;
Vector3 v3View = mtxView.inverse.MultiplyPoint(v3World);
if (v3View.x < fMinX) fMinX = v3View.x;
if (v3View.y < fMinY) fMinY = v3View.y;
if (v3View.z < fMinZ) fMinZ = v3View.z;
if (v3View.x > fMaxX) fMaxX = v3View.x;
if (v3View.y > fMaxY) fMaxY = v3View.y;
if (v3View.z > fMaxZ) fMaxZ = v3View.z;
float fViewSurfaceArea = (fMaxX - fMinX) * (fMaxY - fMinY);
判断大小与每个lod切换条件
-if (fScreenCoverage > m_listLODLevels[nLODLevel + 1].m_fScreenCoverage)
unity编译时流程
创建对象的n个mesh(现在n=3)
把相关依赖的mono附在对象上(AutoMaticLOD,AutoLODAllMesh)
创建mesh对象的减面百分比,距离,大小,最终存储在创建的asset文件下(包括对象的子对象)
附上相关的依赖关系和mesh到每个对象上 打ab
lod切换等级计算方式
ScreenCoverage:
float oneminust = (float)(percent / 100.0f);
data.m_fScreenCoverage = Mathf.Pow(oneminust, 3.0f);
CameraDistance
i == 0 ? 0.0f : nLevel * 100.0f;
未来尝试方向
减面需要符合真实美感:
-有合并过的面选择性平滑过度
-指定的部位或面不进行autolod