diff --git a/CMakeLists.txt b/CMakeLists.txt index ce143d64..2f7f869a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -119,6 +119,7 @@ target_sources(vk-gltf-viewer PRIVATE interface/gltf/AssetProcessError.cppm interface/gltf/AssetSceneGpuBuffers.cppm interface/gltf/AssetSceneHierarchy.cppm + interface/gltf/MaterialVariantsMapping.cppm interface/helpers/concepts.cppm interface/helpers/fastgltf.cppm interface/helpers/formatter/ByteSize.cppm diff --git a/README.md b/README.md index e52f4312..6a87f212 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Blazingly fast[^1] Vulkan glTF viewer. - Binary format (`.glb`). - Support glTF 2.0 extensions: - [`KHR_materials_unlit`](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_unlit) for lighting independent material shading + - [`KHR_materials_variants`](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_variants) - [`KHR_texture_basisu`](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_texture_basisu) for BC7 GPU compression texture decoding - [`EXT_mesh_gpu_instancing`](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Vendor/EXT_mesh_gpu_instancing) for instancing multiple meshes with the same geometry - Use 4x MSAA by default. diff --git a/impl/MainApp.cpp b/impl/MainApp.cpp index 38e624de..338f7fde 100644 --- a/impl/MainApp.cpp +++ b/impl/MainApp.cpp @@ -260,6 +260,12 @@ void vk_gltf_viewer::MainApp::run() { std::array shouldHandleSwapchainResize{}; std::array shouldRegenerateDrawCommands{}; + // TODO: we need more general mechanism to upload the GPU buffer data in shared data. This is just a stopgap solution + // for current KHR_materials_variants implementation. + const vk::raii::CommandPool graphicsCommandPool { gpu.device, vk::CommandPoolCreateInfo { {}, gpu.queueFamilies.graphicsPresent } }; + const auto [sharedDataUpdateCommandBuffer] = vku::allocateCommandBuffers<1>(*gpu.device, *graphicsCommandPool); + bool hasUpdateData = false; + std::vector tasks; for (std::uint64_t frameIndex = 0; !glfwWindowShouldClose(window); frameIndex = (frameIndex + 1) % FRAMES_IN_FLIGHT) { tasks.clear(); @@ -282,6 +288,9 @@ void vk_gltf_viewer::MainApp::run() { if (auto &gltfAsset = appState.gltfAsset) { imguiTaskCollector.assetInspector(gltfAsset->asset, gltf->directory); imguiTaskCollector.materialEditor(gltfAsset->asset, assetTextureDescriptorSets); + if (!gltfAsset->asset.materialVariants.empty()) { + imguiTaskCollector.materialVariants(gltfAsset->asset); + } imguiTaskCollector.sceneHierarchy(gltfAsset->asset, gltfAsset->getSceneIndex(), gltfAsset->nodeVisibilities, gltfAsset->hoveringNodeIndex, gltfAsset->selectedNodeIndices); imguiTaskCollector.nodeInspector(gltfAsset->asset, gltfAsset->selectedNodeIndices); } @@ -513,6 +522,26 @@ void vk_gltf_viewer::MainApp::run() { [&](control::task::InvalidateDrawCommandSeparation) { shouldRegenerateDrawCommands.fill(true); }, + [&](const control::task::SelectMaterialVariants &task) { + assert(gltf && "Synchronization error: gltf is unset but material variants are selected."); + + gpu.device.waitIdle(); + + graphicsCommandPool.reset(); + sharedDataUpdateCommandBuffer.begin(vk::CommandBufferBeginInfo { vk::CommandBufferUsageFlagBits::eOneTimeSubmit }); + + for (const auto &[pPrimitive, materialIndex] : gltf->materialVariantsMapping.at(task.variantIndex)) { + pPrimitive->materialIndex.emplace(materialIndex); + hasUpdateData |= gltf->assetGpuBuffers.updatePrimitiveMaterial(*pPrimitive, materialIndex, sharedDataUpdateCommandBuffer); + } + + sharedDataUpdateCommandBuffer.end(); + + if (hasUpdateData) { + gpu.queues.graphicsPresent.submit(vk::SubmitInfo { {}, {}, sharedDataUpdateCommandBuffer }); + gpu.device.waitIdle(); + } + } }, task); } @@ -817,7 +846,7 @@ void vk_gltf_viewer::MainApp::loadGltf(const std::filesystem::path &path) { }; })); gpu.device.updateDescriptorSets({ - sharedData.assetDescriptorSet.getWriteOne<0>({ gltf->assetGpuBuffers.primitiveBuffer, 0, vk::WholeSize }), + sharedData.assetDescriptorSet.getWriteOne<0>({ gltf->assetGpuBuffers.getPrimitiveBuffer(), 0, vk::WholeSize }), sharedData.assetDescriptorSet.getWriteOne<1>({ gltf->assetGpuBuffers.materialBuffer, 0, vk::WholeSize }), sharedData.assetDescriptorSet.getWrite<2>(imageInfos), sharedData.sceneDescriptorSet.getWriteOne<0>({ gltf->sceneGpuBuffers.nodeBuffer, 0, vk::WholeSize }), diff --git a/impl/control/ImGuiTaskCollector.cpp b/impl/control/ImGuiTaskCollector.cpp index 8fc5c75b..f831874d 100644 --- a/impl/control/ImGuiTaskCollector.cpp +++ b/impl/control/ImGuiTaskCollector.cpp @@ -335,6 +335,7 @@ void vk_gltf_viewer::control::ImGuiTaskCollector::assetSamplers(std::span, i); + } + } + } + ImGui::End(); +} + void vk_gltf_viewer::control::ImGuiTaskCollector::sceneHierarchy( fastgltf::Asset &asset, std::size_t sceneIndex, diff --git a/impl/gltf/AssetGpuBuffers.cpp b/impl/gltf/AssetGpuBuffers.cpp index 05e5ea62..209c4255 100644 --- a/impl/gltf/AssetGpuBuffers.cpp +++ b/impl/gltf/AssetGpuBuffers.cpp @@ -13,6 +13,29 @@ import :helpers.ranges; #define FWD(...) static_cast(__VA_ARGS__) #define LIFT(...) [&](auto &&...xs) { return (__VA_ARGS__)(FWD(xs)...); } +bool vk_gltf_viewer::gltf::AssetGpuBuffers::updatePrimitiveMaterial( + const fastgltf::Primitive &primitive, + std::uint32_t materialIndex, + vk::CommandBuffer transferCommandBuffer +) { + const std::uint16_t orderedPrimitiveIndex = primitiveInfos.at(&primitive).index; + const std::uint32_t paddedMaterialIndex = padMaterialIndex(materialIndex); + return std::visit(multilambda { + [&](vku::MappedBuffer &primitiveBuffer) { + primitiveBuffer.asRange()[orderedPrimitiveIndex].materialIndex = paddedMaterialIndex; + return false; + }, + [&](vk::Buffer primitiveBuffer) { + transferCommandBuffer.updateBuffer( + primitiveBuffer, + sizeof(GpuPrimitive) * orderedPrimitiveIndex + offsetof(GpuPrimitive, materialIndex), + sizeof(GpuPrimitive::materialIndex), + &paddedMaterialIndex); + return true; + } + }, primitiveBuffer); +} + std::vector vk_gltf_viewer::gltf::AssetGpuBuffers::createOrderedPrimitives() const { return asset.meshes | std::views::transform(&fastgltf::Mesh::primitives) @@ -94,8 +117,8 @@ vku::AllocatedBuffer vk_gltf_viewer::gltf::AssetGpuBuffers::createMaterialBuffer return dstBuffer; } -vku::AllocatedBuffer vk_gltf_viewer::gltf::AssetGpuBuffers::createPrimitiveBuffer() { - vku::AllocatedBuffer stagingBuffer = vku::MappedBuffer { +std::variant vk_gltf_viewer::gltf::AssetGpuBuffers::createPrimitiveBuffer() { + vku::MappedBuffer stagingBuffer { gpu.allocator, std::from_range, orderedPrimitives | std::views::transform([this](const fastgltf::Primitive *pPrimitive) { const AssetPrimitiveInfo &primitiveInfo = primitiveInfos[pPrimitive]; @@ -114,15 +137,11 @@ vku::AllocatedBuffer vk_gltf_viewer::gltf::AssetGpuBuffers::createPrimitiveBuffe .positionByteStride = primitiveInfo.positionInfo.byteStride, .normalByteStride = normalInfo.byteStride, .tangentByteStride = tangentInfo.byteStride, - .materialIndex - = primitiveInfo.materialIndex.transform([](std::size_t index) { - return 1U /* index 0 is reserved for the fallback material */ + static_cast(index); - }) - .value_or(0U), + .materialIndex = primitiveInfo.materialIndex.transform(padMaterialIndex).value_or(0U), }; }), gpu.isUmaDevice ? vk::BufferUsageFlagBits::eStorageBuffer : vk::BufferUsageFlagBits::eTransferSrc, - }.unmap(); + }; if (gpu.isUmaDevice || vku::contains(gpu.allocator.getAllocationMemoryProperties(stagingBuffer.allocation), vk::MemoryPropertyFlagBits::eDeviceLocal)) { return stagingBuffer; @@ -134,7 +153,7 @@ vku::AllocatedBuffer vk_gltf_viewer::gltf::AssetGpuBuffers::createPrimitiveBuffe vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferDst, } }; stagingInfos.emplace_back( - std::move(stagingBuffer), + std::move(stagingBuffer).unmap(), dstBuffer, vk::BufferCopy{ 0, 0, dstBuffer.size }); return dstBuffer; diff --git a/interface/MainApp.cppm b/interface/MainApp.cppm index 44612b5d..86035236 100644 --- a/interface/MainApp.cppm +++ b/interface/MainApp.cppm @@ -9,6 +9,7 @@ import :gltf.AssetGpuTextures; import :gltf.AssetGpuFallbackTexture; import :gltf.AssetSceneGpuBuffers; import :gltf.AssetSceneHierarchy; +import :gltf.MaterialVariantsMapping; import :vulkan.dsl.Asset; import :vulkan.dsl.ImageBasedLighting; import :vulkan.dsl.Scene; @@ -46,6 +47,11 @@ namespace vk_gltf_viewer { */ fastgltf::Asset asset; + /** + * @brief Associative data structure for KHR_materials_variants. + */ + gltf::MaterialVariantsMapping materialVariantsMapping { asset }; + private: const vulkan::Gpu &gpu; @@ -127,7 +133,7 @@ namespace vk_gltf_viewer { // glTF resources. // -------------------- - fastgltf::Parser parser { fastgltf::Extensions::KHR_materials_unlit | fastgltf::Extensions::KHR_texture_basisu | fastgltf::Extensions::EXT_mesh_gpu_instancing }; + fastgltf::Parser parser { fastgltf::Extensions::KHR_materials_unlit | fastgltf::Extensions::KHR_materials_variants | fastgltf::Extensions::KHR_texture_basisu | fastgltf::Extensions::EXT_mesh_gpu_instancing }; std::optional gltf; // -------------------- diff --git a/interface/control/ImGuiTaskCollector.cppm b/interface/control/ImGuiTaskCollector.cppm index 4235e3a7..c083d8eb 100644 --- a/interface/control/ImGuiTaskCollector.cppm +++ b/interface/control/ImGuiTaskCollector.cppm @@ -20,6 +20,7 @@ namespace vk_gltf_viewer::control { void menuBar(const std::list &recentGltfs, const std::list &recentSkyboxes); void assetInspector(fastgltf::Asset &asset, const std::filesystem::path &assetDir); void materialEditor(fastgltf::Asset &asset, std::span assetTextureImGuiDescriptorSets); + void materialVariants(const fastgltf::Asset &asset); void sceneHierarchy(fastgltf::Asset &asset, std::size_t sceneIndex, const std::variant>, std::vector> &visibilities, const std::optional &hoveringNodeIndex, const std::unordered_set &selectedNodeIndices); void nodeInspector(fastgltf::Asset &asset, const std::unordered_set &selectedNodeIndices); void background(bool canSelectSkyboxBackground, full_optional &solidBackground); diff --git a/interface/control/Task.cppm b/interface/control/Task.cppm index 3382baba..a4bcb37b 100644 --- a/interface/control/Task.cppm +++ b/interface/control/Task.cppm @@ -21,6 +21,7 @@ namespace vk_gltf_viewer::control { struct TightenNearFarPlane { }; struct ChangeCameraView { }; struct InvalidateDrawCommandSeparation { }; + struct SelectMaterialVariants { std::size_t variantIndex; }; } export using Task = std::variant< @@ -39,5 +40,6 @@ namespace vk_gltf_viewer::control { task::ChangeSelectedNodeWorldTransform, task::TightenNearFarPlane, task::ChangeCameraView, - task::InvalidateDrawCommandSeparation>; + task::InvalidateDrawCommandSeparation, + task::SelectMaterialVariants>; } \ No newline at end of file diff --git a/interface/gltf/AssetGpuBuffers.cppm b/interface/gltf/AssetGpuBuffers.cppm index d91375de..ab83d161 100644 --- a/interface/gltf/AssetGpuBuffers.cppm +++ b/interface/gltf/AssetGpuBuffers.cppm @@ -164,11 +164,13 @@ namespace vk_gltf_viewer::gltf { */ std::unordered_map indexBuffers; + private: /** * @brief Buffer that contains GpuPrimitives. */ - vku::AllocatedBuffer primitiveBuffer = createPrimitiveBuffer(); + std::variant primitiveBuffer = createPrimitiveBuffer(); + public: template AssetGpuBuffers( const fastgltf::Asset &asset, @@ -198,6 +200,8 @@ namespace vk_gltf_viewer::gltf { } } + [[nodiscard]] vk::Buffer getPrimitiveBuffer() const noexcept { return visit_as(primitiveBuffer); } + /** * @brief Get the primitive by its order, which has the same manner of primitiveBuffer. * @param index The order of the primitive. @@ -205,6 +209,15 @@ namespace vk_gltf_viewer::gltf { */ [[nodiscard]] const fastgltf::Primitive &getPrimitiveByOrder(std::uint16_t index) const { return *orderedPrimitives[index]; } + /** + * @brief Update \p primitive's material index inside the GPU buffer. + * @param primitive Primitive to update. + * @param materialIndex New material index. + * @param transferCommandBuffer If buffer is not host-visible memory and so is unable to be updated from the host, this command buffer will be used for recording the buffer update command. Then, its execution MUST be synchronized to be available to the primitiveBuffer's usage. Otherwise, this parameter is not used. + * @return true if the buffer is not host-visible memory and the update command is recorded, false otherwise. + */ + [[nodiscard]] bool updatePrimitiveMaterial(const fastgltf::Primitive &primitive, std::uint32_t materialIndex, vk::CommandBuffer transferCommandBuffer); + private: [[nodiscard]] std::vector createOrderedPrimitives() const; [[nodiscard]] std::unordered_map createPrimitiveInfos() const; @@ -328,7 +341,7 @@ namespace vk_gltf_viewer::gltf { | std::ranges::to(); } - [[nodiscard]] vku::AllocatedBuffer createPrimitiveBuffer(); + [[nodiscard]] std::variant createPrimitiveBuffer(); template void createPrimitiveAttributeBuffers(const DataBufferAdapter &adapter) { @@ -539,5 +552,7 @@ namespace vk_gltf_viewer::gltf { internalBuffers.emplace_back(std::move(buffer)); } + + [[nodiscard]] static std::uint32_t padMaterialIndex(std::uint32_t materialIndex) noexcept { return materialIndex + 1; } }; } \ No newline at end of file diff --git a/interface/gltf/MaterialVariantsMapping.cppm b/interface/gltf/MaterialVariantsMapping.cppm new file mode 100644 index 00000000..b29dedbd --- /dev/null +++ b/interface/gltf/MaterialVariantsMapping.cppm @@ -0,0 +1,37 @@ +export module vk_gltf_viewer:gltf.MaterialVariantsMapping; + +import std; +export import fastgltf; +import :helpers.ranges; + +namespace vk_gltf_viewer::gltf { + /** + * @brief Associative data structure of mappings for KHR_materials_variants. + * + * KHR_materials_variants extension defines the material variants for each primitive. For each variant index, you can call `at` to get the list of primitives and their material indices that use the corresponding material variant. + */ + class MaterialVariantsMapping { + public: + struct VariantPrimitive { + fastgltf::Primitive *pPrimitive; + std::uint32_t materialIndex; + }; + + explicit MaterialVariantsMapping(fastgltf::Asset &asset [[clang::lifetimebound]]) noexcept { + for (fastgltf::Primitive &primitive : asset.meshes | std::views::transform(&fastgltf::Mesh::primitives) | std::views::join) { + for (const auto &[materialVariantIndex, mapping] : primitive.mappings | ranges::views::enumerate) { + if (mapping) { + data[materialVariantIndex].emplace_back(&primitive, *mapping); + } + } + } + } + + [[nodiscard]] std::span at(std::size_t materialVariantIndex) const { + return data.at(materialVariantIndex); + } + + private: + std::unordered_map> data; + }; +} \ No newline at end of file