Neuigkeiten von trion.
Immer gut informiert.

3D im Browser mit WebGPU, Rust, WebAssembly und Angular

Im letzten Artikel zum Thema WebAssembly haben wir uns C und Rust für WebAssembly angeschaut. Darin konnten wir erfahren, wie WebAssemblies als Hallo, Welt! in der Browserwelt das Laufen lernen.

Diesmal schauen wir uns ein etwas komplexeres Szenario an: Eine Rust-basierende WebGPU 3D-Szene, eingebunden in eine Angular-Anwendung.

Das Beispiel-Projekt

3D Programmierung mit Rust und WebGPU ist sehr "low level" und nicht tivial. Auf der anderen Seite eröffnen sich viele Möglichkeiten durch den Browser als vielversprechende Platform der Zukunft. Wir werden uns im Folgenden auf das für das Verständnis Notwendige beschränken.

Der Projektaufbau ist unspektakulär:

Schauen wir in die TOML Spezifikationsdatei:

[package]
name = "wgpu-demo"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

...

[target.'cfg(target_arch = "wasm32")'.dependencies]
reqwest = { version = "0.11" }
console_error_panic_hook = "0.1"
console_log = "0.2"
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
...

Für die WebAssembly Architektur werden Bindings sowie u.a. Bibliotheken für den Zugriff auf die Browser-Konsole angegeben. Der Browser ist schliesslich das natürliche Habitat für WebAssembly-Code.

Wir wollen eine Bibliothek mit der 3D-Szene verwenden. Der Rust-Einstiegspunkt ist daher src/lib.rs. Hier erfolgt der Aufbau der 3D-Modelle, deren Texturierung, Beleuchtung, der Start der Render-Engine, etc. Interessant für unser Szenario sind darin die folgenden Zeilen:

#[cfg(target_arch = "wasm32")]
{
    // Winit prevents sizing with CSS, so we have to set
    // the size manually when on web.
    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.");
}

Hier wird - konditional für die Webassembly-Architektur - das Fenster mit der 3D Szene in das DOM des Browser eingebunden (DOM-Element: "wasm-example").

Bau der Beispielanwendung

Rust bringt bekanntlich Werkzeuge für WebAssemblies mit:

$ wasm-pack build --target web

Dadurch wird in das Unterverzeichnis pkg gebaut. Nach dem Bau finden wir dort die folgenden Dateien:

package.json
wgpu_demo.d.ts
wgpu_demo.js
wgpu_demo_bg.wasm
wgpu_demo_bg.wasm.d.ts

Es handelt sich offensichtlich um ein Paket, das wir via NPM auf dem Repository unserer Wahl verfügbar machen können:

$ npm publish

Ein erster Blick auf die Anwendung

Projekt-lokal können wir die 3D Szene via HTML visualisieren. Hier finden wir auch unser DOM-Element wasm-example sowie die generierte JavaScript-Schnittstelle zum WebAssembly (wgpu_demo.js) wieder:

<!DOCTYPE html>
<html lang="de">

<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>Unsere kleine Welt</title>
</head>

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

</html>

Publiziert man die Anwendung z.B. mit der Lieblingssprache des Autors…​

$ python3 -m http.server 9000

…​dann zeigt der Browser nach Laden der URL die 3D-Szene:

Einbinden in Angular

Um unser WebAssembly-NPM Paket in eine Angular-Anwendung zu integrieren, erzeugen wir zunächst eine solche und binden im Anschluss unser Paket ein:

$ ng new wgpu-angular
$ cd wgpu-angular
$ npm install wgpu-demo

Dann noch etwas TypeScript Code in die Default-Komponente:

import {Component, OnInit} from '@angular/core';
import init from 'wgpu-demo';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  name = 'Angular';
  title = 'wgpu-demo';

  ngOnInit(){
    let wasm_url = new URL('assets/wgpu_demo_bg.wasm')
    init(wasm_url).then(() => {
      console.log("WASM Loaded");
    });
  }
}

Im HTML-Fragment unserer Angular-Komponente ist nicht viel zu tun:

<div>wgpu-demo</div>
<div id="wasm-example"></div>

Hier finden wir wieder unser DOM-Element wasm-example.

Abschließend müssen wir die Ressourcen (Texturen, Modelle, etc. in res) sowie das WebAssembly-Binary in assets statisch verfügbar machen und via ng publizieren:

$ ng serve

Mission erfüllt: Unsere Anwendung mit der 3D Szene in ist im Browser verfügbar!


Die im Artikel verwendeten Quelldateien finden Sie hier.




Zu den Themen Python und Webtechnologien bieten wir sowohl Beratung, Entwicklungsunterstützung als auch passende Schulungen an:

Auch für Ihren individuellen Bedarf können wir Workshops und Schulungen anbieten. Sprechen Sie uns gerne an.

Feedback oder Fragen zu einem Artikel - per Twitter @triondevelop oder E-Mail freuen wir uns auf eine Kontaktaufnahme!

Zur Desktop Version des Artikels