diff --git a/CHANGELOG.md b/CHANGELOG.md index acac4b6ad..df757b61d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -163,6 +163,7 @@ - SkeletonGraphic: You can now offset the skeleton mesh relative to the pivot via a newly added green circle handle. This allows you to e.g. frame only the face of a skeleton inside a masked frame. Previously offsetting the pivot downwards fails when `Layout Scale Mode` scales the mesh smaller and towards the pivot (e.g. the feet) and thus out of the frame. Now you can keep the pivot in the center of the `RectTransform` while offsetting only the mesh downwards, keeping the desired skeleton area (e.g. the face) centered while resizing. Moving the new larger green circle handle moves the mesh offset, while moving the blue pivot circle handle moves the pivot as usual. - `Universal Render Pipeline/Spine/Skeleton` shader now performs proper alpha-testing when `Depth Write` is enabled, using the existing `Shadow alpha cutoff` parameter. - `SkeletonRootMotion` components now provide a public `Initialize()` method which is automatically called when calling `skeletonAnimation.Initialize(true)` to update the necessary skeleton references. If a different root bone shall be used, be sure to set `skeletonRootMotion.rootMotionBoneName` before calling `skeletonAnimation.Initialize(true)`. + - Skeleton Mecanim: Added new `Mix Mode` `Match`. When selected, Spine animation weights are calculated to best match the provided Mecanim clip weights. This mix mode is recommended on any layer using blend tree nodes. - **Breaking changes** diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonMecanim.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonMecanim.cs index aa68a59a1..8687e1883 100644 --- a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonMecanim.cs +++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonMecanim.cs @@ -210,7 +210,7 @@ namespace Spine.Unity { public event OnClipAppliedDelegate OnClipApplied { add { _OnClipApplied += value; } remove { _OnClipApplied -= value; } } - public enum MixMode { AlwaysMix, MixNext, Hard } + public enum MixMode { AlwaysMix, MixNext, Hard, Match } readonly Dictionary animationTable = new Dictionary(IntEqualityComparer.Instance); readonly Dictionary clipNameHashCodeTable = new Dictionary(AnimationClipEqualityComparer.Instance); @@ -226,6 +226,9 @@ namespace Spine.Unity { public readonly List clipInfos = new List(); public readonly List nextClipInfos = new List(); public readonly List interruptingClipInfos = new List(); + public float[] clipResolvedWeights = new float[0]; + public float[] nextClipResolvedWeights = new float[0]; + public float[] interruptingClipResolvedWeights = new float[0]; public AnimatorStateInfo stateInfo; public AnimatorStateInfo nextStateInfo; @@ -273,7 +276,8 @@ namespace Spine.Unity { } private bool ApplyAnimation (Skeleton skeleton, AnimatorClipInfo info, AnimatorStateInfo stateInfo, - int layerIndex, float layerWeight, MixBlend layerBlendMode, bool useClipWeight1 = false) { + int layerIndex, float layerWeight, MixBlend layerBlendMode, + bool useCustomClipWeight = false, float customClipWeight = 1.0f) { float weight = info.weight * layerWeight; if (weight < WeightEpsilon) return false; @@ -283,7 +287,7 @@ namespace Spine.Unity { return false; float time = AnimationTime(stateInfo.normalizedTime, info.clip.length, info.clip.isLooping, stateInfo.speed < 0); - weight = useClipWeight1 ? layerWeight : weight; + weight = useCustomClipWeight ? layerWeight * customClipWeight : weight; clip.Apply(skeleton, 0, time, info.clip.isLooping, null, weight, layerBlendMode, MixDirection.In); if (_OnClipApplied != null) @@ -294,7 +298,7 @@ namespace Spine.Unity { private bool ApplyInterruptionAnimation (Skeleton skeleton, bool interpolateWeightTo1, AnimatorClipInfo info, AnimatorStateInfo stateInfo, int layerIndex, float layerWeight, MixBlend layerBlendMode, float interruptingClipTimeAddition, - bool useClipWeight1 = false) { + bool useCustomClipWeight = false, float customClipWeight = 1.0f) { float clipWeight = interpolateWeightTo1 ? (info.weight + 1.0f) * 0.5f : info.weight; float weight = clipWeight * layerWeight; @@ -307,7 +311,7 @@ namespace Spine.Unity { float time = AnimationTime(stateInfo.normalizedTime + interruptingClipTimeAddition, info.clip.length, info.clip.isLooping, stateInfo.speed < 0); - weight = useClipWeight1 ? layerWeight : weight; + weight = useCustomClipWeight ? layerWeight * customClipWeight : weight; clip.Apply(skeleton, 0, time, info.clip.isLooping, null, weight, layerBlendMode, MixDirection.In); if (_OnClipApplied != null) { @@ -442,11 +446,39 @@ namespace Spine.Unity { layer, layerWeight, layerBlendMode, interruptingClipTimeAddition); } } + } else if (mode == MixMode.Match) { + // Calculate matching Spine lerp(lerp(A, B, w2), C, w3) weights + // from Unity's absolute weights A*W1 + B*W2 + C*W3. + MatchWeights(layerClipInfos[layer], hasNext, isInterruptionActive, clipInfoCount, nextClipInfoCount, interruptingClipInfoCount, + clipInfo, nextClipInfo, interruptingClipInfo); + + float[] customWeights = layerClipInfos[layer].clipResolvedWeights; + for (int c = 0; c < clipInfoCount; c++) { + ApplyAnimation(skeleton, clipInfo[c], stateInfo, layer, layerWeight, layerBlendMode, + useCustomClipWeight: true, customWeights[c]); + } + if (hasNext) { + customWeights = layerClipInfos[layer].nextClipResolvedWeights; + for (int c = 0; c < nextClipInfoCount; c++) { + ApplyAnimation(skeleton, nextClipInfo[c], nextStateInfo, layer, layerWeight, layerBlendMode, + useCustomClipWeight: true, customWeights[c]); + } + } + if (isInterruptionActive) { + customWeights = layerClipInfos[layer].interruptingClipResolvedWeights; + for (int c = 0; c < interruptingClipInfoCount; c++) { + ApplyInterruptionAnimation(skeleton, interpolateWeightTo1, + interruptingClipInfo[c], interruptingStateInfo, + layer, layerWeight, layerBlendMode, interruptingClipTimeAddition, + useCustomClipWeight: true, customWeights[c]); + } + } } else { // case MixNext || Hard // Apply first non-zero weighted clip int c = 0; for (; c < clipInfoCount; c++) { - if (!ApplyAnimation(skeleton, clipInfo[c], stateInfo, layer, layerWeight, layerBlendMode, useClipWeight1: true)) + if (!ApplyAnimation(skeleton, clipInfo[c], stateInfo, layer, layerWeight, layerBlendMode, + useCustomClipWeight: true, 1.0f)) continue; ++c; break; } @@ -460,7 +492,8 @@ namespace Spine.Unity { // Apply next clip directly instead of mixing (ie: no crossfade, ignores mecanim transition weights) if (mode == MixMode.Hard) { for (; c < nextClipInfoCount; c++) { - if (!ApplyAnimation(skeleton, nextClipInfo[c], nextStateInfo, layer, layerWeight, layerBlendMode, useClipWeight1: true)) + if (!ApplyAnimation(skeleton, nextClipInfo[c], nextStateInfo, layer, layerWeight, layerBlendMode, + useCustomClipWeight: true, 1.0f)) continue; ++c; break; } @@ -479,7 +512,7 @@ namespace Spine.Unity { for (; c < interruptingClipInfoCount; c++) { if (ApplyInterruptionAnimation(skeleton, interpolateWeightTo1, interruptingClipInfo[c], interruptingStateInfo, - layer, layerWeight, layerBlendMode, interruptingClipTimeAddition, useClipWeight1: true)) { + layer, layerWeight, layerBlendMode, interruptingClipTimeAddition, useCustomClipWeight: true, 1.0f)) { ++c; break; } @@ -496,6 +529,46 @@ namespace Spine.Unity { } } + /// + /// Resolve matching weights from Unity's absolute weights A*w1 + B*w2 + C*w3 to + /// Spine's lerp(lerp(A, B, x), C, y) weights, in reverse order of clips. + /// + protected void MatchWeights (ClipInfos clipInfos, bool hasNext, bool isInterruptionActive, + int clipInfoCount, int nextClipInfoCount, int interruptingClipInfoCount, + IList clipInfo, IList nextClipInfo, IList interruptingClipInfo) { + + if (clipInfos.clipResolvedWeights.Length < clipInfoCount) { + System.Array.Resize(ref clipInfos.clipResolvedWeights, clipInfoCount); + } + if (hasNext && clipInfos.nextClipResolvedWeights.Length < nextClipInfoCount) { + System.Array.Resize(ref clipInfos.nextClipResolvedWeights, nextClipInfoCount); + } + if (isInterruptionActive && clipInfos.interruptingClipResolvedWeights.Length < interruptingClipInfoCount) { + System.Array.Resize(ref clipInfos.interruptingClipResolvedWeights, interruptingClipInfoCount); + } + + float inverseWeight = 1.0f; + if (isInterruptionActive) { + for (int c = interruptingClipInfoCount - 1; c >= 0; c--) { + float unityWeight = interruptingClipInfo[c].weight; + clipInfos.interruptingClipResolvedWeights[c] = interruptingClipInfo[c].weight * inverseWeight; + inverseWeight /= (1.0f - unityWeight); + } + } + if (hasNext) { + for (int c = nextClipInfoCount - 1; c >= 0; c--) { + float unityWeight = nextClipInfo[c].weight; + clipInfos.nextClipResolvedWeights[c] = nextClipInfo[c].weight * inverseWeight; + inverseWeight /= (1.0f - unityWeight); + } + } + for (int c = clipInfoCount - 1; c >= 0; c--) { + float unityWeight = clipInfo[c].weight; + clipInfos.clipResolvedWeights[c] = (c == 0) ? 1f : clipInfo[c].weight * inverseWeight; + inverseWeight /= (1.0f - unityWeight); + } + } + public KeyValuePair GetActiveAnimationAndTime (int layer) { if (layer >= layerClipInfos.Length) return new KeyValuePair(null, 0); diff --git a/spine-unity/Assets/Spine/package.json b/spine-unity/Assets/Spine/package.json index 94b1c9a08..73f1bc39d 100644 --- a/spine-unity/Assets/Spine/package.json +++ b/spine-unity/Assets/Spine/package.json @@ -2,7 +2,7 @@ "name": "com.esotericsoftware.spine.spine-unity", "displayName": "spine-unity Runtime", "description": "This plugin provides the spine-unity runtime core.", - "version": "4.2.86", + "version": "4.2.87", "unity": "2018.3", "author": { "name": "Esoteric Software",