目录
一、入门教程
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
)
简单的话 用
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