230 std::optional<tinygltf::Model> model;
233 if ( it->future.valid() &&
234 it->future.wait_for( std::chrono::seconds( 0 ) ) == std::future_status::ready )
236 model = std::move( it->future.get() );
246 model = std::move( it->future.get() );
250 modelAsset->
model = std::move( model.value() );
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 )
258 const tinygltf::Image & image = modelAsset->
model->images[i];
259 std::filesystem::path texturePath;
261 if ( !image.uri.empty() )
263 texturePath = it->path.parent_path() / image.uri;
265 else if ( !image.name.empty() )
267 texturePath = it->path.parent_path() / image.name;
271 texturePath = it->path.parent_path() / (
"image_" + std::to_string( i ) );
273 imageTexturePaths.push_back( std::move( texturePath ) );
282 if ( textureRegistry )
285 std::unordered_map<int, EngineCore::TextureType> imageTypeMap;
287 for (
const tinygltf::Material & material : modelAsset->
model->materials )
290 int idx = material.pbrMetallicRoughness.baseColorTexture.index;
291 if ( idx >= 0 && idx <
static_cast<int>( modelAsset->
model->textures.size() ) )
293 int source = modelAsset->
model->textures[idx].source;
299 idx = material.normalTexture.index;
300 if ( idx >= 0 && idx <
static_cast<int>( modelAsset->
model->textures.size() ) )
302 int source = modelAsset->
model->textures[idx].source;
308 idx = material.pbrMetallicRoughness.metallicRoughnessTexture.index;
309 if ( idx >= 0 && idx <
static_cast<int>( modelAsset->
model->textures.size() ) )
311 int source = modelAsset->
model->textures[idx].source;
317 idx = material.emissiveTexture.index;
318 if ( idx >= 0 && idx <
static_cast<int>( modelAsset->
model->textures.size() ) )
320 int source = modelAsset->
model->textures[idx].source;
327 for (
size_t i = 0; i < imageTexturePaths.size(); ++i )
329 auto typeIt = imageTypeMap.find(
static_cast<int>( i ) );
352 PLOGI <<
"Pre-registered " << imageTexturePaths.size() <<
" texture handles before loading";
363 for (
const tinygltf::Material & material : modelAsset->
model->materials )
366 auto materialPath =
Asset::Path( it->path, material.name );
367 PLOGI <<
"processModel: Registering material '" << material.name
368 <<
"' with path handle: " << materialPath.getAssetHandle();
375 PLOGI <<
"processModel: Processing material '" << material.name
376 <<
"' from model '" << it->path.string()
378 <<
" textureRegistry=" << (textureRegistry ?
"valid" :
"NULL");
381 const int baseColorTexIndex = material.pbrMetallicRoughness.baseColorTexture.index;
382 if ( baseColorTexIndex >= 0 &&
383 baseColorTexIndex <
static_cast<int>( modelAsset->
model->textures.size() ) )
385 const tinygltf::Texture & texture = modelAsset->
model->textures[baseColorTexIndex];
386 if ( texture.source >= 0 &&
387 texture.source <
static_cast<int>( imageTexturePaths.size() ) )
389 const std::filesystem::path & texturePath = imageTexturePaths[texture.source];
390 materialAsset->setBaseColorTexturePath( texturePath );
392 if ( textureRegistry )
395 materialAsset->setBaseColorTextureHandle( handle );
397 PLOGD <<
"Material '" << material.name <<
"' uses base color texture: " << texturePath;
402 const int normalTexIndex = material.normalTexture.index;
403 if ( normalTexIndex >= 0 &&
404 normalTexIndex <
static_cast<int>( modelAsset->
model->textures.size() ) )
406 const tinygltf::Texture & texture = modelAsset->
model->textures[normalTexIndex];
407 if ( texture.source >= 0 &&
408 texture.source <
static_cast<int>( imageTexturePaths.size() ) )
410 const std::filesystem::path & texturePath = imageTexturePaths[texture.source];
411 materialAsset->setNormalTexturePath( texturePath );
413 if ( textureRegistry )
416 materialAsset->setNormalTextureHandle( handle );
418 PLOGD <<
"Material '" << material.name <<
"' uses normal texture: " << texturePath;
423 const int metallicRoughnessTexIndex = material.pbrMetallicRoughness.metallicRoughnessTexture.index;
424 if ( metallicRoughnessTexIndex >= 0 &&
425 metallicRoughnessTexIndex <
static_cast<int>( modelAsset->
model->textures.size() ) )
427 const tinygltf::Texture & texture = modelAsset->
model->textures[metallicRoughnessTexIndex];
428 if ( texture.source >= 0 &&
429 texture.source <
static_cast<int>( imageTexturePaths.size() ) )
431 const std::filesystem::path & texturePath = imageTexturePaths[texture.source];
432 materialAsset->setMetallicRoughnessTexturePath( texturePath );
434 if ( textureRegistry )
437 materialAsset->setMetallicRoughnessTextureHandle( handle );
439 PLOGD <<
"Material '" << material.name <<
"' uses metallic-roughness texture: " << texturePath;
444 const int emissiveTexIndex = material.emissiveTexture.index;
445 if ( emissiveTexIndex >= 0 &&
446 emissiveTexIndex <
static_cast<int>( modelAsset->
model->textures.size() ) )
448 const tinygltf::Texture & texture = modelAsset->
model->textures[emissiveTexIndex];
449 if ( texture.source >= 0 &&
450 texture.source <
static_cast<int>( imageTexturePaths.size() ) )
452 const std::filesystem::path & texturePath = imageTexturePaths[texture.source];
453 materialAsset->setEmissiveTexturePath( texturePath );
455 if ( textureRegistry )
458 materialAsset->setEmissiveTextureHandle( handle );
460 PLOGD <<
"Material '" << material.name <<
"' uses emissive texture: " << texturePath;
468 int materialIndex =
static_cast<int>( &material - &modelAsset->
model->materials[0] );
471 for (
const tinygltf::Node & node : modelAsset->
model->nodes )
473 if ( node.mesh < 0 || node.mesh >=
static_cast<int>( modelAsset->
model->meshes.size() ) )
476 const tinygltf::Mesh & mesh = modelAsset->
model->meshes[node.mesh];
479 bool usesMaterial =
false;
480 for (
const tinygltf::Primitive & primitive : mesh.primitives )
482 if ( primitive.material == materialIndex )
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 );
505 if ( textureRegistry )
508 materialAsset->setLightmapTextureHandle( handle );
510 PLOGD <<
"Material '" << material.name <<
"' uses lightmap texture: " << lightmapPath;
521 auto asset = Asset::Path( assetPath, material.name );
524 EngineCore::GltfLoader::StaticMeshExtensions meshExtensions{};
526 *modelAsset->
model, material, meshExtensions);
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;
536 PLOGI <<
"Material '" << material.name <<
"' has NO extension, defaulting to NORMALS_SHADER";
542 using T = std::decay_t<decltype(arg)>;
545 gltfMaterial.getMaterialDataRaw()
548 return { std::move( asset ), type, data };
559 for (
const tinygltf::Node & node : modelAsset->model->nodes )
562 if ( node.mesh < 0 || node.mesh >=
static_cast<int>( modelAsset->model->meshes.size() ) )
569 if ( node.name.find(
"VarC") != std::string::npos || node.name.find(
"scq9") != std::string::npos )
571 PLOGW <<
"*** PROCESSING VarC NODE: index=" << nodeIndex <<
" name=" << node.name
572 <<
" mesh=" << node.mesh <<
" ***";
575 const tinygltf::Mesh & mesh = modelAsset->model->meshes[node.mesh];
579 const auto meshPath =
Asset::Path( it->path, node.name );
581 modelAsset->registerCreatedAssetWithModel( meshAsset );
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() );
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 )
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() ) )
606 const auto & matName = modelAsset->model->materials[matIdx].name;
607 materialPaths.emplace_back( meshPath.getFilePath(), matName );
608 PLOGI <<
" -> created path: " << meshPath.getFilePath().string() <<
" + " << matName;
612 materialPaths.emplace_back();
613 PLOGI <<
" -> EMPTY path (matIdx out of range or -1)";
617 auto vertexData = EngineCore::GltfLoader::GltfVertexData(
618 modelAsset->model->buffers,
619 modelAsset->model->bufferViews,
620 modelAsset->model->accessors,
623 primitiveDataFutures.emplace_back(
624 std::move( pool->submit_task(
625 [gltfLoader, vertexData]() -> PrimitiveData
627 TRACY_ZONE_SCOPED_NAMED_ASSET(
"Loading Primitive Data" );
628 return gltfLoader->loadPrimitiveData( vertexData );
635 size_t emptyCount = 0;
636 for (
const auto& path : materialPaths) {
637 if (path.empty()) ++emptyCount;
639 PLOGI <<
"=== Material paths summary for " << meshPath.getAssetHandle()
640 <<
": total=" << materialPaths.size() <<
" empty=" << emptyCount <<
" ===";
642 std::vector<PrimitiveData> data;
646 data.emplace_back( primitiveDataFuture.get() );
648 return { meshPath, std::move( data ), std::move( materialPaths ) };
654 it = modelLoadingFutures.erase( it );
765 if ( it->valid() && it->wait_for( std::chrono::seconds( 0 ) ) == std::future_status::ready )
767 auto completedGeneration = it->get();
768 PLOGD <<
"Meshlet generation completed for: " << completedGeneration.meshPath.getFilePath();
792 if ( it->valid() && it->wait_for( std::chrono::seconds( 0 ) ) == std::future_status::ready )
794 primitiveDataLoadingData = std::move( it->get() );
804 primitiveDataLoadingData = std::move( it->get() );
814 PLOGI <<
"processMeshData: Setting material paths for " << primitiveDataLoadingData.
data.size()
816 <<
" entity=" <<
static_cast<uint32_t
>(meshAsset->
data) <<
")";
817 for ( uint32_t i = 0; i < primitiveDataLoadingData.
data.size(); ++i )
824 PLOGI <<
" primitive[" << i <<
"] materialPath = "
825 << (primitiveDataLoadingData.
materialPaths[i].empty() ?
"(empty)" : primitiveDataLoadingData.
materialPaths[i].getAssetHandle());
829 PLOGW <<
" primitive[" << i <<
"] has no materialPath (index out of range)";
838 entt::entity meshEntity = meshAsset->
getEntity();
841 [path = primitiveDataLoadingData.
path, meshEntity,
844 CompletedMeshletGeneration result;
845 result.meshPath = path;
846 result.meshEntity = meshEntity;
847 result.primitiveResults.reserve(inputPrimitives.size());
849 std::vector<glm::vec3> allMeshPoints;
851 for (auto& inputData : inputPrimitives)
853 TRACY_ZONE_SCOPED_NAMED_ASSET(
"Generate Meshlets" );
855 PrimitiveMeshletResult primResult;
857 if (inputData.vertices.empty() || inputData.indices.empty()) {
858 result.primitiveResults.push_back(std::move(primResult));
862 auto inVertices = inputData.vertices;
863 auto inIndices = inputData.indices;
865 PLOGI <<
"=== GENERATE MESHLETS STARTED ===";
866 PLOGI <<
"Starting meshlet generation - vertices: " << inVertices.size()
867 <<
", indices: " << inIndices.size();
870 std::vector<uint32_t> remap( inVertices.size() );
871 size_t uniqueVertexCount;
873 TRACY_ZONE_SCOPED_NAMED(
"Generate Vertex Remap" );
874 uniqueVertexCount = meshopt_generateVertexRemap(
885 std::vector<Vertex> uniqueVertices( uniqueVertexCount );
886 std::vector<uint32_t> uniqueIndices( inIndices.size() );
889 TRACY_ZONE_SCOPED_NAMED(
"Remap vertex buffer" );
890 meshopt_remapVertexBuffer(
891 uniqueVertices.data(),
899 TRACY_ZONE_SCOPED_NAMED(
"Remap index buffer" );
900 meshopt_remapIndexBuffer(
901 uniqueIndices.data(), inIndices.data(), inIndices.size(), remap.data()
906 primResult.optimizedData.vertices = std::move(uniqueVertices);
907 primResult.optimizedData.indices = std::move(uniqueIndices);
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()
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(),
932 constexpr size_t maxVerticesPerMeshlet = 252;
933 constexpr size_t maxTrianglesPerMeshlet = 252;
934 constexpr float coneWeight = 0.5f;
937 TRACY_ZONE_SCOPED_NAMED(
"Generate meshlet bounds" );
938 maxMeshlets = meshopt_buildMeshletsBound(
939 primResult.optimizedData.indices.size(), maxVerticesPerMeshlet, maxTrianglesPerMeshlet
943 primResult.meshletData.meshlets.resize( maxMeshlets );
944 primResult.meshletData.meshletVertices.resize(
945 maxMeshlets * maxVerticesPerMeshlet
947 primResult.meshletData.meshletTriangles.resize(
948 maxMeshlets * maxTrianglesPerMeshlet * 3
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(),
962 maxVerticesPerMeshlet,
963 maxTrianglesPerMeshlet,
970 TRACY_ZONE_SCOPED_NAMED(
"Optimize meshlets" );
971 if ( meshletCount > 0 )
973 const auto & [vertex_offset, triangle_offset, vertex_count, triangle_count] =
974 primResult.meshletData.meshlets[meshletCount - 1];
976 primResult.meshletData.meshlets.resize( meshletCount );
977 primResult.meshletData.meshletVertices.resize(
978 vertex_offset + vertex_count
980 primResult.meshletData.meshletTriangles.resize(
981 triangle_offset + ( triangle_count * 3 )
984 for ( size_t i = 0; i < meshletCount; ++i )
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],
997 primResult.meshletData.meshlets.clear();
998 primResult.meshletData.meshletVertices.clear();
999 primResult.meshletData.meshletTriangles.clear();
1005 TRACY_ZONE_SCOPED_NAMED(
"Pre-unpack meshlet data" );
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;
1015 primResult.unpackedData.vertices.reserve(totalVertices);
1016 primResult.unpackedData.triangles.reserve(totalTriangles);
1018 const auto& primitiveVertices = primResult.optimizedData.vertices;
1019 const auto& meshletVerticesData = primResult.meshletData.meshletVertices;
1020 const auto& meshletTrianglesData = primResult.meshletData.meshletTriangles;
1022 for (const auto& meshlet : primResult.meshletData.meshlets) {
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]);
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]));
1037 PLOGI <<
"Pre-unpacked " << totalVertices <<
" vertices, "
1038 << totalTriangles <<
" triangles on background thread";
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 )
1047 primitivePoints.push_back( v.position );
1048 allMeshPoints.push_back( v.position );
1050 Math::BoundingSphere::calculate( primResult.boundingSphere, primitivePoints );
1053 PLOGI <<
"Generated " << primResult.meshletData.meshlets.size()
1054 <<
" meshlets for primitive ("
1055 << primResult.optimizedData.indices.size() / 3 <<
" triangles).";
1058 if (primResult.meshletData.meshlets.size() >= 4) {
1059 TRACY_ZONE_SCOPED_NAMED(
"Generate LOD Hierarchy" );
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;
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,
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());
1085 VS_LOG(LogLOD, Warning,
"[DIAG] LOD generation returned empty (triangles={}, minRequired=256)",
1086 primResult.optimizedData.indices.size() / 3);
1089 VS_LOG(LogLOD, Warning,
"[DIAG] Skipping LOD: only {} meshlets (need >= 4)",
1090 primResult.meshletData.meshlets.size());
1093 result.primitiveResults.push_back(std::move(primResult));
1098 TRACY_ZONE_SCOPED_NAMED_ASSET(
"Generate mesh bounds" );
1099 Math::BoundingSphere::calculate( result.meshBoundingSphere, allMeshPoints );
1111 if ( !async && !meshletGenerationFutures.empty() )
1115 for (
auto & future : meshletGenerationFutures )
1117 auto completedGeneration = future.get();
1120 pendingMeshletCompletions_.push_back(std::move(completedGeneration));
1122 meshletGenerationFutures.clear();
1134 const std::filesystem::path & modelPath )
1138 if ( !modelAsset || !modelAsset->
model.has_value() )
1140 PLOGW <<
"Cannot process textures: model asset is null or model not loaded";
1144 tinygltf::Model & model = modelAsset->
model.value();
1146 for (
size_t i = 0; i < model.images.size(); ++i )
1149 tinygltf::Image & image = model.images[i];
1152 std::filesystem::path texturePath;
1155 if ( !image.uri.empty() )
1158 texturePath = modelPath.parent_path() / image.uri;
1160 else if ( !image.name.empty() )
1163 texturePath = modelPath.parent_path() / image.name;
1168 texturePath = modelPath.parent_path() / (
"image_" + std::to_string( i ) );
1175 PLOGD <<
"Texture already registered: " << texturePath;
1179 PLOGD <<
"processTextures - imageIndex=" << i <<
" path=" << texturePath <<
" dims=" << image.width <<
"x" << image.height;
1186 PLOGD <<
"processTextures - created textureAsset with entity=" <<
static_cast<uint32_t
>(textureAsset->getEntity()) <<
" for path=" << texturePath;
1189 textureAsset->setImage( std::move( image ) );
1202 std::set<std::filesystem::path> processedLightmaps;
1207 if ( textureRegistry )
1209 for (
const tinygltf::Node & node : model.nodes )
1211 if ( node.extensions.empty() )
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 );
1230 PLOGI <<
"Pre-registered lightmap texture handles";
1233 for (
const tinygltf::Node & node : model.nodes )
1235 if ( node.extensions.empty() )
1248 const auto & relativeLightmapPath = nodeExtensions.
vulkanSchneeExtension->lightmapProperties->lightmapPath;
1249 std::filesystem::path lightmapPath = modelPath.parent_path() / relativeLightmapPath;
1252 std::filesystem::path absoluteLightmapPath = std::filesystem::absolute( lightmapPath );
1255 if ( processedLightmaps.count( absoluteLightmapPath ) > 0 )
1257 processedLightmaps.insert( absoluteLightmapPath );
1262 PLOGD <<
"Lightmap texture already registered: " << absoluteLightmapPath;
1267 PLOGI <<
"Lightmap path resolution: relative=" << relativeLightmapPath
1268 <<
" resolved=" << lightmapPath
1269 <<
" absolute=" << absoluteLightmapPath;
1271 if ( !std::filesystem::exists( absoluteLightmapPath ) )
1273 PLOGW <<
"Lightmap EXR file not found: " << absoluteLightmapPath;
1278 PLOGI <<
"Loading lightmap EXR: " << absoluteLightmapPath;
1280 tinygltf::Image lightmapImage = exrLoader.
load( absoluteLightmapPath );
1283 if ( lightmapImage.image.empty() || lightmapImage.width == 0 || lightmapImage.height == 0 )
1285 PLOGW <<
"Failed to load lightmap EXR (empty image): " << lightmapPath;
1295 textureAsset->setImage( std::move( lightmapImage ) );
1303 PLOGI <<
"Loaded lightmap texture: " << absoluteLightmapPath;