using UnityEngine;
using UnityEditor;
using System.IO;
using System.Text;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using UnityEngine.ProBuilder;
namespace UnityEditor.ProBuilder
{
///
/// A set of options used when exporting OBJ models.
///
sealed class ObjOptions
{
// Coordinate system to use when exporting. Unity is left handed where most other
// applications are right handed.
public enum Handedness
{
Left,
Right
};
// Convert from left to right handed coordinates on export?
public Handedness handedness = Handedness.Right;
// If copyTextures is true then the material textures will be copied to the export path.
// If false the material library will point to the existing texture path in the Unity project.
public bool copyTextures = true;
// Should meshes be exported in local (false) or world (true) space.
public bool applyTransforms = true;
// Some modeling programs support reading vertex colors as an additional {r, g, b} following
// the vertex position {x, y, z}
public bool vertexColors = false;
// Write the texture map offset and scale. Not supported by all importers.
public bool textureOffsetScale = false;
}
///
/// Utilities for writing mesh data to the Wavefront OBJ format.
///
static class ObjExporter
{
/**
* Standard shader defines:
* Albedo | map_Kd | _MainTex
* Metallic | Pm/map_Pm* | _MetallicGlossMap
* Normal | map_bump / bump | _BumpMap
* Height | disp | _ParallaxMap
* Occlusion | | _OcclusionMap
* Emission | Ke/map_Ke* | _EmissionMap
* DetailMask | map_d | _DetailMask
* DetailAlbedo | | _DetailAlbedoMap
* DetailNormal | | _DetailNormalMap
*
* *http://exocortex.com/blog/extending_wavefront_mtl_to_support_pbr
*/
static Dictionary s_TextureMapKeys = new Dictionary
{
{ "_MainTex", "map_Kd" },
{ "_MetallicGlossMap", "map_Pm" },
{ "_BumpMap", "bump" },
{ "_ParallaxMap", "disp" },
{ "_EmissionMap", "map_Ke" },
{ "_DetailMask", "map_d" },
// Alternative naming conventions - possibly useful if someone
// runs into an issue with another 3d modeling app.
// { "_MetallicGlossMap", "Pm" },
// { "_BumpMap", "map_bump" },
// { "_BumpMap", "norm" },
// { "_EmissionMap", "Ke" },
};
///
/// Write the contents of a single obj & mtl from a set of models.
///
///
///
///
///
///
///
///
public static bool Export(string name, IEnumerable models, out string objContents, out string mtlContents, out List textures, ObjOptions options = null)
{
if (models == null || models.Count() < 1)
{
objContents = null;
mtlContents = null;
textures = null;
return false;
}
Dictionary materialMap = null;
if (options == null)
options = new ObjOptions();
mtlContents = WriteMtlContents(models, options, out materialMap, out textures);
objContents = WriteObjContents(name, models, materialMap, options);
return true;
}
static string WriteObjContents(string name, IEnumerable models, Dictionary materialMap, ObjOptions options)
{
// Empty names in OBJ groups can crash some 3d programs (meshlab)
if (string.IsNullOrEmpty(name))
name = "ProBuilderModel";
StringBuilder sb = new StringBuilder();
sb.AppendLine("# ProBuilder " + Version.currentInfo.MajorMinorPatch);
sb.AppendLine("# https://unity3d.com/unity/features/worldbuilding/probuilder");
sb.AppendLine(string.Format("# {0}", System.DateTime.Now));
sb.AppendLine();
sb.AppendLine(string.Format("mtllib ./{0}.mtl", name.Replace(" ", "_")));
sb.AppendLine(string.Format("o {0}", name));
sb.AppendLine();
// obj orders indices 1 indexed
int positionOffset = 1;
int normalOffset = 1;
int textureOffset = 1;
bool reverseWinding = options.handedness == ObjOptions.Handedness.Right;
float handedness = options.handedness == ObjOptions.Handedness.Left ? 1f : -1f;
foreach (Model model in models)
{
int subMeshCount = model.submeshCount;
Matrix4x4 matrix = options.applyTransforms ? model.matrix : Matrix4x4.identity;
int vertexCount = model.vertexCount;
Vector3[] positions;
Color[] colors;
Vector2[] textures0;
Vector3[] normals;
Vector4[] tangent;
Vector2[] uv2;
List uv3;
List uv4;
MeshArrays attribs = MeshArrays.Position | MeshArrays.Normal | MeshArrays.Texture0;
if (options.vertexColors)
attribs = attribs | MeshArrays.Color;
Vertex.GetArrays(model.vertices, out positions, out colors, out textures0, out normals, out tangent, out uv2, out uv3, out uv4, attribs);
// Can skip this entirely if handedness matches Unity & not applying transforms.
// matrix is set to identity if not applying transforms.
if (options.handedness != ObjOptions.Handedness.Left || options.applyTransforms)
{
for (int i = 0; i < vertexCount; i++)
{
if (positions != null)
{
positions[i] = matrix.MultiplyPoint3x4(positions[i]);
positions[i].x *= handedness;
}
if (normals != null)
{
normals[i] = matrix.MultiplyVector(normals[i]);
normals[i].x *= handedness;
}
}
}
sb.AppendLine(string.Format("g {0}", model.name));
var positionIndexMap = AppendPositions(sb, positions, colors, true, options.vertexColors);
sb.AppendLine();
var textureIndexMap = AppendArrayVec2(sb, textures0, "vt", true);
sb.AppendLine();
var normalIndexMap = AppendArrayVec3(sb, normals, "vn", true);
sb.AppendLine();
// Material assignment
for (int submeshIndex = 0; submeshIndex < subMeshCount; submeshIndex++)
{
Submesh submesh = model.submeshes[submeshIndex];
if (subMeshCount > 1)
sb.AppendLine(string.Format("g {0}_{1}", model.name, submeshIndex));
else
sb.AppendLine(string.Format("g {0}", model.name));
string materialName = "";
if (materialMap.TryGetValue(model.materials[submeshIndex], out materialName))
sb.AppendLine(string.Format("usemtl {0}", materialName));
else
sb.AppendLine(string.Format("usemtl {0}", "null"));
int[] indexes = submesh.m_Indexes;
int inc = submesh.m_Topology == MeshTopology.Quads ? 4 : 3;
int inc1 = inc - 1;
int o0 = reverseWinding ? inc1 : 0;
int o1 = reverseWinding ? inc1 - 1 : 1;
int o2 = reverseWinding ? inc1 - 2 : 2;
int o3 = reverseWinding ? inc1 - 3 : 3;
for (int ff = 0; ff < indexes.Length; ff += inc)
{
int p0 = positionIndexMap[indexes[ff + o0]] + positionOffset;
int p1 = positionIndexMap[indexes[ff + o1]] + positionOffset;
int p2 = positionIndexMap[indexes[ff + o2]] + positionOffset;
int t0 = textureIndexMap[indexes[ff + o0]] + textureOffset;
int t1 = textureIndexMap[indexes[ff + o1]] + textureOffset;
int t2 = textureIndexMap[indexes[ff + o2]] + textureOffset;
int n0 = normalIndexMap[indexes[ff + o0]] + normalOffset;
int n1 = normalIndexMap[indexes[ff + o1]] + normalOffset;
int n2 = normalIndexMap[indexes[ff + o2]] + normalOffset;
if (inc == 4)
{
int p3 = positionIndexMap[indexes[ff + o3]] + positionOffset;
int n3 = normalIndexMap[indexes[ff + o3]] + normalOffset;
int t3 = textureIndexMap[indexes[ff + o3]] + textureOffset;
sb.AppendLine(string.Format(CultureInfo.InvariantCulture,
"f {0}/{4}/{8} {1}/{5}/{9} {2}/{6}/{10} {3}/{7}/{11}",
p0, p1, p2, p3,
t0, t1, t2, t3,
n0, n1, n2, n3
));
}
else
{
sb.AppendLine(string.Format(CultureInfo.InvariantCulture,
"f {0}/{3}/{6} {1}/{4}/{7} {2}/{5}/{8}",
p0, p1, p2,
t0, t1, t2,
n0, n1, n2
));
}
}
sb.AppendLine();
}
positionOffset += positionIndexMap.Count;
normalOffset += normalIndexMap.Count;
textureOffset += textureIndexMap.Count;
}
return sb.ToString();
}
///
/// Write the material file for an OBJ. This function handles making the list of Materials unique & ensuring unique names for each group. Material to named mtl group are stored in materialMap.
///
///
///
///
///
///
static string WriteMtlContents(IEnumerable models, ObjOptions options, out Dictionary materialMap, out List textures)
{
materialMap = new Dictionary();
foreach (Model model in models)
{
for (int i = 0, c = model.submeshCount; i < c; i++)
{
Material material = model.materials[i];
if (material == null)
continue;
if (!materialMap.ContainsKey(material))
{
string escapedName = material.name.Replace(" ", "_");
string name = escapedName;
int nameIncrement = 1;
while (materialMap.Any(x => x.Value.Equals(name)))
name = string.Format("{0}_{1}", escapedName, nameIncrement++);
materialMap.Add(material, name);
}
}
}
StringBuilder sb = new StringBuilder();
textures = new List();
foreach (KeyValuePair group in materialMap)
{
Material mat = group.Key;
sb.AppendLine(string.Format("newmtl {0}", group.Value));
// Texture maps
if (mat.shader != null)
{
for (int i = 0; i < ShaderUtil.GetPropertyCount(mat.shader); i++)
{
if (ShaderUtil.GetPropertyType(mat.shader, i) != ShaderUtil.ShaderPropertyType.TexEnv)
continue;
string texPropertyName = ShaderUtil.GetPropertyName(mat.shader, i);
Texture texture = mat.GetTexture(texPropertyName);
string path = texture != null ? AssetDatabase.GetAssetPath(texture) : null;
if (!string.IsNullOrEmpty(path))
{
if (options.copyTextures)
textures.Add(path);
// remove "Assets/" from start of path
path = path.Substring(7, path.Length - 7);
string textureName = options.copyTextures ? Path.GetFileName(path) : string.Format("{0}/{1}", Application.dataPath, path);
string mtlKey = null;
if (s_TextureMapKeys.TryGetValue(texPropertyName, out mtlKey))
{
Vector2 offset = mat.GetTextureOffset(texPropertyName);
Vector2 scale = mat.GetTextureScale(texPropertyName);
if (options.textureOffsetScale)
sb.AppendLine(string.Format(CultureInfo.InvariantCulture, "{0} -o {1} {2} -s {3} {4} {5}", mtlKey, offset.x, offset.y, scale.x, scale.y, textureName));
else
sb.AppendLine(string.Format("{0} {1}", mtlKey, textureName));
}
}
}
}
if (mat.HasProperty("_Color"))
{
Color color = mat.color;
// Diffuse
sb.AppendLine(string.Format("Kd {0}", string.Format(CultureInfo.InvariantCulture, "{0} {1} {2}", color.r, color.g, color.b)));
// Transparency
sb.AppendLine(string.Format(CultureInfo.InvariantCulture, "d {0}", color.a));
}
else
{
sb.AppendLine("Kd 1.0 1.0 1.0");
sb.AppendLine("d 1.0");
}
sb.AppendLine();
}
return sb.ToString();
}
struct PositionColorKey : System.IEquatable
{
public IntVec3 position;
public IntVec4 color;
public PositionColorKey(Vector3 p, Color c)
{
position = new IntVec3(p);
color = new IntVec4(c);
}
public bool Equals(PositionColorKey other)
{
return position.Equals(other.position) && color.Equals(other.color);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj))
return false;
return obj is PositionColorKey && Equals(obj);
}
public override int GetHashCode()
{
unchecked
{
return (position.GetHashCode() * 397) ^ color.GetHashCode();
}
}
}
// AppendPositions separately from AppendArrayVec3 to support the non-spec color extension that some DCCs can read
static Dictionary AppendPositions(StringBuilder sb, Vector3[] positions, Color[] colors, bool mergeCoincident, bool includeColors)
{
var writeColors = includeColors && colors != null && colors.Length == positions.Length;
Dictionary common = new Dictionary();
Dictionary map = new Dictionary();
int index = 0;
for (int i = 0, c = positions.Length; i < c; i++)
{
var position = positions[i];
var color = includeColors ? colors[i] : Color.white;
var key = new PositionColorKey(position, color);
int vertexIndex;
if (mergeCoincident)
{
if (!common.TryGetValue(key, out vertexIndex))
{
vertexIndex = index++;
common.Add(key, vertexIndex);
}
else
{
map.Add(i, vertexIndex);
continue;
}
}
else
{
vertexIndex = i;
}
map.Add(i, vertexIndex);
if (writeColors)
{
sb.AppendLine(string.Format(CultureInfo.InvariantCulture, "v {0} {1} {2} {3} {4} {5}",
position.x,
position.y,
position.z,
color.r,
color.g,
color.b));
}
else
{
sb.AppendLine(string.Format(CultureInfo.InvariantCulture, "v {0} {1} {2}",
position.x,
position.y,
position.z));
}
}
return map;
}
static Dictionary AppendArrayVec2(StringBuilder sb, Vector2[] array, string prefix, bool mergeCoincident)
{
if (array == null)
return null;
Dictionary common = new Dictionary();
Dictionary map = new Dictionary();
int index = 0;
for (int i = 0, c = array.Length; i < c; i++)
{
var texture = array[i];
var key = new IntVec2(texture);
int vertexIndex;
if (mergeCoincident)
{
if (!common.TryGetValue(key, out vertexIndex))
{
vertexIndex = index++;
common.Add(key, vertexIndex);
}
else
{
map.Add(i, vertexIndex);
continue;
}
}
else
{
vertexIndex = i;
}
map.Add(i, vertexIndex);
sb.AppendLine(string.Format(CultureInfo.InvariantCulture, "{0} {1} {2}",
prefix,
texture.x,
texture.y));
}
return map;
}
static Dictionary AppendArrayVec3(StringBuilder sb, Vector3[] array, string prefix, bool mergeCoincident)
{
Dictionary common = new Dictionary();
Dictionary map = new Dictionary();
int index = 0;
for (int i = 0, c = array.Length; i < c; i++)
{
var value = array[i];
var key = new IntVec3(value);
int vertexIndex;
if (mergeCoincident)
{
if (!common.TryGetValue(key, out vertexIndex))
{
vertexIndex = index++;
common.Add(key, vertexIndex);
}
else
{
map.Add(i, vertexIndex);
continue;
}
}
else
{
vertexIndex = i;
}
map.Add(i, vertexIndex);
sb.AppendLine(string.Format(CultureInfo.InvariantCulture, "{0} {1} {2} {3}",
prefix,
value.x,
value.y,
value.z));
}
return map;
}
}
}