diff --git a/changelog.txt b/changelog.txt index bd4d316..852bd8f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,16 @@ +v1.0.10 November 19th, 2024 +fbx: + - small fbx spot light fixes + - missing file warning + - file names added to metadata now avoid dupes +gltf: + - ensure consistent light with usd +stl: + - fix up axis rotation, default is assumed to be z-up +utility: + - more robust handling of the no texture coordinate warning + - add mesh name in generated sub mesh + v1.0.9 October 29th, 2024 fbx: - import dependent filenames now added to metadata diff --git a/fbx/src/fbx.h b/fbx/src/fbx.h index 1d14dd9..24fe63b 100644 --- a/fbx/src/fbx.h +++ b/fbx/src/fbx.h @@ -37,6 +37,9 @@ namespace adobe::usd { // lighting doesn't match constexpr float FBX_TO_USD_INTENSITY_SCALE_FACTOR = 1.0; +constexpr float DEFAULT_POINT_LIGHT_RADIUS = 0.01; // 1 cm +constexpr float DEFAULT_SPOT_LIGHT_RADIUS = 0.1; // 10 cm + // Camera rotation to apply to revert to FBX coordinates, on export. Inspired by the Blender code // base, which converts from -Z to +X with a 90ยบ rotation around the Y axis: // https://github.com/blender/blender/blob/e1a44ad129d53fbd47215845be2c42fb0850135d/scripts/addons_core/io_scene_fbx/fbx_utils.py#L74C64-L74C88 diff --git a/fbx/src/fbxExport.cpp b/fbx/src/fbxExport.cpp index 4ffc5cd..d1b0a1f 100644 --- a/fbx/src/fbxExport.cpp +++ b/fbx/src/fbxExport.cpp @@ -765,8 +765,17 @@ exportFbxLights(ExportFbxContext& ctx) type = "spot (from USD disk light)"; lightType = FbxLight::EType::eSpot; - innerAngle = light.coneAngle; - outerAngle = light.coneFalloff; + // FBX inner cone angle is from the center to where falloff begins, and outer cone + // angle is from the center to where falloff ends. Meanwhile, in USD, angle is from + // the center to the edge of the cone, and softness is a number from 0 to 1 + // indicating how close to the center the falloff begins. + + // USD's cone angle is the entire shape of the spot light, corresponding to FBX's + // outer angle + outerAngle = light.coneAngle; + + // Use the fraction of the cone containing the falloff to calculate the inner cone + innerAngle = (1 - light.coneFalloff) * outerAngle; break; case LightType::Rectangle: diff --git a/fbx/src/fbxImport.cpp b/fbx/src/fbxImport.cpp index 31e1f36..f333453 100644 --- a/fbx/src/fbxImport.cpp +++ b/fbx/src/fbxImport.cpp @@ -79,9 +79,6 @@ struct ImportFbxContext // Each ImportedFbxStack has a cache of all anim layers present in that animation stack std::vector animationStacks; - - // paths to files loaded on import - PXR_NS::VtArray filenames; }; // Metadata on USD will be stored uniformily in the CustomLayerData dictionary. @@ -1133,7 +1130,7 @@ importFbxMaterials(ImportFbxContext& ctx) normalizePathFromAnyOS(fileTexture->GetRelativeFileName()); // Add the path to the metadata even if the file is not present on disk. - ctx.filenames.push_back(filePathNormalized.u8string()); + ctx.usd->importedFileNames.insert(filePathNormalized.u8string()); std::filesystem::path absFilePath; if (isAbsolutePathFromAnyOS(filePathNormalized)) { @@ -1183,7 +1180,7 @@ importFbxMaterials(ImportFbxContext& ctx) } else { std::ifstream file(absFileName, std::ios::binary); if (!file.is_open()) { - TF_RUNTIME_ERROR("Failed to open file \"%s\"", absFileName.c_str()); + TF_WARN("Failed to open file \"%s\"", absFileName.c_str()); continue; } file.seekg(0, file.end); @@ -1401,11 +1398,14 @@ importFbxLight(ImportFbxContext& ctx, FbxNodeAttribute* attribute, int parent) LightType usdType; float coneAngle = 0; float coneFalloff = 0; + float radius = 0.5; switch (fbxLight->LightType.Get()) { case FbxLight::ePoint: type = "sphere (from FBX point light)"; usdType = LightType::Sphere; + radius = DEFAULT_POINT_LIGHT_RADIUS; + break; case FbxLight::eDirectional: type = "sun (from FBX directional light)"; @@ -1416,12 +1416,21 @@ importFbxLight(ImportFbxContext& ctx, FbxNodeAttribute* attribute, int parent) type = "disk (from FBX spot light)"; usdType = LightType::Disk; - // According to FBX specs, inner angle is "HotSpot". In USD, this translates to the - // USDLuxShapingAPI ConeAngleAttribute - coneAngle = fbxLight->InnerAngle.Get(); - // According to FBX specs, outer angle is falloff. In USD, this translates to the - // USDLuxShapingAPI ConeSoftnessAttribute - coneFalloff = fbxLight->OuterAngle.Get(); + radius = DEFAULT_SPOT_LIGHT_RADIUS; + + // FBX inner cone angle is from the center to where falloff begins, and outer cone + // angle is from the center to where falloff ends. Meanwhile, in USD, angle is from + // the center to the edge of the cone, and softness is a number from 0 to 1 indicating + // how close to the center the falloff begins. + + // USD's cone angle is the entire shape of the spot light, corresponding to FBX's + // outer angle + coneAngle = fbxLight->OuterAngle.Get(); + + // Get the fraction of the cone containing the falloff + if (fbxLight->OuterAngle.Get()) { + coneFalloff = 1 - (fbxLight->InnerAngle.Get() / fbxLight->OuterAngle.Get()); + } break; case FbxLight::eArea: @@ -1478,8 +1487,10 @@ importFbxLight(ImportFbxContext& ctx, FbxNodeAttribute* attribute, int parent) // TODO: Extract FBX light radius and replace this temporary dummy value with it. When this is // updated, please update corresponding unit tests as well - light.radius = 0.5; - TF_WARN("importFbxLight: ignoring FBX light radius, setting radius=0.5\n"); + light.radius = radius; + TF_WARN("importFbxLight: ignoring FBX light radius for light of type %s, setting radius=%f\n", + type.c_str(), + radius); return true; } @@ -2133,6 +2144,12 @@ importFbx(const ImportFbxOptions& options, Fbx& fbx, UsdData& usd) ctx.scene = fbx.scene; ctx.originalColorSpace = options.originalColorSpace; + // Include the FBX file name itself in the filenames we add to the metadata + { + std::string baseName = TfGetBaseName(fbx.filename); + usd.importedFileNames.emplace(std::move(baseName)); + } + importMetadata(ctx); importFbxSettings(ctx); @@ -2149,10 +2166,6 @@ importFbx(const ImportFbxOptions& options, Fbx& fbx, UsdData& usd) setSkeletonParents(ctx); } - if (!ctx.filenames.empty()) { - usd.metadata.SetValueAtPath("filenames", VtValue(ctx.filenames)); - } - return true; } } diff --git a/gltf/src/gltf.h b/gltf/src/gltf.h index 3715381..1f3fe69 100644 --- a/gltf/src/gltf.h +++ b/gltf/src/gltf.h @@ -15,14 +15,17 @@ governing permissions and limitations under the License. namespace adobe::usd { -// Scale between intensity of USD lights and GLTF lights -const float GLTF_TO_USD_INTENSITY_SCALE_FACTOR = 100.0; +// Experimentally found to result in similar brightnesses between glTF and USD +constexpr float GLTF_POINT_LIGHT_INTENSITY_MULT = 0.225; +constexpr float GLTF_DIRECTIONAL_LIGHT_INTENSITY_MULT = 0.0000625; +constexpr float GLTF_SPOT_LIGHT_INTENSITY_MULT = 1.0; -// lights are by default given a diameter of 1, since there is no concept of light radius in glTF -const float DEFAULT_LIGHT_RADIUS = 0.5; +// Note there is no concept of a light radius in glTF +constexpr float DEFAULT_POINT_LIGHT_RADIUS = 0.01; // 1 cm +constexpr float DEFAULT_SPOT_LIGHT_RADIUS = 0.1; // 10 cm // max color value of a pixel -const float MAX_COLOR_VALUE = 255.0f; +constexpr float MAX_COLOR_VALUE = 255.0f; struct WriteGltfOptions { diff --git a/gltf/src/gltfExport.cpp b/gltf/src/gltfExport.cpp index 131cd7a..871496a 100644 --- a/gltf/src/gltfExport.cpp +++ b/gltf/src/gltfExport.cpp @@ -224,43 +224,95 @@ exportLightExtension(ExportGltfContext& ctx, int lightIndex, ExtMap& extensions) bool exportLights(ExportGltfContext& ctx) { - ctx.gltf->lights.resize(ctx.usd->lights.size()); for (size_t i = 0; i < ctx.usd->lights.size(); ++i) { const Light& light = ctx.usd->lights[i]; tinygltf::Light& gltfLight = ctx.gltf->lights[i]; + float radius = light.radius; + GfVec2f length = light.length; + + // Modify light values if the incoming USD values are in different units + if (ctx.usd->metersPerUnit > 0) { + if (radius > 0) { + radius *= ctx.usd->metersPerUnit; + } + if (length[0] > 0) { + length[0] *= ctx.usd->metersPerUnit; + } + if (length[1] > 0) { + length[1] *= ctx.usd->metersPerUnit; + } + } + + // glTF doesn't use lights that emit based on their surface area, so will multiply the + // intensity below based on the light type + float intensity = light.intensity; + switch (light.type) { case LightType::Disk: { gltfLight.type = "spot"; - // Only spot lights have innerConeAngle and outerConeAngle. We must make a separate - // "spot" attribute with this information - gltfLight.spot.innerConeAngle = GfDegreesToRadians(light.coneAngle); - gltfLight.spot.outerConeAngle = GfDegreesToRadians(light.coneFalloff); + // glTF inner cone angle is from the center to where falloff begins, and outer cone + // angle is from the center to where falloff ends. Meanwhile, in USD, angle is from + // the center to the edge of the cone, and softness is a number from 0 to 1 + // indicating how close to the center the falloff begins. + + // glTF outer cone angle is equivalent to USD cone angle + gltfLight.spot.outerConeAngle = GfDegreesToRadians(light.coneAngle); + + // Use the fraction of the cone containing the falloff to calculate the inner cone + gltfLight.spot.innerConeAngle = + (1 - ctx.usd->lights[i].coneFalloff) * gltfLight.spot.outerConeAngle; + + // inner cone angle must always be less than outer cone angle, according to the + // glTF spec. If it isn't, set it to be just less than the outer cone angle + const float epsilon = 1e-6; + if (gltfLight.spot.innerConeAngle >= gltfLight.spot.outerConeAngle && + gltfLight.spot.outerConeAngle >= epsilon) { + gltfLight.spot.innerConeAngle = gltfLight.spot.outerConeAngle - epsilon; + } + + if (radius > 0) { // Disk light, area = pi r^2 + intensity *= (M_PI * radius * radius); + } + + intensity *= GLTF_SPOT_LIGHT_INTENSITY_MULT; - } break; + break; + } case LightType::Sun: gltfLight.type = "directional"; + intensity *= GLTF_DIRECTIONAL_LIGHT_INTENSITY_MULT; + break; default: // All other light types are encoded as point lights, since gltf supports fewer // light types gltfLight.type = "point"; + if (radius > 0) { // Sphere light, area = 4 pi r^2 + intensity *= (4.0 * M_PI * radius * radius); + } else if (length[0] > 0 && length[1] > 0) { // Rectangle light, area = l * w + intensity *= (length[0] * length[1]); + } + + intensity *= GLTF_POINT_LIGHT_INTENSITY_MULT; + + // TODO: Address environment lights separately + break; } gltfLight.name = light.name; + gltfLight.intensity = intensity; + gltfLight.color.resize(3); gltfLight.color[0] = light.color[0]; gltfLight.color[1] = light.color[1]; gltfLight.color[2] = light.color[2]; - - // Divide by the scale factor to convert from USD to GLTF - gltfLight.intensity = light.intensity / GLTF_TO_USD_INTENSITY_SCALE_FACTOR; } return true; } diff --git a/gltf/src/gltfImport.cpp b/gltf/src/gltfImport.cpp index 974f425..db9c627 100644 --- a/gltf/src/gltfImport.cpp +++ b/gltf/src/gltfImport.cpp @@ -1913,25 +1913,56 @@ importLights(ImportGltfContext& ctx) light.color[1] = gltfLight.color[1]; light.color[2] = gltfLight.color[2]; } - light.intensity = gltfLight.intensity * GLTF_TO_USD_INTENSITY_SCALE_FACTOR; - // GLTF lights have no radius, so we use a default value - light.radius = DEFAULT_LIGHT_RADIUS; + // USD uses lights that emit based on their surface area, so will multiply the intensity + // below based on the light type + float intensity = gltfLight.intensity; // Add type-specific light info if (gltfLight.type == "directional") { light.type = LightType::Sun; + intensity /= GLTF_DIRECTIONAL_LIGHT_INTENSITY_MULT; + } else if (gltfLight.type == "point") { light.type = LightType::Sphere; + // Divide by the surface area of a sphere, 4 pi r^2 + intensity /= (4.0 * M_PI * DEFAULT_POINT_LIGHT_RADIUS * DEFAULT_POINT_LIGHT_RADIUS); + intensity /= GLTF_POINT_LIGHT_INTENSITY_MULT; + + // glTF lights have no radius, so we use a default value + light.radius = DEFAULT_POINT_LIGHT_RADIUS; + } else if (gltfLight.type == "spot") { light.type = LightType::Disk; - ctx.usd->lights[i].coneAngle = GfRadiansToDegrees(gltfLight.spot.innerConeAngle); - ctx.usd->lights[i].coneFalloff = GfRadiansToDegrees(gltfLight.spot.outerConeAngle); + // Divide by the area of a disk, pi r^2 + intensity /= (M_PI * DEFAULT_SPOT_LIGHT_RADIUS * DEFAULT_SPOT_LIGHT_RADIUS); + intensity /= GLTF_SPOT_LIGHT_INTENSITY_MULT; + + // glTF lights have no radius, so we use a default value + light.radius = DEFAULT_SPOT_LIGHT_RADIUS; + + // glTF inner cone angle is from the center to where falloff begins, and outer cone + // angle is from the center to where falloff ends. Meanwhile, in USD, angle is from + // the center to the edge of the cone, and softness is a number from 0 to 1 indicating + // how close to the center the falloff begins. + + // glTF outer cone angle is equivalent to USD cone angle + ctx.usd->lights[i].coneAngle = GfRadiansToDegrees(gltfLight.spot.outerConeAngle); + + if (gltfLight.spot.outerConeAngle > 0) { + // Get the fraction of the cone containing the falloff + ctx.usd->lights[i].coneFalloff = + 1 - (gltfLight.spot.innerConeAngle / gltfLight.spot.outerConeAngle); + } else { + ctx.usd->lights[i].coneFalloff = 0; + } } + + ctx.usd->lights[i].intensity = intensity; } } diff --git a/obj/src/obj.cpp b/obj/src/obj.cpp index 62ee27f..a17be7a 100644 --- a/obj/src/obj.cpp +++ b/obj/src/obj.cpp @@ -1065,7 +1065,7 @@ addImage(Obj& obj, image.uri = basename; image.name = TfStringGetBeforeSuffix(basename); image.format = getFormat(extension); - obj.filenames.push_back(filename); + obj.importedFilenames.insert(filename); if (readImages) { std::string fullFilename = parentPath + filename; if (!readFileContents(fullFilename, @@ -1516,7 +1516,7 @@ readObj(Obj& obj, const std::string& filename, bool readImages) TfStopwatch watch; watch.Start(); std::string baseName = TfGetBaseName(filename); - obj.filenames.push_back(baseName); + obj.importedFilenames.insert(baseName); std::vector objBuffer; GUARD(readFileContents(filename, objBuffer), "Failed reading obj file"); watch.Stop(); @@ -1530,7 +1530,7 @@ readObj(Obj& obj, const std::string& filename, bool readImages) const std::string parentPath = TfGetPathName(filename); for (size_t i = 0; i < obj.libraries.size(); i++) { ObjMaterialLibrary& library = obj.libraries[i]; - obj.filenames.push_back(library.filename); + obj.importedFilenames.insert(library.filename); std::string materialFilename = parentPath + library.filename; std::vector materialBuffer; if (!readFileContents(materialFilename, materialBuffer)) { @@ -1631,12 +1631,11 @@ writeObjHeader(const Obj& obj, std::fstream& file) buffer.directWrite("# Obj model"); buffer.directWrite("\n# This model was generated by the USD fileformat plugin"); - for (const auto& comment: obj.comments) + for (const auto& comment : obj.comments) buffer.directWrite(std::string("\n") + comment); buffer.flush(); } - // Writes obj geometry to the stream `file` in a buffered way. /// See `BufferControl`. void diff --git a/obj/src/obj.h b/obj/src/obj.h index 478f04e..197d677 100644 --- a/obj/src/obj.h +++ b/obj/src/obj.h @@ -181,7 +181,7 @@ struct ObjObject struct Obj { bool hasAdobeProperties = false; - PXR_NS::VtArray filenames; + std::set importedFilenames; std::vector objects; std::vector materials; std::vector images; diff --git a/obj/src/objExport.cpp b/obj/src/objExport.cpp index 4f4d201..bbac723 100644 --- a/obj/src/objExport.cpp +++ b/obj/src/objExport.cpp @@ -82,7 +82,7 @@ writeObjMap(const UsdData& usd, ObjMap& map, const Input& input) if (input.transformTranslation.IsHolding()) { GfVec2f trans = input.transformTranslation.UncheckedGet(); map.origin = GfVec3f(trans[0], trans[1], 0.0f); - } + } } } @@ -211,7 +211,6 @@ exportObj(const ExportObjOptions& options, const UsdData& usd, Obj& obj) obj.comments.push_back("# Meters per unit: " + TfStringify(usd.metersPerUnit)); const std::string name = TfStringGetBeforeSuffix(TfGetBaseName(options.filename)); - obj.filenames.push_back(name + ".obj"); obj.images.resize(usd.images.size()); for (size_t i = 0; i < usd.images.size(); i++) { @@ -235,7 +234,7 @@ exportObj(const ExportObjOptions& options, const UsdData& usd, Obj& obj) UniqueNameEnforcer uniqueMaterialNameEnforcer; std::vector uniqueNames; uniqueNames.reserve(usd.materials.size()); - for (auto &m : usd.materials) { + for (auto& m : usd.materials) { uniqueNames.push_back(m.name); uniqueMaterialNameEnforcer.enforceUniqueness(uniqueNames.back()); } diff --git a/obj/src/objImport.cpp b/obj/src/objImport.cpp index f175fcf..ac7c104 100644 --- a/obj/src/objImport.cpp +++ b/obj/src/objImport.cpp @@ -161,10 +161,16 @@ importEmissive(const ObjMaterial& m, bool importObj(const ImportObjOptions& options, Obj& obj, UsdData& usd) { + // The obj importer collects filenames in the Obj object- add these files to UsdData so that it + // will be incorporated in the metadata + for (const std::string filename : obj.importedFilenames) { + usd.importedFileNames.insert(filename); + } + usd.metadata.SetValueAtPath("hasAdobeProperties", VtValue(obj.hasAdobeProperties)); - usd.metadata.SetValueAtPath("filenames", VtValue(obj.filenames)); - if(!obj.originalColorSpace.IsEmpty()) { - usd.metadata.SetValueAtPath(AdobeTokens->originalColorSpace, VtValue(obj.originalColorSpace)); + if (!obj.originalColorSpace.IsEmpty()) { + usd.metadata.SetValueAtPath(AdobeTokens->originalColorSpace, + VtValue(obj.originalColorSpace)); } if (options.importMaterials) { InputTranslator inputTranslator(options.importImages, obj.images, DEBUG_TAG); diff --git a/stl/src/fileFormat.cpp b/stl/src/fileFormat.cpp index 7f72a96..0253423 100644 --- a/stl/src/fileFormat.cpp +++ b/stl/src/fileFormat.cpp @@ -63,14 +63,14 @@ UsdStlFileFormat::Read(SdfLayer* layer, const std::string& resolvedPath, bool me usd.upAxis = UsdGeomTokens->z; SdfAbstractDataRefPtr layerData(new SdfData()); - StlModel stlModel; stlModel.Read(resolvedPath); std::string fileType = getFileExtension(resolvedPath, DEBUG_TAG); GUARD(stlModel.Populated(), "Failed opening STL file: %s \n", resolvedPath.c_str()); GUARD(importStl(usd, stlModel), "Error translating STL to USD\n"); WriteLayerOptions layerOptions; - GUARD(writeLayer(layerOptions, usd, layer, layerData, fileType, DEBUG_TAG, SdfFileFormat::_SetLayerData), + GUARD(writeLayer( + layerOptions, usd, layer, layerData, fileType, DEBUG_TAG, SdfFileFormat::_SetLayerData), "Error writing to the USD layer\n"); return true; @@ -104,11 +104,13 @@ UsdStlFileFormat::WriteToFile(const SdfLayer& layer, ExportStlOptions options; GUARD(exportStl(options, usd, stl), "Error translating USD to STL\n"); StlFormat format = readStlExportFormat(usd); - TF_DEBUG_MSG(FILE_FORMAT_STL, "START time: %ld\n", static_cast(watch.GetMilliseconds())); + TF_DEBUG_MSG( + FILE_FORMAT_STL, "START time: %ld\n", static_cast(watch.GetMilliseconds())); watch.Start(); GUARD(stl.Write(filename, format), "Error writing STL to %s\n", filename.c_str()); watch.Stop(); - TF_DEBUG_MSG(FILE_FORMAT_STL, "WRITE time: %ld\n", static_cast(watch.GetMilliseconds())); + TF_DEBUG_MSG( + FILE_FORMAT_STL, "WRITE time: %ld\n", static_cast(watch.GetMilliseconds())); return true; } diff --git a/stl/src/stlExport.cpp b/stl/src/stlExport.cpp index a01a63b..010cc7d 100644 --- a/stl/src/stlExport.cpp +++ b/stl/src/stlExport.cpp @@ -10,7 +10,15 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ #include "stlExport.h" + +#include #include + +#include + +using namespace PXR_NS; +using namespace adobe::usd; + namespace usdStl { StlFormat @@ -24,40 +32,45 @@ readStlExportFormat(const UsdData& data) format = StlFormat::Ascii; } } - return format; } bool exportStl(const ExportStlOptions& options, const UsdData& usd, StlModel& stl) { - // precondition: input usd data was previously triangulated - if (usd.nodes.empty()) return false; + GfMatrix4d upAxisTransform(1.0f); + std::string upAxis = usd.upAxis.GetString(); + if (!upAxis.empty() && std::toupper(upAxis[0]) == 'Y') { + upAxisTransform = GfMatrix4d(GfRotation(GfVec3d(1.0f, 0.0f, 0.0f), 90.0f), GfVec3d(0.0f)); + } + for (const Node& node : usd.nodes) { + GfMatrix4d worldTransform = node.worldTransform * upAxisTransform; for (int meshIndex : node.staticMeshes) { + if (meshIndex < 0 || meshIndex >= usd.meshes.size()) { + TF_WARN("Invalid mesh index %d -- Skipping", meshIndex); + continue; + } const Mesh& mesh = usd.meshes[meshIndex]; - PXR_NS::VtArray meshIndices; + VtArray meshIndices; if (mesh.indices.empty()) { meshIndices.resize(mesh.points.size()); - std::iota(std::begin(meshIndices), std::end(meshIndices), 0); + std::iota(meshIndices.begin(), meshIndices.end(), 0); } else { meshIndices = mesh.indices; } - for (size_t i = 0; i < meshIndices.size(); i += 3) { + for (size_t i = 0; i + 2 < meshIndices.size(); i += 3) { StlFacet facet; for (int j = 0; j < 3; j++) { StlVec3f vertex; const int vertex_index = meshIndices[i + j]; const PXR_NS::GfVec3f& vertex_data = mesh.points[vertex_index]; - const PXR_NS::GfVec3f transformedPoint = - node.worldTransform.Transform(vertex_data); - vertex.x = transformedPoint[0]; - vertex.y = transformedPoint[1]; - vertex.z = transformedPoint[2]; + const PXR_NS::GfVec3f transformedPoint = worldTransform.Transform(vertex_data); + vertex = { transformedPoint[0], transformedPoint[1], transformedPoint[2] }; facet.vertices[j] = vertex; } @@ -72,11 +85,12 @@ exportStl(const ExportStlOptions& options, const UsdData& usd, StlModel& stl) faceEdge2.x = facet.vertices[1].x - facet.vertices[0].x; faceEdge2.y = facet.vertices[1].y - facet.vertices[0].y; faceEdge2.z = facet.vertices[1].z - facet.vertices[0].z; - + normal = crossProduct(faceEdge1, faceEdge2); // handle degenerate normals if (normal.x == 0.f && normal.y == 0.f && normal.z == 0.f) - normal.y = 1.f; // Synthesize a valid normal. Actual value is irrelevant because the triangle won't be visible + normal.y = 1.f; // Synthesize a valid normal. Actual value is irrelevant because + // the triangle won't be visible normal.normalize(); facet.normal = normal; diff --git a/stl/src/stlImport.cpp b/stl/src/stlImport.cpp index 72295f5..f75e5e1 100644 --- a/stl/src/stlImport.cpp +++ b/stl/src/stlImport.cpp @@ -11,7 +11,9 @@ governing permissions and limitations under the License. */ #include "stlImport.h" #include "stlModel.h" -#include "usdData.h" +#include +#include + #include #include #include @@ -33,6 +35,7 @@ governing permissions and limitations under the License. using namespace PXR_NS; using namespace adobe; +using namespace adobe::usd; namespace usdStl { @@ -42,12 +45,24 @@ importStl(UsdData& usd, const StlModel& stl) auto [nodeIndex, node] = usd.addNode(-1); auto [meshIndex, mesh] = usd.addMesh(); node.staticMeshes.push_back(meshIndex); - mesh.faces.resize(stl.FacetCount()); - mesh.indices.resize(stl.FacetCount() * 3); - mesh.points.resize(stl.FacetCount() * 3); - mesh.normals.values.resize(stl.FacetCount()); + + // Apply rotation to node's worldTransform if Y-up + std::string upAxis = usd.upAxis.GetString(); + GfMatrix4d rotationMatrix(1.0f); + if (!upAxis.empty() && std::toupper(upAxis[0]) == 'Y') { + rotationMatrix = GfMatrix4d(GfRotation(GfVec3d(1.0f, 0.0f, 0.0f), -90.0f), GfVec3d(0.0f)); + } + node.worldTransform = node.worldTransform * rotationMatrix; + + // Resize mesh data structures based on the number of facets + size_t facetCount = stl.FacetCount(); + mesh.faces.resize(facetCount); + mesh.indices.resize(facetCount * 3); + mesh.points.resize(facetCount * 3); + mesh.normals.values.resize(facetCount); mesh.normals.interpolation = UsdGeomTokens->uniform; - for (int i = 0; i < stl.FacetCount(); i++) { + + for (size_t i = 0; i < facetCount; ++i) { StlFacet facet = stl.GetFacet(i); StlVec3f v0 = facet.vertices[0]; StlVec3f v1 = facet.vertices[1]; @@ -56,11 +71,22 @@ importStl(UsdData& usd, const StlModel& stl) mesh.indices[3 * i] = 3 * i; mesh.indices[3 * i + 1] = 3 * i + 1; mesh.indices[3 * i + 2] = 3 * i + 2; - mesh.points[3 * i] = PXR_NS::GfVec3f(v0.x, v0.y, v0.z); - mesh.points[3 * i + 1] = PXR_NS::GfVec3f(v1.x, v1.y, v1.z); - mesh.points[3 * i + 2] = PXR_NS::GfVec3f(v2.x, v2.y, v2.z); - mesh.normals.values[i] = PXR_NS::GfVec3f(facet.normal.x, facet.normal.y, facet.normal.z); + + // Store STL vertices and normals + mesh.points[3 * i] = GfVec3f(v0.x, v0.y, v0.z); + mesh.points[3 * i + 1] = GfVec3f(v1.x, v1.y, v1.z); + mesh.points[3 * i + 2] = GfVec3f(v2.x, v2.y, v2.z); + GfVec3f usdNormal = GfVec3f(facet.normal.x, facet.normal.y, facet.normal.z); + usdNormal.Normalize(); + + // Handle degenerate normals + if (usdNormal.GetLengthSq() < 1e-3f) { + usdNormal = GfVec3f(0.0f, 1.0f, 0.0f); // Synthesize a valid normal + } + + mesh.normals.values[i] = usdNormal; } + return true; } diff --git a/stl/src/stlModel.h b/stl/src/stlModel.h index a47d8a8..6802ee7 100644 --- a/stl/src/stlModel.h +++ b/stl/src/stlModel.h @@ -51,6 +51,12 @@ struct StlVec3f y = 0.0; z = 0.0; } + StlVec3f(float _x, float _y, float _z) + : x(_x) + , y(_y) + , z(_z) + { + } }; struct StlFacet diff --git a/utils/common.h b/utils/common.h index e6f2a0f..2008cad 100644 --- a/utils/common.h +++ b/utils/common.h @@ -387,4 +387,5 @@ trim(std::string& s) rtrim(s); ltrim(s); } + } diff --git a/utils/layerRead.cpp b/utils/layerRead.cpp index 8741633..06709a3 100644 --- a/utils/layerRead.cpp +++ b/utils/layerRead.cpp @@ -414,7 +414,17 @@ readMeshOrPointsData(ReadLayerContext& ctx, Mesh& mesh, int meshIndex, const Usd TfTokenVector uvTokens = findTextureCoordinatePrimvars(primvarsAPI); if (uvTokens.empty()) { - TF_WARN("No texture coordinates for mesh %s", prim.GetPath().GetText()); + auto path = prim.GetPath(); + if (path.IsEmpty()) { + TF_WARN("No texture coordinates for mesh with an empty path"); + } else { + const char* pathText = path.GetText(); + if (pathText == nullptr) { + TF_WARN("No texture coordinates for mesh with a null path text"); + } else { + TF_WARN("No texture coordinates for mesh %s", pathText); + } + } } else { readPrimvar(primvarsAPI, uvTokens[0], mesh.uvs); for (size_t i = 1; i < uvTokens.size(); ++i) { @@ -1354,6 +1364,43 @@ readCamera(ReadLayerContext& ctx, const UsdPrim& prim, int parent) return true; } +/** + * Reads the attributes of a light that are common to all light types. This includes color and + * intensity. Note that the resulting intensity value will be a combination of the USD light's + * intensity and exposure values. + * + * @tparam T The type of the USD light. This should be a subclass of either + * UsdLuxBoundableLightBase or UsdLuxNonboundableLightBase + * @param usdLight The USD light to read from + * @param light The Light object to write to. This object will be modified by this function + */ +template +void +readCommonLightAttributes(const T& usdLight, Light& light) +{ + // Color + if (!usdLight.GetColorAttr().Get(&light.color)) { + TF_WARN("When reading USD layers, failed to read color of light %s", light.name.c_str()); + } + + // Intensity and exposure + bool hasLightValue = false; + float exposure = 0; // USD default exposure is 0 + if (usdLight.GetIntensityAttr().Get(&light.intensity)) { + hasLightValue = true; + } else { + light.intensity = 1; // USD default intensity is 1 + } + if (usdLight.GetExposureAttr().Get(&exposure)) { + hasLightValue = true; + light.intensity *= std::exp2(exposure); + } + if (!hasLightValue) { + TF_WARN("When reading USD layers, failed to read either intensity or exposure of light %s", + light.name.c_str()); + } +} + bool readLight(ReadLayerContext& ctx, const UsdPrim& prim, int parent) { @@ -1363,22 +1410,14 @@ readLight(ReadLayerContext& ctx, const UsdPrim& prim, int parent) light.name = prim.GetName(); + // Light type specific attributes + if (prim.IsA()) { light.type = LightType::Disk; const UsdLuxDiskLight usdLight(prim); bool hasShapingAPI = prim.HasAPI(); - // Color - if (!usdLight.GetColorAttr().Get(&light.color)) { - TF_WARN("When reading USD layers, failed to read color of disk light %s", - light.name.c_str()); - } - - // Intensity - if (!usdLight.GetIntensityAttr().Get(&light.intensity)) { - TF_WARN("When reading USD layers, failed to read intensity of disk light %s", - light.name.c_str()); - } + readCommonLightAttributes(usdLight, light); // Radius if (!usdLight.GetRadiusAttr().Get(&light.radius)) { @@ -1414,17 +1453,7 @@ readLight(ReadLayerContext& ctx, const UsdPrim& prim, int parent) light.type = LightType::Rectangle; const UsdLuxRectLight usdLight(prim); - // Color - if (!usdLight.GetColorAttr().Get(&light.color)) { - TF_WARN("When reading USD layers, failed to read color of rectangle light %s", - light.name.c_str()); - } - - // Intensity - if (!usdLight.GetIntensityAttr().Get(&light.intensity)) { - TF_WARN("When reading USD layers, failed to read intensity of rectangle light %s", - light.name.c_str()); - } + readCommonLightAttributes(usdLight, light); // Length (width) if (!usdLight.GetWidthAttr().Get(&light.length[0])) { @@ -1446,17 +1475,7 @@ readLight(ReadLayerContext& ctx, const UsdPrim& prim, int parent) light.type = LightType::Sphere; const UsdLuxSphereLight usdLight(prim); - // Color - if (!usdLight.GetColorAttr().Get(&light.color)) { - TF_WARN("When reading USD layers, failed to read color of sphere light %s", - light.name.c_str()); - } - - // Intensity - if (!usdLight.GetIntensityAttr().Get(&light.intensity)) { - TF_WARN("When reading USD layers, failed to read intensity of sphere light %s", - light.name.c_str()); - } + readCommonLightAttributes(usdLight, light); // Radius if (!usdLight.GetRadiusAttr().Get(&light.radius)) { @@ -1472,17 +1491,7 @@ readLight(ReadLayerContext& ctx, const UsdPrim& prim, int parent) light.type = LightType::Environment; const UsdLuxDomeLight usdLight(prim); - // Color - if (!usdLight.GetColorAttr().Get(&light.color)) { - TF_WARN("When reading USD layers, failed to read color of dome light %s", - light.name.c_str()); - } - - // Intensity - if (!usdLight.GetIntensityAttr().Get(&light.intensity)) { - TF_WARN("When reading USD layers, failed to read intensity of dome light %s", - light.name.c_str()); - } + readCommonLightAttributes(usdLight, light); // TODO: Add support for texture @@ -1494,17 +1503,7 @@ readLight(ReadLayerContext& ctx, const UsdPrim& prim, int parent) light.type = LightType::Sun; UsdLuxDistantLight usdLight(prim); - // Color - if (!usdLight.GetColorAttr().Get(&light.color)) { - TF_WARN("When reading USD layers, failed to read color of distant light %s", - light.name.c_str()); - } - - // Intensity - if (!usdLight.GetIntensityAttr().Get(&light.intensity)) { - TF_WARN("When reading USD layers, failed to read intensity of distant light %s", - light.name.c_str()); - } + readCommonLightAttributes(usdLight, light); // Angle if (!usdLight.GetAngleAttr().Get(&light.angle)) { @@ -1524,6 +1523,7 @@ readLight(ReadLayerContext& ctx, const UsdPrim& prim, int parent) return false; } + return true; } diff --git a/utils/layerWriteSdfData.cpp b/utils/layerWriteSdfData.cpp index bce83f0..5144aef 100644 --- a/utils/layerWriteSdfData.cpp +++ b/utils/layerWriteSdfData.cpp @@ -641,7 +641,7 @@ _writeMesh(SdfAbstractData* sdfData, if (mesh.subsets.size()) { for (size_t i = 0; i < mesh.subsets.size(); i++) { const Subset& subset = mesh.subsets[i]; - TfToken subsetName = TfToken("sub" + std::to_string(i)); + TfToken subsetName = TfToken(mesh.name + "_sub" + std::to_string(i)); SdfPath subsetPath = _createGeomSubset(sdfData, primPath, subsetName, subset); if (subset.material >= 0) { @@ -1323,6 +1323,12 @@ writeLayer(const WriteLayerOptions& options, // track data into metadata, and then join all tracks together into one track _writeAnimationTracks(options, data); + // Add file names to metadata + if (!data.importedFileNames.empty()) { + PXR_NS::VtArray filenames(data.importedFileNames.begin(), data.importedFileNames.end()); + data.metadata.SetValueAtPath("filenames", VtValue(filenames)); + } + GUARD(_writeLayerSdfData(options, data, layer->GetDisplayName(), diff --git a/utils/test.cpp b/utils/test.cpp index b0e6668..ae29f11 100644 --- a/utils/test.cpp +++ b/utils/test.cpp @@ -84,10 +84,17 @@ assertArray(VtArray& actual, const ArrayData& expected, const std::string& } bool -floatsEqual(float a, float b) +floatsEqual(float a, float b, float epsilon = 1e-6) { // Ensure that floating point comparison doesn't result in a false negative - return std::abs(a - b) < 1e-6; + return std::abs(a - b) < epsilon; +} + +bool +doublesEqual(double a, double b, double epsilon = 1e-6) +{ + // Ensure that floating point comparison doesn't result in a false negative + return std::abs(a - b) < epsilon; } #define ASSERT_VEC2F(...) assertVec2f(__VA_ARGS__) @@ -172,7 +179,7 @@ assertVec3d(const PXR_NS::GfVec3d& actual, bool valuesMatch = true; size_t i; for (i = 0; i < 3; ++i) { - if (actual[i] != expected[i]) { + if (!doublesEqual(actual[i], expected[i])) { valuesMatch = false; break; } @@ -412,21 +419,24 @@ assertLight(PXR_NS::UsdStageRefPtr stage, const std::string& path, const LightDa GfVec3f scale; // The transformations for lights in our USD assets tend to be stored by a parent node - if (extractUsdAttribute(parent, TfToken("xformOp:translate"), &translation)) { - ASSERT_VEC3D( - translation, lightData.translation, path + "'s parent translation does not match\n"); - } else { - TF_WARN("No translation attribute found for %s\n", path.c_str()); - } - if (extractUsdAttribute(parent, TfToken("xformOp:orient"), &rotation)) { - ASSERT_QUATF(rotation, lightData.rotation, path + "'s parent rotation does not match\n"); - } else { - TF_WARN("No rotation attribute found for %s\n", path.c_str()); - } - if (extractUsdAttribute(parent, TfToken("xformOp:scale"), &scale)) { - ASSERT_VEC3F(scale, lightData.scale, path + "'s parent scale does not match\n"); - } else { - TF_WARN("No scale attribute found for %s\n", path.c_str()); + if (lightData.translation) { + ASSERT_TRUE( + extractUsdAttribute(parent, TfToken("xformOp:translate"), &translation)) + << "Expected translation attribute not found for " << path << "\n"; + ASSERT_VEC3D(translation, + lightData.translation.value(), + path + "'s parent translation does not match\n"); + } + if (lightData.rotation) { + ASSERT_TRUE(extractUsdAttribute(parent, TfToken("xformOp:orient"), &rotation)) + << "Expected orient attribute not found for " << path << "\n"; + ASSERT_QUATF( + rotation, lightData.rotation.value(), path + "'s parent rotation does not match\n"); + } + if (lightData.scale) { + ASSERT_TRUE(extractUsdAttribute(parent, TfToken("xformOp:scale"), &scale)) + << "Expected scale attribute not found for " << path << "\n"; + ASSERT_VEC3F(scale, lightData.scale.value(), path + "'s parent scale does not match\n"); } // Next, we check the light data itself @@ -435,43 +445,74 @@ assertLight(PXR_NS::UsdStageRefPtr stage, const std::string& path, const LightDa UsdLuxSphereLight sphereLight(prim); ASSERT_TRUE(sphereLight) << path << " could not be cast to sphere light\n"; - PXR_NS::GfVec3f color; - ASSERT_TRUE(sphereLight.GetColorAttr().Get(&color)) << path << " has no color attribute\n"; - ASSERT_VEC3F(color, lightData.color, path + " color does not match\n"); - float intensity; - ASSERT_TRUE(sphereLight.GetIntensityAttr().Get(&intensity)) - << path << " has no intensity attribute\n"; - ASSERT_FLOAT_EQ(intensity, lightData.intensity) << path << " intensity does not match\n"; + if (lightData.color) { + PXR_NS::GfVec3f color; + ASSERT_TRUE(sphereLight.GetColorAttr().Get(&color)) + << path << " is missing expected color attribute\n"; + ASSERT_VEC3F(color, lightData.color.value(), path + " color does not match\n"); + } + + if (lightData.intensity) { + float intensity; + ASSERT_TRUE(sphereLight.GetIntensityAttr().Get(&intensity)) + << path << " is missing expected intensity attribute\n"; + ASSERT_FLOAT_EQ(intensity, lightData.intensity.value()) + << path << " intensity does not match\n"; + } - // Add radius support once we support this to import + if (lightData.radius) { + float radius; + ASSERT_TRUE(sphereLight.GetRadiusAttr().Get(&radius)) + << path << " is missing expected radius attribute\n"; + ASSERT_FLOAT_EQ(radius, lightData.radius.value()) << path << " radius does not match\n"; + } } else if (prim.IsA()) { UsdLuxDistantLight distantLight(prim); ASSERT_TRUE(distantLight) << path << " could not be cast to distant light\n"; - PXR_NS::GfVec3f color; - ASSERT_TRUE(distantLight.GetColorAttr().Get(&color)) << path << " has no color attribute\n"; - ASSERT_VEC3F(color, lightData.color, path + " color does not match\n"); - float intensity; - ASSERT_TRUE(distantLight.GetIntensityAttr().Get(&intensity)) - << path << " has no intensity attribute\n"; - ASSERT_FLOAT_EQ(intensity, lightData.intensity) << path << " intensity does not match\n"; + if (lightData.color) { + PXR_NS::GfVec3f color; + ASSERT_TRUE(distantLight.GetColorAttr().Get(&color)) + << path << " is missing expected color attribute\n"; + ASSERT_VEC3F(color, lightData.color.value(), path + " color does not match\n"); + } - // Add radius support once we support importing this + if (lightData.intensity) { + float intensity; + ASSERT_TRUE(distantLight.GetIntensityAttr().Get(&intensity)) + << path << " is missing expected intensity attribute\n"; + ASSERT_FLOAT_EQ(intensity, lightData.intensity.value()) + << path << " intensity does not match\n"; + } + + // Distant lights don't have a radius } else if (prim.IsA()) { UsdLuxDiskLight diskLight(prim); ASSERT_TRUE(diskLight) << path << " could not be cast to disk light\n"; - PXR_NS::GfVec3f color; - ASSERT_TRUE(diskLight.GetColorAttr().Get(&color)) << path << " has no color attribute\n"; - ASSERT_VEC3F(color, lightData.color, path + " color does not match\n"); - float intensity; - ASSERT_TRUE(diskLight.GetIntensityAttr().Get(&intensity)) - << path << " has no intensity attribute\n"; - ASSERT_FLOAT_EQ(intensity, lightData.intensity) << path << " intensity does not match\n"; + if (lightData.color) { + PXR_NS::GfVec3f color; + ASSERT_TRUE(diskLight.GetColorAttr().Get(&color)) + << path << " is missing expected color attribute\n"; + ASSERT_VEC3F(color, lightData.color.value(), path + " color does not match\n"); + } - // Add radius test once we support importing this + if (lightData.intensity) { + float intensity; + ASSERT_TRUE(diskLight.GetIntensityAttr().Get(&intensity)) + << path << " is missing expected intensity attribute\n"; + ASSERT_FLOAT_EQ(intensity, lightData.intensity.value()) + << path << " intensity does not match\n"; + } + + if (lightData.radius) { + float radius; + ASSERT_TRUE(diskLight.GetRadiusAttr().Get(&radius)) + << path << " is missing expected radius attribute\n"; + ASSERT_FLOAT_EQ(radius, lightData.radius.value()) << path << " radius does not match\n"; + } } else if (prim.IsA()) { ASSERT_TRUE(false) << "Rectangle lights are not supported yet on import\n"; @@ -512,14 +553,21 @@ assertLight(PXR_NS::UsdStageRefPtr stage, const std::string& path, const LightDa if (prim.HasAPI()) { UsdLuxShapingAPI shapingAPI(prim); - // Disk specific attributes - float coneAngle, coneFalloff; - if (shapingAPI.GetShapingConeAngleAttr().Get(&coneAngle)) { - ASSERT_FLOAT_EQ(coneAngle, lightData.coneAngle) + // Shaping API specific attributes + + if (lightData.coneAngle) { + float coneAngle; + ASSERT_TRUE(shapingAPI.GetShapingConeAngleAttr().Get(&coneAngle)) + << path << " is missing expected angle attribute\n"; + ASSERT_FLOAT_EQ(coneAngle, lightData.coneAngle.value()) << path << " cone angle does not match\n"; } - if (shapingAPI.GetShapingConeSoftnessAttr().Get(&coneFalloff)) { - ASSERT_FLOAT_EQ(coneFalloff, lightData.coneFalloff) + + if (lightData.coneFalloff) { + float coneFalloff; + ASSERT_TRUE(shapingAPI.GetShapingConeSoftnessAttr().Get(&coneFalloff)) + << path << " is missing expected softness attribute\n"; + ASSERT_FLOAT_EQ(coneFalloff, lightData.coneFalloff.value()) << path << " cone falloff does not match\n"; } } diff --git a/utils/test.h b/utils/test.h index 1c8dcc2..5df1dba 100644 --- a/utils/test.h +++ b/utils/test.h @@ -176,20 +176,19 @@ struct USDFFUTILS_API CameraData struct USDFFUTILS_API LightData { // Light transformation data - PXR_NS::GfVec3d translation = { 0, 0, 0 }; - PXR_NS::GfQuatf rotation = { 0, 0, 0, 0 }; - PXR_NS::GfVec3f scale = { 0, 0, 0 }; + std::optional translation; + std::optional rotation; + std::optional scale; // Light data - PXR_NS::GfVec3f color = { 0, 0, 0 }; - float intensity = 0; - float coneAngle = 0; - - float coneFalloff = 0; + std::optional color; + std::optional intensity; + std::optional coneAngle; + std::optional coneFalloff; + std::optional radius; // Add these in when we support importing lights that use these // PXR_NS::GfVec2f length - // float radius // ImageAsset texture }; USDFFUTILS_API void assertPrim(PXR_NS::UsdStageRefPtr stage, const std::string& path); diff --git a/utils/usdData.h b/utils/usdData.h index cfa6482..1396f80 100644 --- a/utils/usdData.h +++ b/utils/usdData.h @@ -394,6 +394,7 @@ struct USDFFUTILS_API UsdData bool hasAnimations = false; std::vector animationTracks; double timeCodesPerSecond = 24; + std::set importedFileNames; // import only- to be added to metadata std::vector rootNodes; std::vector nodes; diff --git a/version b/version index e5a4a5e..437d26b 100644 --- a/version +++ b/version @@ -1 +1 @@ -1.0.9 \ No newline at end of file +1.0.10 \ No newline at end of file