A two-window Tauri 2 + Rust + wgpu example demonstrating GPU morph-target animation with a fixed mesh topology.
This project follows the established numbered Junkpile native-wgpu format:
main.rs; wgpu owns this window surface directly.The renderer begins with one procedural sphere topology and generates three compatible targets at startup:
Every vertex stores the position and normal for the base shape and all three targets. The mesh and index buffers remain unchanged after startup. The WGSL vertex shader blends the target streams every frame using uniform weights.
controls WebView window
↓ Tauri commands
RendererHandle / RenderCommand channel
↓ renderer thread
uniform upload
↓
WGSL vertex shader
├─ base position / normal
├─ cube position / normal
├─ torus position / normal
└─ bloom position / normal
↓
separate native renderer window
↓
Metal / Vulkan / DX12 surface
The WebView never overlays or owns the graphics surface. Closing the controls window exits the application. Resizing or fullscreening the renderer window updates the wgpu surface independently.
cd 23-wgpu-morph-target-lab
npm install
npm run dev
Optional backend launchers:
npm run dev:metal
npm run dev:vulkan
npm run dev:dx12
The example declares its own Cargo workspace in src-tauri/Cargo.toml. This prevents Cargo from inheriting an unrelated parent Cargo.toml when the project is placed inside a larger repository.
Rounded cube, Torus, and Harmonic bloom are the manual target weights. When Auto morph is enabled, Auto amount blends the manual values toward three phase-offset oscillators. The smaller bars beneath the sliders show the effective values actually sent to the GPU.
Blend rules:
23-wgpu-morph-target-lab/
├── package.json
├── README.md
├── scripts/
│ └── run-backend.mjs
├── src/
│ ├── index.html controls WebView
│ ├── styles.css controls styling
│ └── app.js Tauri IPC and renderer telemetry
└── src-tauri/
├── Cargo.toml
├── tauri.conf.json declares only the controls WebView
├── capabilities/
│ └── default.json
└── src/
├── main.rs creates native renderer window and routes commands
├── mesh.rs procedural compatible target generation
├── renderer.rs wgpu resources, state, surface, and render thread
└── morph.wgsl GPU position/normal blending and shading
Each MorphVertex contains:
base_position vec3
cube_position vec3
torus_position vec3
bloom_position vec3
base_normal vec3
cube_normal vec3
torus_normal vec3
bloom_normal vec3
uv vec2
That is 104 bytes per vertex. This favors clarity over the most memory-efficient production layout. A production SDK could move target deltas into storage buffers, compress normals, stream selected targets, or execute a compute pass into a compact render buffer.
This is morph-target animation, not skeletal skinning: