8.5 KiB
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:
- Tìm root model hiển thị (\FindVisualModelRoot).
- \Instantiate\ clone, đặt tại vị trí cố định xa gameplay (ví dụ (9999, 0, 0)).
- \CleanupCloneComponents: bỏ Collider/Rigidbody/MonoBehaviour thừa, giữ Animator (idle).
- Gán layer Portrait cho toàn hierarchy clone.
- Camera được parent trực tiếp vào xương đầu của clone (cùng style host), đặt theo
faceAxis + cameraDistance. - Trong
LateUpdate, chỉSlerprotation để nhìn về mặt (faceUpLift) — không bámtransformplayer 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:
- Không sửa material gốc đang dùng trong gameplay.
- Clone từng
sharedMaterialtheo từng renderer/slot (new Material(src)). - Đổi clone sang shader
Universal Render Pipeline/Unlit. - 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
ApplyClonedUrpUnlitMaterialstrự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.sharedMaterialscủa model preview. - Áp clone URP/Unlit để render portrait.
- Khi
ClearPortrait()/ đổi target mới: restore lạisharedMaterialsgố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
usePortraitUnlitMaterialsnế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 |