brgd.eu

Getting Spatial Data (and its metadata!) into Unity

2023-08-17

It's astonishingly hard to get geospatial data as 3D data into Unity. Especially if one wants to keep the metadata.

Someone led me to BlenderGIS, a pretty cool tool that can import shapefiles, including their metadata, into Blender, and from there, we can then export them as FBX. This will keep the metadata, which in Blender can be found under "custom properties" of an object. Note that to have the metadata, it's important to check 'Separate Objects' when importing the shapefile.

In Unity, then, we can import the data as we usually do. The separate objects are then actually here. Unfortunately, it seems at first that the metadata is lost.

It's not, actually! However, Unity cannot read the metadata out of the box. To read it, we needed to create a custom importer. I found this forum thread which led me in the right direction.

I will show how I achieved this in Unity with a Shapefile I imported. The shapefile contains trees, and for each tree, it had some metadata stored, like the height and radius of the tree.

First, I created a simple C# class that will hold the metadata:

using UnityEngine;

public class TreeData : MonoBehaviour
{
    public float height;
    public float volume;
    public float radius;

    public float x;
    public float y;
    public float z;
}

Note that this has its own x, y, and z coordinates. These are not the coordinates in Unity, but the CRS coordinates! It's nice to keep them for later.

Then, I created a custom importer for the FBX files.

using UnityEngine;
using UnityEditor;
using System.IO;

class CustomImportProcessor : AssetPostprocessor {
    void OnPostprocessGameObjectWithUserProperties(
        GameObject go, string[] names, System.Object[] values
    ) {
        ModelImporter importer = (ModelImporter)assetImporter;
        var asset_name = Path.GetFileName(importer.assetPath);
        Debug.LogFormat(
            "OnPostprocessGameObjectWithUserProperties(go = {0}) asset = {1}", 
            go.name, asset_name);
        
        float height = 0;
        float volume = 0;
        float radius = 0;
        Vector3 vec3 = new Vector3();
        
        for (int i = 0; i < names.Length; i++) {
            var name = names[i];
            var val = values[i];

            switch (name)
            {
                case "height":
                    height = (float)val;
                    break;
                case "volume":
                    volume = (float)val;
                    break;
                case "radius":
                    radius = (float)val;
                    break;
                case "x":
                    vec3.x = (float)val;
                    break;
                case "y":
                    vec3.y = (float)val;
                    break;
                case "z":
                    vec3.z = (float)val;
                    break;
            }
        }

        var CurrentTreeData = go.AddComponent<TreeData>();
        CurrentTreeData.height = height;
        CurrentTreeData.volume = volume;
        CurrentTreeData.radius = radius;
        CurrentTreeData.x = vec3.x;
        CurrentTreeData.y = vec3.y;
        CurrentTreeData.z = vec3.z;
    }
}

With that, every object that was imported from the FBX file will have the TreeData component attached to it, and we can now access the metadata from the shapefile!

Note that I think this is missing one important piece, namely that it checks the file which is currently imported, and only runs the importer then. As Unity runs this importer for every asset it finds, it will most likely fail for others. That's at least my understanding—I haven't tried it with other assets; for my quick project, this was enough.