找回密码
 立即注册
查看: 951|回复: 11

[笔记] Unity 手游多光源处理方案(低端机篇)

[复制链接]
发表于 2021-2-18 09:39 | 显示全部楼层 |阅读模式
Unity手游支持多光源方案,我觉得主要依据手机硬件分成两种类型,一种是手机支持ComputeShader,StructuredBuffers,如iphone,以及android一些高端机,这种类型手机方案有很多,如Forward+、Cluster-base Lighting或者Tiled Based Deferred Shading,前面两种我也有写过,不过在公司内网机,懒得重新写,有兴趣的可以参照这两篇文章。
现在重点讨论下是低端机多光源处理方案。
低端机,如果光源不多,场景简单,可以直接用Forward Rendering,或者用Uber Shader即将光源写在一个Pass里面,但是随着光源增加,渲染压力会越来越大,因此我们需要光源剔除;unity URP中有种方案是逐模型分配光源,像ReflectionProbe一样,将所影响的光源分配到对应的模型上,但是这种对于一些小模型还好,如果对于地形这种大物件就不适用了。
很早之前看到《明日之后》的点光源处理方案,我们运用了Tiled Point Light这一技术,将画面切分为多个tile,利用上一帧的深度计算tile的world position,然后计算出tile贡献最大的2个点光,使得每个顶点/像素仅需计算2个点光。Tiled Point Light使得我们开销降低(iphone 5s也可使用),大大丰富了场景的光照效果
觉得还算可行,虽然也有很大限制,但是低端机就别要什么自行车了,因此现在有时间写下。 我选用Unity SRP,比较灵活方便,不用额外Depth Pass,用上一帧depth buffer的就行,低端机嘛,能省就省,而且可以和高端机那些裁剪方案无缝切换;用的是render-pipelines.universal@7.4.1改的方案,URP这东西出了这么久我觉得还是个半成品,还不如将Buildin放出来,毕竟比较完整。
第一步、写个Pixel Shader,Pixel Shader怎么分tile,我现在都搞不懂,用mipmap?而且我觉得没多大必要,我是逐像素计算的,主要是用_CameraDepthTexture,生成ndc坐标,再转化成世界坐标,然后比较点光源(spot light可以Cone包围起来,当点光源计算,我这里没做)与该世界坐标的距离。具体代码如下
  1. float4 frag(Varyings input) : SV_Target
  2. {
  3.         float depthValue = SAMPLE_TEXTURE2D(_CameraDepthTexture, sampler_CameraDepthTexture, input.depthuv).r;
  4.         int lightIndexR = 0;
  5.         int lightIndexG = 0;
  6.         float4 ndc = float4(inputuv.x * 2 - 1, input.uv.y * 2 - 1, depthValue, 1);
  7.         float4 worldPos = mul(unity_MatrixInvVP, ndc);
  8.         worldPos /= worldPos.w;
  9.         //取最近的两盏灯光索引
  10.         float LightDistanceR = MAX_POINT_LIGHT_DISTANCE;
  11.         float LightDistanceG = MAX_POINT_LIGHT_DISTANCE;
  12.         for (int i = 0; i < _AdditionalLightsCount.x; i++)
  13.         {
  14.                 float3 tolight = worldPos.xyz - _AdditionalLightsPosition[i].xyz;
  15.                 float lightSqr = dot(tolight, tolight);
  16.                 if (lightSqr > _AdditionalLightsRange[i]* _AdditionalLightsRange[i])
  17.                         continue;
  18.                 if (lightSqr < LightDistanceR)
  19.                 {
  20.                         LightDistanceG = LightDistanceR;
  21.                         lightIndexG = lightIndexR;
  22.                         LightDistanceR = lightSqr;
  23.                         lightIndexR = i + 1;
  24.                 }
  25.                 else if(lightSqr < LightDistanceG)
  26.                 {
  27.                         LightDistanceG = lightSqr;
  28.                         lightIndexG = i + 1;
  29.                 }
  30.         }
  31.         return float4(lightIndexR * POINT_NUM, lightIndexG * POINT_NUM,0,1);
  32. }
复制代码
第二步,写个LightCullPass,生成m_LightIndexTexture,代码如下
  1. using System;
  2. namespace UnityEngine.Rendering.Universal.Internal
  3. {
  4.     /// <summary>
  5.     /// Render all objects that have a 'DepthOnly' pass into the given depth buffer.
  6.     ///
  7.     /// You can use this pass to prime a depth buffer for subsequent rendering.
  8.     /// Use it as a z-prepass, or use it to generate a depth buffer.
  9.     /// </summary>
  10.     public class LightCullPass : ScriptableRenderPass
  11.     {
  12.         Material m_SamplingMaterial;
  13.         private RenderTargetIdentifier source { get; set; }
  14.         private RenderTargetHandle destination { get; set; }
  15.         const string m_ProfilerTag = "LightCull Prepass";
  16.         ProfilingSampler m_ProfilingSampler = new ProfilingSampler(m_ProfilerTag);
  17.         /// <summary>
  18.         /// Create the DepthOnlyPass
  19.         /// </summary>
  20.         public LightCullPass(RenderPassEvent evt, Material samplingMaterial)
  21.         {
  22.             m_SamplingMaterial = samplingMaterial;
  23.             renderPassEvent = evt;
  24.         }
  25.         /// <summary>
  26.         /// Configure the pass
  27.         /// </summary>
  28.         public void Setup(
  29.             RenderTargetIdentifier source, RenderTargetHandle destination)
  30.         {
  31.             this.source = source;
  32.             this.destination = destination;
  33.         }
  34.         public bool Setup(ref RenderingData renderingData)
  35.         {
  36.             int lightCount = renderingData.lightData.additionalLightsCount;
  37.             if (lightCount == 0)
  38.                 return false;
  39.             return true;
  40.         }
  41.         public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
  42.         {
  43.             RenderTextureDescriptor descriptor = cameraTextureDescriptor;
  44.             descriptor.colorFormat = RenderTextureFormat.RGB565;// RenderTextureFormat.RGInt;
  45.             descriptor.bindMS = false;
  46.             descriptor.enableRandomWrite = false;
  47.             descriptor.depthBufferBits = 0;
  48.             descriptor.sRGB = false;
  49.             descriptor.useMipMap = false;
  50.             descriptor.dimension = TextureDimension.Tex2D;
  51.             cmd.GetTemporaryRT(destination.id, descriptor, FilterMode.Point);
  52.         }
  53.         /// <inheritdoc/>
  54.         public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
  55.         {
  56.             if (m_SamplingMaterial == null)
  57.             {
  58.                 Debug.LogErrorFormat("Missing {0}. {1} render pass will not execute. Check for missing reference in the renderer resources.", m_SamplingMaterial, GetType().Name);
  59.                 return;
  60.             }
  61.             CommandBuffer cmd = CommandBufferPool.Get(m_ProfilerTag);
  62.             RenderTargetIdentifier opaqueColorRT = destination.Identifier();
  63.    
  64.             Blit(cmd, source, opaqueColorRT, m_SamplingMaterial);
  65.             context.ExecuteCommandBuffer(cmd);
  66.             CommandBufferPool.Release(cmd);
  67.         }
  68.         /// <inheritdoc/>
  69.         public override void FrameCleanup(CommandBuffer cmd)
  70.         {
  71.             if (cmd == null)
  72.                 throw new ArgumentNullException("cmd");
  73.             if (destination != RenderTargetHandle.CameraTarget)
  74.             {
  75.                 cmd.ReleaseTemporaryRT(destination.id);
  76.                 destination = RenderTargetHandle.CameraTarget;
  77.             }
  78.         }
  79.     }
  80. }
复制代码
第三步,在shader里计算光源,先计算ScreenPosition UV,取得光源索引,按正常计算就行
  1. inputData.screenPosition = (input.clipPosition.xy / input.clipPosition.w) * 0.5 + 0.5;
  2. float4 lightCull = SAMPLE_TEXTURE2D(_LightIndexTexture, sampler_LightIndexTexture, inputData.screenPosition);
  3. int lightIndexR = round(lightCull.r * MaxNum);
  4. int lightIndexG = round(lightCull.g * MaxNum);
复制代码
最后我们看看最终效果
项目奉上,这是我临时写的,我们项目也没用这种,肯定有一些问题,如果大家有些好的方案可以分享下。
  1. 链接:https://pan.baidu.com/s/1esO8XgsOafdKKiXLDH4i3w
  2. 提取码:62w2
复制代码

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2021-2-18 09:47 | 显示全部楼层
当前有手机游戏用延迟了吗
发表于 2021-2-18 09:52 | 显示全部楼层
手游目前应该没有
发表于 2021-2-18 09:52 | 显示全部楼层
马上就会有了
发表于 2021-2-18 09:56 | 显示全部楼层
感谢分享!
这个有测过实机效率吗?尤其是cull pass大概花了几毫秒呢?全场景视锥内大概有多少盏点光呢?
我看到这么大个loop,感觉不太妙啊
发表于 2021-2-18 09:58 | 显示全部楼层
frag里分tile,可以第一个pass取16*16像素的zmax和zmin然后保存到一张rg_float上,第二个pass再做剔除
发表于 2021-2-18 10:05 | 显示全部楼层
这是嫌用户手机电池太大了吗……
发表于 2021-2-18 10:06 | 显示全部楼层
我觉得……只要有ES3,还是上F+吧,毕竟代码写起来方便,ES2的手机……还要啥光源
发表于 2021-2-18 10:13 | 显示全部楼层
我们的延迟管线经过优化带宽消耗非常低,消耗比forward低多了
发表于 2021-2-18 10:13 | 显示全部楼层
没有,这只是个小demo,实际要应用到项目还得大量验证和调整。手游同屏二三十盏灯,已经是算多的了。
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-5-5 00:21 , Processed in 0.107062 second(s), 28 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表