diff --git a/Bundle/Linux/fonts b/Bundle/Linux/fonts index 46a5993..2b430d2 100644 Binary files a/Bundle/Linux/fonts and b/Bundle/Linux/fonts differ diff --git a/Bundle/Linux/scenes b/Bundle/Linux/scenes index 63982c7..f156633 100644 Binary files a/Bundle/Linux/scenes and b/Bundle/Linux/scenes differ diff --git a/Bundle/Win64/fonts b/Bundle/Win64/fonts index 0b8a578..063685d 100644 Binary files a/Bundle/Win64/fonts and b/Bundle/Win64/fonts differ diff --git a/Bundle/Win64/scenes b/Bundle/Win64/scenes index c8fe424..8f65543 100644 Binary files a/Bundle/Win64/scenes and b/Bundle/Win64/scenes differ diff --git a/Core/RenderManager.cs b/Core/RenderManager.cs index e258e98..b4b9692 100644 --- a/Core/RenderManager.cs +++ b/Core/RenderManager.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Threading; using System.Buffers; +using System.Diagnostics.CodeAnalysis; using ADOFAIRenderer.UI; using ADOFAIRenderer.Util; using ADOFAIRenderer.Util.FFmpeg; @@ -52,6 +53,14 @@ namespace ADOFAIRenderer.Core public int throttleCount; public bool isThrottling; + [SuppressMessage("ReSharper", "InconsistentNaming")] + public enum BitrateControlMode + { + CBR, + VBR, + CQP + } + public struct RenderSettings { public double TargetFPS; @@ -60,13 +69,19 @@ namespace ADOFAIRenderer.Core public int RenderWidth; public int RenderHeight; + public BitrateControlMode BitrateMode; public int Bitrate; + public int MaxBitrate; + public int QP; + public string Codec; public double EndWaitTime; public bool ShowHitText; public int? RandomSeed; - public RenderSettings(double targetFPS, double videoFPS, int renderWidth, int renderHeight, int bitrate, string codec, double endWaitTime, bool showHitText, int? randomSeed) + public RenderSettings(double targetFPS, double videoFPS, int renderWidth, int renderHeight, + BitrateControlMode bitrateMode, int bitrate, int maxBitrate, int qp, + string codec, double endWaitTime, bool showHitText, int? randomSeed) { TargetFPS = targetFPS; VideoFPS = videoFPS; @@ -74,7 +89,11 @@ namespace ADOFAIRenderer.Core RenderWidth = renderWidth; RenderHeight = renderHeight; + BitrateMode = bitrateMode; Bitrate = bitrate; + MaxBitrate = maxBitrate; + QP = qp; + Codec = codec; EndWaitTime = endWaitTime; ShowHitText = showHitText; @@ -440,7 +459,10 @@ namespace ADOFAIRenderer.Core var targetFps = Configuration.TargetFPS; var videoFps = Configuration.VideoFPS; - float bitrate = Configuration.Bitrate; + var bitrateMode = Configuration.BitrateMode; + var bitrate = Configuration.Bitrate; + var maxBitrate = Configuration.MaxBitrate; + var qp = Configuration.QP; var codecStr = Configuration.Codec; var selectedCodec = VideoFFmpeg.Codec.H264_Default; @@ -478,7 +500,7 @@ namespace ADOFAIRenderer.Core { _allVideoFFmpegInstances.Add(videoFFmpeg); } - videoFFmpeg.Start(_currentOutPath, bitrate); + videoFFmpeg.Start(_currentOutPath, bitrateMode, bitrate, maxBitrate, qp); while (true) { diff --git a/UI/RenderButton.cs b/UI/RenderButton.cs index 77100f9..7cbd592 100644 --- a/UI/RenderButton.cs +++ b/UI/RenderButton.cs @@ -15,16 +15,24 @@ namespace ADOFAIRenderer.UI [SerializeField] private TMP_InputField videoFPSInput = null!; [SerializeField] private TMP_InputField renderWidthInput = null!; [SerializeField] private TMP_InputField renderHeightInput = null!; - [SerializeField] private TMP_InputField bitrateInput = null!; [SerializeField] private TMP_InputField waitAfterEndInput = null!; [SerializeField] private TMP_InputField randomSeedInput = null!; - [Header("Dropdown")] + [Header("Dropdowns")] [SerializeField] private TMP_Dropdown codecDropdown = null!; - - [Header("Toggle")] + [SerializeField] private TMP_Dropdown bitrateModeDropdown = null!; + + [Header("Toggles")] [SerializeField] private Toggle showHitTextToggle = null!; - + + [Header("Bitrate Dynamic UI Container")] + [SerializeField] private RectTransform bitrateContainer = null!; + + // Dynamically instantiated inputs + private TMP_InputField _bitrateInput = null!; + private TMP_InputField _maxBitrateInput = null!; + private TMP_InputField _qpInput = null!; + private Button _buttonComponent = null!; private void Start() @@ -34,11 +42,93 @@ namespace ADOFAIRenderer.UI RenderMod.FixEventSystem(); - if (codecDropdown == null) return; - codecDropdown.ClearOptions(); + if (codecDropdown != null) + { + codecDropdown.ClearOptions(); + var options = System.Enum.GetNames(typeof(VideoFFmpeg.Codec)).ToList(); + codecDropdown.AddOptions(options); + } + + // 1. Initialize Bitrate Mode Dropdown + if (bitrateModeDropdown != null) + { + bitrateModeDropdown.ClearOptions(); + var modeOptions = System.Enum.GetNames(typeof(RenderManager.BitrateControlMode)).ToList(); + bitrateModeDropdown.AddOptions(modeOptions); + bitrateModeDropdown.onValueChanged.AddListener(delegate { UpdateUIByMode(); }); + } + + // 2. Dynamically instantiate input fields using targetFPSInput as a styling template + if (bitrateContainer != null && targetFPSInput != null) + { + // Instantiate bitrateInput + var bitrateInputObj = Instantiate(targetFPSInput.gameObject, bitrateContainer); + bitrateInputObj.name = "Dynamic_BitrateInput"; + _bitrateInput = bitrateInputObj.GetComponent(); + _bitrateInput.text = "15000"; + SetPlaceholderText(_bitrateInput, "비트레이트 (kbps)"); + + // Instantiate maxBitrateInput + var maxBitrateInputObj = Instantiate(targetFPSInput.gameObject, bitrateContainer); + maxBitrateInputObj.name = "Dynamic_MaxBitrateInput"; + _maxBitrateInput = maxBitrateInputObj.GetComponent(); + _maxBitrateInput.text = "30000"; + SetPlaceholderText(_maxBitrateInput, "최대 비트레이트 (kbps)"); + + // Instantiate qpInput + var qpInputObj = Instantiate(targetFPSInput.gameObject, bitrateContainer); + qpInputObj.name = "Dynamic_QPInput"; + _qpInput = qpInputObj.GetComponent(); + _qpInput.text = "18"; + SetPlaceholderText(_qpInput, "QP (0-51) / CRF"); + } + + // 3. Perform initial UI state updates based on default mode + UpdateUIByMode(); + } + + private void SetPlaceholderText(TMP_InputField inputField, string text) + { + if (inputField.placeholder == null) return; + var tmpText = inputField.placeholder.GetComponent(); - var options = System.Enum.GetNames(typeof(VideoFFmpeg.Codec)).ToList(); - codecDropdown.AddOptions(options); + if (tmpText) + { + tmpText.text = text; + } + } + + private void UpdateUIByMode() + { + if (bitrateModeDropdown == null) return; + if (_bitrateInput == null || _maxBitrateInput == null || _qpInput == null) return; + + var selectedMode = (RenderManager.BitrateControlMode)bitrateModeDropdown.value; + + switch (selectedMode) + { + case RenderManager.BitrateControlMode.CBR: + _bitrateInput.gameObject.SetActive(true); + _maxBitrateInput.gameObject.SetActive(false); + _qpInput.gameObject.SetActive(false); + break; + case RenderManager.BitrateControlMode.VBR: + _bitrateInput.gameObject.SetActive(true); + _maxBitrateInput.gameObject.SetActive(true); + _qpInput.gameObject.SetActive(false); + break; + case RenderManager.BitrateControlMode.CQP: + _bitrateInput.gameObject.SetActive(false); + _maxBitrateInput.gameObject.SetActive(false); + _qpInput.gameObject.SetActive(true); + break; + } + + if (bitrateContainer != null) + { + Canvas.ForceUpdateCanvases(); + LayoutRebuilder.ForceRebuildLayoutImmediate(bitrateContainer); + } } private void Update() @@ -74,11 +164,49 @@ namespace ADOFAIRenderer.UI Melon.Logger.Error($"올바르지 않은 Render Height 값입니다. 1에서 {SystemInfo.maxTextureSize} 사이의 정수여야 합니다."); return; } - if (!int.TryParse(bitrateInput.text, out var bitrate) || bitrate <= 0) + + var bitrateMode = RenderManager.BitrateControlMode.CBR; + if (bitrateModeDropdown != null) { - Melon.Logger.Error("올바르지 않은 Bitrate 값입니다. 0보다 큰 정수여야 합니다."); - return; + bitrateMode = (RenderManager.BitrateControlMode)bitrateModeDropdown.value; } + + var bitrate = 0; + var maxBitrate = 0; + var qp = 18; + + if (bitrateMode == RenderManager.BitrateControlMode.CBR || bitrateMode == RenderManager.BitrateControlMode.VBR) + { + if (!int.TryParse(_bitrateInput.text, out bitrate) || bitrate <= 0) + { + Melon.Logger.Error("올바르지 않은 Bitrate 값입니다. 0보다 큰 정수여야 합니다."); + return; + } + } + + if (bitrateMode == RenderManager.BitrateControlMode.VBR) + { + if (!int.TryParse(_maxBitrateInput.text, out maxBitrate) || maxBitrate <= 0) + { + Melon.Logger.Error("올바르지 않은 Max Bitrate 값입니다. 0보다 큰 정수여야 합니다."); + return; + } + if (maxBitrate < bitrate) + { + Melon.Logger.Error("Max Bitrate는 Target Bitrate보다 크거나 같아야 합니다."); + return; + } + } + + if (bitrateMode == RenderManager.BitrateControlMode.CQP) + { + if (!int.TryParse(_qpInput.text, out qp) || qp < 0 || qp > 51) + { + Melon.Logger.Error("올바르지 않은 QP 값입니다. 0에서 51 사이의 정수여야 합니다."); + return; + } + } + if (!double.TryParse(waitAfterEndInput.text, out var waitAfterEnd) || waitAfterEnd < 0) { Melon.Logger.Error("올바르지 않은 Wait After End 값입니다. 0 이상의 숫자여야 합니다."); @@ -109,18 +237,20 @@ namespace ADOFAIRenderer.UI if (RenderManager.Instance) { - RenderManager.Instance.Configuration = new RenderManager.RenderSettings - { - TargetFPS = targetFPS, - VideoFPS = videoFPS, - RenderWidth = renderWidth, - RenderHeight = renderHeight, - Bitrate = bitrate, - Codec = selectedCodecName, - EndWaitTime = waitAfterEnd, - ShowHitText = showHitText, - RandomSeed = parsedRandomSeed - }; + RenderManager.Instance.Configuration = new RenderManager.RenderSettings( + targetFPS, + videoFPS, + renderWidth, + renderHeight, + bitrateMode, + bitrate, + maxBitrate, + qp, + selectedCodecName, + waitAfterEnd, + showHitText, + parsedRandomSeed + ); } RenderManager.Instance!.SetupCaptureDirectory(); @@ -133,12 +263,13 @@ namespace ADOFAIRenderer.UI if (string.IsNullOrWhiteSpace(videoFPSInput.text)) return true; if (string.IsNullOrWhiteSpace(renderWidthInput.text)) return true; if (string.IsNullOrWhiteSpace(renderHeightInput.text)) return true; - if (string.IsNullOrWhiteSpace(bitrateInput.text)) return true; if (string.IsNullOrWhiteSpace(waitAfterEndInput.text)) return true; - // 드롭다운은 기본적으로 항상 무언가 선택되어 있지만(0번 항목 등), - // 만약 '선택 안함(-1)' 같은 예외 처리가 필요하다면 아래처럼 검사할 수 있습니다. - return codecDropdown.value < 0; + if (_bitrateInput != null && _bitrateInput.gameObject.activeSelf && string.IsNullOrWhiteSpace(_bitrateInput.text)) return true; + if (_maxBitrateInput != null && _maxBitrateInput.gameObject.activeSelf && string.IsNullOrWhiteSpace(_maxBitrateInput.text)) return true; + if (_qpInput != null && _qpInput.gameObject.activeSelf && string.IsNullOrWhiteSpace(_qpInput.text)) return true; + + return codecDropdown.value < 0 || (bitrateModeDropdown != null && bitrateModeDropdown.value < 0); } } } \ No newline at end of file diff --git a/Util/FFmpeg/VideoFFmpeg.cs b/Util/FFmpeg/VideoFFmpeg.cs index c5ddb9d..2cbf0e1 100644 --- a/Util/FFmpeg/VideoFFmpeg.cs +++ b/Util/FFmpeg/VideoFFmpeg.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.IO; +using ADOFAIRenderer.Core; namespace ADOFAIRenderer.Util.FFmpeg { @@ -55,14 +56,49 @@ namespace ADOFAIRenderer.Util.FFmpeg VideoCodec = codec; } - public void Start(string path, float bitrate) + public void Start(string path, RenderManager.BitrateControlMode mode, int bitrate, int maxBitrate, int qp) { if (File.Exists(path)) { File.Delete(path); } - var command = "-y -f rawvideo -vcodec rawvideo -pixel_format rgba -colorspace rgb -color_range 2 -video_size " + Width + "x" + Height + " -framerate " + FrameRate + " -threads 4 -loglevel quiet -i - " + Codec2String(VideoCodec) + " -pix_fmt yuv420p -fps_mode cfr " + " -r " + VideoFPS + $" -b:v {(int)bitrate}k" + " \"" + path + "\""; + + string bitrateArgs = mode switch + { + RenderManager.BitrateControlMode.CBR => $"-b:v {bitrate}k -minrate {bitrate}k -maxrate {bitrate}k -bufsize {bitrate * 2}k", + RenderManager.BitrateControlMode.VBR => $"-b:v {bitrate}k -maxrate {maxBitrate}k -bufsize {maxBitrate * 2}k", + RenderManager.BitrateControlMode.CQP => GetCqpArgs(VideoCodec, qp), + _ => $"-b:v {bitrate}k" + }; + + var command = "-y -f rawvideo -vcodec rawvideo -pixel_format rgba -colorspace rgb -color_range 2 -video_size " + + Width + "x" + Height + " -framerate " + FrameRate + " -threads 4 -loglevel quiet -i - " + + Codec2String(VideoCodec) + " " + bitrateArgs + " -pix_fmt yuv420p -fps_mode cfr -r " + + VideoFPS + " \"" + path + "\""; + Execute(command); } + + private static string GetCqpArgs(Codec codec, int qp) + { + return codec switch + { + Codec.H264_NVIDIA => $"-rc constqp -qp {qp}", + Codec.HEVC_NVIDIA => $"-rc constqp -qp {qp}", + Codec.AV1_NVIDIA => $"-rc constqp -qp {qp}", + Codec.VP9_NVIDIA => $"-rc constqp -qp {qp}", + + Codec.H264_QSV => $"-global_quality {qp}", + Codec.HEVC_QSV => $"-global_quality {qp}", + Codec.AV1_QSV => $"-global_quality {qp}", + + Codec.H264_Default => $"-crf {qp}", + Codec.HEVC_Default => $"-crf {qp}", + Codec.AV1_Default => $"-crf {qp}", + Codec.VP9_Default => $"-crf {qp}", + + _ => $"-qp {qp}" + }; + } } }