灵符lingfu

lingmodeler

↓ 0 downloads

LingModeler: 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

Windows · x86-64Linux · x86-64LingOS · x86-64LingOS · Pi 5 ARM64Web · WASM
Keywords 3d, modeling, vector, svg, gltf, obj, rigging, animation, textures, ling
lingfu install lingmodeler
LicenseApache-2.0 OR MIT
Authorsling-lin
READMECode / FilesVersionsDependency GraphDependentsSecurityBuildsSettings

Code / 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
}

← Back to index