3DMAX脚本语言maxscript的入门教程

今天填两个坑!之前休息了一波,还放假。就忘了这茬,今天给他写完整。


这几天就是有这么个需求,要批量建出1000个在尺寸/相对位置上有所差异,但组装方式比较相似的模型。用来做实验。本来是想在网上买的,哎,果然并不可能买到和需求一样的现成的模型。只好自己学3DMAX了,对着视频教程整了半天,感觉我等新手,用3DMAX建模的速度实在是太慢了。这时候就发现了菜单栏上有一个仿佛是脚本语言的东西。

很好,简直救人于水火之中!当机立断放弃手动建模了。不过网上的maxscript教程参差不齐,很多就是小片段,还不是入门级的,要么又是丢出一大本API让人自己慢慢看。

我只是个接触maxscript总时长不超过20小时的新手,所以写的教程也是自己对着API摸索清楚的超级入门教程。仅供参考。

我的目标是批量建模1000个这样的小房子,长方体是房子主体,四棱锥是屋顶,墙面配置1门,2窗,要能批量贴材质,批量渲染。并且脚本自动计算出各个零件的空间关系,导出文本数据供后续实验,导出渲染截图,保存max文件。

反正感觉基本上都是3dmax里很常用的形状拼接,但需要大批量建模,所以必须靠脚本!

1. maxscript的基本用法:

看名字就觉得和javascript有点像,因为我自己以前是写过前端的,所以用的sublime编辑器写这种轻量级脚本。跟那些脚本语言一样,用记事本也可以写。直接在文件夹里创建一个demo.ms文件,自己改后缀为Ms就可以。并不是一定要在3dmax里头写。我是用sublime打开空的demo.ms,直接在3dmax外写代码,然后3dmax内运行代码就可以了。

切到3dmax,菜单- Maxscript- maxscript侦听器。把这个东西打开。(感觉就像一个交互式命令行)

在命令行里交互式创建长方体:

侦听器里打一句:

Box()

就会交互式的弹出下面这句话,是创建好的一个空间坐标@在0,0,0的box对象(就是长方体对象)名字是默认的Box001

$Box:Box001 @ [0.000000,0.000000,0.000000]

【我叫你一声Box001你敢答应吗?】【undefine】

试过就知道,虽然这对象名为Box001,但无法直接召唤,需要用$Box001,带上$符号,才能选中对象。

老麻烦了,试试下一种创建Box的方法。

a = Box()

跟其他编程语言一样,定义了一个a变量当它的名字。这样创建的Box,名字还是叫Box002,但我们喊一句“a”他也能选中。

我正式用来创建房体的时候是在脚本文件里写:

house01 = Box name:"house01"

maxscript里头的Box,Pyramid这种构造函数,都是可以在后面跟很多参数的,比如我跟着一个name: 还可以跟pos: width : length:  具体参数查手册。这样在创建的同时,就给box起好了名house01,同时赋值给了house01变量。

为了封装好看,我把对Box的参数设置全包裹在了子函数setHouse里。

这涉及到Maxscript里的函数体系:

    调用函数的方法:【函数名 实参】  eg:setHouse house01

    定义函数的方法:【fn 函数名 形参  = (……)】 eg: fn setHouse obj = ( …… )

为某个Box对象随机调整长宽高: 

MINN = 30

MAXX = 60

fn setHouse obj=

(

obj.width = random MINN MAXX

obj.length = random MINN MAXX

obj.height = random MINN MAXX

)

在这里设定(修改)obj的长宽高,我这里设置了随机  和其他语言差不多,反正格式就这样。因为设定房体House01,的坐标就是基准的0,0,0 所以没有改pos


2.创建棱锥

屋顶是个四棱锥,这里setRoof函数传了两个形参,obj是四棱锥,houseobj先前创建的Box主房体,因为我们需要屋顶和房体的长宽一致,并且它的Pos要正好搁在房体上方。所以houseobj要作为参考之一传入。多参数的函数也是一样的写法如下。都是用.访问参数,面向对象的。

--设置屋顶的长宽高,长宽等同于房体,高度随机,位置要刚好搁在房体的上方。

fn setRoof obj houseobj=

(

obj.width = houseobj.width 

obj.depth = houseobj.length

obj.height = random MINN MAXX

obj.pos = houseobj.pos+[0,0,houseobj.height]

)

--屋顶

roof01 = Pyramid name:"roof01"

setRoof roof01 house01

3.创建门

这里涉及到要把创建出的门对象平移到墙面,并且进行旋转,让它能正常的贴立在墙面上。我用的max自带的door对象来构造。其中涉及到的旋转rotate的写法,注意仔细看看。

rot = eulerangles 90 0 -90

rotate obj rot

我比较喜欢这样写,可以先在max里头按E键调整门的方向,手动边拖边试数值(一边拖动旋转,一边关注下方那三个角度信息,这样可以迅速的定出哪个轴要转90,哪个轴转-90)然后用一个rot旋转因子记录角度。rotate obj rot ,对Obj进行旋转。

Biford是门的一种,有好多好多好多门,可以看API去,我觉得Biford是最简单的一种。Frame_Width,Panel_Type这种都是根据API来的,是对门进行的一些设定,其实说是API,如果在Max里头手动选中门对象,右边工具栏调整参数,工具栏里有的参数都是可以设定数值的,调用API的过程,是把可视化的填数字进框变成了Obj.属性=XXX这样代码调用,我觉得还方便些。

door01 = BiFold name:"door01"

setDoor door01 house01

fn setDoor obj houseobj=

(

obj.width = random 10 20

obj.height = random 15 obj.width*1.5

obj.depth = 3

obj.Panel_Type = 0

rot = eulerangles 90 0 -90

rotate obj rot

noise = obj.Frame_Width

posX = houseobj.width/2

posY = random (noise*2+obj.width-houseobj.length*0.5) (houseobj.length*0.5-noise)

posZ = 0

obj.pos = houseobj.pos+[posX, posY, posZ]

)

4.加两个窗子

窗子和门一样,需要随机出现在4个墙面之一,这里考虑到门是只有一扇,房子是可以转的,所以门全固定在了正面一面,但窗子就不同了,窗子需要随机出现在4面之一,并且两个窗子之间不可重叠,窗子和门之间也不可重叠。窗子的边缘不可超过墙的边缘。总之就是限定条件又加多了很多。

fn setWindow obj houseobj =

(

obj.width = random 10 20

obj.height = random 10 20

obj.depth = 3

obj.Rail_Width =0.0

noise = obj.Horizontal_Frame_Width

if(houseobj.height-obj.height-noise*2) < (houseobj.height*0.5) then(

posZ = random 5 (houseobj.height-obj.height-noise*2)

)else(

posZ = random (houseobj.height*0.5) (houseobj.height-obj.height-noise*2)

)

x = random 1 4

if x == 1 then (

rot = eulerangles 90 0 0

rotate obj rot

rot1 = eulerangles 0 0 -90

rotate obj rot1

posX = houseobj.width/2

posY = random (-0.5*(houseobj.length-noise*2-obj.width)) (.5*(houseobj.length-noise*2-obj.width))

obj.pos = houseobj.pos+[posX, posY, posZ]

)

else if x == 2 then (

rot = eulerangles 90 0 0

rotate obj rot

posX = random (-0.5*(houseobj.width-obj.width-2*noise)) (0.5*(houseobj.width-obj.width-2*noise))

posY = houseobj.length/2

obj.pos = houseobj.pos+[posX, posY, posZ]

)

else if x == 3 then(

rot = eulerangles 90 0 -90

rotate obj rot

posX = houseobj.width/2*(-1)

posY = random (-0.5*(houseobj.length-noise*2-obj.width)) (.5*(houseobj.length-noise*2-obj.width))

obj.pos = houseobj.pos+[posX, posY, posZ]

)else(

rot = eulerangles 90 0 0

rotate obj rot

posX = random (-0.5*(houseobj.width-obj.width-2*noise)) (0.5*(houseobj.width-obj.width-2*noise))

posY = houseobj.length*(-0.5)

obj.pos = houseobj.pos+[posX, posY, posZ]

)

)

--检测房屋内部各对象的相交情况

fn checkinsect obj = (

for i in $door* do(

if intersects i obj == True then(

--format "a window[%] crush a door\n" obj.name

return True

)

)

for i in $win* do(

if i == obj then(

return False

)else(

if intersects i obj == True then(

--format "a window[%] crush a window[%]\n" obj.name i.name

return True

)

)

)

)

哎呀窗子代码好长= =,本渣代码写的不好。其实就是因为窗子可能出现在四个不同方向的墙面上,所以POS要改,旋转因子也要根绝不同方向改。

关键点:

intersects i obj == True

这个是判定两个对象有没有重叠的,因为不能把窗子放门上,也不能把窗子重合,所以需要判定

for i in $win*

遍历以win开头的对象们。。



第二部分:渲染、贴图、材质、存图

在做完第一部分之后,可以批量生成N个小房子(1房体,1屋顶,1门,2窗),但是都是白模,没得纹理没得贴图。这太丑了。

fn renderr maxname = (

renderpic = #()  --为贴图们准备空集合

ca=TargetCamera pos:[180,150,75]  --设置摄像机位置

tobj=targetobject pos:[0,0,45] --设置摄像机目标点的位置(ca 和 tobj的连线其实就是照相的方向,决定着之后以何种角度给房子 拍照渲染)

ca.target=tobj

--贴屋顶(roof_num 对应着我准备的4种不同的房屋素材)

roof_num = random 1 4

roof_file = "D:\\max2012\\demo\\sucai\\roof" + (roof_num as string) +".jpg"

meditMaterials[1].diffuseMap = bitmaptexture filename:roof_file   --给材质库的插槽1 ,附上漫反射位图贴图

($roof*).material = meditMaterials[1]     --给屋顶对象,绑定上材质库插槽1的材质

append renderpic roof_file    --把素材图添加到空集合里

--贴墙(同理)

wall_num = random 1 4

wall_file = "D:\\max2012\\demo\\sucai\\wall" + (wall_num as string) +".jpg"

meditMaterials[2].diffuseMap = bitmaptexture filename:wall_file

($house*).material = meditMaterials[2]

append renderpic wall_file

--贴窗户 (这里需要使用UVW贴图,不然窗户素材会平铺到窗子上,显得很丑)

window_num = random 1 4

window_file = "D:\\max2012\\demo\\sucai\\window" + (window_num as string) +".jpg"

meditMaterials[3].diffuseMap = bitmaptexture filename:window_file  --给材质库插槽3赋上窗户贴图

for i in $win* do(   --对每个窗子,添加修改器UVWmap() ,然后在依次进行材质绑定

addModifier i (UVWmap())

i.material = meditMaterials[3]

)

append renderpic window_file

--贴门(同理窗子UVW)

door_num = random 1 4

door_file = "D:\\max2012\\demo\\sucai\\door" + (door_num as string) +".jpg"

meditMaterials[4].diffuseMap = bitmaptexture filename:door_file

addModifier ($door*) (UVWmap())

($door*).material = meditMaterials[4]

append renderpic door_file

--渲染+存图

ambientcolor = (color 255 255 255)  --设置环境光(搞不太懂,白的不出错就行)

render camera:ca outputFile:("D:\\max2012\\demo\\"+FILE_LOCATE+"\\pics\\"+maxname+".bmp") vfb:off  --以照相机ca的角度渲染,输出文件位于指定地点,vfb:off  不清楚是什么。

return renderpic   --把贴图位置存在集合里收好

)

就这么一段,完成了:

1.将贴图以漫反射形式赋给材质库的插槽(其实就是一个个材质球)

2.将门窗等对象绑定上材质球

3.考虑UVW展开并贴图,可以防重复(肯定有其他方法只是我不懂)

4. render 的瞬间,设置以摄像机角度渲染,设置输出位置



第三部分:打印对象数据

我们随机建好了一栋栋小房子,需要对它进行标注,即获取到每个对象的位置,尺寸,相对关系。

keypoint:

1. 使用for i in Geometry,遍历当前的所有形状对象。包括Box pyramid Biford等等

2. 使用classOf i 获取i对象的类别

3. 使用format做格式化输出,注意有的字符要转义(和其他语言共通的那种转义)

fn printall houseobj = (

for i in Geometry do

(

if classOf i == Box then

(

format "[\"root\",\"%\",%,[%,%,%,%,%,%]]\n" (i.name) (i.pos) (-i.width/2) (i.width/2) (-i.length/2) (i.length/2) (0) (i.height)

)

else if classof i == Fixed then

(

win_width = i.width + i.Horizontal_Frame_Width*2

win_height = i.height +i.Vertical_Frame_Width*2

if (i.pos.x == houseobj.width/2) or (i.pos.x == houseobj.width/2*(-1)) then

(

format "[\"%\",\"%\",%,[%,%,%,%,%,%]]\n" houseobj.name i.name i.pos (-i.depth/2) (i.depth/2) (-win_width/2) (win_width/2) (0) (win_height)

)

…………

)

)



第四部分:往文件里写数据

也是使用format。

format "[\"%\",\"%\",%,[%,%,%,%,%,%],\"%\"]\n" houseobj.name i.name i.pos (-win_width/2) (win_width/2) (-i.depth/2) (i.depth/2) (0) (win_height) (renderinfo[3])to:filename

就是format "123" to:filename

这里的filename是文件句柄,用下面的代码创建文件并get句柄,注意自己灵活点改循环啥的,比如1.txt 套循环里建1-1000.txt

out_name = "D:/max2012/demo/data/1.txt"

out_file = createfile out_name

format "123" to:out_file

close out_file

这个close out_file,对于我的max2012来说是无效的,就是我创建的N个txt,除非大退3dmax,不然都显示占用中,不能删除不能改名。不知道为什么,我加上flush out_file 也没用,就是必须大退3dmax才能解除对文件的占用。

如果有解决了问题的请留言告诉我,O(∩_∩)O谢谢


对,还有两个小tips:

F11快捷键打开maxscript侦听器

ctrl+D 对侦听器清屏,看着干净些

ctrl+R 快捷键打开编辑好的“demo.ms”并运行

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,125评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,293评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,054评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,077评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,096评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,062评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,988评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,817评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,266评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,486评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,646评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,375评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,974评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,621评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,642评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,538评论 2 352

推荐阅读更多精彩内容