跳转到内容

GLSL 编程/Unity/屏幕叠加

来自维基教科书,开放的世界,开放的书籍
1934 年一部电影的片头。

本教程涵盖屏幕叠加,在 Unity 中也被称为“GUI 纹理”。

这是关于非标准顶点变换的教程系列的第一个教程,这些教程偏离了在“顶点变换”部分中描述的标准顶点变换。本教程使用在“纹理球体”部分中描述的纹理以及在“透明度”部分中描述的混合。

Unity 的 GUI 纹理

[编辑 | 编辑源代码]

屏幕叠加(即 Unity 术语中的 GUI 纹理)有很多应用,例如左侧图像中的标题,还有其他 GUI(图形用户界面)元素,如按钮或状态信息。这些元素的共同特征是它们应该始终出现在场景的顶部,并且永远不会被任何其他对象遮挡。这些元素也不应该受到任何相机移动的影响。因此,顶点变换应该直接从物体空间到屏幕空间。Unity 的 GUI 纹理允许我们通过在屏幕上指定位置渲染纹理图像来渲染这种元素。本教程尝试借助着色器来重现 GUI 纹理的功能。通常,你仍然会使用 GUI 纹理而不是这种着色器;但是,着色器允许更大的灵活性,因为你可以根据自己的需要对其进行任何方式的调整,而 GUI 纹理只提供有限的可能性。(例如,你可以更改着色器,使 GPU 在对被不透明 GUI 纹理遮挡的三角形进行光栅化时花费更少的时间。)

用 GLSL 着色器模拟 GUI 纹理

[编辑 | 编辑源代码]

Unity 的 GUI 纹理的位置由渲染矩形的左下角的XY 坐标(以像素为单位)指定,其中 在屏幕中心,以及渲染矩形的宽度高度(以像素为单位)。为了模拟 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 纹理。
  • 如何修改着色器以实现不透明屏幕叠加。

进一步阅读

[编辑 | 编辑源代码]

如果您还想了解更多


< GLSL 编程/Unity

除非另有说明,本页上的所有示例源代码均属于公有领域。
华夏公益教科书