实现支持不同渲染管线的天空盒曝光度控制组件(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;
        }
    }
}

使用说明

  1. 附加组件:在主 Camera 或空 GameObject 上附加 SkyboxExposureController。
  2. 参数调整:Inspector 中设置 targetExposure(EV 值),transitionDuration(渐变秒数)。
  3. 事件绑定:onExposureChanged 可连接 UI Slider 或脚本。
  4. 测试:运行时修改 targetExposure,观察天空盒亮度变化。
  5. 自定义曲线: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 集成,请提供更多细节!

类似文章

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注