MayaFlux 0.4.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
StateEncoder.cpp
Go to the documentation of this file.
1#include "StateEncoder.hpp"
2
3#include "Sinks.hpp"
4
8
9namespace MayaFlux::Nexus {
10
11namespace {
12
13 constexpr uint32_t k_exr_rows = 5;
14 constexpr uint32_t k_channels = 4;
15
16 // -------------------------------------------------------------------------
17 // Schema structs
18 // -------------------------------------------------------------------------
19
20 struct Range {
21 float min { 0.0F };
22 float max { 1.0F };
23
24 static constexpr auto describe()
25 {
26 return std::make_tuple(
27 IO::member("min", &Range::min),
28 IO::member("max", &Range::max));
29 }
30 };
31
32 struct RangeSet {
33 Range pos_x, pos_y, pos_z;
34 Range intensity;
36 Range size;
37 Range radius;
39
40 static constexpr auto describe()
41 {
42 return std::make_tuple(
43 IO::member("position.x", &RangeSet::pos_x),
44 IO::member("position.y", &RangeSet::pos_y),
45 IO::member("position.z", &RangeSet::pos_z),
46 IO::member("intensity", &RangeSet::intensity),
47 IO::member("color.r", &RangeSet::color_r),
48 IO::member("color.g", &RangeSet::color_g),
49 IO::member("color.b", &RangeSet::color_b),
50 IO::member("size", &RangeSet::size),
51 IO::member("radius", &RangeSet::radius),
52 IO::member("query_radius", &RangeSet::query_radius));
53 }
54 };
55
56 struct WiringStep {
57 glm::vec3 position {};
58 double delay_seconds { 0.0 };
59
60 static constexpr auto describe()
61 {
62 return std::make_tuple(
63 IO::member("position", &WiringStep::position),
64 IO::member("delay", &WiringStep::delay_seconds));
65 }
66 };
67
68 struct WiringRecord {
69 std::string kind { "commit_driven" };
70 std::optional<double> interval;
71 std::optional<double> duration;
72 std::optional<size_t> times;
73 std::optional<std::vector<WiringStep>> steps;
74
75 static constexpr auto describe()
76 {
77 return std::make_tuple(
78 IO::member("kind", &WiringRecord::kind),
79 IO::opt_member("interval", &WiringRecord::interval),
80 IO::opt_member("duration", &WiringRecord::duration),
81 IO::opt_member("times", &WiringRecord::times),
82 IO::opt_member("steps", &WiringRecord::steps));
83 }
84 };
85
86 struct AudioSinkRecord {
87 uint32_t channel {};
88 std::string fn_name;
89
90 static constexpr auto describe()
91 {
92 return std::make_tuple(
93 IO::member("channel", &AudioSinkRecord::channel),
94 IO::member("fn_name", &AudioSinkRecord::fn_name));
95 }
96 };
97
98 struct RenderSinkRecord {
99 std::string fn_name;
100
101 static constexpr auto describe()
102 {
103 return std::make_tuple(
104 IO::member("fn_name", &RenderSinkRecord::fn_name));
105 }
106 };
107
108 struct EntityRecord {
109 uint32_t id {};
110 std::string kind;
111 glm::vec3 position {};
112 float intensity { 0.0F };
113 float radius { 0.0F };
114 float query_radius { 0.0F };
115 std::optional<glm::vec3> color;
116 std::optional<float> size;
117 std::string influence_fn_name;
119 WiringRecord wiring;
120 std::vector<AudioSinkRecord> audio_sinks;
121 std::vector<RenderSinkRecord> render_sinks;
122
123 static constexpr auto describe()
124 {
125 return std::make_tuple(
126 IO::member("id", &EntityRecord::id),
127 IO::member("kind", &EntityRecord::kind),
128 IO::member("position", &EntityRecord::position),
129 IO::member("intensity", &EntityRecord::intensity),
130 IO::member("radius", &EntityRecord::radius),
131 IO::member("query_radius", &EntityRecord::query_radius),
132 IO::opt_member("color", &EntityRecord::color),
133 IO::opt_member("size", &EntityRecord::size),
134 IO::member("influence_fn_name", &EntityRecord::influence_fn_name),
135 IO::member("perception_fn_name", &EntityRecord::perception_fn_name),
136 IO::member("wiring", &EntityRecord::wiring),
137 IO::member("audio_sinks", &EntityRecord::audio_sinks),
138 IO::member("render_sinks", &EntityRecord::render_sinks));
139 }
140 };
141
142 struct FabricSchema {
143 uint32_t version { 4 };
144 std::string fabric_name;
145 std::vector<EntityRecord> entities;
146 RangeSet ranges;
147
148 static constexpr auto describe()
149 {
150 return std::make_tuple(
151 IO::member("version", &FabricSchema::version),
152 IO::member("fabric_name", &FabricSchema::fabric_name),
153 IO::member("entities", &FabricSchema::entities),
154 IO::member("ranges", &FabricSchema::ranges));
155 }
156 };
157
158 // -------------------------------------------------------------------------
159 // Range helpers
160 // -------------------------------------------------------------------------
161
162 void expand_range(Range& r, float value, bool& initialized)
163 {
164 if (!initialized) {
165 r.min = r.max = value;
166 initialized = true;
167 } else {
168 r.min = std::min(r.min, value);
169 r.max = std::max(r.max, value);
170 }
171 }
172
173 float normalize(float value, const Range& r)
174 {
175 if (r.max <= r.min) {
176 return 0.0F;
177 }
178 return (value - r.min) / (r.max - r.min);
179 }
180
181 // -------------------------------------------------------------------------
182 // Wiring builder
183 // -------------------------------------------------------------------------
184
185 WiringRecord build_wiring(const Fabric& fabric, uint32_t id)
186 {
187 const Wiring* w = fabric.wiring_for(id);
188 if (!w) {
189 return { .kind = "unsupported" };
190 }
191 if (!w->move_steps().empty()) {
192 std::vector<WiringStep> steps;
193 steps.reserve(w->move_steps().size());
194 for (const auto& s : w->move_steps()) {
195 steps.push_back({ .position = s.position, .delay_seconds = s.delay_seconds });
196 }
197 WiringRecord rec { .kind = "move_to", .steps = std::move(steps) };
198 if (w->times_count() > 1) {
199 rec.times = w->times_count();
200 }
201 return rec;
202 }
203 if (w->interval().has_value()) {
204 WiringRecord rec { .kind = "every", .interval = *w->interval() };
205 if (w->duration().has_value()) {
206 rec.duration = *w->duration();
207 }
208 if (w->times_count() > 1) {
209 rec.times = w->times_count();
210 }
211 return rec;
212 }
213 return { .kind = "commit_driven" };
214 }
215
216 void fill_wiring_pixels(const Fabric& fabric, uint32_t id, float& trigger_out, float& time_out)
217 {
218 const Wiring* w = fabric.wiring_for(id);
219 trigger_out = 0.0F;
220 time_out = 0.0F;
221 if (!w)
222 return;
223 if (!w->move_steps().empty()) {
224 time_out = 1.0F;
225 } else if (w->interval().has_value()) {
226 trigger_out = 0.2F;
227 if (w->duration().has_value())
228 time_out = 0.5F;
229 }
230 }
231
232} // namespace
233
234bool StateEncoder::encode(const Fabric& fabric, const std::string& base_path)
235{
236 m_last_error.clear();
237
238 // -------------------------------------------------------------------------
239 // Collect encodable entities.
240 // -------------------------------------------------------------------------
241 struct InternalRecord {
242 uint32_t id;
244 glm::vec3 position {};
245 float intensity { 0.0F };
246 float radius { 0.0F };
247 float query_radius { 0.0F };
248 std::optional<glm::vec3> color;
249 std::optional<float> size;
250 std::string influence_fn_name;
251 std::string perception_fn_name;
252 // Layer 0
253 float entity_type_norm { 0.0F };
254 float trigger_kind { 0.0F };
255 float time_kind { 0.0F };
256 // Layer 2
257 uint32_t sink_type { 0 };
258 uint32_t first_audio_channel { 0 };
259 };
260
261 std::vector<InternalRecord> records;
262
263 for (uint32_t id : fabric.all_ids()) {
264 const auto k = fabric.kind(id);
265 switch (k) {
267 auto e = fabric.get_emitter(id);
268 if (!e || !e->position()) {
269 continue;
270 }
271 if (e->fn_name().empty()) {
273 "StateEncoder: Emitter {} has no fn_name", id);
274 }
275 auto& rec = records.emplace_back(InternalRecord {
276 .id = id,
277 .kind = k,
278 .position = *e->position(),
279 .intensity = e->intensity(),
280 .radius = e->radius(),
281 .color = e->color(),
282 .size = e->size(),
283 .influence_fn_name = e->fn_name(),
284 .entity_type_norm = 0.0F,
285 });
286 fill_wiring_pixels(fabric, id, rec.trigger_kind, rec.time_kind);
287 rec.sink_type = (e->audio_sinks().empty() ? 0U : 1U)
288 | (e->render_sinks().empty() ? 0U : 2U);
289 if (!e->audio_sinks().empty())
290 rec.first_audio_channel = e->audio_sinks().front().channel;
291 break;
292 }
294 auto s = fabric.get_sensor(id);
295 if (!s || !s->position()) {
296 continue;
297 }
298 if (s->fn_name().empty()) {
300 "StateEncoder: Sensor {} has no fn_name", id);
301 }
302 auto& rec = records.emplace_back(InternalRecord {
303 .id = id,
304 .kind = k,
305 .position = *s->position(),
306 .query_radius = s->query_radius(),
307 .perception_fn_name = s->fn_name(),
308 .entity_type_norm = 0.333F,
309 });
310 fill_wiring_pixels(fabric, id, rec.trigger_kind, rec.time_kind);
311 break;
312 }
313 case Fabric::Kind::Agent: {
314 auto a = fabric.get_agent(id);
315 if (!a || !a->position()) {
316 continue;
317 }
318 if (a->perception_fn_name().empty()) {
320 "StateEncoder: Agent {} has no perception_fn_name", id);
321 }
322 if (a->influence_fn_name().empty()) {
324 "StateEncoder: Agent {} has no influence_fn_name", id);
325 }
326 auto& rec = records.emplace_back(InternalRecord {
327 .id = id,
328 .kind = k,
329 .position = *a->position(),
330 .intensity = a->intensity(),
331 .radius = a->radius(),
332 .query_radius = a->query_radius(),
333 .color = a->color(),
334 .size = a->size(),
335 .influence_fn_name = a->influence_fn_name(),
336 .perception_fn_name = a->perception_fn_name(),
337 .entity_type_norm = 0.667F,
338 });
339 fill_wiring_pixels(fabric, id, rec.trigger_kind, rec.time_kind);
340 rec.sink_type = (a->audio_sinks().empty() ? 0U : 1U)
341 | (a->render_sinks().empty() ? 0U : 2U);
342 if (!a->audio_sinks().empty())
343 rec.first_audio_channel = a->audio_sinks().front().channel;
344 break;
345 }
346 }
347 }
348
349 if (records.empty()) {
350 m_last_error = "No entities with positions to encode";
352 return false;
353 }
354
355 // -------------------------------------------------------------------------
356 // Compute per-field ranges.
357 // -------------------------------------------------------------------------
358 RangeSet rs;
359 bool init_pos_x = false, init_pos_y = false, init_pos_z = false;
360 bool init_intensity = false, init_radius = false, init_query_radius = false;
361 bool init_color_r = false, init_color_g = false, init_color_b = false;
362 bool init_size = false;
363
364 for (const auto& rec : records) {
365 expand_range(rs.pos_x, rec.position.x, init_pos_x);
366 expand_range(rs.pos_y, rec.position.y, init_pos_y);
367 expand_range(rs.pos_z, rec.position.z, init_pos_z);
368
369 if (rec.kind == Fabric::Kind::Emitter || rec.kind == Fabric::Kind::Agent) {
370 expand_range(rs.intensity, rec.intensity, init_intensity);
371 expand_range(rs.radius, rec.radius, init_radius);
372 if (rec.color) {
373 expand_range(rs.color_r, rec.color->r, init_color_r);
374 expand_range(rs.color_g, rec.color->g, init_color_g);
375 expand_range(rs.color_b, rec.color->b, init_color_b);
376 }
377 if (rec.size) {
378 expand_range(rs.size, *rec.size, init_size);
379 }
380 }
381 if (rec.kind == Fabric::Kind::Sensor || rec.kind == Fabric::Kind::Agent) {
382 expand_range(rs.query_radius, rec.query_radius, init_query_radius);
383 }
384 }
385
386 // -------------------------------------------------------------------------
387 // Build RGBA32F pixel buffer.
388 // -------------------------------------------------------------------------
389 const auto width = static_cast<uint32_t>(records.size());
390
392 image.width = width;
393 image.height = k_exr_rows;
394 image.channels = k_channels;
396
397 std::vector<float> pixels(static_cast<size_t>(width) * k_exr_rows * k_channels, 0.0F);
398
399 for (size_t i = 0; i < records.size(); ++i) {
400 const auto& rec = records[i];
401
402 const size_t row0 = (static_cast<size_t>(0) * width + i) * k_channels;
403 pixels[row0 + 0] = normalize(rec.position.x, rs.pos_x);
404 pixels[row0 + 1] = normalize(rec.position.y, rs.pos_y);
405 pixels[row0 + 2] = normalize(rec.position.z, rs.pos_z);
406 pixels[row0 + 3] = normalize(rec.intensity, rs.intensity);
407
408 const size_t row1 = (static_cast<size_t>(1) * width + i) * k_channels;
409 if (rec.color) {
410 pixels[row1 + 0] = normalize(rec.color->r, rs.color_r);
411 pixels[row1 + 1] = normalize(rec.color->g, rs.color_g);
412 pixels[row1 + 2] = normalize(rec.color->b, rs.color_b);
413 }
414 if (rec.size) {
415 pixels[row1 + 3] = normalize(*rec.size, rs.size);
416 }
417
418 const size_t row2 = (static_cast<size_t>(2) * width + i) * k_channels;
419 pixels[row2 + 0] = normalize(rec.radius, rs.radius);
420 pixels[row2 + 1] = normalize(rec.query_radius, rs.query_radius);
421
422 const size_t row3 = (static_cast<size_t>(3) * width + i) * k_channels;
423 pixels[row3 + 0] = static_cast<float>(rec.id);
424 pixels[row3 + 1] = rec.entity_type_norm;
425 pixels[row3 + 2] = rec.trigger_kind;
426 pixels[row3 + 3] = rec.time_kind;
427
428 const size_t row4 = (static_cast<size_t>(4) * width + i) * k_channels;
429 pixels[row4 + 0] = static_cast<float>(rec.sink_type);
430 pixels[row4 + 1] = static_cast<float>(rec.first_audio_channel);
431 }
432
433 image.pixels = std::move(pixels);
434
435 // -------------------------------------------------------------------------
436 // Write EXR.
437 // -------------------------------------------------------------------------
438 const std::string exr_path = base_path + ".exr";
439 auto writer = IO::ImageWriterRegistry::instance().create_writer(exr_path);
440 if (!writer) {
441 m_last_error = "No ImageWriter registered for .exr";
443 return false;
444 }
445
446 IO::ImageWriteOptions options;
447 options.channel_names = { "R", "G", "B", "A" };
448
449 if (!writer->write(exr_path, image, options)) {
450 m_last_error = "EXR write failed: " + writer->get_last_error();
452 return false;
453 }
454
455 // -------------------------------------------------------------------------
456 // Build and write schema.
457 // -------------------------------------------------------------------------
458 const char* kind_names[] = { "emitter", "sensor", "agent" };
459
460 FabricSchema schema;
461 schema.version = 4;
462 schema.fabric_name = fabric.name();
463 schema.ranges = rs;
464 schema.entities.reserve(records.size());
465
466 for (const auto& rec : records) {
467 EntityRecord ent;
468 ent.id = rec.id;
469 ent.kind = kind_names[static_cast<int>(rec.kind)];
470 ent.position = rec.position;
471 ent.intensity = rec.intensity;
472 ent.radius = rec.radius;
473 ent.query_radius = rec.query_radius;
474 ent.color = rec.color;
475 ent.size = rec.size;
476 ent.influence_fn_name = rec.influence_fn_name;
477 ent.perception_fn_name = rec.perception_fn_name;
478 ent.wiring = build_wiring(fabric, rec.id);
479
480 if (rec.kind == Fabric::Kind::Emitter) {
481 auto e = fabric.get_emitter(rec.id);
482 for (const auto& s : e->audio_sinks())
483 ent.audio_sinks.push_back({ s.channel, s.fn_name });
484 for (const auto& s : e->render_sinks())
485 ent.render_sinks.push_back({ s.fn_name });
486 } else if (rec.kind == Fabric::Kind::Agent) {
487 auto a = fabric.get_agent(rec.id);
488 for (const auto& s : a->audio_sinks())
489 ent.audio_sinks.push_back({ s.channel, s.fn_name });
490 for (const auto& s : a->render_sinks())
491 ent.render_sinks.push_back({ s.fn_name });
492 }
493
494 schema.entities.push_back(std::move(ent));
495 }
496
497 IO::JSONSerializer ser;
498 const std::string json_path = base_path + ".json";
499 if (!ser.write(json_path, schema)) {
500 m_last_error = "Failed to write schema: " + ser.last_error();
502 return false;
503 }
504
506 "StateEncoder: wrote {} entities to {} + {}",
507 records.size(), exr_path, json_path);
508
509 return true;
510}
511
512} // namespace MayaFlux::Nexus
#define MF_INFO(comp, ctx,...)
#define MF_ERROR(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
size_t a
Range pos_x
Range color_b
RangeSet ranges
std::string perception_fn_name
IO::ImageData image
Range intensity
std::vector< EntityRecord > entities
float max
uint32_t width
Range radius
Range query_radius
Range color_r
std::optional< double > duration
std::optional< double > interval
const std::vector< float > * pixels
std::string fabric_name
Range size
WiringRecord wiring
Range color_g
double delay_seconds
glm::vec3 position
uint32_t id
std::string kind
uint32_t version
std::optional< std::vector< WiringStep > > steps
float min
Range pos_z
std::optional< glm::vec3 > color
Range pos_y
std::string influence_fn_name
std::optional< size_t > times
uint32_t channel
std::string fn_name
std::vector< RenderSinkRecord > render_sinks
std::vector< AudioSinkRecord > audio_sinks
std::unique_ptr< ImageWriter > create_writer(const std::string &filepath) const
static ImageWriterRegistry & instance()
std::shared_ptr< Sensor > get_sensor(uint32_t id) const
Get the Sensor registered under id.
Definition Fabric.cpp:137
std::vector< uint32_t > all_ids() const
List all registered entity ids in insertion order.
Definition Fabric.cpp:100
std::shared_ptr< Agent > get_agent(uint32_t id) const
Get the Agent registered under id.
Definition Fabric.cpp:148
const std::string & name() const
Assigned name, empty if the Fabric was constructed outside a Tapestry.
Definition Fabric.hpp:66
Kind kind(uint32_t id) const
Return the kind of entity registered under id.
Definition Fabric.cpp:110
std::shared_ptr< Emitter > get_emitter(uint32_t id) const
Get the Emitter registered under id.
Definition Fabric.cpp:126
Orchestrates spatial indexing and scheduling for Nexus objects.
Definition Fabric.hpp:37
bool encode(const Fabric &fabric, const std::string &base_path)
Encode the given Fabric to {base_path}.exr and {base_path}.json.
constexpr auto member(std::string_view key, T Class::*ptr)
constexpr auto opt_member(std::string_view key, std::optional< T > Class::*ptr)
@ FileIO
Filesystem I/O operations.
@ Nexus
Spatial indexing and scheduling for user-defined behaviour.
@ RGBA32F
Four channel 32-bit float.
void normalize(std::vector< double > &data, double target_peak)
Normalize single-channel data to specified peak level (in-place)
Definition Yantra.cpp:565
Raw image data loaded from file.
std::vector< std::string > channel_names
Configuration for image writing.