#include "fiko/sahne/fark_hesap.hpp" #include #include #include #include #include #include #include "fiko/core/mat/aabb.hpp" #include "fiko/sahne/isin_bvh.hpp" namespace fiko { f64& farkOtoToleransCarpan() { static f64 c = 0.001; // AyarSistemi "fark.oto_tolerans" (köşegen × bu) return c; } bool& farkGpuKullan() { static bool b = true; // AyarSistemi "fark.gpu_kullan" return b; } f32& farkSkalaYuzdelik() { static f32 y = 0.95f; // AyarSistemi "fark.skala_yuzdelik" — renk skalası ucu yüzdeliği return y; } int& farkBolgeMinVertex() { static int k = 8; // AyarSistemi "fark.bolge_min_vertex" return k; } namespace { // Mesh'i dünya uzayına kopyala (tek seferlik O(N) — sıcak yol değil). BVH ve // sorgular dünya uzayında koşar → iki modelin dönüşümleri doğal hizalanır. std::vector dunyaKonum(const FarkGirdi& g) { std::vector dk(g.konumSayi); for (size_t i = 0; i < g.konumSayi; ++i) { const Vec3d w = g.donusum.noktaUygula( {(f64)g.konum[i].x, (f64)g.konum[i].y, (f64)g.konum[i].z}); dk[i] = {(f32)w.x, (f32)w.y, (f32)w.z}; } return dk; } // Sorgu vertexlerinin hedef mesh'e (BVH) mesafeleri — chunk'lı çok çekirdek. // Maks sapmanın vertex indeksi ve yüzeydeki karşı noktası da toplanır. struct YonSonuc { f32 maks = 0; f64 toplam = 0, toplamKare = 0; size_t maksIdx = 0; Vec3f maksKarsi{}; }; YonSonuc mesafeleriHesapla(const std::vector& sorgular, const IsinBvh& bvh, const Vec3f* hedefKonum, size_t hedefKonumSayi, const u32* hedefIndeks, size_t hedefIndeksSayi, std::vector& cikti, const std::atomic* dur, VulkanBaglam* vb, bool& gpuKullanildi) { const size_t n = sorgular.size(); // GPU yolu: İŞARETLİ mesafe (+ dışa/mavi, − içe/kırmızı) tek dispatch'te. // İstatistik magnitude (abs) üzerinden; maks-karşı-nokta tek CPU sorgusu (ucuz). if (vb && farkGpuKullan() && farkGpuMesafeKare(*vb, bvh, hedefKonum, hedefKonumSayi, hedefIndeks, hedefIndeksSayi, sorgular.data(), n, cikti)) { gpuKullanildi = true; YonSonuc ys; for (size_t i = 0; i < n; ++i) { const f32 s = cikti[i]; // GPU işaretli mesafe döndürür const f32 m = std::abs(s); ys.toplam += m; ys.toplamKare += (f64)m * m; if (m > ys.maks) { ys.maks = m; ys.maksIdx = i; } } if (n) bvhEnYakinMesafeKare(bvh, hedefKonum, hedefIndeks, sorgular[ys.maksIdx], &ys.maksKarsi); return ys; } cikti.resize(n); const u32 donanim = std::max(1u, std::thread::hardware_concurrency()); const u32 isciSayi = n >= 4096 ? std::min(donanim, 32u) : 1u; std::vector yerel(isciSayi); const auto calis = [&](u32 w, size_t bas, size_t son) { YonSonuc& ys = yerel[w]; for (size_t i = bas; i < son; ++i) { if ((i & 0xFFF) == 0 && dur && dur->load(std::memory_order_relaxed)) return; Vec3f karsi, normal; const f32 mk = bvhEnYakinMesafeKare(bvh, hedefKonum, hedefIndeks, sorgular[i], &karsi, &normal); const f32 mag = mk < 1e30f ? std::sqrt(mk) : 0.0f; // İşaret: sorgu noktası hedef yüzeyin DIŞINDA mı (dot ≥ 0 → +, fazla malzeme) // yoksa İÇİNDE mi (− , eksik malzeme). Yeşil bandı tolerans belirler (render). const f32 isaret = (sorgular[i] - karsi).nokta(normal) >= 0.0f ? 1.0f : -1.0f; cikti[i] = mag * isaret; // İŞARETLİ sapma ys.toplam += mag; ys.toplamKare += (f64)mag * mag; if (mag > ys.maks) { ys.maks = mag; ys.maksIdx = i; ys.maksKarsi = karsi; } } }; if (isciSayi <= 1) { calis(0, 0, n); } else { std::vector isciler; isciler.reserve(isciSayi); const size_t adim = (n + isciSayi - 1) / isciSayi; for (u32 w = 0; w < isciSayi; ++w) isciler.emplace_back(calis, w, (size_t)w * adim, std::min(n, (size_t)(w + 1) * adim)); for (auto& t : isciler) t.join(); } YonSonuc birlesik; for (const YonSonuc& ys : yerel) { birlesik.toplam += ys.toplam; birlesik.toplamKare += ys.toplamKare; if (ys.maks > birlesik.maks) { birlesik.maks = ys.maks; birlesik.maksIdx = ys.maksIdx; birlesik.maksKarsi = ys.maksKarsi; } } return birlesik; } // Tolerans-üstü vertexleri üçgen komşuluğuyla bağlantılı bölgelere ayır (union-find, // yol sıkıştırma). Bölge = AABB merkez/yarıçap + maks sapma + vertex sayısı. void bolgeleriKumele(const std::vector& dunya, const u32* indeks, size_t indeksSayi, const std::vector& sapma, f32 tol, bool eklenen, std::vector& cikti) { const size_t n = dunya.size(); if (n == 0 || sapma.size() != n) return; std::vector ebeveyn(n); for (size_t i = 0; i < n; ++i) ebeveyn[i] = (u32)i; const auto kok = [&](u32 v) { while (ebeveyn[v] != v) { ebeveyn[v] = ebeveyn[ebeveyn[v]]; v = ebeveyn[v]; } return v; }; const auto birlestir = [&](u32 a, u32 b) { const u32 ka = kok(a), kb = kok(b); if (ka != kb) ebeveyn[ka] = kb; }; // Üçgenin tolerans-üstü vertexlerini birbirine bağla (kısmi değişen üçgen sınırı // bölgeyi bölmez: yalnız değişen köşeler kümelenir). // sapma İŞARETLİ (± ) → değişim testi MUTLAK DEĞER (|sapma| > tol). for (size_t i = 0; i + 2 < indeksSayi; i += 3) { const u32 a = indeks[i], b = indeks[i + 1], c = indeks[i + 2]; const bool ta = std::abs(sapma[a]) > tol, tb = std::abs(sapma[b]) > tol, tc = std::abs(sapma[c]) > tol; if (ta && tb) birlestir(a, b); if (tb && tc) birlestir(b, c); if (ta && tc) birlestir(a, c); } struct Toplayici { Aabbf kutu; f32 maks = 0; u32 sayi = 0; }; std::unordered_map kumeler; for (size_t i = 0; i < n; ++i) { if (std::abs(sapma[i]) <= tol) continue; Toplayici& t = kumeler[kok((u32)i)]; t.kutu.genislet(dunya[i]); t.maks = std::max(t.maks, std::abs(sapma[i])); ++t.sayi; } // Yüz-sınırı onarımı: OCCT üçgenlemesi yüzler ARASI vertex paylaşmaz → tek // fiziksel değişim bölgesi yüz başına parçalara bölünür. AABB'si (tolerans // payıyla) kesişen parçalar ikinci union-find'la birleştirilir. O(P²) — parça // sayısı taşarsa atlanır (bölgeler parçalı kalır; liste yine doğru sıralı). std::vector parcalar; parcalar.reserve(kumeler.size()); for (auto& [k, t] : kumeler) parcalar.push_back(t); const u32 P = (u32)parcalar.size(); std::vector pk(P); for (u32 i = 0; i < P; ++i) pk[i] = i; const auto pkok = [&](u32 v) { while (pk[v] != v) { pk[v] = pk[pk[v]]; v = pk[v]; } return v; }; if (P > 1 && P <= 4096) { const f32 pay = std::max(tol, 1e-4f); const auto kesisir = [&](const Aabbf& a, const Aabbf& b) { return a.asgari.x - pay <= b.azami.x && b.asgari.x - pay <= a.azami.x && a.asgari.y - pay <= b.azami.y && b.asgari.y - pay <= a.azami.y && a.asgari.z - pay <= b.azami.z && b.asgari.z - pay <= a.azami.z; }; for (u32 i = 0; i < P; ++i) for (u32 j = i + 1; j < P; ++j) if (pkok(i) != pkok(j) && kesisir(parcalar[i].kutu, parcalar[j].kutu)) pk[pkok(i)] = pkok(j); } std::unordered_map birlesik; for (u32 i = 0; i < P; ++i) { Toplayici& t = birlesik[pkok(i)]; t.kutu.genislet(parcalar[i].kutu.asgari); t.kutu.genislet(parcalar[i].kutu.azami); t.maks = std::max(t.maks, parcalar[i].maks); t.sayi += parcalar[i].sayi; } const u32 minV = (u32)std::max(1, farkBolgeMinVertex()); for (const auto& [k, t] : birlesik) { if (t.sayi < minV) continue; // gürültü FarkBolge b; b.merkez = (t.kutu.asgari + t.kutu.azami) * 0.5f; b.boyut = t.kutu.azami - t.kutu.asgari; b.yaricap = std::max(b.boyut.uzunluk() * 0.5f, 1e-3f); b.maksSapma = t.maks; b.vertexSayi = t.sayi; b.eklenen = eklenen; // Tip sezgisi: kenar uzunluklarını sırala (mx≥mid≥mn). Uzun (mx≫mn) → kenar/radius; // düz (mn≪mx ama mid büyük) → yüzey; kompakt → çıkıntı/oyuk. f32 e[3] = {std::abs(b.boyut.x), std::abs(b.boyut.y), std::abs(b.boyut.z)}; std::sort(e, e + 3); // e[0]=mn e[1]=mid e[2]=mx const f32 mx = std::max(e[2], 1e-6f); if (e[0] / mx < 0.18f && e[1] / mx < 0.35f) b.tip = BolgeTip::Uzun; // ince uzun çubuk else if (e[0] / mx < 0.22f) b.tip = BolgeTip::Duz; // ince yassı levha else b.tip = BolgeTip::Kompakt; // hacimsel cikti.push_back(b); } } } // namespace const char* bolgeTipAdi(const FarkBolge& b) { switch (b.tip) { case BolgeTip::Uzun: return b.eklenen ? "Kenar/kabartı" : "Kenar/radius"; case BolgeTip::Duz: return b.eklenen ? "Yüzey kabarması" : "Yüzey çöküğü"; default: return b.eklenen ? "Çıkıntı/ekleme" : "Oyuk/delik"; } } FarkSonuc farkHesapla(const FarkGirdi& eski, const FarkGirdi& yeni, const FarkSecenek& s, VulkanBaglam* vb) { FarkSonuc sonuc; if (!eski.konum || !yeni.konum || eski.indeksSayi < 3 || yeni.indeksSayi < 3) return sonuc; const auto t0 = std::chrono::steady_clock::now(); std::vector eskiDunya = dunyaKonum(eski); std::vector yeniDunya = dunyaKonum(yeni); // Yerel eksen: her mesh kendi centroid'ine ötelenir (konum farkını yok say — // sahnede yan yana duran iki sürüm hizalanıp karşılaştırılır). if (s.yerelEksen) { const auto merkeze = [](std::vector& v) { if (v.empty()) return; Vec3d top{0, 0, 0}; for (const Vec3f& p : v) { top.x += p.x; top.y += p.y; top.z += p.z; } const Vec3f m{(f32)(top.x / (f64)v.size()), (f32)(top.y / (f64)v.size()), (f32)(top.z / (f64)v.size())}; for (Vec3f& p : v) p = p - m; }; merkeze(eskiDunya); merkeze(yeniDunya); } // Otomatik tolerans: birleşik bbox köşegeni × çarpan (model ölçeğinden bağımsız). Aabbf kutu; for (const Vec3f& p : eskiDunya) kutu.genislet(p); for (const Vec3f& p : yeniDunya) kutu.genislet(p); const f32 kosegen = (kutu.azami - kutu.asgari).uzunluk(); sonuc.tolerans = s.degisimTolerans > 0.0f ? s.degisimTolerans : kosegen * (f32)farkOtoToleransCarpan(); // İki BVH paralel kurulur (isinBvhKur zaten büyük mesh'te çok çekirdekli). auto eskiBvhIs = std::async(std::launch::async, [&] { return isinBvhKur(eskiDunya.data(), eskiDunya.size(), eski.indeks, eski.indeksSayi, s.dur); }); auto yeniBvh = isinBvhKur(yeniDunya.data(), yeniDunya.size(), yeni.indeks, yeni.indeksSayi, s.dur); auto eskiBvh = eskiBvhIs.get(); if (!eskiBvh || !yeniBvh) return sonuc; // iptal/dejenere if (s.dur && s.dur->load(std::memory_order_relaxed)) return sonuc; // Yeni model vertexleri → eski yüzey (ısı haritası + eklenen bölge). const YonSonuc yeniYon = mesafeleriHesapla( yeniDunya, *eskiBvh, eskiDunya.data(), eskiDunya.size(), eski.indeks, eski.indeksSayi, sonuc.sapmaYeni, s.dur, vb, sonuc.gpuKullanildi); if (s.dur && s.dur->load(std::memory_order_relaxed)) return sonuc; YonSonuc eskiYon; if (s.simetrik) { // Eski model vertexleri → yeni yüzey (silinen bölge + tam Hausdorff). eskiYon = mesafeleriHesapla(eskiDunya, *yeniBvh, yeniDunya.data(), yeniDunya.size(), yeni.indeks, yeni.indeksSayi, sonuc.sapmaEski, s.dur, vb, sonuc.gpuKullanildi); if (s.dur && s.dur->load(std::memory_order_relaxed)) return sonuc; } // İstatistik (iki yön birleşik; Hausdorff = yönlerin maks'ı). const size_t nYeni = yeniDunya.size(); const size_t nEski = s.simetrik ? eskiDunya.size() : 0; const size_t nToplam = nYeni + nEski; sonuc.maksSapma = std::max(yeniYon.maks, eskiYon.maks); sonuc.ortSapma = nToplam ? (f32)((yeniYon.toplam + eskiYon.toplam) / (f64)nToplam) : 0.0f; sonuc.rmsSapma = nToplam ? (f32)std::sqrt((yeniYon.toplamKare + eskiYon.toplamKare) / (f64)nToplam) : 0.0f; if (yeniYon.maks >= eskiYon.maks) { sonuc.maksNokta = yeniDunya[yeniYon.maksIdx]; sonuc.maksKarsi = yeniYon.maksKarsi; } else { sonuc.maksNokta = eskiDunya[eskiYon.maksIdx]; sonuc.maksKarsi = eskiYon.maksKarsi; } // P99 + standart sapma (web CadDiff istatistik) + renk skalası yüzdeliği (P95 vb.). // Yüzdelikler birleşik sapma dizisinden nth_element (kopya üstünde — girişler bozulmaz). if (nToplam) { std::vector tum; tum.reserve(nToplam); for (const f32 v : sonuc.sapmaYeni) tum.push_back(std::abs(v)); // İŞARETLİ → magnitude if (s.simetrik) for (const f32 v : sonuc.sapmaEski) tum.push_back(std::abs(v)); const auto yuzdelik = [&](f64 q) -> f32 { const size_t i = (size_t)((f64)(tum.size() - 1) * std::clamp(q, 0.0, 1.0)); std::nth_element(tum.begin(), tum.begin() + i, tum.end()); return tum[i]; }; sonuc.p99Sapma = yuzdelik(0.99); // Renk skalası ucu: AyarSistemi yüzdeliği (varsayılan P95 — aykırıları yok say, // dolgun/dengeli skala; P99 aykırı-dominant). Kullanıcı slider ile ayrıca değiştirir. sonuc.skalaSapma = yuzdelik((f64)farkSkalaYuzdelik()); const f64 varyans = std::max(0.0, (f64)sonuc.rmsSapma * sonuc.rmsSapma - (f64)sonuc.ortSapma * sonuc.ortSapma); sonuc.stdSapma = (f32)std::sqrt(varyans); } // Histogram: [0, maksSapma] eşit kovalı, iki yön birleşik. const u32 kova = std::max(1u, s.histogramKova); sonuc.histogram.assign(kova, 0); sonuc.histogramAdim = sonuc.maksSapma > 0.0f ? sonuc.maksSapma / (f32)kova : 1.0f; const auto histogramEkle = [&](const std::vector& sapmalar) { for (const f32 v : sapmalar) { const u32 k = std::min((u32)(std::abs(v) / sonuc.histogramAdim), kova - 1); ++sonuc.histogram[k]; } }; histogramEkle(sonuc.sapmaYeni); if (s.simetrik) histogramEkle(sonuc.sapmaEski); // Eklenen/silinen: |sapma| toleransın üstünde olan (değişen) vertexler. for (const f32 v : sonuc.sapmaYeni) if (std::abs(v) > sonuc.tolerans) ++sonuc.eklenenVertex; for (const f32 v : sonuc.sapmaEski) if (std::abs(v) > sonuc.tolerans) ++sonuc.silinenVertex; // Değişim bölgeleri (sinematik tur hedefleri): iki mesh ayrı kümelenir, // birleşik liste maks sapmaya göre azalan sıralanır (tur en çarpıcıdan başlar). bolgeleriKumele(yeniDunya, yeni.indeks, yeni.indeksSayi, sonuc.sapmaYeni, sonuc.tolerans, true, sonuc.bolgeler); if (s.simetrik) bolgeleriKumele(eskiDunya, eski.indeks, eski.indeksSayi, sonuc.sapmaEski, sonuc.tolerans, false, sonuc.bolgeler); std::sort(sonuc.bolgeler.begin(), sonuc.bolgeler.end(), [](const FarkBolge& a, const FarkBolge& b) { return a.maksSapma > b.maksSapma; }); sonuc.sureMs = std::chrono::duration( std::chrono::steady_clock::now() - t0).count(); sonuc.gecerli = true; return sonuc; } } // namespace fiko