第十节 LOD结合视距分区块mesh地形渲染


yande.re 64127 bathing harukaze_setsuna nopan tinkerbell tinkle.jpg
上一篇:


这一节我们将对不同视距的区块地形进行不同精度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);

保存后,查看效果,发现地形算是随机不重复了,不过每个区块间接缝还是有些问题的,我们针对这个问题,下一节再详细研究了。

Last modification:August 12, 2020
If you think my article is useful to you, please feel free to appreciate