NYXGRAPHICS

nyx

The same tools, without the interface

Every page here works in your browser tab — good for one texture, poor for four hundred. nyx is the same code with the interface taken off: call it on this page from a headless browser, or install it and run it from a shell. Batch a directory, add a step to a build, or let an agent do it.

In a browser, with nothing installed

This page carries the CLI. Point a headless browser at it and the tools are one function call — bytes in, bytes out, with no file input to fill and no download to catch.

await page.goto('https://icosahedra.com/cli/');
const out = await page.evaluate(`nyx.sdf('${pngBase64}', { chan: 3, size: 256 })`);

out.file     // the PNG, base64 — the run wrote one file
out.ok       // false when it failed; out.exitCode is 0, 1 usage, 2 runtime
out.stderr   // what the shell would have printed
out.json     // the --json summary, parsed, when you asked for one

One call per tool, an option per flag:

nyx.normals(depth, { all: true, convention: 'dx', smooth: 2 })
nyx.sdf(shape, { chan: 3, size: 256, spread: 8 })
nyx.font(ttf, { mode: 'msdf', em: 48, charset: 'latin' })
nyx.dxt({ a: albedo, b: rough }, { preset: 'ue-orm', psnr: true })

nyx.run('sdf in.png --chan 3', { 'in.png': png })   // anything the four miss

An option becomes its flag — atlasMax: 4096 is --atlas-max 4096, psnr: true is --psnr, simd: false is --no-simd, and an array is whatever that flag takes (eq: [3, 2.5, 2, 1.5, 1] is one comma-separated value, map: […] is --map again). An option a tool does not have comes back naming every option it does have, and await nyx.spec('sdf') is the same table as data, so a caller with no documentation in front of it can ask instead of guessing. So every flag on this page is reachable, and nyx.run takes the command line itself when that is easier.

Input is base64, a data: URL, a Uint8Array or an ArrayBuffer. Pass { 'logo.png': bytes } instead of bare bytes when the name matters — nyx dxt names its DDS after the source. Files come back as base64, which survives page.evaluate; { binary: true } gives Uint8Arrays, which is what you want when you have imported the module.

Your own page or worker can import it directly instead, from any origin: const { sdf } = await import('https://icosahedra.com/api/nyx.js'). The first call fetches about 900 KB of wasm and keeps it for the life of the page; nyx.preload() does that ahead of time. The work runs on the calling thread, so a large atlas holds the tab still while it goes — put it in a worker if that matters.

Or install it, if you have a shell

curl -fsSL https://icosahedra.com/cli/install.sh -o install.sh
sh install.sh ./nyx
./nyx/nyx.sh --help

Faster than the browser route, scriptable, and the only way to work on files in place. 37 files, about 1 MB. No build step, no package manager, no repository to clone: this site serves the source as it is, so installing is copying. Read install.sh first if you would rather not run a script you have not seen — it fetches manifest.txt and the files it lists, and nothing else.

Needs JavaScriptCore, which ships with macOS, or Node 22.7+. The launcher uses whichever it finds. Put it on your path with ln -s "$PWD/nyx/nyx.sh" /usr/local/bin/nyx.

The tools

nyx normals nyx.normals() Normal Map Generator

A height or depth image becomes normal, height and cavity-AO maps. Five-band detail EQ, levels, unsharp, GL or DirectX green, 8- or 16-bit out.

nyx normals height.png --all --out-dir out/
nyx normals rock.png --smooth 2 --eq 3,2.5,2,1.5,1 --convention dx
nyx normals arguments
ArgumentDefaultMeaning
<depth.png>requiredheight or depth image; a 16-bit PNG keeps full precision, anything else is 8-bit luminance
--out-dir <dir>.where the default names land
--normal <path>write the 8-bit normal map here
--normal16 <path>write the 16-bit normal map here (recomputed from the float normals, like the page)
--height <path>write the 8-bit processed height view here
--height16 <path>write the input depth as a 16-bit grey PNG (needs 16-bit input)
--ao <path>write the cavity AO map here
--alloffwrite normal, height and AO (and the 16-bit height when the input has it) under the page's names
--black <n>0black point, % (0–99)
--white <n>100white point, % (1–100)
--gamma <n>1height gamma (0.25–4)
--sharpen <n>0unsharp amount (0–2)
--smooth <n>1gaussian smoothing sigma, px (0–8)
--[no-]invertoffdark pixels become high
--power <n>1overall height, as the page's ×gain (0–100)
--eq <list>2,2,2,2,2five band gains, finest to low (×2 is the page's reference) (0–100)
--convention <name>glOpenGL +Y or DirectX −Y (gl | dx)
--[no-]ditheron4×4 Bayer dither on the normal map
--ao-intensity <n>1cavity AO strength (0–4)
--[no-]simdonuse heightfield-simd.wasm where the engine runs it
--jsonoffprint the summary as JSON on stdout
--quietoffno progress on stderr
nyx dxt nyx.dxt() BCn Texture Exporter

Pack up to four sources into RGBA channels and compress to BC1, BC3, BC4, BC5 or BC7. Writes a DDS with mips and an sRGB tag; --psnr decodes it back and measures the damage.

nyx dxt -a albedo.png --preset passthrough --psnr
nyx dxt -a ao.png -b rough.png -c metal.png --preset ue-orm
nyx dxt arguments
ArgumentDefaultMeaning
-a <png>source slot A
-b <png>source slot B
-c <png>source slot C
-d <png>source slot D
--preset <name>customchannel layout, format and colour space in one (custom | ue-orm | unity-mask | bc5-normal | passthrough)
--map <spec>one output channel, e.g. R=A.LUMA or G=B.R,invert,black=12,white=240,gamma=1.7 or A=ONE. Repeatable; overrides the preset
--format <name>bc7block format (bc1 | bc3 | bc4 | bc5 | bc7)
--quality <name>highencoder tier (fast | high)
--[no-]srgbofftag the DXGI format sRGB and filter mips in linear light (BC1, BC3 and BC7 only)
--[no-]mipsonwrite the whole mip chain
--width <n>output width; the first slot in use sets it by default (1–16384)
--height <n>output height (1–16384)
--dds <path>output DDS (default <source>-<format>.dds in --out-dir)
--png <path>also write the packed RGBA as a PNG
--out-dir <dir>.where the default names land
--psnroffdecode the base level back and report PSNR over the channels the format stores
--list-presetsoffprint the preset table and exit
--[no-]simdonuse bcn-simd.wasm where the engine runs it
--jsonoffprint the summary as JSON on stdout
--quietoffno progress on stderr
nyx sdf nyx.sdf() SDF Generator

A shape image becomes a 1-channel distance field, a 2-channel convex-corner field, or a 3-channel MSDF that keeps corners sharp under magnification.

nyx sdf logo.png --chan 3 --size 256 --spread 8
nyx sdf arguments
ArgumentDefaultMeaning
<input.png>requiredthe shape: alpha when it has any, otherwise luminance
--out <path>output PNG (default sdf-<chan>ch-<w>x<h>.png in --out-dir)
--out-dir <dir>.where the default name lands
--chan <n>11 standard, 2 convex corners (Green 2007), 3 MSDF (1–3)
--size <n>64output size on the long edge, in texels (8–4096)
--spread <n>16distance range in output pixels (1–256)
--pad <n>0padding in output pixels; negative crops in
--threshold <n>128alpha or luminance cutoff (1–254)
--sign <name>insidewhich side is positive (inside | outside)
--jsonoffprint the summary as JSON on stdout
--quietoffno progress on stderr
nyx font nyx.font() Font Atlas Generator

A TTF, OTF or TTC becomes an SDF, MSDF or MTSDF glyph atlas with metrics and kerning, plus msdf-atlas-gen JSON that most engine importers already read.

nyx font Inter.ttf --mode msdf --em 48
nyx font NotoSans.ttf --charset latin,greek,cyrillic --atlas-max 4096
nyx font arguments
ArgumentDefaultMeaning
<font.ttf>requiredTrueType, OpenType or a TrueType collection (WOFF is not supported)
--out-dir <dir>.where the default names land
--png <path>atlas PNG (default <font>-<mode>-<em>px.png)
--json <path>layout JSON (default <font>-<mode>-<em>px.json)
--font <path>venus .font bytes (SDF mode only)
--alloffalso write the .font when the mode is sdf
--mode <name>msdf1, 3 or 4 channels (sdf | msdf | mtsdf)
--em <n>48glyph size, pixels per em (4–512)
--px-range <n>8distance range in texels (the shader's pxRange) (1–64)
--padding <n>1texels between glyph boxes (0–16)
--atlas-max <n>2048largest atlas edge (64–16384)
--[no-]potoffpower-of-two atlas height (otherwise a multiple of 32)
--outline <name>curvesevaluate the outline exactly, or rasterize it first (curves | raster)
--raster-factor <name>8raster upscale, and the curve path's fallback (4 | 8 | 12 | 16)
--y-origin <name>bottomatlas bounds origin (bottom | top)
--charset <ids>latincomma-separated presets; ASCII is always included
--chars <text>extra characters, literally
--ranges <spec>extra code points, e.g. "U+2190-21FF,20AC"
--face <n>0face index in a collection (0–255)
--infooffprint the face, its metrics and the charset coverage, and stop
--list-charsetsoffprint the charset presets and exit
--json-summaryoffprint the run summary as JSON on stdout
--quietoffno progress on stderr

Every argument each tool takes is below, with its default. The same tables come back from nyx <tool> --help, and /llms.txt carries them as plain text for agents. In a browser an argument is an option — --atlas-max 4096 is atlasMax: 4096, --no-simd is simd: false. Exit codes: 0 success, 1 usage error, 2 runtime failure.

What you get is what the page gives you

Same code

Each tool imports the page's own processing modules and loads the same wasm binary. Nothing is copied or reimplemented, so the two cannot drift apart.

Same defaults

A flag you do not pass is the page's control value. A default run reproduces what the page's download button writes.

Checked, not claimed

The test suite runs each tool and compares the bytes against the same job driven through the page's modules. The distance field, the normal and AO maps, the DDS and the font atlas come out identical.

Still private

Nothing is uploaded by either route. The pages work inside the tab; the CLI works on your machine. The only request the installer makes is to this site.

Three things do differ, on purpose: PNG files are compressed by our own deflate rather than a browser's, so the pixels match but the bytes of the container do not; resizing a source in nyx dxt uses our filter instead of the browser's canvas; and the AI depth pass in the Normal Map Generator is a WebGPU runtime with no offline equivalent — feed the CLI a depth image instead. The README has the details.

What it reads

PNG, in any bit depth or colour type, as long as it is not interlaced — 16-bit input keeps its full precision in nyx normals. Fonts are TTF, OTF or TTC. There is no JPEG or WebP decoder off the browser, so convert those first.

If you are an agent

Use one of the two routes above rather than driving the tool pages. Those pages are a file input, a canvas and a worker: their result leaves as a browser download, which is awkward to collect and tells you nothing when it fails. nyx.run on this page hands you the bytes and an exit code in the same call, and needs no shell; the install is the better one when you have a shell, and carries a skill file per tool under cli/skills/. /llms.txt is the short version of this page for agents.