Chapter 11. Textures

1. To use textures, we first need to load them into the application. Using an image-loading library that supports several popular formats looks like a good solution. I like stb_image.h by Sean Barrett. Especially I love that it's a single h-file, not a lib or dll.

You can download it here.


Save it in C:\CPP\engine folder (the same place where we saved linmath.h earlier).


2. Then we'll need an image itself.

Create a folder for it: \dt under C:\CPP\a999hello (this folder should belong to the project, NOT to engine).

IMPORTANT: the image sizes MUST be powers of 2.
The following sample_img.png is 512x256:

You can download it here.


Save it in C:\CPP\a999hello\dt


Windows

3. Start Visual Studio. Open C:\CPP\a999hello\p_windows\p_windows.sln


4. Add new /dt folder to Post-Build xcopy instruction:

Open p_windows project Properties, All Configurations / x86, Build events -> Post-Build Event -> Command Line -> Edit.

Add new line

xcopy "..\dt\*.*" "$(TargetDir)dt\" /E /R /D /y

so now we have 2 xcopy commands.

Ok, Apply, Ok.


5. Open TheGame.cpp. We'll need to change it a bit:

  • We need a reference to stb_image.h.
  • A bit different variables set.
  • Different vertex structure: (3D coordinates + 2D texture coordinates) instead of just coordinates.
  • We will use 4 vertices instead of 3 and GL_TRIANGLE_STRIP primitive instead of GL_TRIANGLES
  • Also we need to implement reading/loading an image and converting it into the texture.
  • And pass texture id and per-vertex texture coordinates to a new shader program.

So, replaceTheGame.cpp code by following:

#include "TheGame.h"
#include "platform.h"
#include "linmath.h"
#include "utils.h"
#include "Shader.h"

extern std::string filesRoot;

#define STB_IMAGE_IMPLEMENTATION  //required by stb_image.h
#include "stb_image.h"

static const struct
{
    float x, y, z, tu, tv;
} vertices[4] =
{
    { -0.5f,  0.5f, 0.f, 0.f, 0.f }, //top-left
    { -0.5f, -0.5f, 0.f, 0.f, 1.f }, //bottom-left
    {  0.5f,  0.5f, 0.f, 1.f, 0.f }, //top-right
    {  0.5f, -0.5f, 0.f, 1.f, 1.f }  //bottom-right
};
unsigned int vao_buffer, vertex_buffer, shaderProgramId, textureId;
int umvp_location, apos_location, atuv_location;
float angle_z = 0;

int TheGame::getReady() {
    bExitGame = false;

    glGenVertexArrays(1, &vao_buffer);
    glBindVertexArray(vao_buffer);

    glGenBuffers(1, &vertex_buffer);
    glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    shaderProgramId = Shader::linkShaderProgram((filesRoot + "/dt/shaders/flat_tex_v.txt").c_str(), (filesRoot + "/dt/shaders/flat_tex_f.txt").c_str());

    umvp_location = glGetUniformLocation(shaderProgramId, "uMVP");

    apos_location = glGetAttribLocation(shaderProgramId, "aPos");
    glEnableVertexAttribArray(apos_location);
    glVertexAttribPointer(apos_location, 3, GL_FLOAT, GL_FALSE,
        sizeof(vertices[0]), (void*)0);

    atuv_location = glGetAttribLocation(shaderProgramId, "aTuv");
    glEnableVertexAttribArray(atuv_location);
    glVertexAttribPointer(atuv_location, 2, GL_FLOAT, GL_FALSE,
        sizeof(vertices[0]), (void*)(sizeof(float) * 3));

    // loading an image
    int imgWidth, imgHeight, nrChannels;
    unsigned char* imgData = stbi_load((filesRoot + "/dt/sample_img.png").c_str(),
        &imgWidth, &imgHeight, &nrChannels, 4); //"4"-convert to 4 channels -RGBA
    if (imgData == NULL) {
        mylog("ERROR loading image\n");
    }
    // generate texture
    unsigned int textureId;
    glGenTextures(1, &textureId);
    glBindTexture(GL_TEXTURE_2D, textureId);
    // set the texture wrapping/filtering options (on the currently bound texture object)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    // attach/load image data
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, imgWidth, imgHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, imgData);
    glGenerateMipmap(GL_TEXTURE_2D);
    // release image data
    stbi_image_free(imgData);

    //pass textureId to shader program
    glActiveTexture(GL_TEXTURE0); // activate the texture unit first before binding texture
    glBindTexture(GL_TEXTURE_2D, textureId);

    return 1;
}
int TheGame::drawFrame() {
    myPollEvents();

    mat4x4 m, p, mvp;

    //glClearColor(0.0, 0.0, 0.5, 1.0);
    glClear(GL_COLOR_BUFFER_BIT);
    angle_z += 0.01f;
    mat4x4_identity(m);
    mat4x4_rotate_Z(m, m, angle_z);
    mat4x4_scale_aniso(m, m, 2.0, 1.0, 1.0);
    mat4x4_ortho(p, -screenRatio, screenRatio, -1.f, 1.f, 1.f, -1.f);
    mat4x4_mul(mvp, p, m);

    glUseProgram(shaderProgramId);
    glUniformMatrix4fv(umvp_location, 1, GL_FALSE, (const GLfloat*)mvp);

    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

    mySwapBuffers();
    return 1;
}
int TheGame::cleanUp() {
    return 1;
}
int TheGame::onScreenResize(int width, int height) {
    if (screenSize[0] == width && screenSize[1] == height)
        return 0;
    screenSize[0] = width;
    screenSize[1] = height;
    screenRatio = (float)width / (float)height;
    glViewport(0, 0, width, height);
    mylog(" screen size %d x %d\n", width, height);
    return 1;
}
int TheGame::run() {
    getReady();
    while (!bExitGame) {
        drawFrame();
    }
    cleanUp();
    return 1;
}


  1. And, of course, new shaders:

Vertex shader:

#version 320 es
precision lowp float;
uniform mat4 uMVP; // transform matrix (Model-View-Projection)
in vec3 aPos; //attribute position (3D coordinates)
in vec2 aTuv; //attribute TUV (texture coordinates)
out vec2 vTuv; //varying TUV (pass to fragment shader)
void main(){
  gl_Position = uMVP * vec4(aPos, 1.0);
  vTuv = aTuv;
}

Copy-paste this code to a text editor and save it as a txt file in

C:\CPP\engine\dt\shaders\flat_tex_v.txt

Fragment shader:

#version 320 es
precision lowp float;
uniform sampler2D uTex0;  //texture id
in vec2 vTuv; //varying TUV (passed from vertex shader)
out vec4 FragColor; // output pixel color
void main(){
  FragColor = texture(uTex0, vTuv);
}

Copy-paste this code to a text editor and save it as a txt file in

C:\CPP\engine\dt\shaders\flat_tex_f.txt


7. Build and run. Result:


Now - on

Android

This time ALL code changes were on a cross-platform side, so we don't even need to change anything… Except stb_image.h and xcopy post-build instruction.

8. Re-start Visual Studio. Open C:\CPP\a999hello\p_android\p_android.sln.


9. Go to p_android.NativeActivity project Properties, All Configurations / ARM64, Build Events -> Post-Build Event -> Command Line -> Edit

Add new line:

xcopy "..\..\dt\*.*" "..\$(RootNamespace).Packaging\assets\dt\" /E /R /D /y

Ok, Apply, Ok.


9. Now, right-click on xEngine, Add -> Existing Item,

C:\CPP\engine\stb_image.h

Add.


10. Plug in Android (as usual).

Re-build and run.

Nice, just as expected.


BTW, if you are a newbie to programming, now you can modestly indicate in your resume that you are fluent in:

  • C++
  • graphics
  • 3D
  • Visual Studio
  • Cross-platform
  • Android
  • Windows
  • OpenGL ES
  • GLSL

Well, maybe "fluent" is still an exaggeration at this point, but everything else is a pure truth.


Leave a Reply

Your email address will not be published. Required fields are marked *