Refactored RatingComponent to improve rendering accuracy and performance.

This commit is contained in:
Leon Styhre 2022-08-28 20:11:20 +02:00
parent 077c6abf3e
commit 48a9571609
9 changed files with 175 additions and 163 deletions

View file

@ -90,6 +90,7 @@ public:
void setSize(const glm::vec2& size) { setSize(size.x, size.y); }
void setSize(const float w, const float h);
virtual void setResize(float width, float height) {}
virtual void setResize(float width, float height, bool rasterize) {}
virtual void onSizeChanged() {}
virtual glm::vec2 getRotationSize() const { return getSize(); }

View file

@ -36,6 +36,8 @@ ImageComponent::ImageComponent(bool forceLoad, bool dynamic)
, mFlipY {false}
, mTargetIsMax {false}
, mTargetIsMin {false}
, mTileWidth {0.0f}
, mTileHeight {0.0f}
, mColorShift {0xFFFFFFFF}
, mColorShiftEnd {0xFFFFFFFF}
, mColorGradientHorizontal {true}
@ -164,14 +166,15 @@ void ImageComponent::setImage(const std::string& path, bool tile)
// we perform the actual rasterization to have the cache entry updated with the proper
// texture. For SVG images this requires that every call to setImage is made only after
// a call to setResize or setMaxSize (so the requested size is known upfront).
mTexture = TextureResource::get(path, tile, mForceLoad, mDynamic, mLinearInterpolation);
mTexture = TextureResource::get(path, tile, mForceLoad, mDynamic, mLinearInterpolation,
false, 0, 0, mTileWidth, mTileHeight);
if (isScalable) {
resize(false);
mTexture.reset();
mTexture =
TextureResource::get(path, tile, mForceLoad, mDynamic, mLinearInterpolation, false,
static_cast<size_t>(mSize.x), static_cast<size_t>(mSize.y));
mTexture = TextureResource::get(path, tile, mForceLoad, mDynamic, mLinearInterpolation,
false, static_cast<size_t>(mSize.x),
static_cast<size_t>(mSize.y), mTileWidth, mTileHeight);
mTexture->rasterizeAt(mSize.x, mSize.y);
onSizeChanged();
}
@ -206,6 +209,14 @@ void ImageComponent::setResize(float width, float height)
resize();
}
void ImageComponent::setResize(float width, float height, bool rasterize)
{
mTargetSize = glm::vec2 {width, height};
mTargetIsMax = false;
mTargetIsMin = false;
resize(rasterize);
}
void ImageComponent::setMaxSize(const float width, const float height)
{
mTargetSize = glm::vec2 {width, height};

View file

@ -30,6 +30,7 @@ public:
// Use an already existing texture.
void setImage(const std::shared_ptr<TextureResource>& texture, bool resizeTexture = true);
void setDynamic(bool state) { mDynamic = state; }
void onSizeChanged() override { updateVertices(); }
// Resize the image to fit this size. If one axis is zero, scale that axis to maintain
@ -38,7 +39,11 @@ public:
// Can be set before or after an image is loaded.
// setMaxSize() and setResize() are mutually exclusive.
void setResize(const float width, const float height) override;
void setResize(const glm::vec2& size) { setResize(size.x, size.y); }
void setResize(const glm::vec2& size, bool rasterize = true)
{
setResize(size.x, size.y, rasterize);
}
void setResize(const float width, const float height, bool rasterize) override;
// Resize the image to be as large as possible but fit within a box of this size.
// Can be set before or after an image is loaded.
@ -46,6 +51,12 @@ public:
void setMaxSize(const float width, const float height);
void setMaxSize(const glm::vec2& size) { setMaxSize(size.x, size.y); }
void setTileSize(const float width, const float height)
{
mTileWidth = width;
mTileHeight = height;
}
glm::vec2 getRotationSize() const override { return mRotateByTargetSize ? mTargetSize : mSize; }
// Applied AFTER image positioning and sizing.
@ -108,6 +119,9 @@ private:
bool mTargetIsMax;
bool mTargetIsMin;
float mTileWidth;
float mTileHeight;
// Calculates the correct mSize from our resizing information (set by setResize/setMaxSize).
// Used internally whenever the resizing parameters or texture change. This function also
// initiates the SVG rasterization unless explicitly told not to.

View file

@ -19,15 +19,21 @@ RatingComponent::RatingComponent(bool colorizeChanges)
, mColorChangedValue {DEFAULT_COLORSHIFT}
, mColorShift {DEFAULT_COLORSHIFT}
, mColorShiftEnd {DEFAULT_COLORSHIFT}
, mUnfilledColor {DEFAULT_COLORSHIFT}
, mColorizeChanges {colorizeChanges}
{
mFilledTexture = TextureResource::get(":/graphics/star_filled.svg", true);
mUnfilledTexture = TextureResource::get(":/graphics/star_unfilled.svg", true);
mSize = glm::vec2 {mRenderer->getScreenHeight() * 0.06f * NUM_RATING_STARS,
mRenderer->getScreenHeight() * 0.06f};
mIconFilled.setResize(mSize, false);
mIconFilled.setTileSize(mSize.y, mSize.y);
mIconUnfilled.setResize(mSize, false);
mIconUnfilled.setTileSize(mSize.y, mSize.y);
mIconFilled.setImage(std::string(":/graphics/star_filled.svg"), true);
mIconUnfilled.setImage(std::string(":/graphics/star_unfilled.svg"), true);
mValue = 0.5f;
mSize = glm::vec2 {64.0f * NUM_RATING_STARS, 64.0f};
updateVertices();
updateColors();
}
void RatingComponent::setValue(const std::string& value)
@ -44,9 +50,9 @@ void RatingComponent::setValue(const std::string& value)
// color shift accordingly.
if (mColorizeChanges) {
if (static_cast<int>(mValue * 10.0f) == mOriginalValue)
setColorShift(mColorOriginalValue);
mIconFilled.setColorShift(mColorOriginalValue);
else
setColorShift(mColorChangedValue);
mIconFilled.setColorShift(mColorChangedValue);
}
// For the special situation where there is a fractional rating in the gamelist.xml
@ -55,7 +61,7 @@ void RatingComponent::setValue(const std::string& value)
// been manually edited.
if (mColorizeChanges && mValue != stof(value)) {
mOriginalValue = ICONCOLOR_USERMARKED;
setColorShift(0x449944FF);
mIconFilled.setColorShift(0x449944FF);
}
if (mValue > 1.0f)
@ -63,8 +69,6 @@ void RatingComponent::setValue(const std::string& value)
else if (mValue < 0.0f)
mValue = 0.0f;
}
updateVertices();
}
std::string RatingComponent::getValue() const
@ -83,117 +87,55 @@ std::string RatingComponent::getRatingValue(const std::string& rating)
return ss.str();
}
void RatingComponent::setOpacity(float opacity)
{
mOpacity = opacity;
mColorShift =
(mColorShift >> 8 << 8) | static_cast<unsigned char>(mOpacity * mThemeOpacity * 255.0f);
updateColors();
}
void RatingComponent::setDimming(float dimming)
{
mDimming = dimming;
mVertices[0].dimming = mDimming;
mVertices[4].dimming = mDimming;
}
void RatingComponent::setColorShift(unsigned int color)
{
mColorShift = color;
mColorShiftEnd = color;
// Grab the opacity from the color shift because we may need
// to apply it if fading in textures.
mOpacity = static_cast<float>(color & 0xff) / 255.0f;
updateColors();
mIconFilled.setDimming(dimming);
mIconUnfilled.setDimming(dimming);
}
void RatingComponent::onSizeChanged()
{
// Make sure the size is not unreasonably large (which may be caused by a mistake in
// the theme configuration).
mSize.x = glm::clamp(mSize.x, 0.0f, mRenderer->getScreenWidth() / 2.0f);
mSize.y = glm::clamp(mSize.y, 0.0f, mRenderer->getScreenHeight() / 2.0f);
mSize = glm::round(mSize);
if (mSize.y == 0.0f)
mSize.y = mSize.x / NUM_RATING_STARS;
else if (mSize.x == 0.0f)
if (mSize.x == 0.0f)
mSize.x = mSize.y * NUM_RATING_STARS;
if (mSize.y > 0.0f) {
if (mFilledTexture)
mFilledTexture->rasterizeAt(mSize.y, mSize.y);
if (mUnfilledTexture)
mUnfilledTexture->rasterizeAt(mSize.y, mSize.y);
}
mIconFilled.getTexture()->setSize(mSize.y, mSize.y);
mIconFilled.setResize(glm::vec2 {mSize.y * NUM_RATING_STARS, mSize.y}, true);
updateVertices();
}
void RatingComponent::updateVertices()
{
const float numStars {NUM_RATING_STARS};
const float h {getSize().y}; // Ss the same as a single star's width.
const float w {getSize().y * mValue * numStars};
const float fw {getSize().y * numStars};
// clang-format off
mVertices[0] = {{0.0f, 0.0f}, {0.0f, 1.0f}, mColorShift};
mVertices[1] = {{0.0f, h }, {0.0f, 0.0f}, mColorShift};
mVertices[2] = {{w, 0.0f}, {mValue * numStars, 1.0f}, mColorShift};
mVertices[3] = {{w, h }, {mValue * numStars, 0.0f}, mColorShift};
mVertices[4] = {{0.0f, 0.0f}, {0.0f, 1.0f}, mColorShift};
mVertices[5] = {{0.0f, h }, {0.0f, 0.0f}, mColorShift};
mVertices[6] = {{fw, 0.0f}, {numStars, 1.0f}, mColorShift};
mVertices[7] = {{fw, h }, {numStars, 0.0f}, mColorShift};
// clang-format on
}
void RatingComponent::updateColors()
{
for (int i = 0; i < 8; ++i)
mVertices[i].color = mColorShift;
mIconUnfilled.getTexture()->setSize(mSize.y, mSize.y);
mIconUnfilled.setResize(glm::vec2 {mSize.y * NUM_RATING_STARS, mSize.y}, true);
}
void RatingComponent::render(const glm::mat4& parentTrans)
{
if (!isVisible() || mFilledTexture == nullptr || mUnfilledTexture == nullptr ||
mThemeOpacity == 0.0f)
if (!isVisible() || mThemeOpacity == 0.0f || mOpacity == 0.0f)
return;
glm::mat4 trans {parentTrans * getTransform()};
mRenderer->setMatrix(trans);
if (mOpacity > 0.0f) {
if (Settings::getInstance()->getBool("DebugImage")) {
mRenderer->drawRect(0.0f, 0.0f, mSize.y * NUM_RATING_STARS, mSize.y, 0xFF000033,
0xFF000033);
}
mIconUnfilled.setOpacity(mOpacity * mThemeOpacity);
mIconUnfilled.render(trans);
if (mUnfilledTexture->bind()) {
if (mUnfilledColor != mColorShift) {
for (int i = 0; i < 8; ++i)
mVertices[i].color =
(mUnfilledColor & 0xFFFFFF00) + (mVertices[i].color & 0x000000FF);
}
// No need to render the filled texture if the value is zero.
if (mValue == 0.0f)
return;
mRenderer->drawTriangleStrips(&mVertices[4], 4);
mRenderer->bindTexture(0);
glm::ivec2 clipPos {static_cast<int>(std::round(trans[3].x)),
static_cast<int>(std::round(trans[3].y))};
glm::vec3 dimScaled {};
dimScaled.x = std::fabs(trans[3].x + mIconUnfilled.getSize().x);
dimScaled.y = std::fabs(trans[3].y + mIconUnfilled.getSize().y);
if (mUnfilledColor != mColorShift)
updateColors();
}
glm::ivec2 clipDim {static_cast<int>(std::round(dimScaled.x - std::round(trans[3].x)) * mValue),
static_cast<int>(std::round(dimScaled.y - trans[3].y))};
if (mFilledTexture->bind()) {
mRenderer->drawTriangleStrips(&mVertices[0], 4);
mRenderer->bindTexture(0);
}
}
renderChildren(trans);
mIconFilled.setOpacity(mOpacity * mThemeOpacity);
mRenderer->pushClipRect(clipPos, clipDim);
mIconFilled.render(trans);
mRenderer->popClipRect();
}
bool RatingComponent::input(InputConfig* config, Input input)
@ -207,11 +149,10 @@ bool RatingComponent::input(InputConfig* config, Input input)
// set the color shift accordingly.
if (mColorizeChanges) {
if (static_cast<int>(mValue * 10.0f) == mOriginalValue)
setColorShift(mColorOriginalValue);
mIconFilled.setColorShift(mColorOriginalValue);
else
setColorShift(mColorChangedValue);
mIconFilled.setColorShift(mColorChangedValue);
}
updateVertices();
}
return GuiComponent::input(config, input);
@ -228,44 +169,65 @@ void RatingComponent::applyTheme(const std::shared_ptr<ThemeData>& theme,
if (!elem)
return;
// Make sure the size is not unreasonably large (which may be caused by a mistake in
// the theme configuration).
mSize.x = glm::clamp(mSize.x, 0.0f, mRenderer->getScreenWidth() / 2.0f);
mSize.y = glm::clamp(mSize.y, 0.0f, mRenderer->getScreenHeight() / 2.0f);
GuiComponent::applyTheme(theme, view, element, properties ^ ThemeFlags::SIZE);
if (mSize.y == 0.0f)
mSize.y = mSize.x / NUM_RATING_STARS;
else if (mSize.x == 0.0f)
mSize.x = mSize.y * NUM_RATING_STARS;
glm::vec2 scale {getParent() ?
getParent()->getSize() :
glm::vec2(Renderer::getScreenWidth(), Renderer::getScreenHeight())};
const size_t sizeY {static_cast<size_t>(mSize.y)};
bool imgChanged {false};
if (properties & PATH && elem->has("filledPath")) {
mFilledTexture = TextureResource::get(elem->get<std::string>("filledPath"), true, false,
true, false, false, sizeY, sizeY);
imgChanged = true;
if (elem->has("size")) {
glm::vec2 ratingSize {elem->get<glm::vec2>("size")};
if (ratingSize == glm::vec2 {0.0f, 0.0f}) {
LOG(LogWarning) << "RatingComponent: Invalid theme configuration, property <size> "
"for element \""
<< element.substr(7) << "\" is set to zero";
ratingSize.y = 0.01;
}
if (ratingSize.x > 0.0f)
ratingSize.x = glm::clamp(ratingSize.x, 0.01f, 1.0f);
if (ratingSize.y > 0.0f)
ratingSize.y = glm::clamp(ratingSize.y, 0.01f, 0.5f);
mSize = ratingSize * scale;
if (mSize.y == 0.0f)
mSize.y = mSize.x / NUM_RATING_STARS;
else
mSize.x = mSize.y * NUM_RATING_STARS;
}
mIconFilled.setTileSize(mSize.y, mSize.y);
mIconFilled.setResize(glm::vec2 {mSize}, false);
if (properties & PATH && elem->has("filledPath")) {
mIconFilled.setDynamic(true);
mIconFilled.setImage(std::string(elem->get<std::string>("filledPath")), true);
mIconFilled.getTexture()->setSize(mSize.y, mSize.y);
if (!mIconFilled.getTexture()->getScalable())
mIconFilled.onSizeChanged();
}
else {
mIconFilled.setImage(std::string(":/graphics/star_filled.svg"), true);
}
mIconUnfilled.setTileSize(mSize.y, mSize.y);
mIconUnfilled.setResize(glm::vec2 {mSize}, false);
if (properties & PATH && elem->has("unfilledPath")) {
mUnfilledTexture = TextureResource::get(elem->get<std::string>("unfilledPath"), true, false,
true, false, false, sizeY, sizeY);
imgChanged = true;
mIconUnfilled.setDynamic(true);
mIconUnfilled.setImage(std::string(elem->get<std::string>("unfilledPath")), true);
mIconUnfilled.getTexture()->setSize(mSize.y, mSize.y);
if (!mIconUnfilled.getTexture()->getScalable())
mIconUnfilled.onSizeChanged();
}
else {
mIconUnfilled.setImage(std::string(":/graphics/star_unfilled.svg"), true);
}
if (properties & COLOR) {
if (elem->has("color"))
setColorShift(elem->get<unsigned int>("color"));
if (elem->has("unfilledColor"))
mUnfilledColor = elem->get<unsigned int>("unfilledColor");
else
mUnfilledColor = mColorShift;
if (elem->has("color")) {
mIconFilled.setColorShift(elem->get<unsigned int>("color"));
mIconUnfilled.setColorShift(elem->get<unsigned int>("color"));
}
}
GuiComponent::applyTheme(theme, view, element, properties);
if (imgChanged)
onSizeChanged();
}
std::vector<HelpPrompt> RatingComponent::getHelpPrompts()

View file

@ -11,11 +11,12 @@
#define ES_APP_COMPONENTS_RATING_COMPONENT_H
#include "GuiComponent.h"
#include "components/ImageComponent.h"
#include "renderers/Renderer.h"
class TextureResource;
#define NUM_RATING_STARS 5
#define NUM_RATING_STARS 5.0f
class RatingComponent : public GuiComponent
{
@ -32,14 +33,8 @@ public:
void render(const glm::mat4& parentTrans) override;
void onSizeChanged() override;
void setOpacity(float opacity) override;
void setDimming(float dimming) override;
// Multiply all pixels in the image by this color when rendering.
void setColorShift(unsigned int color) override;
unsigned int getColorShift() const override { return mColorShift; }
void setOriginalColor(unsigned int color) override { mColorOriginalValue = color; }
void setChangedColor(unsigned int color) override { mColorChangedValue = color; }
@ -51,23 +46,17 @@ public:
std::vector<HelpPrompt> getHelpPrompts() override;
private:
void updateVertices();
void updateColors();
Renderer* mRenderer;
ImageComponent mIconFilled;
ImageComponent mIconUnfilled;
float mValue;
int mOriginalValue;
unsigned int mColorOriginalValue;
unsigned int mColorChangedValue;
Renderer::Vertex mVertices[8];
unsigned int mColorShift;
unsigned int mColorShiftEnd;
unsigned int mUnfilledColor;
std::shared_ptr<TextureResource> mFilledTexture;
std::shared_ptr<TextureResource> mUnfilledTexture;
bool mColorizeChanges;
};

View file

@ -30,6 +30,8 @@ TextureData::TextureData(bool tile)
, mDataRGBA {}
, mWidth {0}
, mHeight {0}
, mTileWidth {0.0f}
, mTileHeight {0.0f}
, mSourceWidth {0.0f}
, mSourceHeight {0.0f}
, mScalable {false}
@ -72,8 +74,14 @@ bool TextureData::initSVGFromMemory(const std::string& fileData)
bool rasterize {true};
if (mTile) {
mSourceWidth = svgImage->width;
mSourceHeight = svgImage->height;
if (mTileWidth == 0.0f && mTileHeight == 0.0f) {
mSourceWidth = svgImage->width;
mSourceHeight = svgImage->height;
}
else {
mSourceWidth = static_cast<float>(mTileWidth);
mSourceHeight = static_cast<float>(mTileHeight);
}
}
// If there is no image size defined yet, then don't rasterize unless mForceRasterization has

View file

@ -57,6 +57,11 @@ public:
float sourceWidth();
float sourceHeight();
void setSourceSize(float width, float height);
void setTileSize(float tileWidth, float tileHeight)
{
mTileWidth = tileWidth;
mTileHeight = tileHeight;
}
glm::vec2 getSize() { return glm::vec2 {static_cast<int>(mWidth), static_cast<int>(mHeight)}; }
// Whether to use linear filtering when magnifying the texture.
@ -82,6 +87,8 @@ private:
std::vector<unsigned char> mDataRGBA;
std::atomic<int> mWidth;
std::atomic<int> mHeight;
std::atomic<float> mTileWidth;
std::atomic<float> mTileHeight;
std::atomic<float> mSourceWidth;
std::atomic<float> mSourceHeight;
std::atomic<bool> mScalable;

View file

@ -14,8 +14,14 @@
#define DEBUG_RASTER_CACHING false
#define DEBUG_SVG_CACHING false
TextureResource::TextureResource(
const std::string& path, bool tile, bool dynamic, bool linearMagnify, bool forceRasterization)
TextureResource::TextureResource(const std::string& path,
float tileWidth,
float tileHeight,
bool tile,
bool dynamic,
bool linearMagnify,
bool scalable,
bool forceRasterization)
: mTextureData {nullptr}
, mForceLoad {false}
{
@ -27,16 +33,17 @@ TextureResource::TextureResource(
if (dynamic) {
data = sTextureDataManager.add(this, tile);
data->initFromPath(path);
data->setTileSize(tileWidth, tileHeight);
data->setLinearMagnify(linearMagnify);
data->setForceRasterization(forceRasterization);
// Force the texture manager to load it using a blocking load.
sTextureDataManager.load(data, true);
}
else {
mTextureData = std::shared_ptr<TextureData>(new TextureData(tile));
data = mTextureData;
data->initFromPath(path);
data->setTileSize(tileWidth, tileHeight);
data->setLinearMagnify(linearMagnify);
data->setForceRasterization(forceRasterization);
// Load it so we can read the width/height.
@ -151,12 +158,14 @@ std::shared_ptr<TextureResource> TextureResource::get(const std::string& path,
bool linearMagnify,
bool forceRasterization,
size_t width,
size_t height)
size_t height,
float tileWidth,
float tileHeight)
{
const std::string canonicalPath {Utils::FileSystem::getCanonicalPath(path)};
if (canonicalPath.empty()) {
std::shared_ptr<TextureResource> tex(
new TextureResource("", tile, false, linearMagnify, forceRasterization));
std::shared_ptr<TextureResource> tex(new TextureResource(
"", tileWidth, tileHeight, tile, false, linearMagnify, false, forceRasterization));
// Make sure we get properly deinitialized even though we do nothing on reinitialization.
ResourceManager::getInstance().addReloadable(tex);
return tex;
@ -202,7 +211,8 @@ std::shared_ptr<TextureResource> TextureResource::get(const std::string& path,
// Need to create it.
std::shared_ptr<TextureResource> tex {std::shared_ptr<TextureResource>(
new TextureResource(std::get<0>(key), tile, dynamic, linearMagnify, forceRasterization))};
new TextureResource(std::get<0>(key), tileWidth, tileHeight, tile, dynamic, linearMagnify,
isScalable, forceRasterization))};
std::shared_ptr<TextureData> data {sTextureDataManager.get(tex.get())};
if (!isScalable || (isScalable && width != 0.0f && height != 0.0f)) {

View file

@ -33,7 +33,9 @@ public:
bool linearMagnify = false,
bool forceRasterization = false,
size_t width = 0,
size_t height = 0);
size_t height = 0,
float tileWidth = 0.0f,
float tileHeight = 0.0f);
void initFromPixels(const unsigned char* dataRGBA, size_t width, size_t height);
virtual void initFromMemory(const char* data, size_t length);
static void manualUnload(const std::string& path, bool tile);
@ -63,6 +65,11 @@ public:
virtual ~TextureResource();
bool isTiled() const;
void setSize(float width, float height)
{
mSize.x = static_cast<int>(std::round(width));
mSize.y = static_cast<int>(std::round(height));
}
const glm::ivec2 getSize() const { return mSize; }
bool bind();
@ -74,9 +81,12 @@ public:
protected:
TextureResource(const std::string& path,
float tileWidth,
float tileHeight,
bool tile,
bool dynamic,
bool linearMagnify,
bool scalable,
bool forceRasterization);
virtual void unload(ResourceManager& rm);
virtual void reload(ResourceManager& rm);