Draw settings
A setting changes how everything drawn after it comes out: how it blends, what tints it, what it does with depth and stencil, which side of it shows, where it lands. Each has two forms, and its name says which.
The plain name wraps a block. It's set for the draws in the block, and when the block ends, what it found is back:
Draw.Blend(Blend.Add)
{
Draw.Circle(spark.position, 2, spark.color);
}The Set name is a statement. It stays for the draws that follow, in this view and the views after it, until it's set again or the frame ends, which is how a camera is set once for a frame:
view Follow(Player player, with ownership)
{
Draw.SetCamera(player.position, 10);
}| Around a block | For the draws that follow | What it sets |
|---|---|---|
Draw.Blend(mode) |
Draw.SetBlend(mode) |
How colors go over what's there (see Blending) |
Draw.Tint(color) |
Draw.SetTint(color) |
A color everything is multiplied by (see Tints) |
Draw.Stencil(test, value), Draw.Stencil(test, value, operation) |
Draw.SetStencil(test, value), Draw.SetStencil(test, value, operation) |
The test draws pass, against a number kept for each pixel, and what becomes of that number where they draw (see Stencil) |
Draw.Transform(...), Draw.Transform(transform) |
Draw.SetTransform(...), Draw.SetTransform(transform) |
Where draws land: moved, turned, scaled (see Transforms) |
Draw.Clip(rect) |
Draw.SetClip(rect) |
The rectangle draws are kept inside (see Clipping) |
Draw.Camera(center, size), Draw.Camera(...) |
Draw.SetCamera(center, size), Draw.SetCamera(...) |
The camera |
Draw.Screen() |
Draw.SetScreen() |
The screen's pixels in the camera's place (see On the screen) |
Draw.Depth(mode) |
Draw.SetDepth(mode) |
What 3D meshes do with depth: whether they're hidden behind what's nearer, and whether they hide what's behind them (see Depth) |
Draw.Cull(side) |
Draw.SetCull(side) |
Which side of a 3D mesh's triangles isn't drawn: the back, unless this says otherwise (see Which side draws) |
Draw.FrustumCulling(enabled) |
Draw.SetFrustumCulling(enabled) |
Whether to skip 3D instances wholly outside the camera; enabled by default (see Frustum culling) |
Draw.Bounds(center, size), Draw.Bounds() |
Draw.SetBounds(center, size), Draw.SetBounds() |
Conservative mesh-local culling bounds, with full size; no arguments restores automatic bounds |
Draw.LOD(level), Draw.LOD() |
Draw.SetLOD(level), Draw.SetLOD() |
Which level of detail the meshes marked [LOD] draw at, in place of the one each picks; with no level, each picks again (see Levels of detail) |
Draw.LODCamera(...), Draw.LODCamera() |
Draw.SetLODCamera(...), Draw.SetLODCamera() |
The camera those meshes pick their level through, in place of the one they're drawn through; with nothing, their own again |
Every frame starts from the defaults, so a Set never outlives its frame, and the GUI and the stats are never drawn with what a game set. A Set with no arguments puts the default back early: Draw.SetTint(), Draw.SetStencil(), Draw.SetClip(), Draw.SetDepth(), Draw.SetCull(), Draw.SetFrustumCulling(), Draw.SetBounds(), Draw.SetLOD(), Draw.SetLODCamera(); Draw.SetCamera() puts both cameras back, the 2D and the 3D, and Draw.SetTransform() both transforms. Bounds and the two for levels of detail have a block form with no arguments too, which does the same around some draws.
Writing one form as the other is a compile error that names the right one: Draw.Blend(Blend.Add); with no block says to write Draw.SetBlend, and Draw.SetBlend(Blend.Add) { } says Draw.Blend. Your editor offers the rename.
A block that's left early still puts back what it found, as a GUI container still closes:
Draw.Tint(Color(1, 1, 1, 0.5))
{
if (!card.shown) return; // The tint is gone, here too
DrawCard(card);
}Every setting belongs to the destination it's called on. Draw's are the window's, and a render target has its own, by the same names (target.Blend(Blend.Add) { ... }, target.Clip(rect) { ... }, target.SetCamera(...), target.Depth(mode) { ... }): a blend or a tint set for the window doesn't reach a target's draws, and a target's doesn't reach the window's. Depth and cull are for 3D meshes alone: around 2D draws they change nothing, and that's no error.
Info
What a Set with no arguments does, how one setting nests in another, Draw.Depth's name and its three modes', Cull's three names and which way round a triangle's front goes, and how Draw.Stencil is written, its tests' and operations' names and each of its rules, are provisional: they're how it works today, and may still change.
Several at once
Calls that take a block can be listed with commas before one block, so settings don't nest three deep:
Draw.Blend(Blend.Add),
Draw.Tint(Color(1, 1, 1, shown)),
Draw.Clip(panel)
{
DrawGlow(panel);
}It means the same as each inside the one before. That's the language's, not Draw's: the GUI's containers and your own functions that take a block go in the same list.
Blending
Blend.Alpha is the default: a color goes over what's there by its alpha. The others:
| Mode | What it does | For |
|---|---|---|
Blend.Add |
Adds the color to what's there, as light adds | Sparks, trails, glows, fire |
Blend.Multiply |
Multiplies what's there by the color: white changes nothing, black makes black | Shadows, stains, tinted glass |
Blend.Replace |
Puts the color and its alpha in place of what's there | Writing into a render target exactly |
In each, a color's alpha is how much of it there is, so what's clear changes nothing: a shape's smooth edge and the gaps between a text's strokes stay clean when they're added or multiplied.
view Trails(Bullet bullet)
{
Draw.Blend(Blend.Add)
{
// Where trails cross, they're brighter
Draw.Line(bullet.position - bullet.velocity * 0.05, bullet.position, 0.2, Color(1, 0.6, 0.2, 0.6));
}
}Draw.Clear, and a target's Clear, fill with their color whatever the blend, and Draw.Blit takes a blend of its own (Draw.Blit(source, destination, Blend.Add, frag)).
Alpha
A color's alpha is how much of it there is, and you write colors as they are everywhere: Color(1, 0, 0, 0.5) is red, half clear, in a draw, a tint, a grid's cell, a PNG and a frag. There's nothing to multiply yourself and no second kind of blend to pick.
See Transparency for drawing something see-through, fading it, and cutting holes in a 3D mesh.
What that buys:
- No fringes. A sprite's clear pixels have a color too in its file, often black or white, which never shows. Scaled or turned with
Filter.Bilinear, a pixel at the sprite's edge is blended with its clear neighbor, and the neighbor counts for none of the color: a white sprite over a white wall has no gray rim, whatever the file kept in its clear pixels. - Layers. What's drawn into a cleared render target,
target.Clear(Color.clear), and then drawn from it withtarget.colorlooks as it would drawn straight onto the window: a HUD, a minimap or a trail kept in a target goes over the game as it is.
A few things follow from how that's kept, which is each color times its alpha, in textures and in targets:
- A pixel with no alpha has no color. A frag that samples one gets
Color(0, 0, 0, 0), whatever the file or the cell said its color was. An image whose alpha is something other than how clear it is (a mask beside unrelated colors) would lose its colors where the alpha is 0: mark it[Raw], and a frag reads its numbers as the file has them. Blend.Replacewith an alpha under 1 leaves the window, which has no alpha of its own, darker by that much, as if over black. Into a target it's exact: drawn from the target later, it's the color and the alpha that were written.Blend.Addraises the alpha that's there towards 1 by its own, and never past it, so light added into a clear target shows when the target is drawn over something.- A dim color that's nearly clear keeps few of its 256 steps in an image of bytes. It doesn't show once it's blended; a
Format.Rgba16ftarget keeps them all.
Tints
A tint multiplies the color of everything drawn inside, alpha too. Color(1, 1, 1, 0.5) fades it to half, with no alpha to pass to every call:
view Menu(MenuState menu)
{
Draw.Screen(),
Draw.Tint(Color(1, 1, 1, menu.shown))
{
Draw.RoundRect(float2(200, 120), float2(320, 160), 12, Color(0.1, 0.1, 0.15));
Draw.Text("Paused", float2(160, 80), 32, Color.white);
}
}A tint in a tint's block multiplies it: a faded menu's faded button is fainter than either.
Stencil
Beside its color and its depth, every pixel has a whole number from 0 to 255, its stencil. Nothing shows it. Draws store numbers there, and other draws are drawn only where the number passes a test, which is how one thing is cut to the shape of another: a view through a porthole, a map inside a round frame, a mirror that shows its reflection only where the glass is, an outline around whatever is selected.
Draw.Stencil(test, value, operation) says both things for the draws in its block: the test they pass to be drawn at a pixel, their value against the number stored there, and what becomes of the stored number where they are drawn. It usually takes two steps. Something stores a number where it is, then something else is drawn where that number is:
view Porthole(Ship ship)
{
// The glass stores 1 where it is. It's drawn clear: it's there for what it stores
Draw.Stencil(StencilTest.Always, 1, StencilOp.Replace)
{
Draw.Circle(ship.window, 3, Color.clear);
}
// The sea is drawn where the number is 1, and nowhere else
Draw.Stencil(StencilTest.Equal, 1)
{
DrawSea(ship);
}
}Move the lens below. The wall is drawn where the lens didn't store its number, and the pipes where it did:
settings { title = "Stencil"; }
local scene Main { }
local singleton Lens
{
float2 at = float2(188, 170);
float radius = 70;
bool outside; // Show what's behind everywhere but in the lens
float time;
}
// The wall: rows of bricks, each row half a brick along from the last.
void Wall()
{
Draw.Rect(float2(Screen.width, Screen.height) * 0.5, float2(Screen.width, Screen.height), Color(0.24, 0.25, 0.27));
for (var row = 0; row < 17; row++)
{
for (var column = -1; column < 8; column++)
{
var corner = float2(column * 56 + (row % 2) * 28, row * 24);
Draw.RoundRect(corner + float2(28, 12), float2(52, 20), 3, Color(0.13, 0.13, 0.15));
}
}
}
// What's behind it: pipes, and what flows through them.
void Pipes(float time)
{
Draw.Rect(float2(Screen.width, Screen.height) * 0.5, float2(Screen.width, Screen.height), Color(0.05, 0.05, 0.06));
for (var pipe = 0; pipe < 4; pipe++)
{
var y = 60 + pipe * 80;
var color = pipe % 2 == 0 ? Color(0.18, 0.83, 0.75) : Color(0.98, 0.44, 0.52);
Draw.Line(float2(0, y), float2(Screen.width, y), 18, Color(0.24, 0.25, 0.27));
for (var drop = 0; drop < 6; drop++)
{
var along = Math.Frac(time * (0.12 + pipe * 0.03) + drop / 6.0);
var x = pipe % 2 == 0 ? along * Screen.width : (1 - along) * Screen.width;
Draw.Circle(float2(x, y), 6, color);
}
}
}
view Show(mut Lens lens, Devices devices)
{
Draw.Clear(Color(0.086, 0.086, 0.094));
Draw.SetScreen();
lens.time += Frame.dt;
var pointer = Screen.Flip(devices.pointer.position);
if (pointer.y < Screen.height - 64) lens.at = pointer;
// Every pixel has a number, 0 after a clear. The lens stores 1 where it is.
// It's drawn clear, so nothing of it shows: it's there for what it stores.
Draw.Stencil(StencilTest.Always, 1, StencilOp.Replace)
{
Draw.Circle(lens.at, lens.radius, Color.clear);
}
// The wall is drawn where the number isn't the lens's...
Draw.Stencil(lens.outside ? StencilTest.Equal : StencilTest.NotEqual, 1)
{
Wall();
}
// ...and the pipes where it is.
Draw.Stencil(lens.outside ? StencilTest.NotEqual : StencilTest.Equal, 1)
{
Pipes(lens.time);
}
// With no test again: the lens's rim, over both.
Draw.WireCircle(lens.at, lens.radius, 3, Color(0.98, 0.75, 0.14));
GUI.SetTheme(GUITheme { radius = 6 });
GUILayout.Area(Rect(12, Screen.height - 52, Screen.width - 24, 40))
{
GUILayout.Horizontal()
{
GUILayout.Slider("Lens", lens.radius, 20, 140);
GUILayout.Toggle("Outside", lens.outside);
}
}
}The test is between the draw's value, on the left, and the stored number:
| Test | A pixel is drawn where |
|---|---|
StencilTest.Always |
Everywhere: no test, which is what every frame starts with |
StencilTest.Never |
Nowhere |
StencilTest.Equal |
The stored number is value |
StencilTest.NotEqual |
It's any other |
StencilTest.Less |
value is less than the stored number |
StencilTest.LessEqual |
value is less than it, or the same |
StencilTest.Greater |
value is greater than the stored number |
StencilTest.GreaterEqual |
value is greater than it, or the same |
The operation says what becomes of the stored number at each pixel the draw is drawn at. Without one it stays as it is, so Draw.Stencil(test, value) only tests:
| Operation | The stored number becomes |
|---|---|
StencilOp.Keep |
What it was: the draw only tests |
StencilOp.Replace |
value |
StencilOp.Increment |
One more, 255 at the most |
StencilOp.Decrement |
One less, 0 at the least |
StencilOp.Zero |
0 |
StencilOp.Invert |
255 less it: each of its 8 bits the other way |
- Every pixel's number is 0 when a frame starts, in the window. A render target keeps its numbers from frame to frame, as it keeps its colors, until it's cleared.
- A draw stores where it's drawn, whatever its color.
Color.clearstores and shows nothing, which is how to lay a mask down without painting anything. That's so under every blend butBlend.Replace, which puts the clear color there. - Where it's drawn is every pixel it touches: all of a rect, a circle out to its edge, a smooth shape to the pixels its edge only partly covers, text where its glyphs are (not the boxes around them), and all of a mesh's triangles, whatever its texture's alpha is there. For a mask the shape of a picture, draw it through a frag that leaves out what's clear with
this.Discard(): a pixel a frag discards stores nothing. - It's cut along pixels. A pixel is in a mask or it isn't, so what's drawn where a mask is has a stepped edge where the mask's shape is round, however smooth the shape itself would be drawn. Draw a rim over it with no test, as the example does, where that shows.
- A draw that fails the test changes nothing, and neither does a 3D mesh where it's hidden behind what's nearer: the number changes only where the draw is drawn.
- It's for everything that draws: shapes, text, meshes, 3D meshes and the primitives, with a frag or without. Draws in a row under the same stencil are still one draw.
Draw.Clear, and a target'sClear, set every number back to 0, within the clip, whatever is set, and are tested against nothing.Draw.Blitneither tests nor stores, whatever is set.- A render target has its own numbers, and its own setting:
target.Stencil(StencilTest.Equal, 1) { target.Rect(...); }.Draw.Stencilis the window's, and doesn't reach what's drawn into a target; a target's doesn't reach the window. valueis anyint. Under 0 it's 0, and over 255 it's 255. The test and the operation are values too:Draw.Stencil(inside ? StencilTest.Equal : StencilTest.NotEqual, 1).- A block's stencil replaces the one it found, and puts it back when it ends.
Draw.SetStencil()puts back none: every pixel passes, and nothing is stored.
Counting is the other use. Each of these adds one where it's drawn, so afterwards a pixel's number is how many of them cover it, and a draw under StencilTest.Less with a value of 1 is drawn where more than one does:
Draw.Stencil(StencilTest.Always, 0, StencilOp.Increment)
{
foreach (var zone in zones.list) Draw.Circle(zone.center, zone.radius, Color.clear);
}
Draw.Stencil(StencilTest.Less, 1) // Where 1 is less than the count: two zones or more
{
Draw.Rect(zones.middle, zones.size, Color(1, 0.2, 0.2, 0.5));
}What a GPU's stencil has beyond this isn't here: a setting for each side of a triangle, one for what happens where a mesh is hidden, and masks over the number's bits.
Transforms
A transform moves, turns and scales everything drawn inside, in the units the calls are in. Draw.Transform takes an offset, then a rotation in radians, then a scale, each optional after the first:
view Cards(Card card)
{
// Scaled about the card's middle, then turned, then moved there
Draw.Transform(card.position, card.tilt, 1 + card.lift * 0.1)
{
Draw.RoundRect(float2(0, 0), float2(3, 4), 0.3, Color.white);
Draw.Text(card.name, float2(-1.2, 1.6), 0.5, Color.black);
}
}What's inside is drawn around (0, 0), and the transform puts it in the world: scaled, then turned (from +x towards +y, as smooth shapes turn), then moved. The scale is a number, or a float2 to stretch more one way than the other, and Draw.Transform(matrix) takes any float3x3 instead. Draw.Transform(transform) does the same for 3D meshes with a float4x4. A transform in a transform's block is inside it, so a card's badge can have one of its own.
What's drawn through one:
- Shapes stay shapes, with their smooth edges: moved, turned and scaled, exactly. Stretched, a circle is the ellipse it becomes, and a rect follows its own sides; a skew is approximated. Rects that are only moved and scaled still fill whole pixels, so tiles under a transform leave no seams.
- Meshes go through any transform as they are.
- Text that's moved and scaled as much each way is text of that size, as sharp as any. Turned or stretched, it has no pixels to be fitted to, so it's a little softer than upright text: fine for a tilting card, and worth avoiding for text people read at length.
- A clip made inside one is the upright rectangle around what its
Rectbecomes: clips don't turn.
Draw.WorldToScreen and the other conversions are the camera's alone, whatever the transform. Draw.MeasureText answers in the units the transform takes, so inside one, text measures as it's drawn.
Clipping
Draw.Clip(rect) { ... } keeps what's drawn in its block inside a rectangle: meshes, shapes, text, and Draw.Clear, which then fills the rectangle.
view Panel()
{
Draw.Screen(),
Draw.Clip(Rect(20, 20, 200, 100))
{
Draw.Clear(Color.black); // The panel only
Draw.Text("A line too long for the panel is cut off at its edge", float2(28, 28), 18, Color.white);
}
}The Rect goes from (x, y) to (x + width, y + height), in the units the calls are in: the world's under a camera, where y is up, and pixels on the screen. It stays where it is on the screen when the camera changes afterwards.
A clip in a clip's block only narrows it: what draws is inside both, which is what a list in a panel wants. Draw.SetClip(rect) is the other form, for the draws that follow, and sets the rectangle outright; Draw.SetClip() draws everywhere again. Each frame starts with no clip, and a view's clip never clips the GUI.
Transparency
Set a color's alpha, its fourth number, to the opacity you want: 0 is invisible, 0.5 is half opaque, and 1 is solid. Color(r, g, b) is solid. The default Blend.Alpha blends the color over what's already drawn, so shapes and text need no extra setting:
view Overlay()
{
Draw.Screen()
{
Draw.Rect(float2(160, 100), float2(240, 80), Color(0.18, 0.83, 0.75, 0.5));
Draw.Text("Half opaque", float2(40, 60), 20, Color(1, 1, 1, 0.5));
}
}Draw the background first, then what goes over it. This is also how a sprite with transparent pixels works: give Draw.Mesh its PNG or texture, and the pixel's alpha supplies its opacity. A mesh's vertex colors multiply that alpha, and a frag controls it through its out Color.
To fade a whole set of draws, wrap them in Draw.Tint(Color(1, 1, 1, opacity)). This keeps their colors and multiplies each draw's alpha by opacity. Each draw still blends separately: if overlapping elements should fade as one finished layer, draw them into a render target cleared with Color.clear, then tint the draw of target.color. Colors stay as you wrote them; Tide handles premultiplied alpha internally.
Transparent 3D
A 3D mesh also has depth. For glass, smoke and other translucent surfaces:
- Draw solid geometry first with the default
Depth.ReadWrite. - Draw translucent geometry inside
Draw.Depth(Depth.Read) { ... }, with alpha below 1. It tests against the solid geometry's depth and writes no depth itself. - Draw the translucent surfaces farthest first, then the nearer ones, so they blend over what's behind them.
Change Opacity below. The amber cube is behind the panes and shows through them; the solid pillar in front hides them. The camera stays fixed, so the rose pane is always farther away and is drawn first. The bar above the scene uses the same opacity on a 2D shape.
settings { title = "Transparency"; }
local scene Main { }
local singleton Glass
{
float opacity = 0.5;
List<Vertex3> pane = [
Vertex3 { position = float3(-1.3, -1.4, 0) },
Vertex3 { position = float3(-1.3, 0.5, 0) },
Vertex3 { position = float3(1.3, 0.5, 0) },
Vertex3 { position = float3(1.3, -1.4, 0) }
];
List<int> faces = [0, 1, 2, 0, 2, 3];
}
view Solid()
{
Draw.Clear(Color(0.086, 0.086, 0.094));
Draw.SetCamera(float3(0, 0, -7), quaternion.identity, 50);
// Solid geometry comes first, with the default Depth.ReadWrite.
Draw.Cube(float3(-0.5, -0.35, 2), float3(1.5),
quaternion.Euler(float3(0, 0.35, 0)), Color(0.98, 0.75, 0.14));
Draw.Cube(float3(0.8, -0.45, -1), float3(0.5, 1.8, 0.5), Color(0.64, 0.64, 0.68));
}
view Panes(Glass glass)
{
// The camera is fixed: z = 1 is farther away than z = 0.
// Read opaque depth, but let later translucent draws show through.
Draw.Depth(Depth.Read)
{
Draw.Tint(Color(0.98, 0.44, 0.52, glass.opacity))
{
Draw.Mesh(glass.pane, glass.faces, float4x4.Translate(float3(-0.2, 0.15, 1)));
}
Draw.Tint(Color(0.18, 0.83, 0.75, glass.opacity))
{
Draw.Mesh(glass.pane, glass.faces, float4x4.Translate(float3(0.2, 0, 0)));
}
}
}
view Overlay(mut Glass glass)
{
Draw.SetScreen();
Draw.Text("2D: alpha blends over the background", float2(16, 16), 16, Color.white);
var width = (Screen.width - 32) / 8;
for (var i = 0; i < 8; i++)
{
var shade = i % 2 == 0 ? 0.2 : 0.35;
Draw.Rect(float2(16 + (i + 0.5) * width, 85), float2(width, 54), Color(shade, shade, shade));
}
Draw.Rect(float2(Screen.width * 0.5, 85), float2(Screen.width - 64, 36),
Color(0.98, 0.44, 0.52, glass.opacity));
Draw.Text("3D: solids, then panes from far to near", float2(16, 132), 16, Color.white);
Draw.Text("The pillar hides both panes", float2(16, Screen.height - 80), 14, Color(0.64, 0.64, 0.68));
GUI.SetTheme(GUITheme { radius = 6 });
GUI.Slider(Rect(16, Screen.height - 48, Screen.width - 32, 32), "Opacity", glass.opacity, 0, 1);
}Tide draws in the order your views and calls give it; it doesn't sort transparent entities or a mesh's triangles by distance. For a moving camera, gather your transparent objects in a local list, sort it from far to near for that camera, then draw them in that order. Intersecting transparent surfaces, or faces within one mesh, may need to be split and ordered too; sorting whole objects alone can't resolve every overlap.
- Alpha doesn't turn depth writes off. Even alpha 0 can hide later geometry under
Depth.ReadWrite. UseDepth.Readfor translucency, or discard the pixel for a hole. Depth.Nonealso stops testing depth, so the mesh shows through solid walls. UseDepth.Readwhen walls should hide it.- A thin pane has a back too. If it should show from either side, use
Draw.Cull(Cull.None). On a closed mesh that also draws the far faces, whose alpha blends with the near ones. - A target has its own settings: use
target.Depth(Depth.Read)andtarget.Tint(...)for its draws.Draw's settings reach the window alone.
Cutout holes
Leaves, a fence or a sprite standing in a 3D world often want solid pixels with holes between them. A frag can discard pixels below an alpha threshold and keep the rest opaque:
frag Cutout(Vertex3 vertex, out Color color, Texture image)
{
color = image.Sample(vertex.uv);
if (color.a < 0.5) this.Discard();
color.a = 1;
}
// Keep the default Depth.ReadWrite: only the surviving pixels hide what's behind.
Draw.Mesh(vertices, indices, transform, texture, Cutout);this.Discard() ends the frag and writes no color, depth or stencil for that pixel. The surviving pixels can use the usual opaque draw order. The threshold gives a hard edge; use the translucent recipe above when you want partial opacity at the edge instead.