
一、平面反射概述
平面反射(Planar Reflection)是计算机图形学中模拟光滑平面(如水面、镜面、抛光地面)反射效果的核心技术。它能够实时生成倒影,让场景更加真实和生动。
在Unity的通用渲染管线(URP)中,实现平面反射需要综合运用以下技术:
- 反射矩阵计算:将世界空间中的物体沿反射平面进行镜像翻转
- 反射相机:从镜像视角渲染场景到RenderTexture
- 斜截视锥体:优化反射相机的裁剪平面,避免渲染不必要的区域
- Shader投影:将反射纹理正确投影到反射平面上
💡 应用场景:平面反射广泛应用于水面效果、镜面反射、抛光地面、冰面反射等需要真实倒影的场景中。
二、平面反射基本原理
2.1 核心思想
平面反射的核心思想是模拟光线反射:在反射平面的一侧放置一个虚拟的"反射相机",这个相机的位置是主相机关于反射平面的镜像位置,它看到的内容就是倒影应该显示的内容。

2.2 两种实现思路
| 实现方式 | 核心逻辑 | 特点 |
|---|---|---|
| 无反射矩阵法 | 1. 计算主相机关于反射平面的对称位置,生成反射相机 2. 反射相机渲染的画面需要颠倒Y轴作为UV采样 | 矩阵计算量较大,逻辑直观 |
| 反射矩阵法(推荐) | 1. 反射相机和主相机位置相同,在MVP变换的V矩阵前插入反射矩阵,将整个世界沿平面镜像 2. 采样时无需颠倒Y轴 | 计算量更小,是主流实现方式 |
⚠️ 注意:反射矩阵是左乘,实际是加在M矩阵之后、V矩阵之前,而非网传V矩阵之后。修改反射相机的worldToCameraMatrix后,再修改其transform无实际意义。
三、反射矩阵推导
3.1 数学原理
反射矩阵的作用是将世界空间中任意点变换为关于目标平面的对称点。
推导过程:
- 已知平面单位法向量 $\vec{N}(x_n, y_n, z_n)$,平面上任意点 $P_0(x_0, y_0, z_0)$
- 任意点 $P(x, y, z)$ 的对称点 $P'(x', y', z')$ 满足: $$P' = P - 2\vec{N} \cdot \|PQ\|$$
- 其中$\|PQ\|$是P到平面的带符号距离,代入平面方程展开后得到反射矩阵
反射矩阵公式:
$$M=\begin{bmatrix}
1-2x_n^2 & -2y_nx_n & -2z_nx_n & -2x_nd \\
-2x_ny_n & 1-2y_n^2 & -2z_ny_n & -2y_nd \\
-2x_nz_n & -2y_nz_n & 1-2z_n^2 & -2z_nd \\
0 & 0 & 0 & 1
\end{bmatrix}$$
其中 $d = -\vec{N} \cdot P_0$,即平面方程的常数项。
3.2 代码实现
以下是C#中计算反射矩阵的实现:
private static Matrix4x4 CalculateReflectionMatrix(Vector4 plane)
{
Matrix4x4 reflectionMat = Matrix4x4.identity;
// 平面方程: ax + by + cz + d = 0
// plane = (a, b, c, d)
float a = plane.x;
float b = plane.y;
float c = plane.z;
float d = plane.w;
reflectionMat.m00 = 1f - 2f * a * a;
reflectionMat.m01 = -2f * a * b;
reflectionMat.m02 = -2f * a * c;
reflectionMat.m03 = -2f * a * d;
reflectionMat.m10 = -2f * b * a;
reflectionMat.m11 = 1f - 2f * b * b;
reflectionMat.m12 = -2f * b * c;
reflectionMat.m13 = -2f * b * d;
reflectionMat.m20 = -2f * c * a;
reflectionMat.m21 = -2f * c * b;
reflectionMat.m22 = 1f - 2f * c * c;
reflectionMat.m23 = -2f * c * d;
reflectionMat.m30 = 0f;
reflectionMat.m31 = 0f;
reflectionMat.m32 = 0f;
reflectionMat.m33 = 1f;
return reflectionMat;
}
四、反射相机实现
4.1 斜截视锥体(Oblique View Frustum)
问题描述:默认反射相机的视锥体和主相机完全一致,会导致反射画面包含反射平面"背后"的物体(即反射平面下方的物体),出现错误的倒影。

解决方案:将反射相机的近裁剪面设置为反射平面本身,仅渲染平面"上方"的内容。
Unity提供了内置API:Camera.CalculateObliqueMatrix(),可以直接计算斜裁剪矩阵。
手动实现斜截矩阵的核心原理:
- 平面变换规则:平面是共变向量,从视空间变换到裁剪空间需要用投影矩阵的逆的转置
- 投影矩阵修改规则:只能修改投影矩阵的第3行(第4行负责透视矫正插值,不能修改)
- 修改后近平面变为目标反射平面,远平面会相应调整
4.2 URP中的渲染回调
在URP中,需要使用URP的渲染事件系统来插入反射渲染逻辑。主要使用以下回调:
| 回调事件 | 触发时机 | 用途 |
|---|---|---|
RenderPipelineManager.beginCameraRendering | 每个相机开始渲染前 | 执行反射渲染逻辑 |
RenderPipelineManager.endCameraRendering | 每个相机渲染结束后 | 清理状态 |
URP 14+ 渲染API:
// 创建渲染请求
var request = new UniversalRenderPipeline.SingleCameraRequest();
if (RenderPipeline.SupportsRenderRequest(_reflectionCamera, request))
{
// 提交反射相机渲染请求
RenderPipeline.SubmitRenderRequest(_reflectionCamera, request);
}
五、Shader实现
5.1 基础平面反射Shader
以下是一个适用于URP的完整平面反射Shader:
Shader "Custom/PlanarReflection_URP"
{
Properties
{
// 主贴图(地面/水面纹理)
_BaseMap ("Base Map", 2D) = "white" {}
_BaseColor ("Base Color", Color) = (1,1,1,1)
// 反射 RenderTexture(由 C# 脚本自动设置)
_ReflectionTex ("Reflection Texture", 2D) = "black" {}
// 反射强度(0=无反射,1=完全镜面)
_ReflectionStrength ("Reflection Strength", Range(0, 1)) = 0.5
// 反射颜色叠乘(可给倒影添加色调)
_ReflectionTint ("Reflection Tint", Color) = (1,1,1,1)
}
SubShader
{
Tags
{
"RenderPipeline" = "UniversalRenderPipeline"
"Queue" = "Geometry"
"RenderType" = "Opaque"
}
Pass
{
Name "PlanarReflectionPass"
Tags { "LightMode" = "UniversalForward" }
Cull Back
ZWrite On
Blend Off
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_instancing
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
// 贴图声明
TEXTURE2D(_BaseMap);
SAMPLER(sampler_BaseMap);
TEXTURE2D(_ReflectionTex);
SAMPLER(sampler_ReflectionTex);
// 材质属性
float4 _BaseMap_ST;
float4 _BaseColor;
float _ReflectionStrength;
float4 _ReflectionTint;
struct Attributes
{
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float4 positionHCS : SV_POSITION;
float2 uv : TEXCOORD0;
float4 screenPos : TEXCOORD1; // 屏幕空间坐标
UNITY_VERTEX_INPUT_INSTANCE_ID
UNITY_VERTEX_OUTPUT_STEREO
};
Varyings vert(Attributes v)
{
Varyings o;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_TRANSFER_INSTANCE_ID(v, o);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
o.positionHCS = TransformObjectToHClip(v.positionOS.xyz);
o.uv = TRANSFORM_TEX(v.uv, _BaseMap);
// 计算屏幕空间坐标
o.screenPos = ComputeScreenPos(o.positionHCS);
return o;
}
half4 frag(Varyings i) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(i);
// 1. 采样主贴图
half4 baseCol = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv);
baseCol *= (half4)_BaseColor;
// 2. 采样反射RT(屏幕空间UV需做透视除法)
float2 reflUV = i.screenPos.xy / i.screenPos.w;
half4 reflCol = SAMPLE_TEXTURE2D(_ReflectionTex, sampler_ReflectionTex, reflUV);
reflCol *= (half4)_ReflectionTint;
// 3. 混合基础色和反射
half3 finalColor = lerp(baseCol.rgb, reflCol.rgb, _ReflectionStrength);
return half4(finalColor, baseCol.a);
}
ENDHLSL
}
}
}
5.2 高级效果扩展
5.2.1 菲涅尔控制反射强度
模拟真实世界的反射规律:正面看反射弱,掠射角看反射强。
// 在Varyings中新增世界法线和世界空间视角方向
float NdotV = saturate(dot(normalWS, viewDirWS));
float fresnel = pow(1.0 - NdotV, 3.0);
// 菲涅尔调制后的反射强度
float reflStrength = lerp(_ReflectionStrength * 0.3, _ReflectionStrength, fresnel);
half3 finalColor = lerp(baseCol.rgb, reflCol.rgb, reflStrength);
5.2.2 法线扰动倒影(水面波纹效果)
用法线贴图偏移反射UV,模拟水面波纹。
// 采样法线贴图
float3 bumpNormal = UnpackNormal(SAMPLE_TEXTURE2D(_BumpMap, sampler_BumpMap, i.uv));
// 用法线XY分量偏移反射UV
float2 reflUV = i.screenPos.xy / i.screenPos.w;
reflUV += bumpNormal.xy * _DistortionStrength;
half4 reflCol = SAMPLE_TEXTURE2D(_ReflectionTex, sampler_ReflectionTex, reflUV);
5.2.3 模糊反射(粗糙地面效果)
对反射RT做模糊采样,模拟粗糙表面的模糊倒影。
float2 texelSize = 1.0 / float2(_ScreenParams.x, _ScreenParams.y);
half3 blur = half3(0,0,0);
// 3x3卷积模糊
for (int x = -1; x <= 1; x++)
for (int y = -1; y <= 1; y++)
blur += SAMPLE_TEXTURE2D(_ReflectionTex, sampler_ReflectionTex,
reflUV + float2(x, y) * texelSize * _BlurRadius).rgb;
blur /= 9.0;
六、C# 控制脚本
以下是完整的平面反射控制器脚本,适配URP管线:
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
/// <summary>
/// 平面反射控制器,挂载到反射平面对象上
/// </summary>
[ExecuteAlways]
public class PlanarReflection : MonoBehaviour
{
[Header("===== 反射设置 =====")]
[SerializeField] private int _textureSize = 512; // RT分辨率
[SerializeField] private float _clipPlaneOffset = 0.01f; // 裁剪面偏移
[SerializeField] private LayerMask _reflectionLayers = -1; // 反射层过滤
[SerializeField] private bool _renderShadows = false; // 是否渲染阴影
private Camera _reflectionCamera;
private RenderTexture _reflectionRT;
private Material _material;
private static readonly int ReflectionTexID = Shader.PropertyToID("_ReflectionTex");
void OnEnable()
{
// 获取反射平面的材质
Renderer renderer = GetComponent<Renderer>();
if (renderer != null)
_material = renderer.sharedMaterial;
CreateReflectionCamera();
CreateRenderTexture();
// 注册URP渲染回调
RenderPipelineManager.beginCameraRendering += OnBeginCameraRendering;
}
void OnDisable()
{
RenderPipelineManager.beginCameraRendering -= OnBeginCameraRendering;
CleanUp();
}
private void OnBeginCameraRendering(ScriptableRenderContext context, Camera cam)
{
// 仅处理主摄像机和场景视图摄像机
if (cam.cameraType != CameraType.Game && cam.cameraType != CameraType.SceneView)
return;
if (cam == _reflectionCamera)
return;
RenderReflection(cam);
}
private void RenderReflection(Camera mainCamera)
{
if (_material == null)
return;
// 1. 计算反射平面参数
Vector3 planePos = transform.position;
Vector3 planeNormal = transform.up;
// 2. 计算反射矩阵
float d = -Vector3.Dot(planeNormal, planePos) - _clipPlaneOffset;
Vector4 reflectionPlane = new Vector4(planeNormal.x, planeNormal.y, planeNormal.z, d);
Matrix4x4 reflectionMatrix = CalculateReflectionMatrix(reflectionPlane);
// 3. 设置镜像摄像机参数
_reflectionCamera.cullingMask = _reflectionLayers;
_reflectionCamera.targetTexture = _reflectionRT;
// 镜像摄像机的观察矩阵 = 主摄像机观察矩阵 × 反射矩阵
_reflectionCamera.worldToCameraMatrix = mainCamera.worldToCameraMatrix * reflectionMatrix;
// 4. 设置斜裁剪面
Vector4 clipPlane = CameraSpacePlane(_reflectionCamera, planePos, planeNormal);
_reflectionCamera.projectionMatrix = mainCamera.CalculateObliqueMatrix(clipPlane);
// 5. 翻转剔除方向(反射后三角形绕序反转)
GL.invertCulling = true;
// 6. URP 14+ 推荐渲染API
var request = new UniversalRenderPipeline.SingleCameraRequest();
if (RenderPipeline.SupportsRenderRequest(_reflectionCamera, request))
RenderPipeline.SubmitRenderRequest(_reflectionCamera, request);
GL.invertCulling = false;
// 7. 将渲染好的RT传递给材质
_material.SetTexture(ReflectionTexID, _reflectionRT);
// 或者使用全局纹理(适用于多个反射平面共享)
// Shader.SetGlobalTexture(ReflectionTexID, _reflectionRT);
}
// 计算反射矩阵
private static Matrix4x4 CalculateReflectionMatrix(Vector4 plane)
{
Matrix4x4 m = Matrix4x4.identity;
m.m00 = 1f - 2f * plane.x * plane.x;
m.m01 = -2f * plane.x * plane.y;
m.m02 = -2f * plane.x * plane.z;
m.m03 = -2f * plane.x * plane.w;
m.m10 = -2f * plane.y * plane.x;
m.m11 = 1f - 2f * plane.y * plane.y;
m.m12 = -2f * plane.y * plane.z;
m.m13 = -2f * plane.y * plane.w;
m.m20 = -2f * plane.z * plane.x;
m.m21 = -2f * plane.z * plane.y;
m.m22 = 1f - 2f * plane.z * plane.z;
m.m23 = -2f * plane.z * plane.w;
m.m30 = 0; m.m31 = 0; m.m32 = 0; m.m33 = 1;
return m;
}
// 将世界空间平面转换到摄像机空间
private Vector4 CameraSpacePlane(Camera cam, Vector3 pos, Vector3 normal)
{
Matrix4x4 worldToCam = cam.worldToCameraMatrix;
Vector3 camPos = worldToCam.MultiplyPoint(pos);
Vector3 camNormal = worldToCam.MultiplyVector(normal).normalized;
return new Vector4(camNormal.x, camNormal.y, camNormal.z, -Vector3.Dot(camPos, camNormal));
}
// 创建镜像摄像机
private void CreateReflectionCamera()
{
if (_reflectionCamera != null) return;
GameObject go = new GameObject("ReflectionCamera");
go.hideFlags = HideFlags.HideAndDontSave;
_reflectionCamera = go.AddComponent<Camera>();
_reflectionCamera.enabled = false; // 手动控制渲染
// URP需要添加UniversalAdditionalCameraData组件
var cameraData = go.AddComponent<UniversalAdditionalCameraData>();
cameraData.requiresColorOption = CameraOverrideOption.Off;
cameraData.requiresDepthOption = CameraOverrideOption.Off;
cameraData.renderShadows = _renderShadows;
}
// 创建反射RT
private void CreateRenderTexture()
{
if (_reflectionRT != null && _reflectionRT.width == _textureSize) return;
if (_reflectionRT != null)
_reflectionRT.Release();
_reflectionRT = new RenderTexture(_textureSize, _textureSize, 16, RenderTextureFormat.ARGB32);
_reflectionRT.name = "PlanarReflectionRT";
}
// 清理资源
private void CleanUp()
{
if (_reflectionRT != null)
{
_reflectionRT.Release();
_reflectionRT = null;
}
if (_reflectionCamera != null)
{
DestroyImmediate(_reflectionCamera.gameObject);
_reflectionCamera = null;
}
}
}
七、Planar Reflections for Unity 插件
7.1 插件简介
Planar Reflections for Unity 是一个开源的Unity插件,提供了创建平面反射的完整解决方案。该插件支持Unity的内置渲染管线和通用渲染管线(URP)。
📦 插件信息:
- GitHub仓库:eldskald/planar-reflections-unity
- 支持管线:Built-in Pipeline、URP
- 开源协议:MIT License
- 核心功能:平面反射探针(Planar Reflections Probe)组件
7.2 安装配置
安装步骤:
- 从GitHub下载插件源代码
- 根据使用的渲染管线,将对应文件夹复制到项目中:
- Built-in Pipeline:复制
src/built-in文件夹 - URP:复制
src/urp文件夹
- Built-in Pipeline:复制
- 等待Unity编译完成
- 在GameObject上添加
Planar Reflections Probe组件
7.3 组件参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
| targetTextureID | int | 纹理槽位ID(0-3),支持多个反射面同时使用 |
| useCustomNormal | bool | 是否使用自定义法线方向 |
| customNormal | Vector3 | 自定义的法线方向(当useCustomNormal开启时) |
| reflectionsQuality | float | 反射纹理分辨率倍率(0.25, 0.5, 0.75, 1.0等) |
| farClipPlane | float | 反射相机的远裁剪面距离 |
| renderBackground | bool | 是否渲染背景(天空盒等) |
| renderInEditor | bool | 是否在Editor中实时渲染反射 |
7.4 使用方法
步骤1:配置反射探针
- 在场景中创建一个GameObject
- 添加
Planar Reflections Probe组件 - 将GameObject放置在反射平面上(如水面)
- 确保Transform的Forward向量(蓝色箭头)指向平面外
✅ 技巧:将探针放在水面上,这样当移动或旋转水面时,反射会自动保持正确。注意旋转方向,探针的蓝色箭头必须垂直于水面。
步骤2:编写支持反射的Shader
在Shader中包含插件提供的cginc文件,并使用 SampleReflections(screenUV) 函数采样反射:
// URP示例Shader(部分代码)
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
// 如果使用的是插件的URP版本,需要定义关键字
#pragma multi_compile _ _PRID_ONE _PRID_TWO _PRID_THREE _PRID_FOUR
// 在片元着色器中采样反射
float2 screenUV = i.screenPos.xy / i.screenPos.w;
half4 reflection = SampleReflections(screenUV);
可以参考插件Demo中的示例Shader:
- Built-in Pipeline:
demo/Assets/Shaders/Built-In/Water.shader - URP:
demo/Assets/Shaders/URP/PlanarGroundURP.shader
步骤3:多个反射面
如果场景中有多个反射面(如多个水面),可以创建多个探针,每个探针设置不同的 targetTextureID(0-3),Shader中使用对应的关键字(_PRID_ONE, _PRID_TWO等)。
⚠️ 性能警告:每个探针都会产生一次额外的相机渲染调用,多个反射面会成倍增加GPU开销。建议实际项目中仅对主要反射面做实时反射。
公共方法
插件提供了以下公共方法用于运行时控制:
| 方法 | 说明 |
|---|---|
IgnoreCamera(Camera cam) | 忽略指定相机的反射渲染 |
UnignoreCamera(Camera cam) | 取消忽略指定相机 |
IsIgnoring(Camera cam) | 检查是否忽略指定相机 |
ClearIgnoredList() | 清空忽略列表 |
八、性能优化建议
平面反射是一个性能开销较大的特效,以下是常用的优化策略:
- 降低RT分辨率
- 推荐使用512×512或256×256
- 仅在特写镜头时使用1024×1024
- 避免使用全屏分辨率
- Layer过滤
- 仅勾选需要产生倒影的物体层
- 排除粒子、UI、小道具等无关内容
- 排除反射面自身的层(避免无限递归)
- 关闭阴影
- 反射相机不需要渲染阴影
- 设置
renderShadows = false
- 距离剔除
- 设置较小的
farClipPlane - 远处物体不需要产生倒影
- 设置较小的
- 按需渲染
- 不需要每帧更新反射
- 可隔2-3帧更新一次RT
- 或仅在主摄像机移动时更新
- 降低渲染质量
- 临时降低LOD Bias
- 关闭雾效
- 使用更简单的Shader变体
💡 优化技巧:可以使用QualitySettings.lodBias和QualitySettings.maximumLODLevel在反射渲染时临时降低模型质量,反射渲染完成后再恢复,这样可以显著提升性能。
九、常见问题与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 倒影上下颠倒 | 反射平面法线方向错误 | 检查transform.up是否正确指向上方 |
| 倒影中出现反射面自身(无限递归) | 反射面被包含在反射层中 | 将反射面放到单独的Layer,在_reflectionLayers中排除该层 |
| 反射面边缘闪烁/锯齿 | 斜裁剪面和反射面完全重合导致浮点精度问题 | 调大_clipPlaneOffset(建议0.02~0.05) |
| 性能开销过高 | RT分辨率过高或未过滤反射层 | 降低RT分辨率、过滤反射层、关闭阴影、设置远裁剪面 |
| 反射纹理显示为黑色 | 反射相机未正确渲染或RT未传递 | 检查脚本是否正确挂载、URP回调是否注册成功 |
| 倒影位置偏移 | 反射矩阵计算错误 | 检查平面法线和位置的计算是否正确 |

338

被折叠的 条评论
为什么被折叠?



