From f41f94c55a6f4916211b91d897f5612013cf4d34 Mon Sep 17 00:00:00 2001 From: marauder2k7 Date: Mon, 22 Jun 2026 13:45:06 +0100 Subject: [PATCH 1/5] Assimp Animation fixes --- Engine/source/ts/assimp/assimpAppSequence.cpp | 30 +- Engine/source/ts/assimp/assimpShapeLoader.cpp | 269 ++++++++++++++++-- 2 files changed, 267 insertions(+), 32 deletions(-) diff --git a/Engine/source/ts/assimp/assimpAppSequence.cpp b/Engine/source/ts/assimp/assimpAppSequence.cpp index c1d59971a..ebfc1a6f1 100644 --- a/Engine/source/ts/assimp/assimpAppSequence.cpp +++ b/Engine/source/ts/assimp/assimpAppSequence.cpp @@ -48,17 +48,33 @@ AssimpAppSequence::~AssimpAppSequence() void AssimpAppSequence::determineTimeMultiplier(aiAnimation* a) { - // Assimp convention: if mTicksPerSecond == 0, assume 25 Hz - const float ticksPerSecond = - (a->mTicksPerSecond > 0.0) - ? (float)a->mTicksPerSecond - : 25.0f; + const ColladaUtils::ImportOptions& opts = ColladaUtils::getOptions(); - mTimeMultiplier = 1.0f / ticksPerSecond; + switch (opts.animTiming) + { + case ColladaUtils::ImportOptions::Seconds: + mTimeMultiplier = 1.0f; + break; + + case ColladaUtils::ImportOptions::Milliseconds: + mTimeMultiplier = 1.0f / 1000.0f; + break; + + case ColladaUtils::ImportOptions::FrameCount: + default: + { + const float ticksPerSecond = + (a->mTicksPerSecond > 0.0) + ? (float)a->mTicksPerSecond + : (float)ColladaUtils::getOptions().animFPS; // safe fallback + mTimeMultiplier = 1.0f / ticksPerSecond; + break; + } + } Con::printf( "[Assimp] TicksPerSecond: %f, Time Multiplier: %f", - ticksPerSecond, + (a->mTicksPerSecond > 0.0) ? (float)a->mTicksPerSecond : (float)ColladaUtils::getOptions().animFPS, mTimeMultiplier ); } diff --git a/Engine/source/ts/assimp/assimpShapeLoader.cpp b/Engine/source/ts/assimp/assimpShapeLoader.cpp index 7a3d5ae8a..6d7b36b3a 100644 --- a/Engine/source/ts/assimp/assimpShapeLoader.cpp +++ b/Engine/source/ts/assimp/assimpShapeLoader.cpp @@ -372,8 +372,8 @@ void AssimpShapeLoader::configureImportUnits() { } F32 fps; - getMetaFloat("CustomFrameRate", fps); - opts.animFPS = fps; + if(getMetaFloat("CustomFrameRate", fps)) + opts.animFPS = fps; } } @@ -451,47 +451,266 @@ void AssimpShapeLoader::getRootAxisTransform() void AssimpShapeLoader::processAnimations() { - // add all animations into 1 ambient animation. - aiAnimation* ambientSeq = new aiAnimation(); - ambientSeq->mName = "ambient"; + if (mScene->mNumAnimations == 0) + return; - Vector ambientChannels; - F32 duration = 0.0f; - F32 maxKeyTime = 0.0f; - if (mScene->mNumAnimations > 0) + // First pass: check whether this is an export with multiple actions. + // tested on Blender -> FBX only, gltf doesnt work. + bool hasMultipleActions = false; + if(mScene->mNumAnimations > 1) + hasMultipleActions = true; + + if (!hasMultipleActions) { + F64 srcTPS = mScene->mAnimations[0]->mTicksPerSecond; + F64 srcDur = mScene->mAnimations[0]->mDuration; + + ColladaUtils::ImportOptions& opts = ColladaUtils::getOptions(); + if (srcTPS <= 0.0 && srcDur < 100.0) + opts.animTiming = ColladaUtils::ImportOptions::Seconds; + else if (srcTPS >= 999.0 && srcTPS <= 1001.0) + opts.animTiming = ColladaUtils::ImportOptions::Milliseconds; + else + opts.animTiming = ColladaUtils::ImportOptions::FrameCount; + + F64 targetTPS = (srcTPS > 0.0) ? srcTPS : ColladaUtils::getOptions().animFPS; + + // Original single-timeline path (no '|' in any animation name). + // Concatenate all channels into one ambient sequence, exactly as before. + aiAnimation* ambientSeq = new aiAnimation(); + ambientSeq->mName = "ambient"; + + Vector ambientChannels; + F32 maxKeyTime = 0.0f; + for (U32 i = 0; i < mScene->mNumAnimations; ++i) { aiAnimation* anim = mScene->mAnimations[i]; - - duration = 0.0f; for (U32 j = 0; j < anim->mNumChannels; j++) { aiNodeAnim* nodeAnim = anim->mChannels[j]; - // Determine the maximum keyframe time for this animation - for (U32 k = 0; k < nodeAnim->mNumPositionKeys; k++) { + for (U32 k = 0; k < nodeAnim->mNumPositionKeys; k++) maxKeyTime = getMax(maxKeyTime, (F32)nodeAnim->mPositionKeys[k].mTime); - } - for (U32 k = 0; k < nodeAnim->mNumRotationKeys; k++) { + for (U32 k = 0; k < nodeAnim->mNumRotationKeys; k++) maxKeyTime = getMax(maxKeyTime, (F32)nodeAnim->mRotationKeys[k].mTime); - } - for (U32 k = 0; k < nodeAnim->mNumScalingKeys; k++) { + for (U32 k = 0; k < nodeAnim->mNumScalingKeys; k++) maxKeyTime = getMax(maxKeyTime, (F32)nodeAnim->mScalingKeys[k].mTime); - } - ambientChannels.push_back(nodeAnim); - - duration = getMax(duration, maxKeyTime); } } ambientSeq->mNumChannels = ambientChannels.size(); ambientSeq->mChannels = ambientChannels.address(); - ambientSeq->mDuration = duration; - ambientSeq->mTicksPerSecond = ColladaUtils::getOptions().animFPS; + ambientSeq->mDuration = maxKeyTime; + ambientSeq->mTicksPerSecond = targetTPS; - AssimpAppSequence* defaultAssimpSeq = new AssimpAppSequence(ambientSeq); - appSequences.push_back(defaultAssimpSeq); + appSequences.push_back(new AssimpAppSequence(ambientSeq)); + return; + } + + // Calculate the timing used for this import. + { + F64 srcTPS = mScene->mAnimations[0]->mTicksPerSecond; + F64 srcDur = mScene->mAnimations[0]->mDuration; + + ColladaUtils::ImportOptions& opts = ColladaUtils::getOptions(); + if (srcTPS <= 0.0 && srcDur < 100.0) + opts.animTiming = ColladaUtils::ImportOptions::Seconds; + else if (srcTPS >= 999.0 && srcTPS <= 1001.0) + opts.animTiming = ColladaUtils::ImportOptions::Milliseconds; + else + opts.animTiming = ColladaUtils::ImportOptions::FrameCount; + + Con::printf("[ASSIMP] Animation timing: %s (mTicksPerSecond=%.1f)", + opts.animTiming == ColladaUtils::ImportOptions::Seconds ? "Seconds" : + opts.animTiming == ColladaUtils::ImportOptions::Milliseconds ? "Milliseconds" : "FrameCount", + (F32)srcTPS); + } + + // Seperated action path. + // Data structures (kept as parallel vectors to avoid STL map dependency): + // actionNames[i] - unique action name (after '|') + // actionDurations[i] - duration (ticks) of that action + // actionChannels[i] - correctly-matched channels for that action + // actionNodesSeen[i] - node names already added (deduplication guard) + Vector actionNames; + Vector actionDurations; + Vector< Vector > actionChannels; + Vector< Vector > actionNodesSeen; + + F64 ticksPerSecond = mScene->mAnimations[0]->mTicksPerSecond; + if (ticksPerSecond <= 0.0) ticksPerSecond = 30.0; + + // GLTF stores times in seconds (mTicksPerSecond==0, mDuration < 100). + // FBX stores frame-count ticks (mTicksPerSecond > 0, mDuration in hundreds). + // Detect and rescale to ticks so both formats produce the same units downstream. + F64 srcTPS = mScene->mAnimations[0]->mTicksPerSecond; + F64 srcDur = mScene->mAnimations[0]->mDuration; + bool timesInSeconds = (srcTPS <= 0.0 && srcDur < 100.0); + F32 timeScale = timesInSeconds ? (F32)ticksPerSecond : 1.0f; + + if (timesInSeconds) + { + // Rescale all key timestamps in every animation channel in-place + for (U32 i = 0; i < mScene->mNumAnimations; ++i) + { + aiAnimation* anim = mScene->mAnimations[i]; + anim->mDuration *= timeScale; + for (U32 j = 0; j < anim->mNumChannels; ++j) + { + aiNodeAnim* ch = anim->mChannels[j]; + for (U32 k = 0; k < ch->mNumPositionKeys; ++k) ch->mPositionKeys[k].mTime *= timeScale; + for (U32 k = 0; k < ch->mNumRotationKeys; ++k) ch->mRotationKeys[k].mTime *= timeScale; + for (U32 k = 0; k < ch->mNumScalingKeys; ++k) ch->mScalingKeys[k].mTime *= timeScale; + } + } + } + + for (U32 i = 0; i < mScene->mNumAnimations; ++i) + { + aiAnimation* anim = mScene->mAnimations[i]; + const char* fullName = anim->mName.C_Str(); + const char* pipe = dStrchr(fullName, '|'); + + // Extract "NodePrefix" and "ActionName" from "NodePrefix|ActionName" + String nodePrefix = pipe ? String(fullName, (U32)(pipe - fullName)) : String(""); + String actionName = pipe ? String(pipe + 1) : String(fullName); + + // Find or create the action slot + S32 slot = -1; + for (S32 k = 0; k < (S32)actionNames.size(); ++k) + { + if (actionNames[k] == actionName) { slot = k; break; } + } + if (slot == -1) + { + slot = (S32)actionNames.size(); + actionNames.push_back(actionName); + actionDurations.push_back(0.0); + actionChannels.push_back(Vector()); + actionNodesSeen.push_back(Vector()); + } + + // Track maximum duration for this action + if (anim->mDuration > actionDurations[slot]) + actionDurations[slot] = anim->mDuration; + + // Accept channels whose node name matches the prefix + for (U32 j = 0; j < anim->mNumChannels; ++j) + { + aiNodeAnim* chan = anim->mChannels[j]; + String chanNode = chan->mNodeName.C_Str(); + + if (!nodePrefix.isEmpty() && nodePrefix != chanNode) + continue; + + // Deduplicate: first channel for a given node in this action wins + bool alreadyAdded = false; + for (U32 n = 0; n < actionNodesSeen[slot].size(); ++n) + { + if (actionNodesSeen[slot][n] == chanNode) { alreadyAdded = true; break; } + } + if (!alreadyAdded) + { + actionChannels[slot].push_back(chan); + actionNodesSeen[slot].push_back(chanNode); + } + } + } + + // ----------------------------------------------------------------------- + // AMBIENT and NAMED SEQUENCES both use own-action-only channels. + // ----------------------------------------------------------------------- + { + // AMBIENT: use ONLY each node's own canonical action channel. + Vector ownChans; + F64 ambientDuration = 0.0; + + for (U32 i = 0; i < actionNames.size(); ++i) + { + String ownerName; + aiNodeAnim* ownerChan = nullptr; + + for (U32 j = 0; j < actionChannels[i].size(); ++j) + { + const String& nodeName = actionNodesSeen[i][j]; + U32 nodeLen = nodeName.length(); + // Exact owner match: action name = nodeName + "Action" [+ optional suffix]. + const char* actionStr = actionNames[i].c_str(); + if (dStrnicmp(actionStr, nodeName.c_str(), nodeLen) == 0 + && dStrnicmp(actionStr + nodeLen, "Action", 6) == 0 + && nodeLen > ownerName.length()) + { + ownerName = nodeName; + ownerChan = actionChannels[i][j]; + } + } + + if (ownerChan) + { + ownChans.push_back(ownerChan); + ambientDuration = getMax(ambientDuration, actionDurations[i]); + Con::printf("[ASSIMP] Ambient channel: node=%-25s action=%s duration=%.1f ticks", + ownerName.c_str(), actionNames[i].c_str(), (F32)actionDurations[i]); + } + } + + aiAnimation* ambientAnim = new aiAnimation(); + ambientAnim->mName = aiString("ambient"); + ambientAnim->mTicksPerSecond = ticksPerSecond; + ambientAnim->mDuration = ambientDuration; + ambientAnim->mNumChannels = ownChans.size(); + ambientAnim->mChannels = new aiNodeAnim * [ownChans.size()]; + for (U32 i = 0; i < ownChans.size(); ++i) + ambientAnim->mChannels[i] = ownChans[i]; + + Con::printf("[ASSIMP] Ambient: %d own-action channels, duration=%.1f ticks (%.2f sec)", + ambientAnim->mNumChannels, (F32)ambientDuration, (F32)(ambientDuration / ticksPerSecond)); + + appSequences.push_back(new AssimpAppSequence(ambientAnim)); + } + + // ----------------------------------------------------------------------- + // NAMED SEQUENCES: one sequence per unique action, containing ONLY the + // owning node's channel. + // ----------------------------------------------------------------------- + for (U32 i = 0; i < actionNames.size(); ++i) + { + if (actionChannels[i].empty()) + continue; + + // Identify the owning node by exact nodeName+"Action" match (same logic as ambient) + String ownerName; + aiNodeAnim* ownerChan = nullptr; + for (U32 j = 0; j < actionChannels[i].size(); ++j) + { + const String& nodeName = actionNodesSeen[i][j]; + U32 nodeLen = nodeName.length(); + const char* actionStr = actionNames[i].c_str(); + if (dStrnicmp(actionStr, nodeName.c_str(), nodeLen) == 0 + && dStrnicmp(actionStr + nodeLen, "Action", 6) == 0 + && nodeLen > ownerName.length()) + { + ownerName = nodeName; + ownerChan = actionChannels[i][j]; + } + } + + if (!ownerChan) + continue; + + aiAnimation* seq = new aiAnimation(); + seq->mName = aiString(actionNames[i].c_str()); + seq->mTicksPerSecond = ticksPerSecond; + seq->mDuration = actionDurations[i]; + seq->mNumChannels = 1; + seq->mChannels = new aiNodeAnim * [1]; + seq->mChannels[0] = ownerChan; + + Con::printf("[ASSIMP] Sequence '%s': owner=%s duration=%.1f ticks", + actionNames[i].c_str(), ownerName.c_str(), (F32)actionDurations[i]); + + appSequences.push_back(new AssimpAppSequence(seq)); } } From a8640de8eeed5b0124d50c8c72aeed0f94c9a9a2 Mon Sep 17 00:00:00 2001 From: marauder2k7 Date: Mon, 22 Jun 2026 19:07:12 +0100 Subject: [PATCH 2/5] Update assimpShapeLoader.cpp allow named actions with multiple nodes (similar to ambient including everything except each authored action should only have 1 track (channel) for each node already) --- Engine/source/ts/assimp/assimpShapeLoader.cpp | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/Engine/source/ts/assimp/assimpShapeLoader.cpp b/Engine/source/ts/assimp/assimpShapeLoader.cpp index 6d7b36b3a..647501ddc 100644 --- a/Engine/source/ts/assimp/assimpShapeLoader.cpp +++ b/Engine/source/ts/assimp/assimpShapeLoader.cpp @@ -696,19 +696,33 @@ void AssimpShapeLoader::processAnimations() } } - if (!ownerChan) - continue; - aiAnimation* seq = new aiAnimation(); seq->mName = aiString(actionNames[i].c_str()); seq->mTicksPerSecond = ticksPerSecond; seq->mDuration = actionDurations[i]; - seq->mNumChannels = 1; - seq->mChannels = new aiNodeAnim * [1]; - seq->mChannels[0] = ownerChan; - Con::printf("[ASSIMP] Sequence '%s': owner=%s duration=%.1f ticks", - actionNames[i].c_str(), ownerName.c_str(), (F32)actionDurations[i]); + if (ownerChan) + { + // Per-object action: single owner channel only. + // Non-owner nodes inherit parent motion via Torque's hierarchy. + seq->mNumChannels = 1; + seq->mChannels = new aiNodeAnim * [1]; + seq->mChannels[0] = ownerChan; + Con::printf("[ASSIMP] Sequence '%s': owner=%s duration=%.1f ticks", + actionNames[i].c_str(), ownerName.c_str(), (F32)actionDurations[i]); + } + else + { + // No per-object naming match: this is a deliberately multi-node + // authored action (e.g. "GateOpen", "ArmSystemExtend"). Include + // all channels so the full choreography is preserved. + seq->mNumChannels = actionChannels[i].size(); + seq->mChannels = new aiNodeAnim * [seq->mNumChannels]; + for (U32 k = 0; k < actionChannels[i].size(); ++k) + seq->mChannels[k] = actionChannels[i][k]; + Con::printf("[ASSIMP] Sequence '%s': multi-node (%d channels) duration=%.1f ticks", + actionNames[i].c_str(), seq->mNumChannels, (F32)actionDurations[i]); + } appSequences.push_back(new AssimpAppSequence(seq)); } From 07d0eeed777aa6a30d8a89e6e05a74144b66bf0f Mon Sep 17 00:00:00 2001 From: marauder2k7 Date: Wed, 24 Jun 2026 14:11:13 +0100 Subject: [PATCH 3/5] Update assimpShapeLoader.cpp --- Engine/source/ts/assimp/assimpShapeLoader.cpp | 176 +++++++++--------- 1 file changed, 84 insertions(+), 92 deletions(-) diff --git a/Engine/source/ts/assimp/assimpShapeLoader.cpp b/Engine/source/ts/assimp/assimpShapeLoader.cpp index 647501ddc..a48b55631 100644 --- a/Engine/source/ts/assimp/assimpShapeLoader.cpp +++ b/Engine/source/ts/assimp/assimpShapeLoader.cpp @@ -454,11 +454,8 @@ void AssimpShapeLoader::processAnimations() if (mScene->mNumAnimations == 0) return; - // First pass: check whether this is an export with multiple actions. - // tested on Blender -> FBX only, gltf doesnt work. - bool hasMultipleActions = false; - if(mScene->mNumAnimations > 1) - hasMultipleActions = true; + // Multiple animations = multiple actions; single animation = flat timeline. + bool hasMultipleActions = (mScene->mNumAnimations > 1); if (!hasMultipleActions) { @@ -475,8 +472,7 @@ void AssimpShapeLoader::processAnimations() F64 targetTPS = (srcTPS > 0.0) ? srcTPS : ColladaUtils::getOptions().animFPS; - // Original single-timeline path (no '|' in any animation name). - // Concatenate all channels into one ambient sequence, exactly as before. + // Single-timeline path: concatenate all channels into one ambient sequence. aiAnimation* ambientSeq = new aiAnimation(); ambientSeq->mName = "ambient"; @@ -527,19 +523,16 @@ void AssimpShapeLoader::processAnimations() (F32)srcTPS); } - // Seperated action path. // Data structures (kept as parallel vectors to avoid STL map dependency): // actionNames[i] - unique action name (after '|') // actionDurations[i] - duration (ticks) of that action - // actionChannels[i] - correctly-matched channels for that action - // actionNodesSeen[i] - node names already added (deduplication guard) - Vector actionNames; - Vector actionDurations; + // actionChannels[i] - channels for that action (deduped by mNodeName) + Vector actionNames; + Vector actionDurations; Vector< Vector > actionChannels; - Vector< Vector > actionNodesSeen; F64 ticksPerSecond = mScene->mAnimations[0]->mTicksPerSecond; - if (ticksPerSecond <= 0.0) ticksPerSecond = 30.0; + if (ticksPerSecond <= 0.0) ticksPerSecond = ColladaUtils::getOptions().animFPS; // GLTF stores times in seconds (mTicksPerSecond==0, mDuration < 100). // FBX stores frame-count ticks (mTicksPerSecond > 0, mDuration in hundreds). @@ -551,7 +544,6 @@ void AssimpShapeLoader::processAnimations() if (timesInSeconds) { - // Rescale all key timestamps in every animation channel in-place for (U32 i = 0; i < mScene->mNumAnimations; ++i) { aiAnimation* anim = mScene->mAnimations[i]; @@ -572,8 +564,9 @@ void AssimpShapeLoader::processAnimations() const char* fullName = anim->mName.C_Str(); const char* pipe = dStrchr(fullName, '|'); - // Extract "NodePrefix" and "ActionName" from "NodePrefix|ActionName" - String nodePrefix = pipe ? String(fullName, (U32)(pipe - fullName)) : String(""); + // Strip "NodeName|" — only the action name (part after pipe) is needed. + // nodePrefix and the nodePrefix==chanNode filter are removed; node + // identity comes from chan->mNodeName on each channel directly. String actionName = pipe ? String(pipe + 1) : String(fullName); // Find or create the action slot @@ -588,106 +581,58 @@ void AssimpShapeLoader::processAnimations() actionNames.push_back(actionName); actionDurations.push_back(0.0); actionChannels.push_back(Vector()); - actionNodesSeen.push_back(Vector()); } // Track maximum duration for this action if (anim->mDuration > actionDurations[slot]) actionDurations[slot] = anim->mDuration; - // Accept channels whose node name matches the prefix + // Add channels, deduplicating by chan->mNodeName for (U32 j = 0; j < anim->mNumChannels; ++j) { aiNodeAnim* chan = anim->mChannels[j]; - String chanNode = chan->mNodeName.C_Str(); + const char* nodeName = chan->mNodeName.C_Str(); - if (!nodePrefix.isEmpty() && nodePrefix != chanNode) - continue; - - // Deduplicate: first channel for a given node in this action wins bool alreadyAdded = false; - for (U32 n = 0; n < actionNodesSeen[slot].size(); ++n) + for (U32 n = 0; n < actionChannels[slot].size(); ++n) { - if (actionNodesSeen[slot][n] == chanNode) { alreadyAdded = true; break; } - } - if (!alreadyAdded) - { - actionChannels[slot].push_back(chan); - actionNodesSeen[slot].push_back(chanNode); - } - } - } - - // ----------------------------------------------------------------------- - // AMBIENT and NAMED SEQUENCES both use own-action-only channels. - // ----------------------------------------------------------------------- - { - // AMBIENT: use ONLY each node's own canonical action channel. - Vector ownChans; - F64 ambientDuration = 0.0; - - for (U32 i = 0; i < actionNames.size(); ++i) - { - String ownerName; - aiNodeAnim* ownerChan = nullptr; - - for (U32 j = 0; j < actionChannels[i].size(); ++j) - { - const String& nodeName = actionNodesSeen[i][j]; - U32 nodeLen = nodeName.length(); - // Exact owner match: action name = nodeName + "Action" [+ optional suffix]. - const char* actionStr = actionNames[i].c_str(); - if (dStrnicmp(actionStr, nodeName.c_str(), nodeLen) == 0 - && dStrnicmp(actionStr + nodeLen, "Action", 6) == 0 - && nodeLen > ownerName.length()) + if (dStrcmp(actionChannels[slot][n]->mNodeName.C_Str(), nodeName) == 0) { - ownerName = nodeName; - ownerChan = actionChannels[i][j]; + alreadyAdded = true; break; } } - - if (ownerChan) - { - ownChans.push_back(ownerChan); - ambientDuration = getMax(ambientDuration, actionDurations[i]); - Con::printf("[ASSIMP] Ambient channel: node=%-25s action=%s duration=%.1f ticks", - ownerName.c_str(), actionNames[i].c_str(), (F32)actionDurations[i]); - } + if (!alreadyAdded) + actionChannels[slot].push_back(chan); } - - aiAnimation* ambientAnim = new aiAnimation(); - ambientAnim->mName = aiString("ambient"); - ambientAnim->mTicksPerSecond = ticksPerSecond; - ambientAnim->mDuration = ambientDuration; - ambientAnim->mNumChannels = ownChans.size(); - ambientAnim->mChannels = new aiNodeAnim * [ownChans.size()]; - for (U32 i = 0; i < ownChans.size(); ++i) - ambientAnim->mChannels[i] = ownChans[i]; - - Con::printf("[ASSIMP] Ambient: %d own-action channels, duration=%.1f ticks (%.2f sec)", - ambientAnim->mNumChannels, (F32)ambientDuration, (F32)(ambientDuration / ticksPerSecond)); - - appSequences.push_back(new AssimpAppSequence(ambientAnim)); } // ----------------------------------------------------------------------- - // NAMED SEQUENCES: one sequence per unique action, containing ONLY the - // owning node's channel. + // Build NAMED SEQUENCES and collect AMBIENT // ----------------------------------------------------------------------- + + Vector ownChans; + Vector ownNodesSeen; + F64 ambientDuration = 0.0; + for (U32 i = 0; i < actionNames.size(); ++i) { - if (actionChannels[i].empty()) + // Skip actions with no channels or no duration + if (actionChannels[i].empty() || actionDurations[i] <= 0.0) + { + Con::printf("[ASSIMP] Skipping action '%s' (empty or zero duration)", + actionNames[i].c_str()); continue; + } - // Identify the owning node by exact nodeName+"Action" match (same logic as ambient) + // Find owner: chan->mNodeName that action name starts with + "Action" String ownerName; aiNodeAnim* ownerChan = nullptr; for (U32 j = 0; j < actionChannels[i].size(); ++j) { - const String& nodeName = actionNodesSeen[i][j]; - U32 nodeLen = nodeName.length(); + const char* nodeName = actionChannels[i][j]->mNodeName.C_Str(); + U32 nodeLen = dStrlen(nodeName); const char* actionStr = actionNames[i].c_str(); - if (dStrnicmp(actionStr, nodeName.c_str(), nodeLen) == 0 + if (dStrnicmp(actionStr, nodeName, nodeLen) == 0 && dStrnicmp(actionStr + nodeLen, "Action", 6) == 0 && nodeLen > ownerName.length()) { @@ -704,28 +649,75 @@ void AssimpShapeLoader::processAnimations() if (ownerChan) { // Per-object action: single owner channel only. - // Non-owner nodes inherit parent motion via Torque's hierarchy. seq->mNumChannels = 1; seq->mChannels = new aiNodeAnim * [1]; seq->mChannels[0] = ownerChan; Con::printf("[ASSIMP] Sequence '%s': owner=%s duration=%.1f ticks", actionNames[i].c_str(), ownerName.c_str(), (F32)actionDurations[i]); + + // Collect owner channel for ambient (name-matched = safe data) + bool already = false; + for (U32 k = 0; k < ownNodesSeen.size(); ++k) + if (ownNodesSeen[k] == ownerName) { already = true; break; } + if (!already) + { + ownChans.push_back(ownerChan); + ownNodesSeen.push_back(ownerName); + ambientDuration = getMax(ambientDuration, actionDurations[i]); + Con::printf("[ASSIMP] Ambient channel: node=%-25s action=%s duration=%.1f ticks", + ownerName.c_str(), actionNames[i].c_str(), (F32)actionDurations[i]); + } } else { - // No per-object naming match: this is a deliberately multi-node - // authored action (e.g. "GateOpen", "ArmSystemExtend"). Include - // all channels so the full choreography is preserved. + // No name match: renamed or multi-node authored action. seq->mNumChannels = actionChannels[i].size(); seq->mChannels = new aiNodeAnim * [seq->mNumChannels]; for (U32 k = 0; k < actionChannels[i].size(); ++k) seq->mChannels[k] = actionChannels[i][k]; Con::printf("[ASSIMP] Sequence '%s': multi-node (%d channels) duration=%.1f ticks", actionNames[i].c_str(), seq->mNumChannels, (F32)actionDurations[i]); + + // Ambient fallback: only from no-owner slots so bystander channels + // from name-matched action evaluations never corrupt ambient. + // Each channel here came from NodeName|RenamedAction — own data. + for (U32 k = 0; k < actionChannels[i].size(); ++k) + { + const char* nodeName = actionChannels[i][k]->mNodeName.C_Str(); + bool already = false; + for (U32 n = 0; n < ownNodesSeen.size(); ++n) + if (dStrcmp(ownNodesSeen[n].c_str(), nodeName) == 0) { already = true; break; } + if (!already) + { + ownChans.push_back(actionChannels[i][k]); + ownNodesSeen.push_back(String(nodeName)); + ambientDuration = getMax(ambientDuration, actionDurations[i]); + Con::printf("[ASSIMP] Ambient fallback: node=%s action=%s", + nodeName, actionNames[i].c_str()); + } + } } appSequences.push_back(new AssimpAppSequence(seq)); } + + // Build ambient from collected channels, inserted at index 0 + { + aiAnimation* ambientAnim = new aiAnimation(); + ambientAnim->mName = aiString("ambient"); + ambientAnim->mTicksPerSecond = ticksPerSecond; + ambientAnim->mDuration = ambientDuration; + ambientAnim->mNumChannels = ownChans.size(); + ambientAnim->mChannels = new aiNodeAnim * [ownChans.size()]; + for (U32 i = 0; i < ownChans.size(); ++i) + ambientAnim->mChannels[i] = ownChans[i]; + + Con::printf("[ASSIMP] Ambient: %d channels, duration=%.1f ticks (%.2f sec)", + ambientAnim->mNumChannels, (F32)ambientDuration, (F32)(ambientDuration / ticksPerSecond)); + + appSequences.push_back( new AssimpAppSequence(ambientAnim)); + } + } void AssimpShapeLoader::computeBounds(Box3F& bounds) From dc8aa598ed7f6f25eabdb8fb3a15753363217081 Mon Sep 17 00:00:00 2001 From: marauder2k7 Date: Sat, 27 Jun 2026 01:51:07 +0100 Subject: [PATCH 4/5] more fix attempts --- Engine/source/ts/assimp/assimpAppMesh.cpp | 32 +------------- Engine/source/ts/assimp/assimpAppMesh.h | 1 - Engine/source/ts/assimp/assimpAppNode.cpp | 43 +------------------ Engine/source/ts/assimp/assimpAppNode.h | 18 ++++++-- Engine/source/ts/assimp/assimpShapeLoader.cpp | 40 +++++++++++------ Engine/source/ts/loader/appNode.h | 8 ---- 6 files changed, 46 insertions(+), 96 deletions(-) diff --git a/Engine/source/ts/assimp/assimpAppMesh.cpp b/Engine/source/ts/assimp/assimpAppMesh.cpp index eaabde89f..0394f1559 100644 --- a/Engine/source/ts/assimp/assimpAppMesh.cpp +++ b/Engine/source/ts/assimp/assimpAppMesh.cpp @@ -79,37 +79,7 @@ const char* AssimpAppMesh::getName(bool allowFixed) MatrixF AssimpAppMesh::getMeshTransform(F32 time) { - MatrixF transform = appNode->getNodeTransform(time); - - // AssimpAppNode::getTransform() deliberately skips axis correction for the - // bounds node itself, since its (uncorrected) transform is used elsewhere - // as the reference frame the rest of the shape gets normalized against - // (see TSShapeLoader::getLocalNodeMatrix). But if this mesh's geometry was - // hand-modeled as part of the source scene (as opposed to the empty, - // auto-generated bounds node added when none exists), it lives in the same - // source up-axis space as every other mesh and needs the same correction - // baked into its locked vertex data - otherwise it ends up sitting in the - // model's original, unrotated space instead of Torque's Z-up space. - if (appNode->isBounds()) - { - MatrixF axisFix = ColladaUtils::getOptions().axisCorrectionMat; - transform.mulL(axisFix); - } - - return transform; -} - -void AssimpAppMesh::computeBounds(Box3F& bounds) -{ - if (appNode->isBounds()) - { - bounds = Box3F::Invalid; - for (S32 iVert = 0; iVert < points.size(); iVert++) - bounds.extend(points[iVert]); - return; - } - - Parent::computeBounds(bounds); + return appNode->getNodeTransform(time); } void AssimpAppMesh::lockMesh(F32 t, const MatrixF& objOffset) diff --git a/Engine/source/ts/assimp/assimpAppMesh.h b/Engine/source/ts/assimp/assimpAppMesh.h index e0e87add3..2215b1140 100644 --- a/Engine/source/ts/assimp/assimpAppMesh.h +++ b/Engine/source/ts/assimp/assimpAppMesh.h @@ -119,7 +119,6 @@ public: /// @return The mesh transform at the specified time MatrixF getMeshTransform(F32 time) override; F32 getVisValue(F32 t) override; - void computeBounds(Box3F& bounds) override; static Vector sMaterialRemap; }; diff --git a/Engine/source/ts/assimp/assimpAppNode.cpp b/Engine/source/ts/assimp/assimpAppNode.cpp index 3cfb66f1e..177b3ee0e 100644 --- a/Engine/source/ts/assimp/assimpAppNode.cpp +++ b/Engine/source/ts/assimp/assimpAppNode.cpp @@ -49,7 +49,7 @@ F32 AssimpAppNode::sTimeMultiplier = 1.0f; AssimpAppNode::AssimpAppNode(const aiScene* scene, const aiNode* node, AssimpAppNode* parentNode) : mScene(scene), - mNode(node ? node : scene->mRootNode), + mNode(node), mInvertMeshes(false), mLastTransformTime(TSShapeLoader::DefaultTime - 1), mDefaultTransformValid(false) @@ -62,7 +62,7 @@ AssimpAppNode::AssimpAppNode(const aiScene* scene, const aiNode* node, AssimpApp const char* defaultName = "null"; mName = dStrdup(defaultName); } - mParentName = dStrdup(parentNode ? parentNode->mName : "ROOT"); + mParentName = dStrdup(parentNode ? parentNode->mName : "DUMMY"); // Convert transformation matrix assimpToTorqueMat(node->mTransformation, mNodeTransform); Con::printf("[ASSIMP] Node Created: %s, Parent: %s", mName, mParentName); @@ -84,14 +84,6 @@ MatrixF AssimpAppNode::getTransform(F32 time) // no parent (ie. root level) => scale by global shape mLastTransform.identity(); mLastTransform.scale(ColladaUtils::getOptions().unit * ColladaUtils::getOptions().formatScaleFactor); - - if (mScene && mScene->mRootNode) - { - MatrixF sceneRootMat(true); - assimpToTorqueMat(mScene->mRootNode->mTransformation, sceneRootMat); - mLastTransform.mulL(sceneRootMat); - } - if (!isBounds()) { MatrixF axisFix = ColladaUtils::getOptions().axisCorrectionMat; @@ -279,37 +271,6 @@ MatrixF AssimpAppNode::getNodeTransform(F32 time) } } -MatrixF AssimpAppNode::getBoundsReferenceTransform(F32 time) -{ - // Deliberately independent of this node's own raw local data (rotation, - // scale) and of axisCorrectionMat - MatrixF mat(true); - mat.scale(ColladaUtils::getOptions().unit * ColladaUtils::getOptions().formatScaleFactor); - - if (mScene && mScene->mRootNode) - { - MatrixF sceneRootMat(true); - assimpToTorqueMat(mScene->mRootNode->mTransformation, sceneRootMat); - mat.mulL(sceneRootMat); - } - - return mat; -} - -MatrixF AssimpAppNode::getOwnRotationOnly(F32 time) -{ - // This node's own raw mNodeTransform - MatrixF rotOnly(mNodeTransform); - Point3F rawScale = rotOnly.getScale(); - Point3F invScale( - rawScale.x ? 1.0f / rawScale.x : 0.0f, - rawScale.y ? 1.0f / rawScale.y : 0.0f, - rawScale.z ? 1.0f / rawScale.z : 0.0f); - rotOnly.scale(invScale); - rotOnly.setPosition(Point3F::Zero); - return rotOnly; -} - void AssimpAppNode::assimpToTorqueMat(const aiMatrix4x4& inAssimpMat, MatrixF& outMat) { outMat.setRow(0, Point4F((F32)inAssimpMat.a1, (F32)inAssimpMat.a2, diff --git a/Engine/source/ts/assimp/assimpAppNode.h b/Engine/source/ts/assimp/assimpAppNode.h index 1d61c6b02..c6bacb79b 100644 --- a/Engine/source/ts/assimp/assimpAppNode.h +++ b/Engine/source/ts/assimp/assimpAppNode.h @@ -122,10 +122,22 @@ public: } MatrixF getNodeTransform(F32 time) override; - MatrixF getBoundsReferenceTransform(F32 time) override; - MatrixF getOwnRotationOnly(F32 time) override; bool animatesTransform(const AppSequence* appSeq) override; - bool isParentRoot() override { return (appParent == NULL); } + bool isParentRoot() override + { + if (!appParent) + return false; // the scene root itself has no parent — not a content root + + // True when this node's immediate parent is the scene root node. + // mParentName is stored at construction from the AppNode's normalised name + // (empty names become "null"), so apply the same normalisation to the raw + // aiScene root name before comparing. + const char* rootName = mScene->mRootNode->mName.C_Str(); + if (dStrlen(rootName) == 0) + rootName = "null"; + + return dStrcmp(mParentName, rootName) == 0; + } static void assimpToTorqueMat(const aiMatrix4x4& inAssimpMat, MatrixF& outMat); static aiNode* findChildNodeByName(const char* nodeName, aiNode* rootNode); diff --git a/Engine/source/ts/assimp/assimpShapeLoader.cpp b/Engine/source/ts/assimp/assimpShapeLoader.cpp index a48b55631..50f5d3a12 100644 --- a/Engine/source/ts/assimp/assimpShapeLoader.cpp +++ b/Engine/source/ts/assimp/assimpShapeLoader.cpp @@ -299,24 +299,40 @@ void AssimpShapeLoader::enumerateScene() // Setup LOD checks detectDetails(); - aiNode* root = mScene->mRootNode; - for (S32 iNode = 0; iNode < root->mNumChildren; iNode++) + // Process mRootNode as an AppNode directly. + // + // Making it the single parentless node means the !appParent branch in + // AssimpAppNode::getTransform() fires exactly once — for this node only. + // Scale + axisCorrectionMat are therefore applied in exactly one place. + // Every child (bones, mesh nodes, bounds) inherits the correction naturally + // through the parent chain; no per-node special-casing is needed. + AssimpAppNode* sceneRootAppNode = new AssimpAppNode(mScene, mScene->mRootNode, nullptr); + if (!processNode(sceneRootAppNode)) { - aiNode* child = root->mChildren[iNode]; - AssimpAppNode* node = new AssimpAppNode(mScene, child); - if (!processNode(node)) { - delete node; - } + Con::errorf("[ASSIMP] Failed to process scene root node '%s'.", + mScene->mRootNode->mName.C_Str()); + delete sceneRootAppNode; + sceneRootAppNode = nullptr; } - if (!boundsNode) { - aiNode* reqNode = new aiNode("bounds"); - reqNode->mTransformation = aiMatrix4x4(); - AssimpAppNode* appBoundsNode = new AssimpAppNode(mScene, reqNode); - if (!processNode(appBoundsNode)) { + // Bounds check — every Torque shape needs a bounds node. + // If the source file didn't include one, synthesise it + if (!boundsNode) + { + Con::printf("[ASSIMP] No 'bounds' node found - adding synthetic bounds node."); + aiNode* boundsAiNode = new aiNode("bounds"); + boundsAiNode->mTransformation = aiMatrix4x4(); // identity + AssimpAppNode* appBoundsNode = new AssimpAppNode(mScene, boundsAiNode, nullptr); + if (!processNode(appBoundsNode)) + { + Con::errorf("[ASSIMP] Failed to add synthetic bounds node."); delete appBoundsNode; } } + else + { + Con::printf("[ASSIMP] Bounds node found in scene."); + } // Process animations if available processAnimations(); diff --git a/Engine/source/ts/loader/appNode.h b/Engine/source/ts/loader/appNode.h index 2b6f14cab..65ef92e40 100644 --- a/Engine/source/ts/loader/appNode.h +++ b/Engine/source/ts/loader/appNode.h @@ -65,14 +65,6 @@ public: virtual MatrixF getNodeTransform(F32 time) = 0; - /// The transform TSShapeLoader::getLocalNodeMatrix() uses as the bounds - /// reference frame when this node is the shape's bounds node. - virtual MatrixF getBoundsReferenceTransform(F32 time) { return getNodeTransform(time); } - - /// This node's own raw local rotation only (no parent chain, no axis - /// correction, scale zapped out). - virtual MatrixF getOwnRotationOnly(F32 time) { return MatrixF(true); } - virtual bool isEqual(AppNode* node) = 0; virtual bool animatesTransform(const AppSequence* appSeq) = 0; From b3a7049083bb1bc2044d4839ac5ded3e7ce5cdb8 Mon Sep 17 00:00:00 2001 From: marauder2k7 Date: Sun, 28 Jun 2026 07:52:35 +0100 Subject: [PATCH 5/5] Update assimpShapeLoader.cpp fix front sign handling Fix check for bounds in an assimp file --- Engine/source/ts/assimp/assimpShapeLoader.cpp | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/Engine/source/ts/assimp/assimpShapeLoader.cpp b/Engine/source/ts/assimp/assimpShapeLoader.cpp index 50f5d3a12..d1c295a4e 100644 --- a/Engine/source/ts/assimp/assimpShapeLoader.cpp +++ b/Engine/source/ts/assimp/assimpShapeLoader.cpp @@ -277,6 +277,17 @@ void AssimpShapeLoader::enumerateScene() getRootAxisTransform(); + bool fileHasBounds = false; + for (U32 i = 0; i < mScene->mNumMeshes && !fileHasBounds; ++i) + { + if (dStricmp(mScene->mMeshes[i]->mName.C_Str(), "bounds") == 0) + fileHasBounds = true; + } + if (!fileHasBounds) + fileHasBounds = (AssimpAppNode::findChildNodeByName("bounds", mScene->mRootNode) != nullptr); + + Con::printf("[ASSIMP] Bounds pre-scan: %s", fileHasBounds ? "found in scene" : "not found - will synthesise"); + for (U32 i = 0; i < mScene->mNumTextures; ++i) { extractTexture(i, mScene->mTextures[i]); } @@ -317,7 +328,7 @@ void AssimpShapeLoader::enumerateScene() // Bounds check — every Torque shape needs a bounds node. // If the source file didn't include one, synthesise it - if (!boundsNode) + if (!fileHasBounds) { Con::printf("[ASSIMP] No 'bounds' node found - adding synthetic bounds node."); aiNode* boundsAiNode = new aiNode("bounds"); @@ -396,7 +407,7 @@ void AssimpShapeLoader::configureImportUnits() { void AssimpShapeLoader::getRootAxisTransform() { int upAxis = 1, upSign = 1; - int frontAxis = 2, frontSign = -1; + int frontAxis = 2, frontSign = 1; int coordAxis = 0, coordSign = 1; aiMetadata* meta = mScene->mMetaData; @@ -441,7 +452,7 @@ void AssimpShapeLoader::getRootAxisTransform() return v; }; - Point3F forward = axisToVector(frontAxis, -frontSign); + Point3F forward = axisToVector(frontAxis, frontSign == 1 ? -frontSign : frontSign); Point3F up = axisToVector(upAxis, upSign); Point3F right = mCross(forward, up);