using System.Collections.Generic; using BrewMonster.Scripts.Managers; using UnityEngine; namespace BrewMonster { /// /// 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 /// public class ElsePlayerPortraitCapture : MonoSingleton { [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 _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 // ────────────────────────────────────────────────────────────────────────────── /// /// Clones the visual model of into the portrait stage /// and starts rendering. Safe to call multiple times — old clone is destroyed first. /// 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().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); } /// /// Destroys the portrait clone and disables the camera. /// Call from OnTargetHUDClear / TryHideUINPC. /// 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 } } }