# Material looks

> createMaterial({ lighting, steps }): toonLighting(), unlitLighting(), mangaDots() and rim(), with no shader written.

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

A material can be made from looks instead of a shader. `lighting` says how its surface is lit, one look in place of the engine's light, and `steps` what is done to it once lit, in order: `toonLighting()` cuts the light into flat bands, `unlitLighting()` leaves it out, `mangaDots()` inks the shadow with dots laid out on the screen, and `rim()` lights the edge. The engine writes the shader in both languages.

`mangaDots()` after `toonLighting()` inks exactly where the bands went dark, which a screen effect, seeing only colours, cannot. Cast shadows count too: the floor is dotted where the shapes shade it. Every number is a knob of the material (`toonShadow`, `mangaDotsSize`…), to turn while the game runs.

The nightcrawler, the fox and the duck wear the same looks, each piece over its own picture and colour; the two with bones walk and run in them. A model takes a look when it is made with `createModel({ lighting, steps })`; here they change look while running, through `compileShader({ lighting, steps })`.

Click the preview first so it hears the keyboard. 1 to 7: as the engine draws it, toon, toon + dots, toon + dots + rim, dots on the engine's light, unlit + rim, and each surface's tangent as a colour.

## Code

`index.ts`:

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

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

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

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

`components/gallery.ts`:

```ts
import { useWatch, createMaterial, createMesh, createModel, createText, getColor, useConeGeometry, useCubeGeometry, useCylinderGeometry, useIcoSphereGeometry, useKeyboard, useLoadModel, usePlaneGeometry, useSkeletalAnimation, useTorusGeometry, useUpdate, useSphereGeometry } from 'nacatamalon';

import { compileShader } from 'nacatamalon/shader';
import * as Settings from '../settings';
import type { TMaterial } from 'nacatamalon';

export const Gallery = () => {
    const sphere = useSphereGeometry({
        radius: 0.8,
        segments: 48,
        rings: 24
    });

    const cube = useCubeGeometry({
        width: 1,
        height: 1,
        depth: 1
    });

    const things = [
        // The floor goes without the rim: seen at a slant, all of it is edge to the camera.
        {
            geometry: usePlaneGeometry({
                width: 30,
                depth: 30
            }),
            colour: '#f2ead8',
            at: {
                y: 0
            },
            spin: 0,
            floor: true
        },
        {
            geometry: sphere,
            colour: '#7fb8ff',
            at: {
                x: -1.9,
                y: 0.8,
                z: 0.1
            },
            spin: 0
        },
        {
            geometry: useTorusGeometry({
                radius: 0.6,
                tube: 0.24,
                radialSegments: 16,
                tubularSegments: 48
            }),
            colour: '#f0b46a',
            at: {
                x: 1.9,
                y: 0.9,
                z: 0.1
            },
            spin: 1
        },
        {
            geometry: cube,
            colour: '#ff8fa3',
            at: {
                x: -3.2,
                y: 0.7,
                z: -0.4
            },
            spin: 1
        },
        {
            geometry: cube,
            colour: '#b9f18c',
            at: {
                x: 3.3,
                y: 0.7,
                z: -0.4
            },
            spin: 1
        },
        {
            geometry: useConeGeometry({
                radius: 0.6,
                height: 1.3,
                segments: 32
            }),
            colour: '#c9a7ff',
            at: {
                x: -2.2,
                y: 0.65,
                z: 2
            },
            spin: 0.5
        },
        {
            geometry: useCylinderGeometry({
                radius: 0.45,
                height: 1.1,
                segments: 32
            }),
            colour: '#ffd166',
            at: {
                x: 2.3,
                y: 0.55,
                z: 2
            },
            spin: 0.5
        },
        {
            geometry: useIcoSphereGeometry({
                radius: 0.6,
                subdivisions: 1
            }),
            colour: '#6ee7d8',
            at: {
                x: 0,
                y: 0.7,
                z: 2.4
            },
            spin: 1
        },
        {
            geometry: sphere,
            colour: '#ffffff',
            at: {
                x: 0,
                y: 1.1,
                z: -2.6,
                scaleX: 1.4,
                scaleY: 1.4,
                scaleZ: 1.4
            },
            spin: 0
        },
    ];

    // Every look for every thing, made now: a material is made in the scene's body, and a key is
    // pressed long after it. Pressing one only swaps which material each thing wears.
    const meshes = things.map((thing) => {
        const surface = {
            shader: 'mesh3d' as const,
            tint: getColor(thing.colour),
            specular: '#ffffff',
            shininess: 24
        };

        const materials = Settings.LOOKS.map((look) => createMaterial({
            ...surface,
            ...(look.custom ?? {
            }),
            ...(look.lighting ? {
                lighting: look.lighting
            } : {
            }),
            ...(look.steps ? {
                steps: thing.floor ? look.steps.filter((step) => step.look !== 'rim') : look.steps
            } : {
            }),
        }));

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

        return {
            mesh,
            materials,
            spin: thing.spin
        };
    });

    const label = createText({
        text: '',
        tint: '#2a2238',
        transform: {
            x: 10,
            y: 10
        },
        style: {
            fontSize: 16
        }
    });

    createText({
        text: '1-7 LOOKS',
        tint: '#6b5f80',
        transform: {
            x: 10,
            y: 32
        },
        style: {
            fontSize: 8
        }
    });

    // Models take a look when they are made (createModel({ lighting, steps })), each piece over its
    // own picture and colour. Here they change look while running, so each piece keeps the material
    // its file gave it and has only its shader changed, from compileShader({ lighting, steps }).
    // The nightcrawler walks and the fox runs: the bones bend the surface the look is drawn on.
    const crawler = useLoadModel({
        src: '/example-assets/assets/3d/gltf/nightcrawler/nightcrawler.glb'
    });

    const vixen = useLoadModel({
        src: '/example-assets/assets/3d/gltf/Fox/Fox.gltf'
    });

    const duck = useLoadModel({
        src: '/example-assets/assets/3d/gltf/Duck/Duck.gltf'
    });

    const models = [
        createModel({
            model: crawler,
            tint: '#aecde2',
            transform: {
                x: 0,
                z: -0.6
            }
        }),
        createModel({
            model: vixen,
            transform: {
                x: -0.4,
                z: 1.2,
                scaleX: 0.012,
                scaleY: 0.012,
                scaleZ: 0.012
            }
        }),
        createModel({
            model: duck,
            transform: {
                x: 3.4,
                z: 1.4
            }
        }),
    ];

    useSkeletalAnimation(models[0]!, {
        play: 'Walk'
    });
    useSkeletalAnimation(models[1]!, {
        play: 'Run'
    });

    const shaders = Settings.LOOKS.map((look) => look.custom
        ?? (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;
        // A copy of the knobs for each piece, so turning one does not turn them all.
        material.uniforms = shader === null ? {
        } : structuredClone(shader.uniforms);
        material.uniformSig = Object.fromEntries(Object.entries(material.uniforms).map(([name, value]) => [name, kindOf(value)]));
    };

    let current = 0;

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

    wear(3);

    // The pieces arrive with their asset. Dress them when it changes, using the current look.
    useWatch(() => {
        models.forEach(model => model.parts.forEach(part => dress(part.material, current)));
    }, [crawler, vixen, duck]);

    const keys = useKeyboard();

    useUpdate((delta) => {
        Settings.LOOKS.forEach((_, i) => {
            if (keys.justPressed(String(i + 1))) {
                wear(i);
            }
        });
        models.forEach((model, i) => {
            model.transform.rotationY += delta * (i === 0 ? 0.4 : -0.5);
        });
        meshes.forEach((entry, i) => {
            entry.mesh.transform.rotationY += delta * entry.spin * (0.5 + i * 0.07);
            entry.mesh.transform.rotationX += delta * entry.spin * 0.3;
        });
    });
};
```

`scenes/demo.ts`:

```ts
import { createScene, useCamera3d, useDirectionalLight, useUpdate, useSpawn } from 'nacatamalon';

import { Gallery } from '../components/gallery';

export const Game = () => {
    useCamera3d({
        projection: 'perspective',
        fov: 45,
        transform: {
            y: 4.2,
            z: 8,
            rotationX: -0.5
        }
    });

    // The light that casts, so the floor has shadows for the dots to fill too.
    const light = useDirectionalLight({
        color: '#fff6e8',
        intensity: 1.2,
        ambient: 0.18,
        transform: {
            rotationX: -0.7
        },
        castShadow: true
    });

    useSpawn(Gallery)();
    useUpdate((_delta, time) => {
        light.transform.rotationY = Math.sin(time * 0.5) * 1.2;
    });

    return createScene();
};
```

`settings.ts`:

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

export const WIDTH = 640;
export const HEIGHT = 400;
export const LOOKS = [
    {
        name: 'ENGINE'
    },
    {
        name: 'TOON',
        lighting: toonLighting()
    },
    {
        name: 'TOON + DOTS',
        lighting: toonLighting({
            shadow: 0.8
        }),
        steps: [mangaDots()]
    },
    {
        name: 'TOON + DOTS + RIM',
        lighting: toonLighting({
            shadow: 0.8
        }),
        steps: [
            mangaDots(),
            rim({
                color: [1, 0.85, 0.4]
            })
        ]
    },
    {
        name: 'DOTS',
        steps: [mangaDots()]
    },
    {
        name: 'UNLIT + RIM',
        lighting: unlitLighting(),
        steps: [
            rim({
                color: [1, 1, 1],
                amount: 0.9
            })
        ]
    },
    {
        name: 'TANGENT',
        custom: tangentView
    },
];
```

`shaders/tangent.ts`:

```ts
import { compileShader, composerAdd, composerMul, composerVec4, composerWorldTangent } from 'nacatamalon/shader';

export const tangentView = compileShader({
    shader: 'mesh3d',
    color: composerVec4(composerAdd(composerMul(composerWorldTangent(), 0.5), 0.5), 1),
});
```
