【译】Unity3D Shader 新手教程(6/6) —— 更好的卡通Shader (转)

转自:http://www.cnblogs.com/polobymulberry/p/4395127.html

动机

如果你想了解以下几件事,我建议你阅读以下这篇教程:

想知道如何写一个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{

CullFront

LightingOff

}

现在让我们考虑最简单的部分 — 将传入该Pass的所有像素值绘制成黑色!

CGPROGRAM

#pragmavertex vert

#pragmafragment frag

#include"UnityCG.cginc"

//剩下的功能在此处实现

float4 frag(v2f IN):COLOR

{

returnfloat4(0,0,0,1);

}

ENDCG

该fragment函数返回float4(0,0,0,1) — 全黑。

现在为我们的shader添加输入结构体。我们利用该结构体(包含vertex和normal)来将我们模型的每个顶点沿法向进行延伸扩展 — 该顶点是背面面片上的点。所以我们输入结构体必须含有顶点位置vertex和顶点法向normal信息。

structa2v

{

float4 vertex:POSITION;

float3 normal:NORMAL;

};

structv2f

{

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));

returno;

}

我们所做的就是将v.vertex沿着normal伸展了_Outline比例大小,然后使用Unity内置的矩阵UNITY_MATRIX_MVP将结果转换到投影空间(projection space)。

矩阵在shader中用来转化很多事情。我们可以从下图看出,一个4x4的矩阵乘上一个4x1的矩阵,得到还是一个4*1的矩阵。Unity中有很多预定好的矩阵,我们可以使用这些矩阵得到各种空间坐标系的转换。

目前你的代码应该保证像下面这样了(注意这是在第五部分教程的基础上添加的代码):

Pass{

//剔除模型正面,只渲染背面

CullFront

LightingOff

CGPROGRAM

#pragmavertex vert

#pragmafragment frag

#include"UnityCG.cginc"

structa2v

{

float4 vertex:POSITION;

float3 normal:NORMAL;

};

structv2f

{

float4 pos:POSITION;

}

float_Outline;

v2f vert(a2v v)

{

v2f o;

o.pos=mul(UNITY_MATRIX_MVP,v.vertex+(float4(v.normal,0)*_Outline));

returno;

}

floatfrag(v2f IN):COLOR

{

returnfloat4(0,0,0,1)

}

ENDCG

}

看上去好像有点效果,但是仔细看他的嘴巴,我们可以看到是有很大问题。这是因为实现边缘效果的Pass是可以写入深度缓存的。所以在有些情况下,模型正面是无法正常绘制的。

拿此处的嘴举例,此处的嘴巴的上嘴唇是属于正面的,而下嘴唇是反面(多边形方向为逆时针)。所以Cull Front后会剔除上嘴唇,保留下嘴唇。而下嘴唇的法向很明显差不多是朝上的,所以在vert函数中会在下嘴唇上方产生这种黑条状的面片。又因为我们是可以写入深度缓存的,所以会将这黑色面片写入到深度缓存,而这黑色面片恰好在嘴唇前面,所以嘴唇正面在绘制时通过不了深度测试,只留下这黑色的面片。

自然而然地我们肯定能想到,让这个黑色面片不进行深度缓存测试不就行了。下面这幅图就是在该Pass中关闭Z buffer测试的结果。

使用下面这段代码:

Pass{

CullFront

LightingOff

ZWriteOff

关闭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);

returno;

}

注意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;

//计算出光源距离

floatlengthSq=dot(i.lightDirection,i.lightDirection);

//根据计算出的光源位置计算光强的衰减

floatatten=1.0/(1.0+lengthSq);

//光的入射角

floatdiff=saturate(dot(n,normalize(i.lightDirection)));

//利用渐变纹理

diff=tex2D(_Ramp,float2(diff,0.5));

//根据入射角,光衰减得到最终光照亮度

lightColor+=_LightColor0.rgb*(diff*atten);

//将光照亮度与本身颜色相乘,得到最终颜色

c.rgb=lightColor*c.rgb*2;

returnc;

}

我们所要做的就是利用_MainTex纹理进行采样,然后降低颜色种类,最后使用渐变纹理获得的数值作为光强。

下图使我们最终的效果:

完整的源码在这里

对于其他光照的ForwardAdd部分,就留给你们自己写吧!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容