structslJoint
{
// joints must be stored in array, in right order, from root to leaf.// do not use list\treeint32_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.
voidslMesh::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)
{
structhelp
{
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).
staticfloat 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 slFrameworkstatic slMat4* GetMatrixAni();
staticvoidSetMatrixAni(const slMat4&, uint32_t slot);
In framework I have
slMat4 m_matrixAni[255];
Methods
slMat4* slFramework::GetMatrixAni(){
return &g_framework->m_matrixAni[0];
}
voidslFramework::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
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
classslTextBufferReader
{
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()
{
}
voidSet(uint8_t* buffer, size_t size){
m_ptrCurr = buffer;
m_size = size;
_onConstructor();
}
voidSet(char* buffer, size_t size){
Set((uint8_t*)buffer, size);
}
voidSet(char8_t* buffer, size_t size){
Set((uint8_t*)buffer, size);
}
boolIsEnd(){
return (m_ptrCurr >= m_ptrEnd);
}
voidSkipLine(){
while (!IsEnd())
{
if (*m_ptrCurr == '\n')
{
++m_ptrCurr;
break;
}
++m_ptrCurr;
}
}
// Get everything between spacesvoidGetWord(slStringA& out){
_skipSpaces();
out.clear();
while (!IsEnd())
{
if (isspace(*m_ptrCurr))
break;
out.push_back(*m_ptrCurr);
++m_ptrCurr;
}
}
// Like GetWord but save positionvoidPickWord(slStringA& out){
uint8_t* save = m_ptrCurr;
GetWord(out);
m_ptrCurr = save;
}
intGetInt(){
GetWord(m_straTmp);
returnatoi(m_straTmp.c_str());
}
floatGetFloat(){
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 methodvoidGetString(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 linevoidGetLine(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.
voidslMeshLoaderImpl::LoadSMD(constchar* path, slMeshLoaderCallback* cb, uint8_t* buffer, uint32_t bufferSz){
SL_ASSERT_ST(cb);
SL_ASSERT_ST(buffer);
SL_ASSERT_ST(bufferSz);
int errcode = 0;
structNodes
{
int parentIndex = -1;
slStringA name;
structFrame
{
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;
structTriangle
{
int nodeID[3];
slVec3f position[3];
slVec3f normal[3];
slVec2f UV[3];
};
structModel
{
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);
}
}
elseif (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'
}
}
}
}
elseif (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 modelif (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);
}
elseif (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.classslJointTransformation
{
public:
slVec4 m_position;
slQuaternion m_rotation;
slVec4 m_scale = slVec4(1.f, 1.f, 1.f, 0.f);
slMat4 m_matrix;
voidCalculateMatrix();
};
structslJointBase
{
// joints must be stored in array, in right order, from root to leaf.// do not use list\treeint32_t m_parentIndex = -1;
char m_name[50];
};
structslJointData
{
slMat4 m_matrixBindInverse;
slJointTransformation m_transformation;
slMat4 m_matrixFinal; // this will go to GPU
};
structslJoint
{
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.