截取自:https://blog.csdn.net/puppet_master/article/details/80808486
原文是實現平面反射的過程中出現了,裁剪錯誤的問題,通過限制反射相機的投影矩陣進行修復,
用到了Camera.CalculateObliqueMatrix的api,對這個api的原理進行了推導和實現
以下是原文
上面的反射看似完美,但是卻有一個非常致命的問題,看下面一幅圖,我們把其中一個模型向下移動,讓其逐漸到平面以下:
似乎不太對。。。虛像倒着升了起來,恩,不太符合常理。其實仔細考慮一下Planar Reflection的原理,應該就能想明白啦。我們把相機在相對於平面對稱的位置進行渲染,物體在平面上沒有問題,但是物體在平面下的時候,平面並沒有擋住虛像的渲染,在反射圖中還會會存在反射圖像,在採樣的時候,就會得到錯誤的效果。如下圖:
C爲正常相機,AB爲平面,D爲反射相機,GH爲相機D的近裁剪面,EF爲相機D的遠裁剪面。在平面AB上方的物體I渲染正常,但是在AB下方的物體,並沒有在D的近裁剪面內,所以仍然會渲染,就導致了錯誤的結果。
那麼核心需就是,怎樣用反射平面進行裁剪,把在反射平面以下的內容全部裁剪掉。也就是說上圖中反射相機D的近裁剪面不再是GH,而是替換爲AB平面。這個技術也就是所謂的斜視錐體裁剪-Oblique View Frustum Clippling,可以參考《Oblique View Frustum Depth Projection and Clipping》這篇論文。
Unity已經爲我們提供了一個接口,直接可以對相機和一個平面計算出斜視錐體裁剪投影矩陣,代碼如下:
/********************************************************************
FileName: PlanarReflection.cs
Description: 平面反射效果
history: 15:8:2018 by puppet_master
https://blog.csdn.net/puppet_master
*********************************************************************/
using UnityEngine;
[ExecuteInEditMode]
public class PlanarReflection : MonoBehaviour
{
private Camera reflectionCamera = null;
private RenderTexture reflectionRT = null;
private bool isReflectionCameraRendering = false;
private Material reflectionMaterial = null;
private void OnWillRenderObject()
{
if (isReflectionCameraRendering)
return;
isReflectionCameraRendering = true;
if (reflectionCamera == null)
{
var go = new GameObject("Reflection Camera");
reflectionCamera = go.AddComponent<Camera>();
reflectionCamera.CopyFrom(Camera.current);
}
if (reflectionRT == null)
{
reflectionRT = RenderTexture.GetTemporary(1024, 1024, 24);
}
//需要實時同步相機的參數,比如編輯器下滾動滾輪,Editor相機的遠近裁剪面就會變化
UpdateCamearaParams(Camera.current, reflectionCamera);
reflectionCamera.targetTexture = reflectionRT;
reflectionCamera.enabled = false;
var reflectM = CaculateReflectMatrix();
reflectionCamera.worldToCameraMatrix = Camera.current.worldToCameraMatrix * reflectM;
var normal = transform.up;
var d = -Vector3.Dot(normal, transform.position);
var plane = new Vector4(normal.x, normal.y, normal.z, d);
//用逆轉置矩陣將平面從世界空間變換到反射相機空間
var viewSpacePlane = reflectionCamera.worldToCameraMatrix.inverse.transpose * plane;
var clipMatrix = reflectionCamera.CalculateObliqueMatrix(viewSpacePlane);
reflectionCamera.projectionMatrix = clipMatrix;
GL.invertCulling = true;
reflectionCamera.Render();
GL.invertCulling = false;
if (reflectionMaterial == null)
{
var renderer = GetComponent<Renderer>();
reflectionMaterial = renderer.sharedMaterial;
}
reflectionMaterial.SetTexture("_ReflectionTex", reflectionRT);
isReflectionCameraRendering = false;
}
Matrix4x4 CaculateReflectMatrix()
{
var normal = transform.up;
var d = -Vector3.Dot(normal, transform.position);
var reflectM = new Matrix4x4();
reflectM.m00 = 1 - 2 * normal.x * normal.x;
reflectM.m01 = -2 * normal.x * normal.y;
reflectM.m02 = -2 * normal.x * normal.z;
reflectM.m03 = -2 * d * normal.x;
reflectM.m10 = -2 * normal.x * normal.y;
reflectM.m11 = 1 - 2 * normal.y * normal.y;
reflectM.m12 = -2 * normal.y * normal.z;
reflectM.m13 = -2 * d * normal.y;
reflectM.m20 = -2 * normal.x * normal.z;
reflectM.m21 = -2 * normal.y * normal.z;
reflectM.m22 = 1 - 2 * normal.z * normal.z;
reflectM.m23 = -2 * d * normal.z;
reflectM.m30 = 0;
reflectM.m31 = 0;
reflectM.m32 = 0;
reflectM.m33 = 1;
return reflectM;
}
private void UpdateCamearaParams(Camera srcCamera, Camera destCamera)
{
if (destCamera == null || srcCamera == null)
return;
destCamera.clearFlags = srcCamera.clearFlags;
destCamera.backgroundColor = srcCamera.backgroundColor;
destCamera.farClipPlane = srcCamera.farClipPlane;
destCamera.nearClipPlane = srcCamera.nearClipPlane;
destCamera.orthographic = srcCamera.orthographic;
destCamera.fieldOfView = srcCamera.fieldOfView;
destCamera.aspect = srcCamera.aspect;
destCamera.orthographicSize = srcCamera.orthographicSize;
}
private Matrix4x4 CaculateObliqueViewFrustumMatrix(Vector4 plane, Camera camera)
{
var viewSpacePlane = camera.worldToCameraMatrix.inverse.transpose * plane;
return camera.CalculateObliqueMatrix(viewSpacePlane);
}
}
這樣,通過這樣一個API,我們很容易可以求得被視空間的一個平面裁剪過的投影矩陣,再應用回攝像機,這樣就不會出現穿幫啦:
API雖然簡單,但是API背後的原理還是需要一番推導的,下面看一下斜視錐體裁剪的推導過程。
首先需要幾個預備的知識點。第一點,既然需要平面裁剪,就免不了對平面進行一些座標空間的變換,上面我們推導過平面的表示,可以用平面法向量和平面上一點與法向量點積的相反數。平面的變換與法線變換類似,不能直接進行變換,對於非uniform類型可能導致法線不垂直於平面,所以平面的變換也採用矩陣逆轉置的方式進行(關於法線的變換,可以參考之前描邊效果的blog)。即,如果我們已知一個View空間的平面Pv,要想將其轉化到裁剪空間Pc,就需要投影矩陣M的逆轉置矩陣:
這裏就需要線性代數裏面的一個性質啦,如果一個矩陣可逆,那麼這個矩陣的轉置的逆等於逆的轉置,對於上面公式來說:
進而可以將Pv和Pc的變換公式進一步化簡爲:
下面是第二個預備知識點,關於裁剪空間的。我們知道,經過投影矩陣變換後,會被變換到裁剪空間(實際上此時還沒有經過透視除法,屬於用齊次座標系表示的座標,此處我們爲了方便表示最終的立方體,假設進行了透視除法,各個分量除以w分量,將變換的結果置爲一個標準的立方體,二者的表示結果實際上是等價的),視錐體這個平頭截體會被變換成一個立方體(OpenGL是正方體,區間(-1,1),DX是普通立方體,xy區間(-1,1),z區間(0,1)),我們以Unity用的OpenGL風格變換爲例,最終一個視錐體的前後左右上下六個面在裁剪空間就都會被變換到標準立方體的六個面上。
通過我們之前推導的平面表示的方程,我們很容易地可以表示出裁剪空間下視錐體六個面的平面方程,然後根據,我們就可以求得在視空間下視錐體六個面的平面方程,如下圖(該圖片來自上文中提到的論文):
根據上圖中的變換結果,N = M4 + M3,F = M4 - M3,我們需要用一個自定義的平面P來代替默認的近裁剪平面Near,也就是說新的P裁剪面也需要滿足P = M4 + M3這個條件。要想修改近裁剪面N使之變成P,我們就需要調整M4或者M3這兩個向量中的值。M4是投影矩陣的最後一行,包含了z值透視投影等信息,是後續透視除法必須的,所以我們就只能改動M3這一行。
M3' = P - M4,F = M4 - M3,F' = M4 - M3' => F' = 2M4 - P
似乎我們只需要求出M3'然後帶入就大功告成了。不過這樣有一個問題,在於本身P可能不平行於XY平面,得到的遠裁剪面也可能不平行,保證了近裁剪面正確,遠裁剪面的位置是一個未知的值,可能截斷了原來的視錐體,這樣可能會導致一些不該被裁剪掉的物體也被裁減掉了;也可能偏移出視錐體很遠,進而對深度值造成影響。因此,我們需要考慮讓遠裁剪面也位於一個合適的位置。
由於公式F' = 2M4 - P,M4不能動,P平面也不能動,那麼我們可以考慮給P乘以一個係數,一個平面方程,整體乘以一個係數後,表示的仍然是原來的平面,即F' = 2M4 - uP,這樣M4,uP都沒有變化,但是最終的F'就會變化了。我們可以求得一個合適的u,使遠裁剪面不截斷原來的視錐體同時又與P平面夾角最小。那麼這個平面就是過視錐體原始邊界的一個頂點即可,如下圖所示:
HG爲原始的近裁剪面,FE爲原始的遠裁剪面,AB爲新的近裁剪面(也就是上文的P平面),在裁剪空間(此時應該沒進行透視除法,但是爲了方便,我們假設w = 1)下的座標邊界爲E(+-1,+-1,1,1),那麼我們要求的遠裁剪面的邊界點就是AB平面面對的裁剪面的邊界點即可。也就是說,E點xy座標的正負取決於AB平面的朝向,我們可以用AB平面(P平面)的法線進行表示,由於我們目前可以得到視空間的P平面方程,而E點座標目前是裁剪空間的,不過投影變換不會再去改變xy的符號,所以我們直接取視空間P平面xy值的符號即可。
那麼,裁剪空間下E點座標爲E(Sign(P.x), Sign(P.y),1,1),我們可以將其乘以投影矩陣的逆矩陣變換回視空間的E點,即E = (Sign(P.x), Sign(P.y),1,1) * ProjectionMatrix.Inverse。此時,我們知道了新的遠裁剪面方程F' = 2M4 - P,又知道新的遠裁剪面過E點,根據平面公式,F’·E = 0可得:
(2M4 - uP)·E = 0 => u= 2*(M4 · E) / (P · E)
我們就可以求得u值,進而根據M3' = uP - M4得到最終的M3值,對投影矩陣進行修改。
不過,此處有一個小優化。首先,看一下OpenGL版本的投影矩陣:
投影矩陣的逆矩陣:
關於投影矩陣的推導,可以參考之前軟渲染這篇blog,不過本人之前推導的是DX風格的矩陣,GL風格原理也是一樣的。
M4(0,0,-1,0),E = (+-1,+-1,1,1) * Projection.Inverse,看似比較複雜,但是實際上M4·E ,由於M4僅z項非零,我們只看E點的z項即可。那麼其實只需要考慮的是Porjection.Inverse的第三行與E點乘的結果,而Porjection.InverseM3 = (0,0,0,-1),即E.z = (0,0,0,-1)· (+-1, +-1, 1, 1) = -1,最終得到M4·E爲定值1,因此可以直接省去公式u= 2*(M4 · E) / (P · E)中M4·E的計算:
u = 2 / (P · E)
完整C#代碼如下:
/********************************************************************
FileName: PlanarReflection.cs
Description: 平面反射效果
history: 15:8:2018 by puppet_master
https://blog.csdn.net/puppet_master
*********************************************************************/
using UnityEngine;
[ExecuteInEditMode]
public class PlanarReflection : MonoBehaviour
{
private Camera reflectionCamera = null;
private RenderTexture reflectionRT = null;
private bool isReflectionCameraRendering = false;
private Material reflectionMaterial = null;
private void OnWillRenderObject()
{
if (isReflectionCameraRendering)
return;
isReflectionCameraRendering = true;
if (reflectionCamera == null)
{
var go = new GameObject("Reflection Camera");
reflectionCamera = go.AddComponent<Camera>();
reflectionCamera.CopyFrom(Camera.current);
reflectionCamera.hideFlags = HideFlags.HideAndDontSave;
}
if (reflectionRT == null)
{
reflectionRT = RenderTexture.GetTemporary(1024, 1024, 24);
}
//需要實時同步相機的參數,比如編輯器下滾動滾輪,Editor相機的遠近裁剪面就會變化
UpdateCamearaParams(Camera.current, reflectionCamera);
reflectionCamera.targetTexture = reflectionRT;
reflectionCamera.enabled = false;
var reflectM = CaculateReflectMatrix();
reflectionCamera.worldToCameraMatrix = Camera.current.worldToCameraMatrix * reflectM;
var normal = transform.up;
var d = -Vector3.Dot(normal, transform.position);
var plane = new Vector4(normal.x, normal.y, normal.z, d);
//用逆轉置矩陣將平面從世界空間變換到反射相機空間
var clipMatrix = CalculateObliqueMatrix(plane, reflectionCamera);
reflectionCamera.projectionMatrix = clipMatrix;
GL.invertCulling = true;
reflectionCamera.Render();
GL.invertCulling = false;
if (reflectionMaterial == null)
{
var renderer = GetComponent<Renderer>();
reflectionMaterial = renderer.sharedMaterial;
}
reflectionMaterial.SetTexture("_ReflectionTex", reflectionRT);
isReflectionCameraRendering = false;
}
Matrix4x4 CaculateReflectMatrix()
{
var normal = transform.up;
var d = -Vector3.Dot(normal, transform.position);
var reflectM = new Matrix4x4();
reflectM.m00 = 1 - 2 * normal.x * normal.x;
reflectM.m01 = -2 * normal.x * normal.y;
reflectM.m02 = -2 * normal.x * normal.z;
reflectM.m03 = -2 * d * normal.x;
reflectM.m10 = -2 * normal.x * normal.y;
reflectM.m11 = 1 - 2 * normal.y * normal.y;
reflectM.m12 = -2 * normal.y * normal.z;
reflectM.m13 = -2 * d * normal.y;
reflectM.m20 = -2 * normal.x * normal.z;
reflectM.m21 = -2 * normal.y * normal.z;
reflectM.m22 = 1 - 2 * normal.z * normal.z;
reflectM.m23 = -2 * d * normal.z;
reflectM.m30 = 0;
reflectM.m31 = 0;
reflectM.m32 = 0;
reflectM.m33 = 1;
return reflectM;
}
private void OnDisable()
{
DestroyImmediate(reflectionCamera.gameObject);
RenderTexture.ReleaseTemporary(reflectionRT);
reflectionCamera = null;
reflectionRT = null;
}
private void UpdateCamearaParams(Camera srcCamera, Camera destCamera)
{
if (destCamera == null || srcCamera == null)
return;
destCamera.clearFlags = srcCamera.clearFlags;
destCamera.backgroundColor = srcCamera.backgroundColor;
destCamera.farClipPlane = srcCamera.farClipPlane;
destCamera.nearClipPlane = srcCamera.nearClipPlane;
destCamera.orthographic = srcCamera.orthographic;
destCamera.fieldOfView = srcCamera.fieldOfView;
destCamera.aspect = srcCamera.aspect;
destCamera.orthographicSize = srcCamera.orthographicSize;
}
private Matrix4x4 CalculateObliqueMatrix(Vector4 plane, Camera camera)
{
var viewSpacePlane = camera.worldToCameraMatrix.inverse.transpose * plane;
var projectionMatrix = camera.projectionMatrix;
var clipSpaceFarPanelBoundPoint = new Vector4(Mathf.Sign(viewSpacePlane.x), Mathf.Sign(viewSpacePlane.y), 1, 1);
var viewSpaceFarPanelBoundPoint = camera.projectionMatrix.inverse * clipSpaceFarPanelBoundPoint;
var m4 = new Vector4(projectionMatrix.m30, projectionMatrix.m31, projectionMatrix.m32, projectionMatrix.m33);
//u = 2 * (M4·E)/(E·P),而M4·E == 1,化簡得
//var u = 2.0f * Vector4.Dot(m4, viewSpaceFarPanelBoundPoint) / Vector4.Dot(viewSpaceFarPanelBoundPoint, viewSpacePlane);
var u = 2.0f / Vector4.Dot(viewSpaceFarPanelBoundPoint, viewSpacePlane);
var newViewSpaceNearPlane = u * viewSpacePlane;
//M3' = P - M4
var m3 = newViewSpaceNearPlane - m4;
projectionMatrix.m20 = m3.x;
projectionMatrix.m21 = m3.y;
projectionMatrix.m22 = m3.z;
projectionMatrix.m23 = m3.w;
return projectionMatrix;
}
}
效果與API版本的一致,均可以裁減掉位於原始近裁剪面和平面之間的內容: