Unity_Shader
简易的卡通渲染
概:先说明为什么说是简易的卡通渲染,顾名思义,写的比较简单,跑去显示效果不是很好以外,甚至还会有些小小的bug没处理,不过姑且可以在一些不那么讲究的项目中用用,或者说给刚接触的小白入个门。(我摊牌了,我刚入门,我还是个小白)
最终效果:先把效果放出来,觉得还行的继续康,觉得太辣鸡的就可以跑路了(觉得看讲述费时间的也可以直接跳转文末,有完整Shader代码,基本上每句话我都打了注释,Shader基础不错的崽应该可以直接看懂)
总体简述
- 首先是说一下这个卡通渲染实现了什么:实现了一个描边效果,一个光照效果分阶。多的没有了,所以可以看出,细节上有很多比较搓的地方
- 关于这个描边效果还存在个问题,就是如果模型棱角比较明显,会出现描边断开的情况,具体参照下图(不过在一些过度平滑或者说比较精细的模型中,一般不会有影响),这个问题一直找不到解决方法(其他描边法除外),有大佬说可以通过切线和法线计算一个新的法线出来,但没有说具体怎么做,还是不会,如果有大佬不吝赐教那就太感谢了
- 然后关于这个光照分阶,我没有用渐变纹理,只是很简单粗暴的手动设置了俩个颜色,作为光照颜色和阴影颜色。这也就造成了光照的过渡非常僵硬。
描边
- 首先说说这个黑色描边的实现,之前采用的是让顶点坐标直接在模型空间下扩张开来,然后直接渲染黑色,作为描边,后来法线这种方法致命的问题,于是改了现在的方法。
- 让顶点在模型空间下直接冲着法线的方向进行位移,就可以把模型的顶点整体向外扩出一圈,为其加上一个参数来控制其外扩的强度(不如直接叫做描边宽度?),但是还有个问题这个描边宽度是不能固定的,如果宽度固定,那么相机拉近的情况下,就可以看到很粗很粗的一条线,就没有“画”出来的那种感觉了,所以我计算了相机和当前顶点的距离,作为一个控制描边宽度的参数,去根据相机距离来动态控制这个描边宽度。
- 整体外扩后将其全部渲染成黑色,然后再给该pass块加上一句Cull Front指令剔除前面的渲染,只渲染后面,就可以保证不会挡到模型的正常显示,就是黑色描边了。
模型渲染
- 然后再说说这个模型的渲染问题,卡通渲染又叫非真实渲染,通常色阶比较少,过度没那么平缓,具体随便找个二次元的图一般就能康的到,所以我们要做的就是给模型渲染出来的的色阶段给分成比较明显几部分。
- 这里我用的方法是直接对漫反射动手,使用入射光线和法线的夹角cos值作为参数,去与设定好的分阶数进行比较,然后根据结果设定当前片元的漫反射光应该是哪个颜色(预先设定好的俩个)。这也就导致了最终渲染出来效果很一般,虽然由内味儿了,但不够专业,不够强力(要真正学一手卡通渲染,还是参照大佬的详细讲解)
代码(我写了很多注释,帮助详细理解)
Shader "Custom/Myshader/CartoonRandering3"
{
Properties
{
_MainTex("Main Texture",2D) = "while"{} //定义一个2D的纹理输入
_Color1("MyColor1",Color) = (1.0,1.0,1.0,1.0) //色阶1
_Color2("MyColor2",Color) = (1.0,1.0,1.0,1.0) //色阶2
_Line_thickness("Line_thickness",Range(0,2)) = 1.1 //线条粗细
_Density("Density",Range(0,1)) = 0.5 //控制色阶比例
}
SubShader
{
pass
{
//黑色描边pass
//剔除前方,仅渲染后方
Cull Front
Tags{"LightMode" = "ForWardBase"}
CGPROGRAM
#pragma vertex _Vert
#pragma fragment Pixel
//定义顶点函数输入
struct vertexInput
{
float4 Pos:POSITION;
float3 Normal:NORMAL;
};
//定义顶点函数输出
struct vertexOutput
{
float4 Pos:SV_POSITION;
};
//声明线宽参数
float _Line_thickness;
//开写顶点函数
vertexOutput _Vert(vertexInput v)
{
//声明返回值
vertexOutput r;
//计算相机与顶点的距离,准备作为顶点移位的参数
float4 dis = distance(_WorldSpaceCameraPos,mul(unity_ObjectToWorld, v.Pos));
//法线标准化
v.Normal = normalize(v.Normal);
//对顶点坐标按照法线方向移位
//移位距离是由外部参数_Line_thickness和相机与顶点距离dis决定的
v.Pos.xyz += v.Normal*dis*_Line_thickness;
//将移位后的顶点坐标转换到屏幕空间下
r.Pos = UnityObjectToClipPos(v.Pos);
return r;
}
fixed4 Pixel(vertexOutput v) :SV_TARGET
{
//简单粗暴的返回黑色就行
return fixed4(0,0,0,0);
}
ENDCG
}
pass
{
//模型渲染pass
Cull Back
Tags{"LightMode" = "ForWardBase"}
CGPROGRAM
#pragma vertex _Vert
#pragma fragment Pixel
#include "Lighting.cginc"
#include "UnityCG.cginc"
struct vertexInput
{
float4 Pos:POSITION;
float3 Normal:NORMAL;
float4 texcoord : TEXCOORD0;
};
struct vertexOutput
{
float4 Pos:SV_POSITION;
float3 WorldNormal : TEXCOORD0;
float2 uv : TEXCOORD1;
};
//声明我们需要的变量
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _Color1;
float4 _Color2;
float _Density;
vertexOutput _Vert(vertexInput v)
{
vertexOutput r;
//将法线转换到世界坐标下,存入返回值备用
r.WorldNormal = UnityObjectToWorldNormal(v.Normal);
//将顶点的坐标信息转换到剪裁空间
r.Pos = UnityObjectToClipPos(v.Pos);
//对应纹理取得uv坐标存入返回值中备用,不过在对模型的卡通渲染中一般用不到纹理的缩放偏移
//所以这句代码也可以是:r.uv = v.texcoord.xy;
r.uv = TRANSFORM_TEX(v.texcoord,_MainTex);
return r;
}
fixed4 Pixel(vertexOutput v) :SV_TARGET
{
//对MainTex进行采样,直接作为底色
float3 albedo = tex2D(_MainTex, v.uv).xyz;
//将世界坐标下的法线方向标准化
fixed3 WorldNormal = normalize(v.WorldNormal);
//将世界空间的光照方向标准化
fixed3 WorldLightDir = normalize(_WorldSpaceLightPos0.xyz);
//得到环境光
fixed4 ambient = fixed4(UNITY_LIGHTMODEL_AMBIENT.xyz,0);
//定义漫反射光
fixed4 diffuse;
//计算入射光和法线夹角的cos值,并将其作为确定光照颜色的参数与_Density参数作为比较
//最终确定这个点的漫反射光照颜色是哪个
if(saturate(dot(WorldNormal, WorldLightDir))>_Density)
{
diffuse = _Color1;
}
else
{
diffuse = _Color2;
}
//漫反射光和环境光叠加,得到总光照信息
fixed4 Dif = saturate(diffuse+ambient);
//将总的光照信息和底色进行叠底,得到最终显色返回
fixed4 r = fixed4(albedo, 1.0)*Dif;
return r;
}
ENDCG
}
}
FallBack "Diffuse"
}