[Help] Discrepancy between ParaView and VTK C++: vtkCleanPolyData works, but mesh shatters after computing normals

Hi everyone,

I am implementing a mesh cutting feature using vtkBoxClipDataSet on arbitrary meshes.

  • Input: Arbitrary vtkPolyData constructed from triangle parameters (in my current test case, the geometry happens to be a cylinder, but it could be any shape).

  • Pipeline: vtkBoxClipDataSetvtkTriangleFiltervtkCleanPolyDatavtkPolyDataNormals.

I have prototyped this workflow in ParaView, and it produces a smooth, continuous surface. However, when I implement the exact same pipeline in C++, the result looks “shattered” (hard edges everywhere), as if every triangle is detached.

My Investigation & Evidence

To isolate the issue, I exported .vtp files at every stage of my C++ pipeline and compared them with manual filters in ParaView (A/B testing).

1. Confirming “Clean” works in C++

  • File A: 04_Outside_cleaner_Computed.vtp (Exported after C++ vtkCleanPolyData).

  • File B: 04_Outside_triFilter_Computed.vtp (Exported after C++ Triangulation) → Manually applied Clean filter in ParaView.

  • Observation: Both File A and File B have the exact same number of points and cells (Point count reduced from ~15k to ~4k).

  • Conclusion: This confirms that my C++ vtkCleanPolyData (using SetToleranceIsAbsolute(true) with 1e-3) is correctly merging vertices. The topology is closed at this stage.

2. The discrepancy happens at the “Normals” step I compared three scenarios for normal generation:

  • Scenario 1 (Pure C++ Pipeline): Automated code execution.

    • Result: Point count jumps back to ~15k. The mesh is shattered/faceted.
  • Scenario 2 (Hybrid): Load Code-Cleaned 04_Outside_cleaner_Computed.vtp → Manually apply Generate Surface Normals in ParaView.

    • Result: Point count remains high (~15k). The mesh is still shattered.
  • Scenario 3 (Pure ParaView Pipeline): Load Triangulated data → ParaView Manual Clean → ParaView Manual Generate Surface Normals.

    • Result: Point count stays low (~5k). The mesh is SMOOTH. The cylinder wall is smooth, and only the cut edges are sharp.

My Conclusion & Questions

Since Scenario 3 (Pure ParaView) succeeds while Scenario 1 (Pure Code) fails—despite the intermediate “Clean” data being identical—I suspect the issue lies in the vtkPolyDataNormals configuration, specifically the Feature Angle or Splitting logic.

In my C++ code, I set the Feature Angle to 30.0 degrees.

  1. Feature Angle Sensitivity: Is 30.0 degrees generally too low for a standard tessellated cylinder? Does this cause SplittingOn() to incorrectly identify the angle between smooth curved faces as “sharp edges,” effectively re-splitting the vertices I just merged?

  2. ParaView Defaults: Does ParaView’s “Generate Surface Normals” filter use a default Feature Angle of 60.0? Or does it apply some extra logic to prevent over-splitting?

Here is my C++ implementation:

C++

// 1. Clean (Verified to work via export)
auto cleaner = vtkSmartPointer<vtkCleanPolyData>::New();
cleaner->SetInputData(triFilter->GetOutput());
cleaner->PointMergingOn();
cleaner->SetPieceInvariant(true);
// I used absolute tolerance to handle floating point noise from vtkBoxClipDataSet
cleaner->SetToleranceIsAbsolute(true);
cleaner->SetAbsoluteTolerance(0.001); // 1e-3
cleaner->Update();

// 2. Normals (Suspected Culprit)
auto normalGen = vtkSmartPointer<vtkPolyDataNormals>::New();
normalGen->SetInputData(cleaner->GetOutput());
normalGen->ComputePointNormalsOn();
normalGen->SplittingOn();  // I suspect this is re-splitting my welded vertices
normalGen->SetNonManifoldTraversal(true);
normalGen->ConsistencyOn();

// I used 30.0 degrees. Is this the cause?
normalGen->SetFeatureAngle(30.0); 

if (isClosedMesh && IsWatertight(mesh)) {
    normalGen->AutoOrientNormalsOn();
} else {
    normalGen->AutoOrientNormalsOff();
}

normalGen->Update();



Step-by-Step Verification via Exported Files

I exported the model as a .vtp file after every processing step to compare the results.

  1. 04_Outside_cleaner_Computed.vtp: This is the model generated by the C++ code after passing through vtkTriangleFilter and vtkCleanPolyData.
  2. 04_Outside_triFilter_Computed.vtp: This is the model generated by the C++ code after vtkTriangleFilter (before cleaning). I loaded this file into ParaView and manually applied the Clean filter.
  • Observation: Both of the above have the exact same number of points and cells. This confirms the C++ Clean step is working identically to ParaView’s Clean step regarding topology.

However, the divergence happens at the Normals step:

  1. 04_Outside_Normals_Computed.vtp: This is the final output from the full C++ pipeline (Triangulate → Clean → Normals).
  2. GenerateSurfaceNormals1 (in ParaView): I loaded the code-cleaned file (04_Outside_cleaner_Computed.vtp) and manually applied “Generate Surface Normals” in ParaView.
  • Observation: This object has the same high point count as the C++ output (04_Outside_Normals_Computed.vtp).
  1. GenerateSurfaceNormals2 (in ParaView): I loaded the raw triangulated file (04_Outside_triFilter_Computed.vtp), then manually applied the Clean filter followed by the Generate Surface Normals filter entirely within ParaView.
  • Observation: This object has significantly fewer points than the previous two cases.
    Any advice on why the C++ vtkPolyDataNormals splits the edges while ParaView keeps them connected would be greatly appreciated.



I have managed to solve this issue. It turns out the root cause was Non-Manifold Topology hidden inside the mesh, which caused vtkPolyDataNormals to miscalculate orientations when Consistency checks were enabled.

Here is the breakdown of the problem and the solution:

1. The Root Cause: After processing the mesh with vtkCleanPolyData (or vtkStaticCleanPolyData), some internal vertices were merged, creating Non-Manifold Edges (edges shared by 3 or more faces). Specifically, there was an internal “coil” or “septum” structure connecting the inner walls.

When I used normalGen->ConsistencyOn(), the algorithm traversed the mesh to unify normal directions. However, upon hitting these non-manifold edges, the traversal logic propagated the wrong orientation (flipping normals inward) to large portions of the mesh, causing the “shattered” look with black artifacts.

2. How I Diagnosed It: I verified this using ParaView:

  1. Applied CleanPolyData to the model.
  2. Applied the Feature Edges filter.
  3. Unchecked all options and selected only Non-Manifold Edges.
  4. This revealed a disconnected loop of edges inside the cylinder, confirming the topology issue.

3. The Solution (C++ Code): To fix this for rendering, the key is to turn OFF Consistency (trusting the local winding order) while keeping Splitting ON (to handle the sharp 90-degree edges of the cylinder).

Here is the working configuration:

`C++auto normalGen = vtkSmartPointer::New();
normalGen->SetInputData(mesh);

// 1. Enable Splitting to handle sharp edges (e.g., the 90-degree edge of the cylinder cap)
normalGen->SplittingOn();
normalGen->SetFeatureAngle(60.0);

// 2. CRITICAL: Turn OFF Consistency.
// Since the mesh has non-manifold geometry, enforcing consistency causes
// the propagation to fail and flip normals incorrectly.
// We trust the original local winding order here.
normalGen->ConsistencyOff();

// 3. Ensure robustness
normalGen->ComputePointNormalsOn();
normalGen->SetNonManifoldTraversal(true);

normalGen->Update();`

Hope this helps anyone else encountering this issue.