This translation is community contributed and may not be up to date. We only maintain the English version of the documentation. Read this manual in English
模型组件可以播放从 glTF 文件导入的骨骼动画和变形目标动画。骨骼动画使用模型的骨骼对模型中的顶点应用变形。变形目标动画,也称为混合形状动画,通过对备用顶点位置的权重进行动画处理来改变模型的形状。
关于如何将 3D 数据导入到模型中以进行动画的详细信息,请参阅模型文档。

模型使用model.play_anim()函数进行动画处理:
function init(self)
-- 在 #model 上来回播放 "wiggle" 动画
model.play_anim("#model", "wiggle", go.PLAYBACK_LOOP_PINGPONG)
end
Defold 目前仅支持烘焙的骨骼动画。骨骼动画需要为每个动画骨骼的每个关键帧设置矩阵,而不是将位置、旋转和缩放作为单独的键。
动画也是线性插值的。如果您进行更高级的曲线插值,动画需要从导出器进行预烘焙。
变形目标是同一网格的替代形状。每个目标都会存储位置、法线和切线增量,并且每个目标都有一个混合权重,用来控制应用该形状的程度。权重为 0 表示该目标没有效果,权重为 1 表示应用完整的目标形状。如果着色器和资源按此方式制作,超出该范围的值也可用于夸张效果。
Defold 会从 glTF 模型数据导入变形目标和初始变形权重。对变形权重进行动画处理的 glTF 动画会导入到模型动画集中,并且可以像骨骼动画一样使用 model.play_anim() 播放:
function init(self)
model.play_anim("#model", "smile", go.PLAYBACK_LOOP_FORWARD)
end
变形目标数据可以单独使用,也可以与骨骼动画一起使用,但一个模型组件一次只能播放一个模型动画。这意味着不能使用 model.play_anim() 同时播放一个骨骼动画和一个单独的变形目标动画。如果模型包含动画数据但没有骨架,则只会使用变形目标动画数据。
仍然可以将骨骼动画播放与来自其他来源的变形目标变化结合,例如从脚本中使用 model.set_blend_weights() 设置变形目标权重。
也可以从脚本读取并覆盖变形目标权重。model.get_blend_weights() 会返回模型中第一个带有变形目标的网格的当前权重。model.set_blend_weights() 会将脚本覆盖应用到模型中每个带变形的网格:
function init(self)
local weights = model.get_blend_weights("#model")
weights[1] = 0.75
weights[2] = 0.25
model.set_blend_weights("#model", weights)
end
权重表使用 Lua 从 1 开始的索引,顺序与网格中的变形目标相同。额外的值会被忽略;对于变形目标数量多于表中值数量的网格,缺失的值会按零处理。脚本覆盖会在每帧动画之后应用,直到被清除:
model.set_blend_weights("#model") -- clear the override
model.set_blend_weights("#model", nil) -- also clears the override
要渲染变形目标,模型材质的顶点着色器需要采样生成的 morph_targets 纹理,并将加权增量应用到顶点数据。变形目标纹理是一个 2D 数组纹理,每个变形目标使用三个数组层:位置增量、法线增量和切线增量。
引擎会把当前的变形权重提供给名为 morph_targets_weights 的顶点着色器 uniform。每个 vec4 存储四个权重,因此 morph_targets_weights[2] 可以容纳八个变形目标。
下面的示例展示了非实例化模型材质中相关的顶点着色器部分:
#version 140
in highp vec4 position;
in mediump vec2 texcoord0;
in mediump vec3 normal;
in mediump vec4 tangent;
out mediump vec2 var_texcoord0;
out mediump vec3 var_normal;
out mediump vec4 var_tangent;
uniform vs_uniforms
{
mediump mat4 mtx_worldview;
mediump mat4 mtx_proj;
mediump mat4 mtx_normal;
// Each vec4 stores four blend weights. Use morph_targets_weights[1]
// for up to 4 morph targets, [2] for up to 8, [3] for up to 12, etc.
mediump vec4 morph_targets_weights[2];
};
uniform sampler2DArray morph_targets;
vec2 get_morph_uv(int vertex_index, int width, int height)
{
int x = vertex_index % width;
int y = vertex_index / width;
return vec2(
(float(x) + 0.5) / float(width),
(float(y) + 0.5) / float(height)
);
}
void apply_morph_target(vec2 uv, float weight, int target,
inout vec3 position_delta, inout vec3 normal_delta, inout vec3 tangent_delta)
{
if (weight == 0.0) {
return;
}
int position_layer = target * 3 + 0;
int normal_layer = target * 3 + 1;
int tangent_layer = target * 3 + 2;
position_delta += weight * texture(morph_targets, vec3(uv, position_layer)).xyz;
normal_delta += weight * texture(morph_targets, vec3(uv, normal_layer)).xyz;
tangent_delta += weight * texture(morph_targets, vec3(uv, tangent_layer)).xyz;
}
void get_morph_target_data(int vertex_index,
out vec3 position_delta, out vec3 normal_delta, out vec3 tangent_delta)
{
position_delta = vec3(0.0);
normal_delta = vec3(0.0);
tangent_delta = vec3(0.0);
#ifndef EDITOR
ivec3 texture_size = textureSize(morph_targets, 0);
vec2 uv = get_morph_uv(vertex_index, texture_size.x, texture_size.y);
apply_morph_target(uv, morph_targets_weights[0].x, 0, position_delta, normal_delta, tangent_delta);
apply_morph_target(uv, morph_targets_weights[0].y, 1, position_delta, normal_delta, tangent_delta);
apply_morph_target(uv, morph_targets_weights[0].z, 2, position_delta, normal_delta, tangent_delta);
apply_morph_target(uv, morph_targets_weights[0].w, 3, position_delta, normal_delta, tangent_delta);
apply_morph_target(uv, morph_targets_weights[1].x, 4, position_delta, normal_delta, tangent_delta);
apply_morph_target(uv, morph_targets_weights[1].y, 5, position_delta, normal_delta, tangent_delta);
apply_morph_target(uv, morph_targets_weights[1].z, 6, position_delta, normal_delta, tangent_delta);
apply_morph_target(uv, morph_targets_weights[1].w, 7, position_delta, normal_delta, tangent_delta);
#endif
}
void main()
{
vec3 position_delta;
vec3 normal_delta;
vec3 tangent_delta;
get_morph_target_data(gl_VertexIndex, position_delta, normal_delta, tangent_delta);
vec3 morphed_position = position.xyz + position_delta;
vec3 morphed_normal = normalize(normal + normal_delta);
vec3 morphed_tangent = normalize(tangent.xyz + tangent_delta);
var_texcoord0 = texcoord0;
var_normal = normalize((mtx_normal * vec4(morphed_normal, 0.0)).xyz);
var_tangent = vec4(normalize((mtx_normal * vec4(morphed_tangent, 0.0)).xyz), tangent.w);
gl_Position = mtx_proj * mtx_worldview * vec4(morphed_position, 1.0);
}
这里需要 #ifndef EDITOR 包装,因为编辑器中尚不支持模型动画预览,所以生成的变形目标纹理数据只在运行时可用。如果网格包含更多变形目标,请增大 morph_targets_weights 数组大小,并添加更多 apply_morph_target() 调用。
上面的着色器示例使用了 textureSize(),不适用于 OpenGL ES 2.0。
模型骨架中的骨骼在内部表示为游戏对象。
您可以在运行时检索骨骼游戏对象的实例 id。函数model.get_go()返回指定骨骼的游戏对象的 id。
-- 获取我们 wiggler 模型的中间骨骼游戏对象
local bone_go = model.get_go("#wiggler", "Bone_002")
-- 现在可以对游戏对象做一些有用的操作...
除了使用model.play_anim()来推进模型动画外,Model组件还公开了一个”游标”属性,可以使用go.animate()进行操作(有关属性动画的更多信息):
-- 在 #model 上设置动画但不启动它
model.play_anim("#model", "wiggle", go.PLAYBACK_NONE)
-- 将游标设置为动画的开头
go.set("#model", "cursor", 0)
-- 使用 in-out quad 缓动在 0 和 1 之间对游标进行 pingpong 补间。
go.animate("#model", "cursor", go.PLAYBACK_LOOP_PINGPONG, 1, go.EASING_INOUTQUAD, 3)
模型动画model.play_anim()支持一个可选的 Lua 回调函数作为最后一个参数。当动画播放到结束时将调用此函数。对于循环动画,或者当动画通过go.cancel_animations()手动取消时,永远不会调用该函数。回调可用于在动画完成时触发事件或将多个动画链接在一起。
local function wiggle_done(self, message_id, message, sender)
-- 动画完成
end
function init(self)
model.play_anim("#model", "wiggle", go.PLAYBACK_ONCE_FORWARD, nil, wiggle_done)
end
动画可以播放一次或循环播放。动画的播放方式由播放模式决定:
go.PLAYBACK_NONEgo.PLAYBACK_ONCE_FORWARDgo.PLAYBACK_ONCE_BACKWARDgo.PLAYBACK_ONCE_PINGPONGgo.PLAYBACK_LOOP_FORWARDgo.PLAYBACK_LOOP_BACKWARDgo.PLAYBACK_LOOP_PINGPONG