C for Graphic:屏幕空间(屏幕坐标系)技术①

       这篇博文是为了补充前面遗漏的细节技术,以前咱们学习了图形流水线(渲染流程),咱们在CG shader代码中经常使用的图形流程就处理到了MVP变换,也就是只处理了顶点源坐标变换到裁剪空间的过程,可是后续裁剪空间到各个不一样显示设备空间的过程被忽略了,这是由于nvidiaCG以为裁剪空间到全球各个厂商的屏幕设备空间这个变换过程是繁琐的,这个操做过程最好被封装于开发者实现着色器以后,让开发者尽量专一于本身着色特效的研究。数组

       可是特殊状况下,让咱们不得不考虑这个问题,由于裁剪空间到当前屏幕空间这个变换能帮咱们实现一些好玩的着色效果。app

       好比前面我写的实时反射,模拟一面镜面的技术实现,裁剪空间到屏幕空间的变换就是重要的实现环节之一,下面我来画图依次说明一下:函数

       ①.获取当前眼睛渲染场景纹理关于镜面的对称纹理。学习

       

        本来正常渲染的场景画面通过对称变换后变成后面的纹理图案,固然树木超出屏幕长宽应该被截取掉的,我只是绘制保留了。spa

       ②.对三维裁剪空间中镜面网格顶点进行裁剪空间到屏幕设备空间的坐标变换,获得三维裁剪空间镜面网格顶点在二维屏幕设备中的坐标。3d

      

       这就是正视图和侧视图下三维裁剪空间中镜面网格顶点到二维屏幕空间坐标点的示意图了,裁剪空间是一个2*2*2的立方体,裁剪空间变换到屏幕空间,xy[-1,1]坐标份量要处理到屏幕x[0,1920]y[0,1080]像素值的(假设我用的1080p的显示器),z[-1,1]则变为了depth[0,256]深度值。code

      ③.获取到三维裁剪空间中镜面网格顶点在二维屏幕平面中的坐标后,根据坐标与屏幕分辨率的比例进行采样,采样目标就是①中的对称渲染纹理,那么咱们就获得了镜面在屏幕平面中那块区域对对称纹理采样的图像,就完美的在三维裁剪空间中的镜面上显示出了对称纹理,就完成了咱们的镜面反射技术。blog

      

      根据镜面在二维平面的顶点坐标进行对称纹理采样,那么镜面采样的对称纹理局部纹理就和mainCamera主摄像机渲染的画面纹理相结合,就产生了镜面反射效果。ip

      这就是以前真实反射(镜面反射)的实现原理,其中两个关键计算:开发

      ①.三维空间顶点在平面的对称点计算矩阵(前面咱们推导过)

      ②.裁剪空间到屏幕空间的变换(这个也简单,上面第二张图就比较形象了,一个[-1,1]到[0,设备像素长度]的比例计算)

      接下来就让咱们看下unity CG如何帮咱们处理裁剪空间到屏幕空间的变换,UnityCG.cginc和UnityShaderVariables.cginc内置代码以下:

      

      

      

      

      来详细分解一下(暂时不考虑UNITY_SINGLE_PASS_STEREO,这是一种VR双屏渲染的状况,之后作VR着色实现再来说解):

      ①._ProjectionParams.x = 1 or -1 (若是使用了翻转投影则为-1)

      ②.ComputeNonStereoScreenPos(float4 mvpPos);

           float4 o = mvpPos* 0.5f;   //首先mvpPos的(x,y,z)的齐次坐标形式<xw,yw,zw,w>,同时mvpPos(x,y,z)处于[-1,-1,-1]到[1,1,1]的2*2*2裁剪立方空间中,那么mvpPos的齐次坐标xw,yw,zw份量处于[-w]到[w]之间,在*0.5f获得新float4 o的x,y,z分量则处于[-0.5w]到[0.5w]之间,而o.w = 0.5*mvpPos.w。

           o.xy = float2(o.x, o.y*_ProjectionParams.x) + o.w;   //o.y首先要处理是否翻转的状况,而后o.x和o.y分别+o.w平移到[0,w]的区间。

           o.zw = mvpPos.zw;  //o.zw保持mvpPos.zw不变

      这么看来,实际上ComputeScreenPos(float4 mvpPos);并无跟我想象的同样直接计算出mvp变换后的裁剪空间顶点在屏幕设备空间中xy像素值,而是将xy归w化到[0,w]区间,z不变[-w,w],w不变。unity CG这么作的目的是保留w份量的同时能够/w进行归一化,方便咱们进行采样等计算。

      这里顺便再来看一下这个字段:

      

      _ScreenParams的xy储存了当前显示分辨率,咱们用这个参数处理一下ComputeScreenPos返回的值就能获得裁剪空间顶点在屏幕上真正的像素坐标了。

      写到这里,咱们基本上已经知道ComputeScreenPos这个函数的关键做用了,下面咱们就来经过一个CG着色例子来看下能达到什么效果,以下:

      

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TransparentEffect : MonoBehaviour
{
    public MeshFilter mMeshFilter;
    public MeshRenderer mMeshRender;
    public Camera mEyeCamera;

    private Mesh mMesh;

    private int xCount = 60; 
    private int yCount = 30;
    private float mCellLen = 1f;

    void Start()
    {
        genPerfectRenderTarget();
        genRectangleMesh();
    }

    //建立适合分辨率的eyeCamera渲染贴图
    private void genPerfectRenderTarget()
    {
        RenderTexture rt = new RenderTexture(mEyeCamera.pixelWidth, mEyeCamera.pixelHeight, 24);
        mEyeCamera.targetTexture = rt;
    }

    //建立一个rectange网格
    //圆前面已经讲过,这里不在过多讲解
    private void genRectangleMesh()
    {
        //构建一个任意单位长宽的长方形
        mMesh = new Mesh();
        int xPointCount = xCount + 1;   //x轴网格点数量
        int yPointCount = yCount + 1;   //y轴网格点数量
        int xyMeshPointCount = xPointCount * yPointCount;  //网格顶点的数量
        int triangleCount = xCount * yCount * 2;   //三角面数量(小正方形的两倍)
        //构建mesh网格的全部信息数组
        Vector3[] vertices = new Vector3[xyMeshPointCount];
        int[] triangles = new int[triangleCount * 3];
        Vector2[] uvs = new Vector2[xyMeshPointCount];
        //记录拓扑信息循环的间隔
        int triangleIndex = 0;
        for (int x = 0; x < xPointCount; x++)
        {
            for (int y = 0; y < yPointCount; y++)
            {
                int index = x + y * xPointCount;
                vertices[index] = new Vector3((xCount - x) * mCellLen, (yCount - y) * mCellLen, 0);

                if (x < xCount && y < yCount)
                {
                    //这里就是拓扑信息的循环计算,结合绘画的拓扑信息图算一下
                    triangles[triangleIndex] = x + y * xPointCount;
                    triangles[triangleIndex + 1] = x + (y + 1) * xPointCount;
                    triangles[triangleIndex + 2] = x + (y + 1) * xPointCount + 1;

                    triangles[triangleIndex + 3] = x + y * xPointCount;
                    triangles[triangleIndex + 4] = x + (y + 1) * xPointCount + 1;
                    triangles[triangleIndex + 5] = x + y * xPointCount + 1;

                    triangleIndex += 6;
                }
                uvs[index] = new Vector2((float)x / (float)xCount, (float)y / (float)yCount);
            }
        }
        mMesh.vertices = vertices;
        mMesh.triangles = triangles;
        mMesh.uv = uvs;
        mMeshFilter.mesh = mMesh;
    }

    void Update()
    {
        //这个函数标识了反转剔除模式
        //由于eyeCamera相机渲染被我用了对称矩阵修改了顶点
        //可是法向量没有修改,因此在计算机图形处理中会致使错误
        //只能反向剔除再render才能达到正确的现实
        GL.invertCulling = true;
        mEyeCamera.Render();
        GL.invertCulling = false;
        mMeshRender.sharedMaterial.SetTexture("_ReflTex", mEyeCamera.targetTexture);
    }
}

      

Shader "Unlit/TransparentEffectUnlitShader"
{
	Properties
	{
		_ReflTex("ReflTexture",2D) = "white" {}
		_Speed("Speed",Range(0.1,100)) = 0.5
		_Range("Range",Range(0.1,10)) = 0.1
	}
	SubShader
	{
		Tags { "RenderType"="Opaque" }
		LOD 100
		Cull back

		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"

			struct appdata
			{
				float4 vertex : POSITION;
			};

			struct v2f
			{
				float4 vertex : SV_POSITION;
				float4 screenPos : TEXCOORD1;
			};

			sampler2D _ReflTex;   //eyeCamera渲染纹理

			float _Speed;		 //波动速度
			float _Range;        //波动幅度
			
			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				//使用ComputeScreenPos获取mvpPos在屏幕空间中的齐次坐标
				o.screenPos = ComputeScreenPos(o.vertex);
				//这里注意,咱们先计算完screenPos后再进行顶点乱序三角函数波动
				float angle = _Speed*_Time*(o.vertex.x + o.vertex.y + o.vertex.z);
				o.vertex += _Range*sin(angle);
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				//采样eyeCamera渲染纹理
				fixed4 col = tex2D(_ReflTex, i.screenPos.xy / i.screenPos.w);
				return col;
			}
			ENDCG
		}
	}
}

      效果图以下:

  

       下半部的矩形,跟一张扭动的投影布片同样,这效果咋一看也没什么用,还不如上一篇镜面反射的做用大,这里无非就是告诉你们,裁剪空间到屏幕空间这一步很容易让人忽略的变换,也能够拿来作一些效果,后面来点比较实际的。

      so,咱们接下来继续。