# Portrait Capture — Tóm tắt hệ thống **Cập nhật:** 07/05/2026 **Liên quan plan:** \elseplayer_avatar_hudnpc_ff835033 --- ## Mục tiêu - **HUD NPC (else player):** hiển thị avatar 3D khi chọn player làm target — ổn định khi target di chuyển. - **HUD host player:** hiển thị avatar host trên màn chọn nhân vật — tận dụng \PlayerModelPreview\ + camera gắn xương \Bip01 Head\, nhìn thẳng mặt. --- ## Vấn đề gốc (ElsePlayer) Camera bám trực tiếp \ ransform\ nhân vật thật trong world mỗi frame → khi chạy/nhảy: **giật, lệch, render không ổn**. --- ## Giải pháp ElsePlayer: clone + camera kiểu host Thay vì bám live player: 1. Tìm root model hiển thị (\FindVisualModelRoot\). 2. \Instantiate\ clone, đặt tại vị trí cố định xa gameplay (ví dụ \(9999, 0, 0)\). 3. \CleanupCloneComponents\: bỏ Collider/Rigidbody/MonoBehaviour thừa, **giữ Animator** (idle). 4. Gán layer **Portrait** cho toàn hierarchy clone. 5. Camera được **parent trực tiếp vào xương đầu** của clone (cùng style host), đặt theo `faceAxis + cameraDistance`. 6. Trong `LateUpdate`, chỉ `Slerp` rotation để nhìn về mặt (`faceUpLift`) — không bám `transform` player thật ngoài world. **File:** \Assets/PerfectWorld/Scripts/UI/ElsePlayerPortraitCapture.cs **API (giữ tương thích CECUIManager):** - \ElsePlayerPortraitCapture.Instance?.SetTarget(elsePlayer.transform);- \ElsePlayerPortraitCapture.Instance?.ClearTarget();- RawImage: \ElsePlayerPortraitCapture.Instance?.OutputTexture --- ## Tiện ích dùng chung **File:** \Assets/PerfectWorld/Scripts/UI/PortraitCaptureUtils.cs | Nội dung | Mô tả | |----------|--------| | \FindVisualModelRoot\ | Tìm root có \SkinnedMeshRenderer\ trong hierarchy player | | \FindHeadBone\ / \FindChildByName\ | Tìm xương đầu / tìm con theo tên | | \CleanupCloneComponents\ | Dọn clone, giữ Animator | | \SetLayerRecursive\ | Gán layer cho cả cây GameObject | | \FindUrpUnlitShader\ | Tìm shader `Universal Render Pipeline/Unlit` | | \ApplyClonedUrpUnlitMaterials\ | Clone từng material slot của mọi renderer và đổi clone sang URP/Unlit | | \ConvertMaterialToUrpUnlit\ | Giữ lại texture/màu chính (`_BaseMap`/`_MainTex`, `_BaseColor`/`_Color`, `_Cutoff`) | | \CreatePortraitCamera\ / \CreatePortraitRT\ | Tạo camera (culling theo layer) + RT 256xARGB32 | --- ## Material portrait: clone + URP/Unlit Áp dụng cho cả **ElsePlayerPortraitCapture** và **HostPlayerPortraitCapture**: 1. Không sửa material gốc đang dùng trong gameplay. 2. Clone từng `sharedMaterial` theo từng renderer/slot (`new Material(src)`). 3. Đổi clone sang shader `Universal Render Pipeline/Unlit`. 4. Copy lại texture/màu chính để giữ ngoại hình gần giống model gốc. ### ElsePlayer (model clone) - Chạy `ApplyClonedUrpUnlitMaterials` trực tiếp trên `_portraitClone`. - Khi `ClearTarget()`: destroy toàn bộ material instance đã tạo. ### HostPlayer (model từ `PlayerModelPreview`) - Trước khi đổi material: lưu snapshot `renderer.sharedMaterials` của model preview. - Áp clone URP/Unlit để render portrait. - Khi `ClearPortrait()` / đổi target mới: restore lại `sharedMaterials` gốc + destroy material clone. - Mục tiêu: không để model preview ngoài UI bị “kẹt” shader Unlit sau khi đóng portrait. ### Inspector - `usePortraitUnlitMaterials` (Else/Host): bật để dùng pipeline clone material + URP/Unlit. - Nếu project không tìm thấy `Universal Render Pipeline/Unlit`, hệ thống log warning và giữ shader clone gốc. --- ## Host player: \PlayerModelPreview\ + camera trên \Bip01 Head **File:** \Assets/PerfectWorld/Scripts/UI/HostPlayerPortraitCapture.cs Không clone host từ world; dùng model đã load trong **\PlayerModelPreview\** (đủ trang bị, vị trí preview cố định). ### Camera gắn vào \Bip01 Head Camera được **parent trực tiếp vào xương đầu** thay vì Spine2 — mọi chuyển động đầu từ idle animation được camera theo tự động. **Công thức:** \faceDir = headBone. (cấu hình qua Inspector: Forward/Back/Up/Down) camWorldPos = headBone.position + faceDir * cameraDistance camera.SetParent(Bip01 Head) camera.position = camWorldPos → Unity lưu localOffset đúng \ **LateUpdate:** chỉ Slerp rotation về phía mặt — không ghi đè position: \\csharp Vector3 toFace = (headBone.position + Vector3.up * faceUpLift) - camera.position; targetRot = Quaternion.LookRotation(toFace); camera.rotation = Quaternion.Slerp(camera.rotation, targetRot, dt * lookSmoothSpeed); \ ### Inspector | Field | Ý nghĩa | |-------|---------| | \headFaceAxis\ | Trục nào của \Bip01 Head\ chỉ ra phía mặt (Forward/Back/Up/Down/Left/Right) | | \ lipFaceAxis\ | Lật nhanh nếu chọn sai trục | | \portraitLayer\ | Layer chỉ camera portrait render (vd. 29) | | \cameraDistance\ | Khoảng cách camera–mặt (~0.5–1.0m) | | \ aceUpLift\ | Nhích điểm nhìn lên (tránh nhìn cằm thay vì mắt) | | \lookSmoothSpeed\ | Cao = snappy, thấp = smooth | --- ## Kết nối với \PlayerModelPreview\ — xử lý async load \ShowAllPlayerModels()\ là \sync void\ — model load từng cái một. Nếu gọi \AttachToPlayerModelPreview\ quá sớm thì list còn rỗng. ### Giải pháp: event \OnModelReady\ + call tại \ShowSelectedModelWhenReady **\PlayerModelPreview.cs\**: thêm event báo khi model lần đầu được \SetActive(true)\: \\csharp public event Action OnModelReady; // trong ApplyRequestedModelVisibility(): go.SetActive(shouldBeActive); if (shouldBeActive && !wasActive) OnModelReady?.Invoke(playerModelIds[i]); \ **\SelecScreenCharacter.cs\**: gọi \AttachToPlayerModelPreview\ ngay sau \ShowPlayerModel\: \\csharp PlayerModelPreview.Instance.ShowPlayerModel(roleId); HostPlayerPortraitCapture.Instance?.AttachToPlayerModelPreview(roleId); \ Tại đây coroutine đã poll đảm bảo model load xong → \TryAttachFromPreview\ thành công ngay (Path 1), không cần đợi event. ### Hai path trong \AttachToPlayerModelPreview \AttachToPlayerModelPreview(roleId) ├─ TryAttachFromPreview() → model có rồi → SetupPortrait() ✔ (Path 1) └─ không tìm thấy → _pendingRoleId = roleId → subscribe PlayerModelPreview.OnModelReady (Path 2) → OnModelReady fires → OnPreviewModelReady() → TryAttachFromPreview() ✔ \ --- ## API đầy đủ \\csharp HostPlayerPortraitCapture.Instance?.AttachToPlayerModelPreview(roleId); HostPlayerPortraitCapture.Instance?.Refresh(roleId); // sau reload đồ / model preview HostPlayerPortraitCapture.Instance?.ClearPortrait(); HostPlayerPortraitCapture.Instance?.SetHostPlayer(root); // fallback // RawImage: rawImage.texture = HostPlayerPortraitCapture.Instance?.OutputTexture; \ --- ## Checklist Unity Editor - [ ] Tạo layer **Portrait** (Tags and Layers). - [ ] Main Camera: **bỏ** layer Portrait khỏi Culling Mask. - [ ] Gán \portraitLayer\ khớp trên \ElsePlayerPortraitCapture\ và \HostPlayerPortraitCapture\. - [ ] Scene: có GameObject gắn \ElsePlayerPortraitCapture\ và \HostPlayerPortraitCapture\. - [ ] ElsePlayer: \CECUIManager\ gọi \SetTarget\ / \ClearTarget\ — không đổi API. - [ ] Host: \SelecScreenCharacter\ gọi \AttachToPlayerModelPreview\ sau \ShowPlayerModel\; wire \OutputTexture\ vào RawImage HUD. - [ ] Inspector: chỉnh \headFaceAxis\ cho đúng rig (Forward là mặc định, bật \ lipFaceAxis\ nếu portrait quay ngược). - [ ] Bật `usePortraitUnlitMaterials` nếu muốn portrait dùng material clone URP/Unlit ổn định ánh sáng. --- ## Tại sao không dùng \CECClonePlayer\ cho portrait \CECClonePlayer\ là player đầy đủ (AI, di chuyển, combat, equip async) — quá nặng. Portrait chỉ cần **mesh + skeleton + Animator** (else) hoặc **model preview có sẵn** (host). --- ## So sánh nhanh | | ElsePlayer | Host | |---|------------|------| | Nguồn model | Clone từ world player | \PlayerModelPreview\ theo oleId\ | | Camera | Con của head bone trên clone + Slerp LookAt mỗi frame | Con của \Bip01 Head\ + Slerp LookAt mỗi frame | | \LateUpdate\ | Có (chỉ Slerp rotation) | Có (chỉ Slerp rotation) | | Subscribe async | Không cần | \OnModelReady\ event từ \PlayerModelPreview\ | | Material portrait | Clone material trên clone và đổi URP/Unlit | Clone material trên preview + restore material gốc khi clear |