平面反射

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了平面反射相关的知识,希望对你有一定的参考价值。

参考技术A 1、抓取屏幕图像——渲染纹理RenderTexture+额外摄像机
2、反射矩阵(ReflectMatrix)——获得反射的屏幕图像
3、斜裁剪矩阵(ObliqueMatrix)——裁剪掉反射平面以下的部分
4、高斯模糊——模糊反射的倒影
5、将反射纹理渲染到地面

之前的文章已经提过了,抓取屏幕图像有两种方法:
1、GrabPass
2、渲染纹理RenderTexture+额外摄像机
这里需要使用第二种方法:

Unity已经为我们提供了获取斜裁减矩阵的方法CalculateObliqueMatrix,我们只需要注意这个方法的参数是视图空间的平面。

斜裁剪矩阵的推导过程 https://zhuanlan.zhihu.com/p/92633614

高斯模糊我前面的文章已经介绍过了,这里不详细写了。

最后一步,我们需要将获取到的反射纹理渲染到地面。
我们只需要在渲染地面的时候,叠加反射纹理即可。

Unity渲染菜B之 普通版平面反射

1.先看效果

2.简介

其实原理是设置一个和MainCamera关于反射平面的XoZ面镜像的摄像机,拍摄反射内容作为纹理传递给反射平面的shader进行绘制。Unity的老示例项目Angry Bot和公司的实现都是这种方法,算是一种比较古早的方案了。适合前向渲染(SSR要走延迟管线)。

这里做起来只考虑反射面是一个完全的平面,没有任何凹凸的地方。

另外本人只是渲染菜鸡,以下内容若有概念错误或者说不明白的地方还请轻喷= =

3.实现原理

注意:贴出来的代码用的变量名和图片讲解里的不一致,但原理是基本一样的。

3.1 设置反射摄像机的位置和朝向

反射摄像机的位置和朝向都要和主摄像机关于平面(的XoZ面)镜像。

3.1.1 先求反射摄像机位置

  1. 先求出主摄像机到反射平面transform中心的向量 V1,通过两者的position相减得到。

  2. 求V1在平面的y轴方向上的投影Vtmp ,它垂直于平面向下,Vtmp的模(就是长度)是主摄像机到反射平面的距离。

  3. 主摄像机的position + 2*Vtmp = 反射摄像机的position。

		Vector3 v1 = plane.position - MainCam.transform.position;
        v1 = Vector3.Project(v1, plane.up);
        ......
        ReflCam.transform.position = MainCam.transform.position + 2f * v1;

3.1.2 再求反射摄像机的朝向

  1. 以主摄像机的forward方向为V2,这是个单位向量。
  2. V2在反射平面的y轴上的投影向量VpVp方向和反射平面的y轴正方向相反。
  3. V2’ = V2 - 2 * VpV2’ 就是关于反射平面和V2镜像的单位向量。
  4. 令反射摄像机的forward = V2’,完事。
		Vector3 mainCamForward = MainCam.transform.forward;
        Vector3 v2 = - Vector3.Project(mainCamForward, plane.up);
		......
        ReflCam.transform.forward = mainCamForward + 2 * v2;//求反射相机的朝向

3.1.3 检查效果

随意旋转下反射平面,康康反射摄像机的位置、朝向能否都和主摄像机关于平面镜像,康康拍摄到的反射画面正不正常。

3.2 反射摄像机渲染到RT

3.2.1 调整反射摄像机设置

调整下反射摄像机的各项参数(和主摄像机的参数保持一致),防止后面出现各种各样奇怪的问题。下面是我自己的设置,各位可以根据自己的实际情况做加减。

//我把反射摄像机disable了,不影响,可用ReflCam.Render()强制调用一次渲染
ReflCam.enabled = false;
//透视or正交和主摄像机一致
if (MainCam.orthographic != ReflCam.orthographic) 
		ReflCam.orthographic = MainCam.orthographic;
//远近裁剪平面、fov一致
if (MainCam.nearClipPlane != ReflCam.nearClipPlane) 
        ReflCam.nearClipPlane = MainCam.nearClipPlane;
if (MainCam.farClipPlane != ReflCam.farClipPlane) 
		ReflCam.farClipPlane = MainCam.farClipPlane;
if (MainCam.fieldOfView != ReflCam.fieldOfView) 
		ReflCam.fieldOfView = MainCam.fieldOfView;
//反射摄像机的clearFlag也是skybox(天空的内容也要反射的嘛)
if (ReflCam.clearFlags != CameraClearFlags.Skybox) 
		ReflCam.clearFlags = CameraClearFlags.Skybox;
//可以单独给要反射的物体加个layer,让反射摄像机只渲染这个layer的物体
ReflCam.cullingMask = m_ReflectedObjsMask;

3.2.2 调整RT设置,反射摄像机渲染到RT

/*
这里的width和height应该分别对应窗口分辨率的宽和高。如果不希望搞太大的反射图像,可以让width和height都乘上一个相同的倍率
*/
RenderTexture rt = new RenderTexture(width, height, 0, RenderTextureFormat.ARGB32);
rt.name = plane.name + " ReflTex";//命名随意
/*
要不要开mipmap视实际项目需求来定。有时候反射平面可能是粗糙的,不太可能反射出精细的图像。这时候结合自定的mipmap level就能采样出较粗糙的反射图像。
*/
rt.useMipMap = true;
/*
wrapMode可随意,如果反射物超出反射摄像机的视觉范围,可能会出现对应的异常图像。
*/
rt.wrapMode = TextureWrapMode.Clamp;
//反射摄像机内容渲染到rt
targetRT = rt;
......
ReflCam.targetTexture = targetRT;
ReflCam.Render();//调用渲染

3.3 把反射纹理绘制到平面上

3.3.1 怎么绘制?

先看这张效果图,想象一下如果主摄像机保持不变,我们转到其他位置看,会是什么效果?

比如转换到左视图看,可以看到反射物的图像被绘制在它自己脚下往主摄像机方向的那个范围里,在主摄像机能看到正确的结果。观察反射摄像机采样的光路图,其实在像素着色器中把平面上的点转换到反射摄像机的屏幕空间里,得到对应的uv,用这个uv对反射纹理进行采样,不就可以了?

3.3.2 传递反射摄像机的V、P变换矩阵和反射纹理给shader

反射平面上各顶点的M变换(模型空间转世界空间)会在shader内完成,我们只需把完成摄像机空间内视图和投影变换的矩阵P*V传到shader里即可。

	internal struct Uniforms
    
        public static int ReflTex = Shader.PropertyToID("_ReflTex");
        public static int ReflSpaceMatrixVP = Shader.PropertyToID("_ReflSpaceMatrixVP");
		......
    
    
    //用访问器提供P*V矩阵
    private Matrix4x4 ReflSpaceMatrixVP
    
        get
        
            if (ReflCam == null)
            
                Debug.LogError("ReflCam is null!");
                return Matrix4x4.zero;
            
            Matrix4x4 V = ReflCam.worldToCameraMatrix;
            Matrix4x4 P = GL.GetGPUProjectionMatrix(ReflCam.projectionMatrix, false);
            return P * V;
        
    
        //这里我用了MaterialPropertyBlock,如果只是简单测试,用Renderer的material也行了
        ......
        renderer.GetPropertyBlock(mpb);
        mpb.SetTexture(Uniforms.ReflTex, rt);//传递反射纹理
        mpb.SetMatrix(Uniforms.ReflSpaceMatrixVP, ReflSpaceMatrixVP);//传递P*V矩阵
        renderer.SetPropertyBlock(mpb);
        ......

3.3.3 编写shader的反射采样部分

把整个shader贴上来好了。其中的重点部分是WorldToReflectionPos函数。

Shader "Unlit/ReflectionPlane"

    Properties
    
        _MipmapLevel("Mipmap Level", Range(0,7)) = 0
        _ReflTex("Refl Texture",2D) = "black"
    

    CGINCLUDE

        #include "UnityCG.cginc"
        int _MipmapLevel;

        sampler2D _ReflTex;
        float4x4 _ReflSpaceMatrixVP;

        struct appdata
            
                float4 vertex : POSITION;
                fixed2 uv : TEXCOORD0;
            ;

        struct v2f
            
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
                float4 worldPos : TEXCOORD1;
            ;

        fixed4 WorldToReflectionPos(float4 worldPos, inout fixed2 uv_ReflSpace)
        
            float4 refl_space_pos = mul(_ReflSpaceMatrixVP, worldPos);//转换到反射摄像机的投影空间
            uv_ReflSpace = refl_space_pos.xy/refl_space_pos.w;//先进行透视除法把xy映射到[-1,1]区间
            uv_ReflSpace = uv_ReflSpace * 0.5 + 0.5;//再把xy映射到[0,1]区间,可得反射用的uv
            return refl_space_pos;
        

        v2f vertRefl (appdata v)
        
            v2f o;
            o.vertex = UnityObjectToClipPos(v.vertex);
            o.worldPos = mul(unity_ObjectToWorld, v.vertex);
            o.uv = v.uv;
            return o;
        

        fixed4 fragRefl (v2f i) : SV_Target
        
            fixed2 uv_refl;
            fixed4 reflPos = WorldToReflectionPos(i.worldPos, uv_refl);
            //如果只是简单采样可用tex2D
            fixed3 reflect_col = tex2Dlod(_ReflTex,fixed4(uv_refl, 0 ,_MipmapLevel));

            return fixed4(reflect_col,1);
        


    ENDCG

    SubShader
    
        Pass
        
            Tags  "RenderType"="Opaque" "RenderQueue"="Geometry" "LightMode"="ForwardBase"
            LOD 100
            CGPROGRAM
            #pragma vertex vertRefl
            #pragma fragment fragRefl
            ENDCG
        
    

至此一个基础的平面反射就完成了,转动一下平面看结果是否正确。一般项目用上这个就差不多可以了,后续会考虑发下自己做过的一些小改动。(其实都是些没什么卵用的改动)

以上是关于平面反射的主要内容,如果未能解决你的问题,请参考以下文章

反射矩阵(reflection matrix)推导

Cook-Torrance光照模型

如何用平面几何证明从抛物线的焦点发出的光线经抛物线反射会成为平行线

UnityShader镜面反射计算与反射光向量推导

智能反射表面(IRS)代码-两跳

反射向量 及 向量投影