承接上节,本节主要介绍个人开源项目R2grap中动画Json文件的字段定义,以及相应的编解码流程实现。
AE中Shape动画描述形式
要对AE的shape动画进行描述,首先要知道AE中不同layer的表现形式,以及所包含的动画信息,在AE中实际的图层表现如下图所示:
直接看这张图可能有些复杂,上面不仅包含了layer的内部结构,也包含了transform的关键帧信息,以及当前layer与其他layer的绑定关系。会在接下来进行详细描述。
首先对于单独的layer来说,它所包含数据内容从上图中可以看到,AE中每一个layer包含了若干个group,以及其自身的Transform属性。
对于Group来说,其内部所存储的是一个树状结构,即有可能嵌套包含另若干个Group,也有可能直接包含一个描述图元信息。由于最终的子group所包含的shape数据结构是相类似的,我们着重讲解下末尾子group所包含的数据形式;
描述图元信息的主要为:
- Path:以二次贝塞尔曲线描述的shape的顶点信息;
- Fill:用以描述该shape的颜色填充信息,rgb格式;
- Stroke:用以描述该Shape的边线的颜色信息,rgb格式;
其中Fill与Stroke并不是一定存在的,有可能group下仅包含Fill或Stroke,但一定存在Path信息;
对于Transform信息来说,主要用于表示该shape的静态信息与动态关键帧信息。因为transform不管是在group还是layer中,所包含的内容是相类似的,因此做下统一的描述。
描述动画的信息主要为:
- Anchor Pos : 图元的锚节点坐标,该值是当前layer/group所绑定的layer/group的Anchor Pos的相对值;
- Position:图元的相对坐标,以Anchor Pos为基准点;
- Scale: 图元的缩放值,缩放的中心点为Anchor Pos;
- Rotation: 图元的旋转值,旋转的中心点为Anchor Pos;
- Opacity: 图元的透明度值;
对于有关键帧的Transform属性,在Json中除记录了Transform的初始值之外,还记录了每个关键帧的数据。当关键帧类型为linear时,只是记录了时间与关键帧值的信息;当关键帧为Bezier Curve时,还需要记录每个关键帧的控制点坐标;
最终不管是Layer还是Group都存在“主从关系”这一属性,对于layer来说,当一个layer A对另一个layer B进行了链接(link),即代表layer A从属于layer B,layer A会继承B的Transform属性与关键帧信息。对于group来说,当group A 包含了 group B(A作为B的父group),则代表group B会继承A的全部transform与关键帧信息;
实际Json中每一个layer的表现形式,可以通过这个链接进行查看。
构建layer-group-path/transform的数据结构
前面简单分析了Layer内所包含的各类信息,下面我们从代码实现的角度去对其进行构建,首先定义LayersInfo类如下:
class LayersInfo{
public:
LayersInfo(const nlohmann::json& layer);
/*
一些读取成员变量的读取函数,详情可在github上查看
*/
private:
unsigned index_; //layer索引
std::string name_;//layer名称
int link_; //与其他layer的链接情况,当未与其他layer链接时,为-1;
float out_point_;//layer的结束时间
float in_point_;//layer的开始时间
std::vector<std::shared_ptr<ShapeGroup>> groups_;//groups指针数组
std::shared_ptr<Transform> transform_;//transform指针
};
可以看到LayersInfo下包含了一组ShapeGroup,我们接下来继续看ShapeGroup的内部结构:
class ShapeGroup{
public:
ShapeGroup(const nlohmann::json& json);
/*
一些读取成员变量的读取函数,详情可在github上查看
*/
private:
void SetParent(const std::weak_ptr<ShapeGroup> parent){ parent_group_ = parent;}
private:
std::weak_ptr<ShapeGroup> parent_group_;//用以指向父group的指针
std::vector<std::shared_ptr<ShapeGroup>> child_groups_;//数组存放所包含的子group指针
std::shared_ptr<GroupContents> contents_;//group内所包含的信息
std::shared_ptr<Transform> transform_;//transform指针
};
以上述形式构造group的树状结构,其中transform_与LayerInfo中类似;
对于GroupContent的构造形式如下:
class GroupContents{
public:
GroupContents(const nlohmann::json& json);
/*
一些读取成员变量的读取函数,详情可在github上查看
*/
private:
std::vector<std::shared_ptr<PathInfo>> paths_;//可能包含多组Path,使用数组存放
std::shared_ptr<FillInfo> fills_ = nullptr;//Fill信息,由于可能为空,默认置nullptr
std::shared_ptr<StrokeInfo> stroke_ = nullptr;//Stroke信息,由于可能为空,默认置nullptr
bool existMergePaths_ = false;//多组path直接是否存在Merge关系
};
可以看到每一个GroupContent中包含了path,由于fill和storke是可能存在的,所以对其默认置空,同时在AE中多组Path之间可以进行逻辑运算,实现多组path构建同一个图元(可以想象为内部镂空的这种图元);
class PathInfo{
public:
PathInfo(const nlohmann::json& json);
/*
一些读取成员变量的读取函数,详情可在github上查看
*/
private:
bool closed_; //是否为闭合图元,
std::vector<glm::vec2> vertices_;
std::vector<glm::vec2> out_pos_vecs_;
std::vector<glm::vec2> in_pos_vecs_;
std::vector<glm::vec2> bezier_verts_;
std::vector<unsigned int> tri_index_list_;
std::map<unsigned int, std::vector<glm::vec2>> linear_map_; //first: frame index, second: array of vert
std::map<unsigned int, std::vector<unsigned int>> tri_index_map_;//first: frame index, second: array of triangle index
PathInfo的内容较多,逐一进行分析下:
- 在PathInfo的构造函数中会根据其控制点参数(out_pos_vecs_&in_pos_vecs_),对这一条Path进行贝塞尔插值构成曲线,将其存入到bezier_verts_中;
- 使用closed_标记当前path是否为闭合图元,如果是闭合图元,需要对beizier_verts_中的顶点进行三角形切分,即将顶点索引按耳切法组成符合渲染要求的顶点索引;
- Path也存在着有关键帧的情况,对于有关键帧的Path,这里使用了map去存放对应帧下的顶点数组与顶点索引数组;
在FillInfo/StorkeInfo中主要记录了填充/线条的颜色信息、透明度信息,以及关键帧信息;
最后在来说明下Transform的组成部分:
class Transform{
public:
explicit Transform(const nlohmann::json& transform, bool IsShapeTransform = false, int link = -1);
Transform() :
type_(TransformType::t_NoneTrans),
property_values_(std::map<std::string, std::variant<glm::vec3, float>>()),
keyframe_data_(KeyframesMap())
{}
glm::vec3 GetShapeGrapOffset();
static bool IsVectorProperty(std::string);
bool IsNoKeyframe(){return keyframe_data_.size() == 0; }
/*
一些读取成员变量的读取函数,详情可在github上查看
*/
protected:
void ReadProperty(const std::string& propname, const nlohmann::json& transform);
void SetOriginalProerty(){original_property_values_ = property_values_;}
private:
TransformType type_;//记录是LayerTransform(ShapeTransform)还是GroupTransform
std::map<std::string, std::variant<glm::vec3, float>> property_values_;//记录Transform属性值,link运算后
std::map<std::string, std::variant<glm::vec3, float>> original_property_values_;//记录Transform属性值,link运算前
KeyframesMap keyframe_data_;//存放关键帧信息的map,同fill与storke类似,同样需要考虑标量与分量共存的问题
};
由于Transform的信息包含标量与分量,因此使用std::variant进行记录,property_values_和original_property_values_都是以这种形式存储的transform属性。
前文说过不管是Layer还是Group,它们的Transform属性都是与Link的对象相关的,需要在父对象的transform基础上加上子类的偏移量,才是最终图元在画幅中的绝对位置。