Vulkan Schnee 0.0.1
High-performance rendering engine
Loading...
Searching...
No Matches
FrameProcessing.cpp
Go to the documentation of this file.
2
3#include <set>
4#include <variant>
5
11#include "Engine/Ecs/EcsData.h"
12#include "Engine/Files/Path.h"
26#include "plog/Log.h"
27
28namespace Ecs
29{
30
37
38 void TextureAssetPipeline::submitAsset( const std::filesystem::path & asset )
39 {
40 assetQueue.push_back( asset );
41 }
42
43 void TextureAssetPipeline::tick( bool async )
44 {
45 requestExrHeaders( async );
46 loadImageData( async );
47 retrieveImageData( async );
48 }
49
51 {
52 std::chrono::time_point<std::chrono::system_clock> startTime =
53 std::chrono::high_resolution_clock::now();
54 TRACY_ZONE_SCOPED_NAMED_ASSET( "Request Exr Header" );
55 if ( assetQueue.empty() )
56 return;
57 headerFutures.reserve( headerFutures.size() + assetQueue.size() );
58 for ( auto it = assetQueue.begin(); it != assetQueue.end(); )
59 {
60 auto future = assetLoaderPool->submit_task(
61 [assetPath = *it]()
62 {
64 return loader.loadHeader( assetPath );
65 }
66 );
67
68 headerFutures.emplace_back( *it, std::move( future ) );
69 it = assetQueue.erase( it );
70 }
71 }
72
74 {
75 TRACY_ZONE_SCOPED_NAMED_ASSET( "Load image data" );
76 imageFutures.reserve( imageFutures.size() + headerFutures.size() );
77 for ( auto it = headerFutures.begin(); it != headerFutures.end(); )
78 {
80 if ( async && it->future.wait_for( std::chrono::seconds( 0 ) ) == std::future_status::ready )
81 {
82 header = it->future.get();
83 }
84 else if ( !async )
85 {
86 header = it->future.get();
87 }
88 else
89 {
90 ++it;
91 continue;
92 }
93
94 auto * asset = new EngineCore::ExrTextureAsset();
95 asset->requestLoad();
96 asset->setHeader( header );
97 // set the asset in the asset manager
98
99 textureAssetManager->add( it->path, asset );
100
101 const std::filesystem::path assetPath = it->path;
102 std::future<tinygltf::Image> future = assetLoaderPool->submit_task(
103 [assetPath]()
104 {
106 return loader.load( assetPath );
107 }
108 );
109
110 imageFutures.emplace_back( it->path, std::move( future ) );
111 it = headerFutures.erase( it );
112 }
113 }
114
116 {
117 TRACY_ZONE_SCOPED_NAMED_D_ASSET( "Retrieve Image Data with async " << ( async ? "TRUE" : "FALSE" ) );
118 for ( auto it = imageFutures.begin(); it != imageFutures.end(); )
119 {
120 TRACY_ZONE_SCOPED_NAMED_ASSET( "Trying to load image" );
121 bool loaded = false;
122 if ( async )
123 {
124 if ( it->future.valid() &&
125 it->future.wait_for( std::chrono::seconds( 0 ) ) == std::future_status::ready )
126 {
127 loaded = true;
128 }
129 else
130 {
131 ++it;
132 continue;
133 }
134 }
135 else
136 {
137 loaded = true;
138 }
139
140 if ( loaded )
141 {
142 TRACY_ZONE_SCOPED_NAMED_ASSET( "Update Asset state" );
143 auto asset = UnpackOptional( textureAssetManager->getAsset( it->path ) );
144 asset->setImage( it->future.get() );
145
146 // Notify RenderingDataManager that a texture has been loaded
147 // Pass the path so it can register the Vulkan texture with AssetManager
149 {
150 renderingDataManager->onTextureLoaded( asset, it->path );
151 }
152
153 it = imageFutures.erase( it );
154 }
155 }
156 }
157
178
179 void ModelAssetPipeline::submitAsset( const std::filesystem::path & asset )
180 {
181 assetQueue.push_back( asset );
182
183 // Mark the model asset as requested to load
184 auto modelAssetOpt = modelAssetManager->getAsset( asset );
185 if ( modelAssetOpt.has_value() && modelAssetOpt.value() != nullptr )
186 {
187 modelAssetOpt.value()->requestLoad();
188 }
189 }
190
191 void ModelAssetPipeline::tick( bool async )
192 {
193 submitLoadModel( async );
194 processModel( async );
195 processMaterial( async );
196 processMeshData( async );
197 }
198
200 {
201 TRACY_ZONE_SCOPED_NAMED_ASSET( "Submit Load Model" );
202 if ( assetQueue.empty() ) return;
203
204 for ( auto it = assetQueue.begin(); it != assetQueue.end(); )
205 {
207 {
208 PLOGW << "Asset " << it->generic_string()
209 << " does not exist. Will thus not be loaded by asset pipeline!";
210 assetQueue.erase( it );
211 continue;
212 }
213 std::future<tinygltf::Model> future = assetLoaderPool->submit_task(
214 [assetPath = *it, gltfLoader = this->loader]()
215 { return EngineCore::UnpackOptional( gltfLoader->loadModel( assetPath ) ); }
216 );
217
218 modelLoadingFutures.emplace_back( *it, std::move( future ) );
219 it = assetQueue.erase( it );
220 }
221 }
222
224 {
225 TRACY_ZONE_SCOPED_NAMED_ASSET( "Process Model" );
226 if ( modelLoadingFutures.empty() )
227 return;
228 for ( auto it = modelLoadingFutures.begin(); it != modelLoadingFutures.end(); )
229 {
230 std::optional<tinygltf::Model> model;
231 if ( async )
232 {
233 if ( it->future.valid() &&
234 it->future.wait_for( std::chrono::seconds( 0 ) ) == std::future_status::ready )
235 {
236 model = std::move( it->future.get() );
237 }
238 else
239 {
240 ++it;
241 continue;
242 }
243 }
244 else
245 {
246 model = std::move( it->future.get() );
247 }
248
249 EngineCore::ModelAsset * modelAsset = EngineCore::UnpackOptional( modelAssetManager->getAsset( it->path ) );
250 modelAsset->model = std::move( model.value() );
251 model.reset();
252
253 // Pre-compute texture paths before processTextures moves the image data
254 std::vector<std::filesystem::path> imageTexturePaths;
255 imageTexturePaths.reserve( modelAsset->model->images.size() );
256 for ( size_t i = 0; i < modelAsset->model->images.size(); ++i )
257 {
258 const tinygltf::Image & image = modelAsset->model->images[i];
259 std::filesystem::path texturePath;
260
261 if ( !image.uri.empty() )
262 {
263 texturePath = it->path.parent_path() / image.uri;
264 }
265 else if ( !image.name.empty() )
266 {
267 texturePath = it->path.parent_path() / image.name;
268 }
269 else
270 {
271 texturePath = it->path.parent_path() / ( "image_" + std::to_string( i ) );
272 }
273 imageTexturePaths.push_back( std::move( texturePath ) );
274 }
275
276 // Pre-register texture handles BEFORE processTextures loads the actual image data.
277 // This ensures that when onTextureLoaded is called, the handles already exist
278 // and can be updated with descriptor indices.
279 // We scan all materials to determine the correct TextureType for each image.
280 {
281 EngineCore::TextureHandleRegistry* textureRegistry = assetManager_ ? assetManager_->getTextureHandleRegistry() : nullptr;
282 if ( textureRegistry )
283 {
284 // Track which images are used as which texture types
285 std::unordered_map<int, EngineCore::TextureType> imageTypeMap;
286
287 for ( const tinygltf::Material & material : modelAsset->model->materials )
288 {
289 // Base color -> SRGB
290 int idx = material.pbrMetallicRoughness.baseColorTexture.index;
291 if ( idx >= 0 && idx < static_cast<int>( modelAsset->model->textures.size() ) )
292 {
293 int source = modelAsset->model->textures[idx].source;
294 if ( source >= 0 )
295 imageTypeMap[source] = EngineCore::TextureType::BaseColor;
296 }
297
298 // Normal -> LINEAR
299 idx = material.normalTexture.index;
300 if ( idx >= 0 && idx < static_cast<int>( modelAsset->model->textures.size() ) )
301 {
302 int source = modelAsset->model->textures[idx].source;
303 if ( source >= 0 )
304 imageTypeMap[source] = EngineCore::TextureType::Normal;
305 }
306
307 // Metallic-Roughness -> LINEAR
308 idx = material.pbrMetallicRoughness.metallicRoughnessTexture.index;
309 if ( idx >= 0 && idx < static_cast<int>( modelAsset->model->textures.size() ) )
310 {
311 int source = modelAsset->model->textures[idx].source;
312 if ( source >= 0 )
313 imageTypeMap[source] = EngineCore::TextureType::MetallicRoughness;
314 }
315
316 // Emissive -> SRGB
317 idx = material.emissiveTexture.index;
318 if ( idx >= 0 && idx < static_cast<int>( modelAsset->model->textures.size() ) )
319 {
320 int source = modelAsset->model->textures[idx].source;
321 if ( source >= 0 )
322 imageTypeMap[source] = EngineCore::TextureType::Emissive;
323 }
324 }
325
326 // Create handles for all images with their determined types
327 for ( size_t i = 0; i < imageTexturePaths.size(); ++i )
328 {
329 auto typeIt = imageTypeMap.find( static_cast<int>( i ) );
330 EngineCore::TextureType type = ( typeIt != imageTypeMap.end() )
331 ? typeIt->second
333
334 // Pre-create the handle so onTextureLoaded can find it
335 switch ( type )
336 {
338 textureRegistry->getOrCreateHandle<EngineCore::TextureType::Normal>( imageTexturePaths[i] );
339 break;
341 textureRegistry->getOrCreateHandle<EngineCore::TextureType::MetallicRoughness>( imageTexturePaths[i] );
342 break;
344 textureRegistry->getOrCreateHandle<EngineCore::TextureType::Emissive>( imageTexturePaths[i] );
345 break;
347 default:
348 textureRegistry->getOrCreateHandle<EngineCore::TextureType::BaseColor>( imageTexturePaths[i] );
349 break;
350 }
351 }
352 PLOGI << "Pre-registered " << imageTexturePaths.size() << " texture handles before loading";
353 }
354 }
355
356 // Extract and register texture assets from the loaded model (moves image data)
357 processTextures( modelAsset, it->path );
358
359 // synchronously create material assets and then submit the async tasks -> this is important
360 // because the meshes will need the material assets later on. And if
361 {
362 TRACY_ZONE_SCOPED_NAMED_ASSET( "Starting loading for material assets" );
363 for ( const tinygltf::Material & material : modelAsset->model->materials )
364 {
365 TRACY_ZONE_SCOPED_NAMED_D_ASSET( "Loading material asset " << material.name );
366 auto materialPath = Asset::Path( it->path, material.name );
367 PLOGI << "processModel: Registering material '" << material.name
368 << "' with path handle: " << materialPath.getAssetHandle();
369 auto * materialAsset = new EngineCore::MaterialAsset( std::move( Asset::Path( it->path, material.name ) ), true );
370 materialAssetManager->add( std::move( Asset::Path( it->path, material.name ) ), materialAsset );
371 modelAsset->registerCreatedAssetWithModel( materialAsset );
372
373 // Get TextureHandleRegistry for type-safe handle creation
374 EngineCore::TextureHandleRegistry* textureRegistry = assetManager_ ? assetManager_->getTextureHandleRegistry() : nullptr;
375 PLOGI << "processModel: Processing material '" << material.name
376 << "' from model '" << it->path.string()
377 << "' assetManager_=" << (assetManager_ ? "valid" : "NULL")
378 << " textureRegistry=" << (textureRegistry ? "valid" : "NULL");
379
380 // Extract and set base color texture path if available
381 const int baseColorTexIndex = material.pbrMetallicRoughness.baseColorTexture.index;
382 if ( baseColorTexIndex >= 0 &&
383 baseColorTexIndex < static_cast<int>( modelAsset->model->textures.size() ) )
384 {
385 const tinygltf::Texture & texture = modelAsset->model->textures[baseColorTexIndex];
386 if ( texture.source >= 0 &&
387 texture.source < static_cast<int>( imageTexturePaths.size() ) )
388 {
389 const std::filesystem::path & texturePath = imageTexturePaths[texture.source];
390 materialAsset->setBaseColorTexturePath( texturePath );
391 // Set typed handle for O(1) lookup and correct SRGB format
392 if ( textureRegistry )
393 {
394 auto handle = textureRegistry->getOrCreateHandle<EngineCore::TextureType::BaseColor>( texturePath );
395 materialAsset->setBaseColorTextureHandle( handle );
396 }
397 PLOGD << "Material '" << material.name << "' uses base color texture: " << texturePath;
398 }
399 }
400
401 // Extract and set normal texture path if available
402 const int normalTexIndex = material.normalTexture.index;
403 if ( normalTexIndex >= 0 &&
404 normalTexIndex < static_cast<int>( modelAsset->model->textures.size() ) )
405 {
406 const tinygltf::Texture & texture = modelAsset->model->textures[normalTexIndex];
407 if ( texture.source >= 0 &&
408 texture.source < static_cast<int>( imageTexturePaths.size() ) )
409 {
410 const std::filesystem::path & texturePath = imageTexturePaths[texture.source];
411 materialAsset->setNormalTexturePath( texturePath );
412 // Set typed handle for O(1) lookup and correct LINEAR format (critical fix!)
413 if ( textureRegistry )
414 {
415 auto handle = textureRegistry->getOrCreateHandle<EngineCore::TextureType::Normal>( texturePath );
416 materialAsset->setNormalTextureHandle( handle );
417 }
418 PLOGD << "Material '" << material.name << "' uses normal texture: " << texturePath;
419 }
420 }
421
422 // Extract and set metallic-roughness texture path if available
423 const int metallicRoughnessTexIndex = material.pbrMetallicRoughness.metallicRoughnessTexture.index;
424 if ( metallicRoughnessTexIndex >= 0 &&
425 metallicRoughnessTexIndex < static_cast<int>( modelAsset->model->textures.size() ) )
426 {
427 const tinygltf::Texture & texture = modelAsset->model->textures[metallicRoughnessTexIndex];
428 if ( texture.source >= 0 &&
429 texture.source < static_cast<int>( imageTexturePaths.size() ) )
430 {
431 const std::filesystem::path & texturePath = imageTexturePaths[texture.source];
432 materialAsset->setMetallicRoughnessTexturePath( texturePath );
433 // Set typed handle for O(1) lookup and correct LINEAR format
434 if ( textureRegistry )
435 {
436 auto handle = textureRegistry->getOrCreateHandle<EngineCore::TextureType::MetallicRoughness>( texturePath );
437 materialAsset->setMetallicRoughnessTextureHandle( handle );
438 }
439 PLOGD << "Material '" << material.name << "' uses metallic-roughness texture: " << texturePath;
440 }
441 }
442
443 // Extract and set emissive texture path if available
444 const int emissiveTexIndex = material.emissiveTexture.index;
445 if ( emissiveTexIndex >= 0 &&
446 emissiveTexIndex < static_cast<int>( modelAsset->model->textures.size() ) )
447 {
448 const tinygltf::Texture & texture = modelAsset->model->textures[emissiveTexIndex];
449 if ( texture.source >= 0 &&
450 texture.source < static_cast<int>( imageTexturePaths.size() ) )
451 {
452 const std::filesystem::path & texturePath = imageTexturePaths[texture.source];
453 materialAsset->setEmissiveTexturePath( texturePath );
454 // Set typed handle for O(1) lookup and correct SRGB format
455 if ( textureRegistry )
456 {
457 auto handle = textureRegistry->getOrCreateHandle<EngineCore::TextureType::Emissive>( texturePath );
458 materialAsset->setEmissiveTextureHandle( handle );
459 }
460 PLOGD << "Material '" << material.name << "' uses emissive texture: " << texturePath;
461 }
462 }
463
464 // Extract lightmap path from nodes that use this material
465 // Lightmap properties are at node level in GLTF, not material level
466 {
467 // Get the current material index
468 int materialIndex = static_cast<int>( &material - &modelAsset->model->materials[0] );
469
470 // Search through nodes to find one that uses this material and has lightmap properties
471 for ( const tinygltf::Node & node : modelAsset->model->nodes )
472 {
473 if ( node.mesh < 0 || node.mesh >= static_cast<int>( modelAsset->model->meshes.size() ) )
474 continue;
475
476 const tinygltf::Mesh & mesh = modelAsset->model->meshes[node.mesh];
477
478 // Check if any primitive of this mesh uses our material
479 bool usesMaterial = false;
480 for ( const tinygltf::Primitive & primitive : mesh.primitives )
481 {
482 if ( primitive.material == materialIndex )
483 {
484 usesMaterial = true;
485 break;
486 }
487 }
488
489 if ( !usesMaterial )
490 continue;
491
492 // Found a node that uses this material - check for lightmap properties
493 EngineCore::GltfLoader::StaticMeshExtensions nodeExtensions( node.extensions );
494 if ( nodeExtensions.vulkanSchneeExtension.has_value() &&
495 nodeExtensions.vulkanSchneeExtension->hasLightmapProperties() &&
496 nodeExtensions.vulkanSchneeExtension->lightProperties.has_value() &&
497 nodeExtensions.vulkanSchneeExtension->lightProperties->getHasLightmaps() &&
498 nodeExtensions.vulkanSchneeExtension->lightmapProperties->isLightmapPathValid() )
499 {
500 // Resolve lightmap path relative to the GLTF file (use absolute for consistency)
501 const auto & relativeLightmapPath = nodeExtensions.vulkanSchneeExtension->lightmapProperties->lightmapPath;
502 std::filesystem::path lightmapPath = std::filesystem::absolute( it->path.parent_path() / relativeLightmapPath );
503 materialAsset->setLightmapTexturePath( lightmapPath );
504 // Set typed handle for O(1) lookup and correct HDR float format
505 if ( textureRegistry )
506 {
507 auto handle = textureRegistry->getOrCreateHandle<EngineCore::TextureType::Lightmap>( lightmapPath );
508 materialAsset->setLightmapTextureHandle( handle );
509 }
510 PLOGD << "Material '" << material.name << "' uses lightmap texture: " << lightmapPath;
511 break; // Found lightmap, no need to check more nodes
512 }
513 }
514 }
515
516 materialFutures.push_back( threadPool->submit_task(
517 [material, assetPath = it->path, modelAsset]() -> Ecs::MaterialLoadingData
518 {
519 // extract data from the material and return a pointer to the extracted material
520 // data using GltfLoader to properly parse the material extensions
521 auto asset = Asset::Path( assetPath, material.name );
522
523 // Parse material data using GltfLoader - this extracts shader type from VULKAN_SCHNEE extension
524 EngineCore::GltfLoader::StaticMeshExtensions meshExtensions{};
526 *modelAsset->model, material, meshExtensions);
527
528 // Get the material type from the extension (defaults to NORMALS_SHADER if no extension)
530 if (gltfMaterial.hasVulkanSchneeExtension()) {
531 type = gltfMaterial.getVulkanSchneeExtension().materialName;
532 PLOGI << "Material '" << material.name << "' has extension, type="
533 << static_cast<int>(type) << " shaderType="
534 << gltfMaterial.getVulkanSchneeExtension().shaderType;
535 } else {
536 PLOGI << "Material '" << material.name << "' has NO extension, defaulting to NORMALS_SHADER";
537 }
538
539 // Create the appropriate material data on the heap using std::visit
540 EngineCore::MaterialData* data = std::visit(
541 [](auto&& arg) -> EngineCore::MaterialData* {
542 using T = std::decay_t<decltype(arg)>;
543 return new T(arg);
544 },
545 gltfMaterial.getMaterialDataRaw()
546 );
547
548 return { std::move( asset ), type, data };
549 }
550 ) );
551 }
552 }
553
554 {
555 TRACY_ZONE_SCOPED_NAMED_ASSET( "Loading mesh assets" );
556 // Iterate over nodes instead of meshes to use node names (what users expect)
557 // rather than internal mesh names (e.g., "Cube" instead of "Cube.001")
558 int nodeIndex = 0;
559 for ( const tinygltf::Node & node : modelAsset->model->nodes )
560 {
561 // Skip nodes that don't reference a mesh
562 if ( node.mesh < 0 || node.mesh >= static_cast<int>( modelAsset->model->meshes.size() ) )
563 {
564 ++nodeIndex;
565 continue;
566 }
567
568 // Special logging for debugging VarC (node 13) issue
569 if ( node.name.find("VarC") != std::string::npos || node.name.find("scq9") != std::string::npos )
570 {
571 PLOGW << "*** PROCESSING VarC NODE: index=" << nodeIndex << " name=" << node.name
572 << " mesh=" << node.mesh << " ***";
573 }
574
575 const tinygltf::Mesh & mesh = modelAsset->model->meshes[node.mesh];
576 TRACY_ZONE_SCOPED_NAMED_D_ASSET( "Loading full mesh asset" << node.name );
577
578 const auto meshAsset = new EngineCore::MeshAsset(true);
579 const auto meshPath = Asset::Path( it->path, node.name );
580 meshAssetManager->add( meshPath, meshAsset );
581 modelAsset->registerCreatedAssetWithModel( meshAsset );
582
583 {
584 TRACY_ZONE_SCOPED_NAMED_ASSET( "Submit primitive loading task" );
585 primitiveDataFutures.push_back( assetLoaderPool->submit_task(
586 [gltfLoader = loader, mesh, pool = threadPool, modelAsset, meshPath](
588 {
589 TRACY_ZONE_SCOPED_NAMED_D_ASSET( "Loading mesh asset " << meshPath.getAssetName() );
590 std::vector<std::future<PrimitiveData>> primitiveDataFutures;
591 std::vector<Asset::Path> materialPaths;
592 primitiveDataFutures.reserve( mesh.primitives.size() );
593 materialPaths.reserve( mesh.primitives.size() );
594
595 PLOGI << "=== Creating material paths for mesh: " << meshPath.getAssetHandle()
596 << " (primitives: " << mesh.primitives.size() << ") ===";
597 int primitiveIdx = 0;
598 for ( const tinygltf::Primitive & primitive : mesh.primitives )
599 {
600 // Resolve material path while model is still available
601 int matIdx = primitive.material;
602 PLOGI << " primitive[" << primitiveIdx << "] matIdx=" << matIdx
603 << " (materials.size=" << modelAsset->model->materials.size() << ")";
604 if ( matIdx >= 0 && matIdx < static_cast<int>( modelAsset->model->materials.size() ) )
605 {
606 const auto & matName = modelAsset->model->materials[matIdx].name;
607 materialPaths.emplace_back( meshPath.getFilePath(), matName );
608 PLOGI << " -> created path: " << meshPath.getFilePath().string() << " + " << matName;
609 }
610 else
611 {
612 materialPaths.emplace_back(); // empty path = use default pipeline
613 PLOGI << " -> EMPTY path (matIdx out of range or -1)";
614 }
615 ++primitiveIdx;
616
617 auto vertexData = EngineCore::GltfLoader::GltfVertexData(
618 modelAsset->model->buffers,
619 modelAsset->model->bufferViews,
620 modelAsset->model->accessors,
621 primitive
622 );
623 primitiveDataFutures.emplace_back(
624 std::move( pool->submit_task(
625 [gltfLoader, vertexData]() -> PrimitiveData
626 {
627 TRACY_ZONE_SCOPED_NAMED_ASSET( "Loading Primitive Data" );
628 return gltfLoader->loadPrimitiveData( vertexData );
629 }
630 ) )
631 );
632 }
633
634 // Log summary of material paths created
635 size_t emptyCount = 0;
636 for (const auto& path : materialPaths) {
637 if (path.empty()) ++emptyCount;
638 }
639 PLOGI << "=== Material paths summary for " << meshPath.getAssetHandle()
640 << ": total=" << materialPaths.size() << " empty=" << emptyCount << " ===";
641
642 std::vector<PrimitiveData> data;
643 data.reserve( primitiveDataFutures.size() );
644 for ( auto & primitiveDataFuture : primitiveDataFutures )
645 {
646 data.emplace_back( primitiveDataFuture.get() );
647 }
648 return { meshPath, std::move( data ), std::move( materialPaths ) };
649 }
650 ) );
651 }
652 }
653 }
654 it = modelLoadingFutures.erase( it );
655 }
656 }
657
659 {
660 if ( materialFutures.empty() )
661 return;
662 for ( auto it = materialFutures.begin(); it != materialFutures.end(); )
663 {
664 Ecs::MaterialLoadingData materialLoadingData;
665 if ( async )
666 {
667 if ( it->valid() && it->wait_for( std::chrono::seconds( 0 ) ) == std::future_status::ready )
668 {
669 materialLoadingData = std::move( it->get() );
670 }
671 else
672 {
673 ++it;
674 continue;
675 }
676 }
677 else
678 {
679 materialLoadingData = std::move( it->get() );
680 }
681
682 EngineCore::MaterialAsset * materialAsset = EngineCore::UnpackOptional( materialAssetManager->getAsset( materialLoadingData.path ) );
683 materialAsset->type = materialLoadingData.type;
684 PLOGI << "processMaterial: Setting type for material '"
685 << materialLoadingData.path.getAssetHandle()
686 << "' to " << static_cast<int>(materialLoadingData.type);
687
688 auto& registry = RegistryManager::get();
689 switch ( materialAsset->type )
690 {
692 {
693 const auto* normalsData = static_cast<EngineCore::NormalMaterialData*>(materialLoadingData.data);
694 registry.emplace<EngineCore::NormalMaterialData>(materialAsset->data, *normalsData);
695 break;
696 }
698 {
699 const auto* flatColorData = static_cast<EngineCore::DiffuseFlatColorMaterialData*>(materialLoadingData.data);
700 registry.emplace<EngineCore::DiffuseFlatColorMaterialData>(materialAsset->data, *flatColorData);
701 break;
702 }
704 {
705 const auto* diffuseData = static_cast<EngineCore::DiffuseShaderMaterialData*>(materialLoadingData.data);
706 registry.emplace<EngineCore::DiffuseShaderMaterialData>(materialAsset->data, *diffuseData);
707 break;
708 }
710 {
711 const auto* dynamicData = static_cast<EngineCore::DynamicTexturesMaterialData*>(materialLoadingData.data);
712 registry.emplace<EngineCore::DynamicTexturesMaterialData>(materialAsset->data, *dynamicData);
713 break;
714 }
716 {
717 const auto* l0Data = static_cast<EngineCore::L0ShaderMaterialData*>(materialLoadingData.data);
718 registry.emplace<EngineCore::L0ShaderMaterialData>(materialAsset->data, *l0Data);
719 break;
720 }
722 {
723 const auto* l1Data = static_cast<EngineCore::L1ShaderMaterialData*>(materialLoadingData.data);
724 registry.emplace<EngineCore::L1ShaderMaterialData>(materialAsset->data, *l1Data);
725 break;
726 }
728 {
729 const auto* l2Data = static_cast<EngineCore::L2ShaderMaterialData*>(materialLoadingData.data);
730 registry.emplace<EngineCore::L2ShaderMaterialData>(materialAsset->data, *l2Data);
731 break;
732 }
734 {
735 const auto* movableData = static_cast<EngineCore::MovableDiffuseShaderMaterialData*>(materialLoadingData.data);
736 registry.emplace<EngineCore::MovableDiffuseShaderMaterialData>(materialAsset->data, *movableData);
737 break;
738 }
740 {
741 const auto* lightmapData = static_cast<EngineCore::StaticLightmapMaterialData*>(materialLoadingData.data);
742 registry.emplace<EngineCore::StaticLightmapMaterialData>(materialAsset->data, *lightmapData);
743 break;
744 }
745 }
746
747 // Notify RenderingDataManager that a material has been loaded
749 {
750 renderingDataManager->onMaterialLoaded( materialAsset );
751 }
752
753 it = materialFutures.erase( it );
754 }
755 }
756
758 {
759 // First, poll any pending meshlet generation futures (must run even if primitiveDataFutures is empty)
760 if ( async && !meshletGenerationFutures.empty() )
761 {
762 TRACY_ZONE_SCOPED_NAMED( "Poll meshlet generation futures" );
763 for ( auto it = meshletGenerationFutures.begin(); it != meshletGenerationFutures.end(); )
764 {
765 if ( it->valid() && it->wait_for( std::chrono::seconds( 0 ) ) == std::future_status::ready )
766 {
767 auto completedGeneration = it->get();
768 PLOGD << "Meshlet generation completed for: " << completedGeneration.meshPath.getFilePath();
769
770 // Queue the completed generation for processing at a safe point
771 // This prevents race conditions with the main thread reading the registry
772 pendingMeshletCompletions_.push_back(std::move(completedGeneration));
773
774 it = meshletGenerationFutures.erase( it );
775 }
776 else
777 {
778 ++it;
779 }
780 }
781 }
782
783 if ( primitiveDataFutures.empty() )
784 return;
785 TRACY_ZONE_SCOPED_NAMED_ASSET( "Dispatching primitive data" );
786 for ( auto it = primitiveDataFutures.begin(); it != primitiveDataFutures.end(); )
787 {
788 TRACY_ZONE_SCOPED_NAMED_ASSET( "Meshlet generation for primitive" );
789 Ecs::PrimitiveDataLoading primitiveDataLoadingData;
790 if ( async )
791 {
792 if ( it->valid() && it->wait_for( std::chrono::seconds( 0 ) ) == std::future_status::ready )
793 {
794 primitiveDataLoadingData = std::move( it->get() );
795 }
796 else
797 {
798 ++it;
799 continue;
800 }
801 }
802 else
803 {
804 primitiveDataLoadingData = std::move( it->get() );
805 }
806
807 auto & registry = RegistryManager::get();
808 EngineCore::MeshAsset * meshAsset = EngineCore::UnpackOptional( meshAssetManager->getAsset( primitiveDataLoadingData.path ) );
809 MeshPrimitiveData & data = registry.emplace<Ecs::MeshPrimitiveData>( meshAsset->data );
810 data.primitives.resize( primitiveDataLoadingData.data.size() );
811 {
812 TRACY_ZONE_SCOPED_NAMED( "Set material paths" );
813 // Only set material paths here - raw primitive data goes directly to background task
814 PLOGI << "processMeshData: Setting material paths for " << primitiveDataLoadingData.data.size()
815 << " primitives (mesh: " << primitiveDataLoadingData.path.getAssetHandle()
816 << " entity=" << static_cast<uint32_t>(meshAsset->data) << ")";
817 for ( uint32_t i = 0; i < primitiveDataLoadingData.data.size(); ++i )
818 {
819 // Don't store raw data in ECS - it will be moved directly to background task
820 // data.primitives[i].data will be populated with optimized data after meshlet generation
821 if ( i < primitiveDataLoadingData.materialPaths.size() )
822 {
823 data.primitives[i].materialPath = primitiveDataLoadingData.materialPaths[i];
824 PLOGI << " primitive[" << i << "] materialPath = "
825 << (primitiveDataLoadingData.materialPaths[i].empty() ? "(empty)" : primitiveDataLoadingData.materialPaths[i].getAssetHandle());
826 }
827 else
828 {
829 PLOGW << " primitive[" << i << "] has no materialPath (index out of range)";
830 }
831 }
832 }
833
834 {
835 TRACY_ZONE_SCOPED_NAMED( "Submit meshlet generation tasks" );
836 // Move input data directly to background task - no intermediate ECS storage
837 // This avoids expensive copy of vertex/index data on main thread
838 entt::entity meshEntity = meshAsset->getEntity();
839
840 meshletGenerationFutures.push_back( threadPool->submit_task(
841 [path = primitiveDataLoadingData.path, meshEntity,
842 inputPrimitives = std::move(primitiveDataLoadingData.data)]() mutable -> CompletedMeshletGeneration
843 {
844 CompletedMeshletGeneration result;
845 result.meshPath = path;
846 result.meshEntity = meshEntity;
847 result.primitiveResults.reserve(inputPrimitives.size());
848
849 std::vector<glm::vec3> allMeshPoints; // For mesh bounding sphere
850
851 for (auto& inputData : inputPrimitives)
852 {
853 TRACY_ZONE_SCOPED_NAMED_ASSET( "Generate Meshlets" );
854
855 PrimitiveMeshletResult primResult;
856
857 if (inputData.vertices.empty() || inputData.indices.empty()) {
858 result.primitiveResults.push_back(std::move(primResult));
859 continue;
860 }
861
862 auto inVertices = inputData.vertices;
863 auto inIndices = inputData.indices;
864
865 PLOGI << "=== GENERATE MESHLETS STARTED ===";
866 PLOGI << "Starting meshlet generation - vertices: " << inVertices.size()
867 << ", indices: " << inIndices.size();
868
869 // --- 0. Vertex Optimization ---
870 std::vector<uint32_t> remap( inVertices.size() );
871 size_t uniqueVertexCount;
872 {
873 TRACY_ZONE_SCOPED_NAMED( "Generate Vertex Remap" );
874 uniqueVertexCount = meshopt_generateVertexRemap(
875 remap.data(),
876 inIndices.data(),
877 inIndices.size(),
878 inVertices.data(),
879 inVertices.size(),
880 sizeof( Vertex )
881 );
882 }
883
884 {
885 std::vector<Vertex> uniqueVertices( uniqueVertexCount );
886 std::vector<uint32_t> uniqueIndices( inIndices.size() );
887
888 {
889 TRACY_ZONE_SCOPED_NAMED( "Remap vertex buffer" );
890 meshopt_remapVertexBuffer(
891 uniqueVertices.data(),
892 inVertices.data(),
893 inVertices.size(),
894 sizeof( Vertex ),
895 remap.data()
896 );
897 }
898 {
899 TRACY_ZONE_SCOPED_NAMED( "Remap index buffer" );
900 meshopt_remapIndexBuffer(
901 uniqueIndices.data(), inIndices.data(), inIndices.size(), remap.data()
902 );
903 }
904
905 // Store optimized data in result
906 primResult.optimizedData.vertices = std::move(uniqueVertices);
907 primResult.optimizedData.indices = std::move(uniqueIndices);
908 }
909
910 {
911 TRACY_ZONE_SCOPED_NAMED( "Optimize vertex cache" );
912 meshopt_optimizeVertexCache(
913 primResult.optimizedData.indices.data(),
914 primResult.optimizedData.indices.data(),
915 primResult.optimizedData.indices.size(),
916 primResult.optimizedData.vertices.size()
917 );
918 }
919 {
920 TRACY_ZONE_SCOPED_NAMED( "Optimize vertex fetch" );
921 meshopt_optimizeVertexFetch(
922 primResult.optimizedData.vertices.data(),
923 primResult.optimizedData.indices.data(),
924 primResult.optimizedData.indices.size(),
925 primResult.optimizedData.vertices.data(),
926 primResult.optimizedData.vertices.size(),
927 sizeof( Vertex )
928 );
929 }
930
931 // --- 1. Meshlet Generation ---
932 constexpr size_t maxVerticesPerMeshlet = 252;
933 constexpr size_t maxTrianglesPerMeshlet = 252;
934 constexpr float coneWeight = 0.5f;
935 size_t maxMeshlets;
936 {
937 TRACY_ZONE_SCOPED_NAMED( "Generate meshlet bounds" );
938 maxMeshlets = meshopt_buildMeshletsBound(
939 primResult.optimizedData.indices.size(), maxVerticesPerMeshlet, maxTrianglesPerMeshlet
940 );
941 }
942
943 primResult.meshletData.meshlets.resize( maxMeshlets );
944 primResult.meshletData.meshletVertices.resize(
945 maxMeshlets * maxVerticesPerMeshlet
946 );
947 primResult.meshletData.meshletTriangles.resize(
948 maxMeshlets * maxTrianglesPerMeshlet * 3
949 );
950 size_t meshletCount;
951 {
952 TRACY_ZONE_SCOPED_NAMED( "Build meshlets" );
953 meshletCount = meshopt_buildMeshlets(
954 primResult.meshletData.meshlets.data(),
955 primResult.meshletData.meshletVertices.data(),
956 primResult.meshletData.meshletTriangles.data(),
957 primResult.optimizedData.indices.data(),
958 primResult.optimizedData.indices.size(),
959 &primResult.optimizedData.vertices[0].position.x,
960 primResult.optimizedData.vertices.size(),
961 sizeof( Vertex ),
962 maxVerticesPerMeshlet,
963 maxTrianglesPerMeshlet,
964 coneWeight
965 );
966 }
967
968 // --- 2. Finalize and Optimize within Meshlets ---
969 {
970 TRACY_ZONE_SCOPED_NAMED( "Optimize meshlets" );
971 if ( meshletCount > 0 )
972 {
973 const auto & [vertex_offset, triangle_offset, vertex_count, triangle_count] =
974 primResult.meshletData.meshlets[meshletCount - 1];
975
976 primResult.meshletData.meshlets.resize( meshletCount );
977 primResult.meshletData.meshletVertices.resize(
978 vertex_offset + vertex_count
979 );
980 primResult.meshletData.meshletTriangles.resize(
981 triangle_offset + ( triangle_count * 3 )
982 );
983
984 for ( size_t i = 0; i < meshletCount; ++i )
985 {
986 const meshopt_Meshlet & m = primResult.meshletData.meshlets[i];
987 meshopt_optimizeMeshlet(
988 &primResult.meshletData.meshletVertices[m.vertex_offset],
989 &primResult.meshletData.meshletTriangles[m.triangle_offset],
990 m.triangle_count,
991 m.vertex_count
992 );
993 }
994 }
995 else
996 {
997 primResult.meshletData.meshlets.clear();
998 primResult.meshletData.meshletVertices.clear();
999 primResult.meshletData.meshletTriangles.clear();
1000 }
1001 }
1002
1003 // --- 3. Pre-unpack vertex/triangle data for GPU (avoids main thread stall) ---
1004 {
1005 TRACY_ZONE_SCOPED_NAMED( "Pre-unpack meshlet data" );
1006
1007 // Calculate total counts for pre-allocation
1008 size_t totalVertices = 0;
1009 size_t totalTriangles = 0;
1010 for (const auto& meshlet : primResult.meshletData.meshlets) {
1011 totalVertices += meshlet.vertex_count;
1012 totalTriangles += meshlet.triangle_count;
1013 }
1014
1015 primResult.unpackedData.vertices.reserve(totalVertices);
1016 primResult.unpackedData.triangles.reserve(totalTriangles);
1017
1018 const auto& primitiveVertices = primResult.optimizedData.vertices;
1019 const auto& meshletVerticesData = primResult.meshletData.meshletVertices;
1020 const auto& meshletTrianglesData = primResult.meshletData.meshletTriangles;
1021
1022 for (const auto& meshlet : primResult.meshletData.meshlets) {
1023 // Unpack vertices: resolve local indices to actual vertex data
1024 for (uint32_t i = 0; i < meshlet.vertex_count; ++i) {
1025 uint32_t local_vertex_index = meshletVerticesData[meshlet.vertex_offset + i];
1026 primResult.unpackedData.vertices.push_back(primitiveVertices[local_vertex_index]);
1027 }
1028
1029 // Pack triangles: store 3 x 8-bit indices in single uint32 (4x bandwidth reduction)
1030 for (uint32_t i = 0; i < meshlet.triangle_count; ++i) {
1031 const uint8_t* base_idx_ptr = &meshletTrianglesData[meshlet.triangle_offset + i * 3];
1032 primResult.unpackedData.triangles.push_back(
1033 Ecs::PackedTriangle::pack(base_idx_ptr[0], base_idx_ptr[1], base_idx_ptr[2]));
1034 }
1035 }
1036
1037 PLOGI << "Pre-unpacked " << totalVertices << " vertices, "
1038 << totalTriangles << " triangles on background thread";
1039 }
1040
1041 {
1042 TRACY_ZONE_SCOPED_NAMED( "Generate primitive bounds" );
1043 std::vector<glm::vec3> primitivePoints;
1044 primitivePoints.reserve( primResult.optimizedData.vertices.size() );
1045 for ( const auto & v : primResult.optimizedData.vertices )
1046 {
1047 primitivePoints.push_back( v.position );
1048 allMeshPoints.push_back( v.position );
1049 }
1050 Math::BoundingSphere::calculate( primResult.boundingSphere, primitivePoints );
1051 }
1052
1053 PLOGI << "Generated " << primResult.meshletData.meshlets.size()
1054 << " meshlets for primitive ("
1055 << primResult.optimizedData.indices.size() / 3 << " triangles).";
1056
1057 // Generate LOD hierarchy if mesh has enough meshlets
1058 if (primResult.meshletData.meshlets.size() >= 4) {
1059 TRACY_ZONE_SCOPED_NAMED( "Generate LOD Hierarchy" );
1060
1061 EngineCore::LodGenerationConfig lodConfig;
1062 lodConfig.maxTrianglesPerCluster = 128;
1063 lodConfig.simplificationRatio = 0.5f;
1064 lodConfig.targetError = 0.01f;
1065 lodConfig.maxLodLevels = 6;
1066 lodConfig.minTrianglesForLod = 256;
1067
1068 primResult.lodHierarchy = EngineCore::ClusterLodGenerator::generate(
1069 primResult.optimizedData.vertices,
1070 primResult.optimizedData.indices,
1071 primResult.meshletData.meshlets,
1072 primResult.meshletData.meshletVertices,
1073 primResult.meshletData.meshletTriangles,
1074 lodConfig
1075 );
1076
1077 if (primResult.lodHierarchy.has_value() &&
1078 primResult.lodHierarchy->lodLevelCount > 1) {
1079 VS_LOG(LogLOD, Warning, "[DIAG] Generated LOD hierarchy: {} levels, {} clusters, {} groups, {} total meshlets",
1080 primResult.lodHierarchy->lodLevelCount,
1081 primResult.lodHierarchy->clusters.size(),
1082 primResult.lodHierarchy->groups.size(),
1083 primResult.lodHierarchy->meshletData.meshlets.size());
1084 } else {
1085 VS_LOG(LogLOD, Warning, "[DIAG] LOD generation returned empty (triangles={}, minRequired=256)",
1086 primResult.optimizedData.indices.size() / 3);
1087 }
1088 } else {
1089 VS_LOG(LogLOD, Warning, "[DIAG] Skipping LOD: only {} meshlets (need >= 4)",
1090 primResult.meshletData.meshlets.size());
1091 }
1092
1093 result.primitiveResults.push_back(std::move(primResult));
1094 }
1095
1096 // Calculate the overall mesh bounding sphere
1097 {
1098 TRACY_ZONE_SCOPED_NAMED_ASSET( "Generate mesh bounds" );
1099 Math::BoundingSphere::calculate( result.meshBoundingSphere, allMeshPoints );
1100 }
1101
1102 return result;
1103 }
1104 ) );
1105
1106 it = primitiveDataFutures.erase( it );
1107 }
1108 }
1109
1110 // Handle meshlet generation futures based on async mode
1111 if ( !async && !meshletGenerationFutures.empty() )
1112 {
1113 // Synchronous: wait for all meshlet generation tasks to complete
1114 TRACY_ZONE_SCOPED_NAMED( "Wait for meshlet generation" );
1115 for ( auto & future : meshletGenerationFutures )
1116 {
1117 auto completedGeneration = future.get();
1118
1119 // Queue the completed generation for processing at a safe point
1120 pendingMeshletCompletions_.push_back(std::move(completedGeneration));
1121 }
1122 meshletGenerationFutures.clear();
1123 }
1124 }
1125
1126 std::vector<CompletedMeshletGeneration> ModelAssetPipeline::takePendingMeshletCompletions()
1127 {
1128 std::vector<CompletedMeshletGeneration> result = std::move(pendingMeshletCompletions_);
1130 return result;
1131 }
1132
1134 const std::filesystem::path & modelPath )
1135 {
1136 TRACY_ZONE_SCOPED_NAMED_ASSET( "Process glTF Textures" );
1137
1138 if ( !modelAsset || !modelAsset->model.has_value() )
1139 {
1140 PLOGW << "Cannot process textures: model asset is null or model not loaded";
1141 return;
1142 }
1143
1144 tinygltf::Model & model = modelAsset->model.value();
1145
1146 for ( size_t i = 0; i < model.images.size(); ++i )
1147 {
1148 TRACY_ZONE_SCOPED_NAMED_ASSET( "Processing texture" );
1149 tinygltf::Image & image = model.images[i];
1150
1151 // Determine the texture path - use URI for external files, or generate path for embedded
1152 std::filesystem::path texturePath;
1153 {
1154 TRACY_ZONE_SCOPED_NAMED( "Getting Texture Path" );
1155 if ( !image.uri.empty() )
1156 {
1157 // External image file - resolve relative to the model file
1158 texturePath = modelPath.parent_path() / image.uri;
1159 }
1160 else if ( !image.name.empty() )
1161 {
1162 // Embedded image with a name - use model path + name as key
1163 texturePath = modelPath.parent_path() / image.name;
1164 }
1165 else
1166 {
1167 // Embedded image without name - use model path + index as key
1168 texturePath = modelPath.parent_path() / ( "image_" + std::to_string( i ) );
1169 }
1170 }
1171
1172 // Check if texture is already registered (avoid duplicates)
1173 if ( textureAssetManager->getAsset( texturePath ).has_value() )
1174 {
1175 PLOGD << "Texture already registered: " << texturePath;
1176 continue;
1177 }
1178
1179 PLOGD << "processTextures - imageIndex=" << i << " path=" << texturePath << " dims=" << image.width << "x" << image.height;
1180
1181 // Create and register the texture asset
1182 auto * textureAsset = new EngineCore::GltfTextureAsset();
1183 textureAssetManager->add( texturePath, textureAsset );
1184 modelAsset->registerCreatedAssetWithModel( textureAsset );
1185
1186 PLOGD << "processTextures - created textureAsset with entity=" << static_cast<uint32_t>(textureAsset->getEntity()) << " for path=" << texturePath;
1187
1188 // Set the image data - tinygltf has already loaded the pixel data
1189 textureAsset->setImage( std::move( image ) );
1190
1191 // Notify RenderingDataManager to create Vulkan texture and register with AssetManager
1193 {
1194 renderingDataManager->onTextureLoaded( textureAsset, texturePath );
1195 }
1196 }
1197
1198 // Process lightmap textures from node extensions
1199 // Lightmaps are EXR files specified in VulkanSchnee extension, not part of GLTF images array
1200 {
1201 TRACY_ZONE_SCOPED_NAMED_ASSET( "Process lightmap textures" );
1202 std::set<std::filesystem::path> processedLightmaps; // Avoid duplicates
1203
1204 // Pre-register lightmap handles BEFORE loading, so onTextureLoaded can find them
1205 // This mirrors the pre-registration we do for regular textures
1206 EngineCore::TextureHandleRegistry* textureRegistry = assetManager_ ? assetManager_->getTextureHandleRegistry() : nullptr;
1207 if ( textureRegistry )
1208 {
1209 for ( const tinygltf::Node & node : model.nodes )
1210 {
1211 if ( node.extensions.empty() )
1212 continue;
1213
1214 EngineCore::GltfLoader::StaticMeshExtensions nodeExtensions( node.extensions );
1215
1216 if ( nodeExtensions.vulkanSchneeExtension.has_value() &&
1217 nodeExtensions.vulkanSchneeExtension->hasLightmapProperties() &&
1218 nodeExtensions.vulkanSchneeExtension->lightProperties.has_value() &&
1219 nodeExtensions.vulkanSchneeExtension->lightProperties->getHasLightmaps() &&
1220 nodeExtensions.vulkanSchneeExtension->lightmapProperties->isLightmapPathValid() )
1221 {
1222 const auto & relativeLightmapPath = nodeExtensions.vulkanSchneeExtension->lightmapProperties->lightmapPath;
1223 std::filesystem::path lightmapPath = modelPath.parent_path() / relativeLightmapPath;
1224 std::filesystem::path absoluteLightmapPath = std::filesystem::absolute( lightmapPath );
1225
1226 // Pre-create handle with Lightmap type (HDR float format)
1227 textureRegistry->getOrCreateHandle<EngineCore::TextureType::Lightmap>( absoluteLightmapPath );
1228 }
1229 }
1230 PLOGI << "Pre-registered lightmap texture handles";
1231 }
1232
1233 for ( const tinygltf::Node & node : model.nodes )
1234 {
1235 if ( node.extensions.empty() )
1236 continue;
1237
1238 EngineCore::GltfLoader::StaticMeshExtensions nodeExtensions( node.extensions );
1239
1240 // Check if node has lightmap properties
1241 if ( nodeExtensions.vulkanSchneeExtension.has_value() &&
1242 nodeExtensions.vulkanSchneeExtension->hasLightmapProperties() &&
1243 nodeExtensions.vulkanSchneeExtension->lightProperties.has_value() &&
1244 nodeExtensions.vulkanSchneeExtension->lightProperties->getHasLightmaps() &&
1245 nodeExtensions.vulkanSchneeExtension->lightmapProperties->isLightmapPathValid() )
1246 {
1247 // Resolve lightmap path relative to the GLTF file
1248 const auto & relativeLightmapPath = nodeExtensions.vulkanSchneeExtension->lightmapProperties->lightmapPath;
1249 std::filesystem::path lightmapPath = modelPath.parent_path() / relativeLightmapPath;
1250
1251 // Resolve to absolute path for consistent handling
1252 std::filesystem::path absoluteLightmapPath = std::filesystem::absolute( lightmapPath );
1253
1254 // Skip if already processed
1255 if ( processedLightmaps.count( absoluteLightmapPath ) > 0 )
1256 continue;
1257 processedLightmaps.insert( absoluteLightmapPath );
1258
1259 // Check if texture is already registered
1260 if ( textureAssetManager->getAsset( absoluteLightmapPath ).has_value() )
1261 {
1262 PLOGD << "Lightmap texture already registered: " << absoluteLightmapPath;
1263 continue;
1264 }
1265
1266 // Check if the lightmap file exists
1267 PLOGI << "Lightmap path resolution: relative=" << relativeLightmapPath
1268 << " resolved=" << lightmapPath
1269 << " absolute=" << absoluteLightmapPath;
1270
1271 if ( !std::filesystem::exists( absoluteLightmapPath ) )
1272 {
1273 PLOGW << "Lightmap EXR file not found: " << absoluteLightmapPath;
1274 continue;
1275 }
1276
1277 // Load the EXR lightmap texture (use absolute path for reliability)
1278 PLOGI << "Loading lightmap EXR: " << absoluteLightmapPath;
1279 EngineCore::ExrLoader exrLoader;
1280 tinygltf::Image lightmapImage = exrLoader.load( absoluteLightmapPath );
1281
1282 // Check if EXR loading succeeded
1283 if ( lightmapImage.image.empty() || lightmapImage.width == 0 || lightmapImage.height == 0 )
1284 {
1285 PLOGW << "Failed to load lightmap EXR (empty image): " << lightmapPath;
1286 continue;
1287 }
1288
1289 // Create and register the texture asset (use absolute path as key for consistency)
1290 auto * textureAsset = new EngineCore::GltfTextureAsset();
1291 textureAssetManager->add( absoluteLightmapPath, textureAsset );
1292 modelAsset->registerCreatedAssetWithModel( textureAsset );
1293
1294 // Set the image data
1295 textureAsset->setImage( std::move( lightmapImage ) );
1296
1297 // Notify RenderingDataManager to create Vulkan texture and register with AssetManager
1299 {
1300 renderingDataManager->onTextureLoaded( textureAsset, absoluteLightmapPath );
1301 }
1302
1303 PLOGI << "Loaded lightmap texture: " << absoluteLightmapPath;
1304 }
1305 }
1306 }
1307 }
1308} // namespace Ecs
#define TRACY_ZONE_SCOPED_NAMED_ASSET(name)
#define TRACY_ZONE_SCOPED_NAMED_D_ASSET(stream_expr)
#define TRACY_ZONE_SCOPED_NAMED(name)
entt::entity data
Definition Asset.h:98
entt::entity getEntity() const
Getter for data.
Definition Asset.h:125
NamedThreadPool * assetLoaderPool
void submitLoadModel(bool async=true)
void processMeshData(bool async=true)
std::vector< Ecs::CompletedMeshletGeneration > pendingMeshletCompletions_
std::vector< Ecs::CompletedMeshletGeneration > takePendingMeshletCompletions()
Get and clear pending meshlet completions. Call this at a safe point to retrieve completed meshlet ge...
ModelAssetPipeline(EngineCore::MeshAssetManager *meshAssetManager, EngineCore::MaterialAssetManager *materialAssetManager, EngineCore::ModelAssetManager *modelAssetManager, EngineCore::TextureAssetManager *textureAssetManager, NamedThreadPool *assetLoaderPool, EngineCore::AssetManager *assetManager=nullptr)
std::vector< std::filesystem::path > assetQueue
EngineCore::TextureAssetManager * textureAssetManager
std::vector< std::future< Ecs::CompletedMeshletGeneration > > meshletGenerationFutures
void processModel(bool async=true)
void processTextures(EngineCore::ModelAsset *modelAsset, const std::filesystem::path &modelPath)
Extracts and registers texture assets from loaded glTF models.
EngineCore::GltfLoader * loader
std::vector< Ecs::GltfModelFuture > modelLoadingFutures
std::vector< std::future< Ecs::PrimitiveDataLoading > > primitiveDataFutures
void tick(bool async=true)
EngineCore::MeshAssetManager * meshAssetManager
NamedThreadPool * threadPool
std::vector< std::future< Ecs::MaterialLoadingData > > materialFutures
void submitAsset(const std::filesystem::path &asset)
Requests the load of a gltf file.
void processMaterial(bool async=true)
EngineCore::RenderingDataManager * renderingDataManager
EngineCore::MaterialAssetManager * materialAssetManager
EngineCore::AssetManager * assetManager_
For TextureHandleRegistry access.
EngineCore::ModelAssetManager * modelAssetManager
static entt::registry & get()
Gets the registry for all components.
NamedThreadPool * assetLoaderPool
EngineCore::TextureAssetManager * textureAssetManager
void tick(bool async=true)
Advances the state machine for the entire pipeline.
void requestExrHeaders(bool async=true)
std::vector< std::filesystem::path > assetQueue
std::vector< Ecs::AssetLoadingImage > imageFutures
EngineCore::RenderingDataManager * renderingDataManager
std::vector< Ecs::ExrHeaderFuture > headerFutures
void retrieveImageData(bool async=true)
void loadImageData(bool async=true)
TextureAssetPipeline(EngineCore::TextureAssetManager *textureAssetManager, NamedThreadPool *assetLoaderPool)
void submitAsset(const std::filesystem::path &asset)
submit the path to an asset relative from the executable to load
Material data for an Object which displays a flat color.
Dynamic textures material with PBR and lightmap support.
Loader for EXR (OpenEXR) image files.
Definition ExrLoader.h:81
tinygltf::Image load(const std::filesystem::path &filename)
Load a complete EXR image file.
Definition ExrLoader.cpp:40
ExrHeader loadHeader(std::filesystem::path path)
Load only the header information from an EXR file.
Extension of TextureAsset and stores the Exr header information as well as the texture data.
Extension of TextureAsset for images loaded from glTF files (PNG/JPG)
L1 Spherical Harmoics shader.
the material asset is another wrapper for asset data which is stored in entt. It has the EngineCore::...
Stores mesh data with their primitives.
Definition MeshAsset.h:54
The mesh asset stores geometry data and.
Definition MeshAsset.h:15
The model asset is used to bind together all files of one gtlf model. The idea is that textures,...
void registerCreatedAssetWithModel(Asset::AssetBase *asset)
Registers an asset like a texture or 3d model with this gltf model.
std::optional< tinygltf::Model > model
std::filesystem::path path
Moveable diffuse shader with PBR and lightmap support.
Material data for an object which displays its normals.
static bool doesGltfFileExist(const std::filesystem::path &path)
Definition Path.cpp:19
Static lightmap material with PBR support.
Central registry for texture handles, providing O(1) descriptor index lookup.
TypedTextureHandle< Type > getOrCreateHandle(const std::filesystem::path &path)
Get or create a typed handle for a texture path.
Tracy-named thread pool using BS_tracy::tracy_thread_pool.
Data structs for the Entity Component System.
T UnpackOptional(std::optional< T > var)
Definition Optional.h:7
TextureType
Represents the semantic type of a texture, determining its Vulkan format.
Definition TextureType.h:18
@ Lightmap
HDR baked lighting data (SFLOAT format)
Definition TextureType.h:23
@ Emissive
Color data with gamma (SRGB format for 8-bit)
Definition TextureType.h:22
@ BaseColor
Color data with gamma (SRGB format for 8-bit)
Definition TextureType.h:19
@ Normal
Linear data for normal maps (UNORM format for 8-bit)
Definition TextureType.h:20
@ MetallicRoughness
Linear data for PBR parameters (UNORM format for 8-bit)
Definition TextureType.h:21
@ MOVABLE_DIFFUSE_SHADER
std::string getAssetHandle() const
Definition AssetPath.h:19
Complete result of meshlet generation for all primitives of a mesh. This is returned from the backgro...
Definition EcsData.h:266
EngineCore::PipelineNames type
Definition EcsData.h:85
EngineCore::MaterialData * data
Definition EcsData.h:86
std::vector< Ecs::MeshPrimitive > primitives
Definition EcsData.h:284
std::vector< Asset::Path > materialPaths
Resolved material paths per primitive.
Definition EcsData.h:187
std::vector< PrimitiveData > data
Definition EcsData.h:186
Wrapper for EXR file header information with owned data.
Definition ExrLoader.h:42
Stores the material data which is relevant for our current set of shaders.
Definition GltfLoader.h:232
Parses all extensions and extracts recognized extensions.
Definition GltfLoader.h:217
std::optional< VulkanSchneeExtension > vulkanSchneeExtension
Definition GltfLoader.h:221