Загрузка данных
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using MSCLoader;
using UnityEngine;
namespace MapExporter
{
public class MapExporterMod : Mod
{
public override string ID { get { return "MapExporter"; } }
public override string Name { get { return "Map Exporter"; } }
public override string Author { get { return "me"; } }
public override string Version { get { return "2.0"; } }
public override void ModSetup()
{
SetupFunction(Setup.OnLoad, Mod_OnLoad);
}
private void Mod_OnLoad()
{
GameObject go = new GameObject("MapExporterRunner");
go.AddComponent<MapExporterBehaviour>();
UnityEngine.Object.DontDestroyOnLoad(go);
}
}
// F8 - пробный прогон, F9 - выгрузка, F10 - сбор объектов по ходу игры
public class MapExporterBehaviour : MonoBehaviour
{
private const float ZoneSize = 200f;
private const int MaxTextureSize = 2048;
private const int TerrainMaxGrid = 513;
private const int TerrainTexSize = 2048;
private const float TrackInterval = 3f;
private class Entry
{
public MeshFilter mf;
public int zx;
public int zz;
}
private string status = "";
private float statusUntil;
private float hintUntil;
private bool busy;
private bool tracking;
private float nextTrack;
private Dictionary<int, MeshFilter> tracked = new Dictionary<int, MeshFilter>();
private string outDir;
private string texDir;
private Dictionary<int, string> texNames = new Dictionary<int, string>();
private Dictionary<int, string> matNames = new Dictionary<int, string>();
private StringBuilder mtl = new StringBuilder();
private int dupeCount;
private int skippedMeshes;
void Start()
{
hintUntil = Time.realtimeSinceStartup + 25f;
}
void Update()
{
if (busy) return;
if (Input.GetKeyDown(KeyCode.F8))
{
StartCoroutine(DryRun());
}
else if (Input.GetKeyDown(KeyCode.F9))
{
StartCoroutine(Export());
}
else if (Input.GetKeyDown(KeyCode.F10))
{
tracking = !tracking;
nextTrack = 0f;
SetStatus("MapExporter: сбор объектов " + (tracking ? "ВКЛ" : "ВЫКЛ") + " (собрано: " + tracked.Count + ")", 6f);
}
if (tracking && Time.realtimeSinceStartup >= nextTrack)
{
nextTrack = Time.realtimeSinceStartup + TrackInterval;
MeshFilter[] all = UnityEngine.Object.FindObjectsOfType<MeshFilter>();
for (int i = 0; i < all.Length; i++)
{
MeshFilter mf = all[i];
if (mf == null || !mf.gameObject.activeInHierarchy) continue;
int id = mf.GetInstanceID();
if (!tracked.ContainsKey(id)) tracked[id] = mf;
}
}
}
void OnGUI()
{
float now = Time.realtimeSinceStartup;
float y = 10f;
if (status.Length > 0 && (busy || now < statusUntil))
{
GUI.Label(new Rect(10, y, 1000, 25), status);
y += 22f;
}
if (tracking)
{
GUI.Label(new Rect(10, y, 1000, 25), "MapExporter: сбор ВКЛ, объектов: " + tracked.Count);
y += 22f;
}
if (now < hintUntil)
{
GUI.Label(new Rect(10, y, 1000, 25), "MapExporter: F8 пробный прогон | F9 выгрузка | F10 сбор объектов");
}
}
private void SetStatus(string s, float seconds)
{
status = s;
statusUntil = Time.realtimeSinceStartup + seconds;
}
private static string F(float v)
{
return v.ToString("0.#####", CultureInfo.InvariantCulture);
}
private static string Clean(string s)
{
if (string.IsNullOrEmpty(s)) return "unnamed";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < s.Length; i++)
{
char c = s[i];
if (char.IsLetterOrDigit(c) || c == '_' || c == '-') sb.Append(c);
else sb.Append('_');
}
return sb.ToString();
}
private List<Entry> Collect()
{
List<Entry> list = new List<Entry>();
Dictionary<int, bool> seenIds = new Dictionary<int, bool>();
Dictionary<string, bool> seenSig = new Dictionary<string, bool>();
dupeCount = 0;
MeshFilter[] active = UnityEngine.Object.FindObjectsOfType<MeshFilter>();
for (int i = 0; i < active.Length; i++)
{
Accept(active[i], true, seenIds, seenSig, list);
}
foreach (KeyValuePair<int, MeshFilter> kv in tracked)
{
Accept(kv.Value, false, seenIds, seenSig, list);
}
list.Sort(delegate (Entry a, Entry b)
{
int c = a.zx.CompareTo(b.zx);
if (c != 0) return c;
return a.zz.CompareTo(b.zz);
});
return list;
}
private void Accept(MeshFilter mf, bool requireActive, Dictionary<int, bool> seenIds,
Dictionary<string, bool> seenSig, List<Entry> list)
{
if (mf == null) return;
if (mf.sharedMesh == null) return;
Renderer r = mf.GetComponent<Renderer>();
if (r == null) return;
if (requireActive && (!mf.gameObject.activeInHierarchy || !r.enabled)) return;
int id = mf.GetInstanceID();
if (seenIds.ContainsKey(id)) return;
seenIds[id] = true;
Transform t = mf.transform;
Vector3 p = t.position;
Vector3 e = t.eulerAngles;
Vector3 s = t.lossyScale;
string sig = mf.sharedMesh.GetInstanceID() + "|"
+ Mathf.Round(p.x * 100f) + "," + Mathf.Round(p.y * 100f) + "," + Mathf.Round(p.z * 100f) + "|"
+ Mathf.Round(e.x * 10f) + "," + Mathf.Round(e.y * 10f) + "," + Mathf.Round(e.z * 10f) + "|"
+ Mathf.Round(s.x * 100f) + "," + Mathf.Round(s.y * 100f) + "," + Mathf.Round(s.z * 100f);
if (seenSig.ContainsKey(sig))
{
dupeCount++;
return;
}
seenSig[sig] = true;
Entry en = new Entry();
en.mf = mf;
en.zx = Mathf.FloorToInt(p.x / ZoneSize);
en.zz = Mathf.FloorToInt(p.z / ZoneSize);
list.Add(en);
}
private IEnumerator DryRun()
{
busy = true;
SetStatus("MapExporter: пробный прогон...", 5f);
yield return null;
List<Entry> list = Collect();
long verts = 0;
Dictionary<long, bool> zones = new Dictionary<long, bool>();
for (int i = 0; i < list.Count; i++)
{
if (list[i].mf == null || list[i].mf.sharedMesh == null) continue;
verts += list[i].mf.sharedMesh.vertexCount;
zones[((long)list[i].zx << 32) ^ (long)(uint)list[i].zz] = true;
}
Terrain[] terrains = UnityEngine.Object.FindObjectsOfType<Terrain>();
string tinfo = "";
for (int i = 0; i < terrains.Length; i++)
{
if (terrains[i].terrainData != null)
tinfo += " [" + terrains[i].terrainData.heightmapResolution + "x" + terrains[i].terrainData.heightmapResolution + "]";
}
long mb = (verts * 60L) / (1024L * 1024L);
string msg = "MapExporter ПРОБА: объектов " + list.Count + ", дублей пропущено " + dupeCount
+ ", вершин " + verts + " (~" + mb + " МБ), зон-файлов " + zones.Count
+ ", ландшафтов " + terrains.Length + tinfo;
Debug.Log(msg);
SetStatus(msg, 25f);
busy = false;
}
private IEnumerator Export()
{
busy = true;
SetStatus("MapExporter: начинаю выгрузку...", 5f);
yield return null;
outDir = Path.Combine(Path.GetDirectoryName(Application.dataPath), "MapExport");
texDir = Path.Combine(outDir, "textures");
Directory.CreateDirectory(outDir);
Directory.CreateDirectory(texDir);
texNames.Clear();
matNames.Clear();
mtl.Length = 0;
skippedMeshes = 0;
List<Entry> list = Collect();
int exported = 0;
int files = 0;
StreamWriter w = null;
int curZx = 0, curZz = 0;
int vOff = 0, vtOff = 0, vnOff = 0;
for (int i = 0; i < list.Count; i++)
{
if ((i % 100) == 0)
{
SetStatus("MapExporter: объекты " + i + " / " + list.Count, 5f);
yield return null;
}
Entry en = list[i];
if (en.mf == null || en.mf.sharedMesh == null) continue;
if (w == null || en.zx != curZx || en.zz != curZz)
{
if (w != null) w.Close();
curZx = en.zx;
curZz = en.zz;
vOff = 0; vtOff = 0; vnOff = 0;
w = new StreamWriter(Path.Combine(outDir, "objects_x" + curZx + "_z" + curZz + ".obj"), false, new UTF8Encoding(false));
w.WriteLine("# My Summer Car objects, zone " + curZx + "," + curZz);
w.WriteLine("mtllib map.mtl");
files++;
}
if (WriteMesh(w, en.mf, i, ref vOff, ref vtOff, ref vnOff)) exported++;
}
if (w != null) w.Close();
File.WriteAllText(Path.Combine(outDir, "map.mtl"), mtl.ToString());
Terrain[] terrains = UnityEngine.Object.FindObjectsOfType<Terrain>();
int terrainsDone = 0;
for (int ti = 0; ti < terrains.Length; ti++)
{
if (terrains[ti] == null || terrains[ti].terrainData == null) continue;
yield return StartCoroutine(ExportTerrain(terrains[ti], ti));
terrainsDone++;
}
string msg = "MapExporter: ГОТОВО. Объектов: " + exported + " в " + files + " файлах, дублей пропущено: " + dupeCount
+ ", пустых мешей: " + skippedMeshes + ", ландшафтов: " + terrainsDone + ". Папка: " + outDir;
Debug.Log(msg);
SetStatus(msg, 30f);
busy = false;
}
private bool WriteMesh(StreamWriter w, MeshFilter mf, int idx, ref int vOff, ref int vtOff, ref int vnOff)
{
Mesh mesh = mf.sharedMesh;
Renderer r = mf.GetComponent<Renderer>();
Vector3[] verts = mesh.vertices;
if (verts.Length == 0) { skippedMeshes++; return false; }
Vector3[] norms = mesh.normals;
Vector2[] uvs = mesh.uv;
bool hasN = norms != null && norms.Length == verts.Length;
bool hasUV = uvs != null && uvs.Length == verts.Length;
Transform t = mf.transform;
w.WriteLine("o " + Clean(mf.gameObject.name) + "_" + idx);
for (int k = 0; k < verts.Length; k++)
{
Vector3 p = t.TransformPoint(verts[k]);
w.WriteLine("v " + F(-p.x) + " " + F(p.y) + " " + F(p.z));
}
if (hasN)
{
for (int k = 0; k < norms.Length; k++)
{
Vector3 n = t.TransformDirection(norms[k]).normalized;
w.WriteLine("vn " + F(-n.x) + " " + F(n.y) + " " + F(n.z));
}
}
Material[] mats = r != null ? r.sharedMaterials : null;
for (int s = 0; s < mesh.subMeshCount; s++)
{
Material m = null;
if (mats != null && mats.Length > 0) m = mats[Mathf.Min(s, mats.Length - 1)];
Vector2 sc = Vector2.one;
Vector2 off = Vector2.zero;
if (m != null && m.HasProperty("_MainTex"))
{
sc = m.mainTextureScale;
off = m.mainTextureOffset;
}
int vtBase = vtOff + 1;
if (hasUV)
{
for (int k = 0; k < uvs.Length; k++)
{
w.WriteLine("vt " + F(uvs[k].x * sc.x + off.x) + " " + F(uvs[k].y * sc.y + off.y));
}
vtOff += uvs.Length;
}
w.WriteLine("usemtl " + GetMaterialName(m));
int[] tris = mesh.GetTriangles(s);
for (int k = 0; k + 2 < tris.Length; k += 3)
{
int a = tris[k];
int b = tris[k + 1];
int c = tris[k + 2];
w.WriteLine("f " + Idx(a, vOff, vtBase, vnOff, hasUV, hasN) + " "
+ Idx(c, vOff, vtBase, vnOff, hasUV, hasN) + " "
+ Idx(b, vOff, vtBase, vnOff, hasUV, hasN));
}
}
vOff += verts.Length;
if (hasN) vnOff += norms.Length;
return true;
}
private static string Idx(int i, int vOff, int vtBase, int vnOff, bool hasUV, bool hasN)
{
string v = (vOff + i + 1).ToString();
if (hasUV && hasN) return v + "/" + (vtBase + i) + "/" + (vnOff + i + 1);
if (hasUV) return v + "/" + (vtBase + i);
if (hasN) return v + "//" + (vnOff + i + 1);
return v;
}
private IEnumerator ExportTerrain(Terrain terrain, int ti)
{
TerrainData td = terrain.terrainData;
int res = td.heightmapResolution;
float[,] hgt = td.GetHeights(0, 0, res, res);
Vector3 size = td.size;
Vector3 pos = terrain.transform.position;
string prefix = "terrain_" + ti;
SetStatus("MapExporter: ландшафт " + ti + " - карта высот", 5f);
yield return null;
byte[] raw = new byte[res * res * 2];
int bi = 0;
for (int z = 0; z < res; z++)
{
for (int x = 0; x < res; x++)
{
int v = Mathf.Clamp(Mathf.RoundToInt(hgt[z, x] * 65535f), 0, 65535);
raw[bi++] = (byte)(v & 0xFF);
raw[bi++] = (byte)(v >> 8);
}
}
File.WriteAllBytes(Path.Combine(outDir, prefix + "_height_" + res + "x" + res + "_r16.raw"), raw);
File.WriteAllText(Path.Combine(outDir, prefix + "_info.txt"),
"position: " + F(pos.x) + " " + F(pos.y) + " " + F(pos.z) + "\n"
+ "size (x y z): " + F(size.x) + " " + F(size.y) + " " + F(size.z) + "\n"
+ "heightmap resolution: " + res + "\n"
+ "format: 16-bit unsigned, little-endian, row 0 = z min\n");
string colorFile = null;
yield return StartCoroutine(BakeTerrainTexture(td, size, prefix));
string bakedName = prefix + "_color.png";
if (File.Exists(Path.Combine(outDir, bakedName))) colorFile = bakedName;
int step = Mathf.Max(1, (res - 1) / (TerrainMaxGrid - 1));
int n = ((res - 1) / step) + 1;
SetStatus("MapExporter: ландшафт " + ti + " - сетка " + n + "x" + n, 5f);
yield return null;
using (StreamWriter w = new StreamWriter(Path.Combine(outDir, prefix + ".obj"), false, new UTF8Encoding(false)))
{
w.WriteLine("# My Summer Car terrain " + ti);
w.WriteLine("mtllib " + prefix + ".mtl");
w.WriteLine("o Terrain_" + ti);
for (int zi = 0; zi < n; zi++)
{
for (int xi = 0; xi < n; xi++)
{
int hx = Mathf.Min(xi * step, res - 1);
int hz = Mathf.Min(zi * step, res - 1);
float x = pos.x + (hx / (float)(res - 1)) * size.x;
float z = pos.z + (hz / (float)(res - 1)) * size.z;
float y = pos.y + hgt[hz, hx] * size.y;
w.WriteLine("v " + F(-x) + " " + F(y) + " " + F(z));
}
}
for (int zi = 0; zi < n; zi++)
{
for (int xi = 0; xi < n; xi++)
{
float u = Mathf.Min(xi * step, res - 1) / (float)(res - 1);
float v = Mathf.Min(zi * step, res - 1) / (float)(res - 1);
w.WriteLine("vt " + F(u) + " " + F(v));
}
}
w.WriteLine("usemtl terrain");
for (int zi = 0; zi < n - 1; zi++)
{
for (int xi = 0; xi < n - 1; xi++)
{
int a = zi * n + xi + 1;
int b = (zi + 1) * n + xi + 1;
int c = zi * n + (xi + 1) + 1;
int d = (zi + 1) * n + (xi + 1) + 1;
w.WriteLine("f " + a + "/" + a + " " + c + "/" + c + " " + b + "/" + b);
w.WriteLine("f " + c + "/" + c + " " + d + "/" + d + " " + b + "/" + b);
}
}
}
StringBuilder tm = new StringBuilder();
tm.AppendLine("newmtl terrain");
tm.AppendLine("Kd 1 1 1");
if (colorFile != null) tm.AppendLine("map_Kd " + colorFile);
File.WriteAllText(Path.Combine(outDir, prefix + ".mtl"), tm.ToString());
}
private IEnumerator BakeTerrainTexture(TerrainData td, Vector3 size, string prefix)
{
SplatPrototype[] protos = td.splatPrototypes;
if (protos == null || protos.Length == 0) yield break;
int layers = protos.Length;
int aw = td.alphamapWidth;
int ah = td.alphamapHeight;
float[,,] alpha = td.GetAlphamaps(0, 0, aw, ah);
Texture2D[] tex = new Texture2D[layers];
Vector2[] tile = new Vector2[layers];
for (int i = 0; i < layers; i++)
{
tex[i] = ReadableCopy(protos[i].texture, 512);
tile[i] = protos[i].tileSize;
if (tile[i].x < 0.01f) tile[i].x = 1f;
if (tile[i].y < 0.01f) tile[i].y = 1f;
}
int S = TerrainTexSize;
Color32[] pixels = new Color32[S * S];
for (int py = 0; py < S; py++)
{
if ((py % 32) == 0)
{
SetStatus("MapExporter: текстура ландшафта " + (py * 100 / S) + "%", 5f);
yield return null;
}
float v = (py + 0.5f) / S;
int ay = Mathf.Min(ah - 1, (int)(v * ah));
for (int px = 0; px < S; px++)
{
float u = (px + 0.5f) / S;
int ax = Mathf.Min(aw - 1, (int)(u * aw));
float r = 0f, g = 0f, b = 0f, sum = 0f;
for (int l = 0; l < layers; l++)
{
float a = alpha[ay, ax, l];
if (a < 0.003f || tex[l] == null) continue;
Color c = tex[l].GetPixelBilinear(u * size.x / tile[l].x, v * size.z / tile[l].y);
r += c.r * a;
g += c.g * a;
b += c.b * a;
sum += a;
}
if (sum > 0.001f) { r /= sum; g /= sum; b /= sum; }
else { r = 0.4f; g = 0.45f; b = 0.3f; }
pixels[py * S + px] = new Color32((byte)(r * 255f), (byte)(g * 255f), (byte)(b * 255f), 255);
}
}
Texture2D outTex = new Texture2D(S, S, TextureFormat.ARGB32, false);
outTex.SetPixels32(pixels);
outTex.Apply();
File.WriteAllBytes(Path.Combine(outDir, prefix + "_color.png"), outTex.EncodeToPNG());
UnityEngine.Object.Destroy(outTex);
for (int i = 0; i < layers; i++)
{
if (tex[i] != null) UnityEngine.Object.Destroy(tex[i]);
}
}
private string GetMaterialName(Material m)
{
if (m == null) return "default";
int id = m.GetInstanceID();
string name;
if (matNames.TryGetValue(id, out name)) return name;
name = "mat_" + Clean(m.name) + "_" + Mathf.Abs(id);
matNames[id] = name;
mtl.AppendLine("newmtl " + name);
if (m.HasProperty("_Color"))
{
Color c = m.color;
mtl.AppendLine("Kd " + F(c.r) + " " + F(c.g) + " " + F(c.b));
}
else
{
mtl.AppendLine("Kd 0.8 0.8 0.8");
}
if (m.HasProperty("_MainTex") && m.mainTexture != null)
{
string file = ExportTexture(m.mainTexture);
if (file != null) mtl.AppendLine("map_Kd " + file);
}
mtl.AppendLine();
return name;
}
private Texture2D ReadableCopy(Texture tex, int maxSize)
{
if (tex == null) return null;
try
{
int tw = tex.width;
int th = tex.height;
while (tw > maxSize || th > maxSize)
{
tw /= 2;
th /= 2;
}
if (tw < 1) tw = 1;
if (th < 1) th = 1;
RenderTexture rt = RenderTexture.GetTemporary(tw, th, 0, RenderTextureFormat.ARGB32);
Graphics.Blit(tex, rt);
RenderTexture prev = RenderTexture.active;
RenderTexture.active = rt;
Texture2D copy = new Texture2D(tw, th, TextureFormat.ARGB32, false);
copy.ReadPixels(new Rect(0, 0, tw, th), 0, 0);
copy.Apply();
copy.wrapMode = TextureWrapMode.Repeat;
RenderTexture.active = prev;
RenderTexture.ReleaseTemporary(rt);
return copy;
}
catch (Exception e)
{
Debug.Log("MapExporter: не удалось скопировать текстуру " + tex.name + ": " + e.Message);
return null;
}
}
private string ExportTexture(Texture tex)
{
if (tex == null) return null;
int id = tex.GetInstanceID();
string existing;
if (texNames.TryGetValue(id, out existing)) return existing;
string file = null;
Texture2D copy = ReadableCopy(tex, MaxTextureSize);
if (copy != null)
{
try
{
string name = Clean(tex.name) + "_" + Mathf.Abs(id) + ".png";
File.WriteAllBytes(Path.Combine(texDir, name), copy.EncodeToPNG());
file = "textures/" + name;
}
catch (Exception e)
{
Debug.Log("MapExporter: не удалось сохранить текстуру " + tex.name + ": " + e.Message);
}
UnityEngine.Object.Destroy(copy);
}
texNames[id] = file;
return file;
}
}
}