Surface Shader Examples
下面是一些表面着色器(Surface Shaders)的示例。下面的示例都是使用的内置光照模式(lighting models),关于如何实现自定义光照模式可以参考 表面着色器光照范例(Surface Shader Lighting Examples)。
Simple 简单的示例
我们从分析和建立一个简单的着色器开始。下面是这个着色器仅仅设置了表面颜色( surface color)为"白色"。它使用了内置的 Lambert (diffuse)光照模式(lighting model)。
Shader "Example/Diffuse Simple" {
SubShader {
Tags { "RenderType" = "Opaque" }
CGPROGRAM
#pragma surface surf Lambert
struct Input {
float4 color : COLOR;
};
void surf (Input IN, inout SurfaceOutput o) {
o.Albedo = 1;
}
ENDCG
}
Fallback "Diffuse"
}
注解:
struct SurfaceOutput {
half3 Albedo;//漫反射的颜色值
half3 Normal;//法线坐标
half3 Emission;//自发光颜色
half Specular;//镜面反射系数
half Gloss;//光泽系数
half Alpha;//透明度系数
};
pragma surface surfaceFunction lightModel [optionalparams]
surfaceFunction,没什么好说,肯定是函数名了。
lightModel是所采用的光照模型。可以自己写也可使用内置如Lambert和BlinnPhong.
optionalparams:可选参数,一堆可选包括透明度,顶点与颜色函数,投射贴花shader等等。具体用到可以细选。
o.Albedo=1;漫反射的颜色是一个rgb值,如果给一个1,其实就是 float3(1,1,1),就是反射出来的颜色为白色,
如果为100,则是加强反射强度,并不会改变其颜色。为0或为负数时道理类似。
Fallback "Diffuse",Diffuse是自带的shader,可以用自己自定义好的。这里这句的意思是,
如果所有subshader在当前显卡都不支持,则默认返回自带的Diffuse。
下面是效果。它看起来像是在模型上设置了两个光照(lights)。
Texture 纹理
看一个白模是相当枯燥的。所以我们来给它添加纹理(texture)。我们将在着色器的属性(Properties)块中添加。在材质球(Material)中我们选择一个纹理(texture)。有变化的地方在下面用粗体字表示出来了。
Shader "Example/Diffuse Texture" {
Properties {
_MainTex ("Texture", 2D) = "white" {}
}
SubShader {
Tags { "RenderType" = "Opaque" }
CGPROGRAM
#pragma surface surf Lambert
struct Input {
float2 uv_MainTex;
};
sampler2D _MainTex;
void surf (Input IN, inout SurfaceOutput o) {
o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
}
ENDCG
}
Fallback "Diffuse"
}
image注解:
Tex2D,这个方法是根据UV上的点找指定 2DSample上的Texture信息,此处需要其RGB信息,
就打出来赋给了漫反射值。所以对有材质图的情况下,要显示出图,还是要相应的反射其原图的rgb值。
Normal mapping 法线贴图
我们来添加一些法线贴图(normal map)。
Shader "Example/Diffuse Bump" {
Properties {
_MainTex ("Texture", 2D) = "white" {}
_BumpMap ("Bumpmap", 2D) = "bump" {}
}
SubShader {
Tags { "RenderType" = "Opaque" }
CGPROGRAM
#pragma surface surf Lambert
struct Input {
float2 uv_MainTex;
float2 uv_BumpMap;
};
sampler2D _MainTex;
sampler2D _BumpMap;
void surf (Input IN, inout SurfaceOutput o) {
o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap));
}
ENDCG
}
Fallback "Diffuse"
}
image注解:
这个UnpackNormal是unity自带的标准解压法线用的,所谓解压,就是将法线的区间进行变换。由于 tex2D(_BumpMap, IN.uv_BumpMap)取出的是带压缩的[0,1]之间,需要转成[-1,1]。这个函数会针对移动平台或OPENGL ES平台采用 RGB法线贴图,其他采用DXT5nm贴图。因此也可自己写。
Rim Lighting 边缘光照
现在我们试着添加边缘光照(Rim Lighting),在对象的边缘部分增加亮度。我们要在表面法线(surface normal)和视图方向(view direction)的基础上添加散射光照(emissive light)。为了实现它,我们要使用 viewDir,这是表面着色器(surface shader)内置的一个变量。
Shader "Example/Rim" {
Properties {
_MainTex ("Texture", 2D) = "white" {}
_BumpMap ("Bumpmap", 2D) = "bump" {}
_RimColor ("Rim Color", Color) = (0.26,0.19,0.16,0.0)
_RimPower ("Rim Power", Range(0.5,8.0)) = 3.0
}
SubShader {
Tags { "RenderType" = "Opaque" }
CGPROGRAM
#pragma surface surf Lambert
struct Input {
float2 uv_MainTex;
float2 uv_BumpMap;
float3 viewDir;
};
sampler2D _MainTex;
sampler2D _BumpMap;
float4 _RimColor;
float _RimPower;
void surf (Input IN, inout SurfaceOutput o) {
o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap));
half rim = 1.0 - saturate(dot (normalize(IN.viewDir), o.Normal));
o.Emission = _RimColor.rgb * pow (rim, _RimPower);
}
ENDCG
}
Fallback "Diffuse"
}
viewDir.png注解:
viewDir 意为World Space View Direction。就是当前坐标的视角方向。
最里层是Normalize函数,用于获取到的viewDir坐标转成一个单位向量且方向不变,外面再与点的法线做点积。最外层再用 saturate算出一[0,1]之间的最靠近(最小值但大于所指的值)的值。这样算出一个rim边界。为什么这么做。原理以下解释:
1.这里o.Normal就是单位向量。外加Normalize了viewDir。因此求得的点积就是夹角的cos值。
2.因为cos值越大,夹角越小,所以,这时取反来。这样,夹角越大,所反射上的颜色就越多。于是就得到的两边发光的效果。哈哈这样明了吧。
half:CG里还有类似的float和fixed。half是一种低精度的float,但有时也会被选择成与float一样的精度。 fragment是一定会支持fixed类型,同时也会有可能将其精度设成与float一样,这个比较复杂,后面篇章学到fragment时再深入探讨。
Detail Texture 细节纹理
为了实现不同的效果。让我们来添加细节纹理(detail)。它是与基础纹理(base texture)的结合。细节纹理(detail texture)与基础纹理(base texture)使用相同的UV。但是在材质球(Material)中平铺(Tiling)值通常是不同的。所以我们必须输入结构(input structure)中使用不同的UV坐标。
Shader "Example/Detail" {
Properties {
_MainTex ("Texture", 2D) = "white" {}
_BumpMap ("Bumpmap", 2D) = "bump" {}
_Detail ("Detail", 2D) = "gray" {}
}
SubShader {
Tags { "RenderType" = "Opaque" }
CGPROGRAM
#pragma surface surf Lambert
struct Input {
float2 uv_MainTex;
float2 uv_BumpMap;
float2 uv_Detail;
};
sampler2D _MainTex;
sampler2D _BumpMap;
sampler2D _Detail;
void surf (Input IN, inout SurfaceOutput o) {
o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
o.Albedo *= tex2D (_Detail, IN.uv_Detail).rgb * 2;
o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap));
}
ENDCG
}
Fallback "Diffuse"
}
注解:
在原先的反射基础上,在加一层,Texture的反射。
使用一个方格纹理(chercker texture)并没有实际的意义。但至少让我明白了会产生什么效果:
image
Detail Texture in Screen Space 在屏幕空间使用细节纹理
怎样在屏幕空间(screen space)使用细节纹理(detail texture)?它对一个士兵的头部模型没有多大意义。但它说明了在输入结构(input structure)中如何使用内置的screenPos。
Shader "Example/ScreenPos" {
Properties {
_MainTex ("Texture", 2D) = "white" {}
_Detail ("Detail", 2D) = "gray" {}
}
SubShader {
Tags { "RenderType" = "Opaque" }
CGPROGRAM
#pragma surface surf Lambert
struct Input {
float2 uv_MainTex;
float4 screenPos;
};
sampler2D _MainTex;
sampler2D _Detail;
void surf (Input IN, inout SurfaceOutput o) {
o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
float2 screenUV = IN.screenPos.xy / IN.screenPos.w;
screenUV *= float2(8,6);
o.Albedo *= tex2D (_Detail, screenUV).rgb * 2;
}
ENDCG
}
Fallback "Diffuse"
}
注解:
是从上个例子的基础上将第二层叠加上的2D Texture根据当前屏幕的UV进行叠加,而不是根据自身的UV。这样带有含此shader材质的物体的贴图就会跟着移动到的位置而变换图片。
这里只需要说三点:
1.关于screenPos:screenPos是一个三维点,但是用齐次坐标的形式表示出来就是(x,y,z,w),根据齐次坐标的性质。 (x,y,z,w)的齐次坐标对应三维点(x/w,y/w,z/w)。因此把w值除掉可以看来是一种Normalize的作法,这样就取出了实际的屏幕 xy的UV值。
2.对screenUV进行倍剩:此处剩float2(8,6)意为将原获取到屏幕尺寸进行拉大的倍数。即x轴拉大8倍,y轴拉大6倍。
3.如何就平铺了刚好一行8个,一列6个了呢? 原因我觉得是在于2d Texture自己是按Normalize后进行铺的,因此在//2(刚转完标准的)screenPos后,将其剩多少即便将原图铺多少张。
这个东西可以拿来做放大镜的应用。
从着色器中删除法线贴图(normal mapping)只是为了试着色器代码简短一点。
image
Cubemap Reflection 立方图反射
在这里要在输入结构(input structure)中使用内置的worldRefl 来做立方图反射(cubemap reflection)。它实际是与内置的 Reflective/Diffuse着色器非常类似。
Shader "Example/WorldRefl" {
Properties {
_MainTex ("Texture", 2D) = "white" {}
_Cube ("Cubemap", CUBE) = "" {}
}
SubShader {
Tags { "RenderType" = "Opaque" }
CGPROGRAM
#pragma surface surf Lambert
struct Input {
float2 uv_MainTex;
float3 worldRefl;
};
sampler2D _MainTex;
samplerCUBE _Cube;
void surf (Input IN, inout SurfaceOutput o) {
o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb * 0.5;
o.Emission = texCUBE (_Cube, IN.worldRefl).rgb;
}
ENDCG
}
Fallback "Diffuse"
}
因为它指定了和自发光(Emission)一样的反射颜色(reflection color),所以我们得到了一个非常有光泽的士兵。
image
如果你想在这基础上做反射效果,它是要受到法线贴图(normal map)的影响的。这需要在输入结构(input structure)中加入一个稍微复杂的 INTERNAL_DATA。在世界反射向量(WorldReflectionVector)函数中计算每个像素(per-pixel)反射向量(reflection vector),然后你要在输出结构(output structure)中写入法线(normal)。
Shader "Example/WorldRefl Normalmap" {
Properties {
_MainTex ("Texture", 2D) = "white" {}
_BumpMap ("Bumpmap", 2D) = "bump" {}
_Cube ("Cubemap", CUBE) = "" {}
}
SubShader {
Tags { "RenderType" = "Opaque" }
CGPROGRAM
#pragma surface surf Lambert
struct Input {
float2 uv_MainTex;
float2 uv_BumpMap;
float3 worldRefl;
INTERNAL_DATA
};
sampler2D _MainTex;
sampler2D _BumpMap;
samplerCUBE _Cube;
void surf (Input IN, inout SurfaceOutput o) {
o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb * 0.5;
o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap));
o.Emission = texCUBE (_Cube, WorldReflectionVector (IN, o.Normal)).rgb;
}
ENDCG
}
Fallback "Diffuse"
}
注解:
这两段都是加一个cubemap的反射。第二段相比之下是在有normal反射的基础上加的。Cubemap这东西,可设置几种面的不能渲染图,这方面可用于做天空盒。因为这样可以从各个角度看过去以显示不同的渲染效果。
以下说明:
- worldRefl:即为世界空间的反射向量。
- texCUBE:将反射向量一个个的往_Cube反射盒上找出然后做为Emission反射出来。
- 第二个例子只是将其用在Normal反射后,这样一定要多添加一个INTERNAL_DATA的属性,另外也需用到WorldReflectionVectore方法取其利用Normal后的反射向量值。
类似于的效果,可见官网中的。我这也有一个,有点像打了光的样子。
这是一个贴了法线贴图(normal map)的有光泽的士兵。
image
Slices via World Space Position 通过世界空间位置进行切割
在这个着色器里,被"切割" 所抛弃的像素(pixels)形状是几乎与水平位置平行的环状。它是基于世界位置的像索(pixel)通过使用Cg/HLSL语言的 clip()函数实现。我们将使用表面着色器(surface shader)内置的worldPos变量。
Shader "Example/Slices" {
Properties {
_MainTex ("Texture", 2D) = "white" {}
_BumpMap ("Bumpmap", 2D) = "bump" {}
}
SubShader {
Tags { "RenderType" = "Opaque" }
Cull Off
CGPROGRAM
#pragma surface surf Lambert
struct Input {
float2 uv_MainTex;
float2 uv_BumpMap;
float3 worldPos;
};
sampler2D _MainTex;
sampler2D _BumpMap;
void surf (Input IN, inout SurfaceOutput o) {
clip (frac((IN.worldPos.y+IN.worldPos.z*0.1) * 5) - 0.5);
o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap));
}
ENDCG
}
Fallback "Diffuse"
}
image注解:
frac是取小数的函数,如1.23 取出来是 0.23。clip函数用于清Pixel的,负值情况下才进行清pixel。且越小,即绝对值越大则清越多。 这里注意那个* 5,仔细一想,如果frac出来的值越大,-0.5值就越大,绝对值就越小,因此这样清掉的pixel越少,所以就可以间接的增加分段的次数。那为什么要+IN.worldPos.z*0.1呢,主要原因就是空开的断添加一个倾斜角度,可以用空间思想想下。
Normal Extrusion with Vertex Modifier 法线挤压与顶点修改
它可以在顶点着色器(vertex shader)中使用"顶点修改(vertex modifier)"函数修改传入的顶点(vertex)数据。它能作用在程序动画上。比如顺着法线挤压等等。表面着色器(surface shader)是通过编译vertex:functionName函数指令来使用它。这个函数传入的参数是 inout appdata_full 。
这个着色器它在材质(material)里面顶点是随着法线变化的:
Shader "Example/Normal Extrusion" {
Properties {
_MainTex ("Texture", 2D) = "white" {}
_Amount ("Extrusion Amount", Range(-1,1)) = 0.5
}
SubShader {
Tags { "RenderType" = "Opaque" }
CGPROGRAM
#pragma surface surf Lambert vertex:vert
struct Input {
float2 uv_MainTex;
};
float _Amount;
void vert (inout appdata_full v) {
UNITY_INITIALIZE_OUTPUT(Input,data);
v.vertex.xyz += v.normal * _Amount;
}
sampler2D _MainTex;
void surf (Input IN, inout SurfaceOutput o) {
o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
}
ENDCG
}
Fallback "Diffuse"
}
注解:
这是个自定义vertex的例子,效果可以实现点坐标的放大缩小,以形成肥仔与瘦棍的效果,哈哈。
添加一个可选参数为vertex,主要是为了给其添加一个函数vert。
_Amount对应开头的那个属性_Amount。具体是个Range值,可在shader界面外通过滑动条改变这个值。默认为0.5。
v.vertex.xyz += v.normal * _Amount;就是为个点,换当前法线向量的指定倍数进行扩展。
这里除了之前学过的东西外,多了个appdata_full的结构体。这里面的结构(载自UNITY官方论坛)如下:
struct appdata_full {
float4 vertex : POSITION;
float4 tangent : TANGENT;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
float4 texcoord1 : TEXCOORD1;
fixed4 color : COLOR;
};
顶点随着它的法线(normal)变化后生成一个浮肿的士兵:
image
Custom data computed per-vertex 用自定义数据计算每个顶点变化
在一个顶点着色器(vertex shader)中通过计算自定义数据也可以实现上面的顶点修改函数(vertex modifier function)的效果。我们这就通过表面着色器函数(surface shader function)来计算每个顶点(per-vertex)变化。同样需要使用编译vertex:functionName这个函数指令。但是这次我们传入两个参数: inout appdata_full 和 out Input o 。在这里你能传入任何一个属于输入结构(input structure)的成员,而不是内置的值。
下面的例子定义了一个自定义的float3 customColor成员,它将在顶点函数(vertex function)中运算:
Shader "Example/Custom Vertex Data" {
Properties {
_MainTex ("Texture", 2D) = "white" {}
}
SubShader {
Tags { "RenderType" = "Opaque" }
CGPROGRAM
#pragma surface surf Lambert vertex:vert
struct Input {
float2 uv_MainTex;
float3 customColor;
};
void vert (inout appdata_full v, out Input o) {
UNITY_INITIALIZE_OUTPUT(Input,data);
o.customColor = abs(v.normal);
}
sampler2D _MainTex;
void surf (Input IN, inout SurfaceOutput o) {
o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
o.Albedo *= IN.customColor;
}
ENDCG
}
Fallback "Diffuse"
}
注解:
这个例子是用来渲染颜色的:
取一个颜色值,float3,对应RGB。
较前个例子,多一个Input类型的参数,只为输出使用。
RGB颜色值当然只能为正值,所以使用绝对值去取normal的值。
在原先已经渲染上texture颜色值的基础上,加上这层自定义的颜色值。
在这个例子里customColor值设置的是法线(normal)的绝对值。
image
计算所有每个顶点(per-vertex)数据可以有更多的用途。它不是提供内置的输入结构(input structure)内的变量,或者优化着色器计算。例如:它可以计算在对象的顶点((vertex)上计算边缘光照(Rim lighting),而不是在表面着色器(surface shader)的每个像索(per-pixel)内做。
Final Color Modifier 最终颜色修改
这可以使用了一个"最终颜色修改(final color modifier)"函数,这个函数将通过着色器计算变化的最终颜色。为了实现它要使用finalcolor:functionName这个表面着色器(surface shader)编译命令。这个函数传入的参数是Input IN, SurfaceOutput o, inout fixed4 color 。
下面是一个简单的着色器(shader),它适用于给最终颜色着色。这是一个特别的仅适用于给表面反射率的颜色(surface Albedo color)着色。 这个色调也会影响任何颜色的光照贴图(lightmap), 光照探测(light probes)和类似的特别资源。
Shader "Example/Tint Final Color" {
Properties {
_MainTex ("Texture", 2D) = "white" {}
_ColorTint ("Tint", Color) = (1.0, 0.6, 0.6, 1.0)
}
SubShader {
Tags { "RenderType" = "Opaque" }
CGPROGRAM
#pragma surface surf Lambert finalcolor:mycolor
struct Input {
float2 uv_MainTex;
};
fixed4 _ColorTint;
void mycolor (Input IN, SurfaceOutput o, inout fixed4 color){
color *= _ColorTint;
}
sampler2D _MainTex;
void surf (Input IN, inout SurfaceOutput o) {
o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
}
ENDCG
}
Fallback "Diffuse"
}
image注解:
这个例子是跟上面例子的对比,前种使用普通反射进行叠加上颜色,此处则是直接使用finalcolor对其颜色进行处理,这种可以处理整个模型的固定颜色值的渲染。以下做简要的分析:
1.finalcolor:mycolor :这个是另一种可选参数,就是用户自定义的颜色处理函数。函数名为mycolor.
2.mycolor函数:注意到函数除了有surf的两个参数外,还多了个颜色参数,这个颜色参数就是当前模型上颜色对象,对他的更改将直接影响全部来自于lightmap,light probe和一些相关资源的颜色值。
Custom Fog with Final Color Modifier 自定义雾效与最终颜色修改
在共用最终颜色修改(final color modifier)的情况下将完全实现自定义雾效。雾效需要受到最终计算像索的着色器颜色的影响。这正是finalcolor做的。
下面这个着色器适用于给基于屏幕中心距离远近的雾效来着色。这结合了顶点修改(vertex modifier)与自定义顶点数据(custom vertex data)(雾效)还有最终颜色修改(final color modifier)。当附加到正向渲染(forward render)通道(pass)中使用时,雾效(Fog)需要淡入淡出黑色。在这个例子中控制它还不如检查 UNITY_PASS_FORWARDADD。
Shader "Example/Fog via Final Color" {
Properties {
_MainTex ("Texture", 2D) = "white" {}
_FogColor ("Fog Color", Color) = (0.3, 0.4, 0.7, 1.0)
}
SubShader {
Tags { "RenderType" = "Opaque" }
CGPROGRAM
#pragma surface surf Lambert finalcolor:final vertex:vert
struct Input {
float2 uv_MainTex;
half fog;
};
void vert (inout appdata_full v, out Input data){
UNITY_INITIALIZE_OUTPUT(Input,data);
float4 hpos = mul (UNITY_MATRIX_MVP, v.vertex);
data.fog = min (1, dot (hpos.xy, hpos.xy) * 0.1);
}
sampler2D _MainTex;
void surf (Input IN, inout SurfaceOutput o) {
o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
}
fixed4 _FogColor;
void final (Input IN, SurfaceOutput o, inout fixed4 color){
fixed3 fogColor = _FogColor.rgb;
#ifdef UNITY_PASS_FORWARDADD
fogColor = 0;
#endif
color.rgb = lerp (color.rgb, fogColor, IN.fog);
}
ENDCG
}
Fallback "Diffuse"
}
image注解:
mul是矩阵相乘的函数。UNITY_MATRIX_MVP是model、view、projection三个矩阵相乘出来的4x4的 矩阵。v.vertex是一个float4的变量,可理解成4x1的矩阵,两者相乘,则得出一个float4,这个值就是视角窗口的坐标值,这个坐标就跟 camera的关联了。
这个fog的浮点值就是其强度,范围一般在-1到1之间,说一般,只是我个人建议的值,设成其他也行,只是没多大意义。越负就越黑。再 看后面这个点积,这个仔细一想,不难理解,其实就是为了达到一种扩散的效果,因此两个一样的向量相乘,其实就是直接对坐标做平方扩展,这样fog就更有雾 的感觉。
这个宏不好找,就看官方对这个例子的解释为正向渲染时的额外通道。字面不好理解,多多尝试过可以有所发现,其实就是在雾气渐渐消失处那 块额外的渲染区。可以将fogColor = 0; 改成fogColor = fixed3(1,0,0)。外面雾气颜色再选成白色,雾气改成绿色后,效果则如下.
lerp函数是个有趣的函数。第一个参数是左边界,第二个参数是右边界,第三个相当于一个值介于0到1之间的游标。游标为0,则为左边 界,为1为右边界,取中间值则是以此类推,取插值。其实也可以把它看成百分比。这里的fog则可以看来那个游标,值越大,则越接近fogColor,越小 越接近原色。