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.
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.
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(
constroot:TgpuRoot
root,
constview: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.
The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.
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.
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).
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.
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).
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(
constroot:TgpuRoot
root,(
frame:frames.Frame
frame)=>{
const
constcompute: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.
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(
constcompute:TgpuComputePass
compute,...
let tap:[number,number]
tap);
constcompute:TgpuComputePass
compute.
TgpuComputePass.end(): void
Completes the recording of this compute pass
end();
consthits: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.
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
constfound:number|null|undefined
found=
consthits: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(
constfound:number|null|undefined
found!==
var undefined
undefined)
functionselect(wisp:number|null):void
select(
constfound: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
constbyEye:TgpuUniform<d.U32>
byEye=
constroot: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.
constu32: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.
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(
constroot:TgpuRoot
root,
constview: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.
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.
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(
constroot:TgpuRoot
root,
constview: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:
constWISPS: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<
constWISPS:480
WISPS)return
constwisps:TgpuReadonly<d.WgslArray<d.Vec4f>>
wisps.
TgpuReadonly<WgslArray<Vec4f>>.$: d.v4f[]
$[
i: number
i]!.
xyz:d.v3f
xyz;
return
import d
d.
constvec3f:d.Vec3f
(v:AnyNumericVec3Instance)=>d.v3f(+5 overloads)
vec3f(
constguns:TgpuReadonly<d.WgslArray<d.Vec3f>>
guns.
TgpuReadonly<WgslArray<Vec3f>>.$: d.v3f[]
$[
i: number
i-
constWISPS:480
WISPS]!);
},
Options.radius?:((i:number)=> number)|undefined
Returns the candidate radius in world units. Default 0, a point.
Tests whether a candidate can be picked. Default every candidate can be picked.
alive:(
i: number
i)=>{
'use gpu';
return
i: number
i>=
constWISPS:480
WISPS||
constwisps: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:
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(
constwisp:number
wisp===
import picker
picker.
constnone:4294967295
Marks a pick that found no candidate, as returned by found.
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.
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.