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
相关产品推荐
相关产品推荐

