diff --git a/.gitignore b/.gitignore index 174c863288..a7ab4508eb 100644 --- a/.gitignore +++ b/.gitignore @@ -80,4 +80,4 @@ Production /vcpkg_installed/ /export/ /.cache/ -/compile_commands.json \ No newline at end of file +/compile_commands.json diff --git a/meshroom/aliceVision/ExportMeshUSD.py b/meshroom/aliceVision/ExportMeshUSD.py new file mode 100644 index 0000000000..6418dfcde8 --- /dev/null +++ b/meshroom/aliceVision/ExportMeshUSD.py @@ -0,0 +1,44 @@ +__version__ = "1.0" + +from meshroom.core import desc +from meshroom.core.utils import VERBOSE_LEVEL + + +class exportMeshUSD(desc.AVCommandLineNode): + commandLine = "aliceVision_exportMeshUSD {allParams}" + size = desc.DynamicNodeSize("input") + + category = "Utils" + documentation = """ Export a mesh (OBJ file) to USD format. """ + + inputs = [ + desc.File( + name="input", + label="Input", + description="Input mesh file.", + value="", + ), + desc.ChoiceParam( + name="fileType", + label="USD File Format", + description="Output USD file format.", + value="usda", + values=["usda", "usdc", "usdz"] + ), + desc.ChoiceParam( + name="verboseLevel", + label="Verbose Level", + description="Verbosity level (fatal, error, warning, info, debug, trace).", + values=VERBOSE_LEVEL, + value="info", + ), + ] + + outputs = [ + desc.File( + name="output", + label="Output", + description="Path to the output file.", + value="{nodeCacheFolder}/output.{fileTypeValue}", + ), + ] diff --git a/meshroom/aliceVision/ExportUSD.py b/meshroom/aliceVision/ExportUSD.py index c4ddb4e0d4..415981bc16 100644 --- a/meshroom/aliceVision/ExportUSD.py +++ b/meshroom/aliceVision/ExportUSD.py @@ -8,22 +8,25 @@ class ExportUSD(desc.AVCommandLineNode): commandLine = "aliceVision_exportUSD {allParams}" size = desc.DynamicNodeSize("input") - category = "Utils" - documentation = """ Export a mesh (OBJ file) to USD format. """ + category = "Export" + documentation = """ +Convert cameras from an SfM scene into an animated cameras in USD file format. +Based on the input image filenames, it will recognize the input video sequence to create an animated camera. +""" inputs = [ desc.File( name="input", - label="Input", - description="Input mesh file.", + label="Input SfMData", + description="SfMData file containing a complete SfM.", value="", ), - desc.ChoiceParam( - name="fileType", - label="USD File Format", - description="Output USD file format.", - value="usda", - values=["usda", "usdc", "usdz"] + desc.FloatParam( + name="frameRate", + label="Camera Frame Rate", + description="Define the camera's Frames per seconds.", + value=24.0, + range=(1.0, 60.0, 1.0), ), desc.ChoiceParam( name="verboseLevel", @@ -37,8 +40,8 @@ class ExportUSD(desc.AVCommandLineNode): outputs = [ desc.File( name="output", - label="Output", - description="Path to the output file.", - value="{nodeCacheFolder}/output.{fileTypeValue}", - ), + label="USD filename", + description="Output usd filename", + value="{nodeCacheFolder}/animated.usda", + ) ] diff --git a/src/aliceVision/sfmDataIO/CMakeLists.txt b/src/aliceVision/sfmDataIO/CMakeLists.txt index 07cd600a24..c0a28feb17 100644 --- a/src/aliceVision/sfmDataIO/CMakeLists.txt +++ b/src/aliceVision/sfmDataIO/CMakeLists.txt @@ -37,6 +37,16 @@ if (ALICEVISION_HAVE_ALEMBIC) ) endif() +if (ALICEVISION_HAVE_USD) + + list(APPEND sfmDataIO_files_headers + UsdExporter.hpp + ) + list(APPEND sfmDataIO_files_sources + UsdExporter.cpp + ) +endif() + alicevision_add_library(aliceVision_sfmDataIO SOURCES ${sfmDataIO_files_headers} ${sfmDataIO_files_sources} PUBLIC_LINKS @@ -60,6 +70,17 @@ if (ALICEVISION_HAVE_ALEMBIC) ) endif() +if (ALICEVISION_HAVE_USD) + target_link_libraries(aliceVision_sfmDataIO + PRIVATE usd + usdGeom + gf + tf + vt + sdf + ) +endif() + # Unit tests alicevision_add_test(sfmDataIO_test.cpp diff --git a/src/aliceVision/sfmDataIO/UsdExporter.cpp b/src/aliceVision/sfmDataIO/UsdExporter.cpp new file mode 100644 index 0000000000..337bbf9d62 --- /dev/null +++ b/src/aliceVision/sfmDataIO/UsdExporter.cpp @@ -0,0 +1,120 @@ +// This file is part of the AliceVision project. +// Copyright (c) 2025 AliceVision contributors. +// This Source Code Form is subject to the terms of the Mozilla Public License, +// v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include +#include +#include + +#include + +#include +#include +#include + +#include +#include + + +PXR_NAMESPACE_USING_DIRECTIVE + +namespace aliceVision { +namespace sfmDataIO { + + +UsdExporter::UsdExporter(const std::string & filename, double frameRate) +{ + _stage = UsdStage::CreateNew(filename); + + UsdGeomXform worldPrim = UsdGeomXform::Define(_stage, SdfPath("/World")); + + _stage->SetTimeCodesPerSecond(frameRate); + _stage->SetFramesPerSecond(frameRate); + _startTimeCode = std::numeric_limits::max(); + _endTimeCode = 0; +} + +void UsdExporter::terminate() +{ + _stage->SetStartTimeCode(_startTimeCode); + _stage->SetEndTimeCode(_endTimeCode); + + _stage->Save(); +} + +void UsdExporter::createNewCamera(const std::string & cameraName) +{ + SdfPath cameraPath("/World/" + cameraName); + UsdGeomCamera camera = UsdGeomCamera::Define(_stage, cameraPath); + + UsdAttribute projectionAttr = camera.GetProjectionAttr(); + projectionAttr.Set(UsdGeomTokens->perspective); + + UsdGeomXformable xformable(camera); + UsdGeomXformOp motion = xformable.MakeMatrixXform(); + GfMatrix4d identity(1.0); + motion.Set(identity); +} + +void UsdExporter::addFrame(const std::string & cameraName, const sfmData::CameraPose & pose, const camera::Pinhole & intrinsic, IndexT frameId) +{ + SdfPath cameraPath("/World/" + cameraName); + UsdGeomCamera camera = UsdGeomCamera::Get(_stage, cameraPath); + + _startTimeCode = std::min(_startTimeCode, frameId); + _endTimeCode = std::max(_endTimeCode, frameId); + + UsdAttribute focalLengthAttr = camera.GetFocalLengthAttr(); + UsdAttribute horizontalApertureAttr = camera.GetHorizontalApertureAttr(); + UsdAttribute verticalApertureAttr = camera.GetVerticalApertureAttr(); + UsdAttribute horizontalApertureOffsetAttr = camera.GetHorizontalApertureOffsetAttr(); + UsdAttribute verticalApertureOffsetAttr = camera.GetVerticalApertureOffsetAttr(); + + horizontalApertureAttr.Set(static_cast(intrinsic.sensorWidth())); + verticalApertureAttr.Set(static_cast(intrinsic.sensorHeight())); + + UsdTimeCode t(frameId); + double pixToMillimeters = intrinsic.sensorWidth() / intrinsic.w(); + + horizontalApertureOffsetAttr.Set(static_cast(intrinsic.getOffset().x() * pixToMillimeters), t); + verticalApertureOffsetAttr.Set(static_cast(intrinsic.getOffset().y() * pixToMillimeters), t); + focalLengthAttr.Set(static_cast(intrinsic.getFocalLength()), t); + + + + //Transform sfmData pose to usd pose + Eigen::Matrix4d glTransform = Eigen::Matrix4d::Identity(); + glTransform(1, 1) = -1.0; + glTransform(2, 2) = -1.0; + + // Inverse the pose and change the geometric frame + Eigen::Matrix4d camera_T_world = pose.getTransform().getHomogeneous(); + Eigen::Matrix4d world_T_camera = camera_T_world.inverse(); + Eigen::Matrix4d world_gl_T_camera_gl = glTransform * world_T_camera * glTransform; + + //Copy element by element while transposing + GfMatrix4d usdT; + for (int i = 0; i < 4; i++) + { + for (int j = 0; j < 4; j++) + { + usdT[j][i] = world_gl_T_camera_gl(i, j); + } + } + + //Assign pose to motion + UsdGeomXformable xformable(camera); + bool dummy = false; + std::vector xformOps = xformable.GetOrderedXformOps(&dummy); + + + if (!xformOps.empty()) { + UsdGeomXformOp motion = xformOps[0]; + motion.Set(usdT, t); + } +} + +} // namespace sfmDataIO +} // namespace aliceVision diff --git a/src/aliceVision/sfmDataIO/UsdExporter.hpp b/src/aliceVision/sfmDataIO/UsdExporter.hpp new file mode 100644 index 0000000000..64e024601b --- /dev/null +++ b/src/aliceVision/sfmDataIO/UsdExporter.hpp @@ -0,0 +1,34 @@ +// This file is part of the AliceVision project. +// Copyright (c) 2025 AliceVision contributors. +// This Source Code Form is subject to the terms of the Mozilla Public License, +// v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma once + +#include +#include +#include + +namespace aliceVision { +namespace sfmDataIO { + +class UsdExporter +{ +public: + UsdExporter(const std::string & filename, double frameRate); + + void createNewCamera(const std::string & cameraName); + + void addFrame(const std::string & cameraName, const sfmData::CameraPose & pose, const camera::Pinhole & camera, IndexT frameId); + + void terminate(); + +private: + pxr::UsdStageRefPtr _stage; + IndexT _startTimeCode; + IndexT _endTimeCode; +}; + +} // namespace sfmDataIO +} // namespace aliceVision diff --git a/src/software/export/CMakeLists.txt b/src/software/export/CMakeLists.txt index 5f01060a65..178f29c606 100644 --- a/src/software/export/CMakeLists.txt +++ b/src/software/export/CMakeLists.txt @@ -202,8 +202,8 @@ if (ALICEVISION_BUILD_SFM) # Export geometry and textures as USD if (ALICEVISION_HAVE_USD) - alicevision_add_software(aliceVision_exportUSD - SOURCE main_exportUSD.cpp + alicevision_add_software(aliceVision_exportMeshUSD + SOURCE main_exportMeshUSD.cpp FOLDER ${FOLDER_SOFTWARE_EXPORT} LINKS aliceVision_system aliceVision_cmdline @@ -214,6 +214,18 @@ if (ALICEVISION_BUILD_SFM) usdImaging usdShade ) + + alicevision_add_software(aliceVision_exportUSD + SOURCE main_exportUSD.cpp + FOLDER ${FOLDER_SOFTWARE_EXPORT} + LINKS aliceVision_system + aliceVision_cmdline + aliceVision_sfmData + aliceVision_sfmDataIO + Boost::program_options + Boost::boost + usd + ) endif() # Export distortion to be used in external tools diff --git a/src/software/export/main_exportMeshUSD.cpp b/src/software/export/main_exportMeshUSD.cpp new file mode 100644 index 0000000000..043f829d8d --- /dev/null +++ b/src/software/export/main_exportMeshUSD.cpp @@ -0,0 +1,374 @@ +// This file is part of the AliceVision project. +// Copyright (c) 2017 AliceVision contributors. +// This Source Code Form is subject to the terms of the Mozilla Public License, +// v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define ALICEVISION_SOFTWARE_VERSION_MAJOR 1 +#define ALICEVISION_SOFTWARE_VERSION_MINOR 0 + +using namespace aliceVision; + +namespace po = boost::program_options; +namespace fs = std::filesystem; + +PXR_NAMESPACE_USING_DIRECTIVE + +TF_DEFINE_PRIVATE_TOKENS(AvUsdTokens, + (bias)(colorSpace)(diffuseColor)(fallback)(file)(normal)(raw)(Raw)(result)(rgb)(scale)(sourceColorSpace)(st)(varname)); + +enum class EUSDFileType +{ + USDA, + USDC, + USDZ +}; + +std::string EUSDFileType_informations() noexcept +{ + return "USD file type :\n" + "*.usda\n" + "*.usdc\n" + "*.usdz"; +} + +EUSDFileType EUSDFileType_stringToEnum(const std::string& usdFileType) noexcept +{ + const std::string type = boost::to_lower_copy(usdFileType); + + if (type == "usda") + return EUSDFileType::USDA; + if (type == "usdc") + return EUSDFileType::USDC; + if (type == "usdz") + return EUSDFileType::USDZ; + + return EUSDFileType::USDA; +} + +std::string EUSDFileType_enumToString(const EUSDFileType usdFileType) noexcept +{ + switch (usdFileType) + { + case EUSDFileType::USDZ: + return "usdz"; + case EUSDFileType::USDC: + return "usdc"; + case EUSDFileType::USDA: + default: + return "usda"; + } +} + +std::ostream& operator<<(std::ostream& os, EUSDFileType usdFileType) { return os << EUSDFileType_enumToString(usdFileType); } + +std::istream& operator>>(std::istream& in, EUSDFileType& usdFileType) +{ + std::string token(std::istreambuf_iterator(in), {}); + usdFileType = EUSDFileType_stringToEnum(token); + return in; +} + +int aliceVision_main(int argc, char** argv) +{ + system::Timer timer; + + // command line parameters + std::string inputMeshPath; + std::string outputFolderPath; + EUSDFileType fileType = EUSDFileType::USDA; + + // clang-format off + po::options_description requiredParams("Required parameters"); + requiredParams.add_options() + ("input", po::value(&inputMeshPath), + "Input textured mesh to export.") + ("output", po::value(&outputFolderPath), + "Output folder for USD file and textures.") + ("fileType", po::value(&fileType)->default_value(fileType), + EUSDFileType_informations().c_str()); + // clang-format on + + CmdLine cmdline("The program converts a textured mesh to USD.\nAliceVision exportMeshUSD"); + cmdline.add(requiredParams); + if (!cmdline.execute(argc, argv)) + { + return EXIT_FAILURE; + } + + ALICEVISION_LOG_INFO("Loading " << inputMeshPath); + + // load input mesh and textures + mesh::Texturing texturing; + texturing.loadWithMaterial(inputMeshPath); + const mesh::Mesh* inputMesh = texturing.mesh; + + if (inputMesh == nullptr) + { + ALICEVISION_LOG_ERROR("Unable to read input mesh from the file: " << inputMeshPath); + return EXIT_FAILURE; + } + + ALICEVISION_LOG_TRACE("Creating USD stage"); + const std::string extension = fileType == EUSDFileType::USDC || fileType == EUSDFileType::USDZ ? "usdc" : "usda"; + const fs::path stagePath = fs::absolute(outputFolderPath) / ("texturedMesh." + extension); + UsdStageRefPtr stage = UsdStage::CreateNew(stagePath.string()); + if (!stage) + { + ALICEVISION_LOG_ERROR("Cannot create USD stage"); + return EXIT_FAILURE; + } + UsdGeomSetStageUpAxis(stage, UsdGeomTokens->y); + UsdGeomSetStageMetersPerUnit(stage, 0.01); + + // create mesh + ALICEVISION_LOG_TRACE("Creating USD Mesh"); + UsdGeomXform xform = UsdGeomXform::Define(stage, SdfPath("/root")); + UsdGeomMesh mesh = UsdGeomMesh::Define(stage, SdfPath("/root/mesh")); + stage->SetDefaultPrim(xform.GetPrim()); + + // write mesh properties + UsdAttribute doubleSided = mesh.CreateDoubleSidedAttr(); + doubleSided.Set(false); + + UsdAttribute subdSchema = mesh.CreateSubdivisionSchemeAttr(); + subdSchema.Set(UsdGeomTokens->none); + + // write points + ALICEVISION_LOG_TRACE("Creating USD Points"); + UsdAttribute points = mesh.CreatePointsAttr(); + + VtArray pointsData; + pointsData.resize(inputMesh->pts.size()); + + for (int i = 0; i < inputMesh->pts.size(); ++i) + { + const Point3d& point = inputMesh->pts[i]; + pointsData[i] = {static_cast(point.x), static_cast(-point.y), static_cast(-point.z)}; + } + + points.Set(pointsData); + + // write bounding box + ALICEVISION_LOG_TRACE("Creating USD Bounding box"); + const GfBBox3d bounds = mesh.ComputeLocalBound(UsdTimeCode::Default(), UsdGeomTokens->default_); + UsdAttribute extent = mesh.CreateExtentAttr(); + + const GfVec3d& bboxMin = bounds.GetRange().GetMin(); + const GfVec3d& bboxMax = bounds.GetRange().GetMax(); + VtArray extentData{{static_cast(bboxMin[0]), static_cast(bboxMin[1]), static_cast(bboxMin[2])}, + {static_cast(bboxMax[0]), static_cast(bboxMax[1]), static_cast(bboxMax[2])}}; + extent.Set(extentData); + + // write topology + ALICEVISION_LOG_TRACE("Creating USD Topology"); + UsdAttribute faceVertexCounts = mesh.CreateFaceVertexCountsAttr(); + VtArray faceVertexCountsData(inputMesh->tris.size(), 3); + faceVertexCounts.Set(faceVertexCountsData); + + UsdAttribute faceVertexIndices = mesh.CreateFaceVertexIndicesAttr(); + VtArray faceVertexIndicesData; + faceVertexIndicesData.resize(inputMesh->tris.size() * 3); + + for (int i = 0; i < inputMesh->tris.size(); ++i) + { + faceVertexIndicesData[i * 3] = inputMesh->tris[i].v[0]; + faceVertexIndicesData[i * 3 + 1] = inputMesh->tris[i].v[1]; + faceVertexIndicesData[i * 3 + 2] = inputMesh->tris[i].v[2]; + } + faceVertexIndices.Set(faceVertexIndicesData); + + // write face varying normals as primvar + ALICEVISION_LOG_TRACE("Creating USD Normals"); + if (!inputMesh->normals.empty() && !inputMesh->trisNormalsIds.empty()) + { + VtArray normalsData; + normalsData.resize(inputMesh->normals.size()); + + for (int i = 0; i < inputMesh->normals.size(); ++i) + { + const Point3d& normal = inputMesh->normals[i]; + normalsData[i] = {static_cast(normal.x), static_cast(-normal.y), static_cast(-normal.z)}; + } + + VtIntArray normalIndices; + normalIndices.resize(inputMesh->trisNormalsIds.size() * 3); + + for (int i = 0; i < inputMesh->trisNormalsIds.size(); ++i) + { + const Voxel& indices = inputMesh->trisNormalsIds[i]; + normalIndices[i * 3] = indices.x; + normalIndices[i * 3 + 1] = indices.y; + normalIndices[i * 3 + 2] = indices.z; + } + + UsdGeomPrimvarsAPI primvarsApi = UsdGeomPrimvarsAPI(mesh); + UsdGeomPrimvar uvs = primvarsApi.CreateIndexedPrimvar( + TfToken("normals"), SdfValueTypeNames->Normal3fArray, normalsData, normalIndices, UsdGeomTokens->faceVarying); + } + else // compute smooth vertex normals + { + StaticVector normals; + inputMesh->computeNormalsForPts(normals); + + VtArray normalsData; + normalsData.resize(normals.size()); + + for (int i = 0; i < normals.size(); ++i) + { + const Point3d& normal = normals[i]; + normalsData[i] = {static_cast(normal.x), static_cast(-normal.y), static_cast(-normal.z)}; + } + + UsdAttribute normalsAttr = mesh.CreateNormalsAttr(); + normalsAttr.Set(normalsData); + } + + // write UVs + ALICEVISION_LOG_TRACE("Creating USD UVs"); + if (!inputMesh->uvCoords.empty()) + { + VtArray uvsData; + uvsData.resize(inputMesh->uvCoords.size()); + + for (int i = 0; i < inputMesh->uvCoords.size(); ++i) + { + const Point2d& coord = inputMesh->uvCoords[i]; + uvsData[i] = {static_cast(coord.x), static_cast(coord.y)}; + } + + VtIntArray uvsIndices; + uvsIndices.resize(inputMesh->trisUvIds.size() * 3); + + for (int i = 0; i < inputMesh->trisUvIds.size(); ++i) + { + const Voxel& indices = inputMesh->trisUvIds[i]; + uvsIndices[i * 3] = indices.x; + uvsIndices[i * 3 + 1] = indices.y; + uvsIndices[i * 3 + 2] = indices.z; + } + + UsdGeomPrimvarsAPI primvarsApi = UsdGeomPrimvarsAPI(mesh); + UsdGeomPrimvar uvs = + primvarsApi.CreateIndexedPrimvar(TfToken("st"), SdfValueTypeNames->TexCoord2fArray, uvsData, uvsIndices, UsdGeomTokens->faceVarying); + } + + // create material and shaders + ALICEVISION_LOG_TRACE("Creating USD Material"); + UsdShadeMaterial material = UsdShadeMaterial::Define(stage, SdfPath("/root/mesh/mat")); + + UsdShadeShader preview = UsdShadeShader::Define(stage, SdfPath("/root/mesh/mat/preview")); + preview.CreateIdAttr(VtValue(UsdImagingTokens->UsdPreviewSurface)); + material.CreateSurfaceOutput().ConnectToSource(preview.ConnectableAPI(), UsdShadeTokens->surface); + + UsdShadeShader uvReader = UsdShadeShader::Define(stage, SdfPath("/root/mesh/mat/uvReader")); + uvReader.CreateIdAttr(VtValue(UsdImagingTokens->UsdPrimvarReader_float2)); + uvReader.CreateInput(AvUsdTokens->varname, SdfValueTypeNames->Token).Set(AvUsdTokens->st); + + // add textures (only supporting diffuse and normal maps) + if (texturing.material.hasTextures(mesh::Material::TextureType::DIFFUSE)) + { + ALICEVISION_LOG_TRACE("Creating Texture : Diffuse"); + SdfAssetPath diffuseTexturePath{texturing.material.textureName(mesh::Material::TextureType::DIFFUSE, -1)}; + UsdShadeShader diffuseTexture = UsdShadeShader::Define(stage, SdfPath("/root/mesh/mat/diffuseTexture")); + diffuseTexture.CreateIdAttr(VtValue(UsdImagingTokens->UsdUVTexture)); + diffuseTexture.CreateInput(AvUsdTokens->st, SdfValueTypeNames->Float2).ConnectToSource(uvReader.ConnectableAPI(), AvUsdTokens->result); + diffuseTexture.CreateInput(AvUsdTokens->file, SdfValueTypeNames->Asset).Set(diffuseTexturePath); + preview.CreateInput(AvUsdTokens->diffuseColor, SdfValueTypeNames->Color3f).ConnectToSource(diffuseTexture.ConnectableAPI(), AvUsdTokens->rgb); + } + + if (texturing.material.hasTextures(mesh::Material::TextureType::NORMAL)) + { + ALICEVISION_LOG_TRACE("Creating Texture : Normal"); + SdfAssetPath normalTexturePath{texturing.material.textureName(mesh::Material::TextureType::NORMAL, -1)}; + UsdShadeShader normalTexture = UsdShadeShader::Define(stage, SdfPath("/root/mesh/mat/normalTexture")); + normalTexture.CreateIdAttr(VtValue(UsdImagingTokens->UsdUVTexture)); + normalTexture.CreateInput(AvUsdTokens->st, SdfValueTypeNames->Float2).ConnectToSource(uvReader.ConnectableAPI(), AvUsdTokens->result); + UsdShadeInput file = normalTexture.CreateInput(AvUsdTokens->file, SdfValueTypeNames->Asset); + file.Set(normalTexturePath); + file.GetAttr().SetMetadata(AvUsdTokens->colorSpace, AvUsdTokens->Raw); + preview.CreateInput(AvUsdTokens->normal, SdfValueTypeNames->Normal3f).ConnectToSource(normalTexture.ConnectableAPI(), AvUsdTokens->rgb); + + normalTexture.CreateInput(AvUsdTokens->fallback, SdfValueTypeNames->Float4).Set(GfVec4f{0.5, 0.5, 0.5, 1.0}); + normalTexture.CreateInput(AvUsdTokens->scale, SdfValueTypeNames->Float4).Set(GfVec4f{2.0, 2.0, 2.0, 0.0}); + normalTexture.CreateInput(AvUsdTokens->bias, SdfValueTypeNames->Float4).Set(GfVec4f{-1.0, -1.0, -1.0, 1.0}); + normalTexture.CreateInput(AvUsdTokens->sourceColorSpace, SdfValueTypeNames->Token).Set(AvUsdTokens->raw); + } + + mesh.GetPrim().ApplyAPI(UsdShadeTokens->MaterialBindingAPI); + UsdShadeMaterialBindingAPI(mesh).Bind(material); + + stage->GetRootLayer()->Save(); + + // Copy textures to output folder + ALICEVISION_LOG_INFO("Copy textures to output folder"); + const fs::path sourceFolder = fs::path(inputMeshPath).parent_path(); + const fs::path destinationFolder = fs::canonical(outputFolderPath); + + for (int i = 0; i < texturing.material.numAtlases(); ++i) + { + for (const auto& texture : texturing.material.getAllTextures()) + { + if (utils::exists(sourceFolder / texture)) + { + fs::copy_file(sourceFolder / texture, destinationFolder / texture, fs::copy_options::update_existing); + } + } + } + + // write out usdz if requested + if (fileType == EUSDFileType::USDZ) + { + ALICEVISION_LOG_INFO("Create compressed file"); + const fs::path usdzPath = fs::canonical(outputFolderPath) / "texturedMesh.usdz"; + UsdZipFileWriter writer = UsdZipFileWriter::CreateNew(usdzPath.string()); + + if (!writer) + { + ALICEVISION_LOG_ERROR("Cannot create USDZ archive"); + return EXIT_FAILURE; + } + + writer.AddFile(stagePath.string(), "texturedMesh." + extension); + for (int i = 0; i < texturing.material.numAtlases(); ++i) + { + for (const auto& texture : texturing.material.getAllTextures()) + { + if (utils::exists(destinationFolder / texture)) + { + writer.AddFile((destinationFolder / texture).string(), texture); + } + } + } + writer.Save(); + } + + ALICEVISION_LOG_INFO("Task done in (s): " + std::to_string(timer.elapsed())); + return EXIT_SUCCESS; +} diff --git a/src/software/export/main_exportUSD.cpp b/src/software/export/main_exportUSD.cpp index 84c89f315e..093108f0d4 100644 --- a/src/software/export/main_exportUSD.cpp +++ b/src/software/export/main_exportUSD.cpp @@ -1,35 +1,22 @@ // This file is part of the AliceVision project. -// Copyright (c) 2017 AliceVision contributors. +// Copyright (c) 2025 AliceVision contributors. // This Source Code Form is subject to the terms of the Mozilla Public License, // v. 2.0. If a copy of the MPL was not distributed with this file, // You can obtain one at https://mozilla.org/MPL/2.0/. +#include #include -#include -#include #include #include -#include - -#include +#include +#include +#include +#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include +// These constants define the current software version. +// They must be updated when the command line is changed. #define ALICEVISION_SOFTWARE_VERSION_MAJOR 1 #define ALICEVISION_SOFTWARE_VERSION_MINOR 0 @@ -38,337 +25,134 @@ using namespace aliceVision; namespace po = boost::program_options; namespace fs = std::filesystem; -PXR_NAMESPACE_USING_DIRECTIVE - -TF_DEFINE_PRIVATE_TOKENS(AvUsdTokens, - (bias)(colorSpace)(diffuseColor)(fallback)(file)(normal)(raw)(Raw)(result)(rgb)(scale)(sourceColorSpace)(st)(varname)); - -enum class EUSDFileType -{ - USDA, - USDC, - USDZ -}; - -std::string EUSDFileType_informations() noexcept -{ - return "USD file type :\n" - "*.usda\n" - "*.usdc\n" - "*.usdz"; -} - -EUSDFileType EUSDFileType_stringToEnum(const std::string& usdFileType) noexcept -{ - const std::string type = boost::to_lower_copy(usdFileType); - - if (type == "usda") - return EUSDFileType::USDA; - if (type == "usdc") - return EUSDFileType::USDC; - if (type == "usdz") - return EUSDFileType::USDZ; - - return EUSDFileType::USDA; -} - -std::string EUSDFileType_enumToString(const EUSDFileType usdFileType) noexcept -{ - switch (usdFileType) - { - case EUSDFileType::USDZ: - return "usdz"; - case EUSDFileType::USDC: - return "usdc"; - case EUSDFileType::USDA: - default: - return "usda"; - } -} - -std::ostream& operator<<(std::ostream& os, EUSDFileType usdFileType) { return os << EUSDFileType_enumToString(usdFileType); } - -std::istream& operator>>(std::istream& in, EUSDFileType& usdFileType) -{ - std::string token(std::istreambuf_iterator(in), {}); - usdFileType = EUSDFileType_stringToEnum(token); - return in; -} int aliceVision_main(int argc, char** argv) { - system::Timer timer; + // Command-line parameters + std::string sfmDataFilename; + std::string outputFilename; - // command line parameters - std::string inputMeshPath; - std::string outputFolderPath; - EUSDFileType fileType = EUSDFileType::USDA; + // User optional parameters + double frameRate = 24.0; // clang-format off po::options_description requiredParams("Required parameters"); requiredParams.add_options() - ("input", po::value(&inputMeshPath), - "Input textured mesh to export.") - ("output", po::value(&outputFolderPath), - "Output folder for USD file and textures.") - ("fileType", po::value(&fileType)->default_value(fileType), - EUSDFileType_informations().c_str()); + ("input,i", po::value(&sfmDataFilename)->required(), + "SfMData file containing a complete SfM.") + ("output,o", po::value(&outputFilename)->required(), + "Output USD."); + + po::options_description optionalParams("Optional parameters"); + optionalParams.add_options() + ("frameRate", po::value(&frameRate)->default_value(frameRate), + "Define the camera's Frames per seconds."); // clang-format on - CmdLine cmdline("The program converts a textured mesh to USD.\nAliceVision exportUSD"); + CmdLine cmdline("AliceVision exportAnimatedCamera"); cmdline.add(requiredParams); + cmdline.add(optionalParams); if (!cmdline.execute(argc, argv)) { return EXIT_FAILURE; } - ALICEVISION_LOG_INFO("Loading " << inputMeshPath); - - // load input mesh and textures - mesh::Texturing texturing; - texturing.loadWithMaterial(inputMeshPath); - const mesh::Mesh* inputMesh = texturing.mesh; - - if (inputMesh == nullptr) + + // Load SfMData files + sfmData::SfMData sfmData; + if (!sfmDataIO::load(sfmData, sfmDataFilename, sfmDataIO::ESfMData::ALL)) { - ALICEVISION_LOG_ERROR("Unable to read input mesh from the file: " << inputMeshPath); + ALICEVISION_LOG_ERROR("The input SfMData file '" << sfmDataFilename << "' cannot be read."); return EXIT_FAILURE; } - ALICEVISION_LOG_TRACE("Creating USD stage"); - const std::string extension = fileType == EUSDFileType::USDC || fileType == EUSDFileType::USDZ ? "usdc" : "usda"; - const fs::path stagePath = fs::absolute(outputFolderPath) / ("texturedMesh." + extension); - UsdStageRefPtr stage = UsdStage::CreateNew(stagePath.string()); - if (!stage) + if (sfmData.getViews().empty()) { - ALICEVISION_LOG_ERROR("Cannot create USD stage"); + ALICEVISION_LOG_ERROR("The input SfMData file '" << sfmDataFilename << "' is empty."); return EXIT_FAILURE; } - UsdGeomSetStageUpAxis(stage, UsdGeomTokens->y); - UsdGeomSetStageMetersPerUnit(stage, 0.01); - - // create mesh - ALICEVISION_LOG_TRACE("Creating USD Mesh"); - UsdGeomXform xform = UsdGeomXform::Define(stage, SdfPath("/root")); - UsdGeomMesh mesh = UsdGeomMesh::Define(stage, SdfPath("/root/mesh")); - stage->SetDefaultPrim(xform.GetPrim()); - - // write mesh properties - UsdAttribute doubleSided = mesh.CreateDoubleSidedAttr(); - doubleSided.Set(false); - - UsdAttribute subdSchema = mesh.CreateSubdivisionSchemeAttr(); - subdSchema.Set(UsdGeomTokens->none); - - // write points - ALICEVISION_LOG_TRACE("Creating USD Points"); - UsdAttribute points = mesh.CreatePointsAttr(); - - VtArray pointsData; - pointsData.resize(inputMesh->pts.size()); - - for (int i = 0; i < inputMesh->pts.size(); ++i) - { - const Point3d& point = inputMesh->pts[i]; - pointsData[i] = {static_cast(point.x), static_cast(-point.y), static_cast(-point.z)}; - } - - points.Set(pointsData); - - // write bounding box - ALICEVISION_LOG_TRACE("Creating USD Bounding box"); - const GfBBox3d bounds = mesh.ComputeLocalBound(UsdTimeCode::Default(), UsdGeomTokens->default_); - UsdAttribute extent = mesh.CreateExtentAttr(); - - const GfVec3d& bboxMin = bounds.GetRange().GetMin(); - const GfVec3d& bboxMax = bounds.GetRange().GetMax(); - VtArray extentData{{static_cast(bboxMin[0]), static_cast(bboxMin[1]), static_cast(bboxMin[2])}, - {static_cast(bboxMax[0]), static_cast(bboxMax[1]), static_cast(bboxMax[2])}}; - extent.Set(extentData); - // write topology - ALICEVISION_LOG_TRACE("Creating USD Topology"); - UsdAttribute faceVertexCounts = mesh.CreateFaceVertexCountsAttr(); - VtArray faceVertexCountsData(inputMesh->tris.size(), 3); - faceVertexCounts.Set(faceVertexCountsData); + using SequenceGroup = std::map>; + using IntrinsicGroup = std::map; - UsdAttribute faceVertexIndices = mesh.CreateFaceVertexIndicesAttr(); - VtArray faceVertexIndicesData; - faceVertexIndicesData.resize(inputMesh->tris.size() * 3); - - for (int i = 0; i < inputMesh->tris.size(); ++i) + //Organize views among intrinsics + IntrinsicGroup groups; + for (const auto& [viewId, view] : sfmData.getViews()) { - faceVertexIndicesData[i * 3] = inputMesh->tris[i].v[0]; - faceVertexIndicesData[i * 3 + 1] = inputMesh->tris[i].v[1]; - faceVertexIndicesData[i * 3 + 2] = inputMesh->tris[i].v[2]; - } - faceVertexIndices.Set(faceVertexIndicesData); - - // write face varying normals as primvar - ALICEVISION_LOG_TRACE("Creating USD Normals"); - if (!inputMesh->normals.empty() && !inputMesh->trisNormalsIds.empty()) - { - VtArray normalsData; - normalsData.resize(inputMesh->normals.size()); - - for (int i = 0; i < inputMesh->normals.size(); ++i) + if (!sfmData.isPoseAndIntrinsicDefined(viewId)) { - const Point3d& normal = inputMesh->normals[i]; - normalsData[i] = {static_cast(normal.x), static_cast(-normal.y), static_cast(-normal.z)}; + continue; } - VtIntArray normalIndices; - normalIndices.resize(inputMesh->trisNormalsIds.size() * 3); - - for (int i = 0; i < inputMesh->trisNormalsIds.size(); ++i) + if (view->isPartOfRig()) { - const Voxel& indices = inputMesh->trisNormalsIds[i]; - normalIndices[i * 3] = indices.x; - normalIndices[i * 3 + 1] = indices.y; - normalIndices[i * 3 + 2] = indices.z; - } + ALICEVISION_LOG_ERROR("Rigs are not supported"); + return EXIT_FAILURE; + } - UsdGeomPrimvarsAPI primvarsApi = UsdGeomPrimvarsAPI(mesh); - UsdGeomPrimvar uvs = primvarsApi.CreateIndexedPrimvar( - TfToken("normals"), SdfValueTypeNames->Normal3fArray, normalsData, normalIndices, UsdGeomTokens->faceVarying); + groups[view->getIntrinsicId()][view->getFrameId()].push_back(view); } - else // compute smooth vertex normals - { - StaticVector normals; - inputMesh->computeNormalsForPts(normals); - - VtArray normalsData; - normalsData.resize(normals.size()); - for (int i = 0; i < normals.size(); ++i) - { - const Point3d& normal = normals[i]; - normalsData[i] = {static_cast(normal.x), static_cast(-normal.y), static_cast(-normal.z)}; - } + sfmDataIO::UsdExporter exporter(outputFilename, frameRate); - UsdAttribute normalsAttr = mesh.CreateNormalsAttr(); - normalsAttr.Set(normalsData); - } - - // write UVs - ALICEVISION_LOG_TRACE("Creating USD UVs"); - if (!inputMesh->uvCoords.empty()) + for (const auto & [idIntrinsic, group] : groups) { - VtArray uvsData; - uvsData.resize(inputMesh->uvCoords.size()); + bool isSequence = true; + const camera::IntrinsicBase & intrinsic = *(sfmData.getIntrinsics().at(idIntrinsic)); - for (int i = 0; i < inputMesh->uvCoords.size(); ++i) + if (!camera::isPinhole(intrinsic.getType())) { - const Point2d& coord = inputMesh->uvCoords[i]; - uvsData[i] = {static_cast(coord.x), static_cast(coord.y)}; + ALICEVISION_LOG_INFO("Ignoring non pinhole intrinsic " << idIntrinsic << " and all associated views"); + continue; } - VtIntArray uvsIndices; - uvsIndices.resize(inputMesh->trisUvIds.size() * 3); + const camera::Pinhole & pinhole = dynamic_cast(intrinsic); - for (int i = 0; i < inputMesh->trisUvIds.size(); ++i) + //Check that we don't have multiple view per frame + for (const auto & [frameId, vector] : group) { - const Voxel& indices = inputMesh->trisUvIds[i]; - uvsIndices[i * 3] = indices.x; - uvsIndices[i * 3 + 1] = indices.y; - uvsIndices[i * 3 + 2] = indices.z; + if (vector.size() > 1) + { + isSequence = false; + } } - UsdGeomPrimvarsAPI primvarsApi = UsdGeomPrimvarsAPI(mesh); - UsdGeomPrimvar uvs = - primvarsApi.CreateIndexedPrimvar(TfToken("st"), SdfValueTypeNames->TexCoord2fArray, uvsData, uvsIndices, UsdGeomTokens->faceVarying); - } - - // create material and shaders - ALICEVISION_LOG_TRACE("Creating USD Material"); - UsdShadeMaterial material = UsdShadeMaterial::Define(stage, SdfPath("/root/mesh/mat")); - - UsdShadeShader preview = UsdShadeShader::Define(stage, SdfPath("/root/mesh/mat/preview")); - preview.CreateIdAttr(VtValue(UsdImagingTokens->UsdPreviewSurface)); - material.CreateSurfaceOutput().ConnectToSource(preview.ConnectableAPI(), UsdShadeTokens->surface); - - UsdShadeShader uvReader = UsdShadeShader::Define(stage, SdfPath("/root/mesh/mat/uvReader")); - uvReader.CreateIdAttr(VtValue(UsdImagingTokens->UsdPrimvarReader_float2)); - uvReader.CreateInput(AvUsdTokens->varname, SdfValueTypeNames->Token).Set(AvUsdTokens->st); - - // add textures (only supporting diffuse and normal maps) - if (texturing.material.hasTextures(mesh::Material::TextureType::DIFFUSE)) - { - ALICEVISION_LOG_TRACE("Creating Texture : Diffuse"); - SdfAssetPath diffuseTexturePath{texturing.material.textureName(mesh::Material::TextureType::DIFFUSE, -1)}; - UsdShadeShader diffuseTexture = UsdShadeShader::Define(stage, SdfPath("/root/mesh/mat/diffuseTexture")); - diffuseTexture.CreateIdAttr(VtValue(UsdImagingTokens->UsdUVTexture)); - diffuseTexture.CreateInput(AvUsdTokens->st, SdfValueTypeNames->Float2).ConnectToSource(uvReader.ConnectableAPI(), AvUsdTokens->result); - diffuseTexture.CreateInput(AvUsdTokens->file, SdfValueTypeNames->Asset).Set(diffuseTexturePath); - preview.CreateInput(AvUsdTokens->diffuseColor, SdfValueTypeNames->Color3f).ConnectToSource(diffuseTexture.ConnectableAPI(), AvUsdTokens->rgb); - } - - if (texturing.material.hasTextures(mesh::Material::TextureType::NORMAL)) - { - ALICEVISION_LOG_TRACE("Creating Texture : Normal"); - SdfAssetPath normalTexturePath{texturing.material.textureName(mesh::Material::TextureType::NORMAL, -1)}; - UsdShadeShader normalTexture = UsdShadeShader::Define(stage, SdfPath("/root/mesh/mat/normalTexture")); - normalTexture.CreateIdAttr(VtValue(UsdImagingTokens->UsdUVTexture)); - normalTexture.CreateInput(AvUsdTokens->st, SdfValueTypeNames->Float2).ConnectToSource(uvReader.ConnectableAPI(), AvUsdTokens->result); - UsdShadeInput file = normalTexture.CreateInput(AvUsdTokens->file, SdfValueTypeNames->Asset); - file.Set(normalTexturePath); - file.GetAttr().SetMetadata(AvUsdTokens->colorSpace, AvUsdTokens->Raw); - preview.CreateInput(AvUsdTokens->normal, SdfValueTypeNames->Normal3f).ConnectToSource(normalTexture.ConnectableAPI(), AvUsdTokens->rgb); - - normalTexture.CreateInput(AvUsdTokens->fallback, SdfValueTypeNames->Float4).Set(GfVec4f{0.5, 0.5, 0.5, 1.0}); - normalTexture.CreateInput(AvUsdTokens->scale, SdfValueTypeNames->Float4).Set(GfVec4f{2.0, 2.0, 2.0, 0.0}); - normalTexture.CreateInput(AvUsdTokens->bias, SdfValueTypeNames->Float4).Set(GfVec4f{-1.0, -1.0, -1.0, 1.0}); - normalTexture.CreateInput(AvUsdTokens->sourceColorSpace, SdfValueTypeNames->Token).Set(AvUsdTokens->raw); - } - - mesh.GetPrim().ApplyAPI(UsdShadeTokens->MaterialBindingAPI); - UsdShadeMaterialBindingAPI(mesh).Bind(material); - - stage->GetRootLayer()->Save(); - - // Copy textures to output folder - ALICEVISION_LOG_INFO("Copy textures to output folder"); - const fs::path sourceFolder = fs::path(inputMeshPath).parent_path(); - const fs::path destinationFolder = fs::canonical(outputFolderPath); - - for (int i = 0; i < texturing.material.numAtlases(); ++i) - { - for (const auto& texture : texturing.material.getAllTextures()) + if (!isSequence) { - if (utils::exists(sourceFolder / texture)) + ALICEVISION_LOG_INFO("Exporting non sequence"); + for (const auto & [frameId, vector] : group) { - fs::copy_file(sourceFolder / texture, destinationFolder / texture, fs::copy_options::update_existing); + for (const auto & view: vector) + { + const std::string cameraName = "cam_" + std::to_string(view->getViewId()); + const sfmData::CameraPose & cp = sfmData.getPose(*view); + exporter.createNewCamera(cameraName); + exporter.addFrame(cameraName, cp, pinhole, 0); + } } - } - } - - // write out usdz if requested - if (fileType == EUSDFileType::USDZ) - { - ALICEVISION_LOG_INFO("Create compressed file"); - const fs::path usdzPath = fs::canonical(outputFolderPath) / "texturedMesh.usdz"; - UsdZipFileWriter writer = UsdZipFileWriter::CreateNew(usdzPath.string()); - - if (!writer) + } + else { - ALICEVISION_LOG_ERROR("Cannot create USDZ archive"); - return EXIT_FAILURE; - } + + const std::string cameraName = "cam_" + std::to_string(idIntrinsic); + ALICEVISION_LOG_INFO("Exporting sequence " << cameraName); + exporter.createNewCamera(cameraName); - writer.AddFile(stagePath.string(), "texturedMesh." + extension); - for (int i = 0; i < texturing.material.numAtlases(); ++i) - { - for (const auto& texture : texturing.material.getAllTextures()) + for (const auto & [frameId, vector] : group) { - if (utils::exists(destinationFolder / texture)) + + for (const auto & view: vector) { - writer.AddFile((destinationFolder / texture).string(), texture); + + const sfmData::CameraPose & cp = sfmData.getPose(*view); + + exporter.addFrame(cameraName, cp, pinhole, frameId); } } - } - writer.Save(); + } } - ALICEVISION_LOG_INFO("Task done in (s): " + std::to_string(timer.elapsed())); + exporter.terminate(); + return EXIT_SUCCESS; }