~/satyajit

Hexels: what a hexagon with a colour ramp buys a splat, and what it does not

mdjsonmcp

2026-10-31 · 20 min · 3d · gaussian-splatting · gpu

Why read this

Hightop 30%

Reads paper, repo and kernel; checks the area proposition; toys split edge, shape and colour; iPad slowdown from raw telemetry. No CUDA run.

  • Interactive explanations
  • Original analysis
  • Runs on a consumer GPU

3D & spatialMITResearch paper

How this was scored
Is it new?
2 of 3: A real new idea, method or capability
Can I trust it?
2 of 3: Measures key facts from files, code or configs
Can I run it?
2 of 3: Open code or weights with real limits
Will I understand it?
3 of 3: Mechanism carried by interactives built from real code or data
Can I act on it?
2 of 3: A concrete recipe, numbers or comparison
Will it last?
1 of 3: Relevant for months
Does it affect many?
1 of 3: A specialist community
Only here?
2 of 3: A teardown or measurement few others did

Score 68 of 100, ranked 174 of 666 rated articles. Each question is answered 0–3 by hand, and a 3 is rare. How articles are scored

The post that sent me here is Prakhar Kaushik's: "Introducing Honeycomb-Surfels (Hexels). High-quality 3D. Less memory. Faster rendering." The paper is arXiv 2610.37668 (Kaushik, Soumava Paul, Alan Yuille, Johns Hopkins). The code is MIT licensed and includes an iPad and iPhone app, Hexel Edge, that trains a scene on the device.

The idea fits in a sentence. Take a 2DGS surfel, a flat disc you intersect with each pixel's ray. Replace its Gaussian falloff with a soft-edged hexagon, and give it a colour that varies linearly across its face instead of one colour. The authors argue that the world is locally flat with smoothly changing colour, so one such patch can stand in for several Gaussians.

I did not train a scene. The CUDA path needs an NVIDIA GPU and the app needs an Apple device; this box has neither, and the sandbox will not run downloaded code anyway. What I did: read the paper's HTML and appendices, read the main repo and its two submodules (the kernel and the viewer), recounted every number I could from files, checked the paper's one theorem numerically, wrote two small CPU experiments, and re-derived one curve from the iPad run's raw CSV. Numbers are measured (mine, from a file or a run of my code), reported (the paper's or the authors' figure, not re-run) or reasoned (my arithmetic on the other two). Related reading on this site: SteadySplats for the renderer side of splatting, and 3D capture pipelines for where a model like this would sit.

A surfel, and what a Gaussian costs it

A 2DGS primitive is a disc in 3D with a centre, two tangent directions and two scales. For a pixel, you intersect the ray with the disc's plane and get local coordinates (u,v)(u, v). The weight is a Gaussian in those coordinates, exp⁡(−(u2+v2)/2)\exp(-(u^2 + v^2)/2), multiplied by an opacity, and primitives blend front to back.

A Gaussian never reaches zero. To keep the renderer finite you pick a cut-off, and everything inside it must be visited: the tile assignment, the sort and the blend all pay for the tail. Overlapping tails also mean several primitives share the same pixel. The paper counts this as overdraw and shows it on a poster fitted with 256 primitives (Figure 8, below): about 10 blends per pixel for Gaussians against 2.5 to 2.8 for hexels (reported, read off the figure).

Storage is the other cost. The evaluated 2DGS primitive stores 58 floats; by my count 48 of them are spherical-harmonic colour (3 position, 2 scale, 4 rotation, 1 opacity and 48 colour). That is a lot of memory for a surface whose colour, in many places, is nearly constant or a gentle ramp.

What a hexel is

A hexel keeps 2DGS's frame and ray–plane intersection. Its position p\mathbf{p}, two tangents and two scales place a patch in the world. In the patch's own plane the paper defines a hexagon with one distance function,

d(u,v)=max⁡{∣v∣, ∣32u+12v∣, ∣32u−12v∣}−1,d(u,v)=\max\{|v|,\ |\tfrac{\sqrt3}{2}u+\tfrac12 v|,\ |\tfrac{\sqrt3}{2}u-\tfrac12 v|\}-1,

so d<0d<0 inside, d>0d>0 outside and d=0d=0 on a regular hexagon with inradius 1. The edge is soft: K(u,v)=σ(−β d(u,v))K(u,v)=\sigma(-\beta\, d(u,v)), with σ\sigma the logistic function and β\beta the sharpness the optimiser learns. The hexel contributes alpha min⁡{o K, 0.99}\min\{o\,K,\ 0.99\}, where oo is its opacity. Colour is a first-order field, c(u,v)=c0+u gu+v gv\mathbf{c}(u,v)=\mathbf{c}_0+u\,\mathbf{g}_u+v\,\mathbf{g}_v: a base RGB and two RGB gradients, in the patch's own coordinates. Compositing is 2DGS's, unchanged.

Left: a 3D surface tiled by hexagonal patches. Middle: one hexel with its centre, two tangent vectors and a colour ramp running from cyan through pink to yellow. Right: the soft support K for beta 2, 8 and 32, with a slice showing the edge sharpening around the hexagon's boundary.
A hexel is a planar patch with a soft hexagonal boundary and a linear colour field; beta sets how sharp the edge is (paper, Figure 1).

The rasteriser needs to know which pixels a hexel can touch. A contribution below 1/2551/255 is skipped, and since α≤K\alpha \le K, that happens where K<1/255K<1/255, that is, outside d=log⁡(254)/βd=\log(254)/\beta. The bounding circle has radius r=23(1+log⁡(254)/β)r=\tfrac{2}{\sqrt3}\left(1+\log(254)/\beta\right) in the hexel's local units. At the paper's initial β≈10\beta\approx10 that is 1.79 inradii (reasoned, from the paper's Eq. 18 and 19). A Gaussian with σ\sigma equal to that inradius goes on contributing out to 3.33σ\sigma at full opacity, which is 3.4 times the area. That is a comparison at the same nominal size, not at the same quality; I mention it only because it is the mechanism the authors point to.

Twenty floats

The paper's V1 hexel stores 11 geometry-and-support scalars (3 position, 2 scale, 4 rotation, 1 opacity, 1 sharpness) and 9 colour scalars (a base colour and two gradients): 20 floats, 80 bytes. V2 adds a 3×3 linear view-direction residual (29 floats), and V3 adds degree-3 spherical harmonics (65). The paper gives 58 floats for the 2DGS primitive it evaluates; for 3DGS I used the standard 59, which the paper does not state.

The headline comparison is at matched model memory, so the arithmetic matters: 58 / 20 is 2.9, meaning a V1 model has 2.9 times as many primitives as a 2DGS model of the same size. Whatever the paper's gain is, part of it is simply that. The authors also report a matched-primitive-count comparison (up to +4.74 dB and 2.31 times the frames per second over 2DGS); I come back to those numbers below.

How a pixel finds its (u,v)(u, v)

For each pixel the rasteriser needs the intersection of its ray with the hexel's plane. The paper writes the plane's map as a 3×3 matrix T\mathbf{T} from homogeneous local coordinates to the image, takes its rows tx,ty,tz\mathbf{t}_x,\mathbf{t}_y,\mathbf{t}_z, and forms k=x tz−tx\mathbf{k}=x\,\mathbf{t}_z-\mathbf{t}_x, l=y tz−ty\mathbf{l}=y\,\mathbf{t}_z-\mathbf{t}_y and h=k×l\mathbf{h}=\mathbf{k}\times\mathbf{l}. Then (u,v)=(hx/hz, hy/hz)(u,v)=(h_x/h_z,\ h_y/h_z), with no per-pixel matrix inverse. It skips hz=0h_z=0 and anything nearer than 0.2.

That is exact. The paper notes that 3DGS projects a Gaussian with a local affine approximation instead. In a side view the difference is one denominator: the exact local coordinate along a tilted plane is u=xz0s(cos⁡φ−xsin⁡φ)u=\dfrac{x z_0}{s(\cos\varphi - x\sin\varphi)} and the affine one drops the xsin⁡φx\sin\varphi term.

The two agree at the centre and drift apart as the plane tilts or the patch grows relative to its distance. That is general to surfels, not special to hexels; I draw it because a hexel's straight edges make the error visible where a Gaussian's blur would hide it.

Why a hexagon: the proposition, checked

Appendix G of the paper gives the argument for the shape. Proposition G.1 says that within a patch, an affine colour field approximates a twice-differentiable target to within 12h⊤Qh\tfrac12\mathbf{h}^\top\mathbf{Q}\mathbf{h}, where Q\mathbf{Q} bounds the second derivative. That defines an ellipse inside which one affine field stays within a tolerance ε\varepsilon. Proposition G.3 then asks which regular tiling shape, affinely stretched and rotated, covers the most area of that ellipse. The answer is Ammax⁡=msin⁡(2π/m) ε/det⁡QA_m^{\max}=m\sin(2\pi/m)\,\varepsilon/\sqrt{\det\mathbf{Q}} for an mm-gon, so a hexagon covers 33/4≈1.2993\sqrt3/4\approx1.299 times the area of a square and exactly 2 times a triangle.

I checked that numerically. For mm = 3, 4 and 6 I drew 6,000 random affine maps of a regular mm-gon, found the smallest disc enclosing each image by brute force, scaled it to unit radius, and compared the area with the closed form. No sample exceeded it (measured: 0 violations in 6,000 for each shape), and the best sampled areas were 1.2990, 2.0000 and 2.5973 against 1.2990, 2.0000 and 2.5981. The ratios come out at 1.299 and 2.000.

Two caveats the paper does not dwell on. The comparison is among shapes that tile the plane. A disc inside the same ellipse covers π\pi against the hexagon's 2.598, so the hexagon covers 82.7% of the disc's area; it wins because discs do not tile, not because it covers more than a disc. And the proposition bounds a colour error under a local model; it says nothing about edge sharpness, nothing about how Gaussians overlap, and nothing about optimisation. So I tested those separately.

What a fixed lattice says

The first test removes the optimiser. I tile a 128×128128\times128 image with equal-count lattices of squares, regular hexagons and equilateral triangles, fit each cell by least squares with a constant colour or an affine ramp, and compare PSNR. The lattice scale is bisected so every shape has the same number of cells (border cells otherwise give a hexagonal lattice more cells at the same nominal area). Three synthetic images: a flat-colour poster, the same layout with smooth shading, and a high-frequency texture.

Three things come out (all measured, all on synthetic images, with cell geometry free because the lattice is fixed):

What optimised primitives say

The second test brings the optimiser back. N primitives are placed by image gradient (70%) and uniformly (30%), as in the paper's 2D setup, and trained with Adam for 200 steps, with identical hyperparameters for every kind. Pixels are a normalised blend of all primitives, c(x)=∑iwici/(∑iwi+10−4)c(\mathbf{x})=\sum_i w_i c_i / (\sum_i w_i + 10^{-4}), which is the paper's 2D renderer without its top-10 cut. Four kinds follow the paper: a Gaussian and a hexagon, each with a constant colour (8 stored scalars) or a linear ramp (14 and 15). Two controls keep the hexagon's sigmoid edge with a fixed β=10\beta=10 and change only the support, to a disc and to a square, so edge sharpness and shape can be told apart. The images are 48 × 48. The main kinds ran on three seeds; the controls ran on two, on the poster and the shaded image only.

Two rows of 48 by 48 reconstructions magnified four times, for the flat poster and the shaded poster, each fitted with 32 primitives: reference, Gaussian, Gaussian with ramp, round sharp-edged control, hexagon and hexel with ramp. The Gaussian tiles are blurry at every boundary; the round, hexagon and hexel tiles have crisper shapes with some stray patches and streaks. PSNR is printed under each tile.
The same fits at 32 primitives, seed 0, with PSNR under each tile; the Gaussian blurs every boundary and the sharp-edged supports do not (ours, from primitives.py).

A sharp edge, not six sides. At 32 and 64 primitives on the flat and shaded images, a hexagon with constant colour beats a Gaussian with constant colour by 2.0 and 2.7 dB on the poster and by 2.3 and 3.8 dB on the shaded image (measured, three seeds). That is the size of effect the paper's Figure 8 shows at 256 primitives on its flat image, where constant-colour hexels lead constant-colour Gaussians by 3.0 dB. The round control, with the same sigmoid edge and no corners, does the same: 23.85 and 27.01 dB on the poster against the hexagon's 23.62 and 27.25, and 26.23 and 31.02 on the shaded image against 25.31 and 30.43. Those gaps are inside the seed-to-seed spread, which reaches about a decibel. A square sits 1.2 and 1.6 dB behind the hexagon at 64 primitives, so shape is not irrelevant, but hexagon against disc is not distinguishable here. The strip says why a Gaussian loses: a normalised blend of Gaussians softens every boundary. The paper's Figure 8 has no round, sharp-edged primitive, so as far as I can tell it cannot separate the two explanations; I cannot say whether the answer changes at its scale. The paper does note that "sharper footprints do not by themselves prevent sampling artifacts", which is the caveat to this edge advantage.

The ramp helps when primitives are few. At 8 and 16 primitives the ramp adds 1.0 and 0.9 dB to a Gaussian and 1.4 and 1.1 dB to a hexagon on the poster, and 1.1, 0.9, 1.3 and 1.6 dB on the shaded image. At 32 primitives it is level for a Gaussian (+0.05 and +0.35 dB) and costs a hexagon 0.9 and 0.5 dB; at 64 it costs between 0.6 and 1.5 dB. I read that as an optimisation effect and not a representational one: the ramp adds six or seven scalars to the search and 200 steps do not converge, and the strip's ramp tiles show overshoot streaks. The fixed lattice above shows the representation can carry about 4 dB. But a user training briefly would see this curve, and it is a regime the paper does not report. At equal stored scalars the ramp does not pay for itself here: a hexel with a ramp at 16 primitives (240 scalars) is 1.4 dB behind a constant Gaussian at 32 (256) on both smooth images, and a constant hexagon at 32 beats both.

Texture defeats everyone. On the texture image every kind sits within a few tenths of 16.4 dB up to 32 primitives, which is under a decibel above the 15.6 dB of painting the whole image its mean colour (measured). At 64 primitives the hexel with a ramp reaches 17.05 and the constant hexagon 16.69. The paper's limitations paragraph says it directly: a first-order colour model cannot represent arbitrary texture within one primitive, and high-frequency appearance needs smaller primitives or a richer model.

My first run was wrong in a way worth reporting. I had parameterised the ramp in image units, so with Adam's step of 0.01 the coefficients could not reach useful values, and the ramp looked useless at every size. The paper's Eq. 4 uses local, scale-normalised coordinates (u,v)(u, v). I switched to that, reran the linear kinds, and everything above is the second version; the first version's linear rows are kept in the raw file marked as such and are not used.

What the paper reports

I did not re-run any of this. Table 2 compares methods at matched model memory over 34 scenes. Three rows carry the story:

Mean over scenes2DGSTriangle SplattingHexels V1
Tanks and Temples (10): PSNR / VRAM / FPS18.52 / 1.37 GiB / 284.521.35 / 0.74 / 495.223.05 / 0.56 / 438.4
Mip-NeRF 360 (9): PSNR / VRAM / FPS17.82 / 3.24 / 201.222.36 / 1.12 / 312.724.82 / 0.90 / 377.8
DTU (15): PSNR / VRAM / FPS21.56 / 0.68 / 478.026.04 / 0.69 / 517.528.94 / 0.55 / 830.8

The abstract's "+4.53 to +7.38 dB over memory-matched 2DGS" checks out against the table: 23.05 − 18.52 is 4.53, 24.82 − 17.82 is 7.00, and 28.94 − 21.56 is 7.38 (reasoned, from the paper's own table). The teaser's 5.51 times less peak VRAM (0.88 against 4.88 GiB) is consistent with the rounded values: 4.88 / 0.88 is 5.55. The frames per second advantage over 2DGS is 1.54 to 1.88 times at matched memory.

Not every column favours hexels, and the paper shows it. Training time is not lower: 13.3 minutes against 10.6 on Tanks and Temples, and 24.2 against 20.0 on Mip-NeRF 360, with a tie on DTU (both 8.4). Triangle Splatting trains faster than hexels on all three. The claim is about memory and quality, not speed of training. On 2D images the claim is better quantified: 35.82 against 33.16 dB on the 45-image Image-GS benchmark at 10,000 primitives, in 157.07 against 238.40 seconds, which is 34.1% less time and matches the post. For surfaces, the paper reports 28.9% lower Chamfer error than memory-matched 2DGS across 15 DTU scans, and an F1 of 34.38% against 31.72% on six Tanks and Temples scenes.

A grid of image reconstructions of a flat-colour poster and a smoothly shaded version, each fitted with 256 primitives using Gaussians or hexels, with constant colour or colour gradients. PSNR is printed under each: Gaussians 30.84 and 35.28 dB on flat colour, hexels 33.86 and 35.72. Below, overdraw maps show about 10 blends per pixel for Gaussians and about 2.5 for hexels, and error maps are darker for hexels.
The paper's own ablation: shape against colour model at 256 primitives, with overdraw and error maps. Gaussians reach 30.84 dB with constant colour and 35.28 with gradients; hexels reach 33.86 and 35.72 (paper, Figure 8).

This figure is the cleanest piece of evidence for the shape claim, and it is also what my toy cannot reproduce at its scale: the paper's 256 primitives on a large image is a different regime from my 8 to 64 on a 48-pixel one.

Qualitative novel-view-synthesis crops at matched model memory and matched primitive budgets: a room from Mip-NeRF 360 and Francis from Tanks and Temples with fixed V1 hexels, and Caterpillar and DTU scan 63 with fixed V3 hexels, each against baselines, with PSNR printed per crop.
Qualitative comparisons under matched memory and matched primitive budgets (paper, Figure 4).

What the release contains

The main repository is MIT. Its THIRD_PARTY.md is worth reading: the vendored baselines (Image-GS, 2DGS, 3DGS, Convex and Triangle Splatting, BBSplat and others) keep their own licences, and the Gaussian-Splatting licence limits those files to research and evaluation. The CUDA kernel and the WebGL viewer live in two separate submodule repositories, both MIT.

I counted lines by script. The Python package hexels/ is 17,259 lines in 30 files, tests/ is 10,216 in 26 files and scripts/ is 8,710 in 47. Hexel Edge is 5,177 lines of Swift in 26 files and one 365-line Metal file. The kernel repository's CUDA source is dominated by a 5,744-line 3D forward file.

The kernel matches the paper where I could compare. It defines the hexagon as the maximum of three half-plane distances, the circumradius constant 2/32/\sqrt3, log⁡254\log 254 for the cut-off, the 0.99 alpha clamp and the 1/2551/255 threshold. It also has things the paper's main text does not describe:

None of this is a criticism. It is a reminder that the paper's V1 is a subset of what the code can do.

On the iPad

Hexel Edge is the part of the release I can say the most about, because the authors publish raw files: a JSON summary, a 34-row quality curve and a 233-row telemetry CSV. The run trains DTU scan24 on a 12.9-inch iPad Pro with an M2, offline and unplugged, for 20 minutes: 6,598 fixed V1 hexels, 42 training and 6 reserved views at 640 × 480, 32,920 updates, 25.21 dB on the reserved views (reported). The authors state the limits themselves: it is one scene, and the reserved views were reused when choosing the recipe, so it is a development number and not a blind benchmark.

I checked what I could. Ledger arithmetic: 6,598 hexels at 80 bytes is 0.50 MiB, and with gradients and two Adam moments about 2.0 MiB; the telemetry's GPU allocation is 2.44 MiB, constant over the run. The peak process footprint is 64.9 MiB, so the parameters are under 1% of it and the whole training state about a thirtieth; the rest is the app, the images and the system (reasoned).

One thing is not in the README. The telemetry's step time is a running mean, which drifts from 23.4 to 35.4 ms. Rebuilding the cumulative update time from it (the mean times the number of updates after the first 40) reproduces the JSON's 1,164.112 seconds exactly, so the reconstruction is consistent. From it, the first 7,000 updates took 28.2 ms each and the last 3,600 took 43.0 ms (measured, my derivation): throughput fell from 35.5 to 23.2 updates per second over the 20 minutes, about 1.5 times slower per update, while the OS reported a nominal thermal state in all 233 samples. The authors say GPU clocks and temperature were not measured, so I cannot name the cause. It is a reminder that a 20-minute figure on a tablet is not a steady-state figure.

The README's status table is candid about scope: V1 hexels in FP32 with a fixed population are implemented; V2 and V3, densification, standalone SfM, quantisation and 2D fitting in the app are not. The iPhone build has been tested only in the simulator.

What I did not do

I did not run the CUDA kernel, train a scene or reproduce any table. The toys are small: 48-pixel images, up to 64 primitives, 200 Adam steps, a dense normalised blend instead of the paper's top-KK, one set of hyperparameters for every kind. The absolute PSNRs are low because nothing converged; only gaps at the same budget mean anything. I did not tune per kind, and I did not use a photograph, because none is available offline and I would not fetch one I could not license.

What would settle the open questions: a run of the released trainer on one Mip-NeRF 360 scene at matched memory against 2DGS, with the shape and sharpness ablated the way my controls do; the paper's own Figure 8 repeated with a round, sharp-edged control; and a longer iPad run with GPU clocks logged.

Sources

honeycomb-surfels/hexels@698d1af · snapshot 2026-10-31
tracked files
281
license
MIT
branch
main
tests
34 files
source
2.1 MB
commit date
2026-10-29
source by language
Python1.7 MB(111)Swift297.4 kB(26)Metal16.4 kB(1)CUDA5.3 kB(1)Shell1.3 kB(1)

by size of tracked source at this commit, file counts in brackets; docs, data and vendored trees excluded

local clone, 2026-10-31 at 698d1af — branch, commit, commitDate, fileCount, hasTests, languages, license, licenseFile, shallow, testFileCount

shallow clone: counts describe the pinned tree, not the history

Cite this article

For attribution, please use the following reference or BibTeX:

Satyajit Ghana, "Hexels: what a hexagon with a colour ramp buys a splat, and what it does not", ai.thesatyajit.com, October 2026.

bibtex
@misc{ghana2026hexels,
  author = {Satyajit Ghana},
  title  = {Hexels: what a hexagon with a colour ramp buys a splat, and what it does not},
  url    = {https://ai.thesatyajit.com/articles/hexels},
  year   = {2026}
}
share