OpenGL ES SDK for Android ARM Developer Center
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Pages
Integer Logic

The application simulates cellular automata phenomenon following Rule 30 using OpenGL ES 3.0.

Introduction

It's assumed that you have read and understood all of the mechanisms described in Asset Loading,Simple Triangle and Texture Cube.

Overview

IntegerLogic_android.png
The application simulates cellular automata phenomenon following Rule 30.

It uses two programs which operate on two textures used in a ping-pong manner. The first program takes the ping texture (ping) as the input and renders the output to a second texture (pong). Rendering in this case is performed by drawing one row at a time, with each row having height of 1 pixel and being of screen width. Excluding the first row, each row is drawn by reading one row above the currently processed one and applying the cellular automata rule. The first row's contents are set by the application. Since we cannot draw and read from the same texture at a single time, the drawing is performed one row at a time. After a row is drawn to texture A, the application binds texture B for drawing and uses texture A for reading the previous line. In the end, texture A contains even rows and texture B contains odd rows.

Having finished drawing lines to these two textures, we run another GL program that merges both textures into a single one by using texture A for even lines and texture B for odd ones.

In order to be able to render to a texture, we use a custom frame-buffer.

For the first run, the input line has only one pixel lit, so it generates the commonly known Rule 30 pattern. Then, every 5 seconds, textures are reset and the input is randomly generated.

Prepare Textures

As already mentioned in Overview, there are two texture objects being used in the application. So the first step is to generate them and prepare for use. We use a ping-pong technique so we will use the corresponding names for variables that will store texture IDs to make the algorithm more clearere.

Generate texture objects.

GL_CHECK(glGenTextures(2, textureIDs));
pingTextureID = textureIDs[0];
pongTextureID = textureIDs[1];

Bind texture object to specific texture unit and set its property for the ping texture,

/* Load ping texture data. */
GL_CHECK(glActiveTexture(GL_TEXTURE0 + pingTextureUnit));
GL_CHECK(glBindTexture (GL_TEXTURE_2D,
GL_CHECK(glTexImage2D (GL_TEXTURE_2D,
0,
GL_R8UI,
0,
GL_RED_INTEGER,
GL_UNSIGNED_BYTE,
GL_CHECK(glTexParameteri(GL_TEXTURE_2D,
GL_TEXTURE_WRAP_S,
GL_CLAMP_TO_EDGE));
GL_CHECK(glTexParameteri(GL_TEXTURE_2D,
GL_TEXTURE_WRAP_T,
GL_CLAMP_TO_EDGE));
GL_CHECK(glTexParameteri(GL_TEXTURE_2D,
GL_TEXTURE_MAG_FILTER,
GL_NEAREST));
GL_CHECK(glTexParameteri(GL_TEXTURE_2D,
GL_TEXTURE_MIN_FILTER,
GL_NEAREST));

bind texture object to specific texture unit and set its property for the pong texture,

/* Prepare pong texture object. */
GL_CHECK(glActiveTexture(GL_TEXTURE0 + pongTextureUnit));
GL_CHECK(glBindTexture (GL_TEXTURE_2D,
GL_CHECK(glTexStorage2D (GL_TEXTURE_2D,
1,
GL_R8UI,
GL_CHECK(glTexParameteri(GL_TEXTURE_2D,
GL_TEXTURE_WRAP_S,
GL_CLAMP_TO_EDGE));
GL_CHECK(glTexParameteri(GL_TEXTURE_2D,
GL_TEXTURE_WRAP_T,
GL_CLAMP_TO_EDGE));
GL_CHECK(glTexParameteri(GL_TEXTURE_2D,
GL_TEXTURE_MAG_FILTER,
GL_NEAREST));
GL_CHECK(glTexParameteri(GL_TEXTURE_2D,
GL_TEXTURE_MIN_FILTER,
GL_NEAREST));

where texture units are being defined as

/* Texture unit used for configuring 2D texture binding for ping textures. */
/* Texture unit used for configuring 2D texture binding for pong textures. */

Please note, that only in the case of one of the textures (the ping one) is it already filled with data, in case of the pong one, there is only a data store being defined. The data used as an input for the ping texture is generated within the generateRule30Input() call.

/* Please see the specification above. */
void generateRule30Input(unsigned int xoffset,
unsigned int width,
unsigned int height,
unsigned int nComponents,
GLvoid** textureData)
{
ASSERT(textureData != NULL, "Null data passed");
for(unsigned int channelIndex = 0; channelIndex < nComponents; ++channelIndex)
{
(*(unsigned char**)textureData)[(height - 1) * width * nComponents + xoffset * nComponents + channelIndex] = 255;
}
}

In the application, we will use a render to texture technique, which is why we need to create a framebuffer object as well. Just to clarify: rendering into texture can be achieved by using (during a draw call) a framebuffer object (with the ID different from 0), to which there is a texture object bound.

GL_CHECK(glGenFramebuffers(1,
GL_CHECK(glBindFramebuffer(GL_DRAW_FRAMEBUFFER,
GL_CHECK(glDrawBuffers (1,
offscreenFBODrawBuffers));

Then, if a framebuffer object is bound,

GL_CHECK(glBindFramebuffer(GL_DRAW_FRAMEBUFFER, framebufferID));

we can call one of the functions shown below to indicate, in which texture object the result of rendering will be stored.

GL_CHECK(glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER,
GL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D,
0) );
GL_CHECK(glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER,
GL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D,
0));

If we are no longer interested in rendering into a texture object and would like the rendering result to be displayed on screen, we need to use the default framebuffer object, which means we have to call

GL_CHECK(glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0));

Program objects

In the application, we are using two program objects: the first one is responsible for generating data accordingly to Rule 30, while the second one is used to merging the calculated data and display the result on screen. The main mechanism of generating program objects looks like the following (the idea is shown for the first program object):

  1. Create program object ID;
    rule30ProgramID = GL_CHECK(glCreateProgram());
  2. Create shader objects' IDs (should be called twice for shaderType equal to GL_FRAGMENT_SHADER and GL_VERTEX_SHADER);
    *shaderObjectIdPtr = GL_CHECK(glCreateShader(shaderType));
  3. Set shader source;
    strings[0] = loadShader(filename);
    GL_CHECK(glShaderSource(*shaderObjectIdPtr, 1, strings, NULL));
  4. Compile shader (it's always a good idea to check, whether the compilation succeeded: GL_COMPILE_STATUS set to GL_TRUE;
    GL_CHECK(glCompileShader(*shaderObjectIdPtr));
    GL_CHECK(glGetShaderiv(*shaderObjectIdPtr, GL_COMPILE_STATUS, &compileStatus));
  5. Attach shaders to program object;
    GL_CHECK(glAttachShader(rule30ProgramID, vertexRule30ShaderID));
    GL_CHECK(glAttachShader(rule30ProgramID, fragmentRule30ShaderID));
  6. Link program object;
    GL_CHECK(glLinkProgram(rule30ProgramID));
  7. Use program.
    GL_CHECK(glUseProgram(rule30ProgramID) );
    The next thing to do is to retrieve locations of the uniforms and attributes that are used in the shaders. This can be achieved as shown below.
rule30ProgramLocations.inputNeighbourLocation = GL_CHECK(glGetUniformLocation(rule30ProgramID, "inputNeighbour") );
rule30ProgramLocations.inputTextureLocation = GL_CHECK(glGetUniformLocation(rule30ProgramID, "inputTexture") );
rule30ProgramLocations.inputVerticalOffsetLocation = GL_CHECK(glGetUniformLocation(rule30ProgramID, "inputVerticalOffset") );
rule30ProgramLocations.mvpMatrixLocation = GL_CHECK(glGetUniformLocation(rule30ProgramID, "mvpMatrix") );
rule30ProgramLocations.positionLocation = GL_CHECK(glGetAttribLocation (rule30ProgramID, "position") );
rule30ProgramLocations.texCoordLocation = GL_CHECK(glGetAttribLocation (rule30ProgramID, "vertexTexCoord") );
rule30ProgramLocations.verticalOffsetLocation = GL_CHECK(glGetUniformLocation(rule30ProgramID, "verticalOffset") );

It's always a good idea to verify whether the attributes and uniforms are considered as active within the program object. This is why we need to check the returned location value (-1 location value means that the attribute or the uniform has not been found).

ASSERT(rule30ProgramLocations.inputNeighbourLocation != -1, "Could not find location of a uniform in rule30 program: inputNeighbour" );
ASSERT(rule30ProgramLocations.inputTextureLocation != -1, "Could not find location of a uniform in rule30 program: inputTexture" );
ASSERT(rule30ProgramLocations.inputVerticalOffsetLocation != -1, "Could not find location of a uniform in rule30 program: inputVerticalOffset");
ASSERT(rule30ProgramLocations.mvpMatrixLocation != -1, "Could not find location of a uniform in rule30 program: mvpMatrix" );
ASSERT(rule30ProgramLocations.positionLocation != -1, "Could not find location of an attribute in rule30 program: position" );
ASSERT(rule30ProgramLocations.texCoordLocation != -1, "Could not find location of an attribute in rule30 program: vertexTexCoord" );
ASSERT(rule30ProgramLocations.verticalOffsetLocation != -1, "Could not find location of a uniform in rule30 program: verticalOffset" );

Now, if you are interested in setting values for the attributes, and we know you are and would like to set quad coordinates and texture UVs, it's enough to prepare a Vertex Array Object.

First of all, create the object

GL_CHECK(glGenVertexArrays(1, &lineVAOID));

and bind it.

GL_CHECK(glBindVertexArray(lineVAOID));

Now, when a vertex array object became active, we can set the input data for it. It's achieved by using array buffer objects.

GL_CHECK(glGenBuffers(4, boIDs));
linePositionBOID = boIDs[0];
lineUVBOID = boIDs[1];
quadPositionBOID = boIDs[2];
quadUVBOID = boIDs[3];

Please note that in this section we are describing only one program object, so we will discuss the usage of the first two buffer objects only (linePositionBOID and lineUVBOID).

Fill one of the buffer objects with quad coordinates data.

/* Fill buffers with line vertices attribute data. */
GL_CHECK(glBindBuffer (GL_ARRAY_BUFFER,
GL_CHECK(glBufferData (GL_ARRAY_BUFFER,
sizeof(lineVertices),
GL_STATIC_DRAW));
GL_CHECK(glVertexAttribPointer (rule30ProgramLocations.positionLocation,
4,
GL_FLOAT,
GL_FALSE,
0,
0));
GL_CHECK(glEnableVertexAttribArray(rule30ProgramLocations.positionLocation));

Fill the second one with texture UVs data.

/* Fill buffers with line U/V attribute data. */
GL_CHECK(glBindBuffer (GL_ARRAY_BUFFER,
GL_CHECK(glBufferData (GL_ARRAY_BUFFER,
GL_STATIC_DRAW));
GL_CHECK(glVertexAttribPointer (rule30ProgramLocations.texCoordLocation,
2,
GL_FLOAT,
GL_FALSE,
0,
0));
GL_CHECK(glEnableVertexAttribArray(rule30ProgramLocations.texCoordLocation));

The important thing you can find above is the fact that we are binding the attribute location with a specific input data set by calling glVertexAttribPointer().

If you would like to use the specific attributes input data during a draw call, you should remember to make the required vertex array object active for the draw call by calling glBindVertexArray().

If you now would want to set data for the uniforms, you should call one of glUniform() family function, as the examples shown below. The function that should be used depends on the uniform type. If the uniform is of a matrix4x4 type, we should call glUniformMatrix4fv(), a single float value: glUniform1f(), a texture sampler: glUniform1i(). If a uniform value is constant during the rendering process, we can set it once, if it's changing we should call the specific glUniform() function every time we want to update the uniform value.

GL_CHECK(glUniformMatrix4fv(rule30ProgramLocations.mvpMatrixLocation,
1,
GL_FALSE,
modelViewProjectionMatrix.getAsArray()));
GL_CHECK(glUniform1f (rule30ProgramLocations.inputNeighbourLocation,
1.0f / windowWidth) );

Perform the rendering

The algorithm is based on the two main steps:

  1. Generating data (rendering into texture)
  2. Rendering textures on the screen

The first step is considered as follows: we want to render a single row then, depending on the first row, we want to draw a second row and so on, till the whole screen is rendered. The problem is that, we cannot read from and render to the same texture object at the same time. This is why we need two texture objects, each of which will store the even or odd lines of data. So it looks like described below: we render the first row and store the result in the ping texture. Then we want to render a second line based on the data in the first one: in the program object we sample the ping texture and based on the retrieved data, we are generating the current line and store the result in the pong texture. Then, during third line rendering, we use pong texture as an input and store data in the ping texture. It seems to be complicated, however the concept is very simple and is shown in the code presented below. Please note that for each iteration, the input and output textures are switched. The input texture is updated by calling glUniform1i() and the output texture by calling glFramebufferTexture2D().

/* Please see the specification above. */
{
/* Offset of the input line passed to the appropriate uniform. */
float inputVerticalOffset = 0.0f;
/* Activate the first program. */
GL_CHECK(glUseProgram(rule30ProgramID));
GL_CHECK(glBindVertexArray(lineVAOID));
/* [Bind the framebuffer object] */
GL_CHECK(glBindFramebuffer(GL_DRAW_FRAMEBUFFER, framebufferID));
/* [Bind the framebuffer object] */
/* Render each line, beginning from the 2nd one, using the input from the previous line.*/
for (unsigned int y = 1; y <= windowHeight; ++y)
{
/* Even lines should be taken from the ping texture, odd from the pong. */
bool isEvenLineBeingRendered = (y % 2 == 0) ? (true) : (false);
/* Vertical offset of the currently rendered line. */
float verticalOffset = (float) y / (float) windowHeight;
/* Pass data to uniforms. */
GL_CHECK(glUniform1f(rule30ProgramLocations.verticalOffsetLocation, verticalOffset));
GL_CHECK(glUniform1f(rule30ProgramLocations.inputVerticalOffsetLocation, inputVerticalOffset));
if (isEvenLineBeingRendered)
{
/* [Bind ping texture to the framebuffer] */
GL_CHECK(glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER,
GL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D,
0) );
/* [Bind ping texture to the framebuffer] */
GL_CHECK(glUniform1i (rule30ProgramLocations.inputTextureLocation,
pongTextureUnit) );
}
else
{
/* [Bind pong texture to the framebuffer] */
GL_CHECK(glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER,
GL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D,
0));
/* [Bind pong texture to the framebuffer] */
GL_CHECK(glUniform1i (rule30ProgramLocations.inputTextureLocation,
pingTextureUnit) );
}
/* Drawing a horizontal line defined by 2 vertices. */
GL_CHECK(glDrawArrays(GL_LINES, 0, 2));
/* Update the input vertical offset after the draw call, so it points to the previous line. */
inputVerticalOffset = verticalOffset;
}
/* Unbind the framebuffer. */
GL_CHECK(glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0));
}

When this step is completed, we are ready to perform the second step. The main idea here is to take those two textures: the ping and the pong ones and merge them as shown below. Please note that the default framebuffer object is made active, which means the result will be shown on screen.

/* Please see the specification above. */
{
/* Activate the second program. */
GL_CHECK(glUseProgram (mergeProgramID));
GL_CHECK(glBindVertexArray(quadVAOID));
/* Draw a quad as a triangle strip defined by 4 vertices. */
GL_CHECK(glDrawArrays(GL_TRIANGLE_STRIP, 0, 4));
}

In the application we wanted to issue the algorithm many times based on the different input data. For the first time, we use the data generated as described in the previous section Prepare Textures. For each following iteration we use the data generated with the algorithm presented below.

/* Please see the specification above. */
void generateRule30Input(unsigned int width,
unsigned int height,
unsigned int nComponents,
GLvoid** textureData)
{
ASSERT(textureData != NULL, "Null data passed");
for (unsigned int texelIndex = 0; texelIndex < width; ++texelIndex)
{
bool setWhite = (rand() % 2 == 0) ? true : false;
if (setWhite)
{
for (unsigned int channelIndex = 0; channelIndex < nComponents; ++channelIndex)
{
(*(unsigned char**)textureData)[(height - 1) * width * nComponents + texelIndex * nComponents + channelIndex] = 255;
}
}
}
}

and substitute the ping texture data with the new one by calling

/* Since texture objects have already been created, we can substitute ping image using glTexSubImage2D call.
* Pong texture does not require reset, because its content depends entirely on the first line of the ping texture. */
GL_CHECK(glActiveTexture(GL_TEXTURE0));
GL_CHECK(glTexSubImage2D(GL_TEXTURE_2D,
0,
0,
0,
GL_RED_INTEGER,
GL_UNSIGNED_BYTE,
IntegerLogic_result.png
The result of first and second pass.