Skip to content

一、浅析规则模块

一个简单的Max规则模块架构如下图所示。所有的规则都保存在Artease平台上,项目管理员根据项目需要在平台上定制符合项目的规则方案,插件使用者将方案下载到本地进行解析执行。

Max插件的规则是模块化的,一个规则通常由一个或多个函数组成,举个例子:

def compare_pos(**argv):
    import MaxPlus

    obj_name = argv.get("object_name", "")
    params = argv.get("rule", {})
    condition = params.get("condition", "")
    setting_pos = eval(condition)[1]
    ret = MaxPlus.Core.EvalMAXScript('''
        fn comparepos obj_name = (
        obj = getObjByName(obj_name)
        return obj.position as string
    )
    comparepos("%s")
    ''' % obj_name)
    pos = eval(ret.Get())

    flag = True
    msg = pos
    if pos:
        for i in range(len(pos)):
            flag = flag and abs(pos[i] - setting_pos[i]) < 0.0001

    return flag, msg, True
上面是一个简单的Max检查规则,它由一个名为compare_pos的Python函数构成。这个函数存在于【规则方案】中,在资源检查的过程中这个函数会被反复调用去检查每一个选中的子模型,并返回检查的执行结果。但仔细来看,Python函数其实不直接接触*.max,它实际上只负责了一些表层变量的传递以及返回,真正对检查产生效果的是包裹在这个Python函数内部的Maxscript脚本。

Maxscript脚本的代码来源有两个地方,一是随着【规则方案】分发下来的检查规则内(如上面的fn comparePos obj_name),另一种是随插件安装时自带的预置函数(如上面的fn getObjByName obj_name),所有跟检查相关的Maxscript脚本都会在插件初始化的时候被加载进内存中。

一句话概况整个流程,插件在启动的时候拉取Artease平台的规则方案并加载,执行检查时通过包裹在Python内的Maxscript和内置的Maxscript脚本对资源进行检查,最后通过Python返回结果。

二、编写第一条规则

在开始正式编写规则前,我们先熟悉一下Artease后台的规则添加流程和目前支持的规则类型。下图是项目管理员视角下的Artease项目后台,我们建议对于组内定制化的规则,优先在本组内创建使用。点击【新建规则】,在弹出的规则创建卡中即可完成一条规则的创建。

下表介绍了在规则创建卡中每一条字段的实际意义,其中英文部分可以选择性填写,但如果不填写的话当语言环境切换至英文时则无法正常拉取到规则以及规则的英文描述。

字段 描述
适用版本 规则可以执行的最低版本,仅提示,不作强制限制
对象 规则所属的类别,同时决定了规则在插件面板上的显示位置
分类 规则的方法类型
严重程度 规则的错误定级
desc 规则的中文描述
condition 检查条件
func_str 规则函数(中文提示)
en_desc 规则的英文描述
en_func_str 规则函数(英文提示)

下表进一步解释了当前Max插件支持的检查规则类型。

规则类型 描述
compare 简单地比较一个属性
mcompare 以xpath的方式比较一个属性
function 自定义检查方法(批量检查)
tool_function 自定义检查方法(独立检查)
tool_function_m 执行内置maxscript方法(独立检查)
py_file 执行Python文件
ms_file 执行maxscript文件

compare/mcompare

我们先从最简单的compare/mcompare方法入手。compare/mcompare都用于简单地比较一个基本属性,这类规则不需要用户自定义检查函数(func_str),只需要按照指定写法去规定condition字段即可。举个例子,如果我们想检查资源的的材质球命名是否符合以模型名开头 + _mat的规范

type: compare
desc: 材质球命名为模型名+_mat
condition: ['material.name', '==', name + '_mat']
condition的内容可以被分为3个部分,[A(max对象属性), B(比较方法), C(检查条件)],max对象属性的内容实际上是maxscript中预留的一些关键字,它基于当前被检查的子模型,因此你可以把它理解成($.)A的任意操作,比如:
name = $.name
material.name = $.material.name
position = $.position 
比较方法是插件提供的一种条件运算方式,支持正则运算以及Python的条件运算符,比如:
desc: 模型的坐标大于[1, 2, 3]
condition: ['position', '>', [1, 2, 3]]

desc: 以model_开头的模型名字
condition: ['name', 're', '^model_']
检查条件是用于对规则的对象属性做比较的条件,它支持的类型有list、tuple、dict、unicode,不能像对象属性一样支持取属性操作,但使用者可以直接调用name模型名字、os_type模型类型两个变量。 mcompare与compare相比除了使用xpath方式写规则外,比较方法还支持exec()方法。

function

function方法是最从常用的检查方法,它允许用户自由地编写脚本对max资源进行修改处理或检查,如果compare/mcompare无法满足你的检查要求,可以尝试使用这个方法。

基本使用

无参检查

还是用一个例子引入,熟悉一下整个规则函数的写法,如果我们想检查相机的视角是否为back视角:

def checkCameraView(**argv):
    import MaxPlus
    # 获取一些必要的检查信息
    objName = argv.get("object_name", "")

    # 执行Maxscript脚本
    ret = MaxPlus.Core.EvalMAXScript(
        """
        fn checkCameraView =
        (
            res = "view_back"
            if viewport.getType() != #view_back  then
            (
                res = viewport.getType() as string
            ) 
            res
        )
        checkCameraView()
        """
    )

    # 从Maxscript层获取结果
    viewRes = ret.Get()
    flag = True
    msg = ""
    if viewRes != "view_back":
        msg = u"当前的摄像机视角为%s,不是view_back" % viewRes
        flag = False

    # 返回最终结果
    return flag, msg, True
整个检查流程的思路:获取检查的必要信息,传入并在Maxscript层执行,导出Maxscript数据到Python层做进一步判断,返回最终检查结果。形参argv提供用于检查的一些参数如下表所示,列表会实时更新,方便开发同学调用:

类型
obj 所有待检查的模型 list(max_obj)
object_name 当前检查的模型名称 str
rule 当前的检查规则信息 dict
context_data 不同规则之间的上下文传递 dict
window 插件主窗口实例 QWidget
AutoDesk官方为Python提供了2种模块去执行Maxscript代码,MaxPluspymxs,这里不做更多的对比,详见开发手册,需要指明的是MaxPlus模块从2021版本开始被移除,因此除非你的项目组使用的3dsMax低于2017版本,我们建议使用pymxs去执行函数字符串,写法与MaxPlus类似:
import pymxs
ret = pymxs.runtime.execute('''
    Your maxscript codes
'''
)
print ret
检查完成之后,需要按规定返回结果。通常来说,只需要返回三个值:
描述 类型 示例
检查结果 bool True
检查错误显示的信息 str 模型名称不包含_model
是否支持批量检查 bool False表示一条错误信息会保存在多个模型中,反之则分开保存
##### 带参检查
为了使得检查规则更加的通用化,function方法支持传参以最大程度的复用规则在不同的情况下。回到这章最开始的那个例子,我们想去比较一个模型的位置是否符合要求:
def compare_pos(**argv):
    import pymxs

    obj_name = argv.get("object_name", "")
    params = argv.get("rule", {})
    condition = params.get("condition", "")
    setting_pos = eval(condition)[1]
    ret = pymxs.runtime.execute('''
        fn comparepos obj_name = (
        obj = getObjByName(obj_name)
        return obj.position as string
    )
    comparepos("%s")
    ''' % obj_name)
    pos = eval(ret)

    flag = True
    msg = pos
    if pos:
        for i in range(len(pos)):
            flag = flag and abs(pos[i] - setting_pos[i]) < 0.0001

    return flag, msg, True
可以发现,相比于无参检查,规则从rule字段中取出了规则函数的运行参数并将它保存在了setting_pos中。这样对于任意传入的规定坐标,我们都能将其与当前模型的坐标进行对比而无需二次修改规则函数,那么这个参数是在哪里传入的呢?当然是与规则函数名一起从平台上传入:
type: function
desc: 初始位置必须归x, y, z
condition: ['compare_pos', [0, 0, 0]]
#### 进阶使用
function方法提供了一些进阶使用的方式,为那些基本写法无法满足的规则开发者提供更加灵活的检查方式。在返回结果的第四个位置实际上存在一个字典,用于保存用户的一些自定义参数,目前我们开放的两个关键字分别是msgcallback,具体如下表:
关键字 描述 类型
msg 用户自定义输出内容,无论结果正确与否都会输出 str
callback 复现结果的函数变量名 obj

举个例子:

def myCallbackRule(**argv):
    import pymxs

    obj_name = argv.get("object_name", "")
    params = argv.get("rule", {})
    condition = params.get("condition", [])

    def callback():
        ret = pymxs.runtime.execute('''
        fn selectMyFace obj_name =(
            obj = getObjByName(obj_name)
            obj.EditablePoly.SetSelection #Face #{1883}
        )
        selectMyFace "%s"
        ''' % obj_name)
        return ret

    callback()
    user_content = {'callback': callback, 'msg': '这是自定义输出'}
    return False, u'回调测试', True, user_content
如果填写了msg字段,那么在检查结果输出的时候其它的内容都会被省略:

callback字段的设计是为了解决在检查完成之后的某些操作影响了变动了检查结果,而导致需要二次检查的问题。比如,某个检查规则找出了具有问题的面片,并将其选中标记了出来,使用者在对单个错误面片进行修改后,其它的选中便消失了,如果检查逻辑比较耗时,那么再执行一次检查则会浪费不必要的时间上。因此,如果将结果显示逻辑独立出来,单独使用一个函数去执行结果显示操作就可以避开反复执行检查的问题。当然,callback的作用不止于此,规则开发者可以根据自己的需求去设计它。

tool_function/tool_function_m

tool_function与function的规则编写类似,但暂时不支持function内的进阶使用功能。它的目标规则是那些不需要频繁进行检查的规则,以及一些对资源做出修改的相对危险的规则,会以按钮的形式出现在插件面板上,我们同样用两个例子来解释它的写法与效果:

tool_function

无参检查

def resetXform(**argv):
    import MaxPlus
    obj_name = argv.get("object_name", "")
    params = argv.get("rule", {})
    condition = params.get("condition", [])
    ret = MaxPlus.Core.EvalMAXScript('''
        objarray = getcurrentselection()
        for obj in objarray do 
        (
            l = obj.baseobject as string
            ResetXform obj
            if l == "Editable Poly" do
                ConvertTo obj Editable_Poly
            if l == "Editable Mesh" do
                ConvertTo obj Editable_Mesh
        )
        select objarray
        ''')
    return True, u"\n", True
type: tool_function
desc: 重置模型
condition: ['resetXform']

带参检查

def delete_uv_channels(**argv):
    import MaxPlus
    obj_name = argv.get("object_name", "")
    params = argv.get("rule", {})
    condition = params.get("condition", {})
    condition = eval(condition)
    pri = condition[1][0][1]
    if not obj_name.startswith(pri):
        return True, u"\n", True
    ret = MaxPlus.Core.EvalMAXScript(
        '''
       fn delete_uv_channels obj_name =
        (
            m_obj = getObjByName(obj_name)
            if (classOf current_object) == Editable_Poly do
            (
                for i = 1 to polyOp.getNumMaps obj do
                (
                    if (polyop.getMapSupport current_object i) do
                        polyop.setMapSupport current_object i false
                )
            )

            if (classOf current_object) == Editable_mesh do
            (
                for i = 1 to meshOp.getNumMaps obj do
                (
                    if (meshop.getMapSupport current_object i) do
                        meshop.setMapSupport current_object i false
                )
            )
        )

        delete_uv_channels "%s" 
        ''' % obj_name
    )
    return True, u"\n", True
type: tool_function
desc: 清除前缀为%s的模型UV
condtion: ['delete_uv_channels', [['prefix_','col']]]
tool_function的带参检查与 function存在差异,请注意: function: ['compare_pos', [0, 0, 0]] tool_function: ['delete_uv_channels',[['prefix_','col']]] 二者外层列表的第一个位置是函数名,第二个位置是参数,但tool_function需要两层列表包裹,同时传入的实参数量需要2个,其中第2个参数可以以变量的形式显示在desc中,如上面的例子,清除前缀为%s的模型UV在插件端实际上显示的是清除前缀为col的模型UV

tool_function_m

tool_function_m用于直接执行max插件中内置的Maxscript函数,如果需要执行内置的Python脚本请直接使用tool_function方法。具体可调用的函数见API文档。

py_file/ms_file

为了使Artease for max插件能够兼容更多的第三方脚本,插件支持直接执行py和ms文件。根据需求,如果需要正确地将处理结果返回给max插件,请按照下列要求操作,否则直接执行即可。

ms_file

ms文件返回值需要为列表,列表中存在三类值:

描述 类型 例子
结果flag str "True"
模型名称 str "20995_p1_lods"
错误msg str "资源不符合检查规则要求"

一个例子:

fn check_name =(
    ret = #()
    for obj in selection do (
        if obj.name != "Box001" then(
            flag = "False"
            msg = "20995_p1_lods"
            append ret flag
            append ret obj.name
            append ret msg
       )else(
            flag = "True"
            msg = ""
             append ret flag
            append ret obj.name
            append ret msg
        )
    ret
    )
)
check_name()

py_file

  • Python文件必须要import artCheck.global_data,并在其中修改global_data中的值。
  • global_data中有两个值:check_list(列表,长度不限,但必须为3的倍数,形式必须和ms文件一样为 [flag1,name1,msg1,flag2,name2,msg2,flag2=3,name3,msg3......])
  • switch(在函数内必须把switch改为True,表示已修改global_data的值,该值为False时,检查结果不显示)
import artCheck.global_data
def check_name():
    import MaxPlus
    ret = pymxs.runtime.execute('''
        fn check_name =(
            ret = #()
            for obj in selection do (
                if obj.name != "Box001" then(
                    flag = "False"
                    msg = "20995_p1_lods"
                    append ret flag
                    append ret obj.name
                    append ret msg
               )else(
                    flag = "True"
                    msg = ""
                     append ret flag
                    append ret obj.name
                    append ret msg
                )
            ret
            )
        )
    ''')
    for i in range(len(ret)):
        artCheck.global_data.check_list.append(ret[i])
    artCheck.global_data.switch = True
    return True
check_name()