整体思路
此方法并不依赖shader,而是使用MonoBehaviour脚本(挂载到反射平面或者相机上)
核心思路:
简单粗暴,利用脚本创建一个新的相机,这个相机与主相机参数位置一致,将此相机的图像翻转即可,大体步骤为
- 创建一个相机
- 对渲染内容进行镜像
- 输出RT
- 传进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'的位置,我们有
向量N是方向,PQ则是长度
且只有|PQ|是未知的,下面我们就要求得|PQ|的具体值
首先连接PO得到一个 角
我们知道:
上面的暂且先放着,我们点积一下OP和N
提取
嘿,我们有了!带入上面的试试!
我们约分掉OP,因为他们两个都是长度,并且假定是单位向量,即模长为1
所以可直接得
其中可由P - O得到,我们也带入进去
等价于
这里我们再看,剩下的N是我们的法线向量,O是反射平面的原点,P则是顶点是个常量
都是可以得到或者已知的值了
下面我们就可以带入到一开始的式子里了,就是这个
代入后
好了,我们现在要带入每个值的具体坐标了,这里看起来要麻烦一点,但并不复杂
另外别忘了我们的最终目的是得到一个反射矩阵
还不够,再拆
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图做了一些扭曲