Глава 10. Простой шейдер

Наконец, шейдеры! Начнем с самого простого, просто заливка сплошным цветом, который мы передадим шейдеру в качестве инпута вместе с данными повехности (в нашем примере – треугольника).

Шейдеры пишутся на GLSL (OpenGL Shading Language с синтаксисом похожим на C). GLSL исполняется на граф-карте.

Исполняемая шейдер-программа состоит из 2-х шейдеров: Vertex Shader, который обрабатывает данные вешин (включая координаты) и преобразует их в экранные координаты. Выходные данные идут в rasterizer, который вызывает Fragment (Pixel) Shader для каждого вовлеченного пикселя.

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

Скопируем этот код в Текстовый Редактор и сохраним как txt файл в

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

Надеюсь, объяснять его не надо, все вроде достаточно очевидно. Ну или можно обратиться к GLSL документации на Вебе.

  • Файл test0.txt можно оттуда удалить. Больше он не понадобится.

2. Fragment (Pixel) Shader:

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

Сохраним этот код в качестве txt файла в

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


Теперь давайте их опробуем.

Windows

3. Запускаем VS. Открываем C:\CPP\a999hello\p_windows\p_windows.sln.


Во-первых, понадобится функция проверки на предмет GL ошибок. Поместим ее в отдельный набор utils.

4. Под xEngine добавим New Item

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

Код:

#pragma once
#include <string>

int checkGLerrors(std::string ref);


5. Под xEngine добавим New Item

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

Код:

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


Shaders-related код поместим в отдельном классе Shaders.

6. Под xEngine добавим New Item

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

Код:

#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. Под xEngine добавим New Item

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

Код:

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


Заметим, что в функции compileShader(..) для открытия файла мы использовали собственную функцию myFopen_s (строка 31, тот же синтакс как fopen_s). Причина в том, что в Windows-е привычная fopen объявлена устаревшей, надо использовать fopen_s, в то время как на Андроиде fopen_s еще не имплементирована, и надо использовать fopen. Вобщем, нужны 2 platform-specific имплементации.

8. Открываем platform.h и заменяем код следующим:

#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. Открываем platform.cpp и заменяем код следующим:

#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. Теперь откроем TheGame.cpp и заменим код на:

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

Теперь он чуть-ли не вдвое короче.

Важные изменения:

  • Встроенных в код шейдеров vertex_shader_text и fragment_shader_text больше нет.
  • Переменные vertex_shader и fragment_shader тоже больше не нужны.
  • Структура данных треугольника теперь другая. Вместо 2D координат + цвет теперь просто 3D координаты, без цвета.
  • Некоторые переменные переименованы для единообразия.


11. Компиляция и запуск:

Отлично.


Теперь на

Андроиде

12. Закрываем и перезапускаем Visual Studio, открываем C:\CPP\a999hello\p_android\p_android.sln


13. Под xEngine добавляем Existing Item

Идем в C:\CPP\engine, выбираем

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

Add


Нужно еще добавить myFopen_s(..) для Андроида.

14. Откроем platform.h и заменим код на:

#pragma once

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


15. Откроем platform.cpp и заменим код на:

#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. Включаем, разблокируем, подключаем, разрешаем.

Компиляция и запуск.

Норм.


Leave a Reply

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