Maya 拍屏方案汇总

前言   最近接到了一个需求,又是熟悉的 拍屏工具。   其实老早之前我就有写过类似的需求,只是表现形式各不相同。   这里打算将不同的拍屏方案汇总到一起,这样大家可以挑选一个合适的情景的方式完成这个任务。 拍屏方案汇总 Maya Python Publish 检查功能开发   最早在华强实习的时候,就写过将 Arnold 渲染的界面合成并打开 RV 进行预览。   背后主要用 renderWindowEditor 命令导出。 https://github.com/FXTD-ODYSSEY/MayaViewportCapture   后来进入腾讯前,我写了 Maya Viewport Capture 工具。   那个时候写的比较粗糙,我通过 UI 可以定义几个相机的位置,然后规定进行拍屏。   当时研究用 Maya 或者 Qt 的 API 将 Viewport 的画面截取下来。   背后主要用 Maya API M3dView 的 readColorBuffer   Qt 部分其实也是在拿到 Maya 的 MImage 之后转成 QImage 而已。 Maya Python 模型拍屏合并工具   后来正式工作之后,发现前辈用的是 ogsRender 命令将 Maya Hardware 2.0 输出来。   相较于 renderWindowEditor 命令不需要打开渲染窗口。 playblast   实现拍屏有太多的方案,当然最为基础的方法就是使用 playblast 命令。   建议安装上 QuickTime 这样可以极大压缩 Maya 拍屏的文件大小,同时提升 Maya 拍屏的质量。   playblast 命令既可以直接生成视频也可以拍屏序列帧。 拍屏需求汇总   上面提供四种拍屏方案,最常用的时 playblast 方案,因为可以直接输出视频。   如果是图片序列还需要借助 ffmpeg 等命令行工具将图片序列合成为视频。   拍屏的需求千变万化,但是有一些点其实大差不差。 拍屏信息 镜头角度   比较常见的信息有 时间,影片的归属名字(比如动画的某一段),影片负责人 等等。   添加这些信息可以用 headsUpMessage 将相关信息叠加到 Viewport 上。   但是 headsUpMessage 非常难用,而且字体大小等各种非常不方便自定义。   要解决这个问题可以用 插件,它通过 OpenMaya API 扩展了 headsUpMessage 的功能。   作者是 zurbrigg ,只可惜它之前免费的工具现在变成付费了。   劲爆羊工具盒 里面有拍屏王,它就是通过 ZShotmask VP2 插件,将各种信息贴到屏幕上。   具体可以在 劲爆羊工具盒 里面找到脚本 resource\tools\MSTools\MST_DATA\plug-ins\zshotmask.py   当然它是一个 Maya Python 插件,注册之后提供了一个节点,只要设置节点的属性就可以了。   这个方式可以结合 playblast 解决大部分拍屏的问题。   但有些情况并不能很好解决,比如我遇到的问题就是,每一帧都要重新矫正一下镜头的位置。   而且这个矫正还不能单纯使用约束,需要每一帧单独进行计算。   所以我只能改用 ogsRender 的方式,在后台进行拍屏。 Maya ogsRender 输出序列帧   使用 ogsRender 输出序列帧只能输出到默认工程 images 文件夹的路径。   因此要控制 ogsRender 输出的位置只能通过修改工程位置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 import contextlib @contextlib.contextmanager def change_workspace_images(folder): """Change maya project images folder temporarily. Args: folder (str): Image folder path. """ workspace_settings = pm.workspace(q=1, fr=1) image_index = workspace_settings.index("images") original_image_folder = workspace_settings[image_index + 1] pm.workspace(fr=["images", folder]) yield pm.workspace(fr=["images", original_image_folder])   我写了一个函数,可以修改输出位置,在修改回去。   这样我可以输出到任意路径。 Python ThreadPool 多线程后处理   上面拍屏生成的图片,可以放到 imagemagick 进行图片后处理。   imagemagick 是 maya 自带的命令行图形处理库。   在 Maya 2022 之前叫做 imconvert.exe, 2022 之后叫做 magick.exe   之前也研究过通过 imagemagick 处理图片,真的是拳打 Pillow 脚踢 QImage ImageMagick 图像处理介绍   imagemagick 用 C 和 C++ 编写的,非常小巧,而且运行速度很快~   这里我没有使用 ZShotmask VP2 直接拍屏输出我要的信息,因为有些信息想要通过 imagemagick 叠加到图片上。   于是我想到可以利用 Pool 线程池的方式多线程后台调用命令行。   其中 from multiprocessing.dummy import Pool 可以导入 Python 隐藏的线程池。   这个用起来比起使用 threading 库要简单方便很多。 注: from multiprocessing import Pool 导入进程池, Maya 不太支持这个。   下面来个实例演示一下多线程调度后处理函数的好处。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 from multiprocessing.dummy import Pool from functools import partial from functools import wraps import time def log_time(func=None, msg="elapsed time:"): if not func: return partial(log_time, msg=msg) @wraps(func) def wrapper(*args, **kwargs): curr = time.time() res = func(*args, **kwargs) print("[{0}]".format(func.__name__),msg, time.time() - curr) return res return wrapper def post_process(index): time.sleep(0.1) print("test", index) @log_time def multi_thread(): pool = Pool() results = [] for index in range(10): time.sleep(0.1) results.append(pool.apply_async(partial(post_process, index))) [result.wait() for result in results] pool.close() print("done") @log_time def sequence_run(): for index in range(10): time.sleep(0.1) post_process(index) print("done") if __name__ == "__main__": multi_thread() sequence_run()   执行上面的代码 1 2 [sequence_run] elapsed time: 2.201172351837158 [multi_thread] elapsed time: 1.2311382293701172   最后会得到用线程池的方式可以比直接执行快1倍。   而且这个代码是 py2 兼容的。   通过这个方式可以在 Maya 拍屏的时候用多线程调用 imagemagick 来对生成的图像进行处理。   这样用户几乎感受不到图像后处理的时间。 总结   以上就是 Maya 各种拍屏方案汇总,使用序列帧的自由度比较高,但是需要 ffmpeg 和 imagemagick 等依赖进行处理。   简单的需求可以直接用 playblast 加上 ZShotmask VP2 完成。

2022/11/28
articleCard.readMore

C++ 道法器术

前言 https://www.bilibili.com/video/BV1pu411y7n1 https://www.bilibili.com/video/BV1RV4y1x7qH   上面两个链接是 李老师 的直播视屏。   虽然 李老师 在卖课。   但他免费的直播对我 C++ 小白来说,非常有用,让我对 C++ 语言有了一个大局观的认识。   这样才能更好地定位到自己学习的情况。   下面是对他 PPT 内容的一些总结汇总。 C++ 道法器术 C++ 5个术 类型系统 编译映射 内存管理 设计范式 习语与规范 设计范式 面向过程 面向对象 泛型编程 函数式编程 模板化编程 时空人 时间分析 – 发生在什么时候 空间分析 – 变量/对象放在哪里 人物分析 – 代码哪来的,如何耦合 模块一 C++ 类型系统与设施 类型基础 存储: 堆 栈 全局区 值语义与引用语义 指针与引用 初始化与生命周期 其他类型 数组序列: vector array 与 C数组 字符串处理: string string_view与char* 枚举类 联合 位域 类 数据成员 函数成员 静态与实例成员 操作符重载 类型扩展 auto 与自动类型推断 const volatile 结构化绑定 编译与构建 C++ 编译机制 模块 (C++ 20) GCC/Clang/MSVC 模块二 C++ 面向对象编程 C++ 对象模型 对象内存模型 对象成员与指针成员 对象布局 对齐 和尺寸 三法则与五法则 构造函数 / 析构函数 拷贝构造函数 / 赋值操作符 移动拷贝构造函数 / 移动赋值函数 默认定义与删除规则 继承: 类型抽象 基类与子类 成员的继承 抽象类 共有 私有 受保护继承 多继承与虚继承 多态: 运行时绑定 虚函数 虚函数表 虚析构函数 运行时绑定 dynamic_cast 面向对象设计 实现继承与接口继承 组合与继承 编译时 VS 运行时绑定 设计模式: Template Strategy Observer 模块三 内存管理: 原理 优化技巧与避免踩坑 RAII: 内存与资源管理 内存与资源 资源获取即初始化 (RAII) C++ Java Go Rust 内存管理对比 智能指针 unique_ptr shared_ptr weak_ptr 移动语义 右值与左值 移动构造与移动赋值 移动与拷贝 临时对象与返回值优化(RVO) std::move 操作 std::forward 操作 new 与 delete 扩展 全局 new 与 delete new 与 delete 操作符 placement new nothrow new 模板机制 参数化类型 类模板 类型参数与值参数 模板参数推到 参数的隐式绑定 参数化操作 函数模板 函数对象 lambda 表达式 函数式编程 实用类型 pair 与 tuple variant optional any bitset 模板扩展 模板编译模型 类型别名 模板特化 可变参数模板 constexpr 编译时计算 SFINAE \ enable_if \ Tag Dispatching \ if constexpr 模板元编程 模块五 泛型编程与 STL 容器 容器概述 STL 中的常用容器 容器及操作性能考虑 容器最佳实践 算法 STL 算法概览 不同算法的性能考虑 编写泛型算法 适配器 迭代器 迭代器概念 STL 中的迭代器 Ranges 与 for 概念 (Concept) 类型约束与接口规约 概念定义 STL 常用概念 设计原则 Design Principle 正交设计四原则 消除重复性 分离关注点 减少不必要地依赖 向稳定的方向依赖 整洁代码三原则 KISS 原则 (简单以理解) DRY 原则 (不要重复自己) 迪米特原则 (最小依赖) SOLID 五大设计原则 单一职责原则 (SRP) 开闭原则 (OCP) 里氏替换原则(LSP) 接口隔离原则(ISP) 依赖倒置原则(DIP) 面向对象三原则 封装责任 隔离变化 优化使用对象组合 而不是类继承 针对接口编程 而不是针对实现编程 设计习语 Design Idiom RAII 资源获取即初始化 Scope Guard 范围守卫 Copy & Swap 拷贝后转换 SOO 小对象优化 Local Buffer 本地缓存 Copy-On-Write (COW) 变更时拷贝 EBCO 空基类优化 Virtual Constructor 虚构造器 Pimpl 指向实现的指针 NVI (Non-Virtual Interface) 非虚接口 CRTP 奇异递归模板模式 Mixin 混入类 Policy Design 策略设计 Type Traits 类型萃取 Lambda 重载 Tag Dispatcher 标签分发 Type Erasure 类型擦除 SFINAE 替换失败不是错误 Named Template Arguments / Method Chain 命名模板参数 / 方法链 从管理变化的角度理解设计模式 晚期扩展 Template Method Builder 策略对象 Strategy Observer / Event 对象创建 Factory Method Abstract Factory Prototype 单一职责 Decorator Bridge 行为变化 Command Visitor 接口隔离 Adapter Proxy Facade Mediator 对象性能 Singleton Flyweight 数据结构 Composite Iterator Chain of Responsible 状态变化 State Memento 领域规则 Interpreter 接口设计 语言构造习语与模式 封装 - 接口隔离Pimpl 多态基类 - 接口合约NVI 泛型隐式接口Template Method Type TraitsFactory Tag DispatchingAdapter SFINAEProxy 概念 – 泛型显示接口Facade Composite Iterator 继承设计 语言构造习语与模式 继承EBCO 多继承CRTP 虚继承Bridge 实现继承Mixin 接口继承Decorator 变参继承 内存设计 语言构造习语与模式 对象生命周期RAII 值语义/引用语义Scope Guard 对象内存布局SOO 智能指针 {unique_ptr shared_ptr weak_ptr}Local Buffer 移动语义Copy-On-Write Singleton Flyweight 回调设计 语言构造习语与模式 函数指针Policy Design 多态对象 (策略 命令)Strategy 函数对象 (仿函数)Observer 函数适配器 (bine mem_fn)Command Lambda 表达式Lambda Overload std::function (多态回调对象)Visitor std::invoke (多态调用) std::invocable (回调概念)

2022/11/26
articleCard.readMore

C++ 基础入门

前言   随着学习的深入,C++ 的学习越来越迫在眉睫。   虽然我在学习 Maya API 以及 Unreal 过程中已经写过不少的 C++ 代码。   但以前写 C++ 都是用 Python 的经验迁移过去使用的,很多 C++ 的特性都不懂,很多库也不怎么会用。   所以正因为如此,才希望自己可以深入学习好 C++ 课程推荐 C++ MasterClass 在 youtube 上找到了一个非常棒的教程 Youtube地址(不完整): https://www.youtube.com/watch?v=8jLOx1hD3_o udemy 完整版地址: https://www.udemy.com/course/the-modern-cpp-20-masterclass/ B站 https://www.bilibili.com/video/BV1Hr4y1H7wB https://www.bilibili.com/video/BV1JY4y1Y7uZ https://www.bilibili.com/video/BV1iA4y1X76r https://www.bilibili.com/video/BV1A34y1e7KS https://www.bilibili.com/video/BV1434y1e7N4 Github地址: https://github.com/rutura/The-C-20-Masterclass-Source-Code   教程足足有 30 小时长,而且还是 udemy 教程的阉割版本,不过里面有第一章会教导如何使用 MSVC gcc clang 三种 C++ 编译器构建环境。   我 fork 了他的仓库加上我自己的 VSCode 配置 仓库地址: https://github.com/FXTD-ODYSSEY/The-C-20-Masterclass-Source-Code   默认 tasks 是配置了三中不同编译的选项,如果注释掉两个的话,那就可以直接在 VScode 实现 ctrl+shift+b 实现编译并运行。   教程里面主要 IDE 环境是使用 VScode 搭建的,可能会有人困惑,why not VS。   我很久以前开发 Maya C++ 就是使用 VS 进行开发的,说实话,IDE 隐藏了太多细节,一旦出错,反而是无头苍蝇,无从查起。 知乎回答   当然也同其他回答说得也对,用什么工具都无所谓,关键是懂得 C++ 的整个编译流程。 The Cherno C++ https://www.youtube.com/watch?v=18c3MTX0PK0&list=PLlrATfBNZ98dudnM48yfGUldqGD0S4FFb https://www.bilibili.com/video/BV1gk4y1r7UH   游戏开发大佬推出的一系列编程课程。 parallel 101   后来非常偶然地,我翻到一个大佬 (小彭老师) 的课程 https://github.com/parallel101/course https://www.bilibili.com/video/BV1fa411r7zp   这个课程用直播和录播的形式详细介绍了从 cmake 到 C++ 的使用。   而且老师年轻有为,能力很强,经验丰富。 原子之声 C++现代实用教程(一):基础主线(VSCODE) gitlab地址 C++现代实用教程(二):面向对象基础 gitlab地址 C++现代实用教程(三):面向对象之友元与继承 gitlab地址 C++现代实用教程(四):面向对象核心多态 gitlab地址 C++现代实用教程:智能指针 gitlab地址 C++现代实用教程: Namespace命名空间 gitlab地址   这位老师也很赞~   但是还没仔细看… C++ 道法器术 https://www.bilibili.com/video/BV1pu411y7n1 https://www.bilibili.com/video/BV1RV4y1x7qH   C++ 是一门很复杂的语言,像我是从 Python 开始进阶编程的。   当我将 Python 很多用法摸透之后,进入到 Python 底层,发现 C++ 还很多底层的内容等待我去学习(:з」∠)   那上面的视频,比较系统地总结了 C++ 从入门到进阶的各个不同阶段地内容,学习 C++ 有很清晰的整体图谱。   当然视频里面其实是介绍作者推出的课程的~ 个人剖析文章 01_C++ 道法器术.md 搭建运行环境   C++ 语言和 Python 运行方式有相当大的不同, 参考: https://smartkeyerror.com/Python-Virtual-Machine   编译 C++ 需要有 C++ 编译器来生成汇编代码(二进制机器码) ,不同的编译器有不同的优化策略,所以版本和编译器平台都会对生成的汇编有很大影响。   教程提供了 https://en.cppreference.com/w/cpp/compiler_support 这个网站。   可以看到不同平台编译器对各种 CPP 规范的支持情况,如果用了老版本就不能使用新版本的 C++ 写法   目前 C++ 也在不断演进,从古老的 C++98 到现在 C++11 C++14 C++17 C++20 以及后续即将推出的 C++23 C++26   目前主流编译器的最新版本都支持到 C++17 了。 编译器下载配置   市面上最主流的 C++ 编译器有 MSVC gcc clang ,其中 MSVC 是 windows 平台的,另外两个是可跨平台开发。   windows 下如何安装环境呢? 推荐使用 choco 进行安装 1 Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))   用管理员权限打开 powershell 然后输入上面的命令进行安装。 1 2 3 4 5 6 7 8 9 ::安装 MSVC choco install visualstudio2019buildtools --yes choco install vcredist140 --yes ::安装 gcc choco install mingw --yes ::安装 clang choco install llvm --yes   执行上面的命令可以安装相对应的环境到系统中。   需要注意的是 MSVC 需要打开 VS installer 配置 windows SDK C:\Program Files (x86)\Microsoft Visual Studio\Installer\setup.exe   然后选择下载 Windows 10 SDK 再到右下角点击修改。   这样才能将 MSVC 编译器安装到电脑上。   使用 MSVC 进行编译,需要调用 C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\Common7\Tools\VsDevCmd.bat 脚本启动环境。   激活环境之后可以使用 cl.exe 来接链编译 C++ 代码。   而其他编译器默认安装完之后 choco 添加到 PATH 路径下了。 1 2 C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\Common7\Tools\VsDevCmd.bat cl /Zi /std:c++20 /EHsc /Fe: main.exe main.cpp 1 g++ -g -std=c++20 main.exe main.cpp 1 clang -g -std=c++20 main.exe main.cpp   使用上面的命令就可以实现 C++20 标准代码的编译。   如果你使用 Visual Studio 之类的 IDE,那背后其实也是调用编译器对 C++ 代码编译生成二进制机器码文件。 VScode 环境配置   有了上述的环境之后,只要运行命令就可以执行代码了。   开发工具比较推荐使用 VScode   个人体验了 VS 感觉过于笨重,而且隐藏了很多编译的细节,导致很多环节出错了不知道从何查起。   所以我推荐使用 VScode 编辑器作为入门,了解了基础再使用复杂的 IDE 才能事半功倍。   安装 VScode 之后,可以安装微软官方提供的 C++ 扩展   实际上 VScode 官方是比较推荐用 tasks.json 配置来管理编译 用 launch.json 来管理启动的。   但是这些配置对小白来说还是稍显复杂。   这里我推荐安装 Code Runner 插件   去到对应的代码就有启动图标,在右上角,点击一下默认会调用 gcc 编译并执行。   如果想要修改默认的执行命令,可以去修改 code runner 的配置   默认会有不同语言对应执行的命令,我们这里可以把 Cpp 执行的命令改成我们想要的样子即可。   比如我们想要改成 clang 编译也或者 MSVC 编译也是完全可以的。   MSVC 比较麻烦,需要先跑 VsDevCmd.bat 激活环境才能使用 cl 命令。   另外输入源可以改成通配符识别 *.cpp ,这样多个文件只要都在一个目录里面都会一同编译,方便我们初学跑程序。 1 "cpp": "cd $dir && g++ -g -std=c++20 *.cpp -o $fileNameWithoutExt && $dir$fileNameWithoutExt",   另外在线网站 https://godbolt.org/ 内置了很多不用语言的编译器   可以在线编写代码去验证,也能很方便地查看编译出来的汇编语言。   没有本地环境的时候也可以用这个工具来跑代码进行验证。 C++ 入门   学习一门语言,是骡子是马总得遛 一遛才知道代码是否有问题。   所以只是看教程,脑内编译代码是不行的。   这里我用 C++ 入门会以 The-C-20-Masterclass-Source-Code   只要按照我上面的配置,就可以愉快地跑这个仓库任意路径的代码,并编译出可执行文件了~   如果你已经有编程基础,比如学过其他的编程语言,那么我更推荐直接看代码执行来学习,遇到不懂的部分再翻视频。   这样比起纯看看视频会更快上手。   另外为了方便能够查阅 cppreference.com cplusplus.com 等官方文档,可以快速跑里面的案例 Demo   我用 Python 做了个简单的爬虫,将不同的资料汇总到一起 REPO

2022/11/13
articleCard.readMore

FBX 二进制数据解析

前言   最近遇到了一个比较难搞的需求,好不容易解决了,在这里记录一下。   需求是这样的,公司有大佬在 motionbuilder 写了插件,利用 mobu API 做了一个自定义的节点并在里面通过 FBXStore API 存入了自定义数据。   我需要将这些操作通过 Python FBXSDK 来完成这些数据的写入。   主要原因是 motionbuilder 的稳定性不可靠,如果可以利用纯外部调用 FBXSDK 的形式解决问题,就不需要依赖 mobu 了。   用 FBXSDK 来还原自定义节点操作都好说。   主要蛋疼的地方在于需要解决 FBXStore API 调用背后怎么转换成二进制的问题。 motion builder C++ 插件编译   在 motion builder 的安装路径有 OpenRealitySDK 文件夹,里面的 samples 有很多开发 mobu 的参考代码。   其中比较具有代表性的脚本就是 OpenRealitySDK\samples\devices\devicecamera\ordevicecamera_device.cxx   这个脚本就定义怎么将自定义数据存入 FBX 当中,并且利用 FbxRetrieve 方法将功能读取回来。   我们可以把这个东西编译出来作为我们这次测试的内容。   默认 motionbuilder 的 samples 里面提供了 sln 工程,可以直接用 VS 打开。   打开之后需要将平台工具集升级到最新的 VS 版支持的工具集,默认是 2012 工具集太过古老了。   改完之后本想着愉快地编译,然而这样会报错。   这个问题只能归结为新的平台工具集已经去掉了支持,但是头文件依旧引入相应的文件,解决也很简单,将报错的那一行注释即可。   编译完成会默认去到 bin\x64\plugins 的目录,这样只要重启 motion builder 就能加载到这个 dll 了。   这样将这个图标拖拽到场景就可以创建一个 device.   将这场景以 ascii 的格式保存。   检查保存的 FBX 文件,可以看到 FBXStore 的写入逻辑,会将信息写入到节点的 MoBuAttrBlindData 属性上   存储出来可以看到相应的信息。   这里官方的插件将信息转成了 KString 所以里面的信息也是以 FBX ASCII 的形式存在。   但如果将 FBX 存成 Binary 模式,然后再用 Python FBXSDK 来转存成 ASCII 的话,这些 FBXStore 的数据会转成 base64 的二进制数据。   如果用 base64 解码,可以看到里面存储的二进制数据。 1 2 3 4 5 6 7 8 import base64 in_data = r"cBsAAAABAAAABQAAAAhDb21tVHlwZUkQAAAANAAAAAEAAAAFAAAAB1ZlcnNpb25JUQMAAGIAAAADAAAAGwAAAAZTZXJpYWxJAQAAAEkAlgAAUwwAAABTY2VuZQABTW9kZWycAAAABAAAACQAAAAJU2ltdWxhdG9yRAAAAAAAAPA/RAAAAAAAAPA/RAAAAAAAAAAARAAAAAAAAAAAwwAAAAIAAAATAAAAB05ldHdvcmtTCQAAADEyNy4wLjAuMUm5CwAA9gAAAAIAAAAaAAAADFNoYXJlZE1lbW9yeVMKAAAASE1DX1NITV9WMVMGAAAAMDAwMDAwFAEAAAEAAAAJAAAACFNldHRpbmdzRAAAAAAAAAAAMgEAAAEAAAAFAAAADFNhbXBsaW5nTW9kZUkAAAAAVAEAAAEAAAAFAAAAEEluc3RydW1lbnRBY3RpdmVJAQAAAG0BAAABAAAABQAAAAdWZXJzaW9uSVEDAACOAQAAAQAAAAUAAAAPTGVuc1RhYmxlTG9hZGVkSQAAAADbAQAABgAAAC4AAAASTWFudWFsTW9kZVNldHRpbmdzSQAAAABEBzDzdETpTEBEAAAAAACARkBJAAAAAEQAAAAAAAAAAEQAAAAAAAAAAP4BAAACAAAACgAAAAxJbnZlcnRWYWx1ZXNJAAAAAEkAAAAAHwIAAAEAAAAJAAAAC0FzcGVjdFJhdGlvRFVVVVVVVfU/QwIAAAEAAAAJAAAADlpvb21NdWx0aXBsaWVyRAAAAAAAAPA/aAIAAAEAAAAJAAAAD05vZGFsTXVsdGlwbGllckQAAAAAAABZQIgCAAABAAAABQAAAA5BbmdsZUluRGVncmVlc0kAAAAAywIAAAQAAAAkAAAAEkVuY29kZXJDYWxpYnJhdGlvbkQAAADgzxJjQUQAAADgzxJjwUQAAADgzxJjQUQAAADgzxJjwRsDAAAGAAAANgAAAA1TdHVkaW9PZmZzZXRzRAAAAAAAAAAARAAAAAAAAAAARAAAAAAAAAAARAAAAAAAAAAARAAAAAAAAAAARAAAAAAAAAAANAMAAAEAAAAFAAAAB1ZlcnNpb25JUQMAAE8DAAABAAAABQAAAAlTeW5jRGVsYXlJBAAAAAAAAAAAAAAAAAAAAAA=" output_path = r"G:\_TEMP\2022-11-1\test_device.bin" with open(output_path,'wb')as wf: wf.write(base64.b64decode(in_data))   VScode 安装 Hex Editor 可以查看二进制数据。   而我这边需要想办法用 Python 写入二进制数据,从而摆脱 motion builder 的依赖。 FBX 二进制 FBX 数据格式 RenderDoc Python 开发 FBX 导出工具   之前写 renderdoc 导出 FBX 插件的时候,使用的时 FBX ASCII 格式,通过将数据写入到 FBX ASCII 对应的位置。FBX 就可以被读取到。   当时踩了的坑也可以从中窥探到 FBX 存储的结构。 Python 二进制处理 Maya 输出顶点动画到引擎   通过上面的文章,可以了解到 Python 写入二进制数据可以依赖内置的 struct 包。   写入数据需要了解 C++ 的数据类型的长度,按照长度和数据的写入顺序就可以用 Python 还原二进制数据。   通过 C++ 源码可以知道写入了这些数据   通过源码和二进制的对比,可以窥探到其中意思规则   比如利用 C++ 可以知道 Version 数据写入的时 0x0351 的数据 1 2 3 >>> import struct >>> struct.pack("i",0x0351) b'Q\x03\x00\x00'   使用 python 将 0x0351 转换为整形会返回 Q\x03\x00\x00   正好和 二进制 数据是对应的,中间的 I 则表示是 Int 整形数据。   这个规律我经过我对二进制不少数据的解读总结出来的。但还有一些数据的含义是未知的。   后来在网上搜索了一下这个二进制规则,发现 Blender 官方提供了 FBX 二进制的解读。 链接   这个文章有非常完整的 FBX 二进制规则。   通过这个规则可以解读出整个 FBX 二进制数据的存储方式。   比如开头的 CommmType 前面有14位数据,除去开头第一个 0x70 数据,后面的数据分别对应 EndOffset NumProperties PropertyListLen NameLen   完全和 Node Record Format 对应。   理解了数据的存储方式之后,就可以很顺利用 Python 写入同样的二进制数据。 FBXSDK 写入问题   只是我处理的时候发现 Python FBXSDK 无法直接写入 blob 二进制数据。   原因是 FbxProperty.Set 不接受 bytes 数据。   这个部分是用 C++ 模板实现的,可能这个功能并没有映射给 Python FBXSDK,导致功能缺失。(也只能说这个功能少用得很)   为了保证数据的长度,我的处理方式是用 FbxString 写入相同长度的 字符串桩 ,比如一堆 * 的字符串。   保存出去的 FBX 二进制文件再度用 Python 读取,然后将 字符串桩 替换为真实的 二进制 数据。 总结   这次深度挖掘了 FBX 二进制格式,对 FBX 的文件处理更加得心应手😄~

2022/11/6
articleCard.readMore

Maya RBF 算法应用

您好, 这里需要密码.提示(神秘号码 + 光子) 19a56cc198cba1affde8dbbee0cc4fdb5a7736152c78bb6a882495ffb27df7a130edb8662e032afb0a9361d6230be7911efe03c48bd8d11f852f8c00d71944d68351a395de3e48f1e797e263c10debcad286daa77dc2799440b331a6e8badf02926575cf51e0d3b85d59d3ef72152189045a3545c82137eca23553fbcae8310d62b1bee286764db486b0f4dc80bc3edc29b54f4d308655353abe23062c0a4a678e81998471115c7d8cace8da5a950e91ea008afbbad3ddc6309ba4a46bc6b7a1a072a514cb0a0fed405763651da66a305458104a8905806636b28130b4906d6fa6bc520b3af54fbee617e1a9ba2c67bbe6dcbde04ff56ab9ceac684a9263e2d27dfff908f5e8866dbb7e8da99f84adb38eacb1226588193264dc1800505269b633ab3d087532b82b2c4412c5bdd019bc9dee701ed35965cb51c814754e2c510523d6f5a7081b31c8237d8a64d896272f05f0fea3c3c6ab3f83fb6e86a9976cdec9db01a5e7019f76d795d63923c18254610fa61c60f5c8eeceadbe0135456556384a99d8c423450d5add1512a58cfcbe4b7b1d6b6e1c9ffd97915250a0b8f99fe0baba11a9a230639082bf12a010c0dcc3260141758cd1ac9d52131424f582c6f83bb3a692d3dbe4cf391643bc48ecbd64edd66d126f7203347f24924a97a66c72e53468dbb41de26825b63730599a8f40f501809b9a150da37ab602b6f840a74b9d161fdf1f03c6e31ae12fb1d98c89cf3d206bd35cffdf704b6c3e43292db3739b7cc5ff558fb2fe72347a43747abfe0a4f5081900e7010cf21acdd4b5988009f34d2b3263072264c9b613be4d07f7a73b74598b633afc673710e2d3d292681e680bc0d8466834184a139be7c3df764b0efa3f210b9fdf9cef19bef334121d685d68dd5f9bc6cc4867b0f6ca2a334e773d606ff40434dcba472fe3287af781dd7e77fdc8fdbbe609ffc634dc22a3d5afcae4e177a961dd3254ff015d316ac9bfceade6868969f6afe36aeeb3f3e18a63fbc8133b590c3df9c24ccf86052f701f74563b9737b223d707f6554fc9373656e65d74d5238af4e837769e21798f74d5958c9bdad1d09b8d586090999e3444a72c267afa686c641e163f8fc87fe339baeb20d82f664bb33879721f1bd27e69b827936bcc4da55c70245f16298afa1709c023a52c2a61b72c8e7e8105814560ab2906098736e226f07b9a283b17d7405ad6282f6ee8a1938f9d301a6d0455ba8ea6b3c79dc4304b8bd54d3bc30ce3fe828ac9b04d0824ae1879776e8f09895756b6df51b6cd632e8a071a3091c0157a1a566945756690e34a643ba98580509f711994bfa373bd5140e35a749353fc062c9e1f24e273a1ac37d0a58dda87d07d930b5e4ad4865984355b1b447a68b9f527a4c7a1c1617b3517891d39e30a6d3fbb4cc1f9072a02efce089172feb27b324dd5206d3c52c2f738466c2cac597ac1276513a0e32325d04c9691274d4ec935595791c93217b51adcd2bca9e955d019915cbf90ea3c148a652ded78a5245993609bdc9e002f914fc8a61846371b287cf396d03de793b24429d29c51b3654e10539779ce20ff084b071193b8b4f323d31ed3f31d3c836d645bbf5cbb6619e8c14ccb7ba75c9f245826f64a0c85af2f94d873031fea2ac35bc66139b8c0adca8889effd7773934faf040ca73a4fd4c8d96f5c8629e5ea7d271c6becd3bb5bc6c2f1b505ce362dd799c50c1b5c88adb52a57226feb54c67991508d5b4f560c9833da11f72d05f8c08e9fa71602f34bc0a1e6079d2e9012b81ed1f79abf809699428afcfc8e9a133d8a04218419fd8230160154dae2563db2aef3ddbc72daf338298a359aa3b18c33c26bd3431258334b979418c66ba586fb3c15fb768bac63885fbbf0f2f5f625e5bf346490cab3a40cd1650f6d295a0076828fc567c123f524d9f53b02d41fa21c7194b106eacb194d567048c0aeb795617377951fcd9e2e0c265f7582fdbd95d6893bd9066940b45b2c0eba9fa7661f899c38b4c4c9f78b69e4b16217f1fc246d3b5415addd44671e13790b42f0a47f5747a3ed2bd56d7e1893380c7dbb6a65afc297106d309265ebf6b5838c8970885032ab1a8cfe8d81caaa00ea1a7f0ac282d8cdd563a4077fe789d9ecb4b0a70c3ba6c5a30b0dba74289b70455e544e7435d500fb9a628c990b2f45ad770be46284b740a7acebbdcc3d210348c04a42e37ff527eec4640c4a4071a8b3817a8838db16c1c2f29d5b6f4375cd20338bb4d74577463ade7d907d7dc9012b9e1ff81feed2ef6ee6684fae3f1ca182858f037221ad5ec99f78be64a045f4a154a48d31f039c1ed4e9ee818e5470d16c118d930f92553651648c52d3ef08a05451e57b620be06fbdac4b6efc7d00d0647640b0adb9e37c214096a986d2f9ebaac6d743d91348468c6e0d2c862ed96d07a7dba39d5ddce86b76f17aa59bdffb3554c9ef7937157d102dfcc2f525e9094fe2c717bedcdad207a171ea50a33371c2ee7ed5adf307df3df3ba49053bcfbdaf09f3fc6d7cd74c252aa321dae08f2f50535de22a1964b1ea959e0e3e0a374de81ceb134dfdd1dfac7c26e8f7ef130ae811f986a9c427e487dd07f0201cdcf5ee04f682c62923d292b378b8de78239519c47b09fe1531277a3c790d83a2ed5d1c97e0fb9a6bb0b5c9626bf449a37a7f5620126ce3759647c87969b4fb63bc9c75bd77dfd67ef8b4e06d4d73a475b156718fdae30f38c7cff70f769a3cee4d95ba9e3c90d8d12958cfdfd77d93e105983fe22614cf9aa7c73b3c50bfbe3a2cd072de8b262ca0535661d12fb25d220065045ed0ba4ca85221fe26e8c403983b18b06946ac271c41c1b358bf31fe5ff95ec5f2c7576c57848d13bc25a507aa3961be2c7869132e1df521f6d003c47cb84c0243006c438e04ddc05cd124f5254a1bcb8cb87cf28fb1d7531ec94959b092c579a53851375e74e3831822dc3c58de32389f8cfd35a8bc1beaf362a06cb98a7ecd408fa03cc83270c589103e5c568af72919aeaf1ec4d5bd2d34bd05f29f62248eb9cb2e50979cc2e1859c38507a1b0948e4df3f8a8f93202551e3d296e3b8ca5ab4e71e26071b991010a88a3a7cf6dc21297f05aa5c9e75e68520f1caddb0b9e079c3f0b99d3c06e4431f22bcfa0715c49f4ad8cc9d505c28724ed7549ad6f58b82e7adae4f5757ae457dcffefdbbecd408fe8e3a1f9e31f535c971a829f32bc2df1f1ec1533d301ce6d421c7bb724d80607b94bcaad28aa7ae0bda082867bd0b8a0ff0218158e28b17dc2d7a04dcaed84664eab615a543cab2e9d42ecc1041de48f4e258014dbae3299277c19ac4e3f4779abc506cfbe559cd3a17f7a520906e3b7e1f29dde831db7b8e4c8cac4ba45b0560632565b635528a5eadb81b6a0f2e6c9407c55ccb58745c144200a42133d0c4a87c86a160cdb5cbc19e0851e4262fe1004e247c45952752a982d76ed0ec0c0d585f0b6dbf439a14243c3630b89193d8048d8916a775e425580f8b31698d6375412256dae8b745377608daeec0b30089a5cf8898f504dd4e7efbbe4f7487cd568c0f584f362bd4f2dfc2ce13850e31c84be17f37a763327c753bcda76cfeed013f4f31d602166968d1d04c5dcd96a8b2a25207c74f03ab9dc4f14530afd1a4a91ec2b1dc3b6fe47b2e0761af8d3610e6be3364b21620f658a971c386067fe89b8a580f29aa387ebe1470c3453559cf52eb428a011c2d3dfa77986a6486e1d04d7279d218e7a31e36ca12147f7b7bafeff4880644f590102e8859e89ce8e60b72120d15a862ec7822547481f1f1d7e57b5a06570fb6d6ea4539bcb0ebf5dd2c83fdaa159a2da5ac755d8340baac3d7363e0d5c14f32c2e753378602c5436fc44aa5ec83b30ac40d275de100030cbc9a639add8a9083df2298dbe2516a01fb6dd733c9f249bba2575ded893807a0a7be0f32a72d31d8bb2614ab7661bfd5fb84a86839f5b46949aea37e146a0af37b52e612335fa365c7be6eda50e858e606a39ef053942ed72b07967df21302c686fddd81de778b14476fc36bd7dcbd77849e3afa399f425bad2cdb5b456f9b8d2d8550844801ec46d7b9a5c7ff98a8255d6e3ecfae2f5a537452e4eb0f75b23ab6b1fe5581a82ad88ec28dfa4298e52cff830a07c82e29716339d753ad43331864ffccdb1eb5401861899802d397b12d3eb1b8b1266ceb44018c6c147de3caddd93285926926cd47e6b0428dd3c052a9bdaf435ae98ed77275f6ddc5bc7dd8127e7cf2187bbaa4d053fdcd30018722ab42bce291e5cd9c09d5b53c4716052662b8897f2b7adb23dab610a9000ea1cc5a10c503b37c929583d222a4518cfddc80e70ac44a117c02d0d2351cf88b9dac3f402b860dde5fc674a2f0fd634a74946a65530313a680fa6be29df23fce3c277b554bf72ecc58370748857f0dd79d9face84b141c93601fa1c2147de6969895b25b84759d55e5d5a663916a3b6d5d9e10bc0cffd6fd23451b03194ed324d3271a48c2e7e22ddfb01f36a1aed76c039faa1036bb4b2d6faf1ae8510142562378a49a40be1a75ec50cab30661796a8e1c1af6709376d920f5facd14975d8cd1e1259be0d9460fa9fb618993267fad16aa560bd3d2847060bc56eca2b301b5dae1fca5640bd32ed387d34fb7bb459728aaa2154b1301d9551c2692a8804995d6f64a104e480135a7132e120f2e1e562b6078714032237728b2cede70b7214bacc2afdc184b799d971a764cee01f8d33dac9a86f9897ec595999251733b2580d26f94c5009083448f97ded207a8c73b07464d5dfb21593dded63a1fb50fd340f31a4f1d76972b316e4666f677c857b2bb1cbc73dc29640f74661274d491b09f17165ff8d6184a6c126c8f54f52803225f830f8fc4b323d084415a554509cfa1cf2d186ab245db0a4815cf72a2d74ff1371b0baaff5519b3a0db645bf915d116fb89301e50b902b4abf66db75197b2483f31fbfea595a0022b400810edd62282e54981fc323ccd951b3e5295d0aedd46e3f83d98a5720725e77abe3bd4486035ce3c1153ebd1c8b5cb99b24f79b52c6710161615f25cc13b19427101e975e17eb1d765de77e323c637c8b63a56b862d27a04e600b1030ca6f1f97b0b292e56c8708a4c0ef19fa94834bd5203b4131a361c9c193c35840da221057a91dfcb183640aded4665caecfc9c324f75fb94e5eaf930f1d900c20cbcbe67dd56d22db007e684d73142fc0fcf2a0b631fd11c320cd07096e476ed674626356d61ac8a94196537122cb9d6748281a289c72263d5410bcfd6bc893f43efb869289679da5b8aa2b42835e1eb418b827d317a09b47feb031ff26e01ed0a4ff33194fa1c1d1a879568a53d9828a11c712bc485b0322fa951e765ef0fb64389945668fbf1097d1988dabcfd87bef86bc6cc60b8acf9745a9b86f617cde847d7b4518ae542ce377b87de1fbb86c6281829e665d443b56964e989fd71f63c1f90bd1e6743b7a890163c800fc1f5ae5a24f79763157b932d7c02334337e5216f86d254704cd78e9ca2104b08986b0ac06e3a00b52353427351b20736b3a248cac2308bf2ab2f3fdbaa9dbd91db2b57e6fe34a3d50707dd02ddb59b1650ccd36251a84f09919a35c733df4bcdc6c73d599bb9aa26371cb2a66fa475d9e4299d6e9e170cb0a00dd0cf1fb2cb55b386d04bce255cf27edf6263705c79de2eeedb7005660973998c7d596dde688f39f6f34b7b184397ccc8e4d956d04dd26737e15a2389788d18c2fca5855887f62163d6f37b04ef9c77cd215962774b59f8e48b5c31538687e8c8c5ed8c73b93498f618d65b770f7a9bfac4901cc7ae3baae46a44462dd9033e3838c82867d718a9cee96c973b03c4541428e5a56ed0421e98989b7e1b6ed125b12a489f982cdaf7b160104ffc7cd30fac29d9d563cdcf16dadcafbd091817ed6afaf5946b9fc56a2d6daf27254c8239e971b829e2f7d05691405e4f6fb7d224ec34cb69123f7ad50cc2a371fe98ce4c96e4597f25fdd29e5068cd12ae6d368a0e1be8958d55b53cfbe32289c71dddf8d5817c9af6386afc2d1aa8414a67f92c5db0fc11ba3204983282dcfef7a65d998b5ca4a66586fc7704f39786cd32fb9c79b62da6f5d945b76cdb7e581470ae02498e34f94d05cf06992ca6e28ab341511c308a8718019a1164ab23af8fcd2a02d850a9dcbf056bc9688b1aedbb232cd35739586a5b567db48b46011f9a69ba14805eda30a3f9b02296495f83664b8a9d927b541c388eef8eaa87f894a6cc93615769e1aa5ae0d2ac01757c6cc927b3245337707eb930f3aeb1d6dff8301458e35dc9d54864b7fb65bbd739b5a2f336ca48547141f0bace60a6ed9a1e45d32a75bd7703dadaff8db821ef93290d81138f02a54cabf5c1918071983afd4c0f518404ff0747c9bc6a2ed0859c87bace55c59d9cdbf1a9c334439f5637ebe0ea825e2ef37d7fa90a711cce8e5e5315881adb5fb5ff6533c0dab00781d41a483204be52cec6e88fa334aef87124ed674c5f2df8e6da61c8ed5e71fc7915379a45c34f2987cb786fc9232bd8b0f1b6b8104e93f91f1261847fac9fd6927864018c5561641b5217548e97cc0c7b825dd11c3234417e72858eeffd42def796f0d85f8921fd736819faf0ea0933bcac632e5f3a1d55efa5e7fde140f3527ec6be8d561116b68ccc316bbdef7dc9f4ef2aead7b7e2294c8b8bda703e80699e915b79b2146aacbdda9c7c640ab02e94484d71b9d4c1d69c1715f6dc2b7a4101f9e9bbd0b7c28daeedaa068563a0712aa5492b42247e6b34cfe952442c41a6b2182d16b08999f135e153edbcdde3ca802f179efab543cc71ee850ecc212e33422afbc23ccdaed9b47c662c0798528f4d000b5aa3a1ccb4e17699c92a93efebb97a8e4885cb13216767c65d85d90a5f4d8aa8b575e30377ff451acdb32d3f4896177b671957656020e541fc2945b04bfb2226fc95dbd1150d91054cb5c0acc29593f5f7901a2be112ed2eef688f4db2e5cf3674ecd585f645d7cbe5ce7710670063cfa0cb435e156da1305201ddbf55d47bab74f7eb18132bdf7cb81f137148ee9d723bab2146e7a11135f696dced7437b32dbb87052dffbecf16133ef4cf945c5e221cf8f9d92bec9f89ddfa6e4f6aeeed7fa74aed0f8ec6d258f69bb43e49b4e5b942f25043ae0b048ebf95421619ead63e8ecb7a0e493859b9df56257c96443177100c8b5c4bad68cc1ecb7c02be766da3382d019650a75898df27b473109c714a3b6317f7bd75accf39ad42e0f2a71ae0e0065c17b217724212baaecd683c13045d83b6c496db713484cd8304c0c0cdb0dda367708c6a45467f6f936d385dc5034d4050c1d92677a227aff339b2069f836c6e28e2a8a18a7d369f4db0d3abe9521bbc2e9c7c9e65ab3b5857eae00f7e2837268751a727ec85b43223fbf0c6062db73c400e9c8980041a4812f227d7f60009c81a92fe7652c5696cd1bce68e5772dc7030f22f0e40c683d2294b1cdea3037e5d30deabf055236f0d91f9671bc3e78bdbef146ba4620065603737d6c93a4e64f88bc11213d6650ee79d628412401f03397ad6afea1ab9338e44d0b5f17d38fc2e203280e26b633bd4cca591b1665622cb54f69be5bdef49370dfaa4fb0a571bfbd304ed44c557fb8505abc04142f0613670ff8ee5ecce3466a90526f44189f0ac362c88c7db44eba3dff2eba67bdbd56d34d5cdd6d420dc76a44ec4398bc1e062cca4f4221efad1918ac4da18c1afeb1c24c1e8f0abaf523b4d23a5e58c0955f3d289c9c514922fe7b0b49543b07a1deaaa18826c80841631e7e50beede735ae932cb1e029e2a6f1ca87614c9ffe35aa9040ca13284026be3321f10125f7fc62606e516bed68df52f55da8d49837c9b7cd9daf910b9197a36ee3d39395b6fdee6ea2e48e63b795551b217c70f3f5c2e263e2a6bc43126ef7d43f09e1edefd413bf15b2146660f68bd4205c6a64d3573ddeae133472c962e06bd62e57a4eb5565d130ab611aba6a27560988c08010066603dd4f94949fe00ceb9e92e7599f184444d739fb78b7456b79768d49cf890124bba22faad61177b0eff340222966a91b069966b0fedaf01ded680a099072ad04be0c8ce28143c7783197acf7739bc793613d4233eed61f1cba7ffcecb8fd3ed1efd42ae43769709cea778c690871a29713058b694f8b307cbff1d3b8c52a653baac9e461cc57f982b059eecd1009feaedd2ad76411661b9ba440a5fa4734fe2ce443fbe80df59ea4d3dd216257cc61f412593860ed1eac0d92dfddbe454fd2e59fb19b2bf0aebd8c434e96501dbba2945e1d66dc49eb079421152283d01451b18a3d489c351311ba4d27a608d825a38ce99299b99047f5ea87f39b05a60bd1f5e8dfa24e22e9cc111722805490aace4036b86122f50a3481abc7ed0d0a0100b5f6ad3b1113ddcfa34846fb5277e7092ec1568bad3061604dd434cb17b6445fb70fe889843576b869a9f3f3825f68116aa6dffab881abe72ffe38484c1dd07d32e64e1c8542b7adf0d5c254c6a045f2bcfe0bacb7c51c69dbf8e5a4915d2b9367b1c42db0bb55b4116439b3a1cabbd177086e451d5a319e37bc52b1587ad82f12af582927887fcd6b818f90235bc1dec24a375d08b0905798841b05857981f4293d5df0ab5806d5b4ed7339cb3a192a4bebff3eba6e01eaba68d704e9e556080c8dd36b611a7488d915b931493b258a890f993a7af169f8414731ccf1df3a46b1cc46e266330face62264748882d35b9b7fa6bcfacedc5a8d60ca5fef51025d4109c3aa6e04bb36fe3de1e4dca0c7f1cc9a181cb286f04e3f9110d683352f10e9005b30b07517cb9d5c0d74549a3b032467b72d67944f2496706f37afe05d5df89ad61d8bf78e73399ffba5006867ade15133c4b143a43c221d22ab79b2f6357a78407ec551f45a6e29d5a88fcef7e9c499502dfe1e05b1d62a6f9316f773ffeeeeb8f832641d193a54a6dd4e416fd139711ba042bfaa19e16a86d729bb3c8a71d7138ff6cc4efb87d21840b9646996946f4893575a467eb34e3416cb28d4660e7f83b14b88d536d1476f6c485c2053db05a7ea922c5e938304c84d61888ef89fe97d853379d8969886b11b7b890cf092ac15fd297e3af9664b94d236bb1f43b0d28608c85a8ea382cb75a1eaa118e482f00a79dd341874fd0c39cd884274ab2e2189f2f6d78f7a46acd4d4d9c7e51954e17ddde9b40edec1144105ee8838974166e0fa9286ad9a6fae5d2bc2efe2042fd0f5a532d7c084a1640335101da727ba1d8d34c244599af7e63a2e4582c02a3705166a385c16cfe57894dba8e3d8254a68a9c5c20a543631e7164693ad708d4496a8e4ecbaa82943f6e244162ac3b23e1fad2e787fc5b60a2d1064994634c12a8c43d8da0e2ba57e8c7c52332cf633c97b5e228d78d96b3cfb90d88e1c832d241663d74863bd1ffef2893cd3e5533b8b9d789a4c028db2657f4575fd96b5944e77860f1cf51a753e88a85c4ff47f1dcc0a53025365c5ddd27743367ab10d2c07666924a7b0d79142305a81e792cc98a371a76f77441b7583939fcdf2ec088e8fddd132fdcaf00d0f4b6c62ad89d764c9fe4c0d0288de6506c1673c35f16b4c31c1125aa447292f46ae29fc46e2c458396b6f4ee20bc8431aeef68aefe0abf0dd4b1622ec86f4ce9c8e1b3ec61e4ee2a3535c8d818dd89d8e727bc08c66f39f7fbd5ca302323bf5e4afc2b1a324834a442c45d2bdc01cad59a77159026b70975a9b883eac9a8d7a1f474d74b95af86b44572ac5143f3f3a20f6edb3690b997bcf2c33a2a3dbb614c20ca35fac7c06d444335f46e31897f0c7edd2bbeeab60e4c670f8d2662510caca1f5f78f1d34a75fdf262270a144214858c0989b8d04b0493d4d3db3549085c34948472e69b0766bf8a432519f7f6cb719576a9ac2b298a6f921604978add6921e7bdece76749a8c7c4b1096e296fb1c6734199b3f3ffd58923a353f31576909981b6bb6114173795317e37804cc2608e883448b31ad70238d8ca3931bc563a166ea8727a271693be52c70913df4db800a84298926ee12a937873e97ed951a42628c8985ab3e65f07f0df7496e90d2f65ad8db348bfcc0e5db9f5d6b49d56aac2ac96c7e2ebb318a5d2bfff882450762411793d92504651019adb5e816dc1eabaa16dfb21987ed631daa46cfe9ff8ed15f3b9d3bbe8ee08a7f66b37006fa2c3ebaf654ca0c6fc34f1f0363470f0b6bf06b383ed8c17822830b052ee75ad71278c4a257cf3258b3cc095cbe4b961ab42471e6afa88ad82d5d5914b92578d9274fb5fb8447fdaba4a915b83e4b4d1454f3a2690e4050e3e2c32b09a16bfdd98b9183949b4de8924aa9d64b39be456e88e19057111feb6482e990fd0a4093a85004f7b354f701d094b7aa8f493b4b562bb11da06bd5703c5649870ff16f594ae64f774a97bbfe9b59940fa9f6e55e09b938d072263547eb8c1189da0463c2c31fb17f3eae1d1fba0cacac0203bb80518eaee9375e1ab054e2bab010b136382a7d26cfedb42f1b03c4d830c3ea9ce3f8f4ef9726241f5827933cfb8ccbcda6dc4e593b095f28a6a94f6304b8eca59ddfd8496cb87e3b592f4f4341ace1a31d60eeff1ce54e04b893d2c29fc494d2da1745061ed5491b55fd42325f164f480e781968e6af87360426fbb0279216ee26a8a031c88b930f802560293d276cc1c7d93fea7603a497cf96942163bd40746b920827d078de1ea66704f891d1cc2978d9a3508b346656a7d7affdcd64333c2f25d93a90208280f5a55c6a5d8575b2d83c73313180c09a6b8524d105fb8301d3357b8e41f5f4d70235c6001dc3f3111c953cca6bdc80f7b3f2d95da8327f8b9207c92d3ea2cd286f0c6d23b325ae0eb22360256e49580483df7db738a36b1327cb6f35d6149d576efce245f39866ce0c7d61c4fc4d41fb6a334a7d2e043a1f741d4bcb8992ebb7f70d17fbc212212395488a3ff8bab3e7902cb14eaa851be96a26a6a0c53394b7aa6578a6695c4ebd8a2c15a6f313750a1df0cfe0c051ff50e241b1104f338a742893aaee084c25231f9e9ff3d1c943a91451f9ca545a9e3959439d6c3679576ffc7ea1c967b28a66faeadda307e196f7a5ba168a8d04d6728744ccb9b7eb90caa969ddef910b9a9455780f392a2da6e31cc1d34e7ba7f7e468e22217fe0fd23b4bbaca95d76506c09d520a0203ab1b3dd922e2f8f65e9742f236e5d9a3233f287a0db4c069d469748855a1542cc1aba528d7dcb0f21f7af41e53ddfe59ef41455bcb6da5dbd34439e868c6132751a7ad2d8627d53cc77bd75e3b5e1268afd189bc17129b8e9310d7892ae5719ccb584938cad711c38edd31ff7218775075d34bf4253f086f74ace6b3a6c6c12911362368b398049b35a789190b07836981d3e192bc3a4645ae8e3ab01f3a552994825bc58593bf650213baf17c758c31eea4372a8f93f22631178c61adf94d4f5dc65b2f89534b96cd08ecd5e907eefbbfa03034caa2e726a1b8230ac5ff1c30d653d747a8dd1095f04c075dccf543ffb17b51853cb567401a731adea4ba44a70fd6e2a8bc200ffe32d2adaad6bd55cf81ddc1c1cba3773e450376709d18c97d3c27895d219bf03ece55cb4bd40e28b83aa812af5c81fa5d275dd203ea39af1441238e36c6c32d33d742a09d779269aad07088a0e01025786022d91f2fe57ce3b14d04c1cb37550a24e1b01331740afb8b8b449b82e8985f79dbea2d9b75eac5f221bb22a6d0e734a79d118aafab6eae90869846a68cf46683a0bcec567caaa1b638c7dd82aa9184d09726fbeeb9de6069a54f3dd80f5e53f7568256c92f612d8c7c59a4580d11aff42fcc49dcdc5d67aa2202d045dadc8a1959dab2c6ab1b82ac4e8193ff99c1344c674bb3cae7c42724f7cda3e26af5b49af9af68a540181acd67a6b78039ff00e2e40f49c9db17092dc4d96faa2dd6cf6f1c4870e148954cad98e535726c3d19926f3c9fd90052ff8b0dae1d97d98a2b31922f84a364cc303791760f47287371f426d9c08fac48e3373980eaf0e55428a1664171186e94511f2be2c4dc1f822e30e634219b728725a946d206e25a94424f6c15db8de4a47f6adf5b0ea19544d1ae07f6e50de30ec9731535772a3f0037c823c46917775d83278a1d8fc378b4472e0e6b31e673c802c1e012747bd8082924d53188563be3fcdf9c47f3bdcce8d7f1537f4b667bde73120726d51dd6c04711ba14b13d9a8f8d64d90b8539d76064f2f2262642fe55ee50e1b32c9710a49f25e09bd7914bbb5bacff81137f2e4fb9bdabb7feb4326b9f79e69348d72fc6103959e7b5b798e2e3e3bfcebde66bbdb9da4e9ae80d999298bbe23b906b347189e58863c14072be68c3cb6eba1c877dfd22326e36926464f8afce06ec60d5143c2cd615adfa4020520ff45a321c16b584c639151e86196c3470aba490ae0984b0bedbe14616d80c6b1b787cab44eb34d5fc0ec33418d53a4795684304cbb66b9702d6fbc22c74cd2fe4b6ad0a9563bd1cb80b1b74d856dc80fb2825f01b26626767235b7fbe69e3285c5d4de5be5933ebce04c433c40b3bb6a61cbe73b2bf42fc244dbe8116833efbc0becbfa55f32bf7a94008a943e517854d1b83a76f18866b26a774ec152de4370e4b336d84a603a6452da15e0f290b8648c446348e5e314520b01c8aa1f8048f94708236aa924a9412308af19ff6417d07ece8bdec98dde014f4a8e45a56566e1e38150c411d5bc070b7a51a7b3cbde04e05416be4dc45ae7cb9a06bafda502f2992a457121168a44d5a26d73c12969d228b6ad6966b4e108331be4d1c9fe74178a21cb16461a972f06e9e12b3703707fbd1568515fb599a67056fca8ec15a5b208b7285d37b903bd9764996841c24de20457e7e1d99de9979496cca9d67ef737a0bc082272bd2d0ea343849fdabd65ac961f39faea8627d7154e816baaa477c602fe82d19206f7daabf47fc3d8e40974d707d4665da598780ed80fa5cf7810bf7220efe9545de0419edf2005d7ffc8a36bf0468274e5d96fc29bbd828c4703fcc3f597e1b607b724883a9466765e5e54bcd5fe0ee6efdd0050b75f84223759ed9159bd7880786e589fb2a134763b802776745da9c215433522f67dfce880a0e41f7e8d8a4d6a1d4056dfd5f075e5fbe3040a3ac15c7427d5e609022617b54e359a9c2ff78943086312f9d02db08b08de97f2069097f4810845709cd25621e868e747888d0a52b79592298c95377dca307f418dc12d8bf8bdde6c5e00d2f9f0c186ee377ebfbd7bfac1757740b20a3463ca6463dd9d836a00902b51973b72a121e4e4554a3006179831c0a1b5084a677861a81c244bb2f25d195ce1bde092f51564fc8bfa54ae16f76ece746fab6435929775d355b513139c04c15079d8255864f0bf96606f353483399812319d6834446ead0e1607402c8b220663ebf3788b6654824c935b894716a44495d0f70a8899b8b7c7cdce2ee199018fd135054ad4bf0eb8a051fd60d685cd43080f67623b2f31abd14757f417dd8403e878748f6ac56159b6931f2dcf8bd7f59896d019b93ab2c5a61b0e1be1a6821632cd40e786f5e0ceeb2ac027086a71b9aabd37351d5e21874a85229951dcc05836776ffabcd9af765081bc9510dbc422be449985c0c4e908c5850aba475e0c757b8beecd0133d9dda0e945e519d999da41d0263ffb20ab0d2de9ed9185b3115e43a6b9799ef35eb1793e37eeab3ed9d16f93eff8370b3d59eb57c21b907e3fdea73b724c1ef0e87003a26087bc374b8e4294988ff1fb03f186cc23a9a588b794d3421e55c5028b7db0c37c137056fbfcbf8e09a7d5c35e2966c449d75db5f6b594c2b783978de5b4911090acc3fd03fab0e18dc6d54f2e7c3f61e2f0d51f196ab4f7d0cbc54168709c9580fa6773903e52f2486162d4a6fb5e41f72ce18f1735ce3cea37bf020dc63d49a870746799b5b6e0324e05b2792cdd65ef7bf5b1e96cacc8f3ecd4b6f9745767b7d15c904e7a0da410a236fc1fd9b71708d0a40b1d75d8a39bfb17e05445f34495b4812df8d31e7f75508c28a68ba228d230918ff9eb0abf7c55318ea02bed28901a73c681a7d4ff262adb16bbfb2863daf990f93041567936396ca5c1974fae7f9d4ec8d18da22eb0e9c278e846ba177e5a3e374e878f8c3e07ceb441eaf7a758882467a2516e062f39a3316c65eed361af960accf1a906471263b4ca86bba9fecd81cd1674b1e49bbe613aae29f3a4b8422c325a2fafc4175a65f23c17d725e1d2a13b81230c2b1c95d8dec3d4180bfcc4254210b66468278ccd4b2a457f4179e06d69d13d9803a114595028434fd48667da4bce43ee085943bbdeaeb2c3db8a8775c6f557241483186ff061cfe437eebc246eb86c08f652c8e2a433eca7928583211650347c60806c824ceb992d9ce0026e6d7847eafb5f4d42697fb64ff1c253efeb1f87279fd27b8028dc94ef687434146c134ddccf4b85a5434557f9dac58a1a20290d0a91b798cd165f5e4bf5e514045640177974dfafde405873d7b2b01148c353124ca0a4447dfdd42c0f06789dac94991c4a5721ded1492e42d0dac903847895cf980959564d20c1634a419e3ae4cdb5720ca094d84c02eee9d8c0406ed92b2f7c8735c25beeddbf1e6ef717120c5ef293c7e28d76f0d01cbcfc705eebb9ffc9b617344b1a877a639732c20bd0f695381c2430ebd857c7a1c77d1dd466955d05380c72742fcd6ef93baf7a9a003d570f86a08cf0d500716bb1a88ae4047d2c4268bc192aaab9ad5d1a4f01d1ac11b18e88e87f2dfa134eb8f8a8f49449302122fecfdaeb3191b4d7c70feb5fa7a488f9ef74383659a28f231432c8ab182ed6dcd64b6dfa46f6bbae21521fd7fa9941836fbe79766d427eb2b2b9ea2cde6b4084bb3b7d026e047146b2a104a35703cddb474627fa814276fce79b84cbf1e6fd56816b9efa8eb419de2d50f0321985aed0b587678355d9a7324dadbdc1eededc87340da024144664f700863151941eae9709df379796176d0f77127f33d52d07a28eb108f51dca3802ddfb624ba9ddc8c71bf3e2ae3c19df5d66930e303e1566267b93f37540daaa57f09e2a927ac9ad1ce2c28a8b7135a32169ff422fb8863f060fdaef66eeb0c231dbe63a3704856c603bbd5ff541fd4375e39a03601b44f95b643f6e173bdb326dc57cfe02ce3ec74d0b58df546b169c2cd20389f5ac4e5fd9944f79eb8b0388ba7e0ee894b752b9dde4b5215f99735a10c8644f0e5c16349ec7d5a0439cb455c9982adcd94b1e8d8f119d0706cc7586fe6f8966ffffaa16fbeead7630e8bb377b82ceff55415bd71003780b179dc1de2b96f0cd68588f64085ebff13d379d46e247cdfbb953a7c1536f2451b1ab06152651c6dc21fb764ea6194f6158ac72aa94e479b7d1443835ed45bb21c2dd8a7fbeef9e7b68f72d0c653e1f944936c0ef7e633b53193a3123aef61129dc998779f0d910a8b46569b6f3ba5443768855a6667c8d4e63425a93fffe23f9e6dcb9f4a8c55005a3f57f87e3f5ff3b462312895b4496af2ac0cf8ddcc87cce136b7e9fbb6c345ce4902129b106103f1d7a011dfb6893571ac2478511fb1c570a357eae86365be9eeb7df795820e12c8b7a496da5aab9739b6148f387cde52ba664c68f4cf896d7292725ed6eb460d9700b1e941fd15539520f7590be13da1c4c1b43088202968d42cf2de7013bc80dfaa8ed53c81cc17edf84feddb098288cb02e465b9736ea6c56a88f7ba4f4c3c6a465ffc3c6e444b80778b7d843a4632a8ea988caab2789ec723ce44b0d8c36ba8cfe297d1025d0491159591e310eac185b45db84d79c13196541f071da08b7e1779caae67338f0a7fcd5160cd72f445b69aa407dbb7e840bb7f96228ddd41a00b7955358c954b02d777a6afbf2e5cb8d4e488a3ea8dfbf7f310453c3438a647d06206469837700d76b112363117fffe2836cc4658adbed215eb35793626b331e686f578e8683a5c4494610ddeba8cf1b0246d3f4f9e7f0f397d12d9f319a49c156c5914722b85a71a29e1d5cfed5d8e39ed1b7dadb13bf8c2d847795af9386862264d86905f1036b8800fde77ad130bbfb0ae5e73d93fcd683b5252b9848b6171e1555a72f792989a56cba5b292f5258d945760f69aac6deda3175098074f31173caa8fe6ac2dfb8d08dde6962d8698d5181188c0e7c0c86485ad5323567ca9dd6d3532024adee8cb2f602315a8172abc5f2ffb9edf2ea0f14abe5c373b2e1de57cc27e45bced6188d70248acbcabd725ff6c07d0a9deea84ef802846d57768b502b373ba9a844caca12db8ca711ec2936467842ba9a2f90aada424a17eabe117c84b3a2c82c28c3643319aad074ccce89566e15970d9a373059c6826a5117366963621198a95434549bbb669022217def6537e2967e6da8260e0cdfb5126d92c30d43aba3d301b6f8624f2583e079ecf5f70dbdd8c0eef6e28b551ef0077ccbdcf0693c523e969a0ba0f0682d2893605b2cceae95c76b722ef8638b8e6a34213af37f374b7a7cbd519982b0a392e4d76d316421386643823f122460c4212613939d87877199d6d6b58328a2747017580d1561b6b8b7cc6fe20a809ef900516410b1a048f6dabfb4cefc00440b67aae2c603e3b0a2725054c63a4fd8d0e2ba0eab564dcf07d7a3674115c381dcdc15b5794895d901662ca17b91b1abefdd0a8b37a9e185ffbbabdc2caeb4bac3756bc5822abb9ea6501ef305f4ff594b50674b27b9009756f72d62dbcda880c442dcdb43f8b6ff9e72de6512439194e3e82a15bea44944d45858b9cde4c40268af0a4ecc50bd15f9611490d54bf7f26997c6f2b06efcd0846a7c27b289da2b452b03ab2b26bfe1d3be51a113a9124bd00032a249a235b84de96b50a16ce87f4597e9b1ff694a6c49e3b4b429cbe89f51a0d253a363eea6c04fd7a39b7942ea667ba8db9d904344f32a84c4a0807816318c7800c8ff65f58aece9e04b6a93ca7646b5fe3ac3b9773d6a98e1d4f6ee6cee7cb912865ce5996df8a6729b294cc4a0b8c840bada4c441becc087d70fd8e7d1fe38fc295fb6f793328523ea09ee4a3c5e2a33a9a25d4a34e695b8e90bea8feb41ea6f1447593a337de3949bda801485ce18f3ab3dda46fe5233c856c7075298d80f8f7cde86d66418cf015f3cbec9c16356c9f3678843e5366d5efd7d412469fb09c3be6f7cde2a316595af6d34538b3b7cc8f76ddcd00ca09f22e6e310d9067ee96d7ab7867176d31919780d39aa3df69ec2890c56e44650cf949ba168d7b6c0a34edb1eb8dc8a216ad2a5ceb312fdca199878f00b33169d7950b98c52cf885059ba7d6a1eb40e14df74c4754075c98df6268ed427366991abc635e1ec37d2ba0cf9a1b0de801857f706e60516382b4c87f2a822fdb1bd6041e765bc21d58d2df68d61e816bbde1a26dc2e8f305ca768defa7a8f51f2816b932317cc922640e5a1858028c0636182cc9df0793c0354e7be8b535207c66e5078c085c3bdd3fef150aab4abf4f99be577f44f6dd939c0d994f6ba7f0598f4be95b8f93b195727aec3cc1e4753c348dab1ae9a5f78e4c2744428b14d1e34e63b183673b9cd3d238c25d2ce6875522c5d0194accdc2f446973d5aa1f73d68e21d4837cc73331a0adcc6d96fd757bdd85d21455c7ca1269eac56a52271ed487551fd8160618482376b046765a8c2dc6c6722eb3f257d199b921d0cf0adfd82f68d2270bba7504b945326d75a5b67ef471bde01710fa7067c896f2314ad7978deb7f0396535d7d4e3dd3037579290f3aa16b7cef834df82aeeb75cd624752ad21fda7ef1b1198105fba392b25a08ec2e218be174e1b46a52df0c02ddca6561498731da9cd430b95a937d1ba03c8d27553efaa27998ca78d80e2170e4c30707c00b8a2bead782458a1bb6da5d378f13f44aeff414d425293e3a937250b7eee6e538b93e4a5489ac16c9653ea985326f10a2fb34dfec624f41196f061de41477e9e5674455b5e8a04a1c94fe5442021d6297787235882c87fbd0abc33c36d9c22fa1554c8e20df31cfbf5ce0bc9b81d5ecdcd72879548c176a2f439951a1899f1056b3585fd798395c80070dc3e33dec62eb106d5a5993417efe2e93fd83edaeeb94eca5eca8bf799e77637b8ac062d43e4aa0ab88940488e04302d2e33f46bfbca73fad23105a64031e7ddedbccaf6baf142a078d562d11582fcf9d507023a64b7378d2002250d7fe95fb4e88b1303cf4fa8f9a4eea1d78ecff59742941942c706c96bd30cb9797fd82c5d38f70ef4b9b4fac2b0392e0599b0d4c95ec690d1368b2ed0841e72f17aefefb9edc5f0489e5467f8c33430bc19cb80951817eb6dee6c0a90571f78b8e526b848982b37b85a4e375c11308acee4f64073ee9e9c774d4f79242718fc94aa19ad3ac52f46aec95789659aba61a02b36f4ce40eaf3b58032f3cff9fe32adb5e969408d83288ac9bc854f1c8a6888634d31f941e5a4fe15d02205b6da9c572c398113e0fe3e25a72cd825d7334d812146e296d7c4ab486efb73956e28f94a6c34c2e1b2c5977d5289ed4264e35048a9cfb5103e44accf97177c677a7e871eee5d2a346369412720584e93304c77169a2c706355347e0159937e4bda5cdf1638ae25f7ba485a985738fc373c23af11c66523c67b155cea0a423280f8aff7627c103c254ab17086e1dea9395f79fc32cba0d0d409112ee7ac0ae0477e8508c7182c56b1a950b7e50e0424f924630d640e9a4e737ded9d2eda7419242736647b4cc3f167c04d135642c7531f1f6f492e392ee021c10b8dfa9836fc99b38f86f7f1b0a62cfe480d33e6d836ce032c0e4a4fd5f6cc68cf58ccc7c395369ed352000c3f2df406df771ca9b26e4488c8d38e50494977a1e9842d79ef52ea7ebfe3c7d17610c987648ce9931cfb20d40aac0e54f2386b7effe024d72163832c1ef6bcf3c3a80a1cc1e232657aa0b88680a2b299b04d5cb543697e600cfac5365ea55608bc5cddca61e25eaf186f5b60d5674e4a1aac06992ecedbaa3951f8cd3305f5d99569b29f2fa5e3260b17e9e017ac3072cab95f7438626c9753f4a446caed98615a99f280274195e089ca1791b0d5dac090ceecc897b8b8678dad7f6915315f5d6a32a62daeb5576c0d9d0c5d5766476c5d8945c3038d5ca4e9c087f16bac8de3ff225224ad13fd5f9645fc0802431e4125e97ad721962c55050abff366d5f6244248a678408187bfca3d84a150357d6a2c98263e5d6b603788fb2283fcaf02ed717f4697b53a938c6b15f870bff0fec9d289bd5f3bf648874e106fb95f138ed16b3fecb3256c97dc157838dc41a84b312ba85891fba5994ea7976b0a11bbb527fbf952c4fa2d9123dbe3a5411de4716e20c7bfd4fc2754adf1ad178096ef97b7807e91442e529874a84c194e749a28595845bf217562aeb416b226f8e7960ab0552776e0b2bfc612b3fdb950b7d27b92d0f0a34c250b971a6e80f487a6a2b823a68549eed7507e9fc439e0b4d8306d69a9da61f379c247d3ad858b8f33b08f610415d6a47f22dbf9f9658a82c29d4f7c065252e6c22d6d1cfae0c74fe1e7acdfb50c26f286d8496e38e69428ac0af129ba5756cd8ac41aa52d5c2daced30dc3a1b521d735c133b5f19036385bbd4da27dd4be554016cfc51610b829be006243248c40e763eb261b066583c1318fd977cb53b23eade911991a967268c319a88186e9633f39ef159fdfc77d61446866c1b93ad280bc76fef6389b9ec474b3fa1089174993c0504ba54bc39dd4c6bfa7deff817a20ff992798626f366e4d6692e202f8353caa91cb3e3171fa6c4a85f8aa4eb96cecb3bc5e6aed06856756ae3e34be7e3549803e75ae6514f8566af59d3b883301c40b9acf765a163b494eebfebcf0bfcf1e05596c4620bbd0d8a1537e782275fa29dc60d45324e7b231fb1dd1533e98a7c49646500b65bc3b9695a46dcbdbf6ff051b1fb2dd5f8a24dd736df6264976c09474ddc2862e2770ec72e65ae5171936c3d01ae655da4462f05f1957181ac371db24625f4a98428c92119c263905a70d09aae67609b3c356b606df55ed3cf042fa221606f634916550e4bebbba7b935a766cc567c4872a52136c0164a71ba5d7885e18e99a244e73318f5d728f58969ad3ff02e154fc1baaef1cf4583293204089cc1146c4ee305ff2dfdffcedcc0db2091192f7d2342cea25c2c185403647eae943e9dd6b6913e36082a9bad88ba8a519af1319d7ba8423901967cfae2f0dc46a5cd66c57a91158ae9567485974a99c7f6548532851400bcae32d228f31da6b6001a40e8c6f9e6ee57ab2c3c71f5fd5c719a7fd1487099a122e97c6f2c151e6c104d2e131af80a4b56a558e3ff589b0eec89feb9f5c07593303ed881243efed3a9cbe4323e43696252dd02fdac2798ec09f10139aae615ea9a1e8d757e03b4c010e84d56270e84c079e6c57c76687fa4b3d00ae98cf09064db59c230100e41da97e6e030f37241daae08bf28fa82d4f36ecdd8b22bdbc5cd0ca48a5ec5a28cb31209c033319a7f4d662405dce94c84a8402f5a89dc7778f20f013b8cf3a2428027c9c8724040fd14f6369ece86081a0c55279b534d1b67f5cd27cb38a6033a2d1008c97f64e427463dbcbca31f69cd07f6e797bfce4d201d2456077e21e50ba7b6354709283a3fb348b73e28ef30a39e66a1e45fb53af8b019627328711f80e495c05bcbeddca727bf8dff6dd42cb0f39a0ee0c70d904f42249ac5875519f3349921649d6a9c178674908ff835551bb99c8e5b360b474797d910490587ef7495f76083bc316c467133a488e4d4e4a5f98a3ea97c9cdd550711ea04fbace1ff8a2ffc32740dc18fec5522410cd584c037ef317113c6db5541d039d4f6b486591f3ab0722e4d15702da4d84b98cb80c9dd05b32e7dbc8aa07911cfef702b083ece938e93c40b9a86136f572273598b745f135902cbb5b20341cf7037ce03931795ef9c5db6fcebef24291d48bd3e1de7c0a776d6fd22c795ddbc6c4ed46836c9e750d5df097f09d8cb35617e707cd551bdf8fcecb895ee537f647b2e5dfc53e1e69c7d7c922d4ed0e58b953ae709985591471a03dd04e2c7a5c60d3aa315ec95a1ff82e69bdb44e993bec517e910e431415c1c7fea04405b5207ed4125f6440da277f3d4ddbaaf972f53017861464c87da08ac8a017e1aca582ffee10f1196616de585cc6ef66c952abe4f1d05ccc9bab79f9fe8b65624ca001880f54f7079d793f66dc430260d83247038c3f719335aa3229e66bacac285205810f31665bac609f28cda90d821b8e495267350f00517174819111192ee1aabce8183d87828a7dc70a40c995adba5550f88a0b8998c74f3143e98772d58b9f092329853231692fb2de3589da4535017437fbd426ab2788a33f009f7ab122e5f88d80cc41772bdef910c652ffc373d74034db31d33c81958fae03b911b0de44b02b6cdd3eecd6952d4ea16570ecc3eb7d06878cf9f2757a572f13b7b76beaf8b74c549dbd18bc3da0a245adb7c3af134c5ae92ece1f871afaab3a8c45a2ebf23b92fafe3c63c08c697cd1b8c597e97b18e616fa06ab9ac4b90dc5d2775157ec25c42654ab399cd7fecc3d6df8ebfeeaa402edf9cfd38cf855ac74fcedb21906c63a4196930a450249cf0ad4d52fb88ac5de5d56d31aac05c39d26a0590a890799646cad225263ae4402ace5b9d7fcba6dcdbed36025391c2e12a9fd04c4fca0126b2dea757d2fe5f9106456eb8c4e350ed3a5950449c9c05fda7944637366edcbb067107b18d55b447d5ebc63e96b8fe5a4170f4f1e1fe92b0317b4cae7d6ae8d867c766c9a63910b3222d25b06ef3efd696c0f1a946dcb3792844304da22c60f124dc5438424ef7afced05bf3409c9e0b660320a66a5ddf27bb300df6ba2d9095900d641ba287b7a86a2d2838da70e11723d99ac99e16ca51060fbe3730bd488e15a0f60cf8be2b4cceb2cec2fd242ebc154e00419c8112ef2da97f9fa591ab7ca515ff501da952d4b14100ec0c67f42e11f21a0772733532f473e18a86a799e67292e6c451f5be6f78ca1a40a010ebf56ad4f6074305e8fc5863a728aba229148e1ee5f4b02573fe8f054a78b144f9f4339269e138e57e529c51c371d329e7ddf61376753d519e716c50ed99a2e585d783a164c4a8d30df7d80b99ec3e38628471a27d0f280095a0eae6c70f9d827c8fb4015ba7f2d6fc478aab09e158e104008d904f504c94d4f72e37d186de2f9b926337cd007286697900a8816057e73e31d5ef04f4279bd5ec2e4f22e6125194c592bf39f640fcc23d1c1191c468aa933c368b67cc5ba05d1839d1dcdc284fd68cb4ef356f24504366a8358b4ac42791ab88b5ec8cbbee47cd7ee97f223916e2377ef6cab55c2689a46b93dd83ee8dc805f4aa1f19bfde47c98dfd2090d39d50b5b821f7edd634a9b32664d97c71838250757c347319478bb08de272d9e2a630695535f9bc611de5512c0a031342f481ae4f311ed106412ce1dc105e10046c72dc9708d2db71b6cdc99841d624f21ed8d27d8de35cbc10f6f86f43a75aeeddf7c753a5d2758f0404b4db86689995ba470fa9c2d4e69723958b1a39f9e10b3a496f546c2b4966b6a31c918b2e36d4e0abf5b600ad28029787c89663ff0d1f58733c1f2f57346d4a6d03fbde10f930ca7d5a6a927ac47afd4d04a8ceb20cfec0364fd4fdc126af2732f7a4cedb1d8a2ed400b4b40ab387b552ea32629595604397eaf46d914d1ed8d4de019cbc5c7bded8f101c77a57ef442fe5d2398adef697ec08eeeb594327bc2bd2beec4c97917756f8236ac34385e44a40eb269c5e736be946ed1f0bba1723f3e00630285d1cc06dbd82810287fda76cc358c8fb1a8bd85b8131bf9804cb0221a903412b150348d80fd0e7711868127eaba79d811cb54ce8012404828d43191ac8238e1ca67676adc9e518252281294a504ce3606a9cd01d78c561a9c221807ca641dd59df9ce685f00d74f0f57d778817f6ef4f7cff47c273b472471e32b60e20a5ca46f2047ed06502918867473f86116d96c0a28f485bb3486ac634892deaaeba57330a7701b731ad94fffaad2f92dd0b362d311cd8bc4479edd862293188e06edb05aff0e2703358266ab07baea40cce2a6b57fda8a40ee659bfb16b4c8025caf406a30a0ac6efa6a7c70214c0bbf52f73ad2cc5cbc4b2f48b41cf26e3d33d2b9d989e113f005b6978bfa2923953f50c10329eff35e284c5ac7f51c63c56866980dbed653ec78199ed5293ce425573653c563d1c24c4b78572684998cffe8ba2ef133f307c58383c850ad060898a36505e35da012fc688a495b2be37d87da1834ab51415a756e3305ec228f5adbbdc8d4fdd1e970229c6636851af4469a0b92e4cb6e34e5fdb76257302c8730bb6a8add9c6f7b71081ad1868a5f42fca1aa22f01b55b78311e43e2bbd479ce617462c19a33581555c666518360796c6f13b6241316c426c3d9e29892c1d2bcb2b27c81d98bd14c25f2a9433fe9e711d02d82bb506f7e1550613a1343fee0c9abf86aee362b9602c95b41d76866593a26326cbbea16b670312830cedf6d4c58205b7f849a43ab21d870d38a5a639aa638ef73c0379520ff11c0bb7c8686f560995741f8429d55a6c6a9e7b46cabfc6bfc7ebbb6ea0855ae080c65d297c2beb953d4905c4c94b01c0421b47b541271ee2e86f3cd679f101b828d5518f20d66ad11931ff8d838fbac922c5cc4f1babc020c11acb7c135c2a4afe7333dd6e830cb07f1d79aa2bd26cf5d5ebb3a1aec5f7e9d3045fb79601f0cd5e8decf5f4fcccf19818e95b8e40d6a2b2a321844f41c813accf56262e47d1d740913f532673b5c780b1ac3c6e432c92f6e4381c3e1ac95bf288c2af45fef3256bc6b2fad12a4f438c3f679ca2fc59a3be481def124d02b7124318db4670e9a8aba26d64174de1aec9c15c06a55b4db5396bdb6ff7b6ba2f1fde13900b7b2d5d52d0ed0542f8608e2614d20ef3cfadeb9a2d9596d4497e44f90b1dd2210d5f7fdc12184c7b6b07d4fb672762745ff0f9aab03151024aeb2ecca40442208394305d8b1da36eccfb8585111d0a9d4d74ef2c36262f79d85f7da3eddc5c05f09dd614d345210e5c67aa5eac71915c74d9610440e891942490f9dee36fa98d884c12a4dcb9f2863230e458edd904b52c45810f512085b3621f40eda9588c9ca683fa925271b12e80be829f10420a4a2c725c03b43dba01654fafd0cc951c6adbd99a1332163aef3561284059a0b6e7716ea92df29756dbe56a224beaaebb7e7fff1ea33a4434c16ed1bc1beb7342ef8888df6df595382f3358c9a0183625fc9b5bef1831189aab16690288f1db328ff4437f23aa9077950fe5f4394c180c57b91655781650c1d7fc400f86afec4c2ec14c8777a8ea7108d181d3e0a8cd96a159877a1331ad34027b6a9f49ed734404c7f19534a2b193075cd13f538337b9a6516729ab60d9d0f993e35b72a375ca7fe91f9071d0b3fccd060446c79a10cdf29668e6219ca69b3f5e38b2d77b71ef8f44aef2d07ca67de83e86df5525f3116b28f961d3cacd5779484d52d45d1a31ad3a0f2706835e1d93d7c10333d22386ee9fe9bee1300a28e9187f8e26e0165e50066f1c19f665fc5c6f9ab8a47b9299c143adb7e2879a1a7a740a388cad993e7a197332c22d546502889bb90566af26b7e6e1799bca849aa7ab8dea844c7c0059593226242c58a62e4591d8e5faf5d5f0d4d85fa265ade3a932314f7a801279a71a0f16d218b684ed023b6958bea4668cf9f5a7a9a27c98862d1c101a3bbf3355910fd1b70ec94d522a4e1d2fbf35678dcc282ca2dc33c74c3a124959939d9c4d3796056a48b78f238b21ae33c6f8db9a3a0edd9b3194b590acd8c2ea36a3cdba34df70ff56cf3af01ab31654fd8d8a6777da7ec4f8a2679feb46df9cb662b141f38208bf40ad97c17c707a84664b45a18875e1aec3bc62394582236e642a2825c0a1945f1129d214646611a4218c39136c039a89ca4d7219590bbfe10de0baa155651a009280fec3ac0545382310503b864b1eb719c24a2100de0e170772ab0eaab99d4f774f5ef477cff28f5ab7d06b8c949a3e2579000d506d7d23b7f6428f56c802c2408b40ee1a8c65da0788fef8b2aa0cf9c88e321f951b628d684e2c5bbab19bbd44e83840af2e9ff6e8bc28e9c257ee8471103523f0080e53adbd2b2e5adf60d7f66953ff18dbc3af1d15ec02f38cad92bfe2270d155a6dee59aa16e88715a479909acaba21489af8c9db119d76094d6da8965e9d41968f95ff99cec7d75f0d0cea7cc99e309d79b3b36410bb76e47b799ff8e5867da095b62d0a9f2966fd0255946de3d311405f8f421b77ac611303935115c92323151f5003ea7626b5407085ffd0b0cdf00cbf8b3b4043800828f2826f34859c2414435b0f463915e523f908f64f8426e870c21eb2dfbd458560eb3e9e66c229bb6da88ab3e3e7678363a7c463e66bca0857c567462da9735c19b11006ae7a67b69d81f2a64a5ad00efad3f0072b3a68b8803a7290bf8d56b2eb660fb75cecd80616b794ec69219b0b0f5887f49f8318adfbbe5a9619246991726188731db5bd50f3a1d4e47737fae3604d68e3ea30e49a7d3554f98ab69c01b352e408ade9eee6475fc7ebd3fae2c6be4ae9da12f983497356aa3418b342e82c4bfce7080b8139af0bb5ebf573181eccccd7617b4acdd84982597ac0803a26fb34213e5da4beee644ba3aa2917e545482ed019188825c706262448d9ba7000b577ed8500b9df94bf1d28b1ecfa27dc456988694aaae784932a2b63fa53e95cb06916ea574c2bebb9f64f630e5a1120016a6cfbb99d5f0fd67eeca7cc6ad596dc9aca78bc1c05b436df1af49d44dd168f654b24ce8880d774a5bc25ba9509c21a1cb71a58aa661721315409a2b1fcc87e143e1165449d7fba0bf20da89f0a5ac003ef484a707a49be7338603a597d728beb0f4856fff36627fd2566b4f39f3488540dd9dd7bc3629dd42eec8c0c5df8e84b355ea7362b768877277fa6ead1cd45a81f4dd227c223920f42ca09d37032c09831cf68660a45208882bd7d2dd97b0a375fd1357e7b979a390312f83697a68939a6a4c92e1ccf9617962322803fdc11fd460431b776f172d270ba5784db1b45eda5158d4759fd95e1c6df9f8e313060dadfef9a0b5cde1e98c545132f447dd0f4d6cfa835d276aa698ac863b6b11995e46f747d7a0ebe82a35f0bc09acbaf38f26afade105f7fbc3f9c3411ecc82c149c3528dd27f8c2a3faa65051eee67df051244a0c6b8dabda0916158fc0cf35cbaed01b12f78f2963789962c1391523b4b9e4b2d49bc90a983782c7dc87b94ed9d69c65ebad2c479028701d34b7d506a8fa85c2bfe978aeec02d41d18f79c51c92b4fe34e185986728a70bcf30283b42b0a3e784a1131aea4bcabb2c72a6836c2b9e246352bbd844a8da5c171761259f5fa4920b24ac6eb178e545e7b885c52e6dfd2447f0ead19b30f1df82e98428c7f5b80ae1477ea8e6138a66bf92accb7e9c8f4f14e859b34dc0734ac3fbe4ba8917c993e41cd741654770995fae2ebceb1136060e445a07fe15ecb0537e3d5fcc0f87d3bce7c8925c08b058733efbcd2e87ef65113aab807c87c7b42e18d85eae40279959b3169c8a7ff3ca1da8468347240e3b5774635e8fad689e81269ecc2e2f8106f6fc06d6c238cbb3ff103670486f9b58634c0b9185e88a4f7c6f042256a68f8041f6d27a5d8d8642448a401c96dec384b64660da98f6933d9231cff28b

2022/8/25
articleCard.readMore

Maya 顶点色单通道笔刷

前言   上次我们讨论了怎么在 Maya 实现各种笔刷的姿势 Maya CurveBrush 笔刷开发   趁着最近比较有空,我又捡起了之前想要开发顶点颜色单通道笔刷,   仓库早在 1 年前就创建了,但是并没有好好开发出来。 https://github.com/FXTD-ODYSSEY/Maya-VertexColorPainter   关于单通道顶点色笔刷,其实是之前项目组给我提的需求,Maya 官方提供的 Paint Vertex Color Tool 挺好的   就是绘制的时候顶点色是混合在一起的。无法实现分通道绘制。   网上也可以找到有不少帖子抱怨 Maya 竟然没有实现这个功能的。 https://polycount.com/discussion/191918/single-channel-vertex-painting-in-maya-2018 https://www.reddit.com/r/Maya/comments/87znt2/paint_on_separate_channels_in_vertex_painting/   我当时做了一些研究,后来因为太忙了,就将需求转交给其他同事负责了。   那个同事解决了需求,只是解决方案比较复杂,需要用 OpenMaya 写一个节点,再加自定义笔刷实现。   经过我上次笔刷的折腾,我在想能否扩展原本 Maya Paint Vertex Color Tool 的功能   上面就是我最终实现的效果,在 Maya 的原生 UI 上进行修改,提供了额外的 UI 配置来进行单通道绘制。 笔刷选型   Maya CurveBrush 笔刷开发 我这篇文章已经覆盖了写笔刷的各种姿势。   用 Maya 开放的 MPxContext 写笔刷实最为自由的,但是很多功能都没有。   使用 Maya 内置的 artisan 笔刷,则已经实现了好多功能。 自带镜像 笔刷可以自定义笔刷图章实现渐变 内置序列化功能 artisan painting 扩展官方文档   所以如果不是复杂的笔刷,能用 artisan 就用 artisan 去实现。   只可惜 Maya 没有暴露 artisan 笔刷的 C++ 接口,所以如果用 C++ 开发就只能重新实现一遍 artisan 的功能,比较麻烦。   当然绘制顶点色我直接使用 artAttrPaintVertexCtx 即可。 实现原理 单通道 color set 拆分   利用 Maya 提供的 ColorSet 功能,将模型的主顶点色分拆成四个通道的 ColorSet ,   我这里就分别命名为 VertexColorR VertexColorG VertexColorB VertexColorA   绘制的时候根据选择 UI 的选择激活相应的 ColorSet 。   这一步可以用 artAttrPaintVertexCtx 的 toolOnProc 和 toolOffProc 定义激活和关闭的回调。   激活 context 的时候创建 ColorSet 拆分,退出 Context 的时候删除冗余的 ColorSet   这个地方的 toolOnProc toolOffProc 同样只接受 mel 函数,用 Python 解决的方案参考 Maya CurveBrush 笔刷开发 这篇文章。 颜色分解   那么上面拆分 ColorSet 的时候就需要将对应的 MainColorSet 的颜色按通道赋值给对应单通道的 ColorSet. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 import pyeml.core as pm from maya import OpenMaya PAINT_CTX = "artAttrColorPerVertexContext" color_set_representation = { "R": "RGB", "G": "RGB", "B": "RGB", "A": "A", } def get_color_sets(node): color_sets = pm.polyColorSet(node, q=1, allColorSets=1) return color_sets or pm.polyColorSet(node, create=1) def filter_color(color, index, source_color=None): if index > 3: return color is_color = isinstance(source_color, OpenMaya.MColor) color_list = list(source_color) if is_color else [0, 0, 0, 1] color_list[index] = color[index] return OpenMaya.MColor(*color_list) # NOTES(timmyliang): 获取当前正在绘制的节点 for node in set(pm.artAttrPaintVertexCtx(PAINT_CTX, q=1, pna=1).split()): node = pm.PyNode(node) node.displayColors.set(1) color_sets = get_color_sets(node) main_color_set = color_sets[0] mesh = node.__apimfn__() # NOTES(timmyliang): 获取主 color set 顶点色 color_array = OpenMaya.MColorArray() mesh.getVertexColors(color_array, main_color_set) # NOTES(timmyliang): 获取顶点序号数组 vtx_array = OpenMaya.MIntArray() for array_index in range(color_array.length()): vtx_array.append(array_index) final_colors = OpenMaya.MColorArray() for channel_index, color_channel in enumerate(cls.CHANNELS): # NOTES(timmyliang): 如果通道 color set 不存在则创建 color_set = "VertexColor{0}".format(color_channel) if color_set not in color_sets: rpt = color_set_representation.get(color_channel) pm.polyColorSet(node, create=1, rpt=rpt, colorSet=color_set) mesh.setCurrentColorSetName(color_set) final_colors.clear() for array_index in range(color_array.length()): full_color = color_array[array_index] color = filter_color(full_color, index=channel_index) final_colors.append(color) # NOTES(timmyliang): 批量设置顶点色 mesh.setVertexColors(final_colors, vtx_array) mesh.setCurrentColorSetName(main_color_set)   这里利用 pymel 提供的 __apimfn__ 直接获取 MFnMesh 对象   利用 setVertexColors API 批量设置顶点色,性能比起单点设置要好很多。 单通道 单颜色 绘制   下一步就是要实现绘制将颜色锁在对应通道上。   比如我在 UI 上设置为值绘制 R 通道的状态,绘制选择的颜色是 白色 [255,255,255],点击 Viewport 的时候会将颜色过滤成 红色 [255,0,0] ,这样勾选 R 的时候就只会刷出 红色 没有其他颜色。   这里可以监听 Viewport 的 press 和 release 触发,当点击 viewport 的时候根据 UI 勾选的通道过滤 Ctx 颜色配置。 1 2 3 4 5 6 7 8 9 10 # NOTES(timmyliang): 获取 UI 的颜色和透明值 rgb = pm.colorSliderGrp("colorPerVertexColor", q=1, rgb=1) alpha = pm.floatSliderGrp("colorPerVertexAlpha", q=1, value=1) rgb.append(alpha) # NOTES(timmyliang): 组装颜色,过滤掉相应的通道。 # 获取 ui 的选项 sel = pm.radioButtonGrp(SINGLE_CONTROL, q=1, sl=1) # 过滤颜色 color = filter_color(rgb, index=sel) pm.artAttrPaintVertexCtx(PAINT_CTX, e=1, cl4=tuple(color))   release 的时候恢复之前的 顶点色 颜色配置。 release 通道颜色同步   最后还需要实现将绘制完的通道同步到其他的 color set 上的功能。   因此 release 触发的时候要判断当前绘制的模式,如果绘制 rgb 就将颜色分解到对应的单通道上。   相反如果是单通道绘制就要将颜色反馈到 rgb 的主 color set 上。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 def apply_color_channel(cls): index = pm.radioButtonGrp(cls.SINGLE_CONTROL, q=1, sl=1) mode = cls.OPTION_ITEMS[index + 1] is_rgb = mode == "RGB" for node in cls.get_paint_nodes(): dag_path = node.__apimdagpath__() mesh = OpenMaya.MFnMesh(dag_path) color_sets = cls.get_color_sets(node) main_color_set = color_sets[0] current_color_set = mesh.currentColorSetName() main_colors = OpenMaya.MColorArray() mesh.getVertexColors(main_colors, main_color_set) vtx_array = cls.vertex_color_data[node.fullPathName()] final_colors = OpenMaya.MColorArray() # NOTES(timmyliang): 如果当前绘制为非单通道 if is_rgb: # NOTES(timmyliang): 将当前的主颜色 拆分到各个通道上 for channel_index, color_channel in enumerate(cls.CHANNELS): final_colors.clear() color_set = "VertexColor{0}".format(color_channel) mesh.setCurrentColorSetName(color_set) for vtx_index in vtx_array: main_color = main_colors[vtx_index] color = cls.filter_color(main_color, channel_index) final_colors.append(color) mesh.setVertexColors(final_colors, vtx_array) else: mode_index = cls.OPTION_ITEMS.index(mode) - 2 channel_colors = OpenMaya.MColorArray() fix_colors = OpenMaya.MColorArray() color_set = "VertexColor{0}".format(mode) mesh.getVertexColors(channel_colors, color_set) # NOTES(timmyliang): 获取单通道的颜色 回馈到主颜色上 for vtx_index in vtx_array: channel_color = channel_colors[vtx_index] main_color = main_colors[vtx_index] color = cls.filter_color(channel_color, mode_index, main_color) final_colors.append(color) fix_color = cls.filter_color(channel_color, mode_index) fix_colors.append(fix_color) mesh.setVertexColors(fix_colors, vtx_array) mesh.setCurrentColorSetName(main_color_set) mesh.setVertexColors(final_colors, vtx_array) mesh.setCurrentColorSetName(current_color_set)   通过上面的方式就可以每次绘制完之后同步顶点色到对应的 color set 上。 Maya UI 修改 & 扩展   Maya 有个非常好的设计是 UI 使用过 mel 脚本组装的,这样不需要编译就可以改动 UI,而且这部分的 mel 脚本都是开源的。   可以很清楚地知道 Maya 是如何组装出相应工具的界面。 C:\Program Files\Autodesk\Maya2018\scripts\others\artAttrColorPerVertexProperties.mel   Maya 的颜色笔刷是通过上面路径的 mel 脚本实现的。   这样可以找到对应 UI 的名字   如上图所示,可以找到 artAttrColorChannelChoices 的名字。   然后用 cmds 命令可以对这些 UI 进行二次修改。 1 2 from maya import cmds cmds.radioButtonGrp('artAttrColorChannelChoices',e=1,gbc=[255,0,0])   比如执行上面的代码可以修改相应 UI 的背景颜色。   上面已经展示了如何修改原生的 UI   这些操作需要学习 Mel 的 UI 构建方式,会有点复杂。   不过 Mel 的 example 都有案例,比如这里的UI 使用了 columnLayout   那我可以去到 columnLayout 的文档运行案例代码进行学习。   将代码放到代码编辑器执行。   查了一下 columnLayout 的 API ,发现它竟然没有 insert 功能。   于是我找了一下 Mel Tips大全的网站 MEL How-To (上古网站,但对学习Mel很有帮助)   可以找到一个 链接 如何实现UI的置顶插入。   方案一使用 frameLayout 比较繁琐   方案二则是使用一个新的 Layout 然后将旧 Layout 的 UI 删除掉。   这个方法删除 UI 对我想要的效果并不适用。   不过倒是启发了我,我想到了可以利用 childArray 可以拿到 Layout 下所有的 Control 名字。   然后对每个 Control 修改 parent 到新的 Layout 上。 使用 cmds 嵌入 UI 1 2 3 4 5 6 7 8 9 10 from maya import cmds parent = cmds.radioButtonGrp('artAttrColorChannelChoices',q=1,parent=1) print(parent) # ToolSettings|MainToolSettingsLayout|tabLayout1|artAttrColorPerVertex|artCommonOperationFrame|columnLayout1061|columnLayout1065 window = cmds.window() column_layout = cmds.columnLayout() for control in cmds.layout(parent,q=1,childArray=1): cmds.control(control, e=1, p=column_layout) cmds.showWindow(window)   上面的想法写成代码如上所示   直接实现了 UI 的乾坤大挪移   只是显示上有些不一样,主要原因是 mel 构建 UI 的时候使用 setUITemplate 1 2 3 4 5 6 7 8 9 10 11 from maya import cmds window = cmds.window() cmds.setUITemplate("OptionsTemplate", pushTemplate=1) column_layout = cmds.columnLayout() parent = cmds.radioButtonGrp('artAttrColorChannelChoices',q=1,parent=1) for control in cmds.layout(parent,q=1,childArray=1): cmds.control(control, e=1, p=column_layout) cmds.setUITemplate(popTemplate=1) cmds.showWindow(window)   加上了 OptionsTemplate 之后 UI 的显示就保持一致了   所以在 parent control 的过程中加入自己的 UI ,就可以实现对应位置的嵌入效果。 1 2 3 4 5 6 7 8 9 10 11 12 13 from maya import cmds window = cmds.window() cmds.setUITemplate("OptionsTemplate", pushTemplate=1) column_layout = cmds.columnLayout() parent = cmds.radioButtonGrp('artAttrColorChannelChoices',q=1,parent=1) for control in cmds.layout(parent,q=1,childArray=1): cmds.control(control, e=1, p=column_layout) if control == "artAttrColorChannelChoices": cmds.button(label="click me") cmds.setUITemplate(popTemplate=1) cmds.showWindow(window)   比如上面的效果,如此就可以在相应的位置嵌入任意的 UI   最后是怎么将 UI 嵌入到原本的位置,关键就是使用 setParent 命令 1 2 3 4 5 6 7 8 9 10 11 12 from maya import cmds parent = cmds.radioButtonGrp('artAttrColorChannelChoices',q=1,parent=1) cmds.setParent(parent) cmds.setUITemplate("OptionsTemplate", pushTemplate=1) column_layout = cmds.columnLayout() for control in cmds.layout(parent,q=1,childArray=1): cmds.control(control, e=1, p=column_layout) if control == "artAttrColorChannelChoices": cmds.button(label="click me") cmds.setUITemplate(popTemplate=1)   如此就可以了, setParent 会将当前 UI 创建设置到之前的 Layout 下。 使用 Qt 嵌入 UI   既然 cmds 可以实现 UI 嵌入,那能否利用 Qt API 来实现这个效果呢?   我也想过将 Layout 转成 Qt Object 的方式进行调用。   但这个方式获取到的是 QLayout 无法使用 insertWidget 插入,倒是可以使用 addWidget 1 2 3 4 5 6 7 8 9 10 import pymel.core as pm from Qt import QtWidgets widget = pm.uitypes.toQtObject("artAttrColorChannelChoices") parent = widget.parent() print(parent.objectName()) # columnLayout1065 objectName 和 mel 的 controlName 是一样的 layout = parent.layout() print(layout) # <PySide2.QtWidgets.QLayout object at 0x0000014DB0917648> layout.addWidget(QtWidgets.QPushButton("asd"))   利用上面的方式就可以在 Layout 的最末端添加一个按钮。   上面使用了 toQtObject 的 pymel API   背后调用 OpenMayaUI 库通过 objectName 查找到对应的 Qt 组件,然后 wrapInstance 将组件转换为 QObject 类型。   用 pymel 的方式比较方便。   要实现 insert 的效果可以利用 takeAt API 将 widget 提取出来再放回去。 1 2 3 4 5 6 7 8 9 10 import pymel.core as pm from Qt import QtWidgets widget = pm.uitypes.toQtObject("artAttrColorChannelChoices") parent = widget.parent() layout = parent.layout() index = layout.indexOf(widget) widget_list = [layout.takeAt(0).widget() for _ in range(layout.count())] widget_list.insert(index,QtWidgets.QPushButton("click me")) for widget in widget_list: layout.addWidget(widget)   如上所示,也完全实现了 cmds 库一样的效果,使用 Qt API 就比 cmds 要灵活很多。   可以嵌入 Designer 生成的 Widget 等等。   这里只是展望了一下,我的实现还是基于 cmds 的方式。 总结   我的工具已经做成了 Maya 插件,启用按照插件的方式加载即可。

2022/8/16
articleCard.readMore

Maya CurveBrush 笔刷开发

前言   最近因为比较特殊的原因,工作上突然闲下来了,于是我就去研究了我们组做的 Maya 毛发工具。   学习一下 Maya 做笔刷有哪些坑。   这次我的主要目的是模仿 XGen 的毛发笔刷效果,通过最小案例的实现,探讨不同的实现方案。 官方文档: XGen Interactive Grooming   上面的视频就是 XGen 实现的笔刷效果,对于毛发制作非常丝滑好用。   只可惜这个笔刷不能对曲线直接生效。   上面是我用 C++ 写的曲线笔刷,下面我也会来探讨如何用 Python OpenMaya 结合 Qt 开发笔刷的流程。   具体代码已经开源到 https://github.com/FXTD-ODYSSEY/Maya-CurveBrush   C++ 插件提供了 2020 - 2023 支持   Python 插件有 om1_curve_brush.py 和 om2_curve_brush.py Maya C++ 笔刷开发流程 Maya C++ MPxContext   什么是 Maya Context ? 官方文档说明   Maya Context 就是一个开放的接口,可以用于自定义 鼠标 在 Viewport 上执行的逻辑,实现 绘制 修改选择物体 等操作。 MPxContext 官方文档   上面的链接是一个 Maya Devkit 里面的案例 devkit\plug-ins\marqueeTool\marqueeTool.cpp   Maya CMake 构建 C++ 插件编译环境 我的这篇文章有提到如何将 devkit 的源码编译生成 mll maya2020 - marqueeTool.mll   这里提供 Maya2020 windows 版本的 mll 插件   Maya 加载 mll 插件 1 2 3 import maya.cmds as cmds ctx = cmds.marqueeToolContext() cmds.setToolTo(ctx)   加载mll 插件后,可以使用上面的代码激活 Context   上面实现的效果和默认的 框选物体是一样的。   只是框的颜色变成了自定义的 黄色。   实现这个 context 需要继承实现两个类,一个是 MPxContext 另一个是 MPxContextCommand   MPxContext 类定义了鼠标拖拽 移动 等逻辑的虚函数,MPxContextCommand 则是用来读取 MPxContext 数据的 Mel 命令。   通过 MPxContextCommand 就可以用 Mel 命令来修改 MPxContext 的变量(比如笔刷大小之类的) MPxContextCommand 官方文档 Maya C++ MPxToolCommand MPxToolCommand 官方文档   上面提到的方案 Context 进行处理的时候是没有 Undo 功能的。   因此 Maya C++ 提供了 MPxToolCommand 这样将需要 undo 的逻辑放到 Command 当中实现,就可以 undo redo 操作了。   上面文档的案例来自于 devkit\plug-ins\helixTool\helixTool.cpp maya2020 - helixTool.mll   这里照样提供 Maya2020 windows 版本的 mll 插件   Maya 加载 mll 插件 1 2 3 import maya.cmds as cmds ctx = cmds.helixToolContext() cmds.setToolTo(ctx)   加载mll 插件后,可以使用上面的代码激活 Context   需要注意这个插件只能在旧的 Viewport 生效 (我测了好久才明白过来)   这个工具可以在 Maya Viewport 拖拽一个 圆柱预览 ,这个圆柱最后生成 螺旋线。   通过 MPxToolCommand 的方式就可以让生成的 螺旋线 支持undo。   为何这个工具不能在 viewport2.0 下使用   从上面的 API 列表可以看到 doDrag doPress 好几个 API 都有两个实现。   一个是只传入 event 的,这个方法只在 老 Viewport 下调用。   Viewport2.0 调用的是传入 MUIDrawManager 的方法。   helixTool 没有实现 MUIDrawManager 的方法,所以 Viewport2.0 下不起作用。 Tool Contexts 官方文档   官方文档被打散到 Viewport2.0 的目录下了,具体的说法可以参照上面 笔刷工具的 UI Tool property sheets 官方文档 <>Properties.mel 实现左侧的可修改界面 <>Values.mel 获取笔刷数值 (更新到界面上)   Context 激活之后,双击可以看到工具界面   这个界面就是遵循上面两个 mel 的方法来实现的。   可以继续参考 helixTool 的源码目录,它提供了 helixProperties.mel 和 helixValues.mel 脚本   那么上面的命名 <> 是怎么决定的,为啥用 helixProperties 而不是 helixToolProperties   其实这是 getClassName 决定的。   mel脚本并不是重点,双击 Context 调用的是 helixProperties helixValues 两个 mel 方法,如果找不到才会找同名脚本。 用 Python 生成 Mel Proc   如果要编写自定义的 UI,一定要用 mel 才能编写吗?   能否用 Python 解决问题呢? Python function as a MEL procedure 官方文档   如果嫌弃使用 mel 确实可以参考上面的链接用 Python 创建的 Mel Proc C:\Program Files\Autodesk\Maya2020\Python\Lib\site-packages\maya\mel\melutils.py 具体的代码实现可以通过上面的路径找到。   我尝试了一下,默认 returnCmd 是 False 会打开文件窗口生成出 mel 脚本。   可以设置 returnCmd=True 这样就返回 mel 代码了。   后面可以用 mel.eval 来执行返回的代码   就是传入的Python function 如果不在 Python 模块之下会弹出警告 py2melProc 文档   pymel 库也提供了 py2mel 的方法   使用这个方法会比 Maya 内置的处理好一些   实现的原理基本一致,都是通过 Python 构建出 Mel 代码,   Mel 代码本质就是用 python 关键字执行 Python 代码 (一会 Python 一会 Mel 的似乎挺绕的(:з」∠))   pymel 还提供了 mel2pyStr 的方法可以直接将 mel 代码转成 Python 的版本。   这样就可以避免 python 和 mel 混写。 1 2 3 4 5 6 from pymel.tools import mel2py path = r"C:\Program Files\Autodesk\Maya2018\scripts\others\customtoolPaint.mel" with open(path,'r') as rf: content = rf.read() py_str = mel2py.mel2pyStr(content, pymelNamespace="pm") print(py_str)   比如上面就可以将一些内置的 mel 案例转换成 python 版本。   pymelNamespace 可以给所有的调用加上相应的前缀。   利用上面的方法就可以将 helixTool 的 mel 脚本转为 Python 实现   转换完成之后需要注意 function 调用,要将 pm.mel 去掉   因为之前 proc 编程 Python function 用 pm.mel.helixSetCallbacks 是调用不了的。   另外一些变量名 mel 里面可能命名为了 set ,如果这些是 Python 的关键字或者内置命名需要注意。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 import pymel.core as pm from pymel.tools import py2mel def helixProperties(): pm.setUITemplate("DefaultTemplate", pushTemplate=1) parent = str(pm.toolPropertyWindow(q=1, location=1)) pm.setParent(parent) pm.columnLayout("helix") pm.tabLayout("helixTabs", childResizable=True) pm.columnLayout("helixTab") pm.frameLayout("helixFrame", cll=True, l="Helix Options", cl=False) pm.columnLayout("helixOptions") pm.separator(style="none") pm.intSliderGrp( "numCVs", field=1, minValue=20, maxValue=100, value=1, label="Number of CVs" ) pm.checkBoxGrp("upsideDownGrp", numberOfCheckBoxes=1, l1=" ", label="Upside Down") pm.setParent("..") # helixOptions pm.setParent("..") # helixFrame pm.setParent("..") # helixTab pm.setParent("..") # helixTabs pm.setParent("..") # helix # Name the tabs; -tl does not allow tab labelling upon creation pm.tabLayout("helixTabs", tl=("helixTab", "Tool Defaults"), e=1) pm.setUITemplate(popTemplate=1) helixSetCallbacks(parent) def helixSetCallbacks(parent): pm.setParent(parent) pm.checkBoxGrp( "upsideDownGrp", e=1, on1=lambda *args: pm.helixToolContext(pm.currentCtx(), upsideDown=True, e=1), of1=lambda *args: pm.helixToolContext(pm.currentCtx(), upsideDown=False, e=1), ) pm.intSliderGrp( "numCVs", e=1, cc=lambda *args: pm.helixToolContext(pm.currentCtx(), numCVs=args[0], e=1), ) def helixValues(toolName): parent=(str(pm.toolPropertyWindow(q=1, location=1)) + "|helix|helixTabs|helixTab") pm.setParent(parent) icon="helixTool.xpm" help="" pm.mel.toolPropertySetCommon(toolName, icon, help) pm.frameLayout('helixFrame', en=True, e=1, cl=False) helixOptionValues(toolName) pm.mel.toolPropertySelect('helix') def helixOptionValues(toolName): cv_num = 0 cv_num=int(pm.mel.eval("helixToolContext -q -numCVs " + toolName)) pm.intSliderGrp('numCVs', e=1, value=cv_num) cv_num=int(pm.mel.eval("helixToolContext -q -upsideDown " + toolName)) if cv_num: pm.checkBoxGrp('upsideDownGrp', e=1, value1=1) else: pm.checkBoxGrp('upsideDownGrp', e=1, value1=0) py2mel.py2melProc(helixProperties, procName="helixProperties") py2mel.py2melProc(helixValues, procName="helixValues")   经过一些修改之后,可以实现用 Python 的方式来编写 Mel Proc。   只是还是需要熟悉一下 mel UI 构建的语法。 Maya C++ CurveBrush   通过上面一番探讨之后,我们理清楚了做一个笔刷需要什么。   所以我在 C++ 代码层面拆分三个头文件,分别对应 MPxContext MPxContextCommand MPxContextToolCommand 的实现。   如何开发也可以参考 helixTool 的代码。   注册插件的时候需要同时注册 MPxContextCommand 和 MPxContextToolCommand   这样 Maya 就知道这两个命令是关联在一起的, MPxContext 里面调用 newToolCommand 方法就可以获取到 MPxContextToolCommand 笔刷属性调整   我先要让笔刷按住 B 键的时候可以实现 大小 调整。   默认 Maya API 没有提供键盘事件的监听。   于是查找官方的案例,找到了 devkit\plug-ins\grabUVMain.cpp maya2020 - grabUV.mll   这里提供 Maya2020 windows 版本的 mll 插件   Maya 加载 mll 插件 1 2 3 import maya.cmds as cmds ctx = cmds.grabUVContext() cmds.setToolTo(ctx)   这个插件可以按住 B 键调整笔刷的大小。   原理是利用 Qt 的 eventFilter 监听全局的键盘响应,所以编译的 include 路径需要有 Qt 的头文件,默认的 include 路径只有 Qt 头文件压缩包,需要解压缩来索引。   所以我也是用同样的方式监听是否有按 B 键。   左键拖拽调整笔刷大小,中键拖拽调整笔刷强度。 曲线衰变颜色   笔刷覆盖的范围呈现颜色,这个是用 Viewport2.0 的 MUIDrawManager 实现的。 MUIDrawManager   MUIDrawManager 提供了 mesh 的 API 进行曲线模型等的绘制。   最重要的第一点是可以传入颜色数组,根据每个点自定义颜色,其他的 line API 无法实现这个功能 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 MStatus curveBrushContext::doPtrMoved(MEvent &event, MHWRender::MUIDrawManager &drawMgr, const MHWRender::MFrameContext &context) { short x, y; event.getPosition(x, y); mBrushCenterScreenPoint = MPoint(x, y); auto radius = mBrushConfig.size(); drawMgr.beginDrawable(); if (bFalloffMode) { for (unsigned int index = 0; index < objDagPathArray.length(); ++index) { MPointArray pointArray; MColorArray colorArray; MFnNurbsCurve curveFn(objDagPathArray[index]); unsigned int segmentCount = 100; for (unsigned int pointIndex = 0; pointIndex < segmentCount; ++pointIndex) { MPoint point; auto param = curveFn.findParamFromLength(curveFn.length() * pointIndex / segmentCount); curveFn.getPointAtParam(param, point, MSpace::kWorld); pointArray.append(point); // NOTE(timmyliang): draw falloff short x_pos, y_pos; view.worldToView(point, x_pos, y_pos); MPoint screenPoint(x_pos, y_pos); auto distance = (mBrushCenterScreenPoint - screenPoint).length(); auto field = 1 - distance / radius; // NOTE(timmyliang): transparent colorArray.append(distance > radius ? MColor(0.f) : MColor(field, field, field)); } drawMgr.setLineWidth(12.0f); drawMgr.mesh(MHWRender::MUIDrawManager::kLineStrip, pointArray, NULL, &colorArray); } } drawMgr.setColor(MColor(1.f, 1.f, 1.f)); drawMgr.setLineWidth(2.0f); drawMgr.circle2d(mBrushCenterScreenPoint, radius); drawMgr.endDrawable(); return MS::kSuccess; }   那么问题就变成怎么获取顶点上色了,如果曲线的顶点数量很少就很难有好的显示效果。   因此这里使用 findParamFromLength getPointAtParam 的方式重新采样曲线的顶点。   对采样的顶点再判断一下是否在笔刷的圆圈范围内,范围外的附上透明的颜色,范围内的根据距离附上黑白色。 曲线 CV 移动   首先要获取 drag 偏移的向量。   通过 doPress 方法可以获取到点击的时候的向量偏移。   再通过 doDrag 获取拖拽的时候鼠标的位置。   两个位置坐标就可以得到偏移的向量。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 MStatus curveBrushContext::doPress(MEvent &event, MHWRender::MUIDrawManager &drawMgr, const MHWRender::MFrameContext &context) { view = M3dView::active3dView(); event.getPosition(startPosX, startPosY); fStartBrushSize = mBrushConfig.size(); fStartBrushStrength = mBrushConfig.strength(); return MS::kSuccess; } MStatus curveBrushContext::doDrag(MEvent &event, MHWRender::MUIDrawManager &drawMgr, const MHWRender::MFrameContext &context) { view.refresh(false, true); short currentPosX, currentPosY; event.getPosition(currentPosX, currentPosY); auto currentPos = MPoint(currentPosX, currentPosY); MPoint start(startPosX, startPosY); MVector delta = MVector(currentPos - start); drawMgr.beginDrawable(); drawMgr.setColor(MColor(1.f, 1.f, 1.f)); drawMgr.setLineWidth(2.0f); // NOTE(timmyliang): hold down `B` key if (eDragMode == kBrushSize) { float deltaValue; char info[64]; // NOTES(timmyliang): left mouse for size if (event.mouseButton() == MEvent::kLeftMouse) { deltaValue = delta.x > 0 ? delta.length() : -delta.length(); mBrushConfig.setSize(fStartBrushSize + deltaValue); sprintf(info, "Brush Size: %.2f", mBrushConfig.size()); drawMgr.text2d(currentPos, info); } // NOTES(timmyliang): middle mouse for strength else if (event.mouseButton() == MEvent::kMiddleMouse) { deltaValue = delta.y > 0 ? delta.length() : -delta.length(); mBrushConfig.setStrength(fStartBrushStrength + deltaValue); sprintf(info, "Brush Strength: %.2f", mBrushConfig.strength()); drawMgr.text2d(currentPos, info); } drawMgr.line2d(start, MPoint(startPosX, startPosY + mBrushConfig.strength() * 2)); } else { MPoint startNearPos, startFarPos, currNearPos, currFarPos; view.viewToWorld(currentPosX, currentPosY, currNearPos, currFarPos); view.viewToWorld(startPosX, startPosY, startFarPos, startFarPos); // NOTE(timmyliang): use tool command for undo curveBrushTool *cmd = (curveBrushTool *)newToolCommand(); cmd->setStrength(mBrushConfig.strength()); cmd->setRadius(mBrushConfig.size()); cmd->setMoveVector((currFarPos - startFarPos).normal()); cmd->setStartPoint(start); cmd->setDagPathArray(objDagPathArray); cmd->redoIt(); cmd->finalize(); } drawMgr.circle2d(start, mBrushConfig.size()); drawMgr.endDrawable(); return MS::kSuccess; }   doDrag 还会判断是否按住 B 键,按住的话就调整笔刷的大小。   反之则调用 newToolCommand 执行 CV 移动的逻辑   ToolCommand 会获取曲线上 CV 点的位置,将空间坐标转为屏幕坐标。   这样可以判断这些 CV 点是否在笔刷范围内。   如果在范围的 CV 点根据笔刷提供的方向进行偏移。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 MStatus curveBrushTool::redoIt() { MVector offsetVector = moveVector * 0.002 * strength; M3dView view = M3dView::active3dView(); // NOTE(timmyliang): move curves cv in radius short x_pos, y_pos; for (unsigned int index = 0; index < dagPathArray.length(); ++index) { MFnNurbsCurve curveFn(dagPathArray[index]); std::map<int, MVector> offsetMap; for (MItCurveCV cvIter(dagPathArray[index]); !cvIter.isDone(); cvIter.next()) { MPoint pos = cvIter.position(MSpace::kWorld); int cvIndex = cvIter.index(); curvePointMap[index][cvIndex] = pos; view.worldToView(pos, x_pos, y_pos); if ((startPoint - MPoint(x_pos, y_pos)).length() < radius) { offsetMap[cvIndex] = pos + offsetVector; } } for (const auto &it : offsetMap) { curveFn.setCV(it.first, it.second, MSpace::kWorld); } curveFn.updateCurve(); } return MStatus::kSuccess; }   通过 MItCurveCV 遍历曲线上所有的 CV 点。   利用 setCV 方法可以实现顶点的偏移   C++ 这边我发现不能在 MItCurveCV 的遍历过程中调用 setCV ,它会导致遍历中断。   但是用 MItCurveCV 提供的 setCVPosition 无法实现位置的刷新。   最后只好将 CV序号 和 位置通过 Map 保存起来,通过 setCV API 去偏移。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 MStatus curveBrushTool::undoIt() { // NOTE(timmyliang): reset point position for (const auto &kv : curvePointMap) { MFnNurbsCurve curveFn(dagPathArray[kv.first]); for (const auto &it : kv.second) { int cvIndex = it.first; MPoint pos = it.second; curveFn.setCV(cvIndex, pos, MSpace::kWorld); } } return MStatus::kSuccess; }   通过 curvePointMap 变量保存了上一次所有 CV 点的位置,undo 只要遍历这个字典去重置 CV 位置即可。 OpenMaya 2.0 笔刷开发   既然 C++ 可以开发出如上看到的笔刷,理论上也可以通过 Python OpenMaya 库进行笔刷开发。   但是我发现 OpenMaya 1.0 不支持 Viewport 2.0 的 API,比如上面关键的 MUIDrawManager   在 OpenMaya1.0 下是不不存在的。 1 2 3 4 5 from maya import OpenMayaRender OpenMayaRender.MUIDrawManager # Error: AttributeError: file <maya console> line 2: 'module' object has no attribute 'MUIDrawManager' # from maya.api import OpenMayaRender OpenMayaRender.MUIDrawManager   可以看到 OpenMaya 2.0 才有 MUIDrawManager https://matiascodesal.com/blog/maya-python-api-20-it-ready-yet/   以前 18 年的时候还看到有人了文章介绍 OpenMaya 2.0 到底是否可以已经完善了。   OpenMaya 2.0 与 OpenMaya 1.0 相比还缺了挺多的 C++ 类的。   而且 OpenMaya2.0 的案例都有一些代码错误,比如 plug-ins\python\api2\py2LassoTool.py (已经是 2023 的最新版本了)   这实在是令人失望,脚本的第 224 行有明显 true 使用不当,并且 MItCurveCV 这个类 OpenMaya 2.0 不支持的。   我启用这个脚本框选 CV 点直接给我报错(:з」∠)   也因为 OpenMaya 2.0 各种不完善, 👨‍💻mottosso 大佬才会做自己的 Pyd wrapper 封装 C++ API cmdc ,只是目前的进度还需要更多人加入支持开发。   那是用 OpenMaya 2.0 能否完成我上面的 C++ 曲线笔刷的复刻呢?   我查了一下,发现 Maya 2020 之后添加了 MPxToolCommand 命令,似乎可以实现和 C++ 一样的 undo 命令。   然而我的实测却让我非常失望。 https://github.com/FXTD-ODYSSEY/Maya-CurveBrush/blob/main/plug-ins/om2_curve_brush.py MPxContextCommand 缺失 syntax parser 方法   基于 OpenMaya 2.0 版本的插件我已经写完了,只是被它的不完整气得不轻。   首先 MPxContextCommand 缺失了 syntax parser 方法   即便提供了 doQueryFlags doEditFlags 的 API 但是没法和 C++ 一样进行调用,但是 OpenMaya 1.0 提供了 _syntax _parser 方法给 Python 调用。 registerContextCommand 不支持 MPxToolCommand 注册 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 def initializePlugin(plugin): pluginFn = om.MFnPlugin(plugin) try: pluginFn.registerContextCommand(CONTEXT_NAME, CurveBrushContextCmd.creator) # TODO(timmyliang): not support MPxToolCommand registered # pluginFn.registerContextCommand( # CONTEXT_NAME, # CurveBrushContextCmd.creator, # CONTEXT_TOOL_NAME, # CurveBrushTool.creator, # CurveBrushTool.newSyntax, # ) except: sys.stderr.write("Failed to register command: %s\n" % CONTEXT_NAME) raise   OpenMaya 2.0 终于在 Maya 2020 提供了 MPxToolCommand 的接口。   但是 MPxToolCommand 需要通过 registerContextCommand 来注册进去。   但是它目前不支持 5 个参数的调用,导致 MPxToolCommand 无法注册。 1 # Error: TypeError: file F:/repo/CMakeMaya/modules/Maya-CurveBrush/plug-ins/om2_curve_brush.py line 448: function takes exactly 2 arguments (5 given) #   注册的时候会提示 registerContextCommand 只接受两个参数。 MPxToolCommand doFinalize 无法传入参数 1 2 3 4 5 6 7 8 9 10 class CurveBrushTool(omui.MPxToolCommand): def finalize(self): command = om.MArgList() command.addArg(self.commandString) for flag, config in self.flags_data.items(): long_flag = config.get("long") command.addArg(flag) command.addArg(getattr(self, long_flag[1:])) # TODO(timmyliang): not accept the command argument # return self.doFinalize(command)   虽然 registerContextCommand 无法注册 MPxToolCommand 导致 newToolCommand 没有正常的返回。   但我可以单独实例化 MPxToolCommand 从而实现 undo   可是还是不行,而且这个坑爹的情况明显是官方的问题。   doFinalize 明明可以接受一个 MArgList 类型的参数,但是这个 Python 函数却不接受任何参数(:з」∠) OpenMaya 2.0 展示   虽然 2.0 有上述的诸多问题,笔刷的基础功能还是可以实现的。   只是 undo 功能解决不了,倒是可以将曲线的 tweak 操作转移到另一个 Command 上从而实现 undo 的。   不过我这里就点到为止,主要踩了 OpenMaya 2.0 的坑,对它好感度降低了不少(:з」∠) Python Qt Overlay 实现自定义绘制   上面提到了 OpenMaya 1.0 缺失了 MUIDrawManager 所以无法在 Viewport 2.0 下进行图像绘制。   C++ 文档也注明了带 MUIDrawManager 是无法在 Python 下使用的。   我也测试了不传入 MUIDrawManager 的几个方法,他们只能在 Legacy Viewport 下响应触发。   那还有什么方法不用 C++ 也可以实现 Python 的绘制呢?   这就可以参考一个非常棒的 Maya Python 工具 spore   spore 也实现了自己的笔刷工具,并且对低版本 Maya 兼容。   它的做法不是通过 Maya API 实现,而是利用 Qt 的 API 进行绘制。   首先对 Maya 的 Viewport 叠加一层透明的 QWidget 层,通过 paintEvent 的实现,绘制自定义图形叠加到 Viewport 上。   实现效果如上图,基本和 Maya API 的绘制效果很接近。 Overlay 组件实现   组件叠加的方案我之前的文章也有过 Unreal Python 路径定位启动器   核心思路就是取消 Widget 的边框,忽略输入影响,透明化背景并且永远保持在最前面。 1 2 3 4 5 6 7 8 9 10 11 12 class CanvasOverlay(QtWidgets.QWidget): def __init__(self, context): # type: (CurveBrushContext) -> None super(CanvasOverlay, self).__init__() self.setWindowFlags( QtCore.Qt.FramelessWindowHint | QtCore.Qt.SplashScreen | QtCore.Qt.WindowStaysOnTopHint | QtCore.Qt.WindowTransparentForInput ) self.setAttribute(QtCore.Qt.WA_TranslucentBackground) self.setAttribute(QtCore.Qt.WA_NoSystemBackground)   这样就是一个无边框透明的窗口,如果不加上颜色用户是无感知的。 spore 参考 多个 Viewport 叠加支持 注: 这里的 Overlay 加上了大色块方便观察。 >   我添加了多个 Viewport 的 Overlay 支持,spore 默认是只对笔刷激活时的 Viewport 进行 Overlay 操作。 >   如果切换到多视图或者单独的 Viewport 窗口就会让 Overlay 显示不正常。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class AppFilter(QtCore.QObject): def __init__(self, canvas): # type: (CurveBrushContext) -> None super(AppFilter, self).__init__() self.canvas = canvas def eventFilter(self, receiver, event): if event.type() == QtCore.QEvent.MouseButtonPress: widget = QtWidgets.QApplication.widgetAt(QtGui.QCursor.pos()) panel = isinstance(widget, QtCore.QObject) and widget.parent() name = panel and panel.objectName() if name: is_model_editor = cmds.objectTypeUI(name, i="modelEditor") self.canvas.setVisible(is_model_editor) if is_model_editor: QtCore.QTimer.singleShot(0, self.canvas.setup_active_viewport) return super(AppFilter, self).eventFilter(receiver, event) class CurveBrushContext(OpenMayaMPx.MPxContext): # 省略 ... def toolOnSetup(self): self.canvas = CanvasOverlay(self) # NOTES(timmyliang): 获取 QApplication 进行监听 app = QtWidgets.QApplication.instance() app_filter = AppFilter(self.canvas) app.installEventFilter(app_filter) >   我这里的做法是利用 toolOnSetup API ,激活笔刷的时候监听 Maya QApplication 全局的点击事件 >   如果点击的 Widget 是 modelEditor 就将 overlay 同步过去。 >   Qt 的 objectName 就是 Maya 的 UI control Name ,所以从 objectName() 获取的 API 可以直接用 objectTypeUI 判断类型 >   利用这个方法任何 Viewport 点击都可以直接 resize Overlay 上去。 >   本来不想搞得那么复杂的,但是 Maya 原生的监听方案不起作用 stackoverflow >   stackoverflow 的回答是使用 timer 定时触发,也不是很理想,所以还是借助 Qt API 监听鼠标按键的方法最好。 ### 监听 Viewport 事件 >   正如上面所说的 doDrag doPress 等一系列 API 在 Viewport 2.0 下是失效的。 >   通过 Qt 的 installEventFilter 可以实现对 Viewport 的事件监听。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import shiboken2 import maya.OpenMaya as om import maya.OpenMayaUI as omui from PySide2.QtWidgets import QWidget from PySide2.QtCore import QObject def active_view(): """ return the active 3d view """ return omui.M3dView.active3dView() def active_view_wdg(): """ return the active 3d view wrapped in a QWidget """ view = active_view() active_view_widget = shiboken2.wrapInstance(long(view.widget()), QWidget) return active_view_widget spore 参考 >   通过 OpenMaya 1.0 的 API 可以获取当前激活的 Viewport QWidget >   拦截这个 Viewport QWidget 的事件可以实现鼠标点击拖拽等等的响应。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 class MouseFilter(QtCore.QObject): wheel = QtCore.Signal(QtCore.QEvent) moved = QtCore.Signal(QtCore.QEvent) clicked = QtCore.Signal(QtCore.QEvent) dragged = QtCore.Signal(QtCore.QEvent) released = QtCore.Signal(QtCore.QEvent) entered = QtCore.Signal() leaved = QtCore.Signal() def __init__(self, *args, **kwargs): super(MouseFilter, self).__init__(*args, **kwargs) self.is_clicked = False def eventFilter(self, receiver, event): event_type = event.type() if event_type == QtCore.QEvent.MouseMove: self.moved.emit(event) if self.is_clicked: self.dragged.emit(event) elif ( event_type == QtCore.QEvent.MouseButtonPress or event_type == QtCore.QEvent.MouseButtonDblClick ): self.is_clicked = True self.clicked.emit(event) elif event_type == QtCore.QEvent.MouseButtonRelease: self.is_clicked = False self.released.emit(event) elif event_type == QtCore.QEvent.Enter: self.entered.emit() elif event_type == QtCore.QEvent.Leave: self.leaved.emit() elif event_type == QtCore.QEvent.Wheel: self.wheel.emit(event) return super(MouseFilter, self).eventFilter(receiver, event) viewport = active_view_wdg() mouse_filter = MouseFilter() viewport.installEventFilter(mouse_filter) >   通过上面的方式就可以拦截 viewport 的 event 通过 MouseFilter 的信号槽做相应的触发。 ### 绘制实现 >   参考上图可以看到,Qt API 基本上和 Maya API 绘制的效果差不多。 >   Maya API 的 MUIDrawManager 提供了 mesh API 来绘制复杂图形。 >   Qt API 并没有类似的方法,不过 Qt 也有 QGradient >   通过 QLinearGradient 可以实现上面的效果。 >   同样地需要对曲线进行二次采样,提高分段数。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 def paintEvent(self, event): self.draw_shape(self.create_brush_cricle(), QtCore.Qt.white, 2) if self.is_press_B: self.draw_shape(self.create_brush_line(), QtCore.Qt.white, 2) self.draw_text(self._message_info) for curve, data in self.color_data.items(): self.draw_shape(data.get("points"), data.get("colors"), 10) return super(CanvasOverlay, self).paintEvent(event) def create_brush_cricle(self, count=60): shape = [] radius = self.radius pt = self.start_pos if self.is_press_B else self.current_pos for index in range(count + 1): theta = math.radians(360 * index / count) pos_x = pt.x() + radius * math.cos(theta) pos_y = pt.y() + radius * math.sin(theta) shape.append(QtCore.QPointF(pos_x, pos_y)) return shape def create_brush_line(self): shape = [] start_pt = self.start_pos if self.is_press_B else self.current_pos shape.append(start_pt) shape.append(QtCore.QPoint(start_pt.x(), start_pt.y() - self.strength)) return shape def draw_shape(self, line_shapes, colors, width=1): if len(line_shapes) < 2: return colors = colors or QtCore.Qt.white painter = QtGui.QPainter(self) painter.setRenderHint(painter.Antialiasing) painter.begin(self) if ( isinstance(colors, Iterable) and not isinstance(colors, six.string_types) and len(colors) == len(line_shapes) ): # NOTES(timmyliang): paint falloff for index, point in enumerate(line_shapes[:-1]): start_point = point end_point = line_shapes[index + 1] grandient_color = QtGui.QLinearGradient(start_point, end_point) start_color = colors[index] end_color = colors[index + 1] grandient_color.setColorAt(0, start_color) grandient_color.setColorAt(1, end_color) pen = QtGui.QPen(grandient_color, width) pen.setCapStyle(QtCore.Qt.RoundCap) pen.setJoinStyle(QtCore.Qt.RoundJoin) painter.setPen(pen) painter.drawLine(start_point, end_point) else: path = QtGui.QPainterPath() path.moveTo(line_shapes[0]) [path.lineTo(point) for point in line_shapes] color = QtGui.QColor(colors) pen = QtGui.QPen(color, width) painter.setPen(pen) painter.drawPath(path) painter.end() def draw_text(self, text, pos=None, color=QtCore.Qt.white, width=1): if not text: return painter = QtGui.QPainter(self) pen = QtGui.QPen(color, width) painter.setPen(pen) pos = pos or self.current_pos + QtCore.QPoint(10, 0) painter.drawText(pos, text) painter.end() >   上面是绘制用到的 一些 API >   核心就是 draw_shape 里面如果传入了多个 color ,获取color每个顶点画一条渐变的线 >   多条线组合成圆形,由此有了衰变颜色的圆形曲线。 >   其他的绘制比如 绘制文字,Qt 有 drawText API >   绘制圆圈可以利用 sin cos 数学函数来生成圆形的顶点进行绘制。 ### 踩坑注意 >   QtCore.QPoint 和 OpenMaya.MPoint 两者的 Y 轴坐标起始不一样,所以通过 M3dView 将世界坐标转换为屏幕坐标的时候需要额外的处理。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 def world_to_view(position, invertY=True): """ convert the given 3d position to 2d viewport coordinates """ view = OpenMayaUI.M3dView.active3dView() arg_x = OpenMaya.MScriptUtil(0) arg_y = OpenMaya.MScriptUtil(0) arg_x_ptr = arg_x.asShortPtr() arg_y_ptr = arg_y.asShortPtr() view.worldToView(position, arg_x_ptr, arg_y_ptr) x_pos = arg_x.getShort(arg_x_ptr) y_pos = arg_y.getShort(arg_y_ptr) if invertY: y_pos = view.portHeight() - y_pos return (x_pos, y_pos) spore 参考 ## 基于 draggerContext 笔刷 Curve paint and tweak tool   最后在 highend3d 里面也找到了一个直接 tweak CV 点的方案。   这个方案采用 draggerContext 实现   draggerContext 的案例就可以实现在 viewport 拖拽的时候实现回调。   highend3d 的 ysd 曲线工具集还结合软选择的范围作为笔刷移动的范围参数,这是非常聪明的做法。   也可以通过这个方式实现拖拽生成一条曲线。   结合 OpenMaya API 可以做更多的事情,比如散布物体等等,用这个的方案比起 从零构建一个 MPxContext 要简单许多。   绘制功能还是无法通过 draggerContext 解决,不过可以用上面 Qt Overlay 方案来解决。 ysv 工具优化   从 highend3d 下载的 ysv 曲线工具可以用,但是有几个问题 直接调用 PySide 导致不兼容 没有做 Python3 兼容 部分代码在新版本代码下运行有 BUG PySide 兼容   将 PySide 的导入转成 Qt.py 的导入   Qt.py 库的引入则是采用 submodule 的方式放到 scripts 目录下。 Python3 兼容   Python3 兼容使用 Python内置的 lib2to3 库进行转换 参考链接 1 mayapy -m lib2to3 -w F:\repo\Maya-CurveBrush\scripts\ysv\ysvView.py   通过这个方式就可以自动将所有的 print 括号加上等操作。省去繁琐的人工操作。   我当时是写了一个脚本,批量执行,执行完之后调用 black 和 isort 风格化代码。   生成完之后会将之前的文件加上 .bak 后缀,如果没有问题就可以把 bak 删除。 BUG 修复   原代码获取当前摄像机是通过下面的方式 1 2 from pymel.core import * modelEditor(getPanel(wf=1), e=1, nurbsCurves=1)   但是如果当前 focus 的 panel 不是 modelEditor 就遭殃了。 1 2 3 4 5 from pymel.core import * for mp in getPanel(type="modelPanel"): if modelEditor(mp, q=1, av=1): modelEditor(mp, e=1, nurbsCurves=1) break   所以我把代码改成上面的效果。 1 2 3 4 5 6 7 for crv in self.inViewCurves: for cv in [crv.cv[0], crv.cv[-1]]: cv = str(cv) # fix add here setAttr(cv + ".xv", lock=1) setAttr(cv + ".yv", lock=1) setAttr(cv + ".zv", lock=1)   用 pymel 获取 cv 点之后,直接将 NurbsCurveCV 与字符串相加   但是 NurbsCurveCV 有自己的相加逻辑,所以这里需要加上字符串转换可以修复 BUG。 artisan 笔刷   Maya 根据贴图在模型表面散列物体 以前也写过散列物体的文章,不过实现方式是非笔刷的。   利用 artisan 就可以实现笔刷的方式散布物体了。 官方文档: Overview of MEL script painting   官方提到有 spherePaint geometryPaint emitterPaint 几个案例。   具体的代码可以在 mel 脚本库里面找到 eg: C:\Program Files\Autodesk\Maya2018\scripts\others\spherePaint.mel   从最简单的 spherePaint 介绍   在 Modify 页面下找到 Paint Scripts Tool 工具   打开工具属性面板,在 Setup 标签页的 Tool setup cmd 输入 spherePaint 就可以激活笔刷   激活工具之后就可以模型上刷 Sphere   只可惜 artisan 笔刷它不响应 NurbsCurve ,只支持 mesh。   所以无法实现上面探讨的 曲线笔刷 的功能。   artisan 方案更适合颜色绘制或者是物体散布。 总结   以上就是我发现的 Maya 笔刷的多种使用姿势。   后面有机会可以再探讨一下 artisan 笔刷的关于顶点色编辑相关的内容。

2022/8/9
articleCard.readMore

Unreal C++ 工具开发最小实践

前言   Unreal 的学习浩瀚且博杂,有时候一个最小 Demo 就是很好的学习起点。   想起我以前翻阅 UE 的源码一大堆的文件,看得我是无比头疼。   偶然间发现 CSDN YakSue 写了好多篇 Unreal 工具开发的 介绍。   虽然没有配上 Github 链接,但是源码都在文章里面体现了。   对于工具开发的不同模块都大有裨益。   于是我将这些内容整合到一起,并且详细讲解其中实现的核心点。 Custom Asset https://yaksue.blog.csdn.net/article/details/107646900 https://github.com/FXTD-ODYSSEY/Unreal-Playground/tree/main/Plugins/Yaksue/TestAssetEditorPlg   创建一个自定义的 Asset 需要有三个类 Asset (UObject) AssetFactory (UFactory) AssetTypeActions (FAssetTypeActions_Base)   Asset 描述对象本身的数据   AssetFactory 描述如何创建对象   AssetTypeActions 返回对象显示的信息   AssetTypeActions 包含方法 GetName GetTypeColor GetSupportedClass GetCategories 用来描述对应的信息。   GetCategories 会分配 Asset 所属的位置。   这个方式默认打开的窗口是 Details Panel.   如果想要自定义打开的窗口需要添加 FAssetEditorToolkit 类   AssetTypeActions 添加 OpenAssetEditor 方法将 Toolkit 生成并初始化。 1 2 3 4 5 6 7 FAssetEditorToolkit GetToolkitFName GetBaseToolkitName GetWorldCentricTabPrefix GetWorldCentricTabColorScale Initialize RegisterTabSpawners   RegisterTabSpawners 通过这个方法注册生产 Tab 的 ID   后续通过 Initialize 方法调用 AddTab 将 Register 的 Tab 生成。   最后通过 FAssetEditorToolkit::InitAssetEditor 完成 Toolkit 的初始化   如果不想将 Asset 放到 EAssetTypeCategories::Misc 的分类中。   也可以构建一个新的标签附上去。   只是需要将 factory 相关的 GetMenuCategories 放入去掉。   我之前没有去掉,一直很疑惑为啥自定义菜单没有生效。 1 2 3 4 5 6 7 8 9 10 11 FYaksueTestAssetTypeActions::FYaksueTestAssetTypeActions() { // NOTE: 注册新的分类 IAssetTools &AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get(); AssetCategory = AssetTools.RegisterAdvancedAssetCategory(FName(TEXT("Custom Assets")), LOCTEXT("CustomAssetCategory", "Custom Assets")); } uint32 FYaksueTestAssetTypeActions::GetCategories() { return AssetCategory; }   构造函数注册新的分类,头文件需要添加上定义 FYaksueTestAssetTypeActions(); EAssetTypeCategories::Type AssetCategory; Custom Filter https://yaksue.blog.csdn.net/article/details/120929455 https://github.com/FXTD-ODYSSEY/Unreal-Playground/tree/main/Plugins/Yaksue/TestCustomFilter   继承 UContentBrowserFrontEndFilterExtension 可以通过 override AddFrontEndFilterExtensions 方法扩展 filter。   生成一个 FFrontendFilter 子类,然后通过 AddFrontEndFilterExtensions 将过滤对象添加到过滤列表里面。   FFrontendFilter 最核心的方法就是 PassesFilter 它会将每个 item 传到这个函数返回 bool 来决定是否显示。 Slate https://yaksue.blog.csdn.net/article/details/110084013 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 // Put your tab content here! SNew(SOverlay) + SOverlay::Slot()//底层 [ SNew(SHorizontalBox) + SHorizontalBox::Slot().FillWidth(0.3f)//占30% [ SNew(SButton)1 ] + SHorizontalBox::Slot().FillWidth(0.7f)//占70% [ SNew(SVerticalBox) + SVerticalBox::Slot().FillHeight(0.5f)//占50% [ SNew(SButton) ] + SVerticalBox::Slot().FillHeight(0.5f)//占50% [ SNew(SButton) ] ] ] + SOverlay::Slot()//顶层 [ SNew(SHorizontalBox) + SHorizontalBox::Slot().FillWidth(1.0f)//占满剩余空间 + SHorizontalBox::Slot().AutoWidth() [ SNew(SVerticalBox) + SVerticalBox::Slot().FillHeight(1.0f)//占满剩余空间 + SVerticalBox::Slot().AutoHeight() [ SNew(SBox) .HeightOverride(128) .WidthOverride(128) [ SNew(SButton) ] ] + SVerticalBox::Slot().FillHeight(1.0f)//占满剩余空间 ] + SHorizontalBox::Slot().FillWidth(1.0f)//占满剩余空间 ]   使用 Unreal Slate 构建窗口,通过代码的属性结构来描述 UI 的构成和配置。 DockTab Layout https://yaksue.blog.csdn.net/article/details/109321869 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 void FTestLayoutWindowModule::StartupModule() { // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module FTestLayoutWindowStyle::Initialize(); FTestLayoutWindowStyle::ReloadTextures(); FTestLayoutWindowCommands::Register(); PluginCommands = MakeShareable(new FUICommandList); PluginCommands->MapAction( FTestLayoutWindowCommands::Get().OpenLayoutWindow, FExecuteAction::CreateRaw(this, &FTestLayoutWindowModule::PluginButtonClicked), FCanExecuteAction()); FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked<FLevelEditorModule>("LevelEditor"); { TSharedPtr<FExtender> MenuExtender = MakeShareable(new FExtender()); MenuExtender->AddMenuExtension("WindowLayout", EExtensionHook::After, PluginCommands, FMenuExtensionDelegate::CreateRaw(this, &FTestLayoutWindowModule::AddMenuExtension)); LevelEditorModule.GetMenuExtensibilityManager()->AddExtender(MenuExtender); } { TSharedPtr<FExtender> ToolbarExtender = MakeShareable(new FExtender); ToolbarExtender->AddToolBarExtension("Settings", EExtensionHook::After, PluginCommands, FToolBarExtensionDelegate::CreateRaw(this, &FTestLayoutWindowModule::AddToolbarExtension)); LevelEditorModule.GetToolBarExtensibilityManager()->AddExtender(ToolbarExtender); } FGlobalTabmanager::Get()->RegisterNomadTabSpawner(TestLayoutWindowTabName, FOnSpawnTab::CreateRaw(this, &FTestLayoutWindowModule::OnSpawnPluginTab)) .SetDisplayName(LOCTEXT("FTestLayoutWindowTabTitle", "TestLayoutWindow")) .SetMenuType(ETabSpawnerMenuType::Hidden); // ! InnerTab的内容: FGlobalTabmanager::Get()->RegisterNomadTabSpawner(InnerTabName, FOnSpawnTab::CreateLambda([](const FSpawnTabArgs& SpawnTabArgs) { return SNew(SDockTab) .TabRole(ETabRole::NomadTab) [ SNew(STextBlock) .Text(FText::FromString("InnerTab")) ]; })) .SetDisplayName(LOCTEXT("InnerTab", "InnerTab")) .SetMenuType(ETabSpawnerMenuType::Hidden); // ! InnerTab2的内容: FGlobalTabmanager::Get()->RegisterNomadTabSpawner(InnerTabName2, FOnSpawnTab::CreateLambda([](const FSpawnTabArgs& SpawnTabArgs) { return SNew(SDockTab) .TabRole(ETabRole::NomadTab) [ SNew(STextBlock) .Text(FText::FromString("InnerTab2")) ]; })) .SetDisplayName(LOCTEXT("InnerTab2", "InnerTab2")) .SetMenuType(ETabSpawnerMenuType::Hidden); }   核心处理是在插件加载的时候 StartupModule 调用 RegisterNomadTabSpawner 注册 Tab 1 2 3 4 void FTestLayoutWindowModule::PluginButtonClicked() { FGlobalTabmanager::Get()->InvokeTab(TestLayoutWindowTabName); }   点击 GUI 会触发 Tab 生成,调用 OnSpawnPluginTab 方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 TSharedRef<SDockTab> FTestLayoutWindowModule::OnSpawnPluginTab(const FSpawnTabArgs& SpawnTabArgs) { //原来的分页: const TSharedRef<SDockTab> NomadTab = SNew(SDockTab) .TabRole(ETabRole::NomadTab); //创建TabManager if (!TabManager.IsValid()) { TabManager = FGlobalTabmanager::Get()->NewTabManager(NomadTab); } //创建布局: if (!TabManagerLayout.IsValid()) { TabManagerLayout = FTabManager::NewLayout("TestLayoutWindow") ->AddArea ( FTabManager::NewPrimaryArea() ->SetOrientation(Orient_Vertical) ->Split ( FTabManager::NewStack() ->SetSizeCoefficient(.4f) ->AddTab(InnerTabName, ETabState::OpenedTab) ) ->Split ( FTabManager::NewStack() ->SetSizeCoefficient(.4f) ->AddTab(InnerTabName2, ETabState::OpenedTab) ) ); } //从布局中恢复得到控件 TSharedRef<SWidget> TabContents = TabManager->RestoreFrom(TabManagerLayout.ToSharedRef(), TSharedPtr<SWindow>()).ToSharedRef(); //设置内容控件 NomadTab->SetContent( TabContents ); return NomadTab; }   这里将之前注册的 Tab 唤起。 Viewport https://yaksue.blog.csdn.net/article/details/109258860   引入默认的 SEditorViewport 类   然后 override 方法 MakeEditorViewportClient 1 2 3 4 5 TSharedRef<FEditorViewportClient> STestLevelEditorViewport::MakeEditorViewportClient() { TSharedPtr<FEditorViewportClient> EditorViewportClient = MakeShareable(new FEditorViewportClient(nullptr)); return EditorViewportClient.ToSharedRef(); }   然后Slate 代码直接使用 SNew(STestLevelEditorViewport) 初始化界面即可。   不过这个方式沿用了 Viewport ,如何构建一个自定义 Viewport 呢? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 TSharedRef<FEditorViewportClient> STestEditorViewport::MakeEditorViewportClient() { PreviewScene = MakeShareable(new FPreviewScene()); //向预览场景中加一个测试模型 { //读取模型 UStaticMesh* SM = LoadObject<UStaticMesh>(NULL, TEXT("StaticMesh'/Engine/EngineMeshes/Cube.Cube'"), NULL, LOAD_None, NULL); //创建组件 UStaticMeshComponent* SMC = NewObject<UStaticMeshComponent>(); SMC->SetStaticMesh(SM); //向预览场景中增加组件 PreviewScene->AddComponent(SMC, FTransform::Identity); } TSharedPtr<FEditorViewportClient> EditorViewportClient = MakeShareable(new FEditorViewportClient(nullptr, PreviewScene.Get())); return EditorViewportClient.ToSharedRef(); }   新建一个自定义的 FPreviewScene ,可以将物体实例化添加到场景当中。   将 PreviewScene 传入到 FEditorViewportClient 中,这样 Viewport 就显示独立的场景。 1 2 3 4 5 TSharedPtr<SWidget> STestEditorViewport::MakeViewportToolbar() { return SNew(SCommonEditorViewportToolbarBase, SharedThis(this)); }   使用上面的代码可以构建出默认 Viewport 的 Toolbar。 GraphEditor https://yaksue.blog.csdn.net/article/details/107945507 https://yaksue.blog.csdn.net/article/details/108020797 https://yaksue.blog.csdn.net/article/details/108227439 https://yaksue.blog.csdn.net/article/details/109347063 EditorMode

2022/7/15
articleCard.readMore

Maya C++ pyd 模块开发

前言 作者: 👨‍💻sonictk https://github.com/sonictk/maya_python_c_extension   这篇文章也是参考 sonictk 大佬的提供的 pyd 开发文章。   文章也提到之前的 hot reload 方案已经解决了很多 C++ 开发困难的问题。   然而还是有很多情况需要开发一个 python 的 C++ 模块实现 Maya C++ API 的 调用。   这个情况有点像是 Unreal 暴露 C++ API 到 Python 一样。 Maya 编译 c 相关 Python 库 & pyd 编译   之前我也写过关于 Maya pyd 编译的文章,但是这个文章是用 Cython 自动生成 C 代码编译实现的,这次是手写 pyd。 什么是 pyd   pyd 本质上也是一个 dll 文件,就像 Maya 插件的 mll 一样。   只是 pyd 规定了一些暴露规则,从而让 python 解释器可以读取。   这也是 Python 称之为胶水语言的一大特点,它可以无缝和 C++ 编译的模块进行交互。   因此很多 C++ 的包 比如 Qt 等可以暴露接口到 Python 实现调用。 pyd hello world 案例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 #include <Python.h> #include <maya/MGlobal.h> #include <stdio.h> static const char MAYA_PYTHON_C_EXT_DOCSTRING[] = "An example Python C extension that makes use of Maya functionality."; static const char HELLO_WORLD_MAYA_DOCSTRING[] = "Says hello world!"; // NOTE(timmyliang): 调用 MGlobal API 打印 Python 传递的字符串 static PyObject *pyHelloWorldMaya(PyObject *module, PyObject *args) { const char *inputString; if (!PyArg_ParseTuple(args, "s", &inputString)) { return NULL; } PyGILState_STATE pyGILState = PyGILState_Ensure(); MGlobal::displayInfo(inputString); PyObject *result = Py_BuildValue("s", inputString); PyGILState_Release(pyGILState); return result; } // NOTE(timmyliang): 定义模块的函数列表 static PyMethodDef mayaPythonCExtMethods[] = { {"hello_world_maya", pyHelloWorldMaya, METH_VARARGS, HELLO_WORLD_MAYA_DOCSTRING}, {NULL, NULL, 0, NULL} // NOTE: (sonictk) Sentinel value for Python }; // NOTE(timmyliang): python2 初始化函数规范 init<module_name> #if PY_MAJOR_VERSION == 2 extern "C" PyMODINIT_FUNC initpy_hello() { return Py_InitModule3("py_hello", mayaPythonCExtMethods, MAYA_PYTHON_C_EXT_DOCSTRING); } // NOTE(timmyliang): python3 初始化函数规范 PyInit_<module_name> #elif PY_MAJOR_VERSION == 3 extern "C" PyMODINIT_FUNC PyInit_py_hello() { static PyModuleDef hello_module = { PyModuleDef_HEAD_INIT, "py_hello", // Module name to use with Python import statements MAYA_PYTHON_C_EXT_DOCSTRING, // Module description 0, mayaPythonCExtMethods // Structure that defines the methods of the module }; return PyModule_Create(&hello_module); } #endif   上面的代码就是一个小案例,将 C++ 编译成 pyd 给 python 调用。   并且这里引用了 Maya 的 API ,因此只能使用 Maya 的 Python Interpreter (mayapy.exe) 进行加载。   如果使用其他 Python 导入这个模块会出现如下的错误 1 2 3 4 Traceback (most recent call last): File "d:/Obsidian/Personal/2_Area/📝Blog/CG/Maya/C++/test_load.py", line 5, in <module> import py_hello ImportError: DLL load failed while importing py_hello: 找不到指定的程序。   pyd 的 C++ 代码包含三个部分 python 定义的函数 函数列表定义 (需要传入上面的 C++ 编写的 Python 函数) 模块定义 (传入上面的 函数列表)   最后生成模块部分,Python2 和 Python3 暴露的 API 不一致,可以用宏来区分。   编译这个 cpp 需要加上 Maya include 目录的头文件,以及链接 Maya lib 的静态库文件。   另外编译 pyd 需要特别注意的是,它也需要想 mll 一样暴露出初始化的函数。   在 python2 下是 init<module_name> 开头,在 python3 下是 PyInit_<module_name> 开头。   在 cpp 里面配置编译环境是个相当让人头疼的问题。   我在自己的 CMakeMaya 库里面已经配置好了编译用的环境,   具体的使用方法可以看 readme 或者参考我的文章 Maya CMake 构建 C++ 插件编译环境   在我提供的环境下执行 doit c -p pyd -v 2020 即可编译出 pyd 到 plug-ins\Release\maya2022\pyd\py_hello.pyd   需要注意 pyd 在不同的平台不同Maya版本都需要单独编译。这里我提供了编译好给 Windows64 Maya2020 的 pyd 导入 pyd 引入 Maya C++ 节点   在相应的版本执行就可以看到如期触发了 maya API 的方法。   也可以用这个方式注册 Maya 的节点和 Mel 命令,具体可以看 pyDeformer 的代码。   只是由于没有 initializePlugin 拿不到传进来的 MObject 实例化 MFnPlugin。   我测试的 py_deformer 用了 MFnPlugin::findPlug 拿到内置插件 matrixNodes 提供的 MObject 来注册节点。   答案是可以实现的,而且新加入的节点也会显示在 matrixNodes 上。   这种骚操作不建议使用,而且也不知道会不会有什么 BUG 导致 Maya 崩溃。   另外没有办法触发 uninitializePlugin 来注销这个节点的注册。 pyd mll 缝合怪   基于上面的测试我发现还可以生成出既是 Maya 插件又是 Python 模块的 缝合怪文件。   因为 C++ 只要编译的时候 export 出对应的方法就可以加载。   只是 Python 加载二进制包要求文件后缀为 pyd ,Maya 加载二进制插件要求文件命名为 mll 才可以。   解决这个问题,可以用软连接或者拆分成两个文件来实现,经过测试是可以的,具体可以看 pyCommand 的 测试代码 。 使用 mll 嵌入 python 模块   上面主要实现按照 python 的规范加载包的操作,sonitck 的文章还提供了一个方案,加载 mll 获取到 python 包的方式。   做法也不复杂,就是在 initializePlugin 的时候加上加上 C++ 的模块。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 #include <Python.h> #include <maya/MFnPlugin.h> #include <maya/MGlobal.h> const char *kAUTHOR = "TimmyLiang"; const char *kVERSION = "1.0.0"; const char *kREQUIRED_API_VERSION = "Any"; static const char HELLO_WORLD_MAYA_DOCSTRING[] = "Says hello world!"; static const char MAYA_PYTHON_C_EXT_DOCSTRING[] = "An example Python C extension that makes use of Maya functionality."; PyObject *module = NULL; static PyObject *pyHelloWorldMaya(PyObject *module, PyObject *args) { const char *inputString; if (!PyArg_ParseTuple(args, "s", &inputString)) { return NULL; } PyGILState_STATE pyGILState = PyGILState_Ensure(); MGlobal::displayInfo(inputString); PyObject *result = Py_BuildValue("s", inputString); PyGILState_Release(pyGILState); return result; } static PyMethodDef mayaPythonCExtMethods[] = { {"hello_world_maya", pyHelloWorldMaya, METH_VARARGS, HELLO_WORLD_MAYA_DOCSTRING}, {NULL, NULL, 0, NULL} }; MStatus initializePlugin(MObject obj) { MFnPlugin plugin(obj, kAUTHOR, kVERSION, kREQUIRED_API_VERSION); if (!Py_IsInitialized()) Py_Initialize(); if (Py_IsInitialized()) { PyGILState_STATE pyGILState = PyGILState_Ensure(); // NOTE(TimmyLiang): python2 直接初始化模块就不会变成 built-in 模块 #if PY_MAJOR_VERSION == 2 module = Py_InitModule3("mll_py", mayaPythonCExtMethods, MAYA_PYTHON_C_EXT_DOCSTRING); // NOTE(TimmyLiang): python3 用官方的方式添加模块不行,可能是因为 Py_Initialize 已经执行了 #elif PY_MAJOR_VERSION == 3 // NOTE(TimmyLiang): 参考 https://github.com/LinuxCNC/linuxcnc/issues/825 将模块加到 sys.modules 里面 static PyModuleDef hello_module = { PyModuleDef_HEAD_INIT, "mll_py", // Module name to use with Python import statements MAYA_PYTHON_C_EXT_DOCSTRING, // Module description 0, mayaPythonCExtMethods // Structure that defines the methods of the module }; module = PyModule_Create(&hello_module); PyObject *sys_modules = PyImport_GetModuleDict(); PyDict_SetItemString(sys_modules, "mll_py", module); #endif MGlobal::displayInfo("Registered Python bindings!"); if (module == NULL) { return MStatus::kFailure; } // NOTE(timmyliang): 增加引用计数(确保不会 gc) Py_INCREF(module); PyGILState_Release(pyGILState); } return MStatus::kSuccess; } MStatus uninitializePlugin(MObject obj) { MStatus status; // NOTE(timmyliang): 减少引用计数 Py_DECREF(module); return status; }   上面的代码兼容 python2 python3 版本。   python2 直接用默认的 Py_InitModule 方法就可以添加,如果在 Python 打印模块会提示 <module 'mll_py' (built-in)>   但是 python3 下面不行,后来查找了 Github 的 issue 通过将模块添加到 sys.modules 下面解决问题。   只是模块打印就是普通的模块。   那为什么将模块放到 sys.modules 就可以了,这 Python 的 import 机制有关。 Python - Import 机制   这个方式可以将一些 C++ 的 API 暴露给 Python,只是这个操作需要更多的说明。   否则没人知道这个 mll 居然添加一个 Python 模块。 pybind11 自动绑定   通过上面一顿操作,也可以深刻体会到如果跨版本兼容 C++ 需要做很多宏的判断,相当繁琐。   包括 Python2 和 Python3 暴露的方法名不一样,需要在 CMake 上进行判断。   使用 pybind11 进行转换相对方便许多 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <Python.h> #include <maya/MGlobal.h> #include <stdio.h> #include <pybind11/pybind11.h> // https://zhuanlan.zhihu.com/p/80884925 void displayInfo(char *inputString) { MGlobal::displayInfo(inputString); return; } PYBIND11_MODULE( pybind11cpp, m ){ m.doc() = "pybind11 example"; m.def("display_info", &displayInfo, "Maya Display Info" ,pybind11::arg("inputString") = "hello world!"); }   pybind11 会自动将 Python 的参数进行转换   这样只要将纯粹的 C++ 函数放入到 PYBIND11_MODULE 宏   并且 pybind11 的 2.9 版本支持 python2 python3 的 pyd 编译。   只要在 cmake 里面配置 /export 对应的方法即可。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 find_package(Pybind11 REQUIRED) project(pybind11cpp) #project name file(GLOB SRCS "pybind11/*.cpp" "pybind11/*.h") include_directories(${MAYA_INCLUDE_DIR} ${MAYA_PYTHON_INCLUDE_DIR} ${PYBIND11_INCLUDE_DIR}) link_directories(${MAYA_LIBRARY_DIR}) #specifies a directory where a linker should search for libraries add_library(${PROJECT_NAME} SHARED ${SRCS}) #Add a dynamic library to the project using the specified source files # pybind11_add_module(${PROJECT_NAME} ${SRCS}) target_link_libraries(${PROJECT_NAME} ${MAYA_LIBRARIES}) #specifies list of libraries to use when linking the terget and its dependents if(${MAYA_VERSION} GREATER 2020) set(PYBIND_LINK_FLAGS "/export:PyInit_pybind11cpp") else() set(PYBIND_LINK_FLAGS "/export:initpybind11cpp") endif() set_target_properties(${PROJECT_NAME} PROPERTIES LINK_FLAGS ${PYBIND_LINK_FLAGS} SUFFIX ".pyd" )   pybind11 可以使用 pybind11_add_module 来生成 pyd   但是它是自动查找 Python 环境,指定 Maya 的 Python 需要额外的配置。   所以我就不用这个,自己来配置好了。   通过上面的方式可以大大简化 C++ 的编写。 总结   以上就是 pyd 编译的各种折腾结果。   社区里面值得说道的有 cmdc 基于 pybind11 编译的二次封装 C++ API 库。   Python 调用 C++ 还有利用 ctypes 库访问 dll 的方式   后续也可以实验一下在 Python 中从 dll 里面调用 function 实现 参考:https://github.com/Autodesk/animx

2022/7/14
articleCard.readMore

Unreal C++ VScode 配置

前言   这次尝试在 VScode 进行引擎编译。   网上一查发现,官方其实有做支持的,具体可以参考这篇文章 链接   这篇文章传播甚广,可以参照和这个方式配置 VScode 编译。 https://www.youtube.com/watch?v=fydvKedIxKk https://github.com/boocs/ue4-tellisense-fixes C++ 编译过程 深度参考学习这边文章 https://ericlemes.com/2018/11/21/compiling-c-code/ 鉴于本人的 C++ 水平一般,建议阅读原文 编译步骤   C++ 编译可能会用到下面的文件。 .cpp 文件编译成 .obj 生成静态库 .lib 生成动态库 .dll 生成可执行文件 executable VS 工具链   .sln 全称是 solution 解决方案,是 VS 的项目配置文件。 (整合了 .vcxproj .csproj)   他可以同时配置多个项目,最后通过 MSBuild 来构建   sln 包含了项目的各种头文件依赖,库引用等描述,执行顺序,通过这个 IDE 就知道怎么编译你的项目。   Xcode 的情况也是类似的。   其中比较特别的时 CMake ,通过 CMakeLists.txt 文件可以根据不同平台生成工程配置文件。 第一步 编译 输入: Defines Include 文件夹路径Include directories 预编译头文件 (如果有用到的话) 源代码 输出: .obj 文件   MSBuild 使用 CL.exe 进行 C++ 编译。 可能的路径 C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30133\bin\Hostx64\x64\cl.exe   需要安装 VS 或者用 choco 来安装   编译的时候会根据 宏定义(比如 #ifdef)动态 改变编译行为   通过这个方式可以在不同的平台编译出不同的行为。   C++ 最终编译成对应平台的二进制,这个设计和 Java C# 都不同。   头文件最终会拼接到 C++ 里面进行编译,所以需要加上 #pragma once 或者 #if 来避免多次定义。   预编译头则可以生成 .pch 文件实现头文件复用。 第二步 链接 输入: 一些源码生成 .obj 文件 一些源码生成 .lib 文件 第三方的 lib 和 obj 文件 输出: .dll 或者 .exe   这一步会将生成的中间文件合并成 dll 或者 exe   这个过程会完成很多优化的步骤,把不运行的部分清理掉。   最后会将一些平台的 lib 引入确保它在平台上可以运行,比如 wincrt (Windows C Runtime library) 等等   并且 lib 也有很多种类,有 release 版本和 debug 版本等等。 Unreal Build Tool https://ericlemes.com/2018/11/23/understanding-unreal-build-tool/ CS 配置文件说明 https://www.bilibili.com/read/cv15297017/   Unreal 使用自己开发的 UnrealBuildTool 来编译自己的 C++ 代码   与 💾CMake 类似的,UnrealBuildTool 会引用你需要在相应的模块添加 .build.cs 的代码文件来描述仓库链接的东西。   .build.cs 之上配套了 Private Public 文件夹分别放置暴露和不暴露的代码。   .target.cs 则可以用来定义输出的类型,有 Game Editor Client Server 几种类型。 生成工程文件   当我们对 uproject 文件右键生成 project 的时候背后执行就是 UnrealBuildTool 1 C:/EpicGames/git/UnrealEngine-4.27/Engine/Binaries/DotNET/UnrealBuildTool.exe -projectfiles -project="D:/EpicGames/test_plugin/test_plugin.uproject" -game -engine -progress -log="D:\EpicGames\test_plugin/Saved/Logs/UnrealVersionSelector-2022.07.12-15.50.08.log"   UnrealBuildTool 会根据 .build.cs 和 .target.cs 里面配置模块路径生成 sln 工程文件。 编译 C++ 1 D:/EpicGames/UE_4.27/Engine/Binaries/DotNET/UnrealBuildTool.exe Development Win64 -Project="D:/EpicGames/Unreal_Playground/Unreal_Playground.uproject" -TargetType=Editor -Progress -NoEngineChanges -NoHotReloadFromIDE   这个 Build.bat 背后还是调用 UnrealBuildTool.exe 通过它来编译 C++   上面生成工程时候 .build.cs 和 .target.cs 只是收集了路径。   现在会再次读取这两个文件来获取一些编译用的属性。   然根据配置解决各个模块的依赖关系。   最后会运行 UnrealHeaderTool 将 UObject 的一些特性注入到 UObject 的 cpp 文件当中。   这也说明了为什么需要引入 .generated.h 的头文件。   准备好了所有代码之后再调用相应的编译工具去构建 C++。 VScode 编译配置   了解了 C++ 编译和 Unreal 全家桶的编译逻辑之后。   我们终于可以回归到本篇文章的正题。 http://jollymonsterstudio.com/2018/11/02/unreal-c-with-visual-studio-code/   按照这里提供的文章就可以用 Unreal 官方的方式配置好 .vscode 目录的编译配置。   后续只要 Ctrl + shift + B 就可以触发编译。   编译背后的逻辑就在上面解释了。   相应的我也可以用 python 脚本来触发编译。   sln 工程并不是必须的,不过 VS 有 VA 查找代码比较快。

2022/7/12
articleCard.readMore

Maya C++ mll hot reload 研究

前言 作者: 👨‍💻sonictk https://sonictk.github.io/maya_hot_reload_example_public/   详细的说明 & 教程在上面的链接。   Maya 用写 C++ 开发会比较痛苦,一方面是编译问题总是让人烦躁,另一方面加载了 mll 会导致占用,测试起来很不方便。   所以我之前推崇用 Python OpenMaya 做原型设计再转 C++   当然 sonictk 也提到 Fabric Engine 和 Maya Bifrost 使用的时 LLVM IR 的方案来实现 JIT 编译。   具体可以参考另一个项目 giordi91/babycpp LLVM 热加载   babycpp 基于 LLVM 的解决方案我编译没有通过,代码报类型错误,因此也没有测试成功。   不过也了解了 LLVM 是怎么实现热更新的,运行逻辑和 Python 有点像,但是从本质上不一样。   传统的编译器需要有 前端 优化器 后端组成,一般前端是语言,通过 tokenize 和 AST 等方案将语言解析然后通过优化器生成后端的二进制文件。   LLVM 推出了 LLVM IR 中间语言,这样不管前端用什么语言开发,只要有对应的解析工具生成出 LLVM IR ,j就可以利用 LLVM IR 的优化生成 二进制机器语言高效运行。   babycpp 项目就基于 LLVM IR 的机制开发了一个自己的简化版 C++ 语言,通过 LLVM IR JIT 编译动态改变运行逻辑。   我目前个人理解来看,LLVM IR 模式和 Python 模式还是不一样的,Python 是调用自己编译好的模块来运行的,而 LLVM IR 是直接运行时(JIT)生成机器语言,JIT模式的运行效率有时候比 C++ 的静态编译还要高,因为 JIT 可以根据运行过程推断程序下一步的执行来优化非必要的运行逻辑,所以 LLVM IR 的性能要比 Python 好得多。其实我后面了解了一下 numba 提速 Python 的原理就是利用 LLVM 标准实现的。   不过也正如 sonictk 的文章所提到的,这个方案只能调用暴露的东西,无法对内存的细节进行处理。 基于 dll 加载 https://github.com/FXTD-ODYSSEY/CMakeMaya/tree/master/projects/sonictk/hot_reload   如果使用作者提供的 github 仓库的代码编译会有问题,作者的 thirdparty 仓库编译不通过。   所以我后面是根据作者文章的代码稍微调整组装到一起实现的。   详细讲解之前,我先用最简单的话说明这个 hotreload 方案。 编译一个变形器的 mll 插件 和 带逻辑的 dll 文件 mll 加载之后会调用 dll 的function进行计算 修改逻辑之后重新编译 dll mll 会重新健在最新的 dll 实现热更新。 实现思路 https://sonictk.github.io/maya_hot_reload_example_public/getting_started/   这篇文章非常好,不仅仅讲解了作者 hot reload 的思路,还附带了 windows lib dll 之间的运行逻辑等知识。 目录结构   代码结构上需要将插件分成两个部分,一个是调用 logic 生成 dll   另一个是 deformer 的代码生成 mll   具体编译配置通过 cmake 配置两个 project 实现。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 . ├── logic │ ├── logic.cpp dll 代码逻辑 │ └── logic.h ├── maya_deformer │ ├── deformer_platform.cpp 调用 <windows.h> API 加载 dll │ ├── deformer_platform.h │ ├── deformer.cpp Maya 变形器 deform 调用 deform_platform 提供的方法 │ ├── deformer.h │ ├── plugin_main.cpp Maya mll 插件初始化函数 │ └── plugin_main.h ├── scripts │ └── test_deformer.py 测试插件是否修改 ├── CMakeLists.txt └── readme.md dll 加载方案   上面三个函数调用了 window API 提供的 LoadLibrary FreeLibrary GetProcAddress 加载 dll   然后将分装到 loadDeformerLogicDLL 和 unloadDeformerLogicDLL 方法里面。   deformer 在触发计算的时候调用加载 dll。   这样每次触发节点运算的时候会自动按照 dll 的路径进行加载。   问题是怎么在 C++ 动态获取到当前 dll 的路径呢?   在插件加载的时候通过 plugin.loadPath 可以拿到当前 mll 加载的路径。   只要在同一个路径找 logic.dll 路径即可。 遇到的坑 编译 dll 占用问题   需要注意的是,mll 被 Maya 加载会产生占用,mll 去加载 dll 也会造成占用。   只有执行 unloadDeformerLogicDLL 才会解除 dll 的占用   但是占用会造成编译失败。   于是我用 CMake 的 API 将旧的 logic.dll 改名叫 logic_old.dll   windows 下被占用的文件还是可以改名的。   然后执行编译生成新的 logic.dll   这时候需要手动触发 Maya 节点的更新,这样就会按照原来的路径加载新的 dll。   CMake 怎么判断 dll 是否占用,我也没有找到合适方法,于是我想到直接删除这个 dll 在判断 dll 是否存在的方法。 extern 问题 1 2 static MString kPluginLogicLibraryPath; static DeformerLogicLibrary kLogicLibrary;   源码这两个变量用的是 static 静态变量。   但是不知道为什么在其他 cpp 文件里面调动得到的是不同的 内存 地址。 https://blog.csdn.net/sksukai/article/details/105612235 1 2 extern MString kPluginLogicLibraryPath; extern DeformerLogicLibrary kLogicLibrary;   后续是改成 extern   然后在 plugin_main.cpp 里面初始化变量解决问题。 总结   这个方法切实解决了 节点热加载的问题,不需要 unloadPlugin 清空场景之类的操作,测试起来方便了许多。

2022/7/8
articleCard.readMore

Maya CMake 构建 C++ 插件编译环境

前言   过去构建 Maya C++ 插件是按照 Autodesk 官方提供的流程,在 VS 里面配置项目工程。 参考链接   通过配置 devkit 的 pluginwizard 来构建项目。   但是使用 VS 配置 Maya 依赖的头文件和 lib 其实挺不方便的。   依赖和修改都在不同选项里面,配置起来要搞半天。   而且这个工程配置只能兼容 Windows ,如果我们要在 Linux 环境下编译,整个流程又完全不一样了。   其实解决这种问题,有专门的工具去做。   这就是 CMake   通过 cmake 配置可以生成不同平台的工程文件,不需要打开 IDE 就可以调用 compiler 编译结果。 https://github.com/volodinroman/CMakeMaya   这个仓库是别人配置好的基于 CMake 构建 Maya 插件的仓库。 Doit 自动构建环境   但是构建编译环境还是挺麻烦的,一方面需要下载 VS 和 CMake   另外还要配置好 Maya 提供的 SDK https://github.com/FXTD-ODYSSEY/CMakeMaya   我这个仓库提供了懒人包环境,只需要配置有 Python 环境和poetry 库。   在仓库的目录,执行 poetry install 和 poetry shell 就可以进入开发虚拟环境。(注: 需要管理员权限)   poetry 会自动安装配置好的依赖,包括 doit 框架   执行 doit init 会调用 choco 安装 VS 的依赖,以及 CMake   这个过程需要等待一段时间。   执行完之后 VS Build Tool 就添加到系统了。   但还是找不到 C++ compiler ,需要手动打开 installer 下载 C++ CMake 开发包。   使用 doit SDK -v 2020 会下载 Maya 官方的 devkit 到仓库的 SDK 目录。   准备好环境之后,还需要安装好 maya 2020   如此就是完备的编译环境,只需要用 doit c 执行 cmake 编译命令来编译 C++ 插件。 1 doit c -p weightDriver -v 2020   使用 -p 可以指定编译的项目,-v 可以指定编译的 Maya 版本,默认不指定会编译全部项目的 2020 版本   -p 支持完整的projects 相对路径或者最终目录指定 1 2 doit c -p IngoClemens/weightDriver doit c -p weightDriver   执行 doit 的时候会用 python 识别将末端目录变成完整的相对目录   下面是完整执行编译的流程   doit 背后执行的是 拼接输入 执行 cmake 命令 1 cmake -Wno-dev -G "Visual Studio 16 2019" -DMAYA_VERSION={version} -DMAYA_PROJECT={project}. -B build   DMAYA_VERSION 指定 Maya 版本号   DMAYA_PROJECT 指定 Maya 项目,多个项目可以用 ; 分割。   这个命令会读取根目录的 CMakeLists.txt 根据 VS2019 的配置生成 sln 文件到 Build 目录。   windows 下如果需要 Debug 也可以用 VS 打开 sln 去配置 Debug 工具。 1 cmake --build build --config Release   后面会执行 build 命令根据配置编译输出到指定目录。 中文乱码坑 💡Vscode terminal 中文乱码   Window Terminal 默认不支持 MSBuild 的字符输出。   需要在 terminal 上执行 chcp 65001 切换字符集。 添加新工程   如果需要添加自己的 mll 需要自己填充 CMakeLists.txt 配置   使用 doit new 可以快速生成 插件 编译模板 cmake 配置说明 projects 下每个项目目录都有对应的 CMakeLists.txt 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # 设置输出目录 set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE}/maya${MAYA_VERSION}) # 设置项目名称 (一般编译的文件名取项目名) project({{cookiecutter.project_name}}) # 添加编译的文件 file(GLOB SRCS "*.cpp" "*.h") # 添加头文件依赖 include_directories(${MAYA_INCLUDE_DIR}) # 添加 lib 库目录 link_directories(${MAYA_LIBRARY_DIR}) # 链接源码 add_library(${PROJECT_NAME} SHARED ${SRCS}) # 链接 lib target_link_libraries(${PROJECT_NAME} ${MAYA_LIBRARIES}) # mll 输出配置 MAYA_PLUGIN(${PROJECT_NAME})   大部分的结构如上图,默认模板如上。   我加上了注释说明。   MAYA_PLUGIN方法将 mll 的 initializePlugin uninitializePlugin 两个方法暴露出来(Maya 加载用),并且将 dll 的后缀改为 mll。 用 CMake 编译 Devkit 的案例代码   上面提到的 CMake 是基于 https://github.com/volodinroman/CMakeMaya 的方案搭建的。   cmake 文件基本上是自己编写,可以控制每一处的细节。   其实 Maya 的 Devkit 也提供了一套 CMake 的方案。   每个插件都保留了 CMakeLists.txt 用于编译。   如何顺利编译 Maya C++ 的案例插件是一个好问题。   我过去看 Maya 的文档但是因为不会折腾这个编译(编译出错不知道怎么解决) ,导致无法深入学习 C++ 插件。   只能拿 Devkit 提供的 Python 文件进行学习。   通过上面的折腾与学习,自己也算是对 CMake 有了基础的入门,终于有能力搞定这个问题了~ https://help.autodesk.com/view/MAYAUL/2020/ENU/?guid=__developer_Maya_SDK_MERGED_A_First_Plugin_HelloWorld_html   上面的链接是官方文档提供的一个 Maya 插件最简案例。   相应的代码在 devkit\plug-ins\helloCmd 找到 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 cmake_minimum_required(VERSION 2.8) # include the project setting file include($ENV{DEVKIT_LOCATION}/cmake/pluginEntry.cmake) # specify project name set(PROJECT_NAME helloCmd) # set SOURCE_FILES set(SOURCE_FILES helloCmd.cpp ) # set linking libraries set(LIBRARIES OpenMaya Foundation ) # Build plugin build_plugin()   构建插件的 cmake 代码如上,核心部分是 $ENV{DEVKIT_LOCATION} 通过环境变量获取 Devkit 的路径   所以执行 CMake 之前可以配置一下环境变量。 1 2 3 set DEVKIT_LOCATION=F:\maya_devkit\devkitBase cmake -G "Visual Studio 16 2019" . -B build cmake --build build --config Release   如此操作,就可以编译出 mll 了。(前提是要配置好 VS 的环境) 总结   这个环境我通过 虚拟机 测试过,在 win10 环境是没有问题。   通过 cmake 配置可以快速构建好 C++ 编译环境,比起以前折腾 VS 来方便太多了。   利用 choco 来安装依赖也解决了各种缺库导致起不来的问题。   通过这个人懒人包可以极大降低 Maya 写 C++ 的难度。 2022-7-8 补充说明   最近利用 submodule 添加了很多社区的 C++ 库。   clone 仓库之后需要用执行 git submodule update --init 来拉取 submodule   一些注意事项请参阅 readme 文档

2022/7/1
articleCard.readMore

Unreal Python 导出 MetaHuman 控制器关键帧

前言   MetaHuman 已经在数字人领域里面相当成熟的解决方案。   并且 UE 官方开发了源码工程。   目前 github 上有不少人演示自己套用 MetaHuman 动画的效果。   于是我自己也尝试着想将它 UE 里面的控制器动画导出来。   然而却发现行不通。   它的控制器关键帧是在 sequencer 里面。   最初是尝试将 sequencer 的资源全部导出成 FBX。   然而控制器的关键帧并没有跟随导入到 FBX 当中。   于是我想到可以用 unreal python 读取关键帧数据导出 json    Maya 再读取数据设置关键帧到控制器上。 unreal python 导出关键帧   有思路之后就好办。   之前我也写过脚本来获取 sequencer 关键帧的。   需要注意如果想要使用 unreal python 的 API 需要开启相应的 C++ 插件。   否则 python 会获取不到相应的 API 报错。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 # Import built-in modules from collections import defaultdict import json import os # Import local modules import unreal DIR = os.path.dirname(os.path.abspath(__file__)) def unreal_progress(tasks, label="进度", total=None): total = total if total else len(tasks) with unreal.ScopedSlowTask(total, label) as task: task.make_dialog(True) for i, item in enumerate(tasks): if task.should_cancel(): break task.enter_progress_frame(1, "%s %s/%s" % (label, i, total)) yield item def main(): # NOTE: 读取 sequence sequence = unreal.load_asset('/Game/Sequencer/MetaHumanSample_Sequence.MetaHumanSample_Sequence') # NOTE: 收集 sequence 里面所有的 binding binding_dict = defaultdict(list) for binding in sequence.get_bindings(): binding_dict[binding.get_name()].append(binding) # NOTE: 遍历命名为 Face 的 binding for binding in unreal_progress(binding_dict.get("Face", []), "导出 Face 数据"): # NOTE: 获取关键帧 channel 数据 keys_dict = {} for track in binding.get_tracks(): for section in track.get_sections(): for channel in unreal_progress(section.get_channels(), "导出关键帧"): if not channel.get_num_keys(): continue keys = [] for key in channel.get_keys(): frame_time = key.get_time() frame = frame_time.frame_number.value + frame_time.sub_frame keys.append({"frame": frame, "value": key.get_value()}) keys_dict[channel.get_name()] = keys # NOTE: 导出 json name = binding.get_parent().get_name() export_path = os.path.join(DIR, "{0}.json".format(name)) with open(export_path, "w") as wf: json.dump(keys_dict, wf, indent=4)   上面的脚本会定位 MetaHuman 的 sequence 资源,然后导出关键帧的信息为 json   导出会在脚本目录输出两个 json 文件。   Maya 可以解析这个这两个 json 将关键帧设置到 控制器上。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 # Import built-in modules import json import os import traceback # Import third-party modules import pymel.core as pm DIR = os.path.dirname(os.path.abspath(__file__)) def progress(seq, status="", title=""): pm.progressWindow(status=status, title=title, progress=0.0, isInterruptable=True) total = len(seq) for i, item in enumerate(seq): try: if pm.progressWindow(query=True, isCancelled=True): break pm.progressWindow(e=True, progress=float(i) / total * 100) yield item # with body executes here except: traceback.print_exc() pm.progressWindow(ep=1) pm.progressWindow(ep=1) def main(): # NOTE: 读取数据 with open(os.path.join(DIR, "BP_metahuman_001.json"), "r") as rf: data = json.load(rf) attr_map = {"location": "t", "rotation": "r"} status = "Import Keyframe to metahuman controller" # NOTE: undo 支持 pm.undoInfo(ock=1) for channel, frame_list in progress(data.items(), status=status): # NOTE: 解析 channel_name has_attr = channel.count(".") if not has_attr: # NOTE: 处理 `CTRL_C_eye_parallelLook_4311` 格式 ctrl_name = channel.rsplit("_", 1)[0] attr = "ty" else: parts = iter(channel.split(".")) ctrl_name = next(parts, "") param = next(parts, "") axis = next(parts, "") if not axis: # NOTE: 处理 `CTRL_C_teethD.Y_4330` 格式 attr = "t" axis = param else: # NOTE: 处理 `CTRL_L_eyeAim.Rotation.Y_4387` 格式 attr = attr_map.get(param.lower()) attr += axis.split("_")[0].lower() # NOTE: 解析出控制器属性设置关键帧 attribute = pm.PyNode(".".join([ctrl_name, attr])) for frame_data in frame_list: frame = frame_data.get("frame") value = frame_data.get("value") attribute.setKey(t=frame, v=value) pm.undoInfo(cck=1)   加载 unreal 导出的数据。 总结   其实整个流程不复杂,有思路就很好处理。

2022/6/24
articleCard.readMore

TA 工具人知乎分享

您好, 这里需要密码. 提示(神秘号码 + 光子) 8fa7d4705a9ea2d031cb386fd946285e6e06572cf6c48f20e4e93f64b126b72db6b7070420aa236602175645bb6952c708919e626ce7b5e4c9c2ce5233d6ca9453e4f985eb5743e515e7e935ac25f512d0f37151972dbb079320ba80baf7dae7ccaa2eb91279dadf7fae0cd4262f09e41d57257560714d89b9af184023b2489db4bac09dbd48acd6b62afd5caa56a00f6f79ea0af9ac70c063e872bf819a68a2f89eaf8c81a21e51f6ab1f3a91025894049dae0b604fa859081918fe71a35ae052db98ec94da9fc5e86e3b1c47227fdd244f77c5d59986005ff1b37286a218081a45349fb7adfb304c0003889441cf65d8496fdacb5ae626f86cc705c44ec6dda6978212ff1f8765b7703603ed6a599d0a93ffe9f9dfd4e7c9dad9e0c7fb3209cff2d173b0d26a14698b982aa16795ba1c9c4e5fb82e68ceac65c9880efd73f864cc74b71e289cb8746e0a31a88c699d10061e0f6116f89794b6d9354c1efdb850045d101d9e0c030a24efbb7477eb2becacec083ee04f093825fd6962f4317a4b3f553088968ef83a4a75d513bf33b67661ed7de6d6c3031b951e520bfd77cb56bb9e19e51b9423183a90e7d1f5f075f485c636dddd9a8459cfcfe2cadd1650faa702db0d76a330e4a290c8f0d45b5722f9c9c96832eee831e28401f0b6c37dedfcdbd5bf55163e425cd9bf30829fbf0df2cc1b800ebb507259c8b3c9a75fa762ef15d114128837d20e3580fa42e53ce665aae00ab344918af64b2367c6392988deb437ee90ee6b4861b478d04d26e52d7961e89a0e81ca50a7dfd02e120efc9f512509787e17233ec1ff4beaa77529df7a8386a41408d9a844c5cb821b5aa53a5b8cae826ed484ea7c6da24779a250ff142c7ad81c297c5da0ba7f3d28c2a3e2bfc2b03f202bfa704493dfbe15e5f26507c7b0f457bf970078dcd18ca628f22e077502247056c2a1d4b9fa49bc2d351911658c42055ad7565dbbea5a31951b8d57dfbaf4ddebe35f2955d7f432061e053534991dc4b5a2007744109cf95db12d1dbec4d2f1ccadd86e00ecca3c715900d4a3d20a0a2473f286a19338cceb8fefebccd22d3595c47ae4e16e5f765465d5c2a40ce91ff17956dd0d7269026bcfe8f275d7a567bf1ab90c42604644ff7739a7cf8480c886a41f366d4b3be977984df0b5f62ec74ca8b5f0adec0f87056981231c0bc1ff80c9bf140de1e145d4dc1f67fe16ee20e42393fd17123ec10faae3eedbdf412eda34476fc33865ef5e75884850c9b298b08c634770c1200719f2e1b8581a72d3b5c0ba3b36c44735a7154105c1840e67ebf48ae8218a48c50d2a7544069871910348093c1c2daff52d9eaa1979a0ecb8b45225a853788add60a2065357de2ac4b9ebd2bdff6ed6ac397d79a90f131f5f25cca4c579dfc98b42a6b1f7baf9ac75abda0d30630c9a4593d0dfbf5e6f5abd72067cd3c0b6d866e58e9c8f3512b3021df06db8d5136a3a71aa6fa09ea53b22c0bc1b7d493e5c7c5569134a48104d5a733893b69f906eeda936aae13aa421517613df2efb8ec05760c64d9160cebcc2397c87da19316b5c1c6f8bdd75939349208d5ddbbc914cd55ca6ae18cb4eaf00586fbbcabada19aa2d838006b21e01ce6248498a5156a20771d48f782aae443a190095ca944bf566c764822bd990184f16823098204ccbb28cdbf0bc2a29df18a50a2fe0db80c4942bf1a68bf016a402ac07228be42d7963f88715756b5259c186bc06bdd8b85ac7dd7351d0588469710d479fccd5f861bddc58a185ae32af2ca8ed902f520ccc2d3eb89d704926d18b4ad2a236193b964c3926164848442597d26069fbbe6f62ed84adf22eceadbbee7fe316f66ca213bcf2aa4ab1ec4eff93aa7ed8bb3fc4d8642ec387641b70b5638696e8633e0c33fd32c314cb3a555f6585477e35832d241472a45be77133edd20344a06e3a4d048387521aff1f9b32b2bf9ecd118618d7574d8e9c6084e2247891c85f365a1d76356bfd1601f5594435e2772d0d5388dfaafa2593b6f6ba6c62cde9ef6322efa8e906417bc9c5fa63a3172c57a7f8150376517828973f43a387447cc8418510ecd7b7eb755940902dabc5fa709ba77ea0ff0319c20826362ed3bb3f08b833b5e81f6f26507f18ad5dd7496eac072c81bac588ff3ad1a05ac184b1a34653a710699a042fa49c470a9302c696580049c0dd76764eb7b92622039b18f232b371bd2b98a0a9c3c02a55c27ffac699179091e4e1baaee1c350e5e17fd7cbc3ad1a7a1c8a47f7110270f841fc0ef62ae25c2b208ddfdea840c9e68ac15eae56377d2aec241f1b6a33a769b5834ab19ba87e87ec66f54df90f1d2bccc32ac9bd47b19b6e91e095c6ce0a080448abe58ed75e6fd658259a4a9bdf6d0a62247fcedd35137c402443c9a8b567480658522ff0c5bc84daa9bba58738ce29e8bf3182872d377b8df42d429dd451d8888aedc99ee256a5d51324c248f62c25b4fdde33968c9f5988ac211af6946e5c669fab6e3b3c2eac93d9c25073de7cf611da967574d0418d71e6d26903a64b3ed2503116d3ba620ccf937acdc99f97358d01d7a6ceaf8846f1fe47923534c0656cd38730ec02d3a0cedf3354df80cc08abd6fac6bd4c7b2a9a1011a22b1f319bbc27b0f42f8ef8c92e39c81432549c73ba3d14ed0a100cb7ef63ab3d77811758f63a45e15794a10f762ed897510b8f818fc224c3f894d88951eeea89bb5c4802397a28280351e02cd6718364b5ac378826283619a425225c59a428cb714cb8352ced7d4a60cf591b59222a98374e6e2b95077b398dc5491ddadf129dc6116cf888e9d5b89de9a266cf5e041e157dd40d8f985569e9d02edb4796b38c83ac08f75b4fa36a1a0f6689496b5f42b1cada3acec32db498cc8777c63c5c778508fdfde422d8c69900685391869cb8dc684527a650d53c12c38345eebfb48a80d223da4bc65eeac5e6f1bca6293a514207ac8b39ac9fa5981aafad0c9773fe9fdb70f9084320a923ea5ca9105c8110bf360466c64c5b567060e6cb45503ae75c20b12a1ad93e4c0a3f08e9a267da59b8a1dba01bebe51c67f1051699513e52360f30fc1d83f3b32142910fe1fc3ad7f3fd0f7125751d533a5064ca4bd9998336809a992fa7b894103603884e8ebf3029a43587c66f81f5b5f0e4f1c635ced6842a042a807456f956be55d88cfb72ebab0b2ad14ac5ce7b412c739ee05fe845ba9b30b9aefc2d760fb2e1f7e1e3d7da54548ac1dc7255b590fec949885ae239fe529b0cb798f966b30e6e279146fe4fc1ccaa6786167cbcfc734a7055915a767340cb15955dac6809199b407eae57c775b999b982c765926862e5a478f17e33e9099ee21d01ac33ca7419621d0243ac60381fb529896246c2ccecb0c7e4252c8f8da1278cfd82970698a7aca505d7d5f0831f4665f48cf8a02f3040c133837efdd05173c4a5a5cfad1783aa3c21bad167a88414a27747fa68786a037f6f9201644612cd515294d3f5641d24e749ee39f86042aa93b301fec4cfe4df2ea2f7cac4b9ec942443777e17ae2b2bed3a04f31c92cb3a49df11512173597affe9790c85a0963d74eabea4fecdc8e9c7423f0b98ead090109ccd786a574e52777569a7018570d5d1b973826e4607362a9c102e9f96eea5a2dbc68ac24fda58ce49fc39ae06b859fd175f6289bdf37f033b3e27215545a0b1c60756154471003e59ee4d6139a9bd506149d9e84163407b60f489d2b2927baa171c27525437d8a0960612c11dd56b0c8b50bd78dcbd1ac513f3c806304a1434b6659663d3d59fabd4f5395957261a4b63b03541288f04aa4dc1f05a39518b213bcf09dc801ba4a402fa3b5f168d09292a77aa78a4c3140580737c7e5f4442c5477987175acf6fd9c57834177f11e90a88d0536b13f31100f697710d1e8d731d352b73d5e75f3b35fd334ce927f9a3794a6c88d709d3824c51750a215f4c19529d89fe077ad9c6b01c2319ffff7d85f710272fc620c82090e1c697343389f71a6576beea4911cde87cf0411c8dbdb7db05252109b21e7fe5e402c07e5b02aa42e8a074ff3ea0506e927a633855cf3847d90c9b78ddd67d3e6a7184346dbd1c4ed25fc61a5d01e76f0e2edcac5d12ab1f912e4900511d7c7df8ef7730638318d25300018bbbbde83f3e5686282d7b7835e459bda0ce31569449a873a51dff65955942f1f3a270b7dac3238d0031851af179ef39bc140b4d715d6a5484f40af20c8feb2d5035f70f37524be26a10374f93870c405bcc696700ba1e509dbf0e24a840bc28c746596079bd8feeb67a4d933f9f4691e7c131d43f6f89ce0a799b1134b8201b4f3bf0969446926b525542d774b1616fc344455e78240a0bdd13576198d950a0e62d84fb0a488e34c90a7322bd9d6fc4f54d0526b238c0395811b21e2bd51ca6ee7dbba677406239d848c2664821b2d978d910f8e105ac780b9c126df7498eb4aad68b431fcb16a023a1b072b4367881de98c9592e100af419922b3197a34e8aa1487ee91c079c915751f9317904f52bc1ee827358a5c6d1c6090d63397ea1c3c49454d1c49e7d0f8ccea1dd319dedc8075a0b8ad0b094d14c925e9c778fd5601e180cfe6ab550f9362babfcf7f0d041038f6b1710fa4d0c8cce3eeac02246070b47e8ac1ad2606de8ae71c89a416affef37d087fe187d67b9538564b9524fa3df08018a88d9f46ecf1c5140066697c5841594f4702c93dd73ffc20eb824b8ee73461427c620d80716cf143f6d935f886ee7c8d4285eb0850bc546700906f9ad3ad4f6e48c1b66fa5ebd75a95a13c56b0838efb3ab1c59e42301bf10f673024e0533c4a1f99b560e15ebd9657a532860f8917dd8fb9a05f9030d0627014b26a6740494cb2fb90ffa5c53b104596d76107557e12fb8b310e74362e70c4c207b28e172bac9592b3cf8a9bf06f19e47ebb08e41bcbd1a1abcf931b78bf9c137289e1bc5bda8d3c316b713934ec9af620b63fa823289ceef2482c0d0e657b81bbb05afd4755d6f41a93a27ca1fae71c52bcd5c7a84770423204d918de498a78cccb07a129b3ae63ae5d49ebf3e934e084e9f84220f6859d679a98c3053ca81243bd2bf9f8b7b8639eaa8b94239374e0f65e40581bbbcf0ab29def2808900c12378add6ea1c3884dba3dc1a9abf94417f34b539129e8eb2629544771fa32d261676ccf605d389d46152681d30900d4bc1a2221e1509a789814cdbf823015de8e5ac05ae280f7d72085806e8067c9dacf7903fed4569efeef1250e8183ac9238fd8b0dfa7f74a0b462860c47f986a9af2bc57e8081f4af248fff773a91d3a39c2efb90824cbdc1d6898630b6e456d3ad03b258c78240713155e89f30b57d2cdf65ca9b07b04338b378eaedb594fa971aa6e34e4206184288e281ed9d6bfd96b97ce510ccdcc5c5eeebe4d004e1cc999cdc0a71405a0b868475b91978a88115b2df1487ddd25bedab60596e858e8bd1200e28b361c58b732b220e5594cc880d2fb16c8f64b80f68f8bba3f14343c2ec9064c64ac148da0259091446b7c520baa32f2ad9eaf57246225e6bd01a726b8413dce00245473c9f8f91f4db3cdd0cbdc643cadad1994fde266ecb8cb9d7c0a72efab8a6011d8c29b9f31fb8ed25d72a63e13ee7b2858707a6c8cb611b57ca79a217ecc52f2a996c54f959cd13dda00148525ece56d5c3f601cb64d8dbe4755a6b6b7811b4525960df0e4bddf59e8b5fb22c8a5f4d66cfd6b3d71a99927f2a6b394f11c08aecad3898859b2a000e8ab8b1d5541b2d1819c1f389d8b58cb14a848cfa17fc5304fc0041857048f9c683ed3a9656777b60d1171903d4e27b7c727c30b55c8c81b571b6998195642d968f78d1d299c23274ecb752241677c78cd2ec822f7f35912ac028db6f125bb8259a2b603626819a4bd1792cdfa31e099533bdf7456d8b73bcf06f38c2d35125433f66a5b6ccb51128b38079168bc6f90e506861bafe3c5bf9a33277d688ec9aecf4b04c59527f6be82d854eaba5d49b79368b435097168c25121f66638fa33228c484352adc69a07955cd29db03fdd5dcb99fbaa9ec9293c1040b1a933da8dd5161e1c165dd6d0b0082c3c077f076fd46991db5726b91f0d360e2d929e698e0d09d57aa83c412bd04db6b6fc3b772d0606f9b8e9b9bd90a1423db27def94d07fdfecf632cbde3cf2e9d3f080801a04e1ec111c4996e7e7c0fb39b03262388afd6f4192852987fa8c39a319680f1a2a059fd58d2a85c9191287d34eedde8ad7b32b4dfb2b17b1737f7085daaadb7d1227dea0c581acf07201aa02014b934fcddfd7834217537f2ab8adafe5b817fe994b49aa8692343cc7fed84e2ee2dc3fe9171ab6694eb65ee2c15e6a463e468e887570b90f0fb016157fb36a3e5c71a55cd46a09af7232129a2a61ad34673c580c3ae990e881337fea4f04c1096718af3d607c4c4923b0f95fb5f15d5df5b8de95871d708aaffe34ccb146d75561d4ba8507d3e5865316a17d84c036870f744d96151098dad673071a39ea8adf73a1793b617de4a65dac6e63f9d962c9e187ebc8d9772f39064198d6abcccfbde981077a7350d8dd82ec3136a8abaade9b8a36e43e96e12aaecf2f03d7be622170caa8359d69c1c2a71bd21b9b24da721cf0c3e7f695d1cab162ad57b48a3a7a01ffbe5723ffa3826d135bc27e05e57029066cfe8c19427fea48b65fc0c7215dbfdc72f1bcb52db4d1c5a01af1384bea939d9855c39b15b504671290d20ed1fd9efa9c879ccd5dd4d8c4d9ea180565318c77a3a6dfb0a16c14b382cfbb64df4590bd2a3b0674ae7f9418a8f28ab0bd3158ac16421bf144c8c894c64931625365dd006ef22c29b93d14c22babee0d1215538d04a4fae6b534d664e8e8326232d4a985ac1e1f49a717c08e04287d774f395ac12fe3c8c1a6eb766fe3ffc0641774d6ee0b8cf2b59c72e86621f2a790e06f0069b234a692211a357b8c22185cd70acc53856e27ce3bb54b83564cd613592aa74e1b647991a5b6e8e75a5f3571da05fd798abc6c130373fb0f0dbec756d02f827567f0d1d381d60538fd3153c66d56b012050782b1b6de7d6d216c33758a2f7d3780eff6d9e3e1a5adf598a54c9b3cb1f3d28f707766fb7276d1d2c39bb40423648ad239c1c5848d74669af5eea35e914993258f4d6aff95cc7f521eeb1d99b6bdae3cac8e786af29d445d078d19465b26e11e427f2deb7f138a8ab630d1d8fdab636c64b1e3241b0a587e8200530d7ef164c7f9674f24d410c2db6cf556e15029834a7f01aa4b563d2e91fe066ab1d89a373f313432f3663fea34caeb0009ab267d81d8bf003ebf727b552fdbe6ab882c8162e4a4cf589c2243fce3b59d6985621b3ca88d3c8aa7fc8e50c52e5e9c61ffc6a2c910d0deaa7a03f50d68652d0c5b0a4a0908637f93257791c2a872f72f37c27912307e53b5e8385c9ebc6ceda46db959ecb37e565a30b7c5352c87520ac6b2542ebb6ec39e5d0107fe2d833023cb8c9d7b1320ce7b15a41cfdb7bc499938b05178facf71e7c52a88a26e9e44af7674c388267c95dd2547cf34848954d73862d06985efa06c1bcc82e12e4f0ee3ad0481c7b32c77226ac6b5f025fe17dd47de3201d3b25b6526faecc51ef230f028de0df3246615056ea4ca0d901db7ed4e9402469785f7b3416ddd66fa747400929cabbe77d685f24c9c488cea6ce9b1d56c6c81d9396bc15d9d194b5dc37d22d852d20b19a74818f9a32c101d1fdf373ceedf33c99cf6ff5ad64d8c09d77d780c87c0bd20c672fc1e91e6d4d055130b4d42744c903f640db33a2f2f8397518269423175a251f51e810007eb7bb95a099d0364acb9c6f96acfdebf569d0c3d572a406e544702ccb9be27036b325cbd7dd75f08fb0ca2db5d8f65291f6d1dcd9dca4ccaf3de92789b2fb5eeac1a91f56b61fe075a81e6499a7cbef9c3e59ba91ed5fed565a80cc307805f1dfbadb5ebb0cf38ae656f3ac4cea27c2de36c88b31e9e2e7bab92af0fb6b8b04b1efba530781cce6e49deca95172e286ecf324cd1785c2a4d5439d9becdd276fd464e97df27554f6b7e508a35151afb06d5c8113920598cb72bbdd6cb48dd3ad6cab309091b84d4524a58c262b713f459228c424e0fc2340f2e6ea8f6de06186f9e6d1392b56eb35e5093475304c915f4b89d05bd05b23290f2ed3338e09725dee38e109eabcd92168db4d319df75b4ec0a4102a9a806a177bb4e8f1b25e841f4a9cc6621373e89c6595e5f51e7cf1b91cfa58c79679cd653782ea375abbd3223ac1b7b36a4450009883a009a2ba0f44920284026e7a2030882d1c62c4bd0d40643fc7717e708bd78b43b71a65af1ee58e973dee3f70dc32c0663e2636ce28f571beaae7c17699ac8ca802bb29ea2b63c5f26c487ca6487072cc0fecf84ebb7c5d088aa00a218d4548c0c519d47495187b0f062b83630218090c1a68a704118528a225e1d52ddb09ab074e2e1a85170175c03107266ec0fe4c0b6ef3dab705a21d96aa989bbee153b35fcd8d6dea8cf925d7f42f89233e11d72facf4017ecb423b2ecef55c74e109c5fe9a85387d58fb8c285c84f8daa6c9be94080799309b12e65a7b3f4ad911e3c281f0b54db5a579964ad742a248a90056e344e2053d5d919114d3e40378a8db5c8423e177b1ee429ebfca4946b013c79d6a743e5448f6b88a4708d399e5114a6e0660a5d7cf354cefd285c1aa9fcb8d25543f51e32b71656e4b249fe60cf248e98f72d1f661e80cfe54511e7fdcd00f46dd2ddb7b3f9aba026bcb68aea6821703b3dd3fb6b64507287fa36e29afa28a2e1c069ba4d21d1c067376c67fff08105d14204ee472076ba990c4f8652cecc1855b1145503e12e3b07b8db10d468c07fc9ea7fea0138f33d3d13e30e16801330caa88c23e961376390cd390975472a3281f12a59992fa356e8bf97e67f71d64ed2dda7df30d27eb507cb29570beb41d47231ff837669683c82bf0753e8bc9b7e8ac8abdde1cd794ffb838bedcf52e12b789c93deb35e4daa109e07c1306433f037f72d83656dce0ffe1672700812602e988162c9295ca31dc6ab8f0d493a918d4b5bcf7ceb95cebeb4edb295db614b327ab28608fb25f24bb144022a87e66ae43eff370eb302de6467dfcfbbb8329a7aea1676519dfe3114c1ea6668eefe085491325d825d4556daaeccf428e069ad24e5a8e113947d6d19ba72d82ea7fbfea3871c077057674e6a3cd04c467b3c32a8d40d212f975c7003ab81be8c2c827eb1c74756cff558dac3cc13f99017bb1604c6f38888d663fb3a7a96685c862f81b2434c84dd6ba083698d3f2dcb9a4a629bff2120c8cfb7e271844d8e57f5f4cacb7b1aa9394f9980b7775ee24487fa46ad100a021017fb222fcd7cdb4ddb80bdeaee7e64bd954d3822510c61bd38294b4dcd55e175ce231c86419a0ad44373870a795174b15cb6e1f6cceb4e63eebc5bdefea5fed59f253af69bea6e033247c1572c40b47e2792f8d34e34be8f7d90f340a8a7afbc1a3fbb555cb76303fc0527fb993df76334b3887fe613b9573ee12dffb1dd7ab6715eeca5f4f28ac9fc71f08d2cbf98c989e5d5bfaf44a0519c8b9925dd42dd2136e90f2278171bc037ef4c4a1121fd4c616aeabe0aaa02970debce08a97454b5c0bad33e91255d0ab7f44b71c9aafdb37b7365d294db5c6175ad7cdc3d396f65a1b76b3fd0388eea8b8c5e0eb8a1750edf4c16582395da5207b56225fb375d25fe21226647e6ddc0141f7a475bab6781a67065b73e4f6ca2469c20c4d0f3fa762a65d6d61f8bb5060666fa3aca71b0ca682853f259490bfa14520cfd136415888f3034bfb928af4ba16831c4420c25c73f6e51083f6d69e48633bde32b396994e51bac0e5395c00600282e6ceeabe622d315ba7d6f8cc5bbdb49e38ef4e6884ee74a2eb6e0b4688f87da223ff954c8b3ce05f858cb8ff5ce63c3450d8ec8583db7387be852975608e409248a9538980ab20ad8da5344ae3855be2188ef086c94861e3db46c2e65f84bf9f965c168c45f4acbd20beec8f0e2183c8a471a4f38b38348dab3900858e050a4f258148e6184610596334d10382d150cfb4ab54e98051509ae67e87ced0ca6b5eb1cff3c4e1866bd35fec02efa96cd688a59faee57ed1f8b95c21d5e6eca42cb2bb1871233529c4af1eabe79ccdbec8f41d7d0f18378e915ceaa8dec08a148c55edc2f0d6e169e83d36d5577d278b424a433fb2ce6fd3a2ddc9c60bd55c3fcf1e97abad7133c90e3578851a9ae898a6faff8b7788edb76df7c43afbe799a519fd864c46a8eb18ab9bef622969e8e82387e19fa551e7c9ec609c0bb248f796287591c8bd70423cef929028f060ab5c34d273be15408f4b474b85537bb4d27868324bfc52428b454e773d8111bb45bcded7fcde6d9acbe2facc6192fa483b2b2096b868a614382d331f756c08dfe70e2910b0f767778545ad5bbb79cac49b79cf990112f2386036fed9435c7972fafcda30418161eaa68a30f527f1300e3b3a1842aec219a015e8cc2a03b5fcfd2b9b3ba782be8834d91490c243b0e0b984276cac82406897338db70fccf15b42a58cab504cf941194c25b29f199adea59954cb41a3578d6bc23bc2beb5e142ddf587246f77f187c105a4176a8bf78a82b068441a2d51a17af44e7241dd313ec6516f8ff6fe3c6f606cfa917de6413fb4bfaefde58eec54469aff9b6fed5d8618ff19f29a827bbf576f05351df5611f8f3d4ed754b5acc13bd7bb1962ea7deadeb02423ca604f63b85132de332349336cf75d4c4652343dd14536707e3fb4d4fffd1393425662016adb4fdf2eb0a24dccf75b24a075d2b230028b08c29822a174edf14f9349d1b2b48e80b5643836f33b199ca535d1a0fceb8f63d0e8cb1281c134456224681789e271add11650763db2712f29f3a23ca1cc845549fd77f3503c7d7f1aaf1b8e5e8cfbda41847e34e27f80cda5625adb730e77c064ff22035bbeeef398af056839ec8198bbdd5e8b4540c07de52568d81d6f7939d7cdcecfee1242d9baf04b71c761cff6a4aa70c32245515134e4f41b999c58529f9746a9c784c1086a5b62d895d710740e1ce5a9cf3e7f39a9c588c9b5e94f0f2b945c55a1d76dfd2c7e723b48de32aa01ab05b45eae5b00c443af52d72ed86a34c9cbc997d7665ab78389428718352abb5fb7853ab0d238b94eea092a94790449abb88fd58efeb8fe047dca4b5dcb91ac8482a03619c407bb562cd6232c3843ebfb61187fefe890de2d81236076a218fd3a6571827831d8d4912c8c4fe00955b5e48a1e89ab946064431a3e0e4bd76ecb445a0866b245e641a3af747dca807da16c355a444ba5e7a663eec995bb347d246e5c70fb23e2017a13f798fb571ee0dfd98da8aaa6a33c4c2eb1b9eb211cbf88059097e89d8e7afc9f45f589ad1b554f12a979b22b1330aabe09396f57d786fc52718dd7afda8f9679d16d13c6e73b0499bacb411b96bc4e1fc61bf7b3add288241b2c2d08fead59c4d6a385f8bcab775827a3fb9848da3ee319cbc1bd615b0bc8f4747cefe72dbbc2264398bc62b4671a00624106c7ab45a9701e75ce03eea712fc2445d9bd01da87e3365d930ed5390732f26785e0801ba593e5f57ab06463e5d4460e240d62d02e3093fe23c87b5b4a4be59fbf391a94a2bccac00ad12a53c3d1ecdb21fa6dde784ab2474a8bf9ffba514d22bbb653cf62f3feb2ab624bce584628f321fd22b7343878f7b371823bea3550d84e1f91bf2b1e4aa6e87b076d1c4fbc8cac0bef6853c074d27a5b925478951db0c69ebb40d65a4411109d88789e745915acf057e2582c30ac8ace8a01f61b2687e8b71d4bedb241feeb3c0b1fe12ee808fe6f981804fc9c495c6c97cdf4c6202a7a021a2036dbde3b5f54d1271e10891485a8c7eca6a5c4d5c0d3b506527ad00107fca053d28f955f1db0d087ba7f4d954b02b8d169587aa7adfd6889573e1935ada68c88846b0c03b66a490167b78978a59ca9936226bf0d687bf7e3f5a89f5042c0711777ab684eeefdfee6747f9a5b788bdee9356fbc2f560f58611506878e0ec2abafa7fe06f8f3d8f8d4386d50fc050e18c74f09f7a36fcf7b110d2ae78cfcac824f029c252477c4aa944d14614990487df7a4ea79c310558eeeba46e608c351989bc22e36134a439f38de9f6cbe9014f23cd6851c6c08a18f983e75dd2c69187c0becf19df8ac969f87c7b44f8882c01521e0bc225b48bdaa3fc321f03babaaefd60ee0728c39b6d821f963145e335efaa0c1c6d4818bbdeacc339690e9fa6cb945a56f55edd0c67cee9a95f585523b37d3efac8abd5ae3a2f5fb71c0a399a6e780cf87159f405a70c61385d8344300bf535f56505e11d3d2aafff43d4e682eb02735f084a8496742f23593459f8a2984b683371d59096206fdc936f356091590b3ad809ffafb2b7f983050839555d3158e057de7733e4d39d5adbb37f0462c80e30e972dccde17b27aaf8237577676bfabf613772e92bbf9185d3b474d215f7279de634849fd6934334fc711857d9fe337cc7e85f1a24d66b649fac998b3792cf7c46161dfe0574b546dcc1a2ad3196511ab130607a1ac1e772b837573890b27702f9fec7c0545eb0df2129050045a21a11645b3a4b7ca18ae6d9c5b4cad0f7f441472413903ad3aef973b00e5ed88962599b51ab025de8fa1e79c251bcae5e15079b73fae17cb647848860e3a80434df4c9c5b99437258a81e869509e54ca037b4b92c6ed99954683ad3d469f4b22aee33eb37b79ad1ec365554da2bf4415e9247f2b74a9f4499e2c6be4db517ab23a0dbf6b6bc33549927d469513c242b03edd77a5d52e384550b56f749abbafe31760d76ad9edf497bb790a98791542efac0f64d5231ecae5ed00a3ea8d498951a1fce4b4b47218d9374f62d38453b1f3e7dc37d27942ee716ea00e7225f6f8fb3a0e378e491b291006962a133e8ecf61e42eccd003efaf873811c7d116b194fb10f8bf646bdff19b563128b2431d5bdc69881ef0f694305f0ff838e931172392acb88c49b7cb07531bf6f13d983cb8e2d5f50c08df71640635936d3213641d935c4f9ef3680f9db88a82a1677319f3e04aa9fe3742c67e5c3484379089a3188249bc1c5f9605b89661f6926c8bf99c11a78858f058e108be900d169ebc1a1e742aa08c7c462703d80c96298502ea7752e257652cc75f4508957b0c9394c4fa96a055b1d371621b3aca4d923d261d6de397280541946e00c6506022822d4738581196e28956c383099bbeaeab78bada79e7b826a81a7fafa6ed714d36c5b8144d0dcf70fa6198b8d2a3493b3dcc82190dd000582ffce3f7638f478b7a144fc15b7312986995433f701ebcc32bd4fbc8c35e84b69aa2a21738a7d7fac15e4cafddec0b9c0b4f09a23f19e13d3fc7903c67ba1f002ce9e22f49b09ed0a6a0106aa1b81471d6dbd046fba8ccf15174150e8f76d77d8ab9d1cf04c08b0ac8e125a001318927699cbe46df994f43320b77435c087de28235a316e4e8c34cd73c5e48775d98824b88b2112443285c32abb0b57cfee39d80f8b458baa7e43361c9202108e352c8d8c51a8ca0c9113b76707073064ba96844c414f3e5c37f1fe3dc8fc7efbb27b0998dba63d39a2ec82ce19b9a3c0b51e46d0018d06cd9c1b886ab49725fcfd65439b57ab1fe05f0b814eb1d1a819dcf64e6919ad4c9fa78bd1133e58076894eea3616c8846af2ebf926a44881580e617a733eeaf8b2ab5269428111f5b8e597fd0e2baf714099a04e73b94917a87bd4d192b1623c63c287a3cf372bea2cf56b916ced14860c14701ffa23ab2b1cba136c758e48dd4bbd263d87dcdc8e1b1480d349974f4eb5e76db2deff85ac0d48ddd02689406058565cf5f010d7493e7e15c204fdcb5d4dc52f7b11f0b43765a8da0485ffa6b75afe5b881a7353a787adf5ea69db7f9d9af48948d123ac70a7d95f0aed1a8a0bd937249602f5a959934f7208205592f73a8e9f1e2791bcc2c299933c1f1fb7b50db2e35d4b69381d6766f1d2519bb5397c690beaedcdd228e2d9f32578648aed03012c5ecb3c8f28b0b581e16cc9ba6be3e8e625beb0dea5985d96d9dcfeec69824a98dfbb76153fee0969d28c4c49bcefb55342fc2435e549ecc8986624513de76f060bf9bcfdf92d0b3248300ae2b4baca3c9ca7610df44f5f58ca4053d6a8aaf1d795aca713ae0002149e9ef9bb6f2f58b6dca8f10fa23595625c2571c18d0fbed47a92613464f192703112953052a27cb5e0ec40807626b5f4d911b928af6423355ecb74044269f3eb7745be427f33afe8cf7d8d6f55557ad09082a75902d5ef846ffd85a93ad806de9ca02eb4e36b557df019e5fd60b5b9ea26792372511dd33f8b5df6c86681d8c722257b9daf593401c67488ef27ffcb3eac186563657d00c82a3ff53938fdb7dbb5b51c4132d633d57c4519811b61e4fe1baf34f846f001e96c0c16a6edcfd6932b88c333aef51bd364f82986292b0c63fdfab6ab7412f67025fa63fe36caba70c95e4fced67d4f45797e1ed858c2fd32b29fcf51c61bbb0f800410f10123867150625fbbcf64a074cda092925110644ac549e1418b85125f1712f338abcb6c8a516b2a66188a7df577eecb474df0e869cce6d3f5e2a8e822da76f5eb12b763168cd9469e62b664db7ddfb867c3fa6bc0653f684cdc6bb0cecddf5c212362dead5c87987429f6b4aaa98415f7ba0a8ec58ae834359f459fa86e988bf214b26b3e8422e7129b3d0809551122be29b1586159549e0366bed0925875552415951ca71d98904c695313aaf631df87566d566c389a999bf0a5226bc1876c46b587aa66b5f26a534336bf740cdf5656b822a81e48b5068bfa345af1b74f7fbb6a2c59b54751fad370b6f2d77c66928fd0df11f1cee75110b132b5e4c9f7086b43fe828868d46e5edbcf834450e0146bfc0b535e102360bec39d420a1ac0d61d613dd7aade39759e6543e8e7de9a925a2d9c6f4ee89d897d98858306d9d0956df041861c4e18705f1a54bb83037c1d67a42d7c717dad762a3f209cba7b1595687613a0b173413f7e4e3605ba491481225c04c5319c1a4e8ddc79c8299d2f08da66e63ee56d2196f17287964c70593f80617d8a48d68014773d05bc81e8cddb65adb217804f19186b493dc9fdfbecc0fb6cd5a69c9d33801c4c1a31873d7b994bd095e1080181f2198648606798e175f0e05ce9a2e1b83c50d424ee2ff081401fd49c41dc6a8f374cee05291b5b08abbd673f0d66f85f30bb5bdd0dac505a90b4a0bc8f1417388c9b6a5d88634b742b648e8bb39ddd7834d00ef34c5d32c43ee3ba9fbe2c54e12833caddb6fba00b246c83cc28905b3cadc0f48558807738ba1fca33f012085663a6f9852c59a6f37bb9de622c062e6bd9a31c38f0dc1c424e66741a0db74dcecf8273fb66e22b3dae9d87b95b3dc9d9e65567cf7123f8e5681371bdf09debe56a4d274df7598ca5df84521383aee23ac8c1a9c1dc3f48f4772249e45a0dc4fd8145068728bfaeb2b477c07437cf49ecb0f6a05c741aef4df3d495b28a23b12cebb12bd6cec8d629556d55147d6ae3ce601b0247f19180fee1c0992224361b515e5358f874ae2770c29b7a92a4cfb481766835b6a6694048d31a64fd2f958d23273570663168382b70d86467df5093ac7e5837b1b9588a84aba349c0539a68b92e81d966737b506ccf9772b606998d6fa05150357f6dcf0d7c38098b2090dea40677420846ac0dfcab70714581ceb0136e876cf212dd73567b33c2d692dda7e1a59d1fa32d2d53534257e2ad02663cd72f87230d829acc172bcb2766bb596b0f35385c02b0761b236a2f8122329bc1e79d26d5a83eb4c6d39827890049f0a52d8b84c885d621de71bdd116b029b8ac561dac8b66136e362ec017ffdd82c1e6643da74f6fa8cd60f743ddeb3c19bb1daf0b0353a716286be8e99c29d91ddbe41227c606f31a48930005bf2d77bbd2ef82e0a439d4545ad4254f7795edd62837f22a1ccd90762cfde79e42f6441e6cff588c37cef73086415da708b848280d4c1dc9af5b44b51f95dfc156bebe7668c7434edad5b93c27bdf5f7928d00d04692ca61ad6cd07be336ba0326a76ac7faa1b2b6a5755ec6fdea96a22a2a4b2269459b40d8828c2166cbb00e61bd55053387b0392eb11e022455efcd5665b052445b715ccde9ace3ee107c021058df727b8532f806e3a9f11f2aa38513e2742d8f907cc412f65fe12bef6304a64693435dff47750a59cf77ad6485c1d7e2f8724d2d2fcf526b0c6fd0da3c6241ff08952812bab9903103cd617e5be020284c3693d45f757ae8e252ef06be91acd206ed9f5e030e1a6180d3519eb559e5c780073b25e4162da06c46cee20533f1247537084e897b5c7177fb7178736c5337afbf3fcb718023bbe3890f0ffeab56349481d18a7e1abb316d9815cd8bac6f55144e8882786e8bb9c6a06929bda7b00fbaed214a36eee1701c73a51b926c2a5b2b8f1a7eef407a97623aeeb4e74660ed5d44f98b67d9d11e9668788a73a477cdcb64e509b960b9f4fcec688bb9efa916fb43bc8b7a7e4c174de79c8b07603954ceac76cb97117f3460676f30a5fa988a2b436e37a814d04a9fe3641c4c21daf2057476e831fc5b9c72b571a2a0b845726062f78fa0f7bba897a7d82cea9f9dbf8489d33546042704dae1c5420491382b14dd5fe7c8b834216eb37128c458989e822f25ee14acd08b1d8c1024b61d7ae6a138f9a50a09b8833100ee60a83ea08f7bade4c6f8d86464b52698ddb89624a655ebd8501839f74d0804cd8697125d4b050cf401eb1002706e21ce8362cb774301343ad940d8bc5e6c6d4e41fa756194cb83b6e019bba349f4bf56d85d2c2c3202a7f1a488b0d7e2fc48170c5291e5b651714d2c8e4e3869498dd9f39e9e33b5cd938a81b6b27a391fd08e350f198d888eb60ddb788a29f99935e988e9145095a816fd4cedf6306102c7c135e6525fce80df748c9d2a81f64b5da870b2aed8388b96a5bae8e257cf117641e17515a8886a4b91b53d38b71b5f991af3bbbb27d0da7d5eb32318cf654a6f43e080c69294a8e59cffc9bfab71b3cb65c37837a38fc3057c8c9666384faaec63c1ba22d9e043798882d9eee8e171c1e030483b3b779e2317b6d90c47739b9f519f0e04fcfc01beb9b0a56f78862e8399cfda86ae38564e1b01d345fbbecb59589d4f867f756ef19fe215961e6069e027a92b881f648a2362cc5579144ec08e65ffe4a017a1f945089b5713dd643a168be98c313f08ae5dff83378fbad2467e8945910db64614e943cd9abf5d0c8e817ce36b3da1759cbe9ed53ffe16adef734316e0f7ecc0398d9bb25e2ff5e2815ff9d77797848481031afd65b07736e41def9c01919538c06007dcab83bfbf024555d6bbf2fb4f276890a27ae61f9302bcf0a04cb670c47dd896754711663acd8c469316df9a47f7a49d0438513e2eaf9ee796069e4531319c26f7bb7534cf2de0cf2f42d22d6171c65938a2fcb62ad69ba9bbc6aecc34f1a733f09876e949b9c2aa7b7ac52019c90663e184606d0b8959fb1ebcda9c17c086e9a249a0f58cb215b27884538dc45231e105965a63e513d883e3f3321d4807c4c3a9ed32e913808bffe015a2d007da21f1317c6fc5c9fad3537c298599a499937ba57495fe8e62746cd7c1f7efb6e1bb055e7f92eafcd8f0bde2437398eba921136f953a1d843075b1a9aca7ff3446ed63d8c1e7b59f48ecea3665bc4a009bf4e435d0bd6b447b9d9f3ebbdb78ce86e02b2f434c78e2f06b78f671d67da27ef622cf3673cb6e3cd174a304ecf0e6f05df6d111f2ec6cdf1f7d95eaed2061d36ed595a19138d58aa4ebab964898304ed053c092a6f8c183b84391a518c29b999666a1a0abb234b8cf7abe00bc2b1b188632338ab732e1257d0eccacfc72adff1d6474959671df167302ce999527b9677fd47077e272fddef5c2ea572ddc001ab0d9c1bef243982bfd6c4b04ac409518e4b702aa92a8527f8df95f7ce6b0743107db99941c3cfea6de5b08718e8a2e5ab2dafdaf8274e4ac365170130ee50b559d1aa96d9529c7c61be846171d024cbb3721b52faeb8e58ac866e1782f3aa815f4bed58df7930d963e65f2bf977a269d52b8a4b8c108ad1fbd2c85409c240688c473658ab008d08d8db8a018e916c90de0d1316a054872bff56d4b4acbc5b6b35e766fde27981ffb34833b505d3d0d87e29ffbe1cc67f5392608d0af23874f4754f8af995dab066a2df21914e0fcfd9c894e97479726e47b4cc506b8ad66346495a7ba8675cad3867690e21d526286b8a8bbdd0cce291d23e2934ed3e3a2590c39ed3814c951849bc06c8c071e82c75b04e539a31ccb9a9899c2f7bb12e6afaca175de4e88ffa964b8f889e824b65af248f2b79107d114a332b14141e549d0e6bfbc9c7f6813ee7ec7f4a6da42cfca749bce9a128fb7cbb2d8f4bee01106bcb181df75e1088909ec7b5f0b82deff5b559495ef9c03565b381552969a0bccaa0dd98136ecb89e547c0639953556c93604118ed5d8216cc19f3518c349e0814e794dcb24bbdc1156e6025c02a777c58d756dd691ea0ee2a66ae943ae58762b92ff02b583eb55efcbb6cf816abd8a0d5f566c94b8cfc0847cb37d930a6503b9b07af8ee135441d872aa63f3bc7fe934b352220bb62650c86b042e46033826eb425edc2feb60aa1b5d3a3c04294aa5648d98fbec0ae67c24ff9c629586a2cdd3954648fd241bdda48bbaa28cac7fbc30d6977b353472d94836bc724654c40e88e8e1e5e50eb536e352398de44f44e871dc166eb40528f0ed6138de6ebe175d2d400a0d44793c36a4fc9cdce74642baf33fbe610bcc61c74b2429a93488d127cd37d5af15664c36adf1557536a688976691160d740b813e97ece28a11d12b970775a92149ab3ffce8b8466a81d8583e04f80a1d7f726816e0add5287533b86209b7594e41f1c9eb61e39fa8f4164416f69589efbb87cdc6fd1167f6e4f9a1dcbe818afe39d0cadd24bc77861b4a66394d9253d06f17640533ce9893b222a53d4bdc7a88d215b3d15af7a64dc52b6b309f57fc79d3107d496bcad2738ff54d5a0a1ce16e82ef02a6c5172295b0ae2e6eda8a5ed86e0fd026fc77746937bfd4f93321fbb6d22dfee9345bb9c1b96ec71be000d5bd9fae793293db6cff5536b9285c2a0b4e091e3cd03da6de40569f594cc60bde75971ac2712c9ba3514c48470682c215f78a73713543d776fb7b01bf5c1a29d5c55cb79e34506980163a51bfe6d77dec5760933e9ae39522e5b30f151a0c731559ae0e70777b78715cec6b7ec3532019dd5365aa741fb35ad8ef1885f1446ff9c0a84d550a3447649ac94c824f90ff19da8d21ba62c77ebd13573c5d72c27bd33c8b496aebacd3e9e9f465c9bd2fc42570432ed71c3b375dafa0807e855d4047a467c48100e38375c4c2d3acb89140592186ccd78324601286f4edc002f9aa9eeeb9c85679cb30741baa33ba0c5052aea4c503200df8da59c1070f2515efed087c7959e8389f35499c2cb5973c3f85df1ff106cfcffa76fe13043447d158293693627b0f6fd8f1644176df961de64f92fcb2c1c9454b5cf9a19d35f3a597aa9f7a45699fe9e17481f451a91cb5b86c193bec42107e0201feed624a27f930ee045237d93187640e8841318b80679a45e3d0a99e8f30023e76420504520678e387d246dadea6ad8bda37468ad5de81ac43cdc1abd0d95e5665ecdfca0506d4b9167b72659b53f5485f82624c4385acbe4bc93f73d6d4be57f241214c3e5a1cdd4e97b48885ca017587d4ab443e95c8f8a3958aa93ebeeca7b4ebb29d168feb0c25f64b9fae418705bc138586ec17e094ed8bee2269925eb50ef307d939d0a0399a7bd7ab723ab63700ad69cf2e8d17917798a9ae94f865c12aaec6e911157b226fdb0da1aa8fb4787db305a319475fd77ce492ab77a3c472a0056884d53b28fdbf1a211b900bea5094f9be52ba3f507fd1a8428316e8b0ed7a81129264fc72ec029fc836bdd008aa1e1d0f4495f959dcbd001a371a858ec4d5a3b2f24b7a87d9e1bdac41161fc0d65259c7c67c4cec44b7af97fb7afb631334d08aa9a9f5fa05ce8448f41a32add145301292eabcca045c2c15a7f3d3cf7ad261475b78737e42fa4e59a6facba2a2c81e2eb123d6fc02f31967ca71779fdd7a4ca03b04c8944d54cf1bf89f33b40222476a4961e326f60c0014cd8253e8950e456858683811f885569cf361b651b7c62ee0edb36aa764d9a24a4ed5c1591d6c95ad1f2842405e1de0d927474bff647d875cd49c57636be78605c7b74ebf6ba8610933d6a23ee3e6f81f1bd99d7b9a3fc3d6f9a861ef1bb6a3b0766e4078f57cdf6626aefdae32b37dc1a78a0e4a4ecab84310baaadd6585e57d884efc9637e6bda0d4f0d3b021460a06613588bafd45ecc45d13dde939a7fdb4dd47f1809d6b558307161539e10ec7187d852cdb9d41ed6d58898019cb2f6201ca8ec99973e2172ffffa4055cb9f735033dbf67da130f7ecd1fdbf5938b8134a6eeaa2f8986f143db83e5b8bd851079606d5d57c7a6e132dcee394d79aef73bae8d925d1e12cf1e092b23d10ef0f5f13e78776529bdacb76f04e68f1658f1dfb8630395206eb291d17b9a83d58f9a891bf893375bb8b1583a87cbee6c5886c0dec5d1f546a9d5ba964ab0c8bd9458b76e895a075b0ee654e263f58570f70a5ad6d888c0d310e5ee22b6b4f496eb17e1dd5f7a31320693c6ec9d8f6cb77895f27708ddb12b057991013360450f9db1091151c7575a6f764d93583d779b1b6480cad94758be647d4cf8794e20c6b5718381e8fce46fed3820e20c4e4c82b8465794b6984adc52596c6827f01e6e9a99db61784e39d1ea23ccdf1b720210a1b26c8a8bff1509bf7f179bf485388286766cc3094176121983c35d01eef78e1c7c67cf7789eb3450b833db4c3526b74ecf5d15d56074d5e788fe8ee99c7e12f823cc10ab1171ecac2d49a2d4ff80dff1ad371a98bf6d37f60b627e16a7fd0b550a98088356cbde17d0cffa78c6375b6d9daa66e3c8d154a685f515e66667961fab09d412ac4c2ea2b2f0e6213e7f6ff2a717a9ebce30fec12d8de7f3cc31f4eb2bfe95031505a3d75a8b8fb42afa4fca8cccffccfe7b15bc6d02699c86d5c0593f4cedcb3d523bc4c97e9fa5f448d03f75f794528e2a792e67948154d80ad2733030323b6dc51220281e61e2159fd3011197b8035da6104369229f6897ddc15338f761e303c81fd4306c7150fa2c24bb64d805cfd84f41b7ea94c874ea5619cd09e4d8b89bacb71da01ec5e4da36718977876b4f9a9dd8c3c956bb76c3828f105456fad1a2df55e0e91baa5c96c4cb0e94f73a57a9f6ca9dfbecf6af0150db77918fab3c4a8afcbfa5cc3f82748955885b74bf31b1404f88fb08bbcbedbc171bfa546a1bc8a7a207e85605dbf36e73b992da2c38006b0db8b6d2a255a2b12e7bba361710ca620ef471ad4e7016759c12efcc6d4816a677834d4ef1b5246bd778d8de3092aafa52c352b478fb2dc7308e42d9c97edfcf838c9b755568dea4428b7b163fa078084e879d84ec0e5a778098761cab7ad1a1b0ca217adfb3be505d587ef8f018893969db88eb8b44dcf64ac529070b33a42fab1f756df6d87e11f4919578631e4e6a92238aad138c0e73e60f4e376c78a411e644c3434d0e82f60bec1d8feb5c3f594d5fe9d6f2083657aabde969dec3f22b6fad6e6b5db7f941232b108f581ef70ef75e769d6848368d874de979bfe25776f0763fffd4e80051cff954b8c103c6dedcc839c410b95f0065b92a8bc53284557ab82115a0eafef47dd022842b8e36403486f1d6d2d819a65ac183c700d204007aaeb484afd70e108a9db278f3ffd116b83a1c1705933c542c08d2ec4aab489c4c658d038fd7d81cccde7780c7f675c596d32c50a93a2a4f1ceb0681e034357b630bc23cabcaa765173d29a4930

2022/5/13
articleCard.readMore

Python 代码规范

Python 编程规范系列大纲 Python 代码规范 flake8 代码检查工具 wemake python style Python poetry 包管理 Python 工具配置 commitizen isort black pylint falkehell pre-commit tox & nox 测试环境管理 Python mkdocs 文档构建 Python pytest 单元测试 Python cookiecutter 项目模板生成工具 Python Github 开源项目维护流程 Github Action pull request git rebase 说明 前言   过去在项目组开发,需要快速迭代,通常都是面向美术编程,这个需要快准狠地解决问题,至于代码怎么写其实是没有任何要求的。   但是这对于长线维护来说简直是灾难,当这种代码越来越多之后,就会变成一堆没人敢碰的屎山代码。   那怎么才能写出可以长期维护的代码呢?   下面我会开一个 Python 编程规范 系列,整理出一整套 Python 的编程规范,以及配套的工具。   这个过程是这小半年来的一次总结,这里诚挚地感谢我的同事 龙浩 ,它教会了很多~ Python 代码规范 谷歌规范 styleguide | Style guides for Google-originated open-source projects Maya Python 开发规范: Python Scripting for Maya Artists | Chad Vernon TA 101: theodox/ta_101: a coding standards doc for technical artists (github.com)   其中我结合自身理解,翻译了 Maya Python 开发规范 和 TA 101,大家可以自行参阅。 文件头部统一写法   我们的代码优先采用 Python3 写法,因此根据谷歌规范,所有的代码文件需要加上下列规范   __future__ 模块用来兼容 Python3 写法   coding:utf-8 兼容 utf-8 编码 1 2 3 4 5 6 7 8 # -*- coding: utf-8 -*- """ module docstring """ from __future__ import division from __future__ import print_function from __future__ import absolute_import   from __future__ import division 可以引入整数相除可以得到小数 (Python2的默认环境是得到整数)   from __future__ import print_function 可以让 print 关键字变为 Python3 的 print 方法,可以在 lambda 里面使用 print   **from __future__ import absolute_import 引入绝对导入   division 会导致 OpenMaya 一些类型的运算符失效 PowerPoint プレゼンテーション (square-enix.com)   原因是引入 division 之后 除法 除法 __truediv__ 而不再是 __div__ 了 backport 兼容库 six - py2 & py3 兼容 Github: benjaminp/six: Python 2 and 3 compatibility library (github.com)   six 库提供了统一的 API 解决了 Py2 Py3 不统一的问题。   比如加入 metaclass 1 2 3 4 #Python2 import abc class TestClass(object): __metaclass__ = abc.ABCMeta 1 2 3 4 #Python3 import abc class TestClass(metaclass=abc.ABCMeta): pass 1 2 3 4 5 # py2 & py3 兼容 import abc import six class TestClass(six.with_metaclass(abc.ABCMeta, object)): pass future 模块 Quick-start guide — Python-Future documentation 1 2 3 from future.standard_library import install_aliases install_aliases() import queue   可以支持大部分名字迁移的库,比如 Python2 里面用 Queue 在 Python3 下用 queue   使用 future 库就可以用 Python3 写法在 Python2 下运行。 Qt.py Qt库兼容 Github : mottosso/Qt.py: Minimal Python 2 & 3 shim around all Qt bindings - PySide, PySide2, PyQt4 and PyQt5. (github.com) Qt.py 的作者是 流程TD 因此影视行业多采用这个,Qt.py 是运行时 Resolve Qt 的库,因此不太兼容 pyinstaller 打包之类依赖静态分析的库,要解决这个问题可以用 qtpy 库 (qtpy 是多文件 Qt.py 是单文件) Qt 的 Python Binding 因为一些历史瓜葛,导致拆分出了两个可用的库 PyQt & PySide PyQt 商用付费,PySide 商用免费 两者都是 Qt C++ 封装暴露到 Python 库,使用上大部分的代码都是能够兼容的。 需要注意的部分差异有信号槽区别 1 2 3 4 5 from PyQt5 import QtCore signal = QtCore.pyqtSignal() from PySide2 import QtCore signal = QtCore.Signal() MayaC++ Qt 版本PyQtPySide 2014+ 信息源Qt4PyQt4PySide 2017+ 信息源Qt5PyQt5PySide2 未支持Qt6PyQt6PySide6 Dealing with Maya 2017 and PySide2 · Fredrik Averpil 需要注意 PySide 升级到 PySide2 的 API 由以前的两个模块拆分成了三个 (QtCore QtWidgets QtGui) 官方推荐下列的代码解决问题 信息源 1 2 3 4 5 6 7 8 9 10 11 try: from PySide2.QtCore import * from PySide2.QtGui import * from PySide2.QtWidgets import * from PySide2 import __version__ from shiboken2 import wrapInstance except ImportError: from PySide.QtCore import * from PySide.QtGui import * from PySide import __version__ from shiboken import wrapInstance 但是上面的代码需要 * 导入,并不符合我们的代码规范 使用 Qt.py 就可以轻松解决问题 1 2 3 4 from Qt import QtCore from Qt import QtGui from Qt import QtWidgets from Qt.QtCompat import wrapInstance 老版本的 Maya 使用 PySide 也会被映射到 PySide2 的调用规范上。 black 代码格式化 VScode 配置 black 工具 使用的 Python 必须 pip install black Pylint 代码提示 VScode 配置 Pylint 工具 该选项默认是启用的 只要启用 Python 安装过 Pylint (pip install pylint) docstring 规范 docstring有四种通用的标注规范 Epytext reST Google Numpy 四种规范的样式 sphinx.ext.napoleon 支持将 Google 和 Numpy 转换成 reST 建议做到所有的函数都进行 docstirng 注释 VScode 自动生成 docstring Python Docstring Generator - Visual Studio Marketplace 安装 VScode 插件 设定可以修改 Docstring 的生成格式。 默认生成快捷键为 ctrl+shift+2 代码 review 仓库设置必须经过 review 才能合并 review 代码可以大家共同成长,统一代码规范。 tox THM中的tox命令行大全 - 腾讯iWiki (woa.com) 龙浩提供的 thm 仓库会提供 open-dev-shell.cmd 脚本,需要本机安装 thm 启动可以进入 thm 的命令行开发环境,配备了多个中心化部署的工具 利用上面的 tox -a 可以查看龙浩提供的 tox 配置命令。 使用 tox -e pkg-py 可以初始化当前仓库,生成 package.py 和 setup.py 等一系列配置文件。 可以使用 tox -e ide-code 使用中心化的 vscode 打开当前仓库 默认 package.py 已经配置好 docs 和 单元测试 等诸多环境。 环境变量利用 rez 的规则添加到 commands 函数里面。 日常上传代码前使用 tox -e preflight 会运行 pre-commit,black和isort 标准化所有的代码,也能提前发现一些文件错误。 后续使用 git add <文件> 命令将要提交的文件添加到 git 记录里。 commit 步骤 tox -e commit添加提交信息。(这样提交信息有统一规范) 参考链接 发布前可以使用 tox -e build-test <版本号> 测试是否可以正常发布 thm packages 如果 build-test 通过可以使用 tox -e build <版本号> 来发布到 thm 中心化云端上 单元测试 Unit Testing in Maya | Chad Vernon Unit Testing in Maya (Part 2) | Chad Vernon 可以使用 龙浩 配置好的 tox 进行单元测试 Qt 单元测试 pytest-qt — pytest-qt documentation sphinx 文档生成 使用 Sphinx 撰写技术文档并生成 PDF 总结 - 简书 (jianshu.com) 使用 Markdown 编写 Sphinx 文档 使用 龙浩 的 _build_docs 命令可以自动生成文档。 Poetry 依赖管理 Poetry | PYTHON 打包和依赖管理变得简单 (qq.com) 基于 龙浩 提供的 thm(rez) 流程,不是十分需要 poetry。 Sentry 错误追踪 Sentry | 应用程序监控和错误跟踪 (qq.com) typing 静态类型检测 python/mypy: Optional static typing for Python 3 and 2 (PEP 484) (github.com)

2022/5/9
articleCard.readMore

Python - Import 机制

前言   你是否也会为 reload Python 的模块干到烦恼。   需要在不同的脚本加上 reload 导入的模块确保可以看到代码的更新。   Python 是怎么缓存 import 的模块的。 TLDR;   我后来了解了 Python 的加载机制之后弄了一个函数,只要将我们开发的包命名加上,就可以实现整个开发包 reload 。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def module_cleanup(module_name): """Cleanup module_name in sys.modules cache. Args: module_name (str): Module Name """ if module_name in sys.builtin_module_names: return packages = [mod for mod in sys.modules if mod.startswith("%s." % module_name)] for package in packages + [module_name]: module = sys.modules.get(package) if module is not None: del sys.modules[package] # noqa:WPS420 # NOTES(timmyliang): 这个操作等同于对 test_module 下所有的 module 进行 reload module_cleanup("test_module")   如果我们的 test_module 下有众多脚本就不需要逐个去添加 reload 了。   万一不小心把 reload 发布出去了也会稍微降低脚本运行的性能。 Python Import https://docs.python.org/3/reference/import.html   上面是 Python 的官方文档讲述 Python的 import 的时候背后的运行机理,也可以切换成中文进行阅读。   这里我将上面的文章结合自己的实践总结一番。   Python import 模块可以用关键字 import 或者 importlib.import_module() 备注: 关键字调用无法放到 lambda 函数里面,这也是为什么 Python2 下默认 print 无法放入 lambda 里面, python3 print 不再是关键字可以放入 lambda   使用 import 关键字其实背后执行的是 __import__() 内置方法。   import 触发之后会从 sys.modules 查找缓存,找不到就从 sys.path 里面匹配模块 (这个过程也会触发 meta_path 等触发自定义的 import 行为)   找到匹配的模块就会创建模块 否则 raise ModuleNotFoundError   生成的模块会放入到 sys.modules 进行缓存。 import 执行操作(不考虑自定义 import 情况) 从 sys.modules 查找模块缓存 从 sys.path 匹配脚本 生成模块 放入 sys.modules 缓存 sys.modules   由于 sys.modules 的缓存机制,Python 下次导入就从已经加载的缓存中获取模块,导致模块用的还是旧的代码逻辑。   相应的也可以修改 sys.modules 的字典实现骚操作 1 2 3 4 5 import sys sys.modules['a'] = 1 import a print(a) # 打印 1   当然这种骚操作不推荐使用就是了。   另外还有一些危险的操作,比如 del sys.modules["builtins"] 会让 Python 变得不正常(:з」∠) 1 2 3 4 5 del sys.modules["builtins"] map # Traceback (most recent call last): # File "<stdin>", line 1, in <module> # RuntimeError: lost builtins module   基于这个原理,如果将缓存清理了,下次 Python import 就会重新加载这个模块,实现 reload 的效果。   我最初也是在 mGear 的代码里面学习它们的 reload 方法学习到的。   它背后实现的代码就是 del sys.modules["mgear"] 等相关的模块 https://docs.python.org/3/reference/import.html#the-module-cache   根据官方文档的说明,如果一个大模块下有很多子模块,都是单独键值缓存的。   所以要 reload 所有的子模块需要编译键值将匹配的都删除掉。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def module_cleanup(module_name): """Cleanup module_name in sys.modules cache. Args: module_name (str): Module Name """ if module_name in sys.builtin_module_names: return packages = [mod for mod in sys.modules if mod.startswith("%s." % module_name)] for package in packages + [module_name]: module = sys.modules.get(package) if module is not None: del sys.modules[package] # noqa:WPS420 # NOTES(timmyliang): 这个操作等同于对 test_module 下所有的 module 进行 reload module_cleanup("test_module")   这个就是我整理的遍历所有匹配的模块进行缓存删除的函数,sys.builtin_module_names 通过规避对内置模块的清理。   这样源代码不需要添加 reload ,我们只在开发用的调试脚本添加这个函数执行 reload 即可。   另外有一个小小注意点,用这个删除缓存的方式 reload 会将之前的 module 删除生成新的 module 对象,但是如果用 reload 的话是沿用之前的 module 对象。   目前我实践上还没遇到过因为这个导致出现问题的情况。 packages 命名空间包 https://packaging.python.org/en/latest/guides/packaging-namespace-packages/   按照上面链接提供的目录结构 1 2 3 4 5 6 7 8 9 10 11 12 mynamespace-subpackage-a/ setup.py mynamespace/ subpackage_a/ __init__.py mynamespace-subpackage-b/ setup.py mynamespace/ subpackage_b/ __init__.py module_b.py   然后就可以 from mynamespace import subpackage_b from mynamespace import subpackage_a   用同一个 mynamespace 包导入两个不同路径的模块。   但是上面的链接也提到 命名空间包并不适用所有的情况,反而是用前缀包会更好。 模块遍历查找 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import pkgutil import xml for finder,name,ispkg in pkgutil.walk_packages(xml.__path__,xml.__name__+'.'): print(finder,name,ispkg) # 输出如下 # FileFinder('C:\\tools\\Anaconda3\\lib\\xml') xml.dom True # FileFinder('C:\\tools\\Anaconda3\\lib\\xml\\dom') xml.dom.NodeFilter False # FileFinder('C:\\tools\\Anaconda3\\lib\\xml\\dom') xml.dom.domreg False # FileFinder('C:\\tools\\Anaconda3\\lib\\xml\\dom') xml.dom.expatbuilder False # FileFinder('C:\\tools\\Anaconda3\\lib\\xml\\dom') xml.dom.minicompat False # FileFinder('C:\\tools\\Anaconda3\\lib\\xml\\dom') xml.dom.minidom False # FileFinder('C:\\tools\\Anaconda3\\lib\\xml\\dom') xml.dom.pulldom False # FileFinder('C:\\tools\\Anaconda3\\lib\\xml\\dom') xml.dom.xmlbuilder False # FileFinder('C:\\tools\\Anaconda3\\lib\\xml') xml.etree True # FileFinder('C:\\tools\\Anaconda3\\lib\\xml\\etree') xml.etree.ElementInclude False # FileFinder('C:\\tools\\Anaconda3\\lib\\xml\\etree') xml.etree.ElementPath False # FileFinder('C:\\tools\\Anaconda3\\lib\\xml\\etree') xml.etree.ElementTree False # FileFinder('C:\\tools\\Anaconda3\\lib\\xml\\etree') xml.etree.cElementTree False # FileFinder('C:\\tools\\Anaconda3\\lib\\xml') xml.parsers True # FileFinder('C:\\tools\\Anaconda3\\lib\\xml\\parsers') xml.parsers.expat False # FileFinder('C:\\tools\\Anaconda3\\lib\\xml') xml.sax True # FileFinder('C:\\tools\\Anaconda3\\lib\\xml\\sax') xml.sax._exceptions False # FileFinder('C:\\tools\\Anaconda3\\lib\\xml\\sax') xml.sax.expatreader False # FileFinder('C:\\tools\\Anaconda3\\lib\\xml\\sax') xml.sax.handler False # FileFinder('C:\\tools\\Anaconda3\\lib\\xml\\sax') xml.sax.saxutils False # FileFinder('C:\\tools\\Anaconda3\\lib\\xml\\sax') xml.sax.xmlreader False   通过 pkgutil.walk_packages 可以遍历一个模块所有的子模块。   from setuptools import find_packages 也可以实现类似的功能   但是 find_packages 面对命名空间模块不好使,但是 walk_packages 好使。(原因是 find_packages 通过 os.walk 去查找路径的)   也可以通过这个方式将对应模块的缓存进行删除~ 判断模块是否存在 1 2 3 4 5 6 def importable(module_name) try: __import__(module_name) return True except ImportError: return False   过去判断一模块是否可以 import 通常使用异常进行处理。   其实 pkgutil.find_loader 也可以返回模块是否可以 import 1 2 3 4 import pkgutil loader = pkgutil.find_loader("maya") has_maya = loader and loader.load_module("maya") print(has_maya) # 如果存在返回 maya 库,不存在返回 None   上面的方式就不需要用 exception 进行处理。(find_loader 的源码已经有 exception 的逻辑)   如果模块可以导入会返回对应的 loader,使用 load_module 可以进行加载。 注: py2 的 load_module 必须要传参。 自定义 import 行为   除了 sys.path 通过系统路径查找 python 包进行加载之外。   Python 还有 sys.meta_path 存储一系列 Finder 类 (Py3还需要 Loader 类) 来自定义 import 逻辑。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import sys import types class CustomFinder(object): def __init__(self): self.submodule_search_locations = [] self.has_location = False self.origin = None def create_module(self, spec): return self.load_module(spec.name) def exec_module(self, module): """Execute the given module in its own namespace This method is required to be present by importlib.abc.Loader, but since we know our module object is already fully-formed, this method merely no-ops. """ def find_spec(self, fullname,*args): self.name = fullname self.loader = self return self.find_module() # NOTES(timmyliang): compat with Python2 def find_module(self,*args): return self def load_module(self, fullname): module = sys.modules.get(fullname) if module: return module new_module = types.ModuleType(fullname) sys.modules[fullname] = new_module new_module.__name__ = fullname new_module.__loader__ = self return new_module if __name__ == "__main__": sys.meta_path.append(CustomFinder()) import myapp print(myapp) # Py3: <module 'myapp' (<__main__.CustomFinder object at 0x000002A2A2904808>)> # Py2: <module 'myapp' (built-in)>   上面的代码实现了 py2 py3 的 Finder 兼容。   可以实现加载任意名称的模块都能成功返回而不会引发 ImportError   当然这种操作如果用到项目里面,肯定会被人打死 😄   在 Py2 环境下 Finder 需要实现 find_module 和 load_module 方法   Py3 环境可以参考下面的链接。 https://stackoverflow.com/a/58275573/13452951   需要有 Finder 需要实现 find_spec 返回 ModuleSpec 类,这个类需要有 Loader 进行加载逻辑   官方提供的 zipimport.zipimporter 在 Py2 下是 Finder ,在 Py3 下是 Loader。   可以从下面官方文档的类方法中看出来。 https://docs.python.org/2.7/library/zipimport.html?highlight=zip#module-zipimport https://docs.python.org/3.10/library/zipimport.html?highlight=zip#module-zipimport   通过需改 import 机制,可以实现很多黑科技,但是推荐使用侵入性较小的使用方式。   这个机制可以让某个模块虚空导入而不报错,这不符合正常使用 Python 的逻辑,可能会让团队其他人很懵逼的。   如果某个 BUG 是因为这个机制导致的,其他人又不熟悉这块的话,那这问题查半天也不一定有结果 😢   这种黑科技的方式无法支持 mypy 类型检测和回溯,倒是可以做一些代码桩来实现提示,但不是很推荐。 总结   本次深入浅出地学习了 Python Import 的各种底层逻辑。   以后有机会的话也想好好学习一下 CPython 的底层实现。

2022/4/15
articleCard.readMore

Python doit 库

前言   代码开发的过程中可能遇到一些情况想要通过 代码 来自动执行命令行生成一些东西的情况。   如果不使用框架进行管理,这些代码脚本就很零碎地散落在各个地方。   因此就找到这个框架可以很方便管理多个任务,实现 Github 地址 官方说明文档 doit 的基本用法   在 doit 执行命令的地方添加一个 dodo.py 的脚本   doit 会去读取 dodo.py 里面命名开头为 task_ 的方法作为执行的命令。 1 2 3 4 5 6 7 8 9 10 11 def task_hello(): """hello""" def python_hello(targets): with open(targets[0], "a") as output: output.write("Python says Hello World!!!\n") return { 'actions': [python_hello], 'targets': ["hello.txt"], }   比如添加上面的方法到 dodo.py 里面   执行 doit list 可以罗列出当前的可执行的命令 1 2 3 4 F:\thm_git\adam_pose_editor>doit list hello hello F:\thm_git\adam_pose_editor>doit hello . hello   执行 doit hello 就会在 dodo.py 缩在目录下输出一个 hello.txt 的文件。   这个就是 doit 的基本用法。 dodo.py 配置 https://pydoit.org/configuration.html   可以使用 doit -f xxx/dodo.py 配置 dodo.py 的路径   也可以使用 pyproject.toml 进行配置 1 2 [tool.doit] dodoFile = "scripts/dodo.py" task 配置   dodo.py 的 task 支持导入   只要是 task_ 前缀的方法就会自动识别。   也可以给函数添加 create_doit_tasks 属性,这样就可以自动生成了。 文档链接   利用这些机制,我搞了一个装饰器可以给 task 添加一个短名的方案。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 def add_short_name(short_name): """Doit for short decorator. Args: short_name (str): short alias name. Returns: callable: decoartor function. """ def decorator(func): globals()["task_{0}".format(short_name)] = func # noqa: WPS421 return func return decorator @add_short_name("pf") def task_preflight(): """Run pre commit for all files. Returns: dict: doit config. """ command = ["poetry", "run", "pre-commit", "run", "-a"] return {"actions": [command], "verbosity": 2}   这样运行 doit 会识别到两个 task ,可以分别通过 doit pf 或者 doit preflight 触发指令 1 2 3 >doit list pf Run pre commit for all files. preflight Run pre commit for all files.   但是默认排序是按命名来的,如果命令很多就会混在一起 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 >doit list b Run black format all python files. black Run black format all python files. d Run mkdocs serve. dd Run mike to deploy docs. docs Run mkdocs serve. docs_deploy Run mike to deploy docs. f Run `black` `isort`. format Run `black` `isort`. i Run isort format all python files. isort Run isort format all python files. l Run flakehell lint for all python files. lint Run flakehell lint for all python files. m Run mike serve. mike Run mike serve. pf Run pre commit for all files. preflight Run pre commit for all files. pt Run pytest. pytest Run pytest.   可以使用 doit list –sort=definition 的方式让排序变成创建顺序。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 >doit list --sort=definition f Run `black` `isort`. format Run `black` `isort`. pf Run pre commit for all files. preflight Run pre commit for all files. b Run black format all python files. black Run black format all python files. i Run isort format all python files. isort Run isort format all python files. l Run flakehell lint for all python files. lint Run flakehell lint for all python files. pt Run pytest. pytest Run pytest. d Run mkdocs serve. docs Run mkdocs serve. m Run mike serve. mike Run mike serve. dd Run mike to deploy docs. docs_deploy Run mike to deploy docs.   但是每次使用都要加一个参数配置,那是相当的麻烦。   我们可以利用 DOIT_CONFIG 进行配置 文档链接 1 2 3 DOIT_CONFIG = { "sort": "definition", } task group   可以使用 task_dep 的方式执行多个定义好的 task 文档链接 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import glob DIR = os.path.dirname(__file__) PY_FILES = glob.glob(os.path.join(DIR, "**/*.py"), recursive=True) @add_short_name("f") def task_format(): """Run `black` `isort`. Returns: dict: doit config. """ return {"actions": None, "task_dep": ["black", "isort"]} @add_short_name("b") def task_black(): """Run black format all python files. Returns: dict: doit config. """ command = ["poetry", "run", "black"] + PY_FILES return {"actions": [command], "verbosity": 2} @add_short_name("i") def task_isort(): """Run isort format all python files. Returns: dict: doit config. """ command = ["poetry", "run", "isort"] + PY_FILES return {"actions": [command], "verbosity": 2}   通过上面的配置就可以快速给所有的 python 脚本运行 black 和 isort task 传参 文档链接 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 def gen_api(api): """Generate API docs. Args: api (bool): flag to generate docs Returns: str: running command """ # NOTES(timmyliang): remove reference api rmtree(os.path.join(DIR, "docs", "reference"), ignore_errors=True) script_path = os.path.join(DIR, "docs", "gen_api_nav.py") api_command = " ".join(["poetry", "run", "python", script_path]) serve_command = " ".join(["poetry", "run", "mkdocs", "serve"]) return f"{api_command} & {serve_command}" if api else serve_command @add_short_name("d") def task_docs(): """Run mkdocs serve. Returns: dict: doit config. """ return { "actions": [CmdAction(gen_api)], "params": [ { "name": "api", "short": "a", "type": bool, "default": False, "inverse": "flagoff", "help": "generate api docs", }, ], "verbosity": 2, }   通过 params 定义传入的参数,就可以控制 mkdocs 是否自动生成 api 的 markdown 脚本。 总结   目前我使用上面的写法已经很够用了,其实它还有很多其他的配置可以用来做 C 编译。   还可以定义 task 依赖 和 文件依赖,确保 task 的执行顺序。   整体而言,doit 是个非常简单而是用的框架,配置 tox 等工具可谓是锦上添花。

2022/3/28
articleCard.readMore

Python dependencies 库

前言   在 Java Spring Boot 等等的后端领域,会大量使用依赖注入的方式来简化复杂的设计模式。   实现参数的自动化注入。   这些设计方式在 Python 的世界里使用不多,因为 Python 语言足够灵活。   倘若需要开发复杂的框架,使用 依赖注入 框架可以简化很多代码。 Github 地址 官方说明文档 依赖注入解决的问题 参考文章   在日常开发中,我们的方法调用可能会越来越深。 1 2 3 4 5 6 7 8 9 10 def create_robot(robot_name): create_robot_hand() def create_robot_hand(): create_robot_finger() def create_robot_finger(): print("create_robot_finger")   上面是一个简单的机器人创建调用函数。   调用方式会伴随则系统的复杂程度逐层深入。   到了 create_robot_finger 深度的时候,可能会需要在上层传入参数控制 finger 的数量 1 2 3 4 5 6 7 8 9 def create_robot(robot_name,finger_num=10): create_robot_hand(finger_num=finger_num) def create_robot_hand(finger_num=10): create_robot_finger(finger_num=finger_num) def create_robot_finger(finger_num=10): print("create_robot_finger finder_number:{0}".format(finger_num))   这需要将参数补充到 调用链条 的每一个函数当中。   如果只是上面的 三层 调用深度,那可能手动修改维护还不是什么问题。   但倘若调用深度很深,那这个代码修改量就会非常庞大。   不利于代码的扩展和维护。   在 Python 的世界里,解决这个问题的方法有很多。 导入 配置 模块,外部获取参数配置 面向对象 注入依赖,从实例化中获取参数配置 方案一 导入模块 1 2 3 4 5 6 7 8 """settings.py""" from __future__ import division from __future__ import print_function from __future__ import absolute_import ROBOT_FINGER_NUM = 10 1 2 3 4 5 6 7 8 9 10 11 import settings def create_robot(robot_name): create_robot_hand() def create_robot_hand(): create_robot_finger() def create_robot_finger(): print("create_robot_finger finder_number:{0}".format(settings.ROBOT_FINGER_NUM))   通过模块的方式将参数转移到外部,进行配置。   这个做法可以解决参数传递的问题。   缺点就是参数管理会比较麻烦,通常是将全局配置的参数都放到一个文件方便集中管理。   但是这样会导致不同的逻辑调用的参数都会塞到一个文件里面,并不是十分整洁。 方案二 注入依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 from dependencies import Injector import attr @attr.s class Robot(object): finger_num = attr.ib(default=10) def create_robot(self,robot_name): self.create_robot_hand() def create_robot_hand(self): self.create_robot_finger() def create_robot_finger(self): print("create_robot_finger finder_number:{0}".format(self.finger_num)) class Container(Injector): finger_num = 10 robot = Robot Container.robot.create_robot("robot name") # 打印 create_robot_finger finder_number:10 # `dependencies` 的实现等价于下面的代码 robot = Robot(finger_num=10) robot.create_robot("robot name")   使用 dependencies 库实现依赖注入,自动将容器内的数据填充到 类的实例化过程中。   通过类的属性实现参数传递。 dependencies 介绍   通过上面的案例可以看到。   dependencies 可以自动实例化类,填充类初始化需要的参数。   但它的功能还远不止这么简单。   它还可以实现多个类实例化的自动填充,只要参数变量名命名配置好即可。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 from dependencies import Injector import attr @attr.s class Robot(object): servo = attr.ib() controller = attr.ib() settings = attr.ib() di_environment = attr.ib() def run(self): print("controller di_environment",self.controller.di_environment) print("self di_environment",self.di_environment) print("settings threshold",self.settings.threshold) print("servo threshold",self.servo.threshold) @attr.s class Servo(object): threshold = attr.ib() @attr.s class Controller(object): di_environment = attr.ib() @attr.s class Settings(object): threshold = attr.ib() class Container(Injector): threshold = 1 di_environment = "production" robot = Robot servo = Servo settings = Settings controller = Controller Container.robot.run() # 打印: # controller di_environment production # self di_environment production # settings threshold 1 # servo threshold 1   通过 dependencies 可以根据属性命名自动填充多个类的参数数据。   container 的逻辑等价于下面的代码 1 2 3 4 5 6 7 8 9 10 11 12 threshold = 1 di_environment = "production" servo = Servo(threshold) settings = Settings(threshold) controller = Controller(di_environment) robot = Robot(servo,controller,settings,di_environment) robot.run() # 打印: # controller di_environment production # self di_environment production # settings threshold 1 # servo threshold 1   但是 dependencies 库根据参数的命名自动实例化对象,参数的调整变得简单可控。 dependencies 实现 caller 方法 参考文章   利用 依赖注入 可以分离 依赖 和 业务 逻辑 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import attr from dependencies import Injector class Editor(object): def install_language(self,lang): print("install language:{0}".format(lang)) editor = Editor() @attr.s(frozen=True, slots=True) class ChangeLanguage(object): editor = attr.ib() def __call__(self,lang): self.editor.install_language(lang) class Container(Injector): editor = editor change_language = ChangeLanguage Container.change_language("en_US") # 打印: install language:en_US   利用 dependencies 可以构建出 caller 对象。   caller 虽然用类构建,但是调用方式和方法一致,可以方法需要用到的依赖用类实例化的方式进行注入。   实现依赖和传参的分离。 总结   依赖注入可以很好解决函数调用过深的问题,让代码结构更加清晰。

2022/3/28
articleCard.readMore

Python blinker 库

前言   Qt 内置了非常棒的 信号槽的函数。   可以让 UI 进行异步调用。   但是有些时候,并不想依赖 Qt 框架同时又能实现信号槽的功能。   这里可以使用 blinker 库来完成。 Github 地址 官方说明文档 blinker 基本用法 1 2 3 4 5 from blinker import signal,Signal initialized = signal('initialized') initialized is signal('initialized') sig = Signal()   可以使用匿名信号槽,也可以使用带名称的信号槽。 1 2 3 4 5 6 7 8 9 from blinker import signal send_data = signal('send-data') @send_data.connect def receive_data(sender, **kw): print("Caught signal from %r, data %r" % (sender, kw)) return 'received!' result = send_data.send('anonymous', abc=123) print(result) # 打印 [(<function receive_data at 0x000002A3328D4DC8>, 'received!')] # 打印 Caught signal from 'anonymous', data {'abc': 123}   可以用装饰器的方式连接信号槽   触发信号槽使用 send 方法   并且信号槽执行完可以拿到函数触发的返回值。 1 2 3 4 5 6 7 8 9 from blinker import signal dice_roll = signal('dice_roll') @dice_roll.connect_via(1) @dice_roll.connect_via(3) @dice_roll.connect_via(5) def odd_subscriber(sender): print("Observed dice roll %r." % sender) result = dice_roll.send(3)   另外一个特点就是可以根据触发的参数去触发相应注册的函数。   Qt 因为要使用 C++,这种注册方式会非常麻烦。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 from blinker import signal initialized = signal("initialized") @initialized.connect def initialize_call1(): print("initialize_call1") @initialized.connect def initialize_call2(): print("initialize_call2") @initialized.connect def initialize_call3(): print("initialize_call3") for key, weakref in initialized.receivers.items(): func = weakref() func() # 打印: # initialize_call1 # initialize_call2 # initialize_call3   通过信号槽的 receivers 方法可以获取到注册到信号槽的所有函数。 总结   这个库可以摆脱 Qt 的依赖实现函数的异步调用。   如果是 Qt 的环境建议还是使用 Qt 内置的 信号槽,这样可以支持 Qt 的多线程等处理。   但如果是 Python 环境下想要摆脱 Qt 的依赖,则推荐 blinker 来完成信号触发。   blinker 还有个好处是可以获取到注册的函数列表,而 Qt 基于 C++ 的并没有提供这个功能,只能通过 Meta 对象来判断这个信号槽是否有函数连接。 参考实现

2022/2/28
articleCard.readMore

Python marshmallow 库

前言   使用 Python 经常需要将一些数据序列化存储到本地   同时又想要反序列化将本地的 json 数据转换为对象。   通常的解决方案是使用数据库的 orm 方案,用 orm 对象来同步数据库。   数据全部附着在 orm 上,当 orm 上的数据改变时直接修改到数据库上。   但是在我的工作使用场景中,Data Centric 的流程更为推崇,因此输出一个 json 文件会更好一点。   那么 marshmallow 库就是一个很不错的选项。   另外这个库可以和 之前提到的 attrs 库可以结合使用。 文章 Github 地址 官方说明文档 什么是序列化 什么是 orm   序列化就是将代码对象转换为纯数据进行存储   反序列化就是将纯数据重新转换为 代码对象   代码对象可以拥有特定的方法,可以直接触发对数据的处理。   orm 全称是 Object-relational Mappers   通常是一个定义了对象实例化规则的类。   通过操作这个类的实例就可以用代码的方式将数据进行互相转换。   上面的图片就是传统 orm 实现的效果,可以用 orm 对象来执行 sql 语句从而简化数据库同步的操作,同时也增加了代码的安全性。   这个操作实现了内存到硬盘桥梁,管理更加清晰方便。 marshmallow 介绍 marshmallow 基本用法   和其他 orm 库一样,marshmallow 需要定义 Schema 类作为数据约束。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import attr @attr.s class Album(object): title = attr.ib() artist = attr.ib() @attr.s class Artist(object): name = attr.ib() # NOTE 生成 Python 对象 bowie = Artist(name="David Bowie") album = Album(artist=bowie, title="Hunky Dory") from marshmallow import Schema, fields # NOTE 定义 Schema 来约束数据转换 class ArtistSchema(Schema): name = fields.Str() class AlbumSchema(Schema): title = fields.Str() artist = fields.Nested(ArtistSchema()) # NOTE 通过 Schema 将对象转换为字典 schema = AlbumSchema() result = schema.dump(album) print(type(result)) # <class 'dict'> print(result) # {'artist': {'name': 'David Bowie'}, 'title': 'Hunky Dory'} result = schema.dumps(album) print(type(result)) # <class 'str'> print(result) # '{"artist": {"name": "David Bowie"}, "title": "Hunky Dory"}' album = schema.loads(result) print(type(album)) # <class 'dict'> print(album) # {'artist': {'name': 'David Bowie'}, 'title': 'Hunky Dory'}   通过 Schema 定义好数据对象的转换方式。   dump 可以将对象数据转换为字典,dumps 则是转换为 字符串   load 可以将字典转换为对象(默认是字典,需要额外的处理才可以),loads 可以将字符串转换为对象。 反序列化为对象 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import attr from marshmallow import Schema, fields, post_load @attr.s class Album(object): title = attr.ib() artist = attr.ib() @attr.s class Artist(object): name = attr.ib() class ArtistSchema(Schema): name = fields.Str() class ArtistSchema(Schema): name = fields.Str() @post_load def make_artist(self, data, **kwargs): return Artist(**data) class AlbumSchema(Schema): title = fields.Str() artist = fields.Nested(ArtistSchema()) @post_load def make_album(self, data, **kwargs): return Album(**data) bowie = Artist(name="David Bowie") album = Album(artist=bowie, title="Hunky Dory") # NOTE 通过 Schema 将对象转换为字典 schema = AlbumSchema() result = schema.dumps(album) album = schema.loads(result) print(album) # Album(title='Hunky Dory', artist=Artist(name='David Bowie')) print(album.title) # Hunky Dory print(album.artist) # Artist(name='David Bowie') print(album.artist.name) # David Bowie   通过加入 post_load 装饰器可以将字典数据做进一步的转换。   使用 attrs 库就不需要在 __init__ 函数中写入大量传参和初始化数据的信息了。 嵌套 Schema 官方文档   通过 fields.Nested 的方法定义嵌套的对象,从而序列化和反序列化可以复用 Schema。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import attr from marshmallow import Schema, fields @attr.s class Book(Dict): title = attr.ib() author = attr.ib() @attr.s class Author(Dict): name = attr.ib() books = attr.ib() potter = Book("potter", "JK") JK = Author("JK", [potter]) potter.author = JK class BookSchema(Schema): title = fields.Str() author = fields.Nested("AuthorSchema", only=("name",)) class AuthorSchema(Schema): name = fields.Str() books = fields.List(fields.Nested("BookSchema", exclude=("author",))) schema = BookSchema() res = schema.dump(potter) print(res) # {'title': 'potter', 'author': {'name': 'JK'}} 自定义 Field 官方文档   默认提供的 field 可能不能满足需求。   有些库的 field 需要自定义复杂的 序列化 和 反序列化操作。   这个时候就可以定义自己的 field 来解决问题。   简单的情况可以使用 Method 和 Function 来解决问题 1 2 3 4 5 6 7 8 class UserSchema(Schema): name = fields.String() email = fields.String() created_at = fields.DateTime() since_created = fields.Method("get_days_since_created") def get_days_since_created(self, obj): return dt.datetime.now().day - obj.created_at.day 1 2 3 4 5 class UserSchema(Schema): name = fields.String() email = fields.String() created_at = fields.DateTime() uppername = fields.Function(lambda obj: obj.name.upper())   默认情况下是 serialize 函数,如果要自定义 deserialize 可以使用 Method 和 Function 传入 deserialize 参数进行指定。   复杂的情况就需要 fields.Field 类。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 from marshmallow import fields, ValidationError class PinCode(fields.Field): """Field that serializes to a string of numbers and deserializes to a list of numbers. """ def _serialize(self, value, attr, obj, **kwargs): if value is None: return "" return "".join(str(d) for d in value) def _deserialize(self, value, attr, data, **kwargs): try: return [int(c) for c in value] except ValueError as error: raise ValidationError("Pin codes must contain only digits.") from error class UserSchema(Schema): name = fields.String() email = fields.String() created_at = fields.DateTime() pin_code = PinCode() 踩过的坑 双向嵌套数据   如果数据存在相互嵌套引用的关系,是无法通过原生的 json 内置库进行序列化的。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import attr import json from addict import Dict from marshmallow import Schema, fields @attr.s class Book(Dict): title = attr.ib() author = attr.ib() @attr.s class Author(Dict): name = attr.ib() books = attr.ib() potter = Book("potter", "JK") JK = Author("JK", [potter]) potter.author = JK print(json.dumps(potter)) # Traceback (most recent call last): # File "f:/repo/_blog/source/_posts/Python/pacakge/02_marshmallow.py", line 22, in <module> # print(json.dumps(potter)) # File "C:\tools\Anaconda3\lib\json\__init__.py", line 231, in dumps # return _default_encoder.encode(obj) # File "C:\tools\Anaconda3\lib\json\encoder.py", line 199, in encode # chunks = self.iterencode(o, _one_shot=True) # File "C:\tools\Anaconda3\lib\json\encoder.py", line 257, in iterencode # return _iterencode(o, 0) # ValueError: Circular reference detected   marshmallow 则需要通过 Schema 的定义过滤掉特定的嵌套键值才可用。   并且加载数据的时候并不能还原它们原有的关联关系。   需要自己的手动去定义反序列化之后的操作。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 import attr import json from addict import Dict from marshmallow import Schema, fields,post_load @attr.s class Book(Dict): title = attr.ib() author = attr.ib(default="") @attr.s class Author(Dict): name = attr.ib() books = attr.ib(factory=list) potter = Book("potter", "JK") JK = Author("JK", [potter]) potter.author = JK class BookSchema(Schema): title = fields.Str() author = fields.Nested("AuthorSchema", only=("name",)) @post_load def make_object(self, data, **kwargs): book = Book(**data) if 'author' in data: books = book.author.books if book not in books: books.append(book) return book class AuthorSchema(Schema): name = fields.Str() books = fields.List(fields.Nested("BookSchema", exclude=("author",))) @post_load def make_object(self, data, **kwargs): author = Author(**data) for book in author.books: book.author = author return author schema = BookSchema() res = schema.dumps(potter).data new_potter = schema.loads(res).data print(potter) # Book(title='potter', author=Author(name='JK', books=[...])) print(new_potter) # Book(title='potter', author=Author(name='JK', books=[...])) schema = AuthorSchema() res = schema.dumps(JK).data new_JK = schema.loads(res).data print(JK) # Author(name='JK', books=[Book(title='potter', author=...)]) print(new_JK) # Author(name='JK', books=[Book(title='potter', author=...)])   关系重建需要手动处理。 总结   使用 marshmallow 可以很方便实现数据序列化。   使用的时候可以配合 addict 以及下一篇文章要介绍的 cerberus 结合使用。   可以让使用体验更上一层楼。

2022/2/28
articleCard.readMore