Multiple faces

Now that we have added the ability to render some text on top of the faces of the cube, we can know what we are selecting when clicking an option, but we are still limited to four different options. Currently, we have got the geometry hardcoded in the code in several arrays. If we want to make the number of options, or number or faces, dynamic we'd have to generate, programmatically, both the geometry and the face indexes.

Luckily for us, our starting point is to have several choices in a 3D circle, so we only have to generate a hollow cylinder with several faces, exactly as much as the number of options we'd like to have.

Let's add a method to the GLDrawer custom view class, allowing us to set the number of options and faces that we will have:

public void setNumOptions(int options) { 
    double halfAngle = Math.PI / options; 
    float[] coords = new float[options * 3 * 4]; 
    int offset = 0; 
    for (int i = 0; i < options; i++) { 
        float angle = (float) (i * 2.f * Math.PI / options 
                - Math.PI / 2.f - halfAngle); 
 
        float nextAngle = (float) ((i + 1) * 2.f * Math.PI / options 
                - Math.PI / 2.f - halfAngle); 
 
        float x0 = (float) Math.cos(angle) * 1.2f; 
        float x1 = (float) Math.cos(nextAngle) * 1.2f; 
        float z0 = (float) Math.sin(angle) * 1.2f; 
        float z1 = (float) Math.sin(nextAngle) * 1.2f; 
 
        coords[offset++] = x0; 
        coords[offset++] = -1.f; 
        coords[offset++] = z0; 
 
        coords[offset++] = x1; 
        coords[offset++] = -1.f; 
        coords[offset++] = z1; 
 
        coords[offset++] = x0; 
        coords[offset++] = 1.f; 
        coords[offset++] = z0; 
 
        coords[offset++] = x1; 
        coords[offset++] = 1.f; 
        coords[offset++] = z1; 
    } 
 
    short[] index = new short[options * 6]; 
    for (int i = 0; i < options; i++) { 
        index[i * 6 + 0] = (short) (i * 4 + 0); 
        index[i * 6 + 1] = (short) (i * 4 + 1); 
        index[i * 6 + 2] = (short) (i * 4 + 3); 
 
        index[i * 6 + 3] = (short) (i * 4 + 0); 
        index[i * 6 + 4] = (short) (i * 4 + 2); 
        index[i * 6 + 5] = (short) (i * 4 + 3); 
    } 
 
    glRenderer.setCoordinates(options, coords, index); 
} 

To generate diverse faces in a form of a cylinder is as easy as dividing the 360 degrees, or two times PI in radians, of a circle in the amount of faces we'd like to have. Here, we are dividing 2.f*Math.PI by the number of options and then multiplying it by the loop iterator. By calculating the sine and the cosine of that angle we can get two coordinates, usually x and y in a 2D projection, but in our specific case, we'd map it to x and z as we are setting the y coordinate to -1.f as the top vertical edge and 1.f as the bottom vertical edge. We are also calculating the next x and z coordinates, so we can create a face quad between these points.

We're generating four points for each face and we're indexing them as two triangles in the index array. This matches perfectly with the way we were generating colors before, as we're generating four color values for each face and now we are also generating exactly four vertices per face, each face will have a unique solid color.

At the end of the method, we're calling the setCoordinates() method of the GLRenderer, but that is very simple to implement:

private void setCoordinates(int options, float[] coords, short[] index) { 
    this.quadCoords = coords; 
    this.index = index; 
    this.options = options; 
} 

This will work without touching anything else, as long as we call it before the surface is created. As we're talking about it, we have got to update the onSurfaceCreated() method to use the number of options we've set instead of the default four we had hardcoded in the code before:

@Override 
public void onSurfaceCreated(GL10 unused, EGLConfig config) { 
    initBuffers(); 
    initShaders(); 
 
    textureId = new int[options]; 
    for (int i = 0; i < textureId.length; i++) { 
        textureId[i] = generateTextureFromText("Option " + (i + 1)); 
    } 
 
    faceAngle = 360.f / options; 
} 

We're also calculating the amount we've got to rotate to switch from one face to another. In our previous case it was easy, as there were four faces, 360 degrees divided by 4 is 90. Now, the calculation is still straightforward, but we've got to change the hardcoded 90 we had in the code by this new variable we've created, named faceAngle, the value of which is 360 divided by the number of options.

Let's test this new feature by calling it on the MainActivity, just after setting the different colors:

@Override 
protected void onCreate(Bundle savedInstanceState) { 
    super.onCreate(savedInstanceState); 
 
    setContentView(R.layout.activity_main); 
 
    GLDrawer glDrawer = (GLDrawer) findViewById(R.id.gldrawer); 
    glDrawer.setOnMenuClickedListener(new
GLDrawer.OnMenuClickedListener() { @Override public void menuClicked(int option) { Log.i("Example37-Menu3D", "option clicked " + option); } }); glDrawer.setColors(new int[] { 0xff4a90e2, 0xff161616, 0xff594236, 0xffff5964, 0xff8aea92, 0xffffe74c }); glDrawer.setNumOptions(6); }

We've not specifically added a check, but the number of colors must be at least the same number of options or otherwise we will get an exception when rendering.

If we run this example, we will see something similar to the following screenshot, depending always on the current rotation:

Check out the full source code of this example in the Example37-Menu3D folder in the GitHub repository.