mirror of
https://github.com/godotengine/godot.git
synced 2024-11-24 05:04:10 +00:00
GLTF: Implement KHR_animation_pointer for animating custom properties
This commit is contained in:
parent
d373d207c1
commit
0513943f70
@ -5371,6 +5371,33 @@ Error GLTFDocument::_serialize_animations(Ref<GLTFState> p_state) {
|
||||
channels.push_back(t);
|
||||
}
|
||||
}
|
||||
if (!gltf_animation->get_pointer_tracks().is_empty()) {
|
||||
// Serialize glTF pointer tracks with the KHR_animation_pointer extension.
|
||||
if (!p_state->extensions_used.has("KHR_animation_pointer")) {
|
||||
p_state->extensions_used.push_back("KHR_animation_pointer");
|
||||
}
|
||||
for (KeyValue<String, GLTFAnimation::Channel<Variant>> &pointer_track_iter : gltf_animation->get_pointer_tracks()) {
|
||||
const String &json_pointer = pointer_track_iter.key;
|
||||
const GLTFAnimation::Channel<Variant> &pointer_track = pointer_track_iter.value;
|
||||
const Ref<GLTFObjectModelProperty> &obj_model_prop = p_state->object_model_properties[json_pointer];
|
||||
Dictionary channel;
|
||||
channel["sampler"] = samplers.size();
|
||||
Dictionary channel_target;
|
||||
channel_target["path"] = "pointer";
|
||||
Dictionary channel_target_ext;
|
||||
Dictionary channel_target_ext_khr_anim_ptr;
|
||||
channel_target_ext_khr_anim_ptr["pointer"] = json_pointer;
|
||||
channel_target_ext["KHR_animation_pointer"] = channel_target_ext_khr_anim_ptr;
|
||||
channel_target["extensions"] = channel_target_ext;
|
||||
channel["target"] = channel_target;
|
||||
channels.push_back(channel);
|
||||
Dictionary sampler;
|
||||
sampler["input"] = _encode_accessor_as_floats(p_state, pointer_track.times, false);
|
||||
sampler["interpolation"] = interpolation_to_string(pointer_track.interpolation);
|
||||
sampler["output"] = _encode_accessor_as_variant(p_state, pointer_track.values, obj_model_prop->get_variant_type(), obj_model_prop->get_accessor_type());
|
||||
samplers.push_back(sampler);
|
||||
}
|
||||
}
|
||||
if (channels.size() && samplers.size()) {
|
||||
d["channels"] = channels;
|
||||
d["samplers"] = samplers;
|
||||
@ -5451,6 +5478,16 @@ Error GLTFDocument::_parse_animations(Ref<GLTFState> p_state) {
|
||||
const Dictionary &anim_target = anim_channel["target"];
|
||||
ERR_FAIL_COND_V_MSG(!anim_target.has("path"), ERR_PARSE_ERROR, "glTF: Animation channel target missing required 'path' property.");
|
||||
String path = anim_target["path"];
|
||||
if (path == "pointer") {
|
||||
ERR_FAIL_COND_V(!anim_target.has("extensions"), ERR_PARSE_ERROR);
|
||||
Dictionary target_extensions = anim_target["extensions"];
|
||||
ERR_FAIL_COND_V(!target_extensions.has("KHR_animation_pointer"), ERR_PARSE_ERROR);
|
||||
Dictionary khr_anim_ptr = target_extensions["KHR_animation_pointer"];
|
||||
ERR_FAIL_COND_V(!khr_anim_ptr.has("pointer"), ERR_PARSE_ERROR);
|
||||
String anim_json_ptr = khr_anim_ptr["pointer"];
|
||||
_parse_animation_pointer(p_state, anim_json_ptr, animation, interp, times, output_value_accessor_index);
|
||||
} else {
|
||||
// If it's not a pointer, it's a regular animation channel from vanilla glTF (pos/rot/scale/weights).
|
||||
if (!anim_target.has("node")) {
|
||||
WARN_PRINT("glTF: Animation channel target missing 'node' property. Ignoring this channel.");
|
||||
continue;
|
||||
@ -5514,6 +5551,7 @@ Error GLTFDocument::_parse_animations(Ref<GLTFState> p_state) {
|
||||
WARN_PRINT("Invalid path '" + path + "'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p_state->animations.push_back(animation);
|
||||
}
|
||||
@ -5523,6 +5561,96 @@ Error GLTFDocument::_parse_animations(Ref<GLTFState> p_state) {
|
||||
return OK;
|
||||
}
|
||||
|
||||
void GLTFDocument::_parse_animation_pointer(Ref<GLTFState> p_state, const String &p_animation_json_pointer, const Ref<GLTFAnimation> p_gltf_animation, const GLTFAnimation::Interpolation p_interp, const Vector<double> &p_times, const int p_output_value_accessor_index) {
|
||||
// Special case: Convert TRS animation pointers to node track pos/rot/scale.
|
||||
// This is required to handle skeleton bones, and improves performance for regular nodes.
|
||||
// Mark this as unlikely because TRS animation pointers are not recommended,
|
||||
// since vanilla glTF animations can already animate TRS properties directly.
|
||||
// But having this code exist is required to be spec-compliant and handle all test files.
|
||||
// Note that TRS still needs to be handled in the general case as well, for KHR_interactivity.
|
||||
const PackedStringArray split = p_animation_json_pointer.split("/", false, 3);
|
||||
if (unlikely(split.size() == 3 && split[0] == "nodes" && (split[2] == "translation" || split[2] == "rotation" || split[2] == "scale" || split[2] == "matrix" || split[2] == "weights"))) {
|
||||
const GLTFNodeIndex node_index = split[1].to_int();
|
||||
HashMap<int, GLTFAnimation::NodeTrack> &node_tracks = p_gltf_animation->get_node_tracks();
|
||||
if (!node_tracks.has(node_index)) {
|
||||
node_tracks[node_index] = GLTFAnimation::NodeTrack();
|
||||
}
|
||||
GLTFAnimation::NodeTrack *track = &node_tracks[node_index];
|
||||
if (split[2] == "translation") {
|
||||
const Vector<Vector3> positions = _decode_accessor_as_vec3(p_state, p_output_value_accessor_index, false);
|
||||
track->position_track.interpolation = p_interp;
|
||||
track->position_track.times = p_times;
|
||||
track->position_track.values = positions;
|
||||
} else if (split[2] == "rotation") {
|
||||
const Vector<Quaternion> rotations = _decode_accessor_as_quaternion(p_state, p_output_value_accessor_index, false);
|
||||
track->rotation_track.interpolation = p_interp;
|
||||
track->rotation_track.times = p_times;
|
||||
track->rotation_track.values = rotations;
|
||||
} else if (split[2] == "scale") {
|
||||
const Vector<Vector3> scales = _decode_accessor_as_vec3(p_state, p_output_value_accessor_index, false);
|
||||
track->scale_track.interpolation = p_interp;
|
||||
track->scale_track.times = p_times;
|
||||
track->scale_track.values = scales;
|
||||
} else if (split[2] == "matrix") {
|
||||
const Vector<Transform3D> transforms = _decode_accessor_as_xform(p_state, p_output_value_accessor_index, false);
|
||||
track->position_track.interpolation = p_interp;
|
||||
track->position_track.times = p_times;
|
||||
track->position_track.values.resize(transforms.size());
|
||||
track->rotation_track.interpolation = p_interp;
|
||||
track->rotation_track.times = p_times;
|
||||
track->rotation_track.values.resize(transforms.size());
|
||||
track->scale_track.interpolation = p_interp;
|
||||
track->scale_track.times = p_times;
|
||||
track->scale_track.values.resize(transforms.size());
|
||||
for (int i = 0; i < transforms.size(); i++) {
|
||||
track->position_track.values.write[i] = transforms[i].get_origin();
|
||||
track->rotation_track.values.write[i] = transforms[i].basis.get_rotation_quaternion();
|
||||
track->scale_track.values.write[i] = transforms[i].basis.get_scale();
|
||||
}
|
||||
} else { // if (split[2] == "weights")
|
||||
const Vector<float> accessor_weights = _decode_accessor_as_floats(p_state, p_output_value_accessor_index, false);
|
||||
const GLTFMeshIndex mesh_index = p_state->nodes[node_index]->mesh;
|
||||
ERR_FAIL_INDEX(mesh_index, p_state->meshes.size());
|
||||
const Ref<GLTFMesh> gltf_mesh = p_state->meshes[mesh_index];
|
||||
const Vector<float> &blend_weights = gltf_mesh->get_blend_weights();
|
||||
const int blend_weight_count = gltf_mesh->get_blend_weights().size();
|
||||
const int anim_weights_size = accessor_weights.size();
|
||||
// For example, if a mesh has 2 blend weights, and the accessor provides 10 values, then there are 5 frames of animation, each with 2 blend weights.
|
||||
ERR_FAIL_COND_MSG(blend_weight_count == 0 || ((anim_weights_size % blend_weight_count) != 0), "glTF: Cannot apply " + itos(accessor_weights.size()) + " weights to a mesh with " + itos(blend_weights.size()) + " blend weights.");
|
||||
const int frame_count = anim_weights_size / blend_weight_count;
|
||||
track->weight_tracks.resize(blend_weight_count);
|
||||
for (int blend_weight_index = 0; blend_weight_index < blend_weight_count; blend_weight_index++) {
|
||||
GLTFAnimation::Channel<real_t> weight_track;
|
||||
weight_track.interpolation = p_interp;
|
||||
weight_track.times = p_times;
|
||||
weight_track.values.resize(frame_count);
|
||||
for (int frame_index = 0; frame_index < frame_count; frame_index++) {
|
||||
// For example, if a mesh has 2 blend weights, and the accessor provides 10 values,
|
||||
// then the first frame has indices [0, 1], the second frame has [2, 3], and so on.
|
||||
// Here we process all frames of one blend weight, so we want [0, 2, 4, 6, 8] or [1, 3, 5, 7, 9].
|
||||
// For the fist one we calculate 0 * 2 + 0, 1 * 2 + 0, 2 * 2 + 0, etc, then for the second 0 * 2 + 1, 1 * 2 + 1, 2 * 2 + 1, etc.
|
||||
weight_track.values.write[frame_index] = accessor_weights[frame_index * blend_weight_count + blend_weight_index];
|
||||
}
|
||||
track->weight_tracks.write[blend_weight_index] = weight_track;
|
||||
}
|
||||
}
|
||||
// The special case was handled, return to skip the general case.
|
||||
return;
|
||||
}
|
||||
// General case: Convert animation pointers to Variant value pointer tracks.
|
||||
Ref<GLTFObjectModelProperty> obj_model_prop = import_object_model_property(p_state, p_animation_json_pointer);
|
||||
if (obj_model_prop.is_null() || !obj_model_prop->has_node_paths()) {
|
||||
// Exit quietly, `import_object_model_property` already prints a warning if the property is not found.
|
||||
return;
|
||||
}
|
||||
HashMap<String, GLTFAnimation::Channel<Variant>> &anim_ptr_map = p_gltf_animation->get_pointer_tracks();
|
||||
GLTFAnimation::Channel<Variant> channel;
|
||||
channel.interpolation = p_interp;
|
||||
channel.times = p_times;
|
||||
channel.values = _decode_accessor_as_variant(p_state, p_output_value_accessor_index, obj_model_prop->get_variant_type(), obj_model_prop->get_accessor_type());
|
||||
anim_ptr_map[p_animation_json_pointer] = channel;
|
||||
}
|
||||
|
||||
void GLTFDocument::_assign_node_names(Ref<GLTFState> p_state) {
|
||||
for (int i = 0; i < p_state->nodes.size(); i++) {
|
||||
Ref<GLTFNode> gltf_node = p_state->nodes[i];
|
||||
@ -7052,6 +7180,56 @@ void GLTFDocument::_import_animation(Ref<GLTFState> p_state, AnimationPlayer *p_
|
||||
}
|
||||
}
|
||||
|
||||
for (const KeyValue<String, GLTFAnimation::Channel<Variant>> &track_iter : anim->get_pointer_tracks()) {
|
||||
// Determine the property to animate.
|
||||
const String json_pointer = track_iter.key;
|
||||
const Ref<GLTFObjectModelProperty> prop = import_object_model_property(p_state, json_pointer);
|
||||
ERR_FAIL_COND(prop.is_null());
|
||||
// Adjust the animation duration to encompass all keyframes.
|
||||
const GLTFAnimation::Channel<Variant> &channel = track_iter.value;
|
||||
ERR_CONTINUE_MSG(channel.times.size() != channel.values.size(), vformat("glTF: Animation pointer '%s' has mismatched keyframe times and values.", json_pointer));
|
||||
if (p_trimming) {
|
||||
for (int i = 0; i < channel.times.size(); i++) {
|
||||
anim_start = MIN(anim_start, channel.times[i]);
|
||||
anim_end = MAX(anim_end, channel.times[i]);
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < channel.times.size(); i++) {
|
||||
anim_end = MAX(anim_end, channel.times[i]);
|
||||
}
|
||||
}
|
||||
// Begin converting the glTF animation to a Godot animation.
|
||||
const Ref<Expression> gltf_to_godot_expr = prop->get_gltf_to_godot_expression();
|
||||
const bool is_gltf_to_godot_expr_valid = gltf_to_godot_expr.is_valid();
|
||||
for (const NodePath node_path : prop->get_node_paths()) {
|
||||
// If using an expression, determine the base instance to pass to the expression.
|
||||
Object *base_instance = nullptr;
|
||||
if (is_gltf_to_godot_expr_valid) {
|
||||
Ref<Resource> resource;
|
||||
Vector<StringName> leftover_subpath;
|
||||
base_instance = scene_root->get_node_and_resource(node_path, resource, leftover_subpath);
|
||||
if (resource.is_valid()) {
|
||||
base_instance = resource.ptr();
|
||||
}
|
||||
}
|
||||
// Add a track and insert all keys and values.
|
||||
const int track_index = animation->get_track_count();
|
||||
animation->add_track(Animation::TYPE_VALUE);
|
||||
animation->track_set_interpolation_type(track_index, GLTFAnimation::gltf_to_godot_interpolation(channel.interpolation));
|
||||
animation->track_set_path(track_index, node_path);
|
||||
for (int i = 0; i < channel.times.size(); i++) {
|
||||
const double time = channel.times[i];
|
||||
Variant value = channel.values[i];
|
||||
if (is_gltf_to_godot_expr_valid) {
|
||||
Array inputs;
|
||||
inputs.append(value);
|
||||
value = gltf_to_godot_expr->execute(inputs, base_instance);
|
||||
}
|
||||
animation->track_insert_key(track_index, time, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
animation->set_length(anim_end - anim_start);
|
||||
|
||||
Ref<AnimationLibrary> library;
|
||||
@ -7632,6 +7810,8 @@ bool GLTFDocument::_convert_animation_node_track(Ref<GLTFState> p_state, GLTFAni
|
||||
return false;
|
||||
}
|
||||
// If we reached this point, the track was some kind of TRS track and was successfully converted.
|
||||
// All failure paths should return false before this point to indicate this
|
||||
// isn't a node track so it can be handled by KHR_animation_pointer instead.
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -7720,6 +7900,46 @@ void GLTFDocument::_convert_animation(Ref<GLTFState> p_state, AnimationPlayer *p
|
||||
node_tracks[node_i] = track;
|
||||
continue;
|
||||
}
|
||||
// If the track wasn't a TRS track or Blend Shape track, it might be a Value track animating a property.
|
||||
// Then this is something that we need to handle with KHR_animation_pointer.
|
||||
Ref<GLTFObjectModelProperty> obj_model_prop = export_object_model_property(p_state, track_path, animated_node, node_i);
|
||||
if (obj_model_prop.is_valid() && obj_model_prop->has_json_pointers()) {
|
||||
// Insert the property track into the KHR_animation_pointer pointer tracks.
|
||||
GLTFAnimation::Channel<Variant> channel;
|
||||
channel.interpolation = gltf_interpolation;
|
||||
channel.times = times;
|
||||
channel.values.resize(anim_key_count);
|
||||
// If using an expression, determine the base instance to pass to the expression.
|
||||
const Ref<Expression> godot_to_gltf_expr = obj_model_prop->get_godot_to_gltf_expression();
|
||||
const bool is_godot_to_gltf_expr_valid = godot_to_gltf_expr.is_valid();
|
||||
Object *base_instance = nullptr;
|
||||
if (is_godot_to_gltf_expr_valid) {
|
||||
Ref<Resource> resource;
|
||||
Vector<StringName> leftover_subpath;
|
||||
base_instance = anim_player_parent->get_node_and_resource(track_path, resource, leftover_subpath);
|
||||
if (resource.is_valid()) {
|
||||
base_instance = resource.ptr();
|
||||
}
|
||||
}
|
||||
// Convert the Godot animation values into glTF animation values (still Variant).
|
||||
for (int32_t key_i = 0; key_i < anim_key_count; key_i++) {
|
||||
Variant value = animation->track_get_key_value(track_index, key_i);
|
||||
if (is_godot_to_gltf_expr_valid) {
|
||||
Array inputs;
|
||||
inputs.append(value);
|
||||
value = godot_to_gltf_expr->execute(inputs, base_instance);
|
||||
}
|
||||
channel.values.write[key_i] = value;
|
||||
}
|
||||
// Use the JSON pointer to insert the property track into the pointer tracks. There will usually be just one JSON pointer.
|
||||
HashMap<String, GLTFAnimation::Channel<Variant>> &pointer_tracks = gltf_animation->get_pointer_tracks();
|
||||
Vector<PackedStringArray> split_json_pointers = obj_model_prop->get_json_pointers();
|
||||
for (const PackedStringArray &split_json_pointer : split_json_pointers) {
|
||||
String json_pointer_str = "/" + String("/").join(split_json_pointer);
|
||||
p_state->object_model_properties[json_pointer_str] = obj_model_prop;
|
||||
pointer_tracks[json_pointer_str] = channel;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!gltf_animation->is_empty_of_tracks()) {
|
||||
p_state->animations.push_back(gltf_animation);
|
||||
@ -7984,6 +8204,7 @@ HashSet<String> GLTFDocument::get_supported_gltf_extensions_hashset() {
|
||||
// If the extension is supported directly in GLTFDocument, list it here.
|
||||
// Other built-in extensions are supported by GLTFDocumentExtension classes.
|
||||
supported_extensions.insert("GODOT_single_root");
|
||||
supported_extensions.insert("KHR_animation_pointer");
|
||||
supported_extensions.insert("KHR_lights_punctual");
|
||||
supported_extensions.insert("KHR_materials_emissive_strength");
|
||||
supported_extensions.insert("KHR_materials_pbrSpecularGlossiness");
|
||||
|
@ -211,6 +211,7 @@ private:
|
||||
Error _parse_cameras(Ref<GLTFState> p_state);
|
||||
Error _parse_lights(Ref<GLTFState> p_state);
|
||||
Error _parse_animations(Ref<GLTFState> p_state);
|
||||
void _parse_animation_pointer(Ref<GLTFState> p_state, const String &p_animation_json_pointer, const Ref<GLTFAnimation> p_gltf_animation, const GLTFAnimation::Interpolation p_interp, const Vector<double> &p_times, const int p_output_value_accessor_index);
|
||||
Error _serialize_animations(Ref<GLTFState> p_state);
|
||||
BoneAttachment3D *_generate_bone_attachment(Ref<GLTFState> p_state,
|
||||
Skeleton3D *p_skeleton,
|
||||
|
@ -90,8 +90,12 @@ HashMap<int, GLTFAnimation::NodeTrack> &GLTFAnimation::get_node_tracks() {
|
||||
return node_tracks;
|
||||
}
|
||||
|
||||
HashMap<String, GLTFAnimation::Channel<Variant>> &GLTFAnimation::get_pointer_tracks() {
|
||||
return pointer_tracks;
|
||||
}
|
||||
|
||||
bool GLTFAnimation::is_empty_of_tracks() const {
|
||||
return node_tracks.is_empty();
|
||||
return node_tracks.is_empty() && pointer_tracks.is_empty();
|
||||
}
|
||||
|
||||
GLTFAnimation::GLTFAnimation() {
|
||||
|
@ -64,6 +64,7 @@ public:
|
||||
String original_name;
|
||||
bool loop = false;
|
||||
HashMap<int, NodeTrack> node_tracks;
|
||||
HashMap<String, Channel<Variant>> pointer_tracks;
|
||||
Dictionary additional_data;
|
||||
|
||||
public:
|
||||
@ -77,6 +78,7 @@ public:
|
||||
void set_loop(bool p_val);
|
||||
|
||||
HashMap<int, GLTFAnimation::NodeTrack> &get_node_tracks();
|
||||
HashMap<String, GLTFAnimation::Channel<Variant>> &get_pointer_tracks();
|
||||
bool is_empty_of_tracks() const;
|
||||
|
||||
Variant get_additional_data(const StringName &p_extension_name);
|
||||
|
Loading…
Reference in New Issue
Block a user