diff --git a/CMakeLists.txt b/CMakeLists.txt index 18a9273..8ead5a6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -158,6 +158,7 @@ target_sources(vk-gltf-viewer PRIVATE interface/vulkan/buffer/CubeIndices.cppm interface/vulkan/buffer/IndirectDrawCommands.cppm interface/vulkan/buffer/MeshWeights.cppm + interface/vulkan/buffer/MeshNodeWorldTransforms.cppm interface/vulkan/descriptor_set_layout/Asset.cppm interface/vulkan/descriptor_set_layout/ImageBasedLighting.cppm interface/vulkan/descriptor_set_layout/Scene.cppm diff --git a/impl/MainApp.cpp b/impl/MainApp.cpp index 1872a83..5fd689e 100644 --- a/impl/MainApp.cpp +++ b/impl/MainApp.cpp @@ -426,8 +426,8 @@ void vk_gltf_viewer::MainApp::run() { // Update the current and its descendant nodes' world transforms in sceneHierarchy. gltf->sceneHierarchy.updateDescendantNodeTransformsFrom(task.nodeIndex, nodeWorldTransform); - // Passing sceneHierarchy into sceneGpuBuffers to update GPU mesh node transform buffer. - gltf->sceneGpuBuffers.updateMeshNodeTransformsFrom(task.nodeIndex, gltf->sceneHierarchy, gltf->assetExternalBuffers); + // Passing sceneHierarchy into meshNodeWorldTransforms to update GPU mesh node transform buffer. + gltf->meshNodeWorldTransforms.updateTransform(task.nodeIndex, gltf->sceneHierarchy, gltf->assetExternalBuffers); // Scene enclosing sphere would be changed. Adjust the camera's near/far plane if necessary. if (appState.automaticNearFarPlaneAdjustment) { @@ -435,7 +435,7 @@ void vk_gltf_viewer::MainApp::run() { = gltf->sceneMiniball = gltf::algorithm::getMiniball( gltf->asset, gltf->scene, [this](std::size_t nodeIndex, std::size_t instanceIndex) { - return cast(gltf->sceneGpuBuffers.getMeshNodeWorldTransform(nodeIndex, instanceIndex)); + return cast(gltf->meshNodeWorldTransforms.getTransform(nodeIndex, instanceIndex)); }); appState.camera.tightenNearFar(glm::make_vec3(center.data()), radius); } @@ -489,7 +489,7 @@ void vk_gltf_viewer::MainApp::run() { gltf->sceneHierarchy.updateDescendantNodeTransformsFrom(selectedNodeIndex, selectedNodeWorldTransform); // Passing sceneHierarchy into sceneGpuBuffers to update GPU mesh node transform buffer. - gltf->sceneGpuBuffers.updateMeshNodeTransformsFrom(selectedNodeIndex, gltf->sceneHierarchy, gltf->assetExternalBuffers); + gltf->meshNodeWorldTransforms.updateTransform(selectedNodeIndex, gltf->sceneHierarchy, gltf->assetExternalBuffers); // Scene enclosing sphere would be changed. Adjust the camera's near/far plane if necessary. if (appState.automaticNearFarPlaneAdjustment) { @@ -497,7 +497,7 @@ void vk_gltf_viewer::MainApp::run() { = gltf->sceneMiniball = gltf::algorithm::getMiniball( gltf->asset, gltf->scene, [this](std::size_t nodeIndex, std::size_t instanceIndex) { - return cast(gltf->sceneGpuBuffers.getMeshNodeWorldTransform(nodeIndex, instanceIndex)); + return cast(gltf->meshNodeWorldTransforms.getTransform(nodeIndex, instanceIndex)); }); appState.camera.tightenNearFar(glm::make_vec3(center.data()), radius); } @@ -657,17 +657,20 @@ vk_gltf_viewer::MainApp::Gltf::Gltf( gpu { gpu }, assetGpuBuffers { asset, gpu, threadPool, assetExternalBuffers }, assetGpuTextures { asset, directory, gpu, threadPool, assetExternalBuffers }, - sceneGpuBuffers { asset, scene, sceneHierarchy, meshWeights, gpu, assetExternalBuffers }, - sceneMiniball { gltf::algorithm::getMiniball(asset, scene, [this](std::size_t nodeIndex, std::size_t instanceIndex) { - return cast(sceneGpuBuffers.getMeshNodeWorldTransform(nodeIndex, instanceIndex)); - }) } { } + sceneGpuBuffers { asset, meshNodeWorldTransforms, meshWeights, gpu }, + // TODO: meshNodeWorldTransforms.updateTransform is inserted before sceneMiniball construction since it requires + // meshNodeWorldTransforms.getTransform returns the proper data. This is a stopgap solution and refactoring needed. + sceneMiniball { (meshNodeWorldTransforms.updateTransform(scene, sceneHierarchy, assetExternalBuffers), gltf::algorithm::getMiniball(asset, scene, [this](std::size_t nodeIndex, std::size_t instanceIndex) { + return cast(meshNodeWorldTransforms.getTransform(nodeIndex, instanceIndex)); + })) } { } void vk_gltf_viewer::MainApp::Gltf::setScene(std::size_t sceneIndex) { scene = asset.scenes[sceneIndex]; sceneHierarchy = { asset, scene }; - sceneGpuBuffers = { asset, scene, sceneHierarchy, meshWeights, gpu, assetExternalBuffers }; + meshNodeWorldTransforms.updateTransform(asset.scenes[sceneIndex], sceneHierarchy, assetExternalBuffers); + sceneGpuBuffers = { asset, meshNodeWorldTransforms, meshWeights, gpu }; sceneMiniball = gltf::algorithm::getMiniball(asset, scene, [this](std::size_t nodeIndex, std::size_t instanceIndex) { - return cast(sceneGpuBuffers.getMeshNodeWorldTransform(nodeIndex, instanceIndex)); + return cast(meshNodeWorldTransforms.getTransform(nodeIndex, instanceIndex)); }); } diff --git a/impl/gltf/AssetSceneGpuBuffers.cpp b/impl/gltf/AssetSceneGpuBuffers.cpp index 910eed5..c327d49 100644 --- a/impl/gltf/AssetSceneGpuBuffers.cpp +++ b/impl/gltf/AssetSceneGpuBuffers.cpp @@ -6,51 +6,15 @@ module vk_gltf_viewer; import :gltf.AssetSceneGpuBuffers; import std; -import :gltf.algorithm.traversal; import :helpers.fastgltf; import :helpers.ranges; -#define FWD(...) static_cast(__VA_ARGS__) -#define LIFT(...) [&](auto &&...xs) { return (__VA_ARGS__)(FWD(xs)...); } - -const fastgltf::math::fmat4x4 &vk_gltf_viewer::gltf::AssetSceneGpuBuffers::getMeshNodeWorldTransform(std::uint16_t nodeIndex, std::uint32_t instanceIndex) const noexcept { - return meshNodeWorldTransformBuffer.asRange()[instanceOffsets[nodeIndex] + instanceIndex]; -} - -std::vector vk_gltf_viewer::gltf::AssetSceneGpuBuffers::createInstanceCounts(const fastgltf::Scene &scene) const { - std::vector result(pAsset->nodes.size(), 0U); - algorithm::traverseScene(*pAsset, scene, [&](std::size_t nodeIndex) { - result[nodeIndex] = [&]() -> std::uint32_t { - const fastgltf::Node &node = pAsset->nodes[nodeIndex]; - if (!node.meshIndex) { - return 0; - } - if (node.instancingAttributes.empty()) { - return 1; - } - else { - // According to the EXT_mesh_gpu_instancing specification, all attribute accessors in a given node - // must have the same count. Therefore, we can use the count of the first attribute accessor. - return pAsset->accessors[node.instancingAttributes[0].accessorIndex].count; - } - }(); - }); - return result; -} - -std::vector vk_gltf_viewer::gltf::AssetSceneGpuBuffers::createInstanceOffsets() const { - std::vector result(instanceCounts.size()); - std::exclusive_scan(instanceCounts.cbegin(), instanceCounts.cend(), result.begin(), 0U); - return result; -} - vku::AllocatedBuffer vk_gltf_viewer::gltf::AssetSceneGpuBuffers::createNodeBuffer( const fastgltf::Asset &asset, + const vulkan::buffer::MeshNodeWorldTransforms &meshNodeWorldTransforms, const vulkan::buffer::MeshWeights &meshWeights, const vulkan::Gpu &gpu ) const { - const vk::DeviceAddress nodeTransformBufferStartAddress = gpu.device.getBufferAddress({ meshNodeWorldTransformBuffer }); - vku::AllocatedBuffer stagingBuffer = vku::MappedBuffer { gpu.allocator, std::from_range, ranges::views::upto(asset.nodes.size()) | std::views::transform([&](std::size_t nodeIndex) { @@ -60,7 +24,7 @@ vku::AllocatedBuffer vk_gltf_viewer::gltf::AssetSceneGpuBuffers::createNodeBuffe return meshWeights.segments[meshIndex].startAddress; }) .value_or(vk::DeviceAddress { 0 }), - nodeTransformBufferStartAddress + sizeof(fastgltf::math::fmat4x4) * instanceOffsets[nodeIndex], + meshNodeWorldTransforms.getTransformStartAddress(nodeIndex), }; }), gpu.isUmaDevice ? vk::BufferUsageFlagBits::eStorageBuffer : vk::BufferUsageFlagBits::eTransferSrc, diff --git a/interface/MainApp.cppm b/interface/MainApp.cppm index d9d202b..d78db68 100644 --- a/interface/MainApp.cppm +++ b/interface/MainApp.cppm @@ -87,6 +87,8 @@ namespace vk_gltf_viewer { */ gltf::AssetSceneHierarchy sceneHierarchy { asset, scene }; + vulkan::buffer::MeshNodeWorldTransforms meshNodeWorldTransforms { asset, gpu, assetExternalBuffers }; + /** * @brief GPU buffers that are used for rendering the current scene. */ diff --git a/interface/gltf/AssetSceneGpuBuffers.cppm b/interface/gltf/AssetSceneGpuBuffers.cppm index 6942d2e..c6cf9a4 100644 --- a/interface/gltf/AssetSceneGpuBuffers.cppm +++ b/interface/gltf/AssetSceneGpuBuffers.cppm @@ -2,15 +2,12 @@ export module vk_gltf_viewer:gltf.AssetSceneGpuBuffers; import std; export import fastgltf; -import :gltf.algorithm.traversal; export import :gltf.AssetPrimitiveInfo; -export import :gltf.AssetSceneHierarchy; import :helpers.concepts; -import :helpers.fastgltf; import :helpers.functional; -import :helpers.ranges; export import :vulkan.Gpu; export import :vulkan.buffer.IndirectDrawCommands; +export import :vulkan.buffer.MeshNodeWorldTransforms; export import :vulkan.buffer.MeshWeights; namespace vk_gltf_viewer::gltf { @@ -23,81 +20,19 @@ namespace vk_gltf_viewer::gltf { export class AssetSceneGpuBuffers { const fastgltf::Asset *pAsset; - std::vector instanceCounts; - std::vector instanceOffsets = createInstanceOffsets(); - public: - /** - * @brief Buffer that stores the mesh nodes' transform matrices, with flattened instance matrices. - * - * The term "mesh node" means a node that has a mesh. This buffer only contains transform matrices of mesh nodes. In other words, meshNodeWorldTransformBuffer.asRange()[nodeIndex] may NOT represent the world transformation matrix of the nodeIndex-th node, because maybe there were nodes with no mesh prior to the nodeIndex-th node. - * - * For example, a scene has 4 nodes (denoted as A B C D) and A has 2 instances (M1, M2), B has 3 instances (M3, M4, M5), C is meshless, and D has 1 instance (M6), then the flattened matrices will be laid out as: - * @code - * [MA * M1, MA * M2, MB * M3, MB * M4, MB * M5, MD * M6] - * @endcode - * Be careful that there is no transform matrix related about node C, because it is meshless. - */ - vku::MappedBuffer meshNodeWorldTransformBuffer; - /** * @brief Buffer that stores the start address of the flattened node world transform matrices buffer. */ vku::AllocatedBuffer nodeBuffer; - template AssetSceneGpuBuffers( const fastgltf::Asset &asset [[clang::lifetimebound]], - const fastgltf::Scene &scene [[clang::lifetimebound]], - const AssetSceneHierarchy &sceneHierarchy, - const vulkan::buffer::MeshWeights &meshWeights, - const vulkan::Gpu &gpu [[clang::lifetimebound]], - const BufferDataAdapter &adapter = {} + const vulkan::buffer::MeshNodeWorldTransforms &meshNodeWorldTransforms [[clang::lifetimebound]], + const vulkan::buffer::MeshWeights &meshWeights [[clang::lifetimebound]], + const vulkan::Gpu &gpu [[clang::lifetimebound]] ) : pAsset { &asset }, - instanceCounts { createInstanceCounts(scene) }, - meshNodeWorldTransformBuffer { createMeshNodeWorldTransformBuffer(scene, sceneHierarchy, gpu.allocator, adapter) }, - nodeBuffer { createNodeBuffer(asset, meshWeights, gpu) } { } - - /** - * @brief Get world transform matrix of \p nodeIndex-th mesh node's \p instanceIndex-th instance in the scene. - * - * The term "mesh node" means a node that has a mesh, and this function only cares about mesh nodes (you MUST NOT pass a node index that doesn't have a mesh). - * - * @param nodeIndex Index of the mesh node. - * @param instanceIndex Index of the instance in the node. If EXT_mesh_gpu_instancing extension is not used, this value must be 0 (omitted). - * @return World transformation matrix of the mesh node's instance, calculated by post-multiply accumulated transformation matrices from scene root. - * @warning \p nodeIndex-th node MUST have a mesh. No exception thrown for constraint violation. - * @warning \p instanceIndex-th instance MUST be less than the instance count of the node. No exception thrown for constraint violation. - */ - [[nodiscard]] const fastgltf::math::fmat4x4 &getMeshNodeWorldTransform(std::uint16_t nodeIndex, std::uint32_t instanceIndex = 0) const noexcept; - - /** - * @brief Update the mesh node world transforms from given \p nodeIndex, to its descendants. - * @tparam BufferDataAdapter A functor type that acquires the binary buffer data from a glTF buffer view. If you provided fastgltf::Options::LoadExternalBuffers to the fastgltf::Parser while loading the glTF, the parameter can be omitted. - * @param nodeIndex Node index to be started. The target node MUST have a mesh. - * @param sceneHierarchy Scene hierarchy that contains the world transform matrices of the nodes. - * @param adapter Buffer data adapter. - */ - template - void updateMeshNodeTransformsFrom(std::uint16_t nodeIndex, const AssetSceneHierarchy &sceneHierarchy, const BufferDataAdapter &adapter = {}) { - const std::span meshNodeWorldTransforms = meshNodeWorldTransformBuffer.asRange(); - algorithm::traverseNode(*pAsset, nodeIndex, [&](std::size_t nodeIndex) { - const fastgltf::Node &node = pAsset->nodes[nodeIndex]; - if (!node.meshIndex) { - return; - } - - if (std::vector instanceTransforms = getInstanceTransforms(*pAsset, node, adapter); instanceTransforms.empty()) { - meshNodeWorldTransforms[instanceOffsets[nodeIndex]] = sceneHierarchy.nodeWorldTransforms[nodeIndex]; - } - else { - for (std::uint32_t instanceIndex : ranges::views::upto(instanceCounts[nodeIndex])) { - meshNodeWorldTransforms[instanceOffsets[nodeIndex] + instanceIndex] - = sceneHierarchy.nodeWorldTransforms[nodeIndex] * instanceTransforms[instanceIndex]; - } - } - }); - } + nodeBuffer { createNodeBuffer(asset, meshNodeWorldTransforms, meshWeights, gpu) } { } template < std::invocable CriteriaGetter, @@ -182,43 +117,9 @@ namespace vk_gltf_viewer::gltf { } private: - [[nodiscard]] std::vector createInstanceCounts(const fastgltf::Scene &scene) const; - [[nodiscard]] std::vector createInstanceOffsets() const; - - template - [[nodiscard]] vku::MappedBuffer createMeshNodeWorldTransformBuffer( - const fastgltf::Scene &scene, - const AssetSceneHierarchy &sceneHierarchy, - vma::Allocator allocator, - const BufferDataAdapter &adapter - ) const { - std::vector meshNodeWorldTransforms(instanceOffsets.back() + instanceCounts.back()); - algorithm::traverseScene(*pAsset, scene, [&](std::size_t nodeIndex) { - const fastgltf::Node &node = pAsset->nodes[nodeIndex]; - if (!node.meshIndex) { - return; - } - - if (std::vector instanceTransforms = getInstanceTransforms(*pAsset, node, adapter); instanceTransforms.empty()) { - meshNodeWorldTransforms[instanceOffsets[nodeIndex]] = sceneHierarchy.nodeWorldTransforms[nodeIndex]; - } - else { - for (std::uint32_t instanceIndex : ranges::views::upto(instanceCounts[nodeIndex])) { - meshNodeWorldTransforms[instanceOffsets[nodeIndex] + instanceIndex] - = sceneHierarchy.nodeWorldTransforms[nodeIndex] * instanceTransforms[instanceIndex]; - } - } - }); - - return vku::MappedBuffer { - allocator, - std::from_range, as_bytes(std::span { meshNodeWorldTransforms }), - vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eShaderDeviceAddress, - }; - } - [[nodiscard]] vku::AllocatedBuffer createNodeBuffer( const fastgltf::Asset &asset, + const vulkan::buffer::MeshNodeWorldTransforms &meshNodeWorldTransforms, const vulkan::buffer::MeshWeights &meshWeights, const vulkan::Gpu &gpu ) const; diff --git a/interface/vulkan/buffer/MeshNodeWorldTransforms.cppm b/interface/vulkan/buffer/MeshNodeWorldTransforms.cppm new file mode 100644 index 0000000..1b01c0d --- /dev/null +++ b/interface/vulkan/buffer/MeshNodeWorldTransforms.cppm @@ -0,0 +1,177 @@ +export module vk_gltf_viewer:vulkan.buffer.MeshNodeWorldTransforms; + +import std; +import glm; +import :gltf.algorithm.traversal; +export import :gltf.AssetSceneHierarchy; +import :helpers.ranges; +export import :vulkan.Gpu; + +/** + * Convert the span of \p U to the span of \p T. The result span byte size must be same as the \p span's. + * @tparam T Result span type. + * @tparam U Source span type. + * @param span Source span. + * @return Converted span. + * @note Since the source and result span sizes must be same, span.size_bytes() must be divisible by sizeof(T). + */ +template +[[nodiscard]] std::span reinterpret_span(std::span span) { + if (span.size_bytes() % sizeof(T) != 0) { + throw std::invalid_argument { "Span size mismatch: span of T does not fully fit into the current span." }; + } + + return { reinterpret_cast(span.data()), span.size_bytes() / sizeof(T) }; +} + +namespace vk_gltf_viewer::vulkan::buffer { + export class MeshNodeWorldTransforms { + public: + template + MeshNodeWorldTransforms( + const fastgltf::Asset &asset [[clang::lifetimebound]], + const Gpu &gpu [[clang::lifetimebound]], + const BufferDataAdapter &adapter = {} + ) : asset { asset }, + instanceOffsets { createInstanceOffsets() }, + buffer { createBuffer(gpu.allocator, adapter) }, + bufferAddress { gpu.device.getBufferAddress({ buffer }) } { } + + [[nodiscard]] vk::DeviceAddress getTransformStartAddress(std::size_t nodeIndex) const noexcept { + return bufferAddress + sizeof(fastgltf::math::fmat4x4) * instanceOffsets[nodeIndex]; + } + + /** + * @brief Get world transform matrix of \p nodeIndex-th mesh node's \p instanceIndex-th instance in the scene. + * + * The term "mesh node" means a node that has a mesh, and this function only cares about mesh nodes (you MUST NOT pass a node index that doesn't have a mesh). + * + * @param nodeIndex Index of the mesh node. + * @param instanceIndex Index of the instance in the node. If EXT_mesh_gpu_instancing extension is not used, this value must be 0 (omitted). + * @return World transformation matrix of the mesh node's instance, calculated by post-multiply accumulated transformation matrices from scene root. + * @warning \p nodeIndex-th node MUST have a mesh. No exception thrown for constraint violation. + * @warning \p instanceIndex-th instance MUST be less than the instance count of the node. No exception thrown for constraint violation. + */ + [[deprecated("This method reads the Vulkan buffer that may not be cached; unexpected performance degradation may occur.")]] + [[nodiscard]] const fastgltf::math::fmat4x4 &getTransform(std::size_t nodeIndex, std::uint32_t instanceIndex = 0) const noexcept { + const std::span meshNodeWorldTransforms = buffer.asRange(); + return meshNodeWorldTransforms[instanceOffsets[nodeIndex] + instanceIndex]; + } + + /** + * @brief Update the mesh node world transforms from given \p nodeIndex, to its descendants. + * @tparam BufferDataAdapter A functor type that acquires the binary buffer data from a glTF buffer view. If you provided fastgltf::Options::LoadExternalBuffers to the fastgltf::Parser while loading the glTF, the parameter can be omitted. + * @param nodeIndex Node index to be started. The target node MUST have a mesh. + * @param sceneHierarchy Scene hierarchy that contains the world transform matrices of the nodes. + * @param adapter Buffer data adapter. + */ + template + void updateTransform(std::size_t nodeIndex, const gltf::AssetSceneHierarchy &sceneHierarchy, const BufferDataAdapter &adapter = {}) { + const std::span meshNodeWorldTransforms = buffer.asRange(); + gltf::algorithm::traverseNode(asset, nodeIndex, [&](std::size_t nodeIndex) { + const fastgltf::Node &node = asset.get().nodes[nodeIndex]; + if (!node.meshIndex) { + return; + } + + if (std::vector instanceTransforms = getInstanceTransforms(asset, node, adapter); !instanceTransforms.empty()) { + std::ranges::transform( + instanceTransforms, &meshNodeWorldTransforms[instanceOffsets[nodeIndex]], + [&](const fastgltf::math::fmat4x4 &instanceTransform) { + return sceneHierarchy.nodeWorldTransforms[nodeIndex] * instanceTransform; + }); + } + else { + meshNodeWorldTransforms[instanceOffsets[nodeIndex]] = sceneHierarchy.nodeWorldTransforms[nodeIndex]; + } + }); + } + + /** + * @brief Update the mesh node world transforms for all nodes in a scene. + * @tparam BufferDataAdapter A functor type that acquires the binary buffer data from a glTF buffer view. If you provided fastgltf::Options::LoadExternalBuffers to the fastgltf::Parser while loading the glTF, the parameter can be omitted. + * @param scene Scene to be updated. + * @param sceneHierarchy Scene hierarchy that contains the world transform matrices of the nodes. + * @param adapter Buffer data adapter. + */ + template + void updateTransform(const fastgltf::Scene &scene, const gltf::AssetSceneHierarchy &sceneHierarchy, const BufferDataAdapter &adapter = {}) { + for (std::size_t nodeIndex : scene.nodeIndices) { + updateTransform(nodeIndex, sceneHierarchy, adapter); + } + } + + private: + std::reference_wrapper asset; + std::vector instanceOffsets; + + /** + * @brief Buffer that stores the mesh nodes' transform matrices, with flattened instance matrices. + * + * The term "mesh node" means a node that has a mesh. This buffer only contains transform matrices of mesh nodes. In other words, buffer.asRange()[nodeIndex] may NOT represent the world transformation matrix of the nodeIndex-th node, because maybe there were nodes with no mesh prior to the nodeIndex-th node. + * + * For example, a scene has 4 nodes (denoted as A B C D) and A has 2 instances (M1, M2), B has 3 instances (M3, M4, M5), C is meshless, and D has 1 instance (M6), then the flattened matrices will be laid out as: + * @code + * [MA * M1, MA * M2, MB * M3, MB * M4, MB * M5, MD * M6] + * @endcode + * Be careful that there is no transform matrix related about node C, because it is meshless. + */ + vku::MappedBuffer buffer; + vk::DeviceAddress bufferAddress; + + [[nodiscard]] std::vector createInstanceOffsets() const { + std::vector result; + result.reserve(asset.get().nodes.size() + 1); + result.append_range(asset.get().nodes | std::views::transform([this](const fastgltf::Node &node) -> std::uint32_t { + if (!node.meshIndex) { + return 0; + } + if (node.instancingAttributes.empty()) { + return 1; + } + else { + // According to the EXT_mesh_gpu_instancing specification, all attribute accessors in a given node + // must have the same count. Therefore, we can use the count of the first attribute accessor. + return asset.get().accessors[node.instancingAttributes[0].accessorIndex].count; + } + + })); + result.push_back(0); + + std::exclusive_scan(result.cbegin(), result.cend(), result.begin(), 0U); + + return result; + } + + template + [[nodiscard]] vku::MappedBuffer createBuffer(vma::Allocator allocator, const BufferDataAdapter &adapter) const { + vku::MappedBuffer result { allocator, vk::BufferCreateInfo { + {}, + sizeof(fastgltf::math::fmat4x4) * instanceOffsets.back(), + vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eShaderDeviceAddress, + } }; + + const std::span data = result.asRange(); + for (std::size_t nodeIndex : ranges::views::upto(asset.get().nodes.size())) { + const fastgltf::Node &node = asset.get().nodes[nodeIndex]; + const fastgltf::math::fmat4x4 nodeTransform = visit(fastgltf::visitor { + [](const fastgltf::TRS &trs) { return toMatrix(trs); }, + [](fastgltf::math::fmat4x4 matrix) { return matrix; }, + }, node.transform); + + if (std::vector instanceTransforms = getInstanceTransforms(asset, node, adapter); !instanceTransforms.empty()) { + std::ranges::transform( + instanceTransforms, &data[instanceOffsets[nodeIndex]], + [&](const fastgltf::math::fmat4x4 &instanceTransform) { + return nodeTransform * instanceTransform; + }); + } + else { + data[instanceOffsets[nodeIndex]] = nodeTransform; + } + } + + return result; + } + }; +} \ No newline at end of file