move sprite and pad into sb namespace

This commit is contained in:
ohsqueezy 2023-07-15 12:19:37 -04:00
parent 7bbc0f851d
commit d1fe6b927c
2 changed files with 481 additions and 475 deletions

View File

@ -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<void()>()};
* const std::vector<glm::vec2>& 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<typename ReturnType = void, typename... Arguments>
class Pad
namespace sb
{
private:
using Reaction = std::function<ReturnType(bool, Arguments...)>;
sb::Switch<ReturnType, Arguments...> 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<void()>()};
* const std::vector<glm::vec2>& 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<typename ReturnType = void, typename... Arguments>
class Pad
{
box.size({2.0f, 2.0f}, true);
if (translation != glm::vec2{0.0f, 0.0f})
private:
using Reaction = std::function<ReturnType(bool, Arguments...)>;
sb::Switch<ReturnType, Arguments...> 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<float>(), 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<glm::vec3> 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<GLuint> 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<float>(), 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<glm::vec3> 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<GLuint> 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();
}
};
}

View File

@ -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<sb::Texture> 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<sb::Texture> 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<fs::path> 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<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);
}
}
/*!
* 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();
}
/*!
* 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.
*
* @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.
/*!
* 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<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.
*
* @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();
}
};
}