Automatic Thumbnail Image Generator for 3D Objects in Unity

Video games these days usually contain some form of inventory, or list of objects. Just like on Amazon, the end-user can have a better experience perusing through pictures of items rather than a list of text items. Here’s an example from The Sims 4.

Chairs in Buy Mode Inventory in The Sims 4

Generating this group of small images, or thumbnails, can be time-consuming. While you’ll most likely have better looking results if you stage and render each thumbnail yourself, if you have hundreds or thousands of items, this can be prohibitive if not impossible. This especially becomes true when you consider that in game development, every part of the game is repeatedly re-worked. This constant iteration to get things just right means that any changes you make to an object, need to also be propagated to any other processing it needs, such as thumbnail renders, adding hours or even days of extra processing time, and you can basically torpedo your launch date, because your game would pretty much be sunk.

The solution to saving you massive amounts of time from manually creating object thumbnail renders is obviously to have this done automatically. Before we begin, if you just want to see the source itself, just go straight to github here. Otherwise….

Let’s dive in.

The Concept

Suppose you have a group of 3D objects in your Unity project, say in the FBX format.

FBX objects in Unity Project: cube, sphere, pyramid, cylinder

To create a thumbnail image of the object, a camera in the scene renders its view to a texture, called a RenderTexture. The graphics technique itself is called “Render-to-Texture”, or RTT.

A camera, an object, and a render texture.

Once you have a Texture object that contains the view of the camera, that Texture can be written out to an image file; in this case, we’ll write out a PNG file.

WriteAllBytes() to thumbnail.PNG

Then, later on, we’ll cover automating the process of repeating this concept for a list of objects.

Run-Time Generated Thumbnails for Custom Objects

I should mention a quick caveat about the solution below. The design of this solution involves running a Unity scene to generate thumbnails. This technique allows thumbnails to be generated based on any customizations the player may have introduced, such as custom colors or textures, prior to jumping into the game world. If you need to generate thumbnails during design-time (i.e. outside of Play mode of a Unity scene), you may consider automatically generating thumbnails from your 3D Modeling Tool instead.

Manual Thumbnail Generation

Setting this up manually in Unity is pretty straightforward. You can skip this section if you already know how to render a camera view to a texture and write it out to a PNG file. For everyone else, we’ll go step-by-step below.

Unity Setup

  1. Create a new Scene.
  2. Create an Empty Object (this will hold the script), rename it “ThumbGen”.
  3. Create a Camera and parent it to “ThumbGen”.
  4. Create a RenderTexture, assign it to the camera as TargetTexture.
  5. Create an object (say, a Cube) in the scene, in view of the camera. Click on the camera to check that it’s actually looking at the object, and continue positioning the camera and the object until it’s in view.

That’s it as far as the scene setup goes. Now, let’s write the script.

ThumbnailGenerator Script

Create a new script called ThumbnailGenerator.cs and attach it to the “ThumbGen” object. Implement it as follows:

using System.Collections;
using UnityEngine;

namespace ThumbGenExamples
{
    public class ThumbnailGenerator1 : MonoBehaviour
    {
        public RenderTexture TargetRenderTexture;

        public Camera ThumbnailCamera;

        public Transform ObjectPosition;

        public int ThumbnailWidth;

        public int ThumbnailHeight;

        public Texture2D TextureResult { get; private set; }

        /// <summary>
        /// If this is not null or empty, the texture is exported as a png to the file system.
        /// </summary>
        public string ExportFilePath;

        void Start()
        {
            ThumbnailWidth = 256;
            ThumbnailHeight = 256;
            
            Render("render_manual");
        }

        private void AssignRenderTextureToCamera()
        {
            if (ThumbnailCamera != null && TargetRenderTexture != null)
            {
                ThumbnailCamera.targetTexture = TargetRenderTexture;
            }
            else if (ThumbnailCamera.targetTexture != null)
            {
                TargetRenderTexture = ThumbnailCamera.targetTexture;
            }
        }

        private void Render(string filename)
        {
            StartCoroutine(DoRender(filename));
        }

        IEnumerator DoRender(string filename)
        {
            yield return new WaitForEndOfFrame();

            ExecuteRender(filename);
        }

        private void ExecuteRender(string filename)
        {
            if (ThumbnailCamera == null)
            {
                throw new System.InvalidOperationException("ThumbnailCamera not found. Please assign one to the ThumbnailGenerator.");
            }

            if (TargetRenderTexture == null && ThumbnailCamera.targetTexture == null)
            {
                throw new System.InvalidOperationException("RenderTexture not found. Please assign one to the ThumbnailGenerator.");
            }

            AssignRenderTextureToCamera();

            Texture2D tex = null;

            {   // Create the texture from the RenderTexture

                RenderTexture.active = TargetRenderTexture;

                tex = new Texture2D(ThumbnailWidth, ThumbnailHeight);

                tex.ReadPixels(new Rect(0, 0, ThumbnailWidth, ThumbnailHeight), 0, 0);
                tex.Apply();

                TextureResult = tex;
            }

            // Export to the file system, if ExportFilePath is specified.
            if (tex != null && !string.IsNullOrWhiteSpace(ExportFilePath) && !string.IsNullOrWhiteSpace(filename))
            {
                string dir = System.IO.Path.GetDirectoryName(ExportFilePath);
                if (!System.IO.Directory.Exists(dir))
                {
                    System.IO.Directory.CreateDirectory(dir);
                }

                foreach (char c in System.IO.Path.GetInvalidFileNameChars())
                {
                    filename = filename.Replace(c, '_');
                }
                
                string finalPath = string.Format("{0}/{1}.png", ExportFilePath, filename);

                byte[] bytes = tex.EncodeToPNG();
                System.IO.File.WriteAllBytes(ExportFilePath + "/" + filename + ".png", bytes);
            }
        }
    }
}

Finally, assign the Camera you created to the ThumbnailCamera property in the Inspector for the ThumbnailGenerator1 script, and specify the ExportFilePath as “./test”, without the quotes. This will output the rendered image to the test directory.

Run the scene. A thumbnail image is written out to the file system.

Thumbnail image written to the file system.

Automatic Thumbnail Generation

Now that we have a script to render out a thumbnail, we still need to automate object creation and placement into the scene. Basically, Step (5) above can be automated so that an object can be created, snapshotted, and then destroyed.

Object load, render, and destroy process.

Modify the ThumbnailGenerator script

Update the ThumbnailGenerator script.

  1. Make Render() public
  2. Remove the call to Render() in the Start() function.

With this, we still have a ThumbnailGenerator, but Render() must be called from somewhere else to actually render the view. The script is ThumbnailGenerator2.cs in the git example.

Add an EmptyObject called “Stage”, parent it to the “ThumbGen”. When the object is instantiated, it will be placed at the position of the Stage object. It serves as the camera’s “look at” point. Since we’ll be automatically creating the object, we don’t need the cube from before.

Delete the Cube.

(Also note that from here on out, you’ll need the FBX objects from the Resources folder contained in github.)

Hierarchy and components assigned to ThumbGen.

Create an Object Loader

The first thing we’ll need for auto-generating an object’s thumbnail is an object loader. In this example, I’m simply calling Resources.Load() on one of the objects in our list, using the path to the model resource in the /Resources folder as an identifier. You may consider any other sort of identifier (i.e. unique name or inventoryId, etc.) to load your models, but that’s beyond the scope of this article.

Also keep in mind that Resources.Load() loads the prefab of a GameObject from the asset database. It doesn’t create an instance to be used in the game. Technically, we *can* use the loaded GameObject prefab, but it’s typically dangerous to do this because you can potentially modify all subsequent instances of the GameObject. It’s safer to call GameObject.Instantiate() on the loaded GameObject instead.

We’ll use the following class below as our ObjectLoader.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace ThumbGenExamples
{
    public class ObjectThumbnailActivity
    {
        private GameObject _object;
        private ThumbnailGenerator2 _thumbGen;

        private string _resourceName;

        public ObjectThumbnailActivity(ThumbnailGenerator2 thumbGen, string resourceName)
        {
            _thumbGen = thumbGen;
            _resourceName = resourceName;
        }
        
        public bool Setup()
        {
            if (string.IsNullOrWhiteSpace(_resourceName))
                return false;

            GameObject prefab = Resources.Load<GameObject>(_resourceName);

            GameObject stage = GameObject.Find("Stage");

            _object = GameObject.Instantiate(prefab, stage?.transform);

            Camera cam = _thumbGen.ThumbnailCamera.GetComponent<Camera>();

            cam.transform.LookAt(stage.transform);
            
            return true;
        }

        public bool CanProcess()
        {
            return true;
        }

        public void Process()
        {
            if (_thumbGen == null)
                return;

            string filename = string.Format("render_objectloader_{0}", _resourceName);
            _thumbGen.Render(filename);
        }

        public void Cleanup()
        {
            if (_object != null)
            {
                GameObject.Destroy(_object);

                _object = null;
            }
        }
    }
}

Most of the implementation for object loading will occur in Setup() and Cleanup(), but let’s start with a constructor.

private ThumbnailGenerator2 _thumbGen;
private string _resourceName;

public ObjecctThumbnailActivity(ThumbnailGenerator2 thumbGen, string resourceName)
{
     _thumbGen = thumbGen;
     _resourceName = resourceName;
}

We’re simply saving the resourcePath and thumbGen to private members of the ObjectThumbnailActivity, so that the Setup() method can use them. You may be asking yourself, why don’t we just pass in the resourcePath to Setup()? We’ll cover that in the Optional Improvements section below.

Now we can implement Setup() and Cleanup().

Setup() instantiates a GameObject based on the specified resourcePath, and then positions the object at the “Stage” position.

    public bool Setup()
    {
        if (string.IsNullOrWhiteSpace(_resourceName))
            return false;

        GameObject prefab = Resources.Load<GameObject>(_resourceName);

        GameObject stage = GameObject.Find("Stage");

        _object = GameObject.Instantiate(prefab, stage?.transform);

        Camera cam = _thumbGen.ThumbnailCamera.GetComponent<Camera>();

        cam.transform.LookAt(stage.transform);
            
        return true;
    }

Cleanup() simply destroys the instantiated GameObject.

        public void Cleanup()
        {
            if (_object != null)
            {
                GameObject.Destroy(_object);

                _object = null;
            }
        }

Now we can implement CanProcess() and Process() as follows:

        public bool CanProcess()
        {
            return true;
        }

For now, CanProcess() will simply return true.

        public void Process()
        {
            if (_thumbGen == null)
                return;

            string filename = string.Format("render_objectloader_{0}", _resourceName);
            _thumbGen.Render(filename);
        }

Now that we have our ObjectThumbnailActivity, the MonoBehaviour class below uses the activity to execute all its methods.

ObjectProcessor.cs script

    public class ObjectProcessor : MonoBehaviour
    {
        private ObjectThumbnailActivity _activity;

        void Start()
        {

        }

        void Awake()
        {
            ThumbnailGenerator2 thumbGen = GetComponent<ThumbnailGenerator2>();


            _activity = new ObjectThumbnailActivity(thumbGen, "objects/Cube");
            StartCoroutine(DoProcess());
        }

        IEnumerator DoProcess()
        {
            yield return new WaitForEndOfFrame();

            _activity.Setup();

            if(!_activity.CanProcess())
    
            yield return null;

            _activity.Process();

            StartCoroutine(DoCleanup());
        }

        IEnumerator DoCleanup()
        {
            yield return new WaitForEndOfFrame();

            _activity.Cleanup();
        }
    }

Things to notice here, we use coroutines for calling DoProcess() and DoCleanup(). For DoProcess(), we allow the Setup() method to do a lot of the work, so yielding for WaitForEndOfFrame() allows Setup() to finish doing any initialization needed for the GameObject instantiation. The coroutine that calls DoCleanup() also serves the same purpose. The coroutine allows the Process() method to fully run before cleaning up the GameObject.

Attach this MonoBehaviour to the root “ThumbGen” object, run it, and the object’s thumbnail will be generated.

You may need to place the “Stage” object in a position that would result in a more pleasing thumbnail, but that’s on you.

When you ran the scene, you may have caught a glimpse of the instantiated object before it gets destroyed by the activity.

After running the scene and getting a thumbnail generated, you’re probably saying, “But that’s exactly what the manual scene setup does, and even with far less code”. While this is true, the ObjectThumbnailActivity sets us up to repeatedly load objects without us having to delete the current object and place a new one. We’re getting to that next.

Multiple Object Loading

Now that we have a way to load/unload our objects, we still need a way to process all of them automatically. There are a few ways we can do this, and we’ll go over them below.

  • First, is simply loading the objects in a loop. This is simple to implement, but it can pause your game for a significant amount of time while it loads all the objects in the list. This is obviously dependent on the number of items we plan to take thumbnail images of.
  • Second, is multi-threading. This is the ideal solution for this situation, as you can have thumbnails load in the background while your game is loading, and the game would show no signs of slowing down. It’s possible to do this in Unity, but this requires using the newer Jobs system, and the solution is far more complicated. This article does not cover that.
  • Third, is using Unity’s coroutine system. This solution is kind of an “in between” the two solutions above. Thumbnail generation would occur on the same thread as our game, but it splits up the workload at a specified time interval such that it seems like the game is playable while there is some work being done intermittently. This is the solution that we’ll go with in this article, as it strikes a good balance between implementation complexity and end-user experience.

Per-Frame Processor

Lucky for us, we already have an ObjectProcessor using coroutines, from the section above.

This means we can use that class as the basis for our new class. We just need to add a way to repeatedly process a list of objects.

Here’s the implementation of the per-frame processor, called the MultiObjectProcessor:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace ThumbGenExamples
{
    public class MultiObjectProcessor : MonoBehaviour
    {
        private List<ObjectThumbnailActivity> _activities;

        private ObjectThumbnailActivity _curActivity;

        void Start()
        {
        }

        void Awake()
        {
            _curActivity = null;
            _activities = new List<ObjectThumbnailActivity>();

            ThumbnailGenerator2 thumbGen = GetComponent<ThumbnailGenerator2>();

            string[] objectResourceNames =
            {
                "objects/Cube",
                "objects/Cylinder",
                "objects/Capsule",
                "objects/Sphere"
            };

            foreach (var name in objectResourceNames)
            {
                var thumbActivity = new ObjectThumbnailActivity(thumbGen, name);
                _activities.Add(thumbActivity);
            }
        }

        void Update()
        {
            if (_curActivity == null)
            {
                if (_activities.Count > 0)
                {
                    _curActivity = _activities[0];
                    
                    _curActivity.Setup();

                    StartCoroutine("DoProcess");

                    _activities.RemoveAt(0);
                }
            }
        }

        IEnumerator DoProcess()
        {
            yield return new WaitForEndOfFrame();

            if (_curActivity == null)
                yield return null;

            if(!_curActivity.CanProcess())
                yield return null;

            _curActivity.Process();

            StartCoroutine(DoCleanup());
        }

        IEnumerator DoCleanup()
        {
            yield return new WaitForEndOfFrame();

            if (_curActivity != null)
            {
                _curActivity.Cleanup();
                _curActivity = null;
            }
        }
    }
}

First thing to note, is a quick reminder that this is a MonoBehaviour. This is needed so that we can leverage the Coroutine system.

Awake() sets up the list of thumbnail activities to process, and initializes the current activity to null.

The Update() function then checks if the current activity is null. If it is, that means the processor is not doing anything, so the function proceeds to initialize the next activity. We check if the activities list is not empty, set the first activity as the current activity, remove it from the list, and call StartCoroutine() to execute the activity’s Setup() and Process() functions. Once the Process() function has completed, the current activity is set to null, so that the Update() function can pick up the next activity in the list.

On the “ThumbGen” object in the Unity Inspector, remove the ObjectProcessor component, and add the MultiObjectProcessor component.

Run the scene. This time, you may notice that the multiple objects in our list appear and disappear in a split second, similar to our original ObjectProcessor.

Check the output directory, and notice the four thumbnails that have been created.

Four thumbnail output images.

That’s it! That’s pretty much the basics of the thumbnail object processing functionality. Below is a discussion of the design, as well as a few more improvements that you may want to make to the code.

Design

All the work above may seem like some over-engineering. To an extent, it is. It really depends on what level of flexibility you need for your own project. If you need to simply set up an object, snapshot it, and clean it up, why not just run through a for loop or something? Well, the way it’s designed above enforces a lot of principles known as SOLID in computer science.

Okay, here is what the code would look like if it was all thrown into a single class or method:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace ThumbGenExamples
{
    public class ThumbGenProcessor : MonoBehaviour
    {
        private List<string> _objectResourceNames;

        private GameObject _curObject;

        public RenderTexture TargetRenderTexture;

        public Camera ThumbnailCamera;

        public int ThumbnailWidth;

        public int ThumbnailHeight;

        public Texture2D TextureResult { get; private set; }

        /// <summary>
        /// If this is not null or empty, the texture is exported as a png to the file system.
        /// </summary>
        public string ExportFilePath;

        public ThumbGenProcessor()
        {
            ThumbnailWidth = 256;
            ThumbnailHeight = 256;
        }

        void Start()
        {
        }

        void Awake()
        {
            _curObject = null;
            
            _objectResourceNames = new List<string>()
            {
                "objects/Cube",
                "objects/Cylinder",
                "objects/Capsule",
                "objects/Sphere"
            };
        }

        void Update()
        {
            if (_curObject == null)
            {
                if (_objectResourceNames.Count > 0)
                {
                    string resourceName = _objectResourceNames[0];
                    if (!string.IsNullOrWhiteSpace(resourceName))
                    {
                        GameObject prefab = Resources.Load<GameObject>(resourceName);

                        GameObject stage = GameObject.Find("Stage");

                        _curObject = GameObject.Instantiate(prefab, stage?.transform);

                        Camera cam = ThumbnailCamera.GetComponent<Camera>();

                        cam.transform.LookAt(stage.transform);

                        StartCoroutine(DoProcess(resourceName));
                    }
                    _objectResourceNames.RemoveAt(0);
                }
            }
        }

        IEnumerator DoProcess(string resourceName)
        {
            yield return new WaitForEndOfFrame();

            if (_curObject == null)
                yield return null;

            if (ThumbnailCamera == null)
                yield return null;

            string filename = string.Format("render_objectloader_{0}", resourceName);
            Render(filename);

            StartCoroutine(DoCleanup());
        }

        IEnumerator DoCleanup()
        {
            yield return new WaitForEndOfFrame();

            if (_curObject != null)
            {
                GameObject.Destroy(_curObject);

                _curObject = null;
            }
        }

        private void AssignRenderTextureToCamera()
        {
            if (ThumbnailCamera != null)
            {
                ThumbnailCamera.targetTexture = TargetRenderTexture;
            }
        }

        public void Render(string filename)
        {
            StartCoroutine(DoRender(filename));
        }

        IEnumerator DoRender(string filename)
        {
            yield return new WaitForEndOfFrame();

            ExecuteRender(filename);
        }

        private void ExecuteRender(string filename)
        {
            if (ThumbnailCamera == null)
            {
                throw new System.InvalidOperationException("ThumbnailCamera not found. Please assign one to the ThumbnailGenerator.");
            }

            if (TargetRenderTexture == null)
            {
                throw new System.InvalidOperationException("RenderTexture not found. Please assign one to the ThumbnailGenerator.");
            }

            AssignRenderTextureToCamera();

            Texture2D tex = null;

            {   // Create the texture from the RenderTexture

                RenderTexture.active = TargetRenderTexture;

                tex = new Texture2D(ThumbnailWidth, ThumbnailHeight);

                tex.ReadPixels(new Rect(0, 0, ThumbnailWidth, ThumbnailHeight), 0, 0);
                tex.Apply();

                TextureResult = tex;
            }

            // Export to the file system, if ExportFilePath is specified.
            if (tex != null && !string.IsNullOrWhiteSpace(ExportFilePath) && !string.IsNullOrWhiteSpace(filename))
            {
                string dir = System.IO.Path.GetDirectoryName(ExportFilePath);
                if (!System.IO.Directory.Exists(dir))
                {
                    System.IO.Directory.CreateDirectory(dir);
                }

                foreach (char c in System.IO.Path.GetInvalidFileNameChars())
                {
                    filename = filename.Replace(c, '_');
                }


                string finalPath = string.Format("{0}/{1}.png", ExportFilePath, filename);

                byte[] bytes = tex.EncodeToPNG();
                System.IO.File.WriteAllBytes(ExportFilePath + "/" + filename + ".png", bytes);
            }
        }
    }
}

There’s a lot going on here, and sure, it works, but reading it takes a little longer to pick it apart and figure out what’s doing what. It is more difficult to test and debug just the thumbnail code, just the object creation code, or just the multiple-object handling code.

The classes we implemented above separate the concerns of our final goal. One class is solely responsible for taking a snapshot of an object, one class is solely responsible for creating and destroying an object, and one class is solely responsible for looping through a list of objects.

In the section below, we’ll go over a way to extend the MultiObjectLoader (or per-frame processor) to do more than just take thumbnail images of objects. The generic IGameObjectActivity interface makes this architecture a lot more reusable, extensible, flexible, and testable.

Optional Improvements

Taking it a step further…

Focus Point

I have to mention “focus point”. This is optional, but can help improve the resulting thumbnail image. It’s very possible, and maybe very likely, that the focus point of an object for the purpose of a thumbnail image is not the same location as the object’s position.

Here’s an example from one of my game’s models:

Thumbnail without focus point.
Thumbnail with focus point.

The artillery unit’s origin is at the base of the model, so that it can interact with the terrain in the game without having to artificially move the origin during runtime, which can be unnecessary transform calculations. This is a convention that I decided on, and may not be what your game is using. But this has the adverse effect that, for thumbnail generation, the visual center point is too low at the object’s origin. Ideally, the visual center point would be at the base of the turret instead.

If this is the case, there are a couple of techniques we can use. I’ll only mention a few of them, and we’ll use one of them. 

  • One technique is to use the average point of all vertices in the 3D model. This can be time-consuming to calculate, and may not necessarily produce the “aesthetic center point” of the model.
  • Another technique is to take the average point of the object’s bounding box, but this also presents the same problem of possibly resulting in a center point that’s not aesthetically pleasing.
  • Lastly, we can use a focus point object. This is an Empty Object, which just contains a Transform for the sole purpose of informing our ThumbnailGenerator where to look. This is the technique we’ll use. 

Parent an Empty GameObject to each of the target objects to serve as the focus point for the camera, and name it “ThumbFocus”.

Add following block of code to the Setup() function in ObjectThumbnailActivity.cs. Basically, replace the existing camera LookAt() function call with this:

Transform camTarget = stage.transform;
            
List<Transform> transforms = new List<Transform>();
GameUtils.GetTransformsWithNameInHierarchy(_object.transform, "thumbfocus", ref transforms);
if (!transforms.IsEmpty())
{
     Transform first = transforms.FirstOrDefault();
     if (first != null)
     {
         camTarget = first;
         break;
     }
}


cam.transform.LookAt(camTarget);

You’ll need the following utility function below:

public static void GetTransformsWithNameInHierarchy(Transform root, string name, ref List<Transform> transforms)
{
     if (root == null)
          return;

     foreach (Transform t in root)
     {
          if (string.Compare(t.name, name, true) == 0)
              transforms.Add(t);

          GetTransformsWithNameInHierarchy(t, name, ref transforms);
     }
}

This searches for an EmptyObject named “ThumbFocus”, and if it can’t find it, it just resorts to the object’s position.

Refactor ObjectThumbnailActivity to IGameObjectActivity

In the Design section above, I mentioned that the implementation we went over was over-engineered. It certainly is for the immediate use-case, but let’s look at how that architecture works for us and pays itself off with more complex use-cases.

In fact, I had such a use-case, and it’s the very reason why I wanted to write this article. For my turn-based strategy game, I needed thumbnails for not only the units (tanks, planes, ships), but for the structures (factories, seaports, airports), and also for the terrain (field, forest, road, mountain, beach, water, and bridge).

Each of these different types (units, structures, and terrain) all have specific requirements for setting up the object before taking the thumbnail snapshot of it. There are different reasons for this.

For one, the terrain types generate their object geometry in different ways. For example, the forest tile type is basically the field type with some tree geometry, and the beach and bridge types require the water model to indicate its relation to the water.

if (terrainType == TerrainType.Field)
{
    RenderField();
}
else if (terrainType == TerrainType.Forest)
{
    RenderField();
    RenderTrees();
}

...

if (terrainType == TerrainType.Water)
{
    RenderWater();
}
else if (terrainType == TerrainType.Bridge)
{
    RenderWater();
    RenderBridge();
}
else if (terrainType == TerrainType.Shore)
{
    RenderShore();
    RenderWater();
}

For another, my seaport requires additional logic because I have two variants of it. A straight and a diagonal, but I only want to show the straight model, so I need to hide the diagonal.

if (model.Name.Contains("diagonal")
{
    HideModel(model);
}

RenderSeaportModel();

The above is just pseudocode for how all these issues were resolved, but I wanted to point out that there are indeed uses for supporting this architecture.

The best part about this is, you can take advantage of this pattern for other things that need per frame processing without creating a separate thread. You would write a new concrete class that implements the IGameObjectActivity interface, instantiate this new class and add it to the queue. Here is IGameObjectActivity.cs:

    /// <summary>
    /// Interface for any activity to perform
    /// </summary>
    public interface IGameObjectActivity
    {
        /// <summary>
        /// Run any setup, or bail if setup fails
        /// </summary>
        /// <returns></returns>
        bool Setup();

        /// <summary>
        /// Allow the runtime actvity to check
        /// if the process can be performed
        /// </summary>
        /// <returns></returns>
        bool CanProcess();

        /// <summary>
        /// Actually perform the activity.
        /// </summary>
        void Process();

        /// <summary>
        /// Run any cleanup necessary to advance to a next activity
        /// </summary>
        void Cleanup();
    }

So, for example, for the pseudocode above, I would have three separate activities:

public class UnitThumbActivity : IGameObjectActivity
{
    ...  // Implements unit-specific thumbnail behavior.
}

public class StructureThumbActivity : IGameObjectActivity
{
    ... // Implements structure-specific thumbnail behavior.
}

public class TerrainThumbActivity : IGameObjectActivity
{
    ... // Implements terrain-specific thumbnail behavior.
}

Similarly, we can create a new class ThumbnailActivity, which is EXACTLY the same as ObjectThumbnailActivity, but now implements IGameObjectActivity. It’s exactly the same because I had already named the functions to implement the interface.

/// <summary>
/// This class is exactly like ObjectThumbnailActivity.cs, but 
/// this now implements the IGameObjectActivity interface.
/// </summary>
public class ThumbnailActivity : IGameObjectActivity
{
    ... // Implementation is exactly the same as the existing ObjectThumbnailActivity.
}

And now in the MultiObjectProcessor, the _activities list contains objects that implement the IGameObjectActivity interface:

private List<IGameObjectActivity> _activities;

...

_activities = new List<IGameObjectActivity>();

...

Our per-frame processor now operates on any object that implements the interface IGameObjectActivity. It is no longer limited to just rendering thumbnails, or one type of thumbnail, for example.

Conclusion

And there you have it. The entirety of the project is available on github. While you can probably just take that code wholesale, I took the opportunity in this article to start from manual thumbnail creation in Unity to automating the entire process, with a little taste of Dependency Injection (DI) and good ol’ software engineering practices along the way.

Hope you learned something, but either way…

Make it fun!

This entry was posted in Dev, Programming. Bookmark the permalink.

2 Responses to Automatic Thumbnail Image Generator for 3D Objects in Unity

  1. Petit says:

    This seems SUPER! Definitely worth trying out.

    It seems a bit complicated – especially for people like me who are mainly using Blender or different software. If there could be a more in-depth “tutorial” on how to use this, it would be really helpful and I know dozens of people would use it.

    Please consider providing a more detailed explanation of this.
    Thank you!

    • a_a says:

      I’ll think about doing something like a video tutorial, but in the mean time, the full project and source are available to try in the github link in the article.

      Also note that I too am using Blender. I chose to use primitives to keep the focus on thumbnail generation. You can easily use Blender models here as prefabs, and the code would nearly be the same, as long as you use Resources.Load() to load your Blender files exported as fbx.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.