转载自 Unity3D油画滤镜shader的实现
原图
添加后处理效果后
半径为1
半径为2
半径为3
半径为5
思路
简单普遍的油画笔刷比较大,一般每一笔之间都有覆盖到其他的之前下的笔的范围,所以覆盖范围能实现两个笔划之间柔和的过度。
做法简单说是
对相机成像图的每个像素
像素往左上,左下,右上,右下角四个方向一定范围内分若干步进,每次步进进行一次采样,每个方向所有采样到的颜色都各自进行以下处理
- 每次采样的颜色叠加,
- 每次采样的颜色平方后叠加,
在一个方向都采样完之后,将2的结果除以采样次数然后减去1的结果的除以采样次数后的平方,这样做的处理就是方差了。
在普遍的数学原理上来说,方差值越大,形成方差的这些数值之间的差值是越大的,所以方差越大,在某个方向上这些采样的颜色本身之间差值比较大,例如一次采样是白色,一次采样是黑色。
for (j = -_Radius; j <= 0; j++)
{
for (k = -_Radius; k <= 0; k++)
{
half3 c = tex2D(_MainTex, i.uv + half2(k, j) * _PSize).rgb;
m0 += c;
s0 += c * c;
}
}
求到方差之后,再用这个颜色的方差求亮度值,这个含义不是很懂。
最后一个像素的四个方向的这四个值进行比较,求最低值。
Luminance亮度公式:
luminance = 0.2125 * Red + 0.7154 * Green + 0.0721 * Blue
这是一个固定的公式,之前自己用rgb相加然后除以3的这个做法是求的灰度值,和亮度值的概念要区分开来。
模糊的主要思想是像素附近的像素的颜色求平均值,只是不同的模糊,求平均值的方式不同,例如高斯模糊和平均模糊。
方差选择差值最低的方向的颜色亮度作为自身颜色,也类似一种模糊,当所有的像素都用了这个做法之后,一个像素周围的像素 ,比较可能他们四周一定范围内最后选择的的是同个方向, 那么这两个像素的颜色最后就类似, 这样的情况多了之后,就实现了柔和的过渡。
对原因的一点点理解,可能表达有问题,目前理解一下油画的做法就好,做法的原因是更深层次的理解了,目前没必要考虑那么深。
代码
C#代码
namespace Colorful
{
using UnityEngine;
[ExecuteInEditMode]
public class OilPaint : MonoBehaviour
{
public int Radius = 3;
public Shader shader;
protected void OnRenderImage(RenderTexture source, RenderTexture destination)
{
Material material = new Material(shader);
material.SetInt("_Radius", Radius);
material.SetVector("_PSize", new Vector2(1f / (float) source.width, 1f / (float) source.height));
Graphics.Blit(source, destination, material);
}
}
}
Shader代码
Shader "LX/OilPaint"
{
Properties
{
_MainTex ("Base (RGB)", 2D) = "white" {}
_PSize ("Pixel Size (XY)", Vector) = (0,0,0,0)
}
CGINCLUDE
#include "UnityCG.cginc"
#define H3Z half3(0.0, 0.0, 0.0)
sampler2D _MainTex;
half2 _PSize;
int _Radius;
half4 frag(v2f_img i) : SV_Target
{
half3 m0 = H3Z;
half3 m1 = H3Z;
half3 m2 = H3Z;
half3 m3 = H3Z;
half3 s0 = H3Z;
half3 s1 = H3Z;
half3 s2 = H3Z;
half3 s3 = H3Z;
int k, j;
for (j = -_Radius; j <= 0; j++)
{
for (k = -_Radius; k <= 0; k++)
{
half3 c = tex2D(_MainTex, i.uv + half2(k, j) * _PSize).rgb;
m0 += c;
s0 += c * c;
}
}
for (j = -_Radius; j <= 0; j++)
{
for (k = 0; k <= _Radius; k++)
{
half3 c = tex2D(_MainTex, i.uv + half2(k, j) * _PSize).rgb;
m1 += c;
s1 += c * c;
}
}
for (j = 0; j <= _Radius; j++)
{
for (k = 0; k <= _Radius; k++)
{
half3 c = tex2D(_MainTex, i.uv + half2(k, j) * _PSize).rgb;
m2 += c;
s2 += c * c;
}
}
for (j = 0; j <= _Radius; j++)
{
for (k = -_Radius; k <= 0; k++)
{
half3 c = tex2D(_MainTex, i.uv + half2(k, j) * _PSize).rgb;
m3 += c;
s3 += c * c;
}
}
const half n = half((_Radius + 1) * (_Radius + 1));
half minSigma2 = 1;
half3 color = H3Z;
m0 /= n;
s0 = abs(s0 / n - m0 * m0);
half sigma2 = Luminance(s0);
if (sigma2 < minSigma2)
{
minSigma2 = sigma2;
color = m0;
}
m1 /= n;
s1 = abs(s1 / n - m1 * m1);
sigma2 = Luminance(s1);
if (sigma2 < minSigma2)
{
minSigma2 = sigma2;
color = m1;
}
m2 /= n;
s2 = abs(s2 / n - m2 * m2);
sigma2 = Luminance(s2);
if (sigma2 < minSigma2)
{
minSigma2 = sigma2;
color = m2;
}
m3 /= n;
s3 = abs(s3 / n - m3 * m3);
sigma2 = Luminance(s3);
if (sigma2 < minSigma2)
{
minSigma2 = sigma2;
color = m3;
}
return half4(color, 1.0);
}
ENDCG
SubShader
{
ZTest Always Cull Off ZWrite Off
Fog
{
Mode off
}
Pass
{
CGPROGRAM
#pragma vertex vert_img
#pragma fragment frag
ENDCG
}
}
FallBack off
}