956 lines
59 KiB
C++
956 lines
59 KiB
C++
#include "Gallery_Plots.hpp"
|
|
#include "Renderable_Adapter.hpp"
|
|
#include <render_3D/Render_3D.hpp>
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <limits>
|
|
#include <numbers>
|
|
#include <random>
|
|
#include <stdexcept>
|
|
#include <type_traits>
|
|
#include <utility>
|
|
#include <vector>
|
|
namespace aethera::web {
|
|
namespace {
|
|
using namespace render_3d;
|
|
using Scene_3D = Render_Scene_3D;
|
|
using Json = nlohmann::json;
|
|
Json number_field(std::string key, std::string label, std::string description,
|
|
double value, double minimum, double maximum, double step) {
|
|
return {
|
|
{"key", std::move(key)}, {"label", std::move(label)}, {"description", std::move(description)},
|
|
{"editor", "number"}, {"editable", true}, {"value", value},
|
|
{"minimum", minimum}, {"maximum", maximum}, {"step", step}
|
|
};
|
|
}
|
|
Json integer_field(std::string key, std::string label, std::string description,
|
|
std::size_t value, std::size_t minimum, std::size_t maximum) {
|
|
auto field = number_field(std::move(key), std::move(label), std::move(description),
|
|
static_cast<double>(value), static_cast<double>(minimum),
|
|
static_cast<double>(maximum), 1.0);
|
|
field["editor"] = "integer";
|
|
return field;
|
|
}
|
|
template <typename Definition>
|
|
Json generator_schema() {
|
|
Json fields = Json::array();
|
|
if constexpr (std::same_as<Definition, Volume_Visual>) {
|
|
fields.push_back(integer_field("width", "体数据宽度", "体素网格 X 方向尺寸;总量为宽×高×深。", 64, 1, 256));
|
|
fields.push_back(integer_field("height", "体数据高度", "体素网格 Y 方向尺寸;总量为宽×高×深。", 64, 1, 256));
|
|
fields.push_back(integer_field("depth", "体数据深度", "体素网格 Z 方向尺寸;总量为宽×高×深。", 64, 1, 256));
|
|
fields.push_back(number_field("value_min", "体素值下界", "每个体素随机标量值的下界。", 0.0, -1'000'000.0, 1'000'000.0, 0.01));
|
|
fields.push_back(number_field("value_max", "体素值上界", "每个体素随机标量值的上界,必须大于下界。", 1.0, -1'000'000.0, 1'000'000.0, 0.01));
|
|
fields.push_back(integer_field("seed", "随机种子", "固定种子可复现体素压力数据,便于跨策略对比。", 42, 1, 4'294'967'295ULL));
|
|
return {{"label", "生成体素标量场"}, {"description", "按三维网格尺寸生成连续体数据,不使用随机位置。"}, {"fields", std::move(fields)}};
|
|
}
|
|
fields.push_back(integer_field("count", "图元数量", "本次替换到 Visual 的图元数量;用于逐级提升 CPU Prepare、GPU 上传与绘制压力。", 10'000, 1, 5'000'000));
|
|
fields.push_back(integer_field("seed", "随机种子", "固定种子可复现相同空间分布,确保多图与传输模式的性能结果可比较。", 42, 1, 4'294'967'295ULL));
|
|
fields.push_back(number_field("x_min", "Scene X 下界", "随机位置在 Scene X 轴上的下界。", -1.0, -1'000'000.0, 1'000'000.0, 0.01));
|
|
fields.push_back(number_field("x_max", "Scene X 上界", "随机位置在 Scene X 轴上的上界,必须大于下界。", 1.0, -1'000'000.0, 1'000'000.0, 0.01));
|
|
fields.push_back(number_field("y_min", "Scene Y 下界", "随机位置在 Scene Y 轴上的下界。", -1.0, -1'000'000.0, 1'000'000.0, 0.01));
|
|
fields.push_back(number_field("y_max", "Scene Y 上界", "随机位置在 Scene Y 轴上的上界,必须大于下界。", 1.0, -1'000'000.0, 1'000'000.0, 0.01));
|
|
fields.push_back(number_field("z_min", "Scene Z 下界", "随机位置在 Scene Z 轴上的下界。", -1.0, -1'000'000.0, 1'000'000.0, 0.01));
|
|
fields.push_back(number_field("z_max", "Scene Z 上界", "随机位置在 Scene Z 轴上的上界,必须大于下界。", 1.0, -1'000'000.0, 1'000'000.0, 0.01));
|
|
std::string label{"生成三维图元"};
|
|
std::string description{"按各轴独立范围随机生成 Scene 坐标。"};
|
|
if constexpr (std::same_as<Definition, Point_Visual>) {
|
|
label = "生成三维点";
|
|
fields.push_back(number_field("diameter_min", "点直径下界", "随机点直径下界,单位为屏幕像素。", 2.0, 0.1, 4096.0, 0.1));
|
|
fields.push_back(number_field("diameter_max", "点直径上界", "随机点直径上界,单位为屏幕像素。", 12.0, 0.1, 4096.0, 0.1));
|
|
}
|
|
else if constexpr (std::same_as<Definition, Splat_Visual>) {
|
|
label = "生成三维高斯 Splat";
|
|
fields.push_back(number_field("sigma_min", "标准差下界", "高斯主轴标准差下界,使用 Scene 坐标。", 0.01, 0.0001, 1000.0, 0.001));
|
|
fields.push_back(number_field("sigma_max", "标准差上界", "高斯主轴标准差上界,使用 Scene 坐标。", 0.08, 0.0001, 1000.0, 0.001));
|
|
fields.push_back(number_field("angle_min", "旋转角下界", "Splat 主轴旋转角下界,单位为弧度。", -3.14159, -1000.0, 1000.0, 0.01));
|
|
fields.push_back(number_field("angle_max", "旋转角上界", "Splat 主轴旋转角上界,单位为弧度。", 3.14159, -1000.0, 1000.0, 0.01));
|
|
}
|
|
else if constexpr (std::same_as<Definition, Pixel_Visual>) {
|
|
label = "生成三维像素";
|
|
fields.push_back(number_field("size_min", "像素边长下界", "方形像素边长下界,单位为屏幕像素。", 1.0, 0.1, 4096.0, 0.1));
|
|
fields.push_back(number_field("size_max", "像素边长上界", "方形像素边长上界,单位为屏幕像素。", 6.0, 0.1, 4096.0, 0.1));
|
|
}
|
|
else if constexpr (std::same_as<Definition, Marker_Visual>) {
|
|
label = "生成三维标记";
|
|
fields.push_back(number_field("diameter_min", "标记直径下界", "标记直径下界,单位为屏幕像素。", 4.0, 0.1, 4096.0, 0.1));
|
|
fields.push_back(number_field("diameter_max", "标记直径上界", "标记直径上界,单位为屏幕像素。", 18.0, 0.1, 4096.0, 0.1));
|
|
}
|
|
else if constexpr (std::same_as<Definition, Sphere_Visual>) {
|
|
label = "生成三维球体";
|
|
fields.push_back(number_field("radius_min", "球体半径下界", "球体半径下界,使用 Scene 坐标。", 0.01, 0.0001, 1000.0, 0.001));
|
|
fields.push_back(number_field("radius_max", "球体半径上界", "球体半径上界,使用 Scene 坐标。", 0.08, 0.0001, 1000.0, 0.001));
|
|
}
|
|
else if constexpr (std::same_as<Definition, Segment_Visual>) {
|
|
label = "生成三维线段";
|
|
description = "起点和终点分别在各轴范围内随机生成。";
|
|
fields.push_back(number_field("width_min", "线宽下界", "线段宽度下界,单位为屏幕像素。", 1.0, 0.1, 4096.0, 0.1));
|
|
fields.push_back(number_field("width_max", "线宽上界", "线段宽度上界,单位为屏幕像素。", 5.0, 0.1, 4096.0, 0.1));
|
|
}
|
|
else if constexpr (std::same_as<Definition, Vector_Visual>) {
|
|
label = "生成三维向量";
|
|
description = "原点按 Scene 范围随机生成,方向分量使用单独范围。";
|
|
fields.push_back(number_field("direction_min", "方向分量下界", "向量 X/Y/Z 方向分量的随机下界。", -0.3, -1'000'000.0, 1'000'000.0, 0.01));
|
|
fields.push_back(number_field("direction_max", "方向分量上界", "向量 X/Y/Z 方向分量的随机上界。", 0.3, -1'000'000.0, 1'000'000.0, 0.01));
|
|
}
|
|
else if constexpr (std::same_as<Definition, Primitive_Visual>) label = "生成三维图元顶点";
|
|
else if constexpr (std::same_as<Definition, Mesh_Visual>) label = "生成三维网格顶点";
|
|
else if constexpr (std::same_as<Definition, Path_Visual>) {
|
|
label = "生成三维路径顶点";
|
|
fields.push_back(number_field("width_min", "路径宽度下界", "路径宽度下界,单位为屏幕像素。", 1.0, 0.1, 4096.0, 0.1));
|
|
fields.push_back(number_field("width_max", "路径宽度上界", "路径宽度上界,单位为屏幕像素。", 5.0, 0.1, 4096.0, 0.1));
|
|
}
|
|
else if constexpr (std::same_as<Definition, Image_Visual>) {
|
|
label = "生成三维图像实例";
|
|
fields.push_back(number_field("extent_min", "图像尺寸下界", "图像宽高的 Scene 坐标下界。", 0.02, 0.0001, 1000.0, 0.001));
|
|
fields.push_back(number_field("extent_max", "图像尺寸上界", "图像宽高的 Scene 坐标上界。", 0.2, 0.0001, 1000.0, 0.001));
|
|
}
|
|
else if constexpr (std::same_as<Definition, Labels_Visual>) {
|
|
label = "生成三维标签实例";
|
|
fields.push_back(number_field("extent_min", "标签尺寸下界", "标签宽高的屏幕像素下界。", 8.0, 0.1, 4096.0, 0.1));
|
|
fields.push_back(number_field("extent_max", "标签尺寸上界", "标签宽高的屏幕像素上界。", 48.0, 0.1, 4096.0, 0.1));
|
|
}
|
|
else if constexpr (std::same_as<Definition, Glyph_Visual>) {
|
|
label = "生成三维字形实例";
|
|
fields.push_back(number_field("angle_min", "字形旋转下界", "字形旋转角下界,单位为弧度。", -3.14159, -1000.0, 1000.0, 0.01));
|
|
fields.push_back(number_field("angle_max", "字形旋转上界", "字形旋转角上界,单位为弧度。", 3.14159, -1000.0, 1000.0, 0.01));
|
|
}
|
|
else if constexpr (std::same_as<Definition, Text_Visual>) {
|
|
label = "生成三维文本实例";
|
|
fields.push_back(number_field("size_min", "字号下界", "随机文本字号下界,单位为屏幕像素。", 10.0, 0.1, 4096.0, 0.1));
|
|
fields.push_back(number_field("size_max", "字号上界", "随机文本字号上界,单位为屏幕像素。", 28.0, 0.1, 4096.0, 0.1));
|
|
}
|
|
return {{"label", std::move(label)}, {"description", std::move(description)}, {"fields", std::move(fields)}};
|
|
}
|
|
double input_number(const Json& input, std::string_view key) {
|
|
const auto& value = input.at(key);
|
|
if (!value.is_number()) throw std::invalid_argument(std::string(key) + " must be a number");
|
|
const auto result = value.get<double>();
|
|
if (!std::isfinite(result)) throw std::invalid_argument(std::string(key) + " must be finite");
|
|
return result;
|
|
}
|
|
std::size_t input_count(const Json& input, std::string_view key, std::size_t maximum = 1'000'000) {
|
|
const auto value = input_number(input, key);
|
|
if (value < 1.0 || value > static_cast<double>(maximum) || std::floor(value) != value) throw std::invalid_argument(std::string(key) + " is outside the supported integer range");
|
|
return static_cast<std::size_t>(value);
|
|
}
|
|
std::pair<float, float> input_range(const Json& input, std::string_view minimum_key,
|
|
std::string_view maximum_key) {
|
|
const auto minimum = input_number(input, minimum_key);
|
|
const auto maximum = input_number(input, maximum_key);
|
|
if (minimum >= maximum) throw std::invalid_argument(std::string(maximum_key) + " must be greater than " + std::string(minimum_key));
|
|
if (minimum < -std::numeric_limits<float>::max() || maximum > std::numeric_limits<float>::max()) throw std::invalid_argument("generator range exceeds float coordinates");
|
|
return {static_cast<float>(minimum), static_cast<float>(maximum)};
|
|
}
|
|
template <typename Definition>
|
|
struct Item_Randomizer {
|
|
public:
|
|
explicit Item_Randomizer(const Json& input) {
|
|
const auto [x_min, x_max] = input_range(input, "x_min", "x_max");
|
|
const auto [y_min, y_max] = input_range(input, "y_min", "y_max");
|
|
const auto [z_min, z_max] = input_range(input, "z_min", "z_max");
|
|
x = std::uniform_real_distribution<float>(x_min, x_max);
|
|
y = std::uniform_real_distribution<float>(y_min, y_max);
|
|
z = std::uniform_real_distribution<float>(z_min, z_max);
|
|
const auto set_first = [&](std::string_view minimum, std::string_view maximum) {
|
|
const auto [lower, upper] = input_range(input, minimum, maximum);
|
|
first = std::uniform_real_distribution<float>(lower, upper);
|
|
};
|
|
const auto set_second = [&](std::string_view minimum, std::string_view maximum) {
|
|
const auto [lower, upper] = input_range(input, minimum, maximum);
|
|
second = std::uniform_real_distribution<float>(lower, upper);
|
|
};
|
|
if constexpr (std::same_as<Definition, Point_Visual> || std::same_as<Definition, Marker_Visual>) set_first("diameter_min", "diameter_max");
|
|
else if constexpr (std::same_as<Definition, Splat_Visual>) {
|
|
set_first("sigma_min", "sigma_max");
|
|
set_second("angle_min", "angle_max");
|
|
}
|
|
else if constexpr (std::same_as<Definition, Pixel_Visual> || std::same_as<Definition, Text_Visual>) set_first("size_min", "size_max");
|
|
else if constexpr (std::same_as<Definition, Sphere_Visual>) set_first("radius_min", "radius_max");
|
|
else if constexpr (std::same_as<Definition, Segment_Visual> || std::same_as<Definition, Path_Visual>) set_first("width_min", "width_max");
|
|
else if constexpr (std::same_as<Definition, Vector_Visual>) set_first("direction_min", "direction_max");
|
|
else if constexpr (std::same_as<Definition, Image_Visual> || std::same_as<Definition, Labels_Visual>) set_first("extent_min", "extent_max");
|
|
else if constexpr (std::same_as<Definition, Glyph_Visual>) set_first("angle_min", "angle_max");
|
|
}
|
|
void operator()(typename Definition::Item& item, std::mt19937_64& engine) {
|
|
const auto vector = [&] {
|
|
return Vec3{x(engine), y(engine), z(engine)};
|
|
};
|
|
if constexpr (requires { item.position = vector(); }) item.position = vector();
|
|
else if constexpr (requires { item.center = vector(); }) item.center = vector();
|
|
else if constexpr (requires { item.origin = vector(); }) item.origin = vector();
|
|
else if constexpr (requires { item.start = vector(); item.end = vector(); }) {
|
|
item.start = vector();
|
|
item.end = vector();
|
|
}
|
|
if constexpr (std::same_as<Definition, Point_Visual>) item.diameter_px = first(engine);
|
|
else if constexpr (std::same_as<Definition, Splat_Visual>) {
|
|
item.sigma = {first(engine), first(engine)};
|
|
item.angle = second(engine);
|
|
}
|
|
else if constexpr (std::same_as<Definition, Pixel_Visual>) item.size_px = first(engine);
|
|
else if constexpr (std::same_as<Definition, Marker_Visual>) item.diameter_px = first(engine);
|
|
else if constexpr (std::same_as<Definition, Sphere_Visual>) item.radius = first(engine);
|
|
else if constexpr (std::same_as<Definition, Segment_Visual>) item.width_px = first(engine);
|
|
else if constexpr (std::same_as<Definition, Vector_Visual>) {
|
|
item.direction = {first(engine), first(engine), first(engine)};
|
|
}
|
|
else if constexpr (std::same_as<Definition, Path_Visual>) item.width_px = first(engine);
|
|
else if constexpr (std::same_as<Definition, Image_Visual> || std::same_as<Definition, Labels_Visual>) item.extent = {first(engine), first(engine)};
|
|
else if constexpr (std::same_as<Definition, Glyph_Visual>) item.angle = first(engine);
|
|
else if constexpr (std::same_as<Definition, Text_Visual>) item.size_px = first(engine);
|
|
}
|
|
private:
|
|
std::uniform_real_distribution<float> x{};
|
|
std::uniform_real_distribution<float> y{};
|
|
std::uniform_real_distribution<float> z{};
|
|
std::uniform_real_distribution<float> first{};
|
|
std::uniform_real_distribution<float> second{};
|
|
};
|
|
struct Random_Data_Generator {
|
|
static std::uint64_t mix(std::uint64_t value) noexcept {
|
|
value += 0x9E3779B97F4A7C15ULL;
|
|
value = (value ^ (value >> 30U)) * 0xBF58476D1CE4E5B9ULL;
|
|
value = (value ^ (value >> 27U)) * 0x94D049BB133111EBULL;
|
|
return value ^ (value >> 31U);
|
|
}
|
|
static float signed_unit(std::uint64_t sequence, std::size_t index,
|
|
std::uint64_t lane) noexcept {
|
|
const auto bits = mix(sequence ^
|
|
(static_cast<std::uint64_t>(index) << 8U) ^ lane);
|
|
return static_cast<float>(bits >> 40U) /
|
|
static_cast<float>(1U << 24U) * 2.0F - 1.0F;
|
|
}
|
|
static void push(Vec3& value, std::uint64_t sequence,
|
|
std::size_t index, std::uint64_t lane,
|
|
float amount = 0.018F) noexcept {
|
|
value.x = std::clamp(value.x + amount * signed_unit(sequence, index, lane),
|
|
-0.96F, 0.96F);
|
|
value.y = std::clamp(value.y + amount * signed_unit(sequence, index, lane + 1U),
|
|
-0.96F, 0.96F);
|
|
value.z = std::clamp(value.z + amount * signed_unit(sequence, index, lane + 2U),
|
|
-0.96F, 0.96F);
|
|
}
|
|
template <typename Visual_Object>
|
|
void update(Visual_Object& visual, Axes_3D&, Marker_Visual*,
|
|
const Plot_Render_Tick& request) {
|
|
using Definition = typename Visual_Object::Attached_Object;
|
|
using Prop = typename Definition::Prop;
|
|
const auto sequence = request.sequence != 0
|
|
? request.sequence
|
|
: static_cast<std::uint64_t>(request.time_milliseconds * 10.0);
|
|
auto items = visual.template current_buffer<typename Definition::Frame_Tag>()
|
|
.items;
|
|
for (std::size_t index = 0; index < items.size(); ++index) {
|
|
auto& item = items[index];
|
|
if constexpr (requires { item.position; })
|
|
push(item.position, sequence, index, 0U);
|
|
else if constexpr (requires { item.center; })
|
|
push(item.center, sequence, index, 0U);
|
|
else if constexpr (requires { item.origin; }) {
|
|
push(item.origin, sequence, index, 0U);
|
|
if constexpr (requires { item.direction; })
|
|
push(item.direction, sequence, index, 3U, 0.008F);
|
|
}
|
|
else if constexpr (requires { item.start; item.end; }) {
|
|
push(item.start, sequence, index, 0U);
|
|
push(item.end, sequence, index, 3U);
|
|
}
|
|
if constexpr (requires { item.value; })
|
|
item.value = std::clamp(
|
|
item.value + 0.07F * signed_unit(sequence, index, 7U),
|
|
0.0F, 1.0F);
|
|
if constexpr (std::same_as<Definition, Text_Visual>) {
|
|
if (index == 0)
|
|
item.text = "Aethera frame " +
|
|
std::to_string(sequence % 10'000U);
|
|
}
|
|
}
|
|
visual.template pending_buffer<typename Definition::Frame_Tag>().items =
|
|
std::move(items);
|
|
|
|
}
|
|
};
|
|
template <typename Visual_Object, typename Data_Generator = Random_Data_Generator>
|
|
struct Visual_Scene_View final : public Plot::Scene_View {
|
|
public:
|
|
using Camera_Object = Camera_3D;
|
|
using Axes_Object = Axes_3D;
|
|
using Marker_Object = 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 = {},
|
|
std::unique_ptr<Marker_Object> markers = {}) : data_generator_(std::move(data_generator)), camera_(std::move(camera)),
|
|
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;
|
|
using Scene_Adapter = detail::Renderable_Adapter<Scene_3D,
|
|
detail::Prop_Field < &Render_Scene_3D::Prop::clear_color, "clear_color", "Linear scene clear color.">,
|
|
detail::Prop_Field < &Render_Scene_3D::Prop::view_active, "view_active", "Whether the scene publishes rendered frames." >>;
|
|
using Visual_Adapter = detail::Renderable_Adapter<Visual_Object,
|
|
detail::Prop_Field < &Prop::transform, "transform", "World transform applied to the complete visual.">,
|
|
detail::Prop_Field < &Prop::visible, "visible", "Whether the visual participates in rendering." >,
|
|
detail::Prop_Field < &Prop::depth_test, "depth_test", "Whether fragments use depth testing." >,
|
|
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::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."> >;
|
|
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.", "axis3d">,
|
|
detail::Prop_Field < &Axes_3D::Prop::y_axis, "y_axis", "Y axis range, scale, ticks, label and unit.", "axis3d" >,
|
|
detail::Prop_Field<&Axes_3D::Prop::z_axis, "z_axis", "Z axis range, scale, ticks, label and unit.", "axis3d"> >;
|
|
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_}));
|
|
if constexpr (std::same_as<Definition, Marker_Visual>) {
|
|
using Marker_Adapter = detail::Renderable_Adapter<Visual_Object,
|
|
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::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 {
|
|
nlohmann::json components = nlohmann::json::array();
|
|
for (const auto& descriptor : descriptors_) components.push_back(descriptor->schema());
|
|
return {{"protocol", "aethera.plot.inspector"}, {"version", 3}, {"components", std::move(components)}};
|
|
}
|
|
[[nodiscard]] nlohmann::json write_prop(std::string_view component, std::string_view key, const nlohmann::json& value) override {
|
|
const auto found = std::ranges::find_if(descriptors_, [&](const auto& descriptor) {
|
|
return descriptor->id() == component;
|
|
});
|
|
if (found == descriptors_.end()) return {{"success", false}, {"error", "unknown component"}};
|
|
auto result = (*found)->write_prop(key, value);
|
|
result["component"] = component;
|
|
return result;
|
|
}
|
|
[[nodiscard]] nlohmann::json component_state(std::string_view component) const override {
|
|
const auto found = std::ranges::find_if(descriptors_, [&](const auto& descriptor) {
|
|
return descriptor->id() == component;
|
|
});
|
|
if (found == descriptors_.end())
|
|
return {{"success", false}, {"error", "unknown component"}};
|
|
return (*found)->state();
|
|
}
|
|
[[nodiscard]] nlohmann::json capture_components(
|
|
const std::vector<std::string>& components) const override {
|
|
nlohmann::json result = nlohmann::json::object();
|
|
for (const auto& component : components) {
|
|
const auto found = std::ranges::find_if(descriptors_, [&](const auto& descriptor) {
|
|
return descriptor->id() == component;
|
|
});
|
|
if (found != descriptors_.end())
|
|
result[component] = (*found)->values();
|
|
}
|
|
return result;
|
|
}
|
|
[[nodiscard]] nlohmann::json data_generator_schema() const override {
|
|
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 Frame = typename Definition::Frame;
|
|
using Items = std::remove_cvref_t<decltype(std::declval<Frame>().items)>;
|
|
try {
|
|
std::mt19937_64 engine{input_count(input, "seed", 4'294'967'295ULL)};
|
|
Items generated;
|
|
std::size_t count{};
|
|
if constexpr (std::same_as<Definition, Volume_Visual>) {
|
|
const auto width = input_count(input, "width", 256);
|
|
const auto height = input_count(input, "height", 256);
|
|
const auto depth = input_count(input, "depth", 256);
|
|
if (width > 1'000'000 / height || width * height > 1'000'000 / depth) throw std::invalid_argument("volume dimensions exceed 1,000,000 voxels");
|
|
count = width * height * depth;
|
|
const auto [minimum, maximum] = input_range(input, "value_min", "value_max");
|
|
std::uniform_real_distribution<float> distribution(minimum, maximum);
|
|
generated.resize(count);
|
|
for (auto& item : generated) item.value = distribution(engine);
|
|
auto& frame = visual_->template pending_buffer<
|
|
typename Definition::Frame_Tag>();
|
|
frame.field_width = static_cast<std::uint32_t>(width);
|
|
frame.field_height = static_cast<std::uint32_t>(height);
|
|
frame.field_depth = static_cast<std::uint32_t>(depth);
|
|
}
|
|
else {
|
|
count = input_count(input, "count", 5'000'000);
|
|
const auto& current = visual_->template current_buffer<
|
|
typename Definition::Frame_Tag>().items;
|
|
if (current.empty()) throw std::invalid_argument("visual has no item template");
|
|
const auto prototype = current.front();
|
|
Item_Randomizer<Definition> randomize(input);
|
|
generated.reserve(count);
|
|
for (std::size_t index = 0; index < count; ++index) {
|
|
auto item = prototype;
|
|
randomize(item, engine);
|
|
generated.push_back(std::move(item));
|
|
}
|
|
}
|
|
visual_->template pending_buffer<typename Definition::Frame_Tag>()
|
|
.items = std::move(generated);
|
|
return {{"success", true}, {"generated_count", count}};
|
|
}
|
|
catch (const std::exception& error) {
|
|
return {{"success", false}, {"error", error.what()}};
|
|
}
|
|
}
|
|
}
|
|
void update(const Plot_Render_Tick& request) override {
|
|
if constexpr (requires(Data_Generator& generator, Visual_Object& visual,
|
|
Axes_Object& axes,
|
|
Marker_Object* markers,
|
|
const Plot_Render_Tick& 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 协议描述。 */
|
|
};
|
|
struct Scene_Components_3D {
|
|
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 Visual, typename Build_Result>
|
|
Build_Result initialize_visual_frame(
|
|
Build_Result result, typename Visual::Frame frame) {
|
|
if (result)
|
|
result.value()->template initialize_buffer<typename Visual::Frame_Tag>(
|
|
std::move(frame));
|
|
return result;
|
|
}
|
|
template <typename Definition, typename Build_Result,
|
|
typename Data_Generator = Random_Data_Generator>
|
|
std::shared_ptr<Plot> make_visual_plot(std::string label,
|
|
Build_Result build_result,
|
|
Scene_Components_3D components = {},
|
|
Data_Generator data_generator = {},
|
|
std::unique_ptr<Marker_Visual> markers = {}) {
|
|
using Visual_Object = Definition;
|
|
using Camera_Object = Camera_3D;
|
|
using Axes_Object = Axes_3D;
|
|
if (!build_result) throw std::logic_error("3D Gallery visual dependency graph is invalid");
|
|
auto visual = std::move(build_result).value();
|
|
auto camera_result = Camera_Object::Builder<Camera_Object>{}
|
|
.set(&Camera_3D::Prop::initial_view, components.camera.initial_view)
|
|
.set(&Camera_3D::Prop::projection, components.camera.projection)
|
|
.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)
|
|
.set(&Camera_3D::Prop::far_plane, components.camera.far_plane)
|
|
.build();
|
|
auto axes_result = Axes_Object::Builder<Axes_Object>{}
|
|
.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_3D>{};
|
|
scene_builder.add_camera(not_null{camera.get()})
|
|
.add_axes(not_null{axes.get()})
|
|
.add_renderable(not_null{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);
|
|
if (markers) scene_builder.add_renderable(not_null{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(markers));
|
|
return std::make_shared<Plot>(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};
|
|
double animation_speed{1.0};
|
|
std::size_t update_every_n_frames{1};
|
|
bool animation_enabled{true};
|
|
};
|
|
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;
|
|
}
|
|
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;
|
|
};
|
|
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 *
|
|
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);
|
|
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)
|
|
};
|
|
}
|
|
}
|
|
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 {
|
|
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", "时间采样数", "时间方向网格采样数;GPU 顶点数约为 6×(时间采样数-1)×(频率分箱数-1)。", parameters.time_sample_count, 16, 1024));
|
|
fields.push_back(integer_field("frequency_bin_count", "频率分箱数", "对数频率方向分箱数;与时间采样数共同决定三角形和每次上传的数据量。", parameters.frequency_bin_count, 16, 1024));
|
|
fields.push_back(integer_field("ridge_count", "谱峰轨迹数", "生成随时间漂移的窄带谱峰数量。", 5, 1, 12));
|
|
fields.push_back(number_field("time_span_seconds", "时间跨度", "X 轴时间范围,单位秒。", parameters.time_span_seconds, 0.1, 3600.0, 0.1));
|
|
fields.push_back(number_field("minimum_frequency_hz", "最低频率", "对数频率轴下界,必须大于零。", parameters.minimum_frequency_hz, 0.001, 1.0e12, 1.0));
|
|
fields.push_back(number_field("maximum_frequency_hz", "最高频率", "对数频率轴上界,必须大于最低频率。", parameters.maximum_frequency_hz, 0.002, 1.0e12, 10.0));
|
|
fields.push_back(number_field("minimum_level_db", "最低声压级", "Z 轴色阶与高度下界,单位 dB。", parameters.minimum_level_db, -1000.0, 1000.0, 1.0));
|
|
fields.push_back(number_field("maximum_level_db", "最高声压级", "Z 轴色阶与高度上界,必须大于下界。", parameters.maximum_level_db, -1000.0, 1000.0, 1.0));
|
|
fields.push_back(number_field("animation_speed", "动态速度倍率", "谱峰随时间运动的倍率;0 表示保持当前相位。", parameters.animation_speed, 0.0, 100.0, 0.1));
|
|
fields.push_back(integer_field("update_every_n_frames", "数据更新帧间隔", "每 N 个渲染请求重建并上传一次 Mesh;可分离固定几何绘制与持续数据上传压力。", parameters.update_every_n_frames, 1, 10'000));
|
|
fields.push_back({{"key", "animation_enabled"}, {"label", "持续生成动态数据"}, {"description", "关闭后保留生成的数据集,仅测试固定 Mesh 的重复绘制;开启后按更新间隔持续重建。"}, {"editor", "boolean"}, {"editable", true}, {"value", parameters.animation_enabled}});
|
|
return {
|
|
{"label", "生成三维频谱瀑布"},
|
|
{"description", "按时间采样、对数频率分箱和声压级范围生成连续 GPU Mesh 表面。"},
|
|
{"fields", std::move(fields)}
|
|
};
|
|
}
|
|
[[nodiscard]] Json generate(Mesh_Visual& visual, Axes_3D& axes,
|
|
const Json& input) {
|
|
try {
|
|
Spectrogram_Parameters next;
|
|
next.time_sample_count = input_count(input, "time_sample_count", 1024);
|
|
next.frequency_bin_count = input_count(input, "frequency_bin_count", 1024);
|
|
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");
|
|
next.animation_speed = input_number(input, "animation_speed");
|
|
next.update_every_n_frames = input_count(input, "update_every_n_frames", 10'000);
|
|
const auto animation = input.at("animation_enabled");
|
|
if (!animation.is_boolean()) throw std::invalid_argument("animation_enabled must be boolean");
|
|
next.animation_enabled = animation.get<bool>();
|
|
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");
|
|
const auto cells = (next.time_sample_count - 1) * (next.frequency_bin_count - 1);
|
|
if (cells > 1'400'000) throw std::invalid_argument("spectrogram exceeds the 8,400,000 vertex stress-test limit");
|
|
parameters = next;
|
|
auto mesh = spectrogram_mesh(parameters);
|
|
const auto vertex_count = mesh.size();
|
|
visual.template pending_buffer<Mesh_Frame_Tag>().items =
|
|
std::move(mesh);
|
|
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()}};
|
|
}
|
|
}
|
|
void update(Mesh_Visual& visual, Axes_3D&,
|
|
Marker_Visual* markers,
|
|
const Plot_Render_Tick& request) {
|
|
if (!parameters.animation_enabled ||
|
|
request.sequence % parameters.update_every_n_frames != 0)
|
|
return;
|
|
const double animation_seconds = request.time_milliseconds / 1000.0 * parameters.animation_speed;
|
|
auto mesh = spectrogram_mesh(parameters, animation_seconds);
|
|
visual.template pending_buffer<Mesh_Frame_Tag>().items =
|
|
std::move(mesh);
|
|
if (!markers) return;
|
|
auto items = markers->template current_buffer<Marker_Frame_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);
|
|
}
|
|
markers->template pending_buffer<Marker_Frame_Tag>().items =
|
|
std::move(items);
|
|
}
|
|
};
|
|
}
|
|
std::shared_ptr<Plot> make_datoviz_point_plot() {
|
|
return make_visual_plot<Point_Visual>("Point Visual",
|
|
initialize_visual_frame<Point_Visual>(
|
|
Point_Visual::Builder<Point_Visual>{}.build(),
|
|
Point_Frame{{{{-0.65F, -0.25F, 0.05F}, color(255, 91, 110), 28.0F}, {{0.0F, 0.58F, 0.25F}, color(82, 226, 190), 34.0F}, {{0.62F, -0.12F, -0.15F}, color(75, 145, 255), 30.0F}}}));
|
|
}
|
|
std::shared_ptr<Plot> make_datoviz_splat_plot() {
|
|
return make_visual_plot<Splat_Visual>("Splat Visual",
|
|
initialize_visual_frame<Splat_Visual>(
|
|
Splat_Visual::Builder<Splat_Visual>{}.build(),
|
|
Splat_Frame{{{{-0.48F, 0.0F, 0.1F}, color(255, 98, 115, 210), {0.18F, 0.08F}, 0.45F}, {{0.28F, 0.15F, 0.0F}, color(66, 218, 188, 210), {0.12F, 0.22F}, -0.3F}, {{0.15F, -0.38F, 0.2F}, color(76, 132, 255, 210), {0.2F, 0.1F}, 0.9F}}}));
|
|
}
|
|
std::shared_ptr<Plot> make_datoviz_pixel_plot() {
|
|
std::vector<Pixel> pixels;
|
|
for (int y = -8; y <= 8; ++y) for (int x = -12; x <= 12; ++x) pixels.push_back({{x / 13.0F, y / 9.0F, 0.12F * std::sin(x * .45F) * std::cos(y * .35F)}, color(static_cast<std::uint8_t>(90 + 6 * (x + 12)), static_cast<std::uint8_t>(100 + 8 * (y + 8)), 230), 5.0F});
|
|
return make_visual_plot<Pixel_Visual>("Pixel Visual",
|
|
initialize_visual_frame<Pixel_Visual>(
|
|
Pixel_Visual::Builder<Pixel_Visual>{}.build(),
|
|
Pixel_Frame{std::move(pixels)}));
|
|
}
|
|
std::shared_ptr<Plot> make_datoviz_marker_plot() {
|
|
return make_visual_plot<Marker_Visual>("Marker Visual",
|
|
initialize_visual_frame<Marker_Visual>(
|
|
Marker_Visual::Builder<Marker_Visual>{}.build(),
|
|
Marker_Frame{{{{-0.7F, 0.0F, 0.0F}, color(255, 93, 115), 34.0F, 0.0F, Marker_Shape::disc}, {{-0.35F, 0.25F, 0.1F}, color(91, 226, 193), 36.0F, 0.25F, Marker_Shape::square}, {{0.0F, -0.2F, 0.2F}, color(100, 158, 255), 38.0F, 0.5F, Marker_Shape::triangle}, {{0.35F, 0.25F, 0.1F}, color(250, 195, 92), 40.0F, 0.75F, Marker_Shape::diamond}, {{0.7F, 0.0F, 0.0F}, color(201, 132, 255), 42.0F, 1.0F, Marker_Shape::cross}}}));
|
|
}
|
|
std::shared_ptr<Plot> make_datoviz_sphere_plot() {
|
|
return make_visual_plot<Sphere_Visual>("Sphere Visual",
|
|
initialize_visual_frame<Sphere_Visual>(
|
|
Sphere_Visual::Builder<Sphere_Visual>{}.build(),
|
|
Sphere_Frame{{{{-0.48F, -0.2F, 0.0F}, color(255, 91, 110), 0.28F}, {{0.08F, 0.25F, 0.18F}, color(82, 226, 190), 0.36F}, {{0.55F, -0.18F, -0.12F}, color(75, 145, 255), 0.24F}}}));
|
|
}
|
|
std::shared_ptr<Plot> make_datoviz_segment_plot() {
|
|
std::vector<Segment> segments;
|
|
for (int index = 0; index < 12; ++index) {
|
|
const float angle = static_cast<float>(index) * std::numbers::pi_v<float> / 6.0F;
|
|
segments.push_back({{0.0F, 0.0F, 0.0F}, {0.82F * std::cos(angle), 0.82F * std::sin(angle), 0.18F * std::sin(2 * angle)}, color(static_cast<std::uint8_t>(80 + index * 13), static_cast<std::uint8_t>(220 - index * 8), 240), 4.0F});
|
|
}
|
|
return make_visual_plot<Segment_Visual>("Segment Visual",
|
|
initialize_visual_frame<Segment_Visual>(
|
|
Segment_Visual::Builder<Segment_Visual>{}.build(),
|
|
Segment_Frame{std::move(segments)}));
|
|
}
|
|
std::shared_ptr<Plot> make_datoviz_vector_plot() {
|
|
return make_visual_plot<Vector_Visual>("Vector Visual",
|
|
initialize_visual_frame<Vector_Visual>(
|
|
Vector_Visual::Builder<Vector_Visual>{}.build(),
|
|
Vector_Frame{{{{-0.55F, -0.35F, 0.0F}, {0.55F, 0.2F, 0.25F}, color(255, 98, 115), 4.0F}, {{-0.1F, 0.0F, 0.0F}, {0.2F, 0.62F, 0.18F}, color(80, 225, 190), 5.0F}, {{0.35F, -0.25F, 0.0F}, {-0.12F, 0.25F, 0.65F}, color(78, 145, 255), 4.0F}}}));
|
|
}
|
|
std::shared_ptr<Plot> make_datoviz_primitive_plot() {
|
|
return make_visual_plot<Primitive_Visual>("Primitive Visual",
|
|
initialize_visual_frame<Primitive_Visual>(
|
|
Primitive_Visual::Builder<Primitive_Visual>{}.build(),
|
|
Primitive_Frame{{{{-0.72F, -0.55F, 0.0F}, color(255, 86, 110), {0, 0, 1}}, {{0.72F, -0.55F, 0.0F}, color(75, 145, 255), {0, 0, 1}}, {{0.0F, 0.72F, 0.25F}, color(82, 226, 190), {0, 0, 1}}}}));
|
|
}
|
|
std::shared_ptr<Plot> make_datoviz_mesh_plot() {
|
|
const std::vector<Mesh_Vertex> mesh{{{-0.65F, -0.55F, 0.0F}, color(255, 94, 112), {0, 0, 1}, {0, 0}}, {{0.65F, -0.55F, 0.0F}, color(75, 145, 255), {0, 0, 1}, {1, 0}}, {{0.65F, 0.55F, 0.0F}, color(82, 226, 190), {0, 0, 1}, {1, 1}}, {{-0.65F, -0.55F, 0.0F}, color(255, 94, 112), {0, 0, 1}, {0, 0}}, {{0.65F, 0.55F, 0.0F}, color(82, 226, 190), {0, 0, 1}, {1, 1}}, {{-0.65F, 0.55F, 0.0F}, color(244, 190, 86), {0, 0, 1}, {0, 1}}};
|
|
return make_visual_plot<Mesh_Visual>("Mesh Visual",
|
|
initialize_visual_frame<Mesh_Visual>(
|
|
Mesh_Visual::Builder<Mesh_Visual>{}.build(), Mesh_Frame{mesh}));
|
|
}
|
|
std::shared_ptr<Plot> make_datoviz_spectrogram_plot() {
|
|
const Spectrogram_Parameters parameters;
|
|
auto marker_result = initialize_visual_frame<Marker_Visual>(
|
|
Marker_Visual::Builder<Marker_Visual>{}
|
|
.set(&Marker_Visual::Prop::depth_test, false)
|
|
.build(),
|
|
Marker_Frame{std::vector<Marker>{
|
|
{
|
|
{-0.35F, -0.18F, 0.72F}, color(255, 244, 170), 23.0F, 0.0F,
|
|
Marker_Shape::diamond, false
|
|
},
|
|
{
|
|
{0.28F, 0.34F, 0.86F}, color(88, 236, 211), 21.0F, 0.0F,
|
|
Marker_Shape::cross, false
|
|
}
|
|
}});
|
|
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.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},
|
|
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>(
|
|
"3D Spectrogram",
|
|
initialize_visual_frame<Mesh_Visual>(
|
|
Mesh_Visual::Builder<Mesh_Visual>{}.build(),
|
|
Mesh_Frame{spectrogram_mesh(parameters)}),
|
|
std::move(components), Spectrogram_Data_Generator{parameters}, std::move(markers));
|
|
}
|
|
std::shared_ptr<Plot> make_datoviz_path_plot() {
|
|
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});
|
|
}
|
|
return make_visual_plot<Path_Visual>("Path Visual",
|
|
initialize_visual_frame<Path_Visual>(
|
|
Path_Visual::Builder<Path_Visual>{}.build(),
|
|
Path_Frame{std::move(path)}));
|
|
}
|
|
std::shared_ptr<Plot> make_datoviz_image_plot() {
|
|
std::vector<Color> pixels(32U * 32U);
|
|
for (std::uint32_t y = 0; y < 32U; ++y)
|
|
for (std::uint32_t x = 0; x < 32U; ++x)
|
|
pixels[y * 32U + x] = (x / 8U + y / 8U) % 2U == 0U
|
|
? color(40, 235, 205) : color(18, 42, 72);
|
|
return make_visual_plot<Image_Visual>("Image Visual",
|
|
initialize_visual_frame<Image_Visual>(
|
|
Image_Visual::Builder<Image_Visual>{}.build(),
|
|
Image_Frame{{{{0.0F, 0.0F, 0.0F}, {1.45F, 1.0F}, {0, 0, 1, 1}}},
|
|
32U, 32U, std::move(pixels)}));
|
|
}
|
|
std::shared_ptr<Plot> make_datoviz_labels_plot() {
|
|
std::vector<std::int32_t> labels(8U * 8U);
|
|
for (std::uint32_t y = 0; y < 8U; ++y)
|
|
for (std::uint32_t x = 0; x < 8U; ++x)
|
|
labels[y * 8U + x] = static_cast<std::int32_t>(
|
|
x / 4U + 2U * (y / 4U));
|
|
return make_visual_plot<Labels_Visual>("Labels Visual",
|
|
initialize_visual_frame<Labels_Visual>(
|
|
Labels_Visual::Builder<Labels_Visual>{}.build(),
|
|
Labels_Frame{{{{-0.55F, 0.28F, 0.0F}, {72, 34}, {0, 0, 0.5F, 0.5F}}, {{0.5F, 0.25F, 0.1F}, {72, 34}, {0.5F, 0, 1, 0.5F}}, {{-0.45F, -0.32F, 0.1F}, {72, 34}, {0, 0.5F, 0.5F, 1}}, {{0.55F, -0.3F, 0.0F}, {72, 34}, {0.5F, 0.5F, 1, 1}}},
|
|
8U, 8U, std::move(labels)}));
|
|
}
|
|
std::shared_ptr<Plot> make_datoviz_glyph_plot() {
|
|
return make_visual_plot<Glyph_Visual>("Glyph Visual",
|
|
initialize_visual_frame<Glyph_Visual>(
|
|
Glyph_Visual::Builder<Glyph_Visual>{}.build(),
|
|
Glyph_Frame{{{{-0.55F, -0.15F, 0.0F}, {-0.18F, -0.18F, 0.18F, 0.18F}, {0, 0, 1, 1}, color(255, 100, 120), -0.2F}, {{0.0F, 0.22F, 0.1F}, {-0.22F, -0.22F, 0.22F, 0.22F}, {0, 0, 1, 1}, color(82, 226, 190), 0.25F}, {{0.55F, -0.15F, 0.0F}, {-0.2F, -0.2F, 0.2F, 0.2F}, {0, 0, 1, 1}, color(80, 145, 255), 0.55F}}}));
|
|
}
|
|
std::shared_ptr<Plot> make_datoviz_text_plot() {
|
|
return make_visual_plot<Text_Visual>("Text Visual",
|
|
initialize_visual_frame<Text_Visual>(
|
|
Text_Visual::Builder<Text_Visual>{}.build(),
|
|
Text_Frame{{{{-0.72F, 0.28F, 0.0F}, "Aethera", color(82, 226, 190), 28.0F}, {{-0.62F, -0.15F, 0.1F}, "Datoviz Visual", color(90, 155, 255), 22.0F}}}));
|
|
}
|
|
std::shared_ptr<Plot> make_datoviz_volume_plot() {
|
|
std::vector<Voxel> voxels(16U * 16U * 16U);
|
|
for (std::size_t index = 0; index < voxels.size(); ++index) voxels[index].value = static_cast<float>(index % 256U) / 255.0F;
|
|
return make_visual_plot<Volume_Visual>("Volume Visual",
|
|
initialize_visual_frame<Volume_Visual>(
|
|
Volume_Visual::Builder<Volume_Visual>{}.build(),
|
|
Volume_Frame{std::move(voxels), 16U, 16U, 16U}));
|
|
}
|
|
}
|