7-Jul-09 (Created: 7-Jul-09) | More in 'OpenGL'

How to draw a regular polygon using Android OpenGL

Introduction

A number of beginer OpenGL examples show how to draw figures such as triangles and squares by specifying their explicit vertex coordinates using OpenGL Apis. When I really thought through this, a triangle and a square are a special case of a regular polygon.

A regular polygon according to wikipedia "is a polygon which is equiangular (all angles are equal in measure) and equilateral (all sides have the same length). Regular polygons may be convex or star".

So It is not a stretch, instead, to imagine specifying these regular polygons in terms of the number of sides, radius and an origin coordinate instead the "n" number of vertices.

The exercise here abstracts the OpenGL drawing of this RegularPolygon and in the process demonstrates how a) one can derive the vertices and drawing indecies based on the abstract definition of a figure b) Uses animation to start with a triangle and then end up with an approximated circle.

This example demonstrate the following aspects of Android OpenGL and also Java

  • Working with Arrays in Java (initializing, populating, and using)
  • Working with native buffers through java.nio package
  • Using animation to change the scene
  • glVertexPointer
  • glDrawElements

Design Approach

Start by defining a regular polygon with the help of a radius, number of sides and its origin. Then calculate the vertices for each side of the polygon. Use GL_TRIANGLES to draw multiple triangles to make up the polygon. Then have the RegularPolygon provide these vertices and necessary indecies as an array to the drawing class.

Assuming that this RegularPolygon is already designed here is how I have used it in my code


      int sides = 4;
       RegularPolygon p = new RegularPolygon(0,0,0, // x,y,z of the origin
                     1,  // Length of the radius
                     sides); // how many sides
                     
      //Allocate and fill the vertex buffer
       this.mFVertexBuffer = p.getVertexBuffer();
      
      //Allocate and fill the index buffer
       this.mIndexBuffer = p.getIndexBuffer();
       this.numOfIndecies = p.getNumberOfIndecies();
      
      //set the buffers to their begining
        this.mFVertexBuffer.position(0);
        this.mIndexBuffer.position(0);
 
       //set the color
       gl.glColor4f(1.0f, 0, 0, 0.5f);
      
      //set the vertex buffer
       gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mFVertexBuffer);
      
      //draw
       gl.glDrawElements(GL10.GL_TRIANGLES, this.numOfIndecies,
                GL10.GL_UNSIGNED_SHORT, mIndexBuffer);

Sample source code files in this example

The following files illustrate the design of this example

RegularPolygon.java: Implements the abstract idea of a regular polygon. Provides the ability to generate vertices and indecies to draw the figure.

AbstractRenderer.java: Derives from the Android OpenGL Renderer and acts as a base class for the PolygonRenderer that actually uses and draws the RegularPolygon object. This abstract class leaves the "draw" implementation undefined leaving to a derived class.

PolygonRenderer.java: Implementes the "draw" method where the actual drawing takes place.

OpenGLTestHarnessActivity: A simple activity class to instantiate the PolygonRenderer and facilitate drawing on a SurfaceView.

Source code for each of these files is presented below.

Code for RegularPolygon


package com.ai.android.OpenGL.SDK15;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.ShortBuffer;

import android.util.Log;

public class RegularPolygon 
{
   private float cx, cy, cz, r;
   private int sides;
   private float[] xarray = null;
   private float[] yarray = null;
   
   public RegularPolygon(float incx, float incy, float incz, // (x,y,z) center 
         float inr, // radius 
         int insides) // number of sides
   {
      cx = incx;
      cy = incy;
      cz = incz;
      r = inr;
      sides = insides;
      
      xarray = new float[sides];
      yarray = new float[sides];
      calcArrays();
   }
   private void calcArrays()
   {
      float[] xmarray = this.getXMultiplierArray();
      float[] ymarray = this.getYMultiplierArray();
      
      //calc xarray
      for(int i=0;i<sides;i++)
      {
         float curm = xmarray[i];
         float xcoord = cx + r * curm;
         xarray[i] = xcoord;
      }
      this.printArray(xarray, "xarray");
      
      //calc yarray
      for(int i=0;i<sides;i++)
      {
         float curm = ymarray[i];
         float ycoord = cy + r * curm;
         yarray[i] = ycoord;
      }
      this.printArray(yarray, "yarray");
      
   }
   public FloatBuffer getVertexBuffer()
   {
       int vertices = sides + 1;
       int coordinates = 3;
       int floatsize = 4;
       int spacePerVertex = coordinates * floatsize;
       
        ByteBuffer vbb = ByteBuffer.allocateDirect(spacePerVertex * vertices);
        vbb.order(ByteOrder.nativeOrder());
        FloatBuffer mFVertexBuffer = vbb.asFloatBuffer();
        
        //Put the first coordinate (x,y,z:0,0,0)
        mFVertexBuffer.put(0.0f); //x
        mFVertexBuffer.put(0.0f); //y
        mFVertexBuffer.put(0.0f); //z
        
        int totalPuts = 3;
        for (int i=0;i<sides;i++)
        {
            mFVertexBuffer.put(xarray[i]); //x
            mFVertexBuffer.put(yarray[i]); //y
            mFVertexBuffer.put(0.0f); //z
            totalPuts += 3;
        }
        Log.d("total puts:",Integer.toString(totalPuts));
      return mFVertexBuffer;
   }
   public ShortBuffer getIndexBuffer()
   {
      short[] iarray = new short[sides * 3];
        ByteBuffer ibb = ByteBuffer.allocateDirect(sides * 3 * 2);
        ibb.order(ByteOrder.nativeOrder());
        ShortBuffer mIndexBuffer = ibb.asShortBuffer();
        for (int i=0;i<sides;i++)
        {
           short index1 = 0;
           short index2 = (short)(i+1);
           short index3 = (short)(i+2);
           if (index3 == sides+1)
           {
              index3 = 1;
           }
           mIndexBuffer.put(index1);
           mIndexBuffer.put(index2);
           mIndexBuffer.put(index3);
           
           iarray[i*3 + 0]=index1;
           iarray[i*3 + 1]=index2;
           iarray[i*3 + 2]=index3;
        }
        this.printShortArray(iarray, "index array"); 
        return mIndexBuffer;
   }
   private float[] getXMultiplierArray()
   {
      float[] angleArray = getAngleArrays();
      float[] xmultiplierArray = new float[sides];
      for(int i=0;i<angleArray.length;i++)
      {
         float curAngle = angleArray[i];
         float sinvalue = (float)Math.cos(Math.toRadians(curAngle));
         float absSinValue = Math.abs(sinvalue);
         if (isXPositiveQuadrant(curAngle))
         {
            sinvalue = absSinValue;
         }
         else
         {
            sinvalue = -absSinValue;
         }
         xmultiplierArray[i] = this.getApproxValue(sinvalue);
      }
      this.printArray(xmultiplierArray, "xmultiplierArray");
      return xmultiplierArray;
   }
   
   private float[] getYMultiplierArray()
   {
      float[] angleArray = getAngleArrays();
      float[] ymultiplierArray = new float[sides];
      for(int i=0;i<angleArray.length;i++)
      {
         float curAngle = angleArray[i];
         float sinvalue = (float)Math.sin(Math.toRadians(curAngle));
         float absSinValue = Math.abs(sinvalue);
         if (isYPositiveQuadrant(curAngle))
         {
            sinvalue = absSinValue;
         }
         else
         {
            sinvalue = -absSinValue;
         }
         ymultiplierArray[i] = this.getApproxValue(sinvalue);
      }
      this.printArray(ymultiplierArray, "ymultiplierArray");
      return ymultiplierArray;
   }
   
   private boolean isXPositiveQuadrant(float angle)
   {
      if ((0 <= angle) && (angle <= 90))
      {
         return true;
      }
      
      if ((angle < 0) && (angle >= -90))
      {
         return true;
      }
      return false;
   }
   
   private boolean isYPositiveQuadrant(float angle)
   {
      if ((0 <= angle) && (angle <= 90))
      {
         return true;
      }
      
      if ((angle < 180) && (angle >= 90))
      {
         return true;
      }
      return false;
   }
   private float[] getAngleArrays()
   {
      float[] angleArray = new float[sides];
      float commonAngle = 360.0f/sides;
      float halfAngle = commonAngle/2.0f;
      float firstAngle = 360.0f - (90+halfAngle);
      angleArray[0] = firstAngle;
      
      float curAngle = firstAngle;
      for(int i=1;i<sides;i++)
      {
         float newAngle = curAngle - commonAngle;
         angleArray[i] = newAngle;
         curAngle = newAngle;
      }
      printArray(angleArray, "angleArray");
      return angleArray;
   }
   private float getApproxValue(float f)
   {
      if (Math.abs(f) < 0.001)
      {
         return 0;
      }
      return f;
   }
   public int getNumberOfIndecies()
   {
      return sides * 3;
   }
   public static void test()
   {
      RegularPolygon triangle = new RegularPolygon(0,0,0,1,3);
   }
   private void printArray(float array[], String tag)
   {
      StringBuilder sb = new StringBuilder(tag);
      for(int i=0;i<array.length;i++)
      {
         sb.append(";").append(array[i]);
      }
      Log.d("hh",sb.toString());
   }
   private void printShortArray(short array[], String tag)
   {
      StringBuilder sb = new StringBuilder(tag);
      for(int i=0;i<array.length;i++)
      {
         sb.append(";").append(array[i]);
      }
      Log.d(tag,sb.toString());
   }
}

Code for Abstract Renderer


package com.ai.android.OpenGL.SDK15;

import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

import android.opengl.GLU;
import android.opengl.GLSurfaceView.Renderer;

//filename: AbstractRenderer.java
public abstract class AbstractRenderer implements Renderer
{
    public int[] getConfigSpec() {
        int[] configSpec = {
                EGL10.EGL_DEPTH_SIZE, 0,
                EGL10.EGL_NONE
        };
        return configSpec;
    }

    public void onSurfaceCreated(GL10 gl, EGLConfig eglConfig) {
        gl.glDisable(GL10.GL_DITHER);
        gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT,
                GL10.GL_FASTEST);
        gl.glClearColor(.5f, .5f, .5f, 1);
        gl.glShadeModel(GL10.GL_SMOOTH);
        gl.glEnable(GL10.GL_DEPTH_TEST);
    }
    
    public void onSurfaceChanged(GL10 gl, int w, int h) {
        gl.glViewport(0, 0, w, h);
        float ratio = (float) w / h;
        gl.glMatrixMode(GL10.GL_PROJECTION);
        gl.glLoadIdentity();
        gl.glFrustumf(-ratio, ratio, -1, 1, 3, 7);
    }
    
    public void onDrawFrame(GL10 gl) 
    {
        gl.glDisable(GL10.GL_DITHER);
        gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
        gl.glMatrixMode(GL10.GL_MODELVIEW);
        gl.glLoadIdentity();
        GLU.gluLookAt(gl, 0, 0, -5, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
        gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
        draw(gl);
    }
    protected abstract void draw(GL10 gl);
}

Code for PolygonRenderer


package com.ai.android.OpenGL.SDK15;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.ShortBuffer;

import javax.microedition.khronos.opengles.GL10;

import android.content.Context;
import android.os.SystemClock;

//filename: SimpleTriangleRenderer.java
public class PolygonRenderer extends AbstractRenderer
{
   //Number of points or vertices we want to use
    private final static int VERTS = 4;
    
    //A raw native buffer to hold the point coordinates
    private FloatBuffer mFVertexBuffer;
    
    //A raw native buffer to hold indices
    //allowing a reuse of points.
    private ShortBuffer mIndexBuffer;
    
    private int numOfIndecies = 0;
    
    private long prevtime = SystemClock.uptimeMillis();
    
    private int sides = 3;
    
    public PolygonRenderer(Context context) 
    {
       //EvenPolygon t = new EvenPolygon(0,0,0,1,3);
       //EvenPolygon t = new EvenPolygon(0,0,0,1,4);
       prepareBuffers(sides);
    }
    
    private void prepareBuffers(int sides)
    {
       RegularPolygon t = new RegularPolygon(0,0,0,1,sides);
       this.mFVertexBuffer = t.getVertexBuffer();
       this.mIndexBuffer = t.getIndexBuffer();
       this.numOfIndecies = t.getNumberOfIndecies();
        this.mFVertexBuffer.position(0);
        this.mIndexBuffer.position(0);
    }

   //overriden method
    protected void draw(GL10 gl)
    {
        long curtime = SystemClock.uptimeMillis();
        if ((curtime - prevtime) > 2000)
        {
           prevtime = curtime;
           sides += 1;
           if (sides > 20)
           {
              sides = 3;
           }
            this.prepareBuffers(sides);
        }
       //EvenPolygon.test();
       gl.glColor4f(1.0f, 0, 0, 0.5f);
       gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mFVertexBuffer);
       gl.glDrawElements(GL10.GL_TRIANGLES, this.numOfIndecies,
                GL10.GL_UNSIGNED_SHORT, mIndexBuffer);
    }
}

Code for a sample activity that uses the polygon renderer


package com.ai.android.OpenGL.SDK15;

import android.app.Activity;
import android.opengl.GLSurfaceView;
import android.os.Bundle;

//filename: OpenGLTestHarnessActivity.java
public class OpenGL15TestHarnessActivity extends Activity {
   private GLSurfaceView mTestHarness;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mTestHarness = new GLSurfaceView(this);
        mTestHarness.setEGLConfigChooser(false);
//        mTestHarness.setRenderer(new SimpleRectRenderer(this));
//        mTestHarness.setRenderer(new SimpleTriangleRenderer(this));
      mTestHarness.setRenderer(new PolygonRenderer(this));
        mTestHarness.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
        setContentView(mTestHarness);
    }
    @Override
    protected void onResume()    {
        super.onResume();
        mTestHarness.onResume();
    }
    @Override
    protected void onPause() {
        super.onPause();
        mTestHarness.onPause();
    }
}

How to run this code

You will need some background on how OpenGL works in Android. If you are familiar with basics you can try to take these 4 files and build your application to run on the emulator. If not (I apologize as a plug is forthcoming) you can read chapter 10 and chapter 13 of the Pro Android from Apress to get the necessary background.

I will also try to post the zipped up project soon here and I will provide a link to the download.