using System.Collections.Generic; using UnityEngine; namespace BrewMonster { /// /// Shared static helpers used by ElsePlayerPortraitCapture and HostPlayerPortraitCapture. /// 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" }; // ────────────────────────────────────────────────────────────────────────────── // Model / bone search // ────────────────────────────────────────────────────────────────────────────── /// /// Returns the root transform of the visual model inside . /// Tries to find the first child that owns a SkinnedMeshRenderer; falls back to the /// root itself if a SkinnedMeshRenderer is found anywhere in the hierarchy. /// public static Transform FindVisualModelRoot(Transform playerRoot) { if (playerRoot == null) return null; // Walk immediate children first — model root is usually a direct child for (int i = 0; i < playerRoot.childCount; i++) { var child = playerRoot.GetChild(i); if (child.GetComponentInChildren() != null) return child; } // Fallback: if the root itself contains a SMR, use it if (playerRoot.GetComponentInChildren() != null) return playerRoot; return null; } /// /// Searches 's hierarchy for a head bone by common name patterns. /// public static Transform FindHeadBone(Transform modelRoot) { if (modelRoot == null) return null; foreach (var boneName in HeadBoneNames) { var bone = FindChildByName(modelRoot, boneName); if (bone != null) return bone; } return null; } /// Recursive depth-first search for a child transform by exact name. public static Transform FindChildByName(Transform root, string name) { if (root.name == name) return root; for (int i = 0; i < root.childCount; i++) { var result = FindChildByName(root.GetChild(i), name); if (result != null) return result; } return null; } // ────────────────────────────────────────────────────────────────────────────── // Clone setup // ────────────────────────────────────────────────────────────────────────────── /// /// Strips non-visual components from a cloned GameObject so it becomes a lightweight /// render-only puppet. Keeps Animator so idle animations continue to play. /// public static void CleanupCloneComponents(GameObject go) { foreach (var col in go.GetComponentsInChildren(true)) Object.Destroy(col); foreach (var rb in go.GetComponentsInChildren(true)) Object.Destroy(rb); foreach (var mb in go.GetComponentsInChildren(true)) { if (mb is Animator) continue; Object.Destroy(mb); } } /// Recursively sets the layer of and all its children. public static void SetLayerRecursive(GameObject go, int layer) { go.layer = layer; foreach (Transform child in go.transform) 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 // ────────────────────────────────────────────────────────────────────────────── /// /// Creates a Camera child GameObject under whose culling mask /// covers only . Camera starts disabled. /// public static Camera CreatePortraitCamera(Transform parent, string goName, int portraitLayer, float fov) { var camGO = new GameObject(goName); camGO.transform.SetParent(parent, false); var cam = camGO.AddComponent(); cam.cullingMask = 1 << portraitLayer; cam.clearFlags = CameraClearFlags.SolidColor; cam.backgroundColor = Color.clear; cam.fieldOfView = fov; cam.nearClipPlane = 0.1f; cam.farClipPlane = 200f; cam.enabled = false; return cam; } /// /// Creates a 256×256 ARGB32 RenderTexture and wires it to if provided. /// public static RenderTexture CreatePortraitRT(string rtName, Camera cam) { var rt = new RenderTexture(256, 256, 16, RenderTextureFormat.ARGB32) { name = rtName, antiAliasing = 1 }; rt.Create(); if (cam != null) cam.targetTexture = rt; return rt; } } }