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

转载请注明出处——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. 十分钟介绍mobx与react

    原文地址:https://mobxjs.github.io/mobx/getting-started.html 写在前面:本人英语水平有限,主要是写给自己看的,若有哪位同学看到了有问题的地方,请为我指 ...

  2. 微软.NET Core RC2正式发布,横跨所有平台

    .NET官方博客宣布了<Announcing .NET Core RC2 and .NET Core SDK Preview 1>,正式如期发布了.NET Core RC2, 现在可以放心 ...

  3. EditText 基本用法

    title: EditText 基本用法 tags: EditText,编辑框,输入框 --- EditText介绍: EditText 在开发中也是经常用到的控件,也是一个比较必要的组件,可以说它是 ...

  4. 初步认识TDD

    TDD,测试驱动开发(Test Driven Development)是极限编程中倡导的程序开发方法,以其倡导先写测试程序,然后编码实现其功能得名.本文将对TDD有一个较为系统的认识.    基础属性 ...

  5. Oracle碎碎念~2

    1. 如何查看表的列名及类型 SQL> select column_name,data_type,data_length from all_tab_columns where owner='SC ...

  6. 利用apply()或者rest参数来实现用数组传递函数参数

    关于call()和apply()的用法,MDN文档里写的非常清晰明白,在这里就不多做记录了. https://developer.mozilla.org/zh-CN/docs/Web/JavaScri ...

  7. [开发笔记] Graph Databases on developing

    TimeWall is a graph databases github It be used to apply mathematic model and social network with gr ...

  8. App 审核由于 IPv6 网络问题被拒

    昨天 提交App Store 的时候被拒了 We discovered one or more bugs in your app when reviewed on iPhone running iOS ...

  9. Android中Activity的四大启动模式实验简述

    作为Android四大组件之一,Activity可以说是最基本也是最常见的组件,它提供了一个显示界面,从而实现与用户的交互,作为初学者,必须熟练掌握.今天我们就来通过实验演示,来帮助大家理解Activ ...

  10. Maven安装

    开发分布式的商场系统,用到了一些新的技术,做一个记录和分享 这里讲一下maven安装 首先什么是Maven Maven作为一个构建工具,不仅帮我们自动化构建,还能抽象构建过程,提供构建任务实现.他跨平 ...