using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text; using UnityEngine; using UnityEngine.Scripting; namespace NecromancerTome { /// /// necroobj <префаб> <имя> - выгрузить модель префаба в OBJ (09.10.2026, для чертежей питомцев в книге: /// медведь, волк, собака, стервятник - ванильные модели). Скелетные меши «запекаются» в позе префаба (BakeMesh), /// у LODGroup - только LOD0. Координаты - от корня префаба, оси Unity переведены в правые (x -> -x, обход граней /// развёрнут). Файл - %APPDATA%\7DaysToDie\NecroMailbox\obj\<имя>.obj, группы «o» по рендерерам, «usemtl» - /// имя материала. Это сырьё для рендера в Blender: в мод и в репозиторий модели игры не идут (гигиена ассетов). /// [Preserve] public class ConsoleCmdNecroObj : ConsoleCmdAbstract { public override string[] getCommands() => new[] { "necroobj" }; public override string getDescription() => "NecromancerTome: модель префаба в OBJ (necroobj <префаб> <имя>)"; public override void Execute(List _params, CommandSenderInfo _senderInfo) { if (_params.Count < 2) { SdtdConsole.Instance.Output("necroobj <префаб, например @:Entities/Animals/Bear/animalBearZombiePrefab.prefab> <имя>"); return; } GameObject prefab = DataLoader.LoadAsset(_params[0]); if (prefab == null) { SdtdConsole.Instance.Output("Префаб не загрузился: " + _params[0]); return; } GameObject copy = Object.Instantiate(prefab); copy.transform.position = Vector3.zero; copy.transform.rotation = Quaternion.identity; try { var renderers = new List(); var lod = copy.GetComponentInChildren(true); if (lod != null && lod.GetLODs().Length > 0) { renderers.AddRange(lod.GetLODs()[0].renderers); } if (renderers.Count == 0) { foreach (Renderer r in copy.GetComponentsInChildren(true)) { if (r is SkinnedMeshRenderer || r is MeshRenderer) { renderers.Add(r); } } } var sb = new StringBuilder(); var ci = CultureInfo.InvariantCulture; int vBase = 1, tris = 0; Matrix4x4 toRoot = copy.transform.worldToLocalMatrix; foreach (Renderer r in renderers) { if (r == null) { continue; } Mesh mesh; Matrix4x4 m; if (r is SkinnedMeshRenderer smr) { mesh = new Mesh(); smr.BakeMesh(mesh, true); // Запечённый меш - в пространстве рендерера без его масштаба (useScale учтён в вершинах). m = toRoot * Matrix4x4.TRS(smr.transform.position, smr.transform.rotation, Vector3.one); } else { MeshFilter mf = r.GetComponent(); if (mf == null || mf.sharedMesh == null || !mf.sharedMesh.isReadable) { continue; } mesh = mf.sharedMesh; m = toRoot * r.transform.localToWorldMatrix; } if (!mesh.isReadable && !(r is SkinnedMeshRenderer)) { continue; } Vector3[] v = mesh.vertices; Vector2[] uv = mesh.uv; sb.Append("o ").Append(r.name).Append('\n'); foreach (Vector3 p0 in v) { Vector3 p = m.MultiplyPoint3x4(p0); sb.Append("v ").Append((-p.x).ToString("F5", ci)).Append(' ').Append(p.y.ToString("F5", ci)).Append(' ') .Append(p.z.ToString("F5", ci)).Append('\n'); } bool hasUv = uv != null && uv.Length == v.Length; if (hasUv) { foreach (Vector2 t in uv) { sb.Append("vt ").Append(t.x.ToString("F5", ci)).Append(' ').Append(t.y.ToString("F5", ci)).Append('\n'); } } Material[] mats = r.sharedMaterials; for (int s = 0; s < mesh.subMeshCount; s++) { sb.Append("usemtl ").Append(s < mats.Length && mats[s] != null ? mats[s].name.Replace(' ', '_') : "none").Append('\n'); int[] idx = mesh.GetTriangles(s); for (int i = 0; i + 2 < idx.Length; i += 3) { // обход развёрнут из-за смены руки осей int a = idx[i] + vBase, b = idx[i + 2] + vBase, c = idx[i + 1] + vBase; if (hasUv) { sb.Append("f ").Append(a).Append('/').Append(a).Append(' ').Append(b).Append('/').Append(b).Append(' ') .Append(c).Append('/').Append(c).Append('\n'); } else { sb.Append("f ").Append(a).Append(' ').Append(b).Append(' ').Append(c).Append('\n'); } tris++; } } vBase += v.Length; } string dir = Path.Combine(GameIO.GetUserGameDataDir(), "NecroMailbox", "obj"); Directory.CreateDirectory(dir); string path = Path.Combine(dir, _params[1] + ".obj"); File.WriteAllText(path, sb.ToString()); string msg = "necroobj: " + _params[1] + " - рендереров " + renderers.Count + ", вершин " + (vBase - 1) + ", треуг. " + tris + " -> " + path; Debug.Log("[NecromancerTome] " + msg); SdtdConsole.Instance.Output(msg); } finally { Object.Destroy(copy); } } } }