-
Notifications
You must be signed in to change notification settings - Fork 70
Utility Classes
Utility classes wrap boilerplate OpenGL behavior. The classes are extremely small, so it is worth reading through their code to understand what they do and how they can be modified for your specific needs. Otherwise, their default constructors are usually sufficient! These classes have a destructor included that deletes all the data inside, so you don't have to.
Wraps OpenGL textures. Lets you create blank textures of a size, load from images or generate from raw data.
//Construction
Texture tex; //Empty OpenGL Texture
Texture tex(1200, 800); //Size-Based (empty)
Texture tex(image::load("filename.png")); //Using Surface Data
Texture tex(image::make(size, data, algorithm)); //Using raw data + algorithm (function pointer / lambda)
Texture contains a subclass cubetexture, which does the same stuff except as a cube map.
Wraps OpenGL shaders. Lets you define all input properties, and load files for the shader pipeline.
//Construction
Shader shader({"vertex.vs", "fragment.fs"}, {"all", "shader", "input", "properties"});
Shader shader({"vertex.vs", "geometry.gs", "fragment.fs"}, {"all", "shader", "input", "properties"});
A shader can also be told to have a certain number of SSBOs by declaring their name in the constructor.
Shader shader({"vertex.vs", "fragment.fs"}, {"prop"}, {"ssbo1", "ssbo2"});
//Activation
shader.use();
//Uniform Setting (fully templated - automatic handling of type)
shader.uniform(string name, int value);
shader.uniform(string name, glm::vec3 vec);
shader.uniform(string name, glm::mat4 mat);
shader.texture("exampleTexture", tex.texture);
SSBO buffering is templated so it is easy to bind the buffer to the shader before rendering. Buffers are declared at construction, and updated at run time. Allows for dynamic arrays in the shader.
//SSBO Buffering
std::vector<glm::mat4> models;
std::vector<glm::vec2> screen_positions;
shader.buffer("ssbo1", models); //Access in shader with ssbo name "ssbo1"
shader.buffer("ssbo2", screen_positions);
//Raw Update Data - Don't delete buffer (i.e. gl_bufferSubData)
shader.buffer("ssbo1", models, true); //update = true
//... etc
SSBOs are accessed in the shader in a shader as shown here.
//fragment.fs or fragment.vs
#version 430 core
//...
layout (std430, binding = 0) buffer ssbo1 {
mat4 some_name[];
};
layout (std430, binding = 1) buffer ssbo2 {
vec2 some_other_name[];
};
//...
void main(){
//value accessed by: some_name[index]
}
See TinyEngine/examples/11_Voronoi for a simple example using the full functionality.
A compute shader is derived from the shader base class and can be constructed in a similar manner. The difference is that the shader does not have attributes, and only has SSBOs. These are templated like above:
//Construction:
Compute compute("shader.cs", {"ssbo1", "ssbo2"});
The buffers are bound in an identical fashion to regular shaders:
//Binding Buffers
const int size = 1024;
std::vector<glm::vec2> position(size, glm::vec2(0.0f));
std::vector<glm::vec2> velocity(size, glm::vec2(0.0f));
compute.buffer("ssbo2", velocity);
compute.buffer("ssbo1", position); //order does not matter
To dispatch a compute shader, use it and then define how large the work groups are to dispatch:
compute.use();
compute.dispatch(glm::vec3(size, 1, 1));
If you write to an SSBO in the shader, the data can be retrieved from the GPU in a simple manner:
//Retrieve SSBO Data from GPU
shader.retrieve("ssbo1", models);
This requires that the target buffer (in this case: models) is coherent in memory and already has the appropriate size allocated.
For a fully working example application, see TinyEngine/examples/15_Compute here. This is an N-Body gravity simulation in compute shaders. Can also be easily rewritten to simulate boids.
Note that various shader invocations perform incoherent writes to the SSBO, so you may find that your add / subtract operation on the SSBO from various shader invocations don't all register. For this you must use atomic operations, which are only available for integral types (sadly).
OpenGL / GLSL does a weird thing, where it will PAD any vec3 buffers. See the discussion here.
This means that you should store any vec4 SSBOs with appropriate padding or simply as vec4 type. Otherwise, it will load the data correctly into the SSBO but retrieving the data will include the padding, and there is no elegant way to remove it with a stride somehow.
The TinyEngine shader loader extends GLSL to have basic #include directives. It does this by simply doing a recursive load whenever it finds the appropriate macro. The include directives, like in C++, assume a relative path to the position of the shader which is including it. Note that the #include directive only works if your file name does not have apostrophes, i.e.:
//Wrong:
#include "test.incl"
//Correct:
#include test.incl
Wraps VAO and VBOs for rendering. Requires a custom construction function that manipulates the position, normal, color and index data.
//Construction
Model model(algorithm); //User-defined construction algorithm
//Reconstruct model (and update the buffers)
model.construct(algorithm);
//Rendering
model.render(GL_TRIANGLES); //lets you choose how to render
Algorithm is passed as a functional or lambda, that needs to capture the data that the model will be constructed from. Construction process is just pushing vertices into the corresponding vectors.
//Example Model Construction
std::function<void(Model* m)> algorithm = [&](Model* h){
//basic triangle...
h->positions.push_back(0.0);
h->positions.push_back(0.0);
h->positions.push_back(0.0);
h->positions.push_back(0.5);
h->positions.push_back(0.0);
h->positions.push_back(1.0);
h->indices.push_back(0);
h->indices.push_back(1);
h->indices.push_back(2);
//add normal or color data...
//...and so on
}
You can define additional parameters is required inside the functional (e.g. for defining a meshing algorithm for your custom data-type).
//...
std::function<void(Model*, MyClass)> algorithm = [&](Model* h, MyClass m){
//...
}
//Constructing a model from a custom class using algorithm
MyClass m;
Model model(algorithm, m);
See example programs for some examples on how models are constructed.
Models are derived from a base-class "Primitive", which has a few pre-made classes that contain the buffers for e.g. rendering billboards to screen, or sprites / particles in 3D space.
Square2D board; //2D vertex data (for e.g. drawing billboards)
Square3D sprite; //3D vertex data (for e.g. drawing textures on a flat board in 3D space)
Cube cube; //Cube vertex data
These also have render methods.
The target utility class wraps FBOs so that you can render directly for textures. It has two sub-classes "Billboard" and "Cubemap", that give easier construction of the render targets.
//Construction
Target target(1200, 800);
Texture tex(1200, 800);
target.bind(tex);
//Easier Binding
Billboard billboard(1200, 800);
Cubemap cubemap(1200, 800);
Optionally, you can specify in the constructor whether you want to include a depth-buffer or ignore the color buffer (read target.cpp for more info).
//Targeting
billboard.target();
billboard.target(glm::vec3 clearcolor);
When using the target base class, the bound texture is rendered to. When using the derived classes, they have a member "texture" and "depth" for the respective buffers.
To target the main window again, call:
Tiny::view.target(glm::vec3 clearcolor);
This is a class that allows you to instance render any model or primitive with arbitrary data buffers. The instance is constructed with a pointer to the model which it will instance render.
//Construction
Instance instance(&model); //using a pointer to the model we want to instance-render
Data is added to the instance class using the addBuffer method. This prepares an instance buffer object and prepares it for passing to an instanced render.
//Adding buffer data
std::vector<glm::mat4> models; //for e.g. a particle system
//Bind to instance
instance.addBuffer(models);
Finally instance.render can be called with an optional drawing mode. If no argument is passed it assumes GL_TRIANGLE_STRIP which is intended for Square2D and Square3D primitives. For the Model class, just pass whatever drawing mode your VBOs are constructed for.
//Instanced render!
instance.render(GLenum drawing_mode);