Главная Статьи Ссылки Скачать Скриншоты Юмор Почитать Tools Проекты Обо мне Гостевая Форум |
Традиционным подходом для анимации игровых персонажей уже давно стала скелетная анимация или skinning, впервые предложенная еще в 1988 году. Основной идеей скелетной анимации (в отличии от использовавшихся ранее подходов, задающих отдельные анимации для каждой вершины) было использовании специальной иерархической структуры (скелета) для задания анимации.
Модель фактически состоит из двух частей - собственно геометрии (вершин и граней) и скелета (набора костей). Вся анимация строится только для скелета. Мы просто задаем анимацию для каждой кости скелета. Основным преимуществом данного подхода является то, что костей в скелете намного меньше чем вершин (в сотни раз). Поэтому задавать анимацию для скелета гораздо проще и дешевле чем отдельно для каждой вершины и подобная анимация занимает гораздо меньше памяти.
Рис 1. Скелет для модели
Скелет - это иерархия (дерево) костей, у каждой кости (кроме корня) есть родительская кость и могут быть дочерние кости. Также с каждой костью связано некоторое преобразование (матрица 4х4), которое задается относительно родительской кости. Анимация заключается просто в изменении этих преобразований с течением времени.
При этом каждая кость может оказывать влияние на одну или несколько вершин и у каждой вершины может быть несколько костей, влияющих на нее. Обычно число костей, влияющих на вершину, заранее ограничено и мы будем далее считать что на одну вершину может влиять не более четырех костей.
Пусть на вершину v влияют кости b0, b1, b2 и b3. С каждой костью на самом деле связаны две матрицы. Одну из них мы рассмотрели ранее - это локальная матрица, т.е. матрица преобразования относительно родительской кости. Но у родительской кости есть своя локальная матрица, у ее родителя - свой матрица и т.д. Тем самым с каждой костью можно связать еще и глобальную матрицу - матрицу, полученную перемножением всех родительских матриц до самого корня, она задает преобразование данной кости в системе координат всей модели.
Будем считать что кости b0, b1, b2 и b3 имеют глобальные матрицы преобразования B0, B1, B2 и B3. Тогда все эти матрицы будут участвовать в преобразовании данной вершины. Но при этом они могут вносить разный вклад в преобразование вершины - мы будем считать что у каждой вершины кроме четырех костей еще еще четыре коэффициента (веса) w0, w1, w2 и w3. Они задают насколько каждая кость влияет на анимацию данной вершины (их сумма обычно равна единице). В результате анимация вершины задается следующей формулой:
vout=(w0 B0 + ... + w3 B3) * v
Рис 2. Кости, влияющие на вершину
На практике мы передаем в вершинный шейдер массив глобальных матриц костей (по одной матрице на каждую кость) в виде uniform-массива mat4
.
Также каждой вершине добавляется два атрибута - один целочисленный, типа ivec4
, и один вещественный, типа vec4
.
Первый атрибут задает индексы костей (в массив матриц), а второй - их веса.
Тогда вершинный шейдер для скелетной анимации будет выглядеть следующим образом.
#version 330 core
layout (location = 0) in vec3 pos;
layout (location = 1) in vec2 tex;
layout (location = 2) in vec3 normal;
layout (location = 3) in vec3 tangent;
layout (location = 4) in vec3 binormal;
layout (location = 5) in ivec4 boneIDs;
layout (location = 6) in vec4 weights;
uniform mat4 proj;
uniform mat4 mv;
uniform mat3 nm;
uniform vec3 eye; // eye position
uniform vec3 lightDir;
const int MAX_BONES = 100;
uniform mat4 bones [MAX_BONES];
out vec3 l;
out vec3 h;
out vec3 v;
out vec3 t;
out vec3 b;
out vec3 n;
out vec2 tx;
void main(void)
{
mat4 boneTransform;
boneTransform = bones [boneIDs [0]] * weights [0];
boneTransform += bones [boneIDs [1]] * weights [1];
boneTransform += bones [boneIDs [2]] * weights [2];
boneTransform += bones [boneIDs [3]] * weights [3];
vec4 pp = boneTransform * vec4 ( pos, 1.0 );
if ( weights[0] + weights[1] + weights[2] + weights[3] < 0.01 )
pp = vec4 ( pos, 1.0 );
vec4 p = mv * pp;
gl_Position = proj * p;
tx = tex;
n = normalize (nm * normal);
t = normalize (nm * tangent);
b = normalize (nm * binormal);
l = normalize (lightDir);
v = normalize (eye - p.xyz);
h = normalize (l + v);
// convert to TBN
v = vec3 ( dot ( v, t ), dot ( v, b ), dot ( v, n ) );
l = vec3 ( dot ( l, t ), dot ( l, b ), dot ( l, n ) );
h = vec3 ( dot ( h, t ), dot ( h, b ), dot ( h, n ) );
}
Соответствующая ему вершина будет задаваться следующим описанием:
#define NUM_BONES_PER_VERTEX 4
struct SkinnedVertex
{
glm::vec3 pos;
glm::vec2 tex;
glm::vec3 n;
glm::vec3 t, b;
uint32_t ids [NUM_BONES_PER_VERTEX]; // bone ids
float weights [NUM_BONES_PER_VERTEX]; // bone weights
SkinnedVertex ()
{
memset ( ids, 0, sizeof ( ids ) );
memset ( weights, 0, sizeof ( weights ) );
}
// add new bone/weight pair
void addBoneData ( uint32_t bone, float weight )
{
if ( weight <= 0.0f )
return;
uint32_t index = 0;
while ( index < NUM_BONES_PER_VERTEX - 1 && weights [index] > 0 )
index++;
assert ( index < NUM_BONES_PER_VERTEX );
if ( index < NUM_BONES_PER_VERTEX )
{
ids[index] = bone;
weights[index] = weight;
}
};
// ensure bone weights are normalized (sum to 1)
void normalize ()
{
float sum = weights [0];
for ( int i = 1; i < NUM_BONES_PER_VERTEX; i++ )
sum += weights [i];
for ( int i = 0; i < NUM_BONES_PER_VERTEX; i++ )
weights [i] /= sum;
}
};
На сайте www.mixamo.com есть большое количество готовых анимаций и набор простых моделей, к которым эти анимации применимы. Обратите внимание, что обычно принято задавать модель изначально в так называемой T-pose, как показано на следующем рисунке.
Рис 3. T-pose
Очень часто для загрузки моделей разных форматов в приложениях используется библиотека ASSIMP. Она поддерживает большое количество различных форматов моделей и также поддерживает загрузку моделей со скелетной анимацией. Но использование ее для работы со скелетной анимацией несет в себе и некоторые нюансы, в частности там используется своя библиотека векторов и матриц, отличающаяся от GLM. Наиболее заметным отличием является то что матрицы в ней по сравнению с GLM хранятся в транспонированном виде и поэтому они перемножаются в обратном порядке.
Далее мы рассмотрим использовании библиотеки ASSIMP для загрузки моделей и анимаций, но при этом мы будем использовать в качестве математической
библиотеки более традиционную GLM.
В результате мы создадим класс SkinnedModel
для загрузки, анимации и рендеринга таких моделей.
Важной особенностью библиотеки ASSIMP является то, что она поддерживает иерархическую организацию моделей без отношения к скелетной анимации.
Модель (aiScene
) состоит из отдельных мешей (aiMesh
), объединенных в дерево при помощи узлов (aiNode
).
Экземпляр класса aiNode
включает в себя имя, матрицу преобразования (относительно родительского узла) и набор мешей.
Также имеется указатель на родительский класс и массив указателей на дочерние узлы.
Модель содержит ссылку на корень иерархии.
Это позволяет строить сложные, иерархически организованные модели.
Поэтому строить отдельно от этой иерархии еще одну иерархию костей не имеет никакого смысла и для
скелета используется существующая иерархия узлов.
Сами кости задаются объектами класса aiBone
. Каждая кость содержит матрицу преобразования и имя.
При помощи этого имени кость привязывается к узлу иерархия (с этим же именем).
Каждая кость имеет соответствующий узел в иерархии, но не у каждого узла есть своя кость.
На следующем фрагменте кода приводятся используемые классы.
struct Primitive // rendering data for submesh in a common buffer
{
std::string name;
uint32_t numIndices; // # of indices used in this primitive
uint32_t baseVertex;
uint32_t baseIndex; // starting index for this primtitive
uint32_t material; // material index into model's list of materials
uint32_t numBones;
bbox bounds; // bounding box for this primitive
void render () const
{
if ( numIndices > 0 )
glDrawElementsBaseVertex ( GL_TRIANGLES, numIndices, GL_UNSIGNED_INT, (void*)(sizeof(uint32_t) * baseIndex), baseVertex );
}
};
struct Node
{
std::string name;
glm::mat4 transform = glm::mat4 ( 1.0f );
Node * parent = nullptr;
std::vector<Node *> children; // child nodes, owns
std::vector<Primitive*> meshes; // does not own, just a ref
bbox bounds;
Node ( const std::string& n, const glm::mat4& m ) : name ( n ), transform ( m ) {}
~Node ()
{
for ( auto * child : children )
delete child;
}
const bbox& getBox () const
{
return bounds;
}
void computeBounds ()
{
bounds.reset ();
// get bounds for all meshes
for ( auto * mesh : meshes )
bounds.merge ( mesh->bounds );
// collect bounds from children
for ( auto * child : children )
{
child->computeBounds ();
bounds.merge ( child->bounds );
}
// apply transform to them
bounds.apply ( transform );
}
glm::mat4 parentTransform ( glm::mat4& m ) const
{
for ( const Node * node = this; node -> parent; node = node -> parent )
m = node->transform * m;
return m;
}
};
struct Bone
{
std::string name;
glm::mat4 invBindPose; // transforms from bone space to mesh space in bind pose
glm::mat4 globalPose;
Bone * parent;
Node * node;
};
Давайте теперь рассмотрим каким образом задается в ASSIMP анимация.
Для этого в ASSIMP используется всего два класса - aiAnimation
и aiNodeAnim
.
Класс AiAnimation задает всего одну анимацию, но для всей модели целиком.
Он содержит массив отдельных анимаций для узлов (по одной анимации на узел) и параметры,
задающие масштабирование времени анимации - mDuration
и mTicksPerSecond
.
Класс aiNodeAnim
задает анимацию отдельного узла и для идентификации этого узла он хранит его имя.
Также он хранит массив ключевых кадров - для каждого ключевого кадра он хранит момент времени
и соответствующее преобразование.
Преобразование хранится не в виде матрицы 4х4, а как три отдельных значения - вектор переноса (translation),
вектор масштабирования (scaling) и кватернион, задающий поворот (rotation).
Соответственно при построении преобразования для заданного момента времени мы сперва приводим этот момент к времени анимации, после чего путем интерполяции (линейной для переноса и масштабирования и сферической для кватернионов) определяем параметры анимации него. Далее по этим параметрам строится матрица 4х4.
class AnimNode // animation node - animation for a given node
{
template <typename T>
struct KeyValue
{
float t;
T value;
KeyValue ( float tt, const T& v ) : t ( tt ), value ( v ) {}
};
using Key3 = KeyValue<glm::vec3>; // (time, vec3) key (pos, scale)
using KeyQ = KeyValue<glm::quat>; // (time, quat) key (rotation)
std::string name;
std::vector<Key3> positions;
std::vector<KeyQ> rotations;
std::vector<Key3> scalings;
public:
AnimNode ( const aiNodeAnim * node );
const std::string& getName () const
{
return name;
}
template <typename T>
uint32_t findKey ( float time, const T& keys ) const
{
for ( uint32_t i = 0; i < keys.size () - 1; i++ )
if ( time < keys [i+1].t )
return i;
return keys.size() - 1; // sometimes we have error with animation duration and key time stamps
}
glm::vec3 interpolate3 ( float time, const std::vector<Key3>& keys ) const;
glm::quat interpolate4 ( float time, const std::vector<KeyQ>& keys ) const;
glm::mat4 interpolate ( float time ) const;
};
AnimNode::AnimNode ( const aiNodeAnim * node ) : name ( node->mNodeName.C_Str () )
{
for ( size_t i = 0; i < node->mNumPositionKeys; i++ )
positions.push_back ( Key3 ( node->mPositionKeys [i].mTime, toGlmVec3 ( node->mPositionKeys [i].mValue ) ) );
for ( size_t i = 0; i < node->mNumRotationKeys; i++ )
rotations.push_back ( KeyQ ( node->mRotationKeys [i].mTime, toGlmQuat ( node->mRotationKeys [i].mValue ) ) );
for ( size_t i = 0; i < node->mNumScalingKeys; i++ )
scalings.push_back ( Key3 ( node->mScalingKeys [i].mTime, toGlmVec3 ( node->mScalingKeys [i].mValue ) ) );
}
glm::vec3 AnimNode::interpolate3 ( float time, const std::vector<Key3>& keys ) const
{
assert ( keys.size () > 0 );
if ( keys.size () == 1 ) // only one value
return keys [0].value;
auto index = findKey ( time, keys );
auto next = (index + 1) % keys.size ();
assert ( next < keys.size () );
float delta = keys [next].t - keys [index].t;
float factor = clamp ( (time - keys [index].t ) / delta );
return glm::mix ( keys [index].value, keys [next].value, factor );
}
glm::quat AnimNode::interpolate4 ( float time, const std::vector<KeyQ>& keys ) const
{
assert ( keys.size () > 0 );
if ( keys.size () == 1 ) // only one value
return keys [0].value;
auto index = findKey ( time, keys );
auto next = (index + 1) % keys.size ();
assert ( next < keys.size () );
float delta = keys [next].t - keys [index].t;
float factor = clamp ( (time - keys [index].t ) / delta );
return glm::slerp ( keys [index].value, keys [next].value, factor );
}
glm::mat4 AnimNode::interpolate ( float time ) const
{
auto pos = interpolate3 ( time, positions );
auto rot = interpolate4 ( time, rotations );
auto scale = interpolate3 ( time, scalings );
return glm::scale ( glm::mat4 ( 1 ), scale ) * glm::translate ( glm::mat4 ( 1 ), pos ) * glm::mat4_cast ( rot );
}
class Animation // single animation for a node
{
std::string name;
float duration;
float ticksPerSecond;
std::vector<AnimNode *> channels;
public:
Animation ( const aiAnimation * anim ) : name ( toString ( anim->mName ) )
{
duration = anim->mDuration;
ticksPerSecond = anim->mTicksPerSecond;
for ( size_t i = 0; i < anim->mNumChannels; i++ )
channels.push_back ( new AnimNode ( anim->mChannels [i] ) );
}
~Animation ()
{
for ( auto * node : channels )
delete node;
}
float convertTime ( float time ) const
{
return fmod ( time * ticksPerSecond, duration );
}
const AnimNode * findNode ( const std::string& n ) const
{
for ( auto ch : channels )
if ( ch -> getName () == n )
return ch;
return nullptr;
}
};
Рассмотрим теперь класс SkinnedMesh, служащий для представления анимированной модели. Удобнее для всех мешей сразу завести два буфера - вершин и индексов, которые будут хранить в себе геометрию сразу всех мешей. Также удобно сразу завести массив мешей и массив материалов для этих мешей (один материал может использоваться сразу несколькими мешами). Также мы заведем массив костей и массив соответствующих им глобальных матриц. Иерархию узлов мы будем хранить просто как указатель на корень.
class SkinnedModel
{
std::vector<SkinnedVertex> vertices;
std::vector<uint32_t> indices;
std::vector<PbrMaterial *> materials;
std::vector<Primitive> meshes;
std::vector<Bone> bones;
std::vector<Animation*> animations;
std::vector<glm::mat4> boneMatrices;
Node * root = nullptr;
VertexArray vao; // array with all bindings
VertexBuffer vertexBuf; // vertex data (Skinnedvertex) for all submeshes
VertexBuffer indexBuf; // index buffer for all submeshes
public:
SkinnedModel () = default;
~SkinnedModel ()
{
for ( auto * mat : materials )
delete mat;
for ( auto * anim : animations )
delete anim;
}
bool isOk () const
{
return root != nullptr && !meshes.empty () && !materials.empty ();
}
bool load ( const std::string& fileName, const std::string& texturePath )
{
MeshLoader loader;
auto * scene = loader.loadScene ( fileName, true );
if ( scene == nullptr )
return false;
assert (_heapchk () == _HEAPOK );
loadMaterials ( loader, scene, texturePath );
loadMeshes ( loader, scene, 1 );
loadNodes ( loader, scene );
setBonesParents ();
assert (_heapchk () == _HEAPOK );
return true;
}
void render ( const glm::mat4& modelView, Program& program )
{
program.bind ();
program.setUniformMatrices ( "bones", boneMatrices.data (), boneMatrices.size () );
vao.bind ();
renderNode ( root, modelView, program );
vao.unbind ();
program.unbind ();
}
void renderNode ( Node * node, const glm::mat4& modelView, Program& program )
{
glm::mat4 tr = glm::inverse ( root->transform ) * node -> parentTransform ( glm::mat4 ( 1 ) );
glm::mat4 mv = modelView * tr;
glm::mat3 nm = normalMatrix ( mv );
glm::vec3 sz = root->getBox ().getSize ();
float size = max3 ( sz ) / 7;
glm::mat4 translate = glm::translate ( glm::mat4(1), -root->getBox ().getCenter () );
glm::mat4 scale = glm::scale ( glm::mat4(1), glm::vec3 ( 1.0 / size ) );
program.setUniformMatrix ( "mv", scale * translate * mv );
program.setUniformMatrix ( "nm", nm );
for ( auto * mesh : node->meshes )
{
auto * mat = materials [mesh->material];
mat -> bind ( program );
mesh -> render ();
}
for ( auto * c : node->children )
renderNode ( c, modelView, program );
}
Node * createNode ( const aiNode * n, Node * parent )
{
auto node = new Node ( toString ( n->mName ), toGlmMat4 ( n->mTransformation ) );
node->parent = parent;
// add meshes (mNumMeshes, mMeshes (int indices into meshes list)
for ( int i = 0; i < n->mNumMeshes; i++ )
node->meshes.push_back ( &meshes [n->mMeshes[i]] );
// add children
for ( int i = 0; i < n->mNumChildren; i++ )
node->children.push_back ( createNode ( n->mChildren [i], node ) );
return node;
}
void boneTransform ( int animation, float timeInSeconds )
{
glm::mat4 identity = glm::mat4(1.0f);
float time = animations [animation] -> convertTime ( timeInSeconds );
readHierarchy ( animation, time, root, identity );
boneMatrices.resize ( bones.size () );
for ( size_t i = 0; i < bones.size (); i++ )
boneMatrices [i] = bones[i].globalPose;
}
void readHierarchy ( int animation, float time, Node * node, const glm::mat4& parentTransform )
{
int boneIndex = findBone ( node->name );
if ( boneIndex != -1 )
{
auto * anim = animations [animation];
auto * animNode = anim -> findNode ( node->name );
auto nodeTransform = node->transform;
if ( animNode != nullptr )
nodeTransform = animNode -> interpolate ( time );
// Combine with node Transformation with Parent Transformation
glm::mat4 globalTransform = parentTransform * nodeTransform;
bones [boneIndex].globalPose = glm::inverse ( root->transform ) * globalTransform * bones[boneIndex].invBindPose;
for ( auto * child : node->children )
readHierarchy ( animation, time, child, globalTransform );
}
else
{
for ( auto * child : node->children )
readHierarchy ( animation, time, child, parentTransform );
}
}
void loadNodes ( MeshLoader& loader, const aiScene * scene )
{
root = createNode ( scene->mRootNode, nullptr );
root->computeBounds ();
}
void loadMaterials ( MeshLoader& loader, const aiScene * scene, const std::string& texturePath )
{
printf ( "Found %d embedded textures\n", scene -> mNumTextures );
printf ( "Found %d materials\n", scene->mNumMaterials );
std::function<bool(Texture&, const std::string&)> texLoader = [scene] (Texture& texture, const std::string& name)->bool
{
auto nm = getFileName ( name );
for ( size_t i = 0; i < scene -> mNumTextures; i++ )
{
auto * pTex = scene -> mTextures [i];
if ( nm == getFileName ( pTex->mFilename.C_Str () ) )
{
if ( pTex -> mHeight > 0 ) // not compressed data, just raw RGBA bits
return texture.load2DRaw ( pTex->mWidth, pTex->mHeight, GL_RGBA, GL_RGBA, GL_UNSIGNED_BYTE, pTex->pcData );
// it is compressed texture, in pTex->achFormatHint type of texture, mWidth - data size
return texture.load2D ( pTex->pcData, pTex->mWidth, pTex->mFilename.C_Str () );
}
}
return texture.load2D ( name );
};
for ( unsigned i = 0; i < scene->mNumMaterials; i++ )
{
aiMaterial * material = scene->mMaterials[i];
std::string name = toString ( material->GetName() );
std::string path = "";
glm::vec4 diffColor = glm::vec4 ( 0 );
aiColor4D color;
std::string diff = getAiTexture ( material, aiTextureType_DIFFUSE );
std::string bump = getAiTexture ( material, aiTextureType_NORMALS );
std::string bump2 = getAiTexture ( material, aiTextureType_HEIGHT );
std::string spec = getAiTexture ( material, aiTextureType_SPECULAR );
if ( AI_SUCCESS == material->Get ( AI_MATKEY_COLOR_DIFFUSE, color ) )
diffColor = glm::vec4 ( color.r, color.g, color.b, color.a );
printf ( "---- Material: %s\n", name.c_str () );
PbrMaterial * mat = new PbrMaterial ( name, texturePath, diff, bump, spec, diffColor, texLoader );
materials.push_back ( mat );
}
}
void loadMeshes ( MeshLoader& loader, const aiScene * scene, float scale )
{
bbox box;
Primitive mesh;
uint32_t numVertices = 0;
uint32_t numIndices = 0;
// Count the number of vertices and indices
for ( uint32_t i = 0 ; i < scene->mNumMeshes ;i++ )
{
if ( scene->mMeshes [i]->mNormals == nullptr ) // sometimes happen
{
meshes.push_back ( { "", 0, 0, 0, 0 } ); // placeholder for empty mesh
continue;
}
mesh.name = toString ( scene -> mMeshes [i] ->mName );
mesh.material = scene -> mMeshes [i] -> mMaterialIndex;
mesh.numIndices = scene -> mMeshes [i] -> mNumFaces * 3;
mesh.baseVertex = numVertices;
mesh.baseIndex = numIndices;
meshes.push_back ( mesh );
numVertices += scene -> mMeshes [i] -> mNumVertices;
numIndices += scene -> mMeshes [i] -> mNumFaces * 3;
}
for ( uint32_t i = 0 ; i < scene->mNumMeshes ;i++ )
{
if ( scene->mMeshes [i]->mNormals == nullptr ) // sometimes happen
continue;
loader.loadAiMesh<SkinnedVertex> ( scene->mMeshes[i], scale, vertices, indices, box, 0 );
meshes [i].bounds = box;
meshes [i].numBones = scene->mMeshes [i]->mNumBones;
loadBones ( scene, scene->mMeshes [i], meshes [i].baseVertex );
}
boneMatrices.resize ( bones.size () );
loadAnimations ( scene );
createBuffers ( vertices, indices );
}
void loadBones ( const aiScene * scene, aiMesh * mesh, int baseVertex )
{
for ( size_t i = 0; i < mesh->mNumBones; i++ )
{
std::string name = toString ( mesh -> mBones [i]->mName );
int index = findBone ( name );
if ( index < 0 ) // new bone
index = addBone ( mesh->mBones [i] );
for ( size_t j = 0 ; j < mesh -> mBones [i] -> mNumWeights ; j++ )
{
uint32_t vertex = mesh->mBones [i]->mWeights [j].mVertexId + baseVertex;
float weight = mesh->mBones [i]->mWeights [j].mWeight;
assert ( vertex < vertices.size () );
vertices [vertex].addBoneData ( index, weight );
}
}
}
void loadAnimations ( const aiScene * scene )
{
printf ( "Found %d animations\n", scene -> mNumAnimations );
for ( size_t i = 0; i < scene -> mNumAnimations; i++ )
animations.push_back ( new Animation ( scene -> mAnimations [i] ) );
}
int addBone ( const aiBone * bone )
{
Bone info;
info.invBindPose = toGlmMat4 ( bone -> mOffsetMatrix );
info.globalPose = glm::mat4 ( 1.0f );
info.name = toString ( bone -> mName );
info.parent = nullptr;
info.node = nullptr;
bones.push_back ( info );
return (int) (bones.size () - 1);
}
int findBone ( const std::string& name ) const
{
for ( int i = 0; i < bones.size (); i++ )
if ( bones [i].name == name )
return i;
return -1;
}
Node * findNode ( Node * node, const std::string& name )
{
if ( node == nullptr )
return nullptr;
if ( node->name == name )
return node;
for ( auto * child : node->children )
if ( auto * ptr = findNode ( child, name ) )
return ptr;
return nullptr;
}
void setBonesParents ()
{
for ( auto& bone : bones )
{
Node * node = findNode ( root, bone.name );
if ( node == nullptr ) // no node for bone, strange
continue;
// look by name of parent node
if ( node->parent == nullptr )
continue;
auto index = findBone ( node->parent->name );
bone.parent = index >= 0 ? &bones [index] : nullptr;
bone.node = (Node *) node;
}
}
void createBuffers ( std::vector<SkinnedVertex>& vertices, std::vector<uint32_t>& indices )
{
vao.create ();
vao.bind ();
vertexBuf.create ();
vertexBuf.bind ( GL_ARRAY_BUFFER );
vertexBuf.setData ( vertices );
size_t offs = 0; // attribute offset
// register all components for locations
// 0 -> position
glVertexAttribPointer ( 0,
3, // number of values per vertex
GL_FLOAT,
GL_FALSE, // normalized
sizeof(SkinnedVertex), // stride (offset to next vertex data)
(const GLvoid*) offs );
offs += sizeof ( glm::vec3 );
// 1 -> texture coordinates
glVertexAttribPointer ( 1,
2, // number of values per vertex
GL_FLOAT,
GL_FALSE, // normalized
sizeof(SkinnedVertex), // stride (offset to next vertex data)
(const GLvoid*) offs );
offs += sizeof ( glm::vec2 );
// 2 -> normal
glVertexAttribPointer ( 2,
3, // number of values per vertex
GL_FLOAT,
GL_FALSE, // normalized
sizeof(SkinnedVertex), // stride (offset to next vertex data)
(const GLvoid*) offs );
offs += sizeof ( glm::vec3 );
// 3 -> tangent
glVertexAttribPointer ( 3,
3, // number of values per vertex
GL_FLOAT,
GL_FALSE, // normalized
sizeof(SkinnedVertex), // stride (offset to next vertex data)
(const GLvoid*) offs );
offs += sizeof ( glm::vec3 );
// 4 -> binormal
glVertexAttribPointer ( 4,
3, // number of values per vertex
GL_FLOAT,
GL_FALSE, // normalized
sizeof(SkinnedVertex), // stride (offset to next vertex data)
(const GLvoid*) offs );
offs += sizeof ( glm::vec3 );
auto boneIdOffs = offs; // save offset to bone id's
auto boneWeightOffs = boneIdOffs + NUM_BONES_PER_VERTEX * sizeof ( uint32_t );
// 5 - bone ids
glVertexAttribIPointer ( 5, 4, GL_INT, sizeof(SkinnedVertex), (const GLvoid*) boneIdOffs );
// 6 -> bone weights
glVertexAttribPointer ( 6,
4, // number of values per vertex
GL_FLOAT,
GL_FALSE, // normalized
sizeof(SkinnedVertex), // stride (offset to next vertex data)
(const GLvoid*) boneWeightOffs );
glEnableVertexAttribArray ( 0 );
glEnableVertexAttribArray ( 1 );
glEnableVertexAttribArray ( 2 );
glEnableVertexAttribArray ( 3 );
glEnableVertexAttribArray ( 4 );
glEnableVertexAttribArray ( 5 );
glEnableVertexAttribArray ( 6 );
indexBuf.create ();
indexBuf.bind ( GL_ELEMENT_ARRAY_BUFFER );
indexBuf.setData ( indices );
vao.unbind ();
}
};
https://learnopengl.com/Guest-Articles/2020/Skeletal-Animation
Bones animation with OpenGL, ASSIMP and GLM>
Skeletal animation with ASSIMP, COLLADA, and glm
ASSIMP OpenGL collada and Skeletal Animation
Skeletal animation with Assimp and glm
ASSIMP Skeletal Animation Tutorial #1 – Vertex Weights and Indices
ASSIMP Skeletal Animation Tutorial #3 – Something about Skeletons
По этой
ссылке можно скачать весь исходный код к этой статье.