spacebox/src/Attributes.hpp

427 lines
19 KiB
C++

/*!
* /\ +------------------------------------------------------+
* ____/ \____ /| - Open source game framework licensed to freely use, |
* \ / / | copy, modify and sell without restriction |
* +--\ ^__^ /--+ | |
* | ~/ \~ | | - created for <https://foam.shampoo.ooo> |
* | ~~~~~~~~~~~~ | +------------------------------------------------------+
* | SPACE ~~~~~ | /
* | ~~~~~~~ BOX |/
* +--------------+
*
* Attributes
* ==========
*
* An Attributes object is a container that acts as a limited, but dynamically typed vector for
* vertex properties to be passed to a GL buffer object and described by a vertex array object.
* It uses the variant library and type inference to determine the vertex type for an object. The
* appropriate variant will be applied based on the first vertices submitted to the object.
*
* The object can contain float, int, or unsigned int vertices of any dimension from 1D to 4D. It
* can also contain bool vertices from 2D to 4D. Vertices can be submitted at initialization through
* the constructor or added and appended later using the add function. If the object is initialized
* without any vertices, it will be in an empty state indicated by the std::monostate variant
* alternative.
*
* The constructor accepts initializer lists, so unknown types can be submitted as long as a single type
* can be inferred. For example,
*
* Attributes({1.0f, 0.5f, -1.0f}); // glm::vec3 attributes containing one vertex
* Attributes({ // glm::vec3 attributes containing two vertices
* {1.0f, 0.5f, -1.0f},
* {0.0f, 1.0f, 1.0f}
* });
* Attributes({1, 2}); // glm::ivec2 attributes containing one vertex
* Attributes(glm::uvec2{1, 2}); // glm::uvec2 attributes containing one vertex
* Attributes(1, 2); // same as Attributes({1, 2}), each argument is treated as a coordinate
* Attributes({ // inferred as glm::uvec3, so the -5, -555, and the decimal precision are lost
* {-5, 55, -555},
* glm::uvec3{8, 88, 888},
* {9.9, 9.99, 9.999}
* });
*
* The add function also accepts initializer lists and any type that can be converted into the original.
*
* sb::Attributes attr, attr2, attr3, attr4;
* attr.add(420);
* attr.add({69, 9000});
* attr.add({});
* attr.add({{1}});
* std::cout << attr << std::endl;
* attr2.add(glm::ivec4{1, 1, 2, 3});
* attr2.add({{5.0f, 8.0f, 13.0f, 21.0f}, {34.0f, 55.0f, 89.0f, 144.0f}});
* attr2.add({glm::uvec4{5, 8, 13, 21}, glm::uvec4{34, 55, 89, 144}});
* attr2.add({0.0f, 0.0f, 0.0f, 0.0f});
* std::cout << attr2 << std::endl;
* attr3.add(1.1f);
* attr3.add(2);
* attr3.add(std::vector<std::uint32_t>{3, 4});
* std::cout << attr3 << std::endl;
* attr3 = attr2;
* attr2 = attr4;
* std::cout << attr3 << std::endl;
* std::cout << attr2 << std::endl;
* attr4 = sb::Attributes({{-5, 55, -555}, glm::ivec3{8, 88, 888}, {9.99, 9.9, 9.0}});
* attr4.add(1000, 10000, 100000);
* attr4.add(1, 11);
* std::cout << attr4 << std::endl;
* attr2 = sb::Attributes(std::vector<float>{5.5, 6.6, 7.7, 8.8});
* attr2.add(glm::vec2{9.9f, 10.10f});
* std::cout << attr2 << std::endl;
*
* This prints
*
* warning: was not added to the attributes because its type is incompatible
* { 420 69 9000 1 }
* warning: { 0 0 0 0 } was not added to the attributes because its type is incompatible
* { {1, 1, 2, 3} {5, 8, 13, 21} {34, 55, 89, 144} {5, 8, 13, 21} {34, 55, 89, 144} }
* { 1.1 2 3 4 }
* { {1, 1, 2, 3} {5, 8, 13, 21} {34, 55, 89, 144} {5, 8, 13, 21} {34, 55, 89, 144} }
*
* warning: { {1, 11} } was not added to the attributes because its type is incompatible
* { {-5, 55, -555} {8, 88, 888} {9, 9, 9} {1000, 10000, 100000} }
* warning: { {9.9, 10.1} } was not added to the attributes because its type is incompatible
* { 5.5 6.6 7.7 8.8 }
*/
#pragma once
#include <ostream>
#include <vector>
#include <variant>
#include <initializer_list>
#include <type_traits>
#include <utility>
#include <exception>
#include "glm/glm.hpp"
#include "gl.h"
#include "Log.hpp"
#include "extension.hpp"
namespace sb
{
class Attributes;
std::ostream& operator<<(std::ostream&, const Attributes&);
class Attributes
{
private:
/* Memory offset in the VBO */
GLintptr _offset = 0;
/* Every type of vertex in the glm vertex types (bool, unsigned int, int, float) from 1D to 4D is included in this variant.
* Each variant alternative is a vector of vertex type, so only homogenous vectors can be used. Index 0 is the std::monostate
* alternative, which is used here for default initialization to indicate Attributes are in an empty state where no variant
* alternative is selected. 1D vertices are specified by vectors of scalars, rather than the 1D glm vertex types. 1D vertices
* also include an unsigned byte type. The std::vector<bool> alternative is not included because it is a special case of
* std::vector that doesn't have a data member function. */
using Vertices = std::variant<
std::monostate, std::vector<std::uint8_t>, std::vector<std::uint32_t>, std::vector<std::int32_t>, std::vector<float>,
std::vector<glm::bvec2>, std::vector<glm::uvec2>, std::vector<glm::ivec2>, std::vector<glm::vec2>,
std::vector<glm::bvec3>, std::vector<glm::uvec3>, std::vector<glm::ivec3>, std::vector<glm::vec3>,
std::vector<glm::bvec4>, std::vector<glm::uvec4>, std::vector<glm::ivec4>, std::vector<glm::vec4>>;
Vertices vertices;
/* Index is stored as a variant because it needs to be checked whether it is uninitialized (std::monostate) or not. */
std::variant<std::monostate, GLint> _index;
public:
/*!
* The default constructor creates an empty set of attributes. An empty set of attributes is indicated by
* the class's vertex variant having the std::monostate variant.
*/
Attributes() {};
/*!
* Construct a new Attributes object with vertices set to the vertices contained in this vector.
*
* @param vertices vector of vertices
*/
template<typename Type>
Attributes(const std::vector<Type>& vertices) : vertices(vertices) {}
/*!
* Add a vertex. The vertex can be any of the variant types.
*
* @param vertex vertex of 1 to 4 dimensions
*/
template<typename Type>
Attributes(const Type& vertex) : Attributes({vertex}) {}
/*!
* Add a vertex by specifying each coordinate as a separate argument. All arguments must have identical type.
*
* @param coordinate_0 x coordinate
* @param coordinates y, z, and w coordinates can be specified as well, each as their own argument, using a parameter pack
*/
template<typename XType, typename... CoordinateTypes,
std::enable_if_t<(... && std::is_same_v<CoordinateTypes, XType>), std::nullptr_t> = nullptr>
Attributes(const XType& coordinate_0, const CoordinateTypes&... coordinates) :
Attributes({std::initializer_list<XType>({coordinate_0, coordinates ...})}) {}
/*!
* Add vertices by passing an uninitialized list of vertices. This template applies to a list of
* vertices where the list type is undeclared but the containing vertices have been initialized
* or the type can be inferred because the vertices are 1D scalars.
*
* @param vertices uninitialized list of initialized multidimensional vertices or uninitialized scalars
*/
template<typename VertexType>
Attributes(const std::initializer_list<VertexType>& vertices) :
Attributes(std::vector<VertexType>(vertices.begin(), vertices.end())) {}
/*!
* Add vertices by passing a two-dimensional initializer list, a list of uninitialized vertices.
* The appropriate glm vertex size is applied by looking at the length of the first uninitialized
* vertex in the initializer list.
*
* @param vertices uninitialized list of uninitialized vertices
*/
template<typename CoordinateType>
Attributes(const std::initializer_list<std::initializer_list<CoordinateType>>& vertices)
{
for (auto& vertex : vertices)
{
const CoordinateType* coordinate = vertex.begin();
if (vertex.size() == 1)
{
add(vertex);
}
else if (vertex.size() == 2)
{
add(glm::vec<2, CoordinateType, glm::defaultp>({*coordinate++, *coordinate}));
}
else if (vertex.size() == 3)
{
add(glm::vec<3, CoordinateType, glm::defaultp>({*coordinate++, *coordinate++, *coordinate}));
}
else if (vertex.size() == 4)
{
add(glm::vec<4, CoordinateType, glm::defaultp>({*coordinate++, *coordinate++, *coordinate++, *coordinate}));
}
}
}
/*!
* Extend to include these attributes at the end. The argument will be automatically converted to an
* Attributes object, so the vertices can be in any of the Attributes constructor formats. If no attributes
* have been constructed or initialized, the current vertices will be set to these vertices. Otherwise,
* vertices will be inserted at the end of the current vertices.
*
* @param other attributes to add
*/
void add(const Attributes& other);
/*!
* Add a 2D, 3D or 4D vertex by specifying each coordinate as a separate argument. All arguments must have
* identical type.
*
* @param coordinate_0 x-coordinate
* @param coordinate_1 y-coordinate
* @param coordinate_2 z-coordinate (optional)
* @param coordinate_3 w-coordinate (optional)
*/
template<typename XType, typename YType, typename... CoordinateTypes,
std::enable_if_t<std::is_same_v<XType, YType> && (... && std::is_same_v<CoordinateTypes, XType>),
std::nullptr_t> = nullptr>
void add(const XType& coordinate_0, const YType& coordinate_1, const CoordinateTypes&... coordinates)
{
/* The arguments are converted to an initializer list of initializer lists, a 2D initializer list with
* the new vertex as its only element, and passed to the overloaded add function where the vertex is
* converted into an Attributes object */
add({std::initializer_list<XType>({coordinate_0, coordinate_1, coordinates ...})});
}
/*!
* Return a const reference to the vertex at the specified index in the attributes vector.
*
* @param index vertex index
* @return std::vector of type VertexType
*/
template<typename VertexType>
const VertexType& read(std::size_t index) const
{
return std::get<std::vector<VertexType>>(vertices)[index];
}
/*!
* 2D lookup of a value in the attributes. Return a const reference to the coordinate value at inner index
* within a vertex at outer index in the attributes vector.
*
* @param outer vertex index
* @param inner coordinate index within vertex
* @return value of type CoordinateType
*/
template<typename VertexType, typename CoordinateType = float>
const CoordinateType& read(std::size_t outer, std::size_t inner)
{
return std::get<std::vector<VertexType>>(vertices)[outer][inner];
}
/*!
* Return the attributes as a reference to a typed vector. If the type is not the alternative in use by the
* attributes, std::bad_variant_access will be thrown.
*
* @return std::vector of type VertexType
*/
template<typename VertexType>
operator const std::vector<VertexType>&() const
{
return std::get<std::vector<VertexType>>(vertices);
}
/*!
* Repeatedly add attributes to the end count number of times. If count is 1, this has the same effect as the
* add function.
*
* @param other attributes to add repeatedly
* @param count number of times to repeat
*/
void extend(const Attributes& other, std::size_t count = 1);
/*!
* Set the generic vertex attribute index for the attributes.
*
* This must be the same index that is returned when `glGetAttribLocation` is given the shader program and associated
* variable name for the attributes in the shader program.
*
* To make sure those align, either assign an arbitray index with `glBindAttribLocation` and pass it to this function, or
* skip this function and look up the index automatically at bind time by passing the location name and linked shader program
* index to sb::Attributes::bind(const std::string&, GLuint).
*
* The index may also be specified to GL in the shader source code using the location layout qualifier in GLSL 3.3 and above.
*
* If assigning an arbitrary index, this can be any integer not greater than `GL_MAX_VERTEX_ATTRIBS`, and it must also be
* passed to `glBindAttribLocation`.
*
* Once assigned, the index can be passed to `glVertexAttribPointer` to set the properties of the attributes on the GPU, either
* manually or automatically through sb::VBO::add(sb::Attributes&).
*
* @param index index the attributes bind to in the shader program
*/
void index(GLint index);
/*!
* Get the generic vertex attribute index for the attributes. This index can be passed to `glBindAttribLocation` to assign the
* index to a shader input variable if the index isn't already assigned by a layout qualifier in the GLSL source code. The
* index should be passed to `glVertexAttribPointer` to specify the location to use when rendering.
*
* @return index the attributes bind to in the shader program
*/
GLint index() const;
/*!
* Set the byte offset into the VBO where this object's vertex data is stored. This can be set by passing this object to
* `VBO::add`.
*
* @param offset byte offset in the VBO where this object's vertex data is stored
*/
void offset(GLintptr offset);
/*!
* Create a pointer to these attributes in the VAO using this object's index and offset values by calling `glVertexAttribPointer`.
*
* The offset should have been previously set by a call to VBO::add(sb::Attributes&) or some equivalent in manual GL calls.
* The index should have been previously set to the index of the location in the shader program associated with the attributes
* using sb::Attributes::index(GLint) or some equivalent in manual GL calls.
*/
void bind() const;
/*!
* Get the index of the uniform with the given name, store it as this object's index, and create a pointer to these
* attributes in the VAO using this object's offset value.
*
* The offset should have been previously set by a call to VBO::add(sb::Attributes&) or some equivalent in manual GL calls.
* The program must already be linked to get the location.
*
* @param name uniform name
* @param program GLSL program, created with glCreateProgram
*/
void bind(const std::string& name, GLuint program);
/*!
* Enable the attributes in the VAO associated with this object's index. `Attributes::bind` must have been called previously,
* unless the index has been set up manually with direct calls to GL.
*
* The object's current index is used refer to the pointer.
*/
void enable() const;
/*!
* Disable the attributes in the VAO associated with this object's index.
*/
void disable() const;
/*!
* Get the size in bytes of the object's underlying vector of vertices. This can be passed to OpenGL along with the memory
* pointer when copying vertices to the GPU. It can also be used with sb::VBO::allocate(GLsizeiptr, GLenum).
*
* @return size in bytes of the attributes
*/
std::size_t size() const;
/*!
* @return the number of attributes in the object
*/
std::size_t count() const;
/*!
* Get the `GLenum` that corresponds to the type of scalar being held in the vertices. This will return either `GL_FLOAT`,
* `GL_INT`, `GL_UNSIGNED_INT`, `GL_UNSIGNED_BYTE`, `GL_BOOL`, or `GL_INVALID_ENUM`. A return value of `GL_INVALID_ENUM`
* indicates an error meaning there are no attributes and no type exists yet.
*
* @return the attributes type
*/
GLenum type() const;
/*!
* Get the number of dimensions in the vertices. If there are no vertices, return 0.
*
* @return number of dimensions in the vertices
*/
std::size_t dimensions() const;
/*!
* Normalization isn't supported, so this always returns `false`
*
* @return false
*/
bool normalized() const;
/*!
* Overload the stream operator to support attributes. Add a string representation of the vertices to the output stream. Since
* this is defined as a friend function and isn't in the global scope, it should prevent it being looked up with arguments other
* than attributes.
*
* @param out output stream
* @param attributes attributes object to print
* @return edited output stream
*/
friend std::ostream& operator<<(std::ostream& out, const Attributes& attributes);
/*!
* Get a pointer to the first vertex in the object's underlying vector of vertices. This can be used along with the size
* function to pass the raw bytes of the vertices to the GPU.
*
* @return pointer to the first vertex in the object's underlying vector of vertices
*/
operator const void*() const;
/*!
* Attributes can be represented by their GL index when an int is requested.
*
* @return index the attributes bind to in the shader program
*/
operator int() const;
};
}