MIT 许可证 NuGet

首页 / 文档 / 模型与动画

模型与动画

glTF 2.0 是引擎唯一读取的模型格式。一个 Model 和其他控件一样:你给它一个文件名和 一个目标尺寸,把它加到一个面板里,每帧更新它。

加载一个

robot = new Model
{
    Name = "Assets/3DGodotRobot.glb",
    PosX = -2f, PosY = 1f, PosZ = 6f,
    Width = 1f, Height = 1f, Depth = 0.5f,
    Rotation = MathF.PI / 2f,
    Highlight = { Style = HighlightStyle.Wireframe }
};
AddControl(robot);

AddControl 会排队加载。光照、阴影、 环境光遮蔽或天空这些都没有出现在那段代码里,因为那些是管线的事,而不是 逐对象的事 — 一个已添加的模型,被这一帧正在做的一切所照亮和投影。

尺寸是米,旋转是弧度

Width = 1f 让模型宽一个单位,无论 美术导出的是什么,因为引擎从资产自身的边界推导缩放。 Model 上的 Rotation 是一个 绕 Y 轴的弧度 float — 写 90 会给你 90 弧度, 归一化后大约是 117 度,看起来 几乎 是对的。参见 放置、锚点与相机。

文件必须同时在 MauiAsset 和 Content 项组里,否则它会在桌面上加载,而在 打包构建中静默失败。项目布局解释了 原因。

播放一个动画片段

片段来自文件,用美术使用的名字寻址。在控件 Ready 之前,下面的一切都返回 null 或空列表, 所以请在加载之后而不是在构造函数里读取片段列表。

// Advancing the clock is what plays the animation.
robot.Update(time);

// Once Ready:
foreach (var name in robot.GetAnimationNames())
    DeviceServices.BaseApp?.AddLog(LogType.Info, name);

robot.PlayAnimation("Walk");          // returns the name actually activated
robot.SwitchToNextAnimation();        // wraps; what a debug key usually wants
var playing = robot.GetCurrentAnimationName();
成员说明
GetAnimationNames()来自 animations[].name 的片段名。资产没有时为空列表。
GetAnimations()同样的列表,附带时长,作为 ModelAnimationInfo。
PlayAnimation(string)切换片段并返回实际生效的名称,或 null。
SwitchToNextAnimation()在列表中前进,循环回绕。
GetCurrentAnimationName()当前正在播放什么。
Time以秒计的动画时钟,如果你想自己拖动的话。
Model 有自己的 Update 重载

Update(float time, string? name = null, float? alpha = null)。 它转发到基类控件更新,当 name 非 null 时交换资产,而且 — 关键的部分 — 当控件 Ready 且有内容时,把 time 推入蒙皮路径。一个你从不更新的模型会 正确绘制,并保持完全静止。

切换片段是硬切

没有交叉淡化,没有混合树,没有叠加层,也没有状态机。一个 需要同时走路和瞄准的角色,是你自己要做的工作,或者 是等待路线图的工作。

交换资产

SetModel(name, forceReload = false) 保留控件及其 摆放,替换几何体:它清除 Ready,把控件标记为已更改,并重新排队加载。当名称未变且 forceReload 为 false 时,它 返回 false 且什么都不做。

// Same position, size and rotation; different geometry.
robot.SetModel("Assets/3DGodotRobotAlt.glb");

// Equivalent, inside your update:
robot.Update(time, name: "Assets/3DGodotRobotAlt.glb");

从调用到下一次 Ready 之间,控件什么都不绘制, 这与首次加载的规则相同。如果交换不能闪烁,就保留两个控件并交叉它们的 Alpha。

材质覆盖

存在一小撮一次性覆盖,用于你想给导入的材质上色或变暗、又不想编辑文件的情况。 它们会被下一次更新消费,并把自己重置为 null,所以设置一个覆盖是一次请求,而不是一个你必须记得清除的状态。

覆盖效果
MetallicOverride金属度系数,0 到 1。
RoughnessOverride粗糙度系数,0 到 1。
EmissiveFactorOverride自发光颜色,作为 RGB 强度。
LightIntensityScale不是覆盖 — 是文件携带的光照上的持久缩放。见下文。

随文件一起来的光源

glTF 可以携带 KHR_lights_punctual 光源。 Model 把它们暴露为 ImportedPunctualLights,而 AppendWorldLights(ref SceneLightParams) 把它们折入 这一帧。

在导入室内场景之前,有两个值得知道的后果。帧预算总共是八个 点光源,所以一个携带了十二个的资产会挤掉你自己的。而且 glTF 强度是光度学的,这就是 Model.LightIntensityScale 和 RenderQuality.KhrLightIntensityScale 存在的原因 — 后者 默认为 0.05,因为原始的坎德拉值在引擎自身的 强度约定旁边大得离谱。

边界、剔除与拾取

蒙皮网格可能摆出绑定姿势的边界之外,所以用于剔除的盒会被 RenderQuality.AnimatedBoundsScale 放大,默认 1.5。这 留下两个盒,而选错一个是一个可见的 bug,而不是微妙的。

用途读哪个
视锥与阴影剔除LocalBounds / GetWorldBounds() — 膨胀的,有意保守。
拾取、选择框、布局吸附LocalBoundsRaw / GetWorldBoundsRaw() — 与实际渲染的身体匹配。
计算你自己的缩放LocalSize,原始盒的尺寸。
完全退出CullingEnabled = false,用于任何合法超出其边界的对象。

一个把肢体甩出绑定姿势 50 % 以上的动画会在 视锥边缘突然消失;对那个控件调高 AnimatedBoundsScale 或清除 CullingEnabled。与每帧计算精确的蒙皮边界相比, 这两者都便宜,而后者是没人愿意付出的替代方案。

当它不是文件时

Mesh3D 接受一个 Surface 对象列表 — 顶点、索引、一个材质 — 并且从那以后表现得像一个 Model,包括同样的 放置约定和同样的阴影与 GI 参与。参考应用中的海、沙滩瓦片和 立方体田都是用这种方式构建的。它的 Rotation 是四元数而不是 float。

完整类型列表见 模型、动画与实例化, 包括从共享 GLB 中取出一个网格的 GLTFTools,以及 为表面级精确拾取提供 CPU 侧几何的 PickMesh。