242 lines
10 KiB
C#
242 lines
10 KiB
C#
using System.Collections.Generic;
|
||
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.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;
|
||
|
||
[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;
|
||
|
||
[Header("Materials")]
|
||
[Tooltip("Clone each renderer material on the portrait clone and switch copies to URP/Unlit.")]
|
||
[SerializeField] private bool usePortraitUnlitMaterials = true;
|
||
|
||
// ── runtime state ────────────────────────────────────────────────────────────
|
||
private GameObject _portraitClone;
|
||
private Transform _headBone;
|
||
private readonly List<Material> _portraitMaterialInstances = new();
|
||
|
||
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);
|
||
|
||
if (usePortraitUnlitMaterials)
|
||
{
|
||
PortraitCaptureUtils.ApplyClonedUrpUnlitMaterials(
|
||
_portraitClone,
|
||
_portraitMaterialInstances,
|
||
static msg => BMLogger.LogWarning(msg));
|
||
}
|
||
|
||
_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;
|
||
}
|
||
for (int i = 0; i < _portraitMaterialInstances.Count; i++)
|
||
{
|
||
if (_portraitMaterialInstances[i] != null)
|
||
Object.Destroy(_portraitMaterialInstances[i]);
|
||
}
|
||
_portraitMaterialInstances.Clear();
|
||
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 }
|
||
}
|
||
}
|