diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt index c580f5b34c..010228d227 100644 --- a/cmake/CMakeLists.txt +++ b/cmake/CMakeLists.txt @@ -34,6 +34,7 @@ endforeach() OPTION(UNICODE_SUPPORT "Build IfcOpenShell with Unicode support (requires ICU)." ON) OPTION(COLLADA_SUPPORT "Build IfcConvert with COLLADA support (requires OpenCOLLADA)." ON) OPTION(IFCXML_SUPPORT "Build IfcParse with ifcXML support (requires libxml2)." ON) +OPTION(GLTF_SUPPORT "Build IfcConvert with glTF support (requires json.hpp)." OFF) OPTION(ENABLE_BUILD_OPTIMIZATIONS "Enable certain compiler and linker optimizations on RelWithDebInfo and Release builds." OFF) OPTION(IFCCONVERT_DOUBLE_PRECISION "IfcConvert: Use double precision floating-point numbers." ON) OPTION(BUILD_IFCGEOM "Build IfcGeom." ON) @@ -112,6 +113,17 @@ UNIFY_ENVVARS_AND_CACHE(LIBXML2_LIBRARIES) UNIFY_ENVVARS_AND_CACHE(PCRE_LIBRARY_DIR) UNIFY_ENVVARS_AND_CACHE(PYTHON_EXECUTABLE) +if (GLTF_SUPPORT) +UNIFY_ENVVARS_AND_CACHE(JSON_INCLUDE_DIR) +FIND_FILE(json_hpp "json.hpp" ${JSON_INCLUDE_DIR}) +IF(json_hpp) + MESSAGE(STATUS "JSON for Modern C++ header file found") +ELSE() + MESSAGE(FATAL_ERROR "Unable to find JSON for Modern C++ header file, aborting") +ENDIF() +add_definitions(-DWITH_GLTF) +endif() + # Set INSTALL_RPATH for target MACRO(SET_INSTALL_RPATHS _target _paths) SET(${_target}_rpaths "") @@ -468,7 +480,7 @@ if (IFCCONVERT_DOUBLE_PRECISION) endif() INCLUDE_DIRECTORIES(${INCLUDE_DIRECTORIES} ${OCC_INCLUDE_DIR} ${OPENCOLLADA_INCLUDE_DIRS} - ${ICU_INCLUDE_DIR} ${Boost_INCLUDE_DIRS} ${LIBXML2_INCLUDE_DIR} + ${ICU_INCLUDE_DIR} ${Boost_INCLUDE_DIRS} ${LIBXML2_INCLUDE_DIR} ${JSON_INCLUDE_DIR} ) function(files_for_ifc_version IFC_VERSION RESULT_NAME) diff --git a/src/ifcconvert/IfcConvert.cpp b/src/ifcconvert/IfcConvert.cpp index 07f8d343ca..05eeef7d5b 100644 --- a/src/ifcconvert/IfcConvert.cpp +++ b/src/ifcconvert/IfcConvert.cpp @@ -27,6 +27,7 @@ ********************************************************************************/ #include "../serializers/ColladaSerializer.h" +#include "../serializers/GltfSerializer.h" #include "../serializers/IgesSerializer.h" #include "../serializers/StepSerializer.h" #include "../serializers/WavefrontObjSerializer.h" @@ -92,6 +93,9 @@ void print_usage(bool suggest_help = true) << " .obj WaveFront OBJ (a .mtl file is also created)\n" #ifdef WITH_OPENCOLLADA << " .dae Collada Digital Assets Exchange\n" +#endif +#ifdef WITH_GLTF + << " .glb glTF Binary glTF v2.0\n" #endif << " .stp STEP Standard for the Exchange of Product Data\n" << " .igs IGES Initial Graphics Exchange Specification\n" @@ -519,6 +523,7 @@ int main(int argc, char** argv) { const path_t OBJ = IfcUtil::path::from_utf8(".obj"), MTL = IfcUtil::path::from_utf8(".mtl"), DAE = IfcUtil::path::from_utf8(".dae"), + GLB = IfcUtil::path::from_utf8(".glb"), STP = IfcUtil::path::from_utf8(".stp"), IGS = IfcUtil::path::from_utf8(".igs"), SVG = IfcUtil::path::from_utf8(".svg"), @@ -644,6 +649,10 @@ int main(int argc, char** argv) { #ifdef WITH_OPENCOLLADA } else if (output_extension == DAE) { serializer = boost::make_shared(IfcUtil::path::to_utf8(output_temp_filename), settings); +#endif +#ifdef WITH_GLTF + } else if (output_extension == GLB) { + serializer = boost::make_shared(IfcUtil::path::to_utf8(output_temp_filename), settings); #endif } else if (output_extension == STP) { serializer = boost::make_shared(IfcUtil::path::to_utf8(output_temp_filename), settings); diff --git a/src/serializers/GltfSerializer.cpp b/src/serializers/GltfSerializer.cpp new file mode 100644 index 0000000000..ba3f5f2e35 --- /dev/null +++ b/src/serializers/GltfSerializer.cpp @@ -0,0 +1,327 @@ +/******************************************************************************** + * * + * 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" + +#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; + +GltfSerializer::GltfSerializer(const std::string& filename, const SerializerSettings& settings) + : GeometrySerializer(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) + {} + +bool GltfSerializer::ready() { + return fstream_.is_open() && tmp_fstream1_.is_open() && tmp_fstream2_.is_open(); +} + +void GltfSerializer::writeHeader() { + json_["asset"]["generator"] = "IfcOpenShell IfcConvert " 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 IfcGeom::Material& 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.hasDiffuse()) { + for (int i = 0; i < 3; ++i) { + base[i] = style.diffuse()[i]; + } + } + if (style.hasTransparency()) { + base[3] = 1. - style.transparency(); + } + + json_["materials"].push_back({ {"pbrMetallicRoughness", {{"baseColorFactor", base}, {"metallicFactor", 0}}} }); + + if (style.hasTransparency() && 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) { + auto num = std::distance(begin, end) / N; + + json accessor = json::object(); + + accessor["bufferView"] = N == 1 ? 0 : 1; + accessor["byteOffset"] = (size_t)ofs.tellp(); + accessor["componentType"] = component_type::value; + accessor["count"] = num; + + std::array min, max; + min.fill(std::numeric_limits::max()); + max.fill(std::numeric_limits::min()); + 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(It::value_type) * num * N); + + j["accessors"].push_back(accessor); + + return j["accessors"].size() - 1; +} + +void GltfSerializer::write(const IfcGeom::TriangulationElement* o) { + node_array_.push_back(json_["nodes"].size()); + + const std::vector& m = o->transformation().matrix().data(); + // nb: note that this contains the Y-UP transform as well. + const std::array matrix_flat = { + m[0], m[ 2], m[ 1], 0, + m[3], m[ 5], m[ 4], 0, + m[6], m[ 8], m[ 7], 0, + m[9], m[11], m[10], 1 + }; + static const std::array identity_matrix = {1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1}; + + json node; + if (matrix_flat != identity_matrix) { + // glTF validator complains about identity matrices + node["matrix"] = matrix_flat; + } + + 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; + auto fid0 = o->geometry().faces().begin(); + + 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 * 3; + + 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 * 3)); + 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()); + + 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()); + + 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()); + + primitive["material"] = writeMaterial(o->geometry().materials()[*mid0]); + primitive["mode"] = PRIM_TRIANGLES; + + 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"].push_back(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() { + 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; + scene_0["nodes"] = node_array_; + json_["scenes"].push_back(scene_0); + json_["bufferViews"].push_back({ {"buffer", 0}, { "byteLength", indices_length } }); + json_["bufferViews"].push_back({ {"buffer", 0}, {"byteStride", 12}, { "byteOffset", indices_length }, { "byteLength", binary_length - indices_length } }); + json_["buffers"].push_back({ {"byteLength", binary_length} }); + + std::string json_contents = json_.dump(); + auto json_length = json_contents.size(); + + uint32_t header[] = { GLTF, 2U, 12 + 8 + json_length + padding_for(json_length) + 8 + 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); + { + std::ifstream ifs(IfcUtil::path::from_utf8(tmp_filename1_).c_str(), std::ios::binary); + fstream_ << ifs.rdbuf(); + } + { + std::ifstream ifs(IfcUtil::path::from_utf8(tmp_filename2_).c_str(), std::ios::binary); + fstream_ << ifs.rdbuf(); + } + write_padding(fstream_, binary_length); + + IfcUtil::path::delete_file(tmp_filename1_); + IfcUtil::path::delete_file(tmp_filename2_); +} + +#endif diff --git a/src/serializers/GltfSerializer.h b/src/serializers/GltfSerializer.h new file mode 100644 index 0000000000..34be625543 --- /dev/null +++ b/src/serializers/GltfSerializer.h @@ -0,0 +1,55 @@ +/******************************************************************************** + * * + * 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 . * + * * + ********************************************************************************/ + +#ifndef GLTFSERIALIZER_H +#define GLTFSERIALIZER_H + +#ifdef WITH_GLTF + +#include "../serializers/GeometrySerializer.h" + +#include +using json = nlohmann::json; + +#include + +class GltfSerializer : public GeometrySerializer { +private: + std::string filename_, tmp_filename1_, tmp_filename2_; + std::ofstream fstream_, tmp_fstream1_, tmp_fstream2_; + std::map materials_, meshes_; + json json_, node_array_; + + int writeMaterial(const IfcGeom::Material& style); +public: + GltfSerializer(const std::string& filename, const SerializerSettings& settings); + virtual ~GltfSerializer() {} + bool ready(); + void writeHeader(); + void write(const IfcGeom::TriangulationElement* o); + void write(const IfcGeom::BRepElement* /*o*/) {} + void finalize(); + bool isTesselated() const { return true; } + void setUnitNameAndMagnitude(const std::string& /*name*/, float /*magnitude*/) {} + void setFile(IfcParse::IfcFile*) {} +}; + +#endif + +#endif diff --git a/win/build-deps.cmd b/win/build-deps.cmd index b691332345..08a8e9c3d4 100644 --- a/win/build-deps.cmd +++ b/win/build-deps.cmd @@ -195,6 +195,11 @@ call .\b2 toolset=msvc%BOOST_VC_VER% runtime-link=static address-model=%ARCH_BIT variant=%DEBUG_OR_RELEASE_LOWERCASE% %BOOST_LIBS% stage --stagedir=stage/vs%VS_VER%-%VS_PLATFORM% IF NOT %ERRORLEVEL%==0 GOTO :Error +:JSON +set DEPENDENCY_NAME=JSON for Modern C++ v3.6.1 +IF NOT EXIST "%INSTALL_DIR%\json\nlohmann". mkdir "%INSTALL_DIR%\json\nlohmann" +call :DownloadFile https://github.com/nlohmann/json/releases/download/v3.6.1/json.hpp "%INSTALL_DIR%\json\nlohmann" json.hpp + :ICU set DEPENDENCY_NAME=ICU set DEPENDENCY_DIR=N/A diff --git a/win/run-cmake.bat b/win/run-cmake.bat index e46ad6ccd0..4251c81813 100755 --- a/win/run-cmake.bat +++ b/win/run-cmake.bat @@ -79,7 +79,7 @@ set PYTHON_LIBRARY=%PYTHONHOME%\libs\python%PY_VER_MAJOR_MINOR%.lib set PYTHON_EXECUTABLE=%PYTHONHOME%\python.exe set SWIG_DIR=%INSTALL_DIR%\swigwin set PATH=%PATH%;%SWIG_DIR%;%PYTHONHOME% -set THREEDS_MAX_SDK_HOME=C:\Program Files\Autodesk\3ds Max 2016 SDK\maxsdk +set JSON_INCLUDE_DIR=%INSTALL_DIR%\json echo. call cecho.cmd 0 10 "Script configuration:" @@ -102,7 +102,7 @@ echo PYTHON_INCLUDE_DIR = %PYTHON_INCLUDE_DIR% echo PYTHON_LIBRARY = %PYTHON_LIBRARY% echo PYTHON_EXECUTABLE = %PYTHON_EXECUTABLE% echo SWIG_DIR = %SWIG_DIR% -echo THREEDS_MAX_SDK_HOME = %THREEDS_MAX_SDK_HOME% +echo JSON_INCLUDE_DIR = %JSON_INCLUDE_DIR% echo. echo CMAKE_INSTALL_PREFIX = %CMAKE_INSTALL_PREFIX% echo.