199bool StateEncoder::encode_fabric(
201 const std::string& base_path,
202 const std::unordered_set<const Expanse*>& tapestry_owned)
204 m_last_error.clear();
209 struct InternalRecord {
213 std::optional<glm::vec4> orientation;
217 std::optional<glm::vec3>
color;
218 std::optional<float>
size;
219 std::string influence_fn_name;
220 std::string perception_fn_name;
222 float entity_type_norm { 0.0F };
223 float trigger_kind { 0.0F };
224 float time_kind { 0.0F };
226 uint32_t sink_type { 0 };
227 uint32_t first_audio_channel { 0 };
230 std::vector<InternalRecord> records;
233 std::ranges::sort(
ids);
235 for (uint32_t
id :
ids) {
236 const auto k = fabric.
kind(
id);
238 case Fabric::Kind::Emitter: {
243 if (e->fn() && e->fn_name().empty()) {
245 "StateEncoder: Emitter {} has a function but no fn_name",
id);
247 auto& rec = records.emplace_back(InternalRecord {
250 .position = e->position(),
252 .radius = e->radius(),
255 .influence_fn_name = e->fn_name(),
256 .entity_type_norm = 0.0F,
258 fill_wiring_pixels(fabric,
id, rec.trigger_kind, rec.time_kind);
259 rec.sink_type = (e->audio_sinks().empty() ? 0U : 1U)
260 | (e->render_sinks().empty() ? 0U : 2U);
261 if (!e->audio_sinks().empty())
262 rec.first_audio_channel = e->audio_sinks().front().channel;
265 case Fabric::Kind::Sensor: {
270 if (s->fn() && s->fn_name().empty()) {
272 "StateEncoder: Sensor {} has a function but no fn_name",
id);
274 auto& rec = records.emplace_back(InternalRecord {
277 .position = s->position(),
279 .perception_fn_name = s->fn_name(),
280 .entity_type_norm = 0.333F,
282 fill_wiring_pixels(fabric,
id, rec.trigger_kind, rec.time_kind);
285 case Fabric::Kind::Agent: {
290 if (
a->perception_fn() &&
a->perception_fn_name().empty()) {
292 "StateEncoder: Agent {} has a perception function but no perception_fn_name",
id);
294 if (
a->influence_fn() &&
a->influence_fn_name().empty()) {
296 "StateEncoder: Agent {} has an influence function but no influence_fn_name",
id);
298 auto& rec = records.emplace_back(InternalRecord {
301 .position =
a->position(),
303 .radius =
a->radius(),
304 .query_radius =
a->query_radius(),
307 .influence_fn_name =
a->influence_fn_name(),
308 .perception_fn_name =
a->perception_fn_name(),
309 .entity_type_norm = 0.667F,
311 if (
const auto&
q =
a->orientation()) {
312 rec.orientation = glm::vec4(
q->x,
q->y,
q->z,
q->w);
314 fill_wiring_pixels(fabric,
id, rec.trigger_kind, rec.time_kind);
315 rec.sink_type = (
a->audio_sinks().empty() ? 0U : 1U)
316 | (
a->render_sinks().empty() ? 0U : 2U);
317 if (!
a->audio_sinks().empty())
318 rec.first_audio_channel =
a->audio_sinks().front().channel;
324 if (records.empty()) {
325 m_last_error =
"No entities to encode";
334 bool init_pos_x =
false, init_pos_y =
false, init_pos_z =
false;
335 bool init_intensity =
false, init_radius =
false, init_query_radius =
false;
336 bool init_color_r =
false, init_color_g =
false, init_color_b =
false;
337 bool init_size =
false;
339 for (
const auto& rec : records) {
341 expand_range(rs.
pos_x, rec.position->x, init_pos_x);
342 expand_range(rs.
pos_y, rec.position->y, init_pos_y);
343 expand_range(rs.
pos_z, rec.position->z, init_pos_z);
346 if (rec.kind == Fabric::Kind::Emitter || rec.kind == Fabric::Kind::Agent) {
347 expand_range(rs.
intensity, rec.intensity, init_intensity);
348 expand_range(rs.
radius, rec.radius, init_radius);
350 expand_range(rs.
color_r, rec.color->r, init_color_r);
351 expand_range(rs.
color_g, rec.color->g, init_color_g);
352 expand_range(rs.
color_b, rec.color->b, init_color_b);
355 expand_range(rs.
size, *rec.size, init_size);
358 if (rec.kind == Fabric::Kind::Sensor || rec.kind == Fabric::Kind::Agent) {
359 expand_range(rs.
query_radius, rec.query_radius, init_query_radius);
366 const auto width =
static_cast<uint32_t
>(records.size());
372 image.
format = Portal::Graphics::ImageFormat::RGBA32F;
374 std::vector<float>
pixels(
static_cast<size_t>(
width) * State::k_exr_rows * State::k_channels, 0.0F);
376 for (
size_t i = 0; i < records.size(); ++i) {
377 const auto& rec = records[i];
379 const size_t row0 = (
static_cast<size_t>(0) *
width + i) * State::k_channels;
381 pixels[row0 + 0] = normalize(rec.position->x, rs.
pos_x);
382 pixels[row0 + 1] = normalize(rec.position->y, rs.
pos_y);
383 pixels[row0 + 2] = normalize(rec.position->z, rs.
pos_z);
387 const size_t row1 = (
static_cast<size_t>(1) *
width + i) * State::k_channels;
394 pixels[row1 + 3] = normalize(*rec.size, rs.
size);
397 const size_t row2 = (
static_cast<size_t>(2) *
width + i) * State::k_channels;
401 const size_t row3 = (
static_cast<size_t>(3) *
width + i) * State::k_channels;
402 pixels[row3 + 0] =
static_cast<float>(rec.id);
403 pixels[row3 + 1] = rec.entity_type_norm;
404 pixels[row3 + 2] = rec.trigger_kind;
405 pixels[row3 + 3] = rec.time_kind;
407 const size_t row4 = (
static_cast<size_t>(4) *
width + i) * State::k_channels;
408 pixels[row4 + 0] =
static_cast<float>(rec.sink_type);
409 pixels[row4 + 1] =
static_cast<float>(rec.first_audio_channel);
417 const std::string exr_path = base_path +
".exr";
420 m_last_error =
"No ImageWriter registered for .exr";
427 if (!writer->write(exr_path,
image, options)) {
428 m_last_error =
"EXR write failed: " + writer->get_last_error();
437 schema.
version = State::k_schema_version;
441 schema.
entities.reserve(records.size());
443 for (
const auto& rec : records) {
446 ent.
kind = State::kind_to_string(rec.kind);
452 ent.
color = rec.color;
456 ent.
wiring = build_wiring(fabric, rec.
id);
458 if (rec.kind == Fabric::Kind::Emitter) {
460 fill_sinks(ent, e->
audio_sinks(), e->render_sinks());
461 }
else if (rec.kind == Fabric::Kind::Agent) {
465 if (
auto locus = std::dynamic_pointer_cast<Locus>(
a)) {
467 const auto& nav = locus->nav();
470 .target = nav.eye + glm::vec3 { std::cos(nav.pitch) * std::sin(nav.yaw), std::sin(nav.pitch), std::cos(nav.pitch) * std::cos(nav.yaw) },
471 .fov = nav.fov_radians,
472 .near_plane = nav.near_plane,
473 .far_plane = nav.far_plane,
474 .speed = nav.move_speed,
475 .mouse_sensitivity = nav.mouse_sensitivity,
476 .scroll_speed = nav.scroll_speed,
477 .aspect = locus->aspect(),
479 }
else if (
auto presence = std::dynamic_pointer_cast<Presence>(
a)) {
482 ent.
falloff_radius = presence->falloff_radius() != presence->query_radius()
483 ? std::optional<float>(presence->falloff_radius())
485 if (
auto fc = presence->falloff_curve()) {
489 "StateEncoder: Presence {} has a custom falloff, which is not encoded", rec.id);
494 schema.
entities.push_back(std::move(ent));
499 if (!x || tapestry_owned.contains(x.get()))
502 fill_expanse(record, *x);
503 if (record.subkind.empty() && record.fn_name.empty() && !record.bounds_min) {
505 "StateEncoder: Expanse {} has neither fn_name nor bounds, skipping", xid);
508 schema.
expanses.push_back(std::move(record));
512 const std::string json_path = base_path +
".json";
513 if (!ser.
write(json_path, schema)) {
514 m_last_error =
"Failed to write schema: " + ser.
last_error();
520 "StateEncoder: wrote {} entities to {} + {}",
521 records.size(), exr_path, json_path);