반응형

One of the most useful effects that isn’t already present in Unity is outlines.

Screenshot from Left 4 Dead.
There are some scripts online that make the geometry bigger and then render it a second time, behind the first. This isn't what we want.

Artifacts from normal extrusion

What we need is a post-processing effect. We'll start by rendering only the objects we want outlined to an image.
To do this, we'll use culling masks, which use layers to filter what objects should be rendered.

Make a new 'Outline' layer
Then, we attach a script to our camera, which will create and render a second camera, masking everything that isn't to be outlined.

 

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class outline : MonoBehaviour
{
	public Shader DrawAsSolidColor;
	public Shader Outline;
	Material _outlineMaterial;
	Camera TempCam;

	void Start(){
		_outlineMaterial = new Material(Outline);

		//setup the second camera which will render outlined objects
		TempCam = new GameObject().AddComponent<Camera>();
	}
	
	//OnRenderImage is the hook after our scene's image has been rendered, so we can do post-processing.
	void OnRenderImage(RenderTexture src, RenderTexture dst){
		//set up the temporary camera
		TempCam.CopyFrom(Camera.current);
		TempCam.backgroundColor = Color.black;
		TempCam.clearFlags = CameraClearFlags.Color;

		//cull anything that isn't in the outline layer
		TempCam.cullingMask = 1 << LayerMask.NameToLayer("Outline");
		
		//allocate the video memory for the texture
		var rt = RenderTexture.GetTemporary(src.width,src.height,0,RenderTextureFormat.R8);
		
		//set up the camera to render to the new texture
		TempCam.targetTexture = rt;

		//use the simplest 3D shader you can find to redraw those objects
		TempCam.RenderWithShader(DrawAsSolidColor, "");
		
		//pass the temporary texture through the material, and to the destination texture.
		Graphics.Blit(rt, dst, _outlineMaterial);
		
		//free the video memory
		RenderTexture.ReleaseTemporary(rt);
	}
}

 

Note the line:

TempCam.cullingMask = 1 << LayerMask.NameToLayer("Outline");

<< is the bit-shift operator. This takes the bits from the number on the left, and shifts them over by the number on the right. If 'Outline' is layer 8, it will shift 0001 over by 8 bits, giving the binary value 100000000. The hardware can this value to efficiently mask out other layers with a single bitwise AND operation.

We'll also need a shader to redraw our objects. No need for lighting or anything complicated, just a solid color.

 

 

 

 

Shader "Unlit/NewUnlitShader"
{
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
            };

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

            fixed4 frag (v2f i) : SV_Target
            {
                return fixed4(1,1,1,1);
            }
            ENDCG
        }
    }
}

 

 

Finally, let's make a shader that passes an image through as it received it. We'll use this file for outlines later.

Shader "Custom/Post Outline"
{
    Properties
    {
        //Graphics.Blit() sets the "_MainTex" property to the source texture
        _MainTex("Main Texture",2D)="black"{}
    }
    SubShader 
    {
        Pass 
        {
            CGPROGRAM
            sampler2D _MainTex;
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
             
            struct v2f 
            {
                float4 pos : SV_POSITION;
                float2 uvs : TEXCOORD0;
            };
             
            v2f vert (appdata_base v) 
            {
                v2f o;
                 
                //Despite only drawing a quad to the screen from -1,-1 to 1,1, Unity altered our verts, and requires us to use UnityObjectToClipPos.
                o.pos = UnityObjectToClipPos(v.vertex);
                 
                //Also, the UVs show up in the top right corner for some reason, let's fix that.
                o.uvs = o.pos.xy / 2 + 0.5;
                 
                return o;
            }
             
             
            half4 frag(v2f i) : COLOR 
            {
                //return the texture we just looked up
                return tex2D(_MainTex,i.uvs.xy);
            }
             
            ENDCG
 
        }
        //end pass        
    }
    //end subshader
}
//end shader

 


Put the outline component on the camera, drag the shaders to the component, and now we have our mask!

* Render just the geometry to be outlined

 

 

 

Once we have a mask, let's sample every pixel nearby, and if the mask is present, add some color to this pixel. This effectively makes the mask bigger and blurry.

Shader "Custom/Post Outline"
{
    Properties
    {
        _MainTex("Main Texture",2D)="black"{}
    }
    SubShader 
    {
        Pass 
        {
            CGPROGRAM
     
            sampler2D _MainTex;            
            
            //<SamplerName>_TexelSize is a float2 that says how much screen space a texel occupies.
            float2 _MainTex_TexelSize;

            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
             
            struct v2f 
            {
                float4 pos : SV_POSITION;
                float2 uvs : TEXCOORD0;
            };
             
            v2f vert (appdata_base v) 
            {
                v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
                o.uvs = o.pos.xy / 2 + 0.5;
                return o;
            }
                          
            half4 frag(v2f i) : COLOR 
            {
                //arbitrary number of iterations for now
                int NumberOfIterations=19;
 
                //turn "_MainTex_TexelSize" into smaller words for reading
                float TX_x=_MainTex_TexelSize.x;
                float TX_y=_MainTex_TexelSize.y;
 
                //and a final intensity that increments based on surrounding intensities.
                float ColorIntensityInRadius=0;
 
                //for every iteration we need to do horizontally
                for(int k=0;k < NumberOfIterations;k+=1)
                {
                    //for every iteration we need to do vertically
                    for(int j=0;j < NumberOfIterations;j+=1)
                    {
                        //increase our output color by the pixels in the area
                        ColorIntensityInRadius+=tex2D(
                                                      _MainTex, 
                                                      i.uvs.xy+float2
                                                                   (
                                                                        (k-NumberOfIterations/2)*TX_x,
                                                                        (j-NumberOfIterations/2)*TX_y
                                                                   )
                                                     ).r;
                    }
                }
 
                //output some intensity of teal
                return ColorIntensityInRadius*half4(0,1,1,1)*0.005;
            }
             
            ENDCG
 
        }
        //end pass        
    }
    //end subshader
}
//end shader


* Sampling surrounding pixels, we redraw the previous image but all colored elements are now bigger

 

 

Then, sample the input pixel directly underneath, and if it's colored, draw black instead:

*Hey, now it's looking like an outline!

 

Looking good! Next, we'll need to pass the scene to our shader, and have the shader draw the original scene instead of black.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class outline : MonoBehaviour
{

	public Shader DrawAsSolidColor;
	public Shader Outline;
	Material _outlineMaterial;
	Camera TempCam;
	
	void Start()
    {
		_outlineMaterial = new Material(Outline);
		TempCam = new GameObject().AddComponent<Camera>();
	}
	
	void OnRenderImage(RenderTexture src, RenderTexture dst){
		TempCam.CopyFrom(Camera.current);
		TempCam.backgroundColor = Color.black;
		TempCam.clearFlags = CameraClearFlags.Color;

		TempCam.cullingMask = 1 << LayerMask.NameToLayer("Outline");

		var rt = RenderTexture.GetTemporary(src.width,src.height,0,RenderTextureFormat.R8);
		TempCam.targetTexture = rt;

		TempCam.RenderWithShader(DrawAsSolidColor, "");

		_outlineMaterial.SetTexture("_SceneTex", src);
		Graphics.Blit(rt, dst, _outlineMaterial);

		RenderTexture.ReleaseTemporary(rt);
	}
}

 

Shader "Custom/Post Outline"
{
    Properties
    {
        _MainTex("Main Texture",2D)="black"{}
        _SceneTex("Scene Texture",2D)="black"{}
    }
    SubShader 
    {
        Pass 
        {
            CGPROGRAM
            sampler2D _MainTex;    
            sampler2D _SceneTex;
            float2 _MainTex_TexelSize;
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
             
            struct v2f 
            {
                float4 pos : SV_POSITION;
                float2 uvs : TEXCOORD0;
            };
             
            v2f vert (appdata_base v) 
            {
                v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
                o.uvs = o.pos.xy / 2 + 0.5;
                return o;
            }
                          
            half4 frag(v2f i) : COLOR 
            {                
            //if something already exists underneath the fragment, discard the fragment.
                if(tex2D(_MainTex,i.uvs.xy).r>0)
                {
                    return tex2D(_SceneTex,i.uvs.xy);
                }
                int NumberOfIterations=19;
 
                float TX_x=_MainTex_TexelSize.x;
                float TX_y=_MainTex_TexelSize.y;
 
                float ColorIntensityInRadius=0;
 
                for(int k=0;k < NumberOfIterations;k+=1)
                {
                    for(int j=0;j < NumberOfIterations;j+=1)
                    {
                        ColorIntensityInRadius+=tex2D(
                                                      _MainTex, 
                                                      i.uvs.xy+float2
                                                                   (
                                                                        (k-NumberOfIterations/2)*TX_x,
                                                                        (j-NumberOfIterations/2)*TX_y
                                                                   )
                                                     ).r;
                    }
                }

                //this value will be pretty high, so we won't see a blur. let's lower it for now.
                ColorIntensityInRadius*=0.005;
 
                //output some intensity of teal
                half4 color= tex2D(_SceneTex,i.uvs.xy)+ColorIntensityInRadius*half4(0,1,1,1);

                //don't want our teal outline to be white in cases where there's too much red
                color.r=max(tex2D(_SceneTex,i.uvs.xy).r-ColorIntensityInRadius,0);
                return color;
            }
             
            ENDCG
 
        }
        //end pass        
    }
    //end subshader
}
//end shader

 

 

 

 

 

 

여기까지는 평균내어 블러 처리 한것인데 하단 부터는 가우시안 블러를 적용하여 블러 처리 한 부분으로 변경한 것이다

 


This is almost where we want it, but we do have one more step! There are some problems with this outline right now. You might notice it looks a little off, and that the outline doesn't contour the corners nicely.

There's also the issue of performance - at 19x19 samples per pixel for the outline, this isn't a big issue. But what if we want a 40 pixel outline? 40x40 samples would be 1600 samples per pixel!

We're going to solve both of these issues with a gaussian blur.
In a Gaussian blur, a kernel(a weight table) is made and colors are added equal to the nearby colors times the weight.


Gaussian blurs look nice and natural, and they have a performance benefit.
If we start with a 1-dimensional Gaussian kernel, we can blur everything horizontally,


and then blur everything vertically after, to get the same result.


This means our 40 width outline will only have 80 samples per pixel.

I found some code online that will generate a Gaussian kernel. It's a bit beyond my understanding, but just know that this is computationally expensive, and shouldn't be done often. I'm running it on the script's start method.

 

//http://haishibai.blogspot.com/2009/09/image-processing-c-tutorial-4-gaussian.html
public static class GaussianKernel
{
	public static float[] Calculate(double sigma, int size)
	{
		float[] ret = new float[size];
		double sum = 0;
		int half = size / 2;
		for (int i = 0; i < size; i++)
		{
			ret[i] = (float) (1 / (Math.Sqrt(2 * Math.PI) * sigma) * Math.Exp(-(i - half) * (i - half) / (2 * sigma * sigma)));
			sum += ret[i];
		}
		return ret;
	}
}

 

 

 

The sigma is an arbitrary number, indicating the strength of the kernel and the blur.

The size is to stop generating tiny numbers in an infinite kernel. Technically, Gaussian kernels would have no zeroes, so we want to cut it off once it isn't noticeable. I recommend the width be 4 times the sigma.

We calculate the kernel, and pass it to the shader. I have it set to a fixed size, but you could pass a kernel texture instead to handle varying widths.

using System;
using UnityEngine;
public class outline : MonoBehaviour
{
	public Shader DrawAsSolidColor;
	public Shader Outline;
	Material _outlineMaterial;
	Camera TempCam;
	float[] kernel;
	void Start()
	{
		_outlineMaterial = new Material(Outline);
		TempCam = new GameObject().AddComponent<Camera>();

		kernel = GaussianKernel.Calculate(5, 21);
	}

	void OnRenderImage(RenderTexture src, RenderTexture dst)
	{
		TempCam.CopyFrom(Camera.current);
		TempCam.backgroundColor = Color.black;
		TempCam.clearFlags = CameraClearFlags.Color;

		TempCam.cullingMask = 1 << LayerMask.NameToLayer("Outline");

		var rt = RenderTexture.GetTemporary(src.width, src.height, 0, RenderTextureFormat.R8);
		TempCam.targetTexture = rt;

		TempCam.RenderWithShader(DrawAsSolidColor, "");

		_outlineMaterial.SetFloatArray("kernel", kernel);
		_outlineMaterial.SetInt("_kernelWidth", kernel.Length);
		_outlineMaterial.SetTexture("_SceneTex", src);

		//No need for more than 1 sample, which also makes the mask a little bigger than it should be.
		rt.filterMode = FilterMode.Point;

		Graphics.Blit(rt, dst, _outlineMaterial);
		TempCam.targetTexture = src;
		RenderTexture.ReleaseTemporary(rt);
	}
}

 

 

We make a pass with the kernel, sampling every pixel and adding our pixel's value by the kernel times the sampled value. Call GrabPass to grab the resulting texture, and then make a second pass. The second pass does the vertical blur.

Shader "Custom/Post Outline"
{
    Properties
    {
        _MainTex("Main Texture",2D)="black"{}
        _SceneTex("Scene Texture",2D)="black"{}
        _kernel("Gauss Kernel",Vector)=(0,0,0,0)
        _kernelWidth("Gauss Kernel",Float)=1
    }
    SubShader 
    {
        Pass 
        {
            CGPROGRAM
            float kernel[21];
            float _kernelWidth;
            sampler2D _MainTex;    
            sampler2D _SceneTex;
            float2 _MainTex_TexelSize;
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            struct v2f 
            {
                float4 pos : SV_POSITION;
                float2 uvs : TEXCOORD0;
            };
            v2f vert (appdata_base v) 
            {
                v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				o.uvs = o.pos.xy / 2 + 0.5;
                return o;
            }
                          
            float4 frag(v2f i) : COLOR 
            {
                int NumberOfIterations=_kernelWidth;
				
                float TX_x=_MainTex_TexelSize.x;
                float TX_y=_MainTex_TexelSize.y;
                float ColorIntensityInRadius=0;

                //for every iteration we need to do horizontally
                for(int k=0;k<NumberOfIterations;k+=1)
                {
                        ColorIntensityInRadius+=kernel[k]*tex2D(
                                                      _MainTex, 
                                                      float2
                                                                   (
                                                                        i.uvs.x+(k-NumberOfIterations/2)*TX_x,
                                                                        i.uvs.y
                                                                   )
                                                     ).r;
                }
                return ColorIntensityInRadius;
            }
            ENDCG
 
        }
        //end pass

        //retrieve the texture rendered by the last pass, and give it to the next pass as _GrabTexture
        GrabPass{}
		
		//this pass is mostly a copy of the above one.
        Pass 
        {
            CGPROGRAM
            float kernel[21];
            float _kernelWidth;       
            sampler2D _MainTex;    
            sampler2D _SceneTex;
			
            sampler2D _GrabTexture;
            float2 _GrabTexture_TexelSize;

            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            struct v2f 
            {
                float4 pos : SV_POSITION;
                float2 uvs : TEXCOORD0;
            };
            v2f vert (appdata_base v) 
            {
                v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
                o.uvs = o.pos.xy / 2 + 0.5;
                 
                return o;
            }
            float4 frag(v2f i) : COLOR 
            {
                float TX_x=_GrabTexture_TexelSize.x;
                float TX_y=_GrabTexture_TexelSize.y;

                //if something already exists underneath the fragment, draw the scene instead.
                if(tex2D(_MainTex,i.uvs.xy).r>0)
                {
                    return tex2D(_SceneTex,i.uvs.xy);
                }

                int NumberOfIterations=_kernelWidth;
                float4 ColorIntensityInRadius=0;

                for(int k=0;k < NumberOfIterations;k+=1)
                {
                        ColorIntensityInRadius+=kernel[k]*tex2D(
                                                      _GrabTexture,
                                                      float2(i.uvs.x,
                                                             1-i.uvs.y+(k-NumberOfIterations/2)*TX_y)
                                                     );
                }

                //output the scene's color, plus our outline strength in teal.
                half4 color= tex2D(_SceneTex,i.uvs.xy)+ColorIntensityInRadius*half4(0,1,1,1);
                return color;
            }
             
            ENDCG
 
        }
        //end pass        
    }
    //end subshader
}
//end shader

 

 

The finished effect!

 

결과를 보면 좀더 밖으로 빠질 수록 자연스럽게 블러처리가 된 것을 볼 수 있다

 

Where to go from here


Try adding multiple colors.
You could also sample the added color from another texture to make a neat pattern.
Regenerate the kernel when the resolution changes to get an effect that scales up.

Help!

My outline looks blocky.
Most likely, your sigma is too high and your width too low, so the kernel is cut off earlier than it should be.
Or, your kernel isn't being generated or used correctly.

Performance problems/crashes
Are you using RenderTexture.GetTemporary() and RenderTexture.ReleaseTemporary()? Unity doesn't release normal RTs for some reason, so you need to use the temporary methods.
Check the profiler to see what's up.

Something is upside-down/too big/in the corner of the screen
Unity has inconsistencies and so does OpenGL. There is a pragma, #if UNITY_UV_STARTS_AT_TOP, that can be used.

Something isn't working.
Use the frame debugger to see what's happening to your graphics. Otherwise, check for any errors.

 

 

 

ref : https://willweissman.com/unity-outlines

http://haishibai.blogspot.com/2009/09/image-processing-c-tutorial-4-gaussian.html

https://gamedev.stackexchange.com/questions/144702/how-can-i-make-a-soft-outline-shader

https://docs.unity3d.com/kr/530/ScriptReference/Graphics.Blit.html (Graphics.Blit)

https://docs.unity3d.com/kr/2021.3/Manual/SL-BuiltinFunctions.html

 

 

 

반응형

+ Recent posts