@@ -21,6 +21,10 @@ governing permissions and limitations under the License.
2121using namespace PXR_NS ;
2222namespace adobe ::usd {
2323
24+ // Shininess max value derived from Cosine Power
25+ // https://help.autodesk.com/view/MAYAUL/2025/ENU/?guid=GUID-3EDEB1B3-4E48-485A-9714-9998F6E4944D
26+ static constexpr float MAX_FBX_SHININESS = 100 .f;
27+
2428struct ExportFbxContext
2529{
2630 UsdData* usd = nullptr ;
@@ -259,28 +263,25 @@ exportFbxTransform(ExportFbxContext& ctx, const Node& node, FbxNode* fbxNode)
259263 // Helper function calculates the transformation matrix, only to be called if needed
260264 auto getTransformationMatrix = [node]() {
261265 FbxAMatrix localTransform = GetFBXMatrixFromUSD (node.transform );
262- FbxAMatrix AdditionalRotation {};
266+ FbxAMatrix additionalRotation {};
263267
264- // If the node contains a camera, we need to apply the reverse Y axis rotation to orient
265- // the camera to look down the -X axis (see importFbxCamera) which is the default for fbx.
268+ // Account for FBX's different coordinate system, and take the inverse on import. See
269+ // comment at definition of CAMERA_ROTATION_OFFSET_EXPORT for more information
266270 if (node.camera >= 0 ) {
267271 TF_DEBUG_MSG (
268272 FILE_FORMAT_FBX,
269273 " exportFbxTransform: Applying 90 degree rotation around Y axis to camera node\n " );
270- FbxVector4 rotation = FbxVector4 (0 .0f , 90 .f , 0 .0f );
271- AdditionalRotation.SetR (rotation);
274+ additionalRotation.SetR (CAMERA_ROTATION_OFFSET_EXPORT);
272275 }
273- // A similar rotation was expected to be needed for exporting lights. In practice, however,
274- // a 90 degree rotation around the X axis results in the correct camera orientation. The
275- // reasons for this need to be investigated further
276+ // Account for FBX's different coordinate system, and take the inverse on import. See
277+ // comment at definition of LIGHT_ROTATION_OFFSET_EXPORT for more information
276278 if (node.light >= 0 ) {
277279 TF_DEBUG_MSG (
278280 FILE_FORMAT_FBX,
279281 " exportFbxTransform: Applying 90 degree rotation around X axis to light node\n " );
280- FbxVector4 rotation = FbxVector4 (90 .0f , 0 .f , 0 .0f );
281- AdditionalRotation.SetR (rotation);
282+ additionalRotation.SetR (LIGHT_ROTATION_OFFSET_EXPORT);
282283 }
283- return localTransform * AdditionalRotation ;
284+ return localTransform * additionalRotation ;
284285 };
285286
286287 // Translation
@@ -295,6 +296,11 @@ exportFbxTransform(ExportFbxContext& ctx, const Node& node, FbxNode* fbxNode)
295296
296297 } else {
297298 // Copy the translation value from the USD node
299+
300+ // This code path will likely never be run, since LayerRead currently always converts to
301+ // matrix transformations (with getLocalTransformation). If that is changed, this should
302+ // handle alternate situations
303+
298304 fbxNode->LclTranslation .Set (
299305 FbxDouble3 (node.translation [0 ], node.translation [1 ], node.translation [2 ]));
300306 }
@@ -342,6 +348,33 @@ exportFbxTransform(ExportFbxContext& ctx, const Node& node, FbxNode* fbxNode)
342348 } else {
343349 // Convert the USD node's quaternion to Euler angles and use the resulting value
344350 FbxQuaternion fbxQuat = GetFBXQuat (node.rotation );
351+
352+ // This code path will likely never be run, since LayerRead currently always converts to
353+ // matrix transformations (with getLocalTransformation). If that is changed, this should
354+ // handle alternate situations, although the camera and light transformations have not
355+ // been properly tested
356+
357+ if (node.camera >= 0 ) {
358+ FbxQuaternion additionalQuat;
359+
360+ TF_DEBUG_MSG (
361+ FILE_FORMAT_FBX,
362+ " exportFbxTransform: Applying 90 degree rotation around Y axis to camera node\n " );
363+ FbxVector4 rotation (CAMERA_ROTATION_OFFSET_EXPORT);
364+ additionalQuat.ComposeSphericalXYZ (rotation);
365+ fbxQuat = fbxQuat * additionalQuat;
366+ }
367+ if (node.light >= 0 ) {
368+ FbxQuaternion additionalQuat;
369+
370+ TF_DEBUG_MSG (
371+ FILE_FORMAT_FBX,
372+ " exportFbxTransform: Applying 90 degree rotation around X axis to light node\n " );
373+ FbxVector4 rotation (LIGHT_ROTATION_OFFSET_EXPORT);
374+ additionalQuat.ComposeSphericalXYZ (rotation);
375+ fbxQuat = fbxQuat * additionalQuat;
376+ }
377+
345378 FbxVector4 euler;
346379 euler.SetXYZ (fbxQuat);
347380 fbxNode->LclRotation .Set (FbxDouble3 (euler[0 ], euler[1 ], euler[2 ]));
@@ -384,6 +417,10 @@ exportFbxTransform(ExportFbxContext& ctx, const Node& node, FbxNode* fbxNode)
384417 if (node.hasTransform ) {
385418 // Extract the scale from the transformation matrix
386419
420+ // This code path will likely never be run, since LayerRead currently always converts to
421+ // matrix transformations (with getLocalTransformation). If that is changed, this should
422+ // handle alternate situations
423+
387424 if (!transformation) {
388425 transformation = getTransformationMatrix ();
389426 }
@@ -647,6 +684,8 @@ exportFbxCameras(ExportFbxContext& ctx)
647684 FbxCamera::EProjectionType p = c.projection == GfCamera::Projection::Perspective
648685 ? FbxCamera::EProjectionType::ePerspective
649686 : FbxCamera::EProjectionType::eOrthogonal;
687+
688+ fbxCamera->SetName (c.name .c_str ());
650689 fbxCamera->ProjectionType .Set (p);
651690 fbxCamera->FocalLength .Set (c.f );
652691 fbxCamera->FieldOfView .Set (c.fov );
@@ -850,6 +889,74 @@ exportFbxInput(ExportFbxContext& ctx,
850889 return false ;
851890}
852891
892+ // If metallic is present do the following mapping
893+ // 1. Disable diffuse color
894+ // 2. Specular color is the old diffuse color
895+ // 3. Set shininess which was calculated from roughness
896+ bool
897+ exportMetallicInput (ExportFbxContext& ctx,
898+ const InputTranslator& inputTranslator,
899+ const Input& input,
900+ float shininess,
901+ FbxSurfacePhong* phong)
902+ {
903+ bool setDiffuseToZero = false ;
904+ if (input.image >= 0 ) {
905+ setDiffuseToZero = true ;
906+ }
907+ if (!setDiffuseToZero && !input.value .IsEmpty () && input.value .IsHolding <float >()) {
908+ float metallic = input.value .UncheckedGet <float >();
909+ if (metallic > 0 .0f ) {
910+ setDiffuseToZero = true ;
911+ }
912+ }
913+ if (setDiffuseToZero) {
914+ FbxFileTexture* diffuseTexture =
915+ FbxCast<FbxFileTexture>(phong->Diffuse .GetSrcObject <FbxFileTexture>());
916+ if (diffuseTexture) {
917+ phong->Specular .ConnectSrcObject (diffuseTexture);
918+ phong->Diffuse .DisconnectSrcObject (diffuseTexture);
919+ } else {
920+ FbxDouble3 oldBaseColor = phong->Diffuse .Get ();
921+ phong->Specular .Set (oldBaseColor);
922+ }
923+ phong->Diffuse .Set (FbxDouble3 (0.0 , 0.0 , 0.0 ));
924+ phong->DiffuseFactor .Set (0.0 );
925+ if (shininess > 0 .0f ) {
926+ phong->Shininess .Set (shininess);
927+ }
928+ }
929+
930+ exportFbxInput (ctx,
931+ inputTranslator,
932+ input,
933+ phong->ReflectionFactor ,
934+ FbxTexture::eStandard,
935+ AdobeTokens->raw );
936+ return setDiffuseToZero;
937+ }
938+
939+ // Roughness is the inverse of shininess, so we need to convert the roughness value to shininess
940+ // If roughness is an image texture it follows the old workflow of mapping to SpecularFactor
941+ // As this Phong workflow will be replaced by the Autodesk standard surface in the future.
942+ float
943+ exportRoughnessInput (ExportFbxContext& ctx,
944+ const InputTranslator& inputTranslator,
945+ const Input& input,
946+ FbxSurfacePhong* phong)
947+ {
948+ float shininess = -1 .0f ;
949+ if (input.image >= 0 ) {
950+ exportFbxInput (ctx, inputTranslator, input, phong->SpecularFactor , FbxTexture::eStandard);
951+ } else if (!input.value .IsEmpty () && input.value .IsHolding <float >()) {
952+ float roughness = input.value .UncheckedGet <float >();
953+ shininess = (1 .0f - roughness) * MAX_FBX_SHININESS;
954+ phong->Shininess .Set (shininess);
955+ return true ;
956+ }
957+ return shininess;
958+ }
959+
853960void
854961exportFbxMaterials (ExportFbxContext& ctx)
855962{
@@ -872,9 +979,9 @@ exportFbxMaterials(ExportFbxContext& ctx)
872979 inputTranslator.translateOpacity2Transparency (m.opacity , transparency);
873980 inputTranslator.translateDirect (m.normal , normal);
874981 inputTranslator.translateDirect (m.emissiveColor , emissiveColor);
875- // Convert Input data for occlusion, metallic and roughness to single channel textures (if
876- // necessary). This is done so that there is consistency on which channel to reference when
877- // importing.
982+ // Convert Input data for occlusion, metallic and roughness to single channel textures
983+ // (if necessary). This is done so that there is consistency on which channel to
984+ // reference when importing.
878985 inputTranslator.translateToSingle (" occlusion" , m.occlusion , occlusion);
879986 inputTranslator.translateToSingle (" metallic" , m.metallic , metallic);
880987 inputTranslator.translateToSingle (" roughness" , m.roughness , roughness);
@@ -895,22 +1002,13 @@ exportFbxMaterials(ExportFbxContext& ctx)
8951002 ctx, inputTranslator, occlusion, phong->AmbientFactor , FbxTexture::eStandard);
8961003 exportFbxInput (
8971004 ctx, inputTranslator, transparency, phong->TransparencyFactor , FbxTexture::eStandard);
898- exportFbxInput (
899- ctx, inputTranslator, metallic, phong-> ReflectionFactor , FbxTexture::eStandard);
900- exportFbxInput (
901- ctx, inputTranslator, roughness, phong-> SpecularFactor , FbxTexture::eStandard );
1005+
1006+ // if we have a shininess value, pass it on for the metallic workflow
1007+ float shininess = exportRoughnessInput (ctx, inputTranslator, roughness, phong);
1008+ exportMetallicInput ( ctx, inputTranslator, metallic, shininess, phong );
9021009 if (transparency.image >= 0 || !transparency.value .IsEmpty ()) {
9031010 phong->TransparentColor .Set (FbxDouble3 (1 ));
9041011 }
905- // phong->TransparencyFactor.Set(.5);
906- // TODO specularity is not achieved correctly. Probably roughness conversion to shininess is
907- // wrong. exportFbxInput(ctx, m.clearcoat, phong->Reflection, FbxTexture::eStandard);
908- // exportFbxInput(ctx, m.roughness, phong->Shininess, FbxTexture::eStandard);
909- // exportFbxInput(ctx, m.displacement, phong->DisplacementColor, FbxTexture::eStandard);
910- // if (m.useSpecularWorkflow.value)
911- // exportFbxInput(ctx, m.specularColor, phong->Specular, FbxTexture::eStandard);
912- // else
913- // exportFbxInput(ctx, m.metallic, phong->Specular, FbxTexture::eStandard);
9141012 }
9151013 ctx.fbx ->images = inputTranslator.getImages ();
9161014}
@@ -951,7 +1049,7 @@ exportSkeletons(ExportFbxContext& ctx)
9511049 fbxNode->LclTranslation = fbxMatrix.GetT ();
9521050 fbxNode->LclScaling = fbxMatrix.GetS ();
9531051
954- int parent = skeleton.parents [j];
1052+ int parent = skeleton.jointParents [j];
9551053 if (parent >= 0 ) {
9561054 std::string parentJoint = skeleton.joints [parent].GetString ();
9571055 FbxNode* parentNode = skeletonNodesMap[parentJoint];
@@ -1012,7 +1110,7 @@ exportSkeletons(ExportFbxContext& ctx)
10121110 }
10131111
10141112 for (size_t j = 0 ; j < skeleton.animations .size (); j++) {
1015- Animation & anim = ctx.usd ->animations [j];
1113+ SkeletonAnimation & anim = ctx.usd ->skeletonAnimations [j];
10161114 if (!ctx.animStack ) {
10171115 // TODO: Use anim.name.c_str() when implementing multi-track animation
10181116 ctx.animStack = FbxAnimStack::Create (ctx.fbx ->scene , " AnimStack" );
@@ -1039,7 +1137,8 @@ exportSkeletons(ExportFbxContext& ctx)
10391137 sNode ->CreateCurve (sNode ->GetName (), 2U )->KeyModifyBegin ();
10401138 }
10411139
1042- // We need to convert from timeCodesPerSecond to seconds so be compute the multiplier.
1140+ // We need to convert from timeCodesPerSecond to seconds so be compute the
1141+ // multiplier.
10431142 double secondsPerTimeCode =
10441143 ctx.usd ->timeCodesPerSecond != 0.0 ? 1.0 / ctx.usd ->timeCodesPerSecond : 1.0 ;
10451144 for (size_t t = 0 ; t < anim.times .size (); t++) {
0 commit comments