Shader基本结构

发表于2018-10-12
评论0 4.5k浏览
Shader大体上可以分为两类,简单来说

1.表面着色器(Surface Shader) - 为你做了大部分的工作,只需要简单的技巧即可实现很多不错的效果。类比卡片机,上手以后不太需要很多努力就能拍出不错的效果。

2.片段着色器(Fragment Shader) - 可以做的事情更多,但是也比较难写。使用片段着色器的主要目的是可以在比较低的层级上进行更复杂(或者针对目标设备更高效)的开发。

结构:
//指定shader名字
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 : POSITION(语义,参数当做POSITION类型参数);
};
void surf (Input IN, inout SurfaceOutput o) :COLOR0(语义:返回值当做颜色处理)
{
    half4 c = tex2D (_MainTex, IN.uv_MainTex);
    o.Albedo = c.rgb;
    o.Alpha = c.a;
}
ENDCG
}
//回滚
FallBack "Diffuse"
}

属性

在Properties{}中定义着色器属性,在这里定义的属性将被作为输入提供给所有的子着色器。每一条属性的定义的语法是这样的:
_Name("Display Name", type) = defaultValue[{options}]
_Name - 属性的名字,简单说就是变量名,在之后整个Shader代码中将使用这个名字来获取该属性的内容
Display Name - 这个字符串将显示在Unity的材质编辑器中作为Shader的使用者可读的内容
type - 这个属性的类型,可能的type所表示的内容有以下几种:
Color - 一种颜色,由RGBA(红绿蓝和透明度)四个量来定义;
2D - 一张2的阶数大小(256,512之类)的贴图。这张贴图将在采样后被转为对应基于模型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 - 对于贴图来说,默认值可以为一个代表默认tint颜色的字符串,可以是空字符串或者”white”,”black”,”gray”,”bump”中的一个
Float,Range - 某个指定的浮点数
Vector - 一个4维数,写为 (x,y,z,w)

另外还有一个{option},它只对2D,Rect或者Cube贴图有关,在写输入时我们最少要在贴图之后写一对什么都不含的空白的{},当我们需要打开特定选项时可以把其写在这对花括号内。如果需要同时打开多个选项,可以使用空白分隔。可能的选择有ObjectLinear, EyeLinear, SphereMap, CubeReflect, CubeNormal中的一个,这些都是OpenGL中TexGen的模式,具体的留到后面有机会再说。

数据类型:

有3种基本数值类型:float、half和fixed。
这3种基本数值类型可以再组成vector和matrix,比如half3是由3个half组成、float4x4是由16个float组成。
float:32位高精度浮点数。
half:16位中精度浮点数。范围是[-6万, +6万],能精确到十进制的小数点后3.3位。
fixed:11位低精度浮点数。范围是[-2, 2],精度是1/256。

int,32 位整形数据,有些 profile 会将 int 类型作为 float 类型使用。
bool,布尔数据,通常用于 if 和条件操作符(?:)。
string,字符类型。
sampler*,纹理对象的句柄(the handle to a texture object),分为 6 类: sampler, sampler1D, sampler2D, sampler3D, samplerCUBE,和 samplerRECT。

除了上面的基本数据类型外,Cg 还提供了内置的向量数据类型(built-in vector data types),内置的向量数据类型基 于基础数据类型。例如:float4,表示 float 类型的 4 元向量;bool4,表示 bool 类型 4 元向量。 注意:向量最长不能超过 4 元,即在 Cg 程序中可以声明 float1、float2、float3、 float4 类型的数组变量,但是不能声明超过 4 元的向量,例如:
float5 array;//编译报错
向量初始化方式一般为: float4 array = float4(1.0, 2.0, 3.0, 4.0); 较长的向量还可以通过较短的向量进行构建: float2 a = float2(1.0, 1.0); float4 b = float4(a, 0.0, 0.0);

此外,Cg 还提供矩阵数据类型,不过最大的维数不能超过 4*4 阶。例如:
float1x1 matrix1;//等价于 float matirx1; x 是字符,并不是乘号!
float2x3 matrix2;// 表示 2*3 阶矩阵,包含 6 个 float 类型数据
float4x2 matrix3;// 表示 4*2 阶矩阵,包含 8 个 float 类型数据
float4x4 matrix4;//表示 4*4 阶矩阵,这是最大的维数
矩阵的初始化方式为: float2x3 matrix5 = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0};

数组数据类型在 Cg 程序中的作用是:作为函数的形参,用于大 量数据的转递。 Cg 中声明数组变量的方式和 C 语言类似:例如:
float a[10];//声明了一个数组,包含 10 个 float 类型数据
float4 b[10];//声明了一个数组,包含 10 个 float4 类型向量数据
对数组进行初始化的方式为: float a[4] = {1.0, 2.0, 3.0, 4.0}; //初始化一个数组

要获取数组长度,可以调用“.length”,例如: float a[10]; //声明一个数组 int length = a.length;//获取数组长度

声明多维数组以及初始化的方式如下所示: 56 float b[2][3] = {{0.0, 0.0, 0.0},{1.0, 1.0, 1.0}};

数组和矩阵有些类似,但是并不是相同。 例如 4*4 阶数组的的声明方式为: float M[4][4];4 阶矩阵的声明方式为:float4x4 M。前者是一个数据结构,包含 16 个 float 类型数据,后者是一个 4 阶矩阵数据。float4x4 M[4],表示一个数组,包 含 4 个 4 阶矩阵数据。 进行数组变量声明时,一定要指定数组长度。

数据类型影响性能
精度够用就好。
颜色和单位向量,使用fixed
其他情况,尽量使用half(即范围在[-6万, +6万]内、精确到小数点后3.3位);否则才使用float。

Tags

表面着色器可以被若干的标签(tags)所修饰,而硬件将通过判定这些标签来决定什么时候调用该着色器。比如我们的例子中SubShader的第一句
Tags { "RenderType"="Opaque“ ”Queue" = "Background“ ”IgnoreProjector"="True"}(可以多个)

告诉了系统应该在渲染非透明物体时调用我们。Unity定义了一些列这样的渲染过程,与RenderType是Opaque相对应的显而易见的是"RenderType" = "Transparent",表示渲染含有透明效果的物体时调用。在这里Tags其实暗示了你的Shader输出的是什么,如果输出中都是非透明物体,那写在Opaque里;如果想渲染透明或者半透明的像素,那应该写在Transparent中。Tree和grass指定了树和草的一些标签。RenderType可以用于shader切换。

另外比较有用的标签还有"IgnoreProjector"="True"(不被Projectors投影影响),"ForceNoShadowCasting"="True“(从不产生阴影)以及”Queue"="xxx"(指定渲染顺序队列)。这里想要着重说一下的是Queue这个标签,如果你使用Unity做过一些透明和不透明物体的混合的话,很可能已经遇到过不透明物体无法呈现在透明物体之后的情况。这种情况很可能是由于Shader的渲染顺序不正确导致的。Queue指定了物体的渲染顺序,预定义的Queue有:
Background - 最早被调用的渲染,用来渲染天空盒或者背景
Geometry - 这是默认值,用来渲染非透明物体(普通情况下,场景中的绝大多数物体应该是非透明的)
AlphaTest - 用来渲染经过Alpha Test的像素,单独为AlphaTest设定一个Queue是出于对效率的考虑
Transparent - 以从后往前的顺序渲染透明物体,绝大部分透明的物体、包括粒子特效都使用这个;
Overlay - 用来渲染叠加的效果,是渲染的最后阶段(比如镜头光晕等特效)

这些预定义的值本质上是一组定义整数,Background = 1000, Geometry = 2000, AlphaTest = 2450, Transparent = 3000,最后Overlay = 4000。在我们实际设置Queue值时,不仅能使用上面的几个预定义值,我们也可以指定自己的Queue值,写成类似这样:"Queue"="Transparent+100",表示一个在Transparent之后100的Queue上进行调用。通过调整Queue值,我们可以确保某些物体一定在另一些物体之前或者之后渲染,这个技巧有时候很有用处。

PASS Tags:

RenderState:
Cull Off 剔除某一面
ZWrite Off 深度测试
AlphaTest Less[0.5] 透明度测试,符合条件的才显示
Blend SrcAlpha OneMinusSrcAlpha 颜色混合,原色*原色系数 + 目标色*目标色混合系数
ColorMask R(过滤R以外的颜色)

ZTest:深度测试:默认是小等于的时候显示。混合:透过有色玻璃看东西。原始颜色和目标颜色混合。



LOD

LOD很简单,它是Level of Detail的缩写,在这里例子里我们指定了其为200(其实这是Unity的内建Diffuse着色器的设定值)。这个数值决定了我们能用什么样的Shader。在Unity的Quality Settings中我们可以设定允许的最大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

CGPROGRAM。这是一个开始标记,表明从这里开始是一段CG程序(我们在写Unity的Shader时用的是Cg/HLSL语言)。最后一行的ENDCG与CGPROGRAM是对应的,表明CG程序到此结束。

接下来是是一个编译指令:#pragma surface surf Lambert,它声明了我们要写一个表面Shader,并指定了光照模型。它的写法是这样的
#pragma surface surfaceFunction lightModel [optionalparams]

surface - 声明的是一个表面着色器
surfaceFunction - 着色器代码的方法的名字
lightModel - 使用的光照模型。

在我们的例子中,我们声明了一个表面着色器,实际的代码在surf函数中(在下面能找到该函数),使用Lambert(也就是普通的diffuse)作为光照模型。

在CG中,sampler2D是和texture所绑定的一个数据容器接口,简单理解的话,所谓加载以后的texture(贴图)说白了不过是一块内存存储的,使用了RGB(也许还有A)通道,且每个通道8bits的数据。而具体地想知道像素与坐标的对应关系,以及获取这些数据,我们总不能一次一次去自己计算内存地址或者偏移,因此可以通过sampler2D来对贴图进行操作。更简单地理解,sampler2D就是GLSL中的2D贴图的类型,相应的,还有sampler1D,sampler3D,samplerCube等等格式。

我们用来实例的这个shader其实是由两个相对独立的块组成的,外层的属性声明,回滚等等是Unity可以直接使用和编译的ShaderLab;而现在我们是在CGPROGRAM……ENDCG这样一个代码块中,这是一段CG程序。对于这段CG程序,要想访问在Properties中所定义的变量的话,必须使用和之前变量相同的名字进行声明。于是其实sampler2D _MainTex;做的事情就是再次声明并链接了_MainTex,使得接下来的CG程序能够使用这个变量。

接下来是一个struct结构体。作为输入的结构体必须命名为Input。float2,表示浮点数的float后面紧跟一个数字2,float和vec都可以在之后加入一个2到4的数字,来表示被打包在一起的2到4个同类型数。在访问这些值时,我们即可以只使用名称来获得整组值,也可以使用下标的方式(比如。xyzw.rgba或它们的部分比如。x等等)来获得某个值。在这个例子里,我们声明了一个叫做uv_MainTex的包含两个浮点数的变量。UV mapping的作用是将一个2D贴图上的点按照一定规则映射到3D模型上,是3D渲染中最常见的一种顶点处理手段。在CG程序中,我们有这样的约定,在一个贴图变量(在我们例子中是_MainTex)之前加上uv两个字母,就代表提取它的uv值(其实就是两个代表贴图上点的二维坐标 )。我们之后就可以在surf程序中直接通过访问uv_MainTex来取得这张贴图当前需要计算的点的坐标值了。

surf函数。上面的#pragma段已经指出了我们的着色器代码的方法的名字叫做surf,那没跑儿了,就是这段代码是我们的着色器的工作核心。我们已经说过不止一次,着色器就是给定了输入,然后给出输出进行着色的代码。CG规定了声明为表面着色器的方法(就是我们这里的surf)的参数类型和名字,因此我们没有权利决定surf的输入输出参数的类型,只能按照规定写。这个规定就是第一个参数是一个Input结构,第二个参数是一个inout的SurfaceOutput结构。input其实是需要我们去定义的结构,这给我们提供了一个机会,可以把所需要参与计算的数据都放到这个Input结构中,传入surf函数使用;SurfaceOutput是已经定义好了里面类型输出结构,但是一开始的时候内容暂时是空白的,我们需要向里面填写输出,这样就可以完成着色了。

一个SubShader(渲染方案)是由一个个Pass块来执行的。每个Pass都会消耗对应的一个DrawCall。在满足渲染效果的情况下尽可能地减少Pass的数量。
和SubShader有自己专属的Tag类似,Pass也有Pass专属的Tag。

其中最重要Tag是 "LightMode",指定Pass和Unity的哪一种渲染路径(“Rendering Path”)搭配使用。除最重要的ForwardBase、ForwardAdd外,这里需额外提醒的Tag取值可包括:
Always,永远都渲染,但不处理光照
ShadowCaster,用于渲染产生阴影的物体
ShadowCollector,用于收集物体阴影到屏幕坐标Buff里。

其他渲染路径相关的Tag详见下面章节“Unity渲染路径种类”。
具体所有Tag取值,可参考ShaderLab syntax: Pass Tags。

语义词:

下面这组绑定语义关键字被Cg 语言的所有vertex profile 所支持
POSITION 表示该参数中的数据是的顶点位置坐标(通常位于模型空间)
BLENDWEIGHT
NORMAL 表示该参数中的数据是顶点法向量坐标(通常位于模型空间)
TANGENT 顶点切线
BINORMAL
PSIZE
BLENDINDICES
TEXCOORD0---TEXCOORD7 顶点纹理坐标
COLOR 顶点颜色值

下面这些语义词适用于所有的Cg vertex profiles作为输出语义和Cg fragment profiles的输入语义:
POSITION
PSIZE
FOG
COLOR0-COLOR1
TEXCOORD0-TEXCOORD7

如:
struct appdata_base {
    float4 vertex : POSITION;
    float3 normal : NORMAL;
    float4 texcoord : TEXCOORD0;
    UNITY_INSTANCE_ID
};
struct appdata_tan {
    float4 vertex : POSITION;
    float4 tangent : TANGENT;
    float3 normal : NORMAL;
    float4 texcoord : TEXCOORD0;
    UNITY_INSTANCE_ID
};
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;
    UNITY_INSTANCE_ID
};
动画:unity会每帧调用vert等入口函数,通过在函数中改变坐标或颜色值形成动画(常用_Time属性)

以上就是对Shader基本结构的全部结束, 希望能有助于大家去理解。
来自:https://www.cnblogs.com/wang-jin-fu/p/8279101.html

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引