265 lines
12 KiB
C#
265 lines
12 KiB
C#
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
|
||
namespace BrewMonster
|
||
{
|
||
/// <summary>
|
||
/// Shared static helpers used by ElsePlayerPortraitCapture and HostPlayerPortraitCapture.
|
||
/// </summary>
|
||
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
|
||
// ──────────────────────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Returns the root transform of the visual model inside <paramref name="playerRoot"/>.
|
||
/// 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.
|
||
/// </summary>
|
||
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<SkinnedMeshRenderer>() != null)
|
||
return child;
|
||
}
|
||
|
||
// Fallback: if the root itself contains a SMR, use it
|
||
if (playerRoot.GetComponentInChildren<SkinnedMeshRenderer>() != null)
|
||
return playerRoot;
|
||
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Searches <paramref name="modelRoot"/>'s hierarchy for a head bone by common name patterns.
|
||
/// </summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>Recursive depth-first search for a child transform by exact name.</summary>
|
||
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
|
||
// ──────────────────────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Strips non-visual components from a cloned GameObject so it becomes a lightweight
|
||
/// render-only puppet. Keeps Animator so idle animations continue to play.
|
||
/// </summary>
|
||
public static void CleanupCloneComponents(GameObject go)
|
||
{
|
||
foreach (var col in go.GetComponentsInChildren<Collider>(true))
|
||
Object.Destroy(col);
|
||
|
||
foreach (var rb in go.GetComponentsInChildren<Rigidbody>(true))
|
||
Object.Destroy(rb);
|
||
|
||
foreach (var mb in go.GetComponentsInChildren<MonoBehaviour>(true))
|
||
{
|
||
if (mb is Animator) continue;
|
||
Object.Destroy(mb);
|
||
}
|
||
}
|
||
|
||
/// <summary>Recursively sets the layer of <paramref name="go"/> and all its children.</summary>
|
||
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)
|
||
// ──────────────────────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// For every <see cref="Renderer"/> under <paramref name="root"/>, replaces each slot
|
||
/// with <c>new Material(original)</c> (so shared assets are never mutated), then assigns
|
||
/// <see cref="FindUrpUnlitShader"/> and copies main texture / tint where possible.
|
||
/// All created instances are appended to <paramref name="createdInstances"/> for later
|
||
/// <c>Destroy</c> (ElsePlayer clone) or for bookkeeping alongside restore (host preview).
|
||
/// </summary>
|
||
public static void ApplyClonedUrpUnlitMaterials(
|
||
GameObject root,
|
||
List<Material> createdInstances,
|
||
System.Action<string> 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<Renderer>(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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Reads common albedo/texture slots from <paramref name="source"/> and reapplies them
|
||
/// on <paramref name="target"/> after switching to URP Unlit.
|
||
/// </summary>
|
||
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
|
||
// ──────────────────────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Creates a Camera child GameObject under <paramref name="parent"/> whose culling mask
|
||
/// covers only <paramref name="portraitLayer"/>. Camera starts disabled.
|
||
/// </summary>
|
||
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<Camera>();
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Creates a 256×256 ARGB32 RenderTexture and wires it to <paramref name="cam"/> if provided.
|
||
/// </summary>
|
||
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;
|
||
}
|
||
}
|
||
}
|