From 0793a410359996bdf0a18fc1a699d3c0f0b488af247c5676763bea8069c97e11 Mon Sep 17 00:00:00 2001 From: copecone Date: Sat, 13 Jun 2026 21:40:27 +0900 Subject: [PATCH] Add Encoding Config UI --- UI/RenderConfigUIManager.cs | 204 ++++++++++++++++++++++++++++- Util/FFmpeg/FFmpegNativeEncoder.cs | 2 +- 2 files changed, 204 insertions(+), 2 deletions(-) diff --git a/UI/RenderConfigUIManager.cs b/UI/RenderConfigUIManager.cs index a5351e8..1df6898 100644 --- a/UI/RenderConfigUIManager.cs +++ b/UI/RenderConfigUIManager.cs @@ -20,6 +20,7 @@ namespace ADOFAIRenderer.UI private Button _goRenderButton = null!; private Button _encodingConfigButton = null!; + private VisualElement _encodingConfigContainer = null!; private void Awake() { @@ -92,7 +93,208 @@ namespace ADOFAIRenderer.UI private void OpenEncodingConfig() { Melon.Logger.Msg("[UI] OpenEncodingConfig 호출됨"); - RenderManager.Instance.Configuration.EncodingConfig ??= GenerateEncodingConfig(_codecBinder.Value); + + var config = RenderManager.Instance.Configuration; + config.EncodingConfig ??= GenerateEncodingConfig(_codecBinder.Value); + + var tempConfig = new EncodingConfig + { + RateControlMode = config.EncodingConfig.RateControlMode, + Bitrate = config.EncodingConfig.Bitrate, + MaxBitrate = config.EncodingConfig.MaxBitrate, + QP = config.EncodingConfig.QP + }; + + if (_root.Contains(_encodingConfigContainer)) + { + _root.Remove(_encodingConfigContainer); + } + + // 1. 전체 화면 컨테이너 생성 및 배경 적용 + _encodingConfigContainer = new VisualElement { name = "EncodingSettingsContainer" }; + _encodingConfigContainer.style.position = Position.Absolute; + _encodingConfigContainer.style.width = Length.Percent(100); + _encodingConfigContainer.style.height = Length.Percent(100); + _encodingConfigContainer.style.left = 0; + _encodingConfigContainer.style.top = 0; + _encodingConfigContainer.style.backgroundColor = new StyleColor(new Color(0.15f, 0.15f, 0.15f, 1.0f)); + _encodingConfigContainer.style.justifyContent = Justify.Center; + _encodingConfigContainer.style.alignItems = Align.Center; + + // 로우 생성 헬퍼 (UXML의 ui-row 속성 복제) + VisualElement CreateRow() + { + var row = new VisualElement(); + row.AddToClassList("ui-row"); + row.style.flexDirection = FlexDirection.Row; + row.style.height = 130; + row.style.maxHeight = 130; + row.style.minHeight = 80; + row.style.justifyContent = Justify.Center; + row.style.alignItems = Align.Center; + return row; + } + + // 라벨 생성 헬퍼 (우측 정렬 적용하여 인풋과의 인접성 향상) + Label CreateFormLabel(string text) + { + var label = new Label(text); + label.style.unityTextAlign = TextAnchor.MiddleRight; + label.style.color = new StyleColor(Color.white); + return label; + } + + // 2. 타이틀 로우 + var titleRow = CreateRow(); + var title = new Label("인코딩 설정"); + title.style.fontSize = 36; + title.style.unityFontStyleAndWeight = FontStyle.Bold; + title.style.color = new StyleColor(Color.white); + title.style.unityTextAlign = TextAnchor.MiddleCenter; + title.style.width = 770; // 360 * 2 + 50 (여백)에 맞춤 + title.style.height = StyleKeyword.Auto; + titleRow.Add(title); + _encodingConfigContainer.Add(titleRow); + + // 3. 전송률 모드 로우 (라벨 + 드롭다운) + var modeRow = CreateRow(); + var modeLabel = CreateFormLabel("전송률 제어 모드"); + var modeDropdown = new DropdownField { label = "" }; + foreach (RateControlMode modeVal in System.Enum.GetValues(typeof(RateControlMode))) + { + modeDropdown.choices.Add(modeVal.ToString()); + } + modeDropdown.value = tempConfig.RateControlMode.ToString(); + + modeRow.Add(modeLabel); + modeRow.Add(modeDropdown); + _encodingConfigContainer.Add(modeRow); + + // 4. Bitrate 로우 (라벨 + 입력) + var bitrateRow = CreateRow(); + var bitrateLabel = CreateFormLabel("대상 비트레이트 (kbps)"); + var bitrateField = new IntegerField { name = "EncodingBitrate", label = "", value = tempConfig.Bitrate }; + bitrateRow.Add(bitrateLabel); + bitrateRow.Add(bitrateField); + _encodingConfigContainer.Add(bitrateRow); + + // 5. Max Bitrate 로우 (라벨 + 입력) + var maxBitrateRow = CreateRow(); + var maxBitrateLabel = CreateFormLabel("최대 비트레이트 (kbps)"); + var maxBitrateField = new IntegerField { name = "EncodingMaxBitrate", label = "", value = tempConfig.MaxBitrate }; + maxBitrateRow.Add(maxBitrateLabel); + maxBitrateRow.Add(maxBitrateField); + _encodingConfigContainer.Add(maxBitrateRow); + + // 6. QP 로우 (라벨 + 입력) + var qpRow = CreateRow(); + var qpLabel = CreateFormLabel("QP 품질 값 (0 ~ 51)"); + var qpField = new IntegerField { name = "EncodingQP", label = "", value = tempConfig.QP }; + qpRow.Add(qpLabel); + qpRow.Add(qpField); + _encodingConfigContainer.Add(qpRow); + + // 7. QP = 0 (무손실) 경고 라벨 로우 생성 (ui-row 클래스를 상속받아 NanumSquareNeo 폰트가 적용되도록 처리) + var warningRow = CreateRow(); + warningRow.style.height = StyleKeyword.Auto; + warningRow.style.maxHeight = StyleKeyword.Null; + warningRow.style.minHeight = StyleKeyword.Null; + + var warningLabel = new Label("※ QP 0 (무손실)은 일부 하드웨어/코덱에서 지원되지 않을 수 있으며, 인코딩 실패 시 CPU 모드로 폴백됩니다."); + warningLabel.style.fontSize = 20; // 30px에서 20px로 오버라이드 + warningLabel.style.color = new StyleColor(new Color(0.9f, 0.6f, 0.2f)); + warningLabel.style.whiteSpace = WhiteSpace.Normal; + warningLabel.style.width = 720; // 360px에서 720px로 오버라이드 + warningLabel.style.height = StyleKeyword.Auto; + warningLabel.style.unityTextAlign = TextAnchor.MiddleCenter; + warningLabel.style.marginTop = 10; + warningLabel.style.marginBottom = 10; + + warningRow.Add(warningLabel); + _encodingConfigContainer.Add(warningRow); + + // 7.5. 유효성 검사기 설정 (기존에 작성된 validator 재사용 및 이전 값 복원) + var encodingValidator = new IntegerInputValidator(_encodingConfigContainer); + encodingValidator + .AddLimits("EncodingBitrate", new RangeIntegerLimit(100, 1000000)) + .AddLimits("EncodingMaxBitrate", new RangeIntegerLimit(100, 1000000)) + .AddLimits("EncodingQP", new RangeIntegerLimit(0, 51)); + encodingValidator.SetupValidation(); + + // 가시성 필터링 업데이트 (행 단위로 보임/숨김 제어) + void UpdateVisibility(string selectedModeStr) + { + if (System.Enum.TryParse(selectedModeStr, out var mode)) + { + bitrateRow.style.display = (mode == RateControlMode.CBR || mode == RateControlMode.VBR) ? DisplayStyle.Flex : DisplayStyle.None; + maxBitrateRow.style.display = (mode == RateControlMode.VBR) ? DisplayStyle.Flex : DisplayStyle.None; + qpRow.style.display = (mode == RateControlMode.CQP) ? DisplayStyle.Flex : DisplayStyle.None; + warningRow.style.display = (mode == RateControlMode.CQP && qpField.value == 0) ? DisplayStyle.Flex : DisplayStyle.None; + } + } + + modeDropdown.RegisterValueChangedCallback(evt => UpdateVisibility(evt.newValue)); + qpField.RegisterValueChangedCallback(evt => + { + warningRow.style.display = (modeDropdown.value == "CQP" && evt.newValue == 0) ? DisplayStyle.Flex : DisplayStyle.None; + }); + + UpdateVisibility(modeDropdown.value); + + // 화면 전환 제어 + void ToggleScreens(bool showEncoding) + { + foreach (var child in _root.Children()) + { + if (child == _encodingConfigContainer) continue; + child.style.display = showEncoding ? DisplayStyle.None : DisplayStyle.Flex; + } + _encodingConfigContainer.style.display = showEncoding ? DisplayStyle.Flex : DisplayStyle.None; + } + + // 8. 제어 버튼 로우 (취소 / 저장) + var buttonRow = CreateRow(); + var cancelBtn = new Button { text = "취소" }; + var saveBtn = new Button { text = "저장" }; + + buttonRow.Add(cancelBtn); + buttonRow.Add(saveBtn); + _encodingConfigContainer.Add(buttonRow); + + cancelBtn.RegisterCallback(evt => + { + ToggleScreens(false); + _root.Remove(_encodingConfigContainer); + _encodingConfigContainer = null!; + }); + + saveBtn.RegisterCallback(evt => + { + if (System.Enum.TryParse(modeDropdown.value, out var mode)) + { + tempConfig.RateControlMode = mode; + } + tempConfig.Bitrate = bitrateField.value; + tempConfig.MaxBitrate = maxBitrateField.value; + tempConfig.QP = qpField.value; + + if (tempConfig.RateControlMode == RateControlMode.VBR && tempConfig.MaxBitrate < tempConfig.Bitrate) + { + tempConfig.MaxBitrate = tempConfig.Bitrate; + } + + config.EncodingConfig = tempConfig; + + ToggleScreens(false); + _root.Remove(_encodingConfigContainer); + _encodingConfigContainer = null!; + Melon.Logger.Msg($"[UI] EncodingConfig 저장 완료"); + }); + + _root.Add(_encodingConfigContainer); + + // 화면 전환 활성화 + ToggleScreens(true); } private void TryGoRender() diff --git a/Util/FFmpeg/FFmpegNativeEncoder.cs b/Util/FFmpeg/FFmpegNativeEncoder.cs index 9118d96..00f641e 100644 --- a/Util/FFmpeg/FFmpegNativeEncoder.cs +++ b/Util/FFmpeg/FFmpegNativeEncoder.cs @@ -772,7 +772,7 @@ namespace ADOFAIRenderer.Util.FFmpeg case RateControlMode.CQP: // 기본값 18: H.264에서 "시각적으로 거의 무손실"에 가까운 품질. // 다른 코덱에서도 비슷한 수준의 품질을 제공합니다. - var qpValue = _settings.QualityValue > 0 ? _settings.QualityValue : 18; + var qpValue = (_settings.QualityValue >= 0 && _settings.QualityValue <= 51) ? _settings.QualityValue : 18; if (encoderName.Contains("nvenc")) {