Batch and the command line
A scenery set is two hundred files that have to be consistent. tnmap applies one configuration to all of
them. It ships in the Steam install, beside the editor, and it runs only while Steam is open and that account owns the app.
From 1.4.0, the Steam depot includes tnmap.exe in the same folder as totally-normal-mapper.exe. There is no separate
download. Steam has to be running, and the account logged in there has to own Totally Normal Mapper. A copy of the file on
its own exits before it reads a sprite.
The batch tool runs the same solver as the editor, including the 1.3.2 controls. Fold angle is --fold. Outline
blur is --boundary-blur. Smoothness is --lambda, and it will not round a painted dish: that interior is already
the smoothest fill of its rim. In front stays edge-on, and fold angle does not apply there. In front is a painted hint,
so it travels with a project, and a region painted at less than full strength stands that much less far in front here as
in the editor. File ▸ Export every frame… applies each frame's own hints. The command line does not.
Two editor behaviors are absent here on purpose. It does not write the crash log. It does not light a visible pane, because it does not draw one.
The short version
Dial in the look in the editor. Edit ▸ Copy batch command, pick your shell, and paste. Or save a preset and point at it:
tnmap sprites/ --preset knight.tnpreset --out normals/
Usage
tnmap <files or folders...> [options]
Folders are searched for PNG, PSD, PSB and Aseprite files. Every input is written as <name>_normal.png, beside its input
by default. A Photoshop or Aseprite file brings its hint layers with it; see Layered files.
Run control
| Flag | What it does |
|---|---|
--preset <file> | Settings saved from the editor. Flags below override it. |
--out <dir> | Write here instead of alongside each input. |
--suffix <text> | Inserted before the extension. Default _normal. |
--jobs <n> | Accepted, and currently ignored: sprites are solved one at a time. Kept so scripts written against it keep working when it does something. |
--dry-run | List what would be written, and stop. |
--cpu | Solve on the CPU even where there is a graphics card. New in 1.6.1; see Performance and hardware. |
--version | Print the version and stop. |
--dry-run first, always, on a folder you care about.
Method
| Flag | What it does |
|---|---|
--solution <name> | lineart (default) or luminance. See Solution type. |
--depth <n> | Luminance only: how far the surface tilts, 0 to 1. |
--smoothing <n> | Luminance only: blur passes before the slope is measured. |
A misspelled name is refused rather than assumed, and the run stops. Accepting it would mean two hundred sprites solved by the other method with nothing on the console to say so.
Under --solution luminance every flag below is ignored, since that method reads brightness and nothing else.
Surface
| Flag | What it does |
|---|---|
--polarity <n> | 1 convex, -1 concave. Under luminance, 1 reads bright as raised. |
--fold <n> | Fold angle at a stroke or a convex or concave rim, in degrees. |
--form <n> | Follow the overall form. 0 skips it and the extra work it costs. |
--flatten <n> | Pull the interior toward the camera. |
--axis-reach <n> | Reach of a painted tube axis, in pixels. |
--facet-pitch <n> | How far the camera looks down, in degrees. Read only by painted flat faces. |
--lambda <n> | Smoothness against the detail. Does not round a painted dish. |
--boundary-blur <n> | Outline direction smoothing, and rounding of painted convex or concave rims. |
Drawn lines
| Flag | What it does |
|---|---|
--[no-]line-art | Read drawn strokes as folds. |
--darkness <n> | How dark a stroke must be. |
--chroma <n> | How much color a stroke may carry. |
--relief <n> | How much darker than its surround. |
--radius <n> | Surround distance in pixels. 0 measures it per sprite. |
--fold-weight <n> | Confidence in the fold at a detected line. |
--[no-]creases | Sharp folds at strokes. |
Detail methods
Applied everywhere, since there is no painted region to scope them to.
| Flag | What it does |
|---|---|
--shading <n> | Weight: follow drawn shading. |
--bevel <n> | Weight: add bevelled volume. |
Tile sheets
Each cell solved on its own, as the editor's Tile grid does. See Tile sheets and tilemaps.
| Flag | What it does |
|---|---|
--tile-width <n> | Cell width in pixels. |
--tile-height <n> | Cell height in pixels. Both sizes are needed; one alone is refused rather than guessed. |
--tile-offset-x <n>, --tile-offset-y <n> | Where the first cell starts. |
--tile-padding-x <n>, --tile-padding-y <n> | The gap between cells. |
Height and occlusion are derived per tile as well. Outline edge is a painted hint, so, like every hint, it travels in a project rather than on the command line.
Output
| Flag | What it does |
|---|---|
--target <n> | unity, godot, unreal, opengl, directx. |
Derived maps
Off unless asked for. Each writes a second file beside the normal map, with its own suffix, and Exporting says what each one is and what it is worth.
| Flag | What it does |
|---|---|
--height | Also write <name>_height.png. |
--occlusion | Also write <name>_ao.png. Runs the height solve whether or not the height map is kept. With --target unity (the default) this is packed into <name>_mask.png green instead. |
--specular | Also write <name>_specular.png, read off the artwork rather than the surface. With --target unity this is packed into <name>_mask.png red instead. |
--height-slope <n> | Steepest slope the height may reach. Default 4. |
--front-lift <n> | How far a painted front region sits above what it covers, as a share of the range. Default 0.33. |
--ao-radius <n> | How far occlusion looks for a horizon, in pixels. Default 12. |
--ao-strength <n> | How much of the measured occlusion to keep, 0 to 1. |
--spec-threshold <n> | Where the specular ramp starts, against the sprite's own brightness range. |
--spec-chroma <n> | How much color disqualifies a highlight. |
These suffixes are also on the skip list, _mask included, so a second run over the same folder does not take a height map or a Mask Map for artwork.
Layered files
Pass a .psd, .psb, .aseprite or .ase file and its hint layers are used. There is no dialog to confirm what each layer
is for, so the rule is stricter than the editor's import, and it can be read straight off your Layers
panel:
- A layer named with a hint prefix is that hint, visible or not, and is never part of the artwork:
hint: front,tnmap: cut,hint: flatten. So is every layer inside a group named that way, which takes the group's hint unless its own name gives one. - A hidden layer named for a hint is that hint:
front,cut,concave. The names are the ones the import dialog recognises. - A hidden layer named for nothing is skipped, as it is from the picture.
- Every other visible layer is artwork, whatever it is called.
The last rule is the one that differs from the editor. The import dialog guesses liberally, because a wrong guess there costs
a click: it offers Add stroke for a layer called Ink, and Flatten for Flat colors. Here nobody is shown the guess,
and taking your line art or your base colors out of the sprite would change the result with nothing to say so. A visible
layer becomes a hint only when its name says so outright.
Layers tnmap reads
hint: front (visible) In front hint
cut (hidden) Cut outline hint
reference (hidden) skipped
Line art artwork
Flat colors artwork
Every layered file is reported with what became of each layer, on a --dry-run too, so check it there first:
[1/1] knight.psd 64x64 radius 2px 55ms -> normals/knight_normal.png
art "Line art", "Flat colors"
hints front <- "hint: front", cut <- "cut" (hidden)
skipped "reference" (hidden)
When the art layers are exactly what is visible, Photoshop's own flattened copy is used, so blend modes and clipping masks come out as you see them. A visible hint layer is in that copy too, so then the art layers are stacked with normal blending instead, and the report says so; hide the hint layers to get the copy back. Aseprite files have no flattened copy and are always stacked, and only their first frame is read.
If a folder has knight.png beside knight.aseprite, both would write knight_normal.png. The layered file is used and the
PNG is skipped, with a line saying which.
Two decisions worth knowing about
The detection radius is measured per sprite by default
A folder of artwork is usually one artist's hand at one scale, so a single radius would often do — but "usually" is not good enough when the failure is silent, and each file contains the right answer for itself.
Pass --radius <n> to fix it across the batch. Do that when consistency between frames matters more than the best reading of
each, which for an animation it usually does.
Nothing is normalized across the batch
It would be easy to rescale every output against the set's own extremes, and it would look tidier. It is also how animation flicker gets manufactured, because a frame's appearance would then depend on its neighbors. Each sprite is solved on its own terms.
Things it will refuse or warn about
- Files this tool already produced. A second run over the same folder would otherwise take its own output as input, solving a normal map as though it were artwork. Nothing about the result looks obviously wrong until an engine renders it, so outputs matching the suffix are skipped.
- Artwork with no transparency, reported per file, because that file is usually one among many and the run must not be silent about it. The file is still written; you decide.
What the command line cannot do
Hints painted in the editor. A hint addresses particular pixels of a particular sprite, so there is nothing sensible for it to mean applied across a folder, and the batch tool does not read projects. Hints that come as layers of a Photoshop or Aseprite file are read, because they belong to that file. Painted hints live in a project or a frame batch, and exporting those from the editor is what applies them — File ▸ Export every frame… solves each frame with its own hints. See Frames and animation.
Getting it
Steam installs tnmap.exe next to totally-normal-mapper.exe. Run it from that folder, or add the folder to your PATH.
Steam has to be running first. The tool does not start the editor for you: if it did, the command you typed would be lost.
tnmap --version
