前言

  开发藤蔓生长工具可谓是我进入方特一来第一项工作。
  难度也是有的,但是并非是无法做到的程度。
  当时设置部部长丸子找到我,让我尝试开发这个东西,并且当时给我看了他们当时绑定好的一些设置文件。
  其实这种绑定方法我已经很模糊了,但是大一的绑定教程还是有点印象的。
  于是我就兴高采烈的拿来进行研究。
  我发现这套绑定本质上和脊椎绑定基本相同,而神奇的地方在于设置了骨骼的缩放,从而实现了生长的效果。
  那么下面我就要去思考,如何通过插件来快速完成这个步骤。

UI界面开发

  这次依旧是试用 Qt Designer 进行界面开发。
Tab 第一页
Tab 第二页
  在Maya上的显示效果。
Maya 第一页
Maya 第二页

UI功能开发

  在之前开发好的 Alembic 导入导出助手的基础上,我继续加入自己所想要的功能。
  师兄建议如果界面的开发代码太多的话,希望能将界面的功能和按钮功能分开写。
  这样更加有条理,也让人更容易看懂。

Dockable Window

  使用ADV的时候,我偶然发现ADV是有Dock按钮的。
ADV dock 按钮
  如果window是可以Dock的话那么插件就可以随心所欲地摆放了。
  我觉得这种交互是非常人性化的,所以无论如何都要加到自己的插件当中。
  通过ADV我后面了解到 workspace 按钮是在 Maya2017之后才加入的全新工作区按钮
  在Maya2017以前的版本都是不支持的,在过去都是使用Dock方案
  在背后调用的 Maya cmds 是不一样的。
  因此我还要去研究如何获取Maya当前的版本,不过幸好ADV就是最棒的参考。
  所以后面我需要去研究 Maya 的 dockControl 和 workspaceControl 而已
  但是具体要怎么操作呢?又要如何和将这些东西和Qt界面结合在一起呢?

  首先毋容置疑的,先解析 Qt Designer 生成的 ui 文件,具体的操作方法可以参照这里
  后面解析了ui文件就可以通过 setupUi 函数来生成 ui 文件的内容。
  但是这样生成的界面有个缺点,那就是它与Maya的主界面完全没有关联。
  当你点击一下Maya的窗口,Maya的窗口就会将其盖在下面,交互就很不人性化。
  不过幸好 Python For Maya Artist Friendly Programming 这个教程有提到过解决方案
  (我当时一直以为这样将教程的代码全部整理出来是很愚蠢的行为,现在真正干活的时候才体会到真香定律(:з」∠))
  那里提供了workspace的解决方案,原理就是通 OpenMaya API 获取到Maya窗口的元素,然后用Qt进行包裹调用。
  于是我就有样学样地整理出dockControl的写法。
  当然这个过程也查阅了不少的代码,最后三合一写法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
def Dock_Win_Management(self):
Title_Name = u"藤蔓生长快速绑定工具"

# Maya窗口转Qt的万能函数,网上搜罗出来的
def mayaToQT( name ):
# Maya -> QWidget
ptr = omui.MQtUtil.findControl( name )
if ptr is None: ptr = omui.MQtUtil.findLayout( name )
if ptr is None: ptr = omui.MQtUtil.findMenuItem( name )
if ptr is not None: return wrapInstance( long( ptr ), QWidget )

if self.DOCK == "undock":
# undock 窗口

# 这里判断是因为 在Maya2015测试的时候 cc(closeCommand)不起作用
if mel.eval("getApplicationVersionAsFloat;")>=2017:
self.undockWindow = cmds.window( title=Title_Name,cc=partial(self.closeEvent,self.event))
else:
self.undockWindow = cmds.window( title=Title_Name,rc=partial(self.closeEvent,self.event))
# 给Maya窗口添加一个 Layout 防止后面获取不到
cmds.paneLayout()
cmds.showWindow(self.undockWindow)
ptr = mayaToQT(self.undockWindow)
return ptr

elif self.DOCK == "dock":
# dock 窗口
window = cmds.window( title=Title_Name)
cmds.paneLayout()
self.dockControl = cmds.dockControl( area='right', content=window, label=Title_Name,floatChangeCommand=self.Win_Size_Adjustment,vcc=self.closeEvent)
dock = mayaToQT(window)
return dock

elif self.DOCK == "workspace":
# workspace 窗口
name='VineGrowDock'
if cmds.workspaceControl(name,query=True,exists=True) :
cmds.deleteUI(name)
self.workspaceCtrl = cmds.workspaceControl(name,fl=True,label=Title_Name,vcc=self.closeEvent)
cmds.paneLayout()
workspace = mayaToQT(self.workspaceCtrl)
return workspace

  如此一来,在初始化函数里面就可以调用这个函数来完成界面创建


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def __init__(self,dock="dock"):
self.DOCK = dock
# 读取当前DOCK属性
if os.path.exists(GUI_STATE_PATH):
GUI_STATE = {}
with open(GUI_STATE_PATH,'r') as f:
GUI_STATE = json.load(f)
self.DOCK = GUI_STATE["DOCK"]

# 如果2017以下的版 将workspace转换为dock
if mel.eval("getApplicationVersionAsFloat;")<2017:
if self.DOCK == "workspace":
self.DOCK = "dock"

self.ptr = self.Dock_Win_Management()

super(Interface,self).__init__(parent=self.ptr)

self.parent().layout().addWidget(self)
self.setupUi(self)

  这样确实可以通过传入不同的参数来实现不同窗口的创建。
  然而我遇到了让我很头疼的问题,那就是如何去关闭窗口。
  如何实现像ADV那样,点击按钮之后自动重新打开我们想要的窗口类型。
  然而ADV是用MEL写的,是全局函数来的,而我这里是用Python的Class写的,如何才能实现在外部关闭窗口并且重新打开新的窗口就成了我最大的挑战。
  首先我需要在类里面写一个切换函数,但是切换函数又会调用到类本身来创建窗口,此处的类引用变量就无法在类外调用了。
  为了确保点击窗口之后不会创建一个全新的窗口,我需要确保类引用变量是一个全局变量,那么就不会在类里面创建调用自己而产生第二窗口。
  (想通这点可着实花了我好长时间,最主要是这里类里类外的,逻辑有点绕,写出来之后其实并没有那么复杂)
  方法是如此,但是因为我将UI界面和功能函数分开写,但是我又要调用功能函数去生成界面,所以就不得不把这部分的代码写在功能函数的脚本里面(:з」∠)
  (现在想来自己似乎有点傻,其实用UI界面去生成就完事了。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def Dockable_Window_Fun(self,dock="undock",save=True):
# 保存当前UI界面
if save == True:
self.DOCK = dock
self.closeEvent(self.event)

# 检测不同的UI 全部删除
try:
if cmds.window(self.undockWindow,query=True,exists=True) :
cmds.deleteUI(self.undockWindow)
except:
pass

try:
if cmds.dockControl(self.dockControl,query=True,exists=True) :
cmds.deleteUI(self.dockControl)
except:
pass

try:
if cmds.workspaceControl(self.workspaceCtrl,query=True,exists=True) :
cmds.deleteUI(self.workspaceCtrl)
except:
pass

# 重置json 重置按钮调用的功能
if save == False:
os.remove(GUI_STATE_PATH)

global Vine_Grow_UI
Vine_Grow_UI = Vine_Grow(dock=dock)
Vine_Grow_UI.show()

  另外在外部调用则是写一个Main函数,这样也可以解决插件之间的冲突问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def main():
# 检测不同的UI 全部删除
global Vine_Grow_UI

try:
if cmds.window(Vine_Grow_UI.undockWindow,query=True,exists=True) :
cmds.deleteUI(Vine_Grow_UI.undockWindow)
except:
pass

try:
if cmds.dockControl(Vine_Grow_UI.dockControl,query=True,exists=True) :
cmds.deleteUI(Vine_Grow_UI.dockControl)
except:
pass

try:
if cmds.workspaceControl(Vine_Grow_UI.workspaceCtrl,query=True,exists=True) :
cmds.deleteUI(Vine_Grow_UI.workspaceCtrl)
except:
pass

Vine_Grow_UI = Vine_Grow(dock="undock")
Vine_Grow_UI.show()

  这样完整写下来就可以完美实现ADV的Dock功能,不管是按shelf Button 还是点击插件提供的按钮,都可以完美实现界面窗口的切换。

Json 加载保存重置功能

  这次参考着之前开发出来的 PLB Downloader ,也打算弄成 json 可以导入导出,那样插件的所有设置的可控性会更强。
  开发的原理和 PLB Downloader 如出一辙的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263

def Save_Json_File(self,path=GUI_STATE_PATH):

GUI_STATE = {}
# 判断物体是否存在,存在就存储
try:
GUI_STATE['Rig_Obj_Text'] = self.Rig_Obj_Text.text() if len(cmds.ls(self.Rig_Obj_Text.text()))>0 else ""
except:
return
GUI_STATE['Start_JNT_Text'] = self.Start_JNT_Text.text() if len(cmds.ls(self.Start_JNT_Text.text()))>0 else ""
GUI_STATE['End_JNT_Text'] = self.End_JNT_Text.text() if len(cmds.ls(self.End_JNT_Text.text()))>0 else ""
GUI_STATE['Main_JNT_Num'] = self.Main_JNT_Num.text()
GUI_STATE['IK_JNT_Num'] = self.IK_JNT_Num.text()
GUI_STATE['Curve_Span_Num'] = self.Curve_Span_Num.text()
GUI_STATE['IK_CTRL_ColorSlider'] = self.IK_CTRL_ColorSlider.value()
GUI_STATE['Start_Ctrl_ColorSlider'] = self.Start_Ctrl_ColorSlider.value()
GUI_STATE['End_IK_ColorSlider'] = self.End_IK_ColorSlider.value()
GUI_STATE['Character_Ctrl_ColorSlider'] = self.Character_Ctrl_ColorSlider.value()

GUI_STATE['Geo_Name_Text'] = self.Geo_Name_Text.text()
GUI_STATE['Main_JNT_Name_Text'] = self.Main_JNT_Name_Text.text()
GUI_STATE['Curve_Name_Text'] = self.Curve_Name_Text.text()
GUI_STATE['IK_JNT_Name_Text'] = self.IK_JNT_Name_Text.text()
GUI_STATE['IK_CTRL_Name_Text'] = self.IK_CTRL_Name_Text.text()
GUI_STATE['Start_Ctrl_Text'] = self.Start_Ctrl_Text.text()
GUI_STATE['End_IK_Text'] = self.End_IK_Text.text()
GUI_STATE['Character_Ctrl_Text'] = self.Character_Ctrl_Text.text()
GUI_STATE['X_RadioButton'] = self.X_RadioButton.isChecked()
GUI_STATE['Y_RadioButton'] = self.Y_RadioButton.isChecked()
GUI_STATE['Z_RadioButton'] = self.Z_RadioButton.isChecked()
GUI_STATE['Delete_CheckBox'] = self.Delete_CheckBox.isChecked()
GUI_STATE['Rig_Obj_Toggle_Check'] = self.Rig_Obj_Toggle_Check
GUI_STATE['Loc_Generate_Toggle_Check'] = self.Loc_Generate_Toggle_Check
GUI_STATE['JNT_Setting_Toggle_Check'] = self.JNT_Setting_Toggle_Check
GUI_STATE['Rig_Generate_Toggle_Check'] = self.Rig_Generate_Toggle_Check
GUI_STATE['Namespace_Toggle_Check'] = self.Namespace_Toggle_Check
GUI_STATE['Window_Setting_Toggle_Check'] = self.Window_Setting_Toggle_Check
GUI_STATE['Tab_Widget'] = self.Tab_Widget.currentIndex()

styleSheet = self.IK_CTRL_ColorBtn.styleSheet().split("(")[1].split(",")
r = float(styleSheet[0])
g = float(styleSheet[1])
b = float(styleSheet[2].split(")")[0])
color = (r,g,b)
GUI_STATE['IK_CTRL_ColorBtn'] = (color[0],color[1],color[2])

styleSheet = self.Start_Ctrl_ColorBtn.styleSheet().split("(")[1].split(",")
r = float(styleSheet[0])
g = float(styleSheet[1])
b = float(styleSheet[2].split(")")[0])
color = (r,g,b)
GUI_STATE['Start_Ctrl_ColorBtn'] = (color[0],color[1],color[2])

styleSheet = self.End_IK_ColorBtn.styleSheet().split("(")[1].split(",")
r = float(styleSheet[0])
g = float(styleSheet[1])
b = float(styleSheet[2].split(")")[0])
color = (r,g,b)
GUI_STATE['End_IK_ColorBtn'] = (color[0],color[1],color[2])

styleSheet = self.Character_Ctrl_ColorBtn.styleSheet().split("(")[1].split(",")
r = float(styleSheet[0])
g = float(styleSheet[1])
b = float(styleSheet[2].split(")")[0])
color = (r,g,b)
GUI_STATE['Character_Ctrl_ColorBtn'] = (color[0],color[1],color[2])

GUI_STATE['DOCK'] = self.DOCK
try:
with open(path,'w') as f:
json.dump(GUI_STATE,f,indent=4)
return True
except:
QMessageBox.warning(self, "Warning", "保存失败")
return False

def Load_File(self,path=GUI_STATE_PATH,load=False):
if os.path.exists(path):
GUI_STATE = {}
with open(path,'r') as f:
GUI_STATE = json.load(f)
self.X_RadioButton.setChecked(GUI_STATE['X_RadioButton'])
self.Y_RadioButton.setChecked(GUI_STATE['Y_RadioButton'])
self.Z_RadioButton.setChecked(GUI_STATE['Z_RadioButton'])

# 获取上次打开的text信息
self.Rig_Obj_Text.setText(GUI_STATE['Rig_Obj_Text'])
self.Start_JNT_Text.setText(GUI_STATE['Start_JNT_Text'])
self.End_JNT_Text.setText(GUI_STATE['End_JNT_Text'])

self.Main_JNT_Slider.setValue(int(GUI_STATE['Main_JNT_Num']))
self.Main_JNT_Num.setText(GUI_STATE['Main_JNT_Num'])
self.IK_JNT_Slider.setValue(int(GUI_STATE['IK_JNT_Num']))
self.IK_JNT_Num.setText(GUI_STATE['IK_JNT_Num'])
self.Curve_Span_Slider.setValue(int(GUI_STATE['Curve_Span_Num']))
self.Curve_Span_Num.setText(GUI_STATE['Curve_Span_Num'])

self.Tab_Widget.setCurrentIndex(int(GUI_STATE['Tab_Widget']))

self.Geo_Name_Text.setText(GUI_STATE['Geo_Name_Text'])
self.Main_JNT_Name_Text.setText(GUI_STATE['Main_JNT_Name_Text'])
self.Curve_Name_Text.setText(GUI_STATE['Curve_Name_Text'])
self.IK_JNT_Name_Text.setText(GUI_STATE['IK_JNT_Name_Text'])
self.Delete_CheckBox.setChecked(GUI_STATE['Delete_CheckBox'])
self.IK_CTRL_Name_Text.setText(GUI_STATE['IK_CTRL_Name_Text'])
self.Start_Ctrl_Text.setText(GUI_STATE['Start_Ctrl_Text'])
self.End_IK_Text.setText(GUI_STATE['End_IK_Text'])
self.Character_Ctrl_Text.setText(GUI_STATE['Character_Ctrl_Text'])

if self.Rig_Obj_Text.text() != "":
try:
cmds.select(self.Rig_Obj_Text.text())
except Exception:
pass
self.Get_Rig_Obj()
cmds.select(cl=True)

if self.Start_JNT_Text.text() != "":
try:
cmds.select(self.Start_JNT_Text.text())
except Exception:
pass
self.Get_Start_JNT()
cmds.select(cl=True)

if self.End_JNT_Text.text() != "":
try:
cmds.select(self.End_JNT_Text.text())
except Exception:
pass
self.Get_End_JNT()
cmds.select(cl=True)

self.Rig_Generate_Check()

# 设置颜色滑竿
self.IK_CTRL_ColorSlider.setValue(int(GUI_STATE['IK_CTRL_ColorSlider']))
self.Start_Ctrl_ColorSlider.setValue(int(GUI_STATE['Start_Ctrl_ColorSlider']))
self.End_IK_ColorSlider.setValue(int(GUI_STATE['End_IK_ColorSlider']))
self.Character_Ctrl_ColorSlider.setValue(int(GUI_STATE['Character_Ctrl_ColorSlider']))

# 设置按钮颜色
if mel.eval("getApplicationVersionAsFloat;")>=2017:

r = GUI_STATE['IK_CTRL_ColorBtn'][0]
g = GUI_STATE['IK_CTRL_ColorBtn'][1]
b = GUI_STATE['IK_CTRL_ColorBtn'][2]
self.IK_CTRL_ColorBtn.setStyleSheet('background-color:rgb(%s,%s,%s)'%(r,g,b))

r = GUI_STATE['Start_Ctrl_ColorBtn'][0]
g = GUI_STATE['Start_Ctrl_ColorBtn'][1]
b = GUI_STATE['Start_Ctrl_ColorBtn'][2]
self.Start_Ctrl_ColorBtn.setStyleSheet('background-color:rgb(%s,%s,%s)'%(r,g,b))

r = GUI_STATE['End_IK_ColorBtn'][0]
g = GUI_STATE['End_IK_ColorBtn'][1]
b = GUI_STATE['End_IK_ColorBtn'][2]
self.End_IK_ColorBtn.setStyleSheet('background-color:rgb(%s,%s,%s)'%(r,g,b))

r = GUI_STATE['Character_Ctrl_ColorBtn'][0]
g = GUI_STATE['Character_Ctrl_ColorBtn'][1]
b = GUI_STATE['Character_Ctrl_ColorBtn'][2]
self.Character_Ctrl_ColorBtn.setStyleSheet('background-color:rgb(%s,%s,%s)'%(r,g,b))

else:
# 设置滑竿颜色
self.Slider_Change_Color(self.IK_CTRL_ColorBtn,self.IK_CTRL_ColorSlider.value())
self.Slider_Change_Color(self.Start_Ctrl_ColorBtn,self.Start_Ctrl_ColorSlider.value())
self.Slider_Change_Color(self.End_IK_ColorBtn,self.End_IK_ColorSlider.value())
self.Slider_Change_Color(self.Character_Ctrl_ColorBtn,self.Character_Ctrl_ColorSlider.value())

# 根据上次打开的记录expand 或者 collapse 标签
self.Rig_Obj_Toggle_Check = GUI_STATE['Rig_Obj_Toggle_Check']
self.Loc_Generate_Toggle_Check = GUI_STATE['Loc_Generate_Toggle_Check']
self.JNT_Setting_Toggle_Check = GUI_STATE['JNT_Setting_Toggle_Check']
self.JNT_Setting_Toggle_Check = GUI_STATE['JNT_Setting_Toggle_Check']
self.Rig_Generate_Toggle_Check = GUI_STATE['Rig_Generate_Toggle_Check']
self.Namespace_Toggle_Check = GUI_STATE['Namespace_Toggle_Check']
self.Window_Setting_Toggle_Check = GUI_STATE['Window_Setting_Toggle_Check']

self.Window_Setting_Toggle_Fun()
self.Rig_Obj_Toggle_Fun()
self.Loc_Generate_Toggle_Fun()
self.JNT_Setting_Toggle_Fun()
self.Rig_Generate_Toggle_Fun()
self.Namespace_Toggle_Fun()

self.Window_Setting_Toggle_Fun()
self.Rig_Obj_Toggle_Fun()
self.Loc_Generate_Toggle_Fun()
self.JNT_Setting_Toggle_Fun()
self.Rig_Generate_Toggle_Fun()
self.Namespace_Toggle_Fun()

return True
else:

if load==True:
QMessageBox.warning(self, "Warning", "加载失败\n检查路径是否正确")
return False

# 设置默认属性
self.IK_JNT_Slider.setValue(5)
self.Main_JNT_Slider.setValue(20)
self.Curve_Span_Slider.setValue(15)

self.Slider_Change_Color(self.IK_CTRL_ColorBtn,17)
self.Slider_Change_Color(self.Start_Ctrl_ColorBtn,18)
self.Slider_Change_Color(self.End_IK_ColorBtn,13)
self.Slider_Change_Color(self.Character_Ctrl_ColorBtn,14)

def Save_File_Fun(self):
Save_Path = self.Save_Path_Text.text()
check = self.Save_Json_File(path=Save_Path)
if check == True:
QMessageBox.information(self, "保存成功", "保存成功")

def Load_File_Fun(self):
Load_Path = self.Load_Path_Text.text()
check = self.Load_File(path=Load_Path,load=True)
if check:
QMessageBox.information(self, "加载成功", "加载成功")


def Browse_Save_File_Path(self):
save_file = QFileDialog.getSaveFileName(self, caption=u"保存文件到", directory=".",filter="json (*.json)")
if type(save_file) is tuple:
save_file = save_file[0]
self.Save_Path_Text.setText(QDir.toNativeSeparators(save_file))
self.Save_Path_Label.setVisible(False)
self.Save_Path_GetBtn.setVisible(True)
try :
self.Save_Path_GetBtn.clicked.disconnect()
except:
pass
self.Save_Path_GetBtn.clicked.connect(partial(self.Open_Directory,self.Save_Path_Text))

def Browse_Load_File_Path(self):
save_file = QFileDialog.getOpenFileName(self, caption=u"保存文件到", directory=".",filter="json (*.json)")
if type(save_file) is tuple:
save_file = save_file[0]
self.Load_Path_Text.setText(QDir.toNativeSeparators(save_file))
self.Load_Path_Label.setVisible(False)
self.Load_Path_GetBtn.setVisible(True)
try :
self.Load_Path_GetBtn.clicked.disconnect()
except:
pass
self.Load_Path_GetBtn.clicked.connect(partial(self.Open_Directory,self.Load_Path_Text))

def Open_Directory(self,LineEdit):
Save_Location = LineEdit.text()
if Save_Location == "":
Save_Location = os.getcwd()
else:
Save_Location_temp = Save_Location.split('.')[0]
if Save_Location_temp != Save_Location:
Save_Location = os.path.split(Save_Location)[0]

if os.path.exists(Save_Location):
subprocess.call("explorer %s" % Save_Location, shell=True)
else:
QMessageBox.warning(self, "警告", "路径不存在\n检查路径是否正确")

  这个就是json记录加上导入导出的功能,其实并不复杂,导入导出就是参考 PLB Downloader 改写的。
  至于存储和载入功能就更为简单,基本上 Alembic 导入导出工具一致
  在这个过程中最繁琐的是要写这些UI保存的代码,极度花费时间,所以这个也促成了我开发UI2CG软件的意图。

颜色按钮处理

  这个插件的其他UI功能都不算是太复杂,位移复杂一点的地方就是参考节点的对调按钮。
  但是也不过是让获取的参考对调而已,其实底层就和变量交换一样。
  所以让我最头疼的开发是 颜色按钮 的开发。
  不过幸好 Python For Maya Artist Friendly Programming 这个教程也有相关的解决方案。
  但是不幸的是教程的解决方案是针对灯光颜色的控制,而我需要的是对曲线进行着色。
  在不同的Maya版本中,颜色控制也不大一样。
颜色控制
  在Maya2015测试的时候,颜色控制制选择滑竿提供的颜色。
  而在Maya2017测试的时候则可以选择任意的颜色控制。
Maya2017 颜色控制
Maya2017 颜色控制
  因此我需要想办法模拟出Maya这种滑竿改变颜色的效果同时又可以启用颜色编辑器来编辑颜色的效果。
  本来我是想用Maya自带的颜色组合GUI的,但是这样按钮的颜色就没有办法启用颜色编辑器来修改了。
  所以我想了一个办法来模拟滑竿改变颜色的效果,那就是将滑竿颜色的数据记录成json数据,滑动滑竿的时候再次调用写好的json数据。
  原理既然如此,后面实现就相对简单了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def setColor(self,Btn=None):
# 获取灯光的颜色
styleSheet = self.IK_CTRL_ColorBtn.styleSheet().split("(")[1].split(",")
r = float(styleSheet[0])/255
g = float(styleSheet[1])/255
b = float(styleSheet[2].split(")")[0])/255
Btn_Color = (r,g,b)
cursor = QCursor()
# 打开 Maya 的颜色编辑器
color = cmds.colorEditor(rgbValue=Btn_Color,m=True,pos=[cursor.pos().x(),cursor.pos().y()])

# Maya 返回了字符串
# 我们需要手动将其转换为可用的变量
r,g,b,a = [float(c) for c in color.split()]

# 设置新的颜色值
Btn.setStyleSheet('background-color:rgb(%s,%s,%s)'%(r*255,g*255,b*255))
self.Save_Json_File()

def Slider_Change_Color(self,Btn,Index):

COLOR_INDEX = {}
with open(COLOR_INDEX_PATH,'r') as f:
COLOR_INDEX = json.load(f)
r = COLOR_INDEX[str(Index)][0]
g = COLOR_INDEX[str(Index)][1]
b = COLOR_INDEX[str(Index)][2]

Btn.setStyleSheet('background-color:rgb(%s,%s,%s)'%(r,g,b))
self.Save_Json_File()

藤蔓生长制作

  经历上面各种UI的苦难,我们终于可以回归到插件本身的功能来了。
  藤蔓生长的原理是怎样的,其实我最初是一脸茫然的,幸好绑定部门给我提供了一个样品。
  基于这个样品,我才研究出方法。

  藤蔓生长不过是使用了线性IK,也就是带曲线的IK控制器,并且将每根骨头的缩放值链接到控制器上。
  通过控制器的数值来调整骨骼的缩放值。
  另外也要在曲线上蒙皮一套驱动骨骼,用来更方便地控制曲线(这个方法就是当初我入门绑定的时候的脊椎的绑定方法,影响深刻)
  实现起来不难,难在如何更具两个参考物体,生成固定数量的骨骼。
  经过一番摸索之后,我发现可以根据两个坐标的差值,求平均然后循环递增,就可以求出两个坐标之间的距离。
  通过平均可以求出每个骨骼的位置,从而实现藤蔓的根骨骼。
  随后就是控制器的创建了,如果骨骼是偏移的,又如何才能求出控制器的旋转值呢?
  幸好Circle命令提供了根据曲线方向旋转的方法,如此一来,只要输入坐标差值,曲线就会自动沿着曲线方向了。

  后面,最大的问题就是要整理藤蔓生长绑定的层级,公司有制定了一套完整的命名规则。
  不过经过问题不大,毕竟绑定部门提供的样品就是最好的范例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
# 一键生成按钮
def Rig_Generate(self):

##############################################
########## 检查物体是否存在 ############
##############################################

if self.Rig_Obj_Check():
return

# 开启还原按钮
self.Rig_Back_Btn.setEnabled(True)

##############################################
############# 获取变量 ###############
##############################################

# 获取数值
Main_JNT_Num = int(self.Main_JNT_Num.text()) if self.Main_JNT_Num.text() != "" else 20
IK_JNT_Num = int(self.IK_JNT_Num.text()) if self.IK_JNT_Num.text() != "" else 20
Curve_Span_Num = int(self.Curve_Span_Num.text()) if self.Curve_Span_Num.text() != "" else 20

# 获取命名
Geo_Name_Text = self.Geo_Name_Text.text() if self.Geo_Name_Text.text() != "" else "TengMan"
Main_JNT_Name_Text = self.Main_JNT_Name_Text.text() if self.Main_JNT_Name_Text.text() != "" else "gan_joint"
IK_JNT_Name_Text = self.IK_JNT_Name_Text.text() if self.IK_JNT_Name_Text.text() != "" else "ganJoint"
Curve_Name_Text = self.Curve_Name_Text.text() if self.Curve_Name_Text.text() != "" else "gan"
IK_CTRL_Name_Text = self.IK_CTRL_Name_Text.text() if self.IK_CTRL_Name_Text.text() != "" else "gan_ctrl"
Start_Ctrl_Text = self.Start_Ctrl_Text.text() if self.Start_Ctrl_Text.text() != "" else "main2"
End_IK_Text = self.End_IK_Text.text() if self.End_IK_Text.text() != "" else "main"
Character_Ctrl_Text = self.Character_Ctrl_Text.text() if self.Character_Ctrl_Text.text() != "" else "Character"

# 获取按钮颜色
styleSheet = self.IK_CTRL_ColorBtn.styleSheet().split("(")[1].split(",")
r = float(styleSheet[0])/255
g = float(styleSheet[1])/255
b = float(styleSheet[2].split(")")[0])/255
IK_CTRL_ColorBtn = (r,g,b)

styleSheet = self.Start_Ctrl_ColorBtn.styleSheet().split("(")[1].split(",")
r = float(styleSheet[0])/255
g = float(styleSheet[1])/255
b = float(styleSheet[2].split(")")[0])/255
Start_Ctrl_ColorBtn = (r,g,b)

styleSheet = self.End_IK_ColorBtn.styleSheet().split("(")[1].split(",")
r = float(styleSheet[0])/255
g = float(styleSheet[1])/255
b = float(styleSheet[2].split(")")[0])/255
End_IK_ColorBtn = (r,g,b)

styleSheet = self.Character_Ctrl_ColorBtn.styleSheet().split("(")[1].split(",")
r = float(styleSheet[0])/255
g = float(styleSheet[1])/255
b = float(styleSheet[2].split(")")[0])/255
Character_Ctrl_ColorBtn = (r,g,b)

##############################################
############### 绑定 ###############
##############################################

# 获取起始节点和结束节点坐标


cmds.select(self.Start_JNT)
Start_JNT_Coordinate = cmds.xform(q=True,a=True,ws=True,piv=True)

cmds.select(self.End_JNT)

End_JNT_Coordinate = cmds.xform(q=True,a=True,ws=True,piv=True)
cmds.select(cl=True)



# 批量生成主关节
Xdistance = End_JNT_Coordinate[0] - Start_JNT_Coordinate[0]
Ydistance = End_JNT_Coordinate[1] - Start_JNT_Coordinate[1]
Zdistance = End_JNT_Coordinate[2] - Start_JNT_Coordinate[2]

# 主控制器
cmds.circle( nr=(Xdistance, Ydistance, Zdistance), c=(End_JNT_Coordinate[0], End_JNT_Coordinate[1], End_JNT_Coordinate[2]),n=End_IK_Text,r=1)
Main_Ctrl = cmds.ls(sl=True)[0]
cmds.setAttr(Main_Ctrl + ".overrideEnabled",1)

if mel.eval("getApplicationVersionAsFloat;")>=2017:
cmds.setAttr(Main_Ctrl + ".overrideRGBColors",1)
cmds.setAttr(Main_Ctrl + ".overrideColorRGB",End_IK_ColorBtn[0],End_IK_ColorBtn[1],End_IK_ColorBtn[2])
else:
cmds.setAttr( Main_Ctrl +".overrideRGBColors",0)
cmds.setAttr( Main_Ctrl +".overrideColor",self.End_IK_ColorSlider.value())


cmds.setAttr(Main_Ctrl+".visibility",lock=True,keyable=False,channelBox=False)
cmds.setAttr(Main_Ctrl+".sx",lock=True,keyable=False,channelBox=False)
cmds.setAttr(Main_Ctrl+".sy",lock=True,keyable=False,channelBox=False)
cmds.setAttr(Main_Ctrl+".sz",lock=True,keyable=False,channelBox=False)
cmds.addAttr(ln="show_mod",at="double",min=0,max=1,dv=1)
cmds.setAttr(Main_Ctrl+".show_mod",edit=True,keyable=True)
cmds.addAttr(ln="grow",at="double",min=0,max=10,dv=10)

for Main_JNT in range(Main_JNT_Num):
x = Xdistance * Main_JNT / (Main_JNT_Num-1) + Start_JNT_Coordinate[0]
y = Ydistance * Main_JNT / (Main_JNT_Num-1) + Start_JNT_Coordinate[1]
z = Zdistance * Main_JNT / (Main_JNT_Num-1) + Start_JNT_Coordinate[2]
JNT = cmds.joint( p=(x, y, z), n = Main_JNT_Name_Text + str(Main_JNT+1))
cmds.setAttr( JNT + ".tx", k=False,cb=True )
cmds.setAttr( JNT + ".ty", k=False,cb=True )
cmds.setAttr( JNT + ".tz", k=False,cb=True )
cmds.setAttr( JNT + ".rx", k=False,cb=True )
cmds.setAttr( JNT + ".ry", k=False,cb=True )
cmds.setAttr( JNT + ".rz", k=False,cb=True )
cmds.setAttr( JNT + ".sx", k=False,cb=True )
cmds.setAttr( JNT + ".sy", k=False,cb=True )
cmds.setAttr( JNT + ".sz", k=False,cb=True )
cmds.setAttr( JNT + ".visibility", k=False,cb=True )
cmds.expression( s=JNT + ".sx = "+ Main_Ctrl +".grow/10/"+str(Main_JNT+1) )
cmds.expression( s=JNT + ".sy = "+ Main_Ctrl +".grow/10/"+str(Main_JNT+1) )
cmds.expression( s=JNT + ".sz = "+ Main_Ctrl +".grow/10/"+str(Main_JNT+1) )
if Main_JNT == 0:
Start_Main_JNT = cmds.ls(sl=True)[0]

# 生成IK控制器
End_Main_JNT = cmds.ls(sl=True)
cmds.select(Start_Main_JNT)
cmds.select(End_Main_JNT,tgl=True)
ikList = cmds.ikHandle(sol="ikSplineSolver" )

# 生成控制器
cmds.select(cl=True)
CtrlList = []
for IK_JNT in range(IK_JNT_Num):
x = Xdistance * IK_JNT / (IK_JNT_Num-1) + Start_JNT_Coordinate[0]
y = Ydistance * IK_JNT / (IK_JNT_Num-1) + Start_JNT_Coordinate[1]
z = Zdistance * IK_JNT / (IK_JNT_Num-1) + Start_JNT_Coordinate[2]
cmds.joint( p=(x, y, z), n = IK_JNT_Name_Text + str(IK_JNT+1))

Curent_JNT = cmds.ls(sl=True)[0]

IK_JNT_Coordinate = cmds.xform(q=True,ws=True,t=True)

cmds.circle( nr=(Xdistance, Ydistance, Zdistance), c=(IK_JNT_Coordinate[0], IK_JNT_Coordinate[1], IK_JNT_Coordinate[2]),n=IK_CTRL_Name_Text+str(IK_JNT+1),r=0.4)

IK_Ctrl = cmds.ls(sl=True)[0]

cmds.setAttr( IK_Ctrl +".overrideEnabled",1)

if mel.eval("getApplicationVersionAsFloat;")>=2017:
cmds.setAttr( IK_Ctrl +".overrideRGBColors",1)
cmds.setAttr( IK_Ctrl +".overrideColorRGB",IK_CTRL_ColorBtn[0],IK_CTRL_ColorBtn[1],IK_CTRL_ColorBtn[2])
else:
cmds.setAttr( IK_Ctrl +".overrideRGBColors",0)
cmds.setAttr( IK_Ctrl +".overrideColor",self.IK_CTRL_ColorSlider.value())

cmds.CenterPivot()
# 冻结变换
cmds.makeIdentity( apply=True, t=1, r=1, s=1, n=2 )

CtrlList.append(cmds.ls(sl=True)[0])

cmds.parentConstraint( CtrlList[IK_JNT] , Curent_JNT )

cmds.select(Curent_JNT)
cmds.setAttr( Curent_JNT + ".tx", k=False,cb=True )
cmds.setAttr( Curent_JNT + ".ty", k=False,cb=True )
cmds.setAttr( Curent_JNT + ".tz", k=False,cb=True )
cmds.setAttr( Curent_JNT + ".rx", k=False,cb=True )
cmds.setAttr( Curent_JNT + ".ry", k=False,cb=True )
cmds.setAttr( Curent_JNT + ".rz", k=False,cb=True )
cmds.setAttr( Curent_JNT + ".sx", k=False,cb=True )
cmds.setAttr( Curent_JNT + ".sy", k=False,cb=True )
cmds.setAttr( Curent_JNT + ".sz", k=False,cb=True )
cmds.setAttr( Curent_JNT + ".visibility", k=False,cb=True )

if IK_JNT == 0:
Start_IK_JNT = cmds.ls(sl=True)[0]


# 将IK关节蒙皮到曲线上
cmds.select(ikList[2])
cmds.rebuildCurve( rt=0, s=Curve_Span_Num )
cmds.select(Start_IK_JNT,hi=True)
cmds.select(ikList[2],tgl=True)
# 绑定设置
cmds.optionVar(iv=('bindTo',2))
cmds.optionVar(iv=('skinMethod',1))
cmds.optionVar(iv=('multipleBindPosesOpt',1))
cmds.optionVar(iv=('bindMethod',1))
cmds.optionVar(iv=('removeUnusedInfluences',0))
cmds.optionVar(iv=('colorizeSkeleton',0))
cmds.optionVar(iv=('maxInfl',3))
cmds.optionVar(iv=('normalizeWeights',2))
cmds.optionVar(iv=('obeyMaxInfl',2))

cmds.SmoothBindSkin()

# 绑定主物体
cmds.select(Start_Main_JNT,hi=True)
cmds.select(self.Rig_Obj,tgl=True)
cmds.SmoothBindSkin()

##############################################
############# 管理层级 ###############
##############################################
# 删除参考
if self.Delete_CheckBox.isChecked():
cmds.delete(self.Start_JNT)
cmds.delete(self.End_JNT)

# 模型打组
cmds.select(self.Rig_Obj)
Geo_Grp = cmds.group(n=Geo_Name_Text+"_geo")

# 控制器打组
CtrlList = list(reversed(CtrlList))
Grp = ""
for Ctrl in CtrlList:
cmds.select(Ctrl)
cmds.group(n=Ctrl+"_C")
IKFKC_Grp = cmds.group(n=Ctrl+"_IKFKC")
if Grp != "":
cmds.parent( Grp, IKFKC_Grp )
cmds.select(IKFKC_Grp)
Grp = cmds.group(n=Ctrl+"_G")

# MotionSystem 打组
cmds.select(Start_IK_JNT)
temp = cmds.group(n=Start_IK_JNT+"_G")
cmds.setAttr(temp+".visibility",0)
cmds.setAttr(temp+".visibility",lock=True)
cmds.setAttr( temp + ".tx", lock=True )
cmds.setAttr( temp + ".ty", lock=True )
cmds.setAttr( temp + ".tz", lock=True )
cmds.setAttr( temp + ".rx", lock=True )
cmds.setAttr( temp + ".ry", lock=True )
cmds.setAttr( temp + ".rz", lock=True )
cmds.setAttr( temp + ".sx", lock=True )
cmds.setAttr( temp + ".sy", lock=True )
cmds.setAttr( temp + ".sz", lock=True )
cmds.select(Grp,tgl=True)
MotionSystem_Grp = cmds.group(n="MotionSystem")


# DeformationSystem 打组
cmds.select(Start_Main_JNT)
Start_Main_JNT_Grp = cmds.group(n=Start_Main_JNT+"_G")
cmds.setAttr( Start_Main_JNT_Grp+".visibility",0)
cmds.setAttr( Start_Main_JNT_Grp+".visibility",lock=True)
cmds.setAttr( Start_Main_JNT_Grp + ".tx", lock=True )
cmds.setAttr( Start_Main_JNT_Grp + ".ty", lock=True )
cmds.setAttr( Start_Main_JNT_Grp + ".tz", lock=True )
cmds.setAttr( Start_Main_JNT_Grp + ".rx", lock=True )
cmds.setAttr( Start_Main_JNT_Grp + ".ry", lock=True )
cmds.setAttr( Start_Main_JNT_Grp + ".rz", lock=True )
cmds.setAttr( Start_Main_JNT_Grp + ".sx", lock=True )
cmds.setAttr( Start_Main_JNT_Grp + ".sy", lock=True )
cmds.setAttr( Start_Main_JNT_Grp + ".sz", lock=True )
DeformationSystem_Grp = cmds.group(n="DeformationSystem")

# 主控制器打组
cmds.circle( nr=(Xdistance, Ydistance, Zdistance), c=(Start_JNT_Coordinate[0], Start_JNT_Coordinate[1], Start_JNT_Coordinate[2]),n=Start_Ctrl_Text,r=1)

Main2_Ctrl = cmds.ls(sl=True)[0]

cmds.setAttr(Main2_Ctrl + ".overrideEnabled",1)
if mel.eval("getApplicationVersionAsFloat;")>=2017:
cmds.setAttr(Main2_Ctrl + ".overrideRGBColors",1)
cmds.setAttr(Main2_Ctrl + ".overrideColorRGB",Start_Ctrl_ColorBtn[0],Start_Ctrl_ColorBtn[1],Start_Ctrl_ColorBtn[2])
else:
cmds.setAttr( Main2_Ctrl +".overrideRGBColors",0)
cmds.setAttr( Main2_Ctrl +".overrideColor",self.Start_Ctrl_ColorSlider.value())



cmds.setAttr(Main2_Ctrl+".visibility",lock=True,keyable=False,channelBox=False)
cmds.setAttr(Main2_Ctrl+".sx",lock=True,keyable=False,channelBox=False)
cmds.setAttr(Main2_Ctrl+".sy",lock=True,keyable=False,channelBox=False)
cmds.setAttr(Main2_Ctrl+".sz",lock=True,keyable=False,channelBox=False)

cmds.select(DeformationSystem_Grp)
cmds.select(MotionSystem_Grp,tgl=True)
cmds.select(Main2_Ctrl,tgl=True)
cmds.parent()

cmds.pickWalk( direction='up' )
Main2_Ctrl = cmds.ls(sl=True)[0]
Main2_Ctrl_C = cmds.group(n=Main2_Ctrl+"_C")
Main2_Ctrl_G = cmds.group(n=Main2_Ctrl+"_G")
Main2_Ctrl = cmds.ls(sl=True)[0]

cmds.setAttr(Main_Ctrl+".grow",edit=True,keyable=True)
cmds.connectAttr(Main_Ctrl+".show_mod",Geo_Grp+".visibility")
cmds.parent(Main2_Ctrl,Main_Ctrl)
cmds.pickWalk( direction='up' )
cmds.group(n=Main_Ctrl+"_C")
cmds.group(n=Main_Ctrl+"_G")

Main_Ctrl_G = cmds.ls(sl=True)[0]

cmds.curve(d=1, p=[(-1, 0,-1), (-1, 0,1), (1, 0, 1), (1,0, -1), (-1, 0,-1)], k=[0,1,2,3,4] ,n=Character_Ctrl_Text)

Character = cmds.ls(sl=True)[0]
cmds.setAttr(Character + ".overrideEnabled",1)

if mel.eval("getApplicationVersionAsFloat;")>=2017:
cmds.setAttr(Character + ".overrideRGBColors",1)
cmds.setAttr(Character + ".overrideColorRGB",Character_Ctrl_ColorBtn[0],Character_Ctrl_ColorBtn[1],Character_Ctrl_ColorBtn[2])
else:
cmds.setAttr( Character +".overrideRGBColors",0)
cmds.setAttr( Character +".overrideColor",self.Character_Ctrl_ColorSlider.value())


cmds.setAttr(Character+".visibility",lock=True,keyable=False,channelBox=False)
cmds.xform(a=True,ws=True,t=(End_JNT_Coordinate[0], End_JNT_Coordinate[1], End_JNT_Coordinate[2]))
cmds.parent(Main_Ctrl_G,Character)
cmds.pickWalk( direction='up' )
cmds.group(n=Character+"_C")
cmds.group(n=Character+"_G")
Grp = cmds.group(n=Geo_Name_Text+"_rig")

# 设置 other_G 中的属性
cmds.setAttr(ikList[2]+".visibility",0)
cmds.setAttr(ikList[2]+".visibility",lock=True,keyable=False,channelBox=False)
cmds.setAttr(ikList[0]+".visibility",0)
cmds.setAttr(ikList[0]+".visibility",lock=True)
cmds.setAttr( ikList[0] + ".tx", lock=True )
cmds.setAttr( ikList[0] + ".ty", lock=True )
cmds.setAttr( ikList[0] + ".tz", lock=True )
cmds.setAttr( ikList[0] + ".rx", lock=True )
cmds.setAttr( ikList[0] + ".ry", lock=True )
cmds.setAttr( ikList[0] + ".rz", lock=True )
cmds.setAttr( ikList[0] + ".sx", lock=True )
cmds.setAttr( ikList[0] + ".sy", lock=True )
cmds.setAttr( ikList[0] + ".sz", lock=True )
cmds.setAttr( ikList[0] + ".poleVectorX", lock=True )
cmds.setAttr( ikList[0] + ".poleVectorY", lock=True )
cmds.setAttr( ikList[0] + ".poleVectorZ", lock=True )
cmds.setAttr( ikList[0] + ".offset", lock=True )
cmds.setAttr( ikList[0] + ".roll", lock=True )
cmds.setAttr( ikList[0] + ".twist", lock=True )
cmds.setAttr( ikList[0] + ".ikBlend", lock=True )

# 重命名曲线
cmds.rename(ikList[2],Curve_Name_Text)
# other_G 打组
cmds.select(ikList[0])
cmds.select(Curve_Name_Text,tgl=True)
cmds.group(n="IK_G")
cmds.group(n="other_G")
cmds.pickWalk( direction='up' )
cmds.select(Grp,tgl=True)
cmds.parent()
Other_Grp = cmds.pickWalk( direction='up' )

cmds.select(Geo_Grp)
self.Main_Grp = cmds.group(n=Geo_Name_Text+"_all")
cmds.select(Other_Grp)
cmds.select(self.Main_Grp,tgl=True)
cmds.parent()

总结

  这个藤蔓生长工具可谓上煞费苦心,其实实现功能并不苦难,但是我想要在基础功能的基础上,搭建出属于自己的UI形式,不仅可以导入导出插件的所有设置,还可以切换不同的窗口风格,这些虽然不是公司要求我去弄的,但是却是我在开发过程中的一道坎,可能开到ADV实现的效果,自己不甘心甘拜下风吧。
  所以这个开发,耗费的时间几乎有一个星期了,但是最后能够开发出来,我还是非常开怀的。
  也是因为这个插件的启发,我才有了后面UI2CG软件开发的想法,后面一定会写一篇文章详细说说这个自动完成基础代码的软件的。