Skip to content

@typegpu/kitpicker

Picker

Find a candidate under the pointer on the GPU, then use it in the same frame or read it back to the CPU.

kit.picker

module

picker.create(root, camera, { capacity, size, position, … }) → Picker

rootborrows
the root
cameraneeds
the camera the candidates are drawn with
positiontakes
where each candidate is; it, radius, alive and rank carry the fills of what they read, such as Star.position.pipe(stars.inject())
Pickerreturns
a picker you destroy
GPU functions
byPointerbyEye
schemas
SettingsCandidate
constants
nonedefaults
types
Picker
Optionscreate · 9 fields
capacity

number

Sets the number of candidate slots, including dead slots.

size

SizeSource

Supplies the view size at each pick, such as the canvas surface.

position

(i: number) => d.v3f

Returns the candidate center in world space.

radius?

(i: number) => number= 0, a point

Returns the candidate radius in world units.

alive?

(i: number) => boolean= every candidate can be picked

Tests whether a candidate can be picked.

span?

() => number= capacity

Returns the highest live slot plus one, read when pick records, such as a table's from live().

rank?

(candidate: Candidate) => number= byPointer

Ranks candidates within reach in a 'use gpu' function, from 0 to 1.

hz?

number= none

Reads results this many times per second for hovering, which picks every frame.

settings?

Partial<d.InferInput<typeof Settings>>= defaults, 24 points

Sets the initial reach.

records
  • result.captureframe
  • pickcompute

pick once in a frame that picks, in a compute pass after whatever moves the candidates; result.capture(frame) after that pass, which copies what a pick found.

owns

3 buffers ·a pipeline ·a readback

The pick's key, the pointer and the settings; the readback brings the key to the CPU.

lifecycle

initAsync()destroy()[Symbol.dispose]

injects
  • per-instance

    found, for shaders that act on the pick in the same frame.

binds
  • 1 storage

    found, the pick's key.

needs
  • cameras

    As camera; fills a hook in its pipelines.

star-map20 000 stars; the one under the pointer is picked on the GPU.

Hover a star to light it in the same frame through found(). result reaches the CPU frames later. The dashed ring shows reach around the pointer. Set nearest to eye to choose the frontmost star within reach. The picker’s rank reads this choice from a uniform. Click a star to select it.

Wisps move on the GPU, so the CPU needs their positions to find one under a finger. Reading all positions copies the whole set and adds a frame or two of delay. A picking kernel instead projects every candidate with your camera and selects the nearest within reach. It returns one index. Later kernels can use that index in the same frame, before it reaches the CPU.

const
const hits: picker.Picker
hits
=
import picker
picker
.
function create(root: TgpuRoot, camera: cameras.CameraBase, options: picker.Options): picker.Picker

Finds a candidate under a screen point on the GPU. Projects every candidate with the camera and keeps the one within reach with the best rank. Throws unless capacity is an integer from 1 to 2^24.

@injects ― per-instance found, for shaders that act on the pick in the same frame

@binds ― 1 storage: found, the pick's key

@owns ― 3 buffers, a pipeline, a readback: the pick's key, the pointer and the settings; the readback brings the key to the CPU

@records ― pick once in a frame that picks, in a compute pass after whatever moves the candidates; result.capture(frame) after that pass, which copies what a pick found.

@param ― camera the camera the candidates are drawn with

@param ― options.position where each candidate is; it, radius, alive and rank carry the fills of what they read, such as Star.position.pipe(stars.inject())

@returns ― a picker you destroy

create
(
const root: TgpuRoot
root
,
const view: cameras.OrbitCamera
view
, {
Options.capacity: number

Sets the number of candidate slots, including dead slots. Indices range from 0 to `capacity

  • 1`. Must be an integer from 1 to 2^24.

capacity
: 512,
Options.size: SizeSource

Supplies the view size at each pick, such as the canvas surface. pixelRatio scales reach from screen points to pixels. Without it, one point equals one pixel.

size
:
const surface: web.Surface
surface
,
Options.position: (i: number) => d.v3f

Returns the candidate center in world space.

position
: (
i: number
i
) => {
'use gpu';
return
import d
d
.
const vec3f: d.Vec3f
(v: AnyNumericVec3Instance) => d.v3f (+5 overloads)
vec3f
(
const wisps: TgpuReadonly<d.WgslArray<d.Vec3f>>
wisps
.
TgpuReadonly<WgslArray<Vec3f>>.$: d.v3f[]
$
[
i: number
i
]!);
},
});
let
let tap: [number, number] | undefined
tap
: [number, number] | undefined;
const canvas: HTMLCanvasElement
canvas
.
HTMLCanvasElement.addEventListener<"pointerup">(type: "pointerup", listener: (this: HTMLCanvasElement, ev: PointerEvent) => any, options?: boolean | AddEventListenerOptions): void (+1 overload)

The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.

MDN Reference

addEventListener
('pointerup', (
event: PointerEvent
event
) => {
let tap: [number, number] | undefined
tap
= [
event: PointerEvent
event
.
MouseEvent.offsetX: number

The offsetX read-only property of the MouseEvent interface provides the offset in the X coordinate of the mouse pointer between that event and the padding edge of the target node.

MDN Reference

offsetX
/
const canvas: HTMLCanvasElement
canvas
.
Element.clientWidth: number

The clientWidth read-only property of the Element interface is zero for inline elements and elements with no CSS; otherwise, it's the inner width of an element in pixels. It includes padding but excludes borders, margins, and vertical scrollbars (if present).

MDN Reference

clientWidth
,
event: PointerEvent
event
.
MouseEvent.offsetY: number

The offsetY read-only property of the MouseEvent interface provides the offset in the Y coordinate of the mouse pointer between that event and the padding edge of the target node.

MDN Reference

offsetY
/
const canvas: HTMLCanvasElement
canvas
.
Element.clientHeight: number

The clientHeight read-only property of the Element interface is zero for elements with no CSS or inline layout boxes; otherwise, it's the inner height of an element in pixels. It includes padding but excludes borders, margins, and horizontal scrollbars (if present).

MDN Reference

clientHeight
];
});
import frames
frames
.
function create(root: TgpuRoot, record: Recorder): frames.Frames

Records a frame into one encoder and submits once, with a clock. Borrows the root. The synchronous recorder borrows each frame's encoder.

@submits ― always once a frame after your recorder returns, and again at each frame.submit()

@param ― record your frame, recorded into each frame's encoder

@returns ― a loop you run with each frame's time and destroy

create
(
const root: TgpuRoot
root
, (
frame: frames.Frame
frame
) => {
const
const compute: TgpuComputePass
compute
=
frame: frames.Frame
frame
.
Frame.encoder: TgpuCommandEncoder

Records commands for submission after the recorder returns. submit() replaces this encoder. Read it at each call instead of keeping it, and let the frame submit it.

encoder
.
TgpuCommandEncoder.beginComputePass(descriptor?: TgpuComputePassDescriptor): TgpuComputePass

Begins recording a compute pass

beginComputePass
();
// ... the steps that move the wisps
if (
let tap: [number, number] | undefined
tap
)
const hits: picker.Picker
hits
.
Picker.pick(pass: TgpuComputePass, x: number, y: number): void

Picks at x, y from 0 to 1 across the view, from the top-left corner, as view.rayAt and pointing take them. Uses the camera given to create. Records one pick per frame. A second call replaces the first.

pick
(
const compute: TgpuComputePass
compute
, ...
let tap: [number, number]
tap
);
const compute: TgpuComputePass
compute
.
TgpuComputePass.end(): void

Completes the recording of this compute pass

end
();
const hits: picker.Picker
hits
.
Picker.result: Readback<number | null>

Reads the picked candidate index to the CPU, or null if nothing was found. Capture after the picker pass, then call take() or await next(). Without hz, each pick requests a copy for the next capture.

result
.
Readback<number | null>.capture(frame: frames.Frame): void

Records a copy when a next() is waiting, a request is pending or the capture rate is due. Maps the copy after submission. Call after the pass that writes the buffer. A second call in the same frame does nothing.

capture
(
frame: frames.Frame
frame
);
let tap: [number, number] | undefined
tap
=
var undefined
undefined
;
const
const found: number | null | undefined
found
=
const hits: picker.Picker
hits
.
Picker.result: Readback<number | null>

Reads the picked candidate index to the CPU, or null if nothing was found. Capture after the picker pass, then call take() or await next(). Without hz, each pick requests a copy for the next capture.

result
.
Readback<number | null>.take(): number | null | undefined

Returns latest once per completed copy, then undefined until the next copy completes. Each value goes to one caller. Callers sharing a readback split the values between them.

take
(); // undefined until a pick comes back
if (
const found: number | null | undefined
found
!==
var undefined
undefined
)
function select(wisp: number | null): void
select
(
const found: number | null
found
);
});
  • x and y run from 0 to 1 across the view from its top-left corner, as a host’s pointing and a camera’s rayAt take them. settings.reach is a distance in points. The picker reads size and its pixelRatio at each pick, as text does, so reach stays the same when a window moves between screens. Without a pixelRatio, a point is a pixel.
  • A candidate is within reach when its centre is within reach points, plus its radius as it looks from the eye. One behind the eye, or one alive turns down, is never picked.
  • pick writes the point and clears the result with queue writes, so a frame picks once; a second pick replaces the first.
  • result is a readback of the candidate’s index, or null for nothing. Capture it after the pass that picks. Without hz, each tap comes back once, a frame or two later.

Several candidates can be within reach. rank is a 'use gpu' function that maps a picker.Candidate to a number from 0 to 1. The lowest rank wins. Ties go to the lower index. A candidate holds:

  • index;
  • gap, from the point to its centre on the screen, as a fraction of the screen’s diagonal;
  • depth, 0 at the near plane and 1 at the far.

picker.byPointer, the default, ranks by gap, for wisps a finger covers. picker.byEye ranks by depth, the one in front, for a hover among stars that overlap on the screen.

A rank that reads a uniform switches between them without a new picker, as the star map’s nearest does:

const
const byEye: TgpuUniform<d.U32>
byEye
=
const root: TgpuRoot
root
.
TgpuRoot.createUniform<d.U32>(typeSchema: d.U32, initial?: number | ((buffer: TgpuBuffer<NoInfer<d.U32>>) => void) | undefined): TgpuUniform<d.U32> (+1 overload)

Allocates memory on the GPU, allows passing data between host and shader. Read-only on the GPU, optimized for small data. For a general-purpose buffer, use

TgpuRoot.createBuffer

.

@param ― typeSchema The type of data that this buffer will hold.

@param ― initial Either initial value of the buffer, or an initializer to execute on the mapped buffer. (optional)

createUniform
(
import d
d
.
const u32: d.U32
export u32

A schema that represents an unsigned 32-bit integer value. (equivalent to u32 in WGSL)

Can also be called to cast a value to an u32 in accordance with WGSL casting rules.

@example const value = u32(); // 0

@example const value = u32(7); // 7

@example const value = u32(3.14); // 3

@example const value = u32(-1); // 4294967295

@example const value = u32(-3.1); // 0

u32
, 0);
const
const hits: picker.Picker
hits
=
import picker
picker
.
function create(root: TgpuRoot, camera: cameras.CameraBase, options: picker.Options): picker.Picker

Finds a candidate under a screen point on the GPU. Projects every candidate with the camera and keeps the one within reach with the best rank. Throws unless capacity is an integer from 1 to 2^24.

@injects ― per-instance found, for shaders that act on the pick in the same frame

@binds ― 1 storage: found, the pick's key

@owns ― 3 buffers, a pipeline, a readback: the pick's key, the pointer and the settings; the readback brings the key to the CPU

@records ― pick once in a frame that picks, in a compute pass after whatever moves the candidates; result.capture(frame) after that pass, which copies what a pick found.

@param ― camera the camera the candidates are drawn with

@param ― options.position where each candidate is; it, radius, alive and rank carry the fills of what they read, such as Star.position.pipe(stars.inject())

@returns ― a picker you destroy

create
(
const root: TgpuRoot
root
,
const view: cameras.OrbitCamera
view
, {
Options.capacity: number

Sets the number of candidate slots, including dead slots. Indices range from 0 to `capacity

  • 1`. Must be an integer from 1 to 2^24.

capacity
: 512,
Options.size: SizeSource

Supplies the view size at each pick, such as the canvas surface. pixelRatio scales reach from screen points to pixels. Without it, one point equals one pixel.

size
,
Options.position: (i: number) => d.v3f

Returns the candidate center in world space.

position
: (
i: number
i
) => {
'use gpu';
return
import d
d
.
const vec3f: d.Vec3f
(v: AnyNumericVec3Instance) => d.v3f (+5 overloads)
vec3f
(
const stars: TgpuReadonly<d.WgslArray<d.Vec3f>>
stars
.
TgpuReadonly<WgslArray<Vec3f>>.$: d.v3f[]
$
[
i: number
i
]!);
},
Options.rank?: ((candidate: picker.Candidate) => number) | undefined

Ranks candidates within reach in a 'use gpu' function, from 0 to 1. Keeps the lowest rank, then the lowest index among ties. Can read a uniform to switch rankings, such as between byPointer and byEye, without recreating the picker. Default byPointer.

rank
: (
candidate: {
index: number;
gap: number;
depth: number;
}
candidate
) => {
'use gpu';
return
import std
std
.
function select(f: number, t: number, cond: boolean): number (+2 overloads)
export select

Returns t if cond is true, and f otherwise. Component-wise if cond is a vector.

@example select(1, 2, false) // returns 1 select(1, 2, true) // returns 2 select(vec2i(1, 2), vec2i(3, 4), true) // returns vec2i(3, 4) select(vec2i(1, 2), vec2i(3, 4), vec2b(false, true)) // returns vec2i(1, 4)

select
(
import picker
picker
.
const byPointer: (candidate: picker.Candidate) => number

Ranks candidates by the screen distance from their center to the pointer, nearest first.

byPointer
(
candidate: {
index: number;
gap: number;
depth: number;
}
candidate
),
import picker
picker
.
const byEye: (candidate: picker.Candidate) => number

Ranks candidates by depth, nearest the eye first.

byEye
(
candidate: {
index: number;
gap: number;
depth: number;
}
candidate
),
const byEye: TgpuUniform<d.U32>
byEye
.
TgpuUniform<U32>.$: number
$
=== 1);
},
});
const byEye: TgpuUniform<d.U32>
byEye
.
TgpuBufferBindingBase<U32>.write(data: number, options?: BufferWriteOptions): void
write
(1); // the next pick keeps the star in front

radius, in world units, makes a big candidate easier to hit, and grows on the screen as it comes closer. Here guns are wider than wisps:

const
const hits: picker.Picker
hits
=
import picker
picker
.
function create(root: TgpuRoot, camera: cameras.CameraBase, options: picker.Options): picker.Picker

Finds a candidate under a screen point on the GPU. Projects every candidate with the camera and keeps the one within reach with the best rank. Throws unless capacity is an integer from 1 to 2^24.

@injects ― per-instance found, for shaders that act on the pick in the same frame

@binds ― 1 storage: found, the pick's key

@owns ― 3 buffers, a pipeline, a readback: the pick's key, the pointer and the settings; the readback brings the key to the CPU

@records ― pick once in a frame that picks, in a compute pass after whatever moves the candidates; result.capture(frame) after that pass, which copies what a pick found.

@param ― camera the camera the candidates are drawn with

@param ― options.position where each candidate is; it, radius, alive and rank carry the fills of what they read, such as Star.position.pipe(stars.inject())

@returns ― a picker you destroy

create
(
const root: TgpuRoot
root
,
const view: cameras.OrbitCamera
view
, {
Options.capacity: number

Sets the number of candidate slots, including dead slots. Indices range from 0 to `capacity

  • 1`. Must be an integer from 1 to 2^24.

capacity
:
const WISPS: 480
WISPS
+ 32,
Options.size: SizeSource

Supplies the view size at each pick, such as the canvas surface. pixelRatio scales reach from screen points to pixels. Without it, one point equals one pixel.

size
,
Options.position: (i: number) => d.v3f

Returns the candidate center in world space.

position
: (
i: number
i
) => {
'use gpu';
if (
i: number
i
<
const WISPS: 480
WISPS
) return
const wisps: TgpuReadonly<d.WgslArray<d.Vec4f>>
wisps
.
TgpuReadonly<WgslArray<Vec4f>>.$: d.v4f[]
$
[
i: number
i
]!.
xyz: d.v3f
xyz
;
return
import d
d
.
const vec3f: d.Vec3f
(v: AnyNumericVec3Instance) => d.v3f (+5 overloads)
vec3f
(
const guns: TgpuReadonly<d.WgslArray<d.Vec3f>>
guns
.
TgpuReadonly<WgslArray<Vec3f>>.$: d.v3f[]
$
[
i: number
i
-
const WISPS: 480
WISPS
]!);
},
Options.radius?: ((i: number) => number) | undefined

Returns the candidate radius in world units. Default 0, a point.

radius
: (
i: number
i
) => {
'use gpu';
return
import std
std
.
function select(f: number, t: number, cond: boolean): number (+2 overloads)
export select

Returns t if cond is true, and f otherwise. Component-wise if cond is a vector.

@example select(1, 2, false) // returns 1 select(1, 2, true) // returns 2 select(vec2i(1, 2), vec2i(3, 4), true) // returns vec2i(3, 4) select(vec2i(1, 2), vec2i(3, 4), vec2b(false, true)) // returns vec2i(1, 4)

select
(0, 0.6,
i: number
i
>=
const WISPS: 480
WISPS
);
},
Options.alive?: ((i: number) => boolean) | undefined

Tests whether a candidate can be picked. Default every candidate can be picked.

alive
: (
i: number
i
) => {
'use gpu';
return
i: number
i
>=
const WISPS: 480
WISPS
||
const wisps: TgpuReadonly<d.WgslArray<d.Vec4f>>
wisps
.
TgpuReadonly<WgslArray<Vec4f>>.$: d.v4f[]
$
[
i: number
i
]!.
v4f.w: number
w
> 0;
},
});

The picker kernel compiles these callbacks. They can read cameras.current.$ and any resources they have fills for, such as Star.position.pipe(stars.inject()). Candidates in a table take its capacity, alive and span from stars.live(): picker.create(root, view, { ...stars.live(), size, position }).

hits.found() is the candidate the last pick found, or picker.none, in any shader created from root.pipe(hits.inject()). A kernel after pick sees this frame’s pick, so a tapped wisp lights up the frame the tap lands:

const
const mark: TgpuComputeFn<{}>
mark
=
const tgpu: {
const: typeof import("node_modules/typegpu/core/constant/tgpuConstant").constant;
fn: typeof import("node_modules/typegpu/core/function/tgpuFn").fn;
comptime: typeof import("node_modules/typegpu/core/function/comptime").comptime;
resolve: typeof import("node_modules/typegpu/core/resolve/tgpuResolve").resolve;
resolveWithContext: typeof import("node_modules/typegpu/core/resolve/tgpuResolve").resolveWithContext;
init: typeof import("node_modules/typegpu/core/root/init").init;
initFromDevice: typeof import("node_modules/typegpu/core/root/init").initFromDevice;
slot: typeof import("node_modules/typegpu/core/slot/slot").slot;
lazy: typeof import("node_modules/typegpu/core/slot/lazy").lazy;
... 10 more ...;
'~unstable': typeof import("node_modules/typegpu/tgpuUnstable");
}

@module ― typegpu

tgpu
.
computeFn: (options: {
workgroupSize: number[];
}) => TgpuComputeFnShell<{}> (+1 overload)
computeFn
({
workgroupSize: number[]
workgroupSize
: [1] })(() => {
'use gpu';
const
const wisp: number
wisp
=
const hits: picker.Picker
hits
.
Picker.found: () => number

Returns the candidate index from the last pick, or none, in a shader created from root.pipe(hits.inject()). Available in later passes of the same frame.

found
();
if (
const wisp: number
wisp
===
import picker
picker
.
const none: 4294967295

Marks a pick that found no candidate, as returned by found.

none
) return;
const marked: TgpuMutable<d.WgslArray<d.U32>>
marked
.
TgpuMutable<WgslArray<U32>>.$: number[]
$
[
const focus: TgpuMutable<d.U32>
focus
.
TgpuMutable<U32>.$: number
$
] = 0;
const marked: TgpuMutable<d.WgslArray<d.U32>>
marked
.
TgpuMutable<WgslArray<U32>>.$: number[]
$
[
const wisp: number
wisp
] = 1;
const focus: TgpuMutable<d.U32>
focus
.
TgpuMutable<U32>.$: number
$
=
const wisp: number
wisp
;
});
const
const marking: TgpuComputePipeline
marking
=
const root: TgpuRoot
root
.
WithBinding.pipe(transform: (cfg: Configurable) => Configurable): WithBinding
pipe
(
const hits: picker.Picker
hits
.
Picker.inject(): Inject

Fills found for the pipelines created from what it is piped into.

inject
()).
WithBinding.createComputePipeline<{}>(descriptor: TgpuComputePipeline.Descriptor<{}>): TgpuComputePipeline
createComputePipeline
({
compute: TgpuComputeFn<{}>
compute
:
const mark: TgpuComputeFn<{}>
mark
});

For hovering, pick every frame and set hz to control readback. Only captures due at that rate copy the result. The star map reads at 20 Hz. result.latest holds the newest completed read, a frame or two behind. result.latestFrame identifies the frame that picked it. take() returns each completed read once.

When the pointer leaves, call hits.clear() and stop picking. found() returns none from then on, and the next capture reads null, without a dispatch.

For a few candidates already on the CPU, use the camera’s projection to compare their screen positions without a kernel.

  • atomicMin selects the lowest packed key. The index uses the bits needed for capacity. Rank uses the remaining bits, capped at 24. It gets 23 bits at 512 candidates and 8 bits at 2^24. Ranks within one quantisation step can tie.
  • A centre and a radius only approximate a tall or long candidate. A kernel of your own, such as a ray through boxes, can read found() beside its own result.
  • A second pointer needs a picker of its own.