Drawing in 3D

Meshes have a 3D version, which goes through a camera of its own. A corner is a Vertex3:

tide
struct Vertex3 // Built in
{
    float3 position;
    float2 uv;
    Color color = Color.white;
}

Draw.SetCamera(position, rotation, fieldOfView) sets the 3D camera: where it is, which way it looks, along its rotation's +z with +x to the right and +y up as Unity's cameras do, and how much it sees up and down, in degrees, like Unity's Camera.fieldOfView. It sees from 0.3 units in front of it on, however far, and as much across as the screen's shape gives. Draw.Mesh(vertices, indices, transform) draws a 3D mesh through it, placed in the world by transform, which float4x4.TRS(position, rotation, scale) makes:

tide
local singleton Scenery
{
    List<Vertex3> crate;
    List<int> faces;
    Grid2<Color> wood = Grid2(16, 16);
}

view Look(Viewer viewer)
{
    Draw.Clear(Color(0.5, 0.7, 1));
    Draw.SetCamera(viewer.position, viewer.rotation, 60);
}

// Once for each crate, and drawn for them all at once
view Crates(Crate crate, Scenery scenery)
{
    Draw.Mesh(scenery.crate, scenery.faces, float4x4.TRS(crate.position, crate.rotation, float3(1)), scenery.wood);
}
  • 3D meshes hide each other by how far they are, whatever order they're drawn in. Everything else draws over what's there, in order, through the camera it had before: a view can draw a 3D scene and then a HUD over it, with no camera call between. Draw.Clear clears what's in front too.
  • A see-through mesh blends with what's behind it, and still hides what's drawn behind it later, unless it's drawn with Draw.Depth(Depth.Read): draw see-through meshes last, the farthest first. See Transparency for the full recipe, and cutout holes for solid pixels with gaps between them.
  • A triangle is drawn from its front, the side its corners go clockwise on, and not from its back: see Which side draws. A face that's missing is usually a triangle wound the other way.
  • Textures, filters and wraps work as a mesh's do: Draw.Mesh(vertices, indices, transform, texture, filter, wrap).
  • There's no lighting built in: a pixel is its corners' colors times its texture's. A frag lights it your way, with normals in a vertex struct of your own, which instance.Normal gives as they are in the world.
  • A mesh needn't be made in code: asset Mesh<Vertex3> crate = "art/crate.glb"; is a model from a glTF, FBX or OBJ file, and Draw.Mesh(crate, transform) draws it, the mesh where its two lists would go. See Meshes.
  • Each frame starts with the 3D camera at the origin, looking along +z, with a field of view of 60 degrees.

A game's C draws 3D meshes too, with vertices of its own, like a model a library loaded: see Drawing from C.

A model with bones uses SkinnedMesh<V>. Draw.SkinnedMesh(model, transform) draws its authored rest pose, even without animation clips; pass a sampled Pose after the transform to animate it. See Skinned meshes and animation for playback, separate animation files, blending, procedural bone edits and sockets usable by both systems and views.

Primitives

The primitives draw solid colors through the current 3D camera, with depth and no lighting, as meshes do. Cube, cylinder and plane also take a rotation before the color:

tide
Draw.Cube(float3(0, 0, 5), float3(2, 1, 1), Color.red);
Draw.Sphere(float3(3, 0, 5), 1, Color.green);
Draw.Cylinder(float3(-3, 0, 5), 0.5, 2, Color.blue);
Draw.Plane(float3(0, -1, 5), float2(10, 10), quaternion.identity, Color.gray);

Nonpositive dimensions draw nothing. Geometry is cached by shape and color; consecutive instances of the same shape and color are drawn together. Use a mesh with a frag for textures or lighting.

They're solid: a cube, a sphere and a cylinder are drawn from outside, and a plane from above, its local +y. A plane tipped to face the camera is turned so that its top does: quaternion.AxisAngle(float3(1, 0, 0), -Math.PI / 2) stands one up to face a camera that looks along +z.

Everything below is a primitive: a plane, 81 cubes, a sphere and a cylinder, drawn again every frame through a camera that circles them. Drag to turn it yourself, and change its field of view with the slider.

Primitives through a 3D cameraLive Tide example, tap it to play
Open separately
tide
settings { title = "3D"; }

local scene Main { }

local singleton Orbit
{
    float yaw = 0.7;
    float pitch = 0.6;
    float fieldOfView = 50;
    float wave;
}

view Scenery(Devices devices, mut Orbit orbit)
{
    // Drag to look around. Left alone, the camera circles by itself.
    var pointer = devices.pointer;
    if (pointer.press.pressed)
    {
        orbit.yaw += pointer.delta.x * 0.01;
        orbit.pitch = Math.Clamp(orbit.pitch - pointer.delta.y * 0.01, 0.15, 1.4);
    }
    else orbit.yaw += Frame.dt * 0.25;
    orbit.wave += Frame.dt * 1.5;
    if (orbit.wave > Math.TAU) orbit.wave -= Math.TAU;

    // The camera looks along its rotation's +z, from 14 units behind the middle.
    var rotation = quaternion.Euler(float3(orbit.pitch, orbit.yaw, 0));
    Draw.Clear(Color(0.086, 0.086, 0.094));
    Draw.SetCamera(float3(0, 1, 0) - Math.Forward(rotation) * 14, rotation, orbit.fieldOfView);

    // 3D draws hide each other by depth, whatever order they come in.
    Draw.Plane(float3(0, 0, 0), float2(11, 11), Color(0.13, 0.13, 0.15));
    for (var x = -4; x <= 4; x++)
    {
        for (var z = -4; z <= 4; z++)
        {
            var height = 1.3 + Math.Sin(x * 0.8 + orbit.wave) * Math.Cos(z * 0.8) * 1.1;
            var shade = height * 0.3;
            Draw.Cube(float3(x, height * 0.5, z), float3(0.92, height, 0.92), Color(0.3 + shade, 0.22 + shade * 0.75, 0.08 + shade * 0.08));
        }
    }
    Draw.Sphere(float3(0, 4.2, 0), 0.7, Color(0.18, 0.83, 0.75));
    Draw.Cylinder(float3(0, 3, 0), 0.08, 1.6, Color.white);

    Draw.SetScreen();
    Draw.Text("Drag to turn the camera", float2(16, 14), 16, Color.white);
    GUI.SetTheme(GUITheme { radius = 6 });
    GUI.Slider(Rect(16, Screen.height - 48, Screen.width - 32, 32), "Field of view", orbit.fieldOfView, 25, 90);
}

Depth

3D meshes hide each other by how far they are, whatever order they're drawn in: each one is hidden behind what's nearer, and hides what's drawn behind it later. That's Depth.ReadWrite, which every frame starts with, and it's right for everything solid. Draw.Depth says otherwise:

Mode What a 3D mesh does For
Depth.ReadWrite Is hidden behind what's nearer, and hides what's drawn behind it later Everything solid: the default
Depth.Read Is hidden behind what's nearer, and hides nothing What's see-through: glass, smoke, a shadow or a mark lying on a floor
Depth.None Draws over whatever is there, and hides nothing A marker that shows through walls, a sky drawn before everything

A see-through mesh that writes depth hides what's behind it, though that should show through it. Drawn with Depth.Read, it's still behind the crate in front of it, and whatever is see-through behind it shows:

tide
// What's solid, in any order
view Crates(Crate crate, Scenery scenery)
{
    Draw.Mesh(scenery.crate, scenery.faces, float4x4.TRS(crate.position, crate.rotation, float3(1)));
}

// Then what's see-through: behind the crates in front of it, and hiding nothing
view Windows(Pane pane, Scenery scenery)
{
    Draw.Depth(Depth.Read)
    {
        Draw.Mesh(scenery.glass, scenery.faces, float4x4.TRS(pane.position, pane.rotation, float3(1)));
    }
}
  • Draw what only reads depth after what's solid. A mesh is hidden behind what was already drawn in front of it, so the panes come after the crates: views run in the order they're declared, or in the order of their stages.
  • Among themselves, they go over each other in the order they're drawn, like 2D shapes do. Where that shows, draw the farthest first. Shadows that only darken look the same in any order.
  • Something flat on a surface is drawn a little off it. A decal exactly where the floor is has the floor's own depth, and which of the two wins changes from pixel to pixel. A few millimeters above it is enough, and every decal on the floor can be at that one height, since they don't hide each other.
  • It's for 3D meshes of every kind: Draw.Mesh, the primitives, meshes through a frag or a vert and Draw.Instanced. Instances drawn in a row with the same setting are still one draw.
  • A render target has its own, as it has its own camera: target.Depth(Depth.Read) { target.Mesh(...); }. Draw.Depth is the window's, and doesn't reach a target's meshes.
  • Nothing else has depth: shapes, text and 2D meshes draw over what's there, in order, whatever is set. Draw.Clear clears depth whatever is set, and Draw.Blit leaves it as it is.
  • Draw.ClearDepth() sets depth back and nothing else. The picture stays, and the 3D meshes drawn after it are in front of everything drawn before, however near that was: a first-person tool drawn over the world never goes into a wall. It fills the screen, or what a clip leaves of it, and leaves stencil alone. A render target has it too: target.ClearDepth().

Which side draws

A triangle of a 3D mesh has a front and a back. Its front is the side its corners go clockwise on, in the order its indices list them, and a triangle seen from the back isn't drawn. For a closed solid mesh, this avoids shading faces turned away from the camera, which are hidden behind its near faces anyway.

tide
// A square facing a camera that looks along +z: from there, its corners go clockwise
scenery.card = [Vertex3 { position = float3(-1, 1, 0) }, Vertex3 { position = float3(1, 1, 0) },
                Vertex3 { position = float3(1, -1, 0) }, Vertex3 { position = float3(-1, -1, 0) }];
scenery.faces = [0, 1, 2, 0, 2, 3];

Tip

A face that's missing is usually a triangle wound the other way. Swap two of its three indices (0, 1, 2 becomes 0, 2, 1) and it's back. A whole mesh that looks hollow, showing its far side's inside, has every triangle the wrong way round.

That's Cull.Back, which every frame starts with. Draw.Cull says otherwise:

Side Which triangles draw For
Cull.Back The ones seen from the front Everything solid: the default
Cull.Front The ones seen from the back The inside of a shell: a room seen from within, a sky dome
Cull.None All of them What's flat or see-through: a leaf, a flag, a pane of glass
tide
// A leaf is one square, seen from either side
view Leaves(Leaf leaf, Scenery scenery)
{
    Draw.Cull(Cull.None)
    {
        Draw.Mesh(scenery.card, scenery.faces, float4x4.TRS(leaf.position, leaf.rotation, float3(1)));
    }
}
  • A triangle's front stays its front, whatever it's drawn through. A transform that mirrors a mesh (a scale of -1 along one axis, to make a left hand of a right one) turns its triangles round on the screen, and the engine accounts for it: the mesh's own transform, Draw.Transform and a camera that mirrors the world, like one placed for a reflection. Mirrored and unmirrored meshes drawn in a row are drawn apart, a draw each.
  • A transform that flattens a mesh leaves it no front. Squashed onto a plane, as for a shadow lying on a floor, the triangles of its near side and of its far side land on each other, wound opposite ways. Draw it inside Draw.Cull(Cull.None).
  • See-through and closed, a mesh shows its near side alone. For a glass ball's far side to show through its near one, draw it inside Cull.None, or twice: inside Cull.Front first, then as usual.
  • A vert that turns triangles round is yours to draw for. The engine goes by the transforms it's given: one that mirrors its mesh itself (vertex.position.x = -vertex.position.x) is drawn inside Cull.Front.
  • The primitives are solid, their fronts outside. A plane has one side, its top: seen from below it isn't drawn, unless it's inside Cull.None.
  • It's for 3D meshes of every kind: Draw.Mesh, the primitives, meshes through a frag or a vert and Draw.Instanced. Instances drawn in a row with the same setting are still one draw.
  • A render target has its own, as it has its own depth setting: target.Cull(Cull.None) { target.Mesh(...); }. A triangle's front is the same side there.
  • Nothing in 2D has a back: a 2D Draw.Mesh draws its triangles whichever way they're wound, whatever is set.

Frustum culling

Tide skips ordinary 3D mesh instances whose bounds are completely outside the destination's camera. This is automatic for primitives, meshes from lists or assets, and meshes with a fragment shader alone. Bounds are cached with the geometry, and each instance is tested separately. A box crossing the camera's boundary stays drawn.

Skinning and custom vertex shaders can move vertices beyond the original geometry. Those draws stay enabled unless you supply a conservative box containing the resulting vertices, in mesh-local space before the instance transform and Draw.Transform:

tide
Draw.Bounds(float3(0, 1, 0), float3(3, 4, 3))
{
    Draw.SkinnedMesh(art.character, transform, pose);
}

size is the box's full width, height and depth. Include all animation and shader displacement. Draw.SetBounds(center, size) sets it for later draws; Draw.SetBounds() restores automatic bounds. Draw.Bounds() { ... } restores them within a block. Invalid bounds keep the draw.

Draw.FrustumCulling(false) { ... } disables the test within a block. Draw.SetFrustumCulling(false) disables it for later draws, and Draw.SetFrustumCulling() enables it again. Every frame starts with culling enabled and automatic bounds. These settings affect 3D meshes alone; Draw.Cull still controls triangle faces separately.

Every render target uses its own settings and camera, including shadow passes. Draw.LODCamera changes only detail selection. An object outside the player's camera can still draw into a shadow map.

Culling skips drawing, never views, operand evaluation, match simulation or network updates. Animation sampling and skin-palette capture can still run before a draw is rejected. Games with large worlds can additionally select nearby chunks before issuing draws to save that earlier work. This does not test whether another object hides a mesh.

Big scenes

Draw.Instanced(vertices, indices, transforms) draws the mesh at every float4x4 in a list, without an entity for each placement; a mesh from a file goes where the two lists do, Draw.Instanced(tree, transforms). It can also take a list of your own instance struct, with per-item fields read by a vert or frag: see Instances from a list.

The lists a 3D mesh is drawn from are kept on the GPU while frames draw them. An unchanged list needs no upload each frame, so keep meshes in fields rather than making them each frame. The GPU still processes its vertices and pixels: reuse avoids uploading the geometry, rather than making a large mesh free to render. A list that changed is sent again the next frame.

A model from a file that's drawn many times, near and far, can have simpler versions made for where it's small: [LOD] before its asset, and each instance then draws the simplest one that looks the same there. See Levels of detail.

A mesh baked for an entity of the match goes in local fields of its component: this machine's, which the view bakes once and draws from, and which the match never carries. A list in an ordinary field is part of the match, so every player who joins downloads it, though no system reads it.

tide
component Rock
{
    float3 position;
    int seed;
    local List<Vertex3> corners;
    local List<int> faces;
}

view Rocks(mut Rock rock)
{
    if (rock.corners.count == 0) Carve(rock); // Fills corners and faces from the seed, once
    Draw.Mesh(rock.corners, rock.faces, float4x4.Translate(rock.position));
}

A mesh many entities share, like the crate above, is one list in a singleton, or a local one.

A mesh drawn again and again in a row, from the same lists, with the same texture, filter and wrap, is drawn all at once: a view that runs for each of thousands of entities and draws one mesh costs the GPU one draw. Something drawn between them, like another mesh or a camera, starts another.