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

Block-entity rendering

Verified against Minecraft 26.2 · Part XI · a chest on the ground and a chest in your hand, drawn in the same frame by two renderers that share a model and nothing else.

You place a chest, step back, and hold a second one up in front of your face. Both are chests, both are lit, both open the same lid on the same hinge — and almost nothing about how they got onto the screen is shared. The one on the ground is a block entity: the terrain mesh at its position contains no geometry at all, and everything you can see of it was extracted from the live world by ChestRenderer a few microseconds ago. The one in your hand is an item: it has no block entity, no render state and no extract stage, and it is drawn by a class in a different package that exists only because an item cannot be a block entity. The seam shows if you type /tick freeze. The chest on the ground is nailed to its last tick; the chest in your hand keeps swaying with your view bob, because the two are drawn at different partial ticks and only one of them respects the freeze.

This page is the sibling of entity rendering, and does not re-teach it. Extract, submit, prepare, execute; render states that hold no live object; SubmitNodeCollector and the fifteen phases behind it — all of that is that page’s, and all of it is true here. What follows is only the differences, and they are larger than the shared machinery suggests.

The cast

classwhat it decidesthread
LevelExtractorwhich block entities are candidates at all — the two lists it walksRender thread
BlockEntityRenderDispatcherthe renderer for a type, and the two gates every extraction passesRender thread
BlockEntityRendererthe geometry, and its own answers to how far and off screenRender thread
BlockEntityRenderStateone block entity’s whole frame: position, block state, type, light, break progressa value object
ClientLevelmembership of the globally-rendered set, decided once when the block entity is addedRender thread
SpecialModelRendererthe thirteen shapes an item model cannot express, drawn without a block entityRender thread
SpecialModelWrapperhow an item model reaches one — the item road into renderer/specialRender thread
BuiltInBlockModelshow a block state reaches one, in a model table terrain never readsa worker thread, on every resource reload

Three roads to the same collector

flowchart TD
    BE["A chest placed in the world"]
    IT["A chest in your hand, on a shelf, or on the ground as an item"]
    BD["A chest carried by something that is not a chest — a block display, a minecart, an enderman"]
    SEC["LevelExtractor walks the visible sections, then the globally-rendered set"]
    BERD["BlockEntityRenderDispatcher — one shared ChestRenderer, one fresh ChestRenderState"]
    IMR["ItemModelResolver reads the item-model component and opens a layer"]
    SMW["SpecialModelWrapper puts a ChestSpecialRenderer in that layer"]
    BMR["BlockModelResolver reads the built-in block-model table"]
    SBM["SpecialBlockModelWrapper puts a ChestSpecialRenderer in that state"]
    COL["SubmitNodeCollector — the same phases, the same feature renderers, the same vertices"]
    BE --> SEC
    SEC --> BERD
    BERD --> COL
    IT --> IMR
    IMR --> SMW
    SMW --> COL
    BD --> BMR
    BMR --> SBM
    SBM --> COL

Two of the three roads end in renderer/special, and that is the package’s whole reason to exist: a chest that is not a block entity still has to look like a chest. Only the left-hand road has a visibility policy of its own, a state class of its own, or an extract stage that reads the live world.

entityblock entityspecial model
what is walkedthe level’s renderable entitiesthe visible sections’ meshes, then a global setnothing — it is reached from a model
the visibility testa frustum, plus a size-scaled distancethe section is visible, then a per-renderer radiuswhatever drew the thing holding it
the stagesextract, finalize, submitextract, submitresolve and submit, in one call
the statean EntityRenderState subclassa BlockEntityRenderState subclassa layer of an ItemStackRenderState
the partial tickone computed per entityone for every block entity in the worldthe camera entity’s
where the pose comes fromthe dispatcher, from the state’s positionLevelRenderer, translated to the blockthe item transform for the display context
how manyone renderer per entity type24 renderer classes, 26 of the 49 types13 renderers under 13 ids

The chest, both halves, one frame

sequenceDiagram
    participant LX as LevelExtractor
    participant BERD as BlockEntityRenderDispatcher
    participant ChestR as ChestRenderer
    participant LR as LevelRenderer
    participant IIHR as ItemInHandRenderer
    participant IMR as ItemModelResolver
    participant CSR as ChestSpecialRenderer

    Note over LX,CSR: the extract half — one partial tick for every block entity in the world
    LX->>LX: walk visibleSections, skip a section under 0.3 of its fade
    LX->>BERD: tryExtractRenderState with the not-global flag
    BERD->>BERD: the flag must equal shouldRenderOffScreen, then shouldRender within 64 blocks
    BERD->>ChestR: createRenderState, then extractRenderState
    ChestR->>ChestR: combine with the neighbour half — lid openness and the brighter of two lights
    Note over LX,CSR: the draw half — the world first, the hand afterwards, in two storages
    LR->>BERD: submit, with the pose already translated to the block
    BERD->>ChestR: submit — one model, one sprite, no world access
    IIHR->>IMR: resolve the held stack at the camera entity partial tick
    IMR->>CSR: the item model has no quads, only a special renderer
    CSR->>CSR: submit the same ChestModel with its openness fixed

The two halves of the figure are not two stages of one pipeline. The world’s block entities go through LevelRenderer.submitFeatures into the frame graph. The held chest is submitted, prepared and drawn in GameRenderer.renderItemInHand, into GameRenderer.handAndScreenSubmitNodeStorage — a second storage, drained by FeatureRenderDispatcher.renderAllFeatures after the whole world is already on the screen. They never share a submit node.

The chest’s block model is empty, and there are two tables of them

Open blockstates/chest.json and it names one model for every state. Open that model and it declares a particle texture and no elements. So when SectionCompiler walks the section and tesselates every block whose render shape is MODEL, the chest contributes exactly zero quads to the terrain mesh — and, in the same pass, adds itself to the section’s list of block entities. Everything you see of a placed chest is drawn one stage later, by a renderer, from a snapshot.

That is what a block entity renderer is for: the shapes that a cuboid model cannot express, and the state that a block state cannot hold. Only 26 of the 49 entries in BlockEntityTypes have a renderer registered in BlockEntityRenderers, and the other 23 — furnaces, hoppers, barrels, beehives — are drawn entirely by their block models, like any other block. Having a block entity does not make a block interesting to look at.

There are two baked block-model tables, and confusing them is easy. ModelManager.getBlockStateModelSet is the one built from the resource packs, and it is the one SectionCompiler reads: a chest is empty in it. ModelManager.getBlockModelSet is built on top of that one and merged with BuiltInBlockModels.createBlockModels, which attaches a SpecialBlockModelWrapper to every state of every chest, banner, skull, shulker box, conduit, decorated pot, bell, enchanting table and end portal in the game. Nothing in terrain ever reads that table — that is the whole of the separation, and it is membership rather than behaviour. BlockModelResolver is its one reader, and its callers are all entity renderers: item frames, block displays, minecart contents, the block an enderman is carrying. When one of them draws a chest it gets the quads and the special renderer, both, because that road draws whatever it finds; terrain simply never asks this table, and reads BlockStateModelSet instead, where the chest’s entry is empty.

Culling by section, not by frustum

A block entity is never frustum-tested. LevelExtractor.extractVisibleBlockEntities starts from LevelRenderer.visibleSections — the reachability walk that visibility and the frame graph describes — and takes each section’s compiled list of block entities whole. Culling has already happened, one section at a time.

Two extra gates then apply, and both are stricter than they look. The first is the section’s own fade-in: a freshly uploaded section reports a visibility ramping from zero to one over a duration the chunk section fade-in time option sets, and LevelExtractor skips its block entities until that number reaches 0.3. Terrain fades in from the first frame, and the chests inside it appear about a third of the way through — furniture arriving after the room. LevelRenderer.compileSections zeroes the duration for a section within about twenty-eight blocks of the camera or one that was empty before, so the gate only bites on distant terrain, which is exactly where you would blame the draw distance for it.

The second is a distance test with the same name as the entity one and only half of its behaviour. EntityRenderer.shouldRender does a size-scaled distance test and a frustum intersection. BlockEntityRenderer.shouldRender keeps the distance half alone: a camera position, and whether the block’s centre is within BlockEntityRenderer.getViewDistance.

Sixty-four blocks — the default, taken by nineteen of the twenty-four renderer classes, and it does not scale with your render distance the way Entity.shouldRender does.

rendererhow farwhy
the other nineteen64the interface default
PistonHeadRenderer68a moving block starts outside the block it is drawn from
BlockEntityWithBoundingBoxRenderer96the structure block’s outline is a build tool
TheEndGatewayRenderer256the beam is the thing you are looking for
BeaconRendererthe render distance in blocksand measured horizontally only
TestInstanceRendererthe larger of its two delegatesit wraps a beacon and a bounding box

The beacon is the interesting row twice over. Its BlockEntityRenderer.shouldRender flattens both positions onto the horizontal plane before comparing, so altitude never costs you the beam — you can be at build height above a beacon at bedrock and still see it. And its extraction scales the beam’s radius by the horizontal distance divided by 96, floored at one, so the beam gets visibly wider the further away you stand, which is why a distant beacon does not thin into nothing. Raising a spyglass resets the scale to one, because BeaconRenderer checks whether the local player is scoping. The topmost beam segment is drawn to BeaconRenderer.MAX_RENDER_Y, 2048 blocks above the block.

Off screen means off this list

BlockEntityRenderer.shouldRenderOffScreen is not an extra permission — it is a switch between two mutually exclusive lists, and it is enforced twice. ClientLevel.onBlockEntityAdded puts a block entity into ClientLevel.getGloballyRenderedBlockEntities only if its renderer says yes, and BlockEntityRenderDispatcher.tryExtractRenderState throws the extraction away unless the flag it was called with equals the renderer’s answer. The second check is what stops double-drawing: a beacon inside a visible section is in that section’s list and in the global set, and the equality test is the only thing that picks one. Exactly three renderers say yes — BeaconRenderer, BlockEntityWithBoundingBoxRenderer and TestInstanceRenderer, which says yes because either of its two delegates does.

What a block entity’s snapshot carries

blockentity/state holds 26 classes: the base and twenty-five subclasses. The base is five fields wide — the position, the block state, the type, packed light sampled from the level, and the crumbling overlay — and it is filled by one static method, BlockEntityRenderState.extractBase, that every renderer calls before adding its own. There is no EntityRenderer.finalizeRenderState counterpart here: BlockEntityRenderer declares one extraction method, not two, so nothing reaches back into the world after the snapshot is taken.

Twenty-five subclasses, twenty-six classes: BedRenderState is reachable from nothing in the game. There is no bed block entity in BlockEntityTypes and no bed renderer, and a corpus-wide search for the name finds only its own file. It is the only orphan in the package.

Five states carry another pipeline’s snapshot inside them, which is where the machines actually touch — and two of the five carry an entity state, not an item one. SpawnerRenderState.displayEntity is a whole EntityRenderState, extracted through EntityRenderDispatcher from a display entity the spawner creates client-side — a mob that LevelExtractor never sees, never frustum-tests, and whose light the spawner overwrites with the block’s. VaultRenderState.displayItem reads like the item cases and is not one: it is an ItemClusterRenderState, which extends EntityRenderState, and the vault submits it through ItemEntityRenderer — the same renderer that draws a dropped item lying on the ground. The three genuine item carriers are ShelfRenderState.items, an array of three ItemStackRenderState that reaches ChestSpecialRenderer from inside a block-entity render state, and CampfireRenderState.items and BrushableBlockRenderState.itemState for what is cooking and what is buried.

One live handle survives into a snapshot, and it is not unique to this side: MovingBlockRenderState is a one-block fake world that holds the level’s MovingBlockRenderState.lightEngine and MovingBlockRenderState.cardinalLighting by reference, so a moving block is lit at prepare time rather than at extract. PistonHeadRenderState carries up to two of them, and the entity side’s FallingBlockRenderState carries one.

Signs are the other partial exception. SignRenderState stores the two SignText objects rather than laid-out glyphs, and AbstractSignRenderer calls Font.split during submit — the line wrapping of a sign happens a stage later than everything else in the frame.

One partial tick for the whole world

This is the difference a player can see. LevelExtractor gives every entity its own partial tick, asking TickRateManager.isEntityFrozen per entity, so a mob exempt from a freeze keeps interpolating while its neighbours stop. Block entities get no such question: they all receive the single DeltaTracker.getGameTimeDeltaPartialTick value that GameRenderer.extract computed for the world, with the frozen-game flag honoured — which returns exactly 1.0 while the game is frozen. Every block entity in the world is therefore pinned to its last completed tick, with no per-block exemption anywhere in the path.

The held chest is a third answer again. GameRenderer draws it with Camera.getCameraEntityPartialTicks, which asks the frozen check about the camera entity and, when it is not frozen, ignores the frozen game entirely — and TickRateManager.isEntityFrozen never freezes a Player. So under /tick freeze the item in your hand is redrawn from a live partial tick while every chest lid in the world is stopped dead.

The same split shows up in the Christmas textures, which the game implements three times. ChestRenderer reads SpecialDates.isExtendedChristmas once, in its constructor, and its constructor runs only when BlockEntityRenderDispatcher.onResourceManagerReload rebuilds every renderer — so a placed chest that was ordinary at 23:59 on the 23rd stays ordinary until the next resource reload. The item model in items/chest.json selects on the minecraft:local_time property, whose LocalTime implementation re-checks the clock at most once a second. And the built-in block model wraps its two chests in a ConditionalBlockModel whose IsXmas property calls the same static method live, every time the model is resolved. Two of the three notice midnight almost at once. The one you are standing in front of does not.

Where renderer/special borrows its geometry

SpecialModelRenderers.bootstrap registers thirteen renderers under thirteen ids, dispatched by a codec, and the package holds exactly thirteen renderer classes to match. Nine of them implement NoDataSpecialModelRenderer and read nothing at all from the stack; the other four — banner, decorated pot, player head and shield — pull one component out of it through SpecialModelRenderer.extractArgument, which is the closest thing this road has to an extract stage.

Eleven of the thirteen reach into client/renderer/blockentity for their geometry. Three hold an instance of the block-entity renderer outright (BannerSpecialRenderer, DecoratedPotSpecialRenderer, ShulkerBoxSpecialRenderer); the rest call a static submit or name a static texture or model layer on one — SkullBlockRenderer.submitSkull, AbstractEndPortalRenderer.submitSpecial, BannerRenderer.submitPatterns, ChestRenderer.LAYERS. Only TridentSpecialRenderer and CopperGolemStatueSpecialRenderer stand alone. The chest in your hand really is the same ChestModel, baked from the same ModelLayerLocation, posed at a fixed openness instead of an interpolated one.

How an empty item model turns into a chest

Reaching a special renderer from an item is one indirection: SpecialModelWrapper is an ItemModel like any other, and its baked form puts the renderer into a layer of the ItemStackRenderState through ItemStackRenderState.LayerRenderState.setupSpecialModel. A layer either has quads or has a special renderer, never both, and the layer’s submit picks whichever it has. That is the entire mechanism by which an empty item model turns into a chest.

For a 1.21-era reader. BlockEntityRenderer has no render method either: the pair is BlockEntityRenderer.extractRenderState and BlockEntityRenderer.submit, and blockentity/state is a package that did not exist. Three names to stop hunting for: getRenderBoundingBox, absent from the corpus entirely, because visibility is the section’s business now plus the radius in BlockEntityRenderer.getViewDistance, BedRenderer, and every renderItem on a block-entity class — an item held by a block entity now goes through ItemModelResolver into an ItemStackRenderState like any other. BlockEntityRenderer.shouldRenderOffScreen survives with its meaning narrowed to which of two lists, BlockEntityRenderers still registers by BlockEntityType, and the fifteen submit phases these renderers land in are shared with entities.

Where to look

BlockEntityRenderDispatcher.tryExtractRenderState first — it is twenty-one lines and it contains both visibility gates. Then LevelExtractor.extractVisibleBlockEntities for the two lists it is called from, and ChestRenderer as the clearest renderer in the package, since it is the one with a counterpart in renderer/special to compare against. For the other road, SpecialModelWrapper and ItemStackRenderState.LayerRenderState, then BuiltInBlockModels for the block-state road nobody expects to exist. Submit phases and feature renderers is the catalogue everything here submits into.


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