diff --git a/src/Pad.hpp b/src/Pad.hpp index 91f119c..38b5969 100644 --- a/src/Pad.hpp +++ b/src/Pad.hpp @@ -16,226 +16,229 @@ #include "Switch.hpp" #include "math.hpp" -/*! - * A Pad is an object containing an sb::Plane which can be clicked to launch an arbitrary user function. It can be sized and placed by setting its - * translation and scale values. - * - * Each instance: - * - * - Has an sb::Plane or derivative (either a custom one provided, or a default constructed one automatically) - * - Has an arbitrary response function for when pressed - * - Can collide with a point (for example, mouse click) - * - * Example: - * - * glm::vec3 w = glm::mat3({{1, 0, 0}, {0, 1, 0}, {-0.6739, -0.74, 1}}) * glm::mat3({{.1, 0, 0}, {0, .1 * (460.0 / 768.0), 0}, {0, 0, 1}}) * - * glm::vec3({-1, -1, 1}); - * std::cout << w << std::endl << glm::translate(glm::vec3{-0.6739, -0.74, 0}) * - * glm::scale(glm::vec3{.1, .1 * (460.0 / 768.0), 1}) * glm::vec4{-1, -1, 0, 1} << std::endl; - * Pad p {background.current(), {-0.6739f, -0.74f}, 0.1f, get_display().window_box().aspect(), std::function()}; - * const std::vector& p_position = *p.attributes("position"); - * glm::vec4 final_position = p.transformation() * glm::vec4{p_position[2].x, p_position[2].y, 0, 1}; - * std::cout << p.transformation() << std::endl << final_position << std::endl; - * assert(final_position == glm::vec4({w.x, w.y, 0, 1})); - */ -template -class Pad +namespace sb { - -private: - - using Reaction = std::function; - sb::Switch connection; - sb::Plane plane; - Box box; - -public: - /*! - * Construct a Pad object with a default constructed sb::Plane. + * A Pad is an object containing an sb::Plane which can be clicked to launch an arbitrary user function. It can be sized and placed by setting its + * translation and scale values. * - * @see Pad(sb::Plane, const glm::vec2&, float, float, Reaction, float) + * Each instance: + * + * - Has an sb::Plane or derivative (either a custom one provided, or a default constructed one automatically) + * - Has an arbitrary response function for when pressed + * - Can collide with a point (for example, mouse click) + * + * Example: + * + * glm::vec3 w = glm::mat3({{1, 0, 0}, {0, 1, 0}, {-0.6739, -0.74, 1}}) * glm::mat3({{.1, 0, 0}, {0, .1 * (460.0 / 768.0), 0}, {0, 0, 1}}) * + * glm::vec3({-1, -1, 1}); + * std::cout << w << std::endl << glm::translate(glm::vec3{-0.6739, -0.74, 0}) * + * glm::scale(glm::vec3{.1, .1 * (460.0 / 768.0), 1}) * glm::vec4{-1, -1, 0, 1} << std::endl; + * Pad p {background.current(), {-0.6739f, -0.74f}, 0.1f, get_display().window_box().aspect(), std::function()}; + * const std::vector& p_position = *p.attributes("position"); + * glm::vec4 final_position = p.transformation() * glm::vec4{p_position[2].x, p_position[2].y, 0, 1}; + * std::cout << p.transformation() << std::endl << final_position << std::endl; + * assert(final_position == glm::vec4({w.x, w.y, 0, 1})); */ - Pad(const glm::vec2& translation = {0.0f, 0.0f}, float scale = 1.0f, float ratio = 1.0f, Reaction on_state_change = Reaction(), float rotation = 0.0f) : - Pad(sb::Plane(), translation, scale, ratio, on_state_change, rotation) {} - - /*! - * Construct a pad object from an sb::Plane, a translation amount, a scale factor, and a reaction function. The translation is relative - * to (0.0, 0.0), and the scale is relative to the plane object. - * - * The reaction function must accept a boolean as its first argument, which will be given the state of the pad object's switch. - * - * The plane object will be copied into the pad, so any edits must be made before constructing the pad, unless there is a Pad function - * that applies the edit, such as Pad::scale(float, float). - * - * @param plane plane or plane derivative to represent the pad visually - * @param translation x, y amount to translate the position - * @param scale amount to scale both x and y - * @param ratio ratio to adjust scale of x and y - * @param on_state_change reaction function which accepts a boolean as its first argument - * @param rotation angle in radians to rotate the pad - */ - Pad(const sb::Plane& plane, glm::vec2 translation = {0.0f, 0.0f}, float scale = 1.0f, float ratio = 1.0f, - Reaction on_state_change = Reaction(), float rotation = 0.0f) : plane(plane) + template + class Pad { - box.size({2.0f, 2.0f}, true); - if (translation != glm::vec2{0.0f, 0.0f}) + + private: + + using Reaction = std::function; + sb::Switch connection; + sb::Plane plane; + Box box; + + public: + + /*! + * Construct a Pad object with a default constructed sb::Plane. + * + * @see Pad(sb::Plane, const glm::vec2&, float, float, Reaction, float) + */ + Pad(const glm::vec2& translation = {0.0f, 0.0f}, float scale = 1.0f, float ratio = 1.0f, Reaction on_state_change = Reaction(), float rotation = 0.0f) : + Pad(sb::Plane(), translation, scale, ratio, on_state_change, rotation) {} + + /*! + * Construct a pad object from an sb::Plane, a translation amount, a scale factor, and a reaction function. The translation is relative + * to (0.0, 0.0), and the scale is relative to the plane object. + * + * The reaction function must accept a boolean as its first argument, which will be given the state of the pad object's switch. + * + * The plane object will be copied into the pad, so any edits must be made before constructing the pad, unless there is a Pad function + * that applies the edit, such as Pad::scale(float, float). + * + * @param plane plane or plane derivative to represent the pad visually + * @param translation x, y amount to translate the position + * @param scale amount to scale both x and y + * @param ratio ratio to adjust scale of x and y + * @param on_state_change reaction function which accepts a boolean as its first argument + * @param rotation angle in radians to rotate the pad + */ + Pad(const sb::Plane& plane, glm::vec2 translation = {0.0f, 0.0f}, float scale = 1.0f, float ratio = 1.0f, + Reaction on_state_change = Reaction(), float rotation = 0.0f) : plane(plane) { - this->translate(translation); - } - if (scale != 1.0f || ratio != 1.0f) - { - this->scale(scale, ratio); - } - if (rotation) - { - this->rotate(rotation); - } - this->on_state_change(on_state_change); - } - - /*! - * Rotate the pad around its center by 90 degrees. If a count is given, rotate by 90 degrees that many times, so for example, - * a count of 3 will be a 270 degree rotation. If the count is negative, rotate -90 degrees. - * - * @param count number of 90 degree rotations to make, or use a negative count to rotate -90 degrees - */ - void rotate(int count = 1) - { - plane.transform(glm::rotate(count * glm::half_pi(), glm::vec3{0.0f, 0.0f, 1.0f})); - } - - /*! - * Scale using a factor and ratio that will transform the pad in the X and Y dimensions. The ratio determines how much each axis - * is scaled. If the ratio is above one, the X-axis's scale will be divided by the ratio. If the ratio is below one, the Y-axis's - * scale will be multiplied by the aspect ratio. If the aspect ratio of the window is given, this will force the pad to display as - * a square, and the ratio will be relative to the shorter axis. - * - * The collision box will be scaled by the same factors. - * - * @param factor amount to scale in both x and y directions - * @param ratio amount to adjust scaling, set to the window aspect ratio to make the pad appear square - */ - const glm::mat4& scale(float factor, float ratio = 1.0f) - { - glm::vec3 scale { factor, factor, 1.0f }; - if (ratio > 1.0f) - { - scale.x /= ratio; - } - else if (ratio < 1.0f) - { - scale.y *= ratio; - } - box.scale({scale.x, scale.y}, true); - return plane.scale(scale); - } - - /*! - * Set the pad's location in the X and Y dimension using a 2D vector. The collision box will be moved by the same translation. - * - * @param translation x, y distance to translate the pad - */ - const glm::mat4& translate(const glm::vec2& translation) - { - box.move(translation); - return plane.translate({translation.x, translation.y, 0.0f}); - } - - /*! - * Set the function that will run when a pad object is clicked. - * - * Example, always keep state true and print "Hello, World!" whenever the pad is clicked, - * - * start_button.on_state_change([&](bool state, int count){ - * if (state) { - * std::ostringstream message; - * message << "Hello, " << state << " World! " << count; - * sb::Log::log(message); - * start_button.press(1); - * }}); - * - * @param on_state_change reaction function which accepts a boolean as its first argument - */ - void on_state_change(Reaction reaction) - { - connection.on_state_change(reaction); - } - - /*! - * Returns true if the point at given NDC coordinates collides with the pad's collision box. This only works properly if the - * Pad is flat in the z-dimension. - * - * @param position x, y NDC coordinates to check for collision - * @return true if the point is inside Pad::box, false otherwise - */ - bool collide(const glm::vec2& position, const glm::mat4& view, const glm::mat4& projection) const - { - /* Corners of the box */ - std::vector corners; - - /* Transform each of the corners into NDC coordinates */ - for (const glm::vec2& vertex : {box.sw(), box.nw(), box.ne(), box.se()}) - { - corners.push_back(sb::world_to_ndc(vertex, projection * view)); - } - - /* Create a new box using the NDC bottom-left corner */ - glm::vec2 control = corners[0]; - float width = glm::distance(corners[3], corners[0]); - float height = glm::distance(corners[1], corners[0]); - sb::Box transformed {control, {width, height}}; - - /* Collide the NDC box with the caller's coordinates */ - return transformed.collide(position); - } - - /*! - * Set the transformation uniform, bind a texture if any are attached, and draw the vertices. Build the full transformation matrix - * by combining the Pad object's transformation with the supplied projection and view matrices, and pass it to the shader. - * - * @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, glm::mat4 view, glm::mat4 projection, std::optional texture_flag_uniform = std::nullopt) - { - if (!plane.textures().empty()) - { - if (texture_flag_uniform.has_value()) + box.size({2.0f, 2.0f}, true); + if (translation != glm::vec2{0.0f, 0.0f}) { - glUniform1i(texture_flag_uniform.value(), true); + this->translate(translation); } - plane.texture().bind(); + if (scale != 1.0f || ratio != 1.0f) + { + this->scale(scale, ratio); + } + if (rotation) + { + this->rotate(rotation); + } + this->on_state_change(on_state_change); } - else if (texture_flag_uniform.has_value()) + + /*! + * Rotate the pad around its center by 90 degrees. If a count is given, rotate by 90 degrees that many times, so for example, + * a count of 3 will be a 270 degree rotation. If the count is negative, rotate -90 degrees. + * + * @param count number of 90 degree rotations to make, or use a negative count to rotate -90 degrees + */ + void rotate(int count = 1) { - glUniform1i(texture_flag_uniform.value(), false); + plane.transform(glm::rotate(count * glm::half_pi(), glm::vec3{0.0f, 0.0f, 1.0f})); } - glm::mat4 mvp = projection * view * plane.transformation(); - glUniformMatrix4fv(transformation_uniform, 1, GL_FALSE, &mvp[0][0]); - plane.enable(); - glDrawArrays(GL_TRIANGLES, 0, plane.attributes("position")->count()); - plane.disable(); - } - /*! - * Run the reaction function. - * - * @param args arguments to pass to the reaction function - * @return result of the reaction function if it returns a value, or void otherwise - */ - ReturnType press(Arguments... args) - { - return connection.flip(args...); - } + /*! + * Scale using a factor and ratio that will transform the pad in the X and Y dimensions. The ratio determines how much each axis + * is scaled. If the ratio is above one, the X-axis's scale will be divided by the ratio. If the ratio is below one, the Y-axis's + * scale will be multiplied by the aspect ratio. If the aspect ratio of the window is given, this will force the pad to display as + * a square, and the ratio will be relative to the shorter axis. + * + * The collision box will be scaled by the same factors. + * + * @param factor amount to scale in both x and y directions + * @param ratio amount to adjust scaling, set to the window aspect ratio to make the pad appear square + */ + const glm::mat4& scale(float factor, float ratio = 1.0f) + { + glm::vec3 scale { factor, factor, 1.0f }; + if (ratio > 1.0f) + { + scale.x /= ratio; + } + else if (ratio < 1.0f) + { + scale.y *= ratio; + } + box.scale({scale.x, scale.y}, true); + return plane.scale(scale); + } - /*! - * @return size in bytes of the pad object's plane object - */ - std::size_t size() const - { - return plane.size(); - } -}; + /*! + * Set the pad's location in the X and Y dimension using a 2D vector. The collision box will be moved by the same translation. + * + * @param translation x, y distance to translate the pad + */ + const glm::mat4& translate(const glm::vec2& translation) + { + box.move(translation); + return plane.translate({translation.x, translation.y, 0.0f}); + } + + /*! + * Set the function that will run when a pad object is clicked. + * + * Example, always keep state true and print "Hello, World!" whenever the pad is clicked, + * + * start_button.on_state_change([&](bool state, int count){ + * if (state) { + * std::ostringstream message; + * message << "Hello, " << state << " World! " << count; + * sb::Log::log(message); + * start_button.press(1); + * }}); + * + * @param on_state_change reaction function which accepts a boolean as its first argument + */ + void on_state_change(Reaction reaction) + { + connection.on_state_change(reaction); + } + + /*! + * Returns true if the point at given NDC coordinates collides with the pad's collision box. This only works properly if the + * Pad is flat in the z-dimension. + * + * @param position x, y NDC coordinates to check for collision + * @return true if the point is inside Pad::box, false otherwise + */ + bool collide(const glm::vec2& position, const glm::mat4& view, const glm::mat4& projection) const + { + /* Corners of the box */ + std::vector corners; + + /* Transform each of the corners into NDC coordinates */ + for (const glm::vec2& vertex : {box.sw(), box.nw(), box.ne(), box.se()}) + { + corners.push_back(sb::world_to_ndc(vertex, projection * view)); + } + + /* Create a new box using the NDC bottom-left corner */ + glm::vec2 control = corners[0]; + float width = glm::distance(corners[3], corners[0]); + float height = glm::distance(corners[1], corners[0]); + sb::Box transformed {control, {width, height}}; + + /* Collide the NDC box with the caller's coordinates */ + return transformed.collide(position); + } + + /*! + * Set the transformation uniform, bind a texture if any are attached, and draw the vertices. Build the full transformation matrix + * by combining the Pad object's transformation with the supplied projection and view matrices, and pass it to the shader. + * + * @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, glm::mat4 view, glm::mat4 projection, std::optional texture_flag_uniform = std::nullopt) + { + 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(); + glUniformMatrix4fv(transformation_uniform, 1, GL_FALSE, &mvp[0][0]); + plane.enable(); + glDrawArrays(GL_TRIANGLES, 0, plane.attributes("position")->count()); + plane.disable(); + } + + /*! + * Run the reaction function. + * + * @param args arguments to pass to the reaction function + * @return result of the reaction function if it returns a value, or void otherwise + */ + ReturnType press(Arguments... args) + { + return connection.flip(args...); + } + + /*! + * @return size in bytes of the pad object's plane object + */ + std::size_t size() const + { + return plane.size(); + } + }; +} diff --git a/src/Sprite.hpp b/src/Sprite.hpp index 87a37e0..9c825b7 100644 --- a/src/Sprite.hpp +++ b/src/Sprite.hpp @@ -16,285 +16,288 @@ #include "filesystem.hpp" #include "Model.hpp" -/*! - * 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 +namespace sb { - -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 + * 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. */ - 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()) + class Sprite { - 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 textures, glm::vec2 scale = glm::vec2{1.0f}) : Sprite(scale) - { - for (const sb::Texture& texture : textures) + 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->texture(texture); + this->scale(scale); } - } - /*! - * 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 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 textures, glm::vec2 scale = glm::vec2{1.0f}) : Sprite(scale) + { + for (const sb::Texture& texture : textures) + { + this->texture(texture); + } + } - /*! - * 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 paths, glm::vec2 scale = glm::vec2{1.0f}) : Sprite(scale) - { - for (const fs::path& path : paths) + /*! + * 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 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, 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); } - } - /*! - * 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 - */ - 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(); - } - - /*! - * 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); - } - - /*! - * 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. - * - * @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); - } - 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. + /*! + * Add a previously constructed sb::Texture to the sprite's plane object. * - * If the index is -1, the check could be skipped since -1 is a special case where the uniform is not expected to exist. + * @param texture sb::Texture object to add */ + void texture(const sb::Texture& texture) + { + plane.texture(texture); + } - glUniformMatrix4fv(transformation_uniform, 1, GL_FALSE, &mvp[0][0]); - plane.enable(); - glDrawArrays(GL_TRIANGLES, 0, plane.attributes("position")->count()); - plane.disable(); - } + /*! + * 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); + } - /*! - * @return size in bytes of the sprite's plane object - */ - std::size_t size() const - { - return plane.size(); - } -}; + /*! + * 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(); + } + + /*! + * 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); + } + + /*! + * 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. + * + * @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); + } + 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(); + } + }; +}