Cg 编程/Unity/屏幕覆盖
本教程介绍 **屏幕覆盖**。
它是关于非标准顶点变换的一系列教程中的第一个教程,这些教程偏离了在 “顶点变换”部分 中描述的标准顶点变换。本教程使用在 “纹理球体”部分 中描述的纹理,以及在 “透明度”部分 中描述的混合。
屏幕覆盖有很多应用,例如左侧图片中的标题,以及其他 GUI(图形用户界面)元素,如按钮或状态信息。这些元素的共同特点是它们应该始终显示在场景的顶部,并且永远不会被任何其他物体遮挡。这些元素也不应该受到任何相机移动的影响。因此,顶点变换应该直接从物体空间到屏幕空间。Unity 有多种方法可以在屏幕上的指定位置渲染纹理图像。本教程尝试使用一个简单的着色器来实现此目的。
让我们使用 X
和 Y
坐标来指定纹理在屏幕上的位置,这两个坐标表示渲染矩形的左下角的像素坐标,其中 在屏幕中心,以及渲染矩形的像素 Width
和 Height
。(相对于中心指定坐标通常允许我们在不进行进一步调整的情况下支持各种屏幕尺寸和纵横比。)我们使用这些着色器属性
Properties {
_MainTex ("Texture", Rect) = "white" {}
_Color ("Color", Color) = (1.0, 1.0, 1.0, 1.0)
_X ("X", Float) = 0.0
_Y ("Y", Float) = 0.0
_Width ("Width", Float) = 128
_Height ("Height", Float) = 128
}
以及相应的制服
uniform sampler2D _MainTex;
uniform float4 _Color;
uniform float _X;
uniform float _Y;
uniform float _Width;
uniform float _Height;
对于实际的物体,我们可以使用一个网格,它只包含两个三角形来形成一个矩形。但是,我们也可以直接使用默认的立方体物体,因为背面剔除(以及将三角形退化为边的剔除)可以确保只有立方体的两个三角形被光栅化。默认立方体物体的角点在物体空间中的坐标为 和 ,即矩形的左下角位于 ,而右上角位于 。为了将这些坐标变换为用户指定的屏幕空间坐标,我们首先将它们变换为像素的光栅位置,其中 位于屏幕的左下角
uniform float4 _ScreenParams; // x = width; y = height;
// z = 1 + 1.0/width; w = 1 + 1.0/height
...
vertexOutput vert(vertexInput input)
{
vertexOutput output;
float2 rasterPosition = float2(
_X + _ScreenParams.x / 2.0
+ _Width * (input.vertex.x + 0.5),
_Y + _ScreenParams.y / 2.0
+ _Height * (input.vertex.y + 0.5));
...
此变换将我们立方体正面面的左下角从物体空间中的 变换为光栅位置 float2(_X + _ScreenParams.x / 2.0, _Y + _ScreenParams.y / 2.0)
,其中 _ScreenParams.x
是屏幕宽度(以像素为单位),_ScreenParams.y
是屏幕高度(以像素为单位)。右上角从 变换为 float2(_X + _ScreenParams.x / 2.0 + _Width, _Y + _ScreenParams.y / 2.0 + _Height)
。光栅位置很方便,事实上它们经常在 OpenGL 中使用;但是,它们并不是我们这里真正需要的。
顶点着色器的输出参数位于所谓的“裁剪空间”中,如“顶点变换”部分所述。GPU 通过将这些坐标除以透视除法中的第四个坐标,将这些坐标转换为 和 之间的标准化设备坐标。如果我们将此第四个坐标设置为,则此除法不会改变任何内容;因此,我们可以将前三个坐标视为标准化设备坐标中的坐标,其中 指定了近平面上的屏幕左下角,而 指定了近平面上的屏幕右上角。为了将任何屏幕位置指定为顶点输出参数,我们必须在此坐标系中指定它。幸运的是,将光栅位置的 和 坐标转换为标准化设备坐标并不太难。对于 坐标,我们希望使用近裁剪平面的坐标。在 Unity 中,这取决于平台;因此,我们使用 Unity 的内置统一变量 _ProjectionParams.y
,它指定了近裁剪平面的 坐标。
output.pos = float4(
2.0 * rasterPosition.x / _ScreenParams.x - 1.0,
2.0 * rasterPosition.y / _ScreenParams.y - 1.0,
_ProjectionParams.y, // near plane is at -1.0 or at 0.0
1.0);
正如您很容易检查到的那样,这将光栅位置 float2(0,0)
转换为标准化设备坐标,并将光栅位置 float2(_ScreenParams.x, _ScreenParams.y)
转换为,这正是我们需要的。
还有一个复杂之处:有时 Unity 使用翻转的投影矩阵,其中 轴指向相反的方向。在这种情况下,我们必须将 坐标乘以 -1。我们可以通过将它乘以 _ProjectionParams.x
来实现这一点。
output.pos = float4(
2.0 * rasterPosition.x / _ScreenParams.x - 1.0,
_ProjectionParams.x * (2.0 * rasterPosition.y / _ScreenParams.y - 1.0),
_ProjectionParams.y, // near plane is at -1.0 or at 0.0
1.0);
这就是从对象空间到屏幕空间的顶点变换所需的一切。但是,我们仍然需要计算合适的纹理坐标,以便在正确的位置查找纹理图像。纹理坐标应介于 和 之间,这实际上很容易从对象空间中 和 之间的顶点坐标中计算出来。
output.tex = float4(input.vertex.x + 0.5,
input.vertex.y + 0.5, 0.0, 0.0);
// for a cube, vertex.x and vertex.y
// are -0.5 or 0.5
有了顶点输出参数 tex
,我们就可以使用简单的片段程序在纹理图像中查找颜色,并将其与用户指定的颜色 _Color
相乘。
float4 frag(vertexOutput input) : COLOR
{
return _Color * tex2D(_MainTex, input.tex.xy);
}
就是这样。
完整的着色器代码
[edit | edit source]如果将所有部分放在一起,我们将得到以下着色器,它使用 Overlay
队列在所有其他内容之后渲染对象,并使用 alpha 混合(参见“透明度”部分)以允许使用透明纹理。它还禁用深度测试,以确保纹理永远不会被遮挡。
Shader "Cg shader for screen overlays" {
Properties {
_MainTex ("Texture", Rect) = "white" {}
_Color ("Color", Color) = (1.0, 1.0, 1.0, 1.0)
_X ("X", Float) = 0.0
_Y ("Y", Float) = 0.0
_Width ("Width", Float) = 128
_Height ("Height", Float) = 128
}
SubShader {
Tags { "Queue" = "Overlay" } // render after everything else
Pass {
Blend SrcAlpha OneMinusSrcAlpha // use alpha blending
ZTest Always // deactivate depth test
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
// defines float4 _ScreenParams with x = width;
// y = height; z = 1 + 1.0/width; w = 1 + 1.0/height
// and defines float4 _ProjectionParams
// with x = 1 or x = -1 for flipped projection matrix;
// y = near clipping plane; z = far clipping plane; and
// w = 1 / far clipping plane
// User-specified uniforms
uniform sampler2D _MainTex;
uniform float4 _Color;
uniform float _X;
uniform float _Y;
uniform float _Width;
uniform float _Height;
struct vertexInput {
float4 vertex : POSITION;
float4 texcoord : TEXCOORD0;
};
struct vertexOutput {
float4 pos : SV_POSITION;
float4 tex : TEXCOORD0;
};
vertexOutput vert(vertexInput input)
{
vertexOutput output;
float2 rasterPosition = float2(
_X + _ScreenParams.x / 2.0
+ _Width * (input.vertex.x + 0.5),
_Y + _ScreenParams.y / 2.0
+ _Height * (input.vertex.y + 0.5));
output.pos = float4(
2.0 * rasterPosition.x / _ScreenParams.x - 1.0,
_ProjectionParams.x * (2.0 * rasterPosition.y / _ScreenParams.y - 1.0),
_ProjectionParams.y, // near plane is at -1.0 or at 0.0
1.0);
output.tex = float4(input.vertex.x + 0.5,
input.vertex.y + 0.5, 0.0, 0.0);
// for a cube, vertex.x and vertex.y
// are -0.5 or 0.5
return output;
}
float4 frag(vertexOutput input) : COLOR
{
return _Color * tex2D(_MainTex, input.tex.xy);
}
ENDCG
}
}
}
当您将此着色器用于立方体对象时,纹理图像可能会根据摄像机的方向出现和消失。这是由于 Unity 的裁剪造成的,Unity 不会渲染完全位于摄像机可见场景区域(视锥体)之外的对象。这种裁剪是基于对游戏对象的常规变换,这对于我们的着色器来说没有意义。为了禁用此裁剪,我们可以简单地将立方体对象设置为摄像机的子对象(通过将其拖到层次结构窗口中的摄像机上)。如果立方体对象随后放置在摄像机前面,它将始终保持在相同的相对位置,因此不会被 Unity 裁剪。(至少在游戏视图中不会。)
不透明屏幕覆盖的更改
[edit | edit source]着色器的许多更改都是可以想象的,例如不同的混合模式或不同的深度,以便在覆盖层前面拥有 3D 场景的几个对象。在这里,我们只关注不透明覆盖层。
不透明的屏幕覆盖将遮挡场景中的三角形。如果GPU能够识别这种遮挡,它就不需要光栅化这些被遮挡的三角形(例如,通过使用延迟渲染或早期深度测试)。为了确保GPU有机会应用这些优化,我们必须先渲染屏幕覆盖,方法是设置
标签 { "队列" = "背景" }
此外,我们应该避免混合,方法是删除 `混合` 指令。通过这些更改,不透明的屏幕覆盖更有可能提高性能,而不是降低光栅化性能。
恭喜你完成了另一个教程。我们已经看到了
- 如何使用Cg着色器渲染屏幕覆盖。
- 如何修改不透明屏幕覆盖的着色器。
如果你还想了解更多关于