【Unity 3D进阶教程分享】EndlessTerrain无限地形搭建 第十节：LOD结合视距分区块mesh地形渲染
第十节 LOD结合视距分区块mesh地形渲染上一篇：[post cid="406" /]这一节我们将对不同视距的区块地形进行不同精度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);保存后，查看效果，发现地形算是随机不重复了，不过每个区块间接缝还是有些问题的，我们针对这个问题，下一节再详细研究了。
【Unity 3D进阶教程分享】EndlessTerrain无限地形搭建 第九节：多线程同步协调处理多区块思想和基本实现
第九节 多线程同步协调处理多区块思想和基本实现上一篇：[post cid="403" /]这节我们将继续上节的无限地形生成功能完善，将上节制作出来的视野内三维plane的显示和隐藏功能与我们的MapGenerator相结合，这里将会用到多线程技术，之前我们第一节了解过协程的原理，这里多线程技术比协程更高级一些，这算是真正的多个线程都在一起跑，而不是协程的跳来跳去，适合处理一些短时间内计算量很大，任务数量较多，可以同步单独运算，同时又追求高效完成需要快速拿到多个运算结果的事情。由于Unity系统限制，实际上我们unity本身是没有多线程的，我们的副线程是干不了前台的画面渲染，人物运动，物理碰撞，AI运算这些涉及到Unity SDK或者说继承于Monobehaviour的一些操作的，这些只能交给主线程，但是，这不代表多线程没有用，后台的数据运算，比如说伤害计算，MeshData数据构建，网络交互更新，一些复杂的I/O调取操作，都是可以通过分配给副线程去做的，多线程是c#里实现的，可以辅助unity达到多线程的效果，通过Action和Callback之类的指令，和unity主线程交互，决定下一步的运算或者释放。多线程运算，涉及到的核心主要有两点，首先，unity主线程和其他C#副线程的交互，是一个重点，这个交互思想代码模板需要熟悉；其次，对于任务和计算的分配，这个优先级和排序，也是一个重点，否则大家一起抢着干同一件事情，造成数据混乱，显然是帮倒忙，也是不可取的；这一节，主要涉及到线程间交互方式，基本框架的构建和熟悉，这一块基本上都是标准模板，初学复杂，熟练后都是套路，代码看起来多，实际都是固定格式，不用害怕。对于任务优先级排序，我们稍微了解下入门级的五个优先级排序，和Lock指令锁定分配任务和分派线程，今后有时间的话再聊聊Loom指令委托到主线程上进行调度，差不多就可以实现一些基础的多线程操作了，更复杂的，慢慢来，多线程就像一把封印的神器，需要不断积累经验，解封用法，才会用的更加强大。好了，我们正式开始：在多线程编写前，我们需要打扫下之前的代码，MapGenerator脚本，我们GenerateMap方法里MapDisplay display三种drawMode方式判断和执行，算是一个单独的方法，我们为了今后不断扩充的系统考虑，将方法细分独立出来，目前考虑将这三种生成渲染模式放置到Inspector面板方便调试，我们可以在此脚本新建一个方法：public void DrawMapInEditor（）{ 复制所有Display代码到这里接下来，我们需要从GenerateMap方法中调用相应的Map数据，之前我们display这块代码和地图信息数组在同一个方法里，是可以直接调取GenerateNoiseMap里面这些地图数据的，现在分家了，在其他方法里调用另一个方法里的数据，是需要一个载体存储对应数据，再调用载体，读取数据，这也是基本的编程思想于是我们在脚本最后新建地图数据结构体：public struct MapData{ public float[,] heightMap;//灰阶高度图是二维数据，需要二维数组存储宽度和高度数据 public Color[] colourMap;//彩色图是整个色彩像素点数据，一维数组就可以存储Ctrl+.或者 Alt+Enter，快捷键构建结构体内部数据，勾选刚才新建的两个数据类型，然后确定就可以快速初始化了，//(如果后期不需要修改地图数据，可以将结构体内部所有元素加入readonly关键词)接下来我们在GenerateMap方法中返回MapData类型数据，void改成MapData同时别忘记在方法里，最后返回对应数据类型：return new MapData（noiseMap，colourMap）;这样，我们就可以通过MapData载体，在我们的DrawMapInEditor方法中调取需要的地图类型和对应数据了，在DrawMapInEditor方法里，开头申明赋值，准备调用：MapData mapData=GenerateMap（）;然后我们将noiseMap，colourMap修正为现在的 mapData.heightMap,mapData.colourMap，结构体内对应map类型，就可以进行绘制渲染了如果之前没有手动换行的话，一个快捷键组合：Ctrl+E+W，让系统自动换行，避免页面外的信息遗漏，忘记修改这么修改后，我们的GenerateMap就单独负责地图信息生成了，而与外部交流沟通数据，就交给我们新建的DrawMapInEditor方法了，于是我们可以考虑将 public MapData GenerateMap方法改为private，因为暂时不需要用来外部类调用它了，去掉public关键词即可，默认就是private了为了更精准描述方法定位，修改方法名为：GenerateMapData(),快捷键 ctrl+r+r；保存后回到unity，应该会出现一些报错，涉及到其他脚本调用，我们需要进去修改对应的调用，对接到DrawMapInEditor方法原来是MapGeneratorEditor脚本，修改 mapGen.GenerateMap()方法为：mapGen.DrawMapInEditor(),就ok了好了，折腾这么多，我们终于将MapGenerator内部方法具体拆分，以便接下来将我们的MapGenerator和EndlessTerrain统筹分工，通过协作多线程完成多块地形同步生成，下面的内容是我们整个无限地形模块重中之重，我会详细用图示解释清楚：首先，我们的EndlessTerrain脚本，需要获取MapGenerator里面的MapData，meshData各种数据，然而我们的MapGenerator里面是需要通过多个帧数运算才会生成相应的数据，那么我们从EndlessTerrain脚本发生请求，肯定不能立马收到MapGenerator传回的地图数据，于是，我们在调用MapGenerator脚本中请求地图数据方法的时候，为该方法自带传入一个Action请求处理完毕后传回callback数据：void RequestMapData（Action<MapData>callback){ //在此方法中，开始启动MapData处理线程 }意味着MapGenerator外部EndlessTerrain之类的脚本调用请求后，我执行完毕数据生成后，会callback发送返回地图数据，这个Action传递MapData信息给外部脚本好的，我们回到EndlessTerrain脚本，在申请调用MapGenerator地图生成方法的时候，应该传入一个接收地图数据时候需要执行的方法，方法名可以叫做OnMapDataReceived，当RequestMapData方法执行完毕后，收到Callback的数据后，也就是接收地图数据后，调用执行此OnMapDataReceived方法，对数据进行处理：void start（）{ RequestMapData（OnMapDataReceived）； } void OnMapDataReceived（mapData mapData）{ //在这里处理mapData地图信息 }这样，我们传回MapData行为，callback回到我们EndlessTerrain后，就可以调用咱们的OnMapDataReceived方法，开始对传出来的地图数据在此方法里进行处理了即MapGenerator中RequestMapData方法自带callback 行为，一旦被外部请求调用，必定会自动传出数据，配合EndlessTerrain外部脚本调用该方法的时候指定传入OnMapDataReceived方法，以便对传回数据进行处理，这样就做到了脚本间线程间基本交互实现。接下来，我们需要关注下MapGenerator这个副线程处理逻辑，当我们void RequestMapdata方法启动我们的MapData处理线程：void MapDataThread（Action<MapData.callback){ //在此线程先运算得到mapData //这里是重点，我们知道，直接进行mesh网格渲染，需要用到unity内部组件，而unity本质上没有多线程处理这些unity内部操作的，我们不能在c#这些副线程里直接处理渲染，所以需要添加存储mapData，然后传回到等待队列，因为有多个副线程同时在处理各个区块地形数据，EndlessTerrain脚本不可能随时同时接收完毕所有副线程处理出来的地图数据，所以我们先存储到传回队列，排好队。接下来，副线程在Update方法里，执行队列位置计算，在队列中按顺序一个个发送callback传回mapData：void Update（）{ if（queue.Count>0{ //调用callback指令，传出mapData数据 } }这就是我们的多线程相互协作的基本原理，接下来我们开始逐步实现，这些基本框架第一次接触可能觉得代码量很大，不过没关系，所有多线程基础框架都可以按类似的逻辑构建来到MapGenerator脚本，我们通用的多线程调用基础，先引入对应命名空间下方法类型：using System；//获得Actions各种行为操作调用功能 using System.Threading;//使用多线程功能接下来，按着我们刚才的分析，开始第一步，构建我们的RequestMapData方法，附带Action，callback此Action，附带传回Action参数为MapData类型数据：public void RequestMapData(Action<MapData> callback）{ //请求生成对应副线程执行地图数据处理，我们稍后书写。 }方法内部功能稍后构建，我们接下来构建此方法将调用的启动MapData运算副线程：void MapDataThread(Action<MapData> callback) { }这样，我们就可以在RequestMapData方法开始构建启动计算副线程功能了：ThreadStart threadStart = delegate { MapDataThread(callback); }; //启动线程，委托给带参函数MapDataThread，记住这是一个语句，不是什么方法也不是类，一定要在语句结束后加上分号，新手最容易以为大括号代表结构和类，后面漏掉分号咱们的方法是外面这个大括号，RequestMapData才是我们的方法结构，第一句话委托给指定线程处理指定方法，接下来写第二句别忘了启动对应线程：new Thread (threadStart).Start();//调用线程类型下的Start（）方法，ok，现在我们MapDataThread方法就可以运行在另一个线程里了，我们实现下 void MapDataThread方法里具体的线程内部功能：MapData mapData=GenerateMapData();//请务必记住，副线程里直接调用的方法，只能是独立于Unity功能的方法，例如：FindObjectOfType,GameObject.Find,SendMessage,StartCoroutine等等方法，是Unity内部组件API等接口所含的功能，这些方法不能直接调用，否则会报错我们再回顾下刚才的【地图生成器和无限地形生成脚本间多线程协作基本原理】图示，地图数据处理，我们直接调用对应方法，一句话就轻松实现了，接下来需要创建一个结构体，存储我们的地图数据，方便等会扔到队列里，存进去按顺序一个个传回去我们在MapGenerator类里面，最后添加一个结构体：struct MapThreadInfo<T> { public Action<T> callback; public T parameter;Ctrl+.或者 Alt+Enter快速重构勾选两个变量，快速构建结构体初始化由于这个结构体我们不需要对其进行改变，作为存储容器，保持其固定不变，建议对所有元素加入readonly关键字，类似于刚才构建的MapData结构体，内部元素都可以加上readonly关键词接下来，为了调用队列Queue方法，我们需要引用新的命名空间：using Systemcollections.Generic;List ,Array,Dictionary这些数据集合也是在这个破容器空间里面的我们在当前MapGenerator类里面，开头申明一个Queue并初始化：Queue<MapThreadInfo<MapData>> mapDataThreadInfoQueue= new Queue<MapThreadInfo<MapData>>();接下来，我们就可以在MapDataThread里面安排上了mapDataThreadInfoQueue.Enqueue(new MapThreadInfo<MapData>(callback,mapData));使用enqueue方法将新运算出来的地图数据和callback指令都传入队列，方便之后从队列调取传回mapData数据到外部脚本EndlessTerrain。这里由于是在副线程运行的，地图数据可以被多个线程同时访问，为了保证单张地图数据完整性，这里加上lock锁定此队列lock （MapDataThreadInfoQueue）{ mapDataThreadInfoQueue.Enqueue(new MapTheradInfo<MapData>(callback,mapData)); }lock代表一旦有一条线程获取到此代码，其他线程就无法访问该代码了，直到代码运算完成释放接下来我们实现最后一步，在Update方法里，判断queue队列里面的排队信息，如果存在队列元素，遍历内部存储的所有队员，一个个callback传送出去，同时将该队员元素从等待队列中移除：void Update() { if (mapDataThreadInfoQueue.Count > 0) { for (int i = 0; i < mapDataThreadInfoQueue.Count;i ++) { MapThreadInfo<MapData> threadInfo = mapDataThreadInfoQueue.Dequeue(); threadInfo.callback(threadInfo.parameter); } } }我们MapGenerator这边请求地图数据方法，启动多线程地图数据运算线程功能，计算好后存储数据到结构体，并且加入队列，然后Update方法将队列元素一个个传出，这些都已经实现，现在来到EndlessTerrain脚本看看如何接收数据：首先我们要调用MapGenerator脚本下的方法，自然要先申明一个对应的MapGenerator：static MapGenerator mapGenerator;Start（）方法中实例化对应地图生成器：MapGenerator=FindObjectOfType<MapGenerator>();好了，开始构建我们EndlessTerrain脚本多线程配合方法：TerrainBlock类里面，新增方法void OnMapDataReceived（MapData mapData）{}对了，别忘记先调用下MapGenerator脚本里面的请求地图数据脚本，先得请求那边执行下，我们在TerrainBlock类里，构造TerrainBlock内容里，最后加上一个获取地图数据请求：mapGenerator.RequestMapData(OnMapDataReceived);嗯，这样就可以让那边多线程处理数据了，再收到那边队列传过来的mapData，我们就可以在OnMapDataReceived方法进行接收处理了，我们继续编辑OnMapDataReceived方法，先做个测试输出：print("地图数据已接收");先打印下，检测传递是否起效果然，出现25条信息，说明ok了，我们中间9宫格9块，加上外围16块地图，总共25块都已经运算完毕并且通信传回了，查看MapGenerator物体，下方对应生成了25个plane，说明ok，非常不错这里为啥只传回地图数据而不是同时传回mesh数据呢？因为我们接下来要在这里做LOD系统，得先对地图分块进行筛选，等会单独构建OnMeshDataReceived方法，对远处的地图直接绘制低精度mesh，这样节省资源占用，最后将不同精度mesh单独处理匹配生成完毕，再传入后续脚本进行渲染mesh好了，我们继续- 新目标：Mesh网格数据生成由于已经拿到地图数据了，之前做为演示用的 meshObject=GameObject.CreatPrimitive初始模型plane已经失去测试意义了，修改为：meshObject = new GameObject("Terrain Block");//创建mesh网格模型Terrain Block同时修改下注释我们还需要对其赋予mesh Filter和mesh Renderer组件，构建mesh网格需要：TerrainBlock类里面，开头加上两个变量：MeshRenderer meshRenderer; MeshFilter meshFilter;接下来为刚才的新物体添加对应mesh存储器和渲染器：meshRenderer =meshObject.AddComponent<MeshRenderer>(); meshFilter = meshObject.AddComponent<MeshFilter>(); mesh组件都构建完毕，我们可以开始实现OnMeshDataReceived方法第一个功能了，为我们的物体构建基本mesh信息： void OnMeshDataReceived(MeshData meshData){ meshFilter.mesh=meshData.CreateMesh(); }我们再次回到MapGenerator脚本，这里面我们需要按着RequestMapData方法，做一个类似的请求meshData的方法，偷懒的话全部可以复制MapData的流程改改字母：一，申明新队列 Queue<MapThreadInfo<MeshData>> meshDataTheradInfoQueue= new Queue<MapThreadInfo<MeshData>>();2.创建新的请求meshData方法public void RequestMeshData(MapData mapData, Action<MeshData>callback){ ThreadStart threadStart = delegate { MeshDataThread(mapData, callback); }; new Thread(threadStart).Start();开启新的meshData计算线程 void MeshDataThread(MapData mapData, Action<MeshData> callback) { MeshData meshData = MeshGenerator.GenerateTerrainMesh(mapData.heightMap, meshHeightMultiplier, meshHeightCurve, levelOfDetail); lock (meshDataThreadInfoQueue) { meshDataThreadInfoQueue.Enqueue(new MapThreadInfo<MeshData>(callback, meshData)); } }然后在UpDate方法里也跟着抄一遍MapDataQueue传出队列遍历： if (meshDataThreadInfoQueue.Count > 0){ for (int i = 0; i < meshDataThreadInfoQueue.Count; ++) { MapThreadInfo<MeshData> threadInfo = meshDataThreadInfoQueue.Dequeue(); threadInfo.callback(threadInfo.parameter); }好了，终于复制粘贴完毕了，我们回到Endless Terrain脚本，接收到MapData后，我们再次请求对应的meshData对我们的OnMapDataReceived方法，之前的print测试语句删掉，改成：mapGenerator.RequestMeshData(mapData,OnMeshDataReceived);由于我们meshData也拿到了，之前TerrainBlock类下的plane对应的mesh localScale尺寸设定也可以去掉了： meshObject.transform.localScale = Vector3.one * size / 10f;删除此句。然后我们需要对TerrainBlock构造体传入新参Material materal，终于可以对咱们的地图数据和mesh数据进行材质渲染了- 新目标：texture材质渲染TerrainBlock构造传入新参：Material material结构内部添加材质信息：meshRenderer.material=material;然后同时别忘了在EndlessTerrain类里面，开头申明对应材质变量：public Material mapMaterial；再找找下面的UpdateVisibleBlocks方法，别忘了循环遍历判断生成符合要求的新block时候，也需要传入地图材质，对新创建的block添加材质信息：else{ terrainBlockDictionary.Add(ViewdBlockCoordinate,new TerrainBlock(viewedBlockCoordinate,blockSize,transfrom,mapMaterial));//在这里添加材质参数保存，回到unity，点击MapGenerator物体，然后查看我们Inspector面板，Endless脚本上需要挂载下我们的Map Material，点击assets文件夹下的Materials文件夹，里面之前创建的MeshMat就是了，拖拽上去，ok，运行查看生成的地形，似乎比较诡异导致这些诡异的地形生成的原因，应该是在MapGenerator脚本，执行RequestMeshData的时候，调用多线程：MapDataThread获取 MeshData meshData = MeshGenerator.GenerateTerrainMesh时，我们MeshGenerator脚本里，查看这个GenerateTerrainMesh静态方法里面，heightCurve在顶点生成的时候，heightCurve.Evaluate方法，被多个线程同时访问处理，最后evaluate方法运算数据发生异常我们可以通过Lock指令，对meshData.vertices生成语句的heightCurve曲线进行锁定改为：lock （heightCurve）{ meshData.vertices[vertexIndex]=…..}保存后unity运行，移动一下viewer，发现生成的地形正常了但是我们认真考虑后没有采用此方法，为什么？因为一旦我们锁定顶点运算方法，其他线程就得乖乖躺着等一个个线程运算顶点数据，这意味着咱们二十五块mesh差不多变成单线程处理mesh信息了，辛辛苦苦弄的多线程失去了意义，地形生成速度会大大降低于是我们思考一下，现在是大家都抢着访问传入的一个曲线，研究他，解析它，读取曲线的数值，利用数值来进行顶点运算，我们完全可以实例化一个曲线，让该曲线 的值等于传入的曲线参数的值，有了一个实例化的载体，大家就可以愉悦地同时查阅该曲线值了，而不是把传入的参数来回抢夺，造成这种问题的原理，目前我的能力无法解决，希望有大佬可以指点下，好了，我们继续修复：MeshGenerator脚本的GenerateTerrainMesh方法里，我们在开头新建一个曲线：AnimationCurve heightCurve =new AnimationCurve(basedHeightCurve.keys);我们考虑命名重复问题，将之前GenerateTerrainMesh方法传入的heightCurve和这里要赋值的传入参数全改成basedHeightCurve，这样后面生成顶点时读取的曲线数值，就从这个实例化的curve中获取了，可以支持多线程同时访问，保存后执行，发现获取速度快了很多，效果也ok目前来说，我们生成的这些破玩意，都是一模一样的mesh地形，接下来，我们将添加材质，生成连续不同地形，以及LOD细节划分等等，更多内容，我们下节再见了下一篇：[post cid="411" /]
【Unity 3D进阶教程分享】EndlessTerrain无限地形搭建 第八节：多区块地形生成与隐藏
第八节 多区块地形生成与隐藏上一篇：[post cid="400" /]新建EndlessTerrain脚本public const float maxViewDistance=450; public Transform viewer; public static Vector2 viewerPosition; int blockSize; int blocksVisibleInViewDistance;上一节我们说到实际渲染的mesh网格是地图尺寸-1，我们为多区块地形的每个地形单元新定义变量名blockSize，下面在开始方法中进行赋值：void Start(){ blockSize=MapGenerator.mapBlockSize-1; blocksVisibleInViewDistance=Mathf.RoundToInt(maxViewDistance/blockSize); }接下来我们需要更新可见区块，那么继续构建相应的方法：每个mesh区块240尺寸，我们viewer位置设为（0，0）mesh原点的话，左侧block就是（-240，0），右下角的block就是（240，-240），那么最大视距除以每个block尺寸取整后，在区块坐标来说，左侧block就可以设为（-1，0），右侧就是（1，0），右下角的block所属区块坐标就是（1，-1）这个规则弄明白后，我们继续构建更新可视区块方法： void UpdateVisibleBlocks() { int currentBlockCoordinateX = Mathf.RoundToInt(viewerPosition.x / blockSize); int currentBlockCoordinateY = Mathf.RoundToInt(viewerPosition.y / blockSize); //求出当前区块x，y坐标值，使用取整方法 //通过遍历正负两个方向最大可视区块坐标范围，给所有视野内区块赋值对应坐标位置。 for (int yOffset = -blocksVisibleInViewDistance; yOffset <= blocksVisibleInViewDistance; yOffset++) { for (int xOffset = -blocksVisibleInViewDistance; xOffset <= blocksVisibleInViewDistance; xOffset++) { Vector2 viewedBlockCoordinate = new Vector2(currentBlockCoordinateX + xOffset, currentBlockCoordinateY + yOffset); } } } 接下来，我们就可以实例化这些block区块了，但是实例化之前，我们需要一个数据集合来存储这些坐标值和对应 地形区块信息，因为需要一个值存储坐标一个存储地形区块信息，那么 Dictionary字典是比较合适的，key键存储坐标信息，对应value值指向地形区块信息，这样到时候就可以记录所有地形，以免重复生成或者漏掉。创建字典之前，我们对所有地形区块创建一个类来表示：在我们的EndlessTerrain类里面，后面新建一个public class TerrainBlock{ }然后调用Dictionary数据集合类型需要用到using System.Collections.Generic;//默认已有则不用添加EndlessTerrian方法中新增字典：Dictionary<Vector2, TerrainBlock> terrainBlockDictionary = new Dictionary<Vector2, TerrainBlock>();再来到下方UpdateVisibleBlocks方法if (terrainBlockDictionary.ContainsKey(viewedBlockCoordinate)) { //稍后进行存储调取操作 } else { terrainBlockDictionary.Add(viewedBlockCoordinate, new TerrainBlock()); }//如果不存在，则将符合要求的block和对应坐标存储进字典保存我们再思考下public class TerrainBlock{这个类，当前需要通过一个空物体或者说mesh管理器之类的东西，生成对应block的mesh载体plane，最后再和我们的MapGenerator脚本交互，进行block生成功能实现。于是我们开始构建TerrianBlock类的构造函数：public class TerrainBlock{ GameObject meshObject; Vector2 position; public TerrainBlock(Vector2 coordinate, int size) { position = coordinate * size; Vector3 positionV3 = new Vector3(position.x, 0, position.y);//坐标为负值的话，代表位置在viewer的左侧 meshObject = GameObject.CreatePrimitive(PrimitiveType.Plane);//创建基础模型之平面 meshObject.transform.position = positionV3; meshObject.transform.localScale = Vector3.one * size / 10f;//之前[city builder][3]提到过，平面是10x10units的，所以scale再除以10f } }那么，我们就定义好了每个block的三维plane平面载体位置和尺寸，为mesh网格载入做好准备接下来我们新建一个更新地形区块的方法，好让Terrainblock状态随时更新，更新什么呢？一个距离判断，检查这个block周长上所有的点，与viewer最近的距离，实时监测对比我们maxViewerDistance距离，如果大于最大可视距离，就不生成咱们的meshObject，如果小于最大可视距离，就要纳入可见block，激活对应的meshObject，进行实例化生成并存储到对应terrainBlockDictionary字典里检测周长，我们需要用到Bounds结构，在 public class TerrainBlock{ 类里我们新增申明一个新的Bounds结构：Bounds bounds;然后来到public TerrainBlock构造函数里，对应的，我们生成新的bounds，传入对应block的位置和size尺寸：bounds = new Bounds(position,Vector2.one* size);然后我们就可以通过更新方法开始检测block边框也就是周长上的点和viewer之间最短距离了，用到的是Bounds结构下的SqrDistance方法，获得viewer这个point，到我们的bound结构最短距离的平方值在 public class TerrainBlock类里面新增方法：public void UpdateTerrainBlock() { float minDistanceToViewer = Mathf.Sqrt(bounds.SqrDistance(viewerPosition)); //我们需要计算的是最小距离，所以再用数学方法类里面的求平方根函数（square root）还原 bool visible = minDistanceToViewer <= maxViewDistance;// 做边界值true/false判断开关 }依据此bool 判断，我们就可以新建开启显示方法，对符合距离的block激活显示：public void SetVisible(bool visible) { meshObject.SetActive(visible); }然后将此方法在UpdateTerrainBlock（）中调用，加入：SetVisible(visible);同时在TerrainBlock构造函数里最后增添block初始状态为false，由每帧都执行的Update方法来判断是否激活显示：SetVisible(false);TerrainBlock初始构建基本完善，UpdateTerrainBlock检测判断和执行SetVisible方法都构建完毕，我们终于可以回头来继续填写UpdateVisibleBlocks方法了//新增地形区块，我们传入区块坐标和对应尺寸，然后对已存在于字典里的区块，进行Update实时更新检测，用来时刻更新最小距离判断，决定是否显示该区块 if （terrainBlockDictionary.ContainsKey（viewedBlockCoordinate)){ terrainBlockDictionary[viewedBlockCoordinate].UpdateTerrainBlock();//监测最小距离 }else { terrainBlockDictionary.Add（viewedBlockCoordinate，new TerrainBlock（viewedBlockCoordinate，blockSize））;//完善传入参数为了让每帧都实时更新，我们新增unity系统默认的Update方法里让每帧都能调用执行：void Update（）{ viewerPosition=new Vector2（viewer.position.x，viewer.position.z）；//每帧都要监测的信息有两个：我们viewer的位置以及执行刚才的更新可见block方法 UpdateVisibleBlocks（）； }有人会说，既然我们每帧都要执行 UpdateVisibleBlocks方法，那么刚才的if判断体里面需要多次判断执行 UpdateTerrainBlock方法，而这个方法里面每次要进行float minDistanceToViewer=Mathf.Sqrt(bounds.SqrDistance （viewerPosition）)； bool visible=minDistanceToViewer<=maxViewDistance；平方根计算，效率会比较低，我们可以改成float minDistanceToViewer=bounds.SqrDistance （viewerPosition）)； bool visible=minDistanceToViewer<=maxViewDistance*maxViewDistance；这样，直接比较最小距离平方和最大视距平方值，每次计算一次乘法，从运算量来说，会远小于计算 Mathf.Sqrt计算平方根，保存，回到unity，将我们的这节的成果：EndlessTerrain脚本挂载到MapGenerator物体上Hierarchy面板再新建一个方块cube，改名为viewer作为我们的观察点，reset坐标，把这个cube挂载到我们EndlessTerrain脚本上作为对应的viewer transform物体组件点击hierarchy面板Mesh物体，如果mesh显示勾选了，则取消显示，以免遮挡我们生成的planes。再运行，点击Viewer物体，选中它，开始在Scene界面移动它看看效果发现跟随Viewer四周已经可以成功生成一大堆plane了，说明距离判断和实时生成TerrainBlock基本功能已经实现，但是有的plane超过viewer的最大距离后不会自动消失，我们需要继续完善回到我们的EndlessTerrain脚本，我们可以发现 UpdateVisibleBlocks方法里，仅仅循环判断当前帧小于等于 BlocksVisibleInViewDistance范围内的区块，在新的一帧里，上一帧生成的一些区块，有的超出x,yOffset范围的blocks并没有纳入整个for循环判断，因此无法对这些blocks执行 SetVisible（false）操作，viewer移动的越快，上一帧生成的区块，超出当前帧视野范围的就越多，就会导致大量的blocks不会消失，这就需要我们新建一个List清单，记录上一帧生成的所有blocks，在执行当前帧循环判断前，将上一帧所有的blocks 进行 setVisible（false）操作，然后清空List里面所有blocks。当然，你也可以将当前帧生成的视距内的blocks和上一帧生成的blocks全统计起来，最后一并执行 UpdateTerrainBlock方法，统一筛选，也是可以的，只是后者会增大Dictionary空间，增加判断次数，这里不太推荐使用。我们在EndlessTerrain脚本开头申明一个新的List数据集合，因为只需要存储blocks信息，List足够胜任List<TerrainBlock>olderTerrainBlocks=new List<TerrainBlock>();然后在 UpdateVisibleBlocks方法里，循环判断前，将所有上一帧生成的blocks进行隐藏操作：for (int i=0;i<olderTerrainBlocks.Count;i++){ olderTerrainBlocks[i].SetVisible(false); } olderTerrainBlocks.Clear();//，操作完毕，清空List；然后，对当前帧所有 SetVisible（true）的blocks添加进入 olderTerrainBlocks List里面：那么我们在TerrainBlock类中新增一个判断方法，检测所有可视状态blockspublic bool IsVisible() { return meshObject.activeSelf; }回到UpdateVisibleBlocks方法里的循环体，对所有ContainsKey符合要求的字典内Blocks，进行判断，将所有已存在的acitve的blocks加入olderTerrainBlocks列表，方便下一次执行的时候，再次对这些显示的blocks进行 SetVisible（false）操作在if （terrainBlockDictionary.ContainsKey（viewedBlockCoordinate)){ terrainBlockDictionary[viewedBlockCoordinate].UpdateTerrainBlock();里面新增判断：if（terrainBlockDictionary[viewedBlockCoordinate].IsVisible()){ olderTerrainBlocks.Add(terrainBlocksDictionary[viewedBlockCoordinate]) }这样，每次执行前，上一帧的所有blocks就可以设置隐藏了，保存后测试，功能正常，最后，我们注意到左侧Hirarchy面板生成太多plane到根目录，我们将他们生成位置移到我们的MapGenerator物体内，作为子物体，保持美观：回到EndlessTerrain脚本我们对TerrainBlock结构进行优化，传入一个新参数：Transform parent;然后内部构造新增：meshObject.transform.parent=parent;//对所有新增meshObject plane物体，设置父物体为当前脚本挂载的MapGenerator物体然后找到调用TerrainBlock的语句，在UpdateVisibleBlocks循环体里面，对于没有在字典里存在的符合显示的blocks，我们进行实例化一个新的TerrainBlock，这里也传入Transform parent即可： else{ terrainBlockDictionary.Add (viewedBlockCoordinate,new TerrainBlock(viewedBlockCoordinate,blockSize,transform)); //在这里传入新参transform即可实现所有新实例化出来的plane都生成于当前物体下再次保存，测试，发现所有plane都位于MapGenerator物体下了，我们选择Viewer物体，拖动测试，最后的结果比较ok了虽然这节我们的收获成果只有这么个破方块，但是对于后面的无限地形系统来说，这节是非常重要的基础框架，无限地形动态视野控制区块功能，算是核心模块了。编程的核心内容经常是枯燥的，这也是成为高级程序员必经之路，那些堆砌美工和素材弄出来很精致的画面的demo，很多新手上手几个月堆砌各种素材就能做出来，然而对于核心系统的构建，功能基底的实现，程序员的思维，才是你与众不同的地方，也决定着你今后能做多深入的项目。好了，区块视野控制完成，下一节我们可以开始多区块绘制渲染了，这会牵扯到多线程思想，如果是和我一样的新人，建议自己多找下相关资料学习下，这个非常重要的，有时间的话多了解一些没坏处。下一篇：[post cid="406" /]
【Unity 3D进阶教程分享】EndlessTerrain无限地形搭建 第七节：LOD多细节层次渲染功能实现
第七节 LOD多细节层次渲染功能实现上一篇：[post cid="394" /]我们继续上节内容，如何将平面plane mesh网格拉高形成立体网格，这需要新增变量，高度倍数，打开MeshGenerator脚本，GenerateTerrainMesh方法传入新增 float heightMultiplier方法内获取meshData.vertices顶点信息时将y轴信息加上加倍系数heightMap[x,y]乘上刚才新增的 heightMultiplier然后打开MapGenerator脚本,新增同样的加倍因子public float meshHeightMultiplier;在下方display drawMode里面display.DrawMesh(MeshGenerator.GenerateTerrainMesh方法里传入meshHeightMultiplier保存，然后点击MapGenerator物体，inspector面板可以看到咱们的MeshHeightMultiplier系数出来了，调整数值大小可以看见我们的mesh网格就随之起伏了，通过观察可以发现，水域地形也随着起伏剧烈，这和自然环境不太相符，我们应该分区域调整multiplier系数影响比例，水域范围起伏波动应该缓和一些这样我们可以通过加入一条变化曲线（curve）来模拟。打开MapGenerator脚本public AnimationCurve meshHeightCurve;回到unity，我们应该可以在inspector面板看到我们的curve选项了，打开curve选择第三条曲线，0.4这里右键设置新的拐点然后回到脚本，display.DrawMesh方法里，传入的GenerateTerrainMesh方法新增传入一个meshHeightCurve参数MeshGenerator脚本里面对应方法同样也要加入此参数传入：AnimationCurve heightCurve，然后在此方法里进行实现，原来的meshData.vertices获取数值，y轴存储的是 heightMap[x,y]信息，现在要将传入的heightCure曲线信息约束咱们的 heightMap[x,y]后的值存储为y轴数值，于是改此信息为：heightCurve.Evaluate(heightMap[x,y])*heightMultiplierheightCurve.Evaluate就是求出对应括号内位置curve曲线对应该点的数值，再加成高度倍数就拿到了更自然的高度倍数因子了保存，回到unity，点击generate看看效果，若不满意可以微调我们的曲线，达到满意的表现立体拉升效果实现完毕，接下来我们优化下多块地形细节渲染分层，为后面的无限地形生成奠定基础。这里用到的LOD技术，在游戏领域应用非常广泛，我们通过修改不同视野范围的三维模型mesh网格数，来降低系统渲染负荷，优化游戏体验，对远处或者不太容易关注到的模型降低细节表现来获得帧数的提升我们先解释下实现mesh分层的原理之前我们的meshGenerator脚本，存储mesh顶点信息的时候，用到的遍历累加是for (int x=0;x<width,x ++){//创建顶点}那么假设我们width是9，从0~8一共9个点，当index每次累加1的时候，将会逐步累加存储从0-8总共8个顶点信息和对应三角面那么如果我们index遍历规则改为i=2，跳过相邻点，则会存储五个点信息如果i=4，则会存储三个点信息当i=3这种情况，我们会发现需要第十个不存在的点来存储，这明显是不可取的，所以这就是细节划分的限制：简单来说，我们需要找一个公因数，即LOD分层index累加数值和我们地图width宽度值，两者公有的因数我们的演示体现了index和vertices顶点数关系，i=1，vertices个数为9i=2，vertices个数为5i=4，vertices个数为3我们需要找到通用的关系式i=1，v=ii=2，我们观察左边4条曲线代表四个循环，都是被i=2切分后分成了4条，每段记录一个顶点我们可以尝试将总宽度减掉最右边的1个，再除以i=2，获得左边所有切分后记录的顶点数，再补上最右边的点+1，公式可以是 vertices=[(width-1)/i]+1i=4 时候，我们验证下，减去最右侧终点一个后，除以跳跃值i=4，获得2个循环分块，每个分块存储1个顶点信息，最后加上最右侧的终点，结果是3个，和实际相符。公式是可用的下面解释下为何不超过65000个顶点数：一般的，我们 vertices=width*height对于方形地图 vertice=width^2v<=255^2(65025)本来实际限制是 256^2-1即65535，作为边界取值的话，拿不到256这个值，所以最大的正方形就是255的平方了回到我们目前的情况我们上一节提到过，每一行最后一个点不用来记录绘制三角面，那么实际进行顶点计算的点应该是 width-1width-1和i这个index跳跃度系数找最大公倍数，我们分析如下：我们要思考一个优先度，i跳跃系数，能多几档调节为优，能集中在小跳跃范围为优，什么意思呢清晰度细节显示调节档位，如果就三四档，明显不够细腻，变化突兀太明显第二个，index跳跃系数为何尽量限制在较小范围内？如果跳跃值太大，比如说240的宽度你来个40，80甚至120，就绘制出来几个点，太精简了没啥实际意义，所以说，我们理想来说，最好是7，8个档位，能尽量集中在20以内，使得240的宽度也能有基本12个顶点进行绘制，最粗糙的mesh也可以忍受，这样实际应用起来就比较可行了考虑到最大公倍数的原理，我们最好找一批偶数或者相互都有公因数的一批数字或者一批奇数搭配一批偶数，会比较理想，毕竟全奇数的话，20以内3，5，7，11这些破数字很多互相没有啥重复的公因数，这样很难找到一个255以内的公倍数来实现效果通过一番鬼畜的推导最终结果：我们获得可行的 （width-1）最大合适数值是240i的取值可以为1，2，3，4，5，6，8，10，12，15.接下来看自己理念来筛选了上面十个档位对应顶点数依次为240 ，120，80，60，48，40，30，24，20，16.对于这个结果，我们继续分析：我个人觉得高精度过度到低精度开始阶段最好多分细腻过渡合适一些，所以保留2，3，4，从第五档开始，可以认为是中距离视野，5，10舍弃，采用6，8，12间隔跳，远视野设一档15即可那么我们可以得到顶点数依次为240 ，120，80，60，40，30，20，16.也就是我们留下1，2，3，4，6，8，12，15这八档，对应顶点数为240 ，120，80，60，40，30，20，16.width和i都筛选合适之后，我们继续打开MapGenerator脚本我们定义一个常量，这是非常核心的数值，为了防止手贱乱赋值修改，也为了减轻系统负担，我们后期不会去变动这个数值，而且随机地图生成随时都会调用这个常量进行每个区块地图生成，所以这里定义为const常量：public const int mapBlockSize=241;为了尽可能生成最大单块地图，我们将定义每个区块地图为正方形尺寸，所以所有的mapWidth和mapHeight都修改成这个BlockSize好了选择mapWidth变量，ctrl+r+r全部重命名为mapBlockSize，对于mapHeight变量，同样操作，也重命名，搞定之后，删除这两个int值，只保留一个const mapBlockSize即可再新申明一个LOD变量，用来存储我们刚才的八档LOD值[Range(0,7)] public int levelOfDetail;再去下方MapDisplay方法中 drawMesh里面调用的GenerateTerrainMesh方法增加一个传入参数：levelOfDetail最下方我们用OnValidate方法约束mapBlockSize现在也没有必要了，已经锁定为常量241了，删除控制语句然后回到MeshGenerator脚本里面，同步修改GenerateTerrainMesh方法传入新参数 int levelOfDetail该方法内再定义一个mesh渲染等级变量int meshDetailStage = (levelOfDetail == 0) ? 1 : (levelOfDetail == 1) ? 2 : (levelOfDetail == 2) ? 3 : (levelOfDetail == 3) ?4 : (levelOfDetail == 4) ? 6 : (levelOfDetail == 5) ? 8 : (levelOfDetail == 6) ? 12 : 15 ;好吧，我也觉得这代码丑爆了，能力有限，目前只能写出这档次的代码了，希望有大佬指点一下然后将下方x，y循环遍历存储语句y++,x++都改为 y+=meshDetailStage然后新定义一个每行顶点数，公式就是之前说的 [(width-1)/i]+1，这里的i就是刚才的meshDetailStage了:int verticesEachLine = (width-1)/meshDetailStage+1;然后下方我们之前 MeshData meshData = new MeshData(width,height);实例化新的meshData数据传入的是之前的宽和高，这里都改成我们的 verticeEachLine，然后就是x，y循环遍历里面，我们 AddTraiangle方法里面，width也要改成现在的 verticeEachLine，总共应该是三处，改好后，回到unity选中Map Generator物体，然后点右侧Inspector面板的Generate生成我们全新的241尺寸的正方形地图，Scene界面修改shaded渲染为mesh网格渲染模式，然后通过调整meshDetailStage查看网格精细度变化情况stage调的档位越高，mesh就越简单，适合拉远镜头，看远景效果还是ok的，这样，我们LevelOfDetail一个简单的实现效果就ok了，下一节，我们将进行多线程协程处理多个区块地图，为不同视野范围区块做不同的LOD mesh网格渲染，进一步走向我们的无限地图生成目标下一篇：[post cid="403" /]
【Unity 3D进阶教程分享】EndlessTerrain无限地形搭建 第六节：地形Mesh网格生成器实现
第六节 地形Mesh网格生成器实现上一篇：[post cid="389" /]上一节，我们已经生成了一张图片显示彩色分区域图和黑白高度图，这一节，我们将生成一个plane，来构造地形的mesh网格基础，通过对不同区域mesh网格顶点坐标设置不同的height值，来形成真实地形基本构造我们在第一节的时候提到过x，y轴尺寸和顶点数，三角面以及矩形方块计算的数学式子关系，这里再温习一下：再次查阅我们的【总顶点数构图】从左下角原点开始，顺时针方向，第一个三角形是 （0，5，1），第二个为（1，5，6），接下来是（1，6，2），（2，6，7）依次类推顶点数 vertices=width*height,而三角形顶点总数则为 （width-1）*（height-1）*2*3//方块数=》三角形个数=》顶点总数。这也是记录我们三角形顶点总数数组所需长度Length的式子。温习了这些数学关系之后，我们新建MeshGenerator脚本这个也不需要其他脚本进行修改，改成static类，不需要在unity世界直接展示，去掉:MonoBehaviour继承public static void GenerateTerrainMesh(float[,] heightMap){ int width =heightMap.GetLength (0);////获取数组第一维元素长度值，赋值给width int height = heightMap.GetLength(1);////获取数组第二维元素长度值，赋值给height for (int y=0; y<height; y++){ for (int x =0; x<width; x++){ } } }为了方便记录我们的网格数据，在当前类最外面，后面新建一个网格数据类，方便后面的数据保存调用：public class MeshData { public Vector3[] vertices; public int[] triangles; int triangleIndex;//构建AddTriangle方法时，在此申明一个int变量作为三角面索引值 public MeshData (int meshWidth, int meshHeight) { //网格数据存储顶点和三角面信息 vertices = new Vector3[meshWidth * meshHeight]; triangles = new int[(meshWidth - 1) * (meshHeight - 1) * 6]; } public void AddTriangle(int a, int b, int c) { //新建一个添加三角面的方法，a，b，c代替，简化后面的表达式。 triangles[triangleIndex] = a; triangles[triangleIndex + 1] = b; triangles[triangleIndex + 2] = c; triangleIndex += 3; } }完成了MeshData类的构建，我们可以继续完善GenerateTerrainMesh方法了：public static void GenerateTerrainMesh(float[,] heightMap){ int width =heightMap.GetLength (0); int height = heightMap.GetLength(1); //在这继续输入： MeshData meshData =new MeshData (width,height); int vertexIndex=0; //再将遍历循环内加入 meshData.vertices[vertextIndex]=new Vector3(x,heightMap[x,y],y);//将高度图信息存储在(x,z)平面上 vertexIndex ++;接下来，我们如果想让mesh网格顶点居中显示，如下图效果的话需要对居中计算公式做一下推导：假设有一排三个像素点，我们想要中间的点居中显示，则中间点的x轴坐标值设为0，那么左边的点x轴的值就是-1，右边则为1.那么计算最左边的像素点的坐标值公式可以是：首先整个x轴总像素点数width-1得到对应轴的长度，然后对半分/2，就获得左右两半对应的长度值，对应就是最右侧的点的坐标值:x=(width-1)/2，居中计算公式验证：我们可以验证一下，总共9个点的话，9-1拿掉最中间的5号，再除以2得到左右两侧平分为4，最右侧坐标4，这是符合我们预期的。当然，如果总像素点是偶数的时候，比如8，计算得到最右侧坐标值3.5，也是ok的。我们在public static void GenerateTerrainMesh(float[,] heightMap){方法中继续完善我们的mesh网格居中计算，需要用到两个新变量定义最右侧的x坐标值以及最上端Z轴坐标值float maxX=(width-1)/2f; float maxZ=(height-1)/2f;接下来修正我们的meshData,vertices[vertexIndex]存储位置，使其居中：meshData.vertices[vertextIndex]=new Vector3(x,heightMap[x,y],y);改为：meshData.vertices[vertexIndex]=new Vector3(x-maxX,heightMap[x,y],y-maxZ);接下来我们查看演示：用0号点管理第一个方块中2个三角形，1号管理对应两个，最右侧坐标点留空不计算，最后y=height时上方边界也留空不进行计算。我们再思考下，顶点index索引我们是设为i开始遍历，我们将index和之前的三角形构建公式结合起来，我们比方说第一排中间任意一个顶点索引值i，右侧类推就是i+1，i+2，那么一排遍历完毕，往上第二排同列位置就应该是i+width，往右侧类推就是i+width+1，i+width+2；那么我们第一个三角形记录下来就是（i，i+width,i+1），第二个就是（i+1,i+width,i+width+1),以此类推，等于是再次温习下之前提到的三角形数组记录：（i+1,i+width+1,i+2),(i+2,i+width+1,i+width+2)分析完毕，我们在vertexIndex++;之前，构建我们的三角形网格： if (x < width -1&& y < height -1) {//最右侧像素点和上方边界不进行绘制 meshData.AddTriangle(vertexIndex, vertexIndex + width, vertexIndex + 1); meshData.AddTriangle(vertexIndex + 1, vertexIndex + width, vertexIndex + width + 1); meshData.AddTriangle(vertexIndex, vertexIndex + width, vertexIndex + 1); } vertexIndex++;这样，我们网格绘制基础方块就完成了接下来我们思考下UV地图绘制，有了UV贴图，就等于告诉unity我们的二维图片该以何种顺序和方向布局到我们的三维物体表面，这样我们就可以将材质贴到我们的mesh网格上了，这里用到的是plane，所以uv构建是比较简单的，以后接触三维模型制作的时候，uv就需要专门的软件进行自动或者手动引导设置一块块uv布局了，unity就难以胜任复杂模型快速方便地标记uv贴图了。好了，接下来，我们去MeshData类里面新申明一个uv 变量：public Vector2[] uvs;MeshData构造里面新增：uvs =new Vector2[meshWidth*meshHeight];然后回到GenerateTerrainMesh方法里面meshData.vertices顶点绘制语句后，补充uv绘制：meshData.uvs[vertexIndex]=new Vector2(x/(float)width,y/(float)height);平面的三维模型uv贴图绘制还是非常简单的，这样就ok了接下来，我们构建一个mesh绘制方法，对我们刚才已经完毕的meshData进行加工，绘制出Mesh网格在MeshData类里面新增一个公开方法：public Mesh CreateMesh(){ Mesh mesh = new Mesh(); mesh.vertices=vertices; mesh.triangles=triangles; mesh.uv=uvs; mesh.RecalculateNormals();//重新计算法线，使得所有三角面方向正常显示，从而顺利计算光线反射。 return mesh;//最后传出绘制好顶点，三角面，uv贴图和正确法线分部的mesh信息。然后回到我们的静态方法GenerateTerrainMesh，我们现在已经构建完毕MeshData类，获得MeshData数据了，那么此方法就可以传出MeshData数据了，将无传出的void改为MeshData，并在此方法最后补上return meshData;有人会问为什么我们不直接传递出来mesh信息，而是传递整个meshData类干嘛这是因为后面我们创建多个mesh地图块调用多线程计算的时候，不能直接在每个线程里面直接创建或者调用mesh信息，只能将整个类数据传回unity主线程，然后在主线程里进行unity组件类型的调用，所以传出整个meshData类，方便后期主线程调用获取内部的mesh信息。目前MeshGenerator里面基本就是这样了，我们回到MapGenerator脚本里面新增一个DrawMode：Meshpublic enum DrawMode{NoiseMap,ColourMap,Mesh};//新增Draw Mesh枚举内容；接下来在GenerateMap（）方法中新增MapDisplay渲染，如果drawMode不是NoiseMap，ColourMap的话，加入判断：else if (drawMode==DrawMode.Mesh){ display.DrawMesh(MeshGenerator.GenerateTerrainMesh(noiseMap),TextureGenerator.TextureFromColourMap(colourMap,mapWidth,mapHeight));//DrawMesh方法我们还没有构建，先写在这里好了。我们去MapDisplay脚本，把DrawMesh方法构建出来申明两个变量：public MeshFilter meshFilter;//引导mesh信息传递 public MeshRenderer meshRenderer;//mesh信息导入后进行渲染 public void DrawMesh(MeshData meshData , Texture2D texture){ //开始构建绘制mesh方法 meshFilter.sharedMesh=meshData.CreateMesh(); meshRenderer.sharedMaterial.mainTexture=texture;//由于我们可能会在脚本之外会调用修改网格贴图材质，所以这里都采用共用网格和材质类型回到unity，我们新建一个空物体，命名为Mesh，物体新增两个组件：meshFilter和meshRendererAssets里面，Materials文件夹下新建一个材质球，命名Mash Mat；材质球smooth度调成0，让边缘清晰，点Mesh物体，将Mesh Renderer组件下的材质栏打开，拖放我们新建的材质球到这里，然后点击Hierarchy面板的Map Generator物体，挂载的Map Display脚本，我们新建的Mesh Filter和Mesh Renderer字段，都挂载上我们的Mesh物体，这样我们就可以在Inspector面板选择脚本里面的DrawMode模式为Mesh绘制，应该Scene场景就可以看见我们绘制出来的网格了之后我们对这个mesh网格进行高度拉升就可以实现基本的三维地形绘制了，现在我们先把Mesh的Scale改成10，10，10好匹配我们的地图新加一个光源，让效果更舒适,这里目前有一个小问题，我们绘制出来的mesh网格贴图和单独的彩色材质图是180度倒置的，造成此问题的原因目前我的能力无法找到，我们可以通过将MeshGenerator脚本里面meshData.vertices[vertexIndex]=new Vector3(x-maxX,heightMap[x,y],y-maxZ);顶点坐标进行x，y轴翻转，可以得到正确的结果：meshData.vertices[vertexIndex]=new Vector3(maxX-x,heightMap[x,y],maxZ-y);希望有大佬指点下原因，非常感谢(☆ω☆)。好了，下一节我们再讲解如何拉升mesh网格高度，目前来说，我们基本的mesh绘制已经完成，当然对于无限生成的大地形目标来说，目前这个受到unity自身65000个顶点单地形生成限制，如果我们在Inspector面板Map Generator脚本上修改Map Width和Map Height 超过256，则会出现一些鬼畜的问题这在后面的分享中我们会通过分割mesh地图块来实现无限地图生成。这节到此结束。下一篇：[post cid="400" /]
【Unity 3D进阶教程分享】EndlessTerrain无限地形搭建 第五节：对生成的灰度图进行上色填充
第五节 对生成的灰度图进行上色填充上一篇：[post cid="387" /]我们根据不同灰度进行上色填充，为后面的三维地形构建打好基础进入mapGenerator脚本，在最后新建结构体：public struct TerrainType{ public string name; public float height; public Color colour; }为了让此结构体各项属性能够方便地在inspector面板进行展示调整，我们用到[system.Serializable]申明在结构体上方。之前我们知道，对类里面一些基本属性，成员变量进行序列化，用serializefield指令即可而结构体这些是我们自己构建的较为复杂高级的结构，并不是从MonoBehavior派生出来的，默认不会被unity3d识别为可以序列化的结构，通过添加 [System.Serializable]属性，让unity3d检测并且注册这些自建结构为可以序列化类型Serializable只可以对 class类,struct结构体,enum枚举,delegate委托，这些比较复杂的类型进行序列化，不可以对常见的属性序列化简单来说，普通常用的属性，变量用 serializefield，复杂点的结构体，自定义类或者其他代码体用 serialiable申明注册为可序列化对象。好了，介绍完毕 serializable，我们继续，结构体初建完毕，对应的，我们新增一个数组，记录地形不同区域public TerrainType[] regions;保存，进入Map Generator物体，我们可以将TerrianType数组大小size设置为2，即构建2个区域，进行初步测试。name对应为Water，Land，对应Color为蓝色系，绿色系即可。Height为0.4，1，代表从0-0.4就是水域，0.4-1就是陆地，这样就可以着手和上一节获得的灰度图进行数值匹配输出了回到MapGenerator脚本，在我们调用的float[,]noiseMap数组后，就可以开始我们喜闻乐见的遍历数据了for (int y = 0; y < mapHeight; y++){ for (int x = 0; x < mapWidth; x++) { float currentHeight = noiseMap[x, y];//取出数组中的值，作为当前高度进行分析 for (int i = 0; i < regions.Length; i++) { if (currentHeight <= regions[i].height) { break; } } } }break之前，我们需要对颜色进行存储，我们参考下MapDisplay脚本，我们之前在这个类里面对noise数组的值做了普通的黑白配色灰度图，那么在MapGenerator里面，我们现在也需要做类似的功能，对数组值进行刚才的2个regions的水域蓝色和陆地绿色上色，代码可以仿照着写:那么开始仿照灰度图生成方法，①开始构建彩色图尺寸：在这个for循环外部，增加：Color[]colourMap =new Color [mapWidth*mapHeight];接下来我们在循环内部，break之前添加：②遍历所有像素点，填充对应区域色彩colourMap[y*mapWidth+x]=regions[i].colour;这样，我们所有的noiseMap数组里的点，通过regions筛选，储存对应色彩到咱们的colourMap数组里了我们想要生成彩色地图，这就实现了目前我们在测试阶段，灰度图生成方式还是需要暂时保留着的，所以我们可以重写绘制方法，这里用到enum枚举，新建一个enumpublic enum DrawMode {NoiseMap,ColourMap};枚举的含义就是自己定义的集合，方便管理整理归类一些变量，方法，结构体都行，里面的每个元素，还是它本身的取值范围或者定义规则，相当于自己新建一个大箩筐把需要一起管理的数据类型，各种东西放一起，方便选取。再定义一个变量来执行枚举功能：public DrawMode drawMode;再来到下方，我们 display.DrawNoiseMap(noiseMap);显示绘制最终地图功能上方，先进行枚举选择判断支分析：if (drawMode==DrawMode.NoiseMap){ display.DrawNoiseMap(noiseMap); }else if (drawMode ==DrawMode.ColourMap){ //在绘制灰度图和彩色图功能实现之前，考虑到MapDisplay脚本功能是最后输出显示， //而我们当前Display脚本却包括了黑白上色功能， //如果把接下来彩色上色功能也写在这里面，那就有比较完整的材质渲染模块代码了， //这和脚本名字功能不太相符，可以考虑将功能细分，新建一个材质生成脚本， //通过generator脚本下达输出对应色彩地图指令，传递给材质生成脚本里面生成对应材质贴图， //最后传递给display脚本进行渲染输出显示，这样代码结构会更加明晰。 }好了，那么我们先新建一个脚本：TextureGenerator新建2个方法，对传入的地图信息进行绘制材质，一个是彩色图方法，一个是高度图这也是一个静态的固定功能实现脚本，不需要进行外部修改(static)，同时也不需要被unity3d场景直接调用(不继承mono），public static class TextureGenerator { public static Texture2D TextureFromColourMap(Color[] colourMap, int width, int height) { Texture2D texture = new Texture2D(width, height); texture.SetPixels(colourMap); texture.Apply();//存储地图像素到材质的每个像素里面之后别忘记应用，才能生效； return texture;//传出已经存储好了材质信息的材质。 } }接下来是高度图绘制方法：public static Texture2D TextureFromHeightMap(float[,]heightMap){ //将MapDisplay里面DrawNoiseMap方法相关代码内容剪贴出来，粘贴到这里, //将材质生成功能彻底拆分 出来, 别忘记对粘贴过来的代码进行一些细节修正 int width =noiseMap.GetLength(0); int height =noiseMap.GetLength(1); Texture2D texture = new Texture2D (width,height); Color[[ colourMap = new Color [width*height]; for (int y=0;y< height;y++{ for(x=0;x<width;x++){ colourMap[y*width+x]=Color.Lerp(Color.black,Color.white , noiseMap[x,y]); } } texture.SetPixels (colourMap); texture.Apply ();对路径进行修正：总共三处noiseMap改成HeightMap由于我们TextureGenerator只需要填充计算灰度信息到我们的地图数组中，不需要生成材质，这是display渲染方面的任务，于是删除复制的代码中多余的与Display渲染显示相关代码：Texture2D texture = new Texture2D (width,height); texture.SetPixels (colourMap); texture.Apply (); 删除这三段最后我们的TextureGenerator最后是生成高度图的材质信息，返回相关灰度图数组和对应的数组宽高度信息即可：return TexutreFromColourMap(colourMap,width,height);我们回到MapDisplay脚本，对原进行重命名和功能修正：我们现在只负责材质渲染输出功能了：public void DrawNoiseMap改为 DrawTexture传入的参数也变为texture变量：(Texture2D texture)渲染出来的相应的宽度高度也变成对应材质的宽高度：textureRender.sharedMaterial.mainTexture=texture; textureRender.transform.localDcale= new Vector3(texutre.width,1,texture.height);然后回到MapGenerator里面，原来的drawMode为NoiseMap时候，调用的 display.DrawNoiseMap(noiseMap)也要改为 display.DrawTexture(TextureGenerator.TextureFromHeightMap(noiseMap));传入我们TextureGenerator脚本里对应的材质信息，然后我们可以补齐我们的drawMode为colourMap时候的代码了：}else if (drawMode ==DrawMode.ColourMap){ display.DrawTexture(TextureGenerator.TextureFromColourMap(colourMap,mapWidth,mapHeight));同样也是传入TextureGenerator脚本里对应的材质信息保存，unity里面Inspector drawMode选择为NoiseMap或者ColourMap，观察两种地图都已经顺利生成。仔细观察colourMap渲染两种色彩相接处会比较模糊，地图四周边界色彩也不太美观，会溢出显示另一边的颜色，因为默认渲染填充色彩采用的是bilinear双线性插值渲染，我们改为单点point插值，这样每个像素点会明晰呈方块化一些，改材质循环模式为Clamp（强制拉伸），也会比默认的Repeat（重复模式）填充的边界来的更精准，不会出现地图边界色彩越位现象。回到TextureGenerator脚本，我们在TextureFromColourMap静态方法中SetPixels前先修改下材质显示模式，增加texture.filterMode=FilterMode.Point; texture.wrapMode=TextureWrapMode.Clamp;保存，再回到unity界面查看，色彩渲染成标准正方块，地图边界也不会出现越界渐变色彩了我们接下来手动增加regions，最终设置8个区域，依次为Water Deap,Water Shallow,Sand,Grass,Grass 2,Rock,Rock2,Snow,对应Height范围0.3,0.4,0.45,0.55,0.6,0.7,0.9,1,对应颜色为深蓝色，浅蓝色，沙子黄，浅绿，深绿，浅褐色，深褐色，白色。好了，我们基本的灰度图和彩色图生成功能已经实现，目前存在一个明显的问题就是，如果我在region区域中间新增一个区域，那就比较蛋疼了，得手动修改后推所有的已存在region信息，那么，我们在以后的章节里考虑制作一个自定义编辑器，让region上色和增加区域更加方便快捷下一篇：[post cid="394" /]
【Unity 3D进阶教程分享】EndlessTerrain无限地形搭建 第四节：Octave倍频分层叠加地形设计原理及基本实现
第四节 Octave倍频分层叠加地形设计原理及基本实现上一篇：[post cid="385" /]对noiseMap数值获取进行合理化控制，获得合适的自然变化数据这次我们对上一节GenerateNoiseMap方法进行完善，添加我们的octave层数，persistance持续度，lacunarity孔隙度三个变量。我们对Noise脚本原方法进行更新：修改为public static float[,] GenerateNoiseMap(int mapWidth, int mapHight,float scale，int octaves，float persistance，float lacunarity){//先在此新加三个变量传入对应参数 float [,] noiseMap = new float[mapWidth,mapHeight]; if (scale<=0){ scale=0.0001f; } for (int y=0; y<mapHeight;y++){ for(int x =0;x<mapWidth;x++){ float amplitude =1; //给初始振幅和频率赋值为1，初始高度为0，通过下方noiseHeight累加 float frequency = 1;//获得加持了振幅后的柏林噪音高度。 float noiseHeight =0; for (int i=0; i< octaves;i++){ //对不同层进行不同叠加处理，先写好遍历框架，后面补充内部代码，Ctrl+E,D:自动对齐框选内容的格式 float smapleX =x/scale*frequency;//再让频率限制因素也起效，在此对每个样本像素点加入频率因子，频率越大，每个样本点数值变化越剧烈。 float sampleY=y/scale*frequency; float perlinValue=MathfPerlinNoise (sampleX,sampleY); noiseMap[x,y]=perlinValue; noiseHeight +=perlinValue*amplitude;//原本柏林噪音地图仅仅存储默认的柏林噪音值，我们添加振幅因子进行高低范围波动控制。 amplitude *= persistance;//每轮循环后，让对应两个因子累乘，实现我们示意图所示的控制效果，振幅和频率分别受到持久度和间隙度的影响 frequency *= lacunarity;//对不同octave进行不同系数控制对应层数的振幅频率 } noiseMap [x,y] = noiseHeight;//将每个像素点地图数值存储到数组里 } } return noiseMap; }如果要想让起伏高低效果比较好看的话，我们最好让 float perlinValue=MathfPerlinNoise (sampleX,sampleY);这从 [-1,1]随机取值，默认的柏林噪音取值是 [0,1]随机出来都是正数，那么我们的地形3个octave全是正方向向上叠加，最后的效果就是一个0高度平地出现一些高山起伏，而要形成比较自然的山谷甚至海洋低位置自然起伏效果，应该有相应的往下方向的叠加，我们将核心柏林噪音函数取值范围修改为－1到+1区间：float perlinValue=MathfPerlinNoise (sampleX,sampleY)*2-1;这样，我们的Noise脚本就能存储到想要的noiseMap数据了接下来，我们要在x，y loop遍历中找到最低和最高高度：再通过InverseLerp反插约束，将数据范围调整回[0,1]区间。float maxNoiseHeight = float.MinValue; float minNoiseHeight = float.MaxValue;然后在遍历循环里处理完毕octaves每层地图数据后，做个判断，更新当前最大最小高度值：if (noiseHeight > maxNoiseHeight) { maxNoiseHeight = noiseHeight; } else if (noiseHeight < minNoiseHeight) { minNoiseHeight = noiseHeight; }这样我们在遍历的过程中，会不断记录当前最大最小高度值，最后结束获得整个地图的最大最小值，整个遍历结束，每个有效的柏林噪音数值都已经存储完毕，最后我们再遍历一遍noiseMap，通过 Mathf.InverseLerp对所有数值进行范围框定，将所有高度数值再次缩至[0,1]范围内，具体代码为：for (int y=0; y<mapHeight; y++){ for(int x =0; x< mapWidth; x++) { noiseMap [x,y]=Mathf.InverseLerp (minNoiseHeight,maxNoiseHeight, noiseMap[x,y]); } } //这段遍历插入到刚才最后的return返回map前，对地图数据进行范围约束。 return noiseMap;解释一下InverseLerp反插值什么原理，之前我们在城市建造demo里采用普通Lerp指令transform.position = Vector3.Lerp(startPos, targetPos, percentage);对镜头当前位置和目标位置做时间变化百分比线性插值，从开始点到目标点，percentage花费的时间占百分之几，就插值这段向量距离的百分之几，那么反插值和这个普通线性插值比较类似在这里，反插值计算公式：(noiseMap-min)/(max-min)，相当于我们noiseMap在前两个限制参数间的比例值，如果最大高度为100，最低高度为50，那么当我们实际noiseMap高度为80的时候，我们这个80位于50-100之间的比例范围就是80间隔50距离为30，总取值范围是100-50=50，30距离相对于总共50的总距离，比例值为0.6，即位于中间偏右一些。接下来对地图生成器也进行完善，将新的noise信息进行分析加工补充增加3个变量：public int octaves; public float persistance; public float lacunarity;然后在float[,]noiseMap数组获取数据传入这些数值，修改原：float[,] noiseMap=Noise.GenerateNoiseMap(mapWidth,mapHeight,noiseScale);为 float[,] noiseMap=Noise.GenerateNoiseMap(mapWidth,mapHeight,noiseScale,octaves,persistance,lacunarity);保存后，来到Hierarchy面板选择Map Generator物体，看Inspector面板，应该会新生成3项参数，我们对它们进行赋值Octaves 4 ，Persistance为0.5，Lacunarity 为2，随意拖动后它们的值，查看效果变化发现基本的生成功能没有问题了，我们最终目的是生成合适的地图后进行保存，当我们需要的时候，调用这个保存的地图就好，那么我们需要对随机结果进行存储，通常我们对这种随机后数据包命名为seed，种子，很多rpg游戏装备库，掉落率，随机生成的地牢信息，都是一个seed，读取seed内信息就可以获得相应配置数据，当然这种随机产生的seed经常会导致一个伪随机问题，比方说泰坦之旅里面生成一个新的角色后，seed固定，就决定了哪些装备你一辈子可能都刷不出来，最后含泪开新号继续测试非洲人。好了，简单说下为什么命名为seed，我们接下来去Noise脚本对应功能添加传入一个int seed值，并调取下系统的随机方法获得seed：首先Noise.GenerateNoiseMap方法添加传入一个int seed,在方法内新建一个系统随机数（Pseudorandom number generator):System.Random prng = new System.Random (seed);接下来我们还想让每一层octave从不同区域取值，需要加入一个偏移量：Vector2[] octaveOffsets = new Vector2[octaves]; for (int i= 0;i <octaves;i++){ float offsetX = prng.Next(-100000,100000);//偏移量随机取值控制一下取值范围，不需要太奇葩的数据 float offsetY= prng.Next(-100000,100000); octaveOffsets[i]=new Vector2(offsetX,offsetY);//获得对应每个octave层随机偏移向量然后我们需要将此随机生成的偏移量加入我们的smapleX，Y样本像素获取式子中，让我们的样本像素针对不同octave层数做出对应层的随机偏移。float smapleX =x/scale*frequency+octaveOffsets[i].x; float sampleY=y/scale*frequency+octaveOffsets[i].y;这样我们每一层octave都会获得一个X，Y轴方向随机的偏移量，随机范围则是-10万到10万，如果我们想通过手动修改Offset偏移量的x,y轴值，产生平移或者上下拖动效果，那么需要对此方法新增传入Vector2 offset参数，再通过Inspector面板进行调节。修改我们octaveOFfsets的offsetX,offsetY获取式子，加入offset.x,y,使之生效：float offsetX = prng.Next(-100000,100000)+offset.x; float offsetY= prng.Next(-100000,100000)+offset.y;这样我们在随机产生的偏移量基础上，还增加了手动产生偏移量的面板操作入口。Noise获取已经修改完毕，我们去Generator脚本进行同步扩充。public int seed; public Vector2 offset;同时将调用的Noise脚本下的GeneratoNoiseMap方法新增传入seed和offset值，最后效果为： float[,] noiseMap=Noise.GenerateNoiseMap(mapWidth,mapHeight,seed,noiseScale,octaves,persistance.lacunarity,offset);保存后测试，发现调节seed值，我们可以获得完全随机的不同柏林噪音地图了然后手动修改offset的x，y值，也可以顺利实现对生成的地图进行水平和竖直方向平移，最后我们可以做个小优化，当我们改变Noise Scale的时候，尺寸的缩放是相对于地图右上角进行的，如果是针对地图中心进行缩放，效果会更理想，我们对代码进行下改进：观察Noise脚本里面我们每个样本像素点获取式子：float smapleX =x/scale*frequency+octaveOffsets[i].x; float sampleY=y/scale*frequency+octaveOffsets[i].y;我们的x，y取值范围是map总Width和Height，那么除以scale后，scale变化，我们地图会以整个Height和Width随着scale变化而发生同比例变化，缩放的参照点就是坐标（mapWidth,mapHeight)我们要将参照点移至map中心，最简单的方法就是让参照点相对于50%的width和height进行平移，计算式子改为float smapleX =(x-halfWidth)/scale*frequency+octaveOffsets[i].x; float sampleY=(y-halfHeight)/scale*frequency+octaveOffsets[i].y;同时此for 循环外部补充两个变量定义：float halfWidth=mapWidth/2f; float halfHeight=mapHeight/2f;保存，测试，发现缩放效果就ok了最后我们做一点收尾优化，对面板数值调整范围做一下约束：我们对map大小数值进行范围框定，height和width是应该大于0的进入mapGenerator脚本void OnValidate(){ if(mapWidth<1){ mapWidth=1; } if(mapHeight<1){ mapHeight=1; }Onvalidate方法就是Inspector面板发生数值改变时候自动调用，这个范围限制实现也有其他方法可以约束，不过这里采用比较简单的方法进行实现。保存，测试，发现控制成功还剩下其他的数值，也进行下范围控制，间隙度是控制频率的，需要大于1：if(lacunarity<1){ lacunarity=1; } if(octaves<0){ octaves=0; //octave倍频，不能为负数。 }最后持久度persistance应该在(0-1)之间，控制振幅的范围，那么public float persistance定义前加入 [Range(0,1)]即可我们的基本的柏林噪音地图生成算是基本完善了最后提醒一点，我们回到float smapleX =(x-halfWidth)/scale*frequency+octaveOffsets[i].x; float sampleY=(y-halfHeight)/scale*frequency+octaveOffsets[i].y;这个表达式中，我们的Offset并没有受到scale和frequency的约束，导致左右横移或者上下滚动的时候，会在外部直接破坏原scale*frequency约束下形成的整体框架布局，将+octaveOffsets[i].x,.y放置于前方括号内部，改成：float smapleX =(x+octaveOffsets[i].x)/scale*frequency; float sampleY=(y+octaveOffsets[i].y)/scale*frequency;这样我们调整offset值后，最后偏移效果就是在/scale*frequency这个框架内，做出纯粹的x，y方向移动对应数值距离的效果，具体效果我们在后面上色彩图后能够看出明显区别，到时候考虑到无限地形生成，我们mesh网格绘制时需要保持每个区块已经生成地形不再发生变化，就要修正这个式子了，有兴趣的可以不做这个修正，在后面第十一章节发现问题后再修复对比下下一篇：[post cid="389" /]
【Unity 3D进阶教程分享】EndlessTerrain无限地形搭建 第三节：基于柏林噪音生成灰度图
第三节 基于柏林噪音生成灰度图上一篇：[post cid="380" /]柏林噪音介绍octave倍频波形叠加技术讲解上一节我们提到了Mathf方法中的PerlinNoise函数，这里具体讲解下什么是柏林噪音以及如何通过柏林噪音生成灰度图，为接下来地形生成做准备普通噪音，类似电视上的雪花点，杂乱随机黑白像素，柏林噪音则是连续的黑白渐变像素表现。取一段柏林噪音变化横截面切片，对应的数值曲线，类似于高山谷地曲线变化，这可以作为我们的地形构造基础元素。y轴为振幅（ampitude)，x轴为频率(frenquency)，波形的基础常识单条柏林噪音曲线，要构造我们的地形，明显过于平滑，我们需要一些细节我们通常采用多层柏林噪音曲线叠加，对不同层数进行限定进行局部地形模拟，最后获得比较真实的地形模拟效果，这个方法叫做多层波形倍频Octave技术这里讲解一个基本的Octave倍频多层波形叠加实现三个序列octave波形层，我们构想三块地形部分模拟傅立叶提出“任何连续周期信号可以由一组适当的正弦曲线组合而成”大家对波形转换滤波方法感兴趣的话可以研究下傅里叶各种转换公式，同时也可以感受下任何自然事物都可以由简单的共通元素叠加模拟出来的哲学思想，这里主要引入两个参数，对我们的柏林噪音波形进行控制一个是lacunarity孔隙度，对波形频率进行控制，frenquency=lacunarity^0,^1,^2 0次方1次方2次方，对三块地形波动频率进行修饰。孔隙度越大，地形信息越复杂。比方说孔隙度我们设为2，那么三个octave频率系数依次为1，2，4，那么最右侧的碎石模拟地形波纹将获得最复杂的4倍长度的波形，这对于我们的地形构造基本目标也是符合的，大地形信息少，小细节增加丰富信息。另一个参数则是Persistence持续度，用来表示每个频率内的振幅，用来修饰控制每一段波形的振幅波动剧烈情况。越小的持续度，波形变化起伏越平缓ampitude=persistance^0,^1,^2 0次方1次方2次方，对三块地形波动振幅进行修饰。比方说持续度我们设为0.5，那么三个octave振幅系数依次为，1，0.5，0.25，那么最大的地形起伏将会保持不变，细节碎石地形起伏则会更加平缓，这和我们真实地形体现效果也是比较接近的，地壳运动产生大地形高度变化，风化作用流水侵蚀对局部地形进行细微缓慢地改造。这样我们就可能实现一个基本自然地形模拟效果了，接下来就是代码具体实现过程下一个目标，生成柏林噪音灰阶高度图主要代码用到unity3d内置的 Mathf.PerlinNoise(float x , float y);scripts 文件夹下创建一个新的c#脚本：Noise脚本内主要用来定义柏林噪音一些参数，并不准备用来挂载应用到unity物体上，可以不需要继承monobehaviour类，我们也不需要生成各种实例化对象，因此可以定义为静态类，增加static关键词默认的start和update方法也用不着。我们主要是构建一个方法，为了生成noise map，需要返回0，1范围内的值，记录我们的灰度值。创建：public static float[,] GenerateNoiseMap(int mapWidth, int mapHight){ float [,] noiseMap = new float[mapWidth,mapHeight]; for (int y=0; y<mapHeight;y++){ for(int x =0;x<mapWidth;x++){ float smapleX =x; float sampleY=y; } } }遍历整个二维数组，将x，y每个像素点的值存储到noisemap数组里。考虑到每次需要生成不同规模的柏林噪音地图，方法还需要定义一个scale 规模 参数，用来每次传入scale值，随机生成不同规模地图，追加float scale参数传入到我们的GenerateNoiseMap数组中，同时将样板每个像素点存储的柏林噪音像素控制在对应scale大小里：float smapleX =x/scale; float sampleY=y/scale;为了防止除以零的错误发生，遍历之前做一个条件判断，if (scale<=0){ scale=0.0001f; }接下来，遍历数组中，我们获得sampleX,Y之后，float perlinValue=Mathf.PerlinNoise (sampleX,sampleY); noiseMap[x,y]=perlinValue;这样我们就将地图中所有样本点生成对应柏林噪音像素值，同时存储到我们的noiseMap数组里了GenerateNoise方法最后我们返回这个数组,柏林噪音生成基本功能就实现完毕了：return noiseMap;接下来，我们将构建地图生成功能新建一个脚本，命名：MapGenerator这个脚本里，我们将通过对Noise信息加工，最后展现在unity获得柏林地图噪音灰度图。Hierarchy面板新建一个空物体挂载这个生成器，方便最后展示生成效果命名MapGenerator，reset位置归零进入脚本编写：public int mapWidth; public int mapHeight;//我们之前提到需要用到间隙度，持续度来控制，这里先搭建最简单的 public float noiseScale;//引入基本的地图长宽属性和尺寸就好，之后再完善 public void GenerateMap(){ float[,] noiseMap=Noise.GenerateNoiseMap(mapWidth,mapHeight,noiseScale); }生成器脚本主要就是调取咱们Noise生成的noise信息，加工后传递给unity显示出来，那么我们还需要一个显示功能处理脚本新建一个MapDisplay脚本将这两个脚本都挂载到Map Generator空物体上基本思路就是，generator脚本处理noisemap数组里的noise value信息，然后生成黑白像素材质，材质导出到Display脚本，通过Display脚本加工构建对应的三维物体平面，赋予这个noise灰度图材质，展现在unity里面新建一个plane，去掉Mesh colider组件，别忘记位置归零，我们通过mapDisplay脚本构建新的渲染方法来实现MapDisplay脚本内开始构建：public Renderer textureRenderer; public void DrawNoiseMap(float[,] noiseMap){ int width =noiseMap.GetLength(0);//获取数组第一维元素长度值，赋值给width； int height =noiseMap.GetLength(1);//获取数组第二维元素长度值，赋值给height； Texture2D texture = new Texture2D (width,height);//实例化一张空白平面贴图材质 Color[[ colourMap = new Color [width*height];//创建一张色彩分布图 for (int y=0;y< height;y++{ for(int x=0;x<width;x++){ colourMap[y*width+x]=Color.Lerp(Color.black,Color.white , noiseMap[x,y]); //类似之前遍历绘制三角面，这里我们遍历色彩图每个像素点，线性差值转换柏林噪音数值信息为黑白色彩间分布的图像色彩信息。 } } texture.SetPixels (colourMap);//拿到色彩图之后别忘记将每个像素点色彩绘制到材质上 texture.Apply ();//新手最容易忘记的应用我们的材质，才会生效。 textureRender.sharedMaterial.mainTexture = texture; textureRender.transform.localDcale = new Vector3(width, 1, height); }我们这就可以回到Generator脚本调用我们Display脚本里面的方法了：GenerateMap方法里继续编写：MapDisplay display = FindObjectOfType<MapeDisplay>(); display.DrawNoiseMap(noiseMap);来到Heirarchy面板,将plane物体挂载到Map Generator物体上Map Display脚本里面Texture Renderer参数上。为我们的plane新建一个材质，新建文件夹Materials新建材质球Map Mat,材质球右侧inspector面板修改shader为Unlit/Texture因为我们需要排除光线反射干扰，这里先删除场景中的默认光源，材质设为无光线自发光材质方便观察。回到Map Generator物体，我们接下来将要在inspector面板增加编辑生成按钮，用来生成随机柏林噪音地图所以我们还需要一个脚本，定义自己的编辑器Assets主目录新建Editor文件夹，里面新建MapGeneratorEditor脚本Using Unity Editor； [CustomEditor (typeof (MapGenerator))]继承monobehaviour改为继承Editor父类里面进行代码书写：public override void OnInspectorGUI(){ MapGenerator mapGen = (MapGenerator)target; //修改调用Inspector界面方法，如果检测按钮“Generate”变化，执行生成地图方法 DrawDefultInspector(); if(GUILayout.Button("Generate")){ mapGen.GenerateMap(); } }回到unity,mapGenerator物体上设置下地图高度10宽度10还有柏林噪音单元尺寸0.3，然后点击生成按钮，应该就可以实现柏林噪音灰度图了修改高度宽度为100，再次点击Generate应该可以获得更大的地图。基本功能实现，我们优化一下比如修改高度宽度后在scene界面直接体现回到MapGenerator脚本，我们定义一个bool变量：public bool autoUpdate；再来到MapGeneratorEditor脚本原来的public override void OnInspectorGUI(){ MapGenerator mapGen = (MapGenerator)target; DrawDefultInspector();修改为public override void OnInspectorGUI(){ MapGenerator mapGen = (MapGenerator)target; //增加当面板数值发生变化，并且autoUpdate按钮勾选情况下，也执行生成地图方法 if(DrawDefultInspector(){ //表示Inspector面板发生变化，即修改数值了 if(mapGen.autoUpdate){ //如果打开autoUpdate判断开关，只要面板修改数值，同样也执行地图生成功能。 mapGen.GenerateMap(); } }这样，我们实现柏林噪音获得地图基本功能就实现完毕，这一节到此结束。下一篇：[post cid="387" /]
【Unity 3D进阶教程分享】EndlessTerrain无限地形搭建 第二节：绘制mesh网格平面，增加随机高度绘制随机地形
第二节 绘制mesh网格平面，增加随机高度绘制随机地形上一篇：[post cid="373" /]3.新的目标：构建更多方块，组成平面上一阶段咱们的MeshGenerator脚本里面实现了两个方法，一个是创建存储顶点形状信息，CreateShape，另一个是UpdateMesh方法，将顶点和三角面信息载入mesh网格里让咱们的meshrenderer进行渲染。接下来目标我们通过构建更多的方块，组成平面，最后抬升平面一些顶点，完成基础地形构建，因此，我们清空CreateShape方法，开始思考需要重新构建网格平面，思考需要创建的顶点数量顶点数量跟平面尺寸有关，因此我们开头新加一些变量：public int xSize =20; public int zSize =20;通过【总顶点数构图】分析，如果我们x方向有N个方块，则顶点数为N+1；如果z方向方块数为M，则z方向顶点数为M+1；因此我们根据平面尺寸，得到总顶点数公式：vertex count = （xSize+1)*(zSize+1);代码里进行书写：vertices = new Vector3[xSize+1)*(zSize+1)];这样我们就构建好所有顶点存储数组空间了，接下来将遍历所有顶点，将每个顶点在咱们的平面网格的坐标位置和顶点一一对应存储即可。按从左到右顺序(0,0),(1,0),(2,0),(3,0),(0,1),(1,1),(2,1),(3,1)，代码如下：int i=0; for (int z=0;z<=zSize;z++)//因为达到最边界zSize也要进行存储计算，所以中间需要计算等于的时候 { for(int x=0;x<=xSize;x++) { vertices[i]=new Vector3(x,0,z); i++; //这里需要遍历xSize内元素，引入i这个变量，从而我们可以在开头加入int i=0；由于i在咱们这个z，x循环里使用，我们可以将int i=0；条件放入for(int z=0;这里使得代码更加简练理解，最后代码是 for (int i =0 ,z=0;z<=zSize;z++) } }顶点都存储完毕了，但是我们实际渲染后是看不见这些顶点的，要想渲染出来顶点信息，我们需要调用unity的 OnDrawGizmos的方法，来画出咱们的顶点private void OnDrawGizmos(){ if (vertices==null) return; for (int i=0 ;i<vertices.Length;i++) { Gizmos.DrawSphere(vertices[i],.1f); }保存代码，进入unity运行，发现在我们的Scene界面，可以看见完整的顶点铺满一个平面，从我们的(0,0,0)原点。我们还可以改变inspector面板咱们的xSize和zSize大小，查看改变，顶点信息构建完毕。下一个目标继续构建我们的三角形以便完成我们的网格平面在CreateShape方法中，我们继续完善第二个功能，triangles =new int[3]; triangles[0]=0; triangles[1]=xSize+1; triangles[2]=1;这样，我们第一个三角形就构建完毕了，保存查看unity，Scene面板和game面板都ok接下来构建第二个三角形，将triangles数组大小改成6继续加新顶点：triangles[3]=1; triangles[4]=xSize+1; triangles[5]=xSize+2;保存后unity里运行发现第一个方块构建完毕了接下来如果要构建后面的方块，我们只需要重复这一步，同时做偏移量，进行循环即可for (int x=0;x<xSize;x++) {//将之前6个顶点信息全部剪切进来 triangles =new int[6]; triangles[0]=0; triangles[1]=xSize+1; triangles[2]=1; triangles[3]=1; triangles[4]=xSize+1; triangles[5]=xSize+2; }为了对顶点和三角形进行叠加计数，我们在咱们的三角形计算功能前申明两个变量：int vert =0; int tris =0;在for循环中对咱们的遍历进行重复计数，做一些小修改即可实现： for (int x=0;x<xSize;x++) { triangles =new int[6]; triangles[tris +0]=vert+0; triangles[tris +1]=vert+xSize+1; triangles[tris +2]=vert+1; triangles[tris +3]=vert+1; triangles[tris +4]=vert+xSize+1; triangles[tris +5]=vert+xSize+2; vert++; tris +=6; }//第一遍，第一个方块，,tris=0,vert=0,构建完毕后vert+1,第二次循环开始，因为vert=1，此刻进行的是第二个方块顶点存储了。 //同时咱们三角形存储数组大小扩大6格，方便进行第二个方块六个顶点信息存储， //我们不需要每次循环重复构建存储三角形顶点信息的数组，所以还需要将 triangles =new int[6];放到for循环外面，在开始并修改元素总数大小为： triangles =new int[xSize*zSize*6];这样我们通过tris +=6实现每次存储一个方块的6格顶点信息，循环后偏移量+1，达成所有x轴方向的方块顶点信息存储。如果想看到每个三角形如何建成的顺序，我们可以构建一个协程，如果对协程不太熟悉的，我这里简单解释下：Coroutine：协程是让程序员自己作为调度员，告知系统等待某个操作指令完成后再执行后面的代码一般用来控制运动或者地图生成或者一些剧情触发协程关键调度指令就是yield这个中断指令，yield返回的值决定了这个协程等待多久恢复执行，我们新建一个Coroutine Test脚本做个简单测试和说明：void start(){ Debug.Log("start1"); StartCoroutine (Test()); Debug.Log("start2"); } IEnumerator Test(){ Debug.Log("test1"); yield return null; Debug.Log("test2"); }保存，运行，结果应该是start1，test1，start2，test2这startCoroutine调用和普通函数方法调用类似，然后执行Ienumerator，方法类型就不是普通的函数类型了，需要特定的IEnumerator类型执行test1输出后，yield断点，返回空，就立马回到start方法后面的指令，这边暂停，输出start2后，我们的主程序一般都是立马返回的，直接返回到我们yield断点后继续执行协程后面的test2输出，这就是协程的工作流程进阶分析：如果我们return一个等待时间，结果又是如何呢？修改 yield return null;为 yield return new WaitForSeconds(3);保存运行，我们可以发现，test2会在start2输出后三秒才显示，这就是return的vaule对暂停等待时间的控制效果我们再分析下，协程套协程是如何运行的：修改 yield return new WaitForSeconds(3);为 yield return StartCoroutine(AnotherTest());没错，我们协程里返回开始另一个协程，我们构建下这个新协程：IEnumerator AnotherTest(){ Debug.Log("AnotherTest1"); yield return null; Debug.Log("AnotherTest2"); }保存后执行，结果应该是start1，test1，AnotherTest1，start2，AnotherTest2，test2这里注意，第一个协程会等待第二个协程中所有代码执行完毕后再继续执行，第一个yield resturn告诉第一个主程StartCoroutine我这边先暂停了，你去继续执行StartCoroutine（AnotherTest），我在这等着你完事 的消息，就会再回来执行我后面的test2，于是我们第一个StartCoroutine就跑去就执行AnotherTest先，这个AnotherTest1就紧接着输出了，之后遇到yield return null，这个AnotherTest协程就告诉我们的StartCoroutine，行了，我这也先暂停了，你继续执行，你完毕后告诉我，我就执行我后面的AnotherTest2输出，于是咱们的StartCoroutine又跑回来，执行完毕自己剩下的start2输出，然后告诉我们AnotherTest协程，我好了，你可以继续yield return null后面的内容了，于是AnotherTest2输出，告诉第一个StartCoroutine我执行完毕了，你继续，于是第一个StartCoroutine检查了下，我还与test协程说好了，等着AnotherTest协程结束，他就可以继续干活了，最后告诉我们的Test协程你久等了，那个垃圾AnotherTest协程终于完事了，你继续，于是test2就输出执行了协程这东西差不多基础的原理了解这么多就够用了，我们回到这节的内容上来，刚才说到如果想看到每个三角形如何建成的顺序，我们可以构建一个协程，那么在协程里对每一次构建插入一个yield等待时间，就可以放缓构建过程了，这就需要用到IEnmulator和StartCoroutine了，我们将CreateShape方法改为IEnumerator在三角形信息存储for循环里，最后返回yield return new WaitForSeconds (.1f);由于方法改成了coroutine协程了，我们在start方法里调用CreateShape也要修改成StartCoroutine(CreateShape());现在要重复执行createShape方法，完成咱们mesh整个绘制过程，我们需要将UpdateMesh()方法移到Update方法里每一帧多次执行，这样才可以做到查看每个三角形构建过程：private void Update(){ UpdateMesh(); }保存后，这样我们就可以看到一排三角形创建的顺序了继续对z轴方向剩下的方块进行绘制for (int z=0;z<zSize;z++){ 将刚才的x轴for循环体全部剪贴进来 }保存测试，发现确实可以绘制了，但是有个问题，两侧边界的三角面渲染有些不对劲，翻到底部还可以看到每绘制一行新的方块，边界总跟随一条斜线绘制在底部。这是因为我们每次x轴一行绘制完毕了，并没有将每行的vertex顶点进行切断，而是将x轴最右侧的顶点和z轴+1后新的一行顶点直接相连，导致在咱们网格平面背面进行跨行绘制，导致两侧边界三角形出现正反两个方向的面，从而光线计算出现异常，同时绘制出底部飞线效果。要修改这个bug，只需要将x轴for循环外面，在后面加一个vert++;将最后一个顶点跳过，z轴+1后，将左侧新的顶点作为新的一行的初始顶点开始新的绘制即可。保存后测试一切ok最后目标：通过修改一些顶点的高度值获得高低起伏地形效果。createShape方法里for(int x=0;x<=xSize;x++) { vertices[i]=new Vector3(x,0,z); i++;之前循环遍历存储每个顶点信息的时候，y轴储存的都是0高度，这里我们修改下vertices[i]=new Vector3(x,0,z);前增加：float y =Mathf.PerlinNoise(x*.3f,z*.3f)*2f;//2f使得柏林噪音起伏高低效果更加显著，.3f是形成更广阔的柏林噪音分布，使得地形变化更加多样。修改 vertices[i]=new Vector3(x,0,z);为 vertices[i]=new Vector3(x,y,z);使得y轴高度产生柏林噪音高低起伏存储进来。yield return new WaitForSeconds (0.01f);可以修改更快的时间间隔，使得生成的图形更快。最后调试完毕，将IEmulator改成普通void方法，将Update里面updateMesh方法移到start方法里使得生成更加快速，同时修改startcorutine(createShape())为普通的createShape()方法调用,最后Ctrl +K+C注释掉private void OnDrawGizmos()方法，不再需要测试绘制顶点了，保存测试，一张随机全新平面地图快速生成完毕，我们的基本地形生成演示到此完成。下一篇：[post cid="385" /]
【Unity 3D进阶教程分享】EndlessTerrain无限地形搭建 第一节：解析顶点和三角面原理，绘制mesh顶点和三角形
上一篇：[post cid="412" /]第一节 解析顶点和三角面原理，绘制mesh顶点和三角形创建一个空物体，命名Mesh generator，reset transform归零，新加入两个组件：Mesh Filter(储存mesh网格形状)，Mesh Renderer（读取mesh形状，渲染后获得可见模型），mesh Filter和Renderer可以演示基本网格形状和默认渲染材质效果。再添加一个脚本：Mesh Generator，进行脚本编辑mesh内包括很多信息，顶点，三角面，颜色，法线等等，这节我们重点分析下vertices顶点和triangles三角面顶点介绍以方块为例，四个顶点vertices=(0,0),(0,1),(1,0),(1,1)这样存储了方块的位置信息，这个数组就叫做顶点数组。如果是三维空间，我们查看下图示例可以了解到对应的顶点坐标了解顶点之后，我们想要模型面也渲染出来，则用到三角形填充至于如何渲染，unity渲染三角面采用顺时针方向，这涉及到一个知识叫做backface culling（背面剔除）为了节省渲染资源，物体内部和背面一般不进行渲染计算，所以通过填充的顶点顺序判断正反面因此，我们如果要渲染正面，则需要进行正确的三角面顶点排序，第一个三角形应该是0，1，2 第二个三角形则是2，1，3如果你存储为1，2，3的话，unity则会将这个面视为背面，默认为黑色，不进行渲染。好了，进入脚本开始编写1. 目标：第一个三角面渲染新数组Vector3[] vertices; int[]triangles;申明一个 Mesh mesh；我们就可以在start方法中开始赋予网格了：void Start(){ mesh=new Mesh(); GetComponent<MeshFilter>().mesh=mesh; }这样我们就实例化一个新的mesh网格了，并且将刚才的空物体上面的meshfilter里mesh网格属性赋值咱们新建的mesh信息了，要获取meshfilter，我们还需要在最开始class前面申请获取组件：[RequireComponent(typeof(MeshFilter))]继续，start方法中开始创建物体形状:CreateShape();我们新建这个方法：void CreateShape(){ vertices=new Vector3[] { new Vector3 (0,0,0), new Vector3 (0,0,1), new Vector3 (1,0,0) }; triangles = new int[] { 0,1,2 }；我们还未告知系统如何使用这些数据，因此新建一个Mesh更新方法进行分开处理start方法后新加一个调用：UpdateMesh();然后新构建此方法：void UpdateMesh() { mesh.Clear();//方便更新mesh网格前清空之前的网格 mesh.vertices=vertices; mesh.triangles=triangles; }这样我们就可以将上面获得的数据赋值到咱们的mesh信息里面，理论上我们应该能得到三个顶点信息和一个三角形填充信息了保存脚本，进入unity运行测试，调整scene视野，让镜头与view对齐，如果嫌图标过大，点击scene界面右侧Gizmos,调整3d icon后方横条即可。因为还没有赋予材质信息，所以显示紫色三角形。我们手动赋予默认材质，通过光照系统发现光线反射效果比较诡异，这是因为我们还没有设置法线信息，可以通过unity指令直接设置，在UpdateMesh方法里加入一条：mesh.RecalculateNormals();即可。这样就能看到和普通模型一样的橘黄色外框和白色正常反馈光反射效果了第一个三角面和三个顶点构建完毕，2. 新的目标：第一个方块构建我们尝试构建第一个方块剩下部分，需要新增一个顶点CreateShape方法里新加：new Vector3 (1,0,1)即右上角的顶点位置，triangles里面新加1，3，2第二个三角形信息，保存后unity里执行，发现ok如同我们普通的三维物体，这个方块我们可以执行旋转拉伸甚至加入rigidbody刚体组件，实现自由落体等等开头我们说到，如果三角形存储顶点顺序不是顺时针，渲染会发现变化，那么我们试着改成1，2，3看看效果发现一个黑色三角，翻过来看见另一个，都是黑色的，因为三角面无法判断方向，因此无法计算光线走向，从而没有光线渲染。下一节，我们再分析如何拓展我们的顶点绘制方法，将单个方块顶点和三角形的绘制拓展到整个平面所有方块mesh绘制。下一篇：[post cid="380" /]