Skip to content

Commit 8cab4b6

Browse files
authored
Fix CalculateNormals (#1724)
* no flat faces for setNormals * make SmoothOut self-consistent
1 parent bd56861 commit 8cab4b6

7 files changed

Lines changed: 66 additions & 146 deletions

File tree

include/manifold/manifold.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,8 @@ class Manifold {
245245
int numProp,
246246
std::function<void(double*, vec3, const double*)> propFunc) const;
247247
Manifold CalculateCurvature(int gaussianIdx, int meanIdx) const;
248-
Manifold CalculateNormals(int normalIdx = 0, double minSharpAngle = 60) const;
248+
Manifold CalculateNormals(int normalIdx = 0,
249+
double minSharpAngle = 52.5) const;
249250
///@}
250251

251252
/** @name Smoothing
@@ -257,7 +258,8 @@ class Manifold {
257258
Manifold RefineToLength(double) const;
258259
Manifold RefineToTolerance(double) const;
259260
Manifold SmoothByNormals(int normalIdx = 0) const;
260-
Manifold SmoothOut(double minSharpAngle = 60, double minSmoothness = 0) const;
261+
Manifold SmoothOut(double minSharpAngle = 52.5,
262+
double minSmoothness = 0) const;
261263
static Manifold Smooth(const MeshGL&,
262264
const std::vector<Smoothness>& sharpenedEdges = {});
263265
static Manifold Smooth(const MeshGL64&,

include/manifold/mesh.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121

2222
namespace manifold {
2323

24+
/** @addtogroup Core
25+
* @{
26+
*/
27+
2428
/**
2529
* @brief Mesh input/output suitable for pushing directly into graphics
2630
* libraries.
@@ -268,5 +272,5 @@ using MeshGL64 = MeshGLP<double, uint64_t>;
268272
MeshGL64 ReadOBJ(std::istream& stream);
269273
bool WriteOBJ(std::ostream& stream, const MeshGL64& mesh);
270274
#endif
271-
275+
/** @} */
272276
} // namespace manifold

src/manifold.cpp

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -680,7 +680,7 @@ Manifold Manifold::CalculateCurvature(int gaussianIdx, int meanIdx) const {
680680

681681
/**
682682
* Fills in vertex properties for normal vectors, calculated from the mesh
683-
* geometry. Flat faces composed of three or more triangles will remain flat.
683+
* geometry.
684684
*
685685
* @param normalIdx The property channel in which to store the X values of the
686686
* normals. The X, Y, and Z channels will be sequential. The property set will
@@ -718,11 +718,9 @@ Manifold Manifold::CalculateNormals(int normalIdx, double minSharpAngle) const {
718718
* Smooths out the Manifold by filling in the halfedgeTangent vectors. The
719719
* geometry will remain unchanged until Refine, RefineToLength, or
720720
* RefineToTolerance is called to interpolate the surface. This version uses the
721-
* supplied vertex normal properties to define the tangent vectors. Faces of two
722-
* coplanar triangles will be marked as quads, while faces with three or more
723-
* will be flat. Zero-length normals are considered missing and will defer to
724-
* their neighboring normals instead. If all normals are missing, the vertex
725-
* pseudonormal will be used.
721+
* supplied vertex normal properties to define the tangent vectors. Zero-length
722+
* normals are considered missing and will defer to their neighboring normals
723+
* instead. If all normals are missing, the vertex pseudonormal will be used.
726724
*
727725
* @param normalIdx The first property channel of the normals. NumProp must be
728726
* at least normalIdx + 3. Any vertex where multiple normals exist and don't
@@ -746,13 +744,13 @@ Manifold Manifold::SmoothByNormals(int normalIdx) const {
746744
* geometry will remain unchanged until Refine, RefineToLength, or
747745
* RefineToTolerance is called to interpolate the surface. This version uses the
748746
* geometry of the triangles and pseudo-normals to define the tangent vectors.
749-
* Faces of two coplanar triangles will be marked as quads.
747+
* Faces of two coplanar triangles will be marked as quads, while faces with
748+
* three or more will be flat.
750749
*
751-
* @param minSharpAngle degrees, default 60. Any edges with angles greater than
752-
* this value will remain sharp. The rest will be smoothed to G1 continuity,
753-
* with the caveat that flat faces of three or more triangles will always remain
754-
* flat. With a value of zero, the model is faceted, but in this case there is
755-
* no point in smoothing.
750+
* @param minSharpAngle degrees, default 52.5. Any edges with angles greater
751+
* than this value will remain sharp. The rest will be smoothed to G1
752+
* continuity. With a value of zero, the model is faceted, but in this case
753+
* there is no point in smoothing.
756754
*
757755
* @param minSmoothness range: 0 - 1, default 0. The smoothness applied to sharp
758756
* angles. The default gives a hard edge, while values > 0 will give a small
@@ -765,19 +763,7 @@ Manifold Manifold::SmoothOut(double minSharpAngle, double minSmoothness) const {
765763
return PropagateStatus(leafImpl->status_);
766764
auto pImpl = std::make_shared<Impl>(*leafImpl);
767765
if (!IsEmpty()) {
768-
if (minSmoothness == 0) {
769-
const int numProp = pImpl->numProp_;
770-
Vec<double> properties = pImpl->properties_;
771-
Halfedges halfedge(pImpl->halfedge_.ToData());
772-
pImpl->SetNormals(0, minSharpAngle);
773-
pImpl->CreateTangents(0);
774-
// Reset the properties to the original values, removing temporary normals
775-
pImpl->numProp_ = numProp;
776-
pImpl->properties_ = std::move(properties);
777-
pImpl->halfedge_ = std::move(halfedge);
778-
} else {
779-
pImpl->CreateTangents(pImpl->SharpenEdges(minSharpAngle, minSmoothness));
780-
}
766+
pImpl->CreateTangents(pImpl->SharpenEdges(minSharpAngle, minSmoothness));
781767
}
782768
return Manifold(std::make_shared<CsgLeafNode>(pImpl));
783769
}

src/smoothing.cpp

Lines changed: 7 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -466,8 +466,6 @@ void Manifold::Impl::SetNormals(int normalIdx, double minSharpAngle) {
466466

467467
const int oldNumProp = NumProp();
468468

469-
Vec<bool> triIsFlatFace = FlatFaces();
470-
Vec<int> vertFlatFace = VertFlatFace(triIsFlatFace);
471469
Vec<int> vertNumSharp(NumVert(), 0);
472470
for (size_t e = 0; e < halfedge_.size(); ++e) {
473471
if (!halfedge_.IsForward(e)) continue;
@@ -479,17 +477,6 @@ void Manifold::Impl::SetNormals(int normalIdx, double minSharpAngle) {
479477
if (dihedral > minSharpAngle) {
480478
++vertNumSharp[halfedge_.Start(e)];
481479
++vertNumSharp[halfedge_.End(e)];
482-
} else {
483-
const bool faceSplit =
484-
triIsFlatFace[tri1] != triIsFlatFace[tri2] ||
485-
(triIsFlatFace[tri1] && triIsFlatFace[tri2] &&
486-
!meshRelation_.triRef[tri1].SameFace(meshRelation_.triRef[tri2]));
487-
if (vertFlatFace[halfedge_.Start(e)] == -2 && faceSplit) {
488-
++vertNumSharp[halfedge_.Start(e)];
489-
}
490-
if (vertFlatFace[halfedge_.End(e)] == -2 && faceSplit) {
491-
++vertNumSharp[halfedge_.End(e)];
492-
}
493480
}
494481
}
495482

@@ -525,9 +512,7 @@ void Manifold::Impl::SetNormals(int normalIdx, double minSharpAngle) {
525512
const int vert = halfedge_.Start(startEdge);
526513

527514
if (vertNumSharp[vert] < 2) { // vertex has single normal
528-
const vec3 worldNormal = vertFlatFace[vert] >= 0
529-
? faceNormal_[vertFlatFace[vert]]
530-
: vertNormal_[vert];
515+
const vec3 worldNormal = vertNormal_[vert];
531516
// Non-zero normalIdx is the legacy deferred-transform path: store in
532517
// per-mesh frame so GetMeshGL's runTransform application on export
533518
// recovers world frame even after later transforms. Standard slot 0
@@ -574,11 +559,7 @@ void Manifold::Impl::SetNormals(int normalIdx, double minSharpAngle) {
574559

575560
const double dihedral =
576561
degrees(AngleBetween(faceNormal_[face], faceNormal_[prevFace]));
577-
if (dihedral > minSharpAngle ||
578-
triIsFlatFace[face] != triIsFlatFace[prevFace] ||
579-
(triIsFlatFace[face] && triIsFlatFace[prevFace] &&
580-
!meshRelation_.triRef[face].SameFace(
581-
meshRelation_.triRef[prevFace]))) {
562+
if (dihedral > minSharpAngle) {
582563
break;
583564
}
584565
current = next;
@@ -596,44 +577,21 @@ void Manifold::Impl::SetNormals(int normalIdx, double minSharpAngle) {
596577
ForVert<FaceEdge>(
597578
endEdge,
598579
[&](int current) {
599-
if (IsInsideQuad(current)) {
600-
return FaceEdge({current / 3, vec3(NAN)});
601-
}
602580
const int vert = halfedge_.End(current);
603-
vec3 pos = vertPos_[vert];
604-
if (vertNumSharp[vert] < 2) {
605-
// opposite vert has fixed normal
606-
const vec3 normal = vertFlatFace[vert] >= 0
607-
? faceNormal_[vertFlatFace[vert]]
608-
: vertNormal_[vert];
609-
// Flair out the normal we're calculating to give the edge a
610-
// more constant curvature to meet the opposite normal. Achieve
611-
// this by pointing the tangent toward the opposite bezier
612-
// control point instead of the vert itself.
613-
pos += vec3(TangentFromNormal(normal, halfedge_.Pair(current)));
614-
}
615-
return FaceEdge({current / 3, SafeNormalize(pos - centerPos)});
581+
return FaceEdge(
582+
{current / 3, SafeNormalize(vertPos_[vert] - centerPos)});
616583
},
617584
[&](int, const FaceEdge& here, FaceEdge& next) {
618585
const double dihedral = degrees(
619586
AngleBetween(faceNormal_[here.face], faceNormal_[next.face]));
620-
if (dihedral > minSharpAngle ||
621-
triIsFlatFace[here.face] != triIsFlatFace[next.face] ||
622-
(triIsFlatFace[here.face] && triIsFlatFace[next.face] &&
623-
!meshRelation_.triRef[here.face].SameFace(
624-
meshRelation_.triRef[next.face]))) {
587+
if (dihedral > minSharpAngle) {
625588
normals.push_back(vec3(0.0));
626589
meshIds.push_back(meshRelation_.triRef[next.face].meshID);
627590
}
628591
groups.push_back(normals.size() - 1);
629592
if (std::isfinite(next.normalizedEdge.x)) {
630-
// Boolean subtractee triangles keep their original winding but
631-
// have faceNormal_ negated, so the edge-cross points opposite to
632-
// faceNormal_ there - flip to keep the accumulation outward-from-
633-
// result.
634593
vec3 dir = SafeNormalize(
635594
la::cross(next.normalizedEdge, here.normalizedEdge));
636-
if (la::dot(dir, faceNormal_[next.face]) < 0) dir = -dir;
637595
normals.back() +=
638596
dir * AngleBetween(here.normalizedEdge, next.normalizedEdge);
639597
} else {
@@ -642,10 +600,10 @@ void Manifold::Impl::SetNormals(int normalIdx, double minSharpAngle) {
642600
});
643601

644602
for (int i = 0; i < normals.size(); ++i) {
645-
vec3 n = SafeNormalize(normals[i]);
603+
vec3 n = normals[i];
646604
// Same frame-storage rule as the single-normal path above.
647605
if (normalIdx != 0) n = getTransform(meshIds[i]) * n;
648-
normals[i] = n;
606+
normals[i] = SafeNormalize(n);
649607
}
650608

651609
int lastGroup = 0;

test/properties_test.cpp

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,13 +121,14 @@ TEST(Properties, CalculateCurvature) {
121121

122122
TEST(Properties, CalculateNormals) {
123123
Manifold sphere = Manifold::Sphere(10);
124-
Manifold cut = (sphere - sphere.Translate(vec3(10)));
124+
vec3 center(10);
125+
Manifold cut = (sphere - sphere.Translate(center));
125126
cut.Status();
126127
Manifold cut2(cut.GetMeshGL64());
127128
EXPECT_TRUE(cut.MatchesTriNormals());
128129
EXPECT_TRUE(cut2.MatchesTriNormals());
129-
MeshGL64 out = cut.CalculateNormals(0).GetMeshGL64(0);
130-
MeshGL64 out2 = cut2.CalculateNormals(0).GetMeshGL64(0);
130+
MeshGL64 out = cut.CalculateNormals().GetMeshGL64();
131+
MeshGL64 out2 = cut2.CalculateNormals().GetMeshGL64();
131132
ASSERT_EQ(out.NumTri(), out2.NumTri());
132133
ASSERT_EQ(out.NumVert(), out2.NumVert());
133134
ASSERT_EQ(out.numProp, out2.numProp);
@@ -143,12 +144,26 @@ TEST(Properties, CalculateNormals) {
143144
norm[j] = out.vertProperties[np * v + 3 + j];
144145
norm2[j] = out2.vertProperties[np * v + 3 + j];
145146
ASSERT_FLOAT_EQ(pos[j], pos2[j]);
147+
ASSERT_NEAR(norm[j], norm2[j], 1e-14);
146148
}
147149
if (dot(pos, norm) <= 0) ++numBad;
148150
if (dot(pos2, norm2) <= 0) ++numBad2;
151+
EXPECT_FLOAT_EQ(la::length(norm), 1.0);
152+
EXPECT_TRUE(dot(la::normalize(pos), norm) > 0.99 ||
153+
dot(la::normalize(center - pos), norm) > 0.99);
149154
}
150155
EXPECT_EQ(numBad, 0);
151156
EXPECT_EQ(numBad2, 0);
157+
for (int tv = out2.runIndex[0]; tv < out2.runIndex[1]; ++tv) {
158+
const int v = out2.triVerts[tv];
159+
auto pos = out2.GetVertPos(v);
160+
auto norm = pos;
161+
for (int j : {0, 1, 2}) {
162+
norm[j] = out2.vertProperties[np * v + 3 + j];
163+
}
164+
EXPECT_FLOAT_EQ(la::length(norm), 1.0);
165+
EXPECT_GT(dot(la::normalize(pos), norm), 0.99);
166+
}
152167
}
153168

154169
TEST(Properties, Coplanar) {

test/sdf_test.cpp

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -164,22 +164,19 @@ TEST(SDF, Resize) {
164164
}
165165

166166
TEST(SDF, SineSurface) {
167-
Manifold surface =
168-
Manifold::LevelSet(
169-
[](vec3 p) {
170-
double mid = la::sin(p.x) + la::sin(p.y);
171-
return (p.z > mid - 0.5 && p.z < mid + 0.5) ? 1.0f : -1.0f;
172-
},
173-
{vec3(-1.75 * kPi), vec3(1.75 * kPi)}, 1)
174-
.Simplify();
175-
Manifold smoothed = surface.SmoothOut(180).RefineToLength(0.05);
176-
177-
EXPECT_EQ(smoothed.Status(), Manifold::Error::NoError);
178-
EXPECT_EQ(smoothed.Genus(), 38);
179-
EXPECT_NEAR(smoothed.Volume(), 107.1, 0.1);
180-
EXPECT_NEAR(smoothed.SurfaceArea(), 394.0, 0.1);
181-
182-
if (options.exportModels) WriteTestOBJ("sinesurface.obj", smoothed);
167+
Manifold surface = Manifold::LevelSet(
168+
[](vec3 p) {
169+
double mid = la::sin(p.x) + la::sin(p.y);
170+
return (p.z > mid - 0.5 && p.z < mid + 0.5) ? 1.0f : -1.0f;
171+
},
172+
{vec3(-1.75 * kPi), vec3(1.75 * kPi)}, 1);
173+
174+
EXPECT_EQ(surface.Status(), Manifold::Error::NoError);
175+
EXPECT_EQ(surface.Genus(), 38);
176+
EXPECT_NEAR(surface.Volume(), 102.4, 0.1);
177+
EXPECT_NEAR(surface.SurfaceArea(), 392.4, 0.1);
178+
179+
if (options.exportModels) WriteTestOBJ("sinesurface.obj", surface);
183180
}
184181

185182
TEST(SDF, Blobs) {

test/smooth_test.cpp

Lines changed: 8 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ TEST(Smooth, RefineQuads) {
6969
TEST(Smooth, TruncatedCone) {
7070
Manifold cone = Manifold::Cylinder(5, 10, 5, 12);
7171
Manifold smooth = cone.SmoothOut().RefineToLength(0.5).CalculateNormals(0);
72-
EXPECT_NEAR(smooth.Volume(), 1158.61, 0.01);
73-
EXPECT_NEAR(smooth.SurfaceArea(), 768.12, 0.01);
72+
EXPECT_NEAR(smooth.Volume(), 1163.53, 0.01);
73+
EXPECT_NEAR(smooth.SurfaceArea(), 769.33, 0.01);
7474
CheckGL(smooth, false);
7575

7676
Manifold smooth1 = cone.SmoothOut(180, 1).RefineToLength(0.5);
@@ -90,16 +90,16 @@ TEST(Smooth, ToLength) {
9090
Manifold smooth =
9191
cone.AsOriginal().Simplify().SmoothOut(180).RefineToLength(0.1);
9292
ExpectMeshes(smooth, {{85250, 170496}});
93-
EXPECT_NEAR(smooth.Volume(), 4505, 1);
94-
EXPECT_NEAR(smooth.SurfaceArea(), 1337, 1);
93+
EXPECT_NEAR(smooth.Volume(), 4570, 1);
94+
EXPECT_NEAR(smooth.SurfaceArea(), 1348, 1);
9595

9696
MeshGL out = smooth.CalculateCurvature(-1, 0).GetMeshGL();
9797
float maxMeanCurvature = 0;
9898
for (size_t i = 3; i < out.vertProperties.size(); i += 4) {
9999
maxMeanCurvature =
100100
std::max(maxMeanCurvature, std::abs(out.vertProperties[i]));
101101
}
102-
EXPECT_NEAR(maxMeanCurvature, 2.32, 0.01);
102+
EXPECT_NEAR(maxMeanCurvature, 1.63, 0.01);
103103

104104
if (options.exportModels) WriteTestOBJ("smoothToLength.obj", smooth);
105105
}
@@ -213,11 +213,11 @@ TEST(Smooth, MissingNormals) {
213213
}
214214

215215
TEST(Smooth, MissingNormalsCone) {
216-
Manifold cone = Manifold::Cylinder(10, 10, 0, 5).CalculateNormals(0);
216+
Manifold cone = Manifold::Cylinder(10, 10, 0, 5).CalculateNormals(0, 60);
217217
Manifold diff = cone - Manifold::Cube(vec3(10), true).Translate({0, 0, 10});
218218
Manifold out = diff.SmoothByNormals(0).Refine(20);
219-
EXPECT_NEAR(out.Volume(), 1092, 1);
220-
EXPECT_NEAR(out.SurfaceArea(), 748, 1);
219+
EXPECT_NEAR(out.Volume(), 1009, 1);
220+
EXPECT_NEAR(out.SurfaceArea(), 736, 1);
221221
if (options.exportModels) WriteTestOBJ("missingNormalsCone.obj", out);
222222
}
223223

@@ -383,48 +383,6 @@ TEST(Smooth, Torus) {
383383
}
384384
#endif
385385

386-
TEST(Smooth, SineSurface) {
387-
Manifold surface =
388-
Manifold::LevelSet(
389-
[](vec3 p) {
390-
double mid = la::sin(p.x) + la::sin(p.y);
391-
return (p.z > mid - 0.5 && p.z < mid + 0.5) ? 1.0 : -1.0;
392-
},
393-
{vec3(-2 * kPi + 0.2), vec3(0 * kPi - 0.2)}, 1)
394-
.Simplify();
395-
396-
Manifold smoothed =
397-
surface.CalculateNormals(0, 50).SmoothByNormals(0).Refine(8);
398-
EXPECT_NEAR(smoothed.Volume(), 8.07, 0.01);
399-
EXPECT_NEAR(smoothed.SurfaceArea(), 30.87, 0.01);
400-
EXPECT_EQ(smoothed.Genus(), 0);
401-
EXPECT_NEAR(smoothed.TrimByPlane({0, 1, 1}, -3.19487).Volume(),
402-
smoothed.Volume(), 1e-5);
403-
404-
Manifold smoothed1 = surface.SmoothOut(50).Refine(8);
405-
EXPECT_FLOAT_EQ(smoothed1.Volume(), smoothed.Volume());
406-
EXPECT_FLOAT_EQ(smoothed1.SurfaceArea(), smoothed.SurfaceArea());
407-
EXPECT_EQ(smoothed1.Genus(), 0);
408-
EXPECT_NEAR(smoothed1.TrimByPlane({0, 1, 1}, -3.19487).Volume(),
409-
smoothed1.Volume(), 1e-5);
410-
411-
Manifold smoothed2 = surface.SmoothOut(180, 1).Refine(8);
412-
EXPECT_NEAR(smoothed2.Volume(), 8.95, 0.01);
413-
EXPECT_NEAR(smoothed2.SurfaceArea(), 33.35, 0.01);
414-
EXPECT_EQ(smoothed2.Genus(), 0);
415-
EXPECT_NEAR(smoothed2.TrimByPlane({0, 1, 1}, -3.19487).Volume(),
416-
smoothed2.Volume(), 1e-3);
417-
418-
Manifold smoothed3 = surface.SmoothOut(50, 0.5).Refine(8);
419-
EXPECT_NEAR(smoothed3.Volume(), 8.38, 0.01);
420-
EXPECT_NEAR(smoothed3.SurfaceArea(), 31.55, 0.02);
421-
EXPECT_EQ(smoothed3.Genus(), 0);
422-
EXPECT_NEAR(smoothed3.TrimByPlane({0, 1, 1}, -3.19487).Volume(),
423-
smoothed3.Volume(), 1e-5);
424-
425-
if (options.exportModels) WriteTestOBJ("smoothSineSurface.obj", smoothed);
426-
}
427-
428386
TEST(Smooth, SDF) {
429387
const double r = 10;
430388
const double extra = 2;

0 commit comments

Comments
 (0)