在Unity上实时操纵网格

图片

Unity作为游戏开发平台的优势之一是其强大的3D引擎。 在本教程中,您将了解3D对象和网格处理的世界。

由于虚拟和增强现实(VR / AR)技术的发展,大多数开发人员都面临着复杂的3D图形概念。 让本教程成为他们的起点。 不用担心,不会有复杂的3D数学-只有心,绘图,箭头和许多有趣的东西!

注意:本教程适用于熟悉Unity IDE并且具有C#编程经验的用户。 如果您不了解这些知识,请首先学习教程“ Unity UI 简介”“ Unity脚本编制简介”

您将需要一个不低于2017.3.1的Unity版本。 可以在此处下载Unity的最新版本。 本教程使用自定义编辑器,您可以从扩展Unity编辑器教程中了解有关它们的更多信息。

开始工作


首先,请熟悉3D图形的基本术语,这将使您更好地理解本教程。

3D图形的基本技术术语:

  • 顶点 :每个顶点是3D空间中的一个点。
  • 网格 :包含模型的所有顶点,边,三角形,法线和UV数据。
  • 网格过滤器 :存储模型网格数据。
  • 网格渲染器 :渲染场景中的网格数据。
  • 法线 :顶点或曲面的向量。 它指向外部,垂直于网格表面。
  • 线/边 :连接顶点的不可见线。
  • 三角形 :通过连接三个峰形成。
  • UV贴图 :将材质附加到对象上,为其创建纹理和颜色。

3D对象的解剖始于其网格。 该网格的创建始于其顶部。 连接这些顶点的不可见线形成定义对象基本形状的三角形。


然后,法线和UV数据设置阴影,颜色和纹理。 网格数据存储在网格过滤器中,并且网格渲染器使用此数据在场景中绘制对象。

也就是说,用于创建3D模型的伪代码如下所示:

  • 创建一个名为“ myMesh”的新网格。
  • 将数据添加到顶点和三角形myMesh的属性。
  • 创建一个名为“ myMeshFilter”的新网格过滤器。
  • 将网格属性myMeshFilter设置为myMesh。

掌握了基础知识之后,请下载项目 ,解压缩文件,然后在Unity中运行项目的工件。 在“ 项目”窗口中查看文件夹结构:


文件夹说明:

  • 预制件 :包含Sphere预制件,将用于在应用程序执行期间保存3D网格。
  • 场景 :包含本教程中使用的三个场景。
  • 编辑器 :此文件夹中的脚本为我们提供了我们在开发中使用的编辑器的超级功能。
  • 脚本 :这是附加到GameObject并在您单击Play时执行的运行时脚本。
  • 材料 :此文件夹包含网格的材料。

在下一部分中,我们将创建一个自定义编辑器,以可视化3D网格的创建。

使用自定义编辑器更改网格


打开位于Scenes文件夹中的01 Mesh Study Demo 。 在“ 场景”窗口中,您将看到一个3D立方体:


在进入网格之前,让我们看一下自定义编辑器脚本。

编辑编辑器脚本


在“ 项目”窗口中选择“ 编辑器”文件夹。 此文件夹中的脚本在开发过程中向编辑器(Editor)添加功能,并且在Build模式下不可用。


打开MeshInspector.cs并查看源代码。 所有的Editor脚本都必须实现Editor类,其CustomEditor属性告诉Editor类适用于什么类型的对象。 OnSceneGUI()是一种事件方法,允许在“场景”窗口中渲染; OnInspectorGUI()允许您将其他GUI元素添加到Inspector。

MeshInspector.cs中,在启动MeshInspector类之前MeshInspector添加以下内容:

 [CustomEditor(typeof(MeshStudy))] 

代码说明: CustomEditor属性告诉Unity自定义编辑器类可以修改的对象类型。

OnSceneGUI()EditMesh()之前EditMesh()添加以下内容:

 mesh = target as MeshStudy; Debug.Log("Custom editor is running"); 

代码说明: Editor类具有一个标准target变量。 在这里, target是对MeshStudy的转换。 现在,自定义编辑器将在“场景”窗口中绘制所有GameObject ,并将它们附加到MeshStudy.cs 。 添加调试消息使您可以在控制台中验证自定义编辑器是否正在实际运行。

保存文件并返回到Unity。 转到Scripts文件夹,然后将MeshStudy.cs拖到层次结构中的GameObject Cube上以将其附加。


现在,消息“自定义编辑器正在运行”应该显示在控制台中,这意味着我们做对了所有事情! 您可以删除调试消息,以便它不会在控制台中困扰我们。

克隆和转储网格


使用自定义编辑器在“编辑”模式下使用3D网格时,请注意不要覆盖默认的Unity网格。 如果发生这种情况,则必须重新启动Unity。

若要安全地克隆网格而不覆盖原始表单,请从MeshFilter.sharedmesh属性创建网格的副本,然后将其再次分配给网格过滤器。

为此,在Scripts文件夹中双击MeshStudy.cs ,以在代码编辑器中打开文件。 该脚本继承自MonoBehaviour类,并且其Start()函数不在编辑模式下执行。

MeshStudy.cs中,在启动MeshStudy类之前MeshStudy添加以下内容:

 [ExecuteInEditMode] 

代码说明:添加此属性后,将在播放模式和编辑模式下执行Start()函数。 现在,我们可以首先实例化网格对象并将其克隆。

InitMesh()添加以下代码:

 oMeshFilter = GetComponent<MeshFilter>(); oMesh = oMeshFilter.sharedMesh; //1 cMesh = new Mesh(); //2 cMesh.name = "clone"; cMesh.vertices = oMesh.vertices; cMesh.triangles = oMesh.triangles; cMesh.normals = oMesh.normals; cMesh.uv = oMesh.uv; oMeshFilter.mesh = cMesh; //3 vertices = cMesh.vertices; //4 triangles = cMesh.triangles; isCloned = true; Debug.Log("Init & Cloned"); 

代码说明:

  1. MeshFilter组件获取原始的oMesh网格。
  2. cMesh复制到新的Mesh cMesh
  3. 再次分配复制的网格网格过滤器。
  4. 更新局部变量。

保存文件并返回到Unity。 消息“ Init&Cloned”应显示在调试控制台中。 在层次结构中选择GameObject Cube ,然后在检查器中检查其属性。 网格过滤器应显示一个称为clone的网格资产。 太好了! 这意味着我们已经成功克隆了网格。


在编辑器文件夹中,导航到MeshInspector.cs 。 在OnInspectorGUI() ,在第二行代码之后,添加以下内容:

 if (GUILayout.Button("Reset")) //1 { mesh.Reset(); //2 } 

代码说明:

  1. 该代码在Inspector中绘制了一个Reset按钮。
  2. 按下时,它将调用MeshStudy.cs中Reset()函数。

保存文件,打开MeshStudy.cs并将以下代码添加到Reset()函数:

 if (cMesh != null && oMesh != null) //1 { cMesh.vertices = oMesh.vertices; //2 cMesh.triangles = oMesh.triangles; cMesh.normals = oMesh.normals; cMesh.uv = oMesh.uv; oMeshFilter.mesh = cMesh; //3 vertices = cMesh.vertices; //4 triangles = cMesh.triangles; } 

代码说明:

  1. 验证源和克隆网格的存在。
  2. cMesh重置为原始网格。
  3. 分配给cMesh oMeshFilter
  4. 更新局部变量。

保存文件并返回到Unity。 在检查器中,单击“ 测试编辑”按钮以扭曲立方体网格。 接下来,单击“ 重置”按钮; 多维数据集应返回其原始形式。


Unity中的顶点和三角形的说明


网格由通过三角形的边连接的顶点组成。 三角形定义了对象的基本形状。

网格类别:

  • 顶点存储为Vector3值的数组。
  • 三角形被存储为对应于顶点数组索引的整数数组。

也就是说,在一个简单的四边形网格中,该网格由四个顶点和两个三角形组成,网格数据将如下所示:


顶点映射


在这里,我们希望将立方体的顶点显示为蓝点。

MeshInspector.cs中,我们进入EditMesh()函数并添加以下内容:

 handleTransform = mesh.transform; //1 handleRotation = Tools.pivotRotation == PivotRotation.Local ? handleTransform.rotation : Quaternion.identity; //2 for (int i = 0; i < mesh.vertices.Length; i++) //3 { ShowPoint(i); } 

代码说明:

  1. handleTransformmesh获取Transform值。
  2. handleRotation获取当前关节的“旋转”模式。
  3. 遍历网格的顶点并使用ShowPoint()绘制点。

ShowPoint()函数中, //draw dot注释之后,添加以下内容:

 Vector3 point = handleTransform.TransformPoint(mesh.vertices[index]); 

代码说明:这条线将顶点的局部位置转换为世界空间中的坐标。

在同一函数的if块中,在刚添加的代码行之后,立即添加以下内容:

 Handles.color = Color.blue; point = Handles.FreeMoveHandle(point, handleRotation, mesh.handleSize, Vector3.zero, Handles.DotHandleCap); 

代码说明:

  1. 使用Handles帮助器类设置点的颜色,大小和位置。
  2. Handles.FreeMoveHandle()创建一个无限运动操纵器,简化了拖放操作,这在下一节中对我们很有用。

保存文件并返回到Unity。 在检查器中检查多维数据集属性,并确保启用了“ 移动顶点”选项。 现在,您应该看到屏幕上的网格已标记有多个蓝点。 在这里-立方体网格的顶部! 尝试对其他3D对象执行此操作,然后观察结果。


移动单个顶点


让我们从操作网格的最简单步骤开始-移动单个顶点。

转到MeshInspector.cs 。 在ShowPoint()函数内部, //drag注释之后//drag if块的右括号之前,添加以下内容:

 if (GUI.changed) //1 { mesh.DoAction(index, handleTransform.InverseTransformPoint(point)); //2 } 

代码说明:

  1. GUI.changed跟踪所有随点发生的变化,并与Handles.FreeMoveHandle()一起很好地工作,以识别拖放操作。
  2. 对于可拖动的顶点, mesh.DoAction()函数接收其索引和Transform值作为参数。 由于顶点的Transform值位于世界空间中,因此我们可以使用InverseTransformPoint()将其转换为局部空间。

保存脚本文件并转到MeshStudy.cs 。 在DoAction() ,在方括号后面添加以下内容:

 PullOneVertex(index, localPos); 

然后将以下内容添加到PullOneVertex()函数中:

 vertices[index] = newPos; //1 cMesh.vertices = vertices; //2 cMesh.RecalculateNormals(); //3 

代码说明:

  1. 我们使用值newPos更新目标顶点。
  2. cMesh.vertices更新的顶点值cMesh.verticescMesh.vertices
  3. RecalculateNormals()重新计算并重画网格,使其与更改匹配。

保存文件并返回到Unity。 尝试在立方体上拖动点; 你看到破碎的网格了吗?


似乎某些顶点具有相同的位置,因此,当我们仅拖动一个顶点时,其余顶点仍保留在其后面,并且网格中断。 在下一节中,我们将解决此问题。

查找所有相似的顶点


在外观上,一个立方体网格由八个顶点,六个边和12个三角形组成。 让我们检查是否是这样。


打开MeshStudy.cs ,在Start()函数前面看一下,找到vertices变量。 我们将看到以下内容:

 [HideInInspector] public Vector3[] vertices; 

代码说明: [HideInInspector]从“ 检查器”窗口隐藏共享变量。

注释掉此属性:

 //[HideInInspector] public Vector3[] vertices; 

注意:隐藏顶点值有助于[HideInInspector]处理更复杂的3D网格。 由于顶点数组的大小可以达到数千个元素,因此在尝试在Inspector中查看数组值时,这可能导致Unity无法使用。

保存文件并返回到Unity。 转到检查器 。 现在,在“ 网格研究”脚本组件下,显示了vertices属性。 单击它旁边的箭头图标; 因此您可以Vector3元素Vector3数组。


您会看到数组的大小为24,也就是说,存在具有相同位置的顶点! 在继续之前,请确保取消注释[HideInInspector]

为什么有24个顶点?
关于这个主题有很多理论。 但是,最简单的答案是:立方体有六个边,每个边由形成一个平面的四个顶点组成。

因此,计算如下:6 x 4 = 24个顶点。

您可以搜索其他答案。 但就目前而言,知道某些网格物体的顶点位置相同是很简单的。

MeshStudy.cs中,DoAction()函数中的所有代码替换为以下内容:

 PullSimilarVertices(index, localPos); 

让我们进入PullSimilarVertices()函数并添加以下内容:

 Vector3 targetVertexPos = vertices[index]; //1 List<int> relatedVertices = FindRelatedVertices(targetVertexPos, false); //2 foreach (int i in relatedVertices) //3 { vertices[i] = newPos; } cMesh.vertices = vertices; //4 cMesh.RecalculateNormals(); 

代码说明:

  1. 我们获得目标顶点的位置,该位置将用作FindRelatedVertices()方法的参数。
  2. 此方法返回与目标顶点位置相同的索引列表(对应于顶点)。
  3. 循环遍历整个列表,并将相应的顶点设置为newPos
  4. cMesh.vertices更新的vertices cMesh.verticescMesh.vertices 。 然后,我们调用RecalculateNormals()重新绘制具有新值的网格。

保存文件并返回到Unity。 拖动任何一个顶点; 现在,网格应保持其形状而不塌陷。


现在,我们已经完成了操作网格的第一步,请保存场景并继续进行下一部分。

网格处理


在本节中,您将学习有关实时操纵网格的知识。 有很多方法,但是在本教程中,我们将介绍最简单的网格处理类型,即移动先前创建的网格顶点。

收集选定的索引


让我们从选择实时移动的顶点开始。

打开“场景02”,从“ 场景”文件夹中创建“心脏网格 ”。 在“场景”窗口中,您将看到一个红色的球体。 在“ 层次结构”中选择“ 球形” ,然后转到“ 检查器” 。 您将看到Heart Mesh脚本组件已附加到该对象。

现在,我们需要此对象的编辑器脚本,以在“场景”窗口中显示网格的顶点。 转到“ 编辑器”文件夹,然后双击HeartMeshInspector.cs

ShowHandle()函数的if块内,添加以下内容:

 Handles.color = Color.blue; if (Handles.Button(point, handleRotation, mesh.pickSize, mesh.pickSize, Handles.DotHandleCap)) //1 { mesh.selectedIndices.Add(index); //2 } 

代码说明:

  1. 设置并显示网格的顶点,类型为Handles.Button
  2. 单击后,会将选定的索引添加到按下的mesh.selectedIndices

OnInspectorGUI() ,在OnInspectorGUI()括号之前,添加以下内容:

 if (GUILayout.Button("Clear Selected Vertices")) { mesh.ClearAllData(); } 

代码说明:这是我们向“ 检查器”中添加“重置”按钮以调用mesh.ClearAllData()

保存文件, 然后从“ 脚本”文件夹中打开HeartMesh.cs 。 在ClearAllData()函数中,添加以下内容:

 selectedIndices = new List<int>(); targetIndex = 0; targetVertex = Vector3.zero; 

代码说明:代码清除selectedIndicestargetIndex的值。 它还会重置targetVertex

保存文件并返回到Unity。 选择Sphere并转到“ 检查器”中的HeartMesh脚本组件 。 通过单击旁边的箭头图标来展开选定的索引 。 这将使我们能够跟踪添加到列表中的每个顶点。

使用旁边的复选框启用“ 是编辑模式 ”。 因此,将在“场景”窗口中绘制网格的顶点。 单击“ 选定指数”中的蓝点应相应地更改值。 还要测试“ 清除选定的顶点”按钮,以确保它清除了所有值。


注意:在经过修改的自定义Inspector中 ,我们可以选择使用Show Transform Handle显示/隐藏转换操纵器。 因此,如果在其他场景中找不到“变形”操纵器,请不要惊慌! 退出前将其打开。

把球变成一颗心


实时更改网格顶点基本上包括三个步骤:

  1. 将当前网格顶点(在动画之前)复制到mVertices
  2. mVertices计算并更改mVertices的值。
  3. 在每一步更改时,使用mVertices更新当前的网格顶点,并让Unity自动计算法线。

Start()函数之前打开HeartMesh.cs和以下变量:

 public float radiusofeffect = 0.3f; //1 public float pullvalue = 0.3f; //2 public float duration = 1.2f; //3 int currentIndex = 0; //4 bool isAnimate = false; float starttime = 0f; float runtime = 0f; 

代码说明:

  1. 受目标顶点影响的区域的半径。
  2. 拖动力。
  3. 动画的持续时间。
  4. selectedIndices列表的当前索引。

Init()函数的if块之前,添加以下内容:

 currentIndex = 0; 

代码说明:在游戏开始时, currentIndex为0,即selectedIndices列表的第一个索引。

在同一个Init()函数中, else块的右括号之前,添加以下内容:

 StartDisplacement(); 

代码说明:如果isEditMode为false,请运行StartDisplacement()函数。

StartDisplacement()函数内,添加以下内容:

 targetVertex = oVertices[selectedIndices[currentIndex]]; //1 starttime = Time.time; //2 isAnimate = true; 

代码说明:

  1. 选择targetVertex以开始动画。
  2. 设置开始时间并将isAnimate的值isAnimate为true。

StartDisplacement()函数之后,使用以下代码创建FixedUpdate()函数:

 void FixedUpdate() //1 { if (!isAnimate) //2 { return; } runtime = Time.time - starttime; //3 if (runtime < duration) //4 { Vector3 targetVertexPos = oFilter.transform.InverseTransformPoint(targetVertex); DisplaceVertices(targetVertexPos, pullvalue, radiusofeffect); } else //5 { currentIndex++; if (currentIndex < selectedIndices.Count) //6 { StartDisplacement(); } else //7 { oMesh = GetComponent<MeshFilter>().mesh; isAnimate = false; isMeshReady = true; } } } 

代码说明:

  1. FixedUpdate()函数在固定的FPS循环中执行。
  2. 如果isAnimate为false,则跳过以下代码。
  3. 更改runtime动画。
  4. 如果runtimeduration内以内,那么我们将获得targetVertexDisplaceVertices()的世界坐标,并使用pullvalueradiusofeffect覆盖目标顶点。
  5. 否则,时间到了。 将一个添加到currentIndex
  6. 检查currentIndexselectedIndices 。 使用StartDisplacement()转到列表中的下一个顶点。
  7. 否则,在列表的末尾,将oMesh数据更改为当前网格, isAnimate为false以停止动画。

DisplaceVertices()添加以下内容:

 Vector3 currentVertexPos = Vector3.zero; float sqrRadius = radius * radius; //1 for (int i = 0; i < mVertices.Length; i++) //2 { currentVertexPos = mVertices[i]; float sqrMagnitute = (currentVertexPos - targetVertexPos).sqrMagnitude; //3 if (sqrMagnitute > sqrRadius) { continue; //4 } float distance = Mathf.Sqrt(sqrMagnitute); //5 float falloff = GaussFalloff(distance, radius); Vector3 translate = (currentVertexPos * force) * falloff; //6 translate.z = 0f; Quaternion rotation = Quaternion.Euler(translate); Matrix4x4 m = Matrix4x4.TRS(translate, rotation, Vector3.one); mVertices[i] = m.MultiplyPoint3x4(currentVertexPos); } oMesh.vertices = mVertices; //7 oMesh.RecalculateNormals(); 

代码说明:

  1. 半径的平方。
  2. 我们遍历网格的每个顶点。
  3. sqrMagnitude currentVertexPostargetVertexPos之间的targetVertexPos
  4. 如果sqrMagnitude超过sqrRadius ,则转到下一个顶点。
  5. 否则,请继续定义falloff值,该值取决于当前顶点到示波器中心点的distance
  6. Vector3新的Vector3位置,并将其“变换”应用于当前顶点。
  7. 当您退出循环时,我们会将更改的mVertices值分配给mVertices ,并强制Unity重新计算法线。

衰减技术的来源
原始公式来自“ 过程示例”资产软件包文件,该文件可从Unity Asset Store免费下载。

保存文件并返回到Unity。 选择Sphere ,转到HeartMesh组件,然后尝试将一些顶点添加到Selected Indices属性中。 禁用“ 是编辑”模式 ,然后单击“ 播放”以查看工作结果。


使用RadiusofeffectPullvalueDuration值进行实验以获得不同的结果。 准备就绪后,请根据以下屏幕截图更改设置。


点击播放 。 您的领域变成了一颗心吗?


恭喜你! 在下一部分中,我们将网格保存为预制件,以备将来使用。

实时保存网格


要在“播放”模式下保存心形程序网格,您需要准备一个预制件,其子代将是3D对象,然后使用脚本将其网格资源替换为新的预制资产。

在“ 项目”窗口中Prefabs文件夹中找到CustomHeart 。 单击箭头图标以展开其内容,然后选择“ 子级” 。 现在,您将在“ 检查器”预览窗口中看到一个Sphere对象。 这是预制件,将存储新网格的数据。


打开HeartMeshInspector.cs 。 在OnInspectorGUI()函数内,在OnInspectorGUI()括号之前,添加以下内容:

 if (!mesh.isEditMode && mesh.isMeshReady) { string path = "Assets/Prefabs/CustomHeart.prefab"; //1 if (GUILayout.Button("Save Mesh")) { mesh.isMeshReady = false; Object pfObj = AssetDatabase.LoadAssetAtPath(path, typeof(GameObject)); //2 Object pfRef = AssetDatabase.LoadAssetAtPath (path, typeof(GameObject)); GameObject gameObj = (GameObject)PrefabUtility.InstantiatePrefab(pfObj); Mesh pfMesh = (Mesh)AssetDatabase.LoadAssetAtPath(path, typeof(Mesh)); //3 if (!pfMesh) { pfMesh = new Mesh(); } else { pfMesh.Clear(); } pfMesh = mesh.SaveMesh(); //4 AssetDatabase.AddObjectToAsset(pfMesh, path); gameObj.GetComponentInChildren<MeshFilter>().mesh = pfMesh; //5 PrefabUtility.ReplacePrefab(gameObj, pfRef, ReplacePrefabOptions.Default); //6 Object.DestroyImmediate(gameObj); //7 } } 

代码说明:

  1. 将路径设置为CustomHeart 预制对象的路径。
  2. 从CustomHeart 预制中创建两个对象,一个用于创建实例作为pfObjpfObj ),第二个用于创建链接( pfRef )。
  3. pfMesh创建pfMesh网格物体pfMesh 实例。 如果找不到,则创建一个新的网格,否则将清理现有数据。
  4. 用新的网格数据pfMesh ,然后将其作为资产添加到CustomHeart
  5. gameObjpfMesh填充gameObj的网格gameObj资产。
  6. gameObj匹配预先存在的连接,用gameObj替换CustomHeart
  7. 立即销毁gameObj

保存文件并转到HeartMesh.cs 。 在常规的SaveMesh()方法中,创建nMesh实例后nMesh添加以下内容:

 nMesh.name = "HeartMesh"; nMesh.vertices = oMesh.vertices; nMesh.triangles = oMesh.triangles; nMesh.normals = oMesh.normals; 

代码说明:返回带有心形网格物体值的网格物体资产。

保存文件并返回到Unity。 点击播放 。 动画完成后,“ 保存网格”按钮将出现在“ 检查器”中 。 单击按钮保存新的网格,然后停止播放器。

转到Prefabs文件夹,然后查看CustomHeart 预制件 。 您应该看到,现在CustomHeart 预制对象中有了一个全新的心形网格。


干得好!

全部放在一起


在上一个场景中, DisplaceVertices()函数使用Falloff公式来确定在给定半径内应用于每个顶点的拖动力。 阻力开始减小的“下降”点取决于所使用的衰减类型:线性,高斯或针。 每种类型在网格中产生不同的结果。


在本节中,我们将介绍另一种操作顶点的方法:使用给定曲线。 根据速度等于距离除以时间(d =(v / t))的规则,我们可以参考向量的距离除以时间来确定向量的位置。


使用曲线法


保存当前场景,然后从“ 场景”文件夹中打开03“自定义心脏网格 ”。 您将看到CustomHeart 预制件Hierarchy实例。 单击其旁边的箭头图标以展开其内容,然后选择Child

检查器中查看其属性。您将看到带有“ 心脏网格资源” 的“ 网格过滤器”组件将“ 自定义心脏”脚本作为组件附加到Child现在,资产应从HeartMesh更改克隆


接下来,Scripts文件夹中打开CustomHeart.cs在功能之前,添加以下内容:Start()

 public enum CurveType { Curve1, Curve2 } public CurveType curveType; Curve curve; 

代码说明:在此名称下创建了一个通用枚举CurveType,之后可从Inspector中使用它

转到CurveType1()并添加以下内容:

 Vector3[] curvepoints = new Vector3[3]; //1 curvepoints[0] = new Vector3(0, 1, 0); curvepoints[1] = new Vector3(0.5f, 0.5f, 0); curvepoints[2] = new Vector3(1, 0, 0); curve = new Curve(curvepoints[0], curvepoints[1], curvepoints[2], false); //2 

代码说明:

  1. 一个简单的曲线由三个点组成。设置第一条曲线的点。
  2. 我们借助帮助生成第一条曲线Curve()并指定其值curve如果将true指定为最后一个参数,则可以在预览中显示绘制的曲线。

转到CurveType2()并添加以下内容:

 Vector3[] curvepoints = new Vector3[3]; //1 curvepoints[0] = new Vector3(0, 0, 0); curvepoints[1] = new Vector3(0.5f, 1, 0); curvepoints[2] = new Vector3(1, 0, 0); curve = new Curve(curvepoints[0], curvepoints[1], curvepoints[2], false); //2 

代码说明:

  1. 设置第二条曲线的点。
  2. 我们用生成第二条曲线Curve()并指定其值curve如果将true指定为最后一个参数,则可以在预览中显示绘制的曲线。

B StartDisplacement(),在右括号之前,添加以下内容:

 if (curveType == CurveType.Curve1) { CurveType1(); } else if (curveType == CurveType.Curve2) { CurveType2(); } 

代码说明:在这里,我们检查用户选择的选项curveType并相应地生成它curve

B DisplaceVertices(),在循环语句for的右括号之前,添加以下内容:

 float increment = curve.GetPoint(distance).y * force; //1 Vector3 translate = (vert * increment) * Time.deltaTime; //2 Quaternion rotation = Quaternion.Euler(translate); Matrix4x4 m = Matrix4x4.TRS(translate, rotation, Vector3.one); mVertices[i] = m.MultiplyPoint3x4(mVertices[i]); 

代码说明:

  1. 我们得到了一个给定曲线上的位置distance,并乘其价值yforce获得increment
  2. 创建一个新的数据类型Vector3来存储当前顶点的新位置,并相应地应用其“变换”。

保存文件并返回到Unity。检查组件的属性CustomHeart游戏对象儿童您将看到一个下拉列表,您可以在其中选择“ 曲线类型”从“ 编辑类型”下拉列表中,选择“ 添加索引”或“ 删除索引”以更新顶点列表并尝试不同的设置。


要查看不同类型曲线的详细结果,请根据屏幕截图输入值:


对于“ 曲线类型”列表,选择“ Curve1”,确保选择编辑类型”,然后单击“ 播放”您应该看到网格分散到图案中。滚动模型以在侧视图中查看它,并比较两种曲线的结果。在这里,您将看到所选曲线类型如何影响网格偏移。



仅此而已!您可以单击“ 清除选定的顶点”以重置“ 选定的索引”并尝试使用自己的样式。但是不要忘记,还有其他因素会影响网格的最终结果,即:

  • 半径值。
  • 顶点在该区域中的分布。
  • 选定顶点的图案位置。
  • 为偏移量选择的方法。

接下来要去哪里?


完成项目的文件位于教程项目存档中

不要在这里停下来!尝试使用Unity Procedural Maze Generation教程中使用的更复杂的技术

我希望您喜欢本教程并发现信息有帮助。特别感谢我表达碧玉弗里克猫爪编码为他的优秀教程,帮我组装我的项目演示。

Source: https://habr.com/ru/post/zh-CN428796/


All Articles