![](https://i-blog.csdnimg.cn/blog_migrate/d3e0fd079dc2c4a83ed45fa0b062f123.png)
平面反射的具体内容可以参考以下几篇文章,都总结得很不错:
下面是平面反射实现的具体思路:
首先在原相机的基础上拷贝一个反射摄像机,其位置等的参数保持和原来的摄像机一样,之后我们会为这一个反射摄像机添加一个脚本,脚本会为这两个摄像机进行同步,并利用反射摄像机渲染出一张物体经过镜面反射的渲染纹理,最后将这一张渲染纹理传入到平面plane的Shader中进行采样,就可以实现反射效果。
对于世界空间中某一物体变换,可以概括为:先进行M操作将物体变换到世界空间,再利用反射矩阵将处于世界空间中的物体经过一次变换到以某一平面的镜面对称的位置,最后再进行VP操作变换到反射摄像机空间中并进行透视投影;
反射矩阵
首先是反射矩阵的构建,可以看下面这一篇文章,写的很详细这里不多添加:
https://blog.csdn.net/Phantom1516/article/details/128762625
反射矩阵的作用就是将物体在世界空间中变换到以某一平面为准的镜面位置,记住此次操作要在MVP操作的M之后,VP之前!;
下面是挂在反射摄像机的脚本,此脚本会输出物体经过反射矩阵变换,以及反射摄像机空间变换后和透视投影后的一张渲染纹理:
//用于挂在获取反射贴图的摄像机上;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Numerics;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEngine.SceneManagement;
using UnityEngine.UIElements;
using UnityEditor;
using Matrix4x4 = UnityEngine.Matrix4x4;
using Vector3 = UnityEngine.Vector3;
public class plane_Reflection_camera : MonoBehaviour
{
[ExecuteAlways]
public Camera Main_camera;
public Camera Reflection_camera;
public GameObject plane;//定义平面组件的信息;
private readonly RenderTexture _Reflection_camera_RT;//定义反射RT;
private int _Reflection_camera_RT_ID;//定义主帖图_MainTex的ID;
public Material Reflection_material;//传入材质;
public Shader shader;//定义Shader;
//在脚本Start时候我们要先判断当前无Reflection_camera,如果没有则我们要获取新组件,并且设置获取的为摄像机的组件,最后还要
//RenderPipelineManager.beginCameraRendering += OnBeginCameraRendering来订略事件;
void Start()//Start函数在脚本开始时运行;
{
Debug.Log("Plane Reflection Success");
if (Reflection_camera == null)
{
//下面获取新的摄像机组件,并传给Reflection_camera;
var new_Gameogject = new GameObject("Reflection camera");//获取新的组件;
this.Reflection_camera = new_Gameogject.AddComponent<Camera>();//这里设置新的组件为摄像机组件;
}
RenderPipelineManager.beginCameraRendering += OnBeginCameraRendering;//加上渲染事件,这一个会在接下来进行定义;
//不要忘了加上渲染事件;
}
void OnBeginCameraRendering(ScriptableRenderContext context ,Camera camera)//这一个函数传入context,以及摄像机;
{
//1.首先会进行判断,判断传入的摄像机是否为反射的摄像机;
//2.先更新这一个摄像机,然后清除刚定义的摄像机的DepthBuffer以及ColorBuffer,用backgroung属性进行替代;
//3.再清除背景颜色,并同时确定摄像机的渲染层;
if (camera == this.Reflection_camera)
{
Update_camera(this.Reflection_camera);//首先更新新创建的refection_camera;
camera.clearFlags = CameraClearFlags.SolidColor;//清除刚定义的摄像机的DepthBuffer以及ColorBuffer,用backgroung属性进行替代;
camera.backgroundColor = Color.clear;//清除背景颜色;
camera.cullingMask = LayerMask.GetMask("reflect");//获取渲染层;
//接下来会构建反射矩阵,将摄像机转移到世界空间坐标下,并同时进行裁剪顺序的翻转;
var Reflection_camera_M = CalculateReflectionCameraMatrix(this.plane.transform.up, this.plane.transform.position);
GL.invertCulling = true;//进行裁剪顺序的翻转;//将裁剪顺序翻转回去,因为反射矩阵的变化会引起裁剪顺序的变化
//在进行摄像机的渲染之前,要进行视锥体的裁剪;!!
//接下来是摄像机开始渲染的部分;1.首先我们会利用UniversalRenderPipeline.RenderSingleCamera函数来进行开启摄像机;
//2.我们会进行屏幕截图获取一张RT贴图;
//3.我们会获取着色器属性名称_Reflection_camera_RT的唯一标识符_Reflection_camera_RT_ID ;
//4.利用Shader的SetGlobalTexture函数,以及ID,和着色器设置全局纹理;
//5.设置渲染纹理为设想的摄像机的渲染纹理,之后反射相机的输出就会到我们的临时纹理RT上;
//6.将贴图传入材质,并同时释放全局纹理;
UniversalRenderPipeline.RenderSingleCamera(context,camera);//摄像机开始渲染;
RenderTexture Reflection_camera_temporary_RT = RenderTexture.GetTemporary(Screen.width,Screen.height,0);//传入屏幕的狂高获取一张截图;
_Reflection_camera_RT_ID = Shader.PropertyToID("_Reflection_camera_RT_ID");//获取ID;
Shader.SetGlobalTexture(_Reflection_camera_RT_ID,_Reflection_camera_RT);//为着色器设置全局纹理;
camera.targetTexture = Reflection_camera_temporary_RT;//反射摄像机的输出就是这一张纹理;
Reflection_material.SetTexture(_Reflection_camera_RT_ID,Reflection_camera_temporary_RT);//将贴图传入到我们定义的材质中;
//最后释放RT临时纹理;
RenderTexture.ReleaseTemporary(Reflection_camera_temporary_RT);//释放临时纹理RT;
}
else
{
GL.invertCulling = false;//如果判断不为反射的摄像机则不会执行这一步;
}
}
//下面还要进行顶略事件的取消,以及同步两个相机,相当于反射相机的初始化;
private void OnDiable()
{
RenderPipelineManager.beginCameraRendering -= OnBeginCameraRendering;
}
//进行摄像机的同步操作;
private void Update_camera(Camera Reflection_camera)//同步两个摄像机的数据;
{
//先同步两个摄像机的数据,初始化完Reflection_camera后立刻背景颜色和深度的清除,然后设置在相机开始渲染前的各种设置
//1.如果两个摄像机都不存在则返回;
if (Main_camera == null || this.Reflection_camera == null)
{
return;
}
//2.
int target_display = Reflection_camera.targetDisplay;
Reflection_camera.CopyFrom(Main_camera);
Reflection_camera.targetDisplay = target_display;
}
//获取摄像机的一个反射矩阵;
//重在理解如何获取反射矩阵;
private Matrix4x4 CalculateReflectionCameraMatrix(Vector3 N, Vector3 plane_position)
{
//先初始化反射矩阵;
Matrix4x4 Reflection_camera_M = Matrix4x4.identity;
//获取反射矩阵中的d;
float d = -Vector3.Dot(plane_position, N);
Reflection_camera_M.m00 = 1 - 2 * N.x * N.x;
Reflection_camera_M.m01 = -2 * N.x * N.y;
Reflection_camera_M.m02 = -2 * N.x * N.z;
Reflection_camera_M.m03 = -2 * N.x * d;
Reflection_camera_M.m10 = -2 * N.y * N.x;
Reflection_camera_M.m11 = 1 - 2 * N.y * N.y;
Reflection_camera_M.m12 = -2 * N.y * N.z;
Reflection_camera_M.m13 = -2 * N.y * d;
Reflection_camera_M.m20 = -2 * N.z * N.x;
Reflection_camera_M.m21 = -2 * N.z * N.y;
Reflection_camera_M.m22 = 1 - 2 * N.z * N.z;
Reflection_camera_M.m23 = -2 * N.z * d;
Reflection_camera_M.m30 = 0;
Reflection_camera_M.m31 = 0;
Reflection_camera_M.m32 = 0;
Reflection_camera_M.m33 = 1;
return Reflection_camera_M;
}
}
一些脚本的函数解释
Camera.targetDisplay
![](https://i-blog.csdnimg.cn/blog_migrate/a7f2f7867825e247d134eaa12af2fc32.png)
Camera.clearFlags
![](https://i-blog.csdnimg.cn/blog_migrate/e0fa9fa71c71dd89f4ce18e892621dca.png)
CommandBuffer.GetTemporaryRT
![](https://i-blog.csdnimg.cn/blog_migrate/79c3c07002182ad68664feb893034fd8.png)
该函数不仅申请了一个临时rt,同时还将该rt与第一个参数"nameID"所代表的全局的ShaderProperty进行了绑定,也就是说之后的shader都可以用与nameID对应的名称使用该rt。
官方的urp渲染管线中,CopyColorPass就是用该函数将"_CameraColorTexture"这个纹理拷贝到临时rt中供之后的shader使用。
坑点:在调用CommandBuffer.ReleaseTemporaryRT进行释放并不是将该纹理清空了,该纹理依旧在内存中,如果在之后重新调用CommandBuffer.GetTemporaryRT申请一个大小格式相同的临时纹理,会拿到该纹理,也就是说CommandBuffer.GetTemporaryRT得到的不一定是一张干净的纹理,很有可能是已经被写过的,所以必要的时侯要进行clear。其次,如果发生了上述这种回收重用的情况,rt的名称可能会错乱,也就是说尽管shaderproperty的绑定逻辑是没有问题,但是rt的name没有相应的更新,在framedebugger中会让人十分困惑,例如两个相机,分别在afterRenderingTransparent之后进行了一次全屏copyblit,前者给了_Transparent的rt,后者给了_UITranspaernt的rt,但是之后发现在前一个相机渲染中,物体shader中_Transparent获取的纹理名称叫_UITranspaernt,在使用renderdoc会发现两次全屏copyblit的目标纹理使用的是同一个rt,自始至终只有一个叫_UITranspaernt的rt,该rt仅仅是在过程中更换一下和shaderproperty的绑定,因此shader中的_Transparent和_UITranspaernt都会得到该rt。虽然在逻辑上没有错,最终结果是正确的,但是会给debug过程中带来困扰,如果想要规避这种情况可以显示地使用new RenderTexture创建rt。
还有平面Shader的代码:
Shader "Unlit/PlanerReflection_shader"
{
Properties
{
_Diffuse("Diffuse", Color) = (1, 1, 1, 1)
_ReflectColor("_ReflectColor", Color) = (1, 1, 1, 1)
_ReflectAmount("_ReflectAmount", Range(0,1)) = 1
_Reflection_CubeMap("_Reflection_CubeMap", Cube) = "_Skybox"{}
_Fresnel("Fresnel", Float) = 1.0
_Specular("Specular", Color) = (1, 1, 1, 1)
_Gloss("Gloss", Range(8,255)) = 8.0
}
SubShader
{
Tags {"Queue" = "Transparent" "RenderType" = "Transparent" "RenderPipeline" = "UniversalPipeline"}
Pass
{
Tags {"LightMode" = "UniversalForward" }
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS //接收阴影
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE //得到正确的阴影坐标
#pragma multi_compile _ _SHADOWS_SOFT //软阴影
#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.universal/ShaderLibrary/Shadows.hlsl"
CBUFFER_START(UnityPerMaterial)
half4 _Diffuse;
float4 _ReflectColor;//用于控制反射的天空盒子的颜色量
float _ReflectAmount;//用于控制反射的天空盒子的颜色和漫反射diffuse在总体反射中的占比
samplerCUBE _Reflection_CubeMap;
float _Fresnel;
half4 _Specular;
half _Gloss;
TEXTURE2D(_Reflection_camera_RT);
SamplerState sampler_Reflection_camera_RT;//采样采样设置
TEXTURE2D(_Reflection_Scene_camera_RT);
SamplerState sample_Reflection_Scene_camera_RT;
CBUFFER_END
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
//float2 uv: TEXCOORD0;
};
struct v2f
{
float4 position_CS : SV_POSITION;//裁剪空间的坐标
float4 screenPos : TEXCOORD0;//屏幕坐标
float4 position_WS : TEXCOORD1;//顶点位置坐标
float3 view_direct_WS : TEXCOORD2;//人眼光路的入射方向
float3 reflec_direct_WS : TEXCOORD3;//人眼光路反射后的方向,用于采样天空盒
float3 normal_WS : TEXCOORD4;//顶点在世界空间的法线方向
};
v2f vert (appdata v)
{
v2f o;
o.position_CS = TransformObjectToHClip(v.vertex);//获得顶点在裁剪空间的坐标
o.position_WS = mul(unity_ObjectToWorld, v.vertex);
o.screenPos = ComputeScreenPos(o.position_CS);//计算用于采样反射贴图的顶点
o.normal_WS = TransformObjectToWorldNormal(v.normal);//计算世界空间下的法线方向
o.view_direct_WS = GetWorldSpaceViewDir(o.position_WS);//获取世界空间下的人眼光路的入射方向,这里获取的方向是从顶点指向摄像机
o.reflec_direct_WS = reflect(-normalize(o.view_direct_WS),normalize(o.normal_WS));//获取世界空间下人眼光路的反射光线
return o;
}
half4 frag (v2f i) : SV_Target
{
float4 SHADOW_COORDS = TransformWorldToShadowCoord(i.position_WS);//获取将世界空间的顶点坐标转换到光源空间获取的阴影坐标,这里在片元着色器里面进行,利用了插值之后的结果
Light main_light = GetMainLight(SHADOW_COORDS);
float3 light_direction_WS = normalize(TransformObjectToWorld(main_light.direction));//获取世界空间的光照单位矢量
float3 view_direct_WS = normalize(i.view_direct_WS);
float3 reflec_direct_WS = normalize(i.reflec_direct_WS);
float3 normal_WS = normalize(i.normal_WS);
half3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;//获取环境光强度
half3 diffuse = main_light.color.rgb * _Diffuse.rgb * saturate(dot(normal_WS, light_direction_WS));//获取漫反射强度,light.color.rgb是光照强度
half3 reflection = texCUBE(_Reflection_CubeMap,reflec_direct_WS).rgb * _ReflectColor.rgb;//获取反射的天空盒子的颜色
half3 half_dir = normalize(light_direction_WS + view_direct_WS);//获取半程向量
half3 specular = main_light.color.rgb * _Specular.rgb * pow(saturate(dot(half_dir,normal_WS)), _Gloss);//获取高光
reflection = lerp(diffuse, reflection, _ReflectAmount);//通过平滑函数获得最佳的反射
float fresnel = pow(1 - dot(normal_WS, view_direct_WS), _Fresnel);
half4 reflection_map_color = _Reflection_camera_RT.Sample(sampler_Reflection_camera_RT, i.screenPos.xy / i.position_CS.w);//采样反射纹理贴图
half4 color = half4(ambient + fresnel * reflection + specular, 1.0);
color = reflection_map_color + color;
return color;
}
ENDHLSL
}
}
Fallback off
}
一些设置
记住我们在显示物体的镜面效果时,要将物体最上面的渲染层新建一个或选择一个与上述我们自己定义反射摄像机的渲染层相同的渲染层:
![](https://i-blog.csdnimg.cn/blog_migrate/5f3b0cec33c9ab752bc0cf48ec297aba.png)
capsule
此处的Layer层要与我们在摄像机里面的相同,上面我们设置为“reflect”,因此这里我们自己创建的时候也要命名为"reflect"!!;
下面是反射摄像机的设置:
![](https://i-blog.csdnimg.cn/blog_migrate/bb3b5110660b712271e76927b2e2317d.png)
在Rendering里面的Culling Mask要设置为reflect;
下面是效果图:
![](https://i-blog.csdnimg.cn/blog_migrate/fd33160ba61c82732afbbde75a9e52df.png)
![](https://i-blog.csdnimg.cn/blog_migrate/2033962704b077d34d0b6a9bab6a229b.png)
注意到此处我们还没有加入视锥体的裁剪,因此效果很奇怪,下面我们会加入;
视锥体的裁剪
如下图,视锥体的A部分应该被裁剪掉;
![](https://i-blog.csdnimg.cn/blog_migrate/7a9d96335cf6f94148bb9218fbd3e2b2.png)
具体实现方法看这一篇文章:
https://zhuanlan.zhihu.com/p/493766119
需要注意的是我们应该将其放在订阅的时间内,在摄像机开始进行渲染之前;
简单概括,这种技术通过改变 MVP 中的 P 矩阵,实现用指定的平面来当作近平面,但同时会影响到远平面,而其他四个平面不受影响,
我们先用四维向量来对平面进行表示,再将世界空间中的平面表示转到摄像机空间,之后我们会利用Reflection_camera中的CalculateObliqueMatrix函数来设置反射平面成为近平面的矩阵,最后会将这一个透视投影矩阵传入到Reflection_camera的projectionMatrix中进行之后的渲染操作,代码如下:
Vector4 viewPlane = new Vector4(this.Plane.transform.up.x, this.Plane.transform.up.y,this.Plane.transform.up.z,
-Vector3.Dot(this.Plane.transform.position, this.Plane.transform.up));//获取平面的四维表示;
viewPlane = Reflection_camera.worldToCameraMatrix.inverse.transpose * viewPlane;//将世界空间中的平面表示传到摄像机空间;
var newMatrix = Reflection_camera.CalculateObliqueMatrix(viewPlane);//这里获取以摄像机以反射平面为近平面的矩阵;
Reflection_camera.projectionMatrix = newMatrix;
![](https://i-blog.csdnimg.cn/blog_migrate/091123d59cd920827d07e1463a41acc9.png)
这是最后的效果,很明显已经解决了视锥体的问题;
到此就是平面反射的全部内容,其他知识还待以后学习;