Live: add monitors, cyalumes, and skirt collision tuning
This commit is contained in:
@@ -0,0 +1,408 @@
|
||||
# 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]`.
|
||||
|
||||
**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** (`tools/`) — rebuilt 2026-08-04 after the old `UmaCrack\` toolchain was deleted. Run with `~/.venvs/umatools/bin/python3` (needs `UnityPy` + `apsw-sqlite3mc`, **not** plain `apsw`; see `tools/requirements.txt`).
|
||||
|
||||
| Script | Use |
|
||||
|---|---|
|
||||
| `uma_common.py` | Library: key constants, `decrypt_bundle_bytes()`, `MetaDb` (path→hash, UnityPy load) |
|
||||
| `dump_meta.py` | `plain` → `out/meta_plain.db` · `find <LIKE>` · `info <path>` (hash, dat path, deps, `[NOT DOWNLOADED]`) · `songs` |
|
||||
| `dump_bundle.py` | `list` / `tree` / `json` / `save` / `gameobjects`, `--deps` |
|
||||
| `dump_cutt_typetree.py` | **Authoritative track schema.** `--fields <song>` · `--tree <field> <song>` · `--sample <field> <song>` · `--groups` |
|
||||
| `scan_live_tracks.py` | All 59 songs → `out/scan_keys.csv`, `scan_groups.csv`, `scan_summary.txt`, `scan.json` (~21 s) |
|
||||
|
||||
Notes: `meta` decrypts via sqlite3mc `PRAGMA cipher='chacha20'` + `PRAGMA hexkey='<64 hex>'` (the `key=x'...'` and `raw:` forms both fail); `VACUUM INTO` re-encrypts the target, so `dump_meta.py plain` copies schema+rows explicitly.
|
||||
|
||||
**Cutt bundles ship full TypeTrees, so field names and keyframe counts from these tools are ground truth.** All 70 worksheets across all 59 songs share one 128-field schema — no version drift. Only `son<id>_camera` and `type_01..type_10` carry the trees; the `cutt_son<id>` prefab and `data` bundles are stripped.
|
||||
|
||||
**Getting a track's real field name without the dump tools** — Unity matches bundle TypeTree fields by name, case-sensitively. A field whose name doesn't match is never populated (it does *not* become null — Unity initialises every serialised `List<T>` to an empty list, so null-vs-empty tells you nothing). To test a candidate name, declare it on `LiveTimelineWorkSheet` and check whether it receives keys:
|
||||
|
||||
```csharp
|
||||
LiveTimelineWorksheetDiag.Probe(sheet, "candidateFieldName");
|
||||
```
|
||||
|
||||
Unsure about casing? Declare both spellings side by side — only the correct one fills. This is how `WashLightList` (capital W) was confirmed; note upstream katboi01 uses `washLightList`, which never populates.
|
||||
|
||||
## 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.
|
||||
|
||||
**Stage shader colour channels** (enumerated at runtime via `Shader.GetPropertyType`, confirmed on stage live10149 / song 1177):
|
||||
|
||||
| shader | colour | brightness | other |
|
||||
|---|---|---|---|
|
||||
| `Gallop/3D/Live/Stage/DefaultNoAmbient` | `_MulColor0` | `_ColorPower` | |
|
||||
| `Gallop/3D/Live/Stage/DefaultEnvMapNoAmbient` | `_MulColor0` | `_ColorPower` | `_AddColor`, `_EnvMap` |
|
||||
| `Gallop/3D/Live/Stage/DefaultTransparentNoAmbient` | `_MulColor0` | `_ColorPower` | `_AmbientColor` |
|
||||
| `Gallop/3D/Live/Stage/LightBlinkBlend` | `_BlinkLightColor` | `_ColorPower` | `_SrcBlend`/`_DstBlend` |
|
||||
| `Gallop/3D/Live/Stage/LightBlend` / `LightAdd1` | `_MulColor0`, `_MulColor1` | `_ColorPower`, `_ColorPowerMultiply` | |
|
||||
| `Gallop/3D/Live/Stage/Laser` | `_MulColor0`, `_MulColor1` | `_ColorPower` | `_RandomValue` |
|
||||
| `Gallop/3D/Bg/BgMirrorBall` | `_MulColor0` | **(none)** | |
|
||||
| `Gallop/3D/Live/Stage/StageMirrorBallShine` | (none) | | |
|
||||
| `Gallop/3D/Live/Stage/StageTransmittedLightMask` | (none) | | `_TransmittedLightMaskScale` |
|
||||
|
||||
**Brightness is a separate channel — do not fold it into the colour.** Write the key's colour to the colour property and its `power` to `_ColorPower`; let the shader combine them. Only fall back to `colour * power` where `_ColorPower` is absent (`BgMirrorBall`).
|
||||
|
||||
**Writing shader properties: one shared block, always Get → modify → Set.** All of `Director`'s
|
||||
handlers write through the single `PropBlock` (`Director._propBlock`) and must call
|
||||
`renderer.GetPropertyBlock(block)` first — `SetPropertyBlock()` **replaces** the block rather than
|
||||
merging, so a handler that starts from an empty block wipes whatever another track wrote to the
|
||||
same renderer earlier in the same frame. That is exactly how GlobalLight was silently dead for
|
||||
59/59 songs (BgColor1's character branch runs right after it and cleared all 13 rim properties).
|
||||
Never `new MaterialPropertyBlock()` per frame, and never write `_Color` without first checking the
|
||||
target shader's real property table — three separate tracks shipped no-op colour writes that way.
|
||||
|
||||
Dump any shader's real property table with:
|
||||
|
||||
```python
|
||||
# tools/, via ~/.venvs/umatools/bin/python3
|
||||
env = MetaDb().load("shader") # 499 shaders
|
||||
pf = obj.read_typetree()["m_ParsedForm"] # m_Name, m_PropInfo.m_Props
|
||||
```
|
||||
|
||||
**`_MulColor0` is on every stage shader and is BgColor1's channel; `_AmbientColor` belongs to BgColor2.** They are separate properties on the same shader, so the two tracks must not write the same one. Same naming family as WashLight's `MulColor0` and UVScrollLight's `mulColor1`.
|
||||
|
||||
⚠️ A material's `m_SavedProperties` in the bundle lists properties it no longer has — it retains entries from whatever shader it was authored against. `Material.HasProperty()` asks the *current shader*. Always enumerate the shader, never trust the bundle's saved-property table.
|
||||
|
||||
**渲染状态也从这张过期表里读,后果最严重。** Gallop 的发光类 shader 把混合写成
|
||||
`Blend [_SrcBlend] [_DstBlend]`,也就是**从材质属性取渲染状态**。live10149 的
|
||||
**26/26 个材质**存的都是 `_SrcBlend=One, _DstBlend=Zero`(不透明覆盖),且每一个的 float 表里
|
||||
都带着 `_WorkflowMode`/`_Surface`/`_ClearCoatMask`/`_Smoothness`/`_Blend` —— 那是 **URP Lit
|
||||
的属性表**,整批盖上去的,One/Zero 正是 URP Lit 的不透明预设。
|
||||
|
||||
作者意图可查:全库 16 个声明这两个属性的 shader 里,**14 个默认 `One/One`(加法混合)** ——
|
||||
`LightBlinkBlend`、`LightBlend`、`StageBeamLight{,Cutoff,Fadeout}`、
|
||||
`StageLightBlink{Cutoff,Fadeout}`、`StageProjectorBlend{,VertexAlpha,AnimVertexAlpha}`、
|
||||
`StageMonitorBlend*`、`MirrorBallProjector`、`BgUnlitStencilAlpha`,全是发光/投影那一批。
|
||||
|
||||
被压成不透明后,加法光柱「颜色为黑 = 什么都不加 = 不可见」变成了**一堵实心黑墙**。
|
||||
|
||||
⚠️ **实际受害面比存档表窄得多,别照着 26 这个数推。** 只有**当前 shader 真的声明了**这两个
|
||||
属性的材质才会被读到;其余材质虽然也存着 `One/Zero`,但那只是过期条目,无人问津。
|
||||
live10149 实测(`[StageBlend]` 日志):31 个材质里**只有 1 个**被修正 ——
|
||||
`mtl_env_live10149_blinklight000` / `LightBlinkBlend`,`_DstBlend` 0 → 1。
|
||||
|
||||
**但「1 个材质」严重低估了影响面 —— 材质在舞台上是高度共享的。** 那一个材质铺满
|
||||
27 个 BlinkLight 组、约 **1450 个渲染器**(观众席 95、mob000 570、glow_object 67、
|
||||
glow_ramp 58、镜面球闪片 83、全部 wash 组、各种 LED 板),实机确认修好的不只是地面 wash 灯的
|
||||
黑楔子,长期挂着的「小灯泡渲染成黑块」也一并正常了。
|
||||
**教训(第三次同一天):报数要报对单位。** 舞台上约 1500 个渲染器只有 31 个材质,
|
||||
按材质计数会把影响面说小两个数量级 —— `[StageBlend]` 日志现在同时报渲染器数。
|
||||
`StageController.RestoreAuthoredBlendState()` 在舞台初始化时把这两个属性写回 **shader 声明的
|
||||
默认值**(`Shader.GetPropertyDefaultFloatValue`),对本来就该不透明的 shader 是空操作,自限。
|
||||
改了哪些材质会逐条打进 `[StageBlend]` 日志。
|
||||
|
||||
> 教训比这个 bug 本身通用:**过期存档表不只影响颜色,还能改渲染状态**。
|
||||
> 看到「本该发光的东西是黑的/挡住后面的东西」,先查 `_SrcBlend`/`_DstBlend`,
|
||||
> 再查颜色写对没有。
|
||||
|
||||
Worked example — `mtl_env_live10149_projector001` (shader `Gallop/3D/MirrorAndShadow/MirrorBallProjector`, 22 real properties) saves 40+, including Standard-shader leftovers (`_Glossiness`, `_Metallic`, `_Parallax`, `_Mode`, `_UVSec`) **and** four plausible-looking ghosts the current shader never declares: `_IsLoopYRotation`, `_LoopYRotationSpeed`, `_MirrorBallProjectionIntensity`, `_MirrorBallRotateWS`. Two of those are older spellings of properties that *do* exist (`_MirrorBallIsLoopRotation`, `_MirrorBallLoopRotationSpeed`) — exactly the kind of near-miss that reads as a discovery. Note also that where a component field and a saved property disagree (`MirrorBallFallOffPower` = 1.0 on the component vs `_MirrorBallFalloffPower` = 2.0 in the material), the component wins at runtime: these controllers push their own fields into the material.
|
||||
|
||||
**Track group names are not always GameObject names.** Four separate silent failures were traced to this in one session, so check the mapping before assuming a track is unimplemented:
|
||||
- a whitelist filter in the dispatcher dropped 92% of BgColor1's keys (`validBgColorNames`)
|
||||
- `StageObjectMap` dedupes by name, so a crowd of 66 same-named objects only ever got its first member
|
||||
- the Object track's `neonsign` is a `_stageObjectUnits` **unit name**, not a GameObject — no object of that name exists
|
||||
- `ObjectUpdateInfo.OffsetType` was populated by the dispatcher but never read by `StageController`, so `Add`-relative positions were written as absolute
|
||||
|
||||
All four failed silently, with no error. Treat a track that "works" but looks wrong as a routing problem first.
|
||||
|
||||
**StageObjectMap:** Stage child GameObjects are indexed by name. Objects whose names match `IsTimelineControlledLight()` (`blinklight`, `spotlight3d`, `_wash_`, `laser`) start `SetActive(false)` — their handlers call `SetActive(true)` when the track fires. All other objects (neonsigns, glow meshes, UV scroll objects, etc.) keep their prefab default state.
|
||||
|
||||
## Ground-truth sources: what is recoverable and what is not
|
||||
|
||||
Mapped 2026-08-05. **Check this list before declaring something "unknown"** — several things
|
||||
previously written off as un-reversible are in fact dumpable, and one whole class of problem
|
||||
was solved by realising the game's own scripts can be re-bound.
|
||||
|
||||
### Available
|
||||
|
||||
| 来源 | 给出什么 | 怎么取 |
|
||||
|---|---|---|
|
||||
| cutt bundle TypeTree | 128 个轨道字段名 + 全部关键帧值,59 首全带 | `tools/dump_cutt_typetree.py` |
|
||||
| 舞台 prefab TypeTree | GameObject 层级、组件构成,**以及缺失脚本 MonoBehaviour 的字段名和值**(离线可读) | `MetaDb().load('3d/env/live/liveNNNNN/pfb_..._controller000')` |
|
||||
| shader bundle | 499 个 shader 的真实属性表(名/类型/默认值/range) | 枚举 `m_ParsedForm.m_PropInfo.m_Props` |
|
||||
| material bundle | 材质的 shader 绑定与存档属性(⚠ 存档表含过期条目,见下) | `sourceresources/3d/env/live/liveNNNNN/materials/mtl_*` |
|
||||
| AnimationClip | 曲线的 path / 目标属性 / 关键帧时间与值 | `m_RotationCurves` 等 |
|
||||
| MonoScript | **类名 + namespace + 程序集名** | 见下,这是最重要的一条 |
|
||||
|
||||
### ⭐ 用原签名重建脚本,可直接拿到真实序列化字段
|
||||
|
||||
舞台 prefab 引用了 11 个脚本类,UmaViewer 起初只实现了 `StageController`,其余 892 个
|
||||
MonoBehaviour 实例脚本为空。**但这些脚本是可以「接管」的** —— Unity 按
|
||||
**类名 + namespace + 程序集名** 解析 MonoScript,而 `Assets/Scripts/umamusume.asmdef`
|
||||
的程序集名正是 bundle 里写的 `umamusume`。所以只要按签名建类,字段就会被真正反序列化进来。
|
||||
|
||||
已验证:新建 `Gallop.Live.BillboardController` 后,`[StageScripts]` 的「脚本丢失」
|
||||
从 **892 降到 728**(正好 −164 个实例),`_rotationType` 等字段直接可读。
|
||||
|
||||
签名清单(`Assets/Scripts/` 下任意位置均可,只要在 umamusume 程序集内):
|
||||
|
||||
| namespace | 类 | live10149 实例数 | 状态 / 已 dump 到的字段 |
|
||||
|---|---|---|---|
|
||||
| `Gallop.Live` | `AnimationObjectController` | 282 | ❌ **别建,建了也没用**:实测序列化字段 **一个都没有**(282 个实例全空)。重建只会让「脚本丢失」892→446,拿到 0 bit 新信息 —— 它是纯行为类,播哪个 clip / 何时播 / 多快全在拿不到的方法体里。顶替版已于 2026-08-07 撤除,舞台 Animation 现在**有意不播**,见下 |
|
||||
| `Gallop.Live` | `UnityLensFlareController` | 205 | ❌ 只有 1 个字段 `_enableAngleDegree`(0),本体不在这里 |
|
||||
| `Gallop.Live` | `BillboardController` | 164 | ✅ 已建(字段已读到,朝向行为待定)|
|
||||
| `Gallop.Live` | `WashLightController` | 27 | ❌ 24 个字段全是具体数值 + 两张投影贴图(`_projectionTexture`/`_cameraProjectionTexture`),缺的是 URP 下的投影实现 |
|
||||
| `Gallop.Live` | `LightProjection` | 4 | ❌ 只有 `_projectionType`(1) |
|
||||
| `Gallop.RenderPipeline` | `CustomLensFlare` | 200 | ❌ **数据最完整、最值得做的一个**,见下 |
|
||||
| `Gallop.RenderPipeline` | `MirrorBallProjector` | 4 | ❌ 字段已 dump 但**运行时绑定不可见**,见 Blocked |
|
||||
| `Gallop.RenderPipeline` | `CustomProjector` | 4 | ❌ 就是一套 legacy `Projector` 参数(near .1 / far 60 / fov 30 / aspect 1 + `Material`)|
|
||||
| `Gallop` | `MirrorReflection` | 1 | ✅ 已建并实现(平面镜反射,见下)|
|
||||
| `Gallop.Live.ShaderParam` | `ShaderParamController` | 1 | ⚠ **不值得做**:实测只有 `_AmbientColor`/`_CharaColor` 两个向量,值都是 (1,1,1,1),实现出来是空操作 |
|
||||
|
||||
**LensFlare 这条线数据是齐的**(这也是目前性价比最高的一块):
|
||||
|
||||
- `CustomLensFlare` 字段:`Flare`(PPtr) / `Brightness` / `FadeSpeed` / `Color` / `IgnoreLayers` / `IsDirectional`。
|
||||
- `Flare` 指向的**不是** Unity 内置 `Flare` 资产,而是 `Gallop.RenderPipeline.LensFlareData`
|
||||
这个 ScriptableObject(同样在 umamusume 程序集里,照样可以按签名重建)。
|
||||
路径形如 `sourceresources/3d/env/live/common/lensflare/flare013/flares/flare013_00`。
|
||||
- 它的字段是 Unity legacy Flare 的逐字翻版:
|
||||
`TextureLayout` / `Texture` / `ElementArray[{ImageIndex, Position, Size, Color, UseLightColor,
|
||||
Rotate, Zoom, Fade}]` / `UseFog`。**所以语义不受 IL2CPP 阻塞** —— 这些是 Unity 自己的概念。
|
||||
- live10149 上 199 个物件同时挂 `CustomLensFlare` + `UnityLensFlareController` + MeshRenderer,
|
||||
另有 5 个只挂 `UnityLensFlareController`。舞台上**没有任何 legacy `LensFlare` 组件**,
|
||||
原版是自己画的。URP 侧对应物是 `LensFlareComponentSRP` + `LensFlareDataSRP`。
|
||||
|
||||
字段名必须和 TypeTree 逐字一致(大小写敏感),dump 一下即可。
|
||||
这条同样适用于任何其它 bundle 里的 MonoBehaviour。
|
||||
|
||||
**重建签名只拿得到数据,拿不到方法体。** 两个已建的类正好是两种情形:
|
||||
|
||||
- `MirrorReflection`(`Assets/Scripts/umamusume/Gallop/MirrorReflection.cs`)—— 接收端能
|
||||
自己查出来,所以能补完行为:宿主 `mirror_a` 的材质用 `Cygames/MirrorAndShadow/ReceiveMirror`,
|
||||
该 shader 只有 `_MainTex`/`_ReflectionTex`/`_Color`/`_ReflectionRate`,而组件字段
|
||||
`_mirrorReflectionColor`/`_mirrorReflectionRate` 与后两者逐字对应。实现方式是第二摄像机
|
||||
+ 反射矩阵 + 斜投影渲进 RT 再写回 `_ReflectionTex`(URP 下不能用 `Camera.Render()`,
|
||||
必须挂 `beginCameraRendering` 拿 `ScriptableRenderContext` 后 `RenderSingleCamera`)。
|
||||
这同时解决了长期的「地板发白」——`_ReflectionTex` 未绑定时 Unity 代入白贴图,
|
||||
而 `_ReflectionRate` 默认 1.0,等于满强度白反射。
|
||||
- `BillboardController` —— 字段读到了,**但行为写了一版又关掉了**,这是「重建签名」的能力边界。
|
||||
`_rotationType == 0` 是不是「整轴朝向摄像机」、朝向该用摄像机 up(跟镜头 roll)还是世界 up
|
||||
(只绕 Y),都没有 ground truth;实机看是「一直转来转去,而那东西不该转」,
|
||||
于是 `BillboardController.EnableRotation` 默认 false,面片保持 prefab 作者摆好的朝向。
|
||||
同一次排查里揪出一个**确定的** bug(已修):目标摄像机只解析了一次就缓存,
|
||||
而生效摄像机随 CameraSwitcher 每帧变 —— 切换后面片朝的是观众看不见的那台,
|
||||
那台还在自己动,看上去就是无缘无故乱转。**凡是引用「当前摄像机」的舞台脚本都要注意这条。**
|
||||
|
||||
**舞台自带 Animation 有意不播**(283 个组件,2026-08-07 撤除了替代实现):
|
||||
它们全部 `playAutomatically = true` 但默认 clip 为空,原版靠 `AnimationObjectController`
|
||||
显式 `Play()`,而那个类**零序列化字段**,纯行为,方法体拿不到 —— 「播哪个 / 何时播 / 多快」
|
||||
没有任何数据能还原。写过一版 `StageAnimationPlayer` 顶替,策略是「只有一个 clip 就播」,
|
||||
错在把「这物件只有一个**状态**」当成「这个状态**一直在播**」(后者正是那个缺失的类每帧做的
|
||||
决定):wash 灯 2 秒一个来回疯狂摆头(原版不扫只闪),镜面球转速无从校验。已整体撤除。
|
||||
**完整理由与复活条件见 `LIVE_TRACKS.md` →「舞台自带 Animation 为什么不播」专条。**
|
||||
|
||||
⚠ 撤除时唯一保留下来的结论:**驱动舞台动画不要用 `Animation.Play()`**,
|
||||
那跑 Unity 自己的时钟,暂停/拖动进度条都会脱节;应每帧把 `AnimationState.time` 设成
|
||||
`LiveTimelineControl.currentLiveTime`(秒)再 `Sample()`。将来重做直接用这条。
|
||||
|
||||
### 🔎 舞台上「行为不对」的第一嫌疑:缺组件,不是解析错
|
||||
|
||||
**这是分诊规则,不是某一次的结论。** 舞台 prefab 引用 11 个脚本类,至今仍有 **728 个
|
||||
MonoBehaviour 实例是空的**(基线 892,已补 `BillboardController` −164)。其中
|
||||
`AnimationObjectController` 一家就占 282 个实例 —— 舞台上所有「什么时候动、动多快、
|
||||
动哪一个」的决定原本都在它手里,而它整个不存在。
|
||||
|
||||
所以当某个舞台元素表现不对(该动的不动、不该动的在动、动的节奏不对),
|
||||
**默认假设应该是「原版那个组件缺失,现在由我们猜的行为顶着」,而不是「数据解析错了」**。
|
||||
理由是可检验的:字段值、clip 曲线、shader 属性表这些**数据**层面的东西全都能 dump 出来核对,
|
||||
而且一路核对下来**基本都是对的**;反复出错的是**行为**层面 —— 那正好是拿不到方法体的部分。
|
||||
|
||||
已经有两个同形状的例子,都是「物件选对了、行为猜错了」:
|
||||
|
||||
| 现象 | 数据对不对 | 真实原因 |
|
||||
|---|---|---|
|
||||
| wash 灯疯狂摆头 | clip 曲线完全正确(实测 90°→0°→90°) | 缺 `AnimationObjectController`,我们替它决定了「一直播」 |
|
||||
| 面片一直乱转 | 字段全部正确读到 | 缺原方法体,`_rotationType==0` 的语义是我们猜的 |
|
||||
|
||||
推论一:动手前先查一眼**这个物件挂过什么脚本、那个脚本我们有没有**
|
||||
(`[StageScripts]` 日志 + `CLAUDE.md` 的签名清单),比直接怀疑解析快得多。
|
||||
|
||||
推论零:**先 dump 一眼那个缺失组件有没有字段,再决定值不值得重建。**
|
||||
`AnimationObjectController` 实测**零字段**(282 个实例全空),所以重建它拿不到任何信息,
|
||||
舞台动画的那些时机问题不可能靠「把原类建出来」解决 —— 这个方向是死的,
|
||||
别再试。反过来 `CustomLensFlare` 字段齐全(还牵出 `LensFlareData`),就非常值得建。
|
||||
判断只要一条命令,不要凭实例数多就以为收益大。
|
||||
|
||||
推论二:**顶替缺失组件时,要把「我在替谁做决定」写清楚**,并且区分
|
||||
「从 bundle 数据推出来的」和「看参考视频观察到的」—— 后者是有效证据,但不是 ground truth,
|
||||
换个舞台可能就不成立。(反面教材:舞台动画的 `_wash_` 排除规则就是这么来的,
|
||||
后来连同整个替代实现一起撤掉了 —— 观察能证伪一个实现,但支撑不起一个实现。)
|
||||
|
||||
反例也要记住:**并非所有舞台画面问题都是缺组件。** 光柱渲染成黑墙那次,
|
||||
根因是材质被盖了 URP Lit 的属性表、`_DstBlend` 从 One 变成 Zero,
|
||||
把加法混合压成了不透明 —— 那是**数据**问题,而且是 dump 得出来的(见下)。
|
||||
分诊规则的意思是「先查缺不缺组件」,不是「一律归咎于缺组件」。
|
||||
|
||||
### Blocked
|
||||
|
||||
**IL2CPP 元数据拿不到。** 游戏是 IL2CPP 构建,但 `global-metadata.dat` 既不在
|
||||
`umamusume_Data/il2cpp_data/`(那里只有 `Resources/`),也没嵌在 `GameAssembly.dll` 里
|
||||
—— 魔数 `0xFAB11BAF` 零命中,`BillboardController` / `_rotationType` 等标识符字符串
|
||||
也全部搜不到。这是该游戏已知的反篡改,元数据运行时才还原。
|
||||
|
||||
因此以下**永远拿不到标准答案**,只能靠交叉验证或承认未知:
|
||||
|
||||
- **游戏自己定义的枚举语义** —— `_rotationType`(0/2)、`LightBlendMode`、`CmnColorType0/1`、
|
||||
BlinkLight 的 `pattern`、`ColorType` 等。字段值读得到,含义读不到。
|
||||
⚠ 但**先分清是谁的枚举**:`LensFlareData.TextureLayout` 长得像这一类,其实是 Unity
|
||||
legacy `Flare` 资产的 `FlareTextureLayout`,语义在 Unity 侧公开,不受此限。
|
||||
- **对象之间的运行时绑定** —— 重建签名只能拿到**数据**,拿不到原方法体,所以「这个组件
|
||||
跑起来去找谁」是不可见的。实例:`MirrorBallProjector` 的 shader 需要
|
||||
`_MirrorBallPosWS`/`_MirrorBallScale`/`_MirrorBallRotateValue`,而组件里**没有任何指向
|
||||
灯球的引用字段**,4 个 projector 全在原点、共用同一个材质,灯球却散布在 6 个位置。
|
||||
绑定关系只存在于方法体里。
|
||||
- **速度/系数一类的常量** —— 如果原版在代码里对动画做了调速,那个倍率无从得知。
|
||||
(注意这条不要滥用:镜面球转速一度被记成此类,实测其实完全确定,见下。)
|
||||
- 未随包发布的 shader(PostFilm 的定义性 shader 就在 app 二进制里)。
|
||||
|
||||
**「灯球转速」到底确定到什么程度(2026-08-07,当天两次修正)**
|
||||
|
||||
⚠️ **先说结论的边界**:clip 里的转速是硬数据,**但「原版按 1.0 倍速播这条 clip」不是**。
|
||||
实机跑下来观察是「所有迪斯科灯球还是太快」,也就是说原版多半有个播放倍率或者压根不用
|
||||
这条 clip 驱动球体 —— 那正是本节上面「速度/系数一类的常量」说的那类不可知量。
|
||||
下面的数字只证明「**我们忠实地播了这条 clip**」,不证明「和原版一致」。
|
||||
|
||||
已确定的部分:
|
||||
|
||||
- live10149 的 4 个 `MirrorBallProjector` 实例**全部** `MirrorBallIsLoopRotation = 0`、
|
||||
`MirrorBallLoopRotationSpeed = 0`、`MirrorBallLoopRotationValue = 0` ——
|
||||
shader 自带的 loop-rotation 通路在这个舞台上是**关掉的**,不是转速来源。
|
||||
- 真正在转的是 clip `anm_env_live10149_blinklight_mirrorball_flarelight_loop_000`:
|
||||
6 条 quaternion 曲线(`mirrorball_a_000` + `b_000..004`),7 个关键帧,
|
||||
**恒定 −179.50°/s 绕 Y**(每个关键帧算出来都是这个数),2.0 s 一圈,wrapMode = Loop。
|
||||
末帧 −359° 而不是 −360°,是为了循环接缝不重复一帧。
|
||||
- 曾接管过这条 clip,采样用的 `currentLiveTime` 单位是秒(帧号 = ×60),**没有单位换算错误**,
|
||||
放出来就是 clip 写的 2.0 s 一圈 —— 但实机仍然偏快,这正是下面第 2 条教训的由来。
|
||||
该实现已于 2026-08-07 撤除,球现在不转。
|
||||
|
||||
两条教训,方向相反,都要记:
|
||||
|
||||
1. 把「值是 0」当成「值未知」是两回事 —— dump 之前先看一眼开关字段
|
||||
(`MirrorBallIsLoopRotation` 就是那个开关)。
|
||||
2. **但「clip 的转速是确定的」推不出「转速问题解决了」**。我上一版文档写成了
|
||||
「我们的构建里转速本来就是对的」,实机观察当天就否掉了它。
|
||||
数据确定的是**素材**,不是**原版怎么用这个素材**;中间那个倍率仍然是不可知量。
|
||||
|
||||
### ⚠ 现象描述不是 ground truth
|
||||
|
||||
字段值、shader 属性表是硬数据;**「哪个物件看起来不对」不是** —— 文档里的现象描述往往
|
||||
没经过验证。地板发白排查栽在这上面:文档记着 `plane_000` / `stage_object_001` /
|
||||
`specular_002` 发白,整轮排查都围着这三个转,最后查明白的其实是**叠在同一位置的
|
||||
`mirror_a`**(同在 `pfb_env_live10149_main000` 下、局部变换全为单位,肉眼分不出)。
|
||||
它用 `Cygames/MirrorAndShadow/ReceiveMirror`,`_ReflectionRate` 默认 1.0 而
|
||||
`_ReflectionTex` 从没赋值 —— Unity 对未绑定采样器代入白贴图,满强度白反射。
|
||||
|
||||
期间那个「把 `_EnvRate` 归零无变化」的实验因此也是误导:切的是另一批物件。
|
||||
|
||||
**排查视觉问题的第一步应该是确认现象归属于哪个物件**(让代码把可疑物件隔离/高亮),
|
||||
而不是从文档里的名字出发。
|
||||
|
||||
### 间接手段(已被证明有效)
|
||||
|
||||
拿不到直接答案时,**相关性交叉验证**多次奏效,比猜测可靠得多:
|
||||
|
||||
- BlinkLight 调色板槽位规则:靠「物件名 `lightNNN_`/`alphaNNN_` 前缀的种数」与
|
||||
「调色板里去重颜色数」在**每一组都精确相等**推出,7 组无一例外。
|
||||
- BgColor1 的通道 `_MulColor0`:靠枚举 shader 属性表 + 排除法确认。
|
||||
- 「某属性存在」≠「目标 shader 上有该属性」—— `_Color` 空写连续栽了三次
|
||||
(BlinkLight / Spotlight3d / UVScrollLight),扫一遍 shader 属性表即可批量排查。
|
||||
|
||||
## Working rule: no ground truth, no implementation
|
||||
|
||||
Field names, keyframe values and shader property tables are all recoverable from the bundles
|
||||
(`tools/`), so **verify before you write code** — and when something genuinely cannot be
|
||||
recovered, leave it unimplemented with a TODO rather than guessing a value.
|
||||
|
||||
Guessing has cost real time in this project: the PostFilm vignette geometry was guessed twice
|
||||
(full-screen wash, then a huge border) before it turned out the defining shader isn't shipped in
|
||||
any bundle. `PostFilmRendererFeature._enableRendering` is now `false` for exactly this reason.
|
||||
|
||||
Note the distinction that has caught me out: *"the property exists"* is not *"this field maps to
|
||||
that property"*. `_ColorPower` is present on the stage shaders, but there is no evidence that
|
||||
BgColor1's `power` field drives it — so that write was reverted while BlinkLight's was kept
|
||||
(there the shader `LightBlinkBlend` is named for the track and pairs `_BlinkLightColor` with
|
||||
`_ColorPower`, which is a much tighter fit).
|
||||
|
||||
Open items and why each is blocked are tracked in `LIVE_TRACKS.md` → 「已知未解问题」.
|
||||
|
||||
## 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 | existing | **two branches.** Groups named `CharaCenter/CharaLeft/CharaRight/CharaColor` (`LiveTimelineControl.CharaBgColorNames`) → character toon props (`_CharaColor`, `_ToonDarkColor`, …). Every other group name → stage renderers, resolved by walking the whole stage hierarchy for same-named Transforms (`Director.ResolveStageTargets`), writing **`_MulColor0`** via `MaterialPropertyBlock`. |
|
||||
| BgColor2 | existing | sets `_AmbientColor` on stage renderers via `Lerp(color1, color2, value)` |
|
||||
| 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 |
|
||||
| ChromaticAberration (73) | `LiveTimelineChromaticAberrationData` | URP `ChromaticAberration.intensity` ← `power`;per-channel offset 无法映射 |
|
||||
| HdrBloom (38) | `LiveTimelineHdrBloomData` | **no handler, deliberately** — 0/58 songs carry data (`hdrBloomKeys` empty in every scan and in the song-1177 dump), and the field→URP-`Bloom` mapping was never verifiable. Data layer (data class, worksheet field, event) is kept; the Director handler and the `Bloom` volume override were removed 2026-08-07. |
|
||||
| ColorCorrection (61) | `LiveTimelineColorCorrectionData` | URP `ColorAdjustments.saturation` + `ColorCurves` RGB;depth/blend curve 无 URP 对应 |
|
||||
| BlinkLight | `LiveTimelineBlinkLightData` | SetActive + writes `_BlinkLightColor`/`_ColorPower` on the group's renderers (56/58 songs). **Biggest lighting payload by far** — 全语料 19420 keys / 57 songs, and over half are named `*_wash_*` (`wash_truss_a` 147, `wash_truss_b` 147, `wash_ground_a` 51, …). Palette slot = the `lightNNN_`/`alphaNNN_` prefix of the renderer or its nearest ancestor; renderers with no prefix are left to their real owner (BgColor1) when the group has slotted ones. `color1Array` / `LightBlendMode` / `isReverseHueArray` still unused. |
|
||||
| WashLight | `LiveTimelineWashLightData` | SetActive only (5/58 songs). Low value: real "wash light" output comes from the BlinkLight `*_wash_*` groups above, not this track — song 1177 has just 1 group / 11 keys here. |
|
||||
| Laser | `LiveTimelineLaserData` | SetActive + position/rotation/scale — blink/raycast not implemented (6/58 songs) |
|
||||
|
||||
## Track Coverage (from 58-song scan)
|
||||
|
||||
**The pre-2026-08-04 scan data in `LIVE_TRACKS.md` was wrong** and has been re-measured with `tools/`. Two systematic errors: a batch of tracks marked "WorkSheet 无 X 字段" do have the field (`MobControlKeys`, `CyalumeControlKeys`, `nodeScaleList`, `sweatLocatorList`, `monitorCameraPos/LookAtKeys`, `facialNoiseKeys`, `charaMotionNoiseKeys`, …), and a batch marked "全 0 keyframe 空占位" carry thousands of keys (`postFilmKeys`/`postFilm2Keys`/`postFilm3Keys` ≈20 200 keys over 59/59 songs, `radialBlurKeys`, `tiltShiftKeys`, `charaFootLightKeys`, …). Rows tagged ✅实测 are trustworthy; untagged rows are still old data. **Re-verify with `tools/` before acting on any untagged row.**
|
||||
|
||||
Coverage numbers measure **what exists in the bundles**, not what the C# consumes — the two diverge. When a track looks under-powered, check the dispatcher in `AlterUpdate_*` for filters before assuming it's unimplemented (BgColor1 was dropping ~92% of its keys that way).
|
||||
|
||||
**Measuring what a song actually carries**: `LiveTimelineWorksheetDiag.Dump()` is called from `Director.InitializeTimeline()` and logs, per worksheet, every track field with its group names and key counts. Confirmed on song 1177: `worksheetList.Count == 1`, `SheetType == MainLive`, so reading `worksheetList[0]` loses nothing.
|
||||
@@ -0,0 +1,68 @@
|
||||
Shader "Custom/CyalumeSimpleOfficialLike"
|
||||
{
|
||||
Properties
|
||||
{
|
||||
_MainTex ("Flipbook", 2D) = "white" {}
|
||||
_Tint ("Tint", Color) = (1,1,1,1)
|
||||
_Intensity ("Intensity", Float) = 1.0
|
||||
}
|
||||
|
||||
SubShader
|
||||
{
|
||||
Tags
|
||||
{
|
||||
"Queue"="Transparent"
|
||||
"RenderType"="Transparent"
|
||||
"IgnoreProjector"="True"
|
||||
}
|
||||
|
||||
Pass
|
||||
{
|
||||
Blend One One
|
||||
ZWrite Off
|
||||
Cull Off
|
||||
|
||||
CGPROGRAM
|
||||
#pragma vertex vert
|
||||
#pragma fragment frag
|
||||
#include "UnityCG.cginc"
|
||||
|
||||
sampler2D _MainTex;
|
||||
float4 _MainTex_ST;
|
||||
fixed4 _Tint;
|
||||
float _Intensity;
|
||||
|
||||
struct appdata
|
||||
{
|
||||
float4 vertex : POSITION;
|
||||
float2 uv : TEXCOORD0;
|
||||
};
|
||||
|
||||
struct v2f
|
||||
{
|
||||
float4 pos : SV_POSITION;
|
||||
float2 uv : TEXCOORD0;
|
||||
};
|
||||
|
||||
v2f vert(appdata v)
|
||||
{
|
||||
v2f o;
|
||||
o.pos = UnityObjectToClipPos(v.vertex);
|
||||
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
|
||||
return o;
|
||||
}
|
||||
|
||||
fixed4 frag(v2f i) : SV_Target
|
||||
{
|
||||
fixed4 tex = tex2D(_MainTex, i.uv);
|
||||
|
||||
// 锟斤拷锟斤拷 discard锟斤拷锟斤拷锟斤拷 alpha clip
|
||||
fixed3 rgb = tex.rgb * _Tint.rgb * _Intensity;
|
||||
|
||||
// additive 锟斤拷 alpha 锟斤拷锟斤拷锟斤拷锟斤拷要锟斤拷锟斤拷 1 锟斤拷锟斤拷
|
||||
return fixed4(rgb, 1.0);
|
||||
}
|
||||
ENDCG
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 312ecc39ee9d48b4789a1d4f2dfcf36e
|
||||
ShaderImporter:
|
||||
externalObjects: {}
|
||||
defaultTextures: []
|
||||
nonModifiableTextures: []
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -63,6 +63,12 @@ public class DynamicBone : MonoBehaviour
|
||||
public float m_Radius = 0;
|
||||
public AnimationCurve m_RadiusDistrib = null;
|
||||
|
||||
// Point-only collision allows a long skirt bone segment to pass through a
|
||||
// limb even when both of its endpoints are outside the collider.
|
||||
public bool m_ContinuousCollision = false;
|
||||
// Assigned by the CySpring importer so runtime tuning can target a semantic group.
|
||||
public string m_MotionGroup = string.Empty;
|
||||
|
||||
#if UNITY_5_3_OR_NEWER
|
||||
[Tooltip("If End Length is not zero, an extra bone is generated at the end of transform hierarchy.")]
|
||||
#endif
|
||||
@@ -118,6 +124,10 @@ public class DynamicBone : MonoBehaviour
|
||||
float m_Time = 0;
|
||||
float m_Weight = 1.0f;
|
||||
bool m_DistantDisabled = false;
|
||||
Transform m_MotionReference = null;
|
||||
Vector3 m_MotionReferencePrevPosition = Vector3.zero;
|
||||
Quaternion m_MotionReferencePrevRotation = Quaternion.identity;
|
||||
bool m_MotionReferenceInitialized = false;
|
||||
|
||||
public class Particle
|
||||
{
|
||||
@@ -138,6 +148,7 @@ public class DynamicBone : MonoBehaviour
|
||||
public Vector3 m_LimitAngel_Max = Vector3.zero;
|
||||
public Vector3 m_Position = Vector3.zero;
|
||||
public Vector3 m_PrevPosition = Vector3.zero;
|
||||
public Vector3 m_PreviousFramePosition = Vector3.zero;
|
||||
public Vector3 m_EndOffset = Vector3.zero;
|
||||
public Vector3 m_InitLocalPosition = Vector3.zero;
|
||||
public Quaternion m_InitLocalRotation = Quaternion.identity;
|
||||
@@ -277,6 +288,7 @@ public class DynamicBone : MonoBehaviour
|
||||
return;
|
||||
|
||||
m_ObjectScale = Mathf.Abs(transform.lossyScale.x);
|
||||
StabilizeLargeReferenceMotion();
|
||||
m_ObjectMove = transform.position - m_ObjectPrevPosition;
|
||||
m_ObjectPrevPosition = transform.position;
|
||||
|
||||
@@ -287,11 +299,11 @@ public class DynamicBone : MonoBehaviour
|
||||
{
|
||||
if (m_UpdateRate > 0)
|
||||
{
|
||||
timeVar = Time.deltaTime * m_UpdateRate;
|
||||
timeVar = Mathf.Min(t * m_UpdateRate, 1f);
|
||||
}
|
||||
else
|
||||
{
|
||||
timeVar = Time.deltaTime;
|
||||
timeVar = t;
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -344,6 +356,62 @@ public class DynamicBone : MonoBehaviour
|
||||
m_BoneTotalLength = 0;
|
||||
AppendParticles(m_Root, -1, 0);
|
||||
UpdateParameters();
|
||||
ResetMotionReference();
|
||||
}
|
||||
|
||||
private void ResetMotionReference()
|
||||
{
|
||||
m_MotionReference = m_Root != null && m_Root.parent != null ? m_Root.parent : m_Root;
|
||||
if (m_MotionReference == null)
|
||||
{
|
||||
m_MotionReferenceInitialized = false;
|
||||
return;
|
||||
}
|
||||
|
||||
m_MotionReferencePrevPosition = m_MotionReference.position;
|
||||
m_MotionReferencePrevRotation = m_MotionReference.rotation;
|
||||
m_MotionReferenceInitialized = true;
|
||||
}
|
||||
|
||||
private void StabilizeLargeReferenceMotion()
|
||||
{
|
||||
Transform currentReference = m_Root != null && m_Root.parent != null ? m_Root.parent : m_Root;
|
||||
if (currentReference == null)
|
||||
return;
|
||||
|
||||
if (!m_MotionReferenceInitialized || currentReference != m_MotionReference)
|
||||
{
|
||||
ResetMotionReference();
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 currentPosition = currentReference.position;
|
||||
Quaternion currentRotation = currentReference.rotation;
|
||||
float translation = Vector3.Distance(currentPosition, m_MotionReferencePrevPosition);
|
||||
float rotation = Quaternion.Angle(m_MotionReferencePrevRotation, currentRotation);
|
||||
float translationThreshold = Mathf.Max(0.05f * m_ObjectScale, m_BoneTotalLength * 0.2f);
|
||||
|
||||
if (translation > translationThreshold || rotation > 30f)
|
||||
{
|
||||
Quaternion rotationDelta = currentRotation * Quaternion.Inverse(m_MotionReferencePrevRotation);
|
||||
for (int i = 0; i < Particles.Count; ++i)
|
||||
{
|
||||
Particle particle = Particles[i];
|
||||
particle.m_Position = currentPosition + rotationDelta *
|
||||
(particle.m_Position - m_MotionReferencePrevPosition);
|
||||
particle.m_PrevPosition = currentPosition + rotationDelta *
|
||||
(particle.m_PrevPosition - m_MotionReferencePrevPosition);
|
||||
particle.m_PreviousFramePosition = currentPosition + rotationDelta *
|
||||
(particle.m_PreviousFramePosition - m_MotionReferencePrevPosition);
|
||||
}
|
||||
|
||||
// The animated parent motion has already been applied to the particle
|
||||
// state and must not be counted again as Verlet velocity/inertia.
|
||||
m_ObjectPrevPosition = transform.position;
|
||||
}
|
||||
|
||||
m_MotionReferencePrevPosition = currentPosition;
|
||||
m_MotionReferencePrevRotation = currentRotation;
|
||||
}
|
||||
|
||||
void AppendParticles(Transform b, int parentIndex, float boneLength)
|
||||
@@ -472,15 +540,18 @@ public class DynamicBone : MonoBehaviour
|
||||
if (p.m_Transform != null)
|
||||
{
|
||||
p.m_Position = p.m_PrevPosition = p.m_Transform.position;
|
||||
p.m_PreviousFramePosition = p.m_Position;
|
||||
}
|
||||
else // end bone
|
||||
{
|
||||
Transform pb = Particles[p.m_ParentIndex].m_Transform;
|
||||
p.m_Position = p.m_PrevPosition = pb.TransformPoint(p.m_EndOffset);
|
||||
p.m_PreviousFramePosition = p.m_Position;
|
||||
}
|
||||
p.m_isCollide = false;
|
||||
}
|
||||
m_ObjectPrevPosition = transform.position;
|
||||
ResetMotionReference();
|
||||
}
|
||||
|
||||
void UpdateParticles1(float timeVar)
|
||||
@@ -497,8 +568,16 @@ public class DynamicBone : MonoBehaviour
|
||||
Particle p = Particles[i];
|
||||
if (p.m_ParentIndex >= 0)
|
||||
{
|
||||
p.m_PreviousFramePosition = p.m_Position;
|
||||
// verlet integration
|
||||
Vector3 v = p.m_Position - p.m_PrevPosition;
|
||||
if (m_ContinuousCollision)
|
||||
{
|
||||
Particle parent = Particles[p.m_ParentIndex];
|
||||
float maxStep = Vector3.Distance(parent.m_Position, p.m_Position);
|
||||
if (maxStep > Mathf.Epsilon)
|
||||
v = Vector3.ClampMagnitude(v, maxStep);
|
||||
}
|
||||
Vector3 rmove = m_ObjectMove * p.m_Inert;
|
||||
p.m_PrevPosition = p.m_Position + rmove;
|
||||
float damping = p.m_Damping;
|
||||
@@ -560,18 +639,6 @@ public class DynamicBone : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
// collide
|
||||
if (p.m_Colliders != null)
|
||||
{
|
||||
float particleRadius = p.m_Radius * m_ObjectScale;
|
||||
for (int j = 0; j < p.m_Colliders.Count; ++j)
|
||||
{
|
||||
DynamicBoneColliderBase c = p.m_Colliders[j];
|
||||
if (c != null && c.enabled)
|
||||
p.m_isCollide |= c.Collide(ref p.m_Position, particleRadius);
|
||||
}
|
||||
}
|
||||
|
||||
// freeze axis, project to plane
|
||||
if (m_FreezeAxis != FreezeAxis.None)
|
||||
{
|
||||
@@ -595,9 +662,110 @@ public class DynamicBone : MonoBehaviour
|
||||
float leng = dd.magnitude;
|
||||
if (leng > 0)
|
||||
p.m_Position += dd * ((leng - restLen) / leng);
|
||||
|
||||
// This must be the final positional constraint. Otherwise the
|
||||
// length correction can pull the skirt back inside a leg volume.
|
||||
float particleRadius = p.m_Radius * m_ObjectScale;
|
||||
ResolveParticleCollisions(p, particleRadius);
|
||||
|
||||
if (m_ContinuousCollision)
|
||||
{
|
||||
ResolveSegmentCollisions(p0, p, particleRadius);
|
||||
ResolveSweptCollisions(p, particleRadius);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ResolveParticleCollisions(Particle particle, float particleRadius)
|
||||
{
|
||||
if (particle.m_Colliders == null)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < particle.m_Colliders.Count; ++i)
|
||||
{
|
||||
DynamicBoneColliderBase collider = particle.m_Colliders[i];
|
||||
if (collider != null && collider.enabled)
|
||||
particle.m_isCollide |= collider.Collide(ref particle.m_Position, particleRadius);
|
||||
}
|
||||
}
|
||||
|
||||
private void ResolveSegmentCollisions(Particle parent, Particle particle, float particleRadius)
|
||||
{
|
||||
if (particle.m_Colliders == null)
|
||||
return;
|
||||
|
||||
Vector3 correction = Vector3.zero;
|
||||
|
||||
// Test several points because a segment can enter a capsule near either
|
||||
// endpoint while its midpoint remains outside.
|
||||
for (int sampleIndex = 1; sampleIndex <= 3; ++sampleIndex)
|
||||
{
|
||||
float t = sampleIndex * 0.25f;
|
||||
Vector3 segmentPoint = Vector3.Lerp(parent.m_Position, particle.m_Position, t);
|
||||
for (int i = 0; i < particle.m_Colliders.Count; ++i)
|
||||
{
|
||||
DynamicBoneColliderBase collider = particle.m_Colliders[i];
|
||||
if (collider == null || !collider.enabled)
|
||||
continue;
|
||||
|
||||
Vector3 projectedPoint = segmentPoint;
|
||||
if (!collider.Collide(ref projectedPoint, particleRadius))
|
||||
continue;
|
||||
|
||||
// Move the child enough to move this sample onto the surface.
|
||||
Vector3 candidate = (projectedPoint - segmentPoint) / t;
|
||||
if (candidate.sqrMagnitude > correction.sqrMagnitude)
|
||||
correction = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
if (correction == Vector3.zero)
|
||||
return;
|
||||
|
||||
particle.m_Position += correction;
|
||||
particle.m_PrevPosition += correction;
|
||||
particle.m_isCollide = true;
|
||||
ResolveParticleCollisions(particle, particleRadius);
|
||||
}
|
||||
|
||||
private void ResolveSweptCollisions(Particle particle, float particleRadius)
|
||||
{
|
||||
if (particle.m_Colliders == null)
|
||||
return;
|
||||
|
||||
Vector3 start = particle.m_PreviousFramePosition;
|
||||
Vector3 end = particle.m_Position;
|
||||
|
||||
// Catch a skirt particle that crossed a leg between rendered frames.
|
||||
// The final point may already be outside the collider, so checking it
|
||||
// alone cannot detect this case.
|
||||
float travel = Vector3.Distance(start, end);
|
||||
int sampleCount = Mathf.Clamp(Mathf.CeilToInt(travel / Mathf.Max(particleRadius * 0.5f, 0.01f)), 4, 8);
|
||||
for (int sampleIndex = 1; sampleIndex <= sampleCount; ++sampleIndex)
|
||||
{
|
||||
float t = sampleIndex / (float)sampleCount;
|
||||
Vector3 probe = Vector3.Lerp(start, end, t);
|
||||
for (int i = 0; i < particle.m_Colliders.Count; ++i)
|
||||
{
|
||||
DynamicBoneColliderBase collider = particle.m_Colliders[i];
|
||||
if (collider == null || !collider.enabled)
|
||||
continue;
|
||||
|
||||
Vector3 projected = probe;
|
||||
if (!collider.Collide(ref projected, particleRadius))
|
||||
continue;
|
||||
|
||||
Vector3 correction = projected - probe;
|
||||
particle.m_Position += correction;
|
||||
particle.m_PrevPosition += correction;
|
||||
particle.m_isCollide = true;
|
||||
end = particle.m_Position;
|
||||
}
|
||||
}
|
||||
|
||||
ResolveParticleCollisions(particle, particleRadius);
|
||||
}
|
||||
|
||||
// only update stiffness and keep bone length
|
||||
void SkipUpdateParticles()
|
||||
{
|
||||
@@ -718,4 +886,9 @@ public class DynamicBone : MonoBehaviour
|
||||
p.m_Transform.position = p.m_Position;
|
||||
}
|
||||
}
|
||||
|
||||
public void ApplyParticlesToTransformsNow()
|
||||
{
|
||||
ApplyParticlesToTransforms();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,9 @@ public class HandleManager : MonoBehaviour
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (UmaViewerUI.Instance == null || UmaViewerUI.Instance.PoseManager == null)
|
||||
return;
|
||||
|
||||
var camera = Camera.main;
|
||||
var poseModeOn = UmaViewerUI.Instance.PoseManager.PoseModeOn;
|
||||
|
||||
@@ -134,4 +137,4 @@ public class HandleManager : MonoBehaviour
|
||||
{
|
||||
EnabledLines = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,6 +353,109 @@ public class UmaContainerCharacter : UmaContainer
|
||||
{
|
||||
cySpringDataContainers[i].InitializePhysics(bones, colliders);
|
||||
}
|
||||
|
||||
SkirtSurfaceCollisionSolver surfaceSolver = GetComponent<SkirtSurfaceCollisionSolver>();
|
||||
if (surfaceSolver == null)
|
||||
surfaceSolver = gameObject.AddComponent<SkirtSurfaceCollisionSolver>();
|
||||
bones.TryGetValue("Hip", out Transform skirtHip);
|
||||
var skirtColliders = new List<DynamicBoneColliderBase>();
|
||||
foreach (Transform transform in colliders.Values)
|
||||
{
|
||||
DynamicBoneColliderBase collider = transform.GetComponent<DynamicBoneColliderBase>();
|
||||
if (collider != null)
|
||||
{
|
||||
if (IsBodySkirtCollider(collider) && collider.GetComponent<SkirtCollisionVisualizer>() == null)
|
||||
collider.gameObject.AddComponent<SkirtCollisionVisualizer>();
|
||||
if (IsThighSkirtCollider(collider.ColliderName))
|
||||
skirtColliders.Add(collider);
|
||||
}
|
||||
}
|
||||
surfaceSolver.Initialize(GetComponentsInChildren<DynamicBone>(true), skirtHip, skirtColliders);
|
||||
}
|
||||
|
||||
private static bool IsBodySkirtCollider(DynamicBoneColliderBase collider)
|
||||
{
|
||||
if (collider == null || string.IsNullOrEmpty(collider.ColliderName) ||
|
||||
collider.ColliderName.IndexOf("Skirt", StringComparison.OrdinalIgnoreCase) < 0)
|
||||
return false;
|
||||
|
||||
if (collider.ColliderName.IndexOf("Thigh", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
return true;
|
||||
|
||||
DynamicBoneCollider dynamicCollider = collider as DynamicBoneCollider;
|
||||
bool isSideHip = collider.ColliderName.EndsWith("_L", StringComparison.OrdinalIgnoreCase) ||
|
||||
collider.ColliderName.EndsWith("_R", StringComparison.OrdinalIgnoreCase);
|
||||
return collider.ColliderName.IndexOf("Hip", StringComparison.OrdinalIgnoreCase) >= 0 &&
|
||||
isSideHip && (dynamicCollider == null || dynamicCollider.m_Radius <= 0.25f);
|
||||
}
|
||||
|
||||
private static bool IsThighSkirtCollider(string colliderName)
|
||||
{
|
||||
return !string.IsNullOrEmpty(colliderName) &&
|
||||
colliderName.IndexOf("Thigh", StringComparison.OrdinalIgnoreCase) >= 0 &&
|
||||
colliderName.IndexOf("MSkirt", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
}
|
||||
|
||||
public void LoadSchoolUniformSkirtCollision(UmaDatabaseEntry entry)
|
||||
{
|
||||
if (entry == null)
|
||||
return;
|
||||
|
||||
GameObject source = entry.Get<GameObject>();
|
||||
if (source == null)
|
||||
return;
|
||||
|
||||
if (!PhysicsContainer)
|
||||
{
|
||||
PhysicsContainer = new GameObject("PhysicsController");
|
||||
PhysicsContainer.transform.SetParent(transform);
|
||||
}
|
||||
|
||||
CySpringDataContainer destination = PhysicsContainer.GetComponent<CySpringDataContainer>();
|
||||
if (destination == null)
|
||||
destination = PhysicsContainer.AddComponent<CySpringDataContainer>();
|
||||
if (destination.collisionParam == null)
|
||||
destination.collisionParam = new List<CySpringCollisionData>();
|
||||
if (destination.springParam == null)
|
||||
destination.springParam = new List<CySpringParamDataElement>();
|
||||
|
||||
foreach (CySpringDataContainer sourceContainer in source.GetComponentsInChildren<CySpringDataContainer>(true))
|
||||
{
|
||||
foreach (CySpringCollisionData collision in sourceContainer.collisionParam)
|
||||
{
|
||||
if (!IsSchoolUniformSkirtCollider(collision))
|
||||
continue;
|
||||
destination.collisionParam.Add(CloneCollisionData(collision));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsSchoolUniformSkirtCollider(CySpringCollisionData collision)
|
||||
{
|
||||
if (collision == null)
|
||||
return false;
|
||||
|
||||
return string.Equals(collision._collisionName, "Col_B_Thigh_L_MSkirt_L", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(collision._collisionName, "Col_B_Thigh_R_MSkirt_R", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(collision._collisionName, "Col_B_Hip_MSkirt_L", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(collision._collisionName, "Col_B_Hip_MSkirt_R", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static CySpringCollisionData CloneCollisionData(CySpringCollisionData source)
|
||||
{
|
||||
return new CySpringCollisionData
|
||||
{
|
||||
_collisionName = source._collisionName,
|
||||
_targetObjectName = source._targetObjectName,
|
||||
_isOtherTarget = source._isOtherTarget,
|
||||
_offset = source._offset,
|
||||
_offset2 = source._offset2,
|
||||
_radius = source._radius,
|
||||
_distance = source._distance,
|
||||
_normal = source._normal,
|
||||
_type = source._type,
|
||||
_isInner = source._isInner
|
||||
};
|
||||
}
|
||||
|
||||
public void SetDynamicBoneEnable(bool isOn)
|
||||
|
||||
@@ -202,6 +202,13 @@ public class UmaViewerBuilder : MonoBehaviour
|
||||
else
|
||||
{
|
||||
umaContainer.LoadBody(asset);
|
||||
// Winner costumes use their own cloth physics, but their skirt collision
|
||||
// volumes are not consistently authored. Reuse the verified school-uniform
|
||||
// MSkirt collision definition on the current character skeleton.
|
||||
const string schoolUniformCollision = "3d/chara/body/bdy0002_00/clothes/pfb_bdy0002_00_cloth00";
|
||||
if (Main.AbList.TryGetValue(schoolUniformCollision, out UmaDatabaseEntry schoolCollisionAsset))
|
||||
umaContainer.LoadSchoolUniformSkirtCollision(schoolCollisionAsset);
|
||||
|
||||
//Load Physics
|
||||
var asset1 = Main.AbList[UmaDatabaseController.BodyPath + $"bdy{id}_{costumeId}/clothes/pfb_bdy{id}_{costumeId}_cloth00"];
|
||||
umaContainer.LoadPhysics(asset1);
|
||||
|
||||
@@ -150,17 +150,20 @@ public class UmaViewerUI : MonoBehaviour
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (Builder.CurrentAudioSources.Count > 0 && Builder.CurrentAudioSources[0])
|
||||
if (Builder == null)
|
||||
return;
|
||||
|
||||
if (Builder.CurrentAudioSources != null && Builder.CurrentAudioSources.Count > 0 && Builder.CurrentAudioSources[0])
|
||||
{
|
||||
AudioSource MianSource = Builder.CurrentAudioSources[0];
|
||||
if (MianSource.clip)
|
||||
if (MianSource.clip && AudioSettings != null)
|
||||
{
|
||||
AudioSettings.UpdateTrack(MianSource);
|
||||
}
|
||||
}
|
||||
|
||||
var umaContainer = Builder.CurrentUMAContainer;
|
||||
if (umaContainer != null && umaContainer.OverrideController != null)
|
||||
if (umaContainer != null && umaContainer.OverrideController != null && AnimationSettings != null)
|
||||
{
|
||||
AnimationSettings.UpdateAnimationInfo(umaContainer);
|
||||
}
|
||||
@@ -1347,4 +1350,4 @@ public class UmaViewerUI : MonoBehaviour
|
||||
UmaViewerUI.Instance.ShowMessage($"ColorSet data saved: {fullPath}", UIMessageType.Success);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,16 +34,25 @@ namespace Gallop
|
||||
foreach (CySpringCollisionData collider in collisionParam)
|
||||
{
|
||||
if (collider._isInner) continue;
|
||||
if (bones.TryGetValue(collider._targetObjectName, out Transform bone))
|
||||
{
|
||||
if (IsSkirtLegOrHipCollider(collider._collisionName) &&
|
||||
!IsActiveSkirtBodyCollider(collider._collisionName))
|
||||
continue;
|
||||
Transform child;
|
||||
Transform bone = null;
|
||||
bool authoredTransform = bones.TryGetValue(collider._collisionName, out child);
|
||||
if (!authoredTransform && !bones.TryGetValue(collider._targetObjectName, out bone))
|
||||
continue;
|
||||
|
||||
var child = new GameObject(collider._collisionName);
|
||||
if (!authoredTransform)
|
||||
{
|
||||
child = new GameObject(collider._collisionName).transform;
|
||||
child.transform.SetParent(bone);
|
||||
child.transform.localPosition = Vector3.zero;
|
||||
child.transform.localRotation = Quaternion.identity;
|
||||
child.transform.localScale = Vector3.one;
|
||||
colliders.Add(child.name, child.transform);
|
||||
|
||||
}
|
||||
if (!colliders.ContainsKey(child.name))
|
||||
colliders.Add(child.name, child);
|
||||
|
||||
//修改(动骨与碰撞相关)
|
||||
if (collider._collisionName == "Col_B_Hip_Tail")
|
||||
@@ -54,9 +63,9 @@ namespace Gallop
|
||||
{
|
||||
collider._radius *= 1.14f;
|
||||
}
|
||||
else if (collider._collisionName.Contains("Col_B_Hip_Skirt"))
|
||||
else if (collider._collisionName.IndexOf("Skirt", System.StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
collider._radius *= 0.8f;
|
||||
// Skirt collision volumes are authored for the skirt and must stay at source size.
|
||||
}
|
||||
else if (collider._collisionName.Contains("Col_B_Hip_Jacket"))
|
||||
{
|
||||
@@ -80,33 +89,44 @@ namespace Gallop
|
||||
switch (collider._type)
|
||||
{
|
||||
case CySpringCollisionData.CollisionType.Capsule:
|
||||
var dynamic = child.AddComponent<DynamicBoneCollider>();
|
||||
var dynamic = child.gameObject.AddComponent<DynamicBoneCollider>();
|
||||
dynamic.ColliderName = collider._collisionName;
|
||||
child.transform.localPosition = (collider._offset + collider._offset2) / 2;
|
||||
child.transform.LookAt(child.transform.TransformPoint(collider._offset2));
|
||||
dynamic.m_Direction = DynamicBoneColliderBase.Direction.Z;
|
||||
dynamic.m_Height = (collider._offset - collider._offset2).magnitude + collider._radius;
|
||||
Vector3 capsuleAxis = collider._offset2 - collider._offset;
|
||||
if (!authoredTransform)
|
||||
{
|
||||
child.transform.localPosition = (collider._offset + collider._offset2) / 2;
|
||||
if (capsuleAxis.sqrMagnitude > 0.000001f)
|
||||
child.transform.localRotation = Quaternion.FromToRotation(Vector3.forward, capsuleAxis);
|
||||
}
|
||||
dynamic.m_Direction = authoredTransform
|
||||
? DynamicBoneColliderBase.Direction.Y
|
||||
: DynamicBoneColliderBase.Direction.Z;
|
||||
// DynamicBone stores full capsule height, including both hemispheres.
|
||||
dynamic.m_Height = capsuleAxis.magnitude + collider._radius * 2f;
|
||||
dynamic.m_Radius = collider._radius;
|
||||
dynamic.m_Bound = collider._isInner ? DynamicBoneColliderBase.Bound.Inside : DynamicBoneColliderBase.Bound.Outside;
|
||||
break;
|
||||
case CySpringCollisionData.CollisionType.Sphere:
|
||||
var Spheredynamic = child.AddComponent<DynamicBoneCollider>();
|
||||
var Spheredynamic = child.gameObject.AddComponent<DynamicBoneCollider>();
|
||||
Spheredynamic.ColliderName = collider._collisionName;
|
||||
child.transform.localPosition = collider._offset;
|
||||
if (!authoredTransform)
|
||||
child.transform.localPosition = collider._offset;
|
||||
Spheredynamic.m_Radius = collider._radius;
|
||||
Spheredynamic.m_Height = collider._distance;
|
||||
// A CySpring sphere's distance is not DynamicBone capsule height.
|
||||
// Giving it a height turns the authored hip sphere into a long capsule.
|
||||
Spheredynamic.m_Height = 0f;
|
||||
Spheredynamic.m_Bound = collider._isInner ? DynamicBoneColliderBase.Bound.Inside : DynamicBoneColliderBase.Bound.Outside;
|
||||
break;
|
||||
case CySpringCollisionData.CollisionType.Plane:
|
||||
var planedynamic = child.AddComponent<DynamicBonePlaneCollider>();
|
||||
var planedynamic = child.gameObject.AddComponent<DynamicBonePlaneCollider>();
|
||||
planedynamic.ColliderName = collider._collisionName;
|
||||
child.transform.localPosition = collider._offset;
|
||||
if (!authoredTransform)
|
||||
child.transform.localPosition = collider._offset;
|
||||
planedynamic.m_Bound = collider._isInner ? DynamicBoneColliderBase.Bound.Inside : DynamicBoneColliderBase.Bound.Outside;
|
||||
break;
|
||||
case CySpringCollisionData.CollisionType.None:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return colliders;
|
||||
}
|
||||
@@ -117,11 +137,23 @@ namespace Gallop
|
||||
public void InitializePhysics(Dictionary<string, Transform> bones, Dictionary<string, Transform> colliders)
|
||||
{
|
||||
DynamicBones.Clear();
|
||||
var skirtColliders = new List<DynamicBoneColliderBase>();
|
||||
foreach (Transform transform in colliders.Values)
|
||||
{
|
||||
DynamicBoneColliderBase collider = transform.GetComponent<DynamicBoneColliderBase>();
|
||||
if (collider != null && IsSharedThighSkirtCollider(collider.ColliderName))
|
||||
{
|
||||
skirtColliders.Add(collider);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取对象名称并判断类型
|
||||
string nameLower = gameObject.name.ToLower();
|
||||
bool isTailObject = nameLower.Contains("tail"); // 判断是否为尾巴
|
||||
bool isClothObject = !isTailObject && nameLower.Contains("chr"); // 判断是否为头发,排除尾巴避免重复匹配
|
||||
bool isChestBowObject = !isTailObject &&
|
||||
(nameLower.Contains("ribbon") || nameLower.Contains("bow") ||
|
||||
nameLower.Contains("tie") || nameLower.Contains("bust"));
|
||||
|
||||
foreach (CySpringParamDataElement spring in springParam)
|
||||
{
|
||||
@@ -129,6 +161,8 @@ namespace Gallop
|
||||
|
||||
var dynamic = bone.gameObject.AddComponent<DynamicBone>();
|
||||
dynamic.m_Root = bone;
|
||||
dynamic.m_MotionGroup = isChestBowObject ? "ChestBow" :
|
||||
(isClothObject ? "Hair" : string.Empty);
|
||||
|
||||
// 设置重力
|
||||
dynamic.m_Gravity = new Vector3(0, Mathf.Clamp01(-30f / spring._gravity), 0);
|
||||
@@ -166,13 +200,41 @@ namespace Gallop
|
||||
|
||||
dynamic.SetupParticles();
|
||||
DynamicBones.Add(dynamic);
|
||||
|
||||
bool isSkirtChain = spring._boneName.IndexOf("Skirt", System.StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
if (isSkirtChain)
|
||||
{
|
||||
dynamic.m_ContinuousCollision = true;
|
||||
}
|
||||
|
||||
// 主碰撞器添加
|
||||
// The source list belongs to the root spring node. DynamicBone particle zero
|
||||
// is fixed, so apply it to the first simulated particle for skirt chains.
|
||||
foreach (string collisionName in spring._collisionNameList)
|
||||
{
|
||||
if (colliders.TryGetValue(collisionName, out Transform tmp))
|
||||
if (!colliders.TryGetValue(collisionName, out Transform tmp))
|
||||
continue;
|
||||
|
||||
DynamicBoneColliderBase collider = tmp.GetComponent<DynamicBoneColliderBase>();
|
||||
if (isSkirtChain && !ShouldUseAuthoredSkirtCollider(collider))
|
||||
continue;
|
||||
int particleIndex = isSkirtChain && dynamic.Particles.Count > 1 ? 1 : 0;
|
||||
AddCollider(dynamic.Particles[particleIndex], collider);
|
||||
}
|
||||
|
||||
if (isSkirtChain)
|
||||
{
|
||||
Transform hipBone;
|
||||
bones.TryGetValue("Hip", out hipBone);
|
||||
foreach (DynamicBoneColliderBase collider in skirtColliders)
|
||||
AddColliderToSkirtParticles(dynamic, collider);
|
||||
|
||||
foreach (Transform transform in colliders.Values)
|
||||
{
|
||||
dynamic.Particles[0].m_Colliders.Add(tmp.GetComponent<DynamicBoneColliderBase>());
|
||||
DynamicBoneColliderBase collider = transform.GetComponent<DynamicBoneColliderBase>();
|
||||
if (collider == null || !IsHipSkirtCollider(collider.ColliderName) ||
|
||||
!IsSameSkirtSide(dynamic.m_Root, collider, hipBone))
|
||||
continue;
|
||||
AddColliderToSkirtParticles(dynamic, collider);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,18 +280,96 @@ namespace Gallop
|
||||
{
|
||||
if (colliders.TryGetValue(collisionName, out Transform tmp))
|
||||
{
|
||||
var collider = tmp.GetComponent<DynamicBoneColliderBase>();
|
||||
if (collider != null)
|
||||
{
|
||||
tempParticle.m_Colliders.Add(collider);
|
||||
}
|
||||
DynamicBoneColliderBase collider = tmp.GetComponent<DynamicBoneColliderBase>();
|
||||
if (!isSkirtChain || ShouldUseAuthoredSkirtCollider(collider))
|
||||
AddCollider(tempParticle, collider);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddColliderToSkirtParticles(DynamicBone dynamic, DynamicBoneColliderBase collider)
|
||||
{
|
||||
// Particle zero is the fixed skirt root and is not simulated.
|
||||
for (int i = 1; i < dynamic.Particles.Count; i++)
|
||||
AddCollider(dynamic.Particles[i], collider);
|
||||
}
|
||||
|
||||
private static void AddCollider(DynamicBone.Particle particle, DynamicBoneColliderBase collider)
|
||||
{
|
||||
if (particle != null && collider != null && !particle.m_Colliders.Contains(collider))
|
||||
particle.m_Colliders.Add(collider);
|
||||
}
|
||||
|
||||
private static bool IsThighSkirtCollider(string colliderName)
|
||||
{
|
||||
return !string.IsNullOrEmpty(colliderName) &&
|
||||
colliderName.IndexOf("Skirt", System.StringComparison.OrdinalIgnoreCase) >= 0 &&
|
||||
colliderName.IndexOf("Thigh", System.StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
}
|
||||
|
||||
private static bool IsSkirtLegOrHipCollider(string colliderName)
|
||||
{
|
||||
return !string.IsNullOrEmpty(colliderName) &&
|
||||
colliderName.IndexOf("Skirt", System.StringComparison.OrdinalIgnoreCase) >= 0 &&
|
||||
(colliderName.IndexOf("Thigh", System.StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
colliderName.IndexOf("Hip", System.StringComparison.OrdinalIgnoreCase) >= 0);
|
||||
}
|
||||
|
||||
private static bool IsActiveSkirtBodyCollider(string colliderName)
|
||||
{
|
||||
return string.Equals(colliderName, "Col_B_Thigh_L_MSkirt_L", System.StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(colliderName, "Col_B_Thigh_R_MSkirt_R", System.StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(colliderName, "Col_B_Hip_MSkirt_L", System.StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(colliderName, "Col_B_Hip_MSkirt_R", System.StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool IsSharedThighSkirtCollider(string colliderName)
|
||||
{
|
||||
return !string.IsNullOrEmpty(colliderName) &&
|
||||
colliderName.IndexOf("Thigh", System.StringComparison.OrdinalIgnoreCase) >= 0 &&
|
||||
colliderName.IndexOf("MSkirt", System.StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
}
|
||||
|
||||
private static bool ShouldUseAuthoredSkirtCollider(DynamicBoneColliderBase collider)
|
||||
{
|
||||
if (collider == null || string.IsNullOrEmpty(collider.ColliderName))
|
||||
return false;
|
||||
|
||||
if (IsThighSkirtCollider(collider.ColliderName))
|
||||
return false;
|
||||
|
||||
bool isHipSkirt = collider.ColliderName.IndexOf(
|
||||
"Hip", System.StringComparison.OrdinalIgnoreCase) >= 0 &&
|
||||
collider.ColliderName.IndexOf(
|
||||
"Skirt", System.StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
|
||||
// The original CySpring solver can use broad rear/center hip spheres,
|
||||
// but they over-expand an independent DynamicBone skirt chain. This is
|
||||
// true for both the old MSkirt names and newer Col_B_Hip_Skirt_B/BLR.
|
||||
// Keep only an explicitly left/right localized hip pair.
|
||||
return !isHipSkirt || IsHipSkirtCollider(collider.ColliderName);
|
||||
}
|
||||
|
||||
private static bool IsHipSkirtCollider(string colliderName)
|
||||
{
|
||||
return !string.IsNullOrEmpty(colliderName) &&
|
||||
colliderName.IndexOf("Skirt", System.StringComparison.OrdinalIgnoreCase) >= 0 &&
|
||||
colliderName.IndexOf("Hip", System.StringComparison.OrdinalIgnoreCase) >= 0 &&
|
||||
(colliderName.EndsWith("_L", System.StringComparison.OrdinalIgnoreCase) ||
|
||||
colliderName.EndsWith("_R", System.StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static bool IsSameSkirtSide(Transform skirtRoot, DynamicBoneColliderBase collider, Transform hip)
|
||||
{
|
||||
if (skirtRoot == null || collider == null || hip == null)
|
||||
return true;
|
||||
|
||||
bool rootLeft = hip.InverseTransformPoint(skirtRoot.position).x < 0f;
|
||||
bool colliderLeft = hip.InverseTransformPoint(collider.transform.position).x < 0f;
|
||||
return rootLeft == colliderLeft;
|
||||
}
|
||||
|
||||
public void EnablePhysics(bool isOn)
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+8
@@ -0,0 +1,8 @@
|
||||
namespace Gallop
|
||||
{
|
||||
// Asset bundles reference this original qualified signature. The recovered implementation
|
||||
// lives in the global namespace, so inherit it to preserve all serialized collision fields.
|
||||
public class CySpringCollisionComponent : global::CySpringCollisionComponent
|
||||
{
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9459a406c0ff58349bd3255f1827c124
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -78,6 +78,19 @@ namespace Gallop.Live.Cutt
|
||||
public event Action<LiveTimelineChromaticAberrationData, LiveTimelineKeyChromaticAberrationData> OnUpdateChromaticAberration;
|
||||
public event Action<LiveTimelineHdrBloomData, LiveTimelineKeyHdrBloomData> OnUpdateHdrBloom;
|
||||
public event Action<LiveTimelineColorCorrectionData, LiveTimelineKeyColorCorrectionData> OnUpdateColorCorrection;
|
||||
public event MobCyalumeUpdateInfoDelegate OnUpdateMobControl;
|
||||
public event MobCyalumeUpdateInfoDelegate OnUpdateCyalumeControl;
|
||||
public event Action<LiveTimelineMonitorControlData, LiveTimelineKeyMonitorControlData> OnUpdateMonitorControl;
|
||||
public event Action<LiveTimelineMonitorCameraPositionData, LiveTimelineKeyMonitorCameraPositionData> OnUpdateMonitorCameraPosition;
|
||||
public event Action<LiveTimelineMonitorCameraLookAtData, LiveTimelineKeyMonitorCameraLookAtData> OnUpdateMonitorCameraLookAt;
|
||||
public event Action<LiveTimelineLensFlareData, LiveTimelineKeyLensFlareData> OnUpdateLensFlare;
|
||||
public event Action<LiveTimelineStageEnvironmentData, LiveTimelineKeyStageEnvironmentData> OnUpdateEnvironment;
|
||||
public event Action<int, LiveTimelineKeyFacialToonData> OnUpdateFacialToon;
|
||||
public event Action<LiveTimelinePropsData, LiveTimelineKeyPropsData> OnUpdateProps;
|
||||
public event Action<LiveTimelinePropsAttachData, LiveTimelineKeyPropsAttachData> OnUpdatePropsAttach;
|
||||
public event Action<LiveTimelineKeyCharaFootLightData> OnUpdateCharaFootLight;
|
||||
public event Action<LiveTimelineAdditionalLight, LiveTimelineKeyData_AdditionalLight> OnUpdateAdditionalLight;
|
||||
public event Action<LiveTimelineLightProjectionData, LiveTimelineKeyLightProjectionData> OnUpdateLightProjection;
|
||||
|
||||
private static Func<LiveTimelineKeyCameraPositionData, LiveTimelineControl, FindTimelineConfig, Vector3> fnGetCameraPosValue = GetCameraPosValue;
|
||||
|
||||
@@ -210,6 +223,7 @@ namespace Gallop.Live.Cutt
|
||||
private Vector3 _liveStageCenterPos = Vector3.zero;
|
||||
|
||||
private TimelinePlayerMode _playMode = TimelinePlayerMode.Default;
|
||||
public TimelinePlayerMode PlayMode => _playMode;
|
||||
|
||||
public static int liveCharaPositionMax
|
||||
{
|
||||
@@ -382,6 +396,19 @@ namespace Gallop.Live.Cutt
|
||||
// 分发保留,接上只需订阅 OnUpdateHdrBloom。
|
||||
AlterUpdate_SimpleListControl(workSheet.hdrBloomKeys, d => d.keys, OnUpdateHdrBloom, _currentFrame);
|
||||
AlterUpdate_SimpleListControl(workSheet.colorCorrectionDataLists, d => d.keys, OnUpdateColorCorrection, _currentFrame);
|
||||
AlterUpdate_MobCyalumeControl(workSheet.MobControlKeys, _currentFrame, OnUpdateMobControl);
|
||||
AlterUpdate_MobCyalumeControl(workSheet.CyalumeControlKeys, _currentFrame, OnUpdateCyalumeControl);
|
||||
AlterUpdate_SimpleListControl(workSheet.monitorControlList, d => d.keys, OnUpdateMonitorControl, _currentFrame);
|
||||
AlterUpdate_SimpleListControl(workSheet.monitorCameraPosKeys, d => d.keys, OnUpdateMonitorCameraPosition, _currentFrame);
|
||||
AlterUpdate_SimpleListControl(workSheet.monitorCameraLookAtKeys, d => d.keys, OnUpdateMonitorCameraLookAt, _currentFrame);
|
||||
AlterUpdate_SimpleListControl(workSheet.lensFlareList, d => d.keys, OnUpdateLensFlare, _currentFrame);
|
||||
AlterUpdate_SimpleListControl(workSheet.environmentDataLists, d => d.keys, OnUpdateEnvironment, _currentFrame);
|
||||
AlterUpdate_FacialToon(workSheet.facialToonSet, _currentFrame);
|
||||
AlterUpdate_SimpleListControl(workSheet.propsList, d => d.keys, OnUpdateProps, _currentFrame);
|
||||
AlterUpdate_SimpleListControl(workSheet.propsAttachList, d => d.keys, OnUpdatePropsAttach, _currentFrame);
|
||||
AlterUpdate_SimpleListControl(workSheet.AdditionalLightList, d => d.keys, OnUpdateAdditionalLight, _currentFrame);
|
||||
AlterUpdate_CharaFootLight(workSheet.charaFootLightKeys, _currentFrame);
|
||||
AlterUpdate_SimpleListControl(workSheet.lightProjectionList, d => d.keys, OnUpdateLightProjection, _currentFrame);
|
||||
AlterUpdate_PostFilm(workSheet.postFilmKeys, 0, _currentFrame);
|
||||
AlterUpdate_PostFilm(workSheet.postFilm2Keys, 1, _currentFrame);
|
||||
AlterUpdate_PostFilm(workSheet.postFilm3Keys, 2, _currentFrame);
|
||||
@@ -609,9 +636,14 @@ namespace Gallop.Live.Cutt
|
||||
{
|
||||
|
||||
var formationList = data.worksheetList[0].formationOffsetSet.Init();
|
||||
if (formationList == null)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < Director.instance.characterCount; i++)
|
||||
int count = Mathf.Min(Director.instance.characterCount, formationList.Count);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (formationList[i] == null)
|
||||
continue;
|
||||
LiveTimelineKeyIndex curKey = AlterUpdate_Key(formationList[i], liveTime);
|
||||
|
||||
if (curKey != null && curKey.index != -1)
|
||||
@@ -2015,6 +2047,72 @@ namespace Gallop.Live.Cutt
|
||||
}
|
||||
}
|
||||
|
||||
private void AlterUpdate_MobCyalumeControl(
|
||||
List<LiveTimelineMobCyalumeControlData> dataList,
|
||||
float currentFrame,
|
||||
MobCyalumeUpdateInfoDelegate callback)
|
||||
{
|
||||
if (callback == null || dataList == null) return;
|
||||
|
||||
for (int i = 0; i < dataList.Count; i++)
|
||||
{
|
||||
var data = dataList[i];
|
||||
var keys = data != null ? data.keys : null;
|
||||
if (keys == null || keys.Count == 0 || keys.HasAttribute(LiveTimelineKeyDataListAttr.Disable) ||
|
||||
!keys.EnablePlayModeTimeline(_playMode)) continue;
|
||||
|
||||
FindTimelineKey(out var curKey, out var nextKey, keys, currentFrame);
|
||||
var current = curKey as LiveTimelineKeyMobCyalumeControlData;
|
||||
var next = nextKey as LiveTimelineKeyMobCyalumeControlData;
|
||||
if (current == null) continue;
|
||||
|
||||
var info = new MobCyalumeUpdateInfo
|
||||
{
|
||||
data = data,
|
||||
unk0 = (uint)keys.unk48 < 11u ? keys.unk48 : i,
|
||||
currentFrame = currentFrame,
|
||||
currentLiveTime = currentLiveTime
|
||||
};
|
||||
|
||||
if (next != null && next.interpolateType != LiveCameraInterpolateType.None)
|
||||
{
|
||||
float t = CalculateInterpolationValue(current, next, currentFrame);
|
||||
info.position = Vector3.Lerp(current.position, next.position, t);
|
||||
info.rotation = Quaternion.Lerp(current.GetRotation(), next.GetRotation(), t);
|
||||
info.scale = Vector3.Lerp(current.scale, next.scale, t);
|
||||
}
|
||||
else
|
||||
{
|
||||
info.position = current.position;
|
||||
info.rotation = current.GetRotation();
|
||||
info.scale = current.scale;
|
||||
}
|
||||
|
||||
callback.Invoke(ref info);
|
||||
}
|
||||
}
|
||||
|
||||
private void AlterUpdate_FacialToon(LiveTimelineFacialToonData data, float currentFrame)
|
||||
{
|
||||
if (data == null || OnUpdateFacialToon == null) return;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var keys = data.GetKeys(i);
|
||||
if (keys == null || keys.Count == 0 || keys.HasAttribute(LiveTimelineKeyDataListAttr.Disable) ||
|
||||
!keys.EnablePlayModeTimeline(_playMode)) continue;
|
||||
FindTimelineKey(out var current, out var _, keys, currentFrame);
|
||||
if (current is LiveTimelineKeyFacialToonData key) OnUpdateFacialToon.Invoke(i, key);
|
||||
}
|
||||
}
|
||||
|
||||
private void AlterUpdate_CharaFootLight(LiveTimelineKeyCharaFootLightDataList keys, float currentFrame)
|
||||
{
|
||||
if (keys == null || OnUpdateCharaFootLight == null || keys.Count == 0 ||
|
||||
keys.HasAttribute(LiveTimelineKeyDataListAttr.Disable) || !keys.EnablePlayModeTimeline(_playMode)) return;
|
||||
FindTimelineKey(out var current, out var _, keys, currentFrame);
|
||||
if (current is LiveTimelineKeyCharaFootLightData key) OnUpdateCharaFootLight.Invoke(key);
|
||||
}
|
||||
|
||||
private void AlterUpdate_GlobalFogControl(LiveTimelineWorkSheet sheet, float currentFrame)
|
||||
{
|
||||
if (sheet.globalFogDataLists == null || OnUpdateGlobalFog == null) return;
|
||||
@@ -2070,4 +2168,3 @@ namespace Gallop.Live.Cutt
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
+311
@@ -0,0 +1,311 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Gallop.Live.Cutt
|
||||
{
|
||||
[Serializable]
|
||||
public class LiveTimelineKeyLensFlareData : LiveTimelineKeyWithInterpolate
|
||||
{
|
||||
public Vector3 offset;
|
||||
public Color color = Color.white;
|
||||
public float brightness = 1f;
|
||||
public float fadeSpeed = 1f;
|
||||
public int enableParameter;
|
||||
public int enableFlare;
|
||||
public int IsAutoBrightness;
|
||||
public int IsOverridePosition;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class LiveTimelineKeyLensFlareDataList : LiveTimelineKeyDataListTemplate<LiveTimelineKeyLensFlareData> { }
|
||||
|
||||
[Serializable]
|
||||
public class LiveTimelineLensFlareData : ILiveTimelineGroupDataWithName
|
||||
{
|
||||
public LiveTimelineKeyLensFlareDataList keys;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class LiveTimelineKeyStageEnvironmentData : LiveTimelineKey
|
||||
{
|
||||
public bool isValidMirror;
|
||||
public bool isMirror;
|
||||
public bool isBgMirror;
|
||||
public bool IsMirrorBg3d;
|
||||
public bool EnableCharacterMirrorExpandFaceBounds;
|
||||
public int characterMirror;
|
||||
public int CharacterMirrorHead;
|
||||
public int CharacterMirrorExpandFaceBounds;
|
||||
public float mirrorReflectionRate = 1f;
|
||||
public bool isValidShadow;
|
||||
public int characterShadow;
|
||||
public bool isSoftShadow;
|
||||
public bool IsToonMirror;
|
||||
public bool isValidWaterReflection;
|
||||
public float waterReflection;
|
||||
public float waveScale;
|
||||
public float waterDistortion;
|
||||
public float waterUCross;
|
||||
public float waterVCross;
|
||||
public float waterUSpeed;
|
||||
public float waterVSpeed;
|
||||
public float waterNormalPower;
|
||||
public float waveDistortionPower;
|
||||
public float waveClearly;
|
||||
public float waveDiffusion;
|
||||
public float waveDecline;
|
||||
public Color waterColor;
|
||||
public float waterVOffset;
|
||||
public bool IsValidStageFovShift;
|
||||
public float BaseFov;
|
||||
public float ShiftPower;
|
||||
public bool IsShiftY;
|
||||
public bool _isValidMirror;
|
||||
public int _mirror;
|
||||
public int _bgMirror;
|
||||
public int _characterMirror;
|
||||
public float _mirrorReflectionRate = -1f;
|
||||
|
||||
public bool MirrorIsValid => isValidMirror || _isValidMirror;
|
||||
public bool MirrorEnabled => isMirror || isBgMirror || IsMirrorBg3d || _mirror != 0 || _bgMirror != 0;
|
||||
public float ReflectionRate => _mirrorReflectionRate >= 0f ? _mirrorReflectionRate : mirrorReflectionRate;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class LiveTimelineKeyStageEnvironmentDataList : LiveTimelineKeyDataListTemplate<LiveTimelineKeyStageEnvironmentData> { }
|
||||
|
||||
[Serializable]
|
||||
public class LiveTimelineStageEnvironmentData : ILiveTimelineGroupDataWithName
|
||||
{
|
||||
public LiveTimelineKeyStageEnvironmentDataList keys;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class LiveTimelineKeyFacialToonData : LiveTimelineKeyWithInterpolate
|
||||
{
|
||||
public float CheekPretenseThreshold;
|
||||
public float NosePretenseThreshold;
|
||||
public float CylinderBlend;
|
||||
public float HairNormalBlend;
|
||||
public int UseOriginalDirectionalLight;
|
||||
public Vector3 OriginalDirectionalLightDir;
|
||||
public float EyeToonStep;
|
||||
public float EyeToonFeather;
|
||||
public float EyeSaturation;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class LiveTimelineKeyFacialToonDataList : LiveTimelineKeyDataListTemplate<LiveTimelineKeyFacialToonData> { }
|
||||
|
||||
[Serializable]
|
||||
public class LiveTimelineFacialToonData
|
||||
{
|
||||
public LiveTimelineKeyFacialToonDataList centerKeys;
|
||||
public LiveTimelineKeyFacialToonDataList left1Keys;
|
||||
public LiveTimelineKeyFacialToonDataList right1Keys;
|
||||
public LiveTimelineKeyFacialToonDataList left2Keys;
|
||||
public LiveTimelineKeyFacialToonDataList right2Keys;
|
||||
public LiveTimelineKeyFacialToonDataList motion5Keys;
|
||||
public LiveTimelineKeyFacialToonDataList motion6Keys;
|
||||
public LiveTimelineKeyFacialToonDataList motion7Keys;
|
||||
public LiveTimelineKeyFacialToonDataList motion8Keys;
|
||||
public LiveTimelineKeyFacialToonDataList motion9Keys;
|
||||
public LiveTimelineKeyFacialToonDataList motion10Keys;
|
||||
public LiveTimelineKeyFacialToonDataList motion11Keys;
|
||||
public LiveTimelineKeyFacialToonDataList motion12Keys;
|
||||
public LiveTimelineKeyFacialToonDataList motion13Keys;
|
||||
public LiveTimelineKeyFacialToonDataList motion14Keys;
|
||||
public LiveTimelineKeyFacialToonDataList motion15Keys;
|
||||
public LiveTimelineKeyFacialToonDataList motion16Keys;
|
||||
public LiveTimelineKeyFacialToonDataList motion17Keys;
|
||||
public LiveTimelineKeyFacialToonDataList motion18Keys;
|
||||
public LiveTimelineKeyFacialToonDataList motion19Keys;
|
||||
|
||||
public LiveTimelineKeyFacialToonDataList GetKeys(int index)
|
||||
{
|
||||
switch (index)
|
||||
{
|
||||
case 0: return centerKeys; case 1: return left1Keys; case 2: return right1Keys;
|
||||
case 3: return left2Keys; case 4: return right2Keys; case 5: return motion5Keys;
|
||||
case 6: return motion6Keys; case 7: return motion7Keys; case 8: return motion8Keys;
|
||||
case 9: return motion9Keys; case 10: return motion10Keys; case 11: return motion11Keys;
|
||||
case 12: return motion12Keys; case 13: return motion13Keys; case 14: return motion14Keys;
|
||||
case 15: return motion15Keys; case 16: return motion16Keys; case 17: return motion17Keys;
|
||||
case 18: return motion18Keys; case 19: return motion19Keys; default: return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class LiveTimelineKeyCameraLayerData : LiveTimelineKeyWithInterpolate
|
||||
{
|
||||
public Vector3 offsetMaxPosition;
|
||||
public Vector3 offsetMinPosition;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class LiveTimelineKeyCameraLayerDataList : LiveTimelineKeyDataListTemplate<LiveTimelineKeyCameraLayerData> { }
|
||||
|
||||
[Serializable]
|
||||
public class LiveTimelineKeyPropsData : LiveTimelineKeyWithInterpolate
|
||||
{
|
||||
public int settingFlags;
|
||||
public int propsID;
|
||||
public int rendererEnable;
|
||||
public int IsVisibleAttachedCharaLinked;
|
||||
public int AutoSwitchLayerOnMirrorRendering;
|
||||
public Color color = Color.white;
|
||||
public Color rootColor = Color.white;
|
||||
public Color tipColor = Color.white;
|
||||
public float colorPower = 1f;
|
||||
public int IsApplyAnimation;
|
||||
public int IsApplyReserveWarming;
|
||||
public AnimationClip AnimationClip;
|
||||
public float StartAnimationTime;
|
||||
public int AnimationHeadFrame;
|
||||
public Color ToonDarkColor;
|
||||
public Color ToonBrightColor;
|
||||
public int IsCastShadow;
|
||||
public int IsCastShadowForced;
|
||||
public Vector3 _directionalLightAngle;
|
||||
public int IsUpdateOutline;
|
||||
public float OutlineWidth;
|
||||
public Color OutlineColor;
|
||||
public int IsEmissive;
|
||||
public Color EmissiveColor;
|
||||
public float EmissiveScrollTimeScale = 1f;
|
||||
public float EmissiveScrollEnergyScale = 1f;
|
||||
}
|
||||
|
||||
[Serializable] public class LiveTimelineKeyPropsDataList : LiveTimelineKeyDataListTemplate<LiveTimelineKeyPropsData> { }
|
||||
[Serializable]
|
||||
public class LiveTimelinePropsData : ILiveTimelineGroupDataWithName
|
||||
{
|
||||
public LiveTimelineKeyPropsDataList keys;
|
||||
public bool _applyVariation;
|
||||
public int _variationId;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class LiveTimelineKeyPropsAttachData : LiveTimelineKeyWithInterpolate
|
||||
{
|
||||
public string _attachJointName;
|
||||
public int _attachJointHash;
|
||||
public string _copyPositionJointName;
|
||||
public int _copyPositionJointHash;
|
||||
public int _settingFlags;
|
||||
public int _propsId;
|
||||
public Vector3 _offsetPosition;
|
||||
public Vector3 OffsetRotate;
|
||||
public Vector3 OffsetScale = Vector3.one;
|
||||
public int IsLinkAttachBone;
|
||||
public int _attachType;
|
||||
public int _attachPropId = -1;
|
||||
public string _attachTargetPropNodeName;
|
||||
}
|
||||
|
||||
[Serializable] public class LiveTimelineKeyPropsAttachDataList : LiveTimelineKeyDataListTemplate<LiveTimelineKeyPropsAttachData> { }
|
||||
[Serializable]
|
||||
public class LiveTimelinePropsAttachData : ILiveTimelineGroupDataWithName
|
||||
{
|
||||
public LiveTimelineKeyPropsAttachDataList keys;
|
||||
public bool _applyVariation;
|
||||
public int _variationId;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class LiveTimelineKeyCharaFootLightData : LiveTimelineKeyWithInterpolate
|
||||
{
|
||||
public int positionFlag;
|
||||
public float[] hightMax = Array.Empty<float>();
|
||||
public Color[] lightColor = Array.Empty<Color>();
|
||||
public int[] LightBlendModeArray = Array.Empty<int>();
|
||||
public int[] EasingArray = Array.Empty<int>();
|
||||
}
|
||||
|
||||
[Serializable] public class LiveTimelineKeyCharaFootLightDataList : LiveTimelineKeyDataListTemplate<LiveTimelineKeyCharaFootLightData> { }
|
||||
|
||||
[Serializable]
|
||||
public class LiveTimelineKeyData_AdditionalLight : LiveTimelineKeyWithInterpolate
|
||||
{
|
||||
public Vector3 Position;
|
||||
public Vector3 Rotate;
|
||||
public int IsEnable;
|
||||
public int Type;
|
||||
public float Range;
|
||||
public int SpotAngle;
|
||||
public int IndirectMultiplier;
|
||||
public int ShadowType;
|
||||
public float Strength = 1f;
|
||||
public float Bias = 0.05f;
|
||||
public float NormalBias = 0.4f;
|
||||
public float NearPlane = 0.2f;
|
||||
}
|
||||
|
||||
[Serializable] public class LiveTimelineKeyAdditionalLightList : LiveTimelineKeyDataListTemplate<LiveTimelineKeyData_AdditionalLight> { }
|
||||
[Serializable]
|
||||
public class LiveTimelineAdditionalLight : ILiveTimelineGroupDataWithName
|
||||
{
|
||||
public LiveTimelineKeyAdditionalLightList keys;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class LiveTimelineLightProjectionAnimationParam
|
||||
{
|
||||
public int TextureId = -1;
|
||||
public int DivisionNumberX;
|
||||
public int DivisionNumberY;
|
||||
public int MaxCut;
|
||||
public float AnimationTime;
|
||||
public Vector2 ScaleUV = Vector2.one;
|
||||
public Vector2 OffsetUV;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class LiveTimelineKeyLightProjectionData : LiveTimelineKeyWithInterpolate
|
||||
{
|
||||
public int IsEnable;
|
||||
public int OverrideIgnoreLayer;
|
||||
public LayerMask OverrideLayerMask;
|
||||
public int BacksideOff;
|
||||
public float MirrorBallBacksideFeather;
|
||||
public int ProjectionToCharacterModelOnly;
|
||||
public int TextureId;
|
||||
public Color Color = Color.white;
|
||||
public Vector3 Position;
|
||||
public Vector3 Angle;
|
||||
public Vector3 Scale = Vector3.one;
|
||||
public int Orthographic;
|
||||
public float OrthographicSize = 10f;
|
||||
public float NearClipPlane = 0.1f;
|
||||
public float FarClipPlane = 100f;
|
||||
public float FieldOfView = 60f;
|
||||
public float ColorPower = 1f;
|
||||
public int LightBlendMode;
|
||||
public int CharacterAttach;
|
||||
public int CharacterAttachPosition;
|
||||
public string BlinkLightName;
|
||||
public int BlinkLightNameHash;
|
||||
public int BlinkLightContainerIndex;
|
||||
public float BlinkLightBrightnessPower = 1f;
|
||||
public int IsAdjustedBlinkLightColor;
|
||||
public Vector3 MirrorBallRotateAxis = Vector3.up;
|
||||
public float MirrorBallRotateValue;
|
||||
public float MirrorBallProjectionRadius = 20f;
|
||||
public float MirrorBallFallOffPower = 2f;
|
||||
public int MirrorBallIsLoopRotation;
|
||||
public float MirrorBallLoopRotationSpeed;
|
||||
public Vector2 MirrorBallUVOffset;
|
||||
public Vector2 MirrorBallUVScale = Vector2.one;
|
||||
public int MirrorBallUseCubeMap;
|
||||
public LiveTimelineLightProjectionAnimationParam AnimationParam;
|
||||
}
|
||||
|
||||
[Serializable] public class LiveTimelineKeyLightProjectionDataList : LiveTimelineKeyDataListTemplate<LiveTimelineKeyLightProjectionData> { }
|
||||
[Serializable]
|
||||
public class LiveTimelineLightProjectionData : ILiveTimelineGroupDataWithName
|
||||
{
|
||||
public LiveTimelineKeyLightProjectionDataList keys;
|
||||
public int ContentType;
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7ae1a98a234410e4d87de597cb89e723
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Gallop.Live.Cutt
|
||||
{
|
||||
[Serializable]
|
||||
public class LiveTimelineKeyMobCyalumeControlData : LiveTimelineKeyWithInterpolate
|
||||
{
|
||||
public Vector3 position;
|
||||
public Vector3 angle;
|
||||
public Vector3 scale = Vector3.one;
|
||||
|
||||
[NonSerialized] private Quaternion _rotationCache = Quaternion.identity;
|
||||
[NonSerialized] private bool _hasRotationCache;
|
||||
|
||||
public Quaternion GetRotation()
|
||||
{
|
||||
if (!_hasRotationCache)
|
||||
{
|
||||
_rotationCache = Quaternion.Euler(angle);
|
||||
_hasRotationCache = true;
|
||||
}
|
||||
|
||||
return _rotationCache;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class LiveTimelineKeyMobCyalumeControlDataList : LiveTimelineKeyDataListTemplate<LiveTimelineKeyMobCyalumeControlData>
|
||||
{
|
||||
public int unk48;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class LiveTimelineMobCyalumeControlData : ILiveTimelineGroupDataWithName
|
||||
{
|
||||
public LiveTimelineKeyMobCyalumeControlDataList keys;
|
||||
}
|
||||
|
||||
public delegate void MobCyalumeUpdateInfoDelegate(ref MobCyalumeUpdateInfo updateInfo);
|
||||
|
||||
public struct MobCyalumeUpdateInfo
|
||||
{
|
||||
public LiveTimelineMobCyalumeControlData data;
|
||||
public int unk0;
|
||||
public Vector3 position;
|
||||
public Quaternion rotation;
|
||||
public Vector3 scale;
|
||||
public float currentFrame;
|
||||
public float currentLiveTime;
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 280ab9377b79c584084b467f575dcf5e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Gallop.Live.Cutt
|
||||
{
|
||||
[Serializable]
|
||||
public class LiveTimelineMonitorDressCondition
|
||||
{
|
||||
public int IsCheck;
|
||||
public int CharaId;
|
||||
public int DressId;
|
||||
public bool IsEnabled => IsCheck != 0;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class LiveTimelineMonitorChangeUVSetting
|
||||
{
|
||||
public int IsChangeUVSetting;
|
||||
public int DispID = -1;
|
||||
public LiveTimelineMonitorDressCondition[] ConditionArray = Array.Empty<LiveTimelineMonitorDressCondition>();
|
||||
public bool IsEnabled => IsChangeUVSetting != 0;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class LiveTimelineKeyMonitorControlData : LiveTimelineKeyWithInterpolate
|
||||
{
|
||||
public Vector2 position;
|
||||
public Vector2 size = Vector2.one;
|
||||
public int dispID;
|
||||
public float speed = 1f;
|
||||
public string outputTextureLabel = string.Empty;
|
||||
public int playStartOffsetFrame;
|
||||
public float blendFactor = 1f;
|
||||
public Color colorFade = Color.clear;
|
||||
public Color BaseColor = Color.clear;
|
||||
public int SrcBlendMode = 1;
|
||||
public int DstBlendMode;
|
||||
public int RenderQueueNo = 2000;
|
||||
public int IsRenderQueue;
|
||||
public int DispID2 = -1;
|
||||
public float CrossFadeRate;
|
||||
public int LightImageNo;
|
||||
public int LightImageNo2;
|
||||
public float FilterTexScale = 1f;
|
||||
public LiveTimelineMonitorChangeUVSetting[] ChangeUVSettingArray = Array.Empty<LiveTimelineMonitorChangeUVSetting>();
|
||||
public int extraContent;
|
||||
|
||||
private const int AttrMulti = 65536;
|
||||
private const int AttrPlayReverse = 131072;
|
||||
private const int AttrUseMonitorCamera = 262144;
|
||||
private const int AttrForcedUseMonitorCamera = 524288;
|
||||
private const int AttrEnableBlendMode = 1048576;
|
||||
|
||||
public bool IsMultiFlag() => (((int)attribute) & AttrMulti) != 0;
|
||||
public bool IsReversePlayFlag() => (((int)attribute) & AttrPlayReverse) != 0;
|
||||
public bool IsMonitorCameraFlag() => (((int)attribute) & AttrUseMonitorCamera) != 0;
|
||||
public bool IsForcedUseMonitorCamera => (((int)attribute) & AttrForcedUseMonitorCamera) != 0;
|
||||
public bool IsEnabledBlendMode => (((int)attribute) & AttrEnableBlendMode) != 0;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class LiveTimelineKeyMonitorControlDataList : LiveTimelineKeyDataListTemplate<LiveTimelineKeyMonitorControlData> { }
|
||||
|
||||
[Serializable]
|
||||
public class LiveTimelineMonitorControlData : ILiveTimelineGroupDataWithName
|
||||
{
|
||||
public LiveTimelineKeyMonitorControlDataList keys;
|
||||
public string SafeName => string.IsNullOrWhiteSpace(name) ? "Monitor" : name;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class LiveTimelineKeyMonitorCameraPositionData : LiveTimelineKeyCameraPositionData { }
|
||||
|
||||
[Serializable]
|
||||
public class LiveTimelineKeyMonitorCameraPositionDataList : LiveTimelineKeyDataListTemplate<LiveTimelineKeyMonitorCameraPositionData> { }
|
||||
|
||||
[Serializable]
|
||||
public class LiveTimelineMonitorCameraPositionData : ILiveTimelineGroupDataWithName
|
||||
{
|
||||
public LiveTimelineKeyMonitorCameraPositionDataList keys;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class LiveTimelineKeyMonitorCameraLookAtData : LiveTimelineKeyCameraLookAtData { }
|
||||
|
||||
[Serializable]
|
||||
public class LiveTimelineKeyMonitorCameraLookAtDataList : LiveTimelineKeyDataListTemplate<LiveTimelineKeyMonitorCameraLookAtData> { }
|
||||
|
||||
[Serializable]
|
||||
public class LiveTimelineMonitorCameraLookAtData : ILiveTimelineGroupDataWithName
|
||||
{
|
||||
public LiveTimelineKeyMonitorCameraLookAtDataList keys;
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cfdc8a520019e8544b2904b4954b6182
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -952,6 +952,22 @@ namespace Gallop.Live.Cutt
|
||||
[SerializeField] public List<LiveTimelineHdrBloomData> hdrBloomKeys;
|
||||
[SerializeField] public List<LiveTimelineColorCorrectionData> colorCorrectionDataLists;
|
||||
|
||||
// Names verified against the cutt TypeTree. The capital letters are required for Unity deserialization.
|
||||
[SerializeField] public List<LiveTimelineMobCyalumeControlData> MobControlKeys;
|
||||
[SerializeField] public List<LiveTimelineMobCyalumeControlData> CyalumeControlKeys;
|
||||
[SerializeField] public List<LiveTimelineMonitorControlData> monitorControlList;
|
||||
[SerializeField] public List<LiveTimelineMonitorCameraPositionData> monitorCameraPosKeys;
|
||||
[SerializeField] public List<LiveTimelineMonitorCameraLookAtData> monitorCameraLookAtKeys;
|
||||
[SerializeField] public List<LiveTimelineLensFlareData> lensFlareList;
|
||||
[SerializeField] public List<LiveTimelineStageEnvironmentData> environmentDataLists;
|
||||
[SerializeField] public LiveTimelineFacialToonData facialToonSet;
|
||||
[SerializeField] public LiveTimelineKeyCameraLayerDataList cameraLayerKeys;
|
||||
[SerializeField] public List<LiveTimelinePropsData> propsList;
|
||||
[SerializeField] public List<LiveTimelinePropsAttachData> propsAttachList;
|
||||
[SerializeField] public LiveTimelineKeyCharaFootLightDataList charaFootLightKeys;
|
||||
[SerializeField] public List<LiveTimelineAdditionalLight> AdditionalLightList;
|
||||
[SerializeField] public List<LiveTimelineLightProjectionData> lightProjectionList;
|
||||
|
||||
// PostFilm (39):全语料 ~20200 keys / 59 首,是未实现轨道里数据量最大的一块。
|
||||
// 旧扫描把它记成「58/58 首存在但全部 0 keyframe(空占位)」,实测是错的
|
||||
// (song 1177 分别有 100 / 161 / 87 keys)。字段名取自 bundle TypeTree。
|
||||
@@ -1039,4 +1055,3 @@ namespace Gallop.Live.Cutt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
using UnityEngine;
|
||||
|
||||
// Compatibility component for scenes authored with the master4 helper. Runtime setup is now
|
||||
// owned by Director.InitializeCyalumeStage, so this type intentionally has no behavior.
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class CyalumeAutoBinder : MonoBehaviour
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 76a8f506f7e580449b2e8dee47ca1120
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,55 +1,524 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Gallop.Cyalume;
|
||||
using Gallop.Live.Cutt;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Gallop.Live.Cyalume
|
||||
{
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
public class CyalumeController3D : CyalumeControllerBase
|
||||
{
|
||||
float passedtime = 0.0f;
|
||||
float intervaltime = 0.033333333f;
|
||||
float y_offset = 0.0f;
|
||||
GameObject CyalumeObj;
|
||||
Material CyalumeMaterial;
|
||||
private void Awake()
|
||||
[SerializeField] private bool _initializedObjects;
|
||||
[SerializeField] private bool _autoSetupOnStart;
|
||||
[SerializeField] private float _forceReplaceWarmupSeconds = 1.5f;
|
||||
|
||||
private readonly List<Renderer> _defaultRenderers = new List<Renderer>();
|
||||
private readonly List<Renderer> _randomRenderers = new List<Renderer>();
|
||||
|
||||
private GameObject _defaultInstance;
|
||||
private GameObject _randomInstance;
|
||||
private bool _usingRandomTarget;
|
||||
private Coroutine _setupCoroutine;
|
||||
private Coroutine _forceReplaceCoroutine;
|
||||
|
||||
[Header("Mob")]
|
||||
[SerializeField] private bool _enableMobController = true;
|
||||
[SerializeField] private string _mobPrefabName = "mob";
|
||||
[SerializeField] private GameObject _mobRootOverride;
|
||||
[SerializeField] private Color _mobColor = Color.white;
|
||||
[SerializeField] private Color _mobAmbientColor = Color.white;
|
||||
[SerializeField] private bool _mobVerboseLog;
|
||||
|
||||
private GameObject _mobInstance;
|
||||
private MobShadowController _mobShadowController;
|
||||
private LiveTimelineControl _mobTimelineControl;
|
||||
private readonly Vector3[] _mobPositions = new Vector3[11];
|
||||
private readonly Quaternion[] _mobRotations = new Quaternion[11];
|
||||
private readonly Vector3[] _mobScales = new Vector3[11];
|
||||
private readonly bool[] _mobDirtySlots = new bool[11];
|
||||
private bool _mobHasDirty;
|
||||
|
||||
public bool UsingRandomTarget => _usingRandomTarget;
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
InitializeCyalume();
|
||||
gameObject.GetComponent<CyalumeController3D>().enabled = true;
|
||||
|
||||
base.Awake();
|
||||
if (_autoSetupOnStart)
|
||||
InitializeCyalumeObjectsOnly();
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
private void Start()
|
||||
{
|
||||
if (CyalumeMaterial == null)
|
||||
if (_autoSetupOnStart)
|
||||
StartOfficialLikeSetup();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
TryBindGroupTimeline();
|
||||
MarkGroupMatrixDirty();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
UnbindGroupTimeline();
|
||||
}
|
||||
|
||||
public void StartOfficialLikeSetup(bool forceRebuild = false)
|
||||
{
|
||||
if (_setupCoroutine != null)
|
||||
{
|
||||
StopCoroutine(_setupCoroutine);
|
||||
_setupCoroutine = null;
|
||||
}
|
||||
|
||||
_setupCoroutine = StartCoroutine(SetupOfficialLike(forceRebuild));
|
||||
}
|
||||
|
||||
public IEnumerator SetupOfficialLike(bool forceRebuild = false)
|
||||
{
|
||||
ResetRuntimeState();
|
||||
InitializeCyalumeObjectsOnly(forceRebuild);
|
||||
|
||||
int musicId = ResolveMusicId();
|
||||
if (musicId <= 0)
|
||||
{
|
||||
Debug.LogWarning("[CyalumeController3D] musicId is unavailable.");
|
||||
yield break;
|
||||
}
|
||||
|
||||
var provider = ResolvePlaybackProvider();
|
||||
provider?.InitializeForMusicId(musicId);
|
||||
|
||||
string liveKey = CyalumePlaybackProvider.BuildPrimaryLiveKey(musicId);
|
||||
LoadTextureBundlesFromIndex(liveKey);
|
||||
|
||||
ApplyTargetSelection(false, true);
|
||||
EnsureCustomShaderOnCurrentTargets();
|
||||
CollectTargetMeshAndMaterials();
|
||||
CreateVertexColorCacheOfficialConservative();
|
||||
yield return null;
|
||||
|
||||
_textureSet.Clear();
|
||||
foreach (var pair in ResolveTexSetFromLoadedBundles(liveKey))
|
||||
_textureSet[pair.Key] = pair.Value;
|
||||
yield return null;
|
||||
|
||||
InitializeAudienceUvSpreadOfficialConservative();
|
||||
RefreshRendererEnabledState();
|
||||
_isInitialized = _targetRendererList.Count > 0 && _materialArray.Length > 0;
|
||||
ForceReplaceOfficialMaterialsInMemory();
|
||||
ForceReplaceCustomShaderInHierarchy(transform, true);
|
||||
ForceReplaceOfficialShaderAcrossLoadedScene();
|
||||
LogTargetShaderSummary("SetupOfficialLike");
|
||||
WriteCyalumeSceneSnapshot("SetupOfficialLike");
|
||||
|
||||
if (_verboseLog)
|
||||
{
|
||||
Debug.Log($"[CyalumeController3D] Setup complete. liveKey={liveKey}, targets={_targetRendererList.Count}, meshes={_meshFilters.Length}, materials={_materialArray.Length}, patterns={_textureSet.Count}");
|
||||
}
|
||||
|
||||
StartForceReplaceWarmup();
|
||||
_setupCoroutine = null;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
TryBindGroupTimeline();
|
||||
|
||||
if (!_isEnabledCyalume || !_initializedObjects || !_isInitialized)
|
||||
return;
|
||||
if (passedtime >= intervaltime)
|
||||
|
||||
if (!TryGetCurrentPlayback(out int patternId, out _, out _, out _, out _))
|
||||
return;
|
||||
|
||||
bool preferRandom = _playbackProvider != null && _playbackProvider.IsRandomPattern(patternId);
|
||||
bool targetChanged = ApplyTargetSelection(preferRandom);
|
||||
bool shaderChanged = EnsureCustomShaderOnCurrentTargets() > 0;
|
||||
bool memoryChanged = ForceReplaceOfficialMaterialsInMemory() > 0;
|
||||
if (targetChanged)
|
||||
{
|
||||
y_offset += (float)0.03125;
|
||||
if (y_offset >= 1.0f)
|
||||
{
|
||||
y_offset = 0.0f;
|
||||
}
|
||||
CyalumeMaterial.SetTextureOffset("_MainTex", new Vector2(0, y_offset));
|
||||
passedtime = 0;
|
||||
ForceReplaceOfficialMaterialsInMemory();
|
||||
ForceReplaceCustomShaderInHierarchy(transform, true);
|
||||
ForceReplaceOfficialShaderAcrossLoadedScene();
|
||||
CollectTargetMeshAndMaterials();
|
||||
CreateVertexColorCacheOfficialConservative();
|
||||
InitializeAudienceUvSpreadOfficialConservative();
|
||||
}
|
||||
passedtime += Time.fixedDeltaTime;
|
||||
else if (shaderChanged || memoryChanged)
|
||||
{
|
||||
CollectTargetMeshAndMaterials();
|
||||
ForceReplaceOfficialShaderAcrossLoadedScene();
|
||||
LogTargetShaderSummary(memoryChanged ? "ForceReplaceOfficialMaterialsInMemory" : "EnsureCustomShaderOnCurrentTargets");
|
||||
}
|
||||
|
||||
UpdateCyalumeOfficialConservative(false);
|
||||
}
|
||||
|
||||
public void InitializeCyalume()
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
AssetHolder CyalumeAssetHolder = gameObject.GetComponent<AssetHolder>();
|
||||
List<StringObjectPair> CyalumeObject = CyalumeAssetHolder._assetTable.list;
|
||||
foreach (StringObjectPair obj in CyalumeObject)
|
||||
TryBindGroupTimeline();
|
||||
FlushGroupMatrix();
|
||||
FlushMobShadowIfNeeded();
|
||||
}
|
||||
|
||||
private void StartForceReplaceWarmup()
|
||||
{
|
||||
if (_forceReplaceCoroutine != null)
|
||||
{
|
||||
GameObject tempObj = Instantiate((GameObject)obj.Value, transform);
|
||||
if (obj.Value.name.Contains("cyalume")){
|
||||
CyalumeObj = tempObj;
|
||||
CyalumeMaterial = gameObject.GetComponentInChildren<MeshRenderer>().material;
|
||||
StopCoroutine(_forceReplaceCoroutine);
|
||||
_forceReplaceCoroutine = null;
|
||||
}
|
||||
|
||||
if (_forceReplaceWarmupSeconds <= 0f)
|
||||
return;
|
||||
|
||||
_forceReplaceCoroutine = StartCoroutine(ForceReplaceWarmupCoroutine());
|
||||
}
|
||||
|
||||
private IEnumerator ForceReplaceWarmupCoroutine()
|
||||
{
|
||||
float endTime = Time.unscaledTime + _forceReplaceWarmupSeconds;
|
||||
while (Time.unscaledTime < endTime)
|
||||
{
|
||||
int replacedCount = 0;
|
||||
replacedCount += ForceReplaceOfficialMaterialsInMemory();
|
||||
replacedCount += ForceReplaceCustomShaderInHierarchy(transform, true);
|
||||
replacedCount += ForceReplaceOfficialShaderAcrossLoadedScene();
|
||||
if (replacedCount > 0)
|
||||
{
|
||||
CollectTargetMeshAndMaterials();
|
||||
LogTargetShaderSummary("ForceReplaceWarmup");
|
||||
WriteCyalumeSceneSnapshot("ForceReplaceWarmup");
|
||||
}
|
||||
|
||||
yield return null;
|
||||
}
|
||||
|
||||
_forceReplaceCoroutine = null;
|
||||
}
|
||||
|
||||
public override void InitializeCyalumeObjectsOnly(bool forceRebuild = false)
|
||||
{
|
||||
if (_initializedObjects && !forceRebuild)
|
||||
return;
|
||||
|
||||
_initializedObjects = false;
|
||||
_usingRandomTarget = false;
|
||||
_defaultInstance = null;
|
||||
_randomInstance = null;
|
||||
|
||||
_allRendererList.Clear();
|
||||
_targetRendererList.Clear();
|
||||
_defaultRenderers.Clear();
|
||||
_randomRenderers.Clear();
|
||||
|
||||
var assetHolder = GetComponent<AssetHolder>();
|
||||
if (assetHolder == null || assetHolder._assetTable == null)
|
||||
{
|
||||
Debug.LogWarning("[CyalumeController3D] AssetHolder or asset table is missing.");
|
||||
return;
|
||||
}
|
||||
|
||||
DestroyExistingCyalumeChildren();
|
||||
TryInstantiateNamedPrefab(assetHolder, "default", out _defaultInstance, _defaultRenderers);
|
||||
TryInstantiateNamedPrefab(assetHolder, "random", out _randomInstance, _randomRenderers);
|
||||
InitializeMobController(assetHolder);
|
||||
|
||||
_allRendererList.AddRange(_defaultRenderers);
|
||||
_allRendererList.AddRange(_randomRenderers);
|
||||
RefreshRendererEnabledState();
|
||||
|
||||
_initializedObjects = true;
|
||||
ApplyTargetSelection(false, true);
|
||||
}
|
||||
|
||||
public override bool ApplyTargetSelection(bool preferRandom, bool forceRefresh = false)
|
||||
{
|
||||
if (!_initializedObjects)
|
||||
{
|
||||
InitializeCyalumeObjectsOnly();
|
||||
if (!_initializedObjects)
|
||||
return false;
|
||||
}
|
||||
|
||||
bool useRandom = preferRandom || _defaultRenderers.Count == 0;
|
||||
if (!useRandom && _defaultRenderers.Count == 0 && _randomRenderers.Count > 0)
|
||||
useRandom = true;
|
||||
|
||||
bool changed = forceRefresh || _usingRandomTarget != useRandom || _targetRendererList.Count == 0;
|
||||
_usingRandomTarget = useRandom;
|
||||
|
||||
_targetRendererList.Clear();
|
||||
if (_usingRandomTarget)
|
||||
_targetRendererList.AddRange(_randomRenderers.Count > 0 ? _randomRenderers : _defaultRenderers);
|
||||
else
|
||||
_targetRendererList.AddRange(_defaultRenderers.Count > 0 ? _defaultRenderers : _randomRenderers);
|
||||
|
||||
RefreshRendererEnabledState();
|
||||
|
||||
if (changed && _verboseLog)
|
||||
{
|
||||
Debug.Log($"[CyalumeController3D] Target selection -> {(_usingRandomTarget ? "random" : "default")}, count={_targetRendererList.Count}");
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
public bool ContainsTargetRenderer(Renderer renderer)
|
||||
{
|
||||
if (renderer == null)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < _targetRendererList.Count; i++)
|
||||
{
|
||||
if (_targetRendererList[i] == renderer)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool ContainsAnyRenderer(Renderer renderer)
|
||||
{
|
||||
if (renderer == null)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < _allRendererList.Count; i++)
|
||||
{
|
||||
if (_allRendererList[i] == renderer)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void TryBindGroupTimeline()
|
||||
{
|
||||
var director = Gallop.Live.Director.instance;
|
||||
if (director == null)
|
||||
return;
|
||||
|
||||
var timeline = director._liveTimelineControl;
|
||||
if (timeline == null || ReferenceEquals(_mobTimelineControl, timeline))
|
||||
return;
|
||||
|
||||
UnbindGroupTimeline();
|
||||
_mobTimelineControl = timeline;
|
||||
_mobTimelineControl.OnUpdateCyalumeControl += OnCyalumeControl;
|
||||
|
||||
if (_enableMobController)
|
||||
_mobTimelineControl.OnUpdateMobControl += OnMobControl;
|
||||
}
|
||||
|
||||
private void UnbindGroupTimeline()
|
||||
{
|
||||
if (_mobTimelineControl == null)
|
||||
return;
|
||||
|
||||
_mobTimelineControl.OnUpdateCyalumeControl -= OnCyalumeControl;
|
||||
_mobTimelineControl.OnUpdateMobControl -= OnMobControl;
|
||||
_mobTimelineControl = null;
|
||||
}
|
||||
|
||||
private void InitializeMobController(AssetHolder assetHolder)
|
||||
{
|
||||
_mobInstance = null;
|
||||
_mobShadowController = null;
|
||||
_mobHasDirty = false;
|
||||
Array.Clear(_mobDirtySlots, 0, _mobDirtySlots.Length);
|
||||
|
||||
if (!_enableMobController)
|
||||
return;
|
||||
|
||||
GameObject mobRoot = _mobRootOverride;
|
||||
if (!mobRoot && assetHolder != null && !string.IsNullOrEmpty(_mobPrefabName))
|
||||
{
|
||||
TryInstantiateNamedPrefab(assetHolder, _mobPrefabName, out _mobInstance, null);
|
||||
mobRoot = _mobInstance;
|
||||
}
|
||||
|
||||
if (!mobRoot)
|
||||
{
|
||||
if (_mobVerboseLog)
|
||||
Debug.LogWarning($"[CyalumeController3D] Mob root not found. prefabName='{_mobPrefabName}'");
|
||||
return;
|
||||
}
|
||||
|
||||
_mobShadowController = mobRoot.GetComponent<MobShadowController>();
|
||||
if (_mobShadowController == null)
|
||||
_mobShadowController = mobRoot.AddComponent<MobShadowController>();
|
||||
|
||||
_mobShadowController.Initialize(mobRoot);
|
||||
_mobShadowController.SetMobColor(_mobColor);
|
||||
_mobShadowController.SetAmbientColor(_mobAmbientColor);
|
||||
|
||||
if (_mobVerboseLog)
|
||||
Debug.Log($"[CyalumeController3D] Mob controller initialized on '{mobRoot.name}'.");
|
||||
}
|
||||
|
||||
private void OnCyalumeControl(ref MobCyalumeUpdateInfo info)
|
||||
{
|
||||
int index = ResolveGroupIndex(info);
|
||||
if ((uint)index >= 11u)
|
||||
return;
|
||||
|
||||
var position = info.position;
|
||||
var rotation = info.rotation;
|
||||
var scale = info.scale;
|
||||
SetGroupTRS(index, ref position, ref rotation, ref scale);
|
||||
}
|
||||
|
||||
private void OnMobControl(ref MobCyalumeUpdateInfo info)
|
||||
{
|
||||
int index = ResolveGroupIndex(info);
|
||||
if ((uint)index >= 11u)
|
||||
return;
|
||||
|
||||
_mobPositions[index] = info.position;
|
||||
_mobRotations[index] = info.rotation;
|
||||
_mobScales[index] = info.scale;
|
||||
_mobDirtySlots[index] = true;
|
||||
_mobHasDirty = true;
|
||||
}
|
||||
|
||||
private static int ResolveGroupIndex(MobCyalumeUpdateInfo info)
|
||||
{
|
||||
if ((uint)info.unk0 < 11u)
|
||||
return info.unk0;
|
||||
|
||||
if (info.data != null && info.data.keys != null && (uint)info.data.keys.unk48 < 11u)
|
||||
return info.data.keys.unk48;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private void FlushMobShadowIfNeeded()
|
||||
{
|
||||
if (!_mobHasDirty)
|
||||
return;
|
||||
|
||||
if (_mobShadowController == null)
|
||||
{
|
||||
var assetHolder = GetComponent<AssetHolder>();
|
||||
InitializeMobController(assetHolder);
|
||||
if (_mobShadowController == null)
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < _mobDirtySlots.Length; i++)
|
||||
{
|
||||
if (!_mobDirtySlots[i])
|
||||
continue;
|
||||
|
||||
var position = _mobPositions[i];
|
||||
var rotation = _mobRotations[i];
|
||||
var scale = _mobScales[i];
|
||||
_mobShadowController.SetGroupTRS(i, ref position, ref rotation, ref scale);
|
||||
_mobDirtySlots[i] = false;
|
||||
}
|
||||
|
||||
_mobShadowController.FlushGroupMatrix();
|
||||
_mobHasDirty = false;
|
||||
}
|
||||
|
||||
private void DestroyExistingCyalumeChildren()
|
||||
{
|
||||
for (int i = transform.childCount - 1; i >= 0; i--)
|
||||
{
|
||||
var child = transform.GetChild(i);
|
||||
if (child == null)
|
||||
continue;
|
||||
|
||||
string childName = child.name ?? string.Empty;
|
||||
bool isKnownCyalumeChild =
|
||||
childName.Equals("default", StringComparison.OrdinalIgnoreCase) ||
|
||||
childName.Equals("random", StringComparison.OrdinalIgnoreCase) ||
|
||||
(!string.IsNullOrEmpty(_mobPrefabName) && childName.Equals(_mobPrefabName, StringComparison.OrdinalIgnoreCase)) ||
|
||||
childName.IndexOf("cyalume", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
|
||||
if (isKnownCyalumeChild)
|
||||
Destroy(child.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryInstantiateNamedPrefab(AssetHolder assetHolder, string assetName, out GameObject instance, List<Renderer> destination)
|
||||
{
|
||||
instance = null;
|
||||
destination?.Clear();
|
||||
|
||||
if (!TryResolvePrefab(assetHolder, assetName, out var prefab) || !prefab)
|
||||
{
|
||||
if (_verboseLog)
|
||||
Debug.LogWarning($"[CyalumeController3D] Missing cyalume prefab: {assetName}");
|
||||
return false;
|
||||
}
|
||||
|
||||
instance = Instantiate(prefab, transform);
|
||||
instance.name = prefab.name;
|
||||
SetLayerRecursively(instance, gameObject.layer);
|
||||
|
||||
if (destination != null)
|
||||
{
|
||||
var renderers = instance.GetComponentsInChildren<Renderer>(true);
|
||||
ApplyCustomShaderOverrideToRenderers(renderers);
|
||||
destination.AddRange(renderers);
|
||||
}
|
||||
|
||||
if (_verboseLog)
|
||||
{
|
||||
Debug.Log($"[CyalumeController3D] Spawned cyalume prefab '{assetName}' renderers={destination?.Count ?? 0}");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void SetLayerRecursively(GameObject root, int layer)
|
||||
{
|
||||
if (!root)
|
||||
return;
|
||||
|
||||
root.layer = layer;
|
||||
for (int i = 0; i < root.transform.childCount; i++)
|
||||
{
|
||||
var child = root.transform.GetChild(i);
|
||||
if (child)
|
||||
SetLayerRecursively(child.gameObject, layer);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryResolvePrefab(AssetHolder assetHolder, string assetName, out GameObject prefab)
|
||||
{
|
||||
prefab = null;
|
||||
if (assetHolder == null || assetHolder._assetTable == null || assetHolder._assetTable.list == null)
|
||||
return false;
|
||||
|
||||
foreach (var pair in assetHolder._assetTable.list)
|
||||
{
|
||||
var candidate = pair.Value as GameObject;
|
||||
if (!candidate)
|
||||
continue;
|
||||
|
||||
if (candidate.name.Equals(assetName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
prefab = candidate;
|
||||
return true;
|
||||
}
|
||||
|
||||
string keyText = pair.Key != null ? pair.Key.ToString() : string.Empty;
|
||||
if (!string.IsNullOrEmpty(keyText) && keyText.Equals(assetName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
prefab = candidate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,903 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using Gallop.Live;
|
||||
|
||||
namespace Gallop.Live.Cyalume
|
||||
{
|
||||
[Serializable]
|
||||
public class CyalumePlaybackData
|
||||
{
|
||||
public int DataNo;
|
||||
public int PatternId;
|
||||
public float StartTime;
|
||||
public float PlaySpeed = 1f;
|
||||
public int ChoreographyType;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class CyalumePatternDefinition
|
||||
{
|
||||
public int PatternId;
|
||||
public string MoveType;
|
||||
public string ColorPattern;
|
||||
public string[] Colors = Array.Empty<string>();
|
||||
public string[] Widths = Array.Empty<string>();
|
||||
public string Signature;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class CyalumePartFrame
|
||||
{
|
||||
public float StartTime;
|
||||
public int[] States = new int[7];
|
||||
public float[] Volumes = new float[7];
|
||||
public float[] Pans = new float[7];
|
||||
public float VolumeRate = 1f;
|
||||
|
||||
public int GetState(int zoneIndex)
|
||||
{
|
||||
if (States == null || zoneIndex < 0 || zoneIndex >= States.Length)
|
||||
return 0;
|
||||
return States[zoneIndex];
|
||||
}
|
||||
|
||||
public bool IsActive(int zoneIndex)
|
||||
{
|
||||
return GetState(zoneIndex) != 0;
|
||||
}
|
||||
}
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
public class CyalumePlaybackProvider : MonoBehaviour
|
||||
{
|
||||
[Header("Loaded playback data used by the controller.")]
|
||||
public List<CyalumePlaybackData> data = new List<CyalumePlaybackData>();
|
||||
|
||||
[Header("Debug: unique choreography pattern definitions built from the CSV.")]
|
||||
public List<CyalumePatternDefinition> patternDefinitions = new List<CyalumePatternDefinition>();
|
||||
|
||||
[Header("Loaded part data.")]
|
||||
public List<CyalumePartFrame> partFrames = new List<CyalumePartFrame>();
|
||||
|
||||
[Header("Auto-load mXXXX_cyalume.csv for the current live.")]
|
||||
public bool autoLoadForCurrentLive = true;
|
||||
public bool preferResourcesCsv = true;
|
||||
public bool tryLoadBundleEntryFromIndex = true;
|
||||
public bool verboseLog = true;
|
||||
public float bpmReference = 140f;
|
||||
public bool keepPreviousPatternOnPause = true;
|
||||
|
||||
[SerializeField] private int _loadedMusicId;
|
||||
[SerializeField] private string _loadedCsvSource;
|
||||
[SerializeField] private string _loadedPartCsvSource;
|
||||
|
||||
public int LoadedMusicId => _loadedMusicId;
|
||||
public string LoadedCsvSource => _loadedCsvSource;
|
||||
public string LoadedPartCsvSource => _loadedPartCsvSource;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (!autoLoadForCurrentLive || _loadedMusicId > 0 || (data != null && data.Count > 0))
|
||||
return;
|
||||
|
||||
if (Director.instance != null && Director.instance.live != null)
|
||||
{
|
||||
int resolvedMusicId = Director.instance.live.MusicId;
|
||||
if (resolvedMusicId > 0)
|
||||
InitializeForMusicId(resolvedMusicId);
|
||||
}
|
||||
}
|
||||
|
||||
public bool InitializeForMusicId(int musicId, bool forceReload = false)
|
||||
{
|
||||
if (musicId <= 0)
|
||||
{
|
||||
if (verboseLog)
|
||||
Debug.LogWarning("[CyalumePlaybackProvider] InitializeForMusicId failed: invalid musicId.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!forceReload && _loadedMusicId == musicId && data.Count > 0)
|
||||
return true;
|
||||
|
||||
data.Clear();
|
||||
patternDefinitions.Clear();
|
||||
partFrames.Clear();
|
||||
|
||||
_loadedMusicId = musicId;
|
||||
_loadedCsvSource = null;
|
||||
_loadedPartCsvSource = null;
|
||||
|
||||
if (!TryLoadCsvText(musicId, out string csvText, out string sourceName) || string.IsNullOrEmpty(csvText))
|
||||
{
|
||||
if (verboseLog)
|
||||
Debug.LogWarning($"[CyalumePlaybackProvider] Failed to resolve cyalume CSV for musicId={musicId}.");
|
||||
return false;
|
||||
}
|
||||
|
||||
_loadedCsvSource = sourceName;
|
||||
BuildPlaybackDataFromCsv(csvText);
|
||||
|
||||
if (TryLoadPartCsvText(musicId, out string partCsvText, out string partSource) && !string.IsNullOrEmpty(partCsvText))
|
||||
{
|
||||
_loadedPartCsvSource = partSource;
|
||||
BuildPartFramesFromCsv(partCsvText);
|
||||
if (verboseLog)
|
||||
Debug.Log($"[CyalumePlaybackProvider] Loaded part CSV: musicId={musicId}, source={partSource}, rows={partFrames.Count}");
|
||||
}
|
||||
|
||||
if (verboseLog)
|
||||
{
|
||||
Debug.Log($"[CyalumePlaybackProvider] Loaded cyalume CSV: musicId={musicId}, source={sourceName}, rows={data.Count}, uniquePatterns={patternDefinitions.Count}");
|
||||
}
|
||||
|
||||
return data.Count > 0;
|
||||
}
|
||||
|
||||
public bool TryGetCurrent(float liveTime, out CyalumePlaybackData current)
|
||||
{
|
||||
current = null;
|
||||
if (data == null || data.Count == 0)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < data.Count; i++)
|
||||
{
|
||||
var item = data[i];
|
||||
if (item == null)
|
||||
continue;
|
||||
|
||||
if (item.StartTime <= liveTime)
|
||||
current = item;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
return current != null;
|
||||
}
|
||||
|
||||
public bool TryGetCurrentPartFrame(float liveTime, out CyalumePartFrame current)
|
||||
{
|
||||
current = null;
|
||||
if (partFrames == null || partFrames.Count == 0)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < partFrames.Count; i++)
|
||||
{
|
||||
var item = partFrames[i];
|
||||
if (item == null)
|
||||
continue;
|
||||
|
||||
if (item.StartTime <= liveTime)
|
||||
current = item;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
return current != null;
|
||||
}
|
||||
|
||||
public bool TryGetCurrentState(float liveTime, out int patternId, out float patternStartTime, out float playSpeed, out int choreographyType)
|
||||
{
|
||||
if (TryGetCurrent(liveTime, out var current) && current != null)
|
||||
{
|
||||
patternId = Mathf.Max(0, current.PatternId);
|
||||
patternStartTime = current.StartTime;
|
||||
playSpeed = current.PlaySpeed > 0f ? current.PlaySpeed : 1f;
|
||||
choreographyType = current.ChoreographyType;
|
||||
return true;
|
||||
}
|
||||
|
||||
patternId = 0;
|
||||
patternStartTime = 0f;
|
||||
playSpeed = 1f;
|
||||
choreographyType = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryGetPatternDefinition(int patternId, out CyalumePatternDefinition definition)
|
||||
{
|
||||
definition = null;
|
||||
if (patternDefinitions == null || patternDefinitions.Count == 0)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < patternDefinitions.Count; i++)
|
||||
{
|
||||
var item = patternDefinitions[i];
|
||||
if (item != null && item.PatternId == patternId)
|
||||
{
|
||||
definition = item;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool IsRandomPattern(int patternId)
|
||||
{
|
||||
return TryGetPatternDefinition(patternId, out var definition) &&
|
||||
!string.IsNullOrEmpty(definition.ColorPattern) &&
|
||||
definition.ColorPattern.StartsWith("Random", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static string BuildPrimaryLiveKey(int musicId)
|
||||
{
|
||||
return $"m{musicId}";
|
||||
}
|
||||
|
||||
public static string[] BuildLiveKeyCandidates(int musicId)
|
||||
{
|
||||
var keys = new List<string>();
|
||||
string raw = $"m{musicId}";
|
||||
keys.Add(raw);
|
||||
|
||||
string padded4 = $"m{musicId:D4}";
|
||||
if (!keys.Any(x => string.Equals(x, padded4, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
keys.Add(padded4);
|
||||
}
|
||||
|
||||
return keys.ToArray();
|
||||
}
|
||||
|
||||
private bool TryLoadCsvText(int musicId, out string csvText, out string sourceName)
|
||||
{
|
||||
csvText = null;
|
||||
sourceName = null;
|
||||
|
||||
string[] liveKeys = BuildLiveKeyCandidates(musicId);
|
||||
string[] resourcePaths = BuildResourcePathCandidates(liveKeys);
|
||||
|
||||
if (preferResourcesCsv)
|
||||
{
|
||||
for (int i = 0; i < resourcePaths.Length; i++)
|
||||
{
|
||||
string resourcePath = resourcePaths[i];
|
||||
if (string.IsNullOrEmpty(resourcePath))
|
||||
continue;
|
||||
|
||||
TextAsset asset = null;
|
||||
try
|
||||
{
|
||||
asset = Resources.Load<TextAsset>(resourcePath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
asset = null;
|
||||
}
|
||||
|
||||
if (asset != null && !string.IsNullOrEmpty(asset.text))
|
||||
{
|
||||
csvText = asset.text;
|
||||
sourceName = $"Resources/{resourcePath}";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (TryFindCsvInLoadedBundles(liveKeys, out csvText, out sourceName))
|
||||
return true;
|
||||
|
||||
if (tryLoadBundleEntryFromIndex)
|
||||
{
|
||||
TryLoadCsvBundlesFromIndex(liveKeys);
|
||||
if (TryFindCsvInLoadedBundles(liveKeys, out csvText, out sourceName))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryLoadPartCsvText(int musicId, out string csvText, out string sourceName)
|
||||
{
|
||||
csvText = null;
|
||||
sourceName = null;
|
||||
|
||||
string[] liveKeys = BuildLiveKeyCandidates(musicId);
|
||||
|
||||
for (int i = 0; i < liveKeys.Length; i++)
|
||||
{
|
||||
string liveKey = liveKeys[i];
|
||||
string[] candidates =
|
||||
{
|
||||
$"live/musicscores/{liveKey}/{liveKey}_part",
|
||||
$"live/musicscores/{liveKey}/{liveKey}_part.csv",
|
||||
$"{liveKey}_part"
|
||||
};
|
||||
|
||||
for (int j = 0; j < candidates.Length; j++)
|
||||
{
|
||||
TextAsset asset = null;
|
||||
try
|
||||
{
|
||||
asset = Resources.Load<TextAsset>(candidates[j]);
|
||||
}
|
||||
catch
|
||||
{
|
||||
asset = null;
|
||||
}
|
||||
|
||||
if (asset != null && !string.IsNullOrEmpty(asset.text))
|
||||
{
|
||||
csvText = asset.text;
|
||||
sourceName = $"Resources/{candidates[j]}";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (TryFindAuxCsvInLoadedBundles(liveKeys, "part", out csvText, out sourceName))
|
||||
return true;
|
||||
|
||||
if (tryLoadBundleEntryFromIndex)
|
||||
{
|
||||
TryLoadAuxCsvBundlesFromIndex(liveKeys, "part");
|
||||
if (TryFindAuxCsvInLoadedBundles(liveKeys, "part", out csvText, out sourceName))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string[] BuildResourcePathCandidates(string[] liveKeys)
|
||||
{
|
||||
var candidates = new List<string>();
|
||||
for (int i = 0; i < liveKeys.Length; i++)
|
||||
{
|
||||
string liveKey = liveKeys[i];
|
||||
candidates.Add($"live/musicscores/{liveKey}/{liveKey}_cyalume");
|
||||
candidates.Add($"live/musicscores/{liveKey}/{liveKey}_cyalume.csv");
|
||||
candidates.Add($"{liveKey}_cyalume");
|
||||
}
|
||||
return candidates.Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
}
|
||||
|
||||
private void TryLoadCsvBundlesFromIndex(string[] liveKeys)
|
||||
{
|
||||
var main = FindObjectOfType<UmaViewerMain>();
|
||||
if (main == null || main.AbList == null)
|
||||
return;
|
||||
|
||||
var entries = main.AbList.Values
|
||||
.Where(e => e != null && !string.IsNullOrEmpty(e.Name))
|
||||
.Where(e => liveKeys.Any(liveKey =>
|
||||
e.Name.IndexOf($"live/musicscores/{liveKey}/", StringComparison.OrdinalIgnoreCase) >= 0 &&
|
||||
e.Name.IndexOf($"{liveKey}_cyalume", StringComparison.OrdinalIgnoreCase) >= 0))
|
||||
.ToList();
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
try
|
||||
{
|
||||
UmaAssetManager.LoadAssetBundle(entry, neverUnload: true, isRecursive: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (verboseLog)
|
||||
{
|
||||
Debug.LogWarning($"[CyalumePlaybackProvider] Failed to load CSV bundle entry {entry?.Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void TryLoadAuxCsvBundlesFromIndex(string[] liveKeys, string suffix)
|
||||
{
|
||||
var main = FindObjectOfType<UmaViewerMain>();
|
||||
if (main == null || main.AbList == null)
|
||||
return;
|
||||
|
||||
var entries = main.AbList.Values
|
||||
.Where(e => e != null && !string.IsNullOrEmpty(e.Name))
|
||||
.Where(e => liveKeys.Any(liveKey =>
|
||||
e.Name.IndexOf($"live/musicscores/{liveKey}/", StringComparison.OrdinalIgnoreCase) >= 0 &&
|
||||
e.Name.IndexOf($"{liveKey}_{suffix}", StringComparison.OrdinalIgnoreCase) >= 0))
|
||||
.ToList();
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
try
|
||||
{
|
||||
UmaAssetManager.LoadAssetBundle(entry, neverUnload: true, isRecursive: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (verboseLog)
|
||||
{
|
||||
Debug.LogWarning($"[CyalumePlaybackProvider] Failed to load {suffix} CSV bundle entry {entry?.Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryFindCsvInLoadedBundles(string[] liveKeys, out string csvText, out string sourceName)
|
||||
{
|
||||
csvText = null;
|
||||
sourceName = null;
|
||||
|
||||
foreach (var assetBundle in AssetBundle.GetAllLoadedAssetBundles())
|
||||
{
|
||||
if (!assetBundle)
|
||||
continue;
|
||||
|
||||
string[] names;
|
||||
try
|
||||
{
|
||||
names = assetBundle.GetAllAssetNames();
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (names == null || names.Length == 0)
|
||||
continue;
|
||||
|
||||
for (int i = 0; i < names.Length; i++)
|
||||
{
|
||||
string path = names[i];
|
||||
if (!IsMatchingCyalumeCsvPath(path, liveKeys))
|
||||
continue;
|
||||
|
||||
TextAsset textAsset = null;
|
||||
try
|
||||
{
|
||||
textAsset = assetBundle.LoadAsset<TextAsset>(path);
|
||||
}
|
||||
catch
|
||||
{
|
||||
textAsset = null;
|
||||
}
|
||||
|
||||
if (textAsset == null || string.IsNullOrEmpty(textAsset.text))
|
||||
continue;
|
||||
|
||||
csvText = textAsset.text;
|
||||
sourceName = path;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryFindAuxCsvInLoadedBundles(string[] liveKeys, string suffix, out string csvText, out string sourceName)
|
||||
{
|
||||
csvText = null;
|
||||
sourceName = null;
|
||||
|
||||
foreach (var assetBundle in AssetBundle.GetAllLoadedAssetBundles())
|
||||
{
|
||||
if (!assetBundle)
|
||||
continue;
|
||||
|
||||
string[] names;
|
||||
try
|
||||
{
|
||||
names = assetBundle.GetAllAssetNames();
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (names == null || names.Length == 0)
|
||||
continue;
|
||||
|
||||
for (int i = 0; i < names.Length; i++)
|
||||
{
|
||||
string path = names[i];
|
||||
if (!IsMatchingAuxCsvPath(path, liveKeys, suffix))
|
||||
continue;
|
||||
|
||||
TextAsset textAsset = null;
|
||||
try
|
||||
{
|
||||
textAsset = assetBundle.LoadAsset<TextAsset>(path);
|
||||
}
|
||||
catch
|
||||
{
|
||||
textAsset = null;
|
||||
}
|
||||
|
||||
if (textAsset == null || string.IsNullOrEmpty(textAsset.text))
|
||||
continue;
|
||||
|
||||
csvText = textAsset.text;
|
||||
sourceName = path;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsMatchingCyalumeCsvPath(string path, string[] liveKeys)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path) || liveKeys == null || liveKeys.Length == 0)
|
||||
return false;
|
||||
|
||||
string normalized = path.Replace('\\', '/');
|
||||
for (int i = 0; i < liveKeys.Length; i++)
|
||||
{
|
||||
string liveKey = liveKeys[i];
|
||||
if (normalized.IndexOf($"live/musicscores/{liveKey}/", StringComparison.OrdinalIgnoreCase) < 0)
|
||||
continue;
|
||||
|
||||
string fileName = Path.GetFileNameWithoutExtension(normalized);
|
||||
if (string.Equals(fileName, $"{liveKey}_cyalume", StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsMatchingAuxCsvPath(string path, string[] liveKeys, string suffix)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path) || liveKeys == null || liveKeys.Length == 0)
|
||||
return false;
|
||||
|
||||
string normalized = path.Replace('\\', '/');
|
||||
for (int i = 0; i < liveKeys.Length; i++)
|
||||
{
|
||||
string liveKey = liveKeys[i];
|
||||
if (normalized.IndexOf($"live/musicscores/{liveKey}/", StringComparison.OrdinalIgnoreCase) < 0)
|
||||
continue;
|
||||
|
||||
string fileName = Path.GetFileNameWithoutExtension(normalized);
|
||||
if (string.Equals(fileName, $"{liveKey}_{suffix}", StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void BuildPlaybackDataFromCsv(string csvText)
|
||||
{
|
||||
data.Clear();
|
||||
patternDefinitions.Clear();
|
||||
|
||||
if (string.IsNullOrEmpty(csvText))
|
||||
return;
|
||||
|
||||
var rows = ParseCsvRows(csvText);
|
||||
if (rows.Count <= 1)
|
||||
return;
|
||||
|
||||
var headerMap = BuildHeaderMap(rows[0]);
|
||||
if (!headerMap.ContainsKey("time"))
|
||||
{
|
||||
if (verboseLog)
|
||||
Debug.LogWarning("[CyalumePlaybackProvider] CSV missing time column.");
|
||||
return;
|
||||
}
|
||||
|
||||
var patternMap = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
int lastVisualPatternId = 0;
|
||||
bool hasLastVisualPattern = false;
|
||||
|
||||
for (int rowIndex = 1; rowIndex < rows.Count; rowIndex++)
|
||||
{
|
||||
var row = rows[rowIndex];
|
||||
if (row == null || row.Count == 0)
|
||||
continue;
|
||||
|
||||
if (!TryParseTimeSeconds(GetCell(row, headerMap, "time"), out float startTime))
|
||||
continue;
|
||||
|
||||
string moveType = NormalizeCell(GetCell(row, headerMap, "move_type"));
|
||||
string colorPattern = NormalizeCell(GetCell(row, headerMap, "color_pattern"));
|
||||
string[] colors = GetSeriesCells(row, headerMap, "color", 5);
|
||||
string[] widths = GetSeriesCells(row, headerMap, "width", 5);
|
||||
|
||||
int choreographyType = ComputeChoreographyType(moveType);
|
||||
bool isPauseLike = keepPreviousPatternOnPause && choreographyType >= 8;
|
||||
|
||||
int patternId;
|
||||
if (isPauseLike)
|
||||
{
|
||||
patternId = hasLastVisualPattern ? lastVisualPatternId : 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
string signature = BuildPatternSignature(moveType, colorPattern, colors, widths);
|
||||
if (!patternMap.TryGetValue(signature, out patternId))
|
||||
{
|
||||
patternId = patternMap.Count;
|
||||
patternMap[signature] = patternId;
|
||||
|
||||
patternDefinitions.Add(new CyalumePatternDefinition
|
||||
{
|
||||
PatternId = patternId,
|
||||
MoveType = moveType,
|
||||
ColorPattern = colorPattern,
|
||||
Colors = colors.ToArray(),
|
||||
Widths = widths.ToArray(),
|
||||
Signature = signature,
|
||||
});
|
||||
}
|
||||
|
||||
lastVisualPatternId = patternId;
|
||||
hasLastVisualPattern = true;
|
||||
}
|
||||
|
||||
float playSpeed = ComputePlaySpeed(GetCell(row, headerMap, "bpm"));
|
||||
|
||||
data.Add(new CyalumePlaybackData
|
||||
{
|
||||
DataNo = data.Count,
|
||||
PatternId = patternId,
|
||||
StartTime = startTime,
|
||||
PlaySpeed = playSpeed,
|
||||
ChoreographyType = choreographyType,
|
||||
});
|
||||
}
|
||||
|
||||
data.Sort((a, b) => a.StartTime.CompareTo(b.StartTime));
|
||||
for (int i = 0; i < data.Count; i++)
|
||||
{
|
||||
data[i].DataNo = i;
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildPartFramesFromCsv(string csvText)
|
||||
{
|
||||
partFrames.Clear();
|
||||
if (string.IsNullOrEmpty(csvText))
|
||||
return;
|
||||
|
||||
var rows = ParseCsvRows(csvText);
|
||||
if (rows.Count <= 1)
|
||||
return;
|
||||
|
||||
var headerMap = BuildHeaderMap(rows[0]);
|
||||
if (!headerMap.ContainsKey("time"))
|
||||
{
|
||||
if (verboseLog)
|
||||
Debug.LogWarning("[CyalumePlaybackProvider] Part CSV missing time column.");
|
||||
return;
|
||||
}
|
||||
|
||||
string[] zoneNames = { "left3", "left2", "left", "center", "right", "right2", "right3" };
|
||||
|
||||
for (int rowIndex = 1; rowIndex < rows.Count; rowIndex++)
|
||||
{
|
||||
var row = rows[rowIndex];
|
||||
if (row == null || row.Count == 0)
|
||||
continue;
|
||||
|
||||
if (!TryParseTimeSeconds(GetCell(row, headerMap, "time"), out float startTime))
|
||||
continue;
|
||||
|
||||
var frame = new CyalumePartFrame();
|
||||
frame.StartTime = startTime;
|
||||
|
||||
for (int i = 0; i < zoneNames.Length; i++)
|
||||
{
|
||||
frame.States[i] = ParseIntOrDefault(GetCell(row, headerMap, zoneNames[i]), 0);
|
||||
frame.Volumes[i] = ParseFloatOrSentinel(GetCell(row, headerMap, zoneNames[i] + "_vol"));
|
||||
frame.Pans[i] = ParseFloatOrSentinel(GetCell(row, headerMap, zoneNames[i] + "_pan"));
|
||||
}
|
||||
|
||||
frame.VolumeRate = ParseFloatOrDefault(GetCell(row, headerMap, "volume_rate"), 1f);
|
||||
partFrames.Add(frame);
|
||||
}
|
||||
|
||||
partFrames.Sort((a, b) => a.StartTime.CompareTo(b.StartTime));
|
||||
}
|
||||
|
||||
private static Dictionary<string, int> BuildHeaderMap(List<string> header)
|
||||
{
|
||||
var map = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
if (header == null)
|
||||
return map;
|
||||
|
||||
for (int i = 0; i < header.Count; i++)
|
||||
{
|
||||
string key = NormalizeHeader(header[i]);
|
||||
if (string.IsNullOrEmpty(key) || map.ContainsKey(key))
|
||||
continue;
|
||||
map[key] = i;
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
private static string NormalizeHeader(string value)
|
||||
{
|
||||
return (value ?? string.Empty).Trim().Trim('\ufeff').ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static string GetCell(List<string> row, Dictionary<string, int> headerMap, string key)
|
||||
{
|
||||
if (row == null || headerMap == null || !headerMap.TryGetValue(key, out int index))
|
||||
return null;
|
||||
|
||||
return index >= 0 && index < row.Count ? row[index] : null;
|
||||
}
|
||||
|
||||
private static string[] GetSeriesCells(List<string> row, Dictionary<string, int> headerMap, string prefix, int count)
|
||||
{
|
||||
var values = new string[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
values[i] = NormalizeCell(GetCell(row, headerMap, $"{prefix}{i + 1}"));
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
private static string NormalizeCell(string value)
|
||||
{
|
||||
return (value ?? string.Empty).Trim();
|
||||
}
|
||||
|
||||
private static string BuildPatternSignature(string moveType, string colorPattern, string[] colors, string[] widths)
|
||||
{
|
||||
var parts = new List<string>(2 + (colors?.Length ?? 0) + (widths?.Length ?? 0))
|
||||
{
|
||||
NormalizeSignatureValue(moveType),
|
||||
NormalizeSignatureValue(colorPattern)
|
||||
};
|
||||
|
||||
if (colors != null)
|
||||
{
|
||||
for (int i = 0; i < colors.Length; i++)
|
||||
parts.Add(NormalizeSignatureValue(colors[i]));
|
||||
}
|
||||
|
||||
if (widths != null)
|
||||
{
|
||||
for (int i = 0; i < widths.Length; i++)
|
||||
parts.Add(NormalizeSignatureValue(widths[i]));
|
||||
}
|
||||
|
||||
return string.Join("|", parts);
|
||||
}
|
||||
|
||||
private static string NormalizeSignatureValue(string value)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(value) ? "-" : value.Trim().ToLowerInvariant();
|
||||
}
|
||||
|
||||
private bool TryParseTimeSeconds(string rawValue, out float seconds)
|
||||
{
|
||||
seconds = 0f;
|
||||
if (string.IsNullOrWhiteSpace(rawValue))
|
||||
return false;
|
||||
|
||||
rawValue = rawValue.Trim();
|
||||
if (int.TryParse(rawValue, out int milliseconds))
|
||||
{
|
||||
seconds = milliseconds / 1000f;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (float.TryParse(rawValue, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float value))
|
||||
{
|
||||
seconds = value > 1000f ? (value / 1000f) : value;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private float ComputePlaySpeed(string bpmValue)
|
||||
{
|
||||
float fallback = 1f;
|
||||
if (string.IsNullOrWhiteSpace(bpmValue))
|
||||
return fallback;
|
||||
|
||||
if (!float.TryParse(bpmValue, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float bpm))
|
||||
return fallback;
|
||||
|
||||
if (bpm <= 0f)
|
||||
return fallback;
|
||||
|
||||
float reference = bpmReference > 0f ? bpmReference : 140f;
|
||||
return Mathf.Max(0.01f, bpm / reference);
|
||||
}
|
||||
|
||||
private static int ComputeChoreographyType(string moveType)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(moveType))
|
||||
return 0;
|
||||
|
||||
return string.Equals(moveType.Trim(), "Pause", StringComparison.OrdinalIgnoreCase) ? 8 : 0;
|
||||
}
|
||||
|
||||
private static int ParseIntOrDefault(string raw, int fallback)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw))
|
||||
return fallback;
|
||||
|
||||
return int.TryParse(raw.Trim(), out int value) ? value : fallback;
|
||||
}
|
||||
|
||||
private static float ParseFloatOrDefault(string raw, float fallback)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw))
|
||||
return fallback;
|
||||
|
||||
return float.TryParse(raw.Trim(), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float value)
|
||||
? value
|
||||
: fallback;
|
||||
}
|
||||
|
||||
private static float ParseFloatOrSentinel(string raw)
|
||||
{
|
||||
float value = ParseFloatOrDefault(raw, 999f);
|
||||
return Mathf.Approximately(value, 999f) ? float.NaN : value;
|
||||
}
|
||||
|
||||
private static List<List<string>> ParseCsvRows(string text)
|
||||
{
|
||||
var rows = new List<List<string>>();
|
||||
if (string.IsNullOrEmpty(text))
|
||||
return rows;
|
||||
|
||||
var row = new List<string>();
|
||||
var cell = new StringBuilder();
|
||||
bool inQuotes = false;
|
||||
|
||||
for (int i = 0; i < text.Length; i++)
|
||||
{
|
||||
char c = text[i];
|
||||
if (inQuotes)
|
||||
{
|
||||
if (c == '"')
|
||||
{
|
||||
bool escapedQuote = i + 1 < text.Length && text[i + 1] == '"';
|
||||
if (escapedQuote)
|
||||
{
|
||||
cell.Append('"');
|
||||
i++;
|
||||
}
|
||||
else
|
||||
{
|
||||
inQuotes = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
cell.Append(c);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (c)
|
||||
{
|
||||
case '"':
|
||||
inQuotes = true;
|
||||
break;
|
||||
case ',':
|
||||
row.Add(cell.ToString());
|
||||
cell.Length = 0;
|
||||
break;
|
||||
case '\r':
|
||||
break;
|
||||
case '\n':
|
||||
row.Add(cell.ToString());
|
||||
cell.Length = 0;
|
||||
rows.Add(row);
|
||||
row = new List<string>();
|
||||
break;
|
||||
default:
|
||||
cell.Append(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
row.Add(cell.ToString());
|
||||
if (row.Count > 1 || !string.IsNullOrEmpty(row[0]))
|
||||
rows.Add(row);
|
||||
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2acb0c9d0ca50ba45b7d9a2ae70d860d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,140 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Gallop.Cyalume
|
||||
{
|
||||
[AddComponentMenu("")]
|
||||
public class MobShadowController : MonoBehaviour
|
||||
{
|
||||
private static readonly int MobGroupMatrixPropertyId = Shader.PropertyToID("_MobGroupMatrix");
|
||||
|
||||
[SerializeField] private string _mobColorPropertyName = "_MobColor";
|
||||
[SerializeField] private string _maskPosArrayPropertyName = "_MaskPosArray";
|
||||
[SerializeField] private string _groupMatrixArrayPropertyName = "_MobGroupMatrix";
|
||||
|
||||
private GameObject _targetObject;
|
||||
private Transform _targetObjectTransform;
|
||||
private Vector3 _targetObjectInitPosition;
|
||||
private bool _isInitialized;
|
||||
private Renderer[] _renderers;
|
||||
private Color _mobColor;
|
||||
private Color _ambientColor;
|
||||
private int _mobColorPropertyId;
|
||||
private int _maskPosArrayPropertyId;
|
||||
private int _groupMatrixArrayPropertyId;
|
||||
private Vector4[] _maskPosArray;
|
||||
private Matrix4x4[] _groupMatrixArray;
|
||||
private bool _isUpdateGroupMatrix;
|
||||
|
||||
public Vector4[] MaskPosArray
|
||||
{
|
||||
set
|
||||
{
|
||||
_maskPosArray = value;
|
||||
}
|
||||
}
|
||||
|
||||
public MobShadowController()
|
||||
{
|
||||
_mobColor = Color.white;
|
||||
_ambientColor = Color.white;
|
||||
_mobColorPropertyId = -1;
|
||||
_maskPosArrayPropertyId = -1;
|
||||
_groupMatrixArrayPropertyId = -1;
|
||||
_groupMatrixArray = new Matrix4x4[11];
|
||||
}
|
||||
|
||||
public void Initialize(GameObject targetObject)
|
||||
{
|
||||
_mobColorPropertyId = string.IsNullOrEmpty(_mobColorPropertyName) ? -1 : Shader.PropertyToID(_mobColorPropertyName);
|
||||
_maskPosArrayPropertyId = string.IsNullOrEmpty(_maskPosArrayPropertyName) ? -1 : Shader.PropertyToID(_maskPosArrayPropertyName);
|
||||
_groupMatrixArrayPropertyId = string.IsNullOrEmpty(_groupMatrixArrayPropertyName) ? -1 : Shader.PropertyToID(_groupMatrixArrayPropertyName);
|
||||
|
||||
_targetObject = targetObject;
|
||||
if (_targetObject == null)
|
||||
return;
|
||||
|
||||
_targetObjectTransform = _targetObject.transform;
|
||||
if (_targetObjectTransform == null)
|
||||
return;
|
||||
|
||||
_targetObjectInitPosition = _targetObjectTransform.position;
|
||||
_renderers = _targetObject.GetComponentsInChildren<Renderer>();
|
||||
|
||||
if (_groupMatrixArray == null)
|
||||
_groupMatrixArray = new Matrix4x4[11];
|
||||
|
||||
for (int i = 0; i < _groupMatrixArray.Length; i++)
|
||||
{
|
||||
_groupMatrixArray[i] = Matrix4x4.identity;
|
||||
}
|
||||
|
||||
_isUpdateGroupMatrix = true;
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
public void SetMobColor(Color color)
|
||||
{
|
||||
_mobColor = color;
|
||||
_UpdateShaderParamColor();
|
||||
}
|
||||
|
||||
public void SetAmbientColor(Color color)
|
||||
{
|
||||
_ambientColor = color;
|
||||
_UpdateShaderParamColor();
|
||||
}
|
||||
|
||||
public void SetGroupTRS(int index, ref Vector3 position, ref Quaternion rotation, ref Vector3 scale)
|
||||
{
|
||||
if (!_isInitialized)
|
||||
return;
|
||||
|
||||
if (_groupMatrixArray == null)
|
||||
return;
|
||||
|
||||
if ((uint)index >= (uint)_groupMatrixArray.Length)
|
||||
return;
|
||||
|
||||
_groupMatrixArray[index].SetTRS(position, rotation, scale);
|
||||
_isUpdateGroupMatrix = true;
|
||||
}
|
||||
|
||||
public void FlushGroupMatrix()
|
||||
{
|
||||
if (!_isInitialized || !_isUpdateGroupMatrix || _groupMatrixArray == null)
|
||||
return;
|
||||
|
||||
_isUpdateGroupMatrix = false;
|
||||
|
||||
int propertyId = _groupMatrixArrayPropertyId >= 0
|
||||
? _groupMatrixArrayPropertyId
|
||||
: MobGroupMatrixPropertyId;
|
||||
|
||||
Shader.SetGlobalMatrixArray(propertyId, _groupMatrixArray);
|
||||
}
|
||||
|
||||
private void _UpdateShaderParamColor()
|
||||
{
|
||||
if (_renderers == null)
|
||||
return;
|
||||
|
||||
Color finalColor = new Color(
|
||||
_ambientColor.r * _mobColor.r,
|
||||
_ambientColor.g * _mobColor.g,
|
||||
_ambientColor.b * _mobColor.b,
|
||||
_ambientColor.a * _mobColor.a
|
||||
);
|
||||
|
||||
if (_mobColorPropertyId >= 0)
|
||||
Shader.SetGlobalColor(_mobColorPropertyId, finalColor);
|
||||
|
||||
if (_maskPosArray != null && _maskPosArrayPropertyId >= 0)
|
||||
Shader.SetGlobalVectorArray(_maskPosArrayPropertyId, _maskPosArray);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ab50ba8b988b5cd43823243f52c6136b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -6,6 +6,8 @@ using System.Linq;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
using UnityEngine.Rendering.Universal;
|
||||
using Gallop.Live.Cyalume;
|
||||
using Gallop.RenderPipeline;
|
||||
|
||||
namespace Gallop.Live
|
||||
{
|
||||
@@ -54,6 +56,9 @@ namespace Gallop.Live
|
||||
private Dictionary<string, Vector2> _uvScrollAccum = new Dictionary<string, Vector2>();
|
||||
|
||||
private Volume _postProcessVolume;
|
||||
private readonly Dictionary<int, GameObject> _liveProps = new Dictionary<int, GameObject>();
|
||||
private readonly Dictionary<LiveTimelineAdditionalLight, Light> _additionalLights = new Dictionary<LiveTimelineAdditionalLight, Light>();
|
||||
private readonly Dictionary<LiveTimelineLightProjectionData, Light> _projectionLights = new Dictionary<LiveTimelineLightProjectionData, Light>();
|
||||
|
||||
public UmaViewerAudio.CuteAudioSource liveMusic = new UmaViewerAudio.CuteAudioSource();
|
||||
|
||||
@@ -80,6 +85,12 @@ namespace Gallop.Live
|
||||
public Transform MainCameraTransform => _mainCameraTransform;
|
||||
|
||||
private Transform _mainCameraTransform;
|
||||
private bool _playbackPaused;
|
||||
private bool _freeCameraActive;
|
||||
private FreeCam _freeCamera;
|
||||
|
||||
public bool PlaybackPaused => _playbackPaused;
|
||||
public bool FreeCameraActive => _freeCameraActive;
|
||||
|
||||
public bool isTimelineControlled
|
||||
{
|
||||
@@ -111,6 +122,10 @@ namespace Gallop.Live
|
||||
if (live != null)
|
||||
{
|
||||
_instance = this;
|
||||
if (GetComponent<SkirtCollisionRuntimeTuner>() == null)
|
||||
gameObject.AddComponent<SkirtCollisionRuntimeTuner>();
|
||||
if (GetComponent<LivePlaybackToolbar>() == null)
|
||||
gameObject.AddComponent<LivePlaybackToolbar>();
|
||||
Debug.Log(string.Format(CUTT_PATH, live.MusicId));
|
||||
Builder.LoadAssetPath(string.Format(CUTT_PATH, live.MusicId), transform);
|
||||
if (RequireStage)
|
||||
@@ -119,6 +134,8 @@ namespace Gallop.Live
|
||||
Builder.LoadAssetPath(string.Format(STAGE_PATH, live.BackGroundId), transform);
|
||||
_liveTimelineControl.StageObjectMap = _stageController.StageObjectMap;
|
||||
_liveTimelineControl.StageObjectUnitMap = _stageController.StageObjectUnitMap;
|
||||
InitializeCyalumeStage();
|
||||
InitializeStageMonitor();
|
||||
}
|
||||
|
||||
//Make CharacterObject
|
||||
@@ -231,6 +248,14 @@ namespace Gallop.Live
|
||||
_liveTimelineControl.OnUpdateChromaticAberration += OnChromaticAberrationUpdate;
|
||||
_liveTimelineControl.OnUpdateColorCorrection += OnColorCorrectionUpdate;
|
||||
_liveTimelineControl.OnUpdatePostFilm += OnPostFilmUpdate;
|
||||
_liveTimelineControl.OnUpdateLensFlare += OnLensFlareUpdate;
|
||||
_liveTimelineControl.OnUpdateEnvironment += OnEnvironmentUpdate;
|
||||
_liveTimelineControl.OnUpdateFacialToon += OnFacialToonUpdate;
|
||||
_liveTimelineControl.OnUpdateProps += OnPropsUpdate;
|
||||
_liveTimelineControl.OnUpdatePropsAttach += OnPropsAttachUpdate;
|
||||
_liveTimelineControl.OnUpdateCharaFootLight += OnCharaFootLightUpdate;
|
||||
_liveTimelineControl.OnUpdateAdditionalLight += OnAdditionalLightUpdate;
|
||||
_liveTimelineControl.OnUpdateLightProjection += OnLightProjectionUpdate;
|
||||
PostFilmRendererFeature.ResetLayers();
|
||||
|
||||
// 获取或创建摄像机上的 Volume 组件,供后处理 handler 使用
|
||||
@@ -252,6 +277,39 @@ namespace Gallop.Live
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeCyalumeStage()
|
||||
{
|
||||
if (_stageController == null) return;
|
||||
|
||||
foreach (var holder in _stageController.GetComponentsInChildren<AssetHolder>(true))
|
||||
{
|
||||
if (holder == null || holder._assetTable == null || holder._assetTable.list == null) continue;
|
||||
|
||||
bool hasDefault = false;
|
||||
bool hasRandom = false;
|
||||
foreach (var entry in holder._assetTable.list)
|
||||
{
|
||||
if (string.Equals(entry.Key, "default", StringComparison.OrdinalIgnoreCase)) hasDefault = true;
|
||||
if (string.Equals(entry.Key, "random", StringComparison.OrdinalIgnoreCase)) hasRandom = true;
|
||||
}
|
||||
|
||||
if (!hasDefault && !hasRandom) continue;
|
||||
var controller = holder.GetComponent<CyalumeController3D>();
|
||||
if (controller == null) controller = holder.gameObject.AddComponent<CyalumeController3D>();
|
||||
controller.StartOfficialLikeSetup();
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log("[Cyalume] no audience AssetHolder was present on this stage.");
|
||||
}
|
||||
|
||||
private void InitializeStageMonitor()
|
||||
{
|
||||
if (_stageController == null) return;
|
||||
if (_stageController.GetComponent<StageMonitorDriver>() == null)
|
||||
_stageController.gameObject.AddComponent<StageMonitorDriver>();
|
||||
}
|
||||
|
||||
private void OnEffectUpdate(LiveTimelineEffectData effectData, LiveTimelineKeyEffectData keyData)
|
||||
{
|
||||
if (keyData == null) return;
|
||||
@@ -280,6 +338,195 @@ namespace Gallop.Live
|
||||
_activeEffects[effectData] = (keyData.frame, instance);
|
||||
}
|
||||
|
||||
private void OnLensFlareUpdate(LiveTimelineLensFlareData data, LiveTimelineKeyLensFlareData key)
|
||||
{
|
||||
if (key == null || _stageController == null) return;
|
||||
bool enabled = key.enableParameter == 0 || key.enableFlare != 0;
|
||||
foreach (var flare in _stageController.GetComponentsInChildren<CustomLensFlare>(true))
|
||||
{
|
||||
if (flare == null) continue;
|
||||
flare.ApplyTimeline(enabled,
|
||||
key.IsOverridePosition != 0 ? key.offset : flare.transform.localPosition,
|
||||
key.color, key.brightness, key.fadeSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnvironmentUpdate(LiveTimelineStageEnvironmentData data, LiveTimelineKeyStageEnvironmentData key)
|
||||
{
|
||||
if (key == null || _stageController == null) return;
|
||||
foreach (var mirror in _stageController.GetComponentsInChildren<MirrorReflection>(true))
|
||||
mirror.ApplyTimeline(key.MirrorIsValid, key.MirrorEnabled, key.ReflectionRate);
|
||||
}
|
||||
|
||||
private void OnFacialToonUpdate(int characterIndex, LiveTimelineKeyFacialToonData key)
|
||||
{
|
||||
if (key == null || characterIndex < 0 || characterIndex >= CharaContainerScript.Count) return;
|
||||
var container = CharaContainerScript[characterIndex];
|
||||
if (container == null) return;
|
||||
var block = PropBlock;
|
||||
foreach (var renderer in container.GetComponentsInChildren<Renderer>(true))
|
||||
{
|
||||
if (renderer == null) continue;
|
||||
renderer.GetPropertyBlock(block);
|
||||
foreach (var material in renderer.sharedMaterials)
|
||||
{
|
||||
if (material == null) continue;
|
||||
if (material.HasProperty("_CheekPretenseThreshold")) block.SetFloat("_CheekPretenseThreshold", key.CheekPretenseThreshold);
|
||||
if (material.HasProperty("_NosePretenseThreshold")) block.SetFloat("_NosePretenseThreshold", key.NosePretenseThreshold);
|
||||
if (material.HasProperty("_CylinderBlend")) block.SetFloat("_CylinderBlend", key.CylinderBlend);
|
||||
if (material.HasProperty("_HairNormalBlend")) block.SetFloat("_HairNormalBlend", key.HairNormalBlend);
|
||||
if (material.HasProperty("_UseOriginalDirectionalLight")) block.SetFloat("_UseOriginalDirectionalLight", key.UseOriginalDirectionalLight);
|
||||
if (material.HasProperty("_OriginalDirectionalLightDir")) block.SetVector("_OriginalDirectionalLightDir", key.OriginalDirectionalLightDir);
|
||||
if (material.HasProperty("_EyeToonStep")) block.SetFloat("_EyeToonStep", key.EyeToonStep);
|
||||
if (material.HasProperty("_EyeToonFeather")) block.SetFloat("_EyeToonFeather", key.EyeToonFeather);
|
||||
if (material.HasProperty("_EyeSaturation")) block.SetFloat("_EyeSaturation", key.EyeSaturation);
|
||||
}
|
||||
renderer.SetPropertyBlock(block);
|
||||
}
|
||||
}
|
||||
|
||||
private GameObject ResolveLiveProp(int propsId)
|
||||
{
|
||||
if (_liveProps.TryGetValue(propsId, out var existing) && existing != null) return existing;
|
||||
var settings = _liveTimelineControl?.data?.propsSettings;
|
||||
if (settings?.propsDataGroup == null || propsId < 0 || propsId >= settings.propsDataGroup.Length) return null;
|
||||
var group = settings.propsDataGroup[propsId];
|
||||
if (group == null || string.IsNullOrEmpty(group.propsName) || !UmaViewerMain.Instance.AbList.TryGetValue(group.propsName, out var entry)) return null;
|
||||
var prefab = entry.Get<GameObject>();
|
||||
if (prefab == null) return null;
|
||||
var instance = Instantiate(prefab, transform);
|
||||
instance.name = prefab.name;
|
||||
_liveProps[propsId] = instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
private void OnPropsUpdate(LiveTimelinePropsData data, LiveTimelineKeyPropsData key)
|
||||
{
|
||||
if (key == null) return;
|
||||
var prop = ResolveLiveProp(key.propsID);
|
||||
if (prop == null) return;
|
||||
foreach (var renderer in prop.GetComponentsInChildren<Renderer>(true))
|
||||
{
|
||||
renderer.enabled = key.rendererEnable != 0;
|
||||
var block = PropBlock;
|
||||
renderer.GetPropertyBlock(block);
|
||||
foreach (var material in renderer.sharedMaterials)
|
||||
{
|
||||
if (material == null) continue;
|
||||
if (material.HasProperty("_Color")) block.SetColor("_Color", key.color);
|
||||
if (material.HasProperty("_RootColor")) block.SetColor("_RootColor", key.rootColor);
|
||||
if (material.HasProperty("_TipColor")) block.SetColor("_TipColor", key.tipColor);
|
||||
if (material.HasProperty("_ColorPower")) block.SetFloat("_ColorPower", key.colorPower);
|
||||
if (key.IsUpdateOutline != 0 && material.HasProperty("_OutlineWidth")) block.SetFloat("_OutlineWidth", key.OutlineWidth);
|
||||
if (key.IsUpdateOutline != 0 && material.HasProperty("_OutlineColor")) block.SetColor("_OutlineColor", key.OutlineColor);
|
||||
if (key.IsEmissive != 0 && material.HasProperty("_EmissiveColor")) block.SetColor("_EmissiveColor", key.EmissiveColor);
|
||||
}
|
||||
renderer.SetPropertyBlock(block);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPropsAttachUpdate(LiveTimelinePropsAttachData data, LiveTimelineKeyPropsAttachData key)
|
||||
{
|
||||
if (key == null) return;
|
||||
var prop = ResolveLiveProp(key._propsId);
|
||||
if (prop == null) return;
|
||||
Transform target = null;
|
||||
for (int i = 0; i < _liveTimelineControl.liveCharactorLocators.Length && target == null; i++)
|
||||
{
|
||||
if (_liveTimelineControl.liveCharactorLocators[i] is LiveTimelineCharaLocator locator &&
|
||||
locator.Bones != null && locator.Bones.TryGetValue(key._attachJointName, out var bone)) target = bone;
|
||||
}
|
||||
if (target == null) return;
|
||||
prop.transform.SetParent(target, false);
|
||||
prop.transform.localPosition = key._offsetPosition;
|
||||
prop.transform.localRotation = Quaternion.Euler(key.OffsetRotate);
|
||||
prop.transform.localScale = key.OffsetScale;
|
||||
}
|
||||
|
||||
private void OnCharaFootLightUpdate(LiveTimelineKeyCharaFootLightData key)
|
||||
{
|
||||
if (key == null) return;
|
||||
for (int i = 0; i < CharaContainerScript.Count; i++)
|
||||
{
|
||||
if (key.positionFlag != 0 && (key.positionFlag & (1 << i)) == 0) continue;
|
||||
if (i >= key.lightColor.Length || i >= key.hightMax.Length) continue;
|
||||
var container = CharaContainerScript[i];
|
||||
if (container == null) continue;
|
||||
float blend = i < key.LightBlendModeArray.Length ? key.LightBlendModeArray[i] : 0f;
|
||||
float easing = i < key.EasingArray.Length ? key.EasingArray[i] : 0f;
|
||||
var block = PropBlock;
|
||||
foreach (var renderer in container.GetComponentsInChildren<Renderer>(true))
|
||||
{
|
||||
if (renderer == null) continue;
|
||||
bool supported = false;
|
||||
foreach (var material in renderer.sharedMaterials)
|
||||
supported |= material != null && material.HasProperty("_HightLightColor") && material.HasProperty("_HightLightParam");
|
||||
if (!supported) continue;
|
||||
renderer.GetPropertyBlock(block);
|
||||
block.SetColor("_HightLightColor", key.lightColor[i]);
|
||||
block.SetVector("_HightLightParam", new Vector4(key.hightMax[i], blend, easing, 0f));
|
||||
renderer.SetPropertyBlock(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAdditionalLightUpdate(LiveTimelineAdditionalLight data, LiveTimelineKeyData_AdditionalLight key)
|
||||
{
|
||||
if (data == null || key == null) return;
|
||||
if (!_additionalLights.TryGetValue(data, out var light) || light == null)
|
||||
{
|
||||
var go = new GameObject(string.IsNullOrEmpty(data.name) ? "AdditionalLight" : data.name);
|
||||
go.transform.SetParent(_stageController != null ? _stageController.transform : transform, false);
|
||||
light = go.AddComponent<Light>();
|
||||
_additionalLights[data] = light;
|
||||
}
|
||||
light.gameObject.SetActive(key.IsEnable != 0);
|
||||
light.transform.localPosition = key.Position;
|
||||
light.transform.localRotation = Quaternion.Euler(key.Rotate);
|
||||
light.type = (LightType)Mathf.Clamp(key.Type, 0, 2);
|
||||
light.range = Mathf.Max(0f, key.Range);
|
||||
light.spotAngle = Mathf.Clamp(key.SpotAngle, 1f, 179f);
|
||||
light.intensity = Mathf.Max(0f, key.Strength);
|
||||
light.bounceIntensity = Mathf.Max(0f, key.IndirectMultiplier);
|
||||
light.shadows = (LightShadows)Mathf.Clamp(key.ShadowType, 0, 2);
|
||||
light.shadowStrength = Mathf.Clamp01(key.Strength);
|
||||
light.shadowBias = Mathf.Max(0f, key.Bias);
|
||||
light.shadowNormalBias = Mathf.Max(0f, key.NormalBias);
|
||||
light.shadowNearPlane = Mathf.Max(0.01f, key.NearPlane);
|
||||
}
|
||||
|
||||
private void OnLightProjectionUpdate(LiveTimelineLightProjectionData data, LiveTimelineKeyLightProjectionData key)
|
||||
{
|
||||
if (data == null || key == null || _stageController == null) return;
|
||||
if (!_projectionLights.TryGetValue(data, out var light) || light == null)
|
||||
{
|
||||
CustomProjector authored = null;
|
||||
foreach (var candidate in _stageController.GetComponentsInChildren<CustomProjector>(true))
|
||||
{
|
||||
if (candidate != null && string.Equals(candidate.name, data.name, StringComparison.OrdinalIgnoreCase))
|
||||
{ authored = candidate; break; }
|
||||
}
|
||||
var host = authored != null ? authored.gameObject : new GameObject(data.name ?? "LightProjection");
|
||||
if (authored == null) host.transform.SetParent(_stageController.transform, false);
|
||||
light = host.GetComponent<Light>();
|
||||
if (light == null) light = host.AddComponent<Light>();
|
||||
light.type = LightType.Spot;
|
||||
if (authored != null) light.cookie = authored.ProjectionTexture;
|
||||
_projectionLights[data] = light;
|
||||
}
|
||||
|
||||
light.gameObject.SetActive(key.IsEnable != 0);
|
||||
light.transform.localPosition = key.Position;
|
||||
light.transform.localRotation = Quaternion.Euler(key.Angle);
|
||||
light.transform.localScale = key.Scale;
|
||||
light.range = Mathf.Max(0.01f, key.FarClipPlane);
|
||||
light.spotAngle = Mathf.Clamp(key.FieldOfView, 1f, 179f);
|
||||
light.color = key.Color;
|
||||
light.intensity = Mathf.Max(0f, key.ColorPower * key.BlinkLightBrightnessPower);
|
||||
light.shadowNearPlane = Mathf.Max(0.01f, key.NearClipPlane);
|
||||
if (key.OverrideIgnoreLayer != 0) light.cullingMask = key.OverrideLayerMask.value;
|
||||
}
|
||||
|
||||
private static GameObject LoadEffectPrefab(string effectName)
|
||||
{
|
||||
string path = string.Format(EFFECT_PATH, effectName);
|
||||
@@ -1311,9 +1558,15 @@ namespace Gallop.Live
|
||||
|
||||
private void UpdateMainCamera()
|
||||
{
|
||||
if (_cameraObjects == null) return;
|
||||
if (_cameraObjects == null || _cameraNodes == null || _cameraTransforms == null) return;
|
||||
if (_freeCameraActive && _freeCamera != null)
|
||||
{
|
||||
_mainCameraTransform = _freeCamera.transform;
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < _cameraNodes.Length; i++)
|
||||
{
|
||||
if (_cameraNodes[i] == null) continue;
|
||||
bool activeSelf = _cameraNodes[i].activeSelf;
|
||||
bool flag = i == _activeCameraIndex;
|
||||
_cameraNodes[i].SetActive(flag);
|
||||
@@ -1322,7 +1575,77 @@ namespace Gallop.Live
|
||||
_cameraLookAt.ActivationUpdate();
|
||||
}
|
||||
}
|
||||
_mainCameraTransform = _cameraTransforms[_activeCameraIndex];
|
||||
if (_activeCameraIndex >= 0 && _activeCameraIndex < _cameraTransforms.Length)
|
||||
_mainCameraTransform = _cameraTransforms[_activeCameraIndex];
|
||||
}
|
||||
|
||||
public void SetPlaybackPaused(bool paused)
|
||||
{
|
||||
if (_playbackPaused == paused || IsRecordVMD)
|
||||
return;
|
||||
|
||||
_playbackPaused = paused;
|
||||
SetAudioPaused(liveMusic, paused);
|
||||
foreach (var vocal in liveVocal)
|
||||
SetAudioPaused(vocal, paused);
|
||||
}
|
||||
|
||||
public void SeekPlayback(float time)
|
||||
{
|
||||
_liveCurrentTime = Mathf.Clamp(time, 0f, Mathf.Max(0f, totalTime - 0.001f));
|
||||
UI.ProgressBar.SetValueWithoutNotify(totalTime > 0f ? _liveCurrentTime / totalTime : 0f);
|
||||
if (liveMusic != null && liveMusic.sourceList != null)
|
||||
UmaViewerAudio.SetTime(liveMusic, _liveCurrentTime);
|
||||
foreach (var vocal in liveVocal)
|
||||
if (vocal != null && vocal.sourceList != null)
|
||||
UmaViewerAudio.SetTime(vocal, _liveCurrentTime);
|
||||
OnTimelineUpdate(_liveCurrentTime);
|
||||
if (_liveTimelineControl != null)
|
||||
_liveTimelineControl.AlterLateUpdate();
|
||||
_syncTime = !_playbackPaused;
|
||||
}
|
||||
|
||||
public void SetFreeCameraEnabled(bool enabled)
|
||||
{
|
||||
if (_freeCameraActive == enabled)
|
||||
return;
|
||||
|
||||
if (_freeCamera == null)
|
||||
_freeCamera = Resources.FindObjectsOfTypeAll<FreeCam>().FirstOrDefault(cam => cam.gameObject.scene.IsValid());
|
||||
if (_freeCamera == null)
|
||||
return;
|
||||
|
||||
_freeCameraActive = enabled;
|
||||
if (enabled)
|
||||
{
|
||||
Transform source = _cameraTransforms != null && _activeCameraIndex >= 0 && _activeCameraIndex < _cameraTransforms.Length
|
||||
? _cameraTransforms[_activeCameraIndex] : null;
|
||||
if (source != null)
|
||||
_freeCamera.transform.SetPositionAndRotation(source.position, source.rotation);
|
||||
_freeCamera.gameObject.SetActive(true);
|
||||
if (_cameraNodes != null)
|
||||
{
|
||||
for (int i = 0; i < _cameraNodes.Length; i++)
|
||||
if (_cameraNodes[i] != null) _cameraNodes[i].SetActive(false);
|
||||
}
|
||||
_mainCameraTransform = _freeCamera.transform;
|
||||
}
|
||||
else
|
||||
{
|
||||
_freeCamera.gameObject.SetActive(false);
|
||||
UpdateMainCamera();
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetAudioPaused(UmaViewerAudio.CuteAudioSource source, bool paused)
|
||||
{
|
||||
if (source == null || source.sourceList == null)
|
||||
return;
|
||||
foreach (var audioSource in source.sourceList)
|
||||
{
|
||||
if (paused) audioSource.Pause();
|
||||
else audioSource.UnPause();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupCharacterLocator()
|
||||
@@ -1371,6 +1694,13 @@ namespace Gallop.Live
|
||||
public void Play()
|
||||
{
|
||||
|
||||
// Play can be invoked after scene/UI setup has been rebuilt; ensure the
|
||||
// debugging overlay exists and is enabled for the whole playback.
|
||||
LivePlaybackToolbar toolbar = GetComponent<LivePlaybackToolbar>();
|
||||
if (toolbar == null)
|
||||
toolbar = gameObject.AddComponent<LivePlaybackToolbar>();
|
||||
toolbar.enabled = true;
|
||||
|
||||
foreach (var vocal in liveVocal)
|
||||
{
|
||||
UmaViewerAudio.Play(vocal);
|
||||
@@ -1448,6 +1778,10 @@ namespace Gallop.Live
|
||||
OnTimelineUpdate(_liveCurrentTime);
|
||||
_liveTimelineControl.AlterLateUpdate();
|
||||
}
|
||||
else if (_playbackPaused)
|
||||
{
|
||||
// The toolbar can still seek while paused; the timeline itself remains frozen.
|
||||
}
|
||||
else if (sliderControl.is_Outed)
|
||||
{
|
||||
_liveCurrentTime = UI.ProgressBar.value * totalTime;
|
||||
@@ -1612,4 +1946,4 @@ namespace Gallop.Live
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Gallop.Live
|
||||
{
|
||||
/// <summary>Always-visible live debugging controls independent of the scene's auto-hidden UI.</summary>
|
||||
public sealed class LivePlaybackToolbar : MonoBehaviour
|
||||
{
|
||||
private bool _draggingTimeline;
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (Director.instance == null || !Director.instance._isLiveSetup)
|
||||
return;
|
||||
|
||||
if (Input.GetKeyDown(KeyCode.Space))
|
||||
Director.instance.SetPlaybackPaused(!Director.instance.PlaybackPaused);
|
||||
if (Input.GetKeyDown(KeyCode.F6))
|
||||
Director.instance.SetFreeCameraEnabled(!Director.instance.FreeCameraActive);
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
Director director = Director.instance;
|
||||
if (director == null || !director._isLiveSetup)
|
||||
return;
|
||||
|
||||
// Keep this overlay above the scene's Canvas/UI, which may cover the lower edge.
|
||||
int oldDepth = GUI.depth;
|
||||
GUI.depth = 1000;
|
||||
GUILayout.BeginArea(new Rect(16, Mathf.Max(8, Screen.height - 68), 560, 52), GUI.skin.window);
|
||||
GUILayout.BeginHorizontal();
|
||||
|
||||
if (GUILayout.Button(director.PlaybackPaused ? "Play" : "Pause", GUILayout.Width(62)))
|
||||
director.SetPlaybackPaused(!director.PlaybackPaused);
|
||||
|
||||
bool freeCamera = GUILayout.Toggle(director.FreeCameraActive, "Free Camera", GUILayout.Width(105));
|
||||
if (freeCamera != director.FreeCameraActive)
|
||||
director.SetFreeCameraEnabled(freeCamera);
|
||||
|
||||
float time = GUILayout.HorizontalSlider(director._liveCurrentTime, 0f, Mathf.Max(0.01f, director.totalTime), GUILayout.Width(245));
|
||||
Rect sliderRect = GUILayoutUtility.GetLastRect();
|
||||
Event currentEvent = Event.current;
|
||||
if (currentEvent.type == EventType.MouseDown && sliderRect.Contains(currentEvent.mousePosition))
|
||||
_draggingTimeline = true;
|
||||
if (_draggingTimeline && (currentEvent.type == EventType.MouseDrag || currentEvent.type == EventType.MouseDown))
|
||||
director.SeekPlayback(time);
|
||||
if (currentEvent.type == EventType.MouseUp)
|
||||
_draggingTimeline = false;
|
||||
|
||||
GUILayout.Label(director._liveCurrentTime.ToString("0.00") + " / " + director.totalTime.ToString("0.00"), GUILayout.Width(84));
|
||||
GUILayout.EndHorizontal();
|
||||
GUILayout.EndArea();
|
||||
GUI.depth = oldDepth;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5307220bda8922d4ea2911ec6f0dd3e1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b86c3dce210ee2e4bbd36c6c48abfccb
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5b3a700e79167fe49bb840bdda6f8d34
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,2 @@
|
||||
// Kept intentionally empty so stale Unity-generated project files that referenced the
|
||||
// superseded temporary driver remain buildable until Unity regenerates the project.
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3049c310353c9934a8b7c3f938f32bfc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 56c2bfe19657a4c4bb1708407b98fa28
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,357 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Gallop.Live
|
||||
{
|
||||
/// <summary>Runtime tuning for hair, cloth, tail, and accessory spring motion.</summary>
|
||||
public sealed class SkirtCollisionRuntimeTuner : MonoBehaviour
|
||||
{
|
||||
private enum MotionCategory
|
||||
{
|
||||
Skirt,
|
||||
Other
|
||||
}
|
||||
|
||||
private sealed class ParticleDefaults
|
||||
{
|
||||
public DynamicBone.Particle Particle;
|
||||
public MotionCategory Category;
|
||||
public float Damping;
|
||||
public float Elasticity;
|
||||
public float Stiffness;
|
||||
public float Inert;
|
||||
}
|
||||
|
||||
private sealed class ColliderDefaults
|
||||
{
|
||||
public DynamicBoneCollider Collider;
|
||||
public float Radius;
|
||||
}
|
||||
|
||||
private readonly List<ParticleDefaults> _particles = new List<ParticleDefaults>();
|
||||
private readonly HashSet<DynamicBone.Particle> _knownParticles = new HashSet<DynamicBone.Particle>();
|
||||
private readonly List<ColliderDefaults> _hipColliders = new List<ColliderDefaults>();
|
||||
private readonly HashSet<DynamicBoneCollider> _knownHipColliders = new HashSet<DynamicBoneCollider>();
|
||||
private readonly List<ColliderDefaults> _legColliders = new List<ColliderDefaults>();
|
||||
private readonly HashSet<DynamicBoneCollider> _knownLegColliders = new HashSet<DynamicBoneCollider>();
|
||||
private bool _visible;
|
||||
private bool _initialized;
|
||||
private float _dampingScale = 3f;
|
||||
private float _elasticityScale = 1.5f;
|
||||
private float _stiffnessScale = 2f;
|
||||
private float _inertScale = 1.1f;
|
||||
private float _hipRadiusScale = 0.2f;
|
||||
private float _legRadiusScale = 0.8f;
|
||||
private float _horizontalStiffness = 0.5f;
|
||||
private float _otherDampingScale = 3f;
|
||||
private float _otherElasticityScale = 1.5f;
|
||||
private float _otherStiffnessScale = 2f;
|
||||
private float _otherInertScale = 1.1f;
|
||||
private float _otherRigidity = 0.85f;
|
||||
private float _monitorBrightness = 1f;
|
||||
private bool _showSkirtColliders;
|
||||
private bool _skirtBodyCollisionEnabled = true;
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (Input.GetKeyDown(KeyCode.F8))
|
||||
_visible = !_visible;
|
||||
|
||||
if (Director.instance == null || Director.instance.CharaContainerScript.Count == 0)
|
||||
return;
|
||||
|
||||
bool added = false;
|
||||
bool addedCollider = false;
|
||||
foreach (UmaContainerCharacter character in Director.instance.CharaContainerScript)
|
||||
{
|
||||
if (character == null)
|
||||
continue;
|
||||
|
||||
foreach (DynamicBone dynamicBone in character.GetComponentsInChildren<DynamicBone>(true))
|
||||
{
|
||||
MotionCategory category = GetMotionCategory(dynamicBone);
|
||||
foreach (DynamicBone.Particle particle in dynamicBone.Particles)
|
||||
{
|
||||
if (!_knownParticles.Add(particle))
|
||||
continue;
|
||||
|
||||
_particles.Add(new ParticleDefaults
|
||||
{
|
||||
Particle = particle,
|
||||
Category = category,
|
||||
Damping = particle.m_Damping,
|
||||
Elasticity = particle.m_Elasticity,
|
||||
Stiffness = particle.m_Stiffness,
|
||||
Inert = particle.m_Inert
|
||||
});
|
||||
added = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (DynamicBoneCollider collider in character.GetComponentsInChildren<DynamicBoneCollider>(true))
|
||||
{
|
||||
if (IsHipSkirtCollider(collider) && _knownHipColliders.Add(collider))
|
||||
{
|
||||
_hipColliders.Add(new ColliderDefaults
|
||||
{
|
||||
Collider = collider,
|
||||
Radius = collider.m_Radius
|
||||
});
|
||||
addedCollider = true;
|
||||
}
|
||||
if (IsLegSkirtCollider(collider) && _knownLegColliders.Add(collider))
|
||||
{
|
||||
_legColliders.Add(new ColliderDefaults
|
||||
{
|
||||
Collider = collider,
|
||||
Radius = collider.m_Radius
|
||||
});
|
||||
addedCollider = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_initialized = _particles.Count > 0;
|
||||
if (added || addedCollider)
|
||||
Apply();
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
if (!_visible || !_initialized)
|
||||
return;
|
||||
|
||||
GUILayout.BeginArea(new Rect(16, 16, 430, 720), GUI.skin.window);
|
||||
GUILayout.Label("Secondary Motion Tuning");
|
||||
|
||||
bool changed = false;
|
||||
changed |= SliderRow("Damping", ref _dampingScale, 0.25f, 4f);
|
||||
changed |= SliderRow("Elasticity", ref _elasticityScale, 0f, 1.5f);
|
||||
changed |= SliderRow("Stiffness", ref _stiffnessScale, 0.25f, 2f);
|
||||
changed |= SliderRow("Inertia", ref _inertScale, 0f, 1.5f);
|
||||
changed |= SliderRow("Hip radius", ref _hipRadiusScale, 0.1f, 1.5f);
|
||||
changed |= SliderRow("Leg radius", ref _legRadiusScale, 0.5f, 1.5f);
|
||||
changed |= SliderRow("Horizontal stiffness", ref _horizontalStiffness, 0f, 1f);
|
||||
|
||||
GUILayout.Space(5);
|
||||
GUILayout.Label("All non-skirt physics (relative to skirt base)");
|
||||
changed |= SliderRow("Other damping", ref _otherDampingScale, 0.25f, 4f);
|
||||
changed |= SliderRow("Other elasticity", ref _otherElasticityScale, 0f, 1.5f);
|
||||
changed |= SliderRow("Other stiffness", ref _otherStiffnessScale, 0.25f, 2f);
|
||||
changed |= SliderRow("Other inertia", ref _otherInertScale, 0f, 1.5f);
|
||||
changed |= SliderRow("Other rigidity", ref _otherRigidity, 0f, 1f);
|
||||
|
||||
GUILayout.Space(5);
|
||||
GUILayout.Label("Live display");
|
||||
changed |= SliderRow("Screen brightness", ref _monitorBrightness, 0f, 1.5f);
|
||||
|
||||
if (GUILayout.Button("Restore defaults", GUILayout.Width(165)))
|
||||
{
|
||||
_dampingScale = 3f;
|
||||
_elasticityScale = 1.5f;
|
||||
_stiffnessScale = 2f;
|
||||
_inertScale = 1.1f;
|
||||
_hipRadiusScale = 0.2f;
|
||||
_legRadiusScale = 0.8f;
|
||||
_horizontalStiffness = 0.5f;
|
||||
_otherDampingScale = 3f;
|
||||
_otherElasticityScale = 1.5f;
|
||||
_otherStiffnessScale = 2f;
|
||||
_otherInertScale = 1.1f;
|
||||
_otherRigidity = 0.85f;
|
||||
_monitorBrightness = 1f;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed)
|
||||
Apply();
|
||||
|
||||
bool showColliders = GUILayout.Toggle(_showSkirtColliders, "Show skirt colliders");
|
||||
if (showColliders != _showSkirtColliders)
|
||||
{
|
||||
_showSkirtColliders = showColliders;
|
||||
foreach (SkirtCollisionVisualizer visualizer in Object.FindObjectsOfType<SkirtCollisionVisualizer>(true))
|
||||
visualizer.SetVisible(_showSkirtColliders);
|
||||
}
|
||||
|
||||
bool bodyCollisionEnabled = GUILayout.Toggle(_skirtBodyCollisionEnabled, "Enable skirt body collision");
|
||||
if (bodyCollisionEnabled != _skirtBodyCollisionEnabled)
|
||||
{
|
||||
_skirtBodyCollisionEnabled = bodyCollisionEnabled;
|
||||
SetSkirtBodyCollisionEnabled(_skirtBodyCollisionEnabled);
|
||||
}
|
||||
|
||||
UmaContainerCharacter firstCharacter = Director.instance.CharaContainerScript[0];
|
||||
SkirtSurfaceCollisionSolver solver = firstCharacter != null
|
||||
? firstCharacter.GetComponent<SkirtSurfaceCollisionSolver>() : null;
|
||||
GUILayout.Label(solver != null
|
||||
? "Skirt rings: " + solver.RingCount + " colliders: " + solver.ColliderCount +
|
||||
" contacts: " + solver.LastContactCount
|
||||
: "Skirt surface solver: not initialized");
|
||||
if (firstCharacter != null)
|
||||
{
|
||||
foreach (DynamicBoneCollider collider in firstCharacter.GetComponentsInChildren<DynamicBoneCollider>(true))
|
||||
{
|
||||
if (!IsBodySkirtCollider(collider))
|
||||
continue;
|
||||
|
||||
GUILayout.Label(collider.ColliderName + " r=" + collider.m_Radius.ToString("0.000") +
|
||||
" h=" + collider.m_Height.ToString("0.000"));
|
||||
}
|
||||
}
|
||||
GUILayout.Label("F8: close");
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
|
||||
private static bool SliderRow(string label, ref float value, float min, float max)
|
||||
{
|
||||
GUILayout.BeginHorizontal();
|
||||
GUILayout.Label(label, GUILayout.Width(105));
|
||||
float updated = GUILayout.HorizontalSlider(value, min, max, GUILayout.Width(165));
|
||||
GUILayout.Label(updated.ToString("0.00") + "x", GUILayout.Width(55));
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
bool changed = !Mathf.Approximately(value, updated);
|
||||
value = updated;
|
||||
return changed;
|
||||
}
|
||||
|
||||
private void Apply()
|
||||
{
|
||||
foreach (ParticleDefaults defaults in _particles)
|
||||
{
|
||||
DynamicBone.Particle particle = defaults.Particle;
|
||||
if (particle == null)
|
||||
continue;
|
||||
|
||||
float categoryDamping = 1f;
|
||||
float categoryElasticity = 1f;
|
||||
float categoryStiffness = 1f;
|
||||
float categoryInert = 1f;
|
||||
if (defaults.Category == MotionCategory.Other)
|
||||
{
|
||||
categoryDamping = _otherDampingScale;
|
||||
categoryElasticity = _otherElasticityScale;
|
||||
categoryStiffness = _otherStiffnessScale;
|
||||
categoryInert = _otherInertScale;
|
||||
}
|
||||
|
||||
particle.m_Damping = Mathf.Clamp01(defaults.Damping * _dampingScale * categoryDamping);
|
||||
particle.m_Elasticity = Mathf.Clamp01(defaults.Elasticity * _elasticityScale * categoryElasticity);
|
||||
particle.m_Stiffness = Mathf.Clamp01(defaults.Stiffness * _stiffnessScale * categoryStiffness);
|
||||
particle.m_Inert = Mathf.Clamp01(defaults.Inert * _inertScale * categoryInert);
|
||||
|
||||
if (defaults.Category == MotionCategory.Other && _otherRigidity > 0f)
|
||||
{
|
||||
// Multipliers alone cannot make a low-value source spring rigid.
|
||||
// This is a direct, non-skirt constraint preset: high damping and
|
||||
// stiffness, with limited rebound and body-motion lag.
|
||||
particle.m_Damping = Mathf.Max(particle.m_Damping, Mathf.Lerp(0f, 0.9f, _otherRigidity));
|
||||
particle.m_Stiffness = Mathf.Max(particle.m_Stiffness, Mathf.Lerp(0f, 0.98f, _otherRigidity));
|
||||
particle.m_Elasticity = Mathf.Min(particle.m_Elasticity, Mathf.Lerp(1f, 0.05f, _otherRigidity));
|
||||
particle.m_Inert = Mathf.Min(particle.m_Inert, Mathf.Lerp(1f, 0f, _otherRigidity));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (ColliderDefaults defaults in _hipColliders)
|
||||
{
|
||||
if (defaults.Collider != null)
|
||||
defaults.Collider.m_Radius = defaults.Radius * _hipRadiusScale;
|
||||
}
|
||||
|
||||
foreach (ColliderDefaults defaults in _legColliders)
|
||||
{
|
||||
if (defaults.Collider != null)
|
||||
defaults.Collider.m_Radius = defaults.Radius * _legRadiusScale;
|
||||
}
|
||||
|
||||
if (Director.instance == null)
|
||||
return;
|
||||
|
||||
foreach (UmaContainerCharacter character in Director.instance.CharaContainerScript)
|
||||
{
|
||||
if (character == null)
|
||||
continue;
|
||||
|
||||
SkirtSurfaceCollisionSolver solver = character.GetComponent<SkirtSurfaceCollisionSolver>();
|
||||
if (solver != null)
|
||||
solver.HorizontalStiffness = _horizontalStiffness;
|
||||
}
|
||||
|
||||
foreach (StageMonitorDriver monitorDriver in Object.FindObjectsOfType<StageMonitorDriver>(true))
|
||||
monitorDriver.brightnessMultiplier = _monitorBrightness;
|
||||
}
|
||||
|
||||
private void SetSkirtBodyCollisionEnabled(bool enabled)
|
||||
{
|
||||
foreach (UmaContainerCharacter character in Director.instance.CharaContainerScript)
|
||||
{
|
||||
if (character == null)
|
||||
continue;
|
||||
|
||||
foreach (DynamicBoneCollider collider in character.GetComponentsInChildren<DynamicBoneCollider>(true))
|
||||
{
|
||||
if (IsBodySkirtCollider(collider))
|
||||
collider.enabled = enabled;
|
||||
}
|
||||
|
||||
SkirtSurfaceCollisionSolver solver = character.GetComponent<SkirtSurfaceCollisionSolver>();
|
||||
if (solver != null)
|
||||
solver.enabled = enabled;
|
||||
character.ResetDynamicBone();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsBodySkirtCollider(DynamicBoneCollider collider)
|
||||
{
|
||||
if (collider == null || string.IsNullOrEmpty(collider.ColliderName) ||
|
||||
collider.ColliderName.IndexOf("Skirt", System.StringComparison.OrdinalIgnoreCase) < 0)
|
||||
return false;
|
||||
|
||||
if (collider.ColliderName.IndexOf("Thigh", System.StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
return true;
|
||||
|
||||
bool isSideHip = collider.ColliderName.EndsWith("_L", System.StringComparison.OrdinalIgnoreCase) ||
|
||||
collider.ColliderName.EndsWith("_R", System.StringComparison.OrdinalIgnoreCase);
|
||||
return collider.ColliderName.IndexOf("Hip", System.StringComparison.OrdinalIgnoreCase) >= 0 &&
|
||||
isSideHip && collider.m_Radius <= 0.25f;
|
||||
}
|
||||
|
||||
private static bool IsHipSkirtCollider(DynamicBoneCollider collider)
|
||||
{
|
||||
return collider != null && collider.ColliderName != null &&
|
||||
collider.ColliderName.IndexOf("Hip", System.StringComparison.OrdinalIgnoreCase) >= 0 &&
|
||||
collider.ColliderName.IndexOf("Skirt", System.StringComparison.OrdinalIgnoreCase) >= 0 &&
|
||||
(collider.ColliderName.EndsWith("_L", System.StringComparison.OrdinalIgnoreCase) ||
|
||||
collider.ColliderName.EndsWith("_R", System.StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static bool IsLegSkirtCollider(DynamicBoneCollider collider)
|
||||
{
|
||||
return collider != null && collider.ColliderName != null &&
|
||||
collider.ColliderName.IndexOf("Thigh", System.StringComparison.OrdinalIgnoreCase) >= 0 &&
|
||||
collider.ColliderName.IndexOf("MSkirt", System.StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
}
|
||||
|
||||
private static MotionCategory GetMotionCategory(DynamicBone dynamicBone)
|
||||
{
|
||||
string name = GetMotionName(dynamicBone);
|
||||
return dynamicBone != null && (dynamicBone.m_ContinuousCollision ||
|
||||
name.IndexOf("Skirt", System.StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
? MotionCategory.Skirt : MotionCategory.Other;
|
||||
}
|
||||
|
||||
private static string GetMotionName(DynamicBone dynamicBone)
|
||||
{
|
||||
string name = dynamicBone != null && dynamicBone.m_Root != null
|
||||
? dynamicBone.m_Root.name : string.Empty;
|
||||
for (Transform transform = dynamicBone != null ? dynamicBone.transform.parent : null;
|
||||
transform != null; transform = transform.parent)
|
||||
{
|
||||
name += "/" + transform.name;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 65925dd2ac8aec44a9998f1d12dd2b2d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -57,6 +57,20 @@ namespace Gallop
|
||||
private static readonly int kReflectionRate = Shader.PropertyToID("_ReflectionRate");
|
||||
private static readonly int kColor = Shader.PropertyToID("_Color");
|
||||
|
||||
public void ApplyTimeline(bool valid, bool enabled, float reflectionRate)
|
||||
{
|
||||
if (!valid) return;
|
||||
_mirrorReflectionRate = Mathf.Max(0f, reflectionRate);
|
||||
this.enabled = enabled;
|
||||
if (_renderer != null)
|
||||
{
|
||||
_block ??= new MaterialPropertyBlock();
|
||||
_renderer.GetPropertyBlock(_block);
|
||||
_block.SetFloat(kReflectionRate, _mirrorReflectionRate);
|
||||
_renderer.SetPropertyBlock(_block);
|
||||
}
|
||||
}
|
||||
|
||||
// URP 下不能用 Camera.Render()(SRP 会直接报错、什么都不画),
|
||||
// 必须挂 beginCameraRendering 拿到 ScriptableRenderContext 再 RenderSingleCamera。
|
||||
private void OnEnable() => RenderPipelineManager.beginCameraRendering += OnBeginCamera;
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
namespace Gallop.RenderPipeline
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
public class CustomProjector : MonoBehaviour
|
||||
{
|
||||
public float _nearClipPlane = 0.1f;
|
||||
public float _farClipPlane = 60f;
|
||||
public float _fieldOfView = 30f;
|
||||
public float _aspectRatio = 1f;
|
||||
public Material _material;
|
||||
|
||||
public Texture ProjectionTexture
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_material == null) return null;
|
||||
string[] names = { "_MainTex", "_BaseMap", "_ProjectionTex" };
|
||||
foreach (string name in names)
|
||||
if (_material.HasProperty(name) && _material.GetTexture(name) != null) return _material.GetTexture(name);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class LensFlareElement
|
||||
{
|
||||
public int ImageIndex;
|
||||
public float Position;
|
||||
public float Size = 1f;
|
||||
public Color Color = Color.white;
|
||||
public bool UseLightColor;
|
||||
public bool Rotate;
|
||||
public bool Zoom;
|
||||
public bool Fade;
|
||||
}
|
||||
|
||||
public class LensFlareData : ScriptableObject
|
||||
{
|
||||
public int TextureLayout;
|
||||
public Texture2D Texture;
|
||||
public LensFlareElement[] ElementArray = Array.Empty<LensFlareElement>();
|
||||
public bool UseFog;
|
||||
}
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
public class CustomLensFlare : MonoBehaviour
|
||||
{
|
||||
public LensFlareData Flare;
|
||||
public float Brightness = 1f;
|
||||
public float FadeSpeed = 1f;
|
||||
public Color Color = Color.white;
|
||||
public LayerMask IgnoreLayers;
|
||||
public bool IsDirectional;
|
||||
|
||||
private LensFlareComponentSRP _component;
|
||||
private LensFlareDataSRP _runtimeData;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
BuildRuntimeFlare();
|
||||
ApplyRuntimeParameters();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (_component != null) _component.enabled = false;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_runtimeData != null) Destroy(_runtimeData);
|
||||
}
|
||||
|
||||
public void ApplyTimeline(bool enabled, Vector3 offset, Color color, float brightness, float fadeSpeed)
|
||||
{
|
||||
if (_component == null || _runtimeData == null) BuildRuntimeFlare();
|
||||
transform.localPosition = offset;
|
||||
Color = color;
|
||||
Brightness = Mathf.Max(0f, brightness);
|
||||
FadeSpeed = Mathf.Max(0f, fadeSpeed);
|
||||
ApplyRuntimeParameters();
|
||||
if (_component != null) _component.enabled = enabled;
|
||||
}
|
||||
|
||||
private void BuildRuntimeFlare()
|
||||
{
|
||||
if (Flare == null) return;
|
||||
_component = GetComponent<LensFlareComponentSRP>();
|
||||
if (_component == null) _component = gameObject.AddComponent<LensFlareComponentSRP>();
|
||||
|
||||
if (_runtimeData != null) Destroy(_runtimeData);
|
||||
_runtimeData = ScriptableObject.CreateInstance<LensFlareDataSRP>();
|
||||
|
||||
var source = Flare.ElementArray ?? Array.Empty<LensFlareElement>();
|
||||
var elements = new LensFlareDataElementSRP[source.Length];
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
LensFlareElement item = source[i] ?? new LensFlareElement();
|
||||
var element = new LensFlareDataElementSRP
|
||||
{
|
||||
flareType = SRPLensFlareType.Image,
|
||||
lensFlareTexture = Flare.Texture,
|
||||
position = item.Position,
|
||||
uniformScale = Mathf.Max(0.001f, item.Size),
|
||||
sizeXY = Vector2.one,
|
||||
tint = item.Color,
|
||||
autoRotate = item.Rotate,
|
||||
modulateByLightColor = item.UseLightColor,
|
||||
blendMode = SRPLensFlareBlendMode.Additive,
|
||||
preserveAspectRatio = true
|
||||
};
|
||||
elements[i] = element;
|
||||
}
|
||||
|
||||
_runtimeData.elements = elements;
|
||||
_component.lensFlareData = _runtimeData;
|
||||
_component.useOcclusion = true;
|
||||
_component.allowOffScreen = false;
|
||||
_component.attenuationByLightShape = false;
|
||||
}
|
||||
|
||||
private void ApplyRuntimeParameters()
|
||||
{
|
||||
if (_component == null || _runtimeData == null) return;
|
||||
_component.intensity = Mathf.Max(0f, Brightness);
|
||||
for (int i = 0; i < _runtimeData.elements.Length; i++)
|
||||
{
|
||||
LensFlareElement source = Flare != null && Flare.ElementArray != null && i < Flare.ElementArray.Length
|
||||
? Flare.ElementArray[i]
|
||||
: null;
|
||||
Color sourceColor = source != null ? source.Color : UnityEngine.Color.white;
|
||||
_runtimeData.elements[i].tint = sourceColor * Color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: da09622a71394244b822f4f9d136b344
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,128 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>Runtime wireframe display for generated skirt collision volumes.</summary>
|
||||
public sealed class SkirtCollisionVisualizer : MonoBehaviour
|
||||
{
|
||||
private const int Segments = 24;
|
||||
private readonly List<LineRenderer> _lines = new List<LineRenderer>();
|
||||
private DynamicBoneCollider _collider;
|
||||
private bool _visible;
|
||||
private float _lastRadius = -1f;
|
||||
private float _lastHeight = -1f;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_collider = GetComponent<DynamicBoneCollider>();
|
||||
CreateLines();
|
||||
SetVisible(false);
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (_visible && _collider != null &&
|
||||
(!Mathf.Approximately(_lastRadius, _collider.m_Radius) ||
|
||||
!Mathf.Approximately(_lastHeight, _collider.m_Height)))
|
||||
UpdateGeometry();
|
||||
}
|
||||
|
||||
public void SetVisible(bool visible)
|
||||
{
|
||||
_visible = visible;
|
||||
foreach (LineRenderer line in _lines)
|
||||
line.enabled = visible;
|
||||
if (visible)
|
||||
UpdateGeometry();
|
||||
}
|
||||
|
||||
private void CreateLines()
|
||||
{
|
||||
Color color = name.Contains("Hip") ? new Color(1f, 0.35f, 0.2f) : new Color(0.2f, 0.85f, 1f);
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
GameObject lineObject = new GameObject("Wire");
|
||||
lineObject.transform.SetParent(transform, false);
|
||||
LineRenderer line = lineObject.AddComponent<LineRenderer>();
|
||||
line.useWorldSpace = false;
|
||||
line.loop = i < 3;
|
||||
line.widthMultiplier = 0.006f;
|
||||
line.numCornerVertices = 2;
|
||||
line.numCapVertices = 2;
|
||||
line.material = SkirtCollisionWireMaterial.Instance;
|
||||
line.startColor = color;
|
||||
line.endColor = color;
|
||||
_lines.Add(line);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateGeometry()
|
||||
{
|
||||
if (_collider == null)
|
||||
return;
|
||||
|
||||
float radius = _collider.m_Radius;
|
||||
_lastRadius = radius;
|
||||
_lastHeight = _collider.m_Height;
|
||||
float halfSegment = Mathf.Max(0f, _collider.m_Height * 0.5f - radius);
|
||||
if (halfSegment == 0f)
|
||||
{
|
||||
SetCircle(_lines[0], radius, 0f, 0);
|
||||
SetCircle(_lines[1], radius, 0f, 1);
|
||||
SetCircle(_lines[2], radius, 0f, 2);
|
||||
for (int i = 3; i < _lines.Count; i++)
|
||||
_lines[i].positionCount = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
SetCircle(_lines[0], radius, -halfSegment, 0);
|
||||
SetCircle(_lines[1], radius, halfSegment, 0);
|
||||
_lines[2].positionCount = 0;
|
||||
SetLine(_lines[3], new Vector3(radius, 0f, -halfSegment), new Vector3(radius, 0f, halfSegment));
|
||||
SetLine(_lines[4], new Vector3(-radius, 0f, -halfSegment), new Vector3(-radius, 0f, halfSegment));
|
||||
SetLine(_lines[5], new Vector3(0f, radius, -halfSegment), new Vector3(0f, radius, halfSegment));
|
||||
}
|
||||
|
||||
private static void SetCircle(LineRenderer line, float radius, float axisOffset, int plane)
|
||||
{
|
||||
line.positionCount = Segments;
|
||||
for (int i = 0; i < Segments; i++)
|
||||
{
|
||||
float angle = Mathf.PI * 2f * i / Segments;
|
||||
float a = Mathf.Cos(angle) * radius;
|
||||
float b = Mathf.Sin(angle) * radius;
|
||||
switch (plane)
|
||||
{
|
||||
case 0: line.SetPosition(i, new Vector3(a, b, axisOffset)); break;
|
||||
case 1: line.SetPosition(i, new Vector3(a, axisOffset, b)); break;
|
||||
default: line.SetPosition(i, new Vector3(axisOffset, a, b)); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetLine(LineRenderer line, Vector3 from, Vector3 to)
|
||||
{
|
||||
line.positionCount = 2;
|
||||
line.SetPosition(0, from);
|
||||
line.SetPosition(1, to);
|
||||
}
|
||||
}
|
||||
|
||||
internal static class SkirtCollisionWireMaterial
|
||||
{
|
||||
private static Material _instance;
|
||||
|
||||
public static Material Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_instance == null)
|
||||
{
|
||||
Shader shader = Shader.Find("Universal Render Pipeline/Unlit");
|
||||
if (shader == null)
|
||||
shader = Shader.Find("Sprites/Default");
|
||||
_instance = new Material(shader);
|
||||
}
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dc01f9558187f96469b7bd90a47e5ebc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,46 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Keeps a DynamicBone capsule aligned with the animated hip-to-knee segment.
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(-1000)]
|
||||
public sealed class SkirtLegCollisionProxy : MonoBehaviour
|
||||
{
|
||||
public Transform StartBone;
|
||||
public Transform EndBone;
|
||||
public DynamicBoneCollider Collider;
|
||||
public float RadiusScale;
|
||||
|
||||
private void Update()
|
||||
{
|
||||
UpdateColliderTransform();
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
// Refresh after the character animation has written the current leg pose,
|
||||
// before DynamicBone performs its late-frame collision solve.
|
||||
UpdateColliderTransform();
|
||||
}
|
||||
|
||||
public void UpdateColliderTransform()
|
||||
{
|
||||
if (StartBone == null || EndBone == null || Collider == null)
|
||||
return;
|
||||
|
||||
Vector3 segment = EndBone.position - StartBone.position;
|
||||
float length = segment.magnitude;
|
||||
if (length <= Mathf.Epsilon)
|
||||
return;
|
||||
|
||||
transform.SetPositionAndRotation(
|
||||
StartBone.position + segment * 0.5f,
|
||||
Quaternion.LookRotation(segment));
|
||||
|
||||
if (RadiusScale > 0f)
|
||||
Collider.m_Radius = length * RadiusScale;
|
||||
|
||||
// DynamicBone measures capsule height including both rounded ends.
|
||||
Collider.m_Height = length + Collider.m_Radius * 2f;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 73a040a00b60d4243b83dc5eda9511fe
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,305 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves collisions across the horizontal spans between adjacent skirt chains.
|
||||
/// DynamicBone itself only checks each independent vertical chain.
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(1000)]
|
||||
public sealed class SkirtSurfaceCollisionSolver : MonoBehaviour
|
||||
{
|
||||
private sealed class HorizontalLink
|
||||
{
|
||||
public DynamicBone.Particle Left;
|
||||
public DynamicBone.Particle Right;
|
||||
public float RestLength;
|
||||
}
|
||||
|
||||
private readonly List<List<DynamicBone>> _rings = new List<List<DynamicBone>>();
|
||||
private readonly List<DynamicBoneColliderBase> _colliders = new List<DynamicBoneColliderBase>();
|
||||
private readonly List<HorizontalLink> _horizontalLinks = new List<HorizontalLink>();
|
||||
private Transform _hip;
|
||||
|
||||
public int RingCount => _rings.Count;
|
||||
public int ColliderCount => _colliders.Count;
|
||||
public int LastContactCount { get; private set; }
|
||||
public float HorizontalStiffness { get; set; } = 0.25f;
|
||||
|
||||
public void Initialize(DynamicBone[] dynamics, Transform hip, IEnumerable<DynamicBoneColliderBase> colliders)
|
||||
{
|
||||
_hip = hip;
|
||||
_rings.Clear();
|
||||
_colliders.Clear();
|
||||
_horizontalLinks.Clear();
|
||||
foreach (DynamicBoneColliderBase collider in colliders)
|
||||
{
|
||||
if (IsSurfaceCollider(collider) && !_colliders.Contains(collider))
|
||||
_colliders.Add(collider);
|
||||
}
|
||||
|
||||
var layers = new Dictionary<string, List<DynamicBone>>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (DynamicBone dynamicBone in dynamics)
|
||||
{
|
||||
if (dynamicBone == null || !dynamicBone.m_ContinuousCollision ||
|
||||
dynamicBone.m_Root == null || dynamicBone.Particles.Count < 2)
|
||||
continue;
|
||||
|
||||
string layer = GetSkirtLayer(dynamicBone.m_Root.name);
|
||||
if (!layers.TryGetValue(layer, out List<DynamicBone> ring))
|
||||
{
|
||||
ring = new List<DynamicBone>();
|
||||
layers.Add(layer, ring);
|
||||
}
|
||||
ring.Add(dynamicBone);
|
||||
}
|
||||
|
||||
foreach (List<DynamicBone> ring in layers.Values)
|
||||
{
|
||||
if (ring.Count < 3)
|
||||
continue;
|
||||
|
||||
ring.Sort((left, right) => GetAngle(left).CompareTo(GetAngle(right)));
|
||||
_rings.Add(ring);
|
||||
AddHorizontalLinks(ring);
|
||||
}
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
LastContactCount = 0;
|
||||
SolveHorizontalLinks();
|
||||
for (int iteration = 0; iteration < 1; ++iteration)
|
||||
{
|
||||
foreach (List<DynamicBone> ring in _rings)
|
||||
LastContactCount += ResolveRing(ring, _colliders);
|
||||
}
|
||||
|
||||
foreach (List<DynamicBone> ring in _rings)
|
||||
{
|
||||
foreach (DynamicBone dynamicBone in ring)
|
||||
dynamicBone.ApplyParticlesToTransformsNow();
|
||||
}
|
||||
}
|
||||
|
||||
private void AddHorizontalLinks(List<DynamicBone> ring)
|
||||
{
|
||||
for (int chainIndex = 0; chainIndex < ring.Count; ++chainIndex)
|
||||
{
|
||||
DynamicBone leftChain = ring[chainIndex];
|
||||
DynamicBone rightChain = ring[(chainIndex + 1) % ring.Count];
|
||||
int depthCount = Mathf.Min(leftChain.Particles.Count, rightChain.Particles.Count);
|
||||
for (int depth = 1; depth < depthCount; ++depth)
|
||||
{
|
||||
DynamicBone.Particle left = leftChain.Particles[depth];
|
||||
DynamicBone.Particle right = rightChain.Particles[depth];
|
||||
_horizontalLinks.Add(new HorizontalLink
|
||||
{
|
||||
Left = left,
|
||||
Right = right,
|
||||
RestLength = Vector3.Distance(left.m_Position, right.m_Position)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SolveHorizontalLinks()
|
||||
{
|
||||
float strength = Mathf.Clamp01(HorizontalStiffness);
|
||||
if (strength <= 0f)
|
||||
return;
|
||||
|
||||
foreach (HorizontalLink link in _horizontalLinks)
|
||||
{
|
||||
Vector3 delta = link.Right.m_Position - link.Left.m_Position;
|
||||
float length = delta.magnitude;
|
||||
if (length <= Mathf.Epsilon)
|
||||
continue;
|
||||
|
||||
Vector3 correction = delta * ((length - link.RestLength) / length * strength * 0.5f);
|
||||
link.Left.m_Position += correction;
|
||||
link.Left.m_PrevPosition += correction;
|
||||
link.Right.m_Position -= correction;
|
||||
link.Right.m_PrevPosition -= correction;
|
||||
}
|
||||
}
|
||||
|
||||
private static int ResolveRing(List<DynamicBone> ring, List<DynamicBoneColliderBase> colliders)
|
||||
{
|
||||
int contacts = 0;
|
||||
for (int chainIndex = 0; chainIndex < ring.Count; ++chainIndex)
|
||||
{
|
||||
DynamicBone leftChain = ring[chainIndex];
|
||||
DynamicBone rightChain = ring[(chainIndex + 1) % ring.Count];
|
||||
int depthCount = Mathf.Min(leftChain.Particles.Count, rightChain.Particles.Count);
|
||||
|
||||
for (int depth = 1; depth < depthCount; ++depth)
|
||||
{
|
||||
DynamicBone.Particle left = leftChain.Particles[depth];
|
||||
DynamicBone.Particle right = rightChain.Particles[depth];
|
||||
float objectScale = Mathf.Abs(leftChain.transform.lossyScale.x);
|
||||
float particleRadius = Mathf.Max(left.m_Radius, right.m_Radius) * objectScale;
|
||||
float maxCorrection = Mathf.Max(
|
||||
Vector3.Distance(leftChain.Particles[depth - 1].m_Position, left.m_Position),
|
||||
Vector3.Distance(rightChain.Particles[depth - 1].m_Position, right.m_Position)) * 0.75f;
|
||||
maxCorrection = Mathf.Max(maxCorrection, 0.005f * objectScale);
|
||||
contacts += ResolveSpan(left, right, colliders, particleRadius, maxCorrection);
|
||||
contacts += ResolvePanel(
|
||||
leftChain.Particles[depth - 1], rightChain.Particles[depth - 1],
|
||||
left, right, depth > 1, colliders, particleRadius, maxCorrection);
|
||||
}
|
||||
}
|
||||
return contacts;
|
||||
}
|
||||
|
||||
private static int ResolvePanel(
|
||||
DynamicBone.Particle leftTop,
|
||||
DynamicBone.Particle rightTop,
|
||||
DynamicBone.Particle leftBottom,
|
||||
DynamicBone.Particle rightBottom,
|
||||
bool topIsMovable,
|
||||
List<DynamicBoneColliderBase> colliders,
|
||||
float particleRadius,
|
||||
float maxCorrection)
|
||||
{
|
||||
if (colliders.Count == 0)
|
||||
return 0;
|
||||
|
||||
int contacts = 0;
|
||||
|
||||
for (int verticalIndex = 1; verticalIndex <= 2; ++verticalIndex)
|
||||
{
|
||||
float vertical = verticalIndex / 3f;
|
||||
for (int horizontalIndex = 1; horizontalIndex <= 2; ++horizontalIndex)
|
||||
{
|
||||
float horizontal = horizontalIndex / 3f;
|
||||
Vector3 top = Vector3.Lerp(leftTop.m_Position, rightTop.m_Position, horizontal);
|
||||
Vector3 bottom = Vector3.Lerp(leftBottom.m_Position, rightBottom.m_Position, horizontal);
|
||||
Vector3 sample = Vector3.Lerp(top, bottom, vertical);
|
||||
|
||||
foreach (DynamicBoneColliderBase collider in colliders)
|
||||
{
|
||||
if (!IsSurfaceCollider(collider))
|
||||
continue;
|
||||
|
||||
Vector3 projected = sample;
|
||||
if (!collider.Collide(ref projected, particleRadius))
|
||||
continue;
|
||||
|
||||
++contacts;
|
||||
|
||||
float leftTopWeight = (1f - horizontal) * (1f - vertical);
|
||||
float rightTopWeight = horizontal * (1f - vertical);
|
||||
float leftBottomWeight = (1f - horizontal) * vertical;
|
||||
float rightBottomWeight = horizontal * vertical;
|
||||
float normalization = leftBottomWeight * leftBottomWeight + rightBottomWeight * rightBottomWeight;
|
||||
if (topIsMovable)
|
||||
normalization += leftTopWeight * leftTopWeight + rightTopWeight * rightTopWeight;
|
||||
|
||||
Vector3 correction = projected - sample;
|
||||
if (topIsMovable)
|
||||
{
|
||||
ApplyCorrection(leftTop, correction * (leftTopWeight / normalization), maxCorrection);
|
||||
ApplyCorrection(rightTop, correction * (rightTopWeight / normalization), maxCorrection);
|
||||
}
|
||||
ApplyCorrection(leftBottom, correction * (leftBottomWeight / normalization), maxCorrection);
|
||||
ApplyCorrection(rightBottom, correction * (rightBottomWeight / normalization), maxCorrection);
|
||||
sample = projected;
|
||||
}
|
||||
}
|
||||
}
|
||||
return contacts;
|
||||
}
|
||||
|
||||
private static int ResolveSpan(
|
||||
DynamicBone.Particle left,
|
||||
DynamicBone.Particle right,
|
||||
List<DynamicBoneColliderBase> colliders,
|
||||
float particleRadius,
|
||||
float maxCorrection)
|
||||
{
|
||||
if (colliders.Count == 0)
|
||||
return 0;
|
||||
|
||||
int contacts = 0;
|
||||
|
||||
for (int sampleIndex = 1; sampleIndex <= 1; ++sampleIndex)
|
||||
{
|
||||
float t = 0.5f;
|
||||
Vector3 sample = Vector3.Lerp(left.m_Position, right.m_Position, t);
|
||||
|
||||
foreach (DynamicBoneColliderBase collider in colliders)
|
||||
{
|
||||
if (!IsSurfaceCollider(collider))
|
||||
continue;
|
||||
|
||||
Vector3 projected = sample;
|
||||
if (!collider.Collide(ref projected, particleRadius))
|
||||
continue;
|
||||
|
||||
++contacts;
|
||||
|
||||
Vector3 correction = projected - sample;
|
||||
float leftWeight = 1f - t;
|
||||
float rightWeight = t;
|
||||
float normalization = leftWeight * leftWeight + rightWeight * rightWeight;
|
||||
Vector3 leftCorrection = correction * (leftWeight / normalization);
|
||||
Vector3 rightCorrection = correction * (rightWeight / normalization);
|
||||
|
||||
ApplyCorrection(left, leftCorrection, maxCorrection);
|
||||
ApplyCorrection(right, rightCorrection, maxCorrection);
|
||||
sample = projected;
|
||||
}
|
||||
}
|
||||
return contacts;
|
||||
}
|
||||
|
||||
private static bool IsSurfaceCollider(DynamicBoneColliderBase collider)
|
||||
{
|
||||
return collider != null && collider.enabled &&
|
||||
IsBodySkirtCollider(collider);
|
||||
}
|
||||
|
||||
private static bool IsBodySkirtCollider(DynamicBoneColliderBase collider)
|
||||
{
|
||||
if (collider == null || string.IsNullOrEmpty(collider.ColliderName) ||
|
||||
collider.ColliderName.IndexOf("Skirt", StringComparison.OrdinalIgnoreCase) < 0)
|
||||
return false;
|
||||
|
||||
if (collider.ColliderName.IndexOf("Thigh", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
return true;
|
||||
|
||||
DynamicBoneCollider dynamicCollider = collider as DynamicBoneCollider;
|
||||
return collider.ColliderName.IndexOf("Hip", StringComparison.OrdinalIgnoreCase) >= 0 &&
|
||||
(dynamicCollider == null || dynamicCollider.m_Radius <= 0.25f);
|
||||
}
|
||||
|
||||
private static void ApplyCorrection(DynamicBone.Particle particle, Vector3 correction, float maxCorrection)
|
||||
{
|
||||
correction = Vector3.ClampMagnitude(correction, maxCorrection);
|
||||
particle.m_Position += correction;
|
||||
particle.m_PrevPosition += correction;
|
||||
particle.m_isCollide = true;
|
||||
}
|
||||
|
||||
private float GetAngle(DynamicBone dynamicBone)
|
||||
{
|
||||
Vector3 center = _hip != null ? _hip.position : transform.position;
|
||||
Vector3 direction = dynamicBone.m_Root.position - center;
|
||||
if (_hip != null)
|
||||
direction = _hip.InverseTransformDirection(direction);
|
||||
return Mathf.Atan2(direction.x, direction.z);
|
||||
}
|
||||
|
||||
private static string GetSkirtLayer(string boneName)
|
||||
{
|
||||
int marker = boneName.IndexOf("MSkirt", StringComparison.OrdinalIgnoreCase);
|
||||
if (marker < 0)
|
||||
return boneName;
|
||||
|
||||
int end = marker + "MSkirt".Length;
|
||||
while (end < boneName.Length && char.IsDigit(boneName[end]))
|
||||
++end;
|
||||
return boneName.Substring(marker, end - marker);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dae4cd93a4add8345b1a3a77b32a83fa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because one or more lines are too long
+17
-17
@@ -5,7 +5,7 @@
|
||||
"depth": 1,
|
||||
"source": "registry",
|
||||
"dependencies": {},
|
||||
"url": "https://packages.unity.com"
|
||||
"url": "https://packages.unity.cn"
|
||||
},
|
||||
"com.unity.2d.sprite": {
|
||||
"version": "1.0.0",
|
||||
@@ -20,7 +20,7 @@
|
||||
"dependencies": {
|
||||
"com.unity.modules.ai": "1.0.0"
|
||||
},
|
||||
"url": "https://packages.unity.com"
|
||||
"url": "https://packages.unity.cn"
|
||||
},
|
||||
"com.unity.burst": {
|
||||
"version": "1.8.21",
|
||||
@@ -30,14 +30,14 @@
|
||||
"com.unity.mathematics": "1.2.1",
|
||||
"com.unity.modules.jsonserialize": "1.0.0"
|
||||
},
|
||||
"url": "https://packages.unity.com"
|
||||
"url": "https://packages.unity.cn"
|
||||
},
|
||||
"com.unity.ext.nunit": {
|
||||
"version": "1.0.6",
|
||||
"depth": 1,
|
||||
"source": "registry",
|
||||
"dependencies": {},
|
||||
"url": "https://packages.unity.com"
|
||||
"url": "https://packages.unity.cn"
|
||||
},
|
||||
"com.unity.formats.fbx": {
|
||||
"version": "5.0.0",
|
||||
@@ -47,7 +47,7 @@
|
||||
"com.unity.timeline": "1.7.1",
|
||||
"com.autodesk.fbx": "5.0.0"
|
||||
},
|
||||
"url": "https://packages.unity.com"
|
||||
"url": "https://packages.unity.cn"
|
||||
},
|
||||
"com.unity.ide.rider": {
|
||||
"version": "3.0.36",
|
||||
@@ -56,7 +56,7 @@
|
||||
"dependencies": {
|
||||
"com.unity.ext.nunit": "1.0.6"
|
||||
},
|
||||
"url": "https://packages.unity.com"
|
||||
"url": "https://packages.unity.cn"
|
||||
},
|
||||
"com.unity.ide.visualstudio": {
|
||||
"version": "2.0.22",
|
||||
@@ -65,28 +65,28 @@
|
||||
"dependencies": {
|
||||
"com.unity.test-framework": "1.1.9"
|
||||
},
|
||||
"url": "https://packages.unity.com"
|
||||
"url": "https://packages.unity.cn"
|
||||
},
|
||||
"com.unity.ide.vscode": {
|
||||
"version": "1.2.5",
|
||||
"depth": 0,
|
||||
"source": "registry",
|
||||
"dependencies": {},
|
||||
"url": "https://packages.unity.com"
|
||||
"url": "https://packages.unity.cn"
|
||||
},
|
||||
"com.unity.mathematics": {
|
||||
"version": "1.2.6",
|
||||
"depth": 1,
|
||||
"source": "registry",
|
||||
"dependencies": {},
|
||||
"url": "https://packages.unity.com"
|
||||
"url": "https://packages.unity.cn"
|
||||
},
|
||||
"com.unity.nuget.newtonsoft-json": {
|
||||
"version": "3.2.1",
|
||||
"depth": 0,
|
||||
"source": "registry",
|
||||
"dependencies": {},
|
||||
"url": "https://packages.unity.com"
|
||||
"url": "https://packages.unity.cn"
|
||||
},
|
||||
"com.unity.postprocessing": {
|
||||
"version": "3.4.0",
|
||||
@@ -95,7 +95,7 @@
|
||||
"dependencies": {
|
||||
"com.unity.modules.physics": "1.0.0"
|
||||
},
|
||||
"url": "https://packages.unity.com"
|
||||
"url": "https://packages.unity.cn"
|
||||
},
|
||||
"com.unity.recorder": {
|
||||
"version": "4.0.3",
|
||||
@@ -104,7 +104,7 @@
|
||||
"dependencies": {
|
||||
"com.unity.timeline": "1.0.0"
|
||||
},
|
||||
"url": "https://packages.unity.com"
|
||||
"url": "https://packages.unity.cn"
|
||||
},
|
||||
"com.unity.render-pipelines.core": {
|
||||
"version": "14.0.12",
|
||||
@@ -142,7 +142,7 @@
|
||||
"depth": 2,
|
||||
"source": "registry",
|
||||
"dependencies": {},
|
||||
"url": "https://packages.unity.com"
|
||||
"url": "https://packages.unity.cn"
|
||||
},
|
||||
"com.unity.shadergraph": {
|
||||
"version": "14.0.12",
|
||||
@@ -162,7 +162,7 @@
|
||||
"com.unity.modules.imgui": "1.0.0",
|
||||
"com.unity.modules.jsonserialize": "1.0.0"
|
||||
},
|
||||
"url": "https://packages.unity.com"
|
||||
"url": "https://packages.unity.cn"
|
||||
},
|
||||
"com.unity.textmeshpro": {
|
||||
"version": "3.0.7",
|
||||
@@ -171,19 +171,19 @@
|
||||
"dependencies": {
|
||||
"com.unity.ugui": "1.0.0"
|
||||
},
|
||||
"url": "https://packages.unity.com"
|
||||
"url": "https://packages.unity.cn"
|
||||
},
|
||||
"com.unity.timeline": {
|
||||
"version": "1.7.7",
|
||||
"depth": 0,
|
||||
"source": "registry",
|
||||
"dependencies": {
|
||||
"com.unity.modules.audio": "1.0.0",
|
||||
"com.unity.modules.director": "1.0.0",
|
||||
"com.unity.modules.animation": "1.0.0",
|
||||
"com.unity.modules.audio": "1.0.0",
|
||||
"com.unity.modules.particlesystem": "1.0.0"
|
||||
},
|
||||
"url": "https://packages.unity.com"
|
||||
"url": "https://packages.unity.cn"
|
||||
},
|
||||
"com.unity.ugui": {
|
||||
"version": "1.0.0",
|
||||
|
||||
@@ -12,32 +12,25 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 13964, guid: 0000000000000000e000000000000000, type: 0}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_EnablePreviewPackages: 0
|
||||
m_EnablePackageDependencies: 0
|
||||
m_EnablePreReleasePackages: 0
|
||||
m_AdvancedSettingsExpanded: 1
|
||||
m_ScopedRegistriesSettingsExpanded: 1
|
||||
m_SeeAllPackageVersions: 0
|
||||
m_DismissPreviewPackagesInUse: 0
|
||||
oneTimeWarningShown: 0
|
||||
m_Registries:
|
||||
- m_Id: main
|
||||
m_Name:
|
||||
m_Url: https://packages.unity.com
|
||||
m_Url: https://packages.unity.cn
|
||||
m_Scopes: []
|
||||
m_IsDefault: 1
|
||||
m_Capabilities: 7
|
||||
m_ConfigSource: 0
|
||||
m_UserSelectedRegistryName:
|
||||
m_UserAddingNewScopedRegistry: 0
|
||||
m_RegistryInfoDraft:
|
||||
m_ErrorMessage:
|
||||
m_Original:
|
||||
m_Id:
|
||||
m_Name:
|
||||
m_Url:
|
||||
m_Scopes: []
|
||||
m_IsDefault: 0
|
||||
m_Capabilities: 0
|
||||
m_Modified: 0
|
||||
m_Name:
|
||||
m_Url:
|
||||
m_Scopes:
|
||||
-
|
||||
m_SelectedScopeIndex: 0
|
||||
m_ErrorMessage:
|
||||
m_UserModificationsInstanceId: -850
|
||||
m_OriginalInstanceId: -852
|
||||
m_LoadAssets: 0
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
m_EditorVersion: 2022.3.62f1
|
||||
m_EditorVersionWithRevision: 2022.3.62f1 (4af31df58517)
|
||||
m_EditorVersion: 2022.3.62f1c1
|
||||
m_EditorVersionWithRevision: 2022.3.62f1c1 (b0109b07edb8)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 434 KiB |
@@ -0,0 +1,302 @@
|
||||
---
|
||||
name: Live 轨道全览
|
||||
description: UmaViewer Live 时间轴全部103条轨道的ID、名称、作用、实现状态及已知Bug(2026-05-07核查)
|
||||
type: reference
|
||||
originSessionId: 319ae839-0292-4b1e-80de-2f3c91b1f40e
|
||||
---
|
||||
# Live 轨道全览(LiveTimelineKeyDataType 枚举,ID 0–102)
|
||||
|
||||
数据来源:LiveTimelineDefine.cs 枚举 + WorkSheet CS字段核查 + scan_full.py全量扫描(58首歌,2026-05-07)+ Director/Control代码审查
|
||||
|
||||
> **术语**:下文「WorkSheet 有/无 XXX 字段」指的是**游戏 bundle 的 TypeTree**,不是本仓库的 C# `LiveTimelineWorkSheet` 类。两者是分开的:`postFilmKeys`、`radialBlurKeys`、`tiltShiftKeys`、`flashPlayerKeys`、`charaFootLightKeys`、`MultiLightShadowKeys`、`lightProjectionList`、`postFilm1MultiCameraKeys`、`postEffectDOFKeys` 在 bundle 里有,在 C# 里**都还没声明**。
|
||||
>
|
||||
> **覆盖率数字同理**——「N/58 首」量的是 bundle 里有没有数据,不代表 C# 收得到。分发层的过滤可能让有数据的轨道照样落空(BgColor1 就是如此,见 ID 8)。
|
||||
>
|
||||
> ## ⚠️ 2026-08-04:旧扫描数据不可信,已用新工具全量重测
|
||||
>
|
||||
> `tools/` 下的 dump 工具链已重建(见 `CLAUDE.md`)。cutt bundle 自带完整 TypeTree,**字段名和 keyframe 数是权威的**。全 59 首、70 张 worksheet 共享同一套 schema(128 字段),无版本漂移。
|
||||
>
|
||||
> 重测结果与本文件原有记录冲突严重,**下表中带「✅实测」标记的才是可信数据**,其余未标记的行仍是旧扫描结论,谨慎对待。两类系统性错误:
|
||||
>
|
||||
> 1. **「WorkSheet无XXX字段」全部是错的** —— 那些字段都存在,而且多数有大量数据(见 ID 26/27/51/52/59/70/77/84/85/98/99)
|
||||
> 2. **「全0 keyframe空占位」多数是错的** —— RadialBlur/PostFilm/TiltShift/FlashPlayer/CharaFootLight/MultiLightShadow 实测都有真实数据(见 ID 15/39/63/66/72/83)
|
||||
>
|
||||
> 真正确认为空的只有:`hdrBloomKeys`、`other4EyeTrackKeys`、`ScreenCaptureDataList`、`tailMotionDataList`(0/59 首)。
|
||||
>
|
||||
> 复查命令:`~/.venvs/umatools/bin/python3 tools/dump_cutt_typetree.py --fields <songid>`;全量表在 `tools/out/scan_summary.txt`。
|
||||
|
||||
**实现状态图例**
|
||||
- ✅ 完整
|
||||
- ⚠️ 部分实现(有TODO)
|
||||
- ⚠️🔇 仅数据(无视觉输出)
|
||||
- 🔴 Bug(声明但从不触发)
|
||||
- ❌ 未实现
|
||||
|
||||
**★** = song 1001 有真实数据(括号内条数)
|
||||
|
||||
---
|
||||
|
||||
## 摄像机 / 基础轨道
|
||||
|
||||
| ID | 名称 | 作用 | 状态 | Bug |
|
||||
|----|------|------|------|-----|
|
||||
| 0 | Timescale | 播放倍速 | ✅ Control内部 | — |
|
||||
| 1 | CameraPos | 主摄像机位置 | ✅ | — |
|
||||
| 2 | CameraLookAt | 主摄像机朝向 | ✅ | — |
|
||||
| 3 | CameraFov | 视野角 | ✅ | — |
|
||||
| 4 | CameraRoll | 摄像机滚转 | ✅ | — |
|
||||
| 5 | HandShakeCamera | 手持抖动效果 | ❌ | ✅实测 `handShakeCameraKeys` 56/59首 / 594 keys |
|
||||
| 6 | Event | 时间轴触发事件 | ❌ | — |
|
||||
| 7 | CharaMotionSequence ★(5) | 角色动作序列切换 | ✅ | — |
|
||||
| 8 | BgColor1 ★(13) | 角色**和舞台物件**的色调/轮廓/阴影颜色 | ⚠️ | 曾误标 ✅。`AlterUpdate_BgColor1` 原本用 `validBgColorNames` 只放行 4 个角色组,其余全丢——实测约 92% 的 key 被丢弃(全语料 43617 keys / 59首,是最大的轨道)。2026-08-04 已拆掉过滤并加舞台分支,**舞台侧写 `_MulColor0`**(运行时枚举 shader 确认,每个舞台 shader 都有;`_AmbientColor` 归 BgColor2,两者是同一 shader 上的独立通道)。song 1177 的 20 组里 16 组已正常写入,仍未解析:`BgBL`(256 keys)、`FollowSpotColor`(16)、`Shadow`(2)、`mob_00`(2) —— 这 4 个不是物件名,疑为全局通道 |
|
||||
| 9 | BgColor2 ★(1) | 背景渐变/环境色(两色混合)【15/58首】 | ✅ | 写含 `_AmbientColor` 属性的舞台材质,颜色 = `Lerp(color1, color2, value)`;color2 恒为白色 |
|
||||
| 10 | MonitorControl ★(2) | LED舞台屏幕内容控制 | ❌ | ✅实测 `monitorControlList` **55/59首 / 3911 keys**。**舞台上的黑色大屏就是它没实现导致的**:monitor 材质的 `_MainTex`/`_FadeTex`/`_FilterTex` 三个贴图槽在 bundle 里全未绑定,内容靠运行时喂。字段:`dispID` / `speed` / `outputTextureLabel` / `blendFactor` / `Src·DstBlendMode` / `RenderQueueNo` / `LightImageNo` / `CrossFadeRate` / `FilterTexScale`。**内容资源已定位**:`live/uvmovie/gal_uvmovie_<songid>_<001..017>`(图片序列,带 `_tex_NN`),另有 `gal_uvmovie_<songid>_light` 对应 `LightImageNo`。全库 219 个 uvmovie 目录。**但 dispID→资源的映射未解**:song 1177 的 key 是 `dispID=1`,却既没有 `gal_uvmovie_1177_001`,也没有 monitorCamera 数据(`monitorCameraPosKeys` 0 keys),两条路都不通 |
|
||||
| 11 | CameraSwitcher | 切换活动摄像机 | ✅ | — |
|
||||
| 12 | LipSync | 口型同步 | ✅ | — |
|
||||
| 13 | PostEffectDOF | 景深模糊(单机) | ❌ | ✅实测 `postEffectDOFKeys` **59/59首 / 3879 keys**。URP 有内置 DepthOfField,属路径 A |
|
||||
| 14 | PostEffectBloomDiffusion | 泛光扩散(单机) | ❌ | ✅实测 `postEffectBloomDiffusionKeys` **59/59首 / 1969 keys** |
|
||||
| 15 | RadialBlur | 径向运动模糊(单机) | ❌ | ✅实测 `radialBlurKeys` **59/59首 / 1667 keys**(单曲最多95)。旧记录「全部0 keyframe」是错的 |
|
||||
| 16 | CameraLayer | 摄像机渲染层遮罩 | ❌ | ✅实测 `cameraLayerKeys` **58/59首 / 3306 keys** |
|
||||
| 17 | Projector ★(4) | 舞台投影仪(gobo图案) | ❌ | — |
|
||||
|
||||
---
|
||||
|
||||
## 角色表情 / 动作
|
||||
|
||||
| ID | 名称 | 作用 | 状态 | Bug |
|
||||
|----|------|------|------|-----|
|
||||
| 18 | FacialFace | 面部整体表情 | ✅ AlterUpdate_FacialData | — |
|
||||
| 19 | FacialMouth | 嘴部表情 | ✅ | — |
|
||||
| 20 | FacialCheek | 脸颊表情 | ✅ | — |
|
||||
| 21 | FacialEye | 眼部表情 | ✅ | — |
|
||||
| 22 | FacialEyebrow | 眉毛 | ✅ | — |
|
||||
| 23 | FacialEyeTrack | 眼球追踪目标 | ✅ | — |
|
||||
| 24 | FacialEar | 耳部动画 | ✅ | — |
|
||||
| 25 | FacialEffect | 表情叠加特效 | ✅ | — |
|
||||
| 26 | FacialNoise | 表情随机扰动 | ❌ | ✅实测 `facialNoiseKeys` **存在**,50/59首 / 127 keys。旧记录「无字段」是错的 |
|
||||
| 27 | CharaMotionNoise | 动作随机扰动 | ❌ | ✅实测 `charaMotionNoiseKeys` **存在**,**59/59首** / 135 keys。旧记录「无字段」是错的 |
|
||||
| 28 | FormationOffset | 角色队形位置偏移 | ✅ | — |
|
||||
| 29 | Animation ★(1) | GameObject Animation组件序列 | ❌ | ✅实测 `animationList` 39/59首 / 1575 keys(单曲最多218),比 ★(1) 高得多 |
|
||||
| 30 | TextureAnimation | 材质纹理帧动画 | ❌ | — |
|
||||
|
||||
---
|
||||
|
||||
## 舞台物件
|
||||
|
||||
| ID | 名称 | 作用 | 状态 | Bug |
|
||||
|----|------|------|------|-----|
|
||||
| 31 | Transform | 舞台物件位移/旋转/缩放 | ✅ StageController | — |
|
||||
| 32 | Renderer | 舞台Renderer启用/禁用 | ❌ | — |
|
||||
| 33 | Object ★(10) | 舞台GameObject激活/隐藏**+ 位移** | ✅ StageController | 2026-08-04 修了两个静默失效:① 组名可能是 `_stageObjectUnits` 的**单元名**而非 GameObject 名(live10149 的 `neonsign` 就是 unit,底下挂 3 个子对象;没有任何 GameObject 叫 neonsign,整条轨道此前从不触发,灯牌不会从吊顶降下);② `OffsetType` 字段分发层一直在填但 `UpdateObject` 从不读,`Add` 语义被当绝对坐标写,物件被挪到父物体原点。现按 `Direct`/`Add` 分别处理,`Add` 在物件原始 local TRS 上叠加 |
|
||||
| 34 | Audience ★(12) | 观众人群动画参数 | ❌ | — |
|
||||
| 35 | Props | 舞台道具激活控制 | ❌ | ✅实测 `propsList` 28/59首 / 1940 keys(单曲最多567) |
|
||||
| 36 | PropsAttach | 角色附着道具 | ❌ | ✅实测 `propsAttachList` 28/59首 / 298 keys |
|
||||
|
||||
---
|
||||
|
||||
## 灯光 / 特效
|
||||
|
||||
| ID | 名称 | 作用 | 状态 | Bug |
|
||||
|----|------|------|------|-----|
|
||||
| 37 | VolumeLight ★(1) | 体积光/SunShafts | ⚠️🔇 | 无SunShafts组件,仅数据。✅实测 `volumeLightKeys` **58/59首 / 2048 keys** |
|
||||
| 38 | HdrBloom | HDR泛光强度/阈值 | ⚠️🔇 | 0/58首有数据,**但已经实现了**(Director 有 handler,走 URP Bloom)→ 是本仓库里的死代码,建议删。song 1177 实测 `hdrBloomKeys` 为空 |
|
||||
| 39 | PostFilm | 全屏后期叠加(晕影/颜色/UV电影) | ⚠️🔇 | ✅实测三字段全有大量数据:`postFilmKeys` 59/59首6689 keys、`postFilm2Keys` 59/59首**7290 keys**、`postFilm3Keys` 59/59首6240 keys(song 1177 为 100/161/87)。旧记录「全部0 keyframe」是错的。**2026-08-05 已完成数据层**:数据类字段按 TypeTree 顺序重排并补齐 5 个 `BlinkLight*`、新增 `LiveTimelineKeyPostFilmDataList`、WorkSheet 三字段、`AlterUpdate_PostFilm` 分发与插值、`PostFilmUpdateInfo`、`PostFilmRendererFeature` + `Resources/Shaders/PostFilm.shader`。**渲染层默认关闭**(`PostFilmRendererFeature._enableRendering = false`):song 1177 的 100 个 key 全是 Vignette 变体,而 `filmOptionParam`(取值如 (0.2,0.05)/(0,0.05)/(0.2,0.25))的几何语义**无法从资源中还原** —— 游戏自己的 PostFilm shader 不在 `shader` bundle 里(499 个 shader 按名字和属性签名都搜过),应在 app 本体的 resources.assets。要继续做只能对着参考视频标定 Width/Strength 两个系数 |
|
||||
| 40 | Fade | 场景淡入淡出 | ❌ | ✅实测 `fadeKeys` **59/59首** / 243 keys。量小但覆盖全 |
|
||||
| 41 | Particle ★(3) | 粒子系统发射速率 | ✅ | — |
|
||||
| 42 | ParticleGroup ★(3) | 粒子组闪烁速率(双速率曲线) | ✅ | — |
|
||||
| 43 | WashLight | 洗光灯 SetActive+颜色亮度 | ⚠️ | RaycastDistance/CameraProjectionSide/MulColor0未使用。**低优先级**:song 1177 仅 1 组 11 keys,真正的"洗光"效果由 ID 45 的 `*_wash_*` 组承担。字段名 `WashLightList`(大写 W)已用 A/B 探针确认;上游 katboi01 写作 `washLightList`,永远填不上,其 WashLightController 是死代码 |
|
||||
| 44 | Laser | 激光灯变换+SetActive | ⚠️ | blinkRate/blinkOffset/rotateFollowCamera/RaycastDistance未实现。song 1177 该轨道为空 |
|
||||
| 45 | BlinkLight ★(9) | 频闪灯亮灭周期/颜色/亮度 | ⚠️ | **灯光线最大权重**:全语料 19420 keys / 57首;song 1177 实测 27 组,一半以上组名是 `*_wash_*`。★(9) 是 song 1001 的数字,严重低估。2026-08-04 修:写入属性从 `_Color`(这些 shader 上不存在,写入是空操作,灯全渲染成黑)改为按存在性探测 `_BlinkLightColor`/`_MulColor0`/`_Color`。**仍未解**:`color0Array`/`powerArray` 恒为 10 项(调色板),与 renderer 数(可达 570)无关,灯→槽位的映射规则未逆出,暂统一取第 0 槽;`pattern` 语义未逆出,逐灯相位(U闪→M闪→A闪)未实现;`color1Array`/`LightBlendMode`/`isReverseHueArray` 未用。另注意部分 renderer 用 `StageTransmittedLightMask`/`StageMirrorBallShine`,**shader 里一个 Color 属性都没有**,BlinkLight 点不亮(对应未实现的 95/96 TransmittedLight 轨道) |
|
||||
| 46 | UVScrollLight ★(1) | 材质UV滚动灯光效果 | ⚠️ | mulColor1/ColorType/CharacterIndex等未使用 |
|
||||
| 47 | FacialToon | 角色卡通着色参数 | ❌ | ✅实测 `facialToonSet` **59/59首 / 6341 keys**(单曲最多600)。未实现轨道里数据量第二大 |
|
||||
| 48 | GlobalLight ★(2) | 全局光方向/RimLight参数 | ✅ | — |
|
||||
| 49 | GlobalFog ★(1) | 全局雾效(颜色/密度/范围) | ✅ | — |
|
||||
| 50 | LightShafts ★(1) | 光轴/丁达尔效果 | ⚠️🔇 | 无LightShaftsController组件,仅数据 |
|
||||
| 51 | MonitorCameraPos ★(1) | 舞台监控摄像机位置 | ❌ | ✅实测 `monitorCameraPosKeys` **存在**,34/59首 / 964 keys。旧记录「无字段」是错的。用途推测是 LED 大屏的副摄像机实时画面(IMAG),与 ID 10 配套 |
|
||||
| 52 | MonitorCameraLookAt ★(1) | 舞台监控摄像机朝向 | ❌ | ✅实测 `monitorCameraLookAtKeys` **存在**,34/59首 / 971 keys。旧记录「无字段」是错的 |
|
||||
| 53 | MultiCameraPos ★(2) | 多机位摄像机位置 | ✅ | — |
|
||||
| 54 | MultiCameraLookAt ★(2) | 多机位摄像机朝向 | ✅ | — |
|
||||
| 55 | EyeCameraPos | 眼部特写摄像机位置 | ❌ | — |
|
||||
| 56 | EyeCameraLookAt | 眼部特写摄像机朝向 | ❌ | — |
|
||||
| 57 | LensFlare | 镜头光晕 | ❌ | ✅实测 `lensFlareList` 45/59首 / 993 keys(单曲最多181)。舞台用 `CustomLensFlare` 脚本无源码;可先做 SetActive 层 |
|
||||
| 58 | Environment ★(1) | 地板镜面反射/水面波纹/阴影/FovShift | ❌ | ✅实测 `environmentDataLists` 49/59首 / 1375 keys。**注意 schema 里另有独立字段 `MirrorReflectionDataList`**,两者不是一回事(上游 katboi01 把它俩混为一谈,按 name 分流)。核心是 Planar Reflection,工程量大 |
|
||||
| 59 | SweatLocator ★(3) | 角色汗珠特效挂点 | ❌ | ✅实测 `sweatLocatorList` **存在**,32/59首 / 608 keys。旧记录「无字段」是错的 |
|
||||
| 60 | Effect ★(8) | 特效Prefab加载/播放(3d/effect/live/) | ✅ | — |
|
||||
| 61 | ColorCorrection | 色彩校正(饱和度/RGB曲线) | ⚠️ | depthRedCurve/Green/Blue/blendCurve/mode/selective/keyColor/targetColor无URP对应 |
|
||||
| 62 | PreColorCorrection | 前置色彩校正 | ❌ | — |
|
||||
| 63 | TiltShift | 移轴模糊 | ❌ | ✅实测 `tiltShiftKeys` **58/59首 / 1202 keys**(单曲最多127)。旧记录「全部0 keyframe」是错的 |
|
||||
| 64 | A2U | 内部动作系统参数 | ❌ | 非视觉轨道 |
|
||||
| 65 | A2UConfig | A2U配置 | ❌ | 非视觉轨道 |
|
||||
| 66 | FlashPlayer | 全屏闪光播放 | ❌ | ✅实测 `flashPlayerKeys` 20/59首 / 50 keys。旧记录「全部0 keyframe」是错的,但确实量小 |
|
||||
| 67 | Title | 歌曲名字幕显示 | ❌ | ✅实测 `titleKeys` 55/59首 / 137 keys |
|
||||
| 68 | Spotlight3d ★(3) | 3D聚光灯 SetActive+颜色/强度 | ✅ | — |
|
||||
| 69 | CharaNode | 角色骨骼节点位置控制 | ❌ | — |
|
||||
| 70 | NodeScale ★(2) | 舞台节点缩放 | ❌ | ✅实测 `nodeScaleList` **存在**,**57/59首** / 541 keys。旧记录「无字段」是错的,且覆盖率远高于原估计 |
|
||||
| 71 | Fluctuation | 画面波动/震颤效果 | ❌ | ✅实测 `FluctuationKeys` 51/59首 / 330 keys |
|
||||
| 72 | CharaFootLight | 角色脚部补光灯 | ❌ | ✅实测 `charaFootLightKeys` **49/59首 / 340 keys**。旧记录「全部0 keyframe」是错的 |
|
||||
| 73 | ChromaticAberration ★(1) | 色差后处理 | ⚠️ | redOffset/greenOffset/blueOffset(通道偏移)无URP内置对应;effectType未使用 |
|
||||
| 74 | LightProjection ★(18!) | 投影灯(Gobo图案灯) | ❌ | ✅实测 `lightProjectionList` **37/59首 / 3793 keys**(单曲最多566)。URP 下 `Projector` 组件不工作,需 DecalProjector 或自写 |
|
||||
| 75 | Billboard | 广告牌(始终面向摄像机的面片)【12/58首】 | ❌ | — |
|
||||
|
||||
---
|
||||
|
||||
## 多机位后处理
|
||||
|
||||
| ID | 名称 | 作用 | 状态 | Bug |
|
||||
|----|------|------|------|-----|
|
||||
| 76 | MultiCameraPostFilm ★(2) | 多机位全屏后期叠加 | ❌ | ✅实测 `postFilm1MultiCameraKeys` 51/59首 / 708 keys |
|
||||
| 77 | MultiCameraPostEffectBloomDiffusion ★(2) | 多机位泛光 | ❌ | ✅实测 `postEffectBloomDiffusionMultiCameraKeys` **存在**,49/59首 / 381 keys。旧记录「无字段」是错的 |
|
||||
| 78 | MultiCameraColorCorrection | 多机位色彩校正 | ❌ | — |
|
||||
| 79 | MultiCameraTiltShift | 多机位移轴模糊 | ❌ | — |
|
||||
| 80 | MultiCameraRadialBlur | 多机位径向模糊 | ❌ | — |
|
||||
| 81 | MultiCameraPostEffectDOF | 多机位景深 | ❌ | — |
|
||||
|
||||
---
|
||||
|
||||
## 附加灯光 / 系统
|
||||
|
||||
| ID | 名称 | 作用 | 状态 | Bug |
|
||||
|----|------|------|------|-----|
|
||||
| 82 | AdditionalLight | 附加动态光源【22/58首】 | ❌ | — |
|
||||
| 83 | MultiLightShadow | 多光源阴影 | ❌ | ✅实测 `MultiLightShadowKeys` 37/59首 / 183 keys。旧记录「全部0 keyframe」是错的 |
|
||||
| 84 | MobControl ★(4) | 观众人群行为/动画控制 | ❌ | ✅实测 `MobControlKeys` **存在**,32/59首 / **2392 keys**(单曲最多495)。旧记录「无字段」是错的 |
|
||||
| 85 | CyalumeControl ★(4) | 荧光棒颜色/同步控制 | ❌ | ✅实测 `CyalumeControlKeys` **存在**,33/59首 / **2372 keys**(单曲最多496)。旧记录「无字段」是错的。`CyalumeController3D.cs` 已存在,只差接时间轴 |
|
||||
| 86 | CameraMotion | 摄像机动作曲线轨道 | ❌ | — |
|
||||
| 87 | WaveObject | 波浪变形物体 | ❌ | — |
|
||||
| 88 | CharaWind | 角色风效(头发/裙摆) | ❌ | ✅实测 `charaWind` 24/59首 / 744 keys |
|
||||
| 89 | CharaParts | 角色部件显隐控制 | ❌ | — |
|
||||
| 90 | CameraCutNo | 镜头编号标记(元数据) | ❌ | — |
|
||||
| 91 | ToneCurve | 色调曲线(ACES/自定义) | ❌ | — |
|
||||
| 92 | Exposure | 曝光值控制 | ❌ | — |
|
||||
| 93 | Vortex | 旋涡/扭曲屏幕效果 | ❌ | — |
|
||||
| 94 | CharaCollision | 角色碰撞辅助(非视觉) | ❌ | — |
|
||||
| 95 | TransmittedLight | 透射光(次表面散射) | ❌ | — |
|
||||
| 96 | TransmittedLightMask | 透射光遮罩 | ❌ | — |
|
||||
| 97 | Voice | 语音切换控制 | ❌ | — |
|
||||
| 98 | LipSyncPatternRange | 口型模式范围定义 | ❌ | ✅实测 `LipSyncPatternRangeKeys` **存在** |
|
||||
| 99 | LipSyncPattern | 口型模式数据 | ❌ | ✅实测 `LipSyncPatternDataList` **存在** |
|
||||
| 100 | LensDistortion | 镜头桶形/枕形畸变 | ❌ | — |
|
||||
| 101 | CharaNodeOffset | 角色骨骼节点偏移 | ❌ | — |
|
||||
| 102 | TransparentCamera | 透明度摄像机控制 | ❌ | — |
|
||||
|
||||
---
|
||||
|
||||
## 统计摘要
|
||||
|
||||
| 状态 | 数量 |
|
||||
|------|------|
|
||||
| ✅ 完整 | 21 |
|
||||
| ⚠️ 部分实现 | 7 |
|
||||
| ⚠️🔇 仅数据 | 2 |
|
||||
| 🔴 Bug(已修)| 1 |
|
||||
| ❌ 未实现 | 72 |
|
||||
|
||||
## 已知未解问题(2026-08-05)
|
||||
|
||||
按「是否有 ground truth」分类。**缺依据的一律不实现**,代码里保留 TODO 注释而不是猜一个值上去。
|
||||
|
||||
### 有依据、可以直接做
|
||||
|
||||
| 问题 | 依据 |
|
||||
|---|---|
|
||||
| ~~BlinkLight 调色板槽位映射~~ ✅ 2026-08-05 解决 | 槽位号 = 渲染器自身或最近祖先的 `lightNNN_`/`alphaNNN_` 名字前缀。live10149 全组实测「前缀种数 N」与「槽 0..N-1 的去重颜色数」精确相等,槽 N..9 一律白色填充,7 组无一例外。此前统一取第 0 槽,于是镜面球整个发粉红、truss wash 丢掉黄绿蓝紫四色 |
|
||||
| BlinkLight `pattern` 语义 | 实测有 0 和 2 两种;相位公式是推测,`pattern != 0` 时按 `cycle * i / N` 错开 |
|
||||
| BgColor1 剩余 4 组 | `BgBL`(256 keys) / `FollowSpotColor`(16) / `Shadow`(2) / `mob_00`(2) 不是物件名也不是材质名 |
|
||||
| BgColor1 的 `power` → `_ColorPower`? | `_ColorPower` 属性确实存在,但映射关系无证据,已撤回 |
|
||||
|
||||
### 缺 ground truth,已禁用
|
||||
|
||||
| 问题 | 为什么查不到 |
|
||||
|---|---|
|
||||
| **舞台自带 Animation 的播放**(283 个组件,全曲全场景不播) | 见下方专条 |
|
||||
| PostFilm 的 `filmOptionParam` 几何语义 | 游戏自己的 PostFilm shader 不在 `shader` bundle(499 个按名字和属性签名都搜过),应在 app 本体 `resources.assets` |
|
||||
| MonitorControl 的 `dispID` → 资源 | 内容资源族已定位为 `live/uvmovie/gal_uvmovie_<songid>_<NNN>`(全库 219 个目录),但 song 1177 只有 `_light` 没有 `_001`,且 `monitorCameraPosKeys` 为 0,两条路都不通 |
|
||||
|
||||
#### 专条:舞台自带 Animation 为什么**不播**(2026-08-07 决定撤除实现)
|
||||
|
||||
**现状**:舞台上 283 个 legacy `Animation` 组件一个都不播,这是**有意的**,不是漏了。
|
||||
迪斯科灯球不转、wash 灯不扫、霓虹灯牌不动,都属于这一条。
|
||||
|
||||
**机制**:这些组件全部 `playAutomatically = true`,但默认 clip(`m_Animation`)**全是空的**。
|
||||
Unity 的 playAutomatically 播的是默认 clip,为空就什么都不播 —— 原版靠
|
||||
`Gallop.Live.AnimationObjectController` 显式 `Play("clip名")`。
|
||||
|
||||
**为什么补不了**:`AnimationObjectController` 实测 **282 个实例、零序列化字段**。
|
||||
它是纯行为类,「播哪个 clip / 该不该播 / 播多快」全在方法体里,而方法体随 IL2CPP 元数据
|
||||
一起拿不到(见 `CLAUDE.md` → Blocked)。**按原签名重建它拿不到任何信息** ——
|
||||
这条路是死的,别被 282 这个实例数勾回去。
|
||||
|
||||
**素材本身是全可读的**,这正是容易上当的地方:clip 曲线、时长、wrapMode 全能 dump。
|
||||
例如镜面球那条是恒定 −179.50°/s 绕 Y、2.0 s 一圈;wash_ground 那条是 `swing_*` 节点
|
||||
90°→0°→90°、2.0 s 一个来回、12 盏同步。**但「素材是什么」推不出「原版何时以何速播它」。**
|
||||
|
||||
**试过一版替代品 `StageAnimationPlayer`,已整体撤除。** 它的策略是「只有一个 clip 就播」,
|
||||
错在把「这物件只有一个**状态**」当成了「这个状态**一直在播**」—— 后者恰恰是那个缺失的类
|
||||
每帧做的决定。实机后果:一排地面 wash 灯 2 秒一个来回疯狂摆头(原版是不扫只闪),
|
||||
镜面球虽然该转但转速无从校验。缩到最后它只让一颗球转,且转得对不对说不清 ——
|
||||
**一个转错的替代品比不转更糟:既错,又让人以为这块已经做过了。**
|
||||
|
||||
**唯一值得留下的结论**(将来任何人重做都适用):
|
||||
驱动舞台动画**不要用 `Animation.Play()`** —— 那跑 Unity 自己的时钟,live 暂停时舞台照转、
|
||||
拖动进度条不跟随。正确做法是每帧把 `AnimationState.time` 设成
|
||||
`LiveTimelineControl.currentLiveTime`(单位是秒)再 `Sample()`,与演出时间同源。
|
||||
|
||||
**什么条件下可以重做**:拿到参考视频逐首标定(属于经验值,不是 ground truth,且大概率
|
||||
一舞台一套),或者 IL2CPP 元数据解锁(见 `CLAUDE.md` → Blocked)。在那之前**不做**。
|
||||
|
||||
### 与时间轴无关的渲染问题
|
||||
|
||||
| 现象 | 状态 |
|
||||
|---|---|
|
||||
| 舞台地板长期偏亮发白 | ✅ 2026-08-06 解决。**白的不是文档里记的那三个物件** —— 是与 `plane_000` 同位同尺度、叠在一起的 `mirror_a`(同在 `pfb_env_live10149_main000` 下,局部变换全为单位)。它用 `Cygames/MirrorAndShadow/ReceiveMirror`,`_ReflectionRate` 默认 1.0 而 `_ReflectionTex` 从没赋值,Unity 对未绑定采样器代入白贴图 = 满强度白反射。实现 `Gallop.MirrorReflection` 接上反射 RT 后恢复正常。**教训见 `CLAUDE.md`「现象描述不是 ground truth」**:整轮排查都围着文档里那三个名字转,那个「`_EnvRate` 归零无变化」的实验切的是另一批物件 |
|
||||
| 小灯泡渲染成黑块 / 光柱变实心黑墙 | 🔧 2026-08-07 **找到根因并已修**(待实机确认):Gallop 发光类 shader 的混合状态写作 `Blend [_SrcBlend] [_DstBlend]`,从材质属性读;而 live10149 **26/26 个材质**都被盖上了 URP Lit 的属性表,`_DstBlend` 是 URP Lit 不透明预设的 Zero。全库 16 个声明该属性的 shader 里 **14 个作者默认是 One/One(加法)**,于是**凡是当前 shader 真读这两个属性的**光柱/闪光/投影都被按不透明画 —— 加法下「颜色为黑=不可见」变成了「实心黑墙」。`StageController.RestoreAuthoredBlendState()` 已把这两个属性恢复成 shader 声明的默认值,**实机确认修复**。⚠ 两个方向都别照抄数字:**受害材质**比存档表窄(31 个里只有 1 个真被 shader 读到 —— `mtl_env_live10149_blinklight000` / `LightBlinkBlend`,`_DstBlend` 0→1,其余存的是无人问津的过期条目);但**受害渲染器**远比「1 个材质」多 —— 材质高度共享,那一个铺满 27 个 BlinkLight 组约 1450 个渲染器,所以「小灯泡渲染成黑块」和地面 wash 黑楔子是同一个根因,实机确认一并修好。**注**:`StageTransmittedLightMask`/`StageMirrorBallShine` 无颜色属性是另一回事,仍未解 |
|
||||
| LED 大屏黑屏 | MonitorControl (10) 未实现,见上 |
|
||||
|
||||
---
|
||||
|
||||
## 优先级排序
|
||||
|
||||
**2026-08-04 用 `tools/` 全量重测后重排。** 依据 = 实测数据量 × 覆盖率 × 工作性质。
|
||||
全量表:`tools/out/scan_summary.txt`。
|
||||
|
||||
### 第一优先:数据早就在手,只是没用上(性价比最高)
|
||||
|
||||
| 轨道 | 实测数据量 | 难度 | 说明 |
|
||||
|---|---|---|---|
|
||||
| **BgColor1 (8) 舞台分支** | **43617 keys / 59首** | ★☆☆~★★☆ | 全语料最大的轨道。分发过滤已拆,剩下确认舞台侧 shader 属性 |
|
||||
| **BlinkLight (45) 补字段** | **19420 keys / 57首** | ★★☆ | 灯光线最大权重,含全部 `*_wash_*` 效果 |
|
||||
| Laser (44) / UVScrollLight (46) 补字段 | 2661 keys / 33首 | ★☆☆ | 纯管道 |
|
||||
|
||||
### 第二优先:URP 后处理 Feature(路径见 `live-shader-todo.md`)
|
||||
|
||||
| 轨道 | 实测数据量 | 路径 |
|
||||
|---|---|---|
|
||||
| **PostFilm (39)** | **~20200 keys / 59首**(三个字段合计) | B,最大的一块 |
|
||||
| PostEffectDOF (13) | 3879 keys / 59首 | A(URP 内置 DepthOfField) |
|
||||
| VolumeLight/SunShafts (37) | 2048 keys / 58首 | B,数据类已有,字段表齐全 |
|
||||
| PostEffectBloomDiffusion (14) | 1969 keys / 59首 | B |
|
||||
| RadialBlur (15) | 1667 keys / 59首 | B |
|
||||
| TiltShift (63) | 1202 keys / 58首 | B |
|
||||
|
||||
### 第三优先:只差 C# 字段声明 + handler(bundle 里数据齐备)
|
||||
|
||||
FacialToon(47, **6341 keys/59首**) > MonitorControl(10, 3911/55首,dispID 语义仍未知) > CameraLayer(16, 3306/58首) > MobControl(84, 2392/32首) > CyalumeControl(85, 2372/33首,`CyalumeController3D.cs` 已存在) > Props(35, 1940/28首) > Animation(29, 1575/39首) > MonitorCameraPos/LookAt(51/52, ~1900/34首) > CharaWind(88, 744/24首) > AdditionalLight(82, 657/23首) > SweatLocator(59, 608/32首) > NodeScale(70, 541/57首) > Fluctuation(71, 330/51首) > CharaFootLight(72, 340/49首) > Fade(40, 243/59首) > Title(67, 137/55首) > FacialNoise(26)/CharaMotionNoise(27)
|
||||
|
||||
### 第四优先:要从零搭渲染系统 ★★★
|
||||
|
||||
**LensFlare(57, 993/45首) 应当从这一档移到第一档** —— 2026-08-07 dump 确认它不需要「从零搭」:`CustomLensFlare` 的 `Flare` 指向 `Gallop.RenderPipeline.LensFlareData`(可按签名重建的 ScriptableObject),字段是 Unity legacy Flare 的逐字翻版,URP 侧有 `LensFlareComponentSRP` 现成对应。
|
||||
|
||||
LightProjection(74, 3793/37首,URP 下 Projector 不工作) > Environment(58, 1375/49首,注意还有独立的 `MirrorReflectionDataList`;**Planar Reflection 本身已不用从零搭** —— `Gallop.MirrorReflection` 已实现,缺的是把轨道关键帧接上去)
|
||||
|
||||
> `MirrorBallProjector` 不在此列但值得记一笔:它是**卡住**而不是没做。shader 需要
|
||||
> `_MirrorBallPosWS`/`_MirrorBallScale`/`_MirrorBallRotateValue`,而组件里没有任何指向灯球的
|
||||
> 引用字段,4 个 projector 全在原点且共用一个材质 —— 绑定关系只存在于拿不到的方法体里。
|
||||
> 顺带:灯球转速已不是未知(clip 恒定 −179.50°/s,见 `LIVE_DEV_MAP.md`)。
|
||||
|
||||
### 不该做
|
||||
|
||||
**HdrBloom (38)** —— 0/59 首,却已经实现了,是死代码,建议删。
|
||||
其余确认为空:`other4EyeTrackKeys`、`ScreenCaptureDataList`、`tailMotionDataList`。
|
||||
A2U / A2UConfig / CameraCutNo —— 非视觉轨道。
|
||||
|
||||
> 之前「PostFilm 到底有没有数据」的矛盾已解决:**`live-shader-todo.md` 是对的**,本文件旧版记的「全部 0 keyframe」是错的。
|
||||
Reference in New Issue
Block a user