Skeletal animation

    Start from simple things. Generate 3D mesh, add joints, set bone indices and weights, do matrix calculations.

    I will describe all my steps, without optimizations things.

    I created sphere

    
    slPolygonMesh* polygonMesh = slFramework::SummonPolygonMesh();
    slMat4 M;
    slVec4 P;
    M.m_data[1].y = 2.f;
    polygonMesh->AddSphere(1.f, 30, M);
    

    Vertex in mesh must have data for bone index and weight

    
    struct slVertexTriangleSkinned
    {
    	slVertexTriangle baseData;
    	slVec4_t<uint8_t> BoneInds;
    	slVec4f Weights;
    };
    

    Convert mesh into skinned mesh

    
    void slMesh::ToSkinned()
    {
    	SL_ASSERT_ST(m_vertices);
    	SL_ASSERT_ST(m_indices);
    	SL_ASSERT_ST(m_info.m_iCount);
    	SL_ASSERT_ST(m_info.m_vCount);
    	SL_ASSERT_ST(m_info.m_skinned == false);
    	SL_ASSERT_ST(m_info.m_stride == sizeof(slVertexTriangle));
    
    	m_info.m_stride = sizeof(slVertexTriangleSkinned);
    	slVertexTriangleSkinned* newVertices = (slVertexTriangleSkinned*)slMemory::malloc(m_info.m_vCount * sizeof(slVertexTriangleSkinned));
    
    	slVertexTriangleSkinned* dst = newVertices;
    	slVertexTriangle* src = (slVertexTriangle*)m_vertices;
    	for (uint32_t i = 0; i < m_info.m_vCount; ++i)
    	{
    		dst->baseData = *src; ++src;
    		dst->BoneInds.set(0, 0, 0, 0);
    		dst->Weights.set(0.f, 0.f, 0.f, 0.f);
    		++dst;
    	}
    
    	slMemory::free(m_vertices);
    	m_vertices = (uint8_t*)newVertices;
    	m_info.m_skinned = true;
    }
    

    Then I created joints\bones.

    
    struct slJoint
    {
    	// joints must be stored in array, in right order, from root to leaf.
    	// do not use list\tree
    	int32_t m_parentIndex = -1;
    
    	slMat4 m_matrixBind;              // local
    	slMat4 m_matrixBindTransformed;   // local * parent->local
    
    	slMat4 m_matrixOffset; // modify this
    
    	slMat4 m_matrixTransform; // use this for visualizing joints
    	slMat4 m_matrixFinal;     // this will go to GPU
    
    	slStringA m_name;
    };
    

    Logic is: I have basic transformation for joint, when I set it before skining, this is `m_matrixBind`. This is local tranformation. `m_matrixBindTransformed` is `m_matrixBind` multiply by parents `m_matrixBind`.

    `m_matrixBindTransformed` will be inverted later

    `m_matrixOffset` is for user transformations.

    `m_matrixTransform` is for drawing joints.

    `m_matrixFinal` this will go into shader.

    It was easier to understand how all works with all this matrices.

    I think 'm_matrixBind' and 'm_matrixBindTransformed' can be replaced with 'm_matrixBindInverse'. Or only `m_matrixBindTransformed`...

    `m_matrixTransform' can be removed. Drawing joints still possible.

    I will change slJoint later, when I start implement skeletal animation.

    Now. I need to store this joints somewhere.

    Add new class

    
    class slSkeleton
    {
    	slArray<slJoint*> m_joints;
    public:
    	slSkeleton();
    	~slSkeleton();
    
    	slJoint* AddJoint(const slMat4&, const slVec4& position, const char* name, int32_t parentIndex);
    	const slArray<slJoint*>& GetJoints() { return m_joints; }
    	void Update();
    };
    

    AddJoint and Update

    
    slJoint* slSkeleton::AddJoint(const slMat4& m, const slVec4& position, const char* name, int32_t parentIndex)
    {
    	SL_ASSERT_ST(name);
    
    	slJoint* joint = slCreate<slJoint>();
    	joint->m_matrixBind = m;
    	joint->m_matrixBind.m_data[3].x = position.x;
    	joint->m_matrixBind.m_data[3].y = position.y;
    	joint->m_matrixBind.m_data[3].z = position.z;
    	joint->m_name = name;
    	joint->m_parentIndex = parentIndex;
    	m_joints.push_back(joint);
    	return joint;
    }
    
    void slSkeleton::Update()
    {
    	if (m_joints.m_size)
    	{
    		for (size_t i = 0; i < m_joints.m_size; ++i)
    		{
    			m_joints.m_data[i]->m_matrixBindTransformed = m_joints.m_data[i]->m_matrixBind;
    
    			if (m_joints.m_data[i]->m_parentIndex != -1)
    				m_joints.m_data[i]->m_matrixBindTransformed = m_joints.m_data[m_joints.m_data[i]->m_parentIndex]->m_matrixBindTransformed
    				* m_joints.m_data[i]->m_matrixBindTransformed;
    		}
    
    		for (size_t i = 0; i < m_joints.m_size; ++i)
    		{
    			auto m = m_joints.m_data[i]->m_matrixBindTransformed;
    			m.invert();
    
    			m_joints.m_data[i]->m_matrixTransform = m_joints.m_data[i]->m_matrixBind * m_joints.m_data[i]->m_matrixOffset;
    			
    			if (m_joints.m_data[i]->m_parentIndex != -1)
    				m_joints.m_data[i]->m_matrixTransform = m_joints.m_data[m_joints.m_data[i]->m_parentIndex]->m_matrixTransform
    				* m_joints.m_data[i]->m_matrixTransform;
    		
    			m_joints.m_data[i]->m_matrixFinal = m_joints.m_data[i]->m_matrixTransform * m;
    			//m_joints.m_data[i]->m_matrixTransform = m_joints.m_data[i]->m_matrixFinal;
    		}
    	}
    }
    
    

    In slSkeleton::Update() calculation of `m_matrixBindTransformed` can be moved into other method.

    The order of matrix multiplication matters. M1*M2 not same M2*M1. If you have problems, try to change order.

    Now in demo, create skeleton and add joints

    
    m_skeleton = slCreate<slSkeleton>();
    M.identity();
    P.set(0.f, -1.8f, 0.f, 1.f);
    m_skeleton->AddJoint(M, P, "0", -1);
    P.set(0.f, 3.5f, 0.f, 1.f);
    m_skeleton->AddJoint(M, P, "1", 0);
    P.set(-1.f, 0.f, 0.f, 1.f);
    m_skeleton->AddJoint(M, P, "2", 1);
    

    Now set indices and add weights in vertices. I created this method for my mesh. Vertex will take nearest joint.

    
    void slMesh::ApplySkeleton(slSkeleton* skeleton)
    {
    	SL_ASSERT_ST(skeleton);
    	SL_ASSERT_ST(m_vertices);
    	SL_ASSERT_ST(m_indices);
    	SL_ASSERT_ST(m_info.m_iCount);
    	SL_ASSERT_ST(m_info.m_vCount);
    	SL_ASSERT_ST(m_info.m_skinned == true);
    	SL_ASSERT_ST(m_info.m_stride == sizeof(slVertexTriangleSkinned));
    	
    	skeleton->Update();
    
    	auto & joints = skeleton->GetJoints();
    	if (joints.m_size)
    	{
    		struct help
    		{
    			help(slVertexTriangleSkinned* V) :v(V), d(999999.f), j(0) {}
    
    			slVertexTriangleSkinned* v = 0;
    			float d = 999999.f;
    			slJoint* j = 0;
    			uint32_t i = 0;
    		};
    		slArray<help> verts;
    		verts.reserve(m_info.m_vCount);
    
    		slVertexTriangleSkinned* src = (slVertexTriangleSkinned*)m_vertices;
    		for (uint32_t i = 0; i < m_info.m_vCount; ++i)
    		{
    			src->BoneInds.set(0, 0, 0, 0);
    			src->Weights.set(0.f, 0.f, 0.f, 0.f);
    			verts.push_back(help(src));
    
    			++src;
    		}
    
    		src = (slVertexTriangleSkinned*)m_vertices;
    		for (uint32_t k = 0; k < m_info.m_vCount; ++k)
    		{
    			for (size_t i = 0; i < joints.m_size; ++i)
    			{
    				auto dist = slMath::distance(
    					slVec3f(
    						(float)joints.m_data[i]->m_matrixTransform.m_data[3].x,
    						(float)joints.m_data[i]->m_matrixTransform.m_data[3].y,
    						(float)joints.m_data[i]->m_matrixTransform.m_data[3].z),
    					src->baseData.Position);
    				if (dist < verts.m_data[k].d)
    				{
    					verts.m_data[k].d = dist;
    					verts.m_data[k].j = joints.m_data[i];
    					verts.m_data[k].i = i;
    				}
    			}
    
    			++src;
    		}
    
    		for (uint32_t k = 0; k < m_info.m_vCount; ++k)
    		{
    			if (verts.m_data[k].j)
    			{
    				verts.m_data[k].v->BoneInds.x = verts.m_data[k].i;
    				verts.m_data[k].v->Weights.x = 1.f;
    			}
    		}
    	}
    }
    

    Now rotate some joints (in Demo).

    
    static float a = 0.f; a += 1.1f * (*m_app->m_dt); if (a > PIPI) a = 0.f;
    m_skeleton->GetJoints().m_data[2]->m_matrixOffset.set_rotation(slQuaternion(a, 0, 0));
    m_skeleton->GetJoints().m_data[1]->m_matrixOffset.set_rotation(slQuaternion(0, a, 0));
    m_skeleton->Update();
    

    Now need to draw all this. Need to set bone matrix.

    
    // in slFramework
    static slMat4* GetMatrixAni();
    static void SetMatrixAni(const slMat4&, uint32_t slot);
    

    In framework I have

    
    slMat4 m_matrixAni[255];
    

    Methods

    
    slMat4* slFramework::GetMatrixAni()
    {
    	return &g_framework->m_matrixAni[0];
    }
    
    void slFramework::SetMatrixAni(const slMat4& m, uint32_t slot)
    {
    	SL_ASSERT_ST(slot < 255);
    	g_framework->m_matrixAni[slot] = m;
    }
    

    Shader. I created copy of Solid shader. Some code based on code from DirectX SDK

    
    "#define MAX_BONE_MATRICES 255\n"
    ...
    // vertex
    "struct VSIn{\n"
    "   float3 position : POSITION;\n"
    "	float2 uv : TEXCOORD;\n"
    "   float3 normal : NORMAL;\n"
    "   float3 binormal : BINORMAL;\n"
    "   float3 tangent : TANGENT;\n"
    "   float4 color : COLOR;\n"
    "   uint4 boneInds : BONES;\n"
    "   float4 boneWeights : WEIGHTS;\n"
    "};\n"
    // constant buffer for matrices
    "cbuffer cbAnimMatrices\n"
    "{\n"
    "double4x4 g_mConstBoneWorld[MAX_BONE_MATRICES];\n"
    "}; \n"
    // Something from vertex shader
    "VSOut VSMain(VSIn input){\n"
    "   VSOut output;\n"
    
    "   float4 skinnedPos = float4(0.f, 0.f, 0.f, 1.f);\n"
    "   float4 Pos = float4(input.position,1);\n"
    "   uint iBone = input.boneInds.x;\n"
    "   float fWeight = input.boneWeights.x;\n"
    "   double4x4 m = g_mConstBoneWorld[iBone];\n"
    "   skinnedPos += mul( m, Pos );\n"
    
    "   iBone = input.boneInds.y;\n"
    "   fWeight = input.boneWeights.y;\n"
    "   m = g_mConstBoneWorld[iBone];\n"
    "   skinnedPos += fWeight * mul( m, Pos );\n"
    "   iBone = input.boneInds.z;\n"
    "   fWeight = input.boneWeights.z;\n"
    "   m = g_mConstBoneWorld[iBone];\n"
    "   skinnedPos += fWeight * mul( m, Pos );\n"
    "   iBone = input.boneInds.w;\n"
    "   fWeight = input.boneWeights.w;\n"
    "   m = g_mConstBoneWorld[iBone];\n"
    "   skinnedPos += fWeight * mul( m, Pos );\n"
    
    "	skinnedPos.w   = 1.f;\n"
    "	output.pos   = mul(WVP, skinnedPos);\n"
    

    I think everything is good. Now I can load and play some 3D animation.

    Download g0_24

    Reading SMD

    Best text file format for 3D model and animation is SMD from Half-Life. Some years ago I wrote loader. But I deleted all code. Now I write again. I don't remember how I wrote in first time. I will invent new method.

    Because SMD is a text file, I will create class, special for reading such files. Also I need to use it for OBJ and other loaders

    
    class slTextBufferReader
    {
    	uint8_t* m_ptrCurr = 0;
    	uint8_t* m_ptrBegin = 0;
    	uint8_t* m_ptrEnd = 0;
    	size_t m_size = 0;
    	void _onConstructor()
    	{
    		m_ptrBegin = m_ptrCurr;
    		m_ptrEnd = m_ptrCurr + m_size;
    	}
    
    	void _skipSpaces() {
    		while (!IsEnd()) {
    			if (!isspace(*m_ptrCurr))
    				break;
    			++m_ptrCurr;
    		}
    	}
    
    	slStringA m_straTmp;
    
    public:
    	slTextBufferReader() {}
    	slTextBufferReader(uint8_t* buffer, size_t size) :m_ptrCurr(buffer), m_size(size) { _onConstructor(); }
    	slTextBufferReader(char* buffer, size_t size) :m_ptrCurr((uint8_t*)buffer), m_size(size) { _onConstructor(); }
    	slTextBufferReader(char8_t* buffer, size_t size) :m_ptrCurr((uint8_t*)buffer), m_size(size) { _onConstructor(); }
    
    	~slTextBufferReader()
    	{
    	}
    
    	void Set(uint8_t* buffer, size_t size)
    	{
    		m_ptrCurr = buffer;
    		m_size = size;
    		_onConstructor();
    	}
    	void Set(char* buffer, size_t size)
    	{
    		Set((uint8_t*)buffer, size);
    	}
    	void Set(char8_t* buffer, size_t size)
    	{
    		Set((uint8_t*)buffer, size);
    	}
    
    	bool IsEnd()
    	{
    		return (m_ptrCurr >= m_ptrEnd);
    	}
    
    	void SkipLine()
    	{
    		while (!IsEnd())
    		{
    			if (*m_ptrCurr == '\n')
    			{
    				++m_ptrCurr;
    				break;
    			}
    			++m_ptrCurr;
    		}
    	}
    
    	// Get everything between spaces
    	void GetWord(slStringA& out)
    	{
    		_skipSpaces();
    		out.clear();
    		while (!IsEnd())
    		{
    			if (isspace(*m_ptrCurr))
    				break;
    			out.push_back(*m_ptrCurr);
    			++m_ptrCurr;
    		}
    	}
    
    	// Like GetWord but save position
    	void PickWord(slStringA& out)
    	{
    		uint8_t* save = m_ptrCurr;
    		GetWord(out);
    		m_ptrCurr = save;
    	}
    
    	int GetInt()
    	{
    		GetWord(m_straTmp);
    		return atoi(m_straTmp.c_str());
    	}
    
    	float GetFloat()
    	{
    		GetWord(m_straTmp);
    		return (float)atof(m_straTmp.c_str());
    	}
    
    	// From this: "bone01 abc"
    	//  get this: bone01 abc
    	// If first symbol is not " then return from method
    	void GetString(slStringA& out)
    	{
    		out.clear();
    		_skipSpaces();
    
    		if (*m_ptrCurr == '\"')
    		{
    			++m_ptrCurr;
    			while (!IsEnd())
    			{
    				if (*m_ptrCurr == '\"')
    				{
    					++m_ptrCurr;
    					break;
    				}
    				out.push_back(*m_ptrCurr);
    				++m_ptrCurr;
    			}
    		}
    	}
    
    	// Get the rest of current line
    	void GetLine(slStringA& out)
    	{
    		out.clear();
    		_skipSpaces();
    
    		while (!IsEnd())
    		{
    			if (*m_ptrCurr == '\r' || *m_ptrCurr == '\n')
    			{
    				SkipLine();
    				break;
    			}
    			out.push_back(*m_ptrCurr);
    			++m_ptrCurr;
    		}
    	}
    
    };
    

    Now SMD.

    
    void slMeshLoaderImpl::LoadSMD(const char* path, slMeshLoaderCallback* cb, uint8_t* buffer, uint32_t bufferSz)
    {
    	SL_ASSERT_ST(cb);
    	SL_ASSERT_ST(buffer);
    	SL_ASSERT_ST(bufferSz);
    
    	int errcode = 0;
    
    	struct Nodes 
    	{
    		int parentIndex = -1;
    		slStringA name;
    
    		struct Frame
    		{
    			int index = 0;
    			float position[3];
    			float rotation[3];
    		};
    		std::vector<Frame> frames; // try to use slArray next time
    	};
    	slArray<Nodes> nodes;
    	nodes.reserve(0x800);
    
    	int frameCount = 0;
    
    	struct Triangle
    	{
    		int nodeID[3];
    		slVec3f position[3];
    		slVec3f normal[3];
    		slVec2f UV[3];
    	};
    	struct Model
    	{
    		slStringA textureName;
    		std::vector<Triangle> triangles; // try to use slArray next time
    	};
    	slArray<Model> models;
    
    	slTextBufferReader tbr(buffer, bufferSz); // text buffer reader
    	slTextBufferReader lbr; // line buffer reader
    	slStringA line;
    	slStringA word;
    	
    	tbr.GetLine(line);
    	lbr.Set(line.data(), line.size());
    		
    	lbr.GetWord(word);
    	if (strcmp(word.c_str(), "version") == 0)
    	{
    		if (lbr.GetInt() == 1)
    		{
    			while (!tbr.IsEnd())
    			{
    				tbr.GetLine(line);
    
    				if (strcmp(line.c_str(), "nodes") == 0)
    				{
    					while (!tbr.IsEnd())
    					{
    						tbr.GetLine(line);
    						if (strcmp(line.c_str(), "end") == 0)
    							break;
    
    						lbr.Set(line.data(), line.size());
    
    						int index = lbr.GetInt();
    						lbr.GetString(word);
    						int parentIndex = lbr.GetInt();
    
    						Nodes newNode;
    						newNode.parentIndex = parentIndex;
    						newNode.name = word.c_str();
    
    						nodes.push_back(newNode);
    					}
    				}
    				else if (strcmp(line.c_str(), "skeleton") == 0)
    				{
    					tbr.GetLine(line); // expect 'time' or 'end'
    					lbr.Set(line.data(), line.size());
    					lbr.PickWord(word);
    					if (strcmp(word.c_str(), "time") == 0)
    					{
    					time:;
    						++frameCount;
    
    						for (size_t i = 0; i < nodes.m_size; ++i)
    						{
    							tbr.GetLine(line);
    							if (strcmp(line.c_str(), "end") == 0)
    								break;
    
    							lbr.Set(line.data(), line.size());
    
    							lbr.PickWord(word);
    							if (strcmp(word.c_str(), "time") == 0)
    								goto time; // next 'time'
    
    							Nodes::Frame newFrame;
    							newFrame.index = lbr.GetInt();
    							newFrame.position[0] = lbr.GetFloat();
    							newFrame.position[1] = lbr.GetFloat();
    							newFrame.position[2] = lbr.GetFloat();
    							newFrame.rotation[0] = lbr.GetFloat();
    							newFrame.rotation[1] = lbr.GetFloat();
    							newFrame.rotation[2] = lbr.GetFloat();
    
    							nodes.m_data[i].frames.push_back(newFrame);
    
    							tbr.PickWord(word);
    							if (strcmp(word.c_str(), "time") == 0)
    							{
    								tbr.GetLine(line);
    								goto time; // next 'time'
    							}
    						}
    					}
    				}
    				else if (strcmp(line.c_str(), "triangles") == 0)
    				{
    					while (!tbr.IsEnd())
    					{
    						tbr.GetLine(line);
    						if (strcmp(line.c_str(), "end") == 0)
    							break;
    
    						// here 'line' must have texture name
    
    						Model* model = 0;
    						for (size_t i = 0; i < models.m_size; ++i)
    						{
    							if (models.m_data[i].textureName == line)
    							{
    								model = &models.m_data[i];
    								break;
    							}
    						}
    						if (!model)
    						{
    							Model newModel;
    							newModel.textureName = line;
    							models.push_back(newModel);
    
    							model = &models.m_data[models.m_size - 1];
    						}
    
    						Triangle newTriangle;
    						for (int j = 0; j < 3; ++j)
    						{
    							tbr.GetLine(line);
    							lbr.Set(line.data(), line.size());
    
    							newTriangle.nodeID[j] = lbr.GetInt();
    							newTriangle.position[j].x = lbr.GetFloat();
    							newTriangle.position[j].y = lbr.GetFloat();
    							newTriangle.position[j].z = lbr.GetFloat();
    							newTriangle.normal[j].x = lbr.GetFloat();
    							newTriangle.normal[j].y = lbr.GetFloat();
    							newTriangle.normal[j].z = lbr.GetFloat();
    							newTriangle.UV[j].x = lbr.GetFloat();
    							newTriangle.UV[j].y = 1.f - lbr.GetFloat();
    						}
    						model->triangles.push_back(newTriangle);
    					}
    
    					/*printf("%i models\n", models.m_size);
    					for (size_t i = 0; i < models.m_size; ++i)
    					{
    						printf("%s\n", models.m_data[i].textureName);
    					}*/
    				}
    			}
    			
    			// if it `reference` then I need to create slSkeleton
    			// `reference` is when I have model
    			if (models.m_size)
    			{
    				slPolygonCreator polygonCreator;
    
    				for (size_t i = 0; i < models.m_size; ++i)
    				{
    					// create mesh here
    					Model* model = &models.m_data[i];
    					
    					slPolygonMesh* polygonMesh = slFramework::SummonPolygonMesh();
    
    					for (auto & tri : model->triangles)
    					{
    						polygonCreator.Clear();
    						
    						polygonCreator.SetPosition(tri.position[0]);
    						polygonCreator.SetNormal(tri.normal[0]);
    						polygonCreator.SetUV(tri.UV[0]);
    						polygonCreator.SetBoneInds(slVec4_t<uint8_t>(tri.nodeID[0], 0, 0, 0));
    						polygonCreator.SetBoneWeights(slVec4f(1.f, 0.f, 0.f, 0.f));
    						polygonCreator.AddVertex();
    
    						polygonCreator.SetPosition(tri.position[1]);
    						polygonCreator.SetNormal(tri.normal[1]);
    						polygonCreator.SetUV(tri.UV[1]);
    						polygonCreator.SetBoneInds(slVec4_t<uint8_t>(tri.nodeID[1], 0, 0, 0));
    						polygonCreator.SetBoneWeights(slVec4f(1.f, 0.f, 0.f, 0.f));
    						polygonCreator.AddVertex();
    
    						polygonCreator.SetPosition(tri.position[2]);
    						polygonCreator.SetNormal(tri.normal[2]);
    						polygonCreator.SetUV(tri.UV[2]);
    						polygonCreator.SetBoneInds(slVec4_t<uint8_t>(tri.nodeID[2], 0, 0, 0));
    						polygonCreator.SetBoneWeights(slVec4f(1.f, 0.f, 0.f, 0.f));
    						polygonCreator.AddVertex();
    
    						polygonMesh->AddPolygon(&polygonCreator, false);
    					}
    
    					if (polygonMesh->m_first_polygon)
    					{
    						slString name;
    						name.assign(model->textureName.c_str());
    
    						slMaterial mat;
    						memcpy_s(mat.m_maps[0].m_filePath, 0x1000, model->textureName.c_str(), model->textureName.m_size);
    						cb->OnMaterial(&mat, &name);
    						cb->OnMesh(polygonMesh->CreateMeshSkinned(), &name, &name);
    					}
    
    					slDestroy(polygonMesh);
    				}
    
    				slVec4 P;
    				slSkeleton* skeleton = slCreate<slSkeleton>();
    				for (size_t i = 0; i < nodes.m_size; ++i)
    				{
    					Nodes* node = &nodes.m_data[i];
    
    					if (node->frames.size())
    					{
    						slQuaternion qX(-node->frames[0].rotation[0], 0.f, 0.f);
    						slQuaternion qY(0.f, -node->frames[0].rotation[1], 0.f);
    						slQuaternion qZ(0.f, 0.f, -node->frames[0].rotation[2]);
    
    						slQuaternion qR = qX * qY * qZ;
    						
    						P.set(node->frames[0].position[0],
    							node->frames[0].position[1],
    							node->frames[0].position[2], 1.f);
    					
    						skeleton->AddJoint(qR, P, slVec4(1.0), node->name.c_str(), node->parentIndex);
    					}
    				}
    				skeleton->m_preRotation.set_rotation(slQuaternion(PI * 0.5f, 0.f, 0.f));
    				cb->OnSkeleton(skeleton);
    			}
    			else if (frameCount && nodes.m_size)
    			{
    				// it probably animation
    				
    				std::filesystem::path p = path;
    				std::filesystem::path fn = p.filename();
    				if (fn.has_extension())
    					fn.replace_extension();
    
    				slSkeletonAnimation* ani = slCreate<slSkeletonAnimation>(
    					nodes.size(),
    					frameCount,
    					fn.generic_string().c_str());
    
    				//printf("%i nodes, %i frames\n", nodes.size(), frameCount);
    				
    				// Set joints. Need to know name and parent index.
    				for (size_t i = 0; i < nodes.m_size; ++i)
    				{
    					Nodes* node = &nodes.m_data[i];
    
    					slJointBase JB;
    					strcpy_s(JB.m_name, sizeof(JB.m_name), node->name.c_str());
    					JB.m_parentIndex = node->parentIndex;
    					ani->SetJoint(&JB, i);
    				}
    
    				// Set frames.
    				for (size_t i = 0; i < nodes.m_size; ++i)
    				{
    					Nodes* node = &nodes.m_data[i];
    					for (size_t o = 0; o < (size_t)frameCount; ++o)
    					{
    						slJointTransformation JT;
    						JT.m_position.x = node->frames[o].position[0];
    						JT.m_position.y = node->frames[o].position[1];
    						JT.m_position.z = node->frames[o].position[2];
    
    						slQuaternion qX(-node->frames[o].rotation[0], 0.f, 0.f);
    						slQuaternion qY(0.f, -node->frames[o].rotation[1], 0.f);
    						slQuaternion qZ(0.f, 0.f, -node->frames[o].rotation[2]);
    						slQuaternion qR = qX * qY * qZ;
    
    						JT.m_rotation = qR;
    
    						ani->SetTransformation(i, o, JT);
    					}
    				}
    
    				cb->OnAnimation(ani);
    			}
    		}
    		else
    		{
    			errcode = 2;
    			goto error;
    		}
    	}
    	else
    	{
    		errcode = 1;
    		goto error;
    	}
    
    	return;
    
    error:;
    	slLog::PrintWarning(L"SMD: error %i\n", errcode);
    }
    

    I need to change slJoint.

    
    // Move/rotate/scale joints changing this. Call CalculateMatrix() after changing.
    class slJointTransformation
    {
    public:
    	slVec4 m_position;
    	slQuaternion m_rotation;
    	slVec4 m_scale = slVec4(1.f, 1.f, 1.f, 0.f);
    	slMat4 m_matrix;
    
    	void CalculateMatrix();
    };
    
    struct slJointBase
    {
    	// joints must be stored in array, in right order, from root to leaf.
    	// do not use list\tree
    	int32_t m_parentIndex = -1;
    
    	char m_name[50];
    };
    
    struct slJointData
    {
    	slMat4 m_matrixBindInverse;
    	slJointTransformation m_transformation;
    	slMat4 m_matrixFinal;     // this will go to GPU
    };
    
    struct slJoint
    {
    	slJointBase m_base;
    	slJointData m_data;
    };
    

    So, for animation\transformation need to modify values in m_transformation, then call CalculateMatrix(), then skeleton->Update()

    For animation.

    One frame contain array of transformations. Size of this array must be equal size of animated joints.

    
    class slSkeletonAnimationFrame
    {
    public:
    	slArray<slJointTransformation> m_transformation;
    };
    struct slSkeletonAnimationFrames
    {
    	slArray<slSkeletonAnimationFrame> m_keyFrames;
    };
    

    When creating slSkeletonAnimation, need to set number of joints and number of frames. And all that arrays will be initialized in constructor.

    slSkeletonAnimation have array m_joints. This array contain names of animated joints

    
    class slSkeletonAnimation
    {
    	slArray<slJointBase> m_joints; // That joints that need to animate.
    	slArray<slSkeletonAnimationFrame*> m_frames; // all transformations
    public:
    	slSkeletonAnimation(uint32_t jtNum, uint32_t frNum, const char* name);
    	~slSkeletonAnimation();
    
    	slJointBase* GetJoint(size_t i) { return &m_joints.m_data[i]; }
    	void SetJoint(slJointBase* j, size_t i) { m_joints.m_data[i] = *j; }
    	
    	size_t GetJointsNumber() { return m_joints.m_size; }
    
    	slSkeletonAnimationFrame* GetFrame(size_t i) { return m_frames.m_data[i]; }
    	size_t GetFramesNumber() { return m_frames.m_size; }
    
    	void SetTransformation(uint32_t joint, uint32_t frame, const slJointTransformation&);
    
    	char m_name[100];
    };
    

    This slSkeletonAnimation is only to store animation. Need to have only 1 for all objects on scene.

    Objects on scene can have different poses. Need to store this pose.

    I think, all objects with animation must have their own slSkeleton. Because slSkeleton have full hierarchy of joints.

    slSkeleton has method Duplicate, it return copy of this skeleton.

    For playng animation I need another class

    
    class slSkeletonAnimationObject
    {
    	slArray<slJoint*> m_joints;
    	slSkeletonAnimation* m_animation = 0;
    	slSkeleton* m_skeleton = 0;
    	float m_frameCurr = 0.f;
    	float m_frameMax = 0.f;
    public:
    	slSkeletonAnimationObject();
    	~slSkeletonAnimationObject();
    
    	void Init(slSkeletonAnimation*, slSkeleton*);
    
    	void Animate(float dt);
    	void AnimateInterpolate(float dt);
    };
    

    This class contains array with joints from skeleton that need to animate. This array will be initialized using array with names (from slSkeletonAnimation::m_joints)

    Also need to give skeleton that will have new pose.

    Animation:

    
    void slSkeletonAnimationObject::Animate(float dt)
    {
    	uint32_t currFrameIndex = (uint32_t)floor(m_frameCurr);
    	
    	slSkeletonAnimationFrame* frame = m_animation->GetFrame(currFrameIndex);
    	for (size_t i = 0; i < frame->m_transformation.m_size; ++i)
    	{
    		m_joints.m_data[i]->m_data.m_transformation.m_position = frame->m_transformation.m_data[i].m_position;
    		m_joints.m_data[i]->m_data.m_transformation.m_rotation = frame->m_transformation.m_data[i].m_rotation;
    		m_joints.m_data[i]->m_data.m_transformation.m_scale = frame->m_transformation.m_data[i].m_scale;
    		m_joints.m_data[i]->m_data.m_transformation.CalculateMatrix();
    	}
    
    	m_frameCurr += (dt * m_animation->m_fps);
    
    	if (m_frameCurr >= m_frameMax)
    		m_frameCurr = 0.f;
    }
    
    void slSkeletonAnimationObject::AnimateInterpolate(float dt)
    {
    	uint32_t currFrameIndex = (uint32_t)floor(m_frameCurr);
    	float t = m_frameCurr - (float)currFrameIndex;
    
    	slSkeletonAnimationFrame* frame = m_animation->GetFrame(currFrameIndex);
    	
    	uint32_t prevFrameIndex = currFrameIndex;
    	if (prevFrameIndex == 0)
    		prevFrameIndex = (uint32_t)floor(m_frameMax) - 1;
    	else
    		--prevFrameIndex;
    	slSkeletonAnimationFrame* prevFrame = m_animation->GetFrame(prevFrameIndex);
    
    	for (size_t i = 0; i < frame->m_transformation.m_size; ++i)
    	{
    		slVec4 P;
    		slMath::lerp1(prevFrame->m_transformation.m_data[i].m_position,
    			frame->m_transformation.m_data[i].m_position, t, P);
    
    		m_joints.m_data[i]->m_data.m_transformation.m_position = P;
    
    		slQuaternion Q;
    		slMath::slerp(prevFrame->m_transformation.m_data[i].m_rotation,
    			frame->m_transformation.m_data[i].m_rotation,
    			t, 1.f, Q);
    		m_joints.m_data[i]->m_data.m_transformation.m_rotation = Q;
    
    		slVec4 S;
    		slMath::lerp1(prevFrame->m_transformation.m_data[i].m_scale,
    			frame->m_transformation.m_data[i].m_scale, t, S);
    		m_joints.m_data[i]->m_data.m_transformation.m_scale = S;
    
    		m_joints.m_data[i]->m_data.m_transformation.CalculateMatrix();
    	}
    
    	m_frameCurr += (dt * m_animation->m_fps);
    
    	if (m_frameCurr >= m_frameMax)
    		m_frameCurr = 0.f;
    }
    

    In demo: I load model and animation like this

    
    class cb : public slMeshLoaderCallback {
    		ExampleSkinnedMesh1* m_example = 0;
    	public:
    		cb(ExampleSkinnedMesh1* e):m_example(e){}
    		virtual ~cb() {}
    		virtual void OnMaterial(slMaterial* m, slString* name) override 
    		{
    			slMeshLoaderCallback::OnMaterial(m, name);
    
    			if (m)
    			{
    				ExampleSkinnedMesh1::Mesh msh;
    				msh.m_material = *m;
    				
    				slStringA texturePathBase("C:\\HL\\SDK\\Monster Models\\Hgrunt\\");
    				slStringA texturePath;
    				
    				texturePath = texturePathBase;
    				texturePath.append(msh.m_material.m_maps[0].m_filePath);
    
    				slImage* image = slFramework::SummonImage(texturePath.c_str());
    				if (image)
    				{
    					slTextureInfo ti;
    					msh.m_material.m_maps[0].m_texture = m_example->GetGS()->SummonTexture(image, ti);
    
    					slDestroy(image);
    				}
    
    				m_example->m_meshBuffers.push_back(msh);
    				//printf("MAT: %s\n", m->m_maps[0].m_filePath);
    			}
    		}
    
    		// It will be called from mesh loader.
    		virtual void OnMesh(slMesh* newMesh, slString* name, slString* materialName) override {
    			if (newMesh && materialName)
    			{
    				for (size_t i = 0; i < m_example->m_meshBuffers.m_size; ++i)
    				{
    					if (m_example->m_meshBuffers.m_data[i].m_material.m_name == (*materialName))
    					{
    						m_example->m_meshBuffers.m_data[i].m_buffer = m_example->m_gs->SummonMesh(newMesh);
    						break;
    					}
    				}
    				slDestroy(newMesh);
    			}
    		}
    
    		virtual void OnSkeleton(slSkeleton* s) override	{
    			if (s)
    			{
    				s->CalculateBind();
    				s->Update();
    
    				for (size_t i = 0; i < m_example->m_meshBuffers.m_size; ++i)
    				{
    					m_example->m_meshBuffers.m_data[i].m_skeleton = s;
    				}
    			}
    		}
    
    		virtual void OnAnimation(slSkeletonAnimation* a) override
    		{
    			if (a)
    			{
    				a->m_fps = 10.f;
    				m_example->m_skeletonAnimation = a;
    				m_example->m_skeletonAnimationObject = slCreate<slSkeletonAnimationObject>();
    				m_example->m_skeletonForAnimation = m_example->m_meshBuffers.m_data[0].m_skeleton->Duplicate();
    				m_example->m_skeletonAnimationObject->Init(a, m_example->m_skeletonForAnimation);
    			}
    		}
    
    	}_cb(this);
    	slFramework::SummonMesh("C:\\HL\\SDK\\Monster Models\\Hgrunt\\reference.smd", &_cb);
    	// should be only animation
    	slFramework::SummonMesh("C:\\HL\\SDK\\Monster Models\\Hgrunt\\victorydance.smd", &_cb);
    

    I load model from Half-Life SDK. With textures.

    Animation in main loop:

    
    if (m_skeletonForAnimation)
    {
    	m_skeletonAnimationObject->AnimateInterpolate((*m_app->m_dt));
    	m_skeletonForAnimation->Update();
    }
    for (size_t i = 0; i < m_meshBuffers.m_size; ++i)
    {
    	m_meshBuffers.m_data[i].m_material.m_sunPosition = material.m_sunPosition;
    	m_meshBuffers.m_data[i].m_material.m_cullBackFace = material.m_cullBackFace;
    	m_meshBuffers.m_data[i].m_material.m_colorAmbient = material.m_colorAmbient;
    
    	if (m_skeletonForAnimation)
    	{
    		auto joints2 = &m_skeletonForAnimation->GetJoints().m_data[0];
    		
    		size_t jsz = m_skeletonForAnimation->GetJoints().m_size;
    		for (size_t o = 0; o < jsz; ++o)
    		{
    			slFramework::SetMatrixAni(joints2[o].m_data.m_matrixFinal, o);
    
    			m_gs->UseDepth(false);
    			m_gs->SetShader(slShaderType::Line3D, 0);
    			slVec4 p;
    			slVec4 p2;
    			float sz = 0.3f;
    			p = slVec4(sz, 0., 0., 0.);
    
    			slMat4 mI2 = joints2[o].m_data.m_matrixBindInverse;
    			mI2.invert();
    
    			slMat4 mI = joints2[o].m_data.m_matrixFinal * mI2;
    
    			slMath::mul(mI, p, p2);
    			m_gs->DrawLine3D(mI.m_data[3], mI.m_data[3] + p2, ColorRed);
    
    			p = slVec4(0., sz, 0., 0.);
    			slMath::mul(mI, p, p2);
    			m_gs->DrawLine3D(mI.m_data[3], mI.m_data[3] + p2, ColorLime);
    
    			p = slVec4(0., 0., sz, 0.);
    			slMath::mul(mI, p, p2);
    			m_gs->DrawLine3D(mI.m_data[3], mI.m_data[3] + p2, ColorBlue);
    			m_gs->UseDepth(true);
    		}
    	}
    
    	m_gs->SetMaterial(&m_meshBuffers.m_data[i].m_material);
    	m_gs->SetMesh(m_meshBuffers.m_data[i].m_buffer);
    	m_gs->SetShader(slShaderType::SolidAnim, 0);
    	m_gs->Draw();
    }
    

    So...basic skeletal animation is implemented.

    Next task, is to play 2 or more animations at the same time.

    It must be easy, I will not describe it here.

    slSkeleton at this moment:

    
    class slSkeleton
    {
    	slArray<slJoint> m_joints;
    public:
    	slSkeleton();
    	~slSkeleton();
    
    	slJoint* AddJoint(const slQuaternion& rotation, const slVec4& position, const slVec4& scale,
    		const char* name, int32_t parentIndex);
    	
    	slArray<slJoint>& GetJoints() { sizeof(slSkeletonAnimationFrame); return m_joints; }
    
    	slJoint* GetJoint(const char* name);
    
    	void CalculateBind();
    	void Update();
    	slSkeleton* Duplicate();
    
    	// some 3D editors rotates model 90 degrees and you need rotate it back
    	// matrix.set_rotation(slQuaternion(PI * 0.5f, 0.f, 0.f));
    	slMat4 m_preRotation;
    };
    

    Also I changed a lot of things in methods like slSkeleton::Update. Everything is better than it was with that sphere...

    Download