You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

WebGPU Rust与JavaScript交互:实现交互式渲染的规范方案

问题描述

我通过以下方式集成WebGPU+Rust:

index.html

<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>GreenMatterAI graphics preview</title>
    <style>
        canvas {
            background-color: black;
        }
    </style>
</head>

<body id="wasm-example">
<script type="module">
      import init from "./pkg/gmai_cad.js";
      init().then(() => {
          console.log("WASM Loaded");
      });
  </script>
</body>

</html>

lib.rs

#[cfg_attr(target_arch = "wasm32", wasm_bindgen(start))]
pub async fn run() {
    cfg_if::cfg_if! {
        if #[cfg(target_arch = "wasm32")] {
            std::panic::set_hook(Box::new(console_error_panic_hook::hook));
            console_log::init_with_level(log::Level::Warn).expect("Couldn't initialize logger");
        } else {
            env_logger::init();
        }
    }

    let event_loop = EventLoop::new();
    let window = WindowBuilder::new().build(&event_loop).unwrap();
    #[cfg(target_arch = "wasm32")]
    {
        use winit::dpi::PhysicalSize;
        window.set_inner_size(PhysicalSize::new(450, 400));

        use winit::platform::web::WindowExtWebSys;
        web_sys::window()
            .and_then(|win| win.document())
            .and_then(|doc| {
                let dst = doc.get_element_by_id("wasm-example")?;
                let canvas = web_sys::Element::from(window.canvas());
                dst.append_child(&canvas).ok()?;
                Some(())
            })
            .expect("Couldn't append canvas to document body.");
    }
    let mut state = State::new(window).await;
    event_loop.run(move |event, _, control_flow| match event {
        Event::RedrawRequested(window_id) if window_id == state.window().id() => {
            state.update();
            state.render()
        }
        Event::MainEventsCleared => {
            state.window().request_redraw();
        }
        Event::WindowEvent {
            ref event,
            window_id,
        } => { state.event(event) },
        _ => {}
    });
}

当前代码架构无法实现从JavaScript向渲染循环传递数据,我希望添加HTML输入控件,让用户可交互式修改参数以影响渲染效果。目前想到的方案是使用全局可变变量并暴露Rust函数供JavaScript调用,但这是明确的Rust反模式,请问标准的实现方式是什么?


标准实现方案

1. 基于异步通道(Channel)传递参数

利用Rust的std::sync::mpsc(WASM单线程场景下安全)创建消息通道,将发送端暴露给JavaScript,渲染循环在重绘阶段接收消息并更新参数。这种方式完全规避全局可变状态,符合Rust安全规范。

实现示例:

lib.rs 修改:

use std::sync::mpsc;
use wasm_bindgen::prelude::*;

// 定义参数消息枚举
#[derive(Debug)]
pub enum RenderParam {
    SetColor(f32, f32, f32),
    SetScale(f32),
}

// 全局存储发送端(WASM单线程下安全,可结合OnceCell优化初始化)
static mut PARAM_SENDER: Option<mpsc::Sender<RenderParam>> = None;

#[wasm_bindgen]
pub fn set_render_color(r: f32, g: f32, b: f32) {
    unsafe {
        if let Some(sender) = &PARAM_SENDER {
            let _ = sender.send(RenderParam::SetColor(r, g, b));
        }
    }
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen(start))]
pub async fn run() {
    // ... 原有初始化代码 ...

    // 创建消息通道
    let (sender, receiver) = mpsc::channel();
    unsafe {
        PARAM_SENDER = Some(sender);
    }

    let mut state = State::new(window).await;
    event_loop.run(move |event, _, control_flow| match event {
        Event::RedrawRequested(window_id) if window_id == state.window().id() => {
            // 批量处理所有待接收的参数消息
            while let Ok(param) = receiver.try_recv() {
                match param {
                    RenderParam::SetColor(r, g, b) => state.set_color(r, g, b),
                    RenderParam::SetScale(s) => state.set_scale(s),
                }
            }
            state.update();
            state.render()
        }
        // ... 原有事件处理代码 ...
    });
}

index.html 添加输入控件:

<body id="wasm-example">
    <div>
        <label>红色值: <input type="range" min="0" max="1" step="0.01" id="red"></label>
        <label>绿色值: <input type="range" min="0" max="1" step="0.01" id="green"></label>
        <label>蓝色值: <input type="range" min="0" max="1" step="0.01" id="blue"></label>
    </div>
<script type="module">
      import init from "./pkg/gmai_cad.js";
      init().then(() => {
          console.log("WASM Loaded");
          const redInput = document.getElementById('red');
          const greenInput = document.getElementById('green');
          const blueInput = document.getElementById('blue');
          
          const updateColor = () => {
              const r = parseFloat(redInput.value);
              const g = parseFloat(greenInput.value);
              const b = parseFloat(blueInput.value);
              window.set_render_color(r, g, b);
          };
          
          redInput.addEventListener('input', updateColor);
          greenInput.addEventListener('input', updateColor);
          blueInput.addEventListener('input', updateColor);
      });
  </script>
</body>

2. 使用RefCell包装可变参数(WASM单线程场景安全)

WASM在浏览器中是单线程执行,无多线程竞争,可使用RefCell包装State中的可变参数,直接暴露修改方法给JS调用。此方式更简洁,适合简单参数场景。

实现示例:

lib.rs 修改:

use std::cell::RefCell;
use once_cell::sync::OnceCell;
use wasm_bindgen::prelude::*;

struct State {
    // ... 原有字段 ...
    color: RefCell<(f32, f32, f32)>,
}

impl State {
    pub async fn new(window: Window) -> Self {
        // ... 原有初始化代码 ...
        Self {
            // ... 原有字段初始化 ...
            color: RefCell::new((0.0, 1.0, 0.0)), // 默认绿色
        }
    }
    
    // ... 原有update、render方法 ...
}

// 用OnceCell安全存储State实例
static STATE: OnceCell<RefCell<State>> = OnceCell::new();

#[wasm_bindgen]
pub fn set_render_color(r: f32, g: f32, b: f32) {
    if let Some(state) = STATE.get() {
        *state.borrow_mut().color.borrow_mut() = (r, g, b);
    }
}

// 在run函数中初始化STATE
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(start))]
pub async fn run() {
    // ... 原有初始化代码 ...
    let state = State::new(window).await;
    STATE.set(RefCell::new(state)).unwrap();
    let mut state = STATE.get().unwrap().borrow_mut();
    
    event_loop.run(move |event, _, control_flow| match event {
        // ... 原有事件处理代码 ...
    });
}

3. 通过自定义DOM事件传递参数

JS侧触发自定义DOM事件,Rust侧通过winit的WebSysEvent监听事件,解析事件数据后更新渲染参数。此方式无需全局变量,完全基于DOM事件机制。

实现示例:

index.html 添加输入控件:

<body id="wasm-example">
    <div>
        <label>缩放值: <input type="range" min="0.5" max="2" step="0.1" id="scale"></label>
    </div>
<script type="module">
      import init from "./pkg/gmai_cad.js";
      init().then(() => {
          console.log("WASM Loaded");
          const scaleInput = document.getElementById('scale');
          scaleInput.addEventListener('input', () => {
              const scale = parseFloat(scaleInput.value);
              window.dispatchEvent(new CustomEvent('render-param-update', {
                  detail: { type: 'scale', value: scale }
              }));
          });
      });
  </script>
</body>

lib.rs 修改事件处理:

use winit::event::WindowEvent;
use winit::platform::web::WindowEventExtWebSys;

impl State {
    pub fn event(&mut self, event: &WindowEvent) {
        // ... 原有事件处理 ...
        if let WindowEvent::WebSysEvent(web_event) = event {
            if web_event.type_() == "render-param-update" {
                let detail = web_event.detail().unwrap();
                let param_type = js_sys::Reflect::get(&detail, &JsValue::from_str("type")).unwrap();
                let param_value = js_sys::Reflect::get(&detail, &JsValue::from_str("value")).unwrap();
                
                if param_type.as_string().unwrap() == "scale" {
                    let scale = param_value.as_f64().unwrap() as f32;
                    self.set_scale(scale);
                }
            }
        }
    }
}

内容的提问来源于stack exchange,提问作者alagris

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 13:54:55