diff --git a/Assets/PerfectWorld/Scripts/UI/ElsePlayerPortraitCapture.cs b/Assets/PerfectWorld/Scripts/UI/ElsePlayerPortraitCapture.cs index 01d982ef17..9ea0eeaa27 100644 --- a/Assets/PerfectWorld/Scripts/UI/ElsePlayerPortraitCapture.cs +++ b/Assets/PerfectWorld/Scripts/UI/ElsePlayerPortraitCapture.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using BrewMonster.Scripts.Managers; using UnityEngine; @@ -50,9 +51,14 @@ namespace BrewMonster [SerializeField][Range(10f, 60f)] private float fieldOfView = 35f; + [Header("Materials")] + [Tooltip("Clone each renderer material on the portrait clone and switch copies to URP/Unlit.")] + [SerializeField] private bool usePortraitUnlitMaterials = true; + // ── runtime state ──────────────────────────────────────────────────────────── private GameObject _portraitClone; private Transform _headBone; + private readonly List _portraitMaterialInstances = new(); public RenderTexture OutputTexture => outputTexture; @@ -106,6 +112,14 @@ namespace BrewMonster // Hide from main camera — portrait camera sees only this layer PortraitCaptureUtils.SetLayerRecursive(_portraitClone, portraitLayer); + if (usePortraitUnlitMaterials) + { + PortraitCaptureUtils.ApplyClonedUrpUnlitMaterials( + _portraitClone, + _portraitMaterialInstances, + static msg => BMLogger.LogWarning(msg)); + } + _headBone = PortraitCaptureUtils.FindHeadBone(_portraitClone.transform); AttachCameraToHeadBone(); @@ -123,6 +137,12 @@ namespace BrewMonster Object.Destroy(_portraitClone); _portraitClone = null; } + for (int i = 0; i < _portraitMaterialInstances.Count; i++) + { + if (_portraitMaterialInstances[i] != null) + Object.Destroy(_portraitMaterialInstances[i]); + } + _portraitMaterialInstances.Clear(); DetachCamera(); _headBone = null; } diff --git a/Assets/PerfectWorld/Scripts/UI/HostPlayerPortraitCapture.cs b/Assets/PerfectWorld/Scripts/UI/HostPlayerPortraitCapture.cs index 09e737c6b0..c14e8747fa 100644 --- a/Assets/PerfectWorld/Scripts/UI/HostPlayerPortraitCapture.cs +++ b/Assets/PerfectWorld/Scripts/UI/HostPlayerPortraitCapture.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using BrewMonster.Scripts; using BrewMonster.Scripts.Managers; using UnityEngine; @@ -60,6 +61,10 @@ namespace BrewMonster [SerializeField][Range(10f, 60f)] private float fieldOfView = 40f; + [Header("Materials")] + [Tooltip("Clone each renderer material and switch copies to URP/Unlit for stable portrait lighting.")] + [SerializeField] private bool usePortraitUnlitMaterials = true; + // ────────────────────────────────────────────────────────────────────────────── // Runtime state // ────────────────────────────────────────────────────────────────────────────── @@ -67,9 +72,18 @@ namespace BrewMonster private Transform _modelTransform; private Transform _headBone; + private readonly List _portraitMaterialInstances = new(); + private readonly List _previewMaterialRestore = new(); + // Stored while waiting for async model load; -1 means no pending request private int _pendingRoleId = -1; + private struct RendererSharedMaterialsSnapshot + { + public Renderer Renderer; + public Material[] SharedMaterials; + } + public RenderTexture OutputTexture => outputTexture; // ────────────────────────────────────────────────────────────────────────────── @@ -107,6 +121,7 @@ namespace BrewMonster public void AttachToPlayerModelPreview(int roleId) { UnsubscribeModelReady(); + RestorePreviewMaterials(); DetachCamera(); if (PlayerModelPreview.Instance == null) @@ -128,6 +143,7 @@ namespace BrewMonster /// Fallback: attach directly to any player hierarchy root. public void SetHostPlayer(Transform hostPlayerRoot) { + RestorePreviewMaterials(); DetachCamera(); if (hostPlayerRoot == null) return; _modelTransform = hostPlayerRoot; @@ -141,6 +157,7 @@ namespace BrewMonster public void ClearPortrait() { UnsubscribeModelReady(); + RestorePreviewMaterials(); DetachCamera(); _modelTransform = null; _headBone = null; @@ -231,6 +248,15 @@ namespace BrewMonster { if (_modelTransform == null) return; + if (usePortraitUnlitMaterials) + { + CaptureRendererMaterialsForRestore(_modelTransform.gameObject); + PortraitCaptureUtils.ApplyClonedUrpUnlitMaterials( + _modelTransform.gameObject, + _portraitMaterialInstances, + static msg => BMLogger.LogWarning(msg)); + } + // Move model to portrait layer so the dedicated camera can render it PortraitCaptureUtils.SetLayerRecursive(_modelTransform.gameObject, portraitLayer); @@ -246,6 +272,46 @@ namespace BrewMonster SetCameraEnabled(true); } + /// + /// Restores renderers to their original shared materials + /// and destroys portrait-only material instances. + /// + private void RestorePreviewMaterials() + { + for (int i = 0; i < _previewMaterialRestore.Count; i++) + { + var snap = _previewMaterialRestore[i]; + if (snap.Renderer != null && snap.SharedMaterials != null) + snap.Renderer.sharedMaterials = snap.SharedMaterials; + } + _previewMaterialRestore.Clear(); + + for (int i = 0; i < _portraitMaterialInstances.Count; i++) + { + if (_portraitMaterialInstances[i] != null) + Object.Destroy(_portraitMaterialInstances[i]); + } + _portraitMaterialInstances.Clear(); + } + + private void CaptureRendererMaterialsForRestore(GameObject root) + { + if (root == null) return; + + var renderers = root.GetComponentsInChildren(true); + for (int i = 0; i < renderers.Length; i++) + { + var r = renderers[i]; + var shared = r.sharedMaterials; + if (shared == null || shared.Length == 0) continue; + + var copy = new Material[shared.Length]; + System.Array.Copy(shared, copy, shared.Length); + _previewMaterialRestore.Add( + new RendererSharedMaterialsSnapshot { Renderer = r, SharedMaterials = copy }); + } + } + /// /// Parents the portrait camera to "Bip01 Head" and sets its initial position /// directly in front of the face. diff --git a/Assets/PerfectWorld/Scripts/UI/PortraitCaptureUtils.cs b/Assets/PerfectWorld/Scripts/UI/PortraitCaptureUtils.cs index 7e5c4195cd..4f76bad599 100644 --- a/Assets/PerfectWorld/Scripts/UI/PortraitCaptureUtils.cs +++ b/Assets/PerfectWorld/Scripts/UI/PortraitCaptureUtils.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using UnityEngine; namespace BrewMonster @@ -7,6 +8,19 @@ namespace BrewMonster /// public static class PortraitCaptureUtils { + private static readonly string[] UrpUnlitShaderNames = + { + "Universal Render Pipeline/Unlit", + }; + + private static readonly int BaseMapId = Shader.PropertyToID("_BaseMap"); + private static readonly int BaseColorId = Shader.PropertyToID("_BaseColor"); + private static readonly int BaseMapStId = Shader.PropertyToID("_BaseMap_ST"); + private static readonly int MainTexId = Shader.PropertyToID("_MainTex"); + private static readonly int MainTexStId = Shader.PropertyToID("_MainTex_ST"); + private static readonly int ColorId = Shader.PropertyToID("_Color"); + private static readonly int CutoffId = Shader.PropertyToID("_Cutoff"); + private static readonly string[] HeadBoneNames = { "Bip01 Head", "Bip001 Head", "head", "Head", "Bone_Head", "HEAD" @@ -97,6 +111,117 @@ namespace BrewMonster SetLayerRecursive(child.gameObject, layer); } + // ────────────────────────────────────────────────────────────────────────────── + // Portrait materials (clone + URP Unlit) + // ────────────────────────────────────────────────────────────────────────────── + + /// + /// Resolves the built-in URP Unlit shader by name. Returns null if the project + /// does not include URP or the shader is stripped from the build. + /// + public static Shader FindUrpUnlitShader() + { + for (int i = 0; i < UrpUnlitShaderNames.Length; i++) + { + var s = Shader.Find(UrpUnlitShaderNames[i]); + if (s != null) + return s; + } + return null; + } + + /// + /// For every under , replaces each slot + /// with new Material(original) (so shared assets are never mutated), then assigns + /// and copies main texture / tint where possible. + /// All created instances are appended to for later + /// Destroy (ElsePlayer clone) or for bookkeeping alongside restore (host preview). + /// + public static void ApplyClonedUrpUnlitMaterials( + GameObject root, + List createdInstances, + System.Action logWarning = null) + { + if (root == null || createdInstances == null) return; + + Shader unlit = FindUrpUnlitShader(); + if (unlit == null) + { + logWarning?.Invoke( + "[PortraitCaptureUtils] URP Unlit shader not found — portrait keeps cloned materials with original shaders."); + } + + var renderers = root.GetComponentsInChildren(true); + for (int r = 0; r < renderers.Length; r++) + { + var renderer = renderers[r]; + var shared = renderer.sharedMaterials; + if (shared == null || shared.Length == 0) continue; + + var newMats = new Material[shared.Length]; + for (int i = 0; i < shared.Length; i++) + { + var src = shared[i]; + if (src == null) + { + newMats[i] = null; + continue; + } + + var clone = new Material(src); + createdInstances.Add(clone); + + if (unlit != null) + ConvertMaterialToUrpUnlit(clone, src, unlit); + + newMats[i] = clone; + } + + renderer.sharedMaterials = newMats; + } + } + + /// + /// Reads common albedo/texture slots from and reapplies them + /// on after switching to URP Unlit. + /// + public static void ConvertMaterialToUrpUnlit(Material target, Material source, Shader urpUnlit) + { + if (target == null || source == null || urpUnlit == null) return; + + Texture mainTex = null; + if (source.HasProperty(BaseMapId)) + mainTex = source.GetTexture(BaseMapId); + else if (source.HasProperty(MainTexId)) + mainTex = source.GetTexture(MainTexId); + + Vector4 baseSt = new Vector4(1f, 1f, 0f, 0f); + if (source.HasProperty(BaseMapStId)) + baseSt = source.GetVector(BaseMapStId); + else if (source.HasProperty(MainTexStId)) + baseSt = source.GetVector(MainTexStId); + + Color baseColor = Color.white; + if (source.HasProperty(BaseColorId)) + baseColor = source.GetColor(BaseColorId); + else if (source.HasProperty(ColorId)) + baseColor = source.GetColor(ColorId); + + target.shader = urpUnlit; + + if (mainTex != null && target.HasProperty(BaseMapId)) + target.SetTexture(BaseMapId, mainTex); + + if (target.HasProperty(BaseMapStId)) + target.SetVector(BaseMapStId, baseSt); + + if (target.HasProperty(BaseColorId)) + target.SetColor(BaseColorId, baseColor); + + if (source.HasProperty(CutoffId) && target.HasProperty(CutoffId)) + target.SetFloat(CutoffId, source.GetFloat(CutoffId)); + } + // ────────────────────────────────────────────────────────────────────────────── // Camera / RenderTexture factories // ──────────────────────────────────────────────────────────────────────────────