static void ReadAnimations()

in src/fbx/Fbx2Raw.cpp [743:981]


static void ReadAnimations(RawModel& raw, FbxScene* pScene, const GltfOptions& options) {
  FbxTime::EMode eMode = FbxTime::eFrames24;
  switch (options.animationFramerate) {
    case AnimationFramerateOptions::BAKE24:
      eMode = FbxTime::eFrames24;
      break;
    case AnimationFramerateOptions::BAKE30:
      eMode = FbxTime::eFrames30;
      break;
    case AnimationFramerateOptions::BAKE60:
      eMode = FbxTime::eFrames60;
      break;
  }
  const double epsilon = 1e-5f;

  const int animationCount = pScene->GetSrcObjectCount<FbxAnimStack>();
  for (size_t animIx = 0; animIx < animationCount; animIx++) {
    FbxAnimStack* pAnimStack = pScene->GetSrcObject<FbxAnimStack>(animIx);
    FbxString animStackName = pAnimStack->GetName();

    pScene->SetCurrentAnimationStack(pAnimStack);

    /**
     * Individual animations are often concatenated on the timeline, and the
     * only certain way to identify precisely what interval they occupy is to
     * depth-traverse the entire animation stack, and examine the actual keys.
     *
     * There is a deprecated concept of an "animation take" which is meant to
     * provide precisely this time interval information, but the data is not
     * actually derived by the SDK from source-of-truth data structures, but
     * rather provided directly by the FBX exporter, and not sanity checked.
     *
     * Some exporters calculate it correctly. Others do not. In any case, we
     * now ignore it completely.
     */
    FbxLongLong firstFrameIndex = -1;
    FbxLongLong lastFrameIndex = -1;
    for (int layerIx = 0; layerIx < pAnimStack->GetMemberCount(); layerIx++) {
      FbxAnimLayer* layer = pAnimStack->GetMember<FbxAnimLayer>(layerIx);
      for (int nodeIx = 0; nodeIx < layer->GetMemberCount(); nodeIx++) {
        auto* node = layer->GetMember<FbxAnimCurveNode>(nodeIx);
        FbxTimeSpan nodeTimeSpan;
        // Multiple curves per curve node is not even supported by the SDK.
        for (int curveIx = 0; curveIx < node->GetCurveCount(0); curveIx++) {
          FbxAnimCurve* curve = node->GetCurve(0U, curveIx);
          if (curve == nullptr) {
            continue;
          }
          // simply take the interval as first key to last key
          int firstKeyIndex = 0;
          int lastKeyIndex = std::max(firstKeyIndex, curve->KeyGetCount() - 1);
          FbxLongLong firstCurveFrame = curve->KeyGetTime(firstKeyIndex).GetFrameCount(eMode);
          FbxLongLong lastCurveFrame = curve->KeyGetTime(lastKeyIndex).GetFrameCount(eMode);

          // the final interval is the union of all node curve intervals
          if (firstFrameIndex == -1 || firstCurveFrame < firstFrameIndex) {
            firstFrameIndex = firstCurveFrame;
          }
          if (lastFrameIndex == -1 || lastCurveFrame > lastFrameIndex) {
            lastFrameIndex = lastCurveFrame;
          }
        }
      }
    }
    RawAnimation animation;
    animation.name = animStackName;

    fmt::printf(
        "Animation %s: [%lu - %lu]\n", std::string(animStackName), firstFrameIndex, lastFrameIndex);

    if (verboseOutput) {
      fmt::printf("animation %zu: %s (%d%%)", animIx, (const char*)animStackName, 0);
    }

    for (FbxLongLong frameIndex = firstFrameIndex; frameIndex <= lastFrameIndex; frameIndex++) {
      FbxTime pTime;
      // first frame is always at t = 0.0
      pTime.SetFrame(frameIndex - firstFrameIndex, eMode);
      animation.times.emplace_back((float)pTime.GetSecondDouble());
    }

    size_t totalSizeInBytes = 0;

    const int nodeCount = pScene->GetNodeCount();
    for (int nodeIndex = 0; nodeIndex < nodeCount; nodeIndex++) {
      FbxNode* pNode = pScene->GetNode(nodeIndex);
      const FbxAMatrix baseTransform = pNode->EvaluateLocalTransform();
      const FbxVector4 baseTranslation = baseTransform.GetT();
      const FbxQuaternion baseRotation = baseTransform.GetQ();
      const FbxVector4 baseScaling = computeLocalScale(pNode);
      bool hasTranslation = false;
      bool hasRotation = false;
      bool hasScale = false;
      bool hasMorphs = false;

      RawChannel channel;
      channel.nodeIndex = raw.GetNodeById(pNode->GetUniqueID());

      for (FbxLongLong frameIndex = firstFrameIndex; frameIndex <= lastFrameIndex; frameIndex++) {
        FbxTime pTime;
        pTime.SetFrame(frameIndex, eMode);

        const FbxAMatrix localTransform = pNode->EvaluateLocalTransform(pTime);
        const FbxVector4 localTranslation = localTransform.GetT();
        const FbxQuaternion localRotation = localTransform.GetQ();
        const FbxVector4 localScale = computeLocalScale(pNode, pTime);

        hasTranslation |=
            (fabs(localTranslation[0] - baseTranslation[0]) > epsilon ||
             fabs(localTranslation[1] - baseTranslation[1]) > epsilon ||
             fabs(localTranslation[2] - baseTranslation[2]) > epsilon);
        hasRotation |=
            (fabs(localRotation[0] - baseRotation[0]) > epsilon ||
             fabs(localRotation[1] - baseRotation[1]) > epsilon ||
             fabs(localRotation[2] - baseRotation[2]) > epsilon ||
             fabs(localRotation[3] - baseRotation[3]) > epsilon);
        hasScale |=
            (fabs(localScale[0] - baseScaling[0]) > epsilon ||
             fabs(localScale[1] - baseScaling[1]) > epsilon ||
             fabs(localScale[2] - baseScaling[2]) > epsilon);

        channel.translations.push_back(toVec3f(localTranslation) * scaleFactor);
        channel.rotations.push_back(toQuatf(localRotation));
        channel.scales.push_back(toVec3f(localScale));
      }

      std::vector<FbxAnimCurve*> shapeAnimCurves;
      FbxNodeAttribute* nodeAttr = pNode->GetNodeAttribute();
      if (nodeAttr != nullptr && nodeAttr->GetAttributeType() == FbxNodeAttribute::EType::eMesh) {
        // it's inelegant to recreate this same access class multiple times, but it's also dirt
        // cheap...
        FbxBlendShapesAccess blendShapes(static_cast<FbxMesh*>(nodeAttr));

        for (FbxLongLong frameIndex = firstFrameIndex; frameIndex <= lastFrameIndex; frameIndex++) {
          FbxTime pTime;
          pTime.SetFrame(frameIndex, eMode);

          for (size_t channelIx = 0; channelIx < blendShapes.GetChannelCount(); channelIx++) {
            FbxAnimCurve* curve = blendShapes.GetAnimation(channelIx, animIx);
            float influence = (curve != nullptr) ? curve->Evaluate(pTime) : 0; // 0-100

            int targetCount = static_cast<int>(blendShapes.GetTargetShapeCount(channelIx));

            // the target shape 'fullWeight' values are a strictly ascending list of floats (between
            // 0 and 100), forming a sequence of intervals -- this convenience function figures out
            // if 'p' lays between some certain target fullWeights, and if so where (from 0 to 1).
            auto findInInterval = [&](const double p, const int n) {
              if (n >= targetCount) {
                // p is certainly completely left of this interval
                return NAN;
              }
              double leftWeight = 0;
              if (n >= 0) {
                leftWeight = blendShapes.GetTargetShape(channelIx, n).fullWeight;
                if (p < leftWeight) {
                  return NAN;
                }
                // the first interval implicitly includes all lesser influence values
              }
              double rightWeight = blendShapes.GetTargetShape(channelIx, n + 1).fullWeight;
              if (p > rightWeight && n + 1 < targetCount - 1) {
                return NAN;
                // the last interval implicitly includes all greater influence values
              }
              // transform p linearly such that [leftWeight, rightWeight] => [0, 1]
              return static_cast<float>((p - leftWeight) / (rightWeight - leftWeight));
            };

            for (int targetIx = 0; targetIx < targetCount; targetIx++) {
              if (curve) {
                float result = findInInterval(influence, targetIx - 1);
                if (!std::isnan(result)) {
                  // we're transitioning into targetIx
                  channel.weights.push_back(result);
                  hasMorphs = true;
                  continue;
                }
                if (targetIx != targetCount - 1) {
                  result = findInInterval(influence, targetIx);
                  if (!std::isnan(result)) {
                    // we're transitioning AWAY from targetIx
                    channel.weights.push_back(1.0f - result);
                    hasMorphs = true;
                    continue;
                  }
                }
              }

              // this is here because we have to fill in a weight for every channelIx/targetIx
              // permutation, regardless of whether or not they participate in this animation.
              channel.weights.push_back(0.0f);
            }
          }
        }
      }

      if (hasTranslation || hasRotation || hasScale || hasMorphs) {
        if (!hasTranslation) {
          channel.translations.clear();
        }
        if (!hasRotation) {
          channel.rotations.clear();
        }
        if (!hasScale) {
          channel.scales.clear();
        }
        if (!hasMorphs) {
          channel.weights.clear();
        }

        animation.channels.emplace_back(channel);

        totalSizeInBytes += channel.translations.size() * sizeof(channel.translations[0]) +
            channel.rotations.size() * sizeof(channel.rotations[0]) +
            channel.scales.size() * sizeof(channel.scales[0]) +
            channel.weights.size() * sizeof(channel.weights[0]);
      }

      if (verboseOutput) {
        fmt::printf(
            "\ranimation %d: %s (%d%%)",
            animIx,
            (const char*)animStackName,
            nodeIndex * 100 / nodeCount);
      }
    }

    raw.AddAnimation(animation);

    if (verboseOutput) {
      fmt::printf(
          "\ranimation %d: %s (%d channels, %3.1f MB)\n",
          animIx,
          (const char*)animStackName,
          (int)animation.channels.size(),
          (float)totalSizeInBytes * 1e-6f);
    }
  }
}