From a34993f53142701d6d43a57417c3566cafbf7cb7 Mon Sep 17 00:00:00 2001 From: CuongNV <> Date: Wed, 6 May 2026 15:13:34 +0700 Subject: [PATCH] Update code, sence bootrap and prefab hub for avatar hostplayer --- Assets/PerfectWorld/Prefab/UI/HUD.prefab | 73 ++-- Assets/PerfectWorld/Prefab/UI/HUDNPC.prefab | 76 +++- Assets/PerfectWorld/Resources/UI/Avatar.meta | 8 + .../UI/Avatar/Avatar_EslePlayer.renderTexture | 39 ++ .../Avatar_EslePlayer.renderTexture.meta | 8 + .../UI/Avatar/Avatar_HostPlayer.renderTexture | 39 ++ .../Avatar_HostPlayer.renderTexture.meta | 8 + Assets/PerfectWorld/Scene/Bootstrap.unity | 4 +- .../Scripts/Players/PlayerModelPreview.cs | 26 +- .../Scripts/UI/ElsePlayerPortraitCapture.cs | 170 +++++++++ .../UI/ElsePlayerPortraitCapture.cs.meta | 2 + Assets/PerfectWorld/Scripts/UI/HUDNPC.cs | 14 + .../Scripts/UI/HostPlayerPortraitCapture.cs | 353 ++++++++++++++++++ .../UI/HostPlayerPortraitCapture.cs.meta | 2 + .../Scripts/UI/PortraitCaptureUtils.cs | 139 +++++++ .../Scripts/UI/PortraitCaptureUtils.cs.meta | 2 + Assets/Scripts/CECUIManager.cs | 23 ++ Assets/Scripts/SelecScreenCharacter.cs | 1 + 18 files changed, 941 insertions(+), 46 deletions(-) create mode 100644 Assets/PerfectWorld/Resources/UI/Avatar.meta create mode 100644 Assets/PerfectWorld/Resources/UI/Avatar/Avatar_EslePlayer.renderTexture create mode 100644 Assets/PerfectWorld/Resources/UI/Avatar/Avatar_EslePlayer.renderTexture.meta create mode 100644 Assets/PerfectWorld/Resources/UI/Avatar/Avatar_HostPlayer.renderTexture create mode 100644 Assets/PerfectWorld/Resources/UI/Avatar/Avatar_HostPlayer.renderTexture.meta create mode 100644 Assets/PerfectWorld/Scripts/UI/ElsePlayerPortraitCapture.cs create mode 100644 Assets/PerfectWorld/Scripts/UI/ElsePlayerPortraitCapture.cs.meta create mode 100644 Assets/PerfectWorld/Scripts/UI/HostPlayerPortraitCapture.cs create mode 100644 Assets/PerfectWorld/Scripts/UI/HostPlayerPortraitCapture.cs.meta create mode 100644 Assets/PerfectWorld/Scripts/UI/PortraitCaptureUtils.cs create mode 100644 Assets/PerfectWorld/Scripts/UI/PortraitCaptureUtils.cs.meta diff --git a/Assets/PerfectWorld/Prefab/UI/HUD.prefab b/Assets/PerfectWorld/Prefab/UI/HUD.prefab index 4e71b6956e..ab7caebd41 100644 --- a/Assets/PerfectWorld/Prefab/UI/HUD.prefab +++ b/Assets/PerfectWorld/Prefab/UI/HUD.prefab @@ -161,8 +161,8 @@ GameObject: m_Component: - component: {fileID: 2127540135059465312} - component: {fileID: 2085804573552353186} - - component: {fileID: 5477947404616405472} - component: {fileID: 2090693574295861813} + - component: {fileID: 2301294577913282359} m_Layer: 5 m_Name: Avatar m_TagString: Untagged @@ -187,7 +187,7 @@ RectTransform: m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5} m_AnchoredPosition: {x: -151.8, y: 7.8005} - m_SizeDelta: {x: 85.3021, y: 124.4} + m_SizeDelta: {x: 128, y: 180} m_Pivot: {x: 0.5, y: 0.5} --- !u!222 &2085804573552353186 CanvasRenderer: @@ -197,36 +197,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 955704001844978227} m_CullTransparentMesh: 1 ---- !u!114 &5477947404616405472 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 955704001844978227} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: - m_Material: {fileID: 0} - m_Color: {r: 1, g: 1, b: 1, a: 1} - m_RaycastTarget: 1 - m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} - m_Maskable: 1 - m_OnCullStateChanged: - m_PersistentCalls: - m_Calls: [] - m_Sprite: {fileID: 21300000, guid: 88530e3bf1ec5ea4d93e2a0d01a5d525, type: 3} - m_Type: 0 - m_PreserveAspect: 0 - m_FillCenter: 1 - m_FillMethod: 4 - m_FillAmount: 1 - m_FillClockwise: 1 - m_FillOrigin: 0 - m_UseSpriteMesh: 0 - m_PixelsPerUnitMultiplier: 1 --- !u!114 &2090693574295861813 MonoBehaviour: m_ObjectHideFlags: 0 @@ -267,10 +237,37 @@ MonoBehaviour: m_SelectedTrigger: Selected m_DisabledTrigger: Disabled m_Interactable: 1 - m_TargetGraphic: {fileID: 5477947404616405472} + m_TargetGraphic: {fileID: 2301294577913282359} m_OnClick: m_PersistentCalls: m_Calls: [] +--- !u!114 &2301294577913282359 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 955704001844978227} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 1344c3c82d62a2a41a3576d8abb8e3ea, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Texture: {fileID: 8400000, guid: bba090692914cf841bc3173b85871a66, type: 2} + m_UVRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 --- !u!1 &974102734944724072 GameObject: m_ObjectHideFlags: 0 @@ -1804,7 +1801,7 @@ RectTransform: m_GameObject: {fileID: 4823752405346273106} m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1.3, y: 1.3, z: 1.3} + m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: - {fileID: 2127540135059465312} @@ -1818,10 +1815,10 @@ RectTransform: - {fileID: 8118804085881780215} m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 1} - m_AnchorMax: {x: 0, y: 1} - m_AnchoredPosition: {x: 275.1, y: -110.3} - m_SizeDelta: {x: 461, y: 133} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: -143, y: -20} + m_SizeDelta: {x: 128, y: 180} m_Pivot: {x: 0.5, y: 0.5} --- !u!222 &3549955056024652669 CanvasRenderer: diff --git a/Assets/PerfectWorld/Prefab/UI/HUDNPC.prefab b/Assets/PerfectWorld/Prefab/UI/HUDNPC.prefab index c7033b2bec..602118c85d 100644 --- a/Assets/PerfectWorld/Prefab/UI/HUDNPC.prefab +++ b/Assets/PerfectWorld/Prefab/UI/HUDNPC.prefab @@ -30,7 +30,8 @@ RectTransform: m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 - m_Children: [] + m_Children: + - {fileID: 1510810868840459915} m_Father: {fileID: 7601428160728630082} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0.5, y: 0.5} @@ -175,6 +176,7 @@ MonoBehaviour: _nameText: {fileID: 8459104239633154731} _statText: {fileID: 7225922753763360209} healthImage: {fileID: 391766345810538963} + _avatarImage: {fileID: 5732841064457103199} --- !u!1 &5230218528339883288 GameObject: m_ObjectHideFlags: 0 @@ -601,6 +603,78 @@ MonoBehaviour: m_hasFontAssetChanged: 0 m_baseMaterial: {fileID: 0} m_maskOffset: {x: 0, y: 0, z: 0, w: 0} +--- !u!1 &8106823484631380573 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1510810868840459915} + - component: {fileID: 8158437551720672858} + - component: {fileID: 5732841064457103199} + m_Layer: 5 + m_Name: AvatarImage + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1510810868840459915 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8106823484631380573} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 8841982213385894347} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 100, y: 100} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &8158437551720672858 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8106823484631380573} + m_CullTransparentMesh: 1 +--- !u!114 &5732841064457103199 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8106823484631380573} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 1344c3c82d62a2a41a3576d8abb8e3ea, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Texture: {fileID: 8400000, guid: 86b263f7c57f2b2408a59d5c3bafcfe1, type: 2} + m_UVRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 --- !u!1 &8340737021475261245 GameObject: m_ObjectHideFlags: 0 diff --git a/Assets/PerfectWorld/Resources/UI/Avatar.meta b/Assets/PerfectWorld/Resources/UI/Avatar.meta new file mode 100644 index 0000000000..7d95b435d7 --- /dev/null +++ b/Assets/PerfectWorld/Resources/UI/Avatar.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a295172ba5abc95449425dcf9c63a0ae +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/PerfectWorld/Resources/UI/Avatar/Avatar_EslePlayer.renderTexture b/Assets/PerfectWorld/Resources/UI/Avatar/Avatar_EslePlayer.renderTexture new file mode 100644 index 0000000000..3a40376be8 --- /dev/null +++ b/Assets/PerfectWorld/Resources/UI/Avatar/Avatar_EslePlayer.renderTexture @@ -0,0 +1,39 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!84 &8400000 +RenderTexture: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Avatar_EslePlayer + m_ImageContentsHash: + serializedVersion: 2 + Hash: 00000000000000000000000000000000 + m_IsAlphaChannelOptional: 0 + serializedVersion: 6 + m_Width: 90 + m_Height: 128 + m_AntiAliasing: 1 + m_MipCount: -1 + m_DepthStencilFormat: 90 + m_ColorFormat: 12 + m_MipMap: 0 + m_GenerateMips: 1 + m_SRGB: 0 + m_UseDynamicScale: 0 + m_UseDynamicScaleExplicit: 0 + m_BindMS: 0 + m_EnableCompatibleFormat: 1 + m_EnableRandomWrite: 0 + m_TextureSettings: + serializedVersion: 2 + m_FilterMode: 0 + m_Aniso: 0 + m_MipBias: 0 + m_WrapU: 1 + m_WrapV: 1 + m_WrapW: 1 + m_Dimension: 2 + m_VolumeDepth: 1 + m_ShadowSamplingMode: 2 diff --git a/Assets/PerfectWorld/Resources/UI/Avatar/Avatar_EslePlayer.renderTexture.meta b/Assets/PerfectWorld/Resources/UI/Avatar/Avatar_EslePlayer.renderTexture.meta new file mode 100644 index 0000000000..278d424a22 --- /dev/null +++ b/Assets/PerfectWorld/Resources/UI/Avatar/Avatar_EslePlayer.renderTexture.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 86b263f7c57f2b2408a59d5c3bafcfe1 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 8400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/PerfectWorld/Resources/UI/Avatar/Avatar_HostPlayer.renderTexture b/Assets/PerfectWorld/Resources/UI/Avatar/Avatar_HostPlayer.renderTexture new file mode 100644 index 0000000000..f000cdd4a5 --- /dev/null +++ b/Assets/PerfectWorld/Resources/UI/Avatar/Avatar_HostPlayer.renderTexture @@ -0,0 +1,39 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!84 &8400000 +RenderTexture: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Avatar_HostPlayer + m_ImageContentsHash: + serializedVersion: 2 + Hash: 00000000000000000000000000000000 + m_IsAlphaChannelOptional: 0 + serializedVersion: 6 + m_Width: 90 + m_Height: 128 + m_AntiAliasing: 1 + m_MipCount: -1 + m_DepthStencilFormat: 90 + m_ColorFormat: 12 + m_MipMap: 0 + m_GenerateMips: 1 + m_SRGB: 0 + m_UseDynamicScale: 0 + m_UseDynamicScaleExplicit: 0 + m_BindMS: 0 + m_EnableCompatibleFormat: 1 + m_EnableRandomWrite: 0 + m_TextureSettings: + serializedVersion: 2 + m_FilterMode: 0 + m_Aniso: 0 + m_MipBias: 0 + m_WrapU: 1 + m_WrapV: 1 + m_WrapW: 1 + m_Dimension: 2 + m_VolumeDepth: 1 + m_ShadowSamplingMode: 2 diff --git a/Assets/PerfectWorld/Resources/UI/Avatar/Avatar_HostPlayer.renderTexture.meta b/Assets/PerfectWorld/Resources/UI/Avatar/Avatar_HostPlayer.renderTexture.meta new file mode 100644 index 0000000000..a01a0bc8d5 --- /dev/null +++ b/Assets/PerfectWorld/Resources/UI/Avatar/Avatar_HostPlayer.renderTexture.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: bba090692914cf841bc3173b85871a66 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 8400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/PerfectWorld/Scene/Bootstrap.unity b/Assets/PerfectWorld/Scene/Bootstrap.unity index 73241b4ef4..68a55c0caa 100644 --- a/Assets/PerfectWorld/Scene/Bootstrap.unity +++ b/Assets/PerfectWorld/Scene/Bootstrap.unity @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f809da92f7a74929fe4911b38ecddcf5bfe0fa9f667ad04f6c927511246975ae -size 308267 +oid sha256:4691f772549e55bf22d3c3f78dce21577677c6d7d730c6a0ec9fc71f950669c4 +size 318584 diff --git a/Assets/PerfectWorld/Scripts/Players/PlayerModelPreview.cs b/Assets/PerfectWorld/Scripts/Players/PlayerModelPreview.cs index d5dfdfc43b..9de5e5ace7 100644 --- a/Assets/PerfectWorld/Scripts/Players/PlayerModelPreview.cs +++ b/Assets/PerfectWorld/Scripts/Players/PlayerModelPreview.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using BrewMonster.Scripts.Managers; using CSNetwork.Protocols.RPCData; @@ -13,6 +14,13 @@ namespace BrewMonster.Scripts public List playerModels = new(); public List playerModelIds = new(); + /// + /// Fired (with the roleId) the first time a model becomes active after loading completes. + /// Subscribe from HostPlayerPortraitCapture so it can attach the portrait camera at the + /// right moment even when the async load finishes after AttachToPlayerModelPreview is called. + /// + public event Action OnModelReady; + private int _loadVersion; private bool _hasRequestedPlayerModelId; @@ -91,20 +99,28 @@ namespace BrewMonster.Scripts /// /// Applies the visibility of player models based on the requested player model id. + /// Fires the first time a model transitions to active so that + /// portrait capture systems can attach at the correct moment after async loading. /// private void ApplyRequestedModelVisibility() { - if(!_hasRequestedPlayerModelId) + if (!_hasRequestedPlayerModelId) return; int n = Mathf.Min(playerModels.Count, playerModelIds.Count); for (int i = 0; i < n; i++) { GameObject go = playerModels[i]; - if(go == null) + if (go == null) continue; - go.SetActive(playerModelIds[i] == _requestedPlayerModelId); + bool shouldBeActive = playerModelIds[i] == _requestedPlayerModelId; + bool wasActive = go.activeSelf; + go.SetActive(shouldBeActive); + + // Fire once when this model first becomes visible + if (shouldBeActive && !wasActive) + OnModelReady?.Invoke(playerModelIds[i]); } } @@ -162,7 +178,7 @@ namespace BrewMonster.Scripts for(int i = 0; i < role.equipment.Count; i++) { equipment = role.equipment[i]; - + var equipData = elemendataman.get_data_ptr((uint)equipment.id, ID_SPACE.ID_SPACE_ESSENCE, ref DataType); switch (DataType) @@ -278,7 +294,7 @@ namespace BrewMonster.Scripts default: break; - + switch (equipment.pos) { diff --git a/Assets/PerfectWorld/Scripts/UI/ElsePlayerPortraitCapture.cs b/Assets/PerfectWorld/Scripts/UI/ElsePlayerPortraitCapture.cs new file mode 100644 index 0000000000..aacdbce114 --- /dev/null +++ b/Assets/PerfectWorld/Scripts/UI/ElsePlayerPortraitCapture.cs @@ -0,0 +1,170 @@ +using BrewMonster.Scripts.Managers; +using UnityEngine; + +namespace BrewMonster +{ + /// + /// Renders an EC_ElsePlayer portrait into a RenderTexture for the target HUD (HUDNPC). + /// + /// Instead of following the live player transform every frame (which causes jitter when + /// the target moves/jumps), this system clones the visual model into an isolated + /// "portrait stage" at a fixed world position. The portrait camera only ever looks at + /// the static clone, so the output is perfectly stable. + /// + /// Public API (unchanged — CECUIManager needs no edits): + /// SetTarget(Transform playerRoot) — clone model, start rendering + /// ClearTarget() — destroy clone, stop rendering + /// OutputTexture — wire to RawImage.texture + /// + public class ElsePlayerPortraitCapture : MonoSingleton + { + [Header("Portrait Camera")] + [SerializeField] private Camera portraitCamera; + + [Header("Render Output")] + [SerializeField] private RenderTexture outputTexture; + + [Header("Portrait Stage")] + [Tooltip("Layer index for the Portrait layer (Project Settings > Tags and Layers).")] + [SerializeField] private int portraitLayer = 29; + + [Tooltip("Fixed world position of the clone. Keep far from gameplay to avoid overlap.")] + [SerializeField] private Vector3 stagePosition = new Vector3(9999f, 0f, 0f); + + [Header("Framing")] + [Tooltip("Distance from head focus point to camera.")] + [SerializeField] private float cameraDistance = 1.2f; + + [Tooltip("Height offset above stage origin used when head bone cannot be found.")] + [SerializeField] private float fallbackHeadHeight = 1.6f; + + [Tooltip("Slight horizontal angle so the portrait isn't perfectly front-on (degrees).")] + [SerializeField][Range(0f, 45f)] private float horizontalAngleDeg = 15f; + + [SerializeField][Range(10f, 60f)] private float fieldOfView = 35f; + + // ── runtime state ──────────────────────────────────────────────────────────── + private GameObject _portraitClone; + private Transform _headBone; + + public RenderTexture OutputTexture => outputTexture; + + // ────────────────────────────────────────────────────────────────────────────── + // Lifecycle + // ────────────────────────────────────────────────────────────────────────────── + + protected override void Initialize() + { + EnsureCamera(); + EnsureRenderTexture(); + } + + protected override void OnDestroy() + { + base.OnDestroy(); + ClearTarget(); + if (outputTexture != null && outputTexture.IsCreated()) + outputTexture.Release(); + } + + // ────────────────────────────────────────────────────────────────────────────── + // Public API + // ────────────────────────────────────────────────────────────────────────────── + + /// + /// Clones the visual model of into the portrait stage + /// and starts rendering. Safe to call multiple times — old clone is destroyed first. + /// + public void SetTarget(Transform playerRoot) + { + ClearTarget(); + if (playerRoot == null) return; + + Transform modelRoot = PortraitCaptureUtils.FindVisualModelRoot(playerRoot); + if (modelRoot == null) + { + BMLogger.LogWarning("[ElsePlayerPortraitCapture] Visual model root not found — portrait skipped."); + return; + } + + _portraitClone = Object.Instantiate(modelRoot.gameObject); + _portraitClone.name = "[Portrait] ElsePlayer"; + _portraitClone.transform.position = stagePosition; + _portraitClone.transform.rotation = Quaternion.identity; + + // Strip physics / game-logic components; keep Animator for idle animation + PortraitCaptureUtils.CleanupCloneComponents(_portraitClone); + + // Hide from main camera — portrait camera sees only this layer + PortraitCaptureUtils.SetLayerRecursive(_portraitClone, portraitLayer); + + _headBone = PortraitCaptureUtils.FindHeadBone(_portraitClone.transform); + + PositionCamera(); + SetCameraEnabled(true); + } + + /// + /// Destroys the portrait clone and disables the camera. + /// Call from OnTargetHUDClear / TryHideUINPC. + /// + public void ClearTarget() + { + if (_portraitClone != null) + { + Object.Destroy(_portraitClone); + _portraitClone = null; + } + _headBone = null; + SetCameraEnabled(false); + } + + // ────────────────────────────────────────────────────────────────────────────── + // Internal + // ────────────────────────────────────────────────────────────────────────────── + + private void PositionCamera() + { + if (portraitCamera == null) return; + + Vector3 focusPoint = _headBone != null + ? _headBone.position + : stagePosition + Vector3.up * fallbackHeadHeight; + + float rad = horizontalAngleDeg * Mathf.Deg2Rad; + Vector3 offset = new Vector3(Mathf.Sin(rad) * cameraDistance, 0f, -cameraDistance); + + portraitCamera.transform.position = focusPoint + offset; + portraitCamera.transform.LookAt(focusPoint); + } + + private void SetCameraEnabled(bool enabled) + { + if (portraitCamera != null) + portraitCamera.enabled = enabled; + } + + private void EnsureCamera() + { + if (portraitCamera != null) + { + portraitCamera.cullingMask = 1 << portraitLayer; + portraitCamera.fieldOfView = fieldOfView; + portraitCamera.enabled = false; + return; + } + portraitCamera = PortraitCaptureUtils.CreatePortraitCamera( + transform, "PortraitCamera_ElsePlayer", portraitLayer, fieldOfView); + } + + private void EnsureRenderTexture() + { + if (outputTexture != null) + { + if (portraitCamera != null) portraitCamera.targetTexture = outputTexture; + return; + } + outputTexture = PortraitCaptureUtils.CreatePortraitRT("ElsePlayerPortraitRT", portraitCamera); + } + } +} diff --git a/Assets/PerfectWorld/Scripts/UI/ElsePlayerPortraitCapture.cs.meta b/Assets/PerfectWorld/Scripts/UI/ElsePlayerPortraitCapture.cs.meta new file mode 100644 index 0000000000..47c1ab27a7 --- /dev/null +++ b/Assets/PerfectWorld/Scripts/UI/ElsePlayerPortraitCapture.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: aa7ed0ae48461f645932270bfda7ec70 \ No newline at end of file diff --git a/Assets/PerfectWorld/Scripts/UI/HUDNPC.cs b/Assets/PerfectWorld/Scripts/UI/HUDNPC.cs index a28e363118..4e0d68dc8e 100644 --- a/Assets/PerfectWorld/Scripts/UI/HUDNPC.cs +++ b/Assets/PerfectWorld/Scripts/UI/HUDNPC.cs @@ -13,6 +13,7 @@ namespace BrewMonster [SerializeField] private TextMeshProUGUI _nameText; [SerializeField] private TextMeshProUGUI _statText; [SerializeField] private Image healthImage; + [SerializeField] private RawImage _avatarImage; private void OnEnable() { @@ -53,5 +54,18 @@ namespace BrewMonster { healthImage.fillAmount = health; } + + public void SetAvatar(Texture tex) + { + if (_avatarImage == null) return; + _avatarImage.texture = tex; + _avatarImage.gameObject.SetActive(tex != null); + } + + public void ShowAvatar(bool show) + { + if (_avatarImage != null) + _avatarImage.gameObject.SetActive(show); + } } } diff --git a/Assets/PerfectWorld/Scripts/UI/HostPlayerPortraitCapture.cs b/Assets/PerfectWorld/Scripts/UI/HostPlayerPortraitCapture.cs new file mode 100644 index 0000000000..09e737c6b0 --- /dev/null +++ b/Assets/PerfectWorld/Scripts/UI/HostPlayerPortraitCapture.cs @@ -0,0 +1,353 @@ +using BrewMonster.Scripts; +using BrewMonster.Scripts.Managers; +using UnityEngine; + +namespace BrewMonster +{ + /// + /// Renders the host player's portrait into a RenderTexture for the HUD. + /// + /// Integrates with : + /// • If the model is already loaded when AttachToPlayerModelPreview is called → attach immediately. + /// • If the model is still loading (async) → subscribe to PlayerModelPreview.OnModelReady and + /// attach automatically as soon as the model becomes active. + /// + /// Camera is parented directly to the "Bip01 Head" bone so it rides every head + /// movement from idle animation without any world-space tracking code. + /// Every LateUpdate the camera rotation is Slerp-interpolated toward the face. + /// + /// Public API: + /// AttachToPlayerModelPreview(int roleId) — primary; handles async loading + /// SetHostPlayer(Transform root) — fallback; any player root directly + /// Refresh(int roleId) — call after equipment reload + /// ClearPortrait() — detach camera, stop rendering + /// OutputTexture — wire to RawImage.texture on HUD + /// + public class HostPlayerPortraitCapture : MonoSingleton + { + // ────────────────────────────────────────────────────────────────────────────── + // Inspector + // ────────────────────────────────────────────────────────────────────────────── + + [Header("Portrait Camera")] + [SerializeField] private Camera portraitCamera; + + [Header("Render Output")] + [SerializeField] private RenderTexture outputTexture; + + [Header("Portrait Layer")] + [Tooltip("Layer index rendered ONLY by the portrait camera (Project Settings > Tags and Layers).")] + [SerializeField] private int portraitLayer = 29; + + [Header("Head Bone Face Direction")] + [Tooltip("Which local axis of Bip01 Head points OUT of the face (toward the camera).\n" + + "In most Biped rigs exported from 3ds Max/Unity: Forward (+Z).\n" + + "Switch to Back (-Z) or Up (+Y) if portrait faces wrong way.")] + [SerializeField] private FaceAxis headFaceAxis = FaceAxis.Forward; + + [Tooltip("Flip the chosen face axis (e.g. if Forward gives back-of-head, enable this).")] + [SerializeField] private bool flipFaceAxis; + + [Header("Framing")] + [Tooltip("Distance the camera sits in front of the face (meters). ~0.5–1.0 for tight portrait.")] + [SerializeField] private float cameraDistance = 0.7f; + + [Tooltip("Extra upward offset applied to the look-at focus point (moves portrait up toward eyes).")] + [SerializeField] private float faceUpLift = 0.04f; + + [Tooltip("How fast the camera rotation interpolates toward the face each frame (higher = snappier).")] + [SerializeField][Range(1f, 50f)] private float lookSmoothSpeed = 20f; + + [SerializeField][Range(10f, 60f)] private float fieldOfView = 40f; + + // ────────────────────────────────────────────────────────────────────────────── + // Runtime state + // ────────────────────────────────────────────────────────────────────────────── + + private Transform _modelTransform; + private Transform _headBone; + + // Stored while waiting for async model load; -1 means no pending request + private int _pendingRoleId = -1; + + public RenderTexture OutputTexture => outputTexture; + + // ────────────────────────────────────────────────────────────────────────────── + // Lifecycle + // ────────────────────────────────────────────────────────────────────────────── + + protected override void Initialize() + { + EnsureCamera(); + EnsureRenderTexture(); + } + + protected override void OnDestroy() + { + base.OnDestroy(); + UnsubscribeModelReady(); + ClearPortrait(); + if (outputTexture != null && outputTexture.IsCreated()) + outputTexture.Release(); + } + + // ────────────────────────────────────────────────────────────────────────────── + // Public API + // ────────────────────────────────────────────────────────────────────────────── + + /// + /// Attaches the portrait camera to the model matching inside + /// . + /// + /// Two paths: + /// 1. Model already loaded and active → attach immediately. + /// 2. Model still loading (async) → subscribe to + /// and attach automatically when the event fires for this roleId. + /// + public void AttachToPlayerModelPreview(int roleId) + { + UnsubscribeModelReady(); + DetachCamera(); + + if (PlayerModelPreview.Instance == null) + { + BMLogger.LogWarning("[HostPlayerPortraitCapture] PlayerModelPreview.Instance is null."); + return; + } + + // Path 1: model already in the list and active → attach now + if (TryAttachFromPreview(roleId)) + return; + + // Path 2: model not ready yet — wait for OnModelReady event + _pendingRoleId = roleId; + PlayerModelPreview.Instance.OnModelReady += OnPreviewModelReady; + BMLogger.Log($"[HostPlayerPortraitCapture] Model for roleId={roleId} not ready, waiting for load..."); + } + + /// Fallback: attach directly to any player hierarchy root. + public void SetHostPlayer(Transform hostPlayerRoot) + { + DetachCamera(); + if (hostPlayerRoot == null) return; + _modelTransform = hostPlayerRoot; + SetupPortrait(); + } + + /// Re-attach after equipment reload rebuilds the PlayerModelPreview model. + public void Refresh(int roleId) => AttachToPlayerModelPreview(roleId); + + /// Detach camera and stop rendering. Call on logout / scene unload. + public void ClearPortrait() + { + UnsubscribeModelReady(); + DetachCamera(); + _modelTransform = null; + _headBone = null; + _pendingRoleId = -1; + } + + // ────────────────────────────────────────────────────────────────────────────── + // LateUpdate — smooth Slerp toward face every frame + // ────────────────────────────────────────────────────────────────────────────── + + private void LateUpdate() + { + if (portraitCamera == null || !portraitCamera.enabled) return; + if (_headBone == null) return; + + // Focus point: centre of face, nudged upward by faceUpLift + Vector3 faceCenter = _headBone.position + Vector3.up * faceUpLift; + + // Direction from camera to face centre + Vector3 toFace = faceCenter - portraitCamera.transform.position; + if (toFace.sqrMagnitude < 0.0001f) return; + + // Slerp rotation smoothly toward the look-at orientation + Quaternion targetRot = Quaternion.LookRotation(toFace); + portraitCamera.transform.rotation = Quaternion.Slerp( + portraitCamera.transform.rotation, + targetRot, + Time.deltaTime * lookSmoothSpeed); + } + + // ────────────────────────────────────────────────────────────────────────────── + // PlayerModelPreview integration helpers + // ────────────────────────────────────────────────────────────────────────────── + + /// + /// Called by when a model becomes active. + /// Attaches only if the roleId matches the pending request. + /// + private void OnPreviewModelReady(int readyRoleId) + { + if (readyRoleId != _pendingRoleId) return; + + UnsubscribeModelReady(); + TryAttachFromPreview(readyRoleId); + } + + /// + /// Tries to find and attach to the model for in PlayerModelPreview. + /// Returns true if the model was found (active or not) and attachment succeeded. + /// + private bool TryAttachFromPreview(int roleId) + { + var preview = PlayerModelPreview.Instance; + if (preview == null) return false; + + var models = preview.playerModels; + var ids = preview.playerModelIds; + + int count = Mathf.Min(models.Count, ids.Count); + for (int i = 0; i < count; i++) + { + if (ids[i] != roleId || models[i] == null) continue; + + // Ensure the model is active so the camera can render it + if (!models[i].activeSelf) + models[i].SetActive(true); + + _modelTransform = models[i].transform; + SetupPortrait(); + return true; + } + + return false; + } + + private void UnsubscribeModelReady() + { + if (PlayerModelPreview.Instance != null) + PlayerModelPreview.Instance.OnModelReady -= OnPreviewModelReady; + _pendingRoleId = -1; + } + + // ────────────────────────────────────────────────────────────────────────────── + // Internal + // ────────────────────────────────────────────────────────────────────────────── + + private void SetupPortrait() + { + if (_modelTransform == null) return; + + // Move model to portrait layer so the dedicated camera can render it + PortraitCaptureUtils.SetLayerRecursive(_modelTransform.gameObject, portraitLayer); + + // Find head bone — primary target for camera attachment + _headBone = PortraitCaptureUtils.FindChildByName(_modelTransform, "Bip01 Head") + ?? PortraitCaptureUtils.FindChildByName(_modelTransform, "Bip001 Head") + ?? PortraitCaptureUtils.FindHeadBone(_modelTransform); + + if (_headBone == null) + BMLogger.LogWarning("[HostPlayerPortraitCapture] Head bone not found — portrait skipped."); + + AttachCameraToHeadBone(); + SetCameraEnabled(true); + } + + /// + /// Parents the portrait camera to "Bip01 Head" and sets its initial position + /// directly in front of the face. + /// + /// Coordinate math: + /// faceDir = local axis of headBone that points OUT of the face (Inspector: headFaceAxis) + /// camWorldPos = headBone.position + faceDir * cameraDistance + /// + /// After SetParent, Unity converts this world position into a localPosition relative + /// to the head bone automatically. From that point on the camera rides the bone — + /// no per-frame world-space override is needed. Only LookAt is refreshed in LateUpdate. + /// + private void AttachCameraToHeadBone() + { + if (portraitCamera == null) return; + + Transform anchor = _headBone ?? _modelTransform; + + // Face direction in world space — derived from head bone's own local axes + Vector3 faceDir = GetFaceDirectionWorld(anchor); + + Vector3 headPos = anchor.position; + Vector3 faceCenter = headPos + Vector3.up * faceUpLift; + + // Camera sits cameraDistance units in FRONT of the face (outside, looking back in) + Vector3 camWorldPos = headPos + faceDir * cameraDistance; + + // Parent to head bone first with worldPositionStays:false (resets local to zero), + // then assign world position — Unity stores the correct localPosition internally + portraitCamera.transform.SetParent(anchor, worldPositionStays: false); + portraitCamera.transform.position = camWorldPos; + + // Initial look-at so first frame is already correct before LateUpdate fires + portraitCamera.transform.LookAt(faceCenter); + } + + /// + /// Returns the world-space direction that points OUT OF the character's face, + /// derived from the head bone's local coordinate axes according to . + /// + /// Biped rig convention (3ds Max → Unity export): + /// In most Perfect World rigs, the head bone's +Z (forward) points toward the face. + /// If the portrait comes out upside-down or backwards, toggle + /// and/or in the Inspector without changing code. + /// + private Vector3 GetFaceDirectionWorld(Transform bone) + { + Vector3 dir = headFaceAxis switch + { + FaceAxis.Forward => bone.forward, + FaceAxis.Back => -bone.forward, + FaceAxis.Up => bone.up, + FaceAxis.Down => -bone.up, + FaceAxis.Right => bone.right, + FaceAxis.Left => -bone.right, + _ => bone.forward + }; + return flipFaceAxis ? -dir : dir; + } + + private void DetachCamera() + { + if (portraitCamera != null) + { + portraitCamera.transform.SetParent(transform, worldPositionStays: false); + SetCameraEnabled(false); + } + } + + private void SetCameraEnabled(bool enabled) + { + if (portraitCamera != null) + portraitCamera.enabled = enabled; + } + + private void EnsureCamera() + { + if (portraitCamera != null) + { + portraitCamera.cullingMask = 1 << portraitLayer; + portraitCamera.fieldOfView = fieldOfView; + portraitCamera.enabled = false; + return; + } + portraitCamera = PortraitCaptureUtils.CreatePortraitCamera( + transform, "PortraitCamera_HostPlayer", portraitLayer, fieldOfView); + } + + private void EnsureRenderTexture() + { + if (outputTexture != null) + { + if (portraitCamera != null) portraitCamera.targetTexture = outputTexture; + return; + } + outputTexture = PortraitCaptureUtils.CreatePortraitRT("HostPlayerPortraitRT", portraitCamera); + } + + // ────────────────────────────────────────────────────────────────────────────── + // Helpers + // ────────────────────────────────────────────────────────────────────────────── + + public enum FaceAxis { Forward, Back, Up, Down, Right, Left } + } +} diff --git a/Assets/PerfectWorld/Scripts/UI/HostPlayerPortraitCapture.cs.meta b/Assets/PerfectWorld/Scripts/UI/HostPlayerPortraitCapture.cs.meta new file mode 100644 index 0000000000..5538b0d5d7 --- /dev/null +++ b/Assets/PerfectWorld/Scripts/UI/HostPlayerPortraitCapture.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 93accee0d329e0f45a7be8b74cf0cb99 \ No newline at end of file diff --git a/Assets/PerfectWorld/Scripts/UI/PortraitCaptureUtils.cs b/Assets/PerfectWorld/Scripts/UI/PortraitCaptureUtils.cs new file mode 100644 index 0000000000..7e5c4195cd --- /dev/null +++ b/Assets/PerfectWorld/Scripts/UI/PortraitCaptureUtils.cs @@ -0,0 +1,139 @@ +using UnityEngine; + +namespace BrewMonster +{ + /// + /// Shared static helpers used by ElsePlayerPortraitCapture and HostPlayerPortraitCapture. + /// + public static class PortraitCaptureUtils + { + 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); + } + + // ────────────────────────────────────────────────────────────────────────────── + // 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; + } + } +} diff --git a/Assets/PerfectWorld/Scripts/UI/PortraitCaptureUtils.cs.meta b/Assets/PerfectWorld/Scripts/UI/PortraitCaptureUtils.cs.meta new file mode 100644 index 0000000000..c09b8c6e07 --- /dev/null +++ b/Assets/PerfectWorld/Scripts/UI/PortraitCaptureUtils.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5d2d46da24e62da45a4abea222294fd2 \ No newline at end of file diff --git a/Assets/Scripts/CECUIManager.cs b/Assets/Scripts/CECUIManager.cs index 31d407656b..259bf6ca83 100644 --- a/Assets/Scripts/CECUIManager.cs +++ b/Assets/Scripts/CECUIManager.cs @@ -1,4 +1,5 @@ using BrewMonster; +using BrewMonster.Managers; using BrewMonster.Network; using BrewMonster.UI; using System; @@ -8,6 +9,7 @@ using BrewMonster.Scripts.Chat.EmotionData; using BrewMonster.Scripts.Managers; using BrewMonster.Scripts.Task.UI; using BrewMonster.Scripts.UI; +using CSNetwork.GPDataType; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; @@ -172,10 +174,29 @@ public class CECUIManager : MonoSingleton float fill = obj.MaxHealth > 0 ? (float)obj.CurrentHealth / (float)obj.MaxHealth : 1f; npsUI.SetHealthImage(fill); currentTargetNPCID = obj.IDNPC; + + // Render ElsePlayer portrait avatar — mirrors C++ PlayerRenderPortrait / SetRenderCallback + if (GPDataTypeHelper.ISPLAYERID(obj.IDNPC)) + { + var elsePlayer = EC_ManMessageMono.Instance?.GetECManPlayer? + .GetPlayer(obj.IDNPC) as EC_ElsePlayer; + if (elsePlayer != null) + { + ElsePlayerPortraitCapture.Instance?.SetTarget(elsePlayer.transform); + npsUI.SetAvatar(ElsePlayerPortraitCapture.Instance?.OutputTexture); + return; + } + } + + // NPC thường hoặc không tìm thấy ElsePlayer — ẩn avatar + ElsePlayerPortraitCapture.Instance?.ClearTarget(); + npsUI.ShowAvatar(false); } private void OnTargetHUDClear(CECHostPlayer.TargetHUDClearEvent _) { + ElsePlayerPortraitCapture.Instance?.ClearTarget(); + npsUI.ShowAvatar(false); npsUI.gameObject.SetActive(false); } @@ -187,6 +208,8 @@ public class CECUIManager : MonoSingleton private void TryHideUINPC(NPCDiedEvent obj) { if (obj.NPCID != currentTargetNPCID) return; + ElsePlayerPortraitCapture.Instance?.ClearTarget(); + npsUI.ShowAvatar(false); npsUI.gameObject.SetActive(false); } diff --git a/Assets/Scripts/SelecScreenCharacter.cs b/Assets/Scripts/SelecScreenCharacter.cs index 249bb89897..150cdf127e 100644 --- a/Assets/Scripts/SelecScreenCharacter.cs +++ b/Assets/Scripts/SelecScreenCharacter.cs @@ -179,6 +179,7 @@ namespace BrewMonster.UI if (PlayerModelPreview.Instance.playerModelIds != null && PlayerModelPreview.Instance.playerModelIds.Contains(roleId)) { PlayerModelPreview.Instance.ShowPlayerModel(roleId); + HostPlayerPortraitCapture.Instance?.AttachToPlayerModelPreview(roleId); _btnEnterGame.interactable = true; _showModelReadyCoroutine = null; yield break; // Model is ready, show it and stop this coroutine