星座图优化
This commit is contained in:
@@ -29,9 +29,9 @@ target_include_directories(Aethera_render_2D PUBLIC
|
||||
"$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>"
|
||||
)
|
||||
target_compile_features(Aethera_render_2D PUBLIC cxx_std_20)
|
||||
target_link_libraries(Aethera_render_2D PUBLIC
|
||||
Aethera_Kernel
|
||||
blend2d::blend2d)
|
||||
target_link_libraries(Aethera_render_2D
|
||||
PUBLIC Aethera_Kernel
|
||||
PRIVATE blend2d::blend2d)
|
||||
if (MSVC)
|
||||
target_compile_options(Aethera_render_2D PRIVATE /utf-8)
|
||||
endif ()
|
||||
@@ -42,7 +42,8 @@ install(TARGETS Aethera_render_2D
|
||||
RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}")
|
||||
install(DIRECTORY "${Aethera_render_2D_source_dir}/"
|
||||
DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/render_2D"
|
||||
FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp" PATTERN "*.ipp")
|
||||
FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp" PATTERN "*.ipp"
|
||||
PATTERN "Blend2D_Cache.ipp" EXCLUDE)
|
||||
if (RENDERIVE_BUILD_TESTS)
|
||||
set(Aethera_render_2D_test_dir "${CMAKE_CURRENT_LIST_DIR}/tests")
|
||||
append_glob_source(Aethera_render_2D_test_sources "${Aethera_render_2D_test_dir}")
|
||||
|
||||
@@ -1,29 +1,7 @@
|
||||
#include "Frame_2D.hpp"
|
||||
#include <blend2d/core/pixelconverter.h>
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
namespace aethera::render_2d {
|
||||
namespace {
|
||||
const BLPixelConverter& rgba_converter() {
|
||||
static const BLPixelConverter converter = [] {
|
||||
BLFormatInfo source{};
|
||||
if (source.query(BL_FORMAT_PRGB32) != BL_SUCCESS)
|
||||
throw std::runtime_error("failed to query Blend2D PRGB32 format");
|
||||
constexpr std::uint8_t sizes[4]{8, 8, 8, 8};
|
||||
constexpr std::uint8_t shifts[4]{0, 8, 16, 24};
|
||||
BLFormatInfo destination{};
|
||||
destination.init(32, BL_FORMAT_FLAG_RGBA, sizes, shifts);
|
||||
if (destination.sanitize() != BL_SUCCESS)
|
||||
throw std::runtime_error("failed to describe browser RGBA8 format");
|
||||
BLPixelConverter result;
|
||||
if (result.create(destination, source) != BL_SUCCESS)
|
||||
throw std::runtime_error("failed to create Blend2D RGBA8 converter");
|
||||
return result;
|
||||
}();
|
||||
return converter;
|
||||
}
|
||||
}
|
||||
struct Frame_2D::Private {
|
||||
Blend2D_Cache color{}; /* Scene 直接合成的原生 PRGB32 颜色层。 */
|
||||
Pixel_Format output_format{native_pixel_format}; /* 调用方为本帧选择的最终协议格式。 */
|
||||
@@ -45,24 +23,7 @@ Pixel_Format Frame_2D::output_format() const noexcept {
|
||||
}
|
||||
Image_View Frame_2D::image() const { return d->color.view(); }
|
||||
Pixel_Buffer Frame_2D::output_pixels() const {
|
||||
const Image_View source = image();
|
||||
Pixel_Buffer result{{}, source.width, source.height, d->output_format};
|
||||
if (source.empty()) return result;
|
||||
const std::size_t row_size = static_cast<std::size_t>(source.width) * 4U;
|
||||
result.bytes.resize(row_size * static_cast<std::size_t>(source.height));
|
||||
if (d->output_format == native_pixel_format) {
|
||||
for (int y = 0; y < source.height; ++y)
|
||||
std::memcpy(result.bytes.data() + static_cast<std::size_t>(y) * row_size,
|
||||
source.data + static_cast<std::ptrdiff_t>(y) * source.stride,
|
||||
row_size);
|
||||
return result;
|
||||
}
|
||||
if (rgba_converter().convert_rect(
|
||||
result.bytes.data(), static_cast<intptr_t>(row_size), source.data,
|
||||
source.stride, static_cast<std::uint32_t>(source.width),
|
||||
static_cast<std::uint32_t>(source.height)) != BL_SUCCESS)
|
||||
throw std::runtime_error("Blend2D failed to convert PRGB32 to RGBA8");
|
||||
return result;
|
||||
return d->color.output_pixels(d->output_format);
|
||||
}
|
||||
Blend2D_Cache& detail::Frame_2D_Access::render_target(Frame_2D* frame) {
|
||||
if (!frame)
|
||||
|
||||
@@ -21,7 +21,7 @@ public:
|
||||
explicit Frame_2D(Frame_Identity identity,
|
||||
Pixel_Format output_format = native_pixel_format);
|
||||
~Frame_2D() override;
|
||||
/* 保留 Blend2D 图像分配,只重启物理槽承载的新逻辑帧。 */
|
||||
/* 保留二维后端图像分配,只重启物理槽承载的新逻辑帧。 */
|
||||
void begin(Frame_Identity identity,
|
||||
Pixel_Format output_format = native_pixel_format);
|
||||
[[nodiscard]] Pixel_Format output_format() const noexcept;
|
||||
|
||||
@@ -101,8 +101,13 @@ void Constellation_Diagram::Private::paint(Object* object) {
|
||||
return;
|
||||
}
|
||||
detail::Painter painter(cache, prepared.canvas);
|
||||
for (const auto& anchor : prepared.anchors) painter.circle(anchor, 4.0, Pen{state.anchor_color}, Brush{state.anchor_color, Brush_Style::solid});
|
||||
for (const auto& point : prepared.points) painter.circle(point, 2.0, Pen{state.point_color}, Brush{state.point_color, Brush_Style::solid});
|
||||
/*
|
||||
* 原实现每个点都会创建 BLPath,再分别 fill + stroke;这里改为同色实心圆批量入口。
|
||||
* 旧样式是 radius + 1px 同色描边,最终外半径约增加 0.5px,因此直接使用 4.5/2.5
|
||||
* 保持视觉尺寸,同时只做一次填充状态设置并走后端原生 circle primitive。
|
||||
*/
|
||||
painter.solid_circles(prepared.anchors, 4.5, state.anchor_color);
|
||||
painter.solid_circles(prepared.points, 2.5, state.point_color);
|
||||
}
|
||||
template <typename Object, typename Owner, typename Member, typename Prop_Type>
|
||||
void Constellation_Diagram::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access<Prop_Type>) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "Blend2D_Cache.hpp"
|
||||
#include "Blend2D_Cache.ipp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
@@ -22,6 +23,24 @@ void Blend2D_Cache::Private::clear_image(BLImage& image) {
|
||||
std::memset(row, 0, static_cast<std::size_t>(data.size.w) * sizeof(Pixel));
|
||||
}
|
||||
}
|
||||
const BLPixelConverter& Blend2D_Cache::Private::rgba_converter() {
|
||||
static const BLPixelConverter converter = [] {
|
||||
BLFormatInfo source{};
|
||||
if (source.query(BL_FORMAT_PRGB32) != BL_SUCCESS)
|
||||
throw std::runtime_error("failed to query Blend2D PRGB32 format");
|
||||
constexpr std::uint8_t sizes[4]{8, 8, 8, 8};
|
||||
constexpr std::uint8_t shifts[4]{0, 8, 16, 24};
|
||||
BLFormatInfo destination{};
|
||||
destination.init(32, BL_FORMAT_FLAG_RGBA, sizes, shifts);
|
||||
if (destination.sanitize() != BL_SUCCESS)
|
||||
throw std::runtime_error("failed to describe browser RGBA8 format");
|
||||
BLPixelConverter result;
|
||||
if (result.create(destination, source) != BL_SUCCESS)
|
||||
throw std::runtime_error("failed to create Blend2D RGBA8 converter");
|
||||
return result;
|
||||
}();
|
||||
return converter;
|
||||
}
|
||||
Blend2D_Cache::Private::Private(const Private& other) {
|
||||
require_success(image.assign_deep(other.image), "copy Blend2D image");
|
||||
}
|
||||
@@ -81,6 +100,28 @@ Image_View Blend2D_Cache::view() const {
|
||||
return {static_cast<const std::byte*>(data.pixel_data), data.size.w, data.size.h,
|
||||
static_cast<int>(data.stride), Pixel_Format::bgra8_premultiplied};
|
||||
}
|
||||
Pixel_Buffer Blend2D_Cache::output_pixels(Pixel_Format format) const {
|
||||
const Image_View source = view();
|
||||
Pixel_Buffer result{{}, source.width, source.height, format};
|
||||
if (source.empty()) return result;
|
||||
const std::size_t row_size = static_cast<std::size_t>(source.width) * sizeof(Pixel);
|
||||
result.bytes.resize(row_size * static_cast<std::size_t>(source.height));
|
||||
if (format == Pixel_Format::bgra8_premultiplied) {
|
||||
for (int y = 0; y < source.height; ++y)
|
||||
std::memcpy(result.bytes.data() + static_cast<std::size_t>(y) * row_size,
|
||||
source.data + static_cast<std::ptrdiff_t>(y) * source.stride,
|
||||
row_size);
|
||||
return result;
|
||||
}
|
||||
if (format != Pixel_Format::rgba8)
|
||||
throw std::invalid_argument("unsupported 2D output pixel format");
|
||||
if (Private::rgba_converter().convert_rect(
|
||||
result.bytes.data(), static_cast<intptr_t>(row_size), source.data,
|
||||
source.stride, static_cast<std::uint32_t>(source.width),
|
||||
static_cast<std::uint32_t>(source.height)) != BL_SUCCESS)
|
||||
throw std::runtime_error("Blend2D failed to convert PRGB32 to RGBA8");
|
||||
return result;
|
||||
}
|
||||
namespace detail {
|
||||
void Painter::Private::load_font_face(BLFontFace& face, std::initializer_list<const char*> candidates) {
|
||||
for (const char* path : candidates) {
|
||||
@@ -120,7 +161,13 @@ BLStrokeJoin Painter::Private::stroke_join(Line_Join join) noexcept {
|
||||
default: return BL_STROKE_JOIN_MITER_CLIP;
|
||||
}
|
||||
}
|
||||
void Painter::Private::apply_fill(Color color) {
|
||||
if (fill_color && *fill_color == color) return;
|
||||
context.set_fill_style(rgba(color));
|
||||
fill_color = color;
|
||||
}
|
||||
void Painter::Private::apply_pen(const Pen& pen) {
|
||||
if (stroke_pen && *stroke_pen == pen) return;
|
||||
context.set_stroke_style(rgba(pen.color));
|
||||
context.set_stroke_width(std::max(0.1, pen.width));
|
||||
context.set_stroke_caps(stroke_cap(pen.cap));
|
||||
@@ -134,6 +181,7 @@ void Painter::Private::apply_pen(const Pen& pen) {
|
||||
pattern.append(std::max(1.0, pen.width * 2.0));
|
||||
}
|
||||
context.set_stroke_dash_array(pattern);
|
||||
stroke_pen = pen;
|
||||
}
|
||||
BLFont Painter::Private::make_font(const Font& font) {
|
||||
BLFont result;
|
||||
@@ -204,7 +252,11 @@ Painter::Clip_Scope::Clip_Scope(Painter& owner, Rect_F rect) {
|
||||
painter = &owner;
|
||||
}
|
||||
Painter::Clip_Scope::~Clip_Scope() {
|
||||
if (painter) painter->d->context.restore();
|
||||
if (!painter) return;
|
||||
painter->d->context.restore();
|
||||
/* restore 会回滚绘制状态;同步失效本地样式缓存,避免后续错误跳过状态设置。 */
|
||||
painter->d->fill_color.reset();
|
||||
painter->d->stroke_pen.reset();
|
||||
}
|
||||
Painter::Clip_Scope::Clip_Scope(Clip_Scope&& other) noexcept : painter(std::exchange(other.painter, nullptr)) {}
|
||||
Painter::operator bool() const noexcept { return d->active; }
|
||||
@@ -231,26 +283,44 @@ void Painter::polygon(std::span<const Point_F> points, const Pen& pen, const Bru
|
||||
BLPath path; path.move_to(points.front().x, points.front().y);
|
||||
for (std::size_t index = 1; index < points.size(); ++index) path.line_to(points[index].x, points[index].y);
|
||||
path.close();
|
||||
if (brush.enabled()) { d->context.set_fill_style(Private::rgba(brush.color)); d->context.fill_path(path); }
|
||||
if (brush.enabled()) { d->apply_fill(brush.color); d->context.fill_path(path); }
|
||||
if (pen.enabled()) { d->apply_pen(pen); d->context.stroke_path(path); }
|
||||
}
|
||||
void Painter::rect(Rect_F value, const Pen& pen, const Brush& brush) {
|
||||
value = value.normalized(); if (!d->active || value.empty()) return;
|
||||
const BLRect shape(value.x, value.y, value.width, value.height);
|
||||
if (brush.enabled()) { d->context.set_fill_style(Private::rgba(brush.color)); d->context.fill_rect(shape); }
|
||||
if (brush.enabled()) { d->apply_fill(brush.color); d->context.fill_rect(shape); }
|
||||
if (pen.enabled()) { d->apply_pen(pen); d->context.stroke_rect(shape); }
|
||||
}
|
||||
void Painter::circle(Point_F center, double radius, const Pen& pen, const Brush& brush) {
|
||||
if (!d->active || radius <= 0.0) return;
|
||||
BLPath path; path.add_ellipse(BLEllipse(center.x, center.y, radius, radius));
|
||||
if (brush.enabled()) { d->context.set_fill_style(Private::rgba(brush.color)); d->context.fill_path(path); }
|
||||
if (pen.enabled()) { d->apply_pen(pen); d->context.stroke_path(path); }
|
||||
/* 同色实心填充 + 实线描边等价为扩大半径的一次实心圆,避免重复栅格化。 */
|
||||
if (brush.enabled() && pen.enabled() && brush.style == Brush_Style::solid &&
|
||||
pen.style == Line_Style::solid && brush.color == pen.color) {
|
||||
d->apply_fill(brush.color);
|
||||
d->context.fill_circle(center.x, center.y, radius + pen.width * 0.5);
|
||||
return;
|
||||
}
|
||||
if (brush.enabled()) {
|
||||
d->apply_fill(brush.color);
|
||||
d->context.fill_circle(center.x, center.y, radius);
|
||||
}
|
||||
if (pen.enabled()) {
|
||||
d->apply_pen(pen);
|
||||
d->context.stroke_circle(center.x, center.y, radius);
|
||||
}
|
||||
}
|
||||
void Painter::solid_circles(std::span<const Point_F> centers, double radius, Color color) {
|
||||
if (!d->active || centers.empty() || radius <= 0.0 || color.alpha == 0) return;
|
||||
d->apply_fill(color);
|
||||
for (const auto& center : centers)
|
||||
d->context.fill_circle(center.x, center.y, radius);
|
||||
}
|
||||
void Painter::text(Point_F position, std::string_view value, const Font& font, const Pen& pen,
|
||||
double rotation_degrees) {
|
||||
if (!d->active || value.empty() || !pen.enabled()) return;
|
||||
BLFont bl_font = Private::make_font(font); if (!bl_font) return;
|
||||
d->context.set_fill_style(Private::rgba(pen.color));
|
||||
d->apply_fill(pen.color);
|
||||
const double baseline = position.y + bl_font.metrics().ascent;
|
||||
const bool rotated = std::isfinite(rotation_degrees) && rotation_degrees != 0.0;
|
||||
if (rotated) { d->context.save(); d->context.rotate(rotation_degrees * std::numbers::pi / 180.0, position.x, baseline); }
|
||||
|
||||
@@ -8,7 +8,12 @@ namespace aethera::render_2d {
|
||||
namespace detail {
|
||||
struct Painter;
|
||||
}
|
||||
/* 可作为双缓冲值深拷贝的 Blend2D 像素缓存。 */
|
||||
/*
|
||||
* 二维像素缓存的后端封装。
|
||||
*
|
||||
* 该头文件故意不包含任何 Blend2D 头文件,也不暴露 BL* 类型;
|
||||
* 具体 Blend2D 图像/上下文实现全部留在 render 目录的实现文件中。
|
||||
*/
|
||||
struct Blend2D_Cache : Color_Cache {
|
||||
struct Private;
|
||||
Blend2D_Cache();
|
||||
@@ -27,12 +32,17 @@ struct Blend2D_Cache : Color_Cache {
|
||||
[[nodiscard]] Size size() const noexcept;
|
||||
/* 返回由缓存拥有、在下次非 const 修改前有效的只读像素视图。 */
|
||||
[[nodiscard]] Image_View view() const;
|
||||
/*
|
||||
* 将内部原生像素复制/转换为指定协议格式。
|
||||
* 像素转换也由 render 后端完成,调用方无需包含任何 Blend2D 头文件。
|
||||
*/
|
||||
[[nodiscard]] Pixel_Buffer output_pixels(Pixel_Format format) const;
|
||||
private:
|
||||
friend struct detail::Painter;
|
||||
std::unique_ptr<Private> d; /* Blend2D 图像实现的唯一所有权。 */
|
||||
};
|
||||
namespace detail {
|
||||
/* 只在 Renderable::Private 绘制实现中使用的 Blend2D 画笔。 */
|
||||
/* 只在 Renderable::Private 绘制实现中使用的二维画笔;后端类型保持隐藏。 */
|
||||
struct Painter {
|
||||
struct Private;
|
||||
struct Clip_Scope {
|
||||
@@ -58,6 +68,11 @@ struct Painter {
|
||||
void polygon(std::span<const Point_F> points, const Pen& pen, const Brush& brush);
|
||||
void rect(Rect_F rect, const Pen& pen, const Brush& brush = {});
|
||||
void circle(Point_F center, double radius, const Pen& pen, const Brush& brush = {});
|
||||
/*
|
||||
* 同色实心圆批量入口。内部只设置一次填充状态,并走后端原生 circle primitive;
|
||||
* 用于星座图等大量小圆点,避免每个点创建通用 Path 和重复设置样式。
|
||||
*/
|
||||
void solid_circles(std::span<const Point_F> centers, double radius, Color color);
|
||||
void text(Point_F position,
|
||||
std::string_view value,
|
||||
const Font& font,
|
||||
@@ -69,8 +84,7 @@ struct Painter {
|
||||
std::span<const Pixel> pixels,
|
||||
Image_Interpolation_Mode interpolation);
|
||||
private:
|
||||
std::unique_ptr<Private> d; /* 当前 BLContext 及绘制辅助算法的唯一所有权。 */
|
||||
std::unique_ptr<Private> d; /* 当前后端绘制上下文及辅助算法的唯一所有权。 */
|
||||
};
|
||||
}
|
||||
}
|
||||
#include "Blend2D_Cache.ipp"
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#pragma once
|
||||
#include <blend2d/blend2d.h>
|
||||
#include <blend2d/core/pixelconverter.h>
|
||||
#include <array>
|
||||
#include <initializer_list>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
namespace aethera::render_2d {
|
||||
struct Blend2D_Cache::Private {
|
||||
@@ -13,14 +15,20 @@ struct Blend2D_Cache::Private {
|
||||
static void require_success(BLResult result, const char* operation);
|
||||
/* 将图像全部清零为透明像素。 */
|
||||
static void clear_image(BLImage& image);
|
||||
/* 进程级 PRGB32 -> RGBA8 转换器,只在后端实现中可见。 */
|
||||
[[nodiscard]] static const BLPixelConverter& rgba_converter();
|
||||
};
|
||||
namespace detail {
|
||||
struct Painter::Private {
|
||||
BLContext context{}; /* 当前缓存图像上的 Blend2D 绘制上下文。 */
|
||||
bool active{}; /* context 是否已经成功 begin 且尚未 end。 */
|
||||
std::optional<Color> fill_color{}; /* 已应用的填充颜色,减少重复状态写入。 */
|
||||
std::optional<Pen> stroke_pen{}; /* 已应用的描边参数,减少重复状态写入。 */
|
||||
/* 将项目颜色转换为 Blend2D 浮点颜色。 */
|
||||
[[nodiscard]] static BLRgba rgba(Color color) noexcept;
|
||||
/* 按 Pen 配置当前描边样式。 */
|
||||
/* 仅当颜色变化时更新当前填充样式。 */
|
||||
void apply_fill(Color color);
|
||||
/* 仅当 Pen 变化时更新当前描边样式。 */
|
||||
void apply_pen(const Pen& pen);
|
||||
/* 从系统字体候选中创建对应字号和字形的 Blend2D 字体。 */
|
||||
[[nodiscard]] static BLFont make_font(const Font& font);
|
||||
|
||||
Reference in New Issue
Block a user