概述
之前写过一篇去色shader,大致就是使用一种类似区间划分的方法来将原来图片所使用的颜色离散化,来达到一些特殊的视觉效果。
使用之前那个shader,将颜色数量减少到两种的时候,效果如下,已经完全看不出原图是什么东西了。
我们今天使用一种特殊的dither抖动 shader,来实现只用两种颜色但依然能保证一定的图片细节的效果。
先看看效果,下图只使用了纯黑或者纯白两种效果,虽然依旧很模糊,但效果比上面那张图要好的太多了。
可能有人不相信这张图只使用了两种颜色的(我一开始也没有相信),我们这里截取局部来放大,就可以清楚的看到,这张图只有纯黑或纯白两种颜色。另外有学过计算机图形学的应该都看出这是一种什么算法了。
简单概述一下,在计算机图形学中,有一种只用黑白两种颜色来表示更多种颜色的方法。如下图所示,使用一个2x2像素的方格,通过对里面四个点填充黑点的数量来做变化,就可以得到五种不同的灰度,将每个像素都用这种2x2像素的方格替换,就可以得到5种颜色变化,虽然实际上还是只有黑白两色,但是只要不放大看,就可以得到有5种颜色变化的错觉。
用2x2方格可以做出5种变化,那么想要更多的灰度级别,就需要提升方格的大小,4x4的方格可以得到17种灰度,8x8的方格可以得到惊人的65种灰度。
但是使用一个2x2的方格来代表原来的一个像素的话,整张图片就需要更大的分辨率。假设原图为1024x1024长宽的图片,则一共有1048576个像素。如果使用2x2的方格来代表原来的一个像素,虽然可以得到5种灰度,但是分辨率需要提升为2048x2048。使用更大的方格同理,虽然可以得到更多的颜色级别,但是会需要更大的分辨率才能显示下原图。
有一种不用提升分辨率就能在原图上使用dither算法的方法,也就是我们今天要讲的方法。假设使用上面所说的8x8的块,此方法不用把每个像素变大64倍,而是直接在原图上进行这种划分,将原图划分为若干8x8的块,对原图的每个像素,和相应块中的阈值做比较,超过后则打上白点否则打上黑点。
先获得像素对应的屏幕坐标
float2 screenPos = IN.screenPos.xy / IN.screenPos.w * _ScreenParams.xy;
获得像素对应的亮度
int brightness = Luminance(c) * 256;
8x8的方格支持65种亮度,因此先要把256色降低到64色(即0~64,一共65种颜色)
brightness = brightness >> 2;
对坐标求余,然后传入比较函数,来得到该点是映射到一个黑点还是白点。
gray = Dither8x8Bayer(screenPos.x % 8, screenPos.y % 8, brightness);
关键的比较方法如下,传入屏幕坐标和对应像素亮度,通过对1~64之间的阈值做比较,就可以把这个像素转换成一个白点或黑点。
float Dither8x8Bayer(int x, int y, float brightness)
{
const float dither[64] = {
1, 49, 13, 61, 4, 52, 16, 64,
33, 17, 45, 29, 36, 20, 48, 32,
9, 57, 5, 53, 12, 60, 8, 56,
41, 25, 37, 21, 44, 28, 40, 24,
3, 51, 15, 63, 2, 50, 14, 62,
35, 19, 47, 31, 34, 18, 46, 30,
11, 59, 7, 55, 10, 58, 6, 54,
43, 27, 39, 23, 42, 26, 38, 22
};
int r = y * 8 + x;
return step(dither[r], brightness);
}
对单个像素来说,即使他亮度很高,但是因为对应位置的阈值更高,它也有可能会被转化为一个黑点。这样想的话可能会觉得这种方法不合理。
但是对整个8x8的块来说,如果整个块内像素的平均亮度高,那么最后整个块最后会出现的白点也更多,所以最后还是一样的效果,对整个块来说,原来越亮则转换后白点越多,原来越黑,黑点越多,但是这种方法就不需要提升分辨率。
上面讨论的都是灰度图的做法,彩色图也是一样的,只要对每个分量单独考虑就可以了。
彩色图的效果如下,每种分量只使用了两种颜色,那么最后混合后至多也就是使用2^3=8种颜色。
完整代码
上面只讨论了8x8方格下的实现,但是任意NxN的方格都是可以的,下面代码就用宏定义分别实现了2x2,4x4,8x8三种方格大小。
Shader "LX/Dither"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Glossiness ("Smoothness", Range(0,1)) = 0.5
_Metallic ("Metallic", Range(0,1)) = 0.0
_GrayScale("GrayScale", Range(0,1)) = 1
}
SubShader
{
Tags
{
"RenderType"="Opaque"
}
LOD 200
CGPROGRAM
#pragma surface surf Standard fullforwardshadows vertex:vertexDataFunc
#pragma target 3.0
sampler2D _MainTex;
struct Input
{
float2 uv_MainTex;
fixed4 screenPos;
};
float Dither2x2Bayer(int x, int y, float brightness)
{
const float dither[4] = {
0, 2,
3, 1
};
int r = y * 2 + x;
return step(dither[r], brightness);
}
float Dither4x4Bayer(int x, int y, float brightness)
{
const float dither[16] = {
0, 8, 2, 10,
12, 4, 14, 6,
3, 11, 1, 9,
15, 7, 13, 5
};
int r = y * 4 + x;
return step(dither[r], brightness);
}
float Dither8x8Bayer(int x, int y, float brightness)
{
const float dither[64] = {
1, 49, 13, 61, 4, 52, 16, 64,
33, 17, 45, 29, 36, 20, 48, 32,
9, 57, 5, 53, 12, 60, 8, 56,
41, 25, 37, 21, 44, 28, 40, 24,
3, 51, 15, 63, 2, 50, 14, 62,
35, 19, 47, 31, 34, 18, 46, 30,
11, 59, 7, 55, 10, 58, 6, 54,
43, 27, 39, 23, 42, 26, 38, 22
};
int r = y * 8 + x;
return step(dither[r], brightness);
}
half _Glossiness;
half _Metallic;
fixed4 _Color;
float _GrayScale;
void vertexDataFunc(inout appdata_full v, out Input o)
{
UNITY_INITIALIZE_OUTPUT(Input, o);
float4 screenPos = ComputeScreenPos(UnityObjectToClipPos(v.vertex));
o.screenPos = screenPos;
}
#define _8x8
void surf(Input IN, inout SurfaceOutputStandard o)
{
float2 screenPos = IN.screenPos.xy / IN.screenPos.w * _ScreenParams.xy;
fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
int brightness = Luminance(c) * 256;
int colorR = c.r * 256;
int colorG = c.g * 256;
int colorB = c.b * 256;
fixed gray;
fixed r;
fixed g;
fixed b;
#ifdef _8x8
brightness = brightness >> 2;
colorR = colorR >> 2;
colorG = colorG >> 2;
colorB = colorB >> 2;
gray = Dither8x8Bayer(screenPos.x % 8, screenPos.y % 8, brightness);
r = Dither8x8Bayer(screenPos.x % 8, screenPos.y % 8, colorR);
g = Dither8x8Bayer(screenPos.x % 8, screenPos.y % 8, colorG);
b = Dither8x8Bayer(screenPos.x % 8, screenPos.y % 8, colorB);
#elif defined (_4x4)
brightness = brightness >> 4;
colorR=colorR>>4;
colorG=colorG>>4;
colorB=colorB>>4;
gray = Dither8x8Bayer(screenPos.x % 4, screenPos.y % 4, brightness);
r = Dither8x8Bayer(screenPos.x % 4, screenPos.y % 4, colorR);
g = Dither8x8Bayer(screenPos.x % 4, screenPos.y % 4, colorG);
b = Dither8x8Bayer(screenPos.x % 4, screenPos.y % 4, colorB);
#elif defined (_2x2)
brightness = brightness >> 6;
colorR=colorR>>6;
colorG=colorG>>6;
colorB=colorB>>6;
gray = Dither8x8Bayer(screenPos.x % 2, screenPos.y % 2, brightness);
r = Dither8x8Bayer(screenPos.x % 2, screenPos.y % 2, colorR);
g = Dither8x8Bayer(screenPos.x % 2, screenPos.y % 2, colorG);
b = Dither8x8Bayer(screenPos.x % 2, screenPos.y % 2, colorB);
#endif
fixed3 grayColor = fixed3(gray, gray, gray);
fixed3 color = fixed3(r, g, b);
o.Albedo = lerp(color, grayColor, _GrayScale);
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
另外代码也传到github仓库里了,大家也可以关注一下哦~
我的github