Cg 编程/Unity/着色器调试
本教程讨论了顶点输入参数。假设你熟悉“最小着色器”部分和“RGB 立方体”部分.
本教程还介绍了在 Unity 中调试着色器的主要技术:假彩色图像,即通过将片段颜色的一组分量设置为特定值来可视化该值。然后,在生成的图像中,该颜色分量的强度使你能够对着色器中的值做出推断。这可能看起来是一种非常原始的调试技术,因为它确实是一种非常原始的调试技术。不幸的是,在 Unity 中没有其他选择。
在“RGB 立方体”部分中,你已经看到了片段着色器如何通过顶点输出参数的输出结构从顶点着色器获取数据。这里的问题是:顶点着色器从哪里获取数据?在 Unity 中,答案是游戏对象的网格渲染器组件在每一帧都会将游戏对象网格的所有数据发送到 GPU。(这通常被称为“绘制调用”。请注意,每个绘制调用都有一定的性能开销;因此,将一个大型网格发送到 GPU 进行一次绘制调用比发送多个小型网格进行多次绘制调用效率更高。)这些数据通常由一个三角形列表组成,每个三角形由三个顶点定义,每个顶点具有某些属性,包括位置。这些属性通过顶点输入参数在顶点着色器中可用。在 Cg 中,通过语义来实现将不同属性映射到不同顶点输入参数的过程,即每个顶点输入参数都必须指定一个特定的语义,例如 POSITION
、NORMAL
、TEXCOORD0
、TEXCOORD1
、TANGENT
、COLOR
等。(在旧版本的 Unity 中,内置顶点输入参数也必须具有特定的名称,即本示例中使用的名称。)
将所有输入顶点参数包含在一个结构中通常很方便,例如
struct vertexInput {
float4 vertex : POSITION; // position (in object coordinates,
// i.e. local or model coordinates)
float4 tangent : TANGENT;
// vector orthogonal to the surface normal
float3 normal : NORMAL; // surface normal vector (in object
// coordinates; usually normalized to unit length)
float4 texcoord : TEXCOORD0; // 0th set of texture
// coordinates (a.k.a. “UV”; between 0 and 1)
float4 texcoord1 : TEXCOORD1; // 1st set of tex. coors.
float4 texcoord2 : TEXCOORD2; // 2nd set of tex. coors.
float4 texcoord3 : TEXCOORD3; // 3rd set of tex. coors.
fixed4 color : COLOR; // color (usually constant)
};
该结构可以这样使用
Shader "Cg shader with all built-in vertex input parameters" {
SubShader {
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
struct vertexInput {
float4 vertex : POSITION;
float4 tangent : TANGENT;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
float4 texcoord1 : TEXCOORD1;
float4 texcoord2 : TEXCOORD2;
float4 texcoord3 : TEXCOORD3;
fixed4 color : COLOR;
};
struct vertexOutput {
float4 pos : SV_POSITION;
float4 col : TEXCOORD0;
};
vertexOutput vert(vertexInput input)
{
vertexOutput output;
output.pos = UnityObjectToClipPos(input.vertex);
output.col = input.texcoord; // set the output color
// other possibilities to play with:
// output.col = input.vertex;
// output.col = input.tangent;
// output.col = float4(input.normal, 1.0);
// output.col = input.texcoord;
// output.col = input.texcoord1;
// output.col = input.texcoord2;
// output.col = input.texcoord3;
// output.col = input.color;
return output;
}
float4 frag(vertexOutput input) : COLOR
{
return input.col;
}
ENDCG
}
}
}
在“RGB 立方体”部分中,我们已经看到了如何通过将片段颜色设置为这些值来可视化顶点坐标。在本示例中,片段颜色被设置为纹理坐标,这样我们就可以看到 Unity 提供了什么样的纹理坐标。
请注意,只有 tangent
的前三个分量代表切线方向。缩放和第四个分量以特定方式设置,这主要用于视差贴图(参见“凹凸表面的投影”部分)。
通常,通过只指定你实际需要的顶点输入参数(例如位置、法线和一组纹理坐标;有时还有切线向量)可以实现更高的性能。Unity 为最常见的情况提供了预定义输入结构 appdata_base
、appdata_tan
、appdata_full
和 appdata_img
。这些结构在文件 UnityCG.cginc
中定义(位于 Unity > Editor > Data > CGIncludes 目录中)。
struct appdata_base {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct appdata_tan {
float4 vertex : POSITION;
float4 tangent : TANGENT;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct appdata_full {
float4 vertex : POSITION;
float4 tangent : TANGENT;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
float4 texcoord1 : TEXCOORD1;
float4 texcoord2 : TEXCOORD2;
float4 texcoord3 : TEXCOORD3;
fixed4 color : COLOR;
// and additional texture coordinates only on XBOX360
};
struct appdata_img {
float4 vertex : POSITION;
half2 texcoord : TEXCOORD0;
};
文件 UnityCG.cginc
通过以下代码行包含:#include "UnityCG.cginc"
。因此,上面的着色器可以这样重写
Shader "Cg shader with all built-in vertex input parameters" {
SubShader {
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct vertexOutput {
float4 pos : SV_POSITION;
float4 col : TEXCOORD0;
};
vertexOutput vert(appdata_full input)
{
vertexOutput output;
output.pos = UnityObjectToClipPos(input.vertex);
output.col = input.texcoord;
return output;
}
float4 frag(vertexOutput input) : COLOR
{
return input.col;
}
ENDCG
}
}
}
在尝试理解假彩色图像中的信息时,重要的是只关注一个颜色分量。例如,如果将语义为 TEXCOORD0
的输入顶点参数 texcoord
写入片段颜色,那么片段的红色分量会可视化 texcoord
的 x
坐标,即输出颜色是最大纯红色还是最大黄色或最大洋红色都没有关系,在所有情况下红色分量都是 1。另一方面,对于红色分量来说,颜色是蓝色、绿色还是青色以及任何强度也无关紧要,因为在所有情况下红色分量都是 0。如果你从未学会只关注一个颜色分量,这可能相当具有挑战性;因此,你可能需要考虑一次只查看一个颜色分量。例如,使用以下代码行在顶点着色器中设置输出参数
output.col = float4(input.texcoord.x, 0.0, 0.0, 1.0);
这将输出参数的红色分量设置为 texcoord
的 x
分量,但将绿色和蓝色分量设置为 0(以及 alpha 或不透明度分量设置为 1,但这在该着色器中无关紧要)。
如果你只关注红色分量或只可视化红色分量,你应该会看到它在你绕球体移动时从 0 增加到 1,并且在 360° 后又降到 0。实际上,它的行为类似于行星表面上的经度坐标。(在球面坐标系中,它对应于方位角。)
如果 texcoord
的 x
分量对应于经度,那么可以预期 y
分量会对应于纬度(或球面坐标系中的倾角)。但是,请注意,纹理坐标始终介于 0 和 1 之间;因此,该值在底部(南极)为 0,在顶部(北极)为 1。你可以将 y
分量作为绿色独立可视化
output.col = float4(0.0, input.texcoord.y, 0.0, 1.0);
纹理坐标特别容易可视化,因为它们就像颜色分量一样,介于 0 和 1 之间。几乎同样容易可视化的是归一化向量的坐标(即长度为 1 的向量;例如,normal
输入参数通常是归一化的),因为它们始终介于 -1 和 +1 之间。要将此范围映射到 0 到 1 的范围,你需要向每个分量加 1 并将所有分量除以 2,例如
output.col = float4(
(input.normal + float3(1.0, 1.0, 1.0)) / 2.0, 1.0);
请注意,normal
是一个三维向量。黑色对应于坐标 -1,一个分量的全强度对应于坐标 +1。
如果你要可视化的值不在 0 到 1 或 -1 到 +1 的范围内,你需要将其映射到 0 到 1 的范围,这是颜色分量的范围。如果你不知道应该期望什么值,你只需要尝试一下。这方面有帮助的是,如果你指定了超出 0 到 1 范围的颜色分量,它们会自动被限制到此范围内。即,小于 0 的值被设置为 0,大于 1 的值被设置为 1。因此,当颜色分量为 0 或 1 时,你至少知道该值小于或大于你假设的值,然后你可以迭代地调整映射,直到颜色分量介于 0 和 1 之间。
为了练习着色器的调试,本节包含一些代码行,当用它们替换顶点着色器中对 col
的赋值时,会产生黑色。你的任务是找出每行代码为什么导致结果为黑色。为此,你应该尝试可视化你没有完全确定的任何值,并将小于 0 或大于 1 的值映射到其他范围,这样这些值就可以显示出来,并且你至少可以了解它们所在的范围。请注意,大多数函数和运算符在“向量和矩阵运算”部分中有介绍。
output.col = input.texcoord - float4(1.5, 2.3, 1.1, 0.0);
output.col = input.texcoord.zzzz;
output.col = input.texcoord / tan(0.0);
以下代码行需要对点积和叉积有一定的了解
output.col = dot(input.normal, input.tangent.xyz) *
input.texcoord;
output.col = dot(cross(input.normal, input.tangent.xyz),
input.normal) * input.texcoord;
output.col = float4(cross(input.normal, input.normal), 1.0);
output.col = float4(cross(input.normal,
input.vertex.xyz), 1.0);
// only for a sphere!
函数 radians()
总是返回黑色吗?它有什么用?
output.col = radians(input.texcoord);
恭喜你已经学完了本教程!我们已经学习了
- Unity 中内置顶点输入参数的列表。
- 如何通过设置片段输出颜色的组件来可视化这些参数(或任何其他值)。
如果你还想了解更多
- 关于顶点和片段着色器中的数据流,请阅读“可编程图形管道”部分。
- 关于向量操作和函数,请阅读“向量和矩阵操作”部分。
- 关于 Unity 中顶点输入参数的额外语义,请阅读Unity 的着色器语义文档。