Created
July 8, 2018 22:26
-
-
Save Frooxius/7d251f66d331ed92a43b052e00876721 to your computer and use it in GitHub Desktop.
Model Exporter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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