Drawing in 3D
Meshes have a 3D version, which goes through a camera of its own. A corner is a Vertex3:
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:
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.Clearclears 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
fraglights it your way, with normals in a vertex struct of your own, whichinstance.Normalgives 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, andDraw.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:
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.
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:
// 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 andDraw.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.Depthis 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.Clearclears depth whatever is set, andDraw.Blitleaves 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.
// 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 |
// 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
-1along 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.Transformand 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: insideCull.Frontfirst, then as usual. - A
vertthat 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 insideCull.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 andDraw.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.Meshdraws 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:
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.
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.