斜视锥体裁剪Camera.CalculateObliqueMatrix的推导

截取自: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的逆转置矩阵:

Pc = Pv(M^{-1})^{^{T}}

Pv = Pc((M^{-1})^{T})^{-1}

这里就需要线性代数里面的一个性质啦,如果一个矩阵可逆,那么这个矩阵的转置的逆等于逆的转置,对于上面公式来说:

(M^{T})^{-1} = (M^{-1})^{T}

进而可以将Pv和Pc的变换公式进一步化简为:

Pv = PcM^{T}

下面是第二个预备知识点,关于裁剪空间的。我们知道,经过投影矩阵变换后,会被变换到裁剪空间(实际上此时还没有经过透视除法,属于用齐次座标系表示的座标,此处我们为了方便表示最终的立方体,假设进行了透视除法,各个分量除以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版本的一致,均可以裁减掉位于原始近裁剪面和平面之间的内容:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章