Unity3D Shader入门指南(二)

Unity Shader教程

关于本系列

这是Unity3D Shader入门指南系列的第二篇,本系列面向的对象是新接触Shader开发的Unity3D使用者,由于我自己本身也是Shader初学者,所以可能会存在错误或者疏漏,若是您在Shader开发上有所心得,很欢迎并恳请您指出文中纰漏,我会尽快改正。在以前的开篇中介绍了一些Shader的基本知识,包括ShaderLab的基本结构和语法,以及简单逐句地讲解了一个基本的shader。在具备这些基础知识后,阅读简单的shader应该不会有太大问题,在继续教程以前简单阅读一下Unity的Surface Shader Example,以检验您是否掌握了上一节的内容。若是您对阅读大部分示例Shader并无太大问题,能够正确地指出Shader的结构,声明和使用的话,就说明您已经准备好继续阅读本节的内容了。html

法线贴图(Normal Mapping)

法线贴图是凸凹贴图(Bump mapping)的一种常见应用,简单说就是在不增长模型多边形数量的前提下,经过渲染暗部和亮部的不一样颜色深度,来为原来的贴图和模型增长视觉细节和真实效果。简单原理是在普通的贴图的基础上,再另外提供一张对应原来贴图的,能够表示渲染浓淡的贴图。经过将这张附加的表示表面凸凹的贴图的因素于实际的原贴图进行运算后,能够获得新的细节更加丰富富有立体感的渲染效果。在本节中,咱们将首先实现一个法线贴图的Shader,而后对Unity Shader的光照模型进行一些讨论,并实现一个自定义的光照模型。最后再经过更改shader模拟一个石头上的积雪效果,并对模型顶点进行一些修改使积雪效果看起来比较真实。在本节结束的时候,咱们就会有一个比较强大的能够知足一些真实开发工做时可用的shader了,并且更重要的是,咱们将会掌握它是如何被创造出来的。git

关于法线贴图的效果图,能够对比看看下面。模型面数为500,左侧只使用了简单的Diffuse着色,右侧使用了法线贴图。比较两张图片不难发现,使用了法线贴图的石头在暗部和亮部都有着更好的表现。总体来讲,凸凹感比Diffuse的结果加强许多,石头看起来更真实也更具备质感。github

image

本节中须要用到的上面的素材能够在这里下载,其中包括上面的石块的模型,一张贴图以及对应的法线贴图。将下载的package导入到工程中,并新建一个material,使用简单的Diffuse的Shader(好比上一节咱们实现的),再加上一个合适的平行光光源,就能够获得咱们左图的效果。另外,本节以及之后都会涉及到一些Unity内建的Shader的内容,好比一些标准经常使用函数和常量定义等,相关内容能够在Unity的内建Shader中找到,内建Shader能够在Unity下载页面的版本右侧找到。bash

接下来咱们实现法线贴图。在实现以前,咱们先简单地稍微多了解一些法线贴图的基本知识。大多数法线图通常都和下面的图相似,是一张以蓝紫色为主的图。这张法线图实际上是一张RGB贴图,其中红,绿,蓝三个通道分别表示由高度图转换而来的该点的法线指向:Nx、Ny、Nz。在其中绝大部分点的法线都指向z方向,所以图更偏向于蓝色。在shader进行处理时,咱们将光照与该点的法线值进行点积后便可获得在该光线下应有的明暗特性,再将其应用到原图上,便可反应在必定光照环境下物体的凹凸关系了。关于法向贴图的更多信息,能够参考wiki上的相关条目app

一张典型的法线图

回到正题,咱们如今考虑的主要是Shader入门,而不是图像学的原理。再上一节咱们写的Shader的基础上稍微作一些修改,就能够获得适应并完成法线贴图渲染的新Shader。新加入的部分进行了编号并在以后进行说明。函数

Shader "Custom/Normal Mapping" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} //1 _Bump ("Bump", 2D) = "bump" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Lambert sampler2D _MainTex; //2 sampler2D _Bump; struct Input { float2 uv_MainTex; //3 float2 uv_Bump; }; void surf (Input IN, inout SurfaceOutput o) { half4 c = tex2D (_MainTex, IN.uv_MainTex); //4 o.Normal = UnpackNormal(tex2D(_Bump, IN.uv_Bump); o.Albedo = c.rgb; o.Alpha = c.a; } ENDCG } FallBack "Diffuse" } 
  1. 声明并加入一个显示名称为Bump的贴图,用于放置法线图
  2. 为了可以在CG程序中使用这张贴图,必须加入一个sample,但愿你还记得~
  3. 获取Bump的uv信息做为输入
  4. 从法线图中提取法线信息,并将其赋予相应点的输出的Normal属性。UnpackNormal是定义在UnityCG.cginc文件中的方法,这个文件中包含了一系列经常使用的CG变量以及方法。UnpackNormal接受一个fixed4的输入,并将其转换为所对应的法线值(fixed3)。在解包获得这个值以后,将其赋给输出的Normal,就能够参与到光线运算中完成接下来的渲染工做了。

如今保存而且编译这个Shader,建立新的material并使用这个shader,将石头的材质贴图和法线图分别拖放到Base和Bump里,再将其应用到石头模型上,应该就能够看到右侧图的效果了。post

光照模型

在咱们以前的看到的Shader中(其实也就上一节的基本diffuse和这里的normal mapping),都只使用了Lambert的光照模型(#pragma surface surf Lambert),这是一个很经典的漫反射模型,光强与入射光的方向和反射点处表面法向夹角的余弦成正比。关于Lambert和漫反射的一些详细的计算和推论,能够参看wiki(Lambert漫反射)或者其余地方的介绍。一句话的简单解释就是一个点的反射光强是和该点的法线向量和入射光向量和强度和夹角有关系的,其结果就是这两个向量的点积。既然已经知道了光照计算的原理,咱们先来看看如何实现一个本身的光照模型吧。spa

在刚才的Shader上进行以下修改。设计

  • 首先将原来的#pragma行改成这样
#pragma surface surf CustomDiffuse 
  • 而后在SubShader块中添加以下代码
inline float4 LightingCustomDiffuse (SurfaceOutput s, fixed3 lightDir, fixed atten) { float difLight = max(0, dot (s.Normal, lightDir)); float4 col; col.rgb = s.Albedo * _LightColor0.rgb * (difLight * atten * 2); col.a = s.Alpha; return col; } 
  • 最后保存,回到Unity。Shader将编译,若是一切正常,你将不会看到新的shader和以前的在材质表现上有任何不一样。可是事实上咱们如今的shader已经与Unity内建的diffuse光照模型撇清了关系,而在使用咱们本身设定的光照模型了。

喵的,这些代码都干了些什么!相信你必定会有这样的疑惑...没问题,没有疑惑的话那就不叫初学了,仍是一行行讲来。首先正像咱们上一篇所说,#pragma语句在这里声明了接下来的Shader的类型,计算调用的方法名,以及指定光照模型。在以前咱们一直指定Lambert为光照模型,而如今咱们将其换为了CustomDiffuse。3d

接下来添加的代码是计算光照的实现。shader中对于方法的名称有着比较严格的约定,想要建立一个光照模型,首先要作的是按照规则声明一个光照计算的函数名字,即Lighting<Your Chosen Name>。对于咱们的光照模型CustomDiffuse,其计算函数的名称天然就是LightingCustomDiffuse了。光照模型的计算是在surf方法的表面颜色以后,根据输入的光照条件来对原来的颜色在这种光照下的表现进行计算,最后输出新的颜色值给渲染单元完成在屏幕的绘制。

也许你已经猜到了,咱们以前用的Lambert光照模型是否是也有一个名字叫LightingLambert的光照计算函数呢?Bingo。在Unity的内建Shader中,有一个Lighting.cginc文件,里面就包含了LightingLambert的实现。也许你也注意到了,咱们所实现的LightingCustomDiffuse的内容如今和Unity内建中的LightingLambert是彻底同样的,这也就是使用新的shader的原来视觉上没有区别的缘由,由于实现确实是彻底同样的。

首先来看输入量,SurfaceOutput s这个就是通过表面计算函数surf处理后的输出,咱们讲对其上的点根据光线进行处理,fixed3 lightDir是光线的方向,fixed atten表示光衰减的系数。在计算光照的代码中,咱们先将输入的s的法线值(在Normal mapping中的话这个值已是法线图中的对应量了)和输入光线进行点积(dot函数是CG中内置的数学函数,但愿你还记得,能够参考这里)。点积的结果在-1至1之间,这个值越大表示法线与光线间夹角越小,这个点也就应该越亮。以后使用max来将这个系数结果限制在0到1之间,是为了不负数状况的存在而致使最终计算的颜色变为负数,输出一团黑,通常来讲这是咱们不肯意看到的。接下来咱们将surf输出的颜色与光线的颜色_LightColor0.rgb(由Unity根据场景中的光源获得的,它在Lighting.cginc中有声明)进行乘积,而后再与刚才计算的光强系数和输入的衰减系数相乘,最后获得在这个光线下的颜色输出(关于difLight * atten * 2中为何有个乘2,这是一个历史遗留问题,主要是为了进行一些光强补偿,能够参见这里的讨论)。

在了解了基本实现方式以后,咱们能够看看作一些修改玩玩儿。最简单的好比将这个Lambert模型改亮一些,好比换成Half Lambert模型。Half Lambert是由Valve创造的可使物体在低光线条件下增亮的技术,最先被用于半条命(Half Life)中以免在低光下物体的走形。简单说就是把光强系数先取一半,而后在加0.5,代码以下:

inline float4 LightingCustomDiffuse (SurfaceOutput s, fixed3 lightDir, fixed atten) { float difLight = dot (s.Normal, lightDir); float hLambert = difLight * 0.5 + 0.5; float4 col; col.rgb = s.Albedo * _LightColor0.rgb * (hLambert * atten * 2); col.a = s.Alpha; return col; } 

这样一来,原来光强0的点,如今对应的值变为了0.5,而原来是1的地方如今将保持为1。也就是说模型贴图的暗部被加强变亮了,而亮部基本保持和原来同样,防止过曝。使用Half Lambert先后的效果图以下,注意最右侧石头下方的阴影处细节更加明显了,而这一切都只是视觉效果的改变,不涉及任何贴图和模型的变化。

Half Lambert下发现贴图的表现

表面贴图的追加效果

OK,对于光线和自定义光照模型的讨论暂时到此为止,由于若是展开的话这将会一个庞大的图形学和经典光学的话题了。咱们回到Shader,而且一块儿实现一些激动人心的效果吧。好比,在你的游戏场景中有一幕是雪地场景,而你但愿作一些石头上白雪皑皑的覆盖效果,应该怎么办呢?难道让你可爱的3D设计师再去出一套覆雪的贴图而后使用新的贴图?固然不,不是不能,而是不应。由于新的贴图不只会增大项目的资源包体积,更会增大以后修改和维护的难度,想一想要是有好多石头须要实现一样的覆雪效果,或者是要随着游戏时间堆积的雪逐渐变多的话,你应该怎么办?难道让设计师再把全部的石头贴图都盖上雪,而后再按照雪的厚度出5套不一样的贴图么?相信我,他们会疯的。

因而,咱们考虑用Shader来完成这件工做吧!先考虑下咱们须要什么,积雪效果的话,咱们须要积雪等级(用来表示积雪量),雪的颜色,以及积雪的方向。基本思路和实现自定义光照模型相似,经过计算原图的点在世界坐标中的法线方向与积雪方向的点积,若是大于设定的积雪等级的阈值的话则表示这个方向与积雪方向是一致的,其上是能够积雪的,显示雪的颜色,不然使用原贴图的颜色。废话再也不多说,上代码,在上面的Shader的基础上,更改Properties里的内容为

Properties {  
    _MainTex ("Base (RGB)", 2D) = "white" {} _Bump ("Bump", 2D) = "bump" {} _Snow ("Snow Level", Range(0,1) ) = 0 _SnowColor ("Snow Color", Color) = (1.0,1.0,1.0,1.0) _SnowDirection ("Snow Direction", Vector) = (0,1,0) } 

没有太多值得说的,惟一要提一下的是_SnowDirection设定的默认值为(0,1,0),这表示咱们但愿雪是垂直落下的。对应地,在CG程序中对这些变量进行声明:

sampler2D _MainTex;  
sampler2D _Bump;  
float _Snow; float4 _SnowColor; float4 _SnowDirection; 

接下来改变Input的内容:

struct Input { float2 uv_MainTex; float2 uv_Bump; float3 worldNormal; INTERNAL_DATA }; 

相对于上面的Shader输入来讲,加入了一个float3 worldNormal; INTERNAL_DATA,若是SurfaceOutput中设定了Normal值的话,经过worldNormal能够获取当前点在世界中的法线值。详细的解说能够参见Unity的Shader文档。接下来能够改变surf函数,实装积雪效果了。

void surf (Input IN, inout SurfaceOutput o) { half4 c = tex2D (_MainTex, IN.uv_MainTex); o.Normal = UnpackNormal(tex2D(_Bump, IN.uv_Bump)); if (dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz) > lerp(1,-1,_Snow)) { o.Albedo = _SnowColor.rgb; } else { o.Albedo = c.rgb; } o.Alpha = c.a; } 

和上面相比,加入了一个if…else…的判断。首先看这个条件的不等式的左侧,咱们对雪的方向和和输入点的世界法线方向进行点积。WorldNormalVector经过输入的点及这个点的法线值,来计算它在世界坐标中的方向;右侧的lerp函数相信只要对插值有概念的同窗都不难理解:当Snow取最小值0时,这个函数将返回1,而Snow取最大值时,返回-1。这样咱们就能够经过设定Snow的值来控制积雪的阈值,要是积雪等级Snow是0时,不等式左侧不可能大于右侧,所以彻底没有积雪;相反要是_Snow取最大值1时,因为左侧一定大于-1,因此全模型积雪。而随着取中间值的变化,积雪的状况便会有所不一样。

应用这个Shader,而且适当地调节一下积雪等级和颜色,能够获得以下右边的效果。

添加了积雪效果的Shader

更改顶点模型

到如今位置,咱们还仅指是在原贴图上进行操做,无论是用法线图使模型看起来凸凹有致,仍是加上积雪,全部的计算和颜色的输出都只是“障眼法”,并无对模型有任何实质的改动。可是对于积雪效果来讲,实际上积雪是附加到石头上面,而不该当简单替换掉原来的颜色。可是具体实施起来,最简单的办法仍是直接替换颜色,可是咱们能够稍微变动一下模型,使原来的模型在积雪的方向稍微变大一些,这样来达到一种雪是附加到石头上的效果。

咱们继续修改以前的Shader,首先咱们须要告诉surface shadow咱们要改变模型的顶点。首先将#param行改成

#pragma surface surf CustomDiffuse vertex:vert

这告诉Shader咱们想要改变模型顶点,而且咱们会写一个叫作vert的函数来改变顶点。接下来咱们再添加一个参数,在Properties中声明一个_SnowDepth变量,表示积雪的厚度,固然咱们也须要在CG段中进行声明:

//In Properties{…} _SnowDepth ("Snow Depth", Range(0,0.3)) = 0.1 //In CG declare float _SnowDepth; 

接下来实现vert方法,和以前积雪的运算其实比较相似,判断点积大小来决定是否须要扩大模型以及肯定模型扩大的方向。在CG段中加入如下vert方法

void vert (inout appdata_full v) { float4 sn = mul(transpose(_Object2World) , _SnowDirection); if(dot(v.normal, sn.xyz) >= lerp(1,-1, (_Snow * 2) / 3)) { v.vertex.xyz += (sn.xyz + v.normal) * _SnowDepth * _Snow; } } 

和surf的原理差很少,系统会输入一个当前的顶点的值,咱们根据须要计算并填上新的值做为返回便可。上面第一行中使用transpose方法输出原矩阵的转置矩阵,在这里_Object2World是Unity ShaderLab的内建值,它表示将当前模型转换到世界坐标中的矩阵,将其与积雪方向作矩阵乘积获得积雪方向在物体的世界空间中的投影(把积雪方向转换到世界坐标中)。以后咱们计算了这个世界坐标中实际的积雪方向和当前点的法线值的点积,并将结果与使用积雪等级的2/3进行比较lerp后的阈值比较。这样,当前点若是和积雪方向一致,而且积雪较为完整的话,将改变该点的模型顶点高度。

加入模型更改先后的效果对好比下图,加入模型调整的右图表现要更为丰满真实。

image

这节就到这里吧。本节中实现的Shader能够在这里找到完整版本进行参考,但愿你们周末愉快~

原文:http://onevcat.com/2013/08/shader-tutorial-2/

相关文章
相关标签/搜索