lingmodeler
↓ 0 downloadsLingModeler: box modeling with quads and tris, rigging + weight painting, 2D vector illustration and animation, procedural textures and game assets; .lmesh / .ling native formats, OBJ, glTF and SVG import / export
Keywords 3d, modeling, vector, svg, gltf, obj, rigging, animation, textures, ling
lingfu install lingmodelerCode / Files (67)
ling/raster.ling
# ━━━ raster.ling — a small software rasterizer (RGBA buffer -> PNG) for animation frames, sprite sheets and turntables ━━━
# Buffers are flat lists of 4 numbers per pixel (r, g, b, a in 0..1). Polygons are filled with 4 sub-scanlines per row
# and exact horizontal coverage, so edges are anti-aliased.
fn ra_new(w, h, bg) {
bind buf = list_new()
bind r = 0.0 bind g = 0.0 bind b = 0.0 bind a = 0.0
if bg >= 0.0 { bind r = c_r(bg) / 255.0 bind g = c_g(bg) / 255.0 bind b = c_b(bg) / 255.0 bind a = 1.0 }
bind i = 0.0
while i < w * h { bind buf = list_push(buf, r) bind buf = list_push(buf, g) bind buf = list_push(buf, b) bind buf = list_push(buf, a) bind i = i + 1.0 }
st_set("ra_buf", buf)
st_set("ra_w", w) st_set("ra_h", h)
}
# fill a polygon (flat [x,y,...] in buffer pixels), colour, alpha
fn ra_poly(pts, col, alpha) {
bind n = floor(len(pts) / 2.0)
if n < 3.0 || alpha <= 0.0 { return 0.0 }
bind w = st_get("ra_w", 1.0) bind h = st_get("ra_h", 1.0)
bind ymin = 1000000.0 bind ymax = 0.0 - 1000000.0
bind i = 0.0
while i < n { bind y = list_get(pts, i * 2.0 + 1.0) bind ymin = min(ymin, y) bind ymax = max(ymax, y) bind i = i + 1.0 }
bind y0 = max(0.0, floor(ymin)) bind y1 = min(h - 1.0, ceil(ymax))
bind cr = c_r(col) / 255.0 bind cg = c_g(col) / 255.0 bind cb = c_b(col) / 255.0
bind buf = st_get("ra_buf", [])
bind y = y0
while y <= y1 {
bind cov = list_new()
bind xlo = w bind xhi = 0.0 - 1.0
bind s = 0.0
while s < 4.0 {
bind sy = y + (s + 0.5) * 0.25
bind xs = list_new()
bind i = 0.0
while i < n {
bind j = (i + 1.0) % n
bind ay = list_get(pts, i * 2.0 + 1.0) bind by = list_get(pts, j * 2.0 + 1.0)
if (ay <= sy && by > sy) || (by <= sy && ay > sy) {
bind ax = list_get(pts, i * 2.0) bind bx = list_get(pts, j * 2.0)
bind xc = ax + (sy - ay) * (bx - ax) / (by - ay)
bind xs = list_push(xs, xc)
bind k = len(xs) - 1.0
bind go = true
while go {
if k <= 0.0 { bind go = false }
else if list_get(xs, k - 1.0) > list_get(xs, k) {
bind tmp = list_get(xs, k - 1.0)
bind xs = list_set(xs, k - 1.0, list_get(xs, k))
bind xs = list_set(xs, k, tmp)
bind k = k - 1.0
} else { bind go = false }
}
}
bind i = i + 1.0
}
bind k = 0.0
while k + 1.0 < len(xs) {
bind xa = max(0.0, list_get(xs, k)) bind xb = min(w, list_get(xs, k + 1.0))
if xb > xa {
bind px = floor(xa)
while px < xb {
bind ov = min(xb, px + 1.0) - max(xa, px)
# sparse coverage row: pairs (x, amount) appended; merged below
bind cov = list_push(cov, px) bind cov = list_push(cov, ov * 0.25)
bind xlo = min(xlo, px) bind xhi = max(xhi, px)
bind px = px + 1.0
}
}
bind k = k + 2.0
}
bind s = s + 1.0
}
if xhi >= xlo {
bind row = me_zeros(xhi - xlo + 1.0)
bind q = 0.0
while q + 1.0 < len(cov) { bind ix = list_get(cov, q) - xlo bind row = list_set(row, ix, list_get(row, ix) + list_get(cov, q + 1.0)) bind q = q + 2.0 }
bind x = xlo
while x <= xhi {
bind a = min(1.0, list_get(row, x - xlo)) * alpha
if a > 0.001 {
bind p = (y * w + x) * 4.0
bind da = list_get(buf, p + 3.0)
bind oa = a + da * (1.0 - a)
if oa > 0.000001 {
bind buf = list_set(buf, p, (cr * a + list_get(buf, p) * da * (1.0 - a)) / oa)
bind buf = list_set(buf, p + 1.0, (cg * a + list_get(buf, p + 1.0) * da * (1.0 - a)) / oa)
bind buf = list_set(buf, p + 2.0, (cb * a + list_get(buf, p + 2.0) * da * (1.0 - a)) / oa)
bind buf = list_set(buf, p + 3.0, oa)
}
}
bind x = x + 1.0
}
}
bind y = y + 1.0
}
st_set("ra_buf", buf)
}
fn ra_stroke(pts, closed, col, width, alpha) {
bind n = floor(len(pts) / 2.0)
bind segs = sel(closed, n, n - 1.0)
bind hw = max(0.5, width * 0.5)
bind i = 0.0
while i < segs {
bind j = (i + 1.0) % n
bind x0 = list_get(pts, i * 2.0) bind y0 = list_get(pts, i * 2.0 + 1.0)
bind x1 = list_get(pts, j * 2.0) bind y1 = list_get(pts, j * 2.0 + 1.0)
bind dx = x1 - x0 bind dy = y1 - y0
bind l = sqrt(dx * dx + dy * dy)
if l > 0.0001 {
bind nx = 0.0 - dy / l * hw bind ny = dx / l * hw
# extend a little along the segment so joints close
bind ex = dx / l * hw * 0.5 bind ey = dy / l * hw * 0.5
ra_poly([x0 + nx - ex, y0 + ny - ey, x1 + nx + ex, y1 + ny + ey, x1 - nx + ex, y1 - ny + ey, x0 - nx - ex, y0 - ny - ey], col, alpha)
}
bind i = i + 1.0
}
}
# copy the buffer into an image at (ox, oy)
fn ra_blit(img, ox, oy) {
bind buf = st_get("ra_buf", [])
bind w = st_get("ra_w", 1.0) bind h = st_get("ra_h", 1.0)
bind y = 0.0
while y < h {
bind x = 0.0
while x < w {
bind p = (y * w + x) * 4.0
p_img_set(img, ox + x, oy + y, list_get(buf, p) * 255.0, list_get(buf, p + 1.0) * 255.0, list_get(buf, p + 2.0) * 255.0, list_get(buf, p + 3.0) * 255.0)
bind x = x + 1.0
}
bind y = y + 1.0
}
}
# ── vector document -> buffer (current animation state) ──────────────────────
fn ra_vector(size) {
bind dw = st_get("vc_w", 512.0) bind dh = st_get("vc_h", 512.0)
bind s = size / max(dw, dh)
bind w = max(1.0, round(dw * s)) bind h = max(1.0, round(dh * s))
ra_new(w, h, st_get("vc_bg", 0.0 - 1.0))
bind ids = vc_ids()
bind i = 0.0
while i < len(ids) {
bind k = list_get(ids, i)
if vc_layer_visible(st_get(vk(k, "layer"), 0.0)) {
bind f = vc_flatten(k, 0.75 / s)
bind p = list_new()
bind q = 0.0
while q < len(f) { bind p = list_push(p, list_get(f, q) * s) bind q = q + 1.0 }
bind closed = st_get(vk(k, "closed"), 0.0) > 0.5
bind op = st_get(vk(k, "op"), 1.0)
if closed && st_get(vk(k, "fill"), 0.0 - 1.0) >= 0.0 { ra_poly(p, st_get(vk(k, "fill"), 0.0), op) }
if st_get(vk(k, "stroke"), 0.0 - 1.0) >= 0.0 && st_get(vk(k, "sw"), 0.0) > 0.0 { ra_stroke(p, closed, st_get(vk(k, "stroke"), 0.0), st_get(vk(k, "sw"), 1.0) * s, op) }
}
bind i = i + 1.0
}
return [w, h]
}
# ── 3D scene -> buffer from a camera orbit (uses the viewport's projection code) ──
fn ra_scene(size, yaw, pitch) {
bind keep = [st_get("cam_yaw", 0.0), st_get("cam_pitch", 0.0), st_get("cam_dist", 7.0), st_get("cam_tx", 0.0), st_get("cam_ty", 0.0), st_get("cam_tz", 0.0), st_get("cam_persp", 1.0)]
v3_frame(true)
st_set("cam_yaw", yaw) st_set("cam_pitch", pitch)
v3_update_camera(0.0, 0.0, size, size)
v3_project_all()
ra_new(size, size, 0.0 - 1.0)
# painter's order over all faces
bind ids = me_ids()
bind fo = list_new() bind ff = list_new() bind fz = list_new()
bind i = 0.0
while i < len(ids) {
bind o = list_get(ids, i)
if st_get(mk(o, "vis"), 1.0) > 0.5 {
bind fi = st_get(mk(o, "fi"), []) bind fs = st_get(mk(o, "fs"), []) bind fc = st_get(mk(o, "fc"), [])
bind pd = st_get(pj(o, "d"), [])
bind f = 0.0
while f < len(fc) {
bind s = list_get(fs, f) bind c = list_get(fc, f)
if c >= 3.0 {
bind z = 0.0
bind k = 0.0
while k < c { bind z = z + list_get(pd, list_get(fi, s + k)) bind k = k + 1.0 }
bind fo = list_push(fo, o) bind ff = list_push(ff, f) bind fz = list_push(fz, 0.0 - z / c)
}
bind f = f + 1.0
}
}
bind i = i + 1.0
}
bind order = mx_argsort(fz)
bind i = 0.0
while i < len(order) {
bind di = list_get(order, i)
bind o = list_get(fo, di) bind f = list_get(ff, di)
bind fi = st_get(mk(o, "fi"), []) bind fs = st_get(mk(o, "fs"), []) bind fc = st_get(mk(o, "fc"), [])
bind px = st_get(pj(o, "x"), []) bind py = st_get(pj(o, "y"), [])
bind pcx = st_get(pj(o, "cx"), []) bind pcy = st_get(pj(o, "cy"), []) bind pcz = st_get(pj(o, "cz"), [])
bind s = list_get(fs, f) bind c = list_get(fc, f)
bind nx = 0.0 bind ny = 0.0 bind nz = 0.0
bind pts = list_new()
bind k = 0.0
while k < c {
bind a = list_get(fi, s + k) bind b = list_get(fi, s + (k + 1.0) % c)
bind nx = nx + (list_get(pcy, a) - list_get(pcy, b)) * (list_get(pcz, a) + list_get(pcz, b))
bind ny = ny + (list_get(pcz, a) - list_get(pcz, b)) * (list_get(pcx, a) + list_get(pcx, b))
bind nz = nz + (list_get(pcx, a) - list_get(pcx, b)) * (list_get(pcy, a) + list_get(pcy, b))
bind pts = list_push(pts, list_get(px, a)) bind pts = list_push(pts, list_get(py, a))
bind k = k + 1.0
}
bind nl = sqrt(nx * nx + ny * ny + nz * nz)
if nl > 0.0000001 {
bind a0 = list_get(fi, s)
if nx * list_get(pcx, a0) + ny * list_get(pcy, a0) + nz * list_get(pcz, a0) < 0.0 {
bind col = st_get(mk(o, "col"), C_BLUE)
bind fcol = st_get(mk(o, "fcol"), [])
if f < len(fcol) { if list_get(fcol, f) >= 0.0 { bind col = list_get(fcol, f) } }
ra_poly(pts, v3_shade(col, nx / nl, ny / nl, nz / nl), 1.0)
}
}
bind i = i + 1.0
}
st_set("cam_yaw", list_get(keep, 0)) st_set("cam_pitch", list_get(keep, 1)) st_set("cam_dist", list_get(keep, 2))
st_set("cam_tx", list_get(keep, 3)) st_set("cam_ty", list_get(keep, 4)) st_set("cam_tz", list_get(keep, 5)) st_set("cam_persp", list_get(keep, 6))
}
# ── exports ──────────────────────────────────────────────────────────────────
fn ra_turntable() {
if !p_has_images() { st_set("status", "Image export needs the desktop build") return 0.0 }
bind path = p_dlg_save("Save turntable sprite sheet", "png", st_get("scene_name", "scene") + "_turntable.png")
if path == "" { return 0.0 }
ra_turntable_to(path)
}
fn ra_turntable_to(path) {
bind n = max(1.0, st_get("tt_n", 8.0)) bind size = st_get("tt_size", 128.0)
bind img = p_img_new(size * n, size)
bind i = 0.0
while i < n {
ra_scene(size, i / n * 6.28318530718, mx_rad(st_get("tt_pitch", 25.0)))
ra_blit(img, i * size, 0.0)
bind i = i + 1.0
}
p_img_save(img, path)
st_set("status", "Turntable: " + num_str(n) + " frames of " + fmt0(size) + " px -> " + path_base(path))
}
fn sheet_export(kind) {
if !p_has_images() { st_set("status", "Image export needs the desktop build") return 0.0 }
bind frames = max(1.0, st_get("sh_frames", 8.0)) bind size = st_get("sh_size", 128.0) bind cols = st_get("sh_cols", 0.0)
if kind == "texsheet" {
bind p = p_dlg_save("Save texture sprite sheet", "png", tx_cur_name() + "_sheet.png")
if p == "" { return 0.0 }
tx_export_sheet(p, frames, size, cols, st_get("sh_cycle", 1.0))
st_set("status", "Texture sheet: " + num_str(frames) + " frames -> " + path_base(p))
return 0.0
}
if kind == "frames" {
bind dir = p_dlg_folder("Folder for the PNG frames")
if dir == "" { return 0.0 }
ra_vector_frames(dir, frames)
return 0.0
}
bind p = p_dlg_save("Save animation sprite sheet", "png", st_get("vc_name", "drawing") + "_sheet.png")
if p == "" { return 0.0 }
ra_vector_sheet(p, frames, size, cols)
}
# frames sampled evenly across the timeline (start .. end)
fn ra_frame_at(i, frames) {
bind s = st_get("tl_start", 0.0) bind e = st_get("tl_end", 48.0)
if frames < 1.5 { return tl_frame() }
return s + (e - s) * i / frames
}
fn ra_vector_frames(dir, frames) {
an_save()
bind size = st_get("sh_size", 128.0)
bind i = 0.0
while i < frames {
an_eval(ra_frame_at(i, frames))
bind wh = ra_vector(size)
bind img = p_img_new(list_get(wh, 0), list_get(wh, 1))
ra_blit(img, 0.0, 0.0)
bind num = num_str(i)
while str_len(num) < 4.0 { bind num = "0" + num }
p_img_save(img, fs_join(dir, st_get("vc_name", "frame") + "_" + num + ".png"))
bind i = i + 1.0
}
an_restore()
st_set("status", "Wrote " + num_str(frames) + " PNG frames to " + dir)
}
fn ra_vector_sheet(path, frames, size, cols) {
an_save()
bind cols2 = sel(cols < 0.5, frames, cols)
bind rows = ceil(frames / cols2)
bind dw = st_get("vc_w", 512.0) bind dh = st_get("vc_h", 512.0)
bind s = size / max(dw, dh)
bind fw = max(1.0, round(dw * s)) bind fh = max(1.0, round(dh * s))
bind img = p_img_new(fw * cols2, fh * rows)
bind i = 0.0
while i < frames {
an_eval(ra_frame_at(i, frames))
ra_vector(size)
ra_blit(img, (i % cols2) * fw, floor(i / cols2) * fh)
bind i = i + 1.0
}
an_restore()
p_img_save(img, path)
st_set("status", "Sprite sheet: " + num_str(frames) + " frames -> " + path_base(path))
}