Merge pull request 'feature/UpdateAvatar' (#412) from feature/UpdateAvatar into develop

Reviewed-on: https://git.pthub.vn/Unity/perfect-world-unity/pulls/412
This commit is contained in:
cuongnv
2026-05-07 04:26:16 +00:00
4 changed files with 212 additions and 1 deletions
@@ -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<Material> _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;
}
@@ -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<Material> _portraitMaterialInstances = new();
private readonly List<RendererSharedMaterialsSnapshot> _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
/// <summary>Fallback: attach directly to any player hierarchy root.</summary>
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);
}
/// <summary>
/// Restores <see cref="PlayerModelPreview"/> renderers to their original shared materials
/// and destroys portrait-only material instances.
/// </summary>
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<Renderer>(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 });
}
}
/// <summary>
/// Parents the portrait camera to "Bip01 Head" and sets its initial position
/// directly in front of the face.
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using UnityEngine;
namespace BrewMonster
@@ -7,6 +8,19 @@ namespace BrewMonster
/// </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"
@@ -97,6 +111,117 @@ namespace BrewMonster
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
// ──────────────────────────────────────────────────────────────────────────────
+1 -1
View File
@@ -14,7 +14,7 @@ TagManager:
- Terrain
- Brush
- Water
-
- Portrait
-
-
-