3D-Kanten: falsche Diagonalen auf doppelseitigen Meshes (Dach/Glas) behoben
Kontext-Meshes (Dach, Fenster-Glas/-Rahmen, künftig swissBUILDINGS3D-Import) werden von append_context_mesh IMMER doppelseitig aufgebaut (jedes Dreieck + gespiegelte Rückseite mit invertierter Normale), weil die Mesh-Pipeline Backface-Culling aktiv hat. Die Kanten-Erkennung sah dadurch an JEDER Kante ein exakt entgegengesetztes Normalen-Paar (Vorder-/Rückseite desselben Dreiecks) und wertete das fälschlich als Knick — jede Flächen-Innendiagonale eines doppelseitigen Meshes wurde gezeichnet (Nutzer-Report: sichtbare Dreiecks-Diagonalen auf Dach und Fensterglas im "Schattiert mit Kanten"-Modus). Fix: Rückseiten-Duplikate (Normalen-Paar mit dot ≈ -1) werden vor der Rand-/ Knick-Entscheidung zusammengeführt (ein Vertreter je Original-Dreieck) — danach gilt dieselbe Logik wie bei einseitigen Meshes (Wände), unabhängig davon ob doppelseitig gerendert wurde. +5 Tests (doppelseitige Varianten der bestehenden Fälle), 88/88 grün mit --features render.
This commit is contained in:
@@ -159,15 +159,45 @@ pub fn build_mesh_edges(mesh: &Mesh) -> Vec<f32> {
|
|||||||
/// Entscheidet, ob eine Kante gezeichnet wird: Randkante (nur ein Dreieck) oder
|
/// Entscheidet, ob eine Kante gezeichnet wird: Randkante (nur ein Dreieck) oder
|
||||||
/// Knickkante (zwei angrenzende Dreiecke stehen ueber dem Crease-Winkel zuein-
|
/// Knickkante (zwei angrenzende Dreiecke stehen ueber dem Crease-Winkel zuein-
|
||||||
/// ander). Koplanare geteilte Kanten (Flaechendiagonalen) werden unterdrueckt.
|
/// ander). Koplanare geteilte Kanten (Flaechendiagonalen) werden unterdrueckt.
|
||||||
|
///
|
||||||
|
/// DOPPELSEITIGE MESHES (Kontext/Dach/Fenster-Glas, s. `mesh.rs::push_ctx_tri` —
|
||||||
|
/// jedes Dreieck wird dort MIT gespiegelter Rueckseite [-Normale] dupliziert,
|
||||||
|
/// weil die Mesh-Pipeline Backface-Culling aktiv hat): jedes Original-Dreieck
|
||||||
|
/// liefert dadurch IMMER ein exaktes +n/-n-Paar an jeder seiner Kanten. Ohne
|
||||||
|
/// Beruecksichtigung wuerde das faelschlich als extremer Knick (dot ≈ -1)
|
||||||
|
/// gewertet und JEDE Flaechen-Innendiagonale eines doppelseitigen Meshes
|
||||||
|
/// gezeichnet (Nutzer-Report: Dreiecks-Diagonalen sichtbar auf Dach/Glas).
|
||||||
|
/// Fix: zuerst Rueckseiten-Partner (dot ≈ -1 zueinander) einander zuordnen und
|
||||||
|
/// nur EINEN Vertreter je Original-Dreieck behalten — danach greift dieselbe
|
||||||
|
/// Rand-/Knick-Logik wie bei einseitigen Meshes (Waende), unabhaengig davon,
|
||||||
|
/// ob doppelseitig gerendert wurde oder nicht.
|
||||||
fn should_draw(normals: &[[f32; 3]]) -> bool {
|
fn should_draw(normals: &[[f32; 3]]) -> bool {
|
||||||
match normals.len() {
|
let mut representatives: Vec<[f32; 3]> = Vec::new();
|
||||||
|
let mut used = vec![false; normals.len()];
|
||||||
|
for i in 0..normals.len() {
|
||||||
|
if used[i] {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
used[i] = true;
|
||||||
|
representatives.push(normals[i]);
|
||||||
|
for j in (i + 1)..normals.len() {
|
||||||
|
if used[j] {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if dot(normals[i], normals[j]) < -CREASE_COS {
|
||||||
|
used[j] = true; // Rueckseiten-Duplikat desselben Dreiecks
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match representatives.len() {
|
||||||
0 => false,
|
0 => false,
|
||||||
1 => true, // Silhouette/Rand: gehoert nur einem Dreieck
|
1 => true, // Silhouette/Rand: gehoert nur einem Original-Dreieck
|
||||||
_ => {
|
_ => {
|
||||||
// Zeichnen, sobald irgendein Normalen-Paar deutlich abknickt.
|
// Zeichnen, sobald irgendein Normalen-Paar deutlich abknickt.
|
||||||
for i in 0..normals.len() {
|
for i in 0..representatives.len() {
|
||||||
for j in (i + 1)..normals.len() {
|
for j in (i + 1)..representatives.len() {
|
||||||
if dot(normals[i], normals[j]) < CREASE_COS {
|
if dot(representatives[i], representatives[j]) < CREASE_COS {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -248,3 +278,134 @@ mod tests {
|
|||||||
assert!(build_mesh_edges(&m).is_empty());
|
assert!(build_mesh_edges(&m).is_empty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod double_sided_tests {
|
||||||
|
// Deckt den Nutzer-Report ab (Dreiecks-Diagonalen sichtbar auf Dach/Glas):
|
||||||
|
// Kontext-Meshes werden von `mesh.rs::push_ctx_tri` IMMER doppelseitig
|
||||||
|
// aufgebaut (jedes Dreieck + gespiegelte Rueckseite [-Normale], weil die
|
||||||
|
// Mesh-Pipeline Backface-Culling aktiv hat). Diese Tests bauen genau dieses
|
||||||
|
// Muster nach (nicht die einfachen einseitigen Test-Meshes oben).
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn mesh_single_sided(positions: &[[f32; 3]], indices: &[u32]) -> Mesh {
|
||||||
|
let mut verts = Vec::new();
|
||||||
|
for p in positions {
|
||||||
|
verts.extend_from_slice(&[p[0], p[1], p[2], 0.0, 1.0, 0.0, 0.8, 0.8, 0.8]);
|
||||||
|
}
|
||||||
|
Mesh { verts, indices: indices.to_vec() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Baut ein Mesh wie `mesh_single_sided`, aber verdoppelt JEDES Dreieck mit
|
||||||
|
/// umgekehrter Wicklung (a,c,b statt a,b,c) — exakt das Muster von
|
||||||
|
/// `mesh.rs::push_ctx_tri` (Vorder- + Rueckseite fuer doppelseitiges Rendering).
|
||||||
|
fn mesh_double_sided(positions: &[[f32; 3]], indices: &[u32]) -> Mesh {
|
||||||
|
let mut verts = Vec::new();
|
||||||
|
for p in positions {
|
||||||
|
verts.extend_from_slice(&[p[0], p[1], p[2], 0.0, 1.0, 0.0, 0.8, 0.8, 0.8]);
|
||||||
|
}
|
||||||
|
let mut doubled = Vec::with_capacity(indices.len() * 2);
|
||||||
|
for tri in indices.chunks_exact(3) {
|
||||||
|
doubled.extend_from_slice(&[tri[0], tri[1], tri[2]]);
|
||||||
|
doubled.extend_from_slice(&[tri[0], tri[2], tri[1]]); // gespiegelte Rueckseite
|
||||||
|
}
|
||||||
|
Mesh { verts, indices: doubled }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn segment_count(v: &[f32]) -> usize {
|
||||||
|
v.len() / EDGE_FLOATS_PER_VERTEX / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn doubled_coplanar_quad_still_drops_shared_diagonal() {
|
||||||
|
// Dasselbe ebene Quad wie coplanar_quad_drops_shared_diagonal, aber
|
||||||
|
// doppelseitig aufgebaut — VOR dem Fix haette dies faelschlich 6 statt
|
||||||
|
// 4 Segmente geliefert (Diagonale faelschlich als Knick erkannt).
|
||||||
|
let m = mesh_double_sided(
|
||||||
|
&[
|
||||||
|
[0.0, 0.0, 0.0],
|
||||||
|
[1.0, 0.0, 0.0],
|
||||||
|
[1.0, 0.0, 1.0],
|
||||||
|
[0.0, 0.0, 1.0],
|
||||||
|
],
|
||||||
|
&[0, 1, 2, 0, 2, 3],
|
||||||
|
);
|
||||||
|
let e = build_mesh_edges(&m);
|
||||||
|
assert_eq!(segment_count(&e), 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn doubled_single_triangle_still_has_three_boundary_edges() {
|
||||||
|
// Silhouette-Kanten eines EINZELNEN Dreiecks muessen trotz Verdopplung
|
||||||
|
// (Vorder-/Rueckseite) weiterhin als Rand erkannt und gezeichnet werden.
|
||||||
|
let m = mesh_double_sided(
|
||||||
|
&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]],
|
||||||
|
&[0, 1, 2],
|
||||||
|
);
|
||||||
|
let e = build_mesh_edges(&m);
|
||||||
|
assert_eq!(segment_count(&e), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn doubled_folded_quad_still_keeps_crease() {
|
||||||
|
// Echter 90°-Knick bleibt auch doppelseitig als Kante erhalten (keine
|
||||||
|
// Ueberkompensation, die auch echte Knicke unterdruecken wuerde).
|
||||||
|
let m = mesh_double_sided(
|
||||||
|
&[
|
||||||
|
[0.0, 0.0, 0.0],
|
||||||
|
[1.0, 0.0, 0.0],
|
||||||
|
[1.0, 0.0, 1.0],
|
||||||
|
[1.0, 1.0, 0.0],
|
||||||
|
],
|
||||||
|
&[0, 1, 2, 0, 3, 1],
|
||||||
|
);
|
||||||
|
let e = build_mesh_edges(&m);
|
||||||
|
assert_eq!(segment_count(&e), 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn real_opening_box_topology_suppresses_face_diagonals_doubled() {
|
||||||
|
// Exakte Ecken-Reihenfolge/Indizes aus toWalls3d.ts::OPENING_BOX_TRIS,
|
||||||
|
// diesmal ECHT doppelseitig wie im tatsaechlichen Render-Pfad (die
|
||||||
|
// Fenster-Glas-/Rahmen-Boxen laufen ueber append_context_mesh).
|
||||||
|
let from = 0.0f32;
|
||||||
|
let to = 1.2f32;
|
||||||
|
let z_bottom = 0.9f32;
|
||||||
|
let z_top = 2.1f32;
|
||||||
|
let n_min = -0.03f32;
|
||||||
|
let n_max = 0.03f32;
|
||||||
|
let mut positions = Vec::new();
|
||||||
|
for i in 0..8u32 {
|
||||||
|
let s = if i & 1 != 0 { to } else { from };
|
||||||
|
let z = if i & 2 != 0 { z_top } else { z_bottom };
|
||||||
|
let t = if i & 4 != 0 { n_max } else { n_min };
|
||||||
|
positions.push([s, -t, z]);
|
||||||
|
}
|
||||||
|
let indices: Vec<u32> = vec![
|
||||||
|
0, 1, 3, 0, 3, 2,
|
||||||
|
4, 5, 7, 4, 7, 6,
|
||||||
|
0, 1, 5, 0, 5, 4,
|
||||||
|
2, 3, 7, 2, 7, 6,
|
||||||
|
0, 2, 6, 0, 6, 4,
|
||||||
|
1, 3, 7, 1, 7, 5,
|
||||||
|
];
|
||||||
|
let m = mesh_double_sided(&positions, &indices);
|
||||||
|
let e = build_mesh_edges(&m);
|
||||||
|
// Ein Quader hat 12 echte Kanten (Silhouette); bei korrekter Diagonalen-
|
||||||
|
// Unterdrueckung sollten es GENAU 12 sein (nicht 12+6 Diagonalen=18).
|
||||||
|
assert_eq!(segment_count(&e), 12, "erwartet 12 Kanten (Quader-Silhouette), keine Flaechendiagonalen");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_sided_helper_matches_module_level_mesh_helper() {
|
||||||
|
// Absicherung: der lokale mesh_single_sided-Helfer verhaelt sich
|
||||||
|
// identisch zum bestehenden `tests::mesh` (keine versehentliche
|
||||||
|
// Abweichung beim Kopieren).
|
||||||
|
let m = mesh_single_sided(
|
||||||
|
&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]],
|
||||||
|
&[0, 1, 2],
|
||||||
|
);
|
||||||
|
let e = build_mesh_edges(&m);
|
||||||
|
assert_eq!(segment_count(&e), 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user