Skip to content
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

No zero length normals. #734

Closed
wants to merge 1 commit into from
Closed

Conversation

phkahler
Copy link
Member

If a surface normal is zero length, try harder by using tangents at different points along the degenerate U or V curve - like at the poles of a sphere.

@phkahler phkahler linked an issue Oct 12, 2020 that may be closed by this pull request
@phkahler
Copy link
Member Author

@jwesthues This fixes the problem of cutting a hemisphere out of a face, but it introduces some slightly weird shading on cones. Apparently for shading a zero normal is better than this, but for NURBS this is better than zero-length. It is actually correct at the poles of a sphere, which is where the remaining problem was happening in issue #652. Just wondering if you have a better idea how to deal with zero-length normals.

@jwesthues
Copy link
Member

Without having thought too deeply about this; what's the advantage of what you're doing now with 1 - u or 1 - v, vs. just displacing it slightly from the bad point (e.g., if it fails at (u, v) = (0, 0), then try at (0.001, 0.001))?

@phkahler
Copy link
Member Author

What I did here gives a correct normal at the pole of a sphere. I was investigating a problem with a sphere. For a cone the best displacement might be at (0.5 0.001) or (0.001 0.5) but that will be similar to this normal and will need to decide between 0.001 or 0.999? There really is no correct normal for the tip of a cone - ideally I think finding the axis of revolution would give the best normal for some definition of best.

@jwesthues
Copy link
Member

There's no correct normal at the tip of the cone, but there is a correct normal over 100% of the surface area. So it still seems naively like just displacing the test point inwards (like with respect to the trim curves in the (u, v) plane) a bit should always get the desired answer, with either a fixed offset or some kind of heuristic (step until both derivatives have magnitude over some threshold?) to determine that?

I see why your 1 - u approach gets the right answer on the sphere sections, which have a kind of symmetry. It's not obvious to me why that would be correct for arbitrary surfaces, though.

@ruevs
Copy link
Member

ruevs commented Oct 12, 2020

@phkahler I agree with Jonathan.

As you have already explained here sometimes NURBS patches have a "degenerate" (zero length/to a point) edge/side. In this case the tangents along that edge become undefined(?).

Here is a picture:
NURBSPatchWithDegenerateEdgeCausesZeroLengthNormalsBecauseOfUndefinedTangent
A normal n at a point (u.v) is calculated by calculating the tangents tu and tv at that point and taking the cross product.
However when an edge is "degenerate" - as is the (0,1)(1,1) edge in this example part of a cone - then its length becomes zero and both tu and tv become undefined for any point along the (u,1) "degenerate edge".

In my opinion in this case the correct logic is to fix SSurface::TangentsAt so that it calculates sensible tangents as @jwesthues suggested:

if ((1==v) &&
        (trim curve along (0,1)(1,1) is zero length)) {
    v -= 0.00000000001;
}
if ((0==u) &&
        (trim curve along (0,0)(0,1) is zero length)) {
    u += 0.00000000001;
}
etc.

I could not quickly figure out how check if (trim curve along (0,0)(0,1) is zero length) so as an experiment I did this stupidity:

void SSurface::TangentsAt(double u, double v, Vector *tu, Vector *tv) const {
    Vector num   = Vector::From(0, 0, 0),
           num_u = Vector::From(0, 0, 0),
           num_v = Vector::From(0, 0, 0);
    double den   = 0,
           den_u = 0,
           den_v = 0;

    if(0 == u) {
        u += 0.000000001;
    }
    if(0 == v) {
        v += 0.000000001;
    }
    if(1 == u) {
        u -= 0.000000001;
    }
    if(1 == v) {
        v -= 0.000000001;
    }

    int i, j;
    for(i = 0; i <= degm; i++) {
        for(j = 0; j <= degn; j++) {
            double Bi  = Bernstein(i, degm, u),

It is wrong in so many ways (e.g. == comparison on a float), but if "fixes" both "spherecut.zip" #652 (comment) and "triangle.slvs.zip" #652 (comment)

Instead of the above logic it bay be possible to somehow change the algorithm itself to do "the right thing (TM)" in such cases but I am light years away from understanding it :-)

And last: SBezier::TangentAt also uses the same logic so maybe a bezier loop like this:
BezierDiscontinuity
could cause a similar problem is some case... ? But probably not, since the tangent is well defined at the end of each segment, it is just that the tangents of the two segments are not parallel.

@phkahler
Copy link
Member Author

@ruevs The triangle case from 652 is already fixed on master, it's only when you use the sketch line as the rotation axis that there is a problem and it seem to be due to the zero normal vector at the "pole".

My fix works by using 2 different tangent vector at the pole. Only one of them will go to zero which causes the cross product (normal) to be zero. I think what I did has the normal pointing inward or outward at opposite "ends" of the degenerate curve.

I'm going to try what @jwesthues suggests and just take tangents nearby along the "good" U or V axis.

Another thought I had... For argument lets assume the V direction is the zero-length edge at a given U value. I think a better way to find tangent tv (only as a substitute when the real tv is zero) would be to calculate the derivative to tv with respect to U. that should work and would produce different tv vectors depending on the V coordinate even though it's always at the same point in space. Similar to offsetting along U but without actually doing so. I don't have the ambition to write that at the moment so I'm going to try the nearby u,v point ;-)

@phkahler
Copy link
Member Author

I tried this. Both still fail often for the case of a triangle lathed around it's own edge:

    if(tu.MagSquared() < tv.MagSquared()) {
        // weighted average of v and 1-v.
        v = (v*31 + (1-v))/32.0;
    } else {
        u = (u*31 + (1-u))/32.0;
    }
    TangentsAt(u, v, &tu, &tv);
    n = tu.Cross(tv);

But this version doesn't have strange shading issues, probably the other version points the normal inward sometimes and this doesn't.

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
Lathed surfaces with a point on the lathe axis, there is a degenerate edge where one of the tangents is zero and the computed normal will be zero. That messes up booleans when the seams are in-plane with another surface. To prevent that we avoid taking tangents at the vertex and instead use on nearby.
@phkahler
Copy link
Member Author

I've updated to the @ruevs method which works better even for the spherecut. There are probably places that don't like the tangents to be zero, so fixing it there is probably better.

@jwesthues
Copy link
Member

Cool. If you want something nicer numerically than a check for exact equality, you could try checking if the magnitude of the vector is less than a threshold, and then incrementing (if u or v < 0.5) or decrementing (otherwise) u and v until that magnitude is greater or until you give up with an error.

@phkahler
Copy link
Member Author

@ruevs it's fine if you think we can do better. The check for weather a tangent is going to be zero would be that the adjacent control point is equal to the one we're evaluating at.

So if i,j = (0,0) we should check if ctrl[0][0].EQUALS(ctrl[1][0]) then the tu tangent is going to be zero so we should offset our v coordinate to some small value. But if ctrl[0][0].EQUALS(ctrl[0][1]) then tv is going to be zero and we should offset u.

At the other extreme we're at u,v = (1,1) we'd check if ctrl[degm][degn].EQUALS(ctrl[debm-1][degn]) to see if tu is zero and set v = 0.999999. And so on. There are a lot of conditions that can probably be somewhat simplified with a bit of cleverness.

Don't take my word for which control points need to match here, after that other PR for the other NURBS issue I'm not sure I can logic today ;-)

@jwesthues
Copy link
Member

Being lazy, I'd probably just nudge both. The unnecessary nudge won't hurt much.

Another direction is to not generate coincident control points in the first place. I'm not perfectly confident that's always possible, but I suspect that it is (as long as you don't need the trim curve to be a square from (0, 0) to (1, 1), like it is now for all new extruded or lathed surfaces; whenever there's a singularity like the tip of a cone, you'd need to make that correspond to a single point of the trim curve).

ruevs added a commit to ruevs/solvespace that referenced this pull request Oct 13, 2020
ruevs added a commit to ruevs/solvespace that referenced this pull request Oct 13, 2020
This avoids zero length normals.

Sometimes NURBS patches have a "degenerate" (zero length/to a point)
edge/side. In this case the tangent(s) on points "along" that "edge"
become zero length.

A normal `n` at a point (u.v) on a NURBS surface is calculated by calculating
the tangents `tu` and `tv` at that point and taking the cross product.
However when an edge is "degenerate" (for example a cone created by lathing
a triangle) then `tv` (and possibly `tu`) becomes zero and `n` becomes zero.
This causes a problems with NURBS booleans.

To solve this problem the tangent calculation is changed to shift `u` or `v` inwards
if they are on a degenerate/pinched point on the surface.

Fixes: solvespace#652
Alternative to: solvespace#734
@ruevs
Copy link
Member

ruevs commented Oct 13, 2020

Here is what I came up with. Seems to work :-)
#736

ruevs added a commit to ruevs/solvespace that referenced this pull request Oct 14, 2020
This avoids zero length normals.

Sometimes NURBS patches have a "degenerate" (zero length/to a point)
edge/side. For example lathed surfaces with a point on the lathe axis.
In this case the tangent(s) on points "along" that "edge" become zero
length.

A normal `n` at a point (u.v) on a NURBS surface is calculated by calculating
the tangents `tu` and `tv` at that point and taking the cross product.
However when an edge is "degenerate" then `tv` (or possibly `tu`) becomes
zero and `n` becomes zero.
This causes a problems with NURBS booleans when the seams are in-plane with
another surface.

To solve this problem the tangent calculation is changed to shift `u` or `v`
inwards if they are on a degenerate/pinched point on the surface.

Fixes: solvespace#652
Alternative to: solvespace#734
@phkahler
Copy link
Member Author

Closing as inferior to #736.

@phkahler phkahler closed this Oct 15, 2020
@phkahler phkahler deleted the epsilon branch October 15, 2020 22:14
ruevs added a commit to ruevs/solvespace that referenced this pull request Oct 16, 2020
This avoids zero length normals.

Sometimes NURBS patches have a "degenerate" (zero length/to a point)
edge/side. For example lathed surfaces with a point on the lathe axis.
In this case the tangent(s) on points "along" that "edge" become zero
length.

A normal `n` at a point (u.v) on a NURBS surface is calculated by calculating
the tangents `tu` and `tv` at that point and taking the cross product.
However when an edge is "degenerate" then `tv` (or possibly `tu`) becomes
zero and `n` becomes zero.
This causes a problems with NURBS booleans when the seams are in-plane with
another surface.

To solve this problem the tangent calculation is changed to shift `u` or `v`
inwards if `tu` or `tv` is shorter than a threshold value.

Fixes: solvespace#652
Alternative to: solvespace#736 solvespace#734
@ruevs ruevs added the NURBS label Nov 13, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Can't cut half sphere out of a shape in NURBS
3 participants