SpineVisualElement.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. /******************************************************************************
  2. * Spine Runtimes License Agreement
  3. * Last updated July 28, 2023. Replaces all prior versions.
  4. *
  5. * Copyright (c) 2013-2024, Esoteric Software LLC
  6. *
  7. * Integration of the Spine Runtimes into software or otherwise creating
  8. * derivative works of the Spine Runtimes is permitted under the terms and
  9. * conditions of Section 2 of the Spine Editor License Agreement:
  10. * http://esotericsoftware.com/spine-editor-license
  11. *
  12. * Otherwise, it is permitted to integrate the Spine Runtimes into software or
  13. * otherwise create derivative works of the Spine Runtimes (collectively,
  14. * "Products"), provided that each user of the Products must obtain their own
  15. * Spine Editor license and redistribution of the Products in any form must
  16. * include this license and copyright notice.
  17. *
  18. * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
  19. * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  20. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  21. * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
  22. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  23. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
  24. * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
  25. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  26. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
  27. * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  28. *****************************************************************************/
  29. using System;
  30. using Unity.Collections;
  31. using UnityEngine;
  32. using UnityEngine.UIElements;
  33. using UIVertex = UnityEngine.UIElements.Vertex;
  34. namespace Spine.Unity {
  35. public class BoundsFromAnimationAttribute : PropertyAttribute {
  36. public readonly string animationField;
  37. public readonly string dataField;
  38. public readonly string skinField;
  39. public BoundsFromAnimationAttribute (string animationField, string skinField, string dataField = "skeletonDataAsset") {
  40. this.animationField = animationField;
  41. this.skinField = skinField;
  42. this.dataField = dataField;
  43. }
  44. }
  45. [UxmlElement]
  46. public partial class SpineVisualElement : VisualElement {
  47. [UxmlAttribute]
  48. public SkeletonDataAsset SkeletonDataAsset {
  49. get { return skeletonDataAsset; }
  50. set {
  51. if (skeletonDataAsset == value) return;
  52. skeletonDataAsset = value;
  53. #if UNITY_EDITOR
  54. if (!Application.isPlaying)
  55. Initialize(true);
  56. #endif
  57. }
  58. }
  59. public SkeletonDataAsset skeletonDataAsset;
  60. [SpineAnimation(dataField: "SkeletonDataAsset", avoidGenericMenu: true)]
  61. [UxmlAttribute]
  62. public string StartingAnimation {
  63. get { return startingAnimation; }
  64. set {
  65. if (startingAnimation == value) return;
  66. startingAnimation = value;
  67. #if UNITY_EDITOR
  68. if (!Application.isPlaying)
  69. Initialize(true);
  70. #endif
  71. }
  72. }
  73. public string startingAnimation = "";
  74. [SpineSkin(dataField: "SkeletonDataAsset", defaultAsEmptyString: true, avoidGenericMenu: true)]
  75. [UxmlAttribute]
  76. public string InitialSkinName {
  77. get { return initialSkinName; }
  78. set {
  79. if (initialSkinName == value) return;
  80. initialSkinName = value;
  81. #if UNITY_EDITOR
  82. if (!Application.isPlaying)
  83. Initialize(true);
  84. #endif
  85. }
  86. }
  87. public string initialSkinName;
  88. [UxmlAttribute] public bool startingLoop { get; set; } = true;
  89. [UxmlAttribute] public float timeScale { get; set; } = 1.0f;
  90. [SpineAnimation(dataField: "SkeletonDataAsset", avoidGenericMenu: true)]
  91. [UxmlAttribute]
  92. public string BoundsAnimation {
  93. get { return boundsAnimation; }
  94. set {
  95. boundsAnimation = value;
  96. #if UNITY_EDITOR
  97. if (!Application.isPlaying) {
  98. if (!this.IsValid)
  99. Initialize(true);
  100. else {
  101. UpdateAnimation();
  102. }
  103. }
  104. #endif
  105. }
  106. }
  107. public string boundsAnimation = "";
  108. [UxmlAttribute]
  109. [BoundsFromAnimation(animationField: "BoundsAnimation",
  110. skinField: "InitialSkinName", dataField: "SkeletonDataAsset")]
  111. public Bounds ReferenceBounds {
  112. get { return referenceMeshBounds; }
  113. set {
  114. if (referenceMeshBounds == value) return;
  115. #if UNITY_EDITOR
  116. if (!Application.isPlaying && (value.size.x == 0 || value.size.y == 0)) return;
  117. #endif
  118. referenceMeshBounds = value;
  119. if (!this.IsValid) return;
  120. AdjustOffsetScaleToMeshBounds(rendererElement);
  121. }
  122. }
  123. public Bounds referenceMeshBounds;
  124. public AnimationState AnimationState {
  125. get {
  126. Initialize(false);
  127. return state;
  128. }
  129. }
  130. [UxmlAttribute]
  131. public bool freeze { get; set; }
  132. [UxmlAttribute]
  133. public bool unscaledTime { get; set; }
  134. /// <summary>Update mode to optionally limit updates to e.g. only apply animations but not update the mesh.</summary>
  135. public UpdateMode UpdateMode { get { return updateMode; } set { updateMode = value; } }
  136. protected UpdateMode updateMode = UpdateMode.FullUpdate;
  137. protected AnimationState state = null;
  138. protected Skeleton skeleton = null;
  139. protected SkeletonRendererInstruction currentInstructions = new();// to match existing code better
  140. protected Spine.Unity.MeshGeneratorUIElements meshGenerator = new MeshGeneratorUIElements();
  141. protected VisualElement rendererElement;
  142. IVisualElementScheduledItem scheduledItem;
  143. protected float scale = 100;
  144. protected float offsetX, offsetY;
  145. bool IsValid { get { return skeleton != null; } }
  146. public SpineVisualElement () {
  147. RegisterCallback<AttachToPanelEvent>(OnAttachedCallback);
  148. RegisterCallback<DetachFromPanelEvent>(OnDetatchedCallback);
  149. rendererElement = new VisualElement();
  150. rendererElement.generateVisualContent += GenerateVisualContents;
  151. rendererElement.pickingMode = PickingMode.Ignore;
  152. rendererElement.style.position = Position.Absolute;
  153. rendererElement.style.top = 0;
  154. rendererElement.style.left = 0;
  155. rendererElement.style.bottom = 0;
  156. rendererElement.style.right = 0;
  157. Add(rendererElement);
  158. rendererElement.RegisterCallback<GeometryChangedEvent>(OnGeometryChanged);
  159. }
  160. void OnGeometryChanged (GeometryChangedEvent evt) {
  161. if (!this.IsValid) return;
  162. if (referenceMeshBounds.size.x == 0 || referenceMeshBounds.size.y == 0) {
  163. AdjustReferenceMeshBounds();
  164. }
  165. AdjustOffsetScaleToMeshBounds(rendererElement);
  166. }
  167. void OnAttachedCallback (AttachToPanelEvent evt) {
  168. Initialize(false);
  169. }
  170. void OnDetatchedCallback (DetachFromPanelEvent evt) {
  171. ClearElement();
  172. }
  173. public void ClearElement () {
  174. skeleton = null;
  175. DisposeUISubmeshes();
  176. }
  177. public virtual void Update () {
  178. #if UNITY_EDITOR
  179. if (!Application.isPlaying) {
  180. Update(0f);
  181. return;
  182. }
  183. #endif
  184. if (freeze) return;
  185. Update(unscaledTime ? Time.unscaledDeltaTime : Time.deltaTime);
  186. rendererElement.MarkDirtyRepaint();
  187. }
  188. public virtual void Update (float deltaTime) {
  189. if (!this.IsValid) return;
  190. if (updateMode < UpdateMode.OnlyAnimationStatus)
  191. return;
  192. UpdateAnimationStatus(deltaTime);
  193. if (updateMode == UpdateMode.OnlyAnimationStatus)
  194. return;
  195. ApplyAnimation();
  196. }
  197. protected void UpdateAnimationStatus (float deltaTime) {
  198. deltaTime *= timeScale;
  199. state.Update(deltaTime);
  200. skeleton.Update(deltaTime);
  201. }
  202. protected void ApplyAnimation () {
  203. if (updateMode != UpdateMode.OnlyEventTimelines)
  204. state.Apply(skeleton);
  205. else
  206. state.ApplyEventTimelinesOnly(skeleton);
  207. skeleton.UpdateWorldTransform(Skeleton.Physics.Update);
  208. }
  209. void Initialize (bool overwrite) {
  210. if (this.IsValid && !overwrite) return;
  211. if (this.SkeletonDataAsset == null) return;
  212. var skeletonData = this.SkeletonDataAsset.GetSkeletonData(false);
  213. if (skeletonData == null) return;
  214. if (SkeletonDataAsset.atlasAssets.Length <= 0 || SkeletonDataAsset.atlasAssets[0].MaterialCount <= 0) return;
  215. this.state = new Spine.AnimationState(SkeletonDataAsset.GetAnimationStateData());
  216. if (state == null) {
  217. Clear();
  218. return;
  219. }
  220. this.skeleton = new Skeleton(skeletonData) {
  221. ScaleX = 1,
  222. ScaleY = -1
  223. };
  224. // Set the initial Skin and Animation
  225. if (!string.IsNullOrEmpty(initialSkinName))
  226. skeleton.SetSkin(initialSkinName);
  227. string displayedAnimation = Application.isPlaying ? startingAnimation : boundsAnimation;
  228. if (!string.IsNullOrEmpty(displayedAnimation)) {
  229. var animationObject = skeletonData.FindAnimation(displayedAnimation);
  230. if (animationObject != null) {
  231. state.SetAnimation(0, animationObject, startingLoop);
  232. }
  233. }
  234. if (referenceMeshBounds.size.x == 0 || referenceMeshBounds.size.y == 0) {
  235. AdjustReferenceMeshBounds();
  236. AdjustOffsetScaleToMeshBounds(rendererElement);
  237. }
  238. if (scheduledItem == null)
  239. scheduledItem = schedule.Execute(Update).Every(1);
  240. if (!Application.isPlaying)
  241. Update(0.0f);
  242. rendererElement.MarkDirtyRepaint();
  243. }
  244. protected void UpdateAnimation () {
  245. this.state.ClearTracks();
  246. skeleton.SetToSetupPose();
  247. string displayedAnimation = Application.isPlaying ? startingAnimation : boundsAnimation;
  248. if (!string.IsNullOrEmpty(displayedAnimation)) {
  249. var animationObject = SkeletonDataAsset.GetSkeletonData(false).FindAnimation(displayedAnimation);
  250. if (animationObject != null) {
  251. state.SetAnimation(0, animationObject, startingLoop);
  252. }
  253. }
  254. if (referenceMeshBounds.size.x == 0 || referenceMeshBounds.size.y == 0) {
  255. AdjustReferenceMeshBounds();
  256. AdjustOffsetScaleToMeshBounds(rendererElement);
  257. }
  258. Update(0.0f);
  259. rendererElement.MarkDirtyRepaint();
  260. }
  261. protected class UISubmesh {
  262. public NativeArray<UIVertex>? vertices = null;
  263. public NativeArray<ushort>? indices = null;
  264. public NativeSlice<UIVertex> verticesSlice;
  265. public NativeSlice<ushort> indicesSlice;
  266. }
  267. protected readonly ExposedList<UISubmesh> uiSubmeshes = new ExposedList<UISubmesh>();
  268. protected void GenerateVisualContents (MeshGenerationContext context) {
  269. if (!this.IsValid) return;
  270. MeshGeneratorUIElements.GenerateSkeletonRendererInstruction(currentInstructions, skeleton, null,
  271. null,
  272. false,
  273. false);
  274. int submeshCount = currentInstructions.submeshInstructions.Count;
  275. PrepareUISubmeshCount(submeshCount);
  276. // Generate meshes.
  277. for (int i = 0; i < submeshCount; i++) {
  278. var submeshInstructionItem = currentInstructions.submeshInstructions.Items[i];
  279. UISubmesh uiSubmesh = uiSubmeshes.Items[i];
  280. meshGenerator.Begin();
  281. meshGenerator.AddSubmesh(submeshInstructionItem);
  282. // clipping is done, vertex counts are final.
  283. PrepareUISubmesh(uiSubmesh, meshGenerator.VertexCount, meshGenerator.SubmeshIndexCount(0));
  284. meshGenerator.FillVertexData(ref uiSubmesh.verticesSlice);
  285. meshGenerator.FillTrianglesSingleSubmesh(ref uiSubmesh.indicesSlice);
  286. var submeshMaterial = submeshInstructionItem.material;
  287. Texture usedTexture = submeshMaterial.mainTexture;
  288. FillContext(context, uiSubmesh, usedTexture);
  289. }
  290. }
  291. protected void PrepareUISubmeshCount (int targetCount) {
  292. int oldCount = uiSubmeshes.Count;
  293. uiSubmeshes.EnsureCapacity(targetCount);
  294. for (int i = oldCount; i < targetCount; ++i) {
  295. uiSubmeshes.Add(new UISubmesh());
  296. }
  297. }
  298. protected void PrepareUISubmesh (UISubmesh uiSubmesh, int vertexCount, int indexCount) {
  299. bool shallReallocateVertices = uiSubmesh.vertices == null || uiSubmesh.vertices.Value.Length < vertexCount;
  300. if (shallReallocateVertices) {
  301. int allocationCount = vertexCount;
  302. if (uiSubmesh.vertices != null) {
  303. allocationCount = Math.Max(vertexCount, 2 * uiSubmesh.vertices.Value.Length);
  304. uiSubmesh.vertices.Value.Dispose();
  305. }
  306. uiSubmesh.vertices = new NativeArray<UIVertex>(allocationCount, Allocator.Persistent, NativeArrayOptions.ClearMemory);
  307. }
  308. if (shallReallocateVertices || uiSubmesh.verticesSlice.Length != vertexCount) {
  309. uiSubmesh.verticesSlice = new NativeSlice<UIVertex>(uiSubmesh.vertices.Value, 0, vertexCount);
  310. }
  311. bool shallReallocateIndices = uiSubmesh.indices == null || uiSubmesh.indices.Value.Length < indexCount;
  312. if (shallReallocateIndices) {
  313. int allocationCount = indexCount;
  314. if (uiSubmesh.indices != null) {
  315. allocationCount = Math.Max(indexCount, uiSubmesh.indices.Value.Length * 2);
  316. uiSubmesh.indices.Value.Dispose();
  317. }
  318. uiSubmesh.indices = new NativeArray<ushort>(allocationCount, Allocator.Persistent, NativeArrayOptions.ClearMemory);
  319. }
  320. if (shallReallocateIndices || uiSubmesh.indicesSlice.Length != indexCount) {
  321. uiSubmesh.indicesSlice = new NativeSlice<ushort>(uiSubmesh.indices.Value, 0, indexCount);
  322. }
  323. }
  324. protected void DisposeUISubmeshes () {
  325. for (int i = 0, count = uiSubmeshes.Count; i < count; ++i) {
  326. UISubmesh uiSubmesh = uiSubmeshes.Items[i];
  327. if (uiSubmesh.vertices != null) uiSubmesh.vertices.Value.Dispose();
  328. if (uiSubmesh.indices != null) uiSubmesh.indices.Value.Dispose();
  329. }
  330. uiSubmeshes.Clear();
  331. }
  332. void FillContext (MeshGenerationContext context, UISubmesh submesh, Texture texture) {
  333. MeshWriteData meshWriteData = context.Allocate(submesh.verticesSlice.Length, submesh.indicesSlice.Length, texture);
  334. meshWriteData.SetAllVertices(submesh.verticesSlice);
  335. meshWriteData.SetAllIndices(submesh.indicesSlice);
  336. }
  337. public void AdjustReferenceMeshBounds () {
  338. if (skeleton == null)
  339. return;
  340. // Need one update to obtain valid mesh bounds
  341. Update(0.0f);
  342. MeshGeneratorUIElements.GenerateSkeletonRendererInstruction(currentInstructions, skeleton,
  343. null, null, false, false);
  344. int submeshCount = currentInstructions.submeshInstructions.Count;
  345. meshGenerator.Begin();
  346. for (int i = 0; i < submeshCount; i++) {
  347. var submeshInstructionItem = currentInstructions.submeshInstructions.Items[i];
  348. meshGenerator.AddSubmesh(submeshInstructionItem);
  349. }
  350. Bounds meshBounds = meshGenerator.GetMeshBounds();
  351. if (meshBounds.extents.x == 0 || meshBounds.extents.y == 0) {
  352. ReferenceBounds = new Bounds(Vector3.zero, Vector3.one * 2f);
  353. } else {
  354. ReferenceBounds = meshBounds;
  355. }
  356. }
  357. void AdjustOffsetScaleToMeshBounds (VisualElement visualElement) {
  358. Rect targetRect = visualElement.layout;
  359. if (float.IsNaN(targetRect.width)) return;
  360. float xScale = targetRect.width / referenceMeshBounds.size.x;
  361. float yScale = targetRect.height / referenceMeshBounds.size.y;
  362. this.scale = Math.Min(xScale, yScale);
  363. float targetOffsetX = targetRect.width / 2;
  364. float targetOffsetY = targetRect.height / 2;
  365. this.offsetX = targetOffsetX - referenceMeshBounds.center.x * this.scale;
  366. this.offsetY = targetOffsetY - referenceMeshBounds.center.y * this.scale;
  367. visualElement.style.translate = new StyleTranslate(new Translate(offsetX, offsetY, 0));
  368. visualElement.style.transformOrigin = new TransformOrigin(0, 0, 0);
  369. visualElement.style.scale = new Scale(new Vector3(scale, scale, 1));
  370. }
  371. }
  372. }