MayaFlux 0.5.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
VolumeReader.cpp
Go to the documentation of this file.
1#include "VolumeReader.hpp"
2
3#include "VDBArchive.hpp"
4
6
7namespace MayaFlux::IO {
8
9namespace {
10
11 std::string extension_of(const std::string& filepath)
12 {
13 auto ext = std::filesystem::path(filepath).extension().string();
14 if (!ext.empty() && ext[0] == '.') {
15 ext = ext.substr(1);
16 }
17 std::ranges::transform(ext, ext.begin(),
18 [](unsigned char c) { return std::tolower(c); });
19 return ext;
20 }
21
22 /**
23 * @brief Resolve field_names to grid indices, in file order when empty
24 * or in requested order otherwise.
25 *
26 * A requested name with no matching grid is logged and dropped; the
27 * rest of the selection proceeds, matching how a name that is present
28 * in field_names but absent from the data is handled elsewhere in IO.
29 */
30 std::vector<size_t> select_indices(
31 const Detail::VDBArchive& archive,
32 size_t count,
33 const std::vector<std::string>& field_names)
34 {
35 std::vector<size_t> indices;
36
37 if (field_names.empty()) {
38 indices.reserve(count);
39 for (size_t i = 0; i < count; ++i) {
40 indices.push_back(i);
41 }
42 return indices;
43 }
44
45 indices.reserve(field_names.size());
46 for (const auto& name : field_names) {
47 bool found = false;
48 for (size_t i = 0; i < count; ++i) {
49 if (archive.grid_summary(i).name == name) {
50 indices.push_back(i);
51 found = true;
52 break;
53 }
54 }
55 if (!found) {
57 "VolumeReader: no grid named '{}', skipped", name);
58 }
59 }
60 return indices;
61 }
62
63 /**
64 * @brief Inverse of VDBWriter's class_token: metadata string to enum.
65 */
66 Kinesis::LatticeValueClass class_from_token(std::string_view token)
67 {
69 if (token == "level set") {
70 return C::LevelSet;
71 }
72 if (token == "fog volume") {
73 return C::FogVolume;
74 }
75 if (token == "staggered") {
76 return C::Staggered;
77 }
78 return C::Unknown;
79 }
80
81 /**
82 * @brief Inverse of VDBWriter's variance_token: metadata string to enum.
83 */
84 Kinesis::VectorVariance variance_from_token(std::string_view token)
85 {
87 if (token == "covariant") {
88 return V::Covariant;
89 }
90 if (token == "covariant normalize") {
91 return V::CovariantNormalize;
92 }
93 if (token == "contravariant relative") {
94 return V::ContravariantRelative;
95 }
96 if (token == "contravariant absolute") {
97 return V::ContravariantAbsolute;
98 }
99 return V::Invariant;
100 }
101
102} // namespace
103
104// =============================================================================
105// Construction
106// =============================================================================
107
109 : m_archive(std::make_unique<Detail::VDBArchive>())
110{
111}
112
117
118// =============================================================================
119// Primary API
120// =============================================================================
121
122std::optional<Kakshya::VolumeData> VolumeReader::load(
123 const std::string& filepath, const VolumeReadOptions& options)
124{
125 if (!open(filepath)) {
126 return std::nullopt;
127 }
128 auto result = extract(options);
129 close();
130 return result;
131}
132
133std::optional<Kakshya::VolumeData> VolumeReader::load(
134 const std::string& filepath,
135 const Kinesis::Lattice3D& lattice,
136 const VolumeReadOptions& options)
137{
138 if (!open(filepath)) {
139 return std::nullopt;
140 }
141 auto result = extract(lattice, options);
142 close();
143 return result;
144}
145
146std::optional<Kakshya::VolumeData> VolumeReader::extract(
147 const VolumeReadOptions& options) const
148{
149 if (!m_is_open) {
150 set_error("No file open");
151 return std::nullopt;
152 }
153
154 const size_t count = m_archive->read_grid_count();
155 if (count == 0) {
156 set_error("No grids in file");
157 return std::nullopt;
158 }
159
160 const auto indices = select_indices(*m_archive, count, options.field_names);
161 if (indices.empty()) {
162 set_error("No matching grids");
163 return std::nullopt;
164 }
165
166 glm::ivec3 union_min { 0 };
167 glm::ivec3 union_max { 0 };
168 bool have_any = false;
169
170 for (size_t idx : indices) {
171 const auto summary = m_archive->grid_summary(idx);
172 if (!summary.has_active) {
173 continue;
174 }
175 if (!have_any) {
176 union_min = summary.active_min;
177 union_max = summary.active_max;
178 have_any = true;
179 } else {
180 union_min = glm::min(union_min, summary.active_min);
181 union_max = glm::max(union_max, summary.active_max);
182 }
183 }
184
185 if (!have_any) {
186 set_error("Selected grids have no active voxels");
187 return std::nullopt;
188 }
189
190 const auto first = m_archive->grid_summary(indices.front());
191
192 Kinesis::Lattice3D lattice;
193 lattice.resolution = glm::uvec3(union_max - union_min);
194 lattice.bounds.min = first.translation + first.voxel_size * (glm::vec3(union_min) - 0.5F);
195 lattice.bounds.max = first.translation + first.voxel_size * (glm::vec3(union_max) - 0.5F);
196
197 return materialize(indices, union_min, lattice);
198}
199
200std::optional<Kakshya::VolumeData> VolumeReader::extract(
201 const Kinesis::Lattice3D& lattice, const VolumeReadOptions& options) const
202{
203 if (!m_is_open) {
204 set_error("No file open");
205 return std::nullopt;
206 }
207
208 const size_t count = m_archive->read_grid_count();
209 if (count == 0) {
210 set_error("No grids in file");
211 return std::nullopt;
212 }
213
214 const auto indices = select_indices(*m_archive, count, options.field_names);
215 if (indices.empty()) {
216 set_error("No matching grids");
217 return std::nullopt;
218 }
219
220 const auto first = m_archive->grid_summary(indices.front());
221 if (glm::any(glm::lessThanEqual(first.voxel_size, glm::vec3(0.0F)))) {
222 set_error("First selected grid has a degenerate voxel size");
223 return std::nullopt;
224 }
225
226 const glm::vec3 region_min_f = (lattice.bounds.min - first.translation) / first.voxel_size + glm::vec3(0.5F);
227 const glm::ivec3 region_min = glm::ivec3(glm::round(region_min_f));
228
229 return materialize(indices, region_min, lattice);
230}
231
232std::optional<Kakshya::VolumeData> VolumeReader::materialize(
233 const std::vector<size_t>& indices,
234 const glm::ivec3& region_min,
235 const Kinesis::Lattice3D& lattice) const
236{
237 constexpr float k_transform_epsilon = 1e-5F;
238
239 const auto reference = m_archive->grid_summary(indices.front());
240 for (size_t idx : indices) {
241 if (idx == indices.front()) {
242 continue;
243 }
244 const auto summary = m_archive->grid_summary(idx);
245 const bool mismatched = glm::any(glm::greaterThan(
246 glm::abs(summary.voxel_size - reference.voxel_size), glm::vec3(k_transform_epsilon)))
247 || glm::any(glm::greaterThan(
248 glm::abs(summary.translation - reference.translation), glm::vec3(k_transform_epsilon)));
249 if (mismatched) {
251 "VolumeReader: grid '{}' has a different voxel size or translation than '{}', "
252 "its voxel indices are read directly against the shared region and will not be aligned",
253 summary.name, reference.name);
254 }
255 }
256
257 Kakshya::VolumeData result;
258 result.lattice = lattice;
259 result.fields.reserve(indices.size());
260
261 for (size_t idx : indices) {
262 const auto summary = m_archive->grid_summary(idx);
263
265 field.name = summary.name;
266 field.semantics.value_class = class_from_token(m_archive->grid_metadata(idx, "class"));
267
268 bool precision_lost = false;
269
270 if (summary.is_vector) {
271 field.semantics.variance = variance_from_token(m_archive->grid_metadata(idx, "vector_type"));
272
273 std::vector<glm::vec3> values;
274 if (!m_archive->read_dense_vector(
275 idx, region_min, lattice.resolution, summary.background_vector, values, &precision_lost)) {
276 set_error(std::string(m_archive->last_error()));
277 return std::nullopt;
278 }
279 field.values = std::move(values);
280 } else {
281 std::vector<float> values;
282 if (!m_archive->read_dense_scalar(
283 idx, region_min, lattice.resolution, summary.background_scalar, values, &precision_lost)) {
284 set_error(std::string(m_archive->last_error()));
285 return std::nullopt;
286 }
287 field.values = std::move(values);
288 }
289
290 if (precision_lost) {
292 "VolumeReader: grid '{}' is not float/vec3f, narrowing to {} lost precision in at least one value",
293 summary.name, summary.is_vector ? "vec3" : "float");
294 } else if (summary.narrowed) {
296 "VolumeReader: grid '{}' is not float/vec3f, narrowing to {} losslessly",
297 summary.name, summary.is_vector ? "vec3" : "float");
298 }
299
301 "VolumeReader: materialized '{}', {} cells", field.name, field.element_count());
302
303 result.fields.push_back(std::move(field));
304 }
305
306 if (result.fields.empty()) {
307 set_error("No fields materialized");
308 return std::nullopt;
309 }
310
311 if (!result.is_consistent()) {
312 set_error("Resulting VolumeData failed is_consistent()");
313 return std::nullopt;
314 }
315
316 return result;
317}
318
319// =============================================================================
320// FileReader interface
321// =============================================================================
322
323bool VolumeReader::can_read(const std::string& filepath) const
324{
325 return extension_of(filepath) == "vdb";
326}
327
328bool VolumeReader::open(const std::string& filepath, FileReadOptions /*options*/)
329{
330 close();
331
332 if (!can_read(filepath)) {
333 set_error("Unsupported volume format: " + filepath);
335 "VolumeReader: {}", m_last_error);
336 return false;
337 }
338
339 const auto resolved = resolve_path(filepath);
340
341 if (!m_archive->open(resolved)) {
342 set_error(std::string(m_archive->last_error()));
344 "VolumeReader: open failed for '{}' — {}", filepath, m_last_error);
345 return false;
346 }
347
348 m_filepath = filepath;
349 m_is_open = true;
350
352 "VolumeReader: opened '{}' — {} grid(s)",
353 std::filesystem::path(resolved).filename().string(),
354 m_archive->read_grid_count());
355
356 return true;
357}
358
360{
361 if (m_is_open) {
362 m_archive = std::make_unique<Detail::VDBArchive>();
363 m_filepath.clear();
364 m_is_open = false;
365 }
366}
367
368std::optional<FileMetadata> VolumeReader::get_metadata() const
369{
370 if (!m_is_open) {
371 return std::nullopt;
372 }
373
374 FileMetadata meta;
375 meta.format = "vdb";
376 meta.attributes["grid_count"] = static_cast<uint64_t>(m_archive->read_grid_count());
377
378 return meta;
379}
380
381std::shared_ptr<Kakshya::SignalSourceContainer> VolumeReader::create_container()
382{
383 m_last_error = "Volume data does not use SignalSourceContainer. Use load() instead.";
384 return nullptr;
385}
386
388 std::shared_ptr<Kakshya::SignalSourceContainer> /*container*/)
389{
390 m_last_error = "Volume data does not use SignalSourceContainer. Use load() instead.";
391 return false;
392}
393
394} // namespace MayaFlux::IO
#define MF_INFO(comp, ctx,...)
#define MF_ERROR(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
#define MF_DEBUG(comp, ctx,...)
bool * precision_lost
glm::ivec3 region_min
std::vector< std::byte > values
Definition VDBWriter.cpp:26
std::string name
Definition VKDevice.cpp:143
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.
bool load_into_container(std::shared_ptr< Kakshya::SignalSourceContainer > container) override
No-op.
void set_error(std::string msg) const
std::optional< Kakshya::VolumeData > materialize(const std::vector< size_t > &indices, const glm::ivec3 &region_min, const Kinesis::Lattice3D &lattice) const
Build VolumeData from selected grids over an explicit region.
std::unique_ptr< Detail::VDBArchive > m_archive
bool can_read(const std::string &filepath) const override
Check if a file can be read by this reader.
bool open(const std::string &filepath, FileReadOptions options=FileReadOptions::ALL) override
Open a file for reading.
std::optional< FileMetadata > get_metadata() const override
Get metadata from the open file.
std::optional< Kakshya::VolumeData > load(const std::string &filepath, const VolumeReadOptions &options={})
Load every selected grid from a file in one call.
void close() override
Close the currently open file.
std::optional< Kakshya::VolumeData > extract(const VolumeReadOptions &options={}) const
Extract every selected grid after open() has already been called.
std::shared_ptr< Kakshya::SignalSourceContainer > create_container() override
No-op.
FileReadOptions
Generic options for file reading behavior.
@ FileIO
Filesystem I/O operations.
@ IO
Networking, file handling, streaming.
LatticeValueClass
What the values sampled over a lattice mean geometrically.
VectorVariance
How a vector quantity's components behave under a change of frame.
std::unordered_map< std::string, std::any > attributes
Type-specific metadata stored as key-value pairs (e.g., sample rate, channels)
std::string format
File format identifier (e.g., "wav", "mp3", "hdf5")
Generic metadata structure for any file type.
std::vector< std::string > field_names
Grids to load, matched by name.
Configuration for volume reading.
bool is_consistent() const
Check that every field is densely populated over the lattice.
std::vector< VolumeField > fields
A lattice and every field sampled over it, held in host memory.
Kinesis::LatticeSemantics semantics
size_t element_count() const
Number of cells represented, dispatched on variant.
One named quantity sampled over a lattice, held in host memory.
AABB3D bounds
Continuous extent subdivided.
Definition Lattice.hpp:27
glm::uvec3 resolution
Cell count per axis. Zero on any axis is invalid.
Definition Lattice.hpp:26
A regular subdivision of an AABB3D into a cell count per axis.
Definition Lattice.hpp:25
VectorVariance variance
Vector quantities only.
LatticeValueClass value_class
Geometric meaning of the values.