MayaFlux 0.5.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
VDBWriter.cpp
Go to the documentation of this file.
1#include "VDBWriter.hpp"
2
3#include "VDBArchive.hpp"
4
5#include "FileWriter.hpp"
6
8
9namespace MayaFlux::IO {
10
11namespace {
12 /**
13 * @brief Deflate level for the ZIP path. Ignored by the other codecs.
14 */
15 constexpr int k_deflate_level = 5;
16
17 /**
18 * @brief One field reduced to the active cells an archive grid consumes.
19 *
20 * coords and values are parallel: element i of values, read at the width
21 * is_vector implies, belongs to coords[i]. Owned here so the archive can
22 * borrow spans over both without a lifetime question.
23 */
24 struct ActiveSet {
25 std::vector<glm::ivec3> coords;
26 std::vector<std::byte> values;
27 bool is_vector { false };
28 };
29
30 /**
31 * @brief Threshold a field and emit surviving cells as coord/value pairs.
32 *
33 * Walks the lattice in its own index order and emits each coordinate
34 * alongside its value, so no index convention is shared with the archive
35 * backend at all.
36 *
37 * A scalar is compared on magnitude, a vector on length, so a velocity
38 * whose components cancel is correctly inactive.
39 */
40 ActiveSet extract_active(
41 const Kakshya::VolumeField& field,
42 const Kinesis::Lattice3D& lattice,
43 float threshold)
44 {
45 const glm::uvec3 res = lattice.resolution;
46
47 ActiveSet out;
48 out.is_vector = field.is_vector();
49
50 const size_t cells = field.element_count();
51 const size_t guess = cells / 4;
52 out.coords.reserve(guess);
53 out.values.reserve(guess * (out.is_vector ? sizeof(glm::vec3) : sizeof(float)));
54
55 const auto emit = [&](uint32_t x, uint32_t y, uint32_t z,
56 const void* value, size_t width) {
57 out.coords.emplace_back(
58 static_cast<int32_t>(x), static_cast<int32_t>(y), static_cast<int32_t>(z));
59 const auto* bytes = static_cast<const std::byte*>(value);
60 out.values.insert(out.values.end(), bytes, bytes + width);
61 };
62
63 if (const auto* scalars = field.as_scalar()) {
64 size_t i = 0;
65 for (uint32_t z = 0; z < res.z; ++z) {
66 for (uint32_t y = 0; y < res.y; ++y) {
67 for (uint32_t x = 0; x < res.x; ++x, ++i) {
68 const float v = (*scalars)[i];
69 if (std::abs(v) >= threshold) {
70 emit(x, y, z, &v, sizeof(float));
71 }
72 }
73 }
74 }
75 return out;
76 }
77
78 const auto* vectors = field.as_vector();
79 const float threshold_sq = threshold * threshold;
80
81 size_t i = 0;
82 for (uint32_t z = 0; z < res.z; ++z) {
83 for (uint32_t y = 0; y < res.y; ++y) {
84 for (uint32_t x = 0; x < res.x; ++x, ++i) {
85 const glm::vec3& v = (*vectors)[i];
86 if (glm::dot(v, v) >= threshold_sq) {
87 emit(x, y, z, &v, sizeof(glm::vec3));
88 }
89 }
90 }
91 }
92 return out;
93 }
94
95 uint32_t compression_flags(const VolumeWriteOptions& options)
96 {
97 if (options.compression < 0) {
99 }
100 return static_cast<uint32_t>(options.compression);
101 }
102
103 /**
104 * @brief Threshold for a field, honoring any per-field override.
105 */
106 float threshold_for(const std::string& name, const VolumeWriteOptions& options)
107 {
108 auto it = options.field_thresholds.find(name);
109 return it != options.field_thresholds.end() ? it->second : options.activity_threshold;
110 }
111
112 std::string extension_of(const std::string& filepath)
113 {
114 auto ext = std::filesystem::path(filepath).extension().string();
115 if (!ext.empty() && ext[0] == '.') {
116 ext = ext.substr(1);
117 }
118 std::ranges::transform(ext, ext.begin(),
119 [](unsigned char c) { return std::tolower(c); });
120 return ext;
121 }
122
123 const char* class_token(Kinesis::LatticeValueClass c)
124 {
125 switch (c) {
127 return "level set";
129 return "fog volume";
131 return "staggered";
132 default:
133 return "unknown";
134 }
135 }
136
137 const char* variance_token(Kinesis::VectorVariance v)
138 {
139 switch (v) {
141 return "covariant";
143 return "covariant normalize";
145 return "contravariant relative";
147 return "contravariant absolute";
148 default:
149 return "invariant";
150 }
151 }
152
153 /**
154 * @brief Interchange metadata for a field, beyond its name.
155 *
156 * vector_type is emitted only for vector fields: a scalar grid carrying
157 * one would be meaningless and OpenVDB ignores it.
158 */
159 std::vector<std::pair<std::string, std::string>> semantics_metadata(
160 const Kakshya::VolumeField& field)
161 {
162 std::vector<std::pair<std::string, std::string>> meta;
163 meta.reserve(2);
164
165 meta.emplace_back("class", class_token(field.semantics.value_class));
166 if (field.is_vector()) {
167 meta.emplace_back("vector_type", variance_token(field.semantics.variance));
168 }
169
170 return meta;
171 }
172
173} // namespace
174
175// ============================================================================
176// Registry hook
177// ============================================================================
178
180{
181 auto& reg = VolumeWriterRegistry::instance();
182 reg.register_writer(
183 { "vdb" },
184 []() -> std::unique_ptr<VolumeWriter> {
185 return std::make_unique<VDBWriter>();
186 });
187
189 "VDBWriter registered for: vdb");
190}
191
192bool VDBWriter::can_write(const std::string& filepath) const
193{
194 return extension_of(filepath) == "vdb";
195}
196
197std::vector<std::string> VDBWriter::get_supported_extensions() const
198{
199 return { "vdb" };
200}
201
202// ============================================================================
203// Write
204// ============================================================================
205
207 const std::string& filepath,
208 const Kakshya::VolumeData& data,
209 const VolumeWriteOptions& options)
210{
211 m_last_error.clear();
212
213 if (!data.is_consistent()) {
214 m_last_error = "VolumeData failed is_consistent()";
216 return false;
217 }
218
219 const float scalar_bg = options.background;
220 const glm::vec3 vector_bg { options.background };
221
222 Detail::VDBArchive archive;
223
224 for (const auto& field : data.fields) {
225 const ActiveSet active = extract_active(
226 field, data.lattice, threshold_for(field.name, options));
227
228 const auto meta = semantics_metadata(field);
229
230 const auto background = active.is_vector
231 ? std::as_bytes(std::span(&vector_bg, 1))
232 : std::as_bytes(std::span(&scalar_bg, 1));
233
234 const Detail::VDBGridSpec spec {
235 .name = field.name,
236 .coords = active.coords,
237 .values = active.values,
238 .background = background,
239 .is_vector = active.is_vector,
240 .metadata = meta,
241 };
242
243 if (!archive.add_grid(data.lattice, spec)) {
244 m_last_error = std::string(archive.last_error());
246 return false;
247 }
248
250 "VDBWriter: '{}' {} of {} cells active",
251 field.name, active.coords.size(), data.cell_count());
252 }
253
254 const auto resolved = resolve_write_path(filepath);
255
256 if (!archive.save(resolved, compression_flags(options), k_deflate_level)) {
257 m_last_error = std::string(archive.last_error());
259 return false;
260 }
261
263 "VDBWriter: wrote '{}', {} grids", resolved, archive.grid_count());
264 return true;
265}
266
267} // namespace MayaFlux::IO
#define MF_INFO(comp, ctx,...)
#define MF_ERROR(comp, ctx,...)
#define MF_DEBUG(comp, ctx,...)
std::vector< float > * out
glm::vec3 value
std::vector< glm::ivec3 > coords
Definition VDBWriter.cpp:25
std::vector< std::byte > values
Definition VDBWriter.cpp:26
bool is_vector
Definition VDBWriter.cpp:27
std::string name
Definition VKDevice.cpp:143
float threshold
uint32_t width
bool save(const std::string &path, uint32_t compression, int level)
Write every added grid to one file.
std::string_view last_error() const
bool add_grid(const Kinesis::Lattice3D &lattice, const VDBGridSpec &spec)
Build one grid from active cells and retain it for saving.
RAII boundary around tinyvdb's C read and write paths.
bool write(const std::string &filepath, const Kakshya::VolumeData &data, const VolumeWriteOptions &options={}) override
Write volume data to disk.
std::vector< std::string > get_supported_extensions() const override
File extensions handled by this writer (without dot).
static void register_with_registry()
Register this writer with the VolumeWriterRegistry.
bool can_write(const std::string &filepath) const override
Check whether this writer handles the given filepath.
static VolumeWriterRegistry & instance()
std::string resolve_write_path(const std::string &filepath)
Anchor a relative output path to Config::SOURCE_DIR.
@ FileIO
Filesystem I/O operations.
@ Init
Engine/subsystem initialization.
@ IO
Networking, file handling, streaming.
LatticeValueClass
What the values sampled over a lattice mean geometrically.
@ FogVolume
Density of a participating medium, zero outside it.
@ Staggered
Vector components sampled on cell faces, not centres.
@ LevelSet
Signed distance to a surface at the zero crossing.
VectorVariance
How a vector quantity's components behave under a change of frame.
@ ContravariantAbsolute
Contravariant, treated as a world-space position.
@ ContravariantRelative
Transforms by the frame itself; velocities.
@ CovariantNormalize
Inverse transpose, then renormalized.
@ Covariant
Transforms by the inverse transpose.
One grid's worth of input to VDBArchive::add_grid.
float background
Value assumed by every inactive cell.
Configuration for volume writing.
bool is_consistent() const
Check that every field is densely populated over the lattice.
size_t cell_count() const
Cells in the lattice, which every field's element_count() must equal.
std::vector< VolumeField > fields
A lattice and every field sampled over it, held in host memory.