using System; using System.Collections.Generic; using NiloToon.NiloToonURP; using UnityEngine; namespace Streamingle.Background { /// /// 시간 흐름에 따라 스카이박스 블렌딩 + 디렉셔널 라이트를 제어하는 컨트롤러. /// 3단계(Stage1→Stage2→Stage3) 전환을 timeOfDay(0~1)로 통합 제어. /// [ExecuteAlways]로 에디터에서도 실시간 프리뷰 가능. /// [ExecuteAlways] public class SkyboxTimeController : MonoBehaviour { // ───────────────────────── References ───────────────────────── [Tooltip("제어할 디렉셔널 라이트")] public Light directionalLight; [Tooltip("Skybox/Cubemap Blend 머티리얼")] public Material skyboxMaterial; // ───────────────────────── Time ───────────────────────── [Range(0f, 1f)] [Tooltip("0=Stage1, 0.5=Stage2, 1=Stage3")] public float timeOfDay = 0f; [Tooltip("자동 재생 여부")] public bool autoPlay = false; [Tooltip("한 사이클(0→1) 소요 시간(초)")] public float cycleDuration = 60f; [Tooltip("자동 재생 시 루프 여부")] public bool loop = true; // ───────────────────────── Stage Settings ───────────────────────── public StageSettings stage1 = new StageSettings { lightColor = new Color(0.4f, 0.45f, 0.65f), lightIntensity = 0.3f, lightRotation = new Vector3(10f, -30f, 0f), skyboxExposure = 0.5f, skyboxTint = Color.white, }; public StageSettings stage2 = new StageSettings { lightColor = new Color(1f, 0.95f, 0.85f), lightIntensity = 1.2f, lightRotation = new Vector3(50f, -30f, 0f), skyboxExposure = 1.0f, skyboxTint = Color.white, }; public StageSettings stage3 = new StageSettings { lightColor = new Color(1f, 0.55f, 0.3f), lightIntensity = 0.6f, lightRotation = new Vector3(5f, 150f, 0f), skyboxExposure = 0.7f, skyboxTint = new Color(1f, 0.85f, 0.75f), }; // ───────────────────────── Custom Material Properties ───────────────────────── [Tooltip("머티리얼의 임의 프로퍼티를 3단계로 보간 제어")] public List materialPropertyOverrides = new List(); // ───────────────────────── NiloToon Light Override ───────────────────────── [Tooltip("NiloToon 캐릭터 메인 라이트 오버라이더 (디렉셔널 라이트와 동기화)")] public NiloToonCharacterMainLightOverrider niloToonLightOverrider; [Tooltip("NiloToon 라이트 컬러를 별도로 지정할지 여부 (false면 디렉셔널 라이트와 동일)")] public bool niloToonSeparateColor = false; public Color niloToonColorStage1 = Color.white; public Color niloToonColorStage2 = Color.white; public Color niloToonColorStage3 = Color.white; [Tooltip("NiloToon 라이트 Intensity를 별도로 지정할지 여부 (false면 디렉셔널 라이트와 동일)")] public bool niloToonSeparateIntensity = false; public float niloToonIntensityStage1 = 1f; public float niloToonIntensityStage2 = 1f; public float niloToonIntensityStage3 = 1f; // ───────────────────────── Ambient ───────────────────────── [Tooltip("Ambient 컬러도 함께 보간")] public bool controlAmbient = true; public Color ambientStage1 = new Color(0.15f, 0.15f, 0.25f); public Color ambientStage2 = new Color(0.45f, 0.45f, 0.45f); public Color ambientStage3 = new Color(0.3f, 0.2f, 0.15f); // ───────────────────────── Private ───────────────────────── private Material _instanceMaterial; private static readonly int BlendProp = Shader.PropertyToID("_Blend"); private static readonly int TintProp = Shader.PropertyToID("_Tint"); private static readonly int ExposureProp = Shader.PropertyToID("_Exposure"); // ───────────────────────── Lifecycle ───────────────────────── private void OnEnable() { SetupMaterial(); } private void OnDisable() { CleanupMaterial(); } private void Update() { if (autoPlay && Application.isPlaying && cycleDuration > 0f) { timeOfDay += Time.deltaTime / cycleDuration; if (loop) timeOfDay %= 1f; else timeOfDay = Mathf.Clamp01(timeOfDay); } Apply(); } private void OnValidate() { SetupMaterial(); Apply(); } // ───────────────────────── Core ───────────────────────── public void Apply() { float t = Mathf.Clamp01(timeOfDay); StageSettings current = EvaluateStage(t); // ── Skybox ── Material mat = GetActiveMaterial(); if (mat != null) { mat.SetFloat(BlendProp, t); mat.SetColor(TintProp, current.skyboxTint); mat.SetFloat(ExposureProp, current.skyboxExposure); } // ── Custom Material Properties ── foreach (var prop in materialPropertyOverrides) { if (prop.targetMaterial == null || string.IsNullOrEmpty(prop.propertyName)) continue; if (!prop.targetMaterial.HasProperty(prop.propertyName)) continue; int id = Shader.PropertyToID(prop.propertyName); switch (prop.propertyType) { case MaterialPropertyType.Float: { float v = t < 0.5f ? Mathf.Lerp(prop.floatStage1, prop.floatStage2, t * 2f) : Mathf.Lerp(prop.floatStage2, prop.floatStage3, (t - 0.5f) * 2f); prop.targetMaterial.SetFloat(id, v); break; } case MaterialPropertyType.Color: { Color c = t < 0.5f ? Color.Lerp(prop.colorStage1, prop.colorStage2, t * 2f) : Color.Lerp(prop.colorStage2, prop.colorStage3, (t - 0.5f) * 2f); prop.targetMaterial.SetColor(id, c); break; } case MaterialPropertyType.Vector: { Vector4 v = t < 0.5f ? Vector4.Lerp(prop.vectorStage1, prop.vectorStage2, t * 2f) : Vector4.Lerp(prop.vectorStage2, prop.vectorStage3, (t - 0.5f) * 2f); prop.targetMaterial.SetVector(id, v); break; } } } // ── Directional Light ── if (directionalLight != null) { directionalLight.color = current.lightColor; directionalLight.intensity = current.lightIntensity; directionalLight.transform.rotation = Quaternion.Euler(current.lightRotation); } // ── NiloToon Light Override ── if (niloToonLightOverrider != null) { // Direction: 디렉셔널 라이트와 동일한 rotation 적용 niloToonLightOverrider.transform.rotation = Quaternion.Euler(current.lightRotation); // Color if (niloToonSeparateColor) { niloToonLightOverrider.color = t < 0.5f ? Color.Lerp(niloToonColorStage1, niloToonColorStage2, t * 2f) : Color.Lerp(niloToonColorStage2, niloToonColorStage3, (t - 0.5f) * 2f); } else { niloToonLightOverrider.color = current.lightColor; } // Intensity if (niloToonSeparateIntensity) { niloToonLightOverrider.intensity = t < 0.5f ? Mathf.Lerp(niloToonIntensityStage1, niloToonIntensityStage2, t * 2f) : Mathf.Lerp(niloToonIntensityStage2, niloToonIntensityStage3, (t - 0.5f) * 2f); } else { niloToonLightOverrider.intensity = current.lightIntensity; } } // ── Ambient ── if (controlAmbient) { Color amb = t < 0.5f ? Color.Lerp(ambientStage1, ambientStage2, t * 2f) : Color.Lerp(ambientStage2, ambientStage3, (t - 0.5f) * 2f); RenderSettings.ambientLight = amb; } } /// timeOfDay를 직접 지정 (0~1) public void SetTime(float t) { timeOfDay = Mathf.Clamp01(t); Apply(); } /// Stage 1 (timeOfDay = 0) 으로 즉시 이동 public void GoToStage1() => SetTime(0f); /// Stage 2 (timeOfDay = 0.5) 으로 즉시 이동 public void GoToStage2() => SetTime(0.5f); /// Stage 3 (timeOfDay = 1) 으로 즉시 이동 public void GoToStage3() => SetTime(1f); /// 지정한 Stage로 duration초 동안 부드럽게 블렌딩 public void BlendToStage(int stage, float duration) { float target = stage switch { 1 => 0f, 2 => 0.5f, 3 => 1f, _ => Mathf.Clamp01((stage - 1) * 0.5f), }; BlendTo(target, duration); } /// Stage 1으로 duration초 동안 블렌딩 public void BlendToStage1(float duration) => BlendTo(0f, duration); /// Stage 2로 duration초 동안 블렌딩 public void BlendToStage2(float duration) => BlendTo(0.5f, duration); /// Stage 3으로 duration초 동안 블렌딩 public void BlendToStage3(float duration) => BlendTo(1f, duration); /// 임의 timeOfDay 값으로 duration초 동안 블렌딩 public void BlendTo(float targetTime, float duration) { if (_blendCoroutine != null) StopCoroutine(_blendCoroutine); _blendCoroutine = StartCoroutine(BlendCoroutine(targetTime, duration)); } /// 현재 진행 중인 블렌딩 중단 public void StopBlend() { if (_blendCoroutine != null) { StopCoroutine(_blendCoroutine); _blendCoroutine = null; } } private Coroutine _blendCoroutine; private System.Collections.IEnumerator BlendCoroutine(float target, float duration) { float start = timeOfDay; float elapsed = 0f; duration = Mathf.Max(0.001f, duration); while (elapsed < duration) { elapsed += Time.deltaTime; timeOfDay = Mathf.Lerp(start, target, elapsed / duration); Apply(); yield return null; } timeOfDay = target; Apply(); _blendCoroutine = null; } public Material GetActiveMaterial() { return _instanceMaterial != null ? _instanceMaterial : skyboxMaterial; } // ───────────────────────── Helpers ───────────────────────── private StageSettings EvaluateStage(float t) { if (t < 0.5f) return StageSettings.Lerp(stage1, stage2, t * 2f); else return StageSettings.Lerp(stage2, stage3, (t - 0.5f) * 2f); } private void SetupMaterial() { if (skyboxMaterial == null) return; if (Application.isPlaying) { if (_instanceMaterial == null || _instanceMaterial.shader != skyboxMaterial.shader) { CleanupMaterial(); _instanceMaterial = new Material(skyboxMaterial); _instanceMaterial.name = skyboxMaterial.name + " (Instance)"; } } else { _instanceMaterial = skyboxMaterial; } RenderSettings.skybox = _instanceMaterial; } private void CleanupMaterial() { if (_instanceMaterial != null && _instanceMaterial != skyboxMaterial) { if (Application.isPlaying) Destroy(_instanceMaterial); else DestroyImmediate(_instanceMaterial); } _instanceMaterial = null; } // ───────────────────────── Data Structures ───────────────────────── [Serializable] public struct StageSettings { public Color lightColor; public float lightIntensity; public Vector3 lightRotation; public float skyboxExposure; public Color skyboxTint; public static StageSettings Lerp(StageSettings a, StageSettings b, float t) { return new StageSettings { lightColor = Color.Lerp(a.lightColor, b.lightColor, t), lightIntensity = Mathf.Lerp(a.lightIntensity, b.lightIntensity, t), lightRotation = LerpRotation(a.lightRotation, b.lightRotation, t), skyboxExposure = Mathf.Lerp(a.skyboxExposure, b.skyboxExposure, t), skyboxTint = Color.Lerp(a.skyboxTint, b.skyboxTint, t), }; } private static Vector3 LerpRotation(Vector3 a, Vector3 b, float t) { return Quaternion.Slerp(Quaternion.Euler(a), Quaternion.Euler(b), t).eulerAngles; } } public enum MaterialPropertyType { Float, Color, Vector, } [Serializable] public class MaterialPropertyOverride { [Tooltip("제어할 머티리얼")] public Material targetMaterial; [Tooltip("셰이더 프로퍼티 이름 (예: _Metallic, _Color)")] public string propertyName; public MaterialPropertyType propertyType = MaterialPropertyType.Float; // Float public float floatStage1; public float floatStage2; public float floatStage3; // Color public Color colorStage1 = Color.white; public Color colorStage2 = Color.white; public Color colorStage3 = Color.white; // Vector public Vector4 vectorStage1; public Vector4 vectorStage2; public Vector4 vectorStage3; } } }