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;
代码说明:
- 从
MeshFilter
组件获取原始的oMesh
网格。 - 将
cMesh
复制到新的Mesh cMesh
。 - 再次分配复制的网格网格过滤器。
- 更新局部变量。
保存文件并返回到Unity。 消息“ Init&Cloned”应显示在调试控制台中。 在
层次结构中选择GameObject
Cube
,然后在
检查器中检查其属性。
网格过滤器应显示一个称为
clone的网格资产。 太好了! 这意味着我们已经成功克隆了网格。
在编辑器文件夹中,导航到
MeshInspector.cs 。 在
OnInspectorGUI()
,在第二行代码之后,添加以下内容:
if (GUILayout.Button("Reset"))
代码说明:
- 该代码在Inspector中绘制了一个Reset按钮。
- 按下时,它将调用MeshStudy.cs中的
Reset()
函数。
保存文件,打开
MeshStudy.cs并将以下代码添加到
Reset()
函数:
if (cMesh != null && oMesh != null)
代码说明:
- 验证源和克隆网格的存在。
- 将
cMesh
重置为原始网格。 - 分配给
cMesh
oMeshFilter
。 - 更新局部变量。
保存文件并返回到Unity。 在
检查器中,单击“
测试编辑”按钮以扭曲立方体网格。 接下来,单击“
重置”按钮; 多维数据集应返回其原始形式。
Unity中的顶点和三角形的说明
网格由通过三角形的边连接的顶点组成。 三角形定义了对象的基本形状。
网格类别:
- 顶点存储为
Vector3
值的数组。 - 三角形被存储为对应于顶点数组索引的整数数组。
也就是说,在一个简单的四边形网格中,该网格由四个顶点和两个三角形组成,网格数据将如下所示:
顶点映射
在这里,我们希望将立方体的顶点显示为蓝点。
在
MeshInspector.cs中,我们
将进入
EditMesh()
函数并添加以下内容:
handleTransform = mesh.transform;
代码说明:
handleTransform
从mesh
获取Transform值。handleRotation
获取当前关节的“旋转”模式。- 遍历网格的顶点并使用
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);
代码说明:
- 使用
Handles
帮助器类设置点的颜色,大小和位置。 Handles.FreeMoveHandle()
创建一个无限运动操纵器,简化了拖放操作,这在下一节中对我们很有用。
保存文件并返回到Unity。 在检查器中检查多维数据集属性,并确保启用了“
移动顶点”选项。 现在,您应该看到屏幕上的网格已标记有多个蓝点。 在这里-立方体网格的顶部! 尝试对其他3D对象执行此操作,然后观察结果。
移动单个顶点
让我们从操作网格的最简单步骤开始-移动单个顶点。
转到
MeshInspector.cs 。 在
ShowPoint()
函数内部,
//drag
注释之后
//drag
if
块的右括号之前,添加以下内容:
if (GUI.changed)
代码说明:
GUI.changed
跟踪所有随点发生的变化,并与Handles.FreeMoveHandle()
一起很好地工作,以识别拖放操作。- 对于可拖动的顶点,
mesh.DoAction()
函数接收其索引和Transform值作为参数。 由于顶点的Transform值位于世界空间中,因此我们可以使用InverseTransformPoint()
将其转换为局部空间。
保存脚本文件并转到
MeshStudy.cs 。 在
DoAction()
,在方括号后面添加以下内容:
PullOneVertex(index, localPos);
然后将以下内容添加到
PullOneVertex()
函数中:
vertices[index] = newPos;
代码说明:
- 我们使用值
newPos
更新目标顶点。 cMesh.vertices
更新的顶点值cMesh.vertices
回cMesh.vertices
。- 在
RecalculateNormals()
重新计算并重画网格,使其与更改匹配。
保存文件并返回到Unity。 尝试在立方体上拖动点; 你看到破碎的网格了吗?
似乎某些顶点具有相同的位置,因此,当我们仅拖动一个顶点时,其余顶点仍保留在其后面,并且网格中断。 在下一节中,我们将解决此问题。
查找所有相似的顶点
在外观上,一个立方体网格由八个顶点,六个边和12个三角形组成。 让我们检查是否是这样。
打开
MeshStudy.cs ,在
Start()
函数前面看一下,找到
vertices
变量。 我们将看到以下内容:
[HideInInspector] public Vector3[] vertices;
代码说明:
[HideInInspector]
从“
检查器”窗口隐藏共享变量。
注释掉此属性:
注意:隐藏顶点值有助于[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];
代码说明:
- 我们获得目标顶点的位置,该位置将用作
FindRelatedVertices()
方法的参数。 - 此方法返回与目标顶点位置相同的索引列表(对应于顶点)。
- 循环遍历整个列表,并将相应的顶点设置为
newPos
。 cMesh.vertices
更新的vertices
cMesh.vertices
回cMesh.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))
代码说明:
- 设置并显示网格的顶点,类型为
Handles.Button
。 - 单击后,会将选定的索引添加到按下的
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;
代码说明:代码清除
selectedIndices
和
targetIndex
的值。 它还会重置
targetVertex
。
保存文件并返回到Unity。 选择
Sphere并转到“
检查器”中的
HeartMesh脚本
组件 。 通过单击旁边的箭头图标来展开
选定的索引 。 这将使我们能够跟踪添加到列表中的每个顶点。
使用旁边的复选框启用“
是编辑模式 ”。 因此,将在“场景”窗口中绘制网格的顶点。 单击“
选定指数”中的蓝点应相应地更改值。 还要测试“
清除选定的顶点”按钮,以确保它清除了所有值。
注意:在经过修改的自定义
Inspector中 ,我们可以选择使用
Show Transform Handle显示/隐藏转换操纵器。 因此,如果在其他场景中找不到“变形”操纵器,请不要惊慌! 退出前将其打开。
把球变成一颗心
实时更改网格顶点基本上包括三个步骤:
- 将当前网格顶点(在动画之前)复制到
mVertices
。 mVertices
计算并更改mVertices
的值。- 在每一步更改时,使用
mVertices
更新当前的网格顶点,并让Unity自动计算法线。
在
Start()
函数之前打开
HeartMesh.cs和以下变量:
public float radiusofeffect = 0.3f;
代码说明:
- 受目标顶点影响的区域的半径。
- 拖动力。
- 动画的持续时间。
selectedIndices
列表的当前索引。
在
Init()
函数的
if
块之前,添加以下内容:
currentIndex = 0;
代码说明:在游戏开始时,
currentIndex
为0,即
selectedIndices
列表的第一个索引。
在同一个
Init()
函数中,
else
块的右括号之前,添加以下内容:
StartDisplacement();
代码说明:如果
isEditMode
为false,请运行
StartDisplacement()
函数。
在
StartDisplacement()
函数内,添加以下内容:
targetVertex = oVertices[selectedIndices[currentIndex]];
代码说明:
- 选择
targetVertex
以开始动画。 - 设置开始时间并将
isAnimate
的值isAnimate
为true。
在
StartDisplacement()
函数之后,使用以下代码创建
FixedUpdate()
函数:
void FixedUpdate()
代码说明:
FixedUpdate()
函数在固定的FPS循环中执行。- 如果
isAnimate
为false,则跳过以下代码。 - 更改
runtime
动画。 - 如果
runtime
在duration
内以内,那么我们将获得targetVertex
和DisplaceVertices()
的世界坐标,并使用pullvalue
和radiusofeffect
覆盖目标顶点。 - 否则,时间到了。 将一个添加到
currentIndex
。 - 检查
currentIndex
在selectedIndices
。 使用StartDisplacement()
转到列表中的下一个顶点。 - 否则,在列表的末尾,将
oMesh
数据更改为当前网格, isAnimate
为false以停止动画。
在
DisplaceVertices()
添加以下内容:
Vector3 currentVertexPos = Vector3.zero; float sqrRadius = radius * radius;
代码说明:
- 半径的平方。
- 我们遍历网格的每个顶点。
sqrMagnitude
currentVertexPos
和targetVertexPos
之间的targetVertexPos
。- 如果
sqrMagnitude
超过sqrRadius
,则转到下一个顶点。 - 否则,请继续定义
falloff
值,该值取决于当前顶点到示波器中心点的distance
。 Vector3
新的Vector3
位置,并将其“变换”应用于当前顶点。- 当您退出循环时,我们会将更改的
mVertices
值分配给mVertices
,并强制Unity重新计算法线。
衰减技术的来源
原始公式来自“ 过程示例”资产软件包文件,该文件可从Unity Asset Store免费下载。
保存文件并返回到Unity。 选择
Sphere ,转到
HeartMesh组件,然后尝试将一些顶点添加到
Selected Indices属性中。 禁用“
是编辑”模式 ,然后单击“
播放”以查看工作结果。
使用
Radiusofeffect ,
Pullvalue和
Duration值进行实验以获得不同的结果。 准备就绪后,请根据以下屏幕截图更改设置。
点击
播放 。 您的领域变成了一颗心吗?
恭喜你! 在下一部分中,我们将网格保存为预制件,以备将来使用。
实时保存网格
要在“播放”模式下保存心形程序网格,您需要准备一个预制件,其子代将是3D对象,然后使用脚本将其网格资源替换为新的预制资产。
在“
项目”窗口中
,在
Prefabs文件夹中找到
CustomHeart 。 单击箭头图标以展开其内容,然后选择“
子级” 。 现在,您将在“
检查器”预览窗口中看到一个Sphere对象。 这是预制件,将存储新网格的数据。
打开
HeartMeshInspector.cs 。 在
OnInspectorGUI()
函数内,在
OnInspectorGUI()
括号之前,添加以下内容:
if (!mesh.isEditMode && mesh.isMeshReady) { string path = "Assets/Prefabs/CustomHeart.prefab";
代码说明:
- 将路径设置为CustomHeart 预制对象的路径。
- 从CustomHeart 预制中创建两个对象,一个用于创建实例作为
pfObj
( pfObj
),第二个用于创建链接( pfRef
)。 pfMesh
创建pfMesh网格物体pfMesh
的实例。 如果找不到,则创建一个新的网格,否则将清理现有数据。- 用新的网格数据
pfMesh
,然后将其作为资产添加到CustomHeart 。
gameObj
值pfMesh
填充gameObj
的网格gameObj
资产。gameObj
匹配预先存在的连接,用gameObj
替换CustomHeart 。- 立即销毁
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];
代码说明:- 一个简单的曲线由三个点组成。设置第一条曲线的点。
- 我们借助帮助生成第一条曲线
Curve()
并指定其值curve
。如果将true指定为最后一个参数,则可以在预览中显示绘制的曲线。
转到CurveType2()
并添加以下内容: Vector3[] curvepoints = new Vector3[3];
代码说明:- 设置第二条曲线的点。
- 我们用生成第二条曲线
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;
代码说明:- 我们得到了一个给定曲线上的位置
distance
,并乘其价值y
上force
获得increment
。 - 创建一个新的数据类型
Vector3
来存储当前顶点的新位置,并相应地应用其“变换”。
保存文件并返回到Unity。检查组件的属性CustomHeart游戏对象儿童。您将看到一个下拉列表,您可以在其中选择“ 曲线类型”。从“ 编辑类型”下拉列表中,选择“ 添加索引”或“ 删除索引”以更新顶点列表并尝试不同的设置。要查看不同类型曲线的详细结果,请根据屏幕截图输入值:对于“ 曲线类型”列表,选择“ Curve1”,确保未选择“ 编辑类型”,然后单击“ 播放”。您应该看到网格分散到图案中。滚动模型以在侧视图中查看它,并比较两种曲线的结果。在这里,您将看到所选曲线类型如何影响网格偏移。仅此而已!
您可以单击“ 清除选定的顶点”以重置“ 选定的索引”并尝试使用自己的样式。但是不要忘记,还有其他因素会影响网格的最终结果,即:- 半径值。
- 顶点在该区域中的分布。
- 选定顶点的图案位置。
- 为偏移量选择的方法。
接下来要去哪里?
完成项目的文件位于教程项目的存档中。不要在这里停下来!尝试使用Unity Procedural Maze Generation教程中使用的更复杂的技术。我希望您喜欢本教程并发现信息有帮助。特别感谢我表达碧玉弗里克的猫爪编码为他的优秀教程,帮我组装我的项目演示。