Christopher Jones, Games Development
  • Portfolio
    • Unity
    • 2D Games
    • Old Work
    • Source Engine
  • About Me / Contact
  • Blog

Unity, Blender and Instancing Models

5/7/2016

16 Comments

 
As many of you may know, Blender files can be imported directly by Unity as models (teeechnically it's just telling the blender file to export itself as FBX, then importing that, but it's defacto Blender -> Unity for the end user). Honestly, I find it impossible not to want to use Blender as my level / modelling tool rather than trying to build levels out of prefabs in Unity. But with the default importer, there's a catch; instancing.
Instancing is simply the reuse of a model in multiple places. If you have a room full of pillars, and all those pillars are identical, it makes sense to just have one pillar model that you can change and edit with the other copies catching up. Equally, it makes sense to only store that one pillar model in memory, rather than paying the memory cost in full for every single copy of the pillar. Both Blender and Unity support doing this, but it all falls down on the importation; a set of instances in Blender becoming a set of "unique" meshes in Unity.

Thankfully, there is a way to fiddle imported assets automatically as they come in; the AssetPostprocessor. This can actually be levied to serve a wide variety of purposes and automation, where model imports are involved, but for now I'll be happy with simple instancing support. So here it is:
using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Collections.Generic;

public class ModelPostprocess : AssetPostprocessor {

    Dictionary<string, List<Mesh>> meshLookup;

    void OnPostprocessModel (GameObject root)
    {
        if (meshLookup == null) meshLookup = new Dictionary<string, List<Mesh>>();
        else meshLookup.Clear();

        Apply(root.transform);
    }

    //removes the _001 etc after instance names
    string CleanMeshName(string name)
    {
        int end = name.LastIndexOf('_');
        if (end == -1) return name;

        return name.Remove(end, name.Length - end);
    }

    void Apply(Transform transform)
    {
        MeshFilter mF = transform.GetComponent<MeshFilter>();
        if (mF)
        {
            Mesh mesh = mF.sharedMesh;
            string name = CleanMeshName(mesh.name);

            List<Mesh> relatives;
            if(!meshLookup.TryGetValue(name, out relatives)) {
                mesh.name = name;
                relatives = new List<Mesh>();
                relatives.Add(mesh);
                meshLookup.Add(name, relatives);
            }
            else
            {
                //Debug.LogFormat("mesh {0} has {1} relatives", name, relatives.Count);
                for(int i = 0; i < relatives.Count; i++)
                {
                    Mesh relMesh = relatives[i];
                    if (CompareForMeshEquality(relMesh, mesh))
                    {
                        //Debug.LogFormat("mesh {0} is instance of mesh {1}; replacing", mesh.name, relMesh.name);
                        mF.sharedMesh = relMesh;

                        MeshCollider mC = transform.GetComponent<MeshCollider>();
                        if (mC)
                        {
                            mC.sharedMesh = relMesh;
                        }

                        GameObject.DestroyImmediate(mesh); //ensures the duplicate is completely removed
                    }
                }
            }
        }

        for (int i = 0; i < transform.childCount; i++) Apply(transform.GetChild(i));
    }

    const float maxBoundsError = 0.001f; //trying to evade floating point shenanigans
    bool CompareForMeshEquality(Mesh a, Mesh b)
    {
        //check if we're actually comparing a mesh object against itself.
        //since we delete 'b' if equal, we actually want to return false here or we'll delete 'a' as well
        if (a == b) return false;

        //check submeshes and vertex counts; this should early-out most non-duplicates
        if (a.subMeshCount != b.subMeshCount || a.vertexCount != b.vertexCount) return false;

        //check bounds centers
        if (Vector3.SqrMagnitude(a.bounds.center - b.bounds.center) > maxBoundsError) return false;
        //check bounds sizes
        if (Vector3.SqrMagnitude(a.bounds.size - b.bounds.size) > maxBoundsError) return false;

        //if not exited as false by now, they're very probably identical
        return true;
    }
}

It's only a simple postprocessor intended for that one thing; catching instance duplicates and clearing them out. The checks it makes for duplicity are only rudimentary though; if two meshes have the same name once instance numbers are stripped (ie 'Cube', 'Cube_001' and 'Cube_UnderscoresAreMyLife' are all considered the "same" name; everything after the last '_' is ignored) they will be compared on their vertex count, number of submeshes (material IDs) and their AABB bounds; if they are equal they are declared 'identical' and the latter is replaced by the former for both MeshFilters and MeshColliders. The script will do nothing at all with SkinnedMeshRenderers.

It does support having multiple meshes that share the same name but are not considered identical, so if you create some objects in Blender whilst forgetting to change them from their default names it probably won't eat them all outside of some very odd coincidences.
Picture
modelinstancepostprocessor.zip
File Size: 1 kb
File Type: zip
Download File

16 Comments
Dean
6/7/2016 07:55:31 am

Hey Chris,

Its Dean from Uni! Great to see you doing well, I am currently working in Unity with a 2D game up for release any time now! Cheers for all the help you gave me at Uni, catch you around!

Dean

Reply
Chris
6/8/2016 04:25:11 am

Hey there! Glad to hear it's working out and yes, Unity is a fun engine ;)

Drop me a line when you make the release! Have you heard from any of the others?

Reply
Dean
7/28/2016 06:10:20 am

Sorry Chris only just got this, I will let you know as soon as its out. Its a free game but we make money through adverts and in game purchases etc

Only heard from Tom lately, planning to meet up with him soon!
Glad to see your doing well, anyway thanks for all the encourgment and tips at uni! :)

Matthias
2/10/2018 12:51:01 am

Hi there!
I just wanted to thank you for that script. Really helpful.

One small addition: I changed CleanMeshName to use a regex to identify the _### suffix. That's a bit more robust than just searching for any occurrence of '_' in the mesh name and should result in less unnecessary mesh comparisons as a result.

For reference: The regex I use is "_\d{3}$". It matches "_###" at the end of a string.

Reply
Chris
2/10/2018 04:14:56 am

Hey, thanks! And yeah, I've actually re-written this since and run into issues with just taking the last '_'.

I didn't use regex though, I just checked if a) there were characters after the last '_', b) the next character after it was a number and c) the last character in the string was a number.

Was trying to avoid expensive string operations for performance's sake.

You can expand it to use other suffixes too, f.ex right now I have it so that a mesh named [blahblah]_convex will turn itself into a convex meshcollider on its parent.

Reply
Matthias
2/11/2018 12:35:50 am

Isn't this post processor run only once when an asset gets imported into Unity? If that's the case, I wouldn't worry all that much about the performance of a regex matches. Still, I see your point.

Neat idea to use other suffixes for other purposes.

Is there a way to check what kind of asset we're running on in the post-processor? It would be great if we could opt out of handling anything that's not a Blender file.

Chris
2/11/2018 03:09:27 am

It is, but well, I made it for cleaning up instances so I could do level design in Blender; it was written on the assumption it would be working with large files that I'd want to be able to iterate on quickly.

I don't believe there's a way to check what we're running it on, unfortunately; the asset post-processor functions we can hook into give us immediate post-import gameobjects and not the source files.

Ironically enough what I'd really love for is some sort of 'ignore this blender file' functionality so I can work with high-res meshes for baking and sculpting without giving the asset importer a million conniptions.

Matthias
2/11/2018 10:04:03 pm

I don't think there is a way to ignore specific files in the assets folder. I just checked the Unity documentation and found an "assetPath" property in the AssetPostProcessor base class. I've not looked into it yet, but it could be that that contains the path of the file being imported currently.

However, I don't see a way to cancel an import from the OnPreProcess... methods of the AssetPostProcessor class. Destroying the AssetImporter instance seems very impolite and I doubt Unity would like that.

jeff
7/13/2020 08:22:22 am

Is there any tutorials on this!? I know this was posted a long time ago, but just wondering.

Reply
Eating by Eliza link
11/20/2020 11:12:14 pm

Grateful for sharinng this

Reply
MckinneyVia link
11/4/2021 11:13:22 pm

Very much appreciated. Thank you for this excellent article. Keep posting!

Reply
Mister Freeze
12/30/2021 02:09:53 am

Hey there! Thanks for sharing this trick, so essential for people who prefer to build level in Blender. However I can't get it to work in Unity 2020, I put the script under a folder called "Editor" in the project browser, but nothing happen when importing an object. Am I doing something wrong ?

Reply
Brett White link
3/21/2022 08:51:00 pm

What an exquisite article! Your post is very helpful right now. Thank you for sharing this informative one.

Reply
Sunnyvale Cleaners link
6/30/2022 11:48:37 pm

Hello mate nicee post

Reply
David Miller link
10/6/2022 03:40:24 am

Audience affect idea none bed medical although no. Number now section unit detail. Heart even parent student along mouth far.

Reply
Michael Castro link
11/2/2022 03:38:43 pm

Seek share run section community decide central. Rest table option.
Traditional hit capital what above speech glass. Down myself trip within.

Reply



Leave a Reply.

    Author

    A UK-based amateur game developer.

    Archives

    May 2017
    May 2016
    April 2016
    October 2014
    December 2013
    November 2013

    Categories

    All
    Scripting And Theory
    Shaders Are Fun
    Shaders Are Hilarious
    Unity 4

    RSS Feed

Powered by Create your own unique website with customizable templates.