Cg Programming/Unity/Screen Overlays屏幕叠加

来源:互联网 发布:阿里云怎么绑定域名 编辑:程序博客网 时间:2024/05/21 17:07

本教程涵盖了屏幕叠加。

这是关于非标准顶点变换系列教程的第一篇,它偏离了章节“顶点变换”描述的标准顶点变换。这个特别的教程使用了章节“纹理球体”中描述的纹理映射和章节“透明度”中描述的混合。

屏幕叠加

这里写图片描述

有很多屏幕叠加的应用,比如上图中的标题,当然也有其它GUI(图形用户界面)元素如按钮或状态信息。这些元素的共同特征是它们应该总是出现在场景的上面,并且永远不会被其它对象遮挡。这些元素也不应该受到任何摄像机运动的影响。于是,顶点变换应该直接从对象空间变换到屏幕空间。Unity有多种方法将纹理贴图绘制到屏幕的指定位置上。本教程尝试用一个简单的着色器来达到这个目的。

用Cg着色器渲染一张贴图到屏幕上

我们用左下角的XY坐标,屏幕中心点(0, 0)处以像素表示的渲染矩形的左角,以及以像素表示的渲染矩形的WidthHeight来指定贴图的屏幕位置。(指定相对于中心点的坐标通常允许我们支持多种屏幕尺寸和宽高比而不需要进一步调整。)我们使用这些着色器的属性:

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变量为:

         uniform sampler2D _MainTex;         uniform float4 _Color;         uniform float _X;         uniform float _Y;         uniform float _Width;         uniform float _Height;

对于实际对象,我们会使用由两个三角形组成的网格来形成一个矩形。但是,我们也可以使用默认的立方体对象,因为背面剔除(对退化到边缘的三角形的剔除)允许我们确保只有立方体的两个三角形被光栅化。默认立方体对象的角在对象空间中的坐标为-0.5+0.5,即矩形左下角在(-0.5, -0.5)且右上角在(+0.5, +0.5)。为了把这些坐标变换到屏幕空间中用户自定义的坐标,我们首先要把他们变换到屏幕左下角* (0,0)*处以像素表示的光栅化位置上:

  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));            ...

这个变换把我们立方体正面的左下角从对象空间(-0.5,-0.5)变换到光栅化位置float2(_X + _ScreenParams.x / 2.0, _Y + _ScreenParams.y / 2.0),这里_ScreenParams.x是以像素表示的屏幕宽度,_ScreenParams.y是以像素表示的屏幕高度。右上角从(+0.5,+0.5)变换到了float2(_X + _ScreenParams.x / 2.0 + _Width, _Y + _ScreenParams.y / 2.0 + _Height)。光栅位置很实用,并且实际上它经常在OpenGL被使用到。但是,它们不是我们所需要的。

顶点着色器的输出参数在所谓的“裁剪空间”中,如章节“顶点变换”中讨论过的那样。GPU通过在透视除法中除以第四个坐标,把这些坐标变换到-1到1之间的归一化设备坐标。如果我们设置第四个坐标为1,这个除法就没有改变任何东西;于是,我们可以将前三个坐标作为归一化设备坐标中的坐标,这里, (-1,-1,-1)指定了近裁剪平面上屏幕的左下角,(1,1,-1)指定了近裁剪平面的右上角。 为了指定任意坐标作为顶点输出参数,我们必须在这个坐标系统中指定它。幸运地是,把光栅化位置的xy坐标变换到归一化的设备坐标并不是很难。对于坐标z我们希望使用近裁剪平面的坐标。在Unity中,这个取决于平台;因此,我们使用Unity内置的uniform参数_ProjectionParams.y,它指定了近裁剪平面的z坐标。

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)变换到了归一化的设备坐标(-1.0, -1.0),以及把光栅化位置float2(_ScreenParams.x, _ScreenParams.y)变换到(1.0, 1.0),这就是我们想要的。这里还有一个麻烦:有时Unity会使用翻转投影矩阵,它的y轴指向相反的方向。在这种情况下,我们必须用-1乘以y坐标。我们可以通过乘以_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);

这是我们从对象空间到屏幕空间的顶点变换所需要的全部信息。但是,我们仍然需要计算恰当的纹理坐标以便可以在正确的位置查询到纹理贴图。纹理坐标的范围应该在0.01.0之间。它实际上很容易从范围在(-0.5,0.5)之间的在对象空间中的顶点坐标计算出来:

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);     }

完整着色器代码

如果把所有碎片拼接在一起,我们可以得到以下的着色器,它使用叠加队列在所有对象之后渲染对象,并且使用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" } // 在所有对象之后渲染      Pass {         Blend SrcAlpha OneMinusSrcAlpha // 使用alpha混合         ZTest Always // 关闭深度测试         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的裁剪造成的,它不渲染在摄像机(视截体)中可见的完全在场景区域之外的对象。这个裁剪是基于游戏对象的传统变换上的,这跟我们的着色器没有多大关系。为了不激活这个裁剪,我们可以简单地使立方体对象成为摄像机的子对象(通过在Hierarchy Window中把它拖到摄像机下面)。如果立方体对象被放置到摄像机的前面,它会总是保持在相同的相对位置上,这样它就不会被Unity裁剪掉了。(至少在游戏视口中不会了。)

不透明屏幕叠加的改变

着色器的很多变化是可以想象的,比如在叠加前面3D场景的一些对象有不同的混合模式或不同的深度。这里我们只看不透明叠加。

一个不透明的屏幕叠加将会遮挡场景的三角形。如果GPU知道这个遮挡,它就没有必要光栅化这些被遮挡的三角形(比如通过使用延迟渲染或是早期深度测试)。为了确保GPU有机会应用这些优化,我们必须通过设置Tags { "Queue" = "Background" }首先渲染屏幕叠加。而且,我们应该通过移除Blend指令避免混合。随着这些变化,不透明屏幕叠加就有可能提升性能而不是光栅化性能的消耗。

总结

恭喜,你完成了本教程的学习。我们看到了:

  • 如何用Cg着色器渲染屏幕叠加。
  • 如何为不透明屏幕叠加修改这个着色器。

扩展阅读

  • 关于纹理映射,你应该阅读章节“纹理球体”。
  • 关于混合,你应该阅读章节“透明度”。
  • 关于对你空间,屏幕空间,裁剪空间,归一化设置坐标,透视除法等,你应该阅读章节“顶点变换”中标准顶点变换的描述。