Mesh picking material TSL factory — NodeMaterial counterpart to
MESH_PICK_SOURCE in shaders.ts.
Fragment output (the shared RGBA32F pick encoding):
R: nodeId (uniform)
G: elementId LOW 16 bits (= the VERTEX ordinal — mesh has no ordering
attribute to indirect through, §6.5)
B: brightness = the coverage vAlpha * uOpacity, or 1.0 for a cutout
survivor (which the visual shader draws fully opaque), times the shared
perspective near fade — so pick salience tracks visible salience right up
to the near plane, and a fragment below the 0.01 reject is discarded
A: the same elementId's HIGH 16 bits (one f32 channel cannot carry the
whole index exactly — see luxarElementIdSplit)
Depth = 1 - brightness (brightest wins), or the real fragment depth when
uSurfaceDepth == 1 (the depth-ordered opaque/normal surface modes:
front-most wins). Both selectors are runtime uniforms, so a layers-panel
blending-mode switch never rebuilds this graph.
The flat-interpolation requirement is not optional here
vElementId and vNodeId are declared .setInterpolation('flat'), following
the LINE pick precedent rather than the point pick. The point pick omits flat
and gets away with it only because a single-instance quad's four corners all
carry the same id, making interpolation the identity. A mesh is an indexed,
shared-vertex draw where vertexIndex differs at every corner: interpolated, the
id would arrive FRACTIONAL and the readback's Math.round would resolve to
arbitrary unrelated vertices.
WGSL's @interpolate(flat) samples the FIRST vertex of the primitive where
OpenGL ES fixes it to the LAST, so the exact corner reported differs by backend
unless WEBGL_provoking_vertex is available to align them (see ./material.ts).
That is a documented §6.5 exception: at vertex granularity the contract is
"a corner of the front-most triangle", since the cursor is over the face.
Mesh picking material TSL factory — NodeMaterial counterpart to
MESH_PICK_SOURCEinshaders.ts.Fragment output (the shared RGBA32F pick encoding):
vAlpha * uOpacity, or 1.0 for a cutout survivor (which the visual shader draws fully opaque), times the shared perspective near fade — so pick salience tracks visible salience right up to the near plane, and a fragment below the 0.01 reject is discardedluxarElementIdSplit)Depth =
1 - brightness(brightest wins), or the real fragment depth whenuSurfaceDepth == 1(the depth-orderedopaque/normalsurface modes: front-most wins). Both selectors are runtime uniforms, so a layers-panel blending-mode switch never rebuilds this graph.The flat-interpolation requirement is not optional here
vElementIdandvNodeIdare declared.setInterpolation('flat'), following the LINE pick precedent rather than the point pick. The point pick omitsflatand gets away with it only because a single-instance quad's four corners all carry the same id, making interpolation the identity. A mesh is an indexed, shared-vertex draw wherevertexIndexdiffers at every corner: interpolated, the id would arrive FRACTIONAL and the readback'sMath.roundwould resolve to arbitrary unrelated vertices.WGSL's
@interpolate(flat)samples the FIRST vertex of the primitive where OpenGL ES fixes it to the LAST, so the exact corner reported differs by backend unlessWEBGL_provoking_vertexis available to align them (see./material.ts). That is a documented §6.5 exception: at vertex granularity the contract is "a corner of the front-most triangle", since the cursor is over the face.