diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..19059d01 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,10 @@ +{ + "permissions": { + "allow": [ + "Bash(dir:*)", + "Bash(powershell -Command:*)" + ], + "deny": [], + "ask": [] + } +} diff --git a/Assets/Scenes/Sample/ShiPopupSample.cs b/Assets/Scenes/Sample/ShiPopupSample.cs index 26b9cae1..f1910b0e 100644 --- a/Assets/Scenes/Sample/ShiPopupSample.cs +++ b/Assets/Scenes/Sample/ShiPopupSample.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using UnityEngine; using UnityEngine.UI; +using UVC.GLTF; /// /// 샘플 장면 드라이버: 버튼 클릭으로 SHI BlockDetail 모달을 생성/표시하고, @@ -24,6 +25,18 @@ public class ShiPopupSample : MonoBehaviour [SerializeField] private Button openNWModalButton; + + [SerializeField] + private Button loadGLTFMultiLODButton; + + [SerializeField] + private Button loadGLTFButton; + + [SerializeField] + private Transform lodRoot; + + [SerializeField] + private Transform lodRoot2; private ISOPModal? isopModal; private NWModal? nwModal; @@ -65,6 +78,30 @@ public class ShiPopupSample : MonoBehaviour }); } + + if(loadGLTFMultiLODButton != null) + { + loadGLTFMultiLODButton.onClick.AddListener(() => + { + string sa = Application.streamingAssetsPath; + GLTFImporter.ImportWithLOD(new List { + Path.Combine(sa, "model_lod0.glb"), + Path.Combine(sa, "model_lod1.glb"), + Path.Combine(sa, "model_lod2.glb"), + Path.Combine(sa, "model_lod3.glb"), + }, lodRoot).Forget(); + }); + } + + + if(loadGLTFButton != null) + { + loadGLTFButton.onClick.AddListener(() => + { + string sa = Application.streamingAssetsPath; + GLTFImporter.ImportFromFile(Path.Combine(sa, "model_with_lod.glb"), lodRoot2).Forget(); + }); + } } @@ -87,8 +124,8 @@ public class ShiPopupSample : MonoBehaviour Debug.Log($"Loaded blockDetailModal:{isopModal}"); await isopModal.LoadData(new List { - Path.Combine(sa, "B11TC.glb"), - Path.Combine(sa, "B16VC.glb"), + Path.Combine(sa, "model_with_lod.glb"), + Path.Combine(sa, "model_with_lod2.glb"), Path.Combine(sa, "E11VC.glb"), Path.Combine(sa, "E41UC.glb"), Path.Combine(sa, "M11UC.glb"), diff --git a/Assets/Scenes/Sample/ShiPopupSample.unity b/Assets/Scenes/Sample/ShiPopupSample.unity index 71d32667..8523fb87 100644 --- a/Assets/Scenes/Sample/ShiPopupSample.unity +++ b/Assets/Scenes/Sample/ShiPopupSample.unity @@ -119,6 +119,68 @@ NavMeshSettings: debug: m_Flags: 0 m_NavMeshData: {fileID: 0} +--- !u!1 &279219350 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 279219351} + m_Layer: 0 + m_Name: LODRoot + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &279219351 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 279219350} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &519133180 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 519133181} + m_Layer: 0 + m_Name: LODRoot2 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &519133181 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 519133180} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &701037396 GameObject: m_ObjectHideFlags: 0 @@ -289,6 +351,10 @@ MonoBehaviour: nwModalPrefab: {fileID: 4549899540058300928, guid: 5295f7b9a5b84ae4c8230c52ebdabef2, type: 3} openISOPModalButton: {fileID: 1154598113} openNWModalButton: {fileID: 2055194488} + loadGLTFMultiLODButton: {fileID: 1610110302} + loadGLTFButton: {fileID: 1860861870} + lodRoot: {fileID: 279219351} + lodRoot2: {fileID: 519133181} --- !u!4 &717482005 Transform: m_ObjectHideFlags: 0 @@ -304,6 +370,142 @@ Transform: m_Children: [] m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &860110276 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 860110277} + - component: {fileID: 860110279} + - component: {fileID: 860110278} + m_Layer: 5 + m_Name: Text (TMP) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &860110277 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 860110276} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1610110301} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &860110278 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 860110276} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_text: loadGLTFMultiLOD + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4281479730 + m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_StyleSheet: {fileID: 0} + m_TextStyleHashCode: -1183493901 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_fontSize: 14 + m_fontSizeBase: 14 + m_fontWeight: 400 + m_enableAutoSizing: 0 + m_fontSizeMin: 18 + m_fontSizeMax: 72 + m_fontStyle: 0 + m_HorizontalAlignment: 2 + m_VerticalAlignment: 512 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_TextWrappingMode: 1 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 0 + m_ActiveFontFeatures: 6e72656b + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 1 + m_EmojiFallbackSupport: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 1 + m_isCullingEnabled: 0 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_IsTextObjectScaleStatic: 0 + m_VertexBufferAutoSizeReduction: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 0, y: 0, z: 0, w: 0} + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_hasFontAssetChanged: 0 + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} +--- !u!222 &860110279 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 860110276} + m_CullTransparentMesh: 1 --- !u!1 &1154598111 GameObject: m_ObjectHideFlags: 0 @@ -425,6 +627,142 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1154598111} m_CullTransparentMesh: 1 +--- !u!1 &1299547236 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1299547237} + - component: {fileID: 1299547239} + - component: {fileID: 1299547238} + m_Layer: 5 + m_Name: Text (TMP) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1299547237 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1299547236} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1860861869} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1299547238 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1299547236} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_text: loadGLTFButton + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4281479730 + m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_StyleSheet: {fileID: 0} + m_TextStyleHashCode: -1183493901 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_fontSize: 14 + m_fontSizeBase: 14 + m_fontWeight: 400 + m_enableAutoSizing: 0 + m_fontSizeMin: 18 + m_fontSizeMax: 72 + m_fontStyle: 0 + m_HorizontalAlignment: 2 + m_VerticalAlignment: 512 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_TextWrappingMode: 1 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 0 + m_ActiveFontFeatures: 6e72656b + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 1 + m_EmojiFallbackSupport: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 1 + m_isCullingEnabled: 0 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_IsTextObjectScaleStatic: 0 + m_VertexBufferAutoSizeReduction: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 0, y: 0, z: 0, w: 0} + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_hasFontAssetChanged: 0 + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} +--- !u!222 &1299547239 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1299547236} + m_CullTransparentMesh: 1 --- !u!1 &1304768789 GameObject: m_ObjectHideFlags: 0 @@ -521,6 +859,8 @@ RectTransform: m_Children: - {fileID: 1154598112} - {fileID: 2055194487} + - {fileID: 1610110301} + - {fileID: 1860861869} m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} @@ -800,6 +1140,127 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1520929354} m_CullTransparentMesh: 1 +--- !u!1 &1610110300 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1610110301} + - component: {fileID: 1610110304} + - component: {fileID: 1610110303} + - component: {fileID: 1610110302} + m_Layer: 5 + m_Name: loadGLTFMultiLODButton + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1610110301 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1610110300} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 860110277} + m_Father: {fileID: 1304768793} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 0, y: -86.8} + m_SizeDelta: {x: 160, y: 30} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1610110302 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1610110300} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 1610110303} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!114 &1610110303 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1610110300} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &1610110304 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1610110300} + m_CullTransparentMesh: 1 --- !u!1 &1788944821 GameObject: m_ObjectHideFlags: 0 @@ -879,6 +1340,127 @@ Transform: m_Children: [] m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1860861868 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1860861869} + - component: {fileID: 1860861872} + - component: {fileID: 1860861871} + - component: {fileID: 1860861870} + m_Layer: 5 + m_Name: loadGLTFButton + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1860861869 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1860861868} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1299547237} + m_Father: {fileID: 1304768793} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 0, y: -125.6} + m_SizeDelta: {x: 160, y: 30} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1860861870 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1860861868} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 1860861871} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!114 &1860861871 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1860861868} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &1860861872 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1860861868} + m_CullTransparentMesh: 1 --- !u!1 &1977662939 GameObject: m_ObjectHideFlags: 0 @@ -1127,6 +1709,8 @@ SceneRoots: m_Roots: - {fileID: 701037399} - {fileID: 1977662941} - - {fileID: 717482005} - {fileID: 1304768793} - {fileID: 1788944824} + - {fileID: 717482005} + - {fileID: 279219351} + - {fileID: 519133181} diff --git a/Assets/Scripts/SHI/modal/ISOP/ISOPModelView.cs b/Assets/Scripts/SHI/modal/ISOP/ISOPModelView.cs index c0808ee6..df45a9c3 100644 --- a/Assets/Scripts/SHI/modal/ISOP/ISOPModelView.cs +++ b/Assets/Scripts/SHI/modal/ISOP/ISOPModelView.cs @@ -191,7 +191,8 @@ namespace SHI.Modal.ISOP Debug.Log($"ISOPModelView.LoadModelAsync: Loading {path}"); var gltf = new GltfImport(); - var success = await gltf.Load(path, new ImportSettings(), ct); + var importSettings = new ImportSettings(); + var success = await gltf.Load(path, importSettings, ct); if (!success) { Debug.LogError($"glTFast Load failed: {path}"); @@ -425,7 +426,7 @@ namespace SHI.Modal.ISOP if (_idToObject.TryGetValue(id, out var go)) { Debug.Log($"Exporting object: {go.name}"); - UVC.GLTF.GLTFExporter.ExportNodeByExplorer(go); + UVC.GLTF.Export.GLTFExporter.ExportNodeByExplorer(go); } } diff --git a/Assets/Scripts/SHI/modal/NW/NWModelVIew.cs b/Assets/Scripts/SHI/modal/NW/NWModelVIew.cs index 34459fd5..f78f821a 100644 --- a/Assets/Scripts/SHI/modal/NW/NWModelVIew.cs +++ b/Assets/Scripts/SHI/modal/NW/NWModelVIew.cs @@ -427,7 +427,7 @@ namespace SHI.Modal.NW { if (_idToObject.TryGetValue(id, out var go)) { - UVC.GLTF.GLTFExporter.ExportNodeByExplorer(go); + UVC.GLTF.Export.GLTFExporter.ExportNodeByExplorer(go); } } diff --git a/Assets/Scripts/UVC/GLTF/Export.meta b/Assets/Scripts/UVC/GLTF/Export.meta new file mode 100644 index 00000000..b7f4f915 --- /dev/null +++ b/Assets/Scripts/UVC/GLTF/Export.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c5f5aa5fcd3aa0a4eb2a746675279e9c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/UVC/GLTF/GLTFExporter.Runtime.asmdef b/Assets/Scripts/UVC/GLTF/Export/GLTFExporter.Runtime.asmdef similarity index 100% rename from Assets/Scripts/UVC/GLTF/GLTFExporter.Runtime.asmdef rename to Assets/Scripts/UVC/GLTF/Export/GLTFExporter.Runtime.asmdef diff --git a/Assets/Scripts/UVC/GLTF/GLTFExporter.Runtime.asmdef.meta b/Assets/Scripts/UVC/GLTF/Export/GLTFExporter.Runtime.asmdef.meta similarity index 100% rename from Assets/Scripts/UVC/GLTF/GLTFExporter.Runtime.asmdef.meta rename to Assets/Scripts/UVC/GLTF/Export/GLTFExporter.Runtime.asmdef.meta diff --git a/Assets/Scripts/UVC/GLTF/GLTFExporter.cs b/Assets/Scripts/UVC/GLTF/Export/GLTFExporter.cs similarity index 99% rename from Assets/Scripts/UVC/GLTF/GLTFExporter.cs rename to Assets/Scripts/UVC/GLTF/Export/GLTFExporter.cs index 1de3c032..fa669253 100644 --- a/Assets/Scripts/UVC/GLTF/GLTFExporter.cs +++ b/Assets/Scripts/UVC/GLTF/Export/GLTFExporter.cs @@ -27,7 +27,7 @@ using GltfPrimitive = GLTFast.Schema.MeshPrimitive; using GltfTexture = GLTFast.Schema.Texture; using GltfTextureInfo = GLTFast.Schema.TextureInfo; -namespace UVC.GLTF +namespace UVC.GLTF.Export { /// diff --git a/Assets/Scripts/UVC/GLTF/GLTFExporter.cs.meta b/Assets/Scripts/UVC/GLTF/Export/GLTFExporter.cs.meta similarity index 100% rename from Assets/Scripts/UVC/GLTF/GLTFExporter.cs.meta rename to Assets/Scripts/UVC/GLTF/Export/GLTFExporter.cs.meta diff --git a/Assets/Scripts/UVC/GLTF/GLTFImporter.cs b/Assets/Scripts/UVC/GLTF/GLTFImporter.cs new file mode 100644 index 00000000..172d550c --- /dev/null +++ b/Assets/Scripts/UVC/GLTF/GLTFImporter.cs @@ -0,0 +1,137 @@ +#nullable enable +using GLTFast; +using UnityEngine; +using System; +using System.Threading; +using Cysharp.Threading.Tasks; +using System.Collections.Generic; + +namespace UVC.GLTF +{ + public static class GLTFImporter + { + /// + /// glTF/glb 파일을 로드합니다. + /// + /// 파일 경로 + /// 부모 Transform + /// MSFT_lod 확장을 LODGroup으로 변환할지 여부 + public static async UniTask ImportFromFile(string filePath, Transform parentTransform) + { + if (string.IsNullOrEmpty(filePath)) return null; + + Debug.Log($"ImportFromFile {filePath}"); + var gltf = new GltfImport(); + var importSettings = new ImportSettings(); + + var success = await gltf.Load(filePath, importSettings); + + if (!success) + { + Debug.LogError($"glTFast Load failed: {filePath}"); + return null; + } + + // MSFT_lod 사용 시 커스텀 인스턴시에이터 사용 + GameObjectInstantiator instantiator; + // if (useMsftLod) + // { + // // glb/gltf 파일에서 MSFT_lod 정보 파싱 + // var lodInfos = MsftLodParser.ParseFromFile(filePath); + // Debug.Log($"ImportFromFile: Found {lodInfos.Count} LOD definitions in {filePath}"); + + // var lodInstantiator = new LODGameObjectInstantiator(gltf, parentTransform); + // lodInstantiator.SetLodInfo(lodInfos); + // instantiator = lodInstantiator; + // } + // else + // { + instantiator = new GameObjectInstantiator(gltf, parentTransform); + // } + + var sceneOk = await gltf.InstantiateMainSceneAsync(instantiator); + if (!sceneOk) + { + Debug.LogError($"InstantiateMainSceneAsync failed: {filePath}"); + return null; + } + + return instantiator.SceneTransform != null ? instantiator.SceneTransform.gameObject : null; + } + + public static async UniTask ImportWithLOD(List paths, Transform parentTransform) + { + if (paths == null || paths.Count == 0) return null; + + // 1. 부모 객체 생성 (여기에 LODGroup 컴포넌트가 붙습니다) + GameObject rootObject = new GameObject("Loaded_LOD_Model"); + rootObject.transform.position = Vector3.zero; + + bool successAll = true; + List loadedObjects = new List(); + + foreach (var path in paths) + { + if (string.IsNullOrEmpty(path)) continue; + + Debug.Log($"ImportFromWithLOD Loading {path}"); + var gltf = new GltfImport(); + var importSettings = new ImportSettings(); + var success = await gltf.Load(path, importSettings); + if (!success) + { + Debug.LogError($"glTFast Load failed: {path}"); + successAll = false; + break; + } + //path 이름만 + string lodName = System.IO.Path.GetFileNameWithoutExtension(path); + GameObject gameObject = new GameObject(lodName); + gameObject.transform.SetParent(rootObject.transform, false); + var instantiator = new GameObjectInstantiator(gltf, gameObject.transform); + var sceneOk = await gltf.InstantiateMainSceneAsync(instantiator); + if (!sceneOk) + { + Debug.LogError($"InstantiateMainSceneAsync failed: {path}"); + successAll = false; + break; + } + if(instantiator.SceneTransform != null){ + loadedObjects.Add(gameObject); + } + } + + if (successAll) + { + // 3. LODGroup 설정 + var lodGroup = rootObject.AddComponent(); + LOD[] lods = new LOD[loadedObjects.Count]; + float[] lodScreenPercentages = new float[] { 0.5f, 0.25f, 0.1f, 0f}; // 예시로 LOD 전환 비율 설정 + for (int i = 0; i < loadedObjects.Count; i++) + { + Renderer? renderer = loadedObjects[i].GetComponent(); + if(renderer == null) + { + renderer = loadedObjects[i].GetComponentInChildren(); + } + + float screenRelativeTransitionHeight = lodScreenPercentages[Mathf.Min(i, lodScreenPercentages.Length - 1)]; // LOD 전환 비율 설정 사용 + Debug.Log($"{loadedObjects[i].name} LOD {i}: screenRelativeTransitionHeight={screenRelativeTransitionHeight}"); + lods[i] = new LOD(screenRelativeTransitionHeight, new Renderer[]{renderer!}); + } + lodGroup.SetLODs(lods); + lodGroup.RecalculateBounds(); // 바운딩 박스 재계산 (필수) + rootObject.transform.SetParent(parentTransform, false); + return rootObject; + } + else + { + GameObject.Destroy(rootObject); + return null; + } + + + } + + } +} \ No newline at end of file diff --git a/Assets/Scripts/UVC/GLTF/GLTFImporter.cs.meta b/Assets/Scripts/UVC/GLTF/GLTFImporter.cs.meta new file mode 100644 index 00000000..274f2c19 --- /dev/null +++ b/Assets/Scripts/UVC/GLTF/GLTFImporter.cs.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 9737b1961c24c584fa071c051a55b85b +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/UVC/GLTF/LODGameObjectInstantiator.cs b/Assets/Scripts/UVC/GLTF/LODGameObjectInstantiator.cs new file mode 100644 index 00000000..26c3318c --- /dev/null +++ b/Assets/Scripts/UVC/GLTF/LODGameObjectInstantiator.cs @@ -0,0 +1,227 @@ +#nullable enable +using GLTFast; +using UnityEngine; +using System.Collections.Generic; +using System.Linq; + +namespace UVC.GLTF +{ + /// + /// GameObjectInstantiator를 확장하여 MSFT_lod 확장을 LODGroup으로 변환합니다. + /// 제대로 동작 않함. 수정 필요. + /// + public class LODGameObjectInstantiator : GameObjectInstantiator + { + // LOD 정보를 저장 (부모 노드 인덱스 -> LOD 노드 인덱스 리스트) + private readonly Dictionary> m_LodNodes = new(); + + // MSFT_screencoverage 값 저장 + private readonly Dictionary m_ScreenCoverages = new(); + + public LODGameObjectInstantiator(IGltfReadable gltf, Transform? parent) + : base(gltf, parent) + { + } + + /// + /// 외부에서 파싱된 LOD 정보를 설정합니다. + /// + public void SetLodInfo(List lodInfos) + { + m_LodNodes.Clear(); + m_ScreenCoverages.Clear(); + + foreach (var info in lodInfos) + { + m_LodNodes[(uint)info.NodeIndex] = info.LodNodeIds.Select(id => (uint)id).ToList(); + + if (info.ScreenCoverages != null) + { + m_ScreenCoverages[(uint)info.NodeIndex] = info.ScreenCoverages; + } + } + + Debug.Log($"LODGameObjectInstantiator: Set {m_LodNodes.Count} LOD nodes"); + } + + public override void EndScene(uint[] rootNodeIndices) + { + base.EndScene(rootNodeIndices); + + // LODGroup 적용 + ApplyLodGroups(); + } + + /// + /// MSFT_lod 정보를 기반으로 LODGroup을 생성합니다. + /// + private void ApplyLodGroups() + { + if (m_LodNodes.Count == 0) + { + Debug.Log("LODGameObjectInstantiator: No LOD nodes to apply"); + return; + } + + // m_Nodes는 protected이므로 직접 접근 가능 + if (m_Nodes == null) + { + Debug.LogWarning("LODGameObjectInstantiator: m_Nodes is null"); + return; + } + + Debug.Log($"LODGameObjectInstantiator: m_Nodes count = {m_Nodes.Count}, LOD definitions = {m_LodNodes.Count}"); + + foreach (var kvp in m_LodNodes) + { + uint parentNodeIndex = kvp.Key; + List lodNodeIndices = kvp.Value; + + if (!m_Nodes.TryGetValue(parentNodeIndex, out var parentGo)) + { + Debug.LogWarning($"LODGameObjectInstantiator: Parent node {parentNodeIndex} not found in m_Nodes"); + continue; + } + + // LOD 레벨 수집 (LOD0 = parent, LOD1+ = lodNodeIndices) + var allLodObjects = new List { parentGo }; + foreach (var lodIndex in lodNodeIndices) + { + if (m_Nodes.TryGetValue(lodIndex, out var lodGo)) + { + allLodObjects.Add(lodGo); + } + else + { + Debug.LogWarning($"LODGameObjectInstantiator: LOD node {lodIndex} not found in m_Nodes"); + } + } + + if (allLodObjects.Count < 2) + { + Debug.LogWarning($"LODGameObjectInstantiator: Not enough LOD objects for node {parentNodeIndex}"); + continue; + } + + // 먼저 각 LOD 객체의 렌더러를 수집 (부모 이동 전에!) + // 각 LOD는 다른 LOD 객체들을 제외하고 렌더러를 수집 + var lodRenderers = new List(); + for (int i = 0; i < allLodObjects.Count; i++) + { + var lodObject = allLodObjects[i]; + + // 이 LOD 객체를 제외한 다른 LOD 객체들을 제외 목록에 추가 + var excludeSet = new HashSet(); + for (int j = 0; j < allLodObjects.Count; j++) + { + if (i != j) + { + excludeSet.Add(allLodObjects[j]); + } + } + + var renderers = GetRenderersExcluding(lodObject, excludeSet); + lodRenderers.Add(renderers); + } + + // MSFT_screencoverage 값 사용 또는 기본값 계산 + float[] screenCoverages; + if (m_ScreenCoverages.TryGetValue(parentNodeIndex, out var coverage) && coverage.Length >= allLodObjects.Count) + { + screenCoverages = coverage; + } + else + { + // 기본 screen coverage 계산 (균등 분배) + screenCoverages = CalculateDefaultScreenCoverages(allLodObjects.Count); + } + + // LOD1+ 객체들을 LOD0의 자식으로 이동 + for (int i = 1; i < allLodObjects.Count; i++) + { + var lodObject = allLodObjects[i]; + if (lodObject.transform.parent != parentGo.transform) + { + // 월드 위치/회전 유지하면서 부모 변경 + lodObject.transform.SetParent(parentGo.transform, true); + } + } + + // LODGroup 생성 및 설정 + var lodGroup = parentGo.AddComponent(); + var lods = new LOD[allLodObjects.Count]; + + for (int i = 0; i < allLodObjects.Count; i++) + { + float screenRelativeHeight = screenCoverages[i]; + lods[i] = new LOD(screenRelativeHeight, lodRenderers[i]); + + Debug.Log($" LOD{i}: {lodRenderers[i].Length} renderers, screenHeight={screenRelativeHeight}"); + } + + lodGroup.SetLODs(lods); + lodGroup.RecalculateBounds(); + + Debug.Log($"LODGameObjectInstantiator: Applied LODGroup to '{parentGo.name}' with {allLodObjects.Count} LOD levels"); + } + } + + /// + /// GameObject와 자식들의 렌더러를 수집합니다. + /// excludeGameObjects에 포함된 GameObject와 그 자식들은 제외합니다. + /// + private Renderer[] GetRenderersExcluding(GameObject go, HashSet excludeGameObjects) + { + var renderers = new List(); + CollectRenderersRecursive(go.transform, renderers, excludeGameObjects); + return renderers.ToArray(); + } + + /// + /// 재귀적으로 렌더러를 수집하되, 제외 목록에 있는 GameObject는 건너뜁니다. + /// + private void CollectRenderersRecursive(Transform current, List renderers, HashSet excludeGameObjects) + { + // 현재 객체가 제외 목록에 있으면 자식도 모두 건너뜀 + if (excludeGameObjects.Contains(current.gameObject)) + { + return; + } + + // 현재 객체의 렌더러 추가 + var renderer = current.GetComponent(); + if (renderer != null) + { + renderers.Add(renderer); + } + + // 자식들 재귀 탐색 + foreach (Transform child in current) + { + CollectRenderersRecursive(child, renderers, excludeGameObjects); + } + } + + /// + /// 기본 screen coverage 값을 계산합니다. + /// + private float[] CalculateDefaultScreenCoverages(int lodCount) + { + // 일반적인 LOD 전환 비율 + // LOD0: 0.5 (50% 이상일 때) + // LOD1: 0.25 (25% 이상일 때) + // LOD2: 0.1 (10% 이상일 때) + // LOD3: 0 (0% 이상일 때) + // 등등... + + float[] lodScreenPercentages = new float[] { 0.5f, 0.25f, 0.1f, 0f}; + + var coverages = new float[lodCount]; + for (int i = 0; i < lodCount; i++) + { + coverages[i] = lodScreenPercentages[Mathf.Min(i, lodScreenPercentages.Length - 1)]; + } + return coverages; + } + } +} diff --git a/Assets/Scripts/UVC/GLTF/LODGameObjectInstantiator.cs.meta b/Assets/Scripts/UVC/GLTF/LODGameObjectInstantiator.cs.meta new file mode 100644 index 00000000..76461667 --- /dev/null +++ b/Assets/Scripts/UVC/GLTF/LODGameObjectInstantiator.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f5d3912849cbc414093b6265df85581d \ No newline at end of file diff --git a/Assets/Scripts/UVC/GLTF/MsftLodParser.cs b/Assets/Scripts/UVC/GLTF/MsftLodParser.cs new file mode 100644 index 00000000..3d8cf4d1 --- /dev/null +++ b/Assets/Scripts/UVC/GLTF/MsftLodParser.cs @@ -0,0 +1,232 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using UnityEngine; + +namespace UVC.GLTF +{ + /// + /// glTF/glb 파일에서 MSFT_lod 확장 정보를 파싱합니다. + /// GLTFast가 MSFT_lod를 지원하지 않으므로 직접 JSON을 파싱합니다. + /// + public static class MsftLodParser + { + /// + /// LOD 정보를 담는 클래스 + /// + public class LodInfo + { + public int NodeIndex { get; set; } + public int[] LodNodeIds { get; set; } = Array.Empty(); + public float[]? ScreenCoverages { get; set; } + } + + /// + /// glb 또는 gltf 파일에서 MSFT_lod 정보를 파싱합니다. + /// + public static List ParseFromFile(string filePath) + { + var result = new List(); + + if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath)) + { + return result; + } + + try + { + string json; + string extension = Path.GetExtension(filePath).ToLowerInvariant(); + + if (extension == ".glb") + { + json = ExtractJsonFromGlb(filePath); + } + else + { + json = File.ReadAllText(filePath); + } + + if (string.IsNullOrEmpty(json)) + { + return result; + } + + return ParseMsftLodFromJson(json); + } + catch (Exception ex) + { + Debug.LogError($"MsftLodParser: Failed to parse {filePath}: {ex.Message}"); + return result; + } + } + + /// + /// glb 파일에서 JSON 청크를 추출합니다. + /// + private static string ExtractJsonFromGlb(string filePath) + { + using var stream = File.OpenRead(filePath); + using var reader = new BinaryReader(stream); + + // GLB 헤더: magic(4) + version(4) + length(4) = 12 bytes + uint magic = reader.ReadUInt32(); + if (magic != 0x46546C67) // "glTF" in little-endian + { + Debug.LogError("MsftLodParser: Invalid GLB magic number"); + return string.Empty; + } + + uint version = reader.ReadUInt32(); + uint length = reader.ReadUInt32(); + + // JSON 청크: chunkLength(4) + chunkType(4) + chunkData + uint jsonChunkLength = reader.ReadUInt32(); + uint jsonChunkType = reader.ReadUInt32(); + + if (jsonChunkType != 0x4E4F534A) // "JSON" in little-endian + { + Debug.LogError("MsftLodParser: First chunk is not JSON"); + return string.Empty; + } + + byte[] jsonBytes = reader.ReadBytes((int)jsonChunkLength); + return Encoding.UTF8.GetString(jsonBytes); + } + + /// + /// JSON 문자열에서 MSFT_lod 정보를 파싱합니다. + /// Unity의 JsonUtility는 중첩 객체를 잘 처리하지 못하므로 수동 파싱합니다. + /// + private static List ParseMsftLodFromJson(string json) + { + var result = new List(); + + // "nodes" 배열 찾기 + int nodesStart = json.IndexOf("\"nodes\""); + if (nodesStart < 0) return result; + + int nodesArrayStart = json.IndexOf('[', nodesStart); + if (nodesArrayStart < 0) return result; + + // 노드 배열의 끝 찾기 (괄호 매칭) + int nodesArrayEnd = FindMatchingBracket(json, nodesArrayStart, '[', ']'); + if (nodesArrayEnd < 0) return result; + + string nodesJson = json.Substring(nodesArrayStart, nodesArrayEnd - nodesArrayStart + 1); + + // 각 노드를 개별적으로 파싱 + int nodeIndex = 0; + int searchPos = 0; + + while (true) + { + // 다음 노드 객체 찾기 + int nodeStart = nodesJson.IndexOf('{', searchPos); + if (nodeStart < 0) break; + + int nodeEnd = FindMatchingBracket(nodesJson, nodeStart, '{', '}'); + if (nodeEnd < 0) break; + + string nodeJson = nodesJson.Substring(nodeStart, nodeEnd - nodeStart + 1); + + // MSFT_lod 확장 확인 + int msftLodStart = nodeJson.IndexOf("\"MSFT_lod\""); + if (msftLodStart >= 0) + { + var lodInfo = ParseLodInfoFromNode(nodeJson, nodeIndex); + if (lodInfo != null) + { + result.Add(lodInfo); + } + } + + searchPos = nodeEnd + 1; + nodeIndex++; + } + + return result; + } + + /// + /// 노드 JSON에서 LOD 정보를 파싱합니다. + /// + private static LodInfo? ParseLodInfoFromNode(string nodeJson, int nodeIndex) + { + // "ids" 배열 찾기 + int idsStart = nodeJson.IndexOf("\"ids\""); + if (idsStart < 0) return null; + + int idsArrayStart = nodeJson.IndexOf('[', idsStart); + if (idsArrayStart < 0) return null; + + int idsArrayEnd = nodeJson.IndexOf(']', idsArrayStart); + if (idsArrayEnd < 0) return null; + + string idsContent = nodeJson.Substring(idsArrayStart + 1, idsArrayEnd - idsArrayStart - 1); + + // 쉼표로 구분된 숫자 파싱 + var idStrings = idsContent.Split(','); + var ids = new List(); + + foreach (var idStr in idStrings) + { + string trimmed = idStr.Trim(); + if (int.TryParse(trimmed, out int id)) + { + ids.Add(id); + } + } + + if (ids.Count == 0) return null; + + return new LodInfo + { + NodeIndex = nodeIndex, + LodNodeIds = ids.ToArray() + }; + } + + /// + /// 매칭되는 괄호의 위치를 찾습니다. + /// + private static int FindMatchingBracket(string json, int startPos, char openBracket, char closeBracket) + { + int depth = 0; + bool inString = false; + char prevChar = '\0'; + + for (int i = startPos; i < json.Length; i++) + { + char c = json[i]; + + // 문자열 내부 처리 + if (c == '"' && prevChar != '\\') + { + inString = !inString; + } + else if (!inString) + { + if (c == openBracket) + { + depth++; + } + else if (c == closeBracket) + { + depth--; + if (depth == 0) + { + return i; + } + } + } + + prevChar = c; + } + + return -1; + } + } +} diff --git a/Assets/Scripts/UVC/GLTF/MsftLodParser.cs.meta b/Assets/Scripts/UVC/GLTF/MsftLodParser.cs.meta new file mode 100644 index 00000000..a8a7f870 --- /dev/null +++ b/Assets/Scripts/UVC/GLTF/MsftLodParser.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 21ab40cce627f404c895bbacea2ef6f9 \ No newline at end of file diff --git a/Assets/StreamingAssets/model_lod0.glb b/Assets/StreamingAssets/model_lod0.glb new file mode 100644 index 00000000..0edff615 Binary files /dev/null and b/Assets/StreamingAssets/model_lod0.glb differ diff --git a/Assets/StreamingAssets/model_lod0.glb.meta b/Assets/StreamingAssets/model_lod0.glb.meta new file mode 100644 index 00000000..855ac2d5 --- /dev/null +++ b/Assets/StreamingAssets/model_lod0.glb.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1e015621a087aff4b8dc6d4ada0d428b +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/StreamingAssets/model_lod1.glb b/Assets/StreamingAssets/model_lod1.glb new file mode 100644 index 00000000..75143883 Binary files /dev/null and b/Assets/StreamingAssets/model_lod1.glb differ diff --git a/Assets/StreamingAssets/model_lod1.glb.meta b/Assets/StreamingAssets/model_lod1.glb.meta new file mode 100644 index 00000000..983e12fc --- /dev/null +++ b/Assets/StreamingAssets/model_lod1.glb.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a4ae011ff0a6f3b489d3c880ab8a5c89 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/StreamingAssets/model_lod2.glb b/Assets/StreamingAssets/model_lod2.glb new file mode 100644 index 00000000..78a3770c Binary files /dev/null and b/Assets/StreamingAssets/model_lod2.glb differ diff --git a/Assets/StreamingAssets/model_lod2.glb.meta b/Assets/StreamingAssets/model_lod2.glb.meta new file mode 100644 index 00000000..1df15bf0 --- /dev/null +++ b/Assets/StreamingAssets/model_lod2.glb.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 87e82458b8f6c054d9fc363ad161af8f +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/StreamingAssets/model_lod3.glb b/Assets/StreamingAssets/model_lod3.glb new file mode 100644 index 00000000..09d6825c Binary files /dev/null and b/Assets/StreamingAssets/model_lod3.glb differ diff --git a/Assets/StreamingAssets/model_lod3.glb.meta b/Assets/StreamingAssets/model_lod3.glb.meta new file mode 100644 index 00000000..c85b846f --- /dev/null +++ b/Assets/StreamingAssets/model_lod3.glb.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: ca600dc515811e842989a645b784c59a +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/StreamingAssets/model_with_lod.glb b/Assets/StreamingAssets/model_with_lod.glb new file mode 100644 index 00000000..c68b2876 Binary files /dev/null and b/Assets/StreamingAssets/model_with_lod.glb differ diff --git a/Assets/StreamingAssets/model_with_lod.glb.meta b/Assets/StreamingAssets/model_with_lod.glb.meta new file mode 100644 index 00000000..60b2ecc1 --- /dev/null +++ b/Assets/StreamingAssets/model_with_lod.glb.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a797732bb8c481b48b8d6d6dca79a18b +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: