mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-11-01 03:57:17 +01:00
UI: Rounded rectangles from precomputed atlas corners
UIAtlas generates an antialiased white corner for each radius in ROUND_RECT_RADII (8, 16, 24, 32 UI units) and each style (filled, and thin/medium/thick outlines from ROUND_RECT_THICKNESS) at the atlas DPI. DrawBuffer::DrawRoundRect mirrors the corner into all four corners and stretches the corner's last texel row/column along the edges, so a round rect is 9 quads (8 for outlines). Other radii use the nearest corner, scaled. The LogoScreen temporarily draws some for testing. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
42bb9b134b
commit
110fb7fd6a
5 files changed
+182
-2
No files matched your search
@@ -429,6 +429,84 @@ void DrawBuffer::DrawImage4Grid(ImageID atlas_image, float x1, float y1, float x
|
||||
DrawTexRect(xb, yb, x2, y2, um, vm, u2, v2, color);
|
||||
}
|
||||
|
||||
const char *RoundRectCornerImageName(int radiusIndex, RoundRectStyle style) {
|
||||
// Generated from the indices, so the tables in the header are all there is to edit.
|
||||
struct Names {
|
||||
char names[ROUND_RECT_RADIUS_COUNT][(int)RoundRectStyle::COUNT][16];
|
||||
Names() {
|
||||
for (int r = 0; r < ROUND_RECT_RADIUS_COUNT; r++) {
|
||||
for (int s = 0; s < (int)RoundRectStyle::COUNT; s++) {
|
||||
snprintf(names[r][s], sizeof(names[r][s]), "I_RRECT_%d_%d", r, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
static const Names names;
|
||||
return names.names[radiusIndex][(int)style];
|
||||
}
|
||||
|
||||
void DrawBuffer::DrawRoundRect(float x1, float y1, float x2, float y2, float radius, Color color, RoundRectStyle style) {
|
||||
if (!atlas || x2 <= x1 || y2 <= y1) {
|
||||
return;
|
||||
}
|
||||
if (roundRectAtlasImages_ != atlas->images) {
|
||||
for (int r = 0; r < ROUND_RECT_RADIUS_COUNT; r++) {
|
||||
for (int s = 0; s < (int)RoundRectStyle::COUNT; s++) {
|
||||
roundRectCorners_[r][s] = atlas->getImage(ImageID(RoundRectCornerImageName(r, (RoundRectStyle)s)));
|
||||
}
|
||||
}
|
||||
roundRectAtlasImages_ = atlas->images;
|
||||
}
|
||||
|
||||
int radiusIndex = 0;
|
||||
for (int i = 1; i < ROUND_RECT_RADIUS_COUNT; i++) {
|
||||
if (fabsf(ROUND_RECT_RADII[i] - radius) < fabsf(ROUND_RECT_RADII[radiusIndex] - radius)) {
|
||||
radiusIndex = i;
|
||||
}
|
||||
}
|
||||
const AtlasImage *image = roundRectCorners_[radiusIndex][(int)style];
|
||||
if (!image) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool filled = style == RoundRectStyle::Filled;
|
||||
const float dpiScale = atlas->dpiScale;
|
||||
const float r = ROUND_RECT_RADII[radiusIndex] * dpiScale;
|
||||
const int center = RoundRectCornerCenter(r);
|
||||
|
||||
// The corner quad runs from the center of texel 0 to the center of the last texel, which holds
|
||||
// the straight edge profile that gets stretched along the sides. Shrink it to fit small rects.
|
||||
float scale = std::max(radius, 0.0f) / ROUND_RECT_RADII[radiusIndex];
|
||||
const float maxScale = std::min(x2 - x1, y2 - y1) * dpiScale / (2.0f * (r + 0.5f));
|
||||
scale = std::min(scale, maxScale);
|
||||
if (scale <= 0.0f) {
|
||||
if (filled) {
|
||||
DrawImageCenterTexel(ImageID("I_SOLIDWHITE"), x1, y1, x2, y2, color);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const float texel = scale / dpiScale;
|
||||
const float outside = (center - r - 0.5f) * texel; // From the rect edge out to texel 0's center.
|
||||
const float inside = (r + 0.5f) * texel; // From the rect edge in to the last texel's center.
|
||||
|
||||
const float xs[4] = { x1 - outside, x1 + inside, x2 - inside, x2 + outside };
|
||||
const float ys[4] = { y1 - outside, y1 + inside, y2 - inside, y2 + outside };
|
||||
const float us[4] = { image->u1, image->u2, image->u2, image->u1 };
|
||||
const float vs[4] = { image->v1, image->v2, image->v2, image->v1 };
|
||||
|
||||
for (int y = 0; y < 3; y++) {
|
||||
if (ys[y + 1] <= ys[y]) {
|
||||
continue;
|
||||
}
|
||||
for (int x = 0; x < 3; x++) {
|
||||
if (xs[x + 1] <= xs[x] || (x == 1 && y == 1 && !filled)) {
|
||||
continue;
|
||||
}
|
||||
DrawTexRect(xs[x], ys[y], xs[x + 1], ys[y + 1], us[x], vs[y], us[x + 1], vs[y + 1], color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DrawBuffer::DrawImage2GridH(ImageID atlas_image, float x1, float y1, float x2, Color color, float corner_scale) {
|
||||
const AtlasImage *image = atlas->getImage(atlas_image);
|
||||
float um = (image->u1 + image->u2) * 0.5f;
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <cmath>
|
||||
#include <iterator>
|
||||
|
||||
#include "Common/Data/Color/RGBAUtil.h"
|
||||
#include "Common/Render/TextureAtlas.h"
|
||||
@@ -50,6 +52,30 @@ struct GradientStop {
|
||||
|
||||
class TextDrawer;
|
||||
|
||||
// Rounded rectangles are drawn from antialiased corner images that UI/UIAtlas.cpp generates
|
||||
// into the UI atlas, one per radius and style.
|
||||
enum class RoundRectStyle : uint8_t {
|
||||
Filled,
|
||||
OutlineThin,
|
||||
OutlineMedium,
|
||||
OutlineThick,
|
||||
COUNT,
|
||||
};
|
||||
|
||||
// Corner radii in UI units, each of which gets an image per style. Other radii scale the nearest one.
|
||||
// Keep them larger than the thickest outline.
|
||||
constexpr float ROUND_RECT_RADII[] = { 8.0f, 16.0f, 24.0f, 32.0f };
|
||||
constexpr int ROUND_RECT_RADIUS_COUNT = (int)std::size(ROUND_RECT_RADII);
|
||||
// Outline thickness in UI units, per RoundRectStyle.
|
||||
constexpr float ROUND_RECT_THICKNESS[(int)RoundRectStyle::COUNT] = { 0.0f, 1.0f, 2.0f, 3.0f };
|
||||
|
||||
const char *RoundRectCornerImageName(int radiusIndex, RoundRectStyle style);
|
||||
// The corner image is (center + 1) texels square. The arc is centered on texel corner (center, center),
|
||||
// and the last row and column are the straight edge profile.
|
||||
inline int RoundRectCornerCenter(float radiusPx) {
|
||||
return (int)ceilf(radiusPx) + 1;
|
||||
}
|
||||
|
||||
class DrawBuffer {
|
||||
public:
|
||||
DrawBuffer(const DrawBuffer &) = delete;
|
||||
@@ -149,6 +175,13 @@ public:
|
||||
// Results in 18 triangles. Kind of expensive for a button.
|
||||
void DrawImage4Grid(ImageID atlas_image, float x1, float y1, float x2, float y2, Color color = COLOR(0xFFFFFF), float corner_scale = 1.0);
|
||||
|
||||
// Uses the nearest precomputed corner radius, scaled to fit, so outlines get thinner or thicker
|
||||
// when radius isn't one of ROUND_RECT_RADII. 18 triangles filled, 16 outlined.
|
||||
void DrawRoundRect(float x1, float y1, float x2, float y2, float radius, Color color, RoundRectStyle style = RoundRectStyle::Filled);
|
||||
void DrawRoundRect(const Bounds &bounds, float radius, Color color, RoundRectStyle style = RoundRectStyle::Filled) {
|
||||
DrawRoundRect(bounds.x, bounds.y, bounds.x2(), bounds.y2(), radius, color, style);
|
||||
}
|
||||
|
||||
// This is only 6 triangles, much cheaper.
|
||||
void DrawImage2GridH(ImageID atlas_image, float x1, float y1, float x2, Color color = COLOR(0xFFFFFF), float scale = 1.0);
|
||||
|
||||
@@ -225,6 +258,10 @@ private:
|
||||
const Atlas *atlas = nullptr;
|
||||
const Atlas *fontAtlas_ = nullptr;
|
||||
|
||||
// Atlas lookups are linear searches by name, so the corner images are looked up once per atlas.
|
||||
const AtlasImage *roundRectCorners_[ROUND_RECT_RADIUS_COUNT][(int)RoundRectStyle::COUNT]{};
|
||||
const AtlasImage *roundRectAtlasImages_ = nullptr;
|
||||
|
||||
bool inited_ = false;
|
||||
float fontscalex = 1.0f;
|
||||
float fontscaley = 1.0f;
|
||||
|
||||
@@ -143,6 +143,8 @@ struct Atlas {
|
||||
int num_fonts = 0;
|
||||
AtlasImage *images = nullptr;
|
||||
int num_images = 0;
|
||||
// Texels per UI unit that generated images were rasterized at. Not stored in metadata.
|
||||
float dpiScale = 1.0f;
|
||||
|
||||
// These are inefficient linear searches, try not to call every frame.
|
||||
const AtlasFont *getFont(FontID id) const;
|
||||
|
||||
+28
-1
@@ -468,7 +468,34 @@ void LogoScreen::DrawForeground(UIContext &dc) {
|
||||
alphaText = 3.0f - t;
|
||||
uint32_t textColor = colorAlpha(dc.GetTheme().infoStyle.fgColor, alphaText);
|
||||
|
||||
auto cr = GetI18NCategory(I18NCat::PSPCREDITS);
|
||||
// TEMPORARY: Round rect test. Fixed seed so they stay put between frames.
|
||||
{
|
||||
uint32_t seed = 12345;
|
||||
auto rnd = [&seed]() {
|
||||
seed = seed * 1664525 + 1013904223;
|
||||
return (float)(seed >> 8) * (1.0f / 16777216.0f);
|
||||
};
|
||||
for (int i = 0; i < 24; i++) {
|
||||
const float w = 20.0f + rnd() * 200.0f;
|
||||
const float h = 20.0f + rnd() * 120.0f;
|
||||
const float x = bounds.x + rnd() * (bounds.w - w);
|
||||
const float y = bounds.y + rnd() * (bounds.h - h);
|
||||
const float radius = 2.0f + rnd() * 20.0f;
|
||||
const RoundRectStyle style = (RoundRectStyle)(int)(rnd() * (float)RoundRectStyle::COUNT);
|
||||
const uint32_t color = 0x80000000 | ((uint32_t)(rnd() * 16777215.0f) & 0xFFFFFF);
|
||||
dc.Draw()->DrawRoundRect(x, y, x + w, y + h, radius, color, style);
|
||||
}
|
||||
// Every precomputed corner at its native size.
|
||||
for (int r = 0; r < ROUND_RECT_RADIUS_COUNT; r++) {
|
||||
for (int s = 0; s < (int)RoundRectStyle::COUNT; s++) {
|
||||
const float x = bounds.x + 10.0f + s * 130.0f;
|
||||
const float y = bounds.y + 10.0f + r * 90.0f;
|
||||
dc.Draw()->DrawRoundRect(x, y, x + 120.0f, y + 80.0f, ROUND_RECT_RADII[r], 0xFFFFFFFF, (RoundRectStyle)s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto cr =GetI18NCategory(I18NCat::PSPCREDITS);
|
||||
auto gr = GetI18NCategory(I18NCat::GRAPHICS);
|
||||
char temp[256];
|
||||
|
||||
|
||||
+37
-1
@@ -9,6 +9,7 @@
|
||||
#include "Common/UI/Context.h"
|
||||
#include "Common/Data/Format/PNGLoad.h"
|
||||
#include "Common/Render/AtlasGen.h"
|
||||
#include "Common/Render/DrawBuffer.h"
|
||||
#include "Common/Render/ManagedTexture.h"
|
||||
#include "Common/Common.h"
|
||||
#include "Common/Thread/ParallelLoop.h"
|
||||
@@ -40,6 +41,9 @@ Atlas *GetUIAtlas() {
|
||||
struct ImageMeta {
|
||||
std::string_view id;
|
||||
bool addShadow = false;
|
||||
// If set, the image is a generated round rect corner (see DrawBuffer::DrawRoundRect). In UI units.
|
||||
float cornerRadius = 0.0f;
|
||||
float cornerThickness = 0.0f; // 0 = filled
|
||||
};
|
||||
|
||||
// We add shadows to all line-art images that are used for buttons, to improve visibility.
|
||||
@@ -375,6 +379,28 @@ static bool RasterizeSVG(std::string_view filename, float dpiScale, int maxTextu
|
||||
return true;
|
||||
}
|
||||
|
||||
// White, premultiplied, antialiased by distance to the edge.
|
||||
static void GenerateRoundRectCorner(Image &img, float radius, float thickness, float dpiScale) {
|
||||
const float r = radius * dpiScale;
|
||||
const float t = thickness * dpiScale;
|
||||
const int center = RoundRectCornerCenter(r);
|
||||
img.resize(center + 1, center + 1);
|
||||
img.scale = dpiScale;
|
||||
for (int y = 0; y <= center; y++) {
|
||||
const float dy = std::max((float)center - (y + 0.5f), 0.0f);
|
||||
for (int x = 0; x <= center; x++) {
|
||||
const float dx = std::max((float)center - (x + 0.5f), 0.0f);
|
||||
const float d = sqrtf(dx * dx + dy * dy);
|
||||
float coverage = std::clamp(r - d + 0.5f, 0.0f, 1.0f);
|
||||
if (t > 0.0f) {
|
||||
coverage -= std::clamp(r - t - d + 0.5f, 0.0f, 1.0f);
|
||||
}
|
||||
const u32 a = (u32)(coverage * 255.0f + 0.5f);
|
||||
img.set1(x, y, a * 0x01010101);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool GenerateUIAtlasImage(Atlas *atlas, float dpiScale, Image *dest, int maxTextureSize, const ImageMeta *imageIDs, size_t imageCount) {
|
||||
Bucket bucket;
|
||||
|
||||
@@ -445,6 +471,8 @@ static bool GenerateUIAtlasImage(Atlas *atlas, float dpiScale, Image *dest, int
|
||||
} else if (equals(imageIDs[i].id, "I_EMPTY")) {
|
||||
img.resize(16, 16);
|
||||
img.fill(0);
|
||||
} else if (imageIDs[i].cornerRadius > 0.0f) {
|
||||
GenerateRoundRectCorner(img, imageIDs[i].cornerRadius, imageIDs[i].cornerThickness, dpiScale);
|
||||
} else {
|
||||
std::string name = "ui_images/";
|
||||
std::string pngName = PNGNameFromID(imageIDs[i].id);
|
||||
@@ -491,6 +519,7 @@ static bool GenerateUIAtlasImage(Atlas *atlas, float dpiScale, Image *dest, int
|
||||
atlas->images = new AtlasImage[genAtlasImages.size()];
|
||||
std::copy(genAtlasImages.begin(), genAtlasImages.end(), atlas->images);
|
||||
atlas->num_images = (int)genAtlasImages.size();
|
||||
atlas->dpiScale = dpiScale;
|
||||
|
||||
// For debug, write out the atlas.
|
||||
if (SAVE_DEBUG_ATLAS) {
|
||||
@@ -510,8 +539,15 @@ Draw::Texture *GenerateUIAtlas(Draw::DrawContext *draw, Atlas *atlas, float dpiS
|
||||
INFO_LOG(Log::G3D, "Regenerating atlas (empty: %s). Dpi scale (changed: %s): %0.2f (invalidate=%d)",
|
||||
g_cachedUIAtlasImage.IsEmpty() ? "true" : "false", dpiScale != g_cachedDpiScale ? "true" : "false", dpiScale, invalidate);
|
||||
|
||||
std::vector<ImageMeta> imageIDs(std::begin(g_uiImageIDs), std::end(g_uiImageIDs));
|
||||
for (int r = 0; r < ROUND_RECT_RADIUS_COUNT; r++) {
|
||||
for (int style = 0; style < (int)RoundRectStyle::COUNT; style++) {
|
||||
imageIDs.push_back({ RoundRectCornerImageName(r, (RoundRectStyle)style), false, ROUND_RECT_RADII[r], ROUND_RECT_THICKNESS[style] });
|
||||
}
|
||||
}
|
||||
|
||||
g_cachedUIAtlasImage.clear();
|
||||
if (!GenerateUIAtlasImage(atlas, dpiScale, &g_cachedUIAtlasImage, draw->GetDeviceCaps().maxTextureSize, g_uiImageIDs, ARRAY_SIZE(g_uiImageIDs))) {
|
||||
if (!GenerateUIAtlasImage(atlas, dpiScale, &g_cachedUIAtlasImage, draw->GetDeviceCaps().maxTextureSize, imageIDs.data(), imageIDs.size())) {
|
||||
ERROR_LOG(Log::G3D, "Failed to generate UI atlas!");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user