表面着色器的一部分背后内幕


请尊重原作者的工作,转载时请务必注明转载自:www.xionggf.com

有过HLSL/Cg编程经验的读者乍一看这段和HLSL/Cg着色器有点相同的代码,可能会有一些疑问:

  1. 没有看到有顶点着色器的入口函数,片元着色器的入口函数,究竟顶点是究竟怎样处理的,片元又是怎样处理
  2. struct Input貌似是一个描述顶点格式的数据结构,但数来数去,里面只有一个数据成员uv_MainTex,这个数据成员似乎是描述顶点所携带的纹理映射坐标,最基本的描述一个顶点的位置坐标哪里去了?
  3. #pragma surface surf Lambert noforwardadd 是什么东西?
  4. 从surf方法的实现来看,好像是一个处理片元的函数,它和Cg编程里面的片元着色器入口函数有没有什么关联?
  5. SurfaceOutput是一个怎样的结构,在哪里定义?它有什么数据成员,分别表示什么意思?

带着这些疑问,我们一步一步地分析。

首先,这段代码,是使用了Unity3D的ShaderLab语言,并且是使用了U3D特有的“表面着色器(surface shader)”去编写的。ShaderLab语言是U3D开发公司为了便于快捷开发着色器而自定义的一套着色器语言。它高度兼容nVidia公司的Cg语言。通过遵循一定的编写方式,就可以在Unity3D ShaderLab语言中直接编写Cg代码(其实方法很简单,就是在代码段中所写的那样子,在CGPROGRAM宏和ENDCG宏中间,便是我们编写Cg代码的地方)。正因为ShaderLab语言可以通过这样子的方式兼容Cg代码。所谓的Surface Shader实际上是封装了Cg语言,隐藏了很多光照处理的细节,它的设计初衷是为了让用户仅仅使用一些指令(#pragma)就可以完成很多事情,并且封装了很多常用的光照模型和函数,例如Lambert、Blinn-Phong等。而查看Surface Shader生成的代码也很简单:在每个编译完成的Surface Shader的面板上,都有个“Show generated code”的按钮,如下图:

通过点击按钮我们就可以看到,短短的几句surface shader代码,全部转译为Cg代码之后变成了长长的一串。也就是说,上面的问题1我们可以得到答案:surface shader已经把作为顶点着色器和片元/像素着色器的入口函数给包装好了。

接下来分析第二个问题。我们可以看由用户自定义的struct Input。在此我们首先要弄清楚的是,这个Input结构,并不是指我们在做Cg编码时,要传递给顶点着色器的“顶点格式信息描述”结构,准确地说,这个Input结构,应该是由光栅器对顶点进行了光栅化处理之后,要传递给片元着色器的“片元格式信息描述结构”。本章所描述的这段shader代码的这个Input结构体,因为在surf函数中,只需用到片元所携带的纹理映射坐标,因此,我们在定义这个结构时,只需要定义uv_MainTex变量就可以了。只要这样定义了,那么surface shader编译器在将surface shader展开成Cg代码的时候,便会按照一定的规则去生成Cg代码,从而使得能告知光栅器,光栅化过程之后,传递给片元着色器的片元信息,只需要携带这样一个纹理映射坐标的信息便可以了。如果在别的场合下,需要用到诸如摄像机观察方向(view direction)、顶点在世界坐标系下的位置(world space position)等等信息的话。也可以在此结果里面定义,但这些变量只有在真正使用的时候才会被计算生成。

第三个问题即是U3D shader的编译指令。编译指令的一般格式如下:

#pragma surface surfaceFunction lightModel [optionalparams]

由上面的编译格式可以看出,surfaceFunction和lightModel是必须指定的。[optionparams]则说明在指定前面两个必选项的基础上,还可以可选地指定一些其他附加指令。

surfaceFunction通常就是代码中的surf函数(函数名可以任意),它的函数签名格式是固定的:

void surf(Input IN, inout SurfaceOutput o)

lightModel,即光照模型。此模型指明了经过了surfaceFunction处理过的表面片元信息之后,如何利用这些片元信息进行光照计算。光照模型可以使用了Unity3D自带的光照函数,如Lambert模型,也可以使用自定义的光照函数。这些光照函数的命名规则是LightingXXXX。即如果你在编译指令中指定的光照模型名为XXXX,则定义此光照模型的光照函数名为LightingXXXX。以U3D自带的Lambert模型为例。此光照模型函数则是在Lighting.cginc文件中定义的LightingLambert。如下:

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;
}

可以看到,LightingLambert函数是接收一个从surf函数返回的SurfaceOutput类型结构体作为输入参数的。并且在函数内部,使用了内置的灯光变量_LightColor0作为光源去参与颜色计算。光照函数的具体细节我们将在另文详细叙述。

optionalparams可选项包含了很多可用的指令类型:包括开启、关闭一些状态,设置生成的Pass类型,指定可选函数等。这里,我们只关注可指定的函数,其他可去官网自行查看。除了上述的surfaceFuntion和lightModel,可以自定义两种函数:vertex:VertexFunction和finalcolor:ColorFunction。这两个自定义的函数在整个渲染管道中的执行时刻如下图:(本图取自candycat的技术博客)

从上图我们可以看出。SurfaceOutput结构体,就是由我们自定义的surf函数生成,然后传递给对应的光照处理函数。SurfaceOutput结构体在U3D自带的shader头文件Lighting.cginc中定义,代码如下:

struct SurfaceOutput
{
    half3 Albedo;
    half3 Normal;
    half3 Emission;
    half Specular;
    half Gloss;
    half Alpha;
};

所包含的属性的含义如下表:

属性名字 含义
Albedo 该片元对光源的反射率。它是通过在Fragment Shader中计算颜色叠加时,和一些变量(如vertex lights)相乘后,叠加到最后的颜色上的。
Normal 该片元的法线
Emission 该片元的自发光。会在Fragment 最后输出前(调用final函数前,如果定义了的话),使用下面的语句进行简单的颜色叠加:
Specular 该片元的镜面高光反射中的指数部分的系数。影响一些镜面高光反射的计算。
Gloss 该片元的镜面高光反射中的强度系数。和上面的Specular类似,一般在光照模型里使用。
Alpha 该片元的透明通道值

返回首页