1. 全屏特效的底层逻辑与方案选型
1.1 为什么全屏特效值得单独拎出来讲
在Unity的URP管线里做全屏特效,很多人第一反应是去用Render Feature挂一个Pass,或者干脆用内置的OnRenderImage。但真到项目里你会发现,URP下的全屏特效写法跟内置管线完全是两码事,尤其是涉及到深度重建、法线重建、自定义Blit这些操作时,坑一个接一个。我见过不少项目,特效师想要一个全屏的扭曲、热浪、扫描线效果,结果程序折腾半天发现深度图采样不对,或者世界空间位置重建出来是错的,最后只能退而求其次用屏幕空间近似,效果大打折扣。
这个项目的核心就是解决这个问题:在URP下绘制一个覆盖全屏的四边面,然后基于深度图重建每个像素的世界空间位置。听起来简单,但里面涉及到URP的渲染流程、Blit的正确姿势、深度图的非线性反解、以及不同平台下的坐标差异。我前后在三个项目里反复踩过这些坑,今天把完整的思路和实操细节整理出来,不管你是刚接触URP的新手,还是已经写过几个Render Feature的老手,应该都能从中找到一些之前忽略的细节。
1.2 全屏四边面 vs 全屏三角形:选哪个
先说一个最基础的选型问题:全屏特效的几何体到底用什么。常见的有两种方案,一种是画一个覆盖全屏的四边形(Quad),另一种是画一个全屏三角形(Fullscreen Triangle)。很多人可能觉得四边形更直观,四个顶点对应屏幕四个角,UV也好算。但实际上,在全屏特效这个场景下,全屏三角形才是更优的选择。
原因有三点。第一,全屏三角形只需要三个顶点,比四边形少一个顶点,虽然这点开销在现代GPU上几乎可以忽略,但在移动端或者需要大量叠加全屏特效的场景下,能省一点是一点。第二,全屏三角形可以避免四边形对角线接缝处可能出现的插值问题,尤其是在做屏幕空间UV插值时,三角形的插值更均匀。第三,也是最重要的一点,全屏三角形可以天然避免在屏幕边缘出现半个像素的偏移,因为它的顶点坐标可以直接用裁剪空间坐标构造,不需要经过模型矩阵变换。
不过这个项目标题里明确说了是“绘制全屏四边面”,所以我这里还是以四边形为主来讲,但会在关键步骤里说明三角形方案的区别和切换方法。实际项目中,如果你追求极致的性能和稳定性,我建议直接用全屏三角形,代码更简洁,坑也更少。
1.3 URP下全屏特效的几种实现路径对比
在URP里做全屏特效,目前主流的有三条路。第一条是用ScriptableRendererFeature加ScriptableRenderPass,这是URP官方推荐的扩展方式,灵活度最高,可以精确控制渲染时机和渲染目标。第二条是用Renderer Feature里的Blit,配合一个自定义的Shader,这种方式适合简单的后处理效果,代码量少,但控制粒度粗。第三条是直接挂一个Camera上的组件,在OnRenderImage或者RenderPipelineManager的回调里手动Blit,这种方式在URP下其实不太推荐,因为会打乱URP的渲染顺序,容易出问题。
我这个项目用的是第一条路,也就是自定义Render Feature加Render Pass。为什么选这条?因为全屏特效往往需要访问相机的深度图、法线图、颜色图,还要控制渲染目标的切换和Blit的时机。用Render Feature可以精确地插入到URP的渲染流程中,比如在渲染不透明物体之后、渲染透明物体之前,或者在所有渲染完成之后做后处理。这种控制力是其他方式给不了的。
下面这张表是我对三种方式的对比总结,你可以根据自己的项目需求来选。
| 实现方式 | 灵活度 | 代码量 | 适用场景 | 主要坑点 |
|---|---|---|---|---|
| ScriptableRendererFeature | 高 | 中 | 需要精确控制渲染时机、访问多张RT | 需要理解URP的RenderPass生命周期 |
| Renderer Feature Blit | 低 | 少 | 简单后处理、单张RT输入输出 | 无法灵活控制渲染时机 |
| Camera组件手动Blit | 中 | 中 | 快速原型验证 | 容易打乱URP渲染顺序,不推荐生产环境 |
2. 核心细节解析与实操要点
2.1 全屏四边面的顶点构造与UV映射
全屏四边面的构造方式有很多种,最常见的是在Shader里直接用顶点着色器输出裁剪空间坐标。具体来说,我们可以定义一个包含四个顶点的Mesh,每个顶点的位置是(-1, -1)、(1, -1)、(-1, 1)、(1, 1),对应的UV是(0, 0)、(1, 0)、(0, 1)、(1, 1)。然后在顶点着色器里直接把位置当作裁剪空间坐标输出,不需要经过任何矩阵变换。
这里有一个关键细节:在URP下,裁剪空间的Y轴方向在不同平台上可能不一致。DirectX和Metal的Y轴方向是相反的,OpenGL又是另一套。如果你直接用(-1, -1)到(1, 1)的坐标,在某些平台上可能会出现上下颠倒的问题。解决办法是在Shader里用UNITY_UV_STARTS_AT_TOP宏来判断,或者在C#侧根据SystemInfo.graphicsUVStartsAtTop来调整UV。
我个人的习惯是在Shader里统一处理,顶点着色器里这样写:
struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; }; Varyings FullscreenVert(Attributes input) { Varyings output; output.positionCS = float4(input.positionOS.xy, 0.0, 1.0); output.uv = input.uv; #if UNITY_UV_STARTS_AT_TOP if (_ProjectionParams.x < 0) output.uv.y = 1.0 - output.uv.y; #endif return output; }这段代码里,_ProjectionParams.x是URP内置的一个参数,当渲染目标需要翻转时它会变成负值。通过这个判断,可以保证UV在不同平台上都是正确的。
2.2 深度图采样与非线性反解
重建世界空间位置的核心是深度图。URP下获取深度图有两种方式,一种是通过_CameraDepthTexture,另一种是通过_CameraDepthAttachment。前者是经过处理的深度图,后者是原始的深度附件。对于重建世界空间位置来说,用_CameraDepthTexture就够了,但要注意它的采样方式。
深度图里存储的深度值是非线性的,尤其是在透视投影下,近处的精度高,远处的精度低。如果你直接拿这个值当线性深度用,重建出来的世界空间位置会严重失真。所以第一步是要把非线性深度转成线性深度。URP提供了一个内置函数LinearEyeDepth,可以直接用:
float rawDepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, uv); float linearDepth = LinearEyeDepth(rawDepth, _ZBufferParams);这里的_ZBufferParams是URP自动填充的,包含了远近裁剪面的信息。LinearEyeDepth返回的是相机空间下的线性深度,也就是像素到相机的距离。
拿到线性深度之后,下一步是重建世界空间位置。最直接的方法是用相机的逆视图投影矩阵,把裁剪空间坐标转回世界空间。具体来说,我们需要构造一个裁剪空间坐标,其中xy是NDC坐标,z是深度值,w是1,然后乘以unity_MatrixInvVP。但这里有个细节:NDC坐标的z值在Different平台上范围不同,DirectX是[0, 1],OpenGL是[-1, 1]。所以更稳妥的做法是用线性深度和相机方向来重建。
我常用的方法是这样的:先根据UV和相机的视锥体参数,计算出相机空间下的射线方向,然后乘以线性深度,得到相机空间位置,最后用相机的世界矩阵转成世界空间。代码大概长这样:
float3 ReconstructWorldPos(float2 uv, float linearDepth) { float2 ndc = uv * 2.0 - 1.0; #if UNITY_UV_STARTS_AT_TOP ndc.y = -ndc.y; #endif float4 clipPos = float4(ndc, 1.0, 1.0); float4 viewPos = mul(unity_MatrixInvP, clipPos); viewPos /= viewPos.w; viewPos.xyz *= linearDepth; float3 worldPos = mul(unity_MatrixInvV, viewPos).xyz; return worldPos; }这段代码里,unity_MatrixInvP是逆投影矩阵,unity_MatrixInvV是逆视图矩阵。先通过逆投影把NDC转成相机空间方向,然后乘以线性深度得到相机空间位置,最后通过逆视图转成世界空间。这个方法比直接用逆VP矩阵更直观,也更容易调试。
2.3 渲染目标的绑定与Blit的正确姿势
在URP的Render Pass里做全屏特效,绕不开的一个操作是Blit。Blit的本质是把一个渲染目标的内容拷贝到另一个渲染目标,同时可以应用一个材质来做后处理。但URP下的Blit有几个坑,我一个个说。
第一个坑是渲染目标的绑定。在Execute方法里,你不能直接调用cmd.Blit就完事了,因为URP的渲染目标管理是有一套自己的机制的。正确的做法是用cmd.SetRenderTarget来绑定目标,然后用cmd.DrawMesh或者cmd.DrawProcedural来绘制全屏四边面。如果你直接用cmd.Blit,在某些URP版本下会出现渲染目标不匹配的问题,导致画面全黑或者花屏。
第二个坑是Blit的源和目标不能是同一张RT。如果你想把特效叠加到原图上,需要先拷贝一份原图,或者用两个RT来回倒。我一般的做法是在Render Pass里申请两张临时RT,一张用来存原图,一张用来存处理后的结果,最后再把结果Blit回相机的颜色目标。
第三个坑是MSAA。如果相机开启了MSAA,深度图和颜色图的采样数会不一致,直接采样深度图可能会得到错误的结果。解决办法是在Render Pass的配置里把MSAA关掉,或者用cmd.ResolveAntiAliasedSurface来解析。
下面是我常用的一个Render Pass的Execute方法的骨架:
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { CommandBuffer cmd = CommandBufferPool.Get("FullscreenEffect"); var camera = renderingData.cameraData.camera; var descriptor = renderingData.cameraData.cameraTargetDescriptor; descriptor.msaaSamples = 1; descriptor.depthBufferBits = 0; int tempRT1 = Shader.PropertyToID("_TempRT1"); int tempRT2 = Shader.PropertyToID("_TempRT2"); cmd.GetTemporaryRT(tempRT1, descriptor, FilterMode.Bilinear); cmd.GetTemporaryRT(tempRT2, descriptor, FilterMode.Bilinear); cmd.Blit(BuiltinRenderTextureType.CameraTarget, tempRT1); cmd.SetGlobalTexture("_MainTex", tempRT1); cmd.SetRenderTarget(tempRT2); cmd.DrawMesh(RenderingUtils.fullscreenMesh, Matrix4x4.identity, m_Material, 0, 0); cmd.Blit(tempRT2, BuiltinRenderTextureType.CameraTarget); cmd.ReleaseTemporaryRT(tempRT1); cmd.ReleaseTemporaryRT(tempRT2); context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); }这段代码里,RenderingUtils.fullscreenMesh是URP内置的一个全屏四边形Mesh,可以直接用。如果你想要全屏三角形,可以自己构造一个Mesh,或者用cmd.DrawProcedural来绘制。
3. 实操过程与核心环节实现
3.1 创建Render Feature与Render Pass
第一步是创建一个继承自ScriptableRendererFeature的类,然后在里面定义一个继承自ScriptableRenderPass的内部类。Render Feature的作用是告诉URP在什么时机插入这个Pass,Render Pass则是实际执行渲染逻辑的地方。
public class FullscreenEffectFeature : ScriptableRendererFeature { [System.Serializable] public class Settings { public Material material; public RenderPassEvent renderPassEvent = RenderPassEvent.AfterRenderingTransparents; } public Settings settings = new Settings(); private FullscreenEffectPass m_Pass; public override void Create() { m_Pass = new FullscreenEffectPass(settings); m_Pass.renderPassEvent = settings.renderPassEvent; } public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { if (settings.material == null) return; renderer.EnqueuePass(m_Pass); } }这里的关键是renderPassEvent的选择。如果你做的是后处理类的全屏特效,比如调色、模糊、扭曲,一般选AfterRenderingTransparents或者AfterRenderingPostProcessing。如果你需要在渲染不透明物体之后、透明物体之前做特效,那就选AfterRenderingOpaques。这个时机决定了你的特效会影响到哪些物体。
3.2 深度图的获取与配置
在URP下获取深度图,需要在Pipeline Asset里把Depth Texture选项打开。具体位置在URP Asset的Inspector里,找到Depth Texture勾选上。如果不勾选,_CameraDepthTexture就是一张空图,采样出来全是1。
另外,如果你需要相机法线图,还要勾选Opaque Texture,不过法线图在URP下需要通过额外的Pass来生成,稍微麻烦一点。对于重建世界空间位置来说,深度图就够了,不需要法线图。
在Render Pass的Configure方法里,需要告诉URP这个Pass需要深度图:
public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor) { ConfigureInput(ScriptableRenderPassInput.Depth); }这行代码的作用是让URP在渲染这个Pass之前,确保深度图已经生成好了。如果不加这行,深度图可能是上一帧的,或者根本没生成。
3.3 Shader的完整实现与参数说明
Shader部分是这个项目的核心。我把它分成几个关键部分来讲:顶点着色器、深度采样、世界空间重建、特效应用。
先看完整的Shader代码:
Shader "Custom/FullscreenWorldPosEffect" { Properties { _MainTex ("Main Texture", 2D) = "white" {} _EffectStrength ("Effect Strength", Range(0, 1)) = 0.5 _EffectColor ("Effect Color", Color) = (1, 0, 0, 1) } SubShader { Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" } Pass { ZTest Always ZWrite Off Cull Off HLSLPROGRAM #pragma vertex Vert #pragma fragment Frag #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl" TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex); float _EffectStrength; float4 _EffectColor; struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; }; Varyings Vert(Attributes input) { Varyings output; output.positionCS = float4(input.positionOS.xy, 0.0, 1.0); output.uv = input.uv; return output; } float3 ReconstructWorldPos(float2 uv, float linearDepth) { float2 ndc = uv * 2.0 - 1.0; #if UNITY_UV_STARTS_AT_TOP ndc.y = -ndc.y; #endif float4 clipPos = float4(ndc, 1.0, 1.0); float4 viewPos = mul(unity_MatrixInvP, clipPos); viewPos /= viewPos.w; viewPos.xyz *= linearDepth; float3 worldPos = mul(unity_MatrixInvV, viewPos).xyz; return worldPos; } half4 Frag(Varyings input) : SV_Target { float2 uv = input.uv; float rawDepth = SampleSceneDepth(uv); float linearDepth = LinearEyeDepth(rawDepth, _ZBufferParams); float3 worldPos = ReconstructWorldPos(uv, linearDepth); float3 color = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv).rgb; float effect = saturate(worldPos.y * _EffectStrength); color = lerp(color, _EffectColor.rgb, effect); return half4(color, 1.0); } ENDHLSL } } }这段Shader里,SampleSceneDepth是URP提供的深度采样函数,它会自动处理平台差异。LinearEyeDepth把非线性深度转成线性深度。ReconstructWorldPos函数里,先通过逆投影矩阵把NDC转成相机空间方向,然后乘以线性深度,最后通过逆视图矩阵转成世界空间。
这里有一个细节需要注意:unity_MatrixInvP和unity_MatrixInvV是URP自动填充的矩阵,但它们的精度在不同平台上可能不同。如果你发现重建出来的世界空间位置有抖动,可以尝试用unity_MatrixInvVP直接一步到位,或者用UNITY_MATRIX_I_VP宏。
3.4 参数计算与性能优化
全屏特效的性能开销主要来自两个方面:一是全屏四边面的绘制本身,二是深度图的采样和世界空间重建的计算。对于第一个方面,全屏四边面的绘制开销几乎可以忽略,因为只有两个三角形。对于第二个方面,深度图采样和矩阵乘法是主要的开销来源。
如果你在移动端做这个特效,建议把世界空间重建的计算放在顶点着色器里,而不是片元着色器里。因为顶点数量只有四个,计算量可以忽略,而片元数量是屏幕分辨率,计算量很大。具体做法是把UV和深度采样放在顶点着色器里,计算出世界空间位置后传给片元着色器插值。但这样做的缺点是深度图的采样精度会降低,因为顶点插值无法捕捉深度的非线性变化。所以这个方法只适合对精度要求不高的场景。
另一个优化点是降低深度图的分辨率。如果你的特效不需要像素级的精度,可以把深度图的分辨率降到一半或者四分之一,这样可以大幅减少采样开销。在URP里可以通过ConfigureInput和RenderTextureDescriptor来控制深度图的分辨率。
4. 常见问题与排查技巧实录
4.1 深度图采样全黑或全白
这是最常见的问题,通常有三个原因。第一个原因是URP Asset里没有勾选Depth Texture,导致_CameraDepthTexture是空图。解决办法是在URP Asset的Inspector里找到Depth Texture选项并勾选。第二个原因是Render Pass的Configure方法里没有调用ConfigureInput(ScriptableRenderPassInput.Depth),导致深度图没有生成。第三个原因是Shader里采样深度图的UV坐标不对,尤其是在UNITY_UV_STARTS_AT_TOP的情况下,UV的Y轴需要翻转。
排查方法很简单:在片元着色器里直接把深度值输出成颜色,看看是不是全黑或者全白。如果全黑,说明深度值是0,可能是深度图没生成。如果全白,说明深度值是1,可能是采样到了远裁剪面。
4.2 世界空间位置重建错误
世界空间位置重建错误的表现是特效的位置和预期不符,比如特效应该出现在地面上,结果出现在了天花板上。这个问题通常是因为NDC坐标的Y轴方向不对,或者逆投影矩阵用错了。
排查方法是把重建出来的世界空间位置直接输出成颜色,比如用worldPos.x输出红色,worldPos.y输出绿色,worldPos.z输出蓝色。然后移动相机,观察颜色的变化是否符合预期。如果Y轴方向反了,说明NDC的Y轴需要翻转。如果整体偏移,说明逆投影矩阵或者逆视图矩阵用错了。
还有一个容易忽略的点是相机的近裁剪面和远裁剪面。如果近裁剪面设置得太小,比如0.01,深度图的精度会集中在近处,远处的深度值会非常接近1,导致重建出来的世界空间位置在远处出现明显的阶梯状。解决办法是适当增大近裁剪面,比如0.1或者0.3,根据你的场景尺度来定。
4.3 特效在Scene视图正常但Game视图异常
这个问题通常是因为Scene视图和Game视图的相机参数不同,比如Scene视图的相机是正交投影,而Game视图是透视投影。如果你的Shader里假设了透视投影,在Scene视图下就会出错。解决办法是在Shader里判断相机的投影类型,或者直接在Render Feature里过滤掉Scene视图的相机。
过滤的方法是在AddRenderPasses里判断renderingData.cameraData.cameraType,如果是CameraType.SceneView或者CameraType.Preview,就直接返回,不添加这个Pass。
4.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 深度图全黑 | URP Asset未开启Depth Texture | 检查URP Asset的Depth Texture选项 | 勾选Depth Texture |
| 深度图全白 | 深度图未生成或采样UV错误 | 输出深度值到颜色,观察数值 | 调用ConfigureInput,检查UV翻转 |
| 世界空间位置偏移 | 逆矩阵用错或NDC坐标错误 | 输出worldPos到颜色,移动相机观察 | 检查unity_MatrixInvP和unity_MatrixInvV |
| 远处出现阶梯状 | 近裁剪面太小,深度精度不足 | 观察远处深度值的变化 | 增大近裁剪面 |
| Scene视图正常Game视图异常 | 相机投影类型不同 | 检查相机投影类型 | 过滤Scene视图相机 |
| 特效上下颠倒 | UV的Y轴方向不对 | 检查UNITY_UV_STARTS_AT_TOP | 翻转UV的Y轴 |
4.5 独家避坑技巧
第一个技巧是关于深度图的精度。如果你发现重建出来的世界空间位置在远处抖动很厉害,可以尝试用_CameraDepthAttachment代替_CameraDepthTexture。前者是原始的深度附件,精度更高,但需要自己处理平台差异。具体做法是在Shader里用SAMPLE_DEPTH_TEXTURE而不是SampleSceneDepth,然后手动调用LinearEyeDepth。
第二个技巧是关于矩阵的精度。在移动端,unity_MatrixInvP和unity_MatrixInvV的精度可能不够,导致重建出来的世界空间位置有误差。解决办法是在C#侧手动计算逆矩阵,然后用cmd.SetGlobalMatrix传给Shader。虽然多了一步,但精度更可控。
第三个技巧是关于性能。如果你的特效只需要在特定区域生效,比如只在水面上或者只在某个物体周围,可以在Shader里加一个距离判断,超出范围的像素直接返回原色,这样可以省掉大量的计算。具体做法是在重建世界空间位置之后,计算像素到特效中心的距离,如果超过阈值就clip掉或者直接返回原色。
第四个技巧是关于调试。在开发阶段,建议在Shader里加一个调试模式,可以把深度值、世界空间位置、UV坐标等中间结果直接输出成颜色。这样排查问题的时候会快很多。具体做法是用#pragma multi_compile定义一个_DEBUG_DEPTH、_DEBUG_WORLDPOS等关键字,然后在片元着色器里根据关键字输出不同的调试信息。
5. 扩展应用与进阶方向
5.1 基于世界空间位置的特效案例
重建出世界空间位置之后,能做的事情就很多了。比如你可以做一个基于高度的雾效,根据像素的世界空间Y坐标来决定雾的浓度。或者做一个基于距离的扫描线效果,根据像素到某个中心点的距离来决定扫描线的位置。再或者做一个基于世界空间位置的扭曲效果,根据像素的世界空间坐标来偏移UV,产生类似热浪的扭曲。
我做过一个比较有意思的效果是地面上的圆形波纹。具体做法是在世界空间里定义一个圆心和半径,然后根据像素到圆心的距离来计算波纹的相位,最后用正弦函数来偏移UV或者改变颜色。这个效果在场景里看起来就像地面上有一圈圈的水波在扩散,很适合做技能指示器或者环境氛围。
5.2 多Pass叠加与性能考量
如果你需要在一个场景里叠加多个全屏特效,比如先做扭曲再做调色再做模糊,这时候就要考虑性能了。每多一个全屏Pass,就多一次全屏的绘制和采样,开销是线性增长的。在移动端,建议把多个特效合并到一个Shader里,用多个#pragma multi_compile来控制开关,而不是用多个Pass。
合并的方法是在一个Shader里写多个特效函数,然后在片元着色器里依次调用。比如先调用扭曲函数,再调用调色函数,最后调用模糊函数。这样只需要一次全屏绘制,性能会好很多。但缺点是Shader的复杂度会上升,编译时间会变长,调试也会麻烦一些。
5.3 与Volume系统的结合
URP的Volume系统是管理后处理参数的好工具。你可以把全屏特效的参数暴露到Volume组件里,这样美术就可以在场景里直接调参数,不需要改代码。具体做法是定义一个继承自VolumeComponent的类,然后把参数用[Tooltip]和[Range]标注,最后在Render Feature里通过VolumeManager.instance.stack来获取参数。
这样做的好处是参数管理更规范,不同场景可以有不同的特效参数,而且支持平滑过渡。缺点是需要额外写一些样板代码,而且Volume系统的参数获取有一定的性能开销,不过对于全屏特效来说可以忽略。
5.4 在VR项目中的注意事项
如果你在做VR项目,全屏特效会有一些额外的坑。首先是单眼渲染的问题,VR下左右眼是分开渲染的,如果你的全屏特效没有考虑单眼渲染,可能会出现左右眼不一致的问题。解决办法是在Shader里用unity_StereoEyeIndex来判断当前渲染的是哪只眼,然后分别处理。
其次是性能问题,VR对帧率的要求很高,全屏特效的开销需要严格控制。建议在VR项目里把全屏特效的分辨率降到最低,或者只在必要的时候开启。另外,VR下的深度图重建也需要特别注意,因为VR的投影矩阵和普通相机不同,直接用unity_MatrixInvP可能会出错。建议用UNITY_MATRIX_I_VP宏,它会自动处理VR下的矩阵差异。
我在实际项目里踩过最深的坑是VR下的UV翻转问题。因为VR下左右眼的渲染目标可能是上下排列的,UV的Y轴需要根据眼索引来调整。当时调了半天才发现是这个问题,后来在Shader里加了一个判断就解决了。所以如果你做VR项目,一定要在早期就把单眼渲染的问题处理好,不然后面改起来很麻烦。