Billboard: fix the stale camera reference, and gate the unverified facing behaviour off
Reported from a live run: a light keeps rotating, and the object visibly should not be
rotating. Two separate problems, only one of which is a bug.
The bug — BillboardController resolved its target camera once and cached it forever, while
Director.MainCameraTransform is reassigned every frame (_cameraTransforms[_activeCameraIndex])
and switches between three cameras whenever the CameraSwitcher track fires. After the first
switch the billboards face a camera the viewer is not looking through, and that camera keeps
moving along its own timeline — which reads exactly as "spinning for no reason". Now resolved
per frame. Any stage script that references "the current camera" has this hazard.
The guess — whether _rotationType == 0 means "face the camera on all axes" at all, and whether
the up vector should be the camera's (screen-aligned, rolls with the shot) or world up (yaw
only), have no ground truth behind them, and the two look completely different on screen. Per
the project rule, that goes off: EnableRotation now defaults to false, same treatment as
PostFilmRendererFeature._enableRendering. With it off the quads keep the orientation the prefab
author gave them, which is real data rather than my inference.
Target selection was checked rather than assumed, and it holds up: all 106 type-0 instances are
named *_billbord / *_billboard / glow_plane / spotlight, all 106 carry a MeshRenderer, and the
lamp bodies are further up the chain (swing_ < arm_ < stand_). So "these should billboard" is
probably right; "how they should turn" is what is unsupported.
Docs also walk back an over-claim from 050a2c9. That commit said the mirror ball rotation was
settled and "already correct in our build". The clip rate is ground truth for the clip
(-179.50 deg/s, and currentLiveTime is in seconds so there is no unit error), but the same live
run reports the balls are still too fast — so the original either applies a playback multiplier
or does not drive the ball with this clip at all. Data settles the asset, not what the original
does with the asset. The multiplier goes back on the unknowable list.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzXXkLzVC8pNA3JjyUGMbq
This commit is contained in:
@@ -19,24 +19,51 @@ namespace Gallop.Live
|
||||
/// 和 2(58 个,全是 glow_ramp)两种。
|
||||
///
|
||||
/// ⚠ `_rotationType` 的枚举语义仍未知。0 是枚举零值、类名又是 BillboardController,
|
||||
/// 按「整体朝向摄像机」处理是合理的;**2 代表什么没有依据,故不处理**,
|
||||
/// 只打一次日志。等拿到证据再补,不猜。
|
||||
/// 按「整体朝向摄像机」处理**看起来**合理;**2 代表什么没有依据,故不处理**,只打一次日志。
|
||||
///
|
||||
/// ── 2026-08-07:朝向行为默认关闭(<see cref="EnableRotation"/> = false)──
|
||||
///
|
||||
/// 实机观察到「灯一直转来转去,而那东西看起来不该转」。查下来有两层问题:
|
||||
///
|
||||
/// 1. **确定的 bug(已修)**:`_cachedTarget` 只解析一次就再不更新,而
|
||||
/// `Director.MainCameraTransform` 是**每帧重新赋值**的(`UpdateMainCamera()` 里
|
||||
/// `_cameraTransforms[_activeCameraIndex]`),并且会随 CameraSwitcher 轨道在 3 个
|
||||
/// 摄像机之间切换。于是切换之后,面片朝的是一个**观众看不到的摄像机**——而那个
|
||||
/// 摄像机仍在按自己的时间轴运动,看上去就是「无缘无故一直转」。改为每帧解析。
|
||||
///
|
||||
/// 2. **没有依据的部分(因此关掉)**:`_rotationType == 0` 到底是不是「整轴朝向摄像机」,
|
||||
/// 以及朝向该用摄像机的 up(跟着镜头 roll,屏幕对齐)还是世界 up(只绕 Y),
|
||||
/// 两者画面完全不同,而**没有任何 ground truth 能判**。按项目规矩「缺依据即不实现」,
|
||||
/// 默认关闭;关掉时面片保持 prefab 作者摆好的朝向,那至少是真实数据而不是我的猜测。
|
||||
///
|
||||
/// 目标物件本身是查过的,不是猜的:106 个 type-0 实例名字全部是
|
||||
/// `*_billbord` / `*_billboard` / `glow_plane` / `spotlight`,且 106/106 都带 MeshRenderer,
|
||||
/// 灯体在父链上(`swing_ < arm_ < stand_`)。所以「这些该 billboard 化」大概率成立,
|
||||
/// 不成立的是「怎么转」。
|
||||
/// </summary>
|
||||
public class BillboardController : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// 朝向行为总开关。缺 ground truth,默认 false —— 见类注释。
|
||||
/// 要对着参考视频标定时把它打开(和 PostFilmRendererFeature._enableRendering 同一个道理)。
|
||||
/// </summary>
|
||||
public static bool EnableRotation = false;
|
||||
|
||||
// ↓ 这三个名字来自 bundle TypeTree,改名就收不到数据
|
||||
[SerializeField] private Transform _targetCameraTransform;
|
||||
[SerializeField] private int _rotationType;
|
||||
[SerializeField] private bool _isInversedForward;
|
||||
|
||||
private Transform _cachedTarget;
|
||||
private static bool _loggedUnknownType;
|
||||
|
||||
/// <summary>
|
||||
/// **每帧都要重新解析,不能缓存** —— 生效的直播摄像机会随 CameraSwitcher 换人。
|
||||
/// </summary>
|
||||
private Transform ResolveTarget()
|
||||
{
|
||||
if (_targetCameraTransform != null) return _targetCameraTransform;
|
||||
|
||||
// 舞台数据里这个字段全是空的,回落到当前生效的直播摄像机。
|
||||
// 舞台数据里这个字段 164/164 全是空的,回落到当前生效的直播摄像机。
|
||||
if (Director.instance != null && Director.instance.MainCameraTransform != null)
|
||||
return Director.instance.MainCameraTransform;
|
||||
|
||||
@@ -46,6 +73,8 @@ namespace Gallop.Live
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (!EnableRotation) return;
|
||||
|
||||
// 放 LateUpdate:必须在 Director 当帧摆好摄像机之后再朝向它,否则永远差一帧。
|
||||
if (_rotationType != 0)
|
||||
{
|
||||
@@ -58,14 +87,15 @@ namespace Gallop.Live
|
||||
return;
|
||||
}
|
||||
|
||||
if (_cachedTarget == null) _cachedTarget = ResolveTarget();
|
||||
if (_cachedTarget == null) return;
|
||||
Transform target = ResolveTarget();
|
||||
if (target == null) return;
|
||||
|
||||
Vector3 forward = transform.position - _cachedTarget.position;
|
||||
Vector3 forward = transform.position - target.position;
|
||||
if (_isInversedForward) forward = -forward;
|
||||
if (forward.sqrMagnitude < 1e-8f) return;
|
||||
|
||||
transform.rotation = Quaternion.LookRotation(forward.normalized, _cachedTarget.up);
|
||||
// TODO: up 用 target.up(屏幕对齐、跟着镜头 roll)还是 Vector3.up(只绕 Y)无依据。
|
||||
transform.rotation = Quaternion.LookRotation(forward.normalized, target.up);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,8 +191,13 @@ MonoBehaviour 实例脚本为空。**但这些脚本是可以「接管」的**
|
||||
必须挂 `beginCameraRendering` 拿 `ScriptableRenderContext` 后 `RenderSingleCamera`)。
|
||||
这同时解决了长期的「地板发白」——`_ReflectionTex` 未绑定时 Unity 代入白贴图,
|
||||
而 `_ReflectionRate` 默认 1.0,等于满强度白反射。
|
||||
- `BillboardController` —— 字段读到了,但 `_rotationType` 是枚举,语义随 IL2CPP 元数据一起
|
||||
拿不到,所以只有数据、没有行为。这是「重建签名」的能力边界。
|
||||
- `BillboardController` —— 字段读到了,**但行为写了一版又关掉了**,这是「重建签名」的能力边界。
|
||||
`_rotationType == 0` 是不是「整轴朝向摄像机」、朝向该用摄像机 up(跟镜头 roll)还是世界 up
|
||||
(只绕 Y),都没有 ground truth;实机看是「一直转来转去,而那东西不该转」,
|
||||
于是 `BillboardController.EnableRotation` 默认 false,面片保持 prefab 作者摆好的朝向。
|
||||
同一次排查里揪出一个**确定的** bug(已修):目标摄像机只解析了一次就缓存,
|
||||
而生效摄像机随 CameraSwitcher 每帧变 —— 切换后面片朝的是观众看不见的那台,
|
||||
那台还在自己动,看上去就是无缘无故乱转。**凡是引用「当前摄像机」的舞台脚本都要注意这条。**
|
||||
|
||||
**舞台自带 Animation 的播放**(`StageAnimationPlayer`,顶替缺失的 `AnimationObjectController`):
|
||||
283 个 `Animation` 组件全部 `playAutomatically = true` 但默认 clip 为空,Unity 因此什么都不播。
|
||||
@@ -223,9 +228,14 @@ MonoBehaviour 实例脚本为空。**但这些脚本是可以「接管」的**
|
||||
(注意这条不要滥用:镜面球转速一度被记成此类,实测其实完全确定,见下。)
|
||||
- 未随包发布的 shader(PostFilm 的定义性 shader 就在 app 二进制里)。
|
||||
|
||||
**已被实测消解的「未知」(2026-08-07)**
|
||||
**「灯球转速」到底确定到什么程度(2026-08-07,当天两次修正)**
|
||||
|
||||
「灯球转速」曾被列为拿不到的常量。实测结论是它根本不存在这个未知:
|
||||
⚠️ **先说结论的边界**:clip 里的转速是硬数据,**但「原版按 1.0 倍速播这条 clip」不是**。
|
||||
实机跑下来观察是「所有迪斯科灯球还是太快」,也就是说原版多半有个播放倍率或者压根不用
|
||||
这条 clip 驱动球体 —— 那正是本节上面「速度/系数一类的常量」说的那类不可知量。
|
||||
下面的数字只证明「**我们忠实地播了这条 clip**」,不证明「和原版一致」。
|
||||
|
||||
已确定的部分:
|
||||
|
||||
- live10149 的 4 个 `MirrorBallProjector` 实例**全部** `MirrorBallIsLoopRotation = 0`、
|
||||
`MirrorBallLoopRotationSpeed = 0`、`MirrorBallLoopRotationValue = 0` ——
|
||||
@@ -234,10 +244,17 @@ MonoBehaviour 实例脚本为空。**但这些脚本是可以「接管」的**
|
||||
6 条 quaternion 曲线(`mirrorball_a_000` + `b_000..004`),7 个关键帧,
|
||||
**恒定 −179.50°/s 绕 Y**(每个关键帧算出来都是这个数),2.0 s 一圈,wrapMode = Loop。
|
||||
末帧 −359° 而不是 −360°,是为了循环接缝不重复一帧。
|
||||
- 这条 clip 正好是 `StageAnimationPlayer` 已接管的单-clip 唯一解之一,
|
||||
所以**我们的构建里灯球转速本来就是对的**,不需要任何猜测常数。
|
||||
- 这条 clip 正好是 `StageAnimationPlayer` 已接管的单-clip 唯一解之一,采样用的
|
||||
`currentLiveTime` 单位是秒(帧号 = ×60),所以**没有单位换算错误**,
|
||||
我们放出来就是 clip 写的 2.0 s 一圈。
|
||||
|
||||
教训:把「值是 0」当成「值未知」是两回事。dump 之前先看一眼开关字段。
|
||||
两条教训,方向相反,都要记:
|
||||
|
||||
1. 把「值是 0」当成「值未知」是两回事 —— dump 之前先看一眼开关字段
|
||||
(`MirrorBallIsLoopRotation` 就是那个开关)。
|
||||
2. **但「clip 的转速是确定的」推不出「转速问题解决了」**。我上一版文档写成了
|
||||
「我们的构建里转速本来就是对的」,实机观察当天就否掉了它。
|
||||
数据确定的是**素材**,不是**原版怎么用这个素材**;中间那个倍率仍然是不可知量。
|
||||
|
||||
### ⚠ 现象描述不是 ground truth
|
||||
|
||||
|
||||
+8
-5
@@ -99,17 +99,20 @@ Toon shader 已移植,卡住的主要是全屏后处理和几个逆向未完
|
||||
| 脚本 | 状态 |
|
||||
|------|------|
|
||||
| `Gallop.MirrorReflection` | ✅ 已建并实现。第二摄像机 + 反射矩阵 + 斜投影 → RT → `_ReflectionTex`。**顺带解决了长期的「舞台地板发白」** —— `mirror_a` 的 `_ReflectionTex` 从没被赋值,Unity 代入白贴图 × 默认 `_ReflectionRate = 1.0` |
|
||||
| `Gallop.Live.BillboardController` | ⚠️ 已建,字段可读(`[StageScripts]` 丢失数 892→728),但 `_rotationType` 枚举语义随 IL2CPP 元数据一起拿不到,行为未实现 |
|
||||
| `Gallop.Live.BillboardController` | ⚠️ 已建,字段可读(`[StageScripts]` 丢失数 892→728)。朝向行为写过一版,实机是「一直转来转去而那东西不该转」,已默认关闭(`EnableRotation = false`)——`_rotationType` 语义和 up 轴取法都无依据。同批修掉一个确定 bug:目标摄像机被缓存,而生效摄像机随 CameraSwitcher 每帧变 |
|
||||
| `Gallop.Live.AnimationObjectController` | ⚠️ 未建;其最直接的后果(283 个 `Animation` 一个都不播)已由 `StageAnimationPlayer` 顶上:只接管单 clip 的唯一解情形,按 `currentLiveTime` 采样而不是 `Animation.Play()`,暂停/拖动进度条都跟随 |
|
||||
| `Gallop.RenderPipeline.CustomLensFlare` (+`LensFlareData`) | ❌ 未建,但**数据齐全、语义不受 IL2CPP 阻塞**——`LensFlareData` 是 Unity legacy Flare 的逐字翻版,URP 侧有 `LensFlareComponentSRP` 对应。199+5 个物件,星芒在原版截图里很显眼。**下一个该做的就是它** |
|
||||
| `Gallop.RenderPipeline.MirrorBallProjector` | ❌ 字段已 dump,但 4 个 projector 全在原点、共用一个材质、没有任何指向灯球的引用字段——绑定关系只在方法体里。**卡住,不是没做** |
|
||||
| `Gallop.Live.ShaderParam.ShaderParamController` | ⚠️ 实测是空操作(两个向量都是 (1,1,1,1)),从待办里划掉 |
|
||||
| 其余 5 个 | ❌ 未建,见 `CLAUDE.md` 清单 |
|
||||
|
||||
> **镜面球转速不再是悬案。** live10149 的 4 个 `MirrorBallProjector` 全部
|
||||
> `MirrorBallIsLoopRotation = 0` / `LoopRotationSpeed = 0`,shader 的 loop-rotation 通路是
|
||||
> 关的;真正在转的是 clip(恒定 **−179.50°/s 绕 Y**,2.0 s 一圈,7 帧算出来都是这个数),
|
||||
> 而这条 clip `StageAnimationPlayer` 已经在按 `currentLiveTime` 播了 —— 转速本来就是对的。
|
||||
> **镜面球转速:素材确定,倍率仍未知。** live10149 的 4 个 `MirrorBallProjector` 全部
|
||||
> `MirrorBallIsLoopRotation = 0` / `LoopRotationSpeed = 0`,shader 的 loop-rotation 通路是
|
||||
> 关的;真正在转的是 clip(恒定 **−179.50°/s 绕 Y**,2.0 s 一圈,7 帧算出来都是这个数),
|
||||
> `StageAnimationPlayer` 按 `currentLiveTime`(秒)忠实地播它。
|
||||
> **但实机观察是「所有迪斯科灯球还是太快」** —— 所以原版要么有播放倍率,要么根本不用这条
|
||||
> clip 驱动球体。数据确定的是素材,不是原版怎么用素材。倍率属于不可知常量,
|
||||
> 要定它只能对着参考视频标定。
|
||||
|
||||
### 内部系统
|
||||
| 轨道 | 状态 |
|
||||
|
||||
Reference in New Issue
Block a user