注:本文章适用于URP初学者且有一定Unity基础,不适用于纯新手!
参考视频来源:
[Unity6][URP17] Renderer Feature 入门指南
一个项目只有一个Ultra_PipelineAsset,可以在此Asset的List中有多个Ultra_PipelineAsset_ForwardRenderer。
见下图:(一般在Setting文件夹中)
这些date可以应用于不同的相机,当我们不想用某个相机渲染某个Layer时,在date的opaque mask layer中不勾选即可
在一个date下可以通过Add Renderer Feature添加多个Scriptable Renderer Feature
一些内置的Scriptable Renderer Feature:
-
Screen Space Shadows为用屏幕空间计算代替多级阴影贴图(根据光源距离生成多级阴影贴图(如CSM))的方案
-
Screen Space Shadows 本质是屏幕空间阴影贴图,它是通过屏幕空间数据(深度/法线)实时计算的
-
Decal贴花系统:类似FPS等出现在物体上的枪口贴图,它不属于材质自身,而是额外的一张贴图
补充:
-
环境光遮蔽(Ambient Occlusion)“AO”为Ambient Occlusion的缩写,中文译为环境光遮蔽。在DirectX 10.1 API推出后,Amblent Occlusion升级为SSAO(Screen-Space Ambient Occlusion:屏幕空间环境光遮蔽);而在微软推出DirectX 11 API后,SSAO升级至HDAO(高解析度环境光遮蔽:High Definition Ambient Occlusion)。
-
AO是来描绘物体和物体相交或靠近的时候遮挡周围漫反射光线的效果
-
AO特效在直观上给我们玩家的感受主要体现在画面的明暗度上,未开启AO特效的画面光照稍亮一些;而开启AO特效之后,局部的细节画面尤其是暗部阴影会更加明显一些。
-
前向渲染(Forward Rendering)和延迟渲染(Deferred Rendering)是实时图形渲染中两种主流的管线技术,核心区别在于光照计算的时机和数据处理方式。
-
以下是两者的详细对比:
1. 核心原理与流程
前向渲染(Forward Rendering)
-
原理:逐物体渲染,光照计算与几何渲染同步进行。
-
每个物体依次经过顶点着色器→光栅化→片元着色器(直接计算光照)→输出到帧缓冲区。
-
光照计算在片元着色器中完成,每个光源对每个物体独立计算,复杂度为 O(物体数×光源数)。
-
-
示例: 场景有100个物体和10个光源,前向渲染可能需执行 100×10=1000次光照计算(实际通过优化减少,但原理如此)。
延迟渲染(Deferred Rendering)
-
原理:分两阶段处理,解耦几何与光照计算。
-
几何阶段:将所有物体的位置、法线、材质等属性写入G-Buffer(几何缓冲区)。
-
光照阶段:基于G-Buffer数据,逐像素统一计算光照,复杂度仅与光源数相关(O(光源数))。
-
-
示例: 同样100个物体和10个光源,延迟渲染只需 10次全局光照计算。
2. 性能与适用场景对比
特性 | 前向渲染 | 延迟渲染 |
---|---|---|
多光源性能 | 差(光照计算随物体数量线性增长) | 极佳(光照计算与物体数量无关) |
显存占用 | 低(仅需帧缓冲区) | 高(需存储多张G-Buffer纹理) |
透明物体支持 | 完美(天然支持混合) | 困难(需额外前向渲染通道) |
抗锯齿支持 | 易(MSAA直接支持) | 难(依赖TAA/FXAA等后处理) |
3. 优缺点深度分析
前向渲染的局限性
-
过度着色(Over-Shading):被遮挡的片元仍会计算光照,浪费性能。
-
光源数量限制:Unity默认仅4个逐像素光源,超出后需降级为逐顶点或球谐光照(SH),牺牲质量。
延迟渲染的挑战
-
G-Buffer开销:需存储位置、法线、材质等多张纹理,显存压力大。
-
屏幕空间限制:无法处理屏幕外光源或遮挡物(如透明物体需混合时)。
4. 混合渲染方案
现代引擎(如Unreal、Unity URP)常结合两者优势:
-
Forward+:前向渲染基础上分块(Tile)管理光源,减少计算量。(Unity已提供)
-
Deferred + Forward:延迟渲染处理不透明物体,前向渲染补全透明物体。
5. 技术演进与未来
-
光线追踪:延迟渲染更易与光追结合(如RTX的BVH加速)。
-
移动端优化:通过精简G-Buffer(如压缩法线)或混合渲染适配移动设备。
总结:前向渲染简单灵活,适合低复杂度场景;延迟渲染以显存和硬件为代价,换取多光源的高效处理。实际项目中需根据目标平台、场景复杂度及视觉效果需求权衡选择。
延迟渲染对于透明物体的渲染存在明显困难,是因为G-Buffer设计为单片段存储,无法支持多片段混合(半透明需前向补偿),对于透明物体,只能存储其此点对应深度最前方不透明的物体片元数据,不能通过其他物体计算透明物体的片元数据以及额外存储这个透明物体在此点的片元数据。
进入主题:
RenderGraph API可以自动去管理资源的生命周期,但需要主动明确声明读取或写入它的资源
RenderGraph API 是一种用于现代渲染管线的任务调度架构,通过有向无环图(DAG) 管理渲染任务依赖关系、资源生命周期和并行执行,显著提升复杂渲染系统的可维护性与扩展性。
RenderGraph:自动管理 GPU 资源(如纹理、缓冲区)的生命周期:
Setup
阶段声明资源需求 → Compile
阶段计算资源创建/回收时机 → Execute
阶段自动注入资源分配指令
特性 | Unity 生命周期 | RenderGraph |
---|---|---|
核心目标 | 管理脚本与游戏对象状态 | 解耦渲染 Pass、优化 GPU 资源调度 |
优化方向 | 脚本执行顺序与对象状态同步 | 减少资源冗余、提升多线程渲染效率 |
关键挑战 | 跨脚本依赖、协程安全 | 多 Pass 依赖、跨平台资源复用 |
红色为写入,绿色为读取,行为pass,列为资源
在使用 RenderGraph API 编写渲染通道之前,您需要了解以下基本原则:(来自Unity官方文档)
-
不再直接处理资源,而是使用特定于渲染图系统的句柄。所有 RenderGraph API 都使用这些句柄来作资源。渲染图管理的资源类型包括 RTHandles、ComputeBuffers 和 RendererLists。
-
实际资源引用只能在渲染通道的执行代码中访问。
-
该框架需要显式声明渲染通道。每个渲染通道都必须说明它从哪些资源读取和/或写入哪些资源。
-
渲染图的每次执行之间没有持久性。这意味着你在渲染图的一次执行中创建的资源不能延续到下一次执行。
-
对于需要持久性的资源(例如,从一帧到另一帧),您可以在渲染图之外创建它们,例如常规资源,并将它们导入。在依赖项跟踪方面,它们的行为与任何其他渲染图资源类似,但图不处理它们的生存期。
-
渲染图主要用于纹理资源。这对如何编写着色器代码以及如何设置它们有很多影响。
关于Feature脚本框架:
引入命名空间:
继承基类:ScriptableRendererFeature;声明要使用的通道,通道名字,通道渲染时机
初始化方法(只在渲染管线初始化时调用):
每帧设置渲染通道的方法:
将渲染通道加入队列的方法(每帧调用):
资源清理方法(在Feature被禁用或销毁时调用):
关于Pass脚本框架:
引入命名空间:
继承:ScriptableRenderPass基类,表示这是一个自定义渲染通道,声明材质用于后续应用
创建内部类用于向渲染管线中传递数据:
初始化:
-
创建性能分析器:
ProfilingSampler
用于在 Frame Debugger 中标识该通道 -
设置渲染事件位置:决定在渲染流程的哪个阶段执行(如
AfterRenderingPostProcessing
) -
创建后处理材质:通过 Shader Graph 创建的着色器初始化材质
Setup 方法:
-
每帧渲染前,通过
CustomFeature.SetupRenderPasses()
调用 -
作用:
-
准备帧特定的渲染数据
-
可在此处更新材质参数(如基于游戏状态设置
material.SetFloat
)
-
资源清理:
-
安全销毁材质资源
-
区分运行模式/编辑器模式使用不同的销毁方式
-
防止材质内存泄漏
核心方法 RecordRenderGraph()
:
前面的大部分都是框架模版,我们自由度最高的地方在于builder的渲染执行的匿名函数里
ctx.cmd.DrawRendererList()
是 Unity URP 渲染管线中的一个核心渲染命令,用于高效地绘制一组预筛选的渲染对象(Renderer)。
其中,RendererListHandle的实例创建并不简单,下图为步骤:(部分内容为可复用的,部分需要手动创建)
-
CullingResults,剔除结果,可以从UniversalRenderingData中直接获取
-
SortingCriteria,渲染排序顺序,也可以直接拿来用,UniversalCameraData
写法1:
(注释为作者学习时标注,并不为代码说明,请忽略注释)
写法2:
除了在编辑器阶段手动找到Ultra_PipelineAsset_ForwardRenderer并添加Custom Feature外,还可以通过脚本实现在游戏中动态切换和使用它ctx.cmd.DrawRendererList()
类似于事件,我们需要先new一个pass,执行Init初始化,后订阅它,订阅者可为RenderPiplineManager的不同时机(在渲染的不同阶段插入这个pass)
函数内容为:初始化渲染管道,将通道加入渲染队列。这样,就可以实现在游戏中调用不同的后处理效果
### 写法3:
用volume组件
没有的话,先new一个,我们需要使用一个命名空间和一个特性用于在组件的Add Override中找到需要挂载和使用的脚本
并且其变量的声明也有些不同,具体如上图
找到pass并在初始化渲染管道函数中添加
这个管理类是用于查找场景中挂载Volume组件的指定脚本的,在验证其找到并初始化后,应用这个数据,否则,用Init的数据
注:此方法需要在Ultra_PipelineAsset_ForwardRenderer添加Custom Feature,否则Feature脚本不会执行,Pass脚本自然也不会执行
最后一个Demo
模拟实现像FPS游戏中开镜的效果,开镜时,用一个单独的相机渲染镜头并且加上准星(此处有些为了醋包了盘饺子,实际不会这么复杂)
准备:
- 一个静态图片Texture2D:准星图片(用于叠加准星)
- 一个Custom的Render Texture:输出为摄像机Output的Output Texture(用于叠加画面)
- 一个Custom的Render Texture:输出关闭深度缓冲区,仅保留颜色缓冲区(下文解释),用于叠加前两者
- 创建一个材质球:BaseMap用上述叠加过的RenderTexture
看上述准备相比应该知道要做什么了,获取准星图片Texture2D,获取摄像机作为OutPut的输出纹理Custom Render Texture,利用Shader(看下文此处为AddToTogether)将二者Add,得到全新的Custom Render Texture(叠加后),作为材质球的BaseMap,从而实现FPS开镜效果
创建data内部类并声明要使用的数据类型,三个TextureHandle(顺序分别为准星,摄像机,目标纹理的句柄)(前文好像有提到过,我们无法或者说URP不让直接对Texture进行处理,只能给予它纹理让它自己处理)
接下来初始化,定义通道并给予名字,选择后处理时机,为要进行后处理的材质赋值shader处理方式其中,后三行做特殊说明,我们想用Texture和RenderTexture获取句柄TextureHandle,但我们根据下图可知,需要先获取RTHandle,才能进一步获取TextureHandle,因此我们用RTHandles提供的api申请内存获取三者对应的RTHandle,留个坑,对于摄像机输出作为Texture的纹理做特殊处理
因为RTHandle既可以表示深度缓存区,也可以表示颜色缓冲区,对于静态图片,不包含深度信息,因此,即使包装成RTHandle,依旧只有颜色缓冲区,而对于叠加的最终输出纹理,前文提到已经将其深度缓冲区设为None,因此也不需要额外处理,所以,唯一需要额外处理的,即为以摄像机输出作为纹理的renderTexture1需要做特殊处理:
因为它是动态的,所以我们把他的处理逻辑放在SetUp中每帧执行,先创建一个宽高一致的空纹理,并且利用GPU提供的api直接将颜色数据拷贝到这个空纹理,再用RTHandles包装成RTHandle
不要忘记清理申请的内存防止泄露
在进行完一切前置任务后,开始写逻辑执行,对于材质,直接赋值,对于RTHanle,利用renderGraoh提供的api转为TextureHandle添加入类实例,声明要读取的两个纹理句柄以及一个又读又写的纹理句柄
最后,先把shader中的Texture Access赋值,最后通过Blit输出到屏幕