2025-11-12 16:48:34 +09:00
|
|
|
#nullable enable
|
2025-11-13 20:16:25 +09:00
|
|
|
using Cysharp.Threading.Tasks;
|
|
|
|
|
using GLTFast;
|
2025-11-12 12:28:17 +09:00
|
|
|
using System;
|
2025-11-13 20:16:25 +09:00
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Threading;
|
2025-11-12 12:28:17 +09:00
|
|
|
using UnityEngine;
|
2025-11-13 20:16:25 +09:00
|
|
|
using UnityEngine.UI;
|
2025-11-12 16:48:34 +09:00
|
|
|
using UVC.UI.List.Tree;
|
2025-11-13 20:16:25 +09:00
|
|
|
using UVC.Util;
|
2025-11-12 12:28:17 +09:00
|
|
|
|
|
|
|
|
namespace SHI.modal
|
|
|
|
|
{
|
2025-11-13 20:16:25 +09:00
|
|
|
public class ModelDetailView : MonoBehaviour
|
2025-11-12 12:28:17 +09:00
|
|
|
{
|
2025-11-12 16:48:34 +09:00
|
|
|
public Action<TreeListItemData>? OnItemSelected;
|
|
|
|
|
|
2025-11-13 20:16:25 +09:00
|
|
|
[Header("View Output (UI)")]
|
|
|
|
|
[SerializeField] private RawImage outputImage;
|
|
|
|
|
[SerializeField] private Color backgroundColor = new Color(0.2f, 0.2f, 0.22f, 1f);
|
|
|
|
|
[Tooltip("전용 카메라가 렌더링할 레이어. 모델 노드에 재귀적으로 적용됩니다.")]
|
|
|
|
|
[SerializeField] private int modelLayer = 6;
|
|
|
|
|
[SerializeField] private bool createDefaultLight = true;
|
|
|
|
|
|
|
|
|
|
[Header("Mouse Controls")]
|
|
|
|
|
[SerializeField] private float panSpeed = 1.0f; // 픽셀→월드 스케일 보정 계수
|
|
|
|
|
[SerializeField] private float rotateDegPerPixel = 0.2f; // RMB 회전(도/픽셀)
|
|
|
|
|
[SerializeField] private float zoomSpeed = 5f; // 휠 전/후 모델 이동 속도
|
|
|
|
|
|
|
|
|
|
[Header("Output Image Setup")]
|
|
|
|
|
[SerializeField] private bool autoCreateOutputImage = true;
|
|
|
|
|
[SerializeField] private bool autoApplyRectMask = true;
|
|
|
|
|
|
|
|
|
|
[Header("Wireframe")]
|
|
|
|
|
[SerializeField] private bool wireframeMode = false; // 켜면 라인만 렌더
|
|
|
|
|
|
|
|
|
|
private UnityEngine.Camera _viewCamera;
|
|
|
|
|
private RenderTexture _rt;
|
|
|
|
|
private Material _wireframeMat; // Resources에서 로드한 메터리얼
|
|
|
|
|
private bool _wireframeApplied;
|
|
|
|
|
|
|
|
|
|
// Orbit controls state (카메라 위치 계산용)
|
|
|
|
|
private Vector3 _orbitTarget;
|
|
|
|
|
private float _orbitDistance = 5f;
|
|
|
|
|
private float _yaw = 0f;
|
|
|
|
|
private float _pitch = 20f;
|
|
|
|
|
|
|
|
|
|
// Drag state
|
|
|
|
|
private bool _mmbDragging;
|
|
|
|
|
private bool _rmbDragging;
|
|
|
|
|
private Vector3 _mmbLastPos;
|
|
|
|
|
private Vector3 _rmbStartPos;
|
|
|
|
|
private float _yawStart;
|
|
|
|
|
private float _pitchStart;
|
|
|
|
|
private Quaternion _modelStartRot;
|
2025-11-14 17:02:38 +09:00
|
|
|
private Vector3 _modelStartPos; // RMB 드래그 시작 시 모델 위치
|
|
|
|
|
private Vector3 _rmbPivot; // RMB 회전의 피벗(모델 중심)
|
2025-11-13 20:16:25 +09:00
|
|
|
|
|
|
|
|
private readonly Dictionary<Guid, GameObject> _idToObject = new Dictionary<Guid, GameObject>();
|
|
|
|
|
private readonly Dictionary<GameObject, (Renderer[] rends, UnityEngine.Material[][] originals)> _matCache = new Dictionary<GameObject, (Renderer[], UnityEngine.Material[][])>();
|
|
|
|
|
private GameObject? _root;
|
|
|
|
|
private Guid? _focusedId;
|
|
|
|
|
|
2025-11-14 17:02:38 +09:00
|
|
|
|
2025-11-13 20:16:25 +09:00
|
|
|
public async UniTask<IEnumerable<ModelDetailListItemData>> LoadModelAsync(string path, CancellationToken ct)
|
|
|
|
|
{
|
|
|
|
|
Debug.Log($"ModelDetailView.LoadModelAsync: {path}");
|
|
|
|
|
Dispose();
|
|
|
|
|
await UniTask.DelayFrame(1); // Dispose 후1프레임 대기하여 리소스 해제 안정화
|
|
|
|
|
EnsureCameraAndTargetTexture();
|
|
|
|
|
|
|
|
|
|
var items = new List<ModelDetailListItemData>();
|
|
|
|
|
var gltf = new GltfImport();
|
|
|
|
|
var success = await gltf.Load(path, new ImportSettings(), ct);
|
|
|
|
|
if (!success)
|
|
|
|
|
{
|
|
|
|
|
Debug.LogError($"glTFast Load failed: {path}");
|
|
|
|
|
return items;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_root = new GameObject("ModelDetailViewRoot");
|
|
|
|
|
_root.layer = modelLayer;
|
|
|
|
|
var sceneOk = await gltf.InstantiateMainSceneAsync(_root.transform);
|
|
|
|
|
if (!sceneOk)
|
|
|
|
|
{
|
|
|
|
|
Debug.LogError("InstantiateMainSceneAsync failed");
|
|
|
|
|
return items;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SetLayerRecursive(_root, modelLayer);
|
|
|
|
|
|
|
|
|
|
// Build hierarchical item data preserving parent-child relationships
|
|
|
|
|
if (_root != null)
|
|
|
|
|
{
|
|
|
|
|
for (int i = 0; i < _root.transform.childCount; i++)
|
|
|
|
|
{
|
|
|
|
|
var child = _root.transform.GetChild(i);
|
|
|
|
|
var topItem = BuildItemRecursive(child, _root.transform);
|
|
|
|
|
if (topItem != null)
|
|
|
|
|
{
|
|
|
|
|
items.Add(topItem);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var bounds = CalculateBounds(_root);
|
|
|
|
|
FrameToBounds(bounds);
|
|
|
|
|
|
|
|
|
|
// 요청: Resources/SHI/Shader/BasicWireframe 메터리얼 적용
|
|
|
|
|
TryLoadWireframeMaterial();
|
|
|
|
|
if (wireframeMode && _wireframeMat != null)
|
|
|
|
|
{
|
|
|
|
|
ApplyWireframeMaterialToRoot();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return items;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private ModelDetailListItemData BuildItemRecursive(Transform node, Transform root)
|
|
|
|
|
{
|
|
|
|
|
var data = new ModelDetailListItemData
|
|
|
|
|
{
|
|
|
|
|
Name = node.name,
|
|
|
|
|
ExternalKey = BuildFullPath(node, root)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// map id -> object for selection/highlight and cache materials at this subtree root
|
|
|
|
|
_idToObject[data.Id] = node.gameObject;
|
|
|
|
|
CacheOriginalMaterials(node.gameObject);
|
|
|
|
|
|
|
|
|
|
// children
|
|
|
|
|
for (int i = 0; i < node.childCount; i++)
|
|
|
|
|
{
|
|
|
|
|
var child = node.GetChild(i);
|
|
|
|
|
var childData = BuildItemRecursive(child, root);
|
|
|
|
|
data.AddChild(childData);
|
|
|
|
|
}
|
|
|
|
|
return data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void TryLoadWireframeMaterial()
|
|
|
|
|
{
|
|
|
|
|
if (_wireframeMat == null)
|
|
|
|
|
{
|
|
|
|
|
_wireframeMat = Resources.Load<Material>("SHI/Shader/BasicWireframe");
|
|
|
|
|
if (_wireframeMat == null)
|
|
|
|
|
{
|
|
|
|
|
Debug.LogWarning("BasicWireframe material not found at Resources/SHI/Shader/BasicWireframe. Wireframe will be disabled.");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ApplyWireframeMaterialToRoot()
|
|
|
|
|
{
|
|
|
|
|
if (_root == null || _wireframeMat == null) return;
|
|
|
|
|
var rends = _root.GetComponentsInChildren<Renderer>(true);
|
|
|
|
|
foreach (var r in rends)
|
|
|
|
|
{
|
|
|
|
|
if (r == null) continue;
|
|
|
|
|
var count = Mathf.Max(1, r.sharedMaterials.Length);
|
|
|
|
|
var arr = new Material[count];
|
|
|
|
|
for (int i = 0; i < count; i++) arr[i] = _wireframeMat;
|
|
|
|
|
r.sharedMaterials = arr;
|
|
|
|
|
}
|
|
|
|
|
_wireframeApplied = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void RestoreAllOriginalMaterials()
|
|
|
|
|
{
|
|
|
|
|
foreach (var kv in _matCache)
|
|
|
|
|
{
|
|
|
|
|
var (rends, originals) = kv.Value;
|
|
|
|
|
for (int i = 0; i < rends.Length; i++)
|
|
|
|
|
{
|
|
|
|
|
if (rends[i] == null) continue;
|
|
|
|
|
rends[i].sharedMaterials = originals[i];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_wireframeApplied = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private void EnsureCameraAndTargetTexture()
|
|
|
|
|
{
|
|
|
|
|
EnsureOutputImageUI();
|
|
|
|
|
|
|
|
|
|
if (_viewCamera == null)
|
|
|
|
|
{
|
|
|
|
|
// Create a world-space rig not parented to UI to avoid layout side effects
|
|
|
|
|
var rig = new GameObject("ModelDetailViewRig");
|
|
|
|
|
rig.layer = modelLayer;
|
|
|
|
|
rig.transform.SetParent(null, false);
|
|
|
|
|
|
|
|
|
|
var camGo = new GameObject("ModelDetailViewCamera");
|
|
|
|
|
camGo.layer = modelLayer;
|
|
|
|
|
camGo.transform.SetParent(rig.transform, false);
|
|
|
|
|
_viewCamera = camGo.AddComponent<UnityEngine.Camera>();
|
|
|
|
|
_viewCamera.clearFlags = CameraClearFlags.SolidColor;
|
|
|
|
|
_viewCamera.backgroundColor = backgroundColor;
|
|
|
|
|
_viewCamera.nearClipPlane = 0.01f;
|
|
|
|
|
_viewCamera.farClipPlane = 5000f;
|
|
|
|
|
_viewCamera.cullingMask = (modelLayer >= 0 && modelLayer <= 31) ? (1 << modelLayer) : ~0;
|
|
|
|
|
// Prevent drawing to main display when no RT assigned
|
|
|
|
|
_viewCamera.targetDisplay = 1; // secondary display
|
|
|
|
|
_viewCamera.enabled = false; // enable only when RT is bound
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// optional default light (independent of UI)
|
|
|
|
|
if (createDefaultLight && _viewCamera.transform.parent != null && _viewCamera.transform.parent.Find("ModelDetailViewLight") == null)
|
|
|
|
|
{
|
|
|
|
|
var lightGo = new GameObject("ModelDetailViewLight");
|
|
|
|
|
lightGo.layer = modelLayer;
|
|
|
|
|
lightGo.transform.SetParent(_viewCamera.transform.parent, false);
|
|
|
|
|
lightGo.transform.localPosition = Vector3.zero;
|
|
|
|
|
lightGo.transform.localRotation = Quaternion.Euler(50f, -30f, 0f);
|
|
|
|
|
var light = lightGo.AddComponent<Light>();
|
|
|
|
|
light.type = LightType.Directional;
|
|
|
|
|
light.intensity = 1.1f;
|
|
|
|
|
light.shadows = LightShadows.Soft;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
EnsureRenderTargetSize();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void EnsureOutputImageUI()
|
|
|
|
|
{
|
|
|
|
|
var selfRt = GetComponent<RectTransform>();
|
|
|
|
|
if (autoApplyRectMask && selfRt != null && selfRt.GetComponent<RectMask2D>() == null)
|
|
|
|
|
{
|
|
|
|
|
selfRt.gameObject.AddComponent<RectMask2D>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (outputImage == null && autoCreateOutputImage)
|
|
|
|
|
{
|
|
|
|
|
var go = new GameObject("OutputImage", typeof(RectTransform), typeof(RawImage));
|
|
|
|
|
go.transform.SetParent(transform, false);
|
|
|
|
|
outputImage = go.GetComponent<RawImage>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (outputImage != null)
|
|
|
|
|
{
|
|
|
|
|
var rt = outputImage.rectTransform;
|
|
|
|
|
rt.anchorMin = Vector2.zero;
|
|
|
|
|
rt.anchorMax = Vector2.one;
|
|
|
|
|
rt.offsetMin = Vector2.zero;
|
|
|
|
|
rt.offsetMax = Vector2.zero;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void EnsureRenderTargetSize()
|
|
|
|
|
{
|
|
|
|
|
if (_viewCamera == null)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (outputImage == null)
|
|
|
|
|
{
|
|
|
|
|
// No output target; keep camera disabled to avoid drawing to display
|
|
|
|
|
_viewCamera.enabled = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
var rtf = outputImage.rectTransform;
|
|
|
|
|
int w = Mathf.Max(64, Mathf.RoundToInt(rtf.rect.width));
|
|
|
|
|
int h = Mathf.Max(64, Mathf.RoundToInt(rtf.rect.height));
|
|
|
|
|
if (_rt == null || _rt.width != w || _rt.height != h)
|
|
|
|
|
{
|
|
|
|
|
// release old
|
|
|
|
|
if (_rt != null)
|
|
|
|
|
{
|
|
|
|
|
if (_viewCamera.targetTexture == _rt) _viewCamera.targetTexture = null;
|
|
|
|
|
_rt.Release();
|
|
|
|
|
Destroy(_rt);
|
|
|
|
|
}
|
|
|
|
|
// create new
|
|
|
|
|
_rt = new RenderTexture(w, h, 24, RenderTextureFormat.ARGB32)
|
|
|
|
|
{
|
|
|
|
|
name = "ModelDetailViewRT",
|
|
|
|
|
antiAliasing = 2
|
|
|
|
|
};
|
|
|
|
|
_viewCamera.targetTexture = _rt;
|
|
|
|
|
outputImage.texture = _rt;
|
|
|
|
|
// enable camera only when RT is assigned
|
|
|
|
|
_viewCamera.enabled = true;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// ensure binding and enable
|
|
|
|
|
if (_viewCamera.targetTexture != _rt)
|
|
|
|
|
{
|
|
|
|
|
_viewCamera.targetTexture = _rt;
|
|
|
|
|
}
|
|
|
|
|
if (!_viewCamera.enabled) _viewCamera.enabled = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected void OnRectTransformDimensionsChange()
|
|
|
|
|
{
|
|
|
|
|
// Keep RT in sync with UI size changes (e.g., parent resized)
|
|
|
|
|
EnsureRenderTargetSize();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Allow external toggle
|
|
|
|
|
public void SetWireframe(bool on)
|
|
|
|
|
{
|
|
|
|
|
wireframeMode = on;
|
|
|
|
|
TryLoadWireframeMaterial();
|
|
|
|
|
if (_wireframeMat != null)
|
|
|
|
|
{
|
|
|
|
|
if (on) ApplyWireframeMaterialToRoot(); else RestoreAllOriginalMaterials();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
Debug.LogWarning("Wireframe material not found. Wireframe toggle ignored.");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void SetLayerRecursive(GameObject go, int layer)
|
|
|
|
|
{
|
|
|
|
|
if (layer < 0 || layer > 31) return;
|
|
|
|
|
go.layer = layer;
|
|
|
|
|
foreach (Transform c in go.transform) SetLayerRecursive(c.gameObject, layer);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static Bounds CalculateBounds(GameObject root)
|
|
|
|
|
{
|
|
|
|
|
var rends = root.GetComponentsInChildren<Renderer>(true);
|
|
|
|
|
var has = false;
|
|
|
|
|
var bounds = new Bounds(root.transform.position, Vector3.zero);
|
|
|
|
|
foreach (var r in rends)
|
|
|
|
|
{
|
|
|
|
|
if (r == null) continue;
|
|
|
|
|
if (!has) { bounds = r.bounds; has = true; }
|
|
|
|
|
else bounds.Encapsulate(r.bounds);
|
|
|
|
|
}
|
|
|
|
|
if (!has) bounds = new Bounds(root.transform.position, Vector3.one);
|
|
|
|
|
return bounds;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void FrameToBounds(Bounds b)
|
|
|
|
|
{
|
|
|
|
|
if (_viewCamera == null) return;
|
|
|
|
|
var center = b.center;
|
|
|
|
|
var extents = b.extents;
|
|
|
|
|
float radius = Mathf.Max(extents.x, Mathf.Max(extents.y, Mathf.Max(extents.z, 0.001f)));
|
|
|
|
|
float fovRad = _viewCamera.fieldOfView * Mathf.Deg2Rad;
|
|
|
|
|
float dist = radius / Mathf.Sin(fovRad * 0.5f);
|
|
|
|
|
dist = Mathf.Clamp(dist, 1f, 1e4f);
|
|
|
|
|
_viewCamera.transform.position = center + new Vector3(1, 0.5f, 1).normalized * dist;
|
|
|
|
|
_viewCamera.transform.LookAt(center);
|
2025-11-14 17:02:38 +09:00
|
|
|
//_viewCamera.nearClipPlane = Mathf.Max(0.01f, dist - radius * 2f);
|
|
|
|
|
//_viewCamera.farClipPlane = dist + radius * 4f;
|
2025-11-13 20:16:25 +09:00
|
|
|
|
|
|
|
|
_orbitTarget = center;
|
|
|
|
|
_orbitDistance = Vector3.Distance(_viewCamera.transform.position, _orbitTarget);
|
|
|
|
|
var dir = (_viewCamera.transform.position - _orbitTarget).normalized;
|
|
|
|
|
_yaw = Mathf.Atan2(dir.x, dir.z) * Mathf.Rad2Deg;
|
|
|
|
|
_pitch = Mathf.Asin(dir.y) * Mathf.Rad2Deg;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private string BuildFullPath(Transform node, Transform root)
|
|
|
|
|
{
|
|
|
|
|
var stack = new Stack<string>();
|
|
|
|
|
var current = node;
|
|
|
|
|
while (current != null && current != root)
|
|
|
|
|
{
|
|
|
|
|
stack.Push(current.name);
|
|
|
|
|
current = current.parent;
|
|
|
|
|
}
|
|
|
|
|
return "/" + string.Join('/', stack);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void CacheOriginalMaterials(GameObject go)
|
|
|
|
|
{
|
|
|
|
|
var rends = go.GetComponentsInChildren<Renderer>(true);
|
|
|
|
|
if (rends.Length == 0) return;
|
|
|
|
|
var originals = new UnityEngine.Material[rends.Length][];
|
|
|
|
|
for (int i = 0; i < rends.Length; i++) originals[i] = rends[i].sharedMaterials;
|
|
|
|
|
_matCache[go] = (rends, originals);
|
|
|
|
|
}
|
2025-11-12 16:48:34 +09:00
|
|
|
|
|
|
|
|
public void FocusItem(TreeListItemData data)
|
|
|
|
|
{
|
|
|
|
|
if (data == null) return;
|
2025-11-13 20:16:25 +09:00
|
|
|
FocusItemById(data.Id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void FocusItemById(Guid id)
|
|
|
|
|
{
|
|
|
|
|
_focusedId = id;
|
|
|
|
|
if (_idToObject.TryGetValue(id, out var go))
|
|
|
|
|
{
|
|
|
|
|
Highlight(go, true);
|
|
|
|
|
Debug.Log($"ModelDetailView.FocusItemById: {go.name}");
|
|
|
|
|
_orbitTarget = go.transform.position;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void UnfocusItem()
|
|
|
|
|
{
|
|
|
|
|
if (_focusedId.HasValue && _idToObject.TryGetValue(_focusedId.Value, out var go))
|
|
|
|
|
{
|
|
|
|
|
Highlight(go, false);
|
|
|
|
|
}
|
|
|
|
|
_focusedId = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void SetVisibility(Guid id, bool on)
|
|
|
|
|
{
|
|
|
|
|
Debug.Log($"ModelDetailView.SetVisibility: id={id} on={on}");
|
|
|
|
|
if (_idToObject.TryGetValue(id, out var go))
|
|
|
|
|
{
|
|
|
|
|
go.SetActive(on);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Common color property names across pipelines/shaders
|
|
|
|
|
private static readonly string[] ColorProps = new[] {
|
|
|
|
|
"_WireframeColor", "_WireColor", // wireframe shader variants
|
|
|
|
|
"_BaseColor", "_Color", "_LineColor", "_TintColor"
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private bool TrySetColor(Material mat, Color c)
|
|
|
|
|
{
|
|
|
|
|
for (int i = 0; i < ColorProps.Length; i++)
|
|
|
|
|
{
|
|
|
|
|
var prop = ColorProps[i];
|
|
|
|
|
if (mat != null && mat.HasProperty(prop)) { mat.SetColor(prop, c); return true; }
|
|
|
|
|
}
|
|
|
|
|
// fallback to material.color if supported
|
|
|
|
|
try { mat.color = c; return true; } catch { /* ignore */ }
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool TryGetColor(Material mat, out Color c)
|
|
|
|
|
{
|
|
|
|
|
for (int i = 0; i < ColorProps.Length; i++)
|
|
|
|
|
{
|
|
|
|
|
var prop = ColorProps[i];
|
|
|
|
|
if (mat != null && mat.HasProperty(prop)) { c = mat.GetColor(prop); return true; }
|
|
|
|
|
}
|
|
|
|
|
try { c = mat.color; return true; } catch { /* ignore */ }
|
|
|
|
|
c = ColorUtil.FromHex("#888888"); return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool TryGetDefaultWireColor(out Color c)
|
|
|
|
|
{
|
|
|
|
|
if (_wireframeMat != null && TryGetColor(_wireframeMat, out c)) return true;
|
|
|
|
|
c = ColorUtil.FromHex("#888888"); return false;
|
2025-11-12 16:48:34 +09:00
|
|
|
}
|
|
|
|
|
|
2025-11-13 20:16:25 +09:00
|
|
|
private void Highlight(GameObject go, bool on)
|
2025-11-12 16:48:34 +09:00
|
|
|
{
|
2025-11-13 20:16:25 +09:00
|
|
|
if (!_matCache.TryGetValue(go, out var tuple)) return;
|
|
|
|
|
var (rends, originals) = tuple;
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < rends.Length; i++)
|
|
|
|
|
{
|
|
|
|
|
var r = rends[i];
|
|
|
|
|
if (r == null) continue;
|
|
|
|
|
|
|
|
|
|
// Work on instantiated materials to avoid touching shared assets
|
|
|
|
|
var mats = r.materials;
|
|
|
|
|
for (int m = 0; m < mats.Length; m++)
|
|
|
|
|
{
|
|
|
|
|
if (on)
|
|
|
|
|
{
|
|
|
|
|
// set to highlight color regardless of shader
|
|
|
|
|
TrySetColor(mats[m], ColorUtil.FromHex("#888814"));
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// When in wireframe mode (material replaced) restore to default wireframe color.
|
|
|
|
|
bool matIsWire = mats[m] != null && (mats[m].HasProperty("_WireframeColor") || mats[m].HasProperty("_WireColor"));
|
|
|
|
|
if (_wireframeApplied || matIsWire)
|
|
|
|
|
{
|
|
|
|
|
Color baseWire;
|
|
|
|
|
if (TryGetDefaultWireColor(out baseWire))
|
|
|
|
|
TrySetColor(mats[m], baseWire);
|
|
|
|
|
else
|
|
|
|
|
TrySetColor(mats[m], ColorUtil.FromHex("#888888"));
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// restore to original color if known; otherwise white
|
|
|
|
|
Color orig;
|
|
|
|
|
if (i < originals.Length && m < originals[i].Length && originals[i][m] != null && TryGetColor(originals[i][m], out orig))
|
|
|
|
|
TrySetColor(mats[m], orig);
|
|
|
|
|
else
|
|
|
|
|
TrySetColor(mats[m], ColorUtil.FromHex("#888888"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
r.materials = mats;
|
|
|
|
|
}
|
2025-11-12 16:48:34 +09:00
|
|
|
}
|
|
|
|
|
|
2025-11-13 20:16:25 +09:00
|
|
|
|
2025-11-12 16:48:34 +09:00
|
|
|
public void RaiseSelected(TreeListItemData data)
|
|
|
|
|
{
|
|
|
|
|
OnItemSelected?.Invoke(data);
|
|
|
|
|
}
|
2025-11-13 20:16:25 +09:00
|
|
|
|
|
|
|
|
private void Update()
|
|
|
|
|
{
|
|
|
|
|
if (_viewCamera == null) return;
|
|
|
|
|
|
|
|
|
|
// 영역 히트테스트
|
|
|
|
|
if (outputImage != null)
|
|
|
|
|
{
|
|
|
|
|
var cam = outputImage.canvas != null && outputImage.canvas.renderMode != RenderMode.ScreenSpaceOverlay
|
|
|
|
|
? outputImage.canvas.worldCamera
|
|
|
|
|
: null;
|
|
|
|
|
if (!RectTransformUtility.RectangleContainsScreenPoint(outputImage.rectTransform, Input.mousePosition, cam))
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
var selfRt = GetComponent<RectTransform>();
|
|
|
|
|
if (selfRt != null)
|
|
|
|
|
{
|
|
|
|
|
var parentCanvas = selfRt.GetComponentInParent<Canvas>();
|
|
|
|
|
UnityEngine.Camera uiCam = null;
|
|
|
|
|
if (parentCanvas != null && parentCanvas.renderMode != RenderMode.ScreenSpaceOverlay)
|
|
|
|
|
uiCam = parentCanvas.worldCamera;
|
|
|
|
|
if (!RectTransformUtility.RectangleContainsScreenPoint(selfRt, Input.mousePosition, uiCam))
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 드래그 상태 전이 처리 (점프 방지)
|
|
|
|
|
if (Input.GetMouseButtonDown(2)) { _mmbDragging = true; _mmbLastPos = Input.mousePosition; }
|
|
|
|
|
if (Input.GetMouseButtonUp(2)) { _mmbDragging = false; }
|
2025-11-14 17:02:38 +09:00
|
|
|
if (Input.GetMouseButtonDown(1)) { _rmbDragging = true; _rmbStartPos = Input.mousePosition; _yawStart = _yaw; _pitchStart = _pitch; if (_root != null) { _modelStartRot = _root.transform.rotation; _modelStartPos = _root.transform.position; } _rmbPivot = _orbitTarget; }
|
2025-11-13 20:16:25 +09:00
|
|
|
if (Input.GetMouseButtonUp(1)) { _rmbDragging = false; }
|
|
|
|
|
|
|
|
|
|
// 가운데 버튼: 모델 이동 (좌/우/상/하) - 카메라 기준, 픽셀 델타 기반
|
|
|
|
|
if (_mmbDragging && _root != null)
|
|
|
|
|
{
|
|
|
|
|
Vector3 cur = Input.mousePosition;
|
|
|
|
|
Vector2 dp = (Vector2)(cur - _mmbLastPos);
|
|
|
|
|
_mmbLastPos = cur;
|
|
|
|
|
// 뷰 사각 픽셀 크기 구하기
|
|
|
|
|
float wPix, hPix;
|
|
|
|
|
if (outputImage != null)
|
|
|
|
|
{ var r = outputImage.rectTransform.rect; wPix = Mathf.Max(1f, r.width); hPix = Mathf.Max(1f, r.height); }
|
|
|
|
|
else
|
|
|
|
|
{ var rt = GetComponent<RectTransform>(); var r = (rt != null) ? rt.rect : new Rect(0, 0, Screen.width, Screen.height); wPix = Mathf.Max(1f, r.width); hPix = Mathf.Max(1f, r.height); }
|
|
|
|
|
// 카메라에서의 월드/픽셀 스케일(거리 기반)
|
|
|
|
|
float halfV = Mathf.Tan(_viewCamera.fieldOfView * Mathf.Deg2Rad * 0.5f) * _orbitDistance;
|
|
|
|
|
float halfH = halfV * _viewCamera.aspect;
|
|
|
|
|
float worldPerPixelX = (halfH * 2f) / wPix * panSpeed;
|
|
|
|
|
float worldPerPixelY = (halfV * 2f) / hPix * panSpeed;
|
|
|
|
|
Vector3 deltaModel = _viewCamera.transform.right * (dp.x * worldPerPixelX) + _viewCamera.transform.up * (dp.y * worldPerPixelY);
|
|
|
|
|
_root.transform.position += deltaModel;
|
|
|
|
|
_orbitTarget += deltaModel;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 마우스 휠: 모델 전/후 이동(카메라 기준)
|
|
|
|
|
float scroll = Input.GetAxis("Mouse ScrollWheel");
|
|
|
|
|
if (Mathf.Abs(scroll) > 1e-5f && _root != null)
|
|
|
|
|
{
|
|
|
|
|
var forward = _viewCamera.transform.forward;
|
|
|
|
|
Vector3 deltaZ = forward * (-scroll * zoomSpeed);
|
|
|
|
|
_root.transform.position += deltaZ;
|
|
|
|
|
_orbitTarget += deltaZ;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-14 17:02:38 +09:00
|
|
|
// 오른쪽 버튼: 모델 회전 (카메라 기준 yaw/pitch) - 모델 중심(_orbitTarget) 기준으로 회전
|
2025-11-13 20:16:25 +09:00
|
|
|
if (_rmbDragging && _root != null)
|
|
|
|
|
{
|
|
|
|
|
Vector3 cur = Input.mousePosition;
|
|
|
|
|
Vector2 dpAbs = (Vector2)(cur - _rmbStartPos);
|
|
|
|
|
// 반전: 좌우/상하 모두 반대 방향으로 회전
|
|
|
|
|
float yaw = -dpAbs.x * rotateDegPerPixel; // 좌우 반전
|
|
|
|
|
float pitch = dpAbs.y * rotateDegPerPixel; // 위아래 반전
|
2025-11-14 17:02:38 +09:00
|
|
|
|
|
|
|
|
// 카메라 기준 축으로 회전 행렬 구성
|
2025-11-13 20:16:25 +09:00
|
|
|
Quaternion yawQ = Quaternion.AngleAxis(yaw, _viewCamera.transform.up);
|
|
|
|
|
Quaternion pitchQ = Quaternion.AngleAxis(pitch, _viewCamera.transform.right);
|
2025-11-14 17:02:38 +09:00
|
|
|
Quaternion r = yawQ * pitchQ;
|
|
|
|
|
|
|
|
|
|
// 피벗(_rmbPivot, 보통 모델 중심) 기준으로 위치+회전 동시 적용
|
|
|
|
|
Vector3 startVec = _modelStartPos - _rmbPivot;
|
|
|
|
|
_root.transform.position = _rmbPivot + r * startVec;
|
|
|
|
|
_root.transform.rotation = r * _modelStartRot;
|
2025-11-13 20:16:25 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void UpdateOrbitPose()
|
|
|
|
|
{
|
|
|
|
|
if (_viewCamera == null) return;
|
|
|
|
|
_orbitDistance = Mathf.Max(0.01f, _orbitDistance);
|
|
|
|
|
var rot = Quaternion.Euler(_pitch, _yaw, 0f);
|
|
|
|
|
var offset = rot * new Vector3(0, 0, -_orbitDistance);
|
|
|
|
|
_viewCamera.transform.position = _orbitTarget + offset;
|
|
|
|
|
_viewCamera.transform.LookAt(_orbitTarget);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Dispose()
|
|
|
|
|
{
|
|
|
|
|
foreach (var kv in _matCache)
|
|
|
|
|
{
|
|
|
|
|
var (rends, originals) = kv.Value;
|
|
|
|
|
for (int i = 0; i < rends.Length; i++)
|
|
|
|
|
{
|
|
|
|
|
if (rends[i] == null) continue;
|
|
|
|
|
var mats = rends[i].materials;
|
|
|
|
|
for (int m = 0; m < mats.Length; m++)
|
|
|
|
|
{
|
|
|
|
|
if (mats[m] != null) UnityEngine.Object.Destroy(mats[m]);
|
|
|
|
|
}
|
|
|
|
|
rends[i].materials = originals[i];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_matCache.Clear();
|
|
|
|
|
_idToObject.Clear();
|
|
|
|
|
if (_root != null) UnityEngine.Object.Destroy(_root);
|
|
|
|
|
_root = null;
|
|
|
|
|
_focusedId = null;
|
|
|
|
|
_wireframeApplied = false;
|
|
|
|
|
|
|
|
|
|
if (_viewCamera != null)
|
|
|
|
|
{
|
|
|
|
|
// detach RT and keep camera disabled to avoid replacing main display
|
|
|
|
|
if (_rt != null && _viewCamera.targetTexture == _rt)
|
|
|
|
|
{
|
|
|
|
|
_viewCamera.targetTexture = null;
|
|
|
|
|
}
|
|
|
|
|
_viewCamera.enabled = false;
|
|
|
|
|
}
|
|
|
|
|
if (_rt != null)
|
|
|
|
|
{
|
|
|
|
|
_rt.Release();
|
|
|
|
|
Destroy(_rt);
|
|
|
|
|
_rt = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-12 12:28:17 +09:00
|
|
|
}
|
|
|
|
|
}
|