From BIM to Unity: Building a Real-Time BIMlink Importer

July 22, 2020


The problem: BIM models change constantly during construction. Pre-baked Unity builds are outdated before deployment. Users need to see current model state — latest revisions, updated elements, recent changes.

Standard approach: export from Revit, import to Unity, build, deploy. Every model update requires a full rebuild cycle. Slow. Brittle. Doesn't scale.

We needed dynamic loading. Connect directly to BIM database. Stream geometry and metadata at runtime. Display current state without rebuilds.

Architecture Strategy

The importer operates in three phases:

Type extraction — Load unique element types once, create reusable prefabs Element instantiation — Place instances of types at specific locations
Hierarchy construction — Organize by category and storey for filtering

This separates concerns cleanly. Types handle geometry. Elements handle positioning. Hierarchy handles organization.

Type Library Pattern

BIM models have thousands of elements but relatively few unique types. A building might have 500 doors, but only 15 door types. Load each type once, instantiate many times.

public static ModelController ImportBimLinkJsonModel(
    Transform modelParent,
    ImportReport report,
    IList<string> typeStrings,
    IList<string> elementStrings)
{
    Dictionary<long, BimLinkObject> typelib = new Dictionary<long, BimLinkObject>();

    report.StartTask("TypeImport", "importing all types", ImportReport.MsgType.Info);

    List<(long id, string name)> settingsStoreys = 
        ApplicationConfiguration.Configuration.Model.Storeys;

    for (int i = 0; i < settingsStoreys.Count; i++)
    {
        report.StartTask($"StoreyTyped: {settingsStoreys[i]}", 
            $"storey {settingsStoreys[i]} types downloaded", 
            ImportReport.MsgType.Debug);

        // Add types to typelib, duplicates skipped
        BimDataConverter.GetTypesFrom<TypeProxy>(typeStrings[i], typelib);

        report.EndTask($"StoreyTyped: {settingsStoreys[i]}", "");
    }

    report.EndTask("TypeImport", "");

    report.StartTask("ElementImport", "importing all elements", 
        ImportReport.MsgType.Info);

    for (int i = 0; i < settingsStoreys.Count; i++)
    {
        report.StartTask($"StoreyElem:{settingsStoreys[i]}", 
            $"storey {settingsStoreys[i]} elements downloaded", 
            ImportReport.MsgType.Debug);
        
        // Add element instances for types we've just created
        BimDataConverter.AddElements(elementStrings[i], 
            settingsStoreys[i].id, settingsStoreys[i].name, typelib);
        
        report.EndTask($"StoreyElem:{settingsStoreys[i]}", "elements created");
    }

    report.EndTask("ElementImport", "");

    return CreateModel(typelib, modelParent);
}

First pass: Process all type definitions, build type library.
Second pass: Process element instances, referencing types by ID.
Result: Memory-efficient structure ready for scene construction.

Types store geometry data — vertices, submeshes, materials. Elements store only transform and parameter data. This reduces memory significantly compared to duplicating geometry per instance.

Coordinate System Transformation

BIM models use right-handed coordinate systems. Unity uses left-handed. Direct import produces mirrored geometry. The solution: handedness corrector transform.

private static ModelController CreateModel(
    Dictionary<long, BimLinkObject> data, 
    Transform parent)
{
    Transform handednessCorrector = new GameObject("Handedness Corrector").transform;

    handednessCorrector.transform.SetParent(parent);
    handednessCorrector.transform.localPosition = Vector3.zero;
    handednessCorrector.transform.localRotation = Quaternion.identity;
    handednessCorrector.transform.localScale = Vector3.one;
    handednessCorrector.gameObject.tag = "StoreyParent";

    // ... build hierarchy under handednessCorrector ...

    if (!ApplicationConfiguration.Configuration.Model.UseRhs)
    {
        handednessCorrector.localScale = new Vector3(-1, 1, 1);
    }

    ModelController mc = parent.gameObject.AddComponent<ModelController>();
    mc.Categories = handednessCorrector.gameObject
        .AddComponent<CategoryController>();

    return mc;
}

All geometry imports normally. After construction, if the source uses right-handed coordinates, flip the entire hierarchy by inverting X scale. This handles the conversion in one operation rather than transforming each vertex.

Scale of (-1, 1, 1) mirrors across the YZ plane. All child transforms flip correctly. Rotations remain valid. This approach is simple and performant.

The Origin Problem: Floating-Point Precision

BIM models use real-world coordinates. A building in Amsterdam might sit at coordinates like (121000, 485000) meters in the Dutch RD coordinate system. Import those values directly into Unity and everything breaks.

The cause: Unity uses 32-bit floats for position values. Single-precision floating-point has roughly 7 significant digits of precision. At position (121000, 0, 485000), that precision translates to errors measured in centimeters. Move the camera, and objects jitter. Render a wall, and vertices dance.

This isn't a Unity bug — it's fundamental to how floating-point math works. The further from origin, the larger the precision errors.

Symptoms at large coordinates:

A building 500km from origin exhibits millimeter-scale jitter. Unacceptable for construction AR where we need sub-centimeter accuracy.

The solution: Place everything at origin. Offset all coordinates relative to a reference point within the model itself.

// BIM coordinates: real-world survey position
Vector3 bimPosition = element.transform.ExtractPosition();  // e.g. (121543, 12, 485221)

// Reference point: center of the model's bounding box or first element
Vector3 modelOrigin = modelReferencePoint;  // e.g. (121500, 0, 485200)

// Unity position: relative to model origin, now near (0,0,0)
Vector3 unityPosition = (bimPosition - modelOrigin) * scale;  // e.g. (43, 12, 21) * 0.001

All elements position relative to the model's own origin. The model itself sits at Unity's world origin. Every position value stays small. Precision stays high.

The AR anchoring system handles the real-world positioning separately. QR markers encode their real-world locations. When a marker is tracked, the entire model transforms correctly — but the internal geometry stays origin-relative, preserving precision.

This is why the ModelController parents to a transform hierarchy rather than positioning elements in absolute world space. The parent transform handles world positioning. Child elements stay local.

Result: Sub-millimeter precision regardless of where the building actually sits on Earth.

Hierarchical Organization

BIM models have natural hierarchy: Category → Storey → Element. The importer maintains this structure for filtering and visibility control.

Dictionary<string, Transform> storeys = new Dictionary<string, Transform>();
Dictionary<int, Transform> categories = new Dictionary<int, Transform>(23);

foreach (KeyValuePair<long, BimLinkObject> type in data)
{
    int cat = (int)type.Value.category;
    Transform catTransform;
    
    if (categories.ContainsKey(cat))
    {
        catTransform = categories[cat];
    }
    else
    {
        catTransform = new GameObject(((ModelImporter.Category)cat)
            .ToString()).transform;
        catTransform.SetParent(handednessCorrector);
        categories.Add(cat, catTransform);
    }

    foreach (KeyValuePair<Storey, List<Element>> storey in type.Value.elements)
    {
        Transform storeyTransform;
        string key = $"{cat}-{storey.Key.id}";

        if (storeys.ContainsKey(key))
        {
            storeyTransform = storeys[key];
        }
        else
        {
            storeyTransform = new GameObject(storey.Key.name).transform;
            storeyTransform.SetParent(catTransform);
            storeys.Add(key, storeyTransform);
        }

        foreach (Element element in storey.Value)
        {
            Transform elem;
            if (prefabPlaced)
            {
                elem = Instantiate(type.Value.prefab).transform;
            }
            else
            {
                type.Value.prefab.SetActive(true);
                elem = type.Value.prefab.transform;
                prefabPlaced = true;
            }

            // Add element parameters
            elem.GetComponent<BimMetaData>()
                .AddData(element.parameters, element.guid, element.id);

            elem.SetParent(storeyTransform);
            elem.position = element.transform.ExtractPosition() * 
                ApplicationConfiguration.Configuration.Model.Scale;
            elem.rotation = element.transform.ExtractRotation();
            elem.localScale = element.transform.ExtractScale();
        }
    }
}

This creates hierarchy:

Handedness Corrector
  ├─ Walls
     ├─ Ground Floor
        ├─ Wall_001
        └─ Wall_002
     └─ First Floor
         └─ Wall_003
  ├─ Doors
     ├─ Ground Floor
        └─ Door_001
     └─ First Floor
         └─ Door_002
  └─ Structure
      └─ Ground Floor
          └─ Column_001

Categories enable filtering — hide all doors, show only structure. Storeys enable level isolation — focus on specific floor. Both use Unity's built-in transform hierarchy, no custom filtering logic needed.

Prefab Reuse Strategy

First element of each type keeps the original prefab. Subsequent elements instantiate copies. This optimization reduces memory allocations:

bool prefabPlaced = false;

foreach (Element element in storey.Value)
{
    Transform elem;
    if (prefabPlaced)
    {
        elem = Instantiate(type.Value.prefab).transform;
    }
    else
    {
        type.Value.prefab.SetActive(true);
        elem = type.Value.prefab.transform;
        prefabPlaced = true;
    }
    
    // Position and configure element...
}

The first instance reuses the prefab created during type loading. This saves one instantiation per type. For models with thousands of types, this accumulates to significant savings.

JSON Proxy Pattern

BIM data contains extensive information. Most isn't needed for rendering. Proxies extract only required fields, reducing deserialization cost and memory usage.

public static void GetTypesFrom<T>(string data, 
    Dictionary<long, BimLinkObject> typelib) 
    where T : TypeProxyBase
{
    // Create type proxy collection extracting minimally used data
    ProxyCollection<T> types = ProxyCollection<T>.Get(data);
    
    foreach (T typeProxy in types.proxies)
    {
        if (typelib.ContainsKey(typeProxy.id))
        {
            Debug.LogError("Double types");
            continue;
        }

        typelib.Add(typeProxy.id, new BimLinkObject(typeProxy));
    }
}

Full BIM elements have dozens of properties. The proxy extracts:

Everything else is ignored during initial import. If detailed data is needed later, request it specifically for selected elements rather than loading everything upfront.

Performance Characteristics

Import speed: ~2000 elements/second on typical hardware
Memory usage: ~200MB for medium-sized building (50,000 elements)
Hierarchy construction: Less than 500ms after data received
Type deduplication: 10-20x reduction in geometry storage

The type library pattern is the key optimization. Without it, memory scales linearly with element count. With it, memory scales with unique type count — typically 5-10% of total elements.

Scale and Transform Extraction

BIM transform matrices need decomposition into Unity's position-rotation-scale format:

elem.position = element.transform.ExtractPosition() * 
    ApplicationConfiguration.Configuration.Model.Scale;
elem.rotation = element.transform.ExtractRotation();
elem.localScale = element.transform.ExtractScale();

The matrix extensions handle conversion. Position gets scaled to match Unity's world units. Rotation extracts as quaternion. Scale separates from other components.

This preserves all transform information from the source while adapting to Unity's conventions. Elements maintain correct relative positions and orientations.

Metadata Attachment

Each element carries metadata from the BIM model — parameters, properties, relationships:

elem.GetComponent<BimMetaData>()
    .AddData(element.parameters, element.guid, element.id);

This enables selection and inspection. Click an element in AR, retrieve its BIM properties. The metadata stays attached to the GameObject, accessible throughout the application.

Parameters include:

All available for display or filtering without additional database queries.

Why Dynamic Import Matters

Pre-baked models lock in specific model state. Any change requires full rebuild and redeployment cycle. This doesn't match construction reality where models update daily.

Dynamic import enables:

Always current data. Launch app, load latest model state.

No rebuild cycle. Model updates don't require app updates.

Selective loading. Load only relevant storeys or categories.

A/B comparison. Load two model versions simultaneously to see changes.

Parametric exploration. Adjust parameters, reload, see results.

The type library pattern makes this performant. The proxy pattern keeps memory reasonable. The hierarchical organization maintains usability.

Most AR-BIM solutions require pre-baked models. This approach enables dynamic, on-demand loading from live BIM databases. That changes what's possible in the field.