Kursach_StealthAction/Assets/Tremble/Utils/MD3Util.cs
2026-10-11 19:16:11 +03:00

361 lines
11 KiB
C#

//
// This file is part of the Tremble package by Tiny Goose.
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
// This class based on BSP Map Tools for Unity by John Evans (evans3d512@gmail.com)
//
using System;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
namespace TinyGoose.Tremble
{
public static class MD3Util
{
private const int MAX_SURF_INDICES = 12000;
private const string IDP3_SIGNATURE = "IDP3";
private const int IDP3_VERSION = 15;
public static bool HasModel(GameObject prefab, out Bounds bounds)
{
bool result = false;
bounds = new();
foreach (MeshFilter mf in prefab.GetComponentsInChildren<MeshFilter>())
{
MeshRenderer mr = mf.GetComponent<MeshRenderer>();
if (!mr)
continue;
if (!mf.sharedMesh)
continue;
result = true;
GetTransformedMeshVertices(mr.transform, isSkinnedMesh: false, mf.sharedMesh, ref bounds);
}
foreach (SkinnedMeshRenderer smr in prefab.GetComponentsInChildren<SkinnedMeshRenderer>())
{
if (!smr.sharedMesh)
continue;
result = true;
GetTransformedMeshVertices(smr.transform, isSkinnedMesh: true, smr.sharedMesh, ref bounds);
}
return result;
}
private static Bounds GetPrefabBounds(GameObject prefab)
{
Bounds boundingBox = new();
foreach (MeshFilter mf in prefab.GetComponentsInChildren<MeshFilter>())
{
MeshRenderer mr = mf.GetComponent<MeshRenderer>();
if (!mr)
continue;
if (!mf.sharedMesh)
continue;
GetTransformedMeshVertices(mr.transform, isSkinnedMesh: false, mf.sharedMesh, ref boundingBox);
}
foreach (SkinnedMeshRenderer smr in prefab.GetComponentsInChildren<SkinnedMeshRenderer>())
{
if (!smr.sharedMesh)
continue;
GetTransformedMeshVertices(smr.transform, isSkinnedMesh: true, smr.sharedMesh, ref boundingBox);
}
return boundingBox;
}
public static Vector3 GetPrefabUnitySpawnOffset(GameObject prefab, float importScale)
{
Bounds boundingBox = GetPrefabBounds(prefab);
Vector3 boundsMin = boundingBox.min.UnityToQ3Vector(scale: importScale);
Vector3 boundsMax = boundingBox.max.UnityToQ3Vector(scale: importScale);
Vector3 boundsCentre = (boundsMin + boundsMax) / 2f;
return -boundsCentre.Q3ToUnityVector(importScale);
}
public static bool SaveMD3(GameObject prefab, string md3Path, MaterialNameLookup materialNameLookup, float importScale, out Bounds boundingBox)
{
// Gather MD3 surfaces to write from renderers
List<MD3Surface> surfaces = GatherMD3SurfacesFromRenderers(prefab, materialNameLookup, out boundingBox, out int allSurfacesSize);
if (surfaces.Count == 0)
return false;
// Now write the MD3 file
int fileSize = allSurfacesSize + 164;
string modelName = GetPathInFolder(md3Path, "baseq3");
using FileStream stream = new(md3Path, FileMode.Create, FileAccess.Write, FileShare.None, 32768);
using StreamWriteIO writer = new(stream);
// IDP3 signature & version
writer.WriteString(IDP3_SIGNATURE);
writer.WriteInt32(IDP3_VERSION);
// Model name (64 chars)
writer.WriteString(modelName, 64);
// Flags, frames, tags, surfaces, skins
writer.WriteInt32(0); // flags 0
writer.WriteInt32(1); // frames
writer.WriteInt32(0); // tags 0
writer.WriteInt32(surfaces.Count); // surfaces
writer.WriteInt32(0); // skins 0
// Write metrics
const int FRAME_OFFSET = 108;
writer.WriteInt32(FRAME_OFFSET);
const int TAGS_OFFSET = 164;
writer.WriteInt32(TAGS_OFFSET);
const int SURFACE_OFFSET = 164;
writer.WriteInt32(SURFACE_OFFSET);
writer.WriteInt32(fileSize); // file size bytes
// Write first frame
Vector3 boundsMin = boundingBox.min.UnityToQ3Vector(scale: importScale);
Vector3 boundsMax = boundingBox.max.UnityToQ3Vector(scale: importScale);
Vector3 boundsCentre = (boundsMin + boundsMax) / 2f;
FixBounds(ref boundsMin, ref boundsMax);
writer.WriteFloat(boundsMin.x); // bounds (min)
writer.WriteFloat(boundsMin.y);
writer.WriteFloat(boundsMin.z);
writer.WriteFloat(boundsMax.x); // bounds (max)
writer.WriteFloat(boundsMax.y);
writer.WriteFloat(boundsMax.z);
writer.WriteFloat(0f); // origin
writer.WriteFloat(0f);
writer.WriteFloat(0f);
writer.WriteFloat((boundsMax - boundsMin).magnitude); // radius
writer.WriteString("frame1", 16); // frame name
// Write surfaces
for (int surfIdx = 0; surfIdx < surfaces.Count; surfIdx++)
{
MD3Surface surface = surfaces[surfIdx];
int surfVertCount = surface.Vertices.Length;
int triangleCount = surfVertCount / 3;
int triangleOffset = 176;
int shaderOffset = 108;
int uvsOffset = triangleOffset + triangleCount * 12;
int vtxOffset = uvsOffset + surfVertCount * 8;
writer.WriteString(IDP3_SIGNATURE); // IDP3
// Surface
writer.WriteString(surface.Name, 64); // surf name
writer.WriteInt32(0); // flags 0
writer.WriteInt32(1); // frames
writer.WriteInt32(1); // shaders
writer.WriteInt32(surfVertCount);
writer.WriteInt32(triangleCount);
writer.WriteInt32(triangleOffset);
writer.WriteInt32(shaderOffset);
writer.WriteInt32(uvsOffset);
writer.WriteInt32(vtxOffset);
writer.WriteInt32(surface.DataSize);
// Shader
writer.WriteString($"models/{surface.Texture}.png", 64); // shader name
writer.WriteInt32(surfIdx); // Shader index
// Tris
for (int triIdx = 0; triIdx < surfVertCount; triIdx++)
{
writer.WriteInt32(triIdx);
}
// Surfs
for (int uvIdx = 0; uvIdx < surface.Uvs.Length; uvIdx++)
{
writer.WriteFloat(surface.Uvs[uvIdx].x);
writer.WriteFloat(1f - surface.Uvs[uvIdx].y);
}
// Verts
for (int vertIdx = 0; vertIdx < surfVertCount; vertIdx++)
{
Vector3 md3Vector = surface.Vertices[vertIdx].UnityToQ3Vector(scale: importScale);
md3Vector -= boundsCentre;
writer.WriteInt16((short)(md3Vector.x * 64f));
writer.WriteInt16((short)(md3Vector.y * 64f));
writer.WriteInt16((short)(md3Vector.z * 64f));
writer.WriteNormalAsLatLong(surface.Normals[vertIdx]);
}
}
return true;
}
private static List<MD3Surface> GatherMD3SurfacesFromRenderers(GameObject prefab, MaterialNameLookup materialNameLookup, out Bounds boundingBox, out int allSurfacesSize)
{
List<MD3Surface> surfaces = new();
allSurfacesSize = 0;
boundingBox = new();
foreach (MeshFilter mf in prefab.GetComponentsInChildren<MeshFilter>())
{
MeshRenderer mr = mf.GetComponent<MeshRenderer>();
if (!mr)
continue;
AddMeshToMD3(mr.transform, isSkinnedMesh: false, mf.sharedMesh, mr.sharedMaterials, surfaces, materialNameLookup, ref allSurfacesSize, ref boundingBox);
}
foreach (SkinnedMeshRenderer smr in prefab.GetComponentsInChildren<SkinnedMeshRenderer>())
{
AddMeshToMD3(smr.transform, isSkinnedMesh: true, smr.sharedMesh, smr.sharedMaterials, surfaces, materialNameLookup, ref allSurfacesSize, ref boundingBox);
}
return surfaces;
}
private static void FixBounds(ref Vector3 min, ref Vector3 max)
{
Vector3 newMin = new(Mathf.Min(min.x, max.x), Mathf.Min(min.y, max.y), Mathf.Min(min.z, max.z));
Vector3 newMax = new(Mathf.Max(min.x, max.x), Mathf.Max(min.y, max.y), Mathf.Max(min.z, max.z));
min = newMin;
max = newMax;
}
private static Vector3[] GetTransformedMeshVertices(Transform meshTransform, bool isSkinnedMesh, Mesh unityMesh, ref Bounds allBounds)
{
// Gather mesh data
Vector3[] meshVertices = unityMesh.vertices;
Vector3 min = allBounds.min;
Vector3 max = allBounds.max;
Matrix4x4 ltwMatrix = meshTransform.localToWorldMatrix;
for (int i = 0; i < meshVertices.Length; i++)
{
// Transform vertex according to the prefab root it's from
if (isSkinnedMesh)
{
meshVertices[i] = meshTransform.TransformVector(meshVertices[i]);
}
else
{
meshVertices[i] = ltwMatrix.MultiplyPoint(meshVertices[i]);
}
// Clamp vertices, MD3 uses Int16 vectors for vertex positions, so each vertex can be in range from -12.75 to 12.75
float meshVertX = meshVertices[i].x; // = Mathf.Clamp(meshVertices[i].x, -12.75f, +12.75f);
float meshVertY = meshVertices[i].y; // = Mathf.Clamp(meshVertices[i].y, -12.75f, +12.75f);
float meshVertZ = meshVertices[i].z; // = Mathf.Clamp(meshVertices[i].z, -12.75f, +12.75f);
// Inlined Mathf.Min and Mathf.Max
min.x = min.x < meshVertX ? min.x : meshVertX;
min.y = min.y < meshVertY ? min.y : meshVertY;
min.z = min.z < meshVertZ ? min.z : meshVertZ;
max.x = max.x > meshVertX ? max.x : meshVertX;
max.y = max.y > meshVertY ? max.y : meshVertY;
max.z = max.z > meshVertZ ? max.z : meshVertZ;
}
allBounds.SetMinMax(min, max);
return meshVertices;
}
private static void AddMeshToMD3(Transform meshTransform, bool isSkinnedMesh, Mesh unityMesh, Material[] materials, List<MD3Surface> surfaces, MaterialNameLookup materialNameLookup, ref int allSurfacesSize, ref Bounds allBounds)
{
// Gather triangles per submesh
List<int[]> submeshTriIndices = new(unityMesh.subMeshCount);
for (int i = 0; i < unityMesh.subMeshCount; i++)
{
submeshTriIndices.Add(unityMesh.GetTriangles(i));
}
// Gather mesh data
Vector3[] meshVertices = GetTransformedMeshVertices(meshTransform, isSkinnedMesh, unityMesh, ref allBounds);
Vector3[] meshNormals = unityMesh.normals;
Vector2[] meshUVs = unityMesh.uv;
// Write one surface per Unity submesh
for (int submeshIdx = 0; submeshIdx < unityMesh.subMeshCount; submeshIdx++)
{
if (submeshTriIndices[submeshIdx].Length >= MAX_SURF_INDICES)
{
Debug.LogError("Mesh has too many surfaces - skipping!");
continue;
}
// Add verts and triangles
int vertIndexCount = submeshTriIndices[submeshIdx].Length;
Vector3[] surfaceVertices = new Vector3[vertIndexCount];
Vector3[] surfaceNormals = new Vector3[vertIndexCount];
Vector2[] surfaceUVs = new Vector2[vertIndexCount];
int[] thisSubmeshTriIndices = submeshTriIndices[submeshIdx];
for (int vIdx = 0; vIdx < vertIndexCount; vIdx++)
{
int index = thisSubmeshTriIndices[vIdx];
surfaceVertices[vIdx] = meshVertices[index];
surfaceNormals[vIdx] = meshNormals[index];
// Some meshes have no UVs (uh-oh...)
if (meshUVs.Length > 0)
{
surfaceUVs[vIdx] = meshUVs[index];
}
}
// Write material names
string textureName;
if (materials[submeshIdx] && materialNameLookup != null)
{
string path = TrembleAssetLoader.GetPath(materials[submeshIdx]);
textureName = materialNameLookup.GetPrefabNameFromMaterialPath(path);
}
else
{
textureName = "missing";
}
// Create surface
MD3Surface currentSurface = new()
{
Name = $"surface{surfaces.Count}",
Texture = textureName,
Vertices = surfaceVertices,
Normals = surfaceNormals,
Uvs = surfaceUVs,
};
surfaces.Add(currentSurface);
allSurfacesSize += currentSurface.DataSize;
}
}
private static string GetPathInFolder(string path, string folder)
{
int i = path.IndexOfInvariant(folder + "/");
return path.Substring(i + folder.Length + 1);
}
}
}