使用CG包含让着色器模块化

都说关于内建的CG包含文件很棒,但是如果我们想构建自己CG包含文件来保存我们自己的光照模型和帮助函数该怎么办呢?我们是能这样做的,事实上,在我们的着色器编写管线中如果要高效的使用自己包含文件,还需要多学一点代码的语法才能来创建自己的包含文件。不用纠结了,让我们来看看如何创建一个新的CG包含文件的过程吧。


  • 始前准备
    让我们概览一下在这个知识点中生成这些新的项的过程吧。
    • 1.首先我们创建一个新的名为 MyCgInclude.txt 的文本文件。
    • 2.其次将这个文件的扩展名改为 .cginc,Windows可能会给出一个该文件会不可用的警告消息,但它依然时可以正常工作。
    • 3.将这个新的 .cginc 文件导入到Unity工程中并且让它编译。如果一切顺利,你将会看到Unity会把它编译成一个CG包含文件。

      我们现在已经准备好创建我们自定义的CG包含代码了。然后简单的双击这个你创建的CG包含文件在代码编辑器中打开它。

  • 操作步骤
    打开我们的CG包含文件按之后,我们就能向其中添加代码了,好让它可以跟我们表面着色器一起工作。下面的代码可以准备好我们的CG包含文件让它可以跟表面着色器一起使用,并且随着我们开发越来越多着色器,还能对它持续的添加所需的代码:

    • 1.开始创建我们的CG包含文件的时候我们来了解一下 预编译指令(preprocessor directive)。它们时类似于 #pragma#include 这样的声明。在这个例子中,我们想定义一个新的代码设置,在这个代码设置中如果我们的着色器在它的预编译指令中包含了这个文件,那么这个代码设置将会执行。在你的CG包含文件的顶部输入下面的代码:
    #ifndef MY_CG_INCLUDE
    #define MY_CG_INCLUDE
    
    • 2.我们总是需要确保对 #ifndef 或者 #ifdef#endif 将前面的这些定义检测闭合[就是我们编程中常说的成对出现,译者注],就好像C#中的 if 声明一样,我们需要用两个括号将它闭合。在 #define 指令后面输入下面的的代码:
    #endif
    
    • 3.此时,我们只需要给CG包含文件填充代码了。所以我们通过添加下面的代码来完成我们的CG包含文件:
    fixed4 _MyColor;
    inline fixed4 LightingHalfLamber (SurfaceOutput s, fixed3 lightDir, fixed atten)
    {
        fixed diff = max(0, dot(s.Normal, lightDir));
        diff = (diff + 0.5)*0.5;
        fixed4 c;
        c.rgb = s.Albedo * _LightColor0.rgb * ((diff * _MyColor.rgb) * atten);
        c.a = s.Alpha;
        return c;
    }
    #endif
    
    • 4.完成上面这一步之后,你现在有了自己的第一个CG包含文件了。就这点代码,我们就能大量的减少我们要编写的代码量,我们就可以开始将那些要经常使用的光照模型保存到文件中了,这样这些模型也不会丢失。你的CG包含文件看起来就跟下面的代码类似:
    #ifndef MY_CG_INCLUDE
    #define MY_CG_INCLUDE
    fixed4 _MyColor;
    inline fixed4 LightingHalfLamber (SurfaceOutput s, fixed3 lightDir, fixed atten)
    {
        fixed diff = max(0, dot(s.Normal, lightDir));
        diff = (diff + 0.5)*0.5;
        fixed4 c;
        c.rgb = s.Albedo * _LightColor0.rgb * ((diff * _MyColor.rgb) * atten);
        c.a = s.Alpha;
        return c;
    }
    #endif
    

    在我们能完全利用CG包含文件之前我们还有一些步骤需要完成。我们只需要告诉当前的着色器我们要使用这个CG包含文件以及它的代码。为了完成使用和创建CG包含文件的过程,让我们继续完成下面的步骤:

    • 1.如果我们将注意力转移到我们的着色器上来,我们需要告诉我们的 CGPROGRAM代码块(CGPROGRAM block) 去把新的CG包含文件包含进来,好让我们可以去访问其中的代码。添加下面的代码,修改CGPROGRAM代码块的预编译指令:
    #include "MyCgInclude.cginc"
    #pragma surface surf Lambert
    
    • 2.我们当前的着色器使用的是内建的 Lambert 光照模型,但是我们想让它使用我们自己CG包含文件中创建的 Half Lambert 光照模型。通过下面的代码,我们就可以引入来自我们自己的CG包含文件中的代码,就能使用其中的 Half Lambert 光照模型了:
    CGPROGRAM
    #include "MyCgInclude.cginc"
    #pragma surface surf HalfLamber
    
    • 3.最终,我们也在CG包含文件中声明了一个自定义的变量,也就是意味着我们可以给我们使用的着色器设置默认的变量。为了了解这一操作,请在你的着色器的 属性块(Properties block) 中输入下面的代码:
    Properties
    {
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _DesatValue ("Desaturate", Range(0, 1)) = 0.5
        _MyColor ("My Color", Color) = (1,1,1,1)
    }
    
    • 4.当返回Unity编辑器,着色器和CG包含文件都将会被编译,如果你没有遇到什么错误,你就会注意到事实上我们是在使用我们自己的新 Half Lambert 光照模型并且一个新的颜色样本将会出现在材质的 检查器(Inspector) 中。下面的截屏展示了使用我们自己的CG包含文件的结果:
      diagram

  • 原理介绍
    当我们使用着色器的时候,我们可以使用 #include 预编译指令将其他的代码设置包含进来。这其实是在告诉Unity我们想在着色器中让当前的着色器使用来自另一个包含进来的文件中的代码;这就是为什么这些文件被称作CG包含文件的原因。我们使用 #include 指令包含进了Cg代码的部分代码片段。

    一旦我们声明了 #include 指令然后Unity就可以在项目中去搜寻这个文件了,Unity将会从已经定义了的代码中去寻找想要的代码片段。也正是在这里我们开始使用了 #ifndef#endif 指令。当我们使用 #ifndef 指令的时候,我们其实在说,如果没有定义某某,那么就用这个名字定义它(if not defined, define something with a name)。在这个知识点的例子中,我们表达的是我们想 #define MY_CG_INCLUDE。所以如果Unity没有找到名为 MY_CG_INCLUDE 的定义,那么当这个CG包含文件进行编译的时候就会去创建它,因此才让我们在接下来的步骤中可以去访问那些代码。而 #endif 指令则是简单的告诉我们此乃定义的结束,因此没有必要再往下看啦。

    由此你也可以看到这项技术可以变得多么的给力,因为现在我们可以将所有自己的光照模型和自定义变量都保存在这一个文件当中,并且还可以极大的减少我们的代码量。该技术的真正威力在于,在CG包含文件中为不同的功能去定义一些不同的状态,由此增加我们着色器的灵活性。