1.了解UnityShader
在Unity,当我们create一个game object,然后我们使用组件附加其它功能。实际上,每个游戏对象都需要一个Transform组件;Unity中已经包含了许多组件,当我们编写从MonoBehaviour扩展的脚本时,我们创建了自己的组件。游戏中的所有对象都包含许多影响其外观和行为的组件。尽管脚本决定了对象的行为方式,而渲染器决定对象上的显示方式。Unity附带了几个渲染器,具体取决于我们尝试可视化的对象类型;每个3D模型都有一个MeshRenderer组件。一个对象应该只有一个渲染器,但渲染器本身可以包含多个材质。每一种材质都是一个单独shader的包装,是3D图形食物链中的最终环。这些组件之间的关系可在下图中看到。
理解这些组件之间的区别对于理解shader的工作方式是至关重要的。
a.在Project窗口中点击Create在下列菜单中选择shader,并双击打开它,现在让我们给shader提供一个自定义文件夹,shader中第一行代码是我们必须为shader提供的自定义描述,以便Unity在分配到材质时可以在shader下拉列表中使其适用。这里我将它重命名为CookbookShaders/StandardDiffuse,当然我们可以随时随意重命名,所以在这一点上我们不用担心任何依赖关系。保存刚刚的改变,然后回到Unity编辑器,当Unity意识到文件已经更新的时候它会自动编译shader,
Shader "Custom/StandardDiffuse"
b.从技术上来讲,这是一个基于物理渲染(PBR)的Surface Shader。顾名思义,这种shader通过模拟物体撞击物体时的物理行为来实现真实感。
c.shader创建好后,我们将它挂在Material上。这一步让材质准备好分配给一个对象。
Unity已经完成了让你的shader环境正常运行的任务,只需要点击几下,我们就可以轻松上手。关于Surface Shader本身的背后有很多元素在运行。Unity采用了Cg着色器语言,通过为我们提供大量繁重的Cg代码,使编写更高效。Surface Shader语言是一种更基于组件的编写着色器的方法。已经为我们完成了处理自己的纹理坐标和转换矩阵等任务,所以我们不必再从头开始了。在过去,我们必须开始一个新的Shader,并一遍又一遍地重写大量地代码,当你获得更多地Surface Shader经验,你自然会想要探索更多Cg语言地底层功能,以及Unity是如何为你处理所有低级图形处理单元(GPU)任务的。
注意:Unity项目中的所有文件都是独立于它们所在的文件夹引用的。我们可以在编辑器中移动着色器和材质,而不会有破环任何连结的危险。但是,永远不应该从编辑器外部移动文件,因为Unity将无法更新其引用。
内置着色器的源代码通常隐藏在Unity中。您无法像使用自己的着色器一样从编辑器中打开它。有关在何处查找Unity的大量内置Cg函数的更多信息,请转到Unity安装目录并导航到 Editor | Data | CGIncludes 文件夹。
这里有三个文件值得注意:UnityCG.cginc,Lighting.cginc, UnityShaderVariables.cginc。我们当前的着色器正在利用所有这些文件。在以后的高级着色技术,我们将深入探讨如何使用CGInclude为一个模块化的方法着色编码。
2.向shader添加属性
shader的属性对shader管道非常重要,通过属性我们可以在材质的Inspector 面板显示GUI元素,而无需使用单独的编辑器,这里提供了调整shader的可视方式。通过VS打开之前创建的StandardDiffuse Shader后,查看第二行到第七行,这称为脚本的属性块。目前,它将有一个名为——MainTex的纹理属性。
如果我们的材质使用了这个shader,我们会注意到在Inspector选项卡里有有一个纹理GUI元素。shader中的这些代码为我们创建了这个GUI元素。
本来打算把Standard surface shader介绍一遍,写着写着就没有耐性了, 这些比较简单,不再赘述了。
下面直接步入正题
3.Surface Shader的工作原理
一般来说,每个Surface Shader都有两个基本步骤。首先,我们必须指定要描述地材质地某些物理属性,例如其漫反射颜色,平滑度和透明度。这些属性在称为surface function的函数中初始化,并存储在名为SurfaceOutput的结构中。然后,SurfaceOutput传递给照明模型。这是一个特殊函数,它还将获取有关场景中附近灯光的信息。然后使用这两个参数计算模型中每个像素的最终颜色。光照函数是一个着色器的实际计算发生的地方,因为它是决定光接触材料时应该如何表现得代码。
下图大致地概括了Surface Shader得工作原理。
4.Diffuse shading
在我们开始学习纹理贴图前,了解漫反射材质如何工作是至关重要的。某些物体可能具备均匀的颜色和光滑的表面,但在反射光下又不够光滑,这些哑光材质最好用漫反射Shader表示,然而,在现实世界中并不存在纯粹的漫反射材质,漫反射Shader的实现成本相对较低,并且主要应用于低多边形美学的游戏中,因此它们值得学习。
a.创建一个Standrad Surface Shader并命名为SimpleDiffuse,双击打开脚本,在Properties部分,移走除_Color的其他部分。
Properties { _Color ("Color", Color) = (1,1,1,1) }
b.在SubShader{}部分,移走_MainTex,_Glossiness,和_Metallic变量,我们不必删除对uv_MainTex的引用,因为Cg不允许Input结构为空。该值将被忽略
c.另外,删除UNITY_INSTANCING_BUFFER_START/END宏以及它们使用的注释。
d. 移走surf()函数的内容并写入如下代码
void surf (Input IN, inout SurfaceOutputStandard o) {
o.Albedo = _Color.rgb;
}
e.这个时候你的代码应该如下所示
Shader "Custom/StandardDiffuse" {
Properties {
_Color ("Color", Color) = (1,1,1,1)
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
// Physically based Standard lighting model, and enable shadows on all light types
#pragma surface surf Standard fullforwardshadows
// Use shader model 3.0 target, to get nicer looking lighting
#pragma target 3.0
struct Input {
float2 uv_MainTex;
};
fixed4 _Color;
void surf (Input IN, inout SurfaceOutputStandard o) {
o.Albedo = _Color.rgb;
}
ENDCG
}
FallBack "Diffuse"
}
f.由于这个Shader已使用标准Shader进行重新设置,因此它将使用基于物理的渲染来模拟光线在模型上的行为方式。
如果你尝试实现非真实照片的外观,可以更改第一个#pragma指令,使其使用Lambert而不是Standard,如果这样做,还应该使用SurfaceOutput替换 surf函数的SurfaceOutputStandrad参数。 有关这个以及Unity支持的其他照明模型的更多信息,Jordan Stevens汇总了一篇很好的文章。
下面是链接http://www.jordanstevenstechart.com/lighting-models
这里简单介绍一下
Lambert照明模型与视图无关,这意味着表面的表观亮度不应基于视点而改变。它是一个非常简单的模型,可以用伪代码构造如下:
float3 SurfaceColor; //objects color
float3 LightColor; //lights color * intensity
float LightAttenuation;//vallue of light at point(shadow/falloff)
float NdotL = max(0.0, dot(normalDireation, lightDireation));
float LamertDiffuse = NdotL * SurfaceColor;
float3 finalColor = LambertDiffuse * LightAttenuation * LightColor;
g.下面的操作就是将shader放到材质上并将材质附属于物体,效果图如下所示,为了不干扰效果,我把skybox去掉了,去掉方法同样是在Unity左上角Window|Lighting|Setting,在里面的Environment菜单栏里Skybox Material选为None
作用
Shader是通过允许我们通过SurfaceOutput将材质的渲染属性传递给其光照模型,它基本上是当前光照模型的所需的所有参数的包装器,不同的光照模型具有不同的SurfaceOutput结构,下表显示Unity中使用的三个主要输出结构以及如何使用它们
SurfaceOutput 结构有以下属性
fixed3 Albedo;:这是材质的漫反射颜色
fixed3 Normal;:这是切线空间,法向量
fixed3 Emission;:这是材质发出光的颜色(此属性在Standrad Shaders中声明为half3)
fixed Alpha;:这是材质的透明度
half Specular;:这是从0到1的镜面反射率
fixed Gloss;:这是镜面反射强度。
SurfaceOutputStandrad 结构有以下属性
fixed3 Albedo;:这是材质的基本颜色(无论它是漫反射还是镜面反射)
fixed3 Normal;
half3 Emission;
fixed Alpha;
half Occlusion;:这是遮挡(默认为1)
half Smoothness;:这是光滑度,0等于粗糙,1等于光滑
half Metallic;:这是金属性,0是非金属,1是金属。
SurfaceOutputStandradSpecular 结构有以下属性
fixed3 Albedo;
fixed3 Normal;
half3 Emission;
fixed Alpha;
half Occlusion;
half Smoothness;
fixed3 Specular;:这是镜面反射率。这与SurfaceOutput中的Specular属性非常不同,因为它允许您指定颜色而不是单个值。
使用正确的值初始化SurfaceOutput是正确的使用Surface Shader重要的一步。
访问和修改打包数组
简单来说,着色器中的代码必须至少为屏幕中的每个像素执行,这就是GPU要高度优化并行计算的原因。在Cg中可用的变量和操作符的标准类型中,这种理念也很明显。理解他们很重要的,不仅可以正确地使用着色器,还可以编写高度优化地着色器。
作用
Cg中有两种类型地变量:单个值和打包数组。打包数组能够被识别,是因为它们地类型以float3或int4这样的数字结束。正如他们的名字所暗示的那样,这些类型的变量类似于结构,这意味着它们每个都包含几个单独的值,Cg称它们为打包数组,尽管它们不是传统意义上的数组。
打包数组的元素可以作为普通结构访问,它们通常称为x,y,z和w。但是,Cg还为它们提供了另一个别名,即r,g,b和a。着色器编码实际上经常涉及到位置和颜色的计算。你可能在标准着色器中看到过:
o.Alpha = _Color.a;
这里,o是一个结构,_Color是一个打包数组。这也是为什么Cg禁止混合使用这两种语法的原因:你不能使用_Color.xgz.
打包数组还有另一个重要的特性,在C#中没有类似的特性:swizzle。Cg允许在一行内对打包数组中的元素进行寻址和重新排序。再一次,它出现在标准着色器中:
o.Albedo = _Color.rgb;
Albedo是fixed3,这意味着它包含三个fixed类型的值。但是,_Color被定义为fixed4类型。直接赋值会导致编译器错误,因为 _Color比Albedo大。这样做的C#方式如下
o.Albedo.r = _Color.r;
o.Albedo.g = _Color.g;
o.Albedo.b = _Color.b;
但是,它可以用Cg压缩。
Cg还允许重新排序元素,例如,使用_Color bgr交换红色和蓝色通道
最后,将单个值分哦欸给打包数组的时候,会将其分配到所有字段。
o.Albedo = 0;//Black = (0,0,0)
o.Albedo = 1;//White = (1,1,1)
这被称作smearing。
Swizzing也可以在表达式的左侧使用,只允许覆盖打包数组的某些组件。
o.Albedo.rg = _Color.rg;
这被称作masking。
如果它被应用于打包矩阵,那么swizzling真正显示出它的全部潜力。Cg允许类型为float4*4, 它表示具有四行和四列的浮点矩阵。您可以使用_mRC表示法访问矩阵的单个元素,其中R是行,C是列:
float 4*4 matrix;
//..
float first = matrix._m00;
float last = matrix.m33;
_mRC表示法也可以链接:
float4 diagonal = matrix._m00_m11_m22_m33;
可以使用方括号选择整行:
float4 firstRow = matrix[0];
//等价于
float4 fristRow = matrix.m00_m01_m02_m03;
相关涉及
a.除了更容易编写外,swizzle,smearing和mask属性还具有性能优势。
b.但是,不恰当地使用swizzling也会使您的代码乍一看更难理解,并且可能使编译器难以自动优化代码。
c.打包数组是Cg最好的功能之一,你可以在这里发现更多相关信息。
http://http.developer.nvidia.com/CgTutorial/cg_tutorial_ chapter02.html
将纹理添加到着色器
纹理可以让我们的Shaders很快的实现非常逼真的效果,为了有效的使用纹理,我们需要了解2D图像如何映射到3D模型,此过程称为纹理映射。它需要在我们使用的shader和3D模型上完成一些工作。事实上。模型是由三角形组成的,通常称为多边形,模型上的每个顶点都可以存储Shader可以访问的数据并用于确定要绘制的内容。
存储在顶点中的最重要的信息之一是UVdata。它由两个坐标U和V组成,范围从0到1.它们表示将映射到顶点的2D图像中像素的XY位置。UV数据仅适用于顶点,当三角形的内部的顶点必须进行纹理映射时,GPU会插入最接近的UV值,以便在要使用的纹理中找到正确的像素。下图显示了如何从2D纹理如何从3D模型映射到三角形。
UV数据存储在3D模型中,需要编辑建模软件。某些模型缺少UV组件,因此它们不支持纹理映射。例如,之前的那个红色兔子就没有。
5.Texture Mapping
使用标准着色器向模型添加纹理非常简单,如下所示;
a.创建一个名为TextureShader的Standard Surface Shader。
b.创建一个名为TextureMaterial的Material。
c.把a步骤新建的shader赋值于b步骤新建的Material。
d.选择材质后,将纹理拖动到名为Albedo(RGB)的空矩形框里,如图所示。
提示:Standard Shader知道如何使用其UV模型将2D图像映射到3D模型。
e.需要注意的是,模型由不同的对象组成,每个对象提供在特定位置绘制的方向。也就是说我们需要在模型的每个部分上放置Material,在本例中直接把材质拖到模型最外层是不可行的。最后我会将项目链接放出来,里面会有模型basicCharacter。
f.我们也可以使用同一模型,换上不同的Texture,这通常用于游戏中以最小的成本提供不同类型的角色。
它是如何运行的
当从材质的Inspector中使用Standard Shader时,纹理映射背后的过程对开发人员完全透明,那么有必要仔细看看TextureShader。在属性部分,我们可以看到Albedo(RGB) texture 在代码上实际被称为_MainTex:
_MainTex("Albedo (RGB)",2D) = "white"{}
在CGPROGRAM部分,texture被定义为sampler2D,2Dtexture的标准类型为
sampler2D _MainTex;
以下行显示了一个名为Input的结构这是Surface function的输入参数,包含一个名为uv_MainTex的打包数组。
struct Input
{
float2 uv_MainTex;
};
每次调用surf()函数时,Input结构将包含_MainTex的UV,用于需要渲染3D模型的特定点。Standard Shader识别uv_MainTex名称引用_MainTex并自动初始化它。
最后,UV数据用于在surface function的第一行中对texture进行采样:
fixed4 c = tex2D(_MainTex,IN.uv_MainTex) * _Color;
这是使用Cg的tex2D()函数完成的;它需要texture和UV并返回该位置的像素颜色。
注意:U和V坐标从0到1,其中(0,0)和(1,1)对应于两个相对的角,不同的实施方式将UV与不同的角相关联,如果您的纹理恰好反转,请尝试反转V组件。
相关
当我们将纹理导入导入Unity时,我们可以设置sampler2D的一些属性,其中最重要的是FilterMode,它决定了在对纹理进行采样时如何插值颜色。UV数据不太可能精确指向像素的中心;在所有其它情况下,您可能希望在最近的像素之间进行插值以获得更均匀的颜色。
对于大多数应用,Bilinear提供了一种廉价而有效的平滑纹理方法。但是,如果想创建2D游戏,Bilinear可能会生成模糊的图块,在这种情况下,我们可以使用Point从纹理采样中删除任何插值。当从一个陡峭的角度观察纹理时,纹理采样很可能会产生视觉上令人不愉快的伪影,我们可以通过将Aniso Level 设置为更高的值来减少它们,这对于地板和天花板纹理特别有用,在这些纹理中,毛刺可以打破连续性的错觉。
6.Scrolling textures by modifying
使当今游戏行业中最常用的纹理技术之一是允许我们在对象表面上滚动纹理,这允许我们创建瀑布,河流和熔岩等效果。它也是创建动画精灵效果的基础技术。
a.创建一个名为ScrollingUVMat的Material。
b.创建一个名为ScrollingUVs的shader,并打开它。
c.这里需要两个属性允许我们控制纹理滚动的速度。
d.当在ShaderLab运行时,属性会有以下代码所示的语法。
Properties
{
_propertyName("Name in Inspector",Type) = value
}
e.Properties块中包含的每个属性首先都有一个名字,在代码中用于引用该对象,此处指定为_propertyName,下划线不是必需的,但是是一个通用的标准。在括号内,我们将看到两个参数。第一个是一个字符串,用于定义Inspector中显示此属性的文本,第二个参数是我们希望存储的数据类型。
在我们的例子中,对于X和Y滚动速度,我们正在创建一个可能的范围为0到10的数字。最后,我们可以使用默认值初始化属性,这是最后完成。正如我们之前看到的,如果你选择使用这个shader的Material,这些属性会显示在检查器中。
在我们的例子中,我们不需要Smothness和Metallic属性,因此我们可以移走它
f.修改CGPROCRAM部分中的Cg属性并创建新变量,以便我们可以从属性中访问值:
g.我们也移走_Glossiness和_Metallic这两个定义。
源码如下
Shader "Custom/ScrollingUVs" {
Properties {
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_ScrollXSpeed("X Scroll Speed",Range(0,10)) = 2 //控制X方向的速度
_ScrollYSpeed("Y Scroll Speed", Range(0,10)) = 2 //控制Y方向的速度
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
// Physically based Standard lighting model, and enable shadows on all light types
#pragma surface surf Standard fullforwardshadows
// Use shader model 3.0 target, to get nicer looking lighting
#pragma target 3.0
fixed _ScrollXSpeed;
fixed _ScrollYSpeed;
sampler2D _MainTex;
fixed4 _Color;
struct Input {
float2 uv_MainTex;
};
void surf (Input IN, inout SurfaceOutputStandard o) {
//在我们将UV传递给tex2D()函数之前,创建一个单独的变量来储存我们的UVs
fixed2 scrolledUV = IN.uv_MainTex;
//创建存储单个x和y分量的变量,以备UV随时间的缩放
fixed xScrollValue = _ScrollXSpeed * _Time;
fixed yScrollValue = _ScrollYSpeed * _Time;
//应用最终的UV偏移
scrolledUV += fixed2(xScrollValue, yScrollValue);
//应用纹理和色彩
half4 c = tex2D(_MainTex, scrolledUV);
o.Albedo = c.rgb * _Color;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
h.保存脚本,如下图所示设置。
i.回到游戏场景,新建一个Plane将材质赋予它,然后运行游戏,可有下过图如下
提示:如果在游戏过程中修改Material上的变量,则值将保持不变,这与Unity通常的工作方式不同。
学完这些后,我们将学习创建一个简单的河流。
它是如何运行的
滚动系统首先声明两个属性,这将允许使用Shader的人可以增加或减少滚动效果本身的速度。它们的核心是从Material的Inspector选项卡传递到着色器的surface函数的浮点值。
一旦我们从Material的选项卡中获得这些浮点值,我们将可以使用它们来偏移shader着色器中的UV值。
要开始此过程,我们首先将UV存储在名为srolledUV 的单独变量中。这个变量必须是float2/fixed2,因为UV值是从Input结构传递给我们的。
struct Input
{
float2 uv_MainTex;
}
一旦我们访问了网格的uv,我们可以使用滚动速度变量和内置的_Time变量来偏移它们。这个内置变量返回一个float4类型的变量,这意味着该变量的每个组件包含与游戏时间相关的不同时间值。
这个_Time变量会给我们一个基于Unity时钟的浮动值。因此,我们可以使用此值在UV方向上移动UV,并使用滚动速度变量缩放该时间:
//创建变量来存储单独的x和y分量,以备uv随时间的缩放。
fixed xScrollValue = _ScrollXSpeed * _Time;
fixed yScrollValue = _ScrollYSpeed * _Time;
通过按时间计算正确的偏移量,我们可以将新的偏移值添加回原始UV位置,这就是我们在下一行使用 += 运算符的原因。我们想要获取原始UV位置,添加新的偏移值,然后将其作为纹理的新UV传递给tex2D()函数。这会纹理在表面上移动的效果。我们真正在做的是操纵UV,所以我们伪造纹理移动的效果。
scrolledUV += fixed2(xScrollValue,yScrollValue);
half4 c = tex2D(_MainTex,scrolleUV);
该系列均基于Unity2018.0.1f 项目可到我的Github上下载:https://github.com/xiaoshuivv/ShadersUnity2018.1.0f