unity渲染场景流程

大概流程

渲染管线使用物体队列调用多个渲染通道->渲染通道遍历物体队列调用Shader中的某个Pass->Pass中通过Shader执行物体渲染过程

为了区分,把 RenderPipeline 称为 渲染管线, ShaderPipeline 称为着色管线,RenderPass 称为渲染通道,ShaderPass 称为着色通道

渲染管线的大致流程

	//	这个就是渲染管线,不同的管线有不同的实现代码,这里描述的是内置渲染管线
	void 	Pipeline:Render(RenderContext context)
	{
		for (var camera in camearas)
		{
			Render(context,camera);
		}
	}
	
	void 	Pipeline:Render(RenderContext context,Camera camera)
	{
		CullResult cull = CullResults.Cull(context);//	剔除掉不会渲染的物体
		
		context.SetupCameraProperties(camera);		//	设置矩阵及其他属性
		
		var buffer = new CommandBuffer();			//	创建命令缓冲
		buffer.ClearRenderTarget();					//	清除渲染目标
		context.ExecuteCommandBuffer(buffer);		//	执行命令缓冲,没有立即执行,只是拷贝到内部缓冲区
		buffer.Release();
		
		//	现在主流有2种渲染路径:前向渲染和延迟渲染
		//		不同的渲染路径会使用不同的 shader 代码,因此在立项时就要确定好采用哪种渲染路径
		//		一般手机用前向渲染,通用性更好,PC或VR等性能好的用延迟渲染,具有更好的光影表现
		//	参考  教程\图形学\渲染管线\延迟渲染 VS 前向渲染.pdf
		//	参考  Unity User Manual (2019.4 LTS)/图形/Render pipelines/内置渲染管线/Extending the Built-in Render Pipeline with Command Buffers
		//	不同的路径有不同实现代码,通过组合不同的物体队列和着色通道来渲染物体
		//	
		if ( m_useForward )
		{
			//	前向渲染
			//	渲染不透明物体然后渲染透明物体
			
			//	指定使用材质上的哪个Pass,物体材质上可以有多个 SubShader 代表不同的显卡平台,每个SubShader上有多个Pass
			//	每个Pass代表不同参数下如何渲染物体,当我们渲染一个队列的物体时要指定用哪个Pass
			
			//	初始化所有灯光
			setupFirstLight();	
				
			//	渲染不透明物体
			{
				//	前向渲染当重要光源超过1个时会用到多个Pass
				//	第1次设置第1个重要光源和其他不重要光源,然后调用材质中的 ForwardBase Pass
				var drawSettings = new DrawRendererSettings(camera, new ShaderPassName("ForwardBase"));
				
				//	指定排序方式,不透明物体按离摄像机从近到远排序,这样远的物体如果被覆盖,在深度测试时就被pass掉,提高效率
				drawSettings.sorting.flags = SortFlags.CommandOpaque;		
				//	drawSettings.SetOverrideMaterial(errMaterial);	//	可以通过该函数覆盖所有物体渲染时的材质
				//	指定不透明物体队列
				var filterSettings = new FilterRenderersSettings(){renderQueueRange=RenderQueueRange.opaque};	
				//	使用 ForwardBase 着色器通道渲染不透明物体,FrameDebugger中看到的是 A ForwardBase, B ForwardBase
				conext.DrawRenderers(cull.visibleRenderers, ref drawSettings, filterSettings);	
				
				//	然后对剩下的重要光源每次设置1个,然后调用材质中的 ForwardAdd Pass ,
				//		为什么使用不同的Pass 呢,因为第1次Pass还有其它光源
				//	后面的Pass有点不一样,是设置好所有Pass,然后再遍历物体
				for ( int i=1; i<m_importLights.Length; i++ )
				{
					drawSettings.SetShaderPassName(i-1,new ShaderPassName("ForwardAdd"));
				}
				//	设置好所有Pass后再一次性遍历,所以在 FrameDebugger中看到的是 A ForwardAdd, A ForwardAdd, B ForwardAdd, B ForwardAdd
				conext.DrawRenderers(cull.visibleRenderers, ref drawSettings, filterSettings);
			}
			
			contex.DrawSkyBox();	//	画天空盒,没有立即执行,只是提交了个命令到内部缓冲区
			
			//	渲染半透明物体
			{
				//	透明物体队列物体按离摄像头从远到近排序,这样才能正确显示
				drawSettings.sorting.flags = SortFlags.CommandTransparent;			
				//	跟渲染不透明物体有点不一样,是一次性把所有Pass 过掉
				for ( int i=0; i<m_importLights.Length; i++ )
				{
					drawSettings.SetShaderPassName(i,new ShaderPassName(i==0?"ForwardBase":"ForwardAdd"));
				}
				//	只渲染半透明物体
				var filterSettings = new FilterRenderersSettings(){renderQueueRange=RenderQueueRange.transparent};	
				//	渲染结果是 A ForwardBase, A ForwardAdd, A ForwardAdd, B ForwardBase, B ForwardAdd, B ForwardAdd
				conext.DrawRenderers(cull.visibleRenderers, ref drawSettings, filterSettings);
			}
		}
		else 
		{
			//	延迟渲染
			//	渲染不透明物体到GBuffer(记录了颜色,空间位置,法线等信息,但不包括光照)
			//	把GBuffer当成一个物体渲染(计算光照)
			//	用前向渲染渲染半透明物体
			
			//	渲染不透明物体到GBuffer(不包括光照)
			{
				//	使用 Deferred Pass
				var drawSettings = new DrawRendererSettings(camera, new ShaderPassName("Deferred"));
				
				//	指定排序方式,不透明物体按离摄像机从近到远排序,这样远的物体如果被覆盖,在深度测试时就被pass掉,提高效率
				drawSettings.sorting.flags = SortFlags.CommandOpaque;		
				
				//	指定不透明物体队列
				var filterSettings = new FilterRenderersSettings(){renderQueueRange=RenderQueueRange.opaque};	
				
				//	使用 Deferred 着色器通道渲染不透明物体,FrameDebugger中看到的是 A Deferred, B Deferred
				conext.DrawRenderers(cull.visibleRenderers, ref drawSettings, filterSettings);	
			}
			{
				//	渲染 GBuffer 的反射光
				RenderGBufferReflection();
				//	用GBuffer计算自发光
				RenderGBufferEmissive();
		
				//	渲染点光源
				for (var light in m_pointLights )
				{
					RenderGBufferPointLight(light);
				}
				//	渲染方向光
				for (var light in m_directLights )
				{
					RendrGBufferDirectLight(light);
				}
			}
			
			//	到这里延迟渲染结束,后面跟前向渲染是一样的
			contex.DrawSkyBox();	//	画天空盒
			...						//	渲染半透明物体
		}
		
		postProcess();			//	后期处理
		context.Submit();		//	前面都只是提交渲染指令,这里才正执行
	}
	
	//	渲染一遍物体,这里主要介绍影响到排序的因素
	//	由于源码不公开,下面是推测的代码
	ScriptableRenderContext.DrawRenderers()
	{
		//	对物体排序,参考 渲染排序
		
	}

渲染管线

  • 渲染管线确定场景中对象的显示方式,分为三个主要阶段 剔除->渲染->后期处理
    unity提供了几种渲染管线,你最好在一开始就选择好合适的渲染管线,因为不同的管线使用不同的材质,在后期很难切换
    默认使用内置管线
    3种渲染管线

    • 内置渲染管线
      默认的,渲染路径支持前向渲染和延迟渲染和顶点渲染

    • Universal Render Pipeline (URP)
      轻量级管线,可脚本编程,采用 前向渲染,适合跨多平台,
      针对每个对象在一个 ShaderPass 中渲染所有灯光

    • High Definition Render Pipeline (HDRP)
      高性能平台,可脚本编程经,采用 延迟渲染

    默认使用内置渲染管线,其它2个应该要在 Package Manager 中下载,然后通过 Window > Render Pipeline > HD Render Pipeline Wizard 来设置

渲染路径

  • 渲染路径描述了渲染管线采用的方法,包括 物体分组,使用几个 RenderPass 来渲染物体
    内置渲染管线提供了3种渲染路径
    你可以在摄像机组件的 RenderingPath 属性中设置采用哪种渲染路径

    • Forward Rendering

      • 前向渲染,光照和着色同时进行,
        比如 物体A 所有光照和着色 -> 物体B 所有光照和着色
      • 渲染流程:
        把物体分2组,1组透明物体,1组不透明物体
        将灯光分类,像素级灯光,顶点级灯光和圆形光
        每组会渲染多遍:
        第1遍 渲染首个像素级灯光和所有其它光以及光照贴图,使用 ForwardBase Pass
        第2-n遍 逐个渲染其它像素级灯光, 渲染 ForwardAdd Pass
      • 优点
        这个比较通用,支持所有情况,是标准渲染模式
      • 缺点
        实时渲染光源比较耗性能,因为物体如果没有排序好,被遮盖的仍会计算光照,浪费性能
      • 优化
        为了平衡性能你应该设置哪些光源使用像素着色(一般只设置1个),剩下的光源使用廉价的顶点着色
      • 光源处理
        Render Mode 设置为 Not Important 的光源始终为顶点着色。
        最亮的方向光始终像素着色
        Render Mode 设置为 Important 的光源始终为像素着色
        如果上述情况导致光源数少于当前的 Pixel Light Count 质量设置,则按照亮度降低的顺序,更多光源采用像素着色方式。
    • Deferred Shading

      • 延迟渲染
        先把几何数据(包括材质)渲染到缓存中,后期再用缓存数据进行光照处理
        比如 物体A几何和材质->物体B几何和材质->物体A光照1->物体B光照1->物体A光照2->物体B光照2->…

      • 渲染流程
        把物体分2组,1组透明物体,1组不透明物体
        透明物体和 前向渲染 的方法一样,也是用 ForwardBase 和 ForwardAdd
        不透明物体渲染流程:
        第1遍 不计算光照,渲染 Defered Pass,结果存放到 GBuffer
        第2-n遍 用 GBuffer 计算光照 用的是 Internal-DeferredShading 的 Pass 0
        在生成最终像素时,材质纹理叠加前面生成的光照贴图、自发光以及烘焙场景产生的光照贴图模型
        可以看到我们只能在材质上修改第1遍的 Defered Pass ,后面的必须在 Project Settings->Graphics->Built-in Shader Settings 中修改

      • 优点
        灯光数量和场景复杂度对效率影响较小
        能非常好的支持多个实时灯光,有非常逼真的效果,因此在支持的平台上尽量使用这个
        因为对每个光源,只要计算GBuffer上的像素点的光照而不是对所有模型表面的像素点计算光照

      • 缺点
        这个需要GPU支持,一般手机上都不支持,
        PC 2006 年以后制造的大多数 PC 显卡都支持延迟着色
        手机 所有至少运行 OpenGL ES 3.0 的移动设备都支持延迟着色
        而且不支持
        半透明物体 因为GBuffer上每个点只能保存一份数据,处理半透明需要保存多份数据
        硬件抗锯齿 因为GBuffer上无法体现每个像素对应的原始模型
        正交视图

      • GBuffer信息
        RT0,ARGB32 格式:漫射颜色 (RGB),遮挡 (A)。
        RT1,ARGB32 格式:镜面反射颜色 (RGB),粗糙度 (A)。
        RT2,ARGB2101010 格式:世界空间法线 (RGB),未使用 (A)。
        RT3,ARGB2101010(非 HDR)或 ARGBHalf (HDR) 格式:发射 + 光照 + 光照贴图 + 反射探针缓冲区。
        深度+模板缓冲区。

      • 修改延迟光照Shader
        参考 Unity User Manual (2019.4 LTS)/图形/Render pipelines/内置渲染管线/Rendering paths in the Built-in Render Pipeline/Deferred Shading rendering path

    • Vertex Lit Rendering
      顶点渲染,所有光照都只在顶点上计算,因此速度很快,支持范围最广
      不支持 阴影、法线贴图、光照剪影和高度细节化的镜面高光
      渲染流程: 顶点坐标变换MVP->背面拣选->灯光处理->剪裁->投影->光栅化
      灯光处理:处理顶点的灯光,在2d游戏中一般直接设置顶点颜色,在3d游戏中通过灯光
      颜色和顶点法线来计算顶点颜色,在顶点着色器中处理
      光栅化:处理像素颜色,通过顶点灯光插值出像素灯光,再乘以像素的材质,在像素着色器中处理

渲染通道

  • 渲染通道描述了渲染路径中的通道部分,1个通道表示1组物体使用材质关联的shader中的某个Pass渲染一遍
    多个通道表示某组物体全部用通道1渲染一遍,再全部用通道2渲染一遍,…

全局光照 (Global Illumination)

  • 上面描述的都是实时光照部分,在实时光照的最后会混入全局光照
    全局光照是一种在场景中添加直接或间接光照以提供逼真光照效果的一组技术。
    unity中是计算 设置为 烘焙的光源 的直接光照,所有固定光源的间接光照
    GI 有几种方法,如烘焙/动态光照贴图、辐照度体积、光传播体积、烘焙/动态光照探针、基于体素的 GI 和基于距离场的 GI。
    Unity 支持开箱即用的烘焙/动态光照贴图和光照探针

  • 光照贴图
    适用于静态物体,用来保存计算好的光照数据
    光照贴图在你操作场景进行烘焙后会生成并存放在场景的同名目录下

  • 光照探针
    适用于动态物体,采样空间中某些点的光照数据,物体可以用这些数据插值得到近似光照
    会生成LightProbes.asset的光影采集文件

URP 渲染管线研究

  • 相关类

    • UniversalRenderPipeline 渲染管线
    • ScriptableRenderer 渲染器,主要抽象不同的渲染路径
      • ForwordRenderer urp 只有一个前向渲染器
    • ScriptableRenderPass 渲染某个通道
      • MainLightShadowCasterPass 主光源阴影
      • DrawObjectsPass 普通物体渲染(包括透明和不透明)
      • DrawSkyboxPass 天空盒
      • DepthOnlyPass 深度
      • PostProcessPass 后期处理
  • URP渲染流程

    	//	渲染所有相机
    	UniversalRenderPipeline.Render(ScriptableRenderContext renderContext, Camera[] cameras)
    	{
    		//	相机按深度排序
    		SortCameras(cameras);
    
    		for (int i = 0; i < cameras.Length; ++i)
            {
                var camera = cameras[i];
    			BeginCameraRendering(renderContext, camera);
    			UpdateVolumeFramework(camera, null);
                RenderSingleCamera(renderContext, camera)
    			{
    				RenderSingleCamera(context, cameraData, cameraData.postProcessEnabled)
    				{
    					---------------------------> 接下面
    				}
    			}
                EndCameraRendering(renderContext, camera);
            }
    	}
    
    	//	渲染单个相机
    	UniversalRenderPipeline.RenderSingleCamera(ScriptableRenderContext context, CameraData cameraData, bool anyPostProcessingEnabled)
    	{
    		//	剪裁
    		camera.TryGetCullingParameters(IsStereoEnabled(camera), out var cullingParameters);
    		renderer.SetupCullingParameters(ref cullingParameters, ref cameraData);
    		var cullResults = context.Cull(ref cullingParameters);
    
    		//	初始化渲染数据
            InitializeRenderingData(asset, ref cameraData, ref cullResults, anyPostProcessingEnabled, out var renderingData);
    
    		//	渲染,只是把指令加入队列中
    		renderer.Setup(context, ref renderingData)
    		{
    			ForwardRenderer.Setup(ScriptableRenderContext context, ref RenderingData renderingData)
    			{
    				//	不同的渲染路径会加入不同的Pass,这里是前向渲染路径
    				//	下面只列出主要的 Pass,这里只是加入 Pass ,并未开始渲染
    
    				//	给你创建渲染管线配置时,在 UniversalRenderPipelineAsset_Renderer 配置中添加的特性处理渲染Pass的机会
    				for (int i = 0; i < rendererFeatures.Count; ++i)
    				{
    					if(rendererFeatures[i].isActive)
    						rendererFeatures[i].AddRenderPasses(this, ref renderingData);
    				}
    
    				//	渲染主光源阴影
    				if (mainLightShadows)
    					EnqueuePass(m_MainLightShadowCasterPass);
    
    				//	渲染额外光源阴影
    				if (additionalLightShadows)
    					EnqueuePass(m_AdditionalLightsShadowCasterPass);
    
    				//	渲染深度缓冲
    				if (requiresDepthPrepass)
    				{
    					m_DepthPrepass.Setup(cameraTargetDescriptor, m_DepthTexture);
    					EnqueuePass(m_DepthPrepass);
    				}
    
    				//	渲染不透明物体
    				EnqueuePass(m_RenderOpaqueForwardPass);
    
    				//	渲染天空盒
    				EnqueuePass(m_DrawSkyboxPass);
    
    				//	渲染透明物体
    				EnqueuePass(m_RenderTransparentForwardPass);
    
    				//	后期处理
    				EnqueuePass(m_PostProcessPass);
    			}
    		}
            renderer.Execute(context, ref renderingData)
    		{
    			ScriptableRenderer.Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    			{
    				//	对前面加入的各种 Pass 进行排序,主要是根据 RenderPassEvent 排序
    				SortStable(m_ActiveRenderPassQueue);
    
    				//	对 m_ActiveRenderPassQueue 中的 Pass 按 RenderPassEvent 分组,
    				//	blockRanges[i] 表示i组结束的 Pass 在 m_ActiveRenderPassQueue 中的索引
    				FillBlockRanges(blockEventLimits, out NativeArray<int> blockRanges);
    
    				//	设置灯光
    				SetupLights(context, ref renderingData);
    
    				//	按各个分组分别渲染分组中的所有 Pass 
    				ExecuteBlock(RenderPassBlock.BeforeRendering, blockRanges, context, ref renderingData); 
    				ExecuteBlock(RenderPassBlock.MainRenderingOpaque, blockRanges, context, ref renderingData, eyeIndex);
    				DrawGizmos(context, camera, GizmoSubset.PreImageEffects);
    				ExecuteBlock(RenderPassBlock.AfterRendering, blockRanges, context, ref renderingData, eyeIndex);
    				DrawGizmos(context, camera, GizmoSubset.PostImageEffects);
    				InternalFinishRendering(context, cameraData.resolveFinalTarget);
    			}
    		}
    
    		//	执行队列中的指令
    		context.ExecuteCommandBuffer(cmd);
            CommandBufferPool.Release(cmd);
            context.Submit();
    	}
    
    	//	渲染1组Pass
    	ScriptableRenderer.ExecuteBlock(int blockIndex, NativeArray<int> blockRanges,
            ScriptableRenderContext context, ref RenderingData renderingData, int eyeIndex = 0, bool submit = false)
    	{
    		int endIndex = blockRanges[blockIndex + 1];
    		//	遍历分组中的 Pass
            for (int currIndex = blockRanges[blockIndex]; currIndex < endIndex; ++currIndex)
            {
                var renderPass = m_ActiveRenderPassQueue[currIndex];
                ExecuteRenderPass(context, renderPass, ref renderingData, eyeIndex)
    			{
    				//	实际执行  Pass 渲染的地方
    				renderPass.Execute(context, ref renderingData)
    				{
    					---------------------------------> 接下面
    				}
    			}
            }
    
            if (submit)
                context.Submit();
    	}
    
    	//	重点介绍普通物体的渲染 Pass (包括透明和不透明)
    	DrawObjectsPass.Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    	{
    		//	指定排序方式,不透明物体按离摄像机从近到远排序,这样远的物体如果被覆盖,在深度测试时就被pass掉,提高效率
    		//	透明物体队列物体按离摄像头从远到近排序,这样才能正确显示
    		var sortFlags = (m_IsOpaque) ? renderingData.cameraData.defaultOpaqueSortFlags : SortingCriteria.CommonTransparent;
    
    		//	渲染这3个 shader pass
    		List<ShaderTagId> m_ShaderTagIdList = new List<ShaderTagId>();
    		m_ShaderTagIdList.Add(new ShaderTagId("UniversalForward"));
            m_ShaderTagIdList.Add(new ShaderTagId("LightweightForward"));
            m_ShaderTagIdList.Add(new ShaderTagId("SRPDefaultUnlit"));
            var drawSettings = CreateDrawingSettings(m_ShaderTagIdList, ref renderingData, sortFlags);
    
    		//	透明和不透明物体使用不同的 RenderQueueRange
    		RenderQueueRange renderQueueRange = (m_IsOpaque) ? RenderQueueRange.opaque : RenderQueueRange.transparent;
    		//	层遮罩是在 URP 配置中配置的,在 UniversalRenderPipelineAsset_Renderer 配置的 Filtering 下
    		LayerMask layerMask = (m_IsOpaque) ? renderingData.opaqueLayerMask : transparentLayerMask;
            var filterSettings = new FilteringSettings(renderQueueRange, layerMask);
    
    		//	渲染物体
    		context.DrawRenderers(renderingData.cullResults, ref drawSettings, ref filterSettings, ref m_RenderStateBlock);
    
    		//	对于不支持urp管线的物体,渲染成红色
    		RenderingUtils.RenderObjectsWithError(context, ref renderingData.cullResults, camera, filterSettings, SortingCriteria.None);
    	}
    
Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐