三维频谱做好

This commit is contained in:
2026-08-22 20:08:57 +08:00
parent e2ba9b8384
commit cd028e8d4f
22 changed files with 1352 additions and 144 deletions
+2
View File
@@ -1,3 +1,5 @@
#pragma once
#include "axis/Axes_3D.hpp"
#include "camera/Camera_3D.hpp"
#include "scene/Render_Scene_3D.hpp"
#include "visual/Visuals.hpp"
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <plot/Axis.hpp>
#include <render_common.hpp>
namespace aethera::render_3d {
struct Axes_3D : Def<Axes_3D, Root> {
struct Prop : Prev_Prop {
plot::Axis_Descriptor x_axis{{0.0, 4.0}, plot::Axis_Scale::time,
"Time", "s", 6, 1, true, true, true};
plot::Axis_Descriptor y_axis{{10.0, 20'000.0}, plot::Axis_Scale::logarithmic,
"Frequency", "Hz", 6, 1, true, true, true};
plot::Axis_Descriptor z_axis{{18.0, 78.0}, plot::Axis_Scale::linear,
"SPL", "dB", 7, 0, true, true, true};
bool operator==(const Prop&) const = default;
};
struct State : Prev_State {
bool operator==(const State&) const = default;
};
struct Private : Prev_Private {};
};
} // namespace aethera::render_3d
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <plot/Camera.hpp>
#include <render_common.hpp>
namespace aethera::render_3d {
struct Camera_3D : Def<Camera_3D, Root> {
struct Prop : Prev_Prop {
plot::Camera_View initial_view{};
plot::Camera_Projection projection{plot::Camera_Projection::perspective};
plot::Camera_Control control{};
double vertical_field_of_view_degrees{45.0};
double near_plane{0.01};
double far_plane{100.0};
bool operator==(const Prop&) const = default;
};
struct State : Prev_State {
bool operator==(const State&) const = default;
};
struct Private : Prev_Private {};
};
} // namespace aethera::render_3d
@@ -10,7 +10,10 @@
#include <utility>
namespace aethera::render_3d::detail {
namespace {
float wheel_step(double pixel, double angle) { return static_cast<float>(pixel != 0.0 ? pixel : angle / 120.0); }
float wheel_step(double pixel, double angle) {
if (angle != 0.0) return static_cast<float>(angle / 120.0);
return static_cast<float>(pixel / 100.0);
}
Mouse_Button held_button(Mouse_Button_Mask buttons) { if ((buttons & 1U) != 0) return Mouse_Button::left; if ((buttons & 2U) != 0) return Mouse_Button::right; if ((buttons & 4U) != 0) return Mouse_Button::middle; return Mouse_Button::none; }
void record_datoviz_trace(Frame_3D* frame, const Datoviz_Frame_Trace& trace) {
if (trace.apply_ns) frame->record(Frame_Trace_Measurement::backend_apply_ns, trace.apply_ns);
@@ -1,10 +1,16 @@
#pragma once
#include "../visual/Prepared_Visual.hpp"
#include <event.hpp>
#include <plot/Axis.hpp>
#include <plot/Camera.hpp>
namespace aethera::render_3d::detail {
struct Scene_3D_Parameters {
Extent viewport{560, 320}; /* 离屏渲染目标尺寸,单位为像素。 */
Linear_Color clear_color{}; /* 每帧开始时写入的线性背景色。 */
plot::Camera_Descriptor camera{}; /* Camera 组件发布的本帧配置快照。 */
plot::Axis_Descriptor x_axis{}; /* 三维数据 X 轴的业务范围与标签策略。 */
plot::Axis_Descriptor y_axis{}; /* 三维数据 Y 轴的业务范围与标签策略。 */
plot::Axis_Descriptor z_axis{}; /* 三维数据 Z 轴的业务范围与标签策略。 */
bool operator==(const Scene_3D_Parameters&) const = default;
};
}
@@ -12,12 +12,18 @@
#include <map>
#include <memory>
#include <mutex>
#include <numbers>
#include <stdexcept>
#include <string>
#include <utility>
namespace aethera::render_3d::detail {
namespace {
constexpr std::uint64_t color_target_id = 0x5256504f494e54ULL;
DvzCapabilitySnapshot offscreen_capabilities() {
auto capabilities = dvz_capability_snapshot();
capabilities.supports_color_blending = true;
return capabilities;
}
std::uint64_t trace_now_ns() noexcept {
return static_cast<std::uint64_t>(
std::chrono::duration_cast<std::chrono::nanoseconds>(
@@ -73,6 +79,30 @@ DvzShapeAspect aspect(Point_Aspect value) {
}
return DVZ_SHAPE_ASPECT_FILLED;
}
float axis_position(const plot::Axis_Descriptor& axis, double coordinate) {
const auto origin = axis.range.origin;
const auto target = axis.range.target;
if (!std::isfinite(origin) || !std::isfinite(target) || origin == target)
throw std::invalid_argument("3D axis range must be finite and non-empty");
double ratio{};
if (axis.scale == plot::Axis_Scale::logarithmic) {
if (!(origin > 0.0) || !(target > 0.0) || !(coordinate > 0.0))
throw std::invalid_argument("logarithmic 3D axis coordinates must be positive");
ratio = (std::log10(coordinate) - std::log10(origin)) /
(std::log10(target) - std::log10(origin));
} else {
ratio = (coordinate - origin) / (target - origin);
}
return static_cast<float>(-1.0 + 2.0 * ratio);
}
std::string axis_title(const plot::Axis_Descriptor& axis) {
if (axis.label.empty()) return axis.unit;
if (axis.unit.empty()) return axis.label;
return axis.label + " (" + axis.unit + ")";
}
void configure_glyph_text(DvzScene* scene, DvzVisual* visual, const char* text) {
auto font_descriptor = dvz_font_desc();
font_descriptor.family = "Roboto";
@@ -139,6 +169,11 @@ void configure_glyph_text(DvzScene* scene, DvzVisual* visual, const char* text)
dvz_visual_set_depth_test(visual, false) != DVZ_OK)
throw std::runtime_error("failed to upload Datoviz text geometry");
}
bool supports_item_interaction(Visual_Family family) noexcept {
return family == Visual_Family::point || family == Visual_Family::pixel ||
family == Visual_Family::marker;
}
} // namespace
class Datoviz_Render_Context final {
public:
@@ -593,7 +628,7 @@ void Datoviz_Visual_Backend::require_domain() const {
void Datoviz_Visual_Backend::create_scene(Visual_Family family, const Scene_3D_Parameters& initial_scene) {
scene_ = dvz_scene();
if (scene_ == nullptr) throw std::runtime_error("failed to create Datoviz scene");
const auto capabilities = dvz_capability_snapshot();
const auto capabilities = offscreen_capabilities();
if (dvz_scene_set_capabilities(scene_, &capabilities) != DVZ_OK) throw std::runtime_error("failed to configure Datoviz scene capabilities");
figure_ = dvz_figure(scene_, initial_scene.viewport.width,
initial_scene.viewport.height, 0);
@@ -745,33 +780,33 @@ void Datoviz_Visual_Backend::create_scene(Visual_Family family, const Scene_3D_P
if (figure_ == nullptr || panel_ == nullptr || visual_ == nullptr ||
dvz_panel_add_visual(panel_, visual_, nullptr) != DVZ_OK)
throw std::runtime_error("failed to create Datoviz visual family");
DvzVisual* axes = dvz_segment(scene_, 0);
const std::array<vec3, 3> axis_starts{{{0.0F, 0.0F, 0.0F}, {0.0F, 0.0F, 0.0F}, {0.0F, 0.0F, 0.0F}}};
const std::array<vec3, 3> axis_ends{{{1.15F, 0.0F, 0.0F}, {0.0F, 1.15F, 0.0F}, {0.0F, 0.0F, 1.15F}}};
const std::array<DvzColor, 3> axis_colors{{{238, 72, 89, 230}, {64, 210, 143, 230}, {70, 132, 245, 230}}};
const std::array<float, 3> axis_widths{{3.0F, 3.0F, 3.0F}};
const std::array<DvzVisualDataUpdate, 4> axis_updates{{
{"position_start", axis_starts.data(), 3}, {"position_end", axis_ends.data(), 3},
{"color", axis_colors.data(), 3}, {"stroke_width_px", axis_widths.data(), 3}}};
if (axes == nullptr || dvz_visual_set_data_many(axes, axis_updates.data(), 4) != DVZ_OK ||
dvz_segment_set_caps(axes, DVZ_SEGMENT_CAP_BUTT, DVZ_SEGMENT_CAP_BUTT) != DVZ_OK ||
dvz_panel_add_visual(panel_, axes, nullptr) != DVZ_OK)
throw std::runtime_error("failed to create Datoviz coordinate axes");
DvzCameraDesc camera = dvz_camera_desc();
camera.view.eye[0] = 0.0F;
camera.view.eye[1] = 0.0F;
camera.view.eye[2] = 4.0F;
camera.view.target[0] = 0.0F;
camera.view.target[1] = 0.0F;
camera.view.target[2] = 0.0F;
camera.projection.near_clip = 0.01F;
camera.projection.far_clip = 100.0F;
if (dvz_panel_set_camera_desc(panel_, &camera) != DVZ_OK) throw std::runtime_error("failed to create Datoviz point camera");
DvzController* controller = dvz_arcball(scene_, nullptr);
arcball_ = controller != nullptr ? dvz_controller_arcball(controller) : nullptr;
if (controller == nullptr || arcball_ == nullptr ||
dvz_panel_bind_controller(panel_, controller, DVZ_DIM_MASK_XYZ) != DVZ_OK)
throw std::runtime_error("failed to bind Datoviz arcball controller");
if (supports_item_interaction(family)) {
if (dvz_visual_set_query_capabilities(
visual_, DVZ_QUERY_CAPABILITY_ITEM) != DVZ_OK)
throw std::runtime_error("failed to enable Datoviz item queries");
item_interaction_ = dvz_item_interaction(panel_, nullptr);
if (item_interaction_ == nullptr)
throw std::runtime_error("failed to create Datoviz item interaction");
}
axes_visual_ = dvz_segment(scene_, 0);
axes_text_ = dvz_text(panel_, 0);
if (axes_visual_ == nullptr || axes_text_ == nullptr ||
dvz_segment_set_caps(axes_visual_, DVZ_SEGMENT_CAP_BUTT,
DVZ_SEGMENT_CAP_BUTT) != DVZ_OK ||
dvz_visual_set_alpha_mode(axes_visual_, DVZ_ALPHA_OPAQUE) != DVZ_OK ||
dvz_panel_add_visual(panel_, axes_visual_, nullptr) != DVZ_OK)
throw std::runtime_error("failed to create Datoviz 3D axes visuals");
DvzTextPlacement axes_placement = dvz_text_placement();
axes_placement.mode = DVZ_TEXT_PLACEMENT_DATA;
axes_placement.anchor = DVZ_SCENE_ANCHOR_DATA;
axes_placement.depth_test = false;
DvzTextStyle axes_style = dvz_text_style();
axes_style.renderer = DVZ_TEXT_RENDERER_MSDF_ATLAS;
axes_style.size_px = 12.0F;
if (dvz_text_set_placement(axes_text_, &axes_placement) != DVZ_OK ||
dvz_text_set_style(axes_text_, &axes_style) != DVZ_OK)
throw std::runtime_error("failed to configure Datoviz 3D axes text");
apply_axes(initial_scene);
input_router_ = dvz_input_router();
gesture_handler_ = input_router_ != nullptr
? dvz_pointer_gesture_handler(input_router_)
@@ -779,12 +814,188 @@ void Datoviz_Visual_Backend::create_scene(Visual_Family family, const Scene_3D_P
if (input_router_ == nullptr || gesture_handler_ == nullptr ||
dvz_panel_connect_input(panel_, input_router_) != DVZ_OK)
throw std::runtime_error("failed to connect Datoviz point input");
apply_camera(initial_scene.camera);
DvzInputResizeEvent resize{
initial_scene.viewport.width, initial_scene.viewport.height,
initial_scene.viewport.width, initial_scene.viewport.height, 1.0F, 1.0F
};
dvz_input_emit_resize(input_router_, &resize);
}
void Datoviz_Visual_Backend::apply_axes(const Scene_3D_Parameters& scene) {
const std::array descriptors{scene.x_axis, scene.y_axis, scene.z_axis};
if (applied_axes_ && *applied_axes_ == descriptors) return;
using Position = std::array<float, 3>;
std::vector<Position> starts;
std::vector<Position> ends;
std::vector<DvzColor> colors;
std::vector<float> widths;
const DvzColor axis_color{190, 207, 226, 255};
const DvzColor grid_color{58, 70, 86, 255};
const DvzColor tick_color{145, 164, 188, 255};
const auto segment = [&](Position start, Position end, DvzColor color,
float width) {
starts.push_back(start);
ends.push_back(end);
colors.push_back(color);
widths.push_back(width);
};
std::vector<std::string> strings;
std::vector<DvzTextItem> text_items;
const auto text = [&](std::string value, Position position,
std::array<float, 2> offset,
std::array<float, 2> anchor, float size,
DvzColor color) {
strings.push_back(std::move(value));
DvzTextItem item{};
item.struct_size = sizeof(DvzTextItem);
item.position[0] = position[0];
item.position[1] = position[1];
item.position[2] = position[2];
item.offset[0] = offset[0];
item.offset[1] = offset[1];
item.anchor[0] = anchor[0];
item.anchor[1] = anchor[1];
item.size_px = size;
item.color = color;
text_items.push_back(item);
};
const auto append_axis = [&](const plot::Axis_Descriptor& axis,
std::size_t dimension) {
if (!axis.visible) return;
const auto ticks = plot::axis_ticks(axis);
if (dimension == 0)
segment({-1, -1, -1}, {1, -1, -1}, axis_color, 2.2F);
else if (dimension == 1)
segment({-1, -1, -1}, {-1, 1, -1}, axis_color, 2.2F);
else
segment({-1, -1, -1}, {-1, -1, 1}, axis_color, 2.2F);
for (const auto& tick : ticks) {
const auto position = axis_position(axis, tick.coordinate);
if (dimension == 0) {
segment({position, -1, -1}, {position, -1.055F, -1},
tick_color, 1.4F);
if (axis.grid_visible)
segment({position, -1, -1}, {position, 1, -1},
grid_color, 1.0F);
if (axis.labels_visible)
text(tick.label, {position, -1, -1}, {0, 12}, {.5F, 0},
11, tick_color);
} else if (dimension == 1) {
segment({-1, position, -1}, {-1.055F, position, -1},
tick_color, 1.4F);
if (axis.grid_visible)
segment({-1, position, -1}, {1, position, -1},
grid_color, 1.0F);
if (axis.labels_visible)
text(tick.label, {-1, position, -1}, {-10, 0}, {1, .5F},
11, tick_color);
} else {
segment({-1, -1, position}, {-1.055F, -1, position},
tick_color, 1.4F);
if (axis.grid_visible) {
segment({-1, -1, position}, {1, -1, position},
grid_color, 1.0F);
segment({-1, -1, position}, {-1, 1, position},
grid_color, 1.0F);
}
if (axis.labels_visible)
text(tick.label, {-1, -1, position}, {-10, 0}, {1, .5F},
11, tick_color);
}
}
const auto title = axis_title(axis);
if (title.empty()) return;
if (dimension == 0)
text(title, {0, -1, -1}, {0, 34}, {.5F, 0}, 14, axis_color);
else if (dimension == 1)
text(title, {-1, 0, -1}, {-78, 0}, {1, .5F}, 14, axis_color);
else
text(title, {-1, -1, 0}, {-78, 0}, {1, .5F}, 14, axis_color);
};
append_axis(descriptors[0], 0);
append_axis(descriptors[1], 1);
append_axis(descriptors[2], 2);
const auto segment_count = static_cast<std::uint32_t>(starts.size());
const std::array<DvzVisualDataUpdate, 4> updates{{
{"position_start", starts.data(), segment_count},
{"position_end", ends.data(), segment_count},
{"color", colors.data(), segment_count},
{"stroke_width_px", widths.data(), segment_count}}};
if (dvz_visual_set_data_many(axes_visual_, updates.data(),
static_cast<std::uint32_t>(updates.size())) != DVZ_OK ||
dvz_visual_set_visible(axes_visual_, !starts.empty()) != DVZ_OK)
throw std::runtime_error("failed to upload Datoviz 3D axes geometry");
for (std::size_t index = 0; index < text_items.size(); ++index)
text_items[index].string = strings[index].c_str();
if (dvz_text_set_items(
axes_text_, text_items.empty() ? nullptr : text_items.data(),
static_cast<std::uint32_t>(text_items.size())) != DVZ_OK)
throw std::runtime_error("failed to upload Datoviz 3D axes labels");
applied_axes_ = descriptors;
}
void Datoviz_Visual_Backend::apply_camera(const plot::Camera_Descriptor& source) {
if (applied_camera_ && *applied_camera_ == source) return;
if (turntable_ != nullptr) {
if (input_router_ != nullptr)
(void)dvz_turntable_disconnect(turntable_, input_router_);
dvz_turntable_destroy(turntable_);
turntable_ = nullptr;
}
DvzCameraDesc camera = dvz_camera_desc();
const auto assign = [](vec3 target, plot::Spatial_Point value) {
target[0] = static_cast<float>(value.x);
target[1] = static_cast<float>(value.y);
target[2] = static_cast<float>(value.z);
};
assign(camera.view.eye, source.initial_view.eye);
assign(camera.view.target, source.initial_view.target);
assign(camera.view.up, source.initial_view.up);
camera.projection.type = source.projection == plot::Camera_Projection::orthographic
? DVZ_CAMERA_ORTHOGRAPHIC
: DVZ_CAMERA_PERSPECTIVE;
camera.projection.fov_y = static_cast<float>(
source.vertical_field_of_view_degrees * std::numbers::pi / 180.0);
camera.projection.near_clip = static_cast<float>(source.near_plane);
camera.projection.far_clip = static_cast<float>(source.far_plane);
const auto dx = source.initial_view.eye.x - source.initial_view.target.x;
const auto dy = source.initial_view.eye.y - source.initial_view.target.y;
const auto dz = source.initial_view.eye.z - source.initial_view.target.z;
const auto distance = std::sqrt(dx * dx + dy * dy + dz * dz);
camera.projection.ortho_height = static_cast<float>(
2.0 * distance *
std::tan(source.vertical_field_of_view_degrees * std::numbers::pi / 360.0));
if (dvz_panel_set_camera_desc(panel_, &camera) != DVZ_OK)
throw std::runtime_error("failed to apply Datoviz Camera component");
DvzTurntableDesc turntable = dvz_turntable_desc();
turntable.initial_view = camera.view;
turntable.yaw_speed = static_cast<float>(source.control.yaw_speed);
turntable.pitch_speed = static_cast<float>(source.control.pitch_speed);
turntable.zoom_speed = static_cast<float>(source.control.zoom_speed);
turntable.pan_speed = static_cast<float>(source.control.pan_speed);
turntable.min_pitch = static_cast<float>(source.control.minimum_pitch);
turntable.max_pitch = static_cast<float>(source.control.maximum_pitch);
turntable.min_distance = static_cast<float>(source.control.minimum_distance);
turntable.max_distance = static_cast<float>(source.control.maximum_distance);
turntable.controller_flags = DVZ_TURNTABLE_FLAGS_WRAP_YAW |
DVZ_TURNTABLE_FLAGS_CLAMP_DISTANCE;
if (source.control.pan_enabled)
turntable.controller_flags |= DVZ_TURNTABLE_FLAGS_ALLOW_PAN;
turntable_ = dvz_turntable_create(&turntable);
if (turntable_ == nullptr ||
dvz_turntable_set_camera(turntable_, dvz_panel_camera(panel_)) != DVZ_OK ||
dvz_turntable_connect(turntable_, input_router_) != DVZ_OK)
throw std::runtime_error("failed to connect Datoviz Turntable to Panel Camera");
applied_camera_ = source;
}
void Datoviz_Visual_Backend::apply(const Scene_3D_Parameters& scene, const Prepared_Visual& point) {
require_domain();
if (!point.data) throw std::logic_error("3D prepared visual has no immutable payload");
@@ -799,6 +1010,8 @@ void Datoviz_Visual_Backend::apply(const Scene_3D_Parameters& scene, const Prepa
};
dvz_input_emit_resize(input_router_, &resize);
}
apply_camera(scene.camera);
apply_axes(scene);
if (point.revision == applied_visual_revision_) return;
{
mat4 transform{};
@@ -958,6 +1171,13 @@ void Datoviz_Visual_Backend::dispatch_pointer(
::aethera::Mouse_Button mouse_button,
::aethera::Keyboard_Modifier keyboard_modifiers, Extent viewport) {
require_domain();
if (applied_camera_) {
if (mouse_button == ::aethera::Mouse_Button::left &&
!applied_camera_->control.rotate_enabled) return;
if ((mouse_button == ::aethera::Mouse_Button::middle ||
mouse_button == ::aethera::Mouse_Button::right) &&
!applied_camera_->control.pan_enabled) return;
}
const float width = static_cast<float>(viewport.width);
const float height = static_cast<float>(viewport.height);
const DvzPointerEventType type =
@@ -975,6 +1195,7 @@ void Datoviz_Visual_Backend::dispatch_wheel(
float x, float y, float delta_x, float delta_y,
::aethera::Keyboard_Modifier keyboard_modifiers, Extent viewport) {
require_domain();
if (applied_camera_ && !applied_camera_->control.zoom_enabled) return;
dvz_pointer_emit_wheel(
input_router_, x, y, static_cast<float>(viewport.width),
static_cast<float>(viewport.height), delta_x, delta_y,
@@ -984,10 +1205,8 @@ void Datoviz_Visual_Backend::dispatch_key(
const ::aethera::Key_Event& event) {
require_domain();
if (event.key == ::aethera::Key::home && event.type == ::aethera::Event_Type::key_press) {
if (arcball_ == nullptr || target_extent_.empty()) throw std::runtime_error("failed to reset Datoviz arcball camera");
const float width = static_cast<float>(target_extent_.width); const float height = static_cast<float>(target_extent_.height);
const auto emit = [&](DvzPointerEventType type) { dvz_pointer_emit_position(input_router_, type, width * 0.5F, height * 0.5F, width, height, DVZ_POINTER_BUTTON_LEFT, DVZ_KEY_MODIFIER_NONE, 1.0F, dvz_input_timestamp_ns(), nullptr); };
emit(DVZ_POINTER_EVENT_PRESS); emit(DVZ_POINTER_EVENT_RELEASE); emit(DVZ_POINTER_EVENT_PRESS); emit(DVZ_POINTER_EVENT_RELEASE);
if (turntable_ == nullptr || dvz_turntable_reset(turntable_) != DVZ_OK)
throw std::runtime_error("failed to reset Datoviz Turntable Camera");
return;
}
const DvzKeyboardEventType type =
@@ -1013,7 +1232,7 @@ DvzSceneFrameArtifact* Datoviz_Visual_Backend::emit(
configuration.clear_color[1] = scene.clear_color.green;
configuration.clear_color[2] = scene.clear_color.blue;
configuration.clear_color[3] = scene.clear_color.alpha;
const auto capabilities = dvz_capability_snapshot();
const auto capabilities = offscreen_capabilities();
DvzDiagnosticReport report{};
dvz_diagnostic_report_init(&report);
auto* artifact =
@@ -1038,6 +1257,37 @@ std::optional<Datoviz_Visual_Backend::Pending_Frame> Datoviz_Visual_Backend::sub
trace.observed = observe;
std::uint64_t phase_started = observe ? trace_now_ns() : 0;
apply(scene, point);
if (item_interaction_ != nullptr) {
static_cast<void>(dvz_figure_process_queries(figure_, runtime_, nullptr));
DvzQueryResult query{};
bool resolved{};
while (dvz_scene_poll_query(scene_, &query)) resolved = true;
if (resolved) {
if (hover_readout_ != nullptr) {
dvz_pinned_readout_destroy(hover_readout_);
hover_readout_ = nullptr;
}
if (query.hit) {
if (query.value_kind == DVZ_QUERY_VALUE_NONE) {
if (query.has_data_position || query.has_visual_position) {
query.value_kind = DVZ_QUERY_VALUE_VEC3;
const auto& position = query.has_data_position
? query.data_position
: query.visual_position;
std::ranges::copy(position, query.vector);
constexpr char position_label[] = "Position";
std::ranges::copy(position_label, query.label);
} else {
query.value_kind = DVZ_QUERY_VALUE_SCALAR;
query.scalar = static_cast<double>(query.item_id);
constexpr char item_label[] = "Item";
std::ranges::copy(item_label, query.label);
}
}
hover_readout_ = dvz_pinned_readout_query(panel_, &query);
}
}
}
if (observe) trace.apply_ns = trace_now_ns() - phase_started;
if (target_ == nullptr || target_extent_ != scene.viewport) {
target_.reset();
@@ -1142,6 +1392,20 @@ void Datoviz_Visual_Backend::destroy() {
runtime_ = nullptr;
}
target_.reset();
if (turntable_ != nullptr) {
if (input_router_ != nullptr)
(void)dvz_turntable_disconnect(turntable_, input_router_);
dvz_turntable_destroy(turntable_);
turntable_ = nullptr;
}
if (item_interaction_ != nullptr) {
dvz_item_interaction_destroy(item_interaction_);
item_interaction_ = nullptr;
}
if (hover_readout_ != nullptr) {
dvz_pinned_readout_destroy(hover_readout_);
hover_readout_ = nullptr;
}
if (panel_ != nullptr && input_router_ != nullptr) (void)dvz_panel_connect_input(panel_, nullptr);
if (gesture_handler_ != nullptr) {
dvz_pointer_gesture_handler_destroy(gesture_handler_);
@@ -1152,7 +1416,10 @@ void Datoviz_Visual_Backend::destroy() {
input_router_ = nullptr;
}
visual_ = nullptr;
arcball_ = nullptr;
axes_visual_ = nullptr;
axes_text_ = nullptr;
applied_camera_.reset();
applied_axes_.reset();
panel_ = nullptr;
figure_ = nullptr;
if (scene_ != nullptr) {
@@ -7,6 +7,7 @@
#include <datoviz/stream/frame_stream.h>
#include <datoviz/vk/gpu_ctx.h>
#include <datoviz/vklite.h>
#include <array>
#include <cstddef>
#include <cstdint>
#include <memory>
@@ -52,6 +53,8 @@ private:
class Frame_Target;
void require_domain() const;
void create_scene(Visual_Family visual_family, const Scene_3D_Parameters& initial_scene);
void apply_camera(const plot::Camera_Descriptor& camera);
void apply_axes(const Scene_3D_Parameters& scene);
void apply(const Scene_3D_Parameters& scene, const Prepared_Visual& visual);
[[nodiscard]] DvzSceneFrameArtifact* emit(const Scene_3D_Parameters& scene);
void destroy();
@@ -62,13 +65,19 @@ private:
DvzFigure* figure_{}; /* 当前离屏 Figure。 */
DvzPanel* panel_{}; /* 承载唯一 Visual 的全屏 Panel。 */
DvzVisual* visual_{}; /* Builder 指定 family 的唯一 Visual。 */
DvzArcball* arcball_{}; /* 当前 Panel 相机的交互状态;由 Scene Controller 拥有。 */
DvzVisual* axes_visual_{}; /* 三维主轴、刻度和网格 Segment Visual。 */
DvzText* axes_text_{}; /* 随相机变换的三维刻度与轴标题。 */
DvzItemInteraction* item_interaction_{}; /* Datoviz 原生图元悬停与选择控制器。 */
DvzPinnedReadout* hover_readout_{}; /* 最近一次命中的 Datoviz 数据提示卡。 */
DvzTurntable* turntable_{}; /* 直接驱动 Panel Camera 的独立 Turntable。 */
DvzInputRouter* input_router_{}; /* Scene 输入事件路由器。 */
DvzPointerGestureHandler* gesture_handler_{}; /* 指针手势解析器。 */
std::unique_ptr<Frame_Target> target_; /* 当前尺寸对应的离屏提交和读回目标。 */
Extent target_extent_{}; /* target_ 当前适配的像素尺寸。 */
std::uint64_t target_generation_{}; /* 每次重建 target_ 时递增的资源代次。 */
std::uint64_t applied_visual_revision_{}; /* 已上传到 Datoviz 的 Prepared 版本。 */
std::optional<plot::Camera_Descriptor> applied_camera_{}; /* 已应用到 Panel 的 Camera 配置。 */
std::optional<std::array<plot::Axis_Descriptor, 3>> applied_axes_{}; /* 已生成 Visual 的轴描述快照。 */
};
} // namespace aethera::render_3d::detail
+19 -1
View File
@@ -1,8 +1,11 @@
#pragma once
#include "../axis/Axes_3D.hpp"
#include "../base/Frame_3D.hpp"
#include "../camera/Camera_3D.hpp"
#include "../detail/Backend_Types.hpp"
#include "../visual/Visuals.hpp"
#include <scene.hpp>
#include <array>
#include <expected>
#include <functional>
#include <memory>
@@ -24,13 +27,28 @@ struct Render_Scene_3D : Def<Render_Scene_3D, Scene, Dependency_Graph_Type<Submi
template <typename Object>
struct Builder : Prev_Builder<Object> {
using Base = Prev_Builder<Object>;
using Final_Builder = typename Object::Builder;
Builder();
template <Attached Visual_Object>
explicit Builder(Visual_Object* visual, std::uint32_t gpu_index = 0, bool validation_enabled = false);
Final_Builder& add_renderable(Visual_Object* visual);
template <Attached Camera_Object>
requires std::derived_from<Camera_Object, Camera_3D>
Final_Builder& add_camera(Camera_Object* camera);
template <Attached Axes_Object>
requires std::derived_from<Axes_Object, Axes_3D>
Final_Builder& add_axes(Axes_Object* axes);
Final_Builder& use_gpu(std::uint32_t gpu_index, bool validation_enabled = false);
[[nodiscard]] std::expected<std::unique_ptr<Object>, Dependency_Graph_Error> build();
private:
Root* visual{}; /* 不拥有的唯一 Visual;生命周期必须覆盖 Scene。 */
Visual_Family visual_family{Visual_Family::point}; /* Visual 编译期 Spec 对应的后端 family。 */
void (*bind_visual)(Root*, std::shared_ptr<void>){}; /* 把 Scene 拥有的弱提交上下文绑定到最终 Visual Private。 */
Root* camera{}; /* 不拥有的 Camera 组件;生命周期必须覆盖 Scene。 */
Root* axes{}; /* 不拥有的三轴组件;生命周期必须覆盖 Scene。 */
using Camera_Read = plot::Camera_Descriptor (*)(const Root*);
using Axes_Read = std::array<plot::Axis_Descriptor, 3> (*)(const Root*);
Camera_Read read_camera{};
Axes_Read read_axes{};
std::uint32_t gpu_index{}; /* Datoviz 使用的物理 GPU 下标。 */
bool validation_enabled{}; /* 是否启用 Vulkan 验证。 */
};
+101 -5
View File
@@ -8,6 +8,7 @@ struct Scene_Paint_Context {
std::shared_ptr<Async_Render_Backend> backend{}; /* Scene 拥有、异步命令延长生命周期的后端。 */
Object* scene{}; /* 仅在 Scene 拥有本上下文期间读取当前 Prop。 */
Frame_3D* frame{}; /* 当前 Submit 图对应的外部帧;process 返回后清空。 */
Scene_3D_Parameters parameters{}; /* 本帧从 Scene 组件读取的一致快照。 */
};
}
struct Render_Scene_3D::Private : Prev_Private {
@@ -23,10 +24,19 @@ struct Render_Scene_3D::Private : Prev_Private {
};
std::shared_ptr<detail::Async_Render_Backend> backend{}; /* Scene 拥有的异步后端;已入队命令自行延长实现寿命。 */
std::shared_ptr<void> paint_context{}; /* Paint 节点读取 Scene 当前 Prop 的生命周期门闩。 */
Root* camera_component{}; /* Builder 绑定的 Camera 组件。 */
Root* axes_component{}; /* Builder 绑定的三轴组件。 */
plot::Camera_Descriptor (*read_camera)(const Root*){}; /* 读取 Camera 当前配置。 */
std::array<plot::Axis_Descriptor, 3> (*read_axes)(const Root*){}; /* 读取三轴当前配置。 */
Frame_3D* active_frame{}; /* 当前同步 process 借用的外部帧;提交完成后清空。 */
const Dispatch* dispatch{}; /* 最终 Scene 类型对应的静态公开分派表。 */
/* Builder 内部初始化后端;必须在绑定 Visual Paint 目标之前调用一次。 */
template <Attached Object> void initialize_backend(Object* object, std::uint32_t gpu_index, bool validation_enabled, Visual_Family visual_family);
template <Attached Object>
void initialize_backend(Object* object, std::uint32_t gpu_index, bool validation_enabled,
Visual_Family visual_family, Root* camera, Root* axes,
plot::Camera_Descriptor (*camera_reader)(const Root*),
std::array<plot::Axis_Descriptor, 3> (*axes_reader)(const Root*));
template <Attached Object> [[nodiscard]] detail::Scene_3D_Parameters parameters(Object* object) const;
/* CRTP 覆盖:绑定 Scene 机制和 Render_Scene_3D 公开薄壳。 */
template <Attached Object> void bind_private_crtp(Object* object);
/* CRTP 覆盖:先执行 Kernel Prepare Taskflow,再运行只负责异步入队的三维 Submit 阶段。 */
@@ -36,20 +46,105 @@ struct Render_Scene_3D::Private : Prev_Private {
template <Attached Object> [[nodiscard]] Render_Result render(Object* object, Frame_3D* frame);
template <Attached Object> [[nodiscard]] static const Dispatch& dispatch_for();
};
template <typename Object>
Render_Scene_3D::Builder<Object>::Builder() : Base() {}
template <typename Object> template <Attached Visual_Object>
Render_Scene_3D::Builder<Object>::Builder(Visual_Object* visual_value, std::uint32_t gpu_index_value, bool validation_enabled_value) : Base(), visual(visual_value), visual_family(Visual_Object::Attached_Object::Specification::family), gpu_index(gpu_index_value), validation_enabled(validation_enabled_value) {
bind_visual = [](Root* root, std::shared_ptr<void> context) { auto* object = static_cast<Visual_Object*>(root); Base::private_access(object).template get<typename Visual_Object::Attached_Object::Base_Tag>().bind_paint_target(std::move(context), [](void* raw_context, const detail::Prepared_Visual& prepared) { auto& submission = *static_cast<detail::Scene_Paint_Context<Object>*>(raw_context); const auto& prop = submission.scene->template read_prop<Render_Scene_3D::Base_Tag>(); submission.backend->render(prepared, detail::Scene_3D_Parameters{prop.viewport, prop.clear_color}, submission.frame); }); };
typename Render_Scene_3D::Builder<Object>::Final_Builder&
Render_Scene_3D::Builder<Object>::add_renderable(Visual_Object* visual_value) {
if (!visual_value) throw std::invalid_argument("Render_Scene_3D cannot attach a null Visual");
if (visual) throw std::logic_error("Render_Scene_3D currently accepts one primary Visual");
visual = visual_value;
visual_family = Visual_Object::Attached_Object::Specification::family;
bind_visual = [](Root* root, std::shared_ptr<void> context) {
auto* object = static_cast<Visual_Object*>(root);
Base::private_access(object).template get<typename Visual_Object::Attached_Object::Base_Tag>()
.bind_paint_target(std::move(context), [](void* raw_context, const detail::Prepared_Visual& prepared) {
auto& submission = *static_cast<detail::Scene_Paint_Context<Object>*>(raw_context);
submission.backend->render(prepared, submission.parameters, submission.frame);
});
};
this->template add_dependency_node<Prepare_Data_Tag>(visual_value);
this->template add_dependency_node<Submit_Tag>(visual_value);
return static_cast<Final_Builder&>(*this);
}
template <typename Object> template <Attached Camera_Object>
requires std::derived_from<Camera_Object, Camera_3D>
typename Render_Scene_3D::Builder<Object>::Final_Builder&
Render_Scene_3D::Builder<Object>::add_camera(Camera_Object* camera_value) {
if (!camera_value) throw std::invalid_argument("Render_Scene_3D cannot attach a null Camera");
if (camera) throw std::logic_error("Render_Scene_3D accepts one Camera component");
camera = camera_value;
read_camera = [](const Root* root) {
const auto& prop = static_cast<const Camera_Object*>(root)->template read_prop<Camera_3D::Base_Tag>();
return plot::Camera_Descriptor{prop.initial_view, prop.projection, prop.control,
prop.vertical_field_of_view_degrees,
prop.near_plane, prop.far_plane};
};
return static_cast<Final_Builder&>(*this);
}
template <typename Object> template <Attached Axes_Object>
requires std::derived_from<Axes_Object, Axes_3D>
typename Render_Scene_3D::Builder<Object>::Final_Builder&
Render_Scene_3D::Builder<Object>::add_axes(Axes_Object* axes_value) {
if (!axes_value) throw std::invalid_argument("Render_Scene_3D cannot attach null Axes");
if (axes) throw std::logic_error("Render_Scene_3D accepts one Axes component");
axes = axes_value;
read_axes = [](const Root* root) {
const auto& prop = static_cast<const Axes_Object*>(root)->template read_prop<Axes_3D::Base_Tag>();
return std::array<plot::Axis_Descriptor, 3>{prop.x_axis, prop.y_axis, prop.z_axis};
};
return static_cast<Final_Builder&>(*this);
}
template <typename Object>
std::expected<std::unique_ptr<Object>, Dependency_Graph_Error> Render_Scene_3D::Builder<Object>::build() { if (!visual || !bind_visual) throw std::invalid_argument("Render_Scene_3D requires one Visual"); auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto scene = std::move(result).value(); auto& private_data = Base::private_access(scene.get()).template get<Render_Scene_3D::Base_Tag>(); private_data.initialize_backend(scene.get(), gpu_index, validation_enabled, visual_family); bind_visual(visual, private_data.paint_context); return std::move(scene); }
template <Attached Object> void Render_Scene_3D::Private::initialize_backend(Object* object, std::uint32_t gpu_index, bool validation_enabled, Visual_Family visual_family) { const auto& prop = object->template read_prop<Render_Scene_3D::Base_Tag>(); backend = std::make_shared<detail::Async_Render_Backend>(gpu_index, validation_enabled, visual_family, detail::Scene_3D_Parameters{prop.viewport, prop.clear_color}); paint_context = std::make_shared<detail::Scene_Paint_Context<Object>>(detail::Scene_Paint_Context<Object>{backend, object, nullptr}); }
typename Render_Scene_3D::Builder<Object>::Final_Builder&
Render_Scene_3D::Builder<Object>::use_gpu(std::uint32_t gpu_index_value,
bool validation_enabled_value) {
gpu_index = gpu_index_value;
validation_enabled = validation_enabled_value;
return static_cast<Final_Builder&>(*this);
}
template <typename Object>
std::expected<std::unique_ptr<Object>, Dependency_Graph_Error> Render_Scene_3D::Builder<Object>::build() {
if (!visual || !bind_visual) throw std::invalid_argument("Render_Scene_3D requires one Visual");
if (!camera || !read_camera) throw std::invalid_argument("Render_Scene_3D requires one Camera component");
if (!axes || !read_axes) throw std::invalid_argument("Render_Scene_3D requires one Axes component");
auto result = Base::build();
if (!result) return std::unexpected(result.error());
auto scene = std::move(result).value();
auto& private_data = Base::private_access(scene.get()).template get<Render_Scene_3D::Base_Tag>();
private_data.initialize_backend(scene.get(), gpu_index, validation_enabled, visual_family,
camera, axes, read_camera, read_axes);
bind_visual(visual, private_data.paint_context);
return scene;
}
template <Attached Object>
detail::Scene_3D_Parameters Render_Scene_3D::Private::parameters(Object* object) const {
const auto& prop = object->template read_prop<Render_Scene_3D::Base_Tag>();
const auto axis = read_axes(axes_component);
return {prop.viewport, prop.clear_color, read_camera(camera_component), axis[0], axis[1], axis[2]};
}
template <Attached Object>
void Render_Scene_3D::Private::initialize_backend(
Object* object, std::uint32_t gpu_index, bool validation_enabled, Visual_Family visual_family,
Root* camera, Root* axes, plot::Camera_Descriptor (*camera_reader)(const Root*),
std::array<plot::Axis_Descriptor, 3> (*axes_reader)(const Root*)) {
camera_component = camera;
axes_component = axes;
read_camera = camera_reader;
read_axes = axes_reader;
const auto initial = parameters(object);
backend = std::make_shared<detail::Async_Render_Backend>(gpu_index, validation_enabled,
visual_family, initial);
paint_context = std::make_shared<detail::Scene_Paint_Context<Object>>(
detail::Scene_Paint_Context<Object>{backend, object, nullptr, initial});
}
inline void Render_Scene_3D::Private::dispatch_events(Extent viewport) { this->consume_events([&](const std::shared_ptr<Event>& event) { const auto result = backend->dispatch_event(event, viewport); return result != detail::Async_Render_Backend::Dispatch_Event_Result::queue_full && result != detail::Async_Render_Backend::Dispatch_Event_Result::backend_unavailable; }); }
template <Attached Object, typename Callback>
void Render_Scene_3D::Private::process(Object* object, Callback&& callback) requires std::invocable<Callback> {
Frame_3D* frame = active_frame;
if (!frame) throw std::logic_error("3D Scene process has no external frame");
camera_component->advance_object();
axes_component->advance_object();
const auto& prop = object->template read_prop<Render_Scene_3D::Base_Tag>();
if (!prop.view_active || prop.viewport.empty() || !backend || !backend->available()) return;
frame->mark(Frame_Trace_Marker::scene_render_started);
@@ -57,6 +152,7 @@ void Render_Scene_3D::Private::process(Object* object, Callback&& callback) requ
dispatch_events(prop.viewport);
frame->mark(Frame_Trace_Marker::event_dispatch_finished);
auto context = std::static_pointer_cast<detail::Scene_Paint_Context<Object>>(paint_context);
context->parameters = parameters(object);
struct Frame_Context_Scope {
Frame_3D*& target; /* Visual Submit 回调读取的当前外部帧槽位。 */
Frame_3D* previous{}; /* 嵌套调用前的帧;析构时恢复。 */
+237
View File
@@ -0,0 +1,237 @@
明白,只定义 **用户看到的交互能力**,不定义数据、不定义内部实现。下面按图类型分类。
# 3D Plot Interaction Requirement
## 1. 3D Waterfall Plot(三维瀑布图)
### 基础交互
* 旋转视角
* 缩放视图
* 平移视图
* 重置视角
### 时间交互
* 时间窗口调整
* 时间范围拖动
* 实时滚动
* 暂停刷新
* 继续刷新
* 查看历史区域
### 数据查看
* 鼠标悬停显示当前位置数据
* 点击显示详细信息
* 添加标记点
* 删除标记点
* 显示峰值位置
### 分析交互
* 查看指定时间截面
* 查看指定频率截面
* 切换俯视图
* 切换三维视图
* 调整高度显示比例
---
# 2. 3D Surface Plot(三维曲面图)
### 基础交互
* 旋转曲面
* 缩放曲面
* 平移曲面
* 重置视角
### 显示控制
* 显示/隐藏网格
* 显示/隐藏坐标轴
* 显示/隐藏刻度
* 切换曲面显示模式
* 调整透明度
### 数据观察
* 鼠标悬停查看点信息
* 点击选择点
* 显示局部峰值
* 显示局部最低值
### 分析交互
* 显示等高线
* 显示投影平面
* 调整高度比例
* 调整颜色范围
---
# 3. 3D Scatter Plot(三维散点图)
### 基础交互
* 旋转观察
* 缩放
* 平移
* 重置视角
### 点操作
* 悬停显示点信息
* 点击选择点
* 多选点
* 框选区域
* 取消选择
### 显示控制
* 调整点大小
* 调整点透明度
* 显示/隐藏点分类
* 显示/隐藏辅助线
### 分析交互
* 点过滤
* 区域过滤
* 聚类显示
* 高亮指定点集合
---
# 4. 3D Point Cloud(三维点云图)
### 基础交互
* 旋转点云
* 缩放点云
* 平移点云
* 重置视角
### 点云浏览
* 自动调整观察距离
* 点密度调整
* 显示级别调整
* 局部放大
### 选择操作
* 单点选择
* 区域选择
* 框选
* 删除选择
* 隐藏选择
### 显示控制
* 点大小调整
* 颜色模式切换
* 透明度调整
---
# 5. 3D Volume Plot(三维体数据图)
### 基础交互
* 旋转体数据
* 缩放
* 平移
* 重置视角
### 切割查看
* X方向切片
* Y方向切片
* Z方向切片
* 移动切片位置
* 多切片显示
### 显示控制
* 调整透明度
* 调整显示范围
* 调整颜色映射
* 隐藏低强度区域
### 分析交互
* 区域选择
* 局部放大
* 数据点查看
---
# 6. 3D Polar Plot(三维极坐标图)
### 基础交互
* 旋转方向
* 缩放
* 平移
* 重置视角
### 方向查看
* 查看指定角度
* 查看指定方向数据
* 显示方向标记
### 显示控制
* 切换极坐标/直角坐标
* 显示辅助圆环
* 显示角度刻度
---
# 7. 通用 3D Plot 交互
所有 3D 图统一支持:
## Camera
* Rotate
* Zoom
* Pan
* Reset View
## Selection
* Hover
* Select
* Multi Select
* Clear Selection
## Marker
* Add Marker
* Move Marker
* Delete Marker
## Axis
* Axis Visibility
* Axis Range
* Scale Adjustment
## Display
* Show/Hide Grid
* Show/Hide Label
* Transparency Control
## Navigation
* Fit View
* Full Screen
* Save View
* Restore View
---