Files
test/Assets/PerfectWorld/Scripts/UI/ElsePlayerPortraitCapture.cs
T

222 lines
9.6 KiB
C#
Raw 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.
using BrewMonster.Scripts.Managers;
using UnityEngine;
namespace BrewMonster
{
/// <summary>
/// Renders an EC_ElsePlayer portrait into a RenderTexture for the target HUD (HUDNPC).
///
/// Instead of following the live player transform every frame (which causes jitter when
/// the target moves/jumps), this system clones the visual model into an isolated
/// "portrait stage" at a fixed world position. The portrait camera only ever looks at
/// the static clone, so the output is perfectly stable.
///
/// Public API (unchanged — CECUIManager needs no edits):
/// SetTarget(Transform playerRoot) — clone model, start rendering
/// ClearTarget() — destroy clone, stop rendering
/// OutputTexture — wire to RawImage.texture
/// </summary>
public class ElsePlayerPortraitCapture : MonoSingleton<ElsePlayerPortraitCapture>
{
[Header("Portrait Camera")]
[SerializeField] private Camera portraitCamera;
[Header("Render Output")]
[SerializeField] private RenderTexture outputTexture;
[Header("Portrait Stage")]
[Tooltip("Layer index for the Portrait layer (Project Settings > Tags and Layers).")]
[SerializeField] private int portraitLayer = 9;
[Tooltip("Fixed world position of the clone. Keep far from gameplay to avoid overlap.")]
[SerializeField] private Vector3 stagePosition = new Vector3(9999f, 0f, 0f);
[Header("Framing")]
[Tooltip("Distance the camera sits in front of the face (meters). ~0.51.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;
[Header("Head Bone Face Direction")]
[Tooltip("Which local axis of Bip01 Head points OUT of the face (toward the camera).")]
[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;
[SerializeField][Range(10f, 60f)] private float fieldOfView = 35f;
// ── runtime state ────────────────────────────────────────────────────────────
private GameObject _portraitClone;
private Transform _headBone;
public RenderTexture OutputTexture => outputTexture;
// ──────────────────────────────────────────────────────────────────────────────
// Lifecycle
// ──────────────────────────────────────────────────────────────────────────────
protected override void Initialize()
{
EnsureCamera();
EnsureRenderTexture();
}
protected override void OnDestroy()
{
base.OnDestroy();
ClearTarget();
if (outputTexture != null && outputTexture.IsCreated())
outputTexture.Release();
}
// ──────────────────────────────────────────────────────────────────────────────
// Public API
// ──────────────────────────────────────────────────────────────────────────────
/// <summary>
/// Clones the visual model of <paramref name="playerRoot"/> into the portrait stage
/// and starts rendering. Safe to call multiple times — old clone is destroyed first.
/// </summary>
public void SetTarget(Transform playerRoot)
{
ClearTarget();
if (playerRoot == null) return;
Transform modelRoot = PortraitCaptureUtils.FindVisualModelRoot(playerRoot);
if (modelRoot == null)
{
BMLogger.LogWarning("[ElsePlayerPortraitCapture] Visual model root not found — portrait skipped.");
return;
}
_portraitClone = Object.Instantiate(modelRoot.gameObject);
_portraitClone.GetComponentInChildren<Animator>().enabled = false;
_portraitClone.name = "[Portrait] ElsePlayer";
_portraitClone.transform.position = stagePosition;
_portraitClone.transform.rotation = Quaternion.identity;
// Strip physics / game-logic components; keep Animator for idle animation
PortraitCaptureUtils.CleanupCloneComponents(_portraitClone);
// Hide from main camera — portrait camera sees only this layer
PortraitCaptureUtils.SetLayerRecursive(_portraitClone, portraitLayer);
_headBone = PortraitCaptureUtils.FindHeadBone(_portraitClone.transform);
AttachCameraToHeadBone();
SetCameraEnabled(true);
}
/// <summary>
/// Destroys the portrait clone and disables the camera.
/// Call from OnTargetHUDClear / TryHideUINPC.
/// </summary>
public void ClearTarget()
{
if (_portraitClone != null)
{
Object.Destroy(_portraitClone);
_portraitClone = null;
}
DetachCamera();
_headBone = null;
}
private void LateUpdate()
{
if (portraitCamera == null || !portraitCamera.enabled) return;
if (_headBone == null) return;
Vector3 faceCenter = _headBone.position + Vector3.up * faceUpLift;
Vector3 toFace = faceCenter - portraitCamera.transform.position;
if (toFace.sqrMagnitude < 0.0001f) return;
Quaternion targetRot = Quaternion.LookRotation(toFace);
portraitCamera.transform.rotation = Quaternion.Slerp(
portraitCamera.transform.rotation,
targetRot,
Time.deltaTime * lookSmoothSpeed);
}
// ──────────────────────────────────────────────────────────────────────────────
// Internal
// ──────────────────────────────────────────────────────────────────────────────
private void AttachCameraToHeadBone()
{
if (portraitCamera == null) return;
Transform anchor = _headBone != null ? _headBone : _portraitClone.transform;
Vector3 faceDir = GetFaceDirectionWorld(anchor);
Vector3 headPos = anchor.position;
Vector3 faceCenter = headPos + Vector3.up * faceUpLift;
Vector3 camWorldPos = headPos + faceDir * cameraDistance;
portraitCamera.transform.SetParent(anchor, worldPositionStays: false);
portraitCamera.transform.position = camWorldPos;
portraitCamera.transform.LookAt(faceCenter);
}
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_ElsePlayer", portraitLayer, fieldOfView);
}
private void EnsureRenderTexture()
{
if (outputTexture != null)
{
if (portraitCamera != null) portraitCamera.targetTexture = outputTexture;
return;
}
outputTexture = PortraitCaptureUtils.CreatePortraitRT("ElsePlayerPortraitRT", portraitCamera);
}
public enum FaceAxis { Forward, Back, Up, Down, Right, Left }
}
}