Help to find an asset/solution

Hey. Can anyone please advise me good Outline effect? I tried free assets and basically all that are based on shaders showed me this:

I use Spine for 2D animations, and the problem is that it does not create Sprite but Mesh, which is assembled from a texture atlas. On the screen this is clearly seen in the example of the characters head: the Outline line goes over the area that the Spine API cuts out of the texture atlas, rather than in the picture itself.

There is also a solution in the asset store that partially solves my problem:

Here is this asset:Outline Effect | Fullscreen & Camera Effects | Unity Asset Store
But it is no longer supported, and also does not work on some devices. But the main problem with this asset is performance. On my LG G2, it drop FPS from 60 to 25 (and it doesn’t matter if there are objects in the scene that need to be outlined)

I’m afraid to buy something from paid assets because I can pay money for what you can see on the first screen, but I’m ready to pay for a working (for my Spine objects) asset.

P.S. I also noticed that in Unity 2018 (maybe earlier) Outline Effect was added in the editor. It is clearly visible on the second screen on the right side. Perhaps Unity laid out this effect somewhere or maybe it is in the Unity sources?

up

There are generally 3 main kinds of outline shaders.

The expanded mesh, the post process, and the in-shader texture sample outline.

The expanded mesh is the one you’ll find most of the time, which renders the mesh using a shader that expands the vertices to puff up an object, or uses stencils to exclude the outline if you don’t want inner lines. This doesn’t work on flat meshes like sprites or spline characters because there’s nothing to puff up.

In-shader texture sample outlines work by sampling a texture multiple times with an offset and rendering a solid color where the offset samples have alpha, but the main sprite isn’t visible. Works great for single object UI elements or sprites, or even 3D meshes with transparency. But it outlines each “part” separately, so it’d work on your spline mesh, but each body part would have its own outline.

Post processing works great of anything and gets you much more accurate screen space lines than the other options due to working in screen space (which the other two do not). This is actually very similar to the in-shader texture sample method, in some cases exactly the same. The texture being sampled just happens to one rendered to the camera view rather than on the model. More advanced versions allow for a lot of different outline styles. The problem is it’s much more expensive, especially on mobile.

So obviously none of those options work. But you can kind of do a version that’s a mix of the above 3 techniques.
Render the object as a solid color 4 times with screen space offsets then render the object normally.

Shader "Unlit/Spine Outline"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _OutlineColor ("Outline Color", Color) = (1,1,1,1)
        _OutlineWidth ("Outline Width", Range(0, 4)) = 1
    }
    SubShader
    {
        Tags { "Queue"="Transparent" "RenderType"="Transparent" }
        LOD 100
        ZWrite Off
        Blend SrcAlpha OneMinusSrcAlpha

        CGINCLUDE
        #include "UnityCG.cginc"

        sampler2D _MainTex;
        float4 _MainTex_ST;

        fixed4 _OutlineColor;
        float _OutlineWidth;

        struct v2fOutline
        {
            float4 pos : SV_POSITION;
            float2 uv : TEXCOORD0;
        };

        v2fOutline vertOutline (appdata_base v, float2 offset)
        {
            v2fOutline o;
            o.pos = UnityObjectToClipPos(v.vertex);
            o.pos.xy += offset * 2 * o.pos.w * _OutlineWidth / _ScreenParams.xy;
            o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
            return o;
        }

        fixed4 fragOutline (v2fOutline i) : SV_Target
        {
            fixed alpha = tex2D(_MainTex, i.uv).a;
            fixed4 col = _OutlineColor;
            col.a *= alpha;
            return col;
        }
        ENDCG

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment fragOutline

            v2fOutline vert (appdata_base v)
            {
                return vertOutline(v, float2( 1, 1));
            }
            ENDCG
        }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment fragOutline

            v2fOutline vert (appdata_base v)
            {
                return vertOutline(v, float2(-1, 1));
            }
            ENDCG
        }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment fragOutline

            v2fOutline vert (appdata_base v)
            {
                return vertOutline(v, float2( 1,-1));
            }
            ENDCG
        }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment fragOutline

            v2fOutline vert (appdata_base v)
            {
                return vertOutline(v, float2(-1,-1));
            }
            ENDCG
        }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
                float4 color : TEXCOORD1;
            };

            v2f vert (appdata_full v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
                o.color = v.color;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                return tex2D(_MainTex, i.uv) * i.color;
            }
            ENDCG
        }
    }
}

Thanks for the answer! In truth, I don’t know how shaders are written. Now I’ve sat down to study the official Cg tutorial from Nvidia, I hope it helps me somehow. Perhaps you would recommend any material that teaches how to write shaders?

Many thanks!

@bgolus signed distance fields and only 2 passes for all?

Something like my samurai sprite example for the outline pass instead of 4 individual passes? Could work, yes. But has some potential problems for Spine since the the character components aren’t guaranteed to be all of the same resolution, and squash & stretch are common use cases. Could be overcome with fwidth() and a wide enough SDF “glow” so scaled parts don’t loose their outline. Depends on how much you scale objects.

edit: fix typo

very good point!

Please tell me, is it possible to render only the character (which will be marked with an Outline layer) on a second, disabled camera?

The fact is that my DragonBones API collects a character from a texture atlas, and I can’t make a normal Outline around the character itself (and not around each part of the body) without resorting to post processing.

I got the idea to wait until the character appears in the frame buffer (already assembled), transfer only the character to another camera, fill the background with a certain color (which will probably be absent in the character’s texture) and based on this data I can create an Outline around it , and then return the result back to the main camera.

The problem is that I do not quite understand how can I specifically transfer only a character to another camera, process it there, and then return it back.

Yep, totally possible … except what you’re describing is almost exactly the same as what a post process based outline would do, because that’s how they work. And it’s going to be too slow. A lot of what’s slow is that copying and compositing of multiple buffers back and forth which is what you’re adding back into the mix.

Did the example shader I posted not work for you? If not, then it would seem the character isn’t being rendered as a single mesh. If that’s the case you’d need to use two materials with different queues or render orders and render your characters twice. Try taking the shader I posted earlier, remove the last pass, and render that earlier, then render the character normally.

As soon as you provided your Outline shader, I immediately ran to check it.
I ran it and saw this:
5239955--523025--upload_2019-12-3_10-56-6.png
In addition, the number of parties for four such small zombies was 20. And in the game there will be several dozen, which in total will give several hundred batches. Is there a way to reduce batches?

But since from that moment I read the material that you provided + I searched in some training material myself, I could understand that the thing is that by default Culling is in the Front. After “Cull Off”(for some reason he thinks the belly is a Backface, lol), everything worked.
5239955--523031--upload_2019-12-3_11-3-37.png

I was very happy at that point, since you could help me solve one of the problems! I am very grateful to you.(by the way, I will continue to learning shaders in my free time anyway, since this is interesting, although some points are completely incomprehensible to me)

But another problem remained - displaying on top of objects such as buildings. The purpose of this shader in the game is to show that characters are behind buildings or trees (without it, they disappear from view, since the camera is isometric)
5239955--523034--upload_2019-12-3_11-4-34.png
I know that there is a Z buffer. If I done this with ZTest Always, then in this case the whole character will overlap the object, not just Outline.

Could you please tell me how to solve this problem?

If I understand the shader correctly, it creates a copy behind the character, but a little larger and filled with _OutlineColor color. And if so, then when I try to bring only Outline to the foreground, only the fill will be displayed and the Outline effect will disappear, right?

Yep. The outline only exists because the character’s sprite covered up the middle fill. You can use the Frame Debugger window to step through rendering and see each pass render to help you understand things better if you need to.

So, is the goal to only show the outline when behind a wall, or all the time?

For 3D rendering, usually most of the stuff in the scene is opaque with hard edges, so you can rely on ZTest and stencils pretty easily. Sprites are a little harder because everything is usually alpha blended with no ZWrite or hard edges that would allow for easy stencil usage.

For an outline that shows all the time, you could use a stencil based shader that draws after everything else. You’d draw the character once using an alpha tested shader that writes only to the stencil buffer. Then render the 4 offset passes testing against the stencil to exclude it from the center. This means a hard edge on the interior of the outline, but it will work. An alternative would be to do something with destination alpha, which is abusing the mostly ignored alpha channel of the render buffer and careful and creative control of the blend mode. It means a nicer, smooth alpha interior edge. But it also means everything else being rendered needs to careful about what goes into the rendered alpha.

If you want it to only show when behind stuff, that gets more complicated. That probably requires having objects in front of the character write to the stencil as well, probably using alpha tested shaders that only render to the stencil.

ZTest Greater ?

I think to draw constantly on top of all objects. But if I do ZWrite Always, I get the whole character on top of all the items, but I need only Outline.

Sprites don’t usually write to the depth, so only rendering order matters.

but …

The fact ZWrite does anything here confuses me. Does this mean you’re using opaque shaders with ZWrite On (or perhaps more accurately without ZWrite Off)?

Note the technique I described in my last post requires the outline be a completely separate shader & material from the character’s main material. Basically the render order would be:

  • Render Background Objects (default sprite shader)
  • Render Character (default sprite shader)
  • Render Foreground Objects (default sprite shader)
  • Render Character Outline (custom outline shader)

If your character mesh only has one material index, you can add a second material which will cause the mesh to render twice, once with each material (and pop up a warning in the inspector you can ignore).

Here’s some example shaders of the two methods:
Stencil based

Shader "Unlit/Spine Outline Stencil"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _OutlineColor ("Outline Color", Color) = (1,1,1,1)
        _OutlineWidth ("Outline Width", Range(0, 4)) = 1
    }
    SubShader
    {
        Tags { "Queue"="Transparent" "RenderType"="Transparent" }
        LOD 100
        ZWrite Off
        Blend SrcAlpha OneMinusSrcAlpha

        CGINCLUDE
        #include "UnityCG.cginc"

        sampler2D _MainTex;
        float4 _MainTex_ST;

        fixed4 _OutlineColor;
        float _OutlineWidth;

        struct v2fOutline
        {
            float4 pos : SV_POSITION;
            float2 uv : TEXCOORD0;
        };

        v2fOutline vertOutline (appdata_base v, float2 offset)
        {
            v2fOutline o;
            o.pos = UnityObjectToClipPos(v.vertex);
            o.pos.xy += offset * 2 * o.pos.w * _OutlineWidth / _ScreenParams.xy;
            o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
            return o;
        }

        fixed4 fragOutline (v2fOutline i) : SV_Target
        {
            fixed alpha = tex2D(_MainTex, i.uv).a;
            fixed4 col = _OutlineColor;
            col.a *= alpha;
            return col;
        }
        ENDCG

        Pass
        {
            Name "MASK"
            Stencil {
                Ref 1
                Pass Replace
            }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            v2f vert (appdata_full v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
                return o;
            }

            void frag (v2f i)
            {
                clip(tex2D(_MainTex, i.uv).a - 0.5);
            }
            ENDCG
        }

        Pass
        {
            Stencil {
                Ref 1
                Comp NotEqual
            }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment fragOutline

            v2fOutline vert (appdata_base v)
            {
                return vertOutline(v, float2( 1, 1));
            }
            ENDCG
        }

        Pass
        {
            Stencil {
                Ref 1
                Comp NotEqual
            }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment fragOutline

            v2fOutline vert (appdata_base v)
            {
                return vertOutline(v, float2(-1, 1));
            }
            ENDCG
        }

        Pass
        {
            Stencil {
                Ref 1
                Comp NotEqual
            }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment fragOutline

            v2fOutline vert (appdata_base v)
            {
                return vertOutline(v, float2( 1,-1));
            }
            ENDCG
        }

        Pass
        {
            Stencil {
                Ref 1
                Comp NotEqual
            }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment fragOutline

            v2fOutline vert (appdata_base v)
            {
                return vertOutline(v, float2(-1,-1));
            }
            ENDCG
        }
    }
}

Will produce an outline with no interior color, but has a hard interior edge.
5246960--524033--upload_2019-12-4_13-6-19.png

Destination Alpha

Shader "Unlit/Spine Outline Dest Alpha"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _OutlineColor ("Outline Color", Color) = (1,1,1,1)
        _OutlineWidth ("Outline Width", Range(0, 4)) = 1
    }
    SubShader
    {
        Tags { "Queue"="Transparent" "RenderType"="Transparent" }
        LOD 100
        ZWrite Off
        Blend OneMinusDstAlpha DstAlpha
        ColorMask RGB

        CGINCLUDE
        #include "UnityCG.cginc"

        sampler2D _MainTex;
        float4 _MainTex_ST;

        fixed4 _OutlineColor;
        float _OutlineWidth;

        struct v2fOutline
        {
            float4 pos : SV_POSITION;
            float2 uv : TEXCOORD0;
        };

        v2fOutline vertOutline (appdata_base v, float2 offset)
        {
            v2fOutline o;
            o.pos = UnityObjectToClipPos(v.vertex);
            o.pos.xy += offset * 2 * o.pos.w * _OutlineWidth / _ScreenParams.xy;
            o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
            return o;
        }

        fixed4 fragOutlineMask (v2fOutline i) : SV_Target
        {
            fixed alpha = tex2D(_MainTex, i.uv).a;
            fixed4 col = _OutlineColor;
            col.a *= alpha;
            return 1 - col.a;
        }

        fixed4 fragOutline (v2fOutline i) : SV_Target
        {
            fixed alpha = tex2D(_MainTex, i.uv).a;
            fixed4 col = _OutlineColor;
            col.a *= alpha;
            return col;
        }
        ENDCG

        Pass
        {
            Name "OUTLINEALPHA"
            BlendOp Min
            ColorMask A

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment fragOutlineMask

            v2fOutline vert (appdata_base v)
            {
                return vertOutline(v, float2( 1, 1));
            }
            ENDCG
        }

        Pass
        {
            Name "OUTLINEALPHA"
            BlendOp Min
            ColorMask A

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment fragOutlineMask

            v2fOutline vert (appdata_base v)
            {
                return vertOutline(v, float2(-1, 1));
            }
            ENDCG
        }

        Pass
        {
            Name "OUTLINEALPHA"
            BlendOp Min
            ColorMask A

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment fragOutlineMask

            v2fOutline vert (appdata_base v)
            {
                return vertOutline(v, float2( 1,-1));
            }
            ENDCG
        }

        Pass
        {
            Name "OUTLINEALPHA"
            BlendOp Min
            ColorMask A

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment fragOutlineMask

            v2fOutline vert (appdata_base v)
            {
                return vertOutline(v, float2(-1,-1));
            }
            ENDCG
        }

        Pass
        {
            Name "CENTERMASK"
            ColorMask A
            Blend Zero One, One OneMinusSrcAlpha

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            v2f vert (appdata_full v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                return tex2D(_MainTex, i.uv);
            }
            ENDCG
        }

        Pass
        {
            Name "OUTLINECOLOR"
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment fragOutline

            v2fOutline vert (appdata_base v)
            {
                return vertOutline(v, float2( 1, 1));
            }
            ENDCG
        }

        Pass
        {
            Name "OUTLINECOLOR"
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment fragOutline

            v2fOutline vert (appdata_base v)
            {
                return vertOutline(v, float2(-1, 1));
            }
            ENDCG
        }

        Pass
        {
            Name "OUTLINECOLOR"
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment fragOutline

            v2fOutline vert (appdata_base v)
            {
                return vertOutline(v, float2( 1,-1));
            }
            ENDCG
        }

        Pass
        {
            Name "OUTLINECOLOR"
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment fragOutline

            v2fOutline vert (appdata_base v)
            {
                return vertOutline(v, float2(-1,-1));
            }
            ENDCG
        }
    }
}

No interior color, soft interior edge!
Also twice as many passes, and requires the render target alpha be pure white before it renders. Need to make sure camera clears to a color with 100% alpha, and nothing else is writing anything but white either.
5246960--524039--upload_2019-12-4_13-7-59.png

5 Likes

i did not read carefully enough… sorry.

Thank you very much! Here is the result I wanted to get:
5248280--524201--upload_2019-12-5_8-37-59.png
Please tell me, do you have a Patreon or something like that?