本章介绍统一参数。假设你熟悉章节“小型着色器”、“RGB立方体”以及“着色器调试”。 在本章中我们将会查看一个着色器,它会根据世界空间中的位置改变片元颜色。这个概念并不复杂,这里有非常重要的应用,比如光照和环境映照的着色器。我们将会查看真实世界中的着色器;也就是说,让非程序员使用着色器的必要条件是什么?

从物体坐标向世界空间变换

就像在章节“着色器调试”中提到的,带有语义POSITION的顶点输入参数会指定对象坐标,即在一个网格的本地对象(或模型)空间的坐标。对象空间(或对象坐标系)是特定于每个游戏对象的;但是,所有的游戏对象会被变换到一个公共的坐标系—-世界空间。

如果一个游戏对象被直接放入世界空间中,那么对象到世界的变换就由游戏对象的Transform组件来指定。可以在 Scene View或者Hierarchy Window中选择它,然后就会在Inspector Window发现Transform组件。在Transform组件有“Position”、“Rotation”和“Scale”参数,它们会指定顶点如何从对象坐标变换到世界坐标的。(如果一个游戏对象是一组对象的一部l分,它通过缩进显示在Hierarchy Window中,那么Transform组件只是指定了从游戏对象的对象坐标到父对象坐标的变换。在这种情况下,实际的对象世界变换是由对象的变换以及父、爷等对象的变换的组合给出的。)通过平移、旋转和缩放的顶点变换以及变换的组合与它们的4×4矩阵表示,都在章节”顶点变换“中讨论过了。

回到我们的例子:从对象空间到世界空间的变换被放进一个4×4的矩阵中,这也被称为“模型矩阵”(因此这个变换也称为“模型变换”)。这个矩阵在统一参数_Object2World中是可用的,它以这种方式被Unity自动定义:

uniform float4x4 _Object2World;

因为它是自动定义的,我们就不需要定义它了(实际上也不需要)。无需定义我们就能在以下的着色器中使用统一参数_Object2World:

hader "Cg shading in world space" {
   SubShader {
      Pass {
         CGPROGRAM

         #pragma vertex vert  
         #pragma fragment frag 

         // uniform float4x4 _Object2World; 
            // Unity指定的统一参数自动定义

         struct vertexInput {
            float4 vertex : POSITION;
         };
         struct vertexOutput {
            float4 pos : SV_POSITION;
            float4 position_in_world_space : TEXCOORD0;
         };

         vertexOutput vert(vertexInput input) 
         {
            vertexOutput output;  
            output.pos =  mul(UNITY_MATRIX_MVP, input.vertex);
            output.position_in_world_space = mul(_Object2World, input.vertex);
               // 顶点从对象坐标向世界坐标的变换
            return output;
         }

         float4 frag(vertexOutput input) : COLOR 
         {
             // 计算片元的位置和原点(对于点来说第4个坐标必须为1)之间的距离
             float dist = distance(input.position_in_world_space, 
               float4(0.0, 0.0, 0.0, 1.0));

            if (dist < 5.0)
            {
               // 靠近原点的颜色
               return float4(0.0, 1.0, 0.0, 1.0);                   
            }
            else
            {
               // 远离原点的颜色
               return float4(0.1, 0.1, 0.1, 1.0); 
            }
         }

         ENDCG  
      }
   }
}

通常,应用必须设置统一参数的值;但是,像_Object2World,Unity照顾到设置预定义统一参数的正确值;因此我们无需担心这个。

该着色器把顶点位置变换到了世界空间,并且在输出结构体中传递给片元着色器。对于片元着色器,输出结构体中的参数包含了世界坐标中片元插值的位置。基于这个位置跟世界坐标系原点的距离,两个颜色中的一个会被设置。因此,如果你在编辑器中来回移动带有这个着色器的物体,它会在世界坐标系的原点附近变成绿色。离原点远的话,它会变成深灰色。

更多的Unity统一参数

这里有一些跟float4x4矩阵_Object2World类似的Unity自定义的内置统一参数。以下是在一些教程中使用到的uniforms(包括_Object2World)的简短清单:

uniform float4 _Time, _SinTime, _CosTime; // 时间相关的值
   uniform float4 _ProjectionParams;
      // x = 1 or -1 (-1 if projection is flipped)
      // y = near plane; z = far plane; w = 1/far plane
   uniform float4 _ScreenParams; 
      // x = width; y = height; z = 1 + 1/width; w = 1 + 1/height
   uniform float3 _WorldSpaceCameraPos;
   uniform float4x4 _Object2World; // 模型矩阵
   uniform float4x4 _World2Object; // 模型矩阵的逆矩阵 
   uniform float4 _WorldSpaceLightPos0; // 前向渲染中光源的位置或方向

   uniform float4x4 UNITY_MATRIX_MVP; // 模型视图投影矩阵 
   uniform float4x4 UNITY_MATRIX_MV; // 模型视图矩阵
   uniform float4x4 UNITY_MATRIX_V; // 视图矩阵
   uniform float4x4 UNITY_MATRIX_P; // 投影矩阵
   uniform float4x4 UNITY_MATRIX_VP; // 视图投影矩阵
   uniform float4x4 UNITY_MATRIX_T_MV; // 模型视图矩阵的转置
   uniform float4x4 UNITY_MATRIX_IT_MV; // 模型视图逆矩阵的转置
   uniform float4 UNITY_LIGHTMODEL_AMBIENT; // 环境颜色

对于Unity内置uniforms的官方清单,可以参考Unity手册中的章节“内置着色器变量”。

有一些uniforms实际上是在文件UnityShaderVariables.cginc中定义的,它在Unity4.0版本后就会被自动包含进去。 还有一些内置的uniforms没有被自动包含进去,比如_LightColor0,它是在Lighting.cginc中定义的。因此,我们必须明确地定义它(如果有必要的话):

uniform float4 _LightColor0;

或者包含以下定义的文件:

#include "Lighting.cginc"

Unity并不总是会更新所有的uniforms。特别是,_WorldSpaceLightPos0和_LightColor0只有在着色器通道标记合适时才会被正确设置,比如在 Pass {…}代码块中的第一行Tags {“LightMode” = “ForwardBase”};也可以查阅“漫反射”。

用户自定义参数:着色器属性

统一参数还有个更重要的类型:用户可以自定义的参数。实际上,在Unity中它们被称作着色器属性。你可以认为它们是用户自定义的着色器统一参数。一个没有参数的的着色器通常只会被程序员使用,因为即使最小的必要改动都需要编程。另一方面,使用具有描述性名称的参数的着色器会被其它人使用,即使不是程序员,比如CG美术。设想你在一个游戏开发团队中,一个CG美术要求你为100个设计迭代中的每一个调整你的着色器。很显然提供一些甚至CG美术都能使用的参数,也许会节省你很多时间。或者,设想你想要出售你的着色器:参数会显著增加你着色器的价值。

既然Unity对着色器属性的描述相当不错,那么这里只有一个例子,即如何在例子中使用着色器属性。首先我们要声明属性并且用相同名字和相应类型定义uniforms。

Shader "Cg shading in world space" {
   Properties {
      _Point ("a point in world space", Vector) = (0., 0., 0., 1.0)
      _DistanceNear ("threshold distance", Float) = 5.0
      _ColorNear ("color near to point", Color) = (0.0, 1.0, 0.0, 1.0)
      _ColorFar ("color far from point", Color) = (0.3, 0.3, 0.3, 1.0)
   }

   SubShader {
      Pass {
         CGPROGRAM

         #pragma vertex vert  
         #pragma fragment frag 

         #include "UnityCG.cginc" 
            // 定义了_Object2World和_World2Object

         // 对应于属性的uniforms
         uniform float4 _Point;
         uniform float _DistanceNear;
         uniform float4 _ColorNear;
         uniform float4 _ColorFar;

         struct vertexInput {
            float4 vertex : POSITION;
         };
         struct vertexOutput {
            float4 pos : SV_POSITION;
            float4 position_in_world_space : TEXCOORD0;
         };

         vertexOutput vert(vertexInput input) 
         {
            vertexOutput output; 

            output.pos =  mul(UNITY_MATRIX_MVP, input.vertex);
            output.position_in_world_space = 
               mul(_Object2World, input.vertex);
            return output;
         }

         float4 frag(vertexOutput input) : COLOR 
         {
            // 计算_Point位置和片元位置之间的距离
            float dist = distance(input.position_in_world_space,_Point);              

            if (dist < _DistanceNear)
            {
               return _ColorNear; 
            }
            else
            {
               return _ColorFar; 
            }
         }

         ENDCG  
      }
   }
}

利用这些参数,一个非程序员就能够修改我们着色器的效果。这样非常棒;但是着色器(实际上通常是指uniforms)的参数也能被脚本设置!举例来说,一个使用着色器的游戏对象上的C#脚本可以使用以下几行来设置属性:

GetComponent<Renderer>().sharedMaterial.SetVector("_Point", new Vector4(1.0f, 0.0f, 0.0f, 1.0f));
GetComponent<Renderer>().sharedMaterial.SetFloat("_DistanceNear", 10.0f);
GetComponent<Renderer>().sharedMaterial.SetColor("_ColorNear", new Color(1.0f, 0.0f, 0.0f));
GetComponent<Renderer>().sharedMaterial.SetColor("_ColorFar", new Color(1.0f, 1.0f, 1.0f));

GetComponent()会返回Renderer组件。(你也可以写成(效率较低)GetComponent(typeof(Renderer)) as Renderer或者GetComponent(“Renderer”) as Renderer。)使用sharedMaterial如果你想改变所有使用该材质对象的参数,而使用material如果你只想改变一个对象的参数。(但是请注意那个material可能会创建新的material实例,当游戏对象销毁时它并不会自动销毁!)举例来说,你会用脚本把_Point设置为另一个对象的位置(也就是它Transform组件的位置)。在这种方法中,你可以通过移动编辑器中的另一个对象来指定一个点。对了写出这样的脚本,在Project Window中选择Create > C# Script,命名为ShadingInWorldSpace然后拷贝粘贴以下的代码:

using UnityEngine;

[ExecuteInEditMode]
public class ShadingInWorldSpace : MonoBehaviour {
    public GameObject other;
    Renderer rend;
    void Start() {
        rend = GetComponent<Renderer>();
    }

    // Update is called once per frame
    void Update () {
        if(other != null) {
            rend.sharedMaterial.SetVector("_Point", other.transform.position);
        }
    }
}

然后,你可以把脚本挂载到使用该着色器的对象上去(通过在对象上拖拉脚本)以及在Inspector Window中拖拉另一个对象到脚本的其它变量上。现在你可以通过改变其它对象的位置来改变_Point变量的值。

总结

恭喜,你完成了本章的学习!我们讨论了:

  • 如何把一个顶点转换成世界坐标。
  • Unity支持的最重要的自定义uniforms 。
  • 如何通过增加着色器属性