第七节 LOD多细节层次渲染功能实现


yande.re 379053 sample material_ghost tinkle.jpg
上一篇:


我们继续上节内容,如何将平面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])*heightMultiplier

heightCurve.Evaluate就是求出对应括号内位置curve曲线对应该点的数值,再加成高度倍数就拿到了更自然的高度倍数因子了
保存,回到unity,点击generate看看效果,若不满意可以微调我们的曲线,达到满意的表现

立体拉升效果实现完毕,接下来我们优化下多块地形细节渲染分层,为后面的无限地形生成奠定基础。这里用到的LOD技术,在游戏领域应用非常广泛,我们通过修改不同视野范围的三维模型mesh网格数,来降低系统渲染负荷,优化游戏体验,对远处或者不太容易关注到的模型降低细节表现来获得帧数的提升
我们先解释下实现mesh分层的原理
mesh分层的原理.png
之前我们的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个数为9
i=2,vertices个数为5
i=4,vertices个数为3
我们需要找到通用的关系式
i=1,v=i
i=2,我们观察左边4条曲线代表四个循环,都是被i=2切分后分成了4条,每段记录一个顶点
我们可以尝试将总宽度减掉最右边的1个,再除以i=2,获得左边所有切分后记录的顶点数,再补上最右边的点+1,公式可以是 vertices=[(width-1)/i]+1
i=4 时候,我们验证下,减去最右侧终点一个后,除以跳跃值i=4,获得2个循环分块,每个分块存储1个顶点信息,最后加上最右侧的终点,结果是3个,和实际相符。
公式是可用的
下面解释下为何不超过65000个顶点数:
一般的,我们 vertices=width*height
对于方形地图 vertice=width^2

v<=255^2(65025)

本来实际限制是 256^2-1即65535,作为边界取值的话,拿不到256这个值,所以最大的正方形就是255的平方了
回到我们目前的情况
我们上一节提到过,每一行最后一个点不用来记录绘制三角面,那么实际进行顶点计算的点应该是 width-1
width-1和i这个index跳跃度系数找最大公倍数,我们分析如下:
我们要思考一个优先度,i跳跃系数,能多几档调节为优,能集中在小跳跃范围为优,什么意思呢
清晰度细节显示调节档位,如果就三四档,明显不够细腻,变化突兀太明显
第二个,index跳跃系数为何尽量限制在较小范围内?如果跳跃值太大,比如说240的宽度你来个40,80甚至120,就绘制出来几个点,太精简了没啥实际意义,所以说,我们理想来说,最好是7,8个档位,能尽量集中在20以内,使得240的宽度也能有基本12个顶点进行绘制,最粗糙的mesh也可以忍受,这样实际应用起来就比较可行了
考虑到最大公倍数的原理,我们最好找一批偶数或者相互都有公因数的一批数字或者一批奇数搭配一批偶数,会比较理想,毕竟全奇数的话,20以内3,5,7,11这些破数字很多互相没有啥重复的公因数,这样很难找到一个255以内的公倍数来实现效果
通过一番鬼畜的推导
公倍数推导筛选.png
最终结果:
LOD档位和地图尺寸推导结果.png
我们获得可行的 (width-1)最大合适数值是240
i的取值可以为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网格渲染,进一步走向我们的无限地图生成目标
下一篇:

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