473 lines
17 KiB
C#
473 lines
17 KiB
C#
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
using System.Collections;
|
||
|
||
/// <summary>
|
||
/// 子部件UI渲染与观察器
|
||
/// 功能:与 EnhancedModelViewerOrbitCamera 配合,当主脚本进入子物体观察模式时,
|
||
/// 将克隆出的小部件渲染到指定UI上,并允许在UI内进行旋转和缩放观察。
|
||
/// 配置步骤:
|
||
/// 1. 将本脚本挂载到场景中。
|
||
/// 2. 在Inspector中,将主观察脚本拖入 `Main Viewer Script` 字段。
|
||
/// 3. 配置好UI相机 (UIRender Camera)、渲染纹理 (Render Texture) 和UI显示图片 (Widget Display Image)。
|
||
/// 4. 运行场景,点击大模型的子部件即可在UI中查看和操作。
|
||
/// </summary>
|
||
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; // 上一帧鼠标位置,用于计算拖拽增量
|
||
|
||
/// <summary>
|
||
/// 初始化:设置UI和相机的初始状态,并订阅主脚本的事件。
|
||
/// </summary>
|
||
void Start()
|
||
{
|
||
InitializeUIAndCamera();
|
||
SubscribeToMainViewerEvents();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 初始化UI面板和渲染相机。
|
||
/// </summary>
|
||
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面板中显示。");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 订阅主观察脚本的事件。
|
||
/// </summary>
|
||
private void SubscribeToMainViewerEvents()
|
||
{
|
||
if (mainViewerScript == null)
|
||
{
|
||
Debug.LogWarning("ChildWidgetUIRenderer: 未指定主观察脚本,功能将不可用。", this);
|
||
return;
|
||
}
|
||
// 订阅开始观察事件
|
||
mainViewerScript.OnChildObservationStarted += HandleChildObservationStarted;
|
||
// 订阅结束观察事件
|
||
mainViewerScript.OnChildObservationEnded += HandleChildObservationEnded;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 取消订阅主观察脚本的事件(防止内存泄漏)。
|
||
/// </summary>
|
||
private void OnDestroy()
|
||
{
|
||
if (mainViewerScript != null)
|
||
{
|
||
mainViewerScript.OnChildObservationStarted -= HandleChildObservationStarted;
|
||
mainViewerScript.OnChildObservationEnded -= HandleChildObservationEnded;
|
||
}
|
||
CleanupResources(); // 清理可能残留的资源
|
||
}
|
||
|
||
/// <summary>
|
||
/// 主脚本开始观察子物体时的处理函数。
|
||
/// </summary>
|
||
/// <param name="clonedWidget">主脚本创建的克隆部件。</param>
|
||
/// <param name="originalWidget">被点击的原始部件。</param>
|
||
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键退出。");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 主脚本结束观察子物体时的处理函数。
|
||
/// </summary>
|
||
/// <param name="clonedWidget">主脚本创建的克隆部件(即将被销毁)。</param>
|
||
/// <param name="originalWidget">被观察的原始部件。</param>
|
||
private void HandleChildObservationEnded(GameObject clonedWidget, GameObject originalWidget)
|
||
{
|
||
Debug.Log($"UI观察器:结束观察部件 '{originalWidget?.name}'。");
|
||
// 关闭UI面板,清理资源
|
||
CloseUIPanel();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 为UI显示准备克隆部件。
|
||
/// 注意:我们会创建这个部件的另一个副本,专供UI相机渲染,以避免干扰主场景的观察。
|
||
/// </summary>
|
||
/// <param name="sourceWidget">主脚本创建的克隆部件,作为复制源。</param>
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清理部件上不需要的组件,确保其在UI中正常显示且不干扰物理系统。
|
||
/// </summary>
|
||
private void CleanupWidgetComponents(GameObject widget)
|
||
{
|
||
// 移除所有脚本,避免在UI模式下执行不必要的逻辑
|
||
MonoBehaviour[] scripts = widget.GetComponentsInChildren<MonoBehaviour>();
|
||
foreach (var script in scripts)
|
||
{
|
||
Destroy(script);
|
||
}
|
||
// 移除碰撞体,避免意外的射线检测
|
||
Collider[] colliders = widget.GetComponentsInChildren<Collider>();
|
||
foreach (var collider in colliders)
|
||
{
|
||
Destroy(collider);
|
||
}
|
||
// 移除刚体,避免物理模拟
|
||
Rigidbody[] rigidbodies = widget.GetComponentsInChildren<Rigidbody>();
|
||
foreach (var rb in rigidbodies)
|
||
{
|
||
Destroy(rb);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将部件定位到UI相机前方合适的位置。
|
||
/// </summary>
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算包含所有Renderer的包围盒。
|
||
/// </summary>
|
||
private Bounds CalculateRenderBounds(GameObject go)
|
||
{
|
||
Renderer[] renderers = go.GetComponentsInChildren<Renderer>();
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 重置UI内的旋转和缩放控制参数。
|
||
/// </summary>
|
||
private void ResetUIControlParameters()
|
||
{
|
||
uiWidgetRotation = Vector2.zero;
|
||
uiWidgetCurrentZoom = uiDefaultZoom;
|
||
uiWidgetTargetZoom = uiDefaultZoom;
|
||
if (currentClonedWidgetForUI != null)
|
||
{
|
||
currentClonedWidgetForUI.transform.localRotation = Quaternion.identity;
|
||
currentClonedWidgetForUI.transform.localScale = Vector3.one;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 显示UI面板。
|
||
/// </summary>
|
||
private void ShowUIPanel()
|
||
{
|
||
if (uiPanel != null && !uiPanel.activeSelf)
|
||
{
|
||
uiPanel.SetActive(true);
|
||
isUIActive = true;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 关闭UI面板,并清理相关资源。
|
||
/// </summary>
|
||
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观察模式已关闭。");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 每帧更新:处理UI内的输入(旋转和缩放)。
|
||
/// </summary>
|
||
void Update()
|
||
{
|
||
if (!isUIActive || currentClonedWidgetForUI == null) return;
|
||
|
||
HandleUIInput();
|
||
ApplyUITransforms();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理UI显示区域内的鼠标输入。
|
||
/// </summary>
|
||
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();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检测鼠标位置是否在显示模型的RawImage矩形区域内。
|
||
/// </summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将UI控制参数(旋转、缩放)应用到模型上。
|
||
/// </summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新UI上的操作提示文本。
|
||
/// </summary>
|
||
private void UpdateInstructionText(string text)
|
||
{
|
||
if (instructionText != null)
|
||
{
|
||
instructionText.text = text;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清理脚本持有的资源。
|
||
/// </summary>
|
||
private void CleanupResources()
|
||
{
|
||
if (currentClonedWidgetForUI != null)
|
||
{
|
||
Destroy(currentClonedWidgetForUI);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 在编辑器模式下,绘制UI相机的视野范围等调试信息(可选)。
|
||
/// </summary>
|
||
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);
|
||
}
|
||
}
|
||
} |