The coat engine
Body space & regions
Genes do not think in texels. They think in horse body-space and
let HorseSkinGeometry translate. That is what lets a coat rule be a
smooth function of position and still come out seamless across every body part
— no visible join where the neck meets the head, no mismatch where a
leg’s front face meets its side.
The coordinate frame
Right-handed, in model units: 1 unit = 1/16 block =
TEXELS_PER_UNIT = 2 texels on the
SHEET_SIZE = 128px sheet.
| Axis | Zero point | + direction | A plain function of this axis means… |
|---|---|---|---|
| X | rear edge of the tail | toward the nose | a front-to-back gradient (tail → nose) |
| Y | underside of the hooves | straight up | ventral → dorsal (belly → topline) |
| Z | the centre plane | the horse’s right | left ↔ right; z = 0 is the spine / centreline |
There is one absolute scale per mesh
(Skin.ADULT, Skin.BABY), with the origins read off the
mesh itself — X = 0 is the backmost tail texel, Y = 0 the hoof undersides.
So two different parts occupying the same body-space region sample a coat
function at the same value, which is exactly why an X-function gradient
has no seams.
The adult mesh was confirmed smooth and seamless in a play session on 2026-08-31. The foal mesh is an approximation — see the caveat below.
Parts and faces
Every Part is an axis-aligned box. Each box has six
Faces, each looking down one axis and spanned by the other two:
| Faces | Normal | Spanned by | What they are |
|---|---|---|---|
NOSE / TAIL | X | (Z, Y) | the front and back caps |
TOP / BOTTOM | Y | (X, Z) | the dorsal and ventral surfaces |
RIGHT / LEFT | Z | (X, Y) | the two flanks |
So, concretely:
- “paint the topline black” = the
TOPfaces (body-spaceyMax); - “black up the belly” =
BOTTOMfaces (yMin); - “a centreline blaze” = texels with small
|z|on the head’sTOP/RIGHT/LEFTfaces.
faceMapsOf had the Face.TOP and
Face.BOTTOM UV patches assigned to each other, so a
forEachTexel consumer that whitened low-y
(splash, sabino belly, frame off the underline) was painting the
spine and leaving the belly coloured — the long-standing
“white over the whole back” bug. The two put()
lines are now the right way round; coat-golden.txt moved
accordingly.
The part list
public enum Part {
BODY, NECK, HEAD, MUZZLE, MANE, TAIL,
LEFT_EAR, RIGHT_EAR,
LEFT_FRONT_LEG, RIGHT_FRONT_LEG, LEFT_HIND_LEG, RIGHT_HIND_LEG;
public Part mirror(); // LEFT_EAR <-> RIGHT_EAR, etc.
}
Always guard with HorseSkinGeometry.hasPart(skin, part), or use a
CoatRegions helper — those skip a missing part silently.
This is why a mane-and-tail gene gives a foal a coloured
tail only: the mane arrives with adulthood.
Rotated parts use their rest-pose AABB, and the foal’s neck / head / ear pivots are pre-resolved through the tilted neck — so face projection there is approximate, and markings on a foal’s face or neck can land loosely.
The API
forEachTexel(skin, visitor) | Walks every mapped texel, handing back (px, py, Part, Face, BodyPoint). The workhorse. |
|---|---|
forEachTexel(skin, part, visitor) | The same, restricted to one part. |
bounds(skin, part) | The part’s Bounds — min/max/span per axis. |
bodyBounds(skin) | The whole horse’s extents; what a pattern normalises against. |
hasPart(skin, part) | Does this mesh have that box at all. |
sample(skin, px, py) | Texel → Optional<Sample(part, face, point)>. |
project(skin, part, face, point) | Body point → Texel(u, v, x, y, clamped). |
The static no-Skin overloads target Skin.ADULT; the
Skin-first overloads pick the mesh. It is pure data and arithmetic
— keep the geometry tables in sync with HdHorseModel and
HdBabyHorseModel.
HorseSkinGeometry.forEachTexel(ctx.skin(), (px, py, part, face, point) -> {
// point.x() / point.y() / point.z() are body-space model units
double t = (point.y() - hooves) / drop;
field.restrictBlack(px, py, (float) (1.0 - t));
});
CoatRegions — the reusable moves
common/coat/pattern/CoatRegions wraps the common cases so a gene
never touches a px,py directly. Everything takes a
Skin; parts a mesh does not have are silently skipped.
| Helper | Does |
|---|---|
restrictAll(skin, field, rule) | Apply a per-texel rule to every mapped texel. The whole of champagne is one call. |
restrictPart(skin, field, part, rule) | The same for one part. |
blackenPart(skin, field, part) | Full-black a part — black = 1, red = 0. |
blackenLowerLeg(skin, field, leg, heightFraction) | Full-black the bottom fraction of a leg. |
blackenFace(skin, field, upFraction) | Muzzle (if present) plus the front fraction of the head. |
whitenLowerLeg(skin, field, leg, heightFraction) | Remove both pigments up a leg → the white template shows. |
whitenBlaze(skin, field, halfWidth, lengthFraction) | A centreline stripe on muzzle + head, halfWidth body units either side of z = 0. No callers — face markings come from WhitePattern.faceMarking now, which can also draw the detached shapes this never could. |
dorsalStripe(skin, part, point, halfWidth) | Coverage [0,1] of a nose-to-tail dorsal stripe: a band around z = 0, weighted to the upper half of the barrel/neck (not the belly), full on the mane/tail/head. Dun. |
legBar(skin, leg, point, spacing, duty, reach) | Coverage [0,1] of horizontal leg barring — constant-y bands, fading out over the top of the leg. Dun. |
paintPart / fillPart / fillMane / fillTail / fillEars / fillHooves / paintLowerLeg | ARGB overlay painting, for callers working in colour rather than pigment. |
redrawEyes(skin, dst, template) | Copy the eye texels back verbatim — the composer’s last step. |
LEGS | List<Part> of the four legs, for iterating. |
whitenLowerLeg cuts at a hard point.y() <= cutoff,
so every sock it drew ended in a perfect ring; and
whitenBlaze could only ever draw a centreline band, never a
detached star or snip. Neither has a caller. The white loci
went to WhitePattern
instead, which owns the margin and the whole face vocabulary. Both helpers
survive only for the warning in their javadoc — do not reach for them.
BodyNoise — noise that crosses seams
BodyNoise is deterministic procedural noise sampled in
body space — the same (x, y, z) model-unit
coordinates every texel already carries.
Sampling in 3D rather than in texture space is the whole point: two texels that sit next to each other on the horse get neighbouring samples even when they live on opposite ends of the sheet (the body’s side face and its top face, say), so a pattern built from these functions crosses part seams without a visible join.
cellDistance(seed, x, y, z) | Distance to the nearest point of a jittered unit lattice, normalised to roughly [0,1]. Near 0 at a lattice point, near 1 in the gaps — a field of round cells with a web between them. Exactly the shape of dapples. |
|---|---|
value(seed, x, y, z) | Smooth value noise in [0,1] on a unit lattice. Used to warp other fields off the grid. |
Callers scale their coordinates to choose the cell size. Everything is a
pure function of (seed, x, y, z) — no state,
no Random — so a coat rebuilt next session comes out identical.
PatchNoise — smooth patch fields for spotting
common/coat/pattern/PatchNoise is what the white-spotting genes
(tobiano, frame,
KIT sabino) sample for their patches.
BodyNoise.value on its own is one lattice cell across the whole
horse at the low frequencies a big patch needs, and grids up into visible
axis-aligned squares. PatchNoise.field(seed, x, y, z, scale) fixes
both: three octaves (detail at more than one scale) and a
domain warp (the sample point is pushed around by a second noise field,
so the octaves do not line up and the edges wander). z is
stretched before sampling, or the two sides of the horse come out
mirror-identical. fbm2 is the cheaper two-octave version, for fine
speckle (roan, sabino roaning).
HairPattern — the mane and tail painter
common/coat/pattern/HairPattern is to the hair genes what
WhitePattern is to the white-marking loci: one shape family,
with the difference between two genes being its parameters rather than a
second copy of the code. It offers bands (stripes
across the hair), centreStripe (a line along
it, centred), and the bright-hue draw the per-copy colours come from.
Its one interesting trick is that it works in the part’s own
axes rather than the world’s. A mane is a long thin blade and
a tail a short fat one, and both are rotated out of the world axes by their
rest pose, so neither “bands along X” nor “bands along
Y” is right for both. axesBySpan(skin, part) sorts the
part’s three axes by the span of its bounds, and the shapes run along
the longest and across the second. That is why one painter serves the mane
locus, the tail locus and the healer’s stripe, and why it will serve a
fourth without changing.
Read by mane colour, tail colour, healer and light’s gold mane.
Three stripe fields, and why they are three
There are three fields in coat/pattern/ that draw bands in body space,
and the temptation to collapse them into one is exactly the mistake that produced a
zebra with bars up its face and a brindle horse that matched left to right.
| Field | Shape | Used by |
|---|---|---|
BodyStripes |
One function of X for the whole animal: parallel bands, chevron-slanted, noise-warped. Generic and orientation-free. | The data-driven STRIPES mask (gene format). No built-in gene uses it any more. |
ZebraStripes |
A body map: vertical on the barrel, arcing round the hip, ringing the legs, tightening on neck and face, solid on the dorsal stripe, muzzle and tail, pale at the belly. Returns the dark coverage. | Natural zebra (whitens 1 - c) and magic zebra (blackens c). |
BlaschkoStripes |
Soft-edged, broken, unequal streaks in a lazy S down the side, crosswise on the upper leg, nothing on the head — and rolled independently per side, so the two halves of one horse disagree. | Brindle. |
Every generic stripe field phases on |z|, because that is what keeps
a horse from looking lopsided and what stops a constant-X face rendering as one
flat band. It is therefore structurally symmetric — and brindle is
a record of X-inactivation mosaicism, whose whole point is that the two sides were
coloured in by different draws. A field that cannot say that is drawing a
different animal, which is why BlaschkoStripes hashes each band once
per side and blends the two across the spine.
BodyStripes — the shared stripe field
Bands that run mostly across the horse (constant body-space X), so a stripe wraps from the barrel’s side over the spine without a seam.
/**
* @param spacing centre-to-centre distance in body units (the adult barrel is 22 long)
* @param duty how much of each period is stripe, (0,1) - 0.5 is equal stripe and gap
* @param warp how far, in body units, the noise field may bend a stripe off its plane
*/
public static double coverage(long seed, double x, double y, double z,
double spacing, double duty, double warp);
/** Hermite fade from 0 at edge0 to 1 at edge1. */
public static double smoothstep(double edge0, double edge1, double v);
The phase carries a small slant on |z|, which
bends each stripe into a shallow chevron over the back. Without it, every face
perpendicular to X — the chest, the rump, the front and back of every
leg — sits at one phase and renders as a flat band of solid stripe or
solid coat. The slant is symmetric left to right, so the horse does not look
lopsided.
Magic zebra is the first caller.
BodyStripes is deliberately generic, and that turned out to be a
reason to stop reaching for it rather than a reason to keep doing so. Dun’s leg
barring went to CoatRegions.legBar, zebra to ZebraStripes
and brindle to BlaschkoStripes, because in each case what the gene
actually wanted was a different shape, not the same shape somewhere else
— and “where you apply it and what colour you make it” was never
the difference between them.
What it is still exactly right for is the data-driven STRIPES mask, where
a gene author has no Java and wants parallel bands with a chevron: one field, four
numbers, no geometry knowledge required. That is now its only caller.
The vanilla model underneath
Every number in HorseSkinGeometry is read off
HdHorseModel / HdBabyHorseModel, which are in turn
structural copies of vanilla’s AbstractEquineModel.createBodyMesh.
So there are two coordinate systems in play, and it is worth
being explicit about both — most confusion about this engine is really
confusion about which one a number is written in.
Minecraft model space
The convention vanilla’s mesh builders use, and the one the tables below are in. Units are model units, 16 to a block — the same unit body space uses.
| Axis | + direction | Note |
|---|---|---|
| x | the horse’s left | the left legs pivot at x = +4, the right at −4 |
| y | down | the barrel sits at y 3–13, the hooves near y = 25 |
| z | toward the tail | the horse faces −z; the muzzle reaches z = −19 |
A cube is declared as a pivot (PartPose.offset)
plus an addBox(originX, originY, originZ, width, height, depth)
whose origin is the box’s minimum corner relative to that pivot.
PartPose.offsetAndRotation additionally gives a part a rest
rotation about its pivot; on the horse only the neck group and the tail have
one, both a pitch of π/6.
…and the flip into body space
Body space is model space turned the right way up and measured from the horse rather than from an arbitrary pivot. Both axes that reverse do so about the mesh’s own maxima, so the origins land on real anatomy:
bodyX = modelMax.z - mz; // 0 at the backmost tail texel, + toward the nose
bodyY = modelMax.y - my; // 0 at the hoof undersides, + up
bodyZ = -mx; // 0 on the centre plane, + to the horse's right
Two axes swap and all three negate, so the map is orientation-preserving — a face wound outward in model space is still wound outward in body space, which is what lets the gene creator’s 3D preview build its mesh straight out of these tables.
The adult parts, as 26.1.2 declares them
Pivot and origin in model space; texOffs in vanilla’s
64-space (the HD sheet lands each at 2×). Parts listed as children carry
the neck group’s π/6 pitch.
Part | vanilla name | Pivot | Box origin | W×H×D | texOffs | Rest pitch |
|---|---|---|---|---|---|---|
BODY | body | (0, 11, 5) | (−5, −8, −17) | 10×10×22 | (0, 32) | — |
NECK | head_parts | (0, 4, −12) | (−2.05, −6, −2) | 4×12×7 | (0, 35) | π/6 |
HEAD | head | child of neck | (−3, −11, −2) | 6×5×7 | (0, 13) | inherits |
MUZZLE | upper_mouth | child of neck | (−2, −11, −7) | 4×5×5 | (0, 25) | inherits |
MANE | mane | child of neck | (−1, −11, 5.01) | 2×16×2 | (56, 36) | inherits |
LEFT_EAR | left_ear | child of head | (0.55, −13, 4) | 2×3×1 | (19, 0) | inherits |
RIGHT_EAR | right_ear | child of head | (−2.55, −13, 4) | 2×3×1 | (19, 16) | inherits |
TAIL | tail | (0, 6, 7) | (−1.5, 0, 0) | 3×14×4 | (42, 36) | π/6 |
LEFT_FRONT_LEG | left_front_leg | (4, 14, −10) | (−3, −1.01, −1.9) | 4×11×4 | (26, 16) | — |
RIGHT_FRONT_LEG | right_front_leg | (−4, 14, −10) | (−1, −1.01, −1.9) | 4×11×4 | (48, 0) | — |
LEFT_HIND_LEG | left_hind_leg | (4, 14, 7) | (−3, −1.01, −1) | 4×11×4 | (26, 0) | — |
RIGHT_HIND_LEG | right_hind_leg | (−4, 14, 7) | (−1, −1.01, −1) | 4×11×4 | (48, 21) | — |
The hierarchy those poses imply:
root
├─ body
│ └─ tail pose (0, -5, 2), pitch π/6
├─ head_parts (the NECK) pivot (0, 4, -12), pitch π/6
│ ├─ head
│ │ ├─ left_ear
│ │ └─ right_ear
│ ├─ mane
│ └─ upper_mouth (the MUZZLE)
├─ left_front_leg ├─ right_front_leg
└─ left_hind_leg └─ right_hind_leg
Note that the neck group is a child of the root, not of the body: it shares no transform with the barrel, which is why a horse can drop its head to graze without the torso following. The four legs are likewise siblings, each swinging about its own pivot.
Older write-ups of “the Java horse model” give a
4×9×4 upper leg, a 3×5×3
shin and a 4×3×4 hoof meeting exactly at
y = 8 and y = 13. That is the pre-1.13
ModelHorse. Vanilla 26.1.2 — and therefore
HdHorseModel, which copies its texOffs and box
numbers verbatim — uses a single
4×11×4 box per leg, unwrapped 16×15
in 64-space.
This is not trivia. Drawing the trio in a preview tool makes the tool disagree with the game about which texel is a hoof, which is the one thing the gene creator must never do — and it was very nearly built that way once, off exactly those numbers.
z = 5.01
The .01 is deliberate: it lifts the mane a hundredth of a unit
clear of the neck’s rear face so the two are not coplanar and cannot
z-fight. Same trick as the ears’
CubeDeformation(-0.001F), one direction over. It also puts the
mane’s body-space x a hair behind the neck’s, which
no coat rule is remotely fine enough to notice.
HorseSkinGeometry keeps each part as its
axis-aligned bounding box after the rest rotation, because
that is all the coat pipeline needs — it projects texels onto a box
and never asks what shape the horse is. For a pitched part that box is much
bigger than the part: the neck is a 4×12×7 cuboid
tilted 30°, whose AABB is 4×13.9×12.1.
Measured, the mane’s AABB is 4.5× its true
volume, the tail 2.6×, the neck 2.0×, the head and muzzle
~1.9×.
Harmless for painting, fatal for drawing: the gene creator’s 3D
preview once rendered the AABBs and came out a pile of oversized blocks
rather than a horse. It now walks the raw cuboid (origin + size,
rotated about the pivot) through
geometry.toBody(skin, mx, my, mz) and only then flips into
body space — an additive read, so no computed value moves and parity
is untouched.
Java has that walk too, as of 2026-09-08:
HorseSkinGeometry.posed(skin, part, face, fa, fb) returns the
body-space point on the posed cuboid rather than on the bounding
box, with an overload that takes the BodyPoint
forEachTexel hands out, and
posedNormal(skin, part, face) gives the face’s outward
direction after the pitch — which on the neck or the tail is not the
plain body axis, and back-face culling has to ask. It exists because the
wiki’s baked gene icons were drawing the bounding boxes and looked
like it. Additive again: nothing the pipeline computes reads either method.
The duplication with model3d.js emitPart is real and is
gap 100.
Asking a part what shape it is
The AABB approximation above is fine for looking a texel up and wrong for
saying “along this part”. On the six pitched parts — neck, head,
muzzle, mane, ears, tail — a band on body Y is a horizontal slice, so a stripe
meant for the crest comes out as a collar wrapping the throat as well. Eleven gene
files used to work around that with a sawtooth longer than the horse, tilted by hand
to tan(60°), which is the neck’s own long axis written out as two
constants in files this class had never heard of
(gap 103).
HorseSkinGeometry.local(skin, part, point) is the replacement:
the inverse of posed’s frame change, returning the point as three
fractions along the edges of the part’s actual cuboid. Each fraction is
named for the body axis it pairs with and runs the same way, so
"axis": "X" still means “toward the nose” in spirit — on
the neck it is across the part’s depth, low at the crest (the
face the mane box sits on) and high at the throat, while "axis": "Y" runs
along the neck’s length and a band there is a collar square to
the neck. It is what space: "local" reads on AXIS,
WAVES and RAMP.
Two properties are worth knowing and are both pinned by
PartLocalFrameTest. On an unpitched part the box is its
own AABB, so local and part-space normalisation agree to the bit —
there is never a reason to think about which to use on a leg. And the fractions
run outside 0..1 on a pitched part, because the texel grid is the
bounding box and its corners sit off the ends of the real cuboid: on the adult neck
local X spans −0.72 to 1.72. Nothing clamps them, which is why a band meaning
“everything from the crest to here” starts at -1.0.
The texture is an atlas, not a wrap
Every cuboid gets six independently UV-mapped faces packed onto the sheet;
there is no continuous unwrap of a horse-shaped mesh anywhere. That is exactly
what makes forEachTexel necessary — two texels that are
neighbours on the horse are routinely nowhere near each other
on the sheet, so a coat rule written in texture space would seam at
every box edge, and one written in body space cannot.
Vanilla points all four legs at one patch (the left pair additionally
.mirror()ed) and both ears at another.
HdHorseModel gives each its own patch in what was empty space on
the vanilla layout, which is what lets this mod paint four legs and two ears
independently — a horse with one white sock is not expressible on
the vanilla sheet at all.
The visual boxes run to 22 model units (1.375 blocks) long; the adult
horse’s gameplay collision box is separately defined at roughly 1.4
blocks wide and 1.6 high. Nothing in the coat engine touches the hitbox
— but the size genes do, because
vanilla scales the model and the hitbox off the one
Attributes.SCALE number.
The HD models the geometry describes
HdHorseModel and HdBabyHorseModel are 128px, per-part
UV, structural copies of vanilla’s meshes with every cube at
texScale = 0.5 and the layer baked at 128×128 — so the
effective texture size stays 64 and every normalized UV is identical to
vanilla. The 2× sheet just gives each face twice the texels.
GeneticHorseRenderer deliberately does not add
vanilla’s HorseMarkingLayer: that layer paints
horse_markings_white.png over the whole texture, so any horse that
rolled Markings.WHITE rendered as a flat white horse on top of a
correct generated coat. All white markings here come from the
splash genes, inside the coat texture.
common/coat/skin/HorseSkinGeometry.java,
common/coat/pattern/{CoatRegions,BodyNoise,BodyStripes,ZebraStripes,BlaschkoStripes}.java,
neoforge-26.1.2/client/{HdHorseModel,HdBabyHorseModel}.java