Hands-on C++ Game Animation Programming阅读笔记(六)

Chapter 9: Implementing Animation Clips

An animation clip is a collection of the TransformTrack objects. An animation clip animates a collection of transforms
over time and the collection of transforms that is animated is called a pose. Think of a pose as the skeleton of an animated
character at a specific point in time. A pose is a hierarchy of transforms. The value of each transform affects all of its
children.

一个Animation Clip本质上就是各个Bone的Transform Track对象的集合,也就是一堆Bone的Transform的各个Component随时间变化的Curve的集合。Pose可以视作在一个特定时间的所有Bone组成的Skeleton的Transform(A pose is a hierarchy of transforms)。当Sample一个Animation Clip时,返回的是一个Pose,每个Animation Clip有多个Animation Track,每个Track又由多个关键帧Frame数据组成,如下图所示:
在这里插入图片描述


Implementing poses

前面说了,一个Pose其实就是一堆Transform的数据,只不过这些Transform形成了一个Hierarchy而已,所以这里使用两个vector来表示Pose:

  • 第一个vector的元素就是Transform数据,每个Transform数据也就是两个vec3和一个quaternion,每个Transform数据对应一个Joint
  • 第二个vector的元素是与第一个vector一一对应的,它代表对应的Joint的Parent在数组里的ID,如果Joint没有Parent,那么对应数组内的值为负

举个例子,比如说,有0、1、2三个Joint,那么第一个vector为vector<Transform>,size为3,分别对应三个Joint在当前时间的数据,而第二个vector为vector<int>,其值为{-1, 0, 0},那么表示0号Joint为Root,1和2号Joint的Parent都为0号Joint

除了skeleton有一个Root Node,书里提到,有些文件格式会有多个Root Node,如下图所示的GLTF文件,其中一个是Joints的Root Node,一个是Skinned Mesh的Root Node,还有一个是Lights的Root Node:
在这里插入图片描述

对于一个有动画的角色而言,角色的Pose可以分为常用的三种类型

  • the current pose: 也就是Sample Animation Clip返回的Hierarchy of Joints的所有Transform数据
  • the bind pose: (covered in the next chapter)
  • the rest pose: the default configuration of all the bones.(感觉类似于T-Pose?)

创建Pose类

The Pose class needs to keep track of the transformation of every joint in the skeleton of the character that you are animating. It also needs to keep track of the parent joint of every joint. This data is kept in two parallel vectors.

类的声明如下:

#ifndef _H_POSE_
#define _H_POSE_

#include <vector>
#include "Transform.h"

class Pose 
{
protected:
	// 本质数据就是两个vector, 一个代表Joints的hierarchy, 一个代表Joints的数据
	std::vector<Transform> mJoints;
	std::vector<int> mParents;
public:
	// 拥有Ctor, copy constructor和copy assignment operator
	Pose();
	Pose(unsigned int numJoints);
	Pose(const Pose& p);
	Pose& operator=(const Pose& p);
	void Resize(unsigned int size);
	unsigned int Size();
	Transform GetLocalTransform(unsigned int index);
	void SetLocalTransform(unsigned int index, const Transform& transform);
	Transform GetGlobalTransform(unsigned int index);
	Transform operator[](unsigned int index);// 使用[]会默认返回Joint的Global Transform
	// palette是调色板的意思, 由于OpenGL只接受linear array of matrices
	// 为了把Pose数据传给OpenGL, 它可以用矩阵的方式接受Transform
	// 这个函数会根据自己的Transform数组, 创建一个mat4的数组并返回
	void GetMatrixPalette(std::vector<mat4>& out);
	int GetParent(unsigned int index);
	void SetParent(unsigned int index, int parent);

	bool operator==(const Pose& other);
	bool operator!=(const Pose& other);
};

#endif // !_H_POSE_

The Pose class is used to hold the transformation of every bone in an animated hierarchy. Think of it as a frame in an animation; the Pose class represents the state of an animation at a given time.


接下来就可以实现Pose类了:

#include "Pose.h"

Pose::Pose() { }

Pose::Pose(unsigned int numJoints) 
{
	Resize(numJoints);
}

// copy ctor居然这么写, 这里做的是浅拷贝, 拷贝的是引用(感觉是在当C#在用, 不过这里
// 其实没有手动在heap上进行分配, 用默认的copy ctor也是可以的)
Pose::Pose(const Pose& p) 
{
	*this = p;
}

// overload copy assignment operator
Pose& Pose::operator=(const Pose& p) 
{
	if (&p == this)
		return *this;

	// 同步this的两个数组的size
	if (mParents.size() != p.mParents.size()) 
		mParents.resize(p.mParents.size());
	
	if (mJoints.size() != p.mJoints.size())
		mJoints.resize(p.mJoints.size());

	// 使用memcpy去copy(直接使用vector的=不好么), 哦, 那样好像是shallow copy
	if (mParents.size() != 0) 
		memcpy(&mParents[0], &p.mParents[0], sizeof(int) * mParents.size());
	
	if (mJoints.size() != 0)
		memcpy(&mJoints[0], &p.mJoints[0], sizeof(Transform) * mJoints.size());

	return *this;
}

void Pose::Resize(unsigned int size) 
{
	mParents.resize(size);
	mJoints.resize(size);
}

unsigned int Pose::Size() 
{
	return mJoints.size();
}

Transform Pose::GetLocalTransform(unsigned int index) 
{
	return mJoints[index];
}

void Pose::SetLocalTransform(unsigned int index, const Transform& transform) 
{
	mJoints[index] = transform;
}

// 按Hierarchy层层Combine
Transform Pose::GetGlobalTransform(unsigned int index) 
{
	Transform result = mJoints[index];
	// 从底部往上Combine, 因为每个Joint的Parent Joint在vector里的id是可以获取到的
	for (int parent = mParents[index]; parent >= 0; parent = mParents[parent]) 
		// Combine这个函数是之前在mat4.cpp里实现的全局函数, 其实就是return matA*matB
		result = combine(mJoints[parent], result);

	return result;
}

Transform Pose::operator[](unsigned int index) 
{
	return GetGlobalTransform(index);
}

// vector<Transform> globalTrans 转化为mat4数组
void Pose::GetMatrixPalette(std::vector<mat4>& out) 
{
	unsigned int size = Size();
	if (out.size() != size)
		out.resize(size);

	for (unsigned int i = 0; i < size; ++i) 
	{
		Transform t = GetGlobalTransform(i);
		out[i] = transformToMat4(t);
	}
}

int Pose::GetParent(unsigned int index) 
{
	return mParents[index];
}

void Pose::SetParent(unsigned int index, int parent) 
{
	mParents[index] = parent;
}

// 判断两个Pose是否相等
bool Pose::operator==(const Pose& other) 
{
	// 先判断size
	if (mJoints.size() != other.mJoints.size()) 
		return false;

	if (mParents.size() != other.mParents.size())
		return false;
	
	unsigned int size = (unsigned int)mJoints.size();
	// 遍历每个Joint, 其实就是遍历俩vector
	for (unsigned int i = 0; i < size; ++i) 
	{
		Transform thisLocal = mJoints[i];
		Transform otherLocal = other.mJoints[i];

		int thisParent = mParents[i];
		int otherParent = other.mParents[i];

		if (thisParent != otherParent)
			return false;

		if (thisLocal != otherLocal)
			return false;
	}
	return true;
}

bool Pose::operator!=(const Pose& other) 
{
	return !(*this == other);
}

Implementing Clips

在创建完Pose类之后,就可以实现Animation Clip类了,Sample Animation Clip的结果,就是返回一个Pose,Animation Clip本质就是一堆Track,这里甚至可以认为它本质就是一堆Transform的Track,每个Transform的Track对应一个Joint在这段动画里的Curves,加上一个Skeleton,对应的Joints的hierarchy。

相关头文件如下:

#ifndef _H_CLIP_
#define _H_CLIP_

#include <vector>
#include <string>
#include "TransformTrack.h"
#include "Pose.h"

class Clip 
{
protected:
	// 本质就是TransformTracks
	std::vector<TransformTrack> mTracks;
	// 一些用于辅助播放的metadata
	std::string mName;
	float mStartTime;
	float mEndTime;
	bool mLooping;
protected:
	float AdjustTimeToFitRange(float inTime);
public:
	Clip();
	// 获取第index个Track对应的Joint的id
	unsigned int GetIdAtIndex(unsigned int index);
	// 设置第index个Track对应的Joint的id
	void SetIdAtIndex(unsigned int index, unsigned int id);
	unsigned int Size();
	// 这里有两种方法从Clip里获取数据, 一个是直接Sample, 返回Pose, 另一个是直接取第i个Joint的TransformTrack
	// Sample Animation Clip, 结果是一个Pose, 这里的return值表示把inTime进行调整后的结果
	float Sample(Pose& outPose, float inTime);
	// 如果没有TransformTrack数据, 会创建一个默认的TransformTrack对象
	TransformTrack& operator[](unsigned int index);
	// 这个函数会遍历Clip里面的Track数据, 计算出mStartTime和mEndTime
	void RecalculateDuration();
	std::string& GetName();
	void SetName(const std::string& inNewName);
	float GetDuration();
	float GetStartTime();
	float GetEndTime();
	bool GetLooping();
	void SetLooping(bool inLooping);
};

#endif 

具体的类实现如下:

#include "Clip.h"

Clip::Clip() 
{
	mName = "No name given";
	mStartTime = 0.0f;
	mEndTime = 0.0f;
	mLooping = true;
}

// 这里的Sample函数还对输入的Pose有要求, 因为Clip里的Track如果没有涉及到每个Component的
// 动画, 则会按照输入Pose的值来播放, 所以感觉outPose输入的时候要为(rest Pose(T-Pose or A-Pose))
float Clip::Sample(Pose& outPose, float time) 
{
	if (GetDuration() == 0.0f) 
		return 0.0f;
	
	time = AdjustTimeToFitRange(time);// 调用Clip自己实现的函数

	unsigned int size = mTracks.size();
	for (unsigned int i = 0; i < size; ++i) 
	{
		unsigned int joint = mTracks[i].GetId();
		Transform local = outPose.GetLocalTransform(joint);
		Transform animated = mTracks[i].Sample(local, time, mLooping);
		outPose.SetLocalTransform(joint, animated);
	}
	
	return time;
}

// 不仅仅是Track实现了这个函数, 这里的Clip也实现了这个函数
float Clip::AdjustTimeToFitRange(float inTime) 
{
	// 其实就是根据mLooping, mStartTime和mEndTime对时间进行处理
	if (mLooping) 
	{
		float duration = mEndTime - mStartTime;
		if (duration <= 0)
			return 0.0f;
		
		inTime = fmodf(inTime - mStartTime, mEndTime - mStartTime);
		if (inTime < 0.0f)
			inTime += mEndTime - mStartTime;
		
		inTime = inTime + mStartTime;
	}
	else 
	{
		if (inTime < mStartTime) 
			inTime = mStartTime;
			
		if (inTime > mEndTime) 
			inTime = mEndTime;
	}
	return inTime;
}

// 其实就是遍历所有的Joints的TransformTrack, 找到最早和最晚的关键帧的出现时间
// 设置给mStartTime和mEndTime
void Clip::RecalculateDuration() 
{
	mStartTime = 0.0f;
	mEndTime = 0.0f;
	// 这俩相当于first进入的flag
	bool startSet = false;
	bool endSet = false;
	unsigned int tracksSize = (unsigned int)mTracks.size();
	// 遍历每个Joint的TransformTrack
	for (unsigned int i = 0; i < tracksSize; ++i) 
	{
		if (mTracks[i].IsValid()) 
		{
			float trackStartTime = mTracks[i].GetStartTime();
			float trackEndTime = mTracks[i].GetEndTime();

			// 如果trackStartTime小于mStartTime或者startSet为false
			if (trackStartTime < mStartTime || !startSet) 
			{
				// 所以mStartTime的默认值其实是第一个Track的GetStartTime的结果
				mStartTime = trackStartTime;
				startSet = true;
			}

			if (trackEndTime > mEndTime || !endSet) 
			{
				mEndTime = trackEndTime;
				endSet = true;
			}
		}
	}
}


TransformTrack& Clip::operator[](unsigned int joint) 
{
	// 遍历所有的TransformTrack, 获取里面存的Joint的id, 如果找到了指定id的joint
	// 则返回在mTracks里的ID
	for (unsigned int i = 0, size = (unsigned int)mTracks.size(); i < size; ++i) 
	{
		if (mTracks[i].GetId() == joint)
			return mTracks[i];
	}

	// 如果没有这个Joint的动画数据, 说明它在动画里没变过, 这里就创建一个默认的TransformTrack返回
	// 感觉好像没必要啊? 既然没变过, 创建的默认的TransformTrack也不对啊, 会改变它的Transform成默认状态的
	// 除非该Joint本身就是单位矩阵对应的Transform, 不然会有问题
	mTracks.push_back(TransformTrack());
	mTracks[mTracks.size() - 1].SetId(joint);
	return mTracks[mTracks.size() - 1];
}

std::string& Clip::GetName() 
{
	return mName;
}

void Clip::SetName(const std::string& inNewName) 
{
	mName = inNewName;
}

unsigned int Clip::GetIdAtIndex(unsigned int index) 
{
	return mTracks[index].GetId();
}

void Clip::SetIdAtIndex(unsigned int index, unsigned int id) 
{
	return mTracks[index].SetId(id);
}

unsigned int Clip::Size() 
{
	return (unsigned int)mTracks.size();
}

float Clip::GetDuration() 
{
	return mEndTime - mStartTime;
}

float Clip::GetStartTime() 
{
	return mStartTime;
}

float Clip::GetEndTime() 
{
	return mEndTime;
}

bool Clip::GetLooping() 
{
	return mLooping;
}

void Clip::SetLooping(bool inLooping) 
{
	mLooping = inLooping;
}

当单独的一个动画进行播放的时候,原本人物播放动画之前的Pose是不怎么重要的,因为动画会全权接管这里每个Joint的Transform。但是如果角色从动画A切换到动画B,此时就需要先re-set pose,再去播放动画B了,否则可能是A动画修改了某个Joint的位置,然后B动画里又没有这个Joint的动画数据,就保持不变了。(这里的动画里读取的Pose,好像叫Bind Pose)

It’s a good idea to reset the pose that is sampled into so that it is the bind pose whenever we switch animation clips!


Rest Pose

书里提到,Reset Pose是角色不播放动画的Pose,在我理解就是游戏里的T-Pose或者A-Pose


用glTF load rest pose

这里提供的glTF文件,都是假设只有一个动画角色的,此时就可以认为glTF文件的hierarchy是一整个模型的skeleton tree了。

这里先写了一些使用cgltf库来读取glTF的helpler函数,之前创建过一个GLTFLoader.h和GLTFLoader.cpp文件,现在需要加点东西:

#ifndef _H_GLTFLOADER_
#define _H_GLTFLOADER_

#include "vendor/cgltf.h"
#include "Pose.h"
#include "Clip.h"
#include <vector>
#include <string>

namespace GLTFHelpers
{
	// 这是之前已经加好的接口
	cgltf_data* LoadGLTFFile(const char* path);
	void FreeGLTFFile(cgltf_data* handle);
}

// 新加的接口一: 从data里读取rest pose
Pose LoadRestPose(cgltf_data* data);
// 新加的接口二: 从data里读取skeleton, 每个节点的数据用string表示name
std::vector<std::string> LoadJointNames(cgltf_data* data);
// 新加的接口三: 从data里读取AnimationClips
std::vector<Clip> LoadAnimationClips(cgltf_data* data);

#endif

最主要是就是这三个功能,写这三个函数的思路如下:

  • 第一个函数,需要获得Default Pose,那么直接遍历文件里所有的Nodes,它们都代表Joints,取其Transform数据,然后还需要hierarchy的关系,这里的父子关系通过节点里面存的父指针,然后遍历nodes数组,去比对,找到数组索引作为Parent ID即可
  • 第二个函数更简单了,遍历nodes数组,取其name,存到vector即可,对应的hierarchy,获取父节点的方式,其实跟方法一是一样的
  • 第三个函数需要遍历data里的每个Clip,然后遍历Clip里的每个Animation Channel,每个Channel其实对应一个Joint的Property Curve,利用Channel里的Sampler获取所有的关键帧数据,转化为这边的Clip里的数据,存储起来即可

这里提供了一堆GLTF的helper函数,具体的cpp文件如下:

#include "GLTFLoader.h"
#include <iostream>
#include "Transform.h"
#include "Track.h"
#include <vector>

namespace GLTFHelpers
{
	cgltf_data* LoadGLTFFile(const char* path)
	{
		// 加载之前, 要创建一个cgltf_options类的对象
		cgltf_options options;
		memset(&options, 0, sizeof(cgltf_options));
		// 使用库文件, 把data和options都读取出来
		cgltf_data* data = NULL;
		// cgltf_result是个枚举
		cgltf_result result = cgltf_parse_file(&options, path, &data);

		// check
		if (result != cgltf_result_success)
		{
			std::cout << "Could not load input file: " << path << "\n";
			return 0;
		}

		// 根据options和path, 把数据读到data里, 这里的options和path传入的都是const 
		result = cgltf_load_buffers(&options, data, path);
		if (result != cgltf_result_success)
		{
			cgltf_free(data);
			std::cout << "Could not load buffers for: " << path << "\n";
			return 0;
		}

		// 再次check
		result = cgltf_validate(data);
		if (result != cgltf_result::cgltf_result_success)
		{
			cgltf_free(data);
			std::cout << "Invalid gltf file: " << path << "\n";
			return 0;
		}
		return data;
	}

	void FreeGLTFFile(cgltf_data* data)
	{
		if (data == 0)
			std::cout << "WARNING: Can't free null data\n";
		else
			cgltf_free(data);
	}

	// 这里默认认为所有的Node都代表一个Joint
	Transform GetLocalTransform(cgltf_node& node) 
	{
		Transform result;

		// gltf的节点可以用Transform或者矩阵两种方式来存储Transform数据
		// 如果有矩阵的信息, 则直接转为Transform
		if (node.has_matrix) 
		{
			mat4 mat(&node.matrix[0]);
			result = mat4ToTransform(mat);
		}

		if (node.has_translation) 
			result.position = vec3(node.translation[0], node.translation[1], node.translation[2]);

		if (node.has_rotation)
			result.rotation = quat(node.rotation[0], node.rotation[1], node.rotation[2], node.rotation[3]);

		if (node.has_scale)
			result.scale = vec3(node.scale[0], node.scale[1], node.scale[2]);

		return result;
	}

	// 获取joint对应的node在gltf节点数组里的id, 其实就是线性遍历的查找
	// 类似于数组的FindIndexAt
	int GetNodeIndex(cgltf_node* target, cgltf_node* allNodes, unsigned int numNodes) 
	{
		if (target == 0)
			return -1;
	
		for (unsigned int i = 0; i < numNodes; ++i) 
		{
			if (target == &allNodes[i]) 
				return (int)i;
		}

		return -1;
	}

	// 从gltf accessor里读取float数组的值
	void GetScalarValues(std::vector<float>& outScalars, unsigned int inComponentCount, const cgltf_accessor& inAccessor) 
	{
		outScalars.resize(inAccessor.count * inComponentCount);
		for (cgltf_size i = 0; i < inAccessor.count; ++i) 
			cgltf_accessor_read_float(&inAccessor, i, &outScalars[i * inComponentCount], inComponentCount);
	}

	// 核心函数, 从channel的sampler里获取Track, 其实只用到了channel的input, 没有用到output
	// This function does most of the heavy lifting. It converts a glTF animation channel into a
    // VectorTrack or a QuaternionTrack.
	// animation channel可以参考后面的附录, 本质上就是一个记录sampler和target joint引用的wrapper
	template<typename T, int N>
	void TrackFromChannel(Track<T, N>& inOutTrack, const cgltf_animation_channel& inChannel) 
	{
		// Sampler可以当作一个针对特定Property的有SampleAnimationClip功能的对象
		cgltf_animation_sampler& sampler = *inChannel.sampler;

		// 根据sampler获取
		Interpolation interpolation = Interpolation::Constant;
		if (inChannel.sampler->interpolation == cgltf_interpolation_type_linear) 
			interpolation = Interpolation::Linear;
		else if (inChannel.sampler->interpolation == cgltf_interpolation_type_cubic_spline) 
			interpolation = Interpolation::Cubic;
		bool isSamplerCubic = interpolation == Interpolation::Cubic;
		inOutTrack.SetInterpolation(interpolation);

		// 从sampler里获取俩数组, 一个代表关键帧的时间的float数组, 一个代表Property的关键帧的数组
		// 这俩数组的大小应该是一样的(说的是实际数据大小, 不是float的大小)
		std::vector<float> timelineFloats;
		GetScalarValues(timelineFloats, 1, *sampler.input);

		// output数组是已经在sampler里算好的
		// 如果是Constant和Linear的情况下, 它就是个Property的数组
		// 如果是Cubic情况下, 它是个Property、Property对应的mIn和mOut三个属性组成的对象的数组
		std::vector<float> valueFloats;
		GetScalarValues(valueFloats, N, *sampler.output);

		unsigned int numFrames = (unsigned int)sampler.input->count;
		// property由几个float组成
		unsigned int numberOfValuesPerFrame = valueFloats.size() / timelineFloats.size();
		inOutTrack.Resize(numFrames);
		// 遍历关键帧
		for (unsigned int i = 0; i < numFrames; ++i) 
		{
			int baseIndex = i * numberOfValuesPerFrame;
			// 获取frame的引用, 这里面的值应该是空的
			Frame<N>& frame = inOutTrack[i];
			int offset = 0;

			// sapmler的Input数组里获取每个关键帧的时间
			frame.mTime = timelineFloats[i];

			// 遍历Property的每个float component
			
			// 只有Cubic的采样情况下, 才需要mIn和mOut数据
			for (int component = 0; component < N; ++component) 
				frame.mIn[component] = isSamplerCubic ? valueFloats[baseIndex + offset++] : 0.0f;
			
			for (int component = 0; component < N; ++component) 
				frame.mValue[component] = valueFloats[baseIndex + offset++];
			
			for (int component = 0; component < N; ++component) 
				frame.mOut[component] = isSamplerCubic ? valueFloats[baseIndex + offset++] : 0.0f;
		}
	}
} // End of GLTFHelpers

// 接口一实现
// 借助GetLocalTransform和GetNodeIndex函数, 就可以读取出RestPose了
// 由于这里与动画系统逻辑相关, 所以不在GLTFHelpers的namespace里
// Pose的本质就是骨骼hierarchy和一个Transform数组, 与joint一一对应
Pose LoadRestPose(cgltf_data* data) 
{
	// 获取joint的个数
	unsigned int boneCount = (unsigned int)data->nodes_count;
	Pose result(boneCount);

	for (unsigned int i = 0; i < boneCount; ++i) 
	{
		cgltf_node* node = &(data->nodes[i]);

		// 所以读取的Node的默认的LocalTransform就是这里的Rest Pose的值
		Transform transform = GLTFHelpers::GetLocalTransform(data->nodes[i]);
		result.SetLocalTransform(i, transform);

		// 手动设置pose里的joint的父子关系
		int parent = GLTFHelpers::GetNodeIndex(node->parent, data->nodes, boneCount);
		result.SetParent(i, parent);
	}

	return result;
}

// 接口2: 遍历nodes数组, 取其name, 存到vector即可
std::vector<std::string> LoadJointNames(cgltf_data* data) 
{
	unsigned int boneCount = (unsigned int)data->nodes_count;
	std::vector<std::string> result(boneCount, "Not Set");

	for (unsigned int i = 0; i < boneCount; ++i) 
	{
		cgltf_node* node = &(data->nodes[i]);

		if (node->name == 0)
			result[i] = "EMPTY NODE";
	
		else
			result[i] = node->name;
	
	}

	return result;
}

// 接口3: 从data里遍历读取clip, 遍历里面的channel, 每个Joint对应Clip里的一个Joint的TransformTrack
// 从而可以一一填到我这边数据的Track里
std::vector<Clip> LoadAnimationClips(cgltf_data* data) 
{
	// 获取clips的数目
	unsigned int numClips = (unsigned int)data->animations_count;
	// 获取joints的数目
	unsigned int numNodes = (unsigned int)data->nodes_count;

	std::vector<Clip> result;
	result.resize(numClips);

	// 遍历Clip数据
	for (unsigned int i = 0; i < numClips; ++i) 
	{
		// data->animations相当于clips的数组, 这里就是挖数据了
		result[i].SetName(data->animations[i].name);

		// 遍历clips里的每个Channel, 前面提过了channel是个通道, 一端是一个property对应Curve的sampler
		// 另一端是apply property的joint, 每个Channel对应动画Clip里的一条Property Curve
		unsigned int numChannels = (unsigned int)data->animations[i].channels_count;
		// 遍历所有curve
		for (unsigned int j = 0; j < numChannels; ++j) 
		{
			cgltf_animation_channel& channel = data->animations[i].channels[j];
			// channel的Output是Joint Node
			cgltf_node* target = channel.target_node;
			int nodeId = GLTFHelpers::GetNodeIndex(target, data->nodes, numNodes);

			// 看Curve是Transform的哪种类型
			if (channel.target_path == cgltf_animation_path_type_translation) 
			{
				// 获取对应clip的joint的nodeId对应的joint的TransformTrack
				// 之前实现过Clip类的[]重载, 会返回一个TransformTrack&
				// 这里获取的track应该还没有数据
				VectorTrack& track = result[i][nodeId].GetPositionTrack();
				// 使用Helper函数, 把这个channel里的数据提取出来, 把时间数组和Property数组
				// 甚至(mIn和mOut数组)存到track里
				GLTFHelpers::TrackFromChannel<vec3, 3>(track, channel);
			}
			else if (channel.target_path == cgltf_animation_path_type_scale) 
			{
				VectorTrack& track = result[i][nodeId].GetScaleTrack();
				GLTFHelpers::TrackFromChannel<vec3, 3>(track, channel);
			}
			else if (channel.target_path == cgltf_animation_path_type_rotation) 
			{
				QuaternionTrack& track = result[i][nodeId].GetRotationTrack();
				GLTFHelpers::TrackFromChannel<quat, 4>(track, channel);
			}
		}
		result[i].RecalculateDuration();
	}

	return result;
}



附录

glTF animation channels

参考:https://github.com/KhronosGroup/glTF-Tutorials/blob/master/gltfTutorial/gltfTutorial_007_Animations.md

如下所示,是一个glTF的json文件的例子,这里的动画有两个channels,一个是translation,一个是rotation:

  "animations": [
    {
      "samplers" : [
        {
          "input" : 2,
          "interpolation" : "LINEAR",
          "output" : 3
        },
        {
          "input" : 2,
          "interpolation" : "LINEAR",
          "output" : 4
        }
      ],
      "channels" : [ 
        {
          "sampler" : 0,
          "target" : {
            "node" : 0,
            "path" : "rotation"
          }
        },
        {
          "sampler" : 1,
          "target" : {
            "node" : 0,
            "path" : "translation"
          }
        } 
      ]
    }
  ],

Animation samplers

这里提到了这个玩意儿,samplers是一个数组,数组元素是animation.sampler对象,这个对象会决定,如何在accessor提供的关键帧数据之间插值,如下图所示,有俩数组,一个是timeAccessor指向的float数组,一个是translationAccessor指向的vec3数组,下面这个图从两个关键帧插值出了1.2s时的平移的值(感觉sampler就是一个包含了SampleAnimationClip函数的对象):
在这里插入图片描述
我看cgltf的代码里,写的Sampler有input和output两个对象,input应该是关键帧的时间数组,而output是对应的Property数组,俩数组大小应该是相同的,如下所示:

typedef struct cgltf_animation_sampler 
{
	cgltf_accessor* input;
	cgltf_accessor* output;
	cgltf_interpolation_type interpolation;
	cgltf_extras extras;
	cgltf_size extensions_count;
	cgltf_extension* extensions;
} cgltf_animation_sampler;

Animation channels

前面的文件也提到了,Animation部分的glTF的json文件里有一个Samplers数组,还有一个Channels数组,文件里Channels里还包含了Sampler对象(也就是说Sampler对象是属于Channel对象的,Channel对象记录了它的引用),数组元素为animation.channel的对象。

每一个Channel对象都会记录一个Sampler的引用(文件里实际上存储的是它在Samplers数组里的id),Channel对象负责把Sampler采集到的数据,应用到具体场景里的Node的Transform上。所以说它有个Input和Output,Input为Sampler采集的数据,Output为Node的具体Property的值,这个Node被记为animation.channel.target

所以说,这里的channel翻译为通道,通道的一端是Sampler,用于提供Joint Node的Property数据,通道的另一端是Joint Node,用于应用其Transform数据,我理解为:每个Channel对应动画Clip里的一条Property Curve

举个例子,如下图所示,有两个channels对象,id相同,所以都Animate同一个Node,它这里的path代表模拟的property的类型:

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值