本章介绍统一参数。假设你熟悉章节“小型着色器”、“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 。
- 如何通过增加着色器属性