前言

  这个工具是根据摄像机处理的需求进行制作的。
  摄像机的Maya制作遇到了这样的一个问题。
  首先这里的摄像机是针对于乐园的缆车运作所制作的Maya摄像机场景,主要目的是观测缆车所带动的摄像机运动是否符合预期的效果。
  当前已经有一个制作好的在轨道上能加速减速的人眼摄像机,而另外有18个镜头则是针对于单个银幕运动的摄像机(其实最开始还不清楚的)
  当前需要做的就是讲18个摄像机相关的关键帧提取出来,然后将关键帧覆盖到人眼摄像机上。
  然而这18个摄像机的情况却很复杂。
  首先路径的坐标不一致,另外所有的曲线关键帧都被偏移到1000帧左右的地方开始。
  所以我还需要将相关的关键帧重新偏移到正确的位置。
  并且想办法让人眼摄像机逐一跟随单一场景的摄像机来完成整一段的动画。

任务对接

  和我对接的是专门负责摄像机模块的李城佑。
  或许因为主要弄摄像机的缘故,他对Maya这方面的操作还不太熟悉,因此在开发的需求上也让我很难把握到位。
  刚开始的时候,我和沟通要完成这个工作,需要做些什么,哪些步骤是我可以协助完成的。
  至于我自己也确实对相机这方面不太熟悉,虽然苏老板已经事先给我说明了大体的情况,但是更多的细节还是得和李城佑对接好。
  因此我对于这一块的疑问也是很多的,所以就在咨询的过程中,我们两个人都开始不断蒙逼。
  最让我困惑的地方在于交接过来的相机文件时间都是在1000帧左右开始的。
时间文件
  当时我们都对于这个数据感到巨大的困惑,这些数据怎么才能匹配到人眼摄像机呢?人眼摄像机可是有上万帧的。
  于是我们围绕这个讨论很长时间,以至于我们两个人都蒙逼了。
  后面我自己拿出这些相机与人眼摄像机进行匹配,发现他们的位置对应的时间都是不一致的,因此我就更加混乱了。

  后面我向李城佑求解,他也糊涂了,所以最后向摄像机方面的大佬李宸前辈求解了。
  前辈说这个摄像机其实比不是对应到人眼摄像机上,毕竟这里有前跟踪段和后跟踪段。
  所以这里的时间具体是指公园里荧幕上所投放视频的时间长度。
  前跟踪段是进入镜头的时间,后跟踪段是从镜头出去的时间。
场景
  从场景文件可以看到,镜头与镜头之间是存在交错的,
  这番理论瞬间仿佛点醒了梦中人。
  不过前辈认为,所有镜头只是偏移了时间,偏移时间的目的是为了方便调整,那么理论上镜头的所有运动应该都是相互匹配的。
  然而我们当着他的面去匹配之后,发现还是会有错位的情况。
  因此前辈说这个就不对了,赶紧测试一下哪些镜头是有偏差的,向上面汇报。
  于是恍惚间,我似乎就不需要开发插件了,李城佑对好有问题的摄像机向上面汇报即可。

  然而万万没想到,事情远远没有那么简单,第二天李城佑又找到我,重申了开发的需求,还是需要将所有的运动效果对到人眼摄像机上。
  于是我就开始了我的开发。

  然而其实我并没有完全理解清楚需求,我只是知道尽然要偏移关键帧,那么需要在每个摄像机文件中提取出 motionPath 的节点。上面有对应的关键帧可以供我偏移进行参考。
场景
  于是我看到了这些 MotionPath 的关键帧是这样的。
场景
  经过代码偏移之后,我发现这些 MotionPath 的关键帧都是有些不同的。
  那个时候我才真正明白,这个些镜头可能都是在人眼摄像机的关键帧信息上面修改的,所以正确的信息只有文本所提到的部分。
  我要根据文本所提到的时间提取出相关的关键帧,随后将影响关键帧的motionPath偏移到1开始的地方,从而提取出正确的部分。

  在我研究的过程中,李城佑也没有闲着,他手动将所有的关键帧偏移并且放到了合适的位置。
  我拿到了他的新的文件的时候还没有弄懂这些关键帧为什么是偏移的,反而让我蒙逼了。
  于是我又跟他讨论了好久,我认为他的locator很可能都是不对的,因为对不上MotionPath的位置(毕竟他偏移过了,只是我当时不知道)
  所以我要求他以导入的MotionPath坐标为基准,也就是导入的摄像机为基准,Locator的坐标很可能某些操作的原因而不对的。
  不过我当时也发现这个逻辑似乎有点问题的,如果是导入错误导致了偏移,那么locator应该不会只是在曲线上错位的。
  李城佑也似乎被我给说糊涂了,所以就接受了我的观点,不过如此一来,他之前做的大量工作似乎都白费力气了。

  这个时候李宸前辈出来了,他指出这里导入的摄像机都是没有意义的,一切还是以Locator为基准,并且提供了一个神奇的解决方案。
  只要让人眼摄像机和相关的镜头摄像机的MotionPat一一对应就可以实现效果了。
  确实这样可以完成任务,而且还插件还不太好搞了。
  但是如此操作,locator似乎就没有存在的意义了,于是我虽然理解意思,但是对这样操作反而愈发的感到混乱。
  经过了解才知道,locator是为了方便约束而弄出来的东西。
  不过经过我这一番折腾,李城佑也对开发失去了信心吧,他坚持要手动将这个工作做好。
  而我还是觉得用约束的方案可能调整起来会更加便利一点,于是我又开始了插件的制作。

插件开发

  在开发的时候,其实苏老板的意思很简单。
  只要做出一个面板方便约束就可以了。
  但是我考虑后面的相机制作的情况,还是决定开发一个可扩展使用的插件。
  于是就有了现在插件的雏形。
场景
  我开发计划是左边是可以添加镜头的区域,而右边则是属性面板。
场景
  因为最近搞Ziva的空隙之余,我就在弄自己的 UI2CG 2.0 插件。
  当时就研究了 item 的概念,也就是可以增加减少的控件,这样可以大大增加插件的灵活性。
  这里我将这个概念再发挥一下,将属性面板也开发出来。
  为了实现这个效果,我将界面拆分成四个模块。
场景
  MainWindow是包含下面的下拉菜单的部分。
  左边是摄像机面板,右边是属性面板,而滚动区域则是摄像机item的区间。
  这个三个东西都通过 QSplitter 放到了有下拉菜单的主界面下。
场景

镜头Item

  其实刚开始觉得添加这种可以增加减少的UI会很复杂。
  其实并没有想象中那么困难。
  用不同的ui文件就是为了让UI文件对应不同的类,从而实现实例化的多个item效果。
  当点击添加镜头的时候,就实例化一个镜头对象而已。
  至于删除也是相当简单的,给item对象添加删除功能,点击删除按钮触发删除自身就好了。
  清空镜头则稍微复杂了一点点。
  需要先获取 scrollArea 中的 chilren,然后遍历所有的chilren来删除。
  上面这些都不难,反而比较困难的是如何给每个对象合理安排编号和名称。

  我在编号的问题上卡了一段时间,一开始我认为编号应该依附在对象上的,所以保存导出之后还要读取编号。
  但是由于Json的导入是无序的(当然后面上网查了之后就是有序导入了),所以编号还是根据当前存在的chilren数量来生成。
  另外当删除的时候如何确保后续的编号都是正确的还需要经过遍历,并且让后续的编号逐个减1,这个也是研究好一会才做好的。

点击事件管理

  由于这次用了自定义的Item对象,所以点击触发事件就不是简单的信号槽可以实现的。
  这次是给左边的摄像机面板添加了mousePressEvent事件,这样就实现点击任意区域实现触发。
  当然这个过程还需要检索是否点击在合适的区域,因此还需要进行判断。
  幸好Qt的Geometry函数提供了 Contains 的碰撞检测,这样就可以通过点击的坐标是否和item存在关联进行过滤。
  然而这个点击触发的坐标其实是不准的,在纵坐标上存在偏移,经过手动的测试,我发现90的偏移值就是刚刚好不偏不倚的。
  通过这个偏移就可以实现点击Item将信息显示到属性面板上。
  然而当我将Item的数量加多了之后,ScrollArea 的偏移也会影响到这里的坐标。
  最后我给ScrollArea添加了检测时间,当滚动触发的时候记录滚动的偏移值来加入到这里的碰撞检测当中。

点击显示效果

  其实我原本只是想,当点击了Item之后可以有一个效果反馈,说明点中了这个区域。
  实现原理也不复杂,就是使用样式表就可以了。
  然而却在这里栽了跟头。
  首先这里的样式表不能单纯的setStylesheet填写CSS字符串,因为这样的话整个Item包括所有的子对象都会受到样式表的影响。
  所以这里需要通过 # selector 来只影响到Item最外层的区域。
  然而我这么操作却始终看不到效果。
  经过了一大轮的测试之后,我发现问题的原因居然是QWidget。
  我在Qt Designer 上输入样式表是可以看到红色的轮廓的,但是Maya里面却显示不出来。
  后面我尝试着将 QWidget 换成 QFrame ,就实现了我想要的效果了。

属性管理

  相对来说,比较复杂的部分是设置面板的部分。
  因为点击每一个Item都需要属性面板获取相关的数据从而呈现不同。
  所以这里还要给Item类添加上和属性面板关联的属性。
  如此一来就可以点击这些Item的时候可以读取到这些属性,从而显示到属性面板上。

Json 导入导出记录

  其实这个部分并不复杂,却十分重要。
  最重要是需要将Item中所有属性全部读取出来,后续再读取回来。
  主要遇到的问题就是 Json 读取写入都是无序的。
  经过 stackOverflow 的搜索 找到了解决方法。

约束 Checkbox

  其实我原以为这个功能非常简单。
  但是在这种可扩展的Item类中却成了棘手的问题。
  原本约束复选框是想实现点击复选框之后当前Item对应的约束属性改为1,其他属性改为0。同样的这里其他的复选框选项也要去掉。
  所以我的思路就是点击复选框的时候,遍历chilren 取消所有的复选框选择,最后再勾上当前选择的复选框。
  然而直接这么操作的话,在取消选择过程中,也会触发复选框的 Signal
  最后会导致复选框之间产生递归,我操作不当还造成了死循环。
  这个问题很棘手,经过在stackoverflow的一番查找之后,我总算是解决了问题。
  主要需要用到 blocksignal 函数来屏蔽掉 信号槽之间的互相触发。

使用流程

场景
  打开插件的默认状态是这样的。

基准镜头获取

场景
  点击基准镜头,获取相关的数据。
  选择方法,选中相关的物体,点击按钮进行获取。

  镜头组 - 轨道小车(约束的目标物体)
  locator - 轨道小车的MotionPath Locator
  曲线 - 获取轨道小车的路径
  MotionPath - 获取运动路径(选择曲线自动获取)
场景

添加镜头

场景
  点击添加镜头可以生成镜头Item。
  右侧的属性面板可以获取相关的摄像机属性。
场景
  选中相关的镜头组获取即可自动获取相关的子对象。

按钮功能

场景
  选择所有MotionPath可以选中相关MotionPath 节点
  批量偏移实现上面截图显示的偏移效果。
  镜头匹配可以对镜头的位置进行匹配。

场景
  批量添加约束会将镜头组下的locator批量对基准镜头进行点约束和旋转约束。
  同时会解锁开启约束和设置约束关键帧。

场景
  这一步之后可以移动面板的splitter,隐藏不必要的控件。

场景
  点击开启约束就会自动设置到约束节点的相关属性上。
  如果什么都不选就会约束到基准镜头上。

场景
  点击设置约束关键帧就会对相关的约束属性进行关键帧设置