本章内容覆盖了片元丢弃和正面、背面剔除。本章假设读者熟悉之前在“RGB Cube”这章讨论的顶点输出参数。
本章的主题是关于三角形或片元的丢弃,即使他们是正被渲染的网格的一部分。主要有两点理由:我们想要看透一个三角形或者片段(比如左图的屋顶只有部分被裁剪了),或者我们知道一个三角形无论如何都是不可见的;这样我们可以节省性能的开销。GPU有一系列的方法支持以上情况,这里我们集中讨论其中的两种方法。
非常简单的裁剪办法
下面这个shader就用了一个简单的办法裁剪了部分网格:所有被裁剪的片段在模型坐标系中都有一个正向的y坐标(换句话说这就是模型自身的坐标系,详细内容可以参考“顶点变换”章节)。以下是代码:
Shader "Cg shader using discard" { SubShader { Pass { Cull Off // 关闭三角形剔除, 替代方案: // 背面剔除 (或者不剔除): 只剔除背面 // 前面剔除 : 只剔除前面 CGPROGRAM #pragma vertex vert #pragma fragment frag struct vertexInput { float4 vertex : POSITION; }; struct vertexOutput { float4 pos : SV_POSITION; float4 posInObjectCoords : TEXCOORD0; }; vertexOutput vert(vertexInput input) { vertexOutput output; output.pos = mul(UNITY_MATRIX_MVP, input.vertex); output.posInObjectCoords = input.vertex; return output; } float4 frag(vertexOutput input) : COLOR { if (input.posInObjectCoords.y > 0.0) { discard; // drop the fragment if y coordinate > 0 } return float4(0.0, 1.0, 0.0, 1.0); // green } ENDCG } } }
当你把这个shader应用到任意一个默认物体上,物体会被裁切掉一半。这是制作半球或半圆柱的一个非常简单的办法。
丢弃片元
让我们把注意力放在片元着色器的丢弃指令上。该指令基本上就是丢弃被处理过的片元。(在早期的着色器语言中这个操作叫做杀死一个片元,当然我更喜欢“discard”这个术语。)基于目前的硬件,这有可能是一种非常奢侈的技术,在某种意义上只要有一个着色器包含丢弃指令,渲染性能就会表现得相当糟糕(不考虑有多少片元实际被丢弃,光是这个指令的存在就会导致一些重要的优化不起作用)。 因此,你必须尽可能地避免这个指令特别是有性能问题的时候。
还有一点要注意:片元丢弃的条件是只包含物体坐标系。结果就是你可以以任意方式旋转和移动物体,同时被裁剪的部分将会永远跟着物体旋转和平移。你也许想要验证在世界坐标系裁剪是怎么样的:改变顶点和片元着色器以确定世界坐标的y轴被用作片元丢弃的条件。小贴士:可以通过查看“世界空间的着色器”这一章节学习怎么样把顶点转换到世界空间。
比较好的裁剪方法
如果你不太熟悉Unity脚本,你可以尝试以下的想法来提升shader:如果坐标y大于某个给定的阈值,就把该片元舍弃掉。然后就有一个shader属性允许用户控制阈值。小贴士:关于shader属性的讨论可以通过查看“世界空间中的着色器”这章来了解。
如果你熟悉Unity脚本,你可以尝试以下的想法:给一个物体写一个脚本,在脚本里面引用另一个球体对象,然后将球体的逆矩阵(GetComponent(Renderer).worldToLocalMatrix)赋值(使用GetComponent(Renderer).sharedMaterial.SetMatrix())给shader的float4x4参数。在这个shader中,计算出该片元的世界坐标,并把球体对象的逆模型矩阵应用到片元位置。现在你有了片元在球体对象模型空间的位置;这里我们就很容易判断片元是否在球体内部了,因为在Unity默认的坐标系统中球体的半径是0.5.如果片元在球体内部则把它抛弃掉。在裁切球体的帮助下,最终的脚本和shader能够裁剪任意物体的表面点,就像在其它球体一样可以在编辑器中交互操作。
正面或背面剔除
最终,这个shader(特别是shader的pass通道)包含了Cull Off这行。因为不是cg,这行必须在CGPROGRAM之前出现。实际上,这是Unity ShaderLab中关闭三角形裁剪的一条指令。这个指令是必须的,因为默认情况下那条Cull Back指令会把背面剔除掉。你也能用Cull Front指令进行正面剔除。默认背面三角形剔除的原因是通常物体内部是不可见的;因此,背面剔除能够通过避免后面要提到的三角形光栅化来提高性能。当然,因为我们舍弃了一些片元,我们可以看到物体里面了;因此,我们不应该激活背面剔除。
那么裁剪是怎么工作的呢?三角形和顶点的处理和之前一样。但是,在顶点的视口变换到屏蔽坐标后(参考章节"顶点变换"),图形处理器会决定一个三角形的顶点是以逆时针还是顺时针的顺序出现在屏幕上。基于这个结果,每个三角形都被看作是一个正面或者背面的三角形。如果三角形是正面并且裁剪对正面三角形生效的话,它就会被裁剪掉,也就意味着不会处理它,也不会光栅化。相似的,如果三角形是背面并且裁剪对背面三角形生效的话也是这样的。而其它的三角形会被继续处理。
我们能用剔除做什么呢?有一个应用是为正面和反面使用不同的shader,比如在一个物体的外部和内部。以下的shader使用了两个pass。在第一个pass,只有正面是被剔除的,剩下的面就被渲染成红色(如果片元没被丢弃的话)。第二个pass只剔除背面并且剩下的面被渲染成绿色。
Shader "Cg shader with two passes using discard" {
SubShader {
// 第一个 pass (在第二个pass前执行)
Pass {
Cull Front // 只剔除正面
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
struct vertexInput {
float4 vertex : POSITION;
};
struct vertexOutput {
float4 pos : SV_POSITION;
float4 posInObjectCoords : TEXCOORD0;
};
vertexOutput vert(vertexInput input)
{
vertexOutput output;
output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
output.posInObjectCoords = input.vertex;
return output;
}
float4 frag(vertexOutput input) : COLOR
{
if (input.posInObjectCoords.y > 0.0)
{
discard; // drop the fragment if y coordinate > 0
}
return float4(1.0, 0.0, 0.0, 1.0); // red
}
ENDCG
}
// second pass (is executed after the first pass)
Pass {
Cull Back // cull only back faces
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
struct vertexInput {
float4 vertex : POSITION;
};
struct vertexOutput {
float4 pos : SV_POSITION;
float4 posInObjectCoords : TEXCOORD0;
};
vertexOutput vert(vertexInput input)
{
vertexOutput output;
output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
output.posInObjectCoords = input.vertex;
return output;
}
float4 frag(vertexOutput input) : COLOR
{
if (input.posInObjectCoords.y > 0.0)
{
discard; // drop the fragment if y coordinate > 0
}
return float4(0.0, 1.0, 0.0, 1.0); // green
}
ENDCG
}
}
}
记住Unity shader只有一个subshader会被执行(这个取决于GPU支持subshader的能力),但是这个subshader的所有psss会被执行。
在大多数的GPU上,这里有一个行之有效的方法在Cg中区分正面和背面,可以使用带有VFACE语义的片元输入参数;请查看Unity的shader语义文档。但是,不是所有的GPU都支持这种方法。
总结
恭喜你,又看完了一章。(如果你已经尝试了其中的某个任务:干得好!我还没做过呢!)这章我们学习了:
如何丢弃片元。
如何指定正面和背面剔除。
如何利用两个pass配合剔除在一个mesh的内部和外部使用不同的shader。
Shader "Cg shader with two passes using discard" { SubShader { // 第一个 pass (在第二个pass前执行) Pass { Cull Front // 只剔除正面 CGPROGRAM #pragma vertex vert #pragma fragment frag struct vertexInput { float4 vertex : POSITION; }; struct vertexOutput { float4 pos : SV_POSITION; float4 posInObjectCoords : TEXCOORD0; }; vertexOutput vert(vertexInput input) { vertexOutput output; output.pos = mul(UNITY_MATRIX_MVP, input.vertex); output.posInObjectCoords = input.vertex; return output; } float4 frag(vertexOutput input) : COLOR { if (input.posInObjectCoords.y > 0.0) { discard; // drop the fragment if y coordinate > 0 } return float4(1.0, 0.0, 0.0, 1.0); // red } ENDCG } // second pass (is executed after the first pass) Pass { Cull Back // cull only back faces CGPROGRAM #pragma vertex vert #pragma fragment frag struct vertexInput { float4 vertex : POSITION; }; struct vertexOutput { float4 pos : SV_POSITION; float4 posInObjectCoords : TEXCOORD0; }; vertexOutput vert(vertexInput input) { vertexOutput output; output.pos = mul(UNITY_MATRIX_MVP, input.vertex); output.posInObjectCoords = input.vertex; return output; } float4 frag(vertexOutput input) : COLOR { if (input.posInObjectCoords.y > 0.0) { discard; // drop the fragment if y coordinate > 0 } return float4(0.0, 1.0, 0.0, 1.0); // green } ENDCG } } }
记住Unity shader只有一个subshader会被执行(这个取决于GPU支持subshader的能力),但是这个subshader的所有psss会被执行。
在大多数的GPU上,这里有一个行之有效的方法在Cg中区分正面和背面,可以使用带有VFACE语义的片元输入参数;请查看Unity的shader语义文档。但是,不是所有的GPU都支持这种方法。
总结
恭喜你,又看完了一章。(如果你已经尝试了其中的某个任务:干得好!我还没做过呢!)这章我们学习了:
如何丢弃片元。
如何指定正面和背面剔除。
如何利用两个pass配合剔除在一个mesh的内部和外部使用不同的shader。
如何丢弃片元。
如何指定正面和背面剔除。
如何利用两个pass配合剔除在一个mesh的内部和外部使用不同的shader。