Files
test/Docs/portrait-capture-summary.md
2026-05-25 10:33:40 +07:00

187 lines
8.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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****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 cameramặt (~0.51.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 |