Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / UI

Canvas 3D

Draw.Camera(ex, ey, ez, tx, ty, tz, fovDeg) · Draw.Light(dx, dy, dz, sun, ambient) · Draw.Fog(color, near, far) · Draw.Mesh(mesh, x, y, z[, rx, ry, rz, scale | sx, sy, sz], color) · Mesh.Box(w, h, d) · Mesh.Sphere(radius, detail) · Mesh.Cylinder(rTop, rBottom, height, segments) · Mesh.Cone(radius, height, segments) · Mesh.Plane(w, d) · Mesh.From(vertices, indices)

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.

preview2 examples compiled by CIuicanvas3ddrawing

Summary#

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#

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

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

Description#

A scene is drawn each frame, like everything else on a canvas#

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.

[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); }
}

Coordinates and units#

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#

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#

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#

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.

[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#

#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.

It lowers to a kernel like the rest of the frame body#

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#

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#

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#

  • Canvas — the surface itself and the 2D verbs, which draw under and over the scene
  • on mount / on unmounton frame (double dt), the clock that drives it
  • component — component state, where the meshes and the world live

Related

Canvas

A drawing surface, and the verbs that paint on it. Put a `Canvas` in a render block, call `Draw.*` from an `on frame`…

on mount / on unmount

`on mount { … }` runs a block ONCE, the first time a component appears — before its first paint; `on unmount { … }`…

component

The one archetype for all UI: a bounded reactive unit — typed props, reactive members (fields, `live`…