Files
ADOFAIRenderer/UI/RenderConfigUIManager.cs
T
2026-08-04 22:02:53 +09:00

1166 lines
51 KiB
C#

using System.Collections.Generic;
using ADOFAIRenderer.Core;
using ADOFAIRenderer.Data;
using ADOFAIRenderer.Util.FFmpeg;
using MelonLoader;
using UnityEngine;
using UnityEngine.UIElements;
namespace ADOFAIRenderer.UI
{
public class RenderConfigUIManager : MonoBehaviour
{
[SerializeField]
private UIDocument uiDocument = null!;
private VisualElement _root = null!;
private IntegerInputValidator _intValidator = null!;
private DoubleInputValidator _doubleValidator = null!;
private Button _goRenderButton = null!;
private Button _videoEncodingConfigButton = null!;
private VisualElement _videoEncodingConfigContainer = null!;
private Button _audioEncodingConfigButton = null!;
private VisualElement _audioEncodingConfigContainer = null!;
private Button _advancedSettingsButton = null!;
private VisualElement? _advancedConfigContainer;
private void Awake()
{
_root = uiDocument.rootVisualElement;
RenderManager.Instance.Configuration = new RenderConfig();
_intValidator = new IntegerInputValidator(_root);
_doubleValidator = new DoubleInputValidator(_root);
}
private void Start()
{
// Init Validators
_intValidator
.AddLimits("RenderWidth", new PositiveOnlyIntegerLimit(), new RangeIntegerLimit(1, 7680))
.AddLimits("RenderHeight", new PositiveOnlyIntegerLimit(), new RangeIntegerLimit(1, 4320));
_doubleValidator
.AddLimits("RenderFPS", new PositiveOnlyDoubleLimit(), new RangeDoubleLimit(0, 9600))
.AddLimits("VideoFPS", new PositiveOnlyDoubleLimit(), new RangeDoubleLimit(0, 4800))
.AddLimits("DelayAfterLevelEnd", new PositiveOnlyDoubleLimit(), new RangeDoubleLimit(0, 86400));
_intValidator.SetupValidation();
_doubleValidator.SetupValidation();
// Attach Button Event Listener
_goRenderButton = _root.Q<Button>("GoRenderButton");
_goRenderButton?.RegisterCallback<ClickEvent>(_ => TryGoRender());
_videoEncodingConfigButton = _root.Q<Button>("VideoEncodingSettingsButton");
if (_videoEncodingConfigButton != null)
{
Melon<RenderMod>.Logger.Msg($"[UI] VideoEncodingSettingsButton 발견: pickingMode={_videoEncodingConfigButton.pickingMode}, enabledSelf={_videoEncodingConfigButton.enabledSelf}");
_videoEncodingConfigButton.RegisterCallback<ClickEvent>(_ => OpenVideoEncodingConfig());
}
else
{
Melon<RenderMod>.Logger.Error("Button 'VideoEncodingSettingsButton'을(를) 찾을 수 없습니다.");
}
_audioEncodingConfigButton = _root.Q<Button>("AudioEncodingSettingsButton");
if (_audioEncodingConfigButton != null)
{
Melon<RenderMod>.Logger.Msg($"[UI] AudioEncodingSettingsButton 발견: pickingMode={_audioEncodingConfigButton.pickingMode}, enabledSelf={_audioEncodingConfigButton.enabledSelf}");
_audioEncodingConfigButton.RegisterCallback<ClickEvent>(_ => OpenAudioEncodingConfig());
}
else
{
Melon<RenderMod>.Logger.Error("Button 'AudioEncodingSettingsButton'을(를) 찾을 수 없습니다.");
}
var presetSettingsButton = _root.Q<Button>("PresetSettingsButton");
if (presetSettingsButton != null)
{
Melon<RenderMod>.Logger.Msg($"[UI] PresetSettingsButton 발견: pickingMode={presetSettingsButton.pickingMode}, enabledSelf={presetSettingsButton.enabledSelf}");
presetSettingsButton.RegisterCallback<ClickEvent>(_ => OpenPresetConfig());
}
else
{
Melon<RenderMod>.Logger.Error("Button 'PresetSettingsButton'을(를) 찾을 수 없습니다.");
}
_advancedSettingsButton = _root.Q<Button>("AdvancedSettingsButton");
if (_advancedSettingsButton == null)
{
var bottomRow = CreateRow();
bottomRow.pickingMode = PickingMode.Ignore;
bottomRow.style.position = Position.Absolute;
bottomRow.style.right = 0f;
bottomRow.style.bottom = 0f;
bottomRow.style.justifyContent = Justify.FlexEnd;
_advancedSettingsButton = new Button { name = "AdvancedSettingsButton", text = "고급 설정..." };
bottomRow.Add(_advancedSettingsButton);
var targetContainer = _root.childCount > 0 ? _root[0] : _root;
targetContainer.Add(bottomRow);
Melon<RenderMod>.Logger.Msg("[UI] AdvancedSettingsButton 동적 생성 완료.");
}
else
{
_advancedSettingsButton.text = "고급 설정...";
Melon<RenderMod>.Logger.Msg($"[UI] AdvancedSettingsButton 발견: pickingMode={_advancedSettingsButton.pickingMode}, enabledSelf={_advancedSettingsButton.enabledSelf}");
}
_advancedSettingsButton.RegisterCallback<ClickEvent>(_ => OpenAdvancedConfig());
}
private void OpenAdvancedConfig()
{
var config = RenderManager.Instance.Configuration;
config.AdvancedConfig ??= GenerateAdvancedConfig();
var tempConfig = new AdvancedConfig
{
ProcessAudio = config.AdvancedConfig.ProcessAudio,
RandomSeed = config.AdvancedConfig.RandomSeed,
NonTile = config.AdvancedConfig.NonTile
};
if (_advancedConfigContainer != null && _root.Contains(_advancedConfigContainer))
{
_root.Remove(_advancedConfigContainer);
}
_advancedConfigContainer = new VisualElement
{
name = "AdvancedSettingsContainer",
style =
{
position = Position.Absolute,
width = Length.Percent(100),
height = Length.Percent(100),
left = 0,
top = 0,
backgroundColor = new StyleColor(new Color(0.15f, 0.15f, 0.15f, 1.0f)),
justifyContent = Justify.Center,
alignItems = Align.Center
}
};
// 1. 타이틀 로우
var titleRow = CreateRow();
var titleLabel = new Label("고급 설정")
{
style =
{
fontSize = 24,
color = new StyleColor(Color.white)
}
};
titleRow.Add(titleLabel);
_advancedConfigContainer.Add(titleRow);
// 2. 오디오 처리 로우
var processAudioRow = CreateRow();
var processAudioLabel = CreateFormLabel("오디오 처리");
var processAudioToggle = new Toggle { name = "AdvancedProcessAudioToggle", label = "", value = tempConfig.ProcessAudio };
processAudioRow.Add(processAudioLabel);
processAudioRow.Add(processAudioToggle);
_advancedConfigContainer.Add(processAudioRow);
// 3. 논타일 모드 로우
var nonTileRow = CreateRow();
var nonTileLabel = CreateFormLabel("논타일 모드");
var nonTileToggle = new Toggle { name = "AdvancedNonTileToggle", label = "", value = tempConfig.NonTile };
nonTileRow.Add(nonTileLabel);
nonTileRow.Add(nonTileToggle);
_advancedConfigContainer.Add(nonTileRow);
// 4. 랜덤 시드 로우
var randomSeedRow = CreateRow();
var randomSeedLabel = CreateFormLabel("랜덤 시드 (입력 선택)");
var randomSeedField = new IntegerField { name = "AdvancedRandomSeed", label = "", value = tempConfig.RandomSeed ?? 0 };
if (!tempConfig.RandomSeed.HasValue)
{
var textProperty = typeof(IntegerField).GetProperty("text", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.FlattenHierarchy);
if (textProperty == null)
{
textProperty = typeof(TextInputBaseField<int>).GetProperty("text", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.FlattenHierarchy);
}
textProperty?.SetValue(randomSeedField, string.Empty);
}
randomSeedRow.Add(randomSeedLabel);
randomSeedRow.Add(randomSeedField);
_advancedConfigContainer.Add(randomSeedRow);
// 5. 제어 버튼 로우 (취소 / 저장)
var buttonRow = CreateRow();
var cancelBtn = new Button { text = "취소" };
var saveBtn = new Button { text = "저장" };
buttonRow.Add(cancelBtn);
buttonRow.Add(saveBtn);
_advancedConfigContainer.Add(buttonRow);
cancelBtn.RegisterCallback<ClickEvent>(_ =>
{
ToggleAdvancedScreens(false);
_root.Remove(_advancedConfigContainer);
_advancedConfigContainer = null!;
});
saveBtn.RegisterCallback<ClickEvent>(_ =>
{
tempConfig.ProcessAudio = processAudioToggle.value;
tempConfig.NonTile = nonTileToggle.value;
if (!string.IsNullOrEmpty(randomSeedField.text))
{
tempConfig.RandomSeed = randomSeedField.value;
}
else
{
tempConfig.RandomSeed = null;
}
config.AdvancedConfig = tempConfig;
ToggleAdvancedScreens(false);
_root.Remove(_advancedConfigContainer);
_advancedConfigContainer = null!;
});
_root.Add(_advancedConfigContainer);
ToggleAdvancedScreens(true);
void ToggleAdvancedScreens(bool showAdvanced)
{
if (_root.childCount > 0)
{
_root[0].style.display = showAdvanced ? DisplayStyle.None : DisplayStyle.Flex;
}
_advancedConfigContainer.style.display = showAdvanced ? DisplayStyle.Flex : DisplayStyle.None;
}
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.Escape))
{
ADOBase.loader.LoadSceneWithTransition(WipeDirection.StartsFromRight, "scnEditor");
}
}
private void OpenVideoEncodingConfig()
{
var config = RenderManager.Instance.Configuration;
config.VideoConfig ??= GenerateEncodingConfig();
var tempConfig = new VideoEncodingConfig
{
Codec = config.VideoConfig.Codec,
RateControlMode = config.VideoConfig.RateControlMode,
Bitrate = config.VideoConfig.Bitrate,
MaxBitrate = config.VideoConfig.MaxBitrate,
QP = config.VideoConfig.QP,
KeyframeInterval = config.VideoConfig.KeyframeInterval,
ForceSoftwareEncoder = config.VideoConfig.ForceSoftwareEncoder
};
if (_root.Contains(_videoEncodingConfigContainer))
{
_root.Remove(_videoEncodingConfigContainer);
}
// 1. 전체 화면 컨테이너 생성 및 배경 적용
_videoEncodingConfigContainer = new VisualElement
{
name = "VideoEncodingSettingsContainer",
style =
{
position = Position.Absolute,
width = Length.Percent(100),
height = Length.Percent(100),
left = 0,
top = 0,
backgroundColor = new StyleColor(new Color(0.15f, 0.15f, 0.15f, 1.0f)),
justifyContent = Justify.Center,
alignItems = Align.Center
}
};
// 2. 타이틀 로우
var titleRow = CreateRow();
var titleLabel = new Label("비디오 인코딩 설정")
{
style =
{
fontSize = 24,
color = new StyleColor(Color.white)
}
};
titleRow.Add(titleLabel);
_videoEncodingConfigContainer.Add(titleRow);
// 3. 코덱 로우
var codecRow = CreateRow();
var codecLabel = CreateFormLabel("코덱");
var codecChoices = new List<string>(System.Enum.GetNames(typeof(Codec)));
var codecDropdown = new DropdownField(codecChoices, tempConfig.Codec.ToString()) { name = "VideoCodec" };
codecRow.Add(codecLabel);
codecRow.Add(codecDropdown);
_videoEncodingConfigContainer.Add(codecRow);
// 4. 모드 로우
var modeRow = CreateRow();
var modeLabel = CreateFormLabel("모드");
var modeChoices = new List<string>(System.Enum.GetNames(typeof(RateControlMode)));
var modeDropdown = new DropdownField(modeChoices, tempConfig.RateControlMode.ToString()) { name = "VideoRateControlMode" };
modeRow.Add(modeLabel);
modeRow.Add(modeDropdown);
_videoEncodingConfigContainer.Add(modeRow);
// 5. 비트레이트 로우
var bitrateRow = CreateRow();
var bitrateLabel = CreateFormLabel("비트레이트(kbps)");
var bitrateField = new IntegerField { name = "VideoBitrate", label = "", value = tempConfig.Bitrate };
bitrateRow.Add(bitrateLabel);
bitrateRow.Add(bitrateField);
_videoEncodingConfigContainer.Add(bitrateRow);
// 5.5. 최대 비트레이트 로우 (VBR 전용)
var maxBitrateRow = CreateRow();
var maxBitrateLabel = CreateFormLabel("최대 비트레이트(kbps)");
var maxBitrateField = new IntegerField { name = "VideoMaxBitrate", label = "", value = tempConfig.MaxBitrate };
maxBitrateRow.Add(maxBitrateLabel);
maxBitrateRow.Add(maxBitrateField);
_videoEncodingConfigContainer.Add(maxBitrateRow);
// 6. QP 로우
var qpRow = CreateRow();
var qpLabel = CreateFormLabel("품질 (QP)");
var qpField = new IntegerField { name = "VideoQP", label = "", value = tempConfig.QP };
qpRow.Add(qpLabel);
qpRow.Add(qpField);
_videoEncodingConfigContainer.Add(qpRow);
// 6.5. 키프레임 간격 로우 (초 단위)
var keyframeRow = CreateRow();
var keyframeLabel = CreateFormLabel("키프레임 간격(초)");
var keyframeField = new DoubleField { name = "VideoKeyframeInterval", label = "", value = tempConfig.KeyframeInterval };
keyframeRow.Add(keyframeLabel);
keyframeRow.Add(keyframeField);
_videoEncodingConfigContainer.Add(keyframeRow);
// 6.75. 강제 소프트웨어 인코더 로우 (라벨 + 토글)
var forceSoftwareRow = CreateRow();
var forceSoftwareLabel = CreateFormLabel("소프트웨어 인코더");
var forceSoftwareToggle = new Toggle { name = "VideoForceSoftwareEncoderToggle", label = "", value = tempConfig.ForceSoftwareEncoder };
forceSoftwareRow.Add(forceSoftwareLabel);
forceSoftwareRow.Add(forceSoftwareToggle);
_videoEncodingConfigContainer.Add(forceSoftwareRow);
// 7. QP = 0 (무손실) 경고 라벨 로우 생성
var warningRow = CreateRow();
warningRow.style.height = StyleKeyword.Auto;
warningRow.style.maxHeight = StyleKeyword.Null;
warningRow.style.minHeight = StyleKeyword.Null;
var warningLabel = new Label("주의: QP를 0으로 설정하면 무손실 인코딩이 적용되어 용량이 매우 커질 수 있습니다.")
{
style =
{
color = new StyleColor(new Color(1.0f, 0.8f, 0.2f)),
fontSize = 20,
unityTextAlign = TextAnchor.MiddleCenter,
whiteSpace = WhiteSpace.Normal
}
};
warningRow.Add(warningLabel);
_videoEncodingConfigContainer.Add(warningRow);
var encodingValidator = new IntegerInputValidator(_videoEncodingConfigContainer);
encodingValidator
.AddLimits("VideoBitrate", new PositiveOnlyIntegerLimit(), new RangeIntegerLimit(1, 1000000))
.AddLimits("VideoMaxBitrate", new PositiveOnlyIntegerLimit(), new RangeIntegerLimit(1, 1000000))
.AddLimits("VideoQP", new RangeIntegerLimit(0, 51));
encodingValidator.SetupValidation();
var doubleValidator = new DoubleInputValidator(_videoEncodingConfigContainer);
doubleValidator.AddLimits("VideoKeyframeInterval", new PositiveOnlyDoubleLimit(), new RangeDoubleLimit(0.1, 60.0));
doubleValidator.SetupValidation();
modeDropdown.RegisterValueChangedCallback(evt => UpdateVisibility(evt.newValue));
qpField.RegisterValueChangedCallback(evt =>
{
warningRow.style.display = modeDropdown.value == RateControlMode.CQP.ToString() && evt.newValue == 0
? DisplayStyle.Flex
: DisplayStyle.None;
});
// 8. 제어 버튼 로우 (취소 / 저장)
var buttonRow = CreateRow();
var cancelBtn = new Button { text = "취소" };
var saveBtn = new Button { text = "저장" };
buttonRow.Add(cancelBtn);
buttonRow.Add(saveBtn);
_videoEncodingConfigContainer.Add(buttonRow);
cancelBtn.RegisterCallback<ClickEvent>(_ =>
{
ToggleScreens(false);
_root.Remove(_videoEncodingConfigContainer);
_videoEncodingConfigContainer = null!;
});
UpdateVisibility(modeDropdown.value);
saveBtn.RegisterCallback<ClickEvent>(_ =>
{
if (System.Enum.TryParse<RateControlMode>(modeDropdown.value, out var mode))
{
tempConfig.RateControlMode = mode;
}
if (System.Enum.TryParse<Codec>(codecDropdown.value, out var codec))
{
tempConfig.Codec = codec;
}
tempConfig.Bitrate = bitrateField.value;
tempConfig.MaxBitrate = maxBitrateField.value;
tempConfig.QP = qpField.value;
tempConfig.KeyframeInterval = keyframeField.value;
tempConfig.ForceSoftwareEncoder = forceSoftwareToggle.value;
if (tempConfig.RateControlMode == RateControlMode.VBR && tempConfig.MaxBitrate < tempConfig.Bitrate)
{
tempConfig.MaxBitrate = tempConfig.Bitrate;
}
config.VideoConfig = tempConfig;
ToggleScreens(false);
_root.Remove(_videoEncodingConfigContainer);
_videoEncodingConfigContainer = null!;
});
_root.Add(_videoEncodingConfigContainer);
// 화면 전환 활성화
ToggleScreens(true);
return;
// 화면 전환 제어
void ToggleScreens(bool showEncoding)
{
if (_root.childCount > 0)
{
_root[0].style.display = showEncoding ? DisplayStyle.None : DisplayStyle.Flex;
}
_videoEncodingConfigContainer.style.display = showEncoding ? DisplayStyle.Flex : DisplayStyle.None;
}
// 가시성 필터링 업데이트 (행 단위로 보임/숨김 제어)
void UpdateVisibility(string selectedModeStr)
{
if (!System.Enum.TryParse<RateControlMode>(selectedModeStr, out var mode)) return;
bitrateRow.style.display = mode is RateControlMode.CBR or 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;
}
}
private void OpenAudioEncodingConfig()
{
var config = RenderManager.Instance.Configuration;
config.AudioConfig ??= GenerateAudioEncodingConfig();
var tempAudioConfig = new AudioEncodingConfig
{
RenderAudio = config.AudioConfig.RenderAudio,
AudioSampleRate = config.AudioConfig.AudioSampleRate,
AudioChannels = config.AudioConfig.AudioChannels,
AudioBitrate = config.AudioConfig.AudioBitrate
};
if (_audioEncodingConfigContainer != null && _root.Contains(_audioEncodingConfigContainer))
{
_root.Remove(_audioEncodingConfigContainer);
}
_audioEncodingConfigContainer = new VisualElement
{
name = "AudioEncodingSettingsContainer",
style =
{
position = Position.Absolute,
width = Length.Percent(100),
height = Length.Percent(100),
left = 0,
top = 0,
backgroundColor = new StyleColor(new Color(0.15f, 0.15f, 0.15f, 1.0f)),
justifyContent = Justify.Center,
alignItems = Align.Center
}
};
// 1. 타이틀 로우
var titleRow = CreateRow();
var titleLabel = new Label("오디오 인코딩 설정")
{
style =
{
fontSize = 24,
color = new StyleColor(Color.white)
}
};
titleRow.Add(titleLabel);
_audioEncodingConfigContainer.Add(titleRow);
// 2. 오디오 렌더링 활성화 로우 (라벨 + 토글)
var renderAudioRow = CreateRow();
var renderAudioLabel = CreateFormLabel("오디오 렌더링 활성화");
var renderAudioToggle = new Toggle { name = "AudioRenderAudioToggle", label = "", value = tempAudioConfig.RenderAudio };
renderAudioRow.Add(renderAudioLabel);
renderAudioRow.Add(renderAudioToggle);
_audioEncodingConfigContainer.Add(renderAudioRow);
// 3. 샘플 레이트 로우
var sampleRateRow = CreateRow();
var sampleRateLabel = CreateFormLabel("샘플 레이트(Hz)");
var sampleRateStr = tempAudioConfig.AudioSampleRate.ToString();
if (sampleRateStr != "44100" && sampleRateStr != "48000")
{
sampleRateStr = "44100";
}
var sampleRateDropdown = new DropdownField(
new List<string> { "44100", "48000" },
sampleRateStr
) { name = "AudioSampleRate" };
sampleRateRow.Add(sampleRateLabel);
sampleRateRow.Add(sampleRateDropdown);
_audioEncodingConfigContainer.Add(sampleRateRow);
// 4. 채널 로우
var channelsRow = CreateRow();
var channelsLabel = CreateFormLabel("오디오 채널");
var channelsDropdown = new DropdownField(
new List<string> { "1 (Mono)", "2 (Stereo)" },
tempAudioConfig.AudioChannels == 1 ? "1 (Mono)" : "2 (Stereo)"
) { name = "AudioChannels" };
channelsRow.Add(channelsLabel);
channelsRow.Add(channelsDropdown);
_audioEncodingConfigContainer.Add(channelsRow);
// 5. 비트레이트 로우 (kbps 단위 입력)
var bitrateRow = CreateRow();
var bitrateLabel = CreateFormLabel("오디오 비트레이트(kbps)");
var bitrateField = new IntegerField { name = "AudioBitrateKbps", label = "", value = tempAudioConfig.AudioBitrate / 1000 };
bitrateRow.Add(bitrateLabel);
bitrateRow.Add(bitrateField);
_audioEncodingConfigContainer.Add(bitrateRow);
var audioValidator = new IntegerInputValidator(_audioEncodingConfigContainer);
audioValidator.AddLimits("AudioBitrateKbps", new PositiveOnlyIntegerLimit(), new RangeIntegerLimit(32, 1024));
audioValidator.SetupValidation();
// 가시성 업데이트 (오디오 렌더링 비활성화 시 하위 설정 숨김)
renderAudioToggle.RegisterValueChangedCallback(evt => UpdateAudioVisibility(evt.newValue));
UpdateAudioVisibility(tempAudioConfig.RenderAudio);
// 6. 제어 버튼 로우 (취소 / 저장)
var buttonRow = CreateRow();
var cancelBtn = new Button { text = "취소" };
var saveBtn = new Button { text = "저장" };
buttonRow.Add(cancelBtn);
buttonRow.Add(saveBtn);
_audioEncodingConfigContainer.Add(buttonRow);
cancelBtn.RegisterCallback<ClickEvent>(_ =>
{
ToggleAudioScreens(false);
_root.Remove(_audioEncodingConfigContainer);
_audioEncodingConfigContainer = null!;
});
saveBtn.RegisterCallback<ClickEvent>(_ =>
{
tempAudioConfig.RenderAudio = renderAudioToggle.value;
if (int.TryParse(sampleRateDropdown.value, out var sampleRate))
{
tempAudioConfig.AudioSampleRate = sampleRate;
}
tempAudioConfig.AudioChannels = channelsDropdown.value.StartsWith("1") ? 1 : 2;
tempAudioConfig.AudioBitrate = bitrateField.value * 1000;
config.AudioConfig = tempAudioConfig;
ToggleAudioScreens(false);
_root.Remove(_audioEncodingConfigContainer);
_audioEncodingConfigContainer = null!;
});
_root.Add(_audioEncodingConfigContainer);
ToggleAudioScreens(true);
return;
void ToggleAudioScreens(bool showAudio)
{
if (_root.childCount > 0)
{
_root[0].style.display = showAudio ? DisplayStyle.None : DisplayStyle.Flex;
}
_audioEncodingConfigContainer.style.display = showAudio ? DisplayStyle.Flex : DisplayStyle.None;
}
void UpdateAudioVisibility(bool isAudioEnabled)
{
sampleRateRow.style.display = isAudioEnabled ? DisplayStyle.Flex : DisplayStyle.None;
channelsRow.style.display = isAudioEnabled ? DisplayStyle.Flex : DisplayStyle.None;
bitrateRow.style.display = isAudioEnabled ? DisplayStyle.Flex : DisplayStyle.None;
}
}
private void TryGoRender()
{
if (!ValidateInputs())
{
Melon<RenderMod>.Logger.Error("필수 입력 값이 비어 있거나 올바르지 않습니다. 모든 설정을 입력해 주세요.");
return;
}
var widthField = _root.Q<IntegerField>("RenderWidth");
var heightField = _root.Q<IntegerField>("RenderHeight");
var renderFpsField = _root.Q<DoubleField>("RenderFPS");
var videoFpsField = _root.Q<DoubleField>("VideoFPS");
var delayField = _root.Q<DoubleField>("DelayAfterLevelEnd");
var showJudgementText = _root.Q<Toggle>("ShowJudgementText");
// Copy validated values to Configuration manually
var config = RenderManager.Instance.Configuration;
config.RenderWidth = widthField.value;
config.RenderHeight = heightField.value;
config.RenderFPS = renderFpsField.value;
config.VideoFPS = videoFpsField.value;
config.DelayAfterLevelEnd = delayField.value;
if (showJudgementText != null)
config.ShowJudgementText = showJudgementText.value;
RenderManager.Instance.Configuration.VideoConfig ??= GenerateEncodingConfig();
RenderManager.Instance.Configuration.AudioConfig ??= GenerateAudioEncodingConfig();
RenderManager.Instance.Configuration.AdvancedConfig ??= GenerateAdvancedConfig();
RenderManager.Instance.SetupCaptureDirectory();
RenderManager.Instance.SetupGameScene();
}
private VisualElement? _presetConfigContainer;
private bool ValidateInputs()
{
var widthField = _root.Q<IntegerField>("RenderWidth");
var heightField = _root.Q<IntegerField>("RenderHeight");
var renderFpsField = _root.Q<DoubleField>("RenderFPS");
var videoFpsField = _root.Q<DoubleField>("VideoFPS");
var delayField = _root.Q<DoubleField>("DelayAfterLevelEnd");
if (widthField == null || string.IsNullOrEmpty(widthField.text) ||
heightField == null || string.IsNullOrEmpty(heightField.text) ||
renderFpsField == null || string.IsNullOrEmpty(renderFpsField.text) ||
videoFpsField == null || string.IsNullOrEmpty(videoFpsField.text) ||
delayField == null || string.IsNullOrEmpty(delayField.text))
{
return false;
}
if (widthField.value <= 0 || heightField.value <= 0 ||
renderFpsField.value <= 0 || videoFpsField.value <= 0 ||
delayField.value < 0)
{
return false;
}
return true;
}
private void ApplyConfigToUI(RenderConfig config)
{
var widthField = _root.Q<IntegerField>("RenderWidth");
var heightField = _root.Q<IntegerField>("RenderHeight");
var renderFpsField = _root.Q<DoubleField>("RenderFPS");
var videoFpsField = _root.Q<DoubleField>("VideoFPS");
var delayField = _root.Q<DoubleField>("DelayAfterLevelEnd");
var showJudgementText = _root.Q<Toggle>("ShowJudgementText");
if (widthField != null) widthField.value = config.RenderWidth;
if (heightField != null) heightField.value = config.RenderHeight;
if (renderFpsField != null) renderFpsField.value = config.RenderFPS;
if (videoFpsField != null) videoFpsField.value = config.VideoFPS;
if (delayField != null) delayField.value = config.DelayAfterLevelEnd;
if (showJudgementText != null) showJudgementText.value = config.ShowJudgementText;
config.VideoConfig ??= GenerateEncodingConfig();
config.AudioConfig ??= GenerateAudioEncodingConfig();
config.AdvancedConfig ??= GenerateAdvancedConfig();
RenderManager.Instance.Configuration = config;
}
private void OpenPresetConfig()
{
if (_presetConfigContainer != null && _root.Contains(_presetConfigContainer))
{
_root.Remove(_presetConfigContainer);
}
_presetConfigContainer = new VisualElement { name = "PresetSettingsContainer" };
_presetConfigContainer.style.position = Position.Absolute;
_presetConfigContainer.style.width = Length.Percent(100);
_presetConfigContainer.style.height = Length.Percent(100);
_presetConfigContainer.style.left = 0;
_presetConfigContainer.style.top = 0;
_presetConfigContainer.style.backgroundColor = new StyleColor(new Color(0.15f, 0.15f, 0.15f, 1.0f));
_presetConfigContainer.style.justifyContent = Justify.Center;
_presetConfigContainer.style.alignItems = Align.Center;
// Title Row
var titleRow = CreateRow();
titleRow.style.height = StyleKeyword.Auto;
titleRow.style.minHeight = StyleKeyword.Null;
titleRow.style.maxHeight = StyleKeyword.Null;
var title = new Label("프리셋 설정")
{
style =
{
fontSize = 36,
unityFontStyleAndWeight = FontStyle.Bold,
color = new StyleColor(Color.white),
unityTextAlign = TextAnchor.MiddleCenter,
width = 820,
height = StyleKeyword.Auto
}
};
titleRow.Add(title);
_presetConfigContainer.Add(titleRow);
// ScrollView for Preset List
var scrollView = new ScrollView
{
style =
{
width = 820,
height = 380,
backgroundColor = new StyleColor(new Color(0.12f, 0.12f, 0.12f, 0.5f)),
borderTopWidth = 1,
borderBottomWidth = 1,
borderLeftWidth = 1,
borderRightWidth = 1,
borderTopColor = new StyleColor(new Color(0.25f, 0.25f, 0.25f, 0.8f)),
borderBottomColor = new StyleColor(new Color(0.25f, 0.25f, 0.25f, 0.8f)),
borderLeftColor = new StyleColor(new Color(0.25f, 0.25f, 0.25f, 0.8f)),
borderRightColor = new StyleColor(new Color(0.25f, 0.25f, 0.25f, 0.8f)),
borderTopLeftRadius = 5,
borderTopRightRadius = 5,
borderBottomLeftRadius = 5,
borderBottomRightRadius = 5,
paddingBottom = 10,
paddingTop = 10,
paddingLeft = 10,
paddingRight = 10,
marginBottom = 20
}
};
_presetConfigContainer.Add(scrollView);
// Style the vertical scrollbar
scrollView.verticalScroller.style.width = 10f;
var tracker = scrollView.verticalScroller.Q<VisualElement>("unity-tracker");
if (tracker != null)
{
tracker.style.backgroundColor = new StyleColor(new Color(0.08f, 0.08f, 0.08f, 0.3f));
tracker.style.borderTopWidth = 0f;
tracker.style.borderBottomWidth = 0f;
tracker.style.borderLeftWidth = 0f;
tracker.style.borderRightWidth = 0f;
}
var dragger = scrollView.verticalScroller.Q<VisualElement>("unity-dragger");
if (dragger != null)
{
dragger.style.backgroundColor = new StyleColor(new Color(0.25f, 0.25f, 0.25f, 0.8f));
dragger.style.borderTopLeftRadius = 4f;
dragger.style.borderTopRightRadius = 4f;
dragger.style.borderBottomLeftRadius = 4f;
dragger.style.borderBottomRightRadius = 4f;
dragger.style.borderTopWidth = 0f;
dragger.style.borderBottomWidth = 0f;
dragger.style.borderLeftWidth = 0f;
dragger.style.borderRightWidth = 0f;
}
var lowButton = scrollView.verticalScroller.Q<VisualElement>("unity-low-button");
if (lowButton != null)
{
lowButton.style.display = DisplayStyle.None;
}
var highButton = scrollView.verticalScroller.Q<VisualElement>("unity-high-button");
if (highButton != null)
{
highButton.style.display = DisplayStyle.None;
}
// Refresh Preset List
System.Action refreshPresets = null!;
refreshPresets = () =>
{
scrollView.Clear();
var presets = PresetManager.GetPresets();
if (presets.Count == 0)
{
var emptyRow = CreateRow();
emptyRow.style.height = StyleKeyword.Auto;
emptyRow.style.minHeight = StyleKeyword.Null;
emptyRow.style.maxHeight = StyleKeyword.Null;
var emptyLabel = new Label("저장된 프리셋이 없습니다.")
{
style =
{
color = new StyleColor(Color.gray),
unityTextAlign = TextAnchor.MiddleCenter,
marginTop = 80,
fontSize = 20
}
};
emptyRow.Add(emptyLabel);
scrollView.Add(emptyRow);
}
else
{
for (int i = 0; i < presets.Count; i++)
{
var preset = presets[i];
var itemRow = new VisualElement
{
style =
{
flexDirection = FlexDirection.Row,
justifyContent = Justify.SpaceBetween,
alignItems = Align.Center,
paddingBottom = 5,
paddingTop = 5,
borderBottomWidth = (i == presets.Count - 1) ? 0 : 1,
borderBottomColor = new StyleColor(new Color(0.2f, 0.2f, 0.2f, 0.8f))
}
};
itemRow.AddToClassList("ui-row");
var nameLabel = new Label(preset.Name)
{
style =
{
color = new StyleColor(Color.white),
fontSize = 30,
flexGrow = 1
}
};
var btnContainer = new VisualElement
{
style = { flexDirection = FlexDirection.Row }
};
var loadBtn = new Button { text = "불러오기" };
loadBtn.style.marginRight = 10;
loadBtn.style.width = 180f;
loadBtn.style.marginTop = 0f;
loadBtn.style.marginBottom = 0f;
loadBtn.style.backgroundColor = new StyleColor(new Color(0.18f, 0.18f, 0.22f, 1.0f));
loadBtn.style.borderTopColor = new StyleColor(new Color(0.3f, 0.3f, 0.35f, 0.8f));
loadBtn.style.borderBottomColor = new StyleColor(new Color(0.3f, 0.3f, 0.35f, 0.8f));
loadBtn.style.borderLeftColor = new StyleColor(new Color(0.3f, 0.3f, 0.35f, 0.8f));
loadBtn.style.borderRightColor = new StyleColor(new Color(0.3f, 0.3f, 0.35f, 0.8f));
loadBtn.style.borderTopWidth = 1f;
loadBtn.style.borderBottomWidth = 1f;
loadBtn.style.borderLeftWidth = 1f;
loadBtn.style.borderRightWidth = 1f;
loadBtn.style.borderTopLeftRadius = 8f;
loadBtn.style.borderTopRightRadius = 8f;
loadBtn.style.borderBottomLeftRadius = 8f;
loadBtn.style.borderBottomRightRadius = 8f;
loadBtn.style.paddingLeft = 10f;
loadBtn.style.paddingRight = 10f;
loadBtn.style.paddingTop = 10f;
loadBtn.style.paddingBottom = 10f;
loadBtn.style.fontSize = 30f;
loadBtn.style.height = 80f;
loadBtn.style.color = new StyleColor(Color.white);
loadBtn.RegisterCallback<ClickEvent>(_ =>
{
ApplyConfigToUI(preset.Config);
ToggleScreens(false);
_root.Remove(_presetConfigContainer);
_presetConfigContainer = null;
});
var deleteBtn = new Button { text = "삭제" };
deleteBtn.style.width = 120f;
deleteBtn.style.marginTop = 0f;
deleteBtn.style.marginBottom = 0f;
deleteBtn.style.backgroundColor = new StyleColor(new Color(0.25f, 0.12f, 0.12f, 1.0f));
deleteBtn.style.borderTopColor = new StyleColor(new Color(0.45f, 0.2f, 0.2f, 0.8f));
deleteBtn.style.borderBottomColor = new StyleColor(new Color(0.45f, 0.2f, 0.2f, 0.8f));
deleteBtn.style.borderLeftColor = new StyleColor(new Color(0.45f, 0.2f, 0.2f, 0.8f));
deleteBtn.style.borderRightColor = new StyleColor(new Color(0.45f, 0.2f, 0.2f, 0.8f));
deleteBtn.style.borderTopWidth = 1f;
deleteBtn.style.borderBottomWidth = 1f;
deleteBtn.style.borderLeftWidth = 1f;
deleteBtn.style.borderRightWidth = 1f;
deleteBtn.style.borderTopLeftRadius = 8f;
deleteBtn.style.borderTopRightRadius = 8f;
deleteBtn.style.borderBottomLeftRadius = 8f;
deleteBtn.style.borderBottomRightRadius = 8f;
deleteBtn.style.paddingLeft = 10f;
deleteBtn.style.paddingRight = 10f;
deleteBtn.style.paddingTop = 10f;
deleteBtn.style.paddingBottom = 10f;
deleteBtn.style.fontSize = 30f;
deleteBtn.style.height = 80f;
deleteBtn.style.color = new StyleColor(Color.white);
deleteBtn.RegisterCallback<ClickEvent>(_ =>
{
CommonWarningDialogUI.ShowDialog(
"프리셋 삭제",
$"'{preset.Name}' 프리셋을 정말 삭제하시겠습니까?",
new DialogButton("예", () =>
{
PresetManager.DeletePreset(preset.Name);
refreshPresets();
}),
new DialogButton("아니오", () => {})
);
});
btnContainer.Add(loadBtn);
btnContainer.Add(deleteBtn);
itemRow.Add(nameLabel);
itemRow.Add(btnContainer);
scrollView.Add(itemRow);
}
}
};
refreshPresets();
// Save row: Label + TextField + Save Button
var saveRow = CreateRow();
saveRow.style.marginBottom = 20;
var nameLabelLabel = CreateFormLabel("새 프리셋 이름");
nameLabelLabel.style.marginRight = 10;
var nameField = new TextField { label = "", name = "NewPresetName" };
nameField.style.width = 400;
nameField.style.marginRight = 20;
// 기존 폼 입력 필드 스타일과 맞춤
var textInput = nameField.Q("unity-text-input");
if (textInput != null)
{
textInput.style.backgroundColor = new StyleColor(new Color(0.1f, 0.1f, 0.1f, 1.0f));
textInput.style.borderTopLeftRadius = 6f;
textInput.style.borderTopRightRadius = 6f;
textInput.style.borderBottomLeftRadius = 6f;
textInput.style.borderBottomRightRadius = 6f;
textInput.style.borderTopWidth = 1f;
textInput.style.borderBottomWidth = 1f;
textInput.style.borderLeftWidth = 1f;
textInput.style.borderRightWidth = 1f;
textInput.style.borderTopColor = new StyleColor(new Color(0.3f, 0.3f, 0.35f, 0.8f));
textInput.style.borderBottomColor = new StyleColor(new Color(0.3f, 0.3f, 0.35f, 0.8f));
textInput.style.borderLeftColor = new StyleColor(new Color(0.3f, 0.3f, 0.35f, 0.8f));
textInput.style.borderRightColor = new StyleColor(new Color(0.3f, 0.3f, 0.35f, 0.8f));
textInput.style.paddingLeft = 8f;
textInput.style.color = new StyleColor(Color.white);
}
else
{
nameField.style.backgroundColor = new StyleColor(new Color(0.1f, 0.1f, 0.1f, 1.0f));
nameField.style.borderTopLeftRadius = 6f;
nameField.style.borderTopRightRadius = 6f;
nameField.style.borderBottomLeftRadius = 6f;
nameField.style.borderBottomRightRadius = 6f;
}
var saveBtn = new Button { text = "저장" };
saveBtn.RegisterCallback<ClickEvent>(_ =>
{
string presetName = nameField.value;
if (string.IsNullOrWhiteSpace(presetName))
{
CommonWarningDialogUI.ShowDialog(
"저장 실패",
"프리셋 이름을 입력해 주세요.",
new DialogButton("확인", () => {})
);
return;
}
if (!ValidateInputs())
{
CommonWarningDialogUI.ShowDialog(
"저장 실패",
"필수 입력 값이 비어 있거나 올바르지 않아 프리셋을 저장할 수 없습니다.",
new DialogButton("확인", () => {})
);
return;
}
try
{
var widthField = _root.Q<IntegerField>("RenderWidth");
var heightField = _root.Q<IntegerField>("RenderHeight");
var renderFpsField = _root.Q<DoubleField>("RenderFPS");
var videoFpsField = _root.Q<DoubleField>("VideoFPS");
var delayField = _root.Q<DoubleField>("DelayAfterLevelEnd");
var showJudgementText = _root.Q<Toggle>("ShowJudgementText");
var currentConfig = new RenderConfig
{
RenderWidth = widthField.value,
RenderHeight = heightField.value,
RenderFPS = renderFpsField.value,
VideoFPS = videoFpsField.value,
DelayAfterLevelEnd = delayField.value,
ShowJudgementText = showJudgementText?.value ?? true
};
currentConfig.VideoConfig = RenderManager.Instance.Configuration.VideoConfig ?? GenerateEncodingConfig();
currentConfig.AudioConfig = RenderManager.Instance.Configuration.AudioConfig ?? GenerateAudioEncodingConfig();
currentConfig.AdvancedConfig = RenderManager.Instance.Configuration.AdvancedConfig ?? GenerateAdvancedConfig();
PresetManager.SavePreset(presetName, currentConfig);
nameField.value = string.Empty;
refreshPresets();
}
catch (System.Exception ex)
{
CommonWarningDialogUI.ShowDialog(
"저장 실패",
$"프리셋 저장 중 오류가 발생했습니다: {ex.Message}",
new DialogButton("확인", () => {})
);
}
});
saveRow.Add(nameLabelLabel);
saveRow.Add(nameField);
saveRow.Add(saveBtn);
_presetConfigContainer.Add(saveRow);
// Bottom controls: Close Button
var closeRow = CreateRow();
var closeBtn = new Button { text = "닫기" };
closeBtn.RegisterCallback<ClickEvent>(_ =>
{
ToggleScreens(false);
_root.Remove(_presetConfigContainer);
_presetConfigContainer = null;
});
closeRow.Add(closeBtn);
_presetConfigContainer.Add(closeRow);
_root.Add(_presetConfigContainer);
ToggleScreens(true);
void ToggleScreens(bool showPreset)
{
if (_root.childCount > 0)
{
_root[0].style.display = showPreset ? DisplayStyle.None : DisplayStyle.Flex;
}
_presetConfigContainer.style.display = showPreset ? DisplayStyle.Flex : DisplayStyle.None;
}
}
private VisualElement CreateRow()
{
var row = new VisualElement
{
style =
{
flexDirection = FlexDirection.Row,
height = 130,
maxHeight = 130,
minHeight = 80,
justifyContent = Justify.Center,
alignItems = Align.Center
}
};
row.AddToClassList("ui-row");
return row;
}
private Label CreateFormLabel(string text)
{
var label = new Label(text)
{
style =
{
unityTextAlign = TextAnchor.MiddleRight,
color = new StyleColor(Color.white)
}
};
return label;
}
private VideoEncodingConfig GenerateEncodingConfig()
{
return new VideoEncodingConfig
{
Codec = Codec.H264,
RateControlMode = RateControlMode.CBR,
Bitrate = 30000, MaxBitrate = 50000,
QP = 18, KeyframeInterval = 2.0,
ForceSoftwareEncoder = false
};
}
private AudioEncodingConfig GenerateAudioEncodingConfig()
{
return new AudioEncodingConfig
{
RenderAudio = true,
AudioSampleRate = 44100,
AudioChannels = 2,
AudioBitrate = 256000
};
}
private AdvancedConfig GenerateAdvancedConfig()
{
return new AdvancedConfig
{
ProcessAudio = true,
RandomSeed = null,
NonTile = false
};
}
}
}