spacebox/src/Sprite.hpp

390 lines
16 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"
#include "Animation.hpp"
namespace sb
{
/*!
* A Sprite is a wrapper around 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 a class member rather than the class's inherited type, allowing the user to define a custom plane. When the sprite is copied,
* the plane is copied with its references to GPU memory preserved. */
sb::Plane plane;
/* Keep a copy 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. When the sprite is copied, the transformation values are
* copied to the new object, so the new sprite can alter the transformations independently. */
glm::mat4 _scale {1.0f}, _translation {1.0f}, _rotation {1.0f};
/* A sprite by definition has only one texture per draw, so keep an index to the currently active texture. */
int _texture_index = 0;
void frame_by_frame()
{
if (static_cast<std::size_t>(++_texture_index) >= plane.textures().size())
{
_texture_index = 0;
}
}
public:
sb::Animation frames;
/*!
* Construct an instance of Sprite using an existing plane object. The plane will be copied into the Sprite, so further edits must
* be made using 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 drawn at the sprite'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 drawn at the
* sprite'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
* @param filter Resize filter to use when rendering textures
*/
Sprite(std::initializer_list<fs::path> paths, glm::vec2 scale = glm::vec2{1.0f}, std::optional<GLint> filter = std::nullopt) : Sprite(scale)
{
for (const fs::path& path : paths)
{
this->texture(path, filter);
}
}
/*!
* 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
* @param filter Resize filter to use when rendering textures
*/
Sprite(const fs::path& path, glm::vec2 scale = glm::vec2{1.0f}, std::optional<GLint> filter = std::nullopt) : Sprite({path}, scale, filter) {};
/*!
* 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 be 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
* @param filter Resize filter to use when rendering the texture
*/
void texture(const fs::path& path, std::optional<GLint> filter = std::nullopt)
{
sb::Texture texture;
if (filter.has_value())
{
texture.filter(filter.value());
}
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 object's current texture index.
*/
const sb::Texture& texture() const
{
return plane.texture(_texture_index);
}
/*!
* Remove all textures from the sprite object's plane.
*/
void clear_textures()
{
plane.textures() = {};
}
/*!
* @param index set the object's texture index
*/
void texture_index(int index)
{
_texture_index = index;
}
/*!
* @return the object's current texture index value
*/
int texture_index() const
{
return _texture_index;
}
/*!
* Increment the texture index the given number of times. Defaults to 1. It will wrap around at the end. Negative increment can be used.
*
* @param increment amount to increment the texture index
*/
void texture_increment(int increment = 1)
{
/* Add and wrap (even though model wraps as well) */
_texture_index = glm::mod(_texture_index + increment, static_cast<int>(plane.textures().size()));
}
/*!
* 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 its active texture 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_attributes();
texture().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);
}
void update(const sb::Timer& timer)
{
/* Update animation */
if (frames.update(timer.stamp()))
{
frame_by_frame();
}
}
/*!
* 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);
}
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();
}
};
}