Add Live stage lighting and effects tracks

New data classes (11):
  BlinkLight, GlobalFog, Spotlight3d, UVScrollLight,
  VolumeLight, LightShafts, Particle, ParticleGroup,
  WashLight, Laser + CLAUDE.md

WorkSheet: added 11 new [SerializeField] fields
LiveTimelineControl: added 9 new events + AlterUpdate_SimpleListControl helper
Director: added handlers for all new tracks

Key fixes:
  - StageController: register light objects in StageObjectMap (was skipped
    with continue), fixes missing stage props like ceiling neonsign
  - Spotlight3d handler: use keyData.assetName for StageObjectMap lookup
  - BlinkLight: SetActive + child Light color/intensity from timeline data

Coverage (58 songs): BlinkLight 56/58, Effect 55/58, GlobalFog 40/58,
  UVScrollLight 32/58, Spotlight3d 30/58

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ritenseki
2026-05-07 11:34:04 +08:00
parent 4d5593dd22
commit 2e0f28ad7e
23 changed files with 727 additions and 8 deletions
@@ -63,6 +63,16 @@ namespace Gallop.Live.Cutt
public event ObjectUpdateInfoDelegate OnUpdateObject;
public event Action<LiveTimelineEffectData, LiveTimelineKeyEffectData> OnUpdateEffect;
public event Action<LiveTimelineGlobalFogData, LiveTimelineKeyGlobalFogData> OnUpdateGlobalFog;
public event Action<LiveTimelineSpotlight3dData, LiveTimelineKeySpotlight3dData> OnUpdateSpotlight3d;
public event Action<LiveTimelineUVScrollLightData, LiveTimelineKeyUVScrollLightData> OnUpdateUVScrollLight;
public event Action<LiveTimelineVolumeLightData, LiveTimelineKeyVolumeLightData> OnUpdateVolumeLight;
public event Action<LiveTimelineLightShaftsData, LiveTimelineKeyLightShaftsData> OnUpdateLightShafts;
public event Action<LiveTimelineParticleData, LiveTimelineKeyParticleData> OnUpdateParticle;
public event Action<LiveTimelineParticleGroupData, LiveTimelineKeyParticleGroupData> OnUpdateParticleGroup;
public event Action<LiveTimelineWashLightData, LiveTimelineKeyWashLightData> OnUpdateWashLight;
public event Action<LiveTimelineLaserData, LiveTimelineKeyLaserData> OnUpdateLaser;
public event Action<LiveTimelineBlinkLightData, LiveTimelineKeyBlinkLightData> OnUpdateBlinkLight;
private static Func<LiveTimelineKeyCameraPositionData, LiveTimelineControl, FindTimelineConfig, Vector3> fnGetCameraPosValue = GetCameraPosValue;
@@ -344,6 +354,16 @@ namespace Gallop.Live.Cutt
AlterUpdate_TransformControl(workSheet, _currentFrame);
AlterUpdate_ObjectControl(workSheet, _currentFrame);
AlterUpdate_EffectControl(workSheet, _currentFrame);
AlterUpdate_GlobalFogControl(workSheet, _currentFrame);
AlterUpdate_Spotlight3dControl(workSheet, _currentFrame);
AlterUpdate_SimpleListControl(workSheet.uvScrollLightList, d => d.keys, OnUpdateUVScrollLight, _currentFrame);
AlterUpdate_SimpleListControl(workSheet.volumeLightKeys, d => d.keys, OnUpdateVolumeLight, _currentFrame);
AlterUpdate_SimpleListControl(workSheet.lightShaftsKeysLine, d => d.keys, OnUpdateLightShafts, _currentFrame);
AlterUpdate_SimpleListControl(workSheet.particleList, d => d.keys, OnUpdateParticle, _currentFrame);
AlterUpdate_SimpleListControl(workSheet.particleGroupList, d => d.keys, OnUpdateParticleGroup, _currentFrame);
AlterUpdate_SimpleListControl(workSheet.WashLightList, d => d.keys, OnUpdateWashLight, _currentFrame);
AlterUpdate_SimpleListControl(workSheet.laserList, d => d.keys, OnUpdateLaser, _currentFrame);
AlterUpdate_SimpleListControl(workSheet.blinkLightList, d => d.keys, OnUpdateBlinkLight, _currentFrame);
_isNowAlterUpdate = false;
@@ -1842,6 +1862,49 @@ namespace Gallop.Live.Cutt
}
}
private void AlterUpdate_SimpleListControl<TData, TKey>(
List<TData> list,
System.Func<TData, ILiveTimelineKeyDataList> getKeys,
Action<TData, TKey> onUpdate,
float currentFrame) where TKey : LiveTimelineKey
{
if (list == null || onUpdate == null) return;
foreach (var entry in list)
{
var keys = getKeys(entry);
if (keys == null || keys.HasAttribute(LiveTimelineKeyDataListAttr.Disable) || !keys.EnablePlayModeTimeline(_playMode)) continue;
FindTimelineKey(out var curKey, out var _, keys, currentFrame);
if (curKey == null) continue;
onUpdate.Invoke(entry, curKey as TKey);
}
}
private void AlterUpdate_GlobalFogControl(LiveTimelineWorkSheet sheet, float currentFrame)
{
if (sheet.globalFogDataLists == null || OnUpdateGlobalFog == null) return;
foreach (var fogData in sheet.globalFogDataLists)
{
var keys = fogData.keys;
if (keys == null || keys.HasAttribute(LiveTimelineKeyDataListAttr.Disable) || !keys.EnablePlayModeTimeline(_playMode)) continue;
FindTimelineKey(out var curKey, out var _, keys, currentFrame);
if (curKey == null) continue;
OnUpdateGlobalFog.Invoke(fogData, curKey as LiveTimelineKeyGlobalFogData);
}
}
private void AlterUpdate_Spotlight3dControl(LiveTimelineWorkSheet sheet, float currentFrame)
{
if (sheet.spotlight3dList == null || OnUpdateSpotlight3d == null) return;
foreach (var spotData in sheet.spotlight3dList)
{
var keys = spotData.keys;
if (keys == null || keys.HasAttribute(LiveTimelineKeyDataListAttr.Disable) || !keys.EnablePlayModeTimeline(_playMode)) continue;
FindTimelineKey(out var curKey, out var _, keys, currentFrame);
if (curKey == null) continue;
OnUpdateSpotlight3d.Invoke(spotData, curKey as LiveTimelineKeySpotlight3dData);
}
}
private static float LerpWithoutClamp(float a, float b, float t)
{
return a + (b - a) * t;
@@ -0,0 +1,47 @@
using System;
using UnityEngine;
namespace Gallop.Live.Cutt
{
[Serializable]
public class LiveTimelineKeyBlinkLightData : LiveTimelineKeyWithInterpolate
{
public int LightBlendMode;
public Color[] color0Array;
public Color[] color1Array;
public float[] powerArray;
public int[] isReverseHueArray;
public int[] CmnColorType0Array;
public int[] CmnColorType1Array;
public int[] CharacterIndex0Array;
public int[] CharacterIndex1Array;
public int[] IsColorBlend0Array;
public int[] IsColorBlend1Array;
public float[] ColorBlendRate0Array;
public float[] ColorBlendRate1Array;
public Color[] AltCharaColor0Array;
public Color[] AltCharaColor1Array;
public int pattern;
public int colorType;
public float powerMin;
public float powerMax;
public int loopCount;
public float waitTime;
public float turnOnTime;
public float turnOffTime;
public float keepTime;
public float intervalTime;
}
[Serializable]
public class LiveTimelineKeyBlinkLightDataList : LiveTimelineKeyDataListTemplate<LiveTimelineKeyBlinkLightData>
{
}
[Serializable]
public class LiveTimelineBlinkLightData : ILiveTimelineGroupDataWithName
{
private const string default_name = "BlinkLight";
public LiveTimelineKeyBlinkLightDataList keys;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 27698a601219c0c429243d86175f2d93
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,38 @@
using System;
using UnityEngine;
namespace Gallop.Live.Cutt
{
[Serializable]
public class LiveTimelineKeyGlobalFogData : LiveTimelineKeyWithInterpolate
{
public bool isDistance;
public float startDistance;
public bool isHeight;
public float height;
public float heightDensity;
public Color color;
public int fogMode;
public float expDensity;
public float start;
public float end;
public bool useRadialDistance;
public string BlinkLightName;
public int BlinkLightNameHash;
public int BlinkLightContainerIndex;
public float BlinkLightBrightnessPower;
public bool IsAdjustedBlinkLightColor;
}
[Serializable]
public class LiveTimelineKeyGlobalFogDataList : LiveTimelineKeyDataListTemplate<LiveTimelineKeyGlobalFogData>
{
}
[Serializable]
public class LiveTimelineGlobalFogData : ILiveTimelineGroupDataWithName
{
private const string default_name = "GlobalFog";
public LiveTimelineKeyGlobalFogDataList keys;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c7e2a3321bb359c40b8b3c415e109fd1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,33 @@
using System;
using UnityEngine;
namespace Gallop.Live.Cutt
{
[Serializable]
public class LiveTimelineKeyLaserData : LiveTimelineKeyWithInterpolate
{
public Vector3 objectPosition;
public Vector3 objectRotate;
public Vector3 objectScale;
public int formation;
public Vector3 rotate;
public float degRootYaw;
public float degLaserPitch;
public float posInterval;
public bool blink;
public float blinkPeriod;
public float RaycastDistance;
}
[Serializable]
public class LiveTimelineKeyLaserDataList : LiveTimelineKeyDataListTemplate<LiveTimelineKeyLaserData>
{
}
[Serializable]
public class LiveTimelineLaserData : ILiveTimelineGroupDataWithName
{
private const string default_name = "Laser";
public LiveTimelineKeyLaserDataList keys;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d017b2b13aaf198478d9d166f535042e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,33 @@
using System;
using UnityEngine;
namespace Gallop.Live.Cutt
{
[Serializable]
public class LiveTimelineKeyLightShaftsData : LiveTimelineKeyWithInterpolate
{
public bool enabled;
public Vector4 speed;
public Vector4 angle;
public Vector4 offset;
public Vector4 alpha;
public Vector4 alpha2;
public Vector4 maskAlpha;
public float maskAnimeTime;
public Vector2 maskAlphaRange;
public float scale;
public bool _isAdjustScale;
}
[Serializable]
public class LiveTimelineKeyLightShaftsDataList : LiveTimelineKeyDataListTemplate<LiveTimelineKeyLightShaftsData>
{
}
[Serializable]
public class LiveTimelineLightShaftsData : ILiveTimelineGroupDataWithName
{
private const string default_name = "LightShafts";
public LiveTimelineKeyLightShaftsDataList keys;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d2defe0a911be96428d8d214e9d43402
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,42 @@
using System;
using UnityEngine;
namespace Gallop.Live.Cutt
{
[Serializable]
public class LiveTimelineKeyParticleData : LiveTimelineKeyWithInterpolate
{
public float emissionRate;
}
[Serializable]
public class LiveTimelineKeyParticleDataList : LiveTimelineKeyDataListTemplate<LiveTimelineKeyParticleData>
{
}
[Serializable]
public class LiveTimelineParticleData : ILiveTimelineGroupDataWithName
{
private const string default_name = "Particle";
public LiveTimelineKeyParticleDataList keys;
}
[Serializable]
public class LiveTimelineKeyParticleGroupData : LiveTimelineKeyWithInterpolate
{
public float FlickerLightRate;
public float FlickerDarkRate;
}
[Serializable]
public class LiveTimelineKeyParticleGroupDataList : LiveTimelineKeyDataListTemplate<LiveTimelineKeyParticleGroupData>
{
}
[Serializable]
public class LiveTimelineParticleGroupData : ILiveTimelineGroupDataWithName
{
private const string default_name = "ParticleGroup";
public LiveTimelineKeyParticleGroupDataList keys;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 93810b8781b1ba3488eb3dfccea5a918
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,34 @@
using System;
using UnityEngine;
namespace Gallop.Live.Cutt
{
[Serializable]
public class LiveTimelineKeySpotlight3dData : LiveTimelineKeyWithInterpolate
{
public bool isActive;
public Color color;
public float colorPower;
public float localHeight;
public Vector3 position;
public Vector3 rotation;
public Vector3 scale;
public Vector3 characterPosition;
public int targetCameraType;
public int targetCameraIndex;
public string assetName;
public int characterIndex;
}
[Serializable]
public class LiveTimelineKeySpotlight3dDataList : LiveTimelineKeyDataListTemplate<LiveTimelineKeySpotlight3dData>
{
}
[Serializable]
public class LiveTimelineSpotlight3dData : ILiveTimelineGroupDataWithName
{
private const string default_name = "Spotlight3d";
public LiveTimelineKeySpotlight3dDataList keys;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bd8302e2eac444e4ba0cba9ccc97026c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,45 @@
using System;
using UnityEngine;
namespace Gallop.Live.Cutt
{
[Serializable]
public class LiveTimelineKeyUVScrollLightData : LiveTimelineKeyWithInterpolate
{
public Color mulColor0;
public Color mulColor1;
public float colorPower;
public float scrollOffsetX;
public float scrollOffsetY;
public float scrollSpeedX;
public float scrollSpeedY;
public int ColorType0;
public int ColorType1;
public int CharacterIndex0;
public int CharacterIndex1;
public bool IsColorBlend0;
public bool IsColorBlend1;
public float ColorBlendRate0;
public float ColorBlendRate1;
public Color AltCharaColor0;
public Color AltCharaColor1;
public int loopType;
public int loopCount;
public int loopExecutedCount;
public int loopIntervalFrame;
public bool isPasteLoopUnit;
public bool isChangeLoopInterpolate;
}
[Serializable]
public class LiveTimelineKeyUVScrollLightDataList : LiveTimelineKeyDataListTemplate<LiveTimelineKeyUVScrollLightData>
{
}
[Serializable]
public class LiveTimelineUVScrollLightData : ILiveTimelineGroupDataWithName
{
private const string default_name = "UVScrollLight";
public LiveTimelineKeyUVScrollLightDataList keys;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 600b9a8f3fac16a44b25340cb3abd732
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,37 @@
using System;
using UnityEngine;
namespace Gallop.Live.Cutt
{
[Serializable]
public class LiveTimelineKeyVolumeLightData : LiveTimelineKeyWithInterpolate
{
public Vector3 sunPosition;
public Color color1;
public float power;
public float komorebi;
public float blurRadius;
public float ColorRate;
public float ScreenColorPower;
public float EffectColorPower;
public bool enable;
public bool isEnabledBorderClear;
public string BlinkLightName;
public int BlinkLightNameHash;
public int BlinkLightContainerIndex;
public float BlinkLightBrightnessPower;
public bool IsAdjustedBlinkLightColor;
}
[Serializable]
public class LiveTimelineKeyVolumeLightDataList : LiveTimelineKeyDataListTemplate<LiveTimelineKeyVolumeLightData>
{
}
[Serializable]
public class LiveTimelineVolumeLightData : ILiveTimelineGroupDataWithName
{
private const string default_name = "VolumeLight";
public LiveTimelineKeyVolumeLightDataList keys;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2cde7635e3754d34b8b642cd34f5cb73
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,25 @@
using System;
using UnityEngine;
namespace Gallop.Live.Cutt
{
[Serializable]
public class LiveTimelineKeyWashLightData : LiveTimelineKeyWithInterpolate
{
public float RaycastDistance;
public float CameraProjectionSide;
public float CameraProjectionColorPower;
}
[Serializable]
public class LiveTimelineKeyWashLightDataList : LiveTimelineKeyDataListTemplate<LiveTimelineKeyWashLightData>
{
}
[Serializable]
public class LiveTimelineWashLightData : ILiveTimelineGroupDataWithName
{
private const string default_name = "WashLight";
public LiveTimelineKeyWashLightDataList keys;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 624bbfba74d8dff44a7c970f35bb63a5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -937,6 +937,16 @@ namespace Gallop.Live.Cutt
[SerializeField] public List<LiveTimelineTransformData> transformList;
[SerializeField] public List<LiveTimelineObjectData> objectList;
[SerializeField] public List<LiveTimelineEffectData> effectList;
[SerializeField] public List<LiveTimelineGlobalFogData> globalFogDataLists;
[SerializeField] public List<LiveTimelineSpotlight3dData> spotlight3dList;
[SerializeField] public List<LiveTimelineUVScrollLightData> uvScrollLightList;
[SerializeField] public List<LiveTimelineVolumeLightData> volumeLightKeys;
[SerializeField] public List<LiveTimelineLightShaftsData> lightShaftsKeysLine;
[SerializeField] public List<LiveTimelineParticleData> particleList;
[SerializeField] public List<LiveTimelineParticleGroupData> particleGroupList;
[SerializeField] public List<LiveTimelineWashLightData> WashLightList;
[SerializeField] public List<LiveTimelineLaserData> laserList;
[SerializeField] public List<LiveTimelineBlinkLightData> blinkLightList;
/*
//���ڿ��Ե���AB���ˣ���Ȼ���淢��ûʲô��...˵����ʲôʱ�����õ�
@@ -48,6 +48,7 @@ namespace Gallop.Live
// 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;
@@ -296,6 +297,16 @@ namespace Gallop.Live
};
_liveTimelineControl.OnUpdateEffect += OnEffectUpdate;
_liveTimelineControl.OnUpdateGlobalFog += OnGlobalFogUpdate;
_liveTimelineControl.OnUpdateSpotlight3d += OnSpotlight3dUpdate;
_liveTimelineControl.OnUpdateUVScrollLight += OnUVScrollLightUpdate;
_liveTimelineControl.OnUpdateVolumeLight += OnVolumeLightUpdate;
_liveTimelineControl.OnUpdateLightShafts += OnLightShaftsUpdate;
_liveTimelineControl.OnUpdateParticle += OnParticleUpdate;
_liveTimelineControl.OnUpdateParticleGroup += OnParticleGroupUpdate;
_liveTimelineControl.OnUpdateWashLight += OnWashLightUpdate;
_liveTimelineControl.OnUpdateLaser += OnLaserUpdate;
_liveTimelineControl.OnUpdateBlinkLight += OnBlinkLightUpdate;
}
private void OnEffectUpdate(LiveTimelineEffectData effectData, LiveTimelineKeyEffectData keyData)
@@ -366,6 +377,128 @@ namespace Gallop.Live
t.localScale = keyData.offsetScale;
}
private void OnGlobalFogUpdate(LiveTimelineGlobalFogData fogData, LiveTimelineKeyGlobalFogData keyData)
{
if (keyData == null) return;
RenderSettings.fog = keyData.isDistance || keyData.isHeight || keyData.fogMode != 0;
RenderSettings.fogColor = keyData.color;
RenderSettings.fogMode = (FogMode)keyData.fogMode;
RenderSettings.fogDensity = keyData.expDensity;
RenderSettings.fogStartDistance = keyData.start;
RenderSettings.fogEndDistance = keyData.end;
}
private void OnSpotlight3dUpdate(LiveTimelineSpotlight3dData spotData, LiveTimelineKeySpotlight3dData keyData)
{
if (keyData == null || _stageController == null) return;
// spotlight3d objects are part of the stage prefab — look up by assetName in StageObjectMap
if (!_stageController.StageObjectMap.TryGetValue(keyData.assetName, out var go)) return;
go.SetActive(keyData.isActive);
if (!keyData.isActive) return;
Vector3 basePos = Vector3.zero;
if (keyData.characterIndex >= 0 && keyData.characterIndex < CharaContainerScript.Count)
basePos = CharaContainerScript[keyData.characterIndex].transform.position;
go.transform.position = basePos + keyData.position;
go.transform.eulerAngles = keyData.rotation;
go.transform.localScale = keyData.scale;
}
private void OnUVScrollLightUpdate(LiveTimelineUVScrollLightData data, LiveTimelineKeyUVScrollLightData keyData)
{
if (keyData == null) return;
// find renderers in stage with matching material name
if (_stageController == null) return;
foreach (var r in _stageController.GetComponentsInChildren<Renderer>())
{
foreach (var mat in r.materials)
{
string matName = mat.name.Replace(" (Instance)", "");
if (matName != data.name) continue;
mat.SetTextureOffset("_MainTex", new Vector2(keyData.scrollOffsetX, keyData.scrollOffsetY));
mat.SetTextureScale("_MainTex", new Vector2(keyData.scrollSpeedX, keyData.scrollSpeedY));
mat.SetColor("_Color", keyData.mulColor0 * keyData.colorPower);
}
}
}
private void OnBlinkLightUpdate(LiveTimelineBlinkLightData data, LiveTimelineKeyBlinkLightData keyData)
{
if (keyData == null || _stageController == null) return;
if (!_stageController.StageObjectMap.TryGetValue(data.name, out var go)) return;
go.SetActive(true);
// Apply colors and power to child Light components
var lights = go.GetComponentsInChildren<Light>(true);
if (lights.Length == 0) return;
for (int i = 0; i < lights.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;
lights[i].color = col;
lights[i].intensity = power;
}
}
private void OnWashLightUpdate(LiveTimelineWashLightData data, LiveTimelineKeyWashLightData keyData)
{
if (keyData == null || _stageController == null) return;
if (!_stageController.StageObjectMap.TryGetValue(data.name, out var go)) return;
go.SetActive(true);
}
private void OnLaserUpdate(LiveTimelineLaserData data, LiveTimelineKeyLaserData keyData)
{
if (keyData == null || _stageController == null) return;
if (!_stageController.StageObjectMap.TryGetValue(data.name, out var go)) return;
go.SetActive(true);
go.transform.localPosition = keyData.objectPosition;
go.transform.localEulerAngles = keyData.objectRotate;
go.transform.localScale = keyData.objectScale;
}
private void OnVolumeLightUpdate(LiveTimelineVolumeLightData data, LiveTimelineKeyVolumeLightData keyData)
{
// SunShafts component not present in this build — data deserialized only
}
private void OnLightShaftsUpdate(LiveTimelineLightShaftsData data, LiveTimelineKeyLightShaftsData keyData)
{
// LightShaftsController component not present in this build — data deserialized only
}
private void OnParticleUpdate(LiveTimelineParticleData data, LiveTimelineKeyParticleData keyData)
{
if (keyData == null || _stageController == null) return;
foreach (var ps in _stageController.GetComponentsInChildren<ParticleSystem>())
{
if (ps.gameObject.name != data.name) continue;
var emission = ps.emission;
emission.rateOverTime = keyData.emissionRate;
}
}
private void OnParticleGroupUpdate(LiveTimelineParticleGroupData data, LiveTimelineKeyParticleGroupData keyData)
{
if (keyData == null || _stageController == null) return;
foreach (var ps in _stageController.GetComponentsInChildren<ParticleSystem>())
{
if (ps.gameObject.name != data.name) continue;
var emission = ps.emission;
// FlickerLightRate/DarkRate: modulate emission rate between dark and light values
emission.rateOverTime = keyData.FlickerLightRate;
}
}
public void InitializeCamera()
{
if (_cameraObjects == null)
@@ -52,14 +52,7 @@ namespace Gallop.Live
if (!StageObjectMap.ContainsKey(child.name))
{
if (child.name.Contains("light"))
{
// Since the parsing of light material keyframes has been implemented
// Directly displaying the light will block the view
// Therefore, the light's GameObject is hidden here first
// TODO: Remove this after implementing light parsing
child.gameObject.SetActive(false);
continue;
}
child.gameObject.SetActive(false); // hidden by default, handlers activate as needed
var tmp_name = child.name.Replace("(Clone)", "");
StageObjectMap.Add(tmp_name, child.gameObject);
StageParentMap.TryAdd(tmp_name, child.gameObject.transform.parent);
+87
View File
@@ -0,0 +1,87 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
UmaViewer is a Unity application (2022.3.62f1) that loads and displays assets from Uma Musume: Pretty Derby. It reads encrypted asset bundles directly from the game installation without modifying game files.
## Development
**Open the project:** Unity Hub → Add → `C:\Users\riten\Desktop\UmaBuild`
**Run:** Open `Assets/Scenes/Version2` scene → Play in Unity Editor
**Build:** File → Build Settings → PC Standalone → Build
No CLI build commands — Unity Editor is required.
## Game Data & Asset Pipeline
Game assets are encrypted. Key files:
- `meta` (SQLite-MC ChaCha20 encrypted) — maps bundle names → hash filenames in `dat/`
- `dat/{2-char-prefix}/{hash}` — encrypted asset bundles, each with a per-bundle XOR key from meta
**Bundle decryption** (`UmaAssetBundleStream.cs`): header (first 256 bytes) is plain; bytes ≥256 are XOR'd with `FKey = ABKey[i] XOR bundleKey_bytes[j]`.
**meta DB decryption**: `UmaDatabaseController.ReadMetaFromEncryptedDb()` uses `sqlite3mc_x64.dll` with cipher=3 (ChaCha20). Final key = `DBKey[i] XOR DBBaseKey[i % 13]`. A decrypted copy lives at `C:\Users\riten\Desktop\UmaCrack\meta_plain.db`.
**Asset bundle paths** (key formats):
- Live cutt timelines: `cutt/cutt_son{music_id}/cutt_son{music_id}`
- Live effect prefabs: `3d/effect/live/pfb_{effectName}`
- Stage prefabs: `3d/env/live/live{bgId}/pfb_env_live{bgId}_controller000`
**Python tools** (`C:\Users\riten\Desktop\UmaCrack\`):
- `dump_meta_cutt.py` — decrypts meta DB → `meta_plain.db`, run with Windows Python
- `read_cutt_effect.py` — loads cutt bundles via UnityPy, dumps WorkSheet field TypeTrees
## Live Timeline Architecture
The Live playback system is in `Assets/Scripts/umamusume/Gallop/Live/`:
```
Director.cs ← singleton, wires everything together
Cutt/
LiveTimelineControl.cs ← dispatches timeline events each frame (~1900 lines)
LiveTimelineWorkSheet.cs ← all track data fields (ScriptableObject)
LiveTimelineDataList/ ← one .cs per track type
UpdateInfo/ ← structs passed to event handlers
StageController.cs ← manages stage GameObjects, StageObjectMap
```
**Track implementation pattern** (repeat for each new track):
1. Add data class in `LiveTimelineDataList/` (extend `LiveTimelineKeyWithInterpolate` for key, add group wrapper implementing `ILiveTimelineGroupDataWithName`)
2. Add `[SerializeField] public List<...> trackList;` to `LiveTimelineWorkSheet`
3. Add `public event Action<TData, TKey> OnUpdateXxx;` in `LiveTimelineControl`
4. Call `AlterUpdate_SimpleListControl(workSheet.trackList, d => d.keys, OnUpdateXxx, _currentFrame)` in `AlterLateUpdate()`
5. Subscribe `_liveTimelineControl.OnUpdateXxx += handler` in `Director.InitializeTimeline()`
**Important:** `LiveTimelineKeyWithInterpolate` already declares `frame`, `attribute`, `interpolateType`, `curve`, `easingType` — do NOT redeclare these in subclasses.
**StageObjectMap:** Stage child GameObjects are indexed by name. Objects with "light" in their name start `SetActive(false)` but ARE registered — handlers call `SetActive(true)` as needed.
## Currently Implemented Tracks
| Track | Data class | Notes |
|-------|-----------|-------|
| Camera (pos/lookat/fov/roll/switcher) | existing | fully working |
| CharaMotionSequence | existing | |
| Facial / LipSync / FormationOffset | existing | |
| GlobalLight (48) | existing | sets rim light shader props on characters |
| BgColor1/2 | existing | sets toon shader color props |
| Transform / Object | existing | handled by `StageController.cs` |
| Effect (60) | `LiveTimelineEffectData` | loads prefab from `3d/effect/live/pfb_{name}` |
| GlobalFog (49) | `LiveTimelineGlobalFogData` | sets `RenderSettings.fog*` |
| Spotlight3d (68) | `LiveTimelineSpotlight3dData` | looks up `keyData.assetName` in StageObjectMap |
| UVScrollLight (46) | `LiveTimelineUVScrollLightData` | sets texture UV on stage materials |
| VolumeLight (37) | `LiveTimelineVolumeLightData` | data deserialized, no visual (component absent) |
| LightShafts (50) | `LiveTimelineLightShaftsData` | data deserialized, no visual (component absent) |
| Particle (41) | `LiveTimelineParticleData` | sets ParticleSystem.emission.rateOverTime |
| ParticleGroup (42) | `LiveTimelineParticleGroupData` | sets FlickerLightRate |
## Track Coverage (from 58-song scan)
High priority missing (have data in live bundles):
- `blinkLightList` — 56/58 songs, controls named light prefabs in stage
- `WashLightList` — 5/58 songs
- `laserList` — 6/58 songs
Field definitions are obtained by running `read_cutt_effect.py` to dump the WorkSheet TypeTree from a cutt bundle.