Assignment 3

Processing a BIM model using CSG

Deadline is 04 April 2021 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.



wireframe

Overview

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

  1. converting the BIM model to OBJ using IfcConvert;
  2. loading each object in the OBJ file(s) into a CGAL Nef polyhedron;
  3. processing the Nef polyhedra into a single big Nef polyhedron representing the space filled by the building’s floors, walls, windows and so on;
  4. extracting the geometries representing the building’s exterior surface and individual rooms;
  5. writing the geometries to a CityJSON file.

Everything should be implemented in C++ with the help of 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 from the Open IFC Model Repository. We recommend a realistic model that is still relatively simple, like the ERDC:Duplex Apartment Model or one of the KIT models. You can view the models with an IFC viewer, such as Solibri Anywhere, the Open IFC Viewer or the BlenderBIM add-on for Blender.

Once you have understood the homework assignment and did some initial tests, we highly recommend you discuss your strategy with us (through Discord or in person). If you get stuck on a step, please also talk to us!

General CGAL help

CGAL is a bit tricky to use, since it is a complex piece of software that uses a lot of advanced C++ features. We recommend that you use the following headers:

#include <CGAL/Exact_predicates_exact_constructions_kernel.h>
#include <CGAL/Polyhedron_3.h>
#include <CGAL/Polyhedron_incremental_builder_3.h>
#include <CGAL/Nef_polyhedron_3.h>

Based on these headers, you can also add the definitions below. These will make your code shorter and more readable (eg writing Polyhedron rather than CGAL::Polyhedron_3<Kernel>). The names defined here will match with the code snippets below.

typedef CGAL::Exact_predicates_exact_constructions_kernel Kernel;
typedef Kernel::Point_3 Point;
typedef CGAL::Polyhedron_3<Kernel> Polyhedron;
typedef CGAL::Nef_polyhedron_3<Kernel> Nef_polyhedron;

Check the CGAL documentation to understand how to use CGAL’s Point_3, Polyhedron_3 and Nef_polyhedron_3. Note that for each of these, there’s a User Manual with a general explanation and a Reference Manual with explanations of every class and function. There is sample code that you can try in both of these manuals.

In order to compile code that uses CGAL, you should have installed it according to the instructions in Homework 0. Then, if you use CLion (or anything else that uses CMake), you should add the following CMake commands in your CMakeLists.txt to find CGAL and to include it:

find_package(CGAL)
include(${CGAL_USE_FILE})

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 Nef polyhedra

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:

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

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

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

Next, load each shell into a CGAL Polyhedron_3 using the Polyhedron_incremental_builder_3. In order to use the Polyhedron_incremental_builder_3, you need to create a custom struct or class like this one:

template <class HDS>
struct Polyhedron_builder : public CGAL::Modifier_base<HDS> {
  std::vector<Point> vertices;
  std::vector<std::vector<unsigned long>> faces;

  Polyhedron_builder() {}
  void operator()(HDS& hds) {
    CGAL::Polyhedron_incremental_builder_3<HDS> builder(hds, true);
    std::cout << "building surface with " << vertices.size() << " vertices and " << faces.size() << " faces" << std::endl;
    
    builder.begin_surface(vertices.size(), faces.size());
    for (auto const &vertex: vertices) builder.add_vertex(vertex);
    for (auto const &face: faces) builder.add_facet(face.begin(), face.end());
    builder.end_surface();
  }
};

Which you use by defining an instance of Polyhedron_builder_3. Add a shell’s vertices and faces to it, then call the builder using the delegate function. It will look something like this:

Polyhedron polyhedron;
Polyhedron_builder<Polyhedron::HalfedgeDS> polyhedron_builder;
for (auto const &face: ...) {
    polyhedron_builder.faces.emplace_back();
    for (auto const &vertex: face.vertices) {
        polyhedron_builder.vertices.push_back(...);
        polyhedron_builder.faces.back().push_back(...);
    }
} polyhedron.delegate(polyhedron_builder);

Note that the polyhedron builder (which does something similar to what you did in hw1!) is a bit picky. It only works well if the vertices are unique (ie faces sharing a vertex should refer to the same index in the vertices list), whereas the OBJ files generated by IfcConvert repeat vertex coordinates. It also doesn’t like inconsistently oriented input. It is up to you to deal with these issues before passing the input to Polyhedron_builder_3.

Finally, after you have a Polyhedron_3, you can easily create a Nef_polyhedron_3 from it using the interface between the two, as long as the polyhedron is closed (ie polyhedron.is_closed()). For this, you do something like:

Nef_polyhedron nef_polyhedron(polyhedron);

3. The big Nef polyhedron

This is the most open part of the assignment. In general terms, we expect you to apply CSG operations to merge the individual Nef polyhedra into one big Nef polyhedron containing all the space that is filled by a building’s floors, walls, windows and so on.

At the end of this process, you should ideally have something with one exterior shell (the outer surface of the building) and a few inner shells (one for each room).

We recommend you to have a look at the Boolean point set operations available in CGAL’s Nef polyhedra. In addition, you might want to have a look at the 3D Minkowski sum package, since you can use it to fill in small gaps between objects. Feel free to come up with other strategies (eg moving points around).

4. Extracting geometries

Starting from the single big Nef polyhedron, you should navigate through the Nef_polyhedron_3 structure to extract the geometries of the different shells and do some final processing. The aim of this step should be to obtain a set of simpler geometries with semantics.

Thinking of using the interface between Nef_polyhedron_3 and Polyhedron_3? It’s probably a bad idea, since this only works if the Nef polyhedron is simple (nef_polyhedron.is_simple()), which your Nef polyhedron shouldn’t be.

In order to navigate through the (quite complex) Nef_polyhedron_3 structure, CGAL provides a set of useful macros. For this assignment, the most useful ones are CGAL_forall_volumes and CGAL_forall_shells_of. To use the latter one, you need to define a custom struct or class like this one:

struct Shell_explorer {
    std::vector<Point> vertices;
    std::vector<std::vector<unsigned long>> faces;

    void visit(Nef_polyhedron::Vertex_const_handle v) {}
    void visit(Nef_polyhedron::Halfedge_const_handle he) {}
    void visit(Nef_polyhedron::SHalfedge_const_handle she) {}
    void visit(Nef_polyhedron::SHalfloop_const_handle shl) {}
    void visit(Nef_polyhedron::SFace_const_handle sf) {}

    void visit(Nef_polyhedron::Halffacet_const_handle hf) {
        ... // do something to each half-face of a shell
    }
};

Such a structure is meant to follow what is called a visitor pattern, where each function will be called once per each element in the shell. Meanwhile, the main code could look something like this:

Nef_polyhedron::Volume_const_iterator current_volume;
CGAL_forall_volumes(current_volume, big_nef) {
    Nef_polyhedron::Shell_entry_const_iterator current_shell;
    CGAL_forall_shells_of(current_shell, current_volume) {
        Shell_explorer se;
        Nef_polyhedron::SFace_const_handle sface_in_shell(current_shell);
        big_nef.visit_shell_objects(sface_in_shell, se);
        ...
    }
}

If you have more volumes and shells than you expected in your big Nef polyhedron, check this explanation. Check this figure to understand how to navigate the data structure used in Nef polyhedra (SNC).

As you extract the geometries of the different shells, try to keep track of their meaning when possible (eg whether they are part of the exterior surface or the inner surface of a room). Also, you should attempt to derive the other semantics required by CityJSON (eg whether an exterior surface is a RoofSurface, WallSurface or GroundSurface). For this, you can use a surface’s properties, such as its location or normal vector.

5. Writing CityJSON

Starting from the simple geometries with semantics 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 would have something like this:

#include "json.hpp"

nlohmann::json json;
json["type"] = "CityJSON";
json["version"] = "1.0";
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();

Since CityJSON 1.1 (with its support for building rooms) is quite new, unfortunately there aren’t many good ways to view your results yet. Use ninja in a few days (Stelios Vitalis is working on it!), azul on a Mac (works if you put version 1.0 in the file), or maybe use cjio to convert to OBJ? If you figure out a good method, please post something on Discord.

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.

Input IFC file

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

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.

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.

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, 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 12-15 pages for the report (figures and graphs included). Don’t forget to write your names.

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.

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 6
quality of report 3

[last updated: 2022-03-16 13:55]