GLSL 编程/Unity/屏幕叠加
本教程涵盖屏幕叠加,在 Unity 中也被称为“GUI 纹理”。
这是关于非标准顶点变换的教程系列的第一个教程,这些教程偏离了在“顶点变换”部分中描述的标准顶点变换。本教程使用在“纹理球体”部分中描述的纹理以及在“透明度”部分中描述的混合。
屏幕叠加(即 Unity 术语中的 GUI 纹理)有很多应用,例如左侧图像中的标题,还有其他 GUI(图形用户界面)元素,如按钮或状态信息。这些元素的共同特征是它们应该始终出现在场景的顶部,并且永远不会被任何其他对象遮挡。这些元素也不应该受到任何相机移动的影响。因此,顶点变换应该直接从物体空间到屏幕空间。Unity 的 GUI 纹理允许我们通过在屏幕上指定位置渲染纹理图像来渲染这种元素。本教程尝试借助着色器来重现 GUI 纹理的功能。通常,你仍然会使用 GUI 纹理而不是这种着色器;但是,着色器允许更大的灵活性,因为你可以根据自己的需要对其进行任何方式的调整,而 GUI 纹理只提供有限的可能性。(例如,你可以更改着色器,使 GPU 在对被不透明 GUI 纹理遮挡的三角形进行光栅化时花费更少的时间。)
Unity 的 GUI 纹理的位置由渲染矩形的左下角的X
和Y
坐标(以像素为单位)指定,其中 在屏幕中心,以及渲染矩形的宽度
和高度
(以像素为单位)。为了模拟 GUI 纹理,我们使用类似的着色器属性
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
}
以及相应的 uniforms
uniform sampler2D _MainTex;
uniform vec4 _Color;
uniform float _X;
uniform float _Y;
uniform float _Width;
uniform float _Height;
对于实际对象,我们可以使用一个仅包含两个三角形的网格来形成一个矩形。但是,我们也可以只使用默认的立方体对象,因为背面剔除(以及退化为边的三角形的剔除)允许我们确保仅对立方体的两个三角形进行光栅化。默认立方体对象的角在物体空间中的坐标为 和 ,即矩形的左下角在,右上角在。为了将这些坐标变换为屏幕空间中用户指定的坐标,我们首先将它们变换为像素中的光栅位置,其中 在屏幕的左下角
uniform vec4 _ScreenParams; // x = width; y = height;
// z = 1 + 1.0/width; w = 1 + 1.0/height
...
#ifdef VERTEX
void main()
{
vec2 rasterPosition = vec2(
_X + _ScreenParams.x / 2.0
+ _Width * (gl_Vertex.x + 0.5),
_Y + _ScreenParams.y / 2.0
+ _Height * (gl_Vertex.y + 0.5));
...
这种变换将我们立方体正面左下角从物体空间中的 变换为光栅位置vec2(_X + _ScreenParams.x / 2.0, _Y + _ScreenParams.y / 2.0)
,其中_ScreenParams.x
是屏幕宽度(以像素为单位),_ScreenParams.y
是高度(以像素为单位)。右上角从 变换为vec2(_X + _ScreenParams.x / 2.0 + _Width, _Y + _ScreenParams.y / 2.0 + _Height)
。光栅位置很方便,实际上它们经常在 OpenGL 中使用;但是,它们并不是我们这里真正需要的。
顶点着色器在gl_Position
中的输出是在所谓的“裁剪空间”中,如“顶点变换”部分所述。GPU 通过将这些坐标除以透视除法的第四个坐标gl_Position.w
来将它们变换为 和 之间的归一化设备坐标。如果我们将此第四个坐标设置为 ,则此除法不会改变任何内容;因此,我们可以将gl_Position
的前三个坐标视为归一化设备坐标中的坐标,其中 指定了近平面上的屏幕左下角,而 指定了近平面上的屏幕右上角。(我们应该使用近平面以确保矩形位于其他所有内容的前面。)为了在gl_Position
中指定任何屏幕位置,我们必须在此坐标系中指定它。幸运的是,将光栅位置转换为归一化设备坐标并不太难。
gl_Position = vec4(
2.0 * rasterPosition.x / _ScreenParams.x - 1.0,
2.0 * rasterPosition.y / _ScreenParams.y - 1.0,
-1.0, // near plane
1.0 // all coordinates are divided by this coordinate
);
正如你很容易验证的那样,这将光栅位置vec2(0,0)
转换为归一化设备坐标 ,并将光栅位置vec2(_ScreenParams.x, _ScreenParams.y)
转换为 ,这正是我们需要的。
这对于从对象空间到屏幕空间的顶点变换来说已经足够了。但是,我们仍然需要计算适当的纹理坐标,以便在正确的位置查找纹理图像。纹理坐标应该介于 和 之间,这实际上很容易从对象空间中介于 和 之间的顶点坐标中计算出来。
textureCoords =
vec4(gl_Vertex.x + 0.5, gl_Vertex.y + 0.5, 0.0, 0.0);
// for a cube, gl_Vertex.x and gl_Vertex.y
// are -0.5 or 0.5
使用变化的变量textureCoords
,我们就可以使用一个简单的片段程序在纹理图像中查找颜色,并用用户指定的颜色_Color
进行调制。
#ifdef FRAGMENT
void main()
{
gl_FragColor =
_Color * texture2D (_MainTex, vec2(textureCoords));
}
#endif
就是这样。
完整的着色器代码
[edit | edit source]如果我们将所有部分放在一起,我们将得到以下着色器,它使用Overlay
队列在所有其他内容之后渲染对象,并使用 alpha 混合(见“透明度”部分)以允许透明纹理。它还停用深度测试以确保纹理永远不会被遮挡。
Shader "GLSL 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
GLSLPROGRAM
// User-specified uniforms
uniform sampler2D _MainTex;
uniform vec4 _Color;
uniform float _X;
uniform float _Y;
uniform float _Width;
uniform float _Height;
// The following built-in uniforms
// are also defined in "UnityCG.glslinc",
// i.e. one could #include "UnityCG.glslinc"
uniform vec4 _ScreenParams; // x = width; y = height;
// z = 1 + 1.0/width; w = 1 + 1.0/height
// Varyings
varying vec4 textureCoords;
#ifdef VERTEX
void main()
{
vec2 rasterPosition = vec2(
_X + _ScreenParams.x / 2.0
+ _Width * (gl_Vertex.x + 0.5),
_Y + _ScreenParams.y / 2.0
+ _Height * (gl_Vertex.y + 0.5));
gl_Position = vec4(
2.0 * rasterPosition.x / _ScreenParams.x - 1.0,
2.0 * rasterPosition.y / _ScreenParams.y - 1.0,
-1.0, // near plane is -1.0
1.0);
textureCoords =
vec4(gl_Vertex.x + 0.5, gl_Vertex.y + 0.5, 0.0, 0.0);
// for a cube, gl_Vertex.x and gl_Vertex.y
// are -0.5 or 0.5
}
#endif
#ifdef FRAGMENT
void main()
{
gl_FragColor =
_Color * texture2D (_MainTex, vec2(textureCoords));
}
#endif
ENDGLSL
}
}
}
当你将此着色器用于立方体对象时,纹理图像可能会根据相机的方向而出现和消失。这是由于 Unity 的裁剪造成的,Unity 不会渲染完全位于场景中相机可见区域(视锥体)之外的对象。此裁剪基于游戏对象的传统变换,这对我们的着色器来说没有意义。为了停用此裁剪,我们可以简单地使立方体对象成为相机的子对象(通过将它拖到层次结构视图中的相机上)。如果立方体对象随后被放置在相机的前面,它将始终保持在相同的相对位置,因此它不会被 Unity 裁剪。(至少在游戏视图中不会。)
不透明屏幕叠加的更改
[edit | edit source]对着色器可以进行许多更改,例如不同的混合模式或不同的深度,以使 3D 场景中的几个对象位于叠加层前面。在这里,我们只关注不透明叠加层。
不透明屏幕叠加层会遮挡场景中的三角形。如果 GPU 了解此遮挡,它就不需要对这些被遮挡的三角形进行光栅化(例如,通过使用延迟渲染或早期深度测试)。为了确保 GPU 有机会应用这些优化,我们必须首先渲染屏幕叠加层,方法是设置
Tags { "Queue" = "Background" }
此外,我们应该避免混合,方法是移除Blend
指令。通过这些更改,不透明屏幕叠加层可能会提高性能,而不是造成光栅化性能损失。
总结
[edit | edit source]恭喜您完成了另一个教程。我们已经看到了
- 如何使用 GLSL 着色器模拟 GUI 纹理。
- 如何修改着色器以实现不透明屏幕叠加。
如果您还想了解更多