From ed75bffc6f5febfcc07ce0d0739ec761a9afe442 Mon Sep 17 00:00:00 2001 From: Harald Csaszar Date: Mon, 11 Sep 2023 20:29:57 +0200 Subject: [PATCH] [unity] Added `RenderCombinedMesh` sample component for combined outlines with `SkeletonRenderSeparator` or multiple atlas pages. --- CHANGELOG.md | 6 + .../Sample Components/RenderCombinedMesh.cs | 253 ++++++++++++++++++ .../RenderCombinedMesh.cs.meta | 11 + .../Assets/Spine Examples/package.json | 2 +- 4 files changed, 271 insertions(+), 1 deletion(-) create mode 100644 spine-unity/Assets/Spine Examples/Scripts/Sample Components/RenderCombinedMesh.cs create mode 100644 spine-unity/Assets/Spine Examples/Scripts/Sample Components/RenderCombinedMesh.cs.meta diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f04094c3..7096502b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -105,6 +105,12 @@ * Added `SkeletonGraphic.MeshScale` property to allow access to calculated mesh scale. `MeshScale` is based on (1) Canvas pixels per unit, and (2) `RectTransform` bounds when using `Layout Scale Mode` other than `None` at `SkeletonGraphic` which scales the skeleton mesh to fit the parent `RectTransform` bounds accordingly. * Added `updateSeparatorPartScale` property to `SkeletonGraphic` to let render separator parts follow the scale (lossy scale) of the `SkeletonGraphic` GameObject. Defaults to `false` to maintain existing behaviour. * Added experimental `EditorSkeletonPlayer` component to allow Editor playback of the initial animation set at `SkeletonAnimation` or `SkeletonGraphic` components. Add this component to your skeleton GameObject to enable the in-editor animation preview. Allows configurations for continuous playback when selected, deselected, and alternative single-frame preview by setting `Fixed Track Time` to any value other than 0. Limitations: At skeletons with variable material count the Inspector preview may be too unresponsive. It is then recommended to disable the `EditorSkeletonPlayer` component (at the top of the Inspector) to make it responsive again, then you can disable `Play When Selected` and re-enable the component to preview playback only when deselected. + * Added example component `RenderCombinedMesh` to render a combined mesh of multiple meshes or submeshes. This is required by `OutlineOnly` shaders to render a combined outline when using `SkeletonRenderSeparator` or multiple atlas pages which would normally lead to outlines around individual parts. To add a combined outline to your SkeletenRenderer: + 1) Add a child GameObject and move it a bit back (e.g. position Z = 0.01). + 2) Add a `RenderCombinedMesh` component, provided in the `Spine Examples/Scripts/Sample Components` directory. + 3) Copy the original material, add *_Outline* to its name and set the shader to your outline-only shader like `Universal Render Pipeline/Spine/Outline/Skeleton-OutlineOnly` or `Spine/Outline/OutlineOnly-ZWrite`. + 4) Assign this *_Outline* material at the new child GameObject's `MeshRenderer` component. + If you are using `SkeletonRenderSeparator` and need to enable and disable the `SkeletonRenderSeparator` component at runtime, you can increase the `RenderCombinedMesh` `Reference Renderers` array by one and assign the `SkeletonRenderer` itself at the last entry after the parts renderers. Disabled `MeshRenderer` components will be skipped when combining the final mesh, so the combined mesh is automatically filled from the desired active renderers. * **Breaking changes** * Made `SkeletonGraphic.unscaledTime` parameter protected, use the new property `UnscaledTime` instead. diff --git a/spine-unity/Assets/Spine Examples/Scripts/Sample Components/RenderCombinedMesh.cs b/spine-unity/Assets/Spine Examples/Scripts/Sample Components/RenderCombinedMesh.cs new file mode 100644 index 000000000..941ef5d06 --- /dev/null +++ b/spine-unity/Assets/Spine Examples/Scripts/Sample Components/RenderCombinedMesh.cs @@ -0,0 +1,253 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated July 28, 2023. Replaces all prior versions. + * + * Copyright (c) 2013-2023, 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. + *****************************************************************************/ + +#if UNITY_2018_3 || UNITY_2019 || UNITY_2018_3_OR_NEWER +#define NEW_PREFAB_SYSTEM +#endif + +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace Spine.Unity.Examples { + +#if NEW_PREFAB_SYSTEM + [ExecuteAlways] +#else + [ExecuteInEditMode] +#endif + public class RenderCombinedMesh : MonoBehaviour { + public SkeletonRenderer skeletonRenderer; + public SkeletonRenderSeparator renderSeparator; + public MeshRenderer[] referenceRenderers; + + bool updateViaSkeletonCallback = false; + MeshFilter[] referenceMeshFilters; + MeshRenderer ownRenderer; + MeshFilter ownMeshFilter; + + protected DoubleBuffered doubleBufferedMesh; + protected ExposedList positionBuffer; + protected ExposedList colorBuffer; + protected ExposedList uvBuffer; + protected ExposedList indexBuffer; + +#if UNITY_EDITOR + private void Reset () { + if (skeletonRenderer == null) + skeletonRenderer = this.GetComponentInParent(); + GatherRenderers(); + + Awake(); + if (referenceRenderers.Length > 0) + ownRenderer.sharedMaterial = referenceRenderers[0].sharedMaterial; + + LateUpdate(); + } +#endif + protected void GatherRenderers () { + referenceRenderers = this.GetComponentsInChildren(); + if (referenceRenderers.Length == 0 || + (referenceRenderers.Length == 1 && referenceRenderers[0].gameObject == this.gameObject)) { + Transform parent = this.transform.parent; + if (parent) + referenceRenderers = parent.GetComponentsInChildren(); + } + referenceRenderers = referenceRenderers.Where( + (val, idx) => val.gameObject != this.gameObject && val.enabled).ToArray(); + } + + void Awake () { + if (skeletonRenderer == null) + skeletonRenderer = this.GetComponentInParent(); + if (referenceRenderers == null || referenceRenderers.Length == 0) { + GatherRenderers(); + } + + if (renderSeparator == null) { + if (skeletonRenderer) + renderSeparator = skeletonRenderer.GetComponent(); + else + renderSeparator = this.GetComponentInParent(); + } + + int count = referenceRenderers.Length; + referenceMeshFilters = new MeshFilter[count]; + for (int i = 0; i < count; ++i) { + referenceMeshFilters[i] = referenceRenderers[i].GetComponent(); + } + + ownRenderer = this.GetComponent(); + if (ownRenderer == null) + ownRenderer = this.gameObject.AddComponent(); + ownMeshFilter = this.GetComponent(); + if (ownMeshFilter == null) + ownMeshFilter = this.gameObject.AddComponent(); + } + + void OnEnable () { +#if UNITY_EDITOR + if (Application.isPlaying) + Awake(); +#endif + if (skeletonRenderer) { + skeletonRenderer.OnMeshAndMaterialsUpdated -= UpdateOnCallback; + skeletonRenderer.OnMeshAndMaterialsUpdated += UpdateOnCallback; + updateViaSkeletonCallback = true; + } + if (renderSeparator) { + renderSeparator.OnMeshAndMaterialsUpdated -= UpdateOnCallback; + renderSeparator.OnMeshAndMaterialsUpdated += UpdateOnCallback; + updateViaSkeletonCallback = true; + } + } + + void OnDisable () { + if (skeletonRenderer) + skeletonRenderer.OnMeshAndMaterialsUpdated -= UpdateOnCallback; + if (renderSeparator) + renderSeparator.OnMeshAndMaterialsUpdated -= UpdateOnCallback; + } + + void OnDestroy () { + for (int i = 0; i < 2; ++i) { + Mesh mesh = doubleBufferedMesh.GetNext(); +#if UNITY_EDITOR + if (Application.isEditor && !Application.isPlaying) + UnityEngine.Object.DestroyImmediate(mesh); + else + UnityEngine.Object.Destroy(mesh); +#else + UnityEngine.Object.Destroy(mesh); +#endif + } + } + + void LateUpdate () { +#if UNITY_EDITOR + if (!Application.isPlaying) { + UpdateMesh(); + return; + } +#endif + + if (updateViaSkeletonCallback) + return; + UpdateMesh(); + } + + void UpdateOnCallback (SkeletonRenderer r) { + UpdateMesh(); + } + + protected void EnsureBufferSizes (int combinedVertexCount, int combinedIndexCount) { + if (positionBuffer == null) { + positionBuffer = new ExposedList(combinedVertexCount); + uvBuffer = new ExposedList(combinedVertexCount); + colorBuffer = new ExposedList(combinedVertexCount); + indexBuffer = new ExposedList(combinedIndexCount); + } + + if (positionBuffer.Count < combinedVertexCount) { + positionBuffer.Resize(combinedVertexCount); + uvBuffer.Resize(combinedVertexCount); + colorBuffer.Resize(combinedVertexCount); + } + if (indexBuffer.Count < combinedIndexCount) { + indexBuffer.Resize(combinedIndexCount); + } + } + + void InitMesh () { + if (doubleBufferedMesh == null) { + doubleBufferedMesh = new DoubleBuffered(); + for (int i = 0; i < 2; ++i) { + Mesh combinedMesh = doubleBufferedMesh.GetNext(); + combinedMesh.MarkDynamic(); + combinedMesh.name = "RenderCombinedMesh" + i; + combinedMesh.subMeshCount = 1; + } + } + } + + void UpdateMesh () { + InitMesh(); + int combinedVertexCount = 0; + int combinedIndexCount = 0; + GetCombinedMeshInfo(ref combinedVertexCount, ref combinedIndexCount); + + EnsureBufferSizes(combinedVertexCount, combinedIndexCount); + + int combinedV = 0; + int combinedI = 0; + for (int r = 0, rendererCount = referenceMeshFilters.Length; r < rendererCount; ++r) { + MeshFilter meshFilter = referenceMeshFilters[r]; + Mesh mesh = meshFilter.sharedMesh; + if (mesh == null) continue; + + int vertexCount = mesh.vertexCount; + Vector3[] positions = mesh.vertices; + Vector2[] uvs = mesh.uv; + Color32[] colors = mesh.colors32; + + System.Array.Copy(positions, 0, this.positionBuffer.Items, combinedV, vertexCount); + System.Array.Copy(uvs, 0, this.uvBuffer.Items, combinedV, vertexCount); + System.Array.Copy(colors, 0, this.colorBuffer.Items, combinedV, vertexCount); + combinedV += vertexCount; + + for (int s = 0, submeshCount = mesh.subMeshCount; s < submeshCount; ++s) { + int submeshIndexCount = (int)mesh.GetIndexCount(s); + int[] submeshIndices = mesh.GetIndices(s); + System.Array.Copy(submeshIndices, 0, this.indexBuffer.Items, combinedI, submeshIndexCount); + combinedI += submeshIndexCount; + } + } + + Mesh combinedMesh = doubleBufferedMesh.GetNext(); + combinedMesh.SetVertices(this.positionBuffer.Items, 0, this.positionBuffer.Count); + combinedMesh.SetUVs(0, this.uvBuffer.Items, 0, this.uvBuffer.Count); + combinedMesh.SetColors(this.colorBuffer.Items, 0, this.colorBuffer.Count); + combinedMesh.SetTriangles(this.indexBuffer.Items, 0, this.indexBuffer.Count, 0); + ownMeshFilter.sharedMesh = combinedMesh; + } + + void GetCombinedMeshInfo (ref int vertexCount, ref int indexCount) { + for (int r = 0, rendererCount = referenceMeshFilters.Length; r < rendererCount; ++r) { + MeshFilter meshFilter = referenceMeshFilters[r]; + Mesh mesh = meshFilter.sharedMesh; + if (mesh == null) continue; + + vertexCount += mesh.vertexCount; + for (int s = 0, submeshCount = mesh.subMeshCount; s < submeshCount; ++s) { + indexCount += (int)mesh.GetIndexCount(s); + } + } + } + } +} diff --git a/spine-unity/Assets/Spine Examples/Scripts/Sample Components/RenderCombinedMesh.cs.meta b/spine-unity/Assets/Spine Examples/Scripts/Sample Components/RenderCombinedMesh.cs.meta new file mode 100644 index 000000000..739194fc6 --- /dev/null +++ b/spine-unity/Assets/Spine Examples/Scripts/Sample Components/RenderCombinedMesh.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 05709c69e8e14304b9781652ad05daef +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/spine-unity/Assets/Spine Examples/package.json b/spine-unity/Assets/Spine Examples/package.json index 2fd21dc34..fc07e4e58 100644 --- a/spine-unity/Assets/Spine Examples/package.json +++ b/spine-unity/Assets/Spine Examples/package.json @@ -2,7 +2,7 @@ "name": "com.esotericsoftware.spine.spine-unity-examples", "displayName": "spine-unity Runtime Examples", "description": "This plugin provides example scenes and scripts for the spine-unity runtime.", - "version": "4.1.13", + "version": "4.1.21", "unity": "2018.3", "author": { "name": "Esoteric Software",