-
Notifications
You must be signed in to change notification settings - Fork 0
Fix symbolic perturbation to use face normals instead of vertex normals #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
…ertHalfedge initialization Co-authored-by: zalo <[email protected]>
Co-authored-by: zalo <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR attempts to fix symbolic perturbation in boolean operations by replacing vertex normals with face normal components. The implementation introduces a new MaxFaceNormalComponent function that computes the maximum value of a specific face normal component across all faces connected to a vertex, and updates the shadow computation logic to use this approach instead of direct vertex normal lookups.
Key changes include:
- Initializing unreferenced vertices to -1 in
VertHalfedge() - Adding
MaxFaceNormalComponenthelper function to compute maximum face normal component values - Updating
Shadow01,Kernel11, andKernel02to use face normals instead of vertex normals
Reviewed Changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| src/smoothing.cpp | Fixes VertHalfedge() to properly initialize unreferenced vertices to -1 instead of undefined values |
| src/boolean3.cpp | Rewrites shadow computation to use maximum face normal components instead of vertex normals; adds MaxFaceNormalComponent helper; updates Kernel02 and Kernel11 signatures and implementations |
| CMakeLists.txt | Formatting changes to consolidate list() commands onto single lines |
Comments suppressed due to low confidence (4)
src/boolean3.cpp:295
- When
forwardis false ands02 != 0, the code callsMaxFaceNormalComponent(closestVert, 2, vertHalfedgeQ, halfedgeQ, faceNormalQ)whereclosestVertis a vertex from the Q mesh. However, if this vertex is unreferenced (i.e.,vertHalfedgeQ[closestVert] == -1),MaxFaceNormalComponentwill return 0.0.
This could lead to incorrect shadow computation. Consider adding a check or assertion to ensure the closest vertex is actually referenced in the mesh, or handle the unreferenced case explicitly with appropriate fallback logic.
yzzRL[k++] = vec3(yz01[0], yz01[1], yz01[1]);
}
}
}
src/boolean3.cpp:215
- Similar to other uses of
MaxFaceNormalComponent, if the chosen vertex (vert) is unreferenced (i.e.,vertHalfedgeP[vert] == -1), the function will return 0.0, which could lead to incorrect shadow computations.
Consider validating that vertices involved in shadow computation are always referenced, or explicitly document the behavior when unreferenced vertices are encountered.
}
}
}
src/boolean3.cpp:537
- Similar to the issue in
Intersect12, there's a potential inconsistency in the Kernel02 initialization. Whenforward=false:
a = inQ, b = inPvertPosP = a.vertPos_ = inQ.vertPos_andhalfedgeP = a.halfedge_ = inQ.halfedge_- But
faceNormalP = inP.faceNormal_andvertHalfedgePis frominP.VertHalfedge()
This creates a mismatch where halfedge indices from one mesh are used with face normals and vertex-halfedge mappings from another mesh, potentially causing incorrect array accesses or logic errors.
componentsShared.combine_each([&](const std::unordered_set<int>& data) {
components.insert(data.begin(), data.end());
});
src/boolean3.cpp:449
- There's a potential inconsistency in the Kernel02 initialization. When
forward=false:
a = inQ, b = inP(from line 440-441)vertPosP = a.vertPos_ = inQ.vertPos_andhalfedgeP = a.halfedge_ = inQ.halfedge_- But
faceNormalP = inP.faceNormal_andvertHalfedgePis frominP.VertHalfedge()
This means halfedgeP comes from mesh Q, but faceNormalP and vertHalfedgeP come from mesh P, creating a mismatch. The same applies to halfedgeQ, faceNormalQ, and vertHalfedgeQ.
This could lead to incorrect array accesses where halfedge indices from one mesh are used to index into face normals or vertex-halfedge mappings from a different mesh. Verify that this swap is intentional and correct, or fix the initialization to use a.faceNormal_/b.faceNormal_ instead of always using inP/inQ.
}
};
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
|
|
||
| inline std::pair<int, vec2> Shadow01( | ||
| const int p0, const int q1, VecView<const vec3> vertPosP, | ||
| VecView<const vec3> vertPosQ, VecView<const Halfedge> halfedgeQ, | ||
| const double expandP, VecView<const vec3> normal, const bool reverse) { | ||
| VecView<const vec3> vertPosQ, VecView<const Halfedge> halfedgeP, | ||
| VecView<const Halfedge> halfedgeQ, const double expandP, | ||
| VecView<const vec3> faceNormalP, VecView<const vec3> faceNormalQ, | ||
| VecView<const int> vertHalfedgeP, VecView<const int> vertHalfedgeQ, | ||
| const bool reverse) { | ||
| const int q1s = halfedgeQ[q1].startVert; | ||
| const int q1e = halfedgeQ[q1].endVert; | ||
| const double p0x = vertPosP[p0].x; | ||
| const double q1sx = vertPosQ[q1s].x; | ||
| const double q1ex = vertPosQ[q1e].x; | ||
| int s01 = reverse ? Shadows(q1sx, p0x, expandP * normal[q1s].x) - | ||
| Shadows(q1ex, p0x, expandP * normal[q1e].x) | ||
| : Shadows(p0x, q1ex, expandP * normal[p0].x) - | ||
| Shadows(p0x, q1sx, expandP * normal[p0].x); | ||
|
|
||
| int s01 = 0; | ||
| if (reverse) { | ||
| const double dirQ1s = | ||
| MaxFaceNormalComponent(q1s, 0, vertHalfedgeQ, halfedgeQ, faceNormalQ); | ||
| const double dirQ1e = | ||
| MaxFaceNormalComponent(q1e, 0, vertHalfedgeQ, halfedgeQ, faceNormalQ); | ||
| s01 = Shadows(q1sx, p0x, expandP * dirQ1s) - | ||
| Shadows(q1ex, p0x, expandP * dirQ1e); | ||
| } else { | ||
| const double dirP0 = | ||
| MaxFaceNormalComponent(p0, 0, vertHalfedgeP, halfedgeP, faceNormalP); | ||
| s01 = Shadows(p0x, q1ex, expandP * dirP0) - |
Copilot
AI
Nov 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If any of the vertices (q1s, q1e, or p0) are unreferenced (i.e., their corresponding entry in vertHalfedge is -1), MaxFaceNormalComponent will return 0.0, which could lead to incorrect shadow computations.
While unreferenced vertices might be rare in practice, the code should handle this case explicitly or add assertions to validate that vertices used in shadow computation are always referenced. The original code using normal[vertex] would have crashed or produced garbage on unreferenced vertices, so this is actually an improvement, but the 0.0 fallback behavior should be documented or reconsidered.
|
|
||
| return maxVal == -std::numeric_limits<double>::infinity() ? 0.0 : maxVal; | ||
| } | ||
|
|
||
| inline std::pair<int, vec2> Shadow01( | ||
| const int p0, const int q1, VecView<const vec3> vertPosP, | ||
| VecView<const vec3> vertPosQ, VecView<const Halfedge> halfedgeQ, | ||
| const double expandP, VecView<const vec3> normal, const bool reverse) { | ||
| VecView<const vec3> vertPosQ, VecView<const Halfedge> halfedgeP, | ||
| VecView<const Halfedge> halfedgeQ, const double expandP, | ||
| VecView<const vec3> faceNormalP, VecView<const vec3> faceNormalQ, | ||
| VecView<const int> vertHalfedgeP, VecView<const int> vertHalfedgeQ, | ||
| const bool reverse) { |
Copilot
AI
Nov 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The MaxFaceNormalComponent function is called multiple times for the same vertex within shadow computation loops. For example, in Shadow01, when reverse is false, dirP0 is computed once but could be reused if the same vertex is tested multiple times.
Consider caching the results of MaxFaceNormalComponent calls if the same vertices are queried repeatedly within a single shadow computation, especially since the function iterates through all faces connected to a vertex, which could be expensive for high-valence vertices.
| if (vert < 0 || static_cast<size_t>(vert) >= vertHalfedge.size()) return 0.0; | ||
|
|
||
| const int firstEdge = vertHalfedge[vert]; | ||
| if (firstEdge < 0 || static_cast<size_t>(firstEdge) >= halfedge.size()) |
Copilot
AI
Nov 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The boundary check condition is redundant and potentially misleading. The second part static_cast<size_t>(firstEdge) >= halfedge.size() will never be true when firstEdge >= 0 because:
- The
VertHalfedge()function only assigns valid halfedge indices (from 0 to halfedge_.size()-1) or leaves vertices at -1 - If the first condition
firstEdge < 0is false, thenfirstEdge >= 0, and valid indices fromVertHalfedge()are always within bounds
The second check would only catch out-of-bounds access if the vertHalfedge array was corrupted or incorrectly populated, which should not happen. Consider simplifying to just if (firstEdge < 0) return 0.0; for clarity.
| if (firstEdge < 0 || static_cast<size_t>(firstEdge) >= halfedge.size()) | |
| if (firstEdge < 0) return 0.0; // vertex not referenced |
|
|
||
| double maxVal = -std::numeric_limits<double>::infinity(); | ||
| int current = firstEdge; | ||
| int iterations = 0; | ||
| const int maxIterations = halfedge.size(); // prevent infinite loops |
Copilot
AI
Nov 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The MaxFaceNormalComponent function could potentially have an infinite loop if the halfedge topology is corrupted or malformed. The do-while loop assumes that following NextHalfedge(halfedge[current].pairedHalfedge) will eventually return to firstEdge, but this is not guaranteed if:
- The pairedHalfedge value is invalid or points outside the halfedge array
- The halfedge mesh has disconnected components or broken topology
Consider adding a loop counter or visited set to prevent infinite loops in case of corrupted mesh data, similar to how other functions in the codebase handle such cases.
| // Helper function to get the maximum value of a face normal component | ||
| // across all faces connected to a vertex | ||
| inline double MaxFaceNormalComponent(int vert, int component, | ||
| VecView<const int> vertHalfedge, | ||
| VecView<const Halfedge> halfedge, | ||
| VecView<const vec3> faceNormal) { | ||
| if (vert < 0 || static_cast<size_t>(vert) >= vertHalfedge.size()) return 0.0; | ||
|
|
||
| const int firstEdge = vertHalfedge[vert]; | ||
| if (firstEdge < 0 || static_cast<size_t>(firstEdge) >= halfedge.size()) | ||
| return 0.0; // vertex not referenced or invalid index | ||
|
|
||
| double maxVal = -std::numeric_limits<double>::infinity(); | ||
| int current = firstEdge; | ||
| int iterations = 0; | ||
| const int maxIterations = halfedge.size(); // prevent infinite loops | ||
| do { | ||
| if (current < 0 || static_cast<size_t>(current) >= halfedge.size()) { | ||
| break; // invalid halfedge index |
Copilot
AI
Nov 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The MaxFaceNormalComponent function lacks documentation explaining:
- What it does and why it uses the maximum instead of computing a weighted average or other aggregation
- What the
componentparameter represents (0=x, 1=y, 2=z) - What the return value represents when a vertex is unreferenced (currently returns 0.0)
- The rationale for using the maximum value strategy in the symbolic perturbation scheme
Adding a comprehensive comment would help future maintainers understand this critical function in the boolean operation logic.
|
This is the closest version; but it still generates slivers. Perhaps it needs a better tiebreaker than “maximum”. |
Symbolic perturbation using averaged vertex normals yields incorrect results for CSG operations. The perturbation must instead use individual face normals connected to each vertex.
Changes
Core algorithm update:
MaxFaceNormalComponent()that iterates all face normals at a vertex and returns the maximum component value for the perturbation directionShadow01()signature to accept face normal arrays and vertex-to-halfedge maps instead of single normal vectorCritical bug fixes:
VertHalfedge()to initialize unreferenced vertices to-1(was uninitialized, causing random memory access)Kernel02to usea.faceNormal_/b.faceNormal_and swap vertex halfedge maps based onforwardflag (was incorrectly always usinginP/inQ)Example change:
Test Results
✓ Fixes segfaults and assertion failures (MirrorUnion, MinGapCubeSphereOverlapping now pass)
⚠ Two tests still fail with incorrect geometric results (Coplanar has wrong genus, Boolean.Cubes has degenerate triangles)
The "maximum component" interpretation may need refinement for edge cases.
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.