Skip to content

Rendering

Tecs provides a high-performance GPU-accelerated rendering pipeline designed for 2D games with advanced lighting and shadow effects. The rendering system is built on a deferred G-Buffer architecture that decouples geometry rendering from lighting calculations.

Quick Start

teal
local tecs = require("tecs")
local tecs2d = require("tecs2d")
local gfx = require("tecs2d.gfx")

love.run = tecs2d.run({
    fps = 60,
    game = function(world)
        -- Spawn a simple shape
        world:spawn(
            tecs.builtins.Transform(100, 100),
            gfx.Circle(20),
            gfx.Color(1, 0.5, 0, 1)
        )

        -- Spawn a sprite from Aseprite
        world:spawn(
            tecs.builtins.Transform(200, 100),
            gfx.Sprite.fromAseprite("assets/player.png", "idle")
        )
    end,
    render = {
        virtualWidth = 320,
        virtualHeight = 180,
        pixelMode = true,
    }
})

Architecture Overview

The rendering pipeline consists of several passes:

  1. G-Buffer Pass: Renders all geometry (sprites, shapes, text) to multiple render targets storing albedo, normals, specular, and emission information.

  2. Shadow Mask Pass: Renders occluder silhouettes to a shadow mask texture using compute shader culling.

  3. Lighting Pass: Applies dynamic lighting using raymarched shadows and normal-based shading.

  4. Composite Pass: Combines all passes into the final output.

Key Features

  • GPU Instancing: Batches thousands of entities into single draw calls
  • Compute Shader Culling: Visibility testing runs entirely on the GPU
  • Deferred Lighting: Decouples scene complexity from lighting cost
  • 2.5D Lighting: Height-based shading for pseudo-3D effects
  • Dynamic Shadows: Raymarched soft shadows with height-based occlusion
  • Pixel-Perfect Rendering: Opt-in retro mode for integer-scaled pixel art
  • Multiple Cameras: Minimaps, split-screen, and render-to-texture via independent cameras

Components Overview

Drawable Components

ComponentDescription
SpriteAnimated sprites from Aseprite sprite sheets
CircleFilled or outlined circles
EllipseFilled or outlined ellipses
ArcPartial circles/ellipses (pie slices)
RectangleFilled or outlined rectangles
LineLine segments
MeshCustom geometry
TextText using BMFont atlases (bitmap or MSDF)

Styling Components

ComponentDescription
ColorRGBA tinting
Blend ModesBlend mode control (add, multiply, etc.)
UnlitSkip dynamic lighting
PivotCustom pivot points (0-1 range)

Lighting Components

ComponentDescription
LightPoint lights and spotlights
OccluderShadow-casting entities

Special Components

ComponentDescription
CameraTargetMakes camera follow an entity
MaterialGPU-batched fragment shader injection

Layer Features

FeatureDescription
ParallaxLayer-based parallax scrolling effects

Documentation

TopicDescription
CameraCamera controls, multiple cameras, minimaps, split-screen, coordinate conversion
SpritesSprite sheets, animation, slices, collisions
ShapesCircles, rectangles, lines, and other primitives
TextBitmap and MSDF text rendering
StylingColor, blend modes, render flags
LayersLayer configuration and coordinate spaces
LightingPoint lights, spotlights, shadows, and occluders
Custom DrawingCPU drawing with depth sorting
MaterialsGPU-batched fragment shader injection

Performance Characteristics

The rendering system is designed to be GPU-bound rather than CPU-bound:

  • Zero-copy FFI buffers: Entity data is written directly to GPU-mapped memory
  • Archetype batching: Entities with the same components render together
  • Dirty range tracking: Only modified buffer regions are uploaded
  • Indirect drawing: Draw calls are issued from GPU-populated buffers

This architecture allows rendering of 100K+ entities at 60fps on modern hardware, with performance scaling based on GPU fill rate rather than Lua interpreter speed.

RenderConfig

The render pipeline is configured via the render table in tecs2d.run. All fields are optional with sensible defaults.

teal
love.run = tecs2d.run({
    fps = 60,
    game = gamePlugin,
    render = {
        virtualHeight = 180,
        pixelMode = true,
        lightingMode = "deferred",
        layers = {
            [10] = { name = "hud", space = "virtual", unlit = true },
        },
    },
})

Fields

FieldTypeDefaultDescription
virtualHeightintegerwindow heightVirtual resolution height. Controls camera FOV and, in retro mode, the integer scale factor
virtualWidthintegerautoFixed virtual width. Auto-computed from aspect ratio (non-retro) or to fill screen at integer scale (retro)
pixelModebooleanfalsetrue = retro (low-res + nearest-neighbor upscale). See Camera
lightingModestring"deferred""deferred" or "none" (full brightness). See Lighting
shadowsEnabledbooleantrueWhether occluders cast shadows
ambientLight{r, g, b}{1, 1, 1}Base illumination for unlit areas. See Lighting
zoomnumber1.0Initial camera zoom level. See Camera
cameraPosition{x, y}{0, 0}Initial camera position
lerpingEnabledbooleantrueEnable smooth camera movement. See Camera
lerpSpeednumber8.0Lerp speed factor (higher = snappier)
clampToBoundsbooleanfalseClamp camera to world bounds
worldBounds{minX, minY, maxX, maxY}noneWorld bounds for clamping
layers{integer: LayerConfig}noneLayer configurations keyed by layer number (1-16). See Layers
sizeHints{string: integer}noneInitial GPU buffer capacities keyed by name (sprites, circles, lights, etc.). Grows automatically
dropShadowScalenumber0.5Drop shadow AO canvas resolution scale (0.5 = half res, 1.0 = full res)
bloomBloomConfignoneBloom post-processing config: enabled, intensity, radius, threshold