To get something like this:
Create an icosahedron (20-sided regular solid) and subdivide the faces to get a sphere (see code below).
The idea is basically:
- Create a regular n-hedron (a solid where every face is the same size). I use an icosahedron because it's the solid with the greatest number of faces where every face is the same size. (There's a proof for that somewhere out there. Feel free to Google if you're really curious.) This will give you a sphere where nearly every face is the same size, making texturing a little easier.
-
Subdivide each face into four equally-sized faces. Each time you do this, it'll quadruple the number of faces in the model.
/// i0
/// / \
/// m02-m01
/// / \ / \
/// i2---m12---i1
i0
, i1
, and i2
are the vertices of the original triangle. (Actually, indices into the vertex buffer, but that's another topic). m01
is the midpoint of the edge (i0,i1)
, m12 is the midpoint of the edge(i1,12)
, and m02
is, obviously, the midpoint of the edge (i0,i2)
.
Whenever you subdivide a face, make sure that you don't create duplicate vertices. Each midpoint will be shared by one other face (since the edges are shared between faces). The code below accounts for that by maintaining a dictionary of named midpoints that have been created, and returning the index of a previously created midpoint when it's available rather than creating a new one.
-
Repeat until you've reached the desired number of faces for your cube.
-
When you're done, normalize all of the vertices to smooth out the surface. If you don't do this, you'll just get a higher-res icosahedron instead of a sphere.
-
Voila! You're done. Convert the resulting vector and index buffers into a VertexBuffer
andIndexBuffer
, and draw with Device.DrawIndexedPrimitives()
.
Here's what you'd use in your "Sphere" class to create the model (XNA datatypes and C#, but it should be pretty clear):
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]);
And the GeometryProvider
class
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);
}
}
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>
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)
}
);
}
}
EDIT
I just noticed that you're trying to create a lat/long-type sphere rather than a geosphere. I'll see what I can dig up, but unless you have a particular reason for doing this - you're drawing this wireframe and want a particular aesthetic - than a geosphere is a lot more texture friendly.
vertices[i] = normalize(vertices[i])
. Incidentally, this also gives you your new, correct normals, so you should donormals[i] = vertices[i]
afterwards. – Sam Hocevar Jun 29 '12 at 14:54