文章目录
-
- 零、前言
- 一、资源介绍
-
- [1.1 骨架资源](#1.1 骨架资源)
- [1.2 骨架网格体资源](#1.2 骨架网格体资源)
- 二、UE4中的定义
-
- [2.1 骨骼数据](#2.1 骨骼数据)
- [2.2 模型网格数据](#2.2 模型网格数据)
- 三、渲染
-
- [3.1 RenderData的初始化](#3.1 RenderData的初始化)
- [3.2 渲染对象的创建](#3.2 渲染对象的创建)
- [3.3 渲染对象的更新](#3.3 渲染对象的更新)
-
- [3.3.1 游戏线程的更新(*FSkeletalMeshObjectGPUSkin::Update*)](#3.3.1 游戏线程的更新(FSkeletalMeshObjectGPUSkin::Update))
- [3.3.2 渲染线程的更新(*FSkeletalMeshObjectGPUSkin::UpdateDynamicData_RenderThread*)](#3.3.2 渲染线程的更新(FSkeletalMeshObjectGPUSkin::UpdateDynamicData_RenderThread))
- [3.4 Shader浅析](#3.4 Shader浅析)
- 四、类图
- 参考文章
零、前言
本文大量内容来自网络文章,整理作为笔记。
感谢各位原作者!
一、资源介绍
1.1 骨架资源
骨架资源,是整个动画系统的基础。
其主要作用是记录了骨架以下信息:
- 骨骼层级信息
- 参考姿势信息
- 骨骼名称插槽(Socket)信息
- 曲线(Curve)信息
- 动画通知(Animation Notify)信息
- 插槽数据(插槽名称、所属骨骼、Transform等)
- 虚拟骨骼信息
- 骨骼名称Index映射表
- 其他骨骼设置信息:包括位移重定向(Translation Retarget)设置,LOD设置等信息
1.2 骨架网格体资源
骨骼模型是在骨骼基础之上的模型,通俗来说就是绑定骨骼后的网格体。
其主要包含以下信息:
- 模型的顶、线、面信息
- 顶点的骨骼蒙皮权重
- 模型的材质信息
- 模型LOD信息
- Morph Target信息
- Physics Asset(物理)设置信息
- 布料系统相关设置
二、UE4中的定义
2.1 骨骼数据
骨骼相关的数据都封装在USkeleton类,其中包括了骨架数据、插槽、重定向等逻辑。
USkeleton并不会直接使用自身的数据,而是会生成一个FReferenceSkeleton,存储骨骼姿势数据,来提供给Mesh来使用。
FReferenceSkeleton中,将所有的原始Bone数据分成两份:
- 一份存储Bone的名字和父节点的索引,一份是Bone的Transform。
其中,FMeshBoneInfo的数据结构保存BoneName 和父节点的索引。
还包含了其他的数据信息,例如Name和Index的关系。
2.2 模型网格数据
FFbxImporter::ImportSkeletalMesh ,负责将导入的模型生成FSkeletalMeshModel对象(USkeletalMesh的一个编辑器成员变量)。
该对象存储着模型网格的几何数据。
SkeletalMesh存在多个LOD,每个LOD等级下的三角形面数,顶点数是不同的。
FSkeletalMeshModel 中存储了各个LOD等级的模型信息(FSkeletalMeshLODModel)。
在FSkeletalMeshLODModel 中,主要包含了如下的数据:
FSkelMeshSection ,表示一组使用相同材质的三角形网格数据。
- 存储了FSoftSkinVertex的数组。
- FSoftSkinVertex,表示一个顶点,包含了Position、UV、Normal等,以及每个顶点受影响的骨骼索引和权重。
经Debug可以得知:
- 骨骼权重用uint8整数表示,最大骨骼权重值为255, 所有骨骼的权重加起来等于255,真正的骨骼权重只需要除以255。
Section中的BoneMap,存储的是这个Section使用到的Bone在骨骼树上的真正索引。
FSoftSkinVertex存储的骨骼索引InfluenceBones是"虚拟骨骼索引 ",而非真正的骨骼索引。想要获得真正的骨骼索引,需要进行一次转换。即经历Section的BoneMap的映射才得到真正的骨骼索引。
代码如下:
如图所示,LOD0中有两个Section,Section0使用了60个骨骼,而Section1仅有4个骨骼。
可以通过**骨骼树信息(FReferenceSkeleton)**看出Section1使用的是哪些骨骼:
三、渲染
3.1 RenderData的初始化
FSkeletalMeshModel数据不会在运行时使用,运行时使用的是由该几何数据初始化的FSkeletalMeshRenderData(渲染数据)对象。
在RunTime中,Mesh相关的几何数据实际存储FSkeletalMeshRenderData渲染数据中。
在导入模型的过程 中,会调用FSkeletalMeshRenderData::Cache对RenderData进行初始化。
逐级遍历ImportedModel的的LOD,调用FSkeletalMeshLODRenderData::BuildFromLODModel,来实现数据的初始化。
代码分析如下:
1)用ImportedModel中的Sections数据,来初始化LODRenderData的Section信息。
2)获取顶点数据等,来初始化Mesh的各种VertexBuffer。
Buffer的Layout:
-
Position [Section0] [Section1]...
-
Tangent和UV同上
3)初始化蒙皮权重缓冲等
如有颜色,初始化颜色缓冲
如ClothData的初始化
...
4)拷贝ActiveBoneIndices和RequiredBones数据
上述介绍了编辑器下导入模型创建RenderData的过程。
而运行时,RenderData是创建是来自于USkeletalMesh::Serialize序列化。
3.2 渲染对象的创建
USkinnedMeshComponent::CreateRenderState_Concurrent
,在该函数中会进行一些渲染相关数据的创建。
其中,会由SkeletalMesh的SkelMeshRenderData(FSkeletalMeshRenderData)创建出MeshObject(FSkeletalMeshObject)。
- 创建之后,MeshObject将常驻内存,轻易不会销毁。
FSkeletalMeshObject:渲染对象的基类,通过该对象从Game线程往渲染线程传递数据。
会根据选项创建其子类。子类如有:
- FSkeletalMeshObjectCPUSkin
- FSkeletalMeshObjectGPUSkin
- FSkeletalMeshObjectStatic
UE4默认是GPU蒙皮,以FSkeletalMeshObjectGPUSkin为例:
在其构造函数里,调用了FSkeletalMeshObjectGPUSkin::InitResources。
在该方法主要是遍历LODs(FSkeletalMeshObjectLOD的数组) ,调用FSkeletalMeshObjectGPUSkin::FSkeletalMeshObjectLOD::InitResources
。
- 初始化顶点蒙皮权重缓存:MeshObjectWeightBuffer
- 初始化颜色缓存,MeshObjectColorBuffer
- 获取RenderLODData中的顶点数据,存到中间变量FVertexFactoryBuffers
- 用Buffers初始化MeshObjectLOD成员变量GPUSkinVertexFactories(FVertexFactoryData对象),初始化GPUSkin顶点工厂
- 如果有布料数据,则拿FVertexFactoryBuffers也初始化ClothVertexFactories
3.3 渲染对象的更新
FSkeletalMeshObject::Update
,该函数实现了将GameThread更新后的DynamicData发送至渲染线程。
调用堆栈如下:
在UActorComponent::DoDeferredRenderUpdates_Concurrent
中会检查组件当前帧bRenderTransformDirty
与bRenderDynamicDataDirty
是否为真。
bRenderTransformDirty
为真表示Transform发生了变化,调用SendRenderTransform_Concurrent
用于更新渲染线程的Transform数据。bRenderDynamicDataDirty
为真表示DynamicData的数据发生变化,调用SendRenderDynamicData_Concurrent
更新DynamicData。
调用MarkRenderDynamicDataDirty
会将bRenderDynamicDataDirty
标记为真。
其调用堆栈如下:
- 在动画更新完成后,会进行调用,从而触发渲染数据的更新。
FSkeletalMeshObject::Update
,不同的MeshObject,更新的数据不同。
下面以FSkeletalMeshObjectGPUSkin为例,进行介绍。
3.3.1 游戏线程的更新(FSkeletalMeshObjectGPUSkin::Update)
其更新的数据是:FDynamicSkelMeshObjectDataGPUSkin。
存储用来更新蒙皮顶点的矩阵,在GameThread被创建,当更新时会发送到渲染线程。
其部分数据结构如下:
// 蒙皮矩阵相关
TArray<FMatrix> ReferenceToLocal;
TArray<FMatrix> PreviousReferenceToLocal;
TArray<FTransform> MeshComponentSpaceTransforms;
流程如下:
1、InitMorphResources
更新激活的MorphTarget和对应的权重,并会筛选出影响Mesh的曲线,剔除不需要的。
2、InitDynamicSkelMeshObjectDataGPUSkin
初始化GPU蒙皮数据,使用新的动态数据更新 ReferenceToLocal 矩阵。
该函数的主要逻辑:
(1)调用UpdateRefToLocalMatrices,更新ReferenceToLocal
(2)调用UpdatePreviousRefToLocalMatrices,更新PreviousReferenceToLocal
(3)调用UpdateClothSimulationData,更新布料模拟Mesh的顶点位置和法线
UpdateRefToLocalMatrices和UpdatePreviousRefToLocalMatrices,二者的差别在于ComponentTransform。
前者是GetComponentSpaceTransforms,后者是GetPreviousComponentTransformsArray。
二者核心都是调用UpdateRefToLocalMatricesInner ,计算蒙皮矩阵。
蒙皮矩阵的作用 :将顶点从模型空间 下的绑定姿势 ,变换到模型空间 下的当前动画姿势。
- 蒙皮矩阵计算前后都是Local Space。
所以需要先将顶点变换到骨骼空间 ,再应用骨骼全局姿势矩阵。
绑定姿势矩阵:每骨骼的全局绑定姿势矩阵,用于将顶点从某骨骼空间变换到模型空间。
绑定姿势逆矩阵:用于将顶点从模型空间变换到骨骼空间。
在该函数中,先遍历所有会对蒙皮产生影响的骨骼,获取其 Component Space Bone Transform 对应的矩阵(即从Bone Space 变到当前的 Local Space),对于不对蒙皮产生影响的骨骼,这里会保持为 Identity 矩阵。
然后,遍历所有骨骼,乘上 Ref-Pose ,Bone Space 到 Local space 的变化矩阵的逆矩阵(即从 Local Space 变回 Bone Space)。
3、将动态数据更新命令发送到渲染线程
3.3.2 渲染线程的更新(FSkeletalMeshObjectGPUSkin::UpdateDynamicData_RenderThread)
1、调用FreeDynamicSkelMeshObjectDataGPUSkin,释放上一帧的DynamicData。
2、调用ProcessUpdatedDynamicData,具体处理传输的数据。
ProcessUpdatedDynamicData的主要逻辑如下:
-
遍历当前LOD的Section,获得每个Section的顶点工厂VertexFactory。
-
更新顶点工厂的ShaderData,
FGPUBaseSkinVertexFactory::FShaderDataType::UpdateBoneData
。
在 UpdateBoneData
中,UE 并不会将蒙皮矩阵全部传到 GPU。对于任意一个 section,UE 只会传递这个 section 用到的骨骼。
将蒙皮矩阵写入VertexBuffer(BoneBuffer)中。
3.4 Shader浅析
SKeletalMesh使用的顶点工厂是:FGPUBaseSkinVertexFactory
通过IMPLEMENT_GPUSKINNING_VERTEX_FACTORY_TYPE
宏,将GPU蒙皮的VertexFactory与对应的Shader(GpuSkinVertexFactory.ush)进行了绑定。
打开GpuSkinVertexFactory.ush,可以看到包含一大堆宏定义代码。
最好的方式是用RenderDoc截帧一下,获取一份不带宏的示例代码。
XXVertexFactory.ush 使用了模板函数,需要定义接口的函数和结构。后续会整理一个文档。
输入布局FVertexFactoryInput的定义:
- Position,模型顶点位置
- BlendIndices,影响顶点的骨骼Index列表
- BlendWeights,影响顶点的骨骼权重列表
C++
struct FVertexFactoryInput
{
float4 Position : ATTRIBUTE0;
float3 TangentX : ATTRIBUTE1;
float4 TangentZ : ATTRIBUTE2;
uint4 BlendIndices : ATTRIBUTE3;
uint4 BlendIndicesExtra : ATTRIBUTE14;
float4 BlendWeights : ATTRIBUTE4;
float4 BlendWeightsExtra : ATTRIBUTE15;
float2 TexCoords[ 1 ] : ATTRIBUTE5;
float3 PreSkinOffset : ATTRIBUTE11;
float3 PostSkinOffset : ATTRIBUTE12;
float4 Color : ATTRIBUTE13;
};
FVertexFactoryIntermediates的定义
C++
struct FVertexFactoryIntermediates
{
float3x4 BlendMatrix;
float3 UnpackedPosition;
float3x3 TangentToLocal;
float4 Color;
};
这是一个Caching机制(只计算一次)的结构。
- 主要是获取了位置、BlendMatrix、TangentToLocal矩阵、颜色。
C++
FVertexFactoryIntermediates GetVertexFactoryIntermediates(FVertexFactoryInput Input)
{
FVertexFactoryIntermediates Intermediates;
Intermediates.UnpackedPosition = UnpackedPosition(Input);
Intermediates.BlendMatrix = CalcBoneMatrix( Input );
Intermediates.TangentToLocal = SkinTangents(Input, Intermediates);
Intermediates.Color = Input.Color.rgba ;
return Intermediates;
}
其中,CalcBoneMatrix 为计算加权后的蒙皮矩阵,具体的逻辑如下:
- 线性蒙皮,将所有对该顶点有影响的骨骼变换矩阵,乘上权重再累加起来,得到一个完整的蒙皮矩阵。
- 支持4骨骼或8骨骼蒙皮
C++
float3x4 GetBoneMatrix(int Index)
{
float4 A = BoneMatrices[Index * 3];
float4 B = BoneMatrices[Index * 3 + 1];
float4 C = BoneMatrices[Index * 3 + 2];
return float3x4 (A,B,C);
}
float3x4 CalcBoneMatrix( FVertexFactoryInput Input )
{
float3x4 BoneMatrix = Input.BlendWeights.x * GetBoneMatrix(Input.BlendIndices.x);
BoneMatrix += Input.BlendWeights.y * GetBoneMatrix(Input.BlendIndices.y);
BoneMatrix += Input.BlendWeights.z * GetBoneMatrix(Input.BlendIndices.z);
BoneMatrix += Input.BlendWeights.w * GetBoneMatrix(Input.BlendIndices.w);
if (NumBoneInfluencesParam > 4 )
{
BoneMatrix += Input.BlendWeightsExtra.x * GetBoneMatrix(Input.BlendIndicesExtra.x);
BoneMatrix += Input.BlendWeightsExtra.y * GetBoneMatrix(Input.BlendIndicesExtra.y);
BoneMatrix += Input.BlendWeightsExtra.z * GetBoneMatrix(Input.BlendIndicesExtra.z);
BoneMatrix += Input.BlendWeightsExtra.w * GetBoneMatrix(Input.BlendIndicesExtra.w);
}
return BoneMatrix;
}
对于VS而言,是计算顶点的世界位置,具体的计算如下:
- 顶点乘上混合后的蒙皮矩阵 ,再乘以Local到世界的变换矩阵。
C++
float3 SkinPosition( FVertexFactoryInput Input, FVertexFactoryIntermediates Intermediates )
{
float3 Position = Intermediates.UnpackedPosition;
Position += Input.PreSkinOffset;
Position = mul(Intermediates.BlendMatrix, float4(Position, 1));
Position += Input.PostSkinOffset;
return Position;
}
float4 CalcWorldPosition(FVertexFactoryInput Input, FVertexFactoryIntermediates Intermediates)
{
return TransformLocalToTranslatedWorld(SkinPosition(Input, Intermediates));
}
float4 VertexFactoryGetWorldPosition(FVertexFactoryInput Input, FVertexFactoryIntermediates Intermediates)
{
return CalcWorldPosition(Input, Intermediates);
}