MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
SpatialReader.cpp
Go to the documentation of this file.
1#include "SpatialReader.hpp"
2
4
6
8
9#include <fstream>
10
11#ifdef MAYAFLUX_PLATFORM_WINDOWS
12#include <windows.h>
13#endif
14
15#include <Alembic/Abc/All.h>
16#include <Alembic/AbcCoreFactory/All.h>
17#include <Alembic/AbcCoreOgawa/All.h>
18#include <Alembic/AbcGeom/All.h>
19
20#ifdef MAYAFLUX_PLATFORM_WINDOWS
21#ifdef ERROR
22#undef ERROR
23#endif
24#endif // MAYAFLUX_PLATFORM_WINDOWS
25
26namespace MayaFlux::IO {
27
28namespace {
29
30 using namespace Alembic::AbcGeom;
32
33 enum class AttributeKind : uint8_t {
34 Float,
35 Vec2,
36 Vec3,
37 Color,
38 Normal,
39 UInt32,
40 };
41
42 struct AttributeDecl {
43 std::string name;
44 AttributeKind kind;
45 };
46
47 struct SampleStorage {
48 std::vector<glm::vec3> positions;
49 std::vector<int32_t> vertex_counts_per_curve;
50 std::vector<uint64_t> ids;
51 std::vector<glm::vec3> velocities;
52 std::vector<SpatialAttribute> attributes;
53 };
54
55 struct StreamRecord {
56 std::variant<IPoints, ICurves> object;
57 PrimitiveTopology topology { PrimitiveTopology::POINT_LIST };
58 std::unordered_map<std::string, std::string> metadata;
59 std::vector<AttributeDecl> attributes;
60 Alembic::AbcCoreAbstract::TimeSamplingPtr time_sampling;
61 size_t sample_count { 0 };
62 bool warned_curve_type { false };
63 SampleStorage storage;
64 };
65
66 std::optional<SpatialScope> from_alembic_scope(GeometryScope scope)
67 {
68 switch (scope) {
69 case kConstantScope:
71 case kUniformScope:
73 case kVaryingScope:
74 case kVertexScope:
76 default:
77 return std::nullopt;
78 }
79 }
80
81 std::optional<AttributeKind> classify_attribute(
82 const Alembic::AbcCoreAbstract::PropertyHeader& header)
83 {
84 if (IFloatGeomParam::matches(header)) {
85 return AttributeKind::Float;
86 }
87 if (IV2fGeomParam::matches(header)) {
88 return AttributeKind::Vec2;
89 }
90 if (IC3fGeomParam::matches(header)) {
91 return AttributeKind::Color;
92 }
93 if (IN3fGeomParam::matches(header)) {
94 return AttributeKind::Normal;
95 }
96 if (IV3fGeomParam::matches(header)) {
97 return AttributeKind::Vec3;
98 }
99 if (IUInt32GeomParam::matches(header)) {
100 return AttributeKind::UInt32;
101 }
102 return std::nullopt;
103 }
104
105 template <class Elem, class Src>
106 std::vector<Elem> copy_elements(const Src* src, size_t count)
107 {
108 static_assert(sizeof(Elem) == sizeof(Src));
109 const auto* typed = reinterpret_cast<const Elem*>(src);
110 return std::vector<Elem>(typed, typed + count);
111 }
112
113 template <class Elem, class ArrayPtr>
114 std::vector<Elem> copy_array(const ArrayPtr& array)
115 {
116 if (!array) {
117 return {};
118 }
119 return copy_elements<Elem>(array->get(), array->size());
120 }
121
122 template <class Param, class Elem>
123 std::optional<SpatialAttribute> read_param(
124 const ICompoundProperty& arb,
125 const AttributeDecl& decl,
126 const ISampleSelector& selector)
127 {
128 const Param param(arb, decl.name);
129 const auto sample = param.getExpandedValue(selector);
130 const auto values = sample.getVals();
131 const auto scope = from_alembic_scope(sample.getScope());
132 if (!values || !scope) {
133 return std::nullopt;
134 }
135
136 return SpatialAttribute {
137 .name = decl.name,
138 .scope = *scope,
139 .values = Kakshya::DataVariant { copy_array<Elem>(values) }
140 };
141 }
142
143 std::optional<SpatialAttribute> read_attribute(
144 const ICompoundProperty& arb,
145 const AttributeDecl& decl,
146 const ISampleSelector& selector)
147 {
148 switch (decl.kind) {
149 case AttributeKind::Float:
150 return read_param<IFloatGeomParam, float>(arb, decl, selector);
151 case AttributeKind::Vec2:
152 return read_param<IV2fGeomParam, glm::vec2>(arb, decl, selector);
153 case AttributeKind::Vec3:
154 return read_param<IV3fGeomParam, glm::vec3>(arb, decl, selector);
155 case AttributeKind::Color:
156 return read_param<IC3fGeomParam, glm::vec3>(arb, decl, selector);
157 case AttributeKind::Normal:
158 return read_param<IN3fGeomParam, glm::vec3>(arb, decl, selector);
159 case AttributeKind::UInt32:
160 return read_param<IUInt32GeomParam, uint32_t>(arb, decl, selector);
161 }
162 return std::nullopt;
163 }
164
165 std::vector<AttributeDecl> declare_attributes(
166 const ICompoundProperty& arb, const std::string& stream_name)
167 {
168 std::vector<AttributeDecl> decls;
169 if (!arb.valid()) {
170 return decls;
171 }
172
173 for (size_t i = 0; i < arb.getNumProperties(); ++i) {
174 const auto& header = arb.getPropertyHeader(i);
175 const auto kind = classify_attribute(header);
176 if (!kind) {
178 "SpatialReader: '{}': attribute '{}' has a type this reader does not read, skipped",
179 stream_name, header.getName());
180 continue;
181 }
182 decls.push_back({ .name = header.getName(), .kind = *kind });
183 }
184
185 return decls;
186 }
187
188 enum class ArchiveFormat : uint8_t {
189 Ogawa,
190 HDF5,
191 };
192
193 /**
194 * @brief Identify an archive by its leading bytes without involving
195 * Alembic.
196 *
197 * Ogawa files begin with the ASCII "Ogawa", HDF5 files with the HDF5
198 * signature. Checking first means a missing, unreadable or foreign file
199 * is rejected here with one clear message, instead of Alembic probing
200 * each core in turn and printing its own diagnostics for the ones that
201 * fail.
202 *
203 * @return The format, or nullopt if the file cannot be read or is neither.
204 */
205 std::optional<ArchiveFormat> sniff_format(const std::string& path)
206 {
207 constexpr std::string_view k_ogawa { "Ogawa" };
208 constexpr std::string_view k_hdf5 { "\x89HDF\r\n\x1a\n", 8 };
209
210 std::ifstream file(path, std::ios::binary);
211 if (!file) {
212 return std::nullopt;
213 }
214
215 std::array<char, 8> head {};
216 file.read(head.data(), static_cast<std::streamsize>(head.size()));
217 const auto count = static_cast<size_t>(file.gcount());
218
219 if (count >= k_ogawa.size() && std::string_view(head.data(), k_ogawa.size()) == k_ogawa) {
220 return ArchiveFormat::Ogawa;
221 }
222 if (count >= k_hdf5.size() && std::string_view(head.data(), k_hdf5.size()) == k_hdf5) {
223 return ArchiveFormat::HDF5;
224 }
225 return std::nullopt;
226 }
227
228 std::unordered_map<std::string, std::string> user_metadata(const MetaData& md)
229 {
230 std::unordered_map<std::string, std::string> tags;
231 for (const auto& [key, value] : md) {
232 if (key == "schema" || key == "schemaObjTitle" || key == "schemaBaseType") {
233 continue;
234 }
235 tags.emplace(key, value);
236 }
237 return tags;
238 }
239
240} // namespace
241
243 IArchive archive;
244 std::vector<std::string> names;
245 std::unordered_map<std::string, StreamRecord> records;
246
247 void index_streams();
248};
249
251{
252 names.clear();
253 records.clear();
254
255 const IObject top = archive.getTop();
256
257 for (size_t i = 0; i < top.getNumChildren(); ++i) {
258 const auto& header = top.getChildHeader(i);
259 const std::string& name = header.getName();
260
261 StreamRecord record;
262 record.metadata = user_metadata(header.getMetaData());
263
264 ICompoundProperty arb;
265
266 if (IPoints::matches(header)) {
267 IPoints points(top, name);
268 auto& schema = points.getSchema();
269 record.topology = PrimitiveTopology::POINT_LIST;
270 record.sample_count = schema.getNumSamples();
271 record.time_sampling = schema.getTimeSampling();
272 arb = schema.getArbGeomParams();
273 record.object = std::move(points);
274 } else if (ICurves::matches(header)) {
275 ICurves curves(top, name);
276 auto& schema = curves.getSchema();
277 record.topology = PrimitiveTopology::LINE_STRIP;
278 record.sample_count = schema.getNumSamples();
279 record.time_sampling = schema.getTimeSampling();
280 arb = schema.getArbGeomParams();
281 record.object = std::move(curves);
282 } else {
284 "SpatialReader: object '{}' is neither points nor curves, skipped", name);
285 continue;
286 }
287
288 record.attributes = declare_attributes(arb, name);
289
290 names.push_back(name);
291 records.emplace(name, std::move(record));
292 }
293}
294
296 : m_impl(std::make_unique<Impl>())
297{
298}
299
301SpatialReader::SpatialReader(SpatialReader&&) noexcept = default;
302SpatialReader& SpatialReader::operator=(SpatialReader&&) noexcept = default;
303
304bool SpatialReader::open(const std::string& filepath)
305{
306 close();
307
308 const auto resolved = FileReader::resolve_path(filepath);
309
310 std::error_code ec;
311 if (!std::filesystem::is_regular_file(resolved, ec)) {
312 set_error("open: no such file: " + resolved);
314 "SpatialReader::open: '{}' does not exist", resolved);
315 return false;
316 }
317
318 const auto format = sniff_format(resolved);
319 if (!format) {
320 set_error("open: not an Alembic archive: " + resolved);
322 "SpatialReader::open: '{}' is not an Alembic archive", resolved);
323 return false;
324 }
325
326 try {
327 IArchive archive = *format == ArchiveFormat::Ogawa
328 ? IArchive(Alembic::AbcCoreOgawa::ReadArchive(), resolved)
329 : Alembic::AbcCoreFactory::IFactory().getArchive(resolved);
330 if (!archive.valid()) {
331 set_error("open: archive could not be read: " + resolved);
333 "SpatialReader::open: '{}' could not be read as an archive", resolved);
334 return false;
335 }
336
337 m_impl->archive = archive;
338 m_impl->index_streams();
339 } catch (const std::exception& e) {
340 close();
341 set_error(std::string("open: ") + e.what());
343 "SpatialReader::open: failed for '{}': {}", resolved, e.what());
344 return false;
345 }
346
348 "SpatialReader: opened '{}', {} stream(s)", resolved, m_impl->names.size());
349 return true;
350}
351
353{
354 m_impl->records.clear();
355 m_impl->names.clear();
356 m_impl->archive = IArchive();
357}
358
360{
361 return m_impl->archive.valid();
362}
363
364const std::vector<std::string>& SpatialReader::get_stream_names() const
365{
366 return m_impl->names;
367}
368
369std::optional<PrimitiveTopology> SpatialReader::get_topology(const std::string& stream_name) const
370{
371 const auto it = m_impl->records.find(stream_name);
372 if (it == m_impl->records.end()) {
373 return std::nullopt;
374 }
375 return it->second.topology;
376}
377
378size_t SpatialReader::get_sample_count(const std::string& stream_name) const
379{
380 const auto it = m_impl->records.find(stream_name);
381 return it == m_impl->records.end() ? 0 : it->second.sample_count;
382}
383
384std::unordered_map<std::string, std::string> SpatialReader::get_metadata(
385 const std::string& stream_name) const
386{
387 const auto it = m_impl->records.find(stream_name);
388 if (it == m_impl->records.end()) {
389 return {};
390 }
391 return it->second.metadata;
392}
393
394std::optional<SpatialSample> SpatialReader::read_sample(
395 const std::string& stream_name, size_t index) const
396{
397 if (!m_impl->archive.valid()) {
398 set_error("read_sample: no archive open");
399 return std::nullopt;
400 }
401
402 const auto it = m_impl->records.find(stream_name);
403 if (it == m_impl->records.end()) {
404 set_error("read_sample: no stream named '" + stream_name + "'");
405 return std::nullopt;
406 }
407
408 auto& record = it->second;
409 if (index >= record.sample_count) {
410 set_error("read_sample: index " + std::to_string(index) + " out of range for '"
411 + stream_name + "' (" + std::to_string(record.sample_count) + " samples)");
412 return std::nullopt;
413 }
414
415 try {
416 const ISampleSelector selector(static_cast<index_t>(index));
417 SampleStorage fresh;
418 ICompoundProperty arb;
419
420 if (auto* points = std::get_if<IPoints>(&record.object)) {
421 auto& schema = points->getSchema();
422 IPointsSchema::Sample sample;
423 schema.get(sample, selector);
424
425 fresh.positions = copy_array<glm::vec3>(sample.getPositions());
426 fresh.ids = copy_array<uint64_t>(sample.getIds());
427 fresh.velocities = copy_array<glm::vec3>(sample.getVelocities());
428 arb = schema.getArbGeomParams();
429 } else if (auto* curves = std::get_if<ICurves>(&record.object)) {
430 auto& schema = curves->getSchema();
431 ICurvesSchema::Sample sample;
432 schema.get(sample, selector);
433
434 fresh.positions = copy_array<glm::vec3>(sample.getPositions());
435 fresh.vertex_counts_per_curve = copy_array<int32_t>(sample.getCurvesNumVertices());
436 arb = schema.getArbGeomParams();
437
438 if (!record.warned_curve_type
439 && (sample.getType() != kLinear || sample.getWrap() != kNonPeriodic)) {
440 record.warned_curve_type = true;
442 "SpatialReader: '{}' is not linear and non periodic, its control points are "
443 "returned as plain polylines",
444 stream_name);
445 }
446 }
447
448 if (arb.valid()) {
449 for (const auto& decl : record.attributes) {
450 auto attribute = read_attribute(arb, decl, selector);
451 if (!attribute) {
453 "SpatialReader: '{}' sample {}: attribute '{}' has an unsupported scope or "
454 "no values, skipped",
455 stream_name, index, decl.name);
456 continue;
457 }
458 fresh.attributes.push_back(std::move(*attribute));
459 }
460 }
461
462 record.storage = std::move(fresh);
463 return SpatialSample {
464 .topology = record.topology,
465 .positions = record.storage.positions,
466 .vertex_counts_per_curve = record.storage.vertex_counts_per_curve,
467 .ids = record.storage.ids,
468 .velocities = record.storage.velocities,
469 .attributes = record.storage.attributes,
470 };
471 } catch (const std::exception& e) {
472 set_error(std::string("read_sample: ") + e.what());
474 "SpatialReader::read_sample: '{}' sample {}: {}", stream_name, index, e.what());
475 return std::nullopt;
476 }
477}
478
479std::optional<double> SpatialReader::sample_time(
480 const std::string& stream_name, size_t index) const
481{
482 const auto it = m_impl->records.find(stream_name);
483 if (it == m_impl->records.end()
484 || index >= it->second.sample_count
485 || !it->second.time_sampling) {
486 return std::nullopt;
487 }
488
489 return static_cast<double>(
490 it->second.time_sampling->getSampleTime(static_cast<index_t>(index)));
491}
492
493} // namespace MayaFlux::IO
#define MF_INFO(comp, ctx,...)
#define MF_ERROR(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
bool format
std::vector< glm::vec2 > * points
const uint32_t * src
OPoints object
SampleStorage storage
std::vector< int32_t > vertex_counts_per_curve
std::vector< uint64_t > ids
std::unordered_map< std::string, std::string > metadata
bool warned_curve_type
std::vector< glm::vec3 > positions
size_t sample_count
AttributeKind kind
std::vector< SpatialAttribute > attributes
PrimitiveTopology topology
Alembic::AbcCoreAbstract::TimeSamplingPtr time_sampling
std::vector< glm::vec3 > velocities
glm::vec3 value
std::vector< std::byte > values
Definition VDBWriter.cpp:26
std::string name
Definition VKDevice.cpp:123
uint32_t index
Definition VKDevice.cpp:122
size_t count
static std::string resolve_path(const std::string &filepath)
Resolve a filepath against the project source root if not found as-is.
std::optional< SpatialSample > read_sample(const std::string &stream_name, size_t index) const
Read one sample of a stream.
size_t get_sample_count(const std::string &stream_name) const
Number of samples in a stream, 0 for an unknown stream.
void set_error(std::string msg) const
void close()
Release the archive.
std::unique_ptr< Impl > m_impl
std::optional< Portal::Graphics::PrimitiveTopology > get_topology(const std::string &stream_name) const
Topology a stream is read as: POINT_LIST or LINE_STRIP.
const std::vector< std::string > & get_stream_names() const
Names of the streams found at open(), in file order.
std::unordered_map< std::string, std::string > get_metadata(const std::string &stream_name) const
Object level tags a stream was written with, the same form SpatialCache::write_metadata takes.
std::optional< double > sample_time(const std::string &stream_name, size_t index) const
Time in seconds the archive assigns to a sample.
Reads back the time-sampled streams a SpatialCache wrote, in the same SpatialSample format it writes.
@ FileIO
Filesystem I/O operations.
@ IO
Networking, file handling, streaming.
std::variant< std::vector< double >, std::vector< float >, std::vector< uint8_t >, std::vector< uint16_t >, std::vector< uint32_t >, std::vector< std::complex< float > >, std::vector< std::complex< double > >, std::vector< glm::vec2 >, std::vector< glm::vec3 >, std::vector< glm::vec4 >, std::vector< glm::mat4 > > DataVariant
Multi-type data storage for different precision needs.
Definition NDData.hpp:70
PrimitiveTopology
Vertex assembly primitive topology.
std::unordered_map< std::string, StreamRecord > records
std::vector< std::string > names
Portal::Graphics::PrimitiveTopology topology
One sample's worth of data for one named stream.