From abdbfc0f425ca7920c138edeb835e4552cefc085 Mon Sep 17 00:00:00 2001 From: ritenseki Date: Wed, 6 May 2026 21:24:52 +0800 Subject: [PATCH] Add Live Effect track (track ID 60) implementation - LiveTimelineEffectData.cs: new data class with full field set (color, offset, owner, occurrenceSpot, IsLinkOwner*, etc.) derived from actual cutt asset bundle TypeTree dump - LiveTimelineWorkSheet: add effectList field - LiveTimelineControl: add OnUpdateEffect event and AlterUpdate_EffectControl - Director: add OnEffectUpdate handler and ApplyEffectTransform helper loads prefab from 3d/effect/live/pfb_{name}, instantiates per key frame Co-Authored-By: Claude Sonnet 4.6 --- .../Gallop/Live/Cutt/LiveTimelineControl.cs | 23 +++++- .../LiveTimelineEffectData.cs | 43 +++++++++++ .../Gallop/Live/Cutt/LiveTimelineWorkSheet.cs | 7 +- .../Scripts/umamusume/Gallop/Live/Director.cs | 77 ++++++++++++++++++- 4 files changed, 144 insertions(+), 6 deletions(-) create mode 100644 Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineDataList/LiveTimelineEffectData.cs diff --git a/Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineControl.cs b/Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineControl.cs index c390694..15a226d 100644 --- a/Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineControl.cs +++ b/Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineControl.cs @@ -62,6 +62,8 @@ namespace Gallop.Live.Cutt public event ObjectUpdateInfoDelegate OnUpdateObject; + public event Action OnUpdateEffect; + private static Func fnGetCameraPosValue = GetCameraPosValue; private static Func fnGetCameraLookAtValue = GetCameraLookAtValue; @@ -341,6 +343,7 @@ namespace Gallop.Live.Cutt AlterUpdate_BgColor1(workSheet, _currentFrame); AlterUpdate_TransformControl(workSheet, _currentFrame); AlterUpdate_ObjectControl(workSheet, _currentFrame); + AlterUpdate_EffectControl(workSheet, _currentFrame); _isNowAlterUpdate = false; @@ -618,7 +621,7 @@ namespace Gallop.Live.Cutt - //ÐÞ¸Ä(LiveÑݳöµÄÏÔÒþ¿ØÖƺÍλÖñ任Ïà¹Ø) + //�޸�(Live�ݳ����������ƺ�λ�ñ任���) public void LateUpdateFormationOffset_Transform(int targetIndex, LiveTimelineKeyIndex curKeyIndex, float time) { bool ControlMode = UmaViewerUI.Instance != null && UmaViewerUI.Instance.isControlMode; @@ -1158,7 +1161,7 @@ namespace Gallop.Live.Cutt { camera.cacheTransform.position = pos; - // TODO ÕâÀïÓõϹÊÇCGSSµÄlayerö¾Ù£¬Òª½øÐÐÌæ»» + // TODO �����õĻ���CGSS��layerö�٣�Ҫ�����滻 /* int num = liveTimelineKeyCameraPositionData.GetCullingMask(); if (num == 0) @@ -1823,6 +1826,22 @@ namespace Gallop.Live.Cutt } } + private void AlterUpdate_EffectControl(LiveTimelineWorkSheet sheet, float currentFrame) + { + if (sheet.effectList == null || OnUpdateEffect == null) return; + int count = sheet.effectList.Count; + for (int i = 0; i < count; i++) + { + var effectData = sheet.effectList[i]; + var keys = effectData.keys; + if (keys == null || keys.HasAttribute(LiveTimelineKeyDataListAttr.Disable) || !keys.EnablePlayModeTimeline(_playMode)) + continue; + FindTimelineKey(out var curKey, out var _, keys, currentFrame); + if (curKey == null) continue; + OnUpdateEffect.Invoke(effectData, curKey as LiveTimelineKeyEffectData); + } + } + private static float LerpWithoutClamp(float a, float b, float t) { return a + (b - a) * t; diff --git a/Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineDataList/LiveTimelineEffectData.cs b/Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineDataList/LiveTimelineEffectData.cs new file mode 100644 index 0000000..a02dce9 --- /dev/null +++ b/Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineDataList/LiveTimelineEffectData.cs @@ -0,0 +1,43 @@ +using System; +using UnityEngine; + +namespace Gallop.Live.Cutt +{ + [Serializable] + public class LiveTimelineKeyEffectData : LiveTimelineKeyWithInterpolate + { + public Color color; + public float colorPower; + public int ColorProperty; + public int owner; + public int occurrenceSpot; + public string ParentStageObjectName; + public bool IsAttachProps; + public int PropsIndex; + public Vector3 offset; + public Vector3 offsetAngle; + public Vector3 offsetScale; + public string BlinkLightName; + public int BlinkLightNameHash; + public int BlinkLightContainerIndex; + public float BlinkLightBrightnessPower; + public bool IsAdjustedBlinkLightColor; + public bool IsSyncBlinkLight; + public bool IsLinkOwnerPositionX; + public bool IsLinkOwnerPositionY; + public bool IsLinkOwnerPositionZ; + public bool IsLinkOwnerRotate; + } + + [Serializable] + public class LiveTimelineKeyEffectDataList : LiveTimelineKeyDataListTemplate + { + } + + [Serializable] + public class LiveTimelineEffectData : ILiveTimelineGroupDataWithName + { + private const string default_name = "Effect"; + public LiveTimelineKeyEffectDataList keys; + } +} diff --git a/Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineWorkSheet.cs b/Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineWorkSheet.cs index 0f09e7b..31c96eb 100644 --- a/Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineWorkSheet.cs +++ b/Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineWorkSheet.cs @@ -634,7 +634,7 @@ namespace Gallop.Live.Cutt public float Timescale; } -//ÕýÔÚ´¦ÀíÕâÀï +//���ڴ������� [System.Serializable] public class LiveTimelineKeyCameraPositionData : LiveTimelineKeyWithInterpolate { @@ -915,7 +915,7 @@ namespace Gallop.Live.Cutt [SerializeField] public List multiCameraPosKeys; [SerializeField] public List multiCameraLookAtKeys; - //[SerializeField]ÓÃÓÚ¸ÃÀàÔÚ±ðµÄ½Å±¾ÀﶨÒåµÄʱºò + //[SerializeField]���ڸ����ڱ�Ľű��ﶨ���ʱ�� [SerializeField] public LiveTimelineKeyCameraLookAtDataList cameraLookAtKeys; [SerializeField] public LiveTimelineKeyCameraFovDataList cameraFovKeys; [SerializeField] public LiveTimelineKeyCameraRollDataList cameraRollKeys; @@ -936,9 +936,10 @@ namespace Gallop.Live.Cutt [SerializeField] public List transformList; [SerializeField] public List objectList; + [SerializeField] public List effectList; /* - //ÖÕÓÚ¿ÉÒÔµ÷ÓÃAB°üÁË£¬ËäÈ»ºóÃæ·¢ÏÖûʲôÓÃ...˵²»¶¨Ê²Ã´Ê±ºòÄÜÓõ½ + //���ڿ��Ե���AB���ˣ���Ȼ���淢��ûʲô��...˵����ʲôʱ�����õ� private void Start() { LoadCharaMotion(); diff --git a/Assets/Scripts/umamusume/Gallop/Live/Director.cs b/Assets/Scripts/umamusume/Gallop/Live/Director.cs index adaa6cf..b12c138 100644 --- a/Assets/Scripts/umamusume/Gallop/Live/Director.cs +++ b/Assets/Scripts/umamusume/Gallop/Live/Director.cs @@ -34,6 +34,7 @@ namespace Gallop.Live private const string VOCAL_PATH = "sound/l/{0}/snd_bgm_live_{0}_chara_{1}_01"; private const string RANDOM_VOCAL_PATH = "sound/l/{0}/snd_bgm_live_{0}_chara"; private const string LIVE_PART_PATH = "live/musicscores/m{0}/m{0}_part"; + private const string EFFECT_PATH = "3d/effect/live/pfb_{0}"; private UmaViewerBuilder Builder => UmaViewerBuilder.Instance; @@ -43,6 +44,10 @@ namespace Gallop.Live public List charaAnims; public List liveVocal = new List(); + + // effect track: maps effectList entry -> (last key frame, active instance) + private Dictionary _activeEffects + = new Dictionary(); public UmaViewerAudio.CuteAudioSource liveMusic = new UmaViewerAudio.CuteAudioSource(); public PartEntry partInfo; @@ -152,7 +157,7 @@ namespace Gallop.Live sliderControl = UI.ProgressBar.GetComponent(); LiveViewerUI.Instance.RecordingUI.SetActive(IsRecordVMD); - LiveViewerUI.Instance.RecordingText.text = $"¡ñ Recording...\r\n VMD will be saved in {Path.GetFullPath(Application.dataPath + UnityHumanoidVMDRecorder.FileSavePath)}"; + LiveViewerUI.Instance.RecordingText.text = $"�� Recording...\r\n VMD will be saved in {Path.GetFullPath(Application.dataPath + UnityHumanoidVMDRecorder.FileSavePath)}"; } public void InitializeTimeline(List characters, int mode) @@ -289,6 +294,76 @@ namespace Gallop.Live _activeCameraIndex = kTimelineCameraIndices[cameraIndex_]; } }; + + _liveTimelineControl.OnUpdateEffect += OnEffectUpdate; + } + + private void OnEffectUpdate(LiveTimelineEffectData effectData, LiveTimelineKeyEffectData keyData) + { + if (keyData == null) return; + + // Only (re)instantiate when the key frame changes + if (_activeEffects.TryGetValue(effectData, out var current) && current.frame == keyData.frame) + { + // Same key — update position if following owner + if (current.instance != null) + ApplyEffectTransform(current.instance.transform, keyData); + return; + } + + // Destroy previous instance + if (_activeEffects.TryGetValue(effectData, out var old) && old.instance != null) + Destroy(old.instance); + + // Load prefab + string path = string.Format(EFFECT_PATH, effectData.name); + if (!UmaViewerMain.Instance.AbList.ContainsKey(path)) + { + _activeEffects[effectData] = (keyData.frame, null); + return; + } + + AssetBundle bundle = UmaAssetManager.LoadAssetBundle(UmaViewerMain.Instance.AbList[path]); + if (bundle == null) + { + _activeEffects[effectData] = (keyData.frame, null); + return; + } + + GameObject prefab = bundle.LoadAsset(System.IO.Path.GetFileName(path)); + if (prefab == null) + { + _activeEffects[effectData] = (keyData.frame, null); + return; + } + + GameObject instance = Instantiate(prefab, transform); + ApplyEffectTransform(instance.transform, keyData); + _activeEffects[effectData] = (keyData.frame, instance); + } + + private void ApplyEffectTransform(Transform t, LiveTimelineKeyEffectData keyData) + { + Vector3 basePos = Vector3.zero; + + // owner == World (18) or out of range: world origin + int ownerIndex = keyData.owner; + if (ownerIndex >= 0 && ownerIndex < CharaContainerScript.Count) + { + var container = CharaContainerScript[ownerIndex]; + if (container != null) + { + basePos = new Vector3( + keyData.IsLinkOwnerPositionX ? container.transform.position.x : 0f, + keyData.IsLinkOwnerPositionY ? container.transform.position.y : 0f, + keyData.IsLinkOwnerPositionZ ? container.transform.position.z : 0f + ); + } + } + + t.position = basePos + keyData.offset; + t.eulerAngles = keyData.offsetAngle; + t.localScale = keyData.offsetScale; } public void InitializeCamera()