#include "print_remesh.h" #include #include #include #include #include #include #ifdef TRELLIS2_USE_CGAL #include #include #if __has_include() #include #include #define T2_CGAL_AABB_3_NAMES 1 #else #include #include #endif #include #include #include #endif namespace t2print { bool available() { #ifdef TRELLIS2_USE_CGAL return true; #else return false; #endif } namespace { void vertex_normals(const std::vector & verts, const std::vector & tris, std::vector & normals) { normals.assign(verts.size(), 0.0f); for (size_t t = 0; t + 2 < tris.size(); t += 3) { const int32_t ia = tris[t], ib = tris[t + 1], ic = tris[t + 2]; const float * a = verts.data() + (size_t) ia * 3; const float * b = verts.data() + (size_t) ib * 3; const float * c = verts.data() + (size_t) ic * 3; const float ab[3] = {b[0] - a[0], b[1] - a[1], b[2] - a[2]}; const float ac[3] = {c[0] - a[0], c[1] - a[1], c[2] - a[2]}; const float n[3] = { ab[1] * ac[2] - ab[2] * ac[1], ab[2] * ac[0] - ab[0] * ac[2], ab[0] * ac[1] - ab[1] * ac[0], }; for (int32_t i : {ia, ib, ic}) { normals[(size_t) i * 3 + 0] += n[0]; normals[(size_t) i * 3 + 1] += n[1]; normals[(size_t) i * 3 + 2] += n[2]; } } for (size_t i = 0; i < verts.size() / 3; ++i) { float * n = normals.data() + i * 3; const float len = std::sqrt(n[0] * n[0] + n[1] * n[1] + n[2] * n[2]); if (len > 1e-20f) { n[0] /= len; n[1] /= len; n[2] /= len; } } } } // namespace bool alpha_wrap(const std::vector & source_verts, const std::vector & source_tris, float alpha_ratio, float offset_ratio, std::vector & out_verts, std::vector & out_normals, std::vector & out_tris, std::string & err) { out_verts.clear(); out_normals.clear(); out_tris.clear(); #ifndef TRELLIS2_USE_CGAL (void) source_verts; (void) source_tris; (void) alpha_ratio; (void) offset_ratio; err = "print remeshing is unavailable (rebuild with CGAL >= 5.5)"; return false; #else if (source_verts.size() < 9 || source_tris.size() < 3) { err = "empty mesh"; return false; } if (!std::isfinite(alpha_ratio) || !std::isfinite(offset_ratio) || alpha_ratio <= 0.0f || alpha_ratio > 0.5f || offset_ratio <= 0.0f || offset_ratio > 0.5f) { err = "bad Alpha Wrap parameters"; return false; } using Kernel = CGAL::Exact_predicates_inexact_constructions_kernel; using Point = Kernel::Point_3; using Mesh = CGAL::Surface_mesh; try { std::vector points; points.reserve(source_verts.size() / 3); double lo[3] = {1e300, 1e300, 1e300}; double hi[3] = {-1e300, -1e300, -1e300}; for (size_t i = 0; i < source_verts.size() / 3; ++i) { double p[3] = {source_verts[3*i], source_verts[3*i+1], source_verts[3*i+2]}; if (!std::isfinite(p[0]) || !std::isfinite(p[1]) || !std::isfinite(p[2])) { err = "mesh contains a non-finite vertex"; return false; } points.emplace_back(p[0], p[1], p[2]); for (int k = 0; k < 3; ++k) { lo[k] = std::min(lo[k], p[k]); hi[k] = std::max(hi[k], p[k]); } } std::vector> faces; faces.reserve(source_tris.size() / 3); for (size_t t = 0; t < source_tris.size() / 3; ++t) { const int32_t a = source_tris[3*t], b = source_tris[3*t+1], c = source_tris[3*t+2]; if (a < 0 || b < 0 || c < 0 || (size_t) a >= points.size() || (size_t) b >= points.size() || (size_t) c >= points.size()) { err = "triangle index out of range"; return false; } if (a == b || b == c || a == c) continue; faces.push_back({{(size_t) a, (size_t) b, (size_t) c}}); } if (faces.empty()) { err = "mesh has no valid triangles"; return false; } const double dx = hi[0] - lo[0], dy = hi[1] - lo[1], dz = hi[2] - lo[2]; const double diagonal = std::sqrt(dx*dx + dy*dy + dz*dz); if (!(diagonal > 0.0) || !std::isfinite(diagonal)) { err = "mesh has an empty bounding box"; return false; } Mesh wrap; CGAL::alpha_wrap_3(points, faces, diagonal * (double) alpha_ratio, diagonal * (double) offset_ratio, wrap); if (wrap.is_empty() || wrap.number_of_faces() == 0) { err = "CGAL Alpha Wrap produced an empty mesh"; return false; } // Surface_mesh descriptors are indices but are not required to be // densely packed, so retain an explicit descriptor-to-output remap. std::vector remap; out_verts.reserve(wrap.number_of_vertices() * 3); for (Mesh::Vertex_index v : wrap.vertices()) { if ((size_t) v.idx() >= remap.size()) remap.resize((size_t) v.idx() + 1, -1); remap[v.idx()] = (int32_t) (out_verts.size() / 3); const Point & p = wrap.point(v); out_verts.push_back((float) CGAL::to_double(p.x())); out_verts.push_back((float) CGAL::to_double(p.y())); out_verts.push_back((float) CGAL::to_double(p.z())); } out_tris.reserve(wrap.number_of_faces() * 3); for (Mesh::Face_index f : wrap.faces()) { Mesh::Halfedge_index h = wrap.halfedge(f); for (int k = 0; k < 3; ++k) { Mesh::Vertex_index v = wrap.target(h); if ((size_t) v.idx() >= remap.size() || remap[v.idx()] < 0) { err = "CGAL Alpha Wrap returned an invalid face"; return false; } out_tris.push_back(remap[v.idx()]); h = wrap.next(h); } if (h != wrap.halfedge(f)) { err = "CGAL Alpha Wrap returned a non-triangle face"; return false; } } vertex_normals(out_verts, out_tris, out_normals); return true; } catch (const std::exception & ex) { err = std::string("CGAL Alpha Wrap failed: ") + ex.what(); return false; } catch (...) { err = "CGAL Alpha Wrap failed"; return false; } #endif } bool project_pbr(const std::vector & source_verts, const std::vector & source_tris, const std::vector & source_pbr, const std::vector & query_points, std::vector & out_pbr, std::string & err) { out_pbr.clear(); #ifndef TRELLIS2_USE_CGAL (void) source_verts; (void) source_tris; (void) source_pbr; (void) query_points; err = "PBR projection is unavailable (rebuild with CGAL >= 5.5)"; return false; #else using Kernel = CGAL::Exact_predicates_inexact_constructions_kernel; using Point = Kernel::Point_3; using Triangle = Kernel::Triangle_3; using Triangle_iterator = std::vector::const_iterator; #ifdef T2_CGAL_AABB_3_NAMES using Primitive = CGAL::AABB_triangle_primitive_3; using Traits = CGAL::AABB_traits_3; #else // These compatibility names are used by CGAL 5.5, the first Alpha Wrap // release. CGAL 6 selects the non-deprecated aliases above. using Primitive = CGAL::AABB_triangle_primitive; using Traits = CGAL::AABB_traits; #endif using Tree = CGAL::AABB_tree; const size_t source_nv = source_verts.size() / 3; if (source_verts.size() < 9 || source_verts.size() % 3 != 0 || source_tris.size() < 3 || source_tris.size() % 3 != 0) { err = "empty PBR projection source"; return false; } if (source_pbr.size() != source_nv * 6) { err = "PBR projection source has no six-channel material"; return false; } if (query_points.size() % 3 != 0) { err = "PBR projection query array is not xyz-aligned"; return false; } if (query_points.empty()) return true; try { std::vector triangles; std::vector> source_faces; triangles.reserve(source_tris.size() / 3); source_faces.reserve(source_tris.size() / 3); for (size_t t = 0; t < source_tris.size() / 3; ++t) { const int32_t ia = source_tris[3*t], ib = source_tris[3*t+1], ic = source_tris[3*t+2]; if (ia < 0 || ib < 0 || ic < 0 || (size_t) ia >= source_nv || (size_t) ib >= source_nv || (size_t) ic >= source_nv) { err = "PBR projection triangle index out of range"; return false; } const Point a(source_verts[3*(size_t)ia], source_verts[3*(size_t)ia+1], source_verts[3*(size_t)ia+2]); const Point b(source_verts[3*(size_t)ib], source_verts[3*(size_t)ib+1], source_verts[3*(size_t)ib+2]); const Point c(source_verts[3*(size_t)ic], source_verts[3*(size_t)ic+1], source_verts[3*(size_t)ic+2]); Triangle tri(a, b, c); // CGAL explicitly disallows degenerate primitives in an AABB tree. if (tri.is_degenerate()) continue; triangles.push_back(tri); source_faces.push_back({{ia, ib, ic}}); } if (triangles.empty()) { err = "PBR projection source has no non-degenerate triangles"; return false; } Tree tree(triangles.cbegin(), triangles.cend()); tree.build(); tree.accelerate_distance_queries(); out_pbr.resize((query_points.size() / 3) * 6); auto sample_one = [&](size_t qi) { const Point query(query_points[3*qi], query_points[3*qi+1], query_points[3*qi+2]); const auto hit = tree.closest_point_and_primitive(query); const size_t ti = (size_t) std::distance(triangles.cbegin(), hit.second); const auto & ids = source_faces[ti]; const Point & a = triangles[ti].vertex(0); const Point & b = triangles[ti].vertex(1); const Point & c = triangles[ti].vertex(2); const Point & q = hit.first; const double ab[3] = { CGAL::to_double(b.x()-a.x()), CGAL::to_double(b.y()-a.y()), CGAL::to_double(b.z()-a.z())}; const double ac[3] = { CGAL::to_double(c.x()-a.x()), CGAL::to_double(c.y()-a.y()), CGAL::to_double(c.z()-a.z())}; const double aq[3] = { CGAL::to_double(q.x()-a.x()), CGAL::to_double(q.y()-a.y()), CGAL::to_double(q.z()-a.z())}; const double d00 = ab[0]*ab[0] + ab[1]*ab[1] + ab[2]*ab[2]; const double d01 = ab[0]*ac[0] + ab[1]*ac[1] + ab[2]*ac[2]; const double d11 = ac[0]*ac[0] + ac[1]*ac[1] + ac[2]*ac[2]; const double d20 = aq[0]*ab[0] + aq[1]*ab[1] + aq[2]*ab[2]; const double d21 = aq[0]*ac[0] + aq[1]*ac[1] + aq[2]*ac[2]; const double denom = d00*d11 - d01*d01; double wb = (d11*d20 - d01*d21) / denom; double wc = (d00*d21 - d01*d20) / denom; double wa = 1.0 - wb - wc; // The closest point is on the triangle. Clamp only numerical noise // so interpolation remains stable on edges and vertices. wa = std::max(0.0, std::min(1.0, wa)); wb = std::max(0.0, std::min(1.0, wb)); wc = std::max(0.0, std::min(1.0, wc)); const double sum = wa + wb + wc; wa /= sum; wb /= sum; wc /= sum; for (int ch = 0; ch < 6; ++ch) { out_pbr[6*qi + (size_t)ch] = (float) ( wa * source_pbr[6*(size_t)ids[0] + (size_t)ch] + wb * source_pbr[6*(size_t)ids[1] + (size_t)ch] + wc * source_pbr[6*(size_t)ids[2] + (size_t)ch]); } }; const size_t nq = query_points.size() / 3; #ifdef CGAL_HAS_THREADS const unsigned hw = std::max(1u, std::thread::hardware_concurrency()); const unsigned workers = (unsigned) std::min(std::min(16u, hw), (nq + 4095) / 4096); std::atomic next{0}; std::atomic failed{false}; std::mutex failure_mu; std::string failure; auto worker = [&]() { try { for (;;) { const size_t begin = next.fetch_add(4096); if (begin >= nq || failed.load()) break; const size_t end = std::min(nq, begin + 4096); for (size_t qi = begin; qi < end; ++qi) sample_one(qi); } } catch (const std::exception & ex) { failed.store(true); std::lock_guard lock(failure_mu); if (failure.empty()) failure = ex.what(); } catch (...) { failed.store(true); } }; std::vector threads; threads.reserve(workers); for (unsigned i = 0; i < workers; ++i) threads.emplace_back(worker); for (auto & thread : threads) thread.join(); if (failed.load()) { err = failure.empty() ? "CGAL PBR projection failed" : std::string("CGAL PBR projection failed: ") + failure; out_pbr.clear(); return false; } #else for (size_t qi = 0; qi < nq; ++qi) sample_one(qi); #endif return true; } catch (const std::exception & ex) { err = std::string("CGAL PBR projection failed: ") + ex.what(); out_pbr.clear(); return false; } catch (...) { err = "CGAL PBR projection failed"; out_pbr.clear(); return false; } #endif } } // namespace t2print