acron.splats¶
Gaussian splats on Acron: a SOG bundle’s planes as textures, a work-buffer pass that decodes them (and lets the application deform them), one instanced draw in back-to-front order, and the sorter that produces that order.
The design follows the PlayCanvas engine’s gsplat pipeline — planes kept
packed on the GPU, a “work buffer” recomputed per view change, quads batched
per instance, a sorter on a worker — which is the reference for rendering
scans of this size in real time. What the application supplies is a
SplatModifier: a GLSL chunk that may move each splat and reshape its
covariance (a curved-world navigation view, a fisheye, an explosion — the
identity is the default) and the per-frame view. Everything else is here.
The pieces (each its own module; the package re-exports the API):
SplatSource(source) — aSogBundle’s five RGBA8 planes (from a.sogor a 3DGS.ply, seeacron.sog.read_splats()), its codebook and its SH palette as nearest-filtered textures, uploaded once, undecoded (135 MB for a 6.8 M-splat scan without SH; nothing is decoded on the CPU; the planes go from the native bundle to the textures without passing through Python).WorkBuffer(work) — ONE render-to-texture pass over one texel per splat (a card the size of the planes, an orthographic camera, five RGBA32F targets viaset_aux_float) that decodes the planes and runs the modifier, writing the centre, the two halves of the covariance and the colour — with the bundle’s spherical-harmonic bands evaluated for the update’s camera direction, so colour is view-dependent as the scan was trained (sh_bands). It runs only on the frames whose view changed (WorkBuffer.mark_dirty()+WorkBuffer.before_frame()), so the decode and the modifier’s Jacobian are paid per view change, not per frame.SplatNode(draw) — one instanced draw ofINSTANCE_SIZEquads per instance (128, so a 6.8 M-splat scan is ~53 K instances), the vertex shader pulling each quad’s splat through the R32F order texture. The quad spans 2√2 σ (the kernel is the exact gaussian over it, pulled to zero at the edge), is shrunk to the radius wherealpha·gaussiandrops under 1/255, clamped at the viewport, culled below a pixel size and outside the frustum, and dropped only far beyond the clamp (a safety valve: thousands of screen-filling quads hang a GPU); the 0.3 px dilation’s extra opacity is compensated (antialias). Premultiplied over-blending, no depth, its ownfixedbin (RUNTIME rule 18).SplatSorter(sort) — the back-to-front order on a worker thread, from the work buffer’s depth plane: the pass also writes each splat’s view depth and a draw flag (in front, inside the frustum, visible) for the scene camera it is given (WorkBuffer.set_camera()), that one plane is copied to RAM after an update, andacron.gsplat.GSplatSorterreads it and counting-sorts natively with the GIL released (the engine always builds it; there is no Python fallback). So the modifier is GLSL-only: whatever it does to a splat, the order follows, with no CPU twin. The visible set alone is ordered and drawn:instance_countfollows it.submit(fn)still takes a custom depth function for the cases that want one.LinearSplatBuffer(linear) — the opt-in linear-light path: the splats blend in an RGBA16F buffer and a composite pass tonemaps and gamma-encodes the result into the window.shadersholds the GLSL and builds the shaders;shis the CPU mirror of the shader’s spherical-harmonic evaluation (native).
No numpy anywhere: the array work (reading, packing, the CPU decode, the SH
evaluation, the sort) is C++ in acron.gsplat; Python holds Acron
arrays, memoryviews and SplatArray s.
The render options switch live, on the same objects — no GPU object is
recreated: WorkBuffer.set_sh_bands() and SplatNode.set_options()
swap one shader each (the targets, geometry, order texture and uniform
arrays stay), LinearSplatBuffer.attach() / detach()
move a node between the direct and the linear path (a reparent), and
LinearSplatBuffer.set_tonemap() is a uniform write.
This needs the GL 4.1 core profile (PRC): gl_InstanceID,
texelFetch and float render targets, none of which the 2.1 compat
context has (RUNTIME rule 17’s “no gl_VertexID at 120” is the same wall).
Acron’s RGBA RAM images are BGRA (acron.gpu): the planes are swizzled
on upload so .rgba in GLSL reads the SOG’s channels.