Wednesday, November 4, 2020

Android Programming 3D Graphics with OpenGL ES (Including Nehe's Port)

Android 3D with OpenGL ES with Nehe's Port
TABLE OF CONTENTS (HIDE)

Android Programming

3D Graphics with OpenGL ES
(Including Nehe's Port)

Introduction

Read

  1. Android Training "Displaying Graphics with OpenGL ES" @ http://developer.android.com/training/graphics/opengl/index.html.
  2. Android API Guides "OpenGL ES" @ http://developer.android.com/guide/topics/graphics/opengl.html.
  3. Android Reference "Package android.opengl" @ http://developer.android.com/reference/android/opengl/package-summary.html.

Getting Started with 3D Graphics on Android

OpenGL ES

Android supports OpenGL ES in packages android.opengl, javax.microedition.khronos.opengles and javax.microedition.khronos.egl.

GLSurfaceView

For 3D graphics programming, you need to program you own custom view, instead using XML-layout. Fortunately, a 3D OpenGL ES view called GLSurfaceView is provided, which greatly simplifies our tasks.

I shall use the Nehe's Lessons (http://nehe.gamedev.net) to illustrate Android 3D programming

Example 1: Setting up OpenGL ES using GLSurfaceView (Nehe Lesson 1: Setting Up)

Create an android application called "Nehe 01", with project name "Nehe01", package name "com.test". Create a blank activity called "MyGLActivity".

The following program sets up the GLSurfaceView, and show a blank (dark green) screen.

MyGLActivity.java
package com.test;
   
import android.app.Activity;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
/**
 * Our OpenGL program's main activity
 */
public class MyGLActivity extends Activity {
   
   private GLSurfaceView glView;   // Use GLSurfaceView
  
   // Call back when the activity is started, to initialize the view
   @Override
   protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      glView = new GLSurfaceView(this);           // Allocate a GLSurfaceView
      glView.setRenderer(new MyGLRenderer(this)); // Use a custom renderer
      this.setContentView(glView);                // This activity sets to GLSurfaceView
   }
   
   // Call back when the activity is going into the background
   @Override
   protected void onPause() {
      super.onPause();
      glView.onPause();
   }
   
   // Call back after onPause()
   @Override
   protected void onResume() {
      super.onResume();
      glView.onResume();
   }
}
Dissecting MyActivity.java

We define MyActivity by extending Activity, so as to override onCreate(), onPause() and onResume(). We then override onCreate() to allocate a GLSurfaceView, set the view's renderer to a custom renderer (to be defined below), and set this activity to use the view.

MyGLRenderer.java

This is our custom OpenGL renderer class.

package com.test;
  
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.opengl.GLU;
/**
 *  OpenGL Custom renderer used with GLSurfaceView 
 */
public class MyGLRenderer implements GLSurfaceView.Renderer {
   Context context;   // Application's context
   
   // Constructor with global application context
   public MyGLRenderer(Context context) {
      this.context = context;
   }
   
   // Call back when the surface is first created or re-created
   @Override
   public void onSurfaceCreated(GL10 gl, EGLConfig config) {
      gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);  // Set color's clear-value to black
      gl.glClearDepthf(1.0f);            // Set depth's clear-value to farthest
      gl.glEnable(GL10.GL_DEPTH_TEST);   // Enables depth-buffer for hidden surface removal
      gl.glDepthFunc(GL10.GL_LEQUAL);    // The type of depth testing to do
      gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST);  // nice perspective view
      gl.glShadeModel(GL10.GL_SMOOTH);   // Enable smooth shading of color
      gl.glDisable(GL10.GL_DITHER);      // Disable dithering for better performance
  
      // You OpenGL|ES initialization code here
      // ......
   }
   
   // Call back after onSurfaceCreated() or whenever the window's size changes
   @Override
   public void onSurfaceChanged(GL10 gl, int width, int height) {
      if (height == 0) height = 1;   // To prevent divide by zero
      float aspect = (float)width / height;
   
      // Set the viewport (display area) to cover the entire window
      gl.glViewport(0, 0, width, height);
  
      // Setup perspective projection, with aspect ratio matches viewport
      gl.glMatrixMode(GL10.GL_PROJECTION); // Select projection matrix
      gl.glLoadIdentity();                 // Reset projection matrix
      // Use perspective projection
      GLU.gluPerspective(gl, 45, aspect, 0.1f, 100.f);
  
      gl.glMatrixMode(GL10.GL_MODELVIEW);  // Select model-view matrix
      gl.glLoadIdentity();                 // Reset
  
      // You OpenGL|ES display re-sizing code here
      // ......
   }
   
   // Call back to draw the current frame.
   @Override
   public void onDrawFrame(GL10 gl) {
      // Clear color and depth buffers using clear-value set earlier
      gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
     
      // You OpenGL|ES rendering code here
      // ......
   }
}
Dissecting MyGLRenderer.java

Our custom rendering class implements interface GLSurfaceView.Renderer, which is responsible to make the OpenGL calls to render a frame. It declare 3 methods to be called back by the Android graphics sub-system upon specific GL events.

  1. onSurfaceCreated(GL10 gl, EGLConfig config): Called when the surface is first created or recreated. It can be used to perform one-time initialization tasks such as setting the clear-value for color and depth, enabling depth-test, etc.
  2. onSurfaceChanged(GL10 gl, int width, int height): Called when the surface is first displayed and after window's size changes. It is used to set the view port and projection mode.
    In our OpenGL renderer, we set the Android's view port (display area) to cover the entire screen from (0,0) to (width-1, height-1):
    gl.glViewport(0, 0, width, height);
    We also choose the perspective projection and set the projection volume, with aspect ratio matches the view port, as follows:
    // OpenGL uses two transformation matrices: projection matrix and model-view matrix
    // We select the projection matrix to setup the projection
    gl.glMatrixMode(GL10.GL_PROJECTION); // Select projection matrix
    gl.glLoadIdentity();                 // Reset projection matrix
    // Use perspective projection with the projection volume defined by
    //   fovy, aspect-ration, z-near and z-far
    GLU.gluPerspective(gl, 45, aspect, 0.1f, 100.f);
      
    // Select the model-view matrix to manipulate objects (Deselect the projection matrix)
    gl.glMatrixMode(GL10.GL_MODELVIEW);  // Select model-view matrix
    gl.glLoadIdentity();                 // Reset
  3. onDrawFrame(GL10 gl): Called to draw the current frame. You OpenGL rendering codes here.
    In our OpenGL renderer, We clear the color and depth buffers (using the clear-values set via glClear* earlier).
    gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);

In the Activity class, we construct a custom renderer, and use setRenderer() to set it for the view:

glView = new GLSurfaceView(this);       // Allocate a GLSurfaceView
glView.setRenderer(new MyGLRenderer()); // Set the renderer for the view

Run the program. You shall see a blank screen.

Example 2: Drawing 2D Shapes (Nehe Lesson 2: Your First Polygon)

Let us get started by drawing 2D polygons as illustrated (Push Ctrl-F11 to switch the emulator in landscape orientation):

Create an android application called "Nehe 02", with project name "Nehe02", package name "com.test". Create a blank activity called "MyGLActivity".

Triangle.java

We first define a class called Triangle.

package com.test;
  
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.opengles.GL10;
  
/*
 * A triangle with 3 vertices.
 */
public class Triangle {
   private FloatBuffer vertexBuffer;  // Buffer for vertex-array
   private ByteBuffer indexBuffer;    // Buffer for index-array
  
   private float[] vertices = {  // Vertices of the triangle
       0.0f,  1.0f, 0.0f, // 0. top
      -1.0f, -1.0f, 0.0f, // 1. left-bottom
       1.0f, -1.0f, 0.0f  // 2. right-bottom
   };
   private byte[] indices = { 0, 1, 2 }; // Indices to above vertices (in CCW)
 
   // Constructor - Setup the data-array buffers
   public Triangle() {
      // Setup vertex-array buffer. Vertices in float. A float has 4 bytes.
      ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
      vbb.order(ByteOrder.nativeOrder()); // Use native byte order
      vertexBuffer = vbb.asFloatBuffer(); // Convert byte buffer to float
      vertexBuffer.put(vertices);         // Copy data into buffer
      vertexBuffer.position(0);           // Rewind
    
      // Setup index-array buffer. Indices in byte.
      indexBuffer = ByteBuffer.allocateDirect(indices.length);
      indexBuffer.put(indices);
      indexBuffer.position(0);
   }
  
   // Render this shape
   public void draw(GL10 gl) {
      // Enable vertex-array and define the buffers
      gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
      
      // Draw the primitives via index-array
      gl.glDrawElements(GL10.GL_TRIANGLES, indices.length, GL10.GL_UNSIGNED_BYTE, indexBuffer);
      gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
   }
}
Dissecting Triangle.java

In OpenGL ES, you cannot define individual vertex via glVertex command (this command is not supported in ES due to inefficiency). Instead, you have to use a vertex array to define a group of vertices. This is done in two steps:

  1. We first define the (x, y, z) location of the vertices in a Java array:
    private float[] vertices = {  // Vertices of the triangle
        0.0f,  1.0f, 0.0f, // 0. top
       -1.0f, -1.0f, 0.0f, // 1. left-bottom
        1.0f, -1.0f, 0.0f  // 2. right-bottom
    };
  2. We then allocate the vertex-array buffer, and transfer the data into the buffer. We use nio's buffer because they are placed on the native heap and are not garbage-collected.
    private FloatBuffer vertexBuffer;
    ......
    // Allocate a raw byte buffer. A float has 4 bytes
    ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
    // Need to use native byte order
    vbb.order(ByteOrder.nativeOrder());
    // Convert the byte buffer into float buffer
    vertexBuffer = vbb.asFloatBuffer();
    // Transfer the data into the buffer
    vertexBuffer.put(vertices);
    // Rewind
    vertexBuffer.position(0);

To render from the vertex-array, we need to enable client-state vertex-array:

gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);

We can then use method glDrawArrays() to render from the vertex array directly, or glDrawElements() to render via an index array.

In the above example, we set up an index array, which indexes into the vertex array (and its associated color array), as follows. Take note that the vertices are arranged in counter-clockwise (CCW) manner, with normal pointing out of the screen (or positive z-direction).

private ByteBuffer indexBuffer;
......
byte[] indices = { 0, 1, 2 };   // CCW
......
indexBuffer = ByteBuffer.allocateDirect(indices.length);  // Allocate raw byte buffer
indexBuffer.put(indices);   // Transfer data into buffer
indexBuffer.position(0);    // rewind

We render the triangle in the draw() method, with the following steps:

  1. We first enable vertex-array client states.
    gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
    
  2. We then specify the location of the buffers via:
    gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);   
      // gl*Pointer(int size, int type, int stride, Buffer pointer)
      //   size: number of coordinates per vertex (must be 2, 3, or 4).
      //   type: data type of vertex coordinate, GL_BYTE, GL_SHORT, GL_FIXED, or GL_FLOAT
      //   stride: the byte offset between consecutive vertices. 0 for tightly packed.
    
  3. Finally, we render the primitives using glDrawElements(), which uses the index array to reference the vertex and color arrays.
    gl.glDrawElements(GL10.GL_TRIANGLES, numIndices, GL10.GL_UNSIGNED_BYTE, indexBuffer);
      // glDrawElements(int mode, int count, int type, Buffer indices)
      //   mode: GL_POINTS, GL_LINE_STRIP, GL_LINE_LOOP, GL_LINES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, or GL_TRIANGLES
      //   count: the number of elements to be rendered.
      //   type: data-type of indices (must be GL_UNSIGNED_BYTE or GL_UNSIGNED_SHORT).
      //   indices: pointer to the index array. 
    gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
Square.java

Similarly, let's define a quad. Take note that OpenGL ES does not support quad as a primitive. We need to draw two triangles instead. We shall use the same color for all the vertices of the quad. The color can be set via glColor.

package com.test;
  
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.opengles.GL10;
/*
 * A square drawn in 2 triangles (using TRIANGLE_STRIP).
 */
public class Square {
   private FloatBuffer vertexBuffer;  // Buffer for vertex-array
  
   private float[] vertices = {  // Vertices for the square
      -1.0f, -1.0f,  0.0f,  // 0. left-bottom
       1.0f, -1.0f,  0.0f,  // 1. right-bottom
      -1.0f,  1.0f,  0.0f,  // 2. left-top
       1.0f,  1.0f,  0.0f   // 3. right-top
   };
  
   // Constructor - Setup the vertex buffer
   public Square() {
      // Setup vertex array buffer. Vertices in float. A float has 4 bytes
      ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
      vbb.order(ByteOrder.nativeOrder()); // Use native byte order
      vertexBuffer = vbb.asFloatBuffer(); // Convert from byte to float
      vertexBuffer.put(vertices);         // Copy data into buffer
      vertexBuffer.position(0);           // Rewind
   }
  
   // Render the shape
   public void draw(GL10 gl) {
      // Enable vertex-array and define its buffer
      gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
      // Draw the primitives from the vertex-array directly
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, vertices.length / 3);
      gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
   }
}
Dissecting Square.java

OpenGL ES 1.0 does not support quad as primitive. We shall draw a quad using TRIANGLE_STRIP, composing of 2 triangles v0v1v2 and v2v1v3, in counter-clockwise orientation.

For the triangle, we use glDrawElements() which uses an index array to reference the vertex and color array. For the quad, we shall use glDrawArrays() to directly render from the vertex-array, as follows:

gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);           // Enable vertex array
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);  // Set the location of vertex array
gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, numVertices);
  // glDrawArrays(int mode, int first, int count)
  //   mode: GL_POINTS, GL_LINE_STRIP, GL_LINE_LOOP, GL_LINES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, or GL_TRIANGLES
  //   first: the starting index in the enabled arrays.
  //   count: the number of indices to be rendered.
gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
GL Renderer

Now, modify the renderer to draw the triangle and quad.

package com.test;
  
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.opengl.GLU;
  
public class MyGLRenderer implements GLSurfaceView.Renderer {
   
   Triangle triangle;     // ( NEW )
   Square quad;           // ( NEW )
   
   // Constructor
   public MyGLRenderer(Context context) {
      // Set up the data-array buffers for these shapes ( NEW )
      triangle = new Triangle();   // ( NEW )
      quad = new Square();         // ( NEW )
   }

   // Call back when the surface is first created or re-created.
   @Override
   public void onSurfaceCreated(GL10 gl, EGLConfig config) {
      // NO CHANGE - SKIP
      ......
   }
   
   // Call back after onSurfaceCreated() or whenever the window's size changes.
   @Override
   public void onSurfaceChanged(GL10 gl, int width, int height) {
      // NO CHANGE - SKIP
      ......
   }

   // Call back to draw the current frame.
   @Override
   public void onDrawFrame(GL10 gl) {
      // Clear color and depth buffers using clear-values set earlier 
      gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
  
      gl.glLoadIdentity();                 // Reset model-view matrix ( NEW )
      gl.glTranslatef(-1.5f, 0.0f, -6.0f); // Translate left and into the screen ( NEW )
      triangle.draw(gl);                   // Draw triangle ( NEW )
  
      // Translate right, relative to the previous translation ( NEW )
      gl.glTranslatef(3.0f, 0.0f, 0.0f);
      quad.draw(gl);                       // Draw quad ( NEW )
   }
}

We run the shapes' setup codes in the renderer's constructor, as they only have to run once. We invoke the shapes' draw() in renderer's onDrawFrame() which renders the shapes upon each frame refresh.

GL Activity

There is no change to the Activity codes in MyGLActivity.

Example 3: Color (Nehe Lesson 3: Color)

Triangle.java

Modify the triangle.java as follow:

package com.test;
  
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.opengles.GL10;
  
/*
 * A triangle with 3 vertices. Each vertex has its own color.
 */
public class Triangle {
   private FloatBuffer vertexBuffer;  // Buffer for vertex-array
   private FloatBuffer colorBuffer;   // Buffer for color-array (NEW)
   private ByteBuffer indexBuffer;    // Buffer for index-array
  
   private float[] vertices = {  // Vertices of the triangle
       0.0f,  1.0f, 0.0f, // 0. top
      -1.0f, -1.0f, 0.0f, // 1. left-bottom
       1.0f, -1.0f, 0.0f  // 2. right-bottom
   };
   private byte[] indices = { 0, 1, 2 }; // Indices to above vertices (in CCW)
   private float[] colors = { // Colors for the vertices (NEW)
      1.0f, 0.0f, 0.0f, 1.0f, // Red (NEW)
      0.0f, 1.0f, 0.0f, 1.0f, // Green (NEW)
      0.0f, 0.0f, 1.0f, 1.0f  // Blue (NEW)
   };
  
   // Constructor - Setup the data-array buffers
   public Triangle() {
      // Setup vertex-array buffer. Vertices in float. A float has 4 bytes
      ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
      vbb.order(ByteOrder.nativeOrder()); // Use native byte order
      vertexBuffer = vbb.asFloatBuffer(); // Convert byte buffer to float
      vertexBuffer.put(vertices);         // Copy data into buffer
      vertexBuffer.position(0);           // Rewind
   
      // Setup color-array buffer. Colors in float. A float has 4 bytes (NEW)
      ByteBuffer cbb = ByteBuffer.allocateDirect(colors.length * 4);
      cbb.order(ByteOrder.nativeOrder()); // Use native byte order (NEW)
      colorBuffer = cbb.asFloatBuffer();  // Convert byte buffer to float (NEW)
      colorBuffer.put(colors);            // Copy data into buffer (NEW)
      colorBuffer.position(0);            // Rewind (NEW)
    
      // Setup index-array buffer. Indices in byte.
      indexBuffer = ByteBuffer.allocateDirect(indices.length);
      indexBuffer.put(indices);
      indexBuffer.position(0);
   }
  
   // Render this shape
   public void draw(GL10 gl) {
      // Enable arrays and define the buffers
      gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
      gl.glEnableClientState(GL10.GL_COLOR_ARRAY);          // Enable color-array (NEW)
      gl.glColorPointer(4, GL10.GL_FLOAT, 0, colorBuffer);  // Define color-array buffer (NEW)
      
      // Draw the primitives via index-array
      gl.glDrawElements(GL10.GL_TRIANGLES, indices.length, GL10.GL_UNSIGNED_BYTE, indexBuffer);
      gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glDisableClientState(GL10.GL_COLOR_ARRAY);   // Disable color-array (NEW)
   }
}
Dissecting Triangle.java

During rendering, the vertex-array will be rendered together with other attributes (such as color, texture and normal), if these attributes are enabled.

In the above example, we define the colors of the vertices and copy them into a color-array buffer. We enable color-array client-state. The colors will be rendered together with the vertices in glDrawElements().

Square.java

Modify Square.java as follows:

package com.test;
   
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.opengles.GL10;
/*
 * A square drawn in 2 triangles (using TRIANGLE_STRIP). This square has one color.
 */
public class Square {
   private FloatBuffer vertexBuffer;  // Buffer for vertex-array
   private float[] vertices = {  // Vertices for the square
      -1.0f, -1.0f,  0.0f,  // 0. left-bottom
       1.0f, -1.0f,  0.0f,  // 1. right-bottom
      -1.0f,  1.0f,  0.0f,  // 2. left-top
       1.0f,  1.0f,  0.0f   // 3. right-top
   };
  
   // Constructor - Setup the vertex buffer
   public Square() {
      // Setup vertex array buffer. Vertices in float. A float has 4 bytes
      ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
      vbb.order(ByteOrder.nativeOrder()); // Use native byte order
      vertexBuffer = vbb.asFloatBuffer(); // Convert from byte to float
      vertexBuffer.put(vertices);         // Copy data into buffer
      vertexBuffer.position(0);           // Rewind
   }
  
   // Render the shape
   public void draw(GL10 gl) {
      // Enable vertex-array and define its buffer
      gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
      gl.glColor4f(0.5f, 0.5f, 1.0f, 1.0f);      // Set the current color (NEW)
      // Draw the primitives from the vertex array directly
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, vertices.length / 3);
      gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
   }
}
Dissecting Square.java

Our square has one color. That is, all vertices are rendered using the same color. Hence, there is no need to define a color-array. Instead, we added a glColor* command before rendering the square using glDrawArrays().

GL Renderer and GL Activity

No change.

Example 4: Rotation (Nehe Lesson 4: Rotation)

To rotate the shapes created in the previous example, we need to make some minor modifications to our renderer.

package com.test;
  
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.opengl.GLU;
  
public class MyGLRenderer implements GLSurfaceView.Renderer {
   
   private Triangle triangle;
   Square quad;
   
   // Rotational angle and speed (NEW)
   private float angleTriangle = 0.0f; // (NEW)
   private float angleQuad = 0.0f;     // (NEW)
   private float speedTriangle = 0.5f; // (NEW)
   private float speedQuad = -0.4f;    // (NEW)
   
   // Constructor
   public MyGLRenderer(Context context) {
      // Set up the buffers for these shapes
      triangle = new Triangle();
      quad = new Square();
   }
  
   // Call back when the surface is first created or re-created.
   @Override
   public void onSurfaceCreated(GL10 gl, EGLConfig config) {
      // NO CHANGE - SKIP
      ......
   }
   
   // Call back after onSurfaceCreated() or whenever the window's size changes.
   @Override
   public void onSurfaceChanged(GL10 gl, int width, int height) {
      // NO CHANGE - SKIP
      ......
   }
  
   // Call back to draw the current frame.
   @Override
   public void onDrawFrame(GL10 gl) {
      // Clear color and depth buffers using clear-values set earlier
      gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
    
      gl.glLoadIdentity();                 // Reset model-view matrix
      gl.glTranslatef(-1.5f, 0.0f, -6.0f); // Translate left and into the screen
      gl.glRotatef(angleTriangle, 0.0f, 1.0f, 0.0f); // Rotate the triangle about the y-axis (NEW)
      triangle.draw(gl);                   // Draw triangle
   
      gl.glLoadIdentity();                 // Reset the mode-view matrix (NEW)
      gl.glTranslatef(1.5f, 0.0f, -6.0f);  // Translate right and into the screen (NEW)
      gl.glRotatef(angleQuad, 1.0f, 0.0f, 0.0f); // Rotate the square about the x-axis (NEW)
      quad.draw(gl);                       // Draw quad

      // Update the rotational angle after each refresh (NEW)
      angleTriangle += speedTriangle; // (NEW)
      angleQuad += speedQuad;         // (NEW)
   }
}

A glRotate* command was added to rotate the shape, with angle of rotation updated after each refresh.

Example 5: 3D Shapes - Rotating Color Cube and Pyramid (Nehe Lesson 5: 3D Shapes)

Pyramid.java

Set up the color pyramid as follows:

package com.test;
  
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.opengles.GL10;
  
public class Pyramid {
   private FloatBuffer vertexBuffer;  // Buffer for vertex-array
   private FloatBuffer colorBuffer;   // Buffer for color-array
   private ByteBuffer indexBuffer;    // Buffer for index-array
    
   private float[] vertices = { // 5 vertices of the pyramid in (x,y,z)
      -1.0f, -1.0f, -1.0f,  // 0. left-bottom-back
       1.0f, -1.0f, -1.0f,  // 1. right-bottom-back
       1.0f, -1.0f,  1.0f,  // 2. right-bottom-front
      -1.0f, -1.0f,  1.0f,  // 3. left-bottom-front
       0.0f,  1.0f,  0.0f   // 4. top
   };
          
   private float[] colors = {  // Colors of the 5 vertices in RGBA
      0.0f, 0.0f, 1.0f, 1.0f,  // 0. blue
      0.0f, 1.0f, 0.0f, 1.0f,  // 1. green
      0.0f, 0.0f, 1.0f, 1.0f,  // 2. blue
      0.0f, 1.0f, 0.0f, 1.0f,  // 3. green
      1.0f, 0.0f, 0.0f, 1.0f   // 4. red
   };
  
   private byte[] indices = { // Vertex indices of the 4 Triangles
      2, 4, 3,   // front face (CCW)
      1, 4, 2,   // right face
      0, 4, 1,   // back face
      4, 0, 3    // left face
   };
  
   // Constructor - Set up the buffers
   public Pyramid() {
      // Setup vertex-array buffer. Vertices in float. An float has 4 bytes
      ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
      vbb.order(ByteOrder.nativeOrder()); // Use native byte order
      vertexBuffer = vbb.asFloatBuffer(); // Convert from byte to float
      vertexBuffer.put(vertices);         // Copy data into buffer
      vertexBuffer.position(0);           // Rewind
  
      // Setup color-array buffer. Colors in float. An float has 4 bytes
      ByteBuffer cbb = ByteBuffer.allocateDirect(colors.length * 4);
      cbb.order(ByteOrder.nativeOrder());
      colorBuffer = cbb.asFloatBuffer();
      colorBuffer.put(colors);
      colorBuffer.position(0);
  
      // Setup index-array buffer. Indices in byte.
      indexBuffer = ByteBuffer.allocateDirect(indices.length);
      indexBuffer.put(indices);
      indexBuffer.position(0);
   }
  
   // Draw the shape
   public void draw(GL10 gl) {
      gl.glFrontFace(GL10.GL_CCW);  // Front face in counter-clockwise orientation
  
      // Enable arrays and define their buffers
      gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
      gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
      gl.glColorPointer(4, GL10.GL_FLOAT, 0, colorBuffer);
      
      gl.glDrawElements(GL10.GL_TRIANGLES, indices.length, GL10.GL_UNSIGNED_BYTE,
            indexBuffer);
      
      gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
   }
}
Cube.java

Similarly, set up the color cube as follows:

package com.test;
  
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.opengles.GL10;
  
public class Cube {
   private FloatBuffer vertexBuffer;  // Buffer for vertex-array
   private int numFaces = 6;
   
   private float[][] colors = {  // Colors of the 6 faces
      {1.0f, 0.5f, 0.0f, 1.0f},  // 0. orange
      {1.0f, 0.0f, 1.0f, 1.0f},  // 1. violet
      {0.0f, 1.0f, 0.0f, 1.0f},  // 2. green
      {0.0f, 0.0f, 1.0f, 1.0f},  // 3. blue
      {1.0f, 0.0f, 0.0f, 1.0f},  // 4. red
      {1.0f, 1.0f, 0.0f, 1.0f}   // 5. yellow
   };
  
   private float[] vertices = {  // Vertices of the 6 faces
      // FRONT
      -1.0f, -1.0f,  1.0f,  // 0. left-bottom-front
       1.0f, -1.0f,  1.0f,  // 1. right-bottom-front
      -1.0f,  1.0f,  1.0f,  // 2. left-top-front
       1.0f,  1.0f,  1.0f,  // 3. right-top-front
      // BACK
       1.0f, -1.0f, -1.0f,  // 6. right-bottom-back
      -1.0f, -1.0f, -1.0f,  // 4. left-bottom-back
       1.0f,  1.0f, -1.0f,  // 7. right-top-back
      -1.0f,  1.0f, -1.0f,  // 5. left-top-back
      // LEFT
      -1.0f, -1.0f, -1.0f,  // 4. left-bottom-back
      -1.0f, -1.0f,  1.0f,  // 0. left-bottom-front 
      -1.0f,  1.0f, -1.0f,  // 5. left-top-back
      -1.0f,  1.0f,  1.0f,  // 2. left-top-front
      // RIGHT
       1.0f, -1.0f,  1.0f,  // 1. right-bottom-front
       1.0f, -1.0f, -1.0f,  // 6. right-bottom-back
       1.0f,  1.0f,  1.0f,  // 3. right-top-front
       1.0f,  1.0f, -1.0f,  // 7. right-top-back
      // TOP
      -1.0f,  1.0f,  1.0f,  // 2. left-top-front
       1.0f,  1.0f,  1.0f,  // 3. right-top-front
      -1.0f,  1.0f, -1.0f,  // 5. left-top-back
       1.0f,  1.0f, -1.0f,  // 7. right-top-back
      // BOTTOM
      -1.0f, -1.0f, -1.0f,  // 4. left-bottom-back
       1.0f, -1.0f, -1.0f,  // 6. right-bottom-back
      -1.0f, -1.0f,  1.0f,  // 0. left-bottom-front
       1.0f, -1.0f,  1.0f   // 1. right-bottom-front
   };
        
   // Constructor - Set up the buffers
   public Cube() {
      // Setup vertex-array buffer. Vertices in float. An float has 4 bytes
      ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
      vbb.order(ByteOrder.nativeOrder()); // Use native byte order
      vertexBuffer = vbb.asFloatBuffer(); // Convert from byte to float
      vertexBuffer.put(vertices);         // Copy data into buffer
      vertexBuffer.position(0);           // Rewind
   }
  
   // Draw the shape
   public void draw(GL10 gl) {
      gl.glFrontFace(GL10.GL_CCW);    // Front face in counter-clockwise orientation
      gl.glEnable(GL10.GL_CULL_FACE); // Enable cull face
      gl.glCullFace(GL10.GL_BACK);    // Cull the back face (don't display)
  
      gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);

      // Render all the faces
      for (int face = 0; face < numFaces; face++) {
         // Set the color for each of the faces
         gl.glColor4f(colors[face][0], colors[face][1], colors[face][2], colors[face][3]);
         // Draw the primitive from the vertex-array directly
         gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, face*4, 4);
      }
      gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glDisable(GL10.GL_CULL_FACE);
   }
}

The vertices of the color cube is labeled as follows.

The vertices of all the faces are arranged in counter-clockwise orientation with normal pointing outwards in a consistent manner. This enables us to cull the back face with the following codes:

gl.glFrontFace(GL10.GL_CCW);    // Set the front face
gl.glEnable(GL10.GL_CULL_FACE); // Enable cull face
gl.glCullFace(GL10.GL_BACK);    // Cull the back face
GL Renderer

The renderer is as follows:

package com.test;
  
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.opengl.GLU;
  
public class MyGLRenderer implements GLSurfaceView.Renderer {
   
   private Pyramid pyramid;    // (NEW)
   private Cube cube;          // (NEW)
   
   private static float anglePyramid = 0; // Rotational angle in degree for pyramid (NEW)
   private static float angleCube = 0;    // Rotational angle in degree for cube (NEW)
   private static float speedPyramid = 2.0f; // Rotational speed for pyramid (NEW)
   private static float speedCube = -1.5f;   // Rotational speed for cube (NEW)
   
   // Constructor
   public MyGLRenderer(Context context) {
      // Set up the buffers for these shapes
      pyramid = new Pyramid();   // (NEW)
      cube = new Cube();         // (NEW)
   }
  
   // Call back when the surface is first created or re-created.
   @Override
   public void onSurfaceCreated(GL10 gl, EGLConfig config) {
      // NO CHANGE - SKIP
      ......
   }
   
   // Call back after onSurfaceCreated() or whenever the window's size changes.
   @Override
   public void onSurfaceChanged(GL10 gl, int width, int height) {
      // NO CHANGE - SKIP
      ......
   }
  
   // Call back to draw the current frame.
   @Override
   public void onDrawFrame(GL10 gl) {
      // Clear color and depth buffers
      gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
    
      // ----- Render the Pyramid -----
      gl.glLoadIdentity();                 // Reset the model-view matrix
      gl.glTranslatef(-1.5f, 0.0f, -6.0f); // Translate left and into the screen
      gl.glRotatef(anglePyramid, 0.1f, 1.0f, -0.1f); // Rotate (NEW)
      pyramid.draw(gl);                              // Draw the pyramid (NEW)
    
      // ----- Render the Color Cube -----
      gl.glLoadIdentity();                // Reset the model-view matrix
      gl.glTranslatef(1.5f, 0.0f, -6.0f); // Translate right and into the screen
      gl.glScalef(0.8f, 0.8f, 0.8f);      // Scale down (NEW)
      gl.glRotatef(angleCube, 1.0f, 1.0f, 1.0f); // rotate about the axis (1,1,1) (NEW)
      cube.draw(gl);                      // Draw the cube (NEW)
      
      // Update the rotational angle after each refresh (NEW)
      anglePyramid += speedPyramid;   // (NEW)
      angleCube += speedCube;         // (NEW)
   }
}
Cube

There are many ways to render a cube. You could define all the vertices of the 6 faces as in the above example. You could also define one representative face, and render the face 6 times with proper translation and rotation.

Example 1: Cube1.java

package com.test;
  
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.opengles.GL10;
/*
 * Define the vertices for only one face (the front face).
 * Render the cube by translating and rotating the face.
 */
public class Cube1 {
   private FloatBuffer vertexBuffer;  // Buffer for vertex-array
   
   private float[][] colors = {  // Colors of the 6 faces
      {1.0f, 0.5f, 0.0f, 1.0f},  // 0. orange
      {1.0f, 0.0f, 1.0f, 1.0f},  // 1. violet
      {0.0f, 1.0f, 0.0f, 1.0f},  // 2. green
      {0.0f, 0.0f, 1.0f, 1.0f},  // 3. blue
      {1.0f, 0.0f, 0.0f, 1.0f},  // 4. red
      {1.0f, 1.0f, 0.0f, 1.0f}   // 5. yellow
   };
  
   private float[] vertices = {  // Vertices for the front face
      -1.0f, -1.0f, 1.0f,  // 0. left-bottom-front
       1.0f, -1.0f, 1.0f,  // 1. right-bottom-front
      -1.0f,  1.0f, 1.0f,  // 2. left-top-front
       1.0f,  1.0f, 1.0f   // 3. right-top-front
   };
   
   // Constructor - Set up the buffers
   public Cube1() {
      // Setup vertex-array buffer. Vertices in float. An float has 4 bytes
      ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
      vbb.order(ByteOrder.nativeOrder()); // Use native byte order
      vertexBuffer = vbb.asFloatBuffer(); // Convert from byte to float
      vertexBuffer.put(vertices);         // Copy data into buffer
      vertexBuffer.position(0);           // Rewind
   }
  
   // Draw the color cube
   public void draw(GL10 gl) {
      gl.glFrontFace(GL10.GL_CCW);    // Front face in counter-clockwise orientation
      gl.glEnable(GL10.GL_CULL_FACE); // Enable cull face
      gl.glCullFace(GL10.GL_BACK);    // Cull the back face (don't display)
   
      gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);

      // Front
      gl.glColor4f(colors[0][0], colors[0][1], colors[0][2], colors[0][3]);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      
      // Right - Rotate 90 degree about y-axis
      gl.glRotatef(90.0f, 0.0f, 1.0f, 0.0f);
      gl.glColor4f(colors[1][0], colors[1][1], colors[1][2], colors[1][3]);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);

      // Back - Rotate another 90 degree about y-axis
      gl.glRotatef(90.0f, 0.0f, 1.0f, 0.0f);
      gl.glColor4f(colors[2][0], colors[2][1], colors[2][2], colors[2][3]);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);

      // Left - Rotate another 90 degree about y-axis
      gl.glRotatef(90.0f, 0.0f, 1.0f, 0.0f);
      gl.glColor4f(colors[3][0], colors[3][1], colors[3][2], colors[3][3]);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);

      // Bottom - Rotate 90 degree about x-axis
      gl.glRotatef(90.0f, 1.0f, 0.0f, 0.0f);
      gl.glColor4f(colors[4][0], colors[4][1], colors[4][2], colors[4][3]);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      
      // Top - Rotate another 180 degree about x-axis
      gl.glRotatef(180.0f, 1.0f, 0.0f, 0.0f);
      gl.glColor4f(colors[5][0], colors[5][1], colors[5][2], colors[5][3]);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);

      gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glDisable(GL10.GL_CULL_FACE);
   }
}

Example 2: Cube2.java

package com.test;
  
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.opengles.GL10;
/*
 * Define the vertices for a representative face.
 * Render the cube by translating and rotating the face.
 */
public class Cube2 {
   private FloatBuffer vertexBuffer; // Buffer for vertex-array
  
   private float[] vertices = { // Vertices for a face at z=0
      -1.0f, -1.0f, 0.0f,  // 0. left-bottom-front
       1.0f, -1.0f, 0.0f,  // 1. right-bottom-front
      -1.0f,  1.0f, 0.0f,  // 2. left-top-front
       1.0f,  1.0f, 0.0f   // 3. right-top-front
   };
  
   private float[][] colors = {  // Colors of the 6 faces
      {1.0f, 0.5f, 0.0f, 1.0f},  // 0. orange
      {1.0f, 0.0f, 1.0f, 1.0f},  // 1. violet
      {0.0f, 1.0f, 0.0f, 1.0f},  // 2. green
      {0.0f, 0.0f, 1.0f, 1.0f},  // 3. blue
      {1.0f, 0.0f, 0.0f, 1.0f},  // 4. red
      {1.0f, 1.0f, 0.0f, 1.0f}   // 5. yellow
   };
  
   // Constructor - Set up the buffers
   public Cube2() {
      // Setup vertex-array buffer. Vertices in float. An float has 4 bytes
      ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
      vbb.order(ByteOrder.nativeOrder()); // Use native byte order
      vertexBuffer = vbb.asFloatBuffer(); // Convert from byte to float
      vertexBuffer.put(vertices);         // Copy data into buffer
      vertexBuffer.position(0);           // Rewind
   }
   
   // Draw the shape
   public void draw(GL10 gl) {
      gl.glFrontFace(GL10.GL_CCW);    // Front face in counter-clockwise orientation
      gl.glEnable(GL10.GL_CULL_FACE); // Enable cull face
      gl.glCullFace(GL10.GL_BACK);    // Cull the back face (don't display)
  
      gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
      
      // front
      gl.glPushMatrix();
      gl.glTranslatef(0.0f, 0.0f, 1.0f);
      gl.glColor4f(colors[0][0], colors[0][1], colors[0][2], colors[0][3]);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
  
      // left
      gl.glPushMatrix();
      gl.glRotatef(270.0f, 0.0f, 1.0f, 0.0f);
      gl.glTranslatef(0.0f, 0.0f, 1.0f);
      gl.glColor4f(colors[1][0], colors[1][1], colors[1][2], colors[1][3]);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
  
      // back
      gl.glPushMatrix();
      gl.glRotatef(180.0f, 0.0f, 1.0f, 0.0f);
      gl.glTranslatef(0.0f, 0.0f, 1.0f);
      gl.glColor4f(colors[2][0], colors[2][1], colors[2][2], colors[2][3]);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
  
      // right
      gl.glPushMatrix();
      gl.glRotatef(90.0f, 0.0f, 1.0f, 0.0f);
      gl.glTranslatef(0.0f, 0.0f, 1.0f);
      gl.glColor4f(colors[3][0], colors[3][1], colors[3][2], colors[3][3]);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
 
      // top
      gl.glPushMatrix();
      gl.glRotatef(270.0f, 1.0f, 0.0f, 0.0f);
      gl.glTranslatef(0.0f, 0.0f, 1.0f);
      gl.glColor4f(colors[4][0], colors[4][1], colors[4][2], colors[4][3]);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
 
      // bottom
      gl.glPushMatrix();
      gl.glRotatef(90.0f, 1.0f, 0.0f, 0.0f);
      gl.glTranslatef(0.0f, 0.0f, 1.0f);
      gl.glColor4f(colors[5][0], colors[5][1], colors[5][2], colors[5][3]);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
  
      gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glDisable(GL10.GL_CULL_FACE);
   }
}

Example 6: Texture (Nehe Lesson 6: Texture)

Let's convert our color-cube into a texture-cube.

TextureCube.java

Let's modify our earlier Cube2.java to set up the texture array.

package com.test;
   
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLUtils;
/*
 * A cube with texture. 
 * Define the vertices for only one representative face.
 * Render the cube by translating and rotating the face.
 */
public class TextureCube {
   private FloatBuffer vertexBuffer; // Buffer for vertex-array
   private FloatBuffer texBuffer;    // Buffer for texture-coords-array (NEW)
  
   private float[] vertices = { // Vertices for a face
      -1.0f, -1.0f, 0.0f,  // 0. left-bottom-front
       1.0f, -1.0f, 0.0f,  // 1. right-bottom-front
      -1.0f,  1.0f, 0.0f,  // 2. left-top-front
       1.0f,  1.0f, 0.0f   // 3. right-top-front
   };
  
   float[] texCoords = { // Texture coords for the above face (NEW)
      0.0f, 1.0f,  // A. left-bottom (NEW)
      1.0f, 1.0f,  // B. right-bottom (NEW)
      0.0f, 0.0f,  // C. left-top (NEW)
      1.0f, 0.0f   // D. right-top (NEW)
   };
   int[] textureIDs = new int[1];   // Array for 1 texture-ID (NEW)
     
   // Constructor - Set up the buffers
   public TextureCube() {
      // Setup vertex-array buffer. Vertices in float. An float has 4 bytes
      ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
      vbb.order(ByteOrder.nativeOrder()); // Use native byte order
      vertexBuffer = vbb.asFloatBuffer(); // Convert from byte to float
      vertexBuffer.put(vertices);         // Copy data into buffer
      vertexBuffer.position(0);           // Rewind
  
      // Setup texture-coords-array buffer, in float. An float has 4 bytes (NEW)
      ByteBuffer tbb = ByteBuffer.allocateDirect(texCoords.length * 4);
      tbb.order(ByteOrder.nativeOrder());
      texBuffer = tbb.asFloatBuffer();
      texBuffer.put(texCoords);
      texBuffer.position(0);
   }
   
   // Draw the shape
   public void draw(GL10 gl) {
      gl.glFrontFace(GL10.GL_CCW);    // Front face in counter-clockwise orientation
      gl.glEnable(GL10.GL_CULL_FACE); // Enable cull face
      gl.glCullFace(GL10.GL_BACK);    // Cull the back face (don't display) 
   
      gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
      gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);  // Enable texture-coords-array (NEW)
      gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, texBuffer); // Define texture-coords buffer (NEW)
      
      // front
      gl.glPushMatrix();
      gl.glTranslatef(0.0f, 0.0f, 1.0f);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
  
      // left
      gl.glPushMatrix();
      gl.glRotatef(270.0f, 0.0f, 1.0f, 0.0f);
      gl.glTranslatef(0.0f, 0.0f, 1.0f);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
  
      // back
      gl.glPushMatrix();
      gl.glRotatef(180.0f, 0.0f, 1.0f, 0.0f);
      gl.glTranslatef(0.0f, 0.0f, 1.0f);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
  
      // right
      gl.glPushMatrix();
      gl.glRotatef(90.0f, 0.0f, 1.0f, 0.0f);
      gl.glTranslatef(0.0f, 0.0f, 1.0f);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
  
      // top
      gl.glPushMatrix();
      gl.glRotatef(270.0f, 1.0f, 0.0f, 0.0f);
      gl.glTranslatef(0.0f, 0.0f, 1.0f);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
  
      // bottom
      gl.glPushMatrix();
      gl.glRotatef(90.0f, 1.0f, 0.0f, 0.0f);
      gl.glTranslatef(0.0f, 0.0f, 1.0f);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
  
      gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);  // Disable texture-coords-array (NEW)
      gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glDisable(GL10.GL_CULL_FACE);
   }
  
   // Load an image into GL texture
   public void loadTexture(GL10 gl, Context context) {
      gl.glGenTextures(1, textureIDs, 0); // Generate texture-ID array

      gl.glBindTexture(GL10.GL_TEXTURE_2D, textureIDs[0]);   // Bind to texture ID
      // Set up texture filters
      gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
      gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
  
      // Construct an input stream to texture image "res\drawable\nehe.png"
      InputStream istream = context.getResources().openRawResource(R.drawable.nehe);
      Bitmap bitmap;
      try {
         // Read and decode input as bitmap
         bitmap = BitmapFactory.decodeStream(istream);
      } finally {
         try {
            istream.close();
         } catch(IOException e) { }
      }
  
      // Build Texture from loaded bitmap for the currently-bind texture ID
      GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);
      bitmap.recycle();
   }
}
Dissecting TextureCube.java

The vertices of all the 6 faces are arranged in a consistent manner (inverted-Z). Hence, we can use the same texture coordinates for all 6 face. We define the texture coords once, and put into the texture buffer 6 times. Take note that texture coordinates' origin is at the top-left corner. The coordinates are normalized to [0, 1].

private FloatBuffer texBuffer;     // Texture Coords Buffer
......
float[] texCoords = {  // Define the texture coord, applicable to all 6 faces
   // FRONT
   0.0f, 1.0f,  // A. left-bottom
   1.0f, 1.0f,  // B. right-bottom
   0.0f, 0.0f,  // C. left-top
   1.0f, 0.0f   // D. right-top
};
   
// Allocate texture buffer
ByteBuffer tbb = ByteBuffer.allocateDirect(texCoords.length * 4 * 6);
tbb.order(ByteOrder.nativeOrder());
texBuffer = tbb.asFloatBuffer();
// All the 6 faces have the same texture coords, repeat 6 times
for (int face = 0; face < 6; face++) {
   texBuffer.put(texCoords);
}
texBuffer.position(0);     // Rewind

To render the texture, we simply enable client-state texture-coords-array (together with vertex-array). The vertices and texture-coords will be rendered together.

gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, texBuffer);
   
for (int face = 0; face < 6; face++) {
// Render each face in TRIANGLE_STRIP using 4 vertices
   gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, face*4, 4);
}

We store the texture image "nehe.png" into folder "res\drawable".

The following steps are needed to setup texture and load an image:

  1. [TODO]

Take note that we have removed all the color information.

GL Renderer

We need to modify our renderer to setup texture as follows:

package com.test;
  
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.opengl.GLU;

public class MyGLRenderer implements GLSurfaceView.Renderer {
   
   private Context context;   // Application context needed to read image (NEW)
   private TextureCube cube;
   private static float angleCube = 0;     // rotational angle in degree for cube
   private static float speedCube = -1.5f; // rotational speed for cube
   
   // Constructor
   public MyGLRenderer(Context context) {
      this.context = context;   // Get the application context (NEW)
      cube = new TextureCube();
   }
  
   // Call back when the surface is first created or re-created.
   @Override
   public void onSurfaceCreated(GL10 gl, EGLConfig config) {
      gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);  // Set color's clear-value to black
      gl.glClearDepthf(1.0f);            // Set depth's clear-value to farthest
      gl.glEnable(GL10.GL_DEPTH_TEST);   // Enables depth-buffer for hidden surface removal
      gl.glDepthFunc(GL10.GL_LEQUAL);    // The type of depth testing to do
      gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST);  // nice perspective view
      gl.glShadeModel(GL10.GL_SMOOTH);   // Enable smooth shading of color
      gl.glDisable(GL10.GL_DITHER);      // Disable dithering for better performance
  
      // Setup Texture, each time the surface is created (NEW)
      cube.loadTexture(gl, context);    // Load image into Texture (NEW)
      gl.glEnable(GL10.GL_TEXTURE_2D);  // Enable texture (NEW)
   }
   
   // Call back after onSurfaceCreated() or whenever the window's size changes.
   @Override
   public void onSurfaceChanged(GL10 gl, int width, int height) {
      // NO CHANGE - SKIP
      .......
   }
  
   // Call back to draw the current frame.
   @Override
   public void onDrawFrame(GL10 gl) {
      // Clear color and depth buffers
      gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
      
      // ----- Render the Cube -----
      gl.glLoadIdentity();                  // Reset the current model-view matrix
      gl.glTranslatef(0.0f, 0.0f, -6.0f);   // Translate into the screen
      gl.glRotatef(angleCube, 0.1f, 1.0f, 0.2f); // Rotate
      cube.draw(gl);
      
      // Update the rotational angle after each refresh.
      angleCube += speedCube;
   }
}

Example 6a: Photo-Cube

Let's convert the texture cube into photo cube with different images for each of the 6 faces.

PhotoCube.java
package com.test;
  
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLUtils;
/*
 * A photo cube with 6 pictures (textures) on its 6 faces.
 */
public class PhotoCube {
   private FloatBuffer vertexBuffer;  // Vertex Buffer
   private FloatBuffer texBuffer;     // Texture Coords Buffer
   
   private int numFaces = 6;
   private int[] imageFileIDs = {  // Image file IDs
      R.drawable.caldera,
      R.drawable.candice,
      R.drawable.mule,
      R.drawable.glass,
      R.drawable.leonardo,
      R.drawable.tmsk
   };
   private int[] textureIDs = new int[numFaces];
   private Bitmap[] bitmap = new Bitmap[numFaces];
   private float cubeHalfSize = 1.2f;
        
   // Constructor - Set up the vertex buffer
   public PhotoCube(Context context) {
      // Allocate vertex buffer. An float has 4 bytes
      ByteBuffer vbb = ByteBuffer.allocateDirect(12 * 4 * numFaces);
      vbb.order(ByteOrder.nativeOrder());
      vertexBuffer = vbb.asFloatBuffer();
  
      // Read images. Find the aspect ratio and adjust the vertices accordingly.
      for (int face = 0; face < numFaces; face++) {
         bitmap[face] = BitmapFactory.decodeStream(
               context.getResources().openRawResource(imageFileIDs[face]));
         int imgWidth = bitmap[face].getWidth();
         int imgHeight = bitmap[face].getHeight();
         float faceWidth = 2.0f;
         float faceHeight = 2.0f;
         // Adjust for aspect ratio
         if (imgWidth > imgHeight) {
            faceHeight = faceHeight * imgHeight / imgWidth; 
         } else {
            faceWidth = faceWidth * imgWidth / imgHeight;
         }
         float faceLeft = -faceWidth / 2;
         float faceRight = -faceLeft;
         float faceTop = faceHeight / 2;
         float faceBottom = -faceTop;
         
         // Define the vertices for this face
         float[] vertices = {
            faceLeft,  faceBottom, 0.0f,  // 0. left-bottom-front
            faceRight, faceBottom, 0.0f,  // 1. right-bottom-front
            faceLeft,  faceTop,    0.0f,  // 2. left-top-front
            faceRight, faceTop,    0.0f,  // 3. right-top-front
         };
         vertexBuffer.put(vertices);  // Populate
      }
      vertexBuffer.position(0);    // Rewind
  
      // Allocate texture buffer. An float has 4 bytes. Repeat for 6 faces.
      float[] texCoords = {
         0.0f, 1.0f,  // A. left-bottom
         1.0f, 1.0f,  // B. right-bottom
         0.0f, 0.0f,  // C. left-top
         1.0f, 0.0f   // D. right-top
      };
      ByteBuffer tbb = ByteBuffer.allocateDirect(texCoords.length * 4 * numFaces);
      tbb.order(ByteOrder.nativeOrder());
      texBuffer = tbb.asFloatBuffer();
      for (int face = 0; face < numFaces; face++) {
         texBuffer.put(texCoords);
      }
      texBuffer.position(0);   // Rewind
   }
   
   // Render the shape
   public void draw(GL10 gl) {
      gl.glFrontFace(GL10.GL_CCW);
      
      gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
      gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
      gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, texBuffer);
  
      // front
      gl.glPushMatrix();
      gl.glTranslatef(0f, 0f, cubeHalfSize);
      gl.glBindTexture(GL10.GL_TEXTURE_2D, textureIDs[0]);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
  
      // left
      gl.glPushMatrix();
      gl.glRotatef(270.0f, 0f, 1f, 0f);
      gl.glTranslatef(0f, 0f, cubeHalfSize);
      gl.glBindTexture(GL10.GL_TEXTURE_2D, textureIDs[1]);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 4, 4);
      gl.glPopMatrix();
  
      // back
      gl.glPushMatrix();
      gl.glRotatef(180.0f, 0f, 1f, 0f);
      gl.glTranslatef(0f, 0f, cubeHalfSize);
      gl.glBindTexture(GL10.GL_TEXTURE_2D, textureIDs[2]);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 8, 4);
      gl.glPopMatrix();
  
      // right
      gl.glPushMatrix();
      gl.glRotatef(90.0f, 0f, 1f, 0f);
      gl.glTranslatef(0f, 0f, cubeHalfSize);
      gl.glBindTexture(GL10.GL_TEXTURE_2D, textureIDs[3]);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 12, 4);
      gl.glPopMatrix();
  
      // top
      gl.glPushMatrix();
      gl.glRotatef(270.0f, 1f, 0f, 0f);
      gl.glTranslatef(0f, 0f, cubeHalfSize);
      gl.glBindTexture(GL10.GL_TEXTURE_2D, textureIDs[4]);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 16, 4);
      gl.glPopMatrix();
  
      // bottom
      gl.glPushMatrix();
      gl.glRotatef(90.0f, 1f, 0f, 0f);
      gl.glTranslatef(0f, 0f, cubeHalfSize);
      gl.glBindTexture(GL10.GL_TEXTURE_2D, textureIDs[5]);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 20, 4);
      gl.glPopMatrix();
   
      gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
   }
  
   // Load images into 6 GL textures
   public void loadTexture(GL10 gl) {
      gl.glGenTextures(6, textureIDs, 0); // Generate texture-ID array for 6 IDs
  
      // Generate OpenGL texture images
      for (int face = 0; face < numFaces; face++) {
         gl.glBindTexture(GL10.GL_TEXTURE_2D, textureIDs[face]);
         // Build Texture from loaded bitmap for the currently-bind texture ID
         GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap[face], 0);
         bitmap[face].recycle();
      }
   }
}
MyGLRenderer.java
package com.test;
   
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.opengl.GLU;
   
public class MyGLRenderer implements GLSurfaceView.Renderer {
   private PhotoCube cube;     // (NEW)
   private static float angleCube = 0;     // rotational angle in degree for cube
   private static float speedCube = -1.5f; // rotational speed for cube
   
   // Constructor
   public MyGLRenderer(Context context) {
      cube = new PhotoCube(context);    // (NEW)
   }
   
   // Call back when the surface is first created or re-created.
   @Override
   public void onSurfaceCreated(GL10 gl, EGLConfig config) {
      gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);  // Set color's clear-value to black
      gl.glClearDepthf(1.0f);            // Set depth's clear-value to farthest
      gl.glEnable(GL10.GL_DEPTH_TEST);   // Enables depth-buffer for hidden surface removal
      gl.glDepthFunc(GL10.GL_LEQUAL);    // The type of depth testing to do
      gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST);  // nice perspective view
      gl.glShadeModel(GL10.GL_SMOOTH);   // Enable smooth shading of color
      gl.glDisable(GL10.GL_DITHER);      // Disable dithering for better performance
    
      // Setup Texture, each time the surface is created (NEW)
      cube.loadTexture(gl);             // Load images into textures (NEW)
      gl.glEnable(GL10.GL_TEXTURE_2D);  // Enable texture (NEW)
   }
  
   // Call back after onSurfaceCreated() or whenever the window's size changes.
   @Override
   public void onSurfaceChanged(GL10 gl, int width, int height) {
      // NO CHANGE - SKIP
      ......
   }
  
   // Call back to draw the current frame.
   @Override
   public void onDrawFrame(GL10 gl) {
      // Clear color and depth buffers
      gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
  
      // ----- Render the Cube -----
      gl.glLoadIdentity();                  // Reset the model-view matrix
      gl.glTranslatef(0.0f, 0.0f, -6.0f);   // Translate into the screen
      gl.glRotatef(angleCube, 0.15f, 1.0f, 0.3f); // Rotate
      cube.draw(gl);
      
      // Update the rotational angle after each refresh.
      angleCube += speedCube;
   }
}

Example 7a: User Inputs (Nehe Lesson 7 Part 1: Key-Controlled)

Nehe lesson 7 is far too complex, I shall break it into 3 parts: Key-controlled, Texture Filters, and Lighting.

TextureCube.java

No change, except using image "crate.png".

MyGLRenderer.java

We modify our renderer by adding variables and transformation methods to control the cube z-position, x and y rotational angles and speeds.

package com.test;
   
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.opengl.GLU;
   
public class MyGLRenderer implements GLSurfaceView.Renderer {
    
   private Context context;
   private TextureCube cube;
   // For controlling cube's z-position, x and y angles and speeds (NEW)
   float angleX = 0;   // (NEW)
   float angleY = 0;   // (NEW)
   float speedX = 0;   // (NEW)
   float speedY = 0;   // (NEW)
   float z = -6.0f;    // (NEW)
   
   // Constructor
   public MyGLRenderer(Context context) {
      this.context = context;
      cube = new TextureCube();
   }
  
   // Call back when the surface is first created or re-created.
   @Override
   public void onSurfaceCreated(GL10 gl, EGLConfig config) {
      gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);  // Set color's clear-value to black
      gl.glClearDepthf(1.0f);            // Set depth's clear-value to farthest
      gl.glEnable(GL10.GL_DEPTH_TEST);   // Enables depth-buffer for hidden surface removal
      gl.glDepthFunc(GL10.GL_LEQUAL);    // The type of depth testing to do
      gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST);  // nice perspective view
      gl.glShadeModel(GL10.GL_SMOOTH);   // Enable smooth shading of color
      gl.glDisable(GL10.GL_DITHER);      // Disable dithering for better performance
  
      // Setup Texture, each time the surface is created
      cube.loadTexture(gl, context);    // Load image into Texture
      gl.glEnable(GL10.GL_TEXTURE_2D);  // Enable texture
   }
   
   // Call back after onSurfaceCreated() or whenever the window's size changes.
   @Override
   public void onSurfaceChanged(GL10 gl, int width, int height) {
      // NO CHANGE - SKIP
      ......
   }
  
   // Call back to draw the current frame.
   @Override
   public void onDrawFrame(GL10 gl) {
      // Clear color and depth buffers
      gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);

      // ----- Render the Cube -----
      gl.glLoadIdentity();              // Reset the model-view matrix
      gl.glTranslatef(0.0f, 0.0f, z);   // Translate into the screen (NEW)
      gl.glRotatef(angleX, 1.0f, 0.0f, 0.0f); // Rotate (NEW)
      gl.glRotatef(angleY, 0.0f, 1.0f, 0.0f); // Rotate (NEW)
      cube.draw(gl);
      
      // Update the rotational angle after each refresh (NEW)
      angleX += speedX;  // (NEW)
      angleY += speedY;  // (NEW)
   }
}
MyGLSurfaceView.java

In order to capture the user inputs, we need to customize the GLSurfaceView by extending a subclass, so as to override the event handlers (such as onKeyUp(), onTouchEvent()).

package com.test;
   
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.view.KeyEvent;
import android.view.MotionEvent;
/*
 * Custom GL view by extending GLSurfaceView so as
 * to override event handlers such as onKeyUp(), onTouchEvent()
 */
public class MyGLSurfaceView extends GLSurfaceView {
   MyGLRenderer renderer;    // Custom GL Renderer
   
   // For touch event
   private final float TOUCH_SCALE_FACTOR = 180.0f / 320.0f;
   private float previousX;
   private float previousY;

   // Constructor - Allocate and set the renderer
   public MyGLSurfaceView(Context context) {
      super(context);
      renderer = new MyGLRenderer(context);
      this.setRenderer(renderer);
      // Request focus, otherwise key/button won't react
      this.requestFocus();  
      this.setFocusableInTouchMode(true);
   }
   
   // Handler for key event
   @Override
   public boolean onKeyUp(int keyCode, KeyEvent evt) {
      switch(keyCode) {
         case KeyEvent.KEYCODE_DPAD_LEFT:   // Decrease Y-rotational speed
            renderer.speedY -= 0.1f;
            break;
         case KeyEvent.KEYCODE_DPAD_RIGHT:  // Increase Y-rotational speed
            renderer.speedY += 0.1f;
            break;
         case KeyEvent.KEYCODE_DPAD_UP:     // Decrease X-rotational speed
            renderer.speedX -= 0.1f;
            break;
         case KeyEvent.KEYCODE_DPAD_DOWN:   // Increase X-rotational speed 
            renderer.speedX += 0.1f;
            break;
         case KeyEvent.KEYCODE_A:           // Zoom out (decrease z)
            renderer.z -= 0.2f;
            break;
         case KeyEvent.KEYCODE_Z:           // Zoom in (increase z)
            renderer.z += 0.2f;
            break;
      }
      return true;  // Event handled
   }

   // Handler for touch event
   @Override
   public boolean onTouchEvent(final MotionEvent evt) {
      float currentX = evt.getX();
      float currentY = evt.getY();
      float deltaX, deltaY;
      switch (evt.getAction()) {
         case MotionEvent.ACTION_MOVE:
            // Modify rotational angles according to movement
            deltaX = currentX - previousX;
            deltaY = currentY - previousY;
            renderer.angleX += deltaY * TOUCH_SCALE_FACTOR;
            renderer.angleY += deltaX * TOUCH_SCALE_FACTOR;
      }
      // Save current x, y
      previousX = currentX;
      previousY = currentY;
      return true;  // Event handled
   }
}

Clearly, we use key 'A' and 'Z' for zoom out and zoom in. Touch events for modifying x and y rotational angles. Left, right, up and down buttons to control the x and y rotational speeds.

MyGLActivity.java

We need to modify our GL Activity to use the custom GL View.

package com.test;
  
import android.app.Activity;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
/*
 * OpenGL Main Activity.
 */
public class MyGLActivity extends Activity {
   private GLSurfaceView glView;  // Use subclass of GLSurfaceView (NEW)
   
   @Override
   protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      // Allocate a custom subclass of GLSurfaceView (NEW)
      glView = new MyGLSurfaceView(this);
      setContentView(glView);  // Set View (NEW)
   }
   
   @Override
   protected void onPause() {
      super.onPause();
      glView.onPause();
   }
   
   @Override
   protected void onResume() {
      super.onResume();
      glView.onResume();
   }
}

You can, similarly, capture and process other events. [MORE]

Example 7b: Texture Filters (Nehe Lesson 7 Part 2: Texture Filter)

TextureCube.java
package com.test;

import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.opengles.GL10;
import javax.microedition.khronos.opengles.GL11;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLUtils;
/*
 * A cube with texture.
 * Three texture filters are to be set up. 
 */
public class TextureCube {
   private FloatBuffer vertexBuffer; // Buffer for vertex-array
   private FloatBuffer texBuffer;    // Buffer for texture-coords-array
  
   private float[] vertices = { // Vertices for a face
      -1.0f, -1.0f, 0.0f,  // 0. left-bottom-front
       1.0f, -1.0f, 0.0f,  // 1. right-bottom-front
      -1.0f,  1.0f, 0.0f,  // 2. left-top-front
       1.0f,  1.0f, 0.0f   // 3. right-top-front
   };
  
   float[] texCoords = { // Texture coords for the above face
      0.0f, 1.0f,  // A. left-bottom
      1.0f, 1.0f,  // B. right-bottom
      0.0f, 0.0f,  // C. left-top
      1.0f, 0.0f   // D. right-top
   };
   int[] textureIDs = new int[3];  // Array for 3 texture-IDs (NEW)
     
   // Constructor - Set up the buffers
   public TextureCube() {
      // Setup vertex-array buffer. Vertices in float. An float has 4 bytes
      ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
      vbb.order(ByteOrder.nativeOrder()); // Use native byte order
      vertexBuffer = vbb.asFloatBuffer(); // Convert from byte to float
      vertexBuffer.put(vertices);         // Copy data into buffer
      vertexBuffer.position(0);           // Rewind
  
      // Setup texture-coords-array buffer, in float. An float has 4 bytes
      ByteBuffer tbb = ByteBuffer.allocateDirect(texCoords.length * 4);
      tbb.order(ByteOrder.nativeOrder());
      texBuffer = tbb.asFloatBuffer();
      texBuffer.put(texCoords);
      texBuffer.position(0);
   }
   
   // Draw the shape
   public void draw(GL10 gl, int textureFilter) {  // Select the filter (NEW)
      gl.glFrontFace(GL10.GL_CCW);    // Front face in counter-clockwise orientation
      gl.glEnable(GL10.GL_CULL_FACE); // Enable cull face 
      gl.glCullFace(GL10.GL_BACK);    // Cull the back face (don't display) 
   
      gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
      gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);  // Enable texture-coords-array
      gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, texBuffer); // Define texture-coords buffer

      // Select the texture filter to use via texture ID (NEW)
      gl.glBindTexture(GL10.GL_TEXTURE_2D, textureIDs[textureFilter]);
  
      // front
      gl.glPushMatrix();
      gl.glTranslatef(0.0f, 0.0f, 1.0f);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
  
      // left
      gl.glPushMatrix();
      gl.glRotatef(270.0f, 0.0f, 1.0f, 0.0f);
      gl.glTranslatef(0.0f, 0.0f, 1.0f);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
  
      // back
      gl.glPushMatrix();
      gl.glRotatef(180.0f, 0.0f, 1.0f, 0.0f);
      gl.glTranslatef(0.0f, 0.0f, 1.0f);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
  
      // right
      gl.glPushMatrix();
      gl.glRotatef(90.0f, 0.0f, 1.0f, 0.0f);
      gl.glTranslatef(0.0f, 0.0f, 1.0f);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
  
      // top
      gl.glPushMatrix();
      gl.glRotatef(270.0f, 1.0f, 0.0f, 0.0f);
      gl.glTranslatef(0.0f, 0.0f, 1.0f);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
  
      // bottom
      gl.glPushMatrix();
      gl.glRotatef(90.0f, 1.0f, 0.0f, 0.0f);
      gl.glTranslatef(0.0f, 0.0f, 1.0f);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
  
      gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
      gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glDisable(GL10.GL_CULL_FACE);
   }
  
   // Load an image and create 3 textures with different filters (NEW)
   public void loadTexture(GL10 gl, Context context) {
      // Construct an input stream to texture image "res\drawable\crate.png"
      InputStream istream = context.getResources().openRawResource(R.drawable.crate);
      Bitmap bitmap;
      try {
         // Read and decode input as bitmap
         bitmap = BitmapFactory.decodeStream(istream);
      } finally {
         try {
            istream.close();
         } catch(IOException e) { }
      }

      gl.glGenTextures(3, textureIDs, 0);  // Generate texture-ID array for 3 textures (NEW)

      // Create Nearest Filtered Texture and bind it to texture 0 (NEW)
      gl.glBindTexture(GL10.GL_TEXTURE_2D, textureIDs[0]);
      gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_NEAREST);
      gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
      GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);

      // Create Linear Filtered Texture and bind it to texture 1 (NEW)
      gl.glBindTexture(GL10.GL_TEXTURE_2D, textureIDs[1]);
      gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
      GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);

      // Create mipmapped textures and bind it to texture 2 (NEW)
      gl.glBindTexture(GL10.GL_TEXTURE_2D, textureIDs[2]);
      gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
      gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR_MIPMAP_NEAREST);
      if(gl instanceof GL11) {
         gl.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_GENERATE_MIPMAP, GL11.GL_TRUE);
      }
      GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);

      bitmap.recycle();
   }
}
Dissecting TextureCube.java

[TODO]

MyGLRenderer.java
......
public class MyGLRenderer implements GLSurfaceView.Renderer {
   ......
   int currentTextureFilter = 0;  // Texture filter (NEW)
   ......
   
   @Override
   public void onDrawFrame(GL10 gl) {
      // Clear color and depth buffers
      gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);

      // ----- Render the Cube -----
      gl.glLoadIdentity();              // Reset the current model-view matrix
      gl.glTranslatef(0.0f, 0.0f, z);   // Translate into the screen
      gl.glRotatef(angleX, 1.0f, 0.0f, 0.0f); // Rotate
      gl.glRotatef(angleY, 0.0f, 1.0f, 0.0f); // Rotate
      
      cube.draw(gl, currentTextureFilter);    // (NEW)
      
      // Update the rotational angle after each refresh
      angleX += speedX;
      angleY += speedY;
   }
}
MyGLSurfaceView.java
......
public class MyGLSurfaceView extends GLSurfaceView {
   ......
   // Handler for key event
   @Override
   public boolean onKeyUp(int keyCode, KeyEvent evt) {
      switch(keyCode) {
         ......
         case KeyEvent.KEYCODE_DPAD_CENTER:  // Select texture filter (NEW)
            renderer.currentTextureFilter = (renderer.currentTextureFilter + 1) % 3;
            break;
      }
}

Example 7c: Lighting (Nehe Lesson 7 Part 3: Lighting)

MyGLRenderer.java
package com.test;
  
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.opengl.GLU;
  
public class MyGLRenderer implements GLSurfaceView.Renderer {
    
   private Context context;
   private TextureCube cube;
   // For controlling cube's z-position, x and y angles and speeds
   float angleX = 0;
   float angleY = 0;
   float speedX = 0;
   float speedY = 0;
   float z = -6.0f;
   
   int currentTextureFilter = 0;  // Texture filter

   // Lighting (NEW)
   boolean lightingEnabled = false;   // Is lighting on? (NEW)
   private float[] lightAmbient = {0.5f, 0.5f, 0.5f, 1.0f};
   private float[] lightDiffuse = {1.0f, 1.0f, 1.0f, 1.0f};
   private float[] lightPosition = {0.0f, 0.0f, 2.0f, 1.0f};
  
   // Constructor
   public MyGLRenderer(Context context) {
      this.context = context;
      cube = new TextureCube();
   }
  
   // Call back when the surface is first created or re-created.
   @Override
   public void onSurfaceCreated(GL10 gl, EGLConfig config) {
      gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);  // Set color's clear-value to black
      gl.glClearDepthf(1.0f);            // Set depth's clear-value to farthest
      gl.glEnable(GL10.GL_DEPTH_TEST);   // Enables depth-buffer for hidden surface removal
      gl.glDepthFunc(GL10.GL_LEQUAL);    // The type of depth testing to do
      gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST);  // nice perspective view
      gl.glShadeModel(GL10.GL_SMOOTH);   // Enable smooth shading of color
      gl.glDisable(GL10.GL_DITHER);      // Disable dithering for better performance

      // Setup Texture, each time the surface is created
      cube.loadTexture(gl, context);    // Load image into Texture
      gl.glEnable(GL10.GL_TEXTURE_2D);  // Enable texture
      
      // Setup lighting GL_LIGHT1 with ambient and diffuse lights (NEW)
      gl.glLightfv(GL10.GL_LIGHT1, GL10.GL_AMBIENT, lightAmbient, 0);
      gl.glLightfv(GL10.GL_LIGHT1, GL10.GL_DIFFUSE, lightDiffuse, 0);
      gl.glLightfv(GL10.GL_LIGHT1, GL10.GL_POSITION, lightPosition, 0);
      gl.glEnable(GL10.GL_LIGHT1);   // Enable Light 1 (NEW)
      gl.glEnable(GL10.GL_LIGHT0);   // Enable the default Light 0 (NEW)
   }
   
   // Call back after onSurfaceCreated() or whenever the window's size changes.
   @Override
   public void onSurfaceChanged(GL10 gl, int width, int height) {
      // NO CHANGE - SKIP
      .......
   }
  
   // Call back to draw the current frame.
   @Override
   public void onDrawFrame(GL10 gl) {
      // Clear color and depth buffers
      gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
  
      // Enable lighting? (NEW)
      if (lightingEnabled) {
         gl.glEnable(GL10.GL_LIGHTING);
      } else {
         gl.glDisable(GL10.GL_LIGHTING);
      }
      
      // ----- Render the Cube -----
      gl.glLoadIdentity();              // Reset the model-view matrix
      gl.glTranslatef(0.0f, 0.0f, z);   // Translate into the screen
      gl.glRotatef(angleX, 1.0f, 0.0f, 0.0f); // Rotate
      gl.glRotatef(angleY, 0.0f, 1.0f, 0.0f); // Rotate
      cube.draw(gl, currentTextureFilter);
      
      // Update the rotational angle after each refresh
      angleX += speedX;
      angleY += speedY;
   }
}
MyGLSurfaceView.java
......
public class MyGLSurfaceView extends GLSurfaceView {
   .......
  
   // Handler for key event
   @Override
   public boolean onKeyUp(int keyCode, KeyEvent evt) {
      switch(keyCode) {
         .......
         case KeyEvent.KEYCODE_L:  // Toggle lighting on/off (NEW) 
            renderer.lightingEnabled = !renderer.lightingEnabled;
            break;
      }
      ......
   }
}

Example 8: Blending (Nehe Lesson 8: Blending)

TextureCube.java

Use texture image "glass.png". Remove the culling of back face.

MyGLRenderer.java
package com.test;
  
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.opengl.GLU;
  
public class MyGLRenderer implements GLSurfaceView.Renderer {
   private Context context;
   private TextureCube cube;
   // For controlling cube's z-position, x and y angles and speeds
   float angleX = 0;
   float angleY = 0;
   float speedX = 0;
   float speedY = 0;
   float z = -6.0f;
   
   int currentTextureFilter = 0;  // Texture filter
  
   // Lighting
   boolean lightingEnabled = false;
   private float[] lightAmbient = {0.5f, 0.5f, 0.5f, 1.0f};
   private float[] lightDiffuse = {1.0f, 1.0f, 1.0f, 1.0f};
   private float[] lightPosition = {0.0f, 0.0f, 2.0f, 1.0f};
  
   // Blending (NEW)
   boolean blendingEnabled = false;  // Is blending on? (NEW)
  
   // Constructor
   public MyGLRenderer(Context context) {
      this.context = context;
      cube = new TextureCube();
   }
  
   // Call back when the surface is first created or re-created.
   @Override
   public void onSurfaceCreated(GL10 gl, EGLConfig config) {
      gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);  // Set color's clear-value to black
      gl.glClearDepthf(1.0f);            // Set depth's clear-value to farthest
      gl.glEnable(GL10.GL_DEPTH_TEST);   // Enables depth-buffer for hidden surface removal
      gl.glDepthFunc(GL10.GL_LEQUAL);    // The type of depth testing to do
      gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST);  // nice perspective view
      gl.glShadeModel(GL10.GL_SMOOTH);   // Enable smooth shading of color
      gl.glDisable(GL10.GL_DITHER);      // Disable dithering for better performance
  
      // Setup Texture, each time the surface is created
      cube.loadTexture(gl, context);    // Load image into Texture
      gl.glEnable(GL10.GL_TEXTURE_2D);  // Enable texture
      
      // Setup lighting GL_LIGHT1 with ambient and diffuse lights
      gl.glLightfv(GL10.GL_LIGHT1, GL10.GL_AMBIENT, lightAmbient, 0);
      gl.glLightfv(GL10.GL_LIGHT1, GL10.GL_DIFFUSE, lightDiffuse, 0);
      gl.glLightfv(GL10.GL_LIGHT1, GL10.GL_POSITION, lightPosition, 0);
      gl.glEnable(GL10.GL_LIGHT1);   // Enable Light 1
      gl.glEnable(GL10.GL_LIGHT0);   // Enable the default Light 0
      
      // Setup Blending (NEW)
      gl.glColor4f(1.0f, 1.0f, 1.0f, 0.5f);           // Full brightness, 50% alpha (NEW)
      gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE); // Select blending function (NEW)
   }
   
   // Call back after onSurfaceCreated() or whenever the window's size changes.
   @Override
   public void onSurfaceChanged(GL10 gl, int width, int height) {
      // NO CHANGE - SKIP
      ......
   }
  
   // Call back to draw the current frame.
   @Override
   public void onDrawFrame(GL10 gl) {
      // Clear color and depth buffers
      gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
  
      // Enable lighting?
      if (lightingEnabled) {
         gl.glEnable(GL10.GL_LIGHTING);
      } else {
         gl.glDisable(GL10.GL_LIGHTING);
      }
      
      // Blending Enabled? (NEW)
      if (blendingEnabled) {
         gl.glEnable(GL10.GL_BLEND);       // Turn blending on (NEW)
         gl.glDisable(GL10.GL_DEPTH_TEST); // Turn depth testing off (NEW)
         
      } else {
         gl.glDisable(GL10.GL_BLEND);      // Turn blending off (NEW)
         gl.glEnable(GL10.GL_DEPTH_TEST);  // Turn depth testing on (NEW)
      }
      
      // ----- Render the Cube -----
      gl.glLoadIdentity();              // Reset the model-view matrix
      gl.glTranslatef(0.0f, 0.0f, z);   // Translate into the screen
      gl.glRotatef(angleX, 1.0f, 0.0f, 0.0f); // Rotate
      gl.glRotatef(angleY, 0.0f, 1.0f, 0.0f); // Rotate
      cube.draw(gl, currentTextureFilter);
        
      // Update the rotational angle after each refresh
      angleX += speedX;
      angleY += speedY;
   }
}
MyGLSurfaceView.java
......
public class MyGLSurfaceView extends GLSurfaceView {
   .......
  
   // Handler for key event
   @Override
   public boolean onKeyUp(int keyCode, KeyEvent evt) {
      switch(keyCode) {
         .......
         case KeyEvent.KEYCODE_B:  // Toggle Blending on/off (NEW)
            renderer.blendingEnabled = !renderer.blendingEnabled;
            break;
      }
      ......
   }
}

Example 8a: Bouncing Ball in Cube

[TODO] No primitive to draw a sphere in OpenGL ES.

Android Port for Nehe's Lessons

I have ported some of the Nehe's lessons into Android. Refer to Nehe for the problem descriptions.

Setting Up:

  • Nehe's Lesson #1: Setting up OpenGL's window. (Refer to above examples.)

OpenGL Basics: I consider Lessons 2-8 as OpenGL basic lessons, that are extremely important! (Refer to above examples.)

  • Nehe's Lesson #2: Your first polygon
  • Nehe's Lesson #3: Adding Color
  • Nehe's Lesson #4: Rotation
  • Nehe's Lesson #5: 3D Shape
  • Nehe's Lesson #6: Texture
  • Nehe's Lesson #7: Texture Filter, Lighting, and key-controlled
  • Nehe's Lesson #8: Blending

Intermediate: [TODO]

 

REFERENCES & RESOURCES

Sunday, October 25, 2020

Etherum Blockchain guide

Smart Contract in Ethereum Blockchain
TABLE OF CONTENTS (HIDE)

Writing Smart Contracts in Ethereum Blockchain

 

To program Ethereum blockchain, you need to be familiar with JavaScript and full-stack web development under Node.js.

Introduction to Blockchain Technology [Work In Progres]

A blockchain is a distributed digital ledger of transactions. It contains records of all transactions or events that have been executed, which are shared across the nodes participating in the blockchain.

A blockchain is segregated into blocks. Each block contains encypted data on the transaction, sender/receiver, and the previous block's hash. The block is then appended to the chain in chronological order, hence, the name blockchain.

Blockchain is immutable and trusted ...

Blockchain is distributed, peer-to-peer, decentralized, not controlled by an single party, no intermediary to verify.

Consensus algorithms (to prevent double-spending):

  1. Proof of Work (PoW): For each block generation, it will require miners to solve a mathematical puzzle which requires a lot of computational power. The first node who solves the puzzle gets to mine the next block.
  2. Proof of Stake (PoS): Validators use some of their own coins as stakes. Validators will validate blocks by placing a bet on it if they discover a block which they think can be added to the chain. To entice the validators, rewards are given based on their bet. As their bet increases so will their reward. In the end, a validator is chosen to generate a new block based on their economic stake in the network.
History of Blockchain

In 2009, Satoshi Nakamoto, who remains anonymous to-date, published the famous "Bitcoin White Paper" (https://bitcoin.org/bitcoin.pdf) and created the cryptocurrency Bitcoin.

Building Smart Contract in Ethereum Blockchain by Examples

References:
  1. Ethereum @ https://ethereum.org/.
  2. Truffle Boxes - The easier way to get started @ https://www.trufflesuite.com/boxes.

What is Ethereum Blockchain? [Work In Progress]

There are several blockchain platforms that allow developers to create and execute smart contracts. Ethereum (@ https://ethereum.org/) is a global, open-source platform for decentralized applications (dapps). Launched in 2015, Ethereum is among the leading programmable blockchains that you can use it to build new blockchain applications, such as cryptocurrency wallets, financial applications, decentralized markets, and games. Like other blockchains, Ethereum has a native cryptocurrency called Ether (ETH), similar to Bitcoin.

Ethereum consists of:

  1. Ethereum Virtual Machine (EVM): essentially a state machine that allows you to execute codes.
  2. Solidity: a programming language build on top of the EVM for writing smart contracts, to send and receive digital tokens and store states.
  3. Gas: On the Ethereum blockchain, each smart contract is processed by one miner and the resultant block is added to the blockchain. Miners must be rewarded for their efforts, so executing any smart contract on the EVM requires a payment called gas. You need to specify the amount of gas you want to spend for executing any smart contract you create.

To transact on Ethereum blockchain, a user needs an account with a wallet address having some Ether (ETH). Once he connected to the network, he can execute transaction and pay a small transaction fee to write his transaction to the blockchain. This transaction fee is called "gas". Some of the nodes on the network, called miners, compete to complete this transaction. The miner who completes this transaction is awarded the Ether.

It is important to note that reading data from the blockchain is free, but writing to it is not.

Bitcoin vs. Ethereum

[Work In Progress]

Bitcoin offers one application of blockchain technology, a peer-to-peer electronic cash system that enables online Bitcoin payments. Ethereum leverages on blockchain with the implementation of smart contracts.

In the Ethereum blockchain, users use a crypto token known as Ether which fuels the network. Unlike Bitcoin where it is mainly a tradeable cryptocurrency, Ether is also used to pay for transaction fees and services by application developers on the Ethereum network.

In the Ethereum blockchain there is also another type of token known as gas, that is used to pay miners fees for including transactions in their block and every smart contract execution requires a certain amount of gas to be sent along with it to entice miners to put it in the blockchain.

What is a Smart Contract?

The Ethereum blockchain allows us to execute code containing in smart contracts with the Ethereum Virtual Machine (EVM).

Smart contracts contain the business logic of our dapp. There are in charge of reading from and writing to the Ethereum blockchain. Smart contacts are written in a programming language called Solidity, which looks like JavaScript.

Tools

dapp framework

To develop an Ethereum smart contract for a blockchain marketplace, you need to install following toolkits:

Node.js and npm (@ https://nodejs.org/) - JavaScript Runtime and Package Manager

Node.js is a JavaScript runtime environment that executes JavaScript code outside of a browser. It is open-source and cross platform.

npm (originally short for Node Package Manager) is a package manager for the JavaScript and node.js. It consists of a command-line client, also called npm, and a registry of packages.

To install node.js and npm: Goto node.js download site @ https://nodejs.org/en/download/ ⇒ choose "LTS" (Long-Term Support).

  • (Windows) select "Windows Binary (.zip), 64-bit" to download the ZIP file (e.g., "node-v12.16.1-win-x64.zip") ⇒ Unzip into an installed directory of your choice ⇒ Add the installed directory to your PATH environment variable ⇒ the executables "node.exe" and "npm.cmd" can be found at the installed directory (accessible via PATH).
  • (macOS): TODO
  • (Linux): TODO

To verify, issue the following commands from CMD/Terminal/Bash-Shell:

node -v
v12.16.1

npm -v
6.13.4
Truffle Suite (@ https://www.trufflesuite.com/) - A Development Framework for Ethereum Blockchain Applications

The Truffle Suite allows us to build decentralized applications (dapps) on the Ethereum blockchain. It provides a suite of tools for us to write smart contracts with the solidity language, test the smart contracts and deploy them to the blockchain.

You can install Truffle with npm as follows. Global option is used so that it can be used in many projects.

npm install --global truffle

To verify:

truffle version
Truffle v5.1.15 (core: 5.1.15)
Solidity v0.5.16 (solc-js)
Node v12.16.1
Web3.js v1.2.1

Truffle module is installed under "node_modules" sub-directory of node.js installed directory. The executable "truffle.cmd" can be found at the node.js base directory.

Truffle comes with a set of commands, the commonly-used commands are listed below via "truffle help":

truffle help
Truffle v5.1.15 - a development framework for Ethereum
Usage: truffle <command> [options]
compile   Compile contract source files
console   Run a console with contract abstractions and commands available
migrate   Run migrations to deploy contracts
test      Run JavaScript and Solidity tests
unbox     Download a Truffle Box, a pre-built Truffle project
version   Show version number and exit
......
Ganache (@ https://www.trufflesuite.com/ganache) - A Local Personal Blockchain for Ethereum Development

Ganache is a local in-memory personal blockchain, which you can use to run tests, execute commands, and inspect state while controlling how the chain operates.

To install Ganache: Goto Truffle-Ganache @ https://www.trufflesuite.com/ganache ⇒ Download.

Ganache provides 10 accounts preloaded with 100 fake Ether (ETH). Each account has a unique address and a private key.

web3.js (@ https://web3js.readthedocs.io/ and https://github.com/ethereum/web3.js/)

The traditional front-end client written in HTML/CSS/JavaScript connects and interacts with a backend HTTP server. On the other hand, the smart contract client needs to connect to a Ethereum blockchain node.

Web3.js is the Ethereum JavaScript API that let you interacts with Ethereum blockchain nodes, local or remote, using a HTTP or IPC (Inter-Process Call) connection. It can retrieve user accounts, send transactions, interact with smart contracts, and more.

MetaMask Wallet (@ https://metamask.io/)

MetaMask is a crypto wallet and gateway to blockchain dapps. It is available as a browser extension (for Chrome and Firefox) and as a mobile app. It equips you with a key vault, secure login and token wallet - everything you need to manage your digital assets.

  • (For Firefox) Select "Settings" ⇒ "Add-ons" ⇒ Search for "MetaMask" ⇒ Select "Meta<ask (Ethereum Browser Extension)" ⇒ "Add to Firefox" ⇒ You will see a "Fox" icon appears on the top-right of navigation bar.
  • (For Chrome) Goto "Settings" ⇒ "Extensions" ⇒ "Open Chrome Web Store" ⇒ Search for "MetaMask" ⇒ "Add to Chrome" ⇒ You will see a "Fox" icon appears on the top-right of navigation bar.

Other alternative wallets are: EtherWallet...

Lite-Server (@ https://github.com/johnpapa/lite-server) - A Light-weight HTTP server

Bundled with Truffle Box.

Gulp (@ https://gulpjs.com/) - A Task Runner (Build Tool) to Automate the Work Flow

[TODO]

Source-Code Editor or Integrated Development Environment (IDE)

You can use a source-code editor to start learning Ethereum, but a good IDE will greatly improve your productivity.

Many source-code editor and IDE provide support for Ethereum Solidity programming language.

  • VS Code (Microsoft Visual Studio Code) (@ https://code.visualstudio.com/): To install support for Solidity ⇒ Select "Extensions" ⇒ Search "Solidity" ⇒ Choose "Solidity (Solidity support for Visual Studio Code".
  • Sublime Text 3 (Source-Code Editor) (@ https://www.sublimetext.com/): To install support for Solidity ⇒ Select "Preferences" ⇒ Package control ⇒ Enter "Install Package" ⇒ Enter "Ethereum" ⇒ Restart.
  • Remix - Ethereum IDE (@ https://remix.ethereum.org/): an online editor for instant testing and deployment.
  • Ethereum Studio - Solidity IDE (@ https://studio.ethereum.org/): [TODO]

Truffle Box

Truffle box (@ https://www.trufflesuite.com/boxes) provides many boilerplates for you to develop your dapp.

Example 1: First Dapp with Truffle Box Pet-Shop

References:
  1. Josh Quintal, "Ethereum Pet Shop Tutorial - Your First Dapp" @ https://www.trufflesuite.com/tutorials/pet-shop.
  2. Truffle Box Pet-Shop @ https://www.trufflesuite.com/boxes/pet-shop.

 

We shall begin with the Truffle Box "Pet Shop" as our first dapp example (following the tutorial in the references).

Step 1: Create a Truffle Project

First, let us create a project directory called "petshop".

Start a CMD/Terminal/Bash-Shell and issue these commands:

// cd /path/to/base-directory
mkdir petshop
cd petshop
Step 2: Unbox the Pet-Shop Truffle Box into our Project Directory

Download and unpack the Truffle box Pet-Shop:

// cd /path/to/petshop
truffle unbox pet-shop
......
Unbox successful, sweet!
Commands:
  Compile:        truffle compile
  Migrate:        truffle migrate
  Test contracts: truffle test
  Run dev server: npm run dev

The boilerplate codes are unpacked into the "petshop" directory created earlier.

The Truffle's directory structure contains:

  • contracts/: contains the solidity source file (.sol) for smart contracts. The Pet-Shop box provides a smart contract called "Migrations.sol" used for deployment.
  • migrations/: Truffle uses a migration system to deploy smart contracts. A Migration is a special smart contract that keeps track of changes.
  • test/: contains the test scripts (written in JavaScript or Solidity) for the smart contracts.
  • node_modules/: contains the node.js dependencies.
  • src/: contains client-side programs in HTML/CSS/JS and related resources such as images and fonts.
  • truffle-config.js: the Truffle configuration file.
Step 3: Write our Smart Contract

We shall begin writing our smart contract. A smart contract contains the business logic and is in charge of reading from and writing to the Ethereum blockchain.

Use a source-code editor or IDE, create the following Solidity source file, and save as "Adoption.sol" under the "contracts" directory.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pragma solidity ^0.5.0;

contract Adoption {
    // Declare an array of 16 Ethereum addresses as the adopter of each pet
    address[16] public adopters;

    // Adopting a pet
    function adopt(uint petId) public returns (uint) {
        require(petId >= 0 && petId <= 15, "Pet-ID out of range");
        adopters[petId] = msg.sender;  // address of account/smart contract that calls this function
        return petId;
    }

    // Retrieving the adopters
    function getAdopters() public view returns (address[16] memory) {
        return adopters;
    }
}

Disecting the code:

  1. In Line 1, the progma statement, which provides additional information to the compiler, is used to specify the Solidity version. The caret (^) denotes minimum version of 0.5.0, but lower than the next version of 0.6.
  2. Statements are terminated by a semicolon (;). By convention, a contract name begins with an uppercase letter; and the variables/functions begins with a lowercase.
  3. Variable adopters: In Line 5, we declare a public variable called adopters, which is an array of address.
    1. Solidity has a type called address, which holds an Ethereum 20-byte address. Every account and smart contract on the Ethereum blockchain has a unique address and can send and receive Ether (ETH) to and from this address.
    2. Public variables in solidity have automatic getter methods. For an array, the getter method is .adopters(idx) for retrieving individual element.
  4. Function adopt(): In Line 8, we define our business logic function called .adopt(), specifying the parameters and return-type.
    1. In Line 9, the require(condition, errMsg) checks for the validity.
    2. In Line 10, we set the adopters[idx] to the address of the caller of this function. The address of an account or a smart contract who called this function is given by msg.sender.
  5. Function getAdopters(): In Line 15, we define a function called .getAdopters() to return the adopters address array.
    1. The return type is specified as address[16] memory. The memory gives the data location for the variable.
    2. The view keyword specifies that the function will not modify the state of the contract.
Step 4: Compile the Smart Contract

We can compile the smart contracts using Truffle. Start a CMD/Terminal, and issue this command:

// cd /path/to/petshop
truffle compile
> Compiling .\contracts\Adoption.sol
> Compiling .\contracts\Migrations.sol
> Artifacts written to ...\petshop\build\contracts > Compiled successfully using: - solc: 0.5.16+commit.9c3226ce.Emscripten.clang

The outputs are Artifacts "Adoption.json" and "Migrations.json", kept in "petshop/build/contracts/" directory.

Step 5: Migrate to the Ganache Local Personal Blockchain

A Migration is a deployment script meant to alter the state of our application's contracts, moving it from one state to the next. For the first migration, we just deploy new code, but over time, other migrations might move data around or replace a contract with a new one.

In the "Migrations" directory, there is currently a JavaScript called "1_initial_migration.js", as follows, which deploys the "contracts/Migrations.sol".

1
2
3
4
5
var Migrations = artifacts.require("./Migrations.sol");

module.exports = function(deployer) {
  deployer.deploy(Migrations);
};

The "Migrations.sol" smart contract is re-produced as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pragma solidity >=0.4.21 <0.7.0;

contract Migrations {
  address public owner;
  uint public last_completed_migration;

  modifier restricted() {
    if (msg.sender == owner) _;
  }

  constructor() public {
    owner = msg.sender;
  }

  function setCompleted(uint completed) public restricted {
    last_completed_migration = completed;
  }
}

Create the second migration script called "2_deploy_contracts.js" in the "Migrations" directory, as follows, to deploy our "Adoption" smart contract.

1
2
3
4
5
var Adoption = artifacts.require("Adoption");

module.exports = function(deployer) {
  deployer.deploy(Adoption);
};

Before we can migrate our smart contract to a blockchain, we need a blockchain running. We shall use Ganache, which is a local personal blockchain for Ethereum development. Ganache provides 10 accounts preloaded with 100 fake Ether (ETH). Each account has a unique address and a private key.

See the "Tools" section on how to install Ganache.

Launch Ganache ⇒ Choose "New Workspace" ⇒ In "Workspace Name", enter "petshop" ⇒ in "Truffle Project", click "Add Project" and select "petshop/truffle-config.js" ⇒ "Save Workspace".

Ganache Start

Play around and study the Ganache console, which shows that:

  • The RPC (Remote Procedure Call) server runs on localhost (127.0.0.1) port number 7545.
  • The "ACCOUNTS" panel shows 10 accounts with 100 ETH. Each account has a 20-byte address and a private key.
  • A block (Block 0) was created. You can select the "BLOCKS" tab to inspect the block.
  • The "TRANSACTION" panel shows no transactions.
  • The "CONTRACTS" panel shows 2 contracts ("Adoption" and "Migrations") that are not deployed yet.
  • The "EVENTS" panel shows no events.
  • The "LOGS" panel shows the logs and error messages.

Back to CMD/Terminal/Bash-Shell. Issue "truffle migrate" command to migrate (or deploy) our smart contracts:

Truffle migrate
......
1_initial_migration.js
======================
   Deploying 'Migrations'
   ----------------------
   > transaction hash:    0x58f39553c1e3df78dee5196e318444501007edd8028de808f3daeaf7042ff259
   > Blocks: 0            Seconds: 0
   > contract address:    0x69d1Ef9557861D3De103E46E2Fd745042aEC1c03
   > block number:        1
   > block timestamp:     1583308281
   > account:             0xa9734F4FAC4F27BdC55016F93e676c352B4FbEfD (first account in Ganache)
   > balance:             99.99623034
   > gas used:            188483
   > gas price:           20 gwei
   > value sent:          0 ETH
   > total cost:          0.00376966 ETH

   > Saving migration to chain.
   > Saving artifacts
   -------------------------------------
   > Total cost:          0.00376966 ETH

2_deploy_contracts.js
=====================
......

The Ganache console shows that the state of the blockchain has changed.

Ganache First Migrate
  • Four blocks were created by four transactions by the first account - a "contract creation" and a "contract call" for each smart contract.
  • The first account, which is the "Sender Address" for all the transactions, has used some ETH for the transaction costs of migration.
  • Under "CONTRACTS", two contracts ("Adoption" and "Migrations") were deployed. Each contract has an address.
  • Again, play around and study all the panels.
Step 6: Test our Smart Contract

Proper testing is critical in software development. The Truffle framework provides extensive testing support. The Truffle test scripts can be written in JavaScript or Solidity.

In this example, we shall write our test script in Solidity (the next example is in JavaScript). Create a solidity source file called "TestAdoption.sol" in the "test" directory, as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
pragma solidity ^0.5.0;

import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/Adoption.sol";

contract TestAdoption {
    // Deploy an instance of Adoption smart contract for testing
    Adoption adoption = Adoption(DeployedAddresses.Adoption());
    // The id of the pet that will be used for testing
    uint expectedPetId = 8;
    // The expected owner of adopted pet is this contract
    address expectedAdopter = address(this);

    // Test the adopt() function
    function testAdopt() public {
        uint returnedId = adoption.adopt(expectedPetId);
        Assert.equal(returnedId, expectedPetId, "Adoption of the expected pet should match what is returned.");
    }

    // Test adopters public getter to retrieve a single pet's owner
    function testAdoptersGetter() public {
        address adopter = adoption.adopters(expectedPetId);
        Assert.equal(adopter, expectedAdopter, "Owner of the expected pet should be this contract");
    }

    // Test the getAdopters() function to retrieve all pet owners
    function testGetAdopters() public {
        // Store adopters in memory rather than contract's storage
        address[16] memory adopters = adoption.getAdopters();
        Assert.equal(adopters[expectedPetId], expectedAdopter, "Owner of the expected pet should be this contract");
    }
}

Dissecting the code:

  1. The script starts with 3 import statements (Line 3-5):
    1. truffle/Assert.sol: this smart contract provides the various assertions, such as Assert.equal() used in the script.
    2. truffle/DeployAddresses.sol: Truffle deploys a fresh instance of contract being tested to the blockchain. This smart contract gets the address of the deployed contract.
    3. Adoption.sol: the smart contract to be tested.
  2. Next, we declare 3 contract-wide variables:
    1. Adoption adoption: An instance of the smart contract to be tested. The DeployedAddresses.Adoption() returns its address.
    2. uint expectedPetId: The ID of the pet used in testing.
    3. address expectedAdopter: The address of TestAdoption contract that will be the sender of the transactions.
  3. testAdopt() (Line 16): Test the .adopt() function of the our Adoption smart contract, which takes a pet-id and returns the same pet-id.
  4. testAdoptersGetter() (Line 22): Test the automatic public getter of the variable adopters. For an array adopters, the public getter is .adopters(idx).
  5. testGetAdopters() (Line 28): Test the .getAdopters() function of our Adoption smart contract, which returns the entire address array. The memory attribute asks Solidity to temporarily store the value in memory, rather than saving it to the contract's storage.

To run the test, back to the CMD/Terminal and issue:

truffle test
......
  TestAdoption
    ??? testUserCanAdoptPet (107ms)
    ??? testGetAdopterAddressByPetId (84ms)
    ??? testGetAdopterAddressByPetIdInArray (135ms)
  3 passing (9s)

Notes: For Windows, there is a bug for Truffle 5.1.15. I revert back to truffle 5.1.10 to avoid the bug.

npm uninstall --global truffle
npm install --global truffle@5.1.10
(Aside) Truffle Console

Truffle provides a command-line console. Try it out.

truffle console
truffle(development)> Adoption
......
truffle(development)> Adoption.deployed().then(function(instance) { app = instance })
undefined
truffle(development)> app
......
truffle(development)> app.adopters(1)
'0x0000000000000000000000000000000000000000'
truffle(development)> app.getAdopters()
[
  '0x0000000000000000000000000000000000000000',
  '0x0000000000000000000000000000000000000000',
  ......
]
Step 7: Create a User Interface with web3.js to interact with our Smart Contract from a Web Browser

Now, we have (a) written our smart contract, (b) deployed it to our local test blockchain, and (c) tested the functions of the smart contract via the console. We need to create a client-side user interface.

The Pet-Shop Truffle box includes the front-end boilerplate in the "src" directory.

Open the "index.html" under the "src" directory and study the page:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
    <title>Pete's Pet Shop</title>

    <!-- Bootstrap -->
    <link href="css/bootstrap.min.css" rel="stylesheet">

    <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
    <!--[if lt IE 9]>
      <script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
      <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
    <![endif]-->
  </head>
  <body>
    <div class="container">
      <div class="row">
        <div class="col-xs-12 col-sm-8 col-sm-push-2">
          <h1 class="text-center">Pete's Pet Shop</h1>
          <hr/>
          <br/>
        </div>
      </div>

      <div id="petsRow" class="row">
        <!-- PETS LOAD HERE -->
      </div>
    </div>

    <div id="petTemplate" style="display: none;">
      <div class="col-sm-6 col-md-4 col-lg-3">
        <div class="panel panel-default panel-pet">
          <div class="panel-heading">
            <h3 class="panel-title">Scrappy</h3>
          </div>
          <div class="panel-body">
            <img alt="140x140" data-src="holder.js/140x140" class="img-rounded img-center" style="width: 100%;"
              src="https://animalso.com/wp-content/uploads/2017/01/Golden-Retriever_6.jpg" data-holder-rendered="true">
            <br/><br/>
            <strong>Breed</strong>: <span class="pet-breed">Golden Retriever</span><br/>
            <strong>Age</strong>: <span class="pet-age">3</span><br/>
            <strong>Location</strong>: <span class="pet-location">Warren, MI</span><br/><br/>
            <button class="btn btn-default btn-adopt" type="button" data-id="0">Adopt</button>
          </div>
        </div>
      </div>
    </div>

    <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
    <!-- Include all compiled plugins (below), or include individual files as needed -->
    <script src="js/bootstrap.min.js"></script>
    <script src="js/web3.min.js"></script>
    <script src="js/truffle-contract.js"></script>
    <script src="js/app.js"></script>
  </body>
</html>

Dissecting the code:

  1. BootStrap is used to layout the web page.
  2. [TODO]

Open the JavaScript "src/js/app.js". The is a global App object to manage our application, load the pet data in init() and then call the function initWeb3(). The web3.js JavaScript library (@ https://github.com/ethereum/web3.js/) interacts with the Ethereum blockchain. It can retrieve user accounts, send transactions, interact with smart contracts, and more.

Replace the "app.js" with the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
App = {
  web3Provider: null,
  contracts: {},

  init: async function() {
    // Load pets
    $.getJSON('../pets.json', function(data) {
      var petsRow = $('#petsRow');
      var petTemplate = $('#petTemplate');

      for (i = 0; i < data.length; i ++) {
        petTemplate.find('.panel-title').text(data[i].name);
        petTemplate.find('img').attr('src', data[i].picture);
        petTemplate.find('.pet-breed').text(data[i].breed);
        petTemplate.find('.pet-age').text(data[i].age);
        petTemplate.find('.pet-location').text(data[i].location);
        petTemplate.find('.btn-adopt').attr('data-id', data[i].id);

        petsRow.append(petTemplate.html());
      }
    });

    return await App.initWeb3();
  },

  initWeb3: async function() {
    if (window.ethereum) {  // Modern dapp browsers
      App.web3Provider = window.ethereum;
      try {
        await window.ethereum.enable();  // Request account access
      } catch (error) {  // User denied account access...
        console.error("User denied account access")
      }
    } else if (window.web3) {  // Legacy dapp browsers
      App.web3Provider = window.web3.currentProvider;
    } else {  // If no injected web3 instance is detected, fall back to Ganache
      App.web3Provider = new Web3.providers.HttpProvider('http://localhost:7545');
    }
    web3 = new Web3(App.web3Provider);
    return App.initContract();
  },

  initContract: function() {
    $.getJSON('Adoption.json', function(data) {
      // Get the necessary contract artifact file and instantiate it with truffle-contract
      var AdoptionArtifact = data;
      App.contracts.Adoption = TruffleContract(AdoptionArtifact);
      // Set the provider for our contract
      App.contracts.Adoption.setProvider(App.web3Provider);
      // Use our contract to retrieve and mark the adopted pets
      return App.markAdopted();
    });
    return App.bindEvents();
  },

  bindEvents: function() {
    $(document).on('click', '.btn-adopt', App.handleAdopt);
  },

  markAdopted: function(adopters, account) {
    var adoptionInstance;

    App.contracts.Adoption.deployed().then(function(instance) {
      adoptionInstance = instance;
      return adoptionInstance.getAdopters.call();
    }).then(function(adopters) {
      for (i = 0; i < adopters.length; i++) {
        if (adopters[i] !== '0x0000000000000000000000000000000000000000') {
          $('.panel-pet').eq(i).find('button').text('Success').attr('disabled', true);
        }
      }
    }).catch(function(err) {
      console.log(err.message);
    });
  },

  handleAdopt: function(event) {
    event.preventDefault();

    var petId = parseInt($(event.target).data('id'));
    var adoptionInstance;

    web3.eth.getAccounts(function(error, accounts) {
      if (error) {
        console.log(error);
      }
      var account = accounts[0];

      App.contracts.Adoption.deployed().then(function(instance) {
        adoptionInstance = instance;
        // Execute adopt as a transaction by sending account
        return adoptionInstance.adopt(petId, {from: account});
      }).then(function(result) {
        return App.markAdopted();
      }).catch(function(err) {
        console.log(err.message);
      });
    });
  }
};

$(function() {
  $(window).load(function() {
    App.init();
  });
});

Dissecting the code:

  1. jQuery is used ...
  2. An async function can contain an await expression that pauses the execution of the async function and waits for the passed Promise's resolution, and then resumes the async function's execution and returns the resolved value. Note that the await keyword is only valid inside async functions.
Step 8: Install and Configure the MetaMask Wallet

MetaMask Wallet is a browser extension (for Chrome and Firefox) for managing digital assets via the private key. See the "Tools" section on how to install MetaMask.

To configure MetaMask: Click the "Fox" icon on the browser's icon bar ⇒ In "Welcome to MetaMask", click "Get Started" ⇒ In "New to MetaMask", select "Import Wallet" ⇒ In "Wallet Seed", copy and paste the Ganache's "MNEMONIC" (this 12-word phrase is used to generate your private key) and enter a password ⇒ "All Done" ⇒ Close the tab.

MetaMask can connect to any Ethereum nodes. To connect to our local Ganache blockchain (Check that Ganache has started) ⇒ Pull down "Network" on MetaMask ⇒ Select "Custom RPC" ⇒ In "Network Name", enter a name say "MyGanache", in "New RPC URL", enter "http://localhost:7545", where our Ganache RPC server is listening ⇒ "Save". You should see "Account 1" is mapped to Ganache first account with about 100 ETH.

To import an account from Ganache ⇒ Goto Ganache console ⇒ Select an account, say index 5 ⇒ Click the "Key" icon (on the right) to show the private key ⇒ Copy the private key ⇒ Click the "Fox" icon on the browser to pop up MetaMask ⇒ Click the "Circle" icon ⇒ Choose "Import Account" ⇒ Paste the private key copied ⇒ "Import" ⇒ Your shall see a new account called "Account 2" imported from our Ganache with 100 ETH.

Logout MetaMask ⇒ Click on the "Fox" icon on the browser to pop up MetaMask ⇒ Click on the "Circle" icon ⇒ Choose "Logout".

Step 9: Set up the lite-Server

Lite-server is a lightweight development web server (HTTP server) with supports for Single Page Applications (SPAs). It is shipped with Truffle Box Pet-Shop (kept under "petshop/node_modules/lite-server").

The lite-server is configured in "bs-config.json", as follows:

{
  "server": {
    "baseDir": ["./src", "./build/contracts"]
  }
}

The above configuration puts both directories "src" (which contains the HTML/CSS/JS) and "build/contracts" (which contains the smart contract artifacts) as the base-directory (or root-directory) of the server.

In "package.json", we included the following "scripts" object to add an alias "dev" to "lite-server". That is, the command "npm run dev" launch the lite-server.

  "scripts": {
    "dev": "lite-server",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
Step 10: Run our Pet-Shop Dapp

Re-start the Ganache to get a clean state.

On CMD/Terminal, issue truffle migrate command with --reset option to reset the previous smart contracts:

truffle migrate --reset
Starting migrations...
......
> Total deployments:   2
> Final cost:          0.00907878 ETH

Now, start the lite-server:

npm run dev
> pet-shop@1.0.0 dev ...\petshop
> lite-server
......

A new browser tab is started at "http://localhost:3000", displaying the "index.html" under "src" directory ⇒ MetaMask pops up, if you have logged out ⇒ Enter your password ⇒ In "Connect Request", click "Connect".

Click on the "Fox" icon again ⇒ Click the "Circle" icon ⇒ choose an "Account" with some ETH ⇒ If all accounts have zero ETH, import one from Ganache (Select a Ganache account ⇒ Click the "key" icon to reveal the private key ⇒ Copy the private key ⇒ In MetaMask, choose "Import Account" ⇒ Paste the private key.)

Now, goto the web page ⇒ Select a pet ⇒ Click "Adopt" ⇒ MetaMask Pops up showing the transaction details ⇒ Click "Confirm" ⇒ The "Adopt" button changes to "Success" and disabled.

Congratulations! It is pretty hard to reach here (It took me one whole night)!

Step 11: Shutting Down
  • You can shutdown the lite-server via Ctrl-C to terminate the batch job.
  • Logout MetaMask.

Example 2: An eVoting Dapp

References:
  1. Gregory McCubbin, "How to Build a Full Stack Decentralized Application Step-By-Step" @ https://www.dappuniversity.com/articles/the-ultimate-ethereum-dapp-tutorial.
  2. Gleb B., Ihor D. " How To Build an Ethereum Smart Contract for a Blockchain Marketplace" @ https://rubygarage.org/blog/ethereum-smart-contract-tutorial, with code in "https://github.com/dappuniversity/election".

You need to read and understand the "Pet-Shop" example, before attempting this example.

Step 1: Create a Truffle Project and Unbox the Truffle Box Pet-Shop

Create a project directory called "eVoting" and unbox the Truffle box "Pet-Shop" as the boilerplate.

Start a CMD/Terminal/Bash-Shell and issue these commands:

cd /path/to/base-directory
mkdir evoting
cd evoting
truffle unbox pet-shop
Step 2: Write our Smart Contracts

Create a Solidity source file called "EVoting.sol" under the "contracts" directory as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
pragma solidity ^0.5.0;

contract Evoting {
    // Model a Candidate
    struct Candidate {
        uint id;  // starts from 1
        string name;
        uint numVotes;
    }

    // All Candidates by id
    mapping(uint => Candidate) public candidates;

    // Number of Candidates
    uint public numCandidates;

    // Store the addresses that have already voted
    mapping(address => bool) public voters;

    // To trigger this event whenever a vote is cast
    event votedEvent (
        uint indexed _candidateId
    );

    // Add a candidate by name into the mapping, auto-increment id
    function addCandidate (string memory _name) private {
        ++numCandidates;
        candidates[numCandidates] = Candidate(numCandidates, _name, 0);
    }

    // Constructor
    constructor() public {
        addCandidate("Alice");  // id is 1
        addCandidate("Bob");    // id is 2
    }

    // The caller address casts a vote for a candidate
    function vote (uint _candidateId) public {
        // require that they haven't voted before
        require(!voters[msg.sender], "already voted");

        // require a valid candidate
        require(_candidateId > 0 && _candidateId <= numCandidates, "invalid candidate id");

        // record that voter has voted
        voters[msg.sender] = true;  // msg.sender is the caller address

        // update candidate vote Count
        ++candidates[_candidateId].numVotes;

        // trigger voted event
        emit votedEvent(_candidateId);
    }
}

Dissecting the code:

  1. [TODO]
  2. [TODO]

Try compiling the smart contract as follows:

truffle compile
> Compiling .\contracts\Evoting.sol
> Compiling .\contracts\Migrations.sol
> Artifacts written to ...\evoting\build\contracts
> Compiled successfully using:
   - solc: 0.5.16+commit.9c3226ce.Emscripten.clang
Step 3: Migrate to the Ganache Local Personal Blockchain

Under the "Migrations" directory, the Pet-Shop has provided a JavaScript called "1_initial_migration.js", used for deploying "Migrations" smart contract. We shall write the second script called "2_deploy_contracts.js" to deploy our smart contract, as follows:

var Evoting = artifacts.require("Evoting");

module.exports = function(deployer) {
  deployer.deploy(Evoting);
};

Launch Ganache ⇒ Choose "New Workspace" ⇒ In "Workspace Name", enter "Evoting" ⇒ in "Truffle Project", click "Add Project" and select "evoting/truffle-config.js" ⇒ "Save Workspace".

Issue this command:

truffle migrate
......
2_deploy_contracts.js
=====================
   Deploying 'Evoting'
......
Step 4: Test our Smart Contract

Write the following test script (in JavaScript) called "TestEvoting.js" and save in the "test" directory:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
var evoting = artifacts.require("./evoting.sol");

contract("evoting", function(accounts) {
  var evotingInstance;

  it("initializes with two candidates", function() {
    return evoting.deployed().then(function(instance) {
      return instance.numCandidates();
    }).then(function(count) {
      assert.equal(count, 2);
    });
  });

  it("initializes the candidates with the correct values", function() {
    return evoting.deployed().then(function(instance) {
      evotingInstance = instance;
      return evotingInstance.candidates(1);
    }).then(function(candidate) {
      assert.equal(candidate[0], 1, "contains the correct id");
      assert.equal(candidate[1], "Alice", "contains the correct name");
      assert.equal(candidate[2], 0, "contains the correct votes count");
      return evotingInstance.candidates(2);
    }).then(function(candidate) {
      assert.equal(candidate[0], 2, "contains the correct id");
      assert.equal(candidate[1], "Bob", "contains the correct name");
      assert.equal(candidate[2], 0, "contains the correct votes count");
    });
  });

  it("allows a voter to cast a vote", function() {
    return evoting.deployed().then(function(instance) {
      electionInstance = instance;
      candidateId = 1;
      return electionInstance.vote(candidateId, { from: accounts[0] });
    }).then(function(receipt) {
      assert.equal(receipt.logs.length, 1, "an event was triggered");
      assert.equal(receipt.logs[0].event, "votedEvent", "the event type is correct");
      assert.equal(receipt.logs[0].args._candidateId.toNumber(), candidateId, "the candidate id is correct");
      return electionInstance.voters(accounts[0]);
    }).then(function(voted) {
      assert(voted, "the voter was marked as voted");
      return electionInstance.candidates(candidateId);
    }).then(function(candidate) {
      var numVotes = candidate[2];
      assert.equal(numVotes, 1, "increments the candidate's vote count");
    })
  });

  it("throws an exception for invalid candidates", function() {
    return evoting.deployed().then(function(instance) {
      evotingInstance = instance;
      return evotingInstance.vote(99, { from: accounts[1] })
    }).then(assert.fail).catch(function(error) {
      assert(error.message.indexOf('revert') >= 0, "error message must contain revert");
      return evotingInstance.candidates(1);
    }).then(function(candidate1) {
      var numVotes = candidate1[2];
      assert.equal(numVotes, 1, "Alice did not receive any votes");
      return evotingInstance.candidates(2);
    }).then(function(candidate2) {
      var numVotes = candidate2[2];
      assert.equal(numVotes, 0, "Bob did not receive any votes");
    });
  });

  it("throws an exception for double voting", function() {
    return evoting.deployed().then(function(instance) {
      evotingInstance = instance;
      candidateId = 2;
      evotingInstance.vote(candidateId, { from: accounts[1] });
      return evotingInstance.candidates(candidateId);
    }).then(function(candidate) {
      var numVotes = candidate[2];
      assert.equal(numVotes, 1, "accepts first vote");
      // Try to vote again
      return evotingInstance.vote(candidateId, { from: accounts[1] });
    }).then(assert.fail).catch(function(error) {
      assert(error.message.indexOf('revert') >= 0, "error message must contain revert");
      return evotingInstance.candidates(1);
    }).then(function(candidate1) {
      var numVotes = candidate1[2];
      assert.equal(numVotes, 1, "Alice did not receive any votes");
      return evotingInstance.candidates(2);
    }).then(function(candidate2) {
      var numVotes = candidate2[2];
      assert.equal(numVotes, 1, "Bob did not receive any votes");
    });
  });
});

Issue the following Truffle command to run the test:

truffle test
  Contract: Evoting
    ??? initializes with two candidates
    ??? initializes the candidates with the correct values (118ms)
    ??? allows a voter to cast a vote (211ms)
    ??? throws an exception for invalid candidates (154ms)
    ??? throws an exception for double voting (263ms)
  5 passing (863ms)
Step 5: Create the Client-Side User Interface

Under "src" directory, replace "index.html" with the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>E-Voting Results</title>
    <!-- Bootstrap -->
    <link href="css/bootstrap.min.css" rel="stylesheet">
  </head>
  <body>
    <div class="container" style="width: 650px;">
      <div class="row">
        <div class="col-lg-12">
          <h1 class="text-center">E-Voting Results</h1>
          <hr/>
          <br/>
          <div id="loader">
            <p class="text-center">Loading...</p>
          </div>
          <div id="content" style="display: none;">
            <table class="table">
              <thead>
                <tr>
                  <th scope="col">#</th>
                  <th scope="col">Name</th>
                  <th scope="col">Votes</th>
                </tr>
              </thead>
              <tbody id="candidatesResults">
              </tbody>
            </table>
            <hr/>
            <p id="accountAddress" class="text-center"></p>

            <form onSubmit="App.castVote(); return false;">
              <div class="form-group">
                <label for="candidatesSelect">Select Candidate</label>
                <select class="form-control" id="candidatesSelect">
                </select>
              </div>
              <button type="submit" class="btn btn-primary">Vote</button>
              <hr />
            </form>
          </div>
        </div>
      </div>
    </div>

    <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
    <!-- Include all compiled plugins (below), or include individual files as needed -->
    <script src="js/bootstrap.min.js"></script>
    <script src="js/web3.min.js"></script>
    <script src="js/truffle-contract.js"></script>
    <script src="js/app.js"></script>
  </body>
</html>

Dissecting the Code:

  1. [TODO]
  2. [TODO]

Under "src/js" directory, replace "app.js" with the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
App = {
  web3Provider: null,
  contracts: {},
  account: '0x0',
  hasVoted: false,

  init: async function() {
    return await App.initWeb3();
  },

  initWeb3: async function() {
    if (window.ethereum) {  // Modern dapp browsers
      App.web3Provider = window.ethereum;
      try {
        await window.ethereum.enable();  // Request account access
      } catch (error) {  // User denied account access...
        console.error("User denied account access")
      }
    } else if (window.web3) {  // Legacy dapp browsers
      App.web3Provider = window.web3.currentProvider;
    } else {  // If no injected web3 instance is detected, fall back to Ganache
      App.web3Provider = new Web3.providers.HttpProvider('http://localhost:7545');
    }
    web3 = new Web3(App.web3Provider);
    return App.initContract();
  },

  initContract: function() {
    $.getJSON("Evoting.json", function(data) {
      // Instantiate a new truffle contract from the artifact
      App.contracts.Evoting = TruffleContract(data);
      // Connect provider to interact with contract
      App.contracts.Evoting.setProvider(App.web3Provider);

      App.listenForEvents();

      return App.render();
    });
  },

  // Listen for events emitted from the contract
  listenForEvents: function() {
    App.contracts.Evoting.deployed().then(function(instance) {
      // Restart Chrome if you are unable to receive this event
      // This is a known issue with MetaMask
      // https://github.com/MetaMask/metamask-extension/issues/2393
      instance.votedEvent({}, {
        fromBlock: 0,
        toBlock: 'latest'
      }).watch(function(error, event) {
        console.log("event triggered", event)
        // Reload when a new vote is recorded
        App.render();
      });
    });
  },

  render: function() {
    var evotingInstance;
    var loader = $("#loader");
    var content = $("#content");

    loader.show();
    content.hide();

    // Load account data
    web3.eth.getAccounts(function(err, accounts) {
      if (err) {
         console.log(err);
      }
      App.account = accounts[0];
      $("#accountAddress").html("Your Account: " + App.account);
    });

    // Load contract data
    App.contracts.Evoting.deployed().then(function(instance) {
      evotingInstance = instance;
      return evotingInstance.numCandidates();
    }).then(function(numCandidates) {
      var candidatesResults = $("#candidatesResults");
      candidatesResults.empty();

      var candidatesSelect = $('#candidatesSelect');
      candidatesSelect.empty();

      for (var i = 1; i <= numCandidates; i++) {
        evotingInstance.candidates(i).then(function(candidate) {
          var id = candidate[0];
          var name = candidate[1];
          var numVotes = candidate[2];

          // Render candidate Result
          var candidateTemplate = "<tr><th>" + id + "</th><td>" + name + "</td><td>" + numVotes + "</td></tr>"
          candidatesResults.append(candidateTemplate);

          // Render candidate ballot option
          var candidateOption = "<option value='" + id + "' >" + name + "</ option>"
          candidatesSelect.append(candidateOption);
        });
      }
      return evotingInstance.voters(App.account);
    }).then(function(hasVoted) {
      // Do not allow a user to vote
      if(hasVoted) {
        $('form').hide();
      }
      loader.hide();
      content.show();
    }).catch(function(error) {
      console.warn(error);
    });
  },

  castVote: function() {
    var candidateId = $('#candidatesSelect').val();
    App.contracts.Evoting.deployed().then(function(instance) {
      return instance.vote(candidateId, { from: App.account });
    }).then(function(result) {
      // Wait for votes to update
      $("#content").hide();
      $("#loader").show();
    }).catch(function(err) {
      console.error(err);
    });
  }
};

$(function() {
  $(window).load(function() {
    App.init();
  });
});

Dissecting the Code:

  1. [TODO]
  2. [TODO]
Step 6: Setup and Configure MetaMask

See "Pet-Shop" example, if you have not done so.

Step 7: Run our Evoting Dapp

Issue these commands:

truffle migrate --reset
npm run dev

[TODO] There are still some bugs in the "app.js" to be fixed.

Ethereum Blockchain Technologies Review [Work In Progress]

Solidity Programming Language

Reference: Solidity @ https://solidity.readthedocs.io/en/v0.6.3/.

Solidity is an high-level, object-oriented programming language for implementing smart contracts, to be run under the Ethereum Virtual Machine (EVM).

Solidity is statically typed, supports inheritance, libraries and complex user-defined types.

progma solidity ^0.5.0
contract ContractName {
   // variables
   unit myNumber = 1;

   // arrays
   unit[5] aFixedLengthArray;
   unit[] aNonFixedLengthArray;

   // user-defined structures
   struct Certificate {
      address recipientAddress;
      uint recipientID;
   }
   Certificate[] certificates;
   ......
}
"contract" Class

A smart contract is contained within the contract class.

"progma solidity" Compiler Directive

The program solidity compiler directive specifies the solidlity version number. [TODO]

Variables and Types

All variables are stored in the blockchain. Hence, we must be careful of the value assignment or updates, because it will incur cost. [TODO]

Array and User-Defined Structure (struct)

[TODO]

Functions
Syntax:

Example:
function addition(unit _x, uint _y) returns (uint) {   // default is public
   return _x + _y;
}

function subtraction((unit _x, uint _y) private returns (uint) {
}

[TODO]

public vs. private

For public variables, a getter is automatically generated. [TODO]

private entities are accessible within the contract only.

view and pure
string msg = "hello, world";
function getMsg() public view returns (string) {
   return msg;
}
function greeting(string _msg) public pure returns (string) {
   return _msg;
}

view: there is no change in state.

pure: not even accessing any variables.

require()
contract ... {
   address public owner;

   function checkOwner(address _address) returns (bool) {
      require(owner == _address, "The address given is not the owner");
   }
}

Instead of using if-else statement, it is recommended to use require(condition, errMsg), because it will automatically throw an error and revert the flow if the condition is not met.

Starting from Solidity 5, you need to provide an error message string and the second argument.

Web3.js

Web3.js is an Ethereum JavaScript API for interacting with the Ethereum Nodes, via HTTP or IPC.

Setting up Web3 providers

[TODO]

Instantiate a Web3 Contract

In order to communicate with the smart contract, Web3 needs two parameters:

  1. address of the contract
  2. ABI

The deployed smart contract can be instantiated as follows:

var myContract = new web3.eth.Contract(myABI, myContractAddress);

[TODO]

Invoking Smart Contract Functions

There are two methods to invoke smart contract's functions, namely call and send.

  1. call: The call method does not create transaction on the Ethereum blockchain, instead, it only reads from the blockchain. Hence, it does not incur any cost.
    myContract.methods.readDetails("user").call();
  2. send: The send method does create a transaction and change data on the Ethereum blockchain. It requires user to pay "gas" to the miner for the transaction.
    myContract.methods.writeDetails("user").send();

 

 

REFERENCES & RESOURCES