绘画艺术家们自由创作,用想象和幻想等方法构造作品,将油画作品表现为自己精神和情感世界的媒介。本期我们用shader来实现场景的油画风格效果。
梵高作品《星月夜》
首先帖一下在Unity中实现油画实现效果前后的对比图:
未开启屏幕油画效果:
开启屏幕油画效果后:
我们观察发现油画的特点:边缘清晰,中间色块均匀模糊。
我们来看一下用Unity的shader代码实现过程:
Shader "OilPaintEffect"
{
Properties
{
_MainTex("Base (RGB)", 2D) = "white" {}
_Distortion("_Distortion", Range(0.0, 1.0)) = 0.3
_ScreenResolution("_ScreenResolution", Vector) = (0., 0., 0., 0.)
_ResolutionValue("_ResolutionValue", Range(0.0, 5.0)) = 1.0
_Radius("_Radius", Range(0.0, 5.0)) = 2.0
}
SubShader
{
Pass
{
ZTest Always
CGPROGRAM
#pragma target 3.0
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
uniform sampler2D _MainTex;
uniform float _Distortion;
uniform float4 _ScreenResolution;
uniform float _ResolutionValue;
uniform int _Radius;
struct vertexInput
{
float4 vertex : POSITION;
float4 color : COLOR;
float2 texcoord : TEXCOORD0;
};
struct vertexOutput
{
half2 texcoord : TEXCOORD0;
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
};-
vertexOutput vert(vertexInput Input)
{
vertexOutput Output;
Output.vertex = UnityObjectToClipPos(Input.vertex);
Output.texcoord = Input.texcoord;
Output.color = Input.color;
return Output;
}
float4 frag(vertexOutput Input) : COLOR
{
float2 src_size = float2(_ResolutionValue / _ScreenResolution.x, _ResolutionValue / _ScreenResolution.y);
float2 uv = Input.texcoord.xy;
float n = float((_Radius + 1) * (_Radius + 1));;
float3 m0 = 0.0; float3 m1 = 0.0;
float3 s0 = 0.0; float3 s1 = 0.0;
float3 c;
for (int j = -_Radius; j <= 0; ++j)
{
for (int i = -_Radius; i <= 0; ++i)
{
c = tex2D(_MainTex, uv + float2(i, j) * src_size).rgb;
m0 += c;
s0 += c * c;
}
}
for (int j = 0; j <= _Radius; ++j)
{
for (int i = 0; i <= _Radius; ++i)
{
c = tex2D(_MainTex, uv + float2(i, j) * src_size).rgb;
m1 += c;
s1 += c * c;
}
}
float4 finalFragColor = 0.;
float min_sigma2 = 1e+2;
m0 /= n;
s0 = abs(s0 / n - m0 * m0);
float sigma2 = s0.r + s0.g + s0.b;
if (sigma2 < min_sigma2)
{
min_sigma2 = sigma2;
finalFragColor = float4(m0, 1.0);
}
m1 /= n;
s1 = abs(s1 / n - m1 * m1);
sigma2 = s1.r + s1.g + s1.b;
if (sigma2 < min_sigma2)
{
min_sigma2 = sigma2;
finalFragColor = float4(m1, 1.0);
}
return finalFragColor;
}
ENDCG
}
}
}
我们分析片段着色器主要代码部分:
通过设置一个一定数值范围参数与屏幕的分辨率做比值,确认好图像的尺寸
float2 src_size = float2(_ResolutionValue / _ScreenResolution.x, _ResolutionValue / _ScreenResolution.y);
根据动态半径数值,计算出n的值(该参数用于参与模糊算法)
float n = float((_Radius + 1) * (_Radius + 1));
根据动态半径数,迭代计算出m0和s0的值
for (int j = -_Radius; j <= 0; ++j)
{
for (int i = -_Radius; i <= 0; ++i)
{
c = tex2D(_MainTex, uv + float2(i, j) * src_size).rgb;
m0 += c;
s0 += c * c;
}
}
同理迭代计算m1和s1的值
for (int j = 0; j <= _Radius; ++j)
{
for (int i = 0; i <= _Radius; ++i)
{
c = tex2D(_MainTex, uv + float2(i, j) * src_size).rgb;
m1 += c;
s1 += c * c;
}
}
定义相关参数,用于计算最终的颜色值
float4 finalFragColor = 0.;
float min_sigma2 = 1e+2;
根据m0和s0,计算出finalFragColor的值
m0 /= n;
s0 = abs(s0 / n - m0 * m0);
float sigma2 = s0.r + s0.g + s0.b;
if (sigma2 < min_sigma2)
{
min_sigma2 = sigma2;
finalFragColor = float4(m0, 1.0);
}
我们通过细微调整半径值大小,可以实现油画程度的调整,如下图:
设置较小半径
设置较大半径
在材质上赋予着色器之后将材质球贴敷到游戏对象上就能够看到油画效果,可以根据需求取调节材质球上的着色器各个参数,以应对不同的需求