# What the camera skips

> A model wholly outside what a camera sees is not drawn at all. A camera above, told to skip by the eye, shows what the eye leaves out.

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

A model whose box is wholly outside what a camera sees is skipped before it costs anything: its numbers, its draw, every corner the card would have moved. So a field of 625 cubes costs what is in front of you. Each camera decides on its own, so what one leaves out another still draws. There are no shadows here, but a shadow is judged by its light in the same way, so a model out of the picture still throws one into it.

From the camera itself you cannot see it happen: what it skips is exactly what it would not have shown. So the picture in the corner looks from above, and is told to draw only what the eye sees: `above.cullingCamera = eye`. The yellow lines are the eye's view. Walk with the arrows and the field appears ahead and vanishes behind; `C` lets the camera above draw everything again.

A model that bends is judged in the pose it is in. One whose material moves its corners in a `vertex` hook is always drawn until it says how far they go, `boundsMargin: 0.4`, in its own units; `alwaysDrawn: true` never skips one.

## Code

`index.ts`:

```ts
import { createGame } from 'nacatamalon';
import * as Settings from './settings';
import { Level } from './scenes/demo';

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

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

`components/field.ts`:

```ts
import { createMesh, getColor, useCubeGeometry, usePlaneGeometry } from 'nacatamalon';

export const Field = () => {
    createMesh({
        geometry: usePlaneGeometry({
            width: 120,
            depth: 120
        }),
        tint: '#1d2a33'
    });

    const cube = useCubeGeometry();

    for (let i = -12;i <= 12;i++) {
        for (let j = -12;j <= 12;j++) {
            createMesh({
                geometry: cube,
                tint: getColor((i + j) % 2 === 0 ? '#5bd6a0' : '#e8a54b'),
                transform: {
                    x: i * 4,
                    y: 0.6,
                    z: j * 4,
                    scaleX: 1.2,
                    scaleY: 1.2,
                    scaleZ: 1.2
                },
            });
        }
    }
};
```

`scenes/demo.ts`:

```ts
import { createScene, createSprite, createSpriteTexture, createText, screenToRay, useCamera3d, useDirectionalLight, useLines, useKeyboard, useUpdate, useSpawn } from 'nacatamalon';
import * as Settings from '../settings';

import { Field } from '../components/field';
import { Above } from '../views/above';

export const Level = () => {
    const eye = useCamera3d({
        projection: 'perspective',
        fov: 60,
        near: 0.1,
        far: Settings.FAR,
        transform: {
            y: 1.6,
            z: 10
        }
    });

    useDirectionalLight({
        intensity: 1.1,
        ambient: 0.45,
        transform: {
            rotationX: -0.9,
            rotationY: 0.5
        }
    });

    useSpawn(Field)();

    // The eye's view as lines: from where it stands to the four corners of how far it sees. Made
    // inside the picture, so only the camera above draws them: from the eye itself they would lie
    // exactly on the edges of the screen, and flicker there as it moves.
    let setView: ReturnType<typeof useLines>[1] = () => { };
    const corners = [[0, 0], [Settings.WIDTH, 0], [Settings.WIDTH, Settings.HEIGHT], [0, Settings.HEIGHT]];
    const lines = new Float32Array(16 * 7);
    const put = (at: number, p: { x: number; y: number; z: number; }) => lines.set([p.x, p.y, p.z, 1, 0.85, 0.2, 1], at * 7);

    // Up above, looking straight down, into a picture of its own.
    const label = createText({
        text: '',
        tint: '#ffd84f',
        transform: {
            x: 8,
            y: 8
        },
        zIndex: 11
    });

    const map = createSpriteTexture({
        width: Settings.MAP_W,
        height: Settings.MAP_H,
        background: '#0b0f18',
        sees: '3d'
    }, Above, eye, label, (write) => {
        setView = write;
    });

    createSprite({
        texture: map,
        width: Settings.MAP_W * 1.25,
        height: Settings.MAP_H * 1.25,
        anchor: {
            x: 1,
            y: 0
        },
        transform: {
            x: Settings.WIDTH - 8,
            y: 8
        },
        zIndex: 10
    });

    createText({
        text: 'ARROWS: TURN AND WALK',
        tint: '#9aa7b4',
        transform: {
            x: 8,
            y: Settings.HEIGHT - 16
        },
        zIndex: 11
    });

    const keys = useKeyboard();
    const axis = (neg: string, pos: string) => (keys.isDown(pos) ? 1 : 0) - (keys.isDown(neg) ? 1 : 0);

    useUpdate((dt) => {
        const t = eye.transform;

        t.rotationY -= axis('ArrowLeft', 'ArrowRight') * dt * 1.6;

        const step = axis('ArrowDown', 'ArrowUp') * dt * 8;

        t.x -= Math.sin(t.rotationY) * step;
        t.z -= Math.cos(t.rotationY) * step;

        // Each corner of the screen, carried along its ray until it is FAR in front of the eye.
        const ahead = screenToRay(eye, Settings.WIDTH, Settings.HEIGHT, Settings.WIDTH / 2, Settings.HEIGHT / 2).direction;

        const far = corners.map(([x, y]) => {
            const ray = screenToRay(eye, Settings.WIDTH, Settings.HEIGHT, x, y);
            const along = Settings.FAR / (ray.direction.x * ahead.x + ray.direction.y * ahead.y + ray.direction.z * ahead.z);

            return {
                x: t.x + ray.direction.x * along,
                y: t.y + ray.direction.y * along,
                z: t.z + ray.direction.z * along
            };
        });

        for (let k = 0;k < 4;k++) {
            put(k * 2, t);
            put(k * 2 + 1, far[k]);
            put(8 + k * 2, far[k]);
            put(8 + k * 2 + 1, far[(k + 1) % 4]);
        }

        setView(lines);
    });

    return createScene();
};
```

`settings.ts`:

```ts
export const WIDTH = 480;
export const HEIGHT = 360;
// The picture is the screen's shape, so the eye is measured on the same size as on the screen.
export const MAP_W = 160;
export const MAP_H = 120;
export const FAR = 30;
```

`views/above.ts`:

```ts
import { useCamera3d, useLines, useKeyboard, useUpdate } from 'nacatamalon';

import type { TCamera3d, TText } from 'nacatamalon';

export const Above = (eye: TCamera3d, label: TText, connect: (write: ReturnType<typeof useLines>[1]) => void) => {
    const above = useCamera3d({
        projection: 'perspective',
        fov: 70,
        near: 1,
        far: 200,
        transform: {
            y: 70,
            rotationX: -Math.PI / 2
        }
    });

    above.cullingCamera = eye;

    const [, write] = useLines();

    connect(write);

    const keys = useKeyboard();

    useUpdate(() => {
        if (keys.justPressed('c')) {
            above.cullingCamera = above.cullingCamera ? null : eye;
        }

        label.text = above.cullingCamera ? 'C: ABOVE SKIPS BY THE EYE' : 'C: ABOVE DRAWS ALL IT SEES';
    });
};
```
