【译】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 作者:毛星云(浅墨) ...
随机推荐
- Syscan360会议胸牌破解揭秘
Syscan360会议胸牌破解揭秘 背景 有幸参加今年11月份的上海Syscan360安全会议,会议期间有一个亮点就是360的独角兽团队设计了一款电子badge(胸牌)供参加人员进行破解尝试,类似于美 ...
- 在Ubuntu 16.10安装mysql workbench报未安装软件包 libpng12-0错误
1.安装mysql workbench,提示未安装软件包 libpng12-0 下载了MySQL Workbench 6.3.8 在安装的时候报错: -1ubu1604-amd64.deb 提示: ...
- 旺财速啃H5框架之Bootstrap(一)
接下来的时间里,我将和大家一起对当前非常流行的前端框架Bootstrap进行速度的学习,以案例的形式.对刚开始想学习Bootstrap的同学而找不着边的就很有帮助了.如果你想详细的学习Bootstra ...
- jQuery2.x源码解析(缓存篇)
jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 缓存是jQuery中的又一核心设计,jQuery ...
- 体验报告:微信小程序在安卓机和苹果机上的区别
很多人可能会问:微信小程序和在微信里面浏览一个网页有什么区别? 首先,小程序的运行是全屏的,界面跟进入了一个APP很像,更为沉浸跟在微信里面访问h5不一样:其次,它的浏览体验更为稳定. 不过,这还不够 ...
- SAP CRM 显示消息/在消息中进行导航
向用户展示消息,在任何软件中都是十分重要的. 在SAP CRM WEB UI中展示消息,不是一项很难的任务,只需要创建消息并在之后调用方法来显示它 消息类和消息号: 我在SE91中创建了如下的消息类和 ...
- 15个C++项目列表
实验楼上有很多C++的实战项目,从简单到进阶,学习每个项目都可以掌握相应的知识点. 如果你还是C++新手的话,那么这个C++的项目列表你可以拿去练手实战开发,毕竟学编程动手实践是少不了的! 如果你不知 ...
- Ubuntu下配置apache开启https
一.HTTPS简述随着网络的日常,信息安全越来越重要,传统的网站都是http协议明文传输,而HTTPS协议是由SSL+HTTP协议构建的可进行加密传输.身份认证的网络协议,比http协议安全. 那ht ...
- BZOJ 4196: [Noi2015]软件包管理器 [树链剖分 DFS序]
4196: [Noi2015]软件包管理器 Time Limit: 10 Sec Memory Limit: 512 MBSubmit: 1352 Solved: 780[Submit][Stat ...
- Merge Sorted Array
Given two sorted integer arrays nums1 and nums2, merge nums2 into nums1 as one sorted array. Note:Yo ...