屏幕空间反射(Screen Space Reflection):用途蛮广的,诸如水面的倒影、光滑地面的反射、大厦玻璃的反射,写好了的话对于画面表现上来说属于锦上添花的效果。
先来看看实现的效果:
如其名字——屏幕空间,使用了类似于后处理的手法,用渲染好的RenderTexture作为反射源输入,所以没被渲染进RenderTexture的场景信息没有办法得到正确的反射效果。如上图所示,天花板没办法反射出来、背后的方块颜色没法反射。
并且这是一个全屏的效果,没有哪个场景是所有的物体都需要这么强烈的反射效果的,所以我给地板加上一个Stencil Test,这样就只有地板进行了反射。虽然依旧存在瑕疵,但是比上面全屏效果要好的多了,只有地板会进行反射(至于Capsule为啥也有反射,我也属实不懂,反正不是我的原因,估计是Unity Stencil Test自己的原因)。
Shader的算法思想主要参考在Unity中实现屏幕空间反射Screen Space Reflection系列。对于算法的步骤已经有了详细的阐述,我就不再赘述了,以下是Shader代码。
Shader "Hidden/SSR_Blog"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
//LOD 100
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
float4 _MainTex_TexelSize;
fixed _RayPercent;
//Gbuffer1通常为镜面反射 + 粗糙度
sampler2D _CameraGBufferTexture1;
//Gbuffer1通常为法线
sampler2D _CameraGBufferTexture2;
float4x4 _WorldToView;
UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture);
float4x4 _Projection;
sampler2D _BackfaceDepthTexture;
float RAY_LENGTH;
int STEP_COUNT;
bool traceRay(float3 start,float3 direction, out float2 hitPixel,out fixed3 debugCol)
{
UNITY_LOOP
for(int i=1;i<=STEP_COUNT;i++)
{
//p是当前的光线的空间位置
float3 p=start+(float)i/STEP_COUNT*RAY_LENGTH*direction;
//_ProjectionParams.z是far clip plane的值。
//又因为viewspace下正前方z值是负的,所以加个负号。
float pDepth=p.z/-_ProjectionParams.z;
//将光线投影到screen space中。
float4 screenCoord=mul(_Projection,float4(p,1));
screenCoord/=screenCoord.w;
if(screenCoord.x<-1||screenCoord.y<-1||screenCoord.x>1||screenCoord.y>1)
{
return false;
}
//获取当前像素的深度。为了使用循环结构,这里必须用tex2Dlod而不是tex2D。
float compareDepth=Linear01Depth(tex2Dlod(_CameraDepthTexture,
float4(screenCoord.xy/2+0.5,0,0)));
float backZ=tex2Dlod(_BackfaceDepthTexture,float4(screenCoord.xy/2+0.5,0,0));
if(pDepth>compareDepth&&pDepth<backZ)
{
hitPixel=screenCoord.xy/2+0.5;
debugCol=fixed3(hitPixel,0);
return true;
}
}
return false;
}
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float4 csRay:TEXCOORD1;
};
v2f vert ( appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord.xy;
//作为一个后期特效,我们可以通过uv坐标,来获得相机光线方向。
//注意坐标z为1.0,这里的cameraRay是从原点到far clip plane的光线
float4 cameraRay=float4(v.texcoord.xy*2-1,1,1);
//将相机光线从clip space转移到view space
cameraRay=mul(unity_CameraInvProjection,cameraRay);
o.csRay=cameraRay/cameraRay.w;
return o;
}
fixed4 fragA (v2f i) : SV_Target
{
float decodedDepth=Linear01Depth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture,i.uv));
//因为i.csRay是指着far clip plane的光线,此时csRayOrigin是view space的光线起点
float3 csRayOrigin=decodedDepth*i.csRay;
float3 worldNormal=tex2D(_CameraGBufferTexture2,i.uv).rgb*2-1;
float3 cameraNormal=normalize(mul((float3x3)_WorldToView,worldNormal));
float2 hitPixel;
float3 debugCol;
fixed4 reflection=0;
if (traceRay(csRayOrigin,normalize(reflect(csRayOrigin,cameraNormal)),
hitPixel,debugCol))
{
reflection=(1- _RayPercent)*tex2D(_MainTex,hitPixel);
}
return tex2D(_MainTex,i.uv)+tex2D(_CameraGBufferTexture1,i.uv)*reflection;
}
ENDCG
Pass
{
//注意我在这里使用了模板测试
Stencil
{
Ref 1
Comp Equal
}
Cull Off
CGPROGRAM
#pragma vertex vert
#pragma fragment fragA
#pragma target 3.0
ENDCG
}
}
}
这里还需要一个渲染背面的深度的Shader
Shader "Hidden/BackfaceShader"
{
Properties
{
}
SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Geometry+1"}
LOD 100
Cull Front
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f {
float4 position : POSITION;
float4 linearDepth : TEXCOORD0;
};
v2f vert(appdata_base v) {
v2f output;
output.position = UnityObjectToClipPos(v.vertex);
output.linearDepth = float4(0.0, 0.0, COMPUTE_DEPTH_01, 0.0);
return output;
}
float4 frag(v2f input) : COLOR
{
return float4(input.linearDepth.z, 0.0, 0.0, 0.0);
}
ENDCG
}
}
}
最后是C#脚本
using UnityEngine;
[ExecuteInEditMode,RequireComponent(typeof(Camera))]
public class SSREffect : MonoBehaviour
{
private Shader _backfaceShader;
private Material _ssrMaterial;
[Range(0.0f, 1.0f)]
public float RayPercent=0.5f;
[Range(1.0f, 10.0f)]
public float RayLength=4.0f;
[Range(32,124)]
public int StepCount=64;
private void Awake()
{
Camera.main.depthTextureMode = DepthTextureMode.Depth;
}
//SSR材质
Material SSRMaterial
{
get
{
if(_ssrMaterial==null)
{
_ssrMaterial = new Material(Shader.Find("Hidden/SSR_Blog"));
}
return _ssrMaterial;
}
}
//背面深度材质
Shader BFShader
{
get
{
if(_backfaceShader==null)
{
_backfaceShader = Shader.Find("Hidden/BackfaceShader");
}
return _backfaceShader;
}
}
private RenderTexture backfaceTexture;
private Camera backfaceCamera;
RenderTexture _deferredTexture;
private Camera defferredCamere;
public void OnPostRender()
{
RenderBackFace();
DeferredRender();
SSRMaterial.hideFlags = HideFlags.HideAndDontSave;
SSRMaterial.SetMatrix("_WorldToView", GetComponent<Camera>().worldToCameraMatrix);
SSRMaterial.SetTexture("_BackfaceDepthTexture", GetBackFaceTexture());
SSRMaterial.SetMatrix("_Projection", GetComponent<Camera>().projectionMatrix);
SSRMaterial.SetTexture("_MainTex", GetDeferredTexture());
SSRMaterial.SetFloat("_RayPercent", RayPercent);
SSRMaterial.SetFloat("RAY_LENGTH", RayLength);
SSRMaterial.SetInt("STEP_COUNT", StepCount);
GL.PushMatrix();
GL.LoadOrtho();
SSRMaterial.SetPass(0);
// draw a quad over whole screen
GL.Begin(GL.QUADS);
GL.TexCoord2(0, 0);
GL.Vertex3(0, 0, 0);
GL.TexCoord2(1, 0);
GL.Vertex3(1, 0, 0);
GL.TexCoord2(1, 1);
GL.Vertex3(1, 1, 0);
GL.TexCoord2(0, 1);
GL.Vertex3(0, 1, 0);
GL.End();
GL.PopMatrix();
}
//背面渲染深度渲染贴图
private RenderTexture GetBackFaceTexture()
{
if(backfaceTexture==null)
{
backfaceTexture = new RenderTexture(Screen.width, Screen.height, 24, RenderTextureFormat.RFloat);
backfaceTexture.filterMode = FilterMode.Point;
}
return backfaceTexture;
}
//延迟渲染贴图
private RenderTexture GetDeferredTexture()
{
if(_deferredTexture==null)
{
_deferredTexture = new RenderTexture(Screen.width, Screen.height, 24, RenderTextureFormat.ARGB32);
}
return _deferredTexture;
}
//渲染背面深度
private void RenderBackFace()
{
if(backfaceCamera==null)
{
GameObject _object = new GameObject();
Camera _camera = Camera.main;
_object.transform.SetParent(_camera.transform);
_object.hideFlags = HideFlags.HideAndDontSave;
backfaceCamera = _object.AddComponent<Camera>();
backfaceCamera.CopyFrom(_camera);
backfaceCamera.enabled = false;
backfaceCamera.clearFlags = CameraClearFlags.SolidColor;
backfaceCamera.backgroundColor = Color.white;
backfaceCamera.renderingPath = RenderingPath.Forward;
backfaceCamera.SetReplacementShader(BFShader,"RenderType");
backfaceCamera.targetTexture = GetBackFaceTexture();
}
backfaceCamera.Render();
}
//延迟渲染
private void DeferredRender()
{
if (defferredCamere == null)
{
GameObject _object = new GameObject();
_object.name = "DeferredCamera";
Camera _camera = Camera.main;
_object.transform.SetParent(_camera.transform);
_object.hideFlags = HideFlags.HideAndDontSave;
defferredCamere = _object.AddComponent<Camera>();
defferredCamere.CopyFrom(_camera);
defferredCamere.enabled = false;
defferredCamere.clearFlags = CameraClearFlags.Skybox;
defferredCamere.backgroundColor = Color.white;
defferredCamere.allowMSAA = false;
defferredCamere.renderingPath = RenderingPath.DeferredShading;
defferredCamere.targetTexture = GetDeferredTexture();
}
defferredCamere.Render();
}
}
这种方法其实不难,就是有几个坑,而且由于用了延迟渲染和模板测试手机上就别想用了。
下面我来详细讲讲我遇到的几个坑。
为什么用OnPostRender() 而不用 OnRenderImage()?因为用OnRenderImage()模板测试就不好用了,并且貌似Unity的延迟渲染对模板测试也不是很好?还是因为Shader的原因?说到模板测试,这里还用一个写入模板值的Shader,该Shader用作反射物的Shader,我的是用在地板上,以下是代码。
Shader "Custom/FloorStencil"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Glossiness ("Smoothness", Range(0,1)) = 0.5
_Metallic ("Metallic", Range(0,1)) = 0.0
}
SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Geometry"}
//LOD 200
Stencil
{
Ref 1
Comp Always
Pass Replace
}
CGPROGRAM
#pragma surface surf Standard fullforwardshadows
#pragma target 3.0
sampler2D _MainTex;
struct Input
{
float2 uv_MainTex;
};
half _Glossiness;
half _Metallic;
fixed4 _Color;
void surf (Input IN, inout SurfaceOutputStandard o)
{
// Albedo comes from a texture tinted by color
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
// Metallic and smoothness come from slider variables
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
请注意我在这里用了Surface Shader,因为Surface Shader有着对延迟渲染的良好支持,我也有写一个Unlit Shader版本但是关于延迟渲染部分写得不对(想想我就没写过支持延迟渲染的Shader,有时间再研究研究吧),死活没办法实现模板测试…就挺难受的,只能用Surface Shader了。
还有一个坑,看图:
可以看到胶囊体的倒影出现了长长的拖尾,我用自己写的Surface Shader。
然后我改成了Standard…
我属实不知道什么原因,难道还是因为延迟渲染?
这大概就是我遇到的所有的问题了,下一篇准备介绍两个相机的平面反射实现版本。本文到此结束,感谢你的阅读,如有错误欢迎指正。