conquer3d 0.7.4__tar.gz → 0.7.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. {conquer3d-0.7.4/conquer3d.egg-info → conquer3d-0.7.6}/PKG-INFO +1 -1
  2. conquer3d-0.7.6/conquer3d/_C.so +0 -0
  3. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/binds/ops/dc.cpp +21 -2
  4. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/binds/ops/dmc.cpp +16 -2
  5. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/data_structure/mesh_bvh.cu +5 -13
  6. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/data_structure/triangle_mesh.cu +3 -3
  7. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/maths/f3x1.h +34 -0
  8. conquer3d-0.7.6/conquer3d/csrc/maths/ops.h +89 -0
  9. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/maths/qef.h +11 -21
  10. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/ops/dc.cu +44 -22
  11. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/ops/dc.h +3 -1
  12. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/ops/dmc.cu +49 -14
  13. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/ops/dmc.h +3 -1
  14. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/ops/mc.cu +5 -5
  15. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/ops/mca.cu +4 -4
  16. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/ops/mt.cu +5 -5
  17. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/ops/mtg.cu +5 -5
  18. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/primitive/triangle.h +3 -3
  19. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/ops/__init__.py +4 -0
  20. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/ops/dual_contouring.py +28 -2
  21. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/ops/dual_marching_cubes.py +29 -2
  22. conquer3d-0.7.6/conquer3d/ops/hermite.py +263 -0
  23. {conquer3d-0.7.4 → conquer3d-0.7.6/conquer3d.egg-info}/PKG-INFO +1 -1
  24. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d.egg-info/SOURCES.txt +1 -0
  25. {conquer3d-0.7.4 → conquer3d-0.7.6}/pyproject.toml +1 -1
  26. conquer3d-0.7.4/conquer3d/_C.so +0 -0
  27. conquer3d-0.7.4/conquer3d/csrc/maths/ops.h +0 -43
  28. {conquer3d-0.7.4 → conquer3d-0.7.6}/LICENSE +0 -0
  29. {conquer3d-0.7.4 → conquer3d-0.7.6}/MANIFEST.in +0 -0
  30. {conquer3d-0.7.4 → conquer3d-0.7.6}/README.md +0 -0
  31. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/_C.pyi +0 -0
  32. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/__init__.py +0 -0
  33. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/conversion/__init__.py +0 -0
  34. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/conversion/grid.py +0 -0
  35. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/conversion/mesh.py +0 -0
  36. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/conversion/tmesh.py +0 -0
  37. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/creation/__init__.py +0 -0
  38. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/creation/triangle_creation.py +0 -0
  39. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/binds/creation/triangle_creation.cpp +0 -0
  40. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/binds/data_structure/bvh.cpp +0 -0
  41. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/binds/data_structure/grid.cpp +0 -0
  42. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/binds/data_structure/gs_bvh.cpp +0 -0
  43. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/binds/data_structure/kdtree.cpp +0 -0
  44. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/binds/data_structure/mesh_bvh.cpp +0 -0
  45. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/binds/data_structure/pgs_bvh.cpp +0 -0
  46. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/binds/data_structure/triangle_mesh.cpp +0 -0
  47. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/binds/data_structure/zcurve.cpp +0 -0
  48. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/binds/ops/chamfer.cpp +0 -0
  49. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/binds/ops/flood_fill.cpp +0 -0
  50. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/binds/ops/flood_fill_cf.cpp +0 -0
  51. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/binds/ops/mc.cpp +0 -0
  52. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/binds/ops/mca.cpp +0 -0
  53. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/binds/ops/mt.cpp +0 -0
  54. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/binds/ops/mtg.cpp +0 -0
  55. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/binds/ops/volint.cpp +0 -0
  56. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/binds/primitive/gs.cpp +0 -0
  57. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/binds/primitive/pgs.cpp +0 -0
  58. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/binds/primitive/ray.cpp +0 -0
  59. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/binds/primitive/triangle.cpp +0 -0
  60. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/check.h +0 -0
  61. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/constants.h +0 -0
  62. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/creation/triangle_creation.h +0 -0
  63. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/data_structure/bvh.cu +0 -0
  64. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/data_structure/bvh.h +0 -0
  65. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/data_structure/grid.cu +0 -0
  66. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/data_structure/grid.h +0 -0
  67. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/data_structure/gs_bvh.h +0 -0
  68. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/data_structure/kdtree.cu +0 -0
  69. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/data_structure/kdtree.h +0 -0
  70. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/data_structure/mesh_bvh.h +0 -0
  71. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/data_structure/pgs_bvh.h +0 -0
  72. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/data_structure/triangle_mesh.h +0 -0
  73. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/data_structure/zcurve.cu +0 -0
  74. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/data_structure/zcurve.h +0 -0
  75. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/maths/f2x2.h +0 -0
  76. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/maths/f3x3.h +0 -0
  77. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/maths/f3x4.h +0 -0
  78. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/maths/f4x1.h +0 -0
  79. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/maths/f4x4.h +0 -0
  80. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/maths/maths.h +0 -0
  81. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/ops/chamfer.cu +0 -0
  82. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/ops/chamfer.h +0 -0
  83. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/ops/dc_data.h +0 -0
  84. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/ops/dmc_data.h +0 -0
  85. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/ops/flood_fill.cu +0 -0
  86. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/ops/flood_fill.h +0 -0
  87. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/ops/flood_fill_cf.cu +0 -0
  88. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/ops/flood_fill_cf.h +0 -0
  89. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/ops/mc.h +0 -0
  90. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/ops/mc_data.h +0 -0
  91. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/ops/mca.h +0 -0
  92. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/ops/mca_data.h +0 -0
  93. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/ops/mt.h +0 -0
  94. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/ops/mt_data.h +0 -0
  95. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/ops/mtg.h +0 -0
  96. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/ops/mtg_data.h +0 -0
  97. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/ops/volint.cu +0 -0
  98. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/ops/volint.h +0 -0
  99. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/primitive/aabb.h +0 -0
  100. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/primitive/edge.h +0 -0
  101. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/primitive/gs.cu +0 -0
  102. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/primitive/gs.h +0 -0
  103. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/primitive/gs_aabb.cu +0 -0
  104. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/primitive/gs_math.cuh +0 -0
  105. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/primitive/pgs.cu +0 -0
  106. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/primitive/pgs.h +0 -0
  107. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/primitive/pgs_aabb.cu +0 -0
  108. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/primitive/pgs_math.cuh +0 -0
  109. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/primitive/ray.h +0 -0
  110. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/csrc/pybind.cpp +0 -0
  111. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/data/__init__.py +0 -0
  112. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/data/assets/__init__.py +0 -0
  113. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/data/assets/common.py +0 -0
  114. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/data/assets/iphigenia.py +0 -0
  115. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/data/collate/__init__.py +0 -0
  116. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/data/collate/mesh.py +0 -0
  117. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/data/collate/sparse_tensor.py +0 -0
  118. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/data/dataset/__init__.py +0 -0
  119. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/data/dataset/base_mesh.py +0 -0
  120. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/data/dataset/digit3d.py +0 -0
  121. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/data/dataset/digit3dmv.py +0 -0
  122. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/data/dataset/mesh.py +0 -0
  123. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/data/dataset/redwood.py +0 -0
  124. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/data/transform/__init__.py +0 -0
  125. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/data/transform/base.py +0 -0
  126. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/data/transform/ops.py +0 -0
  127. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/data/transform/vertex.py +0 -0
  128. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/data_structure/__init__.py +0 -0
  129. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/data_structure/bmesh.py +0 -0
  130. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/data_structure/grid.py +0 -0
  131. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/data_structure/sort.py +0 -0
  132. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/io/__init__.py +0 -0
  133. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/io/obj.py +0 -0
  134. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/io/off.py +0 -0
  135. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/io/ply.py +0 -0
  136. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/ops/delaunay_triangulation.py +0 -0
  137. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/ops/diff_marching_cubes.py +0 -0
  138. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/ops/diff_marching_tetrahedra_grid.py +0 -0
  139. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/ops/distance.py +0 -0
  140. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/ops/dpsr.py +0 -0
  141. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/ops/marching_cubes.py +0 -0
  142. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/ops/marching_cubes_asymptotic.py +0 -0
  143. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/ops/marching_tetrahedra.py +0 -0
  144. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/ops/marching_tetrahedra_grid.py +0 -0
  145. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/ops/volint.py +0 -0
  146. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/primitive/__init__.py +0 -0
  147. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/primitive/gs.py +0 -0
  148. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d/primitive/pgs.py +0 -0
  149. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d.egg-info/dependency_links.txt +0 -0
  150. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d.egg-info/not-zip-safe +0 -0
  151. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d.egg-info/requires.txt +0 -0
  152. {conquer3d-0.7.4 → conquer3d-0.7.6}/conquer3d.egg-info/top_level.txt +0 -0
  153. {conquer3d-0.7.4 → conquer3d-0.7.6}/setup.cfg +0 -0
  154. {conquer3d-0.7.4 → conquer3d-0.7.6}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conquer3d
3
- Version: 0.7.4
3
+ Version: 0.7.6
4
4
  Summary: Geometric Cuda Tool Box
5
5
  Author-email: Do Hoang Khoi <khoido8899@gmail.com>
6
6
  License-Expression: MIT
Binary file
@@ -21,7 +21,9 @@ std::tuple<torch::Tensor, torch::Tensor, std::optional<torch::Tensor>> dual_cont
21
21
  std::optional<torch::Tensor> colors,
22
22
  std::optional<torch::Tensor> voxel_vertices,
23
23
  float iso,
24
- bool quad_split
24
+ bool quad_split,
25
+ std::optional<torch::Tensor> edge_points,
26
+ std::optional<torch::Tensor> edge_normals
25
27
  ) {
26
28
  CHECK_INPUT(grid_vertices);
27
29
  CHECK_INPUT(voxels);
@@ -36,6 +38,20 @@ std::tuple<torch::Tensor, torch::Tensor, std::optional<torch::Tensor>> dual_cont
36
38
  if (voxel_vertices.has_value() && voxel_vertices.value().defined()) {
37
39
  CHECK_INPUT(voxel_vertices.value());
38
40
  }
41
+ if (edge_points.has_value() && edge_points.value().defined()) {
42
+ CHECK_INPUT(edge_points.value());
43
+ TORCH_CHECK(edge_points.value().dim() == 3 && edge_points.value().size(1) == 12 && edge_points.value().size(2) == 3,
44
+ "edge_points must have shape (M, 12, 3)");
45
+ TORCH_CHECK(edge_points.value().size(0) == voxels.size(0),
46
+ "edge_points must have one row per voxel");
47
+ }
48
+ if (edge_normals.has_value() && edge_normals.value().defined()) {
49
+ CHECK_INPUT(edge_normals.value());
50
+ TORCH_CHECK(edge_normals.value().dim() == 3 && edge_normals.value().size(1) == 12 && edge_normals.value().size(2) == 3,
51
+ "edge_normals must have shape (M, 12, 3)");
52
+ TORCH_CHECK(edge_normals.value().size(0) == voxels.size(0),
53
+ "edge_normals must have one row per voxel");
54
+ }
39
55
 
40
56
  return conquer3d::ops::dual_contouring(
41
57
  grid_vertices,
@@ -45,7 +61,9 @@ std::tuple<torch::Tensor, torch::Tensor, std::optional<torch::Tensor>> dual_cont
45
61
  colors,
46
62
  voxel_vertices,
47
63
  iso,
48
- quad_split
64
+ quad_split,
65
+ edge_points,
66
+ edge_normals
49
67
  );
50
68
  }
51
69
 
@@ -106,6 +124,7 @@ void bind_ops_dc(py::module &m) {
106
124
  py::arg("grid_normals") = py::none(), py::arg("colors") = py::none(),
107
125
  py::arg("voxel_vertices") = py::none(),
108
126
  py::arg("iso") = 0.0f, py::arg("quad_split") = true,
127
+ py::arg("edge_points") = py::none(), py::arg("edge_normals") = py::none(),
109
128
  R"pbdoc(
110
129
  Extracts a sharp-feature preserving surface mesh using Dual Contouring with GPU QEF solver (Ju et al. 2002) or precomputed voxel vertices.
111
130
 
@@ -21,7 +21,9 @@ std::tuple<torch::Tensor, torch::Tensor, std::optional<torch::Tensor>> dual_marc
21
21
  std::optional<torch::Tensor> voxel_vertices,
22
22
  float iso,
23
23
  bool quad_split,
24
- int project_iters
24
+ int project_iters,
25
+ std::optional<torch::Tensor> edge_points,
26
+ std::optional<torch::Tensor> edge_normals
25
27
  ) {
26
28
  CHECK_INPUT(grid_vertices);
27
29
  CHECK_INPUT(voxels);
@@ -33,6 +35,15 @@ std::tuple<torch::Tensor, torch::Tensor, std::optional<torch::Tensor>> dual_marc
33
35
  if (voxel_vertices.has_value() && voxel_vertices.value().defined()) {
34
36
  CHECK_INPUT(voxel_vertices.value());
35
37
  }
38
+ for (auto *t : {&edge_points, &edge_normals}) {
39
+ if (t->has_value() && t->value().defined()) {
40
+ CHECK_INPUT(t->value());
41
+ TORCH_CHECK(t->value().dim() == 3 && t->value().size(1) == 12 && t->value().size(2) == 3,
42
+ "edge_points/edge_normals must have shape (M, 12, 3)");
43
+ TORCH_CHECK(t->value().size(0) == voxels.size(0),
44
+ "edge_points/edge_normals must have one row per voxel");
45
+ }
46
+ }
36
47
 
37
48
  return conquer3d::ops::dual_marching_cubes(
38
49
  grid_vertices,
@@ -42,7 +53,9 @@ std::tuple<torch::Tensor, torch::Tensor, std::optional<torch::Tensor>> dual_marc
42
53
  voxel_vertices,
43
54
  iso,
44
55
  quad_split,
45
- project_iters
56
+ project_iters,
57
+ edge_points,
58
+ edge_normals
46
59
  );
47
60
  }
48
61
 
@@ -99,6 +112,7 @@ void bind_ops_dmc(py::module &m) {
99
112
  py::arg("grid_vertices"), py::arg("voxels"), py::arg("sdf"),
100
113
  py::arg("colors") = py::none(), py::arg("voxel_vertices") = py::none(),
101
114
  py::arg("iso") = 0.0f, py::arg("quad_split") = true, py::arg("project_iters") = 5,
115
+ py::arg("edge_points") = py::none(), py::arg("edge_normals") = py::none(),
102
116
  R"pbdoc(
103
117
  Extracts a watertight 2-manifold surface mesh using Differentiable Dual Marching Cubes (Schaefer & Warren 2004) or precomputed voxel vertices.
104
118
 
@@ -233,16 +233,8 @@ __global__ void filter_ray_triangle_intersections_kernel(
233
233
  float3 v2 = vertices[tri.z];
234
234
  float3 centroid = (v0 + v1 + v2) / 3.0f;
235
235
 
236
- float3 ray_dir = centroid - p;
237
- float dir_len = maths::norm(ray_dir);
238
- if (dir_len > 1e-6f)
239
- {
240
- ray_dir = ray_dir / dir_len;
241
- }
242
- else
243
- {
244
- ray_dir = make_float3(1.0f, 0.0f, 0.0f);
245
- }
236
+ float3 ray_dir = maths::normalize_safe(
237
+ centroid - p, make_float3(1.0f, 0.0f, 0.0f), 1e-6f);
246
238
 
247
239
  Ray ray2(p, ray_dir, 0.0f);
248
240
 
@@ -452,7 +444,7 @@ __global__ void filter_ray_triangle_intersections_kernel(
452
444
  float3 u0 = v1 - v0;
453
445
  float len2_0 = maths::dot2(u0);
454
446
  if (len2_0 > 1e-10f) {
455
- float t0 = fminf(fmaxf(maths::dot(best_pt - v0, u0) / len2_0, 0.0f), 1.0f);
447
+ float t0 = maths::saturate(maths::dot(best_pt - v0, u0) / len2_0);
456
448
  if (maths::dot2(v0 + t0 * u0 - best_pt) <= eps_sq) {
457
449
  N = pseudonormal_edges[3 * best_tri_id + 0];
458
450
  found = true;
@@ -463,7 +455,7 @@ __global__ void filter_ray_triangle_intersections_kernel(
463
455
  float3 u1 = v2 - v1;
464
456
  float len2_1 = maths::dot2(u1);
465
457
  if (len2_1 > 1e-10f) {
466
- float t1 = fminf(fmaxf(maths::dot(best_pt - v1, u1) / len2_1, 0.0f), 1.0f);
458
+ float t1 = maths::saturate(maths::dot(best_pt - v1, u1) / len2_1);
467
459
  if (maths::dot2(v1 + t1 * u1 - best_pt) <= eps_sq) {
468
460
  N = pseudonormal_edges[3 * best_tri_id + 1];
469
461
  found = true;
@@ -474,7 +466,7 @@ __global__ void filter_ray_triangle_intersections_kernel(
474
466
  float3 u2 = v0 - v2;
475
467
  float len2_2 = maths::dot2(u2);
476
468
  if (len2_2 > 1e-10f) {
477
- float t2 = fminf(fmaxf(maths::dot(best_pt - v2, u2) / len2_2, 0.0f), 1.0f);
469
+ float t2 = maths::saturate(maths::dot(best_pt - v2, u2) / len2_2);
478
470
  if (maths::dot2(v2 + t2 * u2 - best_pt) <= eps_sq) {
479
471
  N = pseudonormal_edges[3 * best_tri_id + 2];
480
472
  found = true;
@@ -524,9 +524,9 @@ __global__ void compute_vertex_normals_kernel(
524
524
  float3 e1 = maths::normalize(v2 - v1);
525
525
  float3 e2 = maths::normalize(v0 - v2);
526
526
 
527
- float a0 = acosf(fminf(fmaxf(-maths::dot(e0, e2), -1.0f), 1.0f));
528
- float a1 = acosf(fminf(fmaxf(-maths::dot(e1, e0), -1.0f), 1.0f));
529
- float a2 = acosf(fminf(fmaxf(-maths::dot(e2, e1), -1.0f), 1.0f));
527
+ float a0 = acosf(maths::clamp(-maths::dot(e0, e2), -1.0f, 1.0f));
528
+ float a1 = acosf(maths::clamp(-maths::dot(e1, e0), -1.0f, 1.0f));
529
+ float a2 = acosf(maths::clamp(-maths::dot(e2, e1), -1.0f, 1.0f));
530
530
 
531
531
  if (isfinite(a0)) {
532
532
  atomicAdd(&vertex_normals[tri.x].x, a0 * n.x);
@@ -260,6 +260,40 @@ namespace maths
260
260
  fminf(fmaxf(v.z, min_val.z), max_val.z)
261
261
  );
262
262
  }
263
+
264
+ /**
265
+ * @brief Linear interpolation between two vectors.
266
+ * @details Overloads the scalar lerp() in ops.h. Interpolating along an edge is the
267
+ * single most repeated operation in the extraction kernels -- a crossing point, an
268
+ * interpolated normal, a blended colour -- and naming it keeps those sites readable.
269
+ * @param[in] a Vector returned at $t = 0$.
270
+ * @param[in] b Vector returned at $t = 1$.
271
+ * @param[in] t Interpolation parameter; not clamped.
272
+ * @return $\mathbf{a} + t\,(\mathbf{b} - \mathbf{a})$.
273
+ */
274
+ static inline __host__ __device__ float3 lerp(float3 a, float3 b, float t) {
275
+ return a + (b - a) * t;
276
+ }
277
+
278
+ /**
279
+ * @brief Normalises a vector, falling back when it is too short to normalise.
280
+ * @details normalize() divides unconditionally and yields NaN or an infinity for a
281
+ * degenerate input. Kernels therefore guard the division by hand, and did so with three
282
+ * different epsilons across the tree. This states the policy once: below @p eps the
283
+ * direction is meaningless, so @p fallback is returned instead.
284
+ * @param[in] v Vector to normalise.
285
+ * @param[in] fallback Direction returned when @p v is shorter than @p eps.
286
+ * @param[in] eps Length below which @p v is treated as degenerate.
287
+ * @return The unit vector along @p v, or @p fallback.
288
+ */
289
+ static inline __host__ __device__ float3 normalize_safe(
290
+ float3 v,
291
+ float3 fallback = make_float3(0.0f, 0.0f, 1.0f),
292
+ float eps = 1e-8f)
293
+ {
294
+ float len = norm(v);
295
+ return (len > eps) ? (v * (1.0f / len)) : fallback;
296
+ }
263
297
  }
264
298
 
265
299
  /**
@@ -0,0 +1,89 @@
1
+ #ifndef OPS_H
2
+ #define OPS_H
3
+
4
+ #include <stdint.h>
5
+ #include <cmath>
6
+ #include <vector_types.h>
7
+ #include <vector_functions.h>
8
+ /**
9
+ * @file ops.h
10
+ * @brief Host fallbacks for CUDA math intrinsics, and scalar interpolation helpers.
11
+ *
12
+ * @details `rsqrt` and `rsqrtf` are device intrinsics that do not exist when a translation
13
+ * unit is compiled by the host compiler alone. These definitions appear only outside
14
+ * `__CUDACC__`, so headers shared between host and device code compile either way without
15
+ * `#ifdef` guards at every call site.
16
+ *
17
+ * The scalar helpers below are the counterparts of the `float3` routines in f3x1.h and
18
+ * overload on the same names, so `maths::clamp` and `maths::lerp` read identically whether
19
+ * the operand is a scalar or a vector. They live here rather than in f3x1.h because this
20
+ * header is included first and must not depend on the vector operators.
21
+ */
22
+
23
+ #include <math_constants.h>
24
+
25
+ #ifndef __CUDACC__
26
+ /**
27
+ * @brief Reciprocal square root, double precision (host fallback).
28
+ * @param[in] a Value whose reciprocal square root is taken; must be positive.
29
+ * @return The value $1 / \sqrt{a}$.
30
+ * @note Defined only when not compiling with nvcc, which supplies the intrinsic.
31
+ */
32
+ static inline __host__ __device__ double rsqrt(double a) {
33
+ return 1. / sqrt(a);
34
+ }
35
+
36
+ /**
37
+ * @brief Reciprocal square root, single precision (host fallback).
38
+ * @param[in] a Value whose reciprocal square root is taken; must be positive.
39
+ * @return The value $1 / \sqrt{a}$.
40
+ * @note Defined only when not compiling with nvcc. The device intrinsic is an
41
+ * approximation, so host and device results may differ in the last bits.
42
+ */
43
+ static inline __host__ __device__ float rsqrtf(float a) {
44
+ return 1. / sqrtf(a);
45
+ }
46
+ #endif
47
+
48
+ namespace maths
49
+ {
50
+ /**
51
+ * @brief Confines a scalar to a range.
52
+ * @details Scalar overload of the `float3` clamp in f3x1.h. Spelling the two branches
53
+ * as one call keeps the intent legible where the bound is itself an expression.
54
+ * @param[in] v Value to clamp.
55
+ * @param[in] min_val Lower bound.
56
+ * @param[in] max_val Upper bound.
57
+ * @return @p v confined to $[\text{min\_val}, \text{max\_val}]$.
58
+ * @note Ordered `fmaxf(lo, fminf(hi, v))` to match the form these call sites already
59
+ * used. The order is irrelevant for finite input but decides which bound a NaN
60
+ * collapses to, so keeping it preserves the previous behaviour exactly.
61
+ */
62
+ static inline __host__ __device__ float clamp(float v, float min_val, float max_val) {
63
+ return fmaxf(min_val, fminf(max_val, v));
64
+ }
65
+
66
+ /**
67
+ * @brief Confines a scalar to the unit interval.
68
+ * @details The overwhelmingly common case of clamp(), used wherever an interpolation
69
+ * parameter or a normalised cell coordinate must not escape $[0, 1]$ through rounding.
70
+ * @param[in] v Value to clamp.
71
+ * @return @p v confined to $[0, 1]$.
72
+ */
73
+ static inline __host__ __device__ float saturate(float v) {
74
+ return fmaxf(0.0f, fminf(1.0f, v));
75
+ }
76
+
77
+ /**
78
+ * @brief Linear interpolation between two scalars.
79
+ * @param[in] a Value returned at $t = 0$.
80
+ * @param[in] b Value returned at $t = 1$.
81
+ * @param[in] t Interpolation parameter; not clamped.
82
+ * @return $a + t\,(b - a)$.
83
+ */
84
+ static inline __host__ __device__ float lerp(float a, float b, float t) {
85
+ return a + (b - a) * t;
86
+ }
87
+ }
88
+
89
+ #endif // OPS_H
@@ -144,6 +144,7 @@ __host__ __device__ __forceinline__ void symmetric_eigen_3x3(
144
144
  * @param cell_min Minimum AABB coordinate of the voxel cell.
145
145
  * @param cell_max Maximum AABB coordinate of the voxel cell.
146
146
  * @param svd_tolerance Relative eigenvalue threshold for pseudoinverse (default: 0.01).
147
+ * @param cell_expand Scale applied to the cell half-extents before clamping (default: 2.0).
147
148
  * @return Optimal inner vertex position x*.
148
149
  */
149
150
  __host__ __device__ __forceinline__ float3 solve_qef(
@@ -152,12 +153,18 @@ __host__ __device__ __forceinline__ float3 solve_qef(
152
153
  int count,
153
154
  const float3 &cell_min,
154
155
  const float3 &cell_max,
155
- float svd_tolerance = 0.01f
156
+ float svd_tolerance = 0.01f,
157
+ float cell_expand = 2.0f
156
158
  ) {
157
159
  if (count <= 0) {
158
160
  return (cell_min + cell_max) * 0.5f;
159
161
  }
160
162
 
163
+ const float3 cell_centre = (cell_min + cell_max) * 0.5f;
164
+ const float3 cell_half = (cell_max - cell_min) * (0.5f * cell_expand);
165
+ const float3 clamp_min = cell_centre - cell_half;
166
+ const float3 clamp_max = cell_centre + cell_half;
167
+
161
168
  // 1. Compute Mass Point (Centroid)
162
169
  float3 mass_point = make_float3(0.0f, 0.0f, 0.0f);
163
170
  for (int i = 0; i < count; ++i) {
@@ -166,7 +173,7 @@ __host__ __device__ __forceinline__ float3 solve_qef(
166
173
  mass_point = mass_point * (1.0f / (float)count);
167
174
 
168
175
  if (count == 1) {
169
- return maths::clamp(pts[0], cell_min, cell_max);
176
+ return maths::clamp(pts[0], clamp_min, clamp_max);
170
177
  }
171
178
 
172
179
  // 2. Build Shifted Normal Equation System: (A^T A) y = b_tilde
@@ -224,30 +231,13 @@ __host__ __device__ __forceinline__ float3 solve_qef(
224
231
  y = y + v2 * proj;
225
232
  }
226
233
 
227
- // 5. Unshift and Clamp to Voxel AABB
234
+ // 5. Unshift and Clamp to the (optionally widened) Voxel AABB
228
235
  float3 x = mass_point + y;
229
- x = maths::clamp(x, cell_min, cell_max);
236
+ x = maths::clamp(x, clamp_min, clamp_max);
230
237
 
231
238
  return x;
232
239
  }
233
240
 
234
- /**
235
- * @brief Evaluates the residual Quadratic Error Function (QEF) value at point x.
236
- */
237
- __host__ __device__ __forceinline__ float evaluate_qef_error(
238
- const float3 *pts,
239
- const float3 *normals,
240
- int count,
241
- const float3 &x
242
- ) {
243
- float error = 0.0f;
244
- for (int i = 0; i < count; ++i) {
245
- float d = maths::dot(normals[i], x - pts[i]);
246
- error += d * d;
247
- }
248
- return error;
249
- }
250
-
251
241
  } // namespace maths
252
242
 
253
243
  #endif // QEF_H
@@ -56,11 +56,7 @@ __device__ __forceinline__ float3 compute_trilinear_normal(
56
56
  v * ((1.0f - u) * (s[7] - s[3]) + u * (s[6] - s[2]));
57
57
 
58
58
  float3 grad = make_float3(du / dx, dv / dy, dw / dz);
59
- float len = maths::norm(grad);
60
- if (len > 1e-8f) {
61
- return grad * (1.0f / len);
62
- }
63
- return make_float3(0.0f, 0.0f, 1.0f);
59
+ return maths::normalize_safe(grad);
64
60
  }
65
61
 
66
62
  // Compute minimum angle of a 3D triangle in radians
@@ -91,9 +87,9 @@ __device__ __forceinline__ float triangle_min_angle(
91
87
  float cos1 = -maths::dot(e0, e1) / (l0 * l1);
92
88
  float cos2 = -maths::dot(e1, e2) / (l1 * l2);
93
89
 
94
- cos0 = fmaxf(-1.0f, fminf(1.0f, cos0));
95
- cos1 = fmaxf(-1.0f, fminf(1.0f, cos1));
96
- cos2 = fmaxf(-1.0f, fminf(1.0f, cos2));
90
+ cos0 = maths::clamp(cos0, -1.0f, 1.0f);
91
+ cos1 = maths::clamp(cos1, -1.0f, 1.0f);
92
+ cos2 = maths::clamp(cos2, -1.0f, 1.0f);
97
93
 
98
94
  return fminf(acosf(cos0), fminf(acosf(cos1), acosf(cos2)));
99
95
  }
@@ -138,6 +134,8 @@ __global__ void compute_dual_vertices_kernel(
138
134
  const int *__restrict__ voxels,
139
135
  const float *__restrict__ sdf,
140
136
  const float3 *__restrict__ grid_normals,
137
+ const float3 *__restrict__ edge_points,
138
+ const float3 *__restrict__ edge_normals,
141
139
  float iso,
142
140
  int num_voxels,
143
141
  float3 *__restrict__ dual_vertices,
@@ -182,22 +180,36 @@ __global__ void compute_dual_vertices_kernel(
182
180
  // Bipolar test
183
181
  if ((s0 < iso && s1 >= iso) || (s0 >= iso && s1 < iso)) {
184
182
  float t = (iso - s0) / (s1 - s0);
185
- t = fmaxf(0.0f, fminf(1.0f, t));
183
+ t = maths::saturate(t);
186
184
 
187
- float3 pt = p[v0] + (p[v1] - p[v0]) * t;
185
+ float3 pt;
186
+ float3 n;
187
+ bool have_hermite = (edge_points != nullptr && edge_normals != nullptr);
188
+ if (have_hermite) {
189
+ pt = edge_points[m * 12 + e];
190
+ float3 hn = edge_normals[m * 12 + e];
191
+ float hlen = maths::norm(hn);
192
+ if (hlen > 1e-8f) {
193
+ n = hn * (1.0f / hlen);
194
+ } else {
195
+ have_hermite = false;
196
+ }
197
+ }
198
+ if (!have_hermite) {
199
+ pt = maths::lerp(p[v0], p[v1], t);
200
+ }
188
201
  pts[count] = pt;
189
202
 
190
- float3 n;
191
- if (grid_normals != nullptr) {
203
+ if (have_hermite) {
204
+ // normal already set from the supplied Hermite data
205
+ } else if (grid_normals != nullptr) {
192
206
  float3 n0 = grid_normals[c_idx[v0]];
193
207
  float3 n1 = grid_normals[c_idx[v1]];
194
- float3 n_interp = n0 + (n1 - n0) * t;
195
- float len = maths::norm(n_interp);
196
- n = (len > 1e-8f) ? (n_interp * (1.0f / len)) : make_float3(0, 0, 1);
208
+ n = maths::normalize_safe(maths::lerp(n0, n1, t));
197
209
  } else {
198
- float u = dc_corner_uvw[v0][0] + (dc_corner_uvw[v1][0] - dc_corner_uvw[v0][0]) * t;
199
- float v = dc_corner_uvw[v0][1] + (dc_corner_uvw[v1][1] - dc_corner_uvw[v0][1]) * t;
200
- float w = dc_corner_uvw[v0][2] + (dc_corner_uvw[v1][2] - dc_corner_uvw[v0][2]) * t;
210
+ float u = maths::lerp(dc_corner_uvw[v0][0], dc_corner_uvw[v1][0], t);
211
+ float v = maths::lerp(dc_corner_uvw[v0][1], dc_corner_uvw[v1][1], t);
212
+ float w = maths::lerp(dc_corner_uvw[v0][2], dc_corner_uvw[v1][2], t);
201
213
  n = compute_trilinear_normal(u, v, w, s, dx, dy, dz);
202
214
  }
203
215
  normals[count] = n;
@@ -489,9 +501,9 @@ __global__ void compact_dual_vertices_and_colors_kernel(
489
501
  float dy = fmaxf(c_max.y - c_min.y, 1e-6f);
490
502
  float dz = fmaxf(c_max.z - c_min.z, 1e-6f);
491
503
 
492
- float u = fmaxf(0.0f, fminf(1.0f, (v.x - c_min.x) / dx));
493
- float val_v = fmaxf(0.0f, fminf(1.0f, (v.y - c_min.y) / dy));
494
- float w = fmaxf(0.0f, fminf(1.0f, (v.z - c_min.z) / dz));
504
+ float u = maths::saturate((v.x - c_min.x) / dx);
505
+ float val_v = maths::saturate((v.y - c_min.y) / dy);
506
+ float w = maths::saturate((v.z - c_min.z) / dz);
495
507
 
496
508
  #pragma unroll
497
509
  for (int ch = 0; ch < num_channels; ++ch) {
@@ -665,7 +677,9 @@ std::tuple<at::Tensor, at::Tensor, c10::optional<at::Tensor>> dual_contouring(
665
677
  const c10::optional<at::Tensor> &colors,
666
678
  const c10::optional<at::Tensor> &voxel_vertices,
667
679
  float iso,
668
- bool quad_split
680
+ bool quad_split,
681
+ const c10::optional<at::Tensor> &edge_points,
682
+ const c10::optional<at::Tensor> &edge_normals
669
683
  ) {
670
684
  at::cuda::CUDAGuard device_guard(grid_vertices.device());
671
685
  auto allocator = at::cuda::ThrustAllocator();
@@ -705,12 +719,18 @@ std::tuple<at::Tensor, at::Tensor, c10::optional<at::Tensor>> dual_contouring(
705
719
  dual_vertices = at::empty({num_voxels, 3}, grid_vertices.options());
706
720
  source_dual_vertices_ptr = reinterpret_cast<const float3*>(dual_vertices.data_ptr<float>());
707
721
  const float3 *normals_ptr = grid_normals.has_value() ? reinterpret_cast<const float3*>(grid_normals.value().data_ptr<float>()) : nullptr;
722
+ const float3 *ep_ptr = (edge_points.has_value() && edge_points.value().defined() && edge_points.value().numel() > 0)
723
+ ? reinterpret_cast<const float3*>(edge_points.value().data_ptr<float>()) : nullptr;
724
+ const float3 *en_ptr = (edge_normals.has_value() && edge_normals.value().defined() && edge_normals.value().numel() > 0)
725
+ ? reinterpret_cast<const float3*>(edge_normals.value().data_ptr<float>()) : nullptr;
708
726
 
709
727
  compute_dual_vertices_kernel<<<blocks, threads, 0, at::cuda::getCurrentCUDAStream()>>>(
710
728
  reinterpret_cast<const float3*>(grid_vertices.data_ptr<float>()),
711
729
  voxels.data_ptr<int>(),
712
730
  sdf.data_ptr<float>(),
713
731
  normals_ptr,
732
+ ep_ptr,
733
+ en_ptr,
714
734
  iso,
715
735
  num_voxels,
716
736
  reinterpret_cast<float3*>(dual_vertices.data_ptr<float>()),
@@ -910,6 +930,8 @@ std::tuple<at::Tensor, c10::optional<at::Tensor>> dual_contouring_backward(
910
930
  voxels.data_ptr<int>(),
911
931
  sdf.data_ptr<float>(),
912
932
  normals_ptr,
933
+ nullptr,
934
+ nullptr,
913
935
  iso,
914
936
  num_voxels,
915
937
  reinterpret_cast<float3*>(dual_vertices.data_ptr<float>()),
@@ -30,7 +30,9 @@ std::tuple<at::Tensor, at::Tensor, c10::optional<at::Tensor>> dual_contouring(
30
30
  const c10::optional<at::Tensor> &colors = c10::nullopt,
31
31
  const c10::optional<at::Tensor> &voxel_vertices = c10::nullopt,
32
32
  float iso = 0.0f,
33
- bool quad_split = true
33
+ bool quad_split = true,
34
+ const c10::optional<at::Tensor> &edge_points = c10::nullopt,
35
+ const c10::optional<at::Tensor> &edge_normals = c10::nullopt
34
36
  );
35
37
 
36
38
  /**
@@ -111,9 +111,9 @@ __device__ __forceinline__ float triangle_min_angle(
111
111
  float cos1 = -maths::dot(e0, e1) / (l0 * l1);
112
112
  float cos2 = -maths::dot(e1, e2) / (l1 * l2);
113
113
 
114
- cos0 = fmaxf(-1.0f, fminf(1.0f, cos0));
115
- cos1 = fmaxf(-1.0f, fminf(1.0f, cos1));
116
- cos2 = fmaxf(-1.0f, fminf(1.0f, cos2));
114
+ cos0 = maths::clamp(cos0, -1.0f, 1.0f);
115
+ cos1 = maths::clamp(cos1, -1.0f, 1.0f);
116
+ cos2 = maths::clamp(cos2, -1.0f, 1.0f);
117
117
 
118
118
  float a0 = acosf(cos0);
119
119
  float a1 = acosf(cos1);
@@ -368,6 +368,8 @@ __global__ void dmc_extract_dual_vertices_and_edges_kernel(
368
368
  const float *__restrict__ sdf,
369
369
  const float *__restrict__ colors,
370
370
  const float3 *__restrict__ precomputed_vertices,
371
+ const float3 *__restrict__ edge_points,
372
+ const float3 *__restrict__ edge_normals,
371
373
  int num_channels,
372
374
  const int *__restrict__ vert_offsets,
373
375
  const int *__restrict__ edge_offsets,
@@ -436,18 +438,32 @@ __global__ void dmc_extract_dual_vertices_and_edges_kernel(
436
438
  int dual_vert_idx = v_base + c;
437
439
  float u_sum = 0.0f, v_sum = 0.0f, w_sum = 0.0f;
438
440
 
441
+ const bool use_hermite = (edge_points != nullptr && edge_normals != nullptr);
442
+ float3 h_pts[12];
443
+ float3 h_nrm[12];
444
+ int h_count = 0;
445
+
439
446
  for (int i = 0; i < sz; ++i) {
440
447
  int e = c_edges[c][i];
448
+ if (use_hermite && h_count < 12) {
449
+ float3 hn = edge_normals[m * 12 + e];
450
+ float hlen = maths::norm(hn);
451
+ if (hlen > 1e-8f) {
452
+ h_pts[h_count] = edge_points[m * 12 + e];
453
+ h_nrm[h_count] = hn * (1.0f / hlen);
454
+ h_count++;
455
+ }
456
+ }
441
457
  int v0 = dmc_edge_corners[e][0];
442
458
  int v1 = dmc_edge_corners[e][1];
443
459
  float s0 = s[v0];
444
460
  float s1 = s[v1];
445
461
  float t = (iso - s0) / (s1 - s0);
446
- t = fmaxf(0.0f, fminf(1.0f, t));
462
+ t = maths::saturate(t);
447
463
 
448
- float u_e = dmc_corner_uvw[v0][0] + (dmc_corner_uvw[v1][0] - dmc_corner_uvw[v0][0]) * t;
449
- float v_e = dmc_corner_uvw[v0][1] + (dmc_corner_uvw[v1][1] - dmc_corner_uvw[v0][1]) * t;
450
- float w_e = dmc_corner_uvw[v0][2] + (dmc_corner_uvw[v1][2] - dmc_corner_uvw[v0][2]) * t;
464
+ float u_e = maths::lerp(dmc_corner_uvw[v0][0], dmc_corner_uvw[v1][0], t);
465
+ float v_e = maths::lerp(dmc_corner_uvw[v0][1], dmc_corner_uvw[v1][1], t);
466
+ float w_e = maths::lerp(dmc_corner_uvw[v0][2], dmc_corner_uvw[v1][2], t);
451
467
 
452
468
  u_sum += u_e;
453
469
  v_sum += v_e;
@@ -473,9 +489,19 @@ __global__ void dmc_extract_dual_vertices_and_edges_kernel(
473
489
  // Fast path: use precomputed inside-voxel vertex directly
474
490
  out_vertices[dual_vert_idx] = precomputed_vertices[m];
475
491
  float3 pt = precomputed_vertices[m];
476
- u = fmaxf(0.0f, fminf(1.0f, (pt.x - c_min.x) / dx));
477
- v = fmaxf(0.0f, fminf(1.0f, (pt.y - c_min.y) / dy));
478
- w = fmaxf(0.0f, fminf(1.0f, (pt.z - c_min.z) / dz));
492
+ u = maths::saturate((pt.x - c_min.x) / dx);
493
+ v = maths::saturate((pt.y - c_min.y) / dy);
494
+ w = maths::saturate((pt.z - c_min.z) / dz);
495
+ } else if (use_hermite && h_count > 0) {
496
+ // Solve the quadratic error function over THIS contour's Hermite data.
497
+ // Restricting the solve to one contour is what keeps a cell that carries
498
+ // several contours emitting several distinct vertices, so the manifold
499
+ // guarantee survives while each vertex still lands on its own feature.
500
+ float3 pt = maths::solve_qef(h_pts, h_nrm, h_count, c_min, c_max, 0.01f);
501
+ out_vertices[dual_vert_idx] = pt;
502
+ u = maths::saturate((pt.x - c_min.x) / dx);
503
+ v = maths::saturate((pt.y - c_min.y) / dy);
504
+ w = maths::saturate((pt.z - c_min.z) / dz);
479
505
  } else {
480
506
  // Newton-Raphson Level-Set Projection
481
507
  for (int iter = 0; iter < project_iters; ++iter) {
@@ -484,9 +510,9 @@ __global__ void dmc_extract_dual_vertices_and_edges_kernel(
484
510
  float len_sq = grad.x * grad.x + grad.y * grad.y + grad.z * grad.z;
485
511
  if (len_sq > 1e-8f) {
486
512
  float step = 0.5f * (iso - f_val) / len_sq;
487
- u = fmaxf(0.0f, fminf(1.0f, u + step * grad.x));
488
- v = fmaxf(0.0f, fminf(1.0f, v + step * grad.y));
489
- w = fmaxf(0.0f, fminf(1.0f, w + step * grad.z));
513
+ u = maths::saturate(u + step * grad.x);
514
+ v = maths::saturate(v + step * grad.y);
515
+ w = maths::saturate(w + step * grad.z);
490
516
  }
491
517
  }
492
518
 
@@ -802,7 +828,9 @@ std::tuple<at::Tensor, at::Tensor, c10::optional<at::Tensor>> dual_marching_cube
802
828
  const c10::optional<at::Tensor> &voxel_vertices,
803
829
  float iso,
804
830
  bool quad_split,
805
- int project_iters
831
+ int project_iters,
832
+ const c10::optional<at::Tensor> &edge_points,
833
+ const c10::optional<at::Tensor> &edge_normals
806
834
  ) {
807
835
  TORCH_CHECK(grid_vertices.is_cuda(), "grid_vertices must be a CUDA tensor");
808
836
  TORCH_CHECK(voxels.is_cuda(), "voxels must be a CUDA tensor");
@@ -889,12 +917,19 @@ std::tuple<at::Tensor, at::Tensor, c10::optional<at::Tensor>> dual_marching_cube
889
917
  ? reinterpret_cast<const float3*>(voxel_vertices->data_ptr<float>())
890
918
  : nullptr;
891
919
 
920
+ const float3 *dmc_ep_ptr = (edge_points.has_value() && edge_points->defined() && edge_points->numel() > 0)
921
+ ? reinterpret_cast<const float3*>(edge_points->data_ptr<float>()) : nullptr;
922
+ const float3 *dmc_en_ptr = (edge_normals.has_value() && edge_normals->defined() && edge_normals->numel() > 0)
923
+ ? reinterpret_cast<const float3*>(edge_normals->data_ptr<float>()) : nullptr;
924
+
892
925
  dmc_extract_dual_vertices_and_edges_kernel<<<grid, block, 0, stream>>>(
893
926
  reinterpret_cast<const float3*>(grid_vertices.data_ptr<float>()),
894
927
  voxels.data_ptr<int>(),
895
928
  sdf.data_ptr<float>(),
896
929
  colors_ptr,
897
930
  precomputed_ptr,
931
+ dmc_ep_ptr,
932
+ dmc_en_ptr,
898
933
  num_channels,
899
934
  vert_offsets.data_ptr<int>(),
900
935
  edge_offsets.data_ptr<int>(),
@@ -29,7 +29,9 @@ std::tuple<at::Tensor, at::Tensor, c10::optional<at::Tensor>> dual_marching_cube
29
29
  const c10::optional<at::Tensor> &voxel_vertices = c10::nullopt,
30
30
  float iso = 0.0f,
31
31
  bool quad_split = true,
32
- int project_iters = 5
32
+ int project_iters = 5,
33
+ const c10::optional<at::Tensor> &edge_points = c10::nullopt,
34
+ const c10::optional<at::Tensor> &edge_normals = c10::nullopt
33
35
  );
34
36
 
35
37
  /**