Mesh 翻译文章汇总
AmyBoy:Unity Mesh Basics(Unity Mesh基础)系列翻译汇总zhuanlan.zhihu.com原作者:Jasper Flick
由于水平有限,可能翻译的会有错误,请大家在评论区指出,我会及时更新改正。
原文链接
Cube Sphere, a Unity C# Tutorialcatlikecoding.com本教程的目标
- 把立方体变成球体
- 在Unity中观察转变过程
- 对转换过程进行研究
- 运用数学推理改进转换方法
在本教程中,我们将创建一个立方体为基础的球体网格,然后使用数学推理来改进它。
本教程使用的版本 Unity2018.4.1。
1 调整圆角立方体
你可能已经注意到,你可以用一个圆形的立方体创建一个完美的球体。 确保它在所有三个维度中大小相等,并将其roundness设置为当前值的一半。
如果你想要的只是一个球体,那么圆角立方体所提供的灵活性只会起到阻碍作用。 因此,让我们为它创建一个单独的组件。 复制 RoundedCube 脚本并将其重命名为 CubeSphere。 然后用一个 gridSize 替换这三个尺寸,并删除roundness字段。
public class CubeSphere : MonoBehaviour {
public int gridSize;
// No more roundness.
private Mesh mesh;
private Vector3[] vertices;
private Vector3[] normals;
private Color32[] cubeUV;
…
private void Generate () {
GetComponent<MeshFilter>().mesh = mesh = new Mesh();
mesh.name = "Procedural Sphere";
CreateVertices();
CreateTriangles();
CreateColliders();
}
…
}
如何将三个大小字段替换为一个?
最方便的方法是使用编辑器重构变量名。这将确保对它们的所有引用也被重命名。首先将xSize重命名为gridSize,然后对ySize和zSize执行相同的操作。或者,执行多个搜索和替换操作。
您将得到一些格式为gridSize + gridSize的代码片段。您可以通过重写表达式来合并这些内容,但是并没有真正的必要,因此不必理会。
去除roundness字段将会导致报错,因为它仍然在一些地方使用,所以我们先保留它。让我们首先看看碰撞器的创建。
我们需要多个盒子和胶囊碰撞器来表示圆形立方体,但球体只需要一个球体碰撞器。 这意味着我们可以删除 AddBoxCollider 和 Addcapsulecolider 方法,并将 CreateColliders 减少到一行代码。
private void CreateColliders () {
gameObject.AddComponent<SphereCollider>();
}
接下来是顶点的位置,这也取决于roundness字段。
我们通过将立方体内部的球进行归一化处理,通过从球内部指向原始立方体某处的向量,将这些向量进行计算后,来形成球体的外表面。
如果我们的球体以它的原点为中心也会很方便。我们还没有在网格和圆角立方体上做这个,但是在这种情况下,它使球体的创建更简单,所以让我们把它居中。
要创建一个单位球体(一个半径为1单位的球体)我们需要对一个以原点为中心的立方体的顶点进行归一化。为了让立方体精确地包含球体,它需要边长为2。
我们可以在 SetVertex 中创建这个立方体的顶点,方法是用网格大小除以原始坐标,将其加倍,然后减去单位向量。
public float radius = 1;
private void SetVertex (int i, int x, int y, int z) {
Vector3 v = new Vector3(x, y, z) * 2f / gridSize - Vector3.one;
normals[i] = v.normalized;
vertices[i] = normals[i];
cubeUV[i] = new Color32((byte)x, (byte)y, (byte)z, 0);
}
这将产生一个单位球体,但是你可能需要一个不同的半径。 所以让我们来设置一下。
public float radius = 1f;
private void SetVertex (int i, int x, int y, int z) {
Vector3 v = new Vector3(x, y, z) * 2f / gridSize - Vector3.one;
normals[i] = v.normalized;
vertices[i] = normals[i] * radius;
cubeUV[i] = new Color32((byte)x, (byte)y, (byte)z, 0);
}
现在所有的错误应该都消失了,我们可以在场景中放置一个由立方体生成的球体对象,或者从头开始,或者替换一个圆形立方体对象的组成部分。
2 研究球体表面网格单元
我们有一种创建球体网格的方法,但是它们有多好呢?让我们从多个角度来看一个球体。网格有多均匀?
网格单元的大小变化很大。 最大的单元格来自立方体面的中间。 它们大约是最小单元的四倍,最小单元来自立方体的角落。
为了弄清楚为什么会这样,我们来观察下一个正方形转换另一个圆形的过程。 我们将使用一个圆,因为这比一个球更容易操作。
我们可以使用Gizmos在Unity中创建可视化效果。我们将使用带有OnDrawGizmosSelected方法的自定义组件。它的工作方式与OnDrawGizmos相似,只是只有在选择对象时才会绘制Gizmos。这样,我们可以将可视化对象添加到场景中,除非选择它,否则它将保持不可见。
using UnityEngine;
public class CircleGizmo : MonoBehaviour {
private void OnDrawGizmosSelected () {
}
}
我们首先用黑色球体显示正方形边上的顶点,分辨率随意设置。 让我们从顶部和底部边缘开始。
public int resolution = 10;
private void OnDrawGizmosSelected () {
float step = 2f / resolution;
for (int i = 0; i <= resolution; i++) {
ShowPoint(i * step - 1f, -1f);
ShowPoint(i * step - 1f, 1f);
}
}
private void ShowPoint (float x, float y) {
Vector2 square = new Vector2(x, y);
Gizmos.color = Color.black;
Gizmos.DrawSphere(square, 0.025f);
}
填充左边缘和右边缘完成正方形。 由于角点已经显示,我们现在可以跳过它们。
private void OnDrawGizmosSelected () {
float step = 2f / resolution;
for (int i = 0; i <= resolution; i++) {
ShowPoint(i * step - 1f, -1f);
ShowPoint(i * step - 1f, 1f);
}
for (int i = 1; i < resolution; i++) {
ShowPoint(-1f, i * step - 1f);
ShowPoint(1f, i * step - 1f);
}
}
接下来,对于每个点,我们也将显示相应的圆顶点,使用一个白色的球体。然后用黄线表示这两个顶点之间的关系。最后是一条从圆顶点到圆心的灰线。
现在我们可以看到正方形转换为圆形的工作原理。将正方形上的顶点坐标做归一化处理后,将会得到一个单位圆(半径为1),越靠近正方形角的位置,归一化得到的点与原顶点的距离会越来越大。实际上,恰好位于圆主轴上的顶点不会移动,而位于正方形对角线上的顶点需要移动√2−1单位长度。
3 使用数学推理出优化方法
现在我们知道为什么面上的元素最终会有不同的大小了。 我们能做点什么吗? 也许有一个不同的映射,产生更多的统一的元素。 如果是这样,我们如何找到这样的映射?
这实际上是一个数学问题,所以让我们以数学专业的角度重新来看待这个映射现象。我们一开始只是使用圆来验证,后面我们可以推广到球面上。
我们把一个正方形上的点映射到一个圆上的点。由于点是用向量来描述的,我们实际上是把一个向量映射到另一个向量。
具体地说,我们的圆是单位圆,但包裹它的是一个边长为2的正方形。 所以我们的映射只是向量
向量的归一化是通过除以它自身的长度来完成的。
你如何得到一个二维矢量的长度? 它由两个坐标组成。
这些坐标定义了一个直角三角形,你可以将它应用到勾股定理上。
矢量的长度就是它的平方根。
现在,我们可以以最明确的形式编写映射。
这很好,但是我们如何证明
这很难处理,所以我们把等式两边都平方。
可以简化此过程,直到它变得很简单。
因为最终表达式的分子和分母相等,所以结果必须是1。 也就是说,除非
这个证明有用吗?它告诉我们有一个公式对于正方形的每个点都是1。这个公式对应于一个从正方形到单位圆的映射。
我们能找到一个不同的公式来做同样的事情吗?如果是,也必须有一个不同的映射!让我们把这个过程反过来。提出一个公式,使我们的正方形上的每个点都是1。
如果我们尝试使其尽可能简单,那么该函数将是什么样?我们知道至少一个坐标始终为-1或1。因此,如果对两个坐标求平方,则至少得到一个1,或者我们可以通过
为什么是用1减而不是加?
我们也可以使用两者均有效。在这种情况下,减法更为直观,因为该公式在原点处产生零,而在离原点更远的点处产生更大的结果。当我们重写它时,它也使我们可以消除1,这很方便。
这个公式可以改写成更简单的形式。
你怎么重写它?
这里有两个中间步骤。
最终结果是1减去这个。
现在我们有了一种新的方法来定义
我们重新整理一下方程的右边,使它成为a+b的形式。
我们现在可以把它分成两个坐标,
你怎么重写它?
这是的过度。
我们找到的这个向量给了我们一个新的映射从正方形到圆。让我们在ShowPoint函数上试试吧!
Vector2 circle;
circle.x = square.x * Mathf.Sqrt(1f - square.y * square.y * 0.5f);
circle.y = square.y * Mathf.Sqrt(1f - square.x * square.x * 0.5f);
事实证明,此映射将点从对角线推向主轴方向。现在它们在轴附近最靠近。幸运的是,相邻点之间的距离比第一种方法更均匀,因此是一种改进。
4 调整映射公式
我们有了一个新的从正方形到圆形的映射公式,但是从立方体到球体呢? 我们可以使用同样的方法吗? 是的,但是我们需要加入第三个坐标,就像我们已经有的两个。 这样我们就得到了指向单位球面上点的向量的平方长度。
实际上,这种方法适用于任意数量的坐标,因此我们可以从任意超立方体映射到任意维度的超球体。 扩展公式只会变得更加复杂。
你要怎么重写?
我们已经知道如何处理两个坐标。
中间步骤
最终结果是1减去这个。
这个三维空间的公式有一个新项
最后的映射仍然是一个平方根。
让我们在 SetVertex 函数中使用它,看看它是什么样子的!
private void SetVertex (int i, int x, int y, int z) {
Vector3 v = new Vector3(x, y, z) * 2f / gridSize - Vector3.one;
float x2 = v.x * v.x;
float y2 = v.y * v.y;
float z2 = v.z * v.z;
Vector3 s;
s.x = v.x * Mathf.Sqrt(1f - y2 / 2f - z2 / 2f + y2 * z2 / 3f);
s.y = v.y * Mathf.Sqrt(1f - x2 / 2f - z2 / 2f + x2 * z2 / 3f);
s.z = v.z * Mathf.Sqrt(1f - x2 / 2f - y2 / 2f + x2 * y2 / 3f);
normals[i] = s;
vertices[i] = normals[i] * radius;
cubeUV[i] = new Color32((byte)x, (byte)y, (byte)z, 0);
}
网格单元离对角线越近,就会变得越扭曲,这是无法避免的。 但是这种新的映射产生的单元比标准化方法大得多。 轴线和立方体拐角处的单元格现在看起来大致相同。 这比我们开始的时候好多了! 现在最大的单元格是沿着立方体边缘的那些单元格。 那些曾经被压扁的单元格,现在被拉长了。
你是怎么得出这样的数学公式的?
虽然它在这里是按逻辑顺序呈现的,但是自己搞清楚这些东西通常会走一条更不稳定的路。当机会出现时,遵循一个清晰的逻辑路径是很好的,但是直觉通常只有在实验之后才会出现。在此之前,调查一下其他人已经发现了什么。
互联网上有很多知识,好好利用吧!在本例中,Philip Nowell在他的博客中描述了立方体到球体的映射。映射背后的直觉来自一个数学堆栈交换问题的答案。
本文git仓库地址
Amy6922/UnityMeshDemogithub.com接下来,我们模拟球体表面受外力的变形的状态。
AmyBoy:UnityMesh编程(4)网格变形zhuanlan.zhihu.com