Skip to content

Blender 侧接入

1. 复制 Blender 侧 bridge core

把下面这个目录复制到你自己的 Blender 扩展包中:

text
src\blender_extension\avalonia_bridge\core\

Blender 侧的职责划分:

  • BridgeController:bridge core,本身只负责进程生命周期、传输、frame pipeline、business packet、状态与诊断
  • View3DOverlayHost:可选的 3D View 宿主,负责 overlay 绘制、标题栏拖拽、hit-test、输入转发与 redraw
  • native_gpu:可选的 native GPU hook 加载层,用于更快地把外部 frame 复制到 Blender GPU texture

2. 构建 native GPU hook

Offscreen UI 的快速 frame 路径需要 Blender 侧 native hook。这个 hook 由 ctypes 加载,默认构建产物会输出到扩展目录:

text
src\blender_extension\avalonia_bridge\native\

在仓库根目录执行:

sh
cmake -S src/blender_native -B src/blender_native/build
cmake --build src/blender_native/build --config Release

macOS 上,这个 hook 会解析 Blender 的 Metal GPU texture 符号,把 Avalonia 传来的 IOSurfaceID 导入为 Metal texture,并复制到 Blender 创建的 gpu.types.GPUTexture。如果 hook 不存在或加载失败,bridge 会记录诊断信息,并回退到可用的 frame 传输路径。

集成到你自己的扩展时,可以选择下面任一种方式:

  • 把构建产物随扩展一起放到 your_addon/native/
  • 设置环境变量 AVALONIA_BRIDGE_NATIVE_PATH
  • 在扩展 preferences 中提供 native_library_path

3. 组装 controller

最小组装示例:

python
from .core import (
    BridgeConfig,
    BridgeController,
    DefaultBusinessEndpoint,
    View3DOverlayHost,
)


config = BridgeConfig(
    executable_path="/path/to/YourAvaloniaApp",
    width=1100,
    height=760,
    render_scaling=1.25,
    target_fps=120,
    window_mode="headless",
    supports_business=True,
    supports_frames=True,
    supports_input=True,
    host="127.0.0.1",
    show_overlay_debug=False,
)

presentation_host = View3DOverlayHost() if config.supports_frames else None

controller = BridgeController(
    config,
    host=presentation_host,
    business_endpoint=DefaultBusinessEndpoint(),
    state_callback=lambda snapshot: print(snapshot.last_message),
)

controller.start()
  • offscreen UI(window_mode="headless"):BridgeController(..., host=View3DOverlayHost(...))
  • desktop / business-only:BridgeController(..., host=None)

4. 选择展示宿主

  • View3DOverlayHost 是 Blender 3D View 的可选展示宿主
  • 当前示例在 offscreen UI 模式下使用它把 UI 绘制到 3D View
  • 如果不希望绘制到 3D View,可以不组装 View3DOverlayHost,只保留 business 通道
  • 其他 Blender 展示方式需要在 addon 层自行组装适配

默认宿主会在后台 socket 线程接收 frame,并且只保留最新 frame。Modal timer 调用 tick_once() 时展示最新 frame 并处理排队的 business packet。它不会反向请求 bridge 进程提高产帧频率。

5. 生命周期与事件驱动

Blender 侧分两层接入:

  • runtime adapter:构造 BridgeConfig,并组装 BridgeController 与可选的 View3DOverlayHost
  • modal operator:在 TIMER 中调用 tick_once(),在事件链路中调用 handle_event(context, event)

当前会转发的 packet 类型:

  • 指针类:pointer_downpointer_uppointer_movewheel
  • 键盘类:key_downkey_up
  • 文本类:按键带有非空 event.unicode 时发送 text

当前 View3DOverlayHost 支持的 Blender event.type

  • 指针与滚轮:MOUSEMOVEINBETWEEN_MOUSEMOVELEFTMOUSERIGHTMOUSEMIDDLEMOUSEWHEELUPMOUSEWHEELDOWNMOUSEEVT_TWEAK_LEVT_TWEAK_MEVT_TWEAK_R
  • 仅用于标题栏拖拽:LEFTMOUSEEVT_TWEAK_L
  • 字母键:A-Z
  • 主键盘数字:ZERO-NINE
  • 基本编辑与导航:SPACETABRETNUMPAD_ENTERBACK_SPACEDELINSERTHOMEENDPAGE_UPPAGE_DOWNESCLINE_FEED
  • 方向键:LEFT_ARROWRIGHT_ARROWUP_ARROWDOWN_ARROW
  • 标点键:PERIODNUMPAD_PERIODCOMMAMINUSPLUSEQUALSEMI_COLONQUOTESLASHBACK_SLASHLEFT_BRACKETRIGHT_BRACKETACCENT_GRAVE
  • 修饰键:LEFT_SHIFTRIGHT_SHIFTLEFT_CTRLRIGHT_CTRLLEFT_ALTRIGHT_ALTOSKEYAPP
  • 小键盘:NUMPAD_0-NUMPAD_9NUMPAD_SLASHNUMPAD_ASTERIXNUMPAD_MINUSNUMPAD_PLUS
  • 功能键:F1-F24

说明:

  • 键盘 packet 只会在 overlay 已捕获输入时转发
  • desktop 模式没有 frame host,不转发指针或键盘输入
  • View3DOverlayHost 会在本地消费标题栏拖拽事件,不会把它们作为键盘 packet 转发

最小 modal operator 示例:

python
import bpy


class BRIDGE_OT_start(bpy.types.Operator):
    bl_idname = "your_addon.bridge_start"
    bl_label = "Start Bridge"

    def execute(self, context):
        controller = create_controller(mode="headless")
        context.window_manager.your_bridge_controller = controller
        controller.start()
        bpy.ops.your_addon.bridge_modal("INVOKE_DEFAULT")
        return {"FINISHED"}


class BRIDGE_OT_modal(bpy.types.Operator):
    bl_idname = "your_addon.bridge_modal"
    bl_label = "Bridge Modal"
    bl_options = {"BLOCKING"}

    _timer = None

    def invoke(self, context, _event):
        self._timer = context.window_manager.event_timer_add(1.0 / 120.0, window=context.window)
        context.window_manager.modal_handler_add(self)
        return {"RUNNING_MODAL"}

    def modal(self, context, event):
        controller = getattr(context.window_manager, "your_bridge_controller", None)
        if controller is None:
            self.cancel(context)
            return {"CANCELLED"}

        if not controller.state_snapshot().process_running:
            self.cancel(context)
            return {"CANCELLED"}

        if event.type == "TIMER":
            controller.tick_once()
            return {"RUNNING_MODAL"}

        if context.area and context.area.type == "VIEW_3D":
            if controller.handle_event(context, event):
                return {"RUNNING_MODAL"}

        return {"PASS_THROUGH"}

    def cancel(self, context):
        controller = getattr(context.window_manager, "your_bridge_controller", None)
        if controller is not None:
            controller.stop()
            context.window_manager.your_bridge_controller = None
        if self._timer is not None:
            context.window_manager.event_timer_remove(self._timer)
            self._timer = None

下一步