After some time, I had time to focus on my game engine and I implemented skeletal meshes, their animations and socket components.

Implementing Skeletal Meshes

Importing FBX files

The first thing I needed to do was import FBX files. While I plan to implement my own FBX importer for a more optimized solution, for now I’m using assimp, and it works just fine.

Skeletal Meshes

Skeletal meshes are essentially static meshes with bones that control some portion of them. Bones are components that have a position, rotation, and scaling, just like any game object component. We know the bone’s transformations, but we also need to know which bone affects which part of the mesh. For this purpose, there is another value for every vertex that holds the magnitude of affection of the bones. It is necessary to hold these weight values in an array because vertices are generally controlled by more than one bone. I have found that it is common practice to hold four bone data values for every vertex. The count of the bone data is important because it directly affects performance.

Socket Components

I have implemented socket components in order to attach other components and game objects to bones. This is necessary for a lot of purposes such as making a skeletal mesh hold a bow or a torch etc, and widely used in games.

Bone Manipulation

I have been working experimentally on bone transformation matrix manipulation. For now, I have only implemented setting a bone’s transformation on each frame with a matrix pointer. This step has helped me to better understand bone matrices and what can be done with them. Since I created an archer scene for testing purposes, I used bone manipulation for the bow string during the drawing process.

Result

Implementations

I have implemented armature and bone classes to hold the relationship between bones.

offset matrix in Bone class holds the transformation of a bone in bind pose.

struct GOKNAR_API Bone
{
    Bone(const std::string& n) : name(n) {}
    Bone(const std::string& n, const Matrix& o) : name(n), offset(o) {}
    Bone(const std::string& n, const Matrix& o, const Matrix& t) : name(n), offset(o), transformation(t) {}
    Bone(const Bone& rhs) : offset(rhs.offset), transformation(rhs.transformation) {}

    std::string name;

    Matrix offset{ Matrix::IdentityMatrix };
    Matrix transformation{ Matrix::IdentityMatrix };

    std::vector<Bone*> children;
};

struct GOKNAR_API Armature
{
    Bone* root{ nullptr };
    Matrix globalInverseTransform{ Matrix::IdentityMatrix };
};

I have implemented a VertexBoneData struct for holding bone data I previously mentioned, and every vertex of a skeletal mesh has a variable of this type.

#define MAX_BONE_SIZE_PER_VERTEX 4
struct GOKNAR_API VertexBoneData
{
    void AddBoneData(unsigned int id, float weight)
    {
        int smallestIndex = -1;
        float largestDifference = 0.f;
        for (unsigned int i = 0; i < MAX_BONE_SIZE_PER_VERTEX; ++i)
        {
            if (boneIDs[i] == id && weights[i] != 0.f)
            {
                return;
            }

            float difference = weight - weights[i];
            if (largestDifference < difference)
            {
                largestDifference = difference;
                smallestIndex = i;
            }
        }

        if (0 <= smallestIndex)
        {
            boneIDs[smallestIndex] = id;
            weights[smallestIndex] = weight;
        }
    }

    unsigned int boneIDs[MAX_BONE_SIZE_PER_VERTEX] = { 0 };
    float weights[MAX_BONE_SIZE_PER_VERTEX] = { 0.f };
};

Skeletal Mesh Class

class GOKNAR_API SkeletalMesh : public StaticMesh
{
public:
    SkeletalMesh();

    virtual ~SkeletalMesh();

    virtual void Init();

    void ResizeVertexToBonesArray(unsigned int size)
    {
        vertexBoneDataArray_->resize(size);
    }

    void AddVertexBoneData(unsigned int index, unsigned int id, float weight)
    {
        vertexBoneDataArray_->at(index).AddBoneData(id, weight);
    }

    void AddBone(Bone* bone)
    {
        bones_.push_back(bone);
        ++boneSize_;
    }

    unsigned int GetBoneSize() const
    {
        return boneSize_;
    }
    
    int GetBoneId(const std::string& boneName)
    {
        int boneId = 0;

        if (boneNameToIdMap_->find(boneName) == boneNameToIdMap_->end())
        {
            boneId = boneNameToIdMapSize_;
            (*boneNameToIdMap_)[boneName] = boneId;
            ++boneNameToIdMapSize_;
        }
        else
        {
            boneId = boneNameToIdMap_->at(boneName);
        }

        return boneId;
    }

    const VertexBoneDataArray* GetVertexBoneDataArray() const
    {
        return vertexBoneDataArray_;
    }

    const BoneNameToIdMap* GetBoneNameToIdMap() const
    {
        return boneNameToIdMap_;
    }

    Armature* GetArmature()
    {
        return armature_;
    }

    Bone* GetBone(unsigned int index)
    {
        return bones_[index];
    }

    void GetBoneTransforms(std::vector<Matrix>& transforms, const SkeletalAnimation* skeletalAnimation, float time, std::unordered_map<std::string, SocketComponent*>& socketMap);

    void AddSkeletalAnimation(SkeletalAnimation* skeletalAnimation)
    {
        skeletalAnimations_.push_back(skeletalAnimation);
        if (nameToSkeletalAnimationMap_.find(skeletalAnimation->name) == nameToSkeletalAnimationMap_.end())
        {
            nameToSkeletalAnimationMap_[skeletalAnimation->name] = skeletalAnimation;
        }
    }

    const SkeletalAnimation* GetSkeletalAnimation(const std::string& name)
    {
        if (nameToSkeletalAnimationMap_.find(name) != nameToSkeletalAnimationMap_.end())
        {
            return nameToSkeletalAnimationMap_[name];
        }

        return nullptr;
    }

private:
    void SetupTransforms(Bone* bone, const Matrix& parentTransform, std::vector<Matrix>& transforms, const SkeletalAnimation* skeletalAnimation, float time, std::unordered_map<std::string, SocketComponent*>& socketMap);

    VertexBoneDataArray* vertexBoneDataArray_{ new VertexBoneDataArray() };
    BoneNameToIdMap* boneNameToIdMap_{ new BoneNameToIdMap() };

    SkeletalAnimationVector skeletalAnimations_;
    NameToSkeletalAnimationMap nameToSkeletalAnimationMap_;

    std::vector<Bone*> bones_;
    Armature* armature_{ new Armature() };

    unsigned int boneNameToIdMapSize_{ 0 };
    unsigned int boneSize_{ 0 };
};
SkeletalMesh::~SkeletalMesh()
{
	int skeletalAnimationSize = skeletalAnimations_.size();
	for (unsigned int skeletalAnimationIndex = 0; skeletalAnimationIndex < skeletalAnimationSize; ++skeletalAnimationIndex)
	{
		delete skeletalAnimations_[skeletalAnimationIndex];
	}

	int bonesSize = bones_.size();
	for (unsigned int boneIndex = 0; boneIndex < bonesSize; ++boneIndex)
	{
		delete bones_[boneIndex];
	}

	delete vertexBoneDataArray_;
	delete boneNameToIdMap_;
	delete armature_;
}

void SkeletalMesh::Init()
{
	MeshUnit::Init();

	engine->AddSkeletalMeshToRenderer(this);
}

void SkeletalMesh::GetBoneTransforms(std::vector<Matrix>& transforms, const SkeletalAnimation* skeletalAnimation, float time, std::unordered_map<std::string, SocketComponent*>& socketMap)
{
	SetupTransforms(armature_->root, Matrix::IdentityMatrix, transforms, skeletalAnimation, time, socketMap);
}

void SkeletalMesh::SetupTransforms(Bone* bone, const Matrix& parentTransform, std::vector<Matrix>& transforms, const SkeletalAnimation* skeletalAnimation, float time, std::unordered_map<std::string, SocketComponent*>& socketMap)
{
	if (!bone)
	{
		return;
	}

	Vector3 interpolatedPosition = Vector3::ZeroVector;
	Vector3 interpolatedScaling = Vector3(1.f);
	Quaternion interpolatedRotation = Quaternion::Identity;

	Matrix boneTransformation = bone->transformation;
	if (skeletalAnimation)
	{
		SkeletalAnimationNode* skeletalAnimationNode = skeletalAnimation->affectedBoneNameToSkeletalAnimationNodeMap.at(bone->name);

		if (skeletalAnimationNode)
		{
			skeletalAnimationNode->GetInterpolatedPosition(interpolatedPosition, time);
			skeletalAnimationNode->GetInterpolatedRotation(interpolatedRotation, time);
			skeletalAnimationNode->GetInterpolatedScaling(interpolatedScaling, time);

			boneTransformation = Matrix::GetTransformationMatrix(interpolatedRotation, interpolatedPosition, interpolatedScaling);
		}
	}

	Matrix globalTransformation = parentTransform * boneTransformation;

	Matrix transform = globalTransformation * bone->offset;

	if (socketMap.find(bone->name) != socketMap.end())
	{
		socketMap[bone->name]->SetBoneTransformationMatrix(globalTransformation);
	}

	transforms[(*boneNameToIdMap_)[bone->name]] = transform;
	unsigned int childrenSize = bone->children.size();
	for (unsigned int childIndex = 0; childIndex < childrenSize; ++childIndex)
	{
		SetupTransforms(bone->children[childIndex], globalTransformation, transforms, skeletalAnimation, time, socketMap);
	}
}

OpenGL Side

I have implemented a new buffer for skeletal meshes because skeletal mesh specific variables are needed only with skeletal meshes like bone id and bone weight variables.

class GOKNAR_API Renderer
{
...
	void BindSkeletalVBO();
	void SetAttribPointersForSkeletalMesh();


	std::vector<SkeletalMesh*> skeletalMeshes_;

	std::vector<SkeletalMeshInstance*> opaqueSkeletalMeshInstances_;
	std::vector<SkeletalMeshInstance*> translucentSkeletalMeshInstances_;

	GEuint skeletalVertexBufferId_;
	GEuint skeletalIndexBufferId_;
...
};
void Renderer::BindSkeletalVBO()
{
	glBindBuffer(GL_ARRAY_BUFFER, skeletalVertexBufferId_);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, skeletalIndexBufferId_);
	SetAttribPointersForSkeletalMesh();
}

void Renderer::SetAttribPointersForSkeletalMesh()
{
	GEsizei sizeOfSkeletalMeshVertexData = (GEsizei)(sizeof(VertexData) + sizeof(VertexBoneData));

	long long offset = 0;
	// Vertex color
	glEnableVertexAttribArray(VERTEX_COLOR_LOCATION);
	glVertexAttribPointer(VERTEX_COLOR_LOCATION, 4, GL_FLOAT, GL_FALSE, sizeOfSkeletalMeshVertexData, (void*)offset);
	offset += sizeof(VertexData::color);

	// Vertex position
	glEnableVertexAttribArray(VERTEX_POSITION_LOCATION);
	glVertexAttribPointer(VERTEX_POSITION_LOCATION, 3, GL_FLOAT, GL_FALSE, sizeOfSkeletalMeshVertexData, (void*)offset);
	offset += sizeof(VertexData::position);

	// Vertex normal
	glEnableVertexAttribArray(VERTEX_NORMAL_LOCATION);
	glVertexAttribPointer(VERTEX_NORMAL_LOCATION, 3, GL_FLOAT, GL_FALSE, sizeOfSkeletalMeshVertexData, (void*)offset);
	offset += sizeof(VertexData::normal);

	// Vertex UV
	glEnableVertexAttribArray(VERTEX_UV_LOCATION);
	glVertexAttribPointer(VERTEX_UV_LOCATION, 2, GL_FLOAT, GL_FALSE, sizeOfSkeletalMeshVertexData, (void*)offset);
	offset += sizeof(VertexData::uv);

	// Bone ID
	glEnableVertexAttribArray(BONE_ID_LOCATION);
	glVertexAttribIPointer(BONE_ID_LOCATION, MAX_BONE_SIZE_PER_VERTEX, GL_UNSIGNED_INT, sizeOfSkeletalMeshVertexData, (void*)offset);
	offset += sizeof(VertexBoneData::boneIDs);

	// Bone Weight
	glEnableVertexAttribArray(BONE_WEIGHT_LOCATION);
	glVertexAttribPointer(BONE_WEIGHT_LOCATION, MAX_BONE_SIZE_PER_VERTEX, GL_FLOAT, GL_FALSE, sizeOfSkeletalMeshVertexData, (void*)offset);
}

void Renderer::Render()
{
...
	// Skeletal MeshUnit Instances
	{
		if (0 < totalSkeletalMeshCount_)
		{
			BindSkeletalVBO();

			for (SkeletalMeshInstance* opaqueSkeletalMeshInstance : opaqueSkeletalMeshInstances_)
			{
				if (!opaqueSkeletalMeshInstance->GetIsRendered()) continue;

				// TODO_Baris: Solve mesh instancing to return the exact class type and remove dynamic_cast here for performance
				const SkeletalMesh* skeletalMesh = dynamic_cast<SkeletalMesh*>(opaqueSkeletalMeshInstance->GetMesh());
				opaqueSkeletalMeshInstance->Render();

				int facePointCount = skeletalMesh->GetFaceCount() * 3;
				glDrawElementsBaseVertex(GL_TRIANGLES, facePointCount, GL_UNSIGNED_INT, (void*)(unsigned long long)skeletalMesh->GetVertexStartingIndex(), skeletalMesh->GetBaseVertex());
			}

			for (SkeletalMeshInstance* maskedSkeletalMeshInstance : maskedSkeletalMeshInstances_)
			{
				if (!maskedSkeletalMeshInstance->GetIsRendered()) continue;


				const SkeletalMesh* skeletalMesh = dynamic_cast<SkeletalMesh*>(maskedSkeletalMeshInstance->GetMesh());
				maskedSkeletalMeshInstance->Render();

				int facePointCount = skeletalMesh->GetFaceCount() * 3;
				glDrawElementsBaseVertex(GL_TRIANGLES, facePointCount, GL_UNSIGNED_INT, (void*)(unsigned long long)skeletalMesh->GetVertexStartingIndex(), skeletalMesh->GetBaseVertex());
			}
		}
	}

...
}

GLSL Side

#version 440 core

layout(location = 0) in vec4 color;
layout(location = 1) in vec3 position;
layout(location = 2) in vec3 normal;
layout(location = 3) in vec2 uv;
layout(location = 4) in ivec4 boneIDs;
layout(location = 5) in vec4 weights;

#define MAX_BONE_SIZE_PER_VERTEX 4

uniform mat4 bones[65];

uniform mat4 worldTransformationMatrix;
uniform mat4 relativeTransformationMatrix;
uniform mat4 viewMatrix;
uniform mat4 projectionMatrix;
uniform float deltaTime;
uniform float elapsedTime;

out vec4 fragmentPosition;
out vec3 vertexNormal;
out vec2 textureUV;

void main()
{
	mat4 boneTransformationMatrix = mat4(0.f);
	for(int boneIndex = 0; boneIndex < MAX_BONE_SIZE_PER_VERTEX; ++boneIndex)
	{
		boneTransformationMatrix+= bones[boneIDs[boneIndex]] * weights[boneIndex];
	}

	mat4 modelMatrix = boneTransformationMatrix * relativeTransformationMatrix * worldTransformationMatrix;
	vec4 fragmentPosition4Channel = vec4(position, 1.f) * modelMatrix;
	gl_Position = projectionMatrix * viewMatrix * fragmentPosition4Channel;
	fragmentPosition = fragmentPosition4Channel;
	textureUV = vec2(uv.x, 1.f - uv.y); 

	vertexNormal = normalize(normal * transpose(inverse(mat3(modelMatrix))));
}

Socket Components

class SocketComponent : public Component
{
public:
	SocketComponent(Component* parentComponent) :
		Component(parentComponent)
	{
	}


	virtual void Destroy() override;

	virtual const Matrix& GetRelativeTransformationMatrix() const override
	{
		return boneAndRelativeTransformationMatrix_;
	}

	void SetBoneTransformationMatrix(const Matrix& boneTransformationMatrix)
	{
		boneTransformationMatrix_ = boneTransformationMatrix;
		boneAndRelativeTransformationMatrix_ = boneTransformationMatrix_ * relativeTransformationMatrix_;
		UpdateComponentToWorldTransformationMatrix();
	}

	const Matrix& GetBoneTransformationMatrix()
	{
		return boneTransformationMatrix_;
	}

	void Attach(ObjectBase* object)
	{
		attachedObjects_.push_back(object);
	}

	void RemoveObject(ObjectBase* object)
	{
		std::vector<ObjectBase*>::iterator attachedObjectsIterator = attachedObjects_.begin();
		for (; attachedObjectsIterator != attachedObjects_.end(); ++attachedObjectsIterator)
		{
			if ((*attachedObjectsIterator) == object)
			{
				attachedObjects_.erase(attachedObjectsIterator);
				break;
			}
		}
	}

	virtual void SetIsActive(bool isActive) override;

protected:
	virtual void UpdateComponentToWorldTransformationMatrix();
	virtual void UpdateChildrenComponentToWorldTransformations();

private:
	std::vector<ObjectBase*> attachedObjects_;

	Matrix boneTransformationMatrix_{ Matrix::IdentityMatrix };
	Matrix boneAndRelativeTransformationMatrix_{ Matrix::IdentityMatrix };
};

void SocketComponent::Destroy()
{
	std::vector<Component*>::iterator childrenIterator = children_.begin();
	for (; childrenIterator != children_.end(); ++childrenIterator)
	{
		Component* child = *childrenIterator;

		child->GetOwner()->RemoveFromSocket(this);
	}

	Component::Destroy();
}

void SocketComponent::SetIsActive(bool isActive)
{
	Component::SetIsActive(isActive);

	std::vector<ObjectBase*>::iterator attachedObjectsIterator = attachedObjects_.begin();
	for (; attachedObjectsIterator != attachedObjects_.end(); ++attachedObjectsIterator)
	{
		(*attachedObjectsIterator)->SetIsActive(isActive);
	}
}

void SocketComponent::UpdateComponentToWorldTransformationMatrix()
{
	if (parent_)
	{
		componentToWorldTransformationMatrix_ = parent_->GetComponentToWorldTransformationMatrix();
	}
	else
	{
		componentToWorldTransformationMatrix_ = owner_->GetWorldTransformationMatrix();
	}

	componentToWorldTransformationMatrix_ = componentToWorldTransformationMatrix_ * boneAndRelativeTransformationMatrix_;

	worldPosition_ = Vector3(componentToWorldTransformationMatrix_[3], componentToWorldTransformationMatrix_[7], componentToWorldTransformationMatrix_[11]);

	Vector3 x = Vector3(componentToWorldTransformationMatrix_[0], componentToWorldTransformationMatrix_[4], componentToWorldTransformationMatrix_[8]);
	Vector3 y = Vector3(componentToWorldTransformationMatrix_[1], componentToWorldTransformationMatrix_[5], componentToWorldTransformationMatrix_[9]);
	Vector3 z = Vector3(componentToWorldTransformationMatrix_[2], componentToWorldTransformationMatrix_[6], componentToWorldTransformationMatrix_[10]);
	worldScaling_ = Vector3(x.Length(), y.Length(), z.Length());

	Matrix3x3 unscaledComponentToWorldTransformationMatrix3x3(
		componentToWorldTransformationMatrix_[0] / worldScaling_.x, componentToWorldTransformationMatrix_[1] / worldScaling_.y, componentToWorldTransformationMatrix_[2] / worldScaling_.z,
		componentToWorldTransformationMatrix_[4] / worldScaling_.x, componentToWorldTransformationMatrix_[5] / worldScaling_.y, componentToWorldTransformationMatrix_[6] / worldScaling_.z,
		componentToWorldTransformationMatrix_[8] / worldScaling_.x, componentToWorldTransformationMatrix_[9] / worldScaling_.y, componentToWorldTransformationMatrix_[10] / worldScaling_.z
	);

	worldRotation_ = Quaternion(unscaledComponentToWorldTransformationMatrix3x3);

	UpdateChildrenComponentToWorldTransformations();
}

void SocketComponent::UpdateChildrenComponentToWorldTransformations()
{
	Component::UpdateChildrenComponentToWorldTransformations();

	std::vector<ObjectBase*>::iterator attachedObjectsIterator = attachedObjects_.begin();
	for (; attachedObjectsIterator != attachedObjects_.end(); ++attachedObjectsIterator)
	{
		ObjectBase* attachedObject = *attachedObjectsIterator;

		attachedObject->SetWorldRotation(worldRotation_, false);
		attachedObject->SetWorldPosition(worldPosition_, false);
		attachedObject->SetWorldScaling(worldScaling_, true);
	}
}

Bone Manipulation


class GOKNAR_API SkeletalMeshInstance : public MeshInstance<SkeletalMesh>
{
...
	void AttachBoneToMatrixPointer(const std::string& boneName, const Matrix* matrix);
	std::unordered_map<int, const Matrix*> boneIdToAttachedMatrixPointerMap_;
...
};

void SkeletalMeshInstance::Render()
{
	PreRender();
...

	mesh_->GetBoneTransforms(boneTransformations_, skeletalMeshAnimation_.skeletalAnimation, skeletalMeshAnimation_.animationTime, sockets_);

	std::unordered_map<int, const Matrix*>::iterator boneIdToAttachedMatrixPointerMapIterator = boneIdToAttachedMatrixPointerMap_.begin();
	while (boneIdToAttachedMatrixPointerMapIterator != boneIdToAttachedMatrixPointerMap_.end())
	{
		const Matrix* matrix = boneIdToAttachedMatrixPointerMapIterator->second;

		if (matrix)
		{
			boneTransformations_[boneIdToAttachedMatrixPointerMapIterator->first] = parentComponent_->GetComponentToWorldTransformationMatrix().GetInverse() * *matrix;
		}

		++boneIdToAttachedMatrixPointerMapIterator;
	}


	mesh_->GetMaterial()->GetShader()->SetMatrixVector(SHADER_VARIABLE_NAMES::SKELETAL_MESH::BONES, boneTransformations_);
...
}

Overall time spent

I have spent roughly 2 weeks implementing skeletal meshes, their animations, socket components and bone manipulation.