/******************************************************************************** * * * 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 . * * * ********************************************************************************/ #include "LodBuilder.h" #ifdef WITH_MESH_OPTIMIZER #include #endif #include #include #include #include #include void buildLods(SidecarData& sd, int min_triangles, float target_ratio, float target_error) { #ifndef WITH_MESH_OPTIMIZER (void)sd; (void)min_triangles; (void)target_ratio; (void)target_error; return; #else if (sd.meshes.empty() || sd.vertices.empty() || sd.indices.empty()) return; const size_t vtx_stride_bytes = INSTANCED_VERTEX_STRIDE_BYTES; const size_t total_vertex_count = sd.vertices.size() / vtx_stride_bytes; // Env var knobs so we can tune without rebuilding. // IFC_LOD_ERROR= override target_error (default 0.05 → 0.2). // IFC_LOD_RATIO= override target_ratio. // IFC_LOD_MIN_SAVINGS=<0..1> minimum fraction of tris saved to accept // (default 0.25). // IFC_LOD_DEBUG=1 print per-mesh diagnostics for the first // few meshes of each call. const char* env_err = std::getenv("IFC_LOD_ERROR"); const char* env_ratio = std::getenv("IFC_LOD_RATIO"); const char* env_savings = std::getenv("IFC_LOD_MIN_SAVINGS"); const char* env_debug = std::getenv("IFC_LOD_DEBUG"); if (env_err) target_error = static_cast(std::atof(env_err)); if (env_ratio) target_ratio = static_cast(std::atof(env_ratio)); float min_savings = 0.25f; if (env_savings) min_savings = static_cast(std::atof(env_savings)); const bool debug = env_debug && env_debug[0] == '1'; // Loosened defaults: BIM meshes are non-manifold; LockBorder ≈ zero // collapses. A 0.2 error budget still looks fine at sub-4px. if (target_error < 0.2f) target_error = 0.2f; // Scratch buffers reused across meshes so we only allocate once. std::vector simplified; std::vector dequant_pos; // 3 floats/vertex, dequantized simplified.reserve(1024); dequant_pos.reserve(1024 * 3); int dbg_printed = 0; int dbg_rejected_savings = 0; int dbg_rejected_noreduce = 0; int dbg_accepted = 0; for (auto& mesh : sd.meshes) { mesh.lod1_ebo_byte_offset = 0; mesh.lod1_index_count = 0; const uint32_t tri_count = mesh.index_count / 3; if (static_cast(tri_count) < min_triangles) continue; if (mesh.vertex_count == 0) continue; // meshopt wants a pointer to the *first position* and a vertex_count // equal to the number of referenced vertices (i.e. the absolute upper // bound on indices we might see). Indices in `sd.indices` for this // mesh are mesh-local (0..mesh.vertex_count). Pass the base-vertex // as an offset into sd.vertices so meshopt reads positions at the // right place. const uint32_t base_vertex = mesh.vbo_byte_offset / vtx_stride_bytes; if (base_vertex + mesh.vertex_count > total_vertex_count) continue; const uint32_t first_index = mesh.ebo_byte_offset / sizeof(uint32_t); if (first_index + mesh.index_count > sd.indices.size()) continue; // Dequantize positions for this mesh into a temp float array. // meshopt needs contiguous float3 positions with a known stride; // quantized bytes aren't directly usable. const uint8_t* quant_base = sd.vertices.data() + base_vertex * vtx_stride_bytes; dequant_pos.resize(static_cast(mesh.vertex_count) * 3); const float extent[3] = { mesh.local_aabb_max[0] - mesh.local_aabb_min[0], mesh.local_aabb_max[1] - mesh.local_aabb_min[1], mesh.local_aabb_max[2] - mesh.local_aabb_min[2], }; for (uint32_t v = 0; v < mesh.vertex_count; ++v) { const uint16_t* p = reinterpret_cast( quant_base + v * vtx_stride_bytes); for (int a = 0; a < 3; ++a) { float t = p[a] / 65535.0f; dequant_pos[v * 3 + a] = mesh.local_aabb_min[a] + t * extent[a]; } } const float* positions = dequant_pos.data(); const size_t local_pos_stride = sizeof(float) * 3; const uint32_t* indices = sd.indices.data() + first_index; const size_t target_index_count = std::max( 3, static_cast(mesh.index_count * target_ratio) / 3 * 3); // Cluster-based (sloppy) decimator. Ignores topology entirely; // ideal for BIM brep output which is usually non-manifold / has // T-junctions / per-triangle vertex duplication. Quantises // positions into voxel cells — no welding needed. simplified.resize(mesh.index_count); float result_error = 0.0f; size_t new_index_count = meshopt_simplifySloppy( simplified.data(), indices, mesh.index_count, positions, mesh.vertex_count, local_pos_stride, target_index_count, target_error, &result_error); if (debug && dbg_printed < 8) { std::fprintf(stderr, " [lod] mesh tris=%u target=%zu got=%zu err=%.4f\n", tri_count, target_index_count / 3, new_index_count / 3, result_error); ++dbg_printed; } // Accept only if we actually saved a meaningful chunk of tris. if (new_index_count == 0 || new_index_count >= mesh.index_count) { ++dbg_rejected_noreduce; continue; } const uint32_t saved = mesh.index_count - static_cast(new_index_count); if (static_cast(saved) < min_savings * static_cast(mesh.index_count)) { ++dbg_rejected_savings; continue; } ++dbg_accepted; // Append the surviving indices to sd.indices; record the offset. const size_t append_offset_bytes = sd.indices.size() * sizeof(uint32_t); sd.indices.insert(sd.indices.end(), simplified.begin(), simplified.begin() + new_index_count); mesh.lod1_ebo_byte_offset = static_cast(append_offset_bytes); mesh.lod1_index_count = static_cast(new_index_count); } if (debug) { std::fprintf(stderr, " [lod] summary: accepted=%d rejected_noreduce=%d rejected_savings=%d " "(target_error=%.3f target_ratio=%.3f min_savings=%.3f)\n", dbg_accepted, dbg_rejected_noreduce, dbg_rejected_savings, target_error, target_ratio, min_savings); } #endif } LodStats summariseLods(const SidecarData& sd) { LodStats s; s.meshes_total = static_cast(sd.meshes.size()); for (const auto& m : sd.meshes) { s.tris_lod0 += m.index_count / 3; if (m.lod1_index_count > 0) { ++s.meshes_with_lod1; s.tris_lod1 += m.lod1_index_count / 3; s.tris_lod0_for_lod1 += m.index_count / 3; } } return s; }