Playable sprites · the study explained step by step

The HD fighter, step by step

In Baldur's Gate II, players choose their character's colours: the hero's image is therefore not a finished picture but a paint-by-numbers that the game fills in live. How can it gain the finesse of an HD image without breaking that paint-by-numbers? Three artificial intelligences investigated, then reviewed and corrected one another. Here is their final study, explained for any curious reader, without leaving out any of the technical content.

  • 3 AIs · 4 stages2 studies, 2 cross-reviews, 1 synthesis, 1 final review
  • 774 BAManimation files inventoried · 870 items (ITM)
  • 628 BAMuseful scope identified in the study
  • 12recolouring variants tested
  • −18% / −23%historical E3b/E5 results · Q8 against Q0
  • x2 + BOXselected after in-game trials
Engine verified in the binary Runtime code re-read Off-game measurements reproduced Q3m tested in game · K6 selected Definitive guide: GUIDE_DEFINITIF_SPRITES_HD_BG2EE.md (FR)
From the study to implementation. The explanations and comparators from 29–30 September remain in place. Dated additions cover the next steps: Q3m K6, the V6 reader, x2 + BOX, paperdolls and other animation families. 78 Character animations produced and installed; visual acceptance applies only to explicitly accepted scopes. Production, installation, QA and release remain distinct states.
00 · The starting point

Paint-by-numbers, not a picture

Before the method and the numbers: what is this about, why is it hard, and what was found? The rest of the page details this summary.

The game. Baldur's Gate II: Enhanced Edition (BG2EE) is a role-playing game seen from above. Its characters are small animated images called sprites: the fighter studied here is about sixty pixels tall. On a modern screen, each of those pixels becomes a clearly visible square. BG2 HD Visual Project redraws them more finely, with two (x2) or four (x4) times as many pixels in each direction, without changing their size on screen.

The difficulty. Players choose their character’s colours (hair, skin, two clothing colours), and each piece of equipment brings its own. Every sprite pixel carries a number meaning “this material, this shade”. The BAM also contains a palette, but for these recolourable characters the engine computes the displayed colours at draw time. It is paint-by-numbers, filled on every frame with the current colours. One image serves every fighter; each native range only has 12 shades, from lightest to darkest.

Why a nice upscale is not enough. A neural network can enlarge an image beautifully: it invents volumes and smooth gradients. But what it returns is a finished picture, painted for a single set of colours. To keep it recolourable, it has to be converted back into paint-by-numbers, and that is where everything happens. Twelve shades were enough for a small sprite; in HD, they cut gradients into bands, and a choice of numbers that is perfect under one set of colours leaves fringes under another.

The goal of the study. Find a way to give sprites the modelling of an HD upscale while letting the game colour them itself, whatever colours the player picks, and pin down a recipe precise enough to be programmed.

Why “0x6110”? It is the number the game gives to the “human female fighter” animation (written in hexadecimal, hence the “0x”). She serves as a textbook case: what holds for her will hold for the 78 character animations built on the same principle.

Before, after

Character colours
Original imageE3b prototype
The fighter as the game stores her, enlarged without retouching The same fighter, the HD result the study aims for

Drag the divider. Left: the original image, enlarged without retouching. Right: the E3b Q3m x4 prototype, an off-game preview retained to explain the study. This is not the newer K6 x2 result. Plate-armour body, helmet, shield and sword, idle, facing south. The K6 plate and in-game video provide the later comparisons.

Original fighter, REF palette: dark armour, red and blue details
REF palette30 · 47 · 57 · 12 · 39 · 21 · 3
Original fighter, palette B: steel-grey armour
Palette B21 · 57 · 47 · 8 · 66 · 30 · 0
Original fighter, palette D: blue armour trimmed with gold
Palette D67 · 68 · 47 · 84 · 25 · 57 · 2
Original fighter, palette E: bright blue armour
Palette E30 · 47 · 57 · 12 · 39 · 68 · 3
One and the same file, four sets of colours. Only the seven range numbers shown under each image change, in this order: metal, minor colour, major colour, skin, leather, armour, hair. These are four of the palettes used in the study. Off-game preview: the same palette is applied here to all four layers (body, helmet, shield, sword).
The answer, in one sentence. Keep the paint-by-numbers, but let each pixel sit between two neighbouring shades, in eighths, choosing that position so it stays right under several sets of colours at once; the project's module, already hooked into the game, will do the blending at display time.Method code name: Q3m. New file format: V6 registry. Status: reader implemented, Q3m tested in game; Character x2 production installed.
Finding 1

The limit comes from the game

The image file could address 256 colours. It is the engine that only prepares 12 per material, and it recomputes them on every frame: there is no way to add any without touching the whole game.

Finding 2

The network depends on colours

Show it the same fighter in other colours and it no longer draws the same shadows: 20 to 44% of the numbers change. A recipe tuned under a single set of colours cannot suit them all.

Finding 3

Eighths are enough

With 8 positions between two shades, a range goes from 12 to 89 possible tones. The error against the network's drawing drops by 14% on new colours, and the body's “crawling” in animation falls from 14.1% to 5.7%.

Finding 4

Nothing is settled without the game

The first numbers were measured off-game against a network-generated target. Later trials showed Q3m in the game and led to x2 + BOX. A fidelity metric still cannot decide what looks beautiful: visual judgement belongs to the project author, within the tested scope.

How to read this page

Three cards per section

The goal, the research, the finding: enough to grasp each step without reading everything. The “In plain words” lines rephrase the technical passages.

How to read this page

Proof labels

confirmed measured open chosen rejected
Each statement says where its certainty comes from. The scale is explained in section 02.

How to read this page

The technical content stays whole

Addresses, formulas and file names appear in small print; jargon links to the glossary. Sections 01 to 17 carry the same numbers as the technical presentation (in French).

01 · How the investigation ran

Three AIs, four stages

The study was not written in one go by a single model. It was run like an adversarial expert review: two investigators who do not talk to each other, then review each other; a third who merges; and a final check against the evidence.

The goal

Produce a study you can trust without taking it on faith: every important claim must have been found twice, or checked by someone other than its author.

The research

The same brief given to two models from two different companies, each in its own tool, working on the same game and project files. Then cross-reviews, a synthesis by a third model, and a final review.

The finding

Without reading each other, the two investigators reached the same architecture: the strongest signal in the whole file. And every review found errors, including a calculation mistake that skewed a measurement.

Investigator A · final reviewer

Claude Opus 5.5

Works in the Claude Code tool, with access to the game files and the project code. One of the two initial studies, one cross-review, then the final review.

In the files: labelled “Claude”, “ClaudeCode”.
Investigator B

GPT 6-Astra

Works in the Codex tool, with the same access. The other initial study, then its cross-review, titled “critical comparison”.

In the files: labelled “Codex”.
Synthesiser

5.6 SOL

Wrote neither of the two studies. Its role: merge them into one, keeping the best of each.

In the files: the “ultimate guide”, filed under the label “Codex synthesis”.
Conductor

The project author

Gives the brief, passes the studies from one model to the other at the right moment, and remains the only judge of what looks good in game. No model read the other's work before being allowed to.

Stage 1Two parallel investigations, same brief, no exchange
Claude Opus 5.5

Independent study

Three experiments, ten recolouring methods compared. Proposes sub-shades tuned on several palettes.

GPT 6-Astra

Independent study

Reads the game's program, inventories every file, computes four upscales. Shows that the network depends on colours.

Stage 2Cross-reviews, then an update of one's own study
Claude Opus 5.5

Cross-review

Reads Astra's whole file, replays its own methods on Astra's images (experiment E5), accepts six corrections.

GPT 6-Astra

Critical comparison

Reruns Opus's experiment, gets the same numbers, then finds a sign error in it. Also corrects one of its own claims.

Stage 3Synthesis
5.6 SOL

Merged study, the “ultimate guide”

Removes duplicates, grades every claim by its level of proof, combines the best of both studies into a single production plan.

Stage 4Final review
Claude Opus 5.5

Check against the evidence, last experiments

Goes back to the game's program, the code and the data to re-verify every claim that weighs on a decision. Nine more corrections, file format specified down to the bit.

OutcomeWhat this page presents
Final study

Definitive guide

The reference document for further development. The intermediate studies are kept intact, with their errors listed in section 13.

Why stage 1

Two separate investigations

If two investigators who never compared notes find the same thing, it is probably not chance. If they disagree, you know where to look.

Why stage 2

Reviewing each other

Other people's mistakes are easier to spot than your own. And it was not just about reading: each one reran the other's calculations.

Why stage 3

A third to merge

An author defends their own work. Handing the synthesis to a model that wrote neither study keeps one view from winning by default.

Why stage 4

Back to the evidence

A synthesis can copy a mistake in good faith. The last review starts not from the studies but from the game itself.

The details, in seven steps, in the actual order of the work

  1. Stage 1Two parallel investigations. Same brief, same files, no exchange.
  2. 0129 Sep · 19:46
    Claude Opus 5.5

    Independent study

    Three experiments. E1: measure the game's 256 colour ranges, to see how far apart their 12 shades are. E2: count which colour numbers are actually used, across 65 components of the fighter. E3: the test bench, with 4 layers (body, helmet, shield, sword), two animations (idle and walk), 5 palettes and 10 palettisation methods.

    Proposes the fractional sub-shade fitted on several palettes, with blending at boundaries (method Q8), and a fallback that needs no new DLL (method Q6).
  3. 0229 Sep · 20:08
    Claude Opus 5.5

    E3b erratum

    A correction the model applied to itself. The sampled idle animation contained only 4 distinct poses across 12 frames: the same frames repeated, which diluted the flicker measurement. The experiment was redone as E3b: 20 pose changes, 120 frame occurrences.

    Ranking of methods unchanged; temporal figures revised; two conclusions withdrawn (Bayer dithering “worst everywhere”, flicker “−50%”).
  4. 0329 Sep · 19:52–20:10
    GPT 6-Astra

    Independent study

    An investigator's approach: disassembly of BaldurReal.exe, the game's program, to read what it really does; a complete inventory of its archives (KEY/BIFF); four upscales actually computed on 60 frames; the network's sensitivity to the palette it is shown; dithering tests; interpolation tuned under a single palette.

    The engine reads MPALETTE, not RANGES12; 774 BAM, 870 ITM, 628 useful BAM; rerunning the network under another palette changes 20 to 44% of the indices.
  5. Stage 2Cross-reviews. Each receives the other's study, checks it, then updates its own work.
  6. 0429 Sep · 20:26
    Claude Opus 5.5

    Cross-review + E5

    A full read of Astra's file, then a return match: Opus's methods are replayed on Astra's corpus and palettes (experiment E5), and Astra's method is run with its own quantisation function, untouched.

    On palettes not used for tuning: Q8 −23% vs the current pipeline, −20% vs Astra's interpolation. Six corrections accepted.
  7. 0529 Sep · 20:30
    GPT 6-Astra

    Critical comparison

    Astra reruns Opus's E3b experiment and gets identical numbers (difference 0). It then audits the boundary handling, checks the actual rounding to 3 bits and re-reads the relevant parts of the runtime code.

    Finds a sign error in the temporal alignment; 3 bits ≈ lossless; 13.3% of boundaries involve more than 2 channels; Q6 possible without changing the rendering.
  8. Stage 3Synthesis. Both updated studies are handed to a model that wrote neither.
  9. 0629 Sep · 21:16
    5.6 SOL

    “Ultimate guide” synthesis

    Merging the two files. Before writing, SOL inventories everything: 5,102 files, of which 1,709 are truly distinct once copies are set aside. It sets out a scale of proof and a phased production plan.

    Order Q6 → Q3m → Q8; x2 shipped, x4 after proof; no dithering.
  10. Stage 4Final review. SOL's study goes back to Opus 5.5, which returns to the game files.
  11. 0730 Sep
    Claude Opus 5.5

    Final review

    Every claim that weighs on a decision is re-verified against the binary, the runtime code and the data files. Last checks added: how closely the validation palettes resemble the tuning ones, and which share of the gain comes from each ingredient.

    9 new corrections (Catmull-Rom exists, “unseen” palettes partly seen, Q3m carries 77% of the gain…); V6 format specified to the bit; definitive guide.
What the cross-reviews concluded

Separately, the two models had reached the same architecture: keep the seven 12-shade ranges, and have the project's module blend two neighbouring shades. On everything else they complemented each other. Astra was the more rigorous investigator: it read the game's program itself, found the right colour file and sorted out which files are actually useful. Opus had gone further on method: multi-palette tuning and boundary handling, which Astra described without testing them, were implemented and measured. Both reviews say so in the same terms.

Opus 5.5, in its cross-review“Two independent investigations that converge: that is the strongest signal in this file.” (translated from French)
GPT 6-Astra, in its critical comparison“The best development base is therefore: Codex engine/inventory facts + Claude encoding experiments + corrections and a shared protocol.” (translated from French)

In the images and file names, “Claude” refers to Claude Opus 5.5's work and “Codex” to GPT 6-Astra's (Codex is the tool it worked in). 5.6 SOL's synthesis is filed there as the “ultimate guide”. The study documents themselves are written in French.

Further development · 30 September–4 October

The same architecture, now implemented

The recipe became code: K6 was selected using ten disjoint validation palettes, the Python/C++ V6 reader was implemented, Q3m was tested in game, and x2 + BOX was chosen on 2 October. Production then covered all 78 Character animations and the normal 6110 paperdolls. V7 extends the method to other creature profiles with suitable colour partners.

The four stages above describe the initial investigation. These later steps extend its story while retaining the original measurements and the opinions signed at the time.

The historical measurements below still belong to the original study. The added block at the end describes later decisions and implementation.
02 · The essentials

What is established, measured or open

Every statement carries its level of proof. An off-game measurement is never an in-game validation.

The goal

Separate established facts from expectations. A study written by AIs can sound right and still be wrong: the label says where each certainty comes from.

The research

Every conclusion was graded by its evidence: read in the game or in the code, measured on a sample, observed in game, or merely proposed.

The finding

The initial study distinguished two confirmed facts, two measurements, two open questions and a selected format. K6 validation, the reader and the display choice have since progressed. The following sections retain the original explanation and add implementation at its date.

Confirmed

Read in the game's program, its files or the project's code. Anyone can re-check it, with no visual judgement involved.

Measured

Quantified off-game, on the sample described. Only valid for that sample and that way of measuring.

Observed in game

A dated observation by the project author, on a specific scene. Neither a full quality check nor a published release.

Chosen · candidate

A design choice with its state specified: candidate to implement, implemented, or decided after a trial. Selecting a method does not establish acceptance of all its assets.

Open

The proof is missing. No production decision can rely on it.

Rejected

A discarded direction. The text specifies whether it was tested and rejected or dropped before implementation.

Confirmed

The 12-shade limit does not come from the BAM (256 indices) but from the engine: each range is a row of 12 colours in MPALETTE.BMP, and indices 88 to 255 are integer averages of shades 2 to 9 of two ranges. Everything is recomputed on every frame: the body has no free fixed colour.In plain wordsThe sprite file could address 256 colours; it is the game that only prepares 12 per material, plus blends of two materials. And since it recomputes everything constantly, you cannot “reserve” a slot for a colour of your own.

Confirmed

The project's DLL already captures the realised palette of each layer (body, weapon, off-hand, helmet) and rebuilds the HD texture on the CPU. That is where to act: store a continuous position within the range and let the DLL blend two neighbouring shades.In plain wordsThe project already has a module hooked into the game (a DLL file). It picks up the exact colours the game has just computed for each part of the character, and builds the HD image with them. It only needs to learn how to blend two neighbouring shades.

Measured

Q3m (E3b prototype fitted on several palettes) reduces the error against the model by 14% on the validation palettes; Q8 (Q3m + boundaries) by 18%. Q3m alone carries 77% of the gain. On Astra's corpus, Q8 achieves −23%.In plain words“The error against the model” is the difference between what the neural network drew and what can be reproduced with the game's colours. Sub-shades cut it by 14%, and it is this simple idea, not the refinement of boundaries, that delivers most of the progress.

Measured

Body flicker (alignment corrected): 14.1% → 5.7% with Q3m, 4.8% with Q8. Error diffusion doubles the flicker; dithering is not retained.In plain wordsFrom one frame of an animation to the next, pixels that should stay the same change colour: the surface “crawls”. Sub-shades cut this defect by a factor of about two and a half.

Measured

E3b validations shared 1 to 6 of 7 channels with tuning: only D was nearly independent, with gains retained (−15.5% and −25%). This prompted ten fully new palettes. P1 has since established that separation by range number and RGB ramp at the same channel; K6 results are in section 12.In plain wordsDifferent test colours are not enough if they resemble the tuning colours too closely. The ten new palettes were constructed to avoid that overlap.

Decided

x2 + BOX selected. The Q0 trial on 26 September found x4 slightly better, with a registry 58% larger. The later Q3m and downscaling trials led to retaining x2. It looks more natural, with a slight pixel-art character; x4 can produce a more “plastic” smoothness. These judgements belong to different, dated trials.

Implemented

V6 registry implemented. Base index I, fraction F in eighths, palette dependencies and a decoding contract. Delivered V6 has no boundary plane B and no packed 3-bit fractions: I and F are u8 planes. Without F, reconstructed pixels match V5; files and headers remain different.

State on 4 October

What is now established

Measured: K6 improves all ten validation palettes at x2 and x4. Implemented: V6, exact dependencies and CPU reconstruction. Decided: x2 + BOX for the Q3m Character trials; Nearest for the UI path. Produced and installed: 78 Character animations. Accepted: explicitly validated trials and the 81 normal 6110 paperdoll BAMs.

Constrained Q8 was not implemented: that direction was dropped on 2 October. This does not turn its measured historical prototype into a failed trial. New V7 profiles and V8 contour trials have their own states.

03 · The engine

How the game colours the fighter

First question: what exactly does the game do when it colours a character? The answer was not looked up in documentation but read in the program itself.

The goal

Know the rules of the game, literally. For the HD sprite to react like the original to colours, spells and lighting, you need to know how the engine builds its 256 colours.

The research

Disassembly of the program (reading its machine instructions), tracking the files it opens, re-reading the code of the project's module. Astra disassembled the program itself, Opus started from an earlier project audit; the final review re-checked the addresses.

The finding

Seven “paint pots” of 12 shades, plus 21 blends of two pots computed by a simple average. No free slot on the body. And the project's module already captures these 256 colours at the right moment.

Picture it

A paint-by-numbers where numbers 4 to 15 mean “metal, from lightest to darkest”, 16 to 27 “minor colour”, and so on. Before each frame, the game fills seven rows of 12 pots with the chosen colours, then prepares 21 rows of 8 in-between pots by mixing two rows half and half. The drawing itself never changes: only the pots do.

Verified on BaldurReal.exe 2.7.3.0 (SHA-256 b51093a4…): the string MPALETTE appears once, RANGES12 never. The two files are byte-for-byte identical, which is why off-game tests done with either one remain valid.

  1. 1The character sheet sets seven colours. Metal, minor colour, major colour, skin, leather, armour, hair. The player picks four (hair, skin, major, minor); the rest comes from the class and the items.CRE file: 7 colours.
  2. 2Equipped items impose their own. Plate armour, for example, sets the body's armour, leather and metal. Each item states which layer it recolours and which range it changes.Items (ITM): opcode 7, param2 = 0xPR — P: 0 body, 1 weapon, 2 shield / off-hand, 3 helmet; R: range 0 to 6. E.g. PLAT01: armour 27, leather 23, metal 30.
  3. 3The game fetches each range from its colour chart. MPALETTE.BMP is an image of 256 rows of 12 colours. For each of the seven ranges, the game copies the requested row into the character's palette.SetRange (RVA 0x4221C0) copies a row of MPALETTE into indices 4+12r … 15+12r.
  4. 4It applies the current effects. A spell's tint, lighting, the pause grey, petrification, a magic weapon's glow: all of these alter the 84 shades (7 ranges × 12).Realize applies tint, light, pause grey, petrification and glows to the 84 shades.
  5. 5It computes the blends. For each of the 21 possible pairs of ranges (metal and skin, leather and armour…), it builds 8 in-between colours: the average, rounded down, of shades 2 to 9 of both ranges. They are used for pixels straddling two materials.The 21 pairs (RVA 0x421F7B): P[88+8k+j] = (A[j+2] + B[j+2]) >> 1, byte by byte — neither linear light nor OKLab.
  6. 6Four numbers play a special role. 0 is transparent, 1 is the drop shadow (a semi-transparent black), 2 and 3 are reserved blacks.0 transparent (forced to 0 by the DLL), 1 shadow with partial alpha, 2–3 reserved blacks (2 = marker of 1×1 frames).
  7. 7The project's module takes over. Right after this computation, it copies each layer's 256 colours, builds the layer's HD image with them, stacks the layers in the same order as the game, then hands back a single image to display.The IEE DLL captures P[256] per cell when Realize returns, rebuilds each layer at x2/x4, composites in native order (any non-zero pixel overwrites), then the game draws a texture.

Real palette explorer

Try it: the explorer reproduces the neutral palette’s RGB colours from MPALETTE.BMP and the engine formula. Choose a study palette, enter a row number or click the colour chart: the blends follow. Hover or touch a slot to see what it means. Effects, lighting and native byte packing are not simulated here; in the game, the module captures the palette actually realised by the engine.

The game's colour chart · 256 ranges as columns, 12 shades each, light (top) to dark (bottom)
A click in the chart changes the range…
Palettes from the study
Hover over a swatch.
7 ranges × 12 shades · indices 4 to 87
All 256 indices
21 pairs × 8 · indices 88 to 255

The palettes: REF is the production pipeline's reference; B and C were used to tune the methods; D and E to validate them; “contrast” and “pale” are GPT 6-Astra's profiles; “Default fighter” gives a human female fighter's original colours in the game.

Fixed colours

None on the body

Can you reserve a colour that never changes? On the body, no: every slot follows a colour choice. On equipment, yes in theory: five rows of the chart are a single flat colour. If an item does not use one of its channels, its file can pin one of those rows there. But that means editing shared item files.

Equipment only: rows 74 white, 75 black, 76–78 red, green, blue are constant (re-checked). A genuinely unused channel can be pinned there by the ITM. Intrusive option, outside the first prototype.
What the capture covers

Palette effects: yes

By copying the colours after the game's computation, the module inherits everything that goes through the palette. What the game applies afterwards, however, escapes it and will have to be tested in game.

Covered: player choices, items, resolved RANDCOLR (randomly drawn colours), palette effects and recomputed blends. Not covered: post-palette tint, invisibility, blur, selection, occlusion. To be tested separately.
False oracles

Near Infinity, GemRB

Two reference tools of the community, a file editor and an open-source engine, do not colour exactly like the game. Excellent for inventory and cycles, never as a pixel reference.

Near Infinity samples the blends on shades 0,1,3,4,6,7,9,10; GemRB copies sub-ranges.
04 · The real bottleneck

Why 12 shades are not enough in HD

Twelve shades per material is a staircase of twelve steps from light to dark. On a small sprite, you cannot see the steps. In HD, when the network draws a gentle slope, it has to be rounded to the nearest step: the gradient turns into bands.

The goal

Understand where the bands and flat patches come from when an HD image is brought back to the game's colours.

The research

Opus measured the colour gap between neighbouring shades across the game's 256 ranges (experiment E1). Astra tested what a chart with even steps would give.

The finding

Between two neighbouring shades, the gap is two to five times what the eye can distinguish, and the steps are very uneven. The problem is not picking the wrong step: there are not enough steps.

The measurements. Median step between two neighbouring shades: 0.055 in OKLab, a colour scale matched to perception, i.e. two to five perception thresholds. The steps are very irregular: in metal 30, three times larger in the light shades (0.10 to 0.13) than in the dark ones (0.03 to 0.05). In a typical range, the tallest step is 2.8 times the smallest; in one range in ten, more than 9 times. Invisible at x1, these steps become bands when ReboutCX produces a continuous gradient.

One gradient, four ways to render it

The range comes from the section 03 explorer. First band: the network’s continuous gradient. Second: the 12 native steps. Third: the fractional solution, eight positions per step. Fourth: dithering, alternating two shades in a checkerboard to imitate the intermediate tone.

Between two shades: the eighth

The whole idea of the study, in one slider. The HD file no longer stores just “shade no. I”, but “shade no. I, moved F eighths towards the next one”. The game's module does the blend with the current colours: there is no new colour to choose, and the player keeps their 12 shades.

The eight positions between these two shades, then the next shade
Vanilla ranges and the same ranges redistributed with equal steps
An experiment by GPT 6-Astra (the image keeps its original French labels: “vanilla”, then “écarts égaux” = equal steps): for seven ranges, the top row is the game's, the bottom row the same range redistributed at equal arc length, i.e. with steps of equal height. The coefficient of variation, which measures irregularity, drops from 0.15–0.47 to 0.013–0.027, but 2,437 of 3,072 positions change (79.3%, re-checked). A diagnosis, not a recipe: the fraction already fills the large steps without touching the vanilla colours.

What the demo shows. “12 shades” represents the native limit and the study’s Q0 baseline. “1/8 sub-shades” represents the implemented V6: 89 positions per range (11 intervals × 8, plus the final shade), with no new player colour choices. Bayer imitates the gradient with a pattern. F’s eight values logically need 3 bits, but the current file stores them in bytes.

Odd rows. A few rows of the chart do not run neatly from light to dark, others repeat the same colour. Non-monotonic: 197, 198, 199, 204, 212, 213, 221, 253, 255. Repeated RGB: 74–78, 222, 225. These are cases to interpret, not errors to fix wholesale.

Decision. Never rewrite MPALETTE: the change would be global, for every character, item and mod.

05 · Inventory

What actually needs to be produced

Two counts answered two questions: the repository's index (656 world BAM) and the complete vanilla stock inventoried by GPT 6-Astra (774 BAM). They reconcile exactly, re-checked against the CSV.

The goal

Know how many files must be processed for a single fighter, and which ones are useless, before launching hours of computation.

The research

Astra went through all the game's archives and listed every animation file and every compatible item. Opus started from the index the project already keeps. The two totals differed.

The finding

Both counts were right: they were not counting the same thing. 774 files exist in the game, 656 concern the character in the world, 628 are actually useful for a human female fighter.

In plain words

A character is not one image but a stack of animated layers: the body (which changes with the armour worn), the weapon, the shield or second weapon, the helmet. Each layer has its own animation files, the BAM, one per type of action (walking, striking, casting a spell…). Each BAM holds cycles, i.e. an animation seen from one angle, themselves made of frames, the successive images.

Body

92 BAM

Four versions of the body, one per armour level, each in 23 action files (4 × 23 = 92).

CHFB1 no armour, CHFB2 leather, CHFB3 chain mail, CHFF4 plate (the prefix changes at level 4). 23 suffixes each.
Weapons

166 + 72

166 files for the weapon held in the main hand, 72 for the one held in the off-hand (dual wielding).

Main hand and off-hand (WQN…OA7/OA8/OA9/OG1). Unsplit overlays: their G1 groups the states.
Shields

60 BAM

Twelve shield families. A thirteenth exists in the game, but no item uses it.

Families C0–C7, D1–D4. D0 (5 BAM) has no item.
Helmets

238 BAM

The wearable helmets. Two families filed among them are not helmets: creature attacks, and wings forbidden to humans.

H0–H5, J0–JC. H6 = creature attacks (59 ITM), ZW = wings forbidden to humans.
774 vanilla stock = 656 repository index
              + 85 paperdolls (4 bodies CHFF*INV, 81 WPN*INV/OIN)
              + 33 world stock (D0: 5, H3/H4: 28)

656 index  = 628 useful + 14 ZW + 14 H6

628 useful = 92 body + 166 weapons + 72 off-hand
            + 60 shields + 238 helmets

frames: 178,360 (index) · 185,335 (stock) · 153,422 > 1 px

Reading it: the game holds 774 files for this fighter. 85 are paperdolls, the large figure on the inventory screen, which is a separate project. 33 exist in the game but no item calls them. That leaves the 656 of the repository index (178,360 frames), including 14 files of wings forbidden to humans and 14 creature attacks: hence 628 useful files. Many frames are placeholders (see below): of the 185,335 in the full stock, 153,422 are larger than one pixel.

SuffixCyclesAction
G11 · G10–8 · 9–17walk · one-handed guard
G12 · G1318–26 · 27–35idle · two-handed guard
G14 · G15 · G1636–62hit · death · on the ground
G17 · G18 · G1963–98idle · idle · sleep/get up
A1–A90–8one-handed, two-handed, dual-wield and throwing attacks
SA · SS · SX0–8bow · sling · crossbow
CA0–714 spellcasting variants

The suffix is the end of the file name: CHFF4 + G11 = body in plate armour, walking. Cycles are numbered by direction: 9 directions per action, hence the blocks of 9 (0–8, 9–17…).

Directions

9 + 7 mirrored

The character can face 16 directions. Only nine are drawn; the seven east-facing orientations are obtained by flipping the image as in a mirror. One study said “9 + 5”: corrected.

S to N stored, seven east orientations obtained by mirroring. Not “9 + 5”.
Placeholders

1×1 frames, index 2

Many frames are one-pixel black placeholders. The game relies on them: they are kept exactly as they are, simply not upscaled.

CHFB1G11: 846 entries, 90 real frames. Keep frame, lookup, centre, cycle; skip only the inference.
Sharing

Route, don't replace

The same files serve several characters. So no game file is ever replaced: the project's module already knows how to route, animation by animation, to the right HD version.

CHFB1–3 serves 6 animations, WQN* 13. The DLL already routes by animation ID → memberships → component.
05 · Visual addition

How equipment is assembled

An equipped fighter is built from independent images. The body, weapon, shield and helmet need not have the same size or the same centre in their files. Their anchor points align them; they then follow the cycles and direction defined by the engine.

Body · xBR x2
Body
Weapon · xBR x2
Weapon
Shield · xBR x2
Shield
Helmet · xBR x2
Helmet
Real images reused from the old sprite page: xBR x2 examples in working false colours. They illustrate the components, not a synchronised pose or the newer K6 result.
1 · Select the bodyUnarmoured, leather, chain or plate: CHFB1, CHFB2, CHFB3 or CHFF4. Armour selects a different base body.
2 · Align the layersNative centres place each image on the same anchor. Files with different dimensions can therefore fit together.
3 · Follow the engineDirection, action, mirroring and layer order remain native. A weapon can pass in front of or behind the body depending on orientation.

Each layer is coloured with its own realised palette, then composited. The shield occupies the off-hand path; dual-wielding uses that path for another weapon. An absent layer remains absent. Upscaling preserves native geometry and anchors: the texture has more pixels while the character occupies the same space in the world.

Offline simulation

Separate body, helmet, shield and weapon, native centres and synchronized offline assembly
Offline simulation · Four Q3m K6 x2 layers from the same pose, aligned using their native centres. The helmet’s reference point may lie outside its rectangle. A neutral palette and fixed layer order are used for this demonstration; direction-dependent ordering, effects and engine occlusion are not reproduced.
Explore layers, poses and colours

Layer origin = shared reference − centre × zoom

Teaching display at native zoom ×3; x2 pixels enlarged without interpolation. Three genuinely synchronized poses are available. Engine effects, lighting and occlusion are not simulated.

Inventory and production describe different scopes. The 628 BAMs remain the useful subset explained above. Full production covered the index’s 656 BAMs, or 178,360 frames. For the UI, 81 normal BAMs were selected and accepted (4 bodies + 77 equipment files, 163 frames), from the 85 UI files originally inventoried.

The inventory figure has its own path

Preparatory preview: native paperdoll left, Q3m x2 right
Preparatory CHFF1INV preview: native Nearest enlargement left, Q3m K6 x2 right, DEFAULT palette. Two pieces are placed using native centres. This preview’s shadow alpha 128 is diagnostic; it is not an in-game UI capture and does not carry the final batch’s acceptance.

Complete UI production and acceptance followed this preview. They concern the 81 normal 6110 BAMs and retain Nearest; they do not automatically accept other avatars’ paperdolls.

Research plates from 29 September, retained. xBR remains Q3m’s structural guide; ReboutCX generates shading targets. Delivered output is re-encoded rather than the model’s raw RGB image.
06 · Four real upscales

xBR and ReboutCX, at x2 and x4

GPT 6-Astra computed the four variants on 60 real frames: four bodies, plus a complete attack cycle (14 frames) for the body, sword, shield and helmet. The plates are enlarged to 8 screen pixels per source pixel.

The goal

See concretely what each upscaling technique brings, and what is lost when going back to the game's colours.

The research

Four versions actually computed (xBR x2, xBR x4, ReboutCX x2, ReboutCX x4), then a decisive test: rerun the network with other colours, and count what changes.

The finding

The network does not draw the same shadows depending on the colours it is shown: 20 to 44% of the numbers change. A recipe tuned under a single palette therefore cannot suit all of them.

Family 1

xBR, a rule-based algorithm

It looks at each pixel and its neighbours, and rounds off the staircase edges according to fixed rules. It creates no colour: every enlarged pixel reuses the number of an original pixel. Same input, same result, every time.

xBR picks the geometry from the “clown” colours (the file's working false colours) and carries the indices across.
Family 2

ReboutCX, a neural network

A program trained to redraw images at four times the size. It adds modelling: volumes, highlights, gradients. But it takes in and returns an already coloured image, knowing nothing of numbers, materials or transparency.

ReboutCX processes a recoloured preview and takes alpha and classes from the xBR guide at the same scale.
Three sources, three roles

The original file gives the structure

Cycles, frame order, anchor points, placeholders: taken as they are from the BAM, never recomputed.

Three sources, three roles

xBR gives the material

Transparency, shadow and class (the material) of each HD pixel come from the xBR version at the same size, which serves as a guide.

Three sources, three roles

ReboutCX gives the modelling

The network provides volumes and gradients, and nothing else: never the transparency, never a pixel's material.

Item
View
Comparison plate by GPT 6-Astra

The “inference” views only exist for the unarmoured body, the plate body and the helmet: the three items of the sensitivity test below.

xBR x2

Exact witness

Exact source colours, better outlines, remaining steps. No new shade. Path accepted in QA for playable characters (with the Catmull-Rom display filter).

Direct xBR x4

Fine witness

A true xbr4x, not two x2 passes. Finer curves, same colours: of little interest on its own.

ReboutCX x2

x4 network + BOX

There is no x2 model: the x4 result is reduced by block averaging (BOX), in floating point, before any rounding. Smoother surfaces, small highlights sometimes smoothed away.

ReboutCX x4

Research master

The reference working version. RRDBNet 64nf/23nb, 16.7 M parameters, RGB with no alpha and no temporal memory: it processes each frame on its own. Provenance and licence unknown, to be clarified before any distribution.

What if the player changes colours?

Palette
The same image, recoloured under another palette
Attack cycle, vanilla and four upscales
Complete attack cycle CHFB1A1, 100 ms per frame (a rate chosen for comparison, not the game's). The equipment layers were evaluated separately.

The finding that shapes everything. Recolouring the numbers works, as the plate above shows. But recolouring is not redrawing. Rerunning ReboutCX inference under another palette, instead of recolouring the result, changes 31.99% / 44.39% of the body's indices, 20.26% / 36.28% of the heavy armour, 30.91% / 37.04% of the helmet (“contrast” / “pale” profiles). The network does not shade the same way depending on the colours it receives.

Keeping the classes keeps recolouring possible, but not good modelling under every colour. Hence multi-palette fitting: choosing the numbers so they stay good under several sets of colours at once.

Candidates limited to the source frame. This study’s Q0 baseline only allowed shades already present in the original frame. Allowing the entire class reduced error by only 5.9% at x2 and 6.2% at x4. This remains the conclusion of that trial: the main bottleneck was 12-level discretisation. The selected Q3m then searches fractional positions within the allowed class under several palettes.

For the curious: on the project's machine (RTX 5090), the network loaded in 3.3 s and upscaled the 60 frames to x4 in 2.6 s; xBR processed them in 0.12 s (x2) and 0.14 s (x4). A micro-measurement on this small sample, not a production throughput.

07 · Methods tested

Bringing the model back to a living palette

All start from the same ReboutCX output and the same classes (xBR guide). Only the way back to dynamic colours changes. Verdicts after all the reviews.

The goal

Find the best way to translate the network's image, made of thousands of tones, into numbers the game can recolour.

The research

Ten approaches pitted against each other on the same images, from the same starting point. Only the step back to the palette changes.

The finding

Dithering, single-palette tuning and HD reclassification are discarded. Q3m is selected and implemented with K6. The Q8 prototype remains explained and visible in the study; constrained Q8c was dropped without implementation on 2 October.

Picture it

The network paints with endless tubes of paint; the game has only 12 pencils per material. Palettising means deciding, for each pixel, which pencil best imitates the paint. The methods differ on three points: which pencils are allowed; whether an in-between tone can be faked by alternating two pencils (dithering) or truly mixed (fraction); and under how many sets of colours the choice is checked.

CodeMethodStoresRuntimeFinal verdict
xBRDirect xBR, index provenanceNo neural network: rule-based upscaling that keeps the original numbers.indexcurrentaccepted path · option
Q0Study Q0 baseline: nearest shade among those in the x1 frame, REF paletteRound each pixel to a shade used in the original image, judged under one palette.indexcurrenthistorical baseline
Q1Same, whole classSame, but all 12 shades of the material are allowed.indexdependency maskno gain in validation
Q2Bayer 4×4 anchored at the BAM centreDithering: alternate two shades following a fixed checkerboard to fake the in-between. The checkerboard is pinned to the character so it does not slide when they move.indexcurrentrejected by default
Q7Error diffusion along the rangeAnother kind of dithering: a pixel's rounding error is pushed onto its neighbours. Pretty on a still image, unstable in animation.indexcurrentrejected
Q6One index chosen over K palettes at onceStill a single number per pixel, but chosen to be the least bad under several palettes.indexV6 without fractionoptional step
Q3Fraction fitted on REF only (= Astra's interpolation)Sub-shades: each pixel sits between two neighbouring shades, but the position is tuned under a single palette. Astra had the same idea independently.index + fractionV6overfits the palette
Q3m3-bit fraction fitted on K palettesSub-shades in eighths, tuned to stay right under several palettes. The chosen method.index + fractionV6selected · K6 implemented
Q5Q3m + class re-estimated in HDQ3m, letting the network re-decide the material of some pixels.index + fractionV6no gain outside tuning
Q8Q3m + weighted blend of two classes at boundariesQ3m, plus a measured blend of two materials for the pixels on their boundary.+ boundary planeoff-game prototypeQ8c dropped, unimplemented

“Stores” means the information needed per pixel. Paths labelled “current” refer to the runtime at the study’s date; V6 is now implemented. K is 3 in E3b (REF, B, C), then 6 in P1. The E3b prototype used 4-bit fractions; the 3-bit check and the V6 eighths contract are separate. Ablation: removing an ingredient to measure its contribution.

Study of 29–30 September: original images, numbers and protocol retained. Q0 labelled “current” means the study’s baseline. New K6 results are presented separately in section 12.
08 · E3b comparator

Slide and compare

Composite of the plate body CHFF4 + helmet HELM01 + shield ISHLD03 + sword BDSW1H06, facing south. Tuned on REF, B, C; D and E were not used for any tuning (E differs from REF only in the armour).

The goal

Judge for yourself. The numbers say what is close to the network's drawing; only your eyes say what looks good.

The research

The images from experiment E3b: the complete fighter, idle and walking, rendered by each method under four sets of colours.

The finding

Under the tuning palette, everything looks alike. Under another, the current method leaves light fringes around the armour plates; Q3m and Q8 remove them.

How to use it: pick a method on the left, one on the right, then drag the divider. Change palette to see what happens when the player picks other colours. “Original game” shows the starting image, enlarged without retouching.

Palette
Pose
Left variant Right variant
  • Current Q0 under B, D and E: light or orange fringes around the chest plates and greaves. The 88–255 blends chosen under REF become conspicuous under another palette.WhyAt boundaries between two materials, the current method readily picks a “blend” colour. Under the tuning palette, that blend lands exactly on the right tone. If the player changes one of the two materials, the blend changes too and no longer resembles the network's drawing.
  • Q3m keeps smooth volumes; Q8 removes almost all the fringes. It is the variant closest to the model output.
  • Bayer: a regular pattern on the large plates. Diffusion: grain that changes from one frame to the next.
  • The raw model output is recomputed for each palette: a target, never reachable in game, where a single version of the indices serves all of the player's colours.
  • The original game reminds you of the starting point: same numbers, same colours, four times fewer pixels in each direction. Tile added for this page, with no AI computation.
Palette
Pose
Plate of the variants
Row 1: xBR x2, xBR x4, study ReboutCX x2, study ReboutCX x4, Bayer x4. Row 2: Q6 x4, Q8 x2, Q3 x4, Q8 x4, raw model output. The layer order of the preview is approximate. Labels on the image are in French: “RCX” = ReboutCX, “actuel” = current, “idx multi” = multi-palette indices, “frac+bord” = fraction + boundary, “GT modèle” = model output (ground truth).
Torso detail
Study of 29–30 September: original images, numbers and protocol retained. Q0 labelled “current” means the study’s baseline. New K6 results are presented separately in section 12.
09 · E5 cross-validation

Opus's methods on Astra's ground

Complete attack cycle (14 frames) of CHFB1A1, WQNS0A1, WQNC0A1, WQNJ6A1. Validation on D and Astra's “contrast” and “pale” profiles. Astra's interpolation is run with its own function, imported read-only.

The goal

Decide between the two studies without home advantage: a method can shine on images chosen by its author and disappoint elsewhere.

The research

During its cross-review, Opus replays its methods on Astra's images and palettes (a complete sword swing, four layers), and runs Astra's method with Astra's code, untouched.

The finding

Both models had the same starting idea, with the same results. What makes the difference is tuning on several palettes: −23% error vs the current method, −20% vs Astra's interpolation.

Palette
Body CHFB1A1 frame 6, six variants

Q3 and Astra's interpolation are the same idea: a fraction tuned under a single palette. Same numbers (47.2 and 47.0), excellent under their own palette, weak elsewhere.

Q8: −23% vs the current pipeline, −20% vs Astra; on D alone, 58.1 → 43.6.

sRGB or linear light for interpolation: 37.8 vs 38.1. No useful difference: the runtime will interpolate in sRGB bytes, simpler and bit-exact.In plain wordsThere are two ways to mix two colours: average the stored numbers directly (sRGB), or first convert them into amounts of light (“linear”), which is physically more accurate. Here there is no visible difference: the simpler one is kept, which also gives exactly the same result on every machine.

Limit: E5 does not include Q3m, so there is no ablation of the boundaries on this corpus.

Astra's interpolation tuned on one palette, rendered under three palettes
Figure by GPT 6-Astra: interpolation tuned under a single palette, rendered under three. Under “contrast” and “pale”, grey and green fringes remain at the range boundaries: this is what multi-palette fitting fixes. (Header in French: same indices + same weights; 3 player palettes; simulation without lighting.)
Study of 29–30 September: original images, numbers and protocol retained. Q0 labelled “current” means the study’s baseline. New K6 results are presented separately in section 12.
10 · Temporal stability

In motion, and a sign error

A still image can be superb and the animation unpleasant: if pixels change shade for no reason from one frame to the next, the surface “crawls”. It is also in this measurement that the cross-review found the study's most instructive error.

The goal

Measure the crawling, because players see their character in motion.

The research

Overlay two successive frames of an animation, find the pixels the network draws almost identically, and count those whose rendered colour jumps anyway.

The finding

With sub-shades, the body's crawling drops from 14.1% to 5.7% (4.8% with Q8). Error diffusion doubles it. And the measurement itself contained an error, found by the other model.

The sign error, told

To compare two frames of an animation, you first have to overlay them correctly. Each frame has its anchor point, and that point is not in the same place from one frame to the next. Opus's script computed the offset the wrong way round: a simple flipped sign. As long as the anchor did not move, no consequence; as soon as it moved, pixels that did not correspond were being compared. And it moved often: over the 19 idle transitions, 9 times for the body, 15 for the helmet, 10 for the shield and 19 for the weapon.

GPT 6-Astra found it by redoing the calculations, proved it with an artificial test (the same image, cropped), then recomputed everything. The final review found the same error in experiment E5.

Astra found that the frame alignment compared different positions whenever the anchor moved: ox = (centre_b − centre_a) × scale instead of (centre_a − centre_b). Synthetic test: 5 wrong comparisons out of 5 before, 0 out of 6 after. The figures below are recomputed.
Idle cycle, four methods
Idle, 20 pose changes, palette B, x4. Left to right: study Q0, Bayer, error diffusion, Q8 (French labels: “actuel” = current, “frac+bord” = fraction + boundary). Watch the large plates: that is where grain appears or not.

Share of still pixels (target Δ < 0.01) whose output changes by more than 0.02. Always publish the aggregation: over the 4 unweighted layers, Q8 goes from 13.6% to 10.0%; weighted by pixels, from 13.2% to 4.9%.In plain wordsA pixel is “still” when the network draws it almost identically on both frames; it “flickers” if the final colour changes anyway. Depending on whether the four layers are weighted equally or each is weighted by its number of pixels (the body then counts far more than the helmet), Q8 goes from 13.6% to 10.0%, or from 13.2% to 4.9%. Both are true; always say which one you quote.

Why it crawls

Shade-by-shade decisions

When the model hesitates between two shades, a tiny variation flips whole pixels. With the fraction, the same hesitation costs only an eighth of a shade.

Dithering

Bayer: not by default

Pixel error +6 to +10%, visible pattern. After a σ 0.5 blur (a slightly blurred image, like the eye at a distance), it beats no dithering (0.0125 vs 0.0139), but interpolation does even better (0.0114). Noise renewed on every frame: forbidden.

Limits

No surface tracking

Pixels aligned by anchor, shadow included, loops and durations ignored: if the arm moves, the measurement does not follow it. The E5 attack has too few still pixels to conclude. A class-restricted flow is needed, i.e. tracking the motion of each material.

Real cycle, dithering strategies
GPT 6-Astra, real 14-frame cycle: index changes of 7.61% with a fixed Bayer checkerboard, 8.23% without dithering, 32.65% with noise renewed on every frame. (French labels, left to right: no dithering, fixed Bayer anchored at the actor's origin, random noise per frame.)
Same frame repeated, varying noise
GPT 6-Astra, the same frame repeated 14 times: with the fixed Bayer checkerboard, 0 changes; with renewed noise, 30.06% of the pixels move although nothing moves. (Same French labels.)
P1 addition · 30 September

Temporal evaluation was also expanded

Across ten validation palettes, weighting recolourable pixels and comparing distinct poses including the loop seam, Q0 → Q3m K6 changes fall from 20.28 to 8.36% at x2 and from 21.66 to 9.40% at x4. Unweighted aggregation gives different values: 29.63 → 23.91% and 29.07 → 24.01%.

These results complement the older charts; their masks and aggregation differ. Alignment still uses anchors without optical flow: these figures do not establish moving-surface tracking or actual timing in seconds.

11 · Display

x2 or x4 on screen

The game preserves native geometry and displays the texture at the selected zoom. At the study’s date, the runtime used Nearest, Linear or Catmull-Rom without mipmaps. Later trials added BOX and tested x4 mipmaps. The selected Q3m choice for Character is x2 + BOX; the older simulations and Q0 trial remain below.

The goal

Decide how many pixels to ship. Four times finer only matters if the screen can show it.

The research

Display simulation at several zooms, followed by the Q0 trial on 26 September. Later evidence includes the Q3m x2/x4 videos of 1 October and BOX/mipmap trials at the zoom actually used.

The finding

The final choice for the Q3m trials is x2 + BOX. x4 can smooth outlines further, but its rendering looks more “plastic”. x2 retains a more natural pixel-art character. BOX was preferred to mipmaps when zooming out, and x2 reduces compositing work and upload traffic.

In plain words

The HD sprite is not displayed bigger: it takes the same space on screen, with more pixels inside. Depending on the player's zoom, the screen sometimes has fewer pixels to offer than the image contains, and the image must be shrunk. There are two ways to do it: nearest neighbour, which takes a single image pixel for each screen pixel, at the risk of skipping some (aliasing); or area downscaling, which averages the pixels covered. Mipmaps are copies shrunk in advance, from which the graphics card picks according to the displayed size.

In the historical simulation, Nearest selects one texel while Linear and Catmull-Rom reconstruct the image locally. The later BOX filter measures the texture area covered by a screen pixel and averages it. It keeps nearest neighbour when magnifying. x4 mipmaps were another tested mode, using trilinear minification and Nearest magnification.

Each thumbnail shows nearest neighbour on the left and area downscaling on the right. The number measures the gap between the two: the smaller it is, the less nearest neighbour damages the image.

Corrected reading. The gap between the two methods only puts x4 at a disadvantage at integer zoom 2, where x2 is exact by construction: each x2 pixel then lands exactly on a screen pixel. At 1.0, 1.3 and 3.0, x4 aliases less than x2. The claim “x4 only useful from zoom 3” is withdrawn.

In game, on 26 September: x4 ReboutCX on all of 0x6110, with nearest neighbour, judged “slightly better than x2”, 59.1–60 FPS, p95 16.8–17.3 ms (95% of frames computed in under 17.3 milliseconds: no loss of smoothness), then restored to x2.

x4 test of 26 Sepx2x4
Uncompressed indices947.6 MB3,790.4 MB
Compressed registry195.9 MB309.6 MB
Ratio—×1.58
Verdictreferenceslightly better

Gap between nearest neighbour and area downscaling for every simulated combination, zooms 1.0 to 3.0 (smaller = less aliasing). At zoom 2.0, x2 is 0 by construction; at every other zoom, x4 beats x2 for each method.

x2 retained for Q3m production; BOX selected for Character world rendering. x4 remains a research and comparison scale. The 26 September table describes Q0: its +58% does not describe every new Q3m registry.Catmull-Rom retains its historical xBR acceptance of 11 September. The later Q3m choice is a separate decision.
11 · New trials · 1–2 October

BOX, mipmaps and Catmull-Rom: three filtering approaches

When zooming out, several texels — texture pixels — may fit inside one screen pixel. Picking just one can remove a thin line or make it flicker. Filtering determines how those texels contribute to the displayed colour.

BOX

Average texels within the covered area, weighted by each texel’s overlap. When magnifying: Nearest.

Average texels within the covered area, weighted by each texel’s overlap. When magnifying: Nearest.

Mipmaps

A texture pyramid: original, half, quarter… The GPU blends levels according to minification.

A texture pyramid: original, half, quarter… The GPU blends levels according to minification.

Catmull-Rom

Local cubic interpolation over 4 × 4 neighbours. This fixed neighbourhood does not average an entire large minification footprint.

Local cubic interpolation over 4 × 4 neighbours. This fixed neighbourhood does not average an entire large minification footprint.

Conceptual diagrams, not comparative captures. Squares represent texels; BOX’s gold outline shows a covered area not aligned with the texel grid.

Transparency and edges. BOX and the tested mipmaps use premultiplied alpha: transparent pixels must not contribute black to the outline. RGB is weighted by alpha before reconstructing the colour. The BOX shader samples the base texture; it stores no pyramid.

Why mipmaps cost more here. The sprite is recoloured and recomposited during play. Reduced levels in the x4 trial therefore had to be regenerated after uploads, together with their masks. This cost belongs to that dynamic implementation; mipmaps remain useful for static textures.

Measured sessionCPU composition / frameBase RGBA upload
x2 + BOX0.148 ms11.082 MB/s
x4 + BOX0.280 ms44.269 MB/s
x4 + mipmaps4.258 ms—

Short independent sessions on the studied Character scene, near 60 FPS; movement and zoom paths were not strictly identical. Upload time is nested in composition, so times must not be added. Base traffic is not GPU memory usage; no global FPS improvement is inferred.

Visual result. BOX was preferred to mipmaps as the camera zoomed out. The choice recorded on 2 October is x2 + BOX without mipmaps: a more pleasing appearance and lower cost in these trials. Paperdolls remain Nearest because UI rendering uses a separate path. Later monster experiments can use another filter without changing this Character decision.

Catmull-Rom tested. This filter already existed and had been accepted with xBR. For the Q3m choice presented here, its smoothing was less convincing than BOX. It reconstructs an image locally; it does not replace averaging the full footprint at strong minification. No specific defect is attributed to it without a controlled comparison.

Offline simulation

Simulation of Nearest, BOX, mipmaps and Catmull-Rom on the same pose, with helmet close-ups
Offline simulation · Identical pose, x4 texture, position and zoom for Nearest, BOX, mipmaps and Catmull-Rom. Close-ups enlarge the pixels already displayed. Mipmaps are modeled on the CPU using premultiplied RGBA8 area averages; the OpenGL driver kernel and GPU timings are not reproduced. The established Character choice remains x2 + BOX.
Compare filters or x2 / x4 textures

BOX and Catmull-Rom follow the shader formulas layer by layer with premultiplied alpha. The mipmap pyramid uses a CPU model of RGBA8 area averages: the OpenGL driver kernel is not reproduced. Transparent borders are assumed isolated. GPU precision, FPS and occlusion differences are not represented.

The x2 / x4 comparison uses the actual preserved textures without inventing detail. It complements the already approved video: the more natural feel of x2 and the sometimes plastic appearance of x4 remain visual judgments, not measurements computed here.

11 · Real captures · 1 October

Compare x2 and x4 in motion

The existing HTML player is embedded below with its two original MP4 recordings, without additional re-encoding. Drag the divider, slow playback or step through frames. Character zoom ×3 enlarges the recording without creating detail.

Open the player on its own

The recordings are 2560 × 1440 at about 59.94 frames/s. Initial alignment advances x4 by ten frames, or 0.167 s, for the selected fighter; other actors can have different animation phases. The common excerpt lasts about 5.39 s and shows idle motion, not every action or effect.

Why x2 remains selected. x4 can smooth some outlines further, but this smoothing can give sprites a more “plastic” appearance. Upscaling cannot reliably recover details missing from the original image. x2 retains a more natural look and a slight pixel-art character, considered more pleasing to the eye. x4 trials provided comparisons; x2 was retained as the production scale.

BOX shader detail: area averaging for minification, Nearest for magnification. The current path limits the footprint to 16 texels per axis; beyond that it falls back to Nearest. P4 zoom trials stay within this range.

Study of 29–30 September: original images, numbers and protocol retained. Q0 labelled “current” means the study’s baseline. New K6 results are presented separately in section 12.
12 · E3b measurements

The numbers

4 layers, idle facing south (20 poses) and walk facing south (10), 5 palettes. Error = mean ΔE OKLab ×1000 against the model output under the same palette. Shorter = better. Fidelity to a generated target, not perceived beauty.

The goal

Put a number on each method, to compare other than by eye, and see whether it still holds when the colours change.

The research

120 images (4 layers × 30 poses), 5 palettes, 10 methods, two sizes. Three palettes tune the methods, two check them.

The finding

Q3m cuts the error by 14.1% on the validation palettes, Q8 by 18.4%. Sub-shades tuned on several palettes cover 77% of the way; boundaries add about 5%.

How to read · 1

The error

For each pixel, the distance between the colour the network drew and the one the game will display, measured in OKLab. The values are averaged and multiplied by 1000 to avoid leading zeros: 40 means 0.040, a difference the eye can perceive. The shorter the bar, the better.

How to read · 2

Tuning and validation

Like a student revising on past papers, a method can learn by heart the palettes used to tune it (REF, B, C). So it is also judged on palettes set aside (D and E). Excellent in tuning and mediocre in validation, it has overfitted.

How to read · 3

The limit

This measures fidelity to the network's drawing, not beauty. If the network draws badly, being faithful to it is no virtue. Hence the importance of the in-game test.

The seven numbers are, in order: metal, minor, major, skin, leather, armour, hair. The last column comes from a check added during the final review: it counts the channels identical to those of a tuning palette. The higher it is, the less unfamiliar the validation.

Q0 · September studyChosenStepRejectedOthers
Breakdown. On x4 validation, Q0 50.4 → Q3m 43.3 → Q8 41.1. The multi-palette fraction brings 77% of the gain, the boundary about 5%. On D alone: 68%. Hence the order: Q3m first, Q8 only if its own gain shows.Q6 improves validation (−8.6%) but degrades the REF production palette by +22.6%: a compromise number across three palettes costs on the one that alone used to count.

Historical size: a zlib sample excluding Q8’s boundary plane, masks and metadata. E3b used 4-bit fractions; a separate check showed little loss from switching to 3 bits. These sizes do not represent current V6 files. D shares 1 of 7 channels with tuning, E shares 6. P1’s XPRESS_HUFF measurements are added below with their own scope.

12 · New P1 results · 30 September

K6: six tuning palettes, ten distinct validations

Selected fitting gives equal weight to REF, DEFAULT and four colour rotations. At the same channel, VAL01–VAL10 share neither a range number nor an identical RGB ramp with the union of these six palettes. LEGACY_D and LEGACY_E remain historical diagnostics and do not determine K.

The corpus has 180 occurrences, 144 distinct BAM frames, four layers and all four armour bodies; 18 palettes produced 2,592 targets. The score averages ten OKLab errors, weighted by recolourable pixels, against ReboutCX’s generated image.

Methodx2 ΔE ×1000x2 gainx4 ΔE ×1000x4 gain
Q033.305—35.552—
Q6 K628.72913.74%30.63613.83%
Q3m K326.24321.20%28.53019.75%
Q3m K424.94025.12%27.22023.44%
Q3m K624.28527.08%26.52325.40%

x2 · error, shorter = better

Q033.305
Q3m K624.285

x4 · error, shorter = better

Q035.552
Q3m K626.523

Why K6. K3, K4 and K6 improve all ten validation palettes. K4 stays 2.70% above the best x2 score and 2.63% above the best x4 score; the set criterion was ≤ 2%. K6 is therefore selected at both scales. This does not promise improvement under every palette: REF alone has 6.10% more error at x2 and 11.59% more at x4, while DEFAULT improves by 30.43% and 29.64%.

View the real K6 x2 plate
P1 plate: xBR, Q0, Q6 K6 and Q3m K6 under six palettes
Left to right: xBR, Q0, Q6 K6, Q3m K6. Rows: REF, DEFAULT, VAL01, VAL09, VAL10, LEGACY_D. Off-game illustration, illustrative shadow alpha 128, Nearest ×3 enlargement; not an in-game capture.

Actual compression measurements. With XPRESS_HUFF, separate planes and a 32-byte mask per frame, the P1 sample grows from 173,704 to 343,620 bytes at x2 (×1.98), and from 407,818 to 859,646 at x4 (×2.11). Here Q3m x4 is 2.50 times the size of Q3m x2. V6 headers and the representative table are excluded: these are sample sizes, not final package sizes.

For reference, the study’s partial zlib estimate was about ×1.98 at x2 and ×2.26 at x4 for fractional methods, excluding boundaries and metadata. This historical estimate remains informative but sets no V6 budget. The XPRESS_HUFF sizes above use a different protocol.

13 · Corrections

What each review corrected

The original studies remain intact. The errors are listed here with their evidence; none reverses the architecture, several change the order or scope of decisions.

The goal

Show that the protocol is worth it. A study with no errors found is not a study without errors.

The research

Each review had to redo, not just re-read: rerun the scripts, reopen the game's program, recount the files.

The finding

Sixteen corrections. None overturns the solution; several change the order of work or the scope of a conclusion. Nobody escapes: each model was corrected by another, and Opus also by itself.

ClaimSourceCorrectionFound by
Catmull-Rom “not implemented”5.6 SOL synthesisCatmullRom mode present (premultiplied 4×4 shader); xBR QA of the playable characters accepted with this filter on 11 SepThe display filter does exist, and it is even the one the project author used when validating the playable characters in xBR.final review
“Unseen” validation palettesallE: 6/7 channels shared with training, contrast 5/7, pale 4/7, D 1/7They closely resembled the tuning palettes; only D is truly new.final review
Q8 gain attributed to boundariespresentations77% of the gain comes from Q3m; boundary ≈ 5%Most of the progress comes from sub-shades, not boundary handling.final review
“x4 aliases more than x2 at the usual zoom”OpusTrue only at exactly zoom 2; in game, x4 judged slightly betterfinal review
“ReboutCX accepted in game on 0x6100”Opus's cross-review2 compositions accepted + 1 non-inferiority, preference undecided; xBR remains the accepted path at scaleOn the human male fighter (0x6100), ReboutCX was accepted for two compositions only (Minsc's body and equipment, then Anomen's); a third was judged “non-inferior”.final review
Fraction “q/7”5.6 SOL synthesisf/8 towards the next shade, 89 positions, as measuredfinal review
“The 2nd weapon keeps weapon semantics”5.6 SOL synthesisNot demonstrated; no effect on the encoding (palette captured per cell); in-game testfinal review
“Extend the cache key to the palette”OpusAlready the case (reboutcx_cache_p12.py:33-39)final review
E5 temporalOpus's cross-reviewSame sign error as E3b (e5_cross.py:237)final review
The engine reads RANGES12OpusMPALETTE; identical filesGPT 6-Astra
Paperdolls WPM; H6 and ZW to produceOpusWPN; H6 = attacks, ZW forbidden to humansGPT 6-Astra
Temporal alignmentOpus, E3bSign flipped; body Q0 → Q8: 14.07 → 4.82%GPT 6-Astra
Q8 “3 bits”; size ×2–×2.3OpusMeasured at 4 bits (3 bits ≈ lossless); size = that of Q3m without boundaryGPT 6-Astra
“Floor of 0.04 impassable”OpusResidual of these methods, not a bound0.04 was the error left by these particular methods, not a proven limit.GPT 6-Astra
Full classes: DLL required / no changeAstra / OpusRendering unchanged, but the meaning of dependencies changes: explicit V6 maskEach was half right: the displayed image does not change, but the meaning of a field in the file does. Hence the V6 format's dependency mask.cross
Sampled idle (4 poses / 12)Opus, E3E3b: 20 pose changesOpus itself
Added corrections · 4 October

What changed since the presentation

  • “Method never seen in game”: Q3m trials completed; QA scopes remain separate.
  • “K still to choose”: K6 selected after ten disjoint validations.
  • “V6 with an optional boundary”: I/F and dependencies implemented, without boundaries or packed fractions.
  • “No downscaling or mipmaps”: BOX added, x4 mipmaps tested; x2 + BOX selected.
  • “Monsters have no problem”: V7 adapts partners to fixed or recolourable profiles.
  • The old 4-bit E3b charts remain historical; they are not measurements of 3-bit V6.
14 · The chosen recipe

An HD sprite that stays a palette

The master holds the geometry, each pixel's class and a continuous position within its range; the engine supplies the colours. The player keeps their choices, and palette effects follow with no specific code.

The goal

Turn the results into a precise recipe: what to record in the HD file, and how the game's module must read it back.

The research

Merging the three studies, then a bit-level specification during the final review: the same computation must give exactly the same result when building the files (in Python) and in the game (in C++).

The finding

An implemented V6 that keeps the old format’s idea and adds fractions and explicit dependencies. Without fractions, it reconstructs the same pixels as V5. The boundary plane imagined during the study was not delivered; its proposal is retained below as a historical explanation.

The complete chain, from the original file to the screen

  1. 1The original file (BAM). It provides the structure: pixel numbers, cycles, frame order, anchor points, placeholders.BAM V1 P8 (indices, cycles, lookup, centres) — topological truth.
  2. 2xBR, at the same size as the HD version. It says, for each HD pixel, whether it is transparent or shadow, and which material it is.xBR xN (same scale) → alpha, shadow, class per pixel — semantic truth.
  3. 3ReboutCX x4, under several palettes. The network draws K versions of the same image, one per set of colours: these are the targets.ReboutCX x4 RGB under K palettes → targets T_1..T_K — modelling truth, never semantics.
  4. 4The multi-palette encoder. For each pixel, it picks the base shade and fraction that best match the K targets at once, computing exactly what the game will display.Multi-palette encoder, optimised against the exact decoder.
  5. 5The V6 registry. The HD file shipped with the mod.u8 I plane + optional u8 F plane (values 0..7) + dependencies and decoding contract. No boundary plane.
  6. 6In the game, the IEE module. It captures each layer's 256 colours, prepares a table of 2,048 colours, colours the pixels, stacks the layers and hands the image back to the game, at the original size.IEE DLL (CPU, existing compositor): P[256] captured per layer after Realize → 2,048-entry LUT → pixels → native compositing → texture (x1 geometry).
// Implemented V6: fractions in eighths
I         u8       base index
F         u8       0..7; absent ⇒ 0
dep_mask  256 bits entries actually read
succ(i)   = neighbour in the same range; end ⇒ i
RGB       = (P[I]·(8−F) + P[succ(I)]·F + 4) >> 3
alpha     = native alpha of P[I]
// no cross-class blend in this V6
Plane I

The base shade

One number from 0 to 255 per pixel, exactly as today.

Plane F

The eighths

How many eighths (0 to 7) the pixel moves towards the next shade. When absent, it is 0 everywhere: that is the old format.

dep_mask

The dependencies

The list of palette slots the image needs. If decoding had to read an undeclared slot, the image is rejected rather than displayed wrong (fail-closed). The list also ensures the computation is only redone when a colour actually used has changed.

Historical B proposal

The boundaries

Historical proposal: a second material (I2, F2) and a weight W from 1 to 7 eighths at boundary pixels. This plane does not exist in delivered V6. V7 later uses B to select a partner, with a different meaning.

The formula, read aloud

lerp8 takes (8 − f) parts of the base shade and f parts of the next one, then divides by 8; “+ 4” rounds to nearest and “>> 3” divides by 8. With f = 0, the base shade is unchanged. succ(i) stays within the same 12-shade range or 8-shade blend, never crossing its end or special indices. The LUT conceptually represents 256 × 8 = 2,048 colours. The code prepares the I/F pairs used, rather than necessarily filling every entry on each frame. The mixer uses this formula. V5 compatibility concerns reconstructed pixels, not identical files.

Encoder

Exhaustive search

For each pixel, the 89 positions of its range are decoded under the K palettes and compared in OKLab. What gets kept is what the runtime will really display.

No tricks: every possible position is tried and the best one on average over the K palettes is kept.
Palettes

K = 6

REF + default fighter (30 91 93 12 23 93 2) + four “Latin square” rotations, equally weighted. Ten validation palettes share neither a range number nor an identical RGB ramp at the same channel with the tuning union. K3 and K4 remain the alternatives compared before choosing K6.

“Latin square”: palettes built so that each material receives clearly different tones from one palette to the next, one light and one dark for each.
x2

Re-encode

BOX of the x4 targets in floating point, then encoding at x2 with the xBR2 guide. Never reduce already quantised planes.

The network's drawing is shrunk, then the whole choice of shades is redone at that size. Shrinking a file already converted to numbers would make no sense: the average of two numbers is not a colour.
Boundaries

Dropped historical direction

The proposed Q8c would choose the best neighbouring class, rather than the first, use at most two channels and retain primary weight ≥ 1/8. Acceptance required a clear gain. These constraints remain explained, but Q8c was dropped without implementation.

The prototype took the first neighbour found: 13.3% of its boundary pixels mixed more than two channels, and 8.4% lost their main material entirely.
ParameterValueProof
Palette resourceMPALETTE.BMP, never modifiedconfirmed
ModellingReboutCX x4 under K6, for each required source and paletteThe old ≈282,000 estimate with K3 came from the study’s scope, not a count of K6 production. Deduplication and caches avoid repeated work; actual cost depends on the exact union of compatible keys.measured
Semanticssame-scale xBR guide + index provenanceconfirmed
EncodingQ3m, 3-bit fractionmeasured
BoundariesQ8c dropped without implementation; historical prototype retaineddropped
Runtime interpolationsRGB bytes, defined roundingmeasured
Ditheringnonemeasured
Scalex2 retained; BOX for the world, Nearest for UIdecided
ReconstructionCPU in creature_sprite_x2.cppPalette reconstruction and compositing on CPU; BOX display filtering uses a separate shader. This is not full GPU palette reconstruction.implemented
Soft alphaOptional V8, separate Ankheg experimentThe established path retains native transparency and partial shadow. Spline Fit 1 rejected; light alpha replaced by the V9 SDF contour selected for the Ankheg.previous step
Historical proposal: fractions and boundaries
This retains the September proposal, including its boundary formula. It describes a research direction, not the delivered V6 reader. “Exactly V5” means pixels, not file bytes.
// V6 registry = V5 + optional additions
plane I   u8      base index (same as V5)
plane F   0..7    fraction towards succ(I)   // absent ⇒ exactly V5
dep_mask  256 bits P entries read    // fail-closed
plane B   option  (pixel, I2, F2, W 1..7) at boundaries

// bit-exact decoding, Python = C++
succ(i)      = i+1 within the range or pair, else i
lerp8(a,b,f) = (a·(8−f) + b·f + 4) >> 3
pixel        = lerp8(P[I], P[succ(I)], F)
boundary     = (C(I,F)·W + C(I2,F2)·(8−W) + 4) >> 3
LUT          = 2,048 entries per layer palette
Beyond Character · 3–4 October

V7: suitable partners for other creatures

In a fixed palette, consecutive indices are not necessarily neighbouring shades in one ramp. V7 retains Q3m and live native colours, but selects from four partners allowed by the profile. The byte (B << 3) | F stores partner B and fraction F in eighths. This B does not mean the old Q8 blend between two materials.

The pilot covers 15 profile witnesses, or 49 BAMs and 11,586 frames for selected actions. The complete Ogre, Flying and Ankheg families were then produced and installed. Flying was accepted; installation does not establish complete acceptance of Ogre or the original Ankheg Q3m. The 78 Character animations keep their established V6 contract.

Reuse identical images

Occurrences with identical pixels and a compatible contract share their computation even when centres or cycles differ. Those metadata remain occurrence-specific. Outside Character, 699,374 source frames were grouped into 194,573 source work items, avoiding 504,801 physical repeats. One source with two different contracts can still require two encodings.

V8: separate contour-transparency trials

V8 adds an optional alpha multiplier without recalculating Q3m colours. The Ankheg Spline Fit 1 trial was rejected in game for isolated edge pixels. A separate light-alpha trial was installed on 4 October. The steps remained too visible, so that trial was subsequently replaced by the SDF V9 contour selected below. These experiments use Catmull-Rom; the spline candidate’s defect alone is not evidence against that filter.

V7 profiles and colour partners

Offline simulation

V7 creatures with fixed or recolourable palettes and an actual four-colour-partner table
Offline simulation · Examples from the V7 x2 pilot using preserved K6 palette #0. The Spectator table comes from the actual witness: B selects a permitted partner and F its fraction, from 0 to 7 eighths. All fifteen witnesses are available in the gallery; sizes are fitted to the cards and QA remains specific to each candidate.
Gallery of all 15 witnesses and partner tables

Q3m V7 x2 renders use K6 palette #0 from the preserved oracle. Large witnesses are fitted to their cards, so relative species sizes cannot be compared here. The wyvern assembles four parts and the dragon nine. Weapons or actions absent from the pilot are not added. The Character shown here is a V7 witness; the established 78 Character remain on their V6 production.

The gallery shows poses and contracts, not blanket validation of all 15 families. Production and QA status remain specific to each candidate.

V8: previous contour trials

Offline simulation

Original, spline and light-alpha Ankheg contours on parchment and stone backgrounds
Offline simulation · The same MAKHG1 frame #11 is shown for original Q3m, Spline Fit 1 and light alpha. I/F planes, RGB colours and geometry remain identical for this frame; only alpha coverage changes. The body is shown without its earth layer. Close-ups without interpolation isolate mask A; they do not reproduce the Catmull-Rom combination of the in-game trial. Spline Fit 1 was rejected; light alpha was replaced by the V9 SDF shown next.
Change the background and inspect the contour

Displayed alpha = palette alpha × mask A / 255

The teal rectangle marks the same detail in all three images. The close-up uses Nearest to isolate mask A; it does not reproduce the Catmull-Rom combination used in the in-game trial. Spline Fit 1 was rejected; light alpha was replaced by the V9 SDF shown next. The detail is selected from a genuinely modified region and remains identical for all three candidates.

V9: a distance field for more natural contours

4 October · SDF selected · offline simulation

Light alpha made edge pixels less opaque, but the steps in the silhouette remained visible. The solution selected for the Ankheg is an SDF — a Signed Distance Field. Instead of storing only “opaque or transparent”, it describes distance to the edge: positive inside, negative outside and zero on the contour. The game can then reconstruct that edge at the actual display size.

1 · Describe the shape

The field is smoothed with displacement bounded to one x2 texel. Components and holes are preserved at texel centres: the aim is to reduce steps without erasing thin legs or fragments already present in the source.

2 · Measure the screen pixel

The shader tests 8 × 8 points within the texture area covered by each screen pixel. The fraction of points inside gives body coverage: for example, 32 out of 64 gives 50% opacity. That area depends on zoom.

3 · Keep the Q3m material

The same I/F planes and native palette supply the colours. Neighbouring material is extended into the transparent margin to avoid a black halo; Catmull-Rom filters those colours over 4 × 4 neighbours. The native black shadow at alpha 127 is calculated separately.

x2 is retained. There is no new neural upscale, no new Q3m encoding and no invented detail: source I/F planes, centres and cycles remain those of the previous production. Interpolated screen pixels can change because edge coverage and sampling are reconstructed differently. This Ankheg’s Catmull-Rom + SDF combination addresses a different problem from Character filtering: the established Character choice remains x2 + BOX.

Visual result selected on 4 October. The SDF contour received positive visual feedback and replaces the light-alpha trial. Temporary returns to vanilla x1 observed during animation changes concern resource loading and are being addressed separately. The improvement described here concerns the silhouette; no FPS gain is inferred.
Ankheg before and after the V9 SDF contour: comparison on stone and parchment, close-ups and a 64-point coverage diagram
Offline simulation · The same MAKHG1 pose #25, K6 palette #0, native zoom ×3 and custom backgrounds are used for light alpha and V9 SDF. The ×6 close-ups enlarge the same already computed pixels. The diagram shows the distance field, the 64 sample points within one screen pixel and the resulting coverage. Only the body is shown: earth-layer composition, lighting and selection effects are not simulated.
Compare SDF across poses, zoom levels and backgrounds

Open the comparison to compute both renders.

CPU model of the shader equations using the actual quantized V9 textures produced; light alpha is compared with the same Catmull-Rom colour filter. Source I/F colours are identical; their extension and the contour method differ. The rectangle marks the same region in both views. GPU precision and resource-loading stability are not reproduced.

Produced scope: 12 BAMs / 516 frames; 431 unique fields and 85 reuses. SDF is quantized in 1/16 x2-texel steps, with six texels of padding. At extreme zoom-out, when a screen pixel covers more than two x2 texels, the shader uses a smoothstep approximation. In game, body and earth distances are united while native colour order is preserved.

The 3 October colour diagnostics also compared direct x4, SeedVR2, 16-level fractions and four partners with eight levels. They remain off-production trials; the 16-level candidate was not promoted. Archives and comparisons.

View a real partner and 16-level diagnostic
Spectator: five treatments compared off-game
Spectator diagnostic from 3 October: old x2, Q3m x2, Q3m x4, Q3m x4 with 16 levels, Q3m x4 with four partners and eight levels. Image headers are the archive’s original labels. These are off-game previews, not acceptance of the newer treatments.
15 · Plan

From test bench to the complete fighter

Technical dependencies, not a ritual. Each phase produces new versions; nothing historical is rewritten; no validation is inferred from an installation.

The goal

Go from “it works on 120 images in a script” to “it works in the game, for the whole character”, without skipping steps.

The research

The three studies each proposed an order of work. The final version orders the tasks by what each one needs in order to start.

The finding

Nine phases, each with its exit criterion. The most important is P3: show the method in game, on one armour, one helmet, one shield and one sword, before any mass production.

PhaseWorkMove on whenState on 4 October
P0Oracles: complete BAM reader, MPALETTE realisation, corrected evaluation bench (sign, separate masks, aggregations)Trustworthy reference tools: read the files, recompute colours like the game, measure without the sign error.exact realisation; E3b reproducedDone · oracles and corrected evaluation
P1K-palette inference; Q6 / Q3m encoder; 10 disjoint palettes; K = 3/4/6Redo the experiment at scale and cleanly, with ten truly new test palettes.Q3m beats Q0 on every new paletteDone · K6, ten disjoint validations
P2DLL: V6 reader, mask, F plane, LUT; Python / C++ golden testsTeach the module the new format. “Golden tests”: reference cases whose result is known down to the byte.V6 pixels without fractions = V5 pixels, not identical filesImplemented · 32,832 Python/C++ reference I/F pairs
P3In game: CHFF4 + a helmet, a shield, a sword, Q0 vs Q3mThe first real test: old and new methods side by side, in the game.explicit validation by the userAccepted within the initial in-game trial scope
P4Real zoom; x2 / x4; area downscaling or mipmapsdated decisionDecided on 2 October · x2 + BOX, no mipmaps
P5Q3m ↔ constrained Q8 ablationWith and without boundaries, all else being equal: are they worth their complexity?own gain visible and measuredDropped without implementation or Q8c ablation
P6Complete 0x6110: 628 BAM, CHFF4 → CHFB1–3 → frequent weapons → shields → helmetsno missing frame; costs measuredProduced · 656 BAMs / 178,360 index frames
P7Paperdolls CHFF*INV, WPN*INV/OINclean interface measurementsAccepted · 81 normal BAMs / 163 frames, Nearest UI
P8The 78 Character animations, then other profilesGeneralising to monsters requires suitable classes and partners; see V7. A fixed palette does not eliminate the shading problem.evidence per family78 Character animations produced and installed; QA limited to established scopes
QA setup

Commands

C:MoveToArea("AR0700") then C:CreateItem("PLAT01"), HELM01, ISHLD03, BDSW1H06, SW2H01, BOW01… Game and InfinityLoader closed to install.

Game console commands: go to Waukeen's Promenade (AR0700, daytime, light ground), then spawn plate armour, a helmet, a shield and weapons.
To check in game

Recolouring and layers

One channel at a time; two fighters with opposite colours; off-hand weapon recoloured by 0x1R then 0x2R; 9 directions + 7 mirrored.

Effects

Identical to native

Pulsing glow of BDSW1H06, pause grey, invisibility, petrification, night, selection, occlusion, 59–60 FPS.

The original plan’s QA criteria and observations remain. Listing them here does not establish acceptance of every effect, equipment choice or direction for all 78 animations. Installation evidence is not visual acceptance or release integration.
Claude Opus 5.5’s view on 30 September, retained with the conclusions and uncertainties of that date. The addition below records later answers.
16 · Final position

The last reviewer's view

Claude Opus 5.5, after the two cross-reviews, 5.6 SOL's synthesis and the final review.

In plain words

A signed opinion, not a proof: that of the model that did the last check. It separates what is certain, what is not, what should be done right away and what should definitely not be done.

Certain

  • The bottleneck is the 12-level discretisation, not the choice of candidates.
  • The point of action is the existing IEE compositor: it already has each layer's real palette.
  • Any solution stays expressed as range indices, never as baked-in RGB (colours painted once and for all).
  • A recipe learned under a single palette does not generalise.

Uncertain

  • How well it holds on truly new palettes: only one is today.
  • The boundaries' own gain, small on E3b.
  • The real cost of the format under XPRESS, the module's compression.
  • The zoom range and the value of x4.
  • No measurement says what is beautiful: everything is judged in game.

Not recommended

  • Modifying MPALETTE or equalising the ramps.
  • Switching to BAM V2 or baked RGBA, i.e. pre-coloured images.
  • Adopting Q8 without ablation, or dithering by default.
  • Launching the 628 BAM with a recipe not validated in game.
Later answers

The open questions have progressed

The corrected evaluation and ten disjoint palettes were completed; K6 was selected. The V6 reader is implemented and checked against Python references. Q3m was shown in game. XPRESS_HUFF compression and actual zoom were measured, then x2 + BOX was recorded as the choice. Q8c was dropped without ablation.

The signed opinion remains the one from 30 September. Beauty remains a visual judgement; other families and contour experiments need their own acceptance.

17 · Files

Contents and sources

Original studies remain in sprite/Etudes_Sprite_codex_claude/. Later development evidence also lives in 6110 family research, docs/measurements/, pipeline/ contracts and sprite/index/qa-decisions/. Historical runs retain their dated state; the links below document later work.

In plain words

All the work is kept, including the versions that contained errors: that is what lets anyone retrace the path and check. No study file is rewritten; a correction produces a new version. The technical presentation of 30 September thus replaces Opus's presentation of the 29th, and this page is its explained version. The study documents are written in French.

GUIDE_DEFINITIF_SPRITES_HD_BG2EE.md development guide, authoritative (FR) PRESENTATION_ETUDE_SPRITES_HD_0x6110.html technical presentation (FR) PRESENTATION_PEDAGOGIQUE_SPRITES_HD_0x6110.html explained version (FR) PRESENTATION_PEDAGOGIQUE_SPRITES_HD_0x6110_EN.html this page, explained version (EN) figures_pedagogie/ “original game” tiles for these pages + script GUIDE_ULTIME_SPRITES_HD_BG2EE.md / .html 5.6 SOL synthesis, labelled “Codex” (historical) ClaudeCode_Guide_HD_0x6110_Femme_Humaine_Guerriere/ Claude Opus 5.5 GUIDE_ClaudeCode_HD_0x6110_palettes_dynamiques.md guide v1 PRESENTATION_ClaudeCode_0x6110.html presentation v1 + erratum (replaced) REVUE_CROISEE_Codex_ClaudeCode_0x6110.html cross-review + E5 donnees/ donnees_v2/ donnees_v3/ measurements E1–E3, E3b, E5 figures/ figures_v2/ figures_v3/ outils/ tiles, plates, scripts CODEX_BG2EE_Femme_Humaine_Guerriere_2026-09-29/ GPT 6-Astra GUIDE_CODEX_BG2EE_FEMME_GUERRIERE.md Astra's guide engine_research.md engine_disassembly_codex.txt engine evidence inventory_research.md assets_inventory*.csv 774 BAM, 870 ITM upscale/ palette/ 4 upscales, 60 NPZ, dithering, gradients codex_palette_study_20260929/COMPARAISON_CODEX_CLAUDE_0x6110.md codex_palette_study_20260929/comparaison_claude_20260929/ temporal sign, 3 bits Identities BaldurReal.exe 2.7.3.0 b51093a49140b2b8…94217a14d57 MPALETTE = RANGES12 7a9a654d5cbc4cee…82000d5f829d66dd ReboutCX (weights) c36a14ddb51ae094…56d5b94bdedab05

The “identities” are SHA-256 fingerprints: a string that changes as soon as a single byte of the file changes. They make it possible to check that everyone is talking about the same program, the same colour chart and the same network.

References added for this update

Evidence files are not rewritten to follow later states. For example, P1 may still say “P2 to implement”: that was its state at the experiment’s date. Later work has its own references.

18 · Frequently asked questions

Why not something simpler?

The shortcuts that come to mind, and why each one was discarded or postponed. Answers drawn from the guides' “discarded options” tables.

Why not ship nice HD images already in colour?

You would need one per colour combination, and players have thousands. A pre-painted image (baked RGBA, BAM V2) destroys customisation and no longer reacts to spells or lighting, which also go through the palette.

Single pre-coloured RGBA / BAM V2-PVRZ: rejected. Literal fixed RGB colours: rejected.

Why not add shades to the game's colour chart?

MPALETTE is shared by the whole game: changing it would change every character, every item and every mod at once. And there is no free slot on the body: all 252 useful numbers follow a colour choice.

Modify MPALETTE / equalise the ramps: rejected. Fixed body channel: rejected. Patching RealizeRange: rejected, V6 does better without a binary patch.

Why no dithering, like on old consoles?

Tested. The fixed checkerboard shows on the large plates and raises the error by 6 to 10%; error diffusion doubles the flicker; renewed noise moves 30% of the pixels of an image that is standing still. Sub-shades do better, with no pattern.

Global Bayer: rejected by default. Error diffusion: rejected. Temporal noise: forbidden.

Why not tune everything on a single palette?

Because the network shades differently depending on the colours. Tuned under the reference palette alone, a method excels there (error 16.3 vs 26.5 for the current method) but gains only 5% on the others: that is overfitting. And it is the players who choose their colours.

Q3 / single-palette interpolation: rejected as the final solution.

Why keep xBR if the network does better?

xBR remains sharp, deterministic and useful as a guide for indices, classes, transparency and outlines. Its historical acceptance is retained. The later selected shading path is Q3m K6: the network provides targets, then pixels are re-encoded to stay recolourable. The older Auto / Full xBR / Full ReboutCX choices describe an installer proposal, not a published interface verified here.

Why not compute everything on the graphics card?

Colour reconstruction and compositing remain on CPU with native palettes and caches. BOX does use a shader to filter the texture for display. That filtering is separate from full GPU reconstruction of indices and palettes, which was not adopted.

Immediate full shader: postponed. Second EEex hook: rejected, the IEE capture point exists.

Why x2 by default if x4 is finer?

An x4 texture contains four times as many pixels as x2. Q3m trials retained x2 with BOX: less compositing and upload work, and a look judged more natural. x4 can smooth some outlines further, but also look “plastic” without recovering details missing from the source. Its effects remain visible in comparisons, while the production preference is x2.

P4: x2 + BOX selected on 2 October.

What if the network were shown a grey image, with no colours?

An open lead: having the network draw a colour-independent “shade map” would avoid rerunning it for each palette. The risk: losing the contrasts between materials that colour signals to it. Not tested yet; to be compared with Q3m.

Grey shade map: to be tested.

What about monsters?

A fixed palette does not guarantee that neighbouring indices form usable gradients. V7 selects four profile-appropriate partners while retaining native colours. Fifteen profiles have a witness; Ogre, Flying and Ankheg have complete installed production with separate QA states. The established Character V6 is retained. V8 alpha trials are separate work.

Palette profile and partners per family; no automatic transfer of QA.

BOX during production and BOX on screen: the same step?

The averaging principle is similar, but the roles differ. During production, the network’s x4 RGB targets are reduced in floating point before x2 encoding. On screen, the shader averages the reconstructed texture over the screen pixel’s covered area. I/F indices are never averaged to downscale them.

Why do paperdolls keep Nearest?

The inventory figure uses a UI path separate from world rendering. Its 6110 batch was accepted at x2 with Nearest, preserving centres, lookups and native shadow. The world’s BOX choice does not replace that UI setting.

19 · Glossary

The jargon, explained

In the sense this page uses it. Words underlined with dots in the text link here.

Ablation
An experiment where one ingredient of a method is removed, all else being equal, to measure what it really contributes.
Alpha
A pixel's transparency. In the game's sprites, a pixel is either opaque or transparent; only the shadow is semi-transparent.
BAM
The format of the game's animation files: images (frames) grouped into cycles, with a table of 256 colour numbers. The file name gives the layer and the action (CHFF4G11: plate body, walking).
Bit, “3 bits”
A bit is 0 or 1. Three bits give 8 values (0 to 7): hence the eighths of a shade.
Channel, colour range, ramp
A channel is one of a character's seven colour slots (metal, minor, major, skin, leather, armour, hair). The colour range, or ramp (also called a gradient), is the series of 12 shades from light to dark assigned to it: one row of MPALETTE.
Class
A pixel's material: one of the 7 ranges, one of the 21 blends of two ranges, or one of the four special cases (transparent, shadow, two reserved blacks).
CRE, ITM
Game files describing a creature and an item. Among other things, they hold the imposed colours.
Disassembly
Translating the game's program, a sequence of machine instructions, into a readable form, to learn what it really does. Addresses written as RVA locate a routine within the program.
Dithering (Bayer, error diffusion)
Faking an intermediate tone by alternating two neighbouring tones: following a fixed checkerboard (Bayer) or by pushing the rounding error onto neighbouring pixels (error diffusion).
DLL, module, IEE runtime
The project's module (InfinityEngine-Enhancer), a DLL file loaded by the game that builds the HD image during play. “Runtime”: what happens while the game is running.
False colour
The paint-by-numbers technique: the file carries numbers, the game supplies the colours. The garish working colours stored in the file are nicknamed “clown” colours.
Frame, cycle
A frame is one image of an animation; a cycle, a complete animation seen from one angle.
Index
The number, from 0 to 255, carried by each pixel of the sprite.
Inference
One run of the neural network on an image.
Interpolation, fraction
Computing an intermediate value between two known ones. Here, a colour F eighths of the way between two neighbouring shades.
K
Number of tuning palettes: K3 in E3b; K3/K4/K6 compared in P1; K6 selected with ten disjoint validation palettes.
Layer, cell
Body, weapon, shield or off-hand, helmet: drawn separately, each with its own palette, then stacked.
LUT
A precomputed look-up table. Instead of redoing a calculation for each pixel, it is done once for every possible value, then the table is read.
Mipmap
Reduced texture copies (half, quarter…), used according to on-screen size. The tested x4 mode regenerated levels after recompositing. The Character x2 + BOX choice uses no mipmaps.
MPALETTE.BMP
The game's colour chart: 256 rows of 12 colours. RANGES12.BMP is an identical copy, but the engine only reads MPALETTE.
Nearest neighbour
Display without smoothing: each screen pixel takes the colour of a single image pixel. Sharp, but it can “skip” pixels when shrinking (aliasing). It is the baseline setting for quality checks.
OKLab, ΔE
OKLab is a way of scoring colours where the distance between two scores follows the difference perceived by the eye. ΔE is that distance. A gap of around 0.01 to 0.03 starts to be visible.
Opcode
An effect number in the game's files. Opcode 7 changes a channel's colour.
Overfitting
When a method fits its tuning examples so closely that it gets the others wrong.
Palette, realised palette
The table that assigns a colour to each index. The realised palette is the one the game has just computed for a layer, effects included.
Palettisation, quantisation
Converting an image with free colours (the network's) into indices of a limited palette.
Paperdoll
Large inventory figure, with its own BAMs and UI path. The normal 6110 batch (81 BAMs, 163 frames) was accepted at x2 Nearest.
QA
Quality assurance: here, the in-game check by the project author. No QA can be inferred from a measurement or an installation.
ReboutCX
The x4 upscaling neural network used by the project (RRDBNet architecture, 16.7 million parameters).
Registry V5, V6
HD sprite storage formats. V5 carries indices; implemented V6 adds fractions and dependencies. I/F remain u8. Without fractions, pixels match V5 but headers differ. V7/V8 extensions have distinct contracts.
SHA-256
A digital fingerprint: a string computed from a file, which changes as soon as a single byte changes.
Sprite
An animated image representing a character, a monster or an object in a 2D game.
sRGB, linear light
sRGB is the usual way colours are encoded on screen; linear light converts it into actual amounts of light. Blending in one or the other gives slightly different results.
Tuning, validation
Tuning palettes are used to adjust a method; validation palettes, set aside, check that it also works elsewhere.
Upscale x2, x4
Enlarging an image: two or four times as many pixels in each direction, i.e. 4 or 16 times as many pixels in total.
Vanilla
The original game, as shipped, without mods.
xBR
A classic pixel-art upscaling algorithm made of rules about neighbouring pixels. It invents no colour.
XPRESS, zlib
Two data compression methods. The game's module uses XPRESS; the study's sizes were estimated with zlib, hence their indicative nature.
BOX
Area averaging. During production: reduce RGB targets before encoding. On screen: average covered texels, weighted by overlap and alpha.
Texel, minification
A texel is a texture pixel. Minification reduces the texture on screen; magnification enlarges it. Filters must account for both cases.
Premultiplied alpha
RGB multiplied by alpha during filtering so invisible pixels do not contribute black at the edges.
V7, partner B
Four profile-allowed colour partners selected by B, with eight fractions F. This B is not the old Q8 boundary plane.
V8, coverage A
Optional alpha multiplier for a contour trial; Q3m colours retained. Installation does not establish visual acceptance.
Deduplication
Reuse computations for identical pixels and contracts. Centres and cycles remain stored per occurrence.