三维频谱marker

This commit is contained in:
2026-08-22 21:36:21 +08:00
parent cd028e8d4f
commit 7c3f27474b
15 changed files with 594 additions and 207 deletions
+153 -53
View File
@@ -200,12 +200,14 @@ class Visual_Scene_View final : public Plot::Scene_View {
public:
using Camera_Object = Impl<Camera_3D>;
using Axes_Object = Impl<Axes_3D>;
using Marker_Object = Impl<Marker_Visual>;
Visual_Scene_View(Scene_3D& scene, std::unique_ptr<Camera_Object> camera,
std::unique_ptr<Axes_Object> axes,
std::unique_ptr<Visual_Object> visual, std::string label,
Data_Generator data_generator = {})
Data_Generator data_generator = {},
std::unique_ptr<Marker_Object> markers = {})
: data_generator_(std::move(data_generator)), camera_(std::move(camera)),
axes_(std::move(axes)), visual_(std::move(visual)) {
axes_(std::move(axes)), visual_(std::move(visual)), markers_(std::move(markers)) {
using Definition = typename Visual_Object::Attached_Object;
using Prop = typename Definition::Prop;
using State = typename Definition::State;
@@ -225,7 +227,11 @@ public:
using Camera_Adapter = detail::Renderable_Adapter<Camera_Object,
detail::Prop_Field<&Camera_3D::Prop::initial_view, "initial_view", "Initial eye, target and world-up vectors used by reset view.">,
detail::Prop_Field<&Camera_3D::Prop::projection, "projection", "Perspective or orthographic camera projection.">,
detail::Prop_Field<&Camera_3D::Prop::control, "control", "Turntable rotate, zoom and pan capabilities, speed and limits.">,
detail::Prop_Field<&Camera_3D::Prop::controller, "controller", "Datoviz native camera controller: turntable, arcball, fly or panzoom.">,
detail::Prop_Field<&Camera_3D::Prop::turntable_control, "turntable_control", "Turntable orbit, zoom and pan speeds and limits.">,
detail::Prop_Field<&Camera_3D::Prop::arcball_control, "arcball_control", "Arcball free rotation and optional constraint axis.">,
detail::Prop_Field<&Camera_3D::Prop::fly_control, "fly_control", "Fly camera movement mode, keyboard speed and pointer look settings.">,
detail::Prop_Field<&Camera_3D::Prop::panzoom_control, "panzoom_control", "Planar panzoom axis locks and aspect-ratio policy.">,
detail::Prop_Field<&Camera_3D::Prop::vertical_field_of_view_degrees, "vertical_field_of_view_degrees", "Vertical field of view in degrees.">,
detail::Prop_Field<&Camera_3D::Prop::near_plane, "near_plane", "Nearest visible camera distance.">,
detail::Prop_Field<&Camera_3D::Prop::far_plane, "far_plane", "Farthest visible camera distance.">>;
@@ -236,7 +242,34 @@ public:
descriptors_.push_back(detail::make_renderable_descriptor("scene", "3D 场景", "scene", Scene_Adapter{scene}));
descriptors_.push_back(detail::make_renderable_descriptor("camera", "相机控制", "camera", Camera_Adapter{*camera_}));
descriptors_.push_back(detail::make_renderable_descriptor("axes", "三维坐标轴", "axes", Axes_Adapter{*axes_}));
descriptors_.push_back(detail::make_renderable_descriptor("visual", std::move(label), "visual", Visual_Adapter{*visual_}));
if constexpr (std::same_as<Definition, Marker_Visual>) {
using Marker_Adapter = detail::Renderable_Adapter<Visual_Object,
detail::Prop_Field<&Prop::items, "items", "Marker collection; editing the collection adds, moves or removes actual Scene markers.">,
detail::Prop_Field<&Prop::transform, "transform", "World transform applied to the complete marker collection.">,
detail::Prop_Field<&Prop::visible, "visible", "Whether markers participate in rendering.">,
detail::Prop_Field<&Prop::depth_test, "depth_test", "Whether marker fragments use depth testing.">,
detail::State_Field<Definition::Base_Tag, &State::item_count, "item_count", "Number of markers in the Scene.">,
detail::State_Field<Definition::Base_Tag, &State::prepared_item_count, "prepared_item_count", "Number of prepared marker items.">,
detail::State_Field<Definition::Base_Tag, &State::prepared_revision, "prepared_revision", "Marker GPU data revision.">>;
descriptors_.push_back(detail::make_renderable_descriptor(
"visual", std::move(label), "marker", Marker_Adapter{*visual_}));
} else {
descriptors_.push_back(detail::make_renderable_descriptor(
"visual", std::move(label), "visual", Visual_Adapter{*visual_}));
}
if (markers_) {
using Marker_Prop = Marker_Visual::Prop;
using Marker_State = Marker_Visual::State;
using Marker_Adapter = detail::Renderable_Adapter<Marker_Object,
detail::Prop_Field<&Marker_Prop::items, "items", "Surface marker anchors; X and Y are editable while Z is derived from the current spectrogram surface.", "surface-marker-list">,
detail::Prop_Field<&Marker_Prop::visible, "visible", "Whether the spectrogram marker layer is visible.">,
detail::Prop_Field<&Marker_Prop::depth_test, "depth_test", "Whether markers may be occluded by the surface.">,
detail::State_Field<Marker_Visual::Base_Tag, &Marker_State::item_count, "item_count", "Number of spectrogram markers.">,
detail::State_Field<Marker_Visual::Base_Tag, &Marker_State::prepared_item_count, "prepared_item_count", "Number of markers prepared for Datoviz.">,
detail::State_Field<Marker_Visual::Base_Tag, &Marker_State::prepared_revision, "prepared_revision", "Marker GPU data revision.">>;
descriptors_.push_back(detail::make_renderable_descriptor(
"markers", "频谱标记", "marker", Marker_Adapter{*markers_}));
}
}
[[nodiscard]] nlohmann::json schema() const override {
@@ -312,13 +345,22 @@ public:
}
}
void update(const Plot_Frame_Request&) override {}
void update(const Plot_Frame_Request& request) override {
if constexpr (requires(Data_Generator& generator, Visual_Object& visual,
Axes_Object& axes, Marker_Object* markers,
const Plot_Frame_Request& frame) {
generator.update(visual, axes, markers, frame);
}) {
data_generator_.update(*visual_, *axes_, markers_.get(), request);
}
}
private:
[[no_unique_address]] Data_Generator data_generator_; /* Plot 业务数据生成策略。 */
std::unique_ptr<Camera_Object> camera_; /* Scene 引用的 Camera 唯一所有者。 */
std::unique_ptr<Axes_Object> axes_; /* Scene 引用的 Axes 唯一所有者。 */
std::unique_ptr<Visual_Object> visual_; /* Scene 引用的 Visual 唯一所有者。 */
std::unique_ptr<Marker_Object> markers_; /* 可选的独立 Marker Visual 所有者。 */
std::vector<std::unique_ptr<detail::Renderable_Descriptor>> descriptors_; /* Prop/State 协议描述。 */
};
@@ -335,7 +377,8 @@ template <typename Definition, typename Build_Result,
std::shared_ptr<Plot> make_visual_plot(asio::any_io_executor executor, std::string label,
Build_Result build_result,
Scene_Components_3D components = {},
Data_Generator data_generator = {}) {
Data_Generator data_generator = {},
std::unique_ptr<Impl<Marker_Visual>> markers = {}) {
using Visual_Object = Impl<Definition>;
using Camera_Object = Impl<Camera_3D>;
using Axes_Object = Impl<Axes_3D>;
@@ -344,7 +387,11 @@ std::shared_ptr<Plot> make_visual_plot(asio::any_io_executor executor, std::stri
auto camera_result = Camera_Object::Builder{}
.set(&Camera_3D::Prop::initial_view, components.camera.initial_view)
.set(&Camera_3D::Prop::projection, components.camera.projection)
.set(&Camera_3D::Prop::control, components.camera.control)
.set(&Camera_3D::Prop::controller, components.camera.controller)
.set(&Camera_3D::Prop::turntable_control, components.camera.turntable_control)
.set(&Camera_3D::Prop::arcball_control, components.camera.arcball_control)
.set(&Camera_3D::Prop::fly_control, components.camera.fly_control)
.set(&Camera_3D::Prop::panzoom_control, components.camera.panzoom_control)
.set(&Camera_3D::Prop::vertical_field_of_view_degrees,
components.camera.vertical_field_of_view_degrees)
.set(&Camera_3D::Prop::near_plane, components.camera.near_plane)
@@ -366,12 +413,13 @@ std::shared_ptr<Plot> make_visual_plot(asio::any_io_executor executor, std::stri
.set(&Render_Scene_3D::Prop::viewport, Extent{720, 420})
.set(&Render_Scene_3D::Prop::clear_color, Linear_Color{0.018F, 0.027F, 0.047F, 1.0F})
.set(&Render_Scene_3D::Prop::view_active, true);
if (markers) scene_builder.add_renderable(markers.get());
auto scene_result = scene_builder.build();
if (!scene_result) throw std::logic_error("3D Gallery scene dependency graph is invalid");
auto scene = std::move(scene_result).value();
auto view = std::make_unique<Visual_Scene_View<Visual_Object, Data_Generator>>(
*scene, std::move(camera), std::move(axes), std::move(visual),
std::move(label), std::move(data_generator));
std::move(label), std::move(data_generator), std::move(markers));
return std::make_shared<Plot>(std::move(executor), std::move(scene), std::move(view));
}
@@ -427,21 +475,48 @@ Vec3 face_normal(Vec3 first, Vec3 second, Vec3 third) {
return result;
}
std::vector<Mesh_Vertex> spectrogram_mesh(const Spectrogram_Parameters& parameters) {
float spectrogram_level(float time, float frequency, double animation_seconds,
std::size_t ridge_count) {
struct Ridge { float center; float width; float phase; float speed; float strength; };
std::mt19937_64 engine{std::random_device{}()};
std::uniform_real_distribution<float> center_distribution{0.08F, 0.92F};
std::uniform_real_distribution<float> width_distribution{0.025F, 0.12F};
std::uniform_real_distribution<float> phase_distribution{0.0F, 2.0F * std::numbers::pi_v<float>};
std::uniform_real_distribution<float> speed_distribution{0.35F, 1.8F};
std::uniform_real_distribution<float> strength_distribution{0.38F, 0.9F};
std::normal_distribution<float> noise{0.0F, 0.035F};
std::vector<Ridge> ridges;
ridges.reserve(parameters.ridge_count);
for (std::size_t index = 0; index < parameters.ridge_count; ++index)
ridges.push_back({center_distribution(engine), width_distribution(engine),
phase_distribution(engine), speed_distribution(engine),
strength_distribution(engine)});
static constexpr std::array ridges{
Ridge{0.12F, 0.035F, 0.20F, 0.42F, 0.72F},
Ridge{0.28F, 0.060F, 1.45F, 0.67F, 0.58F},
Ridge{0.47F, 0.045F, 2.70F, 0.53F, 0.82F},
Ridge{0.68F, 0.075F, 4.10F, 0.31F, 0.52F},
Ridge{0.84F, 0.030F, 5.20F, 0.78F, 0.68F},
Ridge{0.57F, 0.022F, 0.90F, 1.05F, 0.44F},
Ridge{0.36F, 0.028F, 3.60F, 0.91F, 0.40F},
Ridge{0.76F, 0.050F, 2.10F, 0.58F, 0.46F},
Ridge{0.20F, 0.024F, 4.80F, 0.72F, 0.38F},
Ridge{0.92F, 0.020F, 1.90F, 0.84F, 0.36F},
Ridge{0.52F, 0.090F, 5.70F, 0.27F, 0.32F},
Ridge{0.08F, 0.018F, 3.00F, 1.12F, 0.30F}};
const float animation = static_cast<float>(animation_seconds);
float level = 0.075F + 0.055F * std::sin(
2.0F * std::numbers::pi_v<float> *
(0.78F * time + 0.24F * frequency + 0.11F * animation));
const auto active_ridges = std::min(ridge_count, ridges.size());
for (std::size_t index = 0; index < active_ridges; ++index) {
const auto& ridge = ridges[index];
const auto moving_center = std::clamp(
ridge.center + 0.065F * std::sin(
ridge.phase + ridge.speed * animation + 1.3F * time),
0.015F, 0.985F);
const auto distance = (frequency - moving_center) / ridge.width;
const auto envelope = 0.52F + 0.48F * std::sin(
ridge.phase * 0.61F + 2.2F * time +
(ridge.speed + 0.18F) * animation);
level += ridge.strength * envelope * std::exp(-0.5F * distance * distance);
}
const float detail = 0.025F * std::sin(
31.0F * frequency + 8.0F * time + 1.7F * animation) +
0.018F * std::cos(
19.0F * frequency - 13.0F * time + animation);
return std::clamp(level + detail, 0.0F, 1.0F);
}
std::vector<Mesh_Vertex> spectrogram_mesh(const Spectrogram_Parameters& parameters,
double animation_seconds = 0.0) {
struct Sample { Vec3 position; Color color; };
std::vector<Sample> samples(parameters.time_sample_count *
@@ -454,20 +529,8 @@ std::vector<Mesh_Vertex> spectrogram_mesh(const Spectrogram_Parameters& paramete
frequency_index < parameters.frequency_bin_count; ++frequency_index) {
const auto frequency = static_cast<float>(frequency_index) /
static_cast<float>(parameters.frequency_bin_count - 1);
float level = 0.08F + 0.08F * std::sin(
2.0F * std::numbers::pi_v<float> * (0.7F * time + 0.3F * frequency));
for (const auto& ridge : ridges) {
const auto moving_center = std::clamp(
ridge.center + 0.055F * std::sin(ridge.phase +
ridge.speed * 2.0F * std::numbers::pi_v<float> * time),
0.02F, 0.98F);
const auto distance = (frequency - moving_center) / ridge.width;
const auto envelope = 0.55F + 0.45F * std::sin(
ridge.phase * 0.63F + (ridge.speed + 0.25F) *
2.0F * std::numbers::pi_v<float> * time);
level += ridge.strength * envelope * std::exp(-0.5F * distance * distance);
}
level = std::clamp(level + noise(engine), 0.0F, 1.0F);
const float level = spectrogram_level(
time, frequency, animation_seconds, parameters.ridge_count);
samples[time_index * parameters.frequency_bin_count + frequency_index] = {
{-1.0F + 2.0F * time, -1.0F + 2.0F * frequency,
-1.0F + 2.0F * level}, spectrogram_color(level)};
@@ -498,6 +561,11 @@ std::vector<Mesh_Vertex> spectrogram_mesh(const Spectrogram_Parameters& paramete
}
struct Spectrogram_Data_Generator {
Spectrogram_Parameters parameters{};
explicit Spectrogram_Data_Generator(Spectrogram_Parameters value = {})
: parameters(std::move(value)) {}
[[nodiscard]] Json schema() const {
Json fields = Json::array();
fields.push_back(integer_field("time_sample_count", "时间采样数", "时间方向的网格采样数量;增大后表面沿时间方向更细密。", 80, 16, 256));
@@ -514,22 +582,23 @@ struct Spectrogram_Data_Generator {
}
[[nodiscard]] Json generate(Impl<Mesh_Visual>& visual, Impl<Axes_3D>& axes,
const Json& input) const {
const Json& input) {
try {
Spectrogram_Parameters parameters;
parameters.time_sample_count = input_count(input, "time_sample_count", 256);
parameters.frequency_bin_count = input_count(input, "frequency_bin_count", 256);
parameters.ridge_count = input_count(input, "ridge_count", 12);
parameters.time_span_seconds = input_number(input, "time_span_seconds");
parameters.minimum_frequency_hz = input_number(input, "minimum_frequency_hz");
parameters.maximum_frequency_hz = input_number(input, "maximum_frequency_hz");
parameters.minimum_level_db = input_number(input, "minimum_level_db");
parameters.maximum_level_db = input_number(input, "maximum_level_db");
if (!(parameters.time_span_seconds > 0.0) ||
!(parameters.minimum_frequency_hz > 0.0) ||
!(parameters.maximum_frequency_hz > parameters.minimum_frequency_hz) ||
!(parameters.maximum_level_db > parameters.minimum_level_db))
Spectrogram_Parameters next;
next.time_sample_count = input_count(input, "time_sample_count", 256);
next.frequency_bin_count = input_count(input, "frequency_bin_count", 256);
next.ridge_count = input_count(input, "ridge_count", 12);
next.time_span_seconds = input_number(input, "time_span_seconds");
next.minimum_frequency_hz = input_number(input, "minimum_frequency_hz");
next.maximum_frequency_hz = input_number(input, "maximum_frequency_hz");
next.minimum_level_db = input_number(input, "minimum_level_db");
next.maximum_level_db = input_number(input, "maximum_level_db");
if (!(next.time_span_seconds > 0.0) ||
!(next.minimum_frequency_hz > 0.0) ||
!(next.maximum_frequency_hz > next.minimum_frequency_hz) ||
!(next.maximum_level_db > next.minimum_level_db))
throw std::invalid_argument("spectrogram ranges are invalid");
parameters = next;
auto mesh = spectrogram_mesh(parameters);
const auto vertex_count = mesh.size();
if (visual.update_items(std::move(mesh)) != Mesh_Visual::Update_Items_Result::updated)
@@ -551,6 +620,25 @@ struct Spectrogram_Data_Generator {
return {{"success", false}, {"error", error.what()}};
}
}
void update(Impl<Mesh_Visual>& visual, Impl<Axes_3D>&,
Impl<Marker_Visual>* markers,
const Plot_Frame_Request& request) const {
const double animation_seconds = request.time_milliseconds / 1000.0;
auto mesh = spectrogram_mesh(parameters, animation_seconds);
if (visual.update_items(std::move(mesh)) != Mesh_Visual::Update_Items_Result::updated)
throw std::logic_error("animated spectrogram mesh was rejected");
if (markers == nullptr) return;
auto items = markers->template read_prop<Marker_Visual::Base_Tag>().items;
for (auto& marker : items) {
const float time = std::clamp((marker.position.x + 1.0F) * 0.5F, 0.0F, 1.0F);
const float frequency = std::clamp((marker.position.y + 1.0F) * 0.5F, 0.0F, 1.0F);
marker.position.z = -1.0F + 2.0F * spectrogram_level(
time, frequency, animation_seconds, parameters.ridge_count);
}
if (markers->update_items(std::move(items)) != Marker_Visual::Update_Items_Result::updated)
throw std::logic_error("surface-attached spectrogram markers were rejected");
}
};
}
@@ -604,11 +692,23 @@ std::shared_ptr<Plot> make_datoviz_mesh_plot(asio::any_io_executor executor) {
std::shared_ptr<Plot> make_datoviz_spectrogram_plot(asio::any_io_executor executor) {
const Spectrogram_Parameters parameters;
auto marker_result = Impl<Marker_Visual>::Builder{}
.set(&Marker_Visual::Prop::items, std::vector<Marker>{
{{-0.35F, -0.18F, 0.72F}, color(255, 244, 170), 23.0F, 0.0F,
Marker_Shape::diamond},
{{0.28F, 0.34F, 0.86F}, color(88, 236, 211), 21.0F, 0.0F,
Marker_Shape::cross}})
.set(&Marker_Visual::Prop::depth_test, false)
.build();
if (!marker_result)
throw std::logic_error("3D Spectrogram marker construction failed");
auto markers = std::move(marker_result).value();
Scene_Components_3D components;
components.camera.initial_view = {{3.35, -3.55, 2.45}, {0.0, 0.0, -0.05},
{0.0, 0.0, 1.0}};
components.camera.control = {0.15, 0.15, 0.10, 0.0015, -1.35, 1.35,
1.25, 12.0, true, true, true};
components.camera.turntable_control = {0.15, 0.15, 0.10, 0.0015,
-1.35, 1.35, 1.25, 12.0,
true, true, true, false};
components.camera.vertical_field_of_view_degrees = 41.0;
components.axes = {
plot::Axis_Descriptor{{0.0, parameters.time_span_seconds},
@@ -624,7 +724,7 @@ std::shared_ptr<Plot> make_datoviz_spectrogram_plot(asio::any_io_executor execut
Impl<Mesh_Visual>::Builder{}
.set(&Mesh_Visual::Prop::items, spectrogram_mesh(parameters))
.build(),
std::move(components), Spectrogram_Data_Generator{});
std::move(components), Spectrogram_Data_Generator{parameters}, std::move(markers));
}
std::shared_ptr<Plot> make_datoviz_path_plot(asio::any_io_executor executor) {