GLTFExporter 개발

This commit is contained in:
logonkhi
2025-11-24 20:24:04 +09:00
parent fa774babea
commit a813aad171
120 changed files with 23991 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 61e24dd2829bd9145a8fe34643674952
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,18 @@
{
"name": "GLTFExporter.Runtime",
"rootNamespace": "",
"references": [
"glTFast",
"glTFast.Export",
"SimpleFileBrowser.Runtime"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": true,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 02c96861b0aad1f42a9608281784177a
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,831 @@
#nullable enable
using GLTFast;
using GLTFast.Export;
using GLTFast.Logging;
using GLTFast.Schema;
using Newtonsoft.Json;
using SimpleFileBrowser;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using GltfAccessor = GLTFast.Schema.Accessor;
using GltfAttributes = GLTFast.Schema.Attributes;
using GltfBufferView = GLTFast.Schema.BufferView;
using GltfImage = GLTFast.Schema.Image;
using GltfMaterial = GLTFast.Schema.Material;
//
// Alias GLTFast.Schema types to avoid ambiguity with UnityEngine types
//
using GltfMesh = GLTFast.Schema.Mesh;
// removed GltfGltf alias because GLTFast v6.6.0 doesn't provide a root 'Gltf' POCO type
using GltfPbr = GLTFast.Schema.PbrMetallicRoughness;
using GltfPrimitive = GLTFast.Schema.MeshPrimitive;
using GltfTexture = GLTFast.Schema.Texture;
using GltfTextureInfo = GLTFast.Schema.TextureInfo;
namespace UVC.GLTF
{
/// <summary>
/// Unity GameObject를 GLB(Binary glTF) 형식으로 내보내는 기능을 제공하는 클래스입니다.
/// GLTFast의 스키마를 사용하여 구조를 생성하고 Newtonsoft.Json으로 직렬화합니다.
/// </summary>
public static class GLTFExporter
{
public static async Task ExportAsync(GameObject node, string filePath)
{
var logger = new CollectingLogger();
// ExportSettings는 일반적인 내보내기 설정을 제공합니다.
var exportSettings = new ExportSettings
{
Format = GltfFormat.Binary,
FileConflictResolution = FileConflictResolution.Overwrite,
// 카메라 또는 애니메이션을 제외한 모든 것을 내보냅니다.
ComponentMask = ~(ComponentType.Camera | ComponentType.Animation),
// 조명 강도를 높입니다.
//LightIntensityFactor = 100f,
// 메시 버텍스 속성 색상과 텍스처 좌표(채널 1~8)가 사용되거나 참조되지 않더라도 항상
// 내보내지도록 합니다.
PreservedVertexAttributes = VertexAttributeUsage.AllTexCoords | VertexAttributeUsage.Color,
};
// GameObjectExportSettings는 게임 오브젝트/컴포넌트 기반 계층 구조에 대한 설정을 제공합니다.
var gameObjectExportSettings = new GameObjectExportSettings
{
// 내보내기에 비활성 게임 오브젝트 포함
OnlyActiveInHierarchy = false,
// 비활성화된 컴포넌트도 내보냅니다.
DisabledComponents = true,
// 특정 레이어의 게임 오브젝트만 내보냅니다.
//LayerMask = LayerMask.GetMask("Default", "MyCustomLayer"),
};
// GameObjectExport를 사용하면 게임 오브젝트 계층 구조에서 glTF를 생성할 수 있습니다.
var export = new GameObjectExport(exportSettings, gameObjectExportSettings, logger: logger);
// 내보낼 게임 오브젝트를 수집하는 예(재귀적으로)
var rootLevelNodes = new GameObject[]
{
node
};// GameObject.FindGameObjectsWithTag("ExportMe");
// 장면 추가
export.AddScene(rootLevelNodes, "glTF scene");
// 비동기 glTF 내보내기
var success = await export.SaveToFileAndDispose(filePath);
if (!success)
{
Debug.LogError("glTF를 내보내는 중 문제가 발생했습니다.");
// 모든 내보내기 메시지를 로깅합니다.
logger.LogAll();
}
}
/// <summary>
/// 지정된 GameObject 노드를 GLB 포맷의 바이트 배열로 내보냅니다.
/// 자식 객체들의 MeshRenderer와 SkinnedMeshRenderer를 포함하여 단일 노드로 병합 export를 시도합니다.
/// </summary>
/// <param name="node">내보낼 대상 GameObject</param>
/// <returns>GLB 파일 포맷의 바이트 배열. 실패하거나 내보낼 메쉬가 없으면 null을 반환합니다.</returns>
/// <summary>
/// 지정된 GameObject 노드를 GLB 포맷의 바이트 배열로 내보냅니다.
/// 자식 객체들의 MeshRenderer와 SkinnedMeshRenderer를 포함하여 단일 노드로 병합 export를 시도합니다.
/// </summary>
/// <param name="node">내보낼 대상 GameObject</param>
/// <returns>GLB 파일 포맷의 바이트 배열. 실패하거나 내보낼 메쉬가 없으면 null을 반환합니다.</returns>
public static byte[]? ExportNodeToGlbBytes(GameObject node)
{
if (node == null) return null;
// 1. 렌더러 수집 (MeshRenderer, SkinnedMeshRenderer)
var renderers = node.GetComponentsInChildren<Renderer>(true);
var entries = new List<MeshEntry>();
foreach (var r in renderers)
{
if (r == null || !r.enabled) continue;
UnityEngine.Mesh? mesh = null;
if (r is MeshRenderer)
{
var mf = r.GetComponent<MeshFilter>();
if (mf != null && mf.sharedMesh != null) mesh = mf.sharedMesh;
}
else if (r is SkinnedMeshRenderer smr)
{
var baked = new UnityEngine.Mesh();
try
{
smr.BakeMesh(baked);
mesh = baked;
}
catch
{
UnityEngine.Object.Destroy(baked);
mesh = null;
}
}
if (mesh == null) continue;
entries.Add(new MeshEntry { mesh = mesh, transform = r.transform, renderer = r });
}
if (entries.Count == 0)
{
Debug.LogWarning("GLTFExporter: no meshes to export.");
return null;
}
// GLTF 구조체 준비 (binary buffer + 메타데이터 컬렉션)
var bin = new List<byte>();
var bufferViews = new List<GltfBufferView>();
var accessors = new List<GltfAccessor>();
var gltfMeshes = new List<GltfMesh>();
var materials = new List<GltfMaterial>();
var images = new List<GltfImage>();
var textures = new List<GltfTexture>();
// 병렬 메타: 각 bufferView의 target, 각 accessor의 type (JSON 생성 시 사용)
var bufferViewTargets = new List<int>();
var accessorTypes = new List<string>();
var matToIndex = new Dictionary<UnityEngine.Material, int>();
var texToImageIndex = new Dictionary<Texture2D, int>();
// 루트 노드 기준으로 로컬 좌표계 변환 매트릭스
var worldToLocal = node.transform.worldToLocalMatrix;
// map transform -> mesh index (entries order -> gltfMeshes order)
var transformToMeshIndex = new Dictionary<Transform, int>();
foreach (var e in entries)
{
var mesh = e.mesh;
var t = e.transform;
int vcount = mesh.vertexCount;
var positions = mesh.vertices;
var normals = mesh.normals;
var uvs = mesh.uv;
// 2. POSITION 데이터 처리
int posBufferViewIndex;
int posAccessorIndex;
{
AlignTo4AppendPadding(bin);
int start = bin.Count;
var min = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
var max = new Vector3(float.MinValue, float.MinValue, float.MinValue);
for (int i = 0; i < vcount; i++)
{
var wp = t.TransformPoint(positions[i]);
var lp = worldToLocal.MultiplyPoint3x4(wp);
AppendFloat(bin, lp.x); AppendFloat(bin, lp.y); AppendFloat(bin, lp.z);
min = Vector3.Min(min, lp); max = Vector3.Max(max, lp);
}
int length = bin.Count - start;
posBufferViewIndex = bufferViews.Count;
bufferViews.Add(new GltfBufferView { buffer = 0, byteOffset = start, byteLength = length });
// positions are vertex data -> ARRAY_BUFFER
bufferViewTargets.Add(34962);
posAccessorIndex = accessors.Count;
var accessor = new GltfAccessor
{
bufferView = posBufferViewIndex,
byteOffset = 0,
componentType = GltfComponentType.Float,
count = vcount,
min = new[] { min.x, min.y, min.z },
max = new[] { max.x, max.y, max.z }
};
accessor.SetAttributeType(GltfAccessorAttributeType.VEC3);
accessors.Add(accessor);
accessorTypes.Add("VEC3");
}
// 3. NORMAL 데이터 처리
int normAccessorIndex = -1;
if (normals != null && normals.Length == vcount)
{
AlignTo4AppendPadding(bin);
int start = bin.Count;
for (int i = 0; i < vcount; i++)
{
var nw = t.TransformDirection(normals[i]).normalized;
var nl = worldToLocal.MultiplyVector(nw).normalized;
AppendFloat(bin, nl.x); AppendFloat(bin, nl.y); AppendFloat(bin, nl.z);
}
int length = bin.Count - start;
int bv = bufferViews.Count;
bufferViews.Add(new GltfBufferView { buffer = 0, byteOffset = start, byteLength = length });
// normals are vertex data -> ARRAY_BUFFER
bufferViewTargets.Add(34962);
normAccessorIndex = accessors.Count;
var accessor = new GltfAccessor
{
bufferView = bv,
byteOffset = 0,
componentType = GltfComponentType.Float,
count = vcount,
};
accessor.SetAttributeType(GltfAccessorAttributeType.VEC3);
accessors.Add(accessor);
accessorTypes.Add("VEC3");
}
// 4. TEXCOORD_0 (UV) 데이터 처리
int uvAccessorIndex = -1;
if (uvs != null && uvs.Length == vcount)
{
AlignTo4AppendPadding(bin);
int start = bin.Count;
for (int i = 0; i < vcount; i++)
{
AppendFloat(bin, uvs[i].x); AppendFloat(bin, uvs[i].y);
}
int length = bin.Count - start;
int bv = bufferViews.Count;
bufferViews.Add(new GltfBufferView { buffer = 0, byteOffset = start, byteLength = length });
// uvs are vertex data -> ARRAY_BUFFER
bufferViewTargets.Add(34962);
uvAccessorIndex = accessors.Count;
var accessor = new GltfAccessor
{
bufferView = bv,
byteOffset = 0,
componentType = GltfComponentType.Float,
count = vcount,
};
accessor.SetAttributeType(GltfAccessorAttributeType.VEC2);
accessors.Add(accessor);
accessorTypes.Add("VEC2");
}
// 5. Submesh(Primitives) 처리
var primitives = new List<GltfPrimitive>();
for (int s = 0; s < mesh.subMeshCount; s++)
{
var indices = mesh.GetTriangles(s);
if (indices == null || indices.Length == 0) continue;
bool useUInt = indices.Length > 65535;
AlignTo4AppendPadding(bin);
int start = bin.Count;
if (useUInt)
{
for (int i = 0; i < indices.Length; i++) AppendUInt(bin, (uint)indices[i]);
}
else
{
for (int i = 0; i < indices.Length; i++) AppendUShort(bin, (ushort)indices[i]);
}
int length = bin.Count - start;
int idxBV = bufferViews.Count;
bufferViews.Add(new GltfBufferView { buffer = 0, byteOffset = start, byteLength = length });
// indices are index buffer -> ELEMENT_ARRAY_BUFFER
bufferViewTargets.Add(34963);
int idxAccessor = accessors.Count;
var accessor = new GltfAccessor
{
bufferView = idxBV,
byteOffset = 0,
componentType = useUInt ? GltfComponentType.UnsignedInt : GltfComponentType.UnsignedShort,
count = indices.Length,
};
accessor.SetAttributeType(GltfAccessorAttributeType.SCALAR);
accessors.Add(accessor);
accessorTypes.Add("SCALAR");
// 6. Material 및 Texture 처리
int matIndex = -1;
UnityEngine.Material unityMat = null;
if (e.renderer != null)
{
var shared = e.renderer.sharedMaterials;
if (shared != null && s < shared.Length) unityMat = shared[s];
}
if (unityMat != null)
{
if (!matToIndex.TryGetValue(unityMat, out matIndex))
{
matIndex = materials.Count;
matToIndex[unityMat] = matIndex;
var pbr = new GltfPbr();
Color bc;
if (TryGetColor(unityMat, out bc))
{
pbr.baseColorFactor = new[] { bc.r, bc.g, bc.b, bc.a };
}
if (unityMat.mainTexture != null && unityMat.mainTexture is Texture2D t2d)
{
if (!texToImageIndex.TryGetValue(t2d, out var imgIdx))
{
try
{
var png = t2d.EncodeToPNG();
if (png != null && png.Length > 0)
{
AlignTo4AppendPadding(bin);
int imgStart = bin.Count;
bin.AddRange(png);
int imgLen = bin.Count - imgStart;
int imgBV = bufferViews.Count;
bufferViews.Add(new GltfBufferView { buffer = 0, byteOffset = imgStart, byteLength = imgLen });
// image bufferView no target
bufferViewTargets.Add(0);
var img = new GltfImage { mimeType = "image/png", bufferView = imgBV };
imgIdx = images.Count;
images.Add(img);
texToImageIndex[t2d] = imgIdx;
}
}
catch
{
Debug.LogWarning($"GLTFExporter: failed to encode texture '{t2d.name}' to PNG.");
}
}
if (texToImageIndex.TryGetValue(t2d, out var imageIndex))
{
int texIdx = textures.Count;
textures.Add(new GltfTexture { source = imageIndex });
pbr.baseColorTexture = new GltfTextureInfo { index = texIdx };
}
}
materials.Add(new GltfMaterial { pbrMetallicRoughness = pbr });
}
}
var prim = new GltfPrimitive
{
attributes = new GltfAttributes { POSITION = posAccessorIndex },
indices = idxAccessor,
mode = DrawMode.Triangles
};
if (normAccessorIndex >= 0) prim.attributes.NORMAL = normAccessorIndex;
if (uvAccessorIndex >= 0) prim.attributes.TEXCOORD_0 = uvAccessorIndex;
if (matIndex >= 0) prim.material = matIndex;
primitives.Add(prim);
}
// add mesh and map transform -> mesh index
int meshIndex = gltfMeshes.Count;
gltfMeshes.Add(new GltfMesh { primitives = primitives.ToArray(), name = mesh.name ?? "mesh" });
transformToMeshIndex[e.transform] = meshIndex;
}
// Ensure accessorTypes fallback (safety)
for (int i = 0; i < accessors.Count; i++)
{
if (i >= accessorTypes.Count || string.IsNullOrEmpty(accessorTypes[i]))
{
accessorTypes.Add(InferAccessorType(accessors[i]));
}
}
// Build plain glTF root using camelCase keys and only required fields
// Create one node per exported mesh (preserves multiple child meshes)
var nodesList = new List<Dictionary<string, object>>();
for (int i = 0; i < gltfMeshes.Count; i++)
{
var entryTransform = entries.Count > i ? entries[i].transform : null;
var name = entryTransform != null ? entryTransform.name : $"mesh_{i}";
nodesList.Add(new Dictionary<string, object> { ["mesh"] = i, ["name"] = name });
}
// scene references all nodes
var sceneNodes = new int[nodesList.Count];
for (int i = 0; i < nodesList.Count; i++) sceneNodes[i] = i;
var scenes = new List<Dictionary<string, object>> { new Dictionary<string, object> { ["nodes"] = sceneNodes } };
var buffersList = new List<Dictionary<string, object>> { new Dictionary<string, object> { ["byteLength"] = (uint)bin.Count } };
var root = new Dictionary<string, object>
{
["asset"] = new Dictionary<string, object> { ["generator"] = "GLTFExporter", ["version"] = "2.0" },
["scenes"] = scenes.ToArray(),
["scene"] = 0,
["nodes"] = nodesList.ToArray(),
// meshes will be filled below as plain objects
["buffers"] = buffersList.ToArray()
};
// bufferViews -> plain objects (use bufferViewTargets)
if (bufferViews.Count > 0)
{
var bvObjs = new List<Dictionary<string, object>>();
for (int i = 0; i < bufferViews.Count; i++)
{
var bv = bufferViews[i];
var d = new Dictionary<string, object>
{
["buffer"] = bv.buffer,
["byteOffset"] = bv.byteOffset,
["byteLength"] = bv.byteLength
};
if (i < bufferViewTargets.Count && bufferViewTargets[i] != 0)
{
d["target"] = bufferViewTargets[i];
}
bvObjs.Add(d);
}
root["bufferViews"] = bvObjs.ToArray();
}
// accessors -> plain objects (use accessorTypes)
if (accessors.Count > 0)
{
var aObjs = new List<Dictionary<string, object>>();
for (int i = 0; i < accessors.Count; i++)
{
var a = accessors[i];
var d = new Dictionary<string, object>
{
["bufferView"] = a.bufferView,
["byteOffset"] = a.byteOffset,
["componentType"] = (int)a.componentType,
["count"] = a.count
};
if (a.min != null) d["min"] = a.min;
if (a.max != null) d["max"] = a.max;
if (i < accessorTypes.Count && !string.IsNullOrEmpty(accessorTypes[i]))
{
d["type"] = accessorTypes[i];
}
aObjs.Add(d);
}
root["accessors"] = aObjs.ToArray();
}
// images -> plain objects
if (images.Count > 0)
{
var imgs = new List<Dictionary<string, object>>();
foreach (var img in images)
{
var d = new Dictionary<string, object>();
if (!string.IsNullOrEmpty(img.mimeType)) d["mimeType"] = img.mimeType;
if (img.bufferView >= 0) d["bufferView"] = img.bufferView;
if (!string.IsNullOrEmpty(img.uri)) d["uri"] = img.uri;
imgs.Add(d);
}
root["images"] = imgs.ToArray();
}
// textures -> plain objects
if (textures.Count > 0)
{
var texs = new List<Dictionary<string, object>>();
foreach (var tex in textures)
{
var d = new Dictionary<string, object>();
if (tex.source >= 0) d["source"] = tex.source;
texs.Add(d);
}
root["textures"] = texs.ToArray();
}
// materials -> plain objects (only include glTF fields)
if (materials.Count > 0)
{
var mats = new List<Dictionary<string, object>>();
foreach (var m in materials)
{
var md = new Dictionary<string, object>();
if (m.pbrMetallicRoughness != null)
{
var p = m.pbrMetallicRoughness;
var pd = new Dictionary<string, object>();
if (p.baseColorFactor != null) pd["baseColorFactor"] = p.baseColorFactor;
if (p.baseColorTexture != null && p.baseColorTexture.index >= 0)
{
pd["baseColorTexture"] = new Dictionary<string, object> { ["index"] = p.baseColorTexture.index };
}
if (pd.Count > 0) md["pbrMetallicRoughness"] = pd;
}
if (!string.IsNullOrEmpty(m.name)) md["name"] = m.name;
// alphaCutoff is only valid when alphaMode == "MASK"
if (!string.IsNullOrEmpty(m.alphaMode)) md["alphaMode"] = m.alphaMode;
if (m.alphaMode == "MASK" && m.alphaCutoff > 0f) md["alphaCutoff"] = m.alphaCutoff;
mats.Add(md);
}
root["materials"] = mats.ToArray();
}
// meshes -> plain objects with primitives (only include actual attribute indices)
if (gltfMeshes.Count > 0)
{
var meshObjs = new List<Dictionary<string, object>>();
foreach (var gm in gltfMeshes)
{
var mObj = new Dictionary<string, object>();
if (!string.IsNullOrEmpty(gm.name)) mObj["name"] = gm.name;
var primObjs = new List<Dictionary<string, object>>();
if (gm.primitives != null)
{
foreach (var gp in gm.primitives)
{
var pObj = new Dictionary<string, object>();
// attributes
var attrs = new Dictionary<string, object>();
if (gp.attributes != null)
{
if (gp.attributes.POSITION >= 0) attrs["POSITION"] = gp.attributes.POSITION;
if (gp.attributes.NORMAL >= 0) attrs["NORMAL"] = gp.attributes.NORMAL;
if (gp.attributes.TEXCOORD_0 >= 0) attrs["TEXCOORD_0"] = gp.attributes.TEXCOORD_0;
if (gp.attributes.TANGENT >= 0) attrs["TANGENT"] = gp.attributes.TANGENT;
// add other TEXCOORD_n / COLOR_0 / JOINTS_0 / WEIGHTS_0 only if >=0
// check up to 8 texcoords
if (gp.attributes.TEXCOORD_1 >= 0) attrs["TEXCOORD_1"] = gp.attributes.TEXCOORD_1;
if (gp.attributes.TEXCOORD_2 >= 0) attrs["TEXCOORD_2"] = gp.attributes.TEXCOORD_2;
if (gp.attributes.TEXCOORD_3 >= 0) attrs["TEXCOORD_3"] = gp.attributes.TEXCOORD_3;
if (gp.attributes.TEXCOORD_4 >= 0) attrs["TEXCOORD_4"] = gp.attributes.TEXCOORD_4;
if (gp.attributes.TEXCOORD_5 >= 0) attrs["TEXCOORD_5"] = gp.attributes.TEXCOORD_5;
if (gp.attributes.TEXCOORD_6 >= 0) attrs["TEXCOORD_6"] = gp.attributes.TEXCOORD_6;
if (gp.attributes.TEXCOORD_7 >= 0) attrs["TEXCOORD_7"] = gp.attributes.TEXCOORD_7;
if (gp.attributes.COLOR_0 >= 0) attrs["COLOR_0"] = gp.attributes.COLOR_0;
if (gp.attributes.JOINTS_0 >= 0) attrs["JOINTS_0"] = gp.attributes.JOINTS_0;
if (gp.attributes.WEIGHTS_0 >= 0) attrs["WEIGHTS_0"] = gp.attributes.WEIGHTS_0;
}
pObj["attributes"] = attrs;
if (gp.indices >= 0) pObj["indices"] = gp.indices;
if (gp.material >= 0) pObj["material"] = gp.material;
// include mode only if not triangles (4) -- optionally include always
pObj["mode"] = (int)gp.mode;
primObjs.Add(pObj);
}
}
mObj["primitives"] = primObjs.ToArray();
meshObjs.Add(mObj);
}
root["meshes"] = meshObjs.ToArray();
}
var jsonSettings = new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
Formatting = Formatting.None,
Culture = CultureInfo.InvariantCulture,
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
};
jsonSettings.Converters.Add(new ColorConverter());
string json = JsonConvert.SerializeObject(root, jsonSettings);
// 9. GLB 헤더 및 청크 작성
var jsonBytes = Encoding.UTF8.GetBytes(json);
int jsonPad = PadTo4(jsonBytes.Length);
var jsonPadded = new byte[jsonBytes.Length + jsonPad];
Array.Copy(jsonBytes, jsonPadded, jsonBytes.Length);
for (int i = jsonBytes.Length; i < jsonPadded.Length; i++) jsonPadded[i] = 0x20;
int binPad = PadTo4(bin.Count);
var binPadded = new byte[bin.Count + binPad];
Array.Copy(bin.ToArray(), binPadded, bin.Count);
using (var ms = new MemoryStream())
using (var bw = new BinaryWriter(ms))
{
bw.Write(0x46546C67);
bw.Write(2);
int length = 12 + 8 + jsonPadded.Length + 8 + binPadded.Length;
bw.Write(length);
bw.Write(jsonPadded.Length);
bw.Write(0x4E4F534A);
bw.Write(jsonPadded);
bw.Write(binPadded.Length);
bw.Write(0x004E4942);
bw.Write(binPadded);
return ms.ToArray();
}
}
private static string InferAccessorType(GltfAccessor a)
{
// 가능하면 min/max 길이로 판단, 아니면 componentType 기준 간단 추정
try
{
if (a.min != null && a.min.Length == 3) return "VEC3";
if (a.min != null && a.min.Length == 2) return "VEC2";
if (a.min != null && a.min.Length == 4) return "VEC4";
}
catch { /* ignore */ }
// componentType 기반 추정(인덱스 등)
switch (a.componentType)
{
case GltfComponentType.UnsignedByte:
case GltfComponentType.UnsignedShort:
case GltfComponentType.UnsignedInt:
return "SCALAR";
case GltfComponentType.Float:
// 기본적으로 float로 만들어졌으면 벡터인지 모름 — 안전하게 SCALAR로 두지 말고 VEC3 우선시
return "VEC3";
default:
return "SCALAR";
}
}
public static void ExportNodeByExplorer(GameObject node, bool useLib = true)
{
if (node == null)
{
Debug.LogWarning("GLTFExporter: node is null, cannot export.");
return;
}
FileBrowser.ShowSaveDialog(
(string[] paths) =>
{
if (paths.Length == 1)
{
if (useLib)
{
ExportAsync(node, paths[0]).ContinueWith(t =>
{
if (t.IsFaulted)
{
Debug.LogError($"GLTFExporter: export failed with exception: {t.Exception}");
}
});
}
else
{
ExportNodeToGlbFile(node, paths[0]);
}
}
}, () =>
{
Debug.Log("User canceled save file dialog.");
}, FileBrowser.PickMode.Files,
false,
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
"model.glb",
"Save",
"Save");
}
public static void ExportNodeToGlbFile(GameObject node, string filePath)
{
if (node == null)
{
Debug.LogWarning("GLTFExporter: node is null, cannot export.");
return;
}
var glbBytes = ExportNodeToGlbBytes(node);
if (glbBytes != null)
{
try
{
File.WriteAllBytes(filePath, glbBytes);
Debug.Log($"GLTFExporter: exported GLB to '{filePath}'.");
}
catch (Exception ex)
{
Debug.LogError($"GLTFExporter: failed to write GLB file '{filePath}': {ex.Message}");
}
}
else
{
Debug.LogWarning("GLTFExporter: export returned no data.");
}
}
public static string? ExtractJsonChunkFromGlbBytes(byte[] glbBytes)
{
if (glbBytes == null || glbBytes.Length < 20) return null;
using (var ms = new MemoryStream(glbBytes))
using (var br = new BinaryReader(ms))
{
uint magic = br.ReadUInt32(); // should be 0x46546C67
uint version = br.ReadUInt32();
uint length = br.ReadUInt32();
if (magic != 0x46546C67) return null;
// JSON chunk
uint jsonChunkLen = br.ReadUInt32();
uint jsonChunkType = br.ReadUInt32();
if (jsonChunkType != 0x4E4F534A) return null; // 'JSON'
var jsonBytes = br.ReadBytes((int)jsonChunkLen);
// Trim trailing spaces (0x20) that were used for padding when creating GLB
int actualLen = jsonBytes.Length;
while (actualLen > 0 && jsonBytes[actualLen - 1] == 0x20) actualLen--;
return Encoding.UTF8.GetString(jsonBytes, 0, actualLen);
}
}
public static void DumpJsonFromGlbFile(string glbPath, string outJsonPath = null)
{
try
{
var bytes = File.ReadAllBytes(glbPath);
var json = ExtractJsonChunkFromGlbBytes(bytes);
if (json == null)
{
Debug.LogWarning($"GLTFExporter: failed to extract JSON chunk from '{glbPath}'.");
return;
}
if (string.IsNullOrEmpty(outJsonPath))
{
outJsonPath = Path.ChangeExtension(glbPath, ".gltf.json");
}
File.WriteAllText(outJsonPath, json, Encoding.UTF8);
Debug.Log($"GLTFExporter: extracted JSON chunk to '{outJsonPath}'.");
}
catch (Exception ex)
{
Debug.LogError($"GLTFExporter: DumpJsonFromGlbFile failed: {ex.Message}");
}
}
#region helpers
private class MeshEntry { public UnityEngine.Mesh mesh; public Transform transform; public Renderer renderer; }
private static void AppendFloat(List<byte> bin, float v) => bin.AddRange(BitConverter.GetBytes(v));
private static void AppendUShort(List<byte> bin, ushort v) => bin.AddRange(BitConverter.GetBytes(v));
private static void AppendUInt(List<byte> bin, uint v) => bin.AddRange(BitConverter.GetBytes(v));
private static void AlignTo4AppendPadding(List<byte> bin)
{
int pad = (4 - (bin.Count % 4)) % 4;
for (int i = 0; i < pad; i++) bin.Add(0);
}
private static int PadTo4(int v) => (4 - (v % 4)) % 4;
private static bool TryGetColor(UnityEngine.Material mat, out Color c)
{
c = Color.white;
if (mat == null) return false;
string[] props = { "_BaseColor", "_Color", "_TintColor", "_MainColor", "_WireframeColor" };
foreach (var p in props)
{
if (mat.HasProperty(p))
{
try { c = mat.GetColor(p); return true; } catch { }
}
}
try { c = mat.color; return true; } catch { return false; }
}
private static int PadTo4(long v) => (int)((4 - (v % 4)) % 4);
#endregion
}
class ColorConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(Color) || objectType == typeof(Color?);
}
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
if (value == null)
{
writer.WriteNull();
return;
}
var c = (Color)value;
// glTF는 보통 선형 색상 값을 기대하므로 linear 사용
var lin = c.linear;
writer.WriteStartArray();
writer.WriteValue((double)lin.r);
writer.WriteValue((double)lin.g);
writer.WriteValue((double)lin.b);
writer.WriteValue((double)lin.a);
writer.WriteEndArray();
}
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null) return null;
var arr = serializer.Deserialize<double[]>(reader);
if (arr == null) return Color.white;
float r = (float)(arr.Length > 0 ? arr[0] : 0.0);
float g = (float)(arr.Length > 1 ? arr[1] : 0.0);
float b = (float)(arr.Length > 2 ? arr[2] : 0.0);
float a = (float)(arr.Length > 3 ? arr[3] : 1.0);
return new Color(r, g, b, a);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 65e74880481a7894184f66e7bdc1460e