Skip to content

Commit 168b5e5

Browse files
add an openxr backend
1 parent 9bd8e41 commit 168b5e5

File tree

9 files changed

+1735
-7
lines changed

9 files changed

+1735
-7
lines changed

Cargo.lock

Lines changed: 89 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ name = "app_core"
1111

1212
[dependencies]
1313
bytemuck = { version = "1.24.0", features = ["derive"] }
14+
clap = { version = "4.5", features = ["derive"] }
1415
egui = "0.33.0"
1516
egui-wgpu = { version = "0.33.0", features = ["winit"] }
1617
futures = "0.3.31"
@@ -19,14 +20,18 @@ nalgebra-glm = { version = "0.20.0", features = [
1920
"convert-bytemuck",
2021
"serde-serialize",
2122
] }
23+
openxr = { version = "0.19", features = ["static", "loaded"], optional = true }
2224
web-time = "1.1.0"
2325
wgpu = { version = "27.0.1", default-features = false }
2426
winit = "0.30.12"
2527

2628
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
29+
ash = { version = "0.38", optional = true }
2730
env_logger = "0.11.8"
2831
egui-winit = "0.33.0"
32+
gpu-allocator = { version = "0.27", optional = true }
2933
pollster = "0.4.0"
34+
wgpu-hal = { version = "27.0.4", optional = true }
3035

3136
[target.'cfg(target_arch = "wasm32")'.dependencies]
3237
console_error_panic_hook = "0.1.7"
@@ -37,5 +42,6 @@ wasm-bindgen-futures = "0.4.55"
3742

3843
[features]
3944
default = ["wgpu/default"]
45+
openxr = ["dep:openxr", "dep:ash", "dep:wgpu-hal", "dep:gpu-allocator"]
4046
webgl = ["wgpu/webgl"]
4147
webgpu = ["wgpu/webgpu"]

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ This project demonstrates how to setup a [rust](https://www.rust-lang.org/) proj
44
that uses [wgpu](https://wgpu.rs/) to render a spinning triangle, supporting
55
both webgl and webgpu [wasm](https://webassembly.org/) as well as native.
66

7+
It also includes an [OpenXR](https://www.khronos.org/openxr/) VR mode with hand tracking, procedural skybox, and infinite grid.
8+
79
> If you're looking for a Vulkan example, check out [the vulkan-example repo](https://github.com/matthewjberger/vulkan-example)
810
>
911
> If you're looking for an OpenGL example, check out [the opengl-example repo](https://github.com/matthewjberger/opengl-example)
@@ -12,7 +14,7 @@ both webgl and webgpu [wasm](https://webassembly.org/) as well as native.
1214

1315
## Quickstart
1416

15-
```
17+
```bash
1618
# native
1719
cargo run -r
1820

@@ -21,11 +23,26 @@ trunk serve --features webgpu --open
2123

2224
# webgl
2325
trunk serve --features webgl --open
26+
27+
# OpenXR VR mode
28+
just run-openxr
2429
```
2530

2631
> All chromium-based browsers like Brave, Vivaldi, Chrome, etc support wgpu.
2732
> Firefox also [supports wgpu](https://mozillagfx.wordpress.com/2025/07/15/shipping-webgpu-on-windows-in-firefox-141/) now starting with version `141`.
2833
34+
## OpenXR VR Mode
35+
36+
The OpenXR VR mode renders a spinning triangle, infinite grid, and procedural skybox in virtual reality with hand tracking.
37+
38+
### Setup
39+
40+
1. Install [SteamVR](https://store.steampowered.com/app/250820/SteamVR/)
41+
2. Install [Virtual Desktop](https://www.vrdesktop.net/) (or another OpenXR-compatible VR runtime)
42+
3. Start Virtual Desktop and stream your desktop to your VR headset
43+
4. On your desktop, run `just run-openxr`
44+
5. The application will appear in VR
45+
2946
## Prerequisites (web)
3047

3148
* [trunk](https://trunkrs.dev/)

justfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ lint:
4141
run:
4242
cargo run -r
4343

44+
# Run the desktop app in OpenXR mode
45+
run-openxr:
46+
cargo run -r --features openxr -- --openxr
47+
4448
# Build the app with wgpu + WebGL
4549
build-webgl:
4650
trunk build --features webgl

src/grid.wgsl

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
struct Uniform {
2+
view_proj: mat4x4<f32>,
3+
camera_world_pos: vec3<f32>,
4+
grid_size: f32,
5+
grid_min_pixels: f32,
6+
grid_cell_size: f32,
7+
orthographic_scale: f32,
8+
is_orthographic: f32,
9+
}
10+
11+
@group(0) @binding(0)
12+
var<uniform> ubo: Uniform;
13+
14+
struct VertexOutput {
15+
@builtin(position) clip_position: vec4<f32>,
16+
@location(0) world_pos: vec3<f32>,
17+
};
18+
19+
@vertex
20+
fn vertex_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
21+
var pos = vec3<f32>(0.0);
22+
23+
switch vertex_index {
24+
case 0u: { pos = vec3<f32>(-10.0, 0.0, -10.0); }
25+
case 1u: { pos = vec3<f32>(10.0, 0.0, -10.0); }
26+
case 2u: { pos = vec3<f32>(-10.0, 0.0, 10.0); }
27+
case 3u: { pos = vec3<f32>(-10.0, 0.0, 10.0); }
28+
case 4u: { pos = vec3<f32>(10.0, 0.0, -10.0); }
29+
case 5u: { pos = vec3<f32>(10.0, 0.0, 10.0); }
30+
default: {}
31+
}
32+
33+
let grid_scale = select(1.0, max(10.0, ubo.orthographic_scale * 100.0), ubo.is_orthographic > 0.5);
34+
pos = pos * ubo.grid_size * grid_scale;
35+
let world_pos = vec3<f32>(
36+
pos.x + ubo.camera_world_pos.x,
37+
0.0,
38+
pos.z + ubo.camera_world_pos.z
39+
);
40+
41+
var output: VertexOutput;
42+
var clip_pos = ubo.view_proj * vec4<f32>(world_pos, 1.0);
43+
44+
if (ubo.is_orthographic > 0.5) {
45+
clip_pos.z = clamp(clip_pos.z, 0.0, clip_pos.w);
46+
}
47+
48+
output.clip_position = clip_pos;
49+
output.world_pos = world_pos;
50+
return output;
51+
}
52+
53+
fn mod_pos(pos: f32, size: f32) -> f32 {
54+
return pos - size * floor(pos / size);
55+
}
56+
57+
58+
@fragment
59+
fn fragment_main(in: VertexOutput) -> @location(0) vec4<f32> {
60+
let dvx = vec2<f32>(dpdx(in.world_pos.x), dpdy(in.world_pos.x));
61+
let dvy = vec2<f32>(dpdx(in.world_pos.z), dpdy(in.world_pos.z));
62+
let lx = length(dvx);
63+
let ly = length(dvy);
64+
let dudv = vec2<f32>(lx, ly);
65+
let l = length(dudv);
66+
67+
let effective_scale = select(l, l * ubo.orthographic_scale, ubo.orthographic_scale > 1.0);
68+
let lod = max(0.0, log10(effective_scale * ubo.grid_min_pixels / ubo.grid_cell_size) + 1.0);
69+
let cell_size_lod0 = ubo.grid_cell_size * pow(10.0, floor(lod));
70+
let cell_size_lod1 = cell_size_lod0 * 10.0;
71+
let cell_size_lod2 = cell_size_lod1 * 10.0;
72+
73+
let dudv4 = dudv * 8.0;
74+
75+
let mod_lod0 = vec2<f32>(
76+
mod_pos(in.world_pos.x, cell_size_lod0),
77+
mod_pos(in.world_pos.z, cell_size_lod0)
78+
) / dudv4;
79+
let lod0_alpha = max2(vec2<f32>(1.0) - abs(saturate(mod_lod0) * 2.0 - vec2<f32>(1.0)));
80+
81+
let mod_lod1 = vec2<f32>(
82+
mod_pos(in.world_pos.x, cell_size_lod1),
83+
mod_pos(in.world_pos.z, cell_size_lod1)
84+
) / dudv4;
85+
let lod1_alpha = max2(vec2<f32>(1.0) - abs(saturate(mod_lod1) * 2.0 - vec2<f32>(1.0)));
86+
87+
let mod_lod2 = vec2<f32>(
88+
mod_pos(in.world_pos.x, cell_size_lod2),
89+
mod_pos(in.world_pos.z, cell_size_lod2)
90+
) / dudv4;
91+
let lod2_alpha = max2(vec2<f32>(1.0) - abs(saturate(mod_lod2) * 2.0 - vec2<f32>(1.0)));
92+
93+
let lod_fade = fract(lod);
94+
95+
let grid_color_thin = vec4<f32>(0.75, 0.75, 0.75, 0.25);
96+
let grid_color_thick = vec4<f32>(0.2, 0.4, 0.8, 0.4);
97+
98+
var color: vec4<f32>;
99+
if (lod2_alpha > 0.0) {
100+
color = grid_color_thick;
101+
color.a *= lod2_alpha * 0.7;
102+
} else if (lod1_alpha > 0.0) {
103+
let fade = smoothstep(0.2, 0.8, lod_fade);
104+
color = mix(grid_color_thick, grid_color_thin, fade);
105+
color.a *= lod1_alpha * 0.5;
106+
} else {
107+
color = grid_color_thin;
108+
color.a *= (lod0_alpha * (1.0 - lod_fade)) * 0.4;
109+
}
110+
111+
if (ubo.is_orthographic < 0.5) {
112+
let dist = length(in.world_pos.xz - ubo.camera_world_pos.xz);
113+
let opacity_falloff = 1.0 - smoothstep(0.8 * ubo.grid_size, ubo.grid_size * 3.0, dist);
114+
color.a *= opacity_falloff;
115+
}
116+
117+
let x_axis_nearby = abs(in.world_pos.z) < 0.03;
118+
let z_axis_nearby = abs(in.world_pos.x) < 0.03;
119+
120+
if (x_axis_nearby) {
121+
color = mix(color, vec4<f32>(0.87, 0.26, 0.24, 0.7), 0.5);
122+
}
123+
if (z_axis_nearby) {
124+
color = mix(color, vec4<f32>(0.24, 0.7, 0.29, 0.7), 0.5);
125+
}
126+
127+
if (color.a < 0.02) {
128+
discard;
129+
}
130+
131+
return color;
132+
}
133+
134+
135+
fn log10(x: f32) -> f32 {
136+
return log2(x) / log2(10.0);
137+
}
138+
139+
fn saturate(x: vec2<f32>) -> vec2<f32> {
140+
return clamp(x, vec2<f32>(0.0), vec2<f32>(1.0));
141+
}
142+
143+
fn saturate_f32(x: f32) -> f32 {
144+
return clamp(x, 0.0, 1.0);
145+
}
146+
147+
fn max2(v: vec2<f32>) -> f32 {
148+
return max(v.x, v.y);
149+
}

0 commit comments

Comments
 (0)