国外CAD设计软件,比如CATIA、UG/NX,被广泛应用于各行各业,也催生出了大量基于成熟平台进行二次开发的工作。这里记录一个UG/NX平台上的一个Ufun接口的使用问题、解决方案和一些思考。
1. 需求描述
需求是这样的:给定距离曲面很近的一个参考点,计算曲面上的最近点及其法向量。这个需求非常简单,计算过程是:(1) 根据参考点反求在该曲面上的uv参数,(2) 根据曲面表示和uv参数,正向计算此处的点坐标和法向量。但是,在第(2)步出现问题,主要代码如下:
//输入
NXOpen::Face* pNXFace; //给定曲面
double coordRef[3]; //给定参考点坐标
//获取uv参数过程略
double uv_pair[2] = { 0.0 };
//初始化计算器
UF_EVALSF_p_t evaluator;
UF_EVALSF_initialize(pNXFace->Tag(), &evaluator);
//计算uv参数上的信息
int deriv_flag = UF_MODL_EVAL_ALL;
UF_MODL_SRF_VALUE_p_t surf_eval = new UF_MODL_SRF_VALUE_s();
UF_EVALSF_evaluate(evaluator, deriv_flag, uv_pair, surf_eval);
//检查结果,tol为容许的最大误差
if (CalDistance(coordRef, surf_eval->srf_pos) > tol)
{
return -1;
}
问题出在第35行,本来参考点距离曲面非常近,最近点和参考点的距离也应该很小,但实际距离大于设定的误差,导致返回错误码(-1)。
2. 深入探究
一开始,这样的问题是让我莫名其妙的,这么强大的CAD平台上的这么简单的功能,怎么会出错呢?接下来就是怎么解决这个问题了,有两种思路:(1) 是不是使用方法有误,比如检查输入条件;(2) 寻找其它相似接口。我简单地看了一下UF_EVALSF_evaluate函数的介绍,提示这个函数可能会在某些场景下有问题,可以尝试UF_MODL_evaluate_face和 UF_MODL_ask_face_props函数。后面就分别写了这三种函数的实现,发现后两个函数的结果是一致的、正确的,也就是说,可以采用这两个函数实现以上功能。
但是,前面一个函数存在的意义是什么?目前猜测可能有两种考虑:(1) 该函数是功能版本,为了兼容以前的应用,所以保留了下来;(2) 该函数为了某个特定场景而开发。有时候确认这样的问题是无从下手的,甚至是无意义的,毕竟无法看到源码,只能从接口介绍里寻找蛛丝马迹。接下来开始系统地看该函数所在的文件,不得不说,NX接口的介绍是很完善的,在所在文件开头写着:
File description:
Open API interface to functions that evaluate surfaces. These functions are the preferred way for fast evaluation of faces and underlying surfaces.
Note that UF_EVALSF_evaluate and UF_EVALSF_evaluate_array may not properly handle parameter values returned by UF_EVALSF_find_closest_point or similar functions for surface types Blend or Trimmed B-surface. The functions UF_MODL_evaluate_face or UF_MODL_ask_face_props should be called instead.
Note - please use the new UF_EVALSF_initialize_2 to avoid above problem.
到这里,前面的两个问题都有了答案:(1) 该函数是为了效率而开发的;(2) 问题的解决办法不在于UF_EVALSF_evaluate函数,而是将初始化函数UF_EVALSF_initialize替换成UF_EVALSF_initialize_2函数。替换后,果然结果就正确了,与另外两个接口的结果一致。效率是CAD软件开发者非常关心的,那么就比较一下三者的计算效率,代码如下:
int TestNXFaceEvaluateEfficiency(NXOpen::Face* pNXFace)
{
int numRow = 100;
int numCol = 100;
int stepRow = 1.0/numRow;
int stepCol = 1.0/numCol;
std::vector<double> vctTestUParm;
std::vector<double> vctTestVParm;
for (int i = 0; i <= numRow; i++)
{
for (int j = 0; j <= numCol; j++)
{
double uParm = stepRow*i;
double vParm = stepRow*j;
vctTestUParm.push_back(uParm);
vctTestVParm.push_back(vParm);
}
}
int numTest = (int)vctTestUParm.size();
clock_t start;
clock_t end;
start = clock();
for (int i = 0; i < numTest; i++)
{
double uv_pair[2] = { vctTestUParm[i], vctTestVParm[i] };
UF_EVALSF_p_t evaluator;
UF_EVALSF_initialize_2(pNXFace->Tag(), &evaluator);
int deriv_flag = UF_MODL_EVAL_ALL;
UF_MODL_SRF_VALUE_p_t surf_eval = new UF_MODL_SRF_VALUE_s();
UF_EVALSF_evaluate(evaluator, deriv_flag,
uv_pair, surf_eval);
}
end = clock();
cout<<"Time1: "<<end-start<<"ms"<<endl;
start = clock();
for (int i = 0; i < numTest; i++)
{
double uv_pair[2] = { vctTestUParm[i], vctTestVParm[i] };
int deriv_flag = UF_MODL_EVAL_ALL;
UF_MODL_SRF_VALUE_p_t surf_eval = new UF_MODL_SRF_VALUE_s();
UF_MODL_evaluate_face(pNXFace->Tag(), deriv_flag,
uv_pair, surf_eval);
}
end = clock();
cout<<"Time2: "<<end-start<<"ms"<<endl;
start = clock();
for (int i = 0; i < numTest; i++)
{
double uv_pair[2] = { vctTestUParm[i], vctTestVParm[i] };
double point_F[3] = { 0.0 };
double u1[3] = { 0.0 };
double v1[3] = { 0.0 };
double u2[3] = { 0.0 };
double v2[3] = { 0.0 };
double unit_norm[3] = { 0.0 };
double radii[2] = { 0.0 };
UF_MODL_ask_face_props(pNXFace->Tag(), uv_pair,
point_F, u1, v1, u2, v2, unit_norm, radii);
}
end = clock();
cout<<"Time3: "<<end-start<<"ms"<<endl;
return 0;
}
运行代码,输出结果如下:
Time1: 127 ms
Time2: 103 ms
Time3: 75 ms
UF_EVALSF_evaluate函数反而用的时间最长。结果也算是在意料之中,在写上面代码时,故意将计算器初始化放在了for循环之内。因为我猜测计算器提效的原理是采用了缓存技术,比如存储初始化时或者当前计算过程中的通用量,以备后续计算重复使用。关键在复用,如果每次计算都需要初始化新的计算器,那就不会起作用,甚至会因为额外计算而导致效率降低。进一步验证,将计算器初始化放在for循环外面。运行代码,输出结果:UF_EVALSF_evaluate函数的运行时间是3ms,与UF_MODL_evaluate_face函数对比,提升了34倍。
总结
整个问题解决的过程就告一段落了,以下是一些个人思考:
- 问题的出现并不一定是坏事,解决的过程可能会带来意外收获。通过不断地挖掘,可以获得对接口甚至是底层技术的深层理解。当然,前提是开发时间允许。
- 接口使用的正确与否很大程度上受限于说明文档的质量,但反过来,说明文档再好,也不一定能正确使用接口,比如NX的注释和说明文档已经很好了,但是关键点不一定能看到,看到了也不一定能联想到解决办法。
- 二次开发总会受制于人。上面的问题其实是个非常简单的功能,定义良好,需求明确,尚且如此。但对于复杂功能,比如曲线投影,场景复杂,求解困难,应用起来会更加困难,一旦出现问题就只能想办法补救,比如总结失败规律,提前处理好输入条件,但是,费了很大劲实现各种补救方案,可能还是会出现问题:(1) 额外处理导致效率变低;(2) 仍然存在失败情况。然后发现问题无法解决,不得不改变整体设计流程。这就导致开发周期难以控制,甚至项目烂尾。
参考文献
[1] UGOpen Function Reference Manual, UGS Corp.
[2] The NURBS Book (2nd Edition), Les Piegl, et al., Springer.
文章首发于微信公众号:CAD智造干将,欢迎关注