Skip to content

Commit 505b11e

Browse files
author
Keith Blackstone
authored
Merge pull request #36 from adobe/v1.0.8
V1.0.8
2 parents f25b3a5 + 5ee979a commit 505b11e

35 files changed

+1877
-425
lines changed

changelog.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
v1.0.8 October 1st, 2024
2+
fbx:
3+
- fix embedded image export
4+
- improved metallic workflow
5+
- fix camera import orientation
6+
gltf:
7+
- export fix - token2channel default rgb tokens to use r channel
8+
- export alphablendmodetest test asset without causing missing texture error
9+
- propagate one animation track through import/export
10+
- anisotropy import and export
11+
ply:
12+
- fix gsplat bounding box for framing into the asset
13+
utility:
14+
- Write a USDRenderSettings prim in the Sdf layer & do not finalize the layer in writeLayer()
15+
116
v1.0.7 August 30th, 2024
217
fbx:
318
- improved error handling in readFbx

fbx/src/fbx.cpp

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -267,23 +267,60 @@ readFbx(Fbx& fbx, const std::string& filename, bool onlyMaterials)
267267
return true;
268268
}
269269

270+
std::string
271+
extractFileName(const char* pFileName)
272+
{
273+
std::string fileName(pFileName);
274+
std::replace(fileName.begin(),
275+
fileName.end(),
276+
'\\',
277+
'/'); // Normalize slashes for cross-platform consistency
278+
size_t lastSlash = fileName.find_last_of('/');
279+
if (lastSlash != std::string::npos) {
280+
fileName = fileName.substr(lastSlash + 1);
281+
}
282+
return fileName;
283+
}
284+
285+
bool
286+
populateFileBufferAndSize(Fbx* fbx,
287+
const char* pFileName,
288+
const void**& pFileBuffer,
289+
size_t*& pSizeInBytes)
290+
{
291+
for (const ImageAsset& image : fbx->images) {
292+
if (image.uri == pFileName) {
293+
*pFileBuffer = image.image.data();
294+
*pSizeInBytes = image.image.size();
295+
if (pFileBuffer && *pSizeInBytes > 0) {
296+
return true;
297+
}
298+
break;
299+
}
300+
}
301+
return false;
302+
}
303+
270304
FbxCallback::State
271305
EmbedWriteCBFunction(void* pUserData,
272306
FbxClassId pDataHint,
273307
const char* pFileName,
274308
const void** pFileBuffer,
275309
size_t* pSizeInBytes)
276310
{
277-
if (!pUserData || !pFileName || !pFileBuffer || !pSizeInBytes || *pSizeInBytes < 1) {
311+
if (!pUserData || !pFileName) {
278312
return FbxCallback::State::eNotHandled;
279313
}
280314

281315
Fbx* fbx = reinterpret_cast<Fbx*>(pUserData);
282316
TF_DEBUG_MSG(FILE_FORMAT_FBX, "EmbedWriteCBFunction: %s\n", pFileName);
283-
for (const ImageAsset& image : fbx->images) {
284-
if (image.uri == pFileName) {
285-
*pFileBuffer = image.image.data();
286-
*pSizeInBytes = image.image.size();
317+
if (populateFileBufferAndSize(fbx, pFileName, pFileBuffer, pSizeInBytes)) {
318+
return FbxCallback::State::eHandled;
319+
} else {
320+
// Embedded pFilename's do not have the exportParentPath or a filepath, so we need to
321+
// extract the file name
322+
std::string fileName = extractFileName(pFileName);
323+
if (populateFileBufferAndSize(fbx, fileName.c_str(), pFileBuffer, pSizeInBytes)) {
287324
return FbxCallback::State::eHandled;
288325
}
289326
}

fbx/src/fbx.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@ namespace adobe::usd {
3737
// lighting doesn't match
3838
constexpr float FBX_TO_USD_INTENSITY_SCALE_FACTOR = 1.0;
3939

40+
// Camera rotation to apply to revert to FBX coordinates, on export. Inspired by the Blender code
41+
// base, which converts from -Z to +X with a 90º rotation around the Y axis:
42+
// https://github.com/blender/blender/blob/e1a44ad129d53fbd47215845be2c42fb0850135d/scripts/addons_core/io_scene_fbx/fbx_utils.py#L74C64-L74C88
43+
inline FbxDouble3 CAMERA_ROTATION_OFFSET_EXPORT(0.0f, 90.f, 0.f);
44+
45+
// Light rotation to apply to revert to FBX coordinates, on export. Inspired by the Blender code
46+
// base, which converts from -Z to -Y with a 90º rotation around the X axis:
47+
// https://github.com/blender/blender/blob/e1a44ad129d53fbd47215845be2c42fb0850135d/scripts/addons_core/io_scene_fbx/fbx_utils.py#L73C63-L73C87
48+
inline FbxDouble3 LIGHT_ROTATION_OFFSET_EXPORT(90.f, 0.f, 0.f);
49+
4050
struct ExportFbxOptions
4151
{
4252
bool embedImages = false;

fbx/src/fbxExport.cpp

Lines changed: 129 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ governing permissions and limitations under the License.
2121
using namespace PXR_NS;
2222
namespace 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+
2428
struct 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+
853960
void
854961
exportFbxMaterials(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

Comments
 (0)