PAYATHON 2026

使用 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 的构建差异需要结合具体报错排查,不能据此认定网格噪声来自构建模式。

备注:内容仅供参考。