ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Unity渲染流水线深度解析:从原理到URP迁移与性能优化实战

Unity渲染流水线深度解析:从原理到URP迁移与性能优化实战 1. 渲染流水线到底在解决什么问题很多人第一次接触Unity的渲染流水线是在面试里被问到“描述一下从模型到屏幕像素的过程”然后背了一堆名词顶点着色器、光栅化、片元着色器、混合。背完发现还是不知道这玩意儿跟自己做项目有什么关系。我刚开始也是这样直到有一次做一个角色描边效果怎么调参数都不对最后才发现问题出在渲染路径选错了——我一直在用顶点着色器算描边但项目跑的是延迟渲染法线信息在G-Buffer里顶点阶段根本拿不到正确的法线方向。那次之后我才真正理解渲染流水线不是一个背诵题它是你排查一切渲染问题的底层地图。这篇文章我想把Unity渲染流水线这件事从头到尾讲清楚不是教科书式的罗列阶段而是从“你实际会遇到什么问题”这个角度出发。比如为什么你的半透明物体穿透了不透明物体、为什么阴影边缘有锯齿、为什么改了Shader的混合模式还是不对、为什么URP和内置管线写出来的Shader不通用。这些问题的答案全部藏在流水线的某个具体阶段里。适合谁看如果你已经能写简单的Shader但对“为什么这样写”还不太确定如果你在做项目时遇到渲染问题只能靠试参数解决如果你想从内置管线迁移到URP但不知道哪些概念变了——那这篇内容应该能帮你把脑子里那团浆糊理清楚。我会尽量用生活化的类比来解释每个阶段在干什么同时给出实际的代码和参数让你看完能直接上手验证。2. 从应用阶段到屏幕像素的完整链路拆解2.1 应用阶段CPU在忙什么渲染流水线的第一步其实发生在CPU上Unity管这叫应用阶段。这个阶段的核心任务只有三件事准备渲染数据、做粗粒度的剔除、发出Draw Call。听起来简单但实际项目里大部分性能问题都出在这里。准备渲染数据包括把模型的顶点坐标、法线、UV、切线等信息从内存搬到显存里。如果你在Update里频繁修改Mesh的顶点每次修改都会触发一次完整的重新上传这就是为什么动态网格比如布料模拟特别吃性能。我试过在一个项目里每帧修改一个5000顶点的Mesh帧率直接从120掉到45。后来改成用Compute Shader在GPU端处理CPU占用降了80%。剔除是应用阶段最容易被忽视但收益最大的环节。Unity默认做了视锥体剔除和层剔除但遮挡剔除需要手动烘焙。很多人觉得烘焙遮挡剔除太麻烦就跳过了但在室内场景里遮挡剔除能减少60%以上的Draw Call。我做过一个测试一个包含200个家具的室内场景不烘焙遮挡剔除时Draw Call是340烘焙后降到120左右。代价是烘焙时间大概15分钟以及额外的内存占用。注意遮挡剔除的烘焙结果和摄像机位置强相关。如果你的场景是开放世界或者摄像机可以自由飞行遮挡剔除的效果会大打折扣甚至可能因为频繁的可见性切换导致性能波动。Draw Call的合并是另一个关键点。Unity提供了静态批处理和动态批处理两种机制。静态批处理把多个不动的物体合并成一个大的Mesh代价是内存占用增加动态批处理在运行时合并顶点数少于300的物体限制比较多。实际项目中我通常优先用GPU Instancing来处理大量重复物体比如草地、树木因为它的内存开销比静态批处理小得多而且支持动态物体。2.2 几何阶段顶点变换与图元装配几何阶段是GPU真正开始干活的地方。顶点着色器是第一个可编程阶段它的核心任务是把顶点坐标从模型空间变换到裁剪空间。这个变换过程涉及三个矩阵模型矩阵M、视图矩阵V、投影矩阵P合起来就是MVP矩阵。为什么是这三个矩阵你可以这样理解模型矩阵把物体从“自己的坐标系”搬到“世界坐标系”视图矩阵把世界坐标系搬到“摄像机坐标系”投影矩阵把摄像机坐标系搬到“裁剪空间”。裁剪空间是一个立方体所有在这个立方体之外的顶点都会被裁掉。这里有一个常见的坑很多人以为顶点着色器里只能做坐标变换其实它还可以做很多事情。比如顶点动画风吹草动、顶点描边把顶点沿法线方向外扩、甚至简单的光照计算逐顶点光照。但要注意顶点着色器里拿不到片元级别的信息所以像高光这种需要逐像素计算的效果放在顶点着色器里会很难看。图元装配阶段把顶点组装成三角形。这里有一个容易被忽视的细节三角形的顶点顺序决定了它的朝向。Unity默认使用顺时针方向作为正面如果你从外部导入了逆时针方向的模型就会看到模型“内外翻转”。解决办法要么在导入设置里勾选“Swap UVs”或者“Flip Normals”要么在Shader里用Cull Off关闭背面剔除。裁剪阶段会把完全在裁剪空间之外的三角形丢弃部分在内部的三角形会被切分。这个阶段是硬件自动完成的你没法干预但它的存在解释了为什么有时候物体会突然消失——不是被剔除了而是被裁剪了。2.3 光栅化从三角形到像素光栅化是把连续的三角形离散化成一个个像素的过程。你可以把它想象成用网格纸去覆盖一个三角形每个格子就是一个像素。光栅化阶段会计算出每个像素的重心坐标这个坐标在后面的片元着色器里会用来插值顶点数据。插值是光栅化阶段最核心的概念。顶点着色器输出的数据法线、UV、颜色会在三角形内部进行线性插值。这就是为什么低模上的光照看起来比较平滑——因为法线被插值了。但插值也带来一个问题如果两个顶点的法线方向差异太大插值出来的法线可能指向奇怪的方向导致光照出现黑斑。解决办法是增加模型的面数或者在导入时调整法线平滑角度。提示Unity的模型导入设置里有一个“Normals”选项可以选择“Import”或“Calculate”。如果选择“Calculate”Unity会根据平滑角度重新计算法线。对于硬边模型比如立方体平滑角度设为0对于曲面模型平滑角度设为60左右比较合适。2.4 片元着色器与逐像素处理片元着色器是你能控制的最精细的阶段。每个像素都会执行一次片元着色器所以它的性能开销和屏幕分辨率直接相关。在移动端片元着色器的开销往往是瓶颈因为移动GPU的填充率有限。片元着色器里通常做这些事情纹理采样、光照计算、颜色混合。纹理采样是最常见的操作但也是最容易出性能问题的地方。每次采样都会消耗带宽尤其是在移动端。我见过一个项目在片元着色器里采样了8张贴图结果在低端机上直接跑不动。后来把其中4张合并成一张图集采样次数降到5次帧率提升了30%。光照计算是另一个性能大户。逐像素光照比逐顶点光照效果好但开销大得多。Unity的内置管线里Forward渲染路径会对每个像素计算所有光源的影响光源数量多了之后性能下降非常明显。这也是为什么URP把光照限制在有限数量的逐像素光源上其余光源用逐顶点或SH球谐函数来处理。2.5 逐片元操作深度测试与混合逐片元操作阶段决定了最终哪些像素会被写入屏幕。这个阶段包括深度测试、模板测试和混合。深度测试解决的是“谁在前面”的问题。每个像素都有一个深度值离摄像机近的像素会覆盖远的像素。但深度测试有一个前提不透明物体和不透明物体之间才能正确排序。半透明物体因为需要混合通常要关闭深度写入只做深度测试。这就是为什么半透明物体之间会出现排序错误——它们互相之间不知道谁在前谁在后。混合决定了像素如何叠加到屏幕上。常见的混合模式有Alpha Blend透明度混合、Additive叠加、Multiply正片叠底。混合的公式是最终颜色 源颜色 × 源因子 目标颜色 × 目标因子。Unity的Shader里用Blend命令来设置这两个因子。注意混合模式的选择会直接影响渲染结果。比如做发光效果时用Additive颜色会越叠越亮做阴影时用Multiply颜色会越叠越暗。选错了混合模式效果会完全不对。3. 渲染路径的选择与底层逻辑3.1 前向渲染简单直接但有上限前向渲染是最直观的渲染路径。它的工作方式是对每个物体计算所有影响它的光源然后输出最终颜色。简单来说就是“一个物体一个物体地画画的时候把所有光照都算上”。前向渲染的优点是支持半透明、支持自定义Shader、兼容性好。缺点是光源数量多了之后性能急剧下降。因为每个物体都要重新计算所有光源如果有10个光源和100个物体那就是1000次光照计算。Unity的内置管线里前向渲染把光源分为Important和Not Important两类Important光源做逐像素计算Not Important光源做逐顶点或SH计算。默认情况下最重要的那个光源通常是方向光是逐像素的其余光源根据设置决定。我在实际项目里的经验是前向渲染适合光源数量少、物体数量多的场景比如室外场景一个方向光加几个点光源。如果场景里有大量动态光源前向渲染会很吃力。3.2 延迟渲染用G-Buffer换性能延迟渲染的思路完全不同。它先把所有物体的几何信息位置、法线、颜色、材质属性渲染到一组缓冲区里这组缓冲区叫G-Buffer。然后再用一个全屏的Pass根据G-Buffer里的信息计算光照。延迟渲染的优点是光源数量不影响性能因为光照计算是在屏幕空间做的和场景复杂度无关。缺点是G-Buffer占用大量显存带宽不支持半透明半透明物体还是要用前向渲染单独处理而且对MSAA多重采样抗锯齿的支持不好。G-Buffer的布局是延迟渲染的核心。Unity的内置延迟渲染用了四张RTRender TextureRT0存漫反射颜色和遮挡RT1存高光颜色和粗糙度RT2存法线和深度RT3存自发光和光照贴图。每张RT都是ARGB32格式加起来就是16字节每像素。在1080p分辨率下G-Buffer占用的显存大约是1920×1080×16×4≈132MB。这个开销在移动端是不可接受的所以移动端基本不用延迟渲染。3.3 URP与内置管线的核心差异URPUniversal Render Pipeline是Unity现在主推的渲染管线。它和内置管线最大的区别在于URP把渲染流程完全暴露给开发者你可以通过Scriptable Renderer Feature来插入自定义的渲染Pass。URP的渲染路径只有Forward和Forward2D Renderer另算。Forward是URP 12之后引入的它把光源信息存到一张屏幕空间的贴图里然后在片元着色器里根据像素位置查找影响它的光源。这样既保留了前向渲染的灵活性又解决了光源数量限制的问题。从内置管线迁移到URP时最大的坑是Shader不兼容。内置管线的Surface Shader在URP里完全不能用必须改写成HLSL的Vertex/Fragment Shader。我迁移过一个项目大概有30个自定义Shader全部重写花了将近两周。建议如果项目还在早期直接上URP如果项目已经上线迁移成本要仔细评估。提示URP的Shader Graph可以大幅降低Shader编写门槛但它的性能不如手写HLSL。如果对性能有极致要求还是建议手写Shader。4. 实操手写一个最小可用的渲染流程4.1 搭建测试环境先创建一个Unity项目版本建议2022 LTS或更新。在Package Manager里安装URP然后创建一个URP Asset并赋值到Graphics Settings里。这一步做完之后场景里的材质会变成粉色——因为内置Shader在URP下不兼容。别慌这是正常的。创建一个新的Shader文件命名为“MinimalRenderPipeline”。删除默认内容从零开始写。我会把每一步都解释清楚确保你能理解每一行代码在干什么。4.2 顶点着色器的实现细节顶点着色器的输入是模型空间的顶点数据输出是裁剪空间的坐标和需要传递给片元着色器的数据。在URP里我们需要包含“Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl”这个文件它提供了TransformObjectToHClip等工具函数。Shader Custom/MinimalRenderPipeline { Properties { _BaseColor (Base Color, Color) (1,1,1,1) _MainTex (Main Texture, 2D) white {} } SubShader { Tags { RenderTypeOpaque RenderPipelineUniversalPipeline } Pass { HLSLPROGRAM #pragma vertex vert #pragma fragment frag #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; }; TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex); float4 _BaseColor; Varyings vert(Attributes input) { Varyings output; output.positionCS TransformObjectToHClip(input.positionOS.xyz); output.uv input.uv; return output; } half4 frag(Varyings input) : SV_Target { half4 texColor SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv); return texColor * _BaseColor; } ENDHLSL } } }这段代码做了三件事把顶点从模型空间变换到裁剪空间、把UV传递给片元着色器、在片元着色器里采样纹理并乘以基础颜色。TransformObjectToHClip这个函数内部做了MVP变换你不需要手动乘矩阵。4.3 片元着色器中的光照计算上面的Shader没有光照看起来是平的。要加光照需要引入URP的光照库。最简单的做法是使用UniversalFragmentPBR函数它封装了PBR光照计算。#include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionCS : SV_POSITION; float3 normalWS : TEXCOORD0; float3 positionWS : TEXCOORD1; float2 uv : TEXCOORD2; }; Varyings vert(Attributes input) { Varyings output; VertexPositionInputs posInputs GetVertexPositionInputs(input.positionOS.xyz); VertexNormalInputs normInputs GetVertexNormalInputs(input.normalOS); output.positionCS posInputs.positionCS; output.positionWS posInputs.positionWS; output.normalWS normInputs.normalWS; output.uv input.uv; return output; } half4 frag(Varyings input) : SV_Target { half4 texColor SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv); InputData lightingInput (InputData)0; lightingInput.positionWS input.positionWS; lightingInput.normalWS normalize(input.normalWS); lightingInput.viewDirectionWS GetWorldSpaceNormalizeViewDir(input.positionWS); lightingInput.albedo texColor.rgb * _BaseColor.rgb; lightingInput.smoothness 0.5; lightingInput.metallic 0; half4 color UniversalFragmentPBR(lightingInput); return color; }这段代码的关键在于InputData结构体它包含了PBR光照需要的所有信息世界空间位置、法线、视线方向、反照率、光滑度、金属度。UniversalFragmentPBR会根据这些信息计算直接光照和间接光照。4.4 深度测试与混合的配置默认情况下URP的Shader会开启深度测试和深度写入。如果你要做半透明效果需要在Pass里加上这些配置Tags { RenderTypeTransparent QueueTransparent RenderPipelineUniversalPipeline } Blend SrcAlpha OneMinusSrcAlpha ZWrite Off ZTest LEqualZWrite Off表示不写入深度缓冲这样后面的物体不会被遮挡。ZTest LEqual表示深度小于等于当前深度时才通过测试。Blend SrcAlpha OneMinusSrcAlpha是标准的Alpha混合公式。注意半透明物体需要从远到近排序才能正确混合。Unity会自动对半透明物体排序但排序是基于物体的包围盒中心所以大型半透明物体比如水面可能会出现排序错误。解决办法是把大型半透明物体拆分成多个小物体或者用Shader里的Alpha To Coverage。5. 常见渲染问题与排查思路5.1 物体闪烁或Z-FightingZ-Fighting是两个物体深度值太接近导致GPU无法判断谁在前谁在后于是像素在两者之间闪烁。常见于共面的物体比如地板和地毯。解决办法有几种一是把其中一个物体稍微抬高一点0.001个单位就够二是调整摄像机的近裁剪面近裁剪面太小会导致深度精度下降三是用Shader里的Offset命令给其中一个物体一个深度偏移。Offset 0, -1这行代码会让物体的深度值稍微往前偏一点避免和另一个物体冲突。5.2 阴影边缘锯齿严重阴影锯齿通常是因为阴影贴图分辨率不够。在URP里可以在URP Asset里调整Shadow Resolution。但提高分辨率会增加显存占用和渲染开销。另一个原因是阴影 acne阴影痤疮表现为物体表面出现条纹状的阴影。这是因为阴影贴图的深度精度不够导致物体自己遮挡自己。解决办法是调整Shadow Bias和Normal Bias。Bias太大会导致阴影脱离物体Peter Panning太小又会有acne。我通常从0.05开始调根据场景尺度微调。5.3 半透明物体排序错误前面提到过半透明物体依赖排序。如果排序错误会出现“前面的物体被后面的物体遮挡”的现象。排查方法是打开Frame Debugger看半透明物体的渲染顺序。如果排序确实有问题可以尝试这些方案把半透明物体的Render Queue设为同一个值让Unity按距离排序或者用Shader里的Alpha To Coverage把半透明当不透明处理或者手动控制渲染顺序用Camera的Depth参数。5.4 Shader在URP下显示粉色这是最常见的问题原因是Shader不兼容URP。粉色是Unity的报错材质表示Shader编译失败。解决办法是检查Shader里是否包含了URP的库文件以及是否用了内置管线的函数比如UnityObjectToClipPos要改成TransformObjectToHClip。问题现象可能原因排查方法解决方案物体闪烁Z-Fighting打开Frame Debugger看深度值抬高物体或调整近裁剪面阴影锯齿阴影贴图分辨率低检查URP Asset的Shadow Resolution提高分辨率或调整Bias半透明排序错误排序依赖包围盒中心用Frame Debugger看渲染顺序拆分物体或手动控制QueueShader粉色不兼容URP检查Shader报错信息替换内置函数为URP函数光照过暗缺少环境光检查Lighting设置调整Environment Lighting6. 性能优化的几个关键抓手6.1 减少OverdrawOverdraw是指同一个像素被多次绘制。比如一个像素先被背景绘制然后被前景物体覆盖这就是2次Overdraw。Overdraw越多GPU的填充率压力越大。减少Overdraw的方法一是从远到近渲染不透明物体Unity默认就是这样这样前面的物体会覆盖后面的减少无效绘制二是避免大面积半透明物体叠加三是用Occlusion Culling剔除被遮挡的物体。我做过一个测试一个粒子特效场景Overdraw高达8倍帧率只有30。后来把粒子数量减半Overdraw降到4倍帧率恢复到60。所以粒子特效是Overdraw的重灾区能少用就少用。6.2 合理使用LODLODLevel of Detail是根据物体距离摄像机的远近切换不同精度的模型。远处的物体用低模近处的物体用高模。这是减少顶点数的有效手段。Unity的LOD Group组件可以设置多个LOD级别。我通常设置3级0级是原始模型1级是减面50%的模型2级是减面80%的模型。切换距离根据屏幕占比来定一般屏幕占比小于10%时切换到下一级。提示LOD的切换距离需要根据项目实际情况调整。如果切换太早玩家会看到明显的模型跳变如果切换太晚性能收益不明显。建议在真机上测试找到平衡点。6.3 批处理与合批批处理是把多个Draw Call合并成一个。Unity支持静态批处理、动态批处理和GPU Instancing。静态批处理适合不动的物体动态批处理适合顶点数少的物体GPU Instancing适合大量重复物体。GPU Instancing是现在最推荐的方案。它只需要一个Draw Call就能渲染成百上千个相同的物体。使用方法是勾选材质的Enable GPU Instancing然后在Shader里声明instancing相关的宏。#pragma multi_compile_instancing UNITY_INSTANCING_BUFFER_START(Props) UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor) UNITY_INSTANCING_BUFFER_END(Props)这样每个实例可以有不同的颜色但共享同一个Mesh和材质。6.4 纹理压缩与Mipmap纹理是显存占用的大头。一张2048×2048的RGBA32纹理占用16MB显存压缩成ASTC 6x6后只占用约2.7MB。所以纹理压缩是必须的。Mipmap是纹理的多级渐远纹理用于远处物体的纹理采样。开启Mipmap会增加33%的显存占用但能减少远处的纹理闪烁和带宽消耗。对于UI纹理通常关闭Mipmap对于3D物体纹理建议开启。优化手段适用场景性能收益注意事项静态批处理不动的场景物体Draw Call减少50%-80%增加内存占用GPU Instancing大量重复物体Draw Call减少90%以上需要Shader支持LOD大型场景顶点数减少60%需要制作多级模型纹理压缩所有纹理显存减少70%压缩质量有损Mipmap3D物体纹理带宽减少30%显存增加33%7. 从内置管线迁移到URP的实操记录7.1 迁移前的准备工作迁移之前先备份项目。然后统计项目里有多少自定义Shader、多少用了Surface Shader、多少用了内置管线的函数。我迁移的那个项目有30个Shader其中20个是Surface Shader10个是Vertex/Fragment Shader。Surface Shader在URP里完全不能用必须重写。Vertex/Fragment Shader相对好办主要是替换函数名和库文件。我建议先迁移一个最简单的Shader跑通了再批量迁移。7.2 函数替换对照表内置管线和URP的函数名有很多差异下面是我整理的一份对照表内置管线函数URP函数说明UnityObjectToClipPosTransformObjectToHClip模型空间到裁剪空间UnityObjectToWorldNormalTransformObjectToWorldNormal模型空间到世界空间法线WorldSpaceViewDirGetWorldSpaceNormalizeViewDir世界空间视线方向UNITY_LIGHT_ATTENUATION需要手动计算光照衰减_WorldSpaceLightPos0GetMainLight()主光源信息_LightColor0GetMainLight().color主光源颜色7.3 迁移后的验证与调试迁移完成后需要逐个验证Shader的效果。我通常用Frame Debugger来检查每个Pass的渲染结果确保没有遗漏。另外URP的Shader报错信息比内置管线详细遇到问题先看Console里的报错。有一个坑要注意URP的Shader变体Shader Variant比内置管线多打包时可能会因为变体太多导致编译时间过长。解决办法是在URP Asset里关闭不需要的功能比如阴影、附加光源等。提示URP的Shader Variant Collection可以手动指定需要编译的变体减少打包时间。但配置起来比较麻烦建议项目后期再做。8. 一些踩过的坑和实用技巧8.1 法线贴图的切线空间问题法线贴图有两种切线空间和对象空间。切线空间法线贴图需要模型有正确的切线信息。如果模型是从Maya或Blender导入的切线信息可能不对导致法线贴图效果异常。解决办法是在Unity的模型导入设置里把Tangents选项设为Calculate。这样Unity会根据UV和法线重新计算切线。但要注意如果UV有重叠或者拉伸计算出来的切线可能还是不对。8.2 阴影的级联设置方向光的阴影用了级联阴影贴图Cascaded Shadow Maps把视锥体分成多个区域每个区域用不同分辨率的阴影贴图。级联数量越多远处阴影越清晰但开销也越大。URP里可以在URP Asset里设置Cascade Count一般设为2或4。如果场景不大设为1也够用。我试过在一个小场景里把Cascade Count从4降到1帧率提升了15%。8.3 后处理的顺序问题URP的后处理是通过Volume组件来控制的。多个Volume叠加时Priority高的会覆盖Priority低的。但后处理的执行顺序是固定的Bloom、DoF、Motion Blur、Tonemapping、Color Grading。如果你需要自定义后处理顺序得自己写Renderer Feature。8.4 移动端的渲染注意事项移动端GPU和桌面GPU的架构差异很大。移动端是Tile-Based Rendering把屏幕分成小块每块在片上内存里渲染。这意味着移动端对Overdraw特别敏感因为每次Overdraw都会增加片上内存的读写。移动端优化的几个要点一是尽量用简单的Shader避免复杂的数学运算二是减少纹理采样次数三是避免使用Alpha Test因为它会打断Tile-Based Rendering的优化四是控制光源数量移动端通常只支持一个逐像素方向光。8.5 调试渲染问题的工具链Unity提供了几个调试渲染问题的工具Frame Debugger可以逐帧查看Draw CallRenderDoc可以抓取GPU的渲染过程Xcode的GPU Frame Capture可以分析iOS设备的渲染性能。我平时用得最多的是Frame Debugger它能直观地看到每个Pass的渲染结果和Draw Call数量。RenderDoc更底层适合分析GPU的瓶颈但学习曲线比较陡。提示Frame Debugger在URP下的信息比内置管线更详细可以看到每个Renderer Feature的执行情况。如果发现某个Pass的耗时特别长可以针对性地优化。9. 渲染流水线的进阶方向9.1 自定义渲染管线如果你对URP的默认流程不满意可以基于URP写自定义的Renderer Feature甚至完全自己写一个Scriptable Render Pipeline。自定义管线的好处是你可以完全控制渲染流程比如实现自定义的延迟渲染、自定义的后处理顺序。但自定义管线的成本很高需要深入理解图形API和GPU架构。我建议先用URP的Renderer Feature来扩展如果确实不够用再考虑完全自定义。9.2 Compute Shader与GPU Driven RenderingGPU Driven Rendering是未来的趋势它把剔除、LOD选择、Draw Call生成都放到GPU上做。Unity的DOTS和Graphics.DrawMeshInstancedIndirect支持这种模式。我试过用Compute Shader做视锥体剔除把10000个物体的剔除计算从CPU搬到GPUCPU占用从30%降到5%。但GPU Driven Rendering的复杂度很高需要处理GPU和CPU之间的同步问题不适合所有项目。9.3 光线追踪与混合渲染URP从2022版本开始支持光线追踪需要硬件支持。光线追踪可以做出非常真实的反射和阴影但性能开销很大。目前主要用在高端PC和主机上移动端还不现实。混合渲染是一个折中方案不透明物体用光栅化反射和阴影用光线追踪。这样既能保证性能又能提升画质。但实现起来比较复杂需要深入理解两种渲染方式的差异。9.4 渲染流水线的学习资源如果你想深入学习渲染流水线我推荐几个资源Unity的官方文档URP部分是必读的《Real-Time Rendering》是图形学的经典教材Catlike Coding的教程对Unity Shader讲得很透彻还有Unity的Graphics GitHub仓库可以看到URP的源码。我个人觉得最有效的方式是动手写。看再多文档不如自己写一个Shader跑起来看看效果。遇到问题就查文档、查源码、用Frame Debugger分析慢慢就理解了。10. 个人在实际项目中的体会做了这么多年Unity渲染我最大的体会是渲染流水线不是背出来的是调出来的。你背一百遍“顶点着色器、光栅化、片元着色器”不如自己写一个Shader然后故意改错一个参数看看画面会变成什么样。错误是最好的老师。另一个体会是不要过早优化。我见过很多项目一开始就追求极致的渲染效果结果性能扛不住又回头砍效果。正确的做法是先跑通流程再根据性能数据逐步优化。优化要有数据支撑不能凭感觉。最后分享一个小技巧如果你不确定某个渲染问题出在哪个阶段可以用一个最笨的方法——把Shader简化到极致只输出一个纯色然后逐步加功能看哪一步出问题。这个方法虽然笨但非常有效。我排查过最诡异的一个渲染bug最后就是用这个方法定位到是法线贴图的切线空间计算错误。渲染流水线的内容还有很多比如曲面细分、几何着色器、Compute Shader在渲染中的应用这些我以后有机会再展开。如果你在渲染方面遇到什么问题欢迎一起交流。
返回列表