Skip to content

Instantly share code, notes, and snippets.

@Qix-
Last active July 16, 2023 06:11
Show Gist options
  • Select an option

  • Save Qix-/41825b2d3321b9134e8837ff49861787 to your computer and use it in GitHub Desktop.

Select an option

Save Qix-/41825b2d3321b9134e8837ff49861787 to your computer and use it in GitHub Desktop.

Revisions

  1. Qix- revised this gist Jul 16, 2023. 1 changed file with 17 additions and 0 deletions.
    17 changes: 17 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -7,6 +7,7 @@ This is the "F**cking Import my Mesh!!!" (.FIM) format for interop of **simple**
    - 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

    @@ -48,6 +49,22 @@ Then switch "Domain" to "Vertex" and "Datatype" to "Color".

    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](https://user-images.githubusercontent.com/885648/253787804-344b27a9-affc-46fd-a241-27c65274a242.png)

    Turns into this:

    ![image](https://user-images.githubusercontent.com/885648/253787820-c6a30e50-a165-4a89-a12f-5263c8e20635.png)

    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](https://user-images.githubusercontent.com/885648/253787884-e07e0894-d9ba-4736-a799-3d7c9a003b36.png)

    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.
  2. Qix- revised this gist Jul 16, 2023. 2 changed files with 34 additions and 20 deletions.
    2 changes: 1 addition & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -10,7 +10,7 @@ This is the "F**cking Import my Mesh!!!" (.FIM) format for interop of **simple**

    ## Using

    Run the attached Python script in Blender. I'm honestly not sure how to auto-load it but it will register and show up as an Export type.
    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.

    52 changes: 33 additions & 19 deletions fim_export.py
    Original file line number Diff line number Diff line change
    @@ -6,6 +6,18 @@
    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',
    @@ -34,9 +46,10 @@ class ExportFIM(Operator, ExportHelper):
    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)),
    @@ -46,32 +59,32 @@ def execute(self, context):
    )
    )
    )

    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:
    @@ -80,21 +93,21 @@ def execute(self, context):
    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
    @@ -116,13 +129,13 @@ def apply_modifiers_and_extract_data(self, context, obj):

    # Apply Triangulate modifier to the copy
    triangulate_modifier = obj_copy.modifiers.new(name="Triangulate", type='TRIANGULATE')
    triangulate_modifier.quad_method = 'BEAUTY'
    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 = [[n for n in obj_copy.data.vertices[i].normal] for i in range(len(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
    @@ -158,7 +171,8 @@ def register():
    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')
    bpy.ops.fim_export.data('INVOKE_DEFAULT')
  3. Qix- revised this gist Jul 16, 2023. 1 changed file with 5 additions and 1 deletion.
    6 changes: 5 additions & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -62,4 +62,8 @@ Yes. It's released under CC0, Unlicense, or Public Domain. Pick one that suits y

    #### Who made FIM?

    Me, [Josh Junon](https://github.com/qix-).
    Me, [Josh Junon](https://github.com/qix-).

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

    Yep, probably. This got the job done though.
  4. Qix- revised this gist Jul 16, 2023. 1 changed file with 13 additions and 0 deletions.
    13 changes: 13 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -19,6 +19,19 @@ were present prior to creating the script in the project will need to be re-load

    ## 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.
  5. Qix- created this gist Jul 16, 2023.
    263 changes: 263 additions & 0 deletions FIMImporter.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,263 @@
    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();
    }
    }
    52 changes: 52 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,52 @@
    # .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.

    ## Using

    Run the attached Python script in Blender. I'm honestly not sure how to auto-load it but it will register and show up as an Export type.
    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

    #### Why aren't my vertex colors being exported?

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

    ![image](https://user-images.githubusercontent.com/885648/253786322-96107f1f-e47f-4dbf-a7b7-81aeddd00103.png)

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

    ![image](https://user-images.githubusercontent.com/885648/253786356-699a57cf-2db1-469e-b13a-2a62df6c589c.png)

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

    ![image](https://user-images.githubusercontent.com/885648/253786386-cf35fb60-834f-4f55-beb7-c19271626b87.png)

    Then re-export the FIM file.

    #### 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](https://github.com/qix-).
    164 changes: 164 additions & 0 deletions fim_export.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,164 @@
    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

    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 = 'BEAUTY'
    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 = [[n for n in obj_copy.data.vertices[i].normal] for i in range(len(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')