七彩极 发表于 2022-4-20 15:10

Unity 实现平面反射(基于 URP)

平面反射(Planar Reflection)原理

平面反射可以模拟光滑度很高的镜面效果,但也只能用于高度一致的平面,用在水面的效果是非常不错的。网上的文章讲反射矩阵和斜截视锥体的文章很多了,但细讲反射原理的很少,我觉得还是有些细节值得讲的。
基本思路

首先看一下平面反射的实现思路。(关于最基本的反射原理可以参考:王二:[数学] 光线的反射(reflect)与折射(refract))



平面反射原理

先介绍一种最简单直接的思路,不需要反射矩阵。
当前目标是,使用相机,在看向光滑平面上的 https://www.zhihu.com/equation?tex=P+ 、 两点时,根据光线的反射原理,应该分别对应显示物体上的、 两点。目前的思路是,平面上采样一张贴图,在对应的点显示点的颜色, 点显示点的颜色,并且要保证光照效果正确,那么这张贴图应该长啥样?并且如何采样?
首先,想象出一个物体关于平面对称后产生一个倒立的一模一样的物体,原物体的 、   对应想象物体的、 https://www.zhihu.com/equation?tex=B%E2%80%99 ,根据光线的反射原理推出,相机要在点通过反射看到点,等同于相机的视线穿过平面看到点,我们只要采样到相机此时看到的点时的颜色就可以了。
按上面的方法,我们有了反射平面的贴图了,该怎么采样它才能正确的显示呢?通过上面的推导和图可以知道,点在和的裁剪空间中的位置是刚好颠倒的,所以直接用裁剪空间的坐标经过透视除法,转为屏幕空间坐标,再把 y 轴颠倒过来,用 xy 坐标当作 uv 去采样就可以了。
计算过程可以概括为以下几步:

[*]将相机   的 World Space 坐标转换到平面的 Object Space(简称平面空间)
[*]将相机   的平面空间坐标的 Y 轴取反,获得关于平面对称的反射相机的平面空间坐标,并转换回 World Space
[*]在平面空间获取相机   的 Z 轴朝向和 Y 轴朝向,将它们关于平面对称,获得反射相机在平面空间对应的两个Z、Y轴朝向向量,然后转换到 World Space 然后赋予给反射相机
[*]将反射相机渲染的图片缓存,在平面上使用将 y 轴颠倒的屏幕坐标采样
相关代码:
      private void UpdateReflectionCamera(Camera curCamera) {
            if (targetPlane == null) {
                Debug.LogError("target plane is null!");
            }

            UpdateCamera(curCamera, _reflectionCamera);// 同步当前相机数据

            // 将相机移转换到平面空间 plane space,再通过平面对称创建反射相机
            Vector3 camPosPS = targetPlane.transform.worldToLocalMatrix.MultiplyPoint(curCamera.transform.position);
            Vector3 reflectCamPosPS = Vector3.Scale(camPosPS, new Vector3(1, -1, 1)) + new Vector3(0, m_planeOffset, 0);// 反射相机平面空间
            Vector3 reflectCamPosWS = targetPlane.transform.localToWorldMatrix.MultiplyPoint(reflectCamPosPS);// 将反射相机转换到世界空间
            _reflectionCamera.transform.position = reflectCamPosWS;

            // 设置反射相机方向
            Vector3 camForwardPS = targetPlane.transform.worldToLocalMatrix.MultiplyVector(curCamera.transform.forward);
            Vector3 reflectCamForwardPS = Vector3.Scale(camForwardPS, new Vector3(1, -1, 1));
            Vector3 reflectCamForwardWS = targetPlane.transform.localToWorldMatrix.MultiplyVector(reflectCamForwardPS);
            
            Vector3 camUpPS = targetPlane.transform.worldToLocalMatrix.MultiplyVector(curCamera.transform.up);
            Vector3 reflectCamUpPS = Vector3.Scale(camUpPS, new Vector3(-1, 1, -1));
            Vector3 reflectCamUpWS = targetPlane.transform.localToWorldMatrix.MultiplyVector(reflectCamUpPS);
            _reflectionCamera.transform.rotation = Quaternion.LookRotation(reflectCamForwardWS, reflectCamUpWS);

            // 斜截视锥体
            Vector3 planeNormal = targetPlane.transform.up;
            Vector3 planePos = targetPlane.transform.position + planeNormal * m_planeOffset;
            var clipPlane = CameraSpacePlane(_reflectionCamera, planePos - Vector3.up * 0.1f, planeNormal, 1.0f);
            _reflectionCamera.projectionMatrix = projection;
            _reflectionCamera.cullingMask = m_settings.m_ReflectLayers; // never render water layer
      }


采样 UV

优化方法思路

上面这种方法矩阵计算比较多,有一个同等效果的,计算量更少的方法,需要用到反射矩阵,我找到的网上现有的脚本也都是用的这种方法。
我们知道模型通过 MVP 矩阵从模型空间变到裁剪空间,我们并不需要让移动到镜面对称的位置,而是让同样处于的位置,然后在   的 M 矩阵和 V矩阵之间加入一个反射矩阵,把整个世界倒过来,这样相机看到的就永远是一个关于平面镜像的世界,而且两个相机视锥体完全重合了,采样时也不用颠倒 y 轴坐标了。
关于这个方法我在找资料的过程中有一些发现:
Unity Shader-反射效果(CubeMap,Reflection Probe,Planar Reflection,Screen Space Reflection), 关于反射这篇文章讲的非常详细,但其中应该有个错误:
我们把R矩阵插在V之后,在一个物体在进行MV变换后,变到正常相机坐标系下,然后再进行一次反射变换,就相当于变换到了相对于平面对称的相机坐标系下,然后再进行正常的投影变换,就可以得到反射贴图了。reflectionCamera.worldToCameraMatrix = curCamera.worldToCameraMatrix * reflection;实际上,因为矩阵是左乘,所以代码中反射矩阵并非在 V 矩阵之后,而应该是在 V 矩阵之前,M 矩阵之后使用的,而且反射矩阵中的计算平面和点用的都是世界空间坐标,所以这一步实际上是将整个世界关于平面对称倒过来之后,再变到视空间的。
另外,的 V 矩阵是基于原相机   的 V 矩阵的,所以两个相机的位置相同,应该没有必要再设置的 transform 。
我看到一些代码,包括官方 URP 的示例项目 《Boat Attack》,有这样的操作:
reflectionCamera.transform.position = newpos;   // newpos 是关于目标平面对称或者世界空间 xz 平面对称的位置可按我的理解是没有必要的,因为反射相机的 V 矩阵都已经改了,再调整它的 transform 应该没有啥意义了,实际上我把这段代码注掉后的脚本都是正常运行,暂未发现有任何影响,如果确实是需要这一步的话,有知道原因的大佬希望能告知一下。

反射矩阵推导

上面的方法要在   的 V 矩阵前插入一个反射矩阵,让整个世界在它眼中倒过来。这个矩阵要做的就是,其中   是关于平面对称的点,下面来推导一下这个反射矩阵。



反射矩阵推导

已知为平面的单位法向量,为平面上任意一点,是 https://www.zhihu.com/equation?tex=PP%27 与平面的交点。 求反射矩阵   使平面外一点 https://www.zhihu.com/equation?tex=P%28x%2Cy%2Cz%29,和其关于平面对称点 https://www.zhihu.com/equation?tex=P%27%28x%27%2Cy%27%2Cz%27%29满足
我们的目标是找到反射矩阵,从图中很容易看出和   的关系:

https://www.zhihu.com/equation?tex=P%27%3DP-2%5Cvec%7BN%7D+%5Ccdot+%7CPQ%7C+++%5Ctag%7B1%7D+%5C%5C
因为其中 https://www.zhihu.com/equation?tex=%5Cvec%7BN%7D 已知

https://www.zhihu.com/equation?tex=%5Cbegin%7Balign%7D+%7CPQ%7C%26%3D%5Cvec%7BN%7D+%5Ccdot+%5Cvec%7BP_0P%7D++%5C%5C+%26%3D%28x_n%2Cy_n%2Cz_n%29+%5Ccdot+%28x-x_0%2Cy-y_0%2Cz-z_0%29++%5C%5C+%26%3Dx_nx%2By_ny%2Bz_nz%5C+-%5C+%28x_nx_0%2By_ny_0%2Bz_nz_0%29+%5Cend%7Balign%7D+%5C%5C
为了简化下面带入https://www.zhihu.com/equation?tex=d%3D-%28x_nx_0%2By_ny_0%2Bz_nz_0%29
将上式带入(1)式

https://www.zhihu.com/equation?tex=%28x%27%2Cy%27%2Cz%27%29%3D%28x%2Cy%2Cz%29-2+%28x_nx%2By_ny%2Bz_nz%2Bd%29%5Ccdot+%28x_n%2Cy_n%2Cz_n%29+%5C%5C
为方便观察,先列出每一项对应的计算

https://www.zhihu.com/equation?tex=x%27%3D%281-2x_n%5E2%29x-%282y_nx_n%29y-%282z_nx_n%29z-2x_nd+%5C%5C

https://www.zhihu.com/equation?tex=y%27%3D-%282x_ny_n%29x%2B%281-2y_n%5E2%29y-%282z_ny_n%29z-2y_nd+%5C%5C

https://www.zhihu.com/equation?tex=z%27%3D-%282x_nz_n%29x-%282y_nz_n%29y%2B%281-2z_n%5E2%29z-2z_nd+%5C%5C
经过观察总结出反射矩阵

https://www.zhihu.com/equation?tex=M%3D+%5Cleft%5B%5Cbegin%7Bmatrix%7D+++1-2x_n%5E2+++++%26%26+++++-2y_nx_n+++++%26%26++++-2z_nx_n++++%26%26++++-2x_nd++%5C%5C+++-2x_ny_n++++%26%26++++1-2y_n%5E2+++++%26%26++++-2z_ny_n++++%26%26++++-2y_nd++++%5C%5C++-2x_nz_n++++%26%26++++-2y_nz_n+++++%26%26+++++1-2z_n%5E2+++++%26%26++++-2z_nd%5C%5C+++0++++%26%26++++0++++%26%26++++0++++%26%26++++1%5C%5C++++%5Cend%7Bmatrix%7D+%5Cright%5D+++++%5C%5C+
斜截视锥体(Oblique View Frustum)

虽然完成了镜面反射,但还有一个问题没解决,那就是反射相机当前的视锥体与主相机相同,所以镜像后能看到反射平面背后的东西,就会出现这种情况



反射相机视锥体问题

它的视锥体应该以反射平面为近平面,如下图所示,视锥体 A 部分应该被剔除,只保留 B 部分。



反射相机视锥体

这需要用到斜截视锥体的技术,简单概括,这种技术通过改变 MVP 中的 P 矩阵,实现用指定的平面来当作近平面,但同时会影响到远平面,而其他四个平面不受影响,这正是我们需要的。
以下内容参考论文:《Oblique View Frustum Depth Projection and Clipping》
平面的共变(covariant)

要想将视空间中的平面当作裁剪空间中的近平面,首先需要了解平面是如何变换的。
平面有两种表示形式,一种是四个系数的平面方程:

https://www.zhihu.com/equation?tex=C%28a%2Cb%2Cc%2Cd%29+%EF%BC%9A+ax%2Bby%2Bcz%2Bd%3D0++%5Ctag%7B1%7D+%5C%5C
还可以用平面的法向量,和平面内已知的任意一点来表示:

https://www.zhihu.com/equation?tex=%5Cvec%7BN%7D%5Ccdot+%28P-P_0%29%3D0++%5Ctag%7B2%7D+%5C%5C
将(2)展开可以得到

https://www.zhihu.com/equation?tex=x_nx%2By_ny%2Bz_nz-%28x_nx_0%2By_ny_0%2Bz_nz_0%29%3D0++%5C%5C
结合(1)可以带入对应系数

https://www.zhihu.com/equation?tex=C%28a%2Cb%2Cc%2Cd%29+%5C+%5C+%5C+%5C+%5C+%5C++%5CRightarrow++%5C+%5C+%5C+%5C+%5C+%5C+C%28x_n%2Cy_n%2Cz_n%2C-%5Cvec%7BN%7D%5Ccdot+P_0%29+%5C%5C
平面可以用一个四维向量表示,不难发现,平面和法线类似,是一个共变向量,于是在透视变换中与法线变换一样,需要透视矩阵的逆的转置,具体原理可以参考:王二:[数学] 法线变换

https://www.zhihu.com/equation?tex=P_%7Bclip%7D%3DM_%7Bper%7DP_%7Bview%7D+%5C%5C

https://www.zhihu.com/equation?tex=C_%7Bclip%7D%3D%28M_%7Bper%7D%5E%7B-1%7D%29%5E%7BT%7DC_%7Bview%7D+%5C%5C
另外,任意点与平面向量相乘可以表示为与平面的带符号的垂直距离,知道这一点后反射矩阵的计算更加容易了。

修改透视矩阵

从视空间变换到裁剪空间再经过透视除,就可以将视锥体转换成 xyz 三轴坐标都在 [-1,1] 之间的立方体(基于 OpenGL 风格,DirectX 的 z 轴是 )也就是 NDC,我们希望修改透视矩阵后,NDC 组成的仍然是这样一个立方体。于是可以将近平面在视空间中的坐标,用经过透视除法后的裁剪空间坐标表示

https://www.zhihu.com/equation?tex=C_%7Bview%7D%3D%28%28M_%7Bper%7D%5E%7B-1%7D%29%5ET%29%5E%7B-1%7DC_%7Bclip%7D%3DM_%7Bper%7D%5ETC_%7Bclip%7D+%5C%5C



平面在视空间和裁剪空间的坐标关系

如上图所示,近平面对应了透视矩阵的第三行和第四行相加,而我们想修改透视矩阵来改变近平面的话,只能修改它的第三行,第四行不能改,因为第四行会把视空间中的 z 坐标转到裁剪空间的 w 坐标,这对于顶点属性的透视矫正插值是必要的,相关原因可以参考:王二:[数学] 重心坐标插值与透视校正插值
修改后的透视矩阵的第三行为 https://www.zhihu.com/equation?tex=M_3%27 ,这会同时影响近平面和远平面,于是修改后的视空间中的近平面 https://www.zhihu.com/equation?tex=C 和远平面 https://www.zhihu.com/equation?tex=F 表示为:

https://www.zhihu.com/equation?tex=C_%7Bview%7D+%3D+M_3%27%2BM_4+%5C%5C

https://www.zhihu.com/equation?tex=F_%7Bview%7D+%3D+M_4+-+M_3%27+%3D+2M_4+-+C_%7Bview%7D+%5C%5C
如果我们指定的近平面的法线不垂直于 xy 平面的话,修改后的近远平面就不平行了,那它们在视空间中是什么样的呢?它们会在 xy 平面上相交,只要找 xy 平面上一点 https://www.zhihu.com/equation?tex=P%28x%2Cy%2C0%2Cw%29 就可以发现,因为 https://www.zhihu.com/equation?tex=M_4+%3D+%280%2C0%2C-1%2C0%29没有改动,所以 https://www.zhihu.com/equation?tex=P+%5Ccdot+C_%7Bview%7D+%3D+0 , https://www.zhihu.com/equation?tex=P+%5Ccdot+F_%7Bview%7D+%3D+0 。所以它们现在可能长这样:



修改透视矩阵后的近远平面在 xy 平面相交

这只是一种可能,远平面也可能会截断原视锥体。这也挺反直觉的,都不能围成一个封闭体积了,还能通过透视矩阵变换成立方体吗?还真的可以,毕竟我们是根据透视矩阵反着推的。
不过这种形状很有问题,可能会裁剪掉原视锥体,也会严重影响深度缓冲的精度,这个在论文的最后做了分析,暂时可以理解为类似于原视锥体,深度值就是沿着z轴坐标值,为了让深度缓冲有更高的精度,在 [-1,1] 之间容纳更小的范围,我们尽可能让近远平面之间的距离更小。而现在深度值变得更复杂了,和近远平面的位置有关,沿着不同方向造成的精度损失也不同,为了让深度缓冲精度值更高,我们要让近远平面之间的夹角更小,同时不裁剪原视锥体,如下图所示



优化远平面

近平面隐藏了一个缩放系数,可以调整这个系数而不影响我们的最终目的,但可以调整远平面的角度:

https://www.zhihu.com/equation?tex=F_%7Bview%7D%3D2M_4+-aC_%7Bview%7D+%5C%5C
下面通过求出视空间中的从而要得到合适的   。 (其中 sgn 是符号函数,输出 ±1)

https://www.zhihu.com/equation?tex=Q_%7Bclip%7D+%3D+%28sgn%28C_%7Bclip_x%7D%29%2C+sgn%28C_%7Bclip_y%7D%29%2C1%2C1%29%5C%5C

https://www.zhihu.com/equation?tex=Q_%7Bview%7D%3DM_%7Bper%7D%5E%7B-1%7DQ_%7Bclip%7D++++%5C%5C

https://www.zhihu.com/equation?tex=F_%7Bview%7D+%5Ccdot+Q_%7Bview%7D+%3D+0+%5C%5C

https://www.zhihu.com/equation?tex=a+%3D+%5Cfrac%7B2M_4+%5Ccdot+Q_%7Bview%7D%7D%7BC_%7Bview%7D+%5Ccdot+Q_%7Bview%7D%7D+%5C%5C
最终我们得到修改后的透视矩阵第三行

https://www.zhihu.com/equation?tex=M_3%27+%3D+aC_%7Bview%7D+-+M_4+%5C%5C
标准透视矩阵的应用

下面将我们得到的结果用于标准透视矩阵(OpenGL 风格)试试

https://www.zhihu.com/equation?tex=M_%7Bper%7D%3D++%5Cleft%5B%5Cbegin%7Bmatrix%7D+++%5Cfrac%7B2n%7D%7Br-l%7D%260%26%5Cfrac%7Br%2Bl%7D%7Br-l%7D%260%5C%5C+++0%26%5Cfrac%7B2n%7D%7Bt-b%7D%26%5Cfrac%7Bt%2Bb%7D%7Bt-b%7D%260%5C%5C+++0%260%26+-+%5Cfrac%7Bf%2Bn%7D%7Bf-n%7D%26+-%5Cfrac%7B2fn%7D%7Bf-n%7D%5C%5C+++0%260%26-1%260%5C%5C+++%5Cend%7Bmatrix%7D+%5Cright%5D++%5C%5C

https://www.zhihu.com/equation?tex=M_%7Bper%7D%5E%7B-1%7D%3D++%5Cleft%5B%5Cbegin%7Bmatrix%7D+++%5Cfrac%7Br-l%7D%7B2n%7D%260%260+%26%5Cfrac%7Br%2Bl%7D%7B2n%7D%5C%5C+++0%26%5Cfrac%7Bt-b%7D%7B2n%7D+%26+0+%26+%5Cfrac%7Bt%2Bb%7D%7B2n%7D%5C%5C+++0+%26+0+%26+0+%26+-1%5C%5C++++0+%26+0+%26+-%5Cfrac%7Bf-n%7D%7B2fn%7D+%26+%5Cfrac%7Bf%2Bn%7D%7B2fn%7D%5C%5C+++%5Cend%7Bmatrix%7D+%5Cright%5D++%5C%5C
当前的透视矩阵不会改变平面法线 xy 轴的正负, https://www.zhihu.com/equation?tex=sgn%28C_%7Bview_x%7D%29+%3D+sgn%28C_%7Bclip_x%7D%29 ,所以可免去将平面转到裁剪空间

https://www.zhihu.com/equation?tex=Q_%7Bview%7D%3D++%5Cleft%5B%5Cbegin%7Bmatrix%7D++sgn%28C_%7Bview_x%7D%29%5Cfrac%7Br-l%7D%7B2n%7D+%2B+%5Cfrac%7Br%2Bl%7D%7B2n%7D+++%5C%5C++sgn%28C_%7Bview_y%7D%29%5Cfrac%7Bt-b%7D%7B2n%7D+%2B+%5Cfrac%7Bt%2Bb%7D%7B2n%7D++++%5C%5C++-1%5C%5C++++%5Cfrac%7B1%7D%7Bf%7D%5C%5C+++%5Cend%7Bmatrix%7D+%5Cright%5D++%5C%5C
系数 https://www.zhihu.com/equation?tex=a+ 也可以化简为:

https://www.zhihu.com/equation?tex=a+%3D+%5Cfrac%7B2%7D%7BC_%7Bview%7D+%5Ccdot+Q_%7Bview%7D%7D+%5C%5C
修改后的投影矩阵可以写为

https://www.zhihu.com/equation?tex=M_%7Bper%7D%27%3D++%5Cleft%5B%5Cbegin%7Bmatrix%7D++M1+%5C%5C++M2+++%5C%5C++%5Cfrac%7B2%7D%7BC_%7Bview%7D+%5Ccdot+Q_%7Bview%7D%7D++-+M4%5C%5C+++M4+%5C%5C+++%5Cend%7Bmatrix%7D+%5Cright%5D++%5C%5C
相关代码:
      private Matrix4x4 CalculateObliqueMatrix(Camera cam, Vector4 plane) {
                Vector4 Q_clip = new Vector4(Mathf.Sign(plane.x), Mathf.Sign(plane.y), 1f, 1f);
                Vector4 Q_view = cam.projectionMatrix.inverse.MultiplyPoint(Q_clip);

                Vector4 scaled_plane = plane * 2.0f / Vector4.Dot(plane, Q_view);
                Vector4 M3 = scaled_plane - cam.projectionMatrix.GetRow(3);
               
                Matrix4x4 new_M = cam.projectionMatrix;
                new_M.SetRow(2, M3);

                // 使用 unity API
                // var new_M = cam.CalculateObliqueMatrix(plane);
                return new_M;
      }
平面反射 C# 代码实现 (基于 Unity URP )

完整代码:
using System;
using UnityEngine.Experimental.Rendering;
using UnityEngine.Serialization;
using Unity.Mathematics;


namespace UnityEngine.Rendering.Universal {
   
    public class plannarReflectionTest : MonoBehaviour {
      
      public enum ResolutionMulltiplier { Full, Half, Third, Quarter }

      
      public class PlanarReflectionSettings {
            public ResolutionMulltiplier m_ResolutionMultiplier = ResolutionMulltiplier.Third;
            public float m_ClipPlaneOffset = 0.07f;
            public LayerMask m_ReflectLayers = -1;
            public bool m_Shadows;
      }
      
      
      public PlanarReflectionSettings m_settings = new PlanarReflectionSettings();
      public GameObject targetPlane;
      public float m_planeOffset;

      private static Camera _reflectionCamera;
      private RenderTexture _reflectionTexture;
      private readonly int _planarReflectionTextureId = Shader.PropertyToID("_ReflectTex");

      // public static event Action<ScriptableRenderContext, Camera> BeginPlanarReflections;
      private void OnEnable() {
            
            RenderPipelineManager.beginCameraRendering += runPlannarReflection;// 订阅 beginCameraRendering 事件,加入平面反射函数
      }

      private void OnDisable() {
            Cleanup();
      }

      private void OnDestroy() {
            Cleanup();
      }

      private void Cleanup() {
            RenderPipelineManager.beginCameraRendering -= runPlannarReflection;
            if(_reflectionCamera) {// 释放相机
                _reflectionCamera.targetTexture = null;
                SafeDestroy(_reflectionCamera.gameObject);
            }
            if (_reflectionTexture) {// 释放纹理
                RenderTexture.ReleaseTemporary(_reflectionTexture);
            }
      }
      private static void SafeDestroy(Object obj) {
            if (Application.isEditor) {
                DestroyImmediate(obj);//TODO
            }
            else {
                Destroy(obj);   //TODO
            }
      }
      private void runPlannarReflection(ScriptableRenderContext context, Camera camera) {
            // we dont want to render planar reflections in reflections or previews
            if (camera.cameraType == CameraType.Reflection || camera.cameraType == CameraType.Preview)
                return;

            if (targetPlane == null) {
                targetPlane = gameObject;
            }
            if (_reflectionCamera == null) {
                _reflectionCamera = CreateReflectCamera();
            }

            var data = new PlanarReflectionSettingData(); // save quality settings and lower them for the planar reflections
            data.Set(); // set quality settings

            UpdateReflectionCamera(camera);// 设置相机位置和方向等参数
            CreatePlanarReflectionTexture(camera);// create and assign RenderTexture

            // BeginPlanarReflections?.Invoke(context, _reflectionCamera); // callback Action for PlanarReflection "?."确保event被订阅
            UniversalRenderPipeline.RenderSingleCamera(context, _reflectionCamera); // render planar reflections开始渲染函数

            data.Restore(); // restore the quality settings
            Shader.SetGlobalTexture(_planarReflectionTextureId, _reflectionTexture); // Assign texture to water shader
      }

      private int2 ReflectionResolution(Camera cam, float scale) {
            var x = (int)(cam.pixelWidth * scale * GetScaleValue());
            var y = (int)(cam.pixelHeight * scale * GetScaleValue());
            return new int2(x, y);
      }

      private float GetScaleValue() {
            switch(m_settings.m_ResolutionMultiplier) {
                case ResolutionMulltiplier.Full:
                  return 1f;
                case ResolutionMulltiplier.Half:
                  return 0.5f;
                case ResolutionMulltiplier.Third:
                  return 0.33f;
                case ResolutionMulltiplier.Quarter:
                  return 0.25f;
                default:
                  return 0.5f; // default to half res
            }
      }

      private void CreatePlanarReflectionTexture(Camera cam) {
            if (_reflectionTexture == null) {
                var res = ReflectionResolution(cam, UniversalRenderPipeline.asset.renderScale);// 获取 RT 的大小
                const bool useHdr10 = true;
                const RenderTextureFormat hdrFormat = useHdr10 ? RenderTextureFormat.RGB111110Float : RenderTextureFormat.DefaultHDR;
                _reflectionTexture = RenderTexture.GetTemporary(res.x, res.y, 16,
                  GraphicsFormatUtility.GetGraphicsFormat(hdrFormat, true));
            }
            _reflectionCamera.targetTexture =_reflectionTexture; // 将 RT 赋予相机
      }
      private void UpdateCamera(Camera src, Camera dest) {
            if (dest == null) return;

            // dest.CopyFrom(src);
            dest.aspect = src.aspect;
            dest.cameraType = src.cameraType;   // 这个参数不同步就错
            dest.clearFlags = src.clearFlags;
            dest.fieldOfView = src.fieldOfView;
            dest.depth = src.depth;
            dest.farClipPlane = src.farClipPlane;
            dest.focalLength = src.focalLength;
            dest.useOcclusionCulling = false;
            if (dest.gameObject.TryGetComponent(out UniversalAdditionalCameraData camData)) {// TODO
                camData.renderShadows = m_settings.m_Shadows; // turn off shadows for the reflection camera
            }
      }

      // Calculates reflection matrix around the given plane
      private static Matrix4x4 CalculateReflectionMatrix(Vector4 plane)
      {
            Matrix4x4 reflectionMat = Matrix4x4.identity;
            reflectionMat.m00 = (1F - 2F * plane * plane);
            reflectionMat.m01 = (-2F * plane * plane);
            reflectionMat.m02 = (-2F * plane * plane);
            reflectionMat.m03 = (-2F * plane * plane);

            reflectionMat.m10 = (-2F * plane * plane);
            reflectionMat.m11 = (1F - 2F * plane * plane);
            reflectionMat.m12 = (-2F * plane * plane);
            reflectionMat.m13 = (-2F * plane * plane);

            reflectionMat.m20 = (-2F * plane * plane);
            reflectionMat.m21 = (-2F * plane * plane);
            reflectionMat.m22 = (1F - 2F * plane * plane);
            reflectionMat.m23 = (-2F * plane * plane);

            reflectionMat.m30 = 0F;
            reflectionMat.m31 = 0F;
            reflectionMat.m32 = 0F;
            reflectionMat.m33 = 1F;

            return reflectionMat;
      }
      // Given position/normal of the plane, calculates plane in camera space.
      private Vector4 CameraSpacePlane(Camera cam, Vector3 pos, Vector3 normal, float sideSign) {
            var offsetPos = pos + normal * m_settings.m_ClipPlaneOffset;
            var m = cam.worldToCameraMatrix;
            var cameraPosition = m.MultiplyPoint(offsetPos);
            var cameraNormal = m.MultiplyVector(normal).normalized * sideSign;
            return new Vector4(cameraNormal.x, cameraNormal.y, cameraNormal.z, -Vector3.Dot(cameraPosition, cameraNormal));
      }

      private void UpdateReflectionCamera(Camera curCamera) {
            if (targetPlane == null) {
                Debug.LogError("target plane is null!");
            }

            Vector3 planeNormal = targetPlane.transform.up;
            Vector3 planePos = targetPlane.transform.position + planeNormal * m_planeOffset;

            UpdateCamera(curCamera, _reflectionCamera);// 同步当前相机数据

            // 获取视空间平面,使用反射矩阵,将图像根据平面对称上下颠倒
            var planVS = new Vector4(planeNormal.x, planeNormal.y, planeNormal.z, -Vector3.Dot(planeNormal, planePos));
            Matrix4x4 reflectionMat = CalculateReflectionMatrix(planVS);
            _reflectionCamera.worldToCameraMatrix = curCamera.worldToCameraMatrix * reflectionMat;
            // 斜截视锥体
            var clipPlane = CameraSpacePlane(_reflectionCamera, planePos, planeNormal, 1.0f);
            var newProjectionMat = CalculateObliqueMatrix(curCamera, clipPlane);
            _reflectionCamera.projectionMatrix = newProjectionMat;
            _reflectionCamera.cullingMask = m_settings.m_ReflectLayers; // never render water layer

      }
      
      // private void UpdateReflectionCamera(Camera curCamera) {
      //   // 不使用反射矩阵的方法
      //   if (targetPlane == null) {
      //         Debug.LogError("target plane is null!");
      //   }

      //   UpdateCamera(curCamera, _reflectionCamera);// 同步当前相机数据

      //   // 将相机移转换到平面空间 plane space,再通过平面对称创建反射相机
      //   Vector3 camPosPS = targetPlane.transform.worldToLocalMatrix.MultiplyPoint(curCamera.transform.position);
      //   Vector3 reflectCamPosPS = Vector3.Scale(camPosPS, new Vector3(1, -1, 1)) + new Vector3(0, m_planeOffset, 0);// 反射相机平面空间
      //   Vector3 reflectCamPosWS = targetPlane.transform.localToWorldMatrix.MultiplyPoint(reflectCamPosPS);// 将反射相机转换到世界空间
      //   _reflectionCamera.transform.position = reflectCamPosWS;

      //   // 设置反射相机方向
      //   Vector3 camForwardPS = targetPlane.transform.worldToLocalMatrix.MultiplyVector(curCamera.transform.forward);
      //   Vector3 reflectCamForwardPS = Vector3.Scale(camForwardPS, new Vector3(1, -1, 1));
      //   Vector3 reflectCamForwardWS = targetPlane.transform.localToWorldMatrix.MultiplyVector(reflectCamForwardPS);
            
      //   Vector3 camUpPS = targetPlane.transform.worldToLocalMatrix.MultiplyVector(curCamera.transform.up);
      //   Vector3 reflectCamUpPS = Vector3.Scale(camUpPS, new Vector3(-1, 1, -1));
      //   Vector3 reflectCamUpWS = targetPlane.transform.localToWorldMatrix.MultiplyVector(reflectCamUpPS);
      //   _reflectionCamera.transform.rotation = Quaternion.LookRotation(reflectCamForwardWS, reflectCamUpWS);

      //   // 斜截视锥体
      //   Vector3 planeNormal = targetPlane.transform.up;
      //   Vector3 planePos = targetPlane.transform.position + planeNormal * m_planeOffset;
      //   var clipPlane = CameraSpacePlane(_reflectionCamera, planePos - Vector3.up * 0.1f, planeNormal, 1.0f);
      //   var newProjectionMat = CalculateObliqueMatrix(curCamera, clipPlane);
      //   _reflectionCamera.projectionMatrix = newProjectionMat;
      //   _reflectionCamera.cullingMask = m_settings.m_ReflectLayers; // never render water layer
      // }
      private Matrix4x4 CalculateObliqueMatrix(Camera cam, Vector4 plane) {
                Vector4 Q_clip = new Vector4(Mathf.Sign(plane.x), Mathf.Sign(plane.y), 1f, 1f);
                Vector4 Q_view = cam.projectionMatrix.inverse.MultiplyPoint(Q_clip);

                Vector4 scaled_plane = plane * 2.0f / Vector4.Dot(plane, Q_view);
                Vector4 M3 = scaled_plane - cam.projectionMatrix.GetRow(3);
               
                Matrix4x4 new_M = cam.projectionMatrix;
                new_M.SetRow(2, M3);

                // 使用 unity API
                // var new_M = cam.CalculateObliqueMatrix(plane);
                return new_M;
      }

      private Camera CreateReflectCamera() {
            var go = new GameObject(gameObject.name + " Planar Reflection Camera",typeof(Camera));
            var cameraData = go.AddComponent(typeof(UniversalAdditionalCameraData)) as UniversalAdditionalCameraData;

            cameraData.requiresColorOption = CameraOverrideOption.Off;
            cameraData.requiresDepthOption = CameraOverrideOption.Off;
            cameraData.renderShadows = false;
            cameraData.SetRenderer(1);// 根据 render list 的索引选择 render TODO

            var t = transform;
            var reflectionCamera = go.GetComponent<Camera>();
            reflectionCamera.transform.SetPositionAndRotation(transform.position, t.rotation);// 相机初始位置设为当前 gameobject 位置
            reflectionCamera.depth = -10;// 渲染优先级 [-100, 100]
            reflectionCamera.enabled = false;
            go.hideFlags = HideFlags.HideAndDontSave;

            return reflectionCamera;
      }
      
      class PlanarReflectionSettingData {
            private readonly bool _fog;
            private readonly int _maxLod;
            private readonly float _lodBias;
            private bool _invertCulling;

            public PlanarReflectionSettingData() {
                _fog = RenderSettings.fog;
                _maxLod = QualitySettings.maximumLODLevel;
                _lodBias = QualitySettings.lodBias;
            }

            public void Set() {
                _invertCulling = GL.invertCulling;
                GL.invertCulling = !_invertCulling;// 因为镜像后绕序会反,将剔除反向
                RenderSettings.fog = false; // disable fog for now as it's incorrect with projection
                QualitySettings.maximumLODLevel = 1;
                QualitySettings.lodBias = _lodBias * 0.5f;
            }

            public void Restore() {
                GL.invertCulling = _invertCulling;
                RenderSettings.fog = _fog;
                QualitySettings.maximumLODLevel = _maxLod;
                QualitySettings.lodBias = _lodBias;
            }
      }
    }
}


URP 中没有 OnWillRenderObject 函数了,使用需要使用 C# 的 event 机制,订阅 beginCameraRendering 函数,脚本中的主函数是 runPlannarReflection ,保持与订阅事件相同函数签名。
UpdateReflectionCamera 是更新反射相机的函数,有两个实现,一种是不使用反射矩阵的,一种是使用的,两种方法切换时要切换 GL.invertCulling,因为使用反射矩阵会让相机的绕序反向。

参考

Unity Shader-反射效果(CubeMap,Reflection Probe,Planar Reflection,Screen Space Reflection)
破晓:Unity平面反射实现
http://www.terathon.com/lengyel/Lengyel-Oblique.pdf
Achieve beautiful, scalable, and performant graphics with the Universal Render Pipeline | Unity Blog
https://github.com/Unity-Technologies/BoatAttack
页: [1]
查看完整版本: Unity 实现平面反射(基于 URP)