好的API命名是可以根据完成的功能猜出来的。

写完这个题目,想起大学里刚开始学习编程时,同宿舍一哥们儿已经编程小牛一枚了。我问他,你怎么知道实现某个操作(比如获得CString对象的长度,获取系统时间,等等)需要调用哪个函数?答曰:靠猜啊,还有MSDN。于是我也学会了这种方法:需要实现什么功能,先猜一猜可能的名称会是什么,然后在MSDN的索引里边去找,屡试不爽。后来随着接触的东西慢慢增多,也会发现不同的系统/库在命名方面会有不同的习惯(convention)或者风格(flavor),但猜出来的东西基本八九不离十,就算没有直接命中也能发现非常相关的信息。

好的API命名应该是告诉用户这个函数要干什么,不多不少。命名反映了设计者的基本素养,就算从命名大致推断设计者的水平可能也不为过。花了好一段时间解决了OSG中世界坐标向屏幕坐标的转换问题,我感觉有点被(可能不好)的API命名给坑了。

项目指定使用OSG进行仿真场景开发,并且需要得到目标(OSG场景中的某个Node)的屏幕坐标位置,作为其他一些操作的输入条件。刚开始时倒是在网上找到了一些代码片段,但是算出来的结果都明显不对。由于时间比较紧张、对OSG也不熟悉,于是采取了其它方法实现了这个功能,不足之处是计算结果有些偏差;好在进行固定值修正后也能满足使用要求。

这两天有些空闲时间,于是又开始捣鼓这一问题的精确解析方法。其实过程说起来也简单,就是根据节点的世界坐标,进行视图(View)、投影(Projection)和窗口(Window)矩阵的转换,然后就完事儿了;这些转换矩阵OSG中都提供了明确的函数调用,于是重要的问题便是得到节点的世界坐标。OSG中场景采用树型结构进行组织,节点可能存在父节点、祖父节点。。。,节点的坐标又有局部和世界之分。根据我的理解,局部坐标就是节点相对其父节点的坐标,世界坐标就是节点在场景中的绝对坐标,也就是相当于场景根节点的坐标。为了计算世界坐标,OSG好心地提供了osg::computeLocalToWorld这样一个全局函数,网上找到的用法类似这样:

1
worldPos = localPos * osg::computeLocalToWorld(node->getParentalNodePaths()[0]);

从字面上看,getParentalNodePaths得到一系列的路径,每条路径由父节点至场景根的多个节点组成;于是computeLocalToWorld得到局部坐标到世界坐标的转换矩阵;再去乘localPos,得到worldPos。看起来合情合理。但实际执行结果却是另一回事,总是得到显然不合理的结果。

为了搞清楚是怎么回事,我写了一个简单的测试程序,搭建了一个简单的场景,试图一步一步验证计算结果是否与预想的情况一致。结果发现世界坐标的计算就出错了:node直接插入场景根并设置其坐标为(10, 0, 0),这样对node来说局部坐标和世界坐标都应保持(10, 0, 0)不变,而通过计算却得到(20, 0, 0)。再检查getParentalNodePath()[0],发现返回的结果是这样:Camera<-SceneRoot<-node:不仅包括一个摄像机节点,而且node自身也包括在内。这些都与预想的情况不一样。会不会由于getParentalNodePaths的结果包括了调用者本身,使得计算时出现了重复呢?修改并验证了一下,果然(居然就)得到了正确的结果。进一步查看了OSG3.4源码,也发现了类似的计算过程(搜索prunedNodePath)。最终经过多次验证,世界坐标的计算方法是这样:

1
2
3
4
5
6
7
// osg::PositionAttitudeTransform *node = ...
osg::NodePath nodePath = node->getParentalNodePaths()[0];
osg::NodePath prunedNodePath(nodePath.begin(), nodePath.end() - 1);
osg::Vec3d worldPos = node->getPosition() * osg::computeLocalToWorld(prunedNodePath)
或者:
osg::NodePath nodePath = node->getParentalNodePaths()[0];
osg::Vec3d worldPos = osg::Vec3d(0, 0, 0) * osg::computeLocalToWorld(nodePath)

以上两种方式,不管是哪一种,在我看来都比较别扭,不像是可以正确工作的代码,因为computeLocalToWorld和getParentalNodePaths其字面上带给人的信息和在代码中的实际作用不太吻合。有点被“不好的API命名”给坑了。(不知我的理解是否正确)

通过这次解决问题得到的一些经验:

  1. 对于不知道的东西,如果通过搜索的方式获得结果,最好能够理解别人提供的解决方案。官方的源代码是第一手资料,看源码中那些你不熟悉的API是怎么用的,作者的用法是API设计意图最好的体现;
  2. 出现问题,首先在尽可能简化的情况下进行逐步检验,看看每一步的结果是否符合预期;这可能比在网上盲目搜索并寄希望得到专家解答更高效;
  3. 需要对代码进行说明时,可以采用assert的方式表述你认为的结论。