前面一篇文章
android OpenGL渲染3D模型文件
介绍了渲染3D模型的方式,但是,它还是静态的,模型本身不会动,还是不够炫酷。所以本文来讨论一下如何让模型自己动起来。

想要动起来,就需要传说中的骨骼动画了。 一般大部分模型文件都支持带骨骼动画的数据,例如fbx, dae,但也有个别不支持,例如obj。

本文分两部分讨论,一是捋一下骨骼动画的背景知识,二是在android上怎么用openGL ES渲染。当然了,渲染骨骼动画还是比较麻烦的,大部分场景下,还是走游戏引擎,例如unity,裸写openGL的还是比较少的,但这有注意理解openGL,理解游戏引擎的实现。

先上图,给个效果,吸引一下大家的注意力。
在这里插入图片描述

2 骨骼动画

骨骼动画(Skeletal animation) ,也叫 骨骼蒙皮(Skinning) 。它包含2个词语,对应两件事情,一个是 骨骼Bone ,一个是 动画Animation
美术同学做好一个模型后,只有顶点和纹理信息,是不会动的,想要动起来,就需要有什么介质,带动模型一起动,这个介质就是骨骼。怎么个动法,就是为骨骼添加一些动画,例如移动1cm并旋转30度。

骨骼 有3个基本元素:
"开始的关节 "叫 首端(root) 或 头部(head) 。
“body(身体)”部分是骨骼的主体。
“结束关节” 部分叫 顶端(tip) 或 尾端 (tail) 。
在这里插入图片描述
基本上是,一根骨骼的root关节会连着另一根骨骼的tail关节。所有的骨骼连在一起,叫 骨骼树 。骨骼树需要有一个 根节点

例如对于人体骨骼,我们可能会设置 后背骨头 作为根节点,然后手臂、腿、手指骨骼等作为下一层级的子节点骨骼。 当父节点骨头运动的时候同时会带动所有子节点骨头运动,但是当子节点骨头运动的时候并不会反过来带动父节点骨头运动 (例如我们的手指头可以在手掌不动的时候自己活动,但是当手掌移动的时候手指会跟着移动)。

来,我们感受一下骨头树到底是啥样子。

下图是Blend软件,正在制作模型文件。
左边是美术同学辛苦做了一天的模型。这个模型包含了多个网格(Mesh),例如头发,脸,衣服,脚,但它不会动。
在这里插入图片描述
于是,美术同学制作了右边的一个骨骼树(当然了,骨骼树也有现成的模板,可以直接导入使用,修改,不需要每次重新制作一个骨骼)。

可以把骨骼树拖到人身上,把每一块Mesh都绑定到骨骼上(一个Mesh可以对应多个骨骼,一个骨骼也可能被多个mesh绑定,例如手,脚,都包含了几块骨骼)。这部分工作叫做 骨骼绑定(Rigging)

下图是把mesh和骨骼绑定后的一个样子。
在这里插入图片描述
骨骼和mesh绑定后,还是不会动,想要动,就要为骨骼添加 动画Animation 了。例如**“行走”,“奔跑”,“死亡”**等。 每一种动画,都可以定义了一组关键帧。关键帧包含沿动画路径的关键点中所有骨骼的变换。这样在渲染的时候,在关键帧之间进行插值,并在关键帧之间创建平滑的运动。
例如动画1秒,定义2个关键帧,位移从0.5 到1.5。1秒内动画20次,则每一次的位移是0.5 + (1.5 - 0.5)/20。

有了动画,骨头就会动,mesh就可以跟着动了。下图就是美术同学开始为骨骼添加动画,让骨骼动起来,于是脚就可以动起来了。
在这里插入图片描述
可以预知,绑定后,每个顶点都有对应的骨骼影响它。在两个骨骼的连接处的顶点,还会被2个骨骼同时影响。于是就有一个很重要的概念,是 权重(weights) 。通常一个顶点如果被多个骨骼影响,则这些骨骼,对该顶点的权重之和为1。另外,一般规范, 一个顶点最多被4个骨骼影响

3 OpenGL ES渲染

如果没有骨骼,则vertex shader很简单:

gl_Position = u_MVPMatrix * position;

也就是乘于MVP转换矩阵,把顶点在模型空间,转换到裁减空间中。

现在有了骨骼,可以猜想,先要把position做一些偏移,然后再乘于MVP矩阵。

这个偏移,是骨骼对顶点产生的影响,数学上就是一个矩阵,有4个骨骼影响,则是4个矩阵。
可以猜想shader的代码如下:

new_position = M1 * position * W1 + M2 * position * W2 + M3 * position * W3 + M4 * position * W4;
gl_Position = u_MVPMatrix * new_position;

其中M1 ~M4是顶点对应的4个骨骼的 转换矩阵 ,W1~W4是对应的权重。

下文分别就如何提取权重 和 转换矩阵 ,来展开说明。

3.1 骨骼的权重数据提取

对模型文件的解析,我们用 assimp ,更多assimp的使用细节,在这篇文章 android OpenGL渲染3D模型文件 已经讨论过,本文不会过多展开。

我们定义一个Vertex数据结构,来存顶点数据,以及顶点所关联的骨骼+权重数据。

struct Vertex {
    // position
    glm::vec3 Position;
    // normal
    glm::vec3 Normal;
    // texCoords
    glm::vec2 TexCoords;
    //bone indexes which will influence this vertex
    int m_BoneIDs[4];
    //weights from each bone
    float m_Weights[4];

和这篇文章想比,android OpenGL渲染3D模型文件,多的就是m_BoneIDsm_Weights。代表该顶点被哪些骨骼影响,以及对应的权重。
m_Weights数组的加和,必然为1。

接下来看下怎么提取权重数据

下图是骨骼在assimp中的数据结构。
在这里插入图片描述
aiScene存放了模型的所有数据,它包含了aiMesh数组。
每个aiMesh都包含了aiBone数组
每个aiBone都包含了名字,一个offset矩阵,一个aiVertexWeight数组,该数组存放所有被当前骨骼影响的顶点,和对应的权重。

来看下如何提取:

如下函数,专门提取一个mesh下的骨骼数据。
其中参数vertices代表当前mesh的所有顶点数据结构Vertex数组。

	void ExtractBoneWeightForVertices(std::vector<Vertex>& vertices, aiMesh* mesh)
    	LOGCATE("ExtractBoneWeightForVertices, mesh->mNumBones %d", mesh->mNumBones);
		auto& boneInfoMap = m_BoneInfoMap;
		int& boneCount = m_BoneCounter;//start from 0
		//一个Mesh可以有多个骨骼
		for (int boneIndex = 0; boneIndex < mesh->mNumBones; ++boneIndex)
			//1. 为这根骨骼分配一个id,方便后续计算
			int boneID = -1;
            aiBone* aiBonePtr = mesh->mBones[boneIndex];//接下来针对这根骨骼提取数据
			std::string boneName = aiBonePtr->mName.C_Str();
			if (boneInfoMap.find(boneName) == boneInfoMap.end())
				BoneInfo newBoneInfo;
				//分配id
				newBoneInfo.id = boneCount;
				//提取offset矩阵
				newBoneInfo.offset = AssimpGLMHelpers::ConvertMatrixToGLMFormat(aiBonePtr->mOffsetMatrix);
				boneInfoMap[boneName] = newBoneInfo;
				boneID = boneCount;//assign an id
				boneCount++;
				boneID = boneInfoMap[boneName].id;
			LOGCATE("boneName %s, boneID %d, boneCount %d", boneName.c_str(), boneID, boneCount);
			assert(boneID != -1);
			auto weightsArray = aiBonePtr->mWeights;//骨骼的权重数组,用指针表示,数组长度为numWeights
			int numWeights = aiBonePtr->mNumWeights;
			LOGCATE("numWeights %d", numWeights);
			//2. 遍历所有的权重数组,提取出weight,来放到顶点数据结构中
			//一根骨骼,可以影响多个顶点,通过权重参数来影响,不同的顶点的权重不同
			//一个顶点,也可以被多个骨骼影响,特别是关节处(2个骨骼交界处),但最多4个
			for (int weightIndex = 0; weightIndex < numWeights; ++weightIndex)
				int vertexId = weightsArray[weightIndex].mVertexId;
				float weight = weightsArray[weightIndex].mWeight;
				assert(vertexId <= vertices.size());
				SetVertexBoneData(vertices[vertexId], boneID, weight);
	//填充数据
	void SetVertexBoneData(Vertex& vertex, int boneID, float weight)
		for (int i = 0; i < 4; ++i)
			if (vertex.m_BoneIDs[i] < 0)//如果第N个骨骼还没填充权重数据,则填充,填充完break
				vertex.m_Weights[i] = weight;
				vertex.m_BoneIDs[i] = boneID;
				break;

上面已经加了很多注释,不再重复说明了。
最终就是每个顶点数据,都添加了所对应的骨骼(不超过4个),以及骨骼的权重。

另外还把每个骨骼的id和offset矩阵存到了一个map,在后面渲染时使用。

3.2 动画数据提取

提取的目标,就是生成一个转换矩阵,把某个顶点的坐标,转换到动画之后的新的坐标。

3.2.1 assimp中的数据结构分析

动画数据在assimp中的存储结构如下:
在这里插入图片描述
一个aiAnimation代表一种动画,例如“奔跑”
aiAnimation的mTicksPerSecond,代表一秒钟几次动画。
mDuration代表总共多少次电话。
举个例子,如果mTicksPerSecond=25, mDuration = 100,则表示动画总时间为4秒。

mChannels代表动画所包含的骨骼节点列表。
来看一下一个channel的类定义:

struct aiNodeAnim {
aiString mNodeName;//节点名字,也就是骨骼名字,唯一
aiVectorKey* mPositionKeys;//位移的关键帧数组
aiQuatKey* mRotationKeys;//旋转的关键帧数组
aiVectorKey* mScalingKeys;//缩放的关键帧数组

可见aiNodeAnim包括骨骼名字,和对应的关键帧的位移,旋转,缩放参数。
来看一下位移数组的类aiVectorKey定义是啥

struct aiVectorKey
    /** The time of this key */
    double mTime;
    /** The value of this key */
    aiVector3D mValue;

发现很简单,一个是关键的时间,一个是具体值。

假如总共定义4个关键帧。那么,对于mTicksPerSecond=25, mDuration = 100,我们程序要做的,就是在非关键帧的时间点,做一下插值,估算这个时间点,mValues大概是多少。

现在清楚assimp怎么存的了,我们就定义一些类,来把这些数据提取出来。

3.2.2 提取准备

首先,定义三个类,来存关键帧的数据,具体如下:

struct KeyPosition glm::vec3 position; float timeStamp; struct KeyRotation glm::quat orientation; float timeStamp; struct KeyScale glm::vec3 scale; float timeStamp;

接着,定义一个类Bone,管理关键帧

class Bone {
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;
public:
Bone(const std::string& name, int ID, const aiNodeAnim* channel);//构造函数,提取aiNodeAnim的数据
void Update(float animationTime);//根据时间,计算一个m_LocalTransform换算矩阵

一个非常重要的函数,是Update,用于根据时间戳,计算矩阵。这个函数在每次onDraw时调用。

现在来看一下怎么把这些数据提取出来。

3.2.3 提取函数

	void ReadMissingBones(const aiAnimation* animation, ModelAnim& model)
		int size = animation->mNumChannels;
		//获得之前解析权重时所记录的骨骼map,其中key为骨骼名字
		m_BoneInfoMap = model.GetBoneInfoMap();//getting m_BoneInfoMap from Model class
		LOGCATE("ReadMissingBones, m_BoneInfoMap address %p, size %d, animation->mNumChannels %d", &m_BoneInfoMap,m_BoneInfoMap.size(), animation->mNumChannels);
		//获得骨骼计数器,用于分配id
		int& boneCount = model.GetBoneCount(); //getting the m_BoneCounter from Model class
		//reading channels(bones engaged in an animation and their keyframes)
		//读取通道列表,每个通道包括所有被该动画影响的骨骼,以及对应的关键帧
		for (int i = 0; i < size; i++)
			auto channel = animation->mChannels[i];//一个channel代表某个骨骼
			std::string boneName = channel->mNodeName.data;//拿到骨骼名字
			if (m_BoneInfoMap.find(boneName) == m_BoneInfoMap.end())
			{//如果万一map不包括这个骨骼,则记录下来
				m_BoneInfoMap[boneName].id = boneCount;
				boneCount++;
			//创建一个Bone对象,添加到m_Bones数组
			m_Bones.push_back(Bone(channel->mNodeName.data,
								   m_BoneInfoMap[channel->mNodeName.data].id, channel));

从上面的代码可见,m_Bones数组,记录了所有骨骼的动画信息。

Bone对象的构造函数,做了实际的提取工作:

Bone(const std::string& name, int ID, const aiNodeAnim* channel)
		m_Name(name),
		m_ID(ID),
		m_LocalTransform(1.0f)
		m_NumPositions = channel->mNumPositionKeys;
        //1. 提取关键帧的位移参数,放到m_Positions列表,后面可以用于计算插值
        LOGCATE("Bone created, m_NumPositions %d", m_NumPositions);
		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);
            LOGCATE("get one key frame's position %c, timeStamp %f", glm::to_string(data.position).c_str(), data.timeStamp);
		//2. 提取关键帧的旋转
		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);
		//3. 提取关键帧的缩放
		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);

3.3 逐帧绘制数据

上面的数据全部准备好了,接下来就看每次onDraw时要怎么让模型动起来了。

3.3.1 一次绘制的全流程

下面是Draw函数。

void Model3DAnimSample::Draw(int screenW, int screenH)
	if(m_pModel == nullptr || m_pShader == nullptr) return;
	//update animation firstly
	float deltaTime = 0.03f;//base on seconds, 30fps, each frame is about 0.03 seconds
	//根据时间戳,计算 动画矩阵
    m_pAnimator->UpdateAnimation(deltaTime);
    LOGCATE("Draw start");
    glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glEnable(GL_DEPTH_TEST);
    //更新MVP矩阵
	UpdateMVPMatrix(m_MVPMatrix, m_AngleX, m_AngleY, (float)screenW / screenH);
    m_pShader->use();
    m_pShader->setMat4("u_MVPMatrix", m_MVPMatrix);
    m_pShader->setMat4("u_ModelMatrix", m_ModelMatrix);
    m_pShader->setVec3("lightPos", glm::vec3(0, 0, m_pModel->GetMaxViewDistance()));
    m_pShader->setVec3("lightColor", glm::vec3(1.0f, 1.0f, 1.0f));
    m_pShader->setVec3("viewPos", glm::vec3(0, 0, m_pModel->GetMaxViewDistance()));
    //重点,获得动画矩阵
    auto transforms = m_pAnimator->GetFinalBoneMatrices();
    LOGCATE("Draw, transform size %d", transforms.size());
    //传递给vertex shader, 用于计算动画之后的新顶点坐标
    for (int i = 0; i < transforms.size(); ++i)
        m_pShader->setMat4("finalBonesMatrices[" + std::to_string(i) + "]", transforms[i]);
    //调用DrawCall,逐网格绘制
    m_pModel->Draw((*m_pShader));
    LOGCATE("Draw done");

和这篇文章android OpenGL渲染3D模型文件不同的,就2个地方:
一个是
m_pAnimator->UpdateAnimation(deltaTime);
用于根据时间戳,计算转换矩阵

一个是
auto transforms = m_pAnimator->GetFinalBoneMatrices();
m_pShader->setMat4("finalBonesMatrices[" + std::to_string(i) + "]", transforms[i]);
把转换矩阵拿出来,上传到vertex shader,用于计算动画之后的新顶点坐标。

我们先不关心finalBonesMatrices转换矩阵怎么生成的,先来看在shader中怎么用的,在第三节开头已经提到了,这里给出具体实现代码:

"#version 300 es
precision mediump float;
layout (location = 0) in vec3 a_position;
layout (location = 1) in vec3 a_normal;
layout (location = 2) in vec2 a_texCoord;
//骨骼id,最多4个
layout (location = 5) in ivec4 boneIds; 
//相应的骨骼的权重
layout (location = 6) in vec4 weights;
out vec2 v_texCoord;
uniform mat4 u_MVPMatrix;
const int MAX_BONES = 100;//最多有100个骨骼
const int MAX_BONE_INFLUENCE = 4;//该顶点最多被4个骨骼影响
uniform mat4 finalBonesMatrices[MAX_BONES];
out vec3 specular;
void main()
    v_texCoord = a_texCoord;    
    vec4 position = vec4(0.0f);
    //把所有影响的骨骼的换算矩阵,乘于原始的顶点坐标,加和,得到动画之后的新的顶点坐标
    for(int i = 0 ; i < MAX_BONE_INFLUENCE ; i++)
        vec4 localPosition = finalBonesMatrices[boneIds[i]] * vec4(a_position,1.0f);
        position += localPosition * weights[i];
    //乘于MVP矩阵,得到gl_Position
    gl_Position = u_MVPMatrix * position;
    //....代码省略

首先,入参多了boneIds & weights 以及finalBonesMatrices,即该顶点被哪些骨骼影响,以及对应的权重和转换矩阵。
接着,一个for循环,计算第i根骨骼产生的影响:
vec4 localPosition = finalBonesMatrices[boneIds[i]] * vec4(a_position,1.0f);
然后加上权重
position += localPosition * weights[i];

for循环退出后,position就代表一帧动画之后的新的顶点位置。

注意,这个顶点仍然是在模型空间内。所以,还需要乘于MVP矩阵,得到最终的gl_Position,即裁减空间下的坐标。

好了,基本上绘制的逻辑已经完成了!!

3.3.2 动画矩阵的计算过程

接下来,回过头来看一下
m_pAnimator->UpdateAnimation(deltaTime);
的实现。

	void 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));

dt的值,可以是1/fps,例如30帧率的话,是0.03。
例如TicksPerSecond = 25, Duration = 100,则绘制第一帧,
mCurrentTime = 25 * 0.03 = 0.75。
fmod函数很简单,是求余函数,保证m_CurrentTime一直不会超过Duration,超过的话就从0开始。说人话就是,动画播放结束,从头开始。

接着,就是CalculateBoneTransform函数了,这是一个递归的函数。
首次传参是动画的第一个骨骼节点。然后递归,算出动画所影响的所有骨骼的矩阵。

来看一下具体实现:

* 计算某个骨骼 影响顶点的换算矩阵 * @param node 存骨骼名字,矩阵 * @param parentTransform 父节点的换算矩阵 void CalculateBoneTransform(const AssimpNodeData* node, glm::mat4 parentTransform) std::string nodeName = node->name; glm::mat4 nodeTransform = node->transformation; LOGCATE("CalculateBoneTransform nodeName %s", nodeName.c_str()); Bone* Bone = m_CurrentAnimation->FindBone(nodeName); if (Bone) LOGCATE("CalculateBoneTransform Bone->Update %.4f", m_CurrentTime); //Bone对象根据时间,计算一个矩阵 Bone->Update(m_CurrentTime); //得到矩阵 nodeTransform = Bone->GetLocalTransform(); //当前骨骼的换算矩阵,会被父节点的矩阵影响,所以要相乘 glm::mat4 globalTransformation = parentTransform * nodeTransform; std::map<std::string,BoneInfo> boneInfoMap = m_CurrentAnimation->GetBoneIDMap(); if (boneInfoMap.find(nodeName) != boneInfoMap.end()) int index = boneInfoMap[nodeName].id; glm::mat4 offset = boneInfoMap[nodeName].offset;//骨骼的原始矩阵 //某个骨骼影响顶点的换算矩阵,该矩阵将传递给vertex shader //需要再乘于根节点的m_GlobalTransform,根节点影响所有子节点的换算 m_FinalBoneMatrices[index] = m_GlobalTransform * globalTransformation * offset; LOGCATE("m_FinalBoneMatrices[%d]: %s, offset %s", index, glm::to_string(m_FinalBoneMatrices[index]).c_str(), glm::to_string(offset).c_str()); //递归,计算子节点的矩阵 for (int i = 0; i < node->childrenCount; i++) CalculateBoneTransform(&node->children[i], globalTransformation);

一个很重要的调用,是
Bone->Update(m_CurrentTime);
这个就是前面说很多次的插值计算,怎么个插值计算,先不管,反正最后是得到了插值后的一个矩阵。
因为父骨骼的动画会影响子骨骼的动画,所以需要乘于parentTransform。

glm::mat4 globalTransformation = parentTransform * nodeTransform;

首次调用parentTransform为单位矩阵。
后面递归调用,parentTransform就是globalTransformation了。

接着,终于开始计算m_FinalBoneMatrices了,这个是要传递到shader的!
来看一下公式:

m_FinalBoneMatrices[index] = m_GlobalTransform * globalTransformation * offset;

offset矩阵是当前从骨骼空间转换到mesh空间的矩阵。在前面的aiBone结构中读取的。更多细节见What does mOffsetMatrix actually do in Assimp?

m_GlobalTransform从是根节点的矩阵的逆矩阵,通过这样获得:

        m_GlobalTransformation = scene->mRootNode->mTransformation;
		m_GlobalTransformation = m_GlobalTransformation.Inverse();

之所以要依赖根结点的矩阵,是因为骨骼树结构中,每个结点都包含一个mat4 Transform矩阵,用于描述自己相对于父结点的方位变化。子结点代表的骨骼,其绝对方位由根结点的Transform逐步地乘到自己的Transform来得到。“绝对方位”指的就是在Model Space中的方位。

4 再次探讨骨骼

我们回到3.1节,那里一笔带过的踢了aiBone的offset矩阵,该矩阵也在3.3.2节使用了。

那么问题来了,为什么一个aiBone,可以用一个4x4的offset矩阵来表示?长度没有,位置也没有,是不是有点寒酸?

实际上每块骨骼可理解为一个坐标空间关节可理解为骨骼坐标空间的原点。关节的位置由它在父骨骼坐标空间中的位置描述。
在这里插入图片描述

上图中有三块骨骼,分别是上臂,前臂和手指。锁骨关节,它是上臂的原点,同样肘关节是前臂的原点,腕关节是手指骨骼的原点。关节既决定了骨骼空间的位置,又是骨骼空间的旋转和缩放中心。

回到上面的问题:
为什么用一个4X4矩阵就可以表达一个骨骼?

因为4X4矩阵中含有的平移分量决定了关节的位置旋转和缩放分量决定了骨骼空间的旋转和缩放。

我们来看前臂这个骨骼,其原点位置是位于上臂上某处的,对于上臂来说,它知道自己的坐标空间某处(即肘关节所在的位置)有一个子空间,那就是前臂,至于前臂里面是啥就不考虑了。当前臂绕肘关节旋转时,实际是前臂坐标空间在旋转,从而其中包含的子空间也在绕肘关节旋转,在这个例子中是手指骨骼。

再总结一下:
骨骼就是坐标空间,骨骼层次就是嵌套的坐标空间。关节只是描述骨骼的位置即骨骼自己的坐标空间原点在其父空间中的位置,绕关节旋转是指骨骼坐标空间(包括所有子空间)自身的旋转。

在骨骼树中,每一块骨骼的位置都依赖于其父骨骼的位置,而根骨骼没有父节点,他的位置就是整个骨骼体系在世界坐标系中的位置。

最最后,上链接了!!!卖货了!!!^^
源码 newchenxf/OpenGLESDemo

6 参考文献

骨骼蒙皮动画(SkinnedMesh)的原理解析
3D骨骼动画(一):原理
Assimp库实现骨骼蒙皮动画
Csharp实现骨骼动画

1 前言前面一篇文章android OpenGL渲染3D模型文件介绍了渲染3D模型的方式,但是,它还是静态的,模型本身不会动,还是不够炫酷。所以本文来讨论一下如何让模型自己动起来。想要动起来,就需要传说中的骨骼动画了。 一般大部分模型文件都支持带骨骼动画的数据,例如fbx, dae,但也有个别不支持,例如obj。本文分两部分讨论,一是捋一下骨骼动画的背景知识,二是在android上怎么用openGL ES渲染。当然了,渲染骨骼动画还是比较麻烦的,大部分场景下,还是走游戏引擎,例如unity。2
此资源是演示程序,大家可以免积分下载。 ---------------------------------------------- 欢迎来我的小站进行骨骼动画的讨论,地址是:http://blog.csdn.net/gamesdev ---------------------------------------------- 骨骼动画是角色动画的重要组成部分,因为只有把骨骼的位置摆准确了才能正确反映角色地姿势,后面的蒙皮动画就好做了。花了我十多天的时间,一直在琢磨着如何顺利地进行骨骼位置的变换,以达到MikuMikuDance中的样子。我做了很多实验,这回也算是有一定的成果吧,虽然关于反向运动学(Inverse Kinematics,IK)的部分我还没有吃透,不过做出一个东西,想分享分享。
  本书从实践的角度出发,详细介绍3D游戏开发的高级技术,并具体描述了一个游戏引擎的构建过程。全书着重讨论三个主题:游戏开发的一般过程;实时渲染过程;角色动画。所有主题均围绕一个具体的游戏开发系统Fly3D SDK2.0加以介绍。   本书旨在为当今的三维游戏引擎技术提供一个综合的解决方案,使读者尽快地进入开发者角色,了解整个游戏的开发过程并初步具备游戏引擎开发能力。   本书适合作为高等院校相关专业的教学参考书,同时可供相关技术人员和游戏开发人员阅读。   本书从实践的角度出发,详细介绍3D游戏开发的高级技术,并具体描述了一个游戏引擎的构建过程。全书着重讨论三个主题:游戏开发的一般过程;实时渲染过程;角色动画。所有主题均围绕一个具体的游戏开发系统Fly3D SDK2.0加以介绍。   本书旨在为当今的三维游戏引擎技术提供一个综合的解决方案,使读者尽快地进入开发者角色,了解整个游戏的开发过程并初步具备游戏引擎开发能力。   本书适合作为高等院校相关专业的教学参考书,同时可供相关技术人员和游戏开发人员阅读。   Alan Watt 英国谢菲尔德大学计算机科学系讲师,是该校计算机图表学研究室主任,曾经编写过多本优秀著作,包括《3D计算机图形学》和《The Computer Lmage》。 Fabio Policarpo 工作在里约热内卢的软件开发者,他是Paralelo计算机公司的创始人,目前正致力于三维动作多玩家游戏的研究。 出版者的话 专家指导委员会 第一部分 高级游戏系统剖析 第1章 高级游戏系统剖析I:构造过程和静态光照 1.1 数据结构 1.1.1 顶点 1.1.2 面 1.1.3 包围盒 1.2 构造过程 1.2.1 从场景几何中创建BSP树 1.2.2 路径规划的凸体和PVS计算 1.2.3 处理复杂的地形 1.2.4 BSP叶节点中的面 1.2.5 寻找叶凸体 1.2.6 凸体和伪人口 1.2.7 潜在可视集 1.3 光照贴图的构造 1.3.1 生成光照贴图的坐标 1.3.2 光照贴图的打包 1.3.3 对光照贴图的解释 1.4 BSP管理 1.5 高级静态光照——辐射度 附录1.1 构造实践 附录1.2 辐射度理论基础 第2章 高级游戏系统剖析Ⅱ:实时处理 2.1 视见和BSP 2.1.1 生成视见约束体的面 2.1.2 远近裁剪面和视见约束体 2.2 照相机控制 2.3 使用BSP的基本碰撞检测和反弹 2.3.1 碰撞和BSP遍历 2.3.2 粒子,场景检测和反弹 2.4 特殊的碰撞检测和反弹 2.4.1 AABB的定义 2.4.2 AABB类的定义和静态成员的定义 2.4.3 碰撞检测和碰撞反弹 2.4.4 使用AABB的伪碰撞反弹 2.4.5 使用AABB的碰撞检测 2.4.6 AABB顶点与场景面相交 2.4.7 场景顶点与AABB面相交 2。4.8 AABB边与场景边相交 2.4.9 更精确的碰撞检测 2.4.10 使用碰撞阈值 2.5 基本的路径规划 附录2.1 实时处理的演示 第3章 高级游戏系统剖析Ⅲ:软件设计与应用编程 3.1 应用的种类 3.1.1 插件 3.1.2 前端 3.1.3 工具 3.2 Fly3D引擎体系结构 3.2.1 FlyMath 3.2.2 FlyDirectX 3.2.3 FlyRender 3.2.4 FlyEngine 附录3.1 编写一个插件 第二部分 实时渲染 第4章 实时渲染 4.1 简介 4.2 顶点、像素和贴图 4.2.1 基本的逐像素着色 4.2.2 着色和坐标空间 4.2.3 25年来主流的插值着色方法和颜色贴图 4.2.4 标量表示 4.3 因式分解法 4.3.1 使用因式分解着色模型的逐像素着色——各向同性模型 4.3.2 使用因式分解着色模型的逐像素着色——各向异性模型 4.4 BRDF和真实材质 4.5 使用BRDF进行逐像素着色 4.6 环境贴图参数化 4.6.1 环境贴图参数化:立方映射 4.6.2 环境贴图参数化:球面映射 4.6.3 环境贴图参数化:对偶抛物面贴图 4.6.4 环境贴图——可比点 4.6.5 立方贴图和向量规范化 4.7 实现BRDF:可分离的近似 4.8 着色语言和着色器 4.8.1 着色语言:简单的历史回顾 4.8.2 RenderMan着色语言 4.8.3 实时渲染的着色语言 第5章 实时渲染:实践 5.1 基本着色器 5.1.1 渲染状态 5.1.2 着色器排序 5.1.3 着色器类的实现 5.2 渲染状态 5.2.1 全局设定 5.2.2 局部设定 5.3 着色器实例 5.3.1 环境映射和铬映射效果——玻璃、金属和铬 5.3.2 移动发光告示牌
课程简介:本课程详细讲解基于Assimp C++库的模型读取模块,并且做了关于动画理论、关键帧插值、骨骼动画矩阵原理、骨骼动画读取与播放等知识的详细讲解,对于游戏行业或者三维可视化从业人员会有比较大的帮助。目前很多公司已经开始构建自己的底层图形引擎,其中动画就是重要的一个版块,本课程可以让学员从原理层面以及底层代码层面了解FBX、OBJ模型的读取本质,并且梳理程序架构,编写骨骼动画。2 课程解决优势:很多同学学习骨骼动画苦于无法找到详细的资料,其中卡主的问题点也比比皆是,比如FBX内嵌材质的读取,骨骼动画各类矩阵的应用,理论结合模型读取库读出来的数据如何一一对应等。我们的课程可以领大家从原理+实践的角度进行学习,每一个知识点都会:a 推导基础公式及原理 b 一行一行进行代码实践从而能够保证每位同学都学有所得,能够看得懂,学得会,用得上,并且能够培养自主研究的能力。3 学习课程所得:学习本课程完毕之后,学员可以全方位的完全了解基于Assimp库的模型读取结构,了解每一个变量背后的含义,并且课程拥有随堂附赠的源代码,保证同学可以随时根据老师的代码纠正自己的错误。跟随课程一行一行写完代码的同学,可以获得自己的模型读取代码库,并且深度理解骨骼动画的原理与模型读取原理 本课程含有全源代码
hellworldcss3d 第一次写自己的插件,主要功能是用css 3d的属性生成伪3d的模型。 之前从没接触过css 3d ,css做动画就是我的弱项,一直以为要玩3d就得用webGL之类的,直到后来看到淘宝造物节,被经验到了,花了2天时间去研究怎么写。 后来做完后我就想,为什么不写一个插件来生成简单的3d模型,就像Minecraft一样的世界。 然后就有了这个项目 代码还没优化,看起来软七八糟的,预计以后还会更新,如果你有兴趣也可以fork改一改 引入 hellworld3d.js 定义一个三维数组 var data=[ [6],[6],[6],[6],[6],[6], [6],[6],[6],[6],[6],[6],
本教程将介绍如何使用GLSL在OpenGL 4.0中渲染3D模型。本教程中的代码基于漫反射教程中的代码https://blog.csdn.net/weixin_44210987/article/details/109300986。 在之前的教程中,我们已经渲染过3D模型,但是它们是由单个三角形组成的,相当没意思。现在已经涵盖了基础知识,我们将继续渲染一个更复杂的对象。在这种情况下,对象将是一个立方体。在介绍如何渲染更复杂的模型之前,我们将首先讨论模型格式。
### 回答1: Qtopengl是一个用于实现跨平台OpenGL应用程序的C++库。在导入3D模型方面,可以使用Qtopengl中的QGLWidget类和OpenGL的相关函数进行加载和渲染。 在加载3D模型之前,需要将模型转换为支持的格式,如obj、dae、fbx等。在此可以使用开源的3D建模软件Blender来进行转换,也可以使用其他支持的软件。 加载3D模型时,可以使用OpenGL中的glTranslatef()、glScalef()和glRotatef()函数对模型进行缩放、旋转和平移。然后使用glBegin()和glEnd()函数对模型进行绘制,可以使用三角形和四边形来绘制3D模型。 同时,还可以使用纹理来给模型添加图片或其他视觉效果。在OpenGL中,可以使用glGenTextures()、glBindTexture()和glTexImage2D()等函数来加载和应用纹理。 总的来说,导入3D模型需要先将其转换为支持的格式,然后使用Qtopengl中的QGLWidget类和OpenGL相关的函数进行加载、渲染和绘制。在这个过程中,还可以使用纹理来增强模型的视觉效果。 ### 回答2: qtopengl是一个强大的3D渲染工具,可以导入3D模型并进行渲染。在导入3D模型之前,需要准备好一个3D模型文件,目前比较常用的3D模型文件格式有OBJ、FBX、3DS等。 首先需要在qtopengl中创建一个OpenGL窗口,然后加载3D模型文件,这可以通过使用OpenGL的glLoadModel函数来完成。在加载完成之后,需要设置好3D场景中的参数,例如相机位置、光照等。 在qtopengl中操作3D模型需要用到渲染循环,通常我们可以把渲染部分写在paintGL函数中。在计算好相机位置和光照后,可以用OpenGL的glDraw函数来进行3D模型渲染。 在渲染过程中需要注意的是,不同的3D模型文件格式可能会有一些差异,例如在OBJ文件中,一个模型可能会包含多个材质和纹理,需要分别进行处理。此外,当3D模型非常大时,可能会导致内存溢出的问题,这时可以使用一些优化技巧来减少内存的使用。 总的来说,qtopengl是一个非常强大的3D渲染工具,可以方便地导入3D模型并进行高效的渲染。对于想要进行3D开发的人来说,掌握qtopengl的技巧是非常重要的。 ### 回答3: 使用QOpenGL导入3D模型,一般需要经历如下步骤: 第一步,需要先对3D模型进行导出。通常,3D模型的导出格式有很多种,比如OBJ、FBX、3DS等等。因此,首先需要根据模型的实际情况选择合适的导出格式。 第二步,引入QOpenGL库。在Qt中,可以通过在.pro文件中加入QT += opengl语句来引入OpenGL库。 第三步,加载模型文件。这需要用到Qt中的QFileDialog类,通过打开文件对话框获取用户选择的3D模型文件路径和名称。 第四步,模型解析。在此步骤中,可能需要使用第三方库,如Assimp、FBX SDK等。根据所选的模型文件格式,解析该模型文件中包含的顶点、纹理、材质、法线等信息。 第五步,绑定和渲染。这一步骤是将解析得到的数据绑定到OpenGL中,并在渲染时进行相关的操作,如顶点缓冲区对象的绑定,着色器程序的编译和链接,材质贴图的绑定等。 最后,需要在OpenGL渲染循环函数中进行模型的绘制和更新操作,以达到不断刷新画面的效果。 总之,QOpenGL导入3D模型的过程需要较多的技术支持和编程经验,建议在进行相关开发时认真学习相关知识和技术,确保在实现功能的同时也保证开发的效率和质量。
chitiantong: 博主好,在unity中调用安卓的readYBuffer和readUVBuffer时会报错,不知道您有没有遇到过? W/Unity: AndroidJNIHelper: converting Byte array is obsolete, use SByte array instead W/Unity: AndroidJNIHelper.GetSignature: using Byte parameters is obsolete, use SByte parameters Unity为人物模型 添加动效Animator qq_49149394: 怎么弄的啊 一款简单好用的Unity任务系统 向氏UV: 学生自用,可以发我一份吗?2014639717@qq.com C++的std::move与std::forward原理大白话总结 newchenxf: 一款简单好用的Unity任务系统 newchenxf: 已发送哦,抱歉抱歉答复晚了