render3d im Browser: WASM/WebGPU-3D-Viewport hinter ?engine=wasm
Feature web (wasm-bindgen) + cdylib analog render2d; WebModelRenderer mit Canvas-Surface, set_model (walls/slabs wie der native Push) und set_camera. Projektion liefert bereits [0,1]-Clip-Z, math.rs unveraendert. wgpu-22-requestDevice-Shim in src/engine/requestDeviceShim.ts geteilt. Neuer Hook useWasm3dRenderer + Wasm3DViewport (Orbit/Pan/Zoom wie three.js- Sicht); Viewport3D dispatcht per ?engine=wasm bzw. localStorage, three.js bleibt Default. Build-Script build:engine3d (wasm-pack, src/engine/pkg3d). Verifiziert headful per scripts/probe-engine3d.mjs (37 % Geometrie-Pixel); headless praesentiert Chromium keine WebGPU-Frames (auch bei render2d).
This commit is contained in:
Generated
+5
@@ -3678,11 +3678,16 @@ name = "render3d"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"console_error_panic_hook",
|
||||
"env_logger",
|
||||
"js-sys",
|
||||
"naga",
|
||||
"pollster",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"wgpu",
|
||||
"winit",
|
||||
]
|
||||
|
||||
@@ -15,6 +15,13 @@ description = "Nativer wgpu-3D-Renderer fuer die CAD-Modellsicht (Wand-Extrusion
|
||||
# Workspace-Root INNERHALB des src-tauri-Baums Cargo mit "multiple workspace
|
||||
# roots" abbrechen. Der Parent-`exclude` haelt die Crate trotzdem eigenstaendig.
|
||||
|
||||
# cdylib: von wasm-pack (Feature "web") fuer das .wasm-Modul benoetigt. rlib:
|
||||
# damit die Crate weiterhin als Pfad-Abhaengigkeit (cad-tauri, Feature native3d)
|
||||
# und im Test-/Bin-Build (spike3d) nutzbar bleibt. Der cdylib-Artefakt-Build auf
|
||||
# nativen Zielen ist harmlos (leere Export-Oberflaeche ohne Feature "web").
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
# Standard: nur Mesh-Erzeugung (Wand-Extrusion) + Kamera/Matrizen + WGSL-Quellen.
|
||||
# Diese Schicht ist serde-only, hat keine GPU-Abhaengigkeit und ist headless
|
||||
@@ -30,8 +37,26 @@ render = ["dep:wgpu", "dep:bytemuck", "dep:pollster"]
|
||||
# Wayland/X11 + eine Display-Session zur visuellen Verifikation.
|
||||
window = ["render", "dep:winit", "dep:env_logger"]
|
||||
|
||||
# Browser-Bindings (wasm32 + WebGPU): dieselbe GPU-Schicht (`render`) hinter einer
|
||||
# wasm-bindgen-Fassade (`web.rs`), die eine HTML-Canvas als wgpu-Surface bespielt.
|
||||
# Baut NUR fuer target wasm32-unknown-unknown sinnvoll (wgpu waehlt dort das
|
||||
# WebGPU-Backend). Die native Fensterschicht (`window`) bleibt davon unberuehrt.
|
||||
# Muster: render2d/Cargo.toml (Feature "web").
|
||||
web = [
|
||||
"render",
|
||||
"dep:wasm-bindgen",
|
||||
"dep:wasm-bindgen-futures",
|
||||
"dep:web-sys",
|
||||
"dep:js-sys",
|
||||
"dep:console_error_panic_hook",
|
||||
"dep:serde_json",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
# JSON-Modell (dieselbe Ableitung wie der native Push): im Web-Pfad zur Laufzeit
|
||||
# geparst; optional, nur mit Feature "web".
|
||||
serde_json = { version = "1", optional = true }
|
||||
|
||||
# --- GPU (nur mit Feature "render") ------------------------------------------
|
||||
# Standard-Features von wgpu (wgsl, webgpu, dx12, metal). Die Linux-Backends
|
||||
@@ -45,6 +70,20 @@ pollster = { version = "0.3", optional = true }
|
||||
winit = { version = "0.30", optional = true }
|
||||
env_logger = { version = "0.11", optional = true }
|
||||
|
||||
# --- Browser/WASM (nur mit Feature "web") ------------------------------------
|
||||
# wasm-bindgen-Fassade + Canvas-Zugriff. Versionen bewusst breit ("0.2"/"0.3"),
|
||||
# damit wasm-pack die passende wasm-bindgen-CLI selbst zieht.
|
||||
wasm-bindgen = { version = "0.2", optional = true }
|
||||
wasm-bindgen-futures = { version = "0.4", optional = true }
|
||||
js-sys = { version = "0.3", optional = true }
|
||||
console_error_panic_hook = { version = "0.1", optional = true }
|
||||
web-sys = { version = "0.3", optional = true, features = [
|
||||
"HtmlCanvasElement",
|
||||
"Window",
|
||||
"Document",
|
||||
"console",
|
||||
] }
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = "1"
|
||||
# naga validiert die WGSL-Quellen headless (Parser + Validator) im Test, ohne
|
||||
|
||||
@@ -22,6 +22,12 @@ pub mod types;
|
||||
#[cfg(feature = "render")]
|
||||
pub mod gpu;
|
||||
|
||||
// Browser-Bindings (wasm32 + WebGPU), nur mit Feature "web". Setzt "render" voraus
|
||||
// (dieselbe GPU-Schicht wie das native Fenster), fuegt aber KEINE winit-Abhaengigkeit
|
||||
// hinzu — die Canvas dient direkt als wgpu-Surface. Muster: render2d/src/web.rs.
|
||||
#[cfg(feature = "web")]
|
||||
pub mod web;
|
||||
|
||||
pub use math::{
|
||||
look_at, orbit_eye, orthographic, perspective, preset_camera, projection_matrix,
|
||||
view_matrix, view_projection, Mat4,
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
// Browser-Bindings (wasm32 + WebGPU): fuehrt die native GPU-Schicht (`gpu::Renderer`)
|
||||
// als WASM-Modul in die App-UI, mit einer HTML-Canvas als wgpu-Surface. Bewusst
|
||||
// duenn — die gesamte Zeichenlogik (Wand-/Decken-Extrusion, Pipeline, Licht) ist
|
||||
// mit dem nativen Fenster-Spike (bin/spike3d) geteilt; hier liegt nur die Canvas-/
|
||||
// WebGPU-Anbindung + die wasm-bindgen-Fassade. Muster: render2d/src/web.rs.
|
||||
//
|
||||
// Ablauf im Frontend (siehe src/viewport/useWasm3dRenderer.ts):
|
||||
// const r = await WebModelRenderer.new(canvas);
|
||||
// r.set_model(JSON.stringify(projectToModel3d(project))); // bei Modelländerung
|
||||
// r.set_camera(eye.x, eye.y, eye.z, target.x, target.y, target.z,
|
||||
// up.x, up.y, up.z, perspective, fovY, orthoHalfHeight);
|
||||
// r.resize(px_w, px_h); r.render(); // pro Frame
|
||||
//
|
||||
// KAMERA: die Fassade nimmt eine FERTIGE Kamera (eye/target/up + Projektionsart)
|
||||
// entgegen — Orbit/Pan/Zoom-Interaktion (Yaw/Pitch/Distanz, Praesets) rechnet die
|
||||
// TS-Seite (dieselbe Konvention wie `math::orbit_eye`/`preset_camera`), damit die
|
||||
// Maus-UX 1:1 der bestehenden three.js-OrbitControls-Bedienung folgen kann, ohne
|
||||
// eine zweite Praeset-Tabelle in Rust pflegen zu muessen.
|
||||
//
|
||||
// MODELL-FORMAT: JSON-Struct { walls: WallInput[], slabs: SlabInput[] } — identisch
|
||||
// zu `RModel3d` (src/plan/toWalls3d.ts) und zum nativen Tauri-Push (nativeSync.ts).
|
||||
|
||||
use serde::Deserialize;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use web_sys::HtmlCanvasElement;
|
||||
|
||||
use crate::gpu::Renderer;
|
||||
use crate::types::{Camera, Projection, SlabInput, WallInput};
|
||||
|
||||
/// JSON-Eingabe des Modells: Waende (bereits in Teilquader zerlegt, siehe
|
||||
/// `projectToWalls3d`) + Deckenplatten. `slabs` optional (leeres Modell ohne
|
||||
/// Decken bleibt gueltig).
|
||||
#[derive(Deserialize)]
|
||||
struct ModelInput {
|
||||
walls: Vec<WallInput>,
|
||||
#[serde(default)]
|
||||
slabs: Vec<SlabInput>,
|
||||
}
|
||||
|
||||
/// Der Browser-Renderer: haelt Surface + Device/Queue + den geteilten `Renderer`
|
||||
/// und die aktuelle Kamera. Wird per `WebModelRenderer.new(canvas)` aus JS erzeugt.
|
||||
#[wasm_bindgen]
|
||||
pub struct WebModelRenderer {
|
||||
surface: wgpu::Surface<'static>,
|
||||
device: wgpu::Device,
|
||||
queue: wgpu::Queue,
|
||||
config: wgpu::SurfaceConfiguration,
|
||||
renderer: Renderer,
|
||||
camera: Camera,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WebModelRenderer {
|
||||
/// Erzeugt den Renderer asynchron (Adapter/Device via WebGPU).
|
||||
/// Aus JS: `await WebModelRenderer.new(canvasEl)`.
|
||||
#[allow(clippy::new_ret_no_self)]
|
||||
pub async fn new(canvas: HtmlCanvasElement) -> Result<WebModelRenderer, JsValue> {
|
||||
// Panics als lesbare Konsolen-Fehler statt "unreachable" ausgeben.
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
let width = canvas.width().max(1);
|
||||
let height = canvas.height().max(1);
|
||||
|
||||
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
|
||||
backends: wgpu::Backends::BROWSER_WEBGPU,
|
||||
..Default::default()
|
||||
});
|
||||
let surface = instance
|
||||
.create_surface(wgpu::SurfaceTarget::Canvas(canvas))
|
||||
.map_err(|e| JsValue::from_str(&format!("create_surface: {e:?}")))?;
|
||||
|
||||
let adapter = instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
power_preference: wgpu::PowerPreference::HighPerformance,
|
||||
force_fallback_adapter: false,
|
||||
compatible_surface: Some(&surface),
|
||||
})
|
||||
.await
|
||||
.ok_or_else(|| JsValue::from_str("kein WebGPU-Adapter (navigator.gpu?)"))?;
|
||||
|
||||
let (device, queue) = adapter
|
||||
.request_device(
|
||||
&wgpu::DeviceDescriptor {
|
||||
label: Some("render3d.web.device"),
|
||||
required_features: wgpu::Features::empty(),
|
||||
required_limits: wgpu::Limits::downlevel_webgl2_defaults()
|
||||
.using_resolution(adapter.limits()),
|
||||
memory_hints: wgpu::MemoryHints::Performance,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| JsValue::from_str(&format!("request_device: {e:?}")))?;
|
||||
|
||||
// WebGPU-Canvas liefert i.d.R. bgra8unorm (kein sRGB-Format fuer die
|
||||
// Surface) — Format vom Kontext abfragen statt hart zu kodieren (dieselbe
|
||||
// Wahl wie render2d: erstes NICHT-sRGB-Format, sonst das erste angebotene).
|
||||
let caps = surface.get_capabilities(&adapter);
|
||||
let format = caps
|
||||
.formats
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|f| !f.is_srgb())
|
||||
.unwrap_or(caps.formats[0]);
|
||||
let config = wgpu::SurfaceConfiguration {
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
format,
|
||||
width,
|
||||
height,
|
||||
present_mode: wgpu::PresentMode::Fifo,
|
||||
alpha_mode: caps.alpha_modes[0],
|
||||
view_formats: vec![],
|
||||
desired_maximum_frame_latency: 2,
|
||||
};
|
||||
surface.configure(&device, &config);
|
||||
|
||||
let renderer = Renderer::new(&device, format);
|
||||
|
||||
web_sys::console::log_1(&JsValue::from_str(&format!(
|
||||
"render3d WebGPU-Viewport initialisiert ({}x{}, {:?})",
|
||||
width, height, format
|
||||
)));
|
||||
|
||||
Ok(WebModelRenderer {
|
||||
surface,
|
||||
device,
|
||||
queue,
|
||||
config,
|
||||
renderer,
|
||||
camera: Camera::default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Setzt/aktualisiert das darzustellende Modell aus einem JSON-String
|
||||
/// (Format = `{ walls: WallInput[], slabs?: SlabInput[] }`, identisch zum
|
||||
/// nativen Push). Laedt die GPU-Puffer neu hoch; ruft NICHT selbst `render`.
|
||||
pub fn set_model(&mut self, json: &str) -> Result<(), JsValue> {
|
||||
let model: ModelInput = serde_json::from_str(json)
|
||||
.map_err(|e| JsValue::from_str(&format!("Modell parsen: {e}")))?;
|
||||
self.renderer
|
||||
.upload_model(&self.device, &model.walls, &model.slabs);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Setzt die Kamera fuer den naechsten `render`-Aufruf. `eye`/`target`/`up`
|
||||
/// in world-Metern (Y-up, wie `types::Camera`). `perspective=true` waehlt eine
|
||||
/// perspektivische Projektion (`fov_y` in Radiant), sonst orthografisch
|
||||
/// (`ortho_half_height` = halbe Sichthoehe in Metern). Die TS-Seite berechnet
|
||||
/// `eye` aus Orbit-Winkeln/Distanz oder einem Praeset (front/top/side/iso/persp)
|
||||
/// — siehe `useWasm3dRenderer`/`Wasm3DViewport`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn set_camera(
|
||||
&mut self,
|
||||
eye_x: f32,
|
||||
eye_y: f32,
|
||||
eye_z: f32,
|
||||
target_x: f32,
|
||||
target_y: f32,
|
||||
target_z: f32,
|
||||
up_x: f32,
|
||||
up_y: f32,
|
||||
up_z: f32,
|
||||
perspective: bool,
|
||||
fov_y: f32,
|
||||
ortho_half_height: f32,
|
||||
) {
|
||||
self.camera = Camera {
|
||||
eye: [eye_x, eye_y, eye_z],
|
||||
target: [target_x, target_y, target_z],
|
||||
up: [up_x, up_y, up_z],
|
||||
projection: if perspective {
|
||||
Projection::Perspective
|
||||
} else {
|
||||
Projection::Orthographic
|
||||
},
|
||||
fov_y,
|
||||
ortho_half_height,
|
||||
..Camera::default()
|
||||
};
|
||||
}
|
||||
|
||||
/// Surface an eine neue Pixelgroesse anpassen (DPR beachtet der Aufrufer).
|
||||
pub fn resize(&mut self, width: u32, height: u32) {
|
||||
let (w, h) = (width.max(1), height.max(1));
|
||||
if self.config.width == w && self.config.height == h {
|
||||
return;
|
||||
}
|
||||
self.config.width = w;
|
||||
self.config.height = h;
|
||||
self.surface.configure(&self.device, &self.config);
|
||||
}
|
||||
|
||||
/// Zeichnet EINEN Frame in die Surface-Textur.
|
||||
pub fn render(&mut self) {
|
||||
let frame = match self.surface.get_current_texture() {
|
||||
Ok(f) => f,
|
||||
Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => {
|
||||
self.surface.configure(&self.device, &self.config);
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
web_sys::console::warn_1(&JsValue::from_str(&format!(
|
||||
"render3d Surface-Fehler: {e:?}"
|
||||
)));
|
||||
return;
|
||||
}
|
||||
};
|
||||
let view = frame
|
||||
.texture
|
||||
.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
self.renderer.render(
|
||||
&self.device,
|
||||
&self.queue,
|
||||
&view,
|
||||
&self.camera,
|
||||
(self.config.width, self.config.height),
|
||||
);
|
||||
frame.present();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user