原理
在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"
}
這裏並沒有灰度對比,但可以簡單的繪製出邊緣形狀,效果爲下方左圖
在字符畫繪製之前,先提取出圖片邊緣得到方右圖