效果概述
本文解析一个基于Unity URP(通用渲染管线)的平面反射系统。该系统通过动态创建反射相机、矩阵变换计算反射、斜裁剪矩阵优化等技术,在平面表面(如水面、镜面、地板)上生成高质量的实时反射效果,适用于水体渲染、镜子效果、光滑地面等场景。
在Built-in时代,平面反射几乎是唯一可选项,在URP下使用,也依然很稳定,效果也不错。
系统架构总览
主相机 → 反射相机创建 → 反射矩阵计算 → 斜裁剪矩阵优化 → 反射纹理渲染 → 全局纹理传递 → 着色器采样
↑ ↑ ↑ ↑ ↑ ↑
FollowCamera CreateMirrorObjects CalculateReflectionMatrix CalculateObliqueMatrix RenderSingleCamera _PlanarReflectionTexture
核心技术原理
1. 反射相机动态管理
private Camera CreateMirrorObjects() { var go = new GameObject("Planar Reflections", typeof(Camera)); var cameraData = go.AddComponent(typeof(UniversalAdditionalCameraData)) as UniversalAdditionalCameraData; cameraData.requiresColorOption = CameraOverrideOption.Off; cameraData.requiresDepthOption = CameraOverrideOption.Off; cameraData.SetRenderer(2); // 指定使用的Renderer索引 var reflectionCamera = go.GetComponent<Camera>(); reflectionCamera.enabled = false; // 手动控制渲染 return reflectionCamera; }
设计要点:
-
动态创建:按需创建反射相机,避免始终占用资源
-
手动控制:设置enabled=false,通过RenderSingleCamera手动触发渲染
-
Renderer指定:可指定特定的Renderer处理反射,与主相机分离
-
HideFlags设计(注释部分):可选择隐藏相机对象,避免场景污染
2. 反射矩阵计算
private static void CalculateReflectionMatrix(ref Matrix4x4 reflectionMat, Vector4 plane)
{
// 平面方程: plane = (normal.x, normal.y, normal.z, d)
// 其中 d = -Dot(normal, pointOnPlane)
reflectionMat.m00 = (1F - 2F * plane[0] * plane[0]);
reflectionMat.m01 = (-2F * plane[0] * plane[1]);
reflectionMat.m02 = (-2F * plane[0] * plane[2]);
reflectionMat.m03 = (-2F * plane[3] * plane[0]);
reflectionMat.m10 = (-2F * plane[1] * plane[0]);
reflectionMat.m11 = (1F - 2F * plane[1] * plane[1]);
reflectionMat.m12 = (-2F * plane[1] * plane[2]);
reflectionMat.m13 = (-2F * plane[3] * plane[1]);
reflectionMat.m20 = (-2F * plane[2] * plane[0]);
reflectionMat.m21 = (-2F * plane[2] * plane[1]);
reflectionMat.m22 = (1F - 2F * plane[2] * plane[2]);
reflectionMat.m23 = (-2F * plane[3] * plane[2]);
reflectionMat.m30 = 0F;
reflectionMat.m31 = 0F;
reflectionMat.m32 = 0F;
reflectionMat.m33 = 1F;
数学原理:反射矩阵是通过平面方程推导出的仿射变换矩阵,它将任意点映射到平面另一侧的对称点。矩阵公式:
R=I−2n⃗n⃗T
其中plane是归一化的平面法线,I是单位矩阵。
矩阵验证:
-
反射矩阵的两次应用等于恒等变换:R × R = I
-
反射是等距变换,保持距离和角度
-
行列式为-1,表示镜像变换
3. 反射相机位置计算
private void UpdateReflectionCamera(Camera realCamera) { // 计算反射平面 Vector3 pos = target.transform.position + Vector3.up * m_planeOffset; Vector3 normal = target.transform.up; var d = -Vector3.Dot(normal, pos) - m_settings.m_ClipPlaneOffset; var reflectionPlane = new Vector4(normal.x, normal.y, normal.z, d); // 构建反射矩阵 var reflection = Matrix4x4.identity; reflection *= Matrix4x4.Scale(new Vector3(1, -1, 1)); CalculateReflectionMatrix(ref reflection, reflectionPlane); // 计算反射后的相机位置和朝向 var oldPosition = realCamera.transform.position; var newPosition = ReflectPosition(oldPosition); _reflectionCamera.transform.forward = Vector3.Scale( realCamera.transform.forward, new Vector3(1, -1, 1)); // 组合视图矩阵 _reflectionCamera.worldToCameraMatrix = realCamera.worldToCameraMatrix * reflection; _reflectionCamera.transform.position = newPosition; }
变换步骤:
-
定义反射平面:基于目标物体的位置和法线
-
构建反射矩阵:通过平面方程计算反射变换
-
反射相机位置:将主相机位置映射到平面对称点
-
反射相机朝向:Y轴反向,保持左右方向一致
-
组合视图矩阵:主相机视图矩阵 × 反射矩阵
4. 斜裁剪矩阵优化
// 计算相机空间的裁剪平面 var clipPlane = CameraSpacePlane(_reflectionCamera, pos - Vector3.up * 0.1f, normal, 1.0f); // 使用斜裁剪矩阵 var projection = IsOrginNearClip ? realCamera.projectionMatrix : realCamera.CalculateObliqueMatrix(clipPlane); _reflectionCamera.projectionMatrix = projection;
斜裁剪原理:
-
标准投影矩阵的近平面垂直于相机视线
-
斜裁剪矩阵允许将近平面倾斜,使其与反射平面对齐
-
这样可以精确地裁剪掉反射平面另一侧的物体,避免不必要的渲染
优势:
-
精确裁剪:只渲染反射平面以上的物体
-
性能优化:减少overdraw,提高渲染效率
-
避免 artifacts:防止反射平面以下的物体出现在反射中
5. 分辨率管理
public enum ResolutionMulltiplier { Full, // 1.0x Half, // 0.5x Third, // 0.33x Quarter // 0.25x } private Vector2Int ReflectionResolution(Camera cam, float scale) { var x = (int)(cam.pixelWidth * scale * GetScaleValue()); var y = (int)(cam.pixelHeight * scale * GetScaleValue()); return new Vector2Int(x, y); }
分辨率策略:
-
主相机分辨率 × URP渲染缩放 × 分辨率系数
-
提供多种分辨率选项,平衡画质和性能
-
移动平台可使用Quarter,PC平台可使用Full/Half
6. 渲染管线集成
private void OnEnable() { RenderPipelineManager.beginCameraRendering += ExecutePlanarReflections; } private void ExecutePlanarReflections(ScriptableRenderContext context, Camera camera) { // 过滤条件 if (camera.cameraType == CameraType.Reflection || camera.cameraType == CameraType.Preview) return; // 创建反射相机和纹理 UpdateReflectionCamera(camera); PlanarReflectionTexture(camera); // 临时质量设置 var data = new PlanarReflectionSettingData(); data.Set(); // 触发回调并渲染 BeginPlanarReflections?.Invoke(context, _reflectionCamera); UniversalRenderPipeline.RenderSingleCamera(context, _reflectionCamera); // 恢复质量设置 data.Restore(); // 传递纹理到全局Shader Shader.SetGlobalTexture(_planarReflectionTextureId, _reflectionTexture); }
性能优化策略
1. 渲染质量临时调整
class PlanarReflectionSettingData { public void Set() { GL.invertCulling = true; // 反转背面剔除(因为反射) // 可选的LOD和雾效控制 } }
2. 纹理复用
private void PlanarReflectionTexture(Camera cam) { if (_reflectionTexture == null) { var res = ReflectionResolution(cam, UniversalRenderPipeline.asset.renderScale); _reflectionTexture = RenderTexture.GetTemporary(res.x, res.y, 32, hdrFormat); } _reflectionCamera.targetTexture = _reflectionTexture; }
3. 相机复用
-
反射相机在启用时创建,禁用时销毁
-
避免每帧创建新相机的开销
-
使用对象池模式管理相机资源
参数调节指南
| 参数 | 作用 | 推荐值 | 性能影响 |
|---|---|---|---|
| m_ResolutionMultiplier | 反射纹理分辨率 | Third/Half | 高(分辨率每增加一倍,性能消耗×4) |
| m_ClipPlaneOffset | 裁剪平面偏移 | 0.07f | 低 |
| m_ReflectLayers | 反射图层 | -1(所有图层) | 中(图层越多,渲染物体越多) |
| m_Shadows | 是否渲染阴影 | false | 高(阴影渲染开销大) |
| IsOrginNearClip | 是否使用原近平面 | false | 低 |
应用场景
1. 水面反射
// 配置水面反射 target = waterPlane; m_planeOffset = 0.0f; // 平面位于水面 m_settings.m_ReflectLayers = LayerMask.GetMask("Default", "Terrain"); m_settings.m_Shadows = true; // 水面需要阴影增强真实感
2. 镜子效果
// 镜子特殊配置 target = mirrorPlane; m_planeOffset = 0.0f; m_settings.m_ResolutionMultiplier = ResolutionMulltiplier.Full; // 镜子需要高清晰度 IsOrginNearClip = true; // 镜子不需要斜裁剪
3. 光滑地板
// 地板反射(模糊版本) m_settings.m_ResolutionMultiplier = ResolutionMulltiplier.Quarter; // 配合模糊Shader使用 Shader.SetGlobalFloat("_ReflectionBlur", 0.5f);
与其他反射技术对比
| 技术 | 真实感 | 性能 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|
| 平面反射(本文) | 高 | 中 | 中 | 水面、镜子、地板 |
| 反射探针 | 中 | 低 | 低 | 静态物体、室内 |
| 屏幕空间反射(SSR) | 中 | 中 | 高 | 屏幕可见区域 |
| 光线追踪 | 极高 | 极低 | 极高 | 高端PC、主机 |
常见问题与解决方案
1. 反射闪烁
// 问题:反射相机和主相机深度冲突 // 解决:设置反射相机深度为负值 _reflectionCamera.depth = -1;
2. 反射裁剪错误
// 问题:反射平面附近的物体被错误裁剪 // 解决:调整裁剪平面偏移 var clipPlane = CameraSpacePlane(_reflectionCamera, pos - Vector3.up * 0.1f, normal, 1.0f); // 偏移平面位置
3. 反射错位
// 问题:反射图像位置不正确 // 检查平面偏移量 Vector3 pos = target.transform.position + Vector3.up * m_planeOffset; // 确保m_planeOffset与目标物体的实际表面对齐
4. 性能问题
// 问题:反射渲染开销过大 // 解决方案:动态降级 if (isMobile) { m_settings.m_ResolutionMultiplier = ResolutionMulltiplier.Quarter; m_settings.m_Shadows = false; m_settings.m_ReflectLayers &= ~LayerMask.GetMask("SmallObjects"); }
扩展优化思路
1. 动态分辨率
// 根据性能动态调整分辨率 float performanceScore = CalculatePerformanceScore(); if (performanceScore < 0.3f) m_settings.m_ResolutionMultiplier = ResolutionMulltiplier.Quarter; else if (performanceScore < 0.6f) m_settings.m_ResolutionMultiplier = ResolutionMulltiplier.Third;
2. 反射模糊
// 在Shader中对反射纹理进行模糊处理 float4 reflection = SAMPLE_TEXTURE2D(_PlanarReflectionTexture, sampler_PointClamp, uv); reflection = GaussianBlur(reflection, uv, _BlurSize);
3. 反射强度控制
// 基于视角和距离的反射衰减 float fresnel = dot(normal, viewDir); float distanceFactor = saturate(1 - distance / _MaxReflectionDistance); reflectionStrength *= fresnel * distanceFactor;
4. 多平面反射支持
// 管理多个反射平面 List<PlanarReflectionPlane> reflectionPlanes = new List<PlanarReflectionPlane>(); foreach(var plane in reflectionPlanes) { RenderReflectionForPlane(plane); }
总结
这个平面反射系统通过精妙的矩阵变换和渲染管线集成,实现了高质量的实时反射效果。其核心优势在于:
-
数学精确性:严格的反射矩阵计算确保镜像准确性
-
性能优化:斜裁剪矩阵、分辨率控制、临时质量调整
-
URP集成:无缝接入URP渲染管线,遵循现代渲染架构
-
灵活配置:丰富的参数选项适应不同平台和场景需求
-
资源管理:动态创建和销毁相机,避免资源浪费
该系统特别适合需要高质量平面反射的项目,如水体渲染、镜子效果、光滑地面等,通过合理的参数配置,可以在不同性能平台上取得良好的平衡。




-DrawMe.png)


暂无评论内容