Shader(着色器)是一段可以针对3D对象进行操做、并被GPU所执行的程序。Shader并非一个统一的标准,不一样的图形接口的Shader并不相同。OpenGL的着色语言是GLSL, NVidia开发了Cg,而微软的Direct3D使用高级着色器语言(HLSL)。而Unity的Shader 是将传统的图形接口的Shader(由 Cg / HLSL编写)嵌入到独有的描述性结构中而造成的一种代码生成框架,最终会自动生成各硬件平台本身的Shader,从而实现跨平台。html
Unity Shader 其实并不难,初学者每每很迷惑是由于它有太多固定的命令和结构,而这些命令又须要咱们对3D渲染有必定的了解才能知道它们是作什么的。数组
OpenGL和Direct3D都提供了三类着色器:ruby
Unity Shader 分为 表面着色器(Surface Shader)和 顶点片断着色器(Vertex And Fragment Shader)。app
Shader语法:框架
//Shader语法:
Shader "name" { [Properties] Subshaders [Fallback] [CustomEditor] } //Properties 语法 Properties { Property [Property ...] } // Subshader 语法 Subshader { [Tags] [CommonState] Passdef [Passdef ...] } // Pass 语法 Pass { [Name and Tags] [RenderSetup] } // Fallback 语法 Fallback "name"
基本的表面着色器示例:函数
Shader "Custom/NewShader" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} } SubShader { Tags { "RenderType" = "Opaque" } LOD 200 CGPROGRAM #pragma surface surf Lambert sampler2D _MainTex; struct Input { float2 uv_MainTex; }; void surf (Input IN, inout SurfaceOutput o) { half4 c = tex2D (_MainTex, IN.uv_MainTex); o.Albedo = c.rgb; o.Alpha = c.a; } ENDCG } FallBack "Diffuse" }
基本的顶点片断着色器示例:性能
Shader "VertexInputSimple" { SubShader { Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct v2f { float4 pos : SV_POSITION; fixed4 color : COLOR; }; v2f vert (appdata_base v) { v2f o; o.pos = mul (UNITY_MATRIX_MVP, v.vertex); o.color.xyz = v.normal * 0.5 + 0.5; o.color.w = 1.0; return o; } fixed4 frag (v2f i) : SV_Target { return i.color; } ENDCG } } }
Shader的输入有两个来源,一是经过属性定义,一是经过Shader.SetGlobalXXX方法全局设置。测试
属性定义变量:属性定义中的变量是Shader参数的主要设置方式。 它是随材质变化的,每一个使用该Shader的材质均可以在Inspector或者脚本中设置这些参数。这些参数除了在Shader的Properties段中定义外,还须要在Cg中声明方可以使用。例如上面表面着色器的例子中咱们定义了_MainTex这个类型为2D的属性,还须要在Cg中声明 sampler2D _MainTex。this
全局变量:Shader有一组SetGlobalXXX方法,能够对Shader的在Cg中定义而没有在属性中定义的uniform变量进行设置。这个设置是全局的,全部定义了该uniform的Shader都会受到影响。例如咱们但愿场景随着时间变化而改变颜色,就能够给场景所使用到的Shader设置统一的全局颜色变量,而后在脚本中经过设置该颜色来改变场景的颜色。在角色释放技能时场景变黑也可使用这个方法。编码
Unity shader 中容许定义的属性类型有:
关键字 | 类型 | 对应Cg类型 | 例 |
---|---|---|---|
Float | 浮点数 | float | _MyFloat (“My float”, Float) = 0.5 |
Range | 浮点数 (在指定范围内) | float | _MyRange (“My Range”, Range(0.01, 0.5)) = 0.1 |
Color | 浮点四元组 | float4 | _MyColor (“Some Color”, Color) = (1,1,1,1) |
Vector | 浮点四元组 | float4 | _MyVector(“Some Vector”,Vector) = (1,1,1,1) |
2D | 2的阶数大小的贴图 | sampler2D | _MyTexture (“Texture”, 2D) = “white” {} |
Rect | 非2的阶数大小的贴图 | sampler2D | _MyRect(“My Rect”, Rect) = “white” {} |
CUBE | CubeMap | samplerCUBE | _MyCubemap (“Cubemap”, CUBE) = “” {} |
注:CubeMap 是6张有联系的2D贴图的组合主要用来作反射效果(好比天空盒和动态反射)
SubShader中除了Pass,有两个标签值得关注:LOD和Tags
LOD是 Level of Detail的简写,确切地说是Shader Level of Detail的简写,由于Unity中还有一个模型的LOD概念,这是两个不一样的东西。咱们这里只介绍Shader中LOD,模型的LOD请参考这里。
Shader LOD 就是让咱们设置一个数值,这个数值决定了咱们能用什么样的Shader。能够经过Shader.maximumLOD或者Shader.globalMaximumLOD 设定容许的最大LOD,当设定的LOD小于SubShader所指定的LOD时,这个SubShader将不可用。经过LOD,咱们就能够为某个材质写一组SubShader,指定不一样的LOD,LOD越大则渲染效果越好,固然对硬件的要求也可能越高,而后根据不一样的终端硬件配置来设置 globalMaximumLOD来达到兼顾性能的最佳显示效果。
Unity内建Shader定义了一组LOD的数值,咱们在实现本身的Shader的时候能够将其做为参考来设定本身的LOD数值
SubShader能够被若干的标签(tags)所修饰,而硬件将经过断定这些标签来决定何时调用该着色器。
比较常见的标签有:
名字 | 值 | 描述 |
---|---|---|
Background | 1000 | 最先被调用的渲染,用来渲染天空盒或者背景 |
Geometry | 2000 | 这是默认值,用来渲染非透明物体(普通状况下,场景中的绝大多数物体应该是非透明的) |
AlphaTest | 2450 | 用来渲染通过Alpha Test的像素,单独为AlphaTest设定一个Queue是出于对效率的考虑 |
Transparent | 3000 | 以从后往前的顺序渲染透明物体 |
Overlay | 4000 | 用来渲染叠加的效果,是渲染的最后阶段(好比镜头光晕等特效) |
SubShader中能够定义一组Render State,基本上就是一些渲染的开关选项,他们对该SubShader的全部的Pass都有效,因此称Common。这些Render State也能够在每一个Pass中分别定义,将在Pass中详细介绍。
Render State主要就是控制渲染过程的一些开关选项,例如是否开启alpha blending ,是否开启depth testing。
经常使用的Render State有:
Cull
用法:Cull Back | Front | Off
多边形表面剔除开关。Back表示背面剔除,Front表示正面剔除,Off表示关闭表面剔除即双面渲染。有时候如裙摆,飘带之类很薄的东西在建模时会作成一个面片,这就须要设置Cull Off来双面渲染,不然背面会是黑色。
ZWrite
用法:ZWrite On | Off
控制当前对象的像素是否写入深度缓冲区(depth buffer),默认是开启的。通常来讲绘制不透明物体的话ZWrite开启,绘制透明或半透明物体则ZWrite关闭。
深度缓冲区:当图形处理卡渲染物体的时候,每个所生成的像素的深度(即 z 坐标)就保存在一个缓冲区中。这个缓冲区叫做 z 缓冲区或者深度缓冲区,这个缓冲区一般组织成一个保存每一个屏幕像素深度的 x-y 二维数组。若是场景中的另一个物体也在同一个像素生成渲染结果,那么图形处理卡就会比较两者的深度,而且保留距离观察者较近的物体。而后这个所保留的物体点深度保存到深度缓冲区中。最后,图形卡就能够根据深度缓冲区正确地生成一般的深度感知效果:较近的物体遮挡较远的物体。
理解了深度缓冲区也就理解了为何绘制透明或半透明物体须要关闭ZWrite, 若是不关闭,透明物体的depth也会被写入深度缓冲区,从而会剔除掉它后面的物体,后面的物体就不会被渲染,看不见后面的物体还能叫透明吗?所以咱们使用Alpha blending的时候须要设置ZWrite Off。
ZTest
用法:ZTest (Less | Greater | LEqual | GEqual | Equal | NotEqual | Always)
控制如何进行深度测试,也就是上面说的图形处理卡比较两者的深度的比较方法。默认是LEqual。
值得一提的是使用Aplha blending的时候ZWrite须要关闭可是ZTest是要开启的,由于若是透明物体前面还有不透明物体,透明物体仍是应该被遮挡剔除的。
Blend
混合。控制了每一个Shader的输出如何和屏幕上已有的颜色混合。
用法:
Blend Off: 关闭混合
Blend SrcFactor DstFactor:最终颜色 = Shader产生的颜色 × SrcFactor + 屏幕上原来的颜色 × DstFactor
Blend SrcFactor DstFactor, SrcFactorA DstFactor:和上面同样,只是Alpha通道使用后面两个参数计算
经常使用的Blend模式有:
Blend SrcAlpha OneMinusSrcAlpha // Alpha blending
Blend One One // Additive
Blend OneMinusDstColor One // Soft Additive
Blend DstColor Zero // Multiplicative
Blend DstColor SrcColor // 2x Multiplicative
具体参考这里
Unity5开始下列固定功能的Shader命令被标记为过期了,这些命令的功能如今建议在Shader(Cg)中经过代码来实现,这里列出是为了方便阅读之前写的Shader:
Surface Shader 隐藏了不少光照处理的细节,它的设计初衷是为了让用户仅仅使用一些指令(#pragma)就能够完成不少事情,而且封装了不少经常使用的光照模型和函数。相比底层的Vertex And Fragment Shader,Suface Shader的限制比较多,它只能有一次Pass。若是作一些常规的功能又须要光照,能够用Surface Shader写,比较快速便捷。若是要写比较高级的Shader仍是建议使用Vertex Shader 和 Fragment Shader。
Surface Shader主要有两部分组成,一个是#pragma后面的指令,一个是surf函数。
pragma的语法是 #pragma surface surfaceFunction lightModel [optionalparams]
- surfaceFunction 一般就是名为surf的函数, 函数名能够本身取
surf函数原型是:void surf (Input IN, inout SurfaceOutput o)
- lightModel是Unity内置的光照模型,能够是Lambert,Blinn-Phong等。
- optionalparams: 包含不少指令 详细参数参考这里
surf函数主要有一个Input结构的输入和SurfaceOutput结构的输出。
Input 结构须要在Shader中定义。它能够包含以下字段, 若是你定义了这些字段就能够在surf函数中使用它们(好神奇的黑科技)
float2 uv_MainTex
SurfaceOutput 描述了表面的特性(光照的颜色反射率、法线、散射、镜面等),这个结构是固定的,不须要在Shader中再定义。
struct SurfaceOutput { half3 Albedo; //反射率,通常就是在光照以前的原始颜色 half3 Normal; //法线 half3 Emission; //自发光,用于加强物体自身的亮度,使之看起来好像能够本身发光 half Specular; //镜面 half Gloss; //光泽 half Alpha; //透明 };
Unity5 因为引入了基于物理的光照模型,因此新增长了两个Output
struct SurfaceOutputStandard { fixed3 Albedo; // base (diffuse or specular) color fixed3 Normal; // tangent space normal, if written half3 Emission; half Metallic; // 0=non-metal, 1=metal half Smoothness; // 0=rough, 1=smooth half Occlusion; // occlusion (default 1) fixed Alpha; // alpha for transparencies }; struct SurfaceOutputStandardSpecular { fixed3 Albedo; // diffuse color fixed3 Specular; // specular color fixed3 Normal; // tangent space normal, if written half3 Emission; half Smoothness; // 0=rough, 1=smooth half Occlusion; // occlusion (default 1) fixed Alpha; // alpha for transparencies };
Unity提供了一些基本的SurfaceShader的例子,有助于咱们理解输入输出是如何被使用的。
Unity提供的SurfaceShader的例子
若是不想使用Surface Shader而直接编写opengl和Direct3D中常见的顶点着色器和片断着色器,能够经过Cg代码段嵌入到Pass中:
Pass {
// ... the usual pass state setup ... CGPROGRAM // compilation directives for this snippet, e.g.: #pragma vertex vert #pragma fragment frag // the Cg/HLSL code itself ENDCG // ... the rest of pass setup ... }
其中vert就是顶点着色器函数,frag就是片断着色器函数。通常来讲,能够在顶点着色器中进行的计算就不该该放到片断着色器中去算,由于顶点着色器是逐顶点计算的而片断着色器是逐像素计算的,一个模型顶点总比代表像素少不少吧。
编写顶点和片断着色器通常须要包含Unity预约义的一个帮助文件UnityCG.cginc,里面预约义了一些经常使用的结构和方法。Windows版Unity这个文件位于({unity install path}/Data/CGIncludes/UnityCG.cginc
。 Mac版位于/Applications/Unity/Unity.app/Contents/CGIncludes/UnityCG.cginc
。
在代码中咱们只须要添加 #include "UnityCG.cginc"
就可使用里面的结构和方法。
顶点着色器的原型是 v2f vert (appdata v)
appdata 是输入,能够本身定义也可使用Unity预约义的。Unity在UnityCG.cginc预约义了三种经常使用的输入结构:appdata_base,appdata_tan,appdata_full。
struct appdata_base { float4 vertex : POSITION; float3 normal : NORMAL; float4 texcoord : TEXCOORD0; }; struct appdata_tan { float4 vertex : POSITION; float4 tangent : TANGENT; float3 normal : NORMAL; float4 texcoord : TEXCOORD0; }; struct appdata_full { float4 vertex : POSITION; float4 tangent : TANGENT; float3 normal : NORMAL; float4 texcoord : TEXCOORD0; float4 texcoord1 : TEXCOORD1; float4 texcoord2 : TEXCOORD2; float4 texcoord3 : TEXCOORD3; #if defined(SHADER_API_XBOX360) half4 texcoord4 : TEXCOORD4; half4 texcoord5 : TEXCOORD5; #endif fixed4 color : COLOR; };
咱们注意到这些结构的字段和表面着色器中的字段不一样,后面多了一个冒号和一个标签。这是该字段的语义,用于告诉GPU这个字段的数据应该去哪里读写。GPU毕竟是为了图形计算而特别设计的东西,不少东西都是固定的,咱们只要记得有这么几个名字能够用行了。
类型 | 名字 | 标签 | 备注 |
---|---|---|---|
float4 | vertex | POSITION | 顶点在模型坐标系下的位置 |
float3 | normal | NORMAL | 顶点的法向量 |
float4 | tangent | TANGENT | 顶点的切向量 |
float4 | color | COLOR | 顶点色 |
float4 | texcoord | TEXCOORD0 | 顶点的第一个uv坐标 |
float4 | texcoord1 | TEXCOORD1 | 顶点的第二个uv坐标,最多能够到5 |
顶点着色器的输出是也是一个能够本身定义的结构,可是结构内容也是比较固定的,通常包含了顶点投影后的位置,uv,顶点色等,也能够加一些后面片断着色器须要用到可是须要在顶点着色器中计算的值。这个输出就是后面片断着色器的输入。
struct v2f { float4 pos : SV_POSITION; half2 uv : TEXCOORD0; };
可使用的字段有:
类型 | 标签 | 描述 |
---|---|---|
float4 | SV_POSITION | 顶点在投影空间下的位置,注意和输入的模型坐标系下的位置不一样,这个字段必必须设置,这个坐标转换是顶点着色器的重要工做 |
float3 | NORMAL | 顶点在视图坐标系下的法向量 |
float4 | TEXCOORD0 | 第一张贴图的uv坐标 |
float4 | TEXCOORD1 | 第二张贴图的uv坐标 |
float4 | TANGENT | 切向量,主要用来修正法线贴图Normal Maps |
fixed4 | COLOR | 第一个定点色 |
fixed4 | COLOR1 | 第二个定点色 |
Any | Any | 其余自定义的字段 |
顶点着色器有一项重要的工做就是进行坐标变换。顶点着色器的输入中的坐标是模型坐标系(ObjectSpace)下的坐标,而最终绘制到屏幕上的是投影坐标。
在咱们Shader里面只须要一句话就能够完成坐标的转换,这也是最简单的顶点着色器:
v2f vert(appdata v) {
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
return o; }
用UNITY_MATRIX_MVP矩阵乘以顶点在模型坐标系下的坐标就获得投影坐标。
UNITY_MATRIX_MVP是Unity内建的模型->视->投影矩阵, Unity内建矩阵以下:
下面简单介绍一下里面提到的几个坐标系:
模型坐标系:也叫物体坐标系,3D建模的时候每一个模型都是在本身的坐标系下创建的,若是一我的物模型脚底是(0,0,0) 点的话它的身上其它点的坐标都是相对脚底这个原点的。
世界坐标系:咱们场景是一个世界,有本身的原点,模型放置到场景中后模型上的每一个顶点就有了一个新的世界坐标。这个坐标能够经过模型矩阵×模型上顶点的模型坐标获得。
视图坐标系:又叫观察坐标系,是以观察者(相机)为原点的坐标系。场景中的物体只有被相机观察到才会绘制到屏幕上,相机能够设置视口大小和裁剪平面来控制可视范围,这些都是相对相机来讲的,因此须要把世界坐标转换到视图坐标系来方便处理。
投影坐标系:场景是3D的,可是最终绘制到屏幕上是2D,投影坐标系完成这个降维的工做,投影变换后3D的坐标就变成2D的坐标了。投影有平行投影和透视投影两种,能够在Unity的相机上设置。
屏幕坐标系 : 最终绘制到屏幕上的坐标。屏幕的左下角为原点。
除了内建矩阵,Unity还内建了一些辅助函数也能够在顶点着色器里面使用: