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` 偏亮发白, 早于时间轴的改动就存在」。这次查清了它们的共同点。