Rendering text

As we have just mentioned, to render text, we can't just call a drawText method that will render some text in 3D inside our small 3D scene. Actually, we'd be using drawText, but just to render it on a background Bitmap that would be used as a texture for an additional plane we will be rendering.

In order to do so, we'd have to define the geometry of that plane:

private float planeCoords[] = { 
        -1.f, -1.f, -1.4f, 
        -1.f,  1.f, -1.4f, 
         1.f,  1.f, -1.4f, 
         1.f, -1.f, -1.4f, 
}; 
 
private short[] planeIndex = { 
        0, 1, 2, 
        0, 2, 3 
}; 
 
private float texCoords[] = { 
        1.f, 1.f, 
        1.f, 0.f, 
        0.f, 0.f, 
        0.f, 1.f 
}; 

As the cube front face is at z-coordinate -1.f, this plane will be at -1.4f, so 0.4f in front of it, otherwise it might get occluded by the cube.

We have got to add the vertex and fragment Shader again, to render with a texture. Although we will not replace the current Shader we have got in our code, we will have to live with both sets of Shader:

private final String vertexShaderCodeText = 
        "uniform mat4 uMVPMatrix;" + 
        "attribute vec4 vPosition;" + 
        "attribute vec2 aTex;" + 
        "varying vec2 vTex;" + 
        "void main() {" + 
        "  gl_Position = uMVPMatrix * vPosition;" + 
        "  vTex = aTex;" + 
        "}"; 
 
private final String fragmentShaderCodeText = 
        "precision mediump float;" + 
        "uniform sampler2D sTex;" + 
        "varying vec2 vTex;" + 
        "void main() {" + 
        "  gl_FragColor = texture2D(sTex, vTex);" + 
        "}"; 

Let's also update the initBuffers method to initialize both sets of Buffers:

private void initBuffers() { 
    ByteBuffer vbb = ByteBuffer.allocateDirect(quadCoords.length  
            * (Float.SIZE / 8)); 
    vbb.order(ByteOrder.nativeOrder()); 
 
    vertexBuffer = vbb.asFloatBuffer(); 
    vertexBuffer.put(quadCoords); 
    vertexBuffer.position(0); 
 
    ByteBuffer ibb = ByteBuffer.allocateDirect(index.length 
            * (Short.SIZE / 8)); 
    ibb.order(ByteOrder.nativeOrder()); 
 
    indexBuffer = ibb.asShortBuffer(); 
    indexBuffer.put(index); 
    indexBuffer.position(0); 
 
    ByteBuffer cbb = ByteBuffer.allocateDirect(colors.length 
            * (Float.SIZE / 8)); 
    cbb.order(ByteOrder.nativeOrder()); 
 
    colorBuffer = cbb.asFloatBuffer(); 
    colorBuffer.put(colors); 
    colorBuffer.position(0); 
 
 
    vbb = ByteBuffer.allocateDirect(planeCoords.length 
            * (Float.SIZE / 8)); 
    vbb.order(ByteOrder.nativeOrder()); 
 
    vertexTextBuffer = vbb.asFloatBuffer(); 
    vertexTextBuffer.put(planeCoords); 
    vertexTextBuffer.position(0); 
 
    ibb = ByteBuffer.allocateDirect(planeIndex.length 
            *  (Short.SIZE / 8)); 
    ibb.order(ByteOrder.nativeOrder()); 
 
    indexTextBuffer = ibb.asShortBuffer(); 
    indexTextBuffer.put(planeIndex); 
    indexTextBuffer.position(0); 
 
    ByteBuffer tbb = ByteBuffer.allocateDirect(texCoords.length 
            * (Float.SIZE / 8)); 
    tbb.order(ByteOrder.nativeOrder()); 
 
    texBuffer = tbb.asFloatBuffer(); 
    texBuffer.put(texCoords); 
    texBuffer.position(0); 
}              

As we can see, this method is allocating both sets of Buffers: one set for the cube and another for the plane we will use to draw the text. We have got to do a similar approach for the vertex and fragment Shaders, we have got to load and link both sets of Shaders:

private void initShaders() { 
    int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode); 
    int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, 
fragmentShaderCode); shaderProgram = GLES20.glCreateProgram(); GLES20.glAttachShader(shaderProgram, vertexShader); GLES20.glAttachShader(shaderProgram, fragmentShader); GLES20.glLinkProgram(shaderProgram); vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCodeText); fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER,
fragmentShaderCodeText); shaderTextProgram = GLES20.glCreateProgram(); GLES20.glAttachShader(shaderTextProgram, vertexShader); GLES20.glAttachShader(shaderTextProgram, fragmentShader); GLES20.glLinkProgram(shaderTextProgram); }

We are attaching the shaders we will use to draw the text in a texture to another Shader program we will store in the shaderTextProgram variable. Depending on what we want to render we could now switch from shaderProgram or shaderTextProgram.

Let's now create a method that returns a Bitmap with a text centered on it:

private Bitmap createBitmapFromText(String text) { 
    Bitmap out = Bitmap.createBitmap(512, 512,
Bitmap.Config.ARGB_8888); out.eraseColor(0x00000000); Paint textPaint = new Paint(); textPaint.setAntiAlias(true); textPaint.setColor(0xffffffff); textPaint.setTextSize(60); textPaint.setStrokeWidth(2.f); textPaint.setStyle(Paint.Style.FILL); Rect textBoundaries = new Rect(); textPaint.getTextBounds(text, 0, text.length(), textBoundaries); Canvas canvas = new Canvas(out); for (int i = 0; i < 2; i++) { canvas.drawText(text, (canvas.getWidth() - textBoundaries.width()) / 2.f, (canvas.getHeight() - textBoundaries.height()) / 2.f +
textBoundaries.height(), textPaint); textPaint.setColor(0xff000000); textPaint.setStyle(Paint.Style.STROKE); } return out; }

This method creates a Bitmap of 512 by 512 with eight bits per color component and four components: alpha, or transparency, red, green, and blue. Then, it is creating a Paint object with the color and size of the text, getting the text boundaries in order to center it on the Bitmap and drawing the text twice on the Canvas object we can get from the Bitmap. Text is drawn twice, because it first draws the text with a solid white color and then, as we change the Paint object style to STROKE, it's draws the silhouette using a black color.

The code we had in previous examples to load a texture was loading it from a local resource. As it was converting it into an unscaled Bitmap, we could reuse most of that code to load our generated Bitmap. Let's recover the loadTexture() method we already had, but let's change it to use a helper method to upload a Bitmap into a Texture:

private int loadTexture(int resId) { 
    final int[] textureIds = new int[1]; 
    GLES20.glGenTextures(1, textureIds, 0); 
 
    if (textureIds[0] == 0) return -1; 
 
    // do not scale the bitmap depending on screen density 
    final BitmapFactory.Options options = new BitmapFactory.Options(); 
    options.inScaled = false; 
 
    final Bitmap textureBitmap =
BitmapFactory.decodeResource(getResources(),
resId, options); attachBitmapToTexture(textureIds[0], textureBitmap); return textureIds[0]; }

The implementation of the helper method is as follows:

private void attachBitmapToTexture(int textureId, Bitmap textureBitmap) { 
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId); 
 
    GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, 
            GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); 
 
    GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, 
            GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); 
 
    GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, 
            GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); 
 
    GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, 
            GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); 
 
    GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, textureBitmap, 0); 
} 

We have only got to create a method that puts everything together: that is, one that generates a Bitmap from a text, generates a textureIds, uploads the Bitmap as a texture, and recycles the Bitmap:

private int generateTextureFromText(String text) { 
    final int[] textureIds = new int[1]; 
    GLES20.glGenTextures(1, textureIds, 0); 
 
    Bitmap textureBitmap = createBitmapFromText(text); 
    attachBitmapToTexture(textureIds[0], textureBitmap); 
    textureBitmap.recycle(); 
    return textureIds[0]; 
} 

Using this method, we can now generate a different texture for each face of the cube:

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

We can now add at the bottom of the onDraw() method some additional code to render a plane in front of each face of the cube:

GLES20.glUseProgram(shaderTextProgram); 
positionHandle = GLES20.glGetAttribLocation(shaderTextProgram, "vPosition"); 
 
GLES20.glVertexAttribPointer(positionHandle, 3, 
        GLES20.GL_FLOAT, false, 
        0, vertexTextBuffer); 
 
int texCoordHandle = GLES20.glGetAttribLocation(shaderTextProgram, "aTex"); 
GLES20.glVertexAttribPointer(texCoordHandle, 2, 
        GLES20.GL_FLOAT, false, 
        0, texBuffer); 
 
int texHandle = GLES20.glGetUniformLocation(shaderTextProgram, "sTex"); 
GLES20.glActiveTexture(GLES20.GL_TEXTURE0); 
GLES20.glEnable(GLES20.GL_BLEND); 
GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA); 
 
for (int i = 0; i < 4; i++) { 
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId[i]); 
    GLES20.glUniform1i(texHandle, 0); 
 
    mMVPMatrixHandle = GLES20.glGetUniformLocation(shaderTextProgram,
"uMVPMatrix"); GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mMVPMatrix,
0); GLES20.glEnableVertexAttribArray(texHandle); GLES20.glEnableVertexAttribArray(positionHandle); GLES20.glDrawElements( GLES20.GL_TRIANGLES, planeIndex.length, GLES20.GL_UNSIGNED_SHORT, indexTextBuffer); GLES20.glDisableVertexAttribArray(positionHandle); GLES20.glDisableVertexAttribArray(texHandle); Matrix.rotateM(mMVPMatrix, 0, -90.f, 0.f, 1.f, 0.f); } GLES20.glDisable(GLES20.GL_BLEND); GLES20.glDisable(GLES20.GL_DEPTH_TEST);

As we can see, we are changing the positionHandle to the plane geometry, enabling the texture vertex array and, in addition, we are enabling the blending mode. As the text texture will be transparent with the exception of the text, we need to enable blending or otherwise, OpenGL ES will render the transparent pixels as black.

To draw different planes, one for each horizontal face of the cube, we are doing a small loop where we bind a different texture and rotate by 90 degrees on each iteration.

If we run this example, we will see something similar to the following screenshot: