diff --git a/CHANGELOG.md b/CHANGELOG.md index f57f3cbfa..40be0079d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -170,6 +170,11 @@ - `Universal Render Pipeline/Spine/Skeleton Lit` shader now supports [Adaptive Probe Volumes (APV)](https://docs.unity3d.com/6000.0/Documentation/Manual/urp/probevolumes-concept.html) introduced in Unity 6. The shader also provides a new material property `APV per Pixel` to either calculate APV lighting contribution per pixel (the default) or per vertex. - `Universal Render Pipeline/Spine/Sprite` shader now also supports [Adaptive Probe Volumes (APV)](https://docs.unity3d.com/6000.0/Documentation/Manual/urp/probevolumes-concept.html) introduced in Unity 6. APV lighting contribution is automatically calculated per pixel. - All Spine Outline shaders, including the URP outline shaders, now provide an additional parameter `Fill`. Enable it to also fill the opaque area inside the outline with the outline color. Prevents a semi-transparent gap between outline and skeleton. Defaults to `disabled` to maintain existing behaviour. + - Added example component `RenderExistingMeshGraphic` (similar to `RenderExistingMesh`) to render a `SkeletonGraphic` mesh again with different materials. This might be required by e.g. URP and SkeletonGraphic outline shaders skipping additional render passes. To add a second outline variant of your SkeletonGraphic: + 1. Add a GameObject at the same hierarchy level as the reference SkeletonGraphic and move it before the reference SkeletonGraphic to render behind. + 2. Add a `RenderExistingMeshGraphic` component. + 3. In the `RenderExistingMeshGraphic` component Inspector at `Reference Skeleton Graphic` assign the original `SkeletonGraphic` object. + 4. At `Replacement Material` assign e.g. the included _SkeletonGraphicDefaultOutline_ material to replace all materials with this material. Alternatively, if `Multiple CanvasRenderers` is enabled at the reference SkeletonGraphic, you can add entries to the `Replacement Materials` list and at each entry assign the original SkeletonGraphic material (e.g. _SkeletonGraphicDefault_) to be replaced and the respective `Replacement Material` (e.g. _SkeletonGraphicDefaultOutline_). - **Breaking changes** diff --git a/spine-unity/Assets/Spine Examples/Scripts/Sample Components/RenderExistingMeshGraphic.cs b/spine-unity/Assets/Spine Examples/Scripts/Sample Components/RenderExistingMeshGraphic.cs new file mode 100644 index 000000000..eace21c00 --- /dev/null +++ b/spine-unity/Assets/Spine Examples/Scripts/Sample Components/RenderExistingMeshGraphic.cs @@ -0,0 +1,214 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated July 28, 2023. Replaces all prior versions. + * + * Copyright (c) 2013-2025, 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; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.UI; + +namespace Spine.Unity.Examples { + using MaterialReplacement = RenderExistingMesh.MaterialReplacement; + +#if NEW_PREFAB_SYSTEM + [ExecuteAlways] +#else + [ExecuteInEditMode] +#endif + public class RenderExistingMeshGraphic : MonoBehaviour { + public SkeletonGraphic referenceSkeletonGraphic; + public Material replacementMaterial; + + public MaterialReplacement[] replacementMaterials = new MaterialReplacement[0]; + + SkeletonSubmeshGraphic ownGraphic; + public List ownSubmeshGraphics; + +#if UNITY_EDITOR + private void Reset () { + Awake(); + LateUpdate(); + } +#endif + + void Awake () { + // subscribe to OnMeshAndMaterialsUpdated + if (referenceSkeletonGraphic) { + referenceSkeletonGraphic.OnMeshAndMaterialsUpdated -= UpdateOnCallback; + referenceSkeletonGraphic.OnMeshAndMaterialsUpdated += UpdateOnCallback; + } + + ownGraphic = this.GetComponent(); + if (referenceSkeletonGraphic) { + if (referenceSkeletonGraphic.allowMultipleCanvasRenderers) + EnsureCanvasRendererCount(referenceSkeletonGraphic.canvasRenderers.Count); + else + SetupSubmeshGraphic(); + } + } + + protected void OnDisable () { + if (referenceSkeletonGraphic) { + referenceSkeletonGraphic.OnMeshAndMaterialsUpdated -= UpdateOnCallback; + } + } + + protected void OnEnable () { +#if UNITY_EDITOR + // handle disabled scene reload + if (Application.isPlaying) { + Awake(); + return; + } +#endif + if (referenceSkeletonGraphic) { + referenceSkeletonGraphic.OnMeshAndMaterialsUpdated -= UpdateOnCallback; + referenceSkeletonGraphic.OnMeshAndMaterialsUpdated += UpdateOnCallback; + } + } + + void SetupSubmeshGraphic () { + if (ownGraphic == null) + ownGraphic = this.gameObject.AddComponent(); + + ownGraphic.maskable = referenceSkeletonGraphic.maskable; + ownGraphic.canvasRenderer.cullTransparentMesh = referenceSkeletonGraphic.canvasRenderer.cullTransparentMesh; + ownGraphic.canvasRenderer.SetMaterial(replacementMaterial, referenceSkeletonGraphic.mainTexture); + } + + protected void EnsureCanvasRendererCount (int targetCount) { + if (ownSubmeshGraphics == null) + ownSubmeshGraphics = new List(); + + bool cullTransparentMesh = referenceSkeletonGraphic.canvasRenderer.cullTransparentMesh; + Vector2 pivot = referenceSkeletonGraphic.rectTransform.pivot; + + int currentCount = ownSubmeshGraphics.Count; + for (int i = currentCount; i < targetCount; ++i) { + GameObject go = new GameObject(string.Format("Renderer{0}", i), typeof(RectTransform)); + go.transform.SetParent(this.transform, false); + go.transform.localPosition = Vector3.zero; + CanvasRenderer canvasRenderer = go.AddComponent(); + canvasRenderer.cullTransparentMesh = cullTransparentMesh; + SkeletonSubmeshGraphic submeshGraphic = go.AddComponent(); + ownSubmeshGraphics.Add(submeshGraphic); + submeshGraphic.maskable = referenceSkeletonGraphic.maskable; + submeshGraphic.raycastTarget = false; + submeshGraphic.rectTransform.pivot = pivot; + submeshGraphic.rectTransform.anchorMin = Vector2.zero; + submeshGraphic.rectTransform.anchorMax = Vector2.one; + submeshGraphic.rectTransform.sizeDelta = Vector2.zero; + } + } + + protected void UpdateCanvasRenderers () { + Mesh[] referenceMeshes = referenceSkeletonGraphic.MeshesMultipleCanvasRenderers.Items; + Material[] referenceMaterials = referenceSkeletonGraphic.MaterialsMultipleCanvasRenderers.Items; + Texture[] referenceTextures = referenceSkeletonGraphic.TexturesMultipleCanvasRenderers.Items; + + int end = Math.Min(ownSubmeshGraphics.Count, referenceSkeletonGraphic.TexturesMultipleCanvasRenderers.Count); + + for (int i = 0; i < end; i++) { + SkeletonSubmeshGraphic submeshGraphic = ownSubmeshGraphics[i]; + CanvasRenderer reference = referenceSkeletonGraphic.canvasRenderers[i]; + + if (reference.gameObject.activeInHierarchy) { + Material usedMaterial = replacementMaterial != null ? + replacementMaterial : GetReplacementMaterialFor(referenceMaterials[i]); + if (usedMaterial == null) + usedMaterial = referenceMaterials[i]; + usedMaterial = referenceSkeletonGraphic.GetModifiedMaterial(usedMaterial); + submeshGraphic.canvasRenderer.SetMaterial(usedMaterial, referenceTextures[i]); + submeshGraphic.canvasRenderer.SetMesh(referenceMeshes[i]); + submeshGraphic.gameObject.SetActive(true); + } else { + submeshGraphic.canvasRenderer.Clear(); + submeshGraphic.gameObject.SetActive(false); + } + } + } + + protected void DisableCanvasRenderers () { + for (int i = 0; i < ownSubmeshGraphics.Count; i++) { + SkeletonSubmeshGraphic submeshGraphic = ownSubmeshGraphics[i]; + submeshGraphic.canvasRenderer.Clear(); + submeshGraphic.gameObject.SetActive(false); + } + } + + protected Material GetReplacementMaterialFor (Material originalMaterial) { + for (int i = 0; i < replacementMaterials.Length; ++i) { + MaterialReplacement entry = replacementMaterials[i]; + if (entry.originalMaterial != null && entry.originalMaterial.shader == originalMaterial.shader) + return entry.replacementMaterial; + } + return null; + } + +#if UNITY_EDITOR + void LateUpdate () { + if (!Application.isPlaying) { + UpdateMesh(); + } + } +#endif + + void UpdateOnCallback (SkeletonGraphic g) { + UpdateMesh(); + } + + void UpdateMesh () { + if (!referenceSkeletonGraphic) return; + + if (referenceSkeletonGraphic.allowMultipleCanvasRenderers) { + EnsureCanvasRendererCount(referenceSkeletonGraphic.canvasRenderers.Count); + UpdateCanvasRenderers(); + if (ownGraphic) + ownGraphic.canvasRenderer.Clear(); + } else { + if (ownGraphic == null) + ownGraphic = this.gameObject.AddComponent(); + + DisableCanvasRenderers(); + + Material referenceMaterial = referenceSkeletonGraphic.materialForRendering; + Material usedMaterial = replacementMaterial != null ? replacementMaterial : GetReplacementMaterialFor(referenceMaterial); + if (usedMaterial == null) + usedMaterial = referenceMaterial; + usedMaterial = referenceSkeletonGraphic.GetModifiedMaterial(usedMaterial); + ownGraphic.canvasRenderer.SetMaterial(usedMaterial, referenceSkeletonGraphic.mainTexture); + Mesh mesh = referenceSkeletonGraphic.GetLastMesh(); + ownGraphic.canvasRenderer.SetMesh(mesh); + } + } + } +} diff --git a/spine-unity/Assets/Spine Examples/Scripts/Sample Components/RenderExistingMeshGraphic.cs.meta b/spine-unity/Assets/Spine Examples/Scripts/Sample Components/RenderExistingMeshGraphic.cs.meta new file mode 100644 index 000000000..ca0fdd068 --- /dev/null +++ b/spine-unity/Assets/Spine Examples/Scripts/Sample Components/RenderExistingMeshGraphic.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ff6ce4ce6b9336a479c6bf5af81fa80a \ No newline at end of file diff --git a/spine-unity/Assets/Spine Examples/package.json b/spine-unity/Assets/Spine Examples/package.json index 9f15b3a28..5c19a8500 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.2.37", + "version": "4.2.38", "unity": "2018.3", "author": { "name": "Esoteric Software",