diff --git a/ADOFAIRenderer.csproj b/ADOFAIRenderer.csproj index ffd87e3..23a594e 100644 --- a/ADOFAIRenderer.csproj +++ b/ADOFAIRenderer.csproj @@ -71,6 +71,9 @@ $(ADOFAI_PATH)\A Dance of Fire and Ice_Data\Managed\UnityEngine.UIModule.dll + + $(ADOFAI_PATH)\A Dance of Fire and Ice_Data\Managed\UnityEngine.VideoModule.dll + diff --git a/Util/FFmpegVideoBGPlayer.cs b/Util/FFmpegVideoBGPlayer.cs index ec71c9e..6cbf54e 100644 --- a/Util/FFmpegVideoBGPlayer.cs +++ b/Util/FFmpegVideoBGPlayer.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using ADOFAIRenderer.Util.FFmpeg; using MelonLoader; @@ -10,6 +11,16 @@ namespace ADOFAIRenderer.Util // ReSharper disable once InconsistentNaming public class FFmpegVideoBGPlayer : MonoBehaviour { + private static readonly int YTex = Shader.PropertyToID("_YTex"); + private static readonly int UVTex = Shader.PropertyToID("_UVTex"); + private static readonly int Result = Shader.PropertyToID("_Result"); + private static readonly int Width = Shader.PropertyToID("Width"); + private static readonly int Height = Shader.PropertyToID("Height"); + private static readonly int UTex = Shader.PropertyToID("_UTex"); + private static readonly int VTex = Shader.PropertyToID("_VTex"); + private static readonly int ColorRange = Shader.PropertyToID("ColorRange"); + + [SuppressMessage("ReSharper", "InconsistentNaming")] private enum GPUMode { None, @@ -20,6 +31,7 @@ namespace ADOFAIRenderer.Util private FFmpegNativeDecoder? _decoder; // CPU fallback rendering + // ReSharper disable once InconsistentNaming private Texture2D? _textureRGBA; // GPU conversion textures @@ -40,13 +52,22 @@ namespace ADOFAIRenderer.Util private bool _isLooping; private bool _isInitialized; - public void Init(string videoPath, RenderTexture targetTexture, bool loop) + private GameObject? _quad; + private Material? _material; + + // ReSharper disable once InconsistentNaming + public void Init(string videoPath, RenderTexture? targetTexture, bool loop, Camera? targetCamera, GameObject videoBGGo) { CleanUp(); _targetTexture = targetTexture; _isLooping = loop; + if (_targetTexture == null && targetCamera != null) + { + SetupBackgroundQuad(targetCamera, videoBGGo); + } + if (!File.Exists(videoPath)) { Melon.Logger.Warning($"[FFmpegVideoBGPlayer] 비디오 파일이 존재하지 않습니다: {videoPath}"); @@ -58,56 +79,70 @@ namespace ADOFAIRenderer.Util _decoder = new FFmpegNativeDecoder(videoPath); _computeShader = RenderMod.ColorConversionShader; - int w = _decoder.Width; - int h = _decoder.Height; - AVPixelFormat pixFmt = _decoder.PixelFormat; + var w = _decoder.Width; + var h = _decoder.Height; + var pixFmt = _decoder.PixelFormat; - bool gpuInitSuccess = false; + var gpuInitSuccess = false; if (_computeShader != null) { - if (pixFmt == AVPixelFormat.AV_PIX_FMT_NV12) + switch (pixFmt) { - try - { - _kernelIndex = _computeShader.FindKernel("CSMain_NV12ToRGBA"); + case AVPixelFormat.AV_PIX_FMT_NV12: + try + { + _kernelIndex = _computeShader.FindKernel("CSMain_NV12ToRGBA"); - _textureY = new Texture2D(w, h, TextureFormat.R8, false); - _textureUV = new Texture2D(w / 2, h / 2, TextureFormat.RG16, false); - ConfigureTexture(_textureY); - ConfigureTexture(_textureUV); + _textureY = new Texture2D(w, h, TextureFormat.R8, false); + _textureUV = new Texture2D(w / 2, h / 2, TextureFormat.RG16, false); + ConfigureTexture(_textureY); + ConfigureTexture(_textureUV); - _gpuMode = GPUMode.NV12; - gpuInitSuccess = true; - Melon.Logger.Msg($"[FFmpegVideoBGPlayer] Compute Shader NV12 모드 초기화 완료"); - } - catch { } - } - else if (pixFmt == AVPixelFormat.AV_PIX_FMT_YUV420P || pixFmt == AVPixelFormat.AV_PIX_FMT_YUVJ420P) - { - try - { - _kernelIndex = _computeShader.FindKernel("CSMain_YUV420PToRGBA"); + _gpuMode = GPUMode.NV12; + gpuInitSuccess = true; + Melon.Logger.Msg($"[FFmpegVideoBGPlayer] Compute Shader NV12 모드 초기화 완료"); + } + catch + { + // ignored + } + + break; + case AVPixelFormat.AV_PIX_FMT_YUV420P: + case AVPixelFormat.AV_PIX_FMT_YUVJ420P: + try + { + _kernelIndex = _computeShader.FindKernel("CSMain_YUV420PToRGBA"); - _textureY = new Texture2D(w, h, TextureFormat.R8, false); - _textureU = new Texture2D(w / 2, h / 2, TextureFormat.R8, false); - _textureV = new Texture2D(w / 2, h / 2, TextureFormat.R8, false); - ConfigureTexture(_textureY); - ConfigureTexture(_textureU); - ConfigureTexture(_textureV); + _textureY = new Texture2D(w, h, TextureFormat.R8, false); + _textureU = new Texture2D(w / 2, h / 2, TextureFormat.R8, false); + _textureV = new Texture2D(w / 2, h / 2, TextureFormat.R8, false); + ConfigureTexture(_textureY); + ConfigureTexture(_textureU); + ConfigureTexture(_textureV); - _gpuMode = GPUMode.YUV420P; - gpuInitSuccess = true; - Melon.Logger.Msg($"[FFmpegVideoBGPlayer] Compute Shader YUV420P 모드 초기화 완료"); - } - catch { } + _gpuMode = GPUMode.YUV420P; + gpuInitSuccess = true; + Melon.Logger.Msg($"[FFmpegVideoBGPlayer] Compute Shader YUV420P 모드 초기화 완료"); + } + catch + { + // ignored + } + + break; + default: + throw new ArgumentOutOfRangeException(); } } if (gpuInitSuccess && _computeShader != null) { - _renderTexture = new RenderTexture(w, h, 0, RenderTextureFormat.ARGB32); - _renderTexture.enableRandomWrite = true; + _renderTexture = new RenderTexture(w, h, 0, RenderTextureFormat.ARGB32) + { + enableRandomWrite = true + }; _renderTexture.Create(); } else @@ -131,35 +166,49 @@ namespace ADOFAIRenderer.Util } } - private void ConfigureTexture(Texture2D tex) + private static void ConfigureTexture(Texture2D tex) { tex.wrapMode = TextureWrapMode.Clamp; tex.filterMode = FilterMode.Bilinear; } - private void Update() + private void LateUpdate() { if (!_isInitialized || _decoder == null) return; - scrConductor cond = scrConductor.instance; - if (cond == null || !cond.hasSongStarted) return; + var cond = scrConductor.instance; + if (!cond || !cond.hasSongStarted) return; + + var pitch = cond.song.pitch; + var separateCountdownOffset = cond.separateCountdownTime ? + cond.crotchetAtStart * cond.adjustedCountdownTicks : 0.0; + var songTime = (cond.dspTime - cond.dspTimeSong) * pitch - cond.addoffset - separateCountdownOffset; - double num4 = (cond.separateCountdownTime ? (cond.crotchetAtStart * (double)ADOBase.conductor.adjustedCountdownTicks) : 0.0); - double vidOffset = scrVfxPlus.instance ? scrVfxPlus.instance.vidOffset : 0.0; - double targetVideoTime = cond.songposition_minusi - num4 + vidOffset; + var vidOffset = scrVfxPlus.instance ? scrVfxPlus.instance.vidOffset : 0.0; + var targetVideoTime = songTime + vidOffset; if (targetVideoTime < 0) { - if (_targetTexture != null) + if (_targetTexture) { - RenderTexture active = RenderTexture.active; + var active = RenderTexture.active; RenderTexture.active = _targetTexture; GL.Clear(true, true, Color.black); RenderTexture.active = active; } + if (_quad) + { + _quad.SetActive(false); + } return; } + + if (ADOBase.customLevel) + { + ADOBase.customLevel.ShowTutorialBackground(false); + } + try { if (targetVideoTime < _currentFrameTimestamp || targetVideoTime - _currentFrameTimestamp > 1.0) @@ -172,10 +221,10 @@ namespace ADOFAIRenderer.Util bool frameUpdated = false; while (targetVideoTime >= _nextFrameTimestamp) { - double timestamp; + double timestamp = 0.0; bool success = false; - if (_gpuMode == GPUMode.NV12 && _textureY != null && _textureUV != null) + if (_gpuMode == GPUMode.NV12 && _textureY && _textureUV) { success = _decoder.TryReadNV12Frame( _textureY.GetRawTextureData(), @@ -183,7 +232,7 @@ namespace ADOFAIRenderer.Util out timestamp ); } - else if (_gpuMode == GPUMode.YUV420P && _textureY != null && _textureU != null && _textureV != null) + else if (_gpuMode == GPUMode.YUV420P && _textureY && _textureU && _textureV) { success = _decoder.TryReadYUV420PFrame( _textureY.GetRawTextureData(), @@ -240,51 +289,95 @@ namespace ADOFAIRenderer.Util private void RenderToTarget() { - if (_decoder == null || _targetTexture == null) return; + if (_decoder == null) return; int w = _decoder.Width; int h = _decoder.Height; - if (_gpuMode == GPUMode.NV12 && _textureY != null && _textureUV != null && _computeShader != null && _renderTexture != null) + if (_gpuMode == GPUMode.NV12 && _textureY && _textureUV && _computeShader && _renderTexture) { _textureY.Apply(); _textureUV.Apply(); - _computeShader.SetTexture(_kernelIndex, "_YTex", _textureY); - _computeShader.SetTexture(_kernelIndex, "_UVTex", _textureUV); - _computeShader.SetTexture(_kernelIndex, "_Result", _renderTexture); - _computeShader.SetInt("Width", w); - _computeShader.SetInt("Height", h); + _computeShader.SetTexture(_kernelIndex, YTex, _textureY); + _computeShader.SetTexture(_kernelIndex, UVTex, _textureUV); + _computeShader.SetTexture(_kernelIndex, Result, _renderTexture); + _computeShader.SetInt(Width, w); + _computeShader.SetInt(Height, h); + _computeShader.SetInt(ColorRange, _decoder.ColorRange == AVColorRange.AVCOL_RANGE_JPEG ? 1 : 0); int threadGroupsX = (w + 7) / 8; int threadGroupsY = (h + 7) / 8; _computeShader.Dispatch(_kernelIndex, threadGroupsX, threadGroupsY, 1); - Graphics.Blit(_renderTexture, _targetTexture); + if (_targetTexture) + { + Graphics.Blit(_renderTexture, _targetTexture); + } + else if (_material) + { + if (_material.mainTexture != _renderTexture) + { + _material.mainTexture = _renderTexture; + } + if (_quad != null && !_quad.activeSelf) + { + _quad.SetActive(true); + } + } } - else if (_gpuMode == GPUMode.YUV420P && _textureY != null && _textureU != null && _textureV != null && _computeShader != null && _renderTexture != null) + else if (_gpuMode == GPUMode.YUV420P && _textureY && _textureU && _textureV && _computeShader && _renderTexture) { _textureY.Apply(); _textureU.Apply(); _textureV.Apply(); - _computeShader.SetTexture(_kernelIndex, "_YTex", _textureY); - _computeShader.SetTexture(_kernelIndex, "_UTex", _textureU); - _computeShader.SetTexture(_kernelIndex, "_VTex", _textureV); - _computeShader.SetTexture(_kernelIndex, "_Result", _renderTexture); - _computeShader.SetInt("Width", w); - _computeShader.SetInt("Height", h); + _computeShader.SetTexture(_kernelIndex, YTex, _textureY); + _computeShader.SetTexture(_kernelIndex, UTex, _textureU); + _computeShader.SetTexture(_kernelIndex, VTex, _textureV); + _computeShader.SetTexture(_kernelIndex, Result, _renderTexture); + _computeShader.SetInt(Width, w); + _computeShader.SetInt(Height, h); + _computeShader.SetInt(ColorRange, _decoder.ColorRange == AVColorRange.AVCOL_RANGE_JPEG ? 1 : 0); int threadGroupsX = (w + 7) / 8; int threadGroupsY = (h + 7) / 8; _computeShader.Dispatch(_kernelIndex, threadGroupsX, threadGroupsY, 1); - Graphics.Blit(_renderTexture, _targetTexture); + if (_targetTexture) + { + Graphics.Blit(_renderTexture, _targetTexture); + } + else if (_material) + { + if (_material.mainTexture != _renderTexture) + { + _material.mainTexture = _renderTexture; + } + if (_quad != null && !_quad.activeSelf) + { + _quad.SetActive(true); + } + } } - else if (_textureRGBA != null) + else if (_textureRGBA) { _textureRGBA.Apply(); - Graphics.Blit(_textureRGBA, _targetTexture); + if (_targetTexture) + { + Graphics.Blit(_textureRGBA, _targetTexture); + } + else if (_material) + { + if (_material.mainTexture != _textureRGBA) + { + _material.mainTexture = _textureRGBA; + } + if (_quad != null && !_quad.activeSelf) + { + _quad.SetActive(true); + } + } } } @@ -326,6 +419,98 @@ namespace ADOFAIRenderer.Util Destroy(_renderTexture); _renderTexture = null; } + + if (_quad != null) + { + Destroy(_quad); + _quad = null; + } + + if (_material != null) + { + Destroy(_material); + _material = null; + } + } + + // ReSharper disable once InconsistentNaming + private void SetupBackgroundQuad(Camera targetCamera, GameObject videoBGGo) + { + _quad = GameObject.CreatePrimitive(PrimitiveType.Quad); + _quad.name = "FFmpegVideoBGQuad"; + + // ReSharper disable once Unity.UnresolvedComponentOrScriptableObject + var col = _quad.GetComponent("MeshCollider"); + if (col) Destroy(col); + + _quad.transform.parent = targetCamera.transform; + + var zPos = targetCamera.farClipPlane - 0.5f; + _quad.transform.localPosition = new Vector3(0f, 0f, zPos); + _quad.transform.localRotation = Quaternion.identity; + + float height; + float width; + if (targetCamera.orthographic) + { + height = 2.0f * targetCamera.orthographicSize; + width = height * targetCamera.aspect; + } + else + { + height = 2.0f * zPos * Mathf.Tan(targetCamera.fieldOfView * 0.5f * Mathf.Deg2Rad); + width = height * targetCamera.aspect; + } + _quad.transform.localScale = new Vector3(width, height, 1.0f); + _quad.layer = videoBGGo.layer; + + // Set vertex colors to white so Sprites/Default works properly (otherwise it may draw transparently) + var meshFilter = _quad.GetComponent(); + if (meshFilter != null && meshFilter.mesh != null) + { + var mesh = meshFilter.mesh; + var colors = new Color[mesh.vertexCount]; + for (var i = 0; i < colors.Length; i++) + { + colors[i] = Color.white; + } + mesh.colors = colors; + } + + Shader? shader = null; + if (scrCamera.instance != null) + { + try + { + var camQuadMeshField = typeof(scrCamera).GetField("camQuadMesh", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + if (camQuadMeshField != null) + { + var renderer = camQuadMeshField.GetValue(scrCamera.instance) as MeshRenderer; + if (renderer != null && renderer.sharedMaterial != null) + { + shader = renderer.sharedMaterial.shader; + } + } + } + catch (Exception) + { + // ignored + } + } + + if (shader == null) shader = Shader.Find("Sprites/Default"); + if (shader == null) shader = Shader.Find("Unlit/Texture"); + if (shader == null) shader = Shader.Find("Standard"); + + _material = new Material(shader) + { + color = Color.white + }; + + _quad.GetComponent().material = _material; + + // Set inactive initially to prevent white flash + _quad.SetActive(false); } } }