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;
}
}
}