using System; using System.Collections.Generic; using System.Data.Common; using System.Reflection; using JetBrains.Annotations; using Needle.Engine.Core; using Needle.Engine.Problems; using Needle.Engine.Utils; using UnityEditor.Animations; using UnityEngine; using Object = UnityEngine.Object; namespace Needle.Engine.Gltf { [UsedImplicitly] public class AnimatorControllerHandler : GltfExtensionHandlerBase { public override void OnBeforeExport(GltfExportContext context) { base.OnBeforeExport(context); context.RegisterValueResolver(new AnimatorControllerSerializer()); } } internal class AnimatorControllerSerializer : IValueResolver { public bool TryGetValue(IExportContext ctx, object instance, MemberInfo member, ref object value) { var bridge = (ctx as GltfExportContext)?.Bridge; if (value is AnimatorController ctr) { var transform = (instance as Component)?.transform; value = new AnimatorControllerModel(ctr, bridge, transform, ctx); return true; } // if (value is AnimatorOverrideController) { value = null; return true; } return false; } [Serializable] public class AnimatorControllerModel : ISerializablePersistentAssetModel { [NonSerialized] private readonly AnimatorController controller; [NonSerialized] private List motions = new List(); public void OnNewObjectDiscovered(Object asset, object owner, MemberInfo member, IExportContext context) { var bridge = (context as GltfExportContext)?.Bridge; if (bridge != null && asset == controller && owner is Component component) { var transform = component.transform; foreach (var motion in motions) { if (motion._clip == null) continue; bridge.AddAnimationClip(motion._clip, transform, 1); var mapping = motion.clips; var newMapping = new ClipNodeMapping(motion._clip, bridge, transform); mapping.Add(newMapping); } } } public AnimatorControllerModel(AnimatorController controller, IGltfBridge bridge, Transform transform, IExportContext context) { this.controller = controller; name = controller.name; guid = controller.GetId(); foreach (var param in controller.parameters) { var p = new ParameterModel(); parameters.Add(p); p.name = param.name; p.type = param.type; p.hash = Animator.StringToHash(param.name); switch (param.type) { case AnimatorControllerParameterType.Float: p.value = param.defaultFloat; break; case AnimatorControllerParameterType.Int: p.value = param.defaultInt; break; case AnimatorControllerParameterType.Bool: p.value = param.defaultBool; break; case AnimatorControllerParameterType.Trigger: p.value = param.defaultBool; break; } } // const int k_maxStatesAfterTrial = 2; // var trialHasEnded = NeedleEngineAuthorization.TrialEnded; // var isInTrial = NeedleEngineAuthorization.IsInTrialPeriod; var totalStates = 0; // Export layers foreach (var layer in controller.layers) { var layerObj = new LayerModel(); layerObj.name = layer.name; layers.Add(layerObj); var stateMachine = layer.stateMachine; var stateMachineModel = new StateMachineModel(); layerObj.stateMachine = stateMachineModel; stateMachineModel.name = stateMachine.name; // if (isInTrial && !LicenseCheck.HasLicense) // { // if (stateMachine.states.Length > k_maxStatesAfterTrial) // { // var msg = // $"AnimatorController {controller.name} has more than {k_maxStatesAfterTrial} states. Upgrade to an Indie or Pro Plan of Needle to export more than {k_maxStatesAfterTrial} states after your Pro trial has ended."; // Debug.LogWarning(msg, controller); // BuildResultInformation.ReportBuildProblem(msg, controller, LicenseType.Indie); // } // } for (var index = 0; index < stateMachine.states.Length; index++) { var stateEntry = stateMachine.states[index]; // if (LicenseCheck.HasLicense == false) // { // if (trialHasEnded && totalStates >= k_maxStatesAfterTrial) // { // var message = // $"AnimatorController {controller.name} has more than {k_maxStatesAfterTrial} states. Upgrade to an Indie or Pro Plan of Needle to export AnimatorControllers with more than {k_maxStatesAfterTrial} animation states."; // Debug.LogWarning(message, controller); // var info = new BuildResultInformation($"AnimatorController \"{controller.name}\" has {stateMachine.states.Length} animation states", controller, ProblemSeverity.Error); // info.ActionDescription = "Purchase a commercial license or reduce to 2 states"; // BuildResultInformation.Report(info); // break; // } // } totalStates += 1; var state = stateEntry.state; var stateModel = new StateModel(); stateMachineModel.states.Add(stateModel); stateModel.name = state.name; stateModel.hash = Animator.StringToHash(state.name); stateModel.speed = state.speed; if (state.speedParameterActive) stateModel.speedParameter = state.speedParameter; state.cycleOffset = state.cycleOffset; if (state.cycleOffsetParameterActive) stateModel.cycleOffsetParameter = state.cycleOffsetParameter; if (state == stateMachine.defaultState) stateMachineModel.defaultState = index; var motion = state.motion; if (motion) { var motionModel = new MotionModel(); motions.Add(motionModel); stateModel.motion = motionModel; motionModel.name = motion.name; motionModel.isLooping = motion.isLooping; motionModel.guid = motion.GetId(); switch (motion) { case AnimationClip clip: // clips.Add(controller, clip); if (context is GltfExportContext ctx) { ctx.Bridge.AddAnimationClip(clip, transform, 1); } motionModel._clip = clip; var clipMapping = new ClipNodeMapping(clip, bridge, transform); motionModel.clips.Add(clipMapping); break; } } foreach (var behaviour in state.behaviours) { if (context.TypeRegistry.IsInstalled(behaviour.GetType())) stateModel.behaviours.Add(new StateMachineBehaviourModel(behaviour)); } if (stateMachine.anyStateTransitions != null) { // ReSharper disable once CoVariantArrayConversion var anyStateTransitions = CreateTransitionsArray(true, stateMachine.anyStateTransitions, stateMachine.states); stateModel.transitions.AddRange(anyStateTransitions); } // ReSharper disable once CoVariantArrayConversion stateModel.transitions.AddRange(CreateTransitionsArray(false, state.transitions, stateMachine.states)); } // ReSharper disable once CoVariantArrayConversion stateMachineModel.entryTransitions.AddRange(CreateTransitionsArray(false, stateMachine.entryTransitions, stateMachine.states)); } } private static IEnumerable CreateTransitionsArray(bool isAny, AnimatorTransitionBase[] transitions, ChildAnimatorState[] states) { foreach (var transition in transitions) { var transitionModel = new TransitionModel(); transitionModel.isExit = transition.isExit; // transitionModel.isAny = isAny; if (transition is AnimatorStateTransition stateTransition) { transitionModel.exitTime = stateTransition.exitTime; transitionModel.hasFixedDuration = stateTransition.hasFixedDuration; transitionModel.offset = stateTransition.offset; transitionModel.duration = stateTransition.duration; transitionModel.hasExitTime = stateTransition.hasExitTime; } // find destination state index var found = false; var destStates = transition.destinationStateMachine?.states ?? states; for (var i = 0; i < destStates.Length; i++) { if (found) break; var dest = destStates[i].state; if (dest != transition.destinationState) continue; transitionModel.destinationState = i; found = true; } if (!found) transitionModel.destinationState = -1; transitionModel.conditions = new List(); foreach (var condition in transition.conditions) { var conditionModel = new ConditionModel(); conditionModel.parameter = condition.parameter; conditionModel.mode = condition.mode; conditionModel.threshold = condition.threshold; transitionModel.conditions.Add(conditionModel); } yield return transitionModel; } } public string name; public string guid; public List parameters = new List(); public List layers = new List(); [Serializable] public class ParameterModel { public string name; public AnimatorControllerParameterType type; public int hash; public object value; } [Serializable] public class LayerModel { public string name; public StateMachineModel stateMachine = new StateMachineModel(); } [Serializable] public class StateMachineModel { public string name; public int defaultState; public List states = new List(); public List entryTransitions = new List(); } [Serializable] public class StateModel { public string name; public int hash; public MotionModel motion; public List transitions = new List(); public List behaviours = new List(); public float speed; public string speedParameter; public float cycleOffset; public string cycleOffsetParameter; } [Serializable] public class MotionModel { public string name; public bool isLooping; public string guid; /// /// Used if multiple animators use the same animator controller /// We dont need to copy the whole controller in the extension /// But instead can store the clip id / pointer per transform id (node id) /// public List clips = new List(); [NonSerialized] [CanBeNull] public AnimationClip _clip; } public class ClipNodeMapping { public readonly string node; public readonly string clip; public ClipNodeMapping(AnimationClip clip, IGltfBridge bridge, Transform transform) { this.node = bridge.TryGetNodeId(transform).AsNodeJsonPointer(); var id = bridge.TryGetAnimationId(clip, transform); if (id < 0) Debug.LogWarning("AnimationClip could not be exported: " + clip.name, transform); this.clip = id.AsAnimationPointer(); } } [Serializable] public class TransitionModel { public bool isExit; public float exitTime; public bool hasFixedDuration; public float offset; public float duration; public bool hasExitTime; public int destinationState; public List conditions = new List(); public bool isAny; } [Serializable] public class ConditionModel { public string parameter; public AnimatorConditionMode mode; public float threshold; } [Serializable] public class StateMachineBehaviourModel { public string typeName; public StateMachineBehaviour properties; public StateMachineBehaviourModel(StateMachineBehaviour stateMachineBehaviour) { typeName = stateMachineBehaviour.GetType().Name; properties = stateMachineBehaviour; } } } } }