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 <noreply@anthropic.com>
This commit is contained in:
ritenseki
2026-05-06 21:24:52 +08:00
parent 37f5352277
commit abdbfc0f42
4 changed files with 144 additions and 6 deletions
@@ -62,6 +62,8 @@ namespace Gallop.Live.Cutt
public event ObjectUpdateInfoDelegate OnUpdateObject;
public event Action<LiveTimelineEffectData, LiveTimelineKeyEffectData> OnUpdateEffect;
private static Func<LiveTimelineKeyCameraPositionData, LiveTimelineControl, FindTimelineConfig, Vector3> fnGetCameraPosValue = GetCameraPosValue;
private static Func<LiveTimelineKeyCameraLookAtData, LiveTimelineControl, Vector3, FindTimelineConfig, Vector3> 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;
@@ -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<LiveTimelineKeyEffectData>
{
}
[Serializable]
public class LiveTimelineEffectData : ILiveTimelineGroupDataWithName
{
private const string default_name = "Effect";
public LiveTimelineKeyEffectDataList keys;
}
}
@@ -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<LiveTimelineMultiCameraPositionData> multiCameraPosKeys;
[SerializeField] public List<LiveTimelineMultiCameraLookAtData> 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<LiveTimelineTransformData> transformList;
[SerializeField] public List<LiveTimelineObjectData> objectList;
[SerializeField] public List<LiveTimelineEffectData> effectList;
/*
//终于可以调用AB包了,虽然后面发现没什么用...说不定什么时候能用到
//���ڿ��Ե���AB���ˣ���Ȼ���淢��ûʲô��...˵����ʲôʱ�����õ�
private void Start()
{
LoadCharaMotion();
@@ -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<Animation> charaAnims;
public List<UmaViewerAudio.CuteAudioSource> liveVocal = new List<UmaViewerAudio.CuteAudioSource>();
// effect track: maps effectList entry -> (last key frame, active instance)
private Dictionary<LiveTimelineEffectData, (int frame, GameObject instance)> _activeEffects
= new Dictionary<LiveTimelineEffectData, (int, GameObject)>();
public UmaViewerAudio.CuteAudioSource liveMusic = new UmaViewerAudio.CuteAudioSource();
public PartEntry partInfo;
@@ -152,7 +157,7 @@ namespace Gallop.Live
sliderControl = UI.ProgressBar.GetComponent<SliderControl>();
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<LiveCharacterSelect> 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<GameObject>(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()