mirror of
https://github.com/Nexoniarz/Fry3DX.git
synced 2026-11-03 12:34:27 +00:00
Combining with Manifold (exact; closed shapes): faces come back whole, flat neighbours from the two shapes joined; the BSP still for open shapes; libraries pinned static (BUILD_SHARED_LIBS off before any); tests for touching, flush and itself
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
5394eeb3d8
commit
b95e436d10
4 changed files with 141 additions and 1 deletions
|
|
@ -38,6 +38,9 @@ else()
|
||||||
set(FRY_CONTENT_DIR ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
|
set(FRY_CONTENT_DIR ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# Libraries are linked in, not .so files beside the program (SDL says for itself). Set before
|
||||||
|
# any of them, so one that has its own BUILD_SHARED_LIBS option (Manifold) can't turn it on.
|
||||||
|
set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build the libraries as shared ones" FORCE)
|
||||||
include(cmake/Dependencies.cmake)
|
include(cmake/Dependencies.cmake)
|
||||||
include(cmake/Shaders.cmake)
|
include(cmake/Shaders.cmake)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,22 @@
|
||||||
# Fry3DX: the app (app.cpp) on the engine, and its modelling core (mesh, scene) as a library the
|
# Fry3DX: the app (app.cpp) on the engine, and its modelling core (mesh, scene) as a library the
|
||||||
# tests use too.
|
# tests use too.
|
||||||
|
# Manifold (github.com/elalish/manifold): exact, robust combining of closed shapes. Just the
|
||||||
|
# library: no threads (TBB), no 2D, no bindings.
|
||||||
|
include(FetchContent)
|
||||||
|
set(MANIFOLD_PAR OFF CACHE BOOL "" FORCE)
|
||||||
|
set(MANIFOLD_CROSS_SECTION OFF CACHE BOOL "" FORCE)
|
||||||
|
set(MANIFOLD_TEST OFF CACHE BOOL "" FORCE)
|
||||||
|
set(MANIFOLD_PYBIND OFF CACHE BOOL "" FORCE)
|
||||||
|
set(MANIFOLD_CBIND OFF CACHE BOOL "" FORCE)
|
||||||
|
set(MANIFOLD_JSBIND OFF CACHE BOOL "" FORCE)
|
||||||
|
set(MANIFOLD_EXPORT OFF CACHE BOOL "" FORCE)
|
||||||
|
FetchContent_Declare(manifold GIT_REPOSITORY https://github.com/elalish/manifold.git
|
||||||
|
GIT_TAG v3.0.1 GIT_SHALLOW ON SYSTEM)
|
||||||
|
FetchContent_MakeAvailable(manifold)
|
||||||
|
|
||||||
add_library(fry_core STATIC mesh.cpp bvh.cpp text.cpp import.cpp tools.cpp cut.cpp uv.cpp image.cpp sticker.cpp shapes.cpp scene.cpp package.cpp export.cpp print.cpp boolean.cpp modify.cpp sculpt.cpp)
|
add_library(fry_core STATIC mesh.cpp bvh.cpp text.cpp import.cpp tools.cpp cut.cpp uv.cpp image.cpp sticker.cpp shapes.cpp scene.cpp package.cpp export.cpp print.cpp boolean.cpp modify.cpp sculpt.cpp)
|
||||||
target_include_directories(fry_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
target_include_directories(fry_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
target_link_libraries(fry_core PUBLIC glm::glm libzstd_static PRIVATE stb cgltf)
|
target_link_libraries(fry_core PUBLIC glm::glm libzstd_static PRIVATE stb cgltf manifold)
|
||||||
target_compile_definitions(fry_core PUBLIC GLM_ENABLE_EXPERIMENTAL)
|
target_compile_definitions(fry_core PUBLIC GLM_ENABLE_EXPERIMENTAL)
|
||||||
# Scripts (Lua) that build and change the scene: also without a window, for the tests.
|
# Scripts (Lua) that build and change the scene: also without a window, for the tests.
|
||||||
add_library(fry_script STATIC script.cpp)
|
add_library(fry_script STATIC script.cpp)
|
||||||
|
|
|
||||||
104
src/boolean.cpp
104
src/boolean.cpp
|
|
@ -3,10 +3,14 @@
|
||||||
// shared, points that land on another piece's edge set into it (so it stays closed all round).
|
// shared, points that land on another piece's edge set into it (so it stays closed all round).
|
||||||
#include "mesh.h"
|
#include "mesh.h"
|
||||||
|
|
||||||
|
#include <manifold/manifold.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
#include <set>
|
#include <set>
|
||||||
|
|
||||||
namespace fry {
|
namespace fry {
|
||||||
|
|
@ -330,9 +334,109 @@ Mesh combineOnce(const Mesh& am, const Mesh& b, Combine op) {
|
||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Manifold's way, for two closed shapes: exact, and doesn't go wrong where faces just touch.
|
||||||
|
// Each triangle carries the face it came from, so its faces come back whole (not triangles).
|
||||||
|
// Nothing if either isn't closed all round the way it needs (then the BSP's way).
|
||||||
|
std::optional<Mesh> combineManifold(const Mesh& a, const Mesh& b, Combine op) {
|
||||||
|
auto toManifold = [](const Mesh& m, uint32_t firstFace) -> std::optional<manifold::Manifold> {
|
||||||
|
manifold::MeshGL g;
|
||||||
|
g.numProp = 3;
|
||||||
|
g.vertProperties.reserve(m.verts.size() * 3);
|
||||||
|
for (const glm::vec3& v : m.verts) g.vertProperties.insert(g.vertProperties.end(), {v.x, v.y, v.z});
|
||||||
|
for (size_t f = 0; f < m.faces.size(); ++f) {
|
||||||
|
if (m.faces[f].size() < 3) return std::nullopt; // (a line: not a solid)
|
||||||
|
for (const auto& t : triangulate(m, int(f)))
|
||||||
|
g.triVerts.insert(g.triVerts.end(), {uint32_t(t[0]), uint32_t(t[1]), uint32_t(t[2])}), g.faceID.push_back(firstFace + uint32_t(f));
|
||||||
|
}
|
||||||
|
if (g.triVerts.empty()) return std::nullopt;
|
||||||
|
g.runOriginalID = {manifold::Manifold::ReserveIDs(1)}; // (so it keeps the faces given, not its own numbering)
|
||||||
|
g.runIndex = {0, uint32_t(g.triVerts.size())};
|
||||||
|
g.Merge(); // (points in the same place, kept apart by a seam: one)
|
||||||
|
manifold::Manifold out(g);
|
||||||
|
if (out.Status() != manifold::Manifold::Error::NoError) return std::nullopt;
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
const auto A = toManifold(a, 0), B = toManifold(b, uint32_t(a.faces.size()));
|
||||||
|
if (!A || !B) return std::nullopt;
|
||||||
|
const manifold::Manifold r = op == Combine::Add ? *A + *B : op == Combine::Subtract ? *A - *B : *A ^ *B;
|
||||||
|
if (r.Status() != manifold::Manifold::Error::NoError) return std::nullopt;
|
||||||
|
const manifold::MeshGL g = r.GetMeshGL();
|
||||||
|
Mesh out;
|
||||||
|
const size_t n = g.NumVert();
|
||||||
|
out.verts.reserve(n);
|
||||||
|
for (size_t i = 0; i < n; ++i) out.verts.push_back({g.vertProperties[i * g.numProp], g.vertProperties[i * g.numProp + 1], g.vertProperties[i * g.numProp + 2]});
|
||||||
|
// The triangles of each face, joined back into it: its outline, walked round (a face cut into
|
||||||
|
// two pieces, or with a hole in it: as it comes, triangles).
|
||||||
|
// A face of one shape lying flat against a face of the other, side by side (both cubes' tops
|
||||||
|
// in a row): one face. A shape's own faces stay as they were.
|
||||||
|
const size_t tris = g.triVerts.size() / 3;
|
||||||
|
const uint32_t split = uint32_t(a.faces.size());
|
||||||
|
std::vector<uint32_t> id(tris);
|
||||||
|
for (size_t t = 0; t < tris; ++t) id[t] = t < g.faceID.size() ? g.faceID[t] : uint32_t(0x80000000u + t);
|
||||||
|
std::map<uint32_t, uint32_t> parent;
|
||||||
|
std::function<uint32_t(uint32_t)> root = [&](uint32_t x) {
|
||||||
|
auto it = parent.find(x);
|
||||||
|
if (it == parent.end() || it->second == x) return x;
|
||||||
|
return it->second = root(it->second);
|
||||||
|
};
|
||||||
|
std::map<uint32_t, glm::vec4> plane; // (each face's: its normal and how far along it)
|
||||||
|
for (size_t t = 0; t < tris; ++t) {
|
||||||
|
if (plane.count(id[t])) continue;
|
||||||
|
const glm::vec3 p0 = out.verts[g.triVerts[t * 3]], p1 = out.verts[g.triVerts[t * 3 + 1]], p2 = out.verts[g.triVerts[t * 3 + 2]];
|
||||||
|
const glm::vec3 c = glm::cross(p1 - p0, p2 - p0);
|
||||||
|
if (glm::length(c) > 1e-12f) plane[id[t]] = glm::vec4(glm::normalize(c), glm::dot(glm::normalize(c), p0));
|
||||||
|
}
|
||||||
|
float size = 0;
|
||||||
|
for (const glm::vec3& v : out.verts) size = std::max(size, std::max(std::abs(v.x), std::max(std::abs(v.y), std::abs(v.z))));
|
||||||
|
std::map<std::pair<int, int>, size_t> edgeTri;
|
||||||
|
for (size_t t = 0; t < tris; ++t)
|
||||||
|
for (int k = 0; k < 3; ++k) edgeTri[{int(g.triVerts[t * 3 + size_t(k)]), int(g.triVerts[t * 3 + size_t((k + 1) % 3)])}] = t;
|
||||||
|
for (const auto& [e, t] : edgeTri) {
|
||||||
|
auto o = edgeTri.find({e.second, e.first});
|
||||||
|
if (o == edgeTri.end()) continue;
|
||||||
|
const uint32_t x = id[t], y = id[o->second];
|
||||||
|
if (x == y || (x < split) == (y < split) || !plane.count(x) || !plane.count(y)) continue;
|
||||||
|
const glm::vec4 px = plane[x], py = plane[y];
|
||||||
|
if (glm::dot(glm::vec3(px), glm::vec3(py)) > 1 - 1e-5f && std::abs(px.w - py.w) < 1e-5f * std::max(size, 1.0f)) {
|
||||||
|
const uint32_t rx = root(x), ry = root(y);
|
||||||
|
if (rx != ry) parent[rx] = ry, parent.emplace(ry, ry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::map<uint32_t, std::vector<std::array<int, 3>>> faces;
|
||||||
|
for (size_t t = 0; t < tris; ++t) faces[root(id[t])].push_back({int(g.triVerts[t * 3]), int(g.triVerts[t * 3 + 1]), int(g.triVerts[t * 3 + 2])});
|
||||||
|
for (const auto& [id, ts] : faces) {
|
||||||
|
std::set<std::pair<int, int>> inside;
|
||||||
|
for (const auto& t : ts)
|
||||||
|
for (int k = 0; k < 3; ++k) inside.insert({t[size_t(k)], t[size_t((k + 1) % 3)]});
|
||||||
|
std::map<int, int> next;
|
||||||
|
bool simple = true;
|
||||||
|
for (const auto& [p, q] : inside)
|
||||||
|
if (!inside.count({q, p})) simple &= next.emplace(p, q).second; // (an outline edge)
|
||||||
|
std::vector<int> loop;
|
||||||
|
if (simple && !next.empty()) {
|
||||||
|
int at = next.begin()->first;
|
||||||
|
do {
|
||||||
|
loop.push_back(at);
|
||||||
|
auto it = next.find(at);
|
||||||
|
if (it == next.end() || loop.size() > next.size()) break;
|
||||||
|
at = it->second;
|
||||||
|
} while (at != loop.front());
|
||||||
|
}
|
||||||
|
if (simple && loop.size() == next.size() && loop.size() >= 3) out.faces.push_back(loop);
|
||||||
|
else
|
||||||
|
for (const auto& t : ts) out.faces.push_back({t[0], t[1], t[2]});
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
bool combine(Mesh& a, const Mesh& b, Combine op) {
|
bool combine(Mesh& a, const Mesh& b, Combine op) {
|
||||||
|
if (auto m = combineManifold(a, b, op)) {
|
||||||
|
a = std::move(*m);
|
||||||
|
return !a.faces.empty();
|
||||||
|
}
|
||||||
Mesh out = combineOnce(a, b, op);
|
Mesh out = combineOnce(a, b, op);
|
||||||
// Closed going in but not coming out: the rounding went wrong somewhere. Again with the
|
// Closed going in but not coming out: the rounding went wrong somewhere. Again with the
|
||||||
// other shape moved a few µm (nothing that shows), and the first that comes out closed kept.
|
// other shape moved a few µm (nothing that shows), and the first that comes out closed kept.
|
||||||
|
|
|
||||||
|
|
@ -709,6 +709,25 @@ int main() {
|
||||||
CHECK(!held.empty() && std::abs(boundsMax(g).z - 0.8f) < 0.01f && closed(g), "grab pulls the middle up with it (%.3f)", boundsMax(g).z);
|
CHECK(!held.empty() && std::abs(boundsMax(g).z - 0.8f) < 0.01f && closed(g), "grab pulls the middle up with it (%.3f)", boundsMax(g).z);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{ // combining where faces lie on each other (the BSP's weak spot): touching, flush, the same
|
||||||
|
Mesh a = makeCube(1.0f), b = makeCube(1.0f);
|
||||||
|
transform(b, glm::translate(glm::mat4(1.0f), glm::vec3(1, 0, 0)));
|
||||||
|
bool ok = combine(a, b, Combine::Add);
|
||||||
|
CHECK(ok && checkForPrint(a).closed() && std::abs(checkForPrint(a).volume - 2.0f) < 1e-4f && a.faces.size() == 6, "two cubes side by side: one 2 m3 block, 6 faces (%zu)", a.faces.size());
|
||||||
|
Mesh c = makeCube(1.0f), d = makeCube(0.5f);
|
||||||
|
transform(d, glm::translate(glm::mat4(1.0f), glm::vec3(0, 0, 0.25f))); // (its top flush with the cube's)
|
||||||
|
ok = combine(c, d, Combine::Subtract);
|
||||||
|
CHECK(ok && checkForPrint(c).closed() && std::abs(checkForPrint(c).volume - 0.875f) < 1e-4f, "a pocket flush with the top: 0.875 m3, closed (%.4f)", checkForPrint(c).volume);
|
||||||
|
Mesh e = makeCube(1.0f);
|
||||||
|
CHECK(!combine(e, makeCube(1.0f), Combine::Subtract), "a cube take itself: nothing left");
|
||||||
|
Mesh s1 = makeSphere(32, 16, 0.5f), s2 = makeSphere(32, 16, 0.5f);
|
||||||
|
transform(s2, glm::translate(glm::mat4(1.0f), glm::vec3(0.3f, 0.1f, 0.05f)));
|
||||||
|
const auto t0 = std::chrono::steady_clock::now();
|
||||||
|
ok = combine(s1, s2, Combine::Subtract);
|
||||||
|
const double ms = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - t0).count();
|
||||||
|
CHECK(ok && checkForPrint(s1).closed(), "a sphere bitten by another: closed (%zu faces, %.0f ms)", s1.faces.size(), ms);
|
||||||
|
}
|
||||||
|
|
||||||
{ // editable cuts: a box with a bar through it, the bar still an object
|
{ // editable cuts: a box with a bar through it, the bar still an object
|
||||||
Scene sc;
|
Scene sc;
|
||||||
Object box, bar;
|
Object box, bar;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue