using UnityEngine; using UnityEngine.Rendering; using Unity.Cinemachine; namespace Streamingle.StreamingleControl.Extensions { /// /// Cinemachine Extension for controlling Beautify post-processing effects. /// This extension allows each Virtual Camera to have its own Beautify settings /// that will be applied when the camera becomes active, similar to CinemachineVolumeSettings. /// [ExecuteAlways] [AddComponentMenu("Cinemachine/Procedural/Extensions/Cinemachine Beautify Volume Settings")] [SaveDuringPlay] [DisallowMultipleComponent] public class CinemachineBeautifyVolumeSettings : CinemachineExtension { /// /// This is the priority for the Beautify volume. It's set to a high number /// to ensure that it overrides other volumes for the active vcam. /// public static float s_VolumePriority = 1001f; /// /// The weight that the Beautify profile will have when the camera is fully active. /// It will blend to and from 0 along with the camera. /// [Tooltip("This weight will be applied to the Volume when this camera is active")] public float Weight = 1f; /// The reference object for focus tracking public enum FocusTrackingMode { /// No focus tracking None, /// Focus offset is relative to the LookAt target LookAtTarget, /// Focus offset is relative to the Follow target FollowTarget, /// Focus offset is relative to the Custom target set here CustomTarget, /// Focus offset is relative to the camera Camera } /// If DOF is enabled, will set the focus distance to be the distance /// from the selected target to the camera. The Focus Offset field will then modify that distance [Tooltip("If DOF is enabled, will set the focus distance to be the distance from the selected target to the camera. The Focus Offset field will then modify that distance.")] public FocusTrackingMode FocusTracking = FocusTrackingMode.None; /// The target to use if Focus Tracks Target is set to Custom Target [Tooltip("The target to use if Focus Tracks Target is set to Custom Target")] public Transform FocusTarget; /// Offset from target distance, to be used with Focus Tracks Target. /// Offsets the sharpest point away from the focus target [Tooltip("Offset from target distance, to be used with Focus Tracks Target. Offsets the sharpest point away from the focus target.")] public float FocusOffset = 0f; /// /// If Focus tracking is enabled, this will return the calculated focus distance /// public float CalculatedFocusDistance { get; private set; } /// /// This profile will be applied whenever this virtual camera is live. /// If null, a default profile will be created automatically. /// [Tooltip("This Beautify profile will be applied whenever this virtual camera is live")] public VolumeProfile Profile; [Header("DOF Settings")] [Tooltip("Enable or disable DOF effect")] public bool EnableDOF = false; [Tooltip("DOF distance value")] public float DOFDistance = 1f; [Tooltip("DOF focal length")] public float FocalLength = 0.3f; [Tooltip("DOF aperture value")] public float Aperture = 5.6f; [Tooltip("Enable bokeh effect")] public bool EnableBokeh = true; [Tooltip("Bokeh threshold")] public float BokehThreshold = 1f; [Tooltip("Bokeh intensity")] public float BokehIntensity = 2f; [Tooltip("Enable foreground blur")] public bool EnableForegroundBlur = true; [Tooltip("Foreground blur distance")] public float ForegroundDistance = 0.5f; // Private fields for profile management class VcamExtraState : VcamExtraStateBase { public VolumeProfile ProfileCopy; public void CreateProfileCopy(VolumeProfile source) { DestroyProfileCopy(); if (source == null) return; VolumeProfile profile = ScriptableObject.CreateInstance(); profile.name = source.name + " (Copy)"; try { for (int i = 0; i < source.components.Count; ++i) { if (source.components[i] != null) { var itemCopy = UnityEngine.Object.Instantiate(source.components[i]); if (itemCopy != null) { itemCopy.hideFlags = HideFlags.DontSave; profile.components.Add(itemCopy); } } } profile.isDirty = true; ProfileCopy = profile; } catch (System.Exception ex) { UnityEngine.Debug.LogWarning($"[CinemachineBeautifyVolumeSettings] Failed to create profile copy: {ex.Message}"); if (profile != null) { if (Application.isPlaying) UnityEngine.Object.Destroy(profile); else UnityEngine.Object.DestroyImmediate(profile); } } } public void DestroyProfileCopy() { if (ProfileCopy != null) { try { // First destroy all components to avoid reference issues for (int i = ProfileCopy.components.Count - 1; i >= 0; i--) { if (ProfileCopy.components[i] != null) { if (Application.isPlaying) UnityEngine.Object.Destroy(ProfileCopy.components[i]); else UnityEngine.Object.DestroyImmediate(ProfileCopy.components[i]); } } ProfileCopy.components.Clear(); // Then destroy the profile itself if (Application.isPlaying) UnityEngine.Object.Destroy(ProfileCopy); else UnityEngine.Object.DestroyImmediate(ProfileCopy); } catch (System.Exception ex) { UnityEngine.Debug.LogWarning($"[CinemachineBeautifyVolumeSettings] Error destroying profile copy: {ex.Message}"); } finally { ProfileCopy = null; } } } } private System.Type m_BeautifyType; private object m_BeautifyOverride; /// True if the profile is enabled and nontrivial public bool IsValid => Profile != null && Profile.components.Count > 0; protected override void Awake() { base.Awake(); InitializeBeautify(); } protected override void OnEnable() { InvalidateCachedProfile(); } protected override void OnDestroy() { InvalidateCachedProfile(); base.OnDestroy(); } /// Called by the editor when the shared asset has been edited public void InvalidateCachedProfile() { var extraStateCache = new System.Collections.Generic.List(); GetAllExtraStates(extraStateCache); for (int i = 0; i < extraStateCache.Count; ++i) extraStateCache[i].DestroyProfileCopy(); } protected override void PostPipelineStageCallback( CinemachineVirtualCameraBase vcam, CinemachineCore.Stage stage, ref CameraState state, float deltaTime) { // Set the focus after the camera has been fully positioned. if (stage == CinemachineCore.Stage.Finalize) { var extra = GetExtraState(vcam); // Only apply volume settings if this camera is live (active) bool isLive = CinemachineCore.IsLive(vcam); if (!IsValid || !isLive) extra.DestroyProfileCopy(); else { var profile = Profile; // Handle Focus Tracking if (FocusTracking == FocusTrackingMode.None) extra.DestroyProfileCopy(); else { if (extra.ProfileCopy == null) extra.CreateProfileCopy(Profile); profile = extra.ProfileCopy; } // Calculate and apply focus distance for DOF (always if Focus Tracking is not None) if (FocusTracking != FocusTrackingMode.None && profile != null) { var beautifyOverride = FindBeautifyOverride(profile); if (beautifyOverride != null) { float focusDistance = FocusOffset; if (FocusTracking == FocusTrackingMode.LookAtTarget) focusDistance += (state.GetFinalPosition() - state.ReferenceLookAt).magnitude; else { Transform focusTarget = null; switch (FocusTracking) { default: break; case FocusTrackingMode.FollowTarget: focusTarget = vcam.Follow; break; case FocusTrackingMode.CustomTarget: focusTarget = FocusTarget; break; } if (focusTarget != null) focusDistance += (state.GetFinalPosition() - focusTarget.position).magnitude; } CalculatedFocusDistance = focusDistance = Mathf.Max(0, focusDistance); SetBeautifyParameter(beautifyOverride, "depthOfFieldDistance", focusDistance); state.Lens.PhysicalProperties.FocusDistance = focusDistance; profile.isDirty = true; } } // Update all Beautify parameters in the profile UpdateBeautifyParameters(profile); // Apply the post-processing state.AddCustomBlendable(new CameraState.CustomBlendableItems.Item { Custom = profile, Weight = Weight }); } } } private void InitializeBeautify() { // Find Beautify type m_BeautifyType = System.Type.GetType("Beautify.Universal.Beautify, Unity.RenderPipelines.Universal.Runtime"); if (m_BeautifyType == null) { UnityEngine.Debug.LogError("[CinemachineBeautifyVolumeSettings] Beautify 타입을 찾을 수 없습니다. Beautify 에셋이 설치되어 있는지 확인하세요."); return; } UnityEngine.Debug.Log("[CinemachineBeautifyVolumeSettings] Beautify type found successfully"); } private void UpdateBeautifyParameters(VolumeProfile profile) { if (profile == null || m_BeautifyType == null) return; // Find or create Beautify override in the profile object beautifyOverride = FindBeautifyOverride(profile); if (beautifyOverride != null) { // Update all Beautify parameters SetBeautifyParameter(beautifyOverride, "depthOfField", EnableDOF); // Use calculated focus distance if Focus Tracking is active, otherwise use DOFDistance float finalDOFDistance = (FocusTracking != FocusTrackingMode.None) ? CalculatedFocusDistance : DOFDistance; SetBeautifyParameter(beautifyOverride, "depthOfFieldDistance", finalDOFDistance); SetBeautifyParameter(beautifyOverride, "depthOfFieldFocalLength", FocalLength); SetBeautifyParameter(beautifyOverride, "depthOfFieldAperture", Aperture); SetBeautifyParameter(beautifyOverride, "depthOfFieldBokeh", EnableBokeh); SetBeautifyParameter(beautifyOverride, "depthOfFieldBokehThreshold", BokehThreshold); SetBeautifyParameter(beautifyOverride, "depthOfFieldBokehIntensity", BokehIntensity); SetBeautifyParameter(beautifyOverride, "depthOfFieldForegroundBlur", EnableForegroundBlur); SetBeautifyParameter(beautifyOverride, "depthOfFieldForegroundDistance", ForegroundDistance); profile.isDirty = true; } } private object FindBeautifyOverride(VolumeProfile profile) { if (profile == null || m_BeautifyType == null) return null; // Try to find existing Beautify override in the profile for (int i = 0; i < profile.components.Count; i++) { if (profile.components[i] != null && profile.components[i].GetType() == m_BeautifyType) { return profile.components[i]; } } return null; } private void SetBeautifyParameter(object beautifyOverride, string fieldName, object value) { if (beautifyOverride == null || m_BeautifyType == null) return; var field = m_BeautifyType.GetField(fieldName); if (field != null) { var fieldValue = field.GetValue(beautifyOverride); if (fieldValue != null) { var valueProperty = fieldValue.GetType().GetProperty("value"); var overrideProperty = fieldValue.GetType().GetProperty("overrideState"); if (valueProperty != null && overrideProperty != null) { // Enable override state overrideProperty.SetValue(fieldValue, true); // Set value with type checking if (valueProperty.PropertyType == value.GetType() || (valueProperty.PropertyType == typeof(bool) && value is bool) || (valueProperty.PropertyType == typeof(float) && value is float)) { valueProperty.SetValue(fieldValue, value); } } } } } void OnValidate() { Weight = Mathf.Max(0, Weight); } void Reset() { Weight = 1; FocusTracking = FocusTrackingMode.None; FocusTarget = null; FocusOffset = 0; Profile = null; EnableDOF = false; DOFDistance = 1f; FocalLength = 0.3f; Aperture = 5.6f; EnableBokeh = true; BokehThreshold = 1f; BokehIntensity = 2f; EnableForegroundBlur = true; ForegroundDistance = 0.5f; } } }