- Главы питомцев с чертежами (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>
145 lines
5.3 KiB
C#
145 lines
5.3 KiB
C#
using System.Collections.Generic;
|
||
using System.Globalization;
|
||
using System.IO;
|
||
using System.Text;
|
||
using UnityEngine;
|
||
using UnityEngine.Scripting;
|
||
|
||
namespace NecromancerTome
|
||
{
|
||
/// <summary>
|
||
/// necroobj <префаб> <имя> - выгрузить модель префаба в OBJ (09.10.2026, для чертежей питомцев в книге:
|
||
/// медведь, волк, собака, стервятник - ванильные модели). Скелетные меши «запекаются» в позе префаба (BakeMesh),
|
||
/// у LODGroup - только LOD0. Координаты - от корня префаба, оси Unity переведены в правые (x -> -x, обход граней
|
||
/// развёрнут). Файл - %APPDATA%\7DaysToDie\NecroMailbox\obj\<имя>.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);
|
||
}
|
||
}
|
||
}
|
||
}
|