MIT 许可证 NuGet

首页 / 功能 / 模型与动画

模型、动画与实例化

glTF 2.0 是唯一的模型和动画格式。不是首选的那个——是唯一的那个。 这个决定移除了一层抽象、一条导入管线,以及一类两种格式在绑定姿势上 不一致的 bug。

单个模型

Model 使用引擎统一的摆放约定把一个 GLB 放进世界:位置是锚点的世界位置,宽、高、深是目标包围盒尺寸。

var robot = new Model
{
    Name = "Assets/3DGodotRobot.glb",
    PosX = -2f, PosY = 1f, PosZ = 6f,
    Width = 1f, Height = 1f, Depth = 0.5f,
    Rotation = MathF.PI / 2f
};
AddControl(robot);
两件事会让每个人绊倒一次

Width、Height 和 Depth 是以世界单位计的目标尺寸,不是倍数。 写 Width = 1f 会让模型正好一个单位宽, 不管文件里怎么说。而 Rotation 是绕 Y 轴的 弧度——写 90 得到的是 90 弧度, 归一化后大约是 117 度,看起来几乎是对的,这是最糟糕的一类 bug。

摆放、锚点与相机 讲解了锚点数学,包括 AnchorWorldOffset——用于你想要钉住模型自身原点、 而不是其包围盒中心的情形。

动画

蒙皮、变形目标和命名片段全都来自文件。片段按名称寻址, 这些名称就是美术使用的名称。

// After Load has completed:
foreach (var name in robot.GetAnimationNames())
    app.AddLog(LogType.Info, name);

robot.PlayAnimation("Walk");

// Or cycle, which is what a debug key usually wants:
robot.SwitchToNextAnimation();

// Advance the clock yourself; Time is the animation clock.
robot.Update(time);
成员说明
GetAnimationNames()片段名,资产加载完成后即可用。
PlayAnimation(string)切换片段。返回实际选中的名称。
GetCurrentAnimationName()当前正在播放什么。
SwitchToNextAnimation()在片段列表中前进,循环回绕。
Time动画时钟,单位为秒。
SetModel(name, forceReload)替换底层 GLB,保持摆放不变。
片段之间没有混合

切换片段是切刀,不是交叉淡入。没有状态机,没有混合树, 没有叠加层。如果一个角色需要一边走路一边瞄准, 这是引擎目前不会替你做的部分。

多个模型,一个控件

一个 InstancedModel 声明一次,并绑定到一列实例变换。 蒙皮和变形目标按实例独立,所以二十个角色可以在一次绘制调用里各播放不同的片段。 参考应用中的机器人、鸟、岩石和海滩用的都是这个模式。

robotField = new InstancedModel { ModelName = "Assets/3DGodotRobot.glb" };
AddControl(robotField);

for (var i = 0; i < 10; i++)
{
    var person = new Person
    {
        PosX = -2f, PosY = 1f, PosZ = 6f + i * 3f,
        Width = 1f, Height = 1f, Depth = 0.5f,
        Animation = animations[i]
    };

    robotField.Instances.Add(person);
}

Person 派生自 MeshInstanceTransform,所以列表里的对象 就是实例本身。编辑它的字段下一帧就生效,没有拷贝步骤; 在列表中间插入或删除会保持顺序。

MeshInstanceTransform 上的字段说明
PosX/Y/Z、Width、Height、Depth、Rotation与单个模型语义相同,相对于共享的模板包围盒。
AnimationClip此副本的片段索引。父对象上的 AnimationClipCount 和 AnimationNames 告诉你取值范围。
AnimationSpeed播放速率。给每个副本稍作变化,才能让人群看起来不机械。
AnimationTimeOffset相位偏移,让副本不至于齐步走。
Enable跳过该副本而不把它移出列表。
Selected、Highlight逐实例的拾取状态,让单个副本可以被描边。
ID、Name身份标识,当列表由数据重建时很有用。

程序化几何

不是所有东西都来自文件。Mesh3D 接受一列 Surface 对象——顶点、索引和一种材质—— 然后就像任何其他控件一样工作。参考应用中的海面、沙滩地砖和立方体田 都是这样构建的。

类型用途
Mesh3D程序化几何,带四元数 Rotation、ColorTint,以及用于不应接收遮蔽的表面的 ExcludeFromAo。
InstancedMesh3D同样的东西,实例化版。没有逐实例动画——程序化几何没有片段。
GLTFAnimationPlayer片段求值,如果你想自己驱动骨架。
GLTFTools在运行时从单个 GLB 中取出各个网格。
PickMesh用于表面精确拾取的 CPU 侧几何。

剔除,以及为什么动画包围盒要膨胀

视锥剔除和光空间阴影剔除都在 CPU 上针对包围体运行。 微妙之处在于:蒙皮网格可能移出它自己的绑定姿势包围盒, 所以用于剔除的包围盒由 AnimatedBoundsScale 放大, 默认为 1.5。

成员哪个包围盒
LocalBounds膨胀后的。剔除测试用的就是它,并且刻意保持保守。
LocalBoundsRaw未膨胀的。拾取测试用的就是它,因为一次拾取不该命中空气。
LocalSize原始包围盒的尺寸,用它去除以尺寸就能算出目标缩放。
GetWorldBounds()世界矩阵之后的包围盒,用于你自己的空间查询。
CullingEnabled逐控件的退出开关。对任何合法超出自身包围盒的东西都有用。

一段动画如果把肢体甩出绑定姿势 50 % 以上,就会在视锥边缘突然消失。 调高 AnimatedBoundsScale,或者在该控件上清掉 CullingEnabled;两者都比每帧计算精确蒙皮包围盒 的替代方案便宜得多。

随模型一起到达的光源

glTF 文件可以携带 KHR_lights_punctual 光源, Model 会把它们作为 ImportedPunctualLights 呈现出来,并通过 AppendWorldLights 追加进帧的光照。

要记住两件事:帧预算是总共八个光源,所以一个携带十二个光源的资产 会挤掉你自己的光源;并且 glTF 强度是光度学的,这就是 Model.LightIntensityScale 和 RenderQuality.KhrLightIntensityScale 存在的原因。参见 光照与阴影。

已知缺口

  • 缺失 动画混合与状态机。 一次一个片段,以切刀方式切换。
  • 缺失 细节层次。 没有自动 LOD 选择或网格简化。
  • 缺失 间接绘制与 GPU 驱动剔除。 实例从 CPU 侧剔除并提交。路线图 A 轨。
  • 缺失 遮挡剔除。 只有视锥和阴影视锥;一面墙不会剔除它背后的东西。