Files
necromants-tome-7d2d-3-2/HarmonySrc/ModelObjExportCommand.cs
T
AlexCubeandClaude Opus 5.5 bdca1bdba5 Книга: главы собаки, стервятника, волка, медведя; гримуары; чертоги 25x13x25 с нумерованными дверями
- Главы питомцев с чертежами (Fig. III-VI), команда necroobj для выгрузки моделей
- Книги призыва питомцев переименованы в гримуары (13 языков)
- Чертоги: второй этаж; таблички necroDoorNumber (только в редакторе) нумеруют двери,
  дверь с номером закрыта до события (SetDoorOpen, necropalace door N open|close),
  с начала открыты 2, 4, 5, 6
- Дух: плавный шаг по ступенькам, проход в проёмы с порогом
- Стервятник: снижение только C, без Ctrl-переключателя

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 21:43:25 +03:00

145 lines
5.3 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using UnityEngine;
using UnityEngine.Scripting;
namespace NecromancerTome
{
/// <summary>
/// necroobj &lt;префаб&gt; &lt;имя&gt; - выгрузить модель префаба в OBJ (09.10.2026, для чертежей питомцев в книге:
/// медведь, волк, собака, стервятник - ванильные модели). Скелетные меши «запекаются» в позе префаба (BakeMesh),
/// у LODGroup - только LOD0. Координаты - от корня префаба, оси Unity переведены в правые (x -&gt; -x, обход граней
/// развёрнут). Файл - %APPDATA%\7DaysToDie\NecroMailbox\obj\&lt;имя&gt;.obj, группы «o» по рендерерам, «usemtl» -
/// имя материала. Это сырьё для рендера в Blender: в мод и в репозиторий модели игры не идут (гигиена ассетов).
/// </summary>
[Preserve]
public class ConsoleCmdNecroObj : ConsoleCmdAbstract
{
public override string[] getCommands() => new[] { "necroobj" };
public override string getDescription() => "NecromancerTome: модель префаба в OBJ (necroobj <префаб> <имя>)";
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
{
if (_params.Count < 2)
{
SdtdConsole.Instance.Output("necroobj <префаб, например @:Entities/Animals/Bear/animalBearZombiePrefab.prefab> <имя>");
return;
}
GameObject prefab = DataLoader.LoadAsset<GameObject>(_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<Renderer>();
var lod = copy.GetComponentInChildren<LODGroup>(true);
if (lod != null && lod.GetLODs().Length > 0)
{
renderers.AddRange(lod.GetLODs()[0].renderers);
}
if (renderers.Count == 0)
{
foreach (Renderer r in copy.GetComponentsInChildren<Renderer>(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<MeshFilter>();
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);
}
}
}
}