实现支持不同渲染管线的天空盒曝光度控制组件(SkyboxExposureController)——参数化控制
【Unity笔记】基于不同渲染管线的天空盒曝光度控制组件(SkyboxExposureController)——参数化控制设计与实现
引言
天空盒曝光度控制是 Unity 项目中优化视觉效果的关键功能,尤其在动态天气、日夜循环或 HDR 渲染场景中。通过参数化组件 SkyboxExposureController,可以实时调整天空盒的亮度/曝光值,支持 Built-in Render Pipeline(内置管线)、Universal Render Pipeline (URP) 和 High Definition Render Pipeline (HDRP)。在 Built-in 中,通过自定义着色器或环境光调整;在 URP/HDRP 中,利用 Volume 系统和 Exposure override 实现精确控制。
设计原则:
- 跨管线兼容:运行时检测当前渲染管线(RenderPipelineManager.currentPipeline),动态切换实现逻辑。
- 参数化控制:暴露曝光值(float, 范围 -10 ~ 10 EV)、渐变时间(缓动动画)和事件回调。
- 性能优化:使用协程平滑过渡,避免每帧计算;支持 Burst 编译(可选)。
- 编辑器支持:自定义 Inspector 预览和测试按钮。
- 局限性:天空盒材质需支持曝光(如 Cubemap HDR);URP/HDRP 需要 Volume 组件。
- 适用版本:Unity 2021.3+(推荐 2022.3 LTS),需导入 URP/HDRP 包(Package Manager)。
时间复杂度:O(1)(每帧仅 lerp 值)。适用于 XR 项目(如 VR 环境光调整),可扩展到雾效或全局照明。
设计原理
- Built-in RP:天空盒曝光通过 RenderSettings.ambientIntensity 或自定义 Skybox 材质的 _Exposure 参数调整(需修改标准 Skybox 着色器)。
- URP/HDRP:使用 Volume Profile 的 Exposure component,动态修改 fixedEV 或 postExposure 值,支持物理基曝光。
- 参数化:曝光值(EV)作为输入,映射到对应 API;渐变使用 Mathf.Lerp 或 AnimationCurve。
- 检测管线:通过 GraphicsSettings.currentRenderPipeline 或 typeof(UniversalRenderPipelineAsset) 判断。
- 事件系统:UnityEvent 回调曝光变化,支持 UI Slider 绑定。
- 优化:协程驱动渐变,避免 Update 每帧计算;编辑器模式下实时预览。
核心组件实现(SkyboxExposureController.cs)
附加到任意 GameObject(推荐 Camera 或全局 Manager)。
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal; // URP 需导入
using UnityEngine.Rendering.HighDefinition; // HDRP 需导入
using UnityEngine.Events;
using System.Collections;
public class SkyboxExposureController : MonoBehaviour
{
[System.Serializable]
public class ExposureEvent : UnityEvent<float> { } // 曝光值变化事件
[Header("曝光设置")]
[Range(-10f, 10f)] public float targetExposure = 0f; // EV 值
public float transitionDuration = 1f; // 渐变时长
public AnimationCurve transitionCurve = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f);
[Header("事件")]
public ExposureEvent onExposureChanged;
private float currentExposure = 0f;
private Coroutine transitionCoroutine;
private Volume globalVolume; // URP/HDRP 用
private enum RenderPipelineType { BuiltIn, URP, HDRP, Unknown }
private RenderPipelineType currentPipeline = RenderPipelineType.Unknown;
void Awake()
{
DetectRenderPipeline();
InitializeExposureSystem();
}
void Start()
{
SetExposure(targetExposure, false); // 初始设置,无渐变
}
private void DetectRenderPipeline()
{
if (GraphicsSettings.currentRenderPipeline == null)
{
currentPipeline = RenderPipelineType.BuiltIn;
}
else if (GraphicsSettings.currentRenderPipeline.GetType().ToString().Contains("Universal"))
{
currentPipeline = RenderPipelineType.URP;
}
else if (GraphicsSettings.currentRenderPipeline.GetType().ToString().Contains("HighDefinition"))
{
currentPipeline = RenderPipelineType.HDRP;
}
else
{
currentPipeline = RenderPipelineType.Unknown;
Debug.LogWarning("未知渲染管线,无法控制曝光");
}
}
private void InitializeExposureSystem()
{
switch (currentPipeline)
{
case RenderPipelineType.URP:
case RenderPipelineType.HDRP:
// 创建全局 Volume
GameObject volumeObj = new GameObject("GlobalExposureVolume");
volumeObj.transform.SetParent(transform);
globalVolume = volumeObj.AddComponent<Volume>();
globalVolume.priority = 999f; // 高优先级
globalVolume.profile = ScriptableObject.CreateInstance<VolumeProfile>();
// 添加 Exposure override
if (currentPipeline == RenderPipelineType.URP)
{
Exposure urpExposure;
globalVolume.profile.TryGet(out urpExposure);
if (urpExposure == null)
urpExposure = globalVolume.profile.Add<Exposure>(true);
}
else // HDRP
{
Exposure hdrpExposure;
globalVolume.profile.TryGet(out hdrpExposure);
if (hdrpExposure == null)
hdrpExposure = globalVolume.profile.Add<Exposure>(true);
}
break;
}
}
/// <summary>
/// 设置曝光值
/// </summary>
/// <param name="ev">曝光值 (EV)</param>
/// <param name="smooth">是否渐变</param>
public void SetExposure(float ev, bool smooth = true)
{
if (transitionCoroutine != null)
StopCoroutine(transitionCoroutine);
if (smooth && transitionDuration > 0f)
{
transitionCoroutine = StartCoroutine(SmoothTransition(currentExposure, ev));
}
else
{
ApplyExposure(ev);
}
}
private IEnumerator SmoothTransition(float startEV, float endEV)
{
float elapsed = 0f;
while (elapsed < transitionDuration)
{
elapsed += Time.deltaTime;
float t = transitionCurve.Evaluate(elapsed / transitionDuration);
float ev = Mathf.Lerp(startEV, endEV, t);
ApplyExposure(ev);
yield return null;
}
ApplyExposure(endEV);
}
private void ApplyExposure(float ev)
{
currentExposure = ev;
onExposureChanged?.Invoke(ev);
switch (currentPipeline)
{
case RenderPipelineType.BuiltIn:
// Built-in: 调整环境光强度(近似曝光)
RenderSettings.ambientIntensity = Mathf.Pow(2f, ev);
DynamicGI.UpdateEnvironment(); // 更新 GI
break;
case RenderPipelineType.URP:
if (globalVolume.profile.TryGet<Exposure>(out Exposure urpExposure))
{
urpExposure.fixedExposure.value = ev;
urpExposure.active = true;
}
break;
case RenderPipelineType.HDRP:
if (globalVolume.profile.TryGet<Exposure>(out Exposure hdrpExposure))
{
hdrpExposure.fixedExposure.value = ev;
hdrpExposure.active = true;
}
break;
}
}
}
使用说明
- 附加组件:在主 Camera 或空 GameObject 上附加 SkyboxExposureController。
- 参数调整:Inspector 中设置 targetExposure(EV 值),transitionDuration(渐变秒数)。
- 事件绑定:onExposureChanged 可连接 UI Slider 或脚本。
- 测试:运行时修改 targetExposure,观察天空盒亮度变化。
- 自定义曲线:transitionCurve 支持 EaseInOut 等缓动。
编辑器扩展(SkyboxExposureControllerEditor.cs)
放置在 Editor 文件夹,提供实时预览按钮。
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(SkyboxExposureController))]
public class SkyboxExposureControllerEditor : Editor
{
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
SkyboxExposureController controller = (SkyboxExposureController)target;
EditorGUILayout.Space();
if (GUILayout.Button("预览曝光变化"))
{
controller.SetExposure(controller.targetExposure, true);
}
if (GUILayout.Button("重置曝光"))
{
controller.SetExposure(0f, false);
}
}
}
#endif
高级扩展
基于距离/时间的自动化控制
public class AutoExposureController : SkyboxExposureController
{
[Header("自动控制")]
public Transform target; // 目标物体
public float maxDistance = 100f;
public float minEV = -5f;
public float maxEV = 5f;
void Update()
{
if (target != null)
{
float distance = Vector3.Distance(transform.position, target.position);
float t = Mathf.Clamp01(distance / maxDistance);
float ev = Mathf.Lerp(minEV, maxEV, t);
SetExposure(ev, true);
}
}
}
日夜循环集成
public class DayNightExposure : SkyboxExposureController
{
[Header("日夜循环")]
public AnimationCurve dayNightCurve; // 时间 -> EV
void Update()
{
float timeOfDay = (Time.time % 86400f) / 86400f; // 模拟 24 小时
float ev = dayNightCurve.Evaluate(timeOfDay);
SetExposure(ev, true);
}
}
性能优化与注意事项
- 优化:渐变协程仅在变化时运行;大场景避免每帧检测。
- 兼容:Built-in 需要自定义 Skybox 材质支持 _Exposure(修改 Shader);URP/HDRP 需全局 Volume Layer。
- 测试:编辑器 Play 模式下观察曝光变化;移动端检查性能(Volume 开销)。
- 常见问题:如果无效果,检查 Skybox 材质 HDR 支持,或 Volume Profile 优先级。
此 SkyboxExposureController 提供跨管线参数化曝光控制,支持动态视觉调整。如果需要着色器修改示例或 XR 集成,请提供更多细节!