SHA256
Add Renderer Feature
This commit is contained in:
1 parent
c9314c264d
commit
2556bc7f62
13 files changed
+727
-21
No files matched your search
@@ -0,0 +1,115 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace ADOFAIRenderer.Component
|
||||
{
|
||||
public class DebugMonitorUI : MonoBehaviour
|
||||
{
|
||||
private Text _text = null!;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// UI 레이어(5) 설정
|
||||
gameObject.layer = 5;
|
||||
|
||||
// Canvas 설정
|
||||
Canvas canvas = gameObject.AddComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
canvas.sortingOrder = 9999;
|
||||
|
||||
// 해상도 다이내믹 스케일링 설정 (Reference: 1920x1080)
|
||||
CanvasScaler scaler = gameObject.AddComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(1920, 1080);
|
||||
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
|
||||
scaler.matchWidthOrHeight = 0.5f;
|
||||
|
||||
gameObject.AddComponent<GraphicRaycaster>();
|
||||
|
||||
// 배경 패널 생성
|
||||
GameObject panelObj = new GameObject("BackgroundPanel");
|
||||
panelObj.transform.SetParent(transform, false);
|
||||
panelObj.layer = 5;
|
||||
|
||||
Image image = panelObj.AddComponent<Image>();
|
||||
image.color = new Color(0.08f, 0.08f, 0.1f, 0.88f); // 약간 더 고급스러운 다크톤 아크릴 감성
|
||||
|
||||
RectTransform panelRect = panelObj.GetComponent<RectTransform>();
|
||||
panelRect.anchorMin = new Vector2(1, 1);
|
||||
panelRect.anchorMax = new Vector2(1, 1);
|
||||
panelRect.pivot = new Vector2(1, 1);
|
||||
panelRect.anchoredPosition = new Vector2(-30, -30);
|
||||
panelRect.sizeDelta = new Vector2(340, 250);
|
||||
|
||||
// 텍스트 생성
|
||||
GameObject textObj = new GameObject("MonitorText");
|
||||
textObj.transform.SetParent(panelObj.transform, false);
|
||||
textObj.layer = 5;
|
||||
|
||||
_text = textObj.AddComponent<Text>();
|
||||
_text.supportRichText = true;
|
||||
_text.fontSize = 13;
|
||||
_text.lineSpacing = 1.3f;
|
||||
_text.alignment = TextAnchor.UpperLeft;
|
||||
|
||||
// uGUI 특유의 텍스트 잘림 현상 원천 예방
|
||||
_text.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
_text.verticalOverflow = VerticalWrapMode.Overflow;
|
||||
|
||||
Font? font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
|
||||
if (font == null)
|
||||
{
|
||||
font = Resources.GetBuiltinResource<Font>("Arial.ttf");
|
||||
}
|
||||
if (font == null)
|
||||
{
|
||||
var fonts = Resources.FindObjectsOfTypeAll<Font>();
|
||||
if (fonts.Length > 0) font = fonts[0];
|
||||
}
|
||||
_text.font = font;
|
||||
|
||||
RectTransform textRect = textObj.GetComponent<RectTransform>();
|
||||
textRect.anchorMin = Vector2.zero;
|
||||
textRect.anchorMax = Vector2.one;
|
||||
textRect.offsetMin = new Vector2(20, 20); // 좀 더 넉넉하고 아름다운 패딩 여백
|
||||
textRect.offsetMax = new Vector2(-20, -20);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!_text) return;
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.AppendLine("<b><color=#00ccff>ADOFAI Renderer Realtime Monitor</color></b>");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"• Rendered Progress: <b>{RenderManager.FrameCount}</b> frames");
|
||||
|
||||
int queueCount;
|
||||
lock (RenderManager.ImageQueue)
|
||||
{
|
||||
queueCount = RenderManager.ImageQueue.Count;
|
||||
}
|
||||
sb.AppendLine($"• Queued Buffers: <b>{queueCount}</b> / 60 frames");
|
||||
|
||||
if (RenderMod.IsThrottling)
|
||||
{
|
||||
sb.AppendLine("• Status: <b><color=yellow>THROTTLING (Safe Mode)</color></b>");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine("• Status: <b><color=green>NORMAL (Encoding)</color></b>");
|
||||
}
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"• Throttling Controls: {RenderMod.ThrottleCount} times");
|
||||
sb.AppendLine($"• Readback Hard Failures: <color=#ff3333>{RenderMod.ReadbackFailureCount}</color> times");
|
||||
sb.AppendLine($"• Render Skip-Retry: {RenderMod.SkipCount} / 60");
|
||||
|
||||
double memoryMb = GC.GetTotalMemory(false) / (1024.0 * 1024.0);
|
||||
sb.AppendLine($"• Mono Heap Memory: {memoryMb:F2} MB");
|
||||
|
||||
_text.text = sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ADOFAIRenderer.Component
|
||||
@@ -5,9 +6,10 @@ namespace ADOFAIRenderer.Component
|
||||
[DefaultExecutionOrder(int.MaxValue)]
|
||||
public class PostRenderHook : MonoBehaviour
|
||||
{
|
||||
private void OnPostRender()
|
||||
private void LateUpdate()
|
||||
{
|
||||
RenderMod.OnPostRender();
|
||||
var val = (MeshRenderer)AccessTools.Field(typeof(scrCamera), "camQuadMesh").GetValue(scrCamera.instance);
|
||||
RenderMod.OnPostRender(val.material.mainTexture as RenderTexture);
|
||||
}
|
||||
}
|
||||
}
|
||||
+228
-8
@@ -1,5 +1,10 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using ADOFAIRenderer.Util;
|
||||
using MelonLoader;
|
||||
using MelonLoader.Utils;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -7,12 +12,13 @@ namespace ADOFAIRenderer.Component
|
||||
{
|
||||
public class RenderManager : MonoBehaviour
|
||||
{
|
||||
public static int Countdown = 0;
|
||||
public static int FrameCount = 0;
|
||||
public static bool SkipPressToStart = false;
|
||||
public static int Countdown;
|
||||
public static int FrameCount;
|
||||
public static bool SkipPressToStart;
|
||||
public static string LevelPath = "";
|
||||
public static RenderManager Instance = null!;
|
||||
private static int _originalInputOffset;
|
||||
private static GameObject? _debugMonitorObj;
|
||||
|
||||
public static RenderSettings Configuration;
|
||||
|
||||
@@ -26,8 +32,9 @@ namespace ADOFAIRenderer.Component
|
||||
|
||||
public int Bitrate;
|
||||
public string Codec;
|
||||
public double EndWaitTime;
|
||||
|
||||
public RenderSettings(double targetFPS, double videoFPS, int renderWidth, int renderHeight, int bitrate, string codec)
|
||||
public RenderSettings(double targetFPS, double videoFPS, int renderWidth, int renderHeight, int bitrate, string codec, double endWaitTime)
|
||||
{
|
||||
TargetFPS = targetFPS;
|
||||
VideoFPS = videoFPS;
|
||||
@@ -37,9 +44,16 @@ namespace ADOFAIRenderer.Component
|
||||
|
||||
Bitrate = bitrate;
|
||||
Codec = codec;
|
||||
EndWaitTime = endWaitTime;
|
||||
}
|
||||
}
|
||||
|
||||
public static SortedList<int, byte[]> ImageQueue = new SortedList<int, byte[]>();
|
||||
public static Thread? RenderThread;
|
||||
public static int PassCount = 0;
|
||||
public static bool EndRequested = false;
|
||||
public static bool EndSuccessful = false;
|
||||
private static int _wonFrame = -1;
|
||||
|
||||
private static bool _renderSetup;
|
||||
|
||||
@@ -67,17 +81,46 @@ namespace ADOFAIRenderer.Component
|
||||
_originalInputOffset = scrConductor.currentPreset.inputOffset;
|
||||
scrConductor.currentPreset.inputOffset = 0;
|
||||
RDC.auto = true;
|
||||
if (scrController.instance)
|
||||
{
|
||||
scrController.instance.noFail = true;
|
||||
scrController.instance.noFailInfiniteMargin = true;
|
||||
}
|
||||
|
||||
RenderMod.TimeBase = Time.timeAsDouble;
|
||||
RenderMod.UnscaledTimeBase = Time.unscaledTimeAsDouble;
|
||||
RenderMod.DspTimeBase = AudioSettings.dspTime;
|
||||
RenderMod.FrameCountBase = Time.frameCount;
|
||||
|
||||
RenderMod.IsRendering = true;
|
||||
Countdown = 3;
|
||||
|
||||
scrController.instance.StartCoroutine(SetupPlay());
|
||||
scrController.instance!.StartCoroutine(SetupPlay());
|
||||
}
|
||||
|
||||
if (RenderMod.IsRendering && scrController.instance.state == States.Won)
|
||||
|
||||
if (!RenderMod.IsRendering) return;
|
||||
if (!scrController.instance || scrController.instance.state == States.Fail || scrController.instance.state == States.Fail2)
|
||||
{
|
||||
RenderMod.IsRendering = RenderMod.IsCapturing = false;
|
||||
|
||||
Time.timeScale = 1f;
|
||||
scrConductor.currentPreset.inputOffset = _originalInputOffset;
|
||||
StopFFmpeg(false);
|
||||
}
|
||||
else if (scrController.instance.state == States.Won)
|
||||
{
|
||||
if (_wonFrame == -1)
|
||||
{
|
||||
_wonFrame = FrameCount;
|
||||
}
|
||||
|
||||
var extraFrames = (int)Math.Ceiling(Configuration.EndWaitTime * Configuration.TargetFPS);
|
||||
|
||||
if (FrameCount - _wonFrame < extraFrames) return;
|
||||
RenderMod.IsRendering = RenderMod.IsCapturing = false;
|
||||
Time.timeScale = 1f;
|
||||
scrConductor.currentPreset.inputOffset = _originalInputOffset;
|
||||
StopFFmpeg(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,8 +152,11 @@ namespace ADOFAIRenderer.Component
|
||||
{
|
||||
yield return new WaitForSecondsRealtime(1);
|
||||
}
|
||||
|
||||
|
||||
FrameCount = 0;
|
||||
|
||||
StartFFmpegThread();
|
||||
|
||||
RenderMod.IsCapturing = true;
|
||||
scrController.instance.paused = scrController.instance.audioPaused = false;
|
||||
scrController.instance.levelWasSkipped = true;
|
||||
@@ -118,5 +164,179 @@ namespace ADOFAIRenderer.Component
|
||||
scrConductor.instance.song.clip = null;
|
||||
scrConductor.instance.song.time = -1000;
|
||||
}
|
||||
|
||||
private string FindFFmpegPath()
|
||||
{
|
||||
bool isWindows = Application.platform == RuntimePlatform.WindowsPlayer;
|
||||
string ffmpegName = isWindows ? "ffmpeg.exe" : "ffmpeg";
|
||||
|
||||
// 1. Mods/ADOFAIRenderer/ffmpeg
|
||||
var path1 = Path.Combine(MelonEnvironment.ModsDirectory, "ADOFAIRenderer", ffmpegName);
|
||||
if (File.Exists(path1)) return path1;
|
||||
|
||||
// 2. Mods/ffmpeg
|
||||
var path2 = Path.Combine(MelonEnvironment.ModsDirectory, ffmpegName);
|
||||
if (File.Exists(path2)) return path2;
|
||||
|
||||
// 3. GameRootDirectory/ffmpeg
|
||||
var path3 = Path.Combine(MelonEnvironment.GameRootDirectory, ffmpegName);
|
||||
if (File.Exists(path3)) return path3;
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
private void StartFFmpegThread()
|
||||
{
|
||||
var ffmpegPath = FindFFmpegPath();
|
||||
if (string.IsNullOrEmpty(ffmpegPath))
|
||||
{
|
||||
Melon<RenderMod>.Logger.Error("ffmpeg 바이너리를 찾을 수 없습니다. Mods/ADOFAIRenderer/ 에 ffmpeg를 넣어주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_debugMonitorObj == null)
|
||||
{
|
||||
_debugMonitorObj = new GameObject("DebugMonitorCanvas");
|
||||
_debugMonitorObj.AddComponent<DebugMonitorUI>();
|
||||
DontDestroyOnLoad(_debugMonitorObj);
|
||||
}
|
||||
|
||||
FFmpegProcess.FFmpegPath = ffmpegPath;
|
||||
lock (ImageQueue)
|
||||
{
|
||||
ImageQueue.Clear();
|
||||
}
|
||||
PassCount = 0;
|
||||
RenderMod.FrameCountForVideo = 0;
|
||||
RenderMod.RenderCheck = true;
|
||||
RenderMod.SkipCount = 0;
|
||||
EndRequested = false;
|
||||
EndSuccessful = false;
|
||||
_wonFrame = -1;
|
||||
|
||||
double targetFps = Configuration.TargetFPS;
|
||||
double videoFps = Configuration.VideoFPS;
|
||||
float bitrate = Configuration.Bitrate;
|
||||
string codecStr = Configuration.Codec;
|
||||
|
||||
VideoFFmpeg.Codec selectedCodec = VideoFFmpeg.Codec.H264_Default;
|
||||
if (Enum.TryParse<VideoFFmpeg.Codec>(codecStr, true, out var parsedCodec))
|
||||
{
|
||||
selectedCodec = parsedCodec;
|
||||
}
|
||||
|
||||
string ext = ".mp4";
|
||||
if (selectedCodec == VideoFFmpeg.Codec.AV1_Default ||
|
||||
selectedCodec == VideoFFmpeg.Codec.AV1_NVIDIA ||
|
||||
selectedCodec == VideoFFmpeg.Codec.AV1_QSV ||
|
||||
selectedCodec == VideoFFmpeg.Codec.VP9_Default ||
|
||||
selectedCodec == VideoFFmpeg.Codec.VP9_NVIDIA)
|
||||
{
|
||||
ext = ".webm";
|
||||
}
|
||||
|
||||
RenderThread = new Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
float workingFps = (float)(targetFps % videoFps == 0.0 ? videoFps : targetFps);
|
||||
VideoFFmpeg videoFFmpeg = new VideoFFmpeg(Configuration.RenderWidth, Configuration.RenderHeight, (int)workingFps, (int)videoFps, selectedCodec);
|
||||
|
||||
var outPath = Path.Combine(MelonEnvironment.GameRootDirectory, "Render", "out" + ext);
|
||||
SetupCaptureDirectory();
|
||||
|
||||
videoFFmpeg.Start(outPath, bitrate);
|
||||
|
||||
int num = targetFps % videoFps != 0.0 ? 1 : (int)(targetFps / videoFps);
|
||||
|
||||
while (true)
|
||||
{
|
||||
byte[]? buffer = null;
|
||||
lock (ImageQueue)
|
||||
{
|
||||
if (ImageQueue.Count > 0)
|
||||
{
|
||||
int firstKey = ImageQueue.Keys[0];
|
||||
if (firstKey == PassCount)
|
||||
{
|
||||
buffer = ImageQueue[firstKey];
|
||||
ImageQueue.RemoveAt(0);
|
||||
PassCount += num;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (buffer != null)
|
||||
{
|
||||
if (buffer.Length > 0)
|
||||
{
|
||||
int bufferSize = Configuration.RenderWidth * Configuration.RenderHeight * 4;
|
||||
videoFFmpeg.SendDataFromBuffer(buffer, bufferSize);
|
||||
System.Buffers.ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (EndRequested)
|
||||
{
|
||||
bool hasMore;
|
||||
lock (ImageQueue)
|
||||
{
|
||||
hasMore = ImageQueue.Count > 0;
|
||||
}
|
||||
if (!hasMore) break;
|
||||
}
|
||||
|
||||
Thread.Sleep(1);
|
||||
}
|
||||
|
||||
videoFFmpeg.Close();
|
||||
EndSuccessful = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Melon<RenderMod>.Logger.Error($"FFmpeg 렌더링 스레드 오류: {ex}");
|
||||
}
|
||||
});
|
||||
|
||||
RenderThread.Start();
|
||||
}
|
||||
|
||||
private void StopFFmpeg(bool success)
|
||||
{
|
||||
EndRequested = true;
|
||||
if (success)
|
||||
{
|
||||
StartCoroutine(StopRenderCoroutine());
|
||||
}
|
||||
else
|
||||
{
|
||||
Melon<RenderMod>.Logger.Error("렌더링이 실패했거나 취소되었습니다.");
|
||||
if (_debugMonitorObj)
|
||||
{
|
||||
Destroy(_debugMonitorObj);
|
||||
_debugMonitorObj = null;
|
||||
}
|
||||
if (RenderThread != null && RenderThread.IsAlive)
|
||||
{
|
||||
RenderThread.Join(2000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerator StopRenderCoroutine()
|
||||
{
|
||||
yield return new WaitUntil(() => EndSuccessful);
|
||||
|
||||
if (_debugMonitorObj)
|
||||
{
|
||||
Destroy(_debugMonitorObj);
|
||||
_debugMonitorObj = null;
|
||||
}
|
||||
|
||||
RDC.auto = false;
|
||||
ADOBase.loader.LoadSceneWithTransition(WipeDirection.StartsFromRight, "scnEditor");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user