# Gas station looks

> Material looks on a model made in a modelling tool: a gas station at dusk, ground painted in code and the nightcrawler walking it.

Example `gas-station-looks`, running at https://nacatamalon.com/en/examples/gas-station-looks. The whole catalogue: https://nacatamalon.com/llms/examples.md.

The material looks on a model made in a modelling tool: a gas station at dusk, its forecourt and the road beside it painted in code, and the nightcrawler walking round the pumps and out along the road. The station keeps the pictures its file gave it; only its shader changes with the look, through `compileShader({ lighting, steps })`.

`toonLighting()` cuts the light of the scene into bands and keeps its colour: the warm lamps under the canopy give warm bands, the street lamp a cold one, and the low sun the shadows the dots fill. The lamps have `ambient: 0`, or their colours would wash the whole forecourt.

Over any look, R lays the old machine (`mosaic`, `bloom`, `paletteMatch` with a dither, `crt`) and A draws the frame in letters with `ascii()`.

Click the preview first so it hears the keyboard. 1 to 5 change the look, R the old machine, P its palette and A the letters.

## Code

`index.ts`:

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

import * as Settings from './settings';
import { Game } from './scenes/demo';

const game = createGame('#app', {
    width: Settings.WIDTH,
    height: Settings.HEIGHT,
    scaling: 'contain',
    background: getColor(Settings.DUSK)
});

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

`art/ground.ts`:

```ts
import { getColor } from 'nacatamalon';
import { createPixelTexture, createPixels, fillCircle, fillNoise, fillRect } from 'nacatamalon/pixels';

const PX = 16;

// Concrete slabs two metres across, with the seams between them and oil where cars stand at the pumps.
export const paintConcrete = () => {
    const size = 16 * PX;
    const ground = createPixels(size, size);

    fillNoise(ground, {
        seed: 3,
        scale: 10,
        octaves: 3,
        from: '#8a8680',
        to: '#a9a59c'
    });

    const seam = getColor('#6f6c66');

    for (let at = 0; at < size; at += 2 * PX) {
        fillRect(ground, at, 0, 1, size, seam);
        fillRect(ground, 0, at, size, 1, seam);
    }

    // The pumps stand a metre from the middle of the forecourt, along it.
    const oil = getColor('#5e5b56');

    for (const [x, y, r] of [[150, 96, 9], [140, 120, 6], [152, 150, 10], [138, 172, 7]]) {
        fillCircle(ground, x, y, r, oil);
    }

    return createPixelTexture(ground);
};

// Asphalt with a white line at each edge and a dashed yellow one down the middle.
export const paintRoad = () => {
    const width = 8 * PX;
    const length = 80 * PX;
    const road = createPixels(width, length);

    fillNoise(road, {
        seed: 7,
        scale: 6,
        octaves: 3,
        from: '#26252c',
        to: '#3b3a43'
    });

    const white = getColor('#d9d6cf');

    fillRect(road, 6, 0, 2, length, white);
    fillRect(road, width - 8, 0, 2, length, white);

    const yellow = getColor('#e8b33a');

    for (let at = 0; at < length; at += 3 * PX) {
        fillRect(road, width / 2 - 1, at, 2, PX + PX / 2, yellow);
    }

    return createPixelTexture(road);
};
```

`components/station.ts`:

```ts
import { createMaterial, createMesh, createModel, createText, useKeyboard, useLoadModel, usePlaneGeometry, useSkeletalAnimation, useUpdate, useWatch } from 'nacatamalon';
import { compileShader } from 'nacatamalon/shader';
import { ascii, bloom, crt, mosaic, paletteMatch, useLoadPalette, usePostProcess, POST_EFFECTS } from 'nacatamalon/effects';
import * as Settings from '../settings';

import type { TMaterial } from 'nacatamalon';
import { paintConcrete, paintRoad } from '../art/ground';

export const Station = () => {
    // The ground, painted in code: the forecourt's concrete slabs and the road beside it.
    // Cut into squares of one unit: the light and the fog are worked out at the corners, so the
    // lamps' pools of light need corners to land on.
    const grounds = [
        {
            geometry: usePlaneGeometry({
                width: 16,
                depth: 16,
                widthSegments: 16,
                depthSegments: 16
            }),
            picture: paintConcrete(),
            at: {
                x: -1,
                y: 0,
                z: 0
            }
        },
        {
            geometry: usePlaneGeometry({
                width: 8,
                depth: 80,
                widthSegments: 8,
                depthSegments: 80
            }),
            picture: paintRoad(),
            at: {
                x: 11,
                y: 0,
                z: 0
            }
        },
    ];

    // Every look for each piece of ground, made now: pressing a key only swaps what it wears. No
    // rim on the ground: seen at a slant, all of it is edge to the camera.
    const floors = grounds.map((ground) => {
        const materials = Settings.LOOKS.map((look) => createMaterial({
            shader: 'mesh3d',
            texture: ground.picture,
            ...(look.lighting ? {
                lighting: look.lighting
            } : {
            }),
            ...(look.steps ? {
                steps: look.steps.filter((step) => step.look !== 'rim')
            } : {
            }),
        }));

        const mesh = createMesh({
            geometry: ground.geometry,
            material: materials[0],
            transform: ground.at,
            castShadow: false
        });

        return {
            mesh,
            materials
        };
    });

    // The station keeps the pictures its file gave it; only its shader changes with the look.
    const stationAsset = useLoadModel({
        src: '/example-assets/assets/3d/gltf/gas_station/gas_station.glb'
    });

    const station = createModel({
        model: stationAsset
    });

    const crawler = useLoadModel({
        src: '/example-assets/assets/3d/gltf/nightcrawler/nightcrawler.glb'
    });

    const walker = createModel({
        model: crawler,
        tint: '#d8d0c0'
    });

    useSkeletalAnimation(walker, {
        play: 'Walk'
    });

    const models = [station, walker];

    const shaders = Settings.LOOKS.map((look) => (look.lighting || look.steps
        ? compileShader({
            shader: 'mesh3d',
            lighting: look.lighting,
            steps: look.steps
        })
        : null));

    const kindOf = (value: number | readonly number[]): 'f32' | 'vec2<f32>' | 'vec3<f32>' | 'vec4<f32>' => (Array.isArray(value) ? (['f32', 'vec2<f32>', 'vec3<f32>', 'vec4<f32>'] as const)[value.length - 1]! : 'f32');

    const dress = (material: TMaterial, which: number) => {
        const shader = shaders[which];

        material.fragment = shader?.fragment ?? null;
        material.fragmentGlsl = shader?.fragmentGlsl ?? null;
        material.uniforms = shader === null ? {
        } : structuredClone(shader.uniforms);
        material.uniformSig = Object.fromEntries(Object.entries(material.uniforms).map(([name, value]) => [name, kindOf(value)]));
    };
    // A model's pieces arrive with its file, after the scene is built, so they are dressed as they come.

    // The old machine, in the order the screen goes through it, off until R. Bigger pixels first,
    // so the palette and the tube are laid over them; one palette effect per palette, one of them on.
    const blocks = usePostProcess({
        ...mosaic({
            size: 2
        }),
        enabled: false
    });

    // The glow before the palette, so what it lights is still shown in the palette's colours.
    const glow = usePostProcess({
        ...bloom({
            threshold: 0.7,
            intensity: 0.5
        }),
        enabled: false
    });

    // Dusk station first: the warm darks of a forecourt at the end of the day, a blue for the street
    // lamp and the red of the canopy. A palette of cold colours puts every warm dark on its one red.
    const names = ['dusk_station', 'old_print', 'night_station', 'moon_dust'];

    const palettes = names.map((name) => usePostProcess({
        ...paletteMatch({
            dither: 0.12
        }),
        palette: useLoadPalette({
            src: '/example-assets/palettes/' + name + '.palette'
        }),
        enabled: false,
    }));

    // The smallest cell there is (6 game pixels), so the nightcrawler is still a shape and not a smear.
    const letters = usePostProcess({
        ...ascii({
            size: 6
        }),
        enabled: false
    });

    // The letters' own glow, after them, so it is the letters that glow; the one before the palette
    // stands aside while they are on.
    const letterGlow = usePostProcess({
        ...bloom({
            threshold: 0.7,
            intensity: 0.5
        }),
        enabled: false
    });

    const tube = usePostProcess({
        ...crt(POST_EFFECTS.CRT_PRESETS.pvm),
        enabled: false
    });

    let retro = false;
    let lettered = false;
    let palette = 0;

    const machine = () => {
        blocks.enabled = retro;
        palettes.forEach((effect, i) => {
            effect.enabled = retro && i === palette;
        });
        letters.enabled = lettered;
        glow.enabled = retro && !lettered;
        letterGlow.enabled = lettered;
        tube.enabled = retro;
    };

    const label = createText({
        text: '',
        tint: '#ffe9c2',
        transform: {
            x: 20,
            y: 20
        },
        style: {
            fontSize: 32
        }
    });

    createText({
        text: '1-5 LOOKS   R OLD MACHINE   P PALETTE   A ASCII',
        tint: '#a99bc9',
        transform: {
            x: 20,
            y: 64
        },
        style: {
            fontSize: 16
        }
    });

    let current = 0;

    const wear = (which: number) => {
        current = which;
        floors.forEach((floor) => {
            floor.mesh.material = floor.materials[which];
        });
        models.forEach((model) => model.parts.forEach((part) => dress(part.material, which)));
        label.text = Settings.LOOKS[which].name;
    };

    wear(1);
    useWatch(() => {
        models.forEach(model => model.parts.forEach(part => dress(part.material, current)));
    }, [stationAsset, crawler]);

    const keys = useKeyboard();
    let turn = 0;

    useUpdate((delta) => {
        Settings.LOOKS.forEach((_, i) => {
            if (keys.justPressed(String(i + 1))) {
                wear(i);
            }
        });

        if (keys.justPressed('r')) {
            retro = !retro;
            machine();
        }

        if (keys.justPressed('p')) {
            palette = (palette + 1) % palettes.length;
            machine();
        }

        if (keys.justPressed('a')) {
            lettered = !lettered;
            machine();
        }

        // Round the pumps and out onto the road, and back.
        turn += delta * 0.18;
        walker.transform.x = 4.5 + Math.cos(turn) * 4.5;
        walker.transform.z = -1.5 + Math.sin(turn) * 5.5;
        walker.transform.rotationY = -turn;
    });
};
```

`scenes/demo.ts`:

```ts
import { cameraLookAt, createScene, getColor, useCamera3d, useFog, useDirectionalLight, usePointLight, useSpawn } from 'nacatamalon';

import * as Settings from '../settings';
import { Station } from '../components/station';

export const Game = () => {
    const camera = useCamera3d({
        projection: 'perspective',
        fov: 50,
        transform: {
            x: 11.5,
            y: 3.4,
            z: 8
        }
    });

    cameraLookAt(camera, {
        x: 0.5,
        y: 1.3,
        z: -0.5
    });
    useFog({
        color: getColor(Settings.DUSK),
        near: 30,
        far: 80
    });

    // A low sun, the colour of the end of the day, that casts; the lamps under the canopy and the
    // street lamp light only what is near them, with no light of their own all round (ambient: 0),
    // or their colours would wash the whole forecourt.
    useDirectionalLight({
        color: '#ffd2a8',
        intensity: 1.1,
        ambient: 0.16,
        transform: {
            rotationX: -0.6,
            rotationY: 0.9
        },
        castShadow: true,
        shadowArea: 40
    });

    for (const z of [-2.2, 2.2]) {
        useSpawn(() => {
            usePointLight({
                color: '#ffc46a',
                intensity: 3,
                ambient: 0,
                range: 6,
                transform: {
                    x: 0,
                    y: 2.7,
                    z
                }
            });
        })();
    }

    useSpawn(() => {
        usePointLight({
            color: '#7cc8ff',
            intensity: 3,
            ambient: 0,
            range: 8,
            transform: {
                x: 10,
                y: 3.5,
                z: -6
            }
        });
    })();

    useSpawn(Station)();

    return createScene();
};
```

`settings.ts`:

```ts
import { mangaDots, rim, toonLighting, unlitLighting } from 'nacatamalon/shader';

export const WIDTH = 1280;
export const HEIGHT = 720;
export const LOOKS = [
    {
        name: 'ENGINE'
    },
    {
        name: 'TOON',
        lighting: toonLighting({
            bands: 3,
            shadow: 0.35
        })
    },
    {
        name: 'TOON + DOTS',
        lighting: toonLighting({
            bands: 3,
            shadow: 0.55
        }),
        steps: [
            mangaDots({
                size: 8,
                fill: 0.45
            })
        ]
    },
    {
        name: 'TOON + DOTS + RIM',
        lighting: toonLighting({
            bands: 3,
            shadow: 0.55
        }),
        steps: [
            mangaDots({
                size: 8,
                fill: 0.45
            }),
            rim({
                color: '#ffd27a'
            })
        ]
    },
    {
        name: 'UNLIT + RIM',
        lighting: unlitLighting(),
        steps: [
            rim({
                color: '#9fe3ff',
                amount: 0.8
            })
        ]
    },
];

export const DUSK = '#2b2440';
```
