diff --git a/Assets/Scripts/umamusume/Gallop/Live/Director.cs b/Assets/Scripts/umamusume/Gallop/Live/Director.cs
index 1ac0780..5b54493 100644
--- a/Assets/Scripts/umamusume/Gallop/Live/Director.cs
+++ b/Assets/Scripts/umamusume/Gallop/Live/Director.cs
@@ -478,8 +478,11 @@ namespace Gallop.Live
// 材质原值上、或者对应完全不同的东西。2026-08-05 试写过一版,无法判断对错,
// 遂按「缺依据即不做」撤回。t.hasColorPower 保留,供将来确认后启用。
//
- // (注:舞台地板 plane_000 / stage_object_001 / specular_002 长期偏亮发白
- // 是**早于本改动就存在**的问题,与 _ColorPower 无关,另行排查。)
+ // (注:曾长期以为「地板 plane_000 / stage_object_001 / specular_002 发白」,
+ // 2026-08-05 查明白的其实是**叠在同一位置的 mirror_a** —— 它用
+ // Cygames/MirrorAndShadow/ReceiveMirror,_ReflectionRate 默认 1.0 而
+ // _ReflectionTex 从没赋值,Unity 代入白贴图 = 满强度白反射。
+ // 实现 Gallop.MirrorReflection 后已解决。与 _ColorPower 无关。)
t.renderer.SetPropertyBlock(_bgColor1Block);
}
diff --git a/Assets/Scripts/umamusume/Gallop/MirrorReflection.cs b/Assets/Scripts/umamusume/Gallop/MirrorReflection.cs
new file mode 100644
index 0000000..abd7ed3
--- /dev/null
+++ b/Assets/Scripts/umamusume/Gallop/MirrorReflection.cs
@@ -0,0 +1,205 @@
+using UnityEngine;
+using UnityEngine.Rendering;
+using UnityEngine.Rendering.Universal;
+
+namespace Gallop
+{
+ ///
+ /// 平面镜反射。live10149 上 1 个实例,宿主是 mirror_a。
+ ///
+ /// 按原签名重建(bundle MonoScript: ClassName=MirrorReflection, Namespace=Gallop,
+ /// AssemblyName=umamusume),所以下面的字段由 Unity 从 bundle 直接反序列化,不是猜的。
+ /// 字段名逐字照抄 TypeTree —— 包括原作的拼写错误 `_mirrorCliipPlaneOffset`,
+ /// 改成正确拼写反而收不到数据。
+ ///
+ /// **接收端是查出来的,不是猜的**:`mtl_env_live10149_mirror000` 用
+ /// `Cygames/MirrorAndShadow/ReceiveMirror`,该 shader 只有 4 个属性 ——
+ /// `_MainTex` / `_ReflectionTex` / `_Color` / `_ReflectionRate`,
+ /// 而组件字段 `_mirrorReflectionColor`、`_mirrorReflectionRate` 与后两者逐字对应。
+ ///
+ /// live10149 实测值:textureSize=256、reflectionRate=1、reflectionColor=白、
+ /// renderLayers=12544、useBackgroundColor=1、backgroundColor=透明黑、
+ /// clipPlaneOffset=0、baseCamera=null(回落当前直播摄像机)。
+ ///
+ /// ── 没有依据、因此没有实现的部分 ──
+ ///
+ /// `_direction` 枚举,值为 0,**语义未知**。IL2CPP 元数据取不到(见 CLAUDE.md),
+ /// 所以无法知道它是不是在选镜面法线轴。这里不使用它,
+ /// 改为**从网格自身的法线推导镜面**——那是几何事实,不是猜测。
+ /// `_mirrorDistortion*` 属于 ReceiveDistortionMirror 变体,本材质用的是普通版,不参与。
+ /// `_useMirrorTextureScale` / `_mirrorTextureScaleForBaseCamera` 语义未知,未使用。
+ ///
+ public class MirrorReflection : MonoBehaviour
+ {
+ // ↓ 名字必须与 bundle TypeTree 逐字一致(含原作拼写错误)
+ [SerializeField] private LayerMask _renderLayers;
+ [SerializeField] private int _mirrorTextureSize = 256;
+ [SerializeField] private float _mirrorCliipPlaneOffset;
+ [SerializeField] private Camera _baseCamera;
+ [SerializeField] private float _mirrorReflectionRate = 1f;
+ [SerializeField] private Vector4 _mirrorDistortionTileOffset;
+ [SerializeField] private Vector4 _mirrorDistortionPower;
+ [SerializeField] private int _direction;
+ [SerializeField] private Color _mirrorReflectionColor = Color.white;
+ [SerializeField] private Color _backgroundColor;
+ [SerializeField] private bool _useBackgroundColor;
+ [SerializeField] private bool _useMirrorTextureScale;
+ [SerializeField] private float _mirrorTextureScaleForBaseCamera = 1f;
+
+ private Camera _mirrorCamera;
+ private RenderTexture _reflectionTexture;
+ private Renderer _renderer;
+ private MaterialPropertyBlock _block;
+ private bool _rendering;
+ private bool _logged;
+
+ private static readonly int kReflectionTex = Shader.PropertyToID("_ReflectionTex");
+ private static readonly int kReflectionRate = Shader.PropertyToID("_ReflectionRate");
+ private static readonly int kColor = Shader.PropertyToID("_Color");
+
+ // URP 下不能用 Camera.Render()(SRP 会直接报错、什么都不画),
+ // 必须挂 beginCameraRendering 拿到 ScriptableRenderContext 再 RenderSingleCamera。
+ private void OnEnable() => RenderPipelineManager.beginCameraRendering += OnBeginCamera;
+
+ private void OnDisable()
+ {
+ RenderPipelineManager.beginCameraRendering -= OnBeginCamera;
+ if (_mirrorCamera != null) { DestroyImmediate(_mirrorCamera.gameObject); _mirrorCamera = null; }
+ if (_reflectionTexture != null) { DestroyImmediate(_reflectionTexture); _reflectionTexture = null; }
+ }
+
+ private Camera ResolveBaseCamera()
+ {
+ if (_baseCamera != null) return _baseCamera;
+ if (Gallop.Live.Director.instance != null)
+ {
+ var t = Gallop.Live.Director.instance.MainCameraTransform;
+ if (t != null)
+ {
+ var c = t.GetComponent();
+ if (c != null) return c;
+ }
+ }
+ return Camera.main;
+ }
+
+ ///
+ /// 镜面法线。`_direction` 的枚举含义拿不到,所以不用它,改从网格法线推导 ——
+ /// 平面镜的网格法线是确定的几何量,比猜枚举可靠。取不到网格时回落 transform.up。
+ ///
+ private Vector3 ResolveNormal()
+ {
+ var mf = GetComponent();
+ var mesh = mf != null ? mf.sharedMesh : null;
+ if (mesh != null && mesh.normals != null && mesh.normals.Length > 0)
+ return transform.TransformDirection(mesh.normals[0]).normalized;
+ return transform.up;
+ }
+
+ private void OnBeginCamera(ScriptableRenderContext ctx, Camera cam)
+ {
+ if (_rendering) return; // 防止镜中镜递归
+ if (cam == _mirrorCamera) return;
+
+ _renderer ??= GetComponent();
+ if (_renderer == null) return;
+
+ Camera baseCam = ResolveBaseCamera();
+ if (baseCam == null || cam != baseCam) return;
+
+ EnsureResources(baseCam);
+ if (_mirrorCamera == null || _reflectionTexture == null) return;
+
+ Vector3 normal = ResolveNormal();
+ Vector3 pos = transform.position;
+ float d = -Vector3.Dot(normal, pos) - _mirrorCliipPlaneOffset;
+ Vector4 plane = new Vector4(normal.x, normal.y, normal.z, d);
+
+ Matrix4x4 reflection = CalculateReflectionMatrix(plane);
+
+ _mirrorCamera.transform.position = reflection.MultiplyPoint(baseCam.transform.position);
+ _mirrorCamera.transform.rotation = baseCam.transform.rotation;
+ _mirrorCamera.worldToCameraMatrix = baseCam.worldToCameraMatrix * reflection;
+
+ // 斜投影:近裁剪面贴合镜面,避免映出镜子背后的东西
+ Vector4 clipPlane = CameraSpacePlane(_mirrorCamera, pos, normal, 1f);
+ _mirrorCamera.projectionMatrix = baseCam.CalculateObliqueMatrix(clipPlane);
+
+ _mirrorCamera.fieldOfView = baseCam.fieldOfView;
+ _mirrorCamera.nearClipPlane = baseCam.nearClipPlane;
+ _mirrorCamera.farClipPlane = baseCam.farClipPlane;
+
+ _rendering = true;
+ GL.invertCulling = true; // 镜像后绕序反了
+#pragma warning disable CS0618
+ UniversalRenderPipeline.RenderSingleCamera(ctx, _mirrorCamera);
+#pragma warning restore CS0618
+ GL.invertCulling = false;
+ _rendering = false;
+
+ _block ??= new MaterialPropertyBlock();
+ _renderer.GetPropertyBlock(_block);
+ _block.SetTexture(kReflectionTex, _reflectionTexture);
+ _block.SetFloat(kReflectionRate, _mirrorReflectionRate);
+ _block.SetColor(kColor, _mirrorReflectionColor);
+ _renderer.SetPropertyBlock(_block);
+
+ if (!_logged)
+ {
+ _logged = true;
+ Debug.Log($"[MirrorReflection] '{name}' size={_mirrorTextureSize} rate={_mirrorReflectionRate} " +
+ $"layers={(int)_renderLayers} baseCam={(baseCam != null ? baseCam.name : "")} " +
+ $"normal={normal} | _direction={_direction}(枚举语义未知,未使用)");
+ }
+ }
+
+ private void EnsureResources(Camera baseCam)
+ {
+ int size = Mathf.Max(16, _mirrorTextureSize);
+ if (_reflectionTexture == null || _reflectionTexture.width != size)
+ {
+ if (_reflectionTexture != null) DestroyImmediate(_reflectionTexture);
+ _reflectionTexture = new RenderTexture(size, size, 24) { name = $"MirrorRT_{name}" };
+ _reflectionTexture.Create();
+ }
+
+ if (_mirrorCamera == null)
+ {
+ var go = new GameObject($"MirrorCamera_{name}") { hideFlags = HideFlags.HideAndDontSave };
+ _mirrorCamera = go.AddComponent();
+ _mirrorCamera.enabled = false; // 只手动 Render()
+ }
+
+ _mirrorCamera.targetTexture = _reflectionTexture;
+ _mirrorCamera.cullingMask = _renderLayers;
+ if (_useBackgroundColor)
+ {
+ _mirrorCamera.clearFlags = CameraClearFlags.SolidColor;
+ _mirrorCamera.backgroundColor = _backgroundColor;
+ }
+ else
+ {
+ _mirrorCamera.clearFlags = baseCam.clearFlags;
+ _mirrorCamera.backgroundColor = baseCam.backgroundColor;
+ }
+ }
+
+ private static Matrix4x4 CalculateReflectionMatrix(Vector4 p)
+ {
+ Matrix4x4 m = Matrix4x4.identity;
+ m.m00 = 1f - 2f * p.x * p.x; m.m01 = -2f * p.x * p.y; m.m02 = -2f * p.x * p.z; m.m03 = -2f * p.w * p.x;
+ m.m10 = -2f * p.y * p.x; m.m11 = 1f - 2f * p.y * p.y; m.m12 = -2f * p.y * p.z; m.m13 = -2f * p.w * p.y;
+ m.m20 = -2f * p.z * p.x; m.m21 = -2f * p.z * p.y; m.m22 = 1f - 2f * p.z * p.z; m.m23 = -2f * p.w * p.z;
+ return m;
+ }
+
+ private Vector4 CameraSpacePlane(Camera cam, Vector3 pos, Vector3 normal, float sign)
+ {
+ Vector3 offsetPos = pos + normal * _mirrorCliipPlaneOffset;
+ Matrix4x4 m = cam.worldToCameraMatrix;
+ Vector3 cpos = m.MultiplyPoint(offsetPos);
+ Vector3 cnormal = m.MultiplyVector(normal).normalized * sign;
+ return new Vector4(cnormal.x, cnormal.y, cnormal.z, -Vector3.Dot(cpos, cnormal));
+ }
+ }
+}
diff --git a/Assets/Scripts/umamusume/Gallop/MirrorReflection.cs.meta b/Assets/Scripts/umamusume/Gallop/MirrorReflection.cs.meta
new file mode 100644
index 0000000..24a3340
--- /dev/null
+++ b/Assets/Scripts/umamusume/Gallop/MirrorReflection.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 74ba506727844be2b5d4af3f9664c42c
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/CLAUDE.md b/CLAUDE.md
index 641f57a..962c424 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -173,6 +173,20 @@ MonoBehaviour 实例脚本为空。**但这些脚本是可以「接管」的**
- **速度/系数一类的常量** —— 如果原版在代码里对动画做了调速,那个倍率无从得知。
- 未随包发布的 shader(PostFilm 的定义性 shader 就在 app 二进制里)。
+### ⚠ 现象描述不是 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` 归零无变化」的实验因此也是误导:切的是另一批物件。
+
+**排查视觉问题的第一步应该是确认现象归属于哪个物件**(让代码把可疑物件隔离/高亮),
+而不是从文档里的名字出发。
+
### 间接手段(已被证明有效)
拿不到直接答案时,**相关性交叉验证**多次奏效,比猜测可靠得多:
diff --git a/LIVE_AUDIT_2026-08-05.md b/LIVE_AUDIT_2026-08-05.md
index 7748c3d..1666c8f 100644
--- a/LIVE_AUDIT_2026-08-05.md
+++ b/LIVE_AUDIT_2026-08-05.md
@@ -359,7 +359,31 @@ live10149 上的受害者:`light001_wash_ground_a`(×4)、`swing_wash_ground_a
跟着祖先一起显隐。容器照常关,子物件保持 prefab 状态。判重键也统一成 `tmp_name`。
纯增益,没有「某盏灯从此不亮」的倒退风险 —— 因为不再有子物件被单独关掉。
-### C4. 舞台地板 / 高光面发白 —— 已定位到 shader,只剩一个运行时未知项
+### C4. 舞台地板发白 —— ✅ 已解决,但**我整段都在查错的物件**
+
+**结论**:发白的是 `mirror_a`,它用 `Cygames/MirrorAndShadow/ReceiveMirror`,
+该 shader 的 `_ReflectionRate` 默认 **1.0**,而 `_ReflectionTex` 从来没有人赋值 ——
+**Unity 对未绑定的贴图采样器代入白贴图**,于是满强度白色反射 = 一块纯白地板。
+实现 `MirrorReflection`(把平面反射 RT 接上 `_ReflectionTex`)之后,地板变成了正常镜面。
+
+**我错在哪**:CLAUDE.md 记着「地板 `plane_000` / `stage_object_001` / `specular_002` 发白」,
+我**直接采信了这个前提**,整段排查都围绕这三个物件(它们用 `DefaultEnvMapNoAmbient`),
+从没验证过屏幕上白的那块到底是哪个物件。
+
+`mirror_a` 与 `plane_000` **同位同尺度**(同在 `pfb_env_live10149_main000` 下,
+局部变换全为单位),是叠在一起的两层地面 —— 肉眼分不出,名字却完全不同。
+
+这也解释了那个 F9 实验为什么"无变化":**我切的是另一批物件的 `_EnvRate`**,
+自然影响不到 `mirror_a`。当时我把这个结果当成"env 反射项无关"的证据,
+其实它只说明"那三个物件不是白的来源"。
+
+**教训**:文档里的现象描述("哪个物件看起来不对")和字段值、shader 属性表不一样,
+**它没有经过验证**。排查前先确认现象归属于哪个物件,再去查那个物件。
+下次遇到"某某看起来不对",第一步应该是让代码把可疑物件高亮/隔离,而不是从名字出发。
+
+以下为定位过程的记录(结论已被上面推翻,保留是因为排除法本身有效)。
+
+### C4-旧. 曾经的排查路径
CLAUDE.md 长期记着「地板 `plane_000` / `stage_object_001` / `specular_002` 偏亮发白,
早于时间轴的改动就存在」。这次查清了它们的共同点。