【Unity Shader实战】卡通风格的Shader(一)

时间:2023-01-18 18:45:20

写在前面

本系列其他文章:

呜,其实很早就看到了这类Shader,实现方法很多,效果也有些许不一样。从这篇开始,陆续学习一下接触到的卡通类型Shader的编写。

本篇的最后效果如下(只有怪物和苹果部分):

【Unity Shader实战】卡通风格的Shader(一)

本篇文章里指的卡通效果具有如下特点:

  • 简化了模型中使用的颜色
  • 简化光照,使模型具有明确的明暗区域
  • 在模型边缘部分绘制轮廓(也就是描边)

我们再来回顾一下Unity Surface Shader的pipeline。(来源:Unity Gems

【Unity Shader实战】卡通风格的Shader(一)

由上图可以看出,我们一共有4个可修改渲染结果的机会(绿色方框中的代码)。在理解这个的基础上,我们来真正学习如何实现上述效果。

简化颜色

在第一步中,我们只实现一个最常见的Bump Diffuse Shader,在这个基础上添加一点其他的技巧来实现简化颜色的目的。Unity的内置Shader也包含了Bump Diffuse Shader,它的作用很简单,就是用一张贴图(也叫法线贴图)记录了模型上的凹凸情况,以此来作为顶点的法线信息,渲染出来的模型也就有了凹凸不平的感觉(详情可见Unity官网)。

基本的Bump Diffuse Shader代码如下:

Shader "Example/Diffuse Bump" {
Properties {
_MainTex ("Texture", 2D) = "white" {}
_BumpMap ("Bumpmap", 2D) = "bump" {}
}
SubShader {
Tags { "RenderType" = "Opaque" }
CGPROGRAM
#pragma surface surf Lambert
struct Input {
float2 uv_MainTex;
float2 uv_BumpMap;
};
sampler2D _MainTex;
sampler2D _BumpMap;
void surf (Input IN, inout SurfaceOutput o) {
o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap));
}
ENDCG
}
Fallback "Diffuse"
}

效果如下:

【Unity Shader实战】卡通风格的Shader(一)

接下来,我们进行以下步骤:

  1. 在Properties块中添加如下新的属性:
    _Tooniness ("Tooniness", Range(0.1,20)) = 4
  2. 在SubShader块中添加对应的引用:
    float _Tooniness;
  3. 给#pragma添加新的指令final:
    #pragma surface surf Lambert finalcolor:final

    解释:由之前pipeline的图可知,我们有最后一次修改像素的机会,就是使用finalcolor:your function。finalcolor后面紧跟就是我们的函数名,Unity将调用该函数进行最后的修改。其他可供选择的准则可见官网

  4. 实现final函数:
            void final(Input IN, SurfaceOutput o, inout fixed4 color) {
    color = floor(color * _Tooniness)/_Tooniness;
    }

    解释:我们把颜色值乘以_Tooniness,向下取整后再除以_Tooniness。由于color的范围是0到1,乘以_Tooniness再取整将会得到一定范围内的特定整数,这样就使得所有的颜色都被归入到一个已知的集合中,达到了简化颜色的目的。_Tooniness越小,输出的颜色种类越少。

完整代码如下:
Shader "Custom/Toon" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_Bump ("Bump", 2D) = "bump" {}
_Tooniness ("Tooniness", Range(0.1,20)) = 4
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200 CGPROGRAM
#pragma surface surf Lambert finalcolor:final sampler2D _MainTex;
sampler2D _Bump;
float _Tooniness; struct Input {
float2 uv_MainTex;
float2 uv_Bump;
}; void surf (Input IN, inout SurfaceOutput o) {
half4 c = tex2D (_MainTex, IN.uv_MainTex);
o.Normal = UnpackNormal( tex2D(_Bump, IN.uv_Bump));
o.Albedo = c.rgb;
o.Alpha = c.a;
} void final(Input IN, SurfaceOutput o, inout fixed4 color) {
color = floor(color * _Tooniness)/_Tooniness;
} ENDCG
}
FallBack "Diffuse"
}

效果如下:

【Unity Shader实战】卡通风格的Shader(一)

卡通光照

除了上述使用取整的方法简化颜色,更常见的是使用一张渐变贴图(ramp texture)来模拟卡通光照达到目的。下图是我们为怪兽使用的渐变贴图(PS里面画的):

【Unity Shader实战】卡通风格的Shader(一)

这张图的特点就是边界明显,而不像其他渐变图那样是缓慢渐变的。正如卡通风格里面经常有分界明显的明暗变化一样。

我们按如下步骤添加光照函数:

  1. 在Properties块中添加渐变图属性:
    _Ramp ("Ramp Texture", 2D) = "white" {}
  2. 在SubShader块中添加对应的引用:
    sampler2D _Ramp;
  3. 给#pragma添加新的指令:
    #pragma surface surf Toon

    解释:我们去掉了final函数,将其功能移到了后面的surf函数中。这样允许我们有更多的可变性。上述语句说明我们将使用名称为Toon的光照函数。

  4. 修改surf函数:
            void surf (Input IN, inout SurfaceOutput o) {
    half4 c = tex2D (_MainTex, IN.uv_MainTex);
    o.Normal = UnpackNormal( tex2D(_Bump, IN.uv_Bump));
    o.Albedo = (floor(c.rgb * _Tooniness)/_Tooniness);
    o.Alpha = c.a;
    }
  5. 实现Toon光照函数:
            half4 LightingToon(SurfaceOutput s, fixed3 lightDir, half3 viewDir, fixed atten)
    {
    float difLight = max(0, dot (s.Normal, lightDir));
    float dif_hLambert = difLight * 0.5 + 0.5; float rimLight = max(0, dot (s.Normal, viewDir));
    float rim_hLambert = rimLight * 0.5 + 0.5; float3 ramp = tex2D(_Ramp, float2(rim_hLambert, dif_hLambert)).rgb; float4 c;
    c.rgb = s.Albedo * _LightColor0.rgb * ramp;
    c.a = s.Alpha;
    return c;
    }

    解释:上述最重要的部分就是如何在ramp中采样,我们使用了两个值:漫反射光照方向和边缘光照方向。max是为了防止明暗突变的区域产生奇怪的现象,0.5的相关操作则是为了改变光照区间,进一步提高整体亮度。具体可参加之前的文章

完整代码如下:
Shader "Custom/Toon" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_Bump ("Bump", 2D) = "bump" {}
_Ramp ("Ramp Texture", 2D) = "white" {}
_Tooniness ("Tooniness", Range(0.1,20)) = 4
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200 CGPROGRAM
#pragma surface surf Toon sampler2D _MainTex;
sampler2D _Bump;
sampler2D _Ramp;
float _Tooniness;
float _Outline; struct Input {
float2 uv_MainTex;
float2 uv_Bump;
}; void surf (Input IN, inout SurfaceOutput o) {
half4 c = tex2D (_MainTex, IN.uv_MainTex);
o.Normal = UnpackNormal( tex2D(_Bump, IN.uv_Bump));
o.Albedo = (floor(c.rgb * _Tooniness)/_Tooniness);
o.Alpha = c.a;
} half4 LightingToon(SurfaceOutput s, fixed3 lightDir, half3 viewDir, fixed atten)
{
float difLight = max(0, dot (s.Normal, lightDir));
float dif_hLambert = difLight * 0.5 + 0.5; float rimLight = max(0, dot (s.Normal, viewDir));
float rim_hLambert = rimLight * 0.5 + 0.5; float3 ramp = tex2D(_Ramp, float2(rim_hLambert, dif_hLambert)).rgb; float4 c;
c.rgb = s.Albedo * _LightColor0.rgb * ramp;
c.a = s.Alpha;
return c;
} ENDCG
}
FallBack "Diffuse"
}

效果如下:

【Unity Shader实战】卡通风格的Shader(一)

添加描边

最后,我们给模型添加描边效果。这是通过边缘光照(rim lighting)来实现的,在本例中我们将边缘渲染成黑色来实现描边。边缘光照找到那些和观察方向接近90°的像素,再把他们变成黑色。你大概也想到了边缘光照使用的方法了:点乘。

我们按如下步骤实现:

  1. 首先为描边的宽度在Properties块中添加属性:
    _Outline ("Outline", Range(0,1)) = 0.4
  2. 在SubShader块中添加对应的引用:
    float _Outline;
  3. 前面说了,边缘光照需要使用观察方向,因此我们修改Input结构体:
            struct Input {
    float2 uv_MainTex;
    float2 uv_Bump;
    float3 viewDir;
    };

    解释:viewDir也是Unity的内置参数,其他内置参数可在官网找到。

  4. 我们在surf函数中使用如下方法检测那些边:
                half edge = saturate(dot (o.Normal, normalize(IN.viewDir)));
    edge = edge < _Outline ? edge/4 : 1; o.Albedo = (floor(c.rgb * _Tooniness)/_Tooniness) * edge;

    解释:我们首先得到该像素的法线方向和观察方向的点乘结果。如果该结果小于我们的阈值,我们认为这就是我们要找的那些边缘点,并除以4(一个实验值)来减少它的值得到黑色;否则,让它等于1,即没有任何效果。

整体代码如下:
Shader "Custom/Toon" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_Bump ("Bump", 2D) = "bump" {}
_Ramp ("Ramp Texture", 2D) = "white" {}
_Tooniness ("Tooniness", Range(0.1,20)) = 4
_Outline ("Outline", Range(0,1)) = 0.4
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200 CGPROGRAM
#pragma surface surf Toon sampler2D _MainTex;
sampler2D _Bump;
sampler2D _Ramp;
float _Tooniness;
float _Outline; struct Input {
float2 uv_MainTex;
float2 uv_Bump;
float3 viewDir;
}; void surf (Input IN, inout SurfaceOutput o) {
half4 c = tex2D (_MainTex, IN.uv_MainTex);
o.Normal = UnpackNormal( tex2D(_Bump, IN.uv_Bump)); half edge = saturate(dot (o.Normal, normalize(IN.viewDir)));
edge = edge < _Outline ? edge/4 : 1; o.Albedo = (floor(c.rgb * _Tooniness)/_Tooniness) * edge;
o.Alpha = c.a;
} half4 LightingToon(SurfaceOutput s, fixed3 lightDir, half3 viewDir, fixed atten)
{
float difLight = max(0, dot (s.Normal, lightDir));
float dif_hLambert = difLight * 0.5 + 0.5; float rimLight = max(0, dot (s.Normal, viewDir));
float rim_hLambert = rimLight * 0.5 + 0.5; float3 ramp = tex2D(_Ramp, float2(rim_hLambert, dif_hLambert)).rgb; float4 c;
c.rgb = s.Albedo * _LightColor0.rgb * ramp;
c.a = s.Alpha;
return c;
} ENDCG
}
FallBack "Diffuse"
}
最后效果如下:
【Unity Shader实战】卡通风格的Shader(一)

弊端

这是更新的内容。这种方法有一个明显的弊端就是,对于那叫平坦、棱角分明的物体,使用上述描边方法会产生突变等非预期的情况。例如下面的效果:

【Unity Shader实战】卡通风格的Shader(一)

这是因为我们采用了顶点法向量来判断边界的,那么对于正方体这种法线固定单一的情况,判断出来的边界要么基本不存在要么就大的离谱!对于这样的对象,一个更好的方法是用Pixel&Fragment Shader、经过两个Pass渲染描边:第一个Pass,我们只渲染背面的网格,在它们的周围进行描边;第二个Pass中,再正常渲染正面的网格。其实,这是符合我们对于边界的认知的,我们看见的物体也都是看到了它们的正面而已。

当然,对于大多数复杂的对象来说,上述方法也是可以支持的~我看到Assets Store上的Free Toony Colors也是使用相同的方法哦~

第二种方法可以参见卡通风格的Shader(二)

还有一个弊端就是它产生的轮廓的不确定性。按这种方法产生的轮廓是无法保证精确的宽度的,尤其对于那些不是非常平滑的表面。例如上面的小怪兽,它头部的轮廓要比手臂的大很多。当然,可以把这个当成是一种艺术风格。但这种不确定性可能会对于某些需要精确黑色轮廓大小的项目不适用。而这个弊端可以靠精确判断正反面交界处来处理。

第三种方法可以参见卡通风格的Shader(三)(哈哈,我还没写。。。)。

更新

  • 补上两种shader,分别对应有无法线纹理;
  • 修正了半兰伯特部分,即去掉了max操作;
有法线纹理:
Shader "MyToon/Toon-Surface_Normal" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_Bump ("Bump", 2D) = "bump" {}
_Ramp ("Ramp Texture", 2D) = "white" {}
_Tooniness ("Tooniness", Range(0.1,20)) = 4
_Outline ("Outline", Range(0,1)) = 0.4
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200 CGPROGRAM
#pragma surface surf Toon sampler2D _MainTex;
sampler2D _Bump;
sampler2D _Ramp;
float _Tooniness;
float _Outline; struct Input {
float2 uv_MainTex;
float2 uv_Bump;
float3 viewDir;
}; void surf (Input IN, inout SurfaceOutput o) {
half4 c = tex2D (_MainTex, IN.uv_MainTex);
o.Normal = UnpackNormal( tex2D(_Bump, IN.uv_Bump)); half edge = saturate(dot (o.Normal, normalize(IN.viewDir)));
edge = edge < _Outline ? edge/4 : 1; o.Albedo = (floor(c.rgb * _Tooniness)/_Tooniness) * edge;
o.Alpha = c.a;
} half4 LightingToon(SurfaceOutput s, fixed3 lightDir, half3 viewDir, fixed atten)
{
float difLight = dot (s.Normal, lightDir);
float dif_hLambert = difLight * 0.5 + 0.5; float rimLight = dot (s.Normal, viewDir);
float rim_hLambert = rimLight * 0.5 + 0.5; float3 ramp = tex2D(_Ramp, float2(rim_hLambert, dif_hLambert)).rgb; float4 c;
c.rgb = s.Albedo * _LightColor0.rgb * ramp * atten * 2;
c.a = s.Alpha;
return c;
} ENDCG
}
FallBack "Diffuse"
}

无法线纹理:

Shader "MyToon/Toon-Surface" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_Ramp ("Ramp Texture", 2D) = "white" {}
_Tooniness ("Tooniness", Range(0.1,20)) = 4
_Outline ("Outline", Range(0,1)) = 0.4
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200 CGPROGRAM
#pragma surface surf Toon sampler2D _MainTex;
sampler2D _Ramp;
float _Tooniness;
float _Outline; struct Input {
float2 uv_MainTex;
float3 viewDir;
}; void surf (Input IN, inout SurfaceOutput o) {
half4 c = tex2D (_MainTex, IN.uv_MainTex); half edge = saturate(dot (o.Normal, normalize(IN.viewDir)));
edge = edge < _Outline ? edge/4 : 1; o.Albedo = (floor(c.rgb * _Tooniness)/_Tooniness) * edge;
o.Alpha = c.a;
} half4 LightingToon(SurfaceOutput s, fixed3 lightDir, half3 viewDir, fixed atten)
{
float difLight = dot (s.Normal, lightDir);
float dif_hLambert = difLight * 0.5 + 0.5; float rimLight = dot (s.Normal, viewDir);
float rim_hLambert = rimLight * 0.5 + 0.5; float3 ramp = tex2D(_Ramp, float2(dif_hLambert, rim_hLambert)).rgb; float4 c;
c.rgb = s.Albedo * _LightColor0.rgb * ramp * atten * 2;
c.a = s.Alpha;
return c;
} ENDCG
}
FallBack "Diffuse"
}

结束语

本篇一开始是参考了Unity Gems的一篇文章,但在学习过程中发现了一面一些错误和改善的地方,例如里面对光照函数的解释,以及渐变贴图的实现。以后的学习还是要多思考,去其糟粕取其精华啊。

在后面的卡通Shader系列,我会首先学习Unity Gems里面用Fragment Shader实现的方法,最后,再学习一下Unity一个资源包里面的卡通效果实现方法。

欢迎交流和指教!

【Unity Shader实战】卡通风格的Shader(一)的更多相关文章

  1. 【Unity Shader实战】卡通风格的Shader(二)

    写在前面 本系列其他文章: 卡通风格的Shader(一) 好久没写博客了,一定是因为课程作业比较多,一定不是因为我懒,恩恩. 三个月以前,在一篇讲卡通风格的Shader的最后,我们说到在Surface ...

  2. 【Unity Shader】Unity Chan的卡通材质

    写在前面 时隔两个月我终于来更新博客了,之前一直在学东西,做一些项目,感觉没什么可以分享的就一直没写.本来之前打算写云彩渲染或是Compute Shader的,觉得时间比较长所以打算先写个简单的. 今 ...

  3. Unity3d shader之卡通着色Toon Shading

    卡通着色的目的是为了让被着色物体显得过渡的不那么好,明暗交界线很明显,等等卡通风格的一系列特征, 也叫Non-photorealisticrendering非真实渲染 重点要做到两点: 1.    描 ...

  4. 【Unity Shaders】Shader学习资源和Surface Shader概述

    写在前面 写这篇文章的时候,我断断续续学习Unity Shader半年了,其实还是个门外汉.我也能体会很多童鞋那种想要学好Shader却无从下手的感觉.在这个期间,我找到一些学习Shader的教程以及 ...

  5. 关于Unity中的模型描边与Shader切换&lpar;专题二&rpar;

    模型描边 1: LOL里面的模型描边效果,点击防御塔会有描边的效果,被攻击的时候模型也要描边凸显一下2: 网上可以找到模型描边的Shader,可以直接下载使用,一组第三方的Shader, 帮我们解决了 ...

  6. 卡通风格的连连看ios游戏源码

    卡通风格的连连看游戏源码,该游戏是一款韩国人做的卡通风格的ios连连看游戏源码,源码设计的效果非常漂亮的,而且运行起来感觉也很好.1.游戏采用倒计时模式2.该游戏是一款社交游戏,需要通过faceboo ...

  7. Unity User Group 北京站图文报道:《Unity虚拟现实实战技巧》

    时间来到了盛夏,北京UUG活动也来到了第八期.本次活动的主题为<Unity虚拟现实实战技巧>,为此我们邀请了4位资深的行业大神.这次我们仍然在北京市海淀区丹棱街5号微软大厦举行活动,在这里 ...

  8. Unity开发实战探讨-资源的加载释放最佳策略简要心得

    Unity开发实战探讨-资源的加载释放最佳策略简要心得 看过我另外一篇关于Unity资源释放随笔<Unity开发实战探讨-资源的加载释放最佳策略>如果觉得略微复杂,那么下面是一些比较简要的 ...

  9. Unity Shader NPR 卡通渲染

    卡通渲染的主要原理包含两个方面: 1.轮廓线的描边效果 2.模型漫反射离散和纯色高光区域的模拟 描边: 描边的实现方法采用将模型的轮廓线顶点向法线(或顶点)的方向扩展一定的像素得到.也可通过边缘检测( ...

随机推荐

  1. nodejs进阶&lpar;2&rpar;—函数模块调用

    函数调用 1. 文件内普通函数调用 创建一个js文件命名为2_callFunction.js,其中定义一个函数fun1,向返回对象输出了一段字符串“你好,我是fun1”. //------------ ...

  2. DES算法详解

    本文主要介绍了DES算法的步骤,包括IP置换.密钥置换.E扩展置换.S盒代替.P盒置换和末置换. 1.DES算法简介 DES算法为密码*中的对称密码*,又被称为美国数据加密标准. DES是一个分组 ...

  3. android下拉选择框spinner

    spinner是什么东西呢?有点像下拉菜单,其实是一个弹出窗口,但是是可以进行进一步操作的弹出窗口.你点击那个三角形的符号,弹出一个窗口,通常是列表,然后进行操作. 它在xml文件中的定义和其它控件没 ...

  4. 从 man 指令起步(info简介)

    前言 小生认为一切指令的学习首先要从帮助入手,深入了解它的功能,即使是在实际项目中我们都离不开它的帮助.因为我们不一定能够记住全部指令的全部的相关功能,因此,查看指令的帮助是我们的不二选择. 正文 下 ...

  5. SSH框架整合 日志处理Spring结合 log4j、slf4j

    1. 加入log4j和slf4j的jar包 2. web.xml: <context-param> <!--log4j配置地址 --> <param-name>lo ...

  6. VB&period;net shell、IO&period;File&period;Open、Process&period;Start、Shellexecute API 运用经验总结

    打开文件还有很多方法,但我了解到运用较多的是上面几种- -,为了防止以后忘记,先把了解到的写下来. 1.Shell 这个看了很多网页,最靠谱的运用方法: Shell("cmd.exe /c ...

  7. 二、使用docker-compose搭建AspNetCore开发环境

    1 使用docker-compose搭建开发环境 我们的目标很简单:使用docker-compose把若干个docker容器组合起来就成了. 首先使用Nginx代理所有的Web程序,这样只需要在主机上 ...

  8. nginx 常用命令

    -?,-h         : this help  -v            : show version and exit  -V            : show version and c ...

  9. JSP概述

    一.JSP页面本质上时一个Servlet,然而,用JSP开发比使用Servlet更容易,主要有两个原因,首先不必编译Servlet,其次JSP页面是一个以.jsp为扩展名的文本文件,可以使用任何编辑器 ...

  10. TCP&sol;IP学习20180627-数据链路层-ethernet

    ifconfig :查看主機支持的網絡協議eth0:以太網接口lo:loopback接口 以太网(Ether-net)的定是指数字设备公司( Digital Equipment Corp.).英特尔公 ...