This time we will look at how to generate a sphere in code, I got tired of the cubes and wanted a little variation. The decision fell on IcoSpheres, mostly as they seem to be more flexible in the long run.

This is part 13 of my series on OpenGL4 with OpenTK.

For other posts in this series:

OpenGL 4 with OpenTK in C# Part 1: Initialize the GameWindow

OpenGL 4 with OpenTK in C# Part 2: Compiling shaders and linking them

OpenGL 4 with OpenTK in C# Part 3: Passing data to shaders

OpenGL 4 with OpenTK in C# Part 4: Refactoring and adding error handling

OpenGL 4 with OpenTK in C# Part 5: Buffers and Triangle

OpenGL 4 with OpenTK in C# Part 6: Rotations and Movement of objects

OpenGL 4 with OpenTK in C# Part 7: Vectors and Matrices

OpenGL 4 with OpenTK in C# Part 8: Drawing multiple objects

OpenGL 4 with OpenTK in C# Part 9: Texturing

OpenGL 4 with OpenTK in C# Part 10: Asteroid Invaders

OpenGL 4 with OpenTK in C# Part 11: Mipmap

OpenGL 4 with OpenTK in C# Part 12: Basic Moveable Camera

OpenGL 4 with OpenTK in C# Part 13: IcoSphere

OpenGL 4 with OpenTK in C# Part 14: Basic Text

As stated in the previous post, I am in no way an expert in OpenGL. I write these posts as a way to learn and if someone else finds these posts useful then all the better :)

If you think that the progress is slow, then know that I am a slow learner :P

This part will build upon the game window and shaders from part 12.

## Introduction

So I take no credit for this algorithm to generate the sphere. I found a WPF version over at catch 22, Andreas Kahlers blog. I ported it to work with our OpenTK implementation of vertex buffers and texturing.Texturing fix for the common artefact of a funny looking stripe that goes from pole to pole is based on a solution described over at sol.gfxil.net.

So, now that credit is where it belongs, lets look at the code.

## Code

private struct Face { public Vector3 V1; public Vector3 V2; public Vector3 V3; public Face(Vector3 v1, Vector3 v2, Vector3 v3) { V1 = v1; V2 = v2; V3 = v3; } }First we need a struct where we will store each face of our sphere. Basically just contains 3 vectors for each point in a triangle.

The algorithm from Khalers blog. As noted above, this does not use vertex indexing. Meaning that we have a little memory overhead as every vertex is doubled instead of reused.

Basic algorithm is to generate the initial points manually and then for each iteration split each face into 4 new faces and project them into the unit sphere by normalizing them.

public class IcoSphereFactory { private List<Vector3> _points; private int _index; private Dictionary<long, int> _middlePointIndexCache; public TexturedVertex[] Create(int recursionLevel) { _middlePointIndexCache = new Dictionary<long, int>(); _points = new List<Vector3>(); _index = 0; var t = (float)((1.0 + Math.Sqrt(5.0)) / 2.0); var s = 1; AddVertex(new Vector3(-s, t, 0)); AddVertex(new Vector3(s, t, 0)); AddVertex(new Vector3(-s, -t, 0)); AddVertex(new Vector3(s, -t, 0)); AddVertex(new Vector3(0, -s, t)); AddVertex(new Vector3(0, s, t)); AddVertex(new Vector3(0, -s, -t)); AddVertex(new Vector3(0, s, -t)); AddVertex(new Vector3(t, 0, -s)); AddVertex(new Vector3(t, 0, s)); AddVertex(new Vector3(-t, 0, -s)); AddVertex(new Vector3(-t, 0, s)); var faces = new List<Face>(); // 5 faces around point 0 faces.Add(new Face(_points[0], _points[11], _points[5])); faces.Add(new Face(_points[0], _points[5], _points[1])); faces.Add(new Face(_points[0], _points[1], _points[7])); faces.Add(new Face(_points[0], _points[7], _points[10])); faces.Add(new Face(_points[0], _points[10], _points[11])); // 5 adjacent faces faces.Add(new Face(_points[1], _points[5], _points[9])); faces.Add(new Face(_points[5], _points[11], _points[4])); faces.Add(new Face(_points[11], _points[10], _points[2])); faces.Add(new Face(_points[10], _points[7], _points[6])); faces.Add(new Face(_points[7], _points[1], _points[8])); // 5 faces around point 3 faces.Add(new Face(_points[3], _points[9], _points[4])); faces.Add(new Face(_points[3], _points[4], _points[2])); faces.Add(new Face(_points[3], _points[2], _points[6])); faces.Add(new Face(_points[3], _points[6], _points[8])); faces.Add(new Face(_points[3], _points[8], _points[9])); // 5 adjacent faces faces.Add(new Face(_points[4], _points[9], _points[5])); faces.Add(new Face(_points[2], _points[4], _points[11])); faces.Add(new Face(_points[6], _points[2], _points[10])); faces.Add(new Face(_points[8], _points[6], _points[7])); faces.Add(new Face(_points[9], _points[8], _points[1])); // refine triangles for (int i = 0; i < recursionLevel; i++) { var faces2 = new List<Face>(); foreach (var tri in faces) { // replace triangle by 4 triangles int a = GetMiddlePoint(tri.V1, tri.V2); int b = GetMiddlePoint(tri.V2, tri.V3); int c = GetMiddlePoint(tri.V3, tri.V1); faces2.Add(new Face(tri.V1, _points[a], _points[c])); faces2.Add(new Face(tri.V2, _points[b], _points[a])); faces2.Add(new Face(tri.V3, _points[c], _points[b])); faces2.Add(new Face(_points[a], _points[b], _points[c])); } faces = faces2; } // done, now add triangles to mesh var vertices = new List<TexturedVertex>(); foreach (var tri in faces) { var uv1 = GetSphereCoord(tri.V1); var uv2 = GetSphereCoord(tri.V2); var uv3 = GetSphereCoord(tri.V3); vertices.Add(new TexturedVertex(new Vector4(tri.V1, 1), uv1)); vertices.Add(new TexturedVertex(new Vector4(tri.V2, 1), uv2)); vertices.Add(new TexturedVertex(new Vector4(tri.V3, 1), uv3)); } return vertices.ToArray(); } private int AddVertex(Vector3 p) { _points.Add(p.Normalized()); return _index++; } // return index of point in the middle of p1 and p2 private int GetMiddlePoint(Vector3 point1, Vector3 point2) { long i1 = _points.IndexOf(point1); long i2 = _points.IndexOf(point2); // first check if we have it already var firstIsSmaller = i1 < i2; long smallerIndex = firstIsSmaller ? i1 : i2; long greaterIndex = firstIsSmaller ? i2 : i1; long key = (smallerIndex << 32) + greaterIndex; int ret; if (_middlePointIndexCache.TryGetValue(key, out ret)) { return ret; } // not in cache, calculate it var middle = new Vector3( (point1.X + point2.X) / 2.0f, (point1.Y + point2.Y) / 2.0f, (point1.Z + point2.Z) / 2.0f); // add vertex makes sure point is on unit sphere int i = AddVertex(middle); // store it, return index _middlePointIndexCache.Add(key, i); return i; } }Get sphere coordinate is my own addition for calculating the texture coordinate for each vertex.

public static Vector2 GetSphereCoord(Vector3 i) { var len = i.Length; Vector2 uv; uv.Y = (float)(Math.Acos(i.Y / len) / Math.PI); uv.X = -(float)((Math.Atan2(i.Z, i.X) / Math.PI + 1.0f) * 0.5f); return uv; }

At this point it looks like the following:

Icosphere that shows the texturing glitch from pole to pole |

private static void FixColorStrip(ref Vector2 uv1, ref Vector2 uv2, ref Vector2 uv3) { if ((uv1.X - uv2.X) >= 0.8f) uv1.X -= 1; if ((uv2.X - uv3.X) >= 0.8f) uv2.X -= 1; if ((uv3.X - uv1.X) >= 0.8f) uv3.X -= 1; if ((uv1.X - uv2.X) >= 0.8f) uv1.X -= 1; if ((uv2.X - uv3.X) >= 0.8f) uv2.X -= 1; if ((uv3.X - uv1.X) >= 0.8f) uv3.X -= 1; }

And call it from here in the Create function

foreach (var tri in faces) { var uv1 = GetSphereCoord(tri.V1); var uv2 = GetSphereCoord(tri.V2); var uv3 = GetSphereCoord(tri.V3); FixColorStrip(ref uv1, ref uv2, ref uv3); vertices.Add(new TexturedVertex(new Vector4(tri.V1, 1), uv1)); vertices.Add(new TexturedVertex(new Vector4(tri.V2, 1), uv2)); vertices.Add(new TexturedVertex(new Vector4(tri.V3, 1), uv3)); }

Now the texture should look OK like this:

Icosphere that shows the texturing glitch fixed |

models.Add("Wooden", new MipMapGeneratedRenderObject(new IcoSphereFactory().Create(3), _texturedProgram.Id, @"Components\Textures\wooden.png", 8)); models.Add("Golden", new MipMapGeneratedRenderObject(new IcoSphereFactory().Create(3), _texturedProgram.Id, @"Components\Textures\golden.bmp", 8)); models.Add("Asteroid", new MipMapGeneratedRenderObject(new IcoSphereFactory().Create(3), _texturedProgram.Id, @"Components\Textures\moonmap1k.jpg", 8)); models.Add("Spacecraft", new MipMapGeneratedRenderObject(RenderObjectFactory.CreateTexturedCube6(1, 1, 1), _texturedProgram.Id, @"Components\Textures\spacecraft.png", 8)); models.Add("Gameover", new MipMapGeneratedRenderObject(RenderObjectFactory.CreateTexturedCube6(1, 1, 1), _texturedProgram.Id, @"Components\Textures\gameover.png", 8)); models.Add("Bullet", new MipMapGeneratedRenderObject(new IcoSphereFactory().Create(3), _texturedProgram.Id, @"Components\Textures\dotted.png", 8));

End result should be as in the following video:

## Known issues that still need to be fixed:

Texturing at the poles is still glitchy. I was unable to get the solution described at sol.gfxil.net for it to work.. Yet.Maybe implement the vertex index solution as soon as I figure it out. As this currently fits my needs that might take a while.

For the complete source code for the tutorial at the end of this part, go to: https://github.com/eowind/dreamstatecoding

So there, thank you for reading. Hope this helps someone out there : )

Until next time: Work to Live, Don’t Live to Work

## No comments:

## Post a Comment