An OpenGL geometry, matrix, and shader library for glitter, a Replicant-style GTK4 renderer for Jolt. Ported from glimmer-gl, which does the same for glimmer (glitter's Reagent-style sibling). It does two things:
- Composable geometry, ported (via glimmer-gl) from thi.ng/geom: build 3D solids as plain Clojure data, transform and combine them, and tessellate to a GL-ready vertex buffer. Vectors, column-major 4×4 matrices, a mesh model, and primitive constructors.
- Shaders as data (
glitter-gl.shader), ported (via glimmer-gl) from thi.ng/geom's shader-spec model: declare a shader's interface (uniforms, attributes, varyings, version) as maps and compose its stages from reusable GLSL snippets; the declarations are generated and only emitted as a GLSL string when you compile. - A GL widget for glitter: requiring
glitter-gl.gtkregisters a:gl-area(a GtkGLArea drawing surface) into glitter's widget registry, so a GL pane lives in the same reconciled hiccup tree as the rest of your UI.
glitter-gl.vector gives unboxed 3D vectors, glitter-gl.matrix gives
column-major 4×4 matrices (the layout glUniformMatrix4fv expects) with
inlined flonum arithmetic, glitter-gl.gl binds the slice of OpenGL you
need to compile shaders and fill buffers/VAOs/uniforms, and
glitter-gl.mesh / glitter-gl.primitives are the thi.ng/geom-style
composition layer.
orbit: six solids over a lit, shadowed ground plane, driven through
glitter-gl.app/reactive-area. Nine more takes, and what each one
demonstrates, in the examples gallery.
Docs site: https://jlt-commons.github.io/glitter-gl/
- Jolt: Clojure on Chez Scheme.
This project is not JVM Clojure and there is no
clojure/leinpath. - GTK4: glitter renders through it, and
:gl-areais aGtkGLArea. - A working OpenGL context. macOS ships OpenGL as a framework; on Linux
it is the
libGLshared object from mesa or your vendor driver. The headlessbb checkneeds no display, but every interactive demo does. - babashka (
bb) for the task wrappers. Optional: every task has ajolt -M:<alias>equivalent.
brew install gtk4 # macOSNothing else needs to sit next to this checkout: deps.edn pins glitter as
a git coordinate, so a fresh clone builds on its own. See
Dependency modes if you want to develop against a local
glitter instead.
jolt -M:plasma # rotating cube/sphere/tetra + composable plasma/stripes shader
jolt -M:ripple # full-screen fragment shader, no mesh at all
jolt -M:orbit # several solids orbiting a lit, shadowed ground plane
jolt -M:knot # a (2,3) torus knot, geometry generated from scratch
jolt -M:gears # three counter-rotating cog outlines, 2D, orthographic
jolt -M:textured # rotating cube wearing a procedural checkerboard texture
jolt -M:picking # pointer-driven ray picking against a ground and a wall
jolt -M:check # headless sanity check: shader compiles, geometry buffers valid
jolt -M:test # unit suite
jolt -M:gl-area-smoke # live-GTK smoke: :gl-area construct/realize/render/resizeIn CI, invoke the alias form (jolt -M:test, not jolt test). On every
released jolt the task shorthand swallows the child's exit status, so a failing
suite reports green. Fixed on jolt main after v0.7.27 but not yet in a
tagged release, so the alias form stays the safe default. See
docs/guide/testing-and-tasks.md for the
probe.
bb info # start here: grouped task list
bb test # jolt -M:test
bb plasma # interactive demo
bb ripple # interactive demo, no mesh
bb orbit # interactive demo, via app/reactive-area
bb knot # interactive demo, generated geometry
bb check # headless sanity check
bb smokes # live-GTK smoke, CI-safe
bb hooks:install / :install:full / :uninstall # git pre-commit hook: fast | +tests | remove
bb hooks:install sets up a fast pre-commit hook (lint errors + format +
ns cleanliness) that gates every commit on staying bb lsp:format-check-
clean: the whole codebase is formatted uniformly, including examples/
and the 22 files ported verbatim from glimmer-gl. See CONTRIBUTING.md's
invariant #1 for why a project-wide clojure-lsp format pass doesn't conflict
with the "don't improve ported files" porting discipline. It mirrors
glitter's identical resolution of the same tension for its own
Replicant-ported files.
deps.edn declares glitter as a pinned git coordinate
(io.github.jlt-commons/glitter at a fixed :git/sha). jolt fetches and builds
against that exact commit, so a fresh clone of this repo builds with no other
setup. This is the default, and what every command above uses:
jolt -M:plasma # builds against the pinned glitter shaTo co-develop this library against an unreleased glitter change, the :dev
alias overrides the pin back to a sibling checkout at ../glitter. Combine it
with any runnable alias:
jolt -M:dev:plasma # builds against ../glitter instead of the pin
jolt -M:dev:test:dev only helps if ../glitter actually exists next to this checkout. It is
not something a first-time user needs or has.
Bumping the pin is deliberate: change the :git/sha in deps.edn, then run
bb test and bb smokes before committing.
flowchart TD
subgraph pure["Pure library: no glitter dependency, usable from any Jolt + OpenGL program"]
geom["Geometry and math (14 files)<br/>vector, vec2, matrix, quaternion<br/>aabb, rect, circle, line, plane<br/>triangle, sphere, polygon<br/>bezier, intersect"]
mesh["Mesh model (4 files)<br/>mesh, glmesh<br/>primitives, polyhedra"]
glplumb["GL plumbing (5 files)<br/>shader, gl, ffi-compat<br/>offscreen, renderer"]
scene["scene.clj<br/>scene tree to render plan<br/>(requires glitter-gl.matrix only)"]
end
subgraph bridge["glitter integration"]
gtk["gtk.clj<br/>registers the :gl-area widget<br/>handlers wire from :apply,<br/>never :connect"]
app["app.clj<br/>reactive-area builds the<br/>:gl-area prop map bound to<br/>glitter's state atom"]
end
glitter["glitter<br/>glitter.ffi, glitter.widget"]
geom --> mesh
mesh --> glplumb
geom --> scene
glplumb --> app
scene --> app
gtk --> app
glitter -.->|"required by gtk.clj alone,<br/>1 of 25 files in src/glitter_gl"| gtk
glitter-gl.gtkregisters:gl-areainto glitter's widget registry. The imperative realize/render/resize/tick/motion/key/button signals are wired from the widget spec's:applyclosure (called once per prop key at construction, and again on every re-render), guarded so each event only ever connects once per widget. This is a real, live-found correction to the original design, which called for wiring them once at construction via glitter.widget's:connecthook.:connectnever actually sees a hiccup element's real props under glitter's reconcile flow, so it silently never fired. Seedocs/guide/gl-area-widget-layer.mdfor the full story.glitter-gl.scene/glitter-gl.appdo NOT track reactive-cell dependencies the way glimmer-gl's originals do: glitter's own state-atom watcher already recomputes the whole view on every change, so a GL scene built the same way (a pure function ofstate) needs no separate tracking layer.
The full guide is nine pages under docs/guide/,
one topic per page:
architecture.md: the four-layer stack, how thin the seam to glitter actually is (one file has a literal:requireonglitter.*), and why a:gl-areakeeps redrawing even though a bare state change doesn't cause it to.geometry-and-shaders.md: orientation over the 22 verbatim-ported namespaces. The three groups, where a mesh becomes GL data, and why the matrices are column-major.gl-area-widget-layer.md: the:gl-areawidget's mechanics in full. The:apply-vs-:connectcorrection, traced through the actual reconciler code path, plus every non-standard GTK4 signal shape it has to handle.scene-and-app.md: the declarative scene graph's mini-hiccup dialect (and why it's not glitter's own hiccup), whyplanhas no reactive-cell tracking, and the write-once handler contract.porting-and-attribution.md: the three sourcing buckets, the Standard Verbatim Port Procedure, and the lineage back through glimmer-gl to thi.ng/geom.examples.md: what each of the tenexamples/glitter_gl/namespaces is for, and which two are actually wired into regression coverage. Includes a four-take gallery of theplasmademo, plus one take each ofripple,orbit,knot,gears,texturedandpicking.testing-and-tasks.md: the unit suite, the live-GTK smoke and headless checkbb smokesruns, and the quality-tooling task surface (lint, format,clj-kondo's FFI hook, the git pre-commit gate).limitations.md: every known v1 gap, each with the reason it was left rather than fixed.index.md: the guide's own nav map, if you'd rather start there.
Plus:
CONTRIBUTING.md: how to build, test, and submit changes, and the ten invariants this project does not regress.NOTICE.md: file-by-file attribution for the ported code.- Design spec and implementation plan are not part of this repo; they live in a private planning store.
See CONTRIBUTING.md for setup, the ten numbered
invariants this project does not regress, and how to add a widget.
Before opening a PR, run the four local gates: bb test, bb lint,
bb lsp:format-check, and bb smokes. CI runs the first three plus
bb lsp:clean-ns-check on every PR, and a second job exercises real
OpenGL under Xvfb on mesa's llvmpipe. Both can fail the build. bb smokes is still worth running yourself, since software rendering is not
your driver.
Ported from glimmer-gl (2026-08-06 arc). See NOTICE.md's porting ledger
for the full verbatim/adapted/new breakdown, including a real live-found
correction to :gl-area's original wiring design (see
docs/guide/gl-area-widget-layer.md).
The unit suite currently stands at 178 tests / 559 assertions (bb test),
plus bb smokes' live-GTK smoke (gl-area-smoke, which drives a real
GtkGLArea under the real reconciler rather than a fake renderer) and its
headless check (check, which needs no GL context or display at all).
glitter-gl.app's scene-graph/shadow-mapping renderer path
(reactive-area) is ported, unit-tested, and, as of this arc, exercised
live end to end by examples/glitter_gl/orbit.clj. The plasma demo
still wires :gl-area directly, the same way glimmer-gl's own upstream
demo does; ripple, knot, gears, textured and picking do too,
for reasons of their own (see NOTICE.md). See
docs/guide/limitations.md for this and
every other known v1 gap.
Copyright (c) 2026 Burin Choomnuan.
glitter-gl's own code is distributed under the
Eclipse Public License 2.0, matching the rest of jlt-commons and jolt
itself. SPDX identifier: EPL-2.0. It was Apache License 2.0 until 2026-09-05.
The geometry, matrix, mesh and shader layer is ported from glimmer-gl, and through
it from thi.ng/geom, which Karsten Schmidt licenses under the Apache License 2.0.
Those files keep that licence and its attribution requirements, and
NOTICE.md is the file-by-file record that carries the required
notice. EPL 2.0 relicenses none of it: the whole is EPL 2.0 and each part keeps
what it came with, as long as that notice travels with it.
