本文为翻译,附上原文链接。

转载请注明出处——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的更多相关文章

  1. 【译】Unity3D Shader 新手教程(2/6) —— 积雪Shader

    本文为翻译,附上原文链接. 转载请注明出处--polobymulberry-博客园. 如果你是一个shader编程的新手,并且你想学到下面这些酷炫的技术,我觉得你可以看看这篇教程: 实现一个积雪效果的 ...

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

    本文为翻译,附上原文链接. 转载请注明出处--polobymulberry-博客园. 刚开始接触Unity3D Shader编程时,你会发现有关shader的文档相当散,这也造成初学者对Unity3D ...

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

    本文为翻译,附上原文链接. 转载请注明出处--polobymulberry-博客园. 暗黑系 动机 如果你满足以下条件,我建议你阅读这篇教程: 你想了解更多有关表面着色器的细节知识. 你想实现一个入门 ...

  4. 【译】Unity3D Shader 新手教程(3/6) —— 更加真实的积雪

    本文为翻译,附上原文链接. 转载请注明出处--polobymulberry-博客园. 如果你满足以下条件,我建议你阅读这篇教程: 你想知道如何在表面着色器中进行混色(blend colour) 你想实 ...

  5. 【译】Unity3D Shader 新手教程(5/6) —— Bumped Diffuse Shader

    本文为翻译,附上原文链接. 转载请注明出处--polobymulberry-博客园. 动机 如果你满足以下条件,我建议你阅读这篇教程: 你想学习片段着色器(Fragment Shader). 你想实现 ...

  6. 【译】Meteor 新手教程:在排行榜上添加新特性

    原文:http://danneu.com/posts/6-meteor-tutorial-for-fellow-noobs-adding-features-to-the-leaderboard-dem ...

  7. Unity3D Shader官方教程翻译(十九)----Shader语法,编写表面着色器

    Writing Surface Shaders Writing shaders that interact with lighting is complex. There are different ...

  8. ionic新手教程第八课-(加更)从无到有说Ionic、绘图说明MVC-U-S

    这节课的内容,有些前面几节已经说过了. 公司这次给我一个任务,让我带一个没有编程基础的同事学习ionic. 今天是我跟他讲的第一课,晚上把讲的笔记整理了一下,认为还是挺适合零基础的朋友学习的. 有些前 ...

  9. 【OpenCV新手教程之十一】 形态学图像处理(二):开运算、闭运算、形态学梯度、顶帽、黑帽合辑

    本系列文章由@浅墨_毛星云 出品,转载请注明出处. 文章链接:http://blog.csdn.net/poem_qianmo/article/details/23184547 作者:毛星云(浅墨) ...

随机推荐

  1. Python学习--04条件控制与循环结构

    Python学习--04条件控制与循环结构 条件控制 在Python程序中,用if语句实现条件控制. 语法格式: if <条件判断1>: <执行1> elif <条件判断 ...

  2. 高大上的微服务可以很简单,使用node写微服务

    安装 npm install m-service --save 使用 编写服务处理函数 // dir1/file1.js // 使用传入的console参数输出可以自动在日志里带上request id ...

  3. HTML kbd键盘元素

    1. 说明 kbd :即Keyboard Input Element(键盘输入元素).表示键盘按键的语义元素,常用于网页上对快捷键.按键说明的场景. 样式规格:内联样式. 为了在页面上突出显示,可以给 ...

  4. WPF 微信 MVVM

    公司的同事离职了,接下来的日子可能会忙碌,能完善DEMO的时间也会少了,因此,把做的简易DEMO整体先记录一下,等后续不断的完善. 参考两位大神的日志:WEB版微信协议部分功能分析.[完全开源]微信客 ...

  5. ASP.NET MVC5+EF6+EasyUI 后台管理系统(69)-微信公众平台开发-功能概述

    系列目录 为什么要先发这个文章? 因为接下来的文章是关于微信开发的系列,心中一定要有一个概念,知道自己接下来要做什么功能. 而且微信到处都是坑,我首先要把微信与本地跑通起来才敢发布,否则中间出现坑导致 ...

  6. Angular企业级开发(3)-Angular MVC实现

    1.MVC介绍 Model-View-Controller 在20世纪80年代为程序语言Smalltalk发明的一种软件架构.MVC模式的目的是实现一种动态的程序设计,使后续对程序的修改和扩展简化,并 ...

  7. 微信小程序前端源码逻辑和工作流

    看完微信小程序的前端代码真的让我热血沸腾啊,代码逻辑和设计一目了然,没有多余的东西,真的是大道至简. 废话不多说,直接分析前端代码.个人观点,难免有疏漏,仅供参考. 文件基本结构: 先看入口app.j ...

  8. μCos-ii学习笔记1_概述

    一.μCos-ii _概述 网上关于μCosii的文章多不胜数,本人学习的过程中也参考了很多人的理解和想法,看的是卢有亮老师的<嵌入式实时操作系统-μC/OS原理与实践>(第2版),同时也 ...

  9. js月份,日期加一天

    js没有直接可以用的函数,所以只能自己写,其中需要涉及到每个月天数的判断,如果是2月份的话,还要涉及到闰年的判断 var addDate = { //日期,在原有日期基础上,增加days天数,默认增加 ...

  10. 探索 Linux 系统的启动过程

    引言 之所以想到写这些东西,那是因为我确实想让大家也和我一样,把 Linux 桌面系统打造成真真正正日常使用的工具,而不是安装之后试用几把再删掉.我是真的在日常生活和工作中都使用 Linux,比如在 ...