Metadata-Version: 2.4
Name: vcti-shader-material
Version: 1.0.0
Summary: The material shader feature: the fragment-stage lighting that shades a surface by a metallic-roughness material.
Author: Visual Collaboration Technologies Inc.
License-Expression: LicenseRef-Proprietary
Project-URL: Repository, https://github.com/vcollab/vcti-python-shader-material
Project-URL: Changelog, https://github.com/vcollab/vcti-python-shader-material/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.2.0
Provides-Extra: gl
Requires-Dist: vcti-shader-compiler[gl]>=4.2.0; extra == "gl"
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Requires-Dist: pytest-cov; extra == "test"
Requires-Dist: vcti-shader-compiler>=4.2.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-material

The material shader feature: the fragment-stage lighting that shades a surface by a metallic-roughness material.

## Overview

A viewer wants a mesh to look like the object it represents: a painted casting,
a brushed steel bracket, a rubber seal. This feature is the lighting that does
that. It shades each fragment by a **metallic-roughness material** — a base colour,
a metallic factor, a roughness, an occlusion factor and an emissive — a subset
of glTF 2.0's metallic-roughness material — under a sky/ground environment and up to four
directional or point lights, with the Cook-Torrance specular model: the GGX
distribution, the Smith geometry term and the Schlick Fresnel approximation.

The lighting takes those values as one resolved `MaterialValues` and does not
care where they came from. The source shipped today is **uniforms**: one material for the whole
draw. A texture, a materials table indexed by a per-vertex id, or the fringe
colour of a result plot are other sources, each a capability of its own, and
none of them changes the BRDF. Tone mapping and gamma are a separate finishing
step the composition applies to a lit colour and never to a fringe colour, which
is a legend colour and must reach the screen exactly.

`vcti-shader-material` is the shader half of that arrangement. It ships the
Slang that computes the lighting, a Python mirror of every function in it so a
caller can predict a shaded colour and the tests have an oracle, the specs
saying what the lighting needs bound, and the `ShaderDefinition` saying what the
feature is.

Everything here is a declaration or fixed shader source. Nothing compiles or
runs a shader; a build step does that, using what this package declares.

## Installation

```bash
pip install vcti-shader-material
```

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-material>=1.0.0
```

### In `pyproject.toml` dependencies

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

## Quick Start

### Describe a material and some lights

A material is a handful of values. The base colour is a linear colour in
`[0, 1]` — a reflectance, so it has a ceiling; the emissive is a linear radiance
and has none; the three factors are in `[0, 1]`. A light is directional, with the unit direction from the surface
toward it, or a point, with a position and inverse-square falloff:

```python
from vcti.shader.material import Light, MaterialValues

steel = MaterialValues(base_color=(0.56, 0.57, 0.58), metallic=1.0, roughness=0.35)
paint = MaterialValues(base_color=(0.8, 0.1, 0.1), metallic=0.0, roughness=0.5)

key = Light.directional((0.3, 0.4, 1.0), (1.0, 1.0, 1.0))
lamp = Light.point((1.0, 1.0, 3.0), (4.0, 4.0, 4.0))
assert key.is_directional and not lamp.is_directional
```

### Predict what the shader produces

The same math the shader runs, in Python. `shade` returns linear light; `finish`
is the tone mapping and gamma step:

```python
from vcti.shader.material import EnvironmentLight, finish, shade

surface = (0.0, 0.0, 0.0)
normal = (0.0, 0.0, 1.0)
eye = (0.0, 0.0, 5.0)

# The environment is a sky/ground gradient. `uniform` is the flat case a single
# ambient colour used to be; `studio` is a bright ceiling over a darker floor.
environment = EnvironmentLight.studio()

lit = shade(surface, normal, eye, paint, environment, [key, lamp])
assert all(channel > 0.0 for channel in lit)

shown = finish(lit)
assert all(0.0 <= channel < 1.0 for channel in shown)
assert shade(surface, normal, eye, steel, environment, [key]) != shade(
    surface, normal, eye, paint, environment, [key]
)

# A metal is almost entirely a reflection of its surroundings, so which way it
# faces changes what it shows. A flat ambient could not express that.
up = shade(surface, (0.0, 0.0, 1.0), eye, steel, environment, [])
down = shade(surface, (0.0, 0.0, -1.0), (0.0, 0.0, -5.0), steel, environment, [])
assert up[0] > down[0]
```

A light behind the surface contributes nothing, a metal has no diffuse term,
and a roughness below the floor shades as the floor. The material refuses a
factor outside `[0, 1]`:

```python
try:
    MaterialValues(roughness=1.5)
except ValueError as error:
    assert "roughness" in str(error)
```

### Set the uniforms

The material and the lights are uniforms, one set per draw. The helpers write
them by the names the specs declare, with the light arrays padded to the size the
shader is built for:

```python
from vcti.shader.material import MAX_LIGHTS, light_uniforms, material_uniforms

uniforms = material_uniforms(paint) | light_uniforms([key, lamp])
uniforms |= {
    "u_viewPosition": eye,
    "u_environmentSky": environment.sky_color,
    "u_environmentGround": environment.ground_color,
    "u_environmentUp": environment.up,
}

assert uniforms["u_materialBaseColor"] == (0.8, 0.1, 0.1)
assert uniforms["u_lightCount"] == 2
assert len(uniforms["u_lightPositions"]) == MAX_LIGHTS == 4
assert uniforms["u_lightPositions"][1] == (1.0, 1.0, 3.0, 1.0)   # w = 1: a point light
```

### What a build step binds

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

(normal_attribute,) = fragment_inputs()
assert (normal_attribute.name, normal_attribute.type, normal_attribute.semantic) == (
    "a_normal", "vec3", "normal"
)
assert [u.name for u in fragment_uniforms()] == [
    "u_materialBaseColor", "u_materialMetallic", "u_materialRoughness",
    "u_materialOcclusion", "u_materialEmissive",
    "u_viewPosition", "u_environmentSky", "u_environmentGround", "u_environmentUp",
    "u_lightPositions", "u_lightColors", "u_lightCount",
]
assert fragment_outputs()[0].name == "fragColor"
```

The vertex stage rotates the normal into world space, through whatever
placement it applies to the position, and forwards it with the world position
as two varyings. Those varyings are contract rather than spec; see
[docs/design.md](docs/design.md).

### Select a pipeline

One tag for the lighting and one naming where the values come from:

```python
from vcti.shader.material import (
    DEFINITION,
    DEFINITIONS,
    TABLE_DEFINITION,
    TEXTURE_DEFINITION,
)

assert DEFINITION.id == "material"
assert DEFINITION.role.value == "fragment"
assert set(DEFINITION.capabilities) == {"material", "material-uniform"}
assert DEFINITION.slang_modules == (
    "material.slang",
    "material_table.slang",
    "material_texture.slang",
    "material_surface_finish.slang",
)

# One definition per source. Same lighting tag, different source tag, so a
# pipeline asking for one never matches another.
assert set(TABLE_DEFINITION.capabilities) == {"material", "material-table"}
assert set(TEXTURE_DEFINITION.capabilities) == {"material", "material-texture"}
assert DEFINITIONS == (DEFINITION, TABLE_DEFINITION, TEXTURE_DEFINITION)
```

### Vary a surface without authoring anything

A **layer** takes a resolved material and returns another, so layers compose
where sources cannot: this is how a painted casting can also carry a label. The
surface finish is the first, and it needs no UVs, no tangents and no images —
just world position:

```python
from vcti.shader.material import SurfaceFinish, apply_surface_finish

cast_iron = MaterialValues(base_color=(0.35, 0.34, 0.33), roughness=0.7)
finish = SurfaceFinish.cast()

here = apply_surface_finish(cast_iron, (0.0, 0.0, 0.0), finish)
there = apply_surface_finish(cast_iron, (0.12, 0.04, -0.08), finish)
assert here.roughness != there.roughness      # the surface varies from point to point

# The default finish is the identity, which is what "no finish" means.
assert apply_surface_finish(cast_iron, (1.0, 2.0, 3.0), SurfaceFinish()) == cast_iron
```

*The* finish (`finish`, `tone_map`, `gamma_encode`) is tone mapping and gamma on
the way to the screen. A *surface* finish is what a surface looks like. They are
unrelated.

### Many materials in one draw

The table source replaces the five material uniforms with an id per vertex and
a table of rows. A client uploads `table_texel_rows(...)` as a three-row
`rgba32f` texture and binds the ids; the lighting is unchanged:

```python
from vcti.shader.material import MaterialValues, pack_row, row_material, table_texel_rows

materials = [steel, paint, MaterialValues(base_color=(0.1, 0.1, 0.1), emissive=(0.0, 0.9, 0.2))]
rows = table_texel_rows(materials)

assert len(rows) == 3                      # three texels per material
assert len(rows[0]) == len(materials) * 4  # one column per material
assert row_material(pack_row(steel)) == steel
```

## The lighting

| Term | Model |
|---|---|
| diffuse | Lambert, `base_color / π`, scaled by `(1 − F)(1 − metallic)` |
| distribution | GGX / Trowbridge-Reitz on `roughness²` |
| geometry | Smith, Schlick-GGX form, `k = (roughness + 1)² / 8` |
| Fresnel | Schlick, `F0 = mix(0.04, base_color, metallic)` |
| environment | a sky/ground gradient: diffuse along `N`, specular along `reflect(-V, N)`, weighted by roughness-aware Fresnel |
| tone map | Reinhard `c / (c + 1)` |
| gamma | `c ^ (1 / 2.2)` — skipped on an sRGB framebuffer |
| finish | the two composed |

Roughness is floored at 0.04. Lights are a fixed array of four; `w` in a
light's position selects directional (0) or point (1). Everything is in linear
light until the finish.

## API surface

| Name | What it is |
|---|---|
| `DEFINITION` | the `ShaderDefinition` a build step imports to compose this feature |
| `SLANG_DIR`, `SLANG_MODULES` | the installed Slang directory, and the module in it |
| `MaterialValues`, `Light`, `EnvironmentLight` | the resolved material a source produces, a directional or point light, and the sky/ground environment |
| `shade`, `light_contribution`, `ambient`, `view_direction` | the mirror of the shader's lighting |
| `tone_map`, `gamma_encode`, `finish` | the output transform, in two independently callable steps |
| `fresnel_schlick`, `distribution_ggx`, `geometry_smith`, `geometry_schlick_ggx`, `f0` | the BRDF's terms |
| `material_uniforms`, `light_uniforms` | the uniforms a caller sets, by the declared names |
| `MAX_LIGHTS`, `MIN_ROUGHNESS`, `DIELECTRIC_F0` | the constants the Slang shares |
| `fragment_inputs`, `fragment_uniforms`, `fragment_outputs` | what a build step binds |
| `CAPABILITY`, `SOURCE_CAPABILITY` | the tags |

## Dependencies

`vcti-shader-base>=2.1.0` 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 lighting, the contract and the decisions behind them | [docs/design.md](docs/design.md) |
| Know what a material is, what an authored one loses on the way in, and what is planned | [docs/material-model.md](docs/material-model.md) |
| Understand how authored textures and composed shaders meet, and what was decided | `vcti-shader-base`, `docs/authored-textures.md` — the decisions there settle the texture and sampler declarations, which ship from that package |
| 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.
