这是屏幕空间反射的第二篇,严格来说是平面反射(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了…妈的,绝了!
本来上个月就该写的,结果一直拖,真佩服我自己。。。