Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Models and atlases

Verified against Minecraft 26.3 · Part XI · a resource pack changes one texture, and every stone block in the world redraws.

You drop a pack into the folder, move it above the default, and the screen goes to the loading overlay for a second. The pack replaced exactly one file, a single stone texture, and when the overlay clears every block you can see has been thrown away and rebuilt — not just the stone. The models for stone were byte-identical before and after, and were still re-listed, re-parsed, re-resolved and re-baked: nothing in the reload asks what changed, and the sprite they point at is now a different object in a different image. Nothing here is incremental. The unit of change is the model layer.

Every block face, every item sprite and every particle texture starts as a PNG in a pack, and most of them as JSON too, and ends as four vertices pointing at a rectangle of a stitched atlas. The stages below are that journey, and they happen inside a resource reload — the resource system owns ReloadableResourceManager and the barrier this rides on — and at no other time.

The cast

classwhat it decidesthread
ModelManagerthe reload’s spine, and what the finished lookup sets containthe Render thread to start and to apply, workers between
AtlasManagerwhich atlases exist, and it publishes their stitches for others to awaitRender thread for the handshake, workers for the work
SpriteLoaderhow one atlas is decoded, packed and mipmappedworkers
ModelDiscoverywhich file an Identifier means, and what its parent chain resolved toone worker task
ModelBakerywhich unbaked model each BlockState and each item file bakes toworkers, in batches
FaceBakerya quad’s positions and UVs, both rotated, and its chunk layerworkers
TextureManagerwho owns each AbstractTexture, and when an animation advancesRender thread
ItemModelResolverwhich ItemModel a given ItemStack draws withRender thread, every frame

The shape of the work: seventeen fans, one barrier

This is the clearest fan-out-and-barrier in the client. Seventeen independent pieces of work start on worker threads at once, and the figure below draws all of them, each set in two boxes: the twelve atlas stitches AtlasManager.reload schedules, and the five roots ModelManager.reload opens. Three of those five are directory listings that are each themselves a fan of one task per file. The bake waits on only two of the twelve stitches, and everything meets at the barrier.

flowchart TD
    RL["a reload starts: F3+T, a pack change, or the game booting"]
    HS["Render thread: AtlasManager.prepareSharedState, then every listener's reload called"]
    subgraph W["on worker threads"]
        subgraph AT["the atlas stitches"]
            S1["the blocks and items atlases"]
            S2["the other ten atlases"]
        end
        subgraph MR["the five model roots"]
            LST["three listings, models/, blockstates/ and items/, one task per file"]
            ONE["EntityModelSet.vanilla and BuiltInBlockModels.createBlockModels"]
        end
        RES["ModelDiscovery resolves what the three listings name"]
        BAKE["the bakes: ModelBakery.bakeModels, then the BlockModel layer and fluids"]
    end
    BAR["the barrier"]
    UP["Render thread: TextureAtlas.upload, then ModelManager.apply"]
    INV["LevelExtractor.allChanged raises a flag the next frame reads"]
    RL --> HS
    HS --> AT
    HS --> MR
    LST --> RES
    S1 & RES & ONE --> BAKE
    BAKE & S2 --> BAR
    BAR --> UP --> INV

The reload as a fan and its joins: the Render thread publishes the pending stitches and calls each listener’s reload, and what those schedule runs on workers, the bake waiting on two atlases and all five roots. Everything meets at the barrier, and what follows it runs on the Render thread in one order.

Read it as spread, converge, upload, invalidate: inside the outer box, worker threads in whatever order the joins allow; below the barrier, the Render thread in exactly one.

Twelve atlases and three listings, all at once

In: every enabled pack. Out: twelve stitched images, three parsed maps.

AtlasManager owns AtlasManager.KNOWN_ATLASES — twelve of them, named in AtlasIds — and each is a definition file in atlases/, read from every pack that ships one, not a folder scan. SpriteSourceList runs that file’s five kinds of source in order — SingleFile, DirectoryLister, SourceFilter, Unstitcher and PalettedPermutations, registered in SpriteSources — and a later source overwrites an earlier one by id, which is how a pack’s own definition file redirects a sprite; replacing a vanilla texture needs none of that, since the pack’s PNG shadows vanilla’s under the same name. SpriteLoader.loadAndStitch decodes and reads metadata one task per sprite, hands the results to Stitcher — which sorts by height and grows by powers of two — and returns a SpriteLoader.Preparations, after which MipmapGenerator builds the mip chain under whatever MipmapStrategy the texture’s own metadata asks for.

Two properties of that packing surprise people, and a third setting rides on both of them. The mip level is clamped by the lowest set bit of each sprite’s width and height, with a warning, so one undersized or oddly sized texture degrades mipmapping for every sprite in the atlas — and only the block atlas asks for mipmaps, the other eleven stitch flat. And sprite padding derives from the mip level and the anisotropic filtering setting, with the UVs computed inside the padded box, so anisotropy changes the layout and every UV in the game. The mipmap slider is blunter still: it writes the new level onto AtlasManager and asks for a full resource reload when the video settings close, so every sprite in the game is decoded and stitched again.

Meanwhile BlockStateModelLoader parses blockstates/ into a BlockStateModel.UnbakedRoot per BlockState, through BlockStateModelDispatcher and VariantSelector, and ClientItemInfoLoader parses items/ into ClientItem.

The handshake that lets listeners share a future

Reload listeners are not supposed to reach into each other, and these have to: baking cannot resolve a texture slot without knowing where the sprite landed. The channel that lets them is the resource system’s shared-state pass, and AtlasManager.PENDING_STITCH is the only key the game declares. What matters here is which end of it this page is: AtlasManager publishes a pending stitch for all twelve atlases, and ModelManager awaits exactly the two it bakes against — the blocks atlas and the items atlas — from inside its own prepare, which lets loading and resolving the models overlap the stitching even though the bake waits for it. ParticleResources awaits the particles atlas the same way.

Interning the models, and dropping the ones that loop

In: the raw models/ map. Out: a ResolvedModel per Identifier.

ModelDiscovery is a single worker task and the narrow waist of the pipeline. Every Identifier becomes one ModelDiscovery.ModelWrapper, caching its resolved texture slots and its baked geometry per ModelState, and ModelDiscovery.resolve returns the map of ResolvedModel, whose helpers walk the parent chain for geometry, slots, ambient occlusion and transforms. A model whose parent chain loops is logged and excluded, one whose parent is missing inherits from the missing model, and a model nothing references is parsed and never baked. In parallel, ModelGroupCollector gives each block state a visual-equality group, which is what lets ModelManager.requiresRender later say that change is invisible: two states sharing a group id cost nothing to draw when one becomes the other, unless their fluid state differs.

The JSON model itself is CuboidModel, made of CuboidModelElement, CuboidFace and CuboidRotation, with UnbakedGeometry and UnbakedCuboidGeometry between a resolved model and its quads, and Material, SpriteId, TextureSlots and MaterialBaker are the indirection from a model’s slot reference to a real sprite.

One bake per block state, and why that is affordable

In: resolved models and the stitched sprites. Out: a ModelBakery.BakingResult.

ModelBakery.bakeModels bakes once per block state and once per item model file, sharing one baker whose caches are concurrent maps, and the bakes are batched rather than scheduled individually, so this is at most sixteen tasks per worker thread for each of the two maps, and not tens of thousands. ModelBakery.MissingModels — a block part, a block, an item and a fluid — is baked first of all, so there is always something to substitute. Two further bakes follow: the BlockModel display layer, including the hard-coded BuiltInBlockModels, and the FluidStateModelSet.

That name is the trap this page owes you, and it is worth taking now rather than later. BlockStateModel and BlockModel are different interfaces in different packages. BlockStateModel is the quad source, the thing a mesher reads. BlockModel is the display model, the thing an entity renderer reads, and the tints and the transform a reader expects on the interface belong to its usual implementation, BlockStateModelWrapper, rather than to the interface itself. Two block tables come out of a bake because there are two interfaces, and block-entity rendering explains which is which.

Dedup is what makes per-state baking cheap. Every state sharing an unbaked variant gets the same baked object, geometry is cached per ModelState, and vertex positions and material infos are interned. Multipart is the exception that proves it: each state gets its own thin MultiPartModel over a shared MultiPartModel.SharedBakedState, so every fence, wall, pane and redstone-wire state really is a distinct object.

The output is a QuadCollection of BakedQuad — a ten-component record of four positions, four packed UVs, a Direction recomputed from the baked vertices, and a BakedQuad.MaterialInfo holding the sprite, the ChunkSectionLayer, the item RenderType and its two glint variants, the tint index, shade and light emission. Three things are decided here, and one of them is to put a thing off. Cull faces are rotated at bake time — a rotated variant’s north-culled quad is filed under the rotated direction, in UnbakedCuboidGeometry, while FaceBakery rotates the quad’s vertices and UVs and applies the uvlock — so the mesher never thinks about it. Tint is not resolved: the quad carries an index, and the colour arrives later from BlockColors or an ItemTintSource. And QuadCollection carries translucent and animated flags, OR-ed up through every model wrapper, so the code that handles a block’s model outside the terrain mesh — the block outline, moving blocks, the breaking overlay, the display and item models — can decide whether they need sorting or re-uploading without reading a quad.

A quad’s chunk layer is read out of the sprite’s pixels

The most surprising decision in the pipeline is the one nobody has to make. FaceBakery.bakeQuad does not take the render layer from the block or from any per-face declaration: it asks SpriteContents what transparency exists inside that quad’s UV rectangle. The answer is a Transparency, two booleans wide — are there fully transparent pixels in there, and are there partly transparent ones — and ChunkSectionLayer.byTransparency reads them the other way round: any partial alpha makes it translucent, otherwise any full transparency makes it cutout, otherwise solid. The item render type is picked here too, out of Sheets, by the partial alpha alone and by whether the sprite is on the blocks atlas.

One pixel — enough alpha inside a quad’s UV rectangle to move that face out of the solid layer (FaceBakery, asking SpriteContents).

A pack author who softens one edge of a texture has changed which chunk layer that face draws in, and so when it is sorted and how it blends with everything behind it. There is no warning, and only one way out: a Material may set Material.forceTranslucent, from a force_translucent key beside the sprite name, which skips the scan and takes the translucent layer whatever the pixels say. A hundred and ten of the shipped models use it — the stained glass, mostly — and it is the exception that shows the rule, because it exists for textures whose alpha the scan would read correctly and whose author wants them sorted anyway.

Every layer fails soft, and the two that do not

Missing and malformed input is a warning and a substitution at every layer of the pipeline above, which is why a broken pack usually produces checkerboards rather than a crash report.

what goes wrongwhat you get instead
a model file will not parsethat model alone is dropped
a blockstate file will not parsethat pack’s copy is skipped and a lower pack’s used; with none left, the block’s states are dropped
one variant selector is malformedthat selector alone is dropped
a model’s parent is missingthe missing model stands in as its parent
the parent chain is a cyclethe model is logged and excluded
a bake throwsModelBakery.MissingModels, baked first for exactly this
a sprite id belongs to no atlasthe MissingTextureAtlasSprite checkerboard
a texture slot is unbound, or its chain does not resolvethe same
a block model draws from outside the block atlasthe model is refused and the missing model substituted
a block state has no entry at allthe missing model — the broadest of them, and the last

The atlas-membership case is caught only at the moment a model is baked, and quietly. Minecraft.selfTest, the startup sweep that throws rather than warns, only runs in a development environment, and it looks for states that got the missing model, not at atlases.

Two places have no soft path. If Stitcher cannot grow an atlas within the device’s maximum texture size it raises StitcherException, which becomes a crash report listing every sprite, and mip generation is the second hard crash site. A pack with too many textures does not degrade — it crashes.

The barrier, and how a sprite reaches the GPU

In: everything above. Out: live lookup sets and live GPU textures.

The Render thread started the reload — every listener’s shared state, AtlasManager.prepareSharedState among them, then every listener’s reload, which scheduled the work — and now it does the rest. TextureAtlas.upload builds the new texture and closes the old sprites, and ModelManager.apply assigns the new lookup sets in one go. Underneath sits TextureManager, a PreparableReloadListener owning every AbstractTexture by Identifier — each atlas included — plus the TickableTextures it advances, and it registers a checkerboard texture at MissingTextureAtlasSprite’s location at construction. The GPU side belongs to blaze3d.

How a sprite’s pixels reach the atlas depends on whether it moves. A static sprite goes into a throwaway scratch texture, is blitted into every mip level by a render pass, and the scratch texture is closed. An animated sprite instead keeps one persistent texture per unique frame for the life of the atlas and is redrawn only when it has something new to show, through a second pipeline entirely if the animation interpolates — SpriteContents.AnimationState drives it and TextureAtlas.cycleAnimationFrames steps it.

The flag that rebuilds the world

In: a successful reload. Out: every visible section re-meshed, a frame later.

Nothing rebuilds during the reload. LevelExtractor.allChanged raises a flag, and on the next frame’s extract LevelRenderer.invalidateCompiledGeometry builds a new SectionCompiler from the new model sets and replaces every section with a fresh, uncompiled one, each re-meshed as it is found visible — see section meshing. SectionCompiler is the biggest consumer of BlockStateModelSet.get but far from the only one: the block-breaking overlay, moving blocks, the in-wall screen effect, the nether-portal sprite on the loading screen, TerrainParticle and BlockMarker all read it too, with the HUD, LevelExtractor, FeatureRenderDispatcher and the display table’s own fallback covering the rest, bar the development-only Minecraft.selfTest. The path from a set to triangles is BlockStateModel.collectParts then BlockStateModelPart.getQuads, over SingleVariant, WeightedVariants or MultiPartModel with SimpleModelWrapper as the usual concrete part. None of it crosses the network: the server has no idea what a model is.

How an item picks its model

An item never asks for a model by name. An ItemStack names its model in DataComponents.ITEM_MODEL — an Identifier, nothing else, and a stack without one draws nothing — and that id is the whole decision: a stack whose component says diamond sword renders as a diamond sword whatever item it actually is.

ItemModelResolver — held by Minecraft, called by the entity, block entity and GUI renderers as they extract a frame — reads the component, asks ModelManager.getItemModel for the baked model and ModelManager.getItemProperties for the flags that travel with it, then calls ItemModel.update, which appends the layers it draws to an ItemStackRenderState. Both lookups fall back rather than fail: an unknown id quietly yields the missing item model and the default ClientItem.Properties, whose two flags are whether the item plays the hand-swap animation and whether it may overflow its slot in the GUI, beside a scale for the swap animation.

What each id maps to came from items/, parsed into ClientItem by ClientItemInfoLoader and baked alongside the block models. ItemModels registers eight kinds of unbaked item model and only one of them, CuboidItemModelWrapper, draws quads of its own. EmptyModel draws nothing, CompositeModel stacks the others, SpecialModelWrapper hands the layer to a renderer instead of quads, BundleSelectedItemSpecialRenderer is a bundle’s peek — and the remaining three, RangeSelectItemModel, SelectItemModel and ConditionalItemModel, do not draw at all. They branch. SpecialModelRenderers covers the thirteen shapes no cuboid model can express, from chests and banners to shields, heads, tridents and decorated pots (block-entity rendering is what eleven of those thirteen borrow from). ItemModelGenerator is the odd one out and by far the most-used model in the game: it extrudes a flat sprite into geometry by tracing its alpha channel, which is what item/generated means.

What the three branching models branch on

The three are the whole of how an item’s appearance changes without the item changing, and each reads from its own registry of properties under client/renderer/item/properties. There are thirty-three of them in three shapes, and the shape decides which model can use it.

the modelwhat the property returnshow manyexamples
RangeSelectItemModela float, thresholded against declared cut-offs10, in RangeSelectItemModelPropertiesCompassAngle, Damage, Cooldown, CrossbowPull, BundleFullness, UseDuration
SelectItemModela value matched against declared cases10, in SelectItemModelPropertiesMainHand, DisplayContext, TrimMaterialProperty, ItemBlockState, LocalTime, ContextDimension
ConditionalItemModela boolean, choosing one of two models13, in ConditionalItemModelPropertiesDamaged, Broken, IsUsingItem, IsSelected, IsCarried, FishingRodCast, IsKeybindDown

Three of them keep state, which nothing else in the baking pipeline does. A compass and a clock each run a NeedleDirectionHelper, a damped follower updated once per tick for the baked model rather than per stack, and a compass that has a target damps only when its owner is the local player — which is the wobble, and a model may declare wobble: false and get the raw angle instead. LocalTime is the third, and it re-reads the wall clock at most once a second, which is why a chest item notices Christmas and a placed chest does not.

DisplayContext is the row worth knowing for a different reason: it is what lets one item file draw one thing in your hand, another in a frame and a third in the GUI, and the context comes from whoever is drawing rather than from the stack.

One rule is enforced here. A block model may only use the block atlas, and a cuboid item model must draw every quad from a single atlas — items or blocks, not both. A model that mixes them throws, the exception is caught upstream, and the item falls back to the missing model. Where the stack itself comes from is items and stacks.

Questions players ask

Why does lag slow the water down but the pause menu not stop it? Because animations do not advance once per tick. TextureManager.tick sits outside the client’s catch-up tick loop, so a laggy client advances every animation by one tick however many ticks it just owed. Pausing does not touch it — DeltaTracker keeps handing out ticks with the menu open — and the one thing that does stop the water is /tick freeze, because the guard on that call, beside a tick being owed, is Minecraft.isLevelRunningNormally.

Can I look at the atlas? Yes, and it is the fastest way to understand this page. Options.keyDebugDumpDynamicTextures runs TextureManager.dumpAllSheets, which writes every atlas in the game to disk under the game directory with a text listing of each sprite’s position and size beside it. A shared-constant flag, SharedConstants.DEBUG_DUMP_TEXTURE_ATLAS, makes every atlas dump itself at each upload too, without the keypress.

For a 1.21-era reader. BlockModelShaper is now BlockStateModelSet; BlockElement and BlockElementFace are CuboidModelElement and CuboidFace; BlockModelDefinition is BlockStateModelDispatcher; and BlockRenderDispatcher and ItemRenderer are gone with no successor of that name. One trap: BlockModel still exists and now means something entirely different — the display model above, where the JSON one is CuboidModel.

Where to look

ModelManager.reload for the shape of the pipeline, then AtlasManager.prepareSharedState for the handshake that makes it parallel. ModelDiscovery for resolution, ModelBakery.bakeModels for baking, SpriteLoader.loadAndStitch and Stitcher for the atlas, FaceBakery.bakeQuad for where a face’s layer is decided, and ItemModelResolver and BlockStateModelSet.get for the two ways out.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.