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.

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:

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.

Module contents

Types

TypeKindDescription
MaterialstructSelects one material for a renderable entity.

Functions

FunctionKindDescription
addRootfunctionAdds a directory to search before the ones already known.
countfunctionReturns how many materials the build numbers.
definefunctionRegisters a material name that no root supplies.
findfunctionReturns the id an instance carries to select a material, or nil.
idfunctionReturns the id an instance carries to select a material.
installfunctionReads every root and assigns ids.
namefunctionReturns the material name an id selects.
namesfunctionReturns every material name in id order.
resetfunctionForgets every root and definition, so a test starts from the built-ins.
revisionfunctionReturns the installed material numbering revision.

Values

ValueKindDescription
DEFAULTvariableTakes id zero, because an instance with no Material writes a zero.
MaterialComponentvariableThe process-wide Material component definition.

Types#

Materialstruct#

struct Material
    id: integer
    param: number
end
@derive(nupp.derive.Debug, nupp.derive.Serde)

Selects one material for a renderable entity.

Fields

id#
id: integer

Caller-writable. Selects a material by an id id returned. Zero draws through textured.

param#
param: number

Caller-writable. Passes a value from zero to one to the material. What it means is the material's business.

Functions#

addRootfunction#

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

NameTypeDescription
pathstring

a directory holding *.wgsl material files, with or without a trailing separator

Returns

TypeDescription
nil

Raises

  • when the path is empty

countfunction#

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

TypeDescription
integer

the count, so the valid ids run from zero to one below it

definefunction#

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

NameTypeDescription
namestring

the material's name, matching the name the pack's dispatch uses

Returns

TypeDescription
nil

Raises

  • when the name is empty

findfunction#

function find(name: string): integer?

Returns the id an instance carries to select a material, or nil.

Arguments

NameTypeDescription
namestring

the material's file name without its extension

Returns

TypeDescription
integer?

the id, or nil when nothing carries that name

idfunction#

function id(name: string): integer

Returns the id an instance carries to select a material.

Arguments

NameTypeDescription
namestring

the material's file name without its extension

Returns

TypeDescription
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

installfunction#

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

TypeDescription
nil

Raises

  • when a root cannot be listed

namefunction#

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

NameTypeDescription
valueinteger

an id as an instance carries it

Returns

TypeDescription
string?

the name, or nil when nothing carries that id

namesfunction#

function names(): {string}

Returns every material name in id order.

Returns

TypeDescription
{string}

a fresh list the caller owns, indexed from one while the ids it describes count from zero

resetfunction#

function reset(): nil

Forgets every root and definition, so a test starts from the built-ins.

Returns

TypeDescription
nil

revisionfunction#

function revision(): integer

Returns the installed material numbering revision.

Returns

TypeDescription
integer

A monotonic count that changes whenever the registry is rebuilt.

Values#

DEFAULTvariable#

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.

MaterialComponentvariable#

const MaterialComponent: components.FFIComponent<Material>

The process-wide Material component definition.