Assignment 3

Processing a BIM model using voxels

Deadline is 14 April 2023 at 11:59 pm

Late submission? 10% will be removed for each day that you are late.

You’re allowed for this assignment to work in a group of 3 (and thus submit only one solution for all of you). If you prefer to work alone or in pairs, it’s also fine, but we really do not recommend it.

The assignment is worth 20% of your final mark.



duplex apartment bim model

Changelog

Overview

The aim of this assignment is to implement a BIM to Geo conversion for one building using voxels. In short, this will involve:

  1. converting the BIM model to OBJ using IfcConvert;
  2. loading the data from the OBJ into memory;
  3. creating a voxel grid covering the model;
  4. voxelising the triangles from the relevant objects of the model to represent the building;
  5. marking the exterior voxels and the connected groups of interior voxels as separate rooms;
  6. extracting the surfaces of the outer envelope of the building and of each room;
  7. writing the geometries to a CityJSON file with correct semantics.

Everything should be implemented in C++ with the help of any packages from the CGAL library. You are also free to use any other external libraries, but only for reading and writing files, such as Niels Lohmann’s JSON for Modern C++ library.

How to get started

There is no sample code to download for this lesson, but we provide some tips and code snippets in the description of each step below.

To start developing your code, we recommend that you start with a very simple IFC file, such as the IfcOpenHouse. This will be useful to get started and to debug your code later on.

However, to fulfil the aim of the assignment, you should also download a more complex IFC file of your choice. For this, check out this repository from buildingSMART or this one from the developers of BIMserver. We recommend a realistic model that is still relatively simple, like the Duplex Apartment or one of the models made by KIT (those starting with AC). You can view the models with an IFC viewer, such as Solibri Anywhere, the Open IFC Viewer or the BlenderBIM add-on for Blender.

1. From IFC to OBJ

The first step is to convert the BIM model in IFC to an OBJ file (or several) containing the relevant parts of the building. It is up to you to decide which parts are relevant for the conversion.

For the conversion, you should use IfcConvert, which is part of the IfcOpenShell library. In order to select the relevant parts, you likely want to check out the options available in IfcConvert (using the --help option).

2. From OBJ to memory

First, parse the OBJ files generated by IfcConvert using your own code, loading each object into memory. In the OBJ files generated by IfcConvert, objects are denoted by the g (group) marker. These can have multiple shells, which are denoted by different materials (the usemtl marker). Check this Wikipedia article if you need help understanding the OBJ files.

You probably want to create your own simple data structures to load the data into. These could look something like this:

std::vector<Kernel::Point_3> points;

struct Face {
  std::vector<unsigned long> vertices; // indices in vector of points
};

struct Shell {
  std::vector<Face> faces;
};

struct Object {
  std::string id;
  std::vector<Shell> shells;
};

std::map<std::string, Object> objects;

3. The voxel grid

This part is quite straightforward: define a voxel grid that covers the whole building with a customisable resolution. Make the voxel grid at least one voxel bigger than necessary in all directions (such that all the voxels on the boundary of the grid are empty). This will make the method described in step 5 possible.

For the resolution, voxels of around 0.1 meters should be a good number to start, but you will want to play with this value to get a good result.

You’ll also likely want to create a data structure to store your voxel grid. If you want, here’s one simple data structure that you could use as a starting point:

struct VoxelGrid {
  std::vector<unsigned int> voxels;
  unsigned int max_x, max_y, max_z;
  
  VoxelGrid(unsigned int x, unsigned int y, unsigned int z) {
    max_x = x;
    max_y = y;
    max_z = z;
    unsigned int total_voxels = x*y*z;
    voxels.reserve(total_voxels);
    for (unsigned int i = 0; i < total_voxels; ++i) voxels.push_back(0);
  }
  
  unsigned int &operator()(const unsigned int &x, const unsigned int &y, const unsigned int &z) {
    assert(x >= 0 && x < max_x);
    assert(y >= 0 && y < max_y);
    assert(z >= 0 && z < max_z);
    return voxels[x + y*max_x + z*max_x*max_y];
  }
  
  unsigned int operator()(const unsigned int &x, const unsigned int &y, const unsigned int &z) const {
    assert(x >= 0 && x < max_x);
    assert(y >= 0 && y < max_y);
    assert(z >= 0 && z < max_z);
    return voxels[x + y*max_x + z*max_x*max_y];
  }
};

If you choose to use it, you can create a voxel grid easily: VoxelGrid voxels(rows_x, rows_y, rows_z). Note that it also has a function to access and modify the value of a specific voxel (as unsigned int v = voxels(x,y,z) or voxels(x,y,z) = 1).

In addition, you’ll likely want to have functions to translate from the model’s coordinates to the (integer) coordinates of the voxel grid and vice versa.

4. Voxelisation of triangles

In this step, you should voxelise the model by marking all voxels that are occupied by a relevant entity of the model with an ID. You can use the same ID for all elements. In other words, you do not need to use special IDs for different elements (eg windows and doors), but feel free to do so if you wish.

One method to voxelise 3D models made of triangles is discussed in the voxelisation chapter of the book. With this method, you will have to test whether a triangle and a line segment (target) intersect, which you can do with CGAL’s do_intersect. Note that triangle-line segment intersections can be quite fast using this method, but you might still run into performance problems, so you could try to minimise the number of intersection tests you have to make. A good heuristic for this is that a (voxelised) triangle can only have voxels (partly) within the bounding box of the original triangle. So, you can first compute the axis-oriented bounding box of a triangle (ie its minimum and maximum x,y,z coordinates), and then only compute the intersections for the voxels (partly) within the bounding box.

5. Marking exterior and room voxels

Once you have marked all the voxels that are part of the building, those voxels that are left unmarked are either part of the exterior of the model or part of the interior of the model. This step is meant to distinguish between the two cases, as well as to mark the voxels of different rooms with different IDs.

In order to mark the exterior voxels, start from any voxel on the boundary of the grid and mark all connected voxels without crossing any building voxels. See why the bigger voxel grid was necessary? Then, iteratively find an unmarked voxel and mark all voxels connected to it as part of a room. When no unmarked voxels remain, you have labelled all the rooms.

It is up to you to figure out what kind of connectivity you should use to determine what “connected voxels” means in this context.

6. Extracting the surfaces

Then, you will need to extract the surfaces that correspond to the outer envelope of the building, as well as those that correspond to each room. These will correspond to the voxels that you marked with different IDs.

In order to get nice surfaces, you’ll likely want to implement marching cubes for this step or to use something like the Poisson Surface Reconstruction package of CGAL.

7. Writing CityJSON

Starting from the surfaces created by the previous step, you should create a CityJSON 1.1 file containing them. This file should have several city objects: one Building and a few instances of BuildingRoom. These should follow the CityJSON specifications.

As for the writing of the file, it can be done manually or through a JSON library, such as Niels Lohmann’s JSON for Modern C++ library. Using this library, you have to create something like this:

#include "json.hpp"

nlohmann::json json;
json["type"] = "CityJSON";
json["version"] = "1.1";
json["transform"] = nlohmann::json::object();
json["transform"]["scale"] = nlohmann::json::array({1.0, 1.0, 1.0});
json["transform"]["translate"] = nlohmann::json::array({0.0, 0.0, 0.0});
json["CityObjects"] = nlohmann::json::object();
...
std::string json_string = json.dump(2);
std::ofstream out_stream("mybuilding.json");
out_stream << json_string;
out_stream.close();

Deliverables

You have to submit a ZIP file (or another compressed format) with:

  1. the original IFC file that you chose to convert,
  2. the intermediate OBJ file(s) generated by IfcConvert,
  3. the CityJSON file with the results of your conversion,
  4. a report in PDF format (not in Microsoft Word),
  5. the code you wrote to do the assignment, and
  6. a Readme file that explains what all the files submitted are and how should I run the code.

Submit your files by uploading them here.

1. Input IFC file

Likely the same as what you downloaded. If you had to modify it for your code to work, document it in the report and submit your modified version.

2. Intermediate OBJ files

Likely the file(s) generated by IfcConvert. If you had to modify them for your code to work, document it in the report and submit your modified versions.

3. Output CityJSON file

The file should be produced with your own code. If specific settings/parameters are required to produce it, explain it in the Readme.

4. Report

You need to submit a simple report outlining your work. It should document the main steps in your conversion methodology, explain briefly how you implemented them, and why you chose to implement them in this manner. Focus on high-level descriptions of how things work and the engineering/design choices that you made, not on explaining your code line by line. Discuss the pros and cons of your method, assess the quality of your results and the performance of your code. We don’t expect perfect results—just document honestly what works well and what doesn’t.

Use a small section of your report to document how the tasks were divided among the members of the team. The workload needs to be relatively equal.

We expect maximum 10 pages for the report (figures and graphs included). Don’t forget to write your names.

5. Code

You should submit all the files that I need to run your code. The code should read the input OBJ file(s) and output the converted CityJSON file. Everything should be implemented in C++ using only the following libraries: CGAL, anything used only to read and write files, and everything from the C++ Standard Library.

6. Readme

It should: (i) describe all files submitted, and (ii) tell us how to run the code. Provide enough instructions so that we can generate exactly the CityJSON file uploaded from the OBJ files submitted.

Marking

Criterion Points
followed all rules 1
quality of results 5
quality of report 4

[last updated: 2023-03-29 13:30]