개발 새발 블로그

[Unity] UI ToolKit Resize,Dragable 본문

Unity 한테 정복당하기

[Unity] UI ToolKit Resize,Dragable

NullException 2026. 6. 2. 02:23

Unity 6 · UIElements · UI Toolkit · Digital Twin 개발 중 마주친 문제들과 해결법을 정리합니다.


1. UIElements RightPanel 드래그 리사이즈

문제

flex-direction: row으로 Main(좌측 3D 뷰)RightPanel(우측 데이터 패널)을 나란히 배치했는데, RightPanel 폭이 width: 40%로 고정되어 사용자가 조절할 수 없었습니다.

해결 전략

Main과 RightPanel 사이에 5px 너비의 드래그 핸들 VisualElement를 삽입하고, C#에서 Pointer 이벤트로 폭을 실시간 조정합니다. 핵심은 width: %width: px 고정값으로 전환해 드래그가 즉시 반영되도록 하는 것입니다.

UXML — 핸들 삽입

<!-- Main 닫는 태그와 RightPanel 사이 -->
<ui:VisualElement name="ResizeHandle" class="resize-handle" />
<ui:VisualElement name="RightPanel"
    style="min-width: 300px; background-color: rgb(30, 31, 32); flex-shrink: 0;">

USS — 핸들 스타일

.resize-handle {
    width: 5px;
    background-color: rgb(50, 51, 52);
    flex-shrink: 0;
    cursor: col-resize;
    transition-property: background-color;
    transition-duration: 0.1s;
}

.resize-handle:hover,
.resize-handle--dragging {
    background-color: rgb(0, 120, 215);
}

C# — 드래그 이벤트

// 초기 width: % → px 고정 (GeometryChangedEvent 이후)
private void OnRightPanelGeometryReady(GeometryChangedEvent evt)
{
    if (_rightPanel.resolvedStyle.width > 0)
    {
        _rightPanel.style.width    = _rightPanel.resolvedStyle.width;
        _rightPanel.style.flexGrow = 0;
        _rightPanel.UnregisterCallback<GeometryChangedEvent>(OnRightPanelGeometryReady);
    }
}

private void OnResizePointerDown(PointerDownEvent evt)
{
    _isDragging     = true;
    _dragStartX     = evt.position.x;
    _dragStartWidth = _rightPanel.resolvedStyle.width;
    _resizeHandle.AddToClassList("resize-handle--dragging");
    _resizeHandle.CapturePointer(evt.pointerId);
    evt.StopPropagation();
}

private void OnResizePointerMove(PointerMoveEvent evt)
{
    if (!_isDragging) return;
    // 핸들을 왼쪽으로 드래그 → RightPanel 넓어짐
    float delta    = _dragStartX - evt.position.x;
    float newWidth = Mathf.Max(300f, _dragStartWidth + delta);
    _rightPanel.style.width = newWidth;
    evt.StopPropagation();
}

private void OnResizePointerUp(PointerUpEvent evt)
{
    _isDragging = false;
    _resizeHandle.RemoveFromClassList("resize-handle--dragging");
    _resizeHandle.ReleasePointer(evt.pointerId);
    evt.StopPropagation();
}
포인트: CapturePointer를 사용해야 드래그 중 마우스가 핸들 밖으로 나가도 이벤트가 끊기지 않습니다.

2. Camera Frustum Shift — RightPanel 폭에 따라 카메라 중심 이동

문제

RightPanel이 화면 우측 40%를 차지하면 카메라가 화면 정중앙을 바라보기 때문에 3D 뷰의 피사체가 실제 보이는 영역 중심이 아닌 오른쪽으로 치우쳐 보입니다. 기존에는 camera.rect.x를 고정값으로 조정했지만, RightPanel 폭이 유동적으로 바뀌면 함께 대응해야 합니다.

왜 Projection Matrix인가

방법 렌더 잘림 정석 여부
Viewport Rect X 조정 좌측 잘림 발생 편법
Projection Matrix Frustum Offset 없음 정석 ✓
Physical Camera Lens Shift 없음 내부적으로 동일 방식

VR의 비대칭 frustum, 와이드 멀티모니터 렌더링 등에서 표준으로 사용하는 방식입니다. Projection Matrix의 proj[0,2] 값이 수평 frustum 비대칭 오프셋을 담당합니다.

수식

// 카메라가 바라봐야 할 영역의 중심 (픽셀)
visibleWidth  = Screen.width - rightPanelWidth
visibleCenter = visibleWidth / 2

// NDC (-1 ~ 1) 기준 오프셋
offsetNDC = (screenCenter - visibleCenter) / screenCenter

// Projection Matrix에 적용
proj[0,2] = offsetNDC

구현 — CameraFrustumShift.cs

[RequireComponent(typeof(Camera))]
public class CameraFrustumShift : MonoBehaviour
{
    public static float RightPanelWidth { get; set; }

    private Camera _cam;

    void Awake() => _cam = GetComponent<Camera>();

    void LateUpdate() => ApplyFrustumShift(RightPanelWidth);

    private void ApplyFrustumShift(float rightPanelWidthPx)
    {
        float visibleCenter = (Screen.width - rightPanelWidthPx) * 0.5f;
        float screenCenter  = Screen.width * 0.5f;
        float offsetNDC     = (screenCenter - visibleCenter) / screenCenter;

        Matrix4x4 proj = Matrix4x4.Perspective(
            _cam.fieldOfView, _cam.aspect,
            _cam.nearClipPlane, _cam.farClipPlane);
        proj[0, 2] = offsetNDC;
        _cam.projectionMatrix = proj;
    }
}

MainUIController의 리사이즈 핸들러에서 CameraFrustumShift.RightPanelWidth = newWidth;만 호출하면 LateUpdate에서 자동으로 매트릭스를 갱신합니다.


3. UIDocument + 3D 오브젝트 포인터 이벤트 공존

문제

3D 오브젝트에 IPointerClickHandler를 붙이고 카메라에 PhysicsRaycaster를 추가해도 클릭/호버 이벤트가 전혀 발생하지 않는 문제가 생깁니다.

원인은 UI Toolkit의 PanelRaycaster가 EventSystem의 모든 포인터 이벤트를 먼저 가로채기 때문입니다. PhysicsRaycaster까지 이벤트가 내려오지 못합니다.

해결책 — Physics.Raycast 직접 사용

EventSystem을 우회하고, 중앙 매니저 스크립트Update()에서 Physics.Raycast로 직접 처리합니다.

// PolePart.cs — 순수 메서드만 (IPointer 인터페이스 제거)
public class PolePart : MonoBehaviour
{
    public void OnEnter() { /* 호버 진입 처리 */ }
    public void OnExit()  { /* 호버 나감 처리 */ }
    public void OnClick() { /* 클릭 처리      */ }
}

// WorldCanvasManager.cs — 중앙 Raycast 루프
public class WorldCanvasManager : MonoBehaviour
{
    [SerializeField] private LayerMask targetLayer;
    private Camera   _cam;
    private PolePart _hovered;

    void Start() => _cam = Camera.main;

    void Update()
    {
        Ray ray = _cam.ScreenPointToRay(Input.mousePosition);

        if (Physics.Raycast(ray, out RaycastHit hit, Mathf.Infinity, targetLayer))
        {
            var part = hit.collider.GetComponent<PolePart>();

            if (part != null && part != _hovered)   // Enter
            {
                _hovered?.OnExit();
                _hovered = part;
                _hovered.OnEnter();
            }
            if (part != null && Input.GetMouseButtonDown(0))  // Click
                part.OnClick();
        }
        else if (_hovered != null)                             // Exit
        {
            _hovered.OnExit();
            _hovered = null;
        }
    }
}
주의: Physics.Raycast의 세 번째 인자는 float maxDistance입니다.
Physics.Raycast(ray, out hit, targetLayer)처럼 쓰면 LayerMask 값(예: 256)이 거리값으로 해석되어 항상 true를 반환합니다.
반드시 Physics.Raycast(ray, out hit, Mathf.Infinity, targetLayer)로 작성하세요.

씬 세팅

  • IPointerClickHandler, PhysicsRaycaster — 제거
  • 각 3D 파트 → Collider + PolePart.cs
  • 씬에 빈 GameObject → WorldCanvasManager.cs
  • Inspector에서 targetLayer에 3D 파트 레이어 지정

정리

주제 핵심 포인트
RightPanel 리사이즈 핸들 삽입 + CapturePointer + width px 고정
Camera Frustum Shift proj[0,2] = offsetNDC — Viewport Rect 잘림 없음
UIDocument + 3D 이벤트 PanelRaycaster 우회 → Physics.Raycast 직접 처리

Unity 6 | UI Toolkit | Digital Twin 개발 노트

'Unity 한테 정복당하기' 카테고리의 다른 글

Unity Spooky Challenge 후기  (0) 2025.10.10
Unity 6.2 AI 로 Sound를 제작해보자  (0) 2025.10.08
[Unity] Rig Error Copied Avatar Rig Configuration mis-match  (4) 2025.07.20
Unity 멀티 Photon PUN  (0) 2025.03.23
UI  (0) 2025.03.17