向往夏威夷清澈湛蓝的海水,干净柔软的沙滩,可惜疫情让我们无法远足.
只要心中有沙,哪里都是马尔代夫。快来动森小憩一会吧
老大:
嗯,这个沙滩不错,蜿蜒曲折,煞是好看,就是它了,看看怎么实现。
码农:
这个简单,让美术做个模型放上去就行了。。。
老大:
有你没你都一样,你准备去度个长假吗?
码农:
不,不,我马上就去想办法!
仔细观察,沙滩应该是一个面片,控制其中一部分顶点沉入水平面以下,就出现了水沙交界线,蜿蜒曲折吗,有几种实现方式,可以给地编提供个顶点笔刷,还可以画个曲线出来,用来标记交界线。笔刷的方式比较简单,但是数据存储量较大,要把顶点坐标都记录下来以便还原,我们就采用第二种方式来实现。既然是曲线,非Beizer莫属。
首先给场景中加一个Bezier功能,这个就不细说了,网上资源一大把。直接上效果
Bezier曲线
现在交界线有了,下面就是怎么做出沙滩的斜坡了,首先做一个面片做地形,通过编辑面片的顶点Y坐标,产生斜坡:
弯曲示意图
Bezier曲线的特性是可以推进time,算出曲线上的点,这样我们就可以细分曲线,算出很多的采样点,连接相邻的采样点,在线段右边的顶点向下做个偏移,根据离线段的距离算出偏移量
细分曲线
上图中最大的长方形是地形的面片,曲线是画出来的Bezier海岸线,左下是局部放大,其中A,B,C,D是曲线上的几个采样点,用直线连接相邻采样点,形成线段AB,BC,CD. 图中小圆圈代表地形面片的三角形顶点。下面面临的问题就是
- 怎么确定每个顶点要和哪段线段进行计算
- 怎么算顶点在线段的左右边
- 顶点离线段的长度,比如 |EF|。
在分析上面三个问题之前,摆在我们面前的是如何遍历采样点形成的线段和地形的所有顶点的问题。最直观的方式是第一层循环遍历采样线段(相邻采样点连线,下同),第二层循环遍历地形所有顶点。这里我们遇到的问题是:
- 为了让海岸线看起来弧度自然,我们细分Bezier曲线做的比较密,同样为了弯曲时不出现棱角,地形面片网格也很密,这意味着线段多,顶点多,计算量是二者的乘积。
- 另一个是我们不好判断哪个顶点该和哪个线段做计算的问题。
左边右边可以通过向量点击确定点到线段的距离也有现成的公式。
现在不好解决的就是计算量大,效率低以及不好确定顶点属于哪个采样线段。有没有更好的解决方案呢?答案是肯定的。
我们放弃采用采样线段的方案,改用角度细分,我们先确定Bezier曲线包围盒的中心点,然后把中心点和每个采样点做连线。这样每相邻的两个采样点和中心连线,就会形成一个夹角,落在这个夹角所覆盖的扇形区域内的所有顶点,我们就和这两个(后面说为什么是两个)采样点做计算。为了便于理解,上图:
角度细分
图中多了一个Bezier曲线包围盒以及中心点,中心点和采样点的连线(只画出了部分连线,360度都会被角度细分).下面给出包围盒以及中心点的算法:
int segmentCount = bezier.Segments * bezier.CurveCount;
//计算包围盒
for (int i = 0; i < segmentCount ; i++)
{
float time = (float)i / (segmentCount - 1);
Vector3 point = bezier.GetPoint(time);
checkPointList.Add(point); //检查点收集到一个List中,以便后面使用
if (i == 0)
{
boundingBox = new Vector4(point.x, point.y, point.x, point.y);
}
else
{
if (point.x - boundingBox.x < 0)
{
boundingBox.x = point.x;
}
if (point.z - boundingBox.y < 0)
{
boundingBox.y = point.z;
}
if (point.x > boundingBox.z)
{
boundingBox.z = point.x;
}
if (point.z > boundingBox.w)
{
boundingBox.w = point.z;
}
}
}
//取中心点
centerPoint.Set(boundingBox.x + (boundingBox.z - boundingBox.x) / 2f, 0, boundingBox.y + (boundingBox.w - boundingBox.y) / 2f);
然后再遍历一遍,把所有连线的角度记录下来:
//角度值的容器
checkAngleArray = new float[checkPointList.Count];
//把中心点和每个检查点的连线,相对于X轴正方向的夹角存下来
int index = 0;
foreach (Vector3 point in checkPoints)
{
Vector3 relativePoint = point - centerPoint;
float angle = Vector3.Angle(Vector3.right, relativePoint);
if (relativePoint.z < 0)
{
angle = -angle;
}
checkAngleArray [index++] = angle;
}
下面就该遍历每个顶点了,一层循环大大减少了计算量:
Mesh mesh = obj.GetComponent<MeshFilter>().mesh;
Vector3[] vertices = mesh.vertices;
//中心点坐标转换到本地坐标
Vector3 localCenterPoint = obj.transform.InverseTransformPoint(centerPoint);
//遍历顶点
for (int i = 0; i < vertices.Length; i++)
{
Vector3 vertex = vertices[i];
//To do process vertex
}
框架已经搭起来了,下面就是每个顶点具体怎么计算的问题了,我们当然可以用上面提到的向量点乘确定左右边,顶点到线段的距离确定Y值偏移量,但是这样的计算量还是比较大的,我们可以采用近似算法:
我们把顶点E也和中心点(标记为O,下同) 连接起来形成 EO,那我们只要计算 |EO| - |BO|就可以了,如果大于0则在右侧,小于零在左侧,得到的模可以确定Y值向下的偏移量。一次计算解决了两个问题,效率飙升,虽然结果不是很准确,但是还有补救办法,就是我们再和C做一次计算 |EO| - |CO|,同样得到一个Y值偏移量,然后根据角BOE/BOC进行一个差值。确定最终Y偏移值.(这就是前面为什么说要和两个检查点做计算的原因)
下面给出伪代码:
//计算顶点到中心点的夹角
Vector3 vertextLine = vertex - localCenterPoint; //顶点到中心点的连线
float angle = Vector3.Angle(Vector3.right, vertextLine); //顶点到中心点的角度
//获取检查点索引
int index = GetCheckPointIndex(angle);
//顶点到中心点的距离
float vertexDistance = Vector3.Distance(vertex, localCenterPoint);
//检查点到中心的距离
float checkDistance = Vector3.Distance(obj.transform.InverseTransformPoint(checkPoints[index]), localCenterPoint);
float distance = vertexDistance - checkDistance;
if(distance > 0)
{
//计算顶点的高度偏移值
float delta = distance * distance * heightScale;
vertex.y -= delta;
}
上面的属于伪代码,大家在理解程序意图的前提下很容易完善,这里主要是没有加入和第二个检查点的计算以及最终差值的部分。
另外这里有个函数是GetCheckPointIndex(angle),这个是根据顶点和中心点连线EO的角度,快速索引到落在那个细分角度覆盖范围,这里面也有效率优化,首先根据角度,除以每个检查点覆盖的平均角度(360/检查点个数),大体算出第几个检查点,然后从这个检查点再向前或者向后遍历寻找,这样大大提高了效率.代码就不贴了,请自行完善。
到这里,核心算法基本就完成了,看看最终效果吧:
效果1
效果2
画面糙了些,凑合看,核心是算法思想,以及解决问题的思路,解决过程。希望能够帮到有需要各位码农。
码农:
老大,快来呀,沙滩做好了,就等你的北冰洋汽水了。。。
网友评论