一、Surface Output (表面着色器的标准输出结构)
Surface Shader的标准输出结构-第一要素
定义一个“表面函数(surface function)”,需要输入相关的UV或数据信息,并在输出结构中填充SurfaceOutput。SurfaceOutput基本上描述了表面的特性(光照的颜色反射率、法线、散射、镜面等)。其实还是需要用CG或者HLSL编写此部分的代码。
通过表面着色器(Surface Shader)来编译这段CG或者HLSL代码的,然后计算出需要填充输入什么,输出什么等相关信息,并产生真实的顶点(vertex)&像素(pixel)着色器,以及把渲染路径传递到正向或延时渲染路径。
Surface Shader的标准输出结构:
struct SurfaceOutput
{
half3 Albedo; //反射率,也就是纹理颜色值(r,g,b)
half3 Normal; //法线,法向量(x, y, z)
half3 Emission; //自发光颜色值(r, g,b)
half Specular; //镜面反射度
half Gloss; //光泽度
half Alpha; //透明度
};
ps:Albedo是half3类型的。所以o.Albedo = 0.6和o.Albedo = float3(0.6,0.6,0.6)是等价的。
二、表面着色器的编译指令
表面着色器的编译指令为编写表面着色器的第二个要素。
表面着色器放在CGPROGRAM .. ENDCG块里面,就像其他的着色器一样。区别是:
其必须嵌在子着色器(SubShader)块里面。而不是Pass块里面。因为表面着色器( Surface shader)将在多重通道(multiple passes)内编译自己,而不是放在某个Pass中。
我们甚至可以这样说,如果你写表面着色器,用不到Pass代码块,一般直接在SubShader块中完成就行了。
使用的 #pragma surface...指令,以声明这是一个表面着色器。指令的句法是:
#pragma surface surfaceFunction lightModel[optionalparams]
所需参数:
surfaceFunction - 表示指定名称的Cg函数中有表面着色器(surface shader)代码。这个函数的格式:void surf (Input IN,inout SurfaceOutput o), 其中Input是我们自己定义的结构。Input结构中应该包含所需的纹理坐标(texture coordinates)和表面函数(surfaceFunction)所需要的额外的必需变量。
lightModel -使用的光照模式。内置的是Lambert (diffuse)和 BlinnPhong (specular)两种,一般习惯用Lambert,也就是兰伯特光照模式。
进阶可选参数:
alpha -透明( Alpha)混合模式。使用它可以写出半透明的着色器。
alphatest:VariableName -透明( Alpha)测试模式。使用它可以写出 镂空效果的着色器。镂空大小的变量(VariableName)是一个float型的变量。
vertex:VertexFunction - 自定义的顶点函数(vertex function)。相关写法可参考Unity内建的Shader:树皮着色器(Tree Bark shader),如Tree Creator Bark、Tree Soft Occlusion Bark这两个Shader。
finalcolor:ColorFunction - 自定义的最终颜色函数(final color function)。
exclude_path:prepass 或者 exclude_path:forward - 使用指定的渲染路径,不需要生成通道。
addshadow - 添加阴影投射 & 收集通道(collector passes)。通常用自定义顶点修改,使阴影也能投射在任何程序的顶点动画上。
dualforward - 在正向(forward)渲染路径中使用双重光照贴图(dual lightmaps)。
fullforwardshadows - 在正向(forward)渲染路径中支持所有阴影类型。
decal:add - 添加贴图着色器(decal shader) (例如: terrain AddPass)。
decal:blend - 混合半透明的贴图着色器(Semitransparent decal shader)。
softvegetation - 使表面着色器(surface shader)仅能在Soft Vegetation打开时渲染。
noambient - 不适用于任何环境光照(ambient lighting)或者球面调和光照(spherical harmonics lights)。
novertexlights - 在正向渲染(Forward rendering)中不适用于球面调和光照(spherical harmonics lights)或者每个顶点光照(per-vertex lights)。
nolightmap - 在这个着色器上禁用光照贴图(lightmap)。(适合写一些小着色器)
nodirlightmap - 在这个着色器上禁用方向光照贴图(directional lightmaps)。 (适合写一些小着色器)。
noforwardadd - 禁用正向渲染添加通道(Forward rendering additive pass)。 这会使这个着色器支持一个完整的方向光和所有光照的per-vertex/SH计算。(也是适合写一些小着色器).
approxview - 着色器需要计算标准视图的每个顶点(per-vertex)方向而不是每个像索(per-pixel)方向。 这样更快,但是视图方向不完全是当前摄像机(camera) 所接近的表面。
halfasview - 在光照函数(lighting function)中传递进来的是half-direction向量,而不是视图方向(view-direction)向量。 Half-direction会计算且会把每个顶点(per vertex)标准化。这样做会提高执行效率,但是准确率会打折扣。
ps:可以在 CGPROGRA内编写 #pragma debug,然后表面编译器(surface compiler)会进行解释生成代码。
三、Input Structure(表面着色器输入结构)
表面着色器书写的第三个要素是指明表面输入结构(Input Structure)。
Input 这个输入结构通常拥有着色器需要的所有纹理坐标(texture coordinates)。纹理坐标(Texturecoordinates)必须被命名为“uv”后接纹理(texture)名字。(或者uv2开始,使用第二纹理坐标集)。
可选附加候选值:
float3 viewDir - 视图方向( view direction)值。为了计算视差效果(Parallax effects),边缘光照(rim lighting)等,需要包含视图方向( view direction)值。
float4 with COLOR semantic -每个顶点(per-vertex)颜色的插值。
float4 screenPos - 屏幕空间中的位置。 为了反射效果,需要包含屏幕空间中的位置信息。比如在Dark Unity中所使用的 WetStreet着色器。
float3 worldPos - 世界空间中的位置。
float3 worldRefl - 世界空间中的反射向量。如果表面着色器(surface shader)不写入法线(o.Normal)参数,将包含这个参数。 请参考这个例子:Reflect-Diffuse 着色器。
float3 worldNormal - 世界空间中的法线向量(normal vector)。如果表面着色器(surface shader)不写入法线(o.Normal)参数,将包含这个参数。
float3 worldRefl; INTERNAL_DATA - 世界空间中的反射向量。如果表面着色器(surface shader)不写入法线(o.Normal)参数,将包含这个参数。为了获得基于每个顶点法线贴图( per-pixel normal map)的反射向量(reflection vector)需要使用世界反射向量(WorldReflectionVector (IN, o.Normal))。请参考这个例子: Reflect-Bumped着色器。
float3 worldNormal; INTERNAL_DATA -世界空间中的法线向量(normal vector)。如果表面着色器(surface shader)不写入法线(o.Normal)参数,将包含这个参数。为了获得基于每个顶点法线贴图( per-pixel normal map)的法线向量(normal vector)需要使用世界法线向量(WorldNormalVector (IN, o.Normal))。
四、一些CG函数
4.1 UnpackNormal()函数
UnpackNormal接受一个fixed4的输入,并将其转换为所对应的法线值(fixed3),并将其赋给输出的Normal,就可以参与到光线运算中完成接下来的渲染工作了。
4.2 saturate()函数
saturate的字面解释是浸湿,浸透。其作用是将取值转化为[0,1]之内的一个值。
可选原型:
float saturate(float x);
float1 saturate(float1 x);
float2 saturate(float2 x);
float3 saturate(float3 x);
float4 saturate(float4 x);
half saturate(half x);
half1 saturate(half1 x);
half2 saturate(half2 x);
half3 saturate(half3 x);
half4 saturate(half4 x);
fixed saturate(fixed x);
fixed1 saturate(fixed1 x);
fixed2 saturate(fixed2 x);
fixed3 saturate(fixed3 x);
fixed4 saturate(fixed4 x);
其唯一的一个参数x表示矢量或者标量的饱和值(Vector or scalar to saturate.),也就是将这个x转化为[0,1]之内的值。
其返回值:
如果x取值小于0,则返回值为0.
如果x取值大于1,则返回值为1.
若x在0到1之间,则直接返回x的值。
4.3 dot()函数
高等数学中的点积操作,用于返回两个向量的标量积。
可选原型:
float dot(float a, float b);
float dot(float1 a, float1 b);
float dot(float2 a, float2 b);
float dot(float3 a, float3 b);
float dot(float4 a, float4 b);
half dot(half a, half b);
half dot(half1 a, half1 b);
half dot(half2 a, half2 b);
half dot(half3 a, half3 b);
half dot(half4 a, half4 b);
fixed dot(fixed a, fixed b);
fixed dot(fixed1 a, fixed1 b);
fixed dot(fixed2 a, fixed2 b);
fixed dot(fixed3 a, fixed3 b);
fixed dot(fixed4 a, fixed4 b);
4.4 tex2D()函数
CG中用于2D纹理采样的tex2D函数的用法。
可选原型:
float4 tex2D(sampler2D samp, float2 s)
float4 tex2D(sampler2D samp, float2 s, inttexelOff)
float4 tex2D(sampler2D samp, float3 s)
float4 tex2D(sampler2D samp, float3 s, inttexelOff)
float4 tex2D(sampler2D samp, float2 s,float2 dx, float2 dy)
float4 tex2D(sampler2D samp, float2 s,float2 dx, float2 dy, int texelOff)
float4 tex2D(sampler2D samp, float3 s,float2 dx, float2 dy)
float4 tex2D(sampler2D samp, float3 s,float2 dx, float2 dy, int texelOff)
int4 tex2D(isampler2D samp, float2 s)
int4 tex2D(isampler2D samp, float2 s, inttexelOff)
int4 tex2D(isampler2D samp, float2 s,float2 dx, float2 dy)
int4 tex2D(isampler2D samp, float2 s,float2 dx, float2 dy, int texelOff)
unsigned int4 tex2D(usampler2D samp, float2s)
unsigned int4 tex2D(usampler2D samp, float2s, int texelOff)
unsigned int4 tex2D(usampler2D samp, float2s, float2 dx, float2 dy)
unsigned int4 tex2D(usampler2D samp, float2s, float2 dx, float2 dy,int texelOff)
参数简介:
samp-需要查找的采样对象,就是在这里填个纹理对象。
s-需进行查找的纹理坐标。
dx-预计算的沿X轴方向的导数。
dy-预计算的沿Y轴方向的导数。
texelOff-添加给最终纹理的偏移量
其返回值,是查找到的纹理。
五、光照模型的概念
5.1 漫反射
环境光是对光照现像的最简单抽象,因而局限性很大。它仅能描述光线在空间中无方向并均匀散布时的状态。很多情况下,入射光是带有方向的,比如典型的阳光。
如果光照射到比较粗糙的物体表面,如粉笔,由于这些表面从各个方向等强度地反射光,因而从各个视角出发,物体表面呈现相同的亮度,所看到的物体表面某点的明暗程度不随观测者的位置变化的,这种等同地向各个方向散射的现象称为光的漫反射(diffuse reflection)。
简单光照模型模拟物体表面对光的反射作用。光源被假定为点光源,其几何形状为一个点,向周围所有方向上辐射等强度的光,在物体表面产生反射作用。
如图:
5.2 Lambert模型
漫反射光的强度近似地服从于Lambert定律,即漫反射光的光强仅与入射光的方向和反射点处表面法向夹角的余弦成正比。
由此可以构造出Lambert漫反射模型:
Idiffuse =Id Kd cosθ
Idiffuse表示物体表面某点的漫反射光强
Id为点光源,Kd(0<Kd<1)表示物体表面该点对漫反射光的反射属性
θ是入射光线的方向与物体表面该点处法线N的夹角,或称为入射角(0≤θ≤90°)
入射角为零时,说明光线垂直于物体表面,漫反射光强最大;
90°时光线与物体表面平行,物体接收不到任何光线。
如图:
把环境光模型添加进来,最后,Lambert光照模型可写为:
I= IaKa + Id Kdcosθ= IaKa + Id Kd(L·N)
该模型包含环境光和漫反射光。
5.3 镜面反射
Lambert模型较好地表现了粗糙表面上的光照现象,如石灰粉刷的墙壁、纸张等
但在用于诸如金属材质制成的物体时,则会显得呆板,表现不出光泽,主要原因是该模型没有考虑这些表面的镜面反射效果。
如果光照射到相当光滑的表面,就产生镜面反射(specular reflection),镜面反射的特点是在光滑表面会产生一块称之为高光(high light)的特亮区域 。
镜面反射遵循光的反射定律:反射光与入射光位于表面法向两侧,对理想反射面(如镜面),入射角等于反射角,观察者只能在表面法向的反射方向一侧才能看到反射。
5.4 Phong光照模型
Lambert模型能很好的表示粗糙表面的光照,但不能表现出镜面反射高光。1975年Phong Bui Tong发明的Phong模型,提出了计算镜面高光的经验模型,镜面反射光强与反射光线和视线的夹角a相关:
Ispecular = KsIs(cos a) n
其中Ks为物体表面的高光系数,Is为光强,a是反射光与视线的夹角,n为高光指数,n越大,则表面越光滑,反射光越集中,高光范围越小。如果V表示顶点到视点的单位向量,R表示反射光反向,则cos a可表示为V和R的点积。模型可表示为:
Ispecular = KsIs(V●R) n
反射光放向R可由入射光放向L(顶点指向光源)和物体法向量N求出。
R = (2N●L)N – L
Phong光照的另一种表现形式:
Ispec = IsKscosns α(α∈(0,90º))
Ks为物体表面某点的高亮光系数
ns为物体表面的镜面反射指数,反映了物体表面的光泽程度, ns越大,表示物体越接近于镜面。
只有视线与光源在物体表面的反射光线非常接近时才能看到镜面反射光,此时,镜面反射光将会在反射方向附近形成很亮的光斑,称为高光现象。ns越小,表示物体越粗糙;当ns为零时,镜面反射光便退化为与视点、光源均无关的环境光。
将镜面反射光与环境光、漫反射光叠加起来形成单一光源照射下更为真实的Phong光照模型:
I = Ia Ka+IdKdcosθ+IsKscosns α
θ :入射角
α :视线与镜面反射方向的夹角
5.5 Blinn-Phong光照模型
Jim Blinn对Phong模型进行了改进,提出了Blinn-Phong模型。Blinn-Phong模型与Phong模型的区别是,把dot(V,R)换成了dot(N,H),其中H为半角向量,位于法线N和光线L的角平分线方向。Blinn-Phong模型可表示为:
Ispecular = KsIs pow(( dot(N,H), n )
其中H = (L + V) / | L+V |,计算H比计算反射向量R更快速。
Unity中,Phong实际上指的就是Blinn-Phong,两者指的同一种内置光照模型。
PS:光照模型部分的内容主要参考为:http://cg.sjtu.edu.cn/
六、custom lighting model(自定义光照模型)
光照模式(lighting model)无外乎是几个Cg/HLSL函数的组合。
Unity内置的 Lambert 和 Blinn-Phong模型的Shader源代码
#ifndef LIGHTING_INCLUDED
#define LIGHTING_INCLUDED
struct SurfaceOutput {
fixed3 Albedo;
fixed3 Normal;
fixed3 Emission;
half Specular;
fixed Gloss;
fixed Alpha;
};
#ifndef USING_DIRECTIONAL_LIGHT
#if defined (DIRECTIONAL_COOKIE) || defined (DIRECTIONAL)
#define USING_DIRECTIONAL_LIGHT
#endif
#endif
// NOTE: you would think using half is fine here, but that would make
// Cg apply some precision emulation, making the constants in the shader
// much different; and do some other stupidity that actually increases ALU
// count on d3d9 at least. So we use float.
//
// Also, the numbers in many components should be the same, but are changed
// very slightly in the last digit, to prevent Cg from mis-optimizing
// the shader (it tried to be super clever at saving one shader constant
// at expense of gazillion extra scalar moves). Saves about 6 ALU instructions
// on d3d9 SM2.
#define UNITY_DIRBASIS \
const float3x3 unity_DirBasis = float3x3( \
float3( 0.81649658, 0.0, 0.57735028), \
float3(-0.40824830, 0.70710679, 0.57735027), \
float3(-0.40824829, -0.70710678, 0.57735026) \
);
inline half3 DirLightmapDiffuse(in half3x3 dirBasis, fixed4 color, fixed4 scale, half3 normal, bool surfFuncWritesNormal, out half3 scalePerBasisVector)
{
half3 lm = DecodeLightmap (color);
// will be compiled out (and so will the texture sample providing the value)
// if it's not used in the lighting function, like in LightingLambert
scalePerBasisVector = DecodeLightmap (scale);
// will be compiled out when surface function does not write into o.Normal
if (surfFuncWritesNormal)
{
half3 normalInRnmBasis = saturate (mul (dirBasis, normal));
lm *= dot (normalInRnmBasis, scalePerBasisVector);
}
return lm;
}
fixed4 _LightColor0;
fixed4 _SpecColor;
inline fixed4 LightingLambert (SurfaceOutput s, fixed3 lightDir, fixed atten)
{
fixed diff = max (0, dot (s.Normal, lightDir));
fixed4 c;
c.rgb = s.Albedo * _LightColor0.rgb * (diff * atten * 2);
c.a = s.Alpha;
return c;
}
inline fixed4 LightingLambert_PrePass (SurfaceOutput s, half4 light)
{
fixed4 c;
c.rgb = s.Albedo * light.rgb;
c.a = s.Alpha;
return c;
}
inline half4 LightingLambert_DirLightmap (SurfaceOutput s, fixed4 color, fixed4 scale, bool surfFuncWritesNormal)
{
UNITY_DIRBASIS
half3 scalePerBasisVector;
half3 lm = DirLightmapDiffuse (unity_DirBasis, color, scale, s.Normal, surfFuncWritesNormal, scalePerBasisVector);
return half4(lm, 0);
}
// NOTE: some intricacy in shader compiler on some GLES2.0 platforms (iOS) needs 'viewDir' & 'h'
// to be mediump instead of lowp, otherwise specular highlight becomes too bright.
inline fixed4 LightingBlinnPhong (SurfaceOutput s, fixed3 lightDir, half3 viewDir, fixed atten)
{
half3 h = normalize (lightDir + viewDir);
fixed diff = max (0, dot (s.Normal, lightDir));
float nh = max (0, dot (s.Normal, h));
float spec = pow (nh, s.Specular*128.0) * s.Gloss;
fixed4 c;
c.rgb = (s.Albedo * _LightColor0.rgb * diff + _LightColor0.rgb * _SpecColor.rgb * spec) * (atten * 2);
c.a = s.Alpha + _LightColor0.a * _SpecColor.a * spec * atten;
return c;
}
inline fixed4 LightingBlinnPhong_PrePass (SurfaceOutput s, half4 light)
{
fixed spec = light.a * s.Gloss;
fixed4 c;
c.rgb = (s.Albedo * light.rgb + light.rgb * _SpecColor.rgb * spec);
c.a = s.Alpha + spec * _SpecColor.a;
return c;
}
inline half4 LightingBlinnPhong_DirLightmap (SurfaceOutput s, fixed4 color, fixed4 scale, half3 viewDir, bool surfFuncWritesNormal, out half3 specColor)
{
UNITY_DIRBASIS
half3 scalePerBasisVector;
half3 lm = DirLightmapDiffuse (unity_DirBasis, color, scale, s.Normal, surfFuncWritesNormal, scalePerBasisVector);
half3 lightDir = normalize (scalePerBasisVector.x * unity_DirBasis[0] + scalePerBasisVector.y * unity_DirBasis[1] + scalePerBasisVector.z * unity_DirBasis[2]);
half3 h = normalize (lightDir + viewDir);
float nh = max (0, dot (s.Normal, h));
float spec = pow (nh, s.Specular * 128.0);
// specColor used outside in the forward path, compiled out in prepass
specColor = lm * _SpecColor.rgb * s.Gloss * spec;
// spec from the alpha component is used to calculate specular
// in the Lighting*_Prepass function, it's not used in forward
return half4(lm, spec);
}
#ifdef UNITY_CAN_COMPILE_TESSELLATION
struct UnityTessellationFactors {
float edge[3] : SV_TessFactor;
float inside : SV_InsideTessFactor;
};
#endif // UNITY_CAN_COMPILE_TESSELLATION
#endif
七、光照模式的声明方式
在Unity Shaderlab和CG语言中,光照模式是一个以Lighting开头+自定义文字组合在一起的函数。即,函数名为:
Lighting+ [自定义部分]
可以在着色器文件(shader file)或导入文件(included files)中的任何一个地方声明此函数。一般情况下,此函数有五种原型可供选择,具体如下:
1.half4 LightingName (SurfaceOutput s, half3 lightDir, half atten);
此种形式的函数可以表示在正向渲染路径(forward rendering path)中的光照模式,且此函数不取决于视图方向(view direction)。例如:漫反射(diffuse)。
2.half4 LightingName (SurfaceOutput s, half3 lightDir, half3 viewDir, half atten);
此种形式的函数可以表示在正向渲染路径(forward rendering path)中使用的光照模式,且此函数包含了视图方向(view direction)。
3.half4 LightingName_PrePass (SurfaceOutput s, half4 light);
此种形式的函数可以在延时光照路径(deferred lighting path)中使用的。
4.half4 LightingName_DirLightmap(SurfaceOutput s, fixed4 color, fixed4 scale, bool surfFuncWritesNormal);
这种形式也是不依赖于视图方向(view direction)的光照模式。例如:漫反射(diffuse)。
5.half4 LightingName_DirLightmap(SurfaceOutput s, fixed4 color, fixed4 scale, half3 viewDir, bool surfFuncWritesNormal,out half3 specColor);
这是使用的依赖于视图方向(view direction)的光照模式(light model)。
ps:对于形式四和形式五的选择,主要取决于光照模式(light model)是否依赖视图方向(view direction)。需要注意的是,这两个函数将自动处理正向和延时光照路径(forwardand deferred lighting rendering paths)。
ps: Unity在移动平台中暂时不支持延迟光照渲染。
ps:在自定义自己的光照模型函数时,根据需要在五种函数原型中选择一种,且:
光照模式的函数名为:Lighting+ [自定义函数名]
pragma声明为: #pragmasurface surf [自定义函数名]
然后就需要仿照着其他的光照模式来填充函数体