/******************************************************************************** * * * This file is part of IfcOpenShell. * * * * IfcOpenShell is free software: you can redistribute it and/or modify * * it under the terms of the Lesser GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * * IfcOpenShell is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * Lesser GNU General Public License for more details. * * * * You should have received a copy of the Lesser GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ #ifdef WITH_GLTF #include "GltfSerializer.h" #include "../ifcparse/utils.h" #ifdef WITH_PROJ #include #endif #include static const uint32_t GLTF = 0x46546C67U; static const uint32_t JSON = 0x4E4F534A; static const uint32_t BIN = 0x004E4942; static const uint32_t CT_BYTE = 5120; static const uint32_t CT_UNSIGNED_BYTE = 5121; static const uint32_t CT_SHORT = 5122; static const uint32_t CT_UNSIGNED_SHORT = 5123; static const uint32_t CT_UNSIGNED_INT = 5125; static const uint32_t CT_FLOAT = 5126; static const uint32_t PRIM_POINTS = 0; static const uint32_t PRIM_LINES = 1; static const uint32_t PRIM_LINE_LOOP = 2; static const uint32_t PRIM_LINE_STRIP = 3; static const uint32_t PRIM_TRIANGLES = 4; static const uint32_t PRIM_TRIANGLE_STRIP = 5; static const uint32_t PRIM_TRIANGLE_FAN = 6; static const uint32_t ELEMENT_ARRAY_BUFFER = 34963; static const uint32_t ARRAY_BUFFER = 34962; GltfSerializer::GltfSerializer(const std::string& filename, const ifcopenshell::geometry::Settings& geometry_settings, const ifcopenshell::geometry::SerializerSettings& settings) : WriteOnlyGeometrySerializer(geometry_settings, settings) , filename_(filename) , tmp_filename1_(filename + ".indices.tmp") , tmp_filename2_(filename + ".vertices.tmp") , fstream_(IfcUtil::path::from_utf8(filename).c_str(), std::ios_base::binary) , tmp_fstream1_(IfcUtil::path::from_utf8(tmp_filename1_).c_str(), std::ios_base::binary) , tmp_fstream2_(IfcUtil::path::from_utf8(tmp_filename2_).c_str(), std::ios_base::binary) , bufferViewId(0) {} GltfSerializer::~GltfSerializer() { tmp_fstream1_.close(); tmp_fstream2_.close(); IfcUtil::path::delete_file(tmp_filename1_); IfcUtil::path::delete_file(tmp_filename2_); } bool GltfSerializer::ready() { return fstream_.is_open() && tmp_fstream1_.is_open() && tmp_fstream2_.is_open(); } void GltfSerializer::writeHeader() { json_["asset"]["generator"] = "IfcOpenShell IfcConvert " + std::string(IFCOPENSHELL_VERSION); json_["asset"]["version"] = "2.0"; json_["scene"] = 0; node_array_ = json::array(); json_["accessors"] = json::array(); json_["scenes"] = json::array(); json_["nodes"] = json::array(); json_["meshes"] = json::array(); json_["materials"] = json::array(); } int GltfSerializer::writeMaterial(const ifcopenshell::geometry::taxonomy::style::ptr style) { auto it = materials_.find(style->name); if (it != materials_.end()) { return it->second; } int idx = json_["materials"].size(); materials_[style->name] = idx; std::array base; base.fill(1.0); if (style->get_color()) { for (int i = 0; i < 3; ++i) { base[i] = style->get_color().ccomponents()(i); } } if (style->transparency == style->transparency) { base[3] = 1. - style->transparency; } if (style->has_specularity()) json_["materials"].push_back({ {"name", style->name}, {"doubleSided", true}, {"pbrMetallicRoughness", {{"baseColorFactor", base}, {"metallicFactor", 0}, {"roughnessFactor", 1.0 / style->specularity}}}}); else json_["materials"].push_back({ {"name", style->name}, {"doubleSided", true}, {"pbrMetallicRoughness", {{"baseColorFactor", base}, {"metallicFactor", 0}}}}); if (style->transparency == style->transparency && style->transparency > 1.e-9) { json_["materials"].back()["alphaMode"] = "BLEND"; } return idx; } template struct stride_name { static const char* const value; }; template <> const char* const stride_name<1U>::value = "SCALAR"; template <> const char* const stride_name<3U>::value = "VEC3"; template struct component_type { static const uint32_t value; }; template <> const uint32_t component_type::value = CT_UNSIGNED_INT; template <> const uint32_t component_type::value = CT_FLOAT; template size_t write_accessor(json& j, std::ofstream& ofs, It begin, It end, int bufferViewId) { auto num = std::distance(begin, end) / N; json accessor = json::object(); accessor["bufferView"] = bufferViewId; accessor["byteOffset"] = 0; accessor["componentType"] = component_type::value; accessor["count"] = num; if constexpr (N == 1) { j["bufferViews"].push_back({ {"buffer", 0}, {"byteOffset", (size_t)ofs.tellp()}, { "byteLength", num * 4}, {"target", ELEMENT_ARRAY_BUFFER} }); } else { j["bufferViews"].push_back({ {"buffer", 0}, {"byteStride", 12}, { "byteOffset", (size_t)ofs.tellp()}, { "byteLength", num * 12}, {"target", ARRAY_BUFFER}}); } std::array min, max; min.fill(std::numeric_limits::max()); max.fill(std::numeric_limits::lowest()); for (auto it = begin; it != end; it += N) { for (size_t i = 0; i < N; ++i) { const float& v = *(it + i); if (v < min[i]) { min[i] = v; } if (v > max[i]) { max[i] = v; } } } accessor["min"] = min; accessor["max"] = max; accessor["type"] = stride_name::value; ofs.write((const char*)&*begin, sizeof(typename It::value_type) * num * N); j["accessors"].push_back(accessor); return j["accessors"].size() - 1; } void GltfSerializer::write(const IfcGeom::TriangulationElement* o) { if (o->geometry().material_ids().empty()) { return; } size_t current_node_index = json_["nodes"].size(); auto current_leaf_index = current_node_index; json_["nodes"].emplace_back(); node_indices_[o->product()] = current_node_index; node_array_.push_back(current_node_index); auto m = o->transformation().data()->ccomponents(); if (o->parents().empty()) { roots_.push_back(current_node_index); } if (!o->parents().empty()) { // apply inverse of last parent -> overwrite product transform (m) m = o->parents().back()->transformation().data()->ccomponents().inverse() * m; for (auto it = o->parents().rbegin(); it != o->parents().rend(); ++it) { const auto jt = it + 1; const bool is_root = jt == o->parents().rend(); auto kt = node_indices_.find((*it)->product()); if (kt != node_indices_.end()) { // parent already processed as part of other parent sequence json_["nodes"][kt->second]["children"].push_back(current_node_index); break; } auto mm = (*it)->transformation().data()->ccomponents(); if (!is_root) { mm = (*jt)->transformation().data()->ccomponents().inverse() * mm; } json parent_node = json::object(); std::array matrix_flat; if (settings_.get().get() || settings_.get().get() || !is_root) { // y-up transform is only accounted for on root matrix_flat = { mm(0,0), mm(1,0), mm(2,0), mm(3,0), mm(0,1), mm(1,1), mm(2,1), mm(3,1), mm(0,2), mm(1,2), mm(2,2), mm(3,2), mm(0,3), mm(1,3), mm(2,3), mm(3,3) }; } else { // nb: note that this contains the Y-UP transform. matrix_flat = { mm(0,0), mm(2,0), -mm(1,0), mm(3,0), mm(0,1), mm(2,1), -mm(1,1), mm(3,1), mm(0,2), mm(2,2), -mm(1,2), mm(3,2), mm(0,3), mm(2,3), -mm(1,3), mm(3,3) }; } static const std::array identity_matrix = {1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1}; if (matrix_flat != identity_matrix) { // glTF validator complains about identity matrices parent_node["matrix"] = matrix_flat; } size_t new_node_index = json_["nodes"].size(); node_indices_[(*it)->product()] = new_node_index; node_array_.push_back(new_node_index); parent_node["name"] = object_id(o); parent_node["children"] = json::array({current_node_index}); json_["nodes"].push_back(parent_node); current_node_index = new_node_index; if (is_root) { roots_.push_back(current_node_index); } } } json node; { std::array matrix_flat; if (settings_.get().get() || settings_.get().get() || !o->parents().empty()) { // y-up transform is only accounted for on root matrix_flat = { m(0,0), m(1,0), m(2,0), m(3,0), m(0,1), m(1,1), m(2,1), m(3,1), m(0,2), m(1,2), m(2,2), m(3,2), m(0,3), m(1,3), m(2,3), m(3,3) }; } else { // nb: note that this contains the Y-UP transform. matrix_flat = { m(0,0), m(2,0), -m(1,0), m(3,0), m(0,1), m(2,1), -m(1,1), m(3,1), m(0,2), m(2,2), -m(1,2), m(3,2), m(0,3), m(2,3), -m(1,3), m(3,3) }; } static const std::array identity_matrix = {1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1}; if (matrix_flat != identity_matrix) { // glTF validator complains about identity matrices node["matrix"] = matrix_flat; } } node["name"] = object_id(o); int current_mesh_index; // See if this mesh has already been processed auto it = meshes_.find(o->geometry().id()); if (it == meshes_.end()) { auto mid1 = o->geometry().material_ids().begin(); auto mid0 = mid1; std::vector::const_iterator fid0; int stride; int primitive_type; if (!o->geometry().faces().empty()) { stride = 3; fid0 = o->geometry().faces().begin(); primitive_type = PRIM_TRIANGLES; } else { stride = 2; fid0 = o->geometry().edges().begin(); primitive_type = PRIM_LINES; } json mesh; mesh["name"] = o->geometry().id(); while (true) { // In glTF we need to decompose a mesh into several primitives // with a constant material. In the triangulations coming from // IfcOpenShell the materials are encoded in an additional set // of indices. Therefore we loop over the material indices to // find equal ranges of materials. Triangle indices then need // to be updated to reference the vertices only for the current // material. mid1++; if ((mid1 == o->geometry().material_ids().end()) || (*mid1 != *mid0)) { auto n = std::distance(mid0, mid1); auto fid1 = fid0 + n * stride; auto idx_range = std::minmax_element(fid0, fid1); const auto& idx_begin = *idx_range.first; const auto& idx_end = *idx_range.second + 1; std::vector idx_transformed; idx_transformed.reserve((n * stride)); std::transform(fid0, fid1, std::back_inserter(idx_transformed), [idx_begin](int i) { return i - idx_begin; }); json primitive = json::object(); primitive["indices"] = write_accessor<1U>(json_, tmp_fstream1_, idx_transformed.begin(), idx_transformed.end(), bufferViewId++); auto vbegin = o->geometry().verts().begin(); std::vector vf(vbegin + idx_begin * 3, vbegin + idx_end * 3); primitive["attributes"]["POSITION"] = write_accessor<3U>(json_, tmp_fstream2_, vf.begin(), vf.end(), bufferViewId++); if (o->geometry().normals().size()) { auto nbegin = o->geometry().normals().begin(); std::vector nf(nbegin + idx_begin * 3, nbegin + idx_end * 3); primitive["attributes"]["NORMAL"] = write_accessor<3U>(json_, tmp_fstream2_, nf.begin(), nf.end(), bufferViewId++); } if (*mid0 >= 0) { primitive["material"] = writeMaterial(o->geometry().materials()[*mid0]); } primitive["mode"] = primitive_type; mesh["primitives"].push_back(primitive); if (mid1 == o->geometry().material_ids().end()) { break; } mid0 = mid1; fid0 = fid1; } } json_["meshes"].push_back(mesh); meshes_[o->geometry().id()] = current_mesh_index = json_["meshes"].size() - 1; } else { current_mesh_index = it->second; } node["mesh"] = current_mesh_index; json_["nodes"][current_leaf_index] = node; } template struct padding_char { static const char value; }; template <> const char padding_char::value = ' '; template <> const char padding_char::value = '\x00'; uint32_t padding_for(uint32_t length) { return ((4 - (length % 4)) % 4); } template void write_padding(std::ostream& fs, uint32_t N) { uint32_t padding = padding_for(N); for (uint32_t i = 0; i < padding; ++i) { fs.put(padding_char::value); } } template void write_header(std::ostream& fs, uint32_t N) { uint32_t padding = padding_for(N); uint32_t header[] = { N + padding, iden }; fs.write((const char*)header, sizeof(header)); } template void write_block(std::ostream& fs, It begin, It end) { uint32_t N = std::distance(begin, end); write_header(fs, N); fs.write((const char*)&*begin, N); write_padding(fs, N); } void GltfSerializer::finalize() { // separate z up if (settings_.get().get()) { z_up_transform_ = json::object(); (*z_up_transform_)["name"] = "Z_UP"; static const std::array z_up_matrix = { 1, 0, 0, 0, 0, 0, -1, 0, 0, 1, 0, 0, 0, 0, 0, 1}; (*z_up_transform_)["matrix"] = z_up_matrix; (*z_up_transform_)["children"] = roots_; json_["nodes"].push_back(*z_up_transform_); } if (north_rotation_) { (*north_rotation_)["children"] = roots_; } if (ecef_transform_) { (*ecef_transform_)["children"] = roots_; } if (z_up_transform_) { (*ecef_transform_)["children"] = roots_; } tmp_fstream1_.close(); tmp_fstream2_.close(); std::vector binary_contents; // nb: uint32_t is the max buffer size in glTF uint32_t indices_length, binary_length; { std::ifstream ifs(IfcUtil::path::from_utf8(tmp_filename1_).c_str(), std::ios::binary); ifs.ignore(std::numeric_limits::max()); indices_length = ifs.gcount(); } { std::ifstream ifs(IfcUtil::path::from_utf8(tmp_filename2_).c_str(), std::ios::binary); ifs.ignore(std::numeric_limits::max()); binary_length = indices_length + ifs.gcount(); } json scene_0; if (geometry_settings().get().get()) { scene_0["nodes"] = roots_; } else if (north_rotation_ || ecef_transform_ || z_up_transform_) { scene_0["nodes"] = std::array{json_["nodes"].size() - 1}; } else { scene_0["nodes"] = node_array_; } json_["scenes"].push_back(scene_0); //The generated glb file will contain the indices buffer followed by the vertices buffer. //Therefore once we know the size of the indices buffer, we update our vertices buffer //to have an offset equal to the size of the indices buffer. for (auto &n : json_["bufferViews"]) { if (n.contains("byteStride")) { n["byteOffset"] = (int)n["byteOffset"] + indices_length; } } json_["buffers"].push_back({ {"byteLength", binary_length} }); std::string json_contents = json_.dump(); uint32_t json_length = (uint32_t) json_contents.size(); const int GLB_FILE_HEADER = 12; const int GLB_JSON_HEADER = 8; const int GLB_BINARY_CHUNK_HEADER = 8; uint32_t header[] = { GLTF, 2U, GLB_FILE_HEADER + GLB_JSON_HEADER + json_length + padding_for(json_length) + GLB_BINARY_CHUNK_HEADER + binary_length + padding_for(binary_length) }; fstream_.write((const char*)header, sizeof(header)); write_block(fstream_, json_contents.begin(), json_contents.end()); write_header(fstream_, binary_length); { //First, write the indices buffer into our glb file std::ifstream ifs(IfcUtil::path::from_utf8(tmp_filename1_).c_str(), std::ios::binary); fstream_ << ifs.rdbuf(); } { //Next, write the vertices buffer into our glb file std::ifstream ifs(IfcUtil::path::from_utf8(tmp_filename2_).c_str(), std::ios::binary); fstream_ << ifs.rdbuf(); } write_padding(fstream_, binary_length); } namespace { void normalize(std::array& v) { auto l = std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); v[0] /= l; v[1] /= l; v[2] /= l; } void cross(const std::array& v1, const std::array& v2, std::array& result) { result[0] = v1[1] * v2[2] - v1[2] * v2[1]; result[1] = v1[2] * v2[0] - v1[0] * v2[2]; result[2] = v1[0] * v2[1] - v1[1] * v2[0]; } void proj_log(void *, int, const char* c) { Logger::Error("PROJ: " + std::string(c)); } } void GltfSerializer::setFile(IfcParse::IfcFile* f) { if (!settings_.get().get()) { return; } boost::optional crs_epsg; boost::optional> crs_x_axis; boost::optional> eastings_northings_elevation; aggregate_of_instance::ptr coordops; try { coordops = f->instances_by_type("IfcCoordinateOperation"); } catch (IfcParse::IfcException&) { // Ignored. Schema likely doesn't support IfcCoordinateOperation. } if (coordops) { for (auto& coordop : *coordops) { IfcUtil::IfcBaseClass* source_crs = coordop->as()->get("SourceCRS"); if (source_crs->declaration().is("IfcGeometricRepresentationContext")) { IfcUtil::IfcBaseClass* target_crs = coordop->as()->get("TargetCRS"); auto name_attr = target_crs->as()->get("Name"); if (coordop->declaration().is("IfcMapConversion")) { if (!name_attr.isNull()) { std::string epsg_code = name_attr; crs_epsg = epsg_code; // @todo in which unit are these? double eastings = coordop->as()->get("Eastings"); double northings = coordop->as()->get("Northings"); double height = coordop->as()->get("OrthogonalHeight"); height = 0.; eastings_northings_elevation = { { eastings, northings, height} }; auto xaxis_attr = coordop->as()->get("XAxisAbscissa"); auto yaxis_attr = coordop->as()->get("XAxisOrdinate"); if (!xaxis_attr.isNull() && !yaxis_attr.isNull()) { double xaxis = xaxis_attr; double yaxis = yaxis_attr; crs_x_axis = { { xaxis, yaxis, 0. } }; } } } } } } if (!crs_epsg) { auto sites = f->instances_by_type("IfcSite"); if (sites && sites->size() == 1) { auto lat_attr = (*sites->begin())->as()->get("RefLatitude"); auto lon_attr = (*sites->begin())->as()->get("RefLongitude"); if (!lat_attr.isNull() && !lon_attr.isNull()) { std::vector lat_dms = lat_attr; std::vector lon_dms = lon_attr; auto to_decimal = [](const std::vector& dms) { double val = dms[0] + dms[1] / 60. + dms[2] / 3600.; if (dms.size() == 4) { val += dms[3] / 3600.e6; } return val; }; auto lat = to_decimal(lat_dms); auto lon = to_decimal(lon_dms); double elev = 0.; /* auto elev_attr = (*sites->begin())->as()->get("RefElevation"); if (!elev_attr->isNull()) { elev = *elev_attr; } */ crs_epsg.reset("EPSG:4326"); eastings_northings_elevation = { { lat, lon, elev } }; } } } auto contexts = f->instances_by_type_excl_subtypes("IfcGeometricRepresentationContext"); if (contexts && contexts->size() > 0) { auto context = (*contexts->begin())->as(); auto north_attr = context->get("TrueNorth"); if (!north_attr.isNull()) { IfcUtil::IfcBaseClass* north = north_attr; if (north->declaration().is("IfcDirection")) { std::vector ratios = north->as()->get("DirectionRatios"); crs_x_axis = { { ratios[1], -ratios[0], 0. } }; } } } #ifdef WITH_PROJ if (crs_epsg) { PJ_COORD wgs84_point; auto C = proj_context_create(); proj_log_func(C, nullptr, proj_log); // @todo a bit ugly we assume a proj.db in current working directory. // a very simplistic but at least portable solution. proj_context_set_database_path(C, "proj.db", nullptr, nullptr); if (*crs_epsg == "EPSG:4326") { wgs84_point = proj_coord( (*eastings_northings_elevation)[0], (*eastings_northings_elevation)[1], (*eastings_northings_elevation)[2], 0); } else { // @todo a bit ugly we assume a proj.db in current working directory. // a very simplistic but at least portable solution. proj_context_set_database_path(C, "proj.db", nullptr, nullptr); auto P = proj_create_crs_to_crs( C, crs_epsg->c_str(), "EPSG:4326", NULL); if (!P) { Logger::Error("Failed to create PROJ transformation object"); return; } auto a = proj_coord( (*eastings_northings_elevation)[0], (*eastings_northings_elevation)[1], (*eastings_northings_elevation)[2], 0); wgs84_point = proj_trans(P, PJ_FWD, a); Logger::Notice("Calculated latitude: " + std::to_string(wgs84_point.lp.lam) + " longitude: " + std::to_string(wgs84_point.lp.phi)); } std::swap(wgs84_point.lp.phi, wgs84_point.lp.lam); const char *input_crs = "+proj=latlong +datum=WGS84"; const char *output_crs = "+proj=geocent +datum=WGS84 +units=m"; // Create a transformation object PJ *transform = proj_create_crs_to_crs(C, input_crs, output_crs, NULL); // Perform the transformation PJ_COORD output_point = proj_trans(transform, PJ_FWD, wgs84_point); // Extract the ECEF coordinates double x = output_point.xyz.x; double y = output_point.xyz.y; double z = output_point.xyz.z; const char *ellipsoid_def = "WGS84"; // Create a CRS object representing the ellipsoid PJ *ellipsoid_crs = proj_create(C, ellipsoid_def); if (!ellipsoid_crs) { Logger::Error("Failed to create ellipsoid CRS"); return; } auto ellipse = proj_get_ellipsoid(C, ellipsoid_crs); int _; double semi_major, semi_minor, __; proj_ellipsoid_get_parameters(C, ellipse, &semi_major, &semi_minor, &_, &__); std::array dxyz = { { x * (1. / (semi_major * semi_major)), y * (1. / (semi_major * semi_major)), z * (1. / (semi_minor * semi_minor)) } }; normalize(dxyz); // Oblate spheroid, so X and Y axis are equal, so rotation around Z yields east axis. std::array east_xyz = { { -y, x, 0. } }; normalize(east_xyz); std::array north; cross(dxyz, east_xyz, north); std::array matrix = { east_xyz[0], east_xyz[1], east_xyz[2], 0, north[0], north[1], north[2], 0., dxyz[0], dxyz[1], dxyz[2], 0, 0,0,0,1 }; ecef_transform_ = json::object({ {"matrix", matrix } }); json_["extensions"]["CESIUM_RTC"]["center"] = std::array{ {x, y, z} }; json_["extensionsUsed"].push_back("CESIUM_RTC"); // Clean up proj_destroy(ellipsoid_crs); proj_destroy(transform); proj_context_destroy(C); } if (crs_x_axis) { normalize(*crs_x_axis); auto phi = std::atan2((*crs_x_axis)[1], (*crs_x_axis)[0]); north_rotation_ = json::object({ {"matrix", std::array{ +std::cos(-phi), -std::sin(-phi), 0., 0., +std::sin(-phi), +std::cos(-phi), 0., 0., 0., 0., 1., 0., 0., 0., 0., 1. }} }); } #endif } #endif