# Canvas 3D

> A lit, shadowed 3D scene on the same `Canvas` the 2D verbs paint. Build meshes once into fields, then each frame place a camera, a sun and some fog, and draw each mesh where it is now. The scene is rendered on the GPU and composited into the canvas, so 2D verbs draw a sky behind it and a score over it in the same frame body.

<!-- id: ui-canvas-3d · area: ui · stability: preview · html: https://osysharp.com/reference/ui/canvas-3d/ -->

## Summary        {#summary}
[Canvas](https://osysharp.com/reference/ui/canvas/) paints flat shapes. A game that wants **depth** — hills that recede, a bird lit from one side, a
shadow under it, haze at the horizon — cannot get it from rectangles at any frame rate, so the canvas carries a
second layer: a **scene** of meshes, drawn with the same immediate-mode discipline as everything else. Nothing is
retained between frames except the meshes themselves; each frame you say where the camera is, where the light
comes from, and where every mesh sits now.

The look is deliberately **low-poly and flat-shaded**: every face is one flat colour, lit by a sun with a soft
shadow, an ambient sky, filmic tone mapping and distance fog. That is the look a few hundred well-placed boxes,
cones and spheres produce best, and it is what runs at 60fps on an ordinary laptop.

## Signature      {#signature}
```osy syntax
Mesh ground = Mesh.Plane(200, 200);            // a field — build a mesh ONCE, in `on mount` or an initializer
Mesh trunk  = Mesh.Cylinder(0.3, 0.4, 2, 8);   // (radiusTop, radiusBottom, height, segments), along y
Mesh crown  = Mesh.Cone(1.4, 3, 8);            // (radius, height, segments), point on top
Mesh rock   = Mesh.Sphere(1, 1);               // (radius, detail 0..4) — an icosphere, 20 × 4^detail faces
Mesh crate  = Mesh.Box(1, 1, 1);               // (width, height, depth), centred on the origin
Mesh hill   = Mesh.From(vertices, indices);    // x, y, z per vertex; three vertex numbers per triangle
Mesh ball   = Mesh.Smooth(rock);               // the same triangles, SMOOTH-shaded — nothing moves
Mesh ball2  = Mesh.Smooth(rock, 30);           // ...sharing a normal only across faces within 30 degrees

Draw.Camera(ex, ey, ez, tx, ty, tz, fovDeg);   // eye position, the point it looks at, vertical field of view
Draw.Light(dx, dy, dz, sun, ambient);          // the direction the sun SHINES ALONG, its colour, the sky colour
Draw.Fog(color, near, far);                    // haze from `near` to fully `color` at `far`, in scene units

Draw.Mesh(crate, x, y, z, color);                                // placed
Draw.Mesh(crate, x, y, z, rx, ry, rz, scale, color);             // rotated (radians) and scaled uniformly
Draw.Mesh(crate, x, y, z, rx, ry, rz, sx, sy, sz, color);        // …or per axis

Draw.Mesh(ground, x, y, z, Textures.Rock);                       // …or one of the app's TEXTURES instead of a colour
Draw.Mesh(ground, x, y, z, Textures.Rock, 0.25);                 // …tiled every four world units
Draw.Mesh(ground, x, y, z, Textures.Rock, 0.25, "#9bba66");      // ...and TINTED: the texture times this colour

Draw.Sprite(Textures.Coin, x, y, z, w, h);                       // a BILLBOARD - always faces the camera
Draw.Sprite(Textures.Sheet, sx, sy, sw, sh, x, y, z, w, h);      // ...a frame out of a sprite SHEET
Draw.Sprite(Textures.Sheet, sx, sy, sw, sh, x, y, z, w, h, "#f80");   // ...tinted
```

## Description    {#description}

### A scene is drawn each frame, like everything else on a canvas   {#a-frame}
The meshes live in fields because building one costs a buffer upload; everything else is said again every frame.
The 2D verbs and the 3D verbs share the frame body: the sky is a `Draw.Rect`, the scene is drawn over it, and the
score is a `Draw.Text` over that — in the order they are written.

```osy title="a tree on a hill, lit from the side" test app=arcade-3d
[Page("/tree")]
[AllowAnonymous]
component Tree() {
  Mesh ground;
  Mesh trunk;
  Mesh crown;
  double t = 0;

  on mount {
    ground = Mesh.Plane(60, 60);
    trunk = Mesh.Cylinder(0.25, 0.35, 1.6, 8);
    crown = Mesh.Cone(1.3, 3.2, 8);
  }

  on frame (double dt) {
    t += dt;
    Draw.Rect(0, 0, 640, 360, "#bfe3ff");                    // the sky, in 2D, BEHIND the scene
    Draw.Camera(0, 3, 12, 0, 1.5, 0, 45);
    Draw.Light(-0.5, -1, -0.4, "#fff4d6", "#9ec5ff");
    Draw.Fog("#bfe3ff", 20, 60);
    Draw.Mesh(ground, 0, 0, 0, "#7bbf5a");
    Draw.Mesh(trunk, 0, 0.8, 0, "#8a5a3c");
    Draw.Mesh(crown, 0, 3.2, 0, 0, t * 0.3, 0, 1, "#3f8f4a");   // slowly turning about y
    Draw.Text("a tree", 12, 12, "#204020", 20);              // the HUD, in 2D, OVER the scene
  }

  render { Canvas(w: 640, h: 360); }
}
```

### Which way is up, where is the origin, and how big is one unit?   {#coordinates}
The scene is **right-handed with y up**: `+x` is screen-right for a camera looking along `−z`, and `+y` is up.
Units are whatever you choose — the builders, the camera and the fog all speak the same ones. Rotations are in
**radians**, like `Math.Sin`, and apply **x, then y, then z** — pitch a bird, then yaw it to its heading.

A mesh's origin is its **centre** (a `Plane` is centred at `y = 0`, a `Cylinder`/`Cone` runs from `−height/2` to
`+height/2`), so a tree of height 3 standing on the ground is drawn at `y = 1.5`.

### The light is a direction, and the second colour is the sky   {#light}
`Draw.Light(dx, dy, dz, sun, ambient)` takes the direction the sun **shines along** — `(−0.5, −1, −0.4)` is a sun
high and to the right, casting shadows down and to the left. Faces turned toward it get `sun`; faces turned away
get the `ambient` colour, stronger on faces that look up (the sky) than on faces that look down. Shadows are
cast by every mesh onto every mesh, softened at the edge, within the range the fog reaches — a shadow far beyond
the fog would never be seen, and the shadow map's resolution is spent where it shows.

### Fog is what makes distance read   {#fog}
`Draw.Fog(color, near, far)` blends every surface toward `color` from `near` (no fog) to `far` (only fog). Give
it the sky's colour and a far hill dissolves into the horizon the way it does outdoors; it also sets how far the
shadow map reaches, so the two are tuned together.

### `Mesh.From` builds anything the builders cannot   {#from}
A heightfield, a bird's body, a rock: give it every vertex as `x, y, z` and every triangle as three vertex
numbers. Faces are **flat-shaded** from their own winding, so wind each triangle **counter-clockwise seen from
the outside** — a face wound the other way is culled as a back face and simply is not there. An index outside
the vertex list, or a list whose length is not a multiple of three, is an error that names the position.

```osy title="a ridge from a heightfield" test app=arcade-3d
[Page("/ridge")]
[AllowAnonymous]
component Ridge() {
  Mesh ridge;

  on mount {
    var verts = new List<double>();
    var idx = new List<int>();
    var cols = 24;
    var rows = 6;
    for (var r = 0; r <= rows; r++) {
      for (var c = 0; c <= cols; c++) {
        var x = (c - cols / 2.0) * 2;
        var z = (r - rows / 2.0) * 2;
        var y = Math.Sin(c * 0.5) * 1.5 + Math.Cos(r * 0.9) * 0.6 + 2;
        verts.Add(x); verts.Add(y); verts.Add(z);
      }
    }
    for (var r = 0; r < rows; r++) {
      for (var c = 0; c < cols; c++) {
        var a = r * (cols + 1) + c;
        var b = a + 1;
        var d = a + cols + 1;
        var e = d + 1;
        idx.Add(a); idx.Add(d); idx.Add(b);   // counter-clockwise seen from above (+y)
        idx.Add(b); idx.Add(d); idx.Add(e);
      }
    }
    ridge = Mesh.From(verts, idx);
  }

  on frame (double dt) {
    Draw.Rect(0, 0, 640, 360, "#cfe8ff");
    Draw.Camera(0, 8, 22, 0, 2, 0, 40);
    Draw.Light(-0.4, -1, -0.6, "#fff3d0", "#a9cbff");
    Draw.Fog("#cfe8ff", 25, 70);
    Draw.Mesh(ridge, 0, 0, 0, "#6faf58");
  }

  render { Canvas(w: 640, h: 360); }
}
```

### Colours are the 2D verbs' colours   {#colours}
`#rgb`, `#rrggbb`, `rgb(…)` and `rgba(…)` — the alpha is ignored, a mesh is opaque. Anything else draws
**magenta**, the colour every renderer uses to mean "this is not a colour", so a typo is loud rather than dark.

### A texture where the colour goes   {#textures}
Put one of the app's own textures ([textures](https://osysharp.com/reference/ui/textures/)) where a mesh's colour would be and it is **projected onto the
surface** — a rock face, a brick wall, a grass floor:

```osy syntax
Draw.Mesh(ground, 0, 0, 0, Textures.Grass, 0.25);   // a tile every four world units
Draw.Mesh(crate, 2, 0.5, 0, Textures.Crate);        // one tile per unit, the default
```

The trailing number is **tiles per world unit**. It is on the draw rather than on the scene because a ground plane
and a crate want different densities in the same frame.

⚠️ **Tiling is per world unit, and getting it too low is the commonest way a texture goes missing.** A hill seven
units across at `0.12` gets less than one tile — the texture is there and stretched until nothing reads. Something
like `0.5` puts three or four across the slope, which is what reads as ground cover.

### One texture, many tones — the tint   {#tint}
A colour after the tiling is a **tint**: the texture is *multiplied* by it, so `"#ffffff"` is the texture untouched
and anything darker or warmer shades it.

```osy syntax
Draw.Mesh(hill, x, y, z, Textures.Grass, 0.5, "#9bba66");   // near: green
Draw.Mesh(hill, x, y, z, Textures.Grass, 0.5, "#c9cf8a");   // far: washed toward the haze
```

⭐ **This is what lets one texture carry a whole scene.** Without a tint every textured surface is the texture's own
colours exactly, so a valley of seventeen hills is seventeen identical hills. With it, the same moss reads as
near-green and far-gold — which is most of what makes a landscape recede.

A `Gradient` is **refused** here rather than quietly ignored: a mesh's colour is one value for the whole surface,
and a canvas fill has nowhere to go in it.

⚠️ **It is projected, not UV-mapped, and the difference is worth knowing.** The texture is a property of the world
*position*: the renderer samples it down the three world axes and blends by the surface normal. That means it works
on every mesh — including one you built with `Mesh.From`, which has no texture coordinates to map with — and it
tiles seamlessly across the joint between two meshes, which is what a floor made of several planes wants.

What it cannot do is a **decal**: there is no way to put a label on one face of a crate, or a face on a character.
For that, draw the mark with the 2D verbs over the scene — they paint on the same canvas, after it.

### Smooth shading, without changing the geometry   {#smooth}
Every mesh is **flat-shaded** by default: each triangle carries one normal, so a sphere reads as facets. That is
the low-poly look, and it is right for a crate or a crystal. For a hill, a cloud, a tree or a character it is what
makes a scene read as *blocky*.

`Mesh.Smooth` averages the shading normals of faces that meet at the same point:

```osy syntax
Mesh hills  = Mesh.Smooth(Mesh.From(vertices, indices));   // rolling, not faceted
Mesh cloud  = Mesh.Smooth(Mesh.Sphere(1, 2));              // a round ball at 320 faces
Mesh trunk  = Mesh.Smooth(Mesh.Cylinder(1, 1, 2, 18));     // round SIDE, flat CAPS - one call
```

**Nothing moves.** The positions, the silhouette and the number of triangles are identical; only what the lighting
is handed changes. So it costs nothing per frame - do it once, where you build the mesh.

* **The angle is what makes one verb safe on any mesh.** Two faces share a normal only if they meet within `60`
degrees, so the same call that rounds a sphere leaves a **box untouched** - every edge of a box is 90 degrees. Pass
your own angle as a second argument when you want more or less: `Mesh.Smooth(m, 100)` will round a box's corners,
and `Mesh.Smooth(m, 20)` keeps all but the gentlest creases sharp.

Because the threshold is per *pair of faces*, one call gives a cylinder a round side and flat caps - the answer you
would otherwise have to build by hand.

**Vertices are welded by position**, to within a hundredth of a millimetre at world scale. Two vertices genuinely
closer together than that are treated as one point.

### Sprites — a picture standing in the world   {#sprites}
A `Draw.Sprite` is a flat rectangle of one of your textures that **always faces the camera**, placed at a world
position and sized in world units:

```osy syntax
Draw.Sprite(Textures.Coin, 4, 1.5, -2, 0.8, 0.8);               // the whole image
Draw.Sprite(Textures.Sparks, 128, 0, 128, 128, x, y, z, s, s);  // one FRAME of a 2x2 sheet
```

The nine-number form takes a **source rectangle in the image's own pixels**, exactly as [Canvas](https://osysharp.com/reference/ui/canvas/)'s
`Draw.Image` does — so one sprite sheet is cut up the same way in 2D and in 3D, and an animation is just picking
`sx` from a frame counter.

⭐ **This is what a textured mesh cannot do.** A mesh's texture is projected by world *position*; a sprite carries
the picture itself, so a specific image lands on it wherever it stands. Particles, pickups, distant trees, motes of
pollen and world-space markers are all sprites.

**They behave correctly against the scene**, which is most of the work:

| | |
|---|---|
| **hidden by what is in front** | a sprite behind a hill is not drawn — it is depth-tested like everything else |
| **transparent** | the image's alpha is respected, so a soft-edged mote is a mote and not a square |
| **drawn in the right order** | overlapping sprites blend back-to-front, and none of them hides another |
| **fogged** | a distant sprite fades into the haze with the rest of the scene |

**A sprite is unlit**, and that is deliberate: it is a painted image — a flame, a coin, a marker — and shading it
by a surface normal it does not have would only mean fighting the sun. A trailing colour **tints** it (multiplied,
so `"#ffffff"` is the image untouched), which is how one flame sheet serves an orange flame and a blue one.

### It lowers to a kernel like the rest of the frame body   {#kernel}
Every 3D verb and every builder is part of the host contract a frame body is lowered against, so a body that uses
them still runs as a compiled JavaScript kernel — the hot loop of a game pays no interpreter cost for being 3D.
A mesh in a field is a plain value to the kernel; build it in `on mount`, read it in `on frame`.

### Where WebGL is missing, the scene is missing   {#degrade}
The scene is rendered by the browser's GPU. In an environment with no WebGL the 3D verbs log one error and draw
nothing, while the 2D verbs keep working — so a HUD and a backdrop still appear over a blank scene rather than
the page failing. Real browsers all have it; the headless DOM the unit tests run in does not, which is why the
pixels are proven by the visual harness and not by a unit test.

### What a frame can afford   {#budget}
The cost is per **mesh drawn** and per **face** — not per pixel — plus one shadow pass over the same meshes. A few
hundred `Draw.Mesh` calls over meshes of tens to low hundreds of faces each is comfortably 60fps; a `Sphere` at
detail 4 is 5,120 faces and is the wrong choice for anything smaller than a planet.

## See also {#see-also}
- [Canvas](https://osysharp.com/reference/ui/canvas/) — the surface itself and the 2D verbs, which draw under and over the scene
- [on mount / on unmount](https://osysharp.com/reference/ui/lifecycle/) — `on frame (double dt)`, the clock that drives it
- [component](https://osysharp.com/reference/ui/component/) — component state, where the meshes and the world live
