用于创建球体的算法?


27

有没有人有算法来程序地创建一个球体,该球体具有la一定数量的纬度线,lo一定数量的经度线和一个半径为r?我需要它与Unity一起使用,因此需要定义顶点位置,然后通过索引定义三角形(更多信息)。


编辑

在此处输入图片说明

我设法使代码统一工作。但是我想我可能做错了什么。当我打开时detailLevel,它所做的就是添加更多的顶点和多边形,而无需四处移动。我忘记了什么吗?


编辑2

在此处输入图片说明

我尝试沿其法线缩放网格。这就是我得到的。我想我缺少了一些东西。我应该只缩放某些法线吗?


1
您为什么不看看现有的开源实现是如何做到的呢?例如,看看Three.js如何使用网格。
布莱斯2012年

3
作为一个小纸条:除非你要做的纬度/经度,你几乎肯定不希望来的,因为你得到的三角形将变得更不统一比你用其它方法获得。(将北极附近的三角形与赤道附近的三角形进行比较:两种情况下,您都使用相同数量的三角形绕过一条纬线,但是在极点附近,该纬度的周长很小,而在赤道处就像大卫·利弗利(David Lively)回答中提到的那样,这种技术通常要好得多。
Steven Stadnicki 2012年

1
细分后,您不对顶点位置进行归一化。我没有在示例中包括该部分。归一化使它们与中心等距,从而创建您要寻找的曲线近似值。
3Dave 2012年

考虑在二十面体的中心充气一个气球。当气球推动网格物体时,它与气球(球体)的形状匹配。
3Dave 2012年

4
“规范化”是指将向量的长度设置为1。您需要执行类似的操作vertices[i] = normalize(vertices[i])。顺便说一句,这也为您提供了新的正确法线,因此您应该在normals[i] = vertices[i]之后进行。
sam hocevar

Answers:


31

要获得这样的东西:

在此处输入图片说明

创建一个二十面体(20面常规实体)并细分这些面以获得一个球体(请参见下面的代码)。

这个想法基本上是:

  • 创建一个常规的n面体(每个面都具有相同大小的实体)。我使用二十面体,因为它是具有最多面数的实体,每个面都是相同大小。(那里有一个证明。如果您真的好奇,请随时使用Google。)这将为您提供一个几乎每个脸都是相同大小的球体,使纹理化变得容易一些。

在此处输入图片说明

  • 将每个面细分为四个大小相等的面。每次执行此操作时,模型中的面数都会增加三倍。

    ///      i0
    ///     /  \
    ///    m02-m01
    ///   /  \ /  \
    /// i2---m12---i1

i0,,i1i2是原始三角形的顶点。(实际上,索引到顶点缓冲区中,但这是另一个主题)。m01是边缘的中点(i0,i1),m12是边缘的中点(i1,12),并且m02显然是边缘的中点(i0,i2)

每当细分面时,请确保不要创建重复的顶点。每个中点将由另一个源面共享(因为边在面之间共享)。下面的代码通过维护已创建的命名中点字典来解决这一问题,并在可用时返回先前创建的中点的索引,而不是创建新的中点。

  • 重复上述步骤,直到达到所需的立方体面数为止。

  • 完成后,将所有顶点规格化以平滑表面。如果不这样做,您将得到一个更高分辨率的二十面体而不是球体。

  • 瞧!你完成了。将生成的向量缓冲区和索引缓冲区转换为VertexBufferand IndexBuffer,并使用绘制Device.DrawIndexedPrimitives()

这是在“ Sphere”类中用于创建模型的内容(XNA数据类型和C#,但应该很清楚):

        var vectors = new List<Vector3>();
        var indices = new List<int>();

        GeometryProvider.Icosahedron(vectors, indices);

        for (var i = 0; i < _detailLevel; i++)
            GeometryProvider.Subdivide(vectors, indices, true);

        /// normalize vectors to "inflate" the icosahedron into a sphere.
        for (var i = 0; i < vectors.Count; i++)
            vectors[i]=Vector3.Normalize(vectors[i]);

GeometryProvider班级

public static class GeometryProvider
{

    private static int GetMidpointIndex(Dictionary<string, int> midpointIndices, List<Vector3> vertices, int i0, int i1)
    {

        var edgeKey = string.Format("{0}_{1}", Math.Min(i0, i1), Math.Max(i0, i1));

        var midpointIndex = -1;

        if (!midpointIndices.TryGetValue(edgeKey, out midpointIndex))
        {
            var v0 = vertices[i0];
            var v1 = vertices[i1];

            var midpoint = (v0 + v1) / 2f;

            if (vertices.Contains(midpoint))
                midpointIndex = vertices.IndexOf(midpoint);
            else
            {
                midpointIndex = vertices.Count;
                vertices.Add(midpoint);
                midpointIndices.Add(edgeKey, midpointIndex);
            }
        }


        return midpointIndex;

    }

    /// <remarks>
    ///      i0
    ///     /  \
    ///    m02-m01
    ///   /  \ /  \
    /// i2---m12---i1
    /// </remarks>
    /// <param name="vectors"></param>
    /// <param name="indices"></param>
    public static void Subdivide(List<Vector3> vectors, List<int> indices, bool removeSourceTriangles)
    {
        var midpointIndices = new Dictionary<string, int>();

        var newIndices = new List<int>(indices.Count * 4);

        if (!removeSourceTriangles)
            newIndices.AddRange(indices);

        for (var i = 0; i < indices.Count - 2; i += 3)
        {
            var i0 = indices[i];
            var i1 = indices[i + 1];
            var i2 = indices[i + 2];

            var m01 = GetMidpointIndex(midpointIndices, vectors, i0, i1);
            var m12 = GetMidpointIndex(midpointIndices, vectors, i1, i2);
            var m02 = GetMidpointIndex(midpointIndices, vectors, i2, i0);

            newIndices.AddRange(
                new[] {
                    i0,m01,m02
                    ,
                    i1,m12,m01
                    ,
                    i2,m02,m12
                    ,
                    m02,m01,m12
                }
                );

        }

        indices.Clear();
        indices.AddRange(newIndices);
    }

    /// <summary>
    /// create a regular icosahedron (20-sided polyhedron)
    /// </summary>
    /// <param name="primitiveType"></param>
    /// <param name="size"></param>
    /// <param name="vertices"></param>
    /// <param name="indices"></param>
    /// <remarks>
    /// You can create this programmatically instead of using the given vertex 
    /// and index list, but it's kind of a pain and rather pointless beyond a 
    /// learning exercise.
    /// </remarks>

    /// note: icosahedron definition may have come from the OpenGL red book. I don't recall where I found it. 
    public static void Icosahedron(List<Vector3> vertices, List<int> indices)
    {

        indices.AddRange(
            new int[]
            {
                0,4,1,
                0,9,4,
                9,5,4,
                4,5,8,
                4,8,1,
                8,10,1,
                8,3,10,
                5,3,8,
                5,2,3,
                2,7,3,
                7,10,3,
                7,6,10,
                7,11,6,
                11,0,6,
                0,1,6,
                6,1,10,
                9,0,11,
                9,11,2,
                9,2,5,
                7,2,11 
            }
            .Select(i => i + vertices.Count)
        );

        var X = 0.525731112119133606f;
        var Z = 0.850650808352039932f;

        vertices.AddRange(
            new[] 
            {
                new Vector3(-X, 0f, Z),
                new Vector3(X, 0f, Z),
                new Vector3(-X, 0f, -Z),
                new Vector3(X, 0f, -Z),
                new Vector3(0f, Z, X),
                new Vector3(0f, Z, -X),
                new Vector3(0f, -Z, X),
                new Vector3(0f, -Z, -X),
                new Vector3(Z, X, 0f),
                new Vector3(-Z, X, 0f),
                new Vector3(Z, -X, 0f),
                new Vector3(-Z, -X, 0f) 
            }
        );


    }



}

好答案。谢谢。我不知道这是统一代码吗?哦,纬度/经度没关系,只要我可以设置分辨率即可。
Daniel Pendergast 2012年

它不是Unity(XNA),但会为您提供顶点坐标和索引列表。用与Unity等价的任何东西替换Vector3。您可以通过调整细分迭代次数来设置分辨率。每个循环将面数乘以4。2或3次迭代将得到一个不错的球体。
3Dave 2012年

啊,我明白了。它几乎与Unity C#相同。只是几个问题...为什么在定义索引时将索引放在int数组中?怎么.Select(i => i + vertices.Count)办?
Daniel Pendergast

.Select(i => i + vertices.Count)根本不适合我。它是仅XNA的功能吗?
Daniel Pendergast 2012年

1
确保您包括“使用System.Linq的”,因为它defines.Select等
3Dave

5

让我们考虑一个球面的参数定义:

球的参数定义

其中theta和phi是两个增量角,我们将其称为var t和,var u并且Rx,Ry和Rz是在所有三个笛卡尔方向上的独立半径(半径),对于球体,将被定义为一个半径var rad

现在让我们考虑以下事实:...符号表示迭代,这暗示了使用循环。的概念stacks,并rows是“有多少次你愿意迭代”。由于每次迭代都将t或u的值相加,因此迭代次数越多,值越小,因此球体的曲率越精确。

“球形绘图”功能的前提是具有以下给定参数:int latitudes, int longitudes, float radius。后置条件(输出)将返回或应用计算出的顶点。根据您打算如何使用此函数,该函数可以返回vector3(三维矢量)数组,或者,如果您使用的是某种简单的OpenGL,则在2.0版之前,您可能希望将顶点直接应用于上下文。

注意在openGL中应用顶点会调用以下函数glVertex3f(x, y, z)。在我们存储顶点的情况下,我们将添加一个新的vector3(x, y, z)以便于存储。

另外,您要求经纬度系统工作的方式需要对球体的定义进行调整(基本上切换z和y),但这仅表明该定义具有很好的延展性,并且您可以自由地在x,y和z参数可更改绘制球体的方向(纬度和经度所在的位置)。

现在,让我们看看如何进行纬度和经度。纬度由变量表示u,它们从0到2π弧度(360度)迭代。因此,我们可以这样编码其迭代:

float latitude_increment = 360.0f / latitudes;

for (float u = 0; u < 360.0f; u += latitude_increment) {
    // further code ...
}

现在,经度由变量表示,t并在0到π(180度)之间进行迭代。因此,以下代码与上一个代码相似:

float latitude_increment = 360.0f / latitudes;
float longitude_increment = 180.0f / longitudes;

for (float u = 0; u <= 360.0f; u += latitude_increment) {
    for (float t = 0; t <= 180.0f; t += longitude_increment) {
        // further code ...
    }
}

(请注意,循环包含终端条件,因为参数积分的间隔为0到2π 包含。如果您的条件不包括在内,则将得到部分球体。)

现在,按照球体的简单定义,我们可以得出变量定义如下(假设float rad = radius;):

float x = (float) (rad * Math.sin(Math.toRadians(t)) * Math.sin(Math.toRadians(u)));
float y = (float) (rad * Math.cos(Math.toRadians(t)));
float z = (float) (rad * Math.sin(Math.toRadians(t)) * Math.cos(Math.toRadians(u)));

一个重要的警告!在大多数情况下,您将使用某种形式的OpenGL,即使不是这样,您可能仍需要这样做。三维物体需要定义多个顶点。通常,这是通过提供下一个可计算的顶点来实现的。

如何使用多个顶点来定义(原始)形状

就像上图中的不同坐标是x+∂和一样y+∂,我们可以轻松地为任何所需的用途生成其他三个顶点。其他顶点是(假设float rad = radius;):

float x = (float) (rad * Math.sin(Math.toRadians(t + longitude_increment)) * Math.sin(Math.toRadians(u)));
float y = (float) (rad * Math.cos(Math.toRadians(t + longitude_increment)));
float z = (float) (rad * Math.sin(Math.toRadians(t + longitude_increment)) * Math.cos(Math.toRadians(u)));

float x = (float) (rad * Math.sin(Math.toRadians(t)) * Math.sin(Math.toRadians(u + latitude_increment)));
float y = (float) (rad * Math.cos(Math.toRadians(t)));
float z = (float) (rad * Math.sin(Math.toRadians(t)) * Math.cos(Math.toRadians(u + latitude_increment)));

float x = (float) (rad * Math.sin(Math.toRadians(t + longitude_increment)) * Math.sin(Math.toRadians(u + latitude_increment)));
float y = (float) (rad * Math.cos(Math.toRadians(t + longitude_increment)));
float z = (float) (rad * Math.sin(Math.toRadians(t + longitude_increment)) * Math.cos(Math.toRadians(u + latitude_increment)));

最后,这是一个可以正常工作的完整函数,它将返回一个球体的所有顶点,第二个函数显示了该代码的有效OpenGL实现(这是C样式的语法,而不是JavaScript,这应该适用于所有C样式的语言,包括使用Unity时的C#)。

static Vector3[] generateSphere(float radius, int latitudes, int longitudes) {

    float latitude_increment = 360.0f / latitudes;
    float longitude_increment = 180.0f / longitudes;

    // if this causes an error, consider changing the size to [(latitude + 1)*(longitudes + 1)], but this should work.
    Vector3[] vertices = new Vector3[latitude*longitudes];

    int counter = 0;

    for (float u = 0; u < 360.0f; u += latitude_increment) {
        for (float t = 0; t < 180.0f; t += longitude_increment) {

            float rad = radius;

            float x = (float) (rad * Math.sin(Math.toRadians(t)) * Math.sin(Math.toRadians(u)));
            float y = (float) (rad * Math.cos(Math.toRadians(t)));
            float z = (float) (rad * Math.sin(Math.toRadians(t)) * Math.cos(Math.toRadians(u)));

            vertices[counter++] = new Vector3(x, y, z);

        }
    }

    return vertices;

}

OpenGL代码:

static int createSphereBuffer(float radius, int latitudes, int longitudes) {

    int lst;

    lst = glGenLists(1);

    glNewList(lst, GL_COMPILE);
    {

        float latitude_increment = 360.0f / latitudes;
        float longitude_increment = 180.0f / longitudes;

        for (float u = 0; u < 360.0f; u += latitude_increment) {

            glBegin(GL_TRIANGLE_STRIP);

            for (float t = 0; t < 180.0f; t += longitude_increment) {

                float rad = radius;

                float x = (float) (rad * Math.sin(Math.toRadians(t)) * Math.sin(Math.toRadians(u)));
                float y = (float) (rad * Math.cos(Math.toRadians(t)));
                float z = (float) (rad * Math.sin(Math.toRadians(t)) * Math.cos(Math.toRadians(u)));

                vertex3f(x, y, z);

                float x1 = (float) (rad * Math.sin(Math.toRadians(t + longitude_increment)) * Math.sin(Math.toRadians(u + latitude_increment)));
                float y1 = (float) (rad * Math.cos(Math.toRadians(t + longitude_increment)));
                float z1 = (float) (rad * Math.sin(Math.toRadians(t + longitude_increment)) * Math.cos(Math.toRadians(u + latitude_increment)));

                vertex3f(x1, y1, z1);

            }

            glEnd();

        }

    }
    glEndList()

    return lst;

}

// to render VVVVVVVVV

// external variable in main file
static int sphereList = createSphereBuffer(desired parameters)

// called by the main program
void render() {

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glCallList(sphereList);

    // any additional rendering and buffer swapping if not handled already.

}

附注:您可能已经注意到此声明rad = radius;。这样可以根据位置或角度在回路中修改半径。这意味着您可以将噪声应用于球体以使其粗糙化,如果所需的效果像行星一样,则使其看起来更自然。例如float rad = radius * noise[x][y][z];

克劳德·亨利。


这行`float z =(float)(rad * Math.sin(Math.toRadians(t))* Math.cos(Math.toRadians(u)));`是错误的。您已经计算了斜边为的X,Y rad。现在,您正在制作一个三角形的腿,并暗示该三角形的斜边也为rad。有效地使您的半径为rad * sqrt(2)
3Dave

@DavidLively感谢您指出这一点,我很早以前就写了这个,所以如果它不好甚至是完全错误的话,我并不感到惊讶。
claudehenry

当我几年前在我的一篇帖子中发现错误时,这总是很有趣。它发生了。:)
3Dave

4

不久前,我创造了类似的东西来制作一个立方体球,以求娱乐和科学。不太难。基本上,您需要使用一个函数来创建一个顶点圆,然后逐步执行要在每个高度上创建圆所需的半径处创建圆的高度增量。在这里,我将代码修改为不适用于多维数据集:

public static void makeSphere(float sphereRadius, Vector3f center, float heightStep, float degreeStep) {
    for (float y = center.y - sphereRadius; y <= center.y + sphereRadius; y+=heightStep) {
        double radius = SphereRadiusAtHeight(sphereRadius, y - center.y); //get the radius of the sphere at this height
        if (radius == 0) {//for the top and bottom points of the sphere add a single point
            addNewPoint((Math.sin(0) * radius) + center.x, y, (Math.cos(0) * radius) + center.z));
        } else { //otherwise step around the circle and add points at the specified degrees
            for (float d = 0; d <= 360; d += degreeStep) {
                addNewPoint((Math.sin(d) * radius) + center.x, y, (Math.cos(d) * radius) + center.z));
            }
        }
    }
}

public static double SphereRadiusAtHeight(double SphereRadius, double Height) {
    return Math.sqrt((SphereRadius * SphereRadius) - (Height * Height));
}

现在,此代码将为纬度创建点。但是,您几乎可以使用相同的代码制作经度线。除非您需要在每个迭代之间旋转,并在每个旋转一个完整的圆degreeStep

抱歉,这不是完整的答案,也不是特定于Unity的答案,但希望它能帮助您入门。


如果您需要一个纬度/经度的球体,这很好,但是可以通过在球面坐标系中工作到最后一步来稍微简化一下。
3Dave 2012年

1
谢谢@大卫。我同意,如果我打算使用球坐标编写一个版本,请在此处发布。
MichaelHouse

3

您不能只是从一个简单的形状开始,可能是一个从中心到角落的距离为r的盒子。要制作更详细的球体,请对所有多边形进行细分,然后将顶点移到与中心的r距离处,并使矢量通过其当前位置。

不断重复直到球形足以满足您的口味。


这与二十面体方法基本相同,只是起始形状不同。从我从未想到的多维数据集开始的一个优点已被提及:为其构建体面的UV贴图要容易得多,因为您可以使用多维数据集贴图,并且知道纹理接缝将与球体网格中的边缘完美对齐。
Steven Stadnicki 2012年

@StevenStadnicki我对多维数据集唯一的问题是,经过细分后,面孔的大小趋于不同。
3Dave 2012年

@DavidLively这很大程度上取决于您如何细分-如果将立方体的方形面切成均匀的网格,然后向外投影/归一化,那么这是正确的,但是如果您将面非均匀地网格化,则实际上可以投影沿边缘的弧线均匀分布;原来效果很好。
Steven Stadnicki 2012年

@StevenStadnicki漂亮!
3Dave 2012年

@EricJohansson顺便说一句,作为一名老师,我不得不提到对于一个以前似乎没有看过细分方法的人,这是一个非常重要的见解。在接下来的12个小时内,您已经重新表达了我对人性的信念。
3Dave 2012年

2

您实际上需要3D几何形状还是仅需要形状?

您可以使用单个四边形制作“假”球体。只需在其上画一个圆圈并正确着色即可。这样做的好处是,无论与相机的距离或分辨率如何,它都会具有所需的分辨率。

这里有一个教程


1
不错的技巧,但是如果您需要对其进行纹理化处理,则会失败。
3Dave 2012年

@DavidLively应该有可能基于其旋转来计算每个像素的纹理坐标,除非您需要分别对多边形进行纹理化。
戴维·C·毕晓普

@DavidCBishop您必须考虑表面的“镜头”问题-由于透视的缘故,纹理像素坐标被挤压到靠近圆形边界的位置-在这一点上您正在伪装旋转。此外,这涉及将更多工作移到可以在顶点着色器中执行的像素着色器中(而且我们都知道VS便宜很多!)。
3Dave 2012年

0

这是球体上任意数量相等间隔的顶点的一些代码,就像橘皮一样,它以螺旋形围绕球体缠绕点线。之后,如何连接顶点取决于您。您可以在循环中使用相邻点作为每个三角形的2个,然后发现第三个点是围绕球体向上或向下按比例的一个扭曲...您也可以通过循环和最近的邻居来做三角形,有人吗知道更好的方法吗?

var spherevertices = vector3 generic list...

public var numvertices= 1234;
var size = .03;  

function sphere ( N:float){//<--- N is the number of vertices i.e 123

var inc =  Mathf.PI  * (3 - Mathf.Sqrt(5));
var off = 2 / N;
for (var k = 0; k < (N); k++)
{
    var y = k * off - 1 + (off / 2);
    var r = Mathf.Sqrt(1 - y*y);
    var phi = k * inc;
    var pos = Vector3((Mathf.Cos(phi)*r*size), y*size, Mathf.Sin(phi)*r*size); 

    spherevertices   add pos...

}

};


-1

尽管大卫的回答是绝对正确的,但我想提出不同的看法。

在分配程序内容时,我研究了二十面体与更传统的细分领域之间的关系。查看这些程序生成的球体:

很棒的领域

两者看起来都是完全有效的球体,对吗?好吧,让我们看看他们的线框:

哇好浓

哇,那里发生了什么?第二个球体的线框版本是如此密集,以至于看起来很粗糙!我给你一个秘密:第二个版本是二十面体。这是一个几乎完美的领域,但代价高昂。

球体1在x轴上使用31个细分,在z轴上使用31个细分,总共3,844个面。

Sphere 2使用5个递归细分,总共109,220张面。

但是,那并不公平。让我们大幅降低质量:

块状

球体1在x轴上使用5个细分,在z轴上使用5个细分,总共100个面。

Sphere 2使用0递归细分,共计100个面。

他们使用相同数量的面孔,但我认为左侧的球体看起来更好。它看起来不那么块状,而是更圆润。让我们看一下用这两种方法生成的面孔数量。

二十面体

  • 0至100级面孔
  • 1-420张面孔
  • 2级-1,700张面孔
  • 3级-6,820张面孔
  • 4级-27,300张面孔
  • 5级-109,220张面孔

细分领域:

  • YZ:5-100张面孔
  • YZ:10-400张脸
  • YZ:15-900张脸
  • YZ:20-1,600张
  • YZ:25-2,500张脸
  • YZ:30-3,600张

如您所见,二十面体的面孔以指数速度增加,达到三次方!这是因为对于每个三角形,我们必须将它们细分为三个新的三角形。

事实是:你没有需要的精度二十面体会给你。因为它们都隐藏了一个更棘手的问题:在3D球体上为2D平面纹理化。这是顶部的样子:

顶糟透了

在左上角,您可以看到正在使用的纹理。巧合的是,它也是按程序生成的。(嘿,这是一门有关程序生成的课程,对吧?)

看起来糟透了吧?好吧,这将是很好的。我为我的纹理贴图获得了最高分,因为大多数人甚至都没有做到这一点。

因此,请考虑使用余弦和正弦生成球体。对于相同数量的细节,它产生的面孔更少。


6
恐怕我只能对此投反对票。环流指数成倍增长?这仅仅是因为决定应该按比例扩展。在相同数量的细节下,UV球比icosphere生成更少的人脸?那是错误的,绝对是错误的,完全是倒退。
sam hocevar

4
细分不必是递归的。您可以根据需要将三角形的边缘分成相等的多个部分。使用N零件将为您提供N*N新的三角形,该三角形是二次的,就像您对UV球所做的一样。
sam hocevar 2012年

6
我还必须补充一点,从最佳角度来看,您所说的看起来“不那么块状,而是更多的圆”的球体也使这一说法不诚实。只需对从上方查看的球体进行相同的屏幕截图即可了解我的意思。
sam hocevar 2012年

4
另外,您的二十面体编号看起来不正确。0级是20个面(按定义),然后是80、320、1280等。您可以细分为任意数量和所需的任何模式。模型的平滑度最终将取决于最终结果中人脸的数量和分布(与生成人脸的方法无关),我们希望保持每个人脸的大小尽可能均匀(无极性)挤压)以保持一致的轮廓(无论视角如何)。此外,细分代码要简单得多(imho)...
3Dave 2012年

2
此答案已作了一些工作,这使我对否决它感到有点不好。但这是完全错误的,所以我必须这样做。完美圆润的Icosphere充满了FullHD的整个屏幕,需要5个细分,而基本的二十面体没有细分。没有细分的二十面体没有100个面,它有20个面。Icosa =20。这就是名字!每个细分将面数乘以4,因此1-> 80、2-> 320、3-> 1280、4-> 5120、5-> 20,480。对于一个地球,我们至少需要40'000个面才能得到一个相等的圆形球。
彼得

-1

下面的脚本将创建一个具有n个多边形...基数为12的二十面体。它将还将这些多边形细分为单独的网格,并计算总的顶点,重复项和多边形。

我找不到类似的东西,所以我创建了这个。只需将脚本附加到GameObject,然后在编辑器中设置细分即可。接下来进行噪声修改。


/* Creates an initial Icosahedron with 12 vertices...
 * ...Adapted from https://medium.com/@peter_winslow/creating-procedural-icosahedrons-in-unity-part-1-df83ecb12e91
 * ...And a couple other Icosahedron C# for Unity scripts
 * 
 * Allows an Icosahedron to be created with multiple separate polygon meshes
 * I used a dictionary of Dictionary<int, List<Vector3>> to represent the 
 * Polygon index and the vertice index
 * polygon[0] corresponds to vertice[0]
 * so that all vertices in dictionary vertice[0] will correspond to the polygons in polygon[0]
 * 
 * If you need help understanding Dictionaries
 * https://msdn.microsoft.com/en-us/library/xfhwa508(v=vs.110).aspx
 * 
 * --I used dictionaries because I didn't know what programming instrument to use, so there may be more
 * elegant or efficient ways to go about this.
 * 
 * Essentially int represents the index, and 
 * List<Vector3> represents the actual Vector3 Transforms of the triangle
 * OR List<Vector3> in the polygon dictionary will act as a reference to the indice/index number of the vertices
 * 
 * For example the polygon dictionary at key[0] will contain a list of Vector3's representing polygons
 * ... Vector3.x , Vector3.y, Vector3.z in the polygon list would represent the 3 indexes of the vertice[0] list
 * AKA the three Vector3 transforms that make up the triangle
 *    .
 *  ./_\.
 * 
 * Create a new GameObject and attach this script
 *  -The folders for the material and saving of the mesh data will be created automatically 
 *    -Line 374/448
 * 
 * numOfMainTriangles will represent the individual meshes created
 * numOfSubdivisionsWithinEachTriangle represents the number of subdivisions within each mesh
 * 
 * Before running with Save Icosahedron checked be aware that it can take several minutes to 
 *   generate and save all the meshes depending on the level of divisions
 * 
 * There may be a faster way to save assets - Line 430 - AssetDatabase.CreateAsset(asset,path);
 * */

using System.Collections.Generic;
using UnityEngine;
using UnityEditor;

public class UnityIcosahedronGenerator : MonoBehaviour {
    IcosahedronGenerator icosahedron;
    public const int possibleSubDivisions = 7;
    public static readonly int[] supportedChunkSizes = { 20, 80, 320, 1280, 5120, 20480, 81920};

    [Range(0, possibleSubDivisions - 1)]
    public int numOfMainTriangles = 0;
    [Range(0,possibleSubDivisions - 1)]
    public int numOfSubdivisionsWithinEachTriangle = 0;
    public bool saveIcosahedron = false;

    // Use this for initialization
    void Start() {
        icosahedron = ScriptableObject.CreateInstance<IcosahedronGenerator>();

        // 0 = 12 verts, 20 tris
        icosahedron.GenBaseIcosahedron();
        icosahedron.SeparateAllPolygons();

        // 0 = 12 verts, 20 tris - Already Generated with GenBaseIcosahedron()
        // 1 = 42 verts, 80 tris
        // 2 = 162 verts, 320 tris
        // 3 = 642 verts, 1280 tris
        // 5 = 2562 verts, 5120 tris
        // 5 = 10242 verts, 20480 tris
        // 6 = 40962verts, 81920 tris
        if (numOfMainTriangles > 0) {
            icosahedron.Subdivide(numOfMainTriangles);
        }
        icosahedron.SeparateAllPolygons();

        if (numOfSubdivisionsWithinEachTriangle > 0) {
            icosahedron.Subdivide(numOfSubdivisionsWithinEachTriangle);
        }

        icosahedron.CalculateMesh(this.gameObject, numOfMainTriangles,numOfSubdivisionsWithinEachTriangle, saveIcosahedron);
        icosahedron.DisplayVertAndPolygonCount();
    }
}

public class Vector3Dictionary {
    public List<Vector3> vector3List;
    public Dictionary<int, List<Vector3>> vector3Dictionary;

    public Vector3Dictionary() {
        vector3Dictionary = new Dictionary<int, List<Vector3>>();
        return;
    }

    public void Vector3DictionaryList(int x, int y, int z) {
        vector3List = new List<Vector3>();

        vector3List.Add(new Vector3(x, y, z));
        vector3Dictionary.Add(vector3Dictionary.Count, vector3List);

        return;
    }

    public void Vector3DictionaryList(int index, Vector3 vertice) {
        vector3List = new List<Vector3>();

        if (vector3Dictionary.ContainsKey(index)) {
            vector3List = vector3Dictionary[index];
            vector3List.Add(vertice);
            vector3Dictionary[index] = vector3List;
        } else {
            vector3List.Add(vertice);
            vector3Dictionary.Add(index, vector3List);
        }

        return;
    }

    public void Vector3DictionaryList(int index, List<Vector3> vertice, bool list) {
        vector3List = new List<Vector3>();

        if (vector3Dictionary.ContainsKey(index)) {
            vector3List = vector3Dictionary[index];
            for (int a = 0; a < vertice.Count; a++) {
                vector3List.Add(vertice[a]);
            }
            vector3Dictionary[index] = vector3List;
        } else {
            for (int a = 0; a < vertice.Count; a++) {
                vector3List.Add(vertice[a]);
            }
            vector3Dictionary.Add(index, vector3List);
        }

        return;
    }

    public void Vector3DictionaryList(int index, int x, int y, int z) {
        vector3List = new List<Vector3>();

        if (vector3Dictionary.ContainsKey(index)) {
            vector3List = vector3Dictionary[index];
            vector3List.Add(new Vector3(x, y, z));
            vector3Dictionary[index] = vector3List;
        } else {
            vector3List.Add(new Vector3(x, y, z));
            vector3Dictionary.Add(index, vector3List);
        }

        return;
    }

    public void Vector3DictionaryList(int index, float x, float y, float z, bool replace) {
        if (replace) {
            vector3List = new List<Vector3>();

            vector3List.Add(new Vector3(x, y, z));
            vector3Dictionary[index] = vector3List;
        }

        return;
    }
}

public class IcosahedronGenerator : ScriptableObject {
    public Vector3Dictionary icosahedronPolygonDict;
    public Vector3Dictionary icosahedronVerticeDict;
    public bool firstRun = true;

    public void GenBaseIcosahedron() {
        icosahedronPolygonDict = new Vector3Dictionary();
        icosahedronVerticeDict = new Vector3Dictionary();

        // An icosahedron has 12 vertices, and
        // since it's completely symmetrical the
        // formula for calculating them is kind of
        // symmetrical too:

        float t = (1.0f + Mathf.Sqrt(5.0f)) / 2.0f;

        icosahedronVerticeDict.Vector3DictionaryList(0, new Vector3(-1, t, 0).normalized);
        icosahedronVerticeDict.Vector3DictionaryList(0, new Vector3(1, t, 0).normalized);
        icosahedronVerticeDict.Vector3DictionaryList(0, new Vector3(-1, -t, 0).normalized);
        icosahedronVerticeDict.Vector3DictionaryList(0, new Vector3(1, -t, 0).normalized);
        icosahedronVerticeDict.Vector3DictionaryList(0, new Vector3(0, -1, t).normalized);
        icosahedronVerticeDict.Vector3DictionaryList(0, new Vector3(0, 1, t).normalized);
        icosahedronVerticeDict.Vector3DictionaryList(0, new Vector3(0, -1, -t).normalized);
        icosahedronVerticeDict.Vector3DictionaryList(0, new Vector3(0, 1, -t).normalized);
        icosahedronVerticeDict.Vector3DictionaryList(0, new Vector3(t, 0, -1).normalized);
        icosahedronVerticeDict.Vector3DictionaryList(0, new Vector3(t, 0, 1).normalized);
        icosahedronVerticeDict.Vector3DictionaryList(0, new Vector3(-t, 0, -1).normalized);
        icosahedronVerticeDict.Vector3DictionaryList(0, new Vector3(-t, 0, 1).normalized);

        // And here's the formula for the 20 sides,
        // referencing the 12 vertices we just created.
        // Each side will be placed in it's own dictionary key.
        // The first number is the key/index, and the next 3 numbers reference the vertice index
        icosahedronPolygonDict.Vector3DictionaryList(0, 0, 11, 5);
        icosahedronPolygonDict.Vector3DictionaryList(1, 0, 5, 1);
        icosahedronPolygonDict.Vector3DictionaryList(2, 0, 1, 7);
        icosahedronPolygonDict.Vector3DictionaryList(3, 0, 7, 10);
        icosahedronPolygonDict.Vector3DictionaryList(4, 0, 10, 11);
        icosahedronPolygonDict.Vector3DictionaryList(5, 1, 5, 9);
        icosahedronPolygonDict.Vector3DictionaryList(6, 5, 11, 4);
        icosahedronPolygonDict.Vector3DictionaryList(7, 11, 10, 2);
        icosahedronPolygonDict.Vector3DictionaryList(8, 10, 7, 6);
        icosahedronPolygonDict.Vector3DictionaryList(9, 7, 1, 8);
        icosahedronPolygonDict.Vector3DictionaryList(10, 3, 9, 4);
        icosahedronPolygonDict.Vector3DictionaryList(11, 3, 4, 2);
        icosahedronPolygonDict.Vector3DictionaryList(12, 3, 2, 6);
        icosahedronPolygonDict.Vector3DictionaryList(13, 3, 6, 8);
        icosahedronPolygonDict.Vector3DictionaryList(14, 3, 8, 9);
        icosahedronPolygonDict.Vector3DictionaryList(15, 4, 9, 5);
        icosahedronPolygonDict.Vector3DictionaryList(16, 2, 4, 11);
        icosahedronPolygonDict.Vector3DictionaryList(17, 6, 2, 10);
        icosahedronPolygonDict.Vector3DictionaryList(18, 8, 6, 7);
        icosahedronPolygonDict.Vector3DictionaryList(19, 9, 8, 1);

        return;
    }

    public void SeparateAllPolygons(){
        // Separates all polygons and vertex keys/indicies into their own key/index
        // For example if the numOfMainTriangles is set to 2,
        // This function will separate each polygon/triangle into it's own index
        // By looping through all polygons in each dictionary key/index

        List<Vector3> originalPolygons = new List<Vector3>();
        List<Vector3> originalVertices = new List<Vector3>();
        List<Vector3> newVertices = new List<Vector3>();
        Vector3Dictionary tempIcosahedronPolygonDict = new Vector3Dictionary();
        Vector3Dictionary tempIcosahedronVerticeDict = new Vector3Dictionary();

        // Cycles through the polygon list
        for (int i = 0; i < icosahedronPolygonDict.vector3Dictionary.Count; i++) {
            originalPolygons = new List<Vector3>();
            originalVertices = new List<Vector3>();

            // Loads all the polygons in a certain index/key
            originalPolygons = icosahedronPolygonDict.vector3Dictionary[i];

            // Since the original script was set up without a dictionary index
            // It was easier to loop all the original triangle vertices into index 0
            // Thus the first time this function runs, all initial vertices will be 
            // redistributed to the correct indicies/index/key

            if (firstRun) {
                originalVertices = icosahedronVerticeDict.vector3Dictionary[0];
            } else {
                // i - 1 to account for the first iteration of pre-set vertices
                originalVertices = icosahedronVerticeDict.vector3Dictionary[i];
            }

            // Loops through all the polygons in a specific Dictionary key/index
            for (int a = 0; a < originalPolygons.Count; a++){
                newVertices = new List<Vector3>();

                int x = (int)originalPolygons[a].x;
                int y = (int)originalPolygons[a].y;
                int z = (int)originalPolygons[a].z;

                // Adds three vertices/transforms for each polygon in the list
                newVertices.Add(originalVertices[x]);
                newVertices.Add(originalVertices[y]);
                newVertices.Add(originalVertices[z]);

                // Overwrites the Polygon indices from their original locations
                // index (20,11,5) for example would become (0,1,2) to correspond to the
                // three new Vector3's added to the list.
                // In the case of this function there will only be 3 Vector3's associated to each dictionary key
                tempIcosahedronPolygonDict.Vector3DictionaryList(0, 1, 2);

                // sets the index to the size of the temp dictionary list
                int tempIndex = tempIcosahedronPolygonDict.vector3Dictionary.Count;
                // adds the new vertices to the corresponding same key in the vertice index
                // which corresponds to the same key/index as the polygon dictionary
                tempIcosahedronVerticeDict.Vector3DictionaryList(tempIndex - 1, newVertices, true);
            }
        }
        firstRun = !firstRun;

        // Sets the temp dictionarys as the main dictionaries
        icosahedronVerticeDict = tempIcosahedronVerticeDict;
        icosahedronPolygonDict = tempIcosahedronPolygonDict;
    }

    public void Subdivide(int recursions) {
        // Divides each triangle into 4 triangles, and replaces the Dictionary entry

        var midPointCache = new Dictionary<int, int>();
        int polyDictIndex = 0;
        List<Vector3> originalPolygons = new List<Vector3>();
        List<Vector3> newPolygons;

        for (int x = 0; x < recursions; x++) {
            polyDictIndex = icosahedronPolygonDict.vector3Dictionary.Count;
            for (int i = 0; i < polyDictIndex; i++) {
                newPolygons = new List<Vector3>();
                midPointCache = new Dictionary<int, int>();
                originalPolygons = icosahedronPolygonDict.vector3Dictionary[i];

                for (int z = 0; z < originalPolygons.Count; z++) {
                    int a = (int)originalPolygons[z].x;
                    int b = (int)originalPolygons[z].y;
                    int c = (int)originalPolygons[z].z;

                    // Use GetMidPointIndex to either create a
                    // new vertex between two old vertices, or
                    // find the one that was already created.
                    int ab = GetMidPointIndex(i,midPointCache, a, b);
                    int bc = GetMidPointIndex(i,midPointCache, b, c);
                    int ca = GetMidPointIndex(i,midPointCache, c, a);

                    // Create the four new polygons using our original
                    // three vertices, and the three new midpoints.
                    newPolygons.Add(new Vector3(a, ab, ca));
                    newPolygons.Add(new Vector3(b, bc, ab));
                    newPolygons.Add(new Vector3(c, ca, bc));
                    newPolygons.Add(new Vector3(ab, bc, ca));
                }
                // Replace all our old polygons with the new set of
                // subdivided ones.
                icosahedronPolygonDict.vector3Dictionary[i] = newPolygons;
            }
        }
        return;
    }

    int GetMidPointIndex(int polyIndex, Dictionary<int, int> cache, int indexA, int indexB) {
        // We create a key out of the two original indices
        // by storing the smaller index in the upper two bytes
        // of an integer, and the larger index in the lower two
        // bytes. By sorting them according to whichever is smaller
        // we ensure that this function returns the same result
        // whether you call
        // GetMidPointIndex(cache, 5, 9)
        // or...
        // GetMidPointIndex(cache, 9, 5)

        int smallerIndex = Mathf.Min(indexA, indexB);
        int greaterIndex = Mathf.Max(indexA, indexB);
        int key = (smallerIndex << 16) + greaterIndex;

        // If a midpoint is already defined, just return it.
        int ret;
        if (cache.TryGetValue(key, out ret))
            return ret;

        // If we're here, it's because a midpoint for these two
        // vertices hasn't been created yet. Let's do that now!
        List<Vector3> tempVertList = icosahedronVerticeDict.vector3Dictionary[polyIndex];

        Vector3 p1 = tempVertList[indexA];
        Vector3 p2 = tempVertList[indexB];
        Vector3 middle = Vector3.Lerp(p1, p2, 0.5f).normalized;

        ret = tempVertList.Count;
        tempVertList.Add(middle);
        icosahedronVerticeDict.vector3Dictionary[polyIndex] = tempVertList;

        cache.Add(key, ret);
        return ret;
    }

    public void CalculateMesh(GameObject icosahedron, int numOfMainTriangles, int numOfSubdivisionsWithinEachTriangle, bool saveIcosahedron) {
        GameObject meshChunk;
        List<Vector3> meshPolyList;
        List<Vector3> meshVertList;
        List<int> triList;

        CreateFolders(numOfMainTriangles, numOfSubdivisionsWithinEachTriangle);
        CreateMaterial();

        // Loads a material from the Assets/Resources/ folder so that it can be saved with the prefab later
        Material material = Resources.Load("BlankSphere", typeof(Material)) as Material;

        int polyDictIndex = icosahedronPolygonDict.vector3Dictionary.Count;

        // Used to assign the child objects as well as to be saved as the .prefab
        // Sets the name
        icosahedron.gameObject.name = "Icosahedron" + numOfMainTriangles + "Recursion" + numOfSubdivisionsWithinEachTriangle;

        for (int i = 0; i < polyDictIndex; i++) {
            meshPolyList = new List<Vector3>();
            meshVertList = new List<Vector3>();
            triList = new List<int>();
            // Assigns the polygon and vertex indices
            meshPolyList = icosahedronPolygonDict.vector3Dictionary[i];
            meshVertList = icosahedronVerticeDict.vector3Dictionary[i];

            // Sets the child gameobject parameters
            meshChunk = new GameObject("MeshChunk");
            meshChunk.transform.parent = icosahedron.gameObject.transform;
            meshChunk.transform.localPosition = new Vector3(0, 0, 0);
            meshChunk.AddComponent<MeshFilter>();
            meshChunk.AddComponent<MeshRenderer>();
            meshChunk.GetComponent<MeshRenderer>().material = material;
            meshChunk.AddComponent<MeshCollider>();
            Mesh mesh = meshChunk.GetComponent<MeshFilter>().mesh;

            // Adds the triangles to the list
            for (int z = 0; z < meshPolyList.Count; z++) {
                triList.Add((int)meshPolyList[z].x);
                triList.Add((int)meshPolyList[z].y);
                triList.Add((int)meshPolyList[z].z);
            }

            mesh.vertices = meshVertList.ToArray();
            mesh.triangles = triList.ToArray();
            mesh.uv = new Vector2[meshVertList.Count];

            /*
            //Not Needed because all normals have been calculated already
            Vector3[] _normals = new Vector3[meshVertList.Count];
            for (int d = 0; d < _normals.Length; d++){
                _normals[d] = meshVertList[d].normalized;
            }
            mesh.normals = _normals;
            */

            mesh.normals = meshVertList.ToArray();

            mesh.RecalculateBounds();

            // Saves each chunk mesh to a specified folder
            // The folder must exist
            if (saveIcosahedron) {
                string sphereAssetName = "icosahedronChunk" + numOfMainTriangles + "Recursion" + numOfSubdivisionsWithinEachTriangle + "_" + i + ".asset";
                AssetDatabase.CreateAsset(mesh, "Assets/Icosahedrons/Icosahedron" + numOfMainTriangles + "Recursion" + numOfSubdivisionsWithinEachTriangle + "/" + sphereAssetName);
                AssetDatabase.SaveAssets();
            }
        }

        // Removes the script for the prefab save
        // Saves the prefab to a specified folder
        // The folder must exist
        if (saveIcosahedron) {
            DestroyImmediate(icosahedron.GetComponent<UnityIcosahedronGenerator>());
            PrefabUtility.CreatePrefab("Assets/Icosahedrons/Icosahedron" + numOfMainTriangles + "Recursion" + numOfSubdivisionsWithinEachTriangle + "/Icosahedron" + numOfMainTriangles + "Recursion" + numOfSubdivisionsWithinEachTriangle + ".prefab", icosahedron);
        }

        return;
    }

    void CreateFolders(int numOfMainTriangles, int numOfSubdivisionsWithinEachTriangle){
        // Creates the folders if they don't exist
        if (!AssetDatabase.IsValidFolder("Assets/Icosahedrons")) {
            AssetDatabase.CreateFolder("Assets", "Icosahedrons");
        }
        if (!AssetDatabase.IsValidFolder("Assets/Icosahedrons/Icosahedron" + numOfMainTriangles + "Recursion" + numOfSubdivisionsWithinEachTriangle)) {
            AssetDatabase.CreateFolder("Assets/Icosahedrons", "Icosahedron" + numOfMainTriangles + "Recursion" + numOfSubdivisionsWithinEachTriangle);
        }
        if (!AssetDatabase.IsValidFolder("Assets/Resources")) {
            AssetDatabase.CreateFolder("Assets", "Resources");
        }

        return;
    }

    static void CreateMaterial() {
        if (Resources.Load("BlankSphere", typeof(Material)) == null) {
            // Create a simple material asset if one does not exist
            Material material = new Material(Shader.Find("Standard"));
            material.color = Color.blue;
            AssetDatabase.CreateAsset(material, "Assets/Resources/BlankSphere.mat");
        }

        return;
    }

    // Displays the Total Polygon/Triangle and Vertice Count
    public void DisplayVertAndPolygonCount(){
        List<Vector3> tempVertices;
        HashSet<Vector3> verticeHash = new HashSet<Vector3>();

        int polygonCount = 0;
        List<Vector3> tempPolygons;

        // Saves Vertices to a hashset to ensure no duplicate vertices are counted
        for (int a = 0; a < icosahedronVerticeDict.vector3Dictionary.Count; a++) {
            tempVertices = new List<Vector3>();
            tempVertices = icosahedronVerticeDict.vector3Dictionary[a];
            for (int b = 0; b < tempVertices.Count; b++) {
                verticeHash.Add(tempVertices[b]);
            }
        }

        for (int a = 0; a < icosahedronPolygonDict.vector3Dictionary.Count; a++) {
            tempPolygons = new List<Vector3>();
            tempPolygons = icosahedronPolygonDict.vector3Dictionary[a];
            for (int b = 0; b < tempPolygons.Count; b++) {
                polygonCount++;
            }
        }

        Debug.Log("Vertice Count: " + verticeHash.Count);
        Debug.Log("Polygon Count: " + polygonCount);

        return;
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.