半径为r的均匀带电球体_UnityMesh编程(3) 以圆角立方体为基础 生成球体

Mesh 翻译文章汇总

AmyBoy:Unity Mesh Basics(Unity Mesh基础)系列翻译汇总​zhuanlan.zhihu.com
f73460ee30e218111af2c8e6d87afd97.png

原作者:Jasper Flick

由于水平有限,可能翻译的会有错误,请大家在评论区指出,我会及时更新改正。

原文链接

Cube Sphere, a Unity C# Tutorial​catlikecoding.com
649ebddd8d10fbcd22fd342c079ebbd6.png

本教程的目标

  • 把立方体变成球体
  • 在Unity中观察转变过程
  • 对转换过程进行研究
  • 运用数学推理改进转换方法

在本教程中,我们将创建一个立方体为基础的球体网格,然后使用数学推理来改进它。

本教程使用的版本 Unity2018.4.1。

e1078d4ef18d31b8277c05fd2ca7b92b.png
从一个形状变化到另一个形状

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。

971dcb5848e56e5af413772180905c9b.png
边长为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);
	}

现在所有的错误应该都消失了,我们可以在场景中放置一个由立方体生成的球体对象,或者从头开始,或者替换一个圆形立方体对象的组成部分。

670339ae48d0ee890aa81e5fb1ae23c3.png
配置立方体球体

2 研究球体表面网格单元

我们有一种创建球体网格的方法,但是它们有多好呢?让我们从多个角度来看一个球体。网格有多均匀?

a6a58e62be9cc629cb9d688163a02a98.png
不同的视角观察

网格单元的大小变化很大。 最大的单元格来自立方体面的中间。 它们大约是最小单元的四倍,最小单元来自立方体的角落。

为了弄清楚为什么会这样,我们来观察下一个正方形转换另一个圆形的过程。 我们将使用一个圆,因为这比一个球更容易操作。

我们可以使用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);
	}

086d8f89773c5345991acc00a0630a73.png
一个环形的gizmo对象

填充左边缘和右边缘完成正方形。 由于角点已经显示,我们现在可以跳过它们。

	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);
		}
	}

48899ab89d154987d2b35ae122e60a09.png
完整的正方形

接下来,对于每个点,我们也将显示相应的圆顶点,使用一个白色的球体。然后用黄线表示这两个顶点之间的关系。最后是一条从圆顶点到圆心的灰线。

f379590ff151c3c7f5841558e2f84499.png
从方形到圆形

现在我们可以看到正方形转换为圆形的工作原理。将正方形上的顶点坐标做归一化处理后,将会得到一个单位圆(半径为1),越靠近正方形角的位置,归一化得到的点与原顶点的距离会越来越大。实际上,恰好位于圆主轴上的顶点不会移动,而位于正方形对角线上的顶点需要移动√2−1单位长度。

3 使用数学推理出优化方法

现在我们知道为什么面上的元素最终会有不同的大小了。 我们能做点什么吗? 也许有一个不同的映射,产生更多的统一的元素。 如果是这样,我们如何找到这样的映射?

这实际上是一个数学问题,所以让我们以数学专业的角度重新来看待这个映射现象。我们一开始只是使用圆来验证,后面我们可以推广到球面上。

我们把一个正方形上的点映射到一个圆上的点。由于点是用向量来描述的,我们实际上是把一个向量映射到另一个向量。

具体地说,我们的圆是单位圆,但包裹它的是一个边长为2的正方形。 所以我们的映射只是向量

的规范化版本。

aacf66a9746814d7a5f44b4a5b9f0b7a.png

向量的归一化是通过除以它自身的长度来完成的。

b7ca7e17624d41d3c9d6949e7c68a22c.png

你如何得到一个二维矢量的长度? 它由两个坐标组成。

ad0f3cb6daa13768d4357bfe8ba4899e.png

这些坐标定义了一个直角三角形,你可以将它应用到勾股定理上。

30ad429d646fad54d3a26870d0fb29d6.png

c729049851d419964697b79c024c4360.png
二维矢量描述一个直角三角形

矢量的长度就是它的平方根。

629f9acb9461fcf81e010576a8d8e73e.png

现在,我们可以以最明确的形式编写映射。

cc2578a9cff8d7cebb6d496346dea848.png

这很好,但是我们如何证明

为单位圆上的一个点?要做到这一点,它的长度必须为1,因此,我们必须证明以下等式是正确的。

0af75b566cb69c7c3c24c6fe2cf40ad2.png

这很难处理,所以我们把等式两边都平方。

2fd91636387dca4b46555f1d4eaccbcd.png

可以简化此过程,直到它变得很简单。

4f4853d1c23145365d34dd7e33d6abc0.png

因为最终表达式的分子和分母相等,所以结果必须是1。 也就是说,除非

最终为零,在这种情况下,结果是未定义的。但是我们仅使用正方形边缘上的点作为输入,这保证一个或两个坐标为1或-1,从而保证
这样我们就不会得到不确定的结果。

这个证明有用吗?它告诉我们有一个公式对于正方形的每个点都是1。这个公式对应于一个从正方形到单位圆的映射。

我们能找到一个不同的公式来做同样的事情吗?如果是,也必须有一个不同的映射!让我们把这个过程反过来。提出一个公式,使我们的正方形上的每个点都是1。

如果我们尝试使其尽可能简单,那么该函数将是什么样?我们知道至少一个坐标始终为-1或1。因此,如果对两个坐标求平方,则至少得到一个1,或者我们可以通过

将它们转变为0,因为其中至少有一个总是0,如果我们把它们相乘,我们保证得到0。这就是我们的公式。

bc4c483eaff5f1064a92700623e529d0.png
为什么是用1减而不是加?
我们也可以使用
两者均有效。在这种情况下,减法更为直观,因为该公式在原点处产生零,而在离原点更远的点处产生更大的结果。当我们重写它时,它也使我们可以消除1,这很方便。

这个公式可以改写成更简单的形式。

3635b51d5e93cc57767ddf8553abcc15.png
你怎么重写它?
这里有两个中间步骤。

最终结果是1减去这个。

现在我们有了一种新的方法来定义

的平方长度。

467cb85e5e4e81c8629621ccd9b3dfc3.png

我们重新整理一下方程的右边,使它成为a+b的形式。

的部分是显而易见的,但是如何处理
部分呢?保持事物尽可能的对称是有意义的,所以要平分。

da2aa21576e1e002ccadab753fbb711a.png

我们现在可以把它分成两个坐标,

and
我们可以用不同的方式拆分它,但是这样可以在圆上产生最佳的点分布。现在我们离最终向量只差平方根了。

aa6dc425921fd0f3bd4beb8bc3d208c1.png
你怎么重写它?
这是
的过度。

2adc119029559b3099b46cd1c3e980e6.png

我们找到的这个向量给了我们一个新的映射从正方形到圆。让我们在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);

ef0160b3a05c435d56d6f6b208ef3df6.png
替代映射

事实证明,此映射将点从对角线推向主轴方向。现在它们在轴附近最靠近。幸运的是,相邻点之间的距离比第一种方法更均匀,因此是一种改进。

4 调整映射公式

我们有了一个新的从正方形到圆形的映射公式,但是从立方体到球体呢? 我们可以使用同样的方法吗? 是的,但是我们需要加入第三个坐标,就像我们已经有的两个。 这样我们就得到了指向单位球面上点的向量的平方长度。

3a4a0a5cf3c743ab7602ddc59d9e3148.png

实际上,这种方法适用于任意数量的坐标,因此我们可以从任意超立方体映射到任意维度的超球体。 扩展公式只会变得更加复杂。

faa74ea8e9559b7db0cdeb910ee38e0b.png
你要怎么重写?
我们已经知道如何处理两个坐标。

159097339402cc7ec42a15b2123e919a.png
中间步骤

6c0a9ea83a414a7ae267d26d5bf04e4c.png
最终结果是1减去这个。

这个三维空间的公式有一个新项

,它将3个坐标联系到一起。就像之前一样,我们把它平分成最终向量的坐标。我们最终得到了更复杂的公式,但概念是一样的。这里是
的公式,其他轴的公式都与下面公式类似。

9f18eadef1bbd49c328a6edca90d8545.png

最后的映射仍然是一个平方根。

3db6e63f69e290b76bcee757a8c5e044.png

让我们在 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);
	}

f00de70dd682537012011b8704a28ffb.png
网格单元更加均匀

网格单元离对角线越近,就会变得越扭曲,这是无法避免的。 但是这种新的映射产生的单元比标准化方法大得多。 轴线和立方体拐角处的单元格现在看起来大致相同。 这比我们开始的时候好多了! 现在最大的单元格是沿着立方体边缘的那些单元格。 那些曾经被压扁的单元格,现在被拉长了。

你是怎么得出这样的数学公式的?
虽然它在这里是按逻辑顺序呈现的,但是自己搞清楚这些东西通常会走一条更不稳定的路。当机会出现时,遵循一个清晰的逻辑路径是很好的,但是直觉通常只有在实验之后才会出现。在此之前,调查一下其他人已经发现了什么。
互联网上有很多知识,好好利用吧!在本例中,Philip Nowell在他的博客中描述了立方体到球体的映射。映射背后的直觉来自一个数学堆栈交换问题的答案。

本文git仓库地址

Amy6922/UnityMeshDemo​github.com
fa6af0f22c95c72bac749c9dd0e735d9.png

接下来,我们模拟球体表面受外力的变形的状态。

AmyBoy:UnityMesh编程(4)网格变形​zhuanlan.zhihu.com
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值