什么是Dither
基本概念
- v. 犹豫不决;对(录音)进行噪声处理;抖色
- n. 犹豫不决;紧张,颤抖
用有限的颜色来表示出多种色阶变化的技术就叫做Dither
上面那句话其实并不严谨,只是在此方便理解,Dither不是新型技术,它在视频,信号处理,音频等领域都有应用
图形中的应用
基于上述的简单且并不严谨的描述,下图是在一个2x2的像素格中,从左到右依次排列出一个黑白色阶的效果
对应黑白色阶
没看出来?我们将像素格缩小平铺
这下就对味了!
现在我们对Dither在图形上的应用已经有了初步的了解,并且我们可以看得到,2x2的像素格只有5种颜色过渡,那么4x4呢?
- 2x2像素格有5种颜色
- 4x4像素格有17种颜色
- 8x8像素格有65种颜色
有序抖动与无序抖动
这里我们不涉及更深的底层逻辑,直观一些,直接看图
这张图展示了不同算法呈现出的结果,其中下面
这张,就是有序Dither的效果,也是我们下面要实现的效果(有点像素画的感觉哈)
Dither的实现
这里我们的主要目标是先绘制一个2x2像素格的矩阵,然后再想办法将这些像素格填充平铺到屏幕中去
有了目标,下面我们开干!
像素格矩阵
首先我们想要绘制下面这样一个2x2的像素格,对应的矩阵应该是什么样的?
显而易见
平铺到屏幕中
方法其实很简单,只需要将上面那个2x2矩阵作为一个贴图一样目标,再用0,1,0,1,0,1不断重复的坐标值采样它就行了
说起来有点抽象,首先我们先看采样它的值从哪来
i.positionCS.xy
没错,就是它,我们知道裁剪空间下的坐标值,从顶点着色器输出到片段着色器后,它的值就是屏幕上每一个像素的值
且值非常大,不是简单的0-1,而且我们的矩阵只能取到0和1,即0行0列、0行1列.....所以必须将这些值映射为0和1,只需要除以2取余即可
直接输出 i.position.y - 400 后
所以我们就是用它的值,来采样2x2像素矩阵(与其说是采样,不如说是2x2像素矩阵的下标)
下面我们用代码实现一下
[4]先声明一个4x4的矩阵,我们知道这种排列方式是一个中灰色
[10]声明uint2二维整数类型,再将屏幕坐标值除以2取余
[12]通过屏幕坐标的x轴与y轴,查找矩阵的值
half4 frag(Varyings i) : SV_TARGET
{
float2x2 M2x2 = float2x2
(
0,1,
1,0
);
uint2 uv = (uint2)i.positionCS.xy%2;
return M2x2[uv.x][uv.y];
}
最终效果
放大看
效果出现了,我们还可以改变矩阵的排列方式,呈现出不同的颜色状态
half4 frag(Varyings i) : SV_TARGET
{
float2x2 M2x2 = float2x2
(
0,1,
0,0
);
uint2 uv = (uint2)i.positionCS.xy%2;
return M2x2[uv.x][uv.y];
}
黑色会更多
优化代码实现
上面我们已经知道了核心代码如何实现,因为还需要用到不同的矩阵数量以及排列方式(颜色),为了方便,我们直接创建自己的方法便于使用
// 2x2矩阵
half Dither2x2_Matrix(uint2 uv )
{
uv %= 2;
float2x2 M2x2 = float2x2
(
0,1,
0,0
);
return M2x2[uv.x][uv.y];
}
half4 frag(Varyings i) : SV_TARGET
{
uint2 uv_gray = (uint2)i.positionCS.xy;
half4 c = Dither2x2_Matrix(uv_gray);
return c;
return 1;
}
趁热再写一个4x4的
// 4x4矩阵
half Dither4x4_Matrix(uint2 uv )
{
uv %= 4;
float4x4 M4x4 = float4x4
(
1,0,0,0,
0,1,0,0,
0,0,1,0,
0,0,0,1
);
return M4x4[uv.x][uv.y];
}
half4 frag(Varyings i) : SV_TARGET
{
uint2 uv = (uint2)i.positionCS.xy;
half4 c1 = Dither4x4_Matrix(uv);
return c1;
}
4x4矩阵采样的效果
这时候我们发现,我们写的4x4矩阵明明是从左往右的斜线呀,为什么显示出来后,是相反的?
主要是因为y轴,屏幕的坐标是以左下角为(0,0)点(OpenGL),但逐行采样矩阵时,却是从左上角开始的,所以就导致了这种情况发生
矩阵转化为数组
像素格元素不但可以用矩阵表示,也可以声明数组,不同的是,矩阵我们可以指定行与列,数组如何指定?
我们只需要用另一种算法作为数组的下标
例如3x3的矩阵
我们可以指定其几行几列,例如2行1列(从0开始),是7
数组
如果用上面那个算法呢?
注意,这里的值仅仅是它们的下标,或者索引值,数组或矩阵内部的值当然可以所以更改
但它们所指定的某一个数是一致的
它们的写法有所不同
// 4x4数组
half Dither4x4_Array(uint2 uv )
{
uv %= 4;
float A4x4[16]=
{
1,0,0,0,
0,1,0,0,
0,0,1,0,
0,0,0,1
};
return A4x4[uv.x*4+uv.y];
}
half4 frag(Varyings i) : SV_TARGET
{
uint2 uv = (uint2)i.positionCS.xy;
half4 c1 = Dither4x4_Array(uv);
return c1;
}
Dither有序抖动
什么是有序抖动?
其实就是利用这样的矩阵,但看起来很乱不是吗?
它有一个规律,就是每两个数字之间都会相差较大,仔细看,还真是这样
下面我们就模仿这个矩阵的写法,创建一个出来
// 8x8数组
half Dither8x8_Array(uint2 uv )
{
uv %= 8;
float A4x4[64]=
{
0,32,8,40,2,34,10,42,
48,16,56,24,50,18,58,26,
12,44,4,36,14,46,6,38,
60,28,52,20,62,30,54,22,
3,35,11,43,1,33,9,41,
51,19,59,27,49,17,57,25,
15,47,7,39,13,45,5,37,
63,31,55,23,61,29,53,21
};
return A4x4[uv.x*8+uv.y];
}
等等
数组里这些数值,除了0以外,其他的超过1的不都是白色?
别担心,我们可以用除法将他们映射到0-1区间,除以64看看
// 8x8数组
half Dither8x8_Array(uint2 uv )
{
uv %= 8;
float A4x4[64]=
{
0,32,8,40,2,34,10,42,
48,16,56,24,50,18,58,26,
12,44,4,36,14,46,6,38,
60,28,52,20,62,30,54,22,
3,35,11,43,1,33,9,41,
51,19,59,27,49,17,57,25,
15,47,7,39,13,45,5,37,
63,31,55,23,61,29,53,21
};
return A4x4[uv.x*8+uv.y]/64;
}
half4 frag(Varyings i) : SV_TARGET
{
uint2 uv = (uint2)i.positionCS.xy;
half4 c = Dither2x2_Matrix(uv);
return c2;
}
这样我们就获得颜色不均的有序Dither效果了
有序抖动Dither的应用
有啥用??????
先给出答案:我们可以配合Clip方法用它来做半透明效果,也就是通过AlphaTest达到半透明效果
相信你已经知道改怎么写了
// 8x8数组
half Dither8x8_Array(uint2 uv )
{
uv %= 8;
float A4x4[64]=
{
0,32,8,40,2,34,10,42,
48,16,56,24,50,18,58,26,
12,44,4,36,14,46,6,38,
60,28,52,20,62,30,54,22,
3,35,11,43,1,33,9,41,
51,19,59,27,49,17,57,25,
15,47,7,39,13,45,5,37,
63,31,55,23,61,29,53,21
};
return A4x4[uv.x*8+uv.y]/64;
}
half4 frag(Varyings i) : SV_TARGET
{
uint2 uv = (uint2)i.positionCS.xy;
half4 c = Dither2x2_Matrix(uv);
clip(c2 - _Clip);
return c2;
}
这样我想到了小米透明电视。。。
放大看看
嘿!像不像MineCraft的玻璃!
像素风格
有序Dither不但可以作为廉价的半透明实现,最明显的作用就是将图像变成像素风格
实现原理
将目标图像(贴图,或者光照等)也传入Dither矩阵的方法中去,并将目标图像的数值与Dither矩阵中的数值做对比(Step/SmoothStep),然后输出即可
代码实现
[18]使用Step方法将有序Dither 与贴图颜色进行对比
[27]将RGB颜色转换为灰度的方法
// 8x8数组
half Dither8x8_Array(uint2 uv , float color)
{
uv %= 8;
float A4x4[64]=
{
0,32,8,40,2,34,10,42,
48,16,56,24,50,18,58,26,
12,44,4,36,14,46,6,38,
60,28,52,20,62,30,54,22,
3,35,11,43,1,33,9,41,
51,19,59,27,49,17,57,25,
15,47,7,39,13,45,5,37,
63,31,55,23,61,29,53,21
};
half pixel = A4x4[uv.x*8+uv.y]/64;
return step(pixel,color);
}
half4 frag(Varyings i) : SV_TARGET
{
half4 mainTex = SAMPLE_TEXTURE2D(_MainTex,smp,i.uv);
half maintexGray = Luminance(mainTex.rgb);
uint2 uv = (uint2)i.positionCS.xy;
half4 c2 = Dither8x8_Array(uv,maintexGray);
return c2;
}
有点意思了
但是现在只是黑白的,毕竟受该方法的限制,我们不能一次性传入RGB进行对比
但是我们可以对每个通道比较最后再合成到一起!
half4 frag(Varyings i) : SV_TARGET
{
half4 mainTex = SAMPLE_TEXTURE2D(_MainTex,smp,i.uv);
uint2 uv = (uint2)i.positionCS.xy;
half c2 = Dither8x8_Array(uv,mainTex.r);
half c3 = Dither8x8_Array(uv,mainTex.g);
half c4 = Dither8x8_Array(uv,mainTex.b);
half4 c = half4(c2,c3,c4,1);
return c;
}
效果一般,真正使用的话,还是需要再做修改
Houdini生成Dither纹理
使用Houdini可以将不同类型的Dither矩阵纹理填入同一张贴图的通道中
这里稍复杂,但节点不多,首先是B通道中填充8x8有序DIther
注意grid节点,需要先把网格设置为8x8的大小,这样每个面都为1个单位,并且x,z的位置也设定好,使得左上角为(0,0)点
Wrangle中的代码
注意Run Over设置为面,我们要取每个面的数值,并使他们为从0到63的排列方式,作为有序Dither矩阵的索引
最后一行输出到b通道
maps_baker节点
输出后的贴图B通道