# `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.