实现思路
大致思路就是在屏幕空间对亮度值进行采样映射,将亮度值离散化为若干个区域,之后对应成不同的颜色,之后还需要再进行一遍处理,让画面有一种呈现在特殊漫画纸上的效果,比如网点纸。
有一个要点就是亮度值如何计算,我们都知道漫反射光照模型可以得出亮度值,有一些非真实感渲染就是使用这个值来进行离散化处理的。
如Unity shader卡通化着色器的实现
但是我们这里需要注意的是屏幕空间对亮度值获取和用漫反射光照模型获取亮度值,两者出来的效果是完全不同的。
根据漫反射光照模型获取某个顶点的亮度值,是不考虑贴在这个模型上的纹理的(但是我们知道有些模型上面贴的纹理本身就隐含了亮度值)。而且也不考虑阴影之类的效果。
而在屏幕空间进行这种处理,会考虑进纹理本身的亮度值,因此最后出来的效果并不会像漫反射模型出来的那么干净,反而会有很多’杂点’,当然这两者效果并没有优劣之分,只是看你需要什么样的效果。
代码实现
具体的实现都在片元着色器中,顶点着色器只是做了一个顶点转换到裁剪空间下的基本操作。
我们首先用Luminance函数提取像素的亮度。
(注意Luminance取亮度函数不是简单的求颜色向量的模,每个颜色分量的权值是不同的,这涉及到人眼对不同颜色亮度感知的不同,建议直接用Luminance来获取一个颜色向量对应的亮度)
half lum = Luminance(tex2D(_MainTex, i.uv).rgb);
接下来判断这个亮度值落在哪一个区域,我们这个shader设置了三个亮度区域,这个判断我们没有使用if来做,虽然shader里是可以执行if语句的,但是为了效率起见,最好还是不要用。
我们用一个小技巧就可以用step函数来得到和if语句相同的效果了。
首先判断lum是不是小于_StripLimitsMin,用一次step函数即可,如果低于则underMin设为1。
step函数可以这样理解,左边的参数是不是小于右边的参数,是的话返回1,不然返回0。
half underMin = step(lum, _StripLimitsMin);
接着要判断lum是不是满足_StripLimitsMin<lum<_StripLimitsMax
这里使用两个step函数来判断。
注意参数的位置。
这里使用乘法其实是模拟了&&运算符,两个step都返回1的时候最终才是1,否则为0,大家可以自己体会这种用法。
half betweenLimit = step(_StripLimitsMin, lum) * step(lum, _StripLimitsMax);
然后我们就把亮度分为了三个区域,分别对应上不同的颜色就可以了。同样我们这里不使用if语句,而是使用两次lerp函数。
half3 color = lerp( _FillColor,lerp(_BackgroundColor, _StripInnerColor, betweenLimit), underMin);
然后我们就可以得到类似这样的效果了。
当然还需要添加一些效果,这个shader既然叫漫画纸张效果,那么就来模拟一些特殊的纸张效果好了。一些漫画可能用的是特殊的网点纸,我们也可以模拟网点纸效果。(至于什么是网点纸大家可以百度去看看效果)
我们这里选择只在中间色上添加网点纸效果。添加一个strip_color函数,并修改之前的lerp函数。
half3 color = lerp(lerp(_BackgroundColor, strip_color(i.uv), betweenLimit), _FillColor, underMin);
网点效果的实现很容易,我们只要想到网点的出现消失是一个周期的效果,因此只要用一个周期的函数,没错就是sin或者cos函数,之后调整输入参数和输出的范围就可以了。
我们这里直接简单粗暴的将uv传入到sin函数里相加,需要说明的是这个公式并不是什么经过精心推导之后得出的公式,因此不用去深究它的意义。只是因为我们需要一个周期效果,所以需要一个周期函数,然后加几个调整参数,调整起来看起来差不多就行了。
我们这里采用的是分别将uv的x,y传入sin函数并相乘,此时就可以得到根据xy变化的周期效果了。然后因为sin函数是一个连续函数,而格点的出现消失是离散化的,因此还要使用step函数进行离散化。
half3 strip_color(half2 uv)
{
fixed passStep=step(_StripThickness,sin(uv.x*_StripDensity)*sin(uv.y*_StripDensity));
return lerp(_StripInnerColor, _StripOuterColor, passStep);
}
加了格点之后的效果
也可以调整参数得到这种效果
我们也可以实现一种倾斜的条纹效果。同样是根据sin,cos函数来计算。
我们先在c#函数里传入一个单位向量。
material.SetFloat("_StripCosAngle", Mathf.Cos(StripAngle/180*Mathf.PI));
material.SetFloat("_StripSinAngle", Mathf.Sin(StripAngle/180*Mathf.PI));
更改strip_color函数如下。这个计算比上面稍微复杂一点,采取的是点积的形式,可能大家会不明白uv坐标和一个向量点积有什么意义。
这里其实可以看成从原点出发到uv坐标的一个向量,到另外一个单位向量上的投影,所以可以保证某个倾斜区域的投影值都是相同的。
后面加1之后除2的操作是为了使cos函数的返回值在[0,1]之间
half3 strip_color(half2 uv)
{
fixed passStep = step(_StripThickness,
(cos(dot(uv * _StripDensity, float2(_StripCosAngle, _StripSinAngle))) + 1) / 2);
return lerp(_StripInnerColor, _StripOuterColor, passStep);
}
最后可以得到倾斜的条纹
这个后处理配合卡通化shader还可以得到更加艺术化的效果。
Unity shader卡通化着色器的实现
完整代码
c#代码
namespace Colorful
{
using UnityEngine;
[ExecuteInEditMode]
public class ComicBook : MonoBehaviour
{
[Tooltip("Strip orientation in radians.")]
public float StripAngle = 0.6f;
[Min(0f), Tooltip("Amount of strips to draw.")]
public float StripDensity = 180f;
[Range(0f, 1f), Tooltip("Thickness of the inner strip fill.")]
public float StripThickness = 0.5f;
public Vector2 StripLimits = new Vector2(0.25f, 0.4f);
[ColorUsage(false)] public Color StripInnerColor = new Color(0.3f, 0.3f, 0.3f);
[ColorUsage(false)] public Color StripOuterColor = new Color(0.8f, 0.8f, 0.8f);
[ColorUsage(false)] public Color FillColor = new Color(0.1f, 0.1f, 0.1f);
[ColorUsage(false)] public Color BackgroundColor = Color.white;
[Range(0f, 1f), Tooltip("Blending factor.")]
public float Amount = 1f;
public Shader shader;
protected void OnRenderImage(RenderTexture source, RenderTexture destination)
{
Material material = new Material(shader);
shader.hideFlags = HideFlags.HideAndDontSave;
material.SetFloat("_StripCosAngle", Mathf.Cos(StripAngle / 180 * Mathf.PI));
material.SetFloat("_StripSinAngle", Mathf.Sin(StripAngle / 180 * Mathf.PI));
material.SetFloat("_StripLimitsMin", StripLimits.x);
material.SetFloat("_StripLimitsMax", StripLimits.y);
material.SetFloat("_StripDensity", StripDensity * 10f);
material.SetFloat("_StripThickness", StripThickness);
material.SetFloat("_Amount", Amount);
material.SetColor("_StripInnerColor", StripInnerColor);
material.SetColor("_StripOuterColor", StripOuterColor);
material.SetColor("_FillColor", FillColor);
material.SetColor("_BackgroundColor", BackgroundColor);
Graphics.Blit(source, destination, material, 0);
}
}
}
shader代码
Shader "LX/ComicBook"
{
Properties
{
_MainTex ("Base (RGB)", 2D) = "white" {}
}
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
float _StripCosAngle;
float _StripSinAngle;
float _StripLimitsMin;
float _StripLimitsMax;
half3 _StripInnerColor;
half3 _StripOuterColor;
half3 _FillColor;
half3 _BackgroundColor;
float _StripDensity;
float _StripThickness;
half _Amount;
struct v2f
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
};
half3 strip_color(half2 uv)
{
//fixed passStep=step(_StripThickness,sin(uv.x*_StripDensity)*sin(uv.y*_StripDensity));
//网点化处理,通过cos函数进行周期变化,同时点积从原点出发到uv坐标的向量和传入的一个向量,实际效果是在单位向量上进行了投影
fixed passStep = step(_StripThickness,
(cos(dot(uv * _StripDensity, float2(_StripCosAngle, _StripSinAngle))) + 1) / 2);
return lerp(_StripInnerColor, _StripOuterColor, passStep);
}
v2f vert(appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}
half4 frag(v2f_img i) : SV_Target
{
//判断亮度落在哪个区域中,该shader一共有三个区域
half lum = Luminance(tex2D(_MainTex, i.uv).rgb);
half underMin = step(lum, _StripLimitsMin);
half betweenLimit = step(_StripLimitsMin, lum) * step(lum, _StripLimitsMax);
//设置最后的颜色,如果亮度落在中间区域中,还要进行网点化处理
half3 color = lerp(lerp(_BackgroundColor, strip_color(i.uv), betweenLimit), _FillColor, underMin);
//用Amount对原像素颜色和处理后的像素颜色进行插值
half3 oldColor = tex2D(_MainTex, i.uv).rgb;
return half4(lerp(oldColor, color, _Amount), 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
}
另外代码也传到github仓库里了,大家也可以关注一下哦~
我的github