using UnityEngine; using UnityEngine.UI; using System.Collections; /// /// 子部件UI渲染与观察器 /// 功能:与 EnhancedModelViewerOrbitCamera 配合,当主脚本进入子物体观察模式时, /// 将克隆出的小部件渲染到指定UI上,并允许在UI内进行旋转和缩放观察。 /// 配置步骤: /// 1. 将本脚本挂载到场景中。 /// 2. 在Inspector中,将主观察脚本拖入 `Main Viewer Script` 字段。 /// 3. 配置好UI相机 (UIRender Camera)、渲染纹理 (Render Texture) 和UI显示图片 (Widget Display Image)。 /// 4. 运行场景,点击大模型的子部件即可在UI中查看和操作。 /// public class ChildWidgetUIRenderer : MonoBehaviour { [Header("【主观察脚本引用】")] [Tooltip("关联的 EnhancedModelViewerOrbitCamera 脚本。")] [SerializeField] private EnhancedModelViewerOrbitCamera mainViewerScript; [Header("【UI渲染核心组件】")] [Tooltip("用于显示部件模型的UI面板,通常包含一个RawImage。")] [SerializeField] public GameObject uiPanel; [Tooltip("用于显示模型的RawImage组件,其Texture将设置为下方的Render Texture。")] [SerializeField] public RawImage widgetDisplayImage; [Tooltip("专用于渲染UI中部件模型的相机。")] [SerializeField] private Camera uiRenderCamera; [Tooltip("渲染纹理,UI相机将画面渲染到此,再由RawImage显示。")] [SerializeField] private RenderTexture renderTexture; [Header("【UI内模型控制设置】")] [Tooltip("在UI区域内拖拽时,模型的旋转速度。")] [SerializeField] private float uiRotationSpeed = 2.0f; [Tooltip("在UI区域内使用滚轮时,模型的缩放速度。")] [SerializeField] private float uiZoomSpeed = 0.5f; [Tooltip("模型在UI中的最小缩放比例。")] [SerializeField] private float uiMinZoom = 0.5f; [Tooltip("模型在UI中的最大缩放比例。")] [SerializeField] private float uiMaxZoom = 3.0f; [Tooltip("模型在UI中的默认缩放比例。")] [SerializeField] private float uiDefaultZoom = 1.5f; [Header("【UI相机设置】")] [Tooltip("UI相机相对于模型初始位置的偏移量。Z值影响初始观察距离。")] [SerializeField] private Vector3 uiCameraOffset = new Vector3(0, 0.5f, 2.5f); [Tooltip("UI相机的视野角度(FOV),影响模型在画面中的大小。")] [SerializeField] private float uiCameraFieldOfView = 45f; [Header("【UI操作提示】(可选)")] [Tooltip("用于显示操作提示的UI文本组件,如“拖拽旋转,滚轮缩放”。")] [SerializeField] private Text instructionText; // --- 私有变量 --- private GameObject currentClonedWidgetForUI = null; // 当前由本脚本管理,用于UI显示的克隆部件 private bool isUIActive = false; // 标识UI面板是否处于激活状态 private bool isDraggingInUI = false; // 标识鼠标是否在UI区域内拖拽 // UI内模型的变换状态 private Vector2 uiWidgetRotation = Vector2.zero; // 累计旋转角度 (x: 绕世界Y轴, y: 绕自身X轴) private float uiWidgetCurrentZoom = 1.5f; // 当前缩放值 private float uiWidgetTargetZoom = 1.5f; // 目标缩放值(用于平滑) private Vector2 lastMousePosition; // 上一帧鼠标位置,用于计算拖拽增量 /// /// 初始化:设置UI和相机的初始状态,并订阅主脚本的事件。 /// void Start() { InitializeUIAndCamera(); SubscribeToMainViewerEvents(); } /// /// 初始化UI面板和渲染相机。 /// private void InitializeUIAndCamera() { // 初始时隐藏UI面板 if (uiPanel != null) { uiPanel.SetActive(false); } // 配置UI相机 if (uiRenderCamera != null) { uiRenderCamera.enabled = false; // 初始禁用 if (renderTexture != null) { uiRenderCamera.targetTexture = renderTexture; } uiRenderCamera.fieldOfView = uiCameraFieldOfView; } // 将渲染纹理赋给UI的RawImage if (widgetDisplayImage != null && renderTexture != null) { widgetDisplayImage.texture = renderTexture; } // 设置初始提示文本 UpdateInstructionText("点击大模型的子部件,将在UI面板中显示。"); } /// /// 订阅主观察脚本的事件。 /// private void SubscribeToMainViewerEvents() { if (mainViewerScript == null) { Debug.LogWarning("ChildWidgetUIRenderer: 未指定主观察脚本,功能将不可用。", this); return; } // 订阅开始观察事件 mainViewerScript.OnChildObservationStarted += HandleChildObservationStarted; // 订阅结束观察事件 mainViewerScript.OnChildObservationEnded += HandleChildObservationEnded; } /// /// 取消订阅主观察脚本的事件(防止内存泄漏)。 /// private void OnDestroy() { if (mainViewerScript != null) { mainViewerScript.OnChildObservationStarted -= HandleChildObservationStarted; mainViewerScript.OnChildObservationEnded -= HandleChildObservationEnded; } CleanupResources(); // 清理可能残留的资源 } /// /// 主脚本开始观察子物体时的处理函数。 /// /// 主脚本创建的克隆部件。 /// 被点击的原始部件。 private void HandleChildObservationStarted(GameObject clonedWidget, GameObject originalWidget) { if (clonedWidget == null) { Debug.LogError("收到观察开始事件,但克隆部件为Null。"); return; } Debug.Log($"UI观察器:开始处理部件 '{originalWidget.name}' 的UI显示。"); // 1. 显示UI面板 ShowUIPanel(); // 2. 设置UI显示用的克隆部件 SetupWidgetForUIDisplay(clonedWidget); // 3. 更新UI提示 UpdateInstructionText("在蓝色区域内拖拽旋转,滚轮缩放。\n按ESC或Shift键退出。"); } /// /// 主脚本结束观察子物体时的处理函数。 /// /// 主脚本创建的克隆部件(即将被销毁)。 /// 被观察的原始部件。 private void HandleChildObservationEnded(GameObject clonedWidget, GameObject originalWidget) { Debug.Log($"UI观察器:结束观察部件 '{originalWidget?.name}'。"); // 关闭UI面板,清理资源 CloseUIPanel(); } /// /// 为UI显示准备克隆部件。 /// 注意:我们会创建这个部件的另一个副本,专供UI相机渲染,以避免干扰主场景的观察。 /// /// 主脚本创建的克隆部件,作为复制源。 private void SetupWidgetForUIDisplay(GameObject sourceWidget) { // 清理可能存在的旧UI部件 if (currentClonedWidgetForUI != null) { Destroy(currentClonedWidgetForUI); } // 创建专用于UI显示的副本 //currentClonedWidgetForUI = Instantiate(sourceWidget, Vector3.zero, Quaternion.identity); //currentClonedWidgetForUI.name = sourceWidget.name + "_UI_View"; currentClonedWidgetForUI = this.transform.GetChild(0).gameObject; currentClonedWidgetForUI.layer = LayerMask.NameToLayer("CloneModel"); // 移除可能干扰的组件(如碰撞体、刚体、主脚本可能添加的临时组件等) CleanupWidgetComponents(currentClonedWidgetForUI); // 将部件放置在UI相机前 //PositionWidgetInFrontOfUICamera(currentClonedWidgetForUI); // 重置UI控制参数 ResetUIControlParameters(); // 启用UI相机 if (uiRenderCamera != null) { uiRenderCamera.enabled = true; } } /// /// 清理部件上不需要的组件,确保其在UI中正常显示且不干扰物理系统。 /// private void CleanupWidgetComponents(GameObject widget) { // 移除所有脚本,避免在UI模式下执行不必要的逻辑 MonoBehaviour[] scripts = widget.GetComponentsInChildren(); foreach (var script in scripts) { Destroy(script); } // 移除碰撞体,避免意外的射线检测 Collider[] colliders = widget.GetComponentsInChildren(); foreach (var collider in colliders) { Destroy(collider); } // 移除刚体,避免物理模拟 Rigidbody[] rigidbodies = widget.GetComponentsInChildren(); foreach (var rb in rigidbodies) { Destroy(rb); } } /// /// 将部件定位到UI相机前方合适的位置。 /// private void PositionWidgetInFrontOfUICamera(GameObject widget) { if (uiRenderCamera == null || widget == null) return; // 将部件设为UI相机的子物体,便于管理 widget.transform.SetParent(uiRenderCamera.transform); // 设置局部位置和旋转 widget.transform.localPosition = uiCameraOffset; widget.transform.localRotation = Quaternion.identity; // 可选:根据部件大小自动调整初始缩放,确保其在视野内 Bounds bounds = CalculateRenderBounds(widget); float maxSize = Mathf.Max(bounds.size.x, bounds.size.y, bounds.size.z); if (maxSize > 0) { // 一个简单的自适应缩放,可以根据uiCameraOffset.z和FOV进一步优化 float adaptiveScale = 2.0f / maxSize; widget.transform.localScale = Vector3.one * adaptiveScale; } } /// /// 计算包含所有Renderer的包围盒。 /// private Bounds CalculateRenderBounds(GameObject go) { Renderer[] renderers = go.GetComponentsInChildren(); if (renderers.Length == 0) return new Bounds(go.transform.position, Vector3.one); Bounds bounds = renderers[0].bounds; for (int i = 1; i < renderers.Length; i++) { bounds.Encapsulate(renderers[i].bounds); } return bounds; } /// /// 重置UI内的旋转和缩放控制参数。 /// private void ResetUIControlParameters() { uiWidgetRotation = Vector2.zero; uiWidgetCurrentZoom = uiDefaultZoom; uiWidgetTargetZoom = uiDefaultZoom; if (currentClonedWidgetForUI != null) { currentClonedWidgetForUI.transform.localRotation = Quaternion.identity; currentClonedWidgetForUI.transform.localScale = Vector3.one; } } /// /// 显示UI面板。 /// private void ShowUIPanel() { if (uiPanel != null && !uiPanel.activeSelf) { uiPanel.SetActive(true); isUIActive = true; } } /// /// 关闭UI面板,并清理相关资源。 /// public void CloseUIPanel() { if (!isUIActive) return; // 隐藏面板 if (uiPanel != null) { uiPanel.SetActive(false); } isUIActive = false; isDraggingInUI = false; // 禁用UI相机 if (uiRenderCamera != null) { uiRenderCamera.enabled = false; } // 销毁专用于UI显示的克隆部件 if (currentClonedWidgetForUI != null) { Destroy(currentClonedWidgetForUI); currentClonedWidgetForUI = null; } // 更新提示文本 UpdateInstructionText("UI观察模式已关闭。"); } /// /// 每帧更新:处理UI内的输入(旋转和缩放)。 /// void Update() { if (!isUIActive || currentClonedWidgetForUI == null) return; HandleUIInput(); ApplyUITransforms(); } /// /// 处理UI显示区域内的鼠标输入。 /// private void HandleUIInput() { // 检查鼠标是否在显示部件的UI区域内 bool isMouseOverUI = IsMouseOverDisplayImage(); // --- 旋转控制(鼠标拖拽)--- if (isMouseOverUI && Input.GetMouseButtonDown(0)) { isDraggingInUI = true; lastMousePosition = Input.mousePosition; } if (Input.GetMouseButton(0) && isDraggingInUI) { Vector2 currentMousePos = Input.mousePosition; Vector2 delta = currentMousePos - lastMousePosition; // 根据鼠标移动增量更新旋转角度 uiWidgetRotation.x += delta.x * uiRotationSpeed * 0.1f; // 绕世界Y轴旋转 uiWidgetRotation.y += delta.y * uiRotationSpeed * 0.1f; // 绕自身X轴旋转 uiWidgetRotation.y = Mathf.Clamp(uiWidgetRotation.y, -80f, 80f); // 限制上下翻转角度 lastMousePosition = currentMousePos; } if (Input.GetMouseButtonUp(0)) { isDraggingInUI = false; } // --- 缩放控制(鼠标滚轮)--- if (isMouseOverUI) { float scroll = Input.GetAxis("Mouse ScrollWheel"); if (Mathf.Abs(scroll) > 0.01f) { // 根据滚轮方向调整目标缩放值 uiWidgetTargetZoom += scroll * uiZoomSpeed; uiWidgetTargetZoom = Mathf.Clamp(uiWidgetTargetZoom, uiMinZoom, uiMaxZoom); } } // --- 退出控制(ESC键)--- if (Input.GetKeyDown(KeyCode.Escape)) { // 通知主脚本退出子物体观察模式,这将触发我们订阅的End事件,进而关闭UI。 if (mainViewerScript != null && mainViewerScript.IsObservingChild()) { // 假设主脚本有公共的退出方法。如果没有,您可能需要通过其他方式触发退出。 // 例如,可以调用 mainViewerScript.ExitChildObservationMode(); 如果它是public的。 // 这里作为一个安全调用,实际情况取决于您的主脚本。 Debug.Log("UI观察器:按ESC键,尝试退出观察模式。"); // 注意:更佳实践是让UI的关闭逻辑与主脚本解耦,通过事件驱动。 // 这里我们直接关闭自己的UI,主脚本的退出由用户按Shift键或其他方式触发。 CloseUIPanel(); } } } /// /// 检测鼠标位置是否在显示模型的RawImage矩形区域内。 /// private bool IsMouseOverDisplayImage() { if (widgetDisplayImage == null) return false; RectTransform rect = widgetDisplayImage.rectTransform; Vector2 localPoint; // 将屏幕鼠标坐标转换为UI矩形内的本地坐标 return RectTransformUtility.ScreenPointToLocalPointInRectangle(rect, Input.mousePosition, null, out localPoint) && rect.rect.Contains(localPoint); } /// /// 将UI控制参数(旋转、缩放)应用到模型上。 /// private void ApplyUITransforms() { if (currentClonedWidgetForUI == null) return; // 应用旋转 Quaternion targetRotation = Quaternion.Euler(uiWidgetRotation.y, -uiWidgetRotation.x, 0); currentClonedWidgetForUI.transform.localRotation = Quaternion.Slerp( currentClonedWidgetForUI.transform.localRotation, targetRotation, 10f * Time.deltaTime ); // 应用缩放 uiWidgetCurrentZoom = Mathf.Lerp(uiWidgetCurrentZoom, uiWidgetTargetZoom, 5f * Time.deltaTime); currentClonedWidgetForUI.transform.localScale = Vector3.one * uiWidgetCurrentZoom; } /// /// 更新UI上的操作提示文本。 /// private void UpdateInstructionText(string text) { if (instructionText != null) { instructionText.text = text; } } /// /// 清理脚本持有的资源。 /// private void CleanupResources() { if (currentClonedWidgetForUI != null) { Destroy(currentClonedWidgetForUI); } } /// /// 在编辑器模式下,绘制UI相机的视野范围等调试信息(可选)。 /// void OnDrawGizmosSelected() { if (uiRenderCamera != null) { Gizmos.color = Color.cyan; Gizmos.matrix = uiRenderCamera.transform.localToWorldMatrix; // 简单绘制一个相机视椎体 float distance = uiCameraOffset.z; Gizmos.DrawFrustum(Vector3.zero, uiCameraFieldOfView, distance, 0.1f, 1.0f); } } }