객체 스냅 기능 업데이트 #4

Merged
UVCXR merged 1 commits from ssl/250224 into main 2025-02-24 16:06:02 +09:00
10 changed files with 1468 additions and 10 deletions

View File

@@ -15,7 +15,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
m_IsActive: 0
--- !u!4 &4257723628152675382
Transform:
m_ObjectHideFlags: 0

View File

@@ -221,8 +221,8 @@ namespace XED.Asset
return;
}
string name = "";
List<GameObject> selectedObjects = new List<GameObject>();
List<HierarchyItem> selectedItems = new List<HierarchyItem>();
List<CustomAssetRenderObject> selectedRenderObjects = new List<CustomAssetRenderObject>();
renderObjectHandler.RemoveAllSelections();
for (int i = 0; i < activeAssets.Count; i++)
{
@@ -231,12 +231,12 @@ namespace XED.Asset
{
name = i == 0 ? activeAsset.hierarchyItem.name : name.Equals(activeAsset.hierarchyItem.name) ? activeAsset.hierarchyItem.name : "-";
selectedItems.Add(activeAsset.hierarchyItem);
selectedObjects.Add(activeAsset.renderObject.gameObject);
renderObjectHandler.AddSelection(activeAsset.renderObject);
selectedRenderObjects.Add(activeAsset.renderObject);
}
}
renderObjectHandler.AddSelection(selectedRenderObjects);
componentScrollView.OnSelect(selectedItems);
onSelectObjects?.Invoke(name, selectedObjects);
onSelectObjects?.Invoke(name, selectedRenderObjects.Select(x => x.gameObject).ToList());
}
public void OnRemoveObject(List<CustomAssetRenderObject> objects)
{

View File

@@ -204,6 +204,30 @@ namespace XED.Util
Destroy(renderObject);
renderObject = null;
}
}
}
public Bounds GetBounds()
{
return boxCollider.bounds;
}
public List<Vector3> GetRenderObjectVertices()
{
MeshFilter[] meshFilters = renderObject.GetComponentsInChildren<MeshFilter>();
List<Vector3> points = new List<Vector3>();
for (int i = 0; i < meshFilters.Length; i++)
{
Vector3[] vertices = meshFilters[i].mesh.vertices;
for (int k = 0; k < vertices.Length; k++)
{
Vector3 pt = renderObject.transform.TransformPoint(vertices[k]);
points.Add(pt);
}
}
return points;
}
public KDTree GetVertexPointKDTree()
{
if (renderObject == null) return null;
return new KDTree(renderObject);
}
}
}

View File

@@ -20,6 +20,7 @@ namespace XED.Util
List<GameObject> selectedGameObjects = new List<GameObject>();
CustomAssetRenderObject prevSelectedObject;
RTGController rtgController;
ConvexHullCalculator convexHullCalculator = new ConvexHullCalculator();
Vector3 clickBeginPos;
float clickLengthThreshold = 5.0f;
int uiLayer;
@@ -72,8 +73,7 @@ namespace XED.Util
//rtg 컨트롤러 조작 후 포커스가 바뀌는 것을 방지하기 위한 용도
if (lockHandler == true)
{
lockHandler = false;
onDragForceEnd?.Invoke();
lockHandler = false;
return;
}
//ui를 선택했을 경우에는 오브젝트 선택을 멈춘다.
@@ -170,6 +170,21 @@ namespace XED.Util
selectedGameObjects.Add(item.gameObject);
rtgController.SetGizmoTargetObjects(selectedGameObjects);
}
public void AddSelection(List<CustomAssetRenderObject> items)
{
for (int i = 0; i < items.Count; i++)
{
CustomAssetRenderObject item = items[i];
if (selectedRenderObjects.Any(x => x == item))
{
return;
}
item.Select();
selectedRenderObjects.Add(item);
selectedGameObjects.Add(item.gameObject);
}
rtgController.SetGizmoTargetObjects(selectedGameObjects);
}
public void RemoveAllSelections()
{
for (int i = 0; i < selectedRenderObjects.Count; i++)
@@ -215,6 +230,7 @@ namespace XED.Util
{
OnTransformChanged(transformObjects);
onTransformChanged?.Invoke(selectedGameObjects);
onDragForceEnd?.Invoke();
}
public void OnTransformChanged(List<GameObject> transformObjects)
{
@@ -307,6 +323,81 @@ namespace XED.Util
});
CommandManager.I.ExecuteCommand(command);
}
public void VertexSnap()
{
if (selectedRenderObjects.Count != 1)
{
return;
}
//가장 가까운 오브젝트에 붙여준다.
//Transform.position이 가깝다고 오브젝트 사이 기리가 꼭 가장 가까운 것은 아니기 때문에
//오버랩 박스에 충돌되는 모든 오브젝트의 버텍스로 KDTree를 구성해 볼까 하다가 관두었다.
float minDist = float.MaxValue;
CustomAssetRenderObject fromObject = selectedRenderObjects[0];
CustomAssetRenderObject toObject = null;
Bounds fromBounds = fromObject.GetBounds();
Collider[] colliders = Physics.OverlapBox(fromBounds.center, fromBounds.size * 2.0f);
foreach (Collider collider in colliders)
{
if (collider.gameObject == fromObject.gameObject) continue;
CustomAssetRenderObject otherObject = collider.gameObject.GetComponent<CustomAssetRenderObject>();
if (otherObject != null)
{
float dist = Vector3.Magnitude(fromObject.transform.position - otherObject.transform.position);
if (dist < minDist)
{
minDist = dist;
toObject = otherObject;
}
}
}
if (toObject != null)
{
//선택된 오브젝트의 ConvexHull 버텍스 점을 계산해준다.
//이는 iteration하는 버텍스 수를 줄이기 위해 해주며,
//만약 선택된 오브젝트도 TDTree로 Nearest 포인트 탐지할 경우 toObject의 센터에 가장 가까운 포인트만 탐지되기 때문에 가장 가까운 버텍스 점이 골라지지 않는 경우가 있다.
List<Vector3> verts = new List<Vector3>();
List<int> tris = new List<int>();
List<Vector3> normals = new List<Vector3>();
convexHullCalculator.GenerateHull(fromObject.GetRenderObjectVertices(), false, ref verts, ref tris, ref normals);
KDTree toTree = toObject.GetVertexPointKDTree();
Vector3 nearestPoint = Vector3.zero;
Vector3 snapPoint = Vector3.zero;
float minDistance = float.MaxValue;
for (int i = 0; i < verts.Count; i++)
{
Vector3 ptNearest = verts[i];
Vector3 ptSnap = toTree.FindNearest(ptNearest);
float tempDist = Vector3.Magnitude(ptSnap - ptNearest);
if (tempDist < minDistance)
{
minDistance = tempDist;
nearestPoint = ptNearest;
snapPoint = ptSnap;
}
}
//Vector3 direction = snapPoint - nearestPoint;
//fromObject.transform.position += direction;
List<Vector3> dir = new List<Vector3>() { snapPoint - nearestPoint };
if (dir[0].magnitude < 0.5f)
{
fromObject.transform.position += dir[0];
}
else
{
ActionCommand command = new ActionCommand(
() =>
{
fromObject.transform.position += dir[0];
},
() =>
{
fromObject.transform.position -= dir[0];
});
CommandManager.I.ExecuteCommand(command);
}
}
}
InputHandler myHandler;
@@ -333,6 +424,7 @@ namespace XED.Util
getKeyActions.Add(KeyCode.W, SetRTGRotate);
getKeyActions.Add(KeyCode.E, SetRTGScale);
getKeyActions.Add(KeyCode.R, ResetRTG);
getKeyActions.Add(KeyCode.V, VertexSnap);
var shortcutTable = new Dictionary<KeyCode, Dictionary<KeyCode, Action>>();
#if UNITY_EDITOR

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c4a665cb7acfb0a48b480078f6e450b0

View File

@@ -0,0 +1,111 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace XED.Util
{
public class KDTree
{
private class KDNode
{
public Vector3 point;
public KDNode left, right;
public int axis;
}
private KDNode root;
public KDTree(Vector3[] points)
{
root = Build(points, 0);
}
public KDTree(GameObject gb)
{
MeshFilter[] meshFilters = gb.GetComponentsInChildren<MeshFilter>();
HashSet<Vector3> listPoints = new HashSet<Vector3>();
for (int i = 0; i < meshFilters.Length; i++)
{
Vector3[] vertices = meshFilters[i].mesh.vertices;
for (int k = 0; k < vertices.Length; k++)
{
Vector3 pt = gb.transform.TransformPoint(vertices[k]);
listPoints.Add(pt);
}
}
root = Build(listPoints.ToArray(), 0);
}
private KDNode Build(Vector3[] points, int depth)
{
if (points.Length == 0) return null;
int axis = depth % 3;
System.Array.Sort(points, (a, b) => (a[axis] < b[axis]) ? -1 : (a[axis] > b[axis]) ? 1 : 0);
//System.Array.Sort(points, (a, b) => a[axis].CompareTo(b[axis]));
int median = points.Length / 2;
return new KDNode
{
point = points[median],
axis = axis,
left = Build(points[..median], depth + 1),
right = Build(points[(median + 1)..], depth + 1)
};
}
private Vector3[] RemoveDuplicates(Vector3[] points, float epsilon = 1e-5f)
{
if (points.Length == 0) return points;
List<Vector3> uniquePoints = new List<Vector3> { points[0] };
for (int i = 1; i < points.Length; i++)
{
if (!ApproximatelyEqual(points[i], points[i - 1], epsilon))
{
uniquePoints.Add(points[i]);
}
}
return uniquePoints.ToArray();
}
private bool ApproximatelyEqual(Vector3 a, Vector3 b, float epsilon = 1e-5f)
{
return Mathf.Abs(a.x - b.x) < epsilon &&
Mathf.Abs(a.y - b.y) < epsilon &&
Mathf.Abs(a.z - b.z) < epsilon;
}
public Vector3 FindNearest(Vector3 target)
{
return FindNearest(root, target, root.point, float.MaxValue);
}
private Vector3 FindNearest(KDNode node, Vector3 target, Vector3 best, float bestDist)
{
if (node == null) return best;
float dist = Vector3.Distance(target, node.point);
if (dist < bestDist)
{
bestDist = dist;
best = node.point;
}
bool searchLeft = target[node.axis] < node.point[node.axis];
KDNode first = searchLeft ? node.left : node.right;
KDNode second = searchLeft ? node.right : node.left;
best = FindNearest(first, target, best, bestDist);
bestDist = Vector3.Distance(target, best);
if (Mathf.Abs(target[node.axis] - node.point[node.axis]) < bestDist)
{
best = FindNearest(second, target, best, bestDist);
}
return best;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d2c4266c7916a7f488400174b07261d8

View File

@@ -50,6 +50,7 @@ namespace XED.UI
}
public void OnEndDrag(Vector3 position)
{
if (!selectionBoxImage.gameObject.activeSelf) return;
selectionBoxImage.gameObject.SetActive(false);
StopAllCoroutines();
SetSelectionBoxSize(position);

View File

@@ -223,9 +223,14 @@ namespace XED.UI
}
if (selectedObjects.Count == 0)
{
return;
return;
}
Text_Name.text = name;
string append = "";
if (!name.Equals("-"))
{
append = string.Format(" ({0})", selectedObjects.Count);
}
Text_Name.text = name + append;
OnTransformChanged(selectedObjects);
if (lastSelectedInputField.type != InputFieldType.none)
{