【译】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 作者:毛星云(浅墨) ...
随机推荐
- H5坦克大战之【建造敌人的坦克】
公司这几天在准备新版本的上线,今天才忙里偷闲来写这篇博客.接着上一篇的"H5坦克大战之[玩家控制坦克移动2]"(http://www.cnblogs.com/zhouhuan/ ...
- Git与Repo入门
版本控制 版本控制是什么已不用在说了,就是记录我们对文件.目录或工程等的修改历史,方便查看更改历史,备份以便恢复以前的版本,多人协作... 一.原始版本控制 最原始的版本控制是纯手工的版本控制:修改文 ...
- iOS开源项目周报0105
由OpenDigg 出品的iOS开源项目周报第四期来啦.我们的iOS开源周报集合了OpenDigg一周来新收录的优质的iOS开发方面的开源项目,方便iOS开发人员便捷的找到自己需要的项目工具等. He ...
- requests的content与text导致lxml的解析问题
title: requests的content与text导致lxml的解析问题 date: 2015-04-29 22:49:31 categories: 经验 tags: [Python,lxml, ...
- JS继承之借用构造函数继承和组合继承
根据少一点套路,多一点真诚这个原则,继续学习. 借用构造函数继承 在解决原型中包含引用类型值所带来问题的过程中,开发人员开始使用一种叫做借用构造函数(constructor stealing)的技术( ...
- JavaScript求两个数字之间所有数字的和
这是在fcc上的中级算法中的第一题,拉出来的原因并不是因为有什么好说的,而是我刚看时以为是求两个数字的和, 很显然错了.我感觉自己的文字理解能力被严重鄙视了- -.故拉出来折腾折腾. 要求: 给你一个 ...
- Javascript 严格模式详解
转自http://www.ruanyifeng.com/blog/2013/01/javascript_strict_mode.html 一.概述 除了正常运行模式,ECMAscript 5添加了第二 ...
- Angular2 Hello World 之 2.0
最近angular2正式版发布了,现在就趁热接着记录一下2.0版的Hello World.据说由RC6升级到2.0代码改动不是很大,在写的时候也感觉改动不是很大,这次记录还是以asp.net core ...
- Android 算法 关于递归和二分法的小算法
// 1. 实现一个函数,在一个有序整型数组中二分查找出指定的值,找到则返回该值的位置,找不到返回 -1. package demo; public class Mytest { public st ...
- Missing Push Notification Entitlement 问题
最近打包上传是遇到一个问题: 描述: Missing Push Notification Entitlement - Your app includes an API for Apple's Push ...