From 3505b8971b751f66794bde6edf69fd313ce6bf27 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 8 Jan 2025 18:48:50 +0100 Subject: [PATCH] Add support for up to `65536` skeleton bones in skinned meshes https://forum.flaxengine.com/t/import-fbx-with-bones-more-than-256/1196 --- Source/Engine/Content/Assets/ModelBase.cpp | 25 +++++++- Source/Engine/Content/Assets/SkinnedModel.cpp | 27 +++++++-- Source/Engine/Graphics/Models/Config.h | 2 +- .../Engine/Graphics/PixelFormatExtensions.cpp | 60 +++++++++++++++++++ .../Graphics/Shaders/GPUVertexLayout.cpp | 10 ++++ .../Engine/Graphics/Shaders/GPUVertexLayout.h | 7 +++ 6 files changed, 123 insertions(+), 8 deletions(-) diff --git a/Source/Engine/Content/Assets/ModelBase.cpp b/Source/Engine/Content/Assets/ModelBase.cpp index bee811a72..747e7d2f9 100644 --- a/Source/Engine/Content/Assets/ModelBase.cpp +++ b/Source/Engine/Content/Assets/ModelBase.cpp @@ -613,6 +613,15 @@ bool ModelBase::SaveLOD(WriteStream& stream, const ModelData& modelData, int32 l Array> vbElements; const bool useSeparatePositions = !isSkinned; const bool useSeparateColors = !isSkinned; + PixelFormat blendIndicesFormat = PixelFormat::R8G8B8A8_UInt; + for (const Int4& indices : mesh.BlendIndices) + { + if (indices.MaxValue() > MAX_uint8) + { + blendIndicesFormat = PixelFormat::R16G16B16A16_UInt; + break; + } + } { byte vbIndex = 0; // TODO: add option to quantize vertex positions (eg. 16-bit) @@ -640,7 +649,7 @@ bool ModelBase::SaveLOD(WriteStream& stream, const ModelData& modelData, int32 l vb.Add({ VertexElement::Types::Tangent, vbIndex, 0, 0, PixelFormat::R10G10B10A2_UNorm }); if (isSkinned) { - vb.Add({ VertexElement::Types::BlendIndices, vbIndex, 0, 0, PixelFormat::R8G8B8A8_UInt }); + vb.Add({ VertexElement::Types::BlendIndices, vbIndex, 0, 0, blendIndicesFormat }); vb.Add({ VertexElement::Types::BlendWeights, vbIndex, 0, 0, PixelFormat::R16G16B16A16_Float }); } if (!useSeparateColors && hasColors) @@ -714,8 +723,18 @@ bool ModelBase::SaveLOD(WriteStream& stream, const ModelData& modelData, int32 l case VertexElement::Types::BlendIndices: { const Int4 blendIndices = mesh.BlendIndices.Get()[vertex]; - const Color32 blendIndicesEnc(blendIndices.X, blendIndices.Y, blendIndices.Z, blendIndices.W); - stream.Write(blendIndicesEnc); + if (blendIndicesFormat == PixelFormat::R8G8B8A8_UInt) + { + // 8-bit indices + const Color32 blendIndicesEnc(blendIndices.X, blendIndices.Y, blendIndices.Z, blendIndices.W); + stream.Write(blendIndicesEnc); + } + else + { + // 16-bit indices + const uint16 blendIndicesEnc[4] = { (uint16)blendIndices.X, (uint16)blendIndices.Y, (uint16)blendIndices.Z, (uint16)blendIndices.W }; + stream.Write(blendIndicesEnc); + } break; } case VertexElement::Types::BlendWeights: diff --git a/Source/Engine/Content/Assets/SkinnedModel.cpp b/Source/Engine/Content/Assets/SkinnedModel.cpp index ff3a5036a..acfa53a09 100644 --- a/Source/Engine/Content/Assets/SkinnedModel.cpp +++ b/Source/Engine/Content/Assets/SkinnedModel.cpp @@ -396,8 +396,27 @@ bool SkinnedModel::SetupSkeleton(const Array& nodes, const Array MAX_uint16) return true; - if (bones.Count() <= 0 || bones.Count() > MODEL_MAX_BONES_PER_MODEL) - return true; + if (bones.Count() <= 0) + { + if (bones.Count() > 255) + { + for (auto& lod : LODs) + { + for (auto& mesh : lod.Meshes) + { + auto* vertexLayout = mesh.GetVertexLayout(); + VertexElement element = vertexLayout ? vertexLayout->FindElement(VertexElement::Types::BlendIndices) : VertexElement(); + if (element.Format == PixelFormat::R8G8B8A8_UInt) + { + LOG(Warning, "Cannot use more than 255 bones if skinned model uses 8-bit storage for blend indices in vertices."); + return true; + } + } + } + } + if (bones.Count() > MODEL_MAX_BONES_PER_MODEL) + return true; + } auto model = this; if (!model->IsVirtual()) return true; @@ -557,7 +576,7 @@ bool SkinnedModel::LoadHeader(ReadStream& stream, byte& headerVersion) lod._model = this; lod._lodIndex = lodIndex; stream.Read(lod.ScreenSize); - + // Meshes uint16 meshesCount; stream.Read(meshesCount); @@ -579,7 +598,7 @@ bool SkinnedModel::LoadHeader(ReadStream& stream, byte& headerVersion) return true; } mesh.SetMaterialSlotIndex(materialSlotIndex); - + // Bounds BoundingBox box; stream.Read(box); diff --git a/Source/Engine/Graphics/Models/Config.h b/Source/Engine/Graphics/Models/Config.h index 3a543ec52..eaf7a978b 100644 --- a/Source/Engine/Graphics/Models/Config.h +++ b/Source/Engine/Graphics/Models/Config.h @@ -32,6 +32,6 @@ #define MAX_BONES_PER_VERTEX MODEL_MAX_BONES_PER_VERTEX // Defines the maximum allowed amount of skeleton bones to be used with skinned model -#define MODEL_MAX_BONES_PER_MODEL 256 +#define MODEL_MAX_BONES_PER_MODEL 0xffff // [Deprecated in v1.10] Use MODEL_MAX_BONES_PER_MODEL #define MAX_BONES_PER_MODEL MODEL_MAX_BONES_PER_MODEL diff --git a/Source/Engine/Graphics/PixelFormatExtensions.cpp b/Source/Engine/Graphics/PixelFormatExtensions.cpp index 8b9a50fe5..289e1b4ee 100644 --- a/Source/Engine/Graphics/PixelFormatExtensions.cpp +++ b/Source/Engine/Graphics/PixelFormatExtensions.cpp @@ -1436,6 +1436,66 @@ static PixelFormatSampler PixelFormatSamplers[] = Platform::MemoryCopy(ptr, data, sizeof(data)); }, }, + { + PixelFormat::R16G16B16A16_UInt, + sizeof(uint16) * 4, + [](const void* ptr) + { + uint16 data[4]; + Platform::MemoryCopy(data, ptr, sizeof(data)); + return Float4(data[0], data[1], data[2], data[3]); + }, + [](void* ptr, const Float4& value) + { + uint16 data[4] = { (uint16)value.X, (uint16)value.Y, (uint16)value.Z, (uint16)value.W}; + Platform::MemoryCopy(ptr, data, sizeof(data)); + }, + }, + { + PixelFormat::R16G16B16A16_SInt, + sizeof(int16) * 4, + [](const void* ptr) + { + int16 data[4]; + Platform::MemoryCopy(data, ptr, sizeof(data)); + return Float4(data[0], data[1], data[2], data[3]); + }, + [](void* ptr, const Float4& value) + { + int16 data[4] = { (int16)value.X, (int16)value.Y, (int16)value.Z, (int16)value.W}; + Platform::MemoryCopy(ptr, data, sizeof(data)); + }, + }, + { + PixelFormat::R32G32B32A32_UInt, + sizeof(uint32) * 4, + [](const void* ptr) + { + uint32 data[4]; + Platform::MemoryCopy(data, ptr, sizeof(data)); + return Float4(data[0], data[1], data[2], data[3]); + }, + [](void* ptr, const Float4& value) + { + uint32 data[4] = { (uint32)value.X, (uint32)value.Y, (uint32)value.Z, (uint32)value.W}; + Platform::MemoryCopy(ptr, data, sizeof(data)); + }, + }, + { + PixelFormat::R32G32B32A32_SInt, + sizeof(int32) * 4, + [](const void* ptr) + { + int32 data[4]; + Platform::MemoryCopy(data, ptr, sizeof(data)); + return Float4(data[0], data[1], data[2], data[3]); + }, + [](void* ptr, const Float4& value) + { + int32 data[4] = { (int32)value.X, (int32)value.Y, (int32)value.Z, (int32)value.W}; + Platform::MemoryCopy(ptr, data, sizeof(data)); + }, + }, }; void PixelFormatSampler::Store(void* data, int32 x, int32 y, int32 rowPitch, const Color& color) const diff --git a/Source/Engine/Graphics/Shaders/GPUVertexLayout.cpp b/Source/Engine/Graphics/Shaders/GPUVertexLayout.cpp index e3afae47e..2791acc48 100644 --- a/Source/Engine/Graphics/Shaders/GPUVertexLayout.cpp +++ b/Source/Engine/Graphics/Shaders/GPUVertexLayout.cpp @@ -126,6 +126,16 @@ String GPUVertexLayout::GetElementsString() const return result; } +VertexElement GPUVertexLayout::FindElement(VertexElement::Types type) const +{ + for (const VertexElement& e : _elements) + { + if (e.Type == type) + return e; + } + return VertexElement(); +} + GPUVertexLayout* GPUVertexLayout::Get(const Elements& elements, bool explicitOffsets) { // Hash input layout diff --git a/Source/Engine/Graphics/Shaders/GPUVertexLayout.h b/Source/Engine/Graphics/Shaders/GPUVertexLayout.h index e804b087e..9e2c3454f 100644 --- a/Source/Engine/Graphics/Shaders/GPUVertexLayout.h +++ b/Source/Engine/Graphics/Shaders/GPUVertexLayout.h @@ -47,6 +47,13 @@ public: return _stride; } + /// + /// Searches for a given element type in a layout. + /// + /// The type of element to find. + /// Found element with properties or empty if missing. + API_FUNCTION() VertexElement FindElement(VertexElement::Types type) const; + /// /// Gets the vertex layout for a given list of elements. Uses internal cache to skip creating layout if it's already exists for a given list. ///