———————————————————— 前序 ———————————————————
AndroidLearnOpenGL是本博主自己实现的LearnOpenGL练习集合:
Github地址:GitHub - wangyongyao1989/AndroidLearnOpenGL: OpenGL基础及运用
系列文章:
9、LearnOpenGL之骨骼动画
——————————————————————————————————————————
—————————————————— 效果展示 ————————————————————
3D蒙皮骨骼动画
———————————————————————————————————————————
3D动画可以让我们的游戏栩栩如生。3D世界中的物体,比如人类和动物,当它们做某些事情移动四肢时,比如走路、跑步和攻击,会使我们感到更生动。
一、插值:
要了解动画是如何工作的基础,我们需要了解插值(Interpolation)的概念。插值可以定义为随着时间的推移而发生的事情。就像敌人在时间T上从A点移动到B点一样,即随着时间的推移发生平移。炮塔平滑旋转以面对目标,即随着时间的推移发生旋转,树在时间T内从尺寸A放大到尺寸B,即随时间推移发生缩放。
(动画)插值就是关键帧的中间值。比如我们使用Blender制作动画,不需要设置每一帧的骨骼位置,只需要在几个关键帧中记录它们的位置,旋转,缩放等等信息。然后由程序自动计算出的中间的过渡帧就是我们的插值。通常插值可以使用曲线描述,比如我们的贝塞尔曲线。
用于平移和缩放的简单插值方程如下所示:
它被称为线性插值方程或Lerp。对于旋转,我们不能使用向量。原因是,如果我们继续尝试在X(俯仰)、Y(偏航)和Z(滚转)的向量上使用线性插值方程,插值就不会是线性的。你会遇到一些奇怪的问题,比如Gimbal Lock。为了避免这个问题,我们使用四元数进行旋转。四元数提供了一种叫做球面插值或Slerp方程的东西,它给出了与Lerp相同的结果,但对于两个旋转A和B。
二、动画模型的组件:蒙皮、骨骼和关键帧
动画的整个过程始于添加第一个组件,即blender或Maya等软件中的蒙皮(Skin)。蒙皮只不过是网格(Mesh),它为模型添加了视觉方面,告诉观察者它的外观。但是,如果你想移动任何网格,那么就像现实世界一样,你需要添加骨骼。你可以看到下面的图片来了解它在blender等软件中的外观。
这些骨头通常是以分层的方式添加给人类和动物等角色的,原因很明显。我们想要四肢之间的父子关系(parent-child relationship)。例如,如果我们移动右肩,那么我们的右二头肌、前臂、手和手指也应该移动。这就是层次结构的样子:
在上图中,如果你抓住髋骨(hip bone)并移动它,所有的肢体都会受到它的移动的影响。
此时,我们已经准备好为动画创建关键帧了。关键帧是动画中不同时间点的姿势。我们将在这些关键帧之间进行插值,以便在代码中从一个姿势平滑地过渡到另一个姿势。下面您可以看到如何为简单的4帧跳跃动画创建姿势:
三、Assimp如何保存动画数据:
首先我们需要了解assimp是如何保存导入的动画数据的。看下图:
我们再次从aiScene
指针开始,该指针包含所有aiMeshes
的数组。每个aiMesh对象都有一个aiBone
数组,其中包含诸如此aiBone
将对网格上的顶点集产生多大影响之类的信息。aiBone
包含骨骼的名称,这是一个aiVertexWeight
数组,基本上告诉此aiBone
对网格上的顶点有多大影响。现在我们有了aiBone
的另一个成员,它是offsetMatrix
。这是一个4x4矩阵,用于将顶点从模型空间转换到骨骼空间。你可以在下面的图片中看到这一点:
当顶点位于骨骼空间中时,它们将按照预期相对于骨骼进行变换。
四、代码实现细节:
Vertex顶点程序:
新属性骨骼ID(boneIds)及权重(weights),uniform属性的finalBoneeMatrices存储所有骨骼的变换。boneIds包含用于读取最终BoneMatrix数组并将这些变换应用于pos顶点的索引,其各自的权重存储在权重数组中。
#version 320 es
layout(location = 0) in vec3 pos;
layout(location = 1) in vec3 norm;
layout(location = 2) in vec2 tex;
layout(location = 3) in vec3 tangent;
layout(location = 4) in vec3 bitangent;
layout(location = 5) in ivec4 boneIds;
layout(location = 6) in vec4 weights;
uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;
const int MAX_BONES = 100;
const int MAX_BONE_INFLUENCE = 4;
uniform mat4 finalBonesMatrices[MAX_BONES];
out vec2 TexCoords;
void main()
{
vec4 totalPosition = vec4(0.0f);
for(int i = 0 ; i < MAX_BONE_INFLUENCE ; i++)
{
if(boneIds[i] == -1)
continue;
if(boneIds[i] >=MAX_BONES)
{
totalPosition = vec4(pos,1.0f);
break;
}
vec4 localPosition = finalBonesMatrices[boneIds[i]] * vec4(pos,1.0f);
totalPosition += localPosition * weights[i];
vec3 localNormal = mat3(finalBonesMatrices[boneIds[i]]) * norm;
}
mat4 viewModel = view * model;
gl_Position = projection * viewModel * totalPosition;
TexCoords = tex;
}
顶点数据的绑定:
依据以上的顶点程序,通过assimp把读取的到的顶点数据Vertices绑定到顶点程序的各个属性当中。
在网格GL3DMesh的setupMesh方法中把顶点数据Vertices绑定属性,代码如下:
#include "GL3DMesh.h"
GL3DMesh::GL3DMesh(vector <Vertex> vertices, vector<unsigned int> indices,
vector <Texture> textures) {
this->vertices = vertices;
this->indices = indices;
this->textures = textures;
// now that we have all the required data, set the vertex buffers
//and its attribute pointers.
setupMesh();
}
void GL3DMesh::Draw(GL3DShader &shader) {
// bind appropriate textures
unsigned int diffuseNr = 1;
unsigned int specularNr = 1;
unsigned int normalNr = 1;
unsigned int heightNr = 1;
for (unsigned int i = 0; i < textures.size(); i++) {
// active proper texture unit before binding
glActiveTexture(GL_TEXTURE0 + i);
// retrieve texture number (the N in diffuse_textureN)
string number;
string name = textures[i].type;
if (name == "texture_diffuse")
number = std::to_string(diffuseNr++);
else if (name == "texture_specular")
// transfer unsigned int to string
number = std::to_string(specularNr++);
else if (name == "texture_normal")
// transfer unsigned int to string
number = std::to_string(normalNr++);
else if (name == "texture_height")
// transfer unsigned int to string
number = std::to_string(heightNr++);
// now set the sampler to the correct texture unit
glUniform1i(glGetUniformLocation(shader.shaderId
, (name + number).c_str()), i);
// and finally bind the texture
glBindTexture(GL_TEXTURE_2D, textures[i].id);
}
// draw mesh
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, static_cast<unsigned int>(indices.size()), GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
// always good practice to set everything back to defaults once configured.
glActiveTexture(GL_TEXTURE0);
}
GL3DMesh::~GL3DMesh() {
}
void GL3DMesh::setupMesh() {
// create buffers/arrays
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
glBindVertexArray(VAO);
// load data into vertex buffers
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// A great thing about structs is that their memory layout is sequential for all its items.
// The effect is that we can simply pass a pointer to the struct and it translates perfectly to a glm::vec3/2 array which
// again translates to 3/2 floats which translates to a byte array.
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), &indices[0],
GL_STATIC_DRAW);
// set the vertex attribute pointers
// vertex Positions
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE,
sizeof(Vertex), (void *) 0);
// vertex normals
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
(void *) offsetof(Vertex, Normal));
// vertex texture coords
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex),
(void *) offsetof(Vertex, TexCoords));
// vertex tangent
glEnableVertexAttribArray(3);
glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
(void *) offsetof(Vertex, Tangent));
// vertex bitangent
glEnableVertexAttribArray(4);
glVertexAttribPointer(4, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
(void *) offsetof(Vertex, Bitangent));
// ids
glEnableVertexAttribArray(5);
glVertexAttribIPointer(5, 4, GL_INT, sizeof(Vertex),
(void *) offsetof(Vertex, m_BoneIDs));
// weights
glEnableVertexAttribArray(6);
glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex),
(void *) offsetof(Vertex, m_Weights));
glBindVertexArray(0);
}
ModelAnimation解析model数据:
此类与LearnOpenGL之3D模型加载的加载3D的模型数据的过程类似,不同点在读取数据时额外添加了骨骼ID(m_BoneIDs)和骨骼权重weights(m_Weights)的数据抽取。SetVertexBoneData()方法是重置骨骼ID和骨骼权重weights,ExtractBoneWeightForVertices()方法从Vertices数据提取出重置骨骼ID(m_BoneIDs)和骨骼权重weights(m_Weights)。
// Author : wangyongyao https://github.com/wangyongyao1989
// Created by MMM on 2025/2/10.
//
#include "../includes/animator/ModelAnimation.h"
ModelAnimation::ModelAnimation(const string &path, bool gamma) {
loadModel(path);
}
void ModelAnimation::Draw(GL3DShader &shader) {
for (unsigned int i = 0; i < meshes.size(); i++)
meshes[i].Draw(shader);
}
std::map<string, BoneInfo> &ModelAnimation::GetBoneInfoMap() {
return m_BoneInfoMap;
}
void ModelAnimation::loadModel(const string &path) {
// read file via ASSIMP
Assimp::Importer importer;
const aiScene *scene = importer.ReadFile(path, aiProcess_Triangulate
| aiProcess_GenSmoothNormals
| aiProcess_CalcTangentSpace);
// check for errors
if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) // if is Not Zero
{
cout << "ERROR::ASSIMP:: " << importer.GetErrorString() << endl;
return;
}
// retrieve the directory path of the filepath
directory = path.substr(0, path.find_last_of('/'));
// process ASSIMP's root node recursively
processNode(scene->mRootNode, scene);
}
void ModelAnimation::processNode(aiNode *node, const aiScene *scene) {
// process each mesh located at the current node
for (unsigned int i = 0; i < node->mNumMeshes; i++) {
// the node object only contains indices to index the actual objects in the scene.
// the scene contains all the data,
// node is just to keep stuff organized (like relations between nodes).
aiMesh *mesh = scene->mMeshes[node->mMeshes[i]];
meshes.push_back(processMesh(mesh, scene));
}
// after we've processed all of the meshes (if any)
// we then recursively process each of the children nodes
for (unsigned int i = 0; i < node->mNumChildren; i++) {
processNode(node->mChildren[i], scene);
}
}
void ModelAnimation::SetVertexBoneDataToDefault(Vertex &vertex) {
for (int i = 0; i < MAX_BONE_INFLUENCE; i++) {
vertex.m_BoneIDs[i] = -1;
vertex.m_Weights[i] = 0.0f;
}
}
GL3DMesh ModelAnimation::processMesh(aiMesh *mesh, const aiScene *scene) {
vector<Vertex> vertices;
vector<unsigned int> indices;
vector<Texture> textures;
for (unsigned int i = 0; i < mesh->mNumVertices; i++) {
Vertex vertex;
SetVertexBoneDataToDefault(vertex);
vertex.Position = AssimpGLMHelpers::GetGLMVec(mesh->mVertices[i]);
vertex.Normal = AssimpGLMHelpers::GetGLMVec(mesh->mNormals[i]);
if (mesh->mTextureCoords[0]) {
glm::vec2 vec;
vec.x = mesh->mTextureCoords[0][i].x;
vec.y = mesh->mTextureCoords[0][i].y;
vertex.TexCoords = vec;
} else
vertex.TexCoords = glm::vec2(0.0f, 0.0f);
vertices.push_back(vertex);
}
for (unsigned int i = 0; i < mesh->mNumFaces; i++) {
aiFace face = mesh->mFaces[i];
for (unsigned int j = 0; j < face.mNumIndices; j++)
indices.push_back(face.mIndices[j]);
}
aiMaterial *material = scene->mMaterials[mesh->mMaterialIndex];
vector<Texture> diffuseMaps = loadMaterialTextures(material,
aiTextureType_DIFFUSE, "texture_diffuse");
textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end());
vector<Texture> specularMaps = loadMaterialTextures(material,
aiTextureType_SPECULAR, "texture_specular");
textures.insert(textures.end(), specularMaps.begin(), specularMaps.end());
std::vector<Texture> normalMaps = loadMaterialTextures(material,
aiTextureType_HEIGHT, "texture_normal");
textures.insert(textures.end(), normalMaps.begin(), normalMaps.end());
std::vector<Texture> heightMaps = loadMaterialTextures(material,
aiTextureType_AMBIENT, "texture_height");
textures.insert(textures.end(), heightMaps.begin(), heightMaps.end());
ExtractBoneWeightForVertices(vertices, mesh, scene);
return GL3DMesh(vertices, indices, textures);
}
void ModelAnimation::SetVertexBoneData(Vertex &vertex, int boneID, float weight) {
for (int i = 0; i < MAX_BONE_INFLUENCE; ++i) {
if (vertex.m_BoneIDs[i] < 0) {
vertex.m_Weights[i] = weight;
vertex.m_BoneIDs[i] = boneID;
break;
}
}
}
void ModelAnimation::ExtractBoneWeightForVertices(vector<Vertex> &vertices, aiMesh *mesh,
const aiScene *scene) {
auto &boneInfoMap = m_BoneInfoMap;
int &boneCount = m_BoneCounter;
for (int boneIndex = 0; boneIndex < mesh->mNumBones; ++boneIndex) {
int boneID = -1;
std::string boneName = mesh->mBones[boneIndex]->mName.C_Str();
if (boneInfoMap.find(boneName) == boneInfoMap.end()) {
BoneInfo newBoneInfo;
newBoneInfo.id = boneCount;
newBoneInfo.offset = AssimpGLMHelpers::ConvertMatrixToGLMFormat(
mesh->mBones[boneIndex]->mOffsetMatrix);
boneInfoMap[boneName] = newBoneInfo;
boneID = boneCount;
boneCount++;
} else {
boneID = boneInfoMap[boneName].id;
}
assert(boneID != -1);
auto weights = mesh->mBones[boneIndex]->mWeights;
int numWeights = mesh->mBones[boneIndex]->mNumWeights;
for (int weightIndex = 0; weightIndex < numWeights; ++weightIndex) {
int vertexId = weights[weightIndex].mVertexId;
float weight = weights[weightIndex].mWeight;
assert(vertexId <= vertices.size());
SetVertexBoneData(vertices[vertexId], boneID, weight);
}
}
}
unsigned int
ModelAnimation::TextureFromFile(const char *path, const string &directory, bool gamma) {
string filename = string(path);
filename = directory + '/' + filename;
unsigned int textureID;
glGenTextures(1, &textureID);
int width, height, nrComponents;
unsigned char *data = stbi_load(filename.c_str(), &width, &height, &nrComponents, 0);
if (data) {
GLenum format;
if (nrComponents == 1)
format = GL_RED;
else if (nrComponents == 3)
format = GL_RGB;
else if (nrComponents == 4)
format = GL_RGBA;
glBindTexture(GL_TEXTURE_2D, textureID);
glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
stbi_image_free(data);
} else {
std::cout << "Texture failed to load at path: " << path << std::endl;
stbi_image_free(data);
}
return textureID;
}
vector<Texture>
ModelAnimation::loadMaterialTextures(aiMaterial *mat, aiTextureType type, string typeName) {
vector<Texture> textures;
for (unsigned int i = 0; i < mat->GetTextureCount(type); i++) {
aiString str;
mat->GetTexture(type, i, &str);
// check if texture was loaded before and if so,
// continue to next iteration: skip loading a new texture
bool skip = false;
for (unsigned int j = 0; j < textures_loaded.size(); j++) {
if (std::strcmp(textures_loaded[j].path.data(), str.C_Str()) == 0) {
textures.push_back(textures_loaded[j]);
// a texture with the same filepath has already been loaded,
// continue to next one. (optimization)
skip = true;
break;
}
}
if (!skip) { // if texture hasn't been loaded already, load it
Texture texture;
texture.id = TextureFromFile(str.C_Str(), this->directory);
texture.type = typeName;
texture.path = str.C_Str();
textures.push_back(texture);
// store it as texture loaded for entire model,
// to ensure we won't unnecessary load duplicate textures.
textures_loaded.push_back(texture);
}
}
return textures;
}
int &ModelAnimation::GetBoneCount() {
return m_BoneCounter;
}
ExtractBoneWeightForVertices()方法解析:
首先声明一个映射m_BoneInfoMap
和一个计数器m_BoneCounter
,一旦我们读取到一个新的骨骼,它就会增加。我们在前面的图表中看到,每个aiMesh
都包含与aiMesh
关联的所有aiBone
。骨量提取的整个过程都是从processMesh
函数开始的。对于每个循环迭代,我们通过调用函数SetVertexBoneDataToDefault
将m_BoneID
和m_Weights
设置为其默认值。就在processMesh
函数结束之前,我们调用ExtractBoneWeightData
。在ExtractBoneWeightData
中,我们为每个aiBone运行for循环,并检查该骨骼是否已存在于m_BoneInfoMap
中。如果我们找不到它,那么它被认为是一块新骨头,我们创建一个带有id的新BoneInfo
,并将其关联的mOffsetMatrix
存储到它。然后我们将这个新BoneIInfo
存储在m_BoneInfoMap
中,然后我们递增m_BoneCounter
计数器,为下一块骨头创建一个id。如果我们在m_BoneInfoMap
中找到骨骼名称,那么这意味着该骨骼会影响超出其范围的网格顶点。所以我们取它的Id,进一步了解它会影响哪些顶点。
void ModelAnimation::ExtractBoneWeightForVertices(vector<Vertex> &vertices, aiMesh *mesh,
const aiScene *scene) {
auto &boneInfoMap = m_BoneInfoMap;
int &boneCount = m_BoneCounter;
for (int boneIndex = 0; boneIndex < mesh->mNumBones; ++boneIndex) {
int boneID = -1;
std::string boneName = mesh->mBones[boneIndex]->mName.C_Str();
if (boneInfoMap.find(boneName) == boneInfoMap.end()) {
BoneInfo newBoneInfo;
newBoneInfo.id = boneCount;
newBoneInfo.offset = AssimpGLMHelpers::ConvertMatrixToGLMFormat(
mesh->mBones[boneIndex]->mOffsetMatrix);
boneInfoMap[boneName] = newBoneInfo;
boneID = boneCount;
boneCount++;
} else {
boneID = boneInfoMap[boneName].id;
}
assert(boneID != -1);
auto weights = mesh->mBones[boneIndex]->mWeights;
int numWeights = mesh->mBones[boneIndex]->mNumWeights;
for (int weightIndex = 0; weightIndex < numWeights; ++weightIndex) {
int vertexId = weights[weightIndex].mVertexId;
float weight = weights[weightIndex].mWeight;
assert(vertexId <= vertices.size());
SetVertexBoneData(vertices[vertexId], boneID, weight);
}
}
}
骨骼、动画和动画制作类:
类关系视图:
对于每个渲染帧,我们希望平滑地插值继承中的所有骨骼,并获得它们的最终变换矩阵,这些矩阵将提供给着色器统一的finalBonesMatrix。以下是每个类的内容:
- Bone:从aiNodeAnim读取所有关键帧数据的单个骨骼。它还将根据当前动画时间在关键帧之间进行插值,即平移、缩放和旋转。
- AssimpNodeData:这个结构体将帮助我们将动画从Assimp提取出来。
- Animation:从aiAnimation读取数据并创建Bones的继承记录的资源。
- Animator:这将读取AssimpNodeData的继承方法,以递归方式插入所有骨骼,然后为我们准备所需的最终骨骼转换矩阵。
Bone类解析:
首先为我们的键类型创建3个结构。每个结构都有一个值和一个时间戳。时间戳告诉我们在动画的哪个点需要插值到它的值。Bone有一个构造函数,它从aiNodeAnim
读取密钥并将密钥及其时间戳存储到mPositionKeys
、mRotationKeys
和mScalingKeys
。主要插值过程从更新(float animationTime)开始,该过程在每帧调用一次。此函数调用所有键类型的相应插值函数,并组合所有最终插值结果,并将其存储到4x4矩阵m_LocalTransform
中。平移和缩放关键点的插值函数相似,但对于旋转,我们使用Slerp在四元数之间进行插值。Lerp和Slerp都有3个论点。第一个参数取最后一个键,第二个参数取下一个键和第三个参数取范围为0-1的值,我们在这里称之为比例因子。让我们看看如何在函数GetScaleFactor
中计算这个比例因子:
代码中:
float midWayLength = animationTime - lastTimeStamp;
float framesDiff = nextTimeStamp - lastTimeStamp;
scaleFactor = midWayLength / framesDiff;
Bone.h头文件:
// Author : wangyongyao https://github.com/wangyongyao1989
// Created by MMM on 2025/2/10.
//
#ifndef ANDROIDLEARNOPENGL_BONE_H
#define ANDROIDLEARNOPENGL_BONE_H
#include <vector>
#include <assimp/scene.h>
#include <list>
#include <glm/glm.hpp>
#define GLM_ENABLE_EXPERIMENTAL
#include <glm/gtx/quaternion.hpp>
#include "../animator/AssimpGLMHelpers.h"
struct KeyPosition {
glm::vec3 position;
float timeStamp;
};
struct KeyRotation {
glm::quat orientation;
float timeStamp;
};
struct KeyScale {
glm::vec3 scale;
float timeStamp;
};
class Bone {
public:
Bone(const std::string &name, int ID, const aiNodeAnim *channel);
void Update(float animationTime);
glm::mat4 GetLocalTransform();
std::string GetBoneName() const;
int GetBoneID();
int GetPositionIndex(float animationTime);
int GetRotationIndex(float animationTime);
int GetScaleIndex(float animationTime);
private:
float GetScaleFactor(float lastTimeStamp, float nextTimeStamp,
float animationTime);
glm::mat4 InterpolatePosition(float animationTime);
glm::mat4 InterpolateRotation(float animationTime);
glm::mat4 InterpolateScaling(float animationTime);
private:
std::vector<KeyPosition> m_Positions;
std::vector<KeyRotation> m_Rotations;
std::vector<KeyScale> m_Scales;
int m_NumPositions;
int m_NumRotations;
int m_NumScalings;
glm::mat4 m_LocalTransform;
std::string m_Name;
int m_ID;
};
#endif //ANDROIDLEARNOPENGL_BONE_H
Bone.cpp代码:
// Author : wangyongyao https://github.com/wangyongyao1989
// Created by MMM on 2025/2/10.
//
#include "../includes/animator/Bone.h"
#include "GL3DLogUtils.h"
Bone::Bone(const std::string &name, int ID, const aiNodeAnim *channel) {
m_NumPositions = channel->mNumPositionKeys;
m_Name = name;
m_ID = ID;
m_LocalTransform = glm::mat4(1.0f);
for (int positionIndex = 0; positionIndex < m_NumPositions; ++positionIndex) {
aiVector3D aiPosition = channel->mPositionKeys[positionIndex].mValue;
float timeStamp = channel->mPositionKeys[positionIndex].mTime;
KeyPosition data;
data.position = AssimpGLMHelpers::GetGLMVec(aiPosition);
data.timeStamp = timeStamp;
m_Positions.push_back(data);
}
m_NumRotations = channel->mNumRotationKeys;
for (int rotationIndex = 0; rotationIndex < m_NumRotations; ++rotationIndex) {
aiQuaternion aiOrientation = channel->mRotationKeys[rotationIndex].mValue;
float timeStamp = channel->mRotationKeys[rotationIndex].mTime;
KeyRotation data;
data.orientation = AssimpGLMHelpers::GetGLMQuat(aiOrientation);
data.timeStamp = timeStamp;
m_Rotations.push_back(data);
}
m_NumScalings = channel->mNumScalingKeys;
for (int keyIndex = 0; keyIndex < m_NumScalings; ++keyIndex) {
aiVector3D scale = channel->mScalingKeys[keyIndex].mValue;
float timeStamp = channel->mScalingKeys[keyIndex].mTime;
KeyScale data;
data.scale = AssimpGLMHelpers::GetGLMVec(scale);
data.timeStamp = timeStamp;
m_Scales.push_back(data);
}
}
void Bone::Update(float animationTime) {
glm::mat4 translation = InterpolatePosition(animationTime);
glm::mat4 rotation = InterpolateRotation(animationTime);
glm::mat4 scale = InterpolateScaling(animationTime);
m_LocalTransform = translation * rotation * scale;
}
glm::mat4 Bone::GetLocalTransform() {
return m_LocalTransform;
}
std::string Bone::GetBoneName() const {
return m_Name;
}
int Bone::GetBoneID() {
return m_ID;
}
int Bone::GetPositionIndex(float animationTime) {
for (int index = 0; index < m_NumPositions - 1; ++index) {
if (animationTime < m_Positions[index + 1].timeStamp)
return index;
}
assert(0);
}
int Bone::GetRotationIndex(float animationTime) {
for (int index = 0; index < m_NumRotations - 1; ++index) {
if (animationTime < m_Rotations[index + 1].timeStamp)
return index;
}
assert(0);
}
int Bone::GetScaleIndex(float animationTime) {
for (int index = 0; index < m_NumScalings - 1; ++index) {
if (animationTime < m_Scales[index + 1].timeStamp)
return index;
}
assert(0);
}
float Bone::GetScaleFactor(float lastTimeStamp, float nextTimeStamp, float animationTime) {
float scaleFactor = 0.0f;
float midWayLength = animationTime - lastTimeStamp;
float framesDiff = nextTimeStamp - lastTimeStamp;
scaleFactor = midWayLength / framesDiff;
return scaleFactor;
}
glm::mat4 Bone::InterpolatePosition(float animationTime) {
if (1 == m_NumPositions)
return glm::translate(glm::mat4(1.0f), m_Positions[0].position);
int p0Index = GetPositionIndex(animationTime);
int p1Index = p0Index + 1;
float scaleFactor = GetScaleFactor(m_Positions[p0Index].timeStamp,
m_Positions[p1Index].timeStamp, animationTime);
glm::vec3 finalPosition = glm::mix(m_Positions[p0Index].position, m_Positions[p1Index].position,
scaleFactor);
return glm::translate(glm::mat4(1.0f), finalPosition);
}
glm::mat4 Bone::InterpolateRotation(float animationTime) {
if (1 == m_NumRotations) {
auto rotation = glm::normalize(m_Rotations[0].orientation);
return glm::toMat4(rotation);
}
int p0Index = GetRotationIndex(animationTime);
int p1Index = p0Index + 1;
float scaleFactor = GetScaleFactor(m_Rotations[p0Index].timeStamp,
m_Rotations[p1Index].timeStamp, animationTime);
glm::quat finalRotation = glm::slerp(m_Rotations[p0Index].orientation,
m_Rotations[p1Index].orientation, scaleFactor);
finalRotation = glm::normalize(finalRotation);
return glm::toMat4(finalRotation);
}
glm::mat4 Bone::InterpolateScaling(float animationTime) {
if (1 == m_NumScalings)
return glm::scale(glm::mat4(1.0f), m_Scales[0].scale);
int p0Index = GetScaleIndex(animationTime);
int p1Index = p0Index + 1;
float scaleFactor = GetScaleFactor(m_Scales[p0Index].timeStamp,
m_Scales[p1Index].timeStamp, animationTime);
glm::vec3 finalScale = glm::mix(m_Scales[p0Index].scale, m_Scales[p1Index].scale, scaleFactor);
return glm::scale(glm::mat4(1.0f), finalScale);
}
Animation类解析:
动画对象的创建从构造函数开始。这需要两个论点。首先,动画文件的路径&第二个参数是该动画的模型。稍后您将看到我们为什么需要此模型参考。然后,我们创建一个Assimp::Importer
来读取动画文件,然后进行断言检查,如果找不到动画,该检查将引发错误。然后我们读取一般的动画数据,比如这个动画有多长,即mDuration
和由mTicksPerSecond
表示的动画速度。然后我们调用ReadHeirarchyData
,它复制Assimp的aiNode
继承权并创建AssimpNodeData
的继承权。
然后我们调用一个名为ReadMissingBones
的函数。我不得不编写这个函数,因为有时当我单独加载FBX模型时,它缺少一些骨骼,而我在动画文件中找到了这些缺失的骨骼。此函数读取丢失的骨骼信息,并将其信息存储在模型的m_BoneInfoMap
中,并在m_BoneIInfoMap
中本地保存m_BoneIinfoMap
的引用。
Animation.h头文件:
// Author : wangyongyao https://github.com/wangyongyao1989
// Created by MMM on 2025/2/10.
//
#ifndef ANDROIDLEARNOPENGL_ANIMATION_H
#define ANDROIDLEARNOPENGL_ANIMATION_H
#include <vector>
#include <map>
#include <glm/glm.hpp>
#include <assimp/scene.h>
#include <functional>
#include "../animator/Bone.h"
#include "../animator/animata.h"
#include "../GL3DMesh.h"
#include "../animator/ModelAnimation.h"
struct AssimpNodeData {
glm::mat4 transformation;
std::string name;
int childrenCount;
std::vector<AssimpNodeData> children;
};
class Animation {
private:
float m_Duration;
int m_TicksPerSecond;
std::vector<Bone> m_Bones;
AssimpNodeData m_RootNode;
std::map<std::string, BoneInfo> m_BoneInfoMap;
public:
Animation() = default;
Animation(const std::string &animationPath, ModelAnimation *model);
~Animation();
Bone *FindBone(const std::string &name);
float GetTicksPerSecond();
float GetDuration();
const AssimpNodeData &GetRootNode();
const std::map<std::string, BoneInfo> &GetBoneIDMap();
private:
void ReadMissingBones(const aiAnimation *animation, ModelAnimation &model);
void ReadHierarchyData(AssimpNodeData &dest, const aiNode *src);
};
#endif //ANDROIDLEARNOPENGL_ANIMATION_H
Animation.cpp代码:
// Author : wangyongyao https://github.com/wangyongyao1989
// Created by MMM on 2025/2/10.
//
#include "../includes/animator/Animation.h"
Animation::Animation(const string &animationPath, ModelAnimation *model) {
Assimp::Importer importer;
const aiScene *scene = importer.ReadFile(animationPath, aiProcess_Triangulate);
assert(scene && scene->mRootNode);
auto animation = scene->mAnimations[0];
m_Duration = animation->mDuration;
LOGI("m_Duration ======:%f", m_Duration);
m_TicksPerSecond = animation->mTicksPerSecond;
LOGI("m_TicksPerSecond ======:%f", m_TicksPerSecond);
aiMatrix4x4 globalTransformation = scene->mRootNode->mTransformation;
globalTransformation = globalTransformation.Inverse();
ReadHierarchyData(m_RootNode, scene->mRootNode);
ReadMissingBones(animation, *model);
}
Animation::~Animation() {
}
Bone *Animation::FindBone(const string &name) {
auto iter = std::find_if(m_Bones.begin(), m_Bones.end(),
[&](const Bone &Bone) {
return Bone.GetBoneName() == name;
}
);
if (iter == m_Bones.end()) return nullptr;
else return &(*iter);
}
float Animation::GetTicksPerSecond() { return m_TicksPerSecond; }
float Animation::GetDuration() { return m_Duration; }
const AssimpNodeData &Animation::GetRootNode() { return m_RootNode; }
const std::map<std::string, BoneInfo> &Animation::GetBoneIDMap() {
return m_BoneInfoMap;
}
void Animation::ReadMissingBones(const aiAnimation *animation, ModelAnimation &model) {
int size = animation->mNumChannels;
//getting m_BoneInfoMap from Model class
auto &boneInfoMap = model.GetBoneInfoMap();
//getting the m_BoneCounter from Model class
int &boneCount = model.GetBoneCount();
//reading channels(bones engaged in an animation and their keyframes)
for (int i = 0; i < size; i++) {
auto channel = animation->mChannels[i];
std::string boneName = channel->mNodeName.data;
if (boneInfoMap.find(boneName) == boneInfoMap.end()) {
boneInfoMap[boneName].id = boneCount;
boneCount++;
}
m_Bones.push_back(Bone(channel->mNodeName.data,
boneInfoMap[channel->mNodeName.data].id, channel));
}
m_BoneInfoMap = boneInfoMap;
}
void Animation::ReadHierarchyData(AssimpNodeData &dest, const aiNode *src) {
assert(src);
dest.name = src->mName.data;
dest.transformation = AssimpGLMHelpers::ConvertMatrixToGLMFormat(src->mTransformation);
dest.childrenCount = src->mNumChildren;
for (int i = 0; i < src->mNumChildren; i++) {
AssimpNodeData newData;
ReadHierarchyData(newData, src->mChildren[i]);
dest.children.push_back(newData);
}
}
Animator类解析:
Animator
构造函数将播放动画,然后继续将动画时间m_CurrentTime
重置为0。它还初始化m_FinalBoneMatrices
,这是一个std::vector\<glm::mat4\>
。这里的主要注意点是UpdateAnimation(float deltaTime)
函数。它以m_TicksPerSecond
的速率推进m_CurrentTime
,然后调用CalculateBoneTransform
函数。我们将在开始时传递两个参数,第一个是m_CurrentAnimation
的m_RootNode
,第二个是作为parentTransform
传递的身份矩阵。然后,通过在animation
的m_Bones
数组中查找m_RootNodes
骨骼来检查该骨骼是否参与该动画。如果找到骨骼,则调用bone.Update()
函数,该函数对所有骨骼进行插值,并将局部骨骼变换矩阵返回到nodeTransform
。但这是局部空间矩阵,如果在着色器中传递,将围绕原点移动骨骼。因此,我们将这个nodeTransform
与parentTransform相乘,并将结果存储在globalTransformation
中。这就足够了,但顶点仍在默认模型空间中。我们在m_BoneInfoMap
中找到偏移矩阵,然后将其与globalTransfromMatrix
相乘。我们还将获得id索引,该索引将用于写入该骨骼到m_FinalBoneMatrices
的最终转换。
最后我们为该节点的每个子节点调用CalculateBoneTransform
,并将globalTransformation
作为parentTransform
传递。当没有子节点需要进一步处理时,我们会跳出这个递归循环。
Animator.h头文件:
// Author : wangyongyao https://github.com/wangyongyao1989
// Created by MMM on 2025/2/10.
//
#ifndef ANDROIDLEARNOPENGL_ANIMATOR_H
#define ANDROIDLEARNOPENGL_ANIMATOR_H
#include <glm/glm.hpp>
#include <map>
#include <vector>
#include <assimp/scene.h>
#include <assimp/Importer.hpp>
#include "../animator/Animation.h"
#include "../animator/Bone.h"
class Animator {
private:
std::vector<glm::mat4> m_FinalBoneMatrices;
Animation *m_CurrentAnimation;
float m_CurrentTime;
float m_DeltaTime;
public:
Animator(Animation *animation);
void UpdateAnimation(float dt);
void PlayAnimation(Animation *pAnimation);
void CalculateBoneTransform(const AssimpNodeData *node,
glm::mat4 parentTransform);
std::vector<glm::mat4> GetFinalBoneMatrices();
};
#endif //ANDROIDLEARNOPENGL_ANIMATOR_H
Animator.cpp代码:
// Author : wangyongyao https://github.com/wangyongyao1989
// Created by MMM on 2025/2/10.
//
#include "../includes/animator/Animator.h"
Animator::Animator(Animation *animation) {
m_CurrentTime = 0.0;
m_CurrentAnimation = animation;
m_FinalBoneMatrices.reserve(100);
for (int i = 0; i < 100; i++)
m_FinalBoneMatrices.push_back(glm::mat4(1.0f));
}
void Animator::UpdateAnimation(float dt) {
m_DeltaTime = dt;
if (m_CurrentAnimation) {
m_CurrentTime += m_CurrentAnimation->GetTicksPerSecond() * dt;
m_CurrentTime = fmod(m_CurrentTime, m_CurrentAnimation->GetDuration());
CalculateBoneTransform(&m_CurrentAnimation->GetRootNode(), glm::mat4(1.0f));
}
}
void Animator::PlayAnimation(Animation *pAnimation) {
m_CurrentAnimation = pAnimation;
m_CurrentTime = 0.0f;
}
void Animator::CalculateBoneTransform(const AssimpNodeData *node, glm::mat4 parentTransform) {
std::string nodeName = node->name;
glm::mat4 nodeTransform = node->transformation;
Bone* Bone = m_CurrentAnimation->FindBone(nodeName);
if (Bone)
{
Bone->Update(m_CurrentTime);
nodeTransform = Bone->GetLocalTransform();
}
glm::mat4 globalTransformation = parentTransform * nodeTransform;
auto boneInfoMap = m_CurrentAnimation->GetBoneIDMap();
if (boneInfoMap.find(nodeName) != boneInfoMap.end())
{
int index = boneInfoMap[nodeName].id;
glm::mat4 offset = boneInfoMap[nodeName].offset;
m_FinalBoneMatrices[index] = globalTransformation * offset;
}
for (int i = 0; i < node->childrenCount; i++)
CalculateBoneTransform(&node->children[i], globalTransformation);
}
std::vector<glm::mat4> Animator::GetFinalBoneMatrices() {
return m_FinalBoneMatrices;
}
GL3DAnimationShow动画的执行:
从加载模型开始,该模型将为着色器设置骨骼重量数据,然后通过为其提供路径来创建动画。然后,我们通过将创建的Animation
传递给它来创建Animator
对象。在渲染循环中,我们更新Animator
,进行最终的骨骼变换并将其提供给着色器。
GL3DAnimationShow.h头文件:
// Author : wangyongyao https://github.com/wangyongyao1989
// Created by MMM on 2025/2/10.
//
#ifndef ANDROIDLEARNOPENGL_GL3DANIMATIONSHOW_H
#define ANDROIDLEARNOPENGL_GL3DANIMATIONSHOW_H
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
#include "GLCamera3D.h"
#include "GL3DShader.h"
#include "../animator/ModelAnimation.h"
#include "animator/Animation.h"
#include "animator/Animator.h"
class GL3DAnimationShow {
private:
GLCamera3D mCamera;
float lastX, lastY;
int mActionMode;
int screenW, screenH;
GL3DShader *modelShader = nullptr;
ModelAnimation *gl3DModel = nullptr;
Animation *animation = nullptr;
Animator *animator = nullptr;
// timing
float deltaTime = 0.0f;
float lastFrame = 0.0f;
public:
GL3DAnimationShow();
~GL3DAnimationShow();
bool setupGraphics(int w, int h);
void renderFrame();
bool setSharderPath(const char *vertexPath, const char *fragmentPath);
bool setModelPath(const char *modelPath);
void setMoveXY(float dx, float dy, int actionMode);
void setOnScale(float scaleFactor, float focusX, float focusY, int actionMode);
void printGLString(const char *name, GLenum s);
void checkGlError(const char *op);
};
#endif //ANDROIDLEARNOPENGL_GL3DANIMATIONSHOW_H
GL3DAnimationShow.cpp代码:
// Author : wangyongyao https://github.com/wangyongyao1989
// Created by MMM on 2025/2/10.
//
#include "../includes/GL3DAnimationShow.h"
bool GL3DAnimationShow::setupGraphics(int w, int h) {
screenW = w;
screenH = h;
GLuint modelProgram = modelShader->createProgram();
if (!modelProgram) {
LOGE("Could not create shaderId.");
return false;
}
return false;
}
void GL3DAnimationShow::renderFrame() {
// input
// -----
float currentFrame = clock() * 5 / CLOCKS_PER_SEC;
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
animator->UpdateAnimation(deltaTime);
// render
// ------
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// don't forget to enable shader before setting uniforms
modelShader->use();
// view/projection transformations
glm::mat4 projection = glm::perspective(glm::radians(mCamera.Zoom),
(float) screenW / (float) screenH, 0.5f, 100.0f);
glm::mat4 view = mCamera.GetViewMatrix();
modelShader->setMat4("projection", projection);
modelShader->setMat4("view", view);
auto transforms = animator->GetFinalBoneMatrices();
for (int i = 0; i < transforms.size(); ++i)
modelShader->setMat4("finalBonesMatrices[" + std::to_string(i) + "]", transforms[i]);
// render the loaded model
glm::mat4 model = glm::mat4(1.0f);
// translate it down so it's at the center of the scene
model = glm::translate(model, glm::vec3(0.0f, -0.4f, 0.0f));
// it's a bit too big for our scene, so scale it down
model = glm::scale(model, glm::vec3(2.0f, 2.0f, 2.0f));
modelShader->setMat4("model", model);
gl3DModel->Draw(*modelShader);
}
bool GL3DAnimationShow::setSharderPath(const char *vertexPath,
const char *fragmentPath) {
modelShader->getSharderPath(vertexPath, fragmentPath);
return false;
}
bool GL3DAnimationShow::setModelPath(const char *modelPath) {
LOGI("setMosetModelPath :%s", modelPath);
string model(modelPath);
gl3DModel = new ModelAnimation(model);
animation = new Animation(model, gl3DModel);
animator = new Animator(animation);
return false;
}
void GL3DAnimationShow::setMoveXY(float dx, float dy, int actionMode) {
LOGI("setMoveXY dx:%f,dy:%f,actionMode:%d", dy, dy, actionMode);
float xoffset = dx - lastX;
// reversed since y-coordinates go from bottom to top
float yoffset = lastY - dy;
lastX = dx;
lastY = dy;
mActionMode = actionMode;
mCamera.ProcessXYMovement(xoffset, yoffset);
}
void GL3DAnimationShow::setOnScale(float scaleFactor, float focusX,
float focusY, int actionMode) {
float scale;
if (actionMode == 1 || actionMode == 3) {
scale = 45.0f;
} else {
if (scaleFactor > 1) {
scale = (scaleFactor - 1) * 1000 + 45;
} else {
scale = 50 - (1 - scaleFactor) * 1000;
}
}
LOGI("setOnScale scale:%f", scale);
mCamera.ProcessScroll(scale);
}
void GL3DAnimationShow::printGLString(const char *name, GLenum s) {
const char *v = (const char *) glGetString(s);
LOGI("OpenGL %s = %s\n", name, v);
}
void GL3DAnimationShow::checkGlError(const char *op) {
for (GLint error = glGetError(); error; error = glGetError()) {
LOGI("after %s() glError (0x%x)\n", op, error);
}
}
GL3DAnimationShow::GL3DAnimationShow() {
modelShader = new GL3DShader();
}
GL3DAnimationShow::~GL3DAnimationShow() {
if (modelShader) {
delete modelShader;
modelShader = nullptr;
}
if (gl3DModel) {
delete gl3DModel;
gl3DModel = nullptr;
}
if (animation) {
delete animation;
animation = nullptr;
}
if (animator) {
delete animator;
animator = nullptr;
}
lastX = 0;
lastY = 0;
mActionMode = 0;
screenW = 0;
screenH = 0;
}
五、项目地址:
以上的全部代码均放置个人github项目上。
Github地址:
https://github.com/wangyongyao1989/AndroidLearnOpenGL
参考资料: