如果你还未了解过Shader lab 建议先了解一下Shader Lab 相关内容:跳转接口。
编写与光照相互做的着色器是很复杂的,有不同的灯光类型,不同的阴影选项,不同的渲染路径,着色需要以某种方式处理所有的复杂性。
Unity中的Surface Shader是一种代码生成方法,它使编写着色比使用顶点/片段着色器要容易的多,Surface Shader只是生成所有需要手工编写的重复性的代码,仍然需要使用HLSL来编写着色器。
Surface Shader 运作原理:
定义一个“surface function”(表面处理函数),他会将我们需要的数据放入Input 结构中,然后在方法内填充输出结构"SurfaceOutput"结构。然后Surface Shader会计算出所需的输入、填充输出等操作,并生成实际的顶点/片段着色,以及创建处理Foward Rending和Deferred shadering渲染路径的渲染通道(Pass)。
例如:
//...
#pragma surface surf Lambert
struct Input
{
//...
}
void surf(Input i,inout SurfaceOutput o)
{
o.Albedo =1;
//...
}
//...
上面就用#pragma surface 定义了一个surf表面函数,并且使用Lambert光照模型,定义了Input结构体,并在surf 方法中对输出结构体SurfaceOutput进行了填充
Surface Shader 编译指令:
Surface Shader 需要在CGPROGRAM和ENDCG块内编写,并且需要注意以下内容:
1.必须放在SubShder块内(注意不是Pass内,表面着色器本身将编译为多个通道)
2.使用#pragma surface ...指令来指示当前shader为Surface Shader。
#pragma surface指令格式:
#pragma surface surfaceFunction lightModel [optionalparams]
指令类型 | 指令 | 描述 |
---|---|---|
必须参数 | surfaceFunction | 是一个CG函数,并包含表面着色器代码,格式:void surf(Input IN,inout SurfaceOutput o),input为自定义的结构,应包含表面函数所需要的任何纹理坐标和额外的自动变量 |
— | lightModel | 要使用的照明模型,内置的有基于物理的Standard和StandardSpecular,以及简单的非基于物理的Lambert、BlinnPhong以及自定义光照模型 |
— | ||
— | StandardSpecular:使用SurfaceOutputStandardSpecular输出结构,并匹配Unity中的Standard(Specular setup)着色器 | |
— | Lamber和BlinnPhong照明模型不是基于物理的,但使用它们的着色器可以更快地在低端硬件上渲染 | |
可选参数 | 透明或透明度测试 | 透明度,透明度通常可以有两种:alpha混合(用于淡出物体)和物理上更合理地"预乘混合"(premultiplied blending,运行半透明表面保留适当地镜面反射)。 |
— | alpha或alpha:auto | 将为简单地照明方法选择淡入透明(与alpha:fade相同),为基于物理的照明方法选择欲乘透明度(与alpha:premul相同)。 |
— | alpha:blend | 启用alpha混合。 |
— | alpha:fade | 实现传统地淡入淡出 |
— | alpha:premul | 启用预乘alpha透明度 |
— | alphatest | 透明度测试,启用alpha剪切透明度。截止使用variableName的浮点数变量,还可以使用addshadow指令来生成正确的阴影。 |
— | keepalpha | 默认情况下,不透明表面着色器将1.0写入alpha通道,无论无论输出结构的alpha值是什么,或者照明功能返回什么。使用此选项可以保持照明功能的alpha值,即使对于不透明的表面着色器也是如此。 |
— | decal:add | 添加贴花着色器(例如地形addpass)。适用于位于其他表面顶部的物体,并使用添加进行混合。 |
— | decal:blend | 半透明贴花着色器,适用于位于其他表面顶部的对象,并使用alpha混合。 |
— | 自定义修改器函数 | 可用于更改或计算传入的顶点数据,或更改最终计算的片段颜色 |
— | vertex:VertexFunction | 自定义顶点修改功能,在生成的顶点着色器的开始处调用此函数,并且可以修改或计算每顶点数据 |
— | finalcolor:ColorFunction | 自定义最终颜色修改功能 |
— | finalgbuffer:ColorFunction | 用于更改gbuffer内容的自定以延迟路径 |
— | finalprepass:ColorFunction | 自定以预通道基本路径 |
— | 阴影和曲面细分 | 可以提供其他指令来控制和曲面细分的处理方式 |
— | addshadow | 生成阴影投射渲染通道(Pass),通常用于自定义顶点修改,以便阴影投射也可以获得任何程序顶点动画。通常着色器不需要任何特殊的阴影处理,因为它们可以使用指定fallback来投射阴影 |
— | fullforwardshadow | 支持Forward Rendering路径中的所有光影类型。默认情况下,着色器仅支持Forward Rendering中一个平行光的阴影(节省内部着色器变量计数)。如果在Forward Rendering中需要点光源或聚光灯阴影,可以使用此指令 |
— | tessellate:TessFunction | 使用DX11 GPU细分。该函数计算曲面细分因子。 |
— | 代码生成选项 | 默认情况下,生成的表面着色器代码会尝试处理所有可能的光照/阴影/光照贴图,但是在某些情况下,是不需要其中的某些,并且可以调整生成的代码以跳过它们,以便生成更小的着色器提升加载速度。 |
— | exclude_path:deferred,exclude_path:forward,exclude_path:prepass | 不产生对于给定的渲染路径渲染通道(Pass)(延迟着色,正向渲染和传统延迟照明) |
— | noshadow | 禁用此着色器中的所有阴影接收支持。 |
— | noambient | 请勿使用任何环境照明或光探头。 |
— | novertexlights | 请勿在向前渲染中应用任何光探测器或每顶点光源。 |
— | nolightmap | 禁用此着色器中的所有光照贴图支持。 |
— | nodynlightmap | 在此着色器中禁用运行时动态全局照明支持。 |
— | nodirlightmap | 禁用此着色器中的方向光照贴图支持。 |
— | nofog | 禁用所有内置的雾支持。 |
— | nometa | 不生成"meta“通道(由光照贴图和动态全局照明用于提取表面信息)。 |
— | moforwardadd | 禁用Forward add 渲染通道,这使得着色支持一个全方向光,所有其他的光源按照顶点/SH的方式计算,可以使着色器更小 |
— | 其他 | |
— | softvegetation | 仅在启用”Soft Vegetation“时才渲染表面着色器 |
— | interpolateview | 在顶点着色器中计算视图方向并进行插值,而不是在像素着色器中计算。这样可以使像素着色器更快,但需要消耗一个纹理插值器 |
— | halfasview | 将半向方向矢量传递到光照函数而不是试图方向,每个顶点将计算并归一化半方向。这样会更快,但不完全正确 |
— | dualforward | 在forward rendering中使用双光照贴图 |
InputStruct(输入结构):
输入结构通常具有着色器所需要的任何纹理坐标。纹理坐标必须命名为"uv",后跟纹理名称(或”uv2“开头,使用第二个纹理坐标)(例如:uv_MainTex 或 uv2_MainTex);
其他可以放入输入结构的值有:
指令 | 描述 |
---|---|
float3 viewDir | 包含视角方向,可用于计算边缘光照等效果 |
float4 变量名:Color | 包含差值后逐顶点颜色 |
float4 screenPos | 包含了屏幕空间的坐标,可用于反射或屏幕特效。注意,这不适合GrabPass;需要使用ComputeGrabScreenPos函数自己计算定义UV。 |
float3 worldPos | 包含世界空间位置。 |
float3 worldRefl | 如果没有修改o.Normal,则包含世界空间下的反射向量。 |
float3 worldNormal | 如果修改o.Normal,则包含世界空间下的法线向量。 |
float3 worldRefl;INTERNAL_DATA | 如果修改了o.Normal,需要使用该变量告诉Unity要基于修改后的法线计算世界空间下的反射向量。可以使用WorldReflectionVector(IN,o.Normal)来得到世界空间下的反射方向。 |
float3 worldNormal;INTERNAL_DATA | 如果修改了o.normal,需要使用该变量告诉Unity要基于修改后的法线计算世界空间下的法线方向。可以使用WorldNormalVector(IN,o.Normal)来得到世界空间下的法线方向 |
SurfaceOutput结构:
SurfaceOutput基本上描述了平面的属性(如albedo、normal、emission、specularity等)
标准输出结构如下:
struct SurfaceOutput
{
fixed3 Albedo; //漫反射
fixed3 Normal; //正切空间法线
fixed3 Emission; //自发光
half Specular; //高光率 0-1
fixed Gloss; // 高光强度
fixed Alpha; // 透明度
};
在Unity中还可以使用基于物理的照明模型,内置Standard和StandardSpecular模型分别使用下面这些输出结构:
struct SurfaceOutputStandard
{
fixed3 Albedo; //漫反射颜色
fixed3 Normal; // 切线空间法线
half3 Emission; //自发光颜色
half Metallic; // 金属 0 - 1
half Smoothness; // 平滑度0-1
half Occlusion; // occlusion (default 1)
fixed Alpha; // 透明度
};
struct SurfaceOutputStandardSpecular
{
fixed3 Albedo; // 漫反射颜色
fixed3 Specular; // 高光颜色
fixed3 Normal; // 切线空间法线
half3 Emission; //自发光颜色
half Smoothness; //平滑度0-1
half Occlusion; // occlusion (default 1)
fixed Alpha; // 透明度
};
实例:
1.简单的着色器:
Shader "Example/DiffuseSimple"
{
SubShader
{
Tags{"RenderType" = "Opaque"}
CGPROGRAM
#pragma surface surf Lambert
struct Input
{
fixed4 color : COLOR;
};
void surf(Input IN, inout SurfaceOutput o)
{
o.Albedo = 1;
}
ENDCG
}
FallBack "Diffuse"
}
//在上述着色器中,声明找色器为表面找色器(#pragma surface) ,定义了表面函数(surf)以及指定基本光照为Lambert。
//声明输入结构(Input) 包含顶点颜色。
//在表面方法(surf)中,设置输出函数的漫反射颜色为1(o.Albedo = 1;)
//如果此着色器在硬件上没法使用的话会使用Diffuse着色器进行替换
2.Texture(纹理):
Shader "Example/Texture"
{
Properties
{
_MainTex("MainTex",2D) = ""{}
}
SubShader
{
Tags{"RenderType" = "Opaque"}
CGPROGRAM
#pragma surface surf Lambert
struct Input //获取_MainTex的第一套纹理坐标
{
float2 uv_MainTex;
};
//注意 在Properties中声明的属性,需要在CGPROGRAM块内声明一个同样名称且类型匹配的变量来进行关联
sampler2D _MainTex;
void surf(Input IN, inout SurfaceOutput o)
{
//根据Input中纹理坐标匹配_MainTex的纹理设置漫反射颜色
o.Albedo = tex2D(_MainTex, IN.uv_MainTex);
}
ENDCG
}
FallBack "Diffuse"
}
3.Normal(法线):
Shader "Example/DiffuseBump"
{
Properties
{
_MainTex("MainTex",2D) = ""{}
// 定义法线贴图
_BumpTex("BumpTex",2D) = ""{}
}
SubShader
{
Tags{"RenderType"="Opaque"}
CGPROGRAM
#pragma surface surf Lambert
struct Input
{
float2 uv_MainTex;
//法线贴图纹理坐标
float2 uv_BumpTex;
};
sampler2D _MainTex;
sampler2D _BumpTex;
void surf(Input IN, inout SurfaceOutput o)
{
o.Albedo = tex2D(_MainTex, IN.uv_MainTex);
//设置法线
o.Normal = UnpackNormal(tex2D(_BumpTex, IN.uv_BumpTex));
}
ENDCG
}
FallBack "Diffuse"
}
4.Rim Light(边缘照明):
Shader "Example/RimLight"
{
Properties
{
_MainTex("MainTex",2D) = ""{}
_BumpTex("BumpTex",2D) = ""{}
_RimColor("Rim Color",Color) = (1,1,1,1)
_RimPower("Rim Power",Range(0,8)) = 0
}
SubShader
{
Tags{"RenderType" = "Opaque"}
CGPROGRAM
#pragma surface surf Lambert
sampler2D _MainTex;
sampler2D _BumpTex;
float4 _RimColor;
float _RimPower;
struct Input
{
float2 uv_MainTex;
float2 uv_BumpTex;
float3 viewDir;//视角方向
};
void surf(Input IN, inout SurfaceOutput o)
{
o.Albedo = tex2D(_MainTex, IN.uv_MainTex);
o.Normal = UnpackNormal(tex2D(_BumpTex, IN.uv_BumpTex));
half rim = 1.0 - saturate(dot(normalize(IN.viewDir), o.Normal));
o.Emission = _RimColor.rgb*pow(rim, _RimPower);
}
ENDCG
}
}