diff --git a/Assets/Resources/Shaders.meta b/Assets/Resources/Shaders.meta new file mode 100644 index 0000000..9a0926e --- /dev/null +++ b/Assets/Resources/Shaders.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7e487cf3ec7419243a47a197ca9f7062 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Resources/Shaders/PostFilm.shader b/Assets/Resources/Shaders/PostFilm.shader new file mode 100644 index 0000000..eaab55e --- /dev/null +++ b/Assets/Resources/Shaders/PostFilm.shader @@ -0,0 +1,130 @@ +// PostFilm (轨道 39) 的全屏叠加 —— MVP 版本。 +// +// 已实现:filmMode 的 None/Lerp/Add/Mul/Monochrome,colorType 全部 4 种布局, +// Vignette* 三种按「基础混合 × 径向衰减」近似。 +// 未实现:depthPower/DepthClip(深度混合)、layerMode=UVMovie(图层贴图)。 +// +// 参数由 Director.OnPostFilmUpdate 每帧写入,见 PostFilmRendererFeature.cs。 +Shader "Hidden/Gallop/PostFilm" +{ + SubShader + { + Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" } + ZWrite Off Cull Off ZTest Always + + Pass + { + Name "PostFilm" + + HLSLPROGRAM + #pragma vertex Vert + #pragma fragment Frag + + // 两个 include 缺一不可,且顺序不能反: + // universal/ShaderLibrary/Core.hlsl —— 定义 TEXTURE2D_X / SAMPLE_TEXTURE2D_X 等 XR 宏 + // core/Runtime/Utilities/Blit.hlsl —— 声明 TEXTURE2D_X(_BlitTexture)、全屏三角形 Vert()、 + // 并带进 GlobalSamplers.hlsl(sampler_LinearClamp) + // 注意 Blit.hlsl 在 **core** 包的 Runtime/Utilities/ 下,不在 universal 包的 ShaderLibrary/。 + #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" + #include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl" + + // filmMode: None=0 Lerp=1 Add=2 Mul=3 VignetteLerp=4 VignetteAdd=5 VignetteMul=6 Monochrome=7 + // colorType: ColorAll=0 Color2TopBottom=1 Color2LeftRight=2 Color4=3 + int _FilmMode; + int _ColorType; + float _FilmPower; + float4 _Color0; + float4 _Color1; + float4 _Color2; + float4 _Color3; + float2 _FilmOffset; + float2 _FilmScale; + float _RollAngle; + float4 _FilmOption; + // Vignette 几何参数的标定系数,由 RendererFeature 暴露到 Inspector 供实时调。 + // 原始 filmOptionParam 的单位未知,只能靠观感标定。 + float _VignetteWidth; // 缩放作用宽度 + float _VignetteStrength; // 缩放整体强度 + + // 按 colorType 在屏幕上铺开颜色 + float4 SampleFilmColor(float2 uv) + { + if (_ColorType == 1) // Color2TopBottom + return lerp(_Color1, _Color0, saturate(uv.y)); + if (_ColorType == 2) // Color2LeftRight + return lerp(_Color0, _Color1, saturate(uv.x)); + if (_ColorType == 3) // Color4:双线性四角插值 + { + float4 bottom = lerp(_Color2, _Color3, saturate(uv.x)); + float4 top = lerp(_Color0, _Color1, saturate(uv.x)); + return lerp(bottom, top, saturate(uv.y)); + } + return _Color0; // ColorAll + } + + float4 Frag(Varyings input) : SV_Target + { + UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input); + float2 uv = input.texcoord; + + // _BlitTexture 是 TEXTURE2D_X,必须用 _X 版本采样 + float4 src = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv); + if (_FilmMode == 0 || _FilmPower <= 0.0) + return src; + + // 颜色采样用的 UV 支持缩放/偏移/旋转(roll) + float2 cuv = uv - 0.5; + float s, c; + sincos(_RollAngle, s, c); + cuv = float2(cuv.x * c - cuv.y * s, cuv.x * s + cuv.y * c); + float2 scale = (abs(_FilmScale.x) < 1e-5 || abs(_FilmScale.y) < 1e-5) + ? float2(1.0, 1.0) : _FilmScale; + cuv = cuv / scale + 0.5 + _FilmOffset; + + float4 film = SampleFilmColor(cuv); + float power = _FilmPower; + + // Vignette 变体:从画面边缘往内衰减。 + // _FilmOption.x = 作用宽度(离边缘多远内为满强度),.y = 过渡柔和度。 + // 依据:song 1177 的实际取值为 (0,0.05) (0.05,0.05) (0.08,0.05) (0.2,0.05) + // (0.04,0.17) (0.1,0.17) (0.2,0.25)。y= 4 && _FilmMode <= 6) + { + // m 归一化到 0(边缘)~1(中心),这样 _FilmOption.x 才是「占半屏的比例」。 + // 之前漏了这一步:m 的原始范围是 0~0.5,等于把作用宽度放大了一倍。 + float2 e = min(uv, 1.0 - uv); + float m = min(e.x, e.y) * 2.0; + float start = max(_FilmOption.x, 0.0) * _VignetteWidth; + float soft = max(_FilmOption.y * _VignetteWidth, 1e-4); + power *= (1.0 - smoothstep(start, start + soft, m)) * _VignetteStrength; + } + + float3 rgb; + int mode = _FilmMode; + if (mode == 7) // Monochrome + { + float lum = dot(src.rgb, float3(0.299, 0.587, 0.114)); + rgb = lerp(src.rgb, lum * film.rgb, power); + } + else if (mode == 2 || mode == 5) // Add / VignetteAdd + { + rgb = src.rgb + film.rgb * film.a * power; + } + else if (mode == 3 || mode == 6) // Mul / VignetteMul + { + rgb = lerp(src.rgb, src.rgb * film.rgb, film.a * power); + } + else // Lerp / VignetteLerp + { + rgb = lerp(src.rgb, film.rgb, film.a * power); + } + + return float4(rgb, src.a); + } + ENDHLSL + } + } + Fallback Off +} diff --git a/Assets/Resources/Shaders/PostFilm.shader.meta b/Assets/Resources/Shaders/PostFilm.shader.meta new file mode 100644 index 0000000..c5184d4 --- /dev/null +++ b/Assets/Resources/Shaders/PostFilm.shader.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 38174debbaebae343a07ad1ae46e1038 +ShaderImporter: + externalObjects: {} + defaultTextures: [] + nonModifiableTextures: [] + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineControl.cs b/Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineControl.cs index 59935c5..34d6691 100644 --- a/Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineControl.cs +++ b/Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineControl.cs @@ -58,6 +58,8 @@ namespace Gallop.Live.Cutt public event BgColor2UpdateInfoDelegate OnUpdateBgColor2; + public event PostFilmUpdateInfoDelegate OnUpdatePostFilm; + public event TransformUpdateInfoDelegate OnUpdateTransform; public event ObjectUpdateInfoDelegate OnUpdateObject; @@ -124,6 +126,13 @@ namespace Gallop.Live.Cutt public Dictionary StageObjectMap = new Dictionary(); + /// + /// Object/Transform 轨道的组名不一定是 GameObject 名,也可能是 StageController + /// 的 _stageObjectUnits 单元名。例如 live10149 的 'neonsign' 就是一个 unit, + /// 底下挂着 glow_neonsign / wash_neonsign_a / wash_neonsign_b 三个子对象。 + /// + public Dictionary StageObjectUnitMap = new Dictionary(); + public float currentLiveTime { get @@ -371,6 +380,9 @@ namespace Gallop.Live.Cutt AlterUpdate_SimpleListControl(workSheet.chromaticAberrationList, d => d.keys, OnUpdateChromaticAberration, _currentFrame); AlterUpdate_SimpleListControl(workSheet.hdrBloomKeys, d => d.keys, OnUpdateHdrBloom, _currentFrame); AlterUpdate_SimpleListControl(workSheet.colorCorrectionDataLists, d => d.keys, OnUpdateColorCorrection, _currentFrame); + AlterUpdate_PostFilm(workSheet.postFilmKeys, 0, _currentFrame); + AlterUpdate_PostFilm(workSheet.postFilm2Keys, 1, _currentFrame); + AlterUpdate_PostFilm(workSheet.postFilm3Keys, 2, _currentFrame); _isNowAlterUpdate = false; @@ -1721,22 +1733,24 @@ namespace Gallop.Live.Cutt } } - private HashSet validBgColorNames = new HashSet { "CharaCenter", "CharaLeft", "CharaRight", "CharaColor" }; + /// BgColor1 里指向角色的组名;其余组名都是舞台物件,由 StageObjectMap 解析。 + public static readonly HashSet CharaBgColorNames = + new HashSet { "CharaCenter", "CharaLeft", "CharaRight", "CharaColor" }; private void AlterUpdate_BgColor1(LiveTimelineWorkSheet sheet, float currentFrame) { int count = sheet.bgColor1List.Count; for (int i = 0; i < count; i++) { - LiveTimelineKeyBgColor1DataList keys = sheet.bgColor1List[i].keys; + var groupData = sheet.bgColor1List[i]; + LiveTimelineKeyBgColor1DataList keys = groupData.keys; if (keys.HasAttribute(LiveTimelineKeyDataListAttr.Disable) || !keys.EnablePlayModeTimeline(_playMode)) { continue; } - else if (!validBgColorNames.Contains(sheet.bgColor1List[i].name)) - { - continue; - } + // 这里原本只放行 validBgColorNames(4 个角色组),把舞台物件组全丢了。 + // 实测一首歌 20 组 / 1239 keys 里有 17 组是舞台物件,约 92% 的数据被丢弃。 + // 现在全部下发,由 Director 按 TimelineName 分流到角色 or 舞台。 FindTimelineKey(out var curKey, out var nextKey, keys, currentFrame); if (curKey == null) { @@ -1745,6 +1759,24 @@ namespace Gallop.Live.Cutt BgColor1UpdateInfo updateInfo = default; LiveTimelineKeyBgColor1Data bgColorData = curKey as LiveTimelineKeyBgColor1Data; LiveTimelineKeyBgColor1Data bgColorData2 = nextKey as LiveTimelineKeyBgColor1Data; + + updateInfo.TimelineName = groupData.name; + updateInfo.TimelineNameHash = string.IsNullOrEmpty(groupData.name) ? 0 : Animator.StringToHash(groupData.name); + updateInfo.TargetCharaIdArray = groupData.TargetCharaIdArray; + updateInfo.TargetDressIdArray = groupData.TargetDressIdArray; + + // 以下字段此前从未填充,handler 拿到的一直是 default。 + updateInfo.colorPower = bgColorData.power; + updateInfo.scale = bgColorData.scale; + updateInfo.vertexColorToonPower = bgColorData.vertexColorToonPower; + updateInfo.outlineWidthPower = bgColorData.outlineWidthPower; + updateInfo.LightBlendMode = bgColorData.LightBlendMode; + updateInfo.IsSilhouette = bgColorData.IsSilhouette; + updateInfo.IsProjector = bgColorData.IsProjector; + updateInfo.CurrentColorType = bgColorData.ColorType; + updateInfo.CurrentColor = bgColorData.color; + updateInfo.CurrentFlags = bgColorData.flags; + if (bgColorData2 != null && bgColorData2.interpolateType != 0) { float t = CalculateInterpolationValue(bgColorData, bgColorData2, currentFrame); @@ -1770,6 +1802,67 @@ namespace Gallop.Live.Cutt } } + /// + /// PostFilm (39)。postFilmKeys 是直接的 key 列表(不是分组列表), + /// 所以不能走 AlterUpdate_SimpleListControl,得自己找 key 和插值。 + /// + private void AlterUpdate_PostFilm(LiveTimelineKeyPostFilmDataList keys, int layerIndex, float currentFrame) + { + if (OnUpdatePostFilm == null || keys == null || keys.Count == 0) return; + if (keys.HasAttribute(LiveTimelineKeyDataListAttr.Disable) || !keys.EnablePlayModeTimeline(_playMode)) + return; + + FindTimelineKey(out var curKey, out var nextKey, keys, currentFrame); + var cur = curKey as LiveTimelineKeyPostFilmData; + if (cur == null) return; + var next = nextKey as LiveTimelineKeyPostFilmData; + + PostFilmUpdateInfo info = default; + info.layerIndex = layerIndex; + info.filmMode = cur.filmMode; + info.colorType = cur.colorType; + info.layerMode = cur.layerMode; + info.colorBlend = cur.colorBlend; + info.movieResId = cur.movieResId; + info.movieFrameOffset = cur.movieFrameOffset; + info.movieSpeed = cur.movieSpeed; + info.enable = cur.filmMode != PostFilmMode.None; + + if (next != null && next.interpolateType != 0) + { + float t = CalculateInterpolationValue(cur, next, currentFrame); + info.filmPower = Mathf.Lerp(cur.filmPower, next.filmPower, t); + info.color0 = Color.Lerp(cur.color0, next.color0, t); + info.color1 = Color.Lerp(cur.color1, next.color1, t); + info.color2 = Color.Lerp(cur.color2, next.color2, t); + info.color3 = Color.Lerp(cur.color3, next.color3, t); + info.filmOffsetParam = Vector2.Lerp(cur.filmOffsetParam, next.filmOffsetParam, t); + info.filmOptionParam = Vector4.Lerp(cur.filmOptionParam, next.filmOptionParam, t); + info.filmScale = Vector2.Lerp(cur.FilmScale, next.FilmScale, t); + info.rollAngle = Mathf.Lerp(cur.RollAngle, next.RollAngle, t); + info.depthPower = Mathf.Lerp(cur.depthPower, next.depthPower, t); + info.depthClip = Mathf.Lerp(cur.DepthClip, next.DepthClip, t); + info.colorBlendFactor = Mathf.Lerp(cur.colorBlendFactor, next.colorBlendFactor, t); + } + else + { + info.filmPower = cur.filmPower; + info.color0 = cur.color0; + info.color1 = cur.color1; + info.color2 = cur.color2; + info.color3 = cur.color3; + info.filmOffsetParam = cur.filmOffsetParam; + info.filmOptionParam = cur.filmOptionParam; + info.filmScale = cur.FilmScale; + info.rollAngle = cur.RollAngle; + info.depthPower = cur.depthPower; + info.depthClip = cur.DepthClip; + info.colorBlendFactor = cur.colorBlendFactor; + } + + OnUpdatePostFilm.Invoke(ref info); + } + private void AlterUpdate_BgColor2(LiveTimelineWorkSheet sheet, float currentFrame) { if (OnUpdateBgColor2 == null) return; @@ -1847,7 +1940,8 @@ namespace Gallop.Live.Cutt { continue; } - else if (!StageObjectMap.ContainsKey(sheet.objectList[i].name)) + else if (!StageObjectMap.ContainsKey(sheet.objectList[i].name) && + !StageObjectUnitMap.ContainsKey(sheet.objectList[i].name)) { continue; } diff --git a/Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineDataList/LiveTimelineKeyPostFilmDataList.cs b/Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineDataList/LiveTimelineKeyPostFilmDataList.cs index 11abfe8..3cca41c 100644 --- a/Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineDataList/LiveTimelineKeyPostFilmDataList.cs +++ b/Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineDataList/LiveTimelineKeyPostFilmDataList.cs @@ -53,17 +53,33 @@ namespace Gallop.Live.Cutt public float DepthClip; // 0x98 public float RollAngle; // 0x9C public Vector2 FilmScale; // 0xA0 - public LiveTimelineKeyLoopType loopType; // 0xA8 - public int loopCount; // 0xAC - public int loopExecutedCount; // 0xB0 - public int loopIntervalFrame; // 0xB4 - public bool isPasteLoopUnit; // 0xB8 - public bool isChangeLoopInterpolate; // 0xB9 - public LiveTimelineKeyPostFilmData.LayerMode layerMode; // 0xBC - public int movieResId; // 0xC0 - public int movieFrameOffset; // 0xC4 - public float movieSpeed; // 0xC8 - public LiveTimelineKeyPostFilmData.ColorBlend colorBlend; // 0xCC - public float colorBlendFactor; // 0xD0 + + // 以下顺序按 bundle TypeTree 的权威顺序排列 + // (tools/dump_cutt_typetree.py --tree postFilmKeys 1177)。 + // 原来的声明把 loop* 组提到了 layerMode 之前,且缺 BlinkLight* 五个字段。 + public LiveTimelineKeyPostFilmData.LayerMode layerMode; + public int movieResId; + public int movieFrameOffset; + public float movieSpeed; + public LiveTimelineKeyPostFilmData.ColorBlend colorBlend; + public float colorBlendFactor; + + public string BlinkLightName; + public int BlinkLightNameHash; + public int BlinkLightContainerIndex; + public float BlinkLightBrightnessPower; + public bool IsAdjustedBlinkLightColor; + + public LiveTimelineKeyLoopType loopType; + public int loopCount; + public int loopExecutedCount; + public int loopIntervalFrame; + public bool isPasteLoopUnit; + public bool isChangeLoopInterpolate; + } + + [System.Serializable] + public class LiveTimelineKeyPostFilmDataList : LiveTimelineKeyDataListTemplate + { } } diff --git a/Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineWorkSheet.cs b/Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineWorkSheet.cs index d6b380c..6e38a7c 100644 --- a/Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineWorkSheet.cs +++ b/Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineWorkSheet.cs @@ -944,6 +944,7 @@ namespace Gallop.Live.Cutt [SerializeField] public List lightShaftsKeysLine; [SerializeField] public List particleList; [SerializeField] public List particleGroupList; + // 字段名已用 A/B 探针验证过命中 bundle TypeTree(大写 W 正确,小写不填充)。 [SerializeField] public List WashLightList; [SerializeField] public List laserList; [SerializeField] public List blinkLightList; @@ -951,6 +952,13 @@ namespace Gallop.Live.Cutt [SerializeField] public List hdrBloomKeys; [SerializeField] public List colorCorrectionDataLists; + // PostFilm (39):全语料 ~20200 keys / 59 首,是未实现轨道里数据量最大的一块。 + // 旧扫描把它记成「58/58 首存在但全部 0 keyframe(空占位)」,实测是错的 + // (song 1177 分别有 100 / 161 / 87 keys)。字段名取自 bundle TypeTree。 + [SerializeField] public LiveTimelineKeyPostFilmDataList postFilmKeys; + [SerializeField] public LiveTimelineKeyPostFilmDataList postFilm2Keys; + [SerializeField] public LiveTimelineKeyPostFilmDataList postFilm3Keys; + /* //���ڿ��Ե���AB���ˣ���Ȼ���淢��ûʲô��...˵����ʲôʱ�����õ� private void Start() diff --git a/Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineWorksheetDiag.cs b/Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineWorksheetDiag.cs new file mode 100644 index 0000000..6d37931 --- /dev/null +++ b/Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineWorksheetDiag.cs @@ -0,0 +1,211 @@ +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using System.Text; +using UnityEngine; + +namespace Gallop.Live.Cutt +{ + /// + /// 临时诊断。当前用途:核对 WorkSheet 字段名是否命中 bundle 的 TypeTree。 + /// + /// Unity 从 AssetBundle 反序列化 ScriptableObject 时按字段名严格匹配(区分大小写)。 + /// 名字没命中的字段不会被赋值,保持 C# 默认值 null;名字命中但本曲没数据的, + /// 会被填成空列表。所以 null 与「空」是两件事: + /// NULL -> 字段名很可能拼错了,该轨道在所有歌里都收不到数据 + /// 空 -> 名字是对的,只是这首歌没有这条轨道 + /// + /// 结论出来后整个文件可删,调用点只有 Director.InitializeTimeline 一处。 + /// + public static class LiveTimelineWorksheetDiag + { + private const BindingFlags kPublicInstance = BindingFlags.Public | BindingFlags.Instance; + + public static void Dump(LiveTimelineData data) + { + if (data == null) + { + Debug.LogWarning("[WorksheetDiag] data == null"); + return; + } + if (data.worksheetList == null) + { + Debug.LogWarning("[WorksheetDiag] data.worksheetList == null"); + return; + } + + int sheetCount = data.worksheetList.Count; + Debug.Log($"[WorksheetDiag] ===== worksheetList.Count = {sheetCount} ====="); + + FieldInfo[] sheetFields = typeof(LiveTimelineWorkSheet).GetFields(kPublicInstance); + var nullFieldsAllSheets = new List(); + + for (int i = 0; i < sheetCount; i++) + { + LiveTimelineWorkSheet sheet = data.worksheetList[i]; + if (sheet == null) + { + Debug.Log($"[WorksheetDiag] [{i}] "); + continue; + } + + var populated = new List(); + var empty = new List(); + var nulls = new List(); + + foreach (FieldInfo f in sheetFields) + { + if (!IsTrackField(f.FieldType)) continue; + + object value; + try { value = f.GetValue(sheet); } + catch { continue; } + + if (value == null) + { + nulls.Add(f.Name); + nullFieldsAllSheets.Add($"[{i}].{f.Name}"); + continue; + } + + string detail = Describe(value); + if (detail == null) empty.Add(f.Name); + else populated.Add($"{f.Name}: {detail}"); + } + + var sb = new StringBuilder(); + sb.AppendLine($"[WorksheetDiag] [{i}] SheetType={sheet.SheetType} version='{sheet.version}' " + + $"targetCameraIndex={sheet.targetCameraIndex} TotalTimeLength={sheet.TotalTimeLength}"); + + sb.AppendLine($" --- 有数据 ({populated.Count}) ---"); + foreach (string s in populated) sb.AppendLine($" {s}"); + + sb.AppendLine($" --- 空,名字对但本曲无此轨道 ({empty.Count}) ---"); + sb.AppendLine($" {string.Join(", ", empty)}"); + + sb.AppendLine($" --- NULL,字段名未命中 TypeTree ({nulls.Count}) ---"); + sb.AppendLine($" {string.Join(", ", nulls)}"); + + Debug.Log(sb.ToString()); + } + + if (nullFieldsAllSheets.Count > 0) + { + Debug.LogWarning("[WorksheetDiag] 以下字段为 NULL,字段名很可能与 bundle 不匹配 -> " + + string.Join(", ", nullFieldsAllSheets)); + } + else + { + // 注意:Unity 加载 bundle 时会把所有 List 字段初始化为空列表, + // 所以「没有 NULL」并不能证明字段名都正确,只能靠 A/B 段判断。 + Debug.Log("[WorksheetDiag] 无 NULL 字段(但这不能证明名字都对,见 WashLight A/B 段)。"); + } + } + + /// + /// 字段名探针。新增一个轨道字段后,用它确认名字是否命中 bundle 的 TypeTree: + /// 名字对且本曲有数据 -> 「★ 有数据」;名字错 -> 恒为「空」。 + /// 拿不准大小写时,并列声明两个拼写各探一次即可(WashLightList 就是这么定下来的)。 + /// + public static void Probe(LiveTimelineWorkSheet sheet, string fieldName) + { + if (sheet == null) { Debug.LogWarning($"[WorksheetDiag] Probe({fieldName}): sheet == null"); return; } + + FieldInfo f = typeof(LiveTimelineWorkSheet).GetField(fieldName, kPublicInstance); + if (f == null) { Debug.LogWarning($"[WorksheetDiag] Probe: C# 里没有字段 '{fieldName}'"); return; } + + Debug.Log($"[WorksheetDiag] Probe {fieldName}: {DescribeOrEmpty(f.GetValue(sheet))}"); + } + + /// 区分 null、空列表、有数据三种状态。 + private static string DescribeOrEmpty(object value) + { + if (value == null) return "null"; + string detail = Describe(value); + if (detail != null) return "★ 有数据 -> " + detail; + return (value is IList list) ? $"空({list.Count} 组)" : "空"; + } + + /// 只关心时间轴轨道字段,跳过 version / targetCameraIndex 之类的普通字段。 + private static bool IsTrackField(System.Type type) + { + if (typeof(ILiveTimelineKeyDataList).IsAssignableFrom(type)) return true; + if (type.IsArray) return type.GetElementType().Name.StartsWith("LiveTimeline"); + if (type.IsGenericType) + { + System.Type[] args = type.GetGenericArguments(); + return args.Length == 1 && args[0].Name.StartsWith("LiveTimeline"); + } + return type.Name.StartsWith("LiveTimeline"); + } + + /// 返回该字段的关键帧摘要;没有任何关键帧时返回 null。 + private static string Describe(object value) + { + if (value == null) return null; + + if (value is ILiveTimelineKeyDataList keyList) + return keyList.Count > 0 ? $"{keyList.Count} keys" : null; + + if (value is IList list && !(value is string)) + { + int totalKeys = 0; + var entries = new List(); + + foreach (object element in list) + { + if (element == null) continue; + + int keys = CountKeys(element, 0); + if (keys <= 0) continue; + + totalKeys += keys; + string name = (element as ILiveTimelineGroupDataWithName)?.name; + entries.Add(string.IsNullOrEmpty(name) ? $"?({keys})" : $"{name}({keys})"); + } + + if (totalKeys == 0) return null; + return $"{entries.Count} 组 / {totalKeys} keys -> {string.Join(", ", entries)}"; + } + + int nested = CountKeys(value, 0); + return nested > 0 ? $"{nested} keys" : null; + } + + /// 递归统计对象里所有 ILiveTimelineKeyDataList 的关键帧总数。 + private static int CountKeys(object obj, int depth) + { + if (obj == null || depth > 2) return 0; + + if (obj is ILiveTimelineKeyDataList keyList) + return keyList.Count; + + System.Type type = obj.GetType(); + if (type.IsPrimitive || type.IsEnum || obj is string) return 0; + + int total = 0; + foreach (FieldInfo f in type.GetFields(kPublicInstance)) + { + object value; + try { value = f.GetValue(obj); } + catch { continue; } + if (value == null) continue; + + if (value is ILiveTimelineKeyDataList nestedList) + { + total += nestedList.Count; + } + else if (value is IList inner && !(value is string)) + { + foreach (object element in inner) + total += CountKeys(element, depth + 1); + } + else if (!f.FieldType.IsPrimitive && !f.FieldType.IsEnum && f.FieldType != typeof(string)) + { + total += CountKeys(value, depth + 1); + } + } + return total; + } + } +} diff --git a/Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineWorksheetDiag.cs.meta b/Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineWorksheetDiag.cs.meta new file mode 100644 index 0000000..de455d8 --- /dev/null +++ b/Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineWorksheetDiag.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f7c7166ff241f1c4197a58591a0ef069 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/umamusume/Gallop/Live/Cutt/UpdateInfo/BgColorUpdateInfo.cs b/Assets/Scripts/umamusume/Gallop/Live/Cutt/UpdateInfo/BgColorUpdateInfo.cs index 36bbf0d..61cbe76 100644 --- a/Assets/Scripts/umamusume/Gallop/Live/Cutt/UpdateInfo/BgColorUpdateInfo.cs +++ b/Assets/Scripts/umamusume/Gallop/Live/Cutt/UpdateInfo/BgColorUpdateInfo.cs @@ -9,6 +9,8 @@ namespace Gallop.Live.Cutt public struct BgColor1UpdateInfo { // Fields + // TimelineName 不在原版结构里,是为了按名字路由到舞台物件而加的(原版只传 hash)。 + public string TimelineName; public int TimelineNameHash; // 0x10 public int[] TargetCharaIdArray; // 0x18 public int[] TargetDressIdArray; // 0x20 diff --git a/Assets/Scripts/umamusume/Gallop/Live/Cutt/UpdateInfo/PostFilmUpdateInfo.cs b/Assets/Scripts/umamusume/Gallop/Live/Cutt/UpdateInfo/PostFilmUpdateInfo.cs new file mode 100644 index 0000000..30cdbd7 --- /dev/null +++ b/Assets/Scripts/umamusume/Gallop/Live/Cutt/UpdateInfo/PostFilmUpdateInfo.cs @@ -0,0 +1,43 @@ +using UnityEngine; + +namespace Gallop.Live.Cutt +{ + /// + /// PostFilm (39) 的每帧结果。三条轨道 postFilmKeys / postFilm2Keys / postFilm3Keys + /// 各自独立叠加,用 layerIndex 区分(0/1/2)。 + /// + public struct PostFilmUpdateInfo + { + public int layerIndex; + public bool enable; + + public PostFilmMode filmMode; + public PostColorType colorType; + public float filmPower; + + public Color color0; + public Color color1; + public Color color2; + public Color color3; + + public Vector2 filmOffsetParam; + public Vector4 filmOptionParam; + public Vector2 filmScale; + public float rollAngle; + + public float depthPower; + public float depthClip; + + public LiveTimelineKeyPostFilmData.LayerMode layerMode; + public LiveTimelineKeyPostFilmData.ColorBlend colorBlend; + public float colorBlendFactor; + + // layerMode == UVMovie 时用;内容资源是 live/uvmovie/gal_uvmovie__。 + // 寻址规则尚未确定,先透传。 + public int movieResId; + public int movieFrameOffset; + public float movieSpeed; + } + + public delegate void PostFilmUpdateInfoDelegate(ref PostFilmUpdateInfo updateInfo); +} diff --git a/Assets/Scripts/umamusume/Gallop/Live/Cutt/UpdateInfo/PostFilmUpdateInfo.cs.meta b/Assets/Scripts/umamusume/Gallop/Live/Cutt/UpdateInfo/PostFilmUpdateInfo.cs.meta new file mode 100644 index 0000000..c94421e --- /dev/null +++ b/Assets/Scripts/umamusume/Gallop/Live/Cutt/UpdateInfo/PostFilmUpdateInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b7dcf9b3ef7cb374fa8054e51711a9e3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/umamusume/Gallop/Live/Director.cs b/Assets/Scripts/umamusume/Gallop/Live/Director.cs index 4a2dca8..470a91a 100644 --- a/Assets/Scripts/umamusume/Gallop/Live/Director.cs +++ b/Assets/Scripts/umamusume/Gallop/Live/Director.cs @@ -118,6 +118,7 @@ namespace Gallop.Live Debug.Log(live.BackGroundId); Builder.LoadAssetPath(string.Format(STAGE_PATH, live.BackGroundId), transform); _liveTimelineControl.StageObjectMap = _stageController.StageObjectMap; + _liveTimelineControl.StageObjectUnitMap = _stageController.StageObjectUnitMap; } //Make CharacterObject @@ -172,6 +173,9 @@ namespace Gallop.Live _uvScrollAccum.Clear(); totalTime = _liveTimelineControl.data.timeLength; + // 临时诊断:确认是否有 worksheet[1..] 携带轨道数据。结论出来后连同 LiveTimelineWorksheetDiag.cs 一起删。 + LiveTimelineWorksheetDiag.Dump(_liveTimelineControl.data); + liveMode = mode; allowCount = characters.Count; @@ -225,6 +229,8 @@ namespace Gallop.Live _liveTimelineControl.OnUpdateChromaticAberration += OnChromaticAberrationUpdate; _liveTimelineControl.OnUpdateHdrBloom += OnHdrBloomUpdate; _liveTimelineControl.OnUpdateColorCorrection += OnColorCorrectionUpdate; + _liveTimelineControl.OnUpdatePostFilm += OnPostFilmUpdate; + PostFilmRendererFeature.ResetLayers(); // 获取或创建摄像机上的 Volume 组件,供后处理 handler 使用 var mainCam = Camera.main; @@ -365,8 +371,39 @@ namespace Gallop.Live } } + // BgColor1 在舞台物件上对应哪个 shader 属性尚未确认,按存在性依次尝试。 + // 首次解析每个轨道组时会打一条日志,说明命中了什么以及材质上有哪些候选属性。 + // 舞台 shader 的染色通道。运行时枚举 shader 属性表实测: + // Gallop/3D/Live/Stage/DefaultNoAmbient -> _MulColor0 + // Gallop/3D/Live/Stage/DefaultEnvMapNoAmbient -> _MulColor0, _AddColor + // Gallop/3D/Live/Stage/LightBlinkBlend -> _BlinkLightColor(BlinkLight 轨道的,不归 BgColor1) + // Gallop/3D/Live/Stage/StageTransmittedLightMask -> 无 + // 和 WashLight 的 MulColor0 / UVScrollLight 的 mulColor1 是同一套命名体系。 + // 刻意不含 _AmbientColor:那个通道归 BgColor2(OnBgColor2Update),两条轨道不该抢同一个属性。 + private static readonly string[] kStageBgColor1Props = { "_MulColor0" }; + + /// 解析结果:目标 Renderer + 它实际拥有的那个颜色属性。 + private struct StageBgColorTarget + { + public Renderer renderer; + public string prop; + public bool hasColorPower; + } + + private readonly Dictionary> _bgColor1StageCache = + new Dictionary>(); + + private MaterialPropertyBlock _bgColor1Block; + private void OnBgColor1Update(ref BgColor1UpdateInfo updateInfo) { + if (!string.IsNullOrEmpty(updateInfo.TimelineName) && + !LiveTimelineControl.CharaBgColorNames.Contains(updateInfo.TimelineName)) + { + ApplyBgColor1ToStage(ref updateInfo); + return; + } + foreach (var locator in _liveTimelineControl.liveCharactorLocators) { var EFlags = (LiveCharaPositionFlag)updateInfo.flags; @@ -384,6 +421,238 @@ namespace Gallop.Live } } + /// + /// BgColor1 的舞台物件分支。轨道组名可能指向 GameObject,也可能指向材质名 + /// (uvScrollLightList 用的就是材质名),所以两种都试。 + /// + private void ApplyBgColor1ToStage(ref BgColor1UpdateInfo updateInfo) + { + if (_stageController == null) return; + + string key = updateInfo.TimelineName; + if (!_bgColor1StageCache.TryGetValue(key, out var targets)) + { + targets = ResolveStageTargets(key); + _bgColor1StageCache[key] = targets; + LogBgColor1Resolution(key, targets); + } + if (targets.Count == 0) return; + + _bgColor1Block ??= new MaterialPropertyBlock(); + + foreach (var t in targets) + { + if (t.renderer == null) continue; + t.renderer.GetPropertyBlock(_bgColor1Block); + _bgColor1Block.SetColor(t.prop, updateInfo.color); + + // ⚠ 不要在这里写 _ColorPower —— 缺 ground truth。 + // 已核实的只有「这些 shader 上 _ColorPower 这个 Float 属性存在」; + // **没有任何证据表明 BgColor1 的 power 字段映射到它**,它也可能是乘在 + // 材质原值上、或者对应完全不同的东西。2026-08-05 试写过一版,无法判断对错, + // 遂按「缺依据即不做」撤回。t.hasColorPower 保留,供将来确认后启用。 + // + // (注:舞台地板 plane_000 / stage_object_001 / specular_002 长期偏亮发白 + // 是**早于本改动就存在**的问题,与 _ColorPower 无关,另行排查。) + + t.renderer.SetPropertyBlock(_bgColor1Block); + } + } + + /// + /// 枚举 shader 真正声明的 Color 属性。 + /// 注意:材质在 bundle 里的 m_SavedProperties 会保留历史属性,和当前 shader 声明的不是一回事, + /// 判断能写什么必须问 shader,不能看材质存档表。 + /// + private static string DescribeShaderColors(Shader sh) + { + if (sh == null) return ""; + + var colors = new List(); + int count = sh.GetPropertyCount(); + for (int i = 0; i < count; i++) + { + if (sh.GetPropertyType(i) == ShaderPropertyType.Color) + colors.Add(sh.GetPropertyName(i)); + } + return $"{sh.name} 颜色属性: {(colors.Count > 0 ? string.Join(",", colors) : "<无>")}"; + } + + /// 用 sharedMaterials 探测(不会实例化材质),只保留确实有候选属性的 Renderer。 + private List ResolveStageTargets(string timelineName) + { + var result = new List(); + + _bgColor1FoundObject = false; + _bgColor1ObjCount = 0; + _bgColor1RendererCount = 0; + _bgColor1MatInfo.Clear(); + + // 按 Transform 名遍历整个舞台层级。 + // 不能只查 StageObjectMap:它按名字去重,而观众群里同名对象有几十上百个 + // (mob_a000 实测 66 个),只取第一个会漏掉绝大多数。 + // StageObjectMap 里的对象本身也在这个层级里,所以这一趟已经覆盖它。 + var seen = new HashSet(); + var byName = new List(); + foreach (var tr in _stageController.GetComponentsInChildren(true)) + { + if (!string.Equals(tr.name.Replace("(Clone)", ""), timelineName, StringComparison.OrdinalIgnoreCase)) + continue; + _bgColor1FoundObject = true; + _bgColor1ObjCount++; + foreach (var r in tr.GetComponentsInChildren(true)) + if (r != null && seen.Add(r)) byName.Add(r); + } + + // StageObjectMap 兜底:万一有对象不在 _stageController 层级下。 + if (_stageController.StageObjectMap.TryGetValue(timelineName, out var go) && go != null) + { + _bgColor1FoundObject = true; + foreach (var r in go.GetComponentsInChildren(true)) + if (r != null && seen.Add(r)) byName.Add(r); + } + + _bgColor1RendererCount = byName.Count; + foreach (var r in byName) + { + if (r == null) continue; + foreach (var mat in r.sharedMaterials) + { + if (mat == null) { _bgColor1MatInfo.Add(""); continue; } + string info = $"{mat.name}[{DescribeShaderColors(mat.shader)}]"; + if (!_bgColor1MatInfo.Contains(info)) _bgColor1MatInfo.Add(info); + } + } + if (byName.Count > 0) + { + CollectTargets(byName, result); + if (result.Count > 0) return result; + } + + // 退回按材质名匹配(uvScrollLightList 用的就是材质名) + var matched = new List(); + foreach (var r in _stageController.GetComponentsInChildren(true)) + { + foreach (var mat in r.sharedMaterials) + { + if (mat == null) continue; + if (mat.name.IndexOf(timelineName, StringComparison.OrdinalIgnoreCase) >= 0) + { + _bgColor1FoundObject = true; + matched.Add(r); + break; + } + } + } + CollectTargets(matched, result); + return result; + } + + // ResolveStageTargets 的副产物,用于把失败原因拆干净: + // 找到几个同名对象 / 它们下面有几个 Renderer / 这些 Renderer 用的材质和 shader。 + private bool _bgColor1FoundObject; + private int _bgColor1ObjCount; + private int _bgColor1RendererCount; + private readonly List _bgColor1MatInfo = new List(); + + private static void CollectTargets(IEnumerable renderers, List result) + { + foreach (var r in renderers) + { + if (r == null) continue; + foreach (var mat in r.sharedMaterials) + { + if (mat == null) continue; + string hit = null; + foreach (string p in kStageBgColor1Props) + { + if (mat.HasProperty(p)) { hit = p; break; } + } + if (hit != null) + { + result.Add(new StageBgColorTarget + { + renderer = r, + prop = hit, + hasColorPower = mat.HasProperty(kColorPowerProp), + }); + break; + } + } + } + } + + private void LogBgColor1Resolution(string timelineName, List targets) + { + if (targets.Count == 0) + { + if (!_bgColor1FoundObject) + Debug.LogWarning($"[BgColor1] '{timelineName}':整个舞台层级里找不到同名对象,材质名也匹配不上"); + else if (_bgColor1RendererCount == 0) + Debug.LogWarning($"[BgColor1] '{timelineName}':找到 {_bgColor1ObjCount} 个同名对象,但它们下面没有任何 Renderer"); + else + Debug.LogWarning($"[BgColor1] '{timelineName}':找到 {_bgColor1ObjCount} 个对象 / {_bgColor1RendererCount} 个 Renderer," + + $"但材质无候选属性。材质[shader]: {string.Join(" | ", _bgColor1MatInfo)}"); + return; + } + + var props = new HashSet(); + foreach (var t in targets) props.Add(t.prop); + + // 成功分支也打印 shader 真实声明的颜色属性,便于核对写对了通道。 + var shaders = new List(); + foreach (var t in targets) + foreach (var mat in t.renderer.sharedMaterials) + { + if (mat == null) continue; + string info = DescribeShaderColors(mat.shader); + if (!shaders.Contains(info)) shaders.Add(info); + } + + Debug.Log($"[BgColor1] '{timelineName}' -> {targets.Count} 个 Renderer,写入 {string.Join(",", props)};" + + $"{string.Join(" | ", shaders)}"); + } + + /// + /// PostFilm (39)。三条轨道各占一层,参数写进 PostFilmRendererFeature.Layers, + /// 由 RendererFeature 在 AfterRenderingPostProcessing 做全屏叠加。 + /// 该 Feature 必须先加进 UMAUniversalRenderPipelineAsset_Renderer.asset 才会生效。 + /// + private void OnPostFilmUpdate(ref PostFilmUpdateInfo info) + { + int i = info.layerIndex; + if (i < 0 || i >= PostFilmRendererFeature.kLayerCount) return; + + PostFilmRendererFeature.Layers[i] = new PostFilmRendererFeature.LayerState + { + enable = info.enable, + filmMode = info.filmMode, + colorType = info.colorType, + filmPower = info.filmPower, + color0 = info.color0, + color1 = info.color1, + color2 = info.color2, + color3 = info.color3, + filmOffset = info.filmOffsetParam, + filmScale = info.filmScale, + rollAngle = info.rollAngle, + filmOption = info.filmOptionParam, + }; + + LogPostFilmOnce(i, ref info); + } + + private readonly HashSet _postFilmLogged = new HashSet(); + + private void LogPostFilmOnce(int layer, ref PostFilmUpdateInfo info) + { + if (!_postFilmLogged.Add(layer)) return; + Debug.Log($"[PostFilm] layer{layer} filmMode={info.filmMode} colorType={info.colorType} " + + $"power={info.filmPower:F3} layerMode={info.layerMode} colorBlend={info.colorBlend} " + + $"movieResId={info.movieResId} color0={info.color0} scale={info.filmScale} " + + $"offset={info.filmOffsetParam} roll={info.rollAngle:F3} option={info.filmOptionParam}"); + } + private void OnCameraSwitcherUpdate(int cameraIndex_) { if (cameraIndex_ < 0) @@ -515,35 +784,128 @@ namespace Gallop.Live var renderers = go.GetComponentsInChildren(true); if (renderers.Length == 0) return; - if (keyData.pattern == 0) - { - for (int i = 0; i < renderers.Length; i++) - { - int idx = i < keyData.powerArray?.Length ? i : 0; - float power = keyData.powerArray != null && keyData.powerArray.Length > idx ? keyData.powerArray[idx] : 1f; - Color col = keyData.color0Array != null && keyData.color0Array.Length > idx ? keyData.color0Array[idx] : Color.white; - foreach (var mat in renderers[i].materials) - { - mat.SetColor("_Color", col); - mat.SetFloat("_ColorPower", power); - } - } - return; - } + _blinkLightBlock ??= new MaterialPropertyBlock(); + int noProp = 0; + + // color0Array/powerArray 恒为 10 项,是「调色板」而不是每盏灯一项 + // (实测:renderer 数可达 570,数组仍是 10;wash_truss_a 的前 4 项正好等于本曲成员数)。 + // 每盏灯到调色板槽位的映射规则尚未逆出来,暂统一取第 0 槽 —— 对灯牌是正确的 + // (它 6 个有效槽同色),对多色组(wash/mob)是简化。 + Color baseCol = (keyData.color0Array != null && keyData.color0Array.Length > 0) + ? keyData.color0Array[0] : Color.white; + float basePower = (keyData.powerArray != null && keyData.powerArray.Length > 0) + ? keyData.powerArray[0] : 1f; + + float baseElapsed = _liveTimelineControl.currentLiveTime - keyData.frame / 60f - keyData.waitTime; + float cycle = keyData.turnOnTime + keyData.keepTime + keyData.turnOffTime + keyData.intervalTime; - // Blink: 按时间周期计算强度 - float elapsed = _liveTimelineControl.currentLiveTime - keyData.frame / 60f - keyData.waitTime; - float intensity = ComputeBlinkIntensity(keyData, elapsed); for (int i = 0; i < renderers.Length; i++) { - Color col = keyData.color0Array != null && i < keyData.color0Array.Length ? keyData.color0Array[i] : Color.white; - foreach (var mat in renderers[i].materials) + var r = renderers[i]; + if (r == null) continue; + + Color col = baseCol; + float power = basePower; + + if (keyData.pattern != 0) { - mat.SetColor("_Color", col * intensity); - mat.SetFloat("_ColorPower", intensity); + // pattern != 0 时逐灯错开相位,做出滚动闪烁(U 闪 → M 闪 → A 闪)。 + // pattern 的确切语义还没逆出来,这里只区分「同步」与「滚动」两种; + // 若方向或速度不对,调这里的相位公式即可。 + float phase = (cycle > 0f && renderers.Length > 1) + ? cycle * i / renderers.Length + : 0f; + power *= ComputeBlinkIntensity(keyData, baseElapsed - phase); + } + + // 这些灯的 shader 是 Gallop/3D/Live/Stage/LightBlinkBlend,唯一的 Color 属性是 + // _BlinkLightColor。之前写的是 _Color —— 该 shader 上没有这个属性,写入是空操作, + // 于是 _BlinkLightColor 一直是默认值,灯全渲染成黑块。 + BlinkTarget t = ResolveBlinkTarget(r); + if (t.colorProp == null) { noProp++; continue; } + + r.GetPropertyBlock(_blinkLightBlock); + if (t.hasColorPower) + { + // shader 自带独立的亮度通道,颜色和强度分开写,合成交给 shader。 + _blinkLightBlock.SetColor(t.colorProp, col); + _blinkLightBlock.SetFloat(kColorPowerProp, power); + } + else + { + // BgMirrorBall 这类只有 _MulColor0、没有 _ColorPower,只能把强度乘进颜色。 + _blinkLightBlock.SetColor(t.colorProp, col * power); + } + r.SetPropertyBlock(_blinkLightBlock); + } + LogBlinkLightOnce(data.name, keyData, renderers, noProp); + // TODO: 调色板槽位映射、color1Array、LightBlendMode、isReverseHueArray 尚未实现。 + } + + private MaterialPropertyBlock _blinkLightBlock; + + // 舞台灯光 shader 的通道(枚举 shader 属性表实测,见 CLAUDE.md): + // LightBlinkBlend _BlinkLightColor + _ColorPower + // DefaultNoAmbient _MulColor0 + _ColorPower + // DefaultEnvMapNoAmbient _MulColor0 + _ColorPower (+_AddColor) + // DefaultTransparentNoAmbient _MulColor0 + _ColorPower (+_AmbientColor) + // BgMirrorBall _MulColor0 (无 _ColorPower) + // StageMirrorBallShine / StageTransmittedLightMask 完全没有颜色属性 + private static readonly string[] kBlinkColorProps = { "_BlinkLightColor", "_MulColor0", "_Color" }; + private const string kColorPowerProp = "_ColorPower"; + + private struct BlinkTarget + { + public string colorProp; + public bool hasColorPower; + } + + private readonly Dictionary _blinkPropCache = new Dictionary(); + private readonly HashSet _blinkLoggedGroups = new HashSet(); + + private BlinkTarget ResolveBlinkTarget(Renderer r) + { + if (_blinkPropCache.TryGetValue(r, out BlinkTarget cached)) return cached; + + BlinkTarget t = default; + foreach (var mat in r.sharedMaterials) + { + if (mat == null) continue; + foreach (string p in kBlinkColorProps) + { + if (mat.HasProperty(p)) { t.colorProp = p; break; } + } + if (t.colorProp != null) + { + t.hasColorPower = mat.HasProperty(kColorPowerProp); + break; } } - // TODO: color1Array, LightBlendMode, isReverseHueArray, color blend fields unused + _blinkPropCache[r] = t; + return t; + } + + /// 每个 BlinkLight 组只打一次:pattern / 数组长度 / renderer 数 / shader 属性,用于设计逐灯相位。 + private void LogBlinkLightOnce(string groupName, LiveTimelineKeyBlinkLightData k, Renderer[] renderers, int noProp) + { + if (!_blinkLoggedGroups.Add(groupName)) return; + + var shaders = new List(); + foreach (var r in renderers) + foreach (var mat in r.sharedMaterials) + { + if (mat == null) continue; + string info = DescribeShaderColors(mat.shader); + if (!shaders.Contains(info)) shaders.Add(info); + } + + Debug.Log($"[BlinkLight] '{groupName}' renderers={renderers.Length} 无可写颜色属性={noProp} " + + $"pattern={k.pattern} colorType={k.colorType} blendMode={k.LightBlendMode} " + + $"color0Array={k.color0Array?.Length ?? -1} color1Array={k.color1Array?.Length ?? -1} " + + $"powerArray={k.powerArray?.Length ?? -1} reverseHue={k.isReverseHueArray?.Length ?? -1} " + + $"power={k.powerMin}~{k.powerMax} loop={k.loopCount} " + + $"wait={k.waitTime} on={k.turnOnTime} keep={k.keepTime} off={k.turnOffTime} interval={k.intervalTime}; " + + $"{string.Join(" | ", shaders)}"); } private static float ComputeBlinkIntensity(LiveTimelineKeyBlinkLightData keyData, float elapsed) diff --git a/Assets/Scripts/umamusume/Gallop/Live/PostEffect.meta b/Assets/Scripts/umamusume/Gallop/Live/PostEffect.meta new file mode 100644 index 0000000..f701a28 --- /dev/null +++ b/Assets/Scripts/umamusume/Gallop/Live/PostEffect.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ecfe64f6a78444342a929112bd228b60 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/umamusume/Gallop/Live/PostEffect/PostFilmRendererFeature.cs b/Assets/Scripts/umamusume/Gallop/Live/PostEffect/PostFilmRendererFeature.cs new file mode 100644 index 0000000..3af5d7d --- /dev/null +++ b/Assets/Scripts/umamusume/Gallop/Live/PostEffect/PostFilmRendererFeature.cs @@ -0,0 +1,216 @@ +using System; +using UnityEngine; +using UnityEngine.Rendering; +using UnityEngine.Rendering.Universal; +using Gallop.Live.Cutt; + +namespace Gallop.Live +{ + /// + /// PostFilm (39) 的 URP 实现。全语料 ~20200 keys / 59 首,是数据量最大的未实现轨道。 + /// + /// 用法:把本 Feature 加到 Assets/Resources/RenderPipeline/UMAUniversalRenderPipelineAsset_Renderer.asset + /// 的 Renderer Features 列表里(一次性操作)。shader 从 Resources 载入,不需要在 Inspector 指派。 + /// + /// Director 每帧通过静态的 Layers 写入参数;三条轨道 + /// postFilmKeys / postFilm2Keys / postFilm3Keys 各占一层,按 0→1→2 顺序叠加。 + /// + public class PostFilmRendererFeature : ScriptableRendererFeature + { + public const int kLayerCount = 3; + + [Serializable] + public struct LayerState + { + public bool enable; + public PostFilmMode filmMode; + public PostColorType colorType; + public float filmPower; + public Color color0, color1, color2, color3; + public Vector2 filmOffset; + public Vector2 filmScale; + public float rollAngle; + public Vector4 filmOption; + } + + /// Director 每帧写这里;Feature 只读。索引 = PostFilmUpdateInfo.layerIndex。 + public static readonly LayerState[] Layers = new LayerState[kLayerCount]; + + /// Live 结束时调用,避免参数残留到下一首歌。 + public static void ResetLayers() + { + for (int i = 0; i < kLayerCount; i++) Layers[i] = default; + } + + [SerializeField] private RenderPassEvent _passEvent = RenderPassEvent.AfterRenderingPostProcessing; + + // ─────────────────────────────────────────────────────────────────────── + // 默认关闭:渲染部分缺少 ground truth,开着会画错。 + // + // 已验证的部分(保留并启用):字段名/类型、三条轨道的关键帧解析、分发与插值、 + // filmMode / colorType 的枚举取值(song 1177 实测 100/161/87 keys)。 + // + // 未验证的部分(就是它导致必须关闭):filmOptionParam 的单位与几何含义。 + // song 1177 的 100 个 key 全是 Vignette 变体,取值形如 (0.2,0.05) (0,0.05) (0.2,0.25), + // 但「0.2」对应屏幕上多大范围无从得知——定义在游戏自己的 PostFilm shader 里, + // 而全库 499 个 shader(`shader` bundle)按名字和属性签名都搜过,**没有这个 shader**, + // 它应该编在 app 本体的 resources.assets 里,我们拿不到。 + // 没有它,vignette 衰减只能靠观感标定,猜了两版都不对(一版全屏泛色、一版大边框)。 + // + // 要继续做:对着参考视频找一帧 vignette 明显的画面,勾上下面的开关, + // 调 Width/Strength 直到吻合,然后把数值写死进代码并注明标定依据。 + // ─────────────────────────────────────────────────────────────────────── + [Header("⚠ 渲染部分未标定,默认关闭")] + [Tooltip("勾上才会实际绘制。filmOptionParam 的几何语义未知,开启后画面大概率不准。")] + [SerializeField] private bool _enableRendering = false; + + [Header("Vignette 标定(仅在上面勾选后有意义)")] + [Tooltip("作用宽度系数。filmOptionParam.x 乘以它后,1.0 = 从边缘一直吃到画面中心。调小 = 边框更窄。")] + [Range(0.02f, 1f)] + [SerializeField] private float _vignetteWidth = 0.25f; + + [Tooltip("整体强度系数。调小 = 边缘颜色更淡。")] + [Range(0f, 2f)] + [SerializeField] private float _vignetteStrength = 1f; + + private PostFilmPass _pass; + private Material _material; + + public override void Create() + { + if (_material == null) + { + Shader shader = Resources.Load("Shaders/PostFilm"); + if (shader == null) + { + Debug.LogError("[PostFilm] 找不到 Resources/Shaders/PostFilm,Feature 不会生效"); + return; + } + _material = CoreUtils.CreateEngineMaterial(shader); + } + _pass = new PostFilmPass(_material) { renderPassEvent = _passEvent }; + _pass.SetCalibration(_vignetteWidth, _vignetteStrength); + } + + public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) + { + // 渲染默认关闭,理由见字段声明处。数据层与分发层仍然照常跑, + // Director 的 [PostFilm] 日志不受影响,方便继续查参数。 + if (!_enableRendering) return; + if (_pass == null || _material == null) return; + // 每帧同步,Inspector 里拖动能立刻在 Play 模式下看到变化 + _pass.SetCalibration(_vignetteWidth, _vignetteStrength); + if (renderingData.cameraData.cameraType != CameraType.Game && + renderingData.cameraData.cameraType != CameraType.SceneView) return; + + bool anyActive = false; + for (int i = 0; i < kLayerCount; i++) + { + if (Layers[i].enable && Layers[i].filmMode != PostFilmMode.None && Layers[i].filmPower > 0f) + { + anyActive = true; + break; + } + } + if (!anyActive) return; + + renderer.EnqueuePass(_pass); + } + + protected override void Dispose(bool disposing) + { + _pass?.Dispose(); + CoreUtils.Destroy(_material); + _material = null; + } + + private class PostFilmPass : ScriptableRenderPass + { + private static readonly int kFilmMode = Shader.PropertyToID("_FilmMode"); + private static readonly int kColorType = Shader.PropertyToID("_ColorType"); + private static readonly int kFilmPower = Shader.PropertyToID("_FilmPower"); + private static readonly int kColor0 = Shader.PropertyToID("_Color0"); + private static readonly int kColor1 = Shader.PropertyToID("_Color1"); + private static readonly int kColor2 = Shader.PropertyToID("_Color2"); + private static readonly int kColor3 = Shader.PropertyToID("_Color3"); + private static readonly int kFilmOffset = Shader.PropertyToID("_FilmOffset"); + private static readonly int kFilmScale = Shader.PropertyToID("_FilmScale"); + private static readonly int kRollAngle = Shader.PropertyToID("_RollAngle"); + private static readonly int kFilmOption = Shader.PropertyToID("_FilmOption"); + + private static readonly int kVigWidth = Shader.PropertyToID("_VignetteWidth"); + private static readonly int kVigStrength = Shader.PropertyToID("_VignetteStrength"); + + private readonly Material _material; + private RTHandle _temp; + private float _vigWidth = 0.25f; + private float _vigStrength = 1f; + + public PostFilmPass(Material material) + { + _material = material; + profilingSampler = new ProfilingSampler("PostFilm"); + } + + public void SetCalibration(float width, float strength) + { + _vigWidth = width; + _vigStrength = strength; + } + + public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData) + { + var desc = renderingData.cameraData.cameraTargetDescriptor; + desc.depthBufferBits = 0; + desc.msaaSamples = 1; + RenderingUtils.ReAllocateIfNeeded(ref _temp, desc, FilterMode.Bilinear, + TextureWrapMode.Clamp, name: "_PostFilmTemp"); + } + + public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) + { + if (_material == null || _temp == null) return; + + RTHandle src = renderingData.cameraData.renderer.cameraColorTargetHandle; + if (src == null) return; + + CommandBuffer cmd = CommandBufferPool.Get(); + using (new ProfilingScope(cmd, profilingSampler)) + { + for (int i = 0; i < kLayerCount; i++) + { + LayerState s = Layers[i]; + if (!s.enable || s.filmMode == PostFilmMode.None || s.filmPower <= 0f) continue; + + _material.SetInt(kFilmMode, (int)s.filmMode); + _material.SetInt(kColorType, (int)s.colorType); + _material.SetFloat(kFilmPower, s.filmPower); + _material.SetColor(kColor0, s.color0); + _material.SetColor(kColor1, s.color1); + _material.SetColor(kColor2, s.color2); + _material.SetColor(kColor3, s.color3); + _material.SetVector(kFilmOffset, s.filmOffset); + _material.SetVector(kFilmScale, s.filmScale); + _material.SetFloat(kRollAngle, s.rollAngle); + _material.SetVector(kFilmOption, s.filmOption); + _material.SetFloat(kVigWidth, _vigWidth); + _material.SetFloat(kVigStrength, _vigStrength); + + // 不能把 RT 直接 blit 回自身,走一次临时 RT。 + Blitter.BlitCameraTexture(cmd, src, _temp, _material, 0); + Blitter.BlitCameraTexture(cmd, _temp, src); + } + } + context.ExecuteCommandBuffer(cmd); + cmd.Clear(); + CommandBufferPool.Release(cmd); + } + + public void Dispose() + { + _temp?.Release(); + _temp = null; + } + } + } +} diff --git a/Assets/Scripts/umamusume/Gallop/Live/PostEffect/PostFilmRendererFeature.cs.meta b/Assets/Scripts/umamusume/Gallop/Live/PostEffect/PostFilmRendererFeature.cs.meta new file mode 100644 index 0000000..c2edb03 --- /dev/null +++ b/Assets/Scripts/umamusume/Gallop/Live/PostEffect/PostFilmRendererFeature.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 61d001d58c2e5434590aaca747d7b830 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/umamusume/Gallop/Live/StageController.cs b/Assets/Scripts/umamusume/Gallop/Live/StageController.cs index 46072de..4de650b 100644 --- a/Assets/Scripts/umamusume/Gallop/Live/StageController.cs +++ b/Assets/Scripts/umamusume/Gallop/Live/StageController.cs @@ -83,6 +83,25 @@ namespace Gallop.Live { return; } + // 组名可能是 _stageObjectUnits 的单元名而不是 GameObject 名 + // (live10149 的 'neonsign' 就是 unit,底下挂 glow/wash_a/wash_b 三个子对象; + // 没有任何 GameObject 叫 neonsign,所以只查 StageObjectMap 会整条轨道失效, + // 灯牌就不会从吊顶降下来)。 + if (!StageObjectMap.ContainsKey(updateInfo.data.name) && + StageObjectUnitMap.TryGetValue(updateInfo.data.name, out StageObjectUnit unit)) + { + foreach (var child in unit.ChildObjects) + { + if (child == null) continue; + if (!StageObjectMap.TryGetValue(child.name.Replace("(Clone)", ""), out var childGo)) + childGo = child; + + childGo.SetActive(updateInfo.renderEnable); + ApplyObjectTrs(childGo.transform, ref updateInfo); + } + return; + } + if (StageObjectMap.TryGetValue(updateInfo.data.name, out GameObject gameObject)) { gameObject.SetActive(updateInfo.renderEnable); @@ -112,21 +131,52 @@ namespace Gallop.Live gameObject.transform.SetParent(attach_transform); } - if (updateInfo.data.enablePosition) - { - gameObject.transform.localPosition = updateInfo.updateData.position; - } - if (updateInfo.data.enableRotate) - { - gameObject.transform.localRotation = updateInfo.updateData.rotation; - } - if (updateInfo.data.enableScale) - { - gameObject.transform.localScale = updateInfo.updateData.scale; - } + ApplyObjectTrs(gameObject.transform, ref updateInfo); } } + /// 物件的原始 local TRS,Add 模式要在它之上叠加。首次触碰时懒记录。 + private readonly Dictionary _objectBaseTrs = + new Dictionary(); + + /// + /// 按 OffsetType 应用 TRS。 + /// Direct(0) = 绝对值;Add(1) = 相对物件原始 local TRS 的偏移。 + /// 之前一直无视这个字段,一律当绝对值写,于是 Add 语义的物件全被挪到父物体原点 + /// ——live10149 的 neonsign 灯牌就是这样掉到地上的(它的 y=12.58→0 是「抬高 12.58 开场,再降回原位」)。 + /// + private void ApplyObjectTrs(Transform tr, ref ObjectUpdateInfo updateInfo) + { + if (!_objectBaseTrs.TryGetValue(tr, out TransformBaseData baseTrs)) + { + baseTrs = new TransformBaseData + { + position = tr.localPosition, + rotation = tr.localRotation, + scale = tr.localScale, + }; + _objectBaseTrs[tr] = baseTrs; + } + + bool add = updateInfo.OffsetType == OffsetType.Add; + + if (updateInfo.data.enablePosition) + tr.localPosition = add + ? baseTrs.position + updateInfo.updateData.position + : updateInfo.updateData.position; + + if (updateInfo.data.enableRotate) + tr.localRotation = add + ? baseTrs.rotation * updateInfo.updateData.rotation + : updateInfo.updateData.rotation; + + // scale 用乘法而不是加法:关键帧里 scale 恒为 1,加法会把物件放大一倍。 + if (updateInfo.data.enableScale) + tr.localScale = add + ? Vector3.Scale(baseTrs.scale, updateInfo.updateData.scale) + : updateInfo.updateData.scale; + } + public void UpdateTransform(ref TransformUpdateInfo updateInfo) { if (updateInfo.data == null)