【Unity Shader URP】平面反射(Planar Reflection)实战教程

文章目录

    • [0. 效果预览](#0. 效果预览)
    • [1. 原理简述](#1. 原理简述)
    • [2. 功能点](#2. 功能点)
    • [3. 完整 Shader(可直接用)](#3. 完整 Shader(可直接用))
    • [4. 使用方法](#4. 使用方法)
      • [4.1 C# 反射脚本](# 反射脚本)
      • [4.2 搭建步骤](#4.2 搭建步骤)
    • [5. 参数说明](#5. 参数说明)
      • [Shader 参数](#Shader 参数)
      • [C# 脚本参数](# 脚本参数)
    • [6. 变体与扩展](#6. 变体与扩展)
      • [6.1 菲涅尔控制反射强度](#6.1 菲涅尔控制反射强度)
      • [6.2 法线扰动倒影(水面波纹)](#6.2 法线扰动倒影(水面波纹))
      • [6.3 模糊反射(粗糙地面)](#6.3 模糊反射(粗糙地面))
    • [7. 常见问题](#7. 常见问题)
    • [8. 性能建议](#8. 性能建议)

0. 效果预览

平面反射(Planar Reflection)是让地面、水面、镜面产生实时倒影的经典技术:用一个镜像摄像机从反射角度渲染场景,把结果投影到反射平面上。效果真实、原理直观,是中级 Shader 的标志性练习。


1. 原理简述

平面反射的本质:在反射平面下方放一个"镜像摄像机",沿平面法线翻转主摄像机的位置和朝向,渲染到 RenderTexture,再把这张 RT 投影到反射平面的 UV 上。

三步流程:

复制代码
1. 计算反射矩阵:沿反射平面翻转主摄像机 → 得到镜像摄像机的 View 矩阵
2. 镜像摄像机渲染到 RenderTexture(用斜裁剪面避免渲染平面以下的物体)
3. 反射平面的 Shader 用屏幕空间 UV 采样这张 RT → 显示倒影

反射矩阵的数学:

给定反射平面方程 ax + by + cz + d = 0(法线 (a,b,c),距离 d),反射矩阵为:

hlsl 复制代码
// 反射矩阵:将点沿平面镜像翻转
// M = I - 2 * n * nT(n 是平面法线,nT 是转置)
float4 plane = float4(normal.x, normal.y, normal.z, -dot(normal, pointOnPlane));
Matrix4x4 reflectionMatrix;
reflectionMatrix.m00 = 1 - 2 * plane.x * plane.x;
reflectionMatrix.m01 =   - 2 * plane.x * plane.y;
reflectionMatrix.m02 =   - 2 * plane.x * plane.z;
reflectionMatrix.m03 =   - 2 * plane.x * plane.w;
// ... 其余行类似

斜裁剪面(Oblique Clip Plane):

镜像摄像机如果用普通近裁面,会渲染到反射平面"下方"的物体(实际在平面上方),产生穿帮。解决方案是把近裁面设为反射平面本身------只渲染平面"上方"的内容。Unity 提供 Camera.CalculateObliqueMatrix() 直接算。


2. 功能点

  • 实时平面反射:镜像摄像机渲染场景倒影到 RenderTexture
  • 反射矩阵计算:C# 脚本自动计算反射平面的镜像变换
  • 斜裁剪面:避免渲染反射平面以下的物体,消除穿帮
  • 屏幕空间 UV 投影:Shader 用屏幕坐标采样 RT,倒影自动对齐
  • 反射强度可调_ReflectionStrength 控制倒影和基础色的混合
  • RT 分辨率可调:在性能和画质之间取舍
  • 主贴图支持:保留地面/水面原始纹理

3. 完整 Shader(可直接用)

hlsl 复制代码
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;  // 屏幕空间坐标(用于采样反射 RT)
                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);
                // 计算屏幕空间坐标(透视除法在 frag 中做)
                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) 采样反射 RenderTexture
                // 用屏幕空间 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
        }
    }
}

4. 使用方法

4.1 C# 反射脚本

这个脚本是平面反射的核心------它创建镜像摄像机、计算反射矩阵、渲染到 RT、传给 Shader。

csharp 复制代码
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

/// <summary>
/// 平面反射控制器
/// 挂载到反射平面对象上(如地面 Plane)
/// </summary>
[ExecuteAlways]
public class PlanarReflection : MonoBehaviour
{
    [Header("===== 反射设置 =====")]
    [SerializeField] private int _textureSize = 512;          // RT 分辨率
    [SerializeField] private float _clipPlaneOffset = 0.01f;  // 裁剪面偏移(防止闪烁)
    [SerializeField] private LayerMask _reflectionLayers = -1; // 反射哪些层

    private Camera _reflectionCamera;
    private RenderTexture _reflectionRT;
    private Material _material;
    // 缓存 Shader 属性 ID,避免每帧字符串查找
    private static readonly int ReflectionTexID = Shader.PropertyToID("_ReflectionTex");

    void OnEnable()
    {
        // 获取反射平面的材质
        _material = GetComponent<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)
    {
        // ===== 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+ 推荐的 RenderRequest API) =====
        var request = new UniversalRenderPipeline.SingleCameraRequest();
        if (RenderPipeline.SupportsRenderRequest(_reflectionCamera, request))
            RenderPipeline.SubmitRenderRequest(_reflectionCamera, request);

        GL.invertCulling = false;

        // ===== 7) 传递 RT 给材质 =====
        _material.SetTexture(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 = false;
    }

    // ===== 创建 RenderTexture =====
    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;
        }
    }
}

4.2 搭建步骤

  1. Assets/Shaders/ 下新建 PlanarReflection_URP.shader,粘贴 Shader 代码。

  2. 新建材质,Shader 选择 Custom/PlanarReflection_URP

  3. 创建一个 Plane(或任意平面网格)作为反射面,赋上材质。

  4. PlanarReflection.cs 脚本挂到这个 Plane 上。

  5. Inspector 中配置:

    • Texture Size:512(性能优先)或 1024(画质优先)
    • Reflection Layers:选择需要产生倒影的层(排除反射面自身的层)
    • 材质的 Reflection Strength:0.5 = 半透明倒影,1.0 = 完全镜面
  6. 在反射面上方放几个物体(球体、方块、角色),运行场景观察倒影效果。


5. 参数说明

Shader 参数

参数 类型 范围/默认值 说明
_BaseMap 2D white 反射面主贴图
_BaseColor Color (1,1,1,1) 主颜色叠乘
_ReflectionTex 2D black 反射 RT(脚本自动设置)
_ReflectionStrength Range(0,1) 0.5 反射强度:0=无反射,1=完全镜面
_ReflectionTint Color (1,1,1,1) 反射颜色叠乘(给倒影加色调)

C# 脚本参数

参数 类型 默认值 说明
_textureSize int 512 RT 分辨率(越大越清晰,越耗性能)
_clipPlaneOffset float 0.01 裁剪面偏移(防止反射面边缘闪烁)
_reflectionLayers LayerMask Everything 哪些层的物体产生倒影

6. 变体与扩展

6.1 菲涅尔控制反射强度

真实世界中,正面看反射弱、掠射角看反射强。用菲涅尔项调制反射强度:

hlsl 复制代码
// 在 Varyings 中加 normalWS 和 viewDirWS
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);

6.2 法线扰动倒影(水面波纹)

用法线贴图偏移反射 UV,让倒影产生水面波纹效果:

hlsl 复制代码
// 采样法线贴图
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);

配合 _Time 滚动法线贴图 UV,就能做出动态水面倒影。

6.3 模糊反射(粗糙地面)

对反射 RT 做模糊采样,模拟粗糙表面的模糊倒影:

hlsl 复制代码
// 简单的 3x3 均值模糊
float2 texelSize = 1.0 / float2(_textureSize, _textureSize);
half3 blur = half3(0,0,0);
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# 端对 RT 做高斯模糊后处理,再传给 Shader。


7. 常见问题

Q: 倒影上下颠倒 / 位置偏移?

A: 检查反射平面的 transform.up 是否正确指向上方。反射矩阵依赖平面法线方向,如果平面旋转了,法线方向也要跟着变。脚本中用 transform.up 自动获取。

Q: 倒影中出现反射面自身(无限递归)?

A: 把反射面放到单独的 Layer,在 _reflectionLayers 中排除这个 Layer。否则镜像摄像机会渲染反射面本身,产生递归反射。

Q: 反射面边缘有闪烁/锯齿?

A: 调大 _clipPlaneOffset(如 0.02~0.05)。斜裁剪面和反射面完全重合时,浮点精度问题会导致边缘像素闪烁。偏移一小段距离可以消除。

Q: 性能开销大吗?

A: 平面反射 = 额外渲染一次场景,相当于多一个摄像机。开销取决于 RT 分辨率和场景复杂度。512×512 的 RT 在大多数场景下可以接受,1024 以上需要注意帧率。

Q: 多个反射面怎么办?

A: 每个反射面需要独立的镜像摄像机和 RT。多个反射面 = 多次额外渲染,性能成倍增加。实际项目中通常只对一个主要反射面(如地面/水面)做实时反射,其他用 Cubemap 或 SSR 替代。


8. 性能建议

  • RT 分辨率是关键:512×512 是性价比最高的选择,1024 用于特写镜头。不要用全屏分辨率,没必要。
  • Layer 过滤_reflectionLayers 只勾选需要产生倒影的物体层,排除粒子、UI、小道具等不重要的层,大幅减少镜像摄像机的渲染量。
  • 关闭阴影 :镜像摄像机的 renderShadows = false,倒影中不需要阴影,省一大块开销。
  • 距离剔除 :给镜像摄像机设较小的 farClipPlane,远处物体不需要产生倒影。
  • 按需渲染:不需要每帧都更新反射。可以隔帧渲染(每 2-3 帧更新一次 RT),或者只在摄像机移动时更新。
相关推荐
汽车芯猿2 小时前
压扁的图像:嵌入式设备中的长方形像素之谜
嵌入式硬件·ui·photoshop
风酥糖2 小时前
Godot游戏练习01-第30节-教程结束我继续
游戏·游戏引擎·godot
Heikepengmu3 小时前
用Unity打造愤怒的小鸟游戏
游戏·unity·游戏引擎
雪儿waii12 小时前
Unity 中的 Resources 详解
unity·游戏引擎
nashane12 小时前
HarmonyOS 6学习:解决异步场景下Toast提示框无法弹出的UI上下文丢失问题
学习·ui·harmonyos·harmony app
悟空爬虫-彪哥1 天前
2026 Python UI 框架选择指南:从 Streamlit 到 Pyside6 的四层体系
开发语言·python·ui
RReality1 天前
【Unity UGUI】Toggle / ToggleGroup 与 Dropdown
ui·unity·游戏引擎·图形渲染·材质
ai_coder_ai1 天前
自动化脚本ui编程之线性布局(linear)
ui·autojs·自动化脚本·冰狐智能辅助·easyclick
雪儿waii1 天前
Unity 中的 InvokeRepeating 详解
unity·游戏引擎