Skip to content

Instantly share code, notes, and snippets.

@Qix-
Last active July 16, 2023 06:11
Show Gist options
  • Save Qix-/41825b2d3321b9134e8837ff49861787 to your computer and use it in GitHub Desktop.
Save Qix-/41825b2d3321b9134e8837ff49861787 to your computer and use it in GitHub Desktop.
The "F**cking Import my Mesh!!!" (.FIM) format for exporting from Blender to Unity.

.FIM Mesh Format

This is the "F**cking Import my Mesh!!!" (.FIM) format for interop of simple models between Blender and Unity.

Features

  • Dead simple vertex / normal / vertex color mesh exporter from Blender to Unity.
  • File format is ASCII and dead simple to parse (see C# script).
  • Supports multiple objects in the same file - just have those meshes selected when exporting.
  • Automatic modifier apply and triangulate on export.

Using

Install the Python script in Blender via the Addons menu. Then enable the "F**king Import my Mesh (.FIM) Export" addon. Have at least one mesh selected (non-meshes get ignored) and hit the export button. There are no options for export. FIM is pretty cut and dry.

Then, have the attached Unity C# script present somewhere in your project's Assets folder. That's it. Any existing .FIM files that were present prior to creating the script in the project will need to be re-loaded.

FAQ

Just... why?

.OBJ exports from Blender include vertex color information but Unity doesn't parse it.

.PLY isn't supported by Unity neither exporter from Blender properly triangulates meshes.

.FBX is bloated.

.GLTwhatever isn't supported by Unity.

And I was tired of exporting materials and lights and cameras and whatever nonsense when I just wanted a f**cking mesh. I do a lot of procedural and minimalistic art work and don't need all the fancy Pabst Blue-Ribbon rendering.

Why aren't my vertex colors being exported?

You're probably using the default vertex color thing that stores them to face corners.

image

Select the group, then go to the Convert Color Attribute dialog:

image

Then switch "Domain" to "Vertex" and "Datatype" to "Color".

image

Then re-export the FIM file.

My model looks puffy and bloated.

Yeah I'm still trying to figure out how Blender automatically determines hard/smooth edges. For example, this....

image

Turns into this:

image

The solution is to mark all of your edges as hard seams and use the Edge Split modifier with the Angle turned off, OR to simply use the Edge Split modifier with an angle of your choice. If you want all edges to be sharp, set it to 0.

image

If you have any idea how Blender does 'magic' edge splitting, please let me know.

Why can't I have multiple vertex color groups?

Unfortunately, Unity doesn't provide an API to do this. It gives you a single group.

Can I export UVs?

Yeah it's certainly possible. I haven't needed them though. Feel free to comment here and I can probably help you out.

Can I use the FIM format in other programs?

Yes. It's released under CC0, Unlicense, or Public Domain. Pick one that suits you best.

Who made FIM?

Me, Josh Junon.

Why does this format do ________? Wouldn't it be better to do ________?

Yep, probably. This got the job done though.

import bpy
# ExportHelper is a helper class, defines filename and
# invoke() function which calls the file selector.
from bpy_extras.io_utils import ExportHelper
from bpy.props import StringProperty, BoolProperty, EnumProperty
from bpy.types import Operator
bl_info = {
'name': 'F**king Import my Mesh!!! (.FIM) Export',
'author': 'Josh Junon',
'version': (1, 0, 0),
'blender': (3, 4, 0),
'location': 'File > Export > F**king Import my Mesh (.FIM)',
'description': 'Export .FIM file for Unity import',
'tracker_url': 'https://gist.github.com/Qix-/41825b2d3321b9134e8837ff49861787',
"support": "COMMUNITY",
'category': 'Import-Export'
}
AXIS_NAME = {
'Z': '+Z',
'POS_Z': '+Z',
'NEG_Z': '-Z',
'Y': '+Y',
'-Y': 'Y',
'POS_Y': '+Y',
'NEG_Y': '-Y',
'X': '+X',
'-X': 'X',
'POS_X': '+X',
'NEG_X': '-X'
}
class ExportFIM(Operator, ExportHelper):
"""Exports the selected mesh as an FIM (F**king Import my Mesh) format"""
bl_idname = "fim_export.data"
bl_label = "Export FIM"
# ExportHelper mixin class uses this
filename_ext = ".fim"
filter_glob: StringProperty(
default="*.fim",
options={'HIDDEN'},
maxlen=255, # Max internal buffer length, longer would be clamped.
)
def execute(self, context):
original_active = context.view_layer.objects.active
meshes = list(
map(
lambda mesh: (mesh, self.apply_modifiers_and_extract_data(context, mesh)),
filter(
lambda obj: obj.type == "MESH",
context.selected_objects
)
)
)
if len(meshes) == 0:
self.report({"ERROR"}, "No meshes are selected (FIM only supports meshes)")
return {'FINISHED'}
with open(self.filepath, 'w', encoding='utf-8') as fd:
for mesh, data in meshes:
fd.write("MESH\n")
print(f"v{len(data['vertices'])} c{len(data['vertex_colors'])}")
fd.write(f" META\n")
fd.write(f" upaxis {AXIS_NAME[mesh.up_axis]}\n")
fd.write(f" forwardaxis {AXIS_NAME[mesh.track_axis]}\n")
fd.write(f" ATEM\n")
fd.write(" VERT\n")
for v in data['vertices']:
fd.write(f" {v[0]},{v[1]},{v[2]}\n")
fd.write(" TREV\n")
fd.write(" NORM\n")
for n in data['normals']:
fd.write(f" {n[0]},{n[1]},{n[2]}\n")
fd.write(" MRON\n")
fd.write(" FACE")
for i, t in enumerate(data['triangles']):
if (i % 3) == 0:
fd.write("\n ")
else:
fd.write(" ")
fd.write(f"{t}")
fd.write("\n ECAF\n")
if data['vertex_colors'] is not None:
fd.write(" COLR\n")
for color in data['vertex_colors']:
fd.write(f" {color[0]},{color[1]},{color[2]},{color[3]}\n")
fd.write(" RLOC\n")
fd.write("HSEM\n")
context.view_layer.objects.active = original_active
return {'FINISHED'}
def apply_modifiers_and_extract_data(self, context, obj):
# Ensure the object is active and selected
context.view_layer.objects.active = obj
obj.select_set(True)
# Create a copy of the object
obj_copy = obj.copy()
obj_copy.data = obj.data.copy()
context.collection.objects.link(obj_copy)
# Ensure the copy object is active and selected
context.view_layer.objects.active = obj_copy
obj_copy.select_set(True)
# Apply all modifiers to the copy
modifiers = obj_copy.modifiers
for modifier in modifiers:
bpy.ops.object.modifier_apply(modifier=modifier.name)
# Apply Triangulate modifier to the copy
triangulate_modifier = obj_copy.modifiers.new(name="Triangulate", type='TRIANGULATE')
triangulate_modifier.quad_method = 'SHORTEST_DIAGONAL'
triangulate_modifier.min_vertices = 4
bpy.ops.object.modifier_apply(modifier=triangulate_modifier.name)
# Extract vertex data
vertices = [v.co for v in obj_copy.data.vertices]
normals = [v.normal for v in obj_copy.data.vertices]
triangles = [index for poly in obj_copy.data.polygons for index in poly.vertices]
# Extract vertex colors if available
vertex_colors = None
if obj_copy.data.color_attributes and len(obj_copy.data.color_attributes) >= 1:
if len(obj_copy.data.color_attributes) > 1:
self.report({'WARNING'}, f"Mesh '{obj.name}' has multiple vertex color attribute groups; choosing the first")
vertex_colors = [v.color for v in obj_copy.data.color_attributes[0].data]
# After exporting, remove the temporary copy from the scene
bpy.data.objects.remove(obj_copy, do_unlink=True)
# Return the extracted data as properties in a dictionary
return {
'vertices': vertices,
'normals': normals,
'triangles': triangles,
'vertex_colors': vertex_colors,
}
# Only needed if you want to add into a dynamic menu
def menu_func_export(self, context):
self.layout.operator(ExportFIM.bl_idname, text="F**king Import my Mesh (.FIM)")
# Register and add to the "file selector" menu (required to use F3 search "Text Export Operator" for quick access).
def register():
bpy.utils.register_class(ExportFIM)
bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
def unregister():
bpy.utils.unregister_class(ExportFIM)
bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
if __name__ == "__main__":
register()
bpy.ops.fim_export.data('INVOKE_DEFAULT')
using UnityEngine;
using UnityEditor.AssetImporters;
using System;
using System.IO;
using System.Text.RegularExpressions;
using System.Collections.Generic;
[ScriptedImporter(1, "fim")]
public class FIMImporter : ScriptedImporter
{
public float m_Scale = 1;
public override void OnImportAsset(AssetImportContext ctx)
{
var tokens = Regex.Split(File.ReadAllText(ctx.assetPath), @"\s+");
var i = 0;
var depth = 0;
var num_meshes = 0;
for (; i < tokens.Length; i++) {
var t = tokens[i];
switch (t) {
case "":
// Edge case where the end of the file has a newline/space/etc.
break;
case "MESH":
++i;
++depth;
var mesh_id = num_meshes++;
var mesh = new Mesh();
// Meta information (collected first, then applied after)
var up_axis = Vector3.up;
for (; i < tokens.Length; i++) {
var t2 = tokens[i];
switch (t2) {
case "HSEM":
--depth;
goto commit_mesh;
case "META":
++i;
++depth;
for (; i < tokens.Length; i++) {
var t3 = tokens[i];
if (t3 == "ATEM") {
--depth;
break;
}
if (++i >= tokens.Length) {
// Meh, we could probably handle this better but this is simpler.
throw new Exception(string.Format("unexpected EOF"));
}
var k = t3;
var v = tokens[i];
switch (k) {
case "upaxis":
switch (v) {
case "+Z":
up_axis = new Vector3(0, 0, 1);
break;
case "-Z":
up_axis = new Vector3(0, 0, -1);
break;
case "+Y":
up_axis = new Vector3(0, 1, 0);
break;
case "-Y":
up_axis = new Vector3(0, -1, 0);
break;
case "+X":
up_axis = new Vector3(1, 0, 0);
break;
case "-X":
up_axis = new Vector3(-1, 0, 0);
break;
default:
Debug.LogWarningFormat("FIM meta upaxis has unknown axis: {0} (mesh offset {1}; skipping upaxis)", v, mesh_id);
break;
}
break;
case "forwardaxis":
// Unused property
break;
default:
++i;
Debug.LogWarningFormat("FIM meta unknown property name: {0} (mesh offset {1}; skipping meta)", k, mesh_id);
break;
}
}
break;
case "VERT":
++i;
++depth;
var vertices = new List<Vector3>();
for (; i < tokens.Length; i++) {
var t3 = tokens[i];
if (t3 == "TREV") {
--depth;
break;
}
string[] vals = t3.Split(',');
if (vals.Length != 3) {
Debug.LogWarningFormat("FIM mesh vertex is malformed: {0} (in mesh offset {1}; skipping vertex)", t3, mesh_id);
continue;
}
var vert = new Vector3(float.Parse(vals[0]), float.Parse(vals[1]), float.Parse(vals[2]));
vertices.Add(vert);
}
mesh.vertices = vertices.ToArray();
break;
case "NORM":
++i;
++depth;
var normals = new List<Vector3>();
for (; i < tokens.Length; i++) {
var t3 = tokens[i];
if (t3 == "MRON") {
--depth;
break;
}
string[] vals = t3.Split(',');
if (vals.Length != 3) {
Debug.LogWarningFormat("FIM mesh normal is malformed: {0} (in mesh offset {1}; skipping normal)", t3, mesh_id);
continue;
}
var norm = new Vector3(float.Parse(vals[0]), float.Parse(vals[1]), float.Parse(vals[2]));
normals.Add(norm);
}
mesh.normals = normals.ToArray();
break;
case "FACE":
++i;
++depth;
var tri_list = new List<int>();
for (; i < tokens.Length; i++) {
var t3 = tokens[i];
if (t3 == "ECAF") {
--depth;
break;
}
tri_list.Add(int.Parse(t3));
}
mesh.triangles = tri_list.ToArray();
break;
case "COLR":
++i;
++depth;
var colors = new List<Color>();
for (; i < tokens.Length; i++) {
var t3 = tokens[i];
if (t3 == "RLOC") {
--depth;
break;
}
string[] vals = t3.Split(',');
if (vals.Length < 3 || vals.Length > 4) {
Debug.LogWarningFormat("FIM mesh color is malformed: {0} (in mesh offset {1}; skipping color)", t3, mesh_id);
continue;
}
var color = new Color(float.Parse(vals[0]), float.Parse(vals[1]), float.Parse(vals[2]), vals.Length == 4 ? float.Parse(vals[3]) : 1.0f);
colors.Add(color);
}
mesh.colors = colors.ToArray();
break;
default:
Debug.LogWarningFormat("FIM unknown MESH block token: {0} (in mesh offset {1}; skipping block)", t2, mesh_id);
{
var end = ReverseString(t2);
++depth;
for (; i < tokens.Length; i++) {
if (tokens[i] == end) {
--depth;
break;
}
}
}
break;
}
}
break;
commit_mesh:
ReorientMesh(mesh, up_axis);
ctx.AddObjectToAsset(mesh_id.ToString(), mesh);
break;
default:
Debug.LogWarningFormat("FIM unknown top-level token: {0} (skipping block)", t);
{
var end = ReverseString(t);
++depth;
for (; i < tokens.Length; i++) {
if (tokens[i] == end) {
--depth;
break;
}
}
}
break;
}
}
if (depth > 0) {
throw new System.Exception("FIM parser hit EOF unexpectedly");
}
}
static string ReverseString(string input)
{
char[] charArray = input.ToCharArray();
Array.Reverse(charArray);
return new string(charArray);
}
static void ReorientMesh(Mesh mesh, Vector3 up_axis) {
if (up_axis == Vector3.up) return;
// Calculate the rotation quaternion to align the mesh with the custom axes
Quaternion rotation = Quaternion.FromToRotation(up_axis, Vector3.up);
// Get the vertices and normals from the mesh
Vector3[] vertices = mesh.vertices;
Vector3[] normals = mesh.normals;
// Apply the rotation to the vertices and normals
for (int i = 0; i < vertices.Length; i++)
{
vertices[i] = rotation * vertices[i];
normals[i] = rotation * normals[i];
}
// Update the modified vertices and normals back to the mesh
mesh.vertices = vertices;
mesh.normals = normals;
// Recalculate the bounds and tangents of the mesh
mesh.RecalculateBounds();
mesh.RecalculateTangents();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment