/* +------------------------------------------------------+ ____/ \____ /| - Open source game framework licensed to freely use, | \ / / | copy, modify and sell without restriction | +--\ ^__^ /--+ | | | ~/ \~ | | - created for | | ~~~~~~~~~~~~ | +------------------------------------------------------+ | SPACE ~~~~~ | / | ~~~~~~~ BOX |/ +-------------*/ #pragma once #include #include #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 to get 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(++_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 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 paths, glm::vec2 scale = glm::vec2{1.0f}, std::optional 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, 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 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 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(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& 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 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(); } }; }