使用 CGAL 对 OpenVDB 文件进行网格化时,如何解决网格噪声问题?
优先检查 relative_error_bound 和 PointSampler。前者可能使细长物体的表面交点定位不够精确,后者会让离散 SDF 的采样结果呈阶梯状。建议先显式减小交点误差,再改用三线性插值,最后调整网格细化参数。单纯提高体素分辨率,未必能解决这两类问题。
没有实际的 .vdb 数据和网格截图,还无法确定主要原因,可以先按这两个方向排查。
为什么高长宽比物体更容易出现噪声?
交点定位误差取决于整体尺寸
Labeled_mesh_domain_3 通过二分法定位查询线段与表面的交点。relative_error_bound 控制二分法的终止误差,默认值为 1e-3,误差尺度与包围对象的整体尺寸有关。CGAL 参数文档
设包围域的特征尺寸为 (D),相对误差参数为 (e),交点误差的数量级可以理解为:
[ \varepsilon_{\text{intersection}} = O(eD) ]
对于细长圆柱,(D) 主要由长度决定,而可接受的表面起伏往往由半径决定。因此,同样的 relative_error_bound 用在细长物体上,可能过于宽松。
增加体素分辨率后,物体的世界空间尺寸基本不变,交点定位的绝对误差尺度也不会随之缩小,噪声可能因此保留下来。这一解释符合代码行为,但仍需修改参数来验证。
facet_distance 和交点误差控制的是不同环节,仅减小 facet_distance 不能替代提高交点定位精度。
PointSampler 会产生不连续的采样场
原代码使用:
float val = openvdb::tools::PointSampler::sample(accessor, idx);
PointSampler 使用最近邻采样。查询点在同一体素附近移动时,返回值保持不变;跨过采样分界后,数值就会跳变。因此,即使体素中保存的是 SDF,提供给 CGAL 的函数仍然呈阶梯状。
可以改用 BoxSampler,在相邻八个体素之间进行三线性插值。这里的 Box 指插值方式,不是对整个模型做平滑滤波。OpenVDB 插值文档
三线性插值能改善连续性,但无法恢复输入体素没有记录的几何细节。
如何修改代码?
建议分两次做对照:先只减小 relative_error_bound,再替换采样器,其余参数保持一致,以便判断两项修改各自的影响。
以下代码同时采用这两项修改,可替换原来的隐式域构造部分,沿用已有的 grid、boundingBox 和类型定义:
#include <openvdb/tools/Interpolation.h>
// grid 必须在采样和网格生成期间保持有效。
// 使用 Grid 类型的 GridSampler,通过树读取,避免共享缓存 accessor。
openvdb::tools::GridSampler<
openvdb::FloatGrid,
openvdb::tools::BoxSampler
> sampler(*grid);
auto implicitFunction = [&sampler](const K::Point_3& p) -> K::FT {
const openvdb::Vec3d worldPoint(p.x(), p.y(), p.z());
// wsSample 接收世界坐标,并在内部转换到索引坐标。
const float value = sampler.wsSample(worldPoint);
return static_cast<K::FT>(value);
};
Mesh_domain domain = Mesh_domain::create_implicit_mesh_domain(
implicitFunction,
boundingBox,
params::relative_error_bound(1e-6)
);
// 排查阶段先保留原来的 criteria。
C3t3 c3t3 = CGAL::make_mesh_3<C3t3>(domain, criteria);
可以从 1e-6 开始对照测试,具体取值需要结合模型调整。逐步收紧参数,观察表面起伏是否收敛,同时记录运行时间。交点定位精度应高于希望达到的表面近似精度,不过,继续收紧参数能带来的改善也受输入数据精度限制。
为什么去掉 value * voxelSize?
OpenVDB 标准 level set 工具通常按世界空间单位保存距离值,球体生成器就采用这一约定。自定义生成的文件需要另外核对数据定义。OpenVDB 距离值实现
对于这里使用的零等值面,乘以正数既不改变零点,也不改变内外符号:
[ f(p)=0 \iff h f(p)=0,\qquad h>0 ]
因此,value * voxelSize 通常不是这类噪声的直接原因,也不能提高表面定位精度。直接返回采样值,单位含义更清楚。
网格参数应该怎样调整?
原代码中的尺寸设置需要结合物体的横截面判断是否合适:
.facet_size(voxelSize * 300)
.facet_distance(voxelSize * 3)
.cell_size(voxelSize * 300)
即使物体很长,只要半径较小,表面单元尺寸仍需足够小,才能表示横截面。
| 参数 | 控制内容 |
|---|---|
facet_size | 表面 Delaunay 球的半径上界,并非直接指定三角形边长 |
facet_distance | 面片外心与对应表面 Delaunay 球球心之间的距离上界 |
cell_size | 四面体外接球半径上界 |
cell_radius_edge_ratio | 四面体的半径边长比上界 |
尺寸和距离参数使用网格域坐标的单位,这里就是世界坐标单位。CGAL 网格准则文档
排查时可以适当减小 facet_size 和 facet_distance,但应先处理采样和交点定位问题。否则,更密的网格可能只是更详细地呈现已有起伏。
原代码还把网格准则与 voxelSize 绑定,因此改变体素分辨率时,也会改变输出网格的尺寸约束。要单独比较输入分辨率的影响,应固定世界空间中的网格准则。
还需要检查哪些输入条件?
- 确认读入的是目标 SDF。
.vdb文件可能包含多个 grid,读取第一个 grid 不保证得到所需数据。还应检查类型转换结果,确认数据采用内部负、外部正、表面为零值的约定。 - 保留非活动区域的符号。标准窄带 level set 的非活动内部区域仍保存负值,外部背景保存正值。不要仅根据 active 状态判断内外,也不要把所有非活动值替换成零。OpenVDB level set 说明
- 检查包围盒。仅对索引包围盒的
min、max两点调用indexToWorld(),适用于轴对齐、各轴正向缩放和平移的情况。对于旋转或剪切的仿射变换,应转换八个角点,再计算世界空间包围盒;非线性变换需要另行处理。 - 确认包围域满足要求。包围域应包住完整零等值面。
create_implicit_mesh_domain()的文档还要求其中心位于内部,对于凹形或多分量模型,需要检查这一条件是否成立。CGAL 隐式域要求
原代码只使用 voxelSize()[0],隐含了体素尺寸均匀的假设。如果三个方向的尺度不同,应按实际世界空间需求设置尺寸准则。
是否有其他网格化路径?
可以先用 OpenVDB 提取零等值面,再交给 CGAL 处理:
#include <openvdb/tools/VolumeToMesh.h>
#include <vector>
std::vector<openvdb::Vec3s> points;
std::vector<openvdb::Vec4I> quads;
openvdb::tools::volumeToMesh(
*grid,
points,
quads,
0.0
);
这个重载输出四边形曲面,points 已是世界空间坐标,无需再次执行 indexToWorld()。OpenVDB volumeToMesh 文档
后续处理取决于目标:
- 只需要曲面网格时,可将结果转为 CGAL 的曲面数据结构,再进行三角化、修复或重网格化。
- 需要四面体体网格时,可先得到封闭、无自交的三角曲面,再使用
Polyhedral_mesh_domain_with_features_3构造域并调用make_mesh_3。需要保留尖锐边时,可使用其特征检测功能,并设置相应的特征边准则。CGAL 多面体域文档
这条路径也可以用来做对照。如果 OpenVDB 提取的曲面平整,而直接构造隐式域的结果出现起伏,应继续检查 CGAL 的交点容差、采样和网格准则。如果两者都出现相近起伏,再检查输入 SDF、体素尺度和生成过程。
上述代码是修改示例,尚未针对给定 .vdb 文件编译运行。Debug 与 Release 的构建差异需要结合具体报错排查,不能据此认定网格噪声来自构建模式。
备注:内容仅供参考。
