引用:http://www.ddove.com/3dmax/sl/tut_controlling_foliage_with_maxscript.html
在本教程中,您将使用一种将树木的图像粘贴到平面上的技术。您将创建和修改用于创建布告牌效果的脚本,这种效果可使一个对象在整个动画期间都朝向另一个对象。朝向对象将只绕世界 Z 轴旋转。这种效果适用于树木、人物以及对平面对象使用位图的场景元素,可使这些对象始终朝向摄影机。
注意:本课程专为要学习如何使用 MAXScript 增强建筑模型的人士所设计。仅当您对学习 MAXScript 感兴趣时,才学习本课程。
您将通过模板创建脚本,并通过添加预置函数修改此脚本。然后,您将修改该脚本使对象选择更灵活。
这些步骤的目的在于,向您示范如何修改现有脚本以并入自己项目所需的函数。在此过程中,您会学习将 2D 树木对象用作 3D 树木代用品的方法。
本教程的所有必需文件都可以在 3ds Max 8 附带的教程文件光盘上找到。在执行教程之前,请将 \tutorials\scripting_fx 目录从光盘复制到您的 \3dsmax8 本地安装目录中。
打开脚本的测试文件:
从 \tutorials\scripting_fx 文件夹中打开文件 tut_billboard_start.max。
注意:如果看到“文件加载:单位不匹配”对话框,请选择“按系统单位比例重缩放文件对象”选项。
此场景将平面(布告牌)置于建筑物的前面,而不是树模型的前面。在布告牌上已经放置了橡树图像。每个图像都有一个 Alpha 通道,此通道用作不透明贴图,以使图像背景变得透明。除布告牌、灯光和摄影机外,所有对象都已冻结。
场景渲染的速度要比存在 3D 树木的情况更快,而且从当前摄影机角度观看,布告牌树木也很逼真。由于场景使用光线跟踪阴影,因此来自树木贴图的阴影被正确渲染,但透明区域没有投影阴影。
从此角度观察,很明显树木只是粘贴在平面布告牌上。可以通过手动为布告牌设置动画以使其跟随摄影机,从而解决此问题。然而,如果有许多树木、人物和需要朝向摄影机的其他对象,则设置动画的过程将很快变得不切实际。
试验此脚本:
首先,将运行脚本的已完成版本,以查看它如何工作。接着在本课程中,将学习自己编写此脚本。
在“选择编辑器文件”对话框中,选择脚本 tut_billboard_01.ms。
会出现一个“Billboard”对话框,其中有一个标记为“Set Billboard Effect”的按钮。
此脚本将处理选定对象。选择要注视摄影机的对象及摄影机本身。此脚本会计算出哪个对象是摄影机,并强制其他对象的局部 Z 轴朝向摄影机。
注意:如果希望使用“按名称选择”对话框来选择对象,请按 H 键并使用对话框选择 Billboard01、Billboard02、Billboard03、Billboard04 和 Camera01。
在“Billboard”对话框中,单击“Set Billboard Effect”。
检查脚本:
脚本的第一部分称为标头。这部分包含基本信息,如脚本文件名、脚本名称、脚本版本号、3ds Max 版本号、作者姓名、编写日期和编写目的。此信息按注释格式编写,这表示查找要执行的代码时,该程序会忽略此信息。跨多行的注释位于符号 /* 和 */ 之间,而单行注释前面带有双连字符 (--)。整个脚本包含临时注释,以告知您每行或段的用途。
在标头之后,定义了 makeBillboard 函数,但不会执行。此函数将脚本控制器指定给每个布告牌对象的旋转。脚本控制器会计算使布告牌保持注视摄影机所需要的世界 Z 轴旋转。布告牌的局部 Z 轴定义对象的前面部分。选择此约定以匹配在“前”视口中创建的对象的方向。
脚本执行的第一个任务是检查“Billboard”对话框是否已打开,如打开,则将其关闭。这样可避免对此对话框进行多次复制。
定义卷展栏并为其命名。在此定义中,已设置变量,并且定义了 UI 控制项(一个按钮)及其相关事件。摄影机变量名为 laObj(对于注视对象),而拥有布告牌对象的阵列名为 bbObjsArr(对于布告牌对象阵列)。
下一步,脚本将查看所有选定对象。通过检查每个对象的超类以识别摄影机。它将摄影机放置在 laObj 变量中,并将其余对象放置在 bbObjsArr 阵列中。
检查 makeBillboard 函数:
makeBillboard 函数执行为注视摄影机的对象创建脚本控制器的实际工作。它包含执行不同任务的若干个部分。
fn makeBillboard obj -- The billboard object. targ -- The lookat object.
跟随 fn makeBillboard 的两个变量 obj 和 targ 是此函数的参数。参数是传递到函数的值,以便函数可以针对其执行操作。参数可以与 fn makeBillboard 出现在同一行上,但将它们放在单独的行上,可用于在每个参数后面添加注释。
将光标放在 setWaitCursor() 行上的任意位置(行尾除外),然后按 Ctrl+B 组合键。
此操作通过查找包含该函数的左括号和右括号,高亮显示整个函数定义。左括号就在 fn makeBillboard 声明和等号后面,而右括号将出现在许多行后面。
提示:为使脚本执行不发生错误,每个左括号都必须有一个相应的右括号。若要快速找到一对特定圆括号内的代码部分,请将光标放在任一左括号后或其后的任意位置,然后按 Ctrl+B 组合键。位于括号内的代码部分将被选定,包含圆括号。再次按Ctrl+B 组合键将选择下一个括号内的代码。如果没有括号存在或只有一个括号,会听到哔声。此技术对于查找脚本中的错误非常有用。
函数 setWaitCursor() 是在函数的开始处调用的,用于在处理函数时显示 Windows 沙漏。函数 setArrowCursor() 是在结尾处调用的,用于还原箭头光标。这两个函数都内置在 MAXScript 中。
函数的剩余部分包含在 if...then... 子句中。这提供了错误处理,以确保将有效对象传递到函数。下一行可防止脚本尝试处理已删除或不存在的对象,这些操作会导致脚本失败。
if obj!=undefined AND (NOT isDeleted obj) AND targ!=undefined AND (NOT isDeleted targ) then
if...then... 子句主体部分构成了一个字符串 scriptStr,此字符串包含脚本控制器的实际脚本。此脚本使用基本三角法计算所需的旋转角度。当使用脚本内的预置函数时,无须知道函数的详细信息。知道此函数的参数和返回的值就足够了。
在此子句结尾,指定了脚本控制器,并扩展了脚本的时间范围。对于脚本控制器,活动时间范围将被自动设置为当前动画范围。如果决定在运行此脚本后增加动画中的帧数,则必须手动扩展控制器范围。通过将控制器的时间范围设置为非常大的间隔,脚本可避免这种情况发生。
-- Assign rotation script controller. ctrl=obj.rotation.controller=rotation_script() -- Set time range wide in case user expands it later. setTimeRange ctrl (interval -1000 10000) -- Put script string into script controller.ctrl. script=scriptStr
填写卷展栏信息:
卷展栏声明为卷展栏标题提供了空间,当运行脚本时,将显示该标题。它还有一个内部卷展栏名称,您可以将其替换为一个更具描述性的名称。
提示:若要使此过程自动执行,请选择文本 rol_RNAME,按 Ctrl+H 组合键访问“替换”对话框,在“替换为”字段中键入 rol_myBB,然后单击“全部替换”。
local bbObjsArr -- Declare billboard objects array. local laObj -- Declare look-at object.
local 声明设置变量的范围。在脚本中,变量可用于整个脚本,也可以仅用于声明所说明的位于圆括号内的代码特定区域。在本例中,local 命令表示这些变量应仅用于此段内容。在本例中,local 声明可确保这些变量仅用于卷展栏。这样可避免名称与代码的其他部分发生冲突,并防止变量的值被不应访问它们的代码覆盖。
button but_setBBEffect "Set Billboard Effect" width:125 height:40\ tooltip:"Select billboard objects and camera, then click button"
在卷展栏上将创建按钮,并将其值指定给变量 but_setBBEffect。该按钮上出现的文本被设置为“Set Billboard Effect”。该按钮的宽度和高度已设置,并提供了鼠标悬停时的工具提示。
-- Set the billboard effect. on but_setBBEffect pressed do ( )
bbObjsArr=#() -- Initialize billboard objects array to null array. laObj=undefined -- Initialize lookat object variable to 'undefined'.
使用先前创建的 local 命令声明了这两个变量,表示已将它们创建为数据的未来占位符。然而,在那时没有数据可替换它们。在上述两行中,已初始化这两个变量,表示已用一组开始数据替换这两个变量。
如果通过选择摄影机和其他对象,然后单击“Set Billboard Effect”按钮的方法来正确使用此脚本,则将使用对象名称填充这些变量。
此时初始化这些变量(而不是在设置布告牌效果的脚本部分中),可提供一种用于以后在脚本中进行错误检查的方法。例如,如果在单击“Set Billboard Effect”后 laObj 仍是“undefined”,这表示未选定摄影机,并且此脚本也不会尝试执行布告牌效果。
下面,您将创建一个循环。循环可多次执行一系列的指令。在本例中,循环将检查每个选定对象和测试,以确定它是摄影机还是其他类型的对象。它还测试对象是否为目标(如摄影机目标),但并不会将这些目标添加到注视摄影机的对象列表中。
-- Note:User must manually select objects in viewport, or via 'Select by Name' dialog. -- Selection should include the billboard objects AND one camera. -- Loop through selected objects. for obj in selection do ( if superClassOf obj==camera -- Check if object is the camera. then laObj=obj -- Assign camera object to 'laObj' variable. else if classOf obj!=targetObject then append bbObjsArr obj -- Append object to billboard objects array, but exclude target objects. )
当循环检查完当前选择后,要注视摄影机的对象都将位于阵列 bbObjsArr 中。剩下的一个任务是设置要检查 bbObjsArr 阵列的循环,并为阵列中的每个对象调用 makeBillboard 函数。在第二次循环前,需要测试当前选择中存在摄影机,并至少有一个对象要注视该摄影机。如果摄影机变量和对象阵列通过此测试,那么代码将继续为阵列中的各项调用 makeBillboard 函数。
-- Finally, set billboard effect:loop through billboard objects and call 'makeBillboard' fn. -- This assigns a script controller to the billboard objects' rotation. if bbObjsArr.count!=0 AND laObj!=undefined then -- Check to ensure objects have been selected. ( for obj in bbObjsArr do makeBillboard obj laObj )
仅当检测到摄影机和布告牌时,才应运行此循环。重新调用已初始化为空阵列 #() 的 bbObjsArr,以便在脚本开始处空阵列中没有元素。bbObjsArr.count 的值将告知您阵列中的元素数目。如果在选定对象中没有检测到布告牌对象(bbObjsArr.count 为 0),那么该检查将阻止循环运行。如果没有检测到摄影机对象,那么循环也不会运行。
修改脚本:
此时,您将修改该脚本使对象选择更灵活。您将添加两个按钮以辅助选择过程。一个按钮用于通过“按名称选择”对话框选择布告牌对象。另一个按钮用于通过“按名称选择”对话框选择注视对象。除此之外,也不会像前一脚本中那样将注视对象限制到摄影机对象。
在 -- UI CONTROL ITEMS 后添加两行,使代码看起来类似于以下内容:
-- UI CONTROL ITEMS. button but_bbObjs "Select Billboard Objects" button but_laObj "Select LookAt Object" button but_setBBEffect "Set Billboard Effect" width:125 height:40
若要设置第一个按钮的功能,请在 -- EVENTS 后输入此文本:
-- Select billboard objects, and put into array. on but_bbObjs pressed do ( bbObjsArr=selectByName title:"Select Billboard Objects" )
此按钮事件显示“按名称选择”对话框,然后接受来自此对话框的选择,并将这些选择放置在布告牌对象阵列 bbObjsArr 中。如果单击“取消”,或关闭此对话框而没有进行任何选择,则将返回值“undefined”。
-- Select lookat object, and put into variable. on but_laObj pressed do ( laObj=selectByName title:"Select LookAt Object" single:true )
此按钮事件显示“按名称选择”对话框,接受来自此对话框的选择,并将该选择指定给注视对象 bbObjsArr。如果单击“取消”,或关闭此对话框而没有进行任何选择,则将返回值“undefined”。
在 setBBEffect 事件中以 on but_setBBEffect pressed do 开头的代码中,删除以 bbObjsArr=#() 开始,且在 -- Finally, set billboard effect. 之前的所有内容。
测试修改过的脚本:
固定阴影:
当布告牌改变方向时,阴影会随之变化。通过为每个布告牌使用单独的灯光投影阴影,可以解决此问题。此方法的优点在于,所用的渲染时间要少于光线跟踪阴影。
已将聚光灯放置在每个布告牌的中心位置,指向与直接光相同的方向。这些灯光当前处于禁用状态。所有聚光灯将被实例化,这样只需打开其中一个聚光灯即可打开全部聚光灯。
在“修改”面板 >“常规参数”卷展栏 >“灯光类型”组中选中“启用”。
此时所有四个聚光灯都已启用。在“强度/颜色/衰减”卷展栏中,还可以看到灯光的“倍增”值为负值。这会导致在灯光所到之处进行灯光移除,以创建阴影效果。
在此卷展栏上,可以看到已将噪波贴图用作投影贴图应用到灯光。这会投影一个噪杂的黑白图案,以模拟树叶的图案。如果按 M 键打开“材质编辑器”,可以在第一个示例窗中看到此贴图。
这些阴影不像光线跟踪阴影那样明快,但它们也足以迷惑一扫而过的眼睛。可以在动画文件 tut_billboards.mov 中看到作为结果的动画。固定的阴影可造成这种幻觉:这些树木不是不断改变方向的布告牌,而是三维对象。
将脚本粘贴到新窗口中:
现在,可以将脚本粘贴到新窗口中以查看它的带颜色的代码。通过颜色可以更容易查看各种脚本函数。
脚本的主体包含在一个 try...catch... 子句中。这提供了错误处理。如果没有错误,则进行求值的最后一个表达式是 (eulerAngles 90 0 z_rot) as quat。如果有错误,则进行求值的最后一个表达式是 (eulerAngles 90 0) as quat。此脚本使用 Euler 角度进行计算,但使用 as quat 命令将它们作为(将它们转换为)四元数掷出。关于四元数旋转的信息,请参见课程装配腕部扭曲,或参见 MAXScript 文档。
每次帧发生变化时,都要对脚本进行求值,但当对象控制器发生变化时,则不一定要进行此操作。当任一渲染的对象更改时,使用命令 dependsOn 可以对脚本进行求值。在本例中,当“Camera01”更改位置时,将对此脚本重新求值,以使这些平面实时注视摄影机。在 dependsOn 行上,摄影机的对象名称用单引号括起,以避免当名称包含空格、虚线或其他问题字符时发生错误。
代码的其余部分使用基本三角法来计算实现布告牌效果所需的旋转角度。从布告牌对象指向目标对象的向量被投影到世界 XY 平面上。投影的向量作为构成直角三角形的斜边。直角三角形之斜边与 X 轴之间的角度即是所需的世界 Z 轴旋转角度。
如果投影的向量的端点位于第三或第四象限,那么此角度的正弦函数为正值,如果端点位于第一或第二象限,那么此角度的正弦函数为负值。但此平面的局部轴并没有与世界轴对齐,因此必须包括 -90 度这一要素,并且必须将 X 轴旋转 90 度。