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

FlagWhat 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-runList what would be written, and stop.
--cpuSolve on the CPU even where there is a graphics card. New in 1.6.1; see Performance and hardware.
--versionPrint the version and stop.

--dry-run first, always, on a folder you care about.

Method

FlagWhat 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

FlagWhat 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

FlagWhat it does
--[no-]line-artRead 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-]creasesSharp folds at strokes.

Detail methods

Applied everywhere, since there is no painted region to scope them to.

FlagWhat 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.

FlagWhat 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

FlagWhat 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.

FlagWhat it does
--heightAlso write <name>_height.png.
--occlusionAlso 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.
--specularAlso 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