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/gltf.ling
# ━━━ gltf.ling — glTF 2.0 export (.gltf embedded / .glb) with materials, textures, skins and animations; import (.gltf) ━━━
# Binary data is produced and consumed in Ling: float32 / uint16 / uint32 little-endian, base64.
fn gl_b64_alpha() { return "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" }
# ── byte encoding ────────────────────────────────────────────────────────────
fn gl_f32_bytes(bytes, v) {
if v == 0.0 || abs(v) < 0.000000000000000000000000000000000000012 {
bind b = list_push(bytes, 0.0) bind b = list_push(b, 0.0) bind b = list_push(b, 0.0)
return list_push(b, sel(v < 0.0, 128.0, 0.0))
}
bind sign = sel(v < 0.0, 1.0, 0.0)
bind a = abs(v)
bind e = floor(mx_ln(a) / mx_ln(2.0))
bind m = a / pow(2.0, e)
if m >= 2.0 { bind m = m / 2.0 bind e = e + 1.0 }
if m < 1.0 { bind m = m * 2.0 bind e = e - 1.0 }
bind be = e + 127.0
bind frac = round((m - 1.0) * 8388608.0)
if frac >= 8388608.0 { bind frac = 0.0 bind be = be + 1.0 }
bind bits = sign * 2147483648.0 + be * 8388608.0 + frac
return gl_u32_bytes(bytes, bits)
}
fn gl_u32_bytes(bytes, v) {
bind b0 = v % 256.0 bind v1 = floor(v / 256.0)
bind b1 = v1 % 256.0 bind v2 = floor(v1 / 256.0)
bind b2 = v2 % 256.0 bind b3 = floor(v2 / 256.0) % 256.0
bind b = list_push(bytes, b0) bind b = list_push(b, b1) bind b = list_push(b, b2)
return list_push(b, b3)
}
fn gl_u16_bytes(bytes, v) {
bind b = list_push(bytes, v % 256.0)
return list_push(b, floor(v / 256.0) % 256.0)
}
fn gl_pad4(bytes, fillv) {
bind b = bytes
while len(b) % 4.0 != 0.0 { bind b = list_push(b, fillv) }
return b
}
fn gl_b64(bytes) {
bind al = gl_b64_alpha()
bind out = list_new()
bind n = len(bytes)
bind i = 0.0
while i < n {
bind a = list_get(bytes, i)
bind b = 0.0 bind c = 0.0
if i + 1.0 < n { bind b = list_get(bytes, i + 1.0) }
if i + 2.0 < n { bind c = list_get(bytes, i + 2.0) }
bind t = a * 65536.0 + b * 256.0 + c
bind s = str_slice(al, floor(t / 262144.0), 1.0) + str_slice(al, floor(t / 4096.0) % 64.0, 1.0)
bind s = s + sel(i + 1.0 < n, str_slice(al, floor(t / 64.0) % 64.0, 1.0), "=")
bind s = s + sel(i + 2.0 < n, str_slice(al, t % 64.0, 1.0), "=")
bind out = list_push(out, s)
bind i = i + 3.0
}
return p_join(out, "")
}
# ASCII text -> bytes (characters are split natively: each is tagged with a separator, then str_split)
fn gl_text_bytes(text) {
bind table = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
bind sep = "¦"
bind t = text
bind i = 0.0
while i < str_len(table) { bind ch = str_slice(table, i, 1.0) bind t = str_replace(t, ch, ch + sep) bind i = i + 1.0 }
bind t = str_replace(t, "\n", "\n" + sep)
bind parts = str_split(t, sep)
bind out = list_new()
bind i = 0.0
while i < len(parts) {
bind ch = list_get(parts, i)
if ch != "" {
if ch == "\n" { bind out = list_push(out, 10.0) }
else { bind k = str_find(table, ch) bind out = list_push(out, sel(k >= 0.0, k + 32.0, 63.0)) }
}
bind i = i + 1.0
}
return out
}
# base64 -> bytes (same native split trick)
fn gl_unb64(s) {
bind al = gl_b64_alpha()
bind sep = "¦"
bind t = str_replace(str_replace(str_replace(s, "\n", ""), "\r", ""), "=", "")
bind i = 0.0
while i < 64.0 { bind ch = str_slice(al, i, 1.0) bind t = str_replace(t, ch, ch + sep) bind i = i + 1.0 }
bind parts = str_split(t, sep)
bind vals = list_new()
bind i = 0.0
while i < len(parts) { bind ch = list_get(parts, i) if ch != "" { bind vals = list_push(vals, str_find(al, ch)) } bind i = i + 1.0 }
bind out = list_new()
bind i = 0.0
while i + 1.0 < len(vals) {
bind a = list_get(vals, i) bind b = list_get(vals, i + 1.0)
bind c = 0.0 bind d = 0.0
bind has_c = i + 2.0 < len(vals) bind has_d = i + 3.0 < len(vals)
if has_c { bind c = list_get(vals, i + 2.0) }
if has_d { bind d = list_get(vals, i + 3.0) }
bind t3 = a * 262144.0 + b * 4096.0 + c * 64.0 + d
bind out = list_push(out, floor(t3 / 65536.0))
if has_c { bind out = list_push(out, floor(t3 / 256.0) % 256.0) }
if has_d { bind out = list_push(out, t3 % 256.0) }
bind i = i + 4.0
}
return out
}
fn gl_rd_f32(bytes, p) {
bind b0 = list_get(bytes, p) bind b1 = list_get(bytes, p + 1.0) bind b2 = list_get(bytes, p + 2.0) bind b3 = list_get(bytes, p + 3.0)
bind sign = sel(b3 >= 128.0, 0.0 - 1.0, 1.0)
bind e = (b3 % 128.0) * 2.0 + floor(b2 / 128.0)
bind frac = (b2 % 128.0) * 65536.0 + b1 * 256.0 + b0
if e == 0.0 { return sign * frac / 8388608.0 * pow(2.0, 0.0 - 126.0) }
if e == 255.0 { return 0.0 }
return sign * (1.0 + frac / 8388608.0) * pow(2.0, e - 127.0)
}
fn gl_rd(bytes, p, ctype) {
if ctype == 5126.0 { return gl_rd_f32(bytes, p) }
if ctype == 5123.0 { return list_get(bytes, p) + list_get(bytes, p + 1.0) * 256.0 }
if ctype == 5125.0 { return list_get(bytes, p) + list_get(bytes, p + 1.0) * 256.0 + list_get(bytes, p + 2.0) * 65536.0 + list_get(bytes, p + 3.0) * 16777216.0 }
if ctype == 5121.0 { return list_get(bytes, p) }
if ctype == 5122.0 { bind v = list_get(bytes, p) + list_get(bytes, p + 1.0) * 256.0 if v >= 32768.0 { return v - 65536.0 } return v }
if ctype == 5120.0 { bind v = list_get(bytes, p) if v >= 128.0 { return v - 256.0 } return v }
return 0.0
}
fn gl_csize(ctype) { if ctype == 5126.0 || ctype == 5125.0 { return 4.0 } if ctype == 5123.0 || ctype == 5122.0 { return 2.0 } return 1.0 }
fn gl_ncomp(t) {
if t == "SCALAR" { return 1.0 }
if t == "VEC2" { return 2.0 }
if t == "VEC3" { return 3.0 }
if t == "VEC4" { return 4.0 }
if t == "MAT4" { return 16.0 }
return 1.0
}
# ── export ───────────────────────────────────────────────────────────────────
# the writer appends to st lists: gw_bin (bytes), gw_views, gw_acc (JSON strings)
fn gw_view(start, length, target) {
bind s = "{\"buffer\":0,\"byteOffset\":" + num_str(start) + ",\"byteLength\":" + num_str(length) + sel(target > 0.0, ",\"target\":" + num_str(target), "") + "}"
st_lpush("gw_views", s)
return st_llen("gw_views") - 1.0
}
# add an accessor from a flat number list. kind: "f" float, "u16", "u32". returns accessor index
fn gw_acc(vals, kind, type, target, minmax) {
bind bin = gl_pad4(st_get("gw_bin", []), 0.0)
bind start = len(bin)
bind i = 0.0
while i < len(vals) {
bind v = list_get(vals, i)
if kind == "f" { bind bin = gl_f32_bytes(bin, v) }
else if kind == "u16" { bind bin = gl_u16_bytes(bin, v) }
else { bind bin = gl_u32_bytes(bin, v) }
bind i = i + 1.0
}
st_set("gw_bin", bin)
bind view = gw_view(start, len(bin) - start, target)
bind nc = gl_ncomp(type)
bind ct = sel(kind == "f", 5126.0, sel(kind == "u16", 5123.0, 5125.0))
bind s = "{\"bufferView\":" + num_str(view) + ",\"componentType\":" + num_str(ct) + ",\"count\":" + num_str(len(vals) / nc) + ",\"type\":\"" + type + "\""
if minmax {
bind mn = list_new() bind mx = list_new()
bind c = 0.0
while c < nc {
bind lo = 1000000000.0 bind hi = 0.0 - 1000000000.0
bind i = c
while i < len(vals) { bind v = list_get(vals, i) if v < lo { bind lo = v } if v > hi { bind hi = v } bind i = i + nc }
bind mn = list_push(mn, mx_num(lo)) bind mx = list_push(mx, mx_num(hi))
bind c = c + 1.0
}
bind s = s + ",\"min\":[" + p_join(mn, ",") + "],\"max\":[" + p_join(mx, ",") + "]"
}
st_lpush("gw_acc", s + "}")
return st_llen("gw_acc") - 1.0
}
fn gl_mat4_cols(m) {
# 3x4 row-major -> column-major 4x4 list
return [list_get(m, 0), list_get(m, 4), list_get(m, 8), 0.0, list_get(m, 1), list_get(m, 5), list_get(m, 9), 0.0, list_get(m, 2), list_get(m, 6), list_get(m, 10), 0.0, list_get(m, 3), list_get(m, 7), list_get(m, 11), 1.0]
}
fn gl_trs_json(m) {
bind t = [list_get(m, 3), list_get(m, 7), list_get(m, 11)]
bind sx = v3_len([list_get(m, 0), list_get(m, 4), list_get(m, 8)])
bind sy = v3_len([list_get(m, 1), list_get(m, 5), list_get(m, 9)])
bind sz = v3_len([list_get(m, 2), list_get(m, 6), list_get(m, 10)])
bind q = m_to_quat(m)
return "\"translation\":" + js_numlist(t) + ",\"rotation\":" + js_numlist(q) + ",\"scale\":" + js_numlist([sx, sy, sz])
}
fn gl_name(s) {
# ASCII-only names keep the binary JSON chunk simple
bind ok = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-. "
bind out = ""
bind i = 0.0
while i < str_len(s) { bind c = str_slice(s, i, 1.0) if str_find(ok, c) >= 0.0 { bind out = out + c } else { bind out = out + "_" } bind i = i + 1.0 }
return out
}
fn gl_export(path, binary) {
st_set("gw_bin", []) st_set("gw_views", []) st_set("gw_acc", [])
bind dir = fs_parent(path)
bind nodes = list_new()
bind meshes = list_new()
bind mats = list_new()
bind skins = list_new()
bind images = list_new()
bind textures = list_new()
bind anims = list_new()
bind roots = list_new()
bind ids = me_ids()
bind fps = tl_fps()
bind j = 0.0
while j < len(ids) {
bind o = list_get(ids, j)
if st_get(mk(o, "vis"), 1.0) > 0.5 && me_nfaces(o) > 0.5 {
bind name = gl_name(me_name(o))
bind vx = st_get(mk(o, "vx"), []) bind vy = st_get(mk(o, "vy"), []) bind vz = st_get(mk(o, "vz"), [])
bind fi = st_get(mk(o, "fi"), []) bind fs = st_get(mk(o, "fs"), []) bind fc = st_get(mk(o, "fc"), [])
bind uv = st_get(mk(o, "uv"), []) bind fcol = st_get(mk(o, "fcol"), [])
bind wb = st_get(mk(o, "wb"), [])
bind skinned = st_get(mk(o, "bound"), 0.0) > 0.5 && rg_count(o) > 0.5 && len(wb) >= len(vx) * 8.0
bind smooth = st_get(mk(o, "smooth"), 0.0) > 0.5
bind vn = me_vertex_normals(o)
bind has_fcol = false
bind q = 0.0
while q < len(fcol) { if list_get(fcol, q) >= 0.0 { bind has_fcol = true } bind q = q + 1.0 }
# unwelded corners: position, normal, uv, colour, joints, weights
bind P = list_new() bind N = list_new() bind T = list_new() bind C = list_new() bind J = list_new() bind W = list_new() bind I = list_new()
bind base = st_get(mk(o, "col"), 13421772.0)
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 fn3 = v3_norm(me_face_normal(vx, vy, vz, fi, s, c))
bind col = base
if f < len(fcol) { if list_get(fcol, f) >= 0.0 { bind col = list_get(fcol, f) } }
bind first = len(P) / 3.0
bind k = 0.0
while k < c {
bind v = list_get(fi, s + k)
bind P = list_push(P, list_get(vx, v)) bind P = list_push(P, list_get(vy, v)) bind P = list_push(P, list_get(vz, v))
if smooth { bind N = list_push(N, list_get(list_get(vn, 0), v)) bind N = list_push(N, list_get(list_get(vn, 1), v)) bind N = list_push(N, list_get(list_get(vn, 2), v)) }
else { bind N = list_push(N, list_get(fn3, 0)) bind N = list_push(N, list_get(fn3, 1)) bind N = list_push(N, list_get(fn3, 2)) }
bind T = list_push(T, list_get(uv, (s + k) * 2.0)) bind T = list_push(T, 1.0 - list_get(uv, (s + k) * 2.0 + 1.0))
if has_fcol { bind C = list_push(C, c_r(col) / 255.0) bind C = list_push(C, c_g(col) / 255.0) bind C = list_push(C, c_b(col) / 255.0) bind C = list_push(C, 1.0) }
if skinned {
bind ws = 0.0
bind q = 0.0
while q < 4.0 { bind ws = ws + max(0.0, list_get(wb, v * 8.0 + q * 2.0 + 1.0)) bind q = q + 1.0 }
bind q = 0.0
while q < 4.0 {
bind bb = list_get(wb, v * 8.0 + q * 2.0) bind ww = list_get(wb, v * 8.0 + q * 2.0 + 1.0)
bind J = list_push(J, max(0.0, bb))
bind W = list_push(W, sel(bb >= 0.0 && ws > 0.0, ww / max(0.000001, ws), sel(q == 0.0 && ws <= 0.0, 1.0, 0.0)))
bind q = q + 1.0
}
}
bind k = k + 1.0
}
bind k = 1.0
while k + 1.0 < c { bind I = list_push(I, first) bind I = list_push(I, first + k) bind I = list_push(I, first + k + 1.0) bind k = k + 1.0 }
}
bind f = f + 1.0
}
if len(I) > 2.5 {
bind aP = gw_acc(P, "f", "VEC3", 34962.0, true)
bind aN = gw_acc(N, "f", "VEC3", 34962.0, false)
bind aT = gw_acc(T, "f", "VEC2", 34962.0, false)
bind attrs = "\"POSITION\":" + num_str(aP) + ",\"NORMAL\":" + num_str(aN) + ",\"TEXCOORD_0\":" + num_str(aT)
if has_fcol { bind attrs = attrs + ",\"COLOR_0\":" + num_str(gw_acc(C, "f", "VEC4", 34962.0, false)) }
if skinned { bind attrs = attrs + ",\"JOINTS_0\":" + num_str(gw_acc(J, "u16", "VEC4", 34962.0, false)) + ",\"WEIGHTS_0\":" + num_str(gw_acc(W, "f", "VEC4", 34962.0, false)) }
bind aI = gw_acc(I, sel(len(P) / 3.0 < 65535.0, "u16", "u32"), "SCALAR", 34963.0, false)
# material
bind mcol = sel(has_fcol, 16777215.0, base)
bind mjs = "{\"name\":\"" + name + "_mat\",\"pbrMetallicRoughness\":{\"baseColorFactor\":" + js_numlist([c_r(mcol) / 255.0, c_g(mcol) / 255.0, c_b(mcol) / 255.0, 1.0]) + ",\"metallicFactor\":0,\"roughnessFactor\":0.8"
bind tex = st_get(mk(o, "tex"), 0.0 - 1.0)
if tex >= 0.0 && p_has_images() {
bind tfile = lf_ident(name) + "_tex.png"
bind img = 0.0 - 1.0
if st_get(tk(tx_id(tex), "type"), "") == "image" { bind img = st_get(tk(tx_id(tex), "img"), 0.0 - 1.0) }
else { bind sz = st_get(tk(tx_id(tex), "size"), 128.0) bind img = tx_render_image(tex, sz, sz, 0.0) }
if img >= 0.0 {
p_img_save(img, fs_join(dir, tfile))
bind images = list_push(images, "{\"uri\":\"" + tfile + "\"}")
bind textures = list_push(textures, "{\"source\":" + num_str(len(images) - 1.0) + "}")
bind mjs = mjs + ",\"baseColorTexture\":{\"index\":" + num_str(len(textures) - 1.0) + "}"
}
}
bind mats = list_push(mats, mjs + "},\"doubleSided\":false}")
bind meshes = list_push(meshes, "{\"name\":\"" + name + "\",\"primitives\":[{\"attributes\":{" + attrs + "},\"indices\":" + num_str(aI) + ",\"material\":" + num_str(len(mats) - 1.0) + ",\"mode\":4}]}")
# nodes: an object node (transform) with the mesh node and the root bones as children
bind objnode = len(nodes)
bind nodes = list_push(nodes, "")
bind meshnode = len(nodes)
bind nodes = list_push(nodes, "")
bind children = [meshnode]
bind mesh_json = "{\"name\":\"" + name + "_mesh\",\"mesh\":" + num_str(len(meshes) - 1.0)
if skinned {
bind nb = rg_count(o)
bind rest = rg_rest_matrices(o)
bind par = st_get(mk(o, "bn_par"), [])
bind first_bone = len(nodes)
bind b = 0.0
while b < nb { bind nodes = list_push(nodes, "") bind b = b + 1.0 }
bind ibm = list_new()
bind joints = list_new()
bind b = 0.0
while b < nb {
bind R = rg_mat_at(rest, b)
bind p = list_get(par, b)
bind L = R
if p >= 0.0 { bind L = m_mul(m_inv(rg_mat_at(rest, p)), R) }
bind kids = list_new()
bind c2 = 0.0
while c2 < nb { if list_get(par, c2) == b { bind kids = list_push(kids, num_str(first_bone + c2)) } bind c2 = c2 + 1.0 }
bind bj = "{\"name\":\"" + gl_name(rg_bone_name(o, b)) + "\"," + gl_trs_json(L)
if len(kids) > 0.5 { bind bj = bj + ",\"children\":[" + p_join(kids, ",") + "]" }
bind nodes = list_set(nodes, first_bone + b, bj + "}")
if p < 0.0 { bind children = list_push(children, first_bone + b) }
bind iv = gl_mat4_cols(m_inv(R))
bind q = 0.0
while q < 16.0 { bind ibm = list_push(ibm, list_get(iv, q)) bind q = q + 1.0 }
bind joints = list_push(joints, num_str(first_bone + b))
bind b = b + 1.0
}
bind aIBM = gw_acc(ibm, "f", "MAT4", 0.0, false)
bind skins = list_push(skins, "{\"name\":\"" + name + "_skin\",\"inverseBindMatrices\":" + num_str(aIBM) + ",\"joints\":[" + p_join(joints, ",") + "]}")
bind mesh_json = mesh_json + ",\"skin\":" + num_str(len(skins) - 1.0)
# animations: every action, sampled per frame
bind acts = st_get(mk(o, "act_names"), [])
bind keep_cur = st_get(mk(o, "act_cur"), 0.0 - 1.0)
bind save = gl_pose_save(o)
bind a = 0.0
while a < len(acts) {
st_set(mk(o, "act_cur"), a)
bind keys = ps_action_keys(o)
if len(keys) > 3.5 {
bind f0 = 1000000.0 bind f1 = 0.0 - 1000000.0
bind q = 0.0
while q + 3.0 < len(keys) { bind kf = list_get(keys, q + 2.0) bind f0 = min(f0, kf) bind f1 = max(f1, kf) bind q = q + 4.0 }
bind times = list_new()
bind f = f0
while f <= f1 + 0.001 { bind times = list_push(times, (f - f0) / fps) bind f = f + 1.0 }
bind aTime = gw_acc(times, "f", "SCALAR", 0.0, true)
bind samplers = list_new() bind channels = list_new()
bind b = 0.0
while b < nb {
bind TT = list_new() bind RR = list_new() bind SS = list_new()
bind f = f0
while f <= f1 + 0.001 {
ps_eval(o, f)
bind X = m_trs(list_get(st_get(mk(o, "bp_tx"), []), b), list_get(st_get(mk(o, "bp_ty"), []), b), list_get(st_get(mk(o, "bp_tz"), []), b), list_get(st_get(mk(o, "bp_rx"), []), b), list_get(st_get(mk(o, "bp_ry"), []), b), list_get(st_get(mk(o, "bp_rz"), []), b), list_get(st_get(mk(o, "bp_sx"), []), b), list_get(st_get(mk(o, "bp_sy"), []), b), list_get(st_get(mk(o, "bp_sz"), []), b))
bind R = rg_mat_at(rest, b)
bind p = list_get(par, b)
bind L = R
if p >= 0.0 { bind L = m_mul(m_inv(rg_mat_at(rest, p)), R) }
bind M = m_mul(L, X)
bind TT = list_push(TT, list_get(M, 3)) bind TT = list_push(TT, list_get(M, 7)) bind TT = list_push(TT, list_get(M, 11))
bind qq = m_to_quat(M)
bind q = 0.0
while q < 4.0 { bind RR = list_push(RR, list_get(qq, q)) bind q = q + 1.0 }
bind SS = list_push(SS, v3_len([list_get(M, 0), list_get(M, 4), list_get(M, 8)])) bind SS = list_push(SS, v3_len([list_get(M, 1), list_get(M, 5), list_get(M, 9)])) bind SS = list_push(SS, v3_len([list_get(M, 2), list_get(M, 6), list_get(M, 10)]))
bind f = f + 1.0
}
bind paths = ["translation", "rotation", "scale"]
bind datas = [TT, RR, SS]
bind types = ["VEC3", "VEC4", "VEC3"]
bind q = 0.0
while q < 3.0 {
bind aOut = gw_acc(list_get(datas, q), "f", list_get(types, q), 0.0, false)
bind samplers = list_push(samplers, "{\"input\":" + num_str(aTime) + ",\"output\":" + num_str(aOut) + ",\"interpolation\":\"LINEAR\"}")
bind channels = list_push(channels, "{\"sampler\":" + num_str(len(samplers) - 1.0) + ",\"target\":{\"node\":" + num_str(first_bone + b) + ",\"path\":\"" + list_get(paths, q) + "\"}}")
bind q = q + 1.0
}
bind b = b + 1.0
}
bind anims = list_push(anims, "{\"name\":\"" + gl_name(list_get(acts, a)) + "\",\"samplers\":[" + p_join(samplers, ",") + "],\"channels\":[" + p_join(channels, ",") + "]}")
}
bind a = a + 1.0
}
st_set(mk(o, "act_cur"), keep_cur)
gl_pose_restore(o, save)
}
bind nodes = list_set(nodes, meshnode, mesh_json + "}")
bind cl = list_new()
bind q = 0.0
while q < len(children) { bind cl = list_push(cl, num_str(list_get(children, q))) bind q = q + 1.0 }
bind nodes = list_set(nodes, objnode, "{\"name\":\"" + name + "\"," + gl_trs_json(me_matrix(o)) + ",\"children\":[" + p_join(cl, ",") + "]}")
bind roots = list_push(roots, num_str(objnode))
}
}
bind j = j + 1.0
}
bind bin = gl_pad4(st_get("gw_bin", []), 0.0)
bind buf = "{\"byteLength\":" + num_str(len(bin))
if !binary { bind buf = buf + ",\"uri\":\"data:application/octet-stream;base64," + gl_b64(bin) + "\"" }
bind buf = buf + "}"
bind js = "{\"asset\":{\"version\":\"2.0\",\"generator\":\"LingModeler " + APP_VERSION + "\"},\"scene\":0,\"scenes\":[{\"name\":\"Scene\",\"nodes\":[" + p_join(roots, ",") + "]}]"
bind js = js + ",\"nodes\":[" + p_join(nodes, ",") + "]"
bind js = js + ",\"meshes\":[" + p_join(meshes, ",") + "]"
bind js = js + ",\"materials\":[" + p_join(mats, ",") + "]"
if len(images) > 0.5 { bind js = js + ",\"images\":[" + p_join(images, ",") + "],\"textures\":[" + p_join(textures, ",") + "],\"samplers\":[{}]" }
if len(skins) > 0.5 { bind js = js + ",\"skins\":[" + p_join(skins, ",") + "]" }
if len(anims) > 0.5 { bind js = js + ",\"animations\":[" + p_join(anims, ",") + "]" }
bind js = js + ",\"buffers\":[" + buf + "],\"bufferViews\":[" + p_join(st_get("gw_views", []), ",") + "],\"accessors\":[" + p_join(st_get("gw_acc", []), ",") + "]}"
if len(meshes) < 0.5 { st_set("status", "Nothing to export (no visible meshes)") return false }
if !binary {
write_file(path, js)
} else {
if !p_has_images() { st_set("status", "GLB needs the desktop build (binary files)") return false }
bind jb = gl_pad4(gl_text_bytes(js), 32.0)
bind out = list_new()
bind out = gl_u32_bytes(out, 1179937895.0)
bind out = gl_u32_bytes(out, 2.0)
bind out = gl_u32_bytes(out, 12.0 + 8.0 + len(jb) + 8.0 + len(bin))
bind out = gl_u32_bytes(out, len(jb))
bind out = gl_u32_bytes(out, 1313821514.0)
bind i = 0.0
while i < len(jb) { bind out = list_push(out, list_get(jb, i)) bind i = i + 1.0 }
bind out = gl_u32_bytes(out, len(bin))
bind out = gl_u32_bytes(out, 5130562.0)
bind i = 0.0
while i < len(bin) { bind out = list_push(out, list_get(bin, i)) bind i = i + 1.0 }
p_write_bytes(path, out)
}
st_set("gw_bin", [])
return true
}
fn gl_pose_save(o) {
bind out = list_new()
bind keys = ["bp_rx", "bp_ry", "bp_rz", "bp_tx", "bp_ty", "bp_tz", "bp_sx", "bp_sy", "bp_sz"]
bind i = 0.0
while i < 9.0 { bind out = list_push(out, st_get(mk(o, list_get(keys, i)), [])) bind i = i + 1.0 }
return out
}
fn gl_pose_restore(o, save) {
bind keys = ["bp_rx", "bp_ry", "bp_rz", "bp_tx", "bp_ty", "bp_tz", "bp_sx", "bp_sy", "bp_sz"]
bind i = 0.0
while i < 9.0 { st_set(mk(o, list_get(keys, i)), list_get(save, i)) bind i = i + 1.0 }
ps_mark(o)
}
# ── import (.gltf with embedded or data-URI buffers) ─────────────────────────
fn gl_accessor(root, ai) {
bind acc = js_idx(js_get(root, "accessors"), ai)
if acc < 0.0 { return [] }
bind vi = js_get_num(acc, "bufferView", 0.0 - 1.0)
bind count = js_get_num(acc, "count", 0.0)
bind ctype = js_get_num(acc, "componentType", 5126.0)
bind nc = gl_ncomp(js_get_str(acc, "type", "SCALAR"))
bind norm = js_get_num(acc, "normalized", 0.0) > 0.5
bind out = list_new()
if vi < 0.0 { return me_zeros(count * nc) }
bind view = js_idx(js_get(root, "bufferViews"), vi)
bind bi = js_get_num(view, "buffer", 0.0)
bind bytes = st_get("gr_buf" + num_str(bi), [])
bind off = js_get_num(view, "byteOffset", 0.0) + js_get_num(acc, "byteOffset", 0.0)
bind cs = gl_csize(ctype)
bind stride = js_get_num(view, "byteStride", 0.0)
if stride < 0.5 { bind stride = cs * nc }
bind i = 0.0
while i < count {
bind c = 0.0
while c < nc {
bind p = off + i * stride + c * cs
bind v = 0.0
if p + cs <= len(bytes) { bind v = gl_rd(bytes, p, ctype) }
if norm { if ctype == 5121.0 { bind v = v / 255.0 } if ctype == 5123.0 { bind v = v / 65535.0 } }
bind out = list_push(out, v)
bind c = c + 1.0
}
bind i = i + 1.0
}
return out
}
fn gl_node_matrix(node) {
bind m = js_get(node, "matrix")
if m >= 0.0 {
bind v = js_nums(m)
if len(v) > 15.5 { return [list_get(v, 0), list_get(v, 4), list_get(v, 8), list_get(v, 12), list_get(v, 1), list_get(v, 5), list_get(v, 9), list_get(v, 13), list_get(v, 2), list_get(v, 6), list_get(v, 10), list_get(v, 14)] }
}
bind t = js_nums(js_get(node, "translation"))
bind r = js_nums(js_get(node, "rotation"))
bind s = js_nums(js_get(node, "scale"))
bind M = m_ident()
if len(s) > 2.5 { bind M = m_scale(list_get(s, 0), list_get(s, 1), list_get(s, 2)) }
if len(r) > 3.5 { bind M = m_mul(q_to_m(r), M) }
if len(t) > 2.5 { bind M = m_mul(m_trans(list_get(t, 0), list_get(t, 1), list_get(t, 2)), M) }
return M
}
fn gl_import(path) {
if !file_exists(path) { return false }
bind root = js_parse(read_file(path))
if root < 0.0 { st_set("status", "Not valid JSON: " + path) return false }
# buffers
bind bufs = js_get(root, "buffers")
bind i = 0.0
while i < js_len(bufs) {
bind uri = js_get_str(js_idx(bufs, i), "uri", "")
bind c = str_find(uri, "base64,")
if c >= 0.0 { st_set("gr_buf" + num_str(i), gl_unb64(str_slice(uri, c + 7.0, str_len(uri) - c - 7.0))) }
else {
st_set("status", "External buffer '" + uri + "' cannot be read here - re-export as embedded .gltf, or convert with `ling convert`")
return false
}
bind i = i + 1.0
}
# world matrices of nodes (scene roots down)
bind nodes = js_get(root, "nodes")
bind nn = js_len(nodes)
bind world = me_zeros(nn * 12.0)
bind done = me_zeros(nn)
bind parent = me_fill(nn, 0.0 - 1.0)
bind i = 0.0
while i < nn {
bind kids = js_get(js_idx(nodes, i), "children")
bind k = 0.0
while k < js_len(kids) { bind parent = list_set(parent, js_num(js_idx(kids, k)), i) bind k = k + 1.0 }
bind i = i + 1.0
}
bind guard = 0.0
bind remaining = nn
while remaining > 0.5 && guard < nn + 2.0 {
bind i = 0.0
while i < nn {
if list_get(done, i) < 0.5 {
bind p = list_get(parent, i)
bind ready = p < 0.0
if p >= 0.0 { if list_get(done, p) > 0.5 { bind ready = true } }
if ready {
bind L = gl_node_matrix(js_idx(nodes, i))
bind W = L
if p >= 0.0 { bind W = m_mul(rg_mat_at(world, p), L) }
bind q = 0.0
while q < 12.0 { bind world = list_set(world, i * 12.0 + q, list_get(W, q)) bind q = q + 1.0 }
bind done = list_set(done, i, 1.0)
bind remaining = remaining - 1.0
}
}
bind i = i + 1.0
}
bind guard = guard + 1.0
}
bind meshes = js_get(root, "meshes")
bind mats = js_get(root, "materials")
bind skins = js_get(root, "skins")
bind made = 0.0
bind i = 0.0
while i < nn {
bind node = js_idx(nodes, i)
bind mi = js_get_num(node, "mesh", 0.0 - 1.0)
if mi >= 0.0 {
bind mesh = js_idx(meshes, mi)
bind prims = js_get(mesh, "primitives")
bind pi = 0.0
while pi < js_len(prims) {
bind prim = js_idx(prims, pi)
bind mode = js_get_num(prim, "mode", 4.0)
bind attrs = js_get(prim, "attributes")
bind pos = gl_accessor(root, js_get_num(attrs, "POSITION", 0.0 - 1.0))
bind nv = len(pos) / 3.0
if mode == 4.0 && nv > 2.5 {
bind vx = list_new() bind vy = list_new() bind vz = list_new()
bind k = 0.0
while k < nv { bind vx = list_push(vx, list_get(pos, k * 3.0)) bind vy = list_push(vy, list_get(pos, k * 3.0 + 1.0)) bind vz = list_push(vz, list_get(pos, k * 3.0 + 2.0)) bind k = k + 1.0 }
bind idx = list_new()
if js_get(prim, "indices") >= 0.0 { bind idx = gl_accessor(root, js_get_num(prim, "indices", 0.0)) }
else { bind k = 0.0 while k < nv { bind idx = list_push(idx, k) bind k = k + 1.0 } }
bind tc = list_new()
if js_get(attrs, "TEXCOORD_0") >= 0.0 { bind tc = gl_accessor(root, js_get_num(attrs, "TEXCOORD_0", 0.0)) }
bind fi = list_new() bind fs = list_new() bind fc = list_new() bind uv = list_new()
bind t = 0.0
while t + 2.0 < len(idx) {
bind fs = list_push(fs, len(fi)) bind fc = list_push(fc, 3.0)
bind q = 0.0
while q < 3.0 {
bind v = list_get(idx, t + q)
bind fi = list_push(fi, v)
if len(tc) > v * 2.0 + 1.0 { bind uv = list_push(uv, list_get(tc, v * 2.0)) bind uv = list_push(uv, 1.0 - list_get(tc, v * 2.0 + 1.0)) }
else { bind uv = list_push(uv, 0.0) bind uv = list_push(uv, 0.0) }
bind q = q + 1.0
}
bind t = t + 3.0
}
bind nm = js_get_str(node, "name", js_get_str(mesh, "name", path_stem(path)))
if str_ends_with(nm, "_mesh") { bind nm = str_slice(nm, 0.0, str_len(nm) - 5.0) }
bind o = me_add_object(nm, vx, vy, vz, fi, fs, fc)
st_set(mk(o, "uv"), uv)
# material colour
bind mat = js_idx(mats, js_get_num(prim, "material", 0.0 - 1.0))
if mat >= 0.0 {
bind bc = js_nums(js_get(js_get(mat, "pbrMetallicRoughness"), "baseColorFactor"))
if len(bc) > 2.5 { st_set(mk(o, "col"), c_rgb(list_get(bc, 0) * 255.0, list_get(bc, 1) * 255.0, list_get(bc, 2) * 255.0)) }
}
# skinning: bones from joint nodes (object space), weights from JOINTS_0 / WEIGHTS_0
bind skin_i = js_get_num(node, "skin", 0.0 - 1.0)
bind W = rg_mat_at(world, i)
bind has_j = js_get(attrs, "JOINTS_0")
if skin_i >= 0.0 && has_j >= 0.0 {
bind skin = js_idx(skins, skin_i)
bind joints = js_nums(js_get(skin, "joints"))
bind Winv = m_inv(W)
bind jn = len(joints)
bind b = 0.0
while b < jn {
bind jnode = list_get(joints, b)
bind JW = m_mul(Winv, rg_mat_at(world, jnode))
bind head = m_apply(JW, [0.0, 0.0, 0.0])
# tail: first child joint, else 10% of the scene along local Y
bind tail = m_apply(JW, [0.0, 0.1, 0.0])
bind kids = js_get(js_idx(nodes, jnode), "children")
if js_len(kids) > 0.5 { bind tail = m_apply(Winv, m_apply(rg_mat_at(world, js_num(js_idx(kids, 0))), [0.0, 0.0, 0.0])) }
if v3_dist(head, tail) < 0.0001 { bind tail = v3_add(head, [0.0, 0.05, 0.0]) }
bind pb = 0.0 - 1.0
bind q = 0.0
while q < jn { if list_get(joints, q) == list_get(parent, jnode) { bind pb = q } bind q = q + 1.0 }
rg_push_bone(o, js_get_str(js_idx(nodes, jnode), "name", "Bone"), pb, head, tail)
bind b = b + 1.0
}
bind JJ = gl_accessor(root, js_get_num(attrs, "JOINTS_0", 0.0))
bind WW = gl_accessor(root, js_get_num(attrs, "WEIGHTS_0", 0.0))
bind wb = list_new()
bind v = 0.0
while v < nv {
bind q = 0.0
while q < 4.0 {
bind ww = 0.0
if v * 4.0 + q < len(WW) { bind ww = list_get(WW, v * 4.0 + q) }
bind bb = 0.0 - 1.0
if v * 4.0 + q < len(JJ) { bind bb = list_get(JJ, v * 4.0 + q) }
bind wb = list_push(wb, sel(ww > 0.0, bb, 0.0 - 1.0)) bind wb = list_push(wb, ww)
bind q = q + 1.0
}
bind v = v + 1.0
}
st_set(mk(o, "wb"), wb)
st_set(mk(o, "bound"), 1.0)
}
# object transform from the node's world matrix
bind e = m_to_euler(W)
st_set(mk(o, "px"), list_get(W, 3)) st_set(mk(o, "py"), list_get(W, 7)) st_set(mk(o, "pz"), list_get(W, 11))
st_set(mk(o, "rx"), list_get(e, 0)) st_set(mk(o, "ry"), list_get(e, 1)) st_set(mk(o, "rz"), list_get(e, 2))
st_set(mk(o, "sx"), v3_len([list_get(W, 0), list_get(W, 4), list_get(W, 8)]))
st_set(mk(o, "sy"), v3_len([list_get(W, 1), list_get(W, 5), list_get(W, 9)]))
st_set(mk(o, "sz"), v3_len([list_get(W, 2), list_get(W, 6), list_get(W, 10)]))
mo_merge_dist(o, 0.000001)
mo_quads(o)
st_set(mk(o, "osel"), 0.0)
bind made = made + 1.0
}
bind pi = pi + 1.0
}
}
bind i = i + 1.0
}
bind i = 0.0
while i < js_len(bufs) { st_set("gr_buf" + num_str(i), []) bind i = i + 1.0 }
js_reset()
return made > 0.5
}