三维频谱做好

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
+282 -13
View File
@@ -2,6 +2,7 @@
#include "Renderable_Adapter.hpp"
#include <render_3D/Render_3D.hpp>
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <numbers>
@@ -192,11 +193,19 @@ private:
std::uniform_real_distribution<float> second{};
};
template <typename Visual_Object>
struct Random_Data_Generator {};
template <typename Visual_Object, typename Data_Generator = Random_Data_Generator>
class Visual_Scene_View final : public Plot::Scene_View {
public:
Visual_Scene_View(Scene_3D& scene, std::unique_ptr<Visual_Object> visual, std::string label)
: visual_(std::move(visual)) {
using Camera_Object = Impl<Camera_3D>;
using Axes_Object = Impl<Axes_3D>;
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_(std::move(data_generator)), camera_(std::move(camera)),
axes_(std::move(axes)), visual_(std::move(visual)) {
using Definition = typename Visual_Object::Attached_Object;
using Prop = typename Definition::Prop;
using State = typename Definition::State;
@@ -213,7 +222,20 @@ public:
detail::State_Field<Definition::Base_Tag, &State::item_count, "item_count", "Number of published input items.">,
detail::State_Field<Definition::Base_Tag, &State::prepared_item_count, "prepared_item_count", "Number of prepared backend items.">,
detail::State_Field<Definition::Base_Tag, &State::prepared_revision, "prepared_revision", "Property revision represented by prepared GPU data.">>;
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::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.">>;
using Axes_Adapter = detail::Renderable_Adapter<Axes_Object,
detail::Prop_Field<&Axes_3D::Prop::x_axis, "x_axis", "X axis range, scale, ticks, label and unit.">,
detail::Prop_Field<&Axes_3D::Prop::y_axis, "y_axis", "Y axis range, scale, ticks, label and unit.">,
detail::Prop_Field<&Axes_3D::Prop::z_axis, "z_axis", "Z axis range, scale, ticks, label and unit.">>;
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_}));
}
@@ -232,11 +254,18 @@ public:
}
[[nodiscard]] nlohmann::json data_generator_schema() const override {
using Definition = typename Visual_Object::Attached_Object;
return generator_schema<Definition>();
if constexpr (std::same_as<Data_Generator, Random_Data_Generator>) {
using Definition = typename Visual_Object::Attached_Object;
return generator_schema<Definition>();
} else {
return data_generator_.schema();
}
}
[[nodiscard]] nlohmann::json generate_data(const nlohmann::json& input) override {
if constexpr (!std::same_as<Data_Generator, Random_Data_Generator>) {
return data_generator_.generate(*visual_, *axes_, input);
} else {
using Definition = typename Visual_Object::Attached_Object;
using Prop = typename Definition::Prop;
using Items = std::remove_cvref_t<decltype(std::declval<Prop>().items)>;
@@ -280,34 +309,249 @@ public:
} catch (const std::exception& error) {
return {{"success", false}, {"error", error.what()}};
}
}
}
void update(const Plot_Frame_Request&) override {}
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::vector<std::unique_ptr<detail::Renderable_Descriptor>> descriptors_; /* Prop/State 协议描述。 */
};
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) {
struct Scene_Components_3D {
plot::Camera_Descriptor camera{};
std::array<plot::Axis_Descriptor, 3> axes{
plot::Axis_Descriptor{{-1.0, 1.0}, plot::Axis_Scale::linear, "X", "", 5, 2, true, true, true},
plot::Axis_Descriptor{{-1.0, 1.0}, plot::Axis_Scale::linear, "Y", "", 5, 2, true, true, true},
plot::Axis_Descriptor{{-1.0, 1.0}, plot::Axis_Scale::linear, "Z", "", 5, 2, true, true, true}};
};
template <typename Definition, typename Build_Result,
typename Data_Generator = Random_Data_Generator>
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 = {}) {
using Visual_Object = Impl<Definition>;
using Camera_Object = Impl<Camera_3D>;
using Axes_Object = Impl<Axes_3D>;
if (!build_result) throw std::logic_error("3D Gallery visual dependency graph is invalid");
auto visual = std::move(build_result).value();
auto scene_result = Scene_3D::Builder(visual.get())
.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)
.build();
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::vertical_field_of_view_degrees,
components.camera.vertical_field_of_view_degrees)
.set(&Camera_3D::Prop::near_plane, components.camera.near_plane)
.set(&Camera_3D::Prop::far_plane, components.camera.far_plane)
.build();
auto axes_result = Axes_Object::Builder{}
.set(&Axes_3D::Prop::x_axis, components.axes[0])
.set(&Axes_3D::Prop::y_axis, components.axes[1])
.set(&Axes_3D::Prop::z_axis, components.axes[2])
.build();
if (!camera_result || !axes_result)
throw std::logic_error("3D Gallery component construction failed");
auto camera = std::move(camera_result).value();
auto axes = std::move(axes_result).value();
auto scene_builder = Scene_3D::Builder{};
scene_builder.add_camera(camera.get())
.add_axes(axes.get())
.add_renderable(visual.get())
.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);
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>>(*scene, std::move(visual), std::move(label));
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));
return std::make_shared<Plot>(std::move(executor), std::move(scene), std::move(view));
}
Color color(std::uint8_t red, std::uint8_t green, std::uint8_t blue, std::uint8_t alpha = 255) {
return {red, green, blue, alpha};
}
struct Spectrogram_Parameters {
std::size_t time_sample_count{80};
std::size_t frequency_bin_count{96};
std::size_t ridge_count{5};
double time_span_seconds{4.0};
double minimum_frequency_hz{10.0};
double maximum_frequency_hz{20'000.0};
double minimum_level_db{18.0};
double maximum_level_db{78.0};
};
Color spectrogram_color(float value) {
struct Stop { float position; std::array<float, 3> rgb; };
static constexpr std::array stops{
Stop{0.0F, {22, 10, 54}}, Stop{0.22F, {76, 18, 112}},
Stop{0.45F, {151, 40, 103}}, Stop{0.68F, {226, 83, 61}},
Stop{0.86F, {252, 169, 52}}, Stop{1.0F, {252, 246, 164}}};
value = std::clamp(value, 0.0F, 1.0F);
for (std::size_t index = 1; index < stops.size(); ++index) {
if (value > stops[index].position) continue;
const auto& lower = stops[index - 1];
const auto& upper = stops[index];
const auto ratio = (value - lower.position) /
(upper.position - lower.position);
const auto channel = [&](std::size_t component) {
return static_cast<std::uint8_t>(std::lround(
std::lerp(lower.rgb[component], upper.rgb[component], ratio)));
};
return color(channel(0), channel(1), channel(2));
}
return color(252, 246, 164);
}
Vec3 face_normal(Vec3 first, Vec3 second, Vec3 third) {
const Vec3 a{second.x - first.x, second.y - first.y, second.z - first.z};
const Vec3 b{third.x - first.x, third.y - first.y, third.z - first.z};
Vec3 result{a.y * b.z - a.z * b.y,
a.z * b.x - a.x * b.z,
a.x * b.y - a.y * b.x};
const auto length = std::sqrt(result.x * result.x + result.y * result.y +
result.z * result.z);
if (!(length > 0.0F)) return {0, 0, 1};
result.x /= length;
result.y /= length;
result.z /= length;
return result;
}
std::vector<Mesh_Vertex> spectrogram_mesh(const Spectrogram_Parameters& parameters) {
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)});
struct Sample { Vec3 position; Color color; };
std::vector<Sample> samples(parameters.time_sample_count *
parameters.frequency_bin_count);
for (std::size_t time_index = 0; time_index < parameters.time_sample_count;
++time_index) {
const auto time = static_cast<float>(time_index) /
static_cast<float>(parameters.time_sample_count - 1);
for (std::size_t frequency_index = 0;
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);
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)};
}
}
std::vector<Mesh_Vertex> mesh;
mesh.reserve((parameters.time_sample_count - 1) *
(parameters.frequency_bin_count - 1) * 6);
const auto append_triangle = [&](const Sample& first, const Sample& second,
const Sample& third) {
const auto normal = face_normal(first.position, second.position, third.position);
mesh.push_back({first.position, first.color, normal, {0, 0}});
mesh.push_back({second.position, second.color, normal, {0, 0}});
mesh.push_back({third.position, third.color, normal, {0, 0}});
};
for (std::size_t time_index = 0; time_index + 1 < parameters.time_sample_count;
++time_index) {
for (std::size_t frequency_index = 0;
frequency_index + 1 < parameters.frequency_bin_count; ++frequency_index) {
const auto current = time_index * parameters.frequency_bin_count + frequency_index;
const auto next_time = current + parameters.frequency_bin_count;
append_triangle(samples[current], samples[next_time], samples[next_time + 1]);
append_triangle(samples[current], samples[next_time + 1], samples[current + 1]);
}
}
return mesh;
}
struct Spectrogram_Data_Generator {
[[nodiscard]] Json schema() const {
Json fields = Json::array();
fields.push_back(integer_field("time_sample_count", "时间采样数", "时间方向的网格采样数量;增大后表面沿时间方向更细密。", 80, 16, 256));
fields.push_back(integer_field("frequency_bin_count", "频率分箱数", "对数频率方向的网格分箱数量。", 96, 16, 256));
fields.push_back(integer_field("ridge_count", "谱峰轨迹数", "生成随时间漂移的窄带谱峰数量。", 5, 1, 12));
fields.push_back(number_field("time_span_seconds", "时间跨度", "X 轴覆盖的时间长度,单位秒。", 4.0, 0.1, 120.0, 0.1));
fields.push_back(number_field("minimum_frequency_hz", "最低频率", "对数频率轴的下界,必须大于零。", 10.0, 1.0, 1.0e9, 1.0));
fields.push_back(number_field("maximum_frequency_hz", "最高频率", "对数频率轴的上界,必须大于最低频率。", 20'000.0, 2.0, 1.0e9, 10.0));
fields.push_back(number_field("minimum_level_db", "最低声压级", "Z 轴色阶和高度的下界,单位 dB。", 18.0, -300.0, 300.0, 1.0));
fields.push_back(number_field("maximum_level_db", "最高声压级", "Z 轴色阶和高度的上界,必须大于下界。", 78.0, -300.0, 300.0, 1.0));
return {{"label", "生成三维频谱瀑布"},
{"description", "按时间采样、对数频率分箱和声压级范围生成连续 GPU Mesh 表面。"},
{"fields", std::move(fields)}};
}
[[nodiscard]] Json generate(Impl<Mesh_Visual>& visual, Impl<Axes_3D>& axes,
const Json& input) const {
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))
throw std::invalid_argument("spectrogram ranges are invalid");
auto mesh = spectrogram_mesh(parameters);
const auto vertex_count = mesh.size();
if (visual.update_items(std::move(mesh)) != Mesh_Visual::Update_Items_Result::updated)
throw std::invalid_argument("generated spectrogram mesh was rejected");
plot::Axis_Descriptor time_axis{{0.0, parameters.time_span_seconds},
plot::Axis_Scale::time, "Time", "s", 6, 1, true, true, true};
plot::Axis_Descriptor frequency_axis{{parameters.minimum_frequency_hz,
parameters.maximum_frequency_hz}, plot::Axis_Scale::logarithmic,
"Frequency", "Hz", 5, 0, true, true, true};
plot::Axis_Descriptor level_axis{{parameters.minimum_level_db,
parameters.maximum_level_db}, plot::Axis_Scale::linear,
"SPL", "dB", 7, 0, true, true, true};
axes.set<&Axes_3D::Prop::x_axis>(std::move(time_axis));
axes.set<&Axes_3D::Prop::y_axis>(std::move(frequency_axis));
axes.set<&Axes_3D::Prop::z_axis>(std::move(level_axis));
return {{"success", true}, {"generated_count", vertex_count},
{"triangle_count", vertex_count / 3}};
} catch (const std::exception& error) {
return {{"success", false}, {"error", error.what()}};
}
}
};
}
std::shared_ptr<Plot> make_datoviz_point_plot(asio::any_io_executor executor) {
@@ -358,6 +602,31 @@ std::shared_ptr<Plot> make_datoviz_mesh_plot(asio::any_io_executor executor) {
return make_visual_plot<Mesh_Visual>(std::move(executor), "Mesh Visual", Impl<Mesh_Visual>::Builder{}.set(&Mesh_Visual::Prop::items, mesh).build());
}
std::shared_ptr<Plot> make_datoviz_spectrogram_plot(asio::any_io_executor executor) {
const Spectrogram_Parameters parameters;
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.vertical_field_of_view_degrees = 41.0;
components.axes = {
plot::Axis_Descriptor{{0.0, parameters.time_span_seconds},
plot::Axis_Scale::time, "Time", "s", 6, 1, true, true, true},
plot::Axis_Descriptor{{parameters.minimum_frequency_hz,
parameters.maximum_frequency_hz}, plot::Axis_Scale::logarithmic,
"Frequency", "Hz", 5, 0, true, true, true},
plot::Axis_Descriptor{{parameters.minimum_level_db,
parameters.maximum_level_db}, plot::Axis_Scale::linear,
"SPL", "dB", 7, 0, true, true, true}};
return make_visual_plot<Mesh_Visual>(
std::move(executor), "3D Spectrogram",
Impl<Mesh_Visual>::Builder{}
.set(&Mesh_Visual::Prop::items, spectrogram_mesh(parameters))
.build(),
std::move(components), Spectrogram_Data_Generator{});
}
std::shared_ptr<Plot> make_datoviz_path_plot(asio::any_io_executor executor) {
std::vector<Path_Vertex> path;
for (int index = 0; index < 64; ++index) { const float t = static_cast<float>(index) / 63.0F; path.push_back({{-0.9F + 1.8F * t, 0.48F * std::sin(t * 4.0F * std::numbers::pi_v<float>), 0.25F * std::cos(t * 2.0F * std::numbers::pi_v<float>)}, color(static_cast<std::uint8_t>(70 + 170 * t), static_cast<std::uint8_t>(220 - 80 * t), 245), 5.0F}); }
+1
View File
@@ -11,6 +11,7 @@ namespace aethera::web {
[[nodiscard]] std::shared_ptr<Plot> make_datoviz_vector_plot(asio::any_io_executor executor);
[[nodiscard]] std::shared_ptr<Plot> make_datoviz_primitive_plot(asio::any_io_executor executor);
[[nodiscard]] std::shared_ptr<Plot> make_datoviz_mesh_plot(asio::any_io_executor executor);
[[nodiscard]] std::shared_ptr<Plot> make_datoviz_spectrogram_plot(asio::any_io_executor executor);
[[nodiscard]] std::shared_ptr<Plot> make_datoviz_path_plot(asio::any_io_executor executor);
[[nodiscard]] std::shared_ptr<Plot> make_datoviz_image_plot(asio::any_io_executor executor);
[[nodiscard]] std::shared_ptr<Plot> make_datoviz_labels_plot(asio::any_io_executor executor);
+4
View File
@@ -228,6 +228,10 @@ inline std::string_view protocol_field_label(std::string_view key) {
static constexpr std::pair<std::string_view, std::string_view> labels[] = {
{"position", "位置"}, {"viewport", "视口尺寸"}, {"background", "背景颜色"},
{"clear_color", "清屏颜色"}, {"view_active", "启用视图"},
{"initial_view", "初始相机视图"}, {"projection", "投影方式"},
{"control", "相机交互控制"}, {"vertical_field_of_view_degrees", "垂直视场角"},
{"near_plane", "近裁剪面"}, {"far_plane", "远裁剪面"},
{"x_axis", "X 坐标轴"}, {"y_axis", "Y 坐标轴"}, {"z_axis", "Z 坐标轴"},
{"pixel_length", "轴线长度"}, {"orientation", "轴线方向"}, {"tick_length", "主刻度长度"},
{"sub_tick_length", "次刻度长度"}, {"axis_pen", "轴线画笔"}, {"unit_text", "单位文字"},
{"unit_text_font", "单位字体"}, {"unit_text_pen", "单位文字画笔"},
+1
View File
@@ -57,6 +57,7 @@ int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root)
plots->emplace("datoviz_vector", make_datoviz_vector_plot(executor));
plots->emplace("datoviz_primitive", make_datoviz_primitive_plot(executor));
plots->emplace("datoviz_mesh", make_datoviz_mesh_plot(executor));
plots->emplace("datoviz_spectrogram", make_datoviz_spectrogram_plot(executor));
plots->emplace("datoviz_path", make_datoviz_path_plot(executor));
plots->emplace("datoviz_image", make_datoviz_image_plot(executor));
plots->emplace("datoviz_labels", make_datoviz_labels_plot(executor));