Unity Shader 实现一个简单的屏幕空间反射(2)

这是屏幕空间反射的第二篇,严格来说是平面反射(Plane Reflection),具体思路是设置一个需要反射的平面,然后生成一个和主摄像机基于反射平面对称的相机,用对称相机将场景渲染一遍得到RendderTexture,反射平面用屏幕坐标采样该RendderTexture即可。这种做法的优点是简单易实现,缺点是DrawCall会翻一倍,最好是用在比较简单的场景,并且该平面不可倾斜。
老规矩先上图:
在这里插入图片描述
其主要难点在于如何寻找对称的反射相机,这里我用的是Planar reflection with Unity中所用到的方法,B站上有搬运。
其主要算法如下:

		m_ReflectionCamere.CopyFrom(m_MainCamera);

        Vector3 cameraDirectionWorldSpace = m_MainCamera.transform.forward;
        Vector3 cameraUpWorldSpace = m_MainCamera.transform.up;
        Vector3 cameraPositionWorldSpace = m_MainCamera.transform.position;

        Vector3 cameraDirectionPlaneSpace = m_ReflectionPlane.transform.InverseTransformDirection(cameraDirectionWorldSpace);
        Vector3 cameraUpPlaneSpace = m_ReflectionPlane.transform.InverseTransformDirection(cameraUpWorldSpace);
        Vector3 cameraPositonPlaneSpace = m_ReflectionPlane.transform.InverseTransformPoint(cameraPositionWorldSpace);

        cameraDirectionPlaneSpace.y *= -1.0f;
        cameraUpPlaneSpace.y *= -1.0f;
        cameraPositonPlaneSpace.y *= -1.0f;

        cameraDirectionWorldSpace = m_ReflectionPlane.transform.TransformDirection(cameraDirectionPlaneSpace);
        cameraUpWorldSpace = m_ReflectionPlane.transform.TransformDirection(cameraUpPlaneSpace);
        cameraPositionWorldSpace = m_ReflectionPlane.transform.TransformPoint(cameraPositonPlaneSpace);

        m_ReflectionCamere.transform.position = cameraPositionWorldSpace;
        m_ReflectionCamere.transform.LookAt(cameraPositionWorldSpace+cameraDirectionWorldSpace,cameraUpWorldSpace);

这里只针对了反射平面为水平的时候(即摄像机基于XZ平面对称),所以可将问题简化成二维的,如下图所示:
在这里插入图片描述
我们需要求出反射摄像机世界空间的Forward、Up向量以及位置Position,代码首先将主摄像机的世界空间Forward、Up向量以及位置Position转换到反射平面(Reflection plane)的模型空间中,在此坐标系下主摄像机与反射摄像机基于XZ平面对称,此时将主摄像机的各项变量Y轴转换为负值便可直接得到反射摄像机的各项值。

		Vector3 cameraDirectionPlaneSpace = m_ReflectionPlane.transform.InverseTransformDirection(cameraDirectionWorldSpace);
        Vector3 cameraUpPlaneSpace = m_ReflectionPlane.transform.InverseTransformDirection(cameraUpWorldSpace);
        Vector3 cameraPositonPlaneSpace = m_ReflectionPlane.transform.InverseTransformPoint(cameraPositionWorldSpace);

        cameraDirectionPlaneSpace.y *= -1.0f;
        cameraUpPlaneSpace.y *= -1.0f;
        cameraPositonPlaneSpace.y *= -1.0f;

之后再将反射摄像机模型空间的Forward、Up向量以及位置Position转换到世界空间。

		cameraDirectionWorldSpace = m_ReflectionPlane.transform.TransformDirection(cameraDirectionPlaneSpace);
        cameraUpWorldSpace = m_ReflectionPlane.transform.TransformDirection(cameraUpPlaneSpace);
        cameraPositionWorldSpace = m_ReflectionPlane.transform.TransformPoint(cameraPositonPlaneSpace);

最后再去设置反射摄像机的位置及朝向便可:

m_ReflectionCamere.transform.position = cameraPositionWorldSpace;
        m_ReflectionCamere.transform.LookAt(cameraPositionWorldSpace+cameraDirectionWorldSpace,cameraUpWorldSpace);

这里的LookAt() 需要的参数为:

public void LookAt (Vector3 worldPosition, Vector3 worldUp= Vector3.up);

所以需要用摄像机位置Position加上Forward。
以下是全部的代码:

using UnityEngine;

public class PlanerReflectionManager : MonoBehaviour
{
    Camera m_ReflectionCamere;
    Camera m_MainCamera;

    public GameObject m_ReflectionPlane;
    [Range(0.0f, 1.0f)]
    public float _Alpha = 0.5f;
    Material ssrMaterial;

    RenderTexture m_RenderTarget;

    void Start()
    {
        GameObject reflectionCameraGo = new GameObject("ReflectionCamera");
        m_ReflectionCamere = reflectionCameraGo.AddComponent<Camera>();
        m_ReflectionCamere.enabled = false;

        m_MainCamera = GetComponent<Camera>();
        m_RenderTarget = new RenderTexture(Screen.width , Screen.height , 24);
        //m_DepthTexture = new RenderTexture(Screen.width, Screen.height, 0);
        if(m_ReflectionPlane!=null)
        {
            ssrMaterial = m_ReflectionPlane.GetComponent<MeshRenderer>().sharedMaterial;
        }
    }


    //private void OnPostRender()
    //{
    //    if (m_ReflectionPlane == null) return;
    //    RenderReflection();
    //    //Blur();
    //    //DrawQuad();
    //}

    private void LateUpdate()
    {
        if (m_ReflectionPlane == null) return;
        RenderReflection();
    }

    void RenderReflection()
    {
        m_ReflectionCamere.CopyFrom(m_MainCamera);

        Vector3 cameraDirectionWorldSpace = m_MainCamera.transform.forward;
        Vector3 cameraUpWorldSpace = m_MainCamera.transform.up;
        Vector3 cameraPositionWorldSpace = m_MainCamera.transform.position;

        Vector3 cameraDirectionPlaneSpace = m_ReflectionPlane.transform.InverseTransformDirection(cameraDirectionWorldSpace);
        Vector3 cameraUpPlaneSpace = m_ReflectionPlane.transform.InverseTransformDirection(cameraUpWorldSpace);
        Vector3 cameraPositonPlaneSpace = m_ReflectionPlane.transform.InverseTransformPoint(cameraPositionWorldSpace);

        cameraDirectionPlaneSpace.y *= -1.0f;
        cameraUpPlaneSpace.y *= -1.0f;
        cameraPositonPlaneSpace.y *= -1.0f;

        cameraDirectionWorldSpace = m_ReflectionPlane.transform.TransformDirection(cameraDirectionPlaneSpace);
        cameraUpWorldSpace = m_ReflectionPlane.transform.TransformDirection(cameraUpPlaneSpace);
        cameraPositionWorldSpace = m_ReflectionPlane.transform.TransformPoint(cameraPositonPlaneSpace);

        m_ReflectionCamere.transform.position = cameraPositionWorldSpace;
        m_ReflectionCamere.transform.LookAt(cameraPositionWorldSpace+cameraDirectionWorldSpace,cameraUpWorldSpace);

        m_ReflectionCamere.targetTexture = m_RenderTarget;

        //Shader.SetGlobalFloat("_WaterPlaneHeight", m_ReflectionPlane.transform.position.y);
         
        m_ReflectionCamere.Render();
        ssrMaterial.SetTexture("_Reflection", m_RenderTarget);
        ssrMaterial.SetFloat("_Alpha", _Alpha);
    }

    private void OnDisable()
    {
        m_RenderTarget.Release();
    }
}

Shader代码,自己谁便写个支持的就行

Shader "RF/Scene/SSR"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _NormalTex("Normal Texyure",2D)="bump"{}
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        #pragma surface surf Standard fullforwardshadows

        #pragma target 3.0

        sampler2D _MainTex;
        sampler2D _Reflection;
        sampler2D _NormalTex;

        struct Input
        {
            float2 uv_MainTex;
            float4 screenPos;
            float3 posWorld;
        };

        half _Glossiness;
        half _Metallic;
        fixed4 _Color;
        fixed _Alpha;

        UNITY_INSTANCING_BUFFER_START(Props)
        UNITY_INSTANCING_BUFFER_END(Props)

        float2 ComputeUV(float4 screenPos)
        {
            float2 uv=screenPos.xy/screenPos.w;
            uv=1-uv;
            #if UNITY_UV_STARTS_AT_TOP
                uv.y=1- uv.y;
            #endif
            return uv;
        }

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            fixed4 c1 = tex2D (_MainTex, IN.uv_MainTex) * _Color*(1- _Alpha);
            fixed4 c2 = tex2D(_Reflection,ComputeUV(IN.screenPos))* _Alpha;
            o.Albedo = c1.rgb+c2.rgb;
            o.Normal=UnpackNormal(tex2D(_NormalTex,IN.uv_MainTex));
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = 1;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

接下来说一下我自己的想法和遇到的一些坑,一开始我是把代码放到OnPostRender()里更新的,但是摄像机移动太快时反射的图像会闪烁,就是有时候图像对不上,看图:
在这里插入图片描述
把它改成LateUpdate()里刷新就没问题了,看图
在这里插入图片描述
第二个问题是,当被反射的物体在反射平面之下将会出现错误,看图:
在这里插入图片描述
这里可以用Clip来解决,因为反射平面是平行于XZ的平面的,所以它的Y轴是统一的,在反射相机开始渲染时给Shader传一个高度值(即反射平面的),用世界坐标进行判断,低于该高度的都裁剪掉,然后在反射相机渲染结束时恢复正常,这样就不影响主摄像机的渲染了。
最后一个是替换渲染,在场景比较复杂而又想用这个方法是可以考虑使用替换渲染,Unity也提供了现成的接口

 Camera.SetReplacementShader 
 public void SetReplacementShader (Shader shader, string replacementTag);

这里的替换规则我开始我怎么也搞不明白,试了N久后终于明白,它这里replacementTag是你要用那个标签做标记,比如说是“RenderType”时,你定义的替换着色器的“RenderType”=”Opaque“,那么它只会替换所有“RenderType”=”Opaque“的Shader,RenderType”不为”Opaque“的则不渲染…则不渲染…(无语,不明白为什么不能直接用原来自身的Shader,效率问题吗?),还有一个坑是Unity自带地形的草,我各种该标签都没有办法成功替换渲染(就没渲染出来),搞了一下午都没出来,最后不知道在哪看到的把Build in Shader里草用到的Shader拖到工程里,之后就OK了…妈的,绝了!
在这里插入图片描述
本来上个月就该写的,结果一直拖,真佩服我自己。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值