mirror of
https://github.com/TorqueGameEngines/Torque3D.git
synced 2026-07-01 01:34:31 +00:00
Merge pull request #1778 from marauder2k9-torque/Assmip-AnimationFixes
Assimp Node axis fixes and Extended Animation support
This commit is contained in:
commit
f93ee21b18
7 changed files with 332 additions and 130 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<S32> sMaterialRemap;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <unit>
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
|
|
@ -299,24 +310,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 (!fileHasBounds)
|
||||
{
|
||||
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();
|
||||
|
|
@ -372,15 +399,15 @@ void AssimpShapeLoader::configureImportUnits() {
|
|||
}
|
||||
|
||||
F32 fps;
|
||||
getMetaFloat("CustomFrameRate", fps);
|
||||
opts.animFPS = fps;
|
||||
if(getMetaFloat("CustomFrameRate", fps))
|
||||
opts.animFPS = fps;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
@ -425,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);
|
||||
|
||||
|
|
@ -451,48 +478,273 @@ 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<aiNodeAnim*> ambientChannels;
|
||||
F32 duration = 0.0f;
|
||||
F32 maxKeyTime = 0.0f;
|
||||
if (mScene->mNumAnimations > 0)
|
||||
// Multiple animations = multiple actions; single animation = flat timeline.
|
||||
bool hasMultipleActions = (mScene->mNumAnimations > 1);
|
||||
|
||||
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;
|
||||
|
||||
// Single-timeline path: concatenate all channels into one ambient sequence.
|
||||
aiAnimation* ambientSeq = new aiAnimation();
|
||||
ambientSeq->mName = "ambient";
|
||||
|
||||
Vector<aiNodeAnim*> 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);
|
||||
}
|
||||
|
||||
// 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] - channels for that action (deduped by mNodeName)
|
||||
Vector<String> actionNames;
|
||||
Vector<F64> actionDurations;
|
||||
Vector< Vector<aiNodeAnim*> > actionChannels;
|
||||
|
||||
F64 ticksPerSecond = mScene->mAnimations[0]->mTicksPerSecond;
|
||||
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).
|
||||
// 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)
|
||||
{
|
||||
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, '|');
|
||||
|
||||
// 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
|
||||
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<aiNodeAnim*>());
|
||||
}
|
||||
|
||||
// Track maximum duration for this action
|
||||
if (anim->mDuration > actionDurations[slot])
|
||||
actionDurations[slot] = anim->mDuration;
|
||||
|
||||
// Add channels, deduplicating by chan->mNodeName
|
||||
for (U32 j = 0; j < anim->mNumChannels; ++j)
|
||||
{
|
||||
aiNodeAnim* chan = anim->mChannels[j];
|
||||
const char* nodeName = chan->mNodeName.C_Str();
|
||||
|
||||
bool alreadyAdded = false;
|
||||
for (U32 n = 0; n < actionChannels[slot].size(); ++n)
|
||||
{
|
||||
if (dStrcmp(actionChannels[slot][n]->mNodeName.C_Str(), nodeName) == 0)
|
||||
{
|
||||
alreadyAdded = true; break;
|
||||
}
|
||||
}
|
||||
if (!alreadyAdded)
|
||||
actionChannels[slot].push_back(chan);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Build NAMED SEQUENCES and collect AMBIENT
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
Vector<aiNodeAnim*> ownChans;
|
||||
Vector<String> ownNodesSeen;
|
||||
F64 ambientDuration = 0.0;
|
||||
|
||||
for (U32 i = 0; i < actionNames.size(); ++i)
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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 char* nodeName = actionChannels[i][j]->mNodeName.C_Str();
|
||||
U32 nodeLen = dStrlen(nodeName);
|
||||
const char* actionStr = actionNames[i].c_str();
|
||||
if (dStrnicmp(actionStr, nodeName, nodeLen) == 0
|
||||
&& dStrnicmp(actionStr + nodeLen, "Action", 6) == 0
|
||||
&& nodeLen > ownerName.length())
|
||||
{
|
||||
ownerName = nodeName;
|
||||
ownerChan = actionChannels[i][j];
|
||||
}
|
||||
}
|
||||
|
||||
aiAnimation* seq = new aiAnimation();
|
||||
seq->mName = aiString(actionNames[i].c_str());
|
||||
seq->mTicksPerSecond = ticksPerSecond;
|
||||
seq->mDuration = actionDurations[i];
|
||||
|
||||
if (ownerChan)
|
||||
{
|
||||
// Per-object action: single owner channel only.
|
||||
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 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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue