【从UnityURP开始探索游戏渲染】专栏-直达
SRP提供的核心功能与架构
可编程管线基础
RenderPipeline基类
- 通过继承该类并重写
Render()
方法,开发者可自定义渲染流程调度逻辑,替代传统固定管线。(URP中UniversalRenderPipeline : RenderPipeline
继承并重写Render方法。)
ScriptableRenderContext
- 作为C#脚本与底层图形API的桥梁,允许通过代码调度渲染命令(如剔除、绘制)。(
public struct ScriptableRenderContext : IEquatable<ScriptableRenderContext>
定义自定义渲染管道 使用的状态 和绘图命令。)
管线资源分离机制
RenderPipelineAsset
:存储配置数据(如材质、Shader参数)(URP中是public partial class UniversalRenderPipelineAsset : RenderPipelineAsset, ISerializationCallbackReceiver
)RenderPipelineInstance
:执行实际渲染逻辑的实例类。(在URP中,上面的RenderPipelineAsset资产类中重写protected override RenderPipeline CreatePipeline()
方法,在其中创建渲染管线实例var pipeline = new UniversalRenderPipeline(this);
)
关键扩展点
事件回调
- 通过
RenderPipelineManager
订阅渲染生命周期事件(如beginContextRendering
),在特定阶段注入自定义逻辑。
动态渲染策略
- 支持运行时切换渲染路径(如正向/延迟渲染),适应不同硬件性能需求(这里的渲染路径是URP或HDRP自己实现的,例如Forward+也是,所以这个渲染路径只是实现管线时的自定义渲染策略,所以运行时能切换。但是一般不建议切换,因为各种shader实现时都会根据渲染路径实现相应执行的Pass,如果随便切换,会导致部分shader可能因为只适配某种渲染路径,对其他渲染路径显示渲染异常。)。
URP在SRP上的具体实现
资源与实例初始化
URP管线资源 (UniversalRenderPipelineAsset
):
- 定义默认Shader、光照模型、后处理栈等参数。
实例化流程:
- 资源创建时调用
CreatePipeline()
生成UniversalRenderPipeline
实例,接管Unity渲染循环。 - 其中的渲染器基类
ScriptableRenderer
作为 渲染器可以用于所有摄像机,也可以在每个摄像机的基础上重写。它将实现光剔除和设置,并描述要在帧中执行的ScriptableRenderPass
列表。渲染器可以通过额外的scriptablerendererfeature
进行扩展,以支持更多的效果。渲染器的资源在ScriptableRendererData
中序列化(编辑器中就在这个资源上挂载设置RendererFeature)。-
UniversalRenderer
默认的3D渲染器继承自ScriptableRenderer
。在其构造函数中public UniversalRenderer(UniversalRendererData data) : base(data)
根据上述序列化的Data数据,创建默认的渲染Pass逻辑。渲染路径就是在这个类文件中一同定义的,在构造函数中根据不同路径,给出不同策略执行Pass。 -
定义渲染路径
csharpnamespace UnityEngine.Rendering.Universal { /// <summary> /// Rendering modes for Universal renderer. /// </summary> public enum RenderingMode { /// <summary>Render all objects and lighting in one pass, with a hard limit on the number of lights that can be applied on an object.</summary> Forward = 0, /// <summary>Render all objects and lighting in one pass using a clustered data structure to access lighting data.</summary> [InspectorName("Forward+")] ForwardPlus = 2, /// <summary>Render all objects first in a g-buffer pass, then apply all lighting in a separate pass using deferred shading.</summary> Deferred = 1 }; // 省略下面代码。。。 }
-
构造函数根据渲染路径给出不同Pass执行策略
csharp/// <summary> /// Constructor for the Universal Renderer. /// </summary> /// <param name="data">The settings to create the renderer with.</param> public UniversalRenderer(UniversalRendererData data) : base(data) { // Query and cache runtime platform info first before setting up URP. PlatformAutoDetect.Initialize(); ...
-
设置各种材质和状态
csharp// 设置各种材质和状态 #if ENABLE_VR && ENABLE_XR_MODULE Experimental.Rendering.XRSystem.Initialize(XRPassUniversal.Create, data.xrSystemData.shaders.xrOcclusionMeshPS, data.xrSystemData.shaders.xrMirrorViewPS); #endif m_BlitMaterial = CoreUtils.CreateEngineMaterial(data.shaders.coreBlitPS); m_BlitHDRMaterial = CoreUtils.CreateEngineMaterial(data.shaders.blitHDROverlay); m_CopyDepthMaterial = CoreUtils.CreateEngineMaterial(data.shaders.copyDepthPS); m_SamplingMaterial = CoreUtils.CreateEngineMaterial(data.shaders.samplingPS); m_StencilDeferredMaterial = CoreUtils.CreateEngineMaterial(data.shaders.stencilDeferredPS); m_CameraMotionVecMaterial = CoreUtils.CreateEngineMaterial(data.shaders.cameraMotionVector); m_ObjectMotionVecMaterial = CoreUtils.CreateEngineMaterial(data.shaders.objectMotionVector); StencilStateData stencilData = data.defaultStencilState; m_DefaultStencilState = StencilState.defaultValue; m_DefaultStencilState.enabled = stencilData.overrideStencilState; m_DefaultStencilState.SetCompareFunction(stencilData.stencilCompareFunction); m_DefaultStencilState.SetPassOperation(stencilData.passOperation); m_DefaultStencilState.SetFailOperation(stencilData.failOperation); m_DefaultStencilState.SetZFailOperation(stencilData.zFailOperation); m_IntermediateTextureMode = data.intermediateTextureMode; if (UniversalRenderPipeline.asset?.supportsLightCookies ?? false) { var settings = LightCookieManager.Settings.Create(); var asset = UniversalRenderPipeline.asset; if (asset) { settings.atlas.format = asset.additionalLightsCookieFormat; settings.atlas.resolution = asset.additionalLightsCookieResolution; } m_LightCookieManager = new LightCookieManager(ref settings); } this.stripShadowsOffVariants = true; this.stripAdditionalLightOffVariants = true; #if ENABLE_VR && ENABLE_VR_MODULE #if PLATFORM_WINRT || PLATFORM_ANDROID // AdditionalLightOff variant is available on HL&Quest platform due to performance consideration. this.stripAdditionalLightOffVariants = !PlatformAutoDetect.isXRMobile; #endif #endif
-
Forward和Forward+灯光准备,深度预处理、深度拷贝模式等设置。
csharpForwardLights.InitParams forwardInitParams; forwardInitParams.lightCookieManager = m_LightCookieManager; forwardInitParams.forwardPlus = data.renderingMode == RenderingMode.ForwardPlus; m_Clustering = data.renderingMode == RenderingMode.ForwardPlus; m_ForwardLights = new ForwardLights(forwardInitParams); //m_DeferredLights.LightCulling = data.lightCulling; this.m_RenderingMode = data.renderingMode; this.m_DepthPrimingMode = data.depthPrimingMode; this.m_CopyDepthMode = data.copyDepthMode; #if UNITY_ANDROID || UNITY_IOS || UNITY_TVOS this.m_DepthPrimingRecommended = false; #else this.m_DepthPrimingRecommended = true; #endif
-
关键来了!URP定制的流程在这里(从灯光阴影投射、深度和深度法线预渲染、到深度拷贝、延迟渲染中的特别执行的LightMode:"UniversalForwardOnly"、再到延迟渲染的GBuffer及其对GBuffer的屏幕空间的光照处理、不透明阶段Pass、深度拷贝、运动向量Pass、天空盒Pass、透明物体Pass、离屏UIPass、覆盖UIPass、最后的混合、深度拷贝、输出到缓冲区"_CameraColorAttachment")
csharp// Note: Since all custom render passes inject first and we have stable sort, // we inject the builtin passes in the before events. m_MainLightShadowCasterPass = new MainLightShadowCasterPass(RenderPassEvent.BeforeRenderingShadows); m_AdditionalLightsShadowCasterPass = new AdditionalLightsShadowCasterPass(RenderPassEvent.BeforeRenderingShadows); #if ENABLE_VR && ENABLE_XR_MODULE m_XROcclusionMeshPass = new XROcclusionMeshPass(RenderPassEvent.BeforeRenderingOpaques); // Schedule XR copydepth right after m_FinalBlitPass m_XRCopyDepthPass = new CopyDepthPass(RenderPassEvent.AfterRendering + k_AfterFinalBlitPassQueueOffset, m_CopyDepthMaterial); #endif m_DepthPrepass = new DepthOnlyPass(RenderPassEvent.BeforeRenderingPrePasses, RenderQueueRange.opaque, data.opaqueLayerMask); m_DepthNormalPrepass = new DepthNormalOnlyPass(RenderPassEvent.BeforeRenderingPrePasses, RenderQueueRange.opaque, data.opaqueLayerMask); if (renderingModeRequested == RenderingMode.Forward || renderingModeRequested == RenderingMode.ForwardPlus) { m_PrimedDepthCopyPass = new CopyDepthPass(RenderPassEvent.AfterRenderingPrePasses, m_CopyDepthMaterial, true); } if (this.renderingModeRequested == RenderingMode.Deferred) { var deferredInitParams = new DeferredLights.InitParams(); deferredInitParams.stencilDeferredMaterial = m_StencilDeferredMaterial; deferredInitParams.lightCookieManager = m_LightCookieManager; m_DeferredLights = new DeferredLights(deferredInitParams, useRenderPassEnabled); m_DeferredLights.AccurateGbufferNormals = data.accurateGbufferNormals; m_GBufferPass = new GBufferPass(RenderPassEvent.BeforeRenderingGbuffer, RenderQueueRange.opaque, data.opaqueLayerMask, m_DefaultStencilState, stencilData.stencilReference, m_DeferredLights); // Forward-only pass only runs if deferred renderer is enabled. // It allows specific materials to be rendered in a forward-like pass. // We render both gbuffer pass and forward-only pass before the deferred lighting pass so we can minimize copies of depth buffer and // benefits from some depth rejection. // - If a material can be rendered either forward or deferred, then it should declare a UniversalForward and a UniversalGBuffer pass. // - If a material cannot be lit in deferred (unlit, bakedLit, special material such as hair, skin shader), then it should declare UniversalForwardOnly pass // - Legacy materials have unamed pass, which is implicitely renamed as SRPDefaultUnlit. In that case, they are considered forward-only too. // TO declare a material with unnamed pass and UniversalForward/UniversalForwardOnly pass is an ERROR, as the material will be rendered twice. StencilState forwardOnlyStencilState = DeferredLights.OverwriteStencil(m_DefaultStencilState, (int)StencilUsage.MaterialMask); ShaderTagId[] forwardOnlyShaderTagIds = new ShaderTagId[] { new ShaderTagId("UniversalForwardOnly"), new ShaderTagId("SRPDefaultUnlit"), // Legacy shaders (do not have a gbuffer pass) are considered forward-only for backward compatibility new ShaderTagId("LightweightForward") // Legacy shaders (do not have a gbuffer pass) are considered forward-only for backward compatibility }; int forwardOnlyStencilRef = stencilData.stencilReference | (int)StencilUsage.MaterialUnlit; m_GBufferCopyDepthPass = new CopyDepthPass(RenderPassEvent.BeforeRenderingGbuffer + 1, m_CopyDepthMaterial, true); m_DeferredPass = new DeferredPass(RenderPassEvent.BeforeRenderingDeferredLights, m_DeferredLights); m_RenderOpaqueForwardOnlyPass = new DrawObjectsPass("Render Opaques Forward Only", forwardOnlyShaderTagIds, true, RenderPassEvent.BeforeRenderingOpaques, RenderQueueRange.opaque, data.opaqueLayerMask, forwardOnlyStencilState, forwardOnlyStencilRef); } // Always create this pass even in deferred because we use it for wireframe rendering in the Editor or offscreen depth texture rendering. m_RenderOpaqueForwardPass = new DrawObjectsPass(URPProfileId.DrawOpaqueObjects, true, RenderPassEvent.BeforeRenderingOpaques, RenderQueueRange.opaque, data.opaqueLayerMask, m_DefaultStencilState, stencilData.stencilReference); m_RenderOpaqueForwardWithRenderingLayersPass = new DrawObjectsWithRenderingLayersPass(URPProfileId.DrawOpaqueObjects, true, RenderPassEvent.BeforeRenderingOpaques, RenderQueueRange.opaque, data.opaqueLayerMask, m_DefaultStencilState, stencilData.stencilReference); bool copyDepthAfterTransparents = m_CopyDepthMode == CopyDepthMode.AfterTransparents; RenderPassEvent copyDepthEvent = copyDepthAfterTransparents ? RenderPassEvent.AfterRenderingTransparents : RenderPassEvent.AfterRenderingSkybox; m_CopyDepthPass = new CopyDepthPass( copyDepthEvent, m_CopyDepthMaterial, shouldClear: true, copyResolvedDepth: RenderingUtils.MultisampleDepthResolveSupported() && SystemInfo.supportsMultisampleAutoResolve && copyDepthAfterTransparents); // Motion vectors depend on the (copy) depth texture. Depth is reprojected to calculate motion vectors. m_MotionVectorPass = new MotionVectorRenderPass(copyDepthEvent + 1, m_CameraMotionVecMaterial, m_ObjectMotionVecMaterial, data.opaqueLayerMask); m_DrawSkyboxPass = new DrawSkyboxPass(RenderPassEvent.BeforeRenderingSkybox); m_CopyColorPass = new CopyColorPass(RenderPassEvent.AfterRenderingSkybox, m_SamplingMaterial, m_BlitMaterial); #if ADAPTIVE_PERFORMANCE_2_1_0_OR_NEWER if (needTransparencyPass) #endif { m_TransparentSettingsPass = new TransparentSettingsPass(RenderPassEvent.BeforeRenderingTransparents, data.shadowTransparentReceive); m_RenderTransparentForwardPass = new DrawObjectsPass(URPProfileId.DrawTransparentObjects, false, RenderPassEvent.BeforeRenderingTransparents, RenderQueueRange.transparent, data.transparentLayerMask, m_DefaultStencilState, stencilData.stencilReference); } m_OnRenderObjectCallbackPass = new InvokeOnRenderObjectCallbackPass(RenderPassEvent.BeforeRenderingPostProcessing); m_DrawOffscreenUIPass = new DrawScreenSpaceUIPass(RenderPassEvent.BeforeRenderingPostProcessing, true); m_DrawOverlayUIPass = new DrawScreenSpaceUIPass(RenderPassEvent.AfterRendering + k_AfterFinalBlitPassQueueOffset, false); // after m_FinalBlitPass { var postProcessParams = PostProcessParams.Create(); postProcessParams.blitMaterial = m_BlitMaterial; postProcessParams.requestHDRFormat = GraphicsFormat.B10G11R11_UFloatPack32; var asset = UniversalRenderPipeline.asset; if (asset) postProcessParams.requestHDRFormat = UniversalRenderPipeline.MakeRenderTextureGraphicsFormat(asset.supportsHDR, asset.hdrColorBufferPrecision, false); m_PostProcessPasses = new PostProcessPasses(data.postProcessData, ref postProcessParams); } m_CapturePass = new CapturePass(RenderPassEvent.AfterRendering); m_FinalBlitPass = new FinalBlitPass(RenderPassEvent.AfterRendering + k_FinalBlitPassQueueOffset, m_BlitMaterial, m_BlitHDRMaterial); #if UNITY_EDITOR m_FinalDepthCopyPass = new CopyDepthPass(RenderPassEvent.AfterRendering + 9, m_CopyDepthMaterial); #endif // RenderTexture format depends on camera and pipeline (HDR, non HDR, etc) // Samples (MSAA) depend on camera and pipeline m_ColorBufferSystem = new RenderTargetBufferSystem("_CameraColorAttachment");
-
最后最一些兼容操作,结束构造。完成URP基本管线的主体流程。
csharpsupportedRenderingFeatures = new RenderingFeatures(); if (this.renderingModeRequested == RenderingMode.Deferred) { // Deferred rendering does not support MSAA. this.supportedRenderingFeatures.msaa = false; // Avoid legacy platforms: use vulkan instead. unsupportedGraphicsDeviceTypes = new GraphicsDeviceType[] { GraphicsDeviceType.OpenGLCore, GraphicsDeviceType.OpenGLES2, GraphicsDeviceType.OpenGLES3 }; } LensFlareCommonSRP.mergeNeeded = 0; LensFlareCommonSRP.maxLensFlareWithOcclusionTemporalSample = 1; LensFlareCommonSRP.Initialize(); m_VulkanEnablePreTransform = GraphicsSettings.HasShaderDefine(BuiltinShaderDefine.UNITY_PRETRANSFORM_TO_DISPLAY_ORIENTATION); }
-
-
核心渲染流程分解
URP将渲染分为五个阶段,均在Render()
方法中调度:
阶段 | URP实现细节 |
---|---|
准备阶段 | 收集场景渲染对象与光源数据,配置相机参数与目标纹理。 |
几何阶段 | 执行视锥剔除,生成GPU顶点数据;通过ScriptableRenderContext.DrawRenderers 提交绘制命令。 |
光照阶段 | 采用简化PBR模型:计算实时光源贡献,支持烘焙光照混合;动态光源采用Tile-Based优化策略(Forward+路径时)。 |
光栅化阶段 | 执行深度预通道(Depth Prepass)减少过度绘制,结合GPU Instancing优化批次处理。 |
后处理阶段 | 在独立Pass中应用抗锯齿(FXAA/TAA)、Bloom等效果,支持自定义RendererFeature扩展。 |
性能优化关键技术
- SRP Batcher:对相同Shader变体但不同材质的物体进行动态合批,显著降低SetPass Call。
- 光照剔除优化 :按层级控制剔除距离(
layerCullDistances
),对静态物体预计算遮挡数据。
URP对SRP的扩展与简化
- 标准化功能封装
- 内置轻量级PBR光照模型,取代HDRP的复杂物理模拟以提升跨平台性能。
- 集成Shader Graph可视化工具链,降低着色器开发门槛。(后期版本内置管线也可用ShaderGraph了)
- 跨平台适配策略
- 动态切换渲染精度(如移动端禁用实时阴影),通过
QualitySettings
分级配置。 - 资源包精简:剔除HDRP的高精度贴图与计算密集型特效,缩小运行时内存占用。
- 动态切换渲染精度(如移动端禁用实时阴影),通过
对URP的扩展
- URP基本管线流程在
UniversalRenderer
的构造函数中已经定义完整。并且在其中每个阶段都给出了插入点。那么只需要在这些插入点用创建RendererFeature的方式插入自定义的Pass来影响和扩充基本的URP管线。 - 还有一种方式用
RenderPipelineManager
提供的点位插入自定义Pass。
【从UnityURP开始探索游戏渲染】专栏-直达
(欢迎点赞留言探讨,更多人加入进来能更加完善这个探索的过程,🙏)