←Back

Sphere

Download: sphere.zip, icosphere.zip

This page describes how to generate various spherical geometries and how to draw them with OpenGL.

Sphere

The definition of sphere is a 3D closed surface where every point on the sphere is same distance (radius) from a given point. The equation of a sphere at the origin is equation of sphere.

Since we cannot draw all the points on a sphere, we only sample a limited amount of points by dividing the sphere by sectors (longitude) and stacks (latitude). Then connect these sampled points together to form surfaces of the sphere.

Sectors and stacks of a sphere
Sectors and stacks of a sphere
Parametric equation of a sphere
A point on a sphere using sector and stack angles

An arbitrary point (x, y, z) on a sphere can be computed by parametric equations with the corresponding sector angle θ and stack angle ϕ.

formula to find a point on a sphere

The range of sector angles is from 0 to 360 degrees, and the stack angles are from 90 (top) to -90 degrees (bottom). The sector and stack angle for each step can be calculated by the following;

The following C++ code generates all vertices of the sphere for the given radius, sectors and stacks. It also creates other vertex attributes; surface normals and texture coordinates. For more details, please refer to buildVerticesSmooth() or buildVerticesFlat() functions in Sphere.cpp class.


// clear memory of prev arrays
std::vector<float>().swap(vertices);
std::vector<float>().swap(normals);
std::vector<float>().swap(texCoords);

float x, y, z, xy;                              // vertex position
float nx, ny, nz, lengthInv = 1.0f / radius;    // vertex normal
float s, t;                                     // vertex texCoord

float sectorStep = 2 * PI / sectorCount;
float stackStep = PI / stackCount;
float sectorAngle, stackAngle;

for(int i = 0; i <= stackCount; ++i)
{
    stackAngle = PI / 2 - i * stackStep;        // starting from pi/2 to -pi/2
    xy = radius * cosf(stackAngle);             // r * cos(u)
    z = radius * sinf(stackAngle);              // r * sin(u)

    // add (sectorCount+1) vertices per stack
    // the first and last vertices have same position and normal, but different tex coods
    for(int j = 0; j <= sectorCount; ++j)
    {
        sectorAngle = j * sectorStep;

        // vertex position (x, y, z)
        x = xy * cosf(sectorAngle);             // r * cos(u) * cos(v)
        y = xy * sinf(sectorAngle);             // r * cos(u) * sin(v)
        vertices.push_back(x);
        vertices.push_back(y);
        vertices.push_back(z);

        // vertex normal (nx, ny, nz)
        nx = x * lengthInv;
        ny = y * lengthInv;
        nz = z * lengthInv;
        normals.push_back(nx);
        normals.push_back(ny);
        normals.push_back(nz);

        // vertex tex coord (s, t)
        s = (float)j / sectorCount;
        t = (float)i / stackCount;
        texCoords.push_back(s);
        texCoords.push_back(t);
    }
}

vertex indices of sphere
vertex indices to draw triangles of a sphere

In order to draw the surface of a sphere in OpenGL, you must triangulate adjacent vertices to form polygons. It is possible to use a single triangle strip to render the whole sphere. However, if the shared vertices have different normals or texture coordinates, then a single triangle strip cannot be used.

Each sector in a stack requires 2 triangles. If the first vertex index in the current stack is k1 and the next stack is k2, then the orders of vertex indices of 2 triangles are;
k1 – k2 – k1+1
k1+1 – k2 – k2+1

But, the top and bottom stacks require only one triangle per sector. The code snippet to generate all triangles of a sphere may look like;


// generate index list of sphere triangles
std::vector<int> indices;
int k1, k2;
for(int i = 0; i < stackCount; ++i)
{
    k1 = i * (sectorCount + 1);     // beginning of current stack
    k2 = k1 + sectorCount + 1;      // beginning of next stack

    for(int j = 0; j < sectorCount; ++j, ++k1, ++k2)
    {
        // 2 triangles per sector excluding 1st and last stacks
        if(i != 0)
        {
            indices.push_back(k1);
            indices.push_back(k2);
            indices.push_back(k1 + 1);
        }

        if(i != (stackCount-1))
        {
            indices.push_back(k1 + 1);
            indices.push_back(k2);
            indices.push_back(k2 + 1);
        }
    }
}

Example: Sphere

example of drawing sphere
This example constructs spheres with 36 sectors and 18 stacks, but with different shadings; flat, smooth or textured. Sphere.cpp class provides pre-defined functions; draw(), drawWithLines() and drawLines(), to draw a sphere using OpenGL VertexArray.
Download: sphere.zip

This class also provides getVertices(), getIndices(), getInterleavedVertices(), etc. in order to access the vertex data. The following code draws a sphere with interleaved vertex data and VBO;


// create a sphere with smooth shading
Sphere sphere(1.0f, 36, 18);                    // radius, sectors, stacks

// copy interleaved vertex data (V/N/T) to VBO
GLuint vboId;
glGenBuffers(1, &vboId);
glBindBuffer(GL_ARRAY_BUFFER, vboId);
glBufferData(GL_ARRAY_BUFFER,                   // target
             sphere.getInterleavedVertexSize(), // data size, bytes
             sphere.getInterleavedVertices(),   // ptr to vertex data
             GL_STATIC_DRAW);                   // usage

// copy index data to VBO
GLuint iboId;
glGenBuffers(1, &iboId);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, iboId);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,           // target
             sphere.getIndexSize(),             // data size, bytes
             sphere.getIndices(),               // ptr to index data
             GL_STATIC_DRAW);                   // usage
...


// draw sphere with VBO
glBindBuffer(GL_ARRAY_BUFFER, vboId);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, iboId);

glEnableVertexAttribArray(attribVertex);
glEnableVertexAttribArray(attribNormal);
glEnableVertexAttribArray(attribTexCoord);

int stride = sphere.getInterleavedStride();     // should be 32 bytes
glVertexAttribPointer(attribVertex, 3, GL_FLOAT, false, stride, 0);
glVertexAttribPointer(attribNormal, 3, GL_FLOAT, false, stride, (void*)(sizeof(float)*3));
glVertexAttribPointer(attribTexCoord, 2, GL_FLOAT, false, stride, (void*)(sizeof(float)*6));

glDrawElements(GL_TRIANGLES,                    // primitive type
               sphere.getIndexCount(),          // # of indices
               GL_UNSIGNED_INT,                 // data type
               (void*)0);                       // offset to indices

glDisableVertexAttribArray(attribVertex);
glDisableVertexAttribArray(attribNormal);
glDisableVertexAttribArray(attribTexCoord);

// unbind
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

Since this class uses cylindrical texture mapping, there is a squeeze/distortion at the north and south pole area. This problem can be solved using Icosphere.

Icosphere / Geosphere

Another way to create a spherical geometry is subdividing an icosahedron multiple times. Icosahedron is a regular polyhedron with 12 vertices and 20 equilateral triangles (the first left image below). Each triangle of an icosahedron is divided into 4 equal sub-triangles per subdivision.

icosphere at subdivision 0
Icosphere at subdivision 0
icosphere at subdivision 1
Icosphere at subdivision 1
icosphere at subdivision 2
Icosphere at subdivision 2
icosphere at subdivision 3
Icosphere at subdivision 3

One way to construct 12 vertices of an icosahedron is using spherical coordinates; aligning 2 vertices at the north and south poles, and the other 10 vertices are placed at latitude arctan(1/2) degrees and 72° aside on the same latitude. Please see the following orthogonal projection images of icosahedron.

side view of icosahedron
Side View of Icosahedron
2 vertices are at north/south pole and 10 vertices are at elevation ±26.565°
side view of icosahedron
Top View of Icosahedron
5 vertices are 72° apart at the same elevation

A typical point at latitude 26.565° and radius r can be computed by;

The following C++ code is to generate 12 vertices of an icosahedron for a given radius, or you can find the complete implementation of Icosahedron.cpp or Icosphere.cpp class.


// constants
const float PI = 3.1415926f;
const float H_ANGLE = PI / 180 * 72;    // 72 degree = 360 / 5
const float V_ANGLE = atanf(1.0f / 2);  // elevation = 26.565 degree

std::vector<float> vertices(12 * 3);    // array of 12 vertices (x,y,z)
int i1, i2;                             // indices
float z, xy;                            // coords
float hAngle1 = -PI / 2 - H_ANGLE / 2;  // start from -126 deg at 1st row
float hAngle2 = -PI / 2;                // start from -90 deg at 2nd row

// the first top vertex at (0, 0, r)
vertices[0] = 0;
vertices[1] = 0;
vertices[2] = radius;

// compute 10 vertices at 1st and 2nd rows
for(int i = 1; i <= 5; ++i)
{
    i1 = i * 3;         // for 1st row
    i2 = (i + 5) * 3;   // for 2nd row

    z = radius * sin(V_ANGLE);              // elevaton
    xy = radius * cos(V_ANGLE);             // length on XY plane

    vertices[i1] = xy * cosf(hAngle1);      // x
    vertices[i2] = xy * cosf(hAngle2);
    vertices[i1 + 1] = xy * sinf(hAngle1);  // y
    vertices[i2 + 1] = xy * sinf(hAngle2);
    vertices[i1 + 2] = z;                   // z
    vertices[i2 + 2] = -z;

    // next horizontal angles
    hAngle1 += H_ANGLE;
    hAngle2 += H_ANGLE;
}

// the last bottom vertex at (0, 0, -r)
i1 = 11 * 3;
vertices[i1] = 0;
vertices[i1 + 1] = 0;
vertices[i1 + 2] = -radius;

The subdivision algorithm is splitting the 3 edge lines of each triangle in half, then extruding the new middle point outward, so its length is the same as sphere's radius.


std::vector<float> tmpVertices;
const float *v1, *v2, *v3;          // ptr to original vertices of a triangle
float newV1[3], newV2[3], newV3[3]; // new vertex positions

// iterate all subdivision levels
for(int i = 1; i <= subdivision; ++i)
{
    // copy prev vertex array and clear
    tmpVertices = vertices;
    vertices.clear();

    // perform subdivision foor each triangle
    for(j = 0; j < indices.size(); j += 3)
    {
        // get 3 vertices of a triangle
        v1 = &tmpVertices[indices[j] * 3];
        v2 = &tmpVertices[indices[j + 1] * 3];
        v3 = &tmpVertices[indices[j + 2] * 3];

        // compute 3 new vertices by spliting half on each edge
        //         v1       
        //        / \       
        // newV1 *---* newV3
        //      / \ / \     
        //    v2---*---v3   
        //       newV2      
        computeHalfVertex(v1, v2, newV1);
        computeHalfVertex(v2, v3, newV2);
        computeHalfVertex(v1, v3, newV3);

        // add 4 new triangles to vertex array
        addVertices(v1,    newV1, newV3);
        addVertices(newV1, v2,    newV2);
        addVertices(newV1, newV2, newV3);
        addVertices(newV3, newV2, v3);
    }
}


///////////////////////////////////////////////////////////////////////////////
// find middle point of 2 vertices
// NOTE: new vertex must be resized, so the length is equal to the radius
///////////////////////////////////////////////////////////////////////////////
void computeHalfVertex(const float v1[3], const float v2[3], float newV[3])
{
    newV[0] = v1[0] + v2[0];    // x
    newV[1] = v1[1] + v2[1];    // y
    newV[2] = v1[2] + v2[2];    // z
    float scale = radius / sqrtf(newV[0]*newV[0] + newV[1]*newV[1] + newV[2]*newV[2]);
    newV[0] *= scale;
    newV[1] *= scale;
    newV[2] *= scale;
}

In order to generate a texture map of an icosphere, you need to unwrap the 3D geometry on a plane (paper model). I use the following texture coordinates of vertices instead of normalized 0 to 1, so the coordinate of each vertex is snapped to exact pixel on the image. For example, if a texture size is 2048x1024, then the horizontal step is 186 pixels and the vertical step is 322 pixels.

texture coordinates of icosphere
Texture coordinates of Icosphere, where S=186/2048, T=322/1024

Example: Icosphere

example of drawing icosphere
This example is to draw an icosphere with a texture. Press the spacebar key to change the subdivision level. If the subdivision is 2, the icosphere consists of 320 triangles, and if the subdivision is 5, it has 20,480 triangles.
Download: icosphere.zip, icosahedron.zip

Drawing an icosphere in OpenGL is identical to Sphere class object. Please refer to Sphere example section above. To construct an icosphere object, it requires 3 parameters; radius, subdivision and surface smoothness. You can change the radius and subdivision level after it has been constructed. If the subdivision is 0, then it is the same as an icosahedron.


// create icosphere with radius=1, subdivision=5 and smooth shading=true
Icosphere sphere(1.0f, 5, true);

// change parameters later
sphere.setRadius(2.0f);
sphere.setSubdivision(6);
sphere.setSmooth(false);

←Back
Hide Comments
comments powered by Disqus