在HDRP中如何自定义一个天空盒渲染算法
解读
国内一线厂面试问“HDRP自定义天空盒”并不是想听你背官方文档,而是验证三件事:
- 是否真正理解HDRP的可编程渲染管线(SRP)架构,知道天空盒不是“贴个CubeMap”那么简单,而是要走Volume Framework、Render Pass、Shader Graph HDRP Master Stack三条主线;
- 能否把算法从CPU搬到GPU,并无缝接入HDRP的Lighting Loop,保证云影、大气散射、昼夜动画都能随曝光、雾效、TAA一起生效;
- 是否具备移动端落地思维:在Quest3、iPhone15这种TBDR芯片上,自定义天空的带宽、ALU、Fetch次数必须可控,否则主程会直接挂电话。
知识点
- HDRP天空盒的Volume System扩展点:
- 继承
SkySettings→生成SkyRenderer→注册SkyManager; - 必须实现
IBuiltinDataProvider接口,把太阳方向、大气透射率写进BuiltinParameters,否则后续Shadow Map、Screen Space Reflection会采样不到。
- 继承
- Render Graph集成:
- 在
HDRP Sky Renderer里不是直接CommandBuffer.Blit,而是向RenderGraph申请一个SkyBuffer的TextureHandle,用Compute Shader或Fullscreen Shader画完再SetGlobalTexture(_SkyTexture); - 必须标记
TextureDesc.clearBuffer = false,否则Unity2022.3以后会把你的天空当成GBuffer被额外Clear一次,帧率直接掉5%。
- 在
- Shader Graph HDRP Master的“Unlit + Sky”模板:
- 把
Color Mode改成Backplate,关闭Exposure Weight,否则后期Exposure Volume会二次乘你的天空亮度; - 用
Custom Function Node引用Nishita.hlsl或Hillaire2019大气代码,记得在Node上声明#pragma multi_compile _ DIRECTIONAL_LIGHT_SHADOW,不然太阳阴影会丢失。
- 把
- 性能红线:
- 国内安卓旗舰(骁龙8 Gen2)2560×1140分辨率下,自定义天空的GPU Cost必须<0.6 ms,Bandwidth<200 MB/s;
- 若用Preetham或Hosek-Wilkie解析模型,建议把Rayleigh/Mie系数烘焙到128×64×6的3D LUT,运行时只采样一次,Texel Fetch从3次降到1次。
- 热更新兼容性:
- 天空算法若需策划动态调参,要把
SkySettings打成ScriptableObject并走Addressable; - 注意
SkyRenderer实例缓存在HDRenderPipeline.m_SkyManager的List<SkyRenderer>里,热更后必须调用SkyManager.UpdateSkyRenderer重新注册,否则新代码不会生效。
- 天空算法若需策划动态调参,要把
答案
步骤化落地,面试时按“骨架→细节→性能”三层回答,时间控制在3分钟内:
-
创建
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); } -
实现
MySkyRenderer.cs,核心在Build与Render: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); } } -
注册到HDRP:
在工程任意[RuntimeInitializeOnLoadMethod]里调用SkyManager.RegisterSkyRenderer<MySkyRenderer>(); -
性能兜底:
- 在
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。
- 在
拓展思考
- 如果项目要跑Meta Quest3的Vulkan Mobile HDRP分支,Tile-Based Deferred对天空带宽极度敏感,需要把天空拆成两级:
- 第一级每帧更新一张256×128的EquiRect天空,用Compute Shader画;
- 第二级用Cubemap做IBL Specular,只在时间片空闲时异步更新,避免GPU Stall。
- 当策划提出“天空要随真实GPS时间变化”时,建议把太阳方向计算放到C#端,用
System.DateTime.ToOADate算出Julian Day,再喂给MySkySettings.sunDirection,这样GPU端只用做一次dot(N, L),省去重复三角函数。 - 国内部分厂商(如米哈游)在Unity2022.3+LTS基础上做了自定义SRP Fork,把
SkyRenderer接口改成了Job System + Burst调度,面试时可以主动提及:若公司也做了Fork,需要把MySkyRenderer.Build里的Material创建改到主线程,否则会在Burst Compile阶段触发UnityException。