Книга: главы собаки, стервятника, волка, медведя; гримуары; чертоги 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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
32065dacac
commit
bdca1bdba5
@@ -0,0 +1,144 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user