Draw in the world
Problem. You want a coloured marker over a building, or a tint over a whole platform.
Solution. Postfix the map draw call and submit a mesh. Nothing in shapez 2 is a
GameObject per building, so there is nothing to parent to — you draw every frame from
the model.
Get into the frame
using ShapezShifter.SharpDetour;
using MonoMod.RuntimeDetour;
private readonly Hook DrawHook;
public MyMod(ILogger logger)
{
DrawHook = DetourHelper.CreatePostfixHook<MapDrawer, FrameDrawOptionsNoLOD>(
(drawer, options) => drawer.Draw(options),
(drawer, options) => Draw(options));
}
public void Dispose() => DrawHook?.Dispose();
Postfix means you draw after the game's own drawers, which is what you want for an
overlay. The options parameter carries everything you need, including
options.Player.CurrentMap — no global lookup.
The first lambda is never executed; it is parsed as an expression tree to identify the target method.
Draw a coloured plane on a tile
private void Draw(FrameDrawOptionsNoLOD options)
{
if (!Enabled) return;
IMapModel map = options.Player?.CurrentMap;
if (map == null) return;
IMaterialReference material = options.Theme.BaseResources.UXOverviewModeMapResourcePlaneMaterial;
foreach (BuildingModel building in InterestingBuildings)
{
float3 position = building.Tile_G.ToCenter_W(2.0f); // 2 units above the tile
options.Renderers.RegularNonInstanced.DrawMesh(
GetPlaneMesh(ColorFor(building)),
material,
Matrix4x4.TRS(position, Quaternion.identity, new Vector3(20f, 20f, 20f)),
RenderCategory.Misc,
MaterialPropertyHelpers.CreateAlphaBlock(0.7f));
}
}
Colour: bake it into the mesh
The material shader used above takes alpha, not an arbitrary colour, so the game bakes colour into the mesh and caches one mesh per colour. Copy that:
private readonly DisposableDictionary<Color, TemporaryMeshReference> PlaneMeshes = new();
private IMeshReference GetPlaneMesh(Color color)
{
if (!PlaneMeshes.TryGetValue(color, out TemporaryMeshReference mesh))
{
mesh = GeometryHelpers.GeneratePlaneMeshUVColoredUncached(color);
PlaneMeshes.Add(color, mesh);
}
return mesh;
}
Quantise your colours so the cache stays small — an eleven-step red→green ramp is plenty for an overlay, and gives you eleven meshes instead of thousands.
Dispose the dictionary in Dispose(). Leaked TemporaryMeshReferences are a known
enough trap that the class ships a WarnAboutNonDisposedInstances() helper.
Only in overview mode
if (!options.InOverviewMode) return; // Viewport.Zoom > 1500f
This is how the game swaps detailed island meshes for flat overview planes, and it is the natural switch between "tint each machine" (zoomed in) and "tint each platform" (zoomed out).
Overview mode reserves specific heights for its own planes — -25 background, -24
explored area, -23 shapes, -22 fluids. Pick offsets away from those.
Cover a whole platform
foreach (GlobalChunkCoordinate chunk in island.Chunks)
{
float3 position = chunk.ToCenter_W(-20.0f);
// …DrawMesh with a chunk-sized scale
}
Batch it before you ship it
One DrawMesh per building is fine for a few hundred and painful for ten thousand.
MeshBuilder combines them into a single mesh:
using MeshBuilder builder = new MeshBuilder("MyOverlay(island)", lod: 0);
foreach (GlobalChunkCoordinate chunk in island.Chunks)
{
builder.AddTranslateScale(planeMesh, chunk.ToCenter_W(-20f), new float3(20f, 20f, 20f));
}
if (builder.Empty) return null;
TemporaryMeshReference combined = builder.GenerateSingleMeshMax65KVertices();
Build these once and cache, keyed by whatever invalidates them — the island's
contents, and your own colour buckets. Rebuilding a combined mesh every frame is worse
than not combining at all. The game caches per SuperChunkCoordinate and invalidates on
map.OnBuildingAdded / OnIslandAdded and friends.
Gotchas
MaterialPropertyHelpers.CreateAlphaBlockandCreateBaseColorBlockmutate and return a shared static block. Submit the draw immediately; never hold one, and never expect to combine both calls. Keep your ownMaterialPropertyBlockif you need two properties at once.options.Themeis[Obsolete]("should be injected to drawers") but is the only easy route to the shared materials.- Cull.
MapDrawer.LastCullResulttells you which super chunks are visible; drawing the whole map when the camera sees one corner is the most common performance mistake. - Exceptions thrown from a postfix are not caught the way sub-drawer exceptions are.
Wrap your draw body in
try/catchor one bad frame kills every frame. options.Renderers.UIignores depth, so anything you draw through it shows through platforms above it. See depth, layers and overlays.- Two translucent quads stacked on each other blend. A coloured marker drawn over a coloured wash comes out as the mix of the two unless you give it its own near-opaque property block.
- "Space view" is a player mode, not a zoom level:
options.Player.InteractionState.BaseState == PlayerInteractionBaseState.Islands.options.InOverviewModeis onlyViewport.Zoom > 1500, so testing zoom alone still draws per-machine detail while the player is placing platforms.