I exported a model to fbx format inside 3ds max. Inside Max the scene look like this:
When i import it in my software i get this:
https://youtu.be/WPQnkxzL24g
The meshes transform is wrong. But if I import the max FBX file inside Blender then i export from there I get it right:
https://youtu.be/LBk9CTjSwlo
Here how a model is loaded:
void BaseModel::loadAssimpModel(const std::wstring& path)
{
// Read file via ASSIMP
aiPropertyStore* props = aiCreatePropertyStore();
aiSetImportPropertyInteger(props, AI_CONFIG_PP_SLM_TRIANGLE_LIMIT, MAX_TRIANGLES_PER_MESH);
const uint32_t flags = aiProcess_Triangulate | aiProcess_CalcTangentSpace;
const std::string pathStr = Utilities::nativeStringToStdString(path);
const aiScene* scene = aiImportFileExWithProperties(pathStr.c_str(), flags, NULL, props);
aiReleasePropertyStore(props);
// Check for errors
const nbBool success = scene && scene->mFlags != AI_SCENE_FLAGS_INCOMPLETE && scene->mRootNode;
assert(success);
// Process ASSIMP's root node recursively here !!! :)
processNode(scene, scene->mRootNode, aiMatrix4x4());
aiReleaseImport(scene);
}
The processNode method is straightforward:
void BaseModel::processNode(const aiScene* scene, aiNode* node, const aiMatrix4x4& transform)
{
const aiMatrix4x4 accTransform = node->mTransformation * transform;
// Process each mesh located at the current node.
if (node->mNumMeshes)
{
// Create group.
DatabaseMeshPtr meshGroup;
{
aiMesh* firstMesh = scene->mMeshes[node->mMeshes[0]];
std::wstring groupName(Utilities::stdStringToNativeString(firstMesh->mName.C_Str()));
meshGroup = EntityDatabaseSingleton::instance()->createEntity<MeshGroup>();
meshGroup->setName(groupName);
meshGroup->m_materialId = m_aiLoadingMaterialIds[firstMesh->mMaterialIndex];
m_meshGroupIdentifiers.push_back(meshGroup->getIdentifier());
}
// Add meshes.
for (uint32_t i = 0; i < node->mNumMeshes; i++)
{
// The node object only contains indices to index the actual objects in the scene.
// The scene contains all the data, node is just to keep stuff organized (like relations between nodes).
aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
this->addMesh(mesh, accTransform, meshGroup);
}
}
// After we've processed all of the meshes (if any) we then recursively process each of the children nodes
for (uint32_t i = 0; i < node->mNumChildren; i++)
{
this->processNode(scene, node->mChildren[i], accTransform);
}
}
The addMesh method:
void BaseModel::addMesh(aiMesh* mesh, const aiMatrix4x4& transform, DatabaseMeshPtr meshGroup)
{
// Data to fill
VertexArray vertices;
std::vector<uint32_t> indices;
// The 3x3 transform.
const aiMatrix3x3 transform3x3 = aiMatrix3x3(transform);
// Walk through each of the mesh's vertices
for (uint32_t i = 0; i < mesh->mNumVertices; i++)
{
FullVertex vertex;
mesh->mVertices[i] = transform * mesh->mVertices[i];
// Position
vertex.position.x = mesh->mVertices[i].x;
vertex.position.y = mesh->mVertices[i].y;
vertex.position.z = mesh->mVertices[i].z;
// Normal.
if (mesh->mNormals)
{
mesh->mNormals[i] = transform3x3 * mesh->mNormals[i];
vertex.normal.x = mesh->mNormals[i].x;
vertex.normal.y = mesh->mNormals[i].y;
vertex.normal.z = mesh->mNormals[i].z;
}
// Tangent
if (mesh->mTangents)
{
vertex.tangent.x = mesh->mTangents[i].x;
vertex.tangent.y = mesh->mTangents[i].y;
vertex.tangent.z = mesh->mTangents[i].z;
}
// Bitangent
if (mesh->mBitangents)
{
vertex.bitangent.x = mesh->mBitangents[i].x;
vertex.bitangent.y = mesh->mBitangents[i].y;
vertex.bitangent.z = mesh->mBitangents[i].z;
}
// Texture Coordinates
if (mesh->mTextureCoords[0]) // Does the mesh contain texture coordinates?
{
// A vertex can contain up to 8 different texture coordinates. We thus make the assumption that we won't
// use models where a vertex can have multiple texture coordinates so we always take the first set (0).
vertex.texCoord.x = mesh->mTextureCoords[0][i].x;
vertex.texCoord.y = mesh->mTextureCoords[0][i].y;
}
vertices.push_back(vertex);
}
// Now wak through each of the mesh's faces (a face is a mesh its triangle) and retrieve the corresponding vertex indices.
for (uint32_t i = 0; i < mesh->mNumFaces; i++)
{
aiFace face = mesh->mFaces[i];
// Retrieve all indices of the face and store them in the indices vector
for (uint32_t j = 0; j < face.mNumIndices; j++)
indices.push_back(face.mIndices[j]);
}
const MeshFlatId meshFlatId = (MeshFlatId)m_flatMeshContainer.size();
Mesh* nativeMesh = new Mesh(vertices, indices, meshFlatId, meshGroup.get());
meshGroup->m_meshes.push_back(nativeMesh);
m_flatMeshContainer.push_back(nativeMesh);
}
Once loading is done vertices are converted to local space. I dont think this is linked to the issue:
for (const EntityIdentifier& entityId : m_meshGroupIdentifiers)
{
Math::Vec3 center;
size_t nbVertices = 0u;
const auto group = getMeshGroupPtr_FromEntity(entityId);
for (const auto*mesh : group->m_meshes)
{
const auto& vertices = mesh->getRealVertices();
for (auto& vertex : vertices)
center += vertex.position;
nbVertices += vertices.size();
}
center /= nbVertices;
for (auto* mesh : group->m_meshes)
{
auto& vertices = mesh->getMutableVertices();
for (auto& vertex : vertices)
vertex.position -= center;
}
group->setPosition(center);
}
The last setPosition call set the worlspace transform of the mesh group. It is used when performing DirectX 12 realtime rendering. This is clearly not the issue:
cbuffer VertexShaderSharedCB : register(b0)
{
float4x4 vpMat;
};
VS_OUTPUT main(VS_INPUT input, uint instanceID : SV_InstanceID)
{
VS_OUTPUT output;
const float4x4 modelMat = meshGroupDatas[instanceID].transform;// transform here!
const float4 worldPosition = mul(float4(input.position, 1.0f), modelMat);
output.worldPosition = worldPosition.xyz;
output.position = mul(worldPosition, vpMat);
output.texCoord = input.texCoord;
output.normal = normalize(mul(float4(input.normal, 0.0f), modelMat));
output.tangent = normalize(mul(float4(input.tangent, 0.0f), modelMat));
output.bitangent = normalize(mul(float4(input.bitangent, 0.0f), modelMat));
return output;
}
What am I doing wrong? And especially why the Blender FBX is loaded properly and not the 3ds max one? I am probably missing a transform somewhere. Note that OBJs are always properly loaded ;)
Thanks!
//---------------------------------------------------------------------------------
The 3ds Max FBX file:
https://www.dropbox.com/scl/fi/5rus8jyhz0xbxghrpus67/max_fbx_nope.fbx?rlkey=c2muf2mhcjbizmllqdx0h0wlx&st=6941tiin&dl=0
The Blender FBX file:
https://www.dropbox.com/scl/fi/zv9slsjz41xlx7nanoyy2/blender_fbx_ok.fbx?rlkey=7olvgnbhceik0qm351rj0lbww&st=dwkzsjn6&dl=0
The 3ds Max 2025 native file:
https://www.dropbox.com/scl/fi/pjcv6896uzv6su8992156/native.max?rlkey=yggek65khewpxntm87zchznjc&st=ze7hq6k7&dl=0
Finally found the fix. The way the current node transform is computed was wrong.
Happens inside the BaseModel::processNode method.
This is wrong:
const aiMatrix4x4 accTransform = node->mTransformation * transform
Need to be changed to
const aiMatrix4x4 accTransform = transform * node->mTransformation;
And things work properly :)