Unity3d shader之卡通着色Toon Shading

卡通着色的目的是为了让被着色物体显得过渡的不那么好,明暗交界线很明显,等等卡通风格的一系列特征,

也叫Non-photorealisticrendering非真实渲染

重点要做到两点:

1.    描边

2.    着色

另:本片中cg函数均用绿色标明,想了解函数作用和函数内部构成请看这篇文章NVIDIA CG语言 函数之所有数学类函数(Mathematical Functions)

就从最初的描边开始

首先声明变量
_Outline挤出描边的粗细
_Factor挤出多远

Properties {
		_Color("Main Color",color)=(1,1,1,1)
		_Outline("Thick of Outline",range(0,0.1))=0.02
		_Factor("Factor",range(0,1))=0.5
	}

我们的挤出操作在第一个pass中进行
Cull Front 裁剪了物体的前面(对着相机的),把背面挤出
ZWrite On 像素的深度写入深度缓冲,如果关闭的话,物体与物体交叠处将不会被描边,因为此处无z值后渲染的物体会把此处挤出的描边“盖住”

在处理顶点的函数vert中把点挤出

dir=normalize(v.vertex.xyz);
建立一个float3方向变量dir
把该点的位置作为距离几何中心的方向的单位向量

float3 dir2=v.normal;
建立一个float3方向变量dir
dir2为法线方向

D=dot(dir,dir2);
D为计算该点位置朝向和法线方向的点积,通过正负值可以确定是指向还是背离几何中心的,正为背离,负为指向

dir=dir*sign(D);
乘上正负值,真正的方向值

dir=dir*_Factor+dir2*(1-_Factor);

把该点位置朝向与法线方向按外部变量_Factor的比重混合,来控制挤出多远

v.vertex.xyz+=dir*_Outline;
把物体背面的点向外挤出

		v2f vert (appdata_full v) {
			v2f o;
			float3 dir=normalize(v.vertex.xyz);
			float3 dir2=v.normal;
			float D=dot(dir,dir2);
			dir=dir*sign(D);
			dir=dir*_Factor+dir2*(1-_Factor);
			v.vertex.xyz+=dir*_Outline;
			o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
			return o;
		}

顶点函数结束,
接下来为描边上色
在frag函数中

挤出轮廓的颜色,此处颜色随意

效果如下:

清楚地描出了轮廓,可以在材质中改变_Outline的值来改变粗细

描边shader如下:

Shader "Tut/Shader/Toon/miaobian" {
Properties {
		_Color("Main Color",color)=(1,1,1,1)
		_Outline("Thick of Outline",range(0,0.1))=0.02
		_Factor("Factor",range(0,1))=0.5
	}
	SubShader {
		pass{
		Tags{"LightMode"="Always"}
		Cull Front
		ZWrite On
		CGPROGRAM
		#pragma vertex vert
		#pragma fragment frag
		#include "UnityCG.cginc"
		float _Outline;
		float _Factor;
		float4 _Color;
		struct v2f {
			float4 pos:SV_POSITION;
		};

		v2f vert (appdata_full v) {
			v2f o;
			float3 dir=normalize(v.vertex.xyz);
			float3 dir2=v.normal;
			float D=dot(dir,dir2);
			dir=dir*sign(D);
			dir=dir*_Factor+dir2*(1-_Factor);
			v.vertex.xyz+=dir*_Outline;
			o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
			return o;
		}
		float4 frag(v2f i):COLOR
		{
			float4 c = _Color / 5;
			return c;
		}
		ENDCG
		}
		pass{
		Tags{"LightMode"="ForwardBase"}
		Cull Back
		CGPROGRAM
		#pragma vertex vert
		#pragma fragment frag
		#include "UnityCG.cginc"

		float4 _LightColor0;
		float4 _Color;
		float _Steps;
		float _ToonEffect;

		struct v2f {
			float4 pos:SV_POSITION;
			float3 lightDir:TEXCOORD0;
			float3 viewDir:TEXCOORD1;
			float3 normal:TEXCOORD2;
		};

		v2f vert (appdata_full v) {
			v2f o;
			o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
			o.normal=v.normal;
			o.lightDir=ObjSpaceLightDir(v.vertex);
			o.viewDir=ObjSpaceViewDir(v.vertex);

			return o;
		}
		float4 frag(v2f i):COLOR
		{
			float4 c=1;
			float3 N=normalize(i.normal);
			float3 viewDir=normalize(i.viewDir);
			float3 lightDir=normalize(i.lightDir);
			float diff=max(0,dot(N,i.lightDir));
			diff=(diff+1)/2;
			diff=smoothstep(0,1,diff);
			c=_Color*_LightColor0*(diff);
			return c;
		}
		ENDCG
		}//
	}
}

开始卡通着色旅程

描边之后就是重头戏着色了

简单的举几个例子

说明一下卡通着色

把diffuse漫反射颜色变成了很明显的几个色阶(本例为四个)

普通的diffuse漫反射龙,没有色阶层,颜色过渡的很好,没有卡通的感觉

普通的漫反射材质

上图的可爱的大河酱就是由二阶颜色着色而成,再加上边缘黑色的描边,这个是真正的卡通,不是渲染出来的= =

《深渊传说》是传说系列的早期作品之一,用的也是卡通渲染

感觉大部分都是卡通式纹理贴图在出力

《仙乐传说》的战斗结束画面

有明显的明暗交界线(两个色阶),并随摄像头(view direction)的变化而变化,人物有明显的描边处理,卡通着色起了很大作用

《无尽传说2》,是传说系列比较近的作品,画面明显比前做好了许多,但万变不离其宗,还是用的卡通着色,(= =没玩过这作)

人物有着明显的描边处理

另外我感觉泛光的效果很好啊,应该是bloom或者是hdr之类的,跑题了= =

开始动手操刀卡通着色

第一个pass就是上面的描边pass

对漫反射的卡通着色在第二个pass中

先声明变量
_Color物体的颜色
_Outline挤出描边的粗细
_Factor挤出多远
_ToonEffect卡通化程度(二次元与三次元的交界线)
_Steps色阶层数

	Properties {
		_Color("Main Color",color)=(1,1,1,1)//物体的颜色
		_Outline("Thick of Outline",range(0,0.1))=0.02//挤出描边的粗细
		_Factor("Factor",range(0,1))=0.5//挤出多远
		_ToonEffect("Toon Effect",range(0,1))=0.5//卡通化程度(二次元与三次元的交界线)
		_Steps("Steps of toon",range(0,9))=3//色阶层数
	}

卡通着色主要在着色函数frag中进行
float diff=max(0,dot(N,i.lightDir));
求出正常的漫反射颜色

diff=(diff+1)/2;
做亮化处理

diff=smoothstep(0,1,diff);
使颜色平滑的在[0,1]范围之内

float toon=floor(diff*_Steps)/_Steps;
把颜色做离散化处理,把diffuse颜色限制在_Steps种(_Steps阶颜色),简化颜色,这样的处理使色阶间能平滑的显示

diff=lerp(diff,toon,_ToonEffect);
根据外部我们可控的卡通化程度值_ToonEffect,调节卡通与现实的比重

c=_Color*_LightColor0*(diff);
把最终颜色混合

第二个pass结束,

		float4 frag(v2f i):COLOR
		{
			float4 c=1;
			float3 N=normalize(i.normal);
			float3 viewDir=normalize(i.viewDir);
			float3 lightDir=normalize(i.lightDir);
			float diff=max(0,dot(N,i.lightDir));//求出正常的漫反射颜色
			diff=(diff+1)/2;//做亮化处理
			diff=smoothstep(0,1,diff);//使颜色平滑的在[0,1]范围之内
			float toon=floor(diff*_Steps)/_Steps;//把颜色做离散化处理,把diffuse颜色限制在_Steps种(_Steps阶颜色),简化颜色,这样的处理使色阶间能平滑的显示
			diff=lerp(diff,toon,_ToonEffect);//根据外部我们可控的卡通化程度值_ToonEffect,调节卡通与现实的比重

			c=_Color*_LightColor0*(diff);//把最终颜色混合
			return c;
		}

第三个pass就是在第二个pass的基础之上加上离散化的高光

建立float变量dist为求出距离光源的距离float

float atten=1/(dist);
根据距光源的距离求出衰减;

漫反射部分与第二个pass相同;

half3 h = normalize (lightDir + viewDir);

求出半角向量

float nh = max (0, dot (N, h));
float spec = pow (nh, 32.0);

求出正常情况下的高光强度

float toonSpec=floor(spec*atten*2)/ 2;
把高光也离散化

spec=lerp(spec,toonSpec,_ToonEffect);
调节卡通与现实高光的比重

		float4 frag(v2f i):COLOR
		{
			float4 c=1;
			float3 N=normalize(i.normal);
			float3 viewDir=normalize(i.viewDir);
			float dist=length(i.lightDir);//求出距离光源的距离
			float3 lightDir=normalize(i.lightDir);
			float diff=max(0,dot(N,i.lightDir));
			diff=(diff+1)/2;
			diff=smoothstep(0,1,diff);
			float atten=1/(dist);//根据距光源的距离求出衰减
			float toon=floor(diff*atten*_Steps)/_Steps;
			diff=lerp(diff,toon,_ToonEffect);

			half3 h = normalize (lightDir + viewDir);//求出半角向量
			float nh = max (0, dot (N, h));
			float spec = pow (nh, 32.0);//求出高光强度
			float toonSpec=floor(spec*atten*2)/ 2;//把高光也离散化
			spec=lerp(spec,toonSpec,_ToonEffect);//调节卡通与现实高光的比重

			c=_Color*_LightColor0*(diff+spec);//求出最终颜色
			return c;
		}

就可以得到这种卡通效果:

shader如下:

Shader "Tut/Shader/Toon/toon" {
	Properties {
		_Color("Main Color",color)=(1,1,1,1)//物体的颜色
		_Outline("Thick of Outline",range(0,0.1))=0.02//挤出描边的粗细
		_Factor("Factor",range(0,1))=0.5//挤出多远
		_ToonEffect("Toon Effect",range(0,1))=0.5//卡通化程度(二次元与三次元的交界线)
		_Steps("Steps of toon",range(0,9))=3//色阶层数
	}
	SubShader {
		pass{//处理光照前的pass渲染
		Tags{"LightMode"="Always"}
		Cull Front
		ZWrite On
		CGPROGRAM
		#pragma vertex vert
		#pragma fragment frag
		#include "UnityCG.cginc"
		float _Outline;
		float _Factor;
		struct v2f {
			float4 pos:SV_POSITION;
		};

		v2f vert (appdata_full v) {
			v2f o;
			float3 dir=normalize(v.vertex.xyz);
			float3 dir2=v.normal;
			float D=dot(dir,dir2);
			dir=dir*sign(D);
			dir=dir*_Factor+dir2*(1-_Factor);
			v.vertex.xyz+=dir*_Outline;
			o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
			return o;
		}
		float4 frag(v2f i):COLOR
		{
			float4 c=0;
			return c;
		}
		ENDCG
		}//end of pass
		pass{//平行光的的pass渲染
		Tags{"LightMode"="ForwardBase"}
		Cull Back
		CGPROGRAM
		#pragma vertex vert
		#pragma fragment frag
		#include "UnityCG.cginc"

		float4 _LightColor0;
		float4 _Color;
		float _Steps;
		float _ToonEffect;

		struct v2f {
			float4 pos:SV_POSITION;
			float3 lightDir:TEXCOORD0;
			float3 viewDir:TEXCOORD1;
			float3 normal:TEXCOORD2;
		};

		v2f vert (appdata_full v) {
			v2f o;
			o.pos=mul(UNITY_MATRIX_MVP,v.vertex);//切换到世界坐标
			o.normal=v.normal;
			o.lightDir=ObjSpaceLightDir(v.vertex);
			o.viewDir=ObjSpaceViewDir(v.vertex);

			return o;
		}
		float4 frag(v2f i):COLOR
		{
			float4 c=1;
			float3 N=normalize(i.normal);
			float3 viewDir=normalize(i.viewDir);
			float3 lightDir=normalize(i.lightDir);
			float diff=max(0,dot(N,i.lightDir));//求出正常的漫反射颜色
			diff=(diff+1)/2;//做亮化处理
			diff=smoothstep(0,1,diff);//使颜色平滑的在[0,1]范围之内
			float toon=floor(diff*_Steps)/_Steps;//把颜色做离散化处理,把diffuse颜色限制在_Steps种(_Steps阶颜色),简化颜色,这样的处理使色阶间能平滑的显示
			diff=lerp(diff,toon,_ToonEffect);//根据外部我们可控的卡通化程度值_ToonEffect,调节卡通与现实的比重

			c=_Color*_LightColor0*(diff);//把最终颜色混合
			return c;
		}
		ENDCG
		}//
		pass{//附加点光源的pass渲染
		Tags{"LightMode"="ForwardAdd"}
		Blend One One
		Cull Back
		ZWrite Off
		CGPROGRAM
		#pragma vertex vert
		#pragma fragment frag
		#include "UnityCG.cginc"

		float4 _LightColor0;
		float4 _Color;
		float _Steps;
		float _ToonEffect;

		struct v2f {
			float4 pos:SV_POSITION;
			float3 lightDir:TEXCOORD0;
			float3 viewDir:TEXCOORD1;
			float3 normal:TEXCOORD2;
		};

		v2f vert (appdata_full v) {
			v2f o;
			o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
			o.normal=v.normal;
			o.viewDir=ObjSpaceViewDir(v.vertex);
			o.lightDir=_WorldSpaceLightPos0-v.vertex;

			return o;
		}
		float4 frag(v2f i):COLOR
		{
			float4 c=1;
			float3 N=normalize(i.normal);
			float3 viewDir=normalize(i.viewDir);
			float dist=length(i.lightDir);//求出距离光源的距离
			float3 lightDir=normalize(i.lightDir);
			float diff=max(0,dot(N,i.lightDir));
			diff=(diff+1)/2;
			diff=smoothstep(0,1,diff);
			float atten=1/(dist);//根据距光源的距离求出衰减
			float toon=floor(diff*atten*_Steps)/_Steps;
			diff=lerp(diff,toon,_ToonEffect);

			half3 h = normalize (lightDir + viewDir);//求出半角向量
			float nh = max (0, dot (N, h));
			float spec = pow (nh, 32.0);//求出高光强度
			float toonSpec=floor(spec*atten*2)/ 2;//把高光也离散化
			spec=lerp(spec,toonSpec,_ToonEffect);//调节卡通与现实高光的比重

			c=_Color*_LightColor0*(diff+spec);//求出最终颜色
			return c;
		}
		ENDCG
		}//
	}
}

这样已经能做出很好的卡通效果了,各位可以在此基础上研制加强版

加强版1:

加上了边缘光Rim

漂亮的水蓝色星球(= =;纹理)

就是在第二个pass中加上rim值

添加的外部变量;
        
_RimPower边缘光亮度程度
        
_ToonRimStep边缘光色阶数

在frag函数中
float rim = 1.0 - saturate(dot(N, normalize (viewDir)));
求出正常的边缘光程度

rim = rim+1;
使之加亮
            
rim = pow(rim, _RimPower);
外部变量_RimPower控制边缘光亮度大小
            
float toonRim = floor(rim * _ToonRimStep) / _ToonRimStep;
再对边缘光进行离散化
            
rim = lerp(rim, toonRim, _ToonEffect);
调节卡通与现实的比重
            
c=_Color*_LightColor0*(diff) * rim * mc*2;

进行最终颜色混合

		float4 frag(v2f i):COLOR
		{
			half4 mc = tex2D (_MainTex, i.uv_MainTex);
			float4 c=1;
			float3 N=normalize(i.normal);
			float3 viewDir=normalize(i.viewDir);
			float3 lightDir=normalize(i.lightDir);
			float diff=max(0,dot(N,i.lightDir));
			diff=(diff+1)/2;
			diff=smoothstep(0,1,diff);
			float toon=floor(diff*_Steps)/_Steps;
			diff=lerp(diff,toon,_ToonEffect);
			float rim = 1.0 - saturate(dot(N, normalize (viewDir)));//求出正常的边缘光程度
			rim = rim+1;//使之加亮
			rim = pow(rim, _RimPower);//外部变量_RimPower控制边缘光亮度大小
			float toonRim = floor(rim * _ToonRimStep) / _ToonRimStep;//再对边缘光进行离散化
			rim = lerp(rim, toonRim, _ToonEffect);//调节卡通与现实的比重
			c=_Color*_LightColor0*(diff) * rim * mc*2;//进行最终颜色混合
			return c;
		}

活在三次元世界的3d布料和活在二次元世界的3d布料

加强版2:

带纹理贴图版

加强版3:

带纹理贴图Rim版

带纹理贴图Rim版变种

不建议把纹理贴图也离散化,效果实在是不好,= =;

接下来就有待各位看官们继续开发了,有什么更好的效果一定要告诉我

-----------------------by wolf96

时间: 2024-10-19 14:49:53

Unity3d shader之卡通着色Toon Shading的相关文章

unity3d shader 预定义着色器预处理宏

编译着色器程序时,Unity 会定义几个预处理宏. 目标平台 SHADER_API_OPENGL - 桌面 OpenGL SHADER_API_D3D9 - Direct3D 9 SHADER_API_XBOX360 - Xbox 360 SHADER_API_PS3 - PlayStation 3 SHADER_API_D3D11 - 桌面 Direct3D 11 SHADER_API_GLES - OpenGL ES 2.0(桌面或移动),使用 SHADER_API_MOBILE 的存在来确

unity3d shader 学习

[浅墨Unity3D Shader编程] 着色器参考 [Unity Shaders]

【译】Unity3D Shader 新手教程(4/6) —— 卡通shader(入门版)

暗黑系 动机 如果你满足以下条件,我建议你阅读这篇教程: 你想了解更多有关表面着色器的细节知识. 你想实现一个入门级别的卡通效果shader(Toon Shader). 你想知道渐变纹理(ramp texture)的使用方式. 你想了解边缘光照(rim lighting)的知识. 准备工作 我们想实现一个toon shader - 一种能让模型看起来具有卡通效果的shader,在图形学领域,这被称作非真实感图形学(Non Photorealistic Rendering). 为了实现这种卡通效果

【浅墨Unity3D Shader编程】之三 光之城堡篇:子着色器、通道与标签的写法 & 纹理混合

本系列文章由@浅墨_毛星云 出品,转载请注明出处. 文章链接:http://blog.csdn.net/poem_qianmo/article/details/41175585 作者:毛星云(浅墨)    微博:http://weibo.com/u/1723155442 邮箱: [email protected] 本文介绍了Unity中子着色器.通道和标签相关的详细概念与写法,以及纹理的设置方法,基本的纹理混合写法,写了5个Shader作为本文Shader讲解的实战内容,最后创建了一个梦幻的光之

【浅墨Unity3D Shader编程】之七 静谧之秋篇: 表面着色器的写法(二)——自定义光照模式

本系列文章由@浅墨_毛星云 出品,转载请注明出处. 文章链接:http://hpw123.net/plus/view.php?aid=183 作者:毛星云(浅墨)    微博:http://weibo.com/u/1723155442 邮箱: [email protected] QQ交流群:330595914 更多文章尽在:http://www.hpw123.net 本文主要讲解了Unity中SurfaceShader的自定义光照模式的写法. 上篇文章中我们已经说到,表面着色器将分为两次讲解,上

【译】Unity3D Shader 新手教程(1/6)

刚开始接触Unity3D Shader编程时,你会发现有关shader的文档相当散,这也造成初学者对Unity3D Shader编程望而却步.该系列教程的第一篇文章(译者注:即本文,后续还有5篇文章)详细介绍了Unity3D中的表面着色器(Surface Shader)的,为学习更复杂的Shader编程打下基础. 动机 如果你是刚刚接触Shader编程的新手,你可能不知道从何开始踏出Shader编程的第一步.本教程将带你一步步完成一个表面着色器(Surface Shader)和片段着色器(Fra

Unity3D ShaderLab 透明裁剪着色器

Unity3D ShaderLab 透明裁剪着色器 上一篇,我们介绍了使用Alpha实现透明的手法,其实Unity为我们的#pragma提供了另一种参数,是我们能做出更高效 简单的透明效果,也就是裁剪透明. 这种透明使用一个值来简单的控制某些特定的像素无需渲染到屏幕上,所以我们也可以通过他实现一个要么完全透明或完全不透的着色器. 我们即将利用灰度的值来控制材质的透明度. 准备工作还是新建Shader Material,一张灰度变化图.同样是分分钟完成的代码,请看完成: Shader "91YGa

【浅墨Unity3D Shader编程】之二 雪山飞狐篇:Unity的基本Shader框架写法&颜色、光照与材质

本系列文章由@浅墨_毛星云 出品,转载请注明出处. 文章链接:http://blog.csdn.net/poem_qianmo/article/details/40955607 作者:毛星云(浅墨)    微博:http://weibo.com/u/1723155442 邮箱: [email protected] 本篇文章中,我们学习了Unity Shader的基本写法框架,以及学习了Shader中Properties(属性)的详细写法,光照.材质与颜色的具体写法.写了6个Shader作为本文S

【浅墨Unity3D Shader编程】之五 圣诞夜篇: Unity中Shader的三种形态对比&混合操作合辑

本系列文章由@浅墨_毛星云 出品,转载请注明出处.  文章链接:http://hpw123.net/a/C__/kongzhitaichengxu/2014/1222/164.html 作者:毛星云(浅墨)    微博:http://weibo.com/u/1723155442 邮箱: [email protected] QQ交流群:330595914 更多文章尽在:http://www.hpw123.net 本文算是固定功能Shader的最后一篇,下一次更新应该就会开始讲解表面Shader,而