术数古籍专卖疤 发表于 2020-12-31 12:23

Unity的立体几何问题

今天我们来讲一点有趣的立体几何问题。


在可视化与可视化分析中,基础的几何图形绘制是一切的基础,比如常见的饼状图由扇形组成,柱状图由长方形组成,玫瑰图由弧形组成。在2D的世界里,这些都是SVG和Canvas绘图的基础。在3D的世界中,前端库three.js也提供了与之对应的3D绘制方法,但是在Unity中对于3D基础模型的提供支持却相对较少,需要专门编写代码来生成。今天我们就讲一讲在Unity里面通过绘制这些基础3D结构的方法,同时也聊一聊这些基本3D结构的数学绘制逻辑。


在Unity绘图中,有Mesh和Render两部分组成,Mesh用于描述物体的结构,Render描述这个物体该绘制成什么颜色。今天主要讲Mesh。


Mesh是由顶点和这些顶点构成的基础三角形组成。
顶点à三角形。


正X面体
对于我们来说,最基础的3D结构应该是正方体,也就是正6面体。由8个顶点,6个正方形组成。这6个正方形又可以被拆分成12个等腰直角三角形。那么描述一个正方体可以表示为:
Vector3 p0 = new Vector3(0.5f, 0.5f, 0.5f).normalized;
Vector3 p1 = new Vector3(-0.5f, 0.5f, 0.5f).normalized;
Vector3 p2 = new Vector3(-0.5f, -0.5f, 0.5f).normalized;
Vector3 p3 = new Vector3(0.5f, -0.5f, 0.5f).normalized;
Vector3 p4 = new Vector3(0.5f, 0.5f, -0.5f).normalized;
Vector3 p5 = new Vector3(-0.5f, 0.5f, -0.5f).normalized;
Vector3 p6 = new Vector3(-0.5f, -0.5f, -0.5f).normalized;
Vector3 p7 = new Vector3(0.5f, -0.5f, -0.5f).normalized;
mesh.vertices = new Vector3[] { p0, p1, p2, p3, p4, p5 };
mesh.triangles = new int[]
{
   0,1,2,
   0,2,3,
   1,5,6
   1,6,2,
   5,4,7,
   5,7,6,
   4,0,3
   4,3,7,
   0,4,1,
   4,5,1,
   3,2,6,
   3,6,7
};绘制效果:


最基础的3D物体是正4面体,也就是每个面都是正三角形的三棱锥。由4个顶点,4个三角形组成。那么描述一个正4面体可以表示为:
Vector3 p0 = new Vector3(0, 0, 0);
Vector3 p1 = new Vector3(1, 0, 0);
Vector3 p2 = new Vector3(0.5f, 0, Mathf.Sqrt(0.75f));
Vector3 p3 = new Vector3(0.5f, Mathf.Sqrt(0.75f), Mathf.Sqrt(0.75f) / 3);
Vector3 center = (p0 + p1 + p2 + p3) / 4;
p0 = (p0 - center).normalized;
p1 = (p1 - center).normalized;
p2 = (p2 - center).normalized;
p3 = (p3 - center).normalized;
mesh.vertices = new Vector3[] { p0, p1, p2, p3 };
mesh.triangles = new int[]{
   0,1,2,
   0,2,3,
   2,1,3,
   0,3,1
};绘制效果:


正8面体,由8个正三角形组成,其中有6个顶点。那么一个正8面体可以描述为:
Vector3 p0 = new Vector3(1, 0, 0);
Vector3 p1 = new Vector3(0, 1, 0);
Vector3 p2 = new Vector3(0, 0, 1);
Vector3 p3 = new Vector3(-1, 0, 0);
Vector3 p4 = new Vector3(0, -1, 0);
Vector3 p5 = new Vector3(0, 0, -1);
mesh.vertices = new Vector3[] { p0, p1, p2, p3, p4, p5 };
mesh.triangles = new int[]
{
   2,0,1,
   3,2,1,
   5,3,1,
   0,5,1,
   3,5,4,
   5,0,4,
   0,2,4,
   2,3,4
};绘制效果:


前面都还比较简单,正20面体,由20个正三角形组成,其中有12个顶点。对于一般人而言,这个时候想在头脑中想象出来正20面体的样子还是有点难的,这里我教大家怎么绘制正20面体。
首先,准备3块一样大小的长方形纸板,其长宽比为(√5 + 1):2,按照如图的方式进行组合:




其顶点连接组成的形状就是一个正20面体,单独对正20面体的任意一个角进行观察,这个角周围贴合了5个正三角形。就像这个样子:


根据上面的三个纸板参考的方式,我们可以容易的描述一个正20面体:
Vector3 p0 = new Vector3(Mathf.Sqrt(5f) + 1f, 2f, 0).normalized;
Vector3 p1 = new Vector3(Mathf.Sqrt(5f) + 1f, -2f, 0).normalized;
Vector3 p2 = new Vector3(-Mathf.Sqrt(5f) - 1f, -2f, 0).normalized;
Vector3 p3 = new Vector3(-Mathf.Sqrt(5f) - 1f, 2f, 0).normalized;
Vector3 p4 = new Vector3(2f, 0, Mathf.Sqrt(5f) + 1f).normalized;
Vector3 p5 = new Vector3(-2f, 0, Mathf.Sqrt(5f) + 1f).normalized;
Vector3 p6 = new Vector3(-2f, 0, -Mathf.Sqrt(5f) - 1f).normalized;
Vector3 p7 = new Vector3(2f, 0, -Mathf.Sqrt(5f) - 1f).normalized;
Vector3 p8 = new Vector3(0, Mathf.Sqrt(5f) + 1f, 2f).normalized;
Vector3 p9 = new Vector3(0, Mathf.Sqrt(5f) + 1f, -2f).normalized;
Vector3 p10 = new Vector3(0, -Mathf.Sqrt(5f) - 1f, -2f).normalized;
Vector3 p11 = new Vector3(0, -Mathf.Sqrt(5f) - 1f, 2f).normalized;
mesh.vertices = new Vector3[] { p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11 };
mesh.triangles = new int[]
{
   0,8,4,
   0,4,1,
   0,1,7,
   0,7,9,
   0,9,8,
   2,6,10,
   2,3,6,
   2,5,3,
   2,11,5,
   2,10,11,
   1,11,10,
   1,4,11,
   11,4,5,
   5,4,8,
   3,5,8,
   3,8,9,
   3,9,6,
   6,9,7,
   6,7,10,
   10,7,1
};绘制出来效果如下:




好了,除了正12面体以外,基础的正X面体的绘制方法已经讲完了。


柱体
基础的3D图形还有哪些呢?


长方体,长方体可以通过正方体的Scale变换而轻易得到,这里不多说。
圆柱,怎么通过点和线来描述一个圆柱体呢?


首先我们知道一个圆可以拆分成很多个同心小扇形,在扇形角度很小的时候,可以用三角形来代替扇形。通过将圆形拉长就可以得到一个圆柱。描述代码如下:
mesh.vertices = new Vector3[(SectorNumber + 1) * 2];
mesh.triangles = new int;
Vector3[] vertices = mesh.vertices;
int[] triangles = mesh.triangles;
vertices.x = 0;
vertices.y = Height / 2;
vertices.z = 0;
float tmpArc;
int tmpCount = 0;
for (int i = 0; i < SectorNumber; ++i)
{
   tmpArc = 2 * Mathf.PI * i / SectorNumber;
   vertices.x = Mathf.Sin(tmpArc) * Radius;
   vertices.y = Height / 2;
   vertices.z = Mathf.Cos(tmpArc) * Radius;
   triangles = 0;
   triangles = i + 1;
   triangles = i != (SectorNumber - 1) ? i + 2 : 1;
}
for(int i = SectorNumber; i < SectorNumber*2; ++i)
{
   tmpArc = 2 * Mathf.PI * i / SectorNumber;
   vertices.x = Mathf.Sin(tmpArc) * Radius;
   vertices.y = -Height / 2;
   vertices.z = Mathf.Cos(tmpArc) * Radius;
   triangles = SectorNumber * 2 + 1;
   triangles = i != (SectorNumber * 2 - 1) ? i + 2 : SectorNumber + 1;
   triangles = i + 1;
}
vertices.x = 0;
vertices.y = -Height / 2;
vertices.z = 0;
for(int i = 1; i < SectorNumber; ++i)
{
   triangles = i;
   triangles = i + SectorNumber;
   triangles = i + 1;
   triangles = i + 1;
   triangles = i + SectorNumber;
   triangles = i + SectorNumber + 1;
}
triangles = SectorNumber;
triangles = 2 * SectorNumber;
triangles = 1;
triangles = 1;
triangles = 2 * SectorNumber;
triangles = SectorNumber + 1;
mesh.vertices = vertices;
mesh.triangles = triangles;绘制效果如下(可以通过控制SectorNumber参数来控制圆柱的精细程度,左边24个扇区,右边12个扇区):




同样,弧形柱的绘制方式也和圆柱相似,代码:
mesh.vertices = new Vector3[(pieces + 1) * 4];
Vector3[] vertices = mesh.vertices;
mesh.triangles = new int[(pieces*8 + 4)*3];
float CurrentArc = 0;
for (int i = 0; i <= pieces; ++i)
{
    CurrentArc = i * Arc / pieces + StartArc;
    vertices.x = outterRadius * Mathf.Sin(CurrentArc * Mathf.PI / 180);
    vertices.z = outterRadius * Mathf.Cos(CurrentArc * Mathf.PI / 180);
    vertices.y = - Height / 2;
    vertices.x = outterRadius * Mathf.Sin(CurrentArc * Mathf.PI / 180);
    vertices.z = outterRadius * Mathf.Cos(CurrentArc * Mathf.PI / 180);
    vertices.y = Height / 2;
    vertices.x = innerRadius * Mathf.Sin(CurrentArc * Mathf.PI / 180);
    vertices.z = innerRadius * Mathf.Cos(CurrentArc * Mathf.PI / 180);
    vertices.y = Height / 2;
    vertices.x = innerRadius * Mathf.Sin(CurrentArc * Mathf.PI / 180);
    vertices.z = innerRadius * Mathf.Cos(CurrentArc * Mathf.PI / 180);
    vertices.y = - Height / 2;
}
mesh.vertices = vertices;
int[] triangles = mesh.triangles;
for (int i = 0; i < pieces; ++i)
{
    triangles = i * 4;
    triangles = (i + 1) * 4;
    triangles = (i + 1) * 4 + 1;
    triangles = i * 4;
    triangles = (i + 1) * 4 + 1;
    triangles = i * 4 + 1;
    triangles = i * 4 + 1;
    triangles = (i + 1) * 4 + 1;
    triangles = (i + 1) * 4 + 2;
    triangles = i * 4 + 1;
    triangles = (i + 1) * 4 + 2;
    triangles = i * 4 + 2;
    triangles = i * 4 + 2;
    triangles = (i + 1) * 4 + 2;
    triangles = (i + 1) * 4 + 3;
    triangles = i * 4 + 2;
    triangles = (i + 1) * 4 + 3;
    triangles = i * 4 + 3;
    triangles = i * 4 + 3;
    triangles = (i + 1) * 4 + 3;
    triangles = (i + 1) * 4;
    triangles = i * 4 + 3;
    triangles = (i + 1) * 4;
    triangles = i * 4;
}
triangles = 0;
triangles = 1;
triangles = 3;
triangles = 1;
triangles = 2;
triangles = 3;
triangles = 4 * pieces;
triangles = 4 * pieces + 2;
triangles = 4 * pieces + 1;
triangles = 4 * pieces;
triangles = 4 * pieces + 3;
triangles = 4 * pieces + 2;
mesh.triangles = triangles;绘制效果:




梯形柱的绘制逻辑就是只有一个扇面的弧形柱,因此在此就不再多加赘述。


球体
在Unity3D世界中,球体也是通过一个一个小三角形来表示的。怎么去生成一个由小三角形组成的球呢,常见的对于地球的划分是经纬度方法,如图:




这种分割球形表面的方法有个弊端,球靠近赤道两边的节点会很稀疏,靠近两极的节点会很密集,因此在这里不多做介绍。


比较好的做法是通过将正X面体每个面进行细分来拟近圆,对于正X面体的每个小三角面,按照每条边的中点将其拆分成四个三角形,拆分方式如图:






将拆分的新的四个三角形,贴合到球面上。然后依次迭代,就可以得到一个拟合不错的“球”。
其拆分逻辑如下:
Vector3[] vectices = mesh.vertices;
int[] triangles = mesh.triangles;
int size1 = vectices.Length;
int size2 = triangles.Length / 3;
Vector3[] vectices2 = new Vector3;
int[] triangles2 = new int;
for (int i = 0; i < size1; ++i)
{
    vectices2 = vectices;
}
for (int i = 0; i < size2; ++i)
{
    Vector3 center1 = Mid(vectices], vectices]).normalized;
    Vector3 center2 = Mid(vectices], vectices]).normalized;
    Vector3 center3 = Mid(vectices], vectices]).normalized;
    vectices2 = center1;
    vectices2 = center2;
    vectices2 = center3;
    triangles2 = triangles;
    triangles2 = size1 + i * 3 + 2;
    triangles2 = size1 + i * 3 + 1;
    triangles2 = size1 + i * 3 + 2;
    triangles2 = triangles;
    triangles2 = size1 + i * 3;
    triangles2 = size1 + i * 3 + 1;
    triangles2 = size1 + i * 3;
    triangles2 = triangles;
    triangles2 = size1 + i * 3;
    triangles2 = size1 + i * 3 + 1;
    triangles2 = size1 + i * 3 + 2;
}
mesh.Clear();
mesh.vertices = vectices2;
mesh.triangles = triangles2;

各种类型的正面体拟合效果:
正4面体(4个基础三角面):
正8面体(8个基础三角面):
正6面体(正方体)(12个基础三角面):
正20面体:
看到这里有人可能就会问了,为什么没有讲正12面体,也就下面这位:




但是正12面体每个基本面是正五边形,再拆分成三角形的话至少会有36个基础三角形,因此不拿它来拟合球。


看到这,可能有人会问,小编讲了这么多到底要干嘛?
其实我就是突然有一天,想画一个海胆,就是下面这位:




但是Unity自带的几何结构很少,没法画。因此,自己动手写代码画。
要画这么一个海胆,首先要能画一个球,再将用于模拟球面的每个三角面通过下图中的变化来突出刺来。






代码逻辑如下:
Vector3[] vectices = mesh.vertices;
int[] triangles = mesh.triangles;
int size1 = vectices.Length;
int size2 = triangles.Length / 3;
Vector3[] vectices2 = new Vector3;
int[] triangles2 = new int;
for (int i = 0; i < size1; ++i)
{
    vectices2 = vectices;
}
for (int i = 0; i < size2; ++i)
{
    Vector3 center = Mid(vectices], vectices], vectices]).normalized;
    //vectices2 = center * (Random.Range(0f,1f)<0.6? 1: outterRadius);
    vectices2 = center * outterRadius;
    triangles2 = triangles;
    triangles2 = triangles;
    triangles2 = size1 + i;
    triangles2 = triangles;
    triangles2 = size1 + i;
    triangles2 = triangles;
    triangles2 = size1 + i;
    triangles2 = triangles;
    triangles2 = triangles;
}
mesh.Clear();
mesh.vertices = vectices2;
mesh.triangles = triangles2;效果如下:
最后,可能有人想问,为什么要画这些?
怎么说呢,因为好玩,就是这么任性,有机会再跟大家分享一些其他的好玩的东西。
页: [1]
查看完整版本: Unity的立体几何问题