Unity3D绘制地形的实现方法

项目中肯定会遇到需要用户自己绘制地形的需求,然后根据地形自动生成房间。下面说说我在绘制地形的实现方法。

我们百度可以看到很多关于自己创建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;

里面的处理代码很繁琐,要不断判断凹凸角的问题以及最大夹角。重要的是理解耳切法算法原理以及他的一些判断标准,就能很好的理解以及完成我们的需求了。希望本博客对你有帮助。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Unity5.6大规模地形资源创建方法

    在很多仿真和游戏应用中都需要大规模地形,这样会使3D环境似乎"无限大",增加用户的真实感,比如飞行模拟游戏.那么在Unity中如何实现大规模地形场景呢?官方中文论坛中有一个帖子讲得比较在点,帖子地址在这里.总结起来有三点:地形分块,动态加载和卸载,内存优化.其中前两点是最基本的. 最近一直在研究这个问题,也查了好多资料,博客.论坛,甚至一些科研论文,还并没有发现一套完整的解决方案(也许Asset Store上有,不过并没有找到免费的).于是决定在这里将研究过程中的一些思路和实施方法分享

  • Unity3D绘制地形的实现方法

    项目中肯定会遇到需要用户自己绘制地形的需求,然后根据地形自动生成房间.下面说说我在绘制地形的实现方法. 我们百度可以看到很多关于自己创建mesh的博客,mesh的生成需要三角面顶点坐标以及顶点序列.所以,想要创建我们想要的mesh,首先要获取到绘制mesh的顶点.我们用户在绘制自己想创建的地形时会有很大的自由性.他是随心所欲想怎么画就怎么画.这也造就了很大的错误风险性,要求程序更加智能.好了,下面说下我们给自己程序设定的一些规则. 首先我们设置在绘制的时候摄像头的forward朝向Y轴向上,即我

  • js+html5实现canvas绘制网页时钟的方法

    本文实例讲述了js+html5实现canvas绘制网页时钟的方法,画的是一个可用于网页的.带摆的钟表,可以通过按钮调整其大小和位置,具体实现内容如下 <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>Clock</title> <script type="tex

  • 利用matplotlib+numpy绘制多种绘图的方法实例

    前言 matplotlib 是Python最著名的绘图库,它提供了一整套和matlab相似的命令API,十分适合交互式地进行制图.本文将以例子的形式分析matplot中支持的,分析中常用的几种图.其中包括填充图.散点图(scatter plots).. 条形图(bar plots).等高线图(contour plots). 点阵图和3D图,下面来一起看看详细的介绍: 一.填充图 参考代码 from matplotlib.pyplot import * x=linspace(-3,3,100) y

  • js+html5实现canvas绘制简单矩形的方法

    本文实例讲述了js+html5实现canvas绘制简单矩形的方法.分享给大家供大家参考.具体实现方法如下: <!DOCTYPE html> <html> <body> <canvas id="myCanvas" width="200" height="100" style="border:1px solid #c3c3c3;"> Your browser does not sup

  • Python基于Matplotlib库简单绘制折线图的方法示例

    本文实例讲述了Python基于Matplotlib库简单绘制折线图的方法.分享给大家供大家参考,具体如下: Matplotlib画折线图,有一些离散点,想看看这些点的变动趋势: import matplotlib.pyplot as plt x1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13] y1=[30,31,31,32,33,35,35,40,47,62,99,186,480] x2 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 1

  • Python使用plotly绘制数据图表的方法

    导语:使用 python-plotly 模块来进行压测数据的绘制,并且生成静态 html 页面结果展示. 不少小伙伴在开发过程中都有对模块进行压测的经历,压测结束后大家往往喜欢使用Excel处理压测数据并绘制数据可视化视图,但这样不能很方便的使用web页面进行数据展示.本文将介绍使用python-plotly模块来进行压测数据的绘制,并且生成静态html页面方便结果展示. Plotly简介 Plotly是一款使用JavaScript开发的制图工具,提供了与主流数据分析语言交互的API(如:Pyt

  • MFC绘制不规则窗体的方法

    本文实例讲述了MFC 绘制不规则窗体的方法.分享给大家供大家参考.具体分析如下: 实现过程: 1.首先创建基于DLG的MFC应用程序,命名为:tryBGDlg,并将DLG的属性设置为:Title Bar :False ,其它设置不变 2.制作两幅图像,其中的一幅黑白图像,是根据播放器外观来制作的,其中白色区域是要保留的最终在桌面上显示的区域.将这两幅图像添加到工程中,第一个ID号设置为IDB_INTERFACE,第二个ID号设置为:IDB_MASK 3.在CtryBGDlg类中添加一个在函数:

  • Android编程绘制圆形图片的方法

    本文实例讲述了Android编程绘制圆形图片的方法.分享给大家供大家参考,具体如下: 效果图如下: 第一步:新建RoundView自定义控件继承View package com.rong.activity; import com.rong.test.R; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.grap

  • Android开发中使用achartengine绘制各种图表的方法

    本文实例讲述了Android开发中使用achartengine绘制各种图表的方法.分享给大家供大家参考,具体如下: 1. ABarChart.java package com.anjoyo.achartengine; import java.util.Random; import org.achartengine.ChartFactory; import org.achartengine.chart.BarChart.Type; import org.achartengine.model.Cat

随机推荐