Skip to content

Instantly share code, notes, and snippets.

@Frooxius
Created July 8, 2018 22:26
Show Gist options
  • Save Frooxius/7d251f66d331ed92a43b052e00876721 to your computer and use it in GitHub Desktop.
Save Frooxius/7d251f66d331ed92a43b052e00876721 to your computer and use it in GitHub Desktop.
Model Exporter
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Assimp;
using Assimp.Configs;
using System.IO;
using BaseX;
namespace FrooxEngine
{
public static class ModelExporter
{
struct MeshMatPair
{
public readonly IAssetProvider<Mesh> mesh;
public readonly IAssetProvider<Material> material;
public MeshMatPair(IAssetProvider<Mesh> mesh, IAssetProvider<Material> material)
{
this.mesh = mesh;
this.material = material;
}
}
class ExportData
{
Engine Engine { get { return root.Engine; } }
public readonly Scene scene;
public readonly Slot root;
public readonly string exportPath;
List<MeshMatPair> meshes = new List<MeshMatPair>();
List<IAssetProvider<Material>> materials = new List<IAssetProvider<Material>>();
Dictionary<IAssetProvider<ITexture2D>, string> textures = new Dictionary<IAssetProvider<ITexture2D>, string>();
List<Task> processingJobs = new List<Task>();
public ExportData(Scene scene, Slot root, string exportPath)
{
this.scene = scene;
this.root = root;
this.exportPath = exportPath;
Directory.CreateDirectory(Path.Combine(exportPath, "textures"));
}
public int GenerateMeshIndex(IAssetProvider<Mesh> mesh, IAssetProvider<Material> material, int materialIndex)
{
var pair = new MeshMatPair(mesh, material);
int index = meshes.IndexOf(pair);
if(index < 0)
{
meshes.Add(pair);
index = meshes.Count - 1;
var assimpMesh = new Assimp.Mesh();
assimpMesh.MaterialIndex = GenerateMaterialIndex(material);
// Start the mesh copying in the background
processingJobs.Add(Engine.JobProcessor.Enqueue(() => ProcessMesh(assimpMesh, mesh.Asset, materialIndex)));
scene.Meshes.Add(assimpMesh);
}
return index;
}
int GenerateMaterialIndex(IAssetProvider<Material> material)
{
int index = materials.IndexOf(material);
if(index < 0)
{
materials.Add(material);
index = materials.Count - 1;
var assimpMaterial = ProcessMaterial(material, this);
scene.Materials.Add(assimpMaterial);
}
return index;
}
public string GenerateTexturePath(IAssetProvider<ITexture2D> tex2D)
{
string path;
if(!textures.TryGetValue(tex2D, out path))
{
// Process the texture
var staticTex = tex2D as StaticTexture2D;
string localFile = null;
if (staticTex != null)
localFile = Engine.LocalDB.TryFetchAssetRecord(staticTex.URL.Value)?.path;
if(File.Exists(localFile))
{
// just copy it over
path = Path.Combine("textures", textures.Count.ToString("D4") + Path.GetExtension(localFile));
processingJobs.Add(Engine.JobProcessor.Enqueue(() =>
{
File.Copy(localFile, Path.Combine(exportPath, path), true);
}));
}
else
{
var tex2Dasset = tex2D.Asset as Texture2D;
var bitmap2D = tex2Dasset?.Data;
if (bitmap2D == null)
{
// cannot save it, skip
path = null;
}
else
{
// save the in-memory texture data to a file
path = Path.Combine("textures", textures.Count.ToString("D4") + ".png");
processingJobs.Add(Engine.JobProcessor.Enqueue(() =>
{
var lockobj = new object();
tex2Dasset.ModificationLock(lockobj);
bitmap2D.Save(Path.Combine(exportPath, path));
tex2Dasset.ModificationUnlock(lockobj);
}));
}
}
textures.Add(tex2D, path);
}
return path;
}
public float3 GlobalPositionInScene(Slot slot)
{
return root.GlobalPointToLocal(slot.GlobalPosition);
}
public floatQ GlobalRotationInScene(Slot slot)
{
return root.GlobalRotationToLocal(slot.GlobalRotation);
}
public MultiTask GetWaitMultitask()
{
return new MultiTask(processingJobs);
}
}
public static IEnumerator<Context> ExportModel(Slot slot, string targetFile)
{
var extension = Path.GetExtension(targetFile).Substring(1).ToLower();
// Check format identifier
var context = new AssimpContext();
string formatId = null;
/*foreach (var format in context.GetSupportedExportFormats())
if (format.FileExtension == extension)
{
formatId = format.FormatId;
break;
}*/
// The Supported formats is a bit iffy, it seems to return garbage characters, which messes the detection up
switch(extension)
{
case "dae":
formatId = "collada";
break;
case "obj":
formatId = "obj";
break;
case "x":
formatId = "x";
break;
case "stl":
formatId = "stlb"; // binary STL
break;
case "ply":
formatId = "ply";
break;
case "glb":
formatId = "glb";
break;
case "3ds":
formatId = "3ds";
break;
}
if(formatId == null)
{
var str = new StringBuilder();
foreach (var f in context.GetSupportedExportFormats())
str.AppendLine($"\tID: {f.FormatId}\tExt: {f.FileExtension}\tInfo: {f.Description}");
UniLog.Error("Unsupported export format: " + extension + ", supported formats: \n" + str);
yield break;
}
UniLog.Log("Starting export in format: " + formatId);
var targetPath = Path.GetDirectoryName(targetFile);
Directory.CreateDirectory(targetPath);
var scene = new Scene();
var data = new ExportData(scene, slot, targetPath);
scene.RootNode = GenerateNode(slot, data);
UniLog.Log("Graph processed, waiting for mesh and texture tasks to finish");
yield return Context.WaitFor(data.GetWaitMultitask());
yield return Context.ToBackground();
UniLog.Log("Textures and meshes processing finished, saving scene");
bool result = context.ExportFile(scene, targetFile, formatId, PostProcessSteps.MakeLeftHanded
| PostProcessSteps.FlipWindingOrder
| PostProcessSteps.ValidateDataStructure);
context.Dispose();
UniLog.Log("Finished export, success: " + result);
}
static Node GenerateNode(Slot slot, ExportData data)
{
var node = new Node(slot.Name);
node.Transform = slot.TRS.ToAssimp();
// Process components
foreach(var c in slot.Components)
{
if (!c.Enabled)
continue;
if (c is MeshRenderer)
ProcessMeshRenderer((MeshRenderer)c, node, data);
if (c is Light)
ProcessLight((Light)c, slot, node, data);
}
// Process children
foreach (var child in slot.Children)
if(child.IsActive)
node.Children.Add(GenerateNode(child, data));
return node;
}
static void ProcessMeshRenderer(MeshRenderer meshRenderer, Node node, ExportData data)
{
int materialIndex = 0;
foreach(var m in meshRenderer.Materials)
{
var meshProvider = meshRenderer.Mesh.Target;
var materialProvider = m;
// skip if there's no MeshX data
if (meshProvider?.Asset?.Data == null)
continue;
node.MeshIndices.Add(data.GenerateMeshIndex(meshProvider, materialProvider, materialIndex));
materialIndex++;
}
}
static Assimp.Material ProcessMaterial(IAssetProvider<Material> material, ExportData data)
{
if (material is PBS_Material)
return ProcessPBS_Material((PBS_Material)material, data);
if (material is PBSLerpMaterial)
return ProcessPBSLerpMaterial((PBSLerpMaterial)material, data);
if (material is UnlitMaterial)
return ProcessUnlitMaterial((UnlitMaterial)material, data);
return new Assimp.Material(); // default material
}
static Assimp.Material ProcessPBS_Material(PBS_Material pbs, ExportData data)
{
var mat = new Assimp.Material();
mat.ColorDiffuse = pbs.AlbedoColor.Value.ToAssimp();
mat.ColorEmissive = pbs.EmissiveColor.Value.ToAssimp();
ProcessTexture(pbs.AlbedoTexture.Target, mat, TextureType.Diffuse, data);
ProcessTexture(pbs.EmissiveMap.Target, mat, TextureType.Emissive, data);
ProcessTexture(pbs.NormalMap.Target, mat, TextureType.Normals, data);
ProcessTexture(pbs.HeightMap.Target, mat, TextureType.Height, data);
ProcessTexture(pbs.OcclusionMap.Target, mat, TextureType.Ambient, data);
var pbsSpec = pbs as PBS_Specular;
if(pbsSpec != null)
{
mat.ColorSpecular = pbsSpec.SpecularColor.Value.ToAssimp();
ProcessTexture(pbsSpec.SpecularMap.Target, mat, TextureType.Specular, data);
}
var pbsMetallic = pbs as PBS_Metallic;
if(pbsMetallic != null)
{
mat.Shininess = pbsMetallic.Metallic.Value;
ProcessTexture(pbsMetallic.MetallicMap.Target, mat, TextureType.Shininess, data);
}
return mat;
}
static Assimp.Material ProcessPBSLerpMaterial(PBSLerpMaterial pbs, ExportData data)
{
var mat = new Assimp.Material();
// TODO!!! Preblend the textures using the lerp texture?
mat.ColorDiffuse = pbs.AlbedoColor0.Value.ToAssimp();
mat.ColorEmissive = pbs.EmissiveColor0.Value.ToAssimp();
ProcessTexture(pbs.AlbedoTexture0.Target, mat, TextureType.Diffuse, data);
ProcessTexture(pbs.EmissiveMap0.Target, mat, TextureType.Emissive, data);
ProcessTexture(pbs.NormalMap0.Target, mat, TextureType.Normals, data);
ProcessTexture(pbs.OcclusionMap0.Target, mat, TextureType.Ambient, data);
var pbsSpec = pbs as PBSLerpSpecular;
if (pbsSpec != null)
{
mat.ColorSpecular = pbsSpec.SpecularColor0.Value.ToAssimp();
ProcessTexture(pbsSpec.SpecularMap0.Target, mat, TextureType.Specular, data);
}
var pbsMetallic = pbs as PBSLerpMetallic;
if (pbsMetallic != null)
{
mat.Shininess = pbsMetallic.Metallic0.Value;
ProcessTexture(pbsMetallic.MetallicMap0.Target, mat, TextureType.Shininess, data);
}
return mat;
}
static Assimp.Material ProcessUnlitMaterial(UnlitMaterial unlit, ExportData data)
{
var mat = new Assimp.Material();
mat.ColorDiffuse = color.Black.ToAssimp();
mat.ColorEmissive = unlit.TintColor.Value.ToAssimp();
ProcessTexture(unlit.Texture.Target, mat, TextureType.Emissive, data);
if (unlit.BlendMode.Value == BlendMode.Additive)
mat.BlendMode = Assimp.BlendMode.Additive;
return mat;
}
static Assimp.Material ProcessFresnelMaterial(FresnelMaterial fresnel, ExportData data)
{
var mat = new Assimp.Material();
mat.ColorDiffuse = fresnel.NearColor.Value.ToAssimp();
ProcessTexture(fresnel.NearTexture.Target, mat, TextureType.Diffuse, data);
ProcessTexture(fresnel.NormalMap.Target, mat, TextureType.Normals, data);
if (fresnel.BlendMode.Value == BlendMode.Additive)
mat.BlendMode = Assimp.BlendMode.Additive;
return mat;
}
static void ProcessTexture(IAssetProvider<ITexture2D> textureProvider, Assimp.Material material, TextureType type,
ExportData data)
{
if(textureProvider != null)
{
var tex = new TextureSlot();
tex.FilePath = data.GenerateTexturePath(textureProvider);
// Failed to generate the texture, skip
if (tex.FilePath == null)
return;
tex.TextureType = type;
tex.Mapping = Assimp.TextureMapping.FromUV;
tex.WrapModeU = TextureWrapMode.Wrap;
tex.WrapModeV = TextureWrapMode.Wrap;
material.AddMaterialTexture(ref tex);
}
}
static void ProcessMesh(Assimp.Mesh assimpMesh, Mesh neosMesh, int materialIndex)
{
var lockObj = new object();
neosMesh.ModificationLock(lockObj);
var meshx = neosMesh.Data;
for (int uv = 0; uv < MeshX.UV_CHANNELS; uv++)
if (meshx.GetHasUV(uv))
assimpMesh.UVComponentCount[uv] = 2;
else
assimpMesh.UVComponentCount[uv] = 0;
// Copy over the data
for (int i = 0; i < meshx.VertexCount; i++)
{
var v = meshx.GetVertex(i);
// TODO!!! What about multimaterial meshes? Prune the vertices first?
assimpMesh.Vertices.Add(Filter(v.Position).ToAssimp());
if (meshx.HasNormals)
assimpMesh.Normals.Add(Filter(v.Normal).ToAssimp());
if (meshx.HasTangents)
assimpMesh.Tangents.Add(Filter(v.Tangent).ToAssimp());
if (meshx.HasColors)
assimpMesh.VertexColorChannels[0].Add(v.Color.ToAssimp());
for (int uv = 0; uv < MeshX.UV_CHANNELS; uv++)
if (meshx.GetHasUV(uv))
assimpMesh.TextureCoordinateChannels[uv].Add(Filter(v.GetUV(uv)).xy_.ToAssimp());
}
var submesh = meshx.GetSubmesh(materialIndex);
var triangles = submesh as TriangleSubmesh;
var points = submesh as PointSubmesh;
if(triangles != null)
{
for (int i = 0; i < triangles.Count; i++)
{
var t = triangles.GetTriangle(i);
var face = new Face();
face.Indices.Add(t.Vertex0Index);
face.Indices.Add(t.Vertex1Index);
face.Indices.Add(t.Vertex2Index);
assimpMesh.Faces.Add(face);
}
}
else if(points != null)
{
for(int i = 0; i < points.Count; i++)
{
var p = points.GetPoint(i);
var face = new Face();
face.Indices.Add(p.VertexIndex);
assimpMesh.Faces.Add(face);
}
}
neosMesh.ModificationUnlock(lockObj);
}
static void ProcessLight(Light light, Slot slot, Node node, ExportData data)
{
// Add child node, because the light name has to match the node name and there's no guarantee that they're unique
// and that there's no mesh on that node as well
var lightName = "Light-" + light.ReferenceID.ToString();
var lightNode = new Node(lightName);
node.Children.Add(lightNode);
var assimpLight = new Assimp.Light();
assimpLight.Name = lightName;
assimpLight.Position = data.GlobalPositionInScene(slot).ToAssimp();
//assimpLight.Direction = (data.GlobalRotationInScene(slot) * float3.Forward).ToAssimp();
assimpLight.Direction = float3.Forward.ToAssimp(); // seems to ignore this on export anyway
assimpLight.ColorDiffuse = light.Color.Value.ToAssimpRGB();
assimpLight.ColorSpecular = light.Color.Value.ToAssimpRGB();
assimpLight.AttenuationConstant = Filter(1f / light.Intensity.Value);
assimpLight.AttenuationLinear = 0f;
assimpLight.AttenuationQuadratic = Filter(1f / light.Range.Value);
// TODO!!! The spotlight angle seems to be broken, not sure if it's assimp or something else
switch (light.LightType.Value)
{
case LightType.Point:
assimpLight.LightType = LightSourceType.Point;
assimpLight.AngleInnerCone = MathX.TAU;
assimpLight.AngleOuterCone = MathX.TAU;
break;
case LightType.Spot:
assimpLight.LightType = LightSourceType.Spot;
assimpLight.AngleInnerCone = 0f;
assimpLight.AngleOuterCone = light.SpotAngle.Value * MathX.Deg2Rad;
break;
case LightType.Directional:
assimpLight.LightType = LightSourceType.Directional;
break;
}
//UniLog.Log("Generated light:\n" + Newtonsoft.Json.JsonConvert.SerializeObject(assimpLight));
data.scene.Lights.Add(assimpLight);
}
static float Filter(float val)
{
if (float.IsNaN(val) || float.IsInfinity(val))
return 0f;
return val;
}
static float2 Filter(float2 val)
{
return new float2(Filter(val.x), Filter(val.y));
}
static float3 Filter(float3 val)
{
return new float3(Filter(val.x), Filter(val.y), Filter(val.z));
}
static float4 Filter(float4 val)
{
return new float4(Filter(val.x), Filter(val.y), Filter(val.z), Filter(val.w));
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment