lingdev
↓ 0 downloadsLingDev: a Godot / Unity-style game editor written in Ling. 2-D and 3-D scenes, Flash-style timeline and cutscenes, HUDs over 3-D, Ling scripting, audio / music / MIDI, builds for Windows, Linux, macOS, web, Raspberry Pi and LingOS. Ships with Ling Agent (an N64-style spy FPS), Ling Breaker (a 3-D breakout) and Moko's Picture Book (a kawaii storybook platformer).
Keywords game-engine, editor, 3d, 2d, cutscene, timeline, ling, lingos, raspberry-pi
lingfu install lingdevCode / Files (136)
ling/rt/rt_draw.ling
# rt_draw.ling - drawing a 3-D scene: with the software renderer (editor viewport, LingOS builds)
# or with the native renderer (desktop / web play mode). Both read the same node rows.
fn nd_colour(i) { return nd_get(i, "col") }
# model handles are cached per path in the store ("mdl:<path>")
fn model_handle(path) {
if path == "" || !plat_has_models() { return 0.0 - 1.0 }
bind key = "mdl:" + path
if st_has(key) > 0.5 { return st_get(key, 0.0 - 1.0) }
bind full = path
if !p_is_abs(full) && st_get("proj_root", "") != "" { bind full = p_join(st_get("proj_root", ""), path) }
bind h = plat_model_load(full)
st_set(key, h)
return h
}
# ── software path ────────────────────────────────────────────────────────────
# editor: gizmos + selection; solid: filled faces for primitives
fn draw_scene_sw(t, solid, editor) {
bind n = nd_count()
bind gizmos = st_get("view_gizmos", 1.0) > 0.5 || !editor
# pass 1: solid faces of the primitives, flushed back to front
if solid {
sw_solid_begin()
bind i = 0.0
while i < n {
bind kind = nd_get(i, "kind")
if nd_get(i, "vis") > 0.5 && (kind == "box" || kind == "sphere" || kind == "cylinder" || kind == "cone" || kind == "capsule" || kind == "torus" || kind == "plane") {
bind wp = nd_world_pos_t(i, t)
sw_shape(kind, list_get(wp, 0), list_get(wp, 1), list_get(wp, 2), nd_val(i, "sx", t), nd_val(i, "sy", t), nd_val(i, "sz", t), nd_val(i, "rx", t), nd_world_yaw_t(i, t), nd_val(i, "rz", t), nd_colour(i), 1.0, true)
}
bind i = i + 1.0
}
sw_solid_flush()
}
# pass 2: wires, gizmos, labels
bind i = 0.0
while i < n {
bind kind = nd_get(i, "kind")
if nd_get(i, "vis") > 0.5 && !nd_is_2d(kind) {
bind wp = nd_world_pos_t(i, t)
bind x = list_get(wp, 0)
bind y = list_get(wp, 1)
bind z = list_get(wp, 2)
bind rx = nd_val(i, "rx", t)
bind ry = nd_world_yaw_t(i, t)
bind rz = nd_val(i, "rz", t)
bind sx = nd_val(i, "sx", t)
bind sy = nd_val(i, "sy", t)
bind sz = nd_val(i, "sz", t)
bind c = nd_colour(i)
bind selected = editor && rt_selected(nd_id(i))
bind a = sel(selected, 1.0, 0.85)
if kind == "box" || kind == "sphere" || kind == "cylinder" || kind == "cone" || kind == "capsule" || kind == "torus" || kind == "plane" {
if !solid || selected || kind == "plane" { sw_shape(kind, x, y, z, sx, sy, sz, rx, ry, rz, sel(selected, C_GIZMO, c), sel(solid, 0.6, a), false) }
} else if kind == "model" {
sw_box(x, y - sy, z, sx, sy, sz, 0.0, ry, 0.0, sel(selected, C_GIZMO, c), 0.8)
sw_line(x, y, z, x + sin(ry) * sx * 1.4, y, z + cos(ry) * sx * 1.4, c, 0.9)
if editor { sw_label(x, y - sy * 2.0, z, 11.0, c, nd_get(i, "name")) }
} else if kind == "actor" {
if nd_ex(i, "shape", "") == "sphere" { sw_sphere(x, y + sy - sx, z, sx, sx, sx, 0.0, ry, 0.0, sel(selected, C_GIZMO, c), 0.95) }
else { sw_actor_gizmo(x, y + sy, z, ry, sy * 2.0, sel(selected, C_GIZMO, c), 0.95) }
if editor { sw_label(x, y - sy, z, 11.0, c, nd_get(i, "name")) }
} else if kind == "text3d" {
sw_label(x, y, z, 12.0 + nd_val(i, "a", t) * 8.0, c, nd_get(i, "txt"))
if selected { sw_point(x, y, z, 3.0, C_GIZMO) }
} else if gizmos {
if kind == "light" { sw_light_gizmo(x, y, z, nd_val(i, "b", t), sel(selected, C_GIZMO, c), 0.9) }
else if kind == "camera" { sw_camera_gizmo(x, y, z, ry, rx, sel(selected, C_GIZMO, c), 0.9) }
else if kind == "spawn" { sw_spawn_gizmo(x, y, z, ry, sel(selected, C_GIZMO, c), 0.95) }
else if kind == "trigger" {
sw_box(x, y, z, sx, sy, sz, 0.0, ry, 0.0, sel(selected, C_GIZMO, c), 0.8)
sw_line(x - sx, y + sy, z - sz, x + sx, y + sy, z + sz, c, 0.3)
sw_line(x + sx, y + sy, z - sz, x - sx, y + sy, z + sz, c, 0.3)
} else if kind == "audio" || kind == "music" || kind == "midi" { sw_audio_gizmo(x, y, z, nd_val(i, "d", t) * 3.0, sel(selected, C_GIZMO, c), 0.9) }
else if kind == "empty" {
sw_axes(x, y, z, 0.6)
if selected { sw_point(x, y, z, 3.0, C_GIZMO) }
}
if editor && (kind == "light" || kind == "camera" || kind == "spawn" || kind == "trigger" || kind == "audio" || kind == "music" || kind == "midi") { sw_label(x, y - sy * 0.5, z, 10.0, c, nd_get(i, "name")) }
}
}
bind i = i + 1.0
}
draw_effects(sel(editor, st_get("now", t), t), editor, false)
}
# ── native path (desktop / web) ──────────────────────────────────────────────
fn draw_scene_native(t) {
bind n = nd_count()
plat_3d_begin(sc_get("amb", 0.25))
plat_depth(to_number(sc_ex("depth", sel(sc_ex("mode", "") == "brick" || sc_ex("mode", "") == "soul" || sc_ex("mode", "") == "story", "1", "0"))))
bind fog = nums_parse(sc_ex("fog", ""))
if len(fog) >= 5.0 { plat_fog(list_get(fog, 0), list_get(fog, 1), list_get(fog, 2), list_get(fog, 3), list_get(fog, 4)) } else { plat_fog(0.0, 0.0, 0.0, 0.0, 0.0) }
# shading: 0 flat, 1 cel (default), 2 holographic; the rim is the Fresnel edge glow (strong rims wash out big planes)
plat_shade(to_number(sc_ex("shade", "1")), to_number(sc_ex("rim", "0.12")), c_parse(sc_ex("rimcol", "115,217,255")))
bind lights = 0.0
bind i = 0.0
while i < n {
if nd_get(i, "kind") == "light" && nd_get(i, "vis") > 0.5 {
bind wp = nd_world_pos_t(i, t)
bind c = nd_colour(i)
plat_light(list_get(wp, 0), list_get(wp, 1), list_get(wp, 2), c_r(c), c_g(c), c_b(c), nd_val(i, "a", t), nd_val(i, "b", t))
bind lights = lights + 1.0
}
bind i = i + 1.0
}
if lights < 0.5 { plat_light(4.0, 0.0 - 8.0, 0.0 - 4.0, 255.0, 245.0, 230.0, 2.5, 30.0) }
fx_lights_apply()
brick_lights()
story_lights()
cam_apply_native()
soul_world_native()
bind i = 0.0
while i < n {
bind kind = nd_get(i, "kind")
bind fps_skip = fps_on() && (kind == "actor" || nd_get(i, "tag") == "pickup")
if brick_on() && (nd_get(i, "tag") == "brick" || nd_get(i, "tag") == "paddle") { bind fps_skip = true }
if soul_on() && (kind == "actor" || nd_get(i, "tag") == "level") { bind fps_skip = true }
if story_on() && l_has(["fruit", "puff", "player", "eggblock", "spring", "flower", "tree", "cloudface"], nd_get(i, "tag")) { bind fps_skip = true }
if story_on() && abs(nd_get(i, "px") - st_get("sy_cam_x", 0.0)) > 22.0 + nd_get(i, "sx") + abs(nd_get(i, "pz")) * 0.6 { bind fps_skip = true }
if nd_get(i, "vis") > 0.5 && !nd_is_2d(kind) && !fps_skip {
bind wp = nd_world_pos_t(i, t)
bind x = list_get(wp, 0)
bind y = list_get(wp, 1)
bind z = list_get(wp, 2)
bind ry = nd_world_yaw_t(i, t)
bind sx = nd_val(i, "sx", t)
bind sy = nd_val(i, "sy", t)
bind sz = nd_val(i, "sz", t)
if kind == "box" || kind == "sphere" || kind == "cylinder" || kind == "cone" || kind == "capsule" || kind == "torus" || kind == "plane" {
plat_color(nd_colour(i))
plat_shape(kind, x, y, z, sx, sy, sz, nd_val(i, "rx", t), ry, nd_val(i, "rz", t), 0.0)
} else if kind == "model" || kind == "actor" {
bind h = model_handle(nd_get(i, "src"))
if h >= 0.0 {
bind hue = to_number(nd_ex(i, "hue", "0"))
if hue != 0.0 { plat_mesh_hue(t * hue + i * 1.257, to_number(nd_ex(i, "gain", "1.1"))) }
plat_model_draw(h, x, y + sel(kind == "actor", sy, 0.0), z, sx, ry)
if hue != 0.0 { plat_mesh_hue(0.0, 1.0) }
} else {
plat_color(nd_colour(i))
if kind == "actor" && nd_ex(i, "shape", "") == "sphere" { plat_shape("sphere", x, y + sy - sx, z, sx, sx, sx, 0.0, ry, 0.0, 0.0) }
else { plat_shape(sel(kind == "actor", "capsule", "box"), x, y, z, sel(kind == "actor", 0.45, sx), sy, sel(kind == "actor", 0.45, sz), 0.0, ry, 0.0, 0.0) }
}
} else if kind == "knot" {
draw_knot_native(i, x, y, z, t)
} else if kind == "swarm" && nd_get(i, "c") == 2.0 {
draw_swarm_native(i, x, y, z, t)
}
}
bind i = i + 1.0
}
if fps_on() { fps_world_native() }
brick_world_native()
story_world_native()
brick_deco_native()
plat_3d_flush()
# labels / text3d on top
bind i = 0.0
while i < n {
if nd_get(i, "kind") == "text3d" && nd_get(i, "vis") > 0.5 {
bind wp = nd_world_pos_t(i, t)
sw_label(list_get(wp, 0), list_get(wp, 1), list_get(wp, 2), 12.0 + nd_val(i, "a", t) * 8.0, nd_colour(i), nd_get(i, "txt"))
}
bind i = i + 1.0
}
}
# the scene the way the player sees it: native when the runtime has it, software otherwise
fn draw_scene_game(t) {
if plat_has_3d() && st_get("force_sw", 0.0) < 0.5 { draw_scene_native(t) draw_effects(st_get("game_t", t), false, true) }
else { draw_scene_sw(t, true, false) draw_effects(st_get("game_t", t), false, false) }
draw_midi_notes(st_get("game_scene_t", t))
}
# ── effect nodes: swarms (stars / wire cubes / orbs) and knotwork chains ─────
fn fx_hash(k, salt) {
bind v = sin(k * 12.9898 + salt * 78.233) * 43758.5453
return v - floor(v)
}
fn fx_hue(h, br) {
return c_rgb((sin(h) * 0.5 + 0.5) * 205.0 * br + 15.0, (sin(h + 2.094) * 0.5 + 0.5) * 205.0 * br + 15.0, (sin(h + 4.189) * 0.5 + 0.5) * 205.0 * br + 15.0)
}
# drawn after the scene (software primitives over either renderer). native_solid: solid parts were already drawn natively
fn draw_effects(t, editor, native_solid) {
bind n = nd_count()
bind i = 0.0
while i < n {
bind kind = nd_get(i, "kind")
if nd_get(i, "vis") > 0.5 && (kind == "swarm" || kind == "knot") {
bind wp = nd_world_pos_t(i, t)
bind x = list_get(wp, 0)
bind y = list_get(wp, 1)
bind z = list_get(wp, 2)
if kind == "swarm" { draw_swarm_sw(i, x, y, z, t, native_solid) }
else if !native_solid { draw_knot_sw(i, x, y, z, t) }
if editor && rt_selected(nd_id(i)) { sw_sphere(x, y, z, nd_get(i, "b"), nd_get(i, "b"), nd_get(i, "b"), 0.0, 0.0, 0.0, C_GIZMO, 0.35) }
}
bind i = i + 1.0
}
}
fn draw_swarm_sw(i, x, y, z, t, native_solid) {
bind cnt = min(400.0, nd_get(i, "a"))
bind rad = nd_get(i, "b")
bind shape = nd_get(i, "c")
bind drift = nd_get(i, "d")
bind size = nd_get(i, "sx")
bind col = nd_colour(i)
bind rainbow = nd_ex(i, "rainbow", "0") == "1"
bind flat = nd_ex(i, "flat", "0") == "1"
bind id = nd_id(i)
bind k = 0.0
while k < cnt {
bind px = x + (fx_hash(k, id) - 0.5) * 2.0 * rad + sin(t * drift + k) * 0.4
bind py = y + (fx_hash(k + 100.0, id) - 0.5) * 2.0 * rad * sel(flat, 0.05, 1.0) + cos(t * drift * 0.7 + k * 1.3) * 0.3
bind pz = z + (fx_hash(k + 200.0, id) - 0.5) * 2.0 * rad + sin(t * drift * 0.5 + k * 0.7) * 0.4
bind c = col
if rainbow { bind c = fx_hue(fx_hash(k + 300.0, id) * 6.2832 + t * 0.3, 1.0) }
if shape < 0.5 {
sw_box(px, py, pz, size, size, size, t * 0.2 + k, t * 0.15 + k * 0.5, 0.0, c, 0.8)
} else if shape < 1.5 {
bind p = cam_project(px, py, pz)
if cam_visible(list_get(p, 2)) {
bind tw = 0.55 + 0.45 * sin(t * 2.0 + k * 2.1)
bind r = max(1.0, size * cam_scale_at(list_get(p, 2)))
px_disc(list_get(p, 0), list_get(p, 1), r, c, tw)
}
} else if !native_solid {
sw_sphere(px, py, pz, size, size, size, 0.0, 0.0, 0.0, c, 0.8)
}
bind k = k + 1.0
}
}
fn draw_swarm_native(i, x, y, z, t) {
bind cnt = min(64.0, nd_get(i, "a"))
bind rad = nd_get(i, "b")
bind drift = nd_get(i, "d")
bind size = nd_get(i, "sx")
bind col = nd_colour(i)
bind rainbow = nd_ex(i, "rainbow", "0") == "1"
bind flat = nd_ex(i, "flat", "0") == "1"
bind id = nd_id(i)
bind k = 0.0
while k < cnt {
bind px = x + (fx_hash(k, id) - 0.5) * 2.0 * rad + sin(t * drift + k) * 0.4
bind py = y + (fx_hash(k + 100.0, id) - 0.5) * 2.0 * rad * sel(flat, 0.05, 1.0) + cos(t * drift * 0.7 + k * 1.3) * 0.3
bind pz = z + (fx_hash(k + 200.0, id) - 0.5) * 2.0 * rad + sin(t * drift * 0.5 + k * 0.7) * 0.4
bind c = col
if rainbow { bind c = fx_hue(fx_hash(k + 300.0, id) * 6.2832 + t * 0.3, 1.0) }
plat_color(c)
plat_shape("sphere", px, py, pz, size, size, size, 0.0, 0.0, 0.0, 0.0)
bind k = k + 1.0
}
}
# the rosette: x = b cos s + c cos 5s, y = b sin s - c sin 5s, z = (c/1.1) sin 10s, hue along the chain
fn knot_pos(i, k, t) {
bind cnt = max(1.0, nd_get(i, "a"))
bind b = nd_get(i, "b")
bind c = nd_get(i, "c")
bind f = t * nd_get(i, "d") + k / cnt
bind f = f - floor(f)
bind s = 6.2832 * f
return [b * cos(s) + c * cos(5.0 * s), b * sin(s) - c * sin(5.0 * s), c / 1.1 * sin(10.0 * s), f]
}
fn knot_col(i, f, t) {
if nd_ex(i, "rainbow", "1") != "1" { return nd_colour(i) }
bind br = 0.55 + 0.45 * sin(f * 15.7 + t * 2.0)
return fx_hue(f * 6.2832 + t, br)
}
fn draw_knot_sw(i, x, y, z, t) {
bind cnt = min(120.0, nd_get(i, "a"))
bind size = nd_get(i, "sx")
bind k = 0.0
while k < cnt {
bind p = knot_pos(i, k, t)
bind f = list_get(p, 3)
bind sz = size * (0.8 + 0.5 * (sin(f * 12.566 + t * 3.0) * 0.5 + 0.5))
sw_box(x + list_get(p, 0), y + list_get(p, 1), z + list_get(p, 2), sz, sz, sz, 0.0, t * 0.4 + k, 0.0, knot_col(i, f, t), 0.9)
bind k = k + 1.0
}
}
fn draw_knot_native(i, x, y, z, t) {
bind cnt = min(120.0, nd_get(i, "a"))
bind size = nd_get(i, "sx")
bind k = 0.0
while k < cnt {
bind p = knot_pos(i, k, t)
bind f = list_get(p, 3)
bind sz = size * (0.8 + 0.5 * (sin(f * 12.566 + t * 3.0) * 0.5 + 0.5))
plat_color(knot_col(i, f, t))
plat_shape("box", x + list_get(p, 0), y + list_get(p, 1), z + list_get(p, 2), sz, sz, sz, 0.0, t * 0.4 + k, 0.0, 0.0)
bind k = k + 1.0
}
}
# MIDI nodes with "visualise" on: upcoming notes as a ribbon of bars flowing toward the node (play mode)
fn draw_midi_notes(t) {
bind n = nd_count()
bind i = 0.0
while i < n {
if nd_get(i, "kind") == "midi" && nd_get(i, "b") > 0.5 && nd_get(i, "vis") > 0.5 {
bind notes = st_get("midin_" + num_str(nd_id(i)), [])
bind cnt = floor(len(notes) / 3.0)
if cnt > 0.0 {
bind wp = nd_world_pos(i)
bind ox = list_get(wp, 0)
bind oy = list_get(wp, 1)
bind oz = list_get(wp, 2)
bind col = nd_colour(i)
bind k = 0.0
bind drawn = 0.0
while k < cnt && drawn < 64.0 {
bind nt = list_get(notes, k * 3.0)
bind pitch = list_get(notes, k * 3.0 + 1.0)
bind dur = list_get(notes, k * 3.0 + 2.0)
bind dt = nt - t
if dt > 0.0 - dur && dt < 6.0 {
bind z = oz + dt * 3.0
bind x = ox + (pitch - 60.0) * 0.18
bind lit = dt <= 0.0
sw_line(x, oy, z, x, oy, z + max(0.2, dur * 3.0), sel(lit, C_WHITE, col), sel(lit, 1.0, 0.7))
sw_line(x - 0.08, oy, z, x + 0.08, oy, z, sel(lit, C_WHITE, col), 1.0)
bind drawn = drawn + 1.0
}
if dt > 6.0 { bind k = cnt }
bind k = k + 1.0
}
sw_line(ox - 6.0, oy, oz, ox + 6.0, oy, oz, col, 0.6)
}
}
bind i = i + 1.0
}
}