spacebox/src/Sprite.hpp

328 lines
14 KiB
C++

/* +------------------------------------------------------+
____/ \____ /| - Open source game framework licensed to freely use, |
\ / / | copy, modify and sell without restriction |
+--\ ^__^ /--+ | |
| ~/ \~ | | - created for <https://foam.shampoo.ooo> |
| ~~~~~~~~~~~~ | +------------------------------------------------------+
| SPACE ~~~~~ | /
| ~~~~~~~ BOX |/
+-------------*/
#pragma once
#include <vector>
#include <optional>
#include "glm/glm.hpp"
#include "filesystem.hpp"
#include "Model.hpp"
namespace sb
{
/*!
* A Sprite is a container for an sb::Plane object that resets and stores scale, translation, and rotation matrices every time they are set
* and combines them automatically when getting the full transformation. This allows those transformations to be set repeatedly without having
* to call sb::Plane::untransform() after each set. In the case of translation, there is also a move function that allows the translation to be
* modified without resetting, so the sprite can move relative amounts.
*/
class Sprite
{
private:
/* The plane is included using composition instead of inheritance, allowing the user to define a custom plane. */
sb::Plane plane;
/* Keep a reference of the matrix transformations generated when the user applies a transformation, so that each can be reapplied
* without having to set all the transformations every time one is changed. */
glm::mat4 _scale {1.0f}, _translation {1.0f}, _rotation {1.0f};
public:
/*!
* Construct an instance of Sprite using an existing plane object. The plane will be copied into the Sprite, so further edits will
* need to go through the Sprite class.
*
* @param plane flat model of the sprite
*/
Sprite(const sb::Plane& plane) : plane(plane) {};
/*!
* Construct a Sprite with a default constructed sb::Plane and optional scale amount.
*
* @param scale amount to scale
*/
Sprite(glm::vec2 scale = glm::vec2{1.0f}) : Sprite(sb::Plane())
{
this->scale(scale);
}
/*!
* Construct a Sprite with a default constructed sb::Plane and attach a list of textures to the plane. Each texture is a frame of the
* sprite's animation. The texture is the 2D graphic that displays in the sprite object's location.
*
* @param textures list of textures
* @param scale amount to scale
*/
Sprite(std::initializer_list<sb::Texture> textures, glm::vec2 scale = glm::vec2{1.0f}) : Sprite(scale)
{
for (const sb::Texture& texture : textures)
{
this->texture(texture);
}
}
/*!
* Construct a Sprite with a default constructed sb::Plane and give the plane a texture. The texture is the 2D graphic that displays
* in the sprite object's location.
*
* @param texture sprite's 2D graphic
* @param scale amount to scale
*/
Sprite(const sb::Texture& texture, glm::vec2 scale = glm::vec2{1.0f}) : Sprite({texture}, scale) {};
/*!
* Construct a ::Sprite object from a list of paths to image files which will be converted into textures.
*
* The textures are loaded into GPU memory if the GL context is active. Otherwise, the path is just attached to
* each texture, and they must be loaded with a call to Sprite::load after the GL context is active.
*
* @see sb::Texture::load()
*
* @param paths list of paths to images
* @param scale amount to scale
*/
Sprite(std::initializer_list<fs::path> paths, glm::vec2 scale = glm::vec2{1.0f}) : Sprite(scale)
{
for (const fs::path& path : paths)
{
this->texture(path);
}
}
/*!
* Construct a ::Sprite object from a path to an image file which will be converted into a texture.
*
* @see ::Sprite(std::initializer_list<fs::path>, glm::vec2)
*
* @param path path to an image
* @param scale amount to scale
*/
Sprite(const fs::path& path, glm::vec2 scale = glm::vec2{1.0f}) : Sprite({path}, scale)
{
this->texture(path);
}
/*!
* Add a previously constructed sb::Texture to the sprite's plane object.
*
* @param texture sb::Texture object to add
*/
void texture(const sb::Texture& texture)
{
plane.texture(texture);
}
/*!
* Add a new texture to the sprite's plane object from a path to an image file which will converted into a new texture.
*
* The texture is loaded into GPU memory if the GL context is active. Otherwise, the path is just attached to the texture,
* and it must be loaded with a call to Sprite::load.
*
* @param path path to an image
*/
void texture(const fs::path& path)
{
sb::Texture texture;
if (SDL_GL_GetCurrentContext() != nullptr)
{
texture.load(path);
}
else
{
texture.associate(path);
}
this->texture(texture);
}
/*!
* Get a constant reference to the texture attached to the sprite's plane object at the given index . If no index is given,
* get the texture at index 0. If there are no textures, an exception will be thrown.
*
* @param index index of texture to get
*/
const sb::Texture& texture(int index = 0) const
{
return plane.texture(index);
}
/*!
* If the GL context is active, this can be called to load image paths previously associated with textures attached to the
* sprite's plane object.
*/
void load()
{
plane.load();
}
/*!
* Add all attributes to the given vertex buffer object. The buffer object should have been previously allocated to at least the
* size of this sprite by passing Sprite::size() to VBO::allocate(GLsizeiptr, GLenum).
*
* The VBO must currently be bound.
*
* @param vbo vertex buffer object that the sprite's attribute vertices will be added to
*/
void add(sb::VBO& vbo)
{
plane.add(vbo);
}
/*!
* Bind all of this sprite's attributes and textures by calling each of their bind methods. Textures and attributes all must already
* have GL indices set, for example by calling Texture::generate() and Attributes::index(GLint) on each.
*/
void bind()
{
plane.bind();
}
/*!
* Get a reference to the plane object's shared pointer to the attributes with the given name. The underlying attributes
* object is fully exposed, meaning it can be edited, and both its const and non-const methods can be used.
*
* @param name name of the attributes, see Model::attributes(const sb::Attributes&, const std::string&)
* @return const reference to a shared pointer held by the plane object that points to the attributes with the given name
*/
const std::shared_ptr<sb::Attributes>& attributes(const std::string name) const
{
return plane.attributes(name);
}
/*!
* Set the sprite plane's translation transformation using an x, y, and z offset. Any previous translation will be reset. Can
* be used to move the sprite relative to the origin.
*
* @param translation transformation along the x, y, and z axes
*/
void translate(const glm::vec3& translation)
{
plane.untransform();
_translation = plane.translate(translation);
transform();
}
/*!
* Add to the sprite's current translation transformation. Can be used to move the sprite relative to its current position.
*
* @param step amount to move sprite's translation transformation in three dimensions
*/
void move(const glm::vec3& step)
{
plane.untransform();
_translation += plane.translate(step);
transform();
}
/*!
* Set the sprite plane's scale transformation in the x and y dimensions. Any previous scale will be reset.
*
* @param scale sprite plane's new scale transformation
*/
void scale(const glm::vec2& scale)
{
plane.untransform();
_scale = plane.scale({scale, 1.0f});
transform();
}
/*!
* Set the sprite plane's rotation transformation to a rotation around the given axis by a given angle. Any previous
* rotation will be reset. This does not rotate the sprite relative to its current rotation, it rotates relative to
* the initial rotation of zero.
*
* @param angle angle in radians amount to rotate
* @param axis three dimensional axis around which to rotate the sprite
*/
void rotate(float angle, const glm::vec3& axis)
{
plane.untransform();
_rotation = plane.rotate(angle, axis);
transform();
}
/*!
* The translation, scale, and rotation transformations, if previously set, are applied to the object's transformation
* property, along with the optional additional transformation matrix argument.
*
* The existing transformation property will be reset to the identity matrix before this transformation is applied.
*
* @warning This function works differently than Model::transform(const glm::mat4&). To apply an arbitrary transformation
* without having the non-arbitrary transformations applied as well, the rotate, scale, and translate transformations should
* be set to the identity matrix.
*
* @param transformation optional additional transformation to apply
*/
void transform(glm::mat4 transformation = glm::mat4{1.0f})
{
plane.untransform();
plane.transform(_translation * _scale * _rotation * transformation);
}
/*!
* Get the sprite's transformation from Sprite::transform(glm::mat4), which combines the translation, scale, rotation, and an
* optional arbitrary transformation, apply the given view and projection transformations, and pass the transformation to the
* shader at the given transformation uniform. The uniform is not checked for existence, so it must be present in the shader.
*
* Then enable the plane's attributes, and draw the amount of vertices in the plane's position attributes using `glDrawArrays`.
* Disable the plane's attributes after the draw.
*
* The optional texture flag uniform can be passed to automatically set that uniform to true if there are textures attached to
* this sprite, and false if not. The currently bound shader should be written to use that flag. For example, it could use the
* flag to choose whether to use the UV or the color attributes.
*
* The vertex data is expected to be bound before this function is called.
*
* @param transformation_uniform transformation uniform ID
* @param view the view matrix for transforming from world space to camera space
* @param projection projection matrix for transforming from camera space to clip space
* @param texture_flag_uniform uniform ID for boolean enabling or disabling texture display
*/
void draw(GLuint transformation_uniform, const glm::mat4 view = glm::mat4{1.0f}, const glm::mat4 projection = glm::mat4{1.0f},
std::optional<GLuint> texture_flag_uniform = std::nullopt) const
{
if (!plane.textures().empty())
{
if (texture_flag_uniform.has_value())
{
glUniform1i(texture_flag_uniform.value(), true);
}
plane.texture().bind();
}
else if (texture_flag_uniform.has_value())
{
glUniform1i(texture_flag_uniform.value(), false);
}
glm::mat4 mvp = projection * view * plane.transformation();
/* It's possible to use glGetActiveUniformName to test for existence of the given uniform index before proceeding, but the
* test is left out to optimize speed since the draw call is expected to be used every frame.
*
* If the index is -1, the check could be skipped since -1 is a special case where the uniform is not expected to exist.
*/
glUniformMatrix4fv(transformation_uniform, 1, GL_FALSE, &mvp[0][0]);
plane.enable();
glDrawArrays(GL_TRIANGLES, 0, plane.attributes("position")->count());
plane.disable();
}
/*!
* @return size in bytes of the sprite's plane object
*/
std::size_t size() const
{
return plane.size();
}
};
}