基于反射相机的平面反射实现

整体思路

此方法并不依赖shader,而是使用MonoBehaviour脚本(挂载到反射平面或者相机上)

核心思路:

 

简单粗暴,利用脚本创建一个新的相机,这个相机与主相机参数位置一致,将此相机的图像翻转即可,大体步骤为

  1. 创建一个相机
  2. 对渲染内容进行镜像
  1. 输出RT
  2. 传进Shader中采样处理

创建反射相机

创建一个GameObject并设置名称和类型,再获取到主相机的组件并复制参数

private Camera reflectionCam;  

void Start()
{
   GameObject cam = new GameObject("ReflectionCamera", new System.Type[] { typeof(Camera) });
   reflectionCam = cam.GetComponent<Camera>();
   reflectionCam.CopyFrom(Camera.main);
}

翻转图像的时机

想最终得到翻转的图像,就需要在最后一步的渲染之前,将图像翻转

所以要考虑这一步的计算的时机,既不是Start也不是Update,而使用OnpreRender()

此方法会在相机渲染之前调用,恰好是我们需要的

OnBeginCameraRendering()方法的使用(URP)

(BuildIn内置管线使用void OnPreRender())

需要声明UnityEngine.Rendering类才能使用

输入完整的带参数的方法块,渲染前的翻转等操作将会在此方法内进行

在Start方法中,使用RenderPipelineManager.beginCameraRendering事件调出该方法

using UnityEngine.Rendering; 
void Start()
    {
        GameObject cam = new GameObject("ReflectionCamera", new System.Type[] { typeof(Camera) });
        reflectionCam = cam.GetComponent<Camera>();
        reflectionCam.CopyFrom(Camera.main);

        RenderPipelineManager.beginCameraRendering += OnBeginCameraRendering;

    }

    void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera)
    {
        
    }

反射矩阵变换(简易版)

顶点从本地空间变换到裁剪空间有如下过程

我们选择在世界空间——相机空间这一过程,进行反射变换,具体做法是将反射空间与相机空间这两个变换矩阵相乘,结果与先乘一个矩阵再乘一个矩阵是一致的

反射矩阵

如果只翻转图像,只需将Sy取负值即可(即-1)

初步代码实现

方法一

这里只展示 OnBeginCameraRendering方法块中内容

[3]判断相机是否为反射相机,我们只翻转反射相机的图像

[5]此翻转矩阵会导致剔除也会被翻转,所以这里需要再把剔除翻转回来

[7]创建一个4x4的单位矩阵,这里特意不使用new的方式,是避免每帧都会生成实例造成冗余

[8]设置1行1列(从0开始)即Sy的值为-1

[9]我们直接对世界空间到相机空间的变换矩阵直接赋值,使用反射矩阵乘以主相机的变换矩阵,注意这里的相乘顺序,确保顶点最后一步是转换到相机空间

  void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera)
    {
        if (camera == reflectionCam)
        {
            GL.invertCulling = true;
            方法一:直接更改矩阵中的第二行第二列(这里是从0开始)即Y轴的参数
             Matrix4x4 reflectionM = Matrix4x4.identity;
             reflectionM.m11 = -1;
             camera.worldToCameraMatrix = reflectionM * Camera.main.worldToCameraMatrix;

        }
        else
        {
            GL.invertCulling = false;
        }

    }

方法二

我们使用三个三维向量直接将参数暴露出来

它们分别是平移T 旋转R 缩放S

为何是三个三维?

很巧,Unity为我们提供了设置TRS的方法(Matrix4x4.TRS()),并且变换也只在这三个维度中

[12]此处的R需要转化为欧拉角来使用

  
    public Vector3 T = Vector3.zero;
    public Vector3 R = Vector3.zero;
    public Vector3 S = Vector3.one;

void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera)
    {
        if (camera == reflectionCam)
        {
            GL.invertCulling = true;
            // 方法二:暴露参数
            camera.worldToCameraMatrix = Matrix4x4.TRS(T, Quaternion.Euler(R), S)* Camera.main.worldToCameraMatrix;

        }
        else
        {
            GL.invertCulling = false;
        }

    }

使用CullingMask剔除其余物体

在我们完成以上的操作后,反射相机反射的是整个画面,但是我们只需要被反射的物体

为了解决这个问题,我们需要用到相机中Rendering一栏下的CullingMask功能

实现方式

CullingMask是通过识别层来实现剔除的,我们将需要渲染反射的物体放入一个单独的层中,指定反射相机的CullingMask只渲染该层即可

代码实现

将此段代码放入反摄像机判断语句中即可

[5]字符串"Reflection"是我们自定义的层名称,是需要反射的物体所设置的层

至此反射相机就只会渲染指定物体了

但是还有一个问题

天空球背景仍然被显示了出来,导致我们仍然不能看到主相机的图像

为了获得带透明通道的反射图片,下面我们有请RenderTexture

void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera)
    {
        if (camera == reflectionCam)
        {
            camera.cullingMask = LayerMask.GetMask("Reflection");
          
        }

RenderTexture

此功能对应相机属性面板中Output——OutTexture

可以指定输出到RenderTexture中

RenderTexture是什么

在U3D中有一种特殊的Texture类型,叫做RenderTexture,它本质上一句话是将一个FrameBufferObjecrt连接到一个server-side的Texture对象。

什么是server-sider的texture?

在渲染过程中,贴图最开始是存在cpu这边的内存中的,这个贴图我们通常称为client-side的texture,它最终要被送到gpu的存储里,gpu才能使用它进行渲染,送到gpu里的那一份被称为server-side的texture。这个tex在cpu和gpu之间拷贝要考虑到一定的带宽瓶颈。

这有什么用?

我们可以将场景的一些渲染结果渲染到一个tex上,这个tex可以被继续使用。例如,汽车的后视镜,后视镜就可以贴一个rendertex,它是从这个视角的摄像机渲染而来。


一旦RenderTex被调用并连接到相机上,则该相机输出的内容不再呈现在显示器当中

代码实现

到这里这里我们优化了一些代码,例如CopyFrom复制主相机参数的步骤放入了渲染前的方法中,cullingMask也是如此,这是因为如果运行中主相机参数有改变或者移动位置,反射相机也需要实时改变

[11]声明一个RT图,并设为公共,以便后续shader能够调用它

[17]Unity会将用此方法创建的RT图放入缓存池中,如果下次需要创建尺寸信息相同的RT图,Unity会直接使用缓存中的RT图,性能更优

[17]第三个重载的值为深度信息,分别有0(无深度信息)16(有深度信息)32(深度和Stencil模板测试)


public class PlaneReflection : MonoBehaviour
{


    public Vector3 T = Vector3.zero;
    public Vector3 R = Vector3.zero;
    public Vector3 S = Vector3.one;

    private Camera reflectionCam;
    public RenderTexture reflectionRT;
    void Start()
    {
        GameObject cam = new GameObject("ReflectionCamera", new System.Type[] { typeof(Camera) });
        reflectionCam = cam.GetComponent<Camera>();

        reflectionRT = RenderTexture.GetTemporary(Screen.width, Screen.height, 0);
        reflectionRT.name = "ReflectionRT";
        RenderPipelineManager.beginCameraRendering += OnBeginCameraRendering;

    }

    void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera)
    {
        if (camera == reflectionCam)
        {
            camera.CopyFrom(Camera.main);
            camera.clearFlags = CameraClearFlags.SolidColor;
            camera.backgroundColor = Color.clear;
            camera.targetTexture = reflectionRT;
            camera.cullingMask = LayerMask.GetMask("Reflection");
            GL.invertCulling = true;

            // 方法二:暴露参数
            camera.worldToCameraMatrix = Matrix4x4.TRS(T, Quaternion.Euler(R), S) * Camera.main.worldToCameraMatrix;


        }
        else
        {
            GL.invertCulling = false;
        }

    }


}

 

万事俱备只欠Shader

获取RT图

首先我们需要解决的问题——如何使Shader获取到脚本中的RT图?

很简单,只需要这样一行语句在脚本的Start方法中

[4]方法内的字符串是我们自定义的属性名称, 很显然此方法是在RT图中的需要在RT图的类中调用

void Start()
    {
      
        reflectionRT.SetGlobalShaderProperty("_ReflectionRT");

    }

此时,Shader就可以直接获取并采样RT图了

无需在Properties中声明,直接声明采样器并采样输出即可

注意我们需要使用屏幕坐标作为UV

Shader "Unlit/SimpleUnlit"
{
    Properties
    {

    }
    SubShader
    {
        Tags
        {
            "RenderPipeline"="UniversalPipeline"
            "RenderType"="Transparent"
            "UniversalMaterialType" = "Unlit"
            "Queue"="Transparent"
        }
        Pass
        {
            Blend SrcAlpha OneMinusSrcAlpha

            HLSLPROGRAM

            #pragma target 4.5
            #pragma exclude_renderers gles gles3 glcore
            #pragma multi_compile_instancing
            #pragma multi_compile_fog
            #pragma multi_compile _ DOTS_INSTANCING_ON
            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl"
            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Texture.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/TextureStack.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/ShaderGraphFunctions.hlsl"

            
            CBUFFER_START(UnityPerMaterial)

            CBUFFER_END
            
            TEXTURE2D(_ReflectionRT);  
            #define smp SamplerState_Linear_Repeat
            SAMPLER(smp);

            
            struct Attributes
            {
                float3 positionOS : POSITION;
                float2 uv : TEXCOORD;
            };
            
            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float2 uv : TEXCOORD;
            };
            
            Varyings vert(Attributes v)
            {
                Varyings o = (Varyings)0;
                o.positionCS = TransformObjectToHClip(v.positionOS);
                o.uv = v.uv;
                return o;
            }

            half4 frag(Varyings i) : SV_TARGET 
            {    
                float2 screenUV = i.positionCS.xy/_ScaledScreenParams;
                half4 mainTex = SAMPLE_TEXTURE2D(_ReflectionRT,smp,screenUV);
                return mainTex;
            }

            ENDHLSL
        }
        
    }
    
}

反射矩阵变换(高级版)

基本原理与公式推导

考虑到单纯的用翻转的图像仍会带来许多问题,我们换一种思路来做

假设这是反射平面的侧视图,N为法线方向O为平面原点,P为被反射的物体顶点,P'为反射后的顶点位置,那么为了求出P'的位置,我们有

P' = P+(\vec{-N})*2|PQ|

向量N是方向,PQ则是长度

且只有|PQ|是未知的,下面我们就要求得|PQ|的具体值

首先连接PO得到一个 \theta

我们知道:

cos(\theta) = \frac{|PQ|}{|OP|}

|PQ| = cos(\theta)*|OP|

上面的暂且先放着,我们点积一下OP和N

dot(\vec{OP},\vec{N}) = |\vec{OP}|*|\vec{N}|*cos(\theta)

提取cos(\theta)

cos(\theta) = \frac{dot(\vec{OP},\vec{N})}{|\vec{OP}|*|\vec{N}|}

嘿,我们有cos(\theta)了!带入上面的|PQ|试试!

|PQ| = \frac{dot(\vec{OP},\vec{N})}{|\vec{OP}|*|\vec{N}|}*|OP|

 

我们约分掉OP,因为他们两个都是长度,并且假定\vec{N}是单位向量,即模长为1

所以可直接得

|PQ| = dot(\vec{OP},\vec{N})

其中\vec{OP}可由P - O得到,我们也带入进去

|PQ| = dot(P - O,\vec{N})

等价于

|PQ| = dot(P,\vec{N})-dot(O,\vec{N})

这里我们再看,剩下的N是我们的法线向量,O是反射平面的原点,P则是顶点是个常量

都是可以得到或者已知的值了

下面我们就可以带入到一开始的式子里了,就是这个

P' = P-2|PQ|*\vec{N}

代入后

P' = P-2(dot(P,\vec{N}))-dot(O,\vec{N})*\vec{N}

好了,我们现在要带入每个值的具体坐标了,这里看起来要麻烦一点,但并不复杂

另外别忘了我们的最终目的是得到一个反射矩阵

还不够,再拆

P'的x轴

y轴

z轴

最后,用矩阵写是这样子的

上面的结果其实就是矩阵的行与列的对应元素相乘再相加

这就是我们最终的反射矩阵了

这里的Px,Py,Pz其实就是模型的顶点坐标

代码实现

[14]声明一个4x4矩阵,对照公式逐一填入

[34]注意这里将反射矩阵放在后面,效果才会正确

 void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera)
    {
        if (camera == reflectionCam)
        {
            camera.CopyFrom(Camera.main);
            camera.clearFlags = CameraClearFlags.SolidColor;
            camera.backgroundColor = Color.clear;
            camera.targetTexture = reflectionRT;
            camera.cullingMask = LayerMask.GetMask("Reflection");
            GL.invertCulling = true;


            // 方法三(高级版)
            Matrix4x4 reflectionM = Matrix4x4.identity;
            Vector3 N = new Vector3(0, 1, 0);
            float dotON = Vector3.Dot(transform.position, N);
            reflectionM.m00 = 1 - 2 * N.x * N.x;
            reflectionM.m01 = -2 * N.x * N.y;
            reflectionM.m02 = -2 * N.x * N.z;
            reflectionM.m03 = 2 * dotON * N.x;
            reflectionM.m10 = -2 * N.x * N.y;
            reflectionM.m11 = 1 - 2 * N.y * N.y;
            reflectionM.m12 = -2 * N.y * N.z;
            reflectionM.m13 = 2 * dotON * N.y;
            reflectionM.m20 = -2 * N.x * N.z;
            reflectionM.m21 = -2 * N.y * N.z;
            reflectionM.m22 = 1 - 2 * N.z * N.z;
            reflectionM.m23 = 2 * dotON * N.z;
            reflectionM.m30 = 0;
            reflectionM.m31 = 0;
            reflectionM.m32 = 0;
            reflectionM.m33 = 1;

            camera.worldToCameraMatrix = Camera.main.worldToCameraMatrix * reflectionM;


        }
        else
        {
            GL.invertCulling = false;
        }

    }

最终效果

对RT图做了一些扭曲

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值