这几天就是有这么个需求,要批量建出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”并运行