Chapter 7: Exploring the glTF File Format
glTF is a standard format that can store both mesh and animation data. a file format that contains everything you need to display animated models. It’s a standard format that most three-dimensional content creation applications can export to and allows you to load any arbitrary model.
glTF: Graphics Language Transmission Format ,是一种文件格式,类似于fbx格式
本章重点:
- 了解glTF文件里存储了哪些数据
- 使用cgltf来实现glTF文件的读取
- 学会从Blender里导出glTF文件
学习之前的须知
看这张之前最好看看这个熟悉一下glTF这种文件格式: https://www.khronos.org/files/gltf20-reference-guide.pdf.
这里会使用cgltf (https://github.com/jkuhlmann/cgltf))来parse glTF文件,有的文件可能本身是坏的,此时需要参考the glTF reference viewer at https://gltfviewer.donmccurdy.com,好像是直接把文件拖进去就能检验好坏。
glTF简介
glTF was designed and specified by the Khronos Group, for the efficient transfer of 3D content over networks.
glTF文件是一种存储3D数据的文件,在网络传输上非常高效,经常用于AR,互联网传输等领域,不过在游戏和Maya里,不如Fbx文件应用广泛。
glTF可以用一个JSON文件表示,大致分为以下内容:
- scenes和nodes: Basic structure of the scene
- cameras:View configurations for the scene
- meshes: Geometry of 3D objects
- buffers, bufferViews, accessors:Data references and data layout descriptions
- materials:Definitions of how objects should be rendered
- textures, images, samplers: Surface appearance of objects
- skins: Information for vertex skinning
- animations:Changes of properties over time
各个部分之间的关系如下图所示:
glTF索引的文件路径 glTF文件并不会包含里面所有的资源,可以用存uri的方式来代表路径,如下所示:
"buffers": [
{
"uri": "buffer01.bin"
"byteLength": 102040,
}
],
"images": [
{
"uri": "image01.png"
}
],
还可以用include path的方式来写JSON文件,base64代表这个data是一个base64 encoded string(The data URI defines the MIME type, and contains the data as a base64 encoded string):
"data:application/gltf-buffer;base64,AAABAAIAAgA..."
"data:image/png;base64,iVBORw0K..."
buffers, bufferViews, accessors
- buffers: contain the data that is used for the geometry of 3D models, animations, and skinning.
- bufferViews: describes what is in a buffer. 相当于buffer的附加信息,比如要取buffer的哪部分内容,并且它可以告诉我Buffer是vertex buffer还是index buffer
- accessors: define the exact type and layout of the data. buffer具体怎么变为数据的
This accessor references a buffer view and the buffer view references a buffer. The buffer view describes what is in a buffer. If a buffer contains the information for glBufferData, then a buffer view contains some of the parameters to call glVertexAttribPointer An accessor stores higher-level information, it describes the type of data you are dealing with
在OpenGL里,需要使用glBufferData和glVertexAttribPointer两个函数来描述Buffer,这里的Buffer相当于glBufferData里的内容,而bufferView和accessors一起组成了glVertexAttribPointer里的内容(或者是index buffer里的内容)
比如:
"buffers": [
{
"byteLength": 35,
"uri": "buffer01.bin"
}
],
"bufferViews": [
{
"buffer": 0,
"byteOffset": 4,
"byteLength": 28,
"byteStride": 12,
"target": 34963
}
],
"accessors": [
{
"bufferView": 0,
"byteOffset": 4,
"type": "VEC2",
"componentType": 5126,
"count": 2,
"min" : [0.1, 0.2]
"max" : [0.9, 0.8]
}
]
Exploring how glTF files are stored
glTF文件一般用JOSN文件或者二进制文件来表示,JOSN文件一般用.gltf 后缀表示,二进制文件一般用.glb 后缀表示
glTF文件可以用三种方式来存储,如下图所示,是Blender里导出glTF文件时可以设置的选项,可以看到,glTF文件里可以包含多个文件,内部会分为多个chunk,导出的时候可以选择要不要一起放到一个glTF文件里(感觉跟fbx文件有点像) 本书里提供的glTF文件都是图中的第二种文件(glTF embedded format),就是JOSN的文本文件,不过后面还会支持其他两种格式的glTF文件。
glTF files store a scene, not a model
这个跟fbx也是一样的,除了模型,glTF文件还可以存储Cameras和PBR材质等,这里列举出glTF文件里包含的用于动画的内容——不同类型的mesh数据:
- static mesh
- morph targets
- skinned mesh(就是static mesh with weights of joints)
Exploring the glTF format
The root of a glTF file is the scene. A glTF file can contain one or more scenes. A scene contains one or more nodes. A node can have a skin, a mesh, an animation, a camera, a light, or blend weights attached to it. Meshes, skins, and animations each store large chunks of information in buffers. To access a buffer, they contain an accessor that contains a buffer view, which in turn contains the buffer
glTF文件的root就是scene,一个glTF文件可以有一个或者多个scene,每个scene包含至少一个node,一个node上面可以存mesh、camera、light、skin等数据的引用,这些数据都存在buffers里,它们都有一个accessor,这个accessor包含了一个buffer view,这个buffer view又包含了对应的buffer(?)
可以看看下面这个图帮助理解:
The parts you need for animation
下图是一个简单版的glTF里动画相关的部分的关系图:
To implement skinned animations, you won’t need lights, cameras, materials, textures, images, and samplers.
读取数据
读取Mesh、skin和animation对象都需要一个glTF accessor,This accessor references a buffer view and the buffer view references a buffer ,如下所示是它们的关系:
示例代码如下:
vector<float> GetPositions(const GLTFAccessor& accessor)
{
assert(!accessor.isSparse);
const GLTFBufferView& bufferView = accessor.bufferView;
const GLTFBuffer& buffer = bufferView.buffer;
uint numComponents = GetNumComponents(accessor);
vector<float> result;
result.resize(accessor.count * numComponents);
uint offset = accessor.offset + bufferView.offset;
for (uint i = 0; i < accessor.count; ++i)
{
uint8* data = buffer.data + offset + accessor.stride * i;
float* target = result[i] * componentCount;
for (uint j = 0; j < numComponents; ++j)
{
target[j] = data + componentCount * j;
}
}
return result;
}
加入cgltf库
如果从头读取glTF文件,那么要从JSON parser写起,这里没必要写这么底层的,所以把cgltf集成进来了,这里的库都实现在头文件里了,所以只从github.com/jkuhlmann/cgltf/blob/master/cgltf.h下载头文件,加到项目中就行了。
然后再添加一个cgltf.c 文件,保证该头文件得到了编译,内容如下:
#pragma warning(disable : 26451)
#define _CRT_SECURE_NO_WARNINGS
#define CGLTF_IMPLEMENTATION
#include "cgltf.h"
创建glTF Loader
创建负责读取的Loader函数,这里创建了俩全局函数,没有创建类,头文件如下:
#ifndef _H_GLTFLOADER_
#define _H_GLTFLOADER_
#include "cgltf.h"
cgltf_data* LoadGLTFFile(const char* path);
void FreeGLTFFile(cgltf_data* handle);
#endif
再创建对应cpp文件:
#include "GLTFLoader.h"
#include <iostream>
cgltf_data* LoadGLTFFile(const char* path)
{
cgltf_options options;
memset(&options, 0, sizeof(cgltf_options));
cgltf_data* data = NULL;
cgltf_result result = cgltf_parse_file(&options, path, &data);
if (result != cgltf_result_success)
{
std::cout << "Could not load input file: " << path << "\n";
return 0;
}
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;
}
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);
}
.fbx、.blend文件转换为.glTF文件
由于这里只能读取glTF文件,所有这里介绍一种方法,利用Blender,把.fbx和.blend文件转换为glTF文件
打开Blender,如果该文件是.blend文件,则直接打开即可,如果是.DAE或者.FBX文件,则需要把它重新导入Blender。打开Blender,删除Blender里的默认Cube,选择File->Import,打开文件后,选择File->Export,导出glTF2.0文件即可,可选择多种格式,如下图所示:
Chapter 8: Creating Curves, Frames, and Tracks
老版的动画是把关键帧处的所有人物的joints的数据都存储进来,但是这样很浪费内存。现阶段的动画就科学多了,它是用Curve来保存动画数据的,如下图所示:
本章重点:
- 理解cubic Bézier splines和对它们取值的方式
- 理解cubic Hermite splines和对它们取值的方式
- 了解常用插值方法
Understanding cubic Bézier splines
一个Bézier spline有四个点,两个点用于插值,两个点是control points,用于帮助生成曲线,如下图所示是 一个cubic Bezier spline: 可以研究一下这个曲线是怎么生成的,要从P1插值得到P2,根据C1、C2两个控制点,可以连接得到: 其中,A、B、C都是三个线段的中点。可以按照同样的方式继续取中点,得到:
再做一次相同的做法,还是取0.5部分的中点,如下图所示,得到最后的R点就是Bezier spline上的一个点: 注意,这只是插值的一个0.5部分处的点而已,各个部分的点都经过这么处理,才能得到最终的Bezier spline,在这个过程中,四个点插值一次,变成了三个点,三个点再插值一次,得到两个点,。
相关代码如下:
template<typename T>
class Bezier
{
public:
T P1;
T C1;
T P2;
T C2;
};
template<typename T>
inline T Interpolate(Bezier<T>&curve, float t)
{
T A = lerp(curve.P1, curve.C1, t);
T B = lerp(curve.C2, curve.P2, t);
T C = lerp(curve.C1, curve.C2, t);
T D = lerp(A, C, t);
T E = lerp(C, B, t);
T R = lerp(D, E, t);
return R;
}
下面介绍绘制贝塞尔曲线的方法,其实就是把上面这个函数按照不同的比例t,得到很多个点,然后把这些点连接起来,近似表示贝塞尔曲线即可,代码如下:
Bezier<vec3> curve;
curve.P1 = vec3(-5, 0, 0);
curve.P2 = vec3(5, 0, 0);
curve.C1 = vec3(-2, 1, 0);
curve.C2 = vec3(2, 1, 0);
vec3 red = vec3(1, 0, 0);
vec3 green = vec3(0, 1, 0);
vec3 blue = vec3(0, 0, 1);
vec3 magenta = vec3(1, 0, 1);
DrawPoint(curve.P1, red);
DrawPoint(curve.C1, green);
DrawPoint(curve.P2, red);
DrawPoint(curve.C2, green);
DrawLine(curve.P1, curve.C1, blue);
DrawLine(curve.P2, curve.C2, blue);
for (int i = 0; i < 199; ++i)
{
float t0 = (float)i / 199.0f;
float t1 = (float)(i + 1) / 199.0f;
vec3 thisPoint = Interpolate(curve, t0);
vec3 nextPoint = Interpolate(curve, t1);
DrawLine(thisPoint, nextPoint, magenta);
}
从上面的例子可以看出,可以通过六次线性插值完成最终的Bezier插值函数,不过上面的函数还要调用Lerp函数,并不是最优化的代码写法,这里作者对其进行数学的优化,得到的新的函数如下:
template<typename T>
inline T Interpolate(const Bezier<T>& curve, float t)
{
return curve.P1 * ((1 - t) * (1 - t) * (1 - t)) +
curve.C1 * (3.0f * ((1 - t) * (1 - t)) * t) +
curve.C2 * (3.0f * (1 - t) * (t * t)) +
curve.P2 *(t * t * t);
}
这其实就是贝塞尔插值的分解形式,把它变成了四个t的三次函数的和,如下图所示: 由于这里的t是三次函数,所以这里的贝塞尔spline属于cubic spline
Understanding cubic Hermite splines
The most common spline type used in animation for games is a cubic Hermite spline.
Unlike Bézier, a Hermite spline doesn’t use points in space for its control; rather, it uses the tangents of points along the spline. You still have four values, as with a Bézier spline, but they are interpreted differently. With the Hermite spline, you don’t have two points and two control points; instead, you have two points and two slopes. The slopes are also referred to as tangents—throughout the rest of this chapter, the slope and tangent terms will be used interchangeably
游戏的动画行业用到最常用的spline类型就是cubic Hermite spline,Bezier Spline可以用四个点来表示,两个points和两个control points,而Hermite spline也可以用四个点来表示,两个points和两个slopes,这里的slopes跟tangents(切线)的概念是一样的,具体的Curve长这样,可以看到是两个点,加两个切线的表达方式:
其函数表达如下: 对应的代码为:
template<typename T>
T Hermite(float t, T& p1, T& s1, T& p2, T& s2)
{
return p1 * ((1.0f + 2.0f * t) * ((1.0f - t) * (1.0f - t)))
+ s1 * (t * ((1.0f - t) * (1.0f - t)))
+ p2 * ((t * t) * (3.0f - 2.0f * t))
+ s2 * ((t * t) * (t - 1.0f));
}
The glTF file format supports the constant, linear, and cubic interpolation types. You just learned how to do cubic interpolation, but you still need to implement both constant and linear interpolation.
Hermite Spline与Bezier Spline
有二者互相转化的方法,但相关内容不在本书涵盖内容里。一些3D建模软件,比如Maya,可以让动画师使用Hermite Splines来创建动画,但是也有别的3D建模软件,比如Blender 3D,使用的是Bezier Curves.
动画本质就是一堆Property随时间的Curve,这里提到的hermite Soline和Bezier Spline是最常见的两种Curve
Interpolation types
也就是三种:
- Constant
- Linear
- Cubic: 这本书里用到的Cubic Curve是Hermite Spline,前面只是介绍了Bezier Spline,但后面不会使用到它。
三种类型依次如下图所示,注意这里的Constant Curve并不是一直是不变常量的Curve,从左到右感觉越来越高级了:
Creating the Frame struct
这里要考虑,一个动画,每帧的具体数据是什么,假设这个动画,只是一个Property的Curve的应用,那么对于Linear和Constant类型的Curve来说,它每帧的数据,其实就是一个value,和对应的time。
对于Cubic类型的Curve,它的帧数据要复杂一些,除了本身的value和对应的time,还需要存储该处的tangent,这里的切线有两个,一个是incoming切线,一个是outgoing切线,前者用于对在control point的前面的时间点的evaluation,后者是则是对control point的后面时间点的evaluation。
A Hermite curve is made by connecting Hermite splines. (Curve和splines的区别?) Each control point consists of a time, a value, an incoming tangent, and an outgoing tangent. The incoming tangent is used if the control point is evaluated with the point that comes before it. The outgoing tangent is used if the control point is evaluated with the point that comes after it.
一个Property的Curve数据,是多个关键帧数据的集合,每个关键帧的数据包含time、出入的tangent和对应的Property的值,具体的可以有两个表示方法:
- 第一种方法,类似于Unity的动画处理方法,就是把所有的Property都细分为每个标量对应的Curve,比如Position可以分解为x、y、z三个变量的Curve
- 第二种方法是设计specialized的frame和curve类型,比如设计scalar frame、vector frame和quaternion frame三种specialized frame类型
第二种方法会比第一种方法更好写代码,但是第一种方法更省内存,因为他每个property都可以化解为一个个scalar的curve(比如float的curve),.glTF文件里就是按照这种方式存储Animation Tracks的,而第二种,比如一个Vector3,它在动画里可能只有x和y变了,但是第二种是把它当整体存储的,所以curve也会带有z的数据,这就会消耗多的内存。
接下来就可以实现Frame类了,这是一个模板类,代码如下:
#ifndef _H_FRAME_
#define _H_FRAME_
template<unsigned int N>
class Frame
{
public:
float mValue[N];
float mIn[N];
float mOut[N];
float mTime;
};
typedef Frame<1> ScalarFrame;
typedef Frame<3> VectorFrame;
typedef Frame<4> QuaternionFrame;
#endif
创建Track类
A Track class is a collection of frames. Interpolating a track returns the data type of the track;
Track 本质上就是frames的集合,也就是一组帧数据,一个Track最少要有两个frame对象。既然Track是frames的集合,这里定义了三种特化的frame,自然也需要把Track定义为模板类,并且实现三个特化版本,头文件代码如下:
#ifndef _H_TRACK_
#define _H_TRACK_
#include <vector>
#include "Frame.h"
#include "vec3.h"
#include "quat.h"
#include "Interpolation.h"
template<typename T, int N>
class Track
{
protected:
std::vector<Frame<N>> mFrames;
Interpolation mInterpolation;
protected:
T SampleConstant(float time, bool looping);
T SampleLinear(float time, bool looping);
T SampleCubic(float time, bool looping);
T Hermite(float time, const T& point1, const T& slope1, const T& point2, const T& slope2);
int FrameIndex(float time, bool looping);
float AdjustTimeToFitTrack(float time, bool looping);
T Cast(float* value);
public:
Track();
void Resize(unsigned int size);
unsigned int Size();
Interpolation GetInterpolation();
void SetInterpolation(Interpolation interpolation);
float GetStartTime();
float GetEndTime();
T Sample(float time, bool looping);
Frame<N>& operator[](unsigned int index);
};
typedef Track<float, 1> ScalarTrack;
typedef Track<vec3, 3> VectorTrack;
typedef Track<quat, 4> QuaternionTrack;
#endif
具体的cpp代码如下:
#include "Track.h"
template Track<float, 1>;
template Track<vec3, 3>;
template Track<quat, 4>;
namespace TrackHelpers
{
inline float Interpolate(float a, float b, float t)
{
return a + (b - a) * t;
}
inline vec3 Interpolate(const vec3& a, const vec3& b, float t)
{
return lerp(a, b, t);
}
inline quat Interpolate(const quat& a, const quat& b, float t)
{
quat result = mix(a, b, t);
if (dot(a, b) < 0)
result = mix(a, -b, t);
return normalized(result);
}
inline float AdjustHermiteResult(float f)
{
return f;
}
inline vec3 AdjustHermiteResult(const vec3& v)
{
return v;
}
inline quat AdjustHermiteResult(const quat& q)
{
return normalized(q);
}
inline void Neighborhood(const float& a, float& b) { }
inline void Neighborhood(const vec3& a, vec3& b) { }
inline void Neighborhood(const quat& a, quat& b)
{
if (dot(a, b) < 0)
b = -b;
}
};
template<typename T, int N>
Track<T, N>::Track()
{
mInterpolation = Interpolation::Linear;
}
template<typename T, int N>
float Track<T, N>::GetStartTime()
{
return mFrames[0].mTime;
}
template<typename T, int N>
float Track<T, N>::GetEndTime()
{
return mFrames[mFrames.size() - 1].mTime;
}
template<typename T, int N>
T Track<T, N>::Sample(float time, bool looping)
{
if (mInterpolation == Interpolation::Constant)
return SampleConstant(time, looping);
else if (mInterpolation == Interpolation::Linear)
return SampleLinear(time, looping);
return SampleCubic(time, looping);
}
template<typename T, int N>
Frame<N>& Track<T, N>::operator[](unsigned int index)
{
return mFrames[index];
}
template<typename T, int N>
void Track<T, N>::Resize(unsigned int size)
{
mFrames.resize(size);
}
template<typename T, int N>
unsigned int Track<T, N>::Size()
{
return mFrames.size();
}
template<typename T, int N>
Interpolation Track<T, N>::GetInterpolation()
{
return mInterpolation;
}
template<typename T, int N>
void Track<T, N>::SetInterpolation(Interpolation interpolation)
{
mInterpolation = interpolation;
}
template<typename T, int N>
T Track<T, N>::Hermite(float t, const T& p1, const T& s1, const T& _p2, const T& s2)
{
float tt = t * t;
float ttt = tt * t;
T p2 = _p2;
TrackHelpers::Neighborhood(p1, p2);
float h1 = 2.0f * ttt - 3.0f * tt + 1.0f;
float h2 = -2.0f * ttt + 3.0f * tt;
float h3 = ttt - 2.0f * tt + t;
float h4 = ttt - tt;
T result = p1 * h1 + p2 * h2 + s1 * h3 + s2 * h4;
return TrackHelpers::AdjustHermiteResult(result);
}
template<typename T, int N>
int Track<T, N>::FrameIndex(float time, bool looping)
{
unsigned int size = (unsigned int)mFrames.size();
if (size <= 1)
return -1;
if (looping)
{
float startTime = mFrames[0].mTime;
float endTime = mFrames[size - 1].mTime;
float duration = endTime - startTime;
time = fmodf(time - startTime, endTime - startTime);
if (time < 0.0f)
time += endTime - startTime;
time = time + startTime;
}
else
{
if (time <= mFrames[0].mTime)
return 0;
if (time >= mFrames[size - 2].mTime)
return (int)size - 2;
}
for (int i = (int)size - 1; i >= 0; --i)
{
if (time >= mFrames[i].mTime)
return i;
}
return -1;
}
template<typename T, int N>
float Track<T, N>::AdjustTimeToFitTrack(float time, bool looping)
{
unsigned int size = (unsigned int)mFrames.size();
if (size <= 1)
return 0.0f;
float startTime = mFrames[0].mTime;
float endTime = mFrames[size - 1].mTime;
float duration = endTime - startTime;
if (duration <= 0.0f)
return 0.0f;
if (looping)
{
time = fmodf(time - startTime, endTime - startTime);
if (time < 0.0f)
time += endTime - startTime;
time = time + startTime;
}
else
{
if (time <= mFrames[0].mTime)
time = startTime;
if (time >= mFrames[size - 1].mTime)
time = endTime;
}
return time;
}
template<> float Track<float, 1>::Cast(float* value)
{
return value[0];
}
template<> vec3 Track<vec3, 3>::Cast(float* value)
{
return vec3(value[0], value[1], value[2]);
}
template<> quat Track<quat, 4>::Cast(float* value)
{
quat r = quat(value[0], value[1], value[2], value[3]);
return normalized(r);
}
template<typename T, int N>
T Track<T, N>::SampleConstant(float time, bool looping)
{
int frame = FrameIndex(time, looping);
if (frame < 0 || frame >= (int)mFrames.size())
return T();
return Cast(&mFrames[frame].mValue[0]);
}
template<typename T, int N>
T Track<T, N>::SampleLinear(float time, bool looping)
{
int thisFrame = FrameIndex(time, looping);
if (thisFrame < 0 || thisFrame >= (int)(mFrames.size() - 1))
return T();
int nextFrame = thisFrame + 1;
float trackTime = AdjustTimeToFitTrack(time, looping);
float frameDelta = mFrames[nextFrame].mTime - mFrames[thisFrame].mTime;
if (frameDelta <= 0.0f)
return T();
float t = (trackTime - mFrames[thisFrame].mTime) / frameDelta;
T start = Cast(&mFrames[thisFrame].mValue[0]);
T end = Cast(&mFrames[nextFrame].mValue[0]);
return TrackHelpers::Interpolate(start, end, t);
}
template<typename T, int N>
T Track<T, N>::SampleCubic(float time, bool looping)
{
int thisFrame = FrameIndex(time, looping);
if (thisFrame < 0 || thisFrame >= (int)(mFrames.size() - 1))
return T();
int nextFrame = thisFrame + 1;
float trackTime = AdjustTimeToFitTrack(time, looping);
float frameDelta = mFrames[nextFrame].mTime - mFrames[thisFrame].mTime;
if (frameDelta <= 0.0f)
return T();
float t = (trackTime - mFrames[thisFrame].mTime) / frameDelta;
T point1 = Cast(&mFrames[thisFrame].mValue[0]);
T slope1;
memcpy(&slope1, mFrames[thisFrame].mOut, N * sizeof(float));
slope1 = slope1 * frameDelta;
T point2 = Cast(&mFrames[nextFrame].mValue[0]);
T slope2;
memcpy(&slope2, mFrames[nextFrame].mIn, N * sizeof(float));
slope2 = slope2 * frameDelta;
return Hermite(t, point1, slope1, point2, slope2);
}
关于Track类的思考
Track类到底是个啥,Property对应的Curve的数据吗,好像不是,因为它好像可以用于时间,比如这段代码:
Track<float, 1> t;
float mAnimTime = 0.0f;
void Update(float dt)
{
mAnimTime = t.AdjustTimeToFitTrack(mAnimTime + dt);
}
Track的本质代码:
template<typename T, int N>
class Track
{
protected:
std::vector<Frame<N>> mFrames;
Interpolation mInterpolation;
...
}
template<unsigned int N>
class Frame
{
public:
float mValue[N];
float mIn[N];
float mOut[N];
float mTime;
};
分析一下Track的特点:
- Track本质的数据就是一个数组,类似C++ STL的泛型vector,虽然都是泛型,但是STL的vector里存的是T的对象,但是Track的数组vector里存的是Frame的对象,这里的Frame就是个简单的Class,代表关键帧数据,里面除了时间是一个float变量,其他的都用float*表示,而这里的T,代表了Frame结构体里的mValue本身是一个什么类型的变量(为什么这里的Frame不是一个模板,T与Frame完全没有关系呢,而是把T的定义放到了Track里,也就是说,单独一个Frame对象,不结合Track,是无法起作用的)
- Cast函数,用于把float数组转型为T对象,这里的float数组,其实是个很小的数组,它只可能对应一个T对象,比如T为vec3时,float*的size为3
所以说,我理解的Track,非常类似于Unity里的Curve数据,它本质上就是一个Property随时间变化的Curve。
可以再来看看之前的代码:
Track<float, 1> t;
float mAnimTime = 0.0f;
void Update(float dt)
{
mAnimTime = t.AdjustTimeToFitTrack(mAnimTime + dt);
}
其实这个Track,也是一种Curve,它的变量是time,得到的结果与time和动画本身的周期有关,其曲线如下图所示:
把position track、quaternion track和scale track组合成transform track
其实跟Unity里的Animation里的数据差不多,里面也是LocalPosition、LocalRotation和LocalScale的Curve,但是这书里的动画好像没有Scale的动画数据,所以就只组合了前俩。
这里有两种做法:
- 为每个Model上的Bone都创建Transform Track,优点是查询方便,缺点是消耗内存大,即使没有动画的Bone也会有对应的Track
- 只为Model上存在动画的Bone创建Transform Track,然后记录每个Track对应的Bone ID,实际用的时候,会遍历每个Track,根据其ID找到对应的Bone
显然方法二更好,这里选择方法二,先创建TransformTrack的头文件:
#ifndef _H_TRANSFORMTRACK_
#define _H_TRANSFORMTRACK_
#include "Track.h"
#include "Transform.h"
class TransformTrack
{
protected:
unsigned int mId;
VectorTrack mPosition;
QuaternionTrack mRotation;
VectorTrack mScale;
public:
TransformTrack();
unsigned int GetId();
void SetId(unsigned int id);
VectorTrack& GetPositionTrack();
QuaternionTrack& GetRotationTrack();
VectorTrack& GetScaleTrack();
float GetStartTime();
float GetEndTime();
bool IsValid();
Transform Sample(const Transform& ref, float time, bool looping);
};
#endif
下面是这些函数的实现,基本就是三个Track的缝合怪,没啥新东西:
#include "TransformTrack.h"
TransformTrack::TransformTrack()
{
mId = 0;
}
unsigned int TransformTrack::GetId()
{
return mId;
}
void TransformTrack::SetId(unsigned int id)
{
mId = id;
}
VectorTrack& TransformTrack::GetPositionTrack()
{
return mPosition;
}
QuaternionTrack& TransformTrack::GetRotationTrack()
{
return mRotation;
}
VectorTrack& TransformTrack::GetScaleTrack()
{
return mScale;
}
bool TransformTrack::IsValid()
{
return mPosition.Size() > 1 || mRotation.Size() > 1 || mScale.Size() > 1;
}
float TransformTrack::GetStartTime()
{
float result = 0.0f;
bool isSet = false;
if (mPosition.Size() > 1)
{
result = mPosition.GetStartTime();
isSet = true;
}
if (mRotation.Size() > 1)
{
float rotationStart = mRotation.GetStartTime();
if (rotationStart < result || !isSet)
{
result = rotationStart;
isSet = true;
}
}
if (mScale.Size() > 1)
{
float scaleStart = mScale.GetStartTime();
if (scaleStart < result || !isSet)
{
result = scaleStart;
isSet = true;
}
}
return result;
}
float TransformTrack::GetEndTime()
{
float result = 0.0f;
bool isSet = false;
if (mPosition.Size() > 1)
{
result = mPosition.GetEndTime();
isSet = true;
}
if (mRotation.Size() > 1)
{
float rotationEnd = mRotation.GetEndTime();
if (rotationEnd > result || !isSet)
{
result = rotationEnd;
isSet = true;
}
}
if (mScale.Size() > 1)
{
float scaleEnd = mScale.GetEndTime();
if (scaleEnd > result || !isSet)
{
result = scaleEnd;
isSet = true;
}
}
return result;
}
Transform TransformTrack::Sample(const Transform& ref, float time, bool looping)
{
Transform result = ref;
if (mPosition.Size() > 1)
{
result.position = mPosition.Sample(time, looping);
}
if (mRotation.Size() > 1)
{
result.rotation = mRotation.Sample(time, looping);
}
if (mScale.Size() > 1)
{
result.scale = mScale.Sample(time, looping);
}
return result;
}
Because not all animations contain the same tracks, it’s important to reset the pose that you are sampling any time the animation that you are sampling switches. This ensures that the reference transform is always correct. To reset the pose, assign it to be the same as the rest pose
本章总结
这章学习了动画的本质,动画的本质就是一堆Track,或者说一堆Property的Curves,Track由一个Property的多个关键帧数据组成。后面的AnimationClips基本就是这章建立的TransformTrack对象的集合,Github上的Sample00带了此课的代码,Sample01把一些Track绘制了出来,因为预览这些Track的Curve对于Debug也是很有帮助的。
附录
fbx与glTF文件的区别
https://www.threekit.com/blog/gltf-vs-fbx-which-format-should-i-use
Difference between Spline, B-Spline and Bezier Curves
参考:https://www.geeksforgeeks.org/difference-between-spline-b-spline-and-bezier-curves/
没写完,以后再研究吧
Spline Spline Curve是一个数学的表达方法,用于表示复杂的Curve和表面,我理解的是,这个东西就类似于 A spline curve is a mathematical representation for which it is easy to build an interface that will allow a user to design and control the shape of complex curves and surfaces.
B-Spline B-Spline is a basis function that contains a set of control points. The B-Spline curves are specified by Bernstein basis function that has limited flexibility.
Attention reader! Don’t stop learning now. Get hold of all the important CS Theory concepts for SDE interviews with the CS Theory Course at a student-friendly price and become industry ready.
Bezier These curves are specified with boundary conditions, with a characterizing matrix or with blending function. A Bezier curve section can be filled by any number of control points. The number of control points to be approximated and their relative position determine the degree of Bezier polynomial.
|