3D character#
The character is a blocky figure — a mesh of rectangular boxes — identical in the app, on the website and in the panel, because all three read one shared specification written twice: once in JavaScript for the web and once in Kotlin for the app.
Three layers of appearance#
- Atlas (a painting). One 1024 × 1024 px image stretched over the body. This is where the face (eyes, eyebrows, smile), clothing patterns, pockets, stitching and fringes are painted. The scale is 1 px = 0.1 of a unit, so the 8 px wide body means 0.80 of a unit.
- Blocks (geometry). Boxes added on top: hats, jackets, skirts, backpacks, scarves. Each block knows which bone it belongs to (head, torso, arm, leg), so it moves together with the animation instead of floating.
- Colours. One base colour per item; darker shades (cuffs, collars, bands, fringes) are derived by the client itself, so the interface does not have to offer a separate field for every detail.
Paint or block — never both#
Every wardrobe item is either painted on the atlas or built from blocks — never both at once. The only exceptions are deliberate anchors, for example a hat that hides the top of the head so hair cannot show through the fabric.
This rule exists because an audit found double representations: the same item existed both as a painting and as a block, and in motion the two “punched through” each other. A duplicate is the most common source of strange artefacts on screen, which is why the rule is treated as hard rather than stylistic.
Two more rules come from the same lesson:
- Every block must touch something — the body or the item it belongs to. A block that floats in the air passes a collision audit, so a separate gap detector exists for it (the first version of that detector measured the opposite of what it claimed and cleared a floating backpack).
- Everything must be checked in motion. A collision test in a standing pose proves nothing; the audit walks the character through all the animation clips.
Dimensions and scale#
| Part | Size (units) | Note |
|---|---|---|
| Whole character | ≈ 2.78 | stands on the ground at the origin, from the sole to the tip of the hair |
| Torso | 0.80 × 0.90 × 0.60 | the anchor of the whole layout; upper clothing belongs to it |
| Head | 0.80 × 0.80 × 0.80 | hair and hats are positioned on the head's scale (8 px = 0.8 of a unit) |
| Atlas | 1024 × 1024 px | the coordinates of every face are described in the design specification |
The model has no neck — the head sits directly on the torso, and the torso is one solid block. That has a design consequence: any horizontal band across the chest reads as the letter “T”, which is why the scarf is a cylindrical collar with its end at the back.
The base mesh is a freely licensed blocky character set (Kenney Blocky Characters 2.0, CC0), stored as a GLB file and shared by the app and the website. Names of the bones in the file: torso, head, arm-left, arm-right, leg-left and leg-right, plus two wrist pivots that were added in code because the base mesh does not have them.
A practical consequence of the unlit base mesh (the whole shading is painted into the atlas): a new element must always be passed through the shading function, otherwise it falls out of the character's lighting and looks like a sticker glued onto the body.
Animations#
| Clip | Used for |
|---|---|
idle | standing still — on the map and in the creator's live preview |
walk | moving; the map pawn uses it natively |
sit | seated poses |
die | the defeat animation |
emote-yes | a gesture, e.g. waving or agreeing |
The base mesh carries 27 node animations, so more clips exist in the file than are currently used — the extra ones (sprint, further emotes) wait for their turn. Notes on how they are played:
- The map pawn renders the same file with a native renderer, not through a WebView. Since 0.120.0 that renderer lives in one shared module used by the main map and by the AR mini-map, so the figure looks the same in both places (Change history).
- Walking has one source. The same movement state that animates the legs on the map drives them in the AR camera view; there is no second, private animation state.
- The rendering is unlit and the contact shadow is drawn by hand as a soft blob on the ground, because a plain GL frame has no lighting engine to produce one.
- Clothes and extras are checked for collisions in motion, in a set of outfits, not only in a standing pose.
Wardrobe and colours#
Categories: hair, hat, top, bottom, shoes and extras (scarf, backpack, bag, glasses, gloves).
| Colour picker | What it offers |
|---|---|
| Presets | ready-made colour strips per item: skin, hair, eyes, clothes, shoes and sole |
| Own colour | an HSV wheel (angle = hue, radius = saturation), a brightness bar, a preview, an R · G · B readout and a HEX field accepting #12AB34, 12AB34 or the short abc |
| Eyedropper | available on the website only, and that is deliberate: on Android picking a colour from the screen would require screen recording, which is far too invasive for a character creator |
| Black | cannot be picked from the wheel, because the wheel always has some hue — pure black comes from the brightness bar or by typing #000000 |
- Sharing a look. The whole appearance can be copied and pasted as a short text code (266 characters), with no server involved.
- Parity. The palettes are defined twice (JavaScript and Kotlin), so a script compares them together with the block tables. Adding a colour on one side only ends as a validator error, not as a silent difference between the phone and the web.
- Authoring a new item has a fixed order: decide whether it is painted or a block, add it to the catalogue and the palettes, paint it in both texture generators, build its blocks in both prop tables, run the validators, and then judge the result by eye on a rendered strip of frames.
Documented for app version 0.120.1 (updated 2026-09-26).
Machine translation — the Polish version prevails.