Repository: khronosgroup/gltf-blender-io
Issue: Duplicate rotation channel

Duplicate rotation channel when both Euler and Delta Euler are keyframed.
See https://developer.blender.org/T77842

Changed files:
addons/io_scene_gltf2/blender/com/gltf2_blender_data_path.py, addons/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channels.py

diff --git a/addons/io_scene_gltf2/blender/com/gltf2_blender_data_path.py b/addons/io_scene_gltf2/blender/com/gltf2_blender_data_path.py
index a4cb6625..549390a0 100644
--- a/addons/io_scene_gltf2/blender/com/gltf2_blender_data_path.py
+++ b/addons/io_scene_gltf2/blender/com/gltf2_blender_data_path.py
@@ -28,11 +28,15 @@ def get_target_object_path(data_path: str) -> str:
 
 def get_rotation_modes(target_property: str) -> str:
     """Retrieve rotation modes based on target_property"""
-    if target_property in ["rotation_euler", "delta_rotation_euler"]:
-        return True, ["XYZ", "XZY", "YXZ", "YZX", "ZXY", "ZYX"]
-    elif target_property in ["rotation_quaternion", "delta_rotation_quaternion"]:
-        return True, ["QUATERNION"]
+    if target_property == "rotation_euler":
+        return True, False, ["XYZ", "XZY", "YXZ", "YZX", "ZXY", "ZYX"]
+    elif target_property == "delta_rotation_euler":
+        return True, True, ["XYZ", "XZY", "YXZ", "YZX", "ZXY", "ZYX"]
+    elif target_property == "rotation_quaternion":
+        return True, False, ["QUATERNION"]
+    elif target_property == "delta_rotation_quaternion":
+        return True, True, ["QUATERNION"]
     elif target_property in ["rotation_axis_angle"]:
-        return True, ["AXIS_ANGLE"]
+        return True, False, ["AXIS_ANGLE"]
     else:
-        return False, []
+        return False, False, []
diff --git a/addons/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channels.py b/addons/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channels.py
index 0917577a..cde69453 100644
--- a/addons/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channels.py
+++ b/addons/io_scene_gltf2/blender/exp/gltf2_blender_gather_animation_channels.py
@@ -278,6 +278,7 @@ def __gather_target(channels: typing.Tuple[bpy.types.FCurve],
 def __get_channel_groups(blender_action: bpy.types.Action, blender_object: bpy.types.Object, export_settings):
     targets = {}
     multiple_rotation_mode_detected = False
+    delta_rotation_detection = [False, False] # Normal / Delta
     for fcurve in blender_action.fcurves:
         # In some invalid files, channel hasn't any keyframes ... this channel need to be ignored
         if len(fcurve.keyframe_points) == 0:
@@ -318,8 +319,22 @@ def __get_channel_groups(blender_action: bpy.types.Action, blender_object: bpy.t
                     continue
 
         # Detect that object or bone are not multiple keyed for euler and quaternion
-        # Keep only the current rotation mode used by object / bone
-        rotation, rotation_modes = get_rotation_modes(target_property)
+        # Keep only the current rotation mode used by object
+        rotation, delta, rotation_modes = get_rotation_modes(target_property)
+
+        # Delta rotation management
+        if delta is False:
+            if delta_rotation_detection[1] is True: # normal rotation coming, but delta is already present
+                multiple_rotation_mode_detected = True
+                continue
+            delta_rotation_detection[0] = True
+        else:
+            if delta_rotation_detection[0] is True: # delta rotation coming, but normal is already present
+                multiple_rotation_mode_detected = True
+                continue
+            delta_rotation_detection[1] = True
+
+
         if rotation and target.rotation_mode not in rotation_modes:
             multiple_rotation_mode_detected = True
             continue
@@ -347,6 +362,8 @@ def __gather_armature_object_channel_groups(blender_action: bpy.types.Action, bl
     if blender_object.type != "ARMATURE":
         return tuple()
 
+    delta_rotation_detection = [False, False] # Normal / Delta
+
     for fcurve in blender_action.fcurves:
         object_path = get_target_object_path(fcurve.data_path)
         if object_path != "":
@@ -363,8 +380,19 @@ def __gather_armature_object_channel_groups(blender_action: bpy.types.Action, bl
         target = gltf2_blender_get.get_object_from_datapath(blender_object, object_path)
 
         # Detect that armature is not multiple keyed for euler and quaternion
-        # Keep only the current rotation mode used by object
-        rotation, rotation_modes = get_rotation_modes(target_property)
+        # Keep only the current rotation mode used by bone
+        rotation, delta, rotation_modes = get_rotation_modes(target_property)
+
+        # Delta rotation management
+        if delta is False:
+            if delta_rotation_detection[1] is True: # normal rotation coming, but delta is already present
+                continue
+            delta_rotation_detection[0] = True
+        else:
+            if delta_rotation_detection[0] is True: # delta rotation coming, but normal is already present
+                continue
+            delta_rotation_detection[1] = True
+
         if rotation and target.rotation_mode not in rotation_modes:
             continue