Metadata-Version: 2.4
Name: vcti-shader-pick
Version: 1.0.0
Summary: The pick shader feature: the fragment-stage pass that writes identifiers for the geometry at each pixel.
Author: Visual Collaboration Technologies Inc.
License-Expression: LicenseRef-Proprietary
Project-URL: Repository, https://github.com/vcollab/vcti-python-shader-pick
Project-URL: Changelog, https://github.com/vcollab/vcti-python-shader-pick/blob/main/CHANGELOG.md
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Python: <3.15,>=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: vcti-shader-base>=2.1.0
Provides-Extra: gl
Requires-Dist: vcti-shader-compiler[gl]>=4.0.0; extra == "gl"
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Requires-Dist: pytest-cov; extra == "test"
Requires-Dist: vcti-shader-compiler>=4.0.0; extra == "test"
Requires-Dist: numpy; extra == "test"
Provides-Extra: lint
Requires-Dist: ruff; extra == "lint"
Provides-Extra: typecheck
Requires-Dist: mypy; extra == "typecheck"
Dynamic: license-file

# vcti-shader-pick

The pick shader feature: the fragment-stage pass that writes identifiers for the geometry at each pixel.

## Overview

A viewer has to answer "what did the user just click on", and the only thing
that knows which surface covers which pixel is the rasterizer. So the answer is
produced the same way the picture is: by drawing. A **pick pass** renders the
scene to an integer target and writes, at every pixel, identifiers for the
geometry that landed there — plus how far away it was.

Reading one texel back then answers the click. What those identifiers *mean* is
not this package's business: one names the atom, and the other is opaque,
resolved by whoever owns the source model. That is what lets the same pass serve
an element, a face, a triangle, or something not yet invented, without changing
a line of shader source.

`vcti-shader-pick` ships the Slang that packs a texel, a Python mirror of the
same layout for decoding a readback, and the `ShaderDefinition` saying what the
feature is. Everything here is a declaration or fixed shader source; nothing
compiles, runs, or reads back a shader.

## Installation

```bash
pip install vcti-shader-pick
```

Requires Python 3.12, 3.13, or 3.14, matching `vcti-shader-base`. Nothing native
is built on that path, and nothing native is built by `[test]` either — only the
`[gl]` extra pulls a GL binding, and only on 3.14 does that compile from source
for want of a cp314 wheel.

### In `requirements.txt`

```
vcti-shader-pick>=1.0.0
```

### In `pyproject.toml` dependencies

```toml
dependencies = [
    "vcti-shader-pick>=1.0.0",
]
```

## Quick Start

### What a build step binds

The feature declares two per-vertex identifiers and one integer target. Both
identifiers must reach the fragment stage as `flat` varyings — GLSL ES requires
that of every integer varying, and it is what keeps them exact:

```python
from vcti.shader.pick import fragment_inputs, fragment_outputs, fragment_uniforms

inputs = fragment_inputs()
assert [attribute.name for attribute in inputs] == ["a_atomId", "a_pickIndex"]
assert inputs[0].semantic == "atom-id"

assert fragment_outputs()[0].name == "pickTarget"
assert fragment_uniforms() == ()
```

`a_atomId` is declared identically by another atom-aware feature, so the two
merge into one attribute when they are composed. `a_pickIndex` is this feature's
own, and is opaque to everything in the shader.

### Reading a click back

The target is four unsigned 32-bit channels. Clear it to `PICK_EMPTY` before the
pass, and draw the pass depth-tested, unblended and single-sampled — each of
those is the caller's to get right, and each fails silently, which
[design.md](docs/design.md) explains. Then decode the texel under the cursor:

```python
from vcti.shader.pick import PICK_EMPTY, decode, float_to_bits

nothing = decode((PICK_EMPTY, PICK_EMPTY, PICK_EMPTY, PICK_EMPTY))
assert nothing is None

hit = decode((7, 4_210_001, float_to_bits(12.5), 0))
assert hit is not None
assert hit.atom_id == 7
assert hit.pick_index == 4_210_001
assert hit.depth == 12.5
```

The depth is measured along the camera's forward axis — `-z` in view space,
positive in front of the camera — not from the eye to the point. Give it the
pixel's ray, and `reconstruct_view_position` returns the point:

```python
from vcti.shader.pick import reconstruct_view_position

# A perspective ray: the origin is the eye, at the view-space origin.
point = reconstruct_view_position(12.5, (0.0, 0.0, 0.0), (0.3, 0.2, -1.0))
assert point == (3.75, 2.5, -12.5)

# An orthographic ray: parallel, each starting at its own point.
point = reconstruct_view_position(12.5, (2.0, 1.0, -0.1), (0.0, 0.0, -1.0))
assert point == (2.0, 1.0, -12.5)
```

The ray is yours to build — inverting the projection and knowing which way the
viewport's Y runs are the caller's, because the caller owns the camera. What the
depth *means* is this package's, which is why the inverse ships here rather than
being a formula every caller re-derives. `direction` need not be normalized, and
the origin need not sit on the camera plane: a ray unprojected to the near plane
works as given.

A Euclidean distance from the eye would have cost the same channel and been
wrong for any ray not starting at a single shared point.

Note that `depth` is *reinterpreted* from its bits rather than converted:

```python
from vcti.shader.pick import bits_to_float

assert bits_to_float(float_to_bits(12.5)) == 12.5
assert float_to_bits(1.0) > 1_000_000_000        # the same bits read as an int
```

### Selecting a pipeline

```python
from vcti.shader.pick import DEFINITION

assert DEFINITION.id == "pick"
assert DEFINITION.role.value == "fragment"
assert DEFINITION.capabilities == ("pick",)
```

## The texel

| Channel | Holds |
|---|---|
| `R` | `a_atomId` |
| `G` | `a_pickIndex` |
| `B` | the view depth, as reinterpreted `float32` bits |
| `A` | reserved, written as zero |

An integer target rather than a float one because identifiers must survive
exactly: a `float32` carries consecutive integers only to 2²⁴, above which one
arrives as its neighbour and resolves to different geometry — a failure that
looks like a mis-click rather than like corruption.

## API surface

| Name | What it is |
|---|---|
| `DEFINITION` | the `ShaderDefinition` a build step imports to compose this feature |
| `SLANG_DIR` | the installed Slang directory |
| `fragment_inputs`, `fragment_outputs`, `fragment_uniforms` | what a build step binds |
| `decode`, `is_empty`, `Pick` | read a texel of a pick readback |
| `bits_to_float`, `float_to_bits` | the bit reinterpretation, on its own |
| `reconstruct_view_position` | a ray and a depth to the view-space point |
| `PICK_EMPTY`, `PICK_RESERVED`, `CHANNELS` | the layout, as data |
| `TARGET_NAME`, `TARGET_TYPE`, `TARGET_FORMAT` | the output's name, GLSL type, and the attachment it needs |

## Dependencies

`vcti-shader-base` is the only runtime dependency — declaring a feature is pure
data. `vcti-shader-compiler>=4.0.0` and `numpy` are test-only, and a separate
`gl` extra adds the GL binding the shader tests need to execute rather than
skip.

## Documentation

| If you want to… | Read |
|---|---|
| Get started using the package | Quick Start above |
| Understand the texel contract and the decisions behind it | [docs/design.md](docs/design.md) |
| Navigate or modify the source, including the Slang | [docs/source-guide.md](docs/source-guide.md) |

The full API reference is generated from the source docstrings and published in
the unified VCollab docs.
