kann
dark
light
crt
mid
KannNet / 2dFace online
SYSTEMS LUAU GEOMETRY ROBLOX
2026.05

Facepainting port


Facepainting banner

the backend pipeline powering the facepainting game. takes a flat alpha channel decal and turns it into a 3D face mesh in real time.

what I built

custom image to mesh pipeline. feeds it a decal with alpha channel data and it spits out a 3D face mesh. most of it is closed source but the math underpinning it is solid and I'm proud of how the quality preset system came out.

tech

low level buffer ops for pixel sampling, UV projection, texture dilation, computational geometry. config driven quality presets from fast to ultra so the whole thing is tunable without touching the core logic.

some code

sadly I don't feel like leaking all of the code to my beloved system!

bilinear alpha sampling

reads alpha values out of the raw pixel buffer using bilinear interpolation. nothing flashy but rounding errors here show up directly in the silhouette outline so it has to be right.

local function sampleAlpha(buf, imgSize, u, v)
    local w, h = imgSize.X, imgSize.Y
    local px, py = u * (w - 1), v * (h - 1)
    local x0 = math.clamp(math.floor(px), 0, w - 1)
    local y0 = math.clamp(math.floor(py), 0, h - 1)
    local x1 = math.clamp(x0 + 1, 0, w - 1)
    local y1 = math.clamp(y0 + 1, 0, h - 1)
    local fx, fy = px - math.floor(px), py - math.floor(py)
    local stride = w * 4
    local a00 = buffer.readu8(buf, y0 * stride + x0 * 4 + 3)
    local a10 = buffer.readu8(buf, y0 * stride + x1 * 4 + 3)
    local a01 = buffer.readu8(buf, y1 * stride + x0 * 4 + 3)
    local a11 = buffer.readu8(buf, y1 * stride + x1 * 4 + 3)
    return a00 + (a10 - a00) * fx + (a01 - a00) * fy + (a11 - a10 - a01 + a00) * fx * fy
end

quality presets

clean separation of tuning knobs from logic. applyQuality just patches preset values into whatever config the caller passed in so nothing downstream has to know about quality levels at all.

local QUALITY_PRESETS = {
    fast     = { resHeadroom=1.3, smoothingPasses=0, delaunayRefine=false, delaunayIters=0, budgetScale=0.45 },
    balanced = { resHeadroom=2.0, smoothingPasses=1, delaunayRefine=true,  delaunayIters=2, budgetScale=1.0  },
    high     = { resHeadroom=2.6, smoothingPasses=2, delaunayRefine=true,  delaunayIters=3, budgetScale=1.25 },
    ultra    = { resHeadroom=3.0, smoothingPasses=2, delaunayRefine=true,  delaunayIters=4, budgetScale=1.6  },
}

function pipeline.applyQuality(cfg)
    local preset = QUALITY_PRESETS[cfg.quality]
    if not preset then return end
    for k, v in pairs(preset) do cfg[k] = v end
end

polygon math

shoelace formula for signed area (tells you orientation), cross product for which side of a line a point is on, and point in triangle which gets used all over the place for mesh validation and texture sampling.

local function signedArea(poly)
    local area, n = 0, #poly
    for i = 1, n do
        local p, q = poly[i], poly[i % n + 1]
        area += p.x * q.y - q.x * p.y
    end
    return area * 0.5
end

local function cross2(ax, ay, bx, by) return ax * by - ay * bx end

local function pointInTri(ax, ay, bx, by, cx, cy, px, py)
    local d1 = cross2(bx - ax, by - ay, px - ax, py - ay)
    local d2 = cross2(cx - bx, cy - by, px - bx, py - by)
    local d3 = cross2(ax - cx, ay - cy, px - cx, py - cy)
    return not ((d1 < 0 or d2 < 0 or d3 < 0) and (d1 > 0 or d2 > 0 or d3 > 0))
end
back to portfolio play on roblox
tech
implementation

buffer ops

UV Projection

Tex Dilation

Luau

tags
#luau #roblox #geometry #systems #pipeline