Skip to content

Commit

Permalink
[glTF] Load scenes with UINT8 indices (#482)
Browse files Browse the repository at this point in the history
Load scenes that contain 8-bit index buffers. How the index buffer ends
up being packed depends on the capabilities of the device:

- On Vulkan with `VK_KHR_index_type_uint8` extension, the UINT8 indices
can be packed into a `INDEX_TYPE_UINT8` buffer. This is the optimal case
as it doesn't require additional memory.
- Otherwise, the UINT8 indices need to be repacked into UINT16 indices.
This comes at the cost of a larger GPU buffer.

This allows us to load more Khronos
[glTF-Sample-Assets](https://github.com/KhronosGroup/glTF-Sample-Assets)
like
[TextureCoordinateTest](https://github.com/KhronosGroup/glTF-Sample-Assets/tree/main/Models/TextureCoordinateTest).


![image](https://github.com/user-attachments/assets/70985607-ed30-4354-9991-436187db4337)

Fixes #453
  • Loading branch information
footballhead authored Nov 26, 2024
1 parent e50fbc8 commit 5aa9f65
Showing 1 changed file with 139 additions and 52 deletions.
191 changes: 139 additions & 52 deletions src/ppx/scene/scene_gltf_loader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
namespace ppx {
namespace scene {

namespace {

#define GLTF_LOD_CLAMP_NONE 1000.0f

enum GltfTextureFilter
Expand Down Expand Up @@ -279,6 +281,86 @@ static const void* GetStartAddress(
return static_cast<const void*>(pAccessorDataStart);
}

const char* ToString(cgltf_component_type componentType)
{
switch (componentType) {
case cgltf_component_type_r_8:
return "BYTE";
case cgltf_component_type_r_8u:
return "UNSIGNED_BYTE";
case cgltf_component_type_r_16:
return "SHORT";
case cgltf_component_type_r_16u:
return "UNSIGNED_SHORT";
case cgltf_component_type_r_32u:
return "UNSIGNED_INT";
case cgltf_component_type_r_32f:
return "FLOAT";
default:
break;
}

return "<unknown cgltf_component_type value>";
}

const char* ToString(cgltf_type type)
{
switch (type) {
case cgltf_type_scalar:
return "SCALAR";
case cgltf_type_vec2:
return "VEC2";
case cgltf_type_vec3:
return "VEC3";
case cgltf_type_vec4:
return "VEC4";
case cgltf_type_mat2:
return "MAT2";
case cgltf_type_mat3:
return "MAT3";
case cgltf_type_mat4:
return "MAT4";
default:
break;
}

return "<unknown cgltf_type value>";
}

// Tries to derive an IndexType from the accessor. Fails for formats that don't comply to the GLTF spec.
// The GLTF 2.0 spec 5.24.2 says "When [format] is undefined, the primitive defines non-indexed geometry. When defined, the accessor MUST have SCALAR type and an unsigned integer component type".
ppx::Result ValidateAccessorIndexType(const cgltf_accessor* pGltfAccessor, grfx::IndexType& outIndexType)
{
if (IsNull(pGltfAccessor)) {
outIndexType = grfx::INDEX_TYPE_UNDEFINED;
return ppx::SUCCESS;
}

if (pGltfAccessor->type != cgltf_type_scalar) {
PPX_ASSERT_MSG(false, "Index accessor type must be SCALAR, got: " << ToString(pGltfAccessor->type));
return ppx::ERROR_SCENE_INVALID_SOURCE_GEOMETRY_INDEX_TYPE;
}

switch (pGltfAccessor->component_type) {
case cgltf_component_type_r_8u:
outIndexType = grfx::INDEX_TYPE_UINT8;
return ppx::SUCCESS;
case cgltf_component_type_r_16u:
outIndexType = grfx::INDEX_TYPE_UINT16;
return ppx::SUCCESS;
case cgltf_component_type_r_32u:
outIndexType = grfx::INDEX_TYPE_UINT32;
return ppx::SUCCESS;
default:
break;
}

PPX_ASSERT_MSG(false, "Index accessor component type must be an unsigned integer, got: " << ToString(pGltfAccessor->component_type));
return ppx::ERROR_SCENE_INVALID_SOURCE_GEOMETRY_INDEX_TYPE;
}

} // namespace

// -------------------------------------------------------------------------------------------------
// GltfMaterialSelector
// -------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -1186,22 +1268,28 @@ ppx::Result GltfLoader::LoadMeshData(

struct BatchInfo
{
scene::MaterialRef material = nullptr;
uint32_t indexDataOffset = 0; // Must have 4 byte alignment
uint32_t indexDataSize = 0;
uint32_t positionDataOffset = 0; // Must have 4 byte alignment
uint32_t positionDataSize = 0;
uint32_t attributeDataOffset = 0; // Must have 4 byte alignment
uint32_t attributeDataSize = 0;
grfx::Format indexFormat = grfx::FORMAT_UNDEFINED;
uint32_t indexCount = 0;
uint32_t vertexCount = 0;
ppx::AABB boundingBox = {};
scene::MaterialRef material = nullptr;
// Start of the index plane in the final repacked GPU buffer.
uint32_t indexDataOffset = 0; // Must have 4 byte alignment
// Total size of the index plane in the final repacked GPU buffer.
uint32_t indexDataSize = 0;
uint32_t positionDataOffset = 0; // Must have 4 byte alignment
uint32_t positionDataSize = 0;
uint32_t attributeDataOffset = 0; // Must have 4 byte alignment
uint32_t attributeDataSize = 0;
// Format of the input index buffer.
grfx::IndexType indexType = grfx::INDEX_TYPE_UNDEFINED;
// Format of the index plane in the final repacked GPU buffer.
grfx::IndexType repackedIndexType = grfx::INDEX_TYPE_UNDEFINED;
// How many indices are in the input index buffer.
uint32_t indexCount = 0;
uint32_t vertexCount = 0;
ppx::AABB boundingBox = {};
};

// Build out batch infos
std::vector<BatchInfo> batchInfos;
//
// Size of the final GPU buffer to allocate. Must account for growth during repacking.
uint32_t totalDataSize = 0;
//
for (cgltf_size primIdx = 0; primIdx < pGltfMesh->primitives_count; ++primIdx) {
Expand All @@ -1213,27 +1301,32 @@ ppx::Result GltfLoader::LoadMeshData(
return ppx::ERROR_SCENE_UNSUPPORTED_TOPOLOGY_TYPE;
}

// Get index format
grfx::IndexType indexType = grfx::INDEX_TYPE_UNDEFINED;
if (ppx::Result ppxres = ValidateAccessorIndexType(pGltfPrimitive->indices, indexType); Failed(ppxres)) {
return ppxres;
}

// We require index data so bail if there isn't index data. See #474
if (IsNull(pGltfPrimitive->indices)) {
if (indexType == grfx::INDEX_TYPE_UNDEFINED) {
PPX_ASSERT_MSG(false, "GLTF mesh primitive does not have index data");
return ppx::ERROR_SCENE_INVALID_SOURCE_GEOMETRY_INDEX_DATA;
}

// Get index format
//
// It's valid for this to be UNDEFINED, means the primitive doesn't have any index data.
// However, if it's not UNDEFINED, UINT16, or UINT32 then it's a format we can't handle.
//
auto indexFormat = GetFormat(pGltfPrimitive->indices);
if ((indexFormat != grfx::FORMAT_UNDEFINED) && (indexFormat != grfx::FORMAT_R16_UINT) && (indexFormat != grfx::FORMAT_R32_UINT)) {
PPX_ASSERT_MSG(false, "GLTF mesh primitive has unrecognized index format");
return ppx::ERROR_SCENE_INVALID_SOURCE_GEOMETRY_INDEX_TYPE;
// UINT8 index buffer availability varies: Vulkan requires an extension, whereas DX12 lacks support entirely.
// If it's not supported then repack as UINT16 (the smallest mandated size for both).
grfx::IndexType repackedIndexType = indexType;
if (repackedIndexType == grfx::INDEX_TYPE_UINT8 && !loadParams.pDevice->IndexTypeUint8Supported()) {
PPX_LOG_INFO("Device doesn't support UINT8 index buffers! Repacking data as UINT16.");
repackedIndexType = grfx::INDEX_TYPE_UINT16;
}

// Index data size
// Index data size of input
const uint32_t indexCount = !IsNull(pGltfPrimitive->indices) ? static_cast<uint32_t>(pGltfPrimitive->indices->count) : 0;
const uint32_t indexElementSize = grfx::GetFormatDescription(indexFormat)->bytesPerTexel;
const uint32_t indexDataSize = indexCount * indexElementSize;
const uint32_t indexElementSize = grfx::IndexTypeSize(indexType);
// If we repack indices into a buffer of a different format then we need to account for disparity between input and output sizes.
const uint32_t repackedSizeRatio = grfx::IndexTypeSize(repackedIndexType) / indexElementSize;
const uint32_t indexDataSize = indexCount * indexElementSize * repackedSizeRatio;

// Get position accessor
const VertexAccessors gltflAccessors = GetVertexAccessors(pGltfPrimitive);
Expand Down Expand Up @@ -1265,7 +1358,8 @@ ppx::Result GltfLoader::LoadMeshData(
batchInfo.positionDataSize = positionDataSize;
batchInfo.attributeDataOffset = attributeDataOffset;
batchInfo.attributeDataSize = attributeDataSize;
batchInfo.indexFormat = indexFormat;
batchInfo.indexType = indexType;
batchInfo.repackedIndexType = repackedIndexType;
batchInfo.indexCount = indexCount;

// Material
Expand Down Expand Up @@ -1354,10 +1448,8 @@ ppx::Result GltfLoader::LoadMeshData(
const bool hasAttributes = (loadParams.requiredVertexAttributes.mask != 0);
//
{
auto createInfo = hasAttributes ? GeometryCreateInfo::PositionPlanarU16() : GeometryCreateInfo::PlanarU16();
if (batch.indexFormat == grfx::FORMAT_R32_UINT) {
createInfo = hasAttributes ? GeometryCreateInfo::PositionPlanarU32() : GeometryCreateInfo::PlanarU32();
}
GeometryCreateInfo createInfo = (hasAttributes ? GeometryCreateInfo::PositionPlanar() : GeometryCreateInfo::Planar()).IndexType(batch.repackedIndexType);

// clang-format off
if (loadParams.requiredVertexAttributes.bits.texCoords) createInfo.AddTexCoord(targetTexCoordFormat);
if (loadParams.requiredVertexAttributes.bits.normals) createInfo.AddNormal(targetNormalFormat);
Expand All @@ -1377,26 +1469,22 @@ ppx::Result GltfLoader::LoadMeshData(
//
// REMINDER: It's possible for a primitive to not have index data
//
if (!IsNull(pGltfPrimitive->indices)) {
// Get start of index data
auto pGltfAccessor = pGltfPrimitive->indices;
auto pGltfIndices = GetStartAddress(pGltfAccessor);
PPX_ASSERT_MSG(!IsNull(pGltfIndices), "GLTF: indices data start is NULL");

// UINT32
if (batch.indexFormat == grfx::FORMAT_R32_UINT) {
const uint32_t* pGltfIndex = static_cast<const uint32_t*>(pGltfIndices);
for (cgltf_size i = 0; i < pGltfAccessor->count; ++i, ++pGltfIndex) {
targetGeometry.AppendIndex(*pGltfIndex);
}
}
// UINT16
else if (batch.indexFormat == grfx::FORMAT_R16_UINT) {
const uint16_t* pGltfIndex = static_cast<const uint16_t*>(pGltfIndices);
for (cgltf_size i = 0; i < pGltfAccessor->count; ++i, ++pGltfIndex) {
targetGeometry.AppendIndex(*pGltfIndex);
switch (batch.indexType) {
case grfx::INDEX_TYPE_UNDEFINED:
PPX_ASSERT_MSG(false, "Non-indexed geoemetry is not supported. See #474");
break;
case grfx::INDEX_TYPE_UINT16:
case grfx::INDEX_TYPE_UINT32:
case grfx::INDEX_TYPE_UINT8:
for (cgltf_size i = 0; i < pGltfPrimitive->indices->count; ++i) {
cgltf_uint value = 0;
if (!cgltf_accessor_read_uint(pGltfPrimitive->indices, i, &value, /*element_size=*/1)) {
PPX_ASSERT_MSG(false, "cgltf_accessor_read_uint failed. Index " << i);
return ppx::ERROR_SCENE_INVALID_SOURCE_GEOMETRY_INDEX_DATA;
}
targetGeometry.AppendIndex(value);
}
}
break;
}
}

Expand Down Expand Up @@ -1506,7 +1594,7 @@ ppx::Result GltfLoader::LoadMeshData(
const uint32_t repackedPositionBufferSize = targetGeometry.GetVertexBuffer(0)->GetSize();
const uint32_t repackedAttributeBufferSize = hasAttributes ? targetGeometry.GetVertexBuffer(1)->GetSize() : 0;
if (repackedIndexBufferSize != batch.indexDataSize) {
PPX_ASSERT_MSG(false, "repacked index buffer size does not match batch's index data size");
PPX_ASSERT_MSG(false, "repacked index buffer size (" << repackedIndexBufferSize << ") does not match batch's index data size (" << batch.indexDataSize << ")");
return ppx::ERROR_SCENE_INVALID_SOURCE_GEOMETRY_INDEX_DATA;
}
if (repackedPositionBufferSize != batch.positionDataSize) {
Expand Down Expand Up @@ -1570,8 +1658,7 @@ ppx::Result GltfLoader::LoadMeshData(
for (uint32_t batchIdx = 0; batchIdx < CountU32(batchInfos); ++batchIdx) {
const auto& batch = batchInfos[batchIdx];

const grfx::IndexType indexType = (batch.indexFormat == grfx::FORMAT_R32_UINT) ? grfx::INDEX_TYPE_UINT32 : grfx::INDEX_TYPE_UINT16;
grfx::IndexBufferView indexBufferView = grfx::IndexBufferView(targetGpuBuffer, indexType, batch.indexDataOffset, batch.indexDataSize);
grfx::IndexBufferView indexBufferView = grfx::IndexBufferView(targetGpuBuffer, batch.repackedIndexType, batch.indexDataOffset, batch.indexDataSize);

grfx::VertexBufferView positionBufferView = grfx::VertexBufferView(targetGpuBuffer, targetPositionElementSize, batch.positionDataOffset, batch.positionDataSize);
grfx::VertexBufferView attributeBufferView = grfx::VertexBufferView((batch.attributeDataSize != 0) ? targetGpuBuffer : nullptr, targetAttributesElementSize, batch.attributeDataOffset, batch.attributeDataSize);
Expand Down

0 comments on commit 5aa9f65

Please sign in to comment.