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.
- 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.