# Shader inputs

> Everything the engine hands a shader, each input doing something you can see: a grid from mu.resolution, a stripe run by mu.time, and ten models each drawing one field of FragContext.

Example `shader-inputs`, running at https://nacatamalon.com/en/examples/shader-inputs. The whole catalogue: https://nacatamalon.com/llms/examples.md.

A material is a function the engine runs for every pixel, and what it can read is decided by the engine. Here each of those inputs does something you can see. The backdrop is one sprite as big as the screen: its `uv` times `mu.resolution` is the pixel it covers, so it draws a line every 40 pixels. The stripe under the title runs on `mu.time`, which is game time: `P` pauses the game and the stripe stops with it, and `S` slows it to a quarter.

Each of the ten models draws one field of a model's `FragContext` as a colour. A direction (`normal`, `tangent`) becomes a colour by taking it from -1..1 to 0..1. `worldPos` is drawn as bands of the world, so they stay put while the model turns through them, and `screen` as a checker laid on the screen rather than on the model. `viewDir` shows as a rim where the surface turns away from the camera. `frontFacing` needs a shape you can see from behind, so it is a sheet drawn with `cull: 'none'`, green in front and orange behind. `carry` is written by the model's own vertex hook: each corner is lifted by a wave and hands on how high, and the colour turns to foam on the crests.

Every shader is built with the shader composer, so the same code runs on WebGPU and WebGL2. The full list of names, with their types and the helpers for sprites, models and screen effects, is on the [What the engine hands a shader](/docs/shader-inputs) page.

## Code

`index.ts`:

```ts
import { createGame } from 'nacatamalon';
import { WIDTH, HEIGHT } from './settings';
import { Level } from './scenes/demo';

const game = createGame('#app', {
    width: WIDTH,
    height: HEIGHT,
    scaling: 'contain',
    background: '#0b0d16'
});

// Declare Scenes
game({
    Level
});
```

`components/backdrop.ts`:

```ts
import { createMaterial, createSprite, createText } from 'nacatamalon';
import { BACKDROP, STRIPE } from '../shaders/inputs';
import * as Settings from '../settings';

export const Backdrop = () => {
    createSprite({
        width: Settings.WIDTH,
        height: Settings.HEIGHT,
        zIndex: -10,
        transform: {
            x: Settings.WIDTH / 2,
            y: Settings.HEIGHT / 2
        },
        material: createMaterial(BACKDROP)
    });
    createText({
        text: 'BACKDROP: mu.resolution, A LINE EVERY 40 PIXELS',
        tint: Settings.DIM,
        transform: {
            x: 12,
            y: 10
        }
    });

    createSprite({
        width: 240,
        height: 14,
        transform: {
            x: Settings.WIDTH / 2,
            y: 52
        },
        material: createMaterial(STRIPE)
    });
    createText({
        text: 'mu.time',
        anchor: {
            x: 0.5,
            y: 0
        },
        tint: Settings.DIM,
        transform: {
            x: Settings.WIDTH / 2,
            y: 64
        }
    });
};
```

`components/panelgrid.ts`:

```ts
import { createMaterial, createMesh, createText, useIcoSphereGeometry, usePlaneGeometry, useSphereGeometry } from 'nacatamalon';
import { PANELS } from '../shaders/inputs';
import * as Settings from '../settings';

export const PanelGrid = () => {
    // Five across, two down, each with the name of what it shows under it. Spheres for most, a
    // rounder one for carry's waves, and a sheet for frontFacing, which a closed shape never shows.
    const round = useSphereGeometry();

    const fine = useIcoSphereGeometry({
        subdivisions: 4
    });

    const sheet = usePlaneGeometry({
        width: 1,
        depth: 1
    });

    const models = PANELS.map((panel, i) => {
        const x = 80 + (i % 5) * 120;
        const y = 150 + Math.floor(i / 5) * 140;
        const geometry = panel.name === 'frontFacing' ? sheet : panel.name === 'carry' ? fine : round;

        createText({
            text: panel.name,
            anchor: {
                x: 0.5,
                y: 0
            },
            tint: Settings.DIM,
            transform: {
                x,
                y: y + 42
            }
        });

        // With no camera of its own a model is drawn in the screen's pixels, but with y growing
        // upwards, as it does in 3D: Settings.HEIGHT - y puts it where the text under it is.
        return createMesh({
            geometry,
            material: createMaterial({
                ...panel.shader,
                ...panel.options
            }),
            transform: {
                x,
                y: Settings.HEIGHT - y,
                scaleX: 64,
                scaleY: 64,
                scaleZ: 64,
                rotationX: 0.4
            },
        });
    });

    return models;
};
```

`scenes/demo.ts`:

```ts
import { createScene, createText, useDirectionalLight, useGame, useKeyboard, useUpdate, useSpawn } from 'nacatamalon';

import * as Settings from '../settings';

import { Backdrop } from '../components/backdrop';
import { PanelGrid } from '../components/panelgrid';

export const Level = () => {
    useDirectionalLight({
        intensity: 1,
        ambient: 0.25,
        transform: {
            rotationX: -0.6,
            rotationY: 0.7
        }
    });

    useSpawn(Backdrop)();

    const models = useSpawn(PanelGrid)().api;

    createText({
        text: 'P: PAUSE   S: SLOW',
        tint: Settings.DIM,
        transform: {
            x: 12,
            y: Settings.HEIGHT - 22
        }
    });

    const game = useGame();
    const keys = useKeyboard();
    let paused = false;
    let slow = false;

    useUpdate((delta) => {
        if (keys.justPressed('p')) {
            paused = !paused;
        }

        if (keys.justPressed('s')) {
            slow = !slow;
        }

        game.setTimeScale(paused ? 0 : slow ? 0.25 : 1);

        for (const model of models) {
            model.transform.rotationY += delta * 0.7;
        }

        // The sheet tips over and back, so both of its sides take their turn.
        models[8].transform.rotationX += delta * 1.1;
    });

    return createScene();
};
```

`settings.ts`:

```ts
import { getColor } from 'nacatamalon';

export const WIDTH = 640;
export const HEIGHT = 400;
export const DIM = getColor('#8a93b4');
```

`shaders/inputs.ts`:

```ts
import type { TMaterialOptions } from 'nacatamalon';
import type { TComposerInput, TCompiledShader } from 'nacatamalon/shader';
import { compileShader, composerAdd, composerCarry, composerDiv, composerDot, composerFloor, composerFract, composerFrontFacing, composerLight, composerMix, composerMod, composerMul, composerNormalize, composerOneMinus, composerPow, composerResolution, composerScreenPos, composerShine, composerSin, composerSmoothstep, composerStep, composerSub, composerTime, composerUv, composerVec3, composerVec4, composerVertexNormal, composerVertexPosition, composerViewDir, composerWorldNormal, composerWorldPos, composerWorldTangent, composerX, composerY } from 'nacatamalon/shader';

// One shader per input, each written once with the shader composer.

// A direction (-1 to 1 on each axis) as a colour (0 to 1).
const asColour = (direction: TComposerInput) => composerVec4(composerAdd(composerMul(direction, 0.5), 0.5), 1);

// mu.resolution, on a sprite as big as the screen: uv times the size is the pixel it covers.
const pixel = composerMul(composerUv(), composerResolution());
const onLine = (along: TComposerInput) => composerStep(composerFract(composerDiv(along, 40)), 1 / 40);
const grid = composerAdd(onLine(composerX(pixel)), onLine(composerY(pixel)));

export const BACKDROP = compileShader({
    shader: 'sprite2d',
    color: composerVec4(composerMix(composerVec3(0.04, 0.05, 0.09), composerVec3(0.12, 0.15, 0.26), grid), 1),
});

// mu.time: a band of light running along the bar, once every two seconds of game time.
const run = composerFract(composerSub(composerX(composerUv()), composerMul(composerTime(), 0.5)));
const band = composerSmoothstep(0.8, 1, run);

export const STRIPE = compileShader({
    shader: 'sprite2d',
    color: composerVec4(composerMix(composerVec3(0.12, 0.14, 0.24), composerVec3(1, 0.85, 0.3), band), 1),
});

// carry: each corner lifted by a wave, and how high handed on to the colour.
const corner = composerVertexPosition();
const lift = composerMul(composerSin(composerAdd(composerMul(composerY(corner), 9), composerMul(composerTime(), 4))), 0.04);
const foam = composerSmoothstep(0.01, 0.04, composerX(composerCarry()));

// Light and the way the surface faces the camera: a rim where it turns away.
const facing = composerDot(composerNormalize(composerWorldNormal()), composerViewDir());
const rim = composerPow(composerOneMinus(facing), 2);

// A checker 6 pixels a square, laid on the screen rather than on the model.
const cell = composerMod(composerAdd(composerFloor(composerDiv(composerX(composerScreenPos()), 6)), composerFloor(composerDiv(composerY(composerScreenPos()), 6))), 2);

export const PANELS = [
    {
        name: 'uv',
        shader: compileShader({
            shader: 'mesh3d',
            color: composerVec4(composerUv(), 0, 1)
        })
    },
    {
        name: 'normal',
        shader: compileShader({
            shader: 'mesh3d',
            color: asColour(composerWorldNormal())
        })
    },
    // Bands every 16 units of the world: they stay put while the model turns through them.
    {
        name: 'worldPos',
        shader: compileShader({
            shader: 'mesh3d',
            color: composerVec4(composerFract(composerDiv(composerWorldPos(), 16)), 1)
        })
    },
    {
        name: 'light',
        shader: compileShader({
            shader: 'mesh3d',
            color: composerVec4(composerLight(), 1)
        })
    },
    {
        name: 'shine',
        shader: compileShader({
            shader: 'mesh3d',
            color: composerVec4(composerShine(), 1)
        }),
        options: {
            specular: '#ffffff',
            shininess: 24
        }
    },
    {
        name: 'viewDir',
        shader: compileShader({
            shader: 'mesh3d',
            color: composerVec4(composerMul(composerVec3(0.3, 0.9, 1), rim), 1)
        })
    },
    {
        name: 'screen',
        shader: compileShader({
            shader: 'mesh3d',
            color: composerVec4(composerMix(composerVec3(0.2, 0.2, 0.35), composerVec3(0.95, 0.5, 0.75), cell), 1)
        })
    },
    {
        name: 'tangent',
        shader: compileShader({
            shader: 'mesh3d',
            color: asColour(composerWorldTangent())
        })
    },
    {
        name: 'frontFacing',
        shader: compileShader({
            shader: 'mesh3d',
            color: composerVec4(composerMix(composerVec3(1, 0.5, 0.2), composerVec3(0.3, 0.9, 0.5), composerFrontFacing()), 1)
        }),
        options: {
            cull: 'none'
        },
    },
    {
        name: 'carry',
        shader: compileShader({
            shader: 'mesh3d',
            position: composerAdd(corner, composerMul(composerVertexNormal(), lift)),
            carry: lift,
            color: composerVec4(composerMul(composerMix(composerVec3(0.1, 0.35, 0.8), composerVec3(0.95, 0.98, 1), foam), composerLight()), 1),
        }),
    },
] satisfies { name: string; shader: TCompiledShader; options?: Partial<TMaterialOptions> }[];
```
