# `tecs.gpu.materials`
Material identity, numbering, and the component that selects one.
A material decides a fragment's color, coverage, surface normal, and lighting
response. It is authored as a WGSL file under a material root, and every
material found is folded into one generated fragment shader that dispatches on
the id an instance carries. This module owns the names and the numbering; the
backend owns the compiled dispatch.
```nupp
world:spawn(
tecs.ecs.Transform2D(120, 80, 1, 0, 0, 64, 64),
tecs.gfx.Material(tecs.gpu.materials.id("rounded"), 0.25),
tecs.gfx.Tint(0.2, 0.6, 1.0, 1.0),
tecs.gfx.Renderable2D
)
```
An entity carrying no `Material` draws through `textured`, which holds id zero
for exactly that reason. Every other name takes an id from sorted order, so
adding or removing a material renumbers the ones after it. Persisted state
therefore stores the name and resolves it again, which is what the component's
own snapshot does.
`Material.param` supplies one scalar from zero to one. The built-ins read it as
follows:
- `ellipse` uses the height fraction.
- `ring` uses the inner-radius fraction.
- `rounded` uses the corner-radius fraction.
- `frame` and `line` use a thickness fraction.
- `capsule` uses a height fraction.
- `pie` uses a full-turn sweep fraction.
- `star` uses valley depth.
- `emissive` uses how much of its own color the surface gives off.
- `glyph` uses the distance-field range.
- `textured`, `circle`, `glyphalpha`, and `triangle` ignore it.
# Authoring a material
A `.wgsl` file under a material root defines one `material` function:
```wgsl
fn material(frag: MaterialInput) -> MaterialOutput {
var result = materialDefaults();
let radius = mix(0.02, 0.20, frag.param);
result.albedo = textureSample(image, imageSampler, frag.uv) * frag.color;
result.coverage = -sdRoundedBox(frag.local, vec2(0.5), radius);
result.lit = 1.0;
return result;
}
```
`frag.local` runs from -0.5 to 0.5 inside the quad. `frag.uv`, `frag.color`,
and `frag.param` carry the image coordinates, tint, and instance parameter.
`frag.blended` says whether the fragment reaches a pass that blends it. Start
from `materialDefaults`, then set `albedo`, `normal`, `orm`, `lit`, `emission`,
and `coverage`.
Coverage above zero keeps a fragment; zero or below discards it. The deferred
lane does not blend partial coverage: a tint alpha below one routes the
instance to the blended lane instead. A material that resolves an edge by
discarding puts the edge in alpha where `frag.blended` is set, which is what
`textured` does.
# Adding materials to a build
`addRoot` names a directory this module globs for `*.wgsl`. The same directory
is read by the build step that produces the shader pack, so the ids this module
assigns and the ids the compiled dispatch answers to come from one rule applied
to one set of files. A root added after the pack was built renumbers the set
and the backend rejects an id its dispatch does not carry, rather than drawing
the wrong material.
Runtime assembly and reload are deliberately absent. A release links no shader
assembler and loads a prebuilt pack, so a changed material body is a rebuild.
## Types
### `Material` _struct_
```nupp
struct Material
id: integer
param: number
end
```
`@derive(nupp.derive.Debug, nupp.derive.Serde)`
Selects one material for a renderable entity.
#### Fields
##### `id`
```nupp
id: integer
```
Caller-writable. Selects a material by an id `id` returned. Zero draws
through `textured`.
##### `param`
```nupp
param: number
```
Caller-writable. Passes a value from zero to one to the material. What
it means is the material's business.
## Functions
### `addRoot` _function_
```nupp
function addRoot(path: string): nil
```
Adds a directory to search before the ones already known.
The directory is read by `install` rather than here, so adding a root costs
nothing until the next name is resolved. A game's own material of a given
name does not replace an engine one: two files with the same stem are one
material, and the build that produces the dispatch decides which body wins.
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `path` | `string` | a directory holding `*.wgsl` material files, with or without a trailing separator |
#### Returns
| Type | Description |
| --- | --- |
| `nil` | |
#### Raises
- when the path is empty
### `count` _function_
```nupp
function count(): integer
```
Returns how many materials the build numbers.
The backend rejects an instance whose material id reaches this, so a packet
built against a larger set than the pack holds fails rather than drawing
the wrong material.
#### Returns
| Type | Description |
| --- | --- |
| `integer` | the count, so the valid ids run from zero to one below it |
### `define` _function_
```nupp
function define(name: string): nil
```
Registers a material name that no root supplies.
A generated material a build writes into the pack without writing a file is
what this is for. It renumbers the set on the next `install`, exactly as
adding a file does.
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `name` | `string` | the material's name, matching the name the pack's dispatch uses |
#### Returns
| Type | Description |
| --- | --- |
| `nil` | |
#### Raises
- when the name is empty
### `find` _function_
```nupp
function find(name: string): integer?
```
Returns the id an instance carries to select a material, or nil.
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `name` | `string` | the material's file name without its extension |
#### Returns
| Type | Description |
| --- | --- |
| `integer?` | the id, or nil when nothing carries that name |
### `id` _function_
```nupp
function id(name: string): integer
```
Returns the id an instance carries to select a material.
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `name` | `string` | the material's file name without its extension |
#### Returns
| Type | Description |
| --- | --- |
| `integer` | the id, counting from zero |
#### Raises
- when nothing carries the name, listing what the build has, because a misspelled material is a build mistake rather than a runtime state
### `install` _function_
```nupp
function install(): nil
```
Reads every root and assigns ids. Idempotent.
Resolving a name installs first, so a game rarely calls this. Adding a root
puts it back on.
#### Returns
| Type | Description |
| --- | --- |
| `nil` | |
#### Raises
- when a root cannot be listed
### `name` _function_
```nupp
function name(value: integer): string?
```
Returns the material name an id selects.
This is what a snapshot writes in place of the number, since an id holds
only for the set of files the build was made from.
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `value` | `integer` | an id as an instance carries it |
#### Returns
| Type | Description |
| --- | --- |
| `string?` | the name, or nil when nothing carries that id |
### `names` _function_
```nupp
function names(): {string}
```
Returns every material name in id order.
#### Returns
| Type | Description |
| --- | --- |
| `{string}` | a fresh list the caller owns, indexed from one while the ids it describes count from zero |
### `reset` _function_
```nupp
function reset(): nil
```
Forgets every root and definition, so a test starts from the built-ins.
#### Returns
| Type | Description |
| --- | --- |
| `nil` | |
### `revision` _function_
```nupp
function revision(): integer
```
Returns the installed material numbering revision.
#### Returns
| Type | Description |
| --- | --- |
| `integer` | A monotonic count that changes whenever the registry is rebuilt. |
## Values
### `DEFAULT` _variable_
```nupp
const DEFAULT: string
```
Takes id zero, because an instance with no `Material` writes a zero.
This name is a compatibility surface: it is what a snapshot with no
material resolves to and what the generated dispatch numbers from.
### `MaterialComponent` _variable_
```nupp
const MaterialComponent: components.FFIComponent
```
The process-wide `Material` component definition.