slightly better outlines

 This approach to outlines is a bit more robust & adds outlines to areas within the shape's silhouette - eg details around say the nose and eyes, vs just the outline of the head.

It utilises passes - first drawing the shape as normal with a standard struct input and void surf... then adding outlines on top using a vert-frag shader. This is quite cool to see both surface and vert frag shaders being mixed
Things to note - we Cull the Front...We're essentially rendering the inside of the mesh as the outlines.

I've found an explanation online about this shader, pasted below the code..

From the docs -
UNITY_MATRIX_IT_MV Inverse transpose of model * view matrix.

 I'm not entirely sure what is happening with this code - but I'm fairly sure we're taking the vertex normal and multiplying it by something, before getting it into screenspace.

 Shader "Dave/Unlit/advancedOutline"
{
    Properties
    {
        _MainTex("Texture",2D) = "white"{}
        _OutlineColor("Outline Colour",color)=(0,0,0,1)
        _Outline("Outline Width",Range(-0.3,1))=0.005
    }

    SubShader{

        CGPROGRAM
        #pragma surface surf Lambert
        struct Input {
            float2 uv_MainTex;
            float3 normal;

        };

        sampler2D _MainTex;
        void surf(Input IN, inout SurfaceOutput o) {

            o.Albedo = tex2D(_MainTex, IN.uv_MainTex).rgb;
        }



        ENDCG

        Pass{
            Cull Front
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            
            #include "UnityCG.cginc"

            struct appdata {
                float4 vertex:POSITION;
                float3 normal:NORMAL;

            };
            
        struct v2f {
            float4 pos:SV_POSITION;
            fixed4 color : COLOR;

            };
        
        float _Outline;
        float4 _OutlineColor;
        
        v2f vert(appdata v) {
            v2f o;
            o.pos = UnityObjectToClipPos(v.vertex);

            float3 norm = normalize(mul((float3x3)UNITY_MATRIX_IT_MV, v.normal));
            //get worldspace normal
            float2 offset = TransformViewToProjection(norm.xy);
            
            o.pos.xy += offset * o.pos.z*_Outline;

            o.color = _OutlineColor;
            return o;
        }

        fixed4 frag(v2f i):SV_TARGET
        {
            return i.color;

        }


            ENDCG

            }
        }

    Fallback "Diffuse"
}

 
Explanation found online, pasted here in case it goes down - https://www.undefinedgames.org/2019/04/24/cel-shading-basics/

The structs are nothing out of the ordinary, the inclusion of a color property will be used to set the eventual outline color:

 struct appdata
 {
     float4 vertex : POSITION;
     float3 normal : NORMAL;
 };

 struct v2f
 {
     float4 pos : SV_POSITION;
     fixed4 color : COLOR;
 };

The _Outline and _OutlineColor properties are used to set the width of the outline as well as the colour. In actuality, this means setting the level of extrusion by which the second render is done, as well as the colouring of the mesh:

 float _Outline;
 float4 _OutlineColor;

All of the logic to handle this kind of effect, is done within the vertex function, vert. The vertex function is where displacement of the vertices in the mesh are performed:

v2f vert(appdata v)
 {
     v2f o;
     o.pos = UnityObjectToClipPos(v.vertex);

      float3 norm = normalize(mul((float3x3)UNITY_MATRIX_IT_MV, v.normal));
     float2 offset = TransformViewToProjection(norm.xy);

     o.pos.xy += offset * o.pos.z * _Outline;
    o.color = _OutlineColor;
    return o;
 }

What happens in the first two lines is nothing out of the ordinary – the vertices from object space are being converted to the vertices in clip space with the UnityObjectToClipPos function.

The next line is pretty confusing if you are not familiar with the way matrix conversions work, however, to summarize:

float3 norm = normalize(mul((float3x3)UNITY_MATRIX_IT_MV, v.normal));

Means that the normal of the object is being turned into a normal as a world coordinate (it has something to do with the “matrix conversion being inversely transposed,” just know that the command is necessary).

The second line is also a little convoluted, but fairly easy to grasp once you get the basics of it:

 float2 offset = TransformViewToProjection(norm.xy);

What this line does is generate a 2D “offset” value by taking the aforementioned world-position normal and projecting it onto clipping space – in other words, this is the conversion from world space to clipping space which allows for manipulation in relation to the screen. This offset value is then applied to the xy-coordinates of the vertices, multiplied by the z-coordinate (depth) and the _Outline property to generate a nice looking outline around the model (the colour is also applied here, to be accessed by the frag function afterwards). Basically, this is where we bloat the model:

o.pos.xy += offset * o.pos.z * _Outline;

o.color = _OutlineColor;

Notice in the multiplication that the o.pos.z value is also used as a parameter. What this does is act as a scaling factor to ensure that the thickness of the line remains the same across the model, no matter how far away the individual parts of the model are from the camera.

All the fragment function frag does, is apply the colour on the model:

 fixed4 frag(v2f i) : SV_Target
 {
     return i.color;
 }

To summarize the entire process, consider it as follows:

  1. An appdata struct containing the vertex positions and normals of the model, is passed into the vertex function
  2. The vertex function instantiates a v2f struct o, to contain the clip space coordinates for the converted vertices and the desired colour for the outline
  3. The object space coordinates (3D) of the vertices, are converted into clip space coordinates (2D) by the UnityObjectToClipPos function
  4. The object space normals on the model are being transformed into normalized worldspace normals
  5. Since the vertex positions in the v2f struct are in clip space, any modifications of these must also be in 2D. Therefore an offset value is found by projecting the worldspace normals onto clip space, via TransformViewToProjection, taking in the X- and Y-coordinates of the worldspace normals
  6. The offset is added to the vertex clip space coordinates, multiplied by the o.pos.z (to act as a scaling factor for consistent line thickness across the model), multiplied again by the _Outline scaling factor
  7. The colour is applied
  8. The whole v2f struct is passed onto the frag(fragment) function, which then prints the colour out

 

Comments

Popular posts from this blog

setting VFX graph properties using C#