From a61e07bd54310abcfb98b519f6343f25b9a6d281 Mon Sep 17 00:00:00 2001 From: Lephenixnoir Date: Sat, 28 Oct 2023 13:37:36 +0200 Subject: [PATCH] gl: WIP shader abstraction --- azur/include/azur/gl/gl.h | 3 + azur/include/azur/gl/shaders.h | 330 +++++++++++++++++++++++++++++++++ azur/src/gl/util.cpp | 18 +- 3 files changed, 342 insertions(+), 9 deletions(-) create mode 100644 azur/include/azur/gl/shaders.h diff --git a/azur/include/azur/gl/gl.h b/azur/include/azur/gl/gl.h index 43cc0b0..fe01ba8 100644 --- a/azur/include/azur/gl/gl.h +++ b/azur/include/azur/gl/gl.h @@ -53,6 +53,9 @@ GLuint compileShaderFile(GLenum type, char const *path); Returns the shader ID, 0 in case of error. Errors are logged. */ GLuint compileShaderSource(GLenum type, char const *code, ssize_t size); +/* Link a program. This function takes an array of shader IDs. */ +GLuint link(GLuint *shaders, int count); + /* Link a program. This function takes a 0-terminated list of shaders IDs and links them into a new program. Returns the new program's ID, or 0 in case of error. Errors are logged. */ diff --git a/azur/include/azur/gl/shaders.h b/azur/include/azur/gl/shaders.h new file mode 100644 index 0000000..0a8c4cf --- /dev/null +++ b/azur/include/azur/gl/shaders.h @@ -0,0 +1,330 @@ +//---------------------------------------------------------------------------// +// ," /\ ", Azur: A game engine for CASIO fx-CG and PC // +// | _/__\_ | Designed by Lephe' and the Planète Casio community. // +// "._`\/'_." License: MIT // +//---------------------------------------------------------------------------// +// azur.gl.shaders: Abstraction of OpenGL 3.3 program objects +// +// This header provides the ShaderProgram class as an abstraction for OpenGL +// shaders. Each shader comes with its own uniform parameters, vertex +// attributes, textures, and potentially other settings; ie., it encapsulates +// all of the OpenGL state needed to properly run the program. +// +// ShaderProgram provides the generic mechanisms for using a shader (ie. it has +// all the downwards-facing interfaces), but it only exposes a low-level vertex +// buffer for describing draw commands. Hence, the intended way to use it is to +// subclass it for each shader, and add shader-specific high-level drawing +// functions to the derived class. +// TODO: Mention how drawing functions add new vertices to the VAO +// Access to low-level OpenGL components is provided in case advanced GL things +// are needed, but that shouldn't be the case for basic use. +// +// The initialization of a ShaderProgram goes as follows: +// 1. Create a ShaderProgram object (directly or through a derived class); +// 2. Load code with one or more calls to addSourceFile(), addSourceText(); +// 3. Compile the program with compile(). +// +// At this stage, if no error occurred the shader has source code and its +// static configuration can be set: +// 4. Specify the attribute format (see below). +// 5. Load relevant textures or state that are the same for all frames. +// +// Once that's done, the shader can be used in rendering frames: +// 6. Set up frame-specific uniforms, resources, etc. +// 7. Queue draw calls. +// +// TODO: Provide a ShaderPipeline for automatically switching between programs +// and checking whether the flow is correct. +// +// The attribute format describes per-vertex data in the form of a structure +// layout, and is basically a meta-level reflection of the structure type used +// for VertexAttr. It tells OpenGL how to read the raw vertex data buffer. +//--- + +#pragma once +#include +#include +#include +#include +#include + +namespace azur::gl { + +template +class ShaderProgram +{ +public: + /* Create an empty shader program with no code and no settings. This is an + inert call which does nothing, and especially no OpenGL calls. */ + ShaderProgram() = default; + ~ShaderProgram(); + + /* OpenGL objects cannot be copied, and we don't care about moving yet. */ + ShaderProgram(ShaderProgram const &) = delete; + ShaderProgram(ShaderProgram &&) = delete; + + /*** Initialization ***/ + + /* Initialize the shader by allocating some OpenGL objects. */ + void init(); + + /* Add a piece of source code taken from a file. */ + bool addSourceFile(GLuint type, std::string const &file); + /* Add a piece of source given directly as a string. */ + void addSourceText(GLuint type, std::string const &code) + { + m_code[type] += code; + } + void addSourceText(GLuint type, char const *code, ssize_t size = -1) + { + m_code[type] += std::string {code, size >= 0 ? size : strlen(code)}; + } + + /* Compile all provided code. */ + bool compile(); + + /* Check whether the program has been compiled successfully. */ + bool isCompiled() const + { + return (m_prog != 0); + } + + /*** Configuration ***/ + + /* Set uniforms. */ + void setUniform(char const *name, float f); + void setUniform(char const *name, float f1, float f2); + void setUniform(char const *name, float f1, float f2, float f3); + void setUniform(char const *name, glm::vec2 const &v2); + void setUniform(char const *name, glm::vec3 const &v3); + void setUniform(char const *name, glm::vec4 const &v4); + void setUniform(char const *name, glm::mat2 const &m2); + void setUniform(char const *name, glm::mat3 const &m3); + void setUniform(char const *name, glm::mat4 const &m4); + + /*** Generating draw commands ***/ + + /* Add a vertex to the list of vertices for this frame. */ + void addVertex(VertexAttr &&attr) + { + m_vertices.push_pack(std::move(attr)); + } + + /* Add three new vertices (makes a triangle in GL_TRIANGLES mode). */ + void addVertexTriangle( + VertexAttr const &a1, VertexAttr const &a2, VertexAttr const &a3) + { + m_vertices.push_back(a1); + m_vertices.push_back(a2); + m_vertices.push_back(a3); + } + + /* Add two side-sharing triangles (makes a quad in GL_TRIANGLES mode). + VertexAttr must be copyable. */ + void addVertexQuad(VertexAttr const &a1, VertexAttr const &a2, + VertexAttr const &a3, VertexAttr const &a4) + { + m_vertices.push_back(a1); + m_vertices.push_back(a2); + m_vertices.push_back(a3); + m_vertices.push_back(a2); + m_vertices.push_back(a3); + m_vertices.push_back(a4); + } + + /* Direct access to vertex list, to facilitate faster methods of building a + vertex list for any given frame. */ + std::vector &vertices() + { + return m_vertices; + } + + /* Queue a draw call. Indices should be within the range of vertices pushed + to the VAO at the time of the call. Info for this call is kept in memory + until the rendering phase, so the VAO and the order should be valid + until the end of the frame. + + With queueDrawArrays, the vertices at positions [first .. first+count) + of the VBO are used to render. With queueDrawElements, the vertices at + positions ind[0], .., ind[count-1] are used. Depending on mode, these + are combined in pairs, triples, or other ways to render primitives. */ + void queueDrawArrays(GLenum mode, GLint first, GLsizei count); + void queueDrawElements(GLenum mode, GLsizei count, uint8_t const *ind); + void queueDrawElements(GLenum mode, GLsizei count, uint16_t const *ind); + void queueDrawElements(GLenum mode, GLsizei count, uint32_t const *ind); + + /*** Executing commands ***/ + + /* Load buffer data; this is usually executed once at the beginning of the + frame. (This is normally called by ShaderPipeline::render().) */ + void loadBuffer(); + + /* Switch to this shader program. This should be called before executing a + draw command if the previous shader to run was different. (This is + normally called by ShaderPipeline::render().) */ + void useProgram(); + + +protected: + /* Program ID */ + GLuint m_prog = 0; + /* Vertex Array Object and Vertex Buffer Object with parameters */ + GLuint m_vao = 0, m_vbo = 0; + /* Size of the VBO on the GPU */ + size_t m_vboSize = 0; + +private: + /* Map from program type (vertex/etc/fragment shader) to code during the + construction phase. */ + std::map m_code; + /* List of vertices during rendering. */ + std::vector m_vertices; +}; + +template +void ShaderProgram::init() +{ + glGenVertexArrays(1, &m_vao); + glGenBuffers(1, &m_vbo); +} + +template +ShaderProgram::~ShaderProgram() +{ + glDeleteProgram(m_prog); + glDeleteBuffers(1, &m_vbo); + glDeleteVertexArrays(1, &m_vao); +} + +template +bool ShaderProgram::addSourceFile( + GLuint type, std::string const &file) +{ + extern char *load_file(char const *, size_t *); + char *data = load_file(file.c_str(), nullptr); + if(!data) + return false; + + m_code[type] += data; + delete[] data; + return true; +} + +template +bool ShaderProgram::compile() +{ + /* List of shader descriptors obtained from OpenGL. */ + std::vector m_shaders; + bool success = true; + + for(auto const &[type, code]: m_code) { + GLuint id = compileShaderSource(type, code.c_str(), -1); + if(id != 0) + m_shaders.push_back(id); + else + success = false; + } + + // TODO: No link error detection? + m_prog = success ? link(m_shaders.data(), m_shaders.size()) : 0; + + for(auto id: m_shaders) + glDeleteShader(id); + + return success; +} + +/* TODO: Complete state associated with each program: + 1. Uniforms (glUniform*) + 2. Vertex array (glBindVertexArray) + 3. Textures (glActiveTexture, glBindTexture) + 4. Other state (glEnable, glDisable, glBlendFunc) + Render with glDrawArrays or glDrawElements. + + TODO: Can we minimize switching (eg. texture switching)? */ + +template +void ShaderProgram::setUniform(char const *name, float f) +{ + glUniform1f(glGetUniformLocation(m_prog, name), f); +} + +template +void ShaderProgram::setUniform(char const *name, float f1, float f2) +{ + glUniform2f(glGetUniformLocation(m_prog, name), f1, f2); +} + +template +void ShaderProgram::setUniform( + char const *name, float f1, float f2, float f3) +{ + glUniform3f(glGetUniformLocation(m_prog, name), f1, f2, f3); +} + +template +void ShaderProgram::setUniform(char const *name, glm::vec2 const &v2) +{ + glUniform2fv(glGetUniformLocation(m_prog, name), 1, &v2[0]); +} + +template +void ShaderProgram::setUniform(char const *name, glm::vec3 const &v3) +{ + glUniform3fv(glGetUniformLocation(m_prog, name), 1, &v3[0]); +} + +template +void ShaderProgram::setUniform(char const *name, glm::vec4 const &v4) +{ + glUniform4fv(glGetUniformLocation(m_prog, name), 1, &v4[0]); +} + +template +void ShaderProgram::setUniform(char const *name, glm::mat2 const &m2) +{ + glUniformMatrix2fv( + glGetUniformLocation(m_prog, name), 1, GL_FALSE, &m2[0][0]); +} + +template +void ShaderProgram::setUniform(char const *name, glm::mat3 const &m3) +{ + glUniformMatrix3fv( + glGetUniformLocation(m_prog, name), 1, GL_FALSE, &m3[0][0]); +} + +template +void ShaderProgram::setUniform(char const *name, glm::mat4 const &m4) +{ + glUniformMatrix4fv( + glGetUniformLocation(m_prog, name), 1, GL_FALSE, &m4[0][0]); +} + +template +void ShaderProgram::loadBuffer() +{ + glBindBuffer(GL_ARRAY_BUFFER, m_vbo); + + /* If the size of the VBO is too small or much larger than needed, resize + it; otherwise, simply swap the data without reallocating */ + if(m_vboSize < m_vertices.size() || m_vboSize > m_vertices.size() * 4) { + glBufferData(GL_ARRAY_BUFFER, sizeof(T) * m_vertices.size(), + m_vertices.data(), GL_DYNAMIC_DRAW); + m_vboSize = m_vertices.size(); + } + else { + glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(T) * m_vertices.size(), + m_vertices.data()); + } +} + +template +void ShaderProgram::useProgram() +{ + glUseProgram(m_prog); + glBindVertexArray(m_vao); + glBindBuffer(GL_ARRAY_BUFFER, m_vbo); +} + +} /* namespace azur::gl */ diff --git a/azur/src/gl/util.cpp b/azur/src/gl/util.cpp index 8ffde87..1ab0e77 100644 --- a/azur/src/gl/util.cpp +++ b/azur/src/gl/util.cpp @@ -13,10 +13,17 @@ #include #include +extern char const *azur_glsl__vs_prelude_gles2; +extern char const *azur_glsl__vs_prelude_gl3; +extern char const *azur_glsl__fs_prelude_gles2; +extern char const *azur_glsl__fs_prelude_gl3; + +namespace azur::gl { + /* Read the full contents of a file into the heap. Returns an malloc'd pointer on success, NULL if an error occurs. */ // TODO: Move the load_file() function to a more convenient fs util header -static char *load_file(char const *path, size_t *out_size) +char *load_file(char const *path, size_t *out_size) { char *contents = NULL; long size = 0; @@ -40,13 +47,6 @@ load_file_end: return contents; } -extern char const *azur_glsl__vs_prelude_gles2; -extern char const *azur_glsl__vs_prelude_gl3; -extern char const *azur_glsl__fs_prelude_gles2; -extern char const *azur_glsl__fs_prelude_gl3; - -namespace azur::gl { - char const *errorString(GLenum ec) { static char str[32]; @@ -157,7 +157,7 @@ GLuint compileShaderSource(GLenum type, char const *code, ssize_t size) return compileShader(type, code, size, ""); } -static GLuint link(GLuint *shaders, int count) +GLuint link(GLuint *shaders, int count) { GLuint prog = glCreateProgram(); if(prog == 0) {