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 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.
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.
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.