Creating a Sphere component

Our Solar System will be constructed from spheres, representing planets, moons, and the Sun. Let's first create a Sphere component. We are going to define a sphere as a triangle mesh of vertices that form the surface of the sphere (For more information on a triangle mesh, refer to https://en.wikipedia.org/wiki/Triangle_mesh).

Right-click on the RenderBoxExt/components folder, select New | Java Class, and name it Sphere. Define it as public class Sphere extends RenderObject:

public class Sphere extends RenderObject{
    private static final String TAG = "RenderBox.Sphere";
    public Sphere() {
        super();
        allocateBuffers();
    }
}

The constructor calls a helper method, allocateBuffers, which allocates buffers for vertices, normals, textures, and indexes. Let's declare variables for these at the top of the class:

    public static FloatBuffer vertexBuffer;
    public static FloatBuffer normalBuffer;
    public static FloatBuffer texCoordBuffer;
    public static ShortBuffer indexBuffer;
    public static int numIndices;

Note that we've decided to declare the buffers public to afford future flexibility in creating arbitrary texture materials for objects.

We'll define a sphere with a radius of 1. Its vertices are arranged by 24 longitude sections (as hours of the day) and 16 latitude sections, providing sufficient resolution for our purposes. The top and bottom caps are handled separately. This is a long method, so we'll break it down for you. Here's the first part of the code where we declare and initialize variables, including the vertices array. Similar to our Material setup methods, we only need to allocate the Sphere buffers once, and in this case, we use the vertex buffer variable to keep track of this state. If it is not null, the buffers have already been allocated. Otherwise, we should continue with the function, which will set this value:

    public static void allocateBuffers(){
        //Already allocated?
        if (vertexBuffer != null) return;
        //Generate a sphere model
        float radius = 1f;
        // Longitude |||
        int nbLong = 24;
        // Latitude ---
        int nbLat = 16;

        Vector3[] vertices = new Vector3[(nbLong+1) * nbLat + nbLong * 2];
        float _pi = MathUtils.PI;
        float _2pi = MathUtils.PI2;

Calculate the vertex positions; first, the top and bottom ones and then along the latitude/longitude spherical grid:

        //Top and bottom vertices are duplicated
        for(int i = 0; i < nbLong; i++){
            vertices[i] = new Vector3(Vector3.up).multiply(radius);
            vertices[vertices.length - i - 1] = new Vector3(Vector3.up).multiply(-radius);
        }
        for( int lat = 0; lat < nbLat; lat++ )
        {
            float a1 = _pi * (float)(lat+1) / (nbLat+1);
            float sin1 = (float)Math.sin(a1);
            float cos1 = (float)Math.cos(a1);

            for( int lon = 0; lon <= nbLong; lon++ )
            {
                float a2 = _2pi * (float)(lon == nbLong ? 0 : lon) / nbLong;
                float sin2 = (float)Math.sin(a2);
                float cos2 = (float)Math.cos(a2);

                vertices[lon + lat * (nbLong + 1) + nbLong] = 
                    new Vector3( sin1 * cos2, cos1, sin1 * sin2 ).multiply(radius);
            }
        }

Next, we calculate the vertex normals and then the UVs for texture mapping:

        Vector3[] normals = new Vector3[vertices.length];
        for( int n = 0; n < vertices.length; n++ )
            normals[n] = new Vector3(vertices[n]).normalize();

        Vector2[] uvs = new Vector2[vertices.length];
        float uvStart = 1.0f / (nbLong * 2);
        float uvStride = 1.0f / nbLong;
        for(int i = 0; i < nbLong; i++) {
            uvs[i] = new Vector2(uvStart + i * uvStride, 1f);
            uvs[uvs.length - i - 1] = new Vector2(1 - (uvStart + i * uvStride), 0f);
        }
        for( int lat = 0; lat < nbLat; lat++ )
            for( int lon = 0; lon <= nbLong; lon++ )
                uvs[lon + lat * (nbLong + 1) + nbLong] = new Vector2( (float)lon / nbLong, 1f - (float)(lat+1) / (nbLat+1) );

This next part of the same allocateBuffers method generates the triangular indices, which connect the vertices:

        int nbFaces = (nbLong+1) * nbLat + 2;
        int nbTriangles = nbFaces * 2;
        int nbIndexes = nbTriangles * 3;
        numIndices = nbIndexes;
        short[] triangles = new short[ nbIndexes ];

        //Top Cap
        int i = 0;
        for( short lon = 0; lon < nbLong; lon++ )
        {
            triangles[i++] = lon;
            triangles[i++] = (short)(nbLong + lon+1);
            triangles[i++] = (short)(nbLong + lon);
        }

        //Middle
        for( short lat = 0; lat < nbLat - 1; lat++ )
        {
            for( short lon = 0; lon < nbLong; lon++ )
            {
                short current = (short)(lon + lat * (nbLong + 1) + nbLong);
                short next = (short)(current + nbLong + 1);

                triangles[i++] = current;
                triangles[i++] = (short)(current + 1);
                triangles[i++] = (short)(next + 1);

                triangles[i++] = current;
                triangles[i++] = (short)(next + 1);
                triangles[i++] = next;
            }
        }

        //Bottom Cap
        for( short lon = 0; lon < nbLong; lon++ )
        {
            triangles[i++] = (short)(vertices.length - lon - 1);
            triangles[i++] = (short)(vertices.length - nbLong - (lon+1) - 1);
            triangles[i++] = (short)(vertices.length - nbLong - (lon) - 1);
        }

Finally, apply these calculated values to the corresponding vertexBuffer, normalBuffer, texCoordBuffer, and indexBuffer arrays, as follows:

        //convert Vector3[] to float[]
        float[] vertexArray = new float[vertices.length * 3];
        for(i = 0; i < vertices.length; i++){
            int step = i * 3;
            vertexArray[step] = vertices[i].x;
            vertexArray[step + 1] = vertices[i].y;
            vertexArray[step + 2] = vertices[i].z;
        }
        float[] normalArray = new float[normals.length * 3];
        for(i = 0; i < normals.length; i++){
            int step = i * 3;
            normalArray[step] = normals[i].x;
            normalArray[step + 1] = normals[i].y;
            normalArray[step + 2] = normals[i].z;
        }
        float[] texCoordArray = new float[uvs.length * 2];
        for(i = 0; i < uvs.length; i++){
            int step = i * 2;
            texCoordArray[step] = uvs[i].x;
            texCoordArray[step + 1] = uvs[i].y;
        }

        vertexBuffer = allocateFloatBuffer(vertexArray);
        normalBuffer = allocateFloatBuffer(normalArray);
        texCoordBuffer = allocateFloatBuffer(texCoordArray);
        indexBuffer = allocateShortBuffer(triangles);
    }

This is a lot of code, and might be hard to read on the pages of a book; you can find a copy in the project GitHub repository if you prefer.

Conveniently, since the sphere is centered at the origin (0,0,0), the normal vectors at each vertex correspond to the vertex position itself (radiating from the origin to the vertex). Strictly speaking, since we used a radius of 1, we can avoid the normalize() step to generate the array of normals as an optimization. The following image shows the 24 x 16 vertex sphere with its normal vectors:

Creating a Sphere component

Note that our algorithm includes an interesting fix that avoids a single vertex at the poles (where all the UVs converge at a single point and cause some swirling texture artifacts).

We create nLon-1 co-located vertices spread across the UV X, offset by 1/(nLon*2), drawing teeth at the top and bottom. The following image shows the flattened UV sheet for the sphere illustrating the polar teeth:

Creating a Sphere component
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset