C for Graphic:螢幕空間(螢幕座標系)技術①
這篇博文是為了補充前面遺漏的細節技術,之前我們學習了圖形流水線(渲染流程),我們在CG shader程式碼中常用的圖形流程就處理到了MVP變換,也就是隻處理了頂點源座標變換到裁剪空間的過程,但是後續裁剪空間到各個不同顯示裝置空間的過程被忽略了,這是因為nvidiaCG覺得裁剪空間到全球各個廠商的螢幕裝置空間這個變換過程是繁瑣的,這個操作過程最好被封裝於開發者實現著色器之後,讓開發者儘可能專注於自己著色特效的研究。
但是特殊情況下,讓我們不得不考慮這個問題,因為裁剪空間到當前螢幕空間這個變換能幫我們實現一些好玩的著色效果。
比如前面我寫的實時反射,模擬一面鏡面的技術實現,裁剪空間到螢幕空間的變換就是重要的實現環節之一,下面我來畫圖依次說明一下:
①.獲取當前眼睛渲染場景紋理關於鏡面的對稱紋理。
原本正常渲染的場景畫面經過對稱變換後變成後面的紋理圖案,當然樹木超出螢幕長寬應該被擷取掉的,我只是繪製保留了。
②.對三維裁剪空間中鏡面網格頂點進行裁剪空間到螢幕裝置空間的座標變換,得到三維裁剪空間鏡面網格頂點在二維螢幕裝置中的座標。
這就是正檢視和側檢視下三維裁剪空間中鏡面網格頂點到二維螢幕空間座標點的示意圖了,裁剪空間是一個2*2*2的立方體,裁剪空間變換到螢幕空間,xy[0-1]座標是需要乘上螢幕x[0-1920]y[0-1080]畫素值的(假設我用的1080p的顯示器),z[0-1]則變為了depth[0-256]深度值。
③.獲取到三維裁剪空間中鏡面網格頂點在二維螢幕平面中的座標後,根據座標與螢幕解析度的比例進行取樣,取樣目標就是①中的對稱渲染紋理,那麼我們就得到了鏡面在螢幕平面中那塊區域對對稱紋理取樣的影象,就完美的在三維裁剪空間中的鏡面上顯示出了對稱紋理,就完成了我們的鏡面反射技術。
根據鏡面在二維平面的頂點座標進行對稱紋理取樣,那麼鏡面取樣的對稱紋理區域性紋理就和mainCamera主攝像機渲染的畫面紋理相結合,就產生了鏡面反射效果。
這就是之前真實反射(鏡面反射)的實現原理,其中兩個關鍵計算:
①.三維空間頂點在平面的對稱點計算矩陣(前面我們推導過,這裡我就不再說廢話了)
②.裁剪空間到螢幕空間的變換(這個也簡單,上面第二張圖就比較形象了,一個0-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不變。
這裡順便再來看一下這個欄位:
_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 = 50;
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,我們接下來繼續。