Add support for up to 65536 skeleton bones in skinned meshes

https://forum.flaxengine.com/t/import-fbx-with-bones-more-than-256/1196
This commit is contained in:
Wojtek Figat
2025-01-08 18:48:50 +01:00
parent 1f294605a3
commit 3505b8971b
6 changed files with 123 additions and 8 deletions

View File

@@ -613,6 +613,15 @@ bool ModelBase::SaveLOD(WriteStream& stream, const ModelData& modelData, int32 l
Array<GPUVertexLayout::Elements, FixedAllocation<MODEL_MAX_VB>> 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:

View File

@@ -396,8 +396,27 @@ bool SkinnedModel::SetupSkeleton(const Array<SkeletonNode>& nodes, const Array<S
// Validate input
if (nodes.Count() <= 0 || nodes.Count() > 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);

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -47,6 +47,13 @@ public:
return _stride;
}
/// <summary>
/// Searches for a given element type in a layout.
/// </summary>
/// <param name="type">The type of element to find.</param>
/// <returns>Found element with properties or empty if missing.</returns>
API_FUNCTION() VertexElement FindElement(VertexElement::Types type) const;
/// <summary>
/// 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.
/// </summary>