Blender 新欢 Armory3D - WebAssembly + Zui Canvas UI

本文继续讲解 Armory Traits 混入式编程的 5 种类型,本文着重介绍其中 Wasm 与 Canvas UI:
1. **haXe** 脚本代码文件;
2. **Nodes** 使用 Logic Node Editor 可视化编程工具定义的节点树;
3. **UI** - User Interface (Canvas trait),使用 Armory2D 用户界面编辑进行可视化编辑;
4. **Bundled** Armory Engine 预定义 Haxe 脚本,是 `Trait` 类型的扩展;
5. **Wasm** 使用 WebAssembly 字节码程序;
# ✨ 源文档 https://github.com/Jeangowhy/opendocs/blob/main/Haxe.md
✨ WebAssembly (Wasm)
WebAssembly 字节码文件称为一个 Module,可以使用 Rust/C/C++/Emscripten 编译得到 WASM 文件。Armory 使用 Wasm 程序以获取高性的代码执行,WebAssembly modules 只在 HTML5 和使用 Krom 的桌面平台有支持,包括 Windows, Linux, macOS。
配置 Render 属性面板:Armory Project - Modules - Append Khafile 指定要附加到 khafile.js 构建脚本的内容,指定 Blender 中编写的脚本名称即可。比如 khaconfig 脚本,
内容如下,用来编译 Wasm 字节码:
官方 wasm_trait_c 示例提供的一个 Wasm 程序,它直接与 Armory API 交互:
Armory examples wasm_call 示范中,就没有在 Armory Scene Traits 或者 Armory Traits
面板中添加 Wasm 模块,而是在 Haxe Script 代码中引用 Bundled 目录下的 main.wasm 字节码文件。`Bundled` 是一个专用目录,Armory 用来存放数据文件,其路径硬编码在源代码中。数据文件,比如 UI 配置文件,canvas.json,在官方 ui_canvas 示例中就可以看到。当然也可以存放 Wasm 文件,然后通过 `Data.getBlob(name)` 方法获取数据。
添加 `Wasm` 模块到 Armory Traits 列表时,如果直接点击 `New Module` 按钮,就会跳转到 Web 在线编译 Wasm 的服务,webassembly.studio。但是界内环境可能用不了,可以按前面介绍的方法调用本地安装好的编译器生成 Wasm。然后将 Wasm 文件放到 `Bundled` 目录下,再点击 `Refresh` 刷新 Armory Traits 中的类型列表,就可以看到相应的模块名出现。
1. [WebAssembly Studio](https://github.com/wasdk/WebAssemblyStudio)
2. [WebAssembly Studio](https://esmbly.github.io/WebAssemblyStudio/)
Bundled 这个目录下的文件不需手动引用,Armory 会自动向 khafile.js 中添加引用:
官方示范中的 .blend 体积非常小,场景同样只包含一个 Cube,示范案例中不到 100kb,而新创建的文件可能接收 1MB。因为官方的文件清理掉了无用数据,Outliner - Orphans Data,但是 Worlds Arm 不能删除,Armory 需要使用这个对象包含的数据。另外,使用喜好配置的文件压缩保存也可以适当减小文件,Preferences - File - Save & Load。如果文件已经采用非压缩式保存,那么可以使用 Save As,指定另存选项中的 Compress 即可以重新启用压缩式保存。
添加 Armory Traits 脚本时,如果工程 Sources 目录下已有脚本文件,可以在 Class 列表中指定要使用的 Traits 类型定义,如果列表没有类型数据记录,可以点击 Refresh 按钮刷新以读取类型信息。
✨ Armory2D Canvas UI
* [UI Editor](https://github.com/armory3d/armory/wiki/ui_editor)
* https://github.com/BlackGoku36/armory-tutorial-download/tree/master/Canvas
Armory2D 是一个可视化 UI 编辑编辑器,使用 **Zui** UI 框架,此构架受 imgui 启发,Immediate Mode 即时刷新模式 UI 构架,使用 Haxe 和 Kha 实现。
Zui 源代码文件有 5 个:
1. Zui.hx 是整个 UI 构架的程序逻辑,每一个用于绘图函数就是一个控件。
2. Nodes.hx 定义了节点类型结构,以及具体的绘图逻辑。
3. Id.hx 提供了 Zui 控件状态数据的获取,CanvasScript.getHandle() 方法返回的控件数据。
4. Ext.hx 扩展了 `Zui` 类型,增加了 textArea、fileBrowser、inlineRadio、colorWheel 等功能。
5. Themes.hx 提供主题功能。
Armory 对 Zui 进行了封装,添加了一些类型定义,画布包装成 `Canvas` 类型,还有弹窗 `Popup`:
并且在 armory.ui.Canvas 内置默认的字体设置:
BG-36's Tutorials - Armory Canvas UI 教程中使用了旧版的类型系统,应该使用 Armory 类空间:
import zui.Canvas.TElement;
import armory.ui.Canvas;
使用 Armory2D 编辑器进行可视化界面设计,Bundled 目录下 JSON 文件中保存。
Zui 是一个 Immediate Mode UI Library,其本身缺少布局容器的设计,但是控件可以设置 Anchor 来影响其放置的位置,Anchor 属性值与对位方式如下,锚点的参数原点由控件的 x y 坐标指定:
文字内容对齐方式由 Alignment 属性指定,0 - Left,1 - Center,2 - Right。
Blender Render 属性面板中设置 Armory Project - Window - Resizable 可以启用窗口的大小调整功能,以观察不同的锚点位置下,控件的定位效果。例如,控件的坐标设置为 0 点,锚点设置为 Center,那么控件就会随着窗口的大小调整,而更新为当前的居中位置。
JSON 配置文件中 assets 字段记录导入的图像、字体资源,Image 控件使用 asset 属性显示图像资源。Button 等等控件可以设置字体资源,以改变文字外观。注意,资源文件路径变动可能导致 Armory2D 界面纯黑显示不了任何内容。
颜色属性有四种基本形式:
1. color 背景颜色
1. color_text 文字颜色
2. color_hover 悬停状态颜色
3. color_press 按下状态颜色
因为采用负值色彩模型,-1 表示白色,-16645630 表示黑色,-16713472 表示绿色,默认值为 null。
Armory3D SDK 本身已经包含 Armory2D 工具,要从 Blender 中启动 Armory2D editor,只需要添加一个 UI Trait,并编辑它。例如,Scene 属性面板中向 Armory Scene Traits 列表添加一个 UI Trait。然后点击编辑按钮打开 Armory2D 编辑器。
或者直接执行 Krom 加载 Armory2D:
Krom.exe C:\HaxeToolkit\armsdk\lib\armory_tools\armory2d\d3d11
添加 UI 配置时,点击 `New Canvas`,比如命名为 MyCanvas,就会生成一个画布配置文件:Bundled\canvas\MyCanvas.json,内容包括画布的名称、坐标、宽高、主题、元素、资源文件:
点击 `Edit Canvas` 打开 Armory2D 工具,界面如下:

1. 左侧,是可用的 UI 组件列表,如 Button、Images、Text、Slider 等等;
2. 中间,是画面设计区,可以将左侧的组件拖放到设计区,进行 UI 布局设计;
3. 右侧,UI 工程保存,组件属性、主题设置、资源文件导入,以及喜好配置,如 UI 界面缩放、参考格大小;
Project 面板中的按钮使用,及注意事项:
- **Current File** 指定当前操作的配置文件,如果在设置了此值,相对路径是 Krom 主程序所在目录。
- **Save** 按钮保存配置文件,如果指定的 Current File,就应该使用绝对路径。
- **Load** 按钮用来加载配置文件,如果 Current File 使用了相对路径,则可能导致程序异常。
- **New** 按钮在 Canvas 分组,用来创建新的 Canvas 配置,Width 和 Height 指定画布大小。
另外,资源文件路径错误也可能导致 Armory2D 界面纯黑,不显示控件。
保存 UI 设计后,就会生成主题配置文件和资源列表文件 MyCanvas.files。
导入的资源文件,如图像可以作为 Image 控件的背景。
Canvas UI 控件的 Script - Event 属性指定一个事件名,点击时会触发此事件,比如 `save_btn`。然后,在需要处理此事件的 Haxe 脚本中调用 `Event.add("save_btn", save);` 注册一个处理函数。
所有激活状态的 UI 界面都会在程序运行时显示,控件的事件处理使用的是观察者编程模式,即需要处理什么事件,就使用 `Event.add()` 方法注册相应的事件处理函数。Observer Pattern 用来解耦 UI 与程序逻辑的非常好用的工具,UI 控件可以任意指定事件名称,处理与否完全取决于程序逻辑。
Canvas UI 控件可以通过 `CanvasScript` 对象来获取引用,将对控制的属性进行修改,就像 Web 脚本编程中对 HTML 元素的操作一样。但是场景中可能存在激活多个 Canvas UI 的情况,通过 Scene API 获取到的 CanvasScript 对象总是为 Armory Traits 列表中最上面(靠前)的那一个。
以下是 CanvasTrait.hx 演示代码,假定场景中 Canvas UI 设计包含 Image 和 Button 控件各一个。点击 Button 触发 on_click() 处理函数,并切换 Image 的显示状态:
Haxe 4.0.0 开始支持 Arrow functions,也支持省略参数和 Nullability。但是也不能将在可省略参数的函数当作 'onEvent' 传给 Event.add() 方法,这是基本的函数签名要求。直接调用则完全可以。
Zui 作为一个立即模式的 UI 框架,特点就表现在控件的属性值的获取上,另一个特点就是事件的处理机制。
GUI 设计有两种模式:
1. RMGUI(Retained Mode Graphics User Interface),绝大多数应用程序是用的这种模式。
2. IMGUI(Immediate Mode Graphics User Interface),绝大多数游戏是这类模式。
保留模式,是指 UI 控件的状态保留,IMGUI 则不会保留控件状态,因此代码实现更简洁。由于 IMGUI 控件无状态,所以就不能直接给控件注册事件处理函数,而需要使用 Observer Pattern 编程模式用来解耦 UI 与程序逻辑,UI 控件可以任意指定事件名称,处理与否完全取决于程序逻辑。所有激活状态的 UI 界面都会在程序运行时显示,控件的事件处理使用的是观察者编程模式,即需要处理什么事件,就使用 `Event.add()` 方法注册相应的事件处理函数,同一个 trait 中同名事件的第一个注册的处理函数才会获得调用机会。
立即模式下,所以控件在每一帧中都会重新绘制,这就导致它占用更高的 CPU 时间,当然这是相对的,只要保持界面整洁,并不需要多少的额外 CPU 时间。另一方面,由于它更简洁,使得它在游戏开发领域中备受欢迎。
因此,要获取 Zui 控件的值,比如 Slider、ProgressBar、InputText 等等控件的值,首先就需要保存到内存的一个位置上,以 `Handle` 类型的形式,这个位置可以通过 CanvasScript.getHandle() 方法返回的控件数据 方法获取,根据控件类型从返回的数据中获取相应的字段:
Zui 定义的 Handle 类型的数据字段:
ComboBox 和 Radio 控件需要在 Text 属性中指定选项字符串,使用分号分隔,如 Bad; Apple,获取控件的 position 值,默认为 0,表示选择第一个选项。
进度条有两种,Progress_bar,CProgress_bar,但它们都没有状态数据,getHandle() 获取不到数据。而要绘制出指定状态的控件,比如环形进行条的百分进度值,就需要在更新回调方法中设置相应的值:
另外,Slider、Keyinput、TextArea 等等都不会触发事件,只有 TextInput 在内容改变后按回车时触发。
Armory 可以使用主题功能,默认为 Default Light 主题,主要是设置控件的色彩属性,替换其默认值。如果控件设置了相应的色彩,则主题配置的项目不起作用。主题涉及的控件属性有三类:
- Text 控件的字体颜色;
- Elements 元素:BUTTON、ACCENT;
- Others 其它:PANEL_BG_COL;
Armory2D 目前还有些未曾完成的功能,其中就有 Timeline 时间轴动画的支持缺失。
最后注意,传入方法的 id 没有对应控件时,getHandle() 方法触发异常,getElement() 返回 null:
Trace: TypeError: Cannot read property 'id' of null
at armory_trait_internal_CanvasScript.getHandle (<anonymous>:8132:34)