Drawing

Drawing in Codea

Codea comes with an immediate mode drawing API that forms the basis of rendering both 2D and 3D graphics

The user-defined global draw() function is called once per frame to update the contents of the screen

The background(r,g,b,a) function is used to clear the background with a given color. When this isn’t called the contents of the background will stay the same as the previous frame

Codea uses an hybrid-immediate mode drawing system, where various built-in functions can be used to paint objects to the screen when called, such as line(), sprite() and rect()

These functions use the current render state, defined by the current style and matrix in use, which effects things like fill color and stroke color / width

Style

The style module contains functions that set the current drawing state

This module uses a fluent syntax, so any call that does not return a value, will instead return the style module itself, allowing multiple style commands to be chained together:

-- Draw a red ellipse with a 5px white stroke
style.push().fill(color.red).stroke(color.white).strokeWidth(5)
ellipse(WIDTH/2, HEIGHT/2, 100, 100)
style.pop()

See style for a complete reference of all functionality

Anywhere <color> is used as a parameter, the following forms can be used:

style.func(r, g, b, a) -- separate color components in the range 0-255
style.func(r, g, b) -- only red, green, blue color components with alpha assumed to be 255
style.func(grey, alpha) -- only greyscale and alpha
style.func(grey) -- only grayscale with alpha assumed to be 255
style.func(color) -- a color object

Matrix

The matrix module is used to manipulate the current immediate mode transform, view and projection matrices, which can be thought of as object pose, camera pose and perspective

By combining different matrix commands, any 2D or 3D drawing setup can be achieved, and there is also the camera class which can be used on its own or part of a scene for a higher level abstraction

The matrix module also uses a fluent syntax, so calls can be chained:

-- Translate then rotate in one expression
matrix.push().translate(100, 100).rotate(45)
rect(0, 0, 50, 50)
matrix.pop()

See matrix for a complete reference

3D Drawing

To draw in 3D, switch from the default 2D orthographic projection to a perspective projection using matrix.perspective(). Always wrap 3D drawing in a matrix.push() / matrix.pop() pair so the 2D projection is restored afterwards.

Important

With matrix.perspective(), the camera sits at the origin and looks toward +Z. Objects must have a positive Z coordinate to be visible. Use matrix.translate() to move objects in front of the camera.

function draw()
    background(20, 20, 30)

    -- All 3D drawing goes inside a matrix.push/pop block
    matrix.push()
        matrix.perspective(60)          -- 60° field of view
        matrix.translate(0, 0, 5)       -- move object 5 units forward (+Z)
        matrix.rotate(angle, 0, 1, 0)   -- spin around Y axis
        myMesh:draw()
    matrix.pop()

    -- 2D drawing after matrix.pop() uses the default screen projection
    text("Hello", WIDTH/2, 40)
end

To position the camera elsewhere in the scene, set a custom view matrix using matrix.view() together with mat4.orbit():

matrix.push()
    matrix.perspective(60)
    -- Orbit camera: looking at origin from distance 8, tilted 20° up and 45° around Y
    matrix.view(mat4.orbit(vec3(0, 0, 0), 8, 20, 45))
    myMesh:draw()
matrix.pop()

Background

background() clears the screen each frame with a solid color (or image/shader):

background(0, 0, 0)        -- black
background(40, 40, 50)     -- dark blue-grey
background(color.white)    -- using a color object

See graphics commands for full documentation

Vector Drawing

A set of 2D drawing functions are available in the global namespace. They all respect the current style settings:

-- Lines
style.push().stroke(255, 0, 0).strokeWidth(4)
line(100, 100, 400, 300)
style.pop()

-- Shapes
style.push().fill(0, 128, 255).stroke(255).strokeWidth(2)
rect(WIDTH/2, HEIGHT/2, 200, 100)       -- centered rectangle
ellipse(WIDTH/2, HEIGHT/2, 150, 150)    -- circle
style.pop()

-- Polygon
style.push().fill(255, 200, 0)
polygon(200,100, 300,200, 250,300, 150,300, 100,200)
style.pop()

See graphics commands for a complete list of vector drawing functions

Text

Use text() to draw strings to the screen. Text color is set with style.fill():

style.push().fill(255).fontSize(32).textAlign(CENTER | MIDDLE)
text("Hello, Codea!", WIDTH/2, HEIGHT/2)
style.pop()

Emojis require native rendering

By default Codea uses its own text renderer, which does not support emoji characters (they appear as empty boxes). To render emojis, enable native text rendering with TEXT_NATIVE:

-- Emojis render as boxes with the default renderer
text("Broken: 🎉 🚀 ❤️", WIDTH/2, HEIGHT/2 + 60)

-- Enable native rendering to display emojis correctly
style.push().textStyle(TEXT_NATIVE)
text("Working: 🎉 🚀 ❤️", WIDTH/2, HEIGHT/2)
style.pop()

Note

TEXT_NATIVE uses the system font renderer, so other textStyle flags (bold, italic, rich text, etc.) are ignored while it is active.

Images and Sprites

Images are loaded with image.read() and drawn with sprite():

function setup()
    img = image.read(asset.builtin.Blocks.Brick)
end

function draw()
    background(0)
    sprite(img, WIDTH/2, HEIGHT/2, 200, 200)
end

Context

The context module lets you draw into an image instead of the screen, using context.push() and context.pop():

local target = image(512, 512)

context.push(target)
    background(0)
    style.push().fill(255, 0, 0)
    ellipse(256, 256, 200, 200)
    style.pop()
context.pop()

-- Now draw that image to screen
sprite(target, WIDTH/2, HEIGHT/2)

Meshes

The mesh class is the primary way to draw custom 3D geometry. You can either use the built-in mesh generators or build geometry manually.

Built-in mesh generators

sphereMesh   = mesh.sphere(1)          -- sphere, radius 1
boxMesh      = mesh.box(vec3(1,1,1))   -- box, 1×1×1
cylinderMesh = mesh.cylinder(0.5, 1)   -- radius 0.5, height 1
torusMesh    = mesh.torus(0.25, 1)     -- torus

Drawing a mesh in 3D

function setup()
    sphereMesh = mesh.sphere(1)
    angle = 0
end

function draw()
    background(20, 20, 30)
    angle = angle + 0.5

    matrix.push()
        matrix.perspective(60)
        matrix.translate(0, 0, 4)
        matrix.rotate(angle, 0, 1, 0)
        sphereMesh:draw()
    matrix.pop()
end

Building a mesh manually — use vertices not positions

When setting mesh geometry by hand, use the mesh.vertices property. It sets positions and automatically creates a sequential index buffer so the mesh draws immediately. mesh.positions only updates positions without touching the index buffer — useful for deforming an existing mesh, but on a fresh empty mesh it will leave the indices empty and nothing will be drawn:

-- CORRECT: vertices sets positions AND indices automatically
m = mesh()
m.vertices = { vec3(0,0,0), vec3(1,0,0), vec3(0.5,1,0) }  -- one triangle
m.colors   = { color(255,0,0), color(0,255,0), color(0,0,255) }
m:draw()

-- Use positions only to deform an existing mesh (preserves its index buffer)
sph = mesh.sphere(1)
-- modify sph.positions each frame to animate without breaking sphere indices

Complete example: Rotating RGB Cube

Example
-- RGB Cube
-- A rotating cube with RGB vertex colors

function setup()
    viewer.mode = FULLSCREEN

    -- Build a cube mesh with 8 corners
    cubeMesh = mesh()
    local s = 0.5

    local v = {
        vec3(-s,-s,-s), vec3( s,-s,-s), vec3( s, s,-s), vec3(-s, s,-s),
        vec3(-s,-s, s), vec3( s,-s, s), vec3( s, s, s), vec3(-s, s, s),
    }

    -- Map each corner's position to an RGB color
    local function posToColor(p)
        return color(
            math.floor((p.x + s) / (2*s) * 255),
            math.floor((p.y + s) / (2*s) * 255),
            math.floor((p.z + s) / (2*s) * 255))
    end

    -- 6 faces × 2 triangles, wound for correct front-face culling
    local faces = {
        {v[5],v[6],v[7]}, {v[5],v[7],v[8]}, -- Front  (+z)
        {v[2],v[1],v[4]}, {v[2],v[4],v[3]}, -- Back   (-z)
        {v[1],v[5],v[8]}, {v[1],v[8],v[4]}, -- Left   (-x)
        {v[6],v[2],v[3]}, {v[6],v[3],v[7]}, -- Right  (+x)
        {v[8],v[7],v[3]}, {v[8],v[3],v[4]}, -- Top    (+y)
        {v[1],v[2],v[6]}, {v[1],v[6],v[5]}, -- Bottom (-y)
    }

    local verts, cols = {}, {}
    for _, tri in ipairs(faces) do
        for _, p in ipairs(tri) do
            table.insert(verts, p)
            table.insert(cols, posToColor(p))
        end
    end

    -- Use 'vertices' (not 'positions') so indices are set automatically
    cubeMesh.vertices = verts
    cubeMesh.colors   = cols

    angleX, angleY = 0, 0
end

function draw()
    background(20, 20, 30)

    angleX = angleX + 0.4
    angleY = angleY + 0.6

    -- Set up perspective projection.
    -- With matrix.perspective() the camera is at the origin looking
    -- toward +Z, so objects must have a positive Z to be visible.
    matrix.push()
        matrix.perspective(60)
        matrix.translate(0, 0, 3)      -- place cube 3 units in front
        matrix.rotate(angleX, 1, 0, 0)
        matrix.rotate(angleY, 0, 1, 0)
        cubeMesh:draw()
    matrix.pop()
end

Shaders and Materials

Both shaders and materials can be applied to a mesh to control how it is rendered. See shader, material and Shaders and Materials for details.

Lighting

Codea supports dynamic lighting via light. Generated meshes (mesh.sphere(), mesh.box(), etc.) use a lit material by default, so without any lights they render black. There are two ways to make them visible:

Option 1: Unlit material with a color (no light needed):

sph = mesh.sphere(1)
sph.material = material.unlit()
sph.material.color = color(100, 180, 255)

Option 2: Lit material with a directional light (produces 3D shading):

function setup()
    sph = mesh.sphere(1)
    sph.material = material.lit()
    sph.material.color = color(100, 180, 255)

    dirLight = light.directional(vec3(1, -1, 1))
    dirLight.intensity = 2
end

function draw()
    background(20, 20, 30)
    matrix.push()
        matrix.perspective(60)
        light.push(dirLight)
        matrix.translate(0, 0, 4)
        sph:draw()
        light.pop()
    matrix.pop()
end

Note

Only light.directional() is currently supported for immediate-mode rendering. light.point() and light.spot() are defined but not yet implemented by the renderer.

Full sphere example
-- 3D Sphere with directional light

function setup()
    viewer.mode = FULLSCREEN
    sphereMesh = mesh.sphere(1)
    sphereMesh.material = material.lit()
    sphereMesh.material.color = color(100, 180, 255)

    dirLight = light.directional(vec3(1, -1, 1))
    dirLight.intensity = 2

    angle = 0
end

function draw()
    background(20, 20, 30)
    angle = angle + 0.5

    matrix.push()
        matrix.perspective(60)
        light.push(dirLight)
        matrix.translate(0, 0, 4)
        matrix.rotate(angle, 0, 1, 0)
        sphereMesh:draw()
        light.pop()
    matrix.pop()
end