Chapter 16, Shapes & Pixels

Modern resource-pack formats, working in Beta 1.7.3. Blockstate JSONs, model JSONs, render layers, tints, item models, and animated textures, all the files you already know, copied straight in.

Here is the pitch, stated flat: the resource formats from modern Minecraft work in beta. Blockstate JSONs pick a model per state. Model JSONs are the vanilla 1.8-era schema, parents, textures, elements, faces. A .png.mcmeta next to any texture animates it. These are not lookalikes; they are the real files, and most of them copy straight out of a modern resource pack with no edits. This chapter is the tour of what RetroAPI understands and the handful of places where beta's age shows.

Blockstate files: choosing a model per state

A blockstate JSON answers one question, "given this block's state, which model do I draw, and how is it rotated?" RetroAPI looks for it in three places, in this order, and the first hit wins:

blockstate JSON search order
assets/{ns}/retroapi/blockstates/{block}.json     # RetroAPI-first
assets/{ns}/blockstates/{block}.json              # the plain modern path
assets/{ns}/stationapi/blockstates/{block}.json   # StationAPI's path

The showcase's lamp from Chapter 14 uses the simplest possible form, a variants map keyed on the lit property:

src/main/resources/assets/example_mod/retroapi/blockstates/lamp.json
{
  "variants": {
    "lit=false": { "model": "example_mod:block/lamp_off" },
    "lit=true": { "model": "example_mod:block/lamp_on" }
  }
}

Two variants, two models, and the lamp's twenty states collapse cleanly onto them because only lit is mentioned, every age shares a look. That partial-key behavior is the heart of the schema.

The variants schema, in full

  • Partial keys, comma-joined. A key like "lit=true,facing=north" matches any state where both hold; the most specific matching key wins. The empty key "" is the catch-all default.
  • Weighted arrays. A variant can be a list, [{"model":...,"weight":3}, {"model":...}], and RetroAPI picks one by a position-seeded hash. The same block at the same coordinates always picks the same model, so there is no flicker on re-render, but neighbors vary.
  • Rotation. "x" and "y" rotate the model in 90-degree steps.
  • Multipart. Instead of variants, a multipart list applies several models at once, each gated by a when condition. Within a condition, "a|b" is alternation (OR), and listing separate properties is also OR.
  • Retro extensions. Two keys beyond the vanilla schema: "properties" (declare data-side state properties, the Chapter 14 feature) and "render_layer" (covered below).

One honest limit. AND is not supported in multipart when conditions, it is a 1.17 feature, so a single condition can only OR its terms. "uvlock", on the other hand, is fully supported: it re-derives UVs from the rotated geometry so the texture stays world-aligned.

A blockstate JSON auto-wires the block at registration. The moment one exists for your block, RetroAPI sets the render type to retroapi:model (the constant RenderTypes.MODEL) for you, and pulls the block's particle sprite from the model's particle texture. The baked model is what renders in the inventory and in your hand, too. You only set RenderTypes.MODEL by hand if you want model rendering without shipping a JSON.

Multipart: many models per state

The lamp's variants map picks one model per state. The wall from Chapter 14 needs the opposite: a post, plus a separate side segment for each direction it connects, all stacked into the same block. That is what multipart is for. Instead of choosing a single winner, multipart walks its list and applies every part whose when condition matches the current state. The wall's 162 states all draw from this one file:

src/main/resources/assets/example_mod/retroapi/blockstates/stone_wall.json
{
  "multipart": [
    { "when": { "up": "true" },
      "apply": { "model": "example_mod:block/stone_wall_post" } },
    { "when": { "north": "low" },
      "apply": { "model": "example_mod:block/stone_wall_side", "uvlock": true } },
    { "when": { "east": "low" },
      "apply": { "model": "example_mod:block/stone_wall_side", "uvlock": true, "y": 90 } },
    { "when": { "south": "low" },
      "apply": { "model": "example_mod:block/stone_wall_side", "uvlock": true, "y": 180 } },
    { "when": { "west": "low" },
      "apply": { "model": "example_mod:block/stone_wall_side", "uvlock": true, "y": 270 } },
    { "when": { "north": "tall" },
      "apply": { "model": "example_mod:block/stone_wall_side_tall", "uvlock": true } },
    { "when": { "east": "tall" },
      "apply": { "model": "example_mod:block/stone_wall_side_tall", "uvlock": true, "y": 90 } },
    { "when": { "south": "tall" },
      "apply": { "model": "example_mod:block/stone_wall_side_tall", "uvlock": true, "y": 180 } },
    { "when": { "west": "tall" },
      "apply": { "model": "example_mod:block/stone_wall_side_tall", "uvlock": true, "y": 270 } }
  ]
}

Trace one state. A wall with up=true, north=low, and east=tall matches three parts: the post (always, here, because up is true), the north low side at its base rotation, and the east tall side rotated "y": 90. All three render together. The "y" rotations are how one side model serves all four directions, the north model spun 90/180/270 degrees becomes east/south/west, so the file ships exactly two side models (a low and a tall) and the post.

Note the "uvlock": true on every rotated part: the side model is authored once facing north, the variants spin the geometry in 90 degree steps, and uvlock re-derives the UVs from the final positions so the cobblestone never appears rotated, no matter which way the segment points.

Model files: the shape itself

Model JSONs live under models/ with the same three-place search order as blockstates. The lamp's off-model is about as small as a model gets, a parent and one texture:

src/main/resources/assets/example_mod/retroapi/models/block/lamp_off.json
{
  "parent": "block/cube_all",
  "textures": { "all": "example_mod:block/lamp_off" }
}

Its lit twin is the model the blockstate switches to on lit=true, the same cube pointed at the on-texture:

src/main/resources/assets/example_mod/retroapi/models/block/lamp_on.json
{
  "parent": "block/cube_all",
  "textures": { "all": "example_mod:block/lamp_on" }
}

That "parent": "block/cube_all" is the key trick. The standard parents are embedded in RetroAPI, so a resource pack ships none of the vanilla model assets and the chain still resolves. The embedded parents are:

embedded standard parents
block/block          block/cube           block/cube_all
block/cube_column    block/cube_bottom_top    block/cross
item/generated       item/handheld

A parent chain does not have to end at an embedded parent in one hop, your own models can be parents too. The wall's multipart file points at three tiny models, and each of those is just a parent and a texture override. Here is the post the wall applies when up=true:

src/main/resources/assets/example_mod/retroapi/models/block/stone_wall_post.json
{
  "parent": "example_mod:block/template_wall_post",
  "textures": { "wall": "minecraft:block/cobblestone" }
}

It carries no geometry at all, only a #wall texture binding. The shape comes from its parent, a template that defines the actual box and routes every face through the #wall variable the child filled in:

src/main/resources/assets/example_mod/retroapi/models/block/template_wall_post.json
{
  "textures": {
    "particle": "#wall"
  },
  "elements": [
    {
      "from": [4, 0, 4],
      "to": [12, 16, 12],
      "faces": {
        "down":  { "texture": "#wall", "cullface": "down" },
        "up":    { "texture": "#wall", "cullface": "up" },
        "north": { "texture": "#wall" },
        "south": { "texture": "#wall" },
        "west":  { "texture": "#wall" },
        "east":  { "texture": "#wall" }
      }
    }
  ]
}

The post is only one of the pieces. The multipart file chooses the post when up=true but a side segment per direction it connects, low or tall, so the wall needs those models too. Same idea as the post: a thin stone_wall_side that binds #wall to cobblestone over a shared template_wall_side parent that carries the geometry.

src/main/resources/assets/example_mod/retroapi/models/block/stone_wall_side.json
{
  "parent": "example_mod:block/template_wall_side",
  "textures": { "wall": "minecraft:block/cobblestone" }
}
src/main/resources/assets/example_mod/retroapi/models/block/template_wall_side.json
{
  "textures": {
    "particle": "#wall"
  },
  "elements": [
    {
      "from": [5, 0, 0],
      "to": [11, 14, 8],
      "faces": {
        "down":  { "texture": "#wall", "cullface": "down" },
        "up":    { "texture": "#wall" },
        "north": { "texture": "#wall", "cullface": "north" },
        "west":  { "texture": "#wall" },
        "east":  { "texture": "#wall" }
      }
    }
  ]
}

The tall side is the same wall raised to the full 16 with its top capped, the variant the blockstate applies when the connection runs up to a block above:

src/main/resources/assets/example_mod/retroapi/models/block/stone_wall_side_tall.json
{
  "parent": "example_mod:block/template_wall_side_tall",
  "textures": { "wall": "minecraft:block/cobblestone" }
}
src/main/resources/assets/example_mod/retroapi/models/block/template_wall_side_tall.json
{
  "textures": {
    "particle": "#wall"
  },
  "elements": [
    {
      "from": [5, 0, 0],
      "to": [11, 16, 8],
      "faces": {
        "down":  { "texture": "#wall", "cullface": "down" },
        "up":    { "texture": "#wall", "cullface": "up" },
        "north": { "texture": "#wall", "cullface": "north" },
        "west":  { "texture": "#wall" },
        "east":  { "texture": "#wall" }
      }
    }
  ]
}

That is the modern resource-pack idiom intact: a reusable template_wall_post with the geometry and #wall placeholders, and a thin stone_wall_post that does nothing but say "#wall is cobblestone." Ship a second wall material and it is one more three-line file. The #wall default of minecraft:block/cobblestone is a vanilla sprite resolved by name, which the next section is about.

The supported model schema

This is the exact set of keys RetroAPI reads. It is most of the vanilla 1.8+ model schema:

  • parent, a chain ending in one of the embedded standard parents above.
  • textures, a map of names to sprite ids, with #variable indirection (a face can name #all and inherit whatever all resolves to).
  • ambientocclusion, the AO flag.
  • elements, the boxes. Each has from/to in 0 to 16 coordinates (out-of-range values are allowed), an optional rotation (origin, axis, an angle from {−45, −22.5, 0, 22.5, 45}, and rescale), a shade flag, and faces.
  • faces, any of down, up, north, south, west, east, each with uv (auto-derived from the element box when omitted), texture, cullface, a rotation of 0/90/180/270, and tintindex.
  • display transforms, gui, firstperson_righthand, and thirdperson_righthand are parsed and mapped onto beta's render contexts.

Lighting follows the cullface. A quad with a cullface gets neighbor-aware flat lighting and is occlusion-culled, it behaves like a real block face. A free quad (no cullface) is shaded by its own geometric facing instead. That single distinction is how a model can mix solid walls with floating inner detail and have both lit correctly.

Vanilla textures by name

The wall's post resolved its texture to "minecraft:block/cobblestone", and that namespace is doing real work. A texture value in the minecraft namespace resolves straight to the vanilla terrain atlas sprite by name, exactly like StationAPI and modern versions do it. There is no mod PNG behind minecraft:block/cobblestone; RetroAPI maps the name onto the beta terrain texture and bakes that sprite into the model. So a model copied out of a modern resource pack, the kind that freely references minecraft:block/oak_planks or minecraft:block/iron_ore, works without re-shipping a single vanilla PNG.

A curated name table covers the beta-era blocks: stone, cobblestone, the plank and log faces, the ores, the sandstone faces, the furnace faces, and so on down the beta block list. A name in the table binds to its terrain sprite. A name that is not in the table falls through to the ordinary mod-file path, RetroAPI looks for assets/{ns}/textures/{path}.png as usual, with a warning that the vanilla name was unrecognized. That fall-through is also the override hook: a mod that ships its own assets/minecraft/textures/block/cobblestone.png takes over that name, so you can either lean on the built-in vanilla sprite or replace it by shipping the file.

The practical upshot: paste a modern block model in, leave its minecraft: texture references alone, and it renders against beta's own terrain pixels. You only need to ship a texture when you want a sprite the beta atlas doesn't have, or when you want to override one it does.

A fancier model: the pedestal

The lamp was a single textured cube. The pedestal puts the whole schema to work in one block, three elements that each demonstrate a different rule, plus a tint that reaches back into client code. Its blockstate JSON is the simplest possible, one model, no states:

src/main/resources/assets/example_mod/retroapi/blockstates/pedestal.json
{
  "variants": {
    "": { "model": "example_mod:block/pedestal" }
  }
}

The empty key "" is the catch-all default, so every state of the block (there is only one) draws block/pedestal. The model itself is where the interesting work happens:

src/main/resources/assets/example_mod/retroapi/models/block/pedestal.json
{
  "textures": {
    "particle": "example_mod:block/pedestal",
    "stone": "example_mod:block/pedestal",
    "gem": "example_mod:block/gem"
  },
  "elements": [
    {
      "from": [1, 0, 1],
      "to": [15, 4, 15],
      "faces": {
        "down": { "texture": "#stone", "cullface": "down" },
        "up": { "texture": "#stone" },
        "north": { "texture": "#stone", "uv": [1, 12, 15, 16] },
        "south": { "texture": "#stone", "uv": [1, 12, 15, 16] },
        "west": { "texture": "#stone", "uv": [1, 12, 15, 16] },
        "east": { "texture": "#stone", "uv": [1, 12, 15, 16] }
      }
    },
    {
      "from": [5, 4, 5],
      "to": [11, 12, 11],
      "faces": {
        "north": { "texture": "#stone", "uv": [5, 4, 11, 12] },
        "south": { "texture": "#stone", "uv": [5, 4, 11, 12] },
        "west": { "texture": "#stone", "uv": [5, 4, 11, 12] },
        "east": { "texture": "#stone", "uv": [5, 4, 11, 12] }
      }
    },
    {
      "from": [5.5, 11, 5.5],
      "to": [10.5, 16, 10.5],
      "rotation": { "origin": [8, 13.5, 8], "axis": "y", "angle": 45, "rescale": false },
      "shade": false,
      "faces": {
        "down": { "texture": "#gem", "tintindex": 0 },
        "up": { "texture": "#gem", "tintindex": 0 },
        "north": { "texture": "#gem", "tintindex": 0 },
        "south": { "texture": "#gem", "tintindex": 0 },
        "west": { "texture": "#gem", "tintindex": 0 },
        "east": { "texture": "#gem", "tintindex": 0 }
      }
    }
  ]
}

Walk the three elements top to bottom, each one is a deliberate demonstration:

  • Element 1, the slab base. A wide, flat box ([1,0,1] to [15,4,15]) whose down face carries "cullface": "down", so it is occlusion-culled and dropped when something solid sits directly below the pedestal, exactly the cullface lighting rule from the note above. The four side faces use explicit uv windows ([1, 12, 15, 16]) to sample just the strip of the texture that should wrap the base, rather than letting the box auto-derive its UVs.
  • Element 2, the column. A tall narrow stalk ([5,4,5] to [11,12,11]) whose four faces have no cullface at all. They are free quads: always drawn, never occlusion-culled, and shaded by their own geometric facing instead of by a neighbor. That is what keeps the inner column lit correctly while it floats above the base.
  • Element 3, the gem. A small cube up top, rotated 45 degrees about the y axis so it reads as a faceted gem, with "shade": false so it ignores directional shading and stays evenly bright. Every face carries "tintindex": 0, which hands its color off to the client-side provider below.

The block registration is a plain block, the model does all the visual work, but two builder calls still matter because collision is a code concern, models are purely visual. .nonOpaque() stops the pedestal from hiding its neighbors' touching faces, and .bounds(...) gives it a one-block-tall, slightly-inset hitbox that matches the base:

src/main/java/com/example/example_mod/ExampleMod.java, in init()
// A COMPLEX model: three elements (a slab base with cullfaces and custom UVs,
// a column of free quads, and a 45-degree-rotated gem with tintindex 0, whose
// color comes from the provider registered in ExampleModClient). Everything
// lives in retroapi/models/block/pedestal.json; this class is a plain block.
PEDESTAL = RetroBlockAccess.create(Material.STONE)
	.sounds(Block.STONE_SOUND_GROUP)
	.strength(2.0f)
	.nonOpaque()
	.bounds(1 / 16.0F, 0.0F, 1 / 16.0F, 15 / 16.0F, 1.0F, 15 / 16.0F)
	.register(id("pedestal"));

The gem's tintindex 0 is satisfied by a tint provider registered on the client, the same RetroBlockColors.register call from the Tints section, here returning a steady ruby red instead of sampling the biome:

src/main/java/com/example/example_mod/ExampleModClient.java, in initClient()
// Tint provider for the pedestal's gem faces (tintindex 0 in the model JSON):
// a steady ruby red. Providers can also sample the world, see
// RetroBlockColors.GRASS / .FOLIAGE for the biome colormap versions.
com.periut.retroapi.client.render.RetroBlockColors.register(ExampleMod.PEDESTAL,
	(state, world, x, y, z, tintIndex) -> 0xE12D55);

Two things you get for free here. The block-breaking crack animation draws over the model geometry automatically, the renderer honors the texture override and overlays the progress sprite on every quad without any work on your part. And because a blockstate JSON exists, the baked model also renders in the inventory and in your hand with no extra setup, just like the lamp.

pedestal
pedestal.png
download
gem
gem.png
download

Render layers

Beta has two render passes: pass 0 alpha-tests (hard-edged, used for everything opaque and for cutouts like leaves), and pass 1 is the sorted translucent pass (the one water draws in). RetroAPI exposes the choice as an enum, RetroRenderLayer in com.periut.retroapi.client.render, with three values:

LayerBeta passMeaning
SOLIDpass 0 (alpha-tested)fully opaque
CUTOUTpass 0 (alpha-tested)hard-edged transparency; intent metadata
TRANSLUCENTpass 1 (sorted, after opaque)real blending, glass and water

You set it two ways, in code or in the blockstate JSON, and they are equivalent:

setting a render layer
// in code
RetroRenderLayers.set(MY_BLOCK, RetroRenderLayer.TRANSLUCENT);

// or in the blockstate JSON
"render_layer": "translucent"

SOLID and CUTOUT both render in pass 0 and both alpha-test; CUTOUT is intent metadata that documents "this has hard-edged holes." Only TRANSLUCENT moves a block into beta's blended water pass.

Choosing in practice: if a texture's pixels are either fully opaque or fully transparent (leaves, grates, the wall's gaps between segments), say CUTOUT and you are done; pass 0 discards the transparent pixels for free, with no sorting concerns. Reach for TRANSLUCENT only when pixels are PARTIALLY transparent (stained glass, ice); pass 1 blends correctly but draws after all opaque geometry and costs sorting. There is deliberately no alpha-test toggle: beta's terrain pass always alpha-tests, so a toggle would have nothing to switch, and declaring CUTOUT today means your blockstate files stay correct if a future version ever maps the layers differently.

Tints

A face with tintindex ≥ 0 asks to be multiplied by a color your code supplies. Register a provider per block:

registering a tint provider
RetroBlockColors.register(MY_BLOCK,
	(state, world, x, y, z, tintIndex) -> 0xRRGGBB);

The lambda receives the state, the world (which may be null for the item form, so guard it), the position, and the face's tint index, and returns a packed 0xRRGGBB color. Two built-in providers sample the biome colormaps for you, RetroBlockColors.GRASS and RetroBlockColors.FOLIAGE, so a custom leaf or grass block tints exactly like vanilla's.

Item models

At item registration RetroAPI checks for models/item/{id}.json, and if it exists, the model overrides any .texture() call. There are three shapes an item model can take.

Shape 1: layered sprites (item/generated)

A model parented to item/generated or item/handheld with layer0, layer1, and so on stacks those textures. The showcase's ruby does exactly this, a base ruby with a sparkle overlaid:

The two parents differ in one way beyond texture stacking, the same way they do in modern Minecraft: item/handheld ALSO flips the item's in-hand render to the diagonal through-the-fist pose that vanilla tools and sticks use. The showcase's Ruby Pick gets the tool pose purely from its model file; an item without a model JSON opts in from code with the chainable RetroItemAccess.handheld() (see the builder table in Chapter 5).

src/main/resources/assets/example_mod/retroapi/models/item/ruby.json
{
  "parent": "item/generated",
  "textures": {
    "layer0": "example_mod:item/ruby",
    "layer1": "example_mod:item/ruby_sparkle"
  }
}

The ruby registers with no .texture() call at all, the model is found by the item's id:

src/main/java/com/example/example_mod/ExampleMod.java, in init()
// Layered item model: models/item/ruby.json (parent item/generated) stacks
// layer0 + layer1 into one sprite at atlas build. No .texture() call at all;
// the model JSON is found by the item's id at registration.
RUBY = RetroItemAccess.create()
	.maxStackSize(64)
	.register(id("ruby"));

Important detail about layers in beta: the layers are flattened into one atlas sprite at atlas build, then drawn through the unchanged vanilla sprite path. That keeps rendering fast and compatible, but it means per-layer tinting is unsupported (it would need separate draws), and a model that asks for it gets a warning.

ruby
ruby.png
download
ruby_sparkle
ruby_sparkle.png
download

Shape 2: block-parent models

An item model parented to a block model renders that baked block in your inventory and hand. Block items get this for free, the BlockItem that every block registration creates already shows the block's baked model, so the lamp you hold is the lamp model. A plain (non-block) item that points its model at a block parent logs a warning in v1; that combination isn't wired yet.

Shape 3: no JSON

If there's no models/item/{id}.json, nothing changes: your existing .texture() / custom-class workflows from Chapter 5 work exactly as before. Model JSONs are purely additive.

Animated textures

Chapter 3 gave the one-paragraph version; the full treatment lives here. Any texture you register, through .texture(id), RetroTextures.addBlockTexture, or addItemTexture, animates automatically the moment a {png}.png.mcmeta file sits next to it. No code changes, no separate registration. The showcase's ember block ships this mcmeta:

src/main/resources/assets/example_mod/textures/block/ember_block.png.mcmeta
{
  "animation": {
    "frametime": 6,
    "interpolate": true
  }
}

And the block that wears it is an ordinary textured block, the animation is entirely in the asset:

src/main/java/com/example/example_mod/ExampleMod.java, in init()
// Animated texture, zero code: ember_block.png is a 4-frame vertical strip
// and ember_block.png.mcmeta sets frametime 6 + interpolate. The ordinary
// .texture() call picks the animation up automatically.
EMBER_BLOCK = RetroBlockAccess.create(Material.STONE)
	.sounds(Block.STONE_SOUND_GROUP)
	.strength(0.8f)
	.texture(id("ember_block"))
	.register(id("ember_block"));

The mcmeta schema

  • frametime, ticks each frame is shown; default 1.
  • interpolate, when true RetroAPI lerps pixels between frames for a smooth fade.
  • frames, an explicit order and per-frame timing, e.g. [0, 1, {"index": 2, "time": 8}]; omit it and every frame plays in PNG order.

The PNG layout is the modern one: square frames stacked vertically, so the image height is the number of frames times its width. A square-frame vertical strip with no mcmeta at all still animates, at one tick per frame, RetroAPI never silently squashes a tall strip into a stretched single frame. The non-square width/height keys (for rectangular frames) are unsupported and warned.

There is a code-driven equivalent if you'd rather not ship an mcmeta, RetroTextures.addAnimatedBlockTexture(id, frameCount, ticksPerFrame) and addAnimatedItemTexture(...). The frameCount there is informational; the PNG's height is what actually decides how many frames exist. And under StationAPI, the same PNG-plus-mcmeta animates through StationAPI's own atlas, no different on your side.

The ember block's PNG is a four-frame vertical strip, four 16×16 frames stacked into a 16×64 image. Shown here at 64×256 (and in game the four frames cycle every six ticks, interpolated):

ember_block
ember_block.png
download

An animated item: the Spectre Staff

The exact same .png.mcmeta mechanism works on item sprites, not just block faces. The Spectre Staff is a tall vertical strip, spectre_staff.png at 16×208, which is thirteen 16×16 frames stacked, and spectre_staff.png.mcmeta drives the playback frame by frame. The registration is a plain textured item; the .texture() call finds the mcmeta sidecar on the item atlas, the same path the ember block walks, so an animated item needs no extra API, just the strip and its mcmeta:

src/main/java/com/example/example_mod/ExampleMod.java, in init()
SPECTRE_STAFF = RetroItemAccess.create()
	.maxStackSize(1)
	.handheld()                        // held like a staff/tool, the through-the-fist pose
	.texture(id("spectre_staff"))
	.register(id("spectre_staff"));

The mcmeta sets frametime 3 as the default, lists the frame order, and gives one frame, index 8, its own longer time of 9 ticks:

src/main/resources/assets/example_mod/textures/item/spectre_staff.png.mcmeta
{
  "animation": {
    "frametime": 3,
    "frames": [
      0, 1, 2, 3, 4, 5, 6, 7,
      { "index": 8, "time": 9 },
      9, 10, 11, 12
    ]
  }
}

Shown here at 64×208, a tall strip of thirteen frames that cycle in game:

spectre_staff
spectre_staff.png
download

What we built

Blockstate JSONs picked models per state and auto-wired the block to model rendering. Model JSONs gave us embedded parents (so packs ship no vanilla assets), elements, faces, and cullface-aware lighting. Render layers placed blocks in the right beta pass; tint providers colored them per biome; item models layered sprites or borrowed a block's baked look; and a lone .png.mcmeta brought a texture to life. Every one of these is a real modern-pack file, copied in and working.

That closes the visual building blocks. You have blocks, items, machines, mobs, dimensions, packets, deep states, and modern models. A few chapters round out the build: blocks that face you, the equipment you carry, the sounds it plays, the lands it generates, and the data items hold. First, putting all of this together into blocks that know which way they point. On to Chapter 17.