Unity3D绘制地形的实现方法
作者:JayW就是我吖 发布时间:2022-12-01 01:06:46
项目中肯定会遇到需要用户自己绘制地形的需求,然后根据地形自动生成房间。下面说说我在绘制地形的实现方法。
我们百度可以看到很多关于自己创建mesh的博客,mesh的生成需要三角面顶点坐标以及顶点序列。所以,想要创建我们想要的mesh,首先要获取到绘制mesh的顶点。我们用户在绘制自己想创建的地形时会有很大的自由性。他是随心所欲想怎么画就怎么画。这也造就了很大的错误风险性,要求程序更加智能。好了,下面说下我们给自己程序设定的一些规则。
首先我们设置在绘制的时候摄像头的forward朝向Y轴向上,即我们可以俯视到的就是xz轴组成的平面。其次我们要使用linerenderer来画线,linerenderer组合起来必须是一个封闭的区间,否则有无限种可能。
而画房间不仅有最外层的墙,还有内部一个个小房间,他们也组成了闭合区间。而我们在画地形时需要抓取的是最外层闭合区间的顶点。本文画复杂多边形使用的算法是耳切法分割多边形,所以我们选择操作的方向是逆时针方向。算法链接。下面就说下在做项目时遇到的核心问题处理。还是老套路,先放效果图吸睛。
一、选取第一个处理的顶点
如上我们指定的规则是摄像机沿Y轴向下俯视。所以我们获取所有顶点,然后选取这些定点中x值最小的,选取出X值最小的点,有可能有一个,也有可能有多个,所以我们要接着筛选。在获取的这些点中我们设置筛选的条件是z值最小,这样就能获取到唯一的一个点。此时,该点即为凸点。代码如下:
Vector3 firstValue=Vector3.zero;
for (int i=0;i<plist.Count;i++)
{
if (plist[i].position.x<= firstValue.x)
{
if (plist[i].position.x == firstValue.x)
{
if (plist[i].position.z > firstValue.z)
{
return;
}
}
firstValue = plist[i].transform.position;
}
}
二、获取逆时针方向的第二个点
我们获取到所有与第一个点连线的线段的斜率,因为是闭合区间,所以至少会有两条线段与第一个点连接,由于第一个点为凸点且x值为所有点里最小,所以我们比较与第一个点连接的线段斜率。会有如下两种情况:斜率存在和斜率不存在。当斜率存在时,我们可以想象,k为最小值时即为逆时针的第二个点,k为最大值时线段连接的另一个端点为逆时针方向的最后一个点。当斜率不存在时即线段是平行于x轴的,所以我们要比较线段的斜率最小值是否小于0,如果小于0则这个线段连接的另一个端点为第二个点。如果斜率大于0,则这条斜率不存在的线段连接的端点为第二个点。同理可获取最后一个端点。
/// <summary>
/// 返回第二个顶点坐标
/// </summary>
/// <param name="v"></param>
/// <returns></returns>
private Vector3 returnSecondValue(Vector3 v)
{
//Debug.Log("v+" + v);
List<LineRendererStruct> lrst = new List<LineRendererStruct>();
for (int i=0;i<lrlist.Count;i++)
{
if (Vector3.Distance(lrlist[i].GetPosition(0),v)<0.1f)
{
lrst.Add(new LineRendererStruct(0, lrlist[i]));
}
else if (Vector3.Distance(lrlist[i].GetPosition(1), v) < 0.1f)
{
lrst.Add(new LineRendererStruct(1, lrlist[i]));
}
}
//Debug.Log("lrst.Count+"+lrst.Count);
if (lrst.Count >= 2)
{
float k1 = 0; //斜率最大
float k2 = 0; //斜率最小
Vector3 v1=Vector3.zero;
Vector3 v2 = Vector3.zero;
LineRenderer llrr=new LineRenderer();
for (int i=0;i< lrst.Count;i++)//选取斜率最大和最小的两个点
{
Vector3 vvv= lrst[i]._lr.GetPosition(lrst[i]._index == 0 ? 1 : 0);
if (vvv.x-v.x==0)//此处斜率不存在 就是平行x轴状态
{
if (k1 <= 0)
{
v1 = vvv;
llrr = lrst[i]._lr;
lastLineRenderer = lrst[i]._lr;
continue;
}
if (k2 >= 0)
{
v2 = vvv;
llrr = lrst[i]._lr;
continue;
}
}
float k= (vvv.z - v.z) / (vvv.x - v.x);
if (i == 0)
{
k1 = k;
v1 = vvv;
lastLineRenderer = lrst[i]._lr;
k2 = k;
v2 = vvv;
llrr = lrst[i]._lr;
}
else
{
if (k1 < k)
{
k1 = k;
v1 = vvv;
lastLineRenderer = lrst[i]._lr;
}
if (k2 > k)
{
k2 = k;
v2 = vvv;
llrr = lrst[i]._lr;
}
}
}
VertexList.Add(new VertexStruct(1,v2));
lrlist.Remove(llrr);
Debug.Log("VertexList[1]._vec+" + VertexList[1]._vec);
return VertexList[1]._vec;
}
else
{
Debug.LogError("此处有错误");
isContinue = false;
if (lrst.Count < 2)
{
_Warning.SetActive(true);
StartCoroutine(Globle.InvokeDelay(()=> { _Warning.SetActive(false); }, fadeTime));
}
return Vector3.zero;
}
}
三、处理其他顶点
处理其他顶点我们就比较复杂,因为一个顶点会有很多线段与之相连,而我们要获取的是最外围的顶点。所以我们在获取到第二个顶点以及与第二个顶点连接的线段后(去除连接第一个顶点和第二个顶点的线段),如下图:三条线段OA,OB,OC.OD.
我们自己分析会知道我们要得到OD,但是程序没有我们直观的分析能力。程序只能依靠计算来作为“视觉”依靠。所以接下来就是我们的处理。首先我们要判断凹凸角。因为毋庸置疑凹角肯定是最外层的闭合回路。如图角EOD.所以接下来我们要进行计算筛选。首先我们要计算各个闭合回路的点是凸角还是凹角。如判断角EOA,角EOB,角EOB,角EOC,角EOD。判断的方法就是向量的叉乘。
3.1判断凹凸角
我们知道第一个点为凸角,所以我们先根据第一个顶点的两条边叉乘得到凸角的方向。即向量o2o1xo1E,这里我们一定要记住判断凹凸角的向量叉乘一定要选取同一走向的向量,即都沿着逆时针方向或者都顺时针方向。而unity的坐标系是右手坐标系,所以叉乘的结果和我们右手定则得到的方向相反。即沿Y轴向下。我们得到标准凸角的叉乘方向,在用其他角的叉乘结果和标准方向比较。如果同向即为凸角,否则为凹角。代码如下:
private float crossValue(Vector3 v1,Vector3 v2)
{
v1 = new Vector3(v1.x,0,v1.z);//把顶点坐标处理下
v2 = new Vector3(v2.x,0,v2.z);//把顶点坐标处理下
return Vector3.Dot(Vector3.up, Vector3.Cross(v1.normalized, v2.normalized));
}
根据float值判断,当为负值即超Y轴向下,为凸角。当为正值时朝Y轴向上,为凹角。
判断结果一般会出现如下三种情况:1.全是凸角2.全是凹角3既有凸角也有凹角。在程序中我们需要加入if判断。第一种情况全是凸角:我们就需要计算组成角的两边向量点积,点积越小,夹角越大,也就是最外围线段。第二种情况和第三种情况处理情况相同,筛选出来凹角,然后根向量点积公式,点积越大,夹角越大。即可求出最外围线段。代码如下:
private void dealOtherPoint(Vector3 sv)
{
int num = lrlist.Count;
int _addIndex;
//后续还要添加
List<VertexStruct> TemporaryList;
while (true)
{
TemporaryList = new List<VertexStruct>();
num--;
if (num<-1)
{
isContinue = false;
Debug.Log("重新智能处理,若处理不了,则警告用户重新操作");
_Warning.SetActive(true);
StartCoroutine(Globle.InvokeDelay(() => { _Warning.SetActive(false); }, fadeTime));
//Debug.LogError("死循环1");
return;
}
//Debug.Log("sv+" + sv);
//在剩下的所有定点中找按顺序排列的下一个顶点
for (int i = 0; i < lrlist.Count; i++)
{
if (Vector3.Distance(lrlist[i].GetPosition(0),sv)<0.1f)
{
if (lastLineRenderer == lrlist[i])
{
//Debug.Log("LastKinerenderer1");
return;
}
_addIndex = VertexList.Count;
TemporaryList.Add(new VertexStruct(i, lrlist[i].GetPosition(1)));
continue;
}
else if (Vector3.Distance(lrlist[i].GetPosition(1), sv) < 0.1f)
{
if (lastLineRenderer == lrlist[i])
{
Debug.Log("LastKinerenderer2");
Debug.Log(lrlist.Count);
return;
}
_addIndex = VertexList.Count;
TemporaryList.Add(new VertexStruct(i, lrlist[i].GetPosition(0)));
continue;
}
}
_addIndex = VertexList.Count;
if (TemporaryList.Count== 1)//一个顶点只有两个linerenderer连接时
{
VertexList.Add(new VertexStruct(_addIndex, TemporaryList[0]._vec));
lrlist.RemoveAt(TemporaryList[0]._num);
}
else if (TemporaryList.Count>1)
{
List<int> AoList =new List<int>();//记录凹角个数
for (int i = 0; i < TemporaryList.Count; i++)
{
if (!ISTuAngle(sv, TemporaryList[i]._vec))
AoList.Add(i);
}
//初始边向量
Vector3 vc = sv - VertexList[VertexList.Count - 1]._vec;
//全是凸角
if (AoList.Count == 0)
{
float dotValue=1;
int dotValueIndex = 0;
for (int i=0;i< TemporaryList.Count; i++)
{
Vector3 vm = TemporaryList[i]._vec - sv;
dotValue = dotValue > GetdotValue(vc, vm) ? GetdotValue(vc, vm) : dotValue;//取余弦值最小值
dotValueIndex = dotValue > GetdotValue(vc, vm) ? i : dotValueIndex;
}
VertexList.Add(new VertexStruct(_addIndex, TemporaryList[dotValueIndex]._vec));
}
//全是凹角
else //if (AoList.Count == 1)
{
float dotValue = 1;
int dotValueIndex = 0;
for (int i = 0; i < TemporaryList.Count; i++)
{
Vector3 vm = TemporaryList[AoList[i]]._vec - sv;
dotValue = dotValue < GetdotValue(vc, vm) ? GetdotValue(vc, vm) : dotValue;//取余弦值最大值
dotValueIndex = dotValue < GetdotValue(vc, vm) ? i : dotValueIndex;
}
VertexList.Add(new VertexStruct(_addIndex, TemporaryList[dotValueIndex]._vec));
}
List<LineRenderer> temporarylrList = new List<LineRenderer>();
for (int i=0;i< TemporaryList.Count;i++)
{
temporarylrList.Add(lrlist[TemporaryList[i]._num]);
}
for (int i=0;i< temporarylrList.Count;i++)
{
if (lrlist.Contains(temporarylrList[i]))
{
lrlist.Remove(temporarylrList[i]);
}
else
Debug.Log("有错误");
}
}
sv = VertexList[VertexList.Count - 1]._vec;
}
}
好了以上我们就可以筛选出最外围顶点了并把他们添加到数组中。
四、划分三角形
耳切法分割三角形算法。点击打开链接。按照文章的讲解就可以明白解决方法。然后将自己想法用程序表达出来。
五、创建mesh
接下来也是最后一步,我们根据顶点来创建mesh。我们在分割多边形时会得到多个三角形以及对应三角形的顶点索引,在创建mesh时将顶点以及对应的索引数组赋值给mesh.vertices和mesh.triangles。代码如下:
//处理下得到的list数组
int[] ints = new int[verticeList.Count * 3];
for (int i = 0; i < verticeList.Count; i++)
{
ints[3 * i + 0] = verticeList[i][0];
ints[3 * i + 1] = verticeList[i][2];
ints[3 * i + 2] = verticeList[i][1];
}
GameObject g = new GameObject("MyPlane");
g.AddComponent<MeshRenderer>().material= myPlaneMaterial;
g.transform.tag = "House";
g.transform.SetParent(_House.transform);
Mesh mesh = new Mesh();
mesh.vertices = vecs;
mesh.triangles = ints;
g.AddComponent<MeshFilter>().mesh = mesh;
g.AddComponent<MeshCollider>().sharedMesh=mesh;
里面的处理代码很繁琐,要不断判断凹凸角的问题以及最大夹角。重要的是理解耳切法算法原理以及他的一些判断标准,就能很好的理解以及完成我们的需求了。希望本博客对你有帮助。
来源:https://blog.csdn.net/qq_33994566/article/details/76546547
猜你喜欢
- 我这里有一个需求需要修改Person类中的一个属性上的注解的值进行修改,例如:public class Person { private i
- json数据交互1.为什么要进行json数据交互json数据格式在接口调用中、html页面中较常用,json格式比较简单,解析还比较方便。比
- 内存分配方式简介在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。栈:在执行函数时,函数内局部变量的存
- 前言Vector是java.util包中的一个类。 SynchronizedList是java.util.Collections中的一个静态
- Java是如何跳出当前多重循环?不建议使用在最外层前面加一个标记A,然后用break A;可以跳出多重循环因为它不会让你的程序变得更加优雅,
- 引言备忘录模式经常可以遇到,譬如下面这些场景:浏览器回退:浏览器一般有浏览记录,当我们在一个网页上点击几次链接之后,可在左上角点击左箭头回退
- 本文实例讲述了C#实现闪动托盘图标效果的方法。分享给大家供大家参考,具体如下:在用户正在登录QQ或者使用Firemail邮件系统自动收取邮件
- SpringBoot2.3.1版本源码一、SpringBoot启动的时候加载主配置类,通过@EnableAutoConfiguration注解
- 这几天在做公司年会的一个抽奖软件,开始做的的时候,认为算法是很简单的,把员工的数据放进list里,把list的标号作为需要获取的随机数,根据
- Java.lang 中自带的注解@Override:表示当前的方法定义将覆盖基类的方法。如果你不小心拼写错误,或者方法签名被错误拼写的时候,
- 微服务feign调用添加token1.一般情况是这么配置的具体的怎么调用就不说了 如下配置,就可以在请求头中添加需要的请求头信息。packa
- 在C# 的应用程序开发中, 我们经常要把UI线程和工作线程分开,防止界面停止响应, 同时我们又需要在工作线程中更新UI界面上的控件。但直接访
- 安全无处不在,趁着放假读了一下 Shiro 文档,并记录一下 Shiro 整合 Spring Boot 在数据库中根据角色控制访问权限简介A
- 系统参数系统级全局变量,该参数在程序中任何位置都可以访问到。优先级最高,覆盖程序中同名配置。系统参数的标准格式为:-Dargname=arg
- 本文实例讲述了Android Dialog对话框用法。分享给大家供大家参考,具体如下:Activities提供了一种方便管理的创建、保存、回
- 第一部分 问题描述1.1 具体任务本次作业任务是轨迹压缩,给定一个GPS数据记录文件,每条记录包含经度和维度两个坐标字段,所有记录的经纬度坐
- 我们知道springboot中的Bean组件的成员变量(属性)如果加上了@Value注解,可以从有效的配置属性资源中找到配置项进行绑定,那么
- 我也不知道这个叫什么,就是比如我要打开我电脑的计算机,可以直接在命令行输入“calc”就可以了。现在用让代码去执行。public stati
- flutter material widget组件之信息展示组件,供大家参考,具体内容如下widget分为两类:widgets librar
- 优点1.一个调用者想创建一个对象,只要知道其名称就可以了。2.扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。3.屏蔽产品的具体实现