#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; } } }