Fix BlinkLight flicker and ParticleGroup dual-rate emission

BlinkLight (pattern != 0): was always on; now computes intensity each
frame from turnOnTime/keepTime/turnOffTime/intervalTime/waitTime/
loopCount/powerMin/powerMax — supports fade-in, hold, fade-out, interval
cycle. pattern == 0 retains the original static per-light path.

ParticleGroup: was driving emission from FlickerLightRate only;
now uses MinMaxCurve(FlickerDarkRate, FlickerLightRate) so Unity
randomly samples between the two rates each emission event.

Also adds TODO comments on WashLight, Laser, UVScrollLight incomplete
fields, updates CLAUDE.md track table and live-shader-todo.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ritenseki
2026-05-07 13:16:44 +08:00
parent af9316b2ea
commit c0c7a5bfbe
3 changed files with 264 additions and 117 deletions
@@ -419,8 +419,13 @@ namespace Gallop.Live
string matName = mat.name.Replace(" (Instance)", "");
if (matName != data.name) continue;
mat.SetTextureOffset("_MainTex", new Vector2(keyData.scrollOffsetX, keyData.scrollOffsetY));
// TODO: scrollSpeedX/Y mapped to SetTextureScale — verify correctness.
// If the original shader uses speed as an incremental offset (offset += speed * deltaTime),
// this should accumulate per frame instead of being set as texture tiling.
mat.SetTextureScale("_MainTex", new Vector2(keyData.scrollSpeedX, keyData.scrollSpeedY));
mat.SetColor("_Color", keyData.mulColor0 * keyData.colorPower);
// TODO: unused — mulColor1, ColorType0/1, CharacterIndex0/1,
// IsColorBlend0/1, ColorBlendRate0/1, AltCharaColor0/1, loopType/loopCount.
}
}
}
@@ -429,24 +434,58 @@ namespace Gallop.Live
{
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;
if (keyData.pattern == 0)
{
// Static: apply per-light power and color directly
for (int i = 0; i < lights.Length; i++)
{
int idx = i < keyData.powerArray?.Length ? i : 0;
lights[i].intensity = keyData.powerArray != null && keyData.powerArray.Length > idx ? keyData.powerArray[idx] : 1f;
lights[i].color = keyData.color0Array != null && keyData.color0Array.Length > idx ? keyData.color0Array[idx] : Color.white;
}
return;
}
// Blink: derive intensity from timing fields relative to key start time
float elapsed = _liveTimelineControl.currentLiveTime - keyData.frame / 60f - keyData.waitTime;
float intensity = ComputeBlinkIntensity(keyData, elapsed);
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;
lights[i].intensity = intensity;
lights[i].color = keyData.color0Array != null && i < keyData.color0Array.Length ? keyData.color0Array[i] : Color.white;
}
// TODO: color1Array, LightBlendMode, isReverseHueArray, color blend fields unused
}
private static float ComputeBlinkIntensity(LiveTimelineKeyBlinkLightData keyData, float elapsed)
{
if (elapsed < 0f) return keyData.powerMin;
float cycleDuration = keyData.turnOnTime + keyData.keepTime + keyData.turnOffTime + keyData.intervalTime;
if (cycleDuration <= 0f) return keyData.powerMax;
if (keyData.loopCount > 0 && elapsed >= cycleDuration * keyData.loopCount)
return keyData.powerMin;
float t = elapsed % cycleDuration;
if (t < keyData.turnOnTime)
return Mathf.Lerp(keyData.powerMin, keyData.powerMax, keyData.turnOnTime > 0f ? t / keyData.turnOnTime : 1f);
t -= keyData.turnOnTime;
if (t < keyData.keepTime)
return keyData.powerMax;
t -= keyData.keepTime;
if (t < keyData.turnOffTime)
return Mathf.Lerp(keyData.powerMax, keyData.powerMin, keyData.turnOffTime > 0f ? t / keyData.turnOffTime : 1f);
return keyData.powerMin; // intervalTime: off
}
private void OnWashLightUpdate(LiveTimelineWashLightData data, LiveTimelineKeyWashLightData keyData)
@@ -454,6 +493,8 @@ namespace Gallop.Live
if (keyData == null || _stageController == null) return;
if (!_stageController.StageObjectMap.TryGetValue(data.name, out var go)) return;
go.SetActive(true);
// TODO: incomplete — keyData.RaycastDistance, CameraProjectionSide, CameraProjectionColorPower unused.
// Needs a Projector or custom projection component on the stage object to apply wash color effect.
}
private void OnLaserUpdate(LiveTimelineLaserData data, LiveTimelineKeyLaserData keyData)
@@ -464,6 +505,8 @@ namespace Gallop.Live
go.transform.localPosition = keyData.objectPosition;
go.transform.localEulerAngles = keyData.objectRotate;
go.transform.localScale = keyData.objectScale;
// TODO: incomplete — keyData.blink/blinkPeriod (SetActive flicker), degLaserPitch (beam angle),
// RaycastDistance (beam length via scale), formation/posInterval (multi-laser layout) unused.
}
private void OnVolumeLightUpdate(LiveTimelineVolumeLightData data, LiveTimelineKeyVolumeLightData keyData)
@@ -494,8 +537,7 @@ namespace Gallop.Live
{
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;
emission.rateOverTime = new ParticleSystem.MinMaxCurve(keyData.FlickerDarkRate, keyData.FlickerLightRate);
}
}
+4 -6
View File
@@ -76,12 +76,10 @@ StageController.cs ← manages stage GameObjects, StageObjectMap
| LightShafts (50) | `LiveTimelineLightShaftsData` | data deserialized, no visual (component absent) |
| Particle (41) | `LiveTimelineParticleData` | sets ParticleSystem.emission.rateOverTime |
| ParticleGroup (42) | `LiveTimelineParticleGroupData` | sets FlickerLightRate |
| BlinkLight | `LiveTimelineBlinkLightData` | SetActive + child Light color/intensity from timeline data (56/58 songs) |
| WashLight | `LiveTimelineWashLightData` | SetActive only — RaycastDistance/CameraProjection fields unused (5/58 songs) |
| Laser | `LiveTimelineLaserData` | SetActive + position/rotation/scale — blink/raycast not implemented (6/58 songs) |
## 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.
All high-priority tracks from the 58-song scan are now implemented. Field definitions are obtained by running `read_cutt_effect.py` to dump the WorkSheet TypeTree from a cutt bundle.
+206 -99
View File
@@ -1,143 +1,250 @@
# Live Post-Processing Shader TODO
UmaViewer uses **URP (Unity 2022.3.62f1)**。游戏原版是 Built-in 管线,以下 shader 需要移植或重写为 URP 实现。
UmaViewer 使用 **URP 14(Unity 2022.3.62f1)**。游戏原版是 Built-in 管线,以下 shader 需要移植或重写为 URP 实现。
---
## 接线方式(通用)
## 已实现但不完整的 Handler(`Director.cs`)
每个效果的 Director handler 在 `Director.cs` 的 `InitializeTimeline()` 里订阅:
| Handler | 问题 | 难度 |
|---------|------|------|
| ~~`OnBlinkLightUpdate`~~ | ✅ 已修复:`pattern==0` 走静态路径,`pattern!=0` 用 `turnOnTime`/`keepTime`/`turnOffTime`/`intervalTime`/`waitTime`/`loopCount`/`powerMin`/`powerMax` 计算每帧强度(含淡入淡出)。剩余未做:`color1Array`、`LightBlendMode`、颜色混合字段 | — |
| `OnUVScrollLightUpdate` | `scrollSpeedX/Y` 映射为 `SetTextureScale` 可能错误——若原版 shader 以速度做每帧增量位移,应改为 `offset += speed * deltaTime`。另:`mulColor1`、`ColorType`、`CharacterIndex`、颜色混合字段未使用 | ★☆☆(待确认语义) |
| ~~`OnParticleGroupUpdate`~~ | ✅ 已修复:改用 `MinMaxCurve(FlickerDarkRate, FlickerLightRate)`,Unity 粒子系统在两个发射率之间随机取值,自然模拟闪烁 | — |
| `OnWashLightUpdate` | 仅 `SetActive(true)`,`RaycastDistance`/`CameraProjectionSide`/`CameraProjectionColorPower` 未使用,需 Projector 或自定义投影组件 | ★★☆ |
| `OnLaserUpdate` | `blink`/`blinkPeriod`/`degLaserPitch`/`RaycastDistance`/`formation`/`posInterval` 未使用 | ★★☆ |
### 剩余问题说明
**UVScrollLight scrollSpeed**:需先用 `read_cutt_effect.py` dump 一首有 UV 滚动的曲目,对比 `scrollSpeedX/Y` 的实际值和视觉效果来确认语义。如果值是每秒 offset 增量,改为在 handler 里维护累计 offset(需持久化,不能每帧从 keyData 重置)。
**WashLight / Laser**:依赖舞台 prefab 的实际组件结构,需先加载一个含 WashLight/Laser 的 Live 看 prefab 里有什么,才能决定如何驱动。在此之前无法实现。
---
## 架构说明(必读)
### 为什么不能直接用旧方法
Built-in 管线支持 `OnRenderImage()`,脚本可直接拦截帧缓冲做全屏处理。**URP 删掉了这个接口**。
项目同时装了 `com.unity.postprocessing 3.4.0`(旧 Built-in 包)和 URP 14。`Screenshot.cs` 在用旧包的 `PostProcessLayer`,但该组件在 URP 管线下**不渲染任何效果**,只是没报错。后续实现不要依赖旧包。
### 两种实现路径
**路径 A — URP Volume Override(适用于内置效果)**
摄像机上挂 `Volume` 组件,`VolumeProfile` 里添加对应 Override,每帧在 Director handler 里设值:
```csharp
_liveTimelineControl.OnUpdateXxx += (data, keyData) => {
var vol = Camera.main.GetComponent<Volume>();
if (vol.profile.TryGet<URP后处理类型>(out var fx)) {
fx.参数.value = keyData.对应字段;
}
if (vol.profile.TryGet<SomeUrpEffect>(out var fx))
fx.someParam.Override(keyData.value);
};
```
---
适用:ChromaticAberration、Bloom、ColorAdjustments、DepthOfField。
## 1. ChromaticAberration ✅ URP 内置
**Track**: `chromaticAberrationList`(22/58 首)
**原版 shader**: `ImageEffect/ChromaticAberra`(Unity 官方开源)
**URP 方案**: `UnityEngine.Rendering.Universal.ChromaticAberration`(Volume Override 内置)
**路径 B — ScriptableRendererFeature(适用于自定义效果)**
字段对应(从 bundle TypeTree 获取,待 dump):
- `effectType` (int) → 目前值为 0,可能控制是否启用
URP 在管线中插入自定义 Render Pass,用 Material 做全屏 Blit:
**实现步骤**:
1. 在摄像机 GameObject 上加 `Volume` 组件
2. Profile 添加 `ChromaticAberration` override
3. `Director.cs` 订阅 `OnUpdateChromaticAberration`,设 `intensity.value`
---
## 2. Bloom / HdrBloom ✅ URP 内置
**Track**: `hdrBloomKeys`(58/58 首,但疑似骨架数据)
**原版 shader**: `ARBloom` / `RBloom`(Cygames 自定义,基于标准 Bloom)
**URP 方案**: `UnityEngine.Rendering.Universal.Bloom`
字段:待 dump hdrBloomKeys bundle 数据。
---
## 3. ColorCorrection ✅ URP 内置(近似)
**Track**: `colorCorrectionDataLists`
**URP 方案**: `ColorAdjustments` + `ColorCurves`
---
## 4. SunShafts (VolumeLight) ⚠️ 需移植
**Track**: `volumeLightKeys`(58/58 首)
**原版 shader**: `ImageEffects/SunShafts`(**Unity 官方开源 MIT**)
**源码**: https://github.com/Unity-Technologies/ImageEffects
字段对应(已从 bundle dump):
```
sunPosition: Vector3 → 光源世界坐标
color1: Color → 光线颜色
power: float → 光线强度(当前 0.0,enable=0 时不渲染)
komorebi: float → 丁达尔分量强度
blurRadius: float → 模糊半径
ScreenColorPower: float → 屏幕混合强度
EffectColorPower: float → 效果强度
enable: bool → 是否启用
ScriptableRendererFeature ← 注册到 Renderer Asset
└── ScriptableRenderPass
└── Blitter.BlitCameraTexture(cmd, src, dst, material, pass)
```
Feature 暴露公开字段,Director 持有 Feature 引用,每帧写入参数:
```csharp
// Director 字段
private PostFilmRendererFeature _postFilmFeature;
// InitializeTimeline 里获取
var data = GraphicsSettings.currentRenderPipeline as UniversalRenderPipelineAsset;
_postFilmFeature = data.GetRenderer(0) ... // 或序列化直接引用
// handler 里设值
_postFilmFeature.settings.color0 = keyData.color0;
_postFilmFeature.settings.filmMode = (int)keyData.filmMode;
```
Feature 必须在 `UMAUniversalRenderPipelineAsset_Renderer.asset` 的 Renderer Features 列表里手动添加。
适用:PostFilm、RadialBlur、TiltShift、SunShafts。
---
## 分组与优先顺序
### 第一组:URP 内置,工作量小
| 优先级 | Track ID | 字段名 | URP 类型 | 覆盖率 |
|--------|----------|--------|---------|--------|
| 1 | 73 | `chromaticAberrationList` | `ChromaticAberration` | 22/58 |
| 2 | 38 | `hdrBloomKeys` | `Bloom` | 58/58(疑似骨架) |
| 3 | 61 | `colorCorrectionDataLists` | `ColorAdjustments` + `ColorCurves` | — |
| 4 | 13 | `postEffectDOFKeys` | `DepthOfField` | — |
这四个照标准 5 步走,在 Director handler 里操作 Volume 即可。
### 第二组:自定义 shader,工作量大
| 优先级 | Track ID | 字段名 | 覆盖率 | 备注 |
|--------|----------|--------|--------|------|
| 5 | 39 | `postFilmKeys` / `postFilm2Keys` / `postFilm3Keys` | 58/58 | 最复杂,优先做 |
| 6 | 37 | `volumeLightKeys` | 58/58 | 数据类已有,shader 需移植 |
| 7 | 15 | `radialBlurKeys` | 58/58(骨架居多) | 大多数曲目值为0 |
| 8 | 63 | `tiltShiftKeys` | 58/58(骨架居多) | 大多数曲目值为0 |
---
## 各效果详情
### 1. ChromaticAberration(路径 A)
**Track ID**: 73
**URP 类型**: `UnityEngine.Rendering.Universal.ChromaticAberration`
字段(待从 bundle dump):
- `effectType` (int) — 目前值为 0,可能控制启用/强度
实现:摄像机 Volume → Profile 加 ChromaticAberration override → handler 设 `intensity.Override(value)`。
---
### 2. HdrBloom(路径 A)
**Track ID**: 38
**URP 类型**: `UnityEngine.Rendering.Universal.Bloom`
字段(待 dump)。注意 `hdrBloomSettings` 在 `LiveTimelineData.cs` 里只有 `bloomBlurIterations` 一个字段,
实际 key 数据需要从 bundle 确认。
---
### 3. ColorCorrection(路径 A)
**Track ID**: 61
**URP 类型**: `ColorAdjustments`(曝光、饱和度、色相)+ `ColorCurves`
字段(待 dump)。
---
### 4. DepthOfField(路径 A)
**Track ID**: 13
**URP 类型**: `UnityEngine.Rendering.Universal.DepthOfField`
字段(待 dump)。注意 URP DOF 分 Gaussian 和 Bokeh 两种模式,选哪种取决于原版字段。
---
### 5. PostFilm(路径 B,优先)
**Track ID**: 39(还有 76=MultiCameraPostFilm)
**数据类**: `LiveTimelineKeyPostFilmData`(`LiveTimelineKeyPostFilmDataList.cs` 已存在)
**为什么复杂**:`filmMode` 决定混合逻辑(8种),`PostColorType` 决定颜色布局(4种),
shader 需要处理 4×8 种组合,用 keyword 或 uniform 分支实现:
```
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
```
已知字段(`LiveTimelineKeyPostFilmData`):
```
filmMode, colorType
filmPower → 全局强度
filmOffsetParam → 位移参数
filmOptionParam → 附加参数(Vector4)
color0/1/2/3 → 最多4个颜色分区
depthPower → 深度混合强度
DepthClip → 深度剪裁
RollAngle → 旋转角度
FilmScale → 缩放
layerMode → Color=0 / UVMovie=1
colorBlend → None/Lerp/Additive/Multiply
colorBlendFactor → 混合系数
```
**实现步骤**:
1. 从 Unity ImageEffects 仓库取 `SunShafts.cs` + `SunShaftsComposite.shader` + `SunShaftsComposite.cginc`
2. 移植为 URP `ScriptableRendererFeature`(参考:https://github.com/search?q=sun+shafts+urp)
3. `Director.cs` 订阅 `OnUpdateVolumeLight`,设置 Feature 参数
1. 创建 `PostFilmRendererFeature.cs` + `PostFilmRenderPass.cs`
2. 写 `PostFilm.shader`(HLSL,`AfterRenderingPostProcessing` 插入点)
3. 在 `UMAUniversalRenderPipelineAsset_Renderer.asset` 添加该 Feature
4. Director 持有 Feature 引用,`OnPostFilmUpdate` handler 写入参数
5. WorkSheet 加 `postFilmKeys`、`postFilm2Keys`、`postFilm3Keys` 字段,Control 加事件
---
## 5. RadialBlur ⚠️ 需自写
**Track**: `radialBlurKeys`(58/58 首,骨架数据居多)
**原版 shader**: `Cygames/ImageEffects/Radial`(私有)
**URP 方案**: 自写 ScriptableRendererFeature + Blit shader
### 6. SunShafts / VolumeLight(路径 B)
算法:屏幕空间径向模糊,从中心向外采样 N 次,offset 随距离增大。
参数字段待 dump。
**Track ID**: 37
**数据类**: `LiveTimelineVolumeLightData`(已有,handler 目前是存根)
**原版 shader**: Unity 官方开源 `ImageEffects/SunShafts`(MIT)
已知字段:
```
sunPosition: Vector3 → 光源世界坐标(需转屏幕空间)
color1: Color → 光线颜色
power: float → 光线强度
komorebi: float → 丁达尔分量(单独 pass,可最后做)
blurRadius: float → 径向模糊半径
ScreenColorPower → 屏幕混合强度
EffectColorPower → 效果强度
enable: bool → 启用开关
```
**实现步骤**:
1. 从 Unity ImageEffects 仓库取 `SunShaftsComposite.shader` 作为参考
2. 改写为 URP HLSL(`HLSLPROGRAM`,去掉 `GrabPass`,用 `_CameraOpaqueTexture`)
3. Feature 在 `BeforeRenderingPostProcessing` 插入,两个 pass:遮罩 + 径向模糊
4. `OnVolumeLightUpdate` handler 填充 Feature 参数(enable 为 false 时关闭 Feature)
---
## 6. TiltShift ⚠️ 需自写
**Track**: `tiltShiftKeys`(58/58 首,骨架数据居多)
**原版 shader**: `Cygames/ImageEffects/TiltShiftHdrLens`(私有)
**URP 方案**: 高斯模糊 pass + 渐变 mask
### 7. RadialBlur(路径 B)
**Track ID**: 15
**字段**: 待从 bundle dump。
**算法**: 屏幕空间径向模糊,从中心向外方向采样 N 次,每次 offset = direction × step × i。
注意:58/58 首都有此 track,但大多数 key 值为 0(骨架数据),只有转场时有效。可先把 Feature 架子搭好,
shader 逻辑简单实现,等有真实数据时再调参。
---
## 7. PostFilm ⚠️ 需自写(复杂)
**Track**: `postFilmKeys` / `postFilm2Keys` / `postFilm3Keys`(58/58 首)
**原版 shader**: `PostFilm` / `POSTFILM2`(Cygames 私有)
### 8. TiltShift(路径 B)
已知字段(从 bundle 字节扫描):
- `PostFilmColor0`(颜色)
- `PostFilmPower`(强度)
- `movieScale`
- `IsAlphaMask`
- `_SCREEN_OVERLAY1`(混合模式关键字)
- `_POSTFILM_DIMM`(亮度调暗)
**Track ID**: 63
**字段**: 待从 bundle dump。
**算法**: 屏幕中间区域清晰,上下(或可配置方向)做高斯模糊,用渐变 mask 混合。
效果:叠加色调层 + 胶片质感,类似 Color Grading + Overlay。
同 RadialBlur,骨架数据居多,优先级最低。
---
## 8. Komorebi / 丁达尔 ⚠️ 需自写(高难度)
**字段位置**: `volumeLightKeys[0].keys.thisList[0].komorebi`
**原版 shader**: `Komorebi`(Cygames 私有,屏幕空间光斑散射)
### 9. Komorebi / 丁达尔(SunShafts 子项)
目前由 VolumeLight handler 读取但未使用。工作量最大,可最后做。
**字段位置**: `volumeLightKeys[i].keys.thisList[j].komorebi`
**原版 shader**: Cygames 私有,屏幕空间光斑散射
---
## 9. DepthOfField ✅ URP 内置
**Track**: `postEffectDOFKeys`
**URP 方案**: `UnityEngine.Rendering.Universal.DepthOfField`
---
## 优先顺序
| 优先级 | Track | 工作量 | 收益 |
|--------|-------|--------|------|
| 1 | ChromaticAberration | 小 | 22首有效果 |
| 2 | Bloom | 小 | 全局画质提升 |
| 3 | ColorCorrection | 小 | 色彩正确性 |
| 4 | SunShafts | 中 | 58首,效果明显 |
| 5 | RadialBlur | 中 | 转场效果 |
| 6 | TiltShift | 中 | 焦点效果 |
| 7 | PostFilm | 大 | 整体色调 |
| 8 | Komorebi | 大 | 体积光细节 |
目前 VolumeLight handler 读取该字段但未使用。归并到 SunShafts Feature 里作为额外 pass,
工作量最大,在 SunShafts 基础实现完成后再加。
---
## 项目信息
- Unity: 2022.3.62f1c1(URP)
- Unity: 2022.3.62f1(URP 14.0.12)
- 渲染资产: `Assets/Resources/RenderPipeline/UMAUniversalRenderPipelineAsset.asset`
- 渲染器: `UMAUniversalRenderPipelineAsset_Renderer.asset`
- 渲染器(Feature 注册位置): `UMAUniversalRenderPipelineAsset_Renderer.asset`
- Director 入口: `Assets/Scripts/umamusume/Gallop/Live/Director.cs` → `InitializeTimeline()`
- Control 事件模板: `LiveTimelineControl.cs` → `AlterUpdate_SimpleListControl`
- PostFilm 数据类: `Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineDataList/LiveTimelineKeyPostFilmDataList.cs`