在HDRP中如何自定义一个天空盒渲染算法

解读

国内一线厂面试问“HDRP自定义天空盒”并不是想听你背官方文档,而是验证三件事:

  1. 是否真正理解HDRP的可编程渲染管线(SRP)架构,知道天空盒不是“贴个CubeMap”那么简单,而是要走Volume FrameworkRender PassShader Graph HDRP Master Stack三条主线;
  2. 能否把算法从CPU搬到GPU,并无缝接入HDRP的Lighting Loop,保证云影、大气散射、昼夜动画都能随曝光、雾效、TAA一起生效;
  3. 是否具备移动端落地思维:在Quest3、iPhone15这种TBDR芯片上,自定义天空的带宽、ALU、Fetch次数必须可控,否则主程会直接挂电话。

知识点

  1. HDRP天空盒的Volume System扩展点:
    • 继承SkySettings→生成SkyRenderer→注册SkyManager
    • 必须实现IBuiltinDataProvider接口,把太阳方向、大气透射率写进BuiltinParameters,否则后续Shadow MapScreen Space Reflection会采样不到。
  2. Render Graph集成:
    • HDRP Sky Renderer里不是直接CommandBuffer.Blit,而是向RenderGraph申请一个SkyBufferTextureHandle,用Compute ShaderFullscreen Shader画完再SetGlobalTexture(_SkyTexture)
    • 必须标记TextureDesc.clearBuffer = false,否则Unity2022.3以后会把你的天空当成GBuffer被额外Clear一次,帧率直接掉5%。
  3. Shader Graph HDRP Master的“Unlit + Sky”模板:
    • Color Mode改成Backplate,关闭Exposure Weight,否则后期Exposure Volume会二次乘你的天空亮度;
    • Custom Function Node引用Nishita.hlslHillaire2019大气代码,记得在Node上声明#pragma multi_compile _ DIRECTIONAL_LIGHT_SHADOW,不然太阳阴影会丢失。
  4. 性能红线
    • 国内安卓旗舰(骁龙8 Gen2)2560×1140分辨率下,自定义天空的GPU Cost必须<0.6 ms,Bandwidth<200 MB/s;
    • 若用PreethamHosek-Wilkie解析模型,建议把Rayleigh/Mie系数烘焙到128×64×6的3D LUT,运行时只采样一次,Texel Fetch从3次降到1次。
  5. 热更新兼容性
    • 天空算法若需策划动态调参,要把SkySettings打成ScriptableObject并走Addressable
    • 注意SkyRenderer实例缓存在HDRenderPipeline.m_SkyManagerList<SkyRenderer>里,热更后必须调用SkyManager.UpdateSkyRenderer重新注册,否则新代码不会生效。

答案

步骤化落地,面试时按“骨架→细节→性能”三层回答,时间控制在3分钟内:

  1. 创建MySkySettings.cs,继承SkySettings,声明公开参数:

    [Serializable, VolumeComponentMenu("Sky/MySky")]
    public class MySkySettings : SkySettings {
        public ClampedFloatParameter rayleigh = new(1, 0, 5);
        public ColorParameter mieColor = new(Color.white, false, true, true);
        // 必须重写的抽象属性
        public override Type GetSkyRendererType() => typeof(MySkyRenderer);
    }
    
  2. 实现MySkyRenderer.cs,核心在BuildRender

    class MySkyRenderer : SkyRenderer {
        Material m_SkyMat; // 用Shader Graph生成的HDRP/UnlitSky
        int m_SkyParamID = Shader.PropertyToID("_MySkyParams");
    
        public override void Build() {
            m_SkyMat = CoreUtils.CreateEngineMaterial("Hidden/MyHDRPSky");
        }
        public override void Render(BuiltinSkyParameters builtinParams) {
            var cmd = builtinParams.commandBuffer;
            // 把参数一次性传GPU,减少SetVector调用
            Vector4 p = new Vector4(settings.rayleigh.value, settings.mieColor.value.r, 0, 0);
            cmd.SetGlobalVector(m_SkyParamID, p);
            // 申请RT,大小与屏幕一致,格式R11G11B10_F
            var skyTarget = builtinParams.colorBuffer;
            CoreUtils.SetRenderTarget(cmd, skyTarget);
            CoreUtils.DrawFullScreen(cmd, m_SkyMat);
        }
    }
    
  3. 注册到HDRP:
    在工程任意[RuntimeInitializeOnLoadMethod]里调用

    SkyManager.RegisterSkyRenderer<MySkyRenderer>();
    
  4. 性能兜底:

    • MySkySettings里加[AdditionalData]标记,让美术在Volume Profile里直接调参;
    • Render里判断builtinParams.camera.cameraType == CameraType.Game才画,SceneView用Unity内置天空,节省编辑器开销;
    • 真机用Unity Frame Debugger抓帧,确保天空Pass在AfterOpaqueAndSky里只占0.4 ms,超标就把3D LUT降到64×32×6。

拓展思考

  1. 如果项目要跑Meta Quest3Vulkan Mobile HDRP分支,Tile-Based Deferred对天空带宽极度敏感,需要把天空拆成两级
    • 第一级每帧更新一张256×128的EquiRect天空,用Compute Shader画;
    • 第二级用CubemapIBL Specular,只在时间片空闲时异步更新,避免GPU Stall
  2. 当策划提出“天空要随真实GPS时间变化”时,建议把太阳方向计算放到C#端,用System.DateTime.ToOADate算出Julian Day,再喂给MySkySettings.sunDirection,这样GPU端只用做一次dot(N, L),省去重复三角函数。
  3. 国内部分厂商(如米哈游)在Unity2022.3+LTS基础上做了自定义SRP Fork,把SkyRenderer接口改成了Job System + Burst调度,面试时可以主动提及:若公司也做了Fork,需要把MySkyRenderer.Build里的Material创建改到主线程,否则会在Burst Compile阶段触发UnityException