Files
o3de/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshInputBuffers.cpp
T
Chris Santora fec79a7d53 Moved the material slot list from ModelLodAsset to ModelAsset, so all the slots live in one main list. This removes data duplication between LODs and cleans up the code a bit.
I had to update the ModelLod class to take in both the ModelLodAsset and ModelAsset for initialization so it can fetch the slots for each mesh.

Signed-off-by: santorac <55155825+santorac@users.noreply.github.com>
2021-07-30 11:41:02 -07:00

686 lines
39 KiB
C++

/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <Atom/Feature/SkinnedMesh/SkinnedMeshInputBuffers.h>
#include <Atom/Feature/MorphTargets/MorphTargetInputBuffers.h>
#include <SkinnedMesh/SkinnedMeshOutputStreamManager.h>
#include <Atom/RPI.Reflect/ResourcePoolAssetCreator.h>
#include <Atom/RPI.Reflect/Buffer/BufferAssetCreator.h>
#include <Atom/RPI.Reflect/Model/ModelAssetCreator.h>
#include <Atom/RPI.Reflect/Model/ModelLodAssetCreator.h>
#include <Atom/RPI.Reflect/Model/MorphTargetMetaAsset.h>
#include <Atom/RPI.Reflect/Model/MorphTargetDelta.h>
#include <Atom/RPI.Public/Shader/ShaderResourceGroup.h>
#include <Atom/RPI.Public/Model/Model.h>
#include <Atom/RHI/Factory.h>
#include <AzCore/std/algorithm.h>
#include <AzCore/Math/PackedVector3.h>
namespace AZ
{
namespace Render
{
Data::Asset<RPI::BufferAsset> CreateBufferAsset(const void* data, const RHI::BufferViewDescriptor& viewDescriptor, RHI::BufferBindFlags bindFlags, Data::Asset<RPI::ResourcePoolAsset> resourcePoolAsset, const char* bufferName)
{
const uint32_t bufferSize = viewDescriptor.m_elementCount * viewDescriptor.m_elementSize;
Data::Asset<RPI::BufferAsset> asset;
{
RHI::BufferDescriptor bufferDescriptor;
bufferDescriptor.m_bindFlags = bindFlags;
bufferDescriptor.m_byteCount = bufferSize;
bufferDescriptor.m_alignment = viewDescriptor.m_elementSize;
RPI::BufferAssetCreator creator;
Uuid uuid = Uuid::CreateRandom();
creator.Begin(uuid);
creator.SetPoolAsset(resourcePoolAsset);
creator.SetBuffer(data, bufferDescriptor.m_byteCount, bufferDescriptor);
// Create a unique buffer name by combining the given, friendly buffer name with the uuid. Use isBrackents=false and isDashes=false to make it look less like some kind of AssetId that has any meaning.
creator.SetBufferName(AZStd::string::format("%s_%s", bufferName, uuid.ToString<AZStd::string>(false,false).c_str()));
creator.SetBufferViewDescriptor(viewDescriptor);
creator.End(asset);
}
return asset;
}
void SkinnedMeshInputLod::SetIndexCount(uint32_t indexCount)
{
m_indexCount = indexCount;
}
void SkinnedMeshInputLod::SetVertexCount(uint32_t vertexCount)
{
m_vertexCount = vertexCount;
}
uint32_t SkinnedMeshInputLod::GetVertexCount() const
{
return m_vertexCount;
}
void SkinnedMeshInputLod::SetIndexBufferAsset(const Data::Asset<RPI::BufferAsset> bufferAsset)
{
m_indexBufferAsset = bufferAsset;
m_indexBuffer = RPI::Buffer::FindOrCreate(bufferAsset);
}
void SkinnedMeshInputLod::CreateIndexBuffer(const uint32_t* data, const AZStd::string& bufferNamePrefix)
{
AZ_Assert(m_indexCount > 0, "SkinnedMeshInputLod::CreateIndexBuffer called with a index count of 0. Make sure SetIndexCount has been called before trying to create any buffers.");
RHI::BufferViewDescriptor indexBufferViewDescriptor = RHI::BufferViewDescriptor::CreateTyped(0, m_indexCount, AZ::RHI::Format::R32_UINT);
// Use the user-specified buffer name if it exits, or a default one otherwise.
const char* bufferName = !bufferNamePrefix.empty() ? bufferNamePrefix.c_str() : "SkinnedMeshStaticIndexBuffer";
Data::Asset<RPI::BufferAsset> bufferAsset = CreateBufferAsset(data, indexBufferViewDescriptor, RHI::BufferBindFlags::InputAssembly, SkinnedMeshVertexStreamPropertyInterface::Get()->GetStaticStreamResourcePool(), bufferName);
m_indexBufferAsset = bufferAsset;
m_indexBuffer = RPI::Buffer::FindOrCreate(bufferAsset);
}
void SkinnedMeshInputLod::CreateSkinningInputBuffer(void* data, SkinnedMeshInputVertexStreams inputStream, const AZStd::string& bufferNamePrefix)
{
AZ_Assert(m_vertexCount > 0, "SkinnedMeshInputLod::CreateSkinningInputBuffer called with a vertex count of 0. Make sure SetVertexCount has been called before trying to create any buffers.");
const SkinnedMeshVertexStreamInfo& streamInfo = SkinnedMeshVertexStreamPropertyInterface::Get()->GetInputStreamInfo(inputStream);
RHI::BufferViewDescriptor viewDescriptor = RHI::BufferViewDescriptor::CreateRaw(0, m_vertexCount * streamInfo.m_elementSize);
// Use the user-specified buffer name if it exits, or a default one from the streamInfo otherwise.
const char* bufferName = !bufferNamePrefix.empty() ? bufferNamePrefix.c_str() : streamInfo.m_bufferName.GetCStr();
Data::Asset<RPI::BufferAsset> bufferAsset = CreateBufferAsset(data, viewDescriptor, RHI::BufferBindFlags::ShaderRead, SkinnedMeshVertexStreamPropertyInterface::Get()->GetInputStreamResourcePool(), bufferName);
m_inputBufferAssets[static_cast<uint8_t>(inputStream)] = bufferAsset;
m_inputBuffers[static_cast<uint8_t>(inputStream)] = RPI::Buffer::FindOrCreate(bufferAsset);
}
void SkinnedMeshInputLod::SetModelLodAsset(const Data::Asset<RPI::ModelLodAsset>& modelLodAsset)
{
m_modelLodAsset = modelLodAsset;
}
void SkinnedMeshInputLod::SetSkinningInputBufferAsset(const Data::Asset<RPI::BufferAsset> bufferAsset, SkinnedMeshInputVertexStreams inputStream)
{
if (inputStream == SkinnedMeshInputVertexStreams::Color)
{
AZ_Assert(!m_hasStaticColors, "Attempting to set colors as skinning input (meaning they are dynamic) when they already exist as a static stream");
m_hasDynamicColors = true;
}
m_inputBufferAssets[static_cast<uint8_t>(inputStream)] = bufferAsset;
Data::Instance<RPI::Buffer> buffer = RPI::Buffer::FindOrCreate(bufferAsset);
m_inputBuffers[static_cast<uint8_t>(inputStream)] = buffer;
// Create a buffer view to use as input to the skinning shader
AZ::RHI::Ptr<AZ::RHI::BufferView> bufferView = RHI::Factory::Get().CreateBufferView();
bufferView->SetName(Name{ AZStd::string(buffer->GetBufferView()->GetName().GetStringView()) + "_SkinningInputBufferView" });
RHI::BufferViewDescriptor bufferViewDescriptor = bufferAsset->GetBufferViewDescriptor();
// 3-component float buffers are not supported on metal for non-input assembly buffer views, so use a float view instead
if (bufferViewDescriptor.m_elementFormat == RHI::Format::R32G32B32_FLOAT)
{
// Use one float per element, with 3x as many elements
bufferViewDescriptor = RHI::BufferViewDescriptor::CreateTyped(
bufferViewDescriptor.m_elementOffset * 3, bufferViewDescriptor.m_elementCount * 3, RHI::Format::R32_FLOAT);
}
[[maybe_unused]] RHI::ResultCode resultCode =
bufferView->Init(*buffer->GetRHIBuffer(), bufferViewDescriptor);
AZ_Error(
"SkinnedMeshInputBuffers", resultCode == RHI::ResultCode::Success,
"Failed to initialize buffer view for skinned mesh input.");
m_bufferViews[static_cast<uint8_t>(inputStream)] = bufferView;
}
void SkinnedMeshInputLod::SetStaticBufferAsset(const Data::Asset<RPI::BufferAsset> bufferAsset, SkinnedMeshStaticVertexStreams staticStream)
{
if (staticStream == SkinnedMeshStaticVertexStreams::Color)
{
AZ_Assert(!m_hasDynamicColors, "Attempting to set colors as a static stream on a skinned mesh, when they already exist as a dynamic stream");
m_hasStaticColors = true;
}
m_staticBufferAssets[static_cast<uint8_t>(staticStream)] = bufferAsset;
m_staticBuffers[static_cast<uint8_t>(staticStream)] = RPI::Buffer::FindOrCreate(bufferAsset);
}
void SkinnedMeshInputLod::CreateStaticBuffer(void* data, SkinnedMeshStaticVertexStreams staticStream, const AZStd::string& bufferNamePrefix)
{
AZ_Assert(m_vertexCount > 0, "SkinnedMeshInputLod::CreateStaticBuffer called with a vertex count of 0. Make sure SetVertexCount has been called before trying to create any buffers.");
const SkinnedMeshVertexStreamInfo& streamInfo = SkinnedMeshVertexStreamPropertyInterface::Get()->GetStaticStreamInfo(staticStream);
RHI::BufferViewDescriptor viewDescriptor = RHI::BufferViewDescriptor::CreateTyped(0, m_vertexCount, streamInfo.m_elementFormat);
// Use the user-specified buffer name if it exits, or a default one from the streamInfo otherwise.
const char* bufferName = !bufferNamePrefix.empty() ? bufferNamePrefix.c_str() : streamInfo.m_bufferName.GetCStr();
Data::Asset<RPI::BufferAsset> bufferAsset = CreateBufferAsset(data, viewDescriptor, RHI::BufferBindFlags::InputAssembly, SkinnedMeshVertexStreamPropertyInterface::Get()->GetStaticStreamResourcePool(), bufferName);
m_staticBufferAssets[static_cast<uint8_t>(staticStream)] = bufferAsset;
m_staticBuffers[static_cast<uint8_t>(staticStream)] = RPI::Buffer::FindOrCreate(bufferAsset);
}
void SkinnedMeshInputLod::SetSubMeshProperties(const AZStd::vector<SkinnedSubMeshProperties>& subMeshProperties)
{
m_subMeshProperties = subMeshProperties;
CreateSharedSubMeshBufferViews();
}
const AZStd::vector<SkinnedSubMeshProperties>& SkinnedMeshInputLod::GetSubMeshProperties() const
{
return m_subMeshProperties;
}
const Data::Asset<RPI::BufferAsset>& SkinnedMeshInputLod::GetSkinningInputBufferAsset(SkinnedMeshInputVertexStreams stream) const
{
return m_inputBufferAssets[static_cast<uint8_t>(stream)];
}
void SkinnedMeshInputLod::WaitForUpload()
{
m_indexBuffer->WaitForUpload();
for (const Data::Instance<RPI::Buffer>& inputBuffer : m_inputBuffers)
{
if (inputBuffer)
{
inputBuffer->WaitForUpload();
}
}
for (const Data::Instance<RPI::Buffer>& staticBuffer : m_staticBuffers)
{
if (staticBuffer)
{
staticBuffer->WaitForUpload();
}
}
}
void SkinnedMeshInputLod::CreateSharedSubMeshBufferViews()
{
AZStd::array_view<RPI::ModelLodAsset::Mesh> meshes = m_modelLodAsset->GetMeshes();
m_sharedSubMeshViews.resize(meshes.size());
// The index and static buffer views will be shared by all instances that use the same SkinnedMeshInputBuffers, so set them here
for (size_t i = 0; i < meshes.size(); ++i)
{
// Set the view into the index buffer
m_sharedSubMeshViews[i].m_indexBufferView = meshes[i].GetIndexBufferAssetView();
// Set the views into the static buffers
for (uint8_t staticStreamIndex = 0; staticStreamIndex < static_cast<uint8_t>(SkinnedMeshStaticVertexStreams::NumVertexStreams); ++staticStreamIndex)
{
// Skip colors if they don't exist or are dynamic
if (staticStreamIndex == static_cast<uint8_t>(SkinnedMeshStaticVertexStreams::Color) && !m_hasStaticColors)
{
continue;
}
const SkinnedMeshVertexStreamInfo& streamInfo = SkinnedMeshVertexStreamPropertyInterface::Get()->GetStaticStreamInfo(static_cast<SkinnedMeshStaticVertexStreams>(staticStreamIndex));
const RPI::BufferAssetView* bufferView = meshes[i].GetSemanticBufferAssetView(streamInfo.m_semantic.m_name);
if (bufferView)
{
m_sharedSubMeshViews[i].m_staticStreamViews[staticStreamIndex] = bufferView;
}
}
}
}
void SkinnedMeshInputLod::AddMorphTarget(const RPI::MorphTargetMetaAsset::MorphTarget& morphTarget, const Data::Asset<RPI::BufferAsset>& morphBufferAsset, const AZStd::string& bufferNamePrefix, float minWeight = 0.0f, float maxWeight = 1.0f)
{
m_morphTargetMetaDatas.push_back(MorphTargetMetaData{ minWeight, maxWeight, morphTarget.m_minPositionDelta, morphTarget.m_maxPositionDelta, morphTarget.m_numVertices, morphTarget.m_startIndex });
// Create a view into the larger per-lod morph buffer for this particular morph
RHI::BufferViewDescriptor morphView = RHI::BufferViewDescriptor::CreateStructured(morphTarget.m_startIndex, morphTarget.m_numVertices, sizeof(RPI::PackedCompressedMorphTargetDelta));
RPI::BufferAssetView morphTargetDeltaView{ morphBufferAsset, morphView };
m_morphTargetInputBuffers.push_back(aznew MorphTargetInputBuffers{ morphTargetDeltaView, bufferNamePrefix });
// If colors are going to be morphed, the SkinnedMeshInputLod needs to know so that it allocates memory for the dynamically updated colors
if (morphTarget.m_hasColorDeltas)
{
m_hasDynamicColors = true;
}
}
bool SkinnedMeshInputLod::HasDynamicColors() const
{
return m_hasDynamicColors;
}
const AZStd::vector<MorphTargetMetaData>& SkinnedMeshInputLod::GetMorphTargetMetaDatas() const
{
return m_morphTargetMetaDatas;
}
const AZStd::vector<AZStd::intrusive_ptr<MorphTargetInputBuffers>>& SkinnedMeshInputLod::GetMorphTargetInputBuffers() const
{
return m_morphTargetInputBuffers;
}
SkinnedMeshInputBuffers::SkinnedMeshInputBuffers() = default;
SkinnedMeshInputBuffers::~SkinnedMeshInputBuffers() = default;
void SkinnedMeshInputBuffers::SetAssetId(Data::AssetId assetId)
{
m_assetId = assetId;
}
void SkinnedMeshInputBuffers::SetLodCount(size_t lodCount)
{
AZ_Assert(lodCount <= RPI::ModelLodAsset::LodCountMax, "Attempting to set lod count of %d in SkinnedMeshInputBuffers, "
"which exceeds the maximum count of %d", lodCount, RPI::ModelLodAsset::LodCountMax);
m_lods.resize(lodCount);
}
size_t SkinnedMeshInputBuffers::GetLodCount() const
{
return m_lods.size();
}
void SkinnedMeshInputBuffers::SetLod(size_t lodIndex, const SkinnedMeshInputLod& lod)
{
AZ_Assert(lodIndex < m_lods.size(), "Attempting to set lod at index %d in SkinnedMeshInputBuffers, which is outside the range of %zu. "
"Make sure SetLodCount has been called before calling SetLod.", lodIndex, m_lods.size());
m_lods[lodIndex] = lod;
m_isUploadPending = true;
}
const SkinnedMeshInputLod& SkinnedMeshInputBuffers::GetLod(size_t lodIndex) const
{
AZ_Assert(lodIndex < m_lods.size(), "Attempting to get lod at index %d in SkinnedMeshInputBuffers, which is outside the range of %zu.", lodIndex, m_lods.size());
return m_lods[lodIndex];
}
AZStd::array_view<AZ::RHI::Ptr<RHI::BufferView>> SkinnedMeshInputBuffers::GetInputBufferViews(size_t lodIndex) const
{
return m_lods[lodIndex].m_bufferViews;
}
AZ::RHI::Ptr<const RHI::BufferView> SkinnedMeshInputBuffers::GetInputBufferView(size_t lodIndex, uint8_t inputStream) const
{
return m_lods[lodIndex].m_inputBuffers[inputStream]->GetBufferView();
}
uint32_t SkinnedMeshInputBuffers::GetVertexCount(size_t lodIndex) const
{
return m_lods[lodIndex].m_vertexCount;
}
void SkinnedMeshInputBuffers::SetBufferViewsOnShaderResourceGroup(size_t lodIndex, const Data::Instance<RPI::ShaderResourceGroup>& perInstanceSRG)
{
// Get the SRG indices for each input stream
for (uint8_t inputStream = 0; inputStream < static_cast<uint8_t>(SkinnedMeshInputVertexStreams::NumVertexStreams); ++inputStream)
{
// Skip colors if they don't exist or are not being morphed
if (inputStream == static_cast<uint8_t>(SkinnedMeshInputVertexStreams::Color) && !m_lods[lodIndex].m_hasDynamicColors)
{
continue;
}
const SkinnedMeshVertexStreamInfo& streamInfo = SkinnedMeshVertexStreamPropertyInterface::Get()->GetInputStreamInfo(static_cast<SkinnedMeshInputVertexStreams>(inputStream));
RHI::ShaderInputBufferIndex srgIndex = perInstanceSRG->FindShaderInputBufferIndex(streamInfo.m_shaderResourceGroupName);
AZ_Error("SkinnedMeshInputBuffers", srgIndex.IsValid(), "Failed to find shader input index for '%s' in the skinning compute shader per-instance SRG.", streamInfo.m_shaderResourceGroupName.GetCStr());
[[maybe_unused]] bool success = false;
if (m_lods[lodIndex].m_inputBuffers[inputStream])
{
success = perInstanceSRG->SetBufferView(srgIndex, m_lods[lodIndex].m_bufferViews[inputStream].get());
}
AZ_Error("SkinnedMeshInputBuffers", success, "Failed to bind buffer view for %s", streamInfo.m_bufferName.GetCStr());
}
// Set the vertex count
RHI::ShaderInputConstantIndex numVerticesIndex;
numVerticesIndex = perInstanceSRG->FindShaderInputConstantIndex(Name{ "m_numVertices" });
AZ_Error("SkinnedMeshInputBuffers", numVerticesIndex.IsValid(), "Failed to find shader input index for m_numVerticies in the skinning compute shader per-instance SRG.");
perInstanceSRG->SetConstant(numVerticesIndex, m_lods[lodIndex].m_vertexCount);
}
// Create a resource view that has a different type than the data it is viewing
static RHI::BufferViewDescriptor CreateResourceViewWithDifferentFormat(size_t offsetInBytes, uint32_t realElementCount, uint32_t realElementSize, RHI::Format format, RHI::BufferBindFlags overrideBindFlags)
{
RHI::BufferViewDescriptor viewDescriptor;
size_t elementOffset = offsetInBytes / aznumeric_cast<size_t>(RHI::GetFormatSize(format));
AZ_Assert(elementOffset <= std::numeric_limits<uint32_t>().max(), "The offset in bytes from the start of the SkinnedMeshOutputStream buffer is too large to be expressed as a uint32_t element offset in the BufferViewDescriptor.");
viewDescriptor.m_elementOffset = aznumeric_cast<uint32_t>(elementOffset);
viewDescriptor.m_elementCount = realElementCount * (realElementSize / RHI::GetFormatSize(format));
viewDescriptor.m_elementFormat = format;
viewDescriptor.m_elementSize = RHI::GetFormatSize(format);
viewDescriptor.m_overrideBindFlags = overrideBindFlags;
return viewDescriptor;
}
static bool AllocateLodStream(
uint8_t outputStreamIndex,
size_t vertexCount,
AZStd::intrusive_ptr<SkinnedMeshInstance> instance,
AZStd::vector<uint32_t>& streamOffsetsFromBufferStart,
AZStd::vector<AZStd::intrusive_ptr<SkinnedMeshOutputStreamAllocation>>& lodAllocations)
{
const SkinnedMeshOutputVertexStreamInfo& outputStreamInfo = SkinnedMeshVertexStreamPropertyInterface::Get()->GetOutputStreamInfo(static_cast<SkinnedMeshOutputVertexStreams>(outputStreamIndex));
// Positions use 2x the number of vertices to hold both the current frame and previous frame's data
size_t positionMultiplier = static_cast<SkinnedMeshOutputVertexStreams>(outputStreamIndex) == SkinnedMeshOutputVertexStreams::Position ? 2u : 1u;
AZStd::intrusive_ptr<SkinnedMeshOutputStreamAllocation> allocation = SkinnedMeshOutputStreamManagerInterface::Get()->Allocate(vertexCount * static_cast<size_t>(outputStreamInfo.m_elementSize) * positionMultiplier);
if (!allocation)
{
// Suppress the OnMemoryFreed signal when releasing the previous successful allocations
// The memory was already free before this function was called, so it's not really newly available memory
AZ_Error("SkinnedMeshInputBuffers", false, "Out of memory to create a skinned mesh instance. Consider increasing r_skinnedMeshInstanceMemoryPoolSize");
instance->m_allocations.push_back(lodAllocations);
instance->SuppressSignalOnDeallocate();
return false;
}
lodAllocations.push_back(allocation);
streamOffsetsFromBufferStart.push_back(aznumeric_cast<uint32_t>(allocation->GetVirtualAddress().m_ptr));
return true;
}
static bool AllocateMorphTargetsForLod(const SkinnedMeshInputLod& lod, size_t vertexCount, AZStd::intrusive_ptr<SkinnedMeshInstance> instance, AZStd::vector<AZStd::intrusive_ptr<SkinnedMeshOutputStreamAllocation>>& lodAllocations)
{
// If this skinned mesh lod has morph targets, allocate a buffer for the accumulated deltas that come from the morph target pass
if (lod.GetMorphTargetMetaDatas().size() > 0)
{
// Naively, we're going to allocate enough memory to store the accumulated delta for every vertex.
// This makes it simple for the skinning shader to index into the buffer, but the memory cost
// could be reduced by keeping a buffer that maps from vertexId to morph target delta offset ATOM-14427
// We're also using the skinned mesh output buffer, since it gives us a read-write pool of memory that can be
// used for dependency tracking between passes. This can be switched to a transient memory pool so that the memory is free
// later in the frame once skinning is finished ATOM-14429
size_t perVertexSizeInBytes = static_cast<size_t>(MorphTargetConstants::s_unpackedMorphTargetDeltaSizeInBytes) * MorphTargetConstants::s_morphTargetDeltaTypeCount;
if (lod.HasDynamicColors())
{
// Naively, if colors are morphed by any of the morph targets,
// we'll allocate enough memory to store the accumulated color deltas for every vertex in the lod.
// This could be reduced by ATOM-14427
// We assume that the model has been padded to include colors even for the meshes which don't use them
// this could be reduced by dispatching the skinning shade
// for one mesh at a time instead of the entire lod at once ATOM-15078
// Add four floats for colors
perVertexSizeInBytes += 4 * sizeof(float);
}
AZStd::intrusive_ptr<SkinnedMeshOutputStreamAllocation> allocation = SkinnedMeshOutputStreamManagerInterface::Get()->Allocate(vertexCount * perVertexSizeInBytes);
if (!allocation)
{
// Suppress the OnMemoryFreed signal when releasing the previous successful allocations
// The memory was already free before this function was called, so it's not really newly available memory
AZ_Error("SkinnedMeshInputBuffers", false, "Out of memory to create a skinned mesh instance. Consider increasing r_skinnedMeshInstanceMemoryPoolSize");
instance->m_allocations.push_back(lodAllocations);
instance->SuppressSignalOnDeallocate();
return false;
}
else
{
// We're using an offset into a global buffer to be able to access the morph target offsets in a bindless manner.
// The offset can at most be a 32-bit uint until AZSL supports 64-bit uints. This gives us a 4GB limit for where the
// morph target deltas can live. In practice, the offsets could end up outside that range even if less that 4GB is used
// if the memory becomes fragmented. To address it, we can split morph target deltas into their own buffer, allocate
// memory in pages with a buffer for each page, or create and bind a buffer view
// so we are not doing an offset from the beginning of the buffer
AZ_Error("SkinnedMeshInputBuffers", allocation->GetVirtualAddress().m_ptr < static_cast<uintptr_t>(std::numeric_limits<uint32_t>::max()), "Morph target deltas allocated from the skinned mesh memory pool are outside the range that can be accessed from the skinning shader");
MorphTargetInstanceMetaData instanceMetaData;
// Positions start at the beginning of the allocation
instanceMetaData.m_accumulatedPositionDeltaOffsetInBytes = allocation->GetVirtualAddress().m_ptr;
uint32_t deltaStreamSizeInBytes = vertexCount * MorphTargetConstants::s_unpackedMorphTargetDeltaSizeInBytes;
// Followed by normals, tangents, and bitangents
instanceMetaData.m_accumulatedNormalDeltaOffsetInBytes = instanceMetaData.m_accumulatedPositionDeltaOffsetInBytes + deltaStreamSizeInBytes;
instanceMetaData.m_accumulatedTangentDeltaOffsetInBytes = instanceMetaData.m_accumulatedNormalDeltaOffsetInBytes + deltaStreamSizeInBytes;
instanceMetaData.m_accumulatedBitangentDeltaOffsetInBytes = instanceMetaData.m_accumulatedTangentDeltaOffsetInBytes + deltaStreamSizeInBytes;
// Followed by colors
if (lod.HasDynamicColors())
{
instanceMetaData.m_accumulatedColorDeltaOffsetInBytes = instanceMetaData.m_accumulatedBitangentDeltaOffsetInBytes + deltaStreamSizeInBytes;
}
else
{
instanceMetaData.m_accumulatedColorDeltaOffsetInBytes = MorphTargetConstants::s_invalidDeltaOffset;
}
// Track both the allocation and the metadata in the instance
instance->m_morphTargetInstanceMetaData.push_back(instanceMetaData);
lodAllocations.push_back(allocation);
}
}
else
{
// No morph targets for this lod
MorphTargetInstanceMetaData instanceMetaData{ MorphTargetConstants::s_invalidDeltaOffset, MorphTargetConstants::s_invalidDeltaOffset, MorphTargetConstants::s_invalidDeltaOffset, MorphTargetConstants::s_invalidDeltaOffset };
instance->m_morphTargetInstanceMetaData.push_back(instanceMetaData);
}
return true;
}
static void AddSubMeshViewToModelLodCreator(
uint8_t outputStreamIndex,
uint32_t lodVertexCount,
uint32_t submeshVertexCount,
Data::Asset<RPI::BufferAsset> skinnedMeshOutputBufferAsset,
const AZStd::vector<uint32_t>& streamOffsetsFromBufferStart,
AZStd::vector<size_t>& subMeshOffsetsFromStreamStart,
RPI::ModelLodAssetCreator& modelLodCreator)
{
const SkinnedMeshOutputVertexStreamInfo& outputStreamInfo = SkinnedMeshVertexStreamPropertyInterface::Get()->GetOutputStreamInfo(static_cast<SkinnedMeshOutputVertexStreams>(outputStreamIndex));
// For the purpose of the model, which is fed to the static mesh feature processor, these buffer views are only going to be used as input assembly.
// The underlying buffer is still writable and will be written to by the skinning shader.
RHI::BufferViewDescriptor viewDescriptor = CreateResourceViewWithDifferentFormat(static_cast<size_t>(streamOffsetsFromBufferStart[outputStreamIndex]) + static_cast<size_t>(subMeshOffsetsFromStreamStart[outputStreamIndex]), submeshVertexCount, outputStreamInfo.m_elementSize, outputStreamInfo.m_elementFormat, RHI::BufferBindFlags::InputAssembly);
AZ_Assert(streamOffsetsFromBufferStart[outputStreamIndex] % outputStreamInfo.m_elementSize == 0, "The SkinnedMeshOutputStreamManager is supposed to guarantee that offsets can always align.");
RPI::BufferAssetView bufferView{ skinnedMeshOutputBufferAsset, viewDescriptor };
modelLodCreator.AddMeshStreamBuffer(outputStreamInfo.m_semantic, AZ::Name(), bufferView);
if (static_cast<SkinnedMeshOutputVertexStreams>(outputStreamIndex) == SkinnedMeshOutputVertexStreams::Position)
{
// Add stream buffer for position history
size_t positionHistoryBufferOffsetInBytes = streamOffsetsFromBufferStart[outputStreamIndex] + subMeshOffsetsFromStreamStart[outputStreamIndex] + lodVertexCount * outputStreamInfo.m_elementSize;
viewDescriptor.m_elementOffset = aznumeric_cast<uint32_t>(positionHistoryBufferOffsetInBytes / outputStreamInfo.m_elementSize);
bufferView = { skinnedMeshOutputBufferAsset, viewDescriptor };
modelLodCreator.AddMeshStreamBuffer(RHI::ShaderSemantic{ Name{"POSITIONT"} }, AZ::Name(), bufferView);
}
subMeshOffsetsFromStreamStart[outputStreamIndex] += viewDescriptor.m_elementCount * viewDescriptor.m_elementSize;
}
AZStd::intrusive_ptr<SkinnedMeshInstance> SkinnedMeshInputBuffers::CreateSkinnedMeshInstance() const
{
// This function creates a SkinnedMeshInstance which describes all the buffer views needed to write the output of the skinned mesh compute shader
// and a model which can be rendered by the MeshFeatureProcessor
// Static data that doesn't get modified during skinning (e.g. index buffer, uvs) is shared between all instances that use the same SkinnedMeshInputBuffers
// The buffers for this static data and the per sub-mesh views into these buffers were created when the SkinnedMeshInputBuffers was created.
// This function adds those views to the model when creating it
// For the output of the skinned mesh shader, each instance has unique vertex data that exists in a single buffer managed by the SkinnedMeshOutputStreamManager
// For a given stream all of the vertices for an entire lod is contiguous in memory, allowing the entire lod to be skinned at once in as part of a single dispatch
// The streams are de-interleaved, and each stream may reside independently within the output buffer as determined by the best fit allocator
// E.g. the positions may or may not be adjacent to normals, but all of the positions for a single lod with be contiguous
// To support multiple sub-meshes, views into each stream for each lod are created for the sub-meshes
// SkinnedMeshOutputBuffer[.....................................................................................................................................]
// lod0 Positions[^ ^] lod0Normals[^ ^] lod1Positions[^ ^] lod1Normals[^ ^]
// lod0 subMesh0+1 Positions[^ ^^ ^] lod0 subMesh0+1 Normals[^ ^^ ^] lod1 sm0+1 pos[^ ^^ ^] lod1 sm0+1 norm[^ ^^ ^]
AZ_PROFILE_FUNCTION(Debug::ProfileCategory::AzRender);
AZStd::intrusive_ptr<SkinnedMeshInstance> instance = aznew SkinnedMeshInstance;
// Each model gets a unique, random ID, so if the same source model is used for multiple instances, multiple target models will be created.
RPI::ModelAssetCreator modelCreator;
modelCreator.Begin(Uuid::CreateRandom());
// Using the filename as a name for the model
AZStd::string assetPath;
Data::AssetCatalogRequestBus::BroadcastResult(assetPath, &Data::AssetCatalogRequests::GetAssetPathById, m_assetId);
AZStd::string fullFileName;
AzFramework::StringFunc::Path::GetFullFileName(assetPath.c_str(), fullFileName);
modelCreator.SetName(fullFileName + "_SkinnedMeshOutput");
Data::Asset<RPI::BufferAsset> skinnedMeshOutputBufferAsset = SkinnedMeshOutputStreamManagerInterface::Get()->GetBufferAsset();
size_t lodIndex = 0;
for (const SkinnedMeshInputLod& lod : m_lods)
{
RPI::ModelLodAssetCreator modelLodCreator;
modelLodCreator.Begin(Data::AssetId(Uuid::CreateRandom()));
//
// Lod
//
// Add a reference to the shared index buffer
modelLodCreator.AddLodStreamBuffer(lod.m_indexBufferAsset);
// There is only one underlying buffer that houses all of the skinned mesh output streams for all skinned mesh instances
modelLodCreator.AddLodStreamBuffer(skinnedMeshOutputBufferAsset);
// Add references to the shared static buffers
// Only uv0 for now
modelLodCreator.AddLodStreamBuffer(lod.m_staticBufferAssets[static_cast<uint8_t>(SkinnedMeshStaticVertexStreams::UV_0)]);
// Track offsets for each stream, so that the sub-meshes know where to begin
AZStd::vector<uint32_t> streamOffsetsFromBufferStart;
AZStd::vector<AZStd::intrusive_ptr<SkinnedMeshOutputStreamAllocation>> lodAllocations;
// The skinning shader doesn't differentiate between sub-meshes, it just writes all the vertices at once.
// So we want to pack all the positions for each sub-mesh together, all the normals together, etc.
for (uint8_t outputStreamIndex = 0; outputStreamIndex < static_cast<uint8_t>(SkinnedMeshOutputVertexStreams::NumVertexStreams); ++outputStreamIndex)
{
// Skip colors if they don't exist or are not being morphed
if (outputStreamIndex == static_cast<uint8_t>(SkinnedMeshOutputVertexStreams::Color) && !lod.m_hasDynamicColors)
{
continue;
}
if (!AllocateLodStream(outputStreamIndex, aznumeric_cast<size_t>(lod.m_vertexCount), instance, streamOffsetsFromBufferStart, lodAllocations))
{
return nullptr;
}
}
if (!AllocateMorphTargetsForLod(lod, lod.m_vertexCount, instance, lodAllocations))
{
return nullptr;
}
instance->m_outputStreamOffsetsInBytes.push_back(streamOffsetsFromBufferStart);
instance->m_allocations.push_back(lodAllocations);
//
// Submesh
//
AZStd::vector<size_t> subMeshOffsetsFromStreamStart(streamOffsetsFromBufferStart.size(), 0);
// Iterate over each sub-mesh for the lod to create views into the buffers
AZ_Assert(lod.m_subMeshProperties.size() == lod.m_sharedSubMeshViews.size(), "Skinned sub-mesh property and view vectors are mis-matched in size.")
for (size_t i = 0; i < lod.m_subMeshProperties.size(); ++i)
{
modelLodCreator.BeginMesh();
// Set the index buffer view
modelLodCreator.SetMeshIndexBuffer(lod.m_sharedSubMeshViews[i].m_indexBufferView);
// Create and set the views into the skinning output buffers
for (uint8_t outputStreamIndex = 0; outputStreamIndex < static_cast<uint8_t>(SkinnedMeshOutputVertexStreams::NumVertexStreams); ++outputStreamIndex)
{
// Skip colors if they don't exist or are not being morphed
if (outputStreamIndex == static_cast<uint8_t>(SkinnedMeshOutputVertexStreams::Color) && !lod.m_hasDynamicColors)
{
continue;
}
AddSubMeshViewToModelLodCreator(outputStreamIndex, lod.m_vertexCount, lod.m_subMeshProperties[i].m_vertexCount, skinnedMeshOutputBufferAsset, streamOffsetsFromBufferStart, subMeshOffsetsFromStreamStart, modelLodCreator);
}
// Set the views into the static buffers
for (uint8_t staticStreamIndex = 0; staticStreamIndex < static_cast<uint8_t>(SkinnedMeshStaticVertexStreams::NumVertexStreams); ++staticStreamIndex)
{
// Skip colors if they don't exist or are dynamic
if (!lod.m_sharedSubMeshViews[i].m_staticStreamViews[staticStreamIndex]
|| (staticStreamIndex == static_cast<uint8_t>(SkinnedMeshStaticVertexStreams::Color) && !lod.m_hasStaticColors))
{
continue;
}
const SkinnedMeshVertexStreamInfo& staticStreamInfo = SkinnedMeshVertexStreamPropertyInterface::Get()->GetStaticStreamInfo(static_cast<SkinnedMeshStaticVertexStreams>(staticStreamIndex));
modelLodCreator.AddMeshStreamBuffer(staticStreamInfo.m_semantic, AZ::Name(), *lod.m_sharedSubMeshViews[i].m_staticStreamViews[staticStreamIndex]);
}
Aabb localAabb = lod.m_subMeshProperties[i].m_aabb;
modelLodCreator.SetMeshAabb(AZStd::move(localAabb));
modelCreator.AddMaterialSlot(lod.m_subMeshProperties[i].m_materialSlot);
modelLodCreator.SetMeshMaterialSlot(lod.m_subMeshProperties[i].m_materialSlot.m_stableId);
modelLodCreator.EndMesh();
}
Data::Asset<RPI::ModelLodAsset> lodAsset;
modelLodCreator.End(lodAsset);
if (!lodAsset.IsReady())
{
// [GFX TODO] During mesh reload the modelLodCreator could report errors and result in the lodAsset not ready.
return nullptr;
}
modelCreator.AddLodAsset(AZStd::move(lodAsset));
lodIndex++;
}
Data::Asset<RPI::ModelAsset> modelAsset;
modelCreator.End(modelAsset);
instance->m_model = RPI::Model::FindOrCreate(modelAsset);
return instance;
}
void SkinnedMeshInputBuffers::WaitForUpload()
{
if (m_isUploadPending)
{
for (SkinnedMeshInputLod& lod : m_lods)
{
lod.WaitForUpload();
}
m_isUploadPending = false;
}
}
bool SkinnedMeshInputBuffers::IsUploadPending() const
{
return m_isUploadPending;
}
} // namespace Render
}// namespace AZ