【UnityShader】字符畫風格屏幕後處理

原理

在UnityShader中實現字符畫,實際工作就是把原圖像分成矩陣塊,分析每個每個塊內的圖像,並替換爲字符。

圖像的分析方法最簡單的就是灰度值,在字符密度較大時能以很簡單的方式達到效果。更準確的方法是對塊內像素與準備好的字符取樣圖像素對比,得出最相近的字符,由於這個方法效率較低,更適合生成靜態圖片。

文中實現了一個根據灰度判斷的方法,和一個採取了及其簡單的形狀判斷與直接映射查找字符的方法(上面黃色背景圖片的邊緣)。

替換字符的方法是根據原圖像小塊所採用的字符,對一張準備好的字符圖採樣。

C#後處理腳本

首先是後處理腳本基類

using UnityEngine;
using System.Collections;

[ExecuteInEditMode]
[RequireComponent (typeof(Camera))]
public class PostEffectsBase : MonoBehaviour {

	protected void CheckResources() {
		bool isSupported = CheckSupport();
		
		if (isSupported == false) {
			NotSupported();
		}
	}

	protected bool CheckSupport() {
		if (SystemInfo.supportsImageEffects == false) 
			return false;

		return true;
	}


	protected void NotSupported() {
		enabled = false;
	}
	
	protected void Start() {
		CheckResources();
	}

	protected Material CheckShaderAndCreateMaterial(Shader shader, Material material) {
		if (shader == null) {
			return null;
		}
		
		if (shader.isSupported && material && material.shader == shader)
			return material;
		
		if (!shader.isSupported) {
			return null;
		}
		else {
                    material = new Material(shader)
                    {
                        hideFlags = HideFlags.DontSave
                    };
                    if (material)
			return material;
		    else 
			return null;
		}
	}
}

接着是字符畫需要的後處理腳本,這裏可以設置字符像素尺寸、字符顏色等信息,這裏的圖像已經通過字符像素尺寸進行了降採樣處理,輸入到shader中的是一張馬賽克圖。

using UnityEngine;
using System.Collections;


[ExecuteInEditMode]
public class ASCIIart : PostEffectsBase {


    public Shader ASCIIartShader;
    private Material ASCIIartMaterial = null;


    public Material material {  
	get {
		ASCIIartMaterial = CheckShaderAndCreateMaterial(ASCIIartShader, ASCIIartMaterial);
		return ASCIIartMaterial;
	    }  
    }

    // 字符正方形邊長
    [Range(1,100)]
    public int texelPerChar;
    //伽馬校正
    public float gamaMutipler = 1;
    //背景色
    public Color bgColor;
    //字符色
    public Color charColor;

    protected new void Start()
    {
        base.Start();
        material.SetColor("_BGColor", bgColor);
        material.SetColor("_CharColor", charColor);
    }
    void OnRenderImage (RenderTexture src, RenderTexture dest) {
	if (material != null) {
            material.SetFloat("_TexelPerChar", texelPerChar);
            material.SetFloat("_GamaMutipler", gamaMutipler);


            int rtW = src.width/ texelPerChar;
	    int rtH = src.height/ texelPerChar;

            RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);
            buffer0.filterMode = FilterMode.Point;

            Graphics.Blit(src, buffer0);
            Graphics.Blit(buffer0, src);
            RenderTexture.ReleaseTemporary(buffer0);

            Graphics.Blit(src, dest, material, 0);
        }
        else {
	    Graphics.Blit(src, dest);
	}
    }
}

Shader實現

字符取樣方式和取樣圖設計有關,本文並沒有設置相關變量,有需求的還要額外定義變量,文中取樣圖片如下:


下面是隻對灰度進行處理的Shader:

Shader "Post/ASCII art"
{
    Properties
    {
        _MainTex ("MainTex", 2D) = "white" { }
        _CharTex ("CharTex", 2D) = "white" { }
        _BGColor ("背景色", Color) = (0.2,0.3,0.5,1)
        _CharColor ("字體色", Color) = (0,0,0,1)
    }

    SubShader 
    {
        CGINCLUDE
        #include "UnityCG.cginc"

        sampler2D _MainTex;
        half4 _MainTex_TexelSize;
        sampler2D _CharTex;
        float _TexelPerChar;
        float4 _BGColor;
        float4 _CharColor;
        float _GamaMutipler;

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

        v2f ASCIIvertex(appdata_img v)
        {
            v2f o;
            o.pos = UnityObjectToClipPos(v.vertex);
            o.uv = v.texcoord;
            return o;
        }

        fixed4 ASCIIfrag(v2f i) : SV_Target
        {
            //每個字符佔用的UV值
			float2 uvPerChar = _TexelPerChar * _MainTex_TexelSize.xy;
            //所在字符的起點UV
            float2 startUV = floor(i.uv / uvPerChar) * uvPerChar + _MainTex_TexelSize.xy;
            //所在字符的座標比例(0-1)
            float2 oppositeUV = (i.uv - startUV)/uvPerChar;
            fixed4 mainColor = tex2D(_MainTex, startUV);
            //如果項目是非線性空間,需要1/2.2的Gama校正
	    //mainColor = pow(mainColor,_GamaMutipler);
            //計算灰度值
            fixed luminosity = dot(mainColor.rgb,fixed3(0.299,0.587,0.114));
            //計算灰度階數
            int luminosityStep = floor(luminosity * 4* 4) - 1;
            //計算灰度圖的座標原點
            float2 charStartUV = float2(fmod(luminosityStep,4),floor(luminosityStep / 4))/4;
            float2 charUV = charStartUV + oppositeUV/_CharCount;
            float4 color = tex2D(_CharTex, charUV);
            color = lerp(_BGColor,_CharColor, 1 - color.r);
            return color;
        }
        ENDCG

        ZTest Always
        Cull Off
        ZWrite Off

        Pass
        {
            Name "ASCII art"

            CGPROGRAM
            #pragma vertex ASCIIvertex
            #pragma fragment ASCIIfrag
            ENDCG
        }
    }
    FallBack "Diffuse"
}

效果:


另外一種聊天中常見的字符畫,如下圖所示,比起明暗這種字符畫更注重形體


爲模擬這種字符畫,建一張簡單的貼圖,由於工作量問題,這裏不考慮平均灰度的影響,且僅採用2X2的採樣區,共需要字符數量是2的2X2次冪(16個),如果3X3就需要512個字符,下面是用到的形狀圖:


下面的shader需要在後處理腳本中增加一個_LuminosityThreshold變量,用來控制灰度閾值

Shader "Post/ASCII art Gird"
{
    Properties
    {
        _MainTex ("MainTex", 2D) = "white" { }
        _CharTex ("CharTex", 2D) = "white" { }
        _BGColor ("背景色", Color) = (0.2,0.3,0.5,1)
        _CharColor ("字體色", Color) = (0,0,0,1)
        _LuminosityThreshold ("LuminosityThreshold", Float) = 0.5
    }

    SubShader
    {
        CGINCLUDE
        #include "UnityCG.cginc"

        sampler2D _MainTex;
        half4 _MainTex_TexelSize;
        sampler2D _CharTex;
        half4 _CharTex_TexelSize;
        float _TexelPerChar;
        float _LuminosityThreshold;
        float4 _BGColor;
        float4 _CharColor;
        float _GamaMutipler;

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

        v2f ASCIIvertex(appdata_img v)
        {
            v2f o;
            o.pos = UnityObjectToClipPos(v.vertex);
            o.uv = v.texcoord;
            return o;
        }

        fixed4 ASCIIfrag(v2f i) : SV_Target
        {
            //每1個字符的像素
	    float2 uvPerChar = _TexelPerChar * _MainTex_TexelSize.xy;
            //每2個字符的像素
	    float2 uvPer2Char = 2 * uvPerChar;
            //原圖網格起點,4個網格爲一組,額外偏移1像素
            float2 startUV = floor(i.uv / uvPer2Char) * uvPer2Char + _MainTex_TexelSize.xy;
            //找到原圖上相對起點的座標的比例,因爲2x2爲一組,要除2被字符像素長寬值,以映射到0-1的值
            float2 oppositeUV = (i.uv - startUV)/uvPer2Char;
            //計算4個灰度值
            fixed4 mainColor0 = tex2D(_MainTex, startUV);
            fixed4 mainColor1 = tex2D(_MainTex, startUV + float2(1,0) * uvPerChar);
            fixed4 mainColor2 = tex2D(_MainTex, startUV+ float2(0,1) * uvPerChar);
            fixed4 mainColor3 = tex2D(_MainTex, startUV+ float2(1,1) * uvPerChar);
            fixed luminosity0 = dot(mainColor0.rgb,fixed3(0.299,0.587,0.114));
            fixed luminosity1 = dot(mainColor1.rgb,fixed3(0.299,0.587,0.114));
            fixed luminosity2 = dot(mainColor2.rgb,fixed3(0.299,0.587,0.114));
            fixed luminosity3 = dot(mainColor3.rgb,fixed3(0.299,0.587,0.114));

            // fixed luminosity = (luminosity0 + luminosity1 + luminosity2 + luminosity3)/4;
            //由形狀圖排版和4個灰度階數求灰度圖上的座標
            int x = 0,y = 0;
            if (luminosity0 > _LuminosityThreshold) y+=2;
            if (luminosity1 > _LuminosityThreshold) y+=1;
            if (luminosity2 > _LuminosityThreshold) x+=2;
            if (luminosity3 > _LuminosityThreshold) x+=1;

            //計算灰度圖的座標原點
            float2 charStartUV = float2(x,y)/4;
            float2 charUV = charStartUV + oppositeUV/4;
            // _CharColor = lerp(_CharColor,_BGColor, luminosity);
            float4 color = tex2D(_CharTex, charUV);
            color = lerp(_CharColor,_BGColor, color.r);
            return color;
        }
        ENDCG

        ZTest Always
        Cull Off
        ZWrite Off

        Pass
        {
            Name "ASCII art"

            CGPROGRAM
            #pragma vertex ASCIIvertex
            #pragma fragment ASCIIfrag
            ENDCG
        }
    }
    FallBack "Diffuse"
}

這裏並沒有灰度對比,但可以簡單的繪製出邊緣形狀,效果爲下方左圖

在字符畫繪製之前,先提取出圖片邊緣得到方右圖




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章