MayaFlux 0.5.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
SpatialCache.cpp
Go to the documentation of this file.
1#include "SpatialCache.hpp"
2
3#include "FileWriter.hpp"
4
6
8
9#ifdef MAYAFLUX_PLATFORM_WINDOWS
10#ifndef WIN32_LEAN_AND_MEAN
11#define WIN32_LEAN_AND_MEAN
12#endif
13#ifndef NOMINMAX
14#define NOMINMAX
15#endif
16#endif // MAYAFLUX_PLATFORM_WINDOWS
17
18#include <Alembic/Abc/All.h>
19#include <Alembic/AbcCoreOgawa/All.h>
20#include <Alembic/AbcGeom/All.h>
21
22#ifdef MAYAFLUX_PLATFORM_WINDOWS
23#ifdef ERROR
24#undef ERROR
25#endif // ERROR
26#endif // MAYAFLUX_PLATFORM_WINDOWS
27
28namespace MayaFlux::IO {
29
30namespace {
31
32 using namespace Alembic::AbcGeom;
34
35 GeometryScope to_alembic_scope(SpatialScope scope)
36 {
37 switch (scope) {
39 return kConstantScope;
41 return kUniformScope;
43 return kVaryingScope;
44 }
45 return kConstantScope;
46 }
47
48 /**
49 * @brief True for a topology written through Alembic's Curves schema
50 * (grouped into curves via vertex_counts_per_curve). The
51 * remaining supported case, PrimitiveTopology::POINT_LIST, goes
52 * through the flat Points schema instead; every other topology
53 * is rejected by write().
54 */
55 bool uses_curve_schema(PrimitiveTopology topology)
56 {
57 return topology == PrimitiveTopology::LINE_LIST
58 || topology == PrimitiveTopology::LINE_STRIP;
59 }
60
61 using GeomParamVariant = std::variant<
62 OFloatGeomParam, OV2fGeomParam, OV3fGeomParam, OC3fGeomParam, ON3fGeomParam, OUInt32GeomParam>;
63
64 struct VertexStream {
65 OPoints object;
66 std::unordered_map<std::string, GeomParamVariant> geom_params;
67 };
68
69 struct CurvesStream {
70 OCurves object;
71 std::unordered_map<std::string, GeomParamVariant> geom_params;
72 };
73
74 /**
75 * @brief Find or declare @p attr's OGeomParam, then set this sample's values.
76 *
77 * A vec3 attribute named "color" or "normal" is declared through
78 * Alembic's dedicated OC3fGeomParam/ON3fGeomParam rather than a plain
79 * OV3fGeomParam: those are MayaFlux's own fixed vertex field names
80 * (PointVertex/LineVertex/MeshVertex all use them for these exact
81 * roles), and the dedicated type, not the value's shape (three floats
82 * either way), is what tells an importer this is actually color or
83 * normal data rather than an arbitrary vector. Any other vec3 name
84 * (e.g. "tangent") stays a generic vector, matching what Alembic
85 * itself offers no dedicated type for.
86 *
87 * @return False if @p attr.values holds a DataVariant alternative with no
88 * Alembic GeomParam counterpart, its size doesn't match
89 * @p expected_count, or attr.name was already declared with a
90 * different type.
91 */
92 bool set_attribute(
93 std::unordered_map<std::string, GeomParamVariant>& params,
94 const OCompoundProperty& arb_params,
95 const SpatialAttribute& attr,
96 size_t expected_count)
97 {
98 const auto scope = to_alembic_scope(attr.scope);
99 auto it = params.find(attr.name);
100
101 if (const auto* floats = std::get_if<std::vector<float>>(&attr.values)) {
102 if (floats->size() != expected_count) {
103 return false;
104 }
105 if (it == params.end()) {
106 it = params.emplace(attr.name, OFloatGeomParam(arb_params, attr.name, false, scope, 1)).first;
107 }
108 auto* param = std::get_if<OFloatGeomParam>(&it->second);
109 if (!param) {
110 return false;
111 }
112 param->set(OFloatGeomParam::Sample(FloatArraySample(floats->data(), floats->size()), scope));
113 return true;
114 }
115 if (const auto* uvs = std::get_if<std::vector<glm::vec2>>(&attr.values)) {
116 if (uvs->size() != expected_count) {
117 return false;
118 }
119 if (it == params.end()) {
120 it = params.emplace(attr.name, OV2fGeomParam(arb_params, attr.name, false, scope, 1)).first;
121 }
122 auto* param = std::get_if<OV2fGeomParam>(&it->second);
123 if (!param) {
124 return false;
125 }
126 param->set(OV2fGeomParam::Sample(
127 V2fArraySample(reinterpret_cast<const V2f*>(uvs->data()), uvs->size()), scope));
128 return true;
129 }
130 if (const auto* vecs = std::get_if<std::vector<glm::vec3>>(&attr.values)) {
131 if (vecs->size() != expected_count) {
132 return false;
133 }
134
135 if (it == params.end()) {
136 if (attr.name == "color") {
137 it = params.emplace(attr.name, OC3fGeomParam(arb_params, attr.name, false, scope, 1)).first;
138 } else if (attr.name == "normal") {
139 it = params.emplace(attr.name, ON3fGeomParam(arb_params, attr.name, false, scope, 1)).first;
140 } else {
141 it = params.emplace(attr.name, OV3fGeomParam(arb_params, attr.name, false, scope, 1)).first;
142 }
143 }
144
145 if (auto* color_param = std::get_if<OC3fGeomParam>(&it->second)) {
146 color_param->set(OC3fGeomParam::Sample(
147 C3fArraySample(reinterpret_cast<const C3f*>(vecs->data()), vecs->size()), scope));
148 return true;
149 }
150 if (auto* normal_param = std::get_if<ON3fGeomParam>(&it->second)) {
151 normal_param->set(ON3fGeomParam::Sample(
152 N3fArraySample(reinterpret_cast<const N3f*>(vecs->data()), vecs->size()), scope));
153 return true;
154 }
155 if (auto* vector_param = std::get_if<OV3fGeomParam>(&it->second)) {
156 vector_param->set(OV3fGeomParam::Sample(
157 V3fArraySample(reinterpret_cast<const V3f*>(vecs->data()), vecs->size()), scope));
158 return true;
159 }
160 return false;
161 }
162 if (const auto* uints = std::get_if<std::vector<uint32_t>>(&attr.values)) {
163 if (uints->size() != expected_count) {
164 return false;
165 }
166 if (it == params.end()) {
167 it = params.emplace(attr.name, OUInt32GeomParam(arb_params, attr.name, false, scope, 1)).first;
168 }
169 auto* param = std::get_if<OUInt32GeomParam>(&it->second);
170 if (!param) {
171 return false;
172 }
173 param->set(OUInt32GeomParam::Sample(UInt32ArraySample(uints->data(), uints->size()), scope));
174 return true;
175 }
176 return false;
177 }
178
179 size_t vertex_attribute_count(SpatialScope scope, size_t point_count)
180 {
181 return scope == SpatialScope::Constant || scope == SpatialScope::Uniform
182 ? 1
183 : point_count;
184 }
185
186 size_t curves_attribute_count(SpatialScope scope, size_t vertex_count, size_t curve_count)
187 {
188 switch (scope) {
190 return 1;
192 return curve_count;
194 return vertex_count;
195 }
196 return 1;
197 }
198
199} // namespace
200
202 std::optional<OArchive> archive;
203 std::unordered_map<std::string, std::unique_ptr<VertexStream>> vertex_streams;
204 std::unordered_map<std::string, std::unique_ptr<CurvesStream>> curves_streams;
205 std::unordered_map<std::string, MetaData> pending_metadata;
206
207 /**
208 * @brief Guards the members above: write() and close() can run on
209 * different threads. In Impl rather than on SpatialCache
210 * directly so its defaulted move constructor still works.
211 */
212 std::mutex mutex;
213};
214
216 : m_impl(std::make_unique<Impl>())
217{
218}
219
221SpatialCache::SpatialCache(SpatialCache&&) noexcept = default;
222SpatialCache& SpatialCache::operator=(SpatialCache&&) noexcept = default;
223
224bool SpatialCache::open(const std::string& filepath)
225{
226 const auto resolved = resolve_write_path(filepath);
227
228 try {
229 m_impl->archive.emplace(Alembic::AbcCoreOgawa::WriteArchive(), resolved);
230 } catch (const std::exception& e) {
231 set_error(std::string("open: ") + e.what());
233 "SpatialCache::open: failed for '{}': {}", resolved, e.what());
234 return false;
235 }
236
238 "SpatialCache: opened '{}'", resolved);
239 return true;
240}
241
242bool SpatialCache::write_vertex_sample(const std::string& stream_name, const SpatialSample& sample)
243{
244 auto it = m_impl->vertex_streams.find(stream_name);
245 if (it == m_impl->vertex_streams.end()) {
246 MetaData md;
247 auto mit = m_impl->pending_metadata.find(stream_name);
248 if (mit != m_impl->pending_metadata.end()) {
249 md = mit->second;
250 m_impl->pending_metadata.erase(mit);
251 }
252 auto stream = std::make_unique<VertexStream>();
253 stream->object = OPoints(m_impl->archive->getTop(), stream_name, md);
254 it = m_impl->vertex_streams.emplace(stream_name, std::move(stream)).first;
255 }
256 auto& stream = *it->second;
257 auto& schema = stream.object.getSchema();
258
259 const P3fArraySample pos_sample(
260 reinterpret_cast<const V3f*>(sample.positions.data()), sample.positions.size());
261 const V3fArraySample vel_sample = sample.velocities.empty()
262 ? V3fArraySample()
263 : V3fArraySample(reinterpret_cast<const V3f*>(sample.velocities.data()), sample.velocities.size());
264
265 if (sample.ids.empty()) {
266 schema.set(OPointsSchema::Sample(pos_sample, vel_sample));
267 } else {
268 const UInt64ArraySample id_sample(sample.ids.data(), sample.ids.size());
269 schema.set(OPointsSchema::Sample(pos_sample, id_sample, vel_sample));
270 }
271
272 for (const auto& attr : sample.attributes) {
273 const size_t expected = vertex_attribute_count(attr.scope, sample.positions.size());
274 if (!set_attribute(stream.geom_params, schema.getArbGeomParams(), attr, expected)) {
275 set_error("write: attribute '" + attr.name + "' has an unsupported type, wrong "
276 + "element count, or was redeclared with a different type");
278 "SpatialCache::write: '{}': {}", stream_name, get_last_error());
279 return false;
280 }
281 }
282
283 return true;
284}
285
286bool SpatialCache::write_curves_sample(const std::string& stream_name, const SpatialSample& sample)
287{
288 const size_t expected_vertex_count = std::accumulate(
289 sample.vertex_counts_per_curve.begin(), sample.vertex_counts_per_curve.end(), size_t { 0 },
290 [](size_t acc, int32_t n) { return acc + static_cast<size_t>(n); });
291 if (expected_vertex_count != sample.positions.size()) {
292 set_error("write: vertex_counts_per_curve sum does not match positions.size()");
294 "SpatialCache::write: '{}': {}", stream_name, get_last_error());
295 return false;
296 }
297
298 auto it = m_impl->curves_streams.find(stream_name);
299 if (it == m_impl->curves_streams.end()) {
300 MetaData md;
301 auto mit = m_impl->pending_metadata.find(stream_name);
302 if (mit != m_impl->pending_metadata.end()) {
303 md = mit->second;
304 m_impl->pending_metadata.erase(mit);
305 }
306 auto stream = std::make_unique<CurvesStream>();
307 stream->object = OCurves(m_impl->archive->getTop(), stream_name, md);
308 it = m_impl->curves_streams.emplace(stream_name, std::move(stream)).first;
309 }
310 auto& stream = *it->second;
311 auto& schema = stream.object.getSchema();
312
313 const P3fArraySample pos_sample(
314 reinterpret_cast<const V3f*>(sample.positions.data()), sample.positions.size());
315 const Int32ArraySample nverts_sample(
316 sample.vertex_counts_per_curve.data(), sample.vertex_counts_per_curve.size());
317
318 schema.set(OCurvesSchema::Sample(pos_sample, nverts_sample, kLinear, kNonPeriodic));
319
320 for (const auto& attr : sample.attributes) {
321 const size_t expected = curves_attribute_count(
322 attr.scope, sample.positions.size(), sample.vertex_counts_per_curve.size());
323 if (!set_attribute(stream.geom_params, schema.getArbGeomParams(), attr, expected)) {
324 set_error("write: attribute '" + attr.name + "' has an unsupported type, wrong "
325 + "element count, or was redeclared with a different type");
327 "SpatialCache::write: '{}': {}", stream_name, get_last_error());
328 return false;
329 }
330 }
331
332 return true;
333}
334
335bool SpatialCache::write(const std::string& stream_name, const SpatialSample& sample)
336{
337 std::lock_guard lock(m_impl->mutex);
338
339 if (!m_impl->archive) {
340 set_error("write: archive not open");
341 return false;
342 }
343 if (sample.positions.empty()) {
344 set_error("write: no positions");
345 return false;
346 }
347
348 if (uses_curve_schema(sample.topology)) {
349 if (!sample.ids.empty() || !sample.velocities.empty()) {
350 set_error("write: ids/velocities must be empty for a curve topology");
351 return false;
352 }
353 if (sample.vertex_counts_per_curve.empty()) {
354 set_error("write: vertex_counts_per_curve is required for a curve topology");
355 return false;
356 }
357 return write_curves_sample(stream_name, sample);
358 }
359
360 if (sample.topology == PrimitiveTopology::POINT_LIST) {
361 if (!sample.vertex_counts_per_curve.empty()) {
362 set_error("write: vertex_counts_per_curve must be empty for POINT_LIST");
363 return false;
364 }
365 return write_vertex_sample(stream_name, sample);
366 }
367
368 set_error("write: unsupported topology; SpatialCache only writes "
369 "POINT_LIST, LINE_LIST, LINE_STRIP");
371 return false;
372}
373
375 const std::string& stream_name,
376 const std::unordered_map<std::string, std::string>& tags)
377{
378 std::lock_guard lock(m_impl->mutex);
379
380 if (m_impl->vertex_streams.contains(stream_name) || m_impl->curves_streams.contains(stream_name)) {
381 set_error("write_metadata: stream '" + stream_name
382 + "' already created; call write_metadata before its first write() call");
384 return false;
385 }
386
387 auto& md = m_impl->pending_metadata[stream_name];
388 for (const auto& [key, value] : tags) {
389 md.set(key, value);
390 }
391 return true;
392}
393
395{
396 std::lock_guard lock(m_impl->mutex);
397
398 if (!m_impl->archive) {
399 return;
400 }
401
402 m_impl->vertex_streams.clear();
403 m_impl->curves_streams.clear();
404 m_impl->pending_metadata.clear();
405 m_impl->archive.reset();
406
408}
409
410} // namespace MayaFlux::IO
#define MF_INFO(comp, ctx,...)
#define MF_ERROR(comp, ctx,...)
Core::GlobalStreamInfo stream
Definition Config.cpp:36
OPoints object
std::unordered_map< std::string, GeomParamVariant > geom_params
glm::vec3 value
std::unique_ptr< Impl > m_impl
bool write_metadata(const std::string &stream_name, const std::unordered_map< std::string, std::string > &tags)
Queue object-level metadata tags for a named stream.
bool write_curves_sample(const std::string &stream_name, const SpatialSample &sample)
bool write_vertex_sample(const std::string &stream_name, const SpatialSample &sample)
std::string get_last_error() const
void set_error(std::string msg) const
bool write(const std::string &stream_name, const SpatialSample &sample)
Append one sample to a named stream.
void close()
Finalize and close the archive.
Alembic-backed writer for time-sampled spatial entity state: particle systems, point clouds,...
SpatialScope
How many elements one attribute contributes per sample.
std::string resolve_write_path(const std::string &filepath)
Anchor a relative output path to Config::SOURCE_DIR.
@ FileIO
Filesystem I/O operations.
@ IO
Networking, file handling, streaming.
PrimitiveTopology
Vertex assembly primitive topology.
std::unordered_map< std::string, MetaData > pending_metadata
std::unordered_map< std::string, std::unique_ptr< CurvesStream > > curves_streams
std::mutex mutex
Guards the members above: write() and close() can run on different threads.
std::optional< OArchive > archive
std::unordered_map< std::string, std::unique_ptr< VertexStream > > vertex_streams
std::span< const glm::vec3 > positions
std::span< const glm::vec3 > velocities
std::span< const SpatialAttribute > attributes
Portal::Graphics::PrimitiveTopology topology
std::span< const int32_t > vertex_counts_per_curve
std::span< const uint64_t > ids
One sample's worth of data for one named stream.