`Gl.*` binds the whole OpenGL 4.1 core API — every entry point of the platform gl3.h with every GL_* constant, generated by `ludic-dev glgen` with per-call ABI thunks. Windowed builds get an NSOpenGLContext on the existing window at Retina resolution; headless builds render into an offscreen CGL context, so a program that uses Gl.* renders and screenshots identically under the test harness. It links gl.ll, the thunks and OpenGL.framework only when used; every other build stays byte-identical. packages/ludic.render3d is a physically based renderer written on that surface: HDRI image-based lighting, GPU-generated terrain with scanned PBR materials, CDLOD, cascaded shadows, glTF with skinning, instanced vegetation with impostors, procedural grass, water, SSAO, and an HDR pipeline with bloom, auto-exposure and ACES. It also carries this session's work on it: the terrain at half its cost (10.3 -> 5.4 ms of frame), the streaming hitch that got worse the longer you played, a resize that emptied the world, and the packaging that lets a game use the renderer from its own repository — `ludic assets`, the material manifest shipping with the package, and shader lookup falling back to the install root. See changes/ for each, with its numbers. The camping game that drove all of it has moved out to its own repository, Maroon Lake; examples/rendering/smooth.ludic stays as the renderer's example here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
116 lines
5.2 KiB
Text
116 lines
5.2 KiB
Text
# ============================================================================
|
|
# programs.ludic — GLSL from files. A shader file carries no #version line; the
|
|
# loader prepends "#version 410 core", any per-variant defines, and the shared
|
|
# noise.glsl + lighting.glsl chunks for fragment stages, then compiles it.
|
|
# ============================================================================
|
|
|
|
# Where the renderer's own files are. The shaders belong to this package and ship with
|
|
# it — in the Ludic checkout they are under packages/, and in an installed toolchain
|
|
# under $LUDIC_HOME/packages/ — so a game built outside the Ludic tree does not have to
|
|
# copy them in. The scanned CC0 materials are the other half: too large to ship with a
|
|
# toolchain and redistributable from their origin, so they are fetched into the project
|
|
# (`ludic assets`) and read from there.
|
|
var r3d_root: string = "packages/ludic.render3d" # where shaders/ lives
|
|
var r3d_assets: string = "assets/polyhaven" # where the CC0 assets live
|
|
var r3d_root_found: bool = false
|
|
# defines prepended to EVERY program (set before any is built): renderer-wide switches
|
|
var r3d_global_defs: string = ""
|
|
var r3d_noise_src: string = null
|
|
var r3d_lighting_src: string = null
|
|
|
|
function r3d_set_paths(root: string, assets: string) -> void { r3d_root = root; r3d_assets = assets }
|
|
|
|
# The install root, as the compiler computes it: $LUDIC_HOME, else the directory of the
|
|
# `ludic` on PATH. A game running from its own tree finds this package there.
|
|
function r3d_home() -> string {
|
|
let env = Os.env("LUDIC_HOME")
|
|
if env != null and env != "" { return env }
|
|
return `{Os.env("HOME")}/.ludic`
|
|
}
|
|
|
|
# Settle r3d_root on first use: the package as checked out beside the project, else the
|
|
# copy that ships with the toolchain.
|
|
function r3d_find_root() -> void {
|
|
if r3d_root_found { return }
|
|
r3d_root_found = true
|
|
if Fs.exists(`{r3d_root}/shaders/lighting.glsl`) { return }
|
|
let home = r3d_home()
|
|
let alt = `{home}/packages/ludic.render3d`
|
|
if Fs.exists(`{alt}/shaders/lighting.glsl`) { r3d_root = alt; return }
|
|
print(`r3d: cannot find the renderer's shaders (looked in {r3d_root}/shaders and {alt}/shaders)`)
|
|
}
|
|
|
|
function r3d_shader_file(name: string) -> string {
|
|
r3d_find_root()
|
|
let path = `{r3d_root}/shaders/{name}`
|
|
let s = Fs.read_text(path)
|
|
if s == null { print(`r3d: missing shader {path}`); return "" }
|
|
return s
|
|
}
|
|
|
|
function r3d_shader_src(name: string, defines: string, is_frag: bool) -> string {
|
|
if r3d_noise_src == null { r3d_noise_src = r3d_shader_file("noise.glsl") }
|
|
if r3d_lighting_src == null { r3d_lighting_src = r3d_shader_file("lighting.glsl") }
|
|
var s = "#version 410 core\n" + r3d_global_defs + defines
|
|
if is_frag { s = s + r3d_noise_src + r3d_lighting_src }
|
|
return s + r3d_shader_file(name)
|
|
}
|
|
|
|
# Build a program with tessellation control/evaluation between vertex and fragment.
|
|
function r3d_program_tess(vs: string, tcs: string, tes: string, fs: string, defines: string) -> int {
|
|
let p = gl_program5(r3d_shader_src(vs, defines, false), r3d_shader_src(tcs, defines, false),
|
|
r3d_shader_src(tes, defines, false), null, r3d_shader_src(fs, defines, true))
|
|
if p == 0 { print(`r3d: tess program failed: {vs} + {tcs} + {tes} + {fs}`) }
|
|
return p
|
|
}
|
|
|
|
# Build a program from a vertex + fragment file pair (defines apply to both).
|
|
function r3d_program(vs: string, fs: string, defines: string) -> int {
|
|
let p = gl_program(r3d_shader_src(vs, defines, false), r3d_shader_src(fs, defines, true))
|
|
if p == 0 { print(`r3d: program failed: {vs} + {fs}`) }
|
|
return p
|
|
}
|
|
|
|
# Bind a texture to a unit and point a sampler uniform at it.
|
|
function r3d_bind_tex(prog: int, name: string, unit: int, target: int, tex: int) -> void {
|
|
gl_active_texture(GL_TEXTURE0 + unit)
|
|
gl_bind_texture(target, tex)
|
|
gl_uniform1i(gl_get_uniform_location(prog, name), unit)
|
|
}
|
|
function r3d_bind_2d(prog: int, name: string, unit: int, tex: int) -> void { r3d_bind_tex(prog, name, unit, GL_TEXTURE_2D, tex) }
|
|
|
|
# A framebuffer with one colour texture (and optionally a depth texture).
|
|
property Target {
|
|
fbo: int = 0,
|
|
color: int = 0,
|
|
depth: int = 0,
|
|
w: int = 0,
|
|
h: int = 0
|
|
}
|
|
function target_new(w: int, h: int, ifmt: int, fmt: int, ty: int, with_depth: bool, filter: int) -> Target {
|
|
let t = new Target
|
|
t.w = w; t.h = h
|
|
t.fbo = gl_framebuffer()
|
|
gl_bind_framebuffer(GL_FRAMEBUFFER, t.fbo)
|
|
t.color = tex_target(w, h, ifmt, fmt, ty, filter)
|
|
gl_framebuffer_texture2d(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, t.color, 0)
|
|
if with_depth {
|
|
t.depth = tex_target(w, h, GL_DEPTH_COMPONENT32F, GL_DEPTH_COMPONENT, GL_FLOAT, GL_NEAREST)
|
|
gl_framebuffer_texture2d(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, t.depth, 0)
|
|
}
|
|
let st = gl_check_framebuffer_status(GL_FRAMEBUFFER)
|
|
if st != GL_FRAMEBUFFER_COMPLETE { print(`r3d: framebuffer incomplete {st} ({w}x{h})`) }
|
|
gl_bind_framebuffer(GL_FRAMEBUFFER, 0)
|
|
return t
|
|
}
|
|
function target_free(t: Target) -> void {
|
|
if t == null { return }
|
|
let ids = gl_scratch()
|
|
ids[0] = t.fbo; gl_delete_framebuffers(1, ids)
|
|
ids[0] = t.color; gl_delete_textures(1, ids)
|
|
if t.depth != 0 { ids[0] = t.depth; gl_delete_textures(1, ids) }
|
|
}
|
|
function target_bind(t: Target) -> void {
|
|
gl_bind_framebuffer(GL_FRAMEBUFFER, t.fbo)
|
|
gl_viewport(0, 0, t.w, t.h)
|
|
}
|