【问题标题】:Skinning Animation - Weights Destroy Mesh蒙皮动画 - 权重破坏网格
【发布时间】:2020-10-10 19:22:01
【问题描述】:

我正在使用自己的 Collada 解析器编写动画系统,但遇到了一个我无法解决的问题。

我收集了我的网格/蒙皮信息(顶点、法线、jointIds、权重等)、我的骨架信息(关节、它们的局部变换、反向绑定位置、层次结构) ,以及我的动画(每个关节的关键帧变换位置,时间戳)

我的问题是,计算所有内容,然后在着色器中实现(权重总和乘以联合变换和顶点位置) - 我得到以下结果:

当我移除权重乘法时,网格仍然完好无损 - 但是皮肤实际上并没有跟随动画。我很迷茫,因为我觉得数学好像是正确的,但很明显我在某个地方出错了。有人能阐明我误解的方面吗?

这是我目前的理解和实现:

  1. 在收集了所有关节的局部变换和层次结构后,我计算了它们的逆绑定变换矩阵。为此,我将每个关节 localTransform 与它们的 parentLocalTransform 相乘以获得一个 bindTransform。反转该 bindTransform 会导致它们的 inverseBindTransform。下面是我的代码:
    // Recursively collect each Joints InverseBindTransform - 
    // root joint's local position is an identity matrix. 
    // Function is only called once after data collection.
    void Joint::CalcInverseBindTransform(glm::mat4 parentLocalPosition)
    {
        glm::mat4 bindTransform = parentLocalPosition * m_LocalTransform;
        m_InverseBindPoseMatrix = glm::inverse(bindTransform);

        for (Joint child : Children) {
            child.CalcInverseBindTransform(bindTransform);
        }
    }
  1. 在动画制作过程中,对于每个关节,我将两个 JointTransform 用于两个帧,我的 currentTime 介于两者之间,并计算插值的 JointTransform。 (JointTransform 简单地有一个用于位置的 vec3 和用于旋转的四元数)。我对每个关节执行此操作,然后通过再次递归地将新的 frameLocalTransform 与它们的 parentLocalTransform 相乘,将这些插值应用于每个关节。我采用该 bindTransform 并将其乘以 invBindTransform ,然后转置矩阵。下面是代码:
    std::unordered_map<int, glm::mat4> Animator::InterpolatePoses(float time) {

        std::unordered_map<int, glm::mat4> poses;
        if (IsPlaying()) {

            for (std::pair<int, JointTransform> keyframe : m_PreviousFrame.GetJointKeyFrames()) {

                JointTransform previousFrame = m_PreviousFrame.GetJointKeyFrames()[keyframe.first];
                JointTransform nextFrame = m_NextFrame.GetJointKeyFrames()[keyframe.first];
                JointTransform interpolated = JointTransform::Interpolate(previousFrame, nextFrame, time);
                poses[keyframe.first] = interpolated.getLocalTransform();
            }
        }
        return poses;
    }

    void Animator::ApplyPosesToJoints(std::unordered_map<int, glm::mat4> newPose, Joint* j, glm::mat4 parentTransform) 
    {
        if (IsPlaying()) {

            glm::mat4 currentPose = newPose[j->GetJointId()];
            glm::mat4 modelSpaceJoint = parentTransform * currentPose;

            for (Joint child : j->GetChildren()) {
                ApplyPosesToJoints(newPose, &child, modelSpaceJoint);
            }

            modelSpaceJoint = glm::transpose(j->GetInvBindPosition() * modelSpaceJoint);
            j->SetAnimationTransform(modelSpaceJoint);
        }
    }
  1. 然后我为每个关节收集所有新的 AnimatedTransform 并将它们发送到着色器:
    void AnimationModel::Render(bool& pass)
    {
                    [...]
        std::vector<glm::mat4> transforms = GetJointTransforms();
        for (int i = 0; i < transforms.size(); ++i) {
            m_Shader->SetMat4f(transforms[i], ("JointTransforms[" + std::to_string(i) + "]").c_str());
        }
                    [...]
    }

    void AnimationModel::AddJointsToArray(Joint current, std::vector<glm::mat4>& matrix)
    {
        glm::mat4 finalMatrix = current.GetAnimatedTransform();
        matrix.push_back(finalMatrix);
        for (Joint child : current.GetChildren()) {
            AddJointsToArray(child, matrix);
        }
    }
  1. 在着色器中,我只是按照在研究这个主题时可以在网上找到的求和公式:
        for (int i = 0; i < total_weight_amnt; ++i) {
            mat4 jointTransform = JointTransforms[jointIds[i]];

            vec4 newVertexPos = jointTransform * vec4(pos, 1.0);
            total_pos += newVertexPos * weights[i];
                        [...]

--------- 回复归一化权重------------

有几个权重总和超过 1,但在解决了我的代码中的错误后,模型看起来像这样:

为了计算权重——我循环遍历向量中所有预先添加的权重,如果我发现一个权重小于我要添加的权重——我在那个位置替换那个权重。否则,我将权重附加到向量的末尾。如果我的向量中的权重比我指定的 max_weights(即 4)少 - 我用 0 填充剩余的权重/jointIds。

【问题讨论】:

  • 您的体重值总和是否为适当的值(通常为 0、1 或 N)? total_pos计算后是否需要调整(除以N)?
  • 我进行了权重计算 - 我确实注意到检查权重向量总和高于 1 的代码中存在错误。我修复了它并确认没有超过 1 的权重向量。出现了更多的几何图形-但是在我的数学中似乎直到其他地方出现错误。我会在帖子里发一张照片。

标签: c++ animation opengl graphics 3d


【解决方案1】:

我了解,当蒙皮动画出现问题时,可能有很多不同的区域出现问题。因此,对于未来遇到同样问题的谷歌员工 - 将其视为您可能做错的建议列表,而不是绝对做错的建议列表。

对于我的问题 - 我在很多次要领域都有正确的想法但错误的方法。这让我非常接近,但正如他们所说,没有雪茄。

  1. 我不需要自己计算 Inverse Bind Pose,Collada 的 Inverse Bind Pose(有时/经常声明为“offsetMatrix”)非常完美。这不是问题,因为我只是在做不必要的计算。

  2. 在 Collada 文件中,它们通常在层次结构中为您提供比动画所需的更多的“关节”或“节点”。在实际动画“关节”开始之前,有场景和初始骨架“节点”类型。场景通常是一个单位矩阵,在读取 Collada 文件时根据您的“上轴”进行操作。 Node 类型将决定骨架中每个关节的整体大小——所以如果它没有调整大小,它可能是单位矩阵。确保您的层次结构仍然包含层次结构中列出的 ALL 节点/关节。我非常没有这样做 - 这极大地扭曲了我的 globalPosition (BindPose)。

  3. 如果您通过四元数表示关节的变换旋转(强烈推荐),请确保在两个旋转位置之间插值后对结果四元数进行归一化。 同样,在将 Rotation 和 Transform 组合到最终矩阵中时,请确保您的乘法顺序和最终输出是正确的。

  4. 最后 - 你的最后一个蒙皮矩阵由你的关节 InvBindMatrix * GlobalPosition * GlobalInverseRootTransform 组成(

有了这个 - 我能够成功地为我的模型制作动画!

最后一点 - 我的网格和动画文件是分开添加的。如果您的动画位于与网格不同的文件中,请确保从包含动画的文件而不是包含网格的文件中收集蒙皮/关节信息。我列出了加载模型的步骤,然后通过不同的文件为其提供多个动画:

  1. 加载到网格中(这包含顶点、法线、TexCoords、JointIds、Weights)
  2. 加载动画文件(这会提供 Skeleton、InverseBindPositions 和其他将骨架绑定到网格所需的信息)- 收集骨架和绑定信息后,还要从该文件中收集第一个动画信息。
  3. 对于另一个动画,上述骨架应该适用于同一网格/模型上的任何其他动画 - 只需读取动画信息并存储在您选择的数据结构中。重复第 3 步,直到满意为止。

【讨论】:

    猜你喜欢
    • 2013-10-01
    • 2020-02-16
    • 2018-01-05
    • 2019-08-23
    • 2013-06-26
    • 2018-01-12
    • 2022-11-21
    • 2020-11-08
    • 2020-05-04
    相关资源
    最近更新 更多