何为shader?
shader 实际就是一小段程序,它负责将输入的Mesh(网格)以指定的方式和输入的贴图或者颜色等组合作用,然后输出。
绘图单元可以依据这个输出来将图像绘制到屏幕上。
输入的贴图或者颜色,加上对应的shader,以及Shader的特定的参数设置,将这些内容打包存储在一起,得到的就是一个Material。之后,我们便可以将材质赋予合适的render来进行渲染(输出)了。
所以Shader 简单来说 就是一段规定好输入(颜色,贴图等)和输出(渲染器能够读懂的点和颜色的对应关系)的程序。
shader 开发者要做的就是根据输入,进行计算变换,产生输出。
Shader 大体分类
表面着色器(Surface Shader): 为你做了大部分的工作,只需要简单的技巧即可实现很多不错的效果。类比卡片机,上手以后不太需要很多努力就能拍出不错的效果。
片段着色器(Fragment): 可以做的事情很多,但是也比较难写。使用片段着色器的主要目的是可以比较低的层级上进行更复杂(或者针对目标设备更高效)的开发
表面着色器
shader程序的基本结构
着色器代码可以说专用性很强,因此人为地规定了它的基本结构。
一个普通的着色器的结构应该是这样的:
首先是一些属性定义,用来指定这段代码将有哪些输入。
接下来是一个或者多个的子着色器,在实际运行中,哪一个子着色器被使用是由平台所决定的。
子着色器是代码的主题,每一个子着色器重包含一个或者多个的Pass.
在计算着色器时,平台先选择最优先可以使用的着色器,然后依次运行其中的Pass,然后得到输出结果。
最后指定一个回滚,用来处理所有SubShader都不能运行的情况。(比如设备实在泰来,所有SubShader中都有其不支持的特性)
需要提前说明的是,在实际进行表面着色器的开发时,我们将直接在Subshader这个层次上写代码,系统将把我们的代码编译成若干个合适的Pass。
Shader "Custom/Diffuse Texture" {
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都是什么意思
属性
在Properties{}中定义了着色器属性,在这里定义的属性将被作为输入提供提供给所有子着色器。
每一条属性的定义的语法是这样的:
_Name("display Name",type)=defaultValue[{options}]
- _Name 属性名字,简单说就是变量名,在之后整个Shader代码中将使用这个名字来获取属性的内容。
- Display Name 这个字符串将显示在Unity的材质编辑器中作为Shader的使用者可读内容
- type 这个属性的类型,可能的type所表示的内容有以下几种:
- Color 一种颜色,由RGBA 四个量来定义
- 2D 一张2的阶数大小的贴图。这张贴图将在采样后 被转为对应基于模型UV的每个像素的颜色,最终被显示出来。
- Rect 一个非2阶数大小的贴图
- Cube Cube map texture(立方体纹理),简单说就是6张有联系的2D贴图的组合,主要用来做反射效果(例如 天空盒子和动态反射),也会被转换为对应的采样。
- Range(min,max) 一个介于最小值和最大值之间的浮点数,一般用来当做调整Shader某些特性的参数。(例如 透明度渲染的截止值可以是从0至1的值)
- Float 任意一个浮点数
- Vector 一个四维数
- defaultValue 定义了这个属性的默认值,通过输入一个符合格式的默认值来指定对应属性的初始值
- Color 以0~1定义的RGBA颜色,比如(1,1,1,1);
- 2D/Rect/Cube 对于贴图来说,默认值可以为一个代表默认颜色的字符串,可以是空字符或者“White”,“black”,“gray”,"bump"中的一个
- Float,Range 某个指定的浮点数
- Vector 一个四维数,写为(x,y,z,w)
-另外还有一个{option},它只对2D,Rect 或者Cube贴图相关,在写输入时我们最少要在贴图之后写一对什么都不含的空白的{},当我们需要打开特定选项时可以把其卸载这对花括号内,如果需要同时打开多个选项,可以使用空白分隔。可能的选择有ObjectLinear,EyeLinear,SphereMap,CubeReflect,CuveNormal中的一个,这些都是OpenGL中TexGen的模式
所以一组属性的申明开起来也许是这样的
_Color ("Color", Color) = (0,0,0,0)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Glossiness ("Smoothness", Range(0,1)) = 0.5
_Metallic ("Metallic", Range(0,1)) = 0.0
Tags
表面着色器可以被若干的标签(tags)所修饰,而硬件将通过判定这些标签来决定什么时候调用该着色器。比如
RenderType 标签
Tags { "RenderType"="Opaque" }
告诉了系统应该在渲染非透明物体时调用我们。
Unity 定义了一些类似这样的渲染功能,与RenderType时Opaque相对应的是
Tags {"RenderType"="transparent"}
表示渲染含有透明效果的物体时调用。
这里的Tags 其实暗示了你的Shader 输出是什么
如果输出中都是非透明物体,那写在Opaque里;
如果想渲染透明或者半透明的像素,那应该写在Transparent中。
IgnoreProjector 标签
Tags {"IgnoreProject"="True" } //不被Projector 影响
ForceNoShadowCasting 标签
Tags {"ForceNoShadowCasting"="true"} //从不产生阴影
Queue 标签 渲染队列
如果使用Unity 做过一些透明和不透明物体的混合,可能会遇到不透明物体无法呈现在透明物体之后的情况。这种情况很可能是由于Shader的渲染顺序不正确导致的。
Queue指定了物体的渲染顺序,预定义的Queue有:
- Background: 最早被调用的渲染,用来渲染天空盒或者背景
- Geometry: 这是默认值,用来渲染非透明物体(普通情况下,场景中的绝大多数物体是非透明的)
- AlphaTest: 用来渲染经过Alpha Test的镜像,单独为AlphaTest设定一个Queue是出于对效率的考虑
- Transparent 从后往前的顺序渲染透明物体
- Overlay 用来渲染叠加的效果,是渲染的最后阶段(比如镜头光晕等特效)
这些预定义的值本质上是一组定义整数。
Background=1000;
Geomety=2000;
AlphaTest=2450;
Transparent=3000;
OverLay=4000;
在实际设置Queue,我们可以指定自己的queue值,类似于“queue”=“Transparent”+100
LOD (level of detail)的缩写
这个数值决定了我们能用什么样的Shader.
在Unity的Edit->project setting中有设置quality setting,我们可以设定允许最大的LOD
当设定的LOD小于SubShader所指定的LOD时,这个SubShader将不可用
Unity内建的Shader定义了一组LOD的数值,我们在实现自己的Shader的时候可以将其作为参考来设定自己的LOD数值,这样在之后调整可以根据设备图形性能来调整画质进行比较精确的控制。
- VertexLit及其系列=100
- Decal,Reflective VertexLit=150;
- Diffuse=200;
- Diffuse Detail,Reflective Bumped Unlit,Reflective Bumped VertexLit=250;
- Bumped,Specular=300
- Bumped Specular=400
- Parallax=500
- parallax Specular=600
Shader 本体
前面 我们说到了 属性 标签 LOD
接下来谈谈 Shader的本体 就是输入转变为输出的代码部分
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
我们还是来进行一个逐句开始:
首先 :是CGPROGRAM,这是一个开始标记,表明从这里开始是一段CG程序(Unity 的shader用的是 Cg/HLSL语言)
最后一行 的ENDCG与CGPROGRAM是想对应的,表明CG程序到此结束。
接下来是一个编译指令:#program surface surf Lambert
它申明了我们要写一个表面的Shader,并指定了光照模型。
它的写法是这样的:
#program surface surfaceFunciton ligthModel[optionalparams]
- surface 声明的是一个表面的着色器
- surfaceFunction 着色器代码的方法的名字
- lightModel 使用的光照模型
接下来一句: sampler2D _MainTex; 其实在CG中,sampler2D就是一个和texture所绑定的数据容器接口。简单理解的话,加载以后的texture说白了不过是一块内存存储的,使用了RGBA通道,且每个通道8bits的数据。而具体的想知道像素与坐标的对应关系,以及获取这些数据,我们总不能一次次自己去计算内存地址或者偏移,因此通过sampler2D来对贴图进行操作。
更简单的理解:sampler2D 就是GLSL中2D贴图的类型,相应的,还有sampler2D,sampler3D,samplerCube等等格式。
那在属性不是已经声明过它是贴图了吗?为什么这里还需要再次声明,而且名字还是一样的?
其实 Shader是由两个相对独立的块组成的,外层的属性申明,回滚等是Unity可以直接使用和编译的ShaderLab;
而在CGPROGRAM…ENDCG这样一个代码中,这是一段CG程序。对于CG程序,要想访问在properties中所定义的变量的话,必须使用和之前变量相同的名字进行申明。
于是呢,sampler2D _mainTex; 做的事情就是再次申明并链接了_MainTex,使得接下里的CG程序能够使用这个变量。
在接下来 是一个Input结构体
最后是surf函数
上面的#pragram段 已经指出了我们的着色器代码的方法名字叫做surf,就是这段代码是我们着色器的工作核心。
着色器就是给定了输入,然后给出输出进行着色的代码。
CG规定了申明为表面着色器的方法的参数类型和名字,因此我们没有权利决定surf的输入输出参数类型,只能按照规定写。
这个规定就是第一个参数是一个Input结构,第二个参数是一个inout的SurfaceOutput结构
那它们分别是什么呢? input 其实是需要我们去定义的结构,这给我们提供了一个机会,可以把所需要参与计算的数据都放到这个input结构,传入surf函数中使用
SurfaceOutput是已经定义好了里面类型输出结构,但是一开始的时候内容暂时是空白的,我们需要向里面填写输出,这样就可以完成着色了。
再跳回来看看Input的结构体,这个结构体定义了一个float2的变量,表示浮点数的float后面紧跟一个数字2.float 和vec 都可以在之后加入2到4的数字,来表示被打包在一起的2到4个同类型数。
比如:
vec2 coordinate;
float4 color;
float3 multipliedColor=color.rgb*coordinate.x;
在访问这些值时,我们即可以只使用名称来获取整组值,也可以使用小标的方式(比如.xyzw.rgba)来获得某个值。
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
在以上的例子中,我们申明了一个包含两个浮点数的变量。
UV 是UV mapping。作用是将一个2D贴图上的点按照一定规则映射到3D模型上,是3D渲染中最常见的一种顶点处理手段。
在CG程序中,在一个贴图变量之前加上UV两个字母,就代表提取它的uv值【就是两个代表贴图的二维坐标】
我们之后就可以在surf程序中直接通过访问uv_MainTex来取得这张贴图当前需要计算的点的坐标值。
在surf函数中,它的两个参数
一个是Input,在计算输出时Shader会多次调用surf函数,每次给入一个贴图上的点坐标,来计算输出。
一个是可写的SurfaceOutPut,surfaceOutPut是预定义的输出结构,我们的surf函数的目标是根据输入把这个输出的结构填上。
SurfaceOutput的结构定义如下:
struct SurfaceOutput {
half3 Albedo; //像素的颜色
half3 Normal; //像素的法向值
half3 Emission; //像素的发散颜色
half Specular; //像素的镜面高光
half Gloss; //像素的发光强度
half Alpha; //像素的透明度
};
这里的half指的是半精度浮点数,精度最低,运算性能相对比高精度浮点数高一些,因此被大量使用。
在这里我们做的事情很简单
half4 c = tex2D (_MainTex, IN.uv_MainTex);
o.Albedo = c.rgb;
o.Alpha = c.a;
这里用到了一个tex2D函数,这是CG程序中用来在一张贴图中对一个点进行采样的方法,返回一个float4.
这里对_MainTex在输入点上进行了采样,并将其颜色的rbg值赋予了输出的像素颜色,将a值赋予透明度。