Skeletal Animations. (I don't understand anything isn't there a simpler way on how to do this ?)

Hi! I just read this tutorial : https://learnopengl.com/Guest-Articles/2020/Skeletal-Animation
But I find this is a very complicated way to animate models. Would do be more simple to load one model per keyframe and then doing a linear interpolation in the animation class like this :

#include "../../../include/odfaeg/Graphics/anim.h"
namespace odfaeg {
    namespace graphic {
        using namespace sf;
        using namespace std;
        Anim::Anim(EntityFactory& factory) : AnimatedEntity (math::Vec3f(0, 0, 0), math::Vec3f(0, 0, 0), math::Vec3f(0, 0, 0), "E_ANIMATION", factory, "", nullptr) {
            currentFrameIndex = 0;
            running = false;
            currentFrameChanged = false;
            currentFrame = nullptr;
            previousFrame = nullptr;
            nextFrame = nullptr;
            loop = false;
            interpLevels = 1;
            interpPerc = 0;
            interpolatedFrame = std::make_unique<Mesh>(math::Vec3f(0, 0, 0), math::Vec3f(0, 0, 0), "E_ANIMATION_FRAME", factory);
            interpolatedFrame->setParent(this);
            auname = "";
            fr = 0.1f;
            //interpolatedFrame->setName("E_ANIMATION_FRAME");
        }
        Anim::Anim (float fr, math::Vec3f position, math::Vec3f size, EntityFactory& factory, Entity *parent) : AnimatedEntity (position, size, size * 0.5f, "E_ANIMATION", factory, "", parent) {
            this->fr = fr;
            currentFrameIndex = 0;
            running = false;
            currentFrameChanged = false;
            currentFrame = nullptr;
            previousFrame = nullptr;
            nextFrame = nullptr;
            loop = false;
            interpLevels = 1;
            interpPerc = 0;
            interpolatedFrame = std::make_unique<Mesh>(position, size, "E_ANIMATION_FRAME", factory);
            interpolatedFrame->setParent(this);
            auname = "";
            //interpolatedFrame->setName("E_ANIMATION_FRAME");
        }
        bool Anim::isCurrentFrameChanged() {
            return currentFrameChanged;
        }
        void Anim::setCurrentFrameChanged(bool b) {
            currentFrameChanged = b;
        }
        void Anim::addFrame (Entity *entity) {
            entity->setParent(this);
            if (getChildren().size() == 0) {
                currentFrame = entity;
                interpolatedFrame->setName("IFRAME");
                createFirstInterpolatedFrame(entity);
            }
            else if (getChildren().size() == 1)
                nextFrame = entity;
            addChild(entity);
        }
        void Anim::removeFrame (Entity *entity) {
            removeChild(entity);
        }
        int Anim::getCurrentFrameIndex () {
            return currentFrameIndex;
        }
        void Anim::setCurrentFrame (int index) {
            if (getChildren().size() >= 2) {
                previousFrame = getChildren()[(index -1 < 0) ? 0 : index - 1];
                currentFrame = getChildren()[index];
                nextFrame = getChildren()[(index + 1 >= getChildren().size()) ? 0 : index + 1];
                currentFrameIndex = index;
                currentFrameChanged = true;
                changeInterpolatedFrame(currentFrame);
            }
        }
        Entity* Anim::getPreviousFrame() {
            return previousFrame;
        }
        void Anim::setFrameRate (float fr) {
            this->fr = fr;
        }
        bool Anim::isRunning () {

            return running;
        }
        void Anim::play (bool loop) {
            if (getChildren().size() > 1 && !running) {
                running = true;
                //std::cout<<"play : "<<running<<std::endl;
                this->loop = loop;
            }
        }
        void Anim::setSelected(bool selected) {
            //std::cout<<"set selected : "<<selected<<std::endl;
            Entity::setSelected(selected);
            interpolatedFrame->setSelected(selected);
        }
        void Anim::setLayer(unsigned int layer) {
            interpolatedFrame->setLayer(layer);
        }
        void Anim::stop (){
            if (running) {

                running = false;
                loop = false;
            }
        }
        void Anim::computeNextFrame () {

            onFrameChanged();
        }
        void Anim::onFrameChanged() {
            if (getChildren().size() >= 2) {

                if (previousFrame == nullptr) {
                    previousFrame = getChildren()[(currentFrameIndex - 1 < 0) ? 0 : getChildren().size() - 1];
                }
                interpPerc++;
                if (interpPerc >= interpLevels) {
                    previousFrame = getChildren()[currentFrameIndex];
                    currentFrameChanged = true;
                    currentFrameIndex++;
                    if (currentFrameIndex >= getChildren().size()) {
                        currentFrameIndex = 0;
                        if (!loop) {
                            running = false;
                        }
                    }

                    currentFrame = getChildren()[currentFrameIndex];
                    nextFrame = getChildren()[(currentFrameIndex + 1 >= getChildren().size()) ? 0 : currentFrameIndex+1];
                    interpPerc = 0;
                    changeInterpolatedFrame(currentFrame);
                }
                interpolate(currentFrame, nextFrame);
            }
        }
        void Anim::createFirstInterpolatedFrame (Entity* currentFrame) {
            if (currentFrame->getChildren().size() > 0) {
                for (unsigned int i = 0; i < currentFrame->getChildren().size(); i++) {
                    createFirstInterpolatedFrame(currentFrame->getChildren()[i]);
                }
            }
            if (currentFrame->isLeaf()) {
                for (unsigned int i = 0; i < currentFrame->getFaces().size(); i++) {
                    VertexArray va = currentFrame->getFaces()[i].getVertexArray();
                    Face face (va,currentFrame->getFaces()[i].getMaterial(), currentFrame->getTransform());
                    interpolatedFrame->addFace(face);
                }
                /*interpolatedFrame->setSize(currentFrame->getSize());
                interpolatedFrame->setRotation(currentFrame->getRotation());
                interpolatedFrame->setPosition(currentFrame->getPosition());*/
                //addChild(interpolatedFrame);
                /*if (interpolatedFrame->getRootType() == "E_ANIMATION")
                    std::cout<<"interpolated frame type : "<<interpolatedFrame->getType()<<std::endl;*/
            }
        }
        void Anim::changeInterpolatedFrame(Entity* currentFrame) {
            if (currentFrame->getChildren().size() > 0) {
                for (unsigned int i = 0; i < currentFrame->getChildren().size(); i++) {
                    changeInterpolatedFrame(currentFrame->getChildren()[i]);
                }
            }
            if (currentFrame->isLeaf()) {
                interpolatedFrame->getFaces().clear();
                for (unsigned int i = 0; i < currentFrame->getFaces().size(); i++) {
                    VertexArray va = currentFrame->getFaces()[i].getVertexArray();
                    Face face (va,currentFrame->getFaces()[i].getMaterial(), currentFrame->getTransform());
                    interpolatedFrame->addFace(face);
                }
                /*interpolatedFrame->setSize(currentFrame->getSize());
                interpolatedFrame->setRotation(currentFrame->getRotation());
                interpolatedFrame->setPosition(currentFrame->getPosition());*/
            }
        }
        void Anim::interpolate(Entity* currentFrame, Entity* nextFrame) {
            if (currentFrame->getNbChildren() == nextFrame->getNbChildren()) {
                if (currentFrame->getNbChildren() != 0 && nextFrame->getNbChildren() != 0) {
                    for (unsigned int i = 0; i < currentFrame->getNbChildren(); i++) {
                         interpolate(currentFrame->getChild(i), nextFrame->getChild(i));
                    }
                }
                if (currentFrame->getNbFaces() == nextFrame->getNbFaces()
                    && currentFrame->getNbFaces() == interpolatedFrame->getNbFaces()) {
                    for (unsigned int i = 0; i < currentFrame->getNbFaces(); i++) {
                        const VertexArray& cva = currentFrame->getFace(i)->getVertexArray();
                        VertexArray& iva = interpolatedFrame->getFace(i)->getVertexArray();
                        const VertexArray& nva = nextFrame->getFace(i)->getVertexArray();
                        if (cva.getVertexCount() == nva.getVertexCount()) {
                            for (unsigned int j = 0; j < cva.getVertexCount(); j++) {
                                iva[j].position.x = cva[j].position.x + (nva[j].position.x - cva[j].position.x) * (interpPerc / interpLevels);
                                iva[j].position.y = cva[j].position.y + (nva[j].position.y - cva[j].position.y) * (interpPerc / interpLevels);
                                iva[j].position.z = cva[j].position.z + (nva[j].position.z - cva[j].position.z) * (interpPerc / interpLevels);
                                iva[j].color = cva[j].color;
                                iva[j].texCoords = cva[j].texCoords;
                                //std::cout<<"position "<<iva[j].position.x<<","<<iva[j].position.y<<","<<iva[j].position.z<<std::endl;
                                /*if (currentFrame->getRootType() == "E_MONSTER")
                                    std::cout<<"interpolation tex coords : "<<interpolatedFrame->getFace(i)->getVertexArray()[j].texCoords.x<<","<<interpolatedFrame->getFace(i)->getVertexArray()[j].texCoords.y<<std::endl;*/
                            }
                            interpolatedFrame->getFace(i)->setMaterial(currentFrame->getFace(i)->getMaterial());
                            interpolatedFrame->getFace(i)->setTransformMatrix(currentFrame->getFace(i)->getTransformMatrix());
                        }
                    }
                }
            }
        }
        Time Anim::getElapsedTime () {
            return clock.getElapsedTime();
        }
        void Anim::resetClock () {
            clock.restart();
        }
        bool Anim::operator== (Entity &other) {

            if (other.getType() != "E_ANIMATION")
                return false;
            Anim &a = static_cast<Anim&> (other);
            if (getPosition().x != a.getPosition().x || getPosition().y != a.getPosition().y
                || getSize().x != a.getSize().x || getSize().y != a.getSize().y)
                return false;
            for (unsigned int i = 0; i < getChildren().size(); i++) {
                if (!(*getChildren()[i] == *a.getChildren()[i]))
                    return false;
            }
            return true;
        }

        float Anim::getFrameRate () {
            return fr;
        }
        Entity* Anim::getCurrentFrame () const {
              return interpolatedFrame.get();
        }
        void Anim::onDraw(RenderTarget &target, RenderStates states) {
             target.draw(*interpolatedFrame, states);
        }
        void Anim::onMove(math::Vec3f& t) {
            GameObject::onMove(t);
            interpolatedFrame->move(t);
        }
        void Anim::onRotate(float angle) {
            GameObject::onRotate(angle);
            interpolatedFrame->rotate(angle);
        }
        void Anim::onScale(math::Vec3f& s) {
            GameObject::onScale(s);
            interpolatedFrame->scale(s);
        }
        Entity* Anim::clone() {
            Anim* anim = factory.make_entity<Anim>(factory);
            GameObject::copy(anim);
            anim->fr = fr;
            anim->currentFrameIndex = currentFrameIndex;
            anim->currentFrame = currentFrame;
            anim->previousFrame = previousFrame;
            anim->nextFrame = nextFrame;
            anim->loop = loop;
            anim->interpLevels = interpLevels;
            anim->interpPerc = interpPerc;
            anim->interpolatedFrame.reset(interpolatedFrame->clone());
            return anim;
        }
        void Anim::setAnimUpdater(std::string name) {
            auname = name;
        }
        std::string Anim::getAnimUpdater() {
            return auname;
        }
        Anim::~Anim () {

        }
    }
}

This is what the md2 model format does.

But the problem is that on website when downloading animations all files are skeletal animations.

I haven’t read your code and am not really familiar with the way md2 stored animations, but one alternative to skeletal animations is to use morph targets (sometimes also called blend shapes). It basically means storing multiple copies of the geometry in various poses (or key frames) and at runtime you blend between two (or more) of them based on weights.This is sometimes used for facial animation, but for many other things skeletal animation has become the norm.

Some reasons for that are:

  • model detail keeps increasing and morph targets require storing a full copy of (at least) position, normal, and tangent for each key frame.
  • the same skeletal animation can be applied to many meshes as long as they are skinned to the same (or similar enough) skeleton.
  • that also means animators can work before the model is finalized allowing to parallelize the asset production process; and changes to the model do not invalidate animation work
  • for models with LODs (level of detail) each LOD needs to be animated (and the data stored), where you can apply the same skeletal animation to a lower detail model (possibly skipping the evaluation of some bones that are only relevant to a more detailed version of the model)

In other words, skeletal animation isn’t the only way, but it has advantages that make it a popular approach.

These are very good tutorials:

It is Java and I do not know Java. But I have ported the tutorial’s code to Python + Pygame, and JavaScript + WebGL. Demo in the browser in pure WebGL 1.0: Hagrid Hip-Hop Dancing, WebGL 1.0 and JavaScript
hagrid-hip-hop-dancing-opengles20-pygame2

Deal with it. If you intend to learn 3D graphics, there are far more complex things than skeletal animation.

Vertex animation (like MD2) was only used in the first few years of 3D gaming (up to around 2000) because it’s less CPU-intensive than skeletal animation. It doesn’t have any advantage beyond that, so once PCs became more powerful, everyone switched to skeletal animation. The only place you’re going to find vertex animations is in 25+ year-old games. Or if you write your own tool to generate vertex animations by “sampling” skeletal animations, which is how those games did it. Note that you can’t just convert the keyframes; linear interpolation between keyframe meshes will produce an unacceptable level of distortion.

Ok I’ve read the tutorial several time and now I undestand better how to implement that in my engine, but I’ll not do vertex transformations in the vertex shader like on the tutorial, because I batch the vertices so I can’t get the array of transform matrices from the animator otherwise I have to do one draw call per animation and changind the uniform and I don’t like to do that so I’ll make the transformation on the CPU. So I need to store boneIds and boneWeights in another class than the vertex because I want to draw all animations with on draw call.

otherwise I have to do one draw call per animation and changind the uniform and I don’t like to do that so I’ll make the transformation on the CPU.

And you believe that doing hundreds of thousands of matrix multiplies on vertices on the CPU and transferring a megabyte of data to the GPU every frame will be faster than the overhead of making more than one draw call?

Speaking of which:

I want to draw all animations with on draw call.

Nothing is stopping you from doing that now. You can put all of the animation matrices used in the scene in one big buffer object. You then make a multi-draw call and use the draw index to pick out the matrices needed for that specific draw call.

Yes you’re right but I need one buffer for every matrices but I’ve one matrix array per animated entity so I need a second buffer : an offset buffer to indicate where is the first matrix of the animation in the matrix buffer. Than I use gl_DrawID to get the offset and doing something like this :

uint offset = offsets[gl_DrawID];
mat4 matrix = boneMatrices[offset+boneID];

Not tested yet but it should work.
Because I can’t use a 2 dimention array as a buffer because buffers are always one dimention array so I need an offset buffer.