资讯详情

资讯详情

mxspycom:3ds Max批处理中MaxScript调用Python

简介MXSPyCOM 源代码包面向使用 3ds Max 进行建模、动画与视觉特效制作并希望提升 MaxScript、Python 脚本开发效率的中高级用户。它针对内置编辑器功能有限、调试不便的问题提供外部编辑器与 3ds Max 之间的通信方案支持实时编辑执行、语法高亮、版本控制与自定义配置也便于排查脚本错误。压缩包共 35 个文件、约 396KB以 21 张 png 配置与菜单截图、json 工作区与任务配置、ms 与 py 示例脚本以及 cs 源码、ps1 发布脚本和文档许可文件为主既包含可运行示例也保留搭建与集成线索。已有 402 人学习下载。读者可据此了解外部编辑器接入 3ds Max 的完整流程参考示例脚本验证 MaxScript 与 Python 的调用方式并借助截图与说明文档完成环境配置、快捷键绑定和常见问题排查适合想搭建高效脚本工作流的开发者学习借鉴。1. 商业项目里的 3ds Max 批处理MaxScript 已经开始撑不住去年接手一个资产整理活三百多个场景文件要按命名规则批量改材质名、导出 FBX、顺手生成一张缩略图最后还要对一遍 CSV 清单。MaxScript 改材质、导 FBX 都还行可一到读 CSV、算哈希、压缩图片就得自己造轮子写到后面全是补丁。换个思路Python 那边 PIL、numpy、requests 都是现成的3dmax和maxscript的生态却接不过去——这就是mxspycom这类工具存在的理由。它是一个常驻的 Python COM 服务器MaxScript 用CreateOLEObject拿到句柄往里丢代码、取返回值等于给 MaxScript 装了一个 Python 内核。适合谁手上有存量 MaxScript 脚本、又不想把整条管线推倒重来的三维开发。2. 选型与定位一条 COM 通道而不是又一个脚本解释器2.1 它补的是进程间通信这块短板先把一件事说清楚mxspycom不做语法转换它不把 MaxScript 编译成 Python也不把 Python 翻译回 MaxScript。它做的事情非常朴素——在 Windows 上注册一个 COM 自动化服务器进程是独立的 CPython3ds Max 作为 COM 客户端去连它连上之后你就可以把一段 Python 源码字符串丢过去执行再把结果字符串拿回来。理解这一点后面所有设计就顺了。因为进程独立所以你在 Python 侧import numpy不会污染 3ds Max 的内存空间也因为是独立进程所以 Python 崩了不会把 Max 拖下水反过来也成立但要看写法第 5 章会讲。它本质上解决的是「两个运行时如何对话」不是「两种语言如何互译」。对比一下常见的三条路子差异很直观方案通信方式启动开销能共享第三方库典型问题doscommand起 Python进程命令行每次调用都起能传参靠拼字符串返回靠读文件pymxsMax 内置 Python同进程零受内置环境限制版本绑死装库麻烦mxspycom常驻 COM 服务只起一次能且是干净环境注册、位数、线程阻塞如果你的场景是「每次跑一次就走」的一次性脚本doscommand反而更省事。mxspycom的价值在「高频、小颗粒」的调用一次会话里调几十上百次每次只是算个名字、转个格式、查个表这时省掉的解释器启动时间就很可观了。2.2 为什么是 COM而不是 socket 或文件轮询这是个会被反复问到的问题。socket 当然能做而且跨平台、可远程看起来更现代。但在 3ds Max 这个生态里COM 有几个现实优势第一MaxScript 原生支持。CreateOLEObject ProgID是一行代码的事不需要额外插件、不需要起监听端口、不需要处理防火墙提示。你要是上 socketMaxScript 侧还得自己封装 TCP 客户端Windows 上还要考虑本地回环被安全软件拦的问题血泪经验某些环境里监听端口这件事本身就过不了审。第二生命周期由系统托管。COM 的 LocalServer32 注册项告诉系统「这个 ProgID 对应哪个可执行文件」客户端一调用系统负责拉起进程客户端全释放了进程可以自己退。你不用写守护脚本也不用管端口占用。第三无需序列化协议。COM 的 VARIANT 能直接承载整数、浮点、字符串、布尔和数组MaxScript 拿到的是原生类型不用先json.parse一层。当然复杂结构还是得走 JSON这个后面讲。文件轮询也有它的位置——如果你的任务是「跑半小时的渲染队列」那用文件当队列反而更稳因为不占调用线程。所以我一般会这么分短任务走 COM 同步调用长任务走「投递 轮询」第 6 章给具体改法。2.3 运行前提依赖、位数与权限三件事装之前先确认三件事缺一件后面就会翻车。Python 解释器。必须是 Windows 上的 CPython建议单独建一个虚拟环境别用系统全局环境。原因很简单这个 COM 服务器一旦注册任何程序都能通过 ProgID 调它环境越干净越好排查。pywin32。这是核心依赖提供pythoncom和win32com.server。安装时注意架构匹配# 建议在专用虚拟环境里装避免污染全局 python -m venv C:\pyenv\mxspy C:\pyenv\mxspy\Scripts\activate # pywin32 是 COM 服务的底座版本要和 Python 版本对得上 pip install pywin32 # 装完必须跑一次把 pywin32 的 DLL 注册到系统 python C:\pyenv\mxspy\Scripts\pywin32_postinstall.py -installpywin32_postinstall.py这一步经常被跳过。跳过之后的表现是import pythoncom报DLL load failed很多人以为是装错了包其实是 DLL 没就位。命令里的-install参数做的事是把pywintypes和pythoncom的 DLL 复制到系统目录并写注册表属于一次性动作。位数与权限。3ds Max 是 64 位的Python 也必须是 64 位这个不匹配会在CreateOLEObject时直接报「类未注册」或者更玄学的「服务器执行失败」。权限方面注册 COM 要写HKEY_CLASSES_ROOT普通用户没有写权限注册那一步必须用管理员身份的终端。这是最容易踩的坑之一在普通终端里注册命令看起来成功了Max 里就是连不上。提示注册用哪个 Python 解释器运行时就必须用同一个。用C:\pyenv\mxspy\python.exe注册LocalServer32 里记的就是这个路径之后你在别的环境里怎么折腾都没用。3. 源码解剖一个 COM 服务器到底由哪几块拼起来3.1 文件清单与各自职责拿到源码包别急着运行先看清结构。这类工具的文件通常不多但每一块职责分明混着看容易乱。典型的组成是这样mxspycom/ ├── mxspycom.py # COM 服务器主体类定义 注册入口 消息泵 ├── mxspycom_install.py # 可选独立注册/反注册脚本便于打包分发 ├── mxspycom.ms # MaxScript 侧封装连接、调用、异常兜底 ├── mxspycom_test.ms # 冒烟测试脚本验证通道是否打通 ├── requirements.txt # 依赖清单核心就 pywin32 └── README / 示例目录 # 用法说明与调用样例mxspycom.py是唯一不能缺的文件。它的结构分三段类定义暴露哪些方法、注册逻辑写注册表、运行逻辑起消息泵或命令行分发。mxspycom.ms是给 MaxScript 用的薄封装把CreateOLEObject、方法调用、错误捕获包成函数避免每个脚本都重复写一遍。mxspycom_install.py的存在意义是分发——如果你的团队里有人只负责跑脚本不碰代码给他一个双击就能注册的入口比让他敲命令行靠谱得多。这类安装脚本内部通常还是调win32com.server.register只是把参数和路径判断写死了。3.2 COM 服务器的类骨架核心就是这个类。win32com.server靠一组约定好的类属性识别「这是个 COM 服务器」少一个都不行# mxspycom.py 的核心骨架精简版 import pythoncom import traceback import sys class MXSPyCOMServer: # COM 暴露的方法名必须显式列出否则客户端看不到 _public_methods_ [Exec, Ping, ExecFile] # 对外暴露的属性一般留空 _public_attrs_ [] # 读属性时走哪个方法留空即只读 _readonly_attrs_ [] # 客户端用这个名字创建对象CreateOLEObject MXSPyCOM.Server _reg_progid_ MXSPyCOM.Server # CLSID 一旦定下就不要改改了等于换了一个 COM 组件 _reg_clsid_ {8B0F1D42-3C7A-4E19-9A55-2D6F1E8C4B71} def Ping(self): # 最轻量的探活接口用于确认服务器没死 return pong def Exec(self, code): # 在服务器进程的全局命名空间里执行一段 Python 源码 # 约定调用方把结果赋给变量 result这里负责读回来 namespace {__name__: __mxspycom__} try: exec(code, namespace) return str(namespace.get(result, )) except Exception: # 异常不能直接抛过 COM 边界转成字符串回传更可控 return MXSPYCOM_ERROR:\n traceback.format_exc()几个关键点值得展开。_public_methods_是个白名单机制。COM 客户端只能调用这里列出的方法类里其他普通方法对外不可见。这意味着你要加新接口必须同时改类实现和这个列表漏改的表现是 MaxScript 调用时报「成员不存在」而 Python 侧一切正常很容易误判。_reg_clsid_是组件的全局唯一标识。很多人图省事用pythoncom.CreateGuid()动态生成那就埋雷了——每次启动 CLSID 都不一样注册表里会堆积一堆僵尸项客户端还是按老的找。正确做法是生成一次硬编码进源码之后永不改动。Exec的返回值设计也值得说。COM 的异常跨进程传递后会丢失堆栈客户端只看到一个语焉不详的 HRESULT。所以这里把traceback.format_exc()整个转成字符串返回前缀一个标记串MaxScript 侧判断前缀就知道成功还是失败。这是个很土但极其有效的做法调试期能省掉大量猜测。命名空间用独立 dict 而不是globals()是为了让多次调用之间不串味。你要是用globals()第一次import os之后第二次调用的os就还在看起来方便实则埋下「本地能跑线上不能跑」的种子。3.3 注册与卸载注册表里到底写了什么注册这一步本质就是往注册表写两处东西。理解了这个出问题时你就知道去哪查HKEY_CLASSES_ROOT\MXSPyCOM.Server (默认值) MXSPyCOMServer CLSID {8B0F1D42-3C7A-4E19-9A55-2D6F1E8C4B71} HKEY_CLASSES_ROOT\CLSID\{8B0F1D42-3C7A-4E19-9A55-2D6F1E8C4B71} (默认值) MXSPyCOMServer \LocalServer32 (默认值) C:\pyenv\mxspy\Scripts\python.exe C:\tools\mxspycom\mxspycom.py第一条把人类可读的 ProgID 映射到 CLSID第二条把 CLSID 映射到具体的启动命令行。MaxScript 里写的MXSPyCOM.Server走的是第一条系统查完第一条再查第二条最后按LocalServer32里的命令行把进程拉起来。LocalServer32里最好用pythonw.exe而不是python.exe。区别在于前者不带控制台窗口否则每次 COM 客户端连上来你屏幕上就弹一个黑框关掉黑框进程就死了这个现象我第一次遇到时排查了快一小时。注册命令一般是# 注册必须在管理员权限的终端里执行 python mxspycom.py --register # 卸载换 Python 路径、升级版本前先卸干净 python mxspycom.py --unregister这套--register/--unregister的约定来自win32com.server.register.UseCommandLine它会把命令行参数解析好然后调用RegisterServer或UnregisterServer。你在mxspycom.py的__main__块里只需要一行if __name__ __main__: import win32com.server.register # UseCommandLine 会自动处理 --register / --unregister / --debug 参数 win32com.server.register.UseCommandLine(MXSPyCOMServer)注意UseCommandLine既负责注册也负责「注册后直接以服务器模式运行」。所以注册完它不会立刻退出而是进入消息泵等待客户端连接。这一点跟直觉相反很多人注册完看到终端卡住以为卡死了直接 CtrlC结果注册虽然写进去了但这次会话的服务器也停了。正确姿势是注册完关掉终端让 3ds Max 去拉起来。3.4 MaxScript 封装把易碎的东西包起来MaxScript 侧的封装核心就三个函数连接、调用、断开。看起来简单但每一项都有讲究-- mxspycom.msMaxScript 侧的薄封装 global MXSPyCOM_Server undefined fn MXSPyCOM_Connect ( -- 幂等已经连上就不重复创建避免多个 OLE 句柄 if MXSPyCOM_Server undefined do ( try ( MXSPyCOM_Server CreateOLEObject MXSPyCOM.Server ) catch ( format MXSPyCOM 连接失败: %\n (getCurrentException()) MXSPyCOM_Server undefined ) ) MXSPyCOM_Server ! undefined ) fn MXSPyCOM_Exec pyCode ( -- 返回值永远是字符串错误通过前缀标记区分 if not MXSPyCOM_Connect() do return undefined local ret MXSPyCOM_Server.Exec pyCode if (substring ret 1 13) MXSPYCOM_ERROR then ( format Python 侧报错:\n%\n ret undefined ) else ret )CreateOLEObject这行是唯一可能抛异常的地方——服务器没注册、位数不匹配、Python 路径失效全在这里暴露。所以外层必须包try/catch并且把getCurrentException()打出来否则你只会看到一个空指针。幂等判断同样重要。MaxScript 的global变量在脚本重载后可能还留着旧值但那个 OLE 句柄对应的服务器进程可能已经死了。所以实际项目里我会再加一层Ping探活连上之后立刻调一次Ping()返回不是pong就重新创建。还有一个隐藏细节MaxScript 的字符串拼接。你要把一段多行 Python 代码传给Exec得用\n或者StringStream拼直接写多行字符串会语法错误。我一般用StringStream拼可读性好很多第 4 章给例子。4. 跑通一次从注册 COM 到 MaxScript 拿到返回值4.1 环境准备与首次注册按顺序来别跳步。每一步都有验证手段跑不通就停在原地查不要往下走。第一步确认 Python 位数# 输出必须是 64bit32bit 的没法跟 64 位 Max 通信 python -c import struct, sys; print(sys.version); print(struct.calcsize(P) * 8, bit)第二步装依赖并执行 post-installpip install pywin32 python -c import pythoncom; print(pythoncom ok)pythoncom能 import 就说明底座没问题。报DLL load failed就回去跑pywin32_postinstall.py -install。第三步注册。用管理员权限打开终端cd 到源码目录执行注册命令。这一步如果报Access is denied就是权限问题报ModuleNotFoundError就是解释器路径不对说明你用的 Python 不是装 pywin32 的那个。第四步验证注册是否真的写进去了# 用 PowerShell 直接查注册表比在 regedit 里翻快得多 Get-ItemProperty Registry::HKEY_CLASSES_ROOT\MXSPyCOM.Server | Select-Object CLSID Get-ItemProperty Registry::HKEY_CLASSES_ROOT\CLSID\{8B0F1D42-3C7A-4E19-9A55-2D6F1E8C4B71}\LocalServer32能查到 CLSID 和 LocalServer32 的具体命令行注册就算成功。命令行里如果还是你之前测试用的旧路径说明你后来换环境了但没重新注册这是「代码改了没生效」类问题的头号元凶。4.2 在 Max 里执行第一段 Python打开 3ds Max新建一个 MaxScript 脚本跑最小验证-- 冒烟测试只验证通道不涉及任何业务逻辑 py CreateOLEObject MXSPyCOM.Server format 探活: %\n (py.Ping()) local code import sys; result sys.version format Python 版本: %\n (py.Exec code)预期输出是pong和一段 Python 版本字符串。如果CreateOLEObject报「没有注册类」说明注册没生效或位数不匹配如果Ping报「服务器执行失败」说明LocalServer32指向的脚本本身启动就崩了这时候去命令行手动跑一遍python mxspycom.py看报什么错比在 Max 里猜快得多。这一步跑通通道就成了。接下来才是正经活。4.3 参数与返回值的传法JSON 是唯一可靠的桥COM 的 VARIANT 能直接传字符串、数字、布尔、数组。但实际业务里你要传的是「一个包含材质名、路径、参数的字典」这东西过不去。常见做法是两端都走 JSON 字符串。MaxScript 侧没有内置 JSON 库但可以用 .NET 的序列化器-- MaxScript 侧字典转 JSON 字符串 fn dictToJson kv ( local sb StringStream format { to:sb local first true for k in kv.keys do ( if not first do format , to:sb format \%\:\%\ k kv[k] to:sb first false ) format } to:sb sb as string )Python 侧接收后json.loads解析处理完把结果json.dumps成字符串赋给resultMaxScript 再拿 JSON 字符串自己解析。这条路子看起来绕但它是最稳的——JSON 是纯文本不涉及任何 COM 类型转换的玄学行为出问题你打个日志就能看到完整报文。写 Python 侧的时候有个约定要固定下来永远把结果赋给名为result的变量。这是Exec实现里约定的读回方式写成别的名字就取不到。同理print的内容不会回传需要回传就老老实实赋值。一个完整的调用示例把两边的写法摆在一起看-- MaxScript 侧把一段带参数的 Python 代码拼出来 local payload {\name\:\M_Brick_01\,\mode\:\prefix\} local pyCode StringStream format import json\n to:pyCode format data json.loads(r%\)\n payload to:pyCode format result json.dumps({newname: data[mode] _ data[name]})\n to:pyCode local ret MXSPyCOM_Exec (pyCode as string) format 返回: %\n retPython 侧拿到的是三段代码解析 JSON、处理、把结果序列化回 JSON 并赋给result。注意r...这个原始字符串写法是为了避免 Windows 路径里的反斜杠被当成转义符。这个坑很隐蔽——C:\temp里的\t会被解析成制表符路径直接废掉。注意拼接 Python 源码时任何来自外部的字符串都必须是原始字符串或经过转义。用户输入的文件名里带个引号就能让你整段代码语法错误。4.4 一段完整业务代码批量重命名材质把上面的零件拼起来写一个真正能干活的例子。需求是遍历场景里所有材质按规则加前缀-- 业务脚本批量给材质加前缀 fn batchPrefixMaterials prefix ( -- 第一步MaxScript 侧收集数据转成 JSON local items #() for m in meditMaterials where m ! undefined do ( append items (m.name) ) local sb StringStream format [ to:sb for i 1 to items.count do ( if i 1 do format , to:sb format \%\ items[i] to:sb ) format ] to:sb -- 第二步丢给 Python 处理返回值是 JSON 数组 local pyCode StringStream format import json\n to:pyCode format names json.loads(r%\)\n (sb as string) to:pyCode format result json.dumps([%_ n for n in names])\n prefix to:pyCode local ret MXSPyCOM_Exec (pyCode as string) if ret undefined do return false -- 第三步解析返回值写回场景 -- 这里用 .NET 的 JavaScriptSerializer 解析避免自己写解析器 local ser dotNetObject System.Web.Extensions.JavaScriptSerializer local newNames ser.DeserializeObject ret for i 1 to (amin items.count newNames.count) do ( -- 按名字找回来再改索引不可靠meditMaterials 可能变 local target for m in meditMaterials where m ! undefined and m.name items[i] collect m if target.count 0 do target[1].name newNames[i] ) true )这段代码的分工是这样的MaxScript 负责「读场景、写场景」因为它有原生 APIPython 负责「纯数据变换」因为这活儿用列表推导一行就完了。中间的 JSON 是契约。这种分工在资产管线里非常通用——你完全可以把 Python 那段换成「查数据库」「调接口校验命名规范」「压缩贴图」MaxScript 侧一行都不用改。注意第三步的写回方式按名字回查而不是按索引。meditMaterials是个动态数组你在处理过程中如果有任何操作改变了它的内容索引就错位了。这是个典型的「本地跑得好好的一到生产就全乱」的坑。5. 避坑与排查COM 这东西出错时几乎不给提示COM 的错误信息是出了名的吝啬往往就一句「服务器执行失败」或者一个0x80080005什么上下文都没有。下面几条是我在项目里真真切切踩过的按现象、原因、解决三段写遇到了直接对号入座。5.1 现象CreateOLEObject 报「没有注册类」原因三种可能按概率排序——注册时没用管理员权限注册表根本没写进去、Python 位数和 Max 不匹配32 位 Python 注册的项64 位 Max 读不到、或者注册用的解释器和LocalServer32里记的不是同一个。解决先查注册表HKEY_CLASSES_ROOT\MXSPyCOM.Server存不存在。不存在就是权限或路径问题用管理员终端重来一遍。存在但LocalServer32的路径不对就是注册完又换了环境重新注册。都对了还报错用python -c import struct; print(struct.calcsize(P)*8)确认位数输出 32 就换 64 位解释器重建虚拟环境。5.2 现象Max 界面卡住转圈半天没反应原因Exec是同步调用Python 侧不返回MaxScript 就一直在等。而 MaxScript 跑在 UI 线程上等就意味着整个界面冻结。常见触发点是 Python 里写了耗时操作——遍历几万个文件、下载贴图、跑一次模型推理。解决短任务控制在几百毫秒内超过这个量级就得改成投递模式。具体做法是把任务写成一个文件或者往 Python 侧的队列里塞一条记录Exec只负责触发立刻返回一个任务 IDMaxScript 侧用timer定期Ping或者查状态。第 6 章给完整改法。另外调试期间可以把Exec里的exec换成带超时的线程但注意 COM 的 STA 模型会限制你能做什么别指望优雅地杀线程。5.3 现象改完 Python 代码Max 里跑的还是旧逻辑原因COM 服务器是常驻进程。你改了磁盘上的mxspycom.py但正在运行的那个进程加载的还是老代码。更隐蔽的情况是Exec里import过的模块被缓存在sys.modules里你改了模块文件也不会重新加载。解决两个层面。改服务器本体必须断开重连——在 MaxScript 里把全局句柄置undefined等旧进程退出或者手动在任务管理器结束掉pythonw.exe再重新CreateOLEObject。改的是被 import 的业务模块可以在Exec的代码里显式importlib.reload(module)但更省心的做法是让Exec每次都执行完整源码别依赖模块缓存。提示调试期我会在Ping的返回值里带上服务器进程的启动时间和脚本文件的修改时间。一对比就知道现在连的是不是最新那份代码比反复重启 Max 高效得多。5.4 现象中文路径或中文材质名变成问号、乱码原因编码在三个环节都可能断——MaxScript 传出去的字符串编码、Python 解释器默认编码、以及返回值往回传时的转换。Python 3 默认 UTF-8但 Windows 上有些环节会按 GBK 处理。解决统一在Exec的入口做一次显式解码约定所有跨边界的文本都按 UTF-8 处理Python 代码开头写# -*- coding: utf-8 -*-不管用那是源码文件声明不是运行时真正起作用的是在拼接时确保字符串本身是正确编码的。实践中最稳的办法是MaxScript 侧尽量只传 ASCII 的 ID中文名通过文件传递Python 侧读文件时显式指定encodingutf-8。5.5 现象Python 侧一报错Max 就直接崩了原因异常没有被捕获直接穿过了 COM 边界。跨进程的异常传递在 Windows 上行为很不稳定有时候变成 HRESULT有时候直接把宿主拖崩。另外如果 Python 侧调用了跟图形、窗口相关的库比如某些 GUI 库并试图弹窗在无窗口的服务进程里会直接触发访问冲突。解决Exec内部必须try/except包死任何异常都转成字符串返回绝对不让它逃出去。业务代码里也不要在这条通道上做任何 UI 操作需要提示就往日志文件写或者返回状态码让 MaxScript 侧去messageBox。这条规矩我写进了团队的代码规范里违反一次就够你重启一下午。6. 二次开发把同步执行改成投递加轮询的任务总线原版Exec是同步的够用但不好用。真正上生产我一般会把它改成一个带任务队列的小总线改造量不大收益很明显。核心思路是让Exec只做「登记」真正的执行放到后台线程MaxScript 侧靠轮询拿结果。先看改造前后的对比维度原版同步 Exec投递加轮询Max 界面长任务时冻结全程可交互超时控制无只能干等可设超时超时标记失败失败重试要重新拼代码按任务 ID 重投即可并发单线程串行可开线程池调试报错即断有任务日志可回溯Python 侧的改法加一个任务表和两个方法# 改造任务登记 状态查询替代纯同步执行 import threading import uuid import traceback _tasks {} # task_id - {status: ..., result: ..., error: ...} _lock threading.Lock() class MXSPyCOMServer: _public_methods_ [Exec, Submit, Query, Ping] _reg_progid_ MXSPyCOM.Server _reg_clsid_ {8B0F1D42-3C7A-4E19-9A55-2D6F1E8C4B71} def Submit(self, code): # 只登记立刻返回 task_id不阻塞调用方 task_id uuid.uuid4().hex[:12] with _lock: _tasks[task_id] {status: pending, result: , error: } t threading.Thread(targetself._run, args(task_id, code), daemonTrue) t.start() return task_id def _run(self, task_id, code): namespace {__name__: __mxspycom__} try: exec(code, namespace) with _lock: _tasks[task_id][status] done _tasks[task_id][result] str(namespace.get(result, )) except Exception: with _lock: _tasks[task_id][status] failed _tasks[task_id][error] traceback.format_exc() def Query(self, task_id): # 返回 状态|结果或错误用竖线分割方便 MaxScript 切 with _lock: t _tasks.get(task_id) if not t: return missing| return %s|%s % (t[status], t[result] or t[error])Submit里立刻返回 ID 是关键它把「触发」和「等待」拆开了。_run在后台线程执行用daemonTrue保证主进程退出时不会被卡住。Query用竖线拼状态和载荷是因为 MaxScript 处理字符串分割比解析 JSON 便宜而且一眼能看懂。MaxScript 侧对应改成一个定时器轮询-- 轮询器提交后每 200ms 查一次最多等 60 秒 fn runAsync pyCode maxWaitMs:60000 ( local tid MXSPyCOM_Server.Submit pyCode local waited 0 while waited maxWaitMs do ( local resp MXSPyCOM_Server.Query tid local parts filterString resp | local status parts[1] if status done do return parts[2] if status failed do (format 任务失败: %\n parts[2]; return undefined) sleep 0.2 waited 200 ) format 任务超时: %\n tid undefined )filterString按竖线切开第一个元素是状态第二个是载荷。sleep 0.2会让 MaxScript 让出执行权界面不冻。等待上限 60 秒是个经验值真实项目里我会按任务类型分开配置——导出 FBX 给 120 秒重命名材质给 5 秒超时直接标记失败别让一个卡死的任务把整批都堵住。改造完之后还有两件事得补上不然迟早出问题。一是任务表要清理。上面代码里_tasks只增不减跑一天下来内存里堆几千条记录。加一个定时清理超过 10 分钟的记录直接删或者干脆设一个上限超了就把最老的挤掉。二是线程安全要过一遍。原版同步执行天然串行怎么写都不出事。上了多线程你在业务代码里如果操作了共享状态比如往同一个日志文件写、改同一个全局字典就得自己加锁。我见过最典型的翻车是批量导出时多个线程同时往同一个 FBX 路径写最后文件损坏报错信息还指向别处。验证改造是否成功有个很土但有效的方法提交一个故意sleep(10)的任务然后在 Max 里拖窗口、切换视图。如果界面流畅说明异步生效了如果卡住说明你的Submit里还有同步操作没挪走。这个验证我每次改完通道层都会跑一遍算是个固定动作。从那以后我每次在这个通道上加新接口都会先问自己一句这东西会不会阻塞 MaxScript 的调用线程。会就改成投递不会才允许同步。这条习惯救过我不少次毕竟三维软件界面一冻结用户第一反应就是强杀进程你连日志都来不及看。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →