#include "stb_image.h" #include #include #include #include "vk_loader.h" #include "core/texture_cache.h" #include "core/vk_engine.h" #include "render/materials.h" #include "core/vk_initializers.h" #include "core/vk_types.h" #include "core/config.h" #include #include #include #include #include #include #include "tangent_space.h" #include "mesh_bvh.h" //> loadimg std::optional load_image(VulkanEngine *engine, fastgltf::Asset &asset, fastgltf::Image &image, bool srgb) { AllocatedImage newImage{}; int width, height, nrChannels; std::visit( fastgltf::visitor{ [](auto &arg) { }, [&](fastgltf::sources::URI &filePath) { assert(filePath.fileByteOffset == 0); // We don't support offsets with stbi. assert(filePath.uri.isLocalPath()); // We're only capable of loading // local files. const std::string path(filePath.uri.path().begin(), filePath.uri.path().end()); // Thanks C++. unsigned char *data = stbi_load(path.c_str(), &width, &height, &nrChannels, 4); if (data) { VkExtent3D imagesize; imagesize.width = width; imagesize.height = height; imagesize.depth = 1; VkFormat fmt = srgb ? VK_FORMAT_R8G8B8A8_SRGB : VK_FORMAT_R8G8B8A8_UNORM; newImage = engine->_resourceManager->create_image( data, imagesize, fmt, VK_IMAGE_USAGE_SAMPLED_BIT, false); // Name the allocation for diagnostics if (vmaDebugEnabled()) vmaSetAllocationName(engine->_deviceManager->allocator(), newImage.allocation, path.c_str()); stbi_image_free(data); } }, [&](fastgltf::sources::Vector &vector) { unsigned char *data = stbi_load_from_memory(vector.bytes.data(), static_cast(vector.bytes.size()), &width, &height, &nrChannels, 4); if (data) { VkExtent3D imagesize; imagesize.width = width; imagesize.height = height; imagesize.depth = 1; VkFormat fmt = srgb ? VK_FORMAT_R8G8B8A8_SRGB : VK_FORMAT_R8G8B8A8_UNORM; newImage = engine->_resourceManager->create_image( data, imagesize, fmt, VK_IMAGE_USAGE_SAMPLED_BIT, false); if (vmaDebugEnabled()) vmaSetAllocationName(engine->_deviceManager->allocator(), newImage.allocation, "gltf.vector.image"); stbi_image_free(data); } }, [&](fastgltf::sources::BufferView &view) { auto &bufferView = asset.bufferViews[view.bufferViewIndex]; auto &buffer = asset.buffers[bufferView.bufferIndex]; std::visit(fastgltf::visitor{ // We only care about VectorWithMime here, because we // specify LoadExternalBuffers, meaning all buffers // are already loaded into a vector. [](auto &arg) { }, [&](fastgltf::sources::Vector &vector) { unsigned char *data = stbi_load_from_memory( vector.bytes.data() + bufferView.byteOffset, static_cast(bufferView.byteLength), &width, &height, &nrChannels, 4); if (data) { VkExtent3D imagesize; imagesize.width = width; imagesize.height = height; imagesize.depth = 1; VkFormat fmt = srgb ? VK_FORMAT_R8G8B8A8_SRGB : VK_FORMAT_R8G8B8A8_UNORM; newImage = engine->_resourceManager->create_image( data, imagesize, fmt, VK_IMAGE_USAGE_SAMPLED_BIT, false); if (vmaDebugEnabled()) vmaSetAllocationName(engine->_deviceManager->allocator(), newImage.allocation, "gltf.bufferview.image"); stbi_image_free(data); } } }, buffer.data); }, }, image.data); // if any of the attempts to load the data failed, we havent written the image // so handle is null if (newImage.image == VK_NULL_HANDLE) { return {}; } else { return newImage; } } //< loadimg //> filters VkFilter extract_filter(fastgltf::Filter filter) { switch (filter) { // nearest samplers case fastgltf::Filter::Nearest: case fastgltf::Filter::NearestMipMapNearest: case fastgltf::Filter::NearestMipMapLinear: return VK_FILTER_NEAREST; // linear samplers case fastgltf::Filter::Linear: case fastgltf::Filter::LinearMipMapNearest: case fastgltf::Filter::LinearMipMapLinear: default: return VK_FILTER_LINEAR; } } VkSamplerMipmapMode extract_mipmap_mode(fastgltf::Filter filter) { switch (filter) { case fastgltf::Filter::NearestMipMapNearest: case fastgltf::Filter::LinearMipMapNearest: return VK_SAMPLER_MIPMAP_MODE_NEAREST; case fastgltf::Filter::NearestMipMapLinear: case fastgltf::Filter::LinearMipMapLinear: default: return VK_SAMPLER_MIPMAP_MODE_LINEAR; } } //< filters std::optional > loadGltf(VulkanEngine *engine, std::string_view filePath) { //> load_1 fmt::println("[GLTF] loadGltf begin: '{}'", filePath); std::shared_ptr scene = std::make_shared(); scene->creator = engine; LoadedGLTF &file = *scene.get(); fastgltf::Parser parser{}; constexpr auto gltfOptions = fastgltf::Options::DontRequireValidAssetMember | fastgltf::Options::AllowDouble | fastgltf::Options::LoadGLBBuffers | fastgltf::Options::LoadExternalBuffers; // fastgltf::Options::LoadExternalImages; fastgltf::GltfDataBuffer data; data.loadFromFile(filePath); fastgltf::Asset gltf; std::filesystem::path path = filePath; auto type = fastgltf::determineGltfFileType(&data); if (type == fastgltf::GltfType::glTF) { auto load = parser.loadGLTF(&data, path.parent_path(), gltfOptions); if (load) { gltf = std::move(load.get()); } else { std::cerr << "Failed to load glTF: " << fastgltf::to_underlying(load.error()) << std::endl; return {}; } } else if (type == fastgltf::GltfType::GLB) { auto load = parser.loadBinaryGLTF(&data, path.parent_path(), gltfOptions); if (load) { gltf = std::move(load.get()); } else { std::cerr << "Failed to load glTF: " << fastgltf::to_underlying(load.error()) << std::endl; return {}; } } else { std::cerr << "Failed to determine glTF container" << std::endl; return {}; } //< load_1 //> load_2 // we can stimate the descriptors we will need accurately fmt::println("[GLTF] loadGltf: materials={} meshes={} images={} samplers={} (creating descriptor pool)", gltf.materials.size(), gltf.meshes.size(), gltf.images.size(), gltf.samplers.size()); std::vector sizes = { {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 3}, {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 3}, {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1} }; file.descriptorPool.init(engine->_deviceManager->device(), gltf.materials.size(), sizes); fmt::println("[GLTF] loadGltf: descriptor pool initialized for '{}' (materials={})", filePath, gltf.materials.size()); //< load_2 //> load_samplers // load samplers for (fastgltf::Sampler &sampler: gltf.samplers) { VkSamplerCreateInfo sampl = {.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, .pNext = nullptr}; sampl.maxLod = VK_LOD_CLAMP_NONE; sampl.minLod = 0.0f; sampl.magFilter = extract_filter(sampler.magFilter.value_or(fastgltf::Filter::Nearest)); sampl.minFilter = extract_filter(sampler.minFilter.value_or(fastgltf::Filter::Nearest)); sampl.mipmapMode = extract_mipmap_mode(sampler.minFilter.value_or(fastgltf::Filter::Nearest)); // Address modes: default to glTF Repeat auto toAddress = [](fastgltf::Wrap w) -> VkSamplerAddressMode { switch (w) { case fastgltf::Wrap::ClampToEdge: return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; case fastgltf::Wrap::MirroredRepeat: return VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT; case fastgltf::Wrap::Repeat: default: return VK_SAMPLER_ADDRESS_MODE_REPEAT; } }; // fastgltf::Sampler::wrapS/wrapT are non-optional and already default to Repeat sampl.addressModeU = toAddress(sampler.wrapS); sampl.addressModeV = toAddress(sampler.wrapT); sampl.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT; sampl.unnormalizedCoordinates = VK_FALSE; VkSampler newSampler; vkCreateSampler(engine->_deviceManager->device(), &sampl, nullptr, &newSampler); file.samplers.push_back(newSampler); } //< load_samplers //> load_arrays // temporal arrays for all the objects to use while creating the GLTF data std::vector > meshes; std::vector > nodes; std::vector > materials; //< load_arrays // Note: glTF images are now loaded on-demand via TextureCache. // Resolve external image paths relative to the source glTF file directory // to avoid failing to find textures when running from a different CWD. const std::filesystem::path baseDir = path.parent_path(); auto buildTextureKey = [&](size_t imgIndex, bool srgb) -> TextureCache::TextureKey { TextureCache::TextureKey key{}; key.srgb = srgb; key.mipmapped = true; if (imgIndex >= gltf.images.size()) { key.hash = 0; // invalid return key; } fastgltf::Image &image = gltf.images[imgIndex]; std::visit(fastgltf::visitor{ [&](fastgltf::sources::URI &filePath) { const std::string rel(filePath.uri.path().begin(), filePath.uri.path().end()); // Build an absolute (or at least baseDir-resolved) path for IO + stable keying std::filesystem::path resolved = std::filesystem::path(rel); if (resolved.is_relative()) { resolved = baseDir / resolved; } key.kind = TextureCache::TextureKey::SourceKind::FilePath; key.path = resolved.string(); std::string id = std::string("GLTF:") + key.path + (srgb ? "#sRGB" : "#UNORM"); key.hash = texcache::fnv1a64(id); }, [&](fastgltf::sources::Vector &vector) { key.kind = TextureCache::TextureKey::SourceKind::Bytes; key.bytes.assign(vector.bytes.begin(), vector.bytes.end()); uint64_t h = texcache::fnv1a64(key.bytes.data(), key.bytes.size()); key.hash = h ^ (srgb ? 0x9E3779B97F4A7C15ull : 0ull); }, [&](fastgltf::sources::BufferView &view) { auto &bufferView = gltf.bufferViews[view.bufferViewIndex]; auto &buffer = gltf.buffers[bufferView.bufferIndex]; std::visit(fastgltf::visitor{ [](auto &arg) {}, [&](fastgltf::sources::Vector &vec) { size_t off = bufferView.byteOffset; size_t len = bufferView.byteLength; key.kind = TextureCache::TextureKey::SourceKind::Bytes; key.bytes.assign(vec.bytes.begin() + off, vec.bytes.begin() + off + len); uint64_t h = texcache::fnv1a64(key.bytes.data(), key.bytes.size()); key.hash = h ^ (srgb ? 0x9E3779B97F4A7C15ull : 0ull); } }, buffer.data); }, [](auto &other) {} }, image.data); return key; }; //> load_buffer // create buffer to hold the material data file.materialDataBuffer = engine->_resourceManager->create_buffer( sizeof(GLTFMetallic_Roughness::MaterialConstants) * gltf.materials.size(), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU); int data_index = 0; GLTFMetallic_Roughness::MaterialConstants *sceneMaterialConstants = (GLTFMetallic_Roughness::MaterialConstants *) file.materialDataBuffer.info.pMappedData; //< load_buffer // //> load_material for (fastgltf::Material &mat: gltf.materials) { std::shared_ptr newMat = std::make_shared(); materials.push_back(newMat); file.materials[mat.name.c_str()] = newMat; GLTFMetallic_Roughness::MaterialConstants constants; // Defaults constants.extra[0].x = 1.0f; // normalScale constants.colorFactors.x = mat.pbrData.baseColorFactor[0]; constants.colorFactors.y = mat.pbrData.baseColorFactor[1]; constants.colorFactors.z = mat.pbrData.baseColorFactor[2]; constants.colorFactors.w = mat.pbrData.baseColorFactor[3]; constants.metal_rough_factors.x = mat.pbrData.metallicFactor; constants.metal_rough_factors.y = mat.pbrData.roughnessFactor; // write material parameters to buffer sceneMaterialConstants[data_index] = constants; MaterialPass passType = MaterialPass::MainColor; if (mat.alphaMode == fastgltf::AlphaMode::Blend) { passType = MaterialPass::Transparent; } GLTFMetallic_Roughness::MaterialResources materialResources; // default the material textures materialResources.colorImage = engine->_whiteImage; materialResources.colorSampler = engine->_samplerManager->defaultLinear(); materialResources.metalRoughImage = engine->_whiteImage; materialResources.metalRoughSampler = engine->_samplerManager->defaultLinear(); materialResources.normalImage = engine->_flatNormalImage; materialResources.normalSampler = engine->_samplerManager->defaultLinear(); // set the uniform buffer for the material data materialResources.dataBuffer = file.materialDataBuffer.buffer; materialResources.dataBufferOffset = data_index * sizeof(GLTFMetallic_Roughness::MaterialConstants); // Dynamic texture bindings via TextureCache (fallbacks are already set) TextureCache *cache = engine->_context->textures; TextureCache::TextureHandle hColor = TextureCache::InvalidHandle; TextureCache::TextureHandle hMRO = TextureCache::InvalidHandle; TextureCache::TextureHandle hNorm = TextureCache::InvalidHandle; if (cache && mat.pbrData.baseColorTexture.has_value()) { const auto &tex = gltf.textures[mat.pbrData.baseColorTexture.value().textureIndex]; const size_t imgIndex = tex.imageIndex.value(); const bool hasSampler = tex.samplerIndex.has_value(); const VkSampler sampler = hasSampler ? file.samplers[tex.samplerIndex.value()] : engine->_samplerManager->defaultLinear(); auto key = buildTextureKey(imgIndex, true); if (key.hash != 0) { hColor = cache->request(key, sampler); materialResources.colorSampler = sampler; } } if (cache && mat.pbrData.metallicRoughnessTexture.has_value()) { const auto &tex = gltf.textures[mat.pbrData.metallicRoughnessTexture.value().textureIndex]; const size_t imgIndex = tex.imageIndex.value(); const bool hasSampler = tex.samplerIndex.has_value(); const VkSampler sampler = hasSampler ? file.samplers[tex.samplerIndex.value()] : engine->_samplerManager->defaultLinear(); auto key = buildTextureKey(imgIndex, false); if (key.hash != 0) { hMRO = cache->request(key, sampler); materialResources.metalRoughSampler = sampler; } } if (cache && mat.normalTexture.has_value()) { const auto &tex = gltf.textures[mat.normalTexture.value().textureIndex]; const size_t imgIndex = tex.imageIndex.value(); const bool hasSampler = tex.samplerIndex.has_value(); const VkSampler sampler = hasSampler ? file.samplers[tex.samplerIndex.value()] : engine->_samplerManager->defaultLinear(); auto key = buildTextureKey(imgIndex, false); key.channels = TextureCache::TextureKey::ChannelsHint::RG; // prefer BC5 for normals if (key.hash != 0) { hNorm = cache->request(key, sampler); materialResources.normalSampler = sampler; } // Store normal scale if provided sceneMaterialConstants[data_index].extra[0].x = mat.normalTexture->scale; } // build material newMat->data = engine->metalRoughMaterial.write_material(engine->_deviceManager->device(), passType, materialResources, file.descriptorPool); // Register descriptor patches for dynamic textures if (cache) { if (hColor != TextureCache::InvalidHandle) { cache->watchBinding(hColor, newMat->data.materialSet, 1u, materialResources.colorSampler, engine->_whiteImage.imageView); } if (hMRO != TextureCache::InvalidHandle) { cache->watchBinding(hMRO, newMat->data.materialSet, 2u, materialResources.metalRoughSampler, engine->_whiteImage.imageView); } if (hNorm != TextureCache::InvalidHandle) { cache->watchBinding(hNorm, newMat->data.materialSet, 3u, materialResources.normalSampler, engine->_flatNormalImage.imageView); } } data_index++; } //< load_material // Flush material constants buffer so GPU sees updated data on non-coherent memory if (!gltf.materials.empty()) { VkDeviceSize totalSize = sizeof(GLTFMetallic_Roughness::MaterialConstants) * gltf.materials.size(); vmaFlushAllocation(engine->_deviceManager->allocator(), file.materialDataBuffer.allocation, 0, totalSize); } // use the same vectors for all meshes so that the memory doesnt reallocate as // often std::vector indices; std::vector vertices; for (fastgltf::Mesh &mesh: gltf.meshes) { std::shared_ptr newmesh = std::make_shared(); meshes.push_back(newmesh); file.meshes[mesh.name.c_str()] = newmesh; newmesh->name = mesh.name; // clear the mesh arrays each mesh, we dont want to merge them by error indices.clear(); vertices.clear(); for (auto &&p: mesh.primitives) { GeoSurface newSurface; newSurface.startIndex = (uint32_t) indices.size(); newSurface.count = (uint32_t) gltf.accessors[p.indicesAccessor.value()].count; size_t initial_vtx = vertices.size(); // load indexes { fastgltf::Accessor &indexaccessor = gltf.accessors[p.indicesAccessor.value()]; indices.reserve(indices.size() + indexaccessor.count); fastgltf::iterateAccessor(gltf, indexaccessor, [&](std::uint32_t idx) { indices.push_back(idx + initial_vtx); }); } // load vertex positions { fastgltf::Accessor &posAccessor = gltf.accessors[p.findAttribute("POSITION")->second]; vertices.resize(vertices.size() + posAccessor.count); fastgltf::iterateAccessorWithIndex(gltf, posAccessor, [&](glm::vec3 v, size_t index) { Vertex newvtx{}; newvtx.position = v; newvtx.normal = {1, 0, 0}; newvtx.color = glm::vec4{1.f}; newvtx.uv_x = 0; newvtx.uv_y = 0; newvtx.tangent = glm::vec4(1,0,0,1); vertices[initial_vtx + index] = newvtx; }); } // load vertex normals auto normals = p.findAttribute("NORMAL"); if (normals != p.attributes.end()) { fastgltf::iterateAccessorWithIndex(gltf, gltf.accessors[(*normals).second], [&](glm::vec3 v, size_t index) { vertices[initial_vtx + index].normal = v; }); } // load UVs auto uv = p.findAttribute("TEXCOORD_0"); if (uv != p.attributes.end()) { fastgltf::iterateAccessorWithIndex(gltf, gltf.accessors[(*uv).second], [&](glm::vec2 v, size_t index) { vertices[initial_vtx + index].uv_x = v.x; vertices[initial_vtx + index].uv_y = v.y; }); } // load vertex colors auto colors = p.findAttribute("COLOR_0"); if (colors != p.attributes.end()) { fastgltf::iterateAccessorWithIndex(gltf, gltf.accessors[(*colors).second], [&](glm::vec4 v, size_t index) { vertices[initial_vtx + index].color = v; }); } // load tangents if present (vec4, w = sign) auto tangents = p.findAttribute("TANGENT"); bool hasTangents = tangents != p.attributes.end(); if (hasTangents) { fastgltf::iterateAccessorWithIndex(gltf, gltf.accessors[(*tangents).second], [&](glm::vec4 v, size_t index) { vertices[initial_vtx + index].tangent = v; }); } // Generate tangents if missing and we have UVs if (!hasTangents) { size_t primIndexStart = newSurface.startIndex; size_t primIndexCount = newSurface.count; size_t primVertexStart = initial_vtx; size_t primVertexCount = vertices.size() - initial_vtx; geom::generate_tangents_range(vertices, indices, primIndexStart, primIndexCount, primVertexStart, primVertexCount); } if (p.materialIndex.has_value()) { newSurface.material = materials[p.materialIndex.value()]; } else { newSurface.material = materials[0]; } // Compute per-surface bounds using only the indices referenced by this primitive. if (newSurface.count > 0) { uint32_t firstIndex = newSurface.startIndex; uint32_t lastIndex = newSurface.startIndex + newSurface.count; uint32_t baseVertex = indices[firstIndex]; glm::vec3 minpos = vertices[baseVertex].position; glm::vec3 maxpos = vertices[baseVertex].position; for (uint32_t i = firstIndex + 1; i < lastIndex; i++) { uint32_t vi = indices[i]; const glm::vec3 &p = vertices[vi].position; minpos = glm::min(minpos, p); maxpos = glm::max(maxpos, p); } newSurface.bounds.origin = (maxpos + minpos) / 2.f; newSurface.bounds.extents = (maxpos - minpos) / 2.f; newSurface.bounds.sphereRadius = glm::length(newSurface.bounds.extents); newSurface.bounds.type = BoundsType::Mesh; } else { newSurface.bounds.origin = glm::vec3(0.0f); newSurface.bounds.extents = glm::vec3(0.5f); newSurface.bounds.sphereRadius = glm::length(newSurface.bounds.extents); newSurface.bounds.type = BoundsType::Mesh; } newmesh->surfaces.push_back(newSurface); } // Build CPU BVH for precise picking over this mesh (triangle-level). { std::span vSpan(vertices.data(), vertices.size()); std::span iSpan(indices.data(), indices.size()); newmesh->bvh = build_mesh_bvh(*newmesh, vSpan, iSpan); } newmesh->meshBuffers = engine->_resourceManager->uploadMesh(indices, vertices); // BLAS for this mesh will be built lazily from RayTracingManager::buildTLASFromDrawContext() // when ray-traced shadows are enabled. This avoids redundant builds and concentrates // RT work in one place. // If CPU vectors ballooned for this mesh, release capacity back to the OS auto shrink_if_huge = [](auto &vec, size_t elemSizeBytes) { const size_t capBytes = vec.capacity() * elemSizeBytes; const size_t kThreshold = 64ull * 1024ull * 1024ull; // 64 MiB if (capBytes > kThreshold) { using Vec = std::remove_reference_t; Vec empty; vec.swap(empty); } }; shrink_if_huge(indices, sizeof(uint32_t)); shrink_if_huge(vertices, sizeof(Vertex)); } //> load_nodes // load all nodes and their meshes for (fastgltf::Node &node: gltf.nodes) { std::shared_ptr newNode; // find if the node has a mesh, and if it does hook it to the mesh pointer and allocate it with the meshnode class if (node.meshIndex.has_value()) { auto meshNode = std::make_shared(); meshNode->mesh = meshes[*node.meshIndex]; meshNode->scene = &file; newNode = meshNode; } else { newNode = std::make_shared(); } nodes.push_back(newNode); if (!node.name.empty()) { file.nodes[std::string(node.name)] = newNode; } std::visit(fastgltf::visitor{ [&](fastgltf::Node::TransformMatrix matrix) { glm::mat4 m(1.0f); memcpy(&m, matrix.data(), sizeof(matrix)); glm::vec3 t = glm::vec3(m[3]); glm::vec3 col0 = glm::vec3(m[0]); glm::vec3 col1 = glm::vec3(m[1]); glm::vec3 col2 = glm::vec3(m[2]); glm::vec3 s(glm::length(col0), glm::length(col1), glm::length(col2)); if (s.x != 0.0f) col0 /= s.x; if (s.y != 0.0f) col1 /= s.y; if (s.z != 0.0f) col2 /= s.z; glm::mat3 rotMat(col0, col1, col2); glm::quat r = glm::quat_cast(rotMat); newNode->setTRS(t, r, s); }, [&](fastgltf::Node::TRS transform) { glm::vec3 tl(transform.translation[0], transform.translation[1], transform.translation[2]); glm::quat rot(transform.rotation[3], transform.rotation[0], transform.rotation[1], transform.rotation[2]); glm::vec3 sc(transform.scale[0], transform.scale[1], transform.scale[2]); newNode->setTRS(tl, rot, sc); } }, node.transform); } //< load_nodes //> load_graph // run loop again to setup transform hierarchy for (int i = 0; i < gltf.nodes.size(); i++) { fastgltf::Node &node = gltf.nodes[i]; std::shared_ptr &sceneNode = nodes[i]; for (auto &c: node.children) { sceneNode->children.push_back(nodes[c]); nodes[c]->parent = sceneNode; } } // find the top nodes, with no parents for (auto &node: nodes) { if (node->parent.lock() == nullptr) { file.topNodes.push_back(node); node->refreshTransform(glm::mat4{1.f}); } } // Load animations (if present) if (!gltf.animations.empty()) { file.animations.reserve(gltf.animations.size()); for (auto &anim: gltf.animations) { LoadedGLTF::Animation dstAnim; dstAnim.name = anim.name.c_str(); dstAnim.duration = 0.0f; dstAnim.channels.reserve(anim.channels.size()); for (auto &ch: anim.channels) { if (ch.nodeIndex >= nodes.size() || ch.samplerIndex >= anim.samplers.size()) { continue; } LoadedGLTF::AnimationChannel channel{}; channel.node = nodes[ch.nodeIndex]; switch (ch.path) { case fastgltf::AnimationPath::Translation: channel.target = LoadedGLTF::AnimationChannel::Target::Translation; break; case fastgltf::AnimationPath::Rotation: channel.target = LoadedGLTF::AnimationChannel::Target::Rotation; break; case fastgltf::AnimationPath::Scale: channel.target = LoadedGLTF::AnimationChannel::Target::Scale; break; default: // Weights and other paths not yet supported continue; } const fastgltf::AnimationSampler &sampler = anim.samplers[ch.samplerIndex]; switch (sampler.interpolation) { case fastgltf::AnimationInterpolation::Step: channel.interpolation = LoadedGLTF::AnimationChannel::Interpolation::Step; break; case fastgltf::AnimationInterpolation::Linear: case fastgltf::AnimationInterpolation::CubicSpline: default: channel.interpolation = LoadedGLTF::AnimationChannel::Interpolation::Linear; break; } // Input times const auto &timeAccessor = gltf.accessors[sampler.inputAccessor]; channel.times.reserve(timeAccessor.count); float maxTime = 0.0f; fastgltf::iterateAccessorWithIndex(gltf, timeAccessor, [&](float value, size_t) { channel.times.push_back(value); if (value > maxTime) maxTime = value; }); // Output values const auto &valueAccessor = gltf.accessors[sampler.outputAccessor]; const bool isCubic = sampler.interpolation == fastgltf::AnimationInterpolation::CubicSpline; if (channel.target == LoadedGLTF::AnimationChannel::Target::Rotation) { channel.vec4Values.clear(); channel.vec4Values.reserve(valueAccessor.count); fastgltf::iterateAccessorWithIndex(gltf, valueAccessor, [&](glm::vec4 v, size_t index) { if (isCubic) { // For cubic-spline, values are [in, value, out]; keep only the middle one. if (index % 3 != 1) return; } channel.vec4Values.push_back(v); }); } else { channel.vec3Values.clear(); channel.vec3Values.reserve(valueAccessor.count); fastgltf::iterateAccessorWithIndex(gltf, valueAccessor, [&](glm::vec3 v, size_t index) { if (isCubic) { if (index % 3 != 1) return; } channel.vec3Values.push_back(v); }); } if (!channel.times.empty()) { dstAnim.duration = std::max(dstAnim.duration, maxTime); dstAnim.channels.push_back(std::move(channel)); } } if (!dstAnim.channels.empty()) { file.animations.push_back(std::move(dstAnim)); } } if (!file.animations.empty()) { file.activeAnimation = 0; file.animationTime = 0.0f; file.animationLoop = true; } } // We no longer need glTF-owned buffer payloads; free any large vectors for (auto &buf : gltf.buffers) { std::visit(fastgltf::visitor{ [](auto &arg) {}, [&](fastgltf::sources::Vector &vec) { std::vector().swap(vec.bytes); } }, buf.data); } for (auto &img : gltf.images) { std::visit(fastgltf::visitor{ [](auto &arg) {}, [&](fastgltf::sources::Vector &vec) { std::vector().swap(vec.bytes); } }, img.data); } fmt::println("[GLTF] loadGltf done: meshes={} materials={} images={} samplers={} animations={} debugName='{}'", file.meshes.size(), file.materials.size(), file.images.size(), file.samplers.size(), file.animations.size(), file.debugName.empty() ? "" : file.debugName); return scene; //< load_graph } void LoadedGLTF::Draw(const glm::mat4 &topMatrix, DrawContext &ctx) { // create renderables from the scenenodes for (auto &n: topNodes) { n->Draw(topMatrix, ctx); } } std::shared_ptr LoadedGLTF::getNode(const std::string &name) { auto it = nodes.find(name); return (it != nodes.end()) ? it->second : nullptr; } void LoadedGLTF::refreshAllTransforms() { for (auto &n: topNodes) { if (n) { n->refreshTransform(glm::mat4{1.f}); } } } void LoadedGLTF::setActiveAnimation(int index, bool resetTime) { if (animations.empty()) { activeAnimation = -1; return; } if (index < 0 || index >= static_cast(animations.size())) { index = 0; } activeAnimation = index; if (resetTime) { animationTime = 0.0f; } } void LoadedGLTF::setActiveAnimation(const std::string &name, bool resetTime) { for (size_t i = 0; i < animations.size(); ++i) { if (animations[i].name == name) { setActiveAnimation(static_cast(i), resetTime); return; } } } void LoadedGLTF::updateAnimation(float dt) { if (animations.empty()) return; if (activeAnimation < 0 || activeAnimation >= static_cast(animations.size())) return; if (dt <= 0.0f) return; Animation &clip = animations[activeAnimation]; if (clip.duration <= 0.0f) return; animationTime += dt; if (animationLoop) { animationTime = std::fmod(animationTime, clip.duration); if (animationTime < 0.0f) { animationTime += clip.duration; } } else if (animationTime > clip.duration) { animationTime = clip.duration; } float t = animationTime; for (auto &ch: clip.channels) { if (!ch.node) continue; const size_t keyCount = ch.times.size(); if (keyCount == 0) continue; size_t k1 = 0; while (k1 < keyCount && ch.times[k1] < t) { ++k1; } size_t k0; if (k1 == 0) { k0 = k1 = 0; } else if (k1 >= keyCount) { k0 = keyCount - 1; k1 = keyCount - 1; } else { k0 = k1 - 1; } float t0 = ch.times[k0]; float t1 = ch.times[k1]; float alpha = 0.0f; if (k0 != k1 && t1 > t0) { alpha = (t - t0) / (t1 - t0); alpha = std::clamp(alpha, 0.0f, 1.0f); } Node &node = *ch.node; switch (ch.target) { case AnimationChannel::Target::Translation: { if (ch.vec3Values.size() != keyCount) break; glm::vec3 v0 = ch.vec3Values[k0]; glm::vec3 v1 = ch.vec3Values[k1]; glm::vec3 v; if (ch.interpolation == AnimationChannel::Interpolation::Step || k0 == k1) { v = v0; } else { v = v0 * (1.0f - alpha) + v1 * alpha; } node.translation = v; node.hasTRS = true; break; } case AnimationChannel::Target::Scale: { if (ch.vec3Values.size() != keyCount) break; glm::vec3 v0 = ch.vec3Values[k0]; glm::vec3 v1 = ch.vec3Values[k1]; glm::vec3 v; if (ch.interpolation == AnimationChannel::Interpolation::Step || k0 == k1) { v = v0; } else { v = v0 * (1.0f - alpha) + v1 * alpha; } node.scale = v; node.hasTRS = true; break; } case AnimationChannel::Target::Rotation: { if (ch.vec4Values.size() != keyCount) break; glm::vec4 v0 = ch.vec4Values[k0]; glm::vec4 v1 = ch.vec4Values[k1]; glm::quat q0(v0.w, v0.x, v0.y, v0.z); glm::quat q1(v1.w, v1.x, v1.y, v1.z); glm::quat q; if (ch.interpolation == AnimationChannel::Interpolation::Step || k0 == k1) { q = q0; } else { q = glm::slerp(q0, q1, alpha); } node.rotation = glm::normalize(q); node.hasTRS = true; break; } } } // Rebuild local matrices from updated TRS and refresh world transforms for (auto &[name, nodePtr]: nodes) { if (nodePtr && nodePtr->hasTRS) { nodePtr->updateLocalFromTRS(); } } refreshAllTransforms(); } void LoadedGLTF::clearAll() { const char *name = debugName.empty() ? "" : debugName.c_str(); fmt::println("[GLTF] clearAll begin for '{}' (meshes={} images={} materials={} samplers={})", name, meshes.size(), images.size(), materials.size(), samplers.size()); VkDevice dv = creator->_deviceManager->device(); // Before destroying descriptor pools, unregister descriptor-set watches so // the TextureCache will not attempt to patch dead sets. if (creator && creator->_context && creator->_context->textures) { TextureCache *cache = creator->_context->textures; for (auto &[k, mat] : materials) { if (mat && mat->data.materialSet != VK_NULL_HANDLE) { cache->unwatchSet(mat->data.materialSet); } } } for (auto &[k, v]: meshes) { if (creator->_rayManager) { creator->_rayManager->removeBLASForMesh(v.get()); } creator->_resourceManager->destroy_buffer(v->meshBuffers.indexBuffer); creator->_resourceManager->destroy_buffer(v->meshBuffers.vertexBuffer); } for (auto &[k, v]: images) { if (v.image == creator->_errorCheckerboardImage.image) { // dont destroy the default images continue; } creator->_resourceManager->destroy_image(v); } for (auto &sampler: samplers) { vkDestroySampler(dv, sampler, nullptr); } auto materialBuffer = materialDataBuffer; auto samplersToDestroy = samplers; descriptorPool.destroy_pools(dv); creator->_resourceManager->destroy_buffer(materialBuffer); fmt::println("[GLTF] clearAll done for '{}' (meshes={}, images={}, materials={}, samplers={})", name, meshes.size(), images.size(), materials.size(), samplers.size()); }