467 lines
15 KiB
C++
467 lines
15 KiB
C++
//---------------------------------------------------------------------------//
|
|
// ," /\ ", Azur: A game engine for CASIO fx-CG and PC //
|
|
// | _/__\_ | Designed by Lephe' and the Planète Casio community. //
|
|
// "._`\/'_." License: MIT <https://opensource.org/licenses/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 <azur/gl/gl.h>
|
|
#include <azur/log.h>
|
|
#include <glm/glm.hpp>
|
|
#include <string.h>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <map>
|
|
|
|
namespace azur::gl {
|
|
|
|
/* 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<typename VertexAttr>
|
|
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();
|
|
/* Link compiled code (usually after binding attribute locations). */
|
|
bool link();
|
|
|
|
/* Check whether the program has been linked successfully. */
|
|
bool isCompiled() const
|
|
{
|
|
return (m_prog != 0) && m_linked;
|
|
}
|
|
|
|
/*** Configuration ***/
|
|
|
|
/* Get an attribute's location, with caching. */
|
|
GLuint getAttribute(char const *name);
|
|
|
|
/* Meta-programming trick to get the offset of a field within a structure.
|
|
Should only be used with POD VertexAttr types. */
|
|
template<typename U>
|
|
constexpr void *offsetOf(U VertexAttr::*x) {
|
|
VertexAttr t {};
|
|
return (void *)((char *)&(t.*x) - (char *)&t);
|
|
}
|
|
|
|
/* Bind a VertexAttr field of arbitrary type U to a vertex attribute using
|
|
glVertexAttribPointer(). */
|
|
template<typename U>
|
|
void bindVertexAttributeFP(U VertexAttr::*x, GLint size, GLenum type,
|
|
GLboolean normalized, GLuint id) {
|
|
glEnableVertexAttribArray(id);
|
|
glVertexAttribPointer(id, size, type, normalized, sizeof(VertexAttr),
|
|
offsetOf(x));
|
|
}
|
|
|
|
/* Same for integer attributes, using glVertexAttribIPointer(). */
|
|
template<typename U>
|
|
void bindVertexAttributeInt(
|
|
U VertexAttr::*x, GLint size, GLenum type, GLuint id) {
|
|
glVertexAttribIPointer(
|
|
id, size, type, sizeof(VertexAttr), offsetOf(x));
|
|
}
|
|
|
|
/* Shortcuts for binding attributes of common types, which automatically
|
|
provide the type details. */
|
|
void bindVertexAttribute(float VertexAttr::*x, GLuint id);
|
|
void bindVertexAttribute(glm::vec2 VertexAttr::*x, GLuint id);
|
|
void bindVertexAttribute(glm::vec3 VertexAttr::*x, GLuint id);
|
|
void bindVertexAttribute(glm::vec4 VertexAttr::*x, GLuint id);
|
|
|
|
/* 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<VertexAttr> &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;
|
|
bool m_linked = false;
|
|
/* 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<GLuint, std::string> m_code;
|
|
/* List of shader descriptors between compiling and linking. */
|
|
std::vector<GLuint> m_shaders;
|
|
/* List of vertices during rendering. */
|
|
std::vector<VertexAttr> m_vertices;
|
|
/* Map of attribute names to shader locations. */
|
|
std::map<std::string, GLuint> m_attributes;
|
|
};
|
|
|
|
template<typename T>
|
|
void ShaderProgram<T>::init()
|
|
{
|
|
glGenVertexArrays(1, &m_vao);
|
|
glGenBuffers(1, &m_vbo);
|
|
}
|
|
|
|
template<typename T>
|
|
ShaderProgram<T>::~ShaderProgram()
|
|
{
|
|
glDeleteProgram(m_prog);
|
|
glDeleteBuffers(1, &m_vbo);
|
|
glDeleteVertexArrays(1, &m_vao);
|
|
}
|
|
|
|
template<typename T>
|
|
bool ShaderProgram<T>::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<typename T>
|
|
bool ShaderProgram<T>::compile()
|
|
{
|
|
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;
|
|
}
|
|
|
|
/* Generate a new program */
|
|
if(success) {
|
|
m_prog = glCreateProgram();
|
|
if(m_prog == 0) {
|
|
azlog(ERROR, "glCreateProgram failed\n");
|
|
success = false;
|
|
}
|
|
}
|
|
|
|
/* Attach all shaders */
|
|
if(success) {
|
|
for(int i = 0; i < m_shaders.size(); i++)
|
|
glAttachShader(m_prog, m_shaders[i]);
|
|
}
|
|
else {
|
|
for(auto id: m_shaders)
|
|
glDeleteShader(id);
|
|
m_shaders.clear();
|
|
}
|
|
return success;
|
|
}
|
|
|
|
template<typename T>
|
|
bool ShaderProgram<T>::link()
|
|
{
|
|
if(!m_shaders.size())
|
|
return false;
|
|
|
|
GLint rc = GL_FALSE;
|
|
GLsizei log_length = 0;
|
|
bool success = true;
|
|
|
|
azlog(INFO, "Linking program\n");
|
|
glLinkProgram(m_prog);
|
|
|
|
glGetProgramiv(m_prog, GL_LINK_STATUS, &rc);
|
|
if(rc == GL_FALSE) {
|
|
azlog(ERROR, "link failed!\n");
|
|
success = false;
|
|
}
|
|
|
|
glGetProgramiv(m_prog, GL_INFO_LOG_LENGTH, &log_length);
|
|
if(log_length > 0) {
|
|
GLchar *log = new GLchar[log_length + 1];
|
|
glGetProgramInfoLog(m_prog, log_length, &log_length, log);
|
|
if(log_length > 0) {
|
|
azlogc(ERROR, "%s", log);
|
|
if(log[log_length - 1] != '\n')
|
|
azlogc(ERROR, "\n");
|
|
}
|
|
delete[] log;
|
|
}
|
|
|
|
/* Detach all shaders */
|
|
for(int i = 0; i < m_shaders.size(); i++) {
|
|
glDetachShader(m_prog, m_shaders[i]);
|
|
glDeleteShader(m_shaders[i]);
|
|
}
|
|
|
|
if(!success) {
|
|
glDeleteProgram(m_prog);
|
|
m_prog = 0;
|
|
}
|
|
m_linked = success;
|
|
return success;
|
|
}
|
|
|
|
template<typename T>
|
|
GLuint ShaderProgram<T>::getAttribute(char const *name)
|
|
{
|
|
std::string name_s {name};
|
|
|
|
if(!m_attributes.count(name_s))
|
|
m_attributes[name_s] = glGetAttribLocation(m_prog, name);
|
|
|
|
return m_attributes[name_s];
|
|
}
|
|
|
|
template<typename T>
|
|
void ShaderProgram<T>::bindVertexAttribute(float T::*x, GLuint id)
|
|
{
|
|
bindVertexAttributeFP(x, 1, GL_FLOAT, GL_FALSE, id);
|
|
}
|
|
|
|
template<typename T>
|
|
void ShaderProgram<T>::bindVertexAttribute(glm::vec2 T::*x, GLuint id)
|
|
{
|
|
bindVertexAttributeFP(x, 2, GL_FLOAT, GL_FALSE, id);
|
|
}
|
|
|
|
template<typename T>
|
|
void ShaderProgram<T>::bindVertexAttribute(glm::vec3 T::*x, GLuint id)
|
|
{
|
|
bindVertexAttributeFP(x, 3, GL_FLOAT, GL_FALSE, id);
|
|
}
|
|
|
|
template<typename T>
|
|
void ShaderProgram<T>::bindVertexAttribute(glm::vec4 T::*x, GLuint id)
|
|
{
|
|
bindVertexAttributeFP(x, 4, GL_FLOAT, GL_FALSE, id);
|
|
}
|
|
|
|
template<typename T>
|
|
void ShaderProgram<T>::setUniform(char const *name, float f)
|
|
{
|
|
glUniform1f(glGetUniformLocation(m_prog, name), f);
|
|
}
|
|
|
|
template<typename T>
|
|
void ShaderProgram<T>::setUniform(char const *name, float f1, float f2)
|
|
{
|
|
glUniform2f(glGetUniformLocation(m_prog, name), f1, f2);
|
|
}
|
|
|
|
template<typename T>
|
|
void ShaderProgram<T>::setUniform(
|
|
char const *name, float f1, float f2, float f3)
|
|
{
|
|
glUniform3f(glGetUniformLocation(m_prog, name), f1, f2, f3);
|
|
}
|
|
|
|
template<typename T>
|
|
void ShaderProgram<T>::setUniform(char const *name, glm::vec2 const &v2)
|
|
{
|
|
glUniform2fv(glGetUniformLocation(m_prog, name), 1, &v2[0]);
|
|
}
|
|
|
|
template<typename T>
|
|
void ShaderProgram<T>::setUniform(char const *name, glm::vec3 const &v3)
|
|
{
|
|
glUniform3fv(glGetUniformLocation(m_prog, name), 1, &v3[0]);
|
|
}
|
|
|
|
template<typename T>
|
|
void ShaderProgram<T>::setUniform(char const *name, glm::vec4 const &v4)
|
|
{
|
|
glUniform4fv(glGetUniformLocation(m_prog, name), 1, &v4[0]);
|
|
}
|
|
|
|
template<typename T>
|
|
void ShaderProgram<T>::setUniform(char const *name, glm::mat2 const &m2)
|
|
{
|
|
glUniformMatrix2fv(
|
|
glGetUniformLocation(m_prog, name), 1, GL_FALSE, &m2[0][0]);
|
|
}
|
|
|
|
template<typename T>
|
|
void ShaderProgram<T>::setUniform(char const *name, glm::mat3 const &m3)
|
|
{
|
|
glUniformMatrix3fv(
|
|
glGetUniformLocation(m_prog, name), 1, GL_FALSE, &m3[0][0]);
|
|
}
|
|
|
|
template<typename T>
|
|
void ShaderProgram<T>::setUniform(char const *name, glm::mat4 const &m4)
|
|
{
|
|
glUniformMatrix4fv(
|
|
glGetUniformLocation(m_prog, name), 1, GL_FALSE, &m4[0][0]);
|
|
}
|
|
|
|
template<typename T>
|
|
void ShaderProgram<T>::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<typename T>
|
|
void ShaderProgram<T>::useProgram()
|
|
{
|
|
glUseProgram(m_prog);
|
|
glBindVertexArray(m_vao);
|
|
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
|
}
|
|
|
|
} /* namespace azur::gl */
|