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

张开发
2026/4/21 6:23:40 15 分钟阅读
【Unity Shader URP】平面反射(Planar Reflection)实战教程
文章目录0. 效果预览1. 原理简述2. 功能点3. 完整 Shader可直接用4. 使用方法4.1 C# 反射脚本4.2 搭建步骤5. 参数说明Shader 参数C# 脚本参数6. 变体与扩展6.1 菲涅尔控制反射强度6.2 法线扰动倒影水面波纹6.3 模糊反射粗糙地面7. 常见问题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反射矩阵为// 反射矩阵将点沿平面镜像翻转 // M I - 2 * n * nTn 是平面法线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可直接用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。usingUnityEngine;usingUnityEngine.Rendering;usingUnityEngine.Rendering.Universal;/// summary/// 平面反射控制器/// 挂载到反射平面对象上如地面 Plane/// /summary[ExecuteAlways]publicclassPlanarReflection:MonoBehaviour{[Header( 反射设置 )][SerializeField]privateint_textureSize512;// RT 分辨率[SerializeField]privatefloat_clipPlaneOffset0.01f;// 裁剪面偏移防止闪烁[SerializeField]privateLayerMask_reflectionLayers-1;// 反射哪些层privateCamera_reflectionCamera;privateRenderTexture_reflectionRT;privateMaterial_material;// 缓存 Shader 属性 ID避免每帧字符串查找privatestaticreadonlyintReflectionTexIDShader.PropertyToID(_ReflectionTex);voidOnEnable(){// 获取反射平面的材质_materialGetComponentRenderer().sharedMaterial;CreateReflectionCamera();CreateRenderTexture();// 注册到 URP 渲染回调RenderPipelineManager.beginCameraRenderingOnBeginCameraRendering;}voidOnDisable(){RenderPipelineManager.beginCameraRendering-OnBeginCameraRendering;CleanUp();}privatevoidOnBeginCameraRendering(ScriptableRenderContextcontext,Cameracam){// 只处理主摄像机跳过反射摄像机自身和其他摄像机if(cam.cameraType!CameraType.Gamecam.cameraType!CameraType.SceneView)return;if(cam_reflectionCamera)return;RenderReflection(cam);}privatevoidRenderReflection(CameramainCamera){// 1) 计算反射平面 Vector3planePostransform.position;Vector3planeNormaltransform.up;// 2) 计算反射矩阵 floatd-Vector3.Dot(planeNormal,planePos)-_clipPlaneOffset;Vector4reflectionPlanenewVector4(planeNormal.x,planeNormal.y,planeNormal.z,d);Matrix4x4reflectionMatrixCalculateReflectionMatrix(reflectionPlane);// 3) 设置镜像摄像机 _reflectionCamera.cullingMask_reflectionLayers;_reflectionCamera.targetTexture_reflectionRT;// 镜像摄像机的世界到相机矩阵 主摄像机矩阵 × 反射矩阵_reflectionCamera.worldToCameraMatrixmainCamera.worldToCameraMatrix*reflectionMatrix;// 4) 斜裁剪面只渲染反射平面上方的物体 Vector4clipPlaneCameraSpacePlane(_reflectionCamera,planePos,planeNormal);_reflectionCamera.projectionMatrixmainCamera.CalculateObliqueMatrix(clipPlane);// 5) 翻转剔除方向反射后三角形绕序反转 GL.invertCullingtrue;// 6) 渲染URP 14 推荐的 RenderRequest API varrequestnewUniversalRenderPipeline.SingleCameraRequest();if(RenderPipeline.SupportsRenderRequest(_reflectionCamera,request))RenderPipeline.SubmitRenderRequest(_reflectionCamera,request);GL.invertCullingfalse;// 7) 传递 RT 给材质 _material.SetTexture(ReflectionTexID,_reflectionRT);}// 反射矩阵计算 privatestaticMatrix4x4CalculateReflectionMatrix(Vector4plane){Matrix4x4mMatrix4x4.identity;m.m001f-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.m111f-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.m221f-2f*plane.z*plane.z;m.m23-2f*plane.z*plane.w;m.m300;m.m310;m.m320;m.m331;returnm;}// 将世界空间平面转到摄像机空间 privateVector4CameraSpacePlane(Cameracam,Vector3pos,Vector3normal){Matrix4x4worldToCamcam.worldToCameraMatrix;Vector3camPosworldToCam.MultiplyPoint(pos);Vector3camNormalworldToCam.MultiplyVector(normal).normalized;returnnewVector4(camNormal.x,camNormal.y,camNormal.z,-Vector3.Dot(camPos,camNormal));}// 创建反射摄像机 privatevoidCreateReflectionCamera(){if(_reflectionCamera!null)return;GameObjectgonewGameObject(ReflectionCamera);go.hideFlagsHideFlags.HideAndDontSave;_reflectionCamerago.AddComponentCamera();_reflectionCamera.enabledfalse;// 手动渲染不自动渲染// URP 需要 UniversalAdditionalCameraDatavarcameraDatago.AddComponentUniversalAdditionalCameraData();cameraData.requiresColorOptionCameraOverrideOption.Off;cameraData.requiresDepthOptionCameraOverrideOption.Off;cameraData.renderShadowsfalse;}// 创建 RenderTexture privatevoidCreateRenderTexture(){if(_reflectionRT!null_reflectionRT.width_textureSize)return;if(_reflectionRT!null)_reflectionRT.Release();_reflectionRTnewRenderTexture(_textureSize,_textureSize,16,RenderTextureFormat.ARGB32);_reflectionRT.namePlanarReflectionRT;}privatevoidCleanUp(){if(_reflectionRT!null){_reflectionRT.Release();_reflectionRTnull;}if(_reflectionCamera!null){DestroyImmediate(_reflectionCamera.gameObject);_reflectionCameranull;}}}4.2 搭建步骤在Assets/Shaders/下新建PlanarReflection_URP.shader粘贴 Shader 代码。新建材质Shader 选择Custom/PlanarReflection_URP。创建一个 Plane或任意平面网格作为反射面赋上材质。将PlanarReflection.cs脚本挂到这个 Plane 上。Inspector 中配置Texture Size512性能优先或 1024画质优先Reflection Layers选择需要产生倒影的层排除反射面自身的层材质的Reflection Strength0.5 半透明倒影1.0 完全镜面在反射面上方放几个物体球体、方块、角色运行场景观察倒影效果。5. 参数说明Shader 参数参数类型范围/默认值说明_BaseMap2Dwhite反射面主贴图_BaseColorColor(1,1,1,1)主颜色叠乘_ReflectionTex2Dblack反射 RT脚本自动设置_ReflectionStrengthRange(0,1)0.5反射强度0无反射1完全镜面_ReflectionTintColor(1,1,1,1)反射颜色叠乘给倒影加色调C# 脚本参数参数类型默认值说明_textureSizeint512RT 分辨率越大越清晰越耗性能_clipPlaneOffsetfloat0.01裁剪面偏移防止反射面边缘闪烁_reflectionLayersLayerMaskEverything哪些层的物体产生倒影6. 变体与扩展6.1 菲涅尔控制反射强度真实世界中正面看反射弱、掠射角看反射强。用菲涅尔项调制反射强度// 在 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让倒影产生水面波纹效果// 采样法线贴图 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 做模糊采样模拟粗糙表面的模糊倒影// 简单的 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或者只在摄像机移动时更新。

更多文章