ShapeBatch
ShapeBatch draws vector shapes without meshes, materials or a UI tree: discs, rings, polygons, rounded
rectangles, sectors, arcs, lines and polylines. Outlines, glows and dashes are measured in pixels, so they keep
their width at any distance or zoom. It works in 2D and 3D scenes and is in the
Stride.CommunityToolkit.Shapes package.
For a tour of every shape, see the ShapeBatch example. For method signatures, see the API reference.
Tip
New to ShapeBatch? The tutorial Build a simple HUD with ShapeBatch
goes through it in eight steps. This page is the reference.
When to use ShapeBatch
| Tool | Use it for | Not suited for |
|---|---|---|
| Stride UI | Menus, forms and anything with layout, focus and input routing | Vector shapes. It draws textured quads |
| Debug Shapes | Wireframes of colliders and meshes, one pixel wide | Anti-aliased or constant-width outlines |
| Mesh and material | Lit, textured objects in the scene | Outlines that keep a pixel width. The outline is geometry and shrinks with distance |
ShapeBatch |
Flat shapes with pixel-measured outlines: markers, HUD panels, gauges, selection rings, thick lines | Concave fills, text, layout |
flowchart TD
Q1{"Is it a menu, form or<br/>something with layout and focus?"} -->|yes| UI["Stride UI"]
Q1 -->|no| Q2{"A lit, textured object<br/>in the scene?"}
Q2 -->|yes| Mesh["Mesh + material"]
Q2 -->|no| Q3{"Wireframe of a collider<br/>or mesh, one pixel is fine?"}
Q3 -->|yes| DS["Debug Shapes"]
Q3 -->|no| Q4{"Flat, convex, and the outline<br/>must stay a constant pixel width?"}
Q4 -->|yes| SB["ShapeBatch<br/>(+ World Text for labels)"]
Q4 -->|no| Compose["Compose: several shapes,<br/>or a mesh with a ShapeBatch outline"]
How it works
Each shape is drawn as one quad. For every pixel of the quad, the pixel shader computes a signed distance to the shape's edge: negative inside, positive outside. The rest follows from that distance.
| Distance | Result |
|---|---|
| Inside by more than half the border width | Fill colour |
| Within half the border width of the edge, inside or outside | Border colour |
| Outside by more than half the border width | Transparent, anti-aliased over the last pixel |
The border width is compared in pixels, so it is constant on screen. Rings, sectors, glows, dashes and gradients are further functions of the same distance. A polyline is the distance to the nearest segment of a run, which gives round joins and caps.
flowchart LR
A["Your update loop<br/><i>shapes.DrawRing(...)</i>"] --> B["ShapeBatch<br/>one record per shape:<br/>plane, colours, style<br/>plus its points, any number"]
B --> C["Three structured buffers<br/>records, points and space points,<br/>uploaded once per frame"]
C --> D["Vertex shader<br/>one quad per instance,<br/>grown to fit the border and glow"]
D --> E["Fragment shader<br/>reads the record and its points,<br/>signed distance per pixel:<br/>fill, border, glow, dash, cut"]
E --> F["One instanced draw call<br/>however many shapes"]
The technique is based on the solid_polygon shader of the Box2D testbed.
Create a batch and draw
Create a batch once, after the graphics compositor exists. Draw from your update code every frame.
var shapes = game.AddShapeBatch(depthTest: true);
// In Update
shapes.Fill.Set(Color.Green, 0.95f);
shapes.DrawRectangle(center, axisX, axisY, new Vector2(120 * fuel, 8), Color.Green);
shapes.DrawDisc(new Vector3(0f, 0.02f, 1.5f), Vector3.UnitY, 3f, Color.OrangeRed);
AddShapeBatch parameter |
Description |
|---|---|
depthTest |
true: scene geometry can cover the shapes. false: the batch draws over everything |
fill |
A fill source for textured shapes. See Fill a shape with a texture or a shader |
afterPostEffects |
Draws the batch after the post-processing chain. See Draw after post effects |
ShapeBatch is immediate mode, like SpriteBatch. Shapes are submitted every frame, drawn once in submission
order, and discarded.
Draw state
Style properties are the batch's current state. Each draw call captures the state at the time of the call.
| Property | Default | Description |
|---|---|---|
BorderWidth |
3 | Outline width in pixels |
Fill |
The draw call's colour, alpha 1 | Fill colour and alpha |
Glow |
Off | Halo outside the outline: width in pixels, colour, Strength, Additive |
Dash |
Solid | Dash pattern in pixels |
Gradient |
None | A second fill colour and a direction |
Opacity |
1 | Multiplies the alpha of border, fill and glow |
DepthFade |
0 | Distance in world units over which a shape fades as it approaches scene geometry |
Textured |
true |
Whether the fill samples the batch's fill source, when the batch has one |
Screen |
false |
Draws in window pixels instead of world units |
Tag |
null |
Marks shapes for picking |
Outline and fill
Every shape has an outline and a fill. The colour passed to a draw call is the outline's colour.
- By default the fill is the same colour at full strength, so a draw call with one colour draws a solid shape.
- The border is centred on the shape's edge: half its width lies outside. With the default width of 3, a
shape is 3 pixels wider and taller than its size. Set
BorderWidth = 0for a shape of exactly its size. Fill.Set(colour)gives the fill a colour of its own, such as a dark panel inside a bright border.Fill.Alpha = 0leaves only the outline.Fill.Alpha = ShapeFill.TestbedAlphawith no fill colour gives the look of the Box2D testbed: a bright outline around a dimmed, see-through inside.Box2DDebugDrawdraws this way.
Note
State persists until you change it. A method that sets Textured, Screen or Tag for its own shapes must
restore the value afterwards.
Glow
The alpha of the glow colour sets how strong the glow is at the outline. At full alpha the glow reads as a thicker stroke. At 30 to 40 percent it reads as light around the stroke.
Glow.Strength(0 to 1) sets the strength when the glow uses each shape's own outline colour.Glow.Additiveadds the glow to the scene instead of blending over it.
shapes.Glow.Set(10f);
shapes.Glow.Strength = 0.35f;
shapes.Glow.Additive = true;
Plane modes
A shape is flat and lies in a plane. The draw method sets how the plane is oriented: a method that takes a normal, two axes or a rotation draws Fixed; DrawBillboard, DrawBillboardCircle, DrawPixelDisc and DrawPixelRing draw Screen; DrawLine, DrawPixelLine and DrawWireBox with Vector3 points draw Axial.
PlaneMode |
Plane | Use |
|---|---|---|
Fixed |
The two axes you pass | Discs on the ground, decals, panels on a wall |
Screen |
Faces the camera | Billboards that keep their shape from any angle |
Axial |
Keeps your X axis and turns about it to face the camera | A capsule between two points: a thick 3D line with round ends |
Pixel-measured widths are correct under perspective and orthographic cameras.
Space strokes
DrawPolyline and DrawPixelPolyline accept Vector3 points. A space stroke has no plane. It is measured on
screen and faces the camera from every angle.
DrawPolylineuses a width in world units, which narrows with distance.DrawPixelPolylineuses a constant width in pixels.- Each fragment is depth-tested at its own point, so in a depth-tested batch a stroke passes behind and in front of geometry.
Depth fade
DepthFade fades a shape over the given distance, in world units, as it approaches the scene geometry behind
it. A marker on the ground blends into it instead of ending in a hard line. A value of 0 keeps the hard cut.
- In a depth-tested batch, the depth test still removes fragments behind a surface.
- In an overlay batch, fragments behind a surface fade out.
- The fade needs the resolved depth texture of the forward renderer. Without it, shapes keep the hard cut.
- The distance is in world units under a perspective camera. Under an orthographic camera the fade is much stronger than the value asks for.
Fill a shape with a texture or a shader
ShapeBatch.FillSource takes a Stride material node (IComputeColor): a texture with scale, offset and
address modes, a combination of nodes, or a shader class. Shapes drawn while Textured is true multiply their
fill by the sample.
var pictures = game.AddShapeBatch(depthTest: true);
pictures.FillWith(texture);
var stripes = game.AddShapeBatch(depthTest: true);
var stripe = stripes.FillWith(texture, scale: new Vector2(4f, 1f), addressMode: TextureAddressMode.Wrap);
// In Update: scroll the texture
stripe.Offset = new Vector2(seconds * 0.25f, 0f);
Remarks:
- The texture coordinate runs from (0, 0) at the top left to (1, 1) across the shape's bounding box.
- The sample multiplies the fill colour. A white fill shows the texture unchanged.
- Border, glow and gradient work as for a plain fill.
- A batch has one fill source.
Texturedis per draw call, so one batch can hold textured and plain shapes. - The batch reads the node every frame. Changing a node's offset, scale or texture needs no rebuild.
- To show another camera's view in a shape, fill it with the texture from
game.AddRenderTextureCamera. See Render to texture.
Use a shader class as the fill
A shader class that derives from ComputeColor can be the fill source.
// examples/code-only/E11_3D_ShapeBatch_Gallery/Program.cs
var clock = new ComputeFloat4(Vector4.Zero);
game.AddShapeBatch(depthTest: true, fill: new ComputeShaderClassColor
{
MixinReference = "GalleryPlasma",
CompositionNodes = { ["Clock"] = clock },
});
// examples/code-only/E11_3D_ShapeBatch_Gallery/Effects/GalleryPlasma.sdsl
shader GalleryPlasma : ComputeColor, Texturing
{
compose ComputeColor Clock;
override float4 Compute()
{
float t = Clock.Compute().x;
float2 p = streams.TexCoord * 7.0;
...
}
};
Place the .sdsl file in the project's Effects folder.
Note
Global.Time is zero in a shape batch's effect. To animate a shader fill, compose a node into the shader
and set its value every frame, as Clock above.
Draw on the screen
Set Screen to true to draw in window pixels. The origin is the top left corner and Y points down.
shapes.DrawDisc(new Vector3(0f, 0.02f, 1.5f), Vector3.UnitY, 3f, Color.OrangeRed); // world
shapes.Screen = true;
var centre = shapes.ScreenSize * 0.5f;
shapes.DrawRing(centre, 22f, Color.White);
shapes.DrawArc(shapes.Anchor(ScreenAnchor.BottomLeft) + new Vector2(100f, -90f), 50f, -MathF.PI * 0.5f, MathF.Tau * health, Color.LimeGreen, width: 14f);
shapes.Screen = false;
| Member | Description |
|---|---|
Screen |
Per-draw state. World and screen shapes can share a batch |
ScreenSize |
The window size in the units the batch draws in |
Anchor(ScreenAnchor) |
The position of a window corner, the middle of an edge or the centre |
Viewport |
A rectangle that offsets screen shapes and sizes Anchor() |
Remarks:
- With
AutoScaleon, which is the default, units are the display's scaled pixels. - A positive angle turns clockwise on screen.
- Screen shapes are drawn at the near plane, in front of scene geometry.
- The
Screenproperty is different fromPlaneMode.Screen, which is a billboard in the world.
Draw after post effects
A batch in the scene is drawn into the HDR buffer before post processing. Auto exposure and tone mapping then
apply to its colours, so Color.White can appear grey.
var world = game.AddShapeBatch(depthTest: true);
var hud = game.AddShapeBatch(afterPostEffects: true);
| Batch | Colours | Depth test | Use |
|---|---|---|---|
| Default | Exposed and tone mapped with the scene | Optional | Markers and decals that belong to the scene |
afterPostEffects: true |
Written as given | None | HUD elements, crosshairs, label lines |
afterPostEffects requires the toolkit's UI stage in the compositor. Without it, the batch draws in the
transparent stage.
Pick shapes
A batch can report which of its shapes is under a screen position. Shapes are tested with the same distance functions that draw them.
shapes.Tag = station;
shapes.DrawRing(station.Origin, Vector3.UnitY, 5.8f, Color.White);
shapes.Tag = null;
// Next frame, in Update
if (shapes.TryPick(Input.MousePosition, out var hit) && hit.Tag is Station picked)
{
GoTo(picked);
}
| Member | Description |
|---|---|
Tag |
Per-draw state. Only shapes drawn with a tag can be picked |
TryPick(screenPosition, out hit, slackPixels) |
Returns the topmost tagged shape. The position is normalised, as Input.MousePosition gives it |
TryPick(ray, out hit, slackPixels) |
The same for a world ray. Screen shapes and space strokes are not tested |
PickAll(screenPosition, slackPixels) |
Returns every hit, front to back |
CanPick |
false until the batch has been drawn once |
ShapeComponent.Pickable |
Makes a component its own tag |
A ShapeHit contains the tag, the world position of the hit (pixels for a screen shape), the position in the
shape's own plane coordinates, the signed distance to the outline, and the depth.
Remarks:
- Picks are answered from the last drawn frame.
- The topmost shape is the one nearest to the camera. At equal depth it is the one drawn last. Screen shapes are in front of world shapes.
- The border is part of the shape.
slackPixelsextends the hit area, which helps with thin strokes. - A ring, arc or polyline is picked on its stroke. To pick a whole area, draw a disc or rectangle with a
transparent fill:
Fill.Set(null, 0f).
Limitations:
- Scene geometry does not block a pick. A depth-tested shape behind a wall can still be picked. Use GPU picking for meshes.
- A dashed outline is picked as if it were solid.
- A batch answers for the last view that drew it.
Text, display scale and components
- Text.
ShapeBatchdoes not draw text. Use World Text. AWorldTextComponentwithBillboard = falselies in its entity's XY plane, so it can sit on a panel. - Display scale. With
AutoScaleon, pixel values follow the display scale: 2 pixels at 100 % are 3 pixels at 150 %. SetAutoScale = falseto use physical pixels. - Components.
ShapeComponentdraws a shape from an entity's transform and appears in Game Studio. It needs noAddShapeBatchcall. If the game has a batch, the component uses it. Otherwise a depth-tested batch is registered automatically.
Colour and blending
- Colours are sRGB values. The shader decodes them to linear light when the render target is linear.
- Shapes are composited with premultiplied alpha, in linear light.
- An additive glow contributes colour and no alpha.
Limitations
- Convex fills only. The fill of a concave polygon is not supported. Strokes can follow any path, open or closed.
- Long translucent strokes. A run of more than 64 points is drawn in pieces. With
Opacitybelow 1, the joint between two pieces is slightly brighter. A space stroke restarts its dash pattern at each piece. - Flat shapes. Only space strokes pass through 3D points. A sphere outline is a billboard disc. Mesh wireframes are not supported.
- No clipping.
Viewportpositions and sizes screen shapes and does not clip them. - No text. Use World Text, or render text to a texture and use it as a fill.
- One fill source per batch. Use a second batch for a second texture.
- No layout or events. Hover and click handling is your code, based on
TryPick. - Sorting. A batch sorts against transparent meshes as one object. Two batches in the same stage draw in the order they were added, the later one on top. Draw shapes that must layer in a fixed order through one batch, and use an overlay batch for shapes that must never be covered.
Example: an in-scene console
The SignalR example (E13_SignalR/Station/) builds a console in the scene from ShapeBatch calls.
| Part | Implementation | Feature |
|---|---|---|
Board |
A plane in the world with (u, v) coordinates | PlaneMode.Fixed |
StationBoard.Draw |
Panel, rail, dividers, buttons and bars, drawn every frame | Immediate mode, draw state |
| Scheme buttons | A solid Fill for the selected one, a Glow for the hovered one |
Fill and glow |
DrawCornerTicks |
A quarter DrawArc and two DrawPixelLine calls per corner |
Arcs, pixel strokes |
Labels |
World text entities with the board's rotation | World Text |
DeckEffects rings |
DrawRing with a fading Opacity and a growing radius |
Opacity |
| Starfield | 420 DrawBillboardCircle calls in the same batch |
PlaneMode.Screen |