Skip to content

Commit

Permalink
WIP: separate node transform buffer from AssetSceneGpuBuffers.
Browse files Browse the repository at this point in the history
Code refactoring needed.
  • Loading branch information
stripe2933 committed Jan 26, 2025
1 parent a3bdc49 commit e95a0bc
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 154 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 14 additions & 11 deletions impl/MainApp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -426,16 +426,16 @@ 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) {
const auto &[center, radius]
= gltf->sceneMiniball
= gltf::algorithm::getMiniball(
gltf->asset, gltf->scene, [this](std::size_t nodeIndex, std::size_t instanceIndex) {
return cast<double>(gltf->sceneGpuBuffers.getMeshNodeWorldTransform(nodeIndex, instanceIndex));
return cast<double>(gltf->meshNodeWorldTransforms.getTransform(nodeIndex, instanceIndex));
});
appState.camera.tightenNearFar(glm::make_vec3(center.data()), radius);
}
Expand Down Expand Up @@ -489,15 +489,15 @@ 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) {
const auto &[center, radius]
= gltf->sceneMiniball
= gltf::algorithm::getMiniball(
gltf->asset, gltf->scene, [this](std::size_t nodeIndex, std::size_t instanceIndex) {
return cast<double>(gltf->sceneGpuBuffers.getMeshNodeWorldTransform(nodeIndex, instanceIndex));
return cast<double>(gltf->meshNodeWorldTransforms.getTransform(nodeIndex, instanceIndex));
});
appState.camera.tightenNearFar(glm::make_vec3(center.data()), radius);
}
Expand Down Expand Up @@ -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<double>(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<double>(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<double>(sceneGpuBuffers.getMeshNodeWorldTransform(nodeIndex, instanceIndex));
return cast<double>(meshNodeWorldTransforms.getTransform(nodeIndex, instanceIndex));
});
}

Expand Down
40 changes: 2 additions & 38 deletions impl/gltf/AssetSceneGpuBuffers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<decltype(__VA_ARGS__) &&>(__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<const fastgltf::math::fmat4x4>()[instanceOffsets[nodeIndex] + instanceIndex];
}

std::vector<std::uint32_t> vk_gltf_viewer::gltf::AssetSceneGpuBuffers::createInstanceCounts(const fastgltf::Scene &scene) const {
std::vector<std::uint32_t> 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<std::uint32_t> vk_gltf_viewer::gltf::AssetSceneGpuBuffers::createInstanceOffsets() const {
std::vector<std::uint32_t> 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) {
Expand All @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions interface/MainApp.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
111 changes: 6 additions & 105 deletions interface/gltf/AssetSceneGpuBuffers.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -23,81 +20,19 @@ namespace vk_gltf_viewer::gltf {
export class AssetSceneGpuBuffers {
const fastgltf::Asset *pAsset;

std::vector<std::uint32_t> instanceCounts;
std::vector<std::uint32_t> 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, <tt>meshNodeWorldTransformBuffer.asRange<const fastgltf::math::fmat4x4>()[nodeIndex]</tt> may NOT represent the world transformation matrix of the <tt>nodeIndex</tt>-th node, because maybe there were nodes with no mesh prior to the <tt>nodeIndex</tt>-th node.
*
* For example, a scene has 4 nodes (denoted as A B C D) and A has 2 instances (<tt>M1</tt>, <tt>M2</tt>), B has 3 instances (<tt>M3</tt>, <tt>M4</tt>, <tt>M5</tt>), C is meshless, and D has 1 instance (<tt>M6</tt>), 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 <typename BufferDataAdapter = fastgltf::DefaultBufferDataAdapter>
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 <tt>fastgltf::Options::LoadExternalBuffers</tt> to the <tt>fastgltf::Parser</tt> 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 <typename BufferDataAdapter = fastgltf::DefaultBufferDataAdapter>
void updateMeshNodeTransformsFrom(std::uint16_t nodeIndex, const AssetSceneHierarchy &sceneHierarchy, const BufferDataAdapter &adapter = {}) {
const std::span<fastgltf::math::fmat4x4> meshNodeWorldTransforms = meshNodeWorldTransformBuffer.asRange<fastgltf::math::fmat4x4>();
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<const AssetPrimitiveInfo&> CriteriaGetter,
Expand Down Expand Up @@ -182,43 +117,9 @@ namespace vk_gltf_viewer::gltf {
}

private:
[[nodiscard]] std::vector<std::uint32_t> createInstanceCounts(const fastgltf::Scene &scene) const;
[[nodiscard]] std::vector<std::uint32_t> createInstanceOffsets() const;

template <typename BufferDataAdapter = fastgltf::DefaultBufferDataAdapter>
[[nodiscard]] vku::MappedBuffer createMeshNodeWorldTransformBuffer(
const fastgltf::Scene &scene,
const AssetSceneHierarchy &sceneHierarchy,
vma::Allocator allocator,
const BufferDataAdapter &adapter
) const {
std::vector<fastgltf::math::fmat4x4> 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;
Expand Down
Loading

0 comments on commit e95a0bc

Please sign in to comment.