using BrewMonster.Scripts;
using BrewMonster.Scripts.Managers;
using UnityEngine;
namespace BrewMonster
{
///
/// Renders the host player's portrait into a RenderTexture for the HUD.
///
/// Integrates with :
/// • If the model is already loaded when AttachToPlayerModelPreview is called → attach immediately.
/// • If the model is still loading (async) → subscribe to PlayerModelPreview.OnModelReady and
/// attach automatically as soon as the model becomes active.
///
/// Camera is parented directly to the "Bip01 Head" bone so it rides every head
/// movement from idle animation without any world-space tracking code.
/// Every LateUpdate the camera rotation is Slerp-interpolated toward the face.
///
/// Public API:
/// AttachToPlayerModelPreview(int roleId) — primary; handles async loading
/// SetHostPlayer(Transform root) — fallback; any player root directly
/// Refresh(int roleId) — call after equipment reload
/// ClearPortrait() — detach camera, stop rendering
/// OutputTexture — wire to RawImage.texture on HUD
///
public class HostPlayerPortraitCapture : MonoSingleton
{
// ──────────────────────────────────────────────────────────────────────────────
// Inspector
// ──────────────────────────────────────────────────────────────────────────────
[Header("Portrait Camera")]
[SerializeField] private Camera portraitCamera;
[Header("Render Output")]
[SerializeField] private RenderTexture outputTexture;
[Header("Portrait Layer")]
[Tooltip("Layer index rendered ONLY by the portrait camera (Project Settings > Tags and Layers).")]
[SerializeField] private int portraitLayer = 29;
[Header("Head Bone Face Direction")]
[Tooltip("Which local axis of Bip01 Head points OUT of the face (toward the camera).\n" +
"In most Biped rigs exported from 3ds Max/Unity: Forward (+Z).\n" +
"Switch to Back (-Z) or Up (+Y) if portrait faces wrong way.")]
[SerializeField] private FaceAxis headFaceAxis = FaceAxis.Forward;
[Tooltip("Flip the chosen face axis (e.g. if Forward gives back-of-head, enable this).")]
[SerializeField] private bool flipFaceAxis;
[Header("Framing")]
[Tooltip("Distance the camera sits in front of the face (meters). ~0.5–1.0 for tight portrait.")]
[SerializeField] private float cameraDistance = 0.7f;
[Tooltip("Extra upward offset applied to the look-at focus point (moves portrait up toward eyes).")]
[SerializeField] private float faceUpLift = 0.04f;
[Tooltip("How fast the camera rotation interpolates toward the face each frame (higher = snappier).")]
[SerializeField][Range(1f, 50f)] private float lookSmoothSpeed = 20f;
[SerializeField][Range(10f, 60f)] private float fieldOfView = 40f;
// ──────────────────────────────────────────────────────────────────────────────
// Runtime state
// ──────────────────────────────────────────────────────────────────────────────
private Transform _modelTransform;
private Transform _headBone;
// Stored while waiting for async model load; -1 means no pending request
private int _pendingRoleId = -1;
public RenderTexture OutputTexture => outputTexture;
// ──────────────────────────────────────────────────────────────────────────────
// Lifecycle
// ──────────────────────────────────────────────────────────────────────────────
protected override void Initialize()
{
EnsureCamera();
EnsureRenderTexture();
}
protected override void OnDestroy()
{
base.OnDestroy();
UnsubscribeModelReady();
ClearPortrait();
if (outputTexture != null && outputTexture.IsCreated())
outputTexture.Release();
}
// ──────────────────────────────────────────────────────────────────────────────
// Public API
// ──────────────────────────────────────────────────────────────────────────────
///
/// Attaches the portrait camera to the model matching inside
/// .
///
/// Two paths:
/// 1. Model already loaded and active → attach immediately.
/// 2. Model still loading (async) → subscribe to
/// and attach automatically when the event fires for this roleId.
///
public void AttachToPlayerModelPreview(int roleId)
{
UnsubscribeModelReady();
DetachCamera();
if (PlayerModelPreview.Instance == null)
{
BMLogger.LogWarning("[HostPlayerPortraitCapture] PlayerModelPreview.Instance is null.");
return;
}
// Path 1: model already in the list and active → attach now
if (TryAttachFromPreview(roleId))
return;
// Path 2: model not ready yet — wait for OnModelReady event
_pendingRoleId = roleId;
PlayerModelPreview.Instance.OnModelReady += OnPreviewModelReady;
BMLogger.Log($"[HostPlayerPortraitCapture] Model for roleId={roleId} not ready, waiting for load...");
}
/// Fallback: attach directly to any player hierarchy root.
public void SetHostPlayer(Transform hostPlayerRoot)
{
DetachCamera();
if (hostPlayerRoot == null) return;
_modelTransform = hostPlayerRoot;
SetupPortrait();
}
/// Re-attach after equipment reload rebuilds the PlayerModelPreview model.
public void Refresh(int roleId) => AttachToPlayerModelPreview(roleId);
/// Detach camera and stop rendering. Call on logout / scene unload.
public void ClearPortrait()
{
UnsubscribeModelReady();
DetachCamera();
_modelTransform = null;
_headBone = null;
_pendingRoleId = -1;
}
// ──────────────────────────────────────────────────────────────────────────────
// LateUpdate — smooth Slerp toward face every frame
// ──────────────────────────────────────────────────────────────────────────────
private void LateUpdate()
{
if (portraitCamera == null || !portraitCamera.enabled) return;
if (_headBone == null) return;
// Focus point: centre of face, nudged upward by faceUpLift
Vector3 faceCenter = _headBone.position + Vector3.up * faceUpLift;
// Direction from camera to face centre
Vector3 toFace = faceCenter - portraitCamera.transform.position;
if (toFace.sqrMagnitude < 0.0001f) return;
// Slerp rotation smoothly toward the look-at orientation
Quaternion targetRot = Quaternion.LookRotation(toFace);
portraitCamera.transform.rotation = Quaternion.Slerp(
portraitCamera.transform.rotation,
targetRot,
Time.deltaTime * lookSmoothSpeed);
}
// ──────────────────────────────────────────────────────────────────────────────
// PlayerModelPreview integration helpers
// ──────────────────────────────────────────────────────────────────────────────
///
/// Called by when a model becomes active.
/// Attaches only if the roleId matches the pending request.
///
private void OnPreviewModelReady(int readyRoleId)
{
if (readyRoleId != _pendingRoleId) return;
UnsubscribeModelReady();
TryAttachFromPreview(readyRoleId);
}
///
/// Tries to find and attach to the model for in PlayerModelPreview.
/// Returns true if the model was found (active or not) and attachment succeeded.
///
private bool TryAttachFromPreview(int roleId)
{
var preview = PlayerModelPreview.Instance;
if (preview == null) return false;
var models = preview.playerModels;
var ids = preview.playerModelIds;
int count = Mathf.Min(models.Count, ids.Count);
for (int i = 0; i < count; i++)
{
if (ids[i] != roleId || models[i] == null) continue;
// Ensure the model is active so the camera can render it
if (!models[i].activeSelf)
models[i].SetActive(true);
_modelTransform = models[i].transform;
SetupPortrait();
return true;
}
return false;
}
private void UnsubscribeModelReady()
{
if (PlayerModelPreview.Instance != null)
PlayerModelPreview.Instance.OnModelReady -= OnPreviewModelReady;
_pendingRoleId = -1;
}
// ──────────────────────────────────────────────────────────────────────────────
// Internal
// ──────────────────────────────────────────────────────────────────────────────
private void SetupPortrait()
{
if (_modelTransform == null) return;
// Move model to portrait layer so the dedicated camera can render it
PortraitCaptureUtils.SetLayerRecursive(_modelTransform.gameObject, portraitLayer);
// Find head bone — primary target for camera attachment
_headBone = PortraitCaptureUtils.FindChildByName(_modelTransform, "Bip01 Head")
?? PortraitCaptureUtils.FindChildByName(_modelTransform, "Bip001 Head")
?? PortraitCaptureUtils.FindHeadBone(_modelTransform);
if (_headBone == null)
BMLogger.LogWarning("[HostPlayerPortraitCapture] Head bone not found — portrait skipped.");
AttachCameraToHeadBone();
SetCameraEnabled(true);
}
///
/// Parents the portrait camera to "Bip01 Head" and sets its initial position
/// directly in front of the face.
///
/// Coordinate math:
/// faceDir = local axis of headBone that points OUT of the face (Inspector: headFaceAxis)
/// camWorldPos = headBone.position + faceDir * cameraDistance
///
/// After SetParent, Unity converts this world position into a localPosition relative
/// to the head bone automatically. From that point on the camera rides the bone —
/// no per-frame world-space override is needed. Only LookAt is refreshed in LateUpdate.
///
private void AttachCameraToHeadBone()
{
if (portraitCamera == null) return;
Transform anchor = _headBone ?? _modelTransform;
// Face direction in world space — derived from head bone's own local axes
Vector3 faceDir = GetFaceDirectionWorld(anchor);
Vector3 headPos = anchor.position;
Vector3 faceCenter = headPos + Vector3.up * faceUpLift;
// Camera sits cameraDistance units in FRONT of the face (outside, looking back in)
Vector3 camWorldPos = headPos + faceDir * cameraDistance;
// Parent to head bone first with worldPositionStays:false (resets local to zero),
// then assign world position — Unity stores the correct localPosition internally
portraitCamera.transform.SetParent(anchor, worldPositionStays: false);
portraitCamera.transform.position = camWorldPos;
// Initial look-at so first frame is already correct before LateUpdate fires
portraitCamera.transform.LookAt(faceCenter);
}
///
/// Returns the world-space direction that points OUT OF the character's face,
/// derived from the head bone's local coordinate axes according to .
///
/// Biped rig convention (3ds Max → Unity export):
/// In most Perfect World rigs, the head bone's +Z (forward) points toward the face.
/// If the portrait comes out upside-down or backwards, toggle
/// and/or in the Inspector without changing code.
///
private Vector3 GetFaceDirectionWorld(Transform bone)
{
Vector3 dir = headFaceAxis switch
{
FaceAxis.Forward => bone.forward,
FaceAxis.Back => -bone.forward,
FaceAxis.Up => bone.up,
FaceAxis.Down => -bone.up,
FaceAxis.Right => bone.right,
FaceAxis.Left => -bone.right,
_ => bone.forward
};
return flipFaceAxis ? -dir : dir;
}
private void DetachCamera()
{
if (portraitCamera != null)
{
portraitCamera.transform.SetParent(transform, worldPositionStays: false);
SetCameraEnabled(false);
}
}
private void SetCameraEnabled(bool enabled)
{
if (portraitCamera != null)
portraitCamera.enabled = enabled;
}
private void EnsureCamera()
{
if (portraitCamera != null)
{
portraitCamera.cullingMask = 1 << portraitLayer;
portraitCamera.fieldOfView = fieldOfView;
portraitCamera.enabled = false;
return;
}
portraitCamera = PortraitCaptureUtils.CreatePortraitCamera(
transform, "PortraitCamera_HostPlayer", portraitLayer, fieldOfView);
}
private void EnsureRenderTexture()
{
if (outputTexture != null)
{
if (portraitCamera != null) portraitCamera.targetTexture = outputTexture;
return;
}
outputTexture = PortraitCaptureUtils.CreatePortraitRT("HostPlayerPortraitRT", portraitCamera);
}
// ──────────────────────────────────────────────────────────────────────────────
// Helpers
// ──────────────────────────────────────────────────────────────────────────────
public enum FaceAxis { Forward, Back, Up, Down, Right, Left }
}
}