MAXScript学习笔记(1)

目录

一、入门教程

二、字符串操作

1.数字以及其他转换成字符串并一起打印 要加上as

2.print后面有语句加上括号

3.对于有变量的字符串 用format打印更方便

4.分解字符串为数组 filterstring

5.根据内容替换字符串 substitutestring

6.删除字符串中的空格

三、代码组织

一、基本知识点

二、代码组织

三、问题

四、UI创建

1.ListView

2.一般.net control属性事件

五、其他操作

1.一行内多个语句要用;分开

2.删除模型

3.单位换算

4.自定义排序

5.中文文本输出 

6.“仅”旋转轴

六、JSON


一、入门教程

b站上找的教程,

有英文版的,也能看懂,费脑子,找中文的,

1.MaxScript脚本教程-人人都学得会的教程(https://www.bilibili.com/video/BV1AE411q7d7?p=1

讲基本语法的

最新的一集(11MeshSurface)看的头晕,跟着抄下来,各种错误,他期间没有调试过,修改一两次就能运行了,我的一堆的错误,最后把源代码拷贝过来看了下。

这个教程是按照他推荐的书本的顺序讲的,还没讲到UI,除了11集的例子。

现在是20年4月22日,他最新的11集的录制时间是3月20日,后续还没有。

原本为什么先看这个视频,7小时多,我就喜欢看多的。但,内容还是不够。面向初学者,特别是没有编程基础的,就不能讲的太快,太辅助。

他的父类子类的概念我是不认同的,作为一个程序员,父类是基础,子类是扩展。而不是他说的,父类是全部,子类是部分继承。

2.【教程】Maxscript系列教程(https://www.bilibili.com/video/BV1o4411D7xf

讲UI的

里面的第一课的vscode环境安装挺好的

简单讲了点UI,和上面的能互相补充一些。

------------------

这两个日期都录制挺新的,但他们用的max版本倒是挺旧的,还是2014。

我是用的2019,公司建模人员用的2016。

怎么说呢,作为开发人员,工具还是要不断与时俱进的。

还有好多教程,时间有限,先开始干活吧,目标做一个处理Revit导入的模型,并导出给Unity的脚本。

需要1.建筑根据楼层设置分组 2.基本单元模型 3.保存模型坐标信息并到unity中重建场景

二、字符串操作

1.数字以及其他转换成字符串并一起打印 要加上as

任何类型的都要 加上as 没什么 主要是还要前后加上括号,不如自己写一个toString呢

a=1

b="a:"+(a as string)

2.print后面有语句加上括号

可能因为print的运算符优先级和其他的运算符优先级一起时会,没有括号明确会出问题

 print classof a => print (classof a) 

特别是要打印某个内容时

print "-----------------------"
struct st1(name)
sList=#()
for i=1 to 5 do
(
	s=st1()
	s.name="s:"+(i as string)
	append sList s
)

for s in sList do
(
	--print "name:"+s.name --这边不行,打印不出来name
	print ("name:"+s.name)
)
print "-----------------------"

不加括号和加括号的结果

简单一点

print "-----------------------"
sl=#("a","b","c")
for s in sl do
(
	print "s:"+s
)
print "-----------------------"

结果

Listener里面单独运行一句的话能看到name的内容

对我来说算是一个坑吧,看不到想打印的内容,还以为内容是空的

3.对于有变量的字符串 用format打印更方便

a=1
b=2
format "a:%,%,%\n" a b b

但是format的返回值是OK,不能传递给变量用 c=format ... 这样的

4.分解字符串为数组 filterstring

5.根据内容替换字符串 substitutestring

不是replace,replace是替换具体的位置上的字符串,这种的不是我需要的。

6.删除字符串中的空格

没有找到trim()这样的操作,

找到一个代码:

fn RemoveAllWhiteSpace inputString = (
	local outputString = StringStream ""
	for x in filterString inputString " \t\r\n" do format "%" x to:outputString
	outputString as string
)

参考:http://www.scriptspot.com/forums/3ds-max/general-scripting/how-to-rename-without-white-spaces-in-3dmax-objects

简单的话 用

a="a 1 2 3 "
b=substitutestring a " " ""

也是可以的

三、代码组织

基于struct和filein的代码组织结构

一、基本知识点

1.结构体可以当类来用,里面可以放属性也可以放方法(函数), on create do (...) 相当于构造函数吧,看别人代码知道的。

2.使用filein能把另一个ms文件的内容引用过来,另外有个include,也能加载其他脚本,但是该文件内的变量的作用域还是那个文件里面。如A文件include了一个B文件,B文件里面的变量(包括struct定义)是无法在A文件中使用的。所以,对我来说基本没用。复用代码和组织代码结构,基本应该是用filein的。例子里面倒是可以考虑把一些简单的界面放到另一个文件再include进来。

参考《3dMAXScprit脚本语言完全学习手册 王华.pdf》

因为时间有限,把这本书当资料查,一开始没注意include里面的作用域提示,花了点时间,一定要用include.....,最后在知道我要的是filein

二、代码组织

1.经验

对于习惯写脚本语言还有面向过程的语言,javascript和c,一个代码文件可以有几千上万行代码,再加上有些人没有代码复用的概念,copy copy copy 最后代码可读性很差 而且耦合度高 功能写好后其他人阅读修改成本很高,就算是“高手”最后想重构都无能为力,成本太高了,所以一开始就要注意代码组织。

我们公司就有这种实际例子,一个学c的老程序员写c#代码,那是惨不忍睹,你还没法说他。一个有点经验的前端javascript写的代码,也是,一块块的copy的代码,各种全局变量耦合在一起。这些代码很快会“死亡”,无法扩展和复用,活不到下一个项目,对于公司来说就是浪费钱,而不懂技术的领导根本看不到这点。

2.思路

分两步

第一步,将不同的代码模块放到独立的文件中,使用filein加载。

第二部,将文件中的内容都放到一个struct定义中,并创建一个该struct创建的变量,使用时从这个标量引用出来

如:

-------------------------Test.ms-----------------------
struct TestClass
(
	v1,v2,
	fn printTest =(
		print "TestClass.printTest1"
		print "TestClass.printTest2"
	)
)
Test=TestClass()

-----------------------Main.ms----------------------------------
filein "Test.ms"
Test.printTest()

碰到一个问题,struct怎么放到struct里面?

默认状态下是做不到的,struct里面不能直接放struct定义,struct只能放变量和函数。想想办法,javascript低版本时还不是在有限的语法环境下实现了类的继承。

查到一个帖子:https://www.reddit.com/r/3dsmax/comments/5mhl5q/magical_nested_structs_in_maxscript/

需求和我一样,要组织代码,这是只有“程序员”才会想的,“写脚本”的人是不会这么考虑的。

根据该帖子有两种方式,1.将struct放到函数里,2.将struct放到变量里

放到函数里面的方式:

-------------------------Test.ms-----------------------
struct TestClass
(
	v1,v2,
	fn printTest =(
		print "TestClass.printTest1"
		print "TestClass.printTest2"
	),
	fn Class2 = (
		(	
			struct _ --这里不和外面的函数名Class2重名
			(
				first,second,
				fn PP =
				(
					print "PP"
				)
			)
		)()
	)
)
Test=TestClass()

-----------------------Main.ms----------------------------------
filein "Test.ms"
Test.printTest()
a=Test.Class2()
a.PP()

放到变量里面的方式:

-------------------------Test.ms-----------------------
struct TestClass
(
	v1,v2,
	fn printTest =(
		print "TestClass.printTest1"
		print "TestClass.printTest2"
	),
	fn Class2 = (
		(	
			struct _ --这里不和外面的函数名Class2重名
			(
				first,second,
				fn PP =
				(
					print "PP1"
				)
			)
		)()
	),
	-- Class3=1
	Class3=
	(
		struct Class3--这里必须和外面的变量名一致,Note that the member name must be the same as its associated struct name
		(
			first,second,three,
			fn PP2 =
			(
				print "PP2"
			)
		)
	)
)
Test=TestClass()
-----------------------Main.ms----------------------------------
filein "Test.ms"
Test.printTest()
a=Test.Class2()
a.PP()
b=Test.Class3()
b.PP2()

其实第一种方式我还能理解,第二种则没见过,按照他的解释,是这个变量被重新定义了,从一个不同变量变成了一个struct...

算了,能用就行吧。

从对原来的代码的改动量来看的话,用变量的方式比较小,就用它了。

--------

在代码重构的过程中,即把一个文件的代码放到一个struct的过程中,发现,我原本在struct外面的代码,放到里面后,不修改原来引用的地方也不会出错。而重启一下3dmax后就会出错,说明定义运行后有缓存在环境中。之所以一般代码修改后是有效,应该是覆盖了。而放到struct中后,原来的全局的struct每人去改它,暂时还是能用的。

这个让我怀疑局部函数是否需要有个local来明确一下,最后发现不需要。

-------------------------------

三、问题

一个struct中的两个struct直接是无法调用的,会出现错误。别说struct了,外部struct定位的任何变量都不能再内部struct中使用。

找到一个信息(http://download.autodesk.com/us/3dsmax/2014/3dsMax2014_Readme_CHS.html

难道不能使用嵌套的结构定义吗?

研究了一下,只找到一个办法,structA要用structB,将structA和structB不用放到一个外部struct中,分别放到两个不同的struct中

修改一下这种方式,不需要一个TestClass2,

使用时还是一样的 c4=Test.Class4()

其实可以全部的struct都这样,不用一口气都放到外部struct里,这样更灵活。

四、UI创建

简单的界面用自带的就行,复杂的界面需要用.net的,官方文档中也有提到ActiveX控件已经过时了,测试也没弄出来。

1.ListView

1.1 基本

fn initListView lv =
	(
	lv.gridlines = true
	lv.view = (dotNetclass "System.Windows.forms.View").Details
	lv.fullRowSelect = true
	lv.Columns.Clear()

	layout_def = #("Name", "Count", "Class")
	
	for i = 1 to layout_def.count do
		(
		case i of 
			(
			1: lv.Columns.add layout_def[1] 124
			2: lv.Columns.add layout_def[2] 46
			3: lv.Columns.add layout_def[3] 98
			)
		)
	)

fn fillListView lv items=
	(
	lv.Items.Clear()
	theRange = #()
	for i in items do
		(
		li = dotNetObject "System.Windows.Forms.ListViewItem"  (i as string)
		sub_li = li.SubItems.add (BaseObjCount i as string)
		sub_li = li.SubItems.add (classof i as string)
		append theRange li
		)
	lv.Items.AddRange theRange 
	)

rollout subBaseInfo "基本信息"
(
	button btnRefreshInfo "刷新"
	dotNetControl lv_objects "System.Windows.Forms.ListView" width:300 height:200 align:#center
	fn initInfo =
	(
		baseObjs=collectBaseObj()
		initListView lv_objects
		fillListView lv_objects baseObjs
	)
	on subBaseInfo open do
	(	
		initInfo();
		lv_objects.height=(25+(baseObjs.count*14))
	)
)

参考:http://www.scriptspot.com/3ds-max/scripts/modifier-modifier-zorb

1.2封装TableView

封装了一下,创建了一个struct TableView,方便复用

struct ListViewColumn
(
	title,width
)
struct TableView
(
	lvControl,
	Columns,
	Rows,
	fn AddColumn tt wt=
	(
		-- print ("AddColumn:"+tt+(wt as string))
		if Columns == undefined do Columns=#()
		column=ListViewColumn title:tt width:wt
		append Columns column
	),
	fn AddColumns cols=
	(
		for c in cols do AddColumn c[1] c[2]
	),
	fn AddRow row =
	(
		--print ("AddRow")
		--print row
		if Rows == undefined do Rows =#()
		append Rows row -- row实际上是个vals=#()
	),
	fn initListView lv =
	(
		lv.gridlines = true
		lv.view = (dotNetclass "System.Windows.forms.View").Details
		lv.fullRowSelect = true
		lv.Columns.Clear()
		for column in this.Columns do 
		(
			lv.Columns.add column.title column.width
		)
	),

	fn fillListView lv rows=
	(
		lv.Items.Clear()
		theRange = #()
		if rows != undefind do
		(
			for row in rows do
			(
				li=undefined
				for i=1 to Columns.count do
				(
					if i==1 then
					(
						li= dotNetObject "System.Windows.Forms.ListViewItem"  (row[i] as string)
					)
					else(
						sub_li = li.SubItems.add (row[i] as string)
					)
				)
				append theRange li
					--print row
			)
			lv.Items.AddRange theRange
		)
	),
	fn ClearRows =
	(
		this.Rows=#()
	),
	fn Show lv=
	(
		initListView lv
		fillListView lv this.Rows
	)
)

使用时

单从一个列表界面的实现来看,代码还真多了,复杂了,但是把界面和数据分开,封装界面内容后,ListView在其他地方用起来就很方便了,使用的人可以不用关系怎么操作ListView的,如另一处使用的地方

1.3 选中事件

on lv SelectedIndexChanged do (print "SelectedIndexChanged")

或者MouseClick也行

on lv MouseClick do (print "MouseClick")

获取选中的行(序号或者内容)

index=lv_objects.SelectedIndices.Item 0
item =lv_objects.selectedItems.Item 0
print item.Text

MaxScript中还有个ListViewOp,可以获取

index=ListViewOps.GetSelectedIndex lv_objects

参考:http://help.autodesk.com/view/3DSMAX/2019/ENU/?guid=GUID-3E7E4EA4-0FCC-4C5A-8825-4DE065209B23

#Struct:ListViewOps(
		InitImageList:<fn>; Public,
		SetLvItemCheck:<fn>; Public,
		MXSColor_to_dotNetColor:<fn>; Public,
		m_dnColor:<data>; Public,
		GetLvSingleSelected:<fn>; Public,
		DeleteLvItem:<fn>; Public,
		SetLvItemRowColor:<fn>; Public,
		GetLvItems:<fn>; Public,
  HighLightLvItem:<fn>; Public,
  SetFontStyle:<fn>; Public,
  GetLvSelection:<fn>; Public,
  GetLvItemCheck:<fn>; Public,
  GetLvItemCount:<fn>; Public,
  dotNetColor_to_MXSColor:<fn>; Public,
  ClearLvItems:<fn>; Public,
  GetLvItemByname:<fn>; Public,
  m_iconClassType:<data>; Public,
  IsIconFile:<fn>; Public,
  ClearColumns:<fn>; Public,
  GetLvItemName:<fn>; Public,
  m_bitmapClassType:<data>; Public,
  AddLvItem:<fn>; Public,
  AddLvColumnHeader:<fn>; Public,
  SetLvItemName:<fn>; Public,
  initListView:<fn>; Public,
  m_bStyle:<data>; Public,
  refreshListView:<fn>; Public,
  SetForeColor:<fn>; Public,
  GetSelectedIndex:<fn>; Public,
  SelectLvItem:<fn>; Public)

ps:还有个TreeViewOp

----------------------------------

2.一般.net control属性事件

参考:http://help.autodesk.com/view/3DSMAX/2019/ENU/?guid=GUID-39D7680D-BE10-401A-B471-A4D64D81CAC2

创建:

dotNetControl <name> <class_type_string> [{<property_name>:<value>}]

dotNetControl f1 "System.Windows.Forms.MonthCalendar" align:#left height:180

获取属性名称:

getPropNames <dotNetControl> [showStaticOnly:<bool>] [declaredOnTypeOnly:<bool>]

获取属性:

showProperties <dotNetControl> ["prop_pat"] [to:<stream>] [showStaticOnly:<bool>] [showMethods:<bool>] [showAttributes:<bool>] [declaredOnTypeOnly:<bool>]

获取函数:

showMethods <dotNetControl> ["prop_pat"] [to:<stream>] [showStaticOnly:<bool>] [showSpecial:<bool>] [showAttributes:<bool>] [declaredOnTypeOnly:<bool>]

获取事件:

showEvents <dotNetControl> ["prop_pat"] [to:<stream>] [showStaticOnly:<bool>] [declaredOnTypeOnly:<bool>]

 

 

五、其他操作

1.一行内多个语句要用;分开

 跨行不需要,因为我是C#的,一开始总是不注意在语句结束时加上';',还要特意删一下。

 a=1 b=2 是会出错,a=1;b=2 没问题

2.删除模型

不要在for里面一个个的删除模型,不然3dmax会报异常,放到一个数组中一起删除

3.单位换算

从脚本上获取的pos的坐标值不是人操作时看到的,需要用【units.formatValue】转换一下,比如我要转换成mm。因为mm单位的数值才是和revit或者unity等其他软件一致的单位,可能相差1000倍吧。

另外,因为数值上不会是完全准确的,在revit中是1200,到3dmax中就变成了1199.999,或者1200.001,需要转换一下。

相应的写了点代码

	fn RoundTo val n =
	(
		local mult = 10.0 ^ n
		(floor ((val * mult) + 0.5)) / mult
	),
	---Common.roundInteger 6.9
	---Common.roundInteger 7.1
	fn RoundInteger val =
	(
		(RoundTo val 0) as integer
	),
	fn GetPosVal val =
	(
		val=units.formatValue val
		val=substituteString val "mm" "" --mm单位去掉
		val=Common.roundInteger (val as Number)
		return val
	),

4.自定义排序

一个基本数组可以直接用sort排序,但是一个坐标数组,或者struct数组,需要自定义排序,需要用qsort

如:

	fn PositionXSort n1 n2 = 
	(
	if n1.x < n2.x then -1 else if n1.x > n2.x then 1 else 0 
	),
	fn PositionYSort n1 n2 = 
	(
	if n1.y < n2.y then -1 else if n1.y > n2.y then 1 else 0 
	),
	fn PositionZSort n1 n2 = 
	(
	if n1.z < n2.z then -1 else if n1.z > n2.z then 1 else 0 
	),
	-- cascade components , let's say X, Y, Z
	fn PositionSort n1 n2 = 
	(
		if (v = PositionXSort n1 n2) != 0 then v 
		else if (v = PositionYSort n1 n2) != 0 then v 
		else PositionZSort n1 n2    
	),
	fn SortPos posList =
	(
		qsort posList PositionSort
	)

5.中文文本输出 

网上一般搜索到的文本输出的例子代码是 openFile 加上 format就好了,参考:https://www.cnblogs.com/3dxy/p/3961003.html

我需要改一下,每次新的内容不是跟在后面,而是重新创建,加了个deleteFile。

比较麻烦的问题是 中文输出到文件中变成了??,我真是满头的问好了,谁都没提到这个,文件编码格式也是utf-8的,也没找到设置编码格式的地方。google了也没找到。

本来考虑用.net的类的File来输出文件试试,不过先找到了个转换成base64的代码,也是用.net的类的。

结合起来,读取时再转换一下就好了。

	fn base64Str str =
	(
		encoding = dotnetclass "system.text.encoding"
		base64class = dotnetclass "system.convert"
		byteArr = encoding.UTF8.getbytes str
		base64 = base64class.tobase64string byteArr
	),
	--64码转字符串
	fn strBase64 b64 =
	(
		encoding = dotnetclass "system.text.encoding"
		base64class = dotnetclass "system.convert"
		encoding.UTF8.getstring (base64class.frombase64string b64)
	),
	fn WriteText filepath filetext isAppend:false useBase64:true=
	(
		if doesFileExist filepath == true 
			then
		(
			fin=undefined
			if isAppend then 
			(
				fin = openfile filepath mode:"r+"
				seek fin #eof
			)
			else
			(
				deletefile filepath
				fin = createFile filepath
			)
			-- filetext="中文123"
			txt = filetext + "\n"
			if useBase64 do txt=base64Str txt
			format txt to:fin
			-- print txt to:fin --一样可以输出,但是前后会多出双引号
			close fin
		)
		else
		(
			newfile = createFile filepath
			close newfile
			WriteText filepath filetext
			)
		)  -- 逐行写入文本
	-- WriteText "C:\Users\Administrator\Desktop\2.txt" "你好MAXScript"

6.“仅”旋转轴

移动轴容易的,$.pivot=[10,20,30],就行了。但是旋转轴就比较麻烦了。

手动操作是点击层,“仅影响轴”,然后旋转。

搜索 “maxscript 仅影响轴”或者百度上“maxscript effetcpivotonly”也没找到,google了一下“maxscript effetcpivotonly”,找到些参考信息,用objectoffsetRot。相关讨论:https://forums.cgsociety.org/t/aligning-only-the-pivot-of-an-object-using-maxscript/1191827/9

后来发现在《3dMAXScprit脚本语言完全学习手册 王华.pdf》里找到了相关的知识点8.3.7,还有现成的代码,所以,学习东西,有时间最好把整体完整教程看了,不然,就会发现,查了半天的东西,书本上其实就有。

    --Common.RotatePivotOnly $ (EulerAngles 90 0 0)
	fn RotatePivotOnly obj rotation=
	( 	if obj == undefined do return undefined
		local rotValInv=inverse(rotation as quat)
		animate off in coordsys local obj.rotation*=RotValInv
		obj.objectoffsetrot*=RotValInv
		obj.objectoffsetpos*=RotValInv
	)

虽然具体一行行我也不懂,能用就行。

这个需要来源是3dmax模型导入unity后,会出现一个(-90,0,0)的旋转参数。在3dmax中是(0,0,0)的旋转,到unity中就变成了(-90,0,0),处理方式就是旋转轴后再导出,旋转全部轴。晚上找到的再导出fbx时甚至向上轴的方法没有效果,理论上讲本质是一样的,旋转到y轴向上。以前不会maxscript时研究了一下,《3dmax导入unity问题(1) 轴角度坐标》那时候就想什么时候写脚本了。

让建模人员自己手动旋转,不放心,常常漏掉,或者没有递归打开,或者用老版本的3dmax(2014),根本就没有递归打开的功能.....

实际使用中发现,上面的代码仅仅适用于旋转一个独立物体,如果旋转组,该组下面的物体将被旋转。修改一下

fn RotatePivotOnlyOne obj angles=
	(
		if obj == undefined do return undefined

		children =#()
		join children obj.children
		for child in children do child.parent=undefined --旋转组时,组下面的物体也会被旋转

		local rotValInv=inverse(angles as quat)
		animate off in coordsys local obj.rotation*=RotValInv
		obj.objectoffsetrot*=RotValInv
		obj.objectoffsetpos*=RotValInv

		for child in children do child.parent=obj
	),
	fn RotatePivotOnly obj angles=
	(
		 list=GetNodes obj
		 for item in list do
		 (
			RotatePivotOnlyOne item angles
		 )
	),

本来建模人员一般建模方式来建,所有的模型在3dmax中都是(0,0,0)的角度,我旋转一下都变成(90,0,0)然后到unity里面会变成(0,0,0)。但是,一个场景中可能有些模型的角度不一致,关键时要改成一致的,需要“仅”设置轴的功能

还是上面的帖子里面有个xform_pivot_only,能够设置轴的方向和位置,需要矩阵。基本3dmax物体,通过结合查看transform的信息,大体上能达到我要的效果,但是,建模人员提供的则不行....继续搜索

unity论坛上有个帖子(https://forum.unity.com/threads/3dsmax-y-up-script.220104/)提供了另一种方法(他的需求和我的一样,废话,都是unity),用修改器XForm来修改。可以,不过还要改一下,对于分组,没有修改器,无法用这种方式处理。要和前面的结合起来。

又查了些资料,scriptspot上面有个相关的脚本(http://www.scriptspot.com/3ds-max/scripts/align-pivot-to-direction),代码里面有个worldAlignPivot,然后查到官网(https://help.autodesk.com/view/3DSMAX/2017/ENU/?guid=__files_GUID_CE525795_1A44_4DB3_BA90_DACA69430115_htm)对这个的解释

操作一下,发现时把轴方向”重置了一下“,在不改变模型的情况下,修改轴,将模型角度恢复到(0,0,0)了,原本我以为ResetPivot时这个作用,ResetPivot后,有些模型角度变成了(0,0,0),有些变成了(0,0,90)。

比较ResetPivot和worldAlignPivot:

原来的模型信息:

3dmax中的角度时(90,0,90)

ResetPivot后:

objectoffsetpos,objectoffsetrot,objectoffsetscale都重置了,但是rotation它不管。

worldAlignPivot后:

角度都重置了。

http://www.scriptspot.com/3ds-max/scripts/align-pivot-to-direction里面的脚本其实就是重置一下角度,再旋转一下角度。

worldAlignPivot在《3dMAXScprit脚本语言完全学习手册 王华.pdf》里面根本没体到。

好了,修改,测试。

六、JSON

内容有点多,独立出来:https://blog.csdn.net/llhswwha/article/details/105700207

  • 5
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值