Skip to content

Minecraft

Platform ID com.mojang.minecraft

DigitalHeaven.Mods.Minecraft Java is a Fabric mod that replaces the local player’s rendering with a DigitalHeaven avatar in Minecraft: Java Edition. The avatar is drawn by DigitalHeaven’s own renderer in a sidecar process and composited into Minecraft’s frame with correct depth, so the world occludes it and water and glass cover it. It animates from Minecraft’s own humanoid pose, carries vanilla held items in its hand, and is lit by the block and sky light where the player is standing. See Architecture below.

Mod loaderFabric (loader 0.19.5+, Fabric API)
Game versionMinecraft 26.2 or 26.3 (one jar per version)
Java25
Rendering pipelineBlaze3D, OpenGL or Vulkan backend, composited with a DigitalHeaven engine render

The mod launches DigitalHeaven.Engine.Host as a child process and talks to it over stdin/stdout and a shared memory-mapped file, so a built engine host is required alongside the mod. Build it in Release: the engine renders a 1080p frame in roughly 6 ms in Release against roughly 45 ms in Debug, and a Debug host makes the composite visibly stutter.

FeatureStatus
Avatar replacement (local player)Yes
Animation (Minecraft-driven)Yes
Held items (world, first person, paper doll, inventory, menu preview)Yes
Armor, elytra, cape and head items (elytra on by default, the rest optional)Partial
Spring bonesYes
Spring collidersYes
Block and sky lighting, including dynamic-light modsYes
HUD paper doll and inventory dollYes
Other playersYes (their pallets must be in your workspace)
NPCsNo
OverlayNo
Shader packs (Iris)No

Separately from the mod, a pallet can hold Minecraft content as a voxel container. A .dh-vox whose data names a Sponge .schem (WorldEdit), a .litematic, a structure block’s .nbt, a legacy MCEdit .schematic or a whole Java Edition world folder is converted by dh build into DH’s own voxel payload, and the source never ships. No mod, no running game and no Minecraft jar is involved: the importers read the files themselves.

  • Block states keep their native ids and properties (minecraft:oak_stairs, waterlogged included), biomes keep Minecraft’s 4x4x4 cells, and block entities and entities are kept as their own NBT, byte for byte, so nothing is lost on the way to a later export.
  • A world imports every dimension, one region file at a time, so its size is not limited by memory. Worlds from 1.13 on are read, in both the 26.x folder layout and the one before it.
  • Light, heightmaps and scheduled ticks are dropped, since the game computes them again, and so does the engine: it lights a world’s blocks in a light field of its own as it streams, and a Minecraft volume is shaded by it in hybrid unless its placement says otherwise.
  • Bedrock Edition is not supported.

A map places one with an object carrying a dh.voxelVolume, and the level editor places one when you drag the .dh-vox out of the asset browser. The engine streams a world’s chunks in around the camera, under a chunk cap and a mesh memory budget, so a whole world never loads at once; a map thumbnail streams around its camera the same way. With a generated Minecraft pallet in the workspace, blocks draw with Minecraft’s own models and textures from it: stairs, slabs, fences, plants, leaves and glass in their shapes, water and lava with their levels and flow and water in waterlogged blocks, every block that gives off light glowing by its level (fire, lava, torches, a lit furnace), grass, leaves and water tinted by the biome, and leaves drawn as Minecraft’s Fast leaves or, with client.voxel.opaqueLeaves off, its Fancy ones. Without one, every block draws as a full cube in a color from DH’s own built-in table, never sampled from Mojang’s textures, and a block the table does not name wears the unresolved checker. The details are on the dh.voxel page.

dh import minecraft-world turns a Java Edition save into the DH map of it, so a world becomes something a player can load and spawn in without hand-writing a .dh-vox and a .dh-map. Schematics are not imported this way: copy the file into a pallet, write a .dh-vox whose data names it and place that in a map yourself (Importing Minecraft Java).

Terminal window
dh import minecraft-world "<saves>/Tutorial" --out Source/io.you/worlds
dh import minecraft-world "<saves>/Tutorial" --out Source/io.you/worlds --all-dimensions --name tutorial
dh build io.you.worlds

--out names an existing pallet folder, one with a pallet.dh. The world is imported into it, as these files, named for the save’s folder in lowercase words joined by dashes (Tutorial is tutorial; --name picks another):

FileWhat it is
worlds/<name>/The save, copied in: level.dat and the region and entities folders of the dimensions imported, the .mcc files beside them included. The save itself is only read
worlds/<name>.dh-voxThe container, its data naming that folder
textures/skies/<name>.hdrThe sky the map’s skyImage names: the world’s own sky as a Radiance image (see Sky and sun below)
maps/<name>.dh-mapThe map: an object carrying a dh.voxelVolume placing the container at the origin (lit as a Minecraft container is, hybrid), one carrying a dh.spawnPoint, one carrying a thumbnail dh.camera that is not auto-captured, and a sky and a sun
  • The spawn is the map’s. A Minecraft spawn belongs to the world, not to the voxel container, so it is an object carrying a dh.spawnPoint in the map. It is read from level.dat: Data.spawn (pos and dimension) in a 26.x world, Data.SpawnX, SpawnY and SpawnZ in an older one. The import then reads the chunk holding that column and finds the ground top in its blocks, the highest block that is not air, grass, a fern, a dead bush or leaves, and does not trust the stored Y: a hill or a house built since the game placed the spawn would bury the player or leave the spawn inside the build. The spawn stands at the center of the block’s column, a meter above the ground’s top face. A column in a chunk that was never generated falls back to the stored Y, with a warning. The spawn faces the map’s default heading.
  • The overworld only, by default. Dimensions share coordinates, so a world drawn with its nether inside it is a confusing picture, and the .mca files of the others are most of a save’s size. --all-dimensions copies the nether, the end and any other too; the container then holds a grid for each, every one but the overworld hidden until a placement’s delta shows it. A spawn in a dimension that was not copied is placed over the overworld at the same X and Z, with a warning.
  • A migrated world’s leftovers are left out. 26.x moves a save’s live data to dimensions/<namespace>/<path>/region, and a save opened by an older version can still hold the region, DIM-1 and DIM1 it had before. Only the live layout is copied, and the import says which folders it left. The build reads a save the same way, preferring the migrated folder.
  • Sky and sun are the Minecraft overworld’s, not the Earth atmosphere sky draws. DH has no gradient sky, so the import writes the sky as an equirectangular image (textures/skies/<name>.hdr) that the map’s skyImage names, which also gives the world its sky-colored ambient. The image follows the game’s own numbers: the sky color is derived from the spawn’s biome temperature (plains is #78A7FF, snowy biomes bluer, deserts paler), it fades to the overworld’s fog color #C0D8FF at the horizon, and both are dimmed by the sun’s height with the game’s curve. The sun and moon are the flat squares the game draws (about 33 and 22 degrees across), placed where level.dat’s DayTime puts them: the sun rises in the east (+X) and sets in the west, and the map’s light is the sun by day and a dim blue moon by night. A world with no DayTime is lit at noon, and a spawn in a chunk older than 1.18, which stores no biomes, gets the plains sky. Not drawn: clouds (DH has no cloud layer to drive), stars, the sunset glow at the horizon, moon phases and rain. They are written in one place in the importer (MinecraftSky and MinecraftWorldMap), so a Minecraft template map that maps will be able to inherit (design-notes/map-inherits.md in the repository) replaces them in one line.
  • Running it again rewrites the copied save and the .dh-vox, so the pallet follows the save, and never rewrites the map. A map already at maps/<name>.dh-map is kept as it is, and the import says so, so edits to the spawn, the camera or the lighting survive a re-import. Delete the map to have it written again (and the .hdr with it, which is written only with the map). A map made before the importer wrote a sky has none: it keeps the flat gray clear color until it is deleted and imported again, or its lighting block is given a skyImage.

The world is converted by dh build, one region at a time, as any voxel container is, so the import itself only copies files and reads one chunk.

To draw Minecraft blocks with Minecraft’s own models and textures, DH reads them from your own client jar into a pallet with the ID com.mojang.minecraft, the same ID a container’s platform names. Mojang’s assets never ship with DH, and the generated pallet stays on your machine: share the command, not the pallet.

Terminal window
dh import minecraft # list the client jars found, and the one it would use
dh import minecraft --out Source/com.mojang/minecraft
dh import minecraft --version 1.21.8 --out <folder> # a found jar other than the newest release
dh import minecraft --jar <client.jar> --out <folder>
dh build com.mojang.minecraft

--out names the pallet folder itself. The import only reads the jar.

Where it looks, on every OS:

LauncherWindowsLinuxmacOS
Official launcher%APPDATA%\.minecraft\versions\<v>\<v>.jar~/.minecraft, and the Flatpak’s ~/.var/app/com.mojang.Minecraft/.minecraft~/Library/Application Support/minecraft
Prism Launcher%APPDATA%\PrismLauncher\libraries\com\mojang\minecraft\<v>\minecraft-<v>-client.jar$XDG_DATA_HOME/PrismLauncher, and the Flatpak’s~/Library/Application Support/PrismLauncher
MultiMC❌ portable only: pass --jar$XDG_DATA_HOME/multimc~/Library/Application Support/MultiMC
Modrinth App%APPDATA%\ModrinthApp\meta\versions\<v>\<v>.jar (or com.modrinth.theseus)$XDG_DATA_HOME/ModrinthApp~/Library/Application Support/ModrinthApp

With no --version, the newest release wins; snapshots and pre-releases are used only when named.

What it writes:

PathWhat
blocks/<block>.dh-blockOne dh.block per blockstate file. Variants become first-match rules and multipart becomes all-match rules, one rule per entry. Most are a few lines over a base, naming their materials; a block whose shape no other shares is written whole, its parent chains flattened and its texture variables resolved
blocks/base/<shape>.dh-blockThe bases: a shape Minecraft blocks share (stairs, slab, cube_all, cube_column, cross, fence_post, door_bottom_left, leaves, …) with its rules and open material slots, found from the parent chains rather than from a list
blocks/base/<recipe>.dh-blockA recipe for blocks the game draws in code: chest, shulker_box, banner, skull, …
materials/base.dh-matWhat every block material shares (roughness, metalness), stated once
materials/entity/..., textures/entity/...The entity textures the recipes bind (chests, shulkers, banners, heads, the pot, the conduit), byte for byte, each with its material
materials/block/<texture>.dh-matOne dh.material per block texture, used or not, plus any other texture a model names. Each inherits materials/base.dh-mat and states only its texture and how it is see-through
textures/block/<texture>.pngThe texture, byte for byte, with a .dh-tex-settings sidecar asking for point filtering and "compression": "none", so the pixel art is never block compressed when the pallet is built (the importer’s version is 2 for that)
textures/block/<texture>.png.mcmetaThe texture’s metadata, copied as it is: an animated texture’s animation, which the engine plays in the volume’s atlas (frame order, frame times and interpolation, on the world clock), and how its mipmaps are reduced (leaves’ dark_cutout), which the atlas follows (how)
textures/colormap/*.pngThe grass, foliage and dry foliage colormaps, as palette textures
pallet.dhThe manifest, with the client’s version in metadata.platformVersion

Transparency follows Minecraft’s own rule: a texture with any partly transparent texel is translucent ("alphaMode": "blend"), one with any fully transparent texel is a cutout ("mask"), and the rest are opaque. A model can force a texture translucent (force_translucent), as plain and stained glass do in 26.2, so glass imports as translucent and casts no shadow, as it renders in the game.

What the jar does not say is kept by hand in MinecraftBlocks, from the game’s behavior: which blocks are tinted and by what (grass and foliage colormaps, fixed colors for birch and spruce leaves, lily pads and attached stems, and computed colors for water, redstone wire and stems), which blocks draw nothing (air, cave and void air, barriers, light blocks, structure voids, and bubble columns, drawn as the water they hold), which models are empty on purpose (a young pitcher crop’s upper half), and the liquids. None of those becomes a placeholder.

Light. Which blocks give off light, and how much in which state, the game decides in code (each block’s lightLevel in Blocks), so the importer keeps it as data in Blocks/emission.json, checked against the 26.2 client: a level for a block that always glows ("torch": 14, "lava": 15), or a list of { "when", "level" } read in order for one whose state decides ("furnace": [{ "when": { "lit": "true" }, "level": 13 }]), with a leading * naming a family ("*candle": three per lit candle). Each block it names is written with that lightEmission, never its materials, since a lit furnace and a cold one wear the same side texture. On 26.2 that is 109 blocks: fire, lava, torches of every kind, glowstone, sea lanterns, shroomlights, froglights, jack o’lanterns and lanterns at full light; lit furnaces, smokers, blast furnaces, redstone lamps, ores, torches, campfires, candles, copper bulbs and respawn anchors by their state; magma, glow lichen, amethyst, sculk, crying obsidian, the nether portal, the end rod, the beacon, the conduit, trial spawners and vaults. The import says how many blocks give off light, and names any key the jar has no block for. The engine draws them glowing and spreads their light through the light field.

Opacity. How much a block cuts light passing through it is code too (each block’s light block, and whether it uses its shape for light occlusion), and DH’s rule from a block’s shape gets most of it right: a block whose model covers all six sides opaquely stops light, anything else passes it, and a block standing in water cuts sky light by one. Blocks/opacity.json holds the rest, built like the emission data: a number is a block’s lightOpacity ("*_leaves": 1, "ice": 1, "cobweb": 1, "tinted_glass": 15), and { "shape": true } writes its lightShape, for the slabs, stairs, snow layers, dirt paths, farmland, lecterns, daylight detectors, stonecutters, end portal frames, composters, sculk sensors and shriekers and extended pistons that stop light across the sides they cover. A door or a trapdoor is not listed, since the game lets light through them. On 26.2 the import writes 168 blocks from it, says how many, and names any key the jar has no block for. Checked against world-older’s own saved light, the field DH computes from this data matches the game in 99.98% of the cells block light reaches and 99.99% of those with partial sky light.

Liquids. Water and lava become fluids: their still and flowing materials, the level property, and for water its biome tint, the waterlogged property that puts a source of it in any block, and the blocks that always stand in it (kelp, seagrass, tall seagrass and bubble columns). Their model is a stand-in for a reader that draws no fluids. The engine draws them with levels, flow and waterlogging, as the dh.voxel page says, and a body swims in them through their media, below. What a block collides with and the blocks drawn in code are data files in the importer, below.

What a liquid is to a body inside it is DH’s own data, shipped in the importer as two materials (Blocks/Fluids/water.dh-mat and lava.dh-mat), never read from the game. The import writes them to materials/fluid/ and each liquid’s block names its own as its fluid’s medium, so the blocks hold pointers and the numbers live in one place. They are tuned to Minecraft’s feel, not its physics:

WaterLava
Base presetlakemud
Swim speedviscosity 2: half the world’s swim speed (about 2 m/s at the defaults) and twice its drag, the game’s 0.8-a-tick drag and 0.02 strokeviscosity 5: about 0.8 m/s, the game’s 0.5-a-tick drag
Buoyancydensity 950: a little thinner than a swimmer (world.water.playerDensity, 1000), so one left alone sinks slowly, about 0.2 m/sdensity 800: sinks, and climbs out slowly
FogClear and blue: transmittanceColor (0.3, 0.45, 0.7) at 3 m, a faint blue scatterSight ends within a block in orange: nearly opaque at 0.5 m, all scatter, colored (1, 0.15, 0)

Lava does no damage. The fog is the medium’s, which the camera reads the way it reads a map body’s, at client.render.underwaterFog’s density; lava’s orange is its in-scatter, lit by the sky as a body’s sediment is, so in a cave it darkens with the light.

Collision. A block’s collision is its model’s elements unless the game gives the shape other boxes. Those few are data, not code, in the importer’s Blocks/collision.json, keyed by the model a shape’s parent chain passes through: fence posts and arms, walls and fence gates stand a block and a half tall, and an open gate is not solid. Every block DH’s block table calls passable (plants, rails, torches, buttons, pressure plates, signs, banners, liquids) gets an empty collision on its models, stated once on a base when every block on it agrees, so the pallet and the table never disagree about what a body walks through.

Some blocks have no model in the jar: the game draws them in code. For most of them the importer writes real models from recipes: DH-authored data in the importer (Blocks/Recipes/*.json), never the game’s code. A recipe is the boxes of a shape, from its public dimensions (a chest’s 14 by 10 by 14 body, its 14 by 5 by 14 lid and its latch; a double chest’s 15-wide halves), laid out on an entity texture the way the game lays a box out, plus the rules for its states and the blocks of its family. It becomes a base, blocks/base/<recipe>.dh-block, and each block of the family a thin block over it naming its entity textures, so every chest, trapped chest and copper chest shares one shape.

RecipeBlocksStates
chestChests, trapped chests, every copper chest and its waxed twinfacing, type single, left and right
ender_chestThe ender chestfacing
shulker_boxAll 17 shulker boxesfacing, all six ways
banner, wall_bannerAll 32 banners, in their dyerotation in sixteen 22.5° turns; facing
skull, wall_skullSkeleton and wither skeleton skulls, creeper headsrotation; facing
head, wall_headZombie and player heads, with their outer layerrotation; facing
piglin_head, piglin_wall_headThe piglin head, its snout and tusksrotation; facing
decorated_potThe decorated potfacing
conduitThe conduit at rest, its shellnone

The entity textures they bind are copied byte for byte under textures/entity/, each with its material under materials/entity/. A block whose texture the jar lacks keeps its placeholder, and the report says which texture.

What a recipe does not draw is per-block data the game keeps in a block entity: a banner’s patterns, a player head’s skin (it draws Steve), a sign’s text, a pot’s sherds. Those come back with per-cell data. Also left out: a piglin head’s turned ears, and an active conduit’s cage and eye. Signs, hanging signs and beds are models in 26.x and need no recipe.

Deferred, with their placeholder cubes: the end portal and end gateway (a shader effect, not a shape), the moving piston (a block in motion), the dragon head (a mob’s model on a 256-texel texture) and the copper golem statues (a mob’s model in its poses). A bubble column draws its water and not its bubbles.

The report ends with the shapes it shared and what it drew in code. On 26.2:

Minecraft 26.2: 1198 blocks, 2586 models (2379 source models), 1337 textures (102 animated), materials 695 opaque / 574 cutout / 65 translucent, 2.4 MB
Shapes: 90 bases, 1065 blocks built on one, 133 self-contained; block definitions 1.7 MB (written whole they would be 6.2 MB)
Drawn in code, from DH's recipes: 74
Special (placeholder cubes): 13: copper_golem_statue, dragon_head, end_gateway, ...

Between machines. The pallet is generated on each machine and never sent, and a server and its players can hold different ones. A server names the fingerprint of its pallet’s block definitions when a map loads, and a player whose pallet differs gets a warning naming the pallet: the server’s collision is the one that counts. See block shapes.

Once it is built, put the pallet in your workspace and any Minecraft voxel container draws from it; nothing has to name it. A rebuilt pallet shows on the next map load.

Running the import again overwrites what it wrote and lists any file under blocks, materials or textures it did not write, which is what an earlier version left behind.

Minecraft is not a Unity game, so the DigitalHeaven.Unity runtime the other mods share does not apply. Its own render API (Blaze3D) exposes no compute pass and no storage buffers, so DigitalHeaven’s skinning shaders cannot run inside it, and porting the engine’s data model to Java would leave a second, permanently drifting copy of it. Instead the mod launches DigitalHeaven.Engine.Host in a headless avatar service mode as a child process, asks it to render the avatar into a shared memory-mapped file every frame, and composites the result over Minecraft’s own frame with a depth-writing full-screen quad — under water, glass and translucents, and occluded by the world exactly as an avatar standing there would be.

The full evaluation of the alternatives (in-process hosting via java.lang.foreign, a Kotlin port of Core, a native Blaze3D re-implementation, and why none of them won) is in design-notes/minecraft-java-adapter.md at the repository root. The exact process contract both sides build against is design-notes/minecraft-avatar-service-contract.md — this section summarizes it, not duplicates it; change the contract doc first if either side needs to change.

The mod launches the engine once, when an avatar is selected, and keeps it alive until the avatar is set back to none or the game exits:

<enginePath> --avatarService --frameFile <path> --frameBytes <n>
<enginePath> --avatarService --frameSection <name> --frameBytes <n>

See the engine-side --avatarService docs for the launch mode itself. Control is line-oriented over stdin/stdout — one command per line, one reply per line, UTF-8 — with commands to list avatars, load <barcode>, request a frame (camera, pawn position, pose, light, in Minecraft’s own coordinate and angle conventions), and quit. The host writes nothing to stdout except protocol replies; logs go to stderr and the engine log file.

The world frame is requested before Minecraft renders the world, so the engine’s render overlaps the game’s instead of stalling the render thread behind it. The finished avatar and its held items composite after opaque terrain and features but before translucent features and terrain. That matches vanilla entity ordering: opaque world depth still occludes the avatar, while water and glass render over it.

The render thread waits at most 50 ms at that seam for the engine’s reply. A frame that misses it is not dropped: the request stays in flight and the next frame consumes it. For up to holdFrames misses in a row (2 by default) the mod draws the last composited frame again, so a late frame costs a frame or two of lag instead of the vanilla skin popping in. A held frame keeps the camera it was rendered from; it is not reprojected. It never crosses an avatar change, a pallet reload, a world or dimension change, a perspective change (first person, F5 back, F5 front) or a window resize: any of those drops it at once. Every fall back to the vanilla body is logged in latest.log with its reason (late, the engine had not answered in time; busy, an earlier late request was still in flight), each late frame is logged with where its time went (queued, waiting behind a doll’s frame, in the engine), and a line once a minute counts fresh, held and fallen-back frames by reason. World frames run on a dedicated thread, never on the shared common pool.

If the engine process dies, the mod restarts it from the client tick within 10 s, also while a screen has singleplayer paused, and re-renders any open preview; closing it on purpose (selecting None, a frame-map remap, quitting) never surfaces as an error. A start or load that itself fails is reported once and not retried until the next world join, menu open or avatar selection. Failures post one chat line with the [DigitalHeaven] prefix, the engine’s last fatal:/error: lines (at most three) and a [copy details] button that copies a full diagnostic report (versions, reason and stack, avatar, frame-map size, engine path, the last 40 engine stderr lines); engine stderr also lands in latest.log as [engine] lines at INFO, or WARN for alarms.

Elytra flight, a riptide spin, sleeping, the swim-pose transition and spyglass scoping all put the first-person camera inside the avatar’s own body. hideFirstPersonBody (on by default; Quality → Hide body while flying, sleeping or scoping) skips the world frame entirely for those poses instead of drawing the inside of a mesh, matching the rule the First Person Model mod uses for the vanilla arms. The four transient poses (sleep, spin, elytra, swim transition) hold the hide for 100 ms after they clear, so a one-tick flicker between states does not flash the body back on; scoping does not get that hold. Third person and the HUD paper doll, inventory and menu preview are unaffected — only the world frame is skipped, so the session, the doll and the vanilla arms all keep working normally. A hide or unhide is treated as a view change, the same as a perspective switch, so a held frame never paints through the transition.

Pixels never travel over stdout. Each frame reply says a sequence number is ready in a shared mapping both sides opened up front, sized by the mod for the largest viewport it will ask for (64 + width * height * 8 + 32 bytes: a 64-byte header, then RGBA8 color, then float32 depth, then the hand item frames, all little-endian, row-major, top row first). Alpha is coverage (255 where the avatar drew a pixel, 0 for background), and depth is the engine’s raw clip depth, which the mod’s composite shader converts to Minecraft’s own depth convention so the engine never has to know it.

Every frame rewrites the whole mapping, so it is kept in memory rather than on disk wherever the OS allows. A mapped regular file would be written back to disk for nothing: on Linux continuously while you play (about 2.3 GB an hour at 1080p), on Windows each time the mapping closes. The mod picks the backing once per launch and says which one in latest.log (Avatar frames live in ...):

OSWhere the frame lives
WindowsA page-file-backed named section (Local\DigitalHeaven-...) the mod creates through java.lang.foreign. Nothing is written to disk.
LinuxA file on a tmpfs: /dev/shm, or $XDG_RUNTIME_DIR when that is the tmpfs. Under the Prism Flatpak this is the sandbox’s own /dev/shm, which the engine shares as the game’s child process.
macOSA POSIX shared-memory object (/dh-...) the mod creates through java.lang.foreign. The engine removes its name as soon as it has it open, so nothing is left behind if either side crashes. Nothing is written to disk. Not yet tried on a Mac: the same code is tested on Linux.
Anything else, or any failure abovedigitalheaven-frame.bin in the instance’s game directory, as before.

On Windows and macOS the JDK prints a one-time WARNING: A restricted method in java.lang.foreign... line to the game’s console, because Prism does not launch Minecraft with --enable-native-access. It is harmless. Add --enable-native-access=ALL-UNNAMED to the instance’s JVM arguments to silence it. On Windows and macOS the mod needs an engine that knows its --frameSection, so update both together. The measurements and the full rules are in the contract note’s “Frame backing” section.

The world, the HUD paper doll, the inventory doll and the menu preview are four views of a single simulated avatar. World frames advance the simulation; the other cameras read the finished bone matrices without advancing time or resetting spring history, so every view shows the same pose, the same spring state and the same lighting without paying for a second physics pass. Each camera still costs its own render and readback. The inventory keeps its own pose as a presentation override, and first-person head hiding is applied to a copy so it does not leak into the other views.

The inventory doll replaces every GUI entity picture of the local player (GuiGraphicsExtractor.entity), not just the inventory screen’s, so a paper-doll mod that draws the player the same way shows the avatar too.

Minecraft, not the engine, draws what the avatar holds and wears. It draws these after the avatar’s composite, depth-tested against it, so the avatar’s fingers and body cover an item exactly as a vanilla hand would. Enchantment glint, resource packs, animated items and modded item renderers all work unchanged, and nothing is converted into the engine.

  • Held items hang at the wrists the engine returns for that same frame, turned the way the avatar’s forearm actually points: vanilla’s arm angle, carried through any elbow bend and the whole-body turn of a glide, a spin, a fall or a bed. They are drawn in every view: the world in first and third person, the paper doll, the inventory and the menu preview.
  • Armor and cape, elytra, and head items (a carved pumpkin, a skull or a banner), are vanilla’s own player layers, each behind its own setting. Vanilla’s player model runs invisibly at the pawn to place them, and its body is never drawn. They are sized for a vanilla player, so on an avatar with other proportions they float off the body. That is why armor and head items are off by default; elytra is on, since a glider showing without the rest of the kit is the common case. In first person the helmet and head items are left out, because they would enclose the camera.
  • No bone moves for any of it. Items only read the wrists and the forearms.

In the world these draw straight into Minecraft’s frame. A UI view composites the engine’s color and depth into its own offscreen target under a camera matching the one the engine rendered from, and draws the items over that. Its picture otherwise goes straight into a texture. With all four settings off, none of this runs: no player state is extracted for it, no depth is copied, and no offscreen target exists.

Minecraft’s vanilla humanoid model supplies six part rotations per frame, sampled after the partial-tick hook so the pose is never a tick stale (including the extensions Not Enough Animations adds). Those rotations are mapped onto the avatar’s standard humanoid bones over their authored rest axes, T-pose and A-pose arms are aligned downward with torso clearance, and only then are the spring chains stepped. The retarget is rotation only: Minecraft’s six parts are disconnected siblings that vanilla re-places by hand while animating (crouching drops the head 4.2 px, the body 3.2 px and shifts the legs 4 px back purely to keep the boxes looking attached), whereas a hierarchical avatar rig gets attachment for free, so each part’s rotation is applied about the mapped bone’s own authored pivot, unmapped bones (spine, neck, clavicles, a tail) follow their parent, and no bone is translated or scaled. The one translation applied is the whole-avatar root: everything vanilla’s player renderer does to the whole body after its yaw, in its own order. That is the crouch drop, the sprint-swim pitch and offset, the elytra glide (pitched by the fall-flying ticks and banked by the angle between the view and the velocity), the riptide spin, the death tip-over, lying in a bed, the frozen shake and an upside-down name tag. Because vanilla’s body pivots at the neck and an avatar chest pivots at its authored base, a crouch lean swings the shoulders forward here where vanilla swings the torso bottom back; that is the rig’s own proportions and is deliberately not compensated. Wrist positions travel back in the frame header, and each forearm’s orientation after the depth plane, which is how a vanilla held item ends up in the avatar’s hand.

Two corrections keep vanilla’s poses readable on other proportions, both rotations only:

  • Arm reach. Minecraft’s arms are exactly as long as its shoulders are wide, so an angle that brings its fists together at the midline carries a longer-armed avatar’s hands past each other. Not Enough Animations’ swap-hands and its custom bow draw are the usual cases. The engine measures how far vanilla’s fist is from the body’s midline and maps that onto the avatar: fists touching become palms touching (the palms’ measured thickness), and a fist at the shoulder becomes a palm at the shoulder. A palm further across than that bends its elbow on the hinge vanilla’s arm carries and swings its shoulder until it lands there. Arms at the sides are never touched. client.anim.minecraftArmReach turns it off.
  • Shoulder share. A raised arm lifts its clavicle too, so a swim stroke or a trident overhead does not fold the shoulder skin shut. The clavicle takes 30% of the arm’s swing away from hanging (client.anim.shoulderShare), easing in from 30° to 90° of swing. That is a little more than Unity’s humanoid defaults give the shoulder, because Minecraft’s stroke goes all the way overhead. It never takes the arm’s twist, and the upper arm keeps exactly vanilla’s direction. A rig without a shoulder bone is left as it was.

-Pdigitalheaven.poseSmoke=true shoots every item pose (swap hands, sword swing, bow, crossbow charge and hold, shield, trident, spear, eat, drink, spyglass, goat horn, brush), the elytra straight and banking both ways, riptide, sleep, death and the swim stroke. Each pose is shot on the avatar and on the vanilla player from the front and the side, with the same script so a moving pose is caught at the same phase. -Pdigitalheaven.poseSmoke.only=bow,elytra narrows it.

Pausing a single-player world stops the avatar with it: spring chains, eye look and blinking hold exactly where they are while the pause menu is open, and carry on from there when you return, with no catch-up jump. A server or LAN world keeps running while its menu is open, so the avatar does too.

Spring chains rest on the floor. Each world frame carries the tops of the block collision boxes around the player’s feet (block tops, slabs, stairs, carpets), and the engine keeps every tail, ear or bell particle above them by turning its parent bone, never by moving or stretching it. A chain drapes over a ledge from its first bone past the edge. Block sides are not sent yet, so a tail can still pass into a wall.

Lighting samples the block and sky light at four points on the body, interpolates them into an ambient term, and adds sun or moon light at its real angle with 64-block occlusion rays — so a roof overhead darkens the avatar on a bright day, and zero sky light disables the directional term entirely.

Each probe reads light through the same vanilla functions Minecraft uses to light terrain and the player’s own model: the terrain brightness getter (LightCoordsUtil.BrightnessGetter.DEFAULT) and the player’s entity-renderer block and sky lookups (EntityRenderer.getBlockLightLevel / getSkyLightLevel). The brighter of the two wins. Dynamic-light mods hook exactly these, so a torch, lantern or glowstone held in either hand, or a light source dropped nearby, lights the avatar just as it lights the vanilla armor and elytra drawn over it. That covers LambDynamicLights, which is tested, and any mod that hooks the same functions. A mod’s fractional light and its own update rate come through unchanged, and DigitalHeaven adds no flicker of its own. If a mod raises only the player’s final packed light, that value still sets a floor. Dynamic light is ordinary block light here, with the same warm tint and falloff as a placed torch. -Pdigitalheaven.dynamicLightSmoke=true photographs the held, offhand, lantern, dropped and placed cases in a sealed dark room, with vanilla armor on as the reference.

Config file: config/digitalheaven.json in the Minecraft instance folder. Every value except enginePath has a control in the DigitalHeaven menu, and editing the file by hand is equivalent. enginePath is set in the file only.

KeyDefaultDescription
avatarnullBarcode of the avatar to wear. null (the default) means the mod does nothing at all — no process is launched and vanilla rendering is untouched.
enginePathnullFull path to DigitalHeaven.Engine.Host.exe. null falls back to the workspace’s own dh-config.jsonc (enginePath).
animationMode"minecraft"Minecraft-driven humanoid animation, or "none" for the authored bind pose.
renderScale0.5Shared avatar resolution scale for the front and back F5 views, from 0.25 to 2.0.
firstPersonRenderScale0.25First-person avatar resolution scale, from 0.25 to 2.0, independent of both F5 views.
hideFirstPersonBodytrueSkip the first-person world frame while flying, sleeping, spinning or scoping. Third person and the dolls are unaffected. See Hiding the body in first person.
uiRenderScale1.0Resolution scale for the HUD doll, inventory doll and menu preview, from 0.25 to 2.0. Independent of both world scales, so world views can render cheaply while the dolls stay sharp.
holdFrames2How many late world frames in a row the last avatar frame is drawn again before the vanilla skin shows, from 0 to 10. 0 falls back at once.
nearFadeInner0.20Distance in meters at which the avatar is fully invisible to its own camera, from 0 to 2.
nearFade0.30Distance in meters at which it is fully opaque, from 0 to 2. Between the two it dithers out. Equal values hard-cut; 0 disables the fade. Setting the pair high (for example 1.8 / 2.0) hides the first-person body entirely.
fadeEnabledtrueThe fade switch. Off draws the body solid and skips the fade’s work, and keeps nearFadeInner and nearFade for when it is turned back on.
compositeDepthTesttrueThe avatar composite’s depth test. Off draws the avatar with no depth compare and no depth write, over everything, for troubleshooting a composite that shows nothing. The first composite also logs its depth convention and the device’s isZZeroToOne to latest.log.
paperDollfalseShow a small avatar on the HUD.
dollSize80HUD doll height in GUI pixels, from 48 to 480.
dollRightfalseUse the top-right corner instead of the top-left.
avatarItemstrueHeld items on the avatar, in every view. Off means no items anywhere, including first person.
avatarArmorfalseVanilla armor and cape over the avatar.
avatarElytratrueVanilla elytra wings over the avatar, independent of avatarArmor.
avatarHeadItemsfalseA carved pumpkin, skull or banner worn on the head.
shareAvatartrueTell the server which avatar you wear, so other players on it see it. Only the barcode is sent, and only to players on the same server. Off sends “no avatar”, which clears you for everyone. See Multiplayer.
remoteAvatarstrueDraw other players as the avatars they wear. Off leaves every other player vanilla.
remoteAvatarDistance64Blocks from you, from 4 to 256, past which another player keeps the vanilla model. A player already drawn stays drawn to 1.25 times this, so one walking along the edge does not reload.
remoteAvatarCount8How many of the nearest other players are drawn as avatars, from 0 to 64. The engine has its own cap, client.avatarService.remotePawnCap (8), and a player past either keeps the vanilla model.

The workspace root is DH_WORKSPACE when that environment variable is set, otherwise ~/Documents/DigitalHeaven, matching every other DigitalHeaven tool.

Press H (unbound in vanilla) to open the DigitalHeaven menu, or use the cloud button on the title screen and in the pause menu. Recompiling an avatar’s pallet while the game is running reloads it in place.

The menu is one screen with four tabs in Minecraft’s own tab bar, the one Create World uses. Done or Esc returns to the title or pause screen it was opened from. Ctrl+Tab and Ctrl+1–4 switch tabs.

TabControls
AvatarThe avatar drop-down (the engine’s list, or None to go back to vanilla rendering), then Animations, Items on avatar, Armor on avatar, Elytra on avatar and Head items on avatar, and under Multiplayer, Share my avatar with other players. On the right is a live preview of the avatar: drag with the left button to rotate it, scroll to zoom (1–4×), and drag with the right button to pan within the zoom margin. The preview stops asking the engine for frames while another tab is open and keeps its camera until you come back.
Paper DollPaper doll on/off, the corner, and the size (48–480 GUI pixels), with no other text on the page; the toggle’s tooltip says F1 hides the doll. While the doll is off, the corner and size controls are disabled rather than hidden.
CameraThe first-person fade: a Fade on/off switch, then Invisible and Opaque (both 0–200 cm, kept in order, and disabled rather than hidden while the switch is off), a ruler of the invisible, dithering and opaque bands, and a live preview, in the window’s shape, of first person looking straight down (Minecraft’s 90° limit) at your own avatar. The preview is the world’s own first-person frame: the same request builder as the world pass (setback, head chop, first-person resolution, eye height, FOV) and the same composite shader and dither, so it fades exactly as the world does. In a world it shows your current pose; at the title screen, a standing one. It asks the engine for frames only while the Camera tab is open. Last is Depth test (compositeDepthTest), a troubleshooting switch that updates live.
QualityThe world views (First person and Third person (F5) resolution, and Hold last avatar frame, 0–10 frames, Off at 0), which apply when you leave the menu; Hide body while flying, sleeping or scoping, which updates live; and Dolls & preview, which also updates live.

While the paper doll is on, it also draws over the menu at its real HUD corner, but only on the Paper Doll and Quality tabs, where its size, corner and resolution are set. The Avatar and Camera tabs never show it. The open-menu key itself is in vanilla’s Controls › Key Binds, under DigitalHeaven.

An avatar the engine cannot load costs that avatar, not the engine. Chat says could not load the barcode and why, your player keeps the vanilla model, and the engine stays up: the drop-down still lists every avatar, another pick loads at once, and picking the same one again (or reopening the menu) retries it, so a fixed pallet loads without restarting the game. Engine unavailable means the engine itself could not be started or listed.

Players who run the mod learn which avatar each other wears through the server, on the digitalheaven:wears channel. Nothing goes through a DigitalHeaven service: there is none, by design.

Where you playAvatars shared?
Singleplayer world opened to LAN (also e4mc and Essential hosting)Yes when the host runs the mod
Fabric dedicated server with the mod jar installedYes
Paper serverPlanned through a DigitalHeaven plugin on the same channel
Vanilla server, RealmsNo — they drop the channel, and there is no workaround

The same jar is the server: it declares "environment": "*", so a Fabric server loads it without any client code. The server keeps each connected player’s latest record and sends it to whoever can see that player, including a player who joins later. It clears a player’s record for everyone when they leave or turn sharing off. A client with the mod finds out whether the server relays by checking that the channel is registered, and says nothing when it is not. A player without the mod is never sent anything.

Only the barcode travels, never pallet bytes. Each player must already have the other players’ pallets in their own workspace. The Share my avatar with other players switch (shareAvatar) stops even the barcode leaving; it is on by default. Each record is capped at 256 characters, and the server rate-limits changes per player and ignores anything malformed.

Your workspace’s dh-friends.jsonc (in the workspace root, the folder dh-config.jsonc’s workspaceRoot names) can say which avatar a player wears, by their Minecraft UUID. It needs no server support, so it also works on servers that relay nothing, and it wins over what the server says. The mod reads it each time you join a world:

{
"steam": {},
"minecraft": {
"bd3ba3bf-4748-3975-9e6c-26c0bb1936e2": {
"displayName": "Poof",
"avatar": "poof.avatars.poof-taidum:poof-taidum.dh-avatar"
}
}
}

Each other player whose avatar you have is drawn in the same engine frame as yours, so it composites with the same depth, fog and near fade, and one player standing in front of another hides them correctly. The nearest remoteAvatarCount within remoteAvatarDistance are loaded, and of those, the ones Minecraft would render this frame are drawn. Spectators and players invisible to you stay out.

  • Pose. Each player’s pose comes from Minecraft’s own animation of that player, the same sampling as your avatar (walking, crouching, swimming, gliding, sleeping, dying, riptide). Their springs and eyes run on your machine, so their tail swings from the motion you see.
  • Kept from vanilla. The name tag, shadow and hitbox. Armor, cape, elytra and head items follow the same attachment settings as yours. Held items are vanilla’s, at vanilla’s hand position rather than the avatar’s wrist.
  • Light. Every avatar in a frame is lit by the light around you. Each other player’s avatar is then darkened or brightened by how much light reaches them compared to you, so someone in a cave looks dark while you stand in daylight. This is one level per player, with no light direction of its own.
  • When the vanilla player shows instead. When you have no entry for them, when their avatar is not in your workspace or does not load (retried every 30 seconds, so a fixed pallet is picked up), while it loads (a first load of a large avatar makes a short hitch), and past the caps. It also shows while your own first-person body is hidden (flying, sleeping or scoping in first person).

In first person, a frame with other players in it is rendered at renderScale rather than firstPersonRenderScale, since they are seen whole.

The mod project lives at Platforms/DigitalHeaven.Mods.Minecraft/ and is a standalone Gradle project (Fabric Loom) — it is not part of DigitalHeaven.sln. Its settings.gradle also includes Platforms/DigitalHeaven.Mods.Java/ as the subproject :bridge, so building the mod builds the bridge first; there is nothing to build or install separately.

Terminal window
cd Platforms/DigitalHeaven.Mods.Minecraft
./gradlew runClient # gradlew.bat runClient on Windows
./gradlew build # jar in build/libs/digitalheaven-mc<version>-<mod version>.jar

runClient launches the Fabric development client with the mod loaded. One source tree builds against both supported game versions: minecraft_version in gradle.properties selects 26.2 or 26.3, the version-specific sources live in src/26.2/java and src/26.3/java, and the jar is named for the version it targets. To install a built jar, drop it into the mods folder of a Fabric instance next to Fabric API.

Two Gradle properties are forwarded to the client as system properties for testing, without any extra wiring per property (every -Pdigitalheaven.* property becomes a -D JVM arg):

  • -Pdigitalheaven.devPerspective=back|front|first pins the camera to a named perspective on join, so acceptance footage shows a chosen angle instead of whatever the options file last left it at.
  • -Pdigitalheaven.traceFrames=N records a frame trace for the first N avatar frames (timings, positions) for debugging composite issues.

The game-agnostic half of the mod lives in its own Gradle module, Platforms/DigitalHeaven.Mods.Java/ (package io.mltn.digitalheaven.bridge), shared with any other Java game mod. It holds the service client, the frame reader, the session lifecycle with its recovery and frame handshake, the shared config keys, the diagnostic report, and the API-neutral half of the world composite, including the shared shader body dh_composite_body.glsl. The module has no game or graphics library on its classpath, so a Minecraft, Fabric or Mojang reference there fails to compile.

Minecraft plugs into it through small interfaces:

  • MinecraftSession implements SessionHost: game paths, monitor size, logging, and the chat messages for reloads and failures.
  • MinecraftConfig implements ModConfig.Extras: the paper doll, near fade, first-person scale, animation mode, avatar attachment and sharing keys.
  • RigPose implements RigPayload: the rig2 suffix.
  • The per-version AvatarCompositor implements CompositeBackend: Blaze3D texture uploads and the composite pass.
  • core/dh_composite.fsh includes the shared body and defines the depth reconstruction and Minecraft’s fog.

The build copies the shader body into assets/digitalheaven/shaders/include/ and packs the bridge classes into the mod jar, so an install is still one jar.

The multiplayer protocol lives in a second subproject, Platforms/DigitalHeaven.Mods.Minecraft/relay/ (:relay, package io.mltn.digitalheaven.relay), packed into the jar the same way. It holds the digitalheaven:wears message and its validation (WearsCodec, Wears), the server’s per-player registry and fan-out (WearsRelay) and the client’s change detection (WearsAnnouncer). Like the bridge it has no Minecraft or Fabric type on its classpath, so a Paper plugin can wrap the same bytes; it compiles for Java 21 because Paper servers commonly still run it. Its JUnit tests run with ./gradlew build.

The message is identical in both directions (big-endian):

FieldSize
versionu81. Later versions only append fields; a reader ignores what follows the fields it knows.
player UUID2 × u64Most significant half first. The server ignores the value a client sends and stamps the connection’s own.
barcode lengthu160 means no avatar, which clears the record. At most 256.
barcodeASCIILetters, digits and . - _ : / + ~ @, not starting or ending with :.
closure countu8Reserved for content transfer and always 0 today. At most 64.
closure hashescount × 16 bytesEach pallet’s 128-bit content hash, high word first.

The Fabric glue is DigitalHeavenRelay (the main entrypoint: payload registration, the server receiver, EntityTrackingEvents.START_TRACKING and disconnect) and WearsClient (the client receiver and a per-tick announce that fires only on a change, so a menu pick, a config reload and the share switch all reach the server the same way). Received records go into RemoteAvatars, keyed by player UUID.

-Pdigitalheaven.relaySmoke=true checks the relay with two dev clients. Start the host, which creates a fresh flat test world and opens it to LAN on port 25599 (-Pdigitalheaven.relaySmoke.port changes it). Once its log says LAN open, start the guest in a separate run folder and under another offline name. -PdevRunDir and -PdevUsername set those, and --quickPlayMultiplayer joins:

Terminal window
./gradlew runClient -PdevRunDir=run-relay-host -PdevUsername=Host -Pdigitalheaven.relaySmoke=true
./gradlew runClient -PdevRunDir=run-relay-guest -PdevUsername=Guest -Pdigitalheaven.relaySmoke=true \
-Pdigitalheaven.relaySmoke.role=guest --args="--quickPlayMultiplayer localhost:25599"

The host’s config/digitalheaven.json names dev.pitr:avatar/plush and the guest’s dev.pitr:avatar/taidum. These are barcodes the fake avatar service lists, so point enginePath at tools/fake-avatar-service.cmd. The two logs record the late join, an avatar change, sharing off and on, and the guest leaving, each as a PASS relay smoke line, and both clients exit on their own. The smoke logs every screen it meets and handles each one in code: it closes the first-run accessibility screen, accepts the dev world’s experimental data pack warning, and fails the run on a disconnect. Seed each run folder’s options.txt anyway (onboardAccessibility:false, narrator:0, skipMultiplayerWarning:true, joinedFirstServer:true, tutorialStep:none, music, record and ambient volume 0.0). The smoke also mutes music, records and ambience itself.

To check a Fabric dedicated server instead, start one with ./gradlew runServer -PdevRunDir=run-relay-server, with online-mode=false and white-list=false in its server.properties. Then start both clients with --args="--quickPlayMultiplayer localhost:<port>", the host with -Pdigitalheaven.relaySmoke.dedicated=true and the guest once the host logs Relay: sent.

-Pdigitalheaven.pawnsSmoke=true checks drawing, against the real engine: the same two clients on port 25601 (pawnsSmoke.port, pawnsSmoke.role=guest), each wearing a real workspace avatar with enginePath pointing at the engine host you built. The host stands the guest four blocks in front of it. Once each client has drawn the other’s avatar for two seconds, it photographs its own game frames into screenshots/pawns: first person, the F5 view, the guest walking with a sword and crouching, and the vanilla player with remoteAvatars off. The host then checks that the guest’s avatar is dropped when it leaves. Every step logs PASS Pawns smoke or FAIL Pawns smoke, and the run recorder keeps every second tick. Turn the guest’s shareAvatar off and name it in the host’s friends file (DH_WORKSPACE pointing at a scratch workspace whose dh-config.jsonc names your real palletsDirectory) to check the friends path and the relay in one run.

Platforms/DigitalHeaven.Mods.Minecraft/tools/fake-avatar-service.js (run through fake-avatar-service.cmd, a Bun script) speaks the same avatar service protocol and renders a lit sphere in place of an avatar — wide enough to be clipped by a doorway, occluded by blocks and tinted by water like a real avatar would be. Point the mod’s enginePath config at it to build and look at the Java-side compositing without building or running the engine.

  • Other players need your own avatar on. Their avatars are drawn by the engine process your avatar starts, so with your avatar set to None everyone is vanilla.
  • Other players’ avatars have to be in your workspace. There is no pallet transfer and no NPC replacement, and vanilla, Realms and (until its plugin ships) Paper servers share nothing. The friends file still works there.
  • No overlay — the DigitalHeaven overlay is not hosted in Minecraft.
  • No shader pack support — the avatar composites correctly under Iris on the OpenGL backend, but it is lit by the block and sky light rather than by the pack, and casts no shadow into the pack’s own lighting. A dynamic-light mod still lights it, but a pack’s own handheld light does not: Iris hands the held item’s light to the pack as uniforms (heldBlockLightValue), not through Minecraft’s light values. Iris integration waits on Iris supporting Vulkan.
  • The first-person camera sits below the avatar’s eyes on rigs whose proportions differ from a vanilla player’s, because the camera arrives as Minecraft’s 1.62-block standing eye while the avatar is fitted by total height. The near-camera fade is the workaround; set nearFadeInner and nearFade high to hide the body entirely.
  • No elbow, knee or finger animation — vanilla supplies six part rotations, and nothing finer.
  • Held items follow the forearm, not the hand bone, so an avatar whose hand sits at an angle to its arm holds them slightly off. A spyglass is held on the arm, not at the eye where vanilla draws it, and maps are not held in two hands. With animations set to None, items follow the bind-pose forearm.
  • Armor, elytra and head items are vanilla-shaped and placed by vanilla’s player proportions, so they float off an avatar built differently.
  • The inventory doll keeps a standing root, so it does not glide, spin, sleep or fall over with the world avatar.
  • Arm reach only acts across the body. A hand vanilla holds out to the side or overhead keeps vanilla’s arm angle, so a longer arm reaches further there than vanilla’s does.
  • No avatar shadow in the world, and no status-effect or gamma matching.
  • Springs collide with floors only — block sides are not sent, and surfaces more than half a block above the feet are left out.