Skip to content

Commit 3dc7f9a

Browse files
Add Primitive Picking sample (#539)
Writes the `primitive_index` to a secondary render target and uses that to highlight the primitive that the pointer is over on subsequent frames. Also includes a mode that visualizes the primitive index. Used as a (somewhat contrived) example of using the `primitive_index` builtin. This was based on the deferred shading sample, but has been modified significantly. --------- Co-authored-by: François Beaufort <[email protected]>
1 parent 8bd908e commit 3dc7f9a

File tree

11 files changed

+628
-4
lines changed

11 files changed

+628
-4
lines changed

package-lock.json

Lines changed: 5 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
struct Frame {
2+
viewProjectionMatrix : mat4x4f,
3+
invViewProjectionMatrix : mat4x4f,
4+
pickCoord : vec2f,
5+
pickedPrimitive : u32,
6+
}
7+
@group(0) @binding(0) var<storage, read_write> frame : Frame;
8+
@group(0) @binding(1) var primitiveTex : texture_2d<u32>;
9+
10+
@compute @workgroup_size(1)
11+
fn main() {
12+
// Load the primitive index from the picking texture and store it in the
13+
// pickedPrimitive value (exposed to the rendering shaders as a uniform).
14+
let texel = vec2u(frame.pickCoord);
15+
frame.pickedPrimitive = textureLoad(primitiveTex, texel, 0).x;
16+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
enable primitive_index;
2+
3+
struct Frame {
4+
viewProjectionMatrix : mat4x4f,
5+
invViewProjectionMatrix : mat4x4f,
6+
pickCoord : vec2f,
7+
pickedPrimitive : u32,
8+
}
9+
@group(0) @binding(1) var<uniform> frame : Frame;
10+
11+
struct PassOutput {
12+
@location(0) color : vec4f,
13+
@location(1) primitive : u32,
14+
}
15+
16+
@fragment
17+
fn main(
18+
@location(0) fragNormal : vec3f,
19+
@builtin(primitive_index) primIndex : u32
20+
) -> PassOutput {
21+
// Very simple N-dot-L lighting model
22+
let lightDirection = normalize(vec3f(4, 10, 6));
23+
let light = dot(normalize(fragNormal), lightDirection) * 0.5 + 0.5;
24+
let surfaceColor = vec4f(0.8, 0.8, 0.8, 1.0);
25+
26+
var output : PassOutput;
27+
28+
// Highlight the primitive if it's the selected one, otherwise shade normally.
29+
if (primIndex+1 == frame.pickedPrimitive) {
30+
output.color = vec4f(1.0, 1.0, 0.0, 1.0);
31+
} else {
32+
output.color = vec4f(surfaceColor.xyz * light, surfaceColor.a);
33+
}
34+
35+
// Adding one to each primitive index so that 0 can mean "nothing picked"
36+
output.primitive = primIndex+1;
37+
return output;
38+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
@group(0) @binding(0) var primitiveTex: texture_2d<u32>;
2+
3+
@fragment
4+
fn main(
5+
@builtin(position) coord : vec4f
6+
) -> @location(0) vec4f {
7+
// Load the primitive index for this pixel from the picking texture.
8+
let primitiveIndex = textureLoad(primitiveTex, vec2i(floor(coord.xy)), 0).x;
9+
var result : vec4f;
10+
11+
// Generate a color for the primitive index. If we only increment the color
12+
// channels by 1 for each primitive index we can show a very large range of
13+
// unique values but it can make the individual primitives hard to distinguish.
14+
// This code steps through 8 distinct values per-channel, which may end up
15+
// repeating some colors for larger meshes but makes the unique primitive
16+
// index values easier to see.
17+
result.r = f32(primitiveIndex % 8) / 8;
18+
result.g = f32((primitiveIndex / 8) % 8) / 8;
19+
result.b = f32((primitiveIndex / 64) % 8) / 8;
20+
result.a = 1.0;
21+
return result;
22+
}

sample/primitivePicking/index.html

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
6+
<title>webgpu-samples: primitivePicking</title>
7+
<!-- WebGPUCompatibilityMode origin token for https://webgpu.github.io expiring April 21, 2026 -->
8+
<meta
9+
http-equiv="origin-trial"
10+
content="Aktu7041jFm00ls336/bRinubASRzg1tPs4wxXOZkF1uP0LaIURinGC7ti0Vf352Q9OKFL1siRfpptLjNIKpKQcAAABheyJvcmlnaW4iOiJodHRwczovL3dlYmdwdS5naXRodWIuaW86NDQzIiwiZmVhdHVyZSI6IldlYkdQVUNvbXBhdGliaWxpdHlNb2RlIiwiZXhwaXJ5IjoxNzc2NzI5NjAwfQ=="
11+
/>
12+
<!-- WebGPUCompatibilityMode origin token for http://localhost:8080 expiring April 21, 2026 -->
13+
<meta
14+
http-equiv="origin-trial"
15+
content="AqW27Ayelg5vbcAaYcweU+sLjZq5r6idHCWU4MJgnkP1YBgmOMqazdGuakSnGylTkyA/bRHkCJZFdfYjFlylOgAAAABaeyJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjgwODAiLCJmZWF0dXJlIjoiV2ViR1BVQ29tcGF0aWJpbGl0eU1vZGUiLCJleHBpcnkiOjE3NzY3Mjk2MDB9"
16+
/>
17+
<style>
18+
:root {
19+
color-scheme: light dark;
20+
}
21+
html, body {
22+
margin: 0; /* remove default margin */
23+
height: 100%; /* make body fill the browser window */
24+
display: flex;
25+
place-content: center center;
26+
}
27+
canvas {
28+
width: 600px;
29+
height: 600px;
30+
max-width: 100%;
31+
display: block;
32+
}
33+
</style>
34+
<script defer src="main.js" type="module"></script>
35+
<script defer type="module" src="../../js/iframe-helper.js"></script>
36+
</head>
37+
<body>
38+
<canvas></canvas>
39+
</body>
40+
</html>

0 commit comments

Comments
 (0)