- Edited
Spine object outline
We are gonna add outline to our sprite objects, is there any way?
There is no easy way to outline the whole skeleton. The best way to do it is using a shader effect if your game toolkit supports it. Eg, render the skeleton to an FBO, then draw the FBO using a shader that colors pixels that are translucent (aka an edge highlight).
BTW, you can use tint black to make a "flash" which is a solid color silhouette.
Attachments - Spine User Guide: Tint black
It's possible but not simple in Unity, unfortunately.
This is true for any multi-Sprite setup, not just Spine.
If you had to google for it, I'd go for "2d sprite outline unity"
You have to make sure to distinguish from 3D outlines, which are usually just the trick where they expand mesh vertices. That won't work for alpha blended sprites.
Here's one I found that works but doesn't have anti-aliasing. https://forum.unity.com/threads/free-open-source-outline-effect.314362/
Wow awesome find!! This is pretty useful, thanks for the link Pharan!
Pharan wroteIt's possible but not simple in Unity, unfortunately.
This is true for any multi-Sprite setup, not just Spine.If you had to google for it, I'd go for "2d sprite outline unity"
You have to make sure to distinguish from 3D outlines, which are usually just the trick where they expand mesh vertices. That won't work for alpha blended sprites.Here's one I found that works but doesn't have anti-aliasing. https://forum.unity.com/threads/free-open-source-outline-effect.314362/
Great!!!!!!
Hi everyone, I know this is quite old post, but I'm just wondering if anyone has found a better solution on drawing outlines out of a spine asset.
The main issue of using the solution highlighted by Pharan is that the outlines of different sprites are not decently combined. Every asset (with outlines) should have a separate render texture, and even so, I saw that outline above outline fight each other.
So what I did was to add a gameObject behind the spine asset and set another material with a simple shader that draws an outline in the frag shader function.
so what's the problem here? It's about performance:
- Even using the same material for all the assets there are 6 batches, maybe because I created a MaterialPropertyBlock for each asset.
- The meshes are duplicated
- The overdraw doubles
So here I propose the question again, does anyone have a better solution?
thanks in advance for any suggestions.
Can you render multiple skeletons to a render texture, then apply an outline to that?
Nate wroteCan you render multiple skeletons to a render texture, then apply an outline to that?
yes but if I do so I will have an outline surrounding the shape of all the combined sprite assets.
And taking a snapshot of every sprite from the camera will generate the second image
Ah I misread that you wanted that. When outlining each skeleton separately, you said they aren't "decently combined". What is it about that you don't like?
Nate wroteWhat is it about that you don't like
the result I get with the render texture is the last image I showed you.
What I want to achieve is the first one in the first post with the outlines sorted in the same order with the characters, but without duplicating meshes.
Now I'm digging in your Spine/sprite/unlit in order to understand how "Write to Depth" works and maybe using Stencil, but I'm not a shader expert and it's really overwhelming.
NicolaSirago wroteSo what I did was to add a gameObject behind the spine asset and set another material with a simple shader that draws an outline in the frag shader function.
so what's the problem here? It's about performance:
- Even using the same material for all the assets there are 6 batches, maybe because I created a MaterialPropertyBlock for each asset.
- The meshes are duplicated
- The overdraw doubles
If you have achieved the desired correct outline without using any render textures, you are already saving a lot performance wise.
Unless you activate depth write (write to the z buffer), you will have a hard time improving on any of the above three drawbacks, for the following reasons:
-
Batch increase: Even when using only an additional pass in the shader at a single material, it will create a separate draw call for this pass. A separate pass is almost always required, if you need the normal Spine character rendered on top of the outline without outlines around each individual body part.
-
The meshes are duplicated depending on what you mean by this: you could and should reuse the existing Mesh, you should not regenerate the identical mesh (which I assume you don't). The easiest way to achieve reuse of the same Mesh is to create a shader with an additional pass for the outline. You could duplicate your desired shader and then add a pass to it that creates the outline.
-
The overdraw doubles. This will always be the case if you need correct outlines (and not outlines around e.g. every single arm or leg part), unless you enable depth write.
Now I'm digging in your Spine/sprite/unlit in order to understand how "Write to Depth" works and maybe using Stencil, but I'm not a shader expert and it's really overwhelming.
You could also have a look at the simpler shader Spine/Skeleton Lit ZWrite
instead, which is shorter and less complex.
Anyway, you will face some drawbacks when using "write to depth" (ZWrite): When relying on the Z-buffer for sorting your parts, your outlines and border regions will not be alpha-blended nicely, but instead will show a stepping-effect (an aliasing effect similar to what you see in 3D games at geometry borders when disabling anti-aliasing).
Regarding the stencil buffer: since Unity uses the stencil buffer for sprite masks, modifying and reading it will have some side effects with existing masks. So we would not recommend using the stencil buffer. Apart from that, you will receive a joint outline of multiple Spine skeletons when using a single stencil buffer, which is not what you said you want.
So I would heavily recommend using your existing solution or moving it to a separate pass in a copy of an existing shader - this will result in the best visual result without any artifacts or degradation of quality.
Harald wrote[
thank you very much for the extended answer, so it seems I was on the right track, (apart from copying the mesh, yes I was doing that :shh: )
anyway, here the result I get so far:
obviously we can improve a lot, but it already does what we need
Shader "Spine/Special/Outline" {
Properties {
_Cutoff ("Shadow alpha cutoff", Range(0,1)) = 0.1
[NoScaleOffset] _MainTex ("Main Texture", 2D) = "black" {}
[Toggle(_STRAIGHT_ALPHA_INPUT)] _StraightAlphaInput("Straight Alpha Texture", Int) = 0
[HideInInspector] _StencilRef("Stencil Reference", Float) = 1.0
[Enum(UnityEngine.Rendering.CompareFunction)] _StencilComp("Stencil Comparison", Float) = 8 // Set to Always as default
[PerRendererData]_Threshold("threshold", Float) = 0.5
[PerRendererData] _Outline("Outline", Float) = 0
[PerRendererData] _OutlineColor("Outline Color", Color) = (1,0,0,1)
}
CGINCLUDE
#include "UnityCG.cginc"
ENDCG
SubShader {
Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "PreviewType"="Plane" }
Fog { Mode Off }
Cull Off
ZWrite Off
Blend One OneMinusSrcAlpha
Lighting Off
Stencil {
Ref[_StencilRef]
Comp[_StencilComp]
Pass Keep
}
Pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
sampler2D _MainTex;
fixed4 _MainTex_TexelSize;
fixed _Threshold;
fixed _Outline;
fixed4 _OutlineColor;
struct VertexInput {
fixed4 vertex : POSITION;
fixed2 uv : TEXCOORD0;
};
struct VertexOutput {
fixed4 pos : SV_POSITION;
fixed2 uv : TEXCOORD0;
};
VertexOutput vert(VertexInput v) {
VertexOutput o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
fixed4 frag(VertexOutput i) : SV_Target{
//fixed4 texColor = tex2D(_MainTex, i.uv);
fixed4 texColor = fixed4(0,0,0,0);
if (_Outline > 0 && texColor.a <= _Threshold) {
// Get the neighbouring four pixels.
fixed pixelUp = tex2D(_MainTex, i.uv + fixed2(0, _MainTex_TexelSize.y*_Outline)).a;
fixed pixelDown = tex2D(_MainTex, i.uv - fixed2(0, _MainTex_TexelSize.y*_Outline)).a;
fixed pixelRight = tex2D(_MainTex, i.uv + fixed2(_MainTex_TexelSize.x*_Outline, 0)).a;
fixed pixelLeft = tex2D(_MainTex, i.uv - fixed2(_MainTex_TexelSize.x*_Outline, 0)).a;
if (pixelUp + pixelDown + pixelRight + pixelLeft > _Threshold)
{
texColor.rgba = _OutlineColor;
}
}
return texColor;
}
ENDCG
}
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma shader_feature _ _STRAIGHT_ALPHA_INPUT
sampler2D _MainTex;
struct VertexInput {
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float4 vertexColor : COLOR;
};
struct VertexOutput {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float4 vertexColor : COLOR;
};
VertexOutput vert (VertexInput v) {
VertexOutput o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
o.vertexColor = v.vertexColor;
return o;
}
float4 frag (VertexOutput i) : SV_Target {
float4 texColor = tex2D(_MainTex, i.uv);
#if defined(_STRAIGHT_ALPHA_INPUT)
texColor.rgb *= texColor.a;
#endif
return (texColor * i.vertexColor);
}
ENDCG
}
Pass {
Name "Caster"
Tags { "LightMode"="ShadowCaster" }
Offset 1, 1
ZWrite On
ZTest LEqual
Fog { Mode Off }
Cull Off
Lighting Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_shadowcaster
#pragma fragmentoption ARB_precision_hint_fastest
sampler2D _MainTex;
fixed _Cutoff;
struct VertexOutput {
V2F_SHADOW_CASTER;
float4 uvAndAlpha : TEXCOORD1;
};
VertexOutput vert (appdata_base v, float4 vertexColor : COLOR) {
VertexOutput o;
o.uvAndAlpha = v.texcoord;
o.uvAndAlpha.a = vertexColor.a;
TRANSFER_SHADOW_CASTER(o)
return o;
}
float4 frag (VertexOutput i) : SV_Target {
fixed4 texcol = tex2D(_MainTex, i.uvAndAlpha.xy);
clip(texcol.a * i.uvAndAlpha.a - _Cutoff);
SHADOW_CASTER_FRAGMENT(i)
}
ENDCG
}
}
}
Glad to hear that it helped, thanks for sharing the shader code!
What I forgot to add in the previous post:
Neighbourhood sampling based outline generation (as you posted above) comes with added cost for larger outline-width. If you only need one pixel wide outlines, that's perfectly fine of course.
In case you need variable-width outlines, distance fields could come in handy. Usually used for crisp font rendering, it is also used for outline rendering without the need for neighbourhood sampling. However, this requires custom prepared image data, which is not too trivial in combination with atlas generation, etc. Just mentioning for the sake fo completeness - your approach is perfectly valid and looks good.
You could also add some antialiasing/smoothing by not only testing against a threshold like this:
if (pixelUp + pixelDown + pixelRight + pixelLeft > _Threshold)
{
texColor.rgba = _OutlineColor;
}
but instead having a semi-transparent outline at corner pixels:
// _ThresholdStart = 0.5
// _ThresholdEnd = 2.0
fixed sum = pixelUp + pixelDown + pixelRight + pixelLeft;
fixed outlineAlpha = (sum - _ThresholdStart) / (_ThresholdEnd - _ThresholdStart);
texColor.rgba = lerp(texColor, _OutlineColor, outlineAlpha);
This way you will get smooth outlines.
Will this be included in Unity runtime?
After thinking about it, I came to the conclusion that I will add outline functionality to all Spine shaders.
I have created a ticket here:
[unity] Provide outline at shaders · #1531
Harald wroteAfter thinking about it, I came to the conclusion that I will add outline functionality to all Spine shaders.
I have created a ticket here:
[unity] Provide outline at shaders · #1531
WOW Thank you, Harald! I'm glad I dug up the issue.
My current goal is to keep the shader doing just a single pass, to improve vertex numbers and most of all keeping the dynamic batching capability. It seems quite hard to figure out.
What I want to do is to first isolate the vertices that enclose the animation, and then do the alpha test just for those pixels that are much closer to the external edges, but I don't understand yet how it's doable. :think: :think: :think:
You're welcome, thanks for the input. The perfect solution (distance field based) would be a lot of work, but a good solution with some constraints might already help a lot of people.
NicolaSirago wroteMy current goal is to keep the shader doing just a single pass, to improve vertex numbers and most of all keeping the dynamic batching capability. It seems quite hard to figure out.
The problem with any single-pass solution will be to remove outlines at overlapping body parts, which is not trivial:
With unchanged geometry, I can only think of a single pass solution using the depth buffer and custom different written depth values for outline (further behind at increased depth) and solid parts (in front at unmodified depth). Writing custom depth values in the pixel shader however comes with a performance penalty, which would most likely outweight the benefits.
The best single pass solution I could think of would be to modify the mesh generation in the skeleton to output the geometry twice in the same vertex buffer, once for the outline with modified z position (further behind), followed by the normal mesh. You would then most likely need to add vertex attributes that encode the isOutlineMeshVertex
information, and then only draw the outline or inner part in the shader respectively.
At least I cannot currently think of an easy single pass solution. If anyone comes up with better ideas, I would love to hear them of course! :nerd:
Hi, any progress on this?
We have not yet started this task, but it will be in the near future.
Outline functionality has now been added to all spine-unity 3.8 and 3.9 shaders. New unitypackages are available for download here as usual:
Spine Unity Download
We will add an example scene and an announcement blog post for this feature soon. Until then, please consult the changelog, section Additions
- Outline rendering functionality for all shaders
on how to use this feature.