benchmark

不开启进度条 遍历 100 100 100 分组
耗时 31.6019760867s

开启进度条 遍历 100 100 100 分组
耗时 34.2383126924s


smooth 四次 的球体(加上复制一个形成重叠) 199690(近20万) 个面

分组操作
不使用多线程 8.25591907051s
不使用多线程 8.11187977459s
多线程 1 耗时 8.75691644773s
多线程 1 耗时 8.77698555687s
多线程 5 耗时 43.4240804933s
多线程 10 耗时 45.5020314805s

分组操作 + 匹配
不使用多线程 11.7565591304s
多线程 1 耗时 10.8794147052s
多线程 2 耗时 20.1982408697s
注: 不使用多线程情况下有进度条显示 多线程无法兼容进度条细节

多线程1分组操作 + 匹配
不使用多线程 10.3431538931s
多线程 1 耗时 30.7188326437s
多线程 2 耗时 39.0282697662s
多线程 2 耗时 41.3114992348s

多线程1分组操作 + 匹配 + 选择面
不使用多线程 37.8165342337s
不使用多线程 37.9345086101s
多线程 1 耗时 34.5169550919s
多线程 2 耗时 44.6464130184s
注: 不使用多线程情况下有进度条显示 多线程无法兼容进度条细节

结论

python的多线程无法由于GIL全局锁的存在,无法调用更多的CPU核进行计算,因此在Maya中的多线程只能应对IO密集型的情况,在对比、三维计算等CPU密集型的工作当中,使用多线程会导致效率更低。


前言

  在开发模型检查的时候,发现有两个功能开发比较难。
  因此我就承担了最难的这个部分,而Maya自带的cleanup开发就交给了吴智博。
  这两个功能分别是 重叠面 和 穿插面

重叠面检查

开发重叠面检查看似并不困难,只需要获取模型面上的中心点,然后逐一匹配其他的面是否与之重合即可。
然而这么想就太天真了,当我将算法写好之后,会发现运行速度非常的缓慢。
张峥前辈给我提出了抽屉的概念,将模型切分成多个抽屉进行匹配,这样切分也可以投入到多线程加快匹配速度。
虽然后面经过我的 多线程BenchMark,多线程匹配不能加快速度,反而会降低速度 (:зゝ∠)
不过这个切分概念还是一个非常不错的算法,我在网上查了之后知道,这个方案称之为 spatial grid 感觉也类似于maya api 当中 uniform grid
切分最大的好处在于可以缩小 第二次遍历的数量,由于这里检查的是重叠,所以无论我切得多小,重叠的部分都会在同一个切分抽屉当中。
而切分之后的算法复杂度将不再是过高的 O(n²) 情况,当然因为切分抽屉也是需要时间的,所以抽屉的数量是需要针对不同情况有不同的优化方案的。
原理很好理解,但是代码写起来坑还是很多的。
我最开始先写了 Python 的版本,然而Python代码在检查20万面(全部重叠)的情况下需要检查30多秒,速度慢到我难以忍受。
于是我就寻找如何让Python加速的方案。

maya Python 多核运算尝试

由于测试了多线程方案是心不通的,拿难道python就无法调用多核运算吗?
答案当然是否定的,只是因为GIL的存在,Python调用多核运算,只能通过多进程实现,每个进程可以占满一个CPU核心。
于是我尝试在Maya开启多核操作, 导入Python multiprocessing 模块。
多核运算库和多线程的 threading 模块在简单的使用上大同小异。
然而在maya环境下一旦使用 multiprocessing 就会导致maya崩溃。
网上查了原因之后才知道Maya本身设计之初就没有考虑过多核处理的情况,因此内部很多api调用都是非 threadsafe 的。
只有在动力学模拟、渲染等的这些高消耗的模块支持 多核 运算。
而作为中间插入的python脚本,多核以及进程交互很容易导致maya崩溃。
官方针对这种情况在官方文档当中有专门的一个部分进行说明 文档
如果要尝试多核操作只能完全冻结mayaUI,等待结果返回,然后在进行操作。
而官方提供的maya库只能在 MainThread 上操作,换而言之还是无法多核操作。
经过多方面的研究和搜索,我感觉Maya Python 的多核运算几乎不可能实现了,除非用 C++ 调用线程池。
然而这时候一个意外的发现让我眼前一亮(然而却也只是昙花一现)。
我发现Maya还有 standalone 模式,也就是可以跳出maya的ui界面,用python重构重构maya的环境,实现在不同python编译器下运算maya。
换而言之就是在python编译器上运行maya的环境,可以实现 cmds、openMaya等命令。
这个模式通常用来批量修改 maya 场景、以及多核运算的情况。
由于这种后台批处理模式,standalone 模式也被称之为 batch mode
然而讲过我简单的测试之后我就告退了, standalone的本质和加载maya整个软件是没有区别的,
尽管可以通过 subprocess 模块实现 mayapy.exe 调用standalone 模式,在运算结束之后可以返回到 maya 图像界面的编译器上。
然而开启standalone模式和打开一个新的maya其实并没有太大的区别,过程也需要加载大量的插件、脚本等,耗时、也耗内存。
而且前辈还给了这个模式的致命一击,这个模式打开就那么费劲了,还没有打开文件呢,一旦要用到 IO 操作,那么电脑内存就捉襟见肘了。
因此经过多方面的考量,python实现多核运算并非不可能,但是使用成本、用户体验都不好控制,还不如用C++加速来得好。

C++重叠面开发

经过了漫长的摸索之后,最后还是回到了最初的起点,还是得使用C++来加快脚本的运行速度。
因为我之前有用C++的SFML库开发过东西,所以C++的基础还不算太大的问题,只是要搭建一下C++的环境废了一点功夫。
当然也很长时间没有接触C++了,所以顺手跑了一个hello的入门命令了解一下maya C++ 的开发。
这里面最坑爹的就是 Visual Studio 的项目配置了。
最后网上又查了一番,发现maya的devkit是由 pluginwizard 这种操作,可以快速搭建Visual Studio 的项目环境。


后面的操作其实差不多就是将我之前写的OpenMaya Python 代码 转换成 C++ 写一遍。
这当中的区别其实也不大。
当然,因为太久没有开发C++了,还忘记头文件的作用了,折腾了好一会才发现用头文件才可以实现 python import 功能。


在C++写重叠面的操作,最最坑爹的地方就是C++的字典操作。
由于我之前用python写了分组操作,而分组不知道如何将组别的序号转换为线性的数字进行存储
1,1,1 => 1
1,1,2 => 2
类似上面的问题,所以最后Python是通过 字典的key值存字符串来区分的。
因此同样的道理,我也找到了 C++ 的字典,stl库中的map函数。
然而用这个方案遍历 map 的运行速度慢出了天际。
当我将创建写完,在Maya中执行20万面检查的时候,我发现 python 需要 37s 而C++要 30s
我简直不敢相信自己的眼睛,于是我将功能拆分看看到底是哪个步骤占用了过多的时间。
于是就发现 分组操作 在 xyz 都是 100 分段的细分条件下 python需要7-8s完成分组,而C++只需要0.2 - 0.3s
因此当我禁用了后面选面的操作,只是遍历所有的边进行匹配
我发现 C++ 依然需要30s 而Python 只需要 13s 左右
于是我肯定了是 map 导致了缓慢计算速度。
至于原因我也不太清楚,在和胡盼大佬商量了之后,结论就是map本身是不太适合进行这种大规模的遍历的,推荐我使用结构体来将数据存储起来。
结构体确实是个好东西。
于是我就利用了结构体的方案,通过 stl 库的 Vector 搭建数组。
最重要的是,我总算想明白了如何将组别转成线性数组来进行存储
x,y,z => num
0,0,0 => 1
0,0,1 => 2
只需要转换为上面的形式就可以实现计算了 计算公式 xy分段z分段+y*z分段+z
其实非常简单的数学问题,只是上面的形式误导了我。
于是我又将之前写好的C++代码转换为重新整理的代码,由于分组过程中有三重循环,在加上自己改变了遍历序号,结果导致了逻辑陷阱产生了意料之外的结果。
而我当时一直没有搞清楚自己的代码到底是哪里导致了这种遍历有问题的情况。
最后我使用了 Visual Stuido 强大的断点调试功能才终于发现了问题的根源所在。

终于,在折腾了这么长时间之后,总算是写好了第一个C++命令,而且20万面测试中 运算时间在 2-3s,非常符合预期。


C++穿插面开发

终于,就轮到了我近一个星期的开发噩梦了。
寻找模型穿插,我最初认为根本不是什么问题,毕竟maya api 就提供了好几种光线计算穿插的方案。
在加上我在网上搜索到了相关的C++编写方案,因此当时我充满了自信。
然而当我真正投身其中才发现我是多么天真。
首先C++的方案根本就不是计算模型穿插的,而是计算面与模型的穿插交线,不过它采用的方案还是被我借鉴过来了。
那就是遍历模型上所有的边发射射线。当然网上原文还是有些区别的,但是它给我提供了灵感。
于是我就想到了遍历所有的边,发射与边长度相等的光线,如果光线存在碰撞,那就说明这个边肯定和模型上的面存在穿插。
这个方案可行但是发射的时候总是将边相邻的面也算成了碰撞,最初也没有想到有什么办法可以过滤这种情况而头疼得很。
最后我发现 allintersections 的命令当中是可以输入相应的碰撞面 id的
通过将相邻面的id去掉就可以过滤掉这种完全不需要考虑的情况。
最后果然实现了我梦寐以求的效果。
然而这个方案最大的缺点就是慢,在进行六千个面测试的情况中,即便是C++方案也需要几秒的计算时间,那就不用考虑20万面的情况了。
于是我开始想办法优化这个穿插情况,首先比较好好想到的是,如果模型是自穿插的(可以参考第一周截屏的图片)
我可以通过cleanup工具找到 模型上不平整 凹凸不平的面,通过这个方案就可以快速过滤掉那些正常的面。
当我觉得这个方案万无一失的时候,张峥前辈一语惊醒梦中人。
如果模型存在combine的穿插情况,那么cleanup工具是没有办法选择出来的。
而针对这种情况也确实是很难缩小穿插的范围来加快计算速度了。
但是我还是不死心,因为我看到Ziva插件就完美做到了各种穿插的计算,我觉得我的方案就未必不行,只需要在解决最后一种穿插情况即可。
然而到头来我发现这种穿插几乎是无解的(:зゝ∠)
我首先想到了 Boundingbox 穿插来缩小检测范围,所以计算出两个物体相交的boundingbox 就可以极大减少计算范围。
然而物体是合并到一起的,有如何才能生成boundingbox呢?
经过我不懈的努力,我发现maya的polyselect中的shell标记是可以选择出模型的各个可以seperate的部分,通过while循环就可以将模型的面进行分组。
然而再通过polyevaluate中的bc标记来计算每个选中面的boundingbox,从而实现了各个部分的boundingbox的计算。
最后确实是可以计算出两个boundingbox穿插部分的交集boundingbox
然而即便是如此,要计算交集boundingbox的范围,还是需要遍历模型上的点去判断是否在boundingbox当中,这个过程仍然是相当耗时的。
而且即便是选中了交集当中所有的component,这个component的数量也可能是上万级别的,那么光线穿插的计算效率依旧堪忧。
因此,最后的最后,我得出了结论,光线穿插本身的计算效率就不行,必须要用更快的算法来计算模型穿插的情况。


于是我又投入了大量的时间去研究 向量 点乘 叉乘 法线等等的概念,尝试通过算法计算出穿插。
确实国外的网站有不少这种穿插情况的计算方法,然而坑也是数不胜数。
我最初是模仿 平面与平面相交的计算情况,没想到这个平面居然是数学意义上的平面,也就是空间上无限大的平面(:зゝ∠),结果算法呢算出的穿插到处都是。
于是我又开始模仿三角面与三角面穿插的算法,然而算法相关的网站没有提供这部分的代码,只有概念,于是我想破头也没有弄出更好的面面相交算法。
不过上面还是有光线与面穿插的计算方案的,上面的数学求解还是挺有意思的,利用了参数方程,将直角坐标系转换到三角面的坐标系上,从而可以算出平面上的点是否在这个三角面区域内。
算法只考虑了面和光线是否有穿插,但是我却忘记了遍历所有的面和所有的边,这个算法复杂度可是 O(n²) 的
结果我花了大量的时间弄出了比光线追踪还要慢的解决方案,我真的是无言以对,而且我开发完成之后发现自己写的算法还有BUG,在很多时候检测的面都是不全的,真是投了大量时间,赔了夫人又折兵。
所以到了这个星期的星期一,我放弃了穿插算法的研究了,毕竟时间都耗尽了。
现在采用的解决方案是沿用 Ziva 插件的穿插方案,Ziva的计算效率真的强无敌。