Chapter 10. Simple shader

Finally, shaders! Let's start from the easiest one, just flat uniform color, which we will pass to the shader as an input, along with mesh data (in our sample - a triangle).

Shaders use GLSL (OpenGL Shading Language with syntax similar to C). GLSL is executed directly by the graphics pipeline.

The executable shader program consists of 2 shaders: Vertex Shader, which processes vertex data and transforms vertex (points) positions into 3D drawing coordinates. Output goes to rasterizer, which will call Fragment (Pixel) Shader for every involved pixel.

  1. Vertex Shader:
#version 320 es
precision lowp float;
uniform mat4 uMVP; // transform matrix (Model-View-Projection)
in vec3 aPos; // position attribute (x,y,z)
void main(){
  gl_Position = uMVP * vec4(aPos, 1.0);
}

Copy this code to a Text Editor and save it as a txt file in

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

Hope, there is no need to explain what it does, guess it's obvious enough. Or you can check the GLSL documentation online.

  • Ok to delete test0.txt from there. We won't need it any more.

2. Fragment (Pixel) Shader:

#version 320 es
precision lowp float;
uniform vec4 uColor;
out vec4 FragColor; //output pixel color
void main(){
  FragColor = uColor;
}

Save this code as a txt file in

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


Now let's try to use them.

Windows

3. Start VS. Open C:\CPP\a999hello\p_windows\p_windows.sln.


First, we'll need a function checking for GL errors. We'll place it in a separate utils set.

4. Under xEngine add New Item

  • Header File (.h)
  • Name: utils.h
  • Location: C:\CPP\engine\

Code:

#pragma once
#include <string>

int checkGLerrors(std::string ref);


5. Under xEngine add New Item

  • C++ File (.cpp)
  • Name: utils.cpp
  • Location: C:\CPP\engine\

Code:

#include "platform.h"
#include <string>

int checkGLerrors(std::string ref) {
    //can be used after any GL call
    int res = glGetError();
    if (res == 0)
        return 0;
    std::string errCode;
    switch (res) {
        //case GL_NO_ERROR: errCode = "GL_NO_ERROR"; break;
        case GL_INVALID_ENUM: errCode = "GL_INVALID_ENUM"; break;
        case GL_INVALID_VALUE: errCode = "GL_INVALID_VALUE"; break;
        case GL_INVALID_OPERATION: errCode = "GL_INVALID_OPERATION"; break;
        case GL_INVALID_FRAMEBUFFER_OPERATION: errCode = "GL_INVALID_FRAMEBUFFER_OPERATION"; break;
        case GL_OUT_OF_MEMORY: errCode = "GL_OUT_OF_MEMORY"; break;
        default: errCode = "??"; break;
    }
    mylog("GL ERROR %d-%s in %s\n", res, errCode.c_str(), ref.c_str());
    return -1;
}


We'll place shaders-related code in a separate Shaders class.

6. Under xEngine add New Item

  • Header File (.h)
  • Name: Shader.h
  • Location: C:\CPP\engine\

Code:

#pragma once
#include "platform.h"
#include <string>

class Shader
{
public:
	static int linkShaderProgram(const char* filePathVertexS, const char* filePathFragmentS);
	static int compileShader(const char* filePath, GLenum shaderType);

	static int shaderErrorCheck(int shaderId, std::string ref);
	static int programErrorCheck(int programId, std::string ref);
};


7. Under xEngine add New Item

  • C++ File (.cpp)
  • Name: Shader.cpp
  • Location: C:\CPP\engine\

Code:

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

GLchar infoLog[1024];
int logLength;
int Shader::shaderErrorCheck(int shaderId, std::string ref) {
    //use after glCompileShader()
    if (checkGLerrors(ref) > 0)
        return -1;
    glGetShaderInfoLog(shaderId, 1024, &logLength, infoLog);
    if (logLength == 0)
        return 0;
    mylog("%s shader infoLog:\n%s\n", ref.c_str(), infoLog);
    return -1;
}
int Shader::programErrorCheck(int programId, std::string ref) {
    //use after glLinkProgram()
    if (checkGLerrors(ref) > 0)
        return -1;
    glGetProgramInfoLog(programId, 1024, &logLength, infoLog);
    if (logLength == 0)
        return 0;
    mylog("%s program infoLog:\n%s\n", ref.c_str(), infoLog);
    return -1;
}

int Shader::compileShader(const char* filePath, GLenum shaderType) {
    int shaderId = glCreateShader(shaderType);
    FILE* pFile;
    myFopen_s(&pFile, filePath, "rt");
    if (pFile != NULL)
    {
        // obtain file size:
        fseek(pFile, 0, SEEK_END);
        int fSize = ftell(pFile);
        rewind(pFile);
        // size obtained, create buffer
        char* shaderSource = new char[fSize + 1];
        fSize = fread(shaderSource, 1, fSize, pFile);
        shaderSource[fSize] = 0;
        fclose(pFile);
        // source code loaded, compile
        glShaderSource(shaderId, 1, (const GLchar**)&shaderSource, NULL);
        //myglErrorCheck("glShaderSource");
        glCompileShader(shaderId);
        if (shaderErrorCheck(shaderId, "glCompileShader") < 0)
            return -1;
        delete[] shaderSource;
    }
    else {
        mylog("ERROR loading %s\n", filePath);
        return -1;
    }
    return shaderId;
}
int Shader::linkShaderProgram(const char* filePathVertexS, const char* filePathFragmentS) {
    int vertexShaderId = compileShader(filePathVertexS, GL_VERTEX_SHADER);
    int fragmentShaderId = compileShader(filePathFragmentS, GL_FRAGMENT_SHADER);
    int programId = glCreateProgram();
    glAttachShader(programId, vertexShaderId);
    glAttachShader(programId, fragmentShaderId);
    glLinkProgram(programId);
    if (programErrorCheck(programId, "glLinkProgram") < 0)
        return -1;
    //don't need shaders any longer - detach and delete them
    glDetachShader(programId, vertexShaderId);
    glDetachShader(programId, fragmentShaderId);
    glDeleteShader(vertexShaderId);
    glDeleteShader(fragmentShaderId);
    return programId;
}


Please note, that in compileShader(..) function, for opening a file, we are using our own custom function myFopen_s (line 31, same syntax as fopen_s). The reason is: in Windows old-fashioned fopen is deprecated, must use fopen_s, while on Android fopen_s is not implemented yet, must use fopen. So, we need 2 platform-specific implementations.

8. Open platform.h and replace code by following:

#pragma once
#include <glad/glad.h>
#include <stdio.h>

void mylog(const char* _Format, ...);
void mySwapBuffers();
void myPollEvents();
int myFopen_s(FILE** pFile, const char* filePath, const char* mode);


9. Open platform.cpp and replace code by following:

#include <stdarg.h>
#include <stdio.h>
#include <GLFW/glfw3.h>
#include "platform.h"
#include "TheGame.h"

extern GLFWwindow* myMainWindow;
extern TheGame theGame;

void mylog(const char* _Format, ...) {
#ifdef _DEBUG
    va_list _ArgList;
    va_start(_ArgList, _Format);
    vprintf(_Format, _ArgList);
    va_end(_ArgList);
#endif
};
void mySwapBuffers() {
    glfwSwapBuffers(myMainWindow);
}
void myPollEvents() {
    glfwPollEvents();
    //check if closing the window
    theGame.bExitGame = glfwWindowShouldClose(myMainWindow);
    //check screen size
    int width, height;
    glfwGetFramebufferSize(myMainWindow, &width, &height);
    theGame.onScreenResize(width, height);
}
int myFopen_s(FILE** pFile, const char* filePath, const char* mode) {
    return fopen_s(pFile, filePath, mode);
}


10. Now, open TheGame.cpp and replace code by following:

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

extern std::string filesRoot;

static const struct
{
    float x, y, z;
} vertices[3] =
{
    { -0.6f, -0.4f, 0.f },
    {  0.6f, -0.4f, 0.f },
    {   0.f,  0.6f, 0.f }
};

unsigned int vao_buffer, vertex_buffer, shaderProgramId;
int umvp_location, apos_location, ucol_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_ucolor_v.txt").c_str(), (filesRoot + "/dt/shaders/flat_ucolor_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);

    ucol_location = glGetUniformLocation(shaderProgramId, "uColor");

    glUseProgram(shaderProgramId);

    float uColor[4] = { 1.f, 0.f, 1.f, 1.f }; //R,G,B, alpha
    glUniform4fv(ucol_location, 1, uColor);

    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_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_TRIANGLES, 0, 3);

    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;
}

Now it's almost twice shorter.

Important changes:

  • No more hard-coded inline shaders vertex_shader_text and fragment_shader_text.
  • Don't need variables vertex_shader and fragment_shader either.
  • Triangle vertex data structure is different. Instead of (2D coordinates + color) now we have (3D coordinates, no color).
  • Few variables renamed for the sake of consistency.


11. Build and run:

Perfect.


Now - on

Android

12. Close and re-start Visual Studio, open C:\CPP\a999hello\p_android\p_android.sln


13. Under xEngine add Existing Item

Go to C:\CPP\engine, pick

  • Shader.cpp
  • Shader.h
  • utils.cpp
  • utils.h

Add


Need to add Android's myFopen_s(..) implementation.

14. Open platform.h and replace code by following:

#pragma once

void mylog(const char* _Format, ...);
void mySwapBuffers();
void myPollEvents();
int myFopen_s(FILE** pFile, const char* filePath, const char* mode);


15. Open platform.cpp and replace code by following:

#include <android/log.h>
#include "stdio.h"
#include "TheGame.h"

extern struct android_app* androidApp;
extern const ASensor* accelerometerSensor;
extern ASensorEventQueue* sensorEventQueue;

extern EGLDisplay androidDisplay;
extern EGLSurface androidSurface;
extern TheGame theGame;

void mylog(const char* _Format, ...) {
#ifdef _DEBUG
    char outStr[1024];
    va_list _ArgList;
    va_start(_ArgList, _Format);
    vsprintf(outStr, _Format, _ArgList);
    __android_log_print(ANDROID_LOG_INFO, "mylog", outStr, NULL);
    va_end(_ArgList);
#endif
};

void mySwapBuffers() {
	eglSwapBuffers(androidDisplay, androidSurface);
}
void myPollEvents() {
	// Read all pending events.
	int ident;
	int events;
	struct android_poll_source* source;

	// If not animating, we will block forever waiting for events.
	// If animating, we loop until all events are read, then continue
	// to draw the next frame of animation.
	while ((ident = ALooper_pollAll(0, NULL, &events,
		(void**)&source)) >= 0) {

		// Process this event.
		if (source != NULL) {
			source->process(androidApp, source);
		}

		// If a sensor has data, process it now.
		if (ident == LOOPER_ID_USER) {
			if (accelerometerSensor != NULL) {
				ASensorEvent event;
				while (ASensorEventQueue_getEvents(sensorEventQueue,
					&event, 1) > 0) {
					//LOGI("accelerometer: x=%f y=%f z=%f",
					//	event.acceleration.x, event.acceleration.y,
					//	event.acceleration.z);
				}
			}
		}

		// Check if we are exiting.
		if (androidApp->destroyRequested != 0) {
			theGame.bExitGame = true;
			break;
		}
	}
}
int myFopen_s(FILE** pFile, const char* filePath, const char* mode) {
	*pFile = fopen(filePath, mode);
	if (*pFile == NULL) {
		mylog("ERROR: can't open file %s\n", filePath);
		return -1;
	}
	return 1;
}


16. Turn on, unlock, plug in, allow.

Build and run.

Good.


Leave a Reply

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