From aed28e9fd9b310269681065fcc66685b95634d27 Mon Sep 17 00:00:00 2001 From: Thomas Arcila <134677+tarcila@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:01:56 -0400 Subject: [PATCH 1/8] Read glTF tangent inputs with accessor strides --- tsd/src/tsd/io/importers/import_GLTF.cpp | 39 +++++++++++++++--------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/tsd/src/tsd/io/importers/import_GLTF.cpp b/tsd/src/tsd/io/importers/import_GLTF.cpp index bdb775922..61cee6fb2 100644 --- a/tsd/src/tsd/io/importers/import_GLTF.cpp +++ b/tsd/src/tsd/io/importers/import_GLTF.cpp @@ -843,6 +843,19 @@ static void copyStridedData( } } +template +static std::vector copyAccessorData( + const tinygltf::Model &model, int accessorIndex) +{ + if (accessorIndex < 0 || accessorIndex >= model.accessors.size()) + return {}; + + const auto &accessor = model.accessors[accessorIndex]; + std::vector data(accessor.count); + copyStridedData(model, accessorIndex, data.data()); + return data; +} + static std::vector importGLTFMeshes(Scene &scene, const tinygltf::Model &model, const std::vector &materials) @@ -1051,13 +1064,11 @@ static std::vector importGLTFMeshes(Scene &scene, && texCoordAccessor.type == TINYGLTF_TYPE_VEC2 && texCoordAccessor.componentType == TINYGLTF_COMPONENT_TYPE_FLOAT) { - // Get the data - const float3 *positions = - getAccessorData(model, posIt->second); - const float3 *normals = - getAccessorData(model, normalIt->second); - const float2 *texCoords = - getAccessorData(model, texCoordIt->second); + // Get stride-aware attribute data for tangent reconstruction. + auto positions = copyAccessorData(model, posIt->second); + auto normals = copyAccessorData(model, normalIt->second); + auto texCoords = + copyAccessorData(model, texCoordIt->second); // Get or generate indices std::vector indices; @@ -1066,8 +1077,8 @@ static std::vector importGLTFMeshes(Scene &scene, const auto &indexAccessor = model.accessors[primitive.indices]; if (indexAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT) { - const uint16_t *indexData = - getAccessorData(model, primitive.indices); + auto indexData = + copyAccessorData(model, primitive.indices); indices.reserve(indexAccessor.count / 3); for (size_t i = 0; i < indexAccessor.count / 3; ++i) { indices.push_back(uint3(indexData[i * 3], @@ -1076,8 +1087,8 @@ static std::vector importGLTFMeshes(Scene &scene, } } else if (indexAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT) { - const uint32_t *indexData = - getAccessorData(model, primitive.indices); + auto indexData = + copyAccessorData(model, primitive.indices); indices.reserve(indexAccessor.count / 3); for (size_t i = 0; i < indexAccessor.count / 3; ++i) { indices.push_back(uint3(indexData[i * 3], @@ -1103,9 +1114,9 @@ static std::vector importGLTFMeshes(Scene &scene, auto *tangents = vertexTangentArray->mapAs(); bool success = calcTangentsForTriangleMesh(indices.data(), - positions, - normals, - texCoords, + positions.data(), + normals.data(), + texCoords.data(), tangents, indices.size(), posAccessor.count); From 0c5979f0d7e68c1871bcb508325b6b9db4dfa87b Mon Sep 17 00:00:00 2001 From: Thomas Arcila <134677+tarcila@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:03:53 -0400 Subject: [PATCH 2/8] Match glTF tangent generation to imported UVs --- .../io/importers/detail/importer_common.cpp | 52 ++++++++++++------- .../io/importers/detail/importer_common.hpp | 3 +- tsd/src/tsd/io/importers/import_GLTF.cpp | 3 +- 3 files changed, 36 insertions(+), 22 deletions(-) diff --git a/tsd/src/tsd/io/importers/detail/importer_common.cpp b/tsd/src/tsd/io/importers/detail/importer_common.cpp index 4e6345015..a10ec63d1 100644 --- a/tsd/src/tsd/io/importers/detail/importer_common.cpp +++ b/tsd/src/tsd/io/importers/detail/importer_common.cpp @@ -255,8 +255,7 @@ static ArrayRef importDdsTextureArray(Scene &scene, } default: { - logError( - "[importDdsTexture] unsupported DDS format '%c%c%c%c' for '%s'", + logError("[importDdsTexture] unsupported DDS format '%c%c%c%c' for '%s'", dds->header.pixelFormat.fourCC & 0xff, (dds->header.pixelFormat.fourCC >> 8) & 0xff, (dds->header.pixelFormat.fourCC >> 16) & 0xff, @@ -307,10 +306,10 @@ SamplerRef importDdsTexture( return {}; } - std::vector buffer((std::istreambuf_iterator(ifs)), - std::istreambuf_iterator()); - auto dataArray = - importDdsTextureArray(scene, buffer.data(), buffer.size(), filepath, cache); + std::vector buffer( + (std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); + auto dataArray = importDdsTextureArray( + scene, buffer.data(), buffer.size(), filepath, cache); return dataArray ? makeCompressedTextureSampler(scene, dataArray, filepath) : SamplerRef{}; } @@ -332,13 +331,18 @@ static ArrayRef importStbTextureArray(Scene &scene, stbi_ldr_to_hdr_scale(1.0f); stbi_ldr_to_hdr_gamma(2.2f); } - void *decodedData = stbi_loadf_from_memory( - static_cast(data), int(numBytes), &width, &height, &n, 0); + void *decodedData = + stbi_loadf_from_memory(static_cast(data), + int(numBytes), + &width, + &height, + &n, + 0); if (!decodedData || n < 1) { if (!decodedData) { - logError("[importTexture] failed to import texture '%s'", - textureId.c_str()); + logError( + "[importTexture] failed to import texture '%s'", textureId.c_str()); } else { logWarning("[importTexture] texture '%s' with %i channels not imported", textureId.c_str(), @@ -378,11 +382,12 @@ SamplerRef importStbTexture( return {}; } - std::vector buffer((std::istreambuf_iterator(ifs)), - std::istreambuf_iterator()); + std::vector buffer( + (std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); auto dataArray = importStbTextureArray( scene, buffer.data(), buffer.size(), cacheKey, cache, isLinear); - return dataArray ? makeTextureSampler(scene, dataArray, filepath) : SamplerRef{}; + return dataArray ? makeTextureSampler(scene, dataArray, filepath) + : SamplerRef{}; } SamplerRef importTexture( @@ -419,9 +424,11 @@ SamplerRef importTextureFromMemory(Scene &scene, }); if (format == "dds") { - auto dataArray = importDdsTextureArray(scene, data, numBytes, cacheKey, cache); - return dataArray ? makeCompressedTextureSampler(scene, dataArray, displayName) - : SamplerRef{}; + auto dataArray = + importDdsTextureArray(scene, data, numBytes, cacheKey, cache); + return dataArray + ? makeCompressedTextureSampler(scene, dataArray, displayName) + : SamplerRef{}; } auto dataArray = @@ -477,7 +484,8 @@ bool calcTangentsForTriangleMesh(const uint3 *indices, const float2 *texCoords, float4 *tangents, size_t numIndices, - size_t numVertices) + size_t numVertices, + bool flipTexCoordY) { if (!texCoords) return false; @@ -492,6 +500,7 @@ bool calcTangentsForTriangleMesh(const uint3 *indices, const float3 *vertexNormals; const float2 *texCoords; float4 *tangents; + bool flipTexCoordY; size_t numIndices; size_t numVertices; } mesh; @@ -501,6 +510,7 @@ bool calcTangentsForTriangleMesh(const uint3 *indices, mesh.vertexNormals = vertexNormals; mesh.texCoords = texCoords; mesh.tangents = tangents; + mesh.flipTexCoordY = flipTexCoordY; mesh.numIndices = numIndices; mesh.numVertices = numVertices; @@ -567,7 +577,9 @@ bool calcTangentsForTriangleMesh(const uint3 *indices, assert(mesh->texCoords); unsigned vID = index[vertID]; - oc = {mesh->texCoords[vID].x, 1.0f - mesh->texCoords[vID].y}; + oc = {mesh->texCoords[vID].x, + mesh->flipTexCoordY ? 1.0f - mesh->texCoords[vID].y + : mesh->texCoords[vID].y}; }; // callback to assign output tangents @@ -667,7 +679,7 @@ static core::TransferFunction importParaViewTransferFunction( filepath.c_str()); return {}; } else if (const auto arrayStart = jsonContent.find("[", rgbPointsPos); - arrayStart == std::string::npos) { + arrayStart == std::string::npos) { logError( "[importParaViewTransferFunction] Invalid RGBPoints format in file: %s", filepath.c_str()); @@ -752,7 +764,7 @@ static core::TransferFunction importParaViewTransferFunction( std::istringstream opacitySS(opacityContent); for (std::string opacityToken; - std::getline(opacitySS, opacityToken, ',');) { + std::getline(opacitySS, opacityToken, ',');) { // Trim whitespace if (const auto first = opacityToken.find_first_not_of(" \t\n\r"); first != std::string::npos) { diff --git a/tsd/src/tsd/io/importers/detail/importer_common.hpp b/tsd/src/tsd/io/importers/detail/importer_common.hpp index 60399ae52..327661f25 100644 --- a/tsd/src/tsd/io/importers/detail/importer_common.hpp +++ b/tsd/src/tsd/io/importers/detail/importer_common.hpp @@ -70,7 +70,8 @@ bool calcTangentsForTriangleMesh(const tsd::math::uint3 *indices, const tsd::math::float2 *texCoords, tsd::math::float4 *tangents, size_t numIndices, - size_t numVertices); + size_t numVertices, + bool flipTexCoordY = true); #if TSD_USE_VTK anari::DataType vtkTypeToANARIType( diff --git a/tsd/src/tsd/io/importers/import_GLTF.cpp b/tsd/src/tsd/io/importers/import_GLTF.cpp index 61cee6fb2..a6c82401e 100644 --- a/tsd/src/tsd/io/importers/import_GLTF.cpp +++ b/tsd/src/tsd/io/importers/import_GLTF.cpp @@ -1119,7 +1119,8 @@ static std::vector importGLTFMeshes(Scene &scene, texCoords.data(), tangents, indices.size(), - posAccessor.count); + posAccessor.count, + false); vertexTangentArray->unmap(); From 16ba4f0498b3882dd6692e6b9404a0d7a55670b7 Mon Sep 17 00:00:00 2001 From: Thomas Arcila <134677+tarcila@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:07:18 -0400 Subject: [PATCH 3/8] Honor glTF texture coordinate sets --- tsd/src/tsd/io/importers/import_GLTF.cpp | 158 +++++++++++++++++++---- 1 file changed, 133 insertions(+), 25 deletions(-) diff --git a/tsd/src/tsd/io/importers/import_GLTF.cpp b/tsd/src/tsd/io/importers/import_GLTF.cpp index a6c82401e..eee7c55c1 100644 --- a/tsd/src/tsd/io/importers/import_GLTF.cpp +++ b/tsd/src/tsd/io/importers/import_GLTF.cpp @@ -78,13 +78,37 @@ static T GetValueOrDefault(const tinygltf::Value &value, return defaultValue; } +static int supportedTexCoordSet(int texCoord, const char *samplerName = nullptr) +{ + if (texCoord >= 0 && texCoord < 4) + return texCoord; + + if (samplerName && samplerName[0] != '\0') { + logWarning( + "[import_GLTF] texture '%s' uses unsupported TEXCOORD_%d; using TEXCOORD_0", + samplerName, + texCoord); + } else { + logWarning( + "[import_GLTF] texture uses unsupported TEXCOORD_%d; using TEXCOORD_0", + texCoord); + } + return 0; +} + +static std::string attributeNameForTexCoord(int texCoord) +{ + return "attribute"s + std::to_string(texCoord); +} + static SamplerRef importGLTFTexture(Scene &scene, const tinygltf::Model &model, int textureIndex, TextureCache &cache, bool isLinear = false, bool flipNormalMapY = false, - const char *samplerName = nullptr) + const char *samplerName = nullptr, + int texCoord = 0) { if (textureIndex < 0 || textureIndex >= model.textures.size()) return {}; @@ -197,7 +221,9 @@ static SamplerRef importGLTFTexture(Scene &scene, auto sampler = scene.createObject(tokens::sampler::image2D); sampler->setParameterObject("image", *dataArray); - sampler->setParameter("inAttribute", "attribute0"); + const auto inAttribute = + attributeNameForTexCoord(supportedTexCoordSet(texCoord, samplerName)); + sampler->setParameter("inAttribute", inAttribute.c_str()); // Apply sampler settings if available if (texture.sampler >= 0 && texture.sampler < model.samplers.size()) { @@ -291,7 +317,8 @@ static std::vector importGLTFMaterials( cache, false, false, - "baseColor")) { + "baseColor", + pbr.baseColorTexture.texCoord)) { // Make this an opaque color. Opacity is handled below. sampler->setParameter("outTransform", mat4({baseColorFactor.x, 0, 0, 0}, @@ -311,7 +338,8 @@ static std::vector importGLTFMaterials( cache, true, false, - "opacity")) { + "opacity", + pbr.baseColorTexture.texCoord)) { sampler->setParameter("outTransform", mat4({0, 0, 0, 0}, {0, 0, 0, 0}, @@ -330,7 +358,8 @@ static std::vector importGLTFMaterials( cache, true, false, - "metallic")) { + "metallic", + pbr.metallicRoughnessTexture.texCoord)) { // Metallic is in the blue channel for glTF sampler->setParameter("outTransform", mat4({0, 0, 0, 0}, @@ -350,7 +379,8 @@ static std::vector importGLTFMaterials( cache, true, false, - "roughness")) { + "roughness", + pbr.metallicRoughnessTexture.texCoord)) { // Roughness is in the green channel for glTF sampler->setParameter("outTransform", mat4({0, 0, 0, 0}, @@ -369,7 +399,8 @@ static std::vector importGLTFMaterials( cache, true, false, - "normal")) { + "normal", + gltfMaterial.normalTexture.texCoord)) { float normalScale = gltfMaterial.normalTexture.scale; sampler->setParameter("outTransform", mat4({normalScale, 0, 0, 0}, @@ -386,7 +417,8 @@ static std::vector importGLTFMaterials( cache, true, false, - "occlusion")) { + "occlusion", + gltfMaterial.occlusionTexture.texCoord)) { material->setParameterObject("occlusion", *sampler); } @@ -410,7 +442,8 @@ static std::vector importGLTFMaterials( cache, false, false, - "emissive")) { + "emissive", + gltfMaterial.emissiveTexture.texCoord)) { sampler->setParameter("outTransform", mat4({emissiveFactor.x, 0, 0, 0}, {0, emissiveFactor.y, 0, 0}, @@ -448,13 +481,16 @@ static std::vector importGLTFMaterials( // Transmission texture auto transmissionTextureIndex = GetValueOrDefault( transmissionExt, -1, "transmissionTexture", "index"); + auto transmissionTexCoord = GetValueOrDefault( + transmissionExt, 0, "transmissionTexture", "texCoord"); if (auto sampler = importGLTFTexture(scene, model, transmissionTextureIndex, cache, true, false, - "transmission")) { + "transmission", + transmissionTexCoord)) { sampler->setParameter("outTransform", mat4({transmissionFactor, 0, 0, 0}, {0, 0, 0, 0}, @@ -492,13 +528,16 @@ static std::vector importGLTFMaterials( // Thickness texture auto thicknessTextureIndex = GetValueOrDefault(volumeExt, -1, "thicknessTexture", "index"); + auto thicknessTexCoord = + GetValueOrDefault(volumeExt, 0, "thicknessTexture", "texCoord"); if (auto sampler = importGLTFTexture(scene, model, thicknessTextureIndex, cache, true, false, - "thickness")) { + "thickness", + thicknessTexCoord)) { sampler->setParameter("outTransform", mat4({0, thicknessFactor, 0, 0}, {0, 0, 0, 0}, @@ -539,13 +578,16 @@ static std::vector importGLTFMaterials( // Clearcoat texture auto clearcoatTextureIndex = GetValueOrDefault(clearcoatExt, -1, "clearcoatTexture", "index"); + auto clearcoatTexCoord = + GetValueOrDefault(clearcoatExt, 0, "clearcoatTexture", "texCoord"); if (auto sampler = importGLTFTexture(scene, model, clearcoatTextureIndex, cache, true, false, - "clearcoat")) { + "clearcoat", + clearcoatTexCoord)) { sampler->setParameter("outTransform", mat4({clearcoatFactor, 0, 0, 0}, {0, 0, 0, 0}, @@ -563,13 +605,16 @@ static std::vector importGLTFMaterials( // Clearcoat roughness texture auto clearcoatRoughnessTextureIndex = GetValueOrDefault( clearcoatExt, -1, "clearcoatRoughnessTexture", "index"); + auto clearcoatRoughnessTexCoord = GetValueOrDefault( + clearcoatExt, 0, "clearcoatRoughnessTexture", "texCoord"); if (auto sampler = importGLTFTexture(scene, model, clearcoatRoughnessTextureIndex, cache, true, false, - "clearcoatRoughness")) { + "clearcoatRoughness", + clearcoatRoughnessTexCoord)) { sampler->setParameter("outTransform", mat4({0, 0, 0, 0}, {clearcoatRoughnessFactor, 0, 0, 0}, @@ -583,13 +628,16 @@ static std::vector importGLTFMaterials( // Clearcoat normal texture auto clearcoatNormalTextureIndex = GetValueOrDefault( clearcoatExt, -1, "clearcoatNormalTexture", "index"); + auto clearcoatNormalTexCoord = GetValueOrDefault( + clearcoatExt, 0, "clearcoatNormalTexture", "texCoord"); if (auto sampler = importGLTFTexture(scene, model, clearcoatNormalTextureIndex, cache, true, false, - "clearcoatNormal")) { + "clearcoatNormal", + clearcoatNormalTexCoord)) { material->setParameterObject("clearcoatNormal", *sampler); } } else { @@ -610,8 +658,16 @@ static std::vector importGLTFMaterials( auto specularTextureIndex = GetValueOrDefault(specularExt, -1, "specularTexture", "index"); - if (auto sampler = importGLTFTexture( - scene, model, specularTextureIndex, cache, true)) { + auto specularTexCoord = + GetValueOrDefault(specularExt, 0, "specularTexture", "texCoord"); + if (auto sampler = importGLTFTexture(scene, + model, + specularTextureIndex, + cache, + true, + false, + "specular", + specularTexCoord)) { sampler->setParameter("outTransform", mat4({0, 0, 0, 0}, {0, 0, 0, 0}, @@ -629,13 +685,16 @@ static std::vector importGLTFMaterials( auto specularColorTextureIndex = GetValueOrDefault(specularExt, -1, "specularColorTexture", "index"); + auto specularColorTexCoord = + GetValueOrDefault(specularExt, 0, "specularColorTexture", "texCoord"); if (auto sampler = importGLTFTexture(scene, model, specularColorTextureIndex, cache, false, false, - "specularColor")) { + "specularColor", + specularColorTexCoord)) { sampler->setParameter("outTransform", mat4({specularColorFactor.x, 0, 0, 0}, {0, specularColorFactor.y, 0, 0}, @@ -664,13 +723,16 @@ static std::vector importGLTFMaterials( // Sheen color texture auto sheenColorTextureIndex = GetValueOrDefault(sheenExt, -1, "sheenColorTexture", "index"); + auto sheenColorTexCoord = + GetValueOrDefault(sheenExt, 0, "sheenColorTexture", "texCoord"); if (auto sampler = importGLTFTexture(scene, model, sheenColorTextureIndex, cache, false, false, - "sheenColor")) { + "sheenColor", + sheenColorTexCoord)) { sampler->setParameter("outTransform", mat4({sheenColorFactor.x, 0, 0, 0}, {0, sheenColorFactor.y, 0, 0}, @@ -688,13 +750,16 @@ static std::vector importGLTFMaterials( // Sheen roughness texture auto sheenRoughnessTextureIndex = GetValueOrDefault(sheenExt, -1, "sheenRoughnessTexture", "index"); + auto sheenRoughnessTexCoord = + GetValueOrDefault(sheenExt, 0, "sheenRoughnessTexture", "texCoord"); if (auto sampler = importGLTFTexture(scene, model, sheenRoughnessTextureIndex, cache, true, false, - "sheenRoughness")) { + "sheenRoughness", + sheenRoughnessTexCoord)) { sampler->setParameter("outTransform", mat4({0, 0, 0, 0}, {0, 0, 0, 0}, @@ -723,13 +788,16 @@ static std::vector importGLTFMaterials( // Iridescence texture auto iridescenceTextureIndex = GetValueOrDefault(iridescenceExt, -1, "iridescenceTexture", "index"); + auto iridescenceTexCoord = GetValueOrDefault( + iridescenceExt, 0, "iridescenceTexture", "texCoord"); if (auto sampler = importGLTFTexture(scene, model, iridescenceTextureIndex, cache, true, false, - "iridescence")) { + "iridescence", + iridescenceTexCoord)) { sampler->setParameter("outTransform", mat4({iridescenceFactor, 0, 0, 0}, {0, 0, 0, 0}, @@ -756,13 +824,16 @@ static std::vector importGLTFMaterials( // Iridescence thickness texture auto iridescenceThicknessTextureIndex = GetValueOrDefault( iridescenceExt, -1, "iridescenceThicknessTexture", "index"); + auto iridescenceThicknessTexCoord = GetValueOrDefault( + iridescenceExt, 0, "iridescenceThicknessTexture", "texCoord"); if (auto sampler = importGLTFTexture(scene, model, iridescenceThicknessTextureIndex, cache, true, false, - "iridescenceThickness")) { + "iridescenceThickness", + iridescenceThicknessTexCoord)) { sampler->setParameter("outTransform", mat4({iridescenceThicknessMaximum - iridescenceThicknessMinimum, 0, @@ -856,6 +927,31 @@ static std::vector copyAccessorData( return data; } +static int tangentTexCoordSetForPrimitive( + const tinygltf::Model &model, const tinygltf::Primitive &primitive) +{ + if (primitive.material < 0 || primitive.material >= model.materials.size()) + return 0; + + const auto &material = model.materials[primitive.material]; + if (material.normalTexture.index >= 0) + return supportedTexCoordSet(material.normalTexture.texCoord, "normal"); + + const auto clearcoatIt = material.extensions.find("KHR_materials_clearcoat"); + if (clearcoatIt == material.extensions.end()) + return 0; + + const auto &clearcoatExt = clearcoatIt->second; + const auto clearcoatNormalTextureIndex = + GetValueOrDefault(clearcoatExt, -1, "clearcoatNormalTexture", "index"); + if (clearcoatNormalTextureIndex < 0) + return 0; + + const auto clearcoatNormalTexCoord = + GetValueOrDefault(clearcoatExt, 0, "clearcoatNormalTexture", "texCoord"); + return supportedTexCoordSet(clearcoatNormalTexCoord, "clearcoatNormal"); +} + static std::vector importGLTFMeshes(Scene &scene, const tinygltf::Model &model, const std::vector &materials) @@ -908,8 +1004,13 @@ static std::vector importGLTFMeshes(Scene &scene, } // Texture coordinate data - auto texCoordIt = primitive.attributes.find("TEXCOORD_0"); - if (texCoordIt != primitive.attributes.end()) { + for (int texCoordSet = 0; texCoordSet < 4; ++texCoordSet) { + const std::string gltfAttributeName = + "TEXCOORD_"s + std::to_string(texCoordSet); + auto texCoordIt = primitive.attributes.find(gltfAttributeName); + if (texCoordIt == primitive.attributes.end()) + continue; + const auto &texCoordAccessor = model.accessors[texCoordIt->second]; if (texCoordAccessor.type == TINYGLTF_TYPE_VEC2 && texCoordAccessor.componentType @@ -919,8 +1020,11 @@ static std::vector importGLTFMeshes(Scene &scene, auto *texCoordDataOut = vertexTexCoordArray->mapAs(); copyStridedData(model, texCoordIt->second, texCoordDataOut); vertexTexCoordArray->unmap(); + + const std::string attributeName = + "vertex."s + attributeNameForTexCoord(texCoordSet); geometry->setParameterObject( - "vertex.attribute0", *vertexTexCoordArray); + attributeName.c_str(), *vertexTexCoordArray); } } @@ -1046,7 +1150,11 @@ static std::vector importGLTFMeshes(Scene &scene, // Check if we have all the required data for tangent calculation auto posIt = primitive.attributes.find("POSITION"); auto normalIt = primitive.attributes.find("NORMAL"); - auto texCoordIt = primitive.attributes.find("TEXCOORD_0"); + const int tangentTexCoordSet = + tangentTexCoordSetForPrimitive(model, primitive); + const std::string tangentTexCoordAttribute = + "TEXCOORD_"s + std::to_string(tangentTexCoordSet); + auto texCoordIt = primitive.attributes.find(tangentTexCoordAttribute); if (posIt != primitive.attributes.end() && normalIt != primitive.attributes.end() From 80065f857572e8a6489563afa6a851908380bf75 Mon Sep 17 00:00:00 2001 From: Thomas Arcila <134677+tarcila@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:08:47 -0400 Subject: [PATCH 4/8] Apply glTF normal texture scale after decode --- tsd/src/tsd/io/importers/import_GLTF.cpp | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/tsd/src/tsd/io/importers/import_GLTF.cpp b/tsd/src/tsd/io/importers/import_GLTF.cpp index eee7c55c1..b5bb63063 100644 --- a/tsd/src/tsd/io/importers/import_GLTF.cpp +++ b/tsd/src/tsd/io/importers/import_GLTF.cpp @@ -283,6 +283,14 @@ static SamplerRef importGLTFTexture(Scene &scene, return sampler; } +static void applyNormalTextureScale(SamplerRef sampler, float scale) +{ + sampler->setParameter("outTransform", + mat4({scale, 0, 0, 0}, {0, scale, 0, 0}, {0, 0, 1, 0}, {0, 0, 0, 1})); + const float offset = 0.5f * (1.0f - scale); + sampler->setParameter("outOffset", float4(offset, offset, 0.0f, 0.0f)); +} + static std::vector importGLTFMaterials( Scene &scene, const tinygltf::Model &model) { @@ -402,11 +410,7 @@ static std::vector importGLTFMaterials( "normal", gltfMaterial.normalTexture.texCoord)) { float normalScale = gltfMaterial.normalTexture.scale; - sampler->setParameter("outTransform", - mat4({normalScale, 0, 0, 0}, - {0, normalScale, 0, 0}, - {0, 0, 1, 0}, // Don't scale Z (blue) channel - {0, 0, 0, 1})); + applyNormalTextureScale(sampler, normalScale); material->setParameterObject("normal", *sampler); } @@ -628,6 +632,8 @@ static std::vector importGLTFMaterials( // Clearcoat normal texture auto clearcoatNormalTextureIndex = GetValueOrDefault( clearcoatExt, -1, "clearcoatNormalTexture", "index"); + float clearcoatNormalScale = GetValueOrDefault( + clearcoatExt, 1.0f, "clearcoatNormalTexture", "scale"); auto clearcoatNormalTexCoord = GetValueOrDefault( clearcoatExt, 0, "clearcoatNormalTexture", "texCoord"); if (auto sampler = importGLTFTexture(scene, @@ -638,6 +644,7 @@ static std::vector importGLTFMaterials( false, "clearcoatNormal", clearcoatNormalTexCoord)) { + applyNormalTextureScale(sampler, clearcoatNormalScale); material->setParameterObject("clearcoatNormal", *sampler); } } else { From 8bd4c101da69c44ce69a63b19ccfa3afe7769840 Mon Sep 17 00:00:00 2001 From: Thomas Arcila <134677+tarcila@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:09:40 -0400 Subject: [PATCH 5/8] Generate face-varying glTF tangents for indexed meshes --- .../tsd/io/importers/detail/importer_common.cpp | 7 ++++++- .../tsd/io/importers/detail/importer_common.hpp | 3 ++- tsd/src/tsd/io/importers/import_GLTF.cpp | 15 +++++++++++---- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/tsd/src/tsd/io/importers/detail/importer_common.cpp b/tsd/src/tsd/io/importers/detail/importer_common.cpp index a10ec63d1..eb6970200 100644 --- a/tsd/src/tsd/io/importers/detail/importer_common.cpp +++ b/tsd/src/tsd/io/importers/detail/importer_common.cpp @@ -485,7 +485,8 @@ bool calcTangentsForTriangleMesh(const uint3 *indices, float4 *tangents, size_t numIndices, size_t numVertices, - bool flipTexCoordY) + bool flipTexCoordY, + bool faceVaryingTangents) { if (!texCoords) return false; @@ -501,6 +502,7 @@ bool calcTangentsForTriangleMesh(const uint3 *indices, const float2 *texCoords; float4 *tangents; bool flipTexCoordY; + bool faceVaryingTangents; size_t numIndices; size_t numVertices; } mesh; @@ -511,6 +513,7 @@ bool calcTangentsForTriangleMesh(const uint3 *indices, mesh.texCoords = texCoords; mesh.tangents = tangents; mesh.flipTexCoordY = flipTexCoordY; + mesh.faceVaryingTangents = faceVaryingTangents; mesh.numIndices = numIndices; mesh.numVertices = numVertices; @@ -593,6 +596,8 @@ bool calcTangentsForTriangleMesh(const uint3 *indices, uint3 index = mesh->indices[faceID]; unsigned vID = index[vertID]; + if (mesh->faceVaryingTangents) + vID = faceID * 3 + vertID; float4 &outtangent = mesh->tangents[vID]; diff --git a/tsd/src/tsd/io/importers/detail/importer_common.hpp b/tsd/src/tsd/io/importers/detail/importer_common.hpp index 327661f25..f399ac592 100644 --- a/tsd/src/tsd/io/importers/detail/importer_common.hpp +++ b/tsd/src/tsd/io/importers/detail/importer_common.hpp @@ -71,7 +71,8 @@ bool calcTangentsForTriangleMesh(const tsd::math::uint3 *indices, tsd::math::float4 *tangents, size_t numIndices, size_t numVertices, - bool flipTexCoordY = true); + bool flipTexCoordY = true, + bool faceVaryingTangents = false); #if TSD_USE_VTK anari::DataType vtkTypeToANARIType( diff --git a/tsd/src/tsd/io/importers/import_GLTF.cpp b/tsd/src/tsd/io/importers/import_GLTF.cpp index b5bb63063..16af9e14b 100644 --- a/tsd/src/tsd/io/importers/import_GLTF.cpp +++ b/tsd/src/tsd/io/importers/import_GLTF.cpp @@ -1224,8 +1224,12 @@ static std::vector importGLTFMeshes(Scene &scene, if (!indices.empty()) { // Create tangent array and compute tangents + const bool outputFaceVaryingTangents = primitive.indices >= 0; + const size_t tangentCount = outputFaceVaryingTangents + ? indices.size() * 3 + : posAccessor.count; auto vertexTangentArray = - scene.createArray(ANARI_FLOAT32_VEC4, posAccessor.count); + scene.createArray(ANARI_FLOAT32_VEC4, tangentCount); auto *tangents = vertexTangentArray->mapAs(); bool success = calcTangentsForTriangleMesh(indices.data(), @@ -1235,13 +1239,16 @@ static std::vector importGLTFMeshes(Scene &scene, tangents, indices.size(), posAccessor.count, - false); + false, + outputFaceVaryingTangents); vertexTangentArray->unmap(); if (success) { - geometry->setParameterObject( - "vertex.tangent", *vertexTangentArray); + geometry->setParameterObject(outputFaceVaryingTangents + ? "faceVarying.tangent" + : "vertex.tangent", + *vertexTangentArray); logInfo( "[import_GLTF] Computed tangents for geometry '%s' with %zu vertices and %zu triangles", geometryName.c_str(), From 50013799cf676fd0b7902224b382d85c85f3670c Mon Sep 17 00:00:00 2001 From: Thomas Arcila <134677+tarcila@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:10:29 -0400 Subject: [PATCH 6/8] Allow glTF tangent reconstruction without normals --- tsd/src/tsd/io/importers/import_GLTF.cpp | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tsd/src/tsd/io/importers/import_GLTF.cpp b/tsd/src/tsd/io/importers/import_GLTF.cpp index 16af9e14b..d13fc11f3 100644 --- a/tsd/src/tsd/io/importers/import_GLTF.cpp +++ b/tsd/src/tsd/io/importers/import_GLTF.cpp @@ -1164,24 +1164,26 @@ static std::vector importGLTFMeshes(Scene &scene, auto texCoordIt = primitive.attributes.find(tangentTexCoordAttribute); if (posIt != primitive.attributes.end() - && normalIt != primitive.attributes.end() && texCoordIt != primitive.attributes.end()) { // Get the accessors const auto &posAccessor = model.accessors[posIt->second]; - const auto &normalAccessor = model.accessors[normalIt->second]; const auto &texCoordAccessor = model.accessors[texCoordIt->second]; + const bool hasUsableNormals = normalIt != primitive.attributes.end() + && model.accessors[normalIt->second].type == TINYGLTF_TYPE_VEC3 + && model.accessors[normalIt->second].componentType + == TINYGLTF_COMPONENT_TYPE_FLOAT; // Verify we have the right data types if (posAccessor.type == TINYGLTF_TYPE_VEC3 && posAccessor.componentType == TINYGLTF_COMPONENT_TYPE_FLOAT - && normalAccessor.type == TINYGLTF_TYPE_VEC3 - && normalAccessor.componentType == TINYGLTF_COMPONENT_TYPE_FLOAT && texCoordAccessor.type == TINYGLTF_TYPE_VEC2 && texCoordAccessor.componentType == TINYGLTF_COMPONENT_TYPE_FLOAT) { // Get stride-aware attribute data for tangent reconstruction. auto positions = copyAccessorData(model, posIt->second); - auto normals = copyAccessorData(model, normalIt->second); + auto normals = hasUsableNormals + ? copyAccessorData(model, normalIt->second) + : std::vector{}; auto texCoords = copyAccessorData(model, texCoordIt->second); @@ -1234,7 +1236,7 @@ static std::vector importGLTFMeshes(Scene &scene, bool success = calcTangentsForTriangleMesh(indices.data(), positions.data(), - normals.data(), + normals.empty() ? nullptr : normals.data(), texCoords.data(), tangents, indices.size(), @@ -1267,10 +1269,9 @@ static std::vector importGLTFMeshes(Scene &scene, } } else { logDebug( - "[import_GLTF] Skipping tangent computation for geometry '%s': missing required attributes (position=%s, normal=%s, texcoord=%s)", + "[import_GLTF] Skipping tangent computation for geometry '%s': missing required attributes (position=%s, texcoord=%s)", geometryName.c_str(), (posIt != primitive.attributes.end()) ? "yes" : "no", - (normalIt != primitive.attributes.end()) ? "yes" : "no", (texCoordIt != primitive.attributes.end()) ? "yes" : "no"); } } From 4bd5f1cfb51b76e2ad817c6c577ea9e834a7a907 Mon Sep 17 00:00:00 2001 From: Thomas Arcila <134677+tarcila@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:12:33 -0400 Subject: [PATCH 7/8] Support byte glTF indices --- tsd/src/tsd/io/importers/import_GLTF.cpp | 188 ++++++++++------------- 1 file changed, 79 insertions(+), 109 deletions(-) diff --git a/tsd/src/tsd/io/importers/import_GLTF.cpp b/tsd/src/tsd/io/importers/import_GLTF.cpp index d13fc11f3..0586ec303 100644 --- a/tsd/src/tsd/io/importers/import_GLTF.cpp +++ b/tsd/src/tsd/io/importers/import_GLTF.cpp @@ -871,54 +871,46 @@ static std::vector importGLTFMaterials( } template -static const T *getAccessorData(const tinygltf::Model &model, int accessorIndex) +static bool copyStridedData( + const tinygltf::Model &model, int accessorIndex, T *outData) { if (accessorIndex < 0 || accessorIndex >= model.accessors.size()) - return nullptr; + return false; const auto &accessor = model.accessors[accessorIndex]; - const auto &bufferView = model.bufferViews[accessor.bufferView]; - const auto &buffer = model.buffers[bufferView.buffer]; - - return reinterpret_cast( - buffer.data.data() + bufferView.byteOffset + accessor.byteOffset); -} -template -static void copyStridedData( - const tinygltf::Model &model, int accessorIndex, T *outData) -{ - if (accessorIndex < 0 || accessorIndex >= model.accessors.size()) - return; + // Refuse to copy when the accessor's element layout does not match the + // template type. + const size_t numComponents = tinygltf::GetNumComponentsInType(accessor.type); + const size_t componentSize = + tinygltf::GetComponentSizeInBytes(accessor.componentType); + const size_t bytesPerElement = numComponents * componentSize; + if (bytesPerElement != sizeof(T)) { + logWarning( + "[import_GLTF] accessor %d element size (%zu) does not match " + "destination size (%zu); skipping copy", + accessorIndex, + bytesPerElement, + sizeof(T)); + return false; + } - const auto &accessor = model.accessors[accessorIndex]; const auto &bufferView = model.bufferViews[accessor.bufferView]; const auto &buffer = model.buffers[bufferView.buffer]; const uint8_t *sourceData = buffer.data.data() + bufferView.byteOffset + accessor.byteOffset; - // Check if data is interleaved (has a stride) if (bufferView.byteStride > 0) { - // Calculate the size of one element based on accessor type and - // component type - size_t elementSize = tinygltf::GetNumComponentsInType(accessor.type); - size_t componentSize = - tinygltf::GetComponentSizeInBytes(accessor.componentType); - - size_t bytesPerElement = elementSize * componentSize; - - // Copy data with stride for (size_t i = 0; i < accessor.count; ++i) { std::memcpy(reinterpret_cast(outData) + i * bytesPerElement, sourceData + i * bufferView.byteStride, bytesPerElement); } } else { - // Data is tightly packed, direct copy - size_t bytesToCopy = accessor.count * sizeof(T); - std::memcpy(outData, sourceData, bytesToCopy); + std::memcpy(outData, sourceData, accessor.count * bytesPerElement); } + return true; } template @@ -930,10 +922,40 @@ static std::vector copyAccessorData( const auto &accessor = model.accessors[accessorIndex]; std::vector data(accessor.count); - copyStridedData(model, accessorIndex, data.data()); + if (!copyStridedData(model, accessorIndex, data.data())) + return {}; return data; } +template +static void copyIndexTriplets( + const tinygltf::Model &model, int accessorIndex, uint3 *outIndices) +{ + auto indexData = copyAccessorData(model, accessorIndex); + + // Drive the loop from the actual returned size so a validation failure + // (empty vector) doesn't OOB-index. + for (size_t i = 0; i < indexData.size() / 3; ++i) { + outIndices[i] = + uint3(indexData[i * 3], indexData[i * 3 + 1], indexData[i * 3 + 2]); + } +} + +template +static void appendIndexTriplets(const tinygltf::Model &model, + int accessorIndex, + std::vector &indices) +{ + auto indexData = copyAccessorData(model, accessorIndex); + const size_t triplets = indexData.size() / 3; + indices.reserve(indices.size() + triplets); + + for (size_t i = 0; i < triplets; ++i) { + indices.push_back( + uint3(indexData[i * 3], indexData[i * 3 + 1], indexData[i * 3 + 2])); + } +} + static int tangentTexCoordSetForPrimitive( const tinygltf::Model &model, const tinygltf::Primitive &primitive) { @@ -1078,73 +1100,30 @@ static std::vector importGLTFMeshes(Scene &scene, if (primitive.indices >= 0) { const auto &indexAccessor = model.accessors[primitive.indices]; - if (indexAccessor.componentType - == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT) { - auto indexArray = - scene.createArray(ANARI_UINT32_VEC3, indexAccessor.count / 3); - auto *outIndices = indexArray->mapAs(); - - // Check if we need to handle strided data - const auto &indexBufferView = - model.bufferViews[indexAccessor.bufferView]; - if (indexBufferView.byteStride > 0 - && indexBufferView.byteStride != sizeof(uint16_t)) { - // Handle strided indices - auto tempIndices = std::vector(indexAccessor.count); - copyStridedData(model, primitive.indices, tempIndices.data()); - - for (size_t i = 0; i < indexAccessor.count / 3; ++i) { - outIndices[i] = uint3(tempIndices[i * 3], - tempIndices[i * 3 + 1], - tempIndices[i * 3 + 2]); - } - } else { - // Direct access for tightly packed data - const uint16_t *inIndices = - getAccessorData(model, primitive.indices); - for (size_t i = 0; i < indexAccessor.count / 3; ++i) { - outIndices[i] = uint3( - inIndices[i * 3], inIndices[i * 3 + 1], inIndices[i * 3 + 2]); - } - } - - indexArray->unmap(); - geometry->setParameterObject("primitive.index", *indexArray); - } else if (indexAccessor.componentType - == TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT) { - auto indexArray = - scene.createArray(ANARI_UINT32_VEC3, indexAccessor.count / 3); - - // Check if we need to handle strided data - const auto &indexBufferView = - model.bufferViews[indexAccessor.bufferView]; - if (indexBufferView.byteStride > 0 - && indexBufferView.byteStride != sizeof(uint32_t)) { - // Handle strided indices - auto tempIndices = std::vector(indexAccessor.count); - copyStridedData(model, primitive.indices, tempIndices.data()); - auto *outIndices = indexArray->mapAs(); - - for (size_t i = 0; i < indexAccessor.count / 3; ++i) { - outIndices[i] = uint3(tempIndices[i * 3], - tempIndices[i * 3 + 1], - tempIndices[i * 3 + 2]); - } - indexArray->unmap(); - } else { - // Direct copy for tightly packed data - const uint32_t *indexData = - getAccessorData(model, primitive.indices); - auto *outIndices = indexArray->mapAs(); - std::memcpy( - outIndices, indexData, indexAccessor.count * sizeof(uint32_t)); - indexArray->unmap(); - } - geometry->setParameterObject("primitive.index", *indexArray); - } else { + if (indexAccessor.componentType != TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE + && indexAccessor.componentType + != TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT + && indexAccessor.componentType + != TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT) { logWarning("[import_GLTF] unsupported index data type"); continue; } + + auto indexArray = + scene.createArray(ANARI_UINT32_VEC3, indexAccessor.count / 3); + auto *outIndices = indexArray->mapAs(); + + if (indexAccessor.componentType + == TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE) + copyIndexTriplets(model, primitive.indices, outIndices); + else if (indexAccessor.componentType + == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT) + copyIndexTriplets(model, primitive.indices, outIndices); + else + copyIndexTriplets(model, primitive.indices, outIndices); + + indexArray->unmap(); + geometry->setParameterObject("primitive.index", *indexArray); } std::string geometryName = mesh.name + "_primitive_" @@ -1193,25 +1172,16 @@ static std::vector importGLTFMeshes(Scene &scene, // Indexed geometry const auto &indexAccessor = model.accessors[primitive.indices]; if (indexAccessor.componentType + == TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE) { + appendIndexTriplets(model, primitive.indices, indices); + } else if (indexAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT) { - auto indexData = - copyAccessorData(model, primitive.indices); - indices.reserve(indexAccessor.count / 3); - for (size_t i = 0; i < indexAccessor.count / 3; ++i) { - indices.push_back(uint3(indexData[i * 3], - indexData[i * 3 + 1], - indexData[i * 3 + 2])); - } + appendIndexTriplets( + model, primitive.indices, indices); } else if (indexAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT) { - auto indexData = - copyAccessorData(model, primitive.indices); - indices.reserve(indexAccessor.count / 3); - for (size_t i = 0; i < indexAccessor.count / 3; ++i) { - indices.push_back(uint3(indexData[i * 3], - indexData[i * 3 + 1], - indexData[i * 3 + 2])); - } + appendIndexTriplets( + model, primitive.indices, indices); } } else { // Non-indexed geometry (triangle soup) - generate sequential From 283368d725ec4707ade5a3c6bc8f534182d17fea Mon Sep 17 00:00:00 2001 From: Thomas Arcila <134677+tarcila@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:14:10 -0400 Subject: [PATCH 8/8] Preserve glTF primitive surface mapping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Downstream code attaches surfaces to nodes via surfaces[surfaceIndex + i], assuming a 1:1 primitive→surface mapping which was not always true. --- tsd/src/tsd/io/importers/import_GLTF.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tsd/src/tsd/io/importers/import_GLTF.cpp b/tsd/src/tsd/io/importers/import_GLTF.cpp index 0586ec303..dbe0b7d86 100644 --- a/tsd/src/tsd/io/importers/import_GLTF.cpp +++ b/tsd/src/tsd/io/importers/import_GLTF.cpp @@ -989,8 +989,11 @@ static std::vector importGLTFMeshes(Scene &scene, for (const auto &mesh : model.meshes) { for (const auto &primitive : mesh.primitives) { + auto skipPrimitive = [&]() { surfaces.push_back({}); }; + if (primitive.mode != TINYGLTF_MODE_TRIANGLES) { logWarning("[import_GLTF] only triangle primitives are supported"); + skipPrimitive(); continue; } @@ -1000,6 +1003,7 @@ static std::vector importGLTFMeshes(Scene &scene, auto posIt = primitive.attributes.find("POSITION"); if (posIt == primitive.attributes.end()) { logWarning("[import_GLTF] primitive missing POSITION attribute"); + skipPrimitive(); continue; } @@ -1007,6 +1011,7 @@ static std::vector importGLTFMeshes(Scene &scene, if (posAccessor.type != TINYGLTF_TYPE_VEC3 || posAccessor.componentType != TINYGLTF_COMPONENT_TYPE_FLOAT) { logWarning("[import_GLTF] unsupported position data format"); + skipPrimitive(); continue; } @@ -1106,6 +1111,7 @@ static std::vector importGLTFMeshes(Scene &scene, && indexAccessor.componentType != TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT) { logWarning("[import_GLTF] unsupported index data type"); + skipPrimitive(); continue; } @@ -1415,7 +1421,7 @@ static void populateGLTFLayer(Scene &scene, } surfaceIndex += i; - if (surfaceIndex < surfaces.size()) { + if (surfaceIndex < surfaces.size() && surfaces[surfaceIndex]) { auto surface = surfaces[surfaceIndex]; scene.insertChildObjectNode(nodeRef, surface, surface->name().c_str()); }