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

8.5 KiB
Raw Permalink Blame History

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ả ElsePlayerPortraitCaptureHostPlayerPortraitCapture:

  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 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 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