SHA256
Translate Codebase Comments and Log Messages to English
This commit is contained in:
23 files changed
+915
-921
No files matched your search
@@ -45,20 +45,20 @@ internal class FirstUpdateHook : MonoBehaviour
|
||||
var currentVersion = driverVer.Value.ToString("F2");
|
||||
|
||||
CommonWarningDialogUI.ShowDialog(
|
||||
"NVIDIA 그래픽 드라이버 업데이트 권장",
|
||||
$"현재 감지된 NVIDIA 그래픽 드라이버 버전: <b><color=#ff5555>{currentVersion}</color></b>\n\n하드웨어 가속(NVENC)을 사용하여 영상을 렌더링하려면 드라이버 버전 <b><color=#55ff55>610.00 이상</color></b>이 필요합니다.\n\n드라이버 버전이 낮으면 인코딩이 실패하거나 소프트웨어(CPU) 인코딩으로 대체되어 렌더링이 매우 느려집니다. 드라이버 업데이트를 진행해 주시기 바랍니다.",
|
||||
new DialogButton("드라이버 업데이트", () =>
|
||||
"NVIDIA Graphics Driver Update Recommended",
|
||||
$"Currently detected NVIDIA graphics driver version: <b><color=#ff5555>{currentVersion}</color></b>\n\nDriver version <b><color=#55ff55>610.00 or higher</color></b> is required to render video using hardware acceleration (NVENC).\n\nIf the driver version is lower, encoding may fail or fallback to software (CPU) encoding, significantly slowing down rendering. Please update your graphics driver.",
|
||||
new DialogButton("Update Driver", () =>
|
||||
{
|
||||
Application.OpenURL("https://www.nvidia.com/Download/index.aspx");
|
||||
Application.Quit();
|
||||
}, new Color(0.0f, 0.8f, 1.0f, 1.0f)),
|
||||
new DialogButton("기능 비활성화", () =>
|
||||
new DialogButton("Disable Feature", () =>
|
||||
{
|
||||
RenderMod.IsFeatureDisabled = true;
|
||||
}),
|
||||
new DialogButton("이번만 그냥 진행", () =>
|
||||
new DialogButton("Continue Anyway", () =>
|
||||
{
|
||||
// 아무 동작 안 함
|
||||
// ignored
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
+28
-28
@@ -40,10 +40,10 @@ internal static class RenderContext
|
||||
if (result.Success && Notification)
|
||||
{
|
||||
var fileString = !string.IsNullOrEmpty(RenderManager.Instance.CurrentOutPath) ?
|
||||
$"'{Path.GetFileName(RenderManager.Instance.CurrentOutPath)}'이(가)" : "동영상이";
|
||||
$"'{Path.GetFileName(RenderManager.Instance.CurrentOutPath)}'" : "Video";
|
||||
|
||||
var renderFolder = Path.GetDirectoryName(RenderManager.Instance.CurrentOutPath);
|
||||
OSNotification.Show("렌더링 완료!", $"{fileString} 성공적으로 저장되었습니다.", renderFolder);
|
||||
OSNotification.Show("Rendering Complete!", $"{fileString} saved successfully.", renderFolder);
|
||||
}
|
||||
};
|
||||
public static Action<RenderResult> OnComplete = OnCompleteDefault;
|
||||
@@ -178,7 +178,7 @@ internal class RenderManager : MonoBehaviour
|
||||
|
||||
if (_activeVideoFFmpeg != null)
|
||||
{
|
||||
Melon<RenderMod>.Logger.Msg("게임 종료 또는 RenderManager 파괴 감지: FFmpeg 네이티브 인코더 리소스를 해제합니다.");
|
||||
Melon<RenderMod>.Logger.Msg("Game exit or RenderManager destruction detected: Disposing FFmpeg native encoder resources.");
|
||||
_activeVideoFFmpeg.Dispose();
|
||||
_activeVideoFFmpeg = null;
|
||||
}
|
||||
@@ -187,7 +187,7 @@ internal class RenderManager : MonoBehaviour
|
||||
{
|
||||
if (_allVideoFFmpegInstances.Count > 0)
|
||||
{
|
||||
Melon<RenderMod>.Logger.Msg($"게임 종료 감지: 생성된 모든 FFmpeg 인코더 인스턴스({_allVideoFFmpegInstances.Count}개)를 수거 및 해제합니다.");
|
||||
Melon<RenderMod>.Logger.Msg($"Game exit detected: Cleaning up and disposing all created FFmpeg encoder instances ({_allVideoFFmpegInstances.Count}).");
|
||||
foreach (var instance in _allVideoFFmpegInstances)
|
||||
{
|
||||
try
|
||||
@@ -196,7 +196,7 @@ internal class RenderManager : MonoBehaviour
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
_allVideoFFmpegInstances.Clear();
|
||||
@@ -278,18 +278,18 @@ internal class RenderManager : MonoBehaviour
|
||||
{
|
||||
if (!IsRendering) return;
|
||||
|
||||
// [렉으로 인한 프레임 렌더링 스킵 시 재시도 로직 (원본 모드 이식)]
|
||||
// [Retry logic when frame rendering is skipped due to lag (ported from original mod)]
|
||||
if (renderCheck && IsCapturing)
|
||||
{
|
||||
if (skipCount < MaxRenderRetry)
|
||||
{
|
||||
Melon<RenderMod>.Logger.Warning($"[Render Skipped] Frame {frameCount} rendering missed. Retrying... (Attempt {skipCount + 1}/{MaxRenderRetry})");
|
||||
skipCount++;
|
||||
Time.timeScale = 0f; // 진행 시간 멈추기 (재시도)
|
||||
return; // 업데이트 진행하지 않고 대기
|
||||
Time.timeScale = 0f; // Pause progress time (retry)
|
||||
return; // Wait without advancing update
|
||||
}
|
||||
|
||||
// 60회 초과 시 최후의 블랙 프레임 삽입하여 전체 파이프라인 마비 방지
|
||||
// If retries exceed limit, supply a fallback black frame to prevent freezing the pipeline
|
||||
Melon<RenderMod>.Logger.Error($"[Render Skipped] Frame {frameCount} rendering failed after 60 retries. Supplying black frame.");
|
||||
|
||||
var currentFrame = frameCountForVideo++;
|
||||
@@ -318,11 +318,11 @@ internal class RenderManager : MonoBehaviour
|
||||
FrameAvailableEvent.Set();
|
||||
}
|
||||
|
||||
renderCheck = false; // 진행 허용
|
||||
renderCheck = false; // Allow progress
|
||||
}
|
||||
|
||||
skipCount = 0;
|
||||
renderCheck = true; // 다음 프레임 검사용으로 초기화
|
||||
renderCheck = true; // Reset for next frame check
|
||||
frameCount++;
|
||||
}
|
||||
|
||||
@@ -344,9 +344,9 @@ internal class RenderManager : MonoBehaviour
|
||||
{
|
||||
if (!IsCapturing || !texture) return;
|
||||
|
||||
renderCheck = false; // 이번 프레임 정상 렌더링 완료
|
||||
renderCheck = false; // Normal rendering complete for this frame
|
||||
|
||||
// [Backpressure Throttling (2번 작업)]
|
||||
// [Backpressure Throttling]
|
||||
isThrottling = false;
|
||||
|
||||
while (IsCapturing && IsRendering)
|
||||
@@ -458,11 +458,11 @@ internal class RenderManager : MonoBehaviour
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Melon<RenderMod>.Logger.Error($"인코더 초기화 에러: {ex}");
|
||||
Melon<RenderMod>.Logger.Error($"Encoder initialization error: {ex}");
|
||||
CommonWarningDialogUI.ShowDialog(
|
||||
"인코더 오류",
|
||||
$"비디오 인코더를 초기화할 수 없습니다.\n오류: {ex.Message}",
|
||||
new DialogButton("확인", () =>
|
||||
"Encoder Error",
|
||||
$"Failed to initialize video encoder.\nError: {ex.Message}",
|
||||
new DialogButton("OK", () =>
|
||||
{
|
||||
ADOBase.loader.LoadSceneWithTransition(WipeDirection.StartsFromRight, "scnEditor");
|
||||
})
|
||||
@@ -676,7 +676,7 @@ internal class RenderManager : MonoBehaviour
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Melon<RenderMod>.Logger.Error($"FFmpeg 렌더링 스레드 오류: {ex}");
|
||||
Melon<RenderMod>.Logger.Error($"FFmpeg rendering thread error: {ex}");
|
||||
videoFFmpeg.Dispose();
|
||||
lock (_allVideoFFmpegInstances)
|
||||
{
|
||||
@@ -720,22 +720,22 @@ internal class RenderManager : MonoBehaviour
|
||||
if (success) StartCoroutine(StopRenderCoroutine());
|
||||
else
|
||||
{
|
||||
Melon<RenderMod>.Logger.Error("렌더링이 실패했거나 취소되었습니다. 상태 및 리소스를 안전하게 청소합니다.");
|
||||
Melon<RenderMod>.Logger.Error("Rendering failed or was cancelled. Safely cleaning up state and resources.");
|
||||
|
||||
// 1. 렌더링 백그라운드 스레드가 큐에 유입된 잔여 프레임 처리를 마무리하고 종료될 때까지 대기
|
||||
// 1. Wait for render background thread to finish processing remaining queued frames and terminate
|
||||
if (RenderThread is { IsAlive: true })
|
||||
{
|
||||
RenderThread.Join(2000);
|
||||
}
|
||||
|
||||
// 2. 스레드 종료 보장 후, 안전하게 FFmpeg 네이티브 인코더 리소스 해제
|
||||
// 2. After ensuring thread termination, safely release FFmpeg native encoder resources
|
||||
if (_activeVideoFFmpeg != null)
|
||||
{
|
||||
_activeVideoFFmpeg.Dispose();
|
||||
_activeVideoFFmpeg = null;
|
||||
}
|
||||
|
||||
// 3. 메모리 누수 방지: 큐에 렌트된 임시 프레임 버퍼 풀 반환 및 클리어
|
||||
// 3. Memory leak prevention: Return rented temporary frame buffer pool in queue and clear
|
||||
lock (ImageQueue)
|
||||
{
|
||||
foreach (var kvp in ImageQueue.Values)
|
||||
@@ -754,14 +754,14 @@ internal class RenderManager : MonoBehaviour
|
||||
|
||||
_frameReader?.Dispose();
|
||||
|
||||
// 디버그 모니터 UI 정리
|
||||
// Clean up Debug Monitor UI
|
||||
if (_debugMonitorObj)
|
||||
{
|
||||
Destroy(_debugMonitorObj);
|
||||
_debugMonitorObj = null;
|
||||
}
|
||||
|
||||
// 4. 인게임 카메라 및 렌더링용 RTCam 상태 안전 원복
|
||||
// 4. Safely restore in-game camera and RTCam state for rendering
|
||||
if (scrCamera.instance)
|
||||
{
|
||||
scrCamera.instance.forceRTCam = false;
|
||||
@@ -771,24 +771,24 @@ internal class RenderManager : MonoBehaviour
|
||||
if (hook) Destroy(hook);
|
||||
}
|
||||
|
||||
// 5. 인게임 실패 방지 플래그 원상 복구
|
||||
// 5. Restore in-game no-fail flags
|
||||
if (scrController.instance)
|
||||
{
|
||||
scrController.instance.noFail = false;
|
||||
scrController.instance.noFailInfiniteMargin = false;
|
||||
}
|
||||
|
||||
// 6. 취소된 미완성 임시 동영상 파일 즉각 삭제
|
||||
// 6. Immediately delete cancelled incomplete temporary video file
|
||||
if (!string.IsNullOrEmpty(CurrentOutPath) && File.Exists(CurrentOutPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(CurrentOutPath);
|
||||
Melon<RenderMod>.Logger.Msg($"[File Cleanup] 미완성 동영상 삭제 완료: {CurrentOutPath}");
|
||||
Melon<RenderMod>.Logger.Msg($"[File Cleanup] Deleted incomplete video: {CurrentOutPath}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Melon<RenderMod>.Logger.Error($"[File Cleanup] 임시 동영상 삭제 실패: {ex.Message}");
|
||||
Melon<RenderMod>.Logger.Error($"[File Cleanup] Failed to delete temporary video: {ex.Message}");
|
||||
}
|
||||
CurrentOutPath = null;
|
||||
}
|
||||
|
||||
+5
-5
@@ -31,7 +31,7 @@ internal class PositiveOnlyDoubleLimit : DoubleLimit
|
||||
return true;
|
||||
}
|
||||
|
||||
errorMessage = "값은 0보다 큰 양수여야 합니다.";
|
||||
errorMessage = "Value must be a positive number greater than 0.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -60,16 +60,16 @@ internal class RangeDoubleLimit : DoubleLimit
|
||||
if (value < Min)
|
||||
{
|
||||
errorMessage = Max.HasValue
|
||||
? $"값은 {Min.Value}에서 {Max.Value} 사이의 숫자여야 합니다."
|
||||
: $"값은 {Min.Value} 이상의 숫자여야 합니다.";
|
||||
? $"Value must be a number between {Min.Value} and {Max.Value}."
|
||||
: $"Value must be a number greater than or equal to {Min.Value}.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value > Max)
|
||||
{
|
||||
errorMessage = Min.HasValue
|
||||
? $"값은 {Min.Value}에서 {Max.Value} 사이의 숫자여야 합니다."
|
||||
: $"값은 {Max.Value} 이하의 숫자여야 합니다.";
|
||||
? $"Value must be a number between {Min.Value} and {Max.Value}."
|
||||
: $"Value must be a number less than or equal to {Max.Value}.";
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ internal class DoubleInputValidator
|
||||
if (field == null)
|
||||
{
|
||||
Melon<RenderMod>.Logger.Warning(
|
||||
$"DoubleField '{fieldName}'을(를) 찾을 수 없습니다. 유효성 검사를 건너뜁니다.");
|
||||
$"DoubleField '{fieldName}' not found. Skipping validation.");
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ internal class EnumDropdownBinder<T> where T : struct, Enum
|
||||
if (_dropdown == null)
|
||||
{
|
||||
Melon<RenderMod>.Logger.Error(
|
||||
$"DropdownField '{fieldName}'을(를) 찾을 수 없습니다.");
|
||||
$"DropdownField '{fieldName}' not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ internal class IntegerInputValidator
|
||||
if (field == null)
|
||||
{
|
||||
Melon<RenderMod>.Logger.Warning(
|
||||
$"IntegerField '{fieldName}'을(를) 찾을 수 없습니다. 유효성 검사를 건너뜁니다.");
|
||||
$"IntegerField '{fieldName}' not found. Skipping validation.");
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ internal class PositiveOnlyIntegerLimit : IntegerLimit
|
||||
return true;
|
||||
}
|
||||
|
||||
errorMessage = "값은 0보다 큰 양의 정수여야 합니다.";
|
||||
errorMessage = "Value must be a positive integer greater than 0.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -60,16 +60,16 @@ internal class RangeIntegerLimit : IntegerLimit
|
||||
if (value < Min)
|
||||
{
|
||||
errorMessage = Max.HasValue
|
||||
? $"값은 {Min.Value}에서 {Max.Value} 사이의 정수여야 합니다."
|
||||
: $"값은 {Min.Value} 이상의 정수여야 합니다.";
|
||||
? $"Value must be an integer between {Min.Value} and {Max.Value}."
|
||||
: $"Value must be an integer greater than or equal to {Min.Value}.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value > Max)
|
||||
{
|
||||
errorMessage = Min.HasValue
|
||||
? $"값은 {Min.Value}에서 {Max.Value} 사이의 정수여야 합니다."
|
||||
: $"값은 {Max.Value} 이하의 정수여야 합니다.";
|
||||
? $"Value must be an integer between {Min.Value} and {Max.Value}."
|
||||
: $"Value must be an integer less than or equal to {Max.Value}.";
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ internal static class PresetManager
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new ArgumentException("프리셋 이름은 비어 있을 수 없습니다.");
|
||||
throw new ArgumentException("Preset name cannot be empty.");
|
||||
}
|
||||
|
||||
string dir = GetPresetsDirectory();
|
||||
@@ -154,13 +154,13 @@ internal static class PresetManager
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MelonLogger.Warning($"프리셋 파일 '{Path.GetFileName(file)}' 로드 중 오류 발생: {ex.Message}");
|
||||
MelonLogger.Warning($"Error loading preset file '{Path.GetFileName(file)}': {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MelonLogger.Error($"프리셋 목록을 불러오는 중 오류 발생: {ex.Message}");
|
||||
MelonLogger.Error($"Error retrieving preset list: {ex.Message}");
|
||||
}
|
||||
|
||||
return presets;
|
||||
|
||||
@@ -18,7 +18,7 @@ public static class scnEditorPatch
|
||||
if (RenderMod.IsFeatureDisabled) return;
|
||||
if (!__instance || !__instance.buttonExit)
|
||||
{
|
||||
Melon<RenderMod>.Logger.Msg("scnEditor에 렌더 버튼을 만들 수 없습니다.");
|
||||
Melon<RenderMod>.Logger.Msg("Cannot create render button in scnEditor.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -48,11 +48,11 @@ namespace ADOFAIRenderer
|
||||
{
|
||||
ShaderBundle = BundleLoader.GetBundle("shaders");
|
||||
ColorConversionShader = ShaderBundle.LoadAsset<ComputeShader>("ColorConversion");
|
||||
Melon<RenderMod>.Logger.Msg("컬러 변환 Compute Shader 로드 완료.");
|
||||
Melon<RenderMod>.Logger.Msg("Color conversion Compute Shader loaded successfully.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Melon<RenderMod>.Logger.Warning($"Shader 에셋번들을 로드할 수 없습니다. GPU 컬러 변환이 비활성화되며 CPU로 폴백합니다. 에러: {ex.Message}");
|
||||
Melon<RenderMod>.Logger.Warning($"Failed to load Shader AssetBundle. GPU color conversion will be disabled and fallback to CPU. Error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ namespace ADOFAIRenderer
|
||||
var currentModule = EventSystem.current.currentInputModule;
|
||||
|
||||
if (currentModule) return;
|
||||
Melon<RenderMod>.Logger.Msg("Unity 6 인풋 모듈 이식.");
|
||||
Melon<RenderMod>.Logger.Msg("Ported Unity 6 Input Module.");
|
||||
|
||||
var newModule = EventSystem.current.gameObject.AddComponent<StandaloneInputModule>();
|
||||
newModule.enabled = true;
|
||||
@@ -71,7 +71,7 @@ namespace ADOFAIRenderer
|
||||
public override void OnApplicationQuit()
|
||||
{
|
||||
if (RenderManager.Instance == null) return;
|
||||
Melon<RenderMod>.Logger.Msg("MelonLoader 종료 감지: RenderManager의 활성 프로세스를 강제 수거 및 종료합니다.");
|
||||
Melon<RenderMod>.Logger.Msg("MelonLoader shutdown detected: Forcefully terminating and cleaning up active RenderManager processes.");
|
||||
RenderManager.Instance.CleanUpActiveRender();
|
||||
}
|
||||
}
|
||||
|
||||
+20
-20
@@ -63,11 +63,11 @@ internal class CommonWarningDialogUI : MonoBehaviour
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 로딩 실패
|
||||
// Loading failed
|
||||
}
|
||||
}
|
||||
|
||||
// 폴백 처리
|
||||
// Fallback processing
|
||||
if (!font)
|
||||
{
|
||||
try
|
||||
@@ -121,15 +121,15 @@ internal class CommonWarningDialogUI : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
// UI 레이어(5) 설정
|
||||
// Set UI layer (5)
|
||||
gameObject.layer = 5;
|
||||
|
||||
// Canvas 설정
|
||||
// Canvas setup
|
||||
var canvas = gameObject.AddComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
canvas.sortingOrder = 32766; // FFmpegDownloadUI(32767) 바로 아래
|
||||
canvas.sortingOrder = 32766; // Just below FFmpegDownloadUI (32767)
|
||||
|
||||
// 해상도 대응 캔버스 스케일러 설정
|
||||
// Responsive Canvas Scaler setup
|
||||
var scaler = gameObject.AddComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(1920, 1080);
|
||||
@@ -138,13 +138,13 @@ internal class CommonWarningDialogUI : MonoBehaviour
|
||||
|
||||
gameObject.AddComponent<GraphicRaycaster>();
|
||||
|
||||
// 1. 전체 화면 오버레이 배경 생성
|
||||
// 1. Create full screen overlay background
|
||||
var bgPanelObj = new GameObject("BackgroundPanel");
|
||||
bgPanelObj.transform.SetParent(transform, false);
|
||||
bgPanelObj.layer = 5;
|
||||
|
||||
var bgImage = bgPanelObj.AddComponent<Image>();
|
||||
bgImage.color = new Color(0f, 0f, 0f, 0.65f); // 어두운 투명 오버레이
|
||||
bgImage.color = new Color(0f, 0f, 0f, 0.65f); // Dark transparent overlay
|
||||
bgImage.raycastTarget = true;
|
||||
|
||||
var bgRect = bgPanelObj.GetComponent<RectTransform>();
|
||||
@@ -155,17 +155,17 @@ internal class CommonWarningDialogUI : MonoBehaviour
|
||||
|
||||
var font = GetNanumSquareNeoBdFontAsset();
|
||||
|
||||
// 2. 모달 컨테이너 생성
|
||||
// 2. Create modal container
|
||||
var modalContainerObj = new GameObject("ModalContainer");
|
||||
modalContainerObj.transform.SetParent(bgPanelObj.transform, false);
|
||||
modalContainerObj.layer = 5;
|
||||
|
||||
var modalImage = modalContainerObj.AddComponent<Image>();
|
||||
modalImage.color = new Color(0.08f, 0.08f, 0.1f, 0.88f); // 기존 UI의 배경 톤과 동일
|
||||
modalImage.color = new Color(0.08f, 0.08f, 0.1f, 0.88f); // Matching background tone of existing UI
|
||||
|
||||
var modalOutline = modalContainerObj.AddComponent<Outline>();
|
||||
modalOutline.effectColor = Color.white;
|
||||
modalOutline.effectDistance = new Vector2(3.0f, -3.0f); // 기존 UI 외곽선 스타일 일관성
|
||||
modalOutline.effectDistance = new Vector2(3.0f, -3.0f); // Consistent with existing UI outline style
|
||||
|
||||
var modalRect = modalContainerObj.GetComponent<RectTransform>();
|
||||
modalRect.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
@@ -173,7 +173,7 @@ internal class CommonWarningDialogUI : MonoBehaviour
|
||||
modalRect.pivot = new Vector2(0.5f, 0.5f);
|
||||
modalRect.sizeDelta = new Vector2(650, 360);
|
||||
|
||||
// 3. 타이틀 텍스트 생성
|
||||
// 3. Create title text
|
||||
var titleObj = new GameObject("TitleText");
|
||||
titleObj.transform.SetParent(modalContainerObj.transform, false);
|
||||
titleObj.layer = 5;
|
||||
@@ -182,7 +182,7 @@ internal class CommonWarningDialogUI : MonoBehaviour
|
||||
titleText.text = title;
|
||||
titleText.fontSize = 24;
|
||||
titleText.font = font;
|
||||
titleText.color = new Color(1.0f, 0.8f, 0.2f, 1.0f); // 경고 테마에 맞는 주황/황색 계열
|
||||
titleText.color = new Color(1.0f, 0.8f, 0.2f, 1.0f); // Orange/yellow tone matching warning theme
|
||||
titleText.alignment = TextAlignmentOptions.Center;
|
||||
|
||||
var titleRect = titleObj.GetComponent<RectTransform>();
|
||||
@@ -192,7 +192,7 @@ internal class CommonWarningDialogUI : MonoBehaviour
|
||||
titleRect.anchoredPosition = new Vector2(0, -35);
|
||||
titleRect.sizeDelta = new Vector2(0, 40);
|
||||
|
||||
// 4. 본문 설명 텍스트 생성
|
||||
// 4. Create description text
|
||||
var descObj = new GameObject("DescriptionText");
|
||||
descObj.transform.SetParent(modalContainerObj.transform, false);
|
||||
descObj.layer = 5;
|
||||
@@ -212,7 +212,7 @@ internal class CommonWarningDialogUI : MonoBehaviour
|
||||
descRect.anchoredPosition = new Vector2(0, -65);
|
||||
descRect.sizeDelta = new Vector2(-40, 120);
|
||||
|
||||
// 5. 버튼 컨테이너 생성
|
||||
// 5. Create button container
|
||||
var buttonContainerObj = new GameObject("ButtonContainer", typeof(RectTransform));
|
||||
buttonContainerObj.transform.SetParent(modalContainerObj.transform, false);
|
||||
buttonContainerObj.layer = 5;
|
||||
@@ -224,7 +224,7 @@ internal class CommonWarningDialogUI : MonoBehaviour
|
||||
btnContainerRect.anchoredPosition = new Vector2(0, 35);
|
||||
btnContainerRect.sizeDelta = new Vector2(0, 50);
|
||||
|
||||
// 6. 가변 버튼 동적 배치 및 스타일링
|
||||
// 6. Dynamic button layout and styling
|
||||
float totalWidth = 560f;
|
||||
float spacing = 15f;
|
||||
float buttonHeight = 45f;
|
||||
@@ -245,8 +245,8 @@ internal class CommonWarningDialogUI : MonoBehaviour
|
||||
btnObj.transform.SetParent(buttonContainerObj.transform, false);
|
||||
btnObj.layer = 5;
|
||||
|
||||
// 기본 색상 정의
|
||||
Color baseColor = btnData.BackgroundColor ?? new Color(0.2f, 0.2f, 0.23f, 1f); // 커스텀 색상 또는 회색톤 기본값
|
||||
// Define default colors
|
||||
Color baseColor = btnData.BackgroundColor ?? new Color(0.2f, 0.2f, 0.23f, 1f); // Custom color or gray default
|
||||
Color hoverColor = ApplyColorBrightness(baseColor, 1.2f);
|
||||
Color pressedColor = ApplyColorBrightness(baseColor, 0.8f);
|
||||
|
||||
@@ -257,7 +257,7 @@ internal class CommonWarningDialogUI : MonoBehaviour
|
||||
var btn = btnObj.AddComponent<Button>();
|
||||
SetupButtonStyle(btn, baseColor, hoverColor, pressedColor);
|
||||
|
||||
// 클릭 콜백 등록
|
||||
// Register click callback
|
||||
btn.onClick.AddListener(() =>
|
||||
{
|
||||
try
|
||||
@@ -283,7 +283,7 @@ internal class CommonWarningDialogUI : MonoBehaviour
|
||||
btnRect.anchoredPosition = new Vector2(posX, 0);
|
||||
btnRect.sizeDelta = new Vector2(buttonWidth, buttonHeight);
|
||||
|
||||
// 텍스트 라벨 추가
|
||||
// Add text label
|
||||
var txtObj = new GameObject("Text");
|
||||
txtObj.transform.SetParent(btnObj.transform, false);
|
||||
txtObj.layer = 5;
|
||||
|
||||
+18
-18
@@ -15,14 +15,14 @@ internal class DebugMonitorUI : MonoBehaviour
|
||||
private const float BarHeight = 28f;
|
||||
private const float BarPosX = 0f;
|
||||
private const float BarPosY = -420f; // Y position relative to screen center (Reference: 1920x1080)
|
||||
private const int BarTextFontSize = 16; // 눈에 띄게 증가된 텍스트 크기
|
||||
private const int BarTextFontSize = 16; // Increased font size for visibility
|
||||
private static readonly Color BarBackgroundColor = new(0.08f, 0.08f, 0.1f, 1.0f);
|
||||
private static readonly Color BarFillColor = new(0.0f, 0.8f, 1.0f, 1.0f); // Sleek cyan/blue
|
||||
private static readonly Color BarTextColor = new(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
|
||||
// --- Progress Bar Outline Configuration Constants ---
|
||||
private static readonly Color BarOutlineColor = Color.white; // 대비되는 흰색 윤곽선
|
||||
private static readonly Vector2 BarOutlineDistance = new(3.0f, -3.0f); // 3px의 굵고 직관적인 윤곽선 두께
|
||||
private static readonly Color BarOutlineColor = Color.white; // Contrasting white outline
|
||||
private static readonly Vector2 BarOutlineDistance = new(3.0f, -3.0f); // Outline thickness
|
||||
|
||||
private TextMeshProUGUI _text = null!;
|
||||
private GameObject _progressBarObj = null!;
|
||||
@@ -41,11 +41,11 @@ internal class DebugMonitorUI : MonoBehaviour
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 로딩 실패
|
||||
// Loading failed
|
||||
}
|
||||
}
|
||||
|
||||
// 폴백 처리: 에셋 번들에서 실패 시 게임 자체 NanumSquareNeo 또는 내장 TMP SDF 폰트 사용
|
||||
// Fallback: Use game's own font or built-in TMP SDF font if AssetBundle loading fails
|
||||
if (font == null)
|
||||
{
|
||||
font = RDString.fontData.fontTMP;
|
||||
@@ -65,15 +65,15 @@ internal class DebugMonitorUI : MonoBehaviour
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// UI 레이어(5) 설정
|
||||
// Set UI layer (5)
|
||||
gameObject.layer = 5;
|
||||
|
||||
// Canvas 설정
|
||||
// Canvas setup
|
||||
var canvas = gameObject.AddComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
canvas.sortingOrder = 9999;
|
||||
|
||||
// 해상도 다이내믹 스케일링 설정 (Reference: 1920x1080)
|
||||
// Dynamic resolution scaling setup (Reference: 1920x1080)
|
||||
var scaler = gameObject.AddComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(1920, 1080);
|
||||
@@ -82,13 +82,13 @@ internal class DebugMonitorUI : MonoBehaviour
|
||||
|
||||
gameObject.AddComponent<GraphicRaycaster>();
|
||||
|
||||
// 배경 패널 생성
|
||||
// Create background panel
|
||||
var panelObj = new GameObject("BackgroundPanel");
|
||||
panelObj.transform.SetParent(transform, false);
|
||||
panelObj.layer = 5;
|
||||
|
||||
var image = panelObj.AddComponent<Image>();
|
||||
image.color = new Color(0.08f, 0.08f, 0.1f, 0.88f); // 약간 더 고급스러운 다크톤 아크릴 감성
|
||||
image.color = new Color(0.08f, 0.08f, 0.1f, 0.88f); // Dark tone acrylic style
|
||||
|
||||
var panelRect = panelObj.GetComponent<RectTransform>();
|
||||
panelRect.anchorMin = new Vector2(1, 1);
|
||||
@@ -97,28 +97,28 @@ internal class DebugMonitorUI : MonoBehaviour
|
||||
panelRect.anchoredPosition = new Vector2(-30, -30);
|
||||
panelRect.sizeDelta = new Vector2(360, 220);
|
||||
|
||||
// 텍스트 생성
|
||||
// Create text
|
||||
var textObj = new GameObject("MonitorText");
|
||||
textObj.transform.SetParent(panelObj.transform, false);
|
||||
textObj.layer = 5;
|
||||
|
||||
_text = textObj.AddComponent<TextMeshProUGUI>();
|
||||
_text.richText = true;
|
||||
_text.fontSize = 16; // 13에서 16으로 증가
|
||||
_text.fontSize = 16;
|
||||
_text.alignment = TextAlignmentOptions.TopLeft;
|
||||
|
||||
// TMPro 텍스트 잘림 현상 예방
|
||||
// Prevent TMPro text clipping
|
||||
_text.textWrappingMode = TextWrappingModes.Normal;
|
||||
_text.overflowMode = TextOverflowModes.Overflow;
|
||||
|
||||
// NanumSquareNeo-Bd SDF 폰트 로드 및 적용
|
||||
// Load and apply font
|
||||
var font = GetNanumSquareNeoBdFontAsset();
|
||||
_text.font = font;
|
||||
|
||||
var textRect = textObj.GetComponent<RectTransform>();
|
||||
textRect.anchorMin = Vector2.zero;
|
||||
textRect.anchorMax = Vector2.one;
|
||||
textRect.offsetMin = new Vector2(20, 20); // 좀 더 넉넉하고 패딩 여백
|
||||
textRect.offsetMin = new Vector2(20, 20); // Padding margin
|
||||
textRect.offsetMax = new Vector2(-20, -20);
|
||||
|
||||
// --- Progress Bar UI Setup ---
|
||||
@@ -130,7 +130,7 @@ internal class DebugMonitorUI : MonoBehaviour
|
||||
var bgImage = _progressBarObj.AddComponent<Image>();
|
||||
bgImage.color = BarBackgroundColor;
|
||||
|
||||
// 프로그레스 바 흰색 윤곽선 컴포넌트 추가
|
||||
// Add progress bar outline component
|
||||
var bgOutline = _progressBarObj.AddComponent<Outline>();
|
||||
bgOutline.effectColor = BarOutlineColor;
|
||||
bgOutline.effectDistance = BarOutlineDistance;
|
||||
@@ -164,12 +164,12 @@ internal class DebugMonitorUI : MonoBehaviour
|
||||
|
||||
_progressText = progressTextObj.AddComponent<TextMeshProUGUI>();
|
||||
_progressText.richText = true;
|
||||
_progressText.fontSize = BarTextFontSize; // 14에서 18로 증가
|
||||
_progressText.fontSize = BarTextFontSize;
|
||||
_progressText.alignment = TextAlignmentOptions.Center;
|
||||
_progressText.font = font;
|
||||
_progressText.color = BarTextColor;
|
||||
|
||||
// 프로그레스 바 내부 텍스트 아웃라인 추가 (진행율 가독성 확보)
|
||||
// Add progress bar text outline for readability
|
||||
var textOutline = progressTextObj.AddComponent<Outline>();
|
||||
textOutline.effectColor = new Color(0f, 0f, 0f, 0.9f);
|
||||
textOutline.effectDistance = new Vector2(1.5f, -1.5f);
|
||||
|
||||
+17
-18
@@ -14,15 +14,15 @@ internal class EscapeHandlerUI : MonoBehaviour
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// UI 레이어(5) 설정
|
||||
// Set UI layer (5)
|
||||
gameObject.layer = 5;
|
||||
|
||||
// Canvas 설정
|
||||
// Canvas setup
|
||||
var canvas = gameObject.AddComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
canvas.sortingOrder = 10000; // 디버그 모니터보다 위에 오도록 설정
|
||||
canvas.sortingOrder = 10000; // Placed above debug monitor
|
||||
|
||||
// 해상도 대응 캔버스 스케일러 설정
|
||||
// Responsive Canvas Scaler setup
|
||||
var scaler = gameObject.AddComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(1920, 1080);
|
||||
@@ -31,23 +31,22 @@ internal class EscapeHandlerUI : MonoBehaviour
|
||||
|
||||
gameObject.AddComponent<GraphicRaycaster>();
|
||||
|
||||
// 1. 컨테이너 및 배경 패널 오브젝트 생성
|
||||
// 1. Create container and background panel object
|
||||
_containerObj = new GameObject("EscapeContainer");
|
||||
_containerObj.transform.SetParent(transform, false);
|
||||
_containerObj.layer = 5;
|
||||
|
||||
var bgImage = _containerObj.AddComponent<Image>();
|
||||
// 고급스러운 다크톤 아크릴 감성
|
||||
bgImage.color = new Color(0.05f, 0.05f, 0.07f, 0.85f);
|
||||
bgImage.color = new Color(0.05f, 0.05f, 0.07f, 0.85f); // Dark tone acrylic style
|
||||
|
||||
var containerRect = _containerObj.GetComponent<RectTransform>();
|
||||
containerRect.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
containerRect.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
containerRect.pivot = new Vector2(0.5f, 0.5f);
|
||||
containerRect.anchoredPosition = Vector2.zero; // 화면 중앙
|
||||
containerRect.anchoredPosition = Vector2.zero; // Screen center
|
||||
containerRect.sizeDelta = new Vector2(460, 160);
|
||||
|
||||
// 2. 텍스트 오브젝트 생성
|
||||
// 2. Create text object
|
||||
var textObj = new GameObject("EscapeText");
|
||||
textObj.transform.SetParent(_containerObj.transform, false);
|
||||
textObj.layer = 5;
|
||||
@@ -58,7 +57,7 @@ internal class EscapeHandlerUI : MonoBehaviour
|
||||
_textComponent.textWrappingMode = TextWrappingModes.NoWrap;
|
||||
_textComponent.richText = true;
|
||||
|
||||
// 게임의 기본 폰트 적용 (RDString.fontData.fontTMP)
|
||||
// Apply game's default font (RDString.fontData.fontTMP)
|
||||
if (RDString.fontData.fontTMP != null)
|
||||
_textComponent.font = RDString.fontData.fontTMP;
|
||||
|
||||
@@ -68,13 +67,13 @@ internal class EscapeHandlerUI : MonoBehaviour
|
||||
textRect.offsetMin = new Vector2(10, 10);
|
||||
textRect.offsetMax = new Vector2(-10, -10);
|
||||
|
||||
// 초기 상태는 비활성화
|
||||
// Initial state disabled
|
||||
_containerObj.SetActive(false);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// 렌더링이 더 이상 활성화되어 있지 않으면 자가 파괴
|
||||
// Destroy self if rendering is no longer active
|
||||
if (!RenderManager.IsActive)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
@@ -93,10 +92,10 @@ internal class EscapeHandlerUI : MonoBehaviour
|
||||
var elapsed = (float)_escapeStopwatch.Elapsed.TotalSeconds;
|
||||
var remaining = Mathf.Max(0f, 3f - elapsed);
|
||||
|
||||
// 남은 시간 텍스트 업데이트
|
||||
_textComponent.text = $"<color=#FF5252>탈출까지 {remaining:F1}초..</color>\n<size=40><color=#DDDDDD>Esc 키를 계속 누르고 계세요</color></size>";
|
||||
// Update remaining time text
|
||||
_textComponent.text = $"<color=#FF5252>Cancelling in {remaining:F1}s..</color>\n<size=40><color=#DDDDDD>Hold Esc to cancel render</color></size>";
|
||||
|
||||
// 마이크로 펄스 애니메이션 적용 (누르는 동안 컨테이너 전체 크기가 부드럽게 커짐)
|
||||
// Micro pulse animation (container smoothly scales up while holding)
|
||||
var scale = 1.0f + elapsed / 3.0f * 0.15f;
|
||||
_containerObj.transform.localScale = new Vector3(scale, scale, 1f);
|
||||
|
||||
@@ -104,10 +103,10 @@ internal class EscapeHandlerUI : MonoBehaviour
|
||||
|
||||
_escapeStopwatch.Stop();
|
||||
_escapeStopwatch = null;
|
||||
_containerObj.transform.localScale = Vector3.one; // 스케일 초기화
|
||||
_containerObj.transform.localScale = Vector3.one; // Reset scale
|
||||
_containerObj.SetActive(false);
|
||||
|
||||
// 렌더링 취소 및 씬 이탈
|
||||
// Cancel rendering and exit scene
|
||||
if (RenderManager.Instance)
|
||||
RenderManager.Instance.CancelRender();
|
||||
}
|
||||
@@ -116,7 +115,7 @@ internal class EscapeHandlerUI : MonoBehaviour
|
||||
if (_escapeStopwatch == null) return;
|
||||
_escapeStopwatch.Stop();
|
||||
_escapeStopwatch = null;
|
||||
_containerObj.transform.localScale = Vector3.one; // 스케일 초기화
|
||||
_containerObj.transform.localScale = Vector3.one; // Reset scale
|
||||
_containerObj.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
+58
-58
@@ -96,11 +96,11 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 로딩 실패
|
||||
// Loading failed
|
||||
}
|
||||
}
|
||||
|
||||
// 폴백 처리
|
||||
// Fallback processing
|
||||
if (font == null)
|
||||
{
|
||||
try
|
||||
@@ -127,15 +127,15 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// UI 레이어(5) 설정
|
||||
// Set UI layer (5)
|
||||
gameObject.layer = 5;
|
||||
|
||||
// Canvas 설정
|
||||
// Canvas setup
|
||||
var canvas = gameObject.AddComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
canvas.sortingOrder = 32767; // 최상위
|
||||
canvas.sortingOrder = 32767; // Topmost
|
||||
|
||||
// 해상도 대응 캔버스 스케일러 설정
|
||||
// Responsive Canvas Scaler setup
|
||||
var scaler = gameObject.AddComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(1920, 1080);
|
||||
@@ -144,13 +144,13 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
|
||||
gameObject.AddComponent<GraphicRaycaster>();
|
||||
|
||||
// 1. 전체 화면 오버레이 배경 생성
|
||||
// 1. Create full screen overlay background
|
||||
var bgPanelObj = new GameObject("BackgroundPanel");
|
||||
bgPanelObj.transform.SetParent(transform, false);
|
||||
bgPanelObj.layer = 5;
|
||||
|
||||
var bgImage = bgPanelObj.AddComponent<Image>();
|
||||
bgImage.color = new Color(0f, 0f, 0f, 0.65f); // 어두운 투명 오버레이
|
||||
bgImage.color = new Color(0f, 0f, 0f, 0.65f); // Dark transparent overlay
|
||||
bgImage.raycastTarget = true;
|
||||
|
||||
var bgRect = bgPanelObj.GetComponent<RectTransform>();
|
||||
@@ -161,16 +161,16 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
|
||||
var font = GetNanumSquareNeoBdFontAsset();
|
||||
|
||||
// 2. 모달 컨테이너 생성 (다른 UI들과 디자인 일관성을 위해 동일한 아크릴 다크톤과 흰색 굵은 외곽선 적용)
|
||||
// 2. Create modal container
|
||||
var modalContainerObj = new GameObject("ModalContainer");
|
||||
modalContainerObj.transform.SetParent(bgPanelObj.transform, false);
|
||||
modalContainerObj.layer = 5;
|
||||
|
||||
var modalImage = modalContainerObj.AddComponent<Image>();
|
||||
modalImage.color = new Color(0.08f, 0.08f, 0.1f, 0.88f); // 다른 UI의 배경 톤과 동일
|
||||
modalImage.color = new Color(0.08f, 0.08f, 0.1f, 0.88f);
|
||||
|
||||
var modalOutline = modalContainerObj.AddComponent<Outline>();
|
||||
modalOutline.effectColor = Color.white; // DebugMonitorUI와 일치하는 흰색 외곽선
|
||||
modalOutline.effectColor = Color.white;
|
||||
modalOutline.effectDistance = new Vector2(3.0f, -3.0f);
|
||||
|
||||
var modalRect = modalContainerObj.GetComponent<RectTransform>();
|
||||
@@ -179,13 +179,13 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
modalRect.pivot = new Vector2(0.5f, 0.5f);
|
||||
modalRect.sizeDelta = new Vector2(600, 320);
|
||||
|
||||
// 4. 타이틀 텍스트 생성
|
||||
// 4. Create title text
|
||||
var titleObj = new GameObject("TitleText");
|
||||
titleObj.transform.SetParent(modalContainerObj.transform, false);
|
||||
titleObj.layer = 5;
|
||||
|
||||
var titleText = titleObj.AddComponent<TextMeshProUGUI>();
|
||||
titleText.text = "FFmpeg 라이브러리 설치 필요";
|
||||
titleText.text = "FFmpeg Library Installation Required";
|
||||
titleText.fontSize = 26;
|
||||
titleText.font = font;
|
||||
titleText.color = Color.white;
|
||||
@@ -198,23 +198,23 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
titleRect.anchoredPosition = new Vector2(0, -35);
|
||||
titleRect.sizeDelta = new Vector2(0, 40);
|
||||
|
||||
// 5. 본문 설명 텍스트 생성
|
||||
// 5. Create description text
|
||||
var descObj = new GameObject("DescriptionText");
|
||||
descObj.transform.SetParent(modalContainerObj.transform, false);
|
||||
descObj.layer = 5;
|
||||
|
||||
var descText = descObj.AddComponent<TextMeshProUGUI>();
|
||||
var descStrBuilder = new StringBuilder();
|
||||
descStrBuilder.AppendLine("동영상을 렌더링하려면 FFmpeg 공유 라이브러리가 필요합니다.");
|
||||
descStrBuilder.AppendLine("FFmpeg shared libraries are required to render video.");
|
||||
if (Application.platform == RuntimePlatform.OSXPlayer)
|
||||
{
|
||||
descStrBuilder.AppendLine("OSX 플랫폼의 경우 신뢰 가능한 공개 빌드가 없어 수동으로 FFmpeg 8.1을 빌드하여야 합니다.");
|
||||
descStrBuilder.AppendLine("아래 버튼을 눌러 ffmpeg 공식 웹사이트를 열 수 있습니다.");
|
||||
descStrBuilder.AppendLine("For macOS, reliable public builds are unavailable, so FFmpeg 8.1 must be built manually.");
|
||||
descStrBuilder.AppendLine("Click the button below to open the official FFmpeg website.");
|
||||
}
|
||||
else
|
||||
{
|
||||
descStrBuilder.AppendLine("아래 버튼을 눌러 인터넷에서 자동으로 다운로드하여 설치합니다.");
|
||||
descStrBuilder.AppendLine("<color=#aaaaaa><size=13>(다운로드 크기: 약 70MB, UserLibs 폴더에 압축 해제됨)</size></color>");
|
||||
descStrBuilder.AppendLine("Click the button below to automatically download and install from the internet.");
|
||||
descStrBuilder.AppendLine("<color=#aaaaaa><size=13>(Download size: ~70MB, extracted to UserLibs folder)</size></color>");
|
||||
}
|
||||
|
||||
descText.text = descStrBuilder.ToString();
|
||||
@@ -231,7 +231,7 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
descRect.anchoredPosition = new Vector2(0, -60);
|
||||
descRect.sizeDelta = new Vector2(-40, 100);
|
||||
|
||||
// 6. 버튼 컨테이너 생성
|
||||
// 6. Create button container
|
||||
_buttonContainerObj = new GameObject("ButtonContainer", typeof(RectTransform));
|
||||
_buttonContainerObj.transform.SetParent(modalContainerObj.transform, false);
|
||||
_buttonContainerObj.layer = 5;
|
||||
@@ -243,7 +243,7 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
btnContainerRect.anchoredPosition = new Vector2(0, 35);
|
||||
btnContainerRect.sizeDelta = new Vector2(0, 50);
|
||||
|
||||
// 7. 다운로드 버튼 생성
|
||||
// 7. Create download button
|
||||
var downloadBtnObj = new GameObject("DownloadButton");
|
||||
downloadBtnObj.transform.SetParent(_buttonContainerObj.transform, false);
|
||||
downloadBtnObj.layer = 5;
|
||||
@@ -273,7 +273,7 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
downloadTextObj.layer = 5;
|
||||
|
||||
var downloadText = downloadTextObj.AddComponent<TextMeshProUGUI>();
|
||||
downloadText.text = Application.platform != RuntimePlatform.OSXPlayer ? "다운로드 및 설치" : "사이트 열기";
|
||||
downloadText.text = Application.platform != RuntimePlatform.OSXPlayer ? "Download & Install" : "Open Website";
|
||||
downloadText.fontSize = 20;
|
||||
downloadText.font = font;
|
||||
downloadText.color = Color.white;
|
||||
@@ -285,7 +285,7 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
downloadTextRect.offsetMin = Vector2.zero;
|
||||
downloadTextRect.offsetMax = Vector2.zero;
|
||||
|
||||
// 8. 취소 버튼 생성
|
||||
// 8. Create cancel button
|
||||
var cancelBtnObj = new GameObject("CancelButton");
|
||||
cancelBtnObj.transform.SetParent(_buttonContainerObj.transform, false);
|
||||
cancelBtnObj.layer = 5;
|
||||
@@ -310,7 +310,7 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
cancelTextObj.layer = 5;
|
||||
|
||||
var cancelText = cancelTextObj.AddComponent<TextMeshProUGUI>();
|
||||
cancelText.text = "취소";
|
||||
cancelText.text = "Cancel";
|
||||
cancelText.fontSize = 20;
|
||||
cancelText.font = font;
|
||||
cancelText.color = new Color(0.9f, 0.9f, 0.9f, 1f);
|
||||
@@ -322,18 +322,18 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
cancelTextRect.offsetMin = Vector2.zero;
|
||||
cancelTextRect.offsetMax = Vector2.zero;
|
||||
|
||||
// 9. 프로그레스 바 컨테이너 생성
|
||||
// 9. Create progress bar container
|
||||
_progressBarObj = new GameObject("ProgressBarContainer");
|
||||
_progressBarObj.transform.SetParent(modalContainerObj.transform, false);
|
||||
_progressBarObj.layer = 5;
|
||||
_progressBarObj.SetActive(false);
|
||||
|
||||
var barBgImg = _progressBarObj.AddComponent<Image>();
|
||||
barBgImg.color = new Color(0.08f, 0.08f, 0.1f, 1.0f); // DebugMonitorUI의 BarBackgroundColor와 동일
|
||||
barBgImg.color = new Color(0.08f, 0.08f, 0.1f, 1.0f);
|
||||
|
||||
var barBgOutline = _progressBarObj.AddComponent<Outline>();
|
||||
barBgOutline.effectColor = Color.white; // DebugMonitorUI의 BarOutlineColor와 동일
|
||||
barBgOutline.effectDistance = new Vector2(3.0f, -3.0f); // DebugMonitorUI의 BarOutlineDistance와 동일
|
||||
barBgOutline.effectColor = Color.white;
|
||||
barBgOutline.effectDistance = new Vector2(3.0f, -3.0f);
|
||||
|
||||
var barRect = _progressBarObj.GetComponent<RectTransform>();
|
||||
barRect.anchorMin = new Vector2(0.5f, 0);
|
||||
@@ -342,28 +342,28 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
barRect.anchoredPosition = new Vector2(0, 75);
|
||||
barRect.sizeDelta = new Vector2(480, 24);
|
||||
|
||||
// 10. 프로그레스 바 채우기 이미지 생성
|
||||
// 10. Create progress bar fill image
|
||||
_progressBarFillObj = new GameObject("ProgressBarFill");
|
||||
_progressBarFillObj.transform.SetParent(_progressBarObj.transform, false);
|
||||
_progressBarFillObj.layer = 5;
|
||||
|
||||
var fillImg = _progressBarFillObj.AddComponent<Image>();
|
||||
fillImg.color = new Color(0.0f, 0.8f, 1.0f, 1.0f); // DebugMonitorUI의 BarFillColor와 동일
|
||||
fillImg.color = new Color(0.0f, 0.8f, 1.0f, 1.0f);
|
||||
|
||||
_fillRect = _progressBarFillObj.GetComponent<RectTransform>();
|
||||
_fillRect.anchorMin = new Vector2(0, 0);
|
||||
_fillRect.anchorMax = new Vector2(0, 1); // anchorMax.x가 동적으로 변경됨
|
||||
_fillRect.anchorMax = new Vector2(0, 1);
|
||||
_fillRect.pivot = new Vector2(0, 0.5f);
|
||||
_fillRect.anchoredPosition = Vector2.zero;
|
||||
_fillRect.sizeDelta = Vector2.zero;
|
||||
|
||||
// 11. 프로그레스 텍스트 생성 (진행률 표시)
|
||||
// 11. Create progress text
|
||||
var progressTextObj = new GameObject("ProgressBarText");
|
||||
progressTextObj.transform.SetParent(_progressBarObj.transform, false);
|
||||
progressTextObj.layer = 5;
|
||||
|
||||
_progressText = progressTextObj.AddComponent<TextMeshProUGUI>();
|
||||
_progressText.fontSize = 16; // DebugMonitorUI의 BarTextFontSize와 일치
|
||||
_progressText.fontSize = 16;
|
||||
_progressText.font = font;
|
||||
_progressText.color = Color.white;
|
||||
_progressText.alignment = TextAlignmentOptions.Center;
|
||||
@@ -378,14 +378,14 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
progressTextRect.offsetMin = Vector2.zero;
|
||||
progressTextRect.offsetMax = Vector2.zero;
|
||||
|
||||
// 12. 상태 텍스트 생성
|
||||
// 12. Create status text
|
||||
_statusTextObj = new GameObject("StatusText");
|
||||
_statusTextObj.transform.SetParent(modalContainerObj.transform, false);
|
||||
_statusTextObj.layer = 5;
|
||||
_statusTextObj.SetActive(false);
|
||||
|
||||
_statusText = _statusTextObj.AddComponent<TextMeshProUGUI>();
|
||||
_statusText.fontSize = 16; // DebugMonitorUI의 텍스트 크기와 일치
|
||||
_statusText.fontSize = 16;
|
||||
_statusText.font = font;
|
||||
_statusText.color = new Color(0.8f, 0.8f, 0.8f, 1f);
|
||||
_statusText.alignment = TextAlignmentOptions.Center;
|
||||
@@ -411,7 +411,7 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// 백그라운드 스레드에서 메인 스레드로 넘겨진 작업 순차 실행
|
||||
// Execute queued actions on main thread
|
||||
while (_mainThreadQueue.TryDequeue(out var action))
|
||||
{
|
||||
action?.Invoke();
|
||||
@@ -428,20 +428,20 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
if (_isDownloading) return;
|
||||
_isDownloading = true;
|
||||
|
||||
// 버튼 숨기고 프로그레스 바 및 상태 텍스트 표시
|
||||
// Hide buttons and show progress bar & status text
|
||||
_buttonContainerObj.SetActive(false);
|
||||
_progressBarObj.SetActive(true);
|
||||
_statusTextObj.SetActive(true);
|
||||
|
||||
_cts = new System.Threading.CancellationTokenSource();
|
||||
|
||||
// 백그라운드 스레드에서 다운로드 시작
|
||||
// Start download in background thread
|
||||
Task.Run(async () =>
|
||||
{
|
||||
string tempZipPath = "";
|
||||
try
|
||||
{
|
||||
UpdateStatus("다운로드 시작하는 중...", new Color(0.8f, 0.8f, 0.8f));
|
||||
UpdateStatus("Starting download...", new Color(0.8f, 0.8f, 0.8f));
|
||||
UpdateProgress(0f, "0%");
|
||||
|
||||
var userLibsDir = MelonEnvironment.UserLibsDirectory;
|
||||
@@ -461,24 +461,24 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
var progressReporter = new Progress<float>(percent =>
|
||||
{
|
||||
UpdateProgress(percent, $"{percent * 100f:F1}%");
|
||||
UpdateStatus($"다운로드 중... ({(percent * 100f):F1}%)", new Color(0f, 0.8f, 1f));
|
||||
UpdateStatus($"Downloading... ({(percent * 100f):F1}%)", new Color(0f, 0.8f, 1f));
|
||||
});
|
||||
|
||||
await DownloadFileAsync(DownloadUrl, tempZipPath, progressReporter, _cts.Token);
|
||||
|
||||
// 다운로드 완료 후 압축 해제
|
||||
UpdateStatus("압축 해제 및 DLL 파일 복사 중...", new Color(0.9f, 0.9f, 0.2f));
|
||||
UpdateProgress(0.99f, "압축 해제 중...");
|
||||
// Extract after download complete
|
||||
UpdateStatus("Extracting and copying library files...", new Color(0.9f, 0.9f, 0.2f));
|
||||
UpdateProgress(0.99f, "Extracting...");
|
||||
|
||||
await ExtractDllsAsync(tempZipPath, userLibsDir);
|
||||
|
||||
// 완료 상태 업데이트
|
||||
UpdateStatus("FFmpeg 설치 완료!", new Color(0.2f, 0.9f, 0.2f));
|
||||
// Update completion status
|
||||
UpdateStatus("FFmpeg installation complete!", new Color(0.2f, 0.9f, 0.2f));
|
||||
UpdateProgress(1f, "100%");
|
||||
|
||||
await Task.Delay(1000);
|
||||
|
||||
// 메인 스레드에서 UI를 삭제
|
||||
// Destroy UI on main thread
|
||||
ExecuteOnMainThread(() =>
|
||||
{
|
||||
Destroy(gameObject);
|
||||
@@ -486,7 +486,7 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 취소 시 임시 파일 정리
|
||||
// Clean up temporary file on cancellation
|
||||
if (!string.IsNullOrEmpty(tempZipPath) && File.Exists(tempZipPath))
|
||||
{
|
||||
try { File.Delete(tempZipPath); }
|
||||
@@ -495,14 +495,14 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
UpdateStatus("다운로드가 취소되었습니다.", new Color(0.9f, 0.5f, 0.2f));
|
||||
UpdateStatus("Download was cancelled.", new Color(0.9f, 0.5f, 0.2f));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Melon<RenderMod>.Logger.Error($"FFmpeg 다운로드/설치 중 에러 발생: {ex}");
|
||||
UpdateStatus($"설치 실패: {ex.Message}", new Color(1f, 0.3f, 0.3f));
|
||||
Melon<RenderMod>.Logger.Error($"Error during FFmpeg download/installation: {ex}");
|
||||
UpdateStatus($"Installation failed: {ex.Message}", new Color(1f, 0.3f, 0.3f));
|
||||
|
||||
// 실패 시 버튼 재설정
|
||||
// Reset buttons on failure
|
||||
ExecuteOnMainThread(() =>
|
||||
{
|
||||
_isDownloading = false;
|
||||
@@ -510,7 +510,7 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
var downloadBtnText = _buttonContainerObj.transform.Find("DownloadButton/DownloadText")?.GetComponent<TextMeshProUGUI>();
|
||||
if (downloadBtnText)
|
||||
{
|
||||
downloadBtnText.text = "재시도 (Retry)";
|
||||
downloadBtnText.text = "Retry";
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -556,7 +556,7 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
private async Task DownloadFileAsync(string url, string destinationPath, IProgress<float> progress, System.Threading.CancellationToken cancellationToken)
|
||||
{
|
||||
using var client = new HttpClient();
|
||||
// User-Agent 헤더 추가하여 차단 방지
|
||||
// Add User-Agent header to prevent blocking
|
||||
client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ADOFAIRenderer/1.0");
|
||||
|
||||
using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||||
@@ -611,7 +611,7 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
}
|
||||
else
|
||||
{
|
||||
// 리눅스 (또는 기타 플랫폼) - tar.xz 해제
|
||||
// Linux (or other platforms) - extract tar.xz
|
||||
await Task.Run(() =>
|
||||
{
|
||||
var tempExtractDir = Path.Combine(outputDirectory, "ffmpeg_temp_extracted");
|
||||
@@ -640,11 +640,11 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
var error = process.StandardError.ReadToEnd();
|
||||
throw new Exception($"tar 압축 해제 실패 (ExitCode: {process.ExitCode}): {error}");
|
||||
throw new Exception($"tar extraction failed (ExitCode: {process.ExitCode}): {error}");
|
||||
}
|
||||
}
|
||||
|
||||
// 압축이 풀린 파일 중에서 .so 파일들을 찾아서 outputDirectory로 이동
|
||||
// Find .so files and copy to outputDirectory
|
||||
var soFiles = Directory.GetFiles(tempExtractDir, "*.so*", SearchOption.AllDirectories);
|
||||
foreach (var soFile in soFiles)
|
||||
{
|
||||
@@ -653,7 +653,7 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
File.Copy(soFile, targetPath, true);
|
||||
}
|
||||
|
||||
// 임시 디렉토리 정리
|
||||
// Clean up temporary directory
|
||||
try
|
||||
{
|
||||
Directory.Delete(tempExtractDir, true);
|
||||
@@ -675,7 +675,7 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Melon<RenderMod>.Logger.Warning($"임시 ZIP 파일 삭제 실패: {ex.Message}");
|
||||
Melon<RenderMod>.Logger.Warning($"Failed to delete temporary zip file: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+79
-82
@@ -62,34 +62,34 @@ internal class RenderConfigUIManager : MonoBehaviour
|
||||
_videoEncodingConfigButton = _root.Q<Button>("VideoEncodingSettingsButton");
|
||||
if (_videoEncodingConfigButton != null)
|
||||
{
|
||||
Melon<RenderMod>.Logger.Msg($"[UI] VideoEncodingSettingsButton 발견: pickingMode={_videoEncodingConfigButton.pickingMode}, enabledSelf={_videoEncodingConfigButton.enabledSelf}");
|
||||
Melon<RenderMod>.Logger.Msg($"[UI] VideoEncodingSettingsButton found: pickingMode={_videoEncodingConfigButton.pickingMode}, enabledSelf={_videoEncodingConfigButton.enabledSelf}");
|
||||
_videoEncodingConfigButton.RegisterCallback<ClickEvent>(_ => OpenVideoEncodingConfig());
|
||||
}
|
||||
else
|
||||
{
|
||||
Melon<RenderMod>.Logger.Error("Button 'VideoEncodingSettingsButton'을(를) 찾을 수 없습니다.");
|
||||
Melon<RenderMod>.Logger.Error("Button 'VideoEncodingSettingsButton' not found.");
|
||||
}
|
||||
|
||||
_audioEncodingConfigButton = _root.Q<Button>("AudioEncodingSettingsButton");
|
||||
if (_audioEncodingConfigButton != null)
|
||||
{
|
||||
Melon<RenderMod>.Logger.Msg($"[UI] AudioEncodingSettingsButton 발견: pickingMode={_audioEncodingConfigButton.pickingMode}, enabledSelf={_audioEncodingConfigButton.enabledSelf}");
|
||||
Melon<RenderMod>.Logger.Msg($"[UI] AudioEncodingSettingsButton found: pickingMode={_audioEncodingConfigButton.pickingMode}, enabledSelf={_audioEncodingConfigButton.enabledSelf}");
|
||||
_audioEncodingConfigButton.RegisterCallback<ClickEvent>(_ => OpenAudioEncodingConfig());
|
||||
}
|
||||
else
|
||||
{
|
||||
Melon<RenderMod>.Logger.Error("Button 'AudioEncodingSettingsButton'을(를) 찾을 수 없습니다.");
|
||||
Melon<RenderMod>.Logger.Error("Button 'AudioEncodingSettingsButton' not found.");
|
||||
}
|
||||
|
||||
var presetSettingsButton = _root.Q<Button>("PresetSettingsButton");
|
||||
if (presetSettingsButton != null)
|
||||
{
|
||||
Melon<RenderMod>.Logger.Msg($"[UI] PresetSettingsButton 발견: pickingMode={presetSettingsButton.pickingMode}, enabledSelf={presetSettingsButton.enabledSelf}");
|
||||
Melon<RenderMod>.Logger.Msg($"[UI] PresetSettingsButton found: pickingMode={presetSettingsButton.pickingMode}, enabledSelf={presetSettingsButton.enabledSelf}");
|
||||
presetSettingsButton.RegisterCallback<ClickEvent>(_ => OpenPresetConfig());
|
||||
}
|
||||
else
|
||||
{
|
||||
Melon<RenderMod>.Logger.Error("Button 'PresetSettingsButton'을(를) 찾을 수 없습니다.");
|
||||
Melon<RenderMod>.Logger.Error("Button 'PresetSettingsButton' not found.");
|
||||
}
|
||||
|
||||
var bottomRow = CreateRow();
|
||||
@@ -99,11 +99,11 @@ internal class RenderConfigUIManager : MonoBehaviour
|
||||
bottomRow.style.bottom = 0f;
|
||||
bottomRow.style.justifyContent = Justify.FlexEnd;
|
||||
|
||||
_advancedSettingsButton = new Button { name = "AdvancedSettingsButton", text = "고급 설정..." };
|
||||
_advancedSettingsButton = new Button { name = "AdvancedSettingsButton", text = "Advanced Settings..." };
|
||||
bottomRow.Add(_advancedSettingsButton);
|
||||
|
||||
(_root.childCount > 0 ? _root[0] : _root).Add(bottomRow);
|
||||
Melon<RenderMod>.Logger.Msg("[UI] AdvancedSettingsButton 동적 생성 완료.");
|
||||
Melon<RenderMod>.Logger.Msg("[UI] AdvancedSettingsButton dynamically created.");
|
||||
|
||||
_advancedSettingsButton.RegisterCallback<ClickEvent>(_ => OpenAdvancedConfig());
|
||||
|
||||
@@ -126,7 +126,7 @@ internal class RenderConfigUIManager : MonoBehaviour
|
||||
}
|
||||
};
|
||||
|
||||
// 기존 UI와 동일한 폰트 적용
|
||||
// Apply same font as existing UI
|
||||
if (RenderMod.FontBundle)
|
||||
{
|
||||
try
|
||||
@@ -140,7 +140,7 @@ internal class RenderConfigUIManager : MonoBehaviour
|
||||
_versionLabel.RegisterCallback<PointerUpLinkTagEvent>(OnLinkTagClicked);
|
||||
|
||||
(_root.childCount > 0 ? _root[0] : _root).Add(_versionLabel);
|
||||
Melon<RenderMod>.Logger.Msg("[UI] VersionLabel 동적 생성 완료.");
|
||||
Melon<RenderMod>.Logger.Msg("[UI] VersionLabel dynamically created.");
|
||||
}
|
||||
|
||||
private void OpenAdvancedConfig()
|
||||
@@ -176,9 +176,9 @@ internal class RenderConfigUIManager : MonoBehaviour
|
||||
}
|
||||
};
|
||||
|
||||
// 1. 타이틀 로우
|
||||
// 1. Title Row
|
||||
var titleRow = CreateRow();
|
||||
var titleLabel = new Label("고급 설정")
|
||||
var titleLabel = new Label("Advanced Settings")
|
||||
{
|
||||
style =
|
||||
{
|
||||
@@ -189,9 +189,9 @@ internal class RenderConfigUIManager : MonoBehaviour
|
||||
titleRow.Add(titleLabel);
|
||||
_advancedConfigContainer.Add(titleRow);
|
||||
|
||||
// 2. 확장자 로우
|
||||
// 2. Extension Row
|
||||
var fileExtensionRow = CreateRow();
|
||||
var fileExtensionLabel = CreateFormLabel("확장자");
|
||||
var fileExtensionLabel = CreateFormLabel("File Extension");
|
||||
var fileExtensionChoices = new List<string> { "mkv", "mp4", "mov", "webm", "avi" };
|
||||
var currentFileExt = string.IsNullOrWhiteSpace(tempConfig.FileExtension) ? "mkv" : tempConfig.FileExtension.TrimStart('.').ToLower();
|
||||
if (!fileExtensionChoices.Contains(currentFileExt))
|
||||
@@ -203,25 +203,25 @@ internal class RenderConfigUIManager : MonoBehaviour
|
||||
fileExtensionRow.Add(fileExtensionDropdown);
|
||||
_advancedConfigContainer.Add(fileExtensionRow);
|
||||
|
||||
// 3. 오디오 처리 로우
|
||||
// 3. Audio Processing Row
|
||||
var processAudioRow = CreateRow();
|
||||
var processAudioLabel = CreateFormLabel("오디오 처리");
|
||||
var processAudioLabel = CreateFormLabel("Process Audio");
|
||||
var processAudioToggle = new Toggle { name = "AdvancedProcessAudioToggle", label = "", value = tempConfig.ProcessAudio };
|
||||
processAudioRow.Add(processAudioLabel);
|
||||
processAudioRow.Add(processAudioToggle);
|
||||
_advancedConfigContainer.Add(processAudioRow);
|
||||
|
||||
// 4. 논타일 모드 로우
|
||||
// 4. Non-Tile Mode Row
|
||||
var nonTileRow = CreateRow();
|
||||
var nonTileLabel = CreateFormLabel("논타일 모드");
|
||||
var nonTileLabel = CreateFormLabel("Non-Tile Mode");
|
||||
var nonTileToggle = new Toggle { name = "AdvancedNonTileToggle", label = "", value = tempConfig.NonTile };
|
||||
nonTileRow.Add(nonTileLabel);
|
||||
nonTileRow.Add(nonTileToggle);
|
||||
_advancedConfigContainer.Add(nonTileRow);
|
||||
|
||||
// 5. 랜덤 시드 로우
|
||||
// 5. Random Seed Row
|
||||
var randomSeedRow = CreateRow();
|
||||
var randomSeedLabel = CreateFormLabel("랜덤 시드 (입력 선택)");
|
||||
var randomSeedLabel = CreateFormLabel("Random Seed (Optional)");
|
||||
var randomSeedField = new TextField { name = "AdvancedRandomSeed", label = "", value = tempConfig.RandomSeed?.ToString() ?? string.Empty };
|
||||
|
||||
randomSeedField.RegisterValueChangedCallback(evt =>
|
||||
@@ -237,10 +237,10 @@ internal class RenderConfigUIManager : MonoBehaviour
|
||||
randomSeedRow.Add(randomSeedField);
|
||||
_advancedConfigContainer.Add(randomSeedRow);
|
||||
|
||||
// 6. 제어 버튼 로우 (취소 / 저장)
|
||||
// 6. Control Button Row (Cancel / Save)
|
||||
var buttonRow = CreateRow();
|
||||
var cancelBtn = new Button { text = "취소" };
|
||||
var saveBtn = new Button { text = "저장" };
|
||||
var cancelBtn = new Button { text = "Cancel" };
|
||||
var saveBtn = new Button { text = "Save" };
|
||||
|
||||
buttonRow.Add(cancelBtn);
|
||||
buttonRow.Add(saveBtn);
|
||||
@@ -315,7 +315,7 @@ internal class RenderConfigUIManager : MonoBehaviour
|
||||
_root.Remove(_videoEncodingConfigContainer);
|
||||
}
|
||||
|
||||
// 1. 전체 화면 컨테이너 생성 및 배경 적용
|
||||
// 1. Create full screen container and apply background
|
||||
_videoEncodingConfigContainer = new VisualElement
|
||||
{
|
||||
name = "VideoEncodingSettingsContainer",
|
||||
@@ -332,9 +332,9 @@ internal class RenderConfigUIManager : MonoBehaviour
|
||||
}
|
||||
};
|
||||
|
||||
// 2. 타이틀 로우
|
||||
// 2. Title Row
|
||||
var titleRow = CreateRow();
|
||||
var titleLabel = new Label("비디오 인코딩 설정")
|
||||
var titleLabel = new Label("Video Encoding Settings")
|
||||
{
|
||||
style =
|
||||
{
|
||||
@@ -345,71 +345,71 @@ internal class RenderConfigUIManager : MonoBehaviour
|
||||
titleRow.Add(titleLabel);
|
||||
_videoEncodingConfigContainer.Add(titleRow);
|
||||
|
||||
// 3. 코덱 로우
|
||||
// 3. Codec Row
|
||||
var codecRow = CreateRow();
|
||||
var codecLabel = CreateFormLabel("코덱");
|
||||
var codecLabel = CreateFormLabel("Codec");
|
||||
var codecChoices = new List<string>(Enum.GetNames(typeof(Codec)));
|
||||
var codecDropdown = new DropdownField(codecChoices, tempConfig.Codec.ToString()) { name = "VideoCodec" };
|
||||
codecRow.Add(codecLabel);
|
||||
codecRow.Add(codecDropdown);
|
||||
_videoEncodingConfigContainer.Add(codecRow);
|
||||
|
||||
// 4. 모드 로우
|
||||
// 4. Mode Row
|
||||
var modeRow = CreateRow();
|
||||
var modeLabel = CreateFormLabel("모드");
|
||||
var modeLabel = CreateFormLabel("Mode");
|
||||
var modeChoices = new List<string>(Enum.GetNames(typeof(RateControlMode)));
|
||||
var modeDropdown = new DropdownField(modeChoices, tempConfig.RateControlMode.ToString()) { name = "VideoRateControlMode" };
|
||||
modeRow.Add(modeLabel);
|
||||
modeRow.Add(modeDropdown);
|
||||
_videoEncodingConfigContainer.Add(modeRow);
|
||||
|
||||
// 5. 비트레이트 로우
|
||||
// 5. Bitrate Row
|
||||
var bitrateRow = CreateRow();
|
||||
var bitrateLabel = CreateFormLabel("비트레이트(kbps)");
|
||||
var bitrateLabel = CreateFormLabel("Bitrate (kbps)");
|
||||
var bitrateField = new IntegerField { name = "VideoBitrate", label = "", value = tempConfig.Bitrate };
|
||||
bitrateRow.Add(bitrateLabel);
|
||||
bitrateRow.Add(bitrateField);
|
||||
_videoEncodingConfigContainer.Add(bitrateRow);
|
||||
|
||||
// 5.5. 최대 비트레이트 로우 (VBR 전용)
|
||||
// 5.5. Max Bitrate Row (VBR only)
|
||||
var maxBitrateRow = CreateRow();
|
||||
var maxBitrateLabel = CreateFormLabel("최대 비트레이트(kbps)");
|
||||
var maxBitrateLabel = CreateFormLabel("Max Bitrate (kbps)");
|
||||
var maxBitrateField = new IntegerField { name = "VideoMaxBitrate", label = "", value = tempConfig.MaxBitrate };
|
||||
maxBitrateRow.Add(maxBitrateLabel);
|
||||
maxBitrateRow.Add(maxBitrateField);
|
||||
_videoEncodingConfigContainer.Add(maxBitrateRow);
|
||||
|
||||
// 6. QP 로우
|
||||
// 6. QP Row
|
||||
var qpRow = CreateRow();
|
||||
var qpLabel = CreateFormLabel("품질 (QP)");
|
||||
var qpLabel = CreateFormLabel("Quality (QP)");
|
||||
var qpField = new IntegerField { name = "VideoQP", label = "", value = tempConfig.QP };
|
||||
qpRow.Add(qpLabel);
|
||||
qpRow.Add(qpField);
|
||||
_videoEncodingConfigContainer.Add(qpRow);
|
||||
|
||||
// 6.5. 키프레임 간격 로우 (초 단위)
|
||||
// 6.5. Keyframe Interval Row (seconds)
|
||||
var keyframeRow = CreateRow();
|
||||
var keyframeLabel = CreateFormLabel("키프레임 간격(초)");
|
||||
var keyframeLabel = CreateFormLabel("Keyframe Interval (s)");
|
||||
var keyframeField = new DoubleField { name = "VideoKeyframeInterval", label = "", value = tempConfig.KeyframeInterval };
|
||||
keyframeRow.Add(keyframeLabel);
|
||||
keyframeRow.Add(keyframeField);
|
||||
_videoEncodingConfigContainer.Add(keyframeRow);
|
||||
|
||||
// 6.75. 강제 소프트웨어 인코더 로우 (라벨 + 토글)
|
||||
// 6.75. Force Software Encoder Row (Label + Toggle)
|
||||
var forceSoftwareRow = CreateRow();
|
||||
var forceSoftwareLabel = CreateFormLabel("소프트웨어 인코더");
|
||||
var forceSoftwareLabel = CreateFormLabel("Software Encoder");
|
||||
var forceSoftwareToggle = new Toggle { name = "VideoForceSoftwareEncoderToggle", label = "", value = tempConfig.ForceSoftwareEncoder };
|
||||
forceSoftwareRow.Add(forceSoftwareLabel);
|
||||
forceSoftwareRow.Add(forceSoftwareToggle);
|
||||
_videoEncodingConfigContainer.Add(forceSoftwareRow);
|
||||
|
||||
// 7. QP = 0 (무손실) 경고 라벨 로우 생성
|
||||
// 7. QP = 0 (Lossless) Warning Label Row
|
||||
var warningRow = CreateRow();
|
||||
warningRow.style.height = StyleKeyword.Auto;
|
||||
warningRow.style.maxHeight = StyleKeyword.Null;
|
||||
warningRow.style.minHeight = StyleKeyword.Null;
|
||||
|
||||
var warningLabel = new Label("주의: QP를 0으로 설정하면 무손실 인코딩이 적용되어 용량이 매우 커질 수 있습니다.")
|
||||
var warningLabel = new Label("Warning: Setting QP to 0 applies lossless encoding and may result in very large file sizes.")
|
||||
{
|
||||
style =
|
||||
{
|
||||
@@ -441,10 +441,10 @@ internal class RenderConfigUIManager : MonoBehaviour
|
||||
: DisplayStyle.None;
|
||||
});
|
||||
|
||||
// 8. 제어 버튼 로우 (취소 / 저장)
|
||||
// 8. Control Button Row (Cancel / Save)
|
||||
var buttonRow = CreateRow();
|
||||
var cancelBtn = new Button { text = "취소" };
|
||||
var saveBtn = new Button { text = "저장" };
|
||||
var cancelBtn = new Button { text = "Cancel" };
|
||||
var saveBtn = new Button { text = "Save" };
|
||||
|
||||
buttonRow.Add(cancelBtn);
|
||||
buttonRow.Add(saveBtn);
|
||||
@@ -489,11 +489,9 @@ internal class RenderConfigUIManager : MonoBehaviour
|
||||
|
||||
_root.Add(_videoEncodingConfigContainer);
|
||||
|
||||
// 화면 전환 활성화
|
||||
ToggleScreens(true);
|
||||
return;
|
||||
|
||||
// 화면 전환 제어
|
||||
void ToggleScreens(bool showEncoding)
|
||||
{
|
||||
if (_root.childCount > 0)
|
||||
@@ -503,7 +501,6 @@ internal class RenderConfigUIManager : MonoBehaviour
|
||||
_videoEncodingConfigContainer.style.display = showEncoding ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
}
|
||||
|
||||
// 가시성 필터링 업데이트 (행 단위로 보임/숨김 제어)
|
||||
void UpdateVisibility(string selectedModeStr)
|
||||
{
|
||||
if (!Enum.TryParse<RateControlMode>(selectedModeStr, out var mode)) return;
|
||||
@@ -547,9 +544,9 @@ internal class RenderConfigUIManager : MonoBehaviour
|
||||
}
|
||||
};
|
||||
|
||||
// 1. 타이틀 로우
|
||||
// 1. Title Row
|
||||
var titleRow = CreateRow();
|
||||
var titleLabel = new Label("오디오 인코딩 설정")
|
||||
var titleLabel = new Label("Audio Encoding Settings")
|
||||
{
|
||||
style =
|
||||
{
|
||||
@@ -560,17 +557,17 @@ internal class RenderConfigUIManager : MonoBehaviour
|
||||
titleRow.Add(titleLabel);
|
||||
_audioEncodingConfigContainer.Add(titleRow);
|
||||
|
||||
// 2. 오디오 렌더링 활성화 로우 (라벨 + 토글)
|
||||
// 2. Enable Audio Rendering Row (Label + Toggle)
|
||||
var renderAudioRow = CreateRow();
|
||||
var renderAudioLabel = CreateFormLabel("오디오 렌더링 활성화");
|
||||
var renderAudioLabel = CreateFormLabel("Enable Audio Rendering");
|
||||
var renderAudioToggle = new Toggle { name = "AudioRenderAudioToggle", label = "", value = tempAudioConfig.RenderAudio };
|
||||
renderAudioRow.Add(renderAudioLabel);
|
||||
renderAudioRow.Add(renderAudioToggle);
|
||||
_audioEncodingConfigContainer.Add(renderAudioRow);
|
||||
|
||||
// 3. 샘플 레이트 로우
|
||||
// 3. Sample Rate Row
|
||||
var sampleRateRow = CreateRow();
|
||||
var sampleRateLabel = CreateFormLabel("샘플 레이트(Hz)");
|
||||
var sampleRateLabel = CreateFormLabel("Sample Rate (Hz)");
|
||||
var isOpus = config.VideoConfig.Codec is Codec.AV1 or Codec.VP9 or Codec.VVC;
|
||||
|
||||
List<string> sampleRateChoices;
|
||||
@@ -599,9 +596,9 @@ internal class RenderConfigUIManager : MonoBehaviour
|
||||
sampleRateRow.Add(sampleRateDropdown);
|
||||
_audioEncodingConfigContainer.Add(sampleRateRow);
|
||||
|
||||
// 4. 채널 로우
|
||||
// 4. Channels Row
|
||||
var channelsRow = CreateRow();
|
||||
var channelsLabel = CreateFormLabel("오디오 채널");
|
||||
var channelsLabel = CreateFormLabel("Audio Channels");
|
||||
var channelsDropdown = new DropdownField(
|
||||
new List<string> { "1 (Mono)", "2 (Stereo)" },
|
||||
tempAudioConfig.AudioChannels == 1 ? "1 (Mono)" : "2 (Stereo)"
|
||||
@@ -610,9 +607,9 @@ internal class RenderConfigUIManager : MonoBehaviour
|
||||
channelsRow.Add(channelsDropdown);
|
||||
_audioEncodingConfigContainer.Add(channelsRow);
|
||||
|
||||
// 5. 비트레이트 로우 (kbps 단위 입력)
|
||||
// 5. Bitrate Row (input in kbps)
|
||||
var bitrateRow = CreateRow();
|
||||
var bitrateLabel = CreateFormLabel("오디오 비트레이트(kbps)");
|
||||
var bitrateLabel = CreateFormLabel("Audio Bitrate (kbps)");
|
||||
var bitrateField = new IntegerField { name = "AudioBitrateKbps", label = "", value = tempAudioConfig.AudioBitrate / 1000 };
|
||||
bitrateRow.Add(bitrateLabel);
|
||||
bitrateRow.Add(bitrateField);
|
||||
@@ -622,14 +619,14 @@ internal class RenderConfigUIManager : MonoBehaviour
|
||||
audioValidator.AddLimits("AudioBitrateKbps", new PositiveOnlyIntegerLimit(), new RangeIntegerLimit(32, 1024));
|
||||
audioValidator.SetupValidation();
|
||||
|
||||
// 가시성 업데이트 (오디오 렌더링 비활성화 시 하위 설정 숨김)
|
||||
// Update visibility (hide sub-settings when audio rendering is disabled)
|
||||
renderAudioToggle.RegisterValueChangedCallback(evt => UpdateAudioVisibility(evt.newValue));
|
||||
UpdateAudioVisibility(tempAudioConfig.RenderAudio);
|
||||
|
||||
// 6. 제어 버튼 로우 (취소 / 저장)
|
||||
// 6. Control Button Row (Cancel / Save)
|
||||
var buttonRow = CreateRow();
|
||||
var cancelBtn = new Button { text = "취소" };
|
||||
var saveBtn = new Button { text = "저장" };
|
||||
var cancelBtn = new Button { text = "Cancel" };
|
||||
var saveBtn = new Button { text = "Save" };
|
||||
|
||||
buttonRow.Add(cancelBtn);
|
||||
buttonRow.Add(saveBtn);
|
||||
@@ -686,7 +683,7 @@ internal class RenderConfigUIManager : MonoBehaviour
|
||||
{
|
||||
if (!ValidateInputs())
|
||||
{
|
||||
Melon<RenderMod>.Logger.Error("필수 입력 값이 비어 있거나 올바르지 않습니다. 모든 설정을 입력해 주세요.");
|
||||
Melon<RenderMod>.Logger.Error("Required input fields are empty or invalid. Please fill in all settings.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -788,7 +785,7 @@ internal class RenderConfigUIManager : MonoBehaviour
|
||||
titleRow.style.height = StyleKeyword.Auto;
|
||||
titleRow.style.minHeight = StyleKeyword.Null;
|
||||
titleRow.style.maxHeight = StyleKeyword.Null;
|
||||
var title = new Label("프리셋 설정")
|
||||
var title = new Label("Preset Settings")
|
||||
{
|
||||
style =
|
||||
{
|
||||
@@ -877,7 +874,7 @@ internal class RenderConfigUIManager : MonoBehaviour
|
||||
var saveRow = CreateRow();
|
||||
saveRow.style.marginBottom = 20;
|
||||
|
||||
var nameLabelLabel = CreateFormLabel("새 프리셋 이름");
|
||||
var nameLabelLabel = CreateFormLabel("New Preset Name");
|
||||
nameLabelLabel.style.marginRight = 10;
|
||||
|
||||
var nameField = new TextField
|
||||
@@ -890,7 +887,7 @@ internal class RenderConfigUIManager : MonoBehaviour
|
||||
}
|
||||
};
|
||||
|
||||
// 기존 폼 입력 필드 스타일과 맞춤
|
||||
// Match existing form input field style
|
||||
var textInput = nameField.Q("unity-text-input");
|
||||
if (textInput != null)
|
||||
{
|
||||
@@ -919,7 +916,7 @@ internal class RenderConfigUIManager : MonoBehaviour
|
||||
nameField.style.borderBottomRightRadius = 6f;
|
||||
}
|
||||
|
||||
var saveBtn = new Button { text = "저장" };
|
||||
var saveBtn = new Button { text = "Save" };
|
||||
|
||||
saveBtn.RegisterCallback<ClickEvent>(_ =>
|
||||
{
|
||||
@@ -927,9 +924,9 @@ internal class RenderConfigUIManager : MonoBehaviour
|
||||
if (string.IsNullOrWhiteSpace(presetName))
|
||||
{
|
||||
CommonWarningDialogUI.ShowDialog(
|
||||
"저장 실패",
|
||||
"프리셋 이름을 입력해 주세요.",
|
||||
new DialogButton("확인", () => {})
|
||||
"Save Failed",
|
||||
"Please enter a preset name.",
|
||||
new DialogButton("OK", () => {})
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -937,9 +934,9 @@ internal class RenderConfigUIManager : MonoBehaviour
|
||||
if (!ValidateInputs())
|
||||
{
|
||||
CommonWarningDialogUI.ShowDialog(
|
||||
"저장 실패",
|
||||
"필수 입력 값이 비어 있거나 올바르지 않아 프리셋을 저장할 수 없습니다.",
|
||||
new DialogButton("확인", () => {})
|
||||
"Save Failed",
|
||||
"Cannot save preset because required input values are empty or invalid.",
|
||||
new DialogButton("OK", () => {})
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -973,9 +970,9 @@ internal class RenderConfigUIManager : MonoBehaviour
|
||||
catch (Exception ex)
|
||||
{
|
||||
CommonWarningDialogUI.ShowDialog(
|
||||
"저장 실패",
|
||||
$"프리셋 저장 중 오류가 발생했습니다: {ex.Message}",
|
||||
new DialogButton("확인", () => {})
|
||||
"Save Failed",
|
||||
$"An error occurred while saving the preset: {ex.Message}",
|
||||
new DialogButton("OK", () => {})
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -988,7 +985,7 @@ internal class RenderConfigUIManager : MonoBehaviour
|
||||
// Bottom controls: Close Button
|
||||
var closeRow = CreateRow();
|
||||
|
||||
var closeBtn = new Button { text = "닫기" };
|
||||
var closeBtn = new Button { text = "Close" };
|
||||
|
||||
closeBtn.RegisterCallback<ClickEvent>(_ =>
|
||||
{
|
||||
@@ -1015,7 +1012,7 @@ internal class RenderConfigUIManager : MonoBehaviour
|
||||
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 } };
|
||||
var emptyLabel = new Label("No saved presets.") { style = { color = new StyleColor(Color.gray), unityTextAlign = TextAnchor.MiddleCenter, marginTop = 80, fontSize = 20 } };
|
||||
emptyRow.Add(emptyLabel);
|
||||
scrollView.Add(emptyRow);
|
||||
}
|
||||
@@ -1045,7 +1042,7 @@ internal class RenderConfigUIManager : MonoBehaviour
|
||||
|
||||
var loadBtn = new Button
|
||||
{
|
||||
text = "불러오기",
|
||||
text = "Load",
|
||||
style =
|
||||
{
|
||||
marginRight = 10,
|
||||
@@ -1085,7 +1082,7 @@ internal class RenderConfigUIManager : MonoBehaviour
|
||||
|
||||
var deleteBtn = new Button
|
||||
{
|
||||
text = "삭제",
|
||||
text = "Delete",
|
||||
style =
|
||||
{
|
||||
width = 120f,
|
||||
@@ -1116,11 +1113,11 @@ internal class RenderConfigUIManager : MonoBehaviour
|
||||
|
||||
deleteBtn.RegisterCallback<ClickEvent>(_ =>
|
||||
{
|
||||
CommonWarningDialogUI.ShowDialog("프리셋 삭제", $"'{preset.Name}' 프리셋을 정말 삭제하시겠습니까?", new DialogButton("예", () =>
|
||||
CommonWarningDialogUI.ShowDialog("Delete Preset", $"Are you sure you want to delete the preset '{preset.Name}'?", new DialogButton("Yes", () =>
|
||||
{
|
||||
PresetManager.DeletePreset(preset.Name);
|
||||
RefreshPresets();
|
||||
}), new DialogButton("아니오", () => { }));
|
||||
}), new DialogButton("No", () => { }));
|
||||
});
|
||||
|
||||
btnContainer.Add(loadBtn);
|
||||
|
||||
@@ -33,11 +33,11 @@ namespace ADOFAIRenderer.Util
|
||||
return AssetBundle.LoadFromStream(resourceStream);
|
||||
}
|
||||
|
||||
Melon<RenderMod>.Logger.Error($"DLL에서 번들 리소스를 찾을 수 없습니다.\n{bundlePath}");
|
||||
Melon<RenderMod>.Logger.Error($"Could not find bundle resource in DLL.\n{bundlePath}");
|
||||
throw new MissingManifestResourceException("Could not find bundle resource in DLL.");
|
||||
}
|
||||
|
||||
Melon<RenderMod>.Logger.Error("실행 어셈블리가 제대로 설정되지 않았습니다.");
|
||||
Melon<RenderMod>.Logger.Error("Executing assembly has not been set properly.");
|
||||
throw new InvalidOperationException("ExecutingAssembly has not been set.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ using Unity.Collections.LowLevel.Unsafe;
|
||||
namespace ADOFAIRenderer.Util.FFmpeg
|
||||
{
|
||||
/// <summary>
|
||||
/// FFmpeg의 C 네이티브 API를 사용하여 비디오 파일을 스트리밍 방식으로 디코딩하는 클래스입니다.
|
||||
/// 플레이어 형태가 아니며, 필요할 때 프레임을 순차적으로 읽거나 앞으로 탐색(Seek/Skip)할 수 있습니다.
|
||||
/// Decodes video files in a streaming fashion using FFmpeg's native C API.
|
||||
/// Unlike a standalone player, it allows reading frames sequentially or seeking/skipping forward on demand.
|
||||
/// </summary>
|
||||
internal sealed unsafe class FFmpegNativeDecoder : IDisposable
|
||||
{
|
||||
@@ -26,41 +26,41 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
private bool _isDraining;
|
||||
private readonly object _lock = new();
|
||||
|
||||
// 탐색(Seek) 후 매칭되는 프레임을 보관하기 위한 캐시 프레임
|
||||
// Cached frame used to hold matching frames after seeking
|
||||
private AVFrame* _pCachedFrame = null;
|
||||
private bool _hasCachedFrame;
|
||||
|
||||
// SwsContext 갱신 감지용 필드
|
||||
// Field to detect SwsContext updates
|
||||
private AVPixelFormat _srcPixFmt = AVPixelFormat.AV_PIX_FMT_NONE;
|
||||
private AVPixelFormat _dstPixFmt = AVPixelFormat.AV_PIX_FMT_NONE;
|
||||
private int _srcWidth;
|
||||
private int _srcHeight;
|
||||
|
||||
/// <summary>비디오 가로 해상도 (픽셀).</summary>
|
||||
/// <summary>Video width in pixels.</summary>
|
||||
public int Width { get; private set; }
|
||||
|
||||
/// <summary>비디오 세로 해상도 (픽셀).</summary>
|
||||
/// <summary>Video height in pixels.</summary>
|
||||
public int Height { get; private set; }
|
||||
|
||||
/// <summary>초당 프레임 수.</summary>
|
||||
/// <summary>Frames per second.</summary>
|
||||
public double FPS { get; private set; }
|
||||
|
||||
/// <summary>전체 재생 시간 (초).</summary>
|
||||
/// <summary>Total duration in seconds.</summary>
|
||||
public double Duration { get; private set; }
|
||||
|
||||
/// <summary>전체 프레임 수 (추정치).</summary>
|
||||
/// <summary>Estimated total number of frames.</summary>
|
||||
public long TotalFrames { get; private set; }
|
||||
|
||||
/// <summary>현재 읽은 프레임의 인덱스.</summary>
|
||||
/// <summary>Index of the currently read frame.</summary>
|
||||
public long CurrentFrameIndex => _currentFrameIndex;
|
||||
|
||||
/// <summary>현재 재생 시간 (초).</summary>
|
||||
/// <summary>Current playback time in seconds.</summary>
|
||||
public double CurrentTime => _currentFrameIndex / FPS;
|
||||
|
||||
/// <summary>비디오 원본 코덱의 픽셀 포맷.</summary>
|
||||
/// <summary>Pixel format of the original video stream.</summary>
|
||||
public AVPixelFormat PixelFormat => _pCodecContext != null ? _pCodecContext->pix_fmt : AVPixelFormat.AV_PIX_FMT_NONE;
|
||||
|
||||
/// <summary>비디오 원본 코덱의 색상 범위.</summary>
|
||||
/// <summary>Color range of the original video stream.</summary>
|
||||
public AVColorRange ColorRange => _pCodecContext != null ? _pCodecContext->color_range : AVColorRange.AVCOL_RANGE_UNSPECIFIED;
|
||||
|
||||
public FFmpegNativeDecoder(string videoPath)
|
||||
@@ -80,11 +80,11 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
{
|
||||
int ret = ffmpeg.avformat_open_input(ppFormatCtx, videoPath, null, null);
|
||||
if (ret < 0)
|
||||
throw new Exception($"비디오 파일을 열 수 없습니다: {videoPath} ({GetFFmpegErrorString(ret)})");
|
||||
throw new Exception($"Failed to open video file: {videoPath} ({GetFFmpegErrorString(ret)})");
|
||||
}
|
||||
|
||||
if (ffmpeg.avformat_find_stream_info(_pFormatContext, null) < 0)
|
||||
throw new Exception("스트림 정보를 읽을 수 없습니다.");
|
||||
throw new Exception("Failed to retrieve stream info.");
|
||||
|
||||
_videoStreamIndex = -1;
|
||||
for (int i = 0; i < _pFormatContext->nb_streams; i++)
|
||||
@@ -97,12 +97,12 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
}
|
||||
|
||||
if (_videoStreamIndex == -1)
|
||||
throw new Exception("비디오 스트림을 찾을 수 없습니다.");
|
||||
throw new Exception("Failed to find video stream.");
|
||||
|
||||
var stream = _pFormatContext->streams[_videoStreamIndex];
|
||||
_timeBase = stream->time_base;
|
||||
|
||||
// FPS 계산
|
||||
// Calculate FPS
|
||||
if (stream->r_frame_rate.num > 0 && stream->r_frame_rate.den > 0)
|
||||
{
|
||||
FPS = ffmpeg.av_q2d(stream->r_frame_rate);
|
||||
@@ -113,32 +113,32 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
}
|
||||
else
|
||||
{
|
||||
FPS = 30.0; // 기본값 폴백
|
||||
FPS = 30.0; // Fallback default
|
||||
}
|
||||
|
||||
var codecId = stream->codecpar->codec_id;
|
||||
var decoder = ffmpeg.avcodec_find_decoder(codecId);
|
||||
if (decoder == null)
|
||||
throw new Exception($"지원하지 않는 비디오 코덱입니다: {codecId}");
|
||||
throw new Exception($"Unsupported video codec: {codecId}");
|
||||
|
||||
_pCodecContext = ffmpeg.avcodec_alloc_context3(decoder);
|
||||
if (_pCodecContext == null)
|
||||
throw new Exception("코덱 컨텍스트를 할당할 수 없습니다.");
|
||||
throw new Exception("Failed to allocate codec context.");
|
||||
|
||||
if (ffmpeg.avcodec_parameters_to_context(_pCodecContext, stream->codecpar) < 0)
|
||||
throw new Exception("코덱 파라미터를 복사할 수 없습니다.");
|
||||
throw new Exception("Failed to copy codec parameters.");
|
||||
|
||||
// 디코딩 성능 향상을 위한 멀티스레딩 설정 (자동)
|
||||
// Multi-threading settings for improved decoding performance (auto)
|
||||
_pCodecContext->thread_count = 0;
|
||||
_pCodecContext->thread_type = ffmpeg.FF_THREAD_FRAME;
|
||||
|
||||
if (ffmpeg.avcodec_open2(_pCodecContext, decoder, null) < 0)
|
||||
throw new Exception("코덱을 열 수 없습니다.");
|
||||
throw new Exception("Failed to open codec.");
|
||||
|
||||
Width = _pCodecContext->width;
|
||||
Height = _pCodecContext->height;
|
||||
|
||||
// 총 프레임 수 및 재생 시간 계산
|
||||
// Calculate total frames and duration
|
||||
if (stream->nb_frames > 0)
|
||||
{
|
||||
TotalFrames = stream->nb_frames;
|
||||
@@ -183,7 +183,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
{
|
||||
ffmpeg.av_packet_unref(_pPacket);
|
||||
int ret = ffmpeg.av_read_frame(_pFormatContext, _pPacket);
|
||||
if (ret < 0) return false; // EOF 또는 에러
|
||||
if (ret < 0) return false; // EOF or error
|
||||
|
||||
if (_pPacket->stream_index == _videoStreamIndex)
|
||||
{
|
||||
@@ -191,7 +191,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
ffmpeg.av_packet_unref(_pPacket);
|
||||
if (ret < 0)
|
||||
{
|
||||
throw new Exception($"디코더에 패킷을 전송하는 중 오류 발생: {GetFFmpegErrorString(ret)}");
|
||||
throw new Exception($"Error sending packet to decoder: {GetFFmpegErrorString(ret)}");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -224,7 +224,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
|
||||
if (_pSwsContext == null)
|
||||
{
|
||||
throw new Exception("SwsContext를 초기화할 수 없습니다.");
|
||||
throw new Exception("Failed to initialize SwsContext.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,13 +251,13 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 다음 비디오 프레임을 읽고 RGBA 형태로 복사합니다.
|
||||
/// Reads the next video frame and copies it in RGBA format.
|
||||
/// </summary>
|
||||
/// <param name="outBuffer">RGBA 데이터를 저장할 버퍼의 포인터</param>
|
||||
/// <param name="outStride">출력 버퍼의 가로 한 행의 바이트 크기 (일반적으로 Width * 4)</param>
|
||||
/// <param name="vflip">Unity 텍스처 시스템에 맞게 상하 flip을 적용할지 여부</param>
|
||||
/// <param name="timestamp">출력된 프레임의 타임스탬프 (초)</param>
|
||||
/// <returns>프레임을 성공적으로 디코딩하여 복사했으면 true, 재생이 끝났거나 실패했으면 false</returns>
|
||||
/// <param name="outBuffer">Pointer to the output buffer storing RGBA data.</param>
|
||||
/// <param name="outStride">Byte stride per row of the output buffer (typically Width * 4).</param>
|
||||
/// <param name="vflip">Whether to flip vertically to match Unity's texture coordinate system.</param>
|
||||
/// <param name="timestamp">Timestamp of the output frame in seconds.</param>
|
||||
/// <returns>True if the frame was successfully decoded and copied; false on EOF or failure.</returns>
|
||||
private bool TryReadFrame(byte* outBuffer, int outStride, bool vflip, out double timestamp)
|
||||
{
|
||||
timestamp = 0;
|
||||
@@ -339,7 +339,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"디코딩 중 오류 발생: {GetFFmpegErrorString(ret)}");
|
||||
throw new Exception($"Error during decoding: {GetFFmpegErrorString(ret)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -354,16 +354,17 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
{
|
||||
if (outBuffer.Length < Width * Height * 4)
|
||||
{
|
||||
throw new ArgumentException("출력 버퍼의 크기가 비디오 해상도보다 작습니다.", nameof(outBuffer));
|
||||
throw new ArgumentException("Output buffer size is smaller than video resolution.", nameof(outBuffer));
|
||||
}
|
||||
int stride = Width * 4;
|
||||
return TryReadFrame((byte*)outBuffer.GetUnsafePtr(), stride, vflip, out timestamp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 다음 비디오 프레임을 읽어 Y, U, V 채널 데이터를 각각 복사합니다.
|
||||
/// 비디오 원본이 YUV420p 계열(YUV420P, YUVJ420P)이면 CPU 컬러 변환(sws_scale)을 우회하고 메모리를 다이렉트로 복사하여 속도가 매우 빠릅니다.
|
||||
/// 그 외 포맷일 경우 자동으로 YUV420p로 변환 후 복사합니다.
|
||||
/// Reads the next video frame and copies its Y, U, and V channel data separately.
|
||||
/// If the source format is YUV420P family (YUV420P, YUVJ420P), CPU color conversion (sws_scale) is bypassed
|
||||
/// and memory is copied directly for maximum performance.
|
||||
/// Other formats are automatically converted to YUV420P before copying.
|
||||
/// </summary>
|
||||
// ReSharper disable once InconsistentNaming
|
||||
private bool TryReadYUV420PFrame(
|
||||
@@ -411,7 +412,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"디코딩 중 오류 발생: {GetFFmpegErrorString(ret)}");
|
||||
throw new Exception($"Error during decoding: {GetFFmpegErrorString(ret)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -445,7 +446,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
int uvW = w / 2;
|
||||
int uvH = h / 2;
|
||||
|
||||
// Y plane 복사
|
||||
// Copy Y plane
|
||||
for (int y = 0; y < h; y++)
|
||||
{
|
||||
Buffer.MemoryCopy(
|
||||
@@ -455,7 +456,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
);
|
||||
}
|
||||
|
||||
// U plane 복사
|
||||
// Copy U plane
|
||||
for (int y = 0; y < uvH; y++)
|
||||
{
|
||||
Buffer.MemoryCopy(
|
||||
@@ -465,7 +466,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
);
|
||||
}
|
||||
|
||||
// V plane 복사
|
||||
// Copy V plane
|
||||
for (int y = 0; y < uvH; y++)
|
||||
{
|
||||
Buffer.MemoryCopy(
|
||||
@@ -495,7 +496,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
|
||||
if (yBuffer.Length < w * h || uBuffer.Length < uvW * uvH || vBuffer.Length < uvW * uvH)
|
||||
{
|
||||
throw new ArgumentException("버퍼 크기가 비디오 YUV 평면 해상도보다 작습니다.");
|
||||
throw new ArgumentException("Buffer size is smaller than video YUV plane resolution.");
|
||||
}
|
||||
|
||||
return TryReadYUV420PFrame(
|
||||
@@ -507,9 +508,10 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 다음 비디오 프레임을 읽어 NV12 형식의 Y, UV 채널 데이터를 각각 복사합니다.
|
||||
/// 비디오 원본이 NV12 계열이면 CPU 컬러 변환(sws_scale)을 우회하고 메모리를 다이렉트로 복사하여 속도가 매우 빠릅니다.
|
||||
/// 그 외 포맷일 경우 자동으로 NV12로 변환 후 복사합니다.
|
||||
/// Reads the next video frame and copies its NV12 format Y and UV channel data separately.
|
||||
/// If the source format is NV12 family, CPU color conversion (sws_scale) is bypassed
|
||||
/// and memory is copied directly for maximum performance.
|
||||
/// Other formats are automatically converted to NV12 before copying.
|
||||
/// </summary>
|
||||
// ReSharper disable once InconsistentNaming
|
||||
private bool TryReadNV12Frame(
|
||||
@@ -556,7 +558,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"디코딩 중 오류 발생: {GetFFmpegErrorString(ret)}");
|
||||
throw new Exception($"Error during decoding: {GetFFmpegErrorString(ret)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -587,7 +589,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
int h = frame->height;
|
||||
int uvH = h / 2;
|
||||
|
||||
// Y plane 복사
|
||||
// Copy Y plane
|
||||
for (int y = 0; y < h; y++)
|
||||
{
|
||||
Buffer.MemoryCopy(
|
||||
@@ -597,7 +599,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
);
|
||||
}
|
||||
|
||||
// UV plane 복사
|
||||
// Copy UV plane
|
||||
for (int y = 0; y < uvH; y++)
|
||||
{
|
||||
Buffer.MemoryCopy(
|
||||
@@ -625,7 +627,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
|
||||
if (yBuffer.Length < w * h || uvBuffer.Length < w * uvH)
|
||||
{
|
||||
throw new ArgumentException("버퍼 크기가 비디오 NV12 평면 해상도보다 작습니다.");
|
||||
throw new ArgumentException("Buffer size is smaller than video NV12 plane resolution.");
|
||||
}
|
||||
|
||||
return TryReadNV12Frame(
|
||||
@@ -636,10 +638,9 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정한 개수만큼 프레임을 디코딩 후 바로 버립니다. (빠른 전방 탐색용)
|
||||
/// Decodes and discards a specified number of frames immediately (for fast forward skipping).
|
||||
/// </summary>
|
||||
/// <param name="count">버릴 프레임 개수</param>
|
||||
/// <returns>실제로 건너뛴 프레임 개수</returns>
|
||||
/// <param name="count">Number of frames to discard.</param>
|
||||
private void SkipFrames(int count)
|
||||
{
|
||||
lock (_lock)
|
||||
@@ -682,16 +683,16 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"프레임 건너뛰기 중 오류 발생: {GetFFmpegErrorString(ret)}");
|
||||
throw new Exception($"Error skipping frames: {GetFFmpegErrorString(ret)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 재생 시간(초)으로 탐색(Seek)합니다.
|
||||
/// Seeks to a specific playback time in seconds.
|
||||
/// </summary>
|
||||
/// <param name="timeSeconds">탐색할 시간 (초)</param>
|
||||
/// <param name="timeSeconds">Target time in seconds to seek to.</param>
|
||||
public void SeekToTime(double timeSeconds)
|
||||
{
|
||||
lock (_lock)
|
||||
@@ -701,7 +702,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
if (timeSeconds < 0) timeSeconds = 0;
|
||||
double targetFrameIndex = timeSeconds * FPS;
|
||||
|
||||
// 이미 해당 인덱스이고 매우 인접한 미래라면 seek 대신 skip 처리 (I-프레임 찾기 오버헤드 방지)
|
||||
// If already at target index or very close future, skip frames instead of seek to avoid I-frame overhead
|
||||
bool useSkip = targetFrameIndex >= _currentFrameIndex && targetFrameIndex - _currentFrameIndex < 60;
|
||||
if (useSkip)
|
||||
{
|
||||
@@ -719,19 +720,19 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
int ret = ffmpeg.av_seek_frame(_pFormatContext, _videoStreamIndex, targetPts, ffmpeg.AVSEEK_FLAG_BACKWARD);
|
||||
if (ret < 0)
|
||||
{
|
||||
// 실패 시 파일 처음부터 탐색 시도
|
||||
// Fallback to start of file on seek failure
|
||||
ret = ffmpeg.av_seek_frame(_pFormatContext, _videoStreamIndex, 0, ffmpeg.AVSEEK_FLAG_BACKWARD);
|
||||
if (ret < 0)
|
||||
{
|
||||
throw new Exception($"Seek 실패: {GetFFmpegErrorString(ret)}");
|
||||
throw new Exception($"Seek failed: {GetFFmpegErrorString(ret)}");
|
||||
}
|
||||
}
|
||||
|
||||
// 디코더 내부 캐시 초기화
|
||||
// Flush decoder internal cache
|
||||
ffmpeg.avcodec_flush_buffers(_pCodecContext);
|
||||
_isDraining = false;
|
||||
|
||||
// 지정된 시간 이후의 첫 번째 프레임을 찾을 때까지 디코딩을 진행하며 버립니다.
|
||||
// Decode and discard frames until the first frame past the specified time is found
|
||||
while (true)
|
||||
{
|
||||
ret = ffmpeg.avcodec_receive_frame(_pCodecContext, _pDecodedFrame);
|
||||
@@ -739,10 +740,10 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
{
|
||||
double currentPts = GetTimestampAndUpdateIndex(_pDecodedFrame);
|
||||
|
||||
// 플로팅 포인트 오차 감안하여 하프 프레임 제외한 시간부터 체크
|
||||
// Check from half frame earlier to account for floating point errors
|
||||
if (currentPts >= timeSeconds - (0.5 / FPS))
|
||||
{
|
||||
// 매칭되는 첫 프레임을 캐시에 임시 저장하여 다음 TryReadFrame에서 소모하도록 유도
|
||||
// Cache the first matching frame so it will be consumed by the next TryReadFrame call
|
||||
ffmpeg.av_frame_move_ref(_pCachedFrame, _pDecodedFrame);
|
||||
_hasCachedFrame = true;
|
||||
break;
|
||||
@@ -768,16 +769,16 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"Seek 디코딩 중 오류 발생: {GetFFmpegErrorString(ret)}");
|
||||
throw new Exception($"Error during seek decoding: {GetFFmpegErrorString(ret)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 프레임 인덱스로 탐색(Seek)합니다.
|
||||
/// Seeks to a specific frame index.
|
||||
/// </summary>
|
||||
/// <param name="frameIndex">탐색할 프레임 인덱스</param>
|
||||
/// <param name="frameIndex">Target frame index to seek to.</param>
|
||||
public void SeekToFrame(long frameIndex)
|
||||
{
|
||||
SeekToTime(frameIndex / FPS);
|
||||
@@ -789,9 +790,9 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
byte* buffer = stackalloc byte[(int)bufferSize];
|
||||
if (ffmpeg.av_strerror(errnum, buffer, bufferSize) >= 0)
|
||||
{
|
||||
return System.Runtime.InteropServices.Marshal.PtrToStringAnsi((IntPtr)buffer) ?? $"알 수 없는 에러 (Code: {errnum})";
|
||||
return System.Runtime.InteropServices.Marshal.PtrToStringAnsi((IntPtr)buffer) ?? $"Unknown error (Code: {errnum})";
|
||||
}
|
||||
return $"알 수 없는 에러 (Code: {errnum})";
|
||||
return $"Unknown error (Code: {errnum})";
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
||||
+559
-562
File diff suppressed because it is too large.
Load diff
@@ -70,7 +70,7 @@ namespace ADOFAIRenderer.Util
|
||||
|
||||
if (!File.Exists(videoPath))
|
||||
{
|
||||
Melon<RenderMod>.Logger.Warning($"[FFmpegVideoBGPlayer] 비디오 파일이 존재하지 않습니다: {videoPath}");
|
||||
Melon<RenderMod>.Logger.Warning($"[FFmpegVideoBGPlayer] Video file does not exist: {videoPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ namespace ADOFAIRenderer.Util
|
||||
|
||||
_gpuMode = GPUMode.NV12;
|
||||
gpuInitSuccess = true;
|
||||
Melon<RenderMod>.Logger.Msg($"[FFmpegVideoBGPlayer] Compute Shader NV12 모드 초기화 완료");
|
||||
Melon<RenderMod>.Logger.Msg("[FFmpegVideoBGPlayer] Initialized Compute Shader NV12 mode successfully");
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -124,7 +124,7 @@ namespace ADOFAIRenderer.Util
|
||||
|
||||
_gpuMode = GPUMode.YUV420P;
|
||||
gpuInitSuccess = true;
|
||||
Melon<RenderMod>.Logger.Msg($"[FFmpegVideoBGPlayer] Compute Shader YUV420P 모드 초기화 완료");
|
||||
Melon<RenderMod>.Logger.Msg("[FFmpegVideoBGPlayer] Initialized Compute Shader YUV420P mode successfully");
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -133,7 +133,7 @@ namespace ADOFAIRenderer.Util
|
||||
|
||||
break;
|
||||
default:
|
||||
Melon<RenderMod>.Logger.Msg("[FFmpegVideoBGPlayer] 색공간 변환에 Compute Shader를 사용할 수 없습니다.");
|
||||
Melon<RenderMod>.Logger.Msg("[FFmpegVideoBGPlayer] Cannot use Compute Shader for colorspace conversion.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -151,18 +151,18 @@ namespace ADOFAIRenderer.Util
|
||||
_textureRGBA = new Texture2D(w, h, TextureFormat.RGBA32, false);
|
||||
ConfigureTexture(_textureRGBA);
|
||||
_gpuMode = GPUMode.None;
|
||||
Melon<RenderMod>.Logger.Warning($"[FFmpegVideoBGPlayer] GPU 가속 미지원 또는 로드 실패로 CPU 폴백(RGBA)으로 진행합니다.");
|
||||
Melon<RenderMod>.Logger.Warning("[FFmpegVideoBGPlayer] GPU acceleration unsupported or failed to load, falling back to CPU (RGBA).");
|
||||
}
|
||||
|
||||
_currentFrameTimestamp = 0.0;
|
||||
_nextFrameTimestamp = 0.0;
|
||||
_isInitialized = true;
|
||||
|
||||
Melon<RenderMod>.Logger.Msg($"[FFmpegVideoBGPlayer] 초기화 성공 - 해상도: {w}x{h}, FPS: {_decoder.FPS:F2}");
|
||||
Melon<RenderMod>.Logger.Msg($"[FFmpegVideoBGPlayer] Successfully initialized - Resolution: {w}x{h}, FPS: {_decoder.FPS:F2}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Melon<RenderMod>.Logger.Error($"[FFmpegVideoBGPlayer] 초기화 중 에러 발생: {ex}");
|
||||
Melon<RenderMod>.Logger.Error($"[FFmpegVideoBGPlayer] Error during initialization: {ex}");
|
||||
CleanUp();
|
||||
}
|
||||
}
|
||||
@@ -283,7 +283,7 @@ namespace ADOFAIRenderer.Util
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Melon<RenderMod>.Logger.Error($"[FFmpegVideoBGPlayer] Update 재생 중 에러 발생: {ex}");
|
||||
Melon<RenderMod>.Logger.Error($"[FFmpegVideoBGPlayer] Error during playback update: {ex}");
|
||||
_isInitialized = false;
|
||||
}
|
||||
}
|
||||
@@ -395,7 +395,7 @@ namespace ADOFAIRenderer.Util
|
||||
{
|
||||
_decoder.Dispose();
|
||||
_decoder = null;
|
||||
Melon<RenderMod>.Logger.Msg("[FFmpegVideoBGPlayer] 디코더 해제 완료.");
|
||||
Melon<RenderMod>.Logger.Msg("[FFmpegVideoBGPlayer] Decoder successfully disposed.");
|
||||
}
|
||||
|
||||
if (_textureRGBA != null)
|
||||
|
||||
+4
-4
@@ -70,14 +70,14 @@ namespace ADOFAIRenderer.Util
|
||||
state.IsInFlight = true;
|
||||
}
|
||||
|
||||
// GPU 컬러 변환 활성화 여부 검사
|
||||
// Check if GPU color conversion is enabled
|
||||
bool useGPUConversion = RenderMod.ColorConversionShader;
|
||||
AVPixelFormat expectedFmt = AVPixelFormat.AV_PIX_FMT_RGBA;
|
||||
|
||||
if (useGPUConversion)
|
||||
{
|
||||
expectedFmt = _targetFormat;
|
||||
// Compute Shader는 NV12와 YUV420P만 지원하므로 그 외 포맷은 CPU 폴백 처리
|
||||
// Compute Shader only supports NV12 and YUV420P; fallback to CPU for other formats
|
||||
if (expectedFmt != AVPixelFormat.AV_PIX_FMT_NV12 && expectedFmt != AVPixelFormat.AV_PIX_FMT_YUV420P)
|
||||
{
|
||||
useGPUConversion = false;
|
||||
@@ -120,7 +120,7 @@ namespace ADOFAIRenderer.Util
|
||||
}
|
||||
else
|
||||
{
|
||||
// 커널을 못 찾은 경우 기존 RGBA 텍스처 백업 기법으로 폴백
|
||||
// If kernel is not found, fallback to standard RGBA texture readback
|
||||
AsyncGPUReadback.RequestIntoNativeArray(
|
||||
ref state.NativeBuffer,
|
||||
texture,
|
||||
@@ -189,7 +189,7 @@ namespace ADOFAIRenderer.Util
|
||||
if (state.IsGPUConversion)
|
||||
{
|
||||
var data = request.GetData<byte>();
|
||||
// 비동기 요청 버퍼 데이터를 지속성(NativeBuffer) 메모리로 복사
|
||||
// Copy async request buffer data to persistent NativeBuffer memory
|
||||
NativeArray<byte>.Copy(data, state.NativeBuffer, data.Length);
|
||||
}
|
||||
|
||||
|
||||
+11
-11
@@ -48,7 +48,7 @@ namespace ADOFAIRenderer.Util
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MelonLogger.Warning($"[OSNotification] 알림을 보내지 못했습니다: {ex.Message}");
|
||||
MelonLogger.Warning($"[OSNotification] Failed to send notification: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace ADOFAIRenderer.Util
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MelonLogger.Warning($"[OSNotification] Windows 알림 실패: {ex.Message}");
|
||||
MelonLogger.Warning($"[OSNotification] Windows notification failed: {ex.Message}");
|
||||
}
|
||||
})
|
||||
{
|
||||
@@ -110,7 +110,7 @@ namespace ADOFAIRenderer.Util
|
||||
$"<text>{xmlTitle}</text><text>{xmlMessage}</text>" +
|
||||
"</binding></visual>" +
|
||||
"<actions>" +
|
||||
"<action content=\"렌더 폴더 열기\" arguments=\"adofai-renderer://\" activationType=\"protocol\"/>" +
|
||||
"<action content=\"Open Render Folder\" arguments=\"adofai-renderer://\" activationType=\"protocol\"/>" +
|
||||
"</actions></toast>";
|
||||
|
||||
psScript =
|
||||
@@ -165,7 +165,7 @@ namespace ADOFAIRenderer.Util
|
||||
{
|
||||
string stderr = process.StandardError.ReadToEnd().Trim();
|
||||
if (!string.IsNullOrEmpty(stderr))
|
||||
MelonLogger.Warning($"[OSNotification] Toast 실패 (fallback 사용): {stderr}");
|
||||
MelonLogger.Warning($"[OSNotification] Toast failed (using fallback): {stderr}");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ namespace ADOFAIRenderer.Util
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MelonLogger.Warning($"[OSNotification] Toast 실패 (fallback 사용): {ex.Message}");
|
||||
MelonLogger.Warning($"[OSNotification] Toast failed (using fallback): {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -283,7 +283,7 @@ namespace ADOFAIRenderer.Util
|
||||
|
||||
if (hWnd == IntPtr.Zero)
|
||||
{
|
||||
MelonLogger.Warning("[OSNotification] 메시지 윈도우를 생성할 수 없습니다.");
|
||||
MelonLogger.Warning("[OSNotification] Cannot create message window.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -334,7 +334,7 @@ namespace ADOFAIRenderer.Util
|
||||
catch (Exception ex)
|
||||
{
|
||||
MelonLogger.Warning(
|
||||
$"[OSNotification] 폴더를 열 수 없습니다: {ex.Message}");
|
||||
$"[OSNotification] Cannot open folder: {ex.Message}");
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -357,7 +357,7 @@ namespace ADOFAIRenderer.Util
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MelonLogger.Warning($"[OSNotification] Balloon 알림 실패: {ex.Message}");
|
||||
MelonLogger.Warning($"[OSNotification] Balloon notification failed: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -465,7 +465,7 @@ namespace ADOFAIRenderer.Util
|
||||
};
|
||||
startInfo.ArgumentList.Add("-a");
|
||||
startInfo.ArgumentList.Add("ADOFAI Renderer");
|
||||
startInfo.ArgumentList.Add("--action=open_folder=렌더 폴더 열기");
|
||||
startInfo.ArgumentList.Add("--action=open_folder=Open Render Folder");
|
||||
startInfo.ArgumentList.Add(title);
|
||||
startInfo.ArgumentList.Add(message);
|
||||
|
||||
@@ -491,7 +491,7 @@ namespace ADOFAIRenderer.Util
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MelonLogger.Warning($"[OSNotification] 리눅스 알림 처리 실패: {ex.Message}");
|
||||
MelonLogger.Warning($"[OSNotification] Linux notification processing failed: {ex.Message}");
|
||||
|
||||
// Fallback: show without action
|
||||
ShowSimple(title, message);
|
||||
@@ -554,7 +554,7 @@ namespace ADOFAIRenderer.Util
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MelonLogger.Warning($"[OSNotification] gdbus 알림 실패: {ex.Message}");
|
||||
MelonLogger.Warning($"[OSNotification] gdbus notification failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-12
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
|
||||
namespace ADOFAIRenderer.Util
|
||||
{
|
||||
@@ -7,49 +7,49 @@ namespace ADOFAIRenderer.Util
|
||||
public static (int Numerator, int Denominator) Double2MaxPrecisionInt(double value,
|
||||
int maxDenominator = 100000000)
|
||||
{
|
||||
// 1. 예외 처리 및 부호 판별
|
||||
// 1. Edge case handling and sign detection
|
||||
if (value == 0.0) return (0, 1);
|
||||
|
||||
var isNegative = value < 0;
|
||||
var target = Math.Abs(value);
|
||||
// 2. 스턴-브로코 / 연속분수 경계값 초기화
|
||||
// 아래 두 행의 분수 (0/1, 1/0) 사이에서 시작하여 타겟에 가까워집니다.
|
||||
// 2. Stern-Brocot / continued fraction boundary initialization
|
||||
// Starts between (0/1, 1/0) and converges towards the target value.
|
||||
long m00 = 0, m01 = 1;
|
||||
long m10 = 1, m11 = 0;
|
||||
|
||||
var x = target;
|
||||
|
||||
// int 범위 오버플로우를 방지하면서 반복 처리
|
||||
// Iterate while preventing integer overflow
|
||||
while (true)
|
||||
{
|
||||
var ai = (long)Math.Floor(x);
|
||||
|
||||
// 다음 단계의 분자, 분모 계산 (오버플로우 방지를 위해 long으로 임시 계산)
|
||||
// Calculate next numerator and denominator (temporarily computed as long to prevent overflow)
|
||||
var nextNumerator = ai * m10 + m00;
|
||||
var nextDenominator = ai * m11 + m01;
|
||||
|
||||
// int 범위를 벗어나거나, 설정한 최대 분모를 초과하면 이전 단계의 최적값을 반환
|
||||
// If exceeding integer range or specified maximum denominator, return previous optimal value
|
||||
if (nextNumerator > int.MaxValue || nextDenominator > maxDenominator)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// 행렬 업데이트
|
||||
// Update matrix
|
||||
m00 = m10; m01 = m11;
|
||||
m10 = nextNumerator; m11 = nextDenominator;
|
||||
|
||||
// 오차가 거의 없으면 (정밀도 도달) 종료
|
||||
// Terminate if error is negligible (precision limit reached)
|
||||
if (Math.Abs(x - ai) < 1e-15) break;
|
||||
|
||||
// 다음 연속분수 항 준비
|
||||
// Prepare next continued fraction term
|
||||
x = 1.0 / (x - ai);
|
||||
}
|
||||
|
||||
// 3. 최종 결정된 int 기반의 분자 분모
|
||||
// 3. Final determined integer numerator and denominator
|
||||
var finalNumerator = (int)m10;
|
||||
var finalDenominator = (int)m11;
|
||||
|
||||
// 부호 복원
|
||||
// Restore sign
|
||||
if (isNegative) finalNumerator = -finalNumerator;
|
||||
|
||||
return (finalNumerator, finalDenominator);
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.IO;
|
||||
using MelonLoader;
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace ADOFAIRenderer.Util
|
||||
}
|
||||
catch (IOException err)
|
||||
{
|
||||
Melon<RenderMod>.Logger.Error($"FFmpeg 파이프가 예상치 못하게 닫혔습니다: {err.Message}");
|
||||
Melon<RenderMod>.Logger.Error($"FFmpeg pipe closed unexpectedly: {err.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user