【译】Unity3D Shader 新手教程(6/6) —— 更好的卡通Shader
本文为翻译,附上原文链接。
转载请注明出处——polobymulberry-博客园。
动机
如果你想了解以下几件事,我建议你阅读以下这篇教程:
- 想知道如何写一个multipass的toon shader。
- 在shader中学习更多不同参考坐标系(空间space)以及其作用。
- 深入学习一个实用的fragment shader。
- 学习矩阵相乘和Unity内建矩阵的使用。
该教程比第五篇教程更实用。
准备工作
为了实现一个描边的toon shader,我们需要做的是:
- 为模型描边。
- 将第四篇文章中的介绍的toon shader(使用的是surface shader)移植到vertex&fragment shader中。
描边
有很多方法进行描边,在第四篇文章中,我们使用了rim lighting(边缘光照)来给我们人物加上描边效果。现在我们采用另一种方法,额外使用一个Pass改善已有的描边效果。
不同于之前描边效果的实现,在这篇教程中,你可以将你看不到的模型部分(比如背面)放大一些,再渲染成全黑,这样也是可以实现描边效果的。这种方法可以将原模型的正面完好无损呈现出来。
所以我们首先试着:
- 单独写一个仅仅用来绘制模型背面的Pass。
- 扩展模型背面的顶点,使其看起来变大了一些。
下面这个Pass就是用来仅仅绘制模型背面(Cull Front,剔除正面的多边形):
Pass {
Cull Front
Lighting Off
}
现在让我们考虑最简单的部分 — 将传入该Pass的所有像素值绘制成黑色!
CGPROGRAM
#pragma vertex vert
#pragma fragment frag #include "UnityCG.cginc" //剩下的功能在此处实现 float4 frag (v2f IN) : COLOR
{
return float4(0,0,0,1);
} ENDCG
该fragment函数返回float4(0,0,0,1) — 全黑。
现在为我们的shader添加输入结构体。我们利用该结构体(包含vertex和normal)来将我们模型的每个顶点沿法向进行延伸扩展 — 该顶点是背面面片上的点。所以我们输入结构体必须含有顶点位置vertex和顶点法向normal信息。
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
}; struct v2f
{
float4 pos : POSITION;
};
接下来我们在Properties代码区域定义一个_Outline属性值,范围为0.0~1.0,我们在CG代码中定义一个相同的变量float _Outline。
最后我们在vertex函数vert中延着法向normal伸展顶点:
float _Outline; v2f vert(a2v v)
{
v2f o;
o.pos = mul( UNITY_MATRIX_MVP, v.vertex + (float4(v.normal, 0) * _Outline));
return o;
}
我们所做的就是将v.vertex沿着normal伸展了_Outline比例大小,然后使用Unity内置的矩阵UNITY_MATRIX_MVP将结果转换到投影空间(projection space)。
矩阵在shader中用来转化很多事情。我们可以从下图看出,一个4x4的矩阵乘上一个4x1的矩阵,得到还是一个4*1的矩阵。Unity中有很多预定好的矩阵,我们可以使用这些矩阵得到各种空间坐标系的转换。
目前你的代码应该保证像下面这样了(注意这是在第五部分教程的基础上添加的代码):
Pass {
// 剔除模型正面,只渲染背面
Cull Front
Lighting Off CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc" struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
}; struct v2f
{
float4 pos : POSITION;
} float _Outline; v2f vert(a2v v)
{
v2f o;
o.pos = mul( UNITY_MATRIX_MVP, v.vertex + (float4(v.normal, 0) * _Outline));
return o;
} float frag(v2f IN) : COLOR
{
return float4(0,0,0,1)
} ENDCG
}
看上去好像有点效果,但是仔细看他的嘴巴,我们可以看到是有很大问题。这是因为实现边缘效果的Pass是可以写入深度缓存的。所以在有些情况下,模型正面是无法正常绘制的。
拿此处的嘴举例,此处的嘴巴的上嘴唇是属于正面的,而下嘴唇是反面(多边形方向为逆时针)。所以Cull Front后会剔除上嘴唇,保留下嘴唇。而下嘴唇的法向很明显差不多是朝上的,所以在vert函数中会在下嘴唇上方产生这种黑条状的面片。又因为我们是可以写入深度缓存的,所以会将这黑色面片写入到深度缓存,而这黑色面片恰好在嘴唇前面,所以嘴唇正面在绘制时通过不了深度测试,只留下这黑色的面片。
自然而然地我们肯定能想到,让这个黑色面片不进行深度缓存测试不就行了。下面这幅图就是在该Pass中关闭Z buffer测试的结果。
使用下面这段代码:
Pass {
Cull Front
Lighting Off
ZWrite Off
关闭Z buffer测试后,哪些多余的黑色面片确实不存在了。可是又有一个新问题出现了。因为黑色面片始终通过不了Z Buffer测试,所以模型本身的面片会覆掉这些黑色面片。我们看到下面这张图,前面的模型挡住了后面模型产生的黑色边缘。这又不是我们想要的。
现在我们大概知道问题的本质就是黑色面片是沿着法向扩展了一定长度,其Z值也就发生了变化。如果我们特意处理下Z值,使其产生的背面的黑色面片的Z值小一点,也就是离视点远一些,而不是像一个新产生的模型一样附在物体表面。这样的话,对于边缘效果,其主要作用的将是x和y分量,而不是z分量。
现在回到我们的vertex函数,然后做一些矩阵变换。
将背面产生的黑色面片在Z方向压扁
首先迎接的挑战是我们的顶点和法向是在模型空间 — 但是我们要将其转换到视空间(相机为原点的空间,还未经过投影变换),这是因为在视空间中,z轴指向相机,也就是模型z值恰好表示模型距离相机的远近。
下面介绍几个Unity内建的矩阵。
首先我们不再将顶点转换到投影空间中,而是将顶点先转换到视空间中 — 这很简单,仅仅需要使用一个不同的矩阵。
然后我们要将对应法向值转化到视空间中 — 这里使用了一个trick,因为将法向从模型空间转换到视空间不能简单使用矩阵UNITY_MATRIX_MV。得使用UNITY_MATRIX_MV的逆转置矩阵UNITY_MATRIX_IT_MV(其中IT表示Inverse Transpose)。直接将法向乘以UNITY_MATRIX_MV得到的结果将不再垂直原来的面片。本质原因其实是因为顶点是一个点,而法向是一个方向向量。
比如下图以及下面的推导公式:
所以我们所要做的就是:
- 将顶点转化到视空间中。— pos = mul( UNITY_MATRIX_MV, v.vertex);
- 将法向转化到视空间中。— normal = mul( (float3x3)UNITY_MATRIX_IT_MV, v.normal);
- 修正法向量的z分量为某个特定最小值 — normal.z = -0.4 (这样黑色边缘延伸扩展就会沿模型背面扩展,不会出现在模型前面了)
- 重新单位化法向(因为在之前的步骤中,我们改变了法向,破坏了它的单位长度)
- 使用_Outline缩放法向长度,然后加到将顶点位置沿法向平移这么长。
- 将顶点转化到投影空间中。
所有代码看起来就像下面这样:
v2f vert (a2v v)
{
v2f o;
float4 pos = mul( UNITY_MATRIX_MV, v.vertex);
float3 normal = mul( (float3x3)UNITY_MATRIX_IT_MV, v.normal);
normal.z = -0.4;
pos = pos + float4(normalize(normal),0) * _Outline;
o.pos = mul(UNITY_MATRIX_P, pos); return o;
}
注意Unity中使用的矩阵是4x4 — 但是我们的法向是float3类型 — 我们必须将矩阵转化为3x3 — (float3x3)UNITY_MATRIX_IT_MV,否则我们会在Unity的控制台得到很多错误。
如果我们使用ZWrite On — 效果看起来像下面这样:
这种效果对我们已经足够了。
卡通化
剩下的就是将我们之前使用表面着色器制作的Toon Shader应用到vertex&fragment shader中。
首先我们像教程第四部分那样定义一个_Ramp属性值,并相应的定义sampler2D _Ramp。
使用ramp texture(渐变纹理) — 然后我们添加一个_ColorMerge属性变量(一个float类型的值),利用其降低模型颜色的种类。
我们改变教程第五部分的fragment函数 — 就像下面这样:
float4 frag(v2f i) : COLOR
{
// 根据uv坐标从纹理中获得对应像素值
float4 c = tex2D (_MainTex, i.uv);
// 降低颜色种类
c.rgb = (floor(c.rgb*_ColorMerge)/_ColorMerge); //从bump纹理中得到对应像素的法向
float3 n = UnpackNormal(tex2D (_Bump, i.uv2)); //获得漫射光颜色
float3 lightColor = UNITY_LIGHTMODEL_AMBIENT.xyz; //计算出光源距离
float lengthSq = dot(i.lightDirection, i.lightDirection);
//根据计算出的光源位置计算光强的衰减
float atten = 1.0 / (1.0 + lengthSq);
//光的入射角
float diff = saturate (dot (n, normalize(i.lightDirection)));
//利用渐变纹理
diff = tex2D(_Ramp, float2(diff, 0.5));
//根据入射角,光衰减得到最终光照亮度
lightColor += _LightColor0.rgb * (diff * atten);
//将光照亮度与本身颜色相乘,得到最终颜色
c.rgb = lightColor * c.rgb * 2;
return c;
}
我们所要做的就是利用_MainTex纹理进行采样,然后降低颜色种类,最后使用渐变纹理获得的数值作为光强。
下图使我们最终的效果:
完整的源码在这里。
对于其他光照的ForwardAdd部分,就留给你们自己写吧!
【译】Unity3D Shader 新手教程(6/6) —— 更好的卡通Shader的更多相关文章
- 【译】Unity3D Shader 新手教程(2/6) —— 积雪Shader
本文为翻译,附上原文链接. 转载请注明出处--polobymulberry-博客园. 如果你是一个shader编程的新手,并且你想学到下面这些酷炫的技术,我觉得你可以看看这篇教程: 实现一个积雪效果的 ...
- 【译】Unity3D Shader 新手教程(1/6)
本文为翻译,附上原文链接. 转载请注明出处--polobymulberry-博客园. 刚开始接触Unity3D Shader编程时,你会发现有关shader的文档相当散,这也造成初学者对Unity3D ...
- 【译】Unity3D Shader 新手教程(4/6) —— 卡通shader(入门版)
本文为翻译,附上原文链接. 转载请注明出处--polobymulberry-博客园. 暗黑系 动机 如果你满足以下条件,我建议你阅读这篇教程: 你想了解更多有关表面着色器的细节知识. 你想实现一个入门 ...
- 【译】Unity3D Shader 新手教程(3/6) —— 更加真实的积雪
本文为翻译,附上原文链接. 转载请注明出处--polobymulberry-博客园. 如果你满足以下条件,我建议你阅读这篇教程: 你想知道如何在表面着色器中进行混色(blend colour) 你想实 ...
- 【译】Unity3D Shader 新手教程(5/6) —— Bumped Diffuse Shader
本文为翻译,附上原文链接. 转载请注明出处--polobymulberry-博客园. 动机 如果你满足以下条件,我建议你阅读这篇教程: 你想学习片段着色器(Fragment Shader). 你想实现 ...
- 【译】Meteor 新手教程:在排行榜上添加新特性
原文:http://danneu.com/posts/6-meteor-tutorial-for-fellow-noobs-adding-features-to-the-leaderboard-dem ...
- Unity3D Shader官方教程翻译(十九)----Shader语法,编写表面着色器
Writing Surface Shaders Writing shaders that interact with lighting is complex. There are different ...
- ionic新手教程第八课-(加更)从无到有说Ionic、绘图说明MVC-U-S
这节课的内容,有些前面几节已经说过了. 公司这次给我一个任务,让我带一个没有编程基础的同事学习ionic. 今天是我跟他讲的第一课,晚上把讲的笔记整理了一下,认为还是挺适合零基础的朋友学习的. 有些前 ...
- 【OpenCV新手教程之十一】 形态学图像处理(二):开运算、闭运算、形态学梯度、顶帽、黑帽合辑
本系列文章由@浅墨_毛星云 出品,转载请注明出处. 文章链接:http://blog.csdn.net/poem_qianmo/article/details/23184547 作者:毛星云(浅墨) ...
随机推荐
- VM(虚拟机安装win7 提示 :units specified don't exist, SHSUCDX can't install)解决方法
改成IDE的模式
- 如何利用ansible callback插件对执行结果进行解析
最近在写一个批量巡检工具,利用ansible将脚本推到各个机器上执行,然后将执行的结果以json格式返回来. 如下所示: # ansible node2 -m script -a /root/pyth ...
- 前端制作动画的几种方式(css3,js)
制作动态的网页是是前端工程师必备的技能,很好的实现动画能够极大的提高用户体验,增强交互效果,那么动画有多少实现方式,一直对此有选择恐惧症的我就总结一下,以便在开发的时候选择最好的实现方式. 1.css ...
- 【需求设计1】VIP积分系统无聊YY
RT,想到什么就写什么呗,这是最简单的方式,顺便给自己做一个记录,反正自己记忆力也不太好.本文是仿陆金所的积分系统,自己YY的一套东西. 首先我想做一个VIP兑换投资卷的功能: 我们先来确定一些我知道 ...
- Axure 8.0.0.3312可用注册码
用户名:aaa 注册码:2GQrt5XHYY7SBK/4b22Gm4Dh8alaR0/0k3gEN5h7FkVPIn8oG3uphlOeytIajxGU 用户名:axureuser 序列号:8wFfI ...
- 自己来实现一个简易的OCR
来做个简易的字符识别 ,既然是简易的 那么我们就不能用任何的第三方库 .啥谷歌的 tesseract-ocr, opencv 之类的 那些玩意是叼 至少图像处理 机器视觉这类课题对我这种高中没毕业的人 ...
- Flexible 弹性盒子模型之CSS flex-grow 属性
实例 让第二个元素的宽度为其他元素的三倍: div:nth-of-type(1){flex-grow:1;} div:nth-of-type(2){flex-grow:3;} div:nth-of-t ...
- BPM始终服务于人,落脚于人
数字经济时代下,云计算.大数据.移动互联已经成为当下企业必须采取的武装力量.随着互联网+.中国制造2025.工业4.0等国家战略的引导与支持,无数的企业在这场数字化浪潮中使尽浑身解数,想要抓住机遇奋力 ...
- Android中访问sdcard路径的几种方式
以前的Android(4.1之前的版本)中,SDcard路径通过"/sdcard"或者"/mnt/sdcard"来表示,而在JellyBean(安卓4.1)系统 ...
- 当 IDENTITY_INSERT 设置为 OFF 时,不能为表 'T_Shell' 中的标识列插入显式值。
--允许将显示值插入表的标识列中-ON:允许 OFF:不允许set identity_insert T_shell ONset identity_insert T_Shell OFF