diff --git a/modules/fbx/fbx_document.cpp b/modules/fbx/fbx_document.cpp index ce097092fbd..43cc68eb586 100644 --- a/modules/fbx/fbx_document.cpp +++ b/modules/fbx/fbx_document.cpp @@ -1389,7 +1389,7 @@ Error FBXDocument::_parse_animations(Ref p_state) { for (const ufbx_baked_node &fbx_baked_node : fbx_baked_anim->nodes) { const GLTFNodeIndex node = fbx_baked_node.typed_id; - GLTFAnimation::Track &track = animation->get_tracks()[node]; + GLTFAnimation::NodeTrack &track = animation->get_node_tracks()[node]; for (const ufbx_baked_vec3 &key : fbx_baked_node.translation_keys) { track.position_track.times.push_back(float(key.time)); @@ -1779,8 +1779,8 @@ void FBXDocument::_import_animation(Ref p_state, AnimationPlayer *p_an double anim_start_offset = p_trimming ? double(additional_animation_data["time_begin"]) : 0.0; - for (const KeyValue &track_i : anim->get_tracks()) { - const GLTFAnimation::Track &track = track_i.value; + for (const KeyValue &track_i : anim->get_node_tracks()) { + const GLTFAnimation::NodeTrack &track = track_i.value; //need to find the path: for skeletons, weight tracks will affect the mesh NodePath node_path; //for skeletons, transform tracks always affect bones diff --git a/modules/gltf/config.py b/modules/gltf/config.py index 67233db5796..823b8dbec2c 100644 --- a/modules/gltf/config.py +++ b/modules/gltf/config.py @@ -20,6 +20,7 @@ def get_doc_classes(): "GLTFLight", "GLTFMesh", "GLTFNode", + "GLTFObjectModelProperty", "GLTFPhysicsBody", "GLTFPhysicsShape", "GLTFSkeleton", diff --git a/modules/gltf/doc_classes/GLTFAccessor.xml b/modules/gltf/doc_classes/GLTFAccessor.xml index bc142797a39..04fa2a98351 100644 --- a/modules/gltf/doc_classes/GLTFAccessor.xml +++ b/modules/gltf/doc_classes/GLTFAccessor.xml @@ -22,7 +22,7 @@ The offset relative to the start of the buffer view in bytes. - The glTF component type as an enum. Possible values are 5120 for "BYTE", 5121 for "UNSIGNED_BYTE", 5122 for "SHORT", 5123 for "UNSIGNED_SHORT", 5125 for "UNSIGNED_INT", and 5126 for "FLOAT". A value of 5125 or "UNSIGNED_INT" must not be used for any accessor that is not referenced by mesh.primitive.indices. + The glTF component type as an enum. See [enum GLTFComponentType] for possible values. Within the core glTF specification, a value of 5125 or "UNSIGNED_INT" must not be used for any accessor that is not referenced by mesh.primitive.indices. The number of elements referenced by this accessor. @@ -80,5 +80,41 @@ Accessor type "MAT4". For the glTF object model, this maps to "float4x4", represented in the glTF JSON as an array of sixteen floats. + + Component type "NONE". This is not a valid component type, and is used to indicate that the component type is not set. + + + Component type "BYTE". The value is [code]0x1400[/code] which comes from OpenGL. This indicates data is stored in 1-byte or 8-bit signed integers. This is a core part of the glTF specification. + + + Component type "UNSIGNED_BYTE". The value is [code]0x1401[/code] which comes from OpenGL. This indicates data is stored in 1-byte or 8-bit unsigned integers. This is a core part of the glTF specification. + + + Component type "SHORT". The value is [code]0x1402[/code] which comes from OpenGL. This indicates data is stored in 2-byte or 16-bit signed integers. This is a core part of the glTF specification. + + + Component type "UNSIGNED_SHORT". The value is [code]0x1403[/code] which comes from OpenGL. This indicates data is stored in 2-byte or 16-bit unsigned integers. This is a core part of the glTF specification. + + + Component type "INT". The value is [code]0x1404[/code] which comes from OpenGL. This indicates data is stored in 4-byte or 32-bit signed integers. This is NOT a core part of the glTF specification, and may not be supported by all glTF importers. May be used by some extensions including [code]KHR_interactivity[/code]. + + + Component type "UNSIGNED_INT". The value is [code]0x1405[/code] which comes from OpenGL. This indicates data is stored in 4-byte or 32-bit unsigned integers. This is a core part of the glTF specification. + + + Component type "FLOAT". The value is [code]0x1406[/code] which comes from OpenGL. This indicates data is stored in 4-byte or 32-bit floating point numbers. This is a core part of the glTF specification. + + + Component type "DOUBLE". The value is [code]0x140A[/code] which comes from OpenGL. This indicates data is stored in 8-byte or 64-bit floating point numbers. This is NOT a core part of the glTF specification, and may not be supported by all glTF importers. May be used by some extensions including [code]KHR_interactivity[/code]. + + + Component type "HALF_FLOAT". The value is [code]0x140B[/code] which comes from OpenGL. This indicates data is stored in 2-byte or 16-bit floating point numbers. This is NOT a core part of the glTF specification, and may not be supported by all glTF importers. May be used by some extensions including [code]KHR_interactivity[/code]. + + + Component type "LONG". The value is [code]0x140E[/code] which comes from OpenGL. This indicates data is stored in 8-byte or 64-bit signed integers. This is NOT a core part of the glTF specification, and may not be supported by all glTF importers. May be used by some extensions including [code]KHR_interactivity[/code]. + + + Component type "UNSIGNED_LONG". The value is [code]0x140F[/code] which comes from OpenGL. This indicates data is stored in 8-byte or 64-bit unsigned integers. This is NOT a core part of the glTF specification, and may not be supported by all glTF importers. May be used by some extensions including [code]KHR_interactivity[/code]. + diff --git a/modules/gltf/doc_classes/GLTFDocument.xml b/modules/gltf/doc_classes/GLTFDocument.xml index 10534594d36..ffc3ab926c7 100644 --- a/modules/gltf/doc_classes/GLTFDocument.xml +++ b/modules/gltf/doc_classes/GLTFDocument.xml @@ -45,6 +45,16 @@ Takes a Godot Engine scene node and exports it and its descendants to the given [GLTFState] object through the [param state] parameter. + + + + + + + + Determines a mapping between the given Godot [param node_path] and the corresponding glTF Object Model JSON pointer(s) in the generated glTF file. The details of this mapping are returned in a [GLTFObjectModelProperty] object. Additional mappings can be supplied via the [method GLTFDocumentExtension._import_object_model_property] callback method. + + @@ -70,6 +80,14 @@ [b]Note:[/b] If this method is run before a GLTFDocumentExtension is registered, its extensions won't be included in the list. Be sure to only run this method after all extensions are registered. If you run this when the engine starts, consider waiting a frame before calling this method to ensure all extensions are registered. + + + + + + Determines a mapping between the given glTF Object Model [param json_pointer] and the corresponding Godot node path(s) in the generated Godot scene. The details of this mapping are returned in a [GLTFObjectModelProperty] object. Additional mappings can be supplied via the [method GLTFDocumentExtension._export_object_model_property] callback method. + + diff --git a/modules/gltf/doc_classes/GLTFDocumentExtension.xml b/modules/gltf/doc_classes/GLTFDocumentExtension.xml index b33e296e1ca..8fcb925a482 100644 --- a/modules/gltf/doc_classes/GLTFDocumentExtension.xml +++ b/modules/gltf/doc_classes/GLTFDocumentExtension.xml @@ -33,6 +33,20 @@ This method can be used to modify the final JSON of each node. Data should be primarily stored in [param gltf_node] prior to serializing the JSON, but the original Godot [param node] is also provided if available. The node may be null if not available, such as when exporting glTF data not generated from a Godot scene. + + + + + + + + + + Part of the export process. Allows GLTFDocumentExtension classes to provide mappings for properties of nodes in the Godot scene tree, to JSON pointers to glTF properties, as defined by the glTF object model. + Returns a [GLTFObjectModelProperty] instance that defines how the property should be mapped. If your extension can't handle the property, return null, or an instance without any JSON pointers (see [method GLTFObjectModelProperty.has_json_pointers]). You should use [method GLTFObjectModelProperty.set_types] to set the types, and set the JSON pointer(s) using the [member GLTFObjectModelProperty.json_pointers] property. + The parameters provide context for the property, including the NodePath, the Godot node, the GLTF node index, and the target object. The [param target_object] will be equal to [param godot_node] if no sub-object can be found, otherwise it will point to a sub-object. For example, if the path is [code]^"A/B/C/MeshInstance3D:mesh:surface_0/material:emission_intensity"[/code], it will get the node, then the mesh, and then the material, so [param target_object] will be the [Material] resource, and [param target_depth] will be 2 because 2 levels were traversed to get to the target. + + @@ -109,6 +123,17 @@ This method can be used to make modifications to each of the generated Godot scene nodes. + + + + + + + Part of the import process. Allows GLTFDocumentExtension classes to provide mappings for JSON pointers to glTF properties, as defined by the glTF object model, to properties of nodes in the Godot scene tree. + Returns a [GLTFObjectModelProperty] instance that defines how the property should be mapped. If your extension can't handle the property, return null, or an instance without any NodePaths (see [method GLTFObjectModelProperty.has_node_paths]). You should use [method GLTFObjectModelProperty.set_types] to set the types, and [method GLTFObjectModelProperty.append_path_to_property] function is useful for most simple cases. + In many cases, [param partial_paths] will contain the start of a path, allowing the extension to complete the path. For example, for [code]/nodes/3/extensions/MY_ext/prop[/code], Godot will pass you a NodePath that leads to node 3, so the GLTFDocumentExtension class only needs to resolve the last [code]MY_ext/prop[/code] part of the path. In this example, the extension should check [code]split.size() > 4 and split[0] == "nodes" and split[2] == "extensions" and split[3] == "MY_ext"[/code] at the start of the function to check if this JSON pointer applies to it, then it can use [param partial_paths] and handle [code]split[4][/code]. + + diff --git a/modules/gltf/doc_classes/GLTFNode.xml b/modules/gltf/doc_classes/GLTFNode.xml index a242a0d1d8b..eb92723a06d 100644 --- a/modules/gltf/doc_classes/GLTFNode.xml +++ b/modules/gltf/doc_classes/GLTFNode.xml @@ -27,6 +27,15 @@ The argument should be the [GLTFDocumentExtension] name (does not have to match the extension name in the glTF file), and the return value can be anything you set. If nothing was set, the return value is null. + + + + + + Returns the [NodePath] that this GLTF node will have in the Godot scene tree after being imported. This is useful when importing glTF object model pointers with [GLTFObjectModelProperty], for handling extensions such as [code]KHR_animation_pointer[/code] or [code]KHR_interactivity[/code]. + If [param handle_skeletons] is true, paths to skeleton bone glTF nodes will be resolved properly. For example, a path that would be [code]^"A/B/C/Bone1/Bone2/Bone3"[/code] if false will become [code]^"A/B/C/Skeleton3D:Bone3"[/code]. + + diff --git a/modules/gltf/doc_classes/GLTFObjectModelProperty.xml b/modules/gltf/doc_classes/GLTFObjectModelProperty.xml new file mode 100644 index 00000000000..e983269ccc0 --- /dev/null +++ b/modules/gltf/doc_classes/GLTFObjectModelProperty.xml @@ -0,0 +1,114 @@ + + + + Describes how to access a property as defined in the glTF object model. + + + GLTFObjectModelProperty defines a mapping between a property in the glTF object model and a NodePath in the Godot scene tree. This can be used to animate properties in a glTF file using the [code]KHR_animation_pointer[/code] extension, or to access them through an engine-agnostic script such as a behavior graph as defined by the [code]KHR_interactivity[/code] extension. + The glTF property is identified by JSON pointer(s) stored in [member json_pointers], while the Godot property it maps to is defined by [member node_paths]. In most cases [member json_pointers] and [member node_paths] will each only have one item, but in some cases a single glTF JSON pointer will map to multiple Godot properties, or a single Godot property will be mapped to multiple glTF JSON pointers, or it might be a many-to-many relationship. + [Expression] objects can be used to define conversions between the data, such as when glTF defines an angle in radians and Godot uses degrees. The [member object_model_type] property defines the type of data stored in the glTF file as defined by the object model, see [enum GLTFObjectModelType] for possible values. + + + https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/ObjectModel.adoc + https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_animation_pointer + + + + + + + Appends a [NodePath] to [member node_paths]. This can be used by [GLTFDocumentExtension] classes to define how a glTF object model property maps to a Godot property, or multiple Godot properties. Prefer using [method append_path_to_property] for simple cases. Be sure to also call [method set_types] once (the order does not matter). + + + + + + + + High-level wrapper over [method append_node_path] that handles the most common cases. It constructs a new [NodePath] using [param node_path] as a base and appends [param prop_name] to the subpath. Be sure to also call [method set_types] once (the order does not matter). + + + + + + The GLTF accessor type associated with this property's [member object_model_type]. See [member GLTFAccessor.accessor_type] for possible values, and see [enum GLTFObjectModelType] for how the object model type maps to accessor types. + + + + + + Returns [code]true[/code] if [member json_pointers] is not empty. This is used during export to determine if a [GLTFObjectModelProperty] can handle converting a Godot property to a glTF object model property. + + + + + + Returns [code]true[/code] if [member node_paths] is not empty. This is used during import to determine if a [GLTFObjectModelProperty] can handle converting a glTF object model property to a Godot property. + + + + + + + + Sets the [member variant_type] and [member object_model_type] properties. This is a convenience method to set both properties at once, since they are almost always known at the same time. This method should be called once. Calling it again with the same values will have no effect. + + + + + + If set, this [Expression] will be used to convert the property value from the glTF object model to the value expected by the Godot property. This is useful when the glTF object model uses a different unit system, or when the data needs to be transformed in some way. If [code]null[/code], the value will be copied as-is. + + + If set, this [Expression] will be used to convert the property value from the Godot property to the value expected by the glTF object model. This is useful when the glTF object model uses a different unit system, or when the data needs to be transformed in some way. If [code]null[/code], the value will be copied as-is. + + + The glTF object model JSON pointers used to identify the property in the glTF object model. In most cases, there will be only one item in this array, but niche cases may require multiple pointers. The items are themselves arrays which represent the JSON pointer split into its components. + + + An array of [NodePath]s that point to a property, or multiple properties, in the Godot scene tree. On import, this will either be set by [GLTFDocument], or by a [GLTFDocumentExtension] class. For simple cases, use [method append_path_to_property] to add properties to this array. + In most cases [member node_paths] will only have one item, but in some cases a single glTF JSON pointer will map to multiple Godot properties. For example, a [GLTFCamera] or [GLTFLight] used on multiple glTF nodes will be represented by multiple Godot nodes. + + + The type of data stored in the glTF file as defined by the object model. This is a superset of the available accessor types, and determines the accessor type. See [enum GLTFObjectModelType] for possible values. + + + The type of data stored in the Godot property. This is the type of the property that the [member node_paths] point to. + + + + + Unknown or not set object model type. If the object model type is set to this value, the real type still needs to be determined. + + + Object model type "bool". Represented in the glTF JSON as a boolean, and encoded in a [GLTFAccessor] as "SCALAR". When encoded in an accessor, a value of 0 is false, and any other value is true. + + + Object model type "float". Represented in the glTF JSON as a number, and encoded in a [GLTFAccessor] as "SCALAR". + + + Object model type "float[lb][rb]". Represented in the glTF JSON as an array of numbers, and encoded in a [GLTFAccessor] as "SCALAR". + + + Object model type "float2". Represented in the glTF JSON as an array of two numbers, and encoded in a [GLTFAccessor] as "VEC2". + + + Object model type "float3". Represented in the glTF JSON as an array of three numbers, and encoded in a [GLTFAccessor] as "VEC3". + + + Object model type "float4". Represented in the glTF JSON as an array of four numbers, and encoded in a [GLTFAccessor] as "VEC4". + + + Object model type "float2x2". Represented in the glTF JSON as an array of four numbers, and encoded in a [GLTFAccessor] as "MAT2". + + + Object model type "float3x3". Represented in the glTF JSON as an array of nine numbers, and encoded in a [GLTFAccessor] as "MAT3". + + + Object model type "float4x4". Represented in the glTF JSON as an array of sixteen numbers, and encoded in a [GLTFAccessor] as "MAT4". + + + Object model type "int". Represented in the glTF JSON as a number, and encoded in a [GLTFAccessor] as "SCALAR". The range of values is limited to signed integers. For [code]KHR_interactivity[/code], only 32-bit integers are supported. + + + diff --git a/modules/gltf/extensions/gltf_document_extension.cpp b/modules/gltf/extensions/gltf_document_extension.cpp index 6e611762b69..0806eee6bfe 100644 --- a/modules/gltf/extensions/gltf_document_extension.cpp +++ b/modules/gltf/extensions/gltf_document_extension.cpp @@ -38,6 +38,7 @@ void GLTFDocumentExtension::_bind_methods() { GDVIRTUAL_BIND(_parse_image_data, "state", "image_data", "mime_type", "ret_image"); GDVIRTUAL_BIND(_get_image_file_extension); GDVIRTUAL_BIND(_parse_texture_json, "state", "texture_json", "ret_gltf_texture"); + GDVIRTUAL_BIND(_import_object_model_property, "state", "split_json_pointer", "partial_paths"); GDVIRTUAL_BIND(_import_post_parse, "state"); GDVIRTUAL_BIND(_import_pre_generate, "state"); GDVIRTUAL_BIND(_generate_scene_node, "state", "gltf_node", "scene_parent"); @@ -48,6 +49,7 @@ void GLTFDocumentExtension::_bind_methods() { GDVIRTUAL_BIND(_convert_scene_node, "state", "gltf_node", "scene_node"); GDVIRTUAL_BIND(_export_post_convert, "state", "root"); GDVIRTUAL_BIND(_export_preserialize, "state"); + GDVIRTUAL_BIND(_export_object_model_property, "state", "node_path", "godot_node", "gltf_node_index", "target_object", "target_depth"); GDVIRTUAL_BIND(_get_saveable_image_formats); GDVIRTUAL_BIND(_serialize_image_to_bytes, "state", "image", "image_dict", "image_format", "lossy_quality"); GDVIRTUAL_BIND(_save_image_at_path, "state", "image", "file_path", "image_format", "lossy_quality"); @@ -100,6 +102,13 @@ Error GLTFDocumentExtension::parse_texture_json(Ref p_state, const Di return err; } +Ref GLTFDocumentExtension::import_object_model_property(Ref p_state, const PackedStringArray &p_split_json_pointer, const TypedArray &p_partial_paths) { + Ref ret; + ERR_FAIL_COND_V(p_state.is_null(), ret); + GDVIRTUAL_CALL(_import_object_model_property, p_state, p_split_json_pointer, p_partial_paths, ret); + return ret; +} + Error GLTFDocumentExtension::import_post_parse(Ref p_state) { ERR_FAIL_COND_V(p_state.is_null(), ERR_INVALID_PARAMETER); Error err = OK; @@ -169,6 +178,15 @@ Error GLTFDocumentExtension::export_preserialize(Ref p_state) { return err; } +Ref GLTFDocumentExtension::export_object_model_property(Ref p_state, const NodePath &p_node_path, const Node *p_godot_node, GLTFNodeIndex p_gltf_node_index, const Object *p_target_object, int p_target_depth) { + Ref ret; + ERR_FAIL_COND_V(p_state.is_null(), ret); + ERR_FAIL_NULL_V(p_godot_node, ret); + ERR_FAIL_NULL_V(p_target_object, ret); + GDVIRTUAL_CALL(_export_object_model_property, p_state, p_node_path, p_godot_node, p_gltf_node_index, p_target_object, p_target_depth, ret); + return ret; +} + Vector GLTFDocumentExtension::get_saveable_image_formats() { Vector ret; GDVIRTUAL_CALL(_get_saveable_image_formats, ret); diff --git a/modules/gltf/extensions/gltf_document_extension.h b/modules/gltf/extensions/gltf_document_extension.h index b70710e0150..a6368ea7808 100644 --- a/modules/gltf/extensions/gltf_document_extension.h +++ b/modules/gltf/extensions/gltf_document_extension.h @@ -49,6 +49,7 @@ public: virtual Error parse_image_data(Ref p_state, const PackedByteArray &p_image_data, const String &p_mime_type, Ref r_image); virtual String get_image_file_extension(); virtual Error parse_texture_json(Ref p_state, const Dictionary &p_texture_json, Ref r_gltf_texture); + virtual Ref import_object_model_property(Ref p_state, const PackedStringArray &p_split_json_pointer, const TypedArray &p_partial_paths); virtual Error import_post_parse(Ref p_state); virtual Error import_pre_generate(Ref p_state); virtual Node3D *generate_scene_node(Ref p_state, Ref p_gltf_node, Node *p_scene_parent); @@ -59,6 +60,7 @@ public: virtual void convert_scene_node(Ref p_state, Ref p_gltf_node, Node *p_scene_node); virtual Error export_post_convert(Ref p_state, Node *p_root); virtual Error export_preserialize(Ref p_state); + virtual Ref export_object_model_property(Ref p_state, const NodePath &p_node_path, const Node *p_godot_node, GLTFNodeIndex p_gltf_node_index, const Object *p_target_object, int p_target_depth); virtual Vector get_saveable_image_formats(); virtual PackedByteArray serialize_image_to_bytes(Ref p_state, Ref p_image, Dictionary p_image_dict, const String &p_image_format, float p_lossy_quality); virtual Error save_image_at_path(Ref p_state, Ref p_image, const String &p_file_path, const String &p_image_format, float p_lossy_quality); @@ -73,6 +75,7 @@ public: GDVIRTUAL4R(Error, _parse_image_data, Ref, PackedByteArray, String, Ref); GDVIRTUAL0R(String, _get_image_file_extension); GDVIRTUAL3R(Error, _parse_texture_json, Ref, Dictionary, Ref); + GDVIRTUAL3R(Ref, _import_object_model_property, Ref, PackedStringArray, TypedArray); GDVIRTUAL1R(Error, _import_post_parse, Ref); GDVIRTUAL1R(Error, _import_pre_generate, Ref); GDVIRTUAL3R(Node3D *, _generate_scene_node, Ref, Ref, Node *); @@ -83,6 +86,7 @@ public: GDVIRTUAL3(_convert_scene_node, Ref, Ref, Node *); GDVIRTUAL2R(Error, _export_post_convert, Ref, Node *); GDVIRTUAL1R(Error, _export_preserialize, Ref); + GDVIRTUAL6R(Ref, _export_object_model_property, Ref, NodePath, const Node *, GLTFNodeIndex, const Object *, int); GDVIRTUAL0R(Vector, _get_saveable_image_formats); GDVIRTUAL5R(PackedByteArray, _serialize_image_to_bytes, Ref, Ref, Dictionary, String, float); GDVIRTUAL5R(Error, _save_image_at_path, Ref, Ref, String, String, float); diff --git a/modules/gltf/extensions/gltf_light.cpp b/modules/gltf/extensions/gltf_light.cpp index f6e91c16353..2bdcab2f0c5 100644 --- a/modules/gltf/extensions/gltf_light.cpp +++ b/modules/gltf/extensions/gltf_light.cpp @@ -30,6 +30,7 @@ #include "gltf_light.h" +#include "../structures/gltf_object_model_property.h" #include "scene/3d/light_3d.h" void GLTFLight::_bind_methods() { @@ -62,6 +63,21 @@ void GLTFLight::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "outer_cone_angle"), "set_outer_cone_angle", "get_outer_cone_angle"); // float } +void GLTFLight::set_cone_inner_attenuation_conversion_expressions(Ref &r_obj_model_prop) { + // Expression to convert glTF innerConeAngle to Godot spot_angle_attenuation. + Ref gltf_to_godot_expr; + gltf_to_godot_expr.instantiate(); + PackedStringArray gltf_to_godot_args = { "inner_cone_angle" }; + gltf_to_godot_expr->parse("0.2 / (1.0 - inner_cone_angle / spot_angle) - 0.1", gltf_to_godot_args); + r_obj_model_prop->set_gltf_to_godot_expression(gltf_to_godot_expr); + // Expression to convert Godot spot_angle_attenuation to glTF innerConeAngle. + Ref godot_to_gltf_expr; + godot_to_gltf_expr.instantiate(); + PackedStringArray godot_to_gltf_args = { "godot_spot_angle_att" }; + godot_to_gltf_expr->parse("spot_angle * maxf(0.0, 1.0 - (0.2 / (0.1 + godot_spot_angle_att)))", godot_to_gltf_args); + r_obj_model_prop->set_godot_to_gltf_expression(godot_to_gltf_expr); +} + Color GLTFLight::get_color() { return color; } diff --git a/modules/gltf/extensions/gltf_light.h b/modules/gltf/extensions/gltf_light.h index e0894fc8c66..3d522bd174e 100644 --- a/modules/gltf/extensions/gltf_light.h +++ b/modules/gltf/extensions/gltf_light.h @@ -33,6 +33,7 @@ #include "core/io/resource.h" +class GLTFObjectModelProperty; class Light3D; // https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_lights_punctual @@ -54,6 +55,8 @@ private: Dictionary additional_data; public: + static void set_cone_inner_attenuation_conversion_expressions(Ref &r_obj_model_prop); + Color get_color(); void set_color(Color p_color); diff --git a/modules/gltf/extensions/physics/gltf_document_extension_physics.cpp b/modules/gltf/extensions/physics/gltf_document_extension_physics.cpp index 5c26a1686b2..512f25a2168 100644 --- a/modules/gltf/extensions/physics/gltf_document_extension_physics.cpp +++ b/modules/gltf/extensions/physics/gltf_document_extension_physics.cpp @@ -31,8 +31,11 @@ #include "gltf_document_extension_physics.h" #include "scene/3d/physics/area_3d.h" +#include "scene/3d/physics/rigid_body_3d.h" #include "scene/3d/physics/static_body_3d.h" +using GLTFShapeIndex = int64_t; + // Import process. Error GLTFDocumentExtensionPhysics::import_preflight(Ref p_state, Vector p_extensions) { if (!p_extensions.has("OMI_collider") && !p_extensions.has("OMI_physics_body") && !p_extensions.has("OMI_physics_shape")) { @@ -105,6 +108,7 @@ Error GLTFDocumentExtensionPhysics::parse_node_extensions(Ref p_state Array state_shapes = p_state->get_additional_data(StringName("GLTFPhysicsShapes")); ERR_FAIL_INDEX_V_MSG(node_shape_index, state_shapes.size(), Error::ERR_FILE_CORRUPT, "glTF Physics: On node " + p_gltf_node->get_name() + ", the shape index " + itos(node_shape_index) + " is not in the state shapes (size: " + itos(state_shapes.size()) + ")."); p_gltf_node->set_additional_data(StringName("GLTFPhysicsColliderShape"), state_shapes[node_shape_index]); + p_gltf_node->set_additional_data(StringName("GLTFPhysicsColliderShapeIndex"), node_shape_index); } else { // If this node is a collider but does not have a collider // shape, then it only serves to combine together shapes. @@ -119,6 +123,7 @@ Error GLTFDocumentExtensionPhysics::parse_node_extensions(Ref p_state Array state_shapes = p_state->get_additional_data(StringName("GLTFPhysicsShapes")); ERR_FAIL_INDEX_V_MSG(node_shape_index, state_shapes.size(), Error::ERR_FILE_CORRUPT, "glTF Physics: On node " + p_gltf_node->get_name() + ", the shape index " + itos(node_shape_index) + " is not in the state shapes (size: " + itos(state_shapes.size()) + ")."); p_gltf_node->set_additional_data(StringName("GLTFPhysicsTriggerShape"), state_shapes[node_shape_index]); + p_gltf_node->set_additional_data(StringName("GLTFPhysicsTriggerShapeIndex"), node_shape_index); } else { // If this node is a trigger but does not have a trigger shape, // then it's a trigger body, what Godot calls an Area3D node. @@ -129,8 +134,8 @@ Error GLTFDocumentExtensionPhysics::parse_node_extensions(Ref p_state } // If this node defines explicit member shape nodes, save this information. if (node_trigger.has("nodes")) { - Array node_trigger_nodes = node_trigger["nodes"]; - p_gltf_node->set_additional_data(StringName("GLTFPhysicsCompoundTriggerNodes"), node_trigger_nodes); + Array compound_trigger_nodes = node_trigger["nodes"]; + p_gltf_node->set_additional_data(StringName("GLTFPhysicsCompoundTriggerNodes"), compound_trigger_nodes); } } if (physics_body_ext.has("motion") || physics_body_ext.has("type")) { @@ -140,6 +145,144 @@ Error GLTFDocumentExtensionPhysics::parse_node_extensions(Ref p_state return OK; } +bool _will_gltf_shape_become_subnode(Ref p_state, const Ref p_gltf_node, GLTFNodeIndex p_gltf_node_index) { + if (p_gltf_node->has_additional_data(StringName("GLTFPhysicsBody"))) { + return true; + } + const TypedArray state_gltf_nodes = p_state->get_nodes(); + const GLTFNodeIndex parent_index = p_gltf_node->get_parent(); + if (parent_index == -1 || parent_index >= state_gltf_nodes.size()) { + return true; + } + const Ref parent_gltf_node = state_gltf_nodes[parent_index]; + const Variant parent_body_maybe = parent_gltf_node->get_additional_data(StringName("GLTFPhysicsBody")); + if (parent_body_maybe.get_type() != Variant::NIL) { + Ref parent_body = parent_body_maybe; + // If the parent matches the triggerness, then this node will be generated as a shape (CollisionShape3D). + // Otherwise, if there is a mismatch, a body will be generated for this node, and a subnode will also be generated for the shape. + if (parent_body->get_body_type() == "trigger") { + return p_gltf_node->has_additional_data(StringName("GLTFPhysicsColliderShape")); + } else { + return p_gltf_node->has_additional_data(StringName("GLTFPhysicsTriggerShape")); + } + } + if (parent_gltf_node->has_additional_data(StringName("GLTFPhysicsColliderShape"))) { + return false; + } + if (parent_gltf_node->has_additional_data(StringName("GLTFPhysicsTriggerShape"))) { + return false; + } + Variant compound_trigger_maybe = parent_gltf_node->has_additional_data(StringName("GLTFPhysicsCompoundTriggerNodes")); + if (compound_trigger_maybe.get_type() != Variant::NIL) { + Array compound_trigger_nodes = compound_trigger_maybe; + // Remember, JSON only has numbers, not integers, so must cast to double. + return !compound_trigger_nodes.has((double)p_gltf_node_index); + } + return true; +} + +NodePath _get_scene_node_path_for_shape_index(Ref p_state, const GLTFNodeIndex p_shape_index) { + TypedArray state_gltf_nodes = p_state->get_nodes(); + for (GLTFNodeIndex node_index = 0; node_index < state_gltf_nodes.size(); node_index++) { + const Ref gltf_node = state_gltf_nodes[node_index]; + ERR_CONTINUE(gltf_node.is_null()); + // Check if this node has a shape index and if it matches the one we are looking for. + Variant shape_index_maybe = gltf_node->get_additional_data(StringName("GLTFPhysicsColliderShapeIndex")); + if (shape_index_maybe.get_type() != Variant::INT) { + shape_index_maybe = gltf_node->get_additional_data(StringName("GLTFPhysicsTriggerShapeIndex")); + if (shape_index_maybe.get_type() != Variant::INT) { + continue; + } + } + const GLTFShapeIndex shape_index = shape_index_maybe; + if (shape_index != p_shape_index) { + continue; + } + NodePath node_path = gltf_node->get_scene_node_path(p_state); + // At this point, we have found a node with the shape index we were looking for. + if (_will_gltf_shape_become_subnode(p_state, gltf_node, node_index)) { + Vector sname_path = node_path.get_names(); + sname_path.append(gltf_node->get_name() + "Shape"); + node_path = NodePath(sname_path, false); + } + return node_path; + } + return NodePath(); +} + +Ref GLTFDocumentExtensionPhysics::import_object_model_property(Ref p_state, const PackedStringArray &p_split_json_pointer, const TypedArray &p_partial_paths) { + Ref ret; + if (p_split_json_pointer.size() != 6) { + // The only properties this class cares about are exactly 6 levels deep. + return ret; + } + ret.instantiate(); + const String &prop_name = p_split_json_pointer[5]; + if (p_split_json_pointer[0] == "extensions" && p_split_json_pointer[2] == "shapes") { + if (p_split_json_pointer[1] == "OMI_physics_shape" || p_split_json_pointer[1] == "KHR_collision_shapes") { + const GLTFNodeIndex shape_index = p_split_json_pointer[3].to_int(); + NodePath node_path = _get_scene_node_path_for_shape_index(p_state, shape_index); + if (node_path.is_empty()) { + return ret; + } + String godot_prop_name = prop_name; + if (prop_name == "size") { + ret->set_types(Variant::VECTOR3, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT3); + } else if (prop_name == "height" || prop_name == "radius") { + ret->set_types(Variant::FLOAT, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT); + } else if (prop_name == "radiusBottom" || prop_name == "radiusTop") { + godot_prop_name = "radius"; + ret->set_types(Variant::FLOAT, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT); + } else { + // Not something we handle, return without appending a NodePath. + return ret; + } + // Example: `A/B/C/CollisionShape3D:shape:radius`. + Vector subnames; + subnames.append("shape"); + subnames.append(godot_prop_name); + node_path = NodePath(node_path.get_names(), subnames, false); + ret->append_node_path(node_path); + } + } else if (p_split_json_pointer[0] == "nodes" && p_split_json_pointer[2] == "extensions" && p_split_json_pointer[4] == "motion") { + if (p_split_json_pointer[3] == "OMI_physics_body" || p_split_json_pointer[3] == "KHR_physics_rigid_bodies") { + const GLTFNodeIndex node_index = p_split_json_pointer[1].to_int(); + const TypedArray all_gltf_nodes = p_state->get_nodes(); + ERR_FAIL_INDEX_V_MSG(node_index, all_gltf_nodes.size(), ret, "GLTF Physics: The node index " + itos(node_index) + " is not in the state nodes (size: " + itos(all_gltf_nodes.size()) + ")."); + const Ref gltf_node = all_gltf_nodes[node_index]; + NodePath node_path; + if (p_partial_paths.is_empty()) { + node_path = gltf_node->get_scene_node_path(p_state); + } else { + // The path is already computed for us, just grab it. + node_path = p_partial_paths[0]; + } + if (prop_name == "mass") { + ret->append_path_to_property(node_path, "mass"); + ret->set_types(Variant::FLOAT, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT); + } else if (prop_name == "linearVelocity") { + ret->append_path_to_property(node_path, "linear_velocity"); + ret->set_types(Variant::VECTOR3, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT3); + } else if (prop_name == "angularVelocity") { + ret->append_path_to_property(node_path, "angular_velocity"); + ret->set_types(Variant::VECTOR3, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT3); + } else if (prop_name == "centerOfMass") { + ret->append_path_to_property(node_path, "center_of_mass"); + ret->set_types(Variant::VECTOR3, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT3); + } else if (prop_name == "inertiaDiagonal") { + ret->append_path_to_property(node_path, "inertia"); + ret->set_types(Variant::VECTOR3, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT3); + } else if (prop_name == "inertiaOrientation") { + WARN_PRINT("GLTF Physics: The 'inertiaOrientation' property is not supported by Godot."); + } else { + // Not something we handle, return without appending a NodePath. + return ret; + } + } + } + return ret; +} + void _setup_shape_mesh_resource_from_index_if_needed(Ref p_state, Ref p_gltf_shape) { GLTFMeshIndex shape_mesh_index = p_gltf_shape->get_mesh_index(); if (shape_mesh_index == -1) { @@ -434,24 +577,126 @@ Array _get_or_create_state_shapes_in_state(Ref p_state) { return state_shapes; } -Dictionary _export_node_shape(Ref p_state, Ref p_physics_shape) { +GLTFShapeIndex _export_node_shape(Ref p_state, Ref p_physics_shape) { Array state_shapes = _get_or_create_state_shapes_in_state(p_state); - int size = state_shapes.size(); + GLTFShapeIndex size = state_shapes.size(); Dictionary shape_property; Dictionary shape_dict = p_physics_shape->to_dictionary(); - for (int i = 0; i < size; i++) { + for (GLTFShapeIndex i = 0; i < size; i++) { Dictionary other = state_shapes[i]; if (other == shape_dict) { // De-duplication: If we already have an identical shape, // set the shape index to the existing one and return. - shape_property["shape"] = i; - return shape_property; + return i; } } // If we don't have an identical shape, add it to the array. state_shapes.push_back(shape_dict); - shape_property["shape"] = size; - return shape_property; + return size; +} + +Error GLTFDocumentExtensionPhysics::export_preserialize(Ref p_state) { + // Note: Need to do _export_node_shape before exporting animations, so export_node is too late. + TypedArray state_gltf_nodes = p_state->get_nodes(); + for (Ref gltf_node : state_gltf_nodes) { + Ref collider_shape = gltf_node->get_additional_data(StringName("GLTFPhysicsColliderShape")); + if (collider_shape.is_valid()) { + GLTFShapeIndex collider_shape_index = _export_node_shape(p_state, collider_shape); + gltf_node->set_additional_data(StringName("GLTFPhysicsColliderShapeIndex"), collider_shape_index); + } + Ref trigger_shape = gltf_node->get_additional_data(StringName("GLTFPhysicsTriggerShape")); + if (trigger_shape.is_valid()) { + GLTFShapeIndex trigger_shape_index = _export_node_shape(p_state, trigger_shape); + gltf_node->set_additional_data(StringName("GLTFPhysicsTriggerShapeIndex"), trigger_shape_index); + } + } + return OK; +} + +Ref GLTFDocumentExtensionPhysics::export_object_model_property(Ref p_state, const NodePath &p_node_path, const Node *p_godot_node, GLTFNodeIndex p_gltf_node_index, const Object *p_target_object, int p_target_depth) { + Ref ret; + const Vector &path_subnames = p_node_path.get_subnames(); + if (path_subnames.is_empty()) { + return ret; + } + ret.instantiate(); + const StringName &node_prop = path_subnames[0]; + if (Object::cast_to(p_target_object)) { + if (path_subnames.size() != 1) { + return ret; + } + // Example: `/nodes/0/extensions/OMI_physics_body/motion/mass` + PackedStringArray split_json_pointer; + split_json_pointer.append("nodes"); + split_json_pointer.append(itos(p_gltf_node_index)); + split_json_pointer.append("extensions"); + split_json_pointer.append("OMI_physics_body"); + split_json_pointer.append("motion"); + if (node_prop == StringName("mass")) { + split_json_pointer.append("mass"); + ret->set_types(Variant::FLOAT, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT); + } else if (node_prop == StringName("linear_velocity")) { + split_json_pointer.append("linearVelocity"); + ret->set_types(Variant::VECTOR3, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT3); + } else if (node_prop == StringName("angular_velocity")) { + split_json_pointer.append("angularVelocity"); + ret->set_types(Variant::VECTOR3, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT3); + } else if (node_prop == StringName("center_of_mass")) { + split_json_pointer.append("centerOfMass"); + ret->set_types(Variant::VECTOR3, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT3); + } else if (node_prop == StringName("inertia")) { + split_json_pointer.append("inertiaDiagonal"); + ret->set_types(Variant::VECTOR3, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT3); + } else { + // Not something we handle, return without setting the JSON pointer. + return ret; + } + ret->set_json_pointers({ split_json_pointer }); + } else if (Object::cast_to(p_godot_node)) { + if (path_subnames.size() != 2) { + return ret; + } + // Example: `/extensions/OMI_physics_shape/shapes/0/box/size` + PackedStringArray split_json_pointer; + split_json_pointer.append("extensions"); + split_json_pointer.append("OMI_physics_shape"); + split_json_pointer.append("shapes"); + TypedArray state_gltf_nodes = p_state->get_nodes(); + ERR_FAIL_INDEX_V(p_gltf_node_index, state_gltf_nodes.size(), ret); + Ref gltf_node = state_gltf_nodes[p_gltf_node_index]; + Variant shape_index_maybe = gltf_node->get_additional_data(StringName("GLTFPhysicsColliderShapeIndex")); + String shape_type; + if (shape_index_maybe.get_type() == Variant::INT) { + Ref collider_shape = gltf_node->get_additional_data(StringName("GLTFPhysicsColliderShape")); + shape_type = collider_shape->get_shape_type(); + } else { + shape_index_maybe = gltf_node->get_additional_data(StringName("GLTFPhysicsTriggerShapeIndex")); + if (shape_index_maybe.get_type() == Variant::INT) { + Ref trigger_shape = gltf_node->get_additional_data(StringName("GLTFPhysicsTriggerShape")); + shape_type = trigger_shape->get_shape_type(); + } + } + ERR_FAIL_COND_V(shape_index_maybe.get_type() != Variant::INT, ret); + GLTFShapeIndex shape_index = shape_index_maybe; + split_json_pointer.append(itos(shape_index)); + split_json_pointer.append(shape_type); + const StringName &shape_prop = path_subnames[1]; + if (shape_prop == StringName("size")) { + split_json_pointer.append("size"); + ret->set_types(Variant::VECTOR3, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT3); + } else if (shape_prop == StringName("radius")) { + split_json_pointer.append("radius"); + ret->set_types(Variant::FLOAT, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT); + } else if (shape_prop == StringName("height")) { + split_json_pointer.append("height"); + ret->set_types(Variant::FLOAT, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT); + } else { + // Not something we handle, return without setting the JSON pointer. + return ret; + } + ret->set_json_pointers({ split_json_pointer }); + } + return ret; } Error GLTFDocumentExtensionPhysics::export_node(Ref p_state, Ref p_gltf_node, Dictionary &r_node_json, Node *p_node) { @@ -465,13 +710,16 @@ Error GLTFDocumentExtensionPhysics::export_node(Ref p_state, Ref collider_shape = p_gltf_node->get_additional_data(StringName("GLTFPhysicsColliderShape")); - if (collider_shape.is_valid()) { - physics_body_ext["collider"] = _export_node_shape(p_state, collider_shape); + Variant collider_shape_index = p_gltf_node->get_additional_data(StringName("GLTFPhysicsColliderShapeIndex")); + if (collider_shape_index.get_type() == Variant::INT) { + Dictionary collider_dict; + collider_dict["shape"] = collider_shape_index; + physics_body_ext["collider"] = collider_dict; } - Ref trigger_shape = p_gltf_node->get_additional_data(StringName("GLTFPhysicsTriggerShape")); - if (trigger_shape.is_valid()) { - physics_body_ext["trigger"] = _export_node_shape(p_state, trigger_shape); + Variant trigger_shape_index = p_gltf_node->get_additional_data(StringName("GLTFPhysicsTriggerShapeIndex")); + if (trigger_shape_index.get_type() == Variant::INT) { + Dictionary trigger_dict = physics_body_ext.get_or_add("trigger", {}); + trigger_dict["shape"] = trigger_shape_index; } if (!physics_body_ext.is_empty()) { Dictionary node_extensions = r_node_json["extensions"]; diff --git a/modules/gltf/extensions/physics/gltf_document_extension_physics.h b/modules/gltf/extensions/physics/gltf_document_extension_physics.h index 3d5027c0dff..76a60a6375a 100644 --- a/modules/gltf/extensions/physics/gltf_document_extension_physics.h +++ b/modules/gltf/extensions/physics/gltf_document_extension_physics.h @@ -43,9 +43,12 @@ public: Error import_preflight(Ref p_state, Vector p_extensions) override; Vector get_supported_extensions() override; Error parse_node_extensions(Ref p_state, Ref p_gltf_node, Dictionary &p_extensions) override; + Ref import_object_model_property(Ref p_state, const PackedStringArray &p_split_json_pointer, const TypedArray &p_partial_paths) override; Node3D *generate_scene_node(Ref p_state, Ref p_gltf_node, Node *p_scene_parent) override; // Export process. void convert_scene_node(Ref p_state, Ref p_gltf_node, Node *p_scene_node) override; + Error export_preserialize(Ref p_state) override; + Ref export_object_model_property(Ref p_state, const NodePath &p_node_path, const Node *p_godot_node, GLTFNodeIndex p_gltf_node_index, const Object *p_target_object, int p_target_depth) override; Error export_node(Ref p_state, Ref p_gltf_node, Dictionary &r_node_json, Node *p_scene_node) override; }; diff --git a/modules/gltf/extensions/physics/gltf_physics_body.cpp b/modules/gltf/extensions/physics/gltf_physics_body.cpp index c11aa5d2ff5..7c40f96e0a2 100644 --- a/modules/gltf/extensions/physics/gltf_physics_body.cpp +++ b/modules/gltf/extensions/physics/gltf_physics_body.cpp @@ -193,9 +193,6 @@ Ref GLTFPhysicsBody::from_node(const CollisionObject3D *p_body_ physics_body->angular_velocity = body->get_angular_velocity(); physics_body->center_of_mass = body->get_center_of_mass(); physics_body->inertia_diagonal = body->get_inertia(); - if (body->get_center_of_mass() != Vector3()) { - WARN_PRINT("GLTFPhysicsBody: This rigid body has a center of mass offset from the origin, which will be ignored when exporting to glTF."); - } if (cast_to(p_body_node)) { physics_body->body_type = PhysicsBodyType::VEHICLE; } else { diff --git a/modules/gltf/gltf_defines.h b/modules/gltf/gltf_defines.h index c1918e5908d..4d88f7c3422 100644 --- a/modules/gltf/gltf_defines.h +++ b/modules/gltf/gltf_defines.h @@ -43,6 +43,7 @@ class GLTFDocumentExtension; class GLTFLight; class GLTFMesh; class GLTFNode; +class GLTFObjectModelProperty; class GLTFSkeleton; class GLTFSkin; class GLTFSpecGloss; diff --git a/modules/gltf/gltf_document.cpp b/modules/gltf/gltf_document.cpp index 360b4ec07f7..571a04936da 100644 --- a/modules/gltf/gltf_document.cpp +++ b/modules/gltf/gltf_document.cpp @@ -69,6 +69,10 @@ #include #include +constexpr int COMPONENT_COUNT_FOR_ACCESSOR_TYPE[7] = { + 1, 2, 3, 4, 4, 9, 16 +}; + static void _attach_extras_to_meta(const Dictionary &p_extras, Ref p_node) { if (!p_extras.is_empty()) { p_node->set_meta("extras", p_extras); @@ -1013,7 +1017,7 @@ Error GLTFDocument::_parse_accessors(Ref p_state) { accessor.instantiate(); ERR_FAIL_COND_V(!d.has("componentType"), ERR_PARSE_ERROR); - accessor->component_type = d["componentType"]; + accessor->component_type = (GLTFAccessor::GLTFComponentType)(int32_t)d["componentType"]; ERR_FAIL_COND_V(!d.has("count"), ERR_PARSE_ERROR); accessor->count = d["count"]; ERR_FAIL_COND_V(!d.has("type"), ERR_PARSE_ERROR); @@ -1050,7 +1054,7 @@ Error GLTFDocument::_parse_accessors(Ref p_state) { ERR_FAIL_COND_V(!si.has("bufferView"), ERR_PARSE_ERROR); accessor->sparse_indices_buffer_view = si["bufferView"]; ERR_FAIL_COND_V(!si.has("componentType"), ERR_PARSE_ERROR); - accessor->sparse_indices_component_type = si["componentType"]; + accessor->sparse_indices_component_type = (GLTFAccessor::GLTFComponentType)(int32_t)si["componentType"]; if (si.has("byteOffset")) { accessor->sparse_indices_byte_offset = si["byteOffset"]; @@ -1082,31 +1086,39 @@ double GLTFDocument::_filter_number(double p_float) { return (double)(float)p_float; } -String GLTFDocument::_get_component_type_name(const uint32_t p_component) { +String GLTFDocument::_get_component_type_name(const GLTFAccessor::GLTFComponentType p_component) { switch (p_component) { - case GLTFDocument::COMPONENT_TYPE_BYTE: + case GLTFAccessor::COMPONENT_TYPE_NONE: + return "None"; + case GLTFAccessor::COMPONENT_TYPE_SIGNED_BYTE: return "Byte"; - case GLTFDocument::COMPONENT_TYPE_UNSIGNED_BYTE: + case GLTFAccessor::COMPONENT_TYPE_UNSIGNED_BYTE: return "UByte"; - case GLTFDocument::COMPONENT_TYPE_SHORT: + case GLTFAccessor::COMPONENT_TYPE_SIGNED_SHORT: return "Short"; - case GLTFDocument::COMPONENT_TYPE_UNSIGNED_SHORT: + case GLTFAccessor::COMPONENT_TYPE_UNSIGNED_SHORT: return "UShort"; - case GLTFDocument::COMPONENT_TYPE_INT: + case GLTFAccessor::COMPONENT_TYPE_SIGNED_INT: return "Int"; - case GLTFDocument::COMPONENT_TYPE_FLOAT: + case GLTFAccessor::COMPONENT_TYPE_UNSIGNED_INT: + return "UInt"; + case GLTFAccessor::COMPONENT_TYPE_SINGLE_FLOAT: return "Float"; + case GLTFAccessor::COMPONENT_TYPE_DOUBLE_FLOAT: + return "Double"; + case GLTFAccessor::COMPONENT_TYPE_HALF_FLOAT: + return "Half"; + case GLTFAccessor::COMPONENT_TYPE_SIGNED_LONG: + return "Long"; + case GLTFAccessor::COMPONENT_TYPE_UNSIGNED_LONG: + return "ULong"; } return ""; } -Error GLTFDocument::_encode_buffer_view(Ref p_state, const double *p_src, const int p_count, const GLTFAccessor::GLTFAccessorType p_accessor_type, const int p_component_type, const bool p_normalized, const int p_byte_offset, const bool p_for_vertex, GLTFBufferViewIndex &r_accessor, const bool p_for_vertex_indices) { - const int component_count_for_type[7] = { - 1, 2, 3, 4, 4, 9, 16 - }; - - const int component_count = component_count_for_type[p_accessor_type]; +Error GLTFDocument::_encode_buffer_view(Ref p_state, const double *p_src, const int p_count, const GLTFAccessor::GLTFAccessorType p_accessor_type, const GLTFAccessor::GLTFComponentType p_component_type, const bool p_normalized, const int p_byte_offset, const bool p_for_vertex, GLTFBufferViewIndex &r_accessor, const bool p_for_vertex_indices) { + const int component_count = COMPONENT_COUNT_FOR_ACCESSOR_TYPE[p_accessor_type]; const int component_size = _get_component_type_size(p_component_type); ERR_FAIL_COND_V(component_size == 0, FAILED); @@ -1114,8 +1126,8 @@ Error GLTFDocument::_encode_buffer_view(Ref p_state, const double *p_ int skip_bytes = 0; //special case of alignments, as described in spec switch (p_component_type) { - case COMPONENT_TYPE_BYTE: - case COMPONENT_TYPE_UNSIGNED_BYTE: { + case GLTFAccessor::COMPONENT_TYPE_SIGNED_BYTE: + case GLTFAccessor::COMPONENT_TYPE_UNSIGNED_BYTE: { if (p_accessor_type == GLTFAccessor::TYPE_MAT2) { skip_every = 2; skip_bytes = 2; @@ -1125,8 +1137,8 @@ Error GLTFDocument::_encode_buffer_view(Ref p_state, const double *p_ skip_bytes = 1; } } break; - case COMPONENT_TYPE_SHORT: - case COMPONENT_TYPE_UNSIGNED_SHORT: { + case GLTFAccessor::COMPONENT_TYPE_SIGNED_SHORT: + case GLTFAccessor::COMPONENT_TYPE_UNSIGNED_SHORT: { if (p_accessor_type == GLTFAccessor::TYPE_MAT3) { skip_every = 6; skip_bytes = 4; @@ -1161,7 +1173,10 @@ Error GLTFDocument::_encode_buffer_view(Ref p_state, const double *p_ } switch (p_component_type) { - case COMPONENT_TYPE_BYTE: { + case GLTFAccessor::COMPONENT_TYPE_NONE: { + ERR_FAIL_V_MSG(ERR_INVALID_DATA, "glTF: Failed to encode buffer view, component type not set."); + } + case GLTFAccessor::COMPONENT_TYPE_SIGNED_BYTE: { Vector buffer; buffer.resize(p_count * component_count); int32_t dst_i = 0; @@ -1185,7 +1200,7 @@ Error GLTFDocument::_encode_buffer_view(Ref p_state, const double *p_ memcpy(gltf_buffer.ptrw() + old_size, buffer.ptrw(), buffer.size() * sizeof(int8_t)); bv->byte_length = buffer.size() * sizeof(int8_t); } break; - case COMPONENT_TYPE_UNSIGNED_BYTE: { + case GLTFAccessor::COMPONENT_TYPE_UNSIGNED_BYTE: { Vector buffer; buffer.resize(p_count * component_count); int32_t dst_i = 0; @@ -1207,7 +1222,7 @@ Error GLTFDocument::_encode_buffer_view(Ref p_state, const double *p_ gltf_buffer.append_array(buffer); bv->byte_length = buffer.size() * sizeof(uint8_t); } break; - case COMPONENT_TYPE_SHORT: { + case GLTFAccessor::COMPONENT_TYPE_SIGNED_SHORT: { Vector buffer; buffer.resize(p_count * component_count); int32_t dst_i = 0; @@ -1231,7 +1246,7 @@ Error GLTFDocument::_encode_buffer_view(Ref p_state, const double *p_ memcpy(gltf_buffer.ptrw() + old_size, buffer.ptrw(), buffer.size() * sizeof(int16_t)); bv->byte_length = buffer.size() * sizeof(int16_t); } break; - case COMPONENT_TYPE_UNSIGNED_SHORT: { + case GLTFAccessor::COMPONENT_TYPE_UNSIGNED_SHORT: { Vector buffer; buffer.resize(p_count * component_count); int32_t dst_i = 0; @@ -1255,8 +1270,8 @@ Error GLTFDocument::_encode_buffer_view(Ref p_state, const double *p_ memcpy(gltf_buffer.ptrw() + old_size, buffer.ptrw(), buffer.size() * sizeof(uint16_t)); bv->byte_length = buffer.size() * sizeof(uint16_t); } break; - case COMPONENT_TYPE_INT: { - Vector buffer; + case GLTFAccessor::COMPONENT_TYPE_SIGNED_INT: { + Vector buffer; buffer.resize(p_count * component_count); int32_t dst_i = 0; for (int i = 0; i < p_count; i++) { @@ -1271,11 +1286,31 @@ Error GLTFDocument::_encode_buffer_view(Ref p_state, const double *p_ } } int64_t old_size = gltf_buffer.size(); - gltf_buffer.resize(old_size + (buffer.size() * sizeof(int32_t))); - memcpy(gltf_buffer.ptrw() + old_size, buffer.ptrw(), buffer.size() * sizeof(int32_t)); - bv->byte_length = buffer.size() * sizeof(int32_t); + gltf_buffer.resize(old_size + (buffer.size() * sizeof(uint32_t))); + memcpy(gltf_buffer.ptrw() + old_size, buffer.ptrw(), buffer.size() * sizeof(uint32_t)); + bv->byte_length = buffer.size() * sizeof(uint32_t); } break; - case COMPONENT_TYPE_FLOAT: { + case GLTFAccessor::COMPONENT_TYPE_UNSIGNED_INT: { + Vector buffer; + buffer.resize(p_count * component_count); + int32_t dst_i = 0; + for (int i = 0; i < p_count; i++) { + for (int j = 0; j < component_count; j++) { + if (skip_every && j > 0 && (j % skip_every) == 0) { + dst_i += skip_bytes; + } + double d = *p_src; + buffer.write[dst_i] = d; + p_src++; + dst_i++; + } + } + int64_t old_size = gltf_buffer.size(); + gltf_buffer.resize(old_size + (buffer.size() * sizeof(uint32_t))); + memcpy(gltf_buffer.ptrw() + old_size, buffer.ptrw(), buffer.size() * sizeof(uint32_t)); + bv->byte_length = buffer.size() * sizeof(uint32_t); + } break; + case GLTFAccessor::COMPONENT_TYPE_SINGLE_FLOAT: { Vector buffer; buffer.resize(p_count * component_count); int32_t dst_i = 0; @@ -1295,6 +1330,71 @@ Error GLTFDocument::_encode_buffer_view(Ref p_state, const double *p_ memcpy(gltf_buffer.ptrw() + old_size, buffer.ptrw(), buffer.size() * sizeof(float)); bv->byte_length = buffer.size() * sizeof(float); } break; + case GLTFAccessor::COMPONENT_TYPE_DOUBLE_FLOAT: { + Vector buffer; + buffer.resize(p_count * component_count); + int32_t dst_i = 0; + for (int i = 0; i < p_count; i++) { + for (int j = 0; j < component_count; j++) { + if (skip_every && j > 0 && (j % skip_every) == 0) { + dst_i += skip_bytes; + } + double d = *p_src; + buffer.write[dst_i] = d; + p_src++; + dst_i++; + } + } + int64_t old_size = gltf_buffer.size(); + gltf_buffer.resize(old_size + (buffer.size() * sizeof(double))); + memcpy(gltf_buffer.ptrw() + old_size, buffer.ptrw(), buffer.size() * sizeof(double)); + bv->byte_length = buffer.size() * sizeof(double); + } break; + case GLTFAccessor::COMPONENT_TYPE_HALF_FLOAT: { + ERR_FAIL_V_MSG(ERR_UNAVAILABLE, "glTF: Half float not supported yet."); + } break; + case GLTFAccessor::COMPONENT_TYPE_SIGNED_LONG: { + Vector buffer; + buffer.resize(p_count * component_count); + int32_t dst_i = 0; + for (int i = 0; i < p_count; i++) { + for (int j = 0; j < component_count; j++) { + if (skip_every && j > 0 && (j % skip_every) == 0) { + dst_i += skip_bytes; + } + // FIXME: This can result in precision loss because int64_t can store some values that double can't. + double d = *p_src; + buffer.write[dst_i] = d; + p_src++; + dst_i++; + } + } + int64_t old_size = gltf_buffer.size(); + gltf_buffer.resize(old_size + (buffer.size() * sizeof(int64_t))); + memcpy(gltf_buffer.ptrw() + old_size, buffer.ptrw(), buffer.size() * sizeof(int64_t)); + bv->byte_length = buffer.size() * sizeof(int64_t); + } break; + case GLTFAccessor::COMPONENT_TYPE_UNSIGNED_LONG: { + Vector buffer; + buffer.resize(p_count * component_count); + int32_t dst_i = 0; + for (int i = 0; i < p_count; i++) { + for (int j = 0; j < component_count; j++) { + if (skip_every && j > 0 && (j % skip_every) == 0) { + dst_i += skip_bytes; + } + // FIXME: This can result in precision loss because int64_t can store some values that double can't. + double d = *p_src; + buffer.write[dst_i] = d; + p_src++; + dst_i++; + } + } + int64_t old_size = gltf_buffer.size(); + gltf_buffer.resize(old_size + (buffer.size() * sizeof(uint64_t))); + memcpy(gltf_buffer.ptrw() + old_size, buffer.ptrw(), buffer.size() * sizeof(uint64_t)); + bv->byte_length = buffer.size() * sizeof(uint64_t); + } break; } ERR_FAIL_COND_V(buffer_end > bv->byte_length, ERR_INVALID_DATA); @@ -1309,7 +1409,7 @@ Error GLTFDocument::_encode_buffer_view(Ref p_state, const double *p_ return OK; } -Error GLTFDocument::_decode_buffer_view(Ref p_state, double *p_dst, const GLTFBufferViewIndex p_buffer_view, const int p_skip_every, const int p_skip_bytes, const int p_element_size, const int p_count, const GLTFAccessor::GLTFAccessorType p_accessor_type, const int p_component_count, const int p_component_type, const int p_component_size, const bool p_normalized, const int p_byte_offset, const bool p_for_vertex) { +Error GLTFDocument::_decode_buffer_view(Ref p_state, double *p_dst, const GLTFBufferViewIndex p_buffer_view, const int p_skip_every, const int p_skip_bytes, const int p_element_size, const int p_count, const GLTFAccessor::GLTFAccessorType p_accessor_type, const int p_component_count, const GLTFAccessor::GLTFComponentType p_component_type, const int p_component_size, const bool p_normalized, const int p_byte_offset, const bool p_for_vertex) { const Ref bv = p_state->buffer_views[p_buffer_view]; int stride = p_element_size; @@ -1348,7 +1448,10 @@ Error GLTFDocument::_decode_buffer_view(Ref p_state, double *p_dst, c double d = 0; switch (p_component_type) { - case COMPONENT_TYPE_BYTE: { + case GLTFAccessor::COMPONENT_TYPE_NONE: { + ERR_FAIL_V_MSG(ERR_INVALID_DATA, "glTF: Failed to decode buffer view, component type not set."); + } break; + case GLTFAccessor::COMPONENT_TYPE_SIGNED_BYTE: { int8_t b = int8_t(*src); if (p_normalized) { d = (double(b) / 128.0); @@ -1356,7 +1459,7 @@ Error GLTFDocument::_decode_buffer_view(Ref p_state, double *p_dst, c d = double(b); } } break; - case COMPONENT_TYPE_UNSIGNED_BYTE: { + case GLTFAccessor::COMPONENT_TYPE_UNSIGNED_BYTE: { uint8_t b = *src; if (p_normalized) { d = (double(b) / 255.0); @@ -1364,7 +1467,7 @@ Error GLTFDocument::_decode_buffer_view(Ref p_state, double *p_dst, c d = double(b); } } break; - case COMPONENT_TYPE_SHORT: { + case GLTFAccessor::COMPONENT_TYPE_SIGNED_SHORT: { int16_t s = *(int16_t *)src; if (p_normalized) { d = (double(s) / 32768.0); @@ -1372,7 +1475,7 @@ Error GLTFDocument::_decode_buffer_view(Ref p_state, double *p_dst, c d = double(s); } } break; - case COMPONENT_TYPE_UNSIGNED_SHORT: { + case GLTFAccessor::COMPONENT_TYPE_UNSIGNED_SHORT: { uint16_t s = *(uint16_t *)src; if (p_normalized) { d = (double(s) / 65535.0); @@ -1380,12 +1483,27 @@ Error GLTFDocument::_decode_buffer_view(Ref p_state, double *p_dst, c d = double(s); } } break; - case COMPONENT_TYPE_INT: { - d = *(int *)src; + case GLTFAccessor::COMPONENT_TYPE_SIGNED_INT: { + d = *(int32_t *)src; } break; - case COMPONENT_TYPE_FLOAT: { + case GLTFAccessor::COMPONENT_TYPE_UNSIGNED_INT: { + d = *(uint32_t *)src; + } break; + case GLTFAccessor::COMPONENT_TYPE_SINGLE_FLOAT: { d = *(float *)src; } break; + case GLTFAccessor::COMPONENT_TYPE_DOUBLE_FLOAT: { + d = *(double *)src; + } break; + case GLTFAccessor::COMPONENT_TYPE_HALF_FLOAT: { + ERR_FAIL_V_MSG(ERR_UNAVAILABLE, "glTF: Half float not supported yet."); + } break; + case GLTFAccessor::COMPONENT_TYPE_SIGNED_LONG: { + d = *(int64_t *)src; + } break; + case GLTFAccessor::COMPONENT_TYPE_UNSIGNED_LONG: { + d = *(uint64_t *)src; + } break; } *p_dst++ = d; @@ -1396,25 +1514,27 @@ Error GLTFDocument::_decode_buffer_view(Ref p_state, double *p_dst, c return OK; } -int GLTFDocument::_get_component_type_size(const int p_component_type) { +int GLTFDocument::_get_component_type_size(const GLTFAccessor::GLTFComponentType p_component_type) { switch (p_component_type) { - case COMPONENT_TYPE_BYTE: - case COMPONENT_TYPE_UNSIGNED_BYTE: - return 1; - break; - case COMPONENT_TYPE_SHORT: - case COMPONENT_TYPE_UNSIGNED_SHORT: - return 2; - break; - case COMPONENT_TYPE_INT: - case COMPONENT_TYPE_FLOAT: - return 4; - break; - default: { + case GLTFAccessor::COMPONENT_TYPE_NONE: ERR_FAIL_V(0); - } + case GLTFAccessor::COMPONENT_TYPE_SIGNED_BYTE: + case GLTFAccessor::COMPONENT_TYPE_UNSIGNED_BYTE: + return 1; + case GLTFAccessor::COMPONENT_TYPE_SIGNED_SHORT: + case GLTFAccessor::COMPONENT_TYPE_UNSIGNED_SHORT: + case GLTFAccessor::COMPONENT_TYPE_HALF_FLOAT: + return 2; + case GLTFAccessor::COMPONENT_TYPE_SIGNED_INT: + case GLTFAccessor::COMPONENT_TYPE_UNSIGNED_INT: + case GLTFAccessor::COMPONENT_TYPE_SINGLE_FLOAT: + return 4; + case GLTFAccessor::COMPONENT_TYPE_DOUBLE_FLOAT: + case GLTFAccessor::COMPONENT_TYPE_SIGNED_LONG: + case GLTFAccessor::COMPONENT_TYPE_UNSIGNED_LONG: + return 8; } - return 0; + ERR_FAIL_V(0); } Vector GLTFDocument::_decode_accessor(Ref p_state, const GLTFAccessorIndex p_accessor, const bool p_for_vertex) { @@ -1425,11 +1545,7 @@ Vector GLTFDocument::_decode_accessor(Ref p_state, const GLTF const Ref a = p_state->accessors[p_accessor]; - const int component_count_for_type[7] = { - 1, 2, 3, 4, 4, 9, 16 - }; - - const int component_count = component_count_for_type[a->accessor_type]; + const int component_count = COMPONENT_COUNT_FOR_ACCESSOR_TYPE[a->accessor_type]; const int component_size = _get_component_type_size(a->component_type); ERR_FAIL_COND_V(component_size == 0, Vector()); int element_size = component_count * component_size; @@ -1438,8 +1554,8 @@ Vector GLTFDocument::_decode_accessor(Ref p_state, const GLTF int skip_bytes = 0; //special case of alignments, as described in spec switch (a->component_type) { - case COMPONENT_TYPE_BYTE: - case COMPONENT_TYPE_UNSIGNED_BYTE: { + case GLTFAccessor::COMPONENT_TYPE_SIGNED_BYTE: + case GLTFAccessor::COMPONENT_TYPE_UNSIGNED_BYTE: { if (a->accessor_type == GLTFAccessor::TYPE_MAT2) { skip_every = 2; skip_bytes = 2; @@ -1451,8 +1567,8 @@ Vector GLTFDocument::_decode_accessor(Ref p_state, const GLTF element_size = 12; //override for this case } } break; - case COMPONENT_TYPE_SHORT: - case COMPONENT_TYPE_UNSIGNED_SHORT: { + case GLTFAccessor::COMPONENT_TYPE_SIGNED_SHORT: + case GLTFAccessor::COMPONENT_TYPE_UNSIGNED_SHORT: { if (a->accessor_type == GLTFAccessor::TYPE_MAT3) { skip_every = 6; skip_bytes = 4; @@ -1550,11 +1666,11 @@ GLTFAccessorIndex GLTFDocument::_encode_accessor_as_ints(Ref p_state, } int64_t size = p_state->buffers[0].size(); const GLTFAccessor::GLTFAccessorType accessor_type = GLTFAccessor::TYPE_SCALAR; - int component_type; + GLTFAccessor::GLTFComponentType component_type; if (max_index > 65535 || p_for_vertex) { - component_type = GLTFDocument::COMPONENT_TYPE_INT; + component_type = GLTFAccessor::COMPONENT_TYPE_UNSIGNED_INT; } else { - component_type = GLTFDocument::COMPONENT_TYPE_UNSIGNED_SHORT; + component_type = GLTFAccessor::COMPONENT_TYPE_UNSIGNED_SHORT; } accessor->max = type_max; @@ -1664,7 +1780,7 @@ GLTFAccessorIndex GLTFDocument::_encode_accessor_as_vec2(Ref p_state, } int64_t size = p_state->buffers[0].size(); const GLTFAccessor::GLTFAccessorType accessor_type = GLTFAccessor::TYPE_VEC2; - const int component_type = GLTFDocument::COMPONENT_TYPE_FLOAT; + const GLTFAccessor::GLTFComponentType component_type = GLTFAccessor::COMPONENT_TYPE_SINGLE_FLOAT; accessor->max = type_max; accessor->min = type_min; @@ -1717,7 +1833,7 @@ GLTFAccessorIndex GLTFDocument::_encode_accessor_as_color(Ref p_state } int64_t size = p_state->buffers[0].size(); const GLTFAccessor::GLTFAccessorType accessor_type = GLTFAccessor::TYPE_VEC4; - const int component_type = GLTFDocument::COMPONENT_TYPE_FLOAT; + const GLTFAccessor::GLTFComponentType component_type = GLTFAccessor::COMPONENT_TYPE_SINGLE_FLOAT; accessor->max = type_max; accessor->min = type_min; @@ -1784,7 +1900,7 @@ GLTFAccessorIndex GLTFDocument::_encode_accessor_as_weights(Ref p_sta } int64_t size = p_state->buffers[0].size(); const GLTFAccessor::GLTFAccessorType accessor_type = GLTFAccessor::TYPE_VEC4; - const int component_type = GLTFDocument::COMPONENT_TYPE_FLOAT; + const GLTFAccessor::GLTFComponentType component_type = GLTFAccessor::COMPONENT_TYPE_SINGLE_FLOAT; accessor->max = type_max; accessor->min = type_min; @@ -1835,7 +1951,7 @@ GLTFAccessorIndex GLTFDocument::_encode_accessor_as_joints(Ref p_stat } int64_t size = p_state->buffers[0].size(); const GLTFAccessor::GLTFAccessorType accessor_type = GLTFAccessor::TYPE_VEC4; - const int component_type = GLTFDocument::COMPONENT_TYPE_UNSIGNED_SHORT; + const GLTFAccessor::GLTFComponentType component_type = GLTFAccessor::COMPONENT_TYPE_UNSIGNED_SHORT; accessor->max = type_max; accessor->min = type_min; @@ -1888,7 +2004,7 @@ GLTFAccessorIndex GLTFDocument::_encode_accessor_as_quaternions(Ref p } int64_t size = p_state->buffers[0].size(); const GLTFAccessor::GLTFAccessorType accessor_type = GLTFAccessor::TYPE_VEC4; - const int component_type = GLTFDocument::COMPONENT_TYPE_FLOAT; + const GLTFAccessor::GLTFComponentType component_type = GLTFAccessor::COMPONENT_TYPE_SINGLE_FLOAT; accessor->max = type_max; accessor->min = type_min; @@ -1932,7 +2048,7 @@ Vector GLTFDocument::_decode_accessor_as_vec2(Ref p_state, c return ret; } -GLTFAccessorIndex GLTFDocument::_encode_accessor_as_floats(Ref p_state, const Vector p_attribs, const bool p_for_vertex) { +GLTFAccessorIndex GLTFDocument::_encode_accessor_as_floats(Ref p_state, const Vector p_attribs, const bool p_for_vertex) { if (p_attribs.size() == 0) { return -1; } @@ -1963,7 +2079,7 @@ GLTFAccessorIndex GLTFDocument::_encode_accessor_as_floats(Ref p_stat } int64_t size = p_state->buffers[0].size(); const GLTFAccessor::GLTFAccessorType accessor_type = GLTFAccessor::TYPE_SCALAR; - const int component_type = GLTFDocument::COMPONENT_TYPE_FLOAT; + const GLTFAccessor::GLTFComponentType component_type = GLTFAccessor::COMPONENT_TYPE_SINGLE_FLOAT; accessor->max = type_max; accessor->min = type_min; @@ -2013,7 +2129,7 @@ GLTFAccessorIndex GLTFDocument::_encode_accessor_as_vec3(Ref p_state, } int64_t size = p_state->buffers[0].size(); const GLTFAccessor::GLTFAccessorType accessor_type = GLTFAccessor::TYPE_VEC3; - const int component_type = GLTFDocument::COMPONENT_TYPE_FLOAT; + const GLTFAccessor::GLTFComponentType component_type = GLTFAccessor::COMPONENT_TYPE_SINGLE_FLOAT; accessor->max = type_max; accessor->min = type_min; @@ -2089,7 +2205,7 @@ GLTFAccessorIndex GLTFDocument::_encode_sparse_accessor_as_vec3(Ref p } int64_t size = p_state->buffers[0].size(); const GLTFAccessor::GLTFAccessorType accessor_type = GLTFAccessor::TYPE_VEC3; - const int component_type = GLTFDocument::COMPONENT_TYPE_FLOAT; + const GLTFAccessor::GLTFComponentType component_type = GLTFAccessor::COMPONENT_TYPE_SINGLE_FLOAT; sparse_accessor->normalized = false; sparse_accessor->count = p_attribs.size(); @@ -2112,9 +2228,9 @@ GLTFAccessorIndex GLTFDocument::_encode_sparse_accessor_as_vec3(Ref p GLTFBufferIndex buffer_view_i_indices = -1; GLTFBufferIndex buffer_view_i_values = -1; if (sparse_accessor_index_stride == 4) { - sparse_accessor->sparse_indices_component_type = GLTFDocument::COMPONENT_TYPE_INT; + sparse_accessor->sparse_indices_component_type = GLTFAccessor::COMPONENT_TYPE_UNSIGNED_INT; } else { - sparse_accessor->sparse_indices_component_type = GLTFDocument::COMPONENT_TYPE_UNSIGNED_SHORT; + sparse_accessor->sparse_indices_component_type = GLTFAccessor::COMPONENT_TYPE_UNSIGNED_SHORT; } if (_encode_buffer_view(p_state, changed_indices.ptr(), changed_indices.size(), GLTFAccessor::TYPE_SCALAR, sparse_accessor->sparse_indices_component_type, sparse_accessor->normalized, sparse_accessor->sparse_indices_byte_offset, false, buffer_view_i_indices) != OK) { return -1; @@ -2194,7 +2310,7 @@ GLTFAccessorIndex GLTFDocument::_encode_accessor_as_xform(Ref p_state } int64_t size = p_state->buffers[0].size(); const GLTFAccessor::GLTFAccessorType accessor_type = GLTFAccessor::TYPE_MAT4; - const int component_type = GLTFDocument::COMPONENT_TYPE_FLOAT; + const GLTFAccessor::GLTFComponentType component_type = GLTFAccessor::COMPONENT_TYPE_SINGLE_FLOAT; accessor->max = type_max; accessor->min = type_min; @@ -2343,6 +2459,325 @@ Vector GLTFDocument::_decode_accessor_as_xform(Ref p_sta return ret; } +Vector GLTFDocument::_decode_accessor_as_variant(Ref p_state, const GLTFAccessorIndex p_accessor, Variant::Type p_variant_type, GLTFAccessor::GLTFAccessorType p_accessor_type) { + const Vector attribs = _decode_accessor(p_state, p_accessor, false); + Vector ret; + ERR_FAIL_COND_V_MSG(attribs.is_empty(), ret, "glTF: The accessor was empty."); + const int component_count = COMPONENT_COUNT_FOR_ACCESSOR_TYPE[p_accessor_type]; + ERR_FAIL_COND_V_MSG(attribs.size() % component_count != 0, ret, "glTF: The accessor size was not a multiple of the component count."); + const int ret_size = attribs.size() / component_count; + ret.resize(ret_size); + for (int i = 0; i < ret_size; i++) { + switch (p_variant_type) { + case Variant::BOOL: { + ret.write[i] = attribs[i * component_count] != 0.0; + } break; + case Variant::INT: { + ret.write[i] = (int64_t)attribs[i * component_count]; + } break; + case Variant::FLOAT: { + ret.write[i] = attribs[i * component_count]; + } break; + case Variant::VECTOR2: + case Variant::RECT2: + case Variant::VECTOR3: + case Variant::VECTOR4: + case Variant::PLANE: + case Variant::QUATERNION: { + // General-purpose code for importing glTF accessor data with any component count into structs up to 4 `real_t`s in size. + Variant v; + switch (component_count) { + case 1: { + v = Vector4(attribs[i * component_count], 0.0f, 0.0f, 0.0f); + } break; + case 2: { + v = Vector4(attribs[i * component_count], attribs[i * component_count + 1], 0.0f, 0.0f); + } break; + case 3: { + v = Vector4(attribs[i * component_count], attribs[i * component_count + 1], attribs[i * component_count + 2], 0.0f); + } break; + default: { + v = Vector4(attribs[i * component_count], attribs[i * component_count + 1], attribs[i * component_count + 2], attribs[i * component_count + 3]); + } break; + } + // Evil hack that relies on the structure of Variant, but it's the + // only way to accomplish this without a ton of code duplication. + *(Variant::Type *)&v = p_variant_type; + ret.write[i] = v; + } break; + case Variant::VECTOR2I: + case Variant::RECT2I: + case Variant::VECTOR3I: + case Variant::VECTOR4I: { + // General-purpose code for importing glTF accessor data with any component count into structs up to 4 `int32_t`s in size. + Variant v; + switch (component_count) { + case 1: { + v = Vector4i((int32_t)attribs[i * component_count], 0, 0, 0); + } break; + case 2: { + v = Vector4i((int32_t)attribs[i * component_count], (int32_t)attribs[i * component_count + 1], 0, 0); + } break; + case 3: { + v = Vector4i((int32_t)attribs[i * component_count], (int32_t)attribs[i * component_count + 1], (int32_t)attribs[i * component_count + 2], 0); + } break; + default: { + v = Vector4i((int32_t)attribs[i * component_count], (int32_t)attribs[i * component_count + 1], (int32_t)attribs[i * component_count + 2], (int32_t)attribs[i * component_count + 3]); + } break; + } + // Evil hack that relies on the structure of Variant, but it's the + // only way to accomplish this without a ton of code duplication. + *(Variant::Type *)&v = p_variant_type; + ret.write[i] = v; + } break; + // No more generalized hacks, each of the below types needs a lot of repetitive code. + case Variant::COLOR: { + Variant v; + switch (component_count) { + case 1: { + v = Color(attribs[i * component_count], 0.0f, 0.0f, 1.0f); + } break; + case 2: { + v = Color(attribs[i * component_count], attribs[i * component_count + 1], 0.0f, 1.0f); + } break; + case 3: { + v = Color(attribs[i * component_count], attribs[i * component_count + 1], attribs[i * component_count + 2], 1.0f); + } break; + default: { + v = Color(attribs[i * component_count], attribs[i * component_count + 1], attribs[i * component_count + 2], attribs[i * component_count + 3]); + } break; + } + ret.write[i] = v; + } break; + case Variant::TRANSFORM2D: { + Transform2D t; + switch (component_count) { + case 4: { + t.columns[0] = Vector2(attribs[i * component_count + 0], attribs[i * component_count + 1]); + t.columns[1] = Vector2(attribs[i * component_count + 2], attribs[i * component_count + 3]); + } break; + case 9: { + t.columns[0] = Vector2(attribs[i * component_count + 0], attribs[i * component_count + 1]); + t.columns[1] = Vector2(attribs[i * component_count + 3], attribs[i * component_count + 4]); + t.columns[2] = Vector2(attribs[i * component_count + 6], attribs[i * component_count + 7]); + } break; + case 16: { + t.columns[0] = Vector2(attribs[i * component_count + 0], attribs[i * component_count + 1]); + t.columns[1] = Vector2(attribs[i * component_count + 4], attribs[i * component_count + 5]); + t.columns[2] = Vector2(attribs[i * component_count + 12], attribs[i * component_count + 13]); + } break; + } + ret.write[i] = t; + } break; + case Variant::BASIS: { + Basis b; + switch (component_count) { + case 4: { + b.rows[0] = Vector3(attribs[i * component_count + 0], attribs[i * component_count + 2], 0.0f); + b.rows[1] = Vector3(attribs[i * component_count + 1], attribs[i * component_count + 3], 0.0f); + } break; + case 9: { + b.rows[0] = Vector3(attribs[i * component_count + 0], attribs[i * component_count + 3], attribs[i * component_count + 6]); + b.rows[1] = Vector3(attribs[i * component_count + 1], attribs[i * component_count + 4], attribs[i * component_count + 7]); + b.rows[2] = Vector3(attribs[i * component_count + 2], attribs[i * component_count + 5], attribs[i * component_count + 8]); + } break; + case 16: { + b.rows[0] = Vector3(attribs[i * component_count + 0], attribs[i * component_count + 4], attribs[i * component_count + 8]); + b.rows[1] = Vector3(attribs[i * component_count + 1], attribs[i * component_count + 5], attribs[i * component_count + 9]); + b.rows[2] = Vector3(attribs[i * component_count + 2], attribs[i * component_count + 6], attribs[i * component_count + 10]); + } break; + } + ret.write[i] = b; + } break; + case Variant::TRANSFORM3D: { + Transform3D t; + switch (component_count) { + case 4: { + t.basis.rows[0] = Vector3(attribs[i * component_count + 0], attribs[i * component_count + 2], 0.0f); + t.basis.rows[1] = Vector3(attribs[i * component_count + 1], attribs[i * component_count + 3], 0.0f); + } break; + case 9: { + t.basis.rows[0] = Vector3(attribs[i * component_count + 0], attribs[i * component_count + 3], attribs[i * component_count + 6]); + t.basis.rows[1] = Vector3(attribs[i * component_count + 1], attribs[i * component_count + 4], attribs[i * component_count + 7]); + t.basis.rows[2] = Vector3(attribs[i * component_count + 2], attribs[i * component_count + 5], attribs[i * component_count + 8]); + } break; + case 16: { + t.basis.rows[0] = Vector3(attribs[i * component_count + 0], attribs[i * component_count + 4], attribs[i * component_count + 8]); + t.basis.rows[1] = Vector3(attribs[i * component_count + 1], attribs[i * component_count + 5], attribs[i * component_count + 9]); + t.basis.rows[2] = Vector3(attribs[i * component_count + 2], attribs[i * component_count + 6], attribs[i * component_count + 10]); + t.origin = Vector3(attribs[i * component_count + 12], attribs[i * component_count + 13], attribs[i * component_count + 14]); + } break; + } + ret.write[i] = t; + } break; + case Variant::PROJECTION: { + Projection p; + switch (component_count) { + case 4: { + p.columns[0] = Vector4(attribs[i * component_count + 0], attribs[i * component_count + 1], 0.0f, 0.0f); + p.columns[1] = Vector4(attribs[i * component_count + 4], attribs[i * component_count + 5], 0.0f, 0.0f); + } break; + case 9: { + p.columns[0] = Vector4(attribs[i * component_count + 0], attribs[i * component_count + 1], attribs[i * component_count + 2], 0.0f); + p.columns[1] = Vector4(attribs[i * component_count + 4], attribs[i * component_count + 5], attribs[i * component_count + 6], 0.0f); + p.columns[2] = Vector4(attribs[i * component_count + 8], attribs[i * component_count + 9], attribs[i * component_count + 10], 0.0f); + } break; + case 16: { + p.columns[0] = Vector4(attribs[i * component_count + 0], attribs[i * component_count + 1], attribs[i * component_count + 2], attribs[i * component_count + 3]); + p.columns[1] = Vector4(attribs[i * component_count + 4], attribs[i * component_count + 5], attribs[i * component_count + 6], attribs[i * component_count + 7]); + p.columns[2] = Vector4(attribs[i * component_count + 8], attribs[i * component_count + 9], attribs[i * component_count + 10], attribs[i * component_count + 11]); + p.columns[3] = Vector4(attribs[i * component_count + 12], attribs[i * component_count + 13], attribs[i * component_count + 14], attribs[i * component_count + 15]); + } break; + } + ret.write[i] = p; + } break; + default: { + ERR_FAIL_V_MSG(ret, "glTF: Cannot decode accessor as Variant of type " + Variant::get_type_name(p_variant_type) + "."); + } + } + } + return ret; +} + +GLTFAccessorIndex GLTFDocument::_encode_accessor_as_variant(Ref p_state, Vector p_attribs, Variant::Type p_variant_type, GLTFAccessor::GLTFAccessorType p_accessor_type, GLTFAccessor::GLTFComponentType p_component_type) { + const int accessor_component_count = COMPONENT_COUNT_FOR_ACCESSOR_TYPE[p_accessor_type]; + Vector encoded_attribs; + for (const Variant &v : p_attribs) { + switch (p_variant_type) { + case Variant::NIL: + case Variant::BOOL: + case Variant::INT: + case Variant::FLOAT: { + // For scalar values, just append them. Variant can convert all of these to double. Some padding may also be needed. + encoded_attribs.append(v); + if (unlikely(accessor_component_count > 1)) { + for (int i = 1; i < accessor_component_count; i++) { + encoded_attribs.append(0.0); + } + } + } break; + case Variant::VECTOR2: + case Variant::VECTOR2I: + case Variant::VECTOR3: + case Variant::VECTOR3I: + case Variant::VECTOR4: + case Variant::VECTOR4I: { + // Variant can handle converting Vector2/2i/3/3i/4/4i to Vector4 for us. + Vector4 vec = v; + if (likely(accessor_component_count < 5)) { + for (int i = 0; i < accessor_component_count; i++) { + encoded_attribs.append(vec[i]); + } + } + } break; + case Variant::PLANE: { + Plane p = v; + if (likely(accessor_component_count == 4)) { + encoded_attribs.append(p.normal.x); + encoded_attribs.append(p.normal.y); + encoded_attribs.append(p.normal.z); + encoded_attribs.append(p.d); + } + } break; + case Variant::QUATERNION: { + Quaternion q = v; + if (likely(accessor_component_count < 5)) { + for (int i = 0; i < accessor_component_count; i++) { + encoded_attribs.append(q[i]); + } + } + } break; + case Variant::COLOR: { + Color c = v; + if (likely(accessor_component_count < 5)) { + for (int i = 0; i < accessor_component_count; i++) { + encoded_attribs.append(c[i]); + } + } + } break; + case Variant::RECT2: + case Variant::RECT2I: { + // Variant can handle converting Rect2i to Rect2 for us. + Rect2 r = v; + if (likely(accessor_component_count == 4)) { + encoded_attribs.append(r.position.x); + encoded_attribs.append(r.position.y); + encoded_attribs.append(r.size.x); + encoded_attribs.append(r.size.y); + } + } break; + case Variant::TRANSFORM2D: + case Variant::BASIS: + case Variant::TRANSFORM3D: + case Variant::PROJECTION: { + // Variant can handle converting Transform2D/Transform3D/Basis to Projection for us. + Projection p = v; + if (accessor_component_count == 16) { + for (int i = 0; i < 4; i++) { + encoded_attribs.append(p.columns[i][0]); + encoded_attribs.append(p.columns[i][1]); + encoded_attribs.append(p.columns[i][2]); + encoded_attribs.append(p.columns[i][3]); + } + } else if (accessor_component_count == 9) { + for (int i = 0; i < 3; i++) { + encoded_attribs.append(p.columns[i][0]); + encoded_attribs.append(p.columns[i][1]); + encoded_attribs.append(p.columns[i][2]); + } + } else if (accessor_component_count == 4) { + encoded_attribs.append(p.columns[0][0]); + encoded_attribs.append(p.columns[0][1]); + encoded_attribs.append(p.columns[1][0]); + encoded_attribs.append(p.columns[1][1]); + } + } break; + default: { + ERR_FAIL_V_MSG(-1, "glTF: Cannot encode accessor from Variant of type " + Variant::get_type_name(p_variant_type) + "."); + } + } + } + // Determine the min and max values for the accessor. + Vector type_max; + type_max.resize(accessor_component_count); + Vector type_min; + type_min.resize(accessor_component_count); + for (int i = 0; i < encoded_attribs.size(); i++) { + if (Math::is_zero_approx(encoded_attribs[i])) { + encoded_attribs.write[i] = 0.0; + } else { + encoded_attribs.write[i] = _filter_number(encoded_attribs[i]); + } + } + for (int i = 0; i < p_attribs.size(); i++) { + _calc_accessor_min_max(i, accessor_component_count, type_max, encoded_attribs, type_min); + } + _round_min_max_components(type_min, type_max); + // Encode the data in a buffer view. + GLTFBufferIndex buffer_view_index = 0; + if (p_state->buffers.is_empty()) { + p_state->buffers.push_back(Vector()); + } + const int64_t buffer_size = p_state->buffers[buffer_view_index].size(); + Error err = _encode_buffer_view(p_state, encoded_attribs.ptr(), p_attribs.size(), p_accessor_type, p_component_type, false, buffer_size, false, buffer_view_index); + if (err != OK) { + return -1; + } + // Create the accessor and fill it with the data. + Ref accessor; + accessor.instantiate(); + accessor->max = type_max; + accessor->min = type_min; + accessor->count = p_attribs.size(); + accessor->accessor_type = p_accessor_type; + accessor->component_type = p_component_type; + accessor->byte_offset = 0; + accessor->buffer_view = buffer_view_index; + const GLTFAccessorIndex new_accessor_index = p_state->accessors.size(); + p_state->accessors.push_back(accessor); + return new_accessor_index; +} + Error GLTFDocument::_serialize_meshes(Ref p_state) { Array meshes; for (GLTFMeshIndex gltf_mesh_i = 0; gltf_mesh_i < p_state->meshes.size(); gltf_mesh_i++) { @@ -2778,41 +3213,42 @@ Error GLTFDocument::_parse_meshes(Ref p_state) { Array meshes = p_state->json["meshes"]; for (GLTFMeshIndex i = 0; i < meshes.size(); i++) { print_verbose("glTF: Parsing mesh: " + itos(i)); - Dictionary d = meshes[i]; + Dictionary mesh_dict = meshes[i]; Ref mesh; mesh.instantiate(); bool has_vertex_color = false; - ERR_FAIL_COND_V(!d.has("primitives"), ERR_PARSE_ERROR); + ERR_FAIL_COND_V(!mesh_dict.has("primitives"), ERR_PARSE_ERROR); - Array primitives = d["primitives"]; - const Dictionary &extras = d.has("extras") ? (Dictionary)d["extras"] : Dictionary(); + Array primitives = mesh_dict["primitives"]; + const Dictionary &extras = mesh_dict.has("extras") ? (Dictionary)mesh_dict["extras"] : Dictionary(); _attach_extras_to_meta(extras, mesh); Ref import_mesh; import_mesh.instantiate(); String mesh_name = "mesh"; - if (d.has("name") && !String(d["name"]).is_empty()) { - mesh_name = d["name"]; + if (mesh_dict.has("name") && !String(mesh_dict["name"]).is_empty()) { + mesh_name = mesh_dict["name"]; mesh->set_original_name(mesh_name); } import_mesh->set_name(_gen_unique_name(p_state, vformat("%s_%s", p_state->scene_name, mesh_name))); mesh->set_name(import_mesh->get_name()); + TypedArray instance_materials; for (int j = 0; j < primitives.size(); j++) { uint64_t flags = RS::ARRAY_FLAG_COMPRESS_ATTRIBUTES; - Dictionary p = primitives[j]; + Dictionary mesh_prim = primitives[j]; Array array; array.resize(Mesh::ARRAY_MAX); - ERR_FAIL_COND_V(!p.has("attributes"), ERR_PARSE_ERROR); + ERR_FAIL_COND_V(!mesh_prim.has("attributes"), ERR_PARSE_ERROR); - Dictionary a = p["attributes"]; + Dictionary a = mesh_prim["attributes"]; Mesh::PrimitiveType primitive = Mesh::PRIMITIVE_TRIANGLES; - if (p.has("mode")) { - const int mode = p["mode"]; + if (mesh_prim.has("mode")) { + const int mode = mesh_prim["mode"]; ERR_FAIL_INDEX_V(mode, 7, ERR_FILE_CORRUPT); // Convert mesh.primitive.mode to Godot Mesh enum. See: // https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#_mesh_primitive_mode @@ -2843,8 +3279,8 @@ Error GLTFDocument::_parse_meshes(Ref p_state) { Vector indices_mapping; Vector indices_rev_mapping; Vector indices_vec4_mapping; - if (p.has("indices")) { - indices = _decode_accessor_as_ints(p_state, p["indices"], false); + if (mesh_prim.has("indices")) { + indices = _decode_accessor_as_ints(p_state, mesh_prim["indices"], false); const int is = indices.size(); if (primitive == Mesh::PRIMITIVE_TRIANGLES) { @@ -3102,7 +3538,7 @@ Error GLTFDocument::_parse_meshes(Ref p_state) { } } - if (p_state->force_disable_compression || is_mesh_2d || !a.has("POSITION") || !a.has("NORMAL") || p.has("targets") || (a.has("JOINTS_0") || a.has("JOINTS_1"))) { + if (p_state->force_disable_compression || is_mesh_2d || !a.has("POSITION") || !a.has("NORMAL") || mesh_prim.has("targets") || (a.has("JOINTS_0") || a.has("JOINTS_1"))) { flags &= ~RS::ARRAY_FLAG_COMPRESS_ATTRIBUTES; } @@ -3134,9 +3570,9 @@ Error GLTFDocument::_parse_meshes(Ref p_state) { Array morphs; // Blend shapes - if (p.has("targets")) { + if (mesh_prim.has("targets")) { print_verbose("glTF: Mesh has targets"); - const Array &targets = p["targets"]; + const Array &targets = mesh_prim["targets"]; import_mesh->set_blend_shape_mode(Mesh::BLEND_SHAPE_MODE_NORMALIZED); @@ -3267,8 +3703,8 @@ Error GLTFDocument::_parse_meshes(Ref p_state) { Ref mat; String mat_name; if (!p_state->discard_meshes_and_materials) { - if (p.has("material")) { - const int material = p["material"]; + if (mesh_prim.has("material")) { + const int material = mesh_prim["material"]; ERR_FAIL_INDEX_V(material, p_state->materials.size(), ERR_FILE_CORRUPT); Ref mat3d = p_state->materials[material]; ERR_FAIL_COND_V(mat3d.is_null(), ERR_FILE_CORRUPT); @@ -3288,6 +3724,7 @@ Error GLTFDocument::_parse_meshes(Ref p_state) { mat = mat3d; } ERR_FAIL_COND_V(mat.is_null(), ERR_FILE_CORRUPT); + instance_materials.append(mat); mat_name = mat->get_name(); } import_mesh->add_surface(primitive, array, morphs, @@ -3300,8 +3737,8 @@ Error GLTFDocument::_parse_meshes(Ref p_state) { blend_weights.write[weight_i] = 0.0f; } - if (d.has("weights")) { - const Array &weights = d["weights"]; + if (mesh_dict.has("weights")) { + const Array &weights = mesh_dict["weights"]; for (int j = 0; j < weights.size(); j++) { if (j >= blend_weights.size()) { break; @@ -3310,6 +3747,7 @@ Error GLTFDocument::_parse_meshes(Ref p_state) { } } mesh->set_blend_weights(blend_weights); + mesh->set_instance_materials(instance_materials); mesh->set_mesh(import_mesh); p_state->meshes.push_back(mesh); @@ -4782,7 +5220,7 @@ Error GLTFDocument::_serialize_animations(Ref p_state) { for (GLTFAnimationIndex animation_i = 0; animation_i < p_state->animations.size(); animation_i++) { Dictionary d; Ref gltf_animation = p_state->animations[animation_i]; - if (!gltf_animation->get_tracks().size()) { + if (gltf_animation->is_empty_of_tracks()) { continue; } @@ -4791,18 +5229,18 @@ Error GLTFDocument::_serialize_animations(Ref p_state) { } Array channels; Array samplers; - - for (KeyValue &track_i : gltf_animation->get_tracks()) { - GLTFAnimation::Track track = track_i.value; + // Serialize glTF node tracks with the vanilla glTF animation system. + for (KeyValue &track_i : gltf_animation->get_node_tracks()) { + GLTFAnimation::NodeTrack track = track_i.value; if (track.position_track.times.size()) { Dictionary t; t["sampler"] = samplers.size(); Dictionary s; s["interpolation"] = interpolation_to_string(track.position_track.interpolation); - Vector times = Variant(track.position_track.times); + Vector times = track.position_track.times; s["input"] = _encode_accessor_as_floats(p_state, times, false); - Vector values = Variant(track.position_track.values); + Vector values = track.position_track.values; s["output"] = _encode_accessor_as_vec3(p_state, values, false); samplers.push_back(s); @@ -4820,7 +5258,7 @@ Error GLTFDocument::_serialize_animations(Ref p_state) { Dictionary s; s["interpolation"] = interpolation_to_string(track.rotation_track.interpolation); - Vector times = Variant(track.rotation_track.times); + Vector times = track.rotation_track.times; s["input"] = _encode_accessor_as_floats(p_state, times, false); Vector values = track.rotation_track.values; s["output"] = _encode_accessor_as_quaternions(p_state, values, false); @@ -4840,9 +5278,9 @@ Error GLTFDocument::_serialize_animations(Ref p_state) { Dictionary s; s["interpolation"] = interpolation_to_string(track.scale_track.interpolation); - Vector times = Variant(track.scale_track.times); + Vector times = track.scale_track.times; s["input"] = _encode_accessor_as_floats(p_state, times, false); - Vector values = Variant(track.scale_track.values); + Vector values = track.scale_track.values; s["output"] = _encode_accessor_as_vec3(p_state, values, false); samplers.push_back(s); @@ -4865,7 +5303,7 @@ Error GLTFDocument::_serialize_animations(Ref p_state) { Dictionary t; t["sampler"] = samplers.size(); Dictionary s; - Vector times; + Vector times; const double increment = 1.0 / p_state->get_bake_fps(); { double time = 0.0; @@ -4906,8 +5344,8 @@ Error GLTFDocument::_serialize_animations(Ref p_state) { track.weight_tracks.write[track_idx].values = weight_track; } - Vector all_track_times = times; - Vector all_track_values; + Vector all_track_times = times; + Vector all_track_values; int32_t values_size = track.weight_tracks[0].values.size(); int32_t weight_tracks_size = track.weight_tracks.size(); all_track_values.resize(weight_tracks_size * values_size); @@ -4934,6 +5372,33 @@ Error GLTFDocument::_serialize_animations(Ref 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> &pointer_track_iter : gltf_animation->get_pointer_tracks()) { + const String &json_pointer = pointer_track_iter.key; + const GLTFAnimation::Channel &pointer_track = pointer_track_iter.value; + const Ref &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; @@ -4958,21 +5423,21 @@ Error GLTFDocument::_parse_animations(Ref p_state) { const Array &animations = p_state->json["animations"]; - for (GLTFAnimationIndex i = 0; i < animations.size(); i++) { - const Dictionary &d = animations[i]; + for (GLTFAnimationIndex anim_index = 0; anim_index < animations.size(); anim_index++) { + const Dictionary &anim_dict = animations[anim_index]; Ref animation; animation.instantiate(); - if (!d.has("channels") || !d.has("samplers")) { + if (!anim_dict.has("channels") || !anim_dict.has("samplers")) { continue; } - Array channels = d["channels"]; - Array samplers = d["samplers"]; + Array channels = anim_dict["channels"]; + Array samplers = anim_dict["samplers"]; - if (d.has("name")) { - const String anim_name = d["name"]; + if (anim_dict.has("name")) { + const String anim_name = anim_dict["name"]; const String anim_name_lower = anim_name.to_lower(); if (anim_name_lower.begins_with("loop") || anim_name_lower.ends_with("loop") || anim_name_lower.begins_with("cycle") || anim_name_lower.ends_with("cycle")) { animation->set_loop(true); @@ -4981,46 +5446,22 @@ Error GLTFDocument::_parse_animations(Ref p_state) { animation->set_name(_gen_unique_animation_name(p_state, anim_name)); } - for (int j = 0; j < channels.size(); j++) { - const Dictionary &c = channels[j]; - if (!c.has("target")) { - continue; - } - - const Dictionary &t = c["target"]; - if (!t.has("node") || !t.has("path")) { - continue; - } - - ERR_FAIL_COND_V(!c.has("sampler"), ERR_PARSE_ERROR); - const int sampler = c["sampler"]; - ERR_FAIL_INDEX_V(sampler, samplers.size(), ERR_PARSE_ERROR); - - GLTFNodeIndex node = t["node"]; - String path = t["path"]; - - ERR_FAIL_INDEX_V(node, p_state->nodes.size(), ERR_PARSE_ERROR); - - GLTFAnimation::Track *track = nullptr; - - if (!animation->get_tracks().has(node)) { - animation->get_tracks()[node] = GLTFAnimation::Track(); - } - - track = &animation->get_tracks()[node]; - - const Dictionary &s = samplers[sampler]; - - ERR_FAIL_COND_V(!s.has("input"), ERR_PARSE_ERROR); - ERR_FAIL_COND_V(!s.has("output"), ERR_PARSE_ERROR); - - const int input = s["input"]; - const int output = s["output"]; - + for (int channel_index = 0; channel_index < channels.size(); channel_index++) { + const Dictionary &anim_channel = channels[channel_index]; + ERR_FAIL_COND_V_MSG(!anim_channel.has("sampler"), ERR_PARSE_ERROR, "glTF: Animation channel missing required 'sampler' property."); + ERR_FAIL_COND_V_MSG(!anim_channel.has("target"), ERR_PARSE_ERROR, "glTF: Animation channel missing required 'target' property."); + // Parse sampler. + const int sampler_index = anim_channel["sampler"]; + ERR_FAIL_INDEX_V(sampler_index, samplers.size(), ERR_PARSE_ERROR); + const Dictionary &sampler_dict = samplers[sampler_index]; + ERR_FAIL_COND_V(!sampler_dict.has("input"), ERR_PARSE_ERROR); + ERR_FAIL_COND_V(!sampler_dict.has("output"), ERR_PARSE_ERROR); + const int input_time_accessor_index = sampler_dict["input"]; + const int output_value_accessor_index = sampler_dict["output"]; GLTFAnimation::Interpolation interp = GLTFAnimation::INTERP_LINEAR; int output_count = 1; - if (s.has("interpolation")) { - const String &in = s["interpolation"]; + if (sampler_dict.has("interpolation")) { + const String &in = sampler_dict["interpolation"]; if (in == "STEP") { interp = GLTFAnimation::INTERP_STEP; } else if (in == "LINEAR") { @@ -5033,52 +5474,83 @@ Error GLTFDocument::_parse_animations(Ref p_state) { output_count = 3; } } - - const Vector times = _decode_accessor_as_floats(p_state, input, false); - if (path == "translation") { - const Vector positions = _decode_accessor_as_vec3(p_state, output, false); - track->position_track.interpolation = interp; - track->position_track.times = Variant(times); //convert via variant - track->position_track.values = Variant(positions); //convert via variant - } else if (path == "rotation") { - const Vector rotations = _decode_accessor_as_quaternion(p_state, output, false); - track->rotation_track.interpolation = interp; - track->rotation_track.times = Variant(times); //convert via variant - track->rotation_track.values = rotations; - } else if (path == "scale") { - const Vector scales = _decode_accessor_as_vec3(p_state, output, false); - track->scale_track.interpolation = interp; - track->scale_track.times = Variant(times); //convert via variant - track->scale_track.values = Variant(scales); //convert via variant - } else if (path == "weights") { - const Vector weights = _decode_accessor_as_floats(p_state, output, false); - - ERR_FAIL_INDEX_V(p_state->nodes[node]->mesh, p_state->meshes.size(), ERR_PARSE_ERROR); - Ref mesh = p_state->meshes[p_state->nodes[node]->mesh]; - ERR_CONTINUE(!mesh->get_blend_weights().size()); - const int wc = mesh->get_blend_weights().size(); - - track->weight_tracks.resize(wc); - - const int expected_value_count = times.size() * output_count * wc; - ERR_CONTINUE_MSG(weights.size() != expected_value_count, "Invalid weight data, expected " + itos(expected_value_count) + " weight values, got " + itos(weights.size()) + " instead."); - - const int wlen = weights.size() / wc; - for (int k = 0; k < wc; k++) { //separate tracks, having them together is not such a good idea - GLTFAnimation::Channel cf; - cf.interpolation = interp; - cf.times = Variant(times); - Vector wdata; - wdata.resize(wlen); - for (int l = 0; l < wlen; l++) { - wdata.write[l] = weights[l * wc + k]; - } - - cf.values = wdata; - track->weight_tracks.write[k] = cf; - } + const Vector times = _decode_accessor(p_state, input_time_accessor_index, false); + // Parse target. + 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 { - WARN_PRINT("Invalid path '" + path + "'."); + // 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; + } + + GLTFNodeIndex node = anim_target["node"]; + + ERR_FAIL_INDEX_V(node, p_state->nodes.size(), ERR_PARSE_ERROR); + + GLTFAnimation::NodeTrack *track = nullptr; + + if (!animation->get_node_tracks().has(node)) { + animation->get_node_tracks()[node] = GLTFAnimation::NodeTrack(); + } + + track = &animation->get_node_tracks()[node]; + + if (path == "translation") { + const Vector positions = _decode_accessor_as_vec3(p_state, output_value_accessor_index, false); + track->position_track.interpolation = interp; + track->position_track.times = times; + track->position_track.values = positions; + } else if (path == "rotation") { + const Vector rotations = _decode_accessor_as_quaternion(p_state, output_value_accessor_index, false); + track->rotation_track.interpolation = interp; + track->rotation_track.times = times; + track->rotation_track.values = rotations; + } else if (path == "scale") { + const Vector scales = _decode_accessor_as_vec3(p_state, output_value_accessor_index, false); + track->scale_track.interpolation = interp; + track->scale_track.times = times; + track->scale_track.values = scales; + } else if (path == "weights") { + const Vector weights = _decode_accessor_as_floats(p_state, output_value_accessor_index, false); + + ERR_FAIL_INDEX_V(p_state->nodes[node]->mesh, p_state->meshes.size(), ERR_PARSE_ERROR); + Ref mesh = p_state->meshes[p_state->nodes[node]->mesh]; + const int wc = mesh->get_blend_weights().size(); + ERR_CONTINUE_MSG(wc == 0, "glTF: Animation tried to animate weights, but mesh has no weights."); + + track->weight_tracks.resize(wc); + + const int expected_value_count = times.size() * output_count * wc; + ERR_CONTINUE_MSG(weights.size() != expected_value_count, "Invalid weight data, expected " + itos(expected_value_count) + " weight values, got " + itos(weights.size()) + " instead."); + + const int wlen = weights.size() / wc; + for (int k = 0; k < wc; k++) { //separate tracks, having them together is not such a good idea + GLTFAnimation::Channel cf; + cf.interpolation = interp; + cf.times = Variant(times); + Vector wdata; + wdata.resize(wlen); + for (int l = 0; l < wlen; l++) { + wdata.write[l] = weights[l * wc + k]; + } + + cf.values = wdata; + track->weight_tracks.write[k] = cf; + } + } else { + WARN_PRINT("Invalid path '" + path + "'."); + } } } @@ -5090,6 +5562,96 @@ Error GLTFDocument::_parse_animations(Ref p_state) { return OK; } +void GLTFDocument::_parse_animation_pointer(Ref p_state, const String &p_animation_json_pointer, const Ref p_gltf_animation, const GLTFAnimation::Interpolation p_interp, const Vector &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 &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 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 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 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 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 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 gltf_mesh = p_state->meshes[mesh_index]; + const Vector &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 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 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> &anim_ptr_map = p_gltf_animation->get_pointer_tracks(); + GLTFAnimation::Channel 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 p_state) { for (int i = 0; i < p_state->nodes.size(); i++) { Ref gltf_node = p_state->nodes[i]; @@ -5255,46 +5817,46 @@ void GLTFDocument::_convert_scene_node(Ref p_state, Node *p_current, gltf_node->set_original_name(p_current->get_name()); gltf_node->set_name(_gen_unique_name(p_state, p_current->get_name())); gltf_node->merge_meta_from(p_current); - if (cast_to(p_current)) { - Node3D *spatial = cast_to(p_current); + if (Object::cast_to(p_current)) { + Node3D *spatial = Object::cast_to(p_current); _convert_spatial(p_state, spatial, gltf_node); } - if (cast_to(p_current)) { - MeshInstance3D *mi = cast_to(p_current); + if (Object::cast_to(p_current)) { + MeshInstance3D *mi = Object::cast_to(p_current); _convert_mesh_instance_to_gltf(mi, p_state, gltf_node); - } else if (cast_to(p_current)) { - BoneAttachment3D *bone = cast_to(p_current); + } else if (Object::cast_to(p_current)) { + BoneAttachment3D *bone = Object::cast_to(p_current); _convert_bone_attachment_to_gltf(bone, p_state, p_gltf_parent, p_gltf_root, gltf_node); return; - } else if (cast_to(p_current)) { - Skeleton3D *skel = cast_to(p_current); + } else if (Object::cast_to(p_current)) { + Skeleton3D *skel = Object::cast_to(p_current); _convert_skeleton_to_gltf(skel, p_state, p_gltf_parent, p_gltf_root, gltf_node); // We ignore the Godot Engine node that is the skeleton. return; - } else if (cast_to(p_current)) { - MultiMeshInstance3D *multi = cast_to(p_current); + } else if (Object::cast_to(p_current)) { + MultiMeshInstance3D *multi = Object::cast_to(p_current); _convert_multi_mesh_instance_to_gltf(multi, p_gltf_parent, p_gltf_root, gltf_node, p_state); #ifdef MODULE_CSG_ENABLED - } else if (cast_to(p_current)) { - CSGShape3D *shape = cast_to(p_current); + } else if (Object::cast_to(p_current)) { + CSGShape3D *shape = Object::cast_to(p_current); if (shape->get_parent() && shape->is_root_shape()) { _convert_csg_shape_to_gltf(shape, p_gltf_parent, gltf_node, p_state); } #endif // MODULE_CSG_ENABLED #ifdef MODULE_GRIDMAP_ENABLED - } else if (cast_to(p_current)) { + } else if (Object::cast_to(p_current)) { GridMap *gridmap = Object::cast_to(p_current); _convert_grid_map_to_gltf(gridmap, p_gltf_parent, p_gltf_root, gltf_node, p_state); #endif // MODULE_GRIDMAP_ENABLED - } else if (cast_to(p_current)) { + } else if (Object::cast_to(p_current)) { Camera3D *camera = Object::cast_to(p_current); _convert_camera_to_gltf(camera, p_state, gltf_node); - } else if (cast_to(p_current)) { + } else if (Object::cast_to(p_current)) { Light3D *light = Object::cast_to(p_current); _convert_light_to_gltf(light, p_state, gltf_node); - } else if (cast_to(p_current)) { + } else if (Object::cast_to(p_current)) { AnimationPlayer *animation_player = Object::cast_to(p_current); - _convert_animation_player_to_gltf(animation_player, p_state, p_gltf_parent, p_gltf_root, gltf_node, p_current); + p_state->animation_players.push_back(animation_player); } for (Ref ext : document_extensions) { ERR_CONTINUE(ext.is_null()); @@ -5372,12 +5934,6 @@ void GLTFDocument::_convert_csg_shape_to_gltf(CSGShape3D *p_current, GLTFNodeInd } #endif // MODULE_CSG_ENABLED -void GLTFDocument::_convert_animation_player_to_gltf(AnimationPlayer *p_animation_player, Ref p_state, GLTFNodeIndex p_gltf_current, GLTFNodeIndex p_gltf_root_index, Ref p_gltf_node, Node *p_scene_parent) { - ERR_FAIL_NULL(p_animation_player); - p_state->animation_players.push_back(p_animation_player); - print_verbose(String("glTF: Converting animation player: ") + p_animation_player->get_name()); -} - void GLTFDocument::_check_visibility(Node *p_node, bool &r_retflag) { r_retflag = true; Node3D *spatial = Object::cast_to(p_node); @@ -5570,9 +6126,9 @@ void GLTFDocument::_convert_bone_attachment_to_gltf(BoneAttachment3D *p_bone_att Skeleton3D *skeleton; // Note that relative transforms to external skeletons and pose overrides are not supported. if (p_bone_attachment->get_use_external_skeleton()) { - skeleton = cast_to(p_bone_attachment->get_node_or_null(p_bone_attachment->get_external_skeleton())); + skeleton = Object::cast_to(p_bone_attachment->get_node_or_null(p_bone_attachment->get_external_skeleton())); } else { - skeleton = cast_to(p_bone_attachment->get_parent()); + skeleton = Object::cast_to(p_bone_attachment->get_parent()); } GLTFSkeletonIndex skel_gltf_i = -1; if (skeleton != nullptr && p_state->skeleton3d_to_gltf_skeleton.has(skeleton->get_instance_id())) { @@ -5855,7 +6411,7 @@ struct SceneFormatImporterGLTFInterpolate { }; template -T GLTFDocument::_interpolate_track(const Vector &p_times, const Vector &p_values, const float p_time, const GLTFAnimation::Interpolation p_interp) { +T GLTFDocument::_interpolate_track(const Vector &p_times, const Vector &p_values, const float p_time, const GLTFAnimation::Interpolation p_interp) { ERR_FAIL_COND_V(p_values.is_empty(), T()); if (p_times.size() != (p_values.size() / (p_interp == GLTFAnimation::INTERP_CUBIC_SPLINE ? 3 : 1))) { ERR_PRINT_ONCE("The interpolated values are not corresponding to its times."); @@ -5926,8 +6482,433 @@ T GLTFDocument::_interpolate_track(const Vector &p_times, const Vector p_state, Ref p_material) { + int mesh_index = 0; + for (Ref gltf_mesh : p_state->meshes) { + TypedArray materials = gltf_mesh->get_instance_materials(); + for (int mat_index = 0; mat_index < materials.size(); mat_index++) { + if (materials[mat_index] == p_material) { + for (Ref gltf_node : p_state->nodes) { + if (gltf_node->mesh == mesh_index) { + NodePath node_path = gltf_node->get_scene_node_path(p_state); + // Example: MyNode:mesh:surface_0/material:albedo_color, so we want the mesh:surface_0/material part. + Vector subpath; + subpath.append("mesh"); + subpath.append("surface_" + itos(mat_index) + "/material"); + return NodePath(node_path.get_names(), subpath, false); + } + } + } + } + mesh_index++; + } + return NodePath(); +} + +Ref GLTFDocument::import_object_model_property(Ref p_state, const String &p_json_pointer) { + if (p_state->object_model_properties.has(p_json_pointer)) { + return p_state->object_model_properties[p_json_pointer]; + } + Ref ret; + // Split the JSON pointer into its components. + const PackedStringArray split = p_json_pointer.split("/", false); + ERR_FAIL_COND_V_MSG(split.size() < 3, ret, "glTF: Cannot use JSON pointer '" + p_json_pointer + "' because it does not contain enough elements. The only animatable properties are at least 3 levels deep (ex: '/nodes/0/translation' or '/materials/0/emissiveFactor')."); + ret.instantiate(); + ret->set_json_pointers({ split }); + // Partial paths are passed to GLTFDocumentExtension classes if GLTFDocument cannot handle a given JSON pointer. + TypedArray partial_paths; + // Note: This might not be an integer, but in that case, we don't use this value anyway. + const int top_level_index = split[1].to_int(); + // For JSON pointers present in the core glTF Object Model, hard-code them in GLTFDocument. + // https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/ObjectModel.adoc + if (split[0] == "nodes") { + ERR_FAIL_INDEX_V_MSG(top_level_index, p_state->nodes.size(), ret, vformat("glTF: Unable to find node %d for JSON pointer '%s'.", top_level_index, p_json_pointer)); + Ref pointed_gltf_node = p_state->nodes[top_level_index]; + NodePath node_path = pointed_gltf_node->get_scene_node_path(p_state); + partial_paths.append(node_path); + // Check if it's something we should be able to handle. + const String &node_prop = split[2]; + if (node_prop == "translation") { + ret->append_path_to_property(node_path, "position"); + ret->set_types(Variant::VECTOR3, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT3); + } else if (node_prop == "rotation") { + ret->append_path_to_property(node_path, "quaternion"); + ret->set_types(Variant::QUATERNION, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT4); + } else if (node_prop == "scale") { + ret->append_path_to_property(node_path, "scale"); + ret->set_types(Variant::VECTOR3, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT3); + } else if (node_prop == "matrix") { + ret->append_path_to_property(node_path, "transform"); + ret->set_types(Variant::TRANSFORM3D, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT4X4); + } else if (node_prop == "globalMatrix") { + ret->append_path_to_property(node_path, "global_transform"); + ret->set_types(Variant::TRANSFORM3D, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT4X4); + } else if (node_prop == "weights") { + if (split.size() > 3) { + const String &weight_index_string = split[3]; + ret->append_path_to_property(node_path, "blend_shapes/morph_" + weight_index_string); + ret->set_types(Variant::FLOAT, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT); + } + // Else, Godot's MeshInstance3D does not expose the blend shape weights as one property. + // But that's fine, we handle this case in _parse_animation_pointer instead. + } + } else if (split[0] == "cameras") { + const String &camera_prop = split[2]; + for (Ref gltf_node : p_state->nodes) { + if (gltf_node->camera == top_level_index) { + NodePath node_path = gltf_node->get_scene_node_path(p_state); + partial_paths.append(node_path); + // Check if it's something we should be able to handle. + if (camera_prop == "orthographic" || camera_prop == "perspective") { + ERR_FAIL_COND_V(split.size() < 4, ret); + ret->set_types(Variant::FLOAT, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT); + const String &sub_prop = split[3]; + if (sub_prop == "xmag" || sub_prop == "ymag") { + ret->append_path_to_property(node_path, "size"); + } else if (sub_prop == "yfov") { + ret->append_path_to_property(node_path, "fov"); + GLTFCamera::set_fov_conversion_expressions(ret); + } else if (sub_prop == "zfar") { + ret->append_path_to_property(node_path, "far"); + } else if (sub_prop == "znear") { + ret->append_path_to_property(node_path, "near"); + } + } + } + } + } else if (split[0] == "materials") { + ERR_FAIL_INDEX_V_MSG(top_level_index, p_state->materials.size(), ret, vformat("glTF: Unable to find material %d for JSON pointer '%s'.", top_level_index, p_json_pointer)); + Ref pointed_material = p_state->materials[top_level_index]; + NodePath mat_path = _find_material_node_path(p_state, pointed_material); + if (mat_path.is_empty()) { + WARN_PRINT(vformat("glTF: Unable to find a path to the material %d for JSON pointer '%s'. This is likely bad data but it's also possible this is intentional. Continuing anyway.", top_level_index, p_json_pointer)); + } else { + partial_paths.append(mat_path); + const String &mat_prop = split[2]; + if (mat_prop == "alphaCutoff") { + ret->append_path_to_property(mat_path, "alpha_scissor_threshold"); + ret->set_types(Variant::FLOAT, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT); + } else if (mat_prop == "emissiveFactor") { + ret->append_path_to_property(mat_path, "emission"); + ret->set_types(Variant::COLOR, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT3); + } else if (mat_prop == "extensions") { + ERR_FAIL_COND_V(split.size() < 5, ret); + const String &ext_name = split[3]; + const String &ext_prop = split[4]; + if (ext_name == "KHR_materials_emissive_strength" && ext_prop == "emissiveStrength") { + ret->append_path_to_property(mat_path, "emission_energy_multiplier"); + ret->set_types(Variant::FLOAT, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT); + } + } else { + ERR_FAIL_COND_V(split.size() < 4, ret); + const String &sub_prop = split[3]; + if (mat_prop == "normalTexture") { + if (sub_prop == "scale") { + ret->append_path_to_property(mat_path, "normal_scale"); + ret->set_types(Variant::FLOAT, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT); + } + } else if (mat_prop == "occlusionTexture") { + if (sub_prop == "strength") { + // This is the closest thing Godot has to an occlusion strength property. + ret->append_path_to_property(mat_path, "ao_light_affect"); + ret->set_types(Variant::FLOAT, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT); + } + } else if (mat_prop == "pbrMetallicRoughness") { + if (sub_prop == "baseColorFactor") { + ret->append_path_to_property(mat_path, "albedo_color"); + ret->set_types(Variant::COLOR, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT4); + } else if (sub_prop == "metallicFactor") { + ret->append_path_to_property(mat_path, "metallic"); + ret->set_types(Variant::FLOAT, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT); + } else if (sub_prop == "roughnessFactor") { + ret->append_path_to_property(mat_path, "roughness"); + ret->set_types(Variant::FLOAT, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT); + } else if (sub_prop == "baseColorTexture") { + ERR_FAIL_COND_V(split.size() < 6, ret); + const String &tex_ext_dict = split[4]; + const String &tex_ext_name = split[5]; + const String &tex_ext_prop = split[6]; + if (tex_ext_dict == "extensions" && tex_ext_name == "KHR_texture_transform") { + // Godot only supports UVs for the whole material, not per texture. + // We treat the albedo texture as the main texture, and import as UV1. + // Godot does not support texture rotation, only offset and scale. + if (tex_ext_prop == "offset") { + ret->append_path_to_property(mat_path, "uv1_offset"); + ret->set_types(Variant::VECTOR3, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT2); + } else if (tex_ext_prop == "scale") { + ret->append_path_to_property(mat_path, "uv1_scale"); + ret->set_types(Variant::VECTOR3, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT2); + } + } + } + } + } + } + } else if (split[0] == "meshes") { + for (Ref gltf_node : p_state->nodes) { + if (gltf_node->mesh == top_level_index) { + NodePath node_path = gltf_node->get_scene_node_path(p_state); + Vector subpath; + subpath.append("mesh"); + partial_paths.append(NodePath(node_path.get_names(), subpath, false)); + break; + } + } + } else if (split[0] == "extensions") { + if (split[1] == "KHR_lights_punctual" && split[2] == "lights" && split.size() > 4) { + const int light_index = split[3].to_int(); + ERR_FAIL_INDEX_V_MSG(light_index, p_state->lights.size(), ret, vformat("glTF: Unable to find light %d for JSON pointer '%s'.", light_index, p_json_pointer)); + const String &light_prop = split[4]; + const Ref pointed_light = p_state->lights[light_index]; + for (Ref gltf_node : p_state->nodes) { + if (gltf_node->light == light_index) { + NodePath node_path = gltf_node->get_scene_node_path(p_state); + partial_paths.append(node_path); + ret->set_types(Variant::FLOAT, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT); + // Check if it's something we should be able to handle. + if (light_prop == "color") { + ret->append_path_to_property(node_path, "light_color"); + ret->set_types(Variant::COLOR, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT3); + } else if (light_prop == "intensity") { + ret->append_path_to_property(node_path, "light_energy"); + } else if (light_prop == "range") { + const String &light_type = p_state->lights[light_index]->light_type; + if (light_type == "spot") { + ret->append_path_to_property(node_path, "spot_range"); + } else { + ret->append_path_to_property(node_path, "omni_range"); + } + } else if (light_prop == "spot") { + ERR_FAIL_COND_V(split.size() < 6, ret); + const String &sub_prop = split[5]; + if (sub_prop == "innerConeAngle") { + ret->append_path_to_property(node_path, "spot_angle_attenuation"); + GLTFLight::set_cone_inner_attenuation_conversion_expressions(ret); + } else if (sub_prop == "outerConeAngle") { + ret->append_path_to_property(node_path, "spot_angle"); + } + } + } + } + } + } + // Additional JSON pointers can be added by GLTFDocumentExtension classes. + // We only need this if no mapping has been found yet from GLTFDocument's internal code. + // When available, we pass the partial paths to the extension to help it generate the full path. + // For example, for `/nodes/3/extensions/MY_ext/prop`, we pass a NodePath that leads to node 3, + // so the GLTFDocumentExtension class only needs to resolve the last `MY_ext/prop` part of the path. + // It should check `split.size() > 4 and split[0] == "nodes" and split[2] == "extensions" and split[3] == "MY_ext"` + // at the start of the function to check if this JSON pointer applies to it, then it can handle `split[4]`. + if (!ret->has_node_paths()) { + for (Ref ext : all_document_extensions) { + ret = ext->import_object_model_property(p_state, split, partial_paths); + if (ret.is_valid() && ret->has_node_paths()) { + if (!ret->has_json_pointers()) { + ret->set_json_pointers({ split }); + } + break; + } + } + if (ret.is_null() || !ret->has_node_paths()) { + if (split.has("KHR_texture_transform")) { + WARN_VERBOSE(vformat("glTF: Texture transforms are only supported per material in Godot. All KHR_texture_transform properties will be ignored except for the albedo texture. Ignoring JSON pointer '%s'.", p_json_pointer)); + } else { + WARN_PRINT(vformat("glTF: Animation contained JSON pointer '%s' which could not be resolved. This property will not be animated.", p_json_pointer)); + } + } + } + p_state->object_model_properties[p_json_pointer] = ret; + return ret; +} + +Ref GLTFDocument::export_object_model_property(Ref p_state, const NodePath &p_node_path, const Node *p_godot_node, GLTFNodeIndex p_gltf_node_index) { + Ref ret; + const Object *target_object = p_godot_node; + const Vector subpath = p_node_path.get_subnames(); + ERR_FAIL_COND_V_MSG(subpath.is_empty(), ret, "glTF: Cannot export empty property. No property was specified in the NodePath: " + p_node_path); + int target_prop_depth = 0; + for (StringName subname : subpath) { + Variant target_property = target_object->get(subname); + if (target_property.get_type() == Variant::OBJECT) { + target_object = target_property; + if (target_object) { + target_prop_depth++; + continue; + } + } + break; + } + const String &target_prop = subpath[target_prop_depth]; + ret.instantiate(); + ret->set_node_paths({ p_node_path }); + Vector split_json_pointers; + PackedStringArray split_json_pointer; + if (Object::cast_to(target_object)) { + for (int i = 0; i < p_state->materials.size(); i++) { + if (p_state->materials[i].ptr() == target_object) { + split_json_pointer.append("materials"); + split_json_pointer.append(itos(i)); + if (target_prop == "alpha_scissor_threshold") { + split_json_pointer.append("alphaCutoff"); + ret->set_types(Variant::FLOAT, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT); + } else if (target_prop == "emission") { + split_json_pointer.append("emissiveFactor"); + ret->set_types(Variant::COLOR, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT3); + } else if (target_prop == "emission_energy_multiplier") { + split_json_pointer.append("extensions"); + split_json_pointer.append("KHR_materials_emissive_strength"); + split_json_pointer.append("emissiveStrength"); + ret->set_types(Variant::FLOAT, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT); + } else if (target_prop == "normal_scale") { + split_json_pointer.append("normalTexture"); + split_json_pointer.append("scale"); + ret->set_types(Variant::FLOAT, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT); + } else if (target_prop == "ao_light_affect") { + split_json_pointer.append("occlusionTexture"); + split_json_pointer.append("strength"); + ret->set_types(Variant::FLOAT, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT); + } else if (target_prop == "albedo_color") { + split_json_pointer.append("pbrMetallicRoughness"); + split_json_pointer.append("baseColorFactor"); + ret->set_types(Variant::COLOR, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT4); + } else if (target_prop == "metallic") { + split_json_pointer.append("pbrMetallicRoughness"); + split_json_pointer.append("metallicFactor"); + ret->set_types(Variant::FLOAT, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT); + } else if (target_prop == "roughness") { + split_json_pointer.append("pbrMetallicRoughness"); + split_json_pointer.append("roughnessFactor"); + ret->set_types(Variant::FLOAT, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT); + } else if (target_prop == "uv1_offset" || target_prop == "uv1_scale") { + split_json_pointer.append("pbrMetallicRoughness"); + split_json_pointer.append("baseColorTexture"); + split_json_pointer.append("extensions"); + split_json_pointer.append("KHR_texture_transform"); + if (target_prop == "uv1_offset") { + split_json_pointer.append("offset"); + } else { + split_json_pointer.append("scale"); + } + ret->set_types(Variant::VECTOR3, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT2); + } else { + split_json_pointer.clear(); + } + break; + } + } + } else { + // Properties directly on Godot nodes. + Ref gltf_node = p_state->nodes[p_gltf_node_index]; + if (Object::cast_to(target_object) && gltf_node->camera >= 0) { + split_json_pointer.append("cameras"); + split_json_pointer.append(itos(gltf_node->camera)); + const Camera3D *camera_node = Object::cast_to(target_object); + const Camera3D::ProjectionType projection_type = camera_node->get_projection(); + if (projection_type == Camera3D::PROJECTION_PERSPECTIVE) { + split_json_pointer.append("perspective"); + } else { + split_json_pointer.append("orthographic"); + } + ret->set_types(Variant::FLOAT, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT); + if (target_prop == "size") { + PackedStringArray xmag = split_json_pointer.duplicate(); + xmag.append("xmag"); + split_json_pointers.append(xmag); + split_json_pointer.append("ymag"); + } else if (target_prop == "fov") { + split_json_pointer.append("yfov"); + GLTFCamera::set_fov_conversion_expressions(ret); + } else if (target_prop == "far") { + split_json_pointer.append("zfar"); + } else if (target_prop == "near") { + split_json_pointer.append("znear"); + } else { + split_json_pointer.clear(); + } + } else if (Object::cast_to(target_object) && gltf_node->light >= 0) { + split_json_pointer.append("extensions"); + split_json_pointer.append("KHR_lights_punctual"); + split_json_pointer.append("lights"); + split_json_pointer.append(itos(gltf_node->light)); + ret->set_types(Variant::FLOAT, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT); + if (target_prop == "light_color") { + split_json_pointer.append("color"); + ret->set_types(Variant::COLOR, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT3); + } else if (target_prop == "light_energy") { + split_json_pointer.append("intensity"); + } else if (target_prop == "spot_range") { + split_json_pointer.append("range"); + } else if (target_prop == "omni_range") { + split_json_pointer.append("range"); + } else if (target_prop == "spot_angle") { + split_json_pointer.append("spot"); + split_json_pointer.append("outerConeAngle"); + } else if (target_prop == "spot_angle_attenuation") { + split_json_pointer.append("spot"); + split_json_pointer.append("innerConeAngle"); + GLTFLight::set_cone_inner_attenuation_conversion_expressions(ret); + } else { + split_json_pointer.clear(); + } + } else if (Object::cast_to(target_object) && target_prop.begins_with("blend_shapes/morph_")) { + const String &weight_index_string = target_prop.trim_prefix("blend_shapes/morph_"); + split_json_pointer.append("nodes"); + split_json_pointer.append(itos(p_gltf_node_index)); + split_json_pointer.append("weights"); + split_json_pointer.append(weight_index_string); + } + // Transform properties. Check for all 3D nodes if we haven't resolved the JSON pointer yet. + // Note: Do not put this in an `else`, because otherwise this will not be reached. + if (split_json_pointer.is_empty() && Object::cast_to(target_object)) { + split_json_pointer.append("nodes"); + split_json_pointer.append(itos(p_gltf_node_index)); + if (target_prop == "position") { + split_json_pointer.append("translation"); + ret->set_types(Variant::VECTOR3, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT3); + } else if (target_prop == "quaternion") { + // Note: Only Quaternion rotation can be converted from Godot in this mapping. + // Struct methods like from_euler are not accessible from the Expression class. :( + split_json_pointer.append("rotation"); + ret->set_types(Variant::QUATERNION, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT4); + } else if (target_prop == "scale") { + split_json_pointer.append("scale"); + ret->set_types(Variant::VECTOR3, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT3); + } else if (target_prop == "transform") { + split_json_pointer.append("matrix"); + ret->set_types(Variant::TRANSFORM3D, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT4X4); + } else if (target_prop == "global_transform") { + split_json_pointer.append("globalMatrix"); + ret->set_types(Variant::TRANSFORM3D, GLTFObjectModelProperty::GLTF_OBJECT_MODEL_TYPE_FLOAT4X4); + } else { + split_json_pointer.clear(); + } + } + } + // Additional JSON pointers can be added by GLTFDocumentExtension classes. + // We only need this if no mapping has been found yet from GLTFDocument's internal code. + // We pass as many pieces of information as we can to the extension to give it lots of context. + if (split_json_pointer.is_empty()) { + for (Ref ext : all_document_extensions) { + ret = ext->export_object_model_property(p_state, p_node_path, p_godot_node, p_gltf_node_index, target_object, target_prop_depth); + if (ret.is_valid() && ret->has_json_pointers()) { + if (!ret->has_node_paths()) { + ret->set_node_paths({ p_node_path }); + } + break; + } + } + } else { + // GLTFDocument's internal code found a mapping, so set it and return it. + split_json_pointers.append(split_json_pointer); + ret->set_json_pointers(split_json_pointers); + } + return ret; +} + void GLTFDocument::_import_animation(Ref p_state, AnimationPlayer *p_animation_player, const GLTFAnimationIndex p_index, const bool p_trimming, const bool p_remove_immutable_tracks) { ERR_FAIL_COND(p_state.is_null()); + Node *scene_root = p_animation_player->get_parent(); + ERR_FAIL_NULL(scene_root); Ref anim = p_state->animations[p_index]; String anim_name = anim->get_name(); @@ -5948,8 +6929,8 @@ void GLTFDocument::_import_animation(Ref p_state, AnimationPlayer *p_ double anim_start = p_trimming ? INFINITY : 0.0; double anim_end = 0.0; - for (const KeyValue &track_i : anim->get_tracks()) { - const GLTFAnimation::Track &track = track_i.value; + for (const KeyValue &track_i : anim->get_node_tracks()) { + const GLTFAnimation::NodeTrack &track = track_i.value; //need to find the path: for skeletons, weight tracks will affect the mesh NodePath node_path; //for skeletons, transform tracks always affect bones @@ -5961,14 +6942,12 @@ void GLTFDocument::_import_animation(Ref p_state, AnimationPlayer *p_ const Ref gltf_node = p_state->nodes[track_i.key]; - Node *root = p_animation_player->get_parent(); - ERR_FAIL_NULL(root); HashMap::Iterator node_element = p_state->scene_nodes.find(node_index); ERR_CONTINUE_MSG(!node_element, vformat("Unable to find node %d for animation.", node_index)); - node_path = root->get_path_to(node_element->value); + node_path = scene_root->get_path_to(node_element->value); HashMap::Iterator mesh_instance_element = p_state->scene_mesh_instances.find(node_index); if (mesh_instance_element) { - mesh_instance_node_path = root->get_path_to(mesh_instance_element->value); + mesh_instance_node_path = scene_root->get_path_to(mesh_instance_element->value); } else { mesh_instance_node_path = node_path; } @@ -6202,6 +7181,56 @@ void GLTFDocument::_import_animation(Ref p_state, AnimationPlayer *p_ } } + for (const KeyValue> &track_iter : anim->get_pointer_tracks()) { + // Determine the property to animate. + const String json_pointer = track_iter.key; + const Ref 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 &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 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; + Vector 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 library; @@ -6372,41 +7401,56 @@ void GLTFDocument::_process_mesh_instances(Ref p_state, Node *p_scene } } -GLTFAnimation::Track GLTFDocument::_convert_animation_track(Ref p_state, GLTFAnimation::Track p_track, Ref p_animation, int32_t p_track_i, GLTFNodeIndex p_node_i) { - Animation::InterpolationType interpolation = p_animation->track_get_interpolation_type(p_track_i); +GLTFNodeIndex GLTFDocument::_node_and_or_bone_to_gltf_node_index(Ref p_state, const Vector &p_node_subpath, const Node *p_godot_node) { + const Skeleton3D *skeleton = Object::cast_to(p_godot_node); + if (skeleton && p_node_subpath.size() == 1) { + // Special case: Handle skeleton bone TRS tracks. They use the format `A/B/C/Skeleton3D:bone_name`. + // We have a Skeleton3D, check if it has a bone with the same name as this subpath. + const String &bone_name = p_node_subpath[0]; + const int32_t bone_index = skeleton->find_bone(bone_name); + if (bone_index != -1) { + // A bone was found! But we still need to figure out which glTF node it corresponds to. + for (GLTFSkeletonIndex skeleton_i = 0; skeleton_i < p_state->skeletons.size(); skeleton_i++) { + const Ref &skeleton_gltf = p_state->skeletons[skeleton_i]; + if (skeleton == skeleton_gltf->godot_skeleton) { + GLTFNodeIndex node_i = skeleton_gltf->godot_bone_node[bone_index]; + return node_i; + } + } + ERR_FAIL_V_MSG(-1, vformat("glTF: Found a bone %s in a Skeleton3D that wasn't in the GLTFState. Ensure that all nodes referenced by the AnimationPlayer are in the scene you are exporting.", bone_name)); + } + } + // General case: Not a skeleton bone, usually this means a normal node, or it could be the Skeleton3D itself. + for (const KeyValue &scene_node_i : p_state->scene_nodes) { + if (scene_node_i.value == p_godot_node) { + return scene_node_i.key; + } + } + ERR_FAIL_V_MSG(-1, vformat("glTF: A node was animated, but it wasn't found in the GLTFState. Ensure that all nodes referenced by the AnimationPlayer are in the scene you are exporting.")); +} - GLTFAnimation::Interpolation gltf_interpolation = GLTFAnimation::INTERP_LINEAR; - if (interpolation == Animation::InterpolationType::INTERPOLATION_LINEAR) { - gltf_interpolation = GLTFAnimation::INTERP_LINEAR; - } else if (interpolation == Animation::InterpolationType::INTERPOLATION_NEAREST) { - gltf_interpolation = GLTFAnimation::INTERP_STEP; - } else if (interpolation == Animation::InterpolationType::INTERPOLATION_CUBIC) { - gltf_interpolation = GLTFAnimation::INTERP_CUBIC_SPLINE; - } - Animation::TrackType track_type = p_animation->track_get_type(p_track_i); - int32_t key_count = p_animation->track_get_key_count(p_track_i); - Vector times; - times.resize(key_count); - String path = p_animation->track_get_path(p_track_i); - for (int32_t key_i = 0; key_i < key_count; key_i++) { - times.write[key_i] = p_animation->track_get_key_time(p_track_i, key_i); - } - double anim_end = p_animation->get_length(); +bool GLTFDocument::_convert_animation_node_track(Ref p_state, GLTFAnimation::NodeTrack &p_gltf_node_track, const Ref &p_godot_animation, int32_t p_godot_anim_track_index, Vector &p_times) { + GLTFAnimation::Interpolation gltf_interpolation = GLTFAnimation::godot_to_gltf_interpolation(p_godot_animation, p_godot_anim_track_index); + const Animation::TrackType track_type = p_godot_animation->track_get_type(p_godot_anim_track_index); + const int32_t key_count = p_godot_animation->track_get_key_count(p_godot_anim_track_index); + const NodePath node_path = p_godot_animation->track_get_path(p_godot_anim_track_index); + const Vector subpath = node_path.get_subnames(); + double anim_end = p_godot_animation->get_length(); if (track_type == Animation::TYPE_SCALE_3D) { if (gltf_interpolation == GLTFAnimation::INTERP_CUBIC_SPLINE) { gltf_interpolation = GLTFAnimation::INTERP_LINEAR; - p_track.scale_track.times.clear(); - p_track.scale_track.values.clear(); + p_gltf_node_track.scale_track.times.clear(); + p_gltf_node_track.scale_track.values.clear(); // CATMULLROMSPLINE or CUBIC_SPLINE have to be baked, apologies. const double increment = 1.0 / p_state->get_bake_fps(); double time = 0.0; bool last = false; while (true) { Vector3 scale; - Error err = p_animation->try_scale_track_interpolate(p_track_i, time, &scale); + Error err = p_godot_animation->try_scale_track_interpolate(p_godot_anim_track_index, time, &scale); ERR_CONTINUE(err != OK); - p_track.scale_track.values.push_back(scale); - p_track.scale_track.times.push_back(time); + p_gltf_node_track.scale_track.values.push_back(scale); + p_gltf_node_track.scale_track.times.push_back(time); if (last) { break; } @@ -6417,31 +7461,31 @@ GLTFAnimation::Track GLTFDocument::_convert_animation_track(Ref p_sta } } } else { - p_track.scale_track.times = times; - p_track.scale_track.interpolation = gltf_interpolation; - p_track.scale_track.values.resize(key_count); + p_gltf_node_track.scale_track.times = p_times; + p_gltf_node_track.scale_track.interpolation = gltf_interpolation; + p_gltf_node_track.scale_track.values.resize(key_count); for (int32_t key_i = 0; key_i < key_count; key_i++) { Vector3 scale; - Error err = p_animation->scale_track_get_key(p_track_i, key_i, &scale); + Error err = p_godot_animation->scale_track_get_key(p_godot_anim_track_index, key_i, &scale); ERR_CONTINUE(err != OK); - p_track.scale_track.values.write[key_i] = scale; + p_gltf_node_track.scale_track.values.write[key_i] = scale; } } } else if (track_type == Animation::TYPE_POSITION_3D) { if (gltf_interpolation == GLTFAnimation::INTERP_CUBIC_SPLINE) { gltf_interpolation = GLTFAnimation::INTERP_LINEAR; - p_track.position_track.times.clear(); - p_track.position_track.values.clear(); + p_gltf_node_track.position_track.times.clear(); + p_gltf_node_track.position_track.values.clear(); // CATMULLROMSPLINE or CUBIC_SPLINE have to be baked, apologies. const double increment = 1.0 / p_state->get_bake_fps(); double time = 0.0; bool last = false; while (true) { Vector3 scale; - Error err = p_animation->try_position_track_interpolate(p_track_i, time, &scale); + Error err = p_godot_animation->try_position_track_interpolate(p_godot_anim_track_index, time, &scale); ERR_CONTINUE(err != OK); - p_track.position_track.values.push_back(scale); - p_track.position_track.times.push_back(time); + p_gltf_node_track.position_track.values.push_back(scale); + p_gltf_node_track.position_track.times.push_back(time); if (last) { break; } @@ -6452,31 +7496,31 @@ GLTFAnimation::Track GLTFDocument::_convert_animation_track(Ref p_sta } } } else { - p_track.position_track.times = times; - p_track.position_track.values.resize(key_count); - p_track.position_track.interpolation = gltf_interpolation; + p_gltf_node_track.position_track.times = p_times; + p_gltf_node_track.position_track.values.resize(key_count); + p_gltf_node_track.position_track.interpolation = gltf_interpolation; for (int32_t key_i = 0; key_i < key_count; key_i++) { Vector3 position; - Error err = p_animation->position_track_get_key(p_track_i, key_i, &position); + Error err = p_godot_animation->position_track_get_key(p_godot_anim_track_index, key_i, &position); ERR_CONTINUE(err != OK); - p_track.position_track.values.write[key_i] = position; + p_gltf_node_track.position_track.values.write[key_i] = position; } } } else if (track_type == Animation::TYPE_ROTATION_3D) { if (gltf_interpolation == GLTFAnimation::INTERP_CUBIC_SPLINE) { gltf_interpolation = GLTFAnimation::INTERP_LINEAR; - p_track.rotation_track.times.clear(); - p_track.rotation_track.values.clear(); + p_gltf_node_track.rotation_track.times.clear(); + p_gltf_node_track.rotation_track.values.clear(); // CATMULLROMSPLINE or CUBIC_SPLINE have to be baked, apologies. const double increment = 1.0 / p_state->get_bake_fps(); double time = 0.0; bool last = false; while (true) { Quaternion rotation; - Error err = p_animation->try_rotation_track_interpolate(p_track_i, time, &rotation); + Error err = p_godot_animation->try_rotation_track_interpolate(p_godot_anim_track_index, time, &rotation); ERR_CONTINUE(err != OK); - p_track.rotation_track.values.push_back(rotation); - p_track.rotation_track.times.push_back(time); + p_gltf_node_track.rotation_track.values.push_back(rotation); + p_gltf_node_track.rotation_track.times.push_back(time); if (last) { break; } @@ -6487,306 +7531,326 @@ GLTFAnimation::Track GLTFDocument::_convert_animation_track(Ref p_sta } } } else { - p_track.rotation_track.times = times; - p_track.rotation_track.values.resize(key_count); - p_track.rotation_track.interpolation = gltf_interpolation; + p_gltf_node_track.rotation_track.times = p_times; + p_gltf_node_track.rotation_track.values.resize(key_count); + p_gltf_node_track.rotation_track.interpolation = gltf_interpolation; for (int32_t key_i = 0; key_i < key_count; key_i++) { Quaternion rotation; - Error err = p_animation->rotation_track_get_key(p_track_i, key_i, &rotation); + Error err = p_godot_animation->rotation_track_get_key(p_godot_anim_track_index, key_i, &rotation); ERR_CONTINUE(err != OK); - p_track.rotation_track.values.write[key_i] = rotation; + p_gltf_node_track.rotation_track.values.write[key_i] = rotation; } } - } else if (track_type == Animation::TYPE_VALUE) { - if (path.contains(":position")) { - p_track.position_track.interpolation = gltf_interpolation; - p_track.position_track.times = times; - p_track.position_track.values.resize(key_count); + } else if (subpath.size() > 0) { + const StringName &node_prop = subpath[0]; + if (track_type == Animation::TYPE_VALUE) { + if (node_prop == "position") { + p_gltf_node_track.position_track.interpolation = gltf_interpolation; + p_gltf_node_track.position_track.times = p_times; + p_gltf_node_track.position_track.values.resize(key_count); - if (gltf_interpolation == GLTFAnimation::INTERP_CUBIC_SPLINE) { - gltf_interpolation = GLTFAnimation::INTERP_LINEAR; - p_track.position_track.times.clear(); - p_track.position_track.values.clear(); - // CATMULLROMSPLINE or CUBIC_SPLINE have to be baked, apologies. - const double increment = 1.0 / p_state->get_bake_fps(); - double time = 0.0; - bool last = false; - while (true) { - Vector3 position; - Error err = p_animation->try_position_track_interpolate(p_track_i, time, &position); - ERR_CONTINUE(err != OK); - p_track.position_track.values.push_back(position); - p_track.position_track.times.push_back(time); - if (last) { - break; + if (gltf_interpolation == GLTFAnimation::INTERP_CUBIC_SPLINE) { + gltf_interpolation = GLTFAnimation::INTERP_LINEAR; + p_gltf_node_track.position_track.times.clear(); + p_gltf_node_track.position_track.values.clear(); + // CATMULLROMSPLINE or CUBIC_SPLINE have to be baked, apologies. + const double increment = 1.0 / p_state->get_bake_fps(); + double time = 0.0; + bool last = false; + while (true) { + Vector3 position; + Error err = p_godot_animation->try_position_track_interpolate(p_godot_anim_track_index, time, &position); + ERR_CONTINUE(err != OK); + p_gltf_node_track.position_track.values.push_back(position); + p_gltf_node_track.position_track.times.push_back(time); + if (last) { + break; + } + time += increment; + if (time >= anim_end) { + last = true; + time = anim_end; + } } - time += increment; - if (time >= anim_end) { - last = true; - time = anim_end; + } else { + for (int32_t key_i = 0; key_i < key_count; key_i++) { + Vector3 position = p_godot_animation->track_get_key_value(p_godot_anim_track_index, key_i); + p_gltf_node_track.position_track.values.write[key_i] = position; + } + } + } else if (node_prop == "rotation" || node_prop == "rotation_degrees" || node_prop == "quaternion") { + p_gltf_node_track.rotation_track.interpolation = gltf_interpolation; + p_gltf_node_track.rotation_track.times = p_times; + p_gltf_node_track.rotation_track.values.resize(key_count); + if (gltf_interpolation == GLTFAnimation::INTERP_CUBIC_SPLINE) { + gltf_interpolation = GLTFAnimation::INTERP_LINEAR; + p_gltf_node_track.rotation_track.times.clear(); + p_gltf_node_track.rotation_track.values.clear(); + // CATMULLROMSPLINE or CUBIC_SPLINE have to be baked, apologies. + const double increment = 1.0 / p_state->get_bake_fps(); + double time = 0.0; + bool last = false; + while (true) { + Quaternion rotation; + Error err = p_godot_animation->try_rotation_track_interpolate(p_godot_anim_track_index, time, &rotation); + ERR_CONTINUE(err != OK); + p_gltf_node_track.rotation_track.values.push_back(rotation); + p_gltf_node_track.rotation_track.times.push_back(time); + if (last) { + break; + } + time += increment; + if (time >= anim_end) { + last = true; + time = anim_end; + } + } + } else { + for (int32_t key_i = 0; key_i < key_count; key_i++) { + Quaternion rotation_quaternion; + if (node_prop == "quaternion") { + rotation_quaternion = p_godot_animation->track_get_key_value(p_godot_anim_track_index, key_i); + } else { + Vector3 rotation_euler = p_godot_animation->track_get_key_value(p_godot_anim_track_index, key_i); + if (node_prop == "rotation_degrees") { + rotation_euler *= Math_TAU / 360.0; + } + rotation_quaternion = Quaternion::from_euler(rotation_euler); + } + p_gltf_node_track.rotation_track.values.write[key_i] = rotation_quaternion; + } + } + } else if (node_prop == "scale") { + p_gltf_node_track.scale_track.interpolation = gltf_interpolation; + p_gltf_node_track.scale_track.times = p_times; + p_gltf_node_track.scale_track.values.resize(key_count); + + if (gltf_interpolation == GLTFAnimation::INTERP_CUBIC_SPLINE) { + gltf_interpolation = GLTFAnimation::INTERP_LINEAR; + p_gltf_node_track.scale_track.times.clear(); + p_gltf_node_track.scale_track.values.clear(); + // CATMULLROMSPLINE or CUBIC_SPLINE have to be baked, apologies. + const double increment = 1.0 / p_state->get_bake_fps(); + double time = 0.0; + bool last = false; + while (true) { + Vector3 scale; + Error err = p_godot_animation->try_scale_track_interpolate(p_godot_anim_track_index, time, &scale); + ERR_CONTINUE(err != OK); + p_gltf_node_track.scale_track.values.push_back(scale); + p_gltf_node_track.scale_track.times.push_back(time); + if (last) { + break; + } + time += increment; + if (time >= anim_end) { + last = true; + time = anim_end; + } + } + } else { + for (int32_t key_i = 0; key_i < key_count; key_i++) { + Vector3 scale_track = p_godot_animation->track_get_key_value(p_godot_anim_track_index, key_i); + p_gltf_node_track.scale_track.values.write[key_i] = scale_track; + } + } + } else if (node_prop == "transform") { + p_gltf_node_track.position_track.interpolation = gltf_interpolation; + p_gltf_node_track.position_track.times = p_times; + p_gltf_node_track.position_track.values.resize(key_count); + p_gltf_node_track.rotation_track.interpolation = gltf_interpolation; + p_gltf_node_track.rotation_track.times = p_times; + p_gltf_node_track.rotation_track.values.resize(key_count); + p_gltf_node_track.scale_track.interpolation = gltf_interpolation; + p_gltf_node_track.scale_track.times = p_times; + p_gltf_node_track.scale_track.values.resize(key_count); + if (gltf_interpolation == GLTFAnimation::INTERP_CUBIC_SPLINE) { + gltf_interpolation = GLTFAnimation::INTERP_LINEAR; + p_gltf_node_track.position_track.times.clear(); + p_gltf_node_track.position_track.values.clear(); + p_gltf_node_track.rotation_track.times.clear(); + p_gltf_node_track.rotation_track.values.clear(); + p_gltf_node_track.scale_track.times.clear(); + p_gltf_node_track.scale_track.values.clear(); + // CATMULLROMSPLINE or CUBIC_SPLINE have to be baked, apologies. + const double increment = 1.0 / p_state->get_bake_fps(); + double time = 0.0; + bool last = false; + while (true) { + Vector3 position; + Quaternion rotation; + Vector3 scale; + Error err = p_godot_animation->try_position_track_interpolate(p_godot_anim_track_index, time, &position); + ERR_CONTINUE(err != OK); + err = p_godot_animation->try_rotation_track_interpolate(p_godot_anim_track_index, time, &rotation); + ERR_CONTINUE(err != OK); + err = p_godot_animation->try_scale_track_interpolate(p_godot_anim_track_index, time, &scale); + ERR_CONTINUE(err != OK); + p_gltf_node_track.position_track.values.push_back(position); + p_gltf_node_track.position_track.times.push_back(time); + p_gltf_node_track.rotation_track.values.push_back(rotation); + p_gltf_node_track.rotation_track.times.push_back(time); + p_gltf_node_track.scale_track.values.push_back(scale); + p_gltf_node_track.scale_track.times.push_back(time); + if (last) { + break; + } + time += increment; + if (time >= anim_end) { + last = true; + time = anim_end; + } + } + } else { + for (int32_t key_i = 0; key_i < key_count; key_i++) { + Transform3D transform = p_godot_animation->track_get_key_value(p_godot_anim_track_index, key_i); + p_gltf_node_track.position_track.values.write[key_i] = transform.get_origin(); + p_gltf_node_track.rotation_track.values.write[key_i] = transform.basis.get_rotation_quaternion(); + p_gltf_node_track.scale_track.values.write[key_i] = transform.basis.get_scale(); } } } else { - for (int32_t key_i = 0; key_i < key_count; key_i++) { - Vector3 position = p_animation->track_get_key_value(p_track_i, key_i); - p_track.position_track.values.write[key_i] = position; - } + // This is a Value track animating a property, but not a TRS property, so it can't be converted into a node track. + return false; } - } else if (path.contains(":rotation")) { - p_track.rotation_track.interpolation = gltf_interpolation; - p_track.rotation_track.times = times; - p_track.rotation_track.values.resize(key_count); - if (gltf_interpolation == GLTFAnimation::INTERP_CUBIC_SPLINE) { - gltf_interpolation = GLTFAnimation::INTERP_LINEAR; - p_track.rotation_track.times.clear(); - p_track.rotation_track.values.clear(); - // CATMULLROMSPLINE or CUBIC_SPLINE have to be baked, apologies. - const double increment = 1.0 / p_state->get_bake_fps(); - double time = 0.0; - bool last = false; - while (true) { - Quaternion rotation; - Error err = p_animation->try_rotation_track_interpolate(p_track_i, time, &rotation); - ERR_CONTINUE(err != OK); - p_track.rotation_track.values.push_back(rotation); - p_track.rotation_track.times.push_back(time); - if (last) { - break; + } else if (track_type == Animation::TYPE_BEZIER) { + const int32_t keys = anim_end * p_state->get_bake_fps(); + if (node_prop == "scale") { + if (p_gltf_node_track.scale_track.times.is_empty()) { + p_gltf_node_track.scale_track.interpolation = gltf_interpolation; + Vector new_times; + new_times.resize(keys); + for (int32_t key_i = 0; key_i < keys; key_i++) { + new_times.write[key_i] = key_i / p_state->get_bake_fps(); } - time += increment; - if (time >= anim_end) { - last = true; - time = anim_end; + p_gltf_node_track.scale_track.times = new_times; + + p_gltf_node_track.scale_track.values.resize(keys); + + for (int32_t key_i = 0; key_i < keys; key_i++) { + p_gltf_node_track.scale_track.values.write[key_i] = Vector3(1.0f, 1.0f, 1.0f); } + + for (int32_t key_i = 0; key_i < keys; key_i++) { + Vector3 bezier_track = p_gltf_node_track.scale_track.values[key_i]; + if (subpath.size() == 2) { + if (subpath[1] == StringName("x")) { + bezier_track.x = p_godot_animation->bezier_track_interpolate(p_godot_anim_track_index, key_i / p_state->get_bake_fps()); + } else if (subpath[1] == StringName("y")) { + bezier_track.y = p_godot_animation->bezier_track_interpolate(p_godot_anim_track_index, key_i / p_state->get_bake_fps()); + } else if (subpath[1] == StringName("z")) { + bezier_track.z = p_godot_animation->bezier_track_interpolate(p_godot_anim_track_index, key_i / p_state->get_bake_fps()); + } + } + p_gltf_node_track.scale_track.values.write[key_i] = bezier_track; + } + } + } else if (node_prop == "position") { + if (p_gltf_node_track.position_track.times.is_empty()) { + p_gltf_node_track.position_track.interpolation = gltf_interpolation; + Vector new_times; + new_times.resize(keys); + for (int32_t key_i = 0; key_i < keys; key_i++) { + new_times.write[key_i] = key_i / p_state->get_bake_fps(); + } + p_gltf_node_track.position_track.times = new_times; + + p_gltf_node_track.position_track.values.resize(keys); + } + + for (int32_t key_i = 0; key_i < keys; key_i++) { + Vector3 bezier_track = p_gltf_node_track.position_track.values[key_i]; + if (subpath.size() == 2) { + if (subpath[1] == StringName("x")) { + bezier_track.x = p_godot_animation->bezier_track_interpolate(p_godot_anim_track_index, key_i / p_state->get_bake_fps()); + } else if (subpath[1] == StringName("y")) { + bezier_track.y = p_godot_animation->bezier_track_interpolate(p_godot_anim_track_index, key_i / p_state->get_bake_fps()); + } else if (subpath[1] == StringName("z")) { + bezier_track.z = p_godot_animation->bezier_track_interpolate(p_godot_anim_track_index, key_i / p_state->get_bake_fps()); + } + } + p_gltf_node_track.position_track.values.write[key_i] = bezier_track; + } + } else if (node_prop == "quaternion") { + if (p_gltf_node_track.rotation_track.times.is_empty()) { + p_gltf_node_track.rotation_track.interpolation = gltf_interpolation; + Vector new_times; + new_times.resize(keys); + for (int32_t key_i = 0; key_i < keys; key_i++) { + new_times.write[key_i] = key_i / p_state->get_bake_fps(); + } + p_gltf_node_track.rotation_track.times = new_times; + + p_gltf_node_track.rotation_track.values.resize(keys); + } + for (int32_t key_i = 0; key_i < keys; key_i++) { + Quaternion bezier_track = p_gltf_node_track.rotation_track.values[key_i]; + if (subpath.size() == 2) { + if (subpath[1] == StringName("x")) { + bezier_track.x = p_godot_animation->bezier_track_interpolate(p_godot_anim_track_index, key_i / p_state->get_bake_fps()); + } else if (subpath[1] == StringName("y")) { + bezier_track.y = p_godot_animation->bezier_track_interpolate(p_godot_anim_track_index, key_i / p_state->get_bake_fps()); + } else if (subpath[1] == StringName("z")) { + bezier_track.z = p_godot_animation->bezier_track_interpolate(p_godot_anim_track_index, key_i / p_state->get_bake_fps()); + } else if (subpath[1] == StringName("w")) { + bezier_track.w = p_godot_animation->bezier_track_interpolate(p_godot_anim_track_index, key_i / p_state->get_bake_fps()); + } + } + p_gltf_node_track.rotation_track.values.write[key_i] = bezier_track; } } else { - for (int32_t key_i = 0; key_i < key_count; key_i++) { - Vector3 rotation_radian = p_animation->track_get_key_value(p_track_i, key_i); - p_track.rotation_track.values.write[key_i] = Quaternion::from_euler(rotation_radian); - } - } - } else if (path.contains(":scale")) { - p_track.scale_track.times = times; - p_track.scale_track.interpolation = gltf_interpolation; - - p_track.scale_track.values.resize(key_count); - p_track.scale_track.interpolation = gltf_interpolation; - - if (gltf_interpolation == GLTFAnimation::INTERP_CUBIC_SPLINE) { - gltf_interpolation = GLTFAnimation::INTERP_LINEAR; - p_track.scale_track.times.clear(); - p_track.scale_track.values.clear(); - // CATMULLROMSPLINE or CUBIC_SPLINE have to be baked, apologies. - const double increment = 1.0 / p_state->get_bake_fps(); - double time = 0.0; - bool last = false; - while (true) { - Vector3 scale; - Error err = p_animation->try_scale_track_interpolate(p_track_i, time, &scale); - ERR_CONTINUE(err != OK); - p_track.scale_track.values.push_back(scale); - p_track.scale_track.times.push_back(time); - if (last) { - break; - } - time += increment; - if (time >= anim_end) { - last = true; - time = anim_end; - } - } - } else { - for (int32_t key_i = 0; key_i < key_count; key_i++) { - Vector3 scale_track = p_animation->track_get_key_value(p_track_i, key_i); - p_track.scale_track.values.write[key_i] = scale_track; - } - } - } - } else if (track_type == Animation::TYPE_BEZIER) { - const int32_t keys = anim_end * p_state->get_bake_fps(); - if (path.contains(":scale")) { - if (!p_track.scale_track.times.size()) { - p_track.scale_track.interpolation = gltf_interpolation; - Vector new_times; - new_times.resize(keys); - for (int32_t key_i = 0; key_i < keys; key_i++) { - new_times.write[key_i] = key_i / p_state->get_bake_fps(); - } - p_track.scale_track.times = new_times; - - p_track.scale_track.values.resize(keys); - - for (int32_t key_i = 0; key_i < keys; key_i++) { - p_track.scale_track.values.write[key_i] = Vector3(1.0f, 1.0f, 1.0f); - } - - for (int32_t key_i = 0; key_i < keys; key_i++) { - Vector3 bezier_track = p_track.scale_track.values[key_i]; - if (path.contains(":scale:x")) { - bezier_track.x = p_animation->bezier_track_interpolate(p_track_i, key_i / p_state->get_bake_fps()); - } else if (path.contains(":scale:y")) { - bezier_track.y = p_animation->bezier_track_interpolate(p_track_i, key_i / p_state->get_bake_fps()); - } else if (path.contains(":scale:z")) { - bezier_track.z = p_animation->bezier_track_interpolate(p_track_i, key_i / p_state->get_bake_fps()); - } - p_track.scale_track.values.write[key_i] = bezier_track; - } - } - } else if (path.contains(":position")) { - if (!p_track.position_track.times.size()) { - p_track.position_track.interpolation = gltf_interpolation; - Vector new_times; - new_times.resize(keys); - for (int32_t key_i = 0; key_i < keys; key_i++) { - new_times.write[key_i] = key_i / p_state->get_bake_fps(); - } - p_track.position_track.times = new_times; - - p_track.position_track.values.resize(keys); - } - - for (int32_t key_i = 0; key_i < keys; key_i++) { - Vector3 bezier_track = p_track.position_track.values[key_i]; - if (path.contains(":position:x")) { - bezier_track.x = p_animation->bezier_track_interpolate(p_track_i, key_i / p_state->get_bake_fps()); - } else if (path.contains(":position:y")) { - bezier_track.y = p_animation->bezier_track_interpolate(p_track_i, key_i / p_state->get_bake_fps()); - } else if (path.contains(":position:z")) { - bezier_track.z = p_animation->bezier_track_interpolate(p_track_i, key_i / p_state->get_bake_fps()); - } - p_track.position_track.values.write[key_i] = bezier_track; - } - } else if (path.contains(":rotation")) { - if (!p_track.rotation_track.times.size()) { - p_track.rotation_track.interpolation = gltf_interpolation; - Vector new_times; - new_times.resize(keys); - for (int32_t key_i = 0; key_i < keys; key_i++) { - new_times.write[key_i] = key_i / p_state->get_bake_fps(); - } - p_track.rotation_track.times = new_times; - - p_track.rotation_track.values.resize(keys); - } - for (int32_t key_i = 0; key_i < keys; key_i++) { - Quaternion bezier_track = p_track.rotation_track.values[key_i]; - if (path.contains(":rotation:x")) { - bezier_track.x = p_animation->bezier_track_interpolate(p_track_i, key_i / p_state->get_bake_fps()); - } else if (path.contains(":rotation:y")) { - bezier_track.y = p_animation->bezier_track_interpolate(p_track_i, key_i / p_state->get_bake_fps()); - } else if (path.contains(":rotation:z")) { - bezier_track.z = p_animation->bezier_track_interpolate(p_track_i, key_i / p_state->get_bake_fps()); - } else if (path.contains(":rotation:w")) { - bezier_track.w = p_animation->bezier_track_interpolate(p_track_i, key_i / p_state->get_bake_fps()); - } - p_track.rotation_track.values.write[key_i] = bezier_track; + // This is a Bezier track animating a property, but not a TRS property, so it can't be converted into a node track. + return false; } + } else { + // This property track isn't a Value track or Bezier track, so it can't be converted into a node track. + return false; } + } else { + // This isn't a TRS track or a property track, so it can't be converted into a node track. + return false; } - return p_track; + // 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; } -void GLTFDocument::_convert_animation(Ref p_state, AnimationPlayer *p_animation_player, String p_animation_track_name) { +void GLTFDocument::_convert_animation(Ref p_state, AnimationPlayer *p_animation_player, const String &p_animation_track_name) { Ref animation = p_animation_player->get_animation(p_animation_track_name); Ref gltf_animation; gltf_animation.instantiate(); gltf_animation->set_original_name(p_animation_track_name); gltf_animation->set_name(_gen_unique_name(p_state, p_animation_track_name)); - for (int32_t track_i = 0; track_i < animation->get_track_count(); track_i++) { - if (!animation->track_is_enabled(track_i)) { + HashMap &node_tracks = gltf_animation->get_node_tracks(); + for (int32_t track_index = 0; track_index < animation->get_track_count(); track_index++) { + if (!animation->track_is_enabled(track_index)) { continue; } - String final_track_path = animation->track_get_path(track_i); - Node *animation_base_node = p_animation_player->get_parent(); - ERR_CONTINUE_MSG(!animation_base_node, "Cannot get the parent of the animation player."); - if (String(final_track_path).contains(":position")) { - const Vector node_suffix = String(final_track_path).split(":position"); - const NodePath path = node_suffix[0]; - const Node *node = animation_base_node->get_node_or_null(path); - ERR_CONTINUE_MSG(!node, "Cannot get the node from a position path."); - for (const KeyValue &position_scene_node_i : p_state->scene_nodes) { - if (position_scene_node_i.value == node) { - GLTFNodeIndex node_index = position_scene_node_i.key; - HashMap::Iterator position_track_i = gltf_animation->get_tracks().find(node_index); - GLTFAnimation::Track track; - if (position_track_i) { - track = position_track_i->value; - } - track = _convert_animation_track(p_state, track, animation, track_i, node_index); - gltf_animation->get_tracks().insert(node_index, track); - } - } - } else if (String(final_track_path).contains(":rotation_degrees")) { - const Vector node_suffix = String(final_track_path).split(":rotation_degrees"); - const NodePath path = node_suffix[0]; - const Node *node = animation_base_node->get_node_or_null(path); - ERR_CONTINUE_MSG(!node, "Cannot get the node from a rotation degrees path."); - for (const KeyValue &rotation_degree_scene_node_i : p_state->scene_nodes) { - if (rotation_degree_scene_node_i.value == node) { - GLTFNodeIndex node_index = rotation_degree_scene_node_i.key; - HashMap::Iterator rotation_degree_track_i = gltf_animation->get_tracks().find(node_index); - GLTFAnimation::Track track; - if (rotation_degree_track_i) { - track = rotation_degree_track_i->value; - } - track = _convert_animation_track(p_state, track, animation, track_i, node_index); - gltf_animation->get_tracks().insert(node_index, track); - } - } - } else if (String(final_track_path).contains(":scale")) { - const Vector node_suffix = String(final_track_path).split(":scale"); - const NodePath path = node_suffix[0]; - const Node *node = animation_base_node->get_node_or_null(path); - ERR_CONTINUE_MSG(!node, "Cannot get the node from a scale path."); - for (const KeyValue &scale_scene_node_i : p_state->scene_nodes) { - if (scale_scene_node_i.value == node) { - GLTFNodeIndex node_index = scale_scene_node_i.key; - HashMap::Iterator scale_track_i = gltf_animation->get_tracks().find(node_index); - GLTFAnimation::Track track; - if (scale_track_i) { - track = scale_track_i->value; - } - track = _convert_animation_track(p_state, track, animation, track_i, node_index); - gltf_animation->get_tracks().insert(node_index, track); - } - } - } else if (String(final_track_path).contains(":transform")) { - const Vector node_suffix = String(final_track_path).split(":transform"); - const NodePath path = node_suffix[0]; - const Node *node = animation_base_node->get_node_or_null(path); - ERR_CONTINUE_MSG(!node, "Cannot get the node from a transform path."); - for (const KeyValue &transform_track_i : p_state->scene_nodes) { - if (transform_track_i.value == node) { - GLTFAnimation::Track track; - track = _convert_animation_track(p_state, track, animation, track_i, transform_track_i.key); - gltf_animation->get_tracks().insert(transform_track_i.key, track); - } - } - } else if (String(final_track_path).contains(":") && animation->track_get_type(track_i) == Animation::TYPE_BLEND_SHAPE) { - const Vector node_suffix = String(final_track_path).split(":"); - const NodePath path = node_suffix[0]; - const String suffix = node_suffix[1]; - Node *node = animation_base_node->get_node_or_null(path); - ERR_CONTINUE_MSG(!node, "Cannot get the node from a blend shape path."); - MeshInstance3D *mi = cast_to(node); - if (!mi) { - continue; - } - Ref mesh = mi->get_mesh(); + // Get the Godot node and the glTF node index for the animation track. + const NodePath track_path = animation->track_get_path(track_index); + const Node *anim_player_parent = p_animation_player->get_parent(); + const Node *animated_node = anim_player_parent->get_node_or_null(track_path); + ERR_CONTINUE_MSG(!animated_node, "glTF: Cannot get node for animated track using path: " + String(track_path)); + const GLTFAnimation::Interpolation gltf_interpolation = GLTFAnimation::godot_to_gltf_interpolation(animation, track_index); + // First, check if it's a Blend Shape track. + if (animation->track_get_type(track_index) == Animation::TYPE_BLEND_SHAPE) { + const MeshInstance3D *mesh_instance = Object::cast_to(animated_node); + ERR_CONTINUE_MSG(!mesh_instance, "glTF: Animation had a Blend Shape track, but the node wasn't a MeshInstance3D. Ignoring this track."); + Ref mesh = mesh_instance->get_mesh(); ERR_CONTINUE(mesh.is_null()); int32_t mesh_index = -1; for (const KeyValue &mesh_track_i : p_state->scene_nodes) { - if (mesh_track_i.value == node) { + if (mesh_track_i.value == animated_node) { mesh_index = mesh_track_i.key; } } ERR_CONTINUE(mesh_index == -1); - HashMap &tracks = gltf_animation->get_tracks(); - GLTFAnimation::Track track = gltf_animation->get_tracks().has(mesh_index) ? gltf_animation->get_tracks()[mesh_index] : GLTFAnimation::Track(); - if (!tracks.has(mesh_index)) { + GLTFAnimation::NodeTrack track = node_tracks.has(mesh_index) ? node_tracks[mesh_index] : GLTFAnimation::NodeTrack(); + if (!node_tracks.has(mesh_index)) { for (int32_t shape_i = 0; shape_i < mesh->get_blend_shape_count(); shape_i++) { String shape_name = mesh->get_blend_shape_name(shape_i); - NodePath shape_path = String(path) + ":" + shape_name; + NodePath shape_path = NodePath(track_path.get_names(), { shape_name }, false); int32_t shape_track_i = animation->find_track(shape_path, Animation::TYPE_BLEND_SHAPE); if (shape_track_i == -1) { GLTFAnimation::Channel weight; @@ -6798,15 +7862,6 @@ void GLTFDocument::_convert_animation(Ref p_state, AnimationPlayer *p track.weight_tracks.push_back(weight); continue; } - Animation::InterpolationType interpolation = animation->track_get_interpolation_type(track_i); - GLTFAnimation::Interpolation gltf_interpolation = GLTFAnimation::INTERP_LINEAR; - if (interpolation == Animation::InterpolationType::INTERPOLATION_LINEAR) { - gltf_interpolation = GLTFAnimation::INTERP_LINEAR; - } else if (interpolation == Animation::InterpolationType::INTERPOLATION_NEAREST) { - gltf_interpolation = GLTFAnimation::INTERP_STEP; - } else if (interpolation == Animation::InterpolationType::INTERPOLATION_CUBIC) { - gltf_interpolation = GLTFAnimation::INTERP_CUBIC_SPLINE; - } int32_t key_count = animation->track_get_key_count(shape_track_i); GLTFAnimation::Channel weight; weight.interpolation = gltf_interpolation; @@ -6820,64 +7875,74 @@ void GLTFDocument::_convert_animation(Ref p_state, AnimationPlayer *p } track.weight_tracks.push_back(weight); } - tracks[mesh_index] = track; + node_tracks[mesh_index] = track; } - } else if (String(final_track_path).contains(":")) { - //Process skeleton - const Vector node_suffix = String(final_track_path).split(":"); - const String &node = node_suffix[0]; - const NodePath node_path = node; - const String &suffix = node_suffix[1]; - Node *godot_node = animation_base_node->get_node_or_null(node_path); - if (!godot_node) { - continue; - } - Skeleton3D *skeleton = cast_to(animation_base_node->get_node_or_null(node)); - if (!skeleton) { - continue; - } - GLTFSkeletonIndex skeleton_gltf_i = -1; - for (GLTFSkeletonIndex skeleton_i = 0; skeleton_i < p_state->skeletons.size(); skeleton_i++) { - if (p_state->skeletons[skeleton_i]->godot_skeleton == cast_to(godot_node)) { - skeleton = p_state->skeletons[skeleton_i]->godot_skeleton; - skeleton_gltf_i = skeleton_i; - ERR_CONTINUE(!skeleton); - Ref skeleton_gltf = p_state->skeletons[skeleton_gltf_i]; - int32_t bone = skeleton->find_bone(suffix); - ERR_CONTINUE_MSG(bone == -1, vformat("Cannot find the bone %s.", suffix)); - if (!skeleton_gltf->godot_bone_node.has(bone)) { - continue; - } - GLTFNodeIndex node_i = skeleton_gltf->godot_bone_node[bone]; - HashMap::Iterator property_track_i = gltf_animation->get_tracks().find(node_i); - GLTFAnimation::Track track; - if (property_track_i) { - track = property_track_i->value; - } - track = _convert_animation_track(p_state, track, animation, track_i, node_i); - gltf_animation->get_tracks()[node_i] = track; + continue; + } + // If it's not a Blend Shape track, it must either be a TRS track, a property Value track, or something we can't handle. + // For the cases we can handle, we will need to know the glTF node index, glTF interpolation, and the times of the track. + const Vector subnames = track_path.get_subnames(); + const GLTFNodeIndex node_i = _node_and_or_bone_to_gltf_node_index(p_state, subnames, animated_node); + ERR_CONTINUE_MSG(node_i == -1, "glTF: Cannot get glTF node index for animated track using path: " + String(track_path)); + const int anim_key_count = animation->track_get_key_count(track_index); + Vector times; + times.resize(anim_key_count); + for (int32_t key_i = 0; key_i < anim_key_count; key_i++) { + times.write[key_i] = animation->track_get_key_time(track_index, key_i); + } + // Try converting the track to a TRS glTF node track. This will only succeed if the Godot animation is a TRS track. + const HashMap::Iterator node_track_iter = node_tracks.find(node_i); + GLTFAnimation::NodeTrack track; + if (node_track_iter) { + track = node_track_iter->value; + } + if (_convert_animation_node_track(p_state, track, animation, track_index, times)) { + // If the track was successfully converted, save it and continue to the next track. + 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 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 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 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; + Vector leftover_subpath; + base_instance = anim_player_parent->get_node_and_resource(track_path, resource, leftover_subpath); + if (resource.is_valid()) { + base_instance = resource.ptr(); } } - } else if (!String(final_track_path).contains(":")) { - ERR_CONTINUE(!animation_base_node); - Node *godot_node = animation_base_node->get_node_or_null(final_track_path); - ERR_CONTINUE_MSG(!godot_node, vformat("Cannot get the node from a skeleton path %s.", final_track_path)); - for (const KeyValue &scene_node_i : p_state->scene_nodes) { - if (scene_node_i.value == godot_node) { - GLTFNodeIndex node_i = scene_node_i.key; - HashMap::Iterator node_track_i = gltf_animation->get_tracks().find(node_i); - GLTFAnimation::Track track; - if (node_track_i) { - track = node_track_i->value; - } - track = _convert_animation_track(p_state, track, animation, track_i, node_i); - gltf_animation->get_tracks()[node_i] = track; - break; + // 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> &pointer_tracks = gltf_animation->get_pointer_tracks(); + Vector 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->get_tracks().size()) { + if (!gltf_animation->is_empty_of_tracks()) { p_state->animations.push_back(gltf_animation); } } @@ -7076,6 +8141,9 @@ void GLTFDocument::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "lossy_quality"), "set_lossy_quality", "get_lossy_quality"); ADD_PROPERTY(PropertyInfo(Variant::INT, "root_node_mode"), "set_root_node_mode", "get_root_node_mode"); + ClassDB::bind_static_method("GLTFDocument", D_METHOD("import_object_model_property", "state", "json_pointer"), &GLTFDocument::import_object_model_property); + ClassDB::bind_static_method("GLTFDocument", D_METHOD("export_object_model_property", "state", "node_path", "godot_node", "gltf_node_index"), &GLTFDocument::export_object_model_property); + ClassDB::bind_static_method("GLTFDocument", D_METHOD("register_gltf_document_extension", "extension", "first_priority"), &GLTFDocument::register_gltf_document_extension, DEFVAL(false)); ClassDB::bind_static_method("GLTFDocument", D_METHOD("unregister_gltf_document_extension", "extension"), @@ -7137,6 +8205,7 @@ HashSet 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"); @@ -7314,6 +8383,10 @@ Error GLTFDocument::_parse_gltf_state(Ref p_state, const String &p_se err = SkinTool::_determine_skeletons(p_state->skins, p_state->nodes, p_state->skeletons, p_state->get_import_as_skeleton_bones() ? p_state->root_nodes : Vector()); ERR_FAIL_COND_V(err != OK, ERR_PARSE_ERROR); + /* ASSIGN SCENE NODE NAMES */ + // This must be run AFTER determining skeletons, and BEFORE parsing animations. + _assign_node_names(p_state); + /* PARSE MESHES (we have enough info now) */ err = _parse_meshes(p_state); ERR_FAIL_COND_V(err != OK, ERR_PARSE_ERROR); @@ -7330,9 +8403,6 @@ Error GLTFDocument::_parse_gltf_state(Ref p_state, const String &p_se err = _parse_animations(p_state); ERR_FAIL_COND_V(err != OK, ERR_PARSE_ERROR); - /* ASSIGN SCENE NAMES */ - _assign_node_names(p_state); - return OK; } diff --git a/modules/gltf/gltf_document.h b/modules/gltf/gltf_document.h index d347d491029..a6d6caa3f0a 100644 --- a/modules/gltf/gltf_document.h +++ b/modules/gltf/gltf_document.h @@ -56,13 +56,6 @@ public: enum { ARRAY_BUFFER = 34962, ELEMENT_ARRAY_BUFFER = 34963, - - COMPONENT_TYPE_BYTE = 5120, - COMPONENT_TYPE_UNSIGNED_BYTE = 5121, - COMPONENT_TYPE_SHORT = 5122, - COMPONENT_TYPE_UNSIGNED_SHORT = 5123, - COMPONENT_TYPE_INT = 5125, - COMPONENT_TYPE_FLOAT = 5126, }; enum { TEXTURE_TYPE_GENERIC = 0, @@ -95,6 +88,10 @@ public: static Vector get_supported_gltf_extensions(); static HashSet get_supported_gltf_extensions_hashset(); + static NodePath _find_material_node_path(Ref p_state, Ref p_material); + static Ref import_object_model_property(Ref p_state, const String &p_json_pointer); + static Ref export_object_model_property(Ref p_state, const NodePath &p_node_path, const Node *p_godot_node, GLTFNodeIndex p_gltf_node_index); + void set_naming_version(int p_version); int get_naming_version() const; void set_image_format(const String &p_image_format); @@ -109,8 +106,8 @@ private: void _build_parent_hierachy(Ref p_state); double _filter_number(double p_float); void _round_min_max_components(Vector &r_type_min, Vector &r_type_max); - String _get_component_type_name(const uint32_t p_component); - int _get_component_type_size(const int p_component_type); + String _get_component_type_name(const GLTFAccessor::GLTFComponentType p_component_type); + int _get_component_type_size(const GLTFAccessor::GLTFComponentType p_component_type); Error _parse_scenes(Ref p_state); Error _parse_nodes(Ref p_state); String _get_accessor_type_name(const GLTFAccessor::GLTFAccessorType p_accessor_type); @@ -140,7 +137,7 @@ private: const int p_skip_every, const int p_skip_bytes, const int p_element_size, const int p_count, const GLTFAccessor::GLTFAccessorType p_accessor_type, const int p_component_count, - const int p_component_type, const int p_component_size, + const GLTFAccessor::GLTFComponentType p_component_type, const int p_component_size, const bool p_normalized, const int p_byte_offset, const bool p_for_vertex); Vector _decode_accessor(Ref p_state, @@ -178,6 +175,15 @@ private: Vector _decode_accessor_as_xform(Ref p_state, const GLTFAccessorIndex p_accessor, const bool p_for_vertex); + Vector _decode_accessor_as_variant(Ref p_state, + const GLTFAccessorIndex p_accessor, + Variant::Type p_variant_type, + GLTFAccessor::GLTFAccessorType p_accessor_type); + GLTFAccessorIndex _encode_accessor_as_variant(Ref p_state, + Vector p_attribs, + Variant::Type p_variant_type, + GLTFAccessor::GLTFAccessorType p_accessor_type, + GLTFAccessor::GLTFComponentType p_component_type = GLTFAccessor::COMPONENT_TYPE_SINGLE_FLOAT); Error _parse_meshes(Ref p_state); Error _serialize_textures(Ref p_state); Error _serialize_texture_samplers(Ref p_state); @@ -205,6 +211,7 @@ private: Error _parse_cameras(Ref p_state); Error _parse_lights(Ref p_state); Error _parse_animations(Ref p_state); + void _parse_animation_pointer(Ref p_state, const String &p_animation_json_pointer, const Ref p_gltf_animation, const GLTFAnimation::Interpolation p_interp, const Vector &p_times, const int p_output_value_accessor_index); Error _serialize_animations(Ref p_state); BoneAttachment3D *_generate_bone_attachment(Ref p_state, Skeleton3D *p_skeleton, @@ -216,7 +223,7 @@ private: Node3D *_generate_spatial(Ref p_state, const GLTFNodeIndex p_node_index); void _assign_node_names(Ref p_state); template - T _interpolate_track(const Vector &p_times, const Vector &p_values, + T _interpolate_track(const Vector &p_times, const Vector &p_values, const float p_time, const GLTFAnimation::Interpolation p_interp); GLTFAccessorIndex _encode_accessor_as_quaternions(Ref p_state, @@ -229,7 +236,7 @@ private: const Vector p_attribs, const bool p_for_vertex); GLTFAccessorIndex _encode_accessor_as_floats(Ref p_state, - const Vector p_attribs, + const Vector p_attribs, const bool p_for_vertex); GLTFAccessorIndex _encode_accessor_as_vec2(Ref p_state, const Vector p_attribs, @@ -269,7 +276,7 @@ private: const bool p_for_vertex); Error _encode_buffer_view(Ref p_state, const double *p_src, const int p_count, const GLTFAccessor::GLTFAccessorType p_accessor_type, - const int p_component_type, const bool p_normalized, + const GLTFAccessor::GLTFComponentType p_component_type, const bool p_normalized, const int p_byte_offset, const bool p_for_vertex, GLTFBufferViewIndex &r_accessor, const bool p_for_indices = false); @@ -280,11 +287,6 @@ private: Error _serialize_nodes(Ref p_state); Error _serialize_scenes(Ref p_state); String interpolation_to_string(const GLTFAnimation::Interpolation p_interp); - GLTFAnimation::Track _convert_animation_track(Ref p_state, - GLTFAnimation::Track p_track, - Ref p_animation, - int32_t p_track_i, - GLTFNodeIndex p_node_i); Error _encode_buffer_bins(Ref p_state, const String &p_path); Error _encode_buffer_glb(Ref p_state, const String &p_path); PackedByteArray _serialize_glb_buffer(Ref p_state, Error *r_err); @@ -342,11 +344,6 @@ public: void _convert_csg_shape_to_gltf(CSGShape3D *p_current, GLTFNodeIndex p_gltf_parent, Ref p_gltf_node, Ref p_state); #endif // MODULE_CSG_ENABLED - void _convert_animation_player_to_gltf( - AnimationPlayer *p_animation_player, Ref p_state, - GLTFNodeIndex p_gltf_current, - GLTFNodeIndex p_gltf_root_index, - Ref p_gltf_node, Node *p_scene_parent); void _check_visibility(Node *p_node, bool &r_retflag); void _convert_camera_to_gltf(Camera3D *p_camera, Ref p_state, Ref p_gltf_node); @@ -377,7 +374,15 @@ public: Ref p_gltf_node); GLTFMeshIndex _convert_mesh_to_gltf(Ref p_state, MeshInstance3D *p_mesh_instance); - void _convert_animation(Ref p_state, AnimationPlayer *p_animation_player, String p_animation_track_name); + + GLTFNodeIndex _node_and_or_bone_to_gltf_node_index(Ref p_state, const Vector &p_node_subpath, const Node *p_godot_node); + bool _convert_animation_node_track(Ref p_state, + GLTFAnimation::NodeTrack &p_gltf_node_track, + const Ref &p_godot_animation, + int32_t p_godot_anim_track_index, + Vector &p_times); + void _convert_animation(Ref p_state, AnimationPlayer *p_animation_player, const String &p_animation_track_name); + Error _serialize(Ref p_state); Error _parse(Ref p_state, String p_path, Ref p_file); }; diff --git a/modules/gltf/gltf_state.h b/modules/gltf/gltf_state.h index 042384f5466..d667cf8858f 100644 --- a/modules/gltf/gltf_state.h +++ b/modules/gltf/gltf_state.h @@ -38,6 +38,7 @@ #include "structures/gltf_camera.h" #include "structures/gltf_mesh.h" #include "structures/gltf_node.h" +#include "structures/gltf_object_model_property.h" #include "structures/gltf_skeleton.h" #include "structures/gltf_skin.h" #include "structures/gltf_texture.h" @@ -48,6 +49,7 @@ class GLTFState : public Resource { GDCLASS(GLTFState, Resource); friend class GLTFDocument; + friend class GLTFNode; protected: String base_path; @@ -102,6 +104,7 @@ protected: Vector> animations; HashMap scene_nodes; HashMap scene_mesh_instances; + HashMap> object_model_properties; HashMap skeleton3d_to_gltf_skeleton; HashMap> skin_and_skeleton3d_to_gltf_skin; diff --git a/modules/gltf/register_types.cpp b/modules/gltf/register_types.cpp index 53e9f2b84c9..fbc3ae611cb 100644 --- a/modules/gltf/register_types.cpp +++ b/modules/gltf/register_types.cpp @@ -37,6 +37,7 @@ #include "extensions/physics/gltf_document_extension_physics.h" #include "gltf_document.h" #include "gltf_state.h" +#include "structures/gltf_object_model_property.h" #ifdef TOOLS_ENABLED #include "editor/editor_import_blend_runner.h" @@ -112,6 +113,7 @@ void initialize_gltf_module(ModuleInitializationLevel p_level) { GDREGISTER_CLASS(GLTFLight); GDREGISTER_CLASS(GLTFMesh); GDREGISTER_CLASS(GLTFNode); + GDREGISTER_CLASS(GLTFObjectModelProperty); GDREGISTER_CLASS(GLTFPhysicsBody); GDREGISTER_CLASS(GLTFPhysicsShape); GDREGISTER_CLASS(GLTFSkeleton); diff --git a/modules/gltf/structures/gltf_accessor.cpp b/modules/gltf/structures/gltf_accessor.cpp index 1ebc00a5146..300fce09ff5 100644 --- a/modules/gltf/structures/gltf_accessor.cpp +++ b/modules/gltf/structures/gltf_accessor.cpp @@ -39,6 +39,19 @@ void GLTFAccessor::_bind_methods() { BIND_ENUM_CONSTANT(TYPE_MAT3); BIND_ENUM_CONSTANT(TYPE_MAT4); + BIND_ENUM_CONSTANT(COMPONENT_TYPE_NONE); + BIND_ENUM_CONSTANT(COMPONENT_TYPE_SIGNED_BYTE); + BIND_ENUM_CONSTANT(COMPONENT_TYPE_UNSIGNED_BYTE); + BIND_ENUM_CONSTANT(COMPONENT_TYPE_SIGNED_SHORT); + BIND_ENUM_CONSTANT(COMPONENT_TYPE_UNSIGNED_SHORT); + BIND_ENUM_CONSTANT(COMPONENT_TYPE_SIGNED_INT); + BIND_ENUM_CONSTANT(COMPONENT_TYPE_UNSIGNED_INT); + BIND_ENUM_CONSTANT(COMPONENT_TYPE_SINGLE_FLOAT); + BIND_ENUM_CONSTANT(COMPONENT_TYPE_DOUBLE_FLOAT); + BIND_ENUM_CONSTANT(COMPONENT_TYPE_HALF_FLOAT); + BIND_ENUM_CONSTANT(COMPONENT_TYPE_SIGNED_LONG); + BIND_ENUM_CONSTANT(COMPONENT_TYPE_UNSIGNED_LONG); + ClassDB::bind_method(D_METHOD("get_buffer_view"), &GLTFAccessor::get_buffer_view); ClassDB::bind_method(D_METHOD("set_buffer_view", "buffer_view"), &GLTFAccessor::set_buffer_view); ClassDB::bind_method(D_METHOD("get_byte_offset"), &GLTFAccessor::get_byte_offset); @@ -108,7 +121,7 @@ int GLTFAccessor::get_component_type() { } void GLTFAccessor::set_component_type(int p_component_type) { - component_type = p_component_type; + component_type = (GLTFComponentType)p_component_type; } bool GLTFAccessor::get_normalized() { @@ -188,7 +201,7 @@ int GLTFAccessor::get_sparse_indices_component_type() { } void GLTFAccessor::set_sparse_indices_component_type(int p_sparse_indices_component_type) { - sparse_indices_component_type = p_sparse_indices_component_type; + sparse_indices_component_type = (GLTFComponentType)p_sparse_indices_component_type; } int GLTFAccessor::get_sparse_values_buffer_view() { diff --git a/modules/gltf/structures/gltf_accessor.h b/modules/gltf/structures/gltf_accessor.h index 1a3a2cb4943..b00e6a0f920 100644 --- a/modules/gltf/structures/gltf_accessor.h +++ b/modules/gltf/structures/gltf_accessor.h @@ -50,10 +50,25 @@ public: TYPE_MAT4, }; + enum GLTFComponentType { + COMPONENT_TYPE_NONE = 0, + COMPONENT_TYPE_SIGNED_BYTE = 5120, + COMPONENT_TYPE_UNSIGNED_BYTE = 5121, + COMPONENT_TYPE_SIGNED_SHORT = 5122, + COMPONENT_TYPE_UNSIGNED_SHORT = 5123, + COMPONENT_TYPE_SIGNED_INT = 5124, + COMPONENT_TYPE_UNSIGNED_INT = 5125, + COMPONENT_TYPE_SINGLE_FLOAT = 5126, + COMPONENT_TYPE_DOUBLE_FLOAT = 5130, + COMPONENT_TYPE_HALF_FLOAT = 5131, + COMPONENT_TYPE_SIGNED_LONG = 5134, + COMPONENT_TYPE_UNSIGNED_LONG = 5135, + }; + private: GLTFBufferViewIndex buffer_view = -1; int byte_offset = 0; - int component_type = 0; + GLTFComponentType component_type = COMPONENT_TYPE_NONE; bool normalized = false; int count = 0; GLTFAccessorType accessor_type = GLTFAccessorType::TYPE_SCALAR; @@ -62,7 +77,7 @@ private: int sparse_count = 0; int sparse_indices_buffer_view = 0; int sparse_indices_byte_offset = 0; - int sparse_indices_component_type = 0; + GLTFComponentType sparse_indices_component_type = COMPONENT_TYPE_NONE; int sparse_values_buffer_view = 0; int sparse_values_byte_offset = 0; @@ -117,5 +132,6 @@ public: }; VARIANT_ENUM_CAST(GLTFAccessor::GLTFAccessorType); +VARIANT_ENUM_CAST(GLTFAccessor::GLTFComponentType); #endif // GLTF_ACCESSOR_H diff --git a/modules/gltf/structures/gltf_animation.cpp b/modules/gltf/structures/gltf_animation.cpp index 94fda8e2f57..adc0354c4b1 100644 --- a/modules/gltf/structures/gltf_animation.cpp +++ b/modules/gltf/structures/gltf_animation.cpp @@ -42,6 +42,34 @@ void GLTFAnimation::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::BOOL, "loop"), "set_loop", "get_loop"); // bool } +GLTFAnimation::Interpolation GLTFAnimation::godot_to_gltf_interpolation(const Ref &p_godot_animation, int32_t p_godot_anim_track_index) { + Animation::InterpolationType interpolation = p_godot_animation->track_get_interpolation_type(p_godot_anim_track_index); + switch (interpolation) { + case Animation::INTERPOLATION_LINEAR: + case Animation::INTERPOLATION_LINEAR_ANGLE: + return INTERP_LINEAR; + case Animation::INTERPOLATION_NEAREST: + return INTERP_STEP; + case Animation::INTERPOLATION_CUBIC: + case Animation::INTERPOLATION_CUBIC_ANGLE: + return INTERP_CUBIC_SPLINE; + } + return INTERP_LINEAR; +} + +Animation::InterpolationType GLTFAnimation::gltf_to_godot_interpolation(Interpolation p_gltf_interpolation) { + switch (p_gltf_interpolation) { + case INTERP_LINEAR: + return Animation::INTERPOLATION_LINEAR; + case INTERP_STEP: + return Animation::INTERPOLATION_NEAREST; + case INTERP_CATMULLROMSPLINE: + case INTERP_CUBIC_SPLINE: + return Animation::INTERPOLATION_CUBIC; + } + return Animation::INTERPOLATION_LINEAR; +} + String GLTFAnimation::get_original_name() { return original_name; } @@ -58,8 +86,16 @@ void GLTFAnimation::set_loop(bool p_val) { loop = p_val; } -HashMap &GLTFAnimation::get_tracks() { - return tracks; +HashMap &GLTFAnimation::get_node_tracks() { + return node_tracks; +} + +HashMap> &GLTFAnimation::get_pointer_tracks() { + return pointer_tracks; +} + +bool GLTFAnimation::is_empty_of_tracks() const { + return node_tracks.is_empty() && pointer_tracks.is_empty(); } GLTFAnimation::GLTFAnimation() { diff --git a/modules/gltf/structures/gltf_animation.h b/modules/gltf/structures/gltf_animation.h index afc97848953..6b692d06e6b 100644 --- a/modules/gltf/structures/gltf_animation.h +++ b/modules/gltf/structures/gltf_animation.h @@ -50,33 +50,41 @@ public: template struct Channel { Interpolation interpolation = INTERP_LINEAR; - Vector times; + Vector times; Vector values; }; - struct Track { + struct NodeTrack { Channel position_track; Channel rotation_track; Channel scale_track; Vector> weight_tracks; }; + String original_name; + bool loop = false; + HashMap node_tracks; + HashMap> pointer_tracks; + Dictionary additional_data; + public: + static Interpolation godot_to_gltf_interpolation(const Ref &p_godot_animation, int32_t p_godot_anim_track_index); + static Animation::InterpolationType gltf_to_godot_interpolation(Interpolation p_gltf_interpolation); + String get_original_name(); void set_original_name(String p_name); bool get_loop() const; void set_loop(bool p_val); - HashMap &get_tracks(); + + HashMap &get_node_tracks(); + HashMap> &get_pointer_tracks(); + bool is_empty_of_tracks() const; + Variant get_additional_data(const StringName &p_extension_name); void set_additional_data(const StringName &p_extension_name, Variant p_additional_data); - GLTFAnimation(); -private: - String original_name; - bool loop = false; - HashMap tracks; - Dictionary additional_data; + GLTFAnimation(); }; #endif // GLTF_ANIMATION_H diff --git a/modules/gltf/structures/gltf_camera.cpp b/modules/gltf/structures/gltf_camera.cpp index 863e1df967a..2960ec351d9 100644 --- a/modules/gltf/structures/gltf_camera.cpp +++ b/modules/gltf/structures/gltf_camera.cpp @@ -30,6 +30,7 @@ #include "gltf_camera.h" +#include "gltf_object_model_property.h" #include "scene/3d/camera_3d.h" void GLTFCamera::_bind_methods() { @@ -57,6 +58,21 @@ void GLTFCamera::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "depth_near"), "set_depth_near", "get_depth_near"); } +void GLTFCamera::set_fov_conversion_expressions(Ref &r_obj_model_prop) { + // Expression to convert glTF yfov in radians to Godot fov in degrees. + Ref gltf_to_godot_expr; + gltf_to_godot_expr.instantiate(); + PackedStringArray gltf_to_godot_args = { "yfov_rad" }; + gltf_to_godot_expr->parse("rad_to_deg(yfov_rad)", gltf_to_godot_args); + r_obj_model_prop->set_gltf_to_godot_expression(gltf_to_godot_expr); + // Expression to convert Godot fov in degrees to glTF yfov in radians. + Ref godot_to_gltf_expr; + godot_to_gltf_expr.instantiate(); + PackedStringArray godot_to_gltf_args = { "fov_deg" }; + godot_to_gltf_expr->parse("deg_to_rad(fov_deg)", godot_to_gltf_args); + r_obj_model_prop->set_godot_to_gltf_expression(godot_to_gltf_expr); +} + Ref GLTFCamera::from_node(const Camera3D *p_camera) { Ref c; c.instantiate(); diff --git a/modules/gltf/structures/gltf_camera.h b/modules/gltf/structures/gltf_camera.h index 1a583c82ccb..497b6cd4f19 100644 --- a/modules/gltf/structures/gltf_camera.h +++ b/modules/gltf/structures/gltf_camera.h @@ -34,6 +34,7 @@ #include "core/io/resource.h" class Camera3D; +class GLTFObjectModelProperty; // Reference and test file: // https://github.com/KhronosGroup/glTF-Tutorials/blob/master/gltfTutorial/gltfTutorial_015_SimpleCameras.md @@ -54,6 +55,8 @@ protected: static void _bind_methods(); public: + static void set_fov_conversion_expressions(Ref &r_obj_model_prop); + bool get_perspective() const { return perspective; } void set_perspective(bool p_val) { perspective = p_val; } real_t get_fov() const { return fov; } diff --git a/modules/gltf/structures/gltf_node.cpp b/modules/gltf/structures/gltf_node.cpp index ccee5e8ca43..1626313551e 100644 --- a/modules/gltf/structures/gltf_node.cpp +++ b/modules/gltf/structures/gltf_node.cpp @@ -30,6 +30,8 @@ #include "gltf_node.h" +#include "../gltf_state.h" + void GLTFNode::_bind_methods() { ClassDB::bind_method(D_METHOD("get_original_name"), &GLTFNode::get_original_name); ClassDB::bind_method(D_METHOD("set_original_name", "original_name"), &GLTFNode::set_original_name); @@ -60,6 +62,7 @@ void GLTFNode::_bind_methods() { ClassDB::bind_method(D_METHOD("set_light", "light"), &GLTFNode::set_light); ClassDB::bind_method(D_METHOD("get_additional_data", "extension_name"), &GLTFNode::get_additional_data); ClassDB::bind_method(D_METHOD("set_additional_data", "extension_name", "additional_data"), &GLTFNode::set_additional_data); + ClassDB::bind_method(D_METHOD("get_scene_node_path", "gltf_state", "handle_skeletons"), &GLTFNode::get_scene_node_path, DEFVAL(true)); ADD_PROPERTY(PropertyInfo(Variant::STRING, "original_name"), "set_original_name", "get_original_name"); // String ADD_PROPERTY(PropertyInfo(Variant::INT, "parent"), "set_parent", "get_parent"); // GLTFNodeIndex @@ -187,6 +190,48 @@ Variant GLTFNode::get_additional_data(const StringName &p_extension_name) { return additional_data[p_extension_name]; } +bool GLTFNode::has_additional_data(const StringName &p_extension_name) { + return additional_data.has(p_extension_name); +} + void GLTFNode::set_additional_data(const StringName &p_extension_name, Variant p_additional_data) { additional_data[p_extension_name] = p_additional_data; } + +NodePath GLTFNode::get_scene_node_path(Ref p_state, bool p_handle_skeletons) { + Vector path; + Vector subpath; + Ref current_gltf_node = this; + const int gltf_node_count = p_state->nodes.size(); + if (p_handle_skeletons && skeleton != -1) { + // Special case for skeleton nodes, skip all bones so that the path is to the Skeleton3D node. + // A path that would otherwise be `A/B/C/Bone1/Bone2/Bone3` becomes `A/B/C/Skeleton3D:Bone3`. + subpath.append(get_name()); + // The generated Skeleton3D node will be named Skeleton3D, so add it to the path. + path.append("Skeleton3D"); + do { + const int parent_index = current_gltf_node->get_parent(); + ERR_FAIL_INDEX_V(parent_index, gltf_node_count, NodePath()); + current_gltf_node = p_state->nodes[parent_index]; + } while (current_gltf_node->skeleton != -1); + } + const bool is_godot_single_root = p_state->extensions_used.has("GODOT_single_root"); + while (true) { + const int parent_index = current_gltf_node->get_parent(); + if (is_godot_single_root && parent_index == -1) { + // For GODOT_single_root scenes, the root glTF node becomes the Godot scene root, so it + // should not be included in the path. Ex: A/B/C, A is single root, we want B/C only. + break; + } + path.insert(0, current_gltf_node->get_name()); + if (!is_godot_single_root && parent_index == -1) { + break; + } + ERR_FAIL_INDEX_V(parent_index, gltf_node_count, NodePath()); + current_gltf_node = p_state->nodes[parent_index]; + } + if (unlikely(path.is_empty())) { + path.append("."); + } + return NodePath(path, subpath, false); +} diff --git a/modules/gltf/structures/gltf_node.h b/modules/gltf/structures/gltf_node.h index f3f6bfa2f16..f72b65a0036 100644 --- a/modules/gltf/structures/gltf_node.h +++ b/modules/gltf/structures/gltf_node.h @@ -103,7 +103,10 @@ public: void set_light(GLTFLightIndex p_light); Variant get_additional_data(const StringName &p_extension_name); + bool has_additional_data(const StringName &p_extension_name); void set_additional_data(const StringName &p_extension_name, Variant p_additional_data); + + NodePath get_scene_node_path(Ref p_state, bool p_handle_skeletons = true); }; #endif // GLTF_NODE_H diff --git a/modules/gltf/structures/gltf_object_model_property.cpp b/modules/gltf/structures/gltf_object_model_property.cpp new file mode 100644 index 00000000000..d405c362db3 --- /dev/null +++ b/modules/gltf/structures/gltf_object_model_property.cpp @@ -0,0 +1,173 @@ +/**************************************************************************/ +/* gltf_object_model_property.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "gltf_object_model_property.h" + +#include "../gltf_template_convert.h" + +void GLTFObjectModelProperty::_bind_methods() { + BIND_ENUM_CONSTANT(GLTF_OBJECT_MODEL_TYPE_UNKNOWN); + BIND_ENUM_CONSTANT(GLTF_OBJECT_MODEL_TYPE_BOOL); + BIND_ENUM_CONSTANT(GLTF_OBJECT_MODEL_TYPE_FLOAT); + BIND_ENUM_CONSTANT(GLTF_OBJECT_MODEL_TYPE_FLOAT_ARRAY); + BIND_ENUM_CONSTANT(GLTF_OBJECT_MODEL_TYPE_FLOAT2); + BIND_ENUM_CONSTANT(GLTF_OBJECT_MODEL_TYPE_FLOAT3); + BIND_ENUM_CONSTANT(GLTF_OBJECT_MODEL_TYPE_FLOAT4); + BIND_ENUM_CONSTANT(GLTF_OBJECT_MODEL_TYPE_FLOAT2X2); + BIND_ENUM_CONSTANT(GLTF_OBJECT_MODEL_TYPE_FLOAT3X3); + BIND_ENUM_CONSTANT(GLTF_OBJECT_MODEL_TYPE_FLOAT4X4); + BIND_ENUM_CONSTANT(GLTF_OBJECT_MODEL_TYPE_INT); + + ClassDB::bind_method(D_METHOD("append_node_path", "node_path"), &GLTFObjectModelProperty::append_node_path); + ClassDB::bind_method(D_METHOD("append_path_to_property", "node_path", "prop_name"), &GLTFObjectModelProperty::append_path_to_property); + + ClassDB::bind_method(D_METHOD("get_accessor_type"), &GLTFObjectModelProperty::get_accessor_type); + ClassDB::bind_method(D_METHOD("get_gltf_to_godot_expression"), &GLTFObjectModelProperty::get_gltf_to_godot_expression); + ClassDB::bind_method(D_METHOD("set_gltf_to_godot_expression", "gltf_to_godot_expr"), &GLTFObjectModelProperty::set_gltf_to_godot_expression); + ClassDB::bind_method(D_METHOD("get_godot_to_gltf_expression"), &GLTFObjectModelProperty::get_godot_to_gltf_expression); + ClassDB::bind_method(D_METHOD("set_godot_to_gltf_expression", "godot_to_gltf_expr"), &GLTFObjectModelProperty::set_godot_to_gltf_expression); + ClassDB::bind_method(D_METHOD("get_node_paths"), &GLTFObjectModelProperty::get_node_paths); + ClassDB::bind_method(D_METHOD("has_node_paths"), &GLTFObjectModelProperty::has_node_paths); + ClassDB::bind_method(D_METHOD("set_node_paths", "node_paths"), &GLTFObjectModelProperty::set_node_paths); + ClassDB::bind_method(D_METHOD("get_object_model_type"), &GLTFObjectModelProperty::get_object_model_type); + ClassDB::bind_method(D_METHOD("set_object_model_type", "type"), &GLTFObjectModelProperty::set_object_model_type); + ClassDB::bind_method(D_METHOD("get_json_pointers"), &GLTFObjectModelProperty::get_json_pointers_bind); + ClassDB::bind_method(D_METHOD("has_json_pointers"), &GLTFObjectModelProperty::has_json_pointers); + ClassDB::bind_method(D_METHOD("set_json_pointers", "json_pointers"), &GLTFObjectModelProperty::set_json_pointers_bind); + ClassDB::bind_method(D_METHOD("get_variant_type"), &GLTFObjectModelProperty::get_variant_type); + ClassDB::bind_method(D_METHOD("set_variant_type", "variant_type"), &GLTFObjectModelProperty::set_variant_type); + ClassDB::bind_method(D_METHOD("set_types", "variant_type", "obj_model_type"), &GLTFObjectModelProperty::set_types); + + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "gltf_to_godot_expression", PROPERTY_HINT_RESOURCE_TYPE, "Expression"), "set_gltf_to_godot_expression", "get_gltf_to_godot_expression"); // Ref + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "godot_to_gltf_expression", PROPERTY_HINT_RESOURCE_TYPE, "Expression"), "set_godot_to_gltf_expression", "get_godot_to_gltf_expression"); // Ref + ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "node_paths", PROPERTY_HINT_TYPE_STRING, "NodePath"), "set_node_paths", "get_node_paths"); // TypedArray + ADD_PROPERTY(PropertyInfo(Variant::INT, "object_model_type"), "set_object_model_type", "get_object_model_type"); // GLTFObjectModelType + ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "json_pointers"), "set_json_pointers", "get_json_pointers"); // TypedArray + ADD_PROPERTY(PropertyInfo(Variant::INT, "variant_type"), "set_variant_type", "get_variant_type"); // Variant::Type +} + +void GLTFObjectModelProperty::append_node_path(const NodePath &p_node_path) { + node_paths.push_back(p_node_path); +} + +void GLTFObjectModelProperty::append_path_to_property(const NodePath &p_node_path, const StringName &p_prop_name) { + Vector node_names = p_node_path.get_names(); + Vector subpath = p_node_path.get_subnames(); + subpath.append(p_prop_name); + node_paths.push_back(NodePath(node_names, subpath, false)); +} + +GLTFAccessor::GLTFAccessorType GLTFObjectModelProperty::get_accessor_type() const { + switch (object_model_type) { + case GLTF_OBJECT_MODEL_TYPE_FLOAT2: + return GLTFAccessor::TYPE_VEC2; + case GLTF_OBJECT_MODEL_TYPE_FLOAT3: + return GLTFAccessor::TYPE_VEC3; + case GLTF_OBJECT_MODEL_TYPE_FLOAT4: + return GLTFAccessor::TYPE_VEC4; + case GLTF_OBJECT_MODEL_TYPE_FLOAT2X2: + return GLTFAccessor::TYPE_MAT2; + case GLTF_OBJECT_MODEL_TYPE_FLOAT3X3: + return GLTFAccessor::TYPE_MAT3; + case GLTF_OBJECT_MODEL_TYPE_FLOAT4X4: + return GLTFAccessor::TYPE_MAT4; + default: + return GLTFAccessor::TYPE_SCALAR; + } +} + +Ref GLTFObjectModelProperty::get_gltf_to_godot_expression() const { + return gltf_to_godot_expr; +} + +void GLTFObjectModelProperty::set_gltf_to_godot_expression(Ref p_gltf_to_godot_expr) { + gltf_to_godot_expr = p_gltf_to_godot_expr; +} + +Ref GLTFObjectModelProperty::get_godot_to_gltf_expression() const { + return godot_to_gltf_expr; +} + +void GLTFObjectModelProperty::set_godot_to_gltf_expression(Ref p_godot_to_gltf_expr) { + godot_to_gltf_expr = p_godot_to_gltf_expr; +} + +TypedArray GLTFObjectModelProperty::get_node_paths() const { + return node_paths; +} + +bool GLTFObjectModelProperty::has_node_paths() const { + return !node_paths.is_empty(); +} + +void GLTFObjectModelProperty::set_node_paths(TypedArray p_node_paths) { + node_paths = p_node_paths; +} + +GLTFObjectModelProperty::GLTFObjectModelType GLTFObjectModelProperty::get_object_model_type() const { + return object_model_type; +} + +void GLTFObjectModelProperty::set_object_model_type(GLTFObjectModelType p_type) { + object_model_type = p_type; +} + +Vector GLTFObjectModelProperty::get_json_pointers() const { + return json_pointers; +} + +bool GLTFObjectModelProperty::has_json_pointers() const { + return !json_pointers.is_empty(); +} + +void GLTFObjectModelProperty::set_json_pointers(const Vector &p_json_pointers) { + json_pointers = p_json_pointers; +} + +TypedArray GLTFObjectModelProperty::get_json_pointers_bind() const { + return GLTFTemplateConvert::to_array(json_pointers); +} + +void GLTFObjectModelProperty::set_json_pointers_bind(const TypedArray &p_json_pointers) { + GLTFTemplateConvert::set_from_array(json_pointers, p_json_pointers); +} + +Variant::Type GLTFObjectModelProperty::get_variant_type() const { + return variant_type; +} + +void GLTFObjectModelProperty::set_variant_type(Variant::Type p_variant_type) { + variant_type = p_variant_type; +} + +void GLTFObjectModelProperty::set_types(Variant::Type p_variant_type, GLTFObjectModelType p_obj_model_type) { + variant_type = p_variant_type; + object_model_type = p_obj_model_type; +} diff --git a/modules/gltf/structures/gltf_object_model_property.h b/modules/gltf/structures/gltf_object_model_property.h new file mode 100644 index 00000000000..d8a4ed420a3 --- /dev/null +++ b/modules/gltf/structures/gltf_object_model_property.h @@ -0,0 +1,104 @@ +/**************************************************************************/ +/* gltf_object_model_property.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef GLTF_OBJECT_MODEL_PROPERTY_H +#define GLTF_OBJECT_MODEL_PROPERTY_H + +#include "core/math/expression.h" +#include "core/variant/typed_array.h" +#include "gltf_accessor.h" + +// Object model: https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/ObjectModel.adoc +// KHR_animation_pointer: https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_animation_pointer + +class GLTFObjectModelProperty : public RefCounted { + GDCLASS(GLTFObjectModelProperty, RefCounted); + +public: + enum GLTFObjectModelType { + GLTF_OBJECT_MODEL_TYPE_UNKNOWN, + GLTF_OBJECT_MODEL_TYPE_BOOL, + GLTF_OBJECT_MODEL_TYPE_FLOAT, + GLTF_OBJECT_MODEL_TYPE_FLOAT_ARRAY, + GLTF_OBJECT_MODEL_TYPE_FLOAT2, + GLTF_OBJECT_MODEL_TYPE_FLOAT3, + GLTF_OBJECT_MODEL_TYPE_FLOAT4, + GLTF_OBJECT_MODEL_TYPE_FLOAT2X2, + GLTF_OBJECT_MODEL_TYPE_FLOAT3X3, + GLTF_OBJECT_MODEL_TYPE_FLOAT4X4, + GLTF_OBJECT_MODEL_TYPE_INT, + }; + +private: + Ref gltf_to_godot_expr; + Ref godot_to_gltf_expr; + TypedArray node_paths; + GLTFObjectModelType object_model_type = GLTF_OBJECT_MODEL_TYPE_UNKNOWN; + Vector json_pointers; + Variant::Type variant_type = Variant::NIL; + +protected: + static void _bind_methods(); + +public: + void append_node_path(const NodePath &p_node_path); + void append_path_to_property(const NodePath &p_node_path, const StringName &p_prop_name); + + GLTFAccessor::GLTFAccessorType get_accessor_type() const; + + Ref get_gltf_to_godot_expression() const; + void set_gltf_to_godot_expression(Ref p_gltf_to_godot_expr); + + Ref get_godot_to_gltf_expression() const; + void set_godot_to_gltf_expression(Ref p_godot_to_gltf_expr); + + TypedArray get_node_paths() const; + bool has_node_paths() const; + void set_node_paths(TypedArray p_node_paths); + + GLTFObjectModelType get_object_model_type() const; + void set_object_model_type(GLTFObjectModelType p_type); + + Vector get_json_pointers() const; + bool has_json_pointers() const; + void set_json_pointers(const Vector &p_json_pointers); + + TypedArray get_json_pointers_bind() const; + void set_json_pointers_bind(const TypedArray &p_json_pointers); + + Variant::Type get_variant_type() const; + void set_variant_type(Variant::Type p_variant_type); + + void set_types(Variant::Type p_variant_type, GLTFObjectModelType p_obj_model_type); +}; + +VARIANT_ENUM_CAST(GLTFObjectModelProperty::GLTFObjectModelType); + +#endif // GLTF_OBJECT_MODEL_PROPERTY_H