【UnityShader】字符画风格屏幕后处理
程序员文章站
2024-01-17 09:41:46
...
原理
在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"
}
这里并没有灰度对比,但可以简单的绘制出边缘形状,效果为下方左图
在字符画绘制之前,先提取出图片边缘得到方右图