OMO萌 - 2018年12月
type: channel
link: https://omo.moe/2018/12/
description: 使用朴素typecho搭建，开启新的人生
items: 【Unity 3D进阶教程分享】EndlessTerrain无限地形搭建 第十节：LOD结合视距分区块mesh地形渲染
items: 【Unity 3D进阶教程分享】EndlessTerrain无限地形搭建 第九节：多线程同步协调处理多区块思想和基本实现
items: 【Unity 3D进阶教程分享】EndlessTerrain无限地形搭建 第八节：多区块地形生成与隐藏
items: 【Unity 3D进阶教程分享】EndlessTerrain无限地形搭建 第七节：LOD多细节层次渲染功能实现
items: 【Unity 3D进阶教程分享】EndlessTerrain无限地形搭建 第六节：地形Mesh网格生成器实现
items: 【Unity 3D进阶教程分享】EndlessTerrain无限地形搭建 第五节：对生成的灰度图进行上色填充
items: 【Unity 3D进阶教程分享】EndlessTerrain无限地形搭建 第四节：Octave倍频分层叠加地形设计原理及基本实现
items: 【Unity 3D进阶教程分享】EndlessTerrain无限地形搭建 第三节：基于柏林噪音生成灰度图
items: 【Unity 3D进阶教程分享】EndlessTerrain无限地形搭建 第二节：绘制mesh网格平面，增加随机高度绘制随机地形
items: 【Unity 3D进阶教程分享】EndlessTerrain无限地形搭建 第一节：解析顶点和三角面原理，绘制mesh顶点和三角形

【Unity 3D进阶教程分享】EndlessTerrain无限地形搭建 第十节：LOD结合视距分区块mesh地形渲染
type: item
date: 2018-12-25T18:11:00+00:00
description: 第十节 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看看再回…

【Unity 3D进阶教程分享】EndlessTerrain无限地形搭建 第九节：多线程同步协调处理多区块思想和基本实现
type: item
date: 2018-12-25T17:54:00+00:00
description: 第九节 多线程同步协调处理多区块思想和基本实现上一篇：[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，因为暂时不需要用来外部类调用它了，去掉publi…

【Unity 3D进阶教程分享】EndlessTerrain无限地形搭建 第八节：多区块地形生成与隐藏
type: item
date: 2018-12-25T17:43:00+00:00
description: 第八节 多区块地形生成与隐藏上一篇：[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 Terrai…

【Unity 3D进阶教程分享】EndlessTerrain无限地形搭建 第七节：LOD多细节层次渲染功能实现
type: item
date: 2018-12-25T17:28:00+00:00
description: 第七节 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，作为边界取值的话，拿不到…

【Unity 3D进阶教程分享】EndlessTerrain无限地形搭建 第六节：地形Mesh网格生成器实现
type: item
date: 2018-12-25T17:18:00+00:00
description: 第六节 地形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…

【Unity 3D进阶教程分享】EndlessTerrain无限地形搭建 第五节：对生成的灰度图进行上色填充
type: item
date: 2018-12-25T17:11:00+00:00
description: 第五节 对生成的灰度图进行上色填充上一篇：[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脚本却包括了黑白上色功能， //如果把接下…

【Unity 3D进阶教程分享】EndlessTerrain无限地形搭建 第四节：Octave倍频分层叠加地形设计原理及基本实现
type: item
date: 2018-12-25T17:03:00+00:00
description: 第四节 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< mapWid…

【Unity 3D进阶教程分享】EndlessTerrain无限地形搭建 第三节：基于柏林噪音生成灰度图
type: item
date: 2018-12-25T16:50:00+00:00
description: 第三节 基于柏林噪音生成灰度图上一篇：[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方法最后我们返回这个数组,柏林噪音生成基本功能就实现完毕了：r…

【Unity 3D进阶教程分享】EndlessTerrain无限地形搭建 第二节：绘制mesh网格平面，增加随机高度绘制随机地形
type: item
date: 2018-12-25T16:39:00+00:00
description: 第二节 绘制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; tri…

【Unity 3D进阶教程分享】EndlessTerrain无限地形搭建 第一节：解析顶点和三角面原理，绘制mesh顶点和三角形
type: item
date: 2018-12-25T16:22:00+00:00
description: 上一篇：[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" /]