MayaFlux 0.5.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
ModelExport.cpp
Go to the documentation of this file.
1#include "ModelExport.hpp"
2
3#include "FileWriter.hpp"
4#include "ImageExport.hpp"
5
11
14
17
19
20#include <chrono>
21
22namespace MayaFlux::IO {
23
24namespace {
25
26 bool write_via_registry(
27 const std::string& filepath,
28 const std::vector<Kakshya::MeshData>& meshes,
29 const ModelWriteOptions& options)
30 {
31 auto writer = ModelWriterRegistry::instance().create_writer(filepath);
32 if (!writer) {
34 "save_mesh: no writer registered for '{}'", filepath);
35 return false;
36 }
37
38 const bool ok = writer->write(filepath, meshes, options);
39 if (!ok) {
41 "save_mesh: writer failed for '{}': {}", filepath, writer->get_last_error());
42 } else {
44 "save_mesh: wrote '{}'", filepath);
45 }
46 return ok;
47 }
48
49 /**
50 * @brief Download a bound diffuse texture and write it next to the model
51 * file, so AssimpModelWriter's "diffuse_path" submesh attribute has
52 * somewhere to point.
53 *
54 * Named "<model-stem>_diffuse.png", or "<model-stem>_<suffix>_diffuse.png"
55 * when more than one texture is being written alongside the same model
56 * file (one per MeshNetwork slot).
57 *
58 * @return The sibling path on success, empty string if there is nothing
59 * to write or the download/encode fails.
60 */
61 std::string pack_diffuse_texture(
62 const std::shared_ptr<Core::VKImage>& texture,
63 const std::string& model_filepath,
64 const std::string& suffix)
65 {
66 if (!texture) {
67 return {};
68 }
69
70 const std::filesystem::path model_path(model_filepath);
71 std::string name = model_path.stem().string() + "_diffuse";
72 if (!suffix.empty()) {
73 name += "_" + suffix;
74 }
75 name += ".png";
76
77 const auto image_path = (model_path.parent_path() / name).string();
78 if (!save_image(texture, image_path)) {
80 "save_mesh: failed to write diffuse texture to '{}'", image_path);
81 return {};
82 }
83 return image_path;
84 }
85
86 /**
87 * @brief Give mesh_data a single full-mesh submesh Region if it has none,
88 * so a caller-supplied diffuse_path has an attribute to attach to.
89 */
90 void ensure_submesh_region(Kakshya::MeshData& mesh_data, const std::string& name)
91 {
92 if (mesh_data.submeshes && !mesh_data.submeshes->regions.empty()) {
93 return;
94 }
95
96 Kakshya::MeshSubrange sub;
97 sub.index_start = 0;
98 sub.index_count = mesh_data.face_count() * 3;
99 sub.name = name;
100
101 Kakshya::RegionGroup rg("submeshes");
102 rg.add_region(sub.to_region());
103 mesh_data.submeshes = std::move(rg);
104 }
105
106 /**
107 * @brief Pack one MeshWriterNode's current vertices/indices into a
108 * MeshData, baking @p world into position/normal/tangent.
109 * @return nullopt if the node has no vertices yet, or insertion fails.
110 */
111 std::optional<Kakshya::MeshData> pack_node(
112 const std::shared_ptr<Nodes::GpuSync::MeshWriterNode>& node,
113 const glm::mat4& world,
114 const std::string& name,
115 const std::string& diffuse_path = {})
116 {
117 const auto& src_verts = node->get_mesh_vertices();
118 const auto& indices = node->get_mesh_indices();
119 if (src_verts.empty() || indices.empty()) {
120 return std::nullopt;
121 }
122
123 const glm::mat3 normal_matrix(world);
124
125 std::vector<Kakshya::MeshVertex> verts(src_verts.begin(), src_verts.end());
126 for (auto& v : verts) {
127 v.position = glm::vec3(world * glm::vec4(v.position, 1.0F));
128 v.normal = glm::normalize(normal_matrix * v.normal);
129 v.tangent = glm::normalize(normal_matrix * v.tangent);
130 }
131
132 auto mesh_data = Kakshya::MeshData::empty();
133 Kakshya::MeshInsertion ins(mesh_data.vertex_variant, mesh_data.index_variant);
134 ins.insert_flat(
135 std::span<const uint8_t>(
136 reinterpret_cast<const uint8_t*>(verts.data()),
137 verts.size() * sizeof(Kakshya::MeshVertex)),
138 std::span<const uint32_t>(indices),
139 Kakshya::VertexLayout::for_meshes(sizeof(Kakshya::MeshVertex)));
140 auto access = ins.build();
141 if (!access) {
142 return std::nullopt;
143 }
144 mesh_data.layout = access->layout;
145
146 Kakshya::MeshSubrange sub;
147 sub.index_start = 0;
148 sub.index_count = static_cast<uint32_t>(indices.size());
149 sub.name = name;
150 sub.diffuse_path = diffuse_path;
151 Kakshya::RegionGroup rg("submeshes");
152 rg.add_region(sub.to_region());
153 mesh_data.submeshes = std::move(rg);
154
155 return mesh_data;
156 }
157
158 std::string splice_timestamp(const std::string& pattern)
159 {
160 const auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
161 std::chrono::system_clock::now().time_since_epoch())
162 .count();
163 return resolve_sequence_path(pattern, static_cast<uint64_t>(now_ms));
164 }
165
166} // namespace
167
169 const std::shared_ptr<Buffers::MeshBuffer>& buffer,
170 const std::string& filepath,
171 const ModelWriteOptions& options)
172{
173 if (!buffer) {
174 MF_ERROR(Journal::Component::IO, Journal::Context::FileIO, "save_mesh: null buffer");
175 return false;
176 }
177
178 auto mesh_data = buffer->get_mesh_data();
179 if (buffer->has_diffuse_texture()) {
180 const auto image_path = pack_diffuse_texture(buffer->get_diffuse_texture(), filepath, "");
181 if (!image_path.empty()) {
182 ensure_submesh_region(mesh_data, "mesh");
183 mesh_data.submeshes->regions.front().set_attribute("diffuse_path", image_path);
184 }
185 }
186
187 return write_via_registry(filepath, { mesh_data }, options);
188}
189
191 const std::shared_ptr<Nodes::Network::MeshNetwork>& network,
192 const std::string& filepath,
193 const ModelWriteOptions& options)
194{
195 if (!network) {
196 MF_ERROR(Journal::Component::IO, Journal::Context::FileIO, "save_mesh: null network");
197 return false;
198 }
199
200 std::vector<Kakshya::MeshData> meshes;
201 meshes.reserve(network->slots().size());
202 for (const auto& slot : network->slots()) {
203 if (!slot.node) {
204 continue;
205 }
206 std::string diffuse_path;
207 if (slot.diffuse_texture) {
208 const auto suffix = !slot.name.empty() ? slot.name : std::to_string(slot.index);
209 diffuse_path = pack_diffuse_texture(slot.diffuse_texture, filepath, suffix);
210 }
211 if (auto packed = pack_node(slot.node, slot.world_transform, slot.name, diffuse_path)) {
212 meshes.push_back(std::move(*packed));
213 }
214 }
215
216 if (meshes.empty()) {
218 "save_mesh: no exportable slots in network");
219 return false;
220 }
221
222 return write_via_registry(filepath, meshes, options);
223}
224
226 const std::shared_ptr<Buffers::MeshNetworkBuffer>& network_buffer,
227 const std::string& filepath,
228 const ModelWriteOptions& options)
229{
230 if (!network_buffer) {
231 MF_ERROR(Journal::Component::IO, Journal::Context::FileIO, "save_mesh: null network_buffer");
232 return false;
233 }
234
235 auto net = network_buffer->get_network();
236 if (!net) {
238 "save_mesh: MeshNetworkBuffer has no network");
239 return false;
240 }
241
242 std::vector<Kakshya::MeshData> meshes;
243 meshes.reserve(net->slots().size());
244 const std::string shared_diffuse_path = network_buffer->has_diffuse_texture()
245 ? pack_diffuse_texture(network_buffer->get_diffuse_texture(), filepath, "")
246 : std::string {};
247 for (const auto& slot : net->slots()) {
248 if (!slot.node) {
249 continue;
250 }
251 std::string diffuse_path = shared_diffuse_path;
252 if (slot.diffuse_texture) {
253 const auto suffix = !slot.name.empty() ? slot.name : std::to_string(slot.index);
254 diffuse_path = pack_diffuse_texture(slot.diffuse_texture, filepath, suffix);
255 }
256 if (auto packed = pack_node(slot.node, slot.world_transform, slot.name, diffuse_path)) {
257 meshes.push_back(std::move(*packed));
258 }
259 }
260
261 if (meshes.empty()) {
263 "save_mesh: no exportable slots in network");
264 return false;
265 }
266
267 return write_via_registry(filepath, meshes, options);
268}
269
271 const std::shared_ptr<Nodes::GpuSync::MeshWriterNode>& node,
272 const std::string& filepath,
273 const ModelWriteOptions& options)
274{
275 if (!node) {
277 return false;
278 }
279
280 auto packed = pack_node(node, glm::mat4(1.0F), "mesh");
281 if (!packed) {
283 "save_mesh: node has no vertices/indices yet");
284 return false;
285 }
286
287 return write_via_registry(filepath, { *packed }, options);
288}
289
290std::optional<Kakshya::MeshData> download_compute_mesh(
291 const std::shared_ptr<Buffers::ComputeMeshBuffer>& buffer)
292{
293 if (!buffer) {
294 MF_ERROR(Journal::Component::IO, Journal::Context::FileIO, "download_compute_mesh: null buffer");
295 return std::nullopt;
296 }
297
298 auto processor = buffer->get_mesh_processor();
299 if (!processor) {
301 "download_compute_mesh: no mesh processor; setup_processors() has not been called");
302 return std::nullopt;
303 }
304
305 auto counter_buf = processor->counter_buf();
306 const auto* counter_ptr = counter_buf
307 ? static_cast<const uint32_t*>(counter_buf->get_mapped_ptr())
308 : nullptr;
309 if (!counter_ptr) {
311 "download_compute_mesh: counter buffer is not host-visible or not allocated yet");
312 return std::nullopt;
313 }
314
315 const uint32_t vertex_count = *counter_ptr;
316 if (vertex_count == 0) {
318 "download_compute_mesh: live vertex count is zero");
319 return std::nullopt;
320 }
321
322 std::vector<Kakshya::MeshVertex> verts(vertex_count);
323 std::shared_ptr<Buffers::VKBuffer> staging;
325 buffer, verts.data(), verts.size() * sizeof(Kakshya::MeshVertex), staging);
326
327 std::vector<uint32_t> indices(vertex_count);
328 std::ranges::iota(indices, 0U);
329
330 auto mesh_data = Kakshya::MeshData::empty();
331 Kakshya::MeshInsertion ins(mesh_data.vertex_variant, mesh_data.index_variant);
332 ins.insert_flat(
333 std::span<const uint8_t>(
334 reinterpret_cast<const uint8_t*>(verts.data()),
335 verts.size() * sizeof(Kakshya::MeshVertex)),
336 std::span<const uint32_t>(indices),
338 auto access = ins.build();
339 if (!access) {
341 "download_compute_mesh: MeshInsertion::build() failed");
342 return std::nullopt;
343 }
344 mesh_data.layout = access->layout;
345
346 return mesh_data;
347}
348
350 const std::shared_ptr<Buffers::ComputeMeshBuffer>& buffer,
351 const std::string& filepath,
352 const ModelWriteOptions& options)
353{
354 auto mesh_data = download_compute_mesh(buffer);
355 if (!mesh_data) {
356 return false;
357 }
358
359 if (buffer->has_diffuse_texture()) {
360 const auto image_path = pack_diffuse_texture(buffer->get_diffuse_texture(), filepath, "");
361 if (!image_path.empty()) {
362 ensure_submesh_region(*mesh_data, "mesh");
363 mesh_data->submeshes->regions.front().set_attribute("diffuse_path", image_path);
364 }
365 }
366
367 return write_via_registry(filepath, { *mesh_data }, options);
368}
369
371 const std::shared_ptr<Buffers::ComputeMeshBuffer>& buffer,
372 const std::string& path_pattern,
373 const ModelWriteOptions& options)
374{
375 return save_mesh(buffer, splice_timestamp(path_pattern), options);
376}
377
379 const std::shared_ptr<Buffers::MeshBuffer>& buffer,
380 const std::string& path_pattern,
381 const ModelWriteOptions& options)
382{
383 return save_mesh(buffer, splice_timestamp(path_pattern), options);
384}
385
387 const std::shared_ptr<Buffers::MeshNetworkBuffer>& network_buffer,
388 const std::string& path_pattern,
389 const ModelWriteOptions& options)
390{
391 return save_mesh(network_buffer, splice_timestamp(path_pattern), options);
392}
393
395 const std::shared_ptr<Nodes::Network::MeshNetwork>& network,
396 const std::string& path_pattern,
397 const ModelWriteOptions& options)
398{
399 return save_mesh(network, splice_timestamp(path_pattern), options);
400}
401
403 const std::shared_ptr<Nodes::GpuSync::MeshWriterNode>& node,
404 const std::string& path_pattern,
405 const ModelWriteOptions& options)
406{
407 return save_mesh(node, splice_timestamp(path_pattern), options);
408}
409
410} // namespace MayaFlux::IO
#define MF_INFO(comp, ctx,...)
#define MF_ERROR(comp, ctx,...)
std::string diffuse_path
Core::GlobalNetworkConfig network
Definition Config.cpp:39
std::shared_ptr< NetworkGeometryBuffer > buffer
std::string name
Definition VKDevice.cpp:143
std::unique_ptr< ModelWriter > create_writer(const std::string &filepath) const
static ModelWriterRegistry & instance()
std::optional< MeshAccess > build() const
Produce a MeshAccess over the current variant contents.
void insert_flat(std::span< const uint8_t > vertex_bytes, std::span< const uint32_t > index_data, const VertexLayout &layout)
Insert a single flat mesh (no submesh tracking).
Write counterpart to MeshAccess.
void download_from_gpu_async(const std::shared_ptr< VKBuffer > &source, void *data, size_t size, std::shared_ptr< VKBuffer > &staging)
Download from a device-local GPU buffer without stalling the graphics queue.
bool save_image(const std::shared_ptr< Core::VKImage > &image, const std::string &filepath, const ImageWriteOptions &options)
Save a VKImage directly to disk via the ImageWriter registry.
bool save_mesh_snapshot(const std::shared_ptr< Buffers::ComputeMeshBuffer > &buffer, const std::string &path_pattern, const ModelWriteOptions &options)
Save a ComputeMeshBuffer with a millisecond epoch timestamp spliced into the path.
std::string resolve_sequence_path(std::string_view pattern, uint64_t frame)
Substitute a frame index into a numbered output pattern.
bool save_mesh(const std::shared_ptr< Buffers::MeshBuffer > &buffer, const std::string &filepath, const ModelWriteOptions &options)
Save a MeshBuffer's current mesh data to disk via ModelWriterRegistry.
std::optional< Kakshya::MeshData > download_compute_mesh(const std::shared_ptr< Buffers::ComputeMeshBuffer > &buffer)
Download a ComputeMeshBuffer's current live geometry to CPU.
@ FileIO
Filesystem I/O operations.
@ IO
Networking, file handling, streaming.
Configuration for model writing.
static MeshData empty()
Construct an empty MeshData with the canonical 60-byte mesh layout.
Definition MeshData.hpp:49
Vertex type for indexed triangle mesh primitives (TRIANGLE_LIST topology)
static VertexLayout for_meshes(uint32_t stride=60)
Factory: layout for MeshVertex (position, color, weight, uv, normal, tangent)