187 lines
8.5 KiB
Markdown
187 lines
8.5 KiB
Markdown
# 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.<FaceAxis> (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<int> 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 |
|