• Unity
  • Created a Spine Edge Shader

  • Bearbeitet
Related Discussions
...

It's probably not the most efficient thing ever, but it looks and feels cool!

If anyone's interested, I can point you at how to do it. 🙂

Give me the script please!
I may give you a kidney in change!

The exchange rate for a Kidney is $42,000. So yes, I will accept that ;D

It's pretty complex, but here you go:

OutlineShader.shader

/*
//  Copyright (c) 2015 José Guerreiro. All rights reserved.
//
//  MIT license, see http://www.opensource.org/licenses/mit-license.php
//  
// Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: //
// The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. //
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. */ Shader "Hidden/OutlineEffect" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _LineColor ("Line Color", Color) = (1,1,1,.5)
} SubShader { Pass { Tags { "RenderType"="Opaque" } LOD 200 ZTest Always ZWrite Off Cull Off
CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma target 3.0 #include "UnityCG.cginc" sampler2D _MainTex; sampler2D _OutlineSource; struct v2f { float4 position : SV_POSITION; float2 uv : TEXCOORD0; }; v2f vert(appdata_img v) { v2f o; o.position = mul(UNITY_MATRIX_MVP, v.vertex); o.uv = v.texcoord; return o; } float _LineThicknessX; float _LineThicknessY; float _LineIntensity; half4 _LineColor1; half4 _LineColor2; half4 _LineColor3; int _FlipY; half4 frag (v2f input) : COLOR { float2 uv = input.uv; if (_FlipY == 1) uv.y = 1 - uv.y; half4 originalPixel = tex2D(_MainTex,input.uv); half4 outlineSource = tex2D(_OutlineSource, uv); float h = .95f; half4 outline = 0; half4 sample1 = tex2D(_OutlineSource, uv + float2(_LineThicknessX,0.0)); half4 sample2 = tex2D(_OutlineSource, uv + float2(-_LineThicknessX,0.0)); half4 sample3 = tex2D(_OutlineSource, uv + float2(.0,_LineThicknessY)); half4 sample4 = tex2D(_OutlineSource, uv + float2(.0,-_LineThicknessY)); if(outlineSource.a < h) { if(sample1.r > h || sample2.r > h || sample3.r > h || sample4.r > h) outline = _LineColor1 * _LineIntensity; else if(sample1.g > h || sample2.g > h || sample3.g > h || sample4.g > h) outline = _LineColor2 * _LineIntensity; else if(sample1.b > h || sample2.b > h || sample3.b > h || sample4.b > h) outline = _LineColor3 * _LineIntensity; } //return outlineSource; return originalPixel + outline; } ENDCG } } FallBack "Diffuse" }

OutlineBufferShader.shader

/*
//  Copyright (c) 2015 José Guerreiro. All rights reserved.
//
//  MIT license, see http://www.opensource.org/licenses/mit-license.php
//  
// Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: //
// The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. //
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. */ Shader "Hidden/OutlineBufferEffect" { Properties { [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {} _Color ("Tint", Color) = (1,1,1,1) [MaterialToggle] PixelSnap ("Pixel snap", Float) = 0 } SubShader { Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "PreviewType"="Plane" "CanUseSpriteAtlas"="True" } Cull Off Lighting Off ZWrite Off Blend One OneMinusSrcAlpha CGPROGRAM #pragma surface surf Lambert vertex:vert nofog keepalpha #pragma multi_compile _ PIXELSNAP_ON sampler2D _MainTex; fixed4 _Color; struct Input { float2 uv_MainTex; fixed4 color; }; void vert (inout appdata_full v, out Input o) { #if defined(PIXELSNAP_ON) v.vertex = UnityPixelSnap (v.vertex); #endif UNITY_INITIALIZE_OUTPUT(Input, o); o.color = v.color; } void surf (Input IN, inout SurfaceOutput o) { fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * IN.color; float alpha = c.a * 99999999; o.Albedo = _Color * alpha; o.Alpha = alpha; } ENDCG } Fallback "Transparent/VertexLit" }

OutlineEffect.cs

You will need to tweak this to suit your project.

/*
//  Copyright (c) 2015 José Guerreiro. All rights reserved.
//
//  MIT license, see http://www.opensource.org/licenses/mit-license.php
//  
// Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: //
// The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. //
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. */ using UnityEngine; using System.Collections; using System.Collections.Generic; using Spine.Unity; [RequireComponent(typeof(Camera))] public class OutlineEffect : MonoBehaviour { public List<SkeletonRenderer> spineCharacters = new List<SkeletonRenderer>(); public List<Renderer> outlineRenderers = new List<Renderer>(); public List<int> outlineRendererColors = new List<int>(); public List<Renderer> eraseRenderers = new List<Renderer>(); public float lineThickness = 4f; public float lineIntensity = .5f; public Color lineColor1 = Color.red; public Color lineColor2 = Color.green; public Color lineColor3 = Color.blue; public bool flipY = false; private Material outline1Material; private Material outline2Material; private Material outline3Material; private Material outlineEraseMaterial; private Shader outlineShader; private Shader outlineBufferShader; private Shader outlineBufferShaderSpine; private Material outlineShaderMaterial; private RenderTexture renderTexture; private Camera _camera; Dictionary<Material, Material> materialMap = new Dictionary<Material, Material>(); List<Material[]> originalMaterials = new List<Material[]>(); int[] originalLayers = new int[1]; Material[] originalEraseMaterials = new Material[1]; int[] originalEraseLayers = new int[1]; void OnEnable() {
CreateMaterialsIfNeeded(); } void OnDisable() { DestroyMaterials(); if( _camera) { DestroyImmediate( _camera.gameObject); _camera = null; } } Material GetMaterialFromID(int ID) { if (ID == 0) return outline1Material; else if (ID == 1) return outline2Material; else return outline3Material; } Material CreateMaterial(Color emissionColor) { Material m = new Material(outlineBufferShader); m.SetColor("_Color", emissionColor); m.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha); m.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha); m.SetInt("_ZWrite", 0); m.DisableKeyword("_ALPHATEST_ON"); m.EnableKeyword("_ALPHABLEND_ON"); m.DisableKeyword("_ALPHAPREMULTIPLY_ON"); m.renderQueue = 3000; return m; } void Start () { spineCharacters = new List<SkeletonRenderer>(Object.FindObjectsOfType<SkeletonRenderer>()); if (spineCharacters != null && spineCharacters.Count != 0) { foreach ( SkeletonRenderer sr in spineCharacters ) { outlineRenderers.Add(sr.GetComponent<MeshRenderer>()); /* Good vs Evil BaseUnit b = sr.GetComponentInParent<BaseUnit>(); if ( b[PS.Faction] == Faction.Player) outlineRendererColors.Add(0); else outlineRendererColors.Add(1); */ }
} CreateMaterialsIfNeeded(); } void OnPreCull() { if (outlineRenderers != null && outlineRenderers.Count != 0) { Camera camera = GetComponent<Camera>(); int width = camera.pixelWidth; int height = camera.pixelHeight; renderTexture = RenderTexture.GetTemporary(width, height, 16, RenderTextureFormat.Default); if (_camera == null) { GameObject cameraGameObject = new GameObject("OutlineCamera"); cameraGameObject.hideFlags = HideFlags.HideAndDontSave; _camera = cameraGameObject.AddComponent<Camera>(); } _camera.CopyFrom(camera); _camera.renderingPath = RenderingPath.Forward; _camera.enabled = false; _camera.backgroundColor = new Color(0.0f, 0.0f, 0.0f, 0.0f); _camera.clearFlags = CameraClearFlags.SolidColor; _camera.cullingMask = LayerMask.GetMask("Outline"); if (outlineRenderers != null) { originalLayers = new int[outlineRenderers.Count]; for (int i = 0; i < outlineRenderers.Count; i++) { if (outlineRenderers[i] != null) { if ( i >= originalMaterials.Count ) originalMaterials.Add(outlineRenderers[i].sharedMaterials); else originalMaterials[i] = outlineRenderers[i].sharedMaterials; originalLayers[i] = outlineRenderers[i].gameObject.layer; Material[] newMats = outlineRenderers[i].sharedMaterials; for (int j = 0; j < originalMaterials[i].Length; j++) { Material subMaterial = null; if ( materialMap.TryGetValue(originalMaterials[i][j], out subMaterial) ) { newMats[j] = subMaterial; } else { Debug.Log("Lol, you need to make a new texture~"); } } outlineRenderers[i].sharedMaterials = newMats; outlineRenderers[i].gameObject.layer = LayerMask.NameToLayer("Outline"); } } } if (eraseRenderers != null) { originalEraseMaterials = new Material[eraseRenderers.Count]; originalEraseLayers = new int[eraseRenderers.Count]; for (int i = 0; i < eraseRenderers.Count; i++) { if (eraseRenderers[i] != null) { originalEraseMaterials[i] = eraseRenderers[i].sharedMaterial; originalEraseLayers[i] = eraseRenderers[i].gameObject.layer; eraseRenderers[i].sharedMaterial = outlineEraseMaterial; eraseRenderers[i].gameObject.layer = LayerMask.NameToLayer("Outline"); } } } _camera.targetTexture = renderTexture; _camera.Render(); if (outlineRenderers != null) { for (int i = 0; i < outlineRenderers.Count; i++) { if (outlineRenderers[i] != null) { outlineRenderers[i].sharedMaterials = originalMaterials[i]; outlineRenderers[i].gameObject.layer = originalLayers[i]; } } } if (eraseRenderers != null) { for (int i = 0; i < eraseRenderers.Count; i++) { if (eraseRenderers[i] != null) { eraseRenderers[i].sharedMaterial = originalEraseMaterials[i]; eraseRenderers[i].gameObject.layer = originalEraseLayers[i]; } } } } } void OnRenderImage( RenderTexture source, RenderTexture destination) { CreateMaterialsIfNeeded(); UpdateMaterialsPublicProperties(); outlineShaderMaterial.SetTexture("_OutlineSource", renderTexture); Graphics.Blit(source, destination, outlineShaderMaterial); RenderTexture.ReleaseTemporary(renderTexture); } private void CreateMaterialsIfNeeded() { if(outlineShader == null) outlineShader = Resources.Load<Shader>("OutlineEffect/OutlineShader"); if (outlineBufferShader == null) outlineBufferShader = Resources.Load<Shader>("OutlineEffect/OutlineBufferShader"); if(outlineShaderMaterial == null) { outlineShaderMaterial = new Material(outlineShader); outlineShaderMaterial.hideFlags = HideFlags.HideAndDontSave; UpdateMaterialsPublicProperties(); } if(outlineEraseMaterial == null) outlineEraseMaterial = CreateMaterial(new Color(0, 0, 0, 0)); if(outline1Material == null) outline1Material = CreateMaterial(new Color(1, 0, 0, 0)); if(outline2Material == null) outline2Material = CreateMaterial(new Color(0, 1, 0, 0)); if (outline3Material == null) outline3Material = CreateMaterial(new Color(0, 0, 1, 0)); if (spineCharacters != null && spineCharacters.Count != 0) { SkeletonRenderer sr = spineCharacters[0]; var mr = sr.GetComponent<MeshRenderer>(); for( int i = 0; i < mr.sharedMaterials.Length; i++ ) { var mat = mr.sharedMaterials[i]; Material newMat = null; if (outlineRendererColors != null && outlineRendererColors.Contains(0)) newMat = new Material(GetMaterialFromID(outlineRendererColors[0])); else newMat = new Material(outline1Material); #ifdef UNITY_4 var texture = mat.GetTexture(0); newMat.SetTexture(0,texture); materialMap[mat] = newMat; #endif #ifdef UNITY_5 newMat.mainTexture = original.mainTexture; #endif } } } private void DestroyMaterials() { DestroyImmediate(outlineShaderMaterial); DestroyImmediate(outlineEraseMaterial); DestroyImmediate(outline1Material); DestroyImmediate(outline2Material); DestroyImmediate(outline3Material); outlineShader = null; outlineBufferShader = null; outlineShaderMaterial = null; outlineEraseMaterial = null; outline1Material = null; outline2Material = null; outline3Material = null; } private void UpdateMaterialsPublicProperties() { if(outlineShaderMaterial) { outlineShaderMaterial.SetFloat("_LineThicknessX", lineThickness / 1000); outlineShaderMaterial.SetFloat("_LineThicknessY", (lineThickness * 2) / 1000); outlineShaderMaterial.SetFloat("_LineIntensity", lineIntensity); outlineShaderMaterial.SetColor("_LineColor1", lineColor1); outlineShaderMaterial.SetColor("_LineColor2", lineColor2); outlineShaderMaterial.SetColor("_LineColor3", lineColor3); if(flipY) outlineShaderMaterial.SetInt("_FlipY", 1); else outlineShaderMaterial.SetInt("_FlipY", 0); } } }

Attach the MonoBehaviour to your main camera, create a new Layer called "Outline".
Then set your camera to HDR mode if you want it to glow instead of just outline. Maybe some bloom if you feel like it.


22 Oct 2015, 21:07


If you make some improvements to this shader, please share with us 🙂

16 Tage später

Hi,

very interesting! We are trying to do something similar, but we have to draw oultines for many objects, that are created and destroyed during runtime.

Is there a way to avoid to use a global script like your, that must reference any skeleton?

Thanks a lot,

Devis

5 Tage später

Add or remove things to the global script object. Create a simple script that just adds the object when the script is enabled and removes it from the list when disabled.

e.g. (Pseudo code)

MyHelper : MonoBehaviour
{
  void OnEnable()
{
var OutlineEffObj =  GameObject.Find("MyOutlineCamera").GetComponent<OutlineEffect>()
OutlineEffObj.Add(this);
}

... same thing, but disable. 
}

This is important, because if you don't do it, then you'll be doing separate rendering passes per object - which will consume even more performance and look bad.


12 Nov 2015, 18:56


Edit:

Alternatively, if you would like to pay me to write the described script, I'd be happy to work out terms.

5 Tage später

en. i think you design has a problem when 2 spine character overlap,like thie below

4 Tage später

No, that's intended. 🙂

5 Tage später

en. but it's no satisfy my demand, do you have some ideas to solve this problem when 2 charaters overlap but one before another one?

4 Monate später

I guess you could do a separate render pass per character.


25 Mar 2016, 00:47


Found the issue with Unity 5.3.4

You need to change newMat.SexTexture(0, texture);

to

    newMat.mainTexture = original.mainTexture;
9 Tage später

I'm getting an error on OutlineEffect.cs saying that "original" doesn't exist in the current context. I've no idea where do add it or what to assign it to. Help please!

8 Tage später
Xelnath schrieb

I guess you could do a separate render pass per character.


25 Mar 2016, 00:47


Found the issue with Unity 5.3.4

You need to change newMat.SexTexture(0, texture);

to

    newMat.mainTexture = original.mainTexture;[/quote]

There is another error with Unity 5.3.4 :
error CS0103: The name `original' does not exist in the current context.
And I don't know how to fix it.Pls help me out. 😢 Waiting for your reply.Thanks.

PS.There should be a ' } ' at the end of function Start() and ‘using Spine.Unity;’ at the beginning of OutlineEffect.cs.(with Spine-Unity runtime 3.0)

7 Tage später

Okay, I have made the
}
and using "Spine.Unit" changes for future players.

To do more proper management, I would need to setup a public github, but that takes some time andI would need to solicit some donations in order to make up for lost time updating this.

Very cool!

ein Monat später

Hey, this has been serving me awesomely for a while now (thanks!), but for some reason the outline is appearing upside down on Mac. I found this in my Googling, but I'm not sure where to put this into the script. Any idea?

http://docs.unity3d.com/Manual/SL-PlatformDifferences.html

// On non-GL when AA is used, the main texture and scene depth texture
// will come out in different vertical orientations.
// So flip sampling of the texture when that is the case (main texture
// texel size will have negative Y).

#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
        uv.y = 1-uv.y;
#endif

Probably in the primary shader.

I am working on a mac this week, I'll take a look.

13 Tage später

Did you get a chance to look into it? 🙂

No, I didn't. I'm tracking down other more basic bugs right now 🙁

No worries, let me know if you ever do.