|
@@ -0,0 +1,355 @@
|
|
|
|
+/******************************************************************************
|
|
|
|
+ * Spine Runtimes License Agreement
|
|
|
|
+ * Last updated July 28, 2023. Replaces all prior versions.
|
|
|
|
+ *
|
|
|
|
+ * Copyright (c) 2013-2024, Esoteric Software LLC
|
|
|
|
+ *
|
|
|
|
+ * Integration of the Spine Runtimes into software or otherwise creating
|
|
|
|
+ * derivative works of the Spine Runtimes is permitted under the terms and
|
|
|
|
+ * conditions of Section 2 of the Spine Editor License Agreement:
|
|
|
|
+ * http://esotericsoftware.com/spine-editor-license
|
|
|
|
+ *
|
|
|
|
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
|
|
|
|
+ * otherwise create derivative works of the Spine Runtimes (collectively,
|
|
|
|
+ * "Products"), provided that each user of the Products must obtain their own
|
|
|
|
+ * Spine Editor license and redistribution of the Products in any form must
|
|
|
|
+ * include this license and copyright notice.
|
|
|
|
+ *
|
|
|
|
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
|
|
|
|
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
|
|
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
|
|
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
|
|
|
|
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
|
|
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
|
|
|
|
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
|
|
|
|
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
|
|
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
|
|
|
|
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
+ *****************************************************************************/
|
|
|
|
+
|
|
|
|
+using System;
|
|
|
|
+using Unity.Collections;
|
|
|
|
+using Unity.Jobs;
|
|
|
|
+using UnityEngine;
|
|
|
|
+using UnityEngine.Profiling;
|
|
|
|
+using UnityEngine.UIElements;
|
|
|
|
+using UIVertex = UnityEngine.UIElements.Vertex;
|
|
|
|
+
|
|
|
|
+namespace Spine.Unity {
|
|
|
|
+
|
|
|
|
+ [UxmlElement]
|
|
|
|
+ public partial class SpineVisualElement : VisualElement {
|
|
|
|
+ public new class UxmlFactory : UxmlFactory<SpineVisualElement, UxmlTraits> { }
|
|
|
|
+
|
|
|
|
+ [SpineAnimation(dataField: "SkeletonDataAsset", avoidGenericMenu: true)]
|
|
|
|
+ [UxmlAttribute]
|
|
|
|
+ public string StartingAnimation {
|
|
|
|
+ get { return startingAnimation; }
|
|
|
|
+ set {
|
|
|
|
+ if (startingAnimation == value) return;
|
|
|
|
+ startingAnimation = value;
|
|
|
|
+#if UNITY_EDITOR
|
|
|
|
+ if (!Application.isPlaying)
|
|
|
|
+ Initialize(true);
|
|
|
|
+#endif
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ public string startingAnimation = "";
|
|
|
|
+
|
|
|
|
+ [SpineSkin(dataField: "SkeletonDataAsset", avoidGenericMenu: true)]
|
|
|
|
+ [UxmlAttribute]
|
|
|
|
+ public string InitialSkinName {
|
|
|
|
+ get { return initialSkinName; }
|
|
|
|
+ set {
|
|
|
|
+ if (initialSkinName == value) return;
|
|
|
|
+ initialSkinName = value;
|
|
|
|
+#if UNITY_EDITOR
|
|
|
|
+ if (!Application.isPlaying)
|
|
|
|
+ Initialize(true);
|
|
|
|
+#endif
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ public string initialSkinName;
|
|
|
|
+
|
|
|
|
+ [UxmlAttribute] public bool startingLoop { get; set; } = true;
|
|
|
|
+ [UxmlAttribute] public float timeScale { get; set; } = 1.0f;
|
|
|
|
+
|
|
|
|
+ [UxmlAttribute]
|
|
|
|
+ public SkeletonDataAsset SkeletonDataAsset {
|
|
|
|
+ get { return skeletonDataAsset; }
|
|
|
|
+ set {
|
|
|
|
+ if (skeletonDataAsset == value) return;
|
|
|
|
+ skeletonDataAsset = value;
|
|
|
|
+#if UNITY_EDITOR
|
|
|
|
+ if (!Application.isPlaying)
|
|
|
|
+ Initialize(true);
|
|
|
|
+#endif
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ public SkeletonDataAsset skeletonDataAsset;
|
|
|
|
+
|
|
|
|
+ public AnimationState AnimationState {
|
|
|
|
+ get {
|
|
|
|
+ Initialize(false);
|
|
|
|
+ return state;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ public bool freeze { get; set; }
|
|
|
|
+ public bool unscaledTime { get; set; }
|
|
|
|
+
|
|
|
|
+ /// <summary>Update mode to optionally limit updates to e.g. only apply animations but not update the mesh.</summary>
|
|
|
|
+ public UpdateMode UpdateMode { get { return updateMode; } set { updateMode = value; } }
|
|
|
|
+ protected UpdateMode updateMode = UpdateMode.FullUpdate;
|
|
|
|
+
|
|
|
|
+ protected AnimationState state;
|
|
|
|
+ protected Skeleton skeleton;
|
|
|
|
+ protected SkeletonRendererInstruction currentInstructions = new();// to match existing code better
|
|
|
|
+ protected Spine.Unity.MeshGeneratorUIElements meshGenerator = new MeshGeneratorUIElements();
|
|
|
|
+
|
|
|
|
+ protected VisualElement rendererElement;
|
|
|
|
+ IVisualElementScheduledItem scheduledItem;
|
|
|
|
+ protected Bounds referenceMeshBounds;
|
|
|
|
+ protected float scale = 100;
|
|
|
|
+ protected float offsetX, offsetY;
|
|
|
|
+
|
|
|
|
+ bool IsValid { get { return skeleton != null; } }
|
|
|
|
+
|
|
|
|
+ public SpineVisualElement () {
|
|
|
|
+ RegisterCallback<AttachToPanelEvent>(OnAttachedCallback);
|
|
|
|
+ RegisterCallback<DetachFromPanelEvent>(OnDetatchedCallback);
|
|
|
|
+
|
|
|
|
+ rendererElement = new VisualElement();
|
|
|
|
+ rendererElement.generateVisualContent += GenerateVisualContents;
|
|
|
|
+ rendererElement.pickingMode = PickingMode.Ignore;
|
|
|
|
+ rendererElement.style.position = Position.Absolute;
|
|
|
|
+ rendererElement.style.top = 0;
|
|
|
|
+ rendererElement.style.left = 0;
|
|
|
|
+ rendererElement.style.bottom = 0;
|
|
|
|
+ rendererElement.style.right = 0;
|
|
|
|
+ Add(rendererElement);
|
|
|
|
+
|
|
|
|
+ rendererElement.RegisterCallback<GeometryChangedEvent>(OnGeometryChanged);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ void OnGeometryChanged (GeometryChangedEvent evt) {
|
|
|
|
+ AdjustOffsetScaleToMeshBounds(rendererElement);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ void OnAttachedCallback (AttachToPanelEvent evt) {
|
|
|
|
+ Initialize(false);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ void OnDetatchedCallback (DetachFromPanelEvent evt) {
|
|
|
|
+ ClearElement();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public void ClearElement () {
|
|
|
|
+ skeleton = null;
|
|
|
|
+ DisposeUISubmeshes();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public virtual void Update () {
|
|
|
|
+#if UNITY_EDITOR
|
|
|
|
+ if (!Application.isPlaying) {
|
|
|
|
+ Update(0f);
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+#endif
|
|
|
|
+
|
|
|
|
+ if (freeze) return;
|
|
|
|
+ Update(unscaledTime ? Time.unscaledDeltaTime : Time.deltaTime);
|
|
|
|
+ rendererElement.MarkDirtyRepaint();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public virtual void Update (float deltaTime) {
|
|
|
|
+ if (!this.IsValid) return;
|
|
|
|
+
|
|
|
|
+ if (updateMode < UpdateMode.OnlyAnimationStatus)
|
|
|
|
+ return;
|
|
|
|
+ UpdateAnimationStatus(deltaTime);
|
|
|
|
+
|
|
|
|
+ if (updateMode == UpdateMode.OnlyAnimationStatus)
|
|
|
|
+ return;
|
|
|
|
+ ApplyAnimation();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ protected void UpdateAnimationStatus (float deltaTime) {
|
|
|
|
+ deltaTime *= timeScale;
|
|
|
|
+ state.Update(deltaTime);
|
|
|
|
+ skeleton.Update(deltaTime);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ protected void ApplyAnimation () {
|
|
|
|
+
|
|
|
|
+ if (updateMode != UpdateMode.OnlyEventTimelines)
|
|
|
|
+ state.Apply(skeleton);
|
|
|
|
+ else
|
|
|
|
+ state.ApplyEventTimelinesOnly(skeleton);
|
|
|
|
+
|
|
|
|
+ skeleton.UpdateWorldTransform(Skeleton.Physics.Update);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ void Initialize (bool overwrite) {
|
|
|
|
+ if (this.IsValid && !overwrite) return;
|
|
|
|
+
|
|
|
|
+ if (this.SkeletonDataAsset == null) return;
|
|
|
|
+ var skeletonData = this.SkeletonDataAsset.GetSkeletonData(false);
|
|
|
|
+ if (skeletonData == null) return;
|
|
|
|
+
|
|
|
|
+ if (SkeletonDataAsset.atlasAssets.Length <= 0 || SkeletonDataAsset.atlasAssets[0].MaterialCount <= 0) return;
|
|
|
|
+
|
|
|
|
+ this.state = new Spine.AnimationState(SkeletonDataAsset.GetAnimationStateData());
|
|
|
|
+ if (state == null) {
|
|
|
|
+ Clear();
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ this.skeleton = new Skeleton(skeletonData) {
|
|
|
|
+ ScaleX = 1,
|
|
|
|
+ ScaleY = -1
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ // Set the initial Skin and Animation
|
|
|
|
+ if (!string.IsNullOrEmpty(initialSkinName))
|
|
|
|
+ skeleton.SetSkin(initialSkinName);
|
|
|
|
+
|
|
|
|
+ if (!string.IsNullOrEmpty(startingAnimation)) {
|
|
|
|
+ var animationObject = SkeletonDataAsset.GetSkeletonData(false).FindAnimation(startingAnimation);
|
|
|
|
+ if (animationObject != null) {
|
|
|
|
+ state.SetAnimation(0, animationObject, startingLoop);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ AdjustReferenceMeshBounds();
|
|
|
|
+ if (scheduledItem == null)
|
|
|
|
+ scheduledItem = schedule.Execute(Update).Every(1);
|
|
|
|
+
|
|
|
|
+ rendererElement.MarkDirtyRepaint();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ protected class UISubmesh {
|
|
|
|
+ public NativeArray<UIVertex>? vertices = null;
|
|
|
|
+ public NativeArray<ushort>? indices = null;
|
|
|
|
+
|
|
|
|
+ public NativeSlice<UIVertex> verticesSlice;
|
|
|
|
+ public NativeSlice<ushort> indicesSlice;
|
|
|
|
+ }
|
|
|
|
+ protected readonly ExposedList<UISubmesh> uiSubmeshes = new ExposedList<UISubmesh>();
|
|
|
|
+
|
|
|
|
+ protected void GenerateVisualContents (MeshGenerationContext context) {
|
|
|
|
+ if (!this.IsValid) return;
|
|
|
|
+
|
|
|
|
+ MeshGeneratorUIElements.GenerateSkeletonRendererInstruction(currentInstructions, skeleton, null,
|
|
|
|
+ null,
|
|
|
|
+ false,
|
|
|
|
+ false);
|
|
|
|
+
|
|
|
|
+ int submeshCount = currentInstructions.submeshInstructions.Count;
|
|
|
|
+ PrepareUISubmeshCount(submeshCount);
|
|
|
|
+
|
|
|
|
+ // Generate meshes.
|
|
|
|
+ for (int i = 0; i < submeshCount; i++) {
|
|
|
|
+ var submeshInstructionItem = currentInstructions.submeshInstructions.Items[i];
|
|
|
|
+ UISubmesh uiSubmesh = uiSubmeshes.Items[i];
|
|
|
|
+
|
|
|
|
+ meshGenerator.Begin();
|
|
|
|
+ meshGenerator.AddSubmesh(submeshInstructionItem);
|
|
|
|
+ // clipping is done, vertex counts are final.
|
|
|
|
+
|
|
|
|
+ PrepareUISubmesh(uiSubmesh, meshGenerator.VertexCount, meshGenerator.SubmeshIndexCount(0));
|
|
|
|
+ meshGenerator.FillVertexData(ref uiSubmesh.verticesSlice);
|
|
|
|
+ meshGenerator.FillTrianglesSingleSubmesh(ref uiSubmesh.indicesSlice);
|
|
|
|
+
|
|
|
|
+ var submeshMaterial = submeshInstructionItem.material;
|
|
|
|
+
|
|
|
|
+ Texture usedTexture = submeshMaterial.mainTexture;
|
|
|
|
+
|
|
|
|
+ FillContext(context, uiSubmesh, usedTexture);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ protected void PrepareUISubmeshCount (int targetCount) {
|
|
|
|
+ int oldCount = uiSubmeshes.Count;
|
|
|
|
+ uiSubmeshes.EnsureCapacity(targetCount);
|
|
|
|
+ for (int i = oldCount; i < targetCount; ++i) {
|
|
|
|
+ uiSubmeshes.Add(new UISubmesh());
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ protected void PrepareUISubmesh (UISubmesh uiSubmesh, int vertexCount, int indexCount) {
|
|
|
|
+ bool shallReallocateVertices = uiSubmesh.vertices == null || uiSubmesh.vertices.Value.Length < vertexCount;
|
|
|
|
+ if (shallReallocateVertices) {
|
|
|
|
+ int allocationCount = vertexCount;
|
|
|
|
+ if (uiSubmesh.vertices != null) {
|
|
|
|
+ allocationCount = Math.Max(vertexCount, 2 * uiSubmesh.vertices.Value.Length);
|
|
|
|
+ uiSubmesh.vertices.Value.Dispose();
|
|
|
|
+ }
|
|
|
|
+ uiSubmesh.vertices = new NativeArray<UIVertex>(allocationCount, Allocator.Persistent, NativeArrayOptions.ClearMemory);
|
|
|
|
+ }
|
|
|
|
+ if (shallReallocateVertices || uiSubmesh.verticesSlice.Length != vertexCount) {
|
|
|
|
+ uiSubmesh.verticesSlice = new NativeSlice<UIVertex>(uiSubmesh.vertices.Value, 0, vertexCount);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ bool shallReallocateIndices = uiSubmesh.indices == null || uiSubmesh.indices.Value.Length < indexCount;
|
|
|
|
+ if (shallReallocateIndices) {
|
|
|
|
+ int allocationCount = indexCount;
|
|
|
|
+ if (uiSubmesh.indices != null) {
|
|
|
|
+ allocationCount = Math.Max(indexCount, uiSubmesh.indices.Value.Length * 2);
|
|
|
|
+ uiSubmesh.indices.Value.Dispose();
|
|
|
|
+ }
|
|
|
|
+ uiSubmesh.indices = new NativeArray<ushort>(allocationCount, Allocator.Persistent, NativeArrayOptions.ClearMemory);
|
|
|
|
+ }
|
|
|
|
+ if (shallReallocateIndices || uiSubmesh.indicesSlice.Length != indexCount) {
|
|
|
|
+ uiSubmesh.indicesSlice = new NativeSlice<ushort>(uiSubmesh.indices.Value, 0, indexCount);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ protected void DisposeUISubmeshes () {
|
|
|
|
+ for (int i = 0, count = uiSubmeshes.Count; i < count; ++i) {
|
|
|
|
+ UISubmesh uiSubmesh = uiSubmeshes.Items[i];
|
|
|
|
+ if (uiSubmesh.vertices != null) uiSubmesh.vertices.Value.Dispose();
|
|
|
|
+ if (uiSubmesh.indices != null) uiSubmesh.indices.Value.Dispose();
|
|
|
|
+ }
|
|
|
|
+ uiSubmeshes.Clear();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ void FillContext (MeshGenerationContext context, UISubmesh submesh, Texture texture) {
|
|
|
|
+ MeshWriteData meshWriteData = context.Allocate(submesh.verticesSlice.Length, submesh.indicesSlice.Length, texture);
|
|
|
|
+
|
|
|
|
+ meshWriteData.SetAllVertices(submesh.verticesSlice);
|
|
|
|
+ meshWriteData.SetAllIndices(submesh.indicesSlice);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public void AdjustReferenceMeshBounds () {
|
|
|
|
+
|
|
|
|
+ // Need one update to obtain valid mesh bounds
|
|
|
|
+ Update(0.0f);
|
|
|
|
+ MeshGeneratorUIElements.GenerateSkeletonRendererInstruction(currentInstructions, skeleton,
|
|
|
|
+ null, null, false, false);
|
|
|
|
+ int submeshCount = currentInstructions.submeshInstructions.Count;
|
|
|
|
+ meshGenerator.Begin();
|
|
|
|
+
|
|
|
|
+ for (int i = 0; i < submeshCount; i++) {
|
|
|
|
+ var submeshInstructionItem = currentInstructions.submeshInstructions.Items[i];
|
|
|
|
+ meshGenerator.AddSubmesh(submeshInstructionItem);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ referenceMeshBounds = meshGenerator.GetMeshBounds();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ void AdjustOffsetScaleToMeshBounds (VisualElement visualElement) {
|
|
|
|
+ Rect targetRect = visualElement.layout;
|
|
|
|
+ float xScale = targetRect.width / referenceMeshBounds.size.x;
|
|
|
|
+ float yScale = targetRect.height / referenceMeshBounds.size.y;
|
|
|
|
+ this.scale = Math.Min(xScale, yScale);
|
|
|
|
+ float targetOffsetX = targetRect.width / 2;
|
|
|
|
+ float targetOffsetY = targetRect.height / 2;
|
|
|
|
+ this.offsetX = targetOffsetX - referenceMeshBounds.center.x * this.scale;
|
|
|
|
+ this.offsetY = targetOffsetY - referenceMeshBounds.center.y * this.scale;
|
|
|
|
+
|
|
|
|
+ visualElement.style.translate = new StyleTranslate(new Translate(offsetX, offsetY, 0));
|
|
|
|
+ visualElement.style.transformOrigin = new TransformOrigin(0, 0, 0);
|
|
|
|
+ visualElement.style.scale = new Scale(new Vector3(scale, scale, 1));
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|