Character Art Standard
Body Shape System v1
Parametric bare-skin torsos built from ovals and rectangles. Source cell 512×512, exported at 8:1 into a 64×64 character grid. Line art is two passes on every body type: a 4px stroke-selection core for the edge, then a 13px black airbrush over it for weight.
Player wiki Heads controls ↓ Export preview ↓
Source cell
512 × 512
Export cell
64 × 64
Line art
4px core + 13px
Skin mask
{{ skinColor }}
Base width
272px · 28.5px
Step ratio
{{ ratioLabel }}
1 · Generator + Heads
Heads controls are in the left Parameters column (gold Heads box). Export heads from the right column under Heads export.
Heads
Show head Show eyes Attach points Guide grid Reset
Head shape
Ellipse Circle Tall oval Capsule
Blink Auto blink Export blink
Skin tint (body + head fill)
{{ skinColor }}
Eye tint (fill only)
{{ eyeColor }}
Hair tint (grey-base recolor)
{{ hairColor }}
Live preview when Show head is on. Eyes: solid tinted fills only (no outlines, no iris/highlight dots). Blink flattens the fill ovals. Skin/eye/hair swatches tint fills; body strokes stay black. Style: live cloth (clerkshirt_sturdy, postal_jacket_slim_new, tee_sturdy) and ladders/body-ladder.svg. Cloth facing: Front|Back|Side.
Body parameters
Torso height {{ h }} px · locked
Load an archetype
{{ p.name }}
Facing
Front Left Right Back Blink
{{ facingHint }}
HEAD EYE_L EYE_R SHLD_L SHLD_R HAND_L HAND_R HIP THIGH_L THIGH_R HEMLINE
Scale chips
64px
32px
{{ r.k }} {{ r.v }}
Ladder export
1c archetypes (default): 6 unisex cells Slight→Sturdy. Sex is hairstyle-only in-game; bodies are shared. Width morph: six stepRatio widths from the live silhouette. ★ marks the ref rung.
Ladder mode
1c archetypes Width morph
Heads export
Export heads sheet
8 head-only cells (Slight→Heavy) + anchors. Uses current Head shape + Head size. GIMP Columns = 8.
{{ exportHint }}
Downloads .svg plus a *-anchors.json sidecar (coords). Allow multiple downloads if the browser asks.
Export ladder SVG from current body Export this body SVG
Export body PNG Export character PNG (outfit)
Export preview
Live ladder strip from current export settings (same geom as Export ladder SVG / CLI). Body + head use the same two-pass stroke (glow + core). Skin/eye swatches tint fills only (eye paths stay outline-free). Optional blink + cloth overlay. Recolor samples below.
Facing
Front Left Right Back {{ facingHint }}
Show cloth Show hair Randomize outfit Randomize colors Reshuffle Next outfit Next hair {{ clothLabel }} · {{ hairLabel }}
Hat tint (grey-base hard hat)
{{ clothHatColor }}
Civilian top
{{ clothTopColor }}
Civilian bottom
{{ clothBottomColor }}
Brand uniforms (postal / clerk)
{{ p.label }} {{ brandLabel }} · badge {{ brandPrimary }}
{{ clothHint }} Front/side: hair above head/eyes and under hat. Back: head then hair on skull, then body+cloth (nape trails tuck under torso; soft fringe hardened). When a hat is on, hair is masked to the hat opaque region (sideburns/nape below the brim stay). Grey-base recolor uses the hair / hat / civilian swatches (or Randomize colors). Postal and clerk layers use the brand preset (hat / top / bottom + badge underfill). Facing syncs with Generator. Left is the side profile; Right = FlipH of Left. Export ladder SVG / CLI emit the active facing. Export character PNG (outfit) is a 512×512 transparent PNG of this live stack.
{{ b.name }}
Recolor samples
Standard body with the skin/eye test palette. Click a swatch to apply those colors to the ladder above.
{{ exportHint }}
2 · Ladder proposals
On-page design studies. Default export dumps the 1c named archetypes as 6 shared unisex bodies (Slight→Sturdy). Sex presentation is hairstyle-only.
1a
Uniform width ladder (live Generator)
Eight widths around the current shoulder W at {{ ratioLabel }} per step. Same silhouette family as the Generator. Secondary export mode (Width morph).
{{ b.name }}
{{ b.dims }}
1b
Width × taper matrix
Four widths crossed with three shoulder-to-hip tapers. Design study only. Export does not dump this matrix.
{{ row.name }}
{{ row.note }}
{{ b.dims }}
1c
Named archetypes
Six hand-tuned shapes (Slight→Sturdy). Preview + export follow Facing (Front / Left / Right / Back). ★ = closest shoulder W to the live Generator.
Facing
Front Left Right Back
{{ b.name }}
{{ b.dims }}
{{ b.note }}
Recommendation: adopt 1c as the shipped set and keep 1b’s two axes as the procgen parameters behind it. Archetypes give art direction; the axes give the engine room to jitter between them.
3 · Construction rules
What must stay identical across every body, facing and variant.
Shape
Torso is one closed path: a rounded rectangle from the shoulder line down, closed by a half-ellipse at the hips. No third shape, no compound outlines.
Shoulder corner radius is a fraction of shoulder width, 0.165–0.20×W, so the corner reads the same at every size. Wider bodies sit at the low end.
Hip cap height is 40–49% of torso height, hard-clamped so its top edge stays in the lower torso (never above ~40% from the shoulder line). Below 35% the body stops reading as top-down.
Stroke
Two passes, in this order. 1. Stroke selection, Line, foreground colour, 4px, round cap and join — this is the exact silhouette edge. 2. Airbrush #000 at 13px over it — this is the weight and the soft outer falloff.
The 4px core does the geometry, the airbrush never does. A hand airbrush pass alone wanders; laying the core first means the edge is identical on every body and every facing.
Neither width scales with body width. That is what keeps a thin body and a heavy body looking like the same art style.
The soft falloff is the blurred airbrush pass sitting under the fill, so it only shows outside the silhouette. Gaussian {{ falloffLabel }} at source scale.
The crisp #808080 bucket fill is what kills the inner blur, inset by half the core stroke. Keep it as a separate layer, not merged.
Attach points
Overlays snap to these, expressed as fractions of the torso box so they survive any width. Each has a jitter box the procgen engine may sample inside.
PointPositionJitter
{{ a.k }} {{ a.pos }} {{ a.jit }}
Facings
Bodies ship in three. The blink variant is a head-layer swap only — it never duplicates a body.
SuffixRule
{{ f.k }} {{ f.v }}
4 · GIMP
Your current settings, locked down — plus a script that rebuilds the whole sheet from an exported SVG.
Tool settings
{{ g.k }} {{ g.v }}
Layer stack, bottom to top: _glow → _fill → _line. Clothing and colour layers go above all three.
body-ladder.scm
Download
; body-ladder.scm - Sorting Floor · body shape standard v1
; Drop in  Preferences ▸ Folders ▸ Scripts,  then
; Filters ▸ Script-Fu ▸ Refresh Scripts.
; Run from  Filters ▸ Postal Sim ▸ Build Body Ladder…
;
; Takes the SVG exported by the generator and rebuilds every body as
; three named layers - _glow (13px airbrush pass, blurred), _fill (#808080
; skin mask, crisp) and _line (4px core stroke). Same two-pass order as the
; hand process - core stroke first for the exact edge, airbrush over it for
; weight - reproducible and re-runnable.
;
; Geometry: torso H locked at 300px; heads nestled (cy = HEAD.y − 0.15·ry).
; Eyes: eye_L_/eye_R_ solid fills at ±0.32·rx, −0.06·ry (no iris/hl/lids).
;
; The export emits body_<id> silhouettes, optional head_<id> shapes
; (ellipse / circle / tall oval / capsule), optional eye_L_/eye_R_ fills, and anchor_* guide circles.
; Paths whose names start with body_ or head_ get glow/fill/line; eye_ fills are fill-only.
; Script-Fu skips anchor_* so guides never become ink. Prefer the maintained
; copy at art_src/procgen/ladders/body-ladder.scm (GIMP 2.10 + 3).
; "Columns" counts body/head cells, not paths. Default 1c sheet uses Columns=8.

(define (psim-body-ladder svgfile cell cols core airbrush falloff)
  (let* ((w    (* cell cols))
         (img  (car (gimp-image-new w cell RGB)))
         (res  (gimp-image-import-paths-from-file img svgfile TRUE TRUE))
         ; GIMP 2.10 instead: (gimp-vectors-import-from-file img svgfile TRUE TRUE)
         (parr (cadr res))
         (n    (vector-length parr))
         (i    0))
    (gimp-image-undo-disable img)
    (gimp-context-push)
    (gimp-context-set-antialias TRUE)
    (gimp-context-set-feather FALSE)
    (gimp-context-set-stroke-method STROKE-LINE)
    (gimp-context-set-line-cap-style CAP-ROUND)
    (gimp-context-set-line-join-style JOIN-ROUND)

    (while (< i n)
      (let* ((p    (vector-ref parr i))
             (nm   (car (gimp-item-get-name p)))
             (body? (and (>= (string-length nm) 5)
                         (string=? (substring nm 0 5) "body_")))
             (head? (and (>= (string-length nm) 5)
                         (string=? (substring nm 0 5) "head_")))
             (eye?  (and (>= (string-length nm) 4)
                         (string=? (substring nm 0 4) "eye_")))
        )
        ; Skip anchor_* / other non-ink paths
        (if (or body? head? eye?)
            (let ((glow (car (gimp-layer-new img w cell RGBA-IMAGE
                              (string-append nm "_glow") 100 LAYER-MODE-NORMAL)))
                  (line (car (gimp-layer-new img w cell RGBA-IMAGE
                              (string-append nm "_line") 100 LAYER-MODE-NORMAL))))

              ; PASS 2 - airbrush, 13px, blurred
              (gimp-image-insert-layer img glow 0 0)
              (gimp-context-set-foreground '(0 0 0))
              (gimp-context-set-line-width airbrush)
              (gimp-drawable-edit-stroke-item glow p)
              (plug-in-gauss RUN-NONINTERACTIVE img glow falloff falloff 0)

              ; PASS 3 - #808080 fill on closed silhouettes
              (let ((fill (car (gimp-layer-new img w cell RGBA-IMAGE
                                 (string-append nm "_fill") 100 LAYER-MODE-NORMAL))))
                (gimp-image-insert-layer img fill 0 0)
                (gimp-image-select-item img CHANNEL-OP-REPLACE p)
                (gimp-selection-shrink img (/ core 2))
                (gimp-context-set-foreground '(128 128 128))
                (gimp-drawable-edit-fill fill FILL-FOREGROUND)
                (gimp-selection-none img))

              ; PASS 1 - 4px core stroke
              (gimp-image-insert-layer img line 0 0)
              (gimp-context-set-foreground '(0 0 0))
              (gimp-context-set-line-width core)
              (gimp-drawable-edit-stroke-item line p))
            TRUE))
      (set! i (+ i 1)))

    (gimp-context-pop)
    (gimp-image-undo-enable img)
    (gimp-display-new img)
    (gimp-displays-flush)))

(script-fu-register "psim-body-ladder"
  "Build Body Ladder..."
  "Rebuild the standardized body sheet from an exported ladder SVG."
  "Sorting Floor" "Sorting Floor" "2026"
  ""
  SF-FILENAME   "Ladder SVG"      ""
  SF-ADJUSTMENT "Cell size (px)"  '(512 64 2048 1 64 0 1)
  SF-ADJUSTMENT "Columns"         '(8 1 64 1 1 0 1)
  SF-ADJUSTMENT "Core stroke (px)" '(4 1 32 1 2 0 1)
  SF-ADJUSTMENT "Airbrush (px)"   '(13 1 64 1 4 0 1)
  SF-ADJUSTMENT "Falloff blur"    '(6 0 40 1 4 1 1))

(script-fu-menu-register "psim-body-ladder" "<Image>/Filters/Postal Sim")
Closed silhouettes (body + head + eyes) get the grow / fill / shrink treatment. anchor_* guide circles are imported but skipped (only body_ / head_ / eye_ are inked). Prefer art_src/procgen/ladders/body-ladder.scm for GIMP 2.10 + 3.
Path import was renamed between GIMP 2.10 and 3.0 — if gimp-image-import-paths-from-file errors, swap in the commented 2.10 line. Everything else is stable across both.
5 · Naming and export
One pattern, so Godot can build sprite paths from procgen output without a lookup table.
body_<archetype>_<facing>.png
body_heavy_front.png · body_lean_side.png · head_standard_front_blink.png · hair carries sex presentation
{{ n.k }}
{{ n.v }}
{{ e.k }} {{ e.v }}
Open decisions
Which ladder is canon — 1a, 1b or 1c. Everything downstream keys off the archetype list.
Side facing uses H-only squash scaleX 0.85 (height unchanged, no tilt) + shift −0.04W toward facing + headShift −0.18·rx (Right = FlipH of Left). Side eyes tighten toward facing on the shifted head; draw order back-hand→body→cloth→front-hand→head→hat. Left: viewer-right hand overlaps jacket; Right FlipH: viewer-left hand overlaps. Export preview, 1c ladder, ladder SVG, and CLI --facing all emit the active facing.
Head (v2.1): nestled seating cy = HEAD.y − 0.15·ry (no neck). Base 98×88 at Standard W (wider / less tall than 84×95), Head size 0.7–2.0×, shape ellipse/circle/tall/capsule. Eyes: smaller fills at ±0.32·rx, −0.06·ry, no outlines. Blink flattens fills. Hands: r=32 circles on hemline (cuff overlap), x = cx±(W/2+28), stroke = body core (4). Skin/eye swatches tint fills; body strokes stay black. Style: live cloth (clerkshirt_sturdy, postal_jacket_slim_new, tee_sturdy) and body-ladder.svg. Torso H locked at 300px (no height slider).
Whether the skin mask stays flat #808080 or gains a second value for a baked ambient occlusion band under the shoulder scoop.