第十节 LOD结合视距分区块mesh地形渲染
上一篇:
这一节我们将对不同视距的区块地形进行不同精度LOD区分绘制mesh网格
首先我们需要新建一个类,来管理不同精细度的LOD mesh,针对每一个地形区块,获取其对应精度的mesh
打开EndlessTerrain脚本:
在EndlessTerrain类里面,最后新建:
class LODMesh
{
public Mesh mesh;
public bool hasRequestedMesh;//追踪是否已经请求mesh状态
public bool hasMesh;
int lod;//当前mesh对应lod档位
public LODMesh(int lod)
{
this.lod = lod;
}
void OnMeshDataReceived(MeshData meshData)//②再处理收到mesh数据进行mesh生成,调整状态为true
{
mesh = meshData.CreateMesh();
hasMesh = true;
}
public void RequestMesh(MapData mapData)//①,先请求mesh,标记已请求状态,传入对应多线程方法
{
hasRequestedMesh = true;
mapGenerator.RequestMeshData(mapData, OnMeshDataReceived);
}
}
我们再回到MapGenerator脚本,因为刚才在EndlessTerrain脚本新设置了LOD mesh对应功能
而我们MapGenerator脚本也有个levelOfDetail变量,这个是用来管理DrawMapInEditor里面的LOD档位的,我们为了区分两者用途,将原来的levelOfDetal ctrl+r+r
重命名为:PreviewLODInEditor,代表这个LOD属于编辑器界面预览LOD效果用的LOD,这样,我们在MapGenerator里面的RequestMeshData方法里请求传入 int lod
,就不会弄混淆了,
同样的,MeshDataThread方法中也增加此参数传入:int lod
,
再检查下,RequestMeshData方法里调用了MeshDataThread,这里也要加入 int lod
参数,同时将里面调用的MeshDataThread也传入 lod
参数,
最后,我们检查下MeshDataThread方法,我们这个方法现在是专用来获取不同视距区块不同mesh精度用,上一节为了测试多线程通信,我们简单的对默认的LOD进行接收运算mesh,也就是现在的 PreviewLODInEditor,现在我们不再需要复制PreviewLODInEditor进行处理运算了,我们在这节的主要目的,就是要将所有多线程处理的mesh,改为基于新的lod判断分块体系,获取对应精度lod,再进行处理对应精细度的mesh数据,而不是之前的和PreviewLODInEditor每块地形都是一模一样的mesh精度,于是这里PreviewLODInEditor也要改成 lod
。
同样的,上节为了测试,与MapGenerator脚本对应的EndlessTerrain脚本中,我们在这里的OnMapDataReceived方法里面,请求执行RequestMeshData方法这句也可以暂时注释掉,稍等我们这节完善新的功能,同样的,OnMeshDataReceived方法暂时也不需要了,我们选中,Ctrl+K+C
也全部注释掉,这节稍后我们会重新编写新的。
将EndlessTerrain最后方RequestMeshData方法,中间也新增传入 lod
参数
这样,我们的 lod
参数添加已经改造完毕,接下来
接着我们在EndlessTerrain脚本需要新建一个结构体,用来管理不同LOD信息,属于哪档LOD,存储对应档位LOD精度,等等信息,方便调取,
那么在脚本最后,新建结构体:
public struct LODInfo{
public int lod;
public float visibleDistThreshold;//可视距离阈值,用来对区块进行距离判定,对不同视距区块划分不同LOD精度,而不仅仅是固定的450视距了
为了方便在面板调整视距阈值档位,我们加上 [System.Serializable]
修饰,方便我们修改
脚本最前方新加入数组:
public LODInfo[] detailLevels;
保存,来到Unity,去MapGenerator脚本开始手动设置我们LODInfo
先将size设为3,来三档视野阈值看看
Lod分别设为,0,1,2,VisibleDistThreshold设为200,400,600看看
再回到EndlessTerrain脚本,查看我们的MaxViewDistance常数,现在已经不再固定,而是跟随我们最大档位视距阈值改变了,于是我们修改下
public static float maxViewDistance;
再到Start方法里面,定义 maxViewDistance=detailLevels[detailLevels.Length-1].visibleDstThreshold;
数组长度是从1开始计算的,数组的索引值是从0开始的,所以我们最大档位索引值,这里举例就是第三档,对应索引值是长度3-1,即2号索引位置,将这个索引位置的visibleDSTThreshold阈值作为我们最大视距。
构建完毕LOD信息相关结构体和存储数组后,我们需要将存储后的信息传入我们TerrainBlock地形里
于是我们来到 public TerrainBlock
构造函数,随便找个位置,传入参数加一个 LODInfo[] detailLevels
,
当然了,我们在对应的TErrainBlock类里面也要申明对应的数组信息:
LODInfo[] detailLevels;
同时在TerrainBlock构造函数里赋值
this.detailLevels=detailLevels;//该类里面detailLevels变量赋值为传入参数detailLevels的值
对应的,上方UpdateVisibleBlocks方法中调用实例化生成新的TerrainBlock这里也要传入对应数组信息:
else{
terrainBlockDictionary.Add(viewdBlockCoordinate,new TerrainBlock (viewdBlockCorrdinate,blockSize,detailLevels,transform,mapMaterials));
我们接下来继续完善TerrainBlock地形构建,LODInfo已经顺利加入,我们可以构建对应mesh信息了,TerrainBlock类里面,新加入 LodMesh[] lodMeshes;
然后去对应构造函数里新增赋值:
lodMeshes= new LODMesh[detailLevels.Length];//然后遍历这个LODInfo信息数组,将里面的LOD信息对应传递给mesh里面;
for (int i = 0; i < detailLevels.Length; i++)
{
lodMeshes[i] = new LODMesh(detailLevels[i].lod);
}
这些准备工作完成之后,我们终于可以正式处理新的MapData与meshData获取流程了,
OnMapDataReceived这里,我们开始对新的地形数据进行处理
TerrainBlock类里面新申明两个变量:
MapData mapData;
bool mapDataReceived;
然后去OnMapDataReceived开始构建新的判断逻辑:
this.mapData = mapData;
mapDataReceived=true;
接下来,mesh网格请求前,我们需要新增视距更新,而不是直接调用mapGenerator.RequestMeshData,
我们查看下方的UpdateTerrainBlock方法中,我们之前是做了单个区块边界平方和maxViewDistance平方大小比较,仅仅判断是否显示而已,
现在要对LODInfo数组里存储的多块地形进行细分,对所有在maxViewDistance内,也就是所有会显示的地形,从i=0开始遍历,将小于当前阈值的地形区块划分给当前的lodIndex,赋值对应精细度lod档位,超过当前阈值距离的地形区块,我们让lodIndex索引+1,划分到更大一档lodIndex再做判断,如果比该档位阈值还大,继续lodIndex+1,直到划分匹配到对应精度LOD,然后break跳出当前循环。在setVisible前插入:
if (visible)
{
int lodIndex = 0;
for (int i = 0; i < detailLevels.Length - 1; i++)
{
if (minDistanceToViewer > detailLevels[i].visibleDistThreshold*detailLevels[i].visibleDistThreshold)
{
lodIndex = i + 1;
}
else
{
break;//与viewer距离小于等于当前LOD阈值,归属正确的lodIndex索引,直接返回,进行下一步遍历
}
}
}
这里为什么不判断 i=detailLevels.Length-1
时候的情况呢?因为我们之前已经做了约束,所有的visible区块被 <=maxViewDistance
约束着,在visible属性的距离内还比minDistanceToViewer距离大的区块理论上不存在的,实际上 i=Length-1
这一堆区块都归属于精度最低那档的lodIndex,在 i=detailLevels.Length-2
这步已经将这些区块划分到 lodIndex=i+1
这档了,无需再做多余判断。
好了,目前我们就拿到了每块地形对应应该显示的LOD档位了,再进行最终mesh绘制前我们需要做个优化,分析下目前获得的lodIndex分类情况和上一帧的index对应分类是否一致,如果一致,我们无需重复再次绘制渲染一遍,避免资源浪费
在TerrainBlock类里面,前方我们新申明变量:
int previousLODIndex=-1;
这样可以确保第一次运行检查对比的时候肯定不会一致,确保首次运行强制绘制一遍初始mesh布局信息
我们在 if(visible)
内,for循环遍历中,判断当前所有lodIndex后,继续写新的判断:检测与 previous Index
的差别:
if (lodIndex!=previousLODIndex){
LODMesh lodMesh=lodMeshes[lodIndex];
if (lodMesh.hasMesh){
previousLODIndex=lodIndex;//将当前Index赋值给下一轮的,相对于下一轮,这轮的算previous
meshFilter.mesh=lodMesh.mesh;
}else if (!lodMesh.hasRequestMesh){
lodMesh.RequestedMesh(mapData);
}
}
}
这边的mesh分LOD精度请求生成和每帧更新机制都 已经完成,UpdateTerrainBlock这整个方法功能已经基本实现,加上最后一个前提条件,就是当mapDataReceived后,才会执行运算mesh,所以将updateTerrainBlock方法内全部内容放在一个大判断框架里,增添
if(mapDataReceived){//将原updateTerrainBlock方法所有内容剪贴进来}
按Ctrl+K,D:自动对齐框选内容的格式
保存,我们来到unity界面运行看看,查看边界区块果然粗糙很多了,说明LOD分精度渲染mesh起作用了,为了更清楚查看效果,我们改shaded渲染模式为shaded wireframe,这样可以看到mesh网格复杂度区分的更明显了。
如果我们改lod档位为0,4,6,会发现三部分阈值对应的lod精细度差别更明显了,拖动viewer应该也更流畅了,毕竟我们降低了周围两档的精度
我们甚至可以修改为size 为4档,0,2,4,7对应视野阈值250,500,750,1000,执行后可以看见更精细丰富的lodmesh信息。
如果想更加流畅的效果,我们可以考虑在Update方法里,修改原来每帧调用 UpdateVisibleBlocks()
方法,为其他条件,比方说,当我们的viewer移动距离超过24个像素点后再调用,就可以降低系统负担
也就是说,对viewer移动距离也加一个Threshold阈值,当移动距离超过该阈值的时候,再进行对应UpdateVisibleBlocks方法调用,系统负担应该会有效降低
EndlessTerrain脚本开头我们新增常量:
const float viewerMoveThresholdToCallBlockUpdate=24f;
const float sqlViewerMoveThresholdToCallBlockUpdate=viewerMoveThresholdToCallBlockUpdate*viewerMoveThresholdToCallBlockUpdate;//这个是方便每次运算比较sql平方,运算量比开方小,之前比较maxViewDistance时候分析过这个。
Vector2 viewerPositionOld;//新增旧位置记录,方便做当前位置和旧位置距离比较
然后在Update方法开始实现:在viewerPosition赋值语句下一行插入判断:
if((viewerPositionOld - viewerPosition).sqrMagnitude>sqrViewerMoveThresholdToCallBlockUpdate){
viewerPositionOld=viewerPosition;
UpdateVisibleBlocks();
这样的话有两个问题,初始我们地图区块绘制没有被请求,第二,如果viewer不移动,Update里就不会再执行UpdateVisibleBlocks方法了,那么造成问题就是,运行后,相关blocks信息就无法获取到,如果移动viewer,初始所有区块信息的缺失还会造成部分区块信息不加载的问题,
我们可以考虑在start方法中,做Blocks信息更新,添加UpdateVisibleBlocks();
语句到Start方法里
这样就可以解决了初始地图信息获取问题,然后我们来处理下Update方法中,只有当old-当前position值超过阈值后才会UpdateVisibleBlocks,我们可以考虑在请求地图数据的时候加入一个Action ,当收到新的地图数据后,请求callback调用此方法,最后的调用逻辑就是,对UpdateVisibleBlocks方法调用,不仅仅是在Update更新调用,拓展如下:
一,每帧检测直到移动距离超过移动阈值,
二,获取新的地图数据后,
这两种情况下都会进行调用。这样通过简单的移动阈值检测,可以降低每帧自动全部计算一次地图数据的消耗,同时又保证当新的地图数据计算完成,接收地图数据后,必要的绘制请求也能执行获取
于是我们在LODMesh类里面,加入一个callback Action:
System.Action updateCallback;
同时在此LODMesh类构造函数中也传入System.Action updateCallback参数,构造函数内部加入:
this.updateCallback=updateCallback;
然后在OnMeshDataReceived方法里,我们收到mesh信息后,绘制Mesh数据同时调用此方法:
updateCallback();
同时我们考虑在一开始生成TerrainBlocks,就绘制好对应LODmesh:
TerrainBlock构造函数里,刚才的lodMeshes信息存储时候,我们添加UpdateTerrainBlock方法:
for (int i = 0; i < detailLevels.Length; i++)
{
lodMeshes[i] = new LODMesh(detailLevels[i].lod, UpdateTerrainBlock);
}
然后就是,在OnMapDataReceived的时候,也进行mesh绘制:
来到OnMapDataReceived方法里,我们加入
UpdateTerrainBlock();
保存,unity运行,应该就ok了,拖动后发现运算也流畅一些了
好了,基本功能差不多完成了,我们快速地进行材质绘制功能实现:OnMapDataRecieved()
时候,添加材质:
Texture2D texture = TextureGenerator.TextureFromColourMap(mapData.colourMap, MapGenerator.mapBlockSize, MapGenerator.mapBlockSize);
meshRenderer.material.mainTexture = texture;
保存,改shaded模式为shaded,发现彩色材质已经渲染ok,不过目前还是重复绘制的相同色彩分布,我们修正下,
打开MapGenerator,找到RequestMapData和MapDataThread方法,传入新参数 Vector2 center
,同时在RequestMapData方法里调用到的MapDataThread方法也别忘记加入 center
参数。
之后,MapDataThread线程里,当我们生成地图数据的时候,传入该 center
参数:MapData mapData=GenerateMapData(center)
;//在生成地图数据时候传入 center
参数
然后ctrl加鼠标追踪该GnerateMapData方法,在 GenerateMapData方法传入参数 Vector2 center
此方法内部,在生成noiseMap数据时候,我们在offset偏移量这里增加加入 center
因子,改为:
center+offset
最后在DrawMapInEditor方法里,传入 Vector2.zero
参数到 GenerateMapData(Vector2.zero)
然后回到endlessTerrain脚本,在TerrainBlock构造函数里,最后也有一个地RequestMaapData的调用不要漏掉:MapGenerator.RequestMapData(OnMapDataReceived);
我们传入 positon
参数,最后效果:
MapGenerator.RequestMapData(position,OnMapDataReceived);
保存后,查看效果,发现地形算是随机不重复了,不过每个区块间接缝还是有些问题的,我们针对这个问题,下一节再详细研究了。