Custom Drawing
Most visuals are components — sprites, tilemaps, particles. When you need something they can’t express — debug overlays, procedural shapes, custom geometry — the engine exposes its drawing layer directly.
Which API to use
Section titled “Which API to use”| API | Model | Reach for it when |
|---|---|---|
Draw |
Immediate, global — commands batch and clear every frame. | Debug overlays, gizmos, anything redrawn from scratch each frame. |
Graphics |
Retained path recorder — build once, replay through Draw with flush(). |
Vector shapes with paths/béziers/arcs you don’t want to re-tessellate per frame. |
ShapeRenderer / MeshRenderer |
Components — entities with sorting layers, materials, editor gizmos. | Shapes/meshes that are part of the scene (see Sprites). |
Rule of thumb: if the visual belongs to the scene (sorted, saved, inspectable), make it a component; if it belongs to a frame (debug, HUD-ish overlays, procedural effects), draw it in a callback.
Draw callbacks
Section titled “Draw callbacks”Register a callback and it runs inside the render pass every frame, with the camera’s view-projection already active — no setup, just draw:
import { registerDrawCallback, Draw } from 'esengine';
registerDrawCallback('debug-overlay', (elapsed) => { Draw.line({ x: 0, y: 0 }, { x: 100, y: 50 }, { r: 1, g: 0, b: 0, a: 1 }, 2); Draw.circleOutline({ x: 0, y: 0 }, aggroRadius, { r: 1, g: 1, b: 0, a: 0.5 });});Callbacks draw on top of the scene; registerPreSceneDrawCallback draws
under it (its callback receives { width, height, elapsed }). Remove one
with unregisterDrawCallback(id). All coordinates are world-space.
The Draw API
Section titled “The Draw API”Draw is an immediate-mode singleton — commands are batched automatically and
cleared every frame, so call it each frame you want the shape visible:
| Method | Draws |
|---|---|
line(from, to, color, thickness?) |
A line segment (thickness in px, default 1). |
rect(pos, size, color, filled?) / rectOutline(pos, size, color, thickness?) |
A rectangle — pos is the center. |
circle(center, radius, color, filled?, segments?) / circleOutline(…) |
A circle (default 32 segments). |
texture(pos, size, textureHandle, tint?) |
A textured quad. |
textureRotated(pos, size, rotation, textureHandle, tint?) |
Same, rotated (radians). |
drawMesh(geometry, shader, transform) / drawMeshWithMaterial(geometry, material, transform?) |
A custom mesh by handle (see the Geometry API below). |
setLayer(layer) / setDepth(depth) |
Sorting of subsequent commands. |
setBlendMode(mode) / setDepthTest(on) |
Blend / depth state for subsequent commands. |
getDrawCallCount() / getPrimitiveCount() |
Frame stats. |
Vector paths: Graphics
Section titled “Vector paths: Graphics”For shapes built from paths — polylines, béziers, arcs, rounded rects —
Graphics records a path and replays it through Draw when you flush():
import { Graphics, registerDrawCallback } from 'esengine';
const g = new Graphics();g.lineStyle(2, { r: 1, g: 1, b: 1, a: 1 });g.beginFill({ r: 0, g: 0.5, b: 1, a: 1 });g.drawRoundRect(-50, -25, 100, 50, 8);g.endFill();g.moveTo(0, 0);g.curveTo(50, 80, 100, 0); // quadratic bézier
registerDrawCallback('hud-shape', () => g.flush());Path methods: moveTo / lineTo, curveTo (quadratic) / cubicCurveTo,
arc, drawRect / drawRoundRect / drawCircle / drawEllipse, clear().
lineStyle(thickness, color) sets the stroke; beginFill(color) / endFill()
fill rects and circles (path outlines stay strokes). The path is retained —
build it once, flush() it every frame.
Custom meshes: MeshRenderer
Section titled “Custom meshes: MeshRenderer”MeshRenderer is a component that renders an arbitrary indexed triangle list —
procedural terrain slices, radial health arcs, water surfaces. Author the
geometry on the component (positions as x,y pairs; optional per-vertex UVs and
colors):
cmds.spawn() .insert(Transform, { position: { x: 0, y: 0, z: 0 } }) .insert(MeshRenderer, { geometry: { positions: [0, 0, 100, 0, 50, 80], colors: [1,0,0,1, 0,1,0,1, 0,0,1,1], indices: [0, 1, 2], }, });The mesh shares the standard renderable surface: texture, color, layer,
lit, parallax, and material. Geometry set in a scene (or at spawn) uploads
automatically; to mutate it at runtime, go through the MeshRenderers resource —
setGeometry(entity, geometry) re-uploads (indices are validated engine-side),
getGeometry / clearGeometry round it out. Direct writes to the geometry
field alone are not change-detected.
Custom geometry: the Geometry API
Section titled “Custom geometry: the Geometry API”Where MeshRenderer is the component path, Geometry is the immediate one: it
uploads an indexed vertex buffer and hands back a handle you draw yourself —
paired with a material via
Draw.drawMeshWithMaterial(geometry, material, transform?) (or a raw shader via
Draw.drawMesh(geometry, shader, transform)).
import { Geometry, Draw, Material, registerDrawCallback } from 'esengine';
const quad = Geometry.createQuad(120, 120); // 1 unit ≙ 1 design pxconst mat = Material.create({ shader }); // a compileShader material
registerDrawCallback('custom-mesh', () => { Draw.drawMeshWithMaterial(quad, mat); // identity transform});GeometryOptions
Section titled “GeometryOptions”| Field | Default | Description |
|---|---|---|
vertices |
— | Interleaved vertex data as one Float32Array (max 64K floats per upload). |
layout |
— | VertexAttributeDescriptor[] — one { name, type } per attribute, in buffer order. |
indices |
— | Uint16Array or Uint32Array triangle list (max 16K indices per upload). |
dynamic |
false |
Allocate for frequent updates via updateVertices. |
DataType names each attribute’s shape: Float, Float2, Float3, Float4,
Int, Int2, Int3, Int4.
Methods & helpers
Section titled “Methods & helpers”| Method | Does |
|---|---|
create(options) |
Uploads the mesh; returns a GeometryHandle (throws on failure or oversized data). |
updateVertices(handle, vertices, offset?) |
Rewrites vertex data on a dynamic geometry (offset in floats). |
release(handle) / isValid(handle) |
Free / probe a handle. |
createQuad(width?, height?) |
A centered quad (defaults 1×1), layout a_position + a_texCoord. |
createCircle(radius?, segments?) |
A triangle-fan circle (defaults 1, 32), same layout. |
createPolygon(points) |
A fan-triangulated polygon from {x, y} points, UVs from its bounding box. |
The helper meshes all use the two-attribute Float2 position + Float2 UV
layout, so they pair directly with fragment-only .esshader materials.
Offscreen targets: RenderTexture
Section titled “Offscreen targets: RenderTexture”For minimaps, portraits, mirrors — and as the backing of
cache-as-bitmap — RenderTexture allocates an offscreen
render target. Wrap draw commands in begin/end to render into it; the
resulting texture is an ordinary texture handle usable anywhere (a
Sprite.texture, Draw.texture, a material texture param).
A whole scene goes into one by pointing a camera at it: set
Camera.renderTarget to the target’s handle and that camera draws there instead
of the screen, before the screen cameras and without competing to be the main
view. Its own sorting layers (Camera.cullingMask) decide what it sees, so a
minimap camera and the player’s camera can look at the same world and show
different parts of it. On WebGL2 today; the WebGPU backend cannot yet hand a
render target’s texture to a Sprite:
import { RenderTexture, Draw } from 'esengine';
const rt = RenderTexture.create({ width: 256, height: 256, filter: 'linear' });
RenderTexture.begin(rt, viewProjection); // an ortho view-projection you supplyDraw.circle({ x: 0, y: 0 }, 80, { r: 1, g: 0.5, b: 0, a: 1 });RenderTexture.end();
sprite.texture = rt.texture;RenderTextureOptions
Section titled “RenderTextureOptions”| Field | Default | Description |
|---|---|---|
width / height |
— | Target size in pixels (required). |
depth |
true |
Attach a depth buffer — turn off for pure 2D compositing to save memory. |
filter |
'nearest' |
Sampling filter when the result is drawn: 'nearest' or 'linear'. |
Methods
Section titled “Methods”| Method | Does |
|---|---|
create(options) |
Allocates the target; returns a handle with texture (resource-table handle for components), textureId (raw device id for low-level paths), width, height. |
begin(rt, viewProjection) |
Redirects subsequent draws into the target (you supply the matrix). |
end() |
Ends offscreen rendering, back to the screen. |
resize(rt, width, height) |
Releases and re-creates — returns a new handle (the texture handle changes). |
getDepthTexture(rt) |
The depth attachment as a sampleable texture handle. |
release(rt) |
Frees the target and its textures. |
This is a low-level surface: it doesn’t render the scene for you — it redirects
whatever you draw between begin/end, under a view-projection matrix you build.
See also
Section titled “See also”- Sprites & Rendering — the component path for images and shapes.
- Materials & Shaders — custom shaders for any renderable.
- Camera — where the view-projection your callbacks draw under comes from.