Files
test/Assets/PerfectWorld/Scripts/UI/PortraitCaptureUtils.cs
2026-05-07 11:24:11 +07:00

265 lines
12 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}
}