dh.voxel
Extension: .dh-vox
Type ID: dh.voxel
A voxel container holds block worlds and voxel models: a MagicaVoxel model, a Teardown prop, a Minecraft schematic, structure or whole world. The .dh-vox file is a small definition that says whose rules the data follows and where the data is. The data itself is a binary payload named after the definition with .bin added (castle.dh-vox → castle.dh-vox.bin), and Studio shows the pair as one asset.
data can also name a foreign source the compiler converts on build, the way an FBX is converted today. The compiled pallet always holds the canonical .dh-vox.bin, and the source never ships.
| Source | data names | format | platform |
|---|---|---|---|
| MagicaVoxel or Teardown | a .vox | vox | required: MagicaVoxel or Teardown |
| Sponge schematic (WorldEdit) | a .schem | schem | Minecraft, implied |
| Litematica schematic | a .litematic | litematic | Minecraft, implied |
| Vanilla structure (structure block) | a .nbt | nbt | Minecraft, implied |
| MCEdit / Schematica schematic | a .schematic | schematic | Minecraft, implied |
| Java Edition world | a world folder, or one dimension’s folder | anvil | Minecraft, implied |
Properties
Section titled “Properties”The field set is closed: an unknown field is a compile error that lists the valid ones.
| Property | Type | Required | Description |
|---|---|---|---|
$type | "dh.voxel" | no | Type identifier |
name | string | no | Display name |
description | string | no | One line on what this is |
platform | string | for a .vox | Whose rules the data follows, as a publisher-first ID or a built-in alias such as minecraft, stored as the ID. It decides what a palette index means. A Minecraft source implies com.mojang.minecraft, and naming another platform for one is an error |
format | string | no | The format the data arrived in. The compiler records the source’s own (vox, schem, …), and naming another one is an error |
platformVersion | string | no | The source’s own version, such as 26.2. Left out, a Minecraft source fills it from its data version |
voxelSize | number | no | Meters per voxel. Must be > 0. Seeded from the platform when left out |
axes | string | no | Up axis and handedness: x-up-right, x-up-left, y-up-right, y-up-left, z-up-right or z-up-left. Seeded from the platform when left out |
origin | [int, int, int] | no | The origin cell. Defaults to [0, 0, 0] |
data | string | yes | A .dh-vox.bin, or one of the sources above, in this pallet |
namespaces | { string: string } | no | Maps each block namespace to the pallet that provides it, e.g. { "minecraft": "com.mojang.minecraft" }. A namespace with no mapping falls back like a missing pallet |
lighting | string | no | How the container is lit where a placement does not say: dh, hybrid or minecraft (see lighting modes). Left out, the platform’s default. Read at load, so changing it rebuilds nothing but the definition |
The type is inferred from the file extension, so
$typeis not needed in source files. Adh.voxeldoes not takeinherits.
Full Example
Section titled “Full Example”{ "name": "Crate", // A Teardown prop: palette index 57 to 72 is wood, whatever color sits there. "platform": "com.tuxedolabs.teardown", "data": "crate.vox"}The build converts crate.vox into props/crate.dh-vox.bin and points the compiled definition’s data at it. The .vox itself does not ship.
Platforms
Section titled “Platforms”The platform field names whose rules the data follows. It is the term Platforms/ uses, for games and tools alike. IDs are reverse-DNS, publisher first, like the workspace’s pallets; a built-in alias such as teardown reads as its ID and is stored as the ID.
A built-in table of platform defaults fills in voxelSize and axes only when a definition leaves them out. When an authored value disagrees with the table, the build warns and keeps the authored value. The file always wins, so data from an unknown platform, a modded game or a neutral tool still builds.
| Platform | platform | voxelSize | axes | A palette index is |
|---|---|---|---|---|
| MagicaVoxel | com.ephtracy.magicavoxel | 0.1 (a convention; the tool has no scale) | z-up-right | A color: the importer mints dh:color entries |
| Teardown | com.tuxedolabs.teardown | 0.1 | z-up-right | A material: teardown:<material> with props.index |
| Minecraft Java | com.mojang.minecraft | 1 | y-up-right | A block state: minecraft:oak_stairs with its properties as strings |
Importing a .vox
Section titled “Importing a .vox”A .vox is read from MagicaVoxel’s published format (MagicaVoxel-file-format-vox.txt and its extension):
- Models.
SIZEandXYZIchunks. The size limit of 256 per axis is eight 32-cell chunks. - Scene graph. Every
nTRNtransform node becomes a grid, so the hierarchy survives. A group’s transform carries its children. A shape’s model fills the grid of the transform above it. - Transform. The rotation byte is decoded to its signed permutation, mirrors included. A model’s cells sit around the pivot
floor(size / 2), so a grid’s transform isR * (cell - pivot) + t, with the pivot folded into the translation. - Axes. MagicaVoxel is Z-up. The axes are recorded as
z-up-right, never baked into the cells. - Palette. All 255 entries are kept, so a global palette index equals the source index. Each entry carries
indexandcolor(#rrggbbaa). AMATLaddsmaterial(its_type) and the rest of its properties as strings, as written. A file with noRGBAchunk uses MagicaVoxel’s default palette. - Layers. A node’s
LAYRis kept as grid attributes (layer,layerHidden), as is a hidden node (hidden). These are MagicaVoxel’s layers, not the container’s named layers below. - Everything else (
rOBJ,rCAM,NOTE,IMAP, and any chunk DH does not read) is kept whole as asourceextra taggedmagicavoxel.chunk.<ID>. - Animation. Only frame 0 of a transform or shape is kept, with a warning.
Teardown’s materials
Section titled “Teardown’s materials”In a Teardown .vox, the palette index is the material and the color is only its tint. The ranges come from the annotated teardown_palette.vox Tuxedo Labs publishes. Its notes name each row of eight. Two documented facts confirm the reading: index 9 is grass and indices 57 to 72 are wood.
| Indices | Material |
|---|---|
| 1–8 | glass |
| 9–24 | grass |
| 25–40 | dirt |
| 41–56 | rock |
| 57–72 | wood |
| 73–88 | concrete |
| 89–104 | brick |
| 105–120 | plaster |
| 121–136 | weak_metal |
| 137–152 | heavy_metal |
| 153–168 | plastic |
| 169–176 | hard_metal |
| 177–184 | hard_masonry |
| 225–240 | unphysical |
| everything else | reserved |
Importing Minecraft Java
Section titled “Importing Minecraft Java”{ "name": "Spawn", // A world folder in this pallet. "spawn/dimensions/minecraft/the_nether" would import the nether alone. "data": "spawn"}Bedrock is not supported. What every Minecraft source shares:
- Palette. A block state keeps its native id (
minecraft:oak_stairs) and its properties as strings, exactly as Java writes them,waterloggedincluded. Equal states share one entry, so a whole world has one palette. - Air is a palette entry like any other. Structure void means “leave what is there”, so it becomes
dh:empty, the value of an absent cell. It is never a block and neverdh:erase. - Block entities and entities (chest contents, signs, mobs, item frames) are kept as extras, one per block entity or entity: the source NBT byte for byte, as an uncompressed root compound with an empty name, tagged
nbt-java, with its grid and cell. DH does not interpret them yet; per-cell data is a later design. - Version. The source’s
DataVersionis kept in the header’s meta asdataVersion.platformVersion, when the definition leaves it out, comes from the world’s ownlevel.dat, or from the Java release table for a schematic. A data version that is not a release (a snapshot) leaves it empty. DataFixers are never run. - Derived data is dropped: light, heightmaps, scheduled ticks and the rest the game computes again. The meta’s
droppedlists what. The engine computes the light again itself, as the light field.
| Source | What DH reads |
|---|---|
.schem | Sponge versions 1, 2 and 3. One grid, its minimum corner at cell 0. Block entities (BlockEntities, or TileEntities in version 1) and entities. Biomes into the biome layer at block scale: per block in version 3, per column in version 2. Offset and the Metadata strings go into the meta (offset, metadata.<key>) |
.litematic | Every region becomes a grid, named for the region, whose transform is the region’s offset: its Position, moved to the minimum corner when a Size is negative. Block states are bit-packed end to end, at least two bits wide. Tile entities and entities are placed in their region’s cells. Pending ticks are dropped |
.nbt | One grid, or one per palette when the structure has palettes (shipwrecks, ruins): the same blocks read through each palette, named palette <n>, every grid after the first hidden, since the game picks one variant. Block entities belong to the block list and are kept once, on the first grid. A cell the file does not list was structure void, so it stays dh:empty |
.schematic | MCEdit and Schematica, with AddBlocks or Add for ids past 255. Every entry keeps its numeric id:data in its raw blob (format minecraft-legacy), so a re-export is exact. Common blocks whose meaning does not depend on orientation map to their flattened state (stone, dirt, planks, logs and leaves, the sixteen colors of wool, glass, terracotta, carpet and concrete, ores, …). Everything else becomes minecraft-legacy:<id> with an integer data property: a guess at a stair’s facing would be a wrong block, not a missing one |
| world | See below |
Worlds
Section titled “Worlds”A world converts one Minecraft region at a time, so a world of any size never sits in memory whole. The container is chunked at 16, so a DH chunk is a Minecraft section and a DH region is exactly one r.<x>.<z>.mca. Coordinates are the world’s own: cell (x, y, z) is block (x, y, z).
- Dimensions. A world folder imports every dimension it has, each as its own grid named by its id,
minecraft:overworldfirst. Every grid but the first is hidden, because dimensions share coordinates and are never seen together. Pointdataat one dimension’s folder to import that one alone. The current layout (dimensions/<namespace>/<path>/region, which 26.x writes) and the previous one (region,DIM-1andDIM1at the root) are both read, and where a save holds both, the current one is the dimension: the earlier folders are what the game migrated away from. - Chunks. Every compression Java writes is read: gzip, zlib, none and LZ4 (1.20.5 and later), and a chunk too large for its region, which the game keeps beside it as
c.<x>.<z>.mcc. Chunks still being generated at the world’s edge (any status but full) are skipped and counted in the meta’sskippedChunks. - Biomes go into the
biomelayer at Minecraft’s own 4x4x4 cells (cellScale4). - Entities are read from the
entitiesfolder (1.17 and later) or from the chunk (before). - How far back. Chunks from 1.13 on are read: 1.18 and later (
sections,block_states,biomes), and 1.13 to 1.17 (Level.SectionswithPaletteandBlockStates, packed either way), whose numeric biomes are not kept. A chunk from before 1.13 (numeric ids) is skipped with a warning; open the world in a newer Minecraft once to upgrade it. The meta’schunkDataVersionsgives the lowest and highest chunk version read. - Never shipped. Every file under a folder with a
level.dat, and any.mcaor.mcc, is left out of the pallet.
To import a whole save as a map, with its spawn point, run dh import minecraft-world: it copies the world into a pallet, writes this definition and the map, and the build converts the world as above. See Importing a world as a map. A schematic is placed by hand: copy the file into the pallet, write a .dh-vox whose data names it, and place that in a map.
Placing it in a map
Section titled “Placing it in a map”A map places a container with an object carrying a dh.voxelVolume. The whole container is that one node, a Minecraft world included: it moves, turns and scales as one, and its chunks are storage, never objects in the hierarchy. In the level editor, drag a .dh-vox from the asset browser into the viewport and it lands as an object carrying a dh.voxelVolume where you drop it.
The entity’s pose places the container’s origin cell. Under it, the container’s own fields turn cells into meters: each grid’s transform composes under its parent’s, the origin cell moves to zero, cells become meters at voxelSize, and axes turns the source’s up axis into the engine’s Y. A right-handed source stays right-handed (MagicaVoxel’s z-up-right becomes X, Y = Z, Z = -Y), and a left-handed one comes out mirrored, as it looks in its own game. MagicaVoxel centers a model on its pivot, so half of a model placed at a floor’s height sits below it.
A grid the container marks hidden, another dimension or another structure palette, is not drawn. The entity’s delta can show it or hide any other.
How the engine draws it
Section titled “How the engine draws it”A volume is drawn in regions: cubes of client.voxel.regionChunks chunks a side (2 by default, eight chunks), each meshed together into one mesh, never one model per block. A region draws once per surface (opaque, cutout, translucent), so a world costs an eighth of the draws it would one chunk at a time. Regions are meshed on worker threads and uploaded under a per-frame budget, and they draw through the scene’s ordinary material path, so a volume is lit, casts shadows and receives them like map geometry. A face is drawn only where its cell touches something that does not hide it. Neighboring chunks are read across the border, so two chunks never draw the face between them, and a region draws exactly the faces its chunks would one by one.
A block the workspace’s platform pallet names is drawn from its model and textures. Without that pallet, or for a block it does not name, the look comes from a built-in block table, one per namespace, shipped with DH. The colors are DH’s own picks, never sampled from a game’s textures. The same row says whether the block is solid and which surface it is made of, so drawing and collision read one record per block and cannot disagree.
| Source | Color | Drawn as |
|---|---|---|
| Minecraft | DH’s table: about 300 common blocks by id. A shape of a block (_stairs, _slab, _wall, _fence, …) wears its block’s color. The dyed families (_wool, _concrete, _terracotta, _stained_glass, …) wear their dye’s. Any _ore is stone | Opaque, except air, cave air, void air, structure void, barrier and light (not drawn), glass, ice, water, slime and honey (translucent), and leaves and plants (cutout) |
MagicaVoxel .vox | The file’s own palette colors, which are the data | Opaque; a color below full alpha is translucent |
Teardown .vox | The color the file was painted with, or the material’s own color in DH’s small table when the entry has none | Opaque, except glass (translucent) |
| Anything else | The unresolved look: the engine’s glowing red checker, the same one a map slot with no material wears | Opaque |
The block tables
Section titled “The block tables”A block table is data, not code: a text file per namespace (minecraft.blocks, teardown.blocks, dh.blocks), embedded in the engine’s content layer and read once. Nothing in the engine branches on a block id. A row is an id or a pattern, a base and modifiers:
namespace minecraftplatform com.mojang.minecraftset dye red a8302cforms block - _planks s _block _log _stem
barrier - empty solid # not drawn, still a wallwater 3b6ad6 translucent passable # drawn, never stood onglass c6dde4 translucent alpha=60 surface=glass # see-through and solidoak_leaves 4d7f2a cutout surface=grass inner leaves # solid unless a row says passableoak_planks a3834f surface=wood # footsteps, grip and impacts of wood{dye}_wool @{dye} surface=fabric # a set: red_wool wears red{block}_button @{block} passable # forms: oak_button wears oak_planks, and is wooddeepslate_*_ore @deepslate # a wildcard wears another row, and is made of it| Part | Meaning |
|---|---|
namespace | The palette namespace the table answers for |
platform | A platform it applies to, repeatable. None means every platform, and every table applies to a container with no platform |
set <name> <key> <rrggbb> | Colors a {name} placeholder runs over |
forms <name> <suffix>... | A {name} placeholder whose capture is looked up as a block of this table with each suffix in turn (- is none) |
| base | A color, - (none), @id (another row), @{name} (the placeholder’s color or block) or @own (the entry’s own color property, which is how a Teardown entry keeps its paint) |
| drawn as | opaque (the default), cutout, translucent or empty |
solid, passable | Whether it collides. A row is solid unless it is empty or says passable, and a row built on another keeps that one’s solidity |
surface=preset | The surface preset it is made of: what footsteps, grip, impacts and scrapes read where a body touches it. A row built on another is made of what that one is, and a row that names none is stone. A name that is not a preset fails the table, so a typo cannot ship |
alpha=hh, shade=f, else=rrggbb | The alpha and a brightness factor of a color the table names, and the color of an @own entry that carries none |
inner | A see-through block that keeps the faces between two cells of its own kind, which a see-through block otherwise hides: Minecraft’s fancy leaves, whose canopy has depth from inside and from a distance |
leaves | A block client.voxel.opaqueLeaves draws solid |
Exact ids win, and patterns are tried in file order. A row built on another (@id, @{name}) keeps that one’s flags. Loading a table from a pallet or a platform file later is the same parser over that file’s text, put in front of the built-in tables so its rows win.
A see-through cell hides only cells of its own kind: glass beside glass and water beside water draw no face between them, so a lake has a surface and no inner walls. An opaque or unresolved cell hides everything behind it. Leaves are cutout so they do not hide their neighbors; with a flat color they still read as solid blocks, and with the pallet’s texture the gaps show through.
Leaves
Section titled “Leaves”Minecraft draws leaves two ways, and so does DH. client.voxel.opaqueLeaves picks one; it is on by default.
| On: Minecraft’s Fast leaves | Off: Minecraft’s Fancy leaves | |
|---|---|---|
| Drawn as | Opaque. The texture’s holes are filled with three quarters of its darkest texel, tinted, as the game fills them | Cut out, the holes see-through |
| Faces between two leaf blocks | Hidden, and a leaf block hides what it touches, like any opaque block | Kept (the row’s inner), so the inside of a canopy is drawn |
| At a distance | Solid, so nothing dissolves | The mips darken toward the holes (the texture’s .mcmeta says dark_cutout) and keep the cut-out coverage of the top level |
It is a drawing choice only: collision reads its own looks, so leaves are solid either way. Changing it opens every volume again with the new looks and meshes it again, live; each mode’s resolved palette is kept, so switching back resolves nothing. Fancy leaves cost about a fifth more triangles where the camera stands in a forest, and they fill client.voxel.meshBudgetMb sooner: see what each mode costs.
Volumes stream: a region whose bounds come within client.voxel.streamRadius meters of the camera is loaded and meshed, and one past that radius plus client.voxel.unloadMargin is unloaded, so a whole world never sits in VRAM. Two ceilings hold however dense the world is. client.voxel.maxMeshes caps the meshes meshing or resident, a region counting as one; a region that draws nothing (air, or stone buried on every side) frees its slot. client.voxel.meshBudgetMb caps the bytes of vertex and index buffers every volume holds together: spent nearest first, so once it runs out the farthest regions are dropped and nothing farther streams in. The view gets shorter instead of memory growing, and the engine logs one warning naming the preference the first time it happens on a map.
The meshing threads share one queue that hands out the most urgent region as the camera stands when a thread asks, not when the region was queued: the nearest first, and one behind the camera weighed up to client.voxel.behindWeight times farther than one the same distance ahead. So the ground underfoot lands first, then what the camera faces, spreading outward ring by ring. A region unloaded while it waited is dropped unmeshed, so a fast flight never spends the threads on ground already left behind.
A frame’s finished meshes go to the GPU in one batch, ahead of the frame that draws them, and nothing waits on the copies: the device is never idled for a voxel upload. A mesh that streamed out is freed once the frames that may still have drawn it have finished.
An offscreen capture, a map thumbnail included, streams around its camera within client.voxel.captureRadius meters (384 by default, since an overview camera stands far above a world and the live radius would leave most of its frame sky) under both ceilings, and lands everything in range before it shoots. It skips only the per-frame upload budget. A capture that names no camera streams outward from the volume’s origin, still under both ceilings. --gpuTimings streams a map’s volumes the live client’s way instead and times it.
| Preference | Default | What it does |
|---|---|---|
client.voxel.streamRadius | 128 | Meters from the camera chunks load within |
client.voxel.unloadMargin | 32 | Meters past the radius a loaded chunk is kept |
client.voxel.workers | 2 | Threads that decode and mesh chunks, read when the first volume streams in |
client.voxel.regionChunks | 2 | Chunks along each side of a draw region, read when a volume opens. 1 draws every chunk on its own |
client.voxel.behindWeight | 3 | How much later a region behind the camera meshes than one the same distance ahead. 1 orders by distance alone |
client.voxel.uploadBudgetMs | 2 | Milliseconds a frame may spend uploading meshes |
client.voxel.maxMeshes | 1500 | Most meshes (one per region) resident or meshing across every volume. Each is two GPU buffers; a region that draws nothing does not count |
client.voxel.captureRadius | 384 | Meters from its camera an offscreen capture streams within, in place of the stream radius |
client.voxel.meshBudgetMb | 512 | Megabytes of mesh buffers every volume together may hold before the farthest chunks are dropped |
client.voxel.cellCacheMb | 64 | Decoded cells each volume keeps for meshing neighbors |
client.voxel.idleFrames | 120 | Frames an undrawn volume stays loaded |
client.voxel.roughness | 0.9 | Roughness of the built-in colors, read when a volume opens |
client.voxel.opaqueLeaves | true | Draws leaves solid (Minecraft’s Fast) rather than cut out with their inner faces (Fancy). Applies live |
client.voxel.emissiveIntensity | 2.5 | How bright a block that gives off light glows, read when a volume opens |
client.voxel.emissiveFloor | 0.75 | The least a block that gives off light glows by, 0 to 1, after the light-level curve: max(curve(level), floor). Lifts a level-7 redstone torch (about 0.33 on the curve) to a clear glow and leaves level 15 alone. Applies live |
client.voxel.translucentCoverage | 1.2 | The gain on a blended face’s coverage, read when a volume opens |
A payload is read by range from the compiled pallet: its header and region index when it opens, a region’s chunk table the first time a chunk in it is needed, and each chunk’s own record when it is meshed.
Not yet built: merging drawn faces into larger quads (collision merges its own; the drawn mesh still draws one quad per face) and block edits from the editor (the engine can re-mesh a chunk and its border neighbor when a cell changes, but nothing changes cells yet).
Collision
Section titled “Collision”A placed volume is solid. Every chunk near a player becomes one static triangle-mesh body, through the same cook-then-attach path a map’s own mesh collision takes, on the server and in the client’s prediction alike.
- The faces are the renderer’s. Collision runs the drawing’s own face pass over each block’s solidity instead of its look: a solid block is a closed cube to collision unless the platform pallet gives it a shape, so a face shows between a solid cell and a passable one and nowhere else, across chunk borders included. The cube faces are then merged greedily across blocks made of the same surface, since collision does not care what a surface shows, so a flat stone floor is one quad per chunk rather than one per block, and a grass field meeting a plank deck splits along the seam.
- The pose is the renderer’s. A chunk’s cells reach the world through the same composition the drawing uses: the grid transforms, the origin,
voxelSizeandaxes, then the volume’s pose with the editor’s edit laid over it. A mirroring pose turns the triangles the right way out again. - Edges. Each chunk is cooked with Box3D’s edge identification, so a body sliding across the seams between merged quads does not catch on them. A chunk border has no face on it at all, so a player walks across one as across open floor.
- Hidden grids (another dimension, another structure palette) are not solid, unless the placement’s
deltashows them. - The editor. Moving, turning or scaling a volume cooks its chunks again where it now stands, since a static body cannot move. Turning it off or deleting it removes its bodies.
What is solid
Section titled “What is solid”| Platform | Solid | Passable |
|---|---|---|
| Minecraft | Everything drawn opaque, plus glass, ice, slime, honey, leaves and barriers (which are not drawn). An unknown or unresolved block is solid | Air, cave air, void air, structure void, light, water, lava, bubble columns, powder snow, plants, flowers, crops, saplings, grass, ferns, vines, coral, torches, rails, redstone wire, buttons, pressure plates, signs and banners |
MagicaVoxel .vox | Every color, see-through ones included | A color at zero alpha, which is not there at all |
Teardown .vox | Every material, painted or not, unknown ones included | unphysical |
| Anything else | Everything: an unresolved block is solid | Nothing |
Block shapes
Section titled “Block shapes”Where the workspace has the volume’s platform pallet, a solid block collides as its shape: a slab is half a block, a stair has its two steps, a fence post stands a block and a half. The shape is the block’s collision, and the model’s elements where it states none, turned by the same rules that turn the drawn model.
- Solid or not is still the table’s. The block table decides whether a block is solid; the pallet only says what shape a solid block has, or that it is not solid after all (an empty
collision). The Minecraft importer writes an emptycollisionon every block the table calls passable, so the pallet read alone agrees with the table. - One pass. A shaped block’s boxes go through the same face pass as the drawn models and the cubes, each box face culled against the side of the block it touches. A block whose shape is a whole block (stone, planks, a barrier, which draws nothing) stays on the cube fast path, merged with its neighbors.
- Per triangle. Every box face carries its block’s surface, as a cube face does.
- Without the pallet, every solid block is a whole cube, as before.
The server’s pallet rules. A platform pallet is generated from each machine’s own install and never sent, so the server and a client can hold different ones. The server names the platform pallets its collision reads, by a fingerprint of their block definitions, beside the map’s dependencies; nothing of the pallet travels. A client whose fingerprint differs says so loudly in its log, naming the pallet:
PLATFORM PALLET MISMATCH: this machine's 'com.mojang.minecraft' pallet (26.1) (blocks 3f2a...) differs from the server's (blocks 9c41...): block shapes may predict differently here, and the server's win. Generate both from the same Minecraft versionIts prediction uses its own pallet anyway, and the server’s movement corrects it where they disagree. When the server has no pallet, its blocks are whole cubes, so the client sets its own aside and predicts cubes too. An older server that names nothing leaves the client on its own pallet.
Surfaces
Section titled “Surfaces”A block is made of a surface preset, named by the surface= column of its row, so a voxel block reads the way map geometry whose material names that preset reads: its grip, its footsteps, the landing thud and how hard a fall onto it hurts, and the sound of running into it. Ice is as slippery as an ice material (0.12 grip), and grass is as soft to land on as grass. Scrapes and bullet impacts do not choose by surface on any geometry yet; when they do, voxel blocks need nothing more.
Each collision triangle carries its block’s preset as a slot in the scene’s one surface table, the same per-triangle material a map’s mesh collision carries. The map’s own material slots come first and the presets come after them, in the same order on the server and in the client’s prediction, so a contact resolves to the same surface on both ends. Faces of different surfaces are never merged into one quad, so the seam between a grass field and a plank floor is exact to the block.
| Platform | Made of |
|---|---|
| Minecraft | grass: grass blocks, mycelium, moss, hay, leaves of every tree, wart blocks, shroomlight, cactus, and every plant, flower and crop. dirt: dirt, coarse dirt, rooted dirt, podzol, mud, clay and soul soil. wood: every log, stem, stripped log, wood, hyphae and plank of every wood type, bamboo blocks, and every stair, slab, fence, gate, door, trapdoor, button, plate and sign built on them, plus pumpkins, melons, bookshelves, crafting tables, chests, note blocks, jukeboxes and torches. sand: sand, red sand, soul sand and concrete powder. gravel: gravel. metal: iron, copper, gold, netherite, diamond, emerald and redstone blocks, iron doors and trapdoors, rails and lanterns. glass: glass, tinted glass, stained glass and panes, glowstone and sea lanterns. ice: ice, packed ice and blue ice. snow: snow, snow blocks and powder snow. fabric: wool, carpets, beds, banners, sponge, cobwebs and TNT. ceramic: terracotta and glazed terracotta. plastic: shulker boxes, candles and honey. flesh: slime. stone: every stone, cobblestone, deepslate, brick, ore, sandstone, nether and end stone, concrete, obsidian, quartz, prismarine, bedrock and anything no row names |
| Teardown | glass glass; grass grass; dirt dirt; wood wood; weak_metal, heavy_metal, hard_metal metal; plastic plastic; rock, concrete, brick, plaster, hard_masonry and any other material stone |
MagicaVoxel .vox | stone. A color carries no surface; a .vox material (MATL) is kept on the entry but not read yet |
| Anything else | stone |
There is no mud or leaves preset, so mud is dirt and leaves are grass; every rock and every concrete block is stone.
Streaming, and never falling through
Section titled “Streaming, and never falling through”The server keeps chunks solid around every player, and the client’s prediction around its own player. A chunk within the radius is queued and cooked on a worker thread, then attached on the simulation thread before a tick steps; one past the radius plus the margin loses its body.
A chunk within the urgent radius of a player is never waited for: it is cooked on the spot, before that tick moves anyone, so a player cannot fall through a floor that is still in the queue. Spawning somewhere new costs one tick a few chunk cooks (about 9 ms for four chunks of terrain), instead of a fall.
| Preference | Default | What it does |
|---|---|---|
world.voxel.collisionRadius | 48 | Meters from each player the server keeps chunks solid within |
world.voxel.collisionMargin | 16 | Meters past the radius a chunk keeps its body |
world.voxel.collisionUrgent | 4 | Meters from a player a chunk is cooked on the spot instead of queued |
world.voxel.collisionWorkers | 1 | Threads that cook the server’s chunks |
world.voxel.collisionCellCacheMb | 16 | Decoded cells each volume keeps for the server’s cooks |
client.voxel.collisionRadius | 32 | Meters from the local player the prediction keeps chunks solid within |
client.voxel.collisionMargin | 16 | Meters past that radius a predicted chunk keeps its body |
client.voxel.collisionUrgent | 4 | Meters from the local player a predicted chunk is cooked on the spot |
client.voxel.collisionWorkers | 1 | Threads that cook the prediction’s chunks |
client.voxel.collisionCellCacheMb | 16 | Decoded cells each volume keeps for the prediction’s cooks |
The two radii may differ. The chunks near a player are the same on both ends, so the prediction never stands on anything the server does not.
What it costs
Section titled “What it costs”Collision is cooked when it is needed, not at compile time. Measured on 256 by 96 by 256 cells of rolling terrain with water and trees, at Minecraft’s 16-cell chunks, around one player at the default radius:
| Bodies alive | 109 chunks |
| Triangles | about 14,500 (133 a chunk) |
| Cook | about 1 ms a chunk: the face pass, then Box3D’s weld and tree |
| Cooked memory | 0.6 MiB, about 6 KiB a chunk (45 bytes a triangle) |
| First tick at a new place | about 9 ms: the urgent chunks under the player |
A compile-time cook, like a map’s .dh-colcook, would trade the load-time cook for pallet size and is not built.
Seeing it
Section titled “Seeing it”client.debug.colliderGizmo draws each chunk’s body as a wireframe, the same way it draws a map’s mesh collision, labeled with the volume’s name. client.debug.contacts names a voxel contact by its volume, its grid, its chunk, and the cell, block and surface the player touched:
player mover contact ENTER — body #12: voxel volume 'village' | grid 0 'minecraft:overworld' chunk (1, 4, 0) | 182 triangles | cell (20, 70, 5) minecraft:grass_block | surface grass | contact ...Collision support
Section titled “Collision support”| Minecraft | MagicaVoxel .vox | Teardown .vox | |
|---|---|---|---|
| Engine server | ✅ | ✅ | ✅ |
| Engine client prediction | ✅ | ✅ | ✅ |
| Level editor (move, turn, scale, off, delete) | ✅ | ✅ | ✅ |
| Block shapes (slabs, stairs, fences as their real shapes) | ✅ From the workspace’s Minecraft pallet; whole cubes without it. A pallet that differs from the server’s is warned about, and the server’s has the last word | ✅ Cubes are the shape | ✅ Cubes are the shape |
| Walking up a stair | ✅ A stair’s step and a slab are half a block, which the default world.stepHeight (0.5 m) climbs at full speed, up and down | ✅ A whole block is a ledge, not a step | ✅ A whole block is a ledge, not a step |
| Per-block surface (grip, footsteps, landings) | ✅ From the block table | 🟡 Stone for every color | ✅ From the material |
| Schedule I | ❔ Streamed around the player, not yet checked in a game; block shapes from the workspace’s Minecraft pallet | ❔ | ❔ |
| BONELAB, Garry’s Mod and the other game platforms | ❔ | ❔ | ❔ |
The payload
Section titled “The payload”A .dh-vox.bin payload is little-endian throughout. A string is a u16 byte length and UTF-8.
| Section | Contents |
|---|---|
| Header | DHVX, u16 version (2), u16 flags; platform, format, platformVersion; f64 voxel size; u8 axes (bits 0–1 the up axis, bit 2 left-handed); i32×3 origin; u8 chunk edge (8, 16 or 32; 32 by default, 16 for a Minecraft world); u8 region edge (32); then the meta: a u16 count of string pairs (dataVersion, dropped, …) |
| Global palette | u32 count, then per entry ns, id, u16 property count, each property a key, a u8 kind (bool, int, string) and its value, then an optional raw blob: a format tag and the source’s own bytes. Entry 0 is always dh:empty |
| Layers | u16 count, then per layer a name and a u8 cell scale. blocks is always first, at scale 1. Other layers (fluid, biome, decor, small numeric channels) are paletted grids on the same chunks; a layer at scale s has cells of s×s×s blocks, so its chunk covers the same space with (edge / s)³ cells |
| Grids | u32 count, then per grid a name, a parent index (-1 for a root; parents come first), a 3×4 row-major f32 transform into the parent’s cells, i32×3 bounds min and size, and string attributes (hidden marks a grid a viewer leaves out by default) |
| Extras | u32 count, then per extra a kind (source, blockEntity, entity), an i32 grid (-1 for the container), an optional i32×3 cell, and an opaque blob: a format tag and bytes. Per-cell extras are postponed, so nothing here is interpreted |
| Region index | u32 count, then per region the grid, the region coordinates on the two horizontal axes, the u16 shard, and a u64 offset and length |
| Regions | Each region holds a chunk table (u16 layer, i32×3 chunk, u32 offset and length), then the chunk records |
A region spans 32 by 32 chunks across the two axes that are not up, and the whole height. Shard 0 is the payload itself, and shard n is the sibling entry <payload>.<n>, whose region offsets count from its own first byte. The compiler starts a new shard once one holds 256 MiB of regions, so a world larger than a pallet entry’s 1 GiB cap splits across entries. A .vox only ever produces one region per grid.
A chunk record is a codec tag byte (0 stored, 1 LZ4), the body’s raw length, then the body. The tag means a codec such as zstd can be added without breaking files. A body is a mode byte, a local palette of global indices, and the cells, in one of three modes:
- Uniform. One local entry, no index data.
- Packed. One index per cell at 1, 2, 4, 8 or 16 bits, so a value never spans two 64-bit words. Cells run X fastest, then Y, then Z.
- Sparse. A list of (cell, local index) pairs over local entry 0.
A chunk that is entirely dh:empty is not stored.
The payload entry is stored, not recompressed, in the compiled pallet. It is already LZ4 per chunk, so a chunk is a plain byte range of the entry.
How a block is drawn
Section titled “How a block is drawn”A palette entry names a block, such as minecraft:oak_stairs with its properties. It does not carry a color. A renderer resolves its appearance in this order:
- The platform’s pallet. The workspace pallet whose ID equals
platform. Itsdh.blockatblocks/<id>.dh-block, with itsinheritschain folded, supplies the models and materials, and its rule table picks them from the entry’s properties. For Minecraft that pallet iscom.mojang.minecraft, whichdh import minecraftgenerates on your machine from your own client jar. DH never ships it, because Mojang’s assets are not DH’s to redistribute. The pallet answers for the platform’s own namespace (minecraft); anamespacestable mapping a mod’s namespace to its own pallet is not read yet. - A small built-in table per platform, shipped with DH, mapping a block ID to a color DH chose itself. The colors are never sampled from a game’s textures. A block the pallet does not name, and every block when the workspace has no such pallet, is a cube of this color.
- A visible “unresolved” pattern for anything else, so a guess is never mistaken for an authored color.
A neutral .vox (MagicaVoxel) is drawn from its own palette instead: there the color is the data. A Teardown palette index is a material and resolves through the same three steps; Teardown has no platform pallet.
Models in the chunk mesh
Section titled “Models in the chunk mesh”The volume resolves its palette once, on a worker, when it opens, and the renderer keeps it: the same container opened again (after going idle, placed twice to show different grids, or the same map loaded again) reuses it. A palette is kept only while every pallet file it could have read is unchanged, by size and write time, so a rebuilt pallet resolves afresh, and a map swap keeps only the palettes the map just left used. Meshing never resolves anything; each cell reads its palette entry’s baked faces. The engine’s log says how many entries the pallet drew, how many atlas tiles they took, how long the resolve took, and how many pallets it opened, and in how long, to find the platform’s. Each entry’s state picks its rules: the first match, or every match for a multipart block. Each choice is baked to faces in the block’s own space with its element rotation (and rescale), its x, y and z turn, uvlock, and each face’s uv and rotation. A weighted choice, such as a grass block’s four turns, is picked by the cell’s own position, so the same cell always shows the same choice.
A modeled block is meshed by the same face pass as a colored cube, into the same chunk mesh. A face with a cullFace is hidden only when the block on that side covers that whole side with an opaque face: a full cube hides what touches it, and so does the bottom of a bottom slab, but its open top does not, so the block above a slab still shows its underside. A see-through block hides faces of its own kind, as a cube does. Which sides a block covers is read off its baked faces, so a turned stair reports the sides it turned to.
Textures, transparency and tint
Section titled “Textures, transparency and tint”Every texture a volume’s blocks draw is packed into one atlas per volume, below the palette colors the built-in cubes use, so a chunk stays one draw per surface (opaque, cutout, translucent) however many block types it holds. An atlas fits this renderer because the voxel material is the scene’s ordinary one: a texture array would need a shader of its own. A block’s material decides its surface: mask is cutout (leaves, plants, glass panes), blend translucent (water, stained glass, ice), anything else opaque. A face on a material that does not resolve, or an open slot, shows the unresolved checker.
Blended faces write no depth, so the order they are drawn in is the order they composite in. The scene sorts its blended draws back to front by their centers, one draw per surface of a region; inside a region’s mesh the blended faces are written lowest first, which is back to front for the view from above (a lake’s floor, what stands in the water, then the surface). Faces seen from below or level with them can composite in the wrong order; between faces of one water color that is hard to see, and per-face sorting is not done. The scene blends in linear light, where an alpha authored for a game that blends in gamma space lands lighter: Minecraft’s water washes a sand floor to a lavender gray. client.voxel.translucentCoverage (1.2) multiplies a blended face’s coverage, clamped, so a clear texel stays clear and the water over sand reads blue again; the textures keep their own alpha.
The atlas has a mip chain, so distant ground does not shimmer. Every tile is a power of two, is reduced on its own, never as part of the atlas, and is laid into each level with half a tile of its own edge texels around it, which is still a texel at the last level, a tile two texels wide. No level ever averages one block’s texture into the next block’s. A cutout tile keeps its alpha coverage down the chain (the same reduction the pallet compiler gives a mask texture), so leaves and plants do not thin away with distance. What a cutout’s fully clear texels hold decides its color as it shrinks, and the importer’s copied .mcmeta says which of Minecraft’s rules it follows: a texture marked dark_cutout (the leaves) has black holes, so it darkens toward them at a distance as the game’s canopy does; any other cutout spreads each hole’s nearest opaque color into it, so a torch or a flower keeps its color instead of growing a dark rim. The sampler is point within a level and linear between levels, Minecraft’s own block sampling: crisp texels up close, no hard line where one level hands over to the next. The palette colors need no reduction: a built-in cube samples one texel at a fixed UV and always reads the top level. client.display.mipmaps turns the chain off with every other texture’s.
An animated texture plays. Its .mcmeta beside the PNG gives the frame size (square on the image’s shorter side unless it states width or height), the frametime in game ticks (twenty a second), an optional frames order, each frame a number or an { "index", "time" }, and interpolate, which blends each frame’s color toward the next across its time and keeps its own alpha, as the game does. Time is the world clock the water’s waves read, so a capture’s --worldSeconds picks the frame. Each animated tile has a fixed block in a buffer on the GPU; when a tile would look different (a new frame, or a new tick of a blended one) it is composed, reduced and bordered on the CPU and written into its block inline in the frame’s command buffer, then copied over its cells at every level. Only the tiles that changed move, at most twenty times a second each, and nothing else of the atlas is touched.
A tinted face takes one atlas tile per color it is drawn in. A colormap tint is sampled by the cell’s biome: the container’s biome layer names it, and DH’s built-in biome table (minecraft.biomes, beside the block tables) gives its temperature, downfall and the colors it states outright (a swamp’s grass, a badlands’ foliage). A cell with no biome, and a container with no biome layer, reads as plains. A water tint is the biome’s water color. A fixed color is that color, and redstone wire and stems take theirs from their power and age.
A rebuilt platform pallet is read again by the next map load.
A block that gives off light (its dh.block’s lightEmission, by state, or an element’s own lightEmission) glows: its faces are drawn at least as bright as that light, whatever lights them, as Minecraft draws a torch in a dark room or lava in shadow. The level, 0 to 15, sets how bright, through Minecraft’s own lightmap curve at its default brightness: 15 is the texture itself, 14 nine tenths of it, 7 about a third, 1 almost nothing. So a lit furnace’s front glows, a glow lichen glimmers, and a brewing stand reads as an ordinary block in daylight.
A glowing face is drawn with an atlas tile of its own, so a lit furnace’s side glows where the cold one’s, the very same texture, does not. The atlas has a second texture of glow levels, laid out exactly like it: each glowing tile’s texels times its level’s brightness, and black everywhere else. Every surface of a volume that has any glowing tile glows by it, at the same UV its color is read at, times client.voxel.emissiveIntensity, added over its lighting; so a torch shares its draw with the cut-out plants around it, its flame cut out like theirs. At the default 2.5 a level-15 block’s brightest texels pass the bloom threshold, so fire and lava bloom and read as lit in shadow. A glowing tile that animates (lava, fire) animates in the glow levels too. A material with an emissiveColor glows as a level-15 block would.
The curve is floored by client.voxel.emissiveFloor (default 0.75): a glowing tile glows by max(curve(level), floor), so a dim torch stays readable in shadow while level 15 stays the texture itself. The floor is part of the glow levels, so a change opens every volume again.
A volume’s resolved look keeps each entry’s level (VoxelLook.Light) and how much it cuts light passing through it (VoxelLook.Opacity), the two numbers the light field spreads.
The light field
Section titled “The light field”A volume lit in a mode other than dh (see lighting modes) keeps block light and sky light per cell, computed while it streams and never saved, as Minecraft does in every session. In hybrid the scene’s shading reads it (below); it is also what --debugView voxelLight shows and what any code may ask (VoxelVolumeStreamer.LightAt). The design and its next steps are in design-notes/voxel-lighting.md.
- The rules are Minecraft’s. Block light loses 1 a step, a taxicab diamond around each emitter: a torch (14) gives 13 beside it and 12 diagonally. Sky light at 15 falls straight down through cells of opacity 0 with no loss, and loses 1 a step sideways and up. A step into a cell loses
max(1, opacity), so leaves and water (opacity 1) cut only sky light, and opacity 15 stops everything. A block withlightShapestops light across the sides its model covers: a bottom slab passes light up and sideways but not down. Day and night will be a multiplier when shading reads the field; the stored levels never change with the clock. - The cell is 16 bits,
SSSS RRRR GGGG BBBB: sky, then block light’s red, green and blue, mixed by the larger of each channel. Every emitter is seeded gray today (all three channels at its level), so the brightest channel is exactly the level a monochrome engine computes; per-block light colors come next. - Where it lives. Bricks of 16×16×16 cells in each grid’s light space (its cells with the up axis turned to Y), 8 KiB each, and a brick whose cells all hold one value (open sky, buried stone) stores that value alone. Each cell column keeps its lowest cell sky reaches unhindered.
- How it is computed. Starlight’s propagation, on increase and decrease queues of packed integers. A column of bricks is lit whole when the regions around it stream in (sky from each cell column’s sources, then every emitter, then what lit neighbors already hold across the border), on the meshing threads, one job per volume at a time, so the field has one writer and nothing locks. Light is kept for every column a resident region stands in and the ring around them, since light reaches 15 cells. A changed cell (
VoxelVolumeStreamer.SetCell) is relit incrementally: what it lit is taken back, and what is brighter around it fills in again. - On the GPU it is one storage buffer: a window of bricks around the camera for each lit grid drawn, written inline at the head of each frame, so a relight is a brick write and never a remesh. The shader reads a fragment’s light through Minecraft’s smooth corner rule: the cell in front of the face and its edge and corner neighbors averaged per corner (a diagonal behind two dark edges left out, so light does not leak around corners), then blended across the face.
On world-older, one region around the spawn (1,024 columns, Release, one thread): the field matches the light the game itself saved in 99.98% of the cells any block light reaches and 99.99% of the cells with sky light between 1 and 14, and every stored cell agrees in 100.00% to two places. Lighting the spawn radius (306 columns) cold takes 0.68 s, about 2.2 ms a column with its cells decoded; the field holds about 46 KiB a column. Placing a torch relights in 2.5 ms and removing it in 0.5 ms.
| Preference | Default | What it does |
|---|---|---|
client.voxel.lightBudgetMb | 96 | Megabytes of light each volume may hold, on the CPU and again on the GPU, before no farther column is lit. 0 computes none. Turning it on or off opens every volume again |
client.voxel.lightSliceMs | 4 | How long one light job lights new columns before it hands its thread back |
client.voxel.lightUploadKb | 1024 | Kilobytes of light one frame may write to the GPU, 8 KiB a brick. An offscreen capture writes everything |
Lighting modes
Section titled “Lighting modes”How a volume is lit is set per volume: the dh.voxelVolume placement’s lighting, else the dh.voxel definition’s lighting, else the platform’s default. The mode is a fact of the scene, which lights exist, so it is not part of a look.
| Mode | Lights | Light field | Default for |
|---|---|---|---|
dh | DH’s own: the sun and its shadows, the ambient, the map’s GI volume, lightmaps and analytic lights | Not computed | MagicaVoxel and Teardown containers, and any platform DH does not know |
hybrid | DH’s sun and shadows, the volume’s sky light gating the ambient and the far sun, its block light as a fill, and each face’s corner occlusion (below) | Computed | Minecraft containers |
minecraft | Minecraft’s lightmap model, with no sun term on the volume’s faces. Not built yet: a volume asking for it is shaded as hybrid, and its log line says so | Computed | Nothing yet; the Minecraft template map will adopt it |
An unknown mode is a compile error naming the three. A placement that changes its mode opens the volume again, since the mode decides whether a field exists.
{ "$type": "object", "id": "6a0c27e1-0000-4000-8000-000000000010", "name": "world", "components": [ { "$type": "dh.voxelVolume", "volume": "/worlds/survival.dh-vox", "lighting": "dh" } ] }Hybrid shading
Section titled “Hybrid shading”In hybrid the volume keeps DH’s sun and DH’s look, and Minecraft’s light decides what the sky and the torches reach. Every fragment inside a lit volume’s window reads the field, a voxel face through the corner rule above, and so does anything else standing in the volume (a prop, an avatar) at its own position:
- The sun keeps the shadow cascades, PBR and half-Lambert. Past the last cascade, where no shadow map reaches, the sun is scaled by the sky light, so a distant cave mouth is not sunlit underneath.
- The ambient (the sky’s harmonics or the hemisphere) is multiplied by the sky level through the curve. A cave with no opening goes dark, near black as in Minecraft, and an overhang darkens with depth.
- Block light fills, with no direction:
albedo × tint × curve(level) × client.voxel.blockLightIntensity, per channel. The tint multiplies the curve before it is linearized, as a glowing tile’s texels do, so a level reads as bright as Minecraft draws it. - The map’s GI volume stands down inside the field: its probes never saw the voxels, so the sky-gated ambient replaces its radiance, and its highlight fades with it.
- Glowing faces keep their emission. A glowing block’s own cell holds its level, so its faces are lit by it too.
- Corner occlusion. The mesher measures each face corner as vanilla smooth lighting does: how many of the cells beside it in the layer in front hide it, the two along its edges and the diagonal, with two hiding edges counting as fully hidden. Each hiding cell takes
client.voxel.occlusionStep(0.2, vanilla’s) off the ambient there, so a tucked corner keeps 0.4. A model face reads the same cells (a face inside its cell, a slab’s top, reads its own cell’s layer) and a corner inside the face blends the four cell corners; a face at an angle, a torch’s cross, is not measured. Each quad is split along the diagonal whose corners are lighter, so one dark corner shades its own triangle. The count rides UV 2, and measuring it adds about a tenth to a chunk’s extraction. - Screen-space occlusion (
client.render.ao, off by default) measures the same thing again and draws halos along every block edge, so a lit volume’s faces skip it unlessclient.voxel.screenOcclusionis on. Everything else in the volume keeps it. - At the window’s edge the field fades out over
client.voxel.lightEdgeFadecells, back to DH’s own lighting, so the end of the light the GPU holds is a ramp.
The light curve has one copy, VoxelLightCurve in Content: v / (4 - 3v) lifted toward 1 - (1 - x)^4 by the brightness slider. The glow levels are drawn with it, and the GPU field’s header carries a table filled from it each frame (the sky gate per level, and the fill’s color per level), so no shader carries the formula.
| Preference | Default | What it does |
|---|---|---|
client.voxel.lightBrightness | 0.5 | Minecraft’s brightness slider, 0 (Moody) to 1 (Bright): lifts the dim levels of both the sky gate and the fill |
client.voxel.blockLightTint | [255, 214, 170, 255] | The fill’s color, sRGB: Minecraft’s warm torchlight |
client.voxel.blockLightIntensity | 1 | The gain on the fill |
client.voxel.occlusionStep | 0.2 | The ambient a face corner loses for each cell hiding it, 0 to 1/3. 0 turns corner occlusion off |
client.voxel.screenOcclusion | false | Whether lit voxel faces also take the screen-space occlusion |
client.voxel.lightEdgeFade | 8 | Cells the field fades out over at the edge of the window the GPU holds |
All of them apply on the next frame. Brightness and the curve move to the map’s dh.look, and the tints to the volume, when minecraft mode lands.
Measured on world-older with --profile --gpuTimings (Release host, RTX 4080 SUPER, 1920x1080, two runs each), dh against hybrid: the GPU frame is 1.57–1.59 ms against 1.56–1.58 ms from the spawn overview and 1.61–1.67 ms against 1.62–1.63 ms in the torch-lit cave, the opaque pass unchanged within noise. On the CPU, hybrid lights 740 to 900 columns in about 1.25 s of worker time beside meshing, and an offscreen capture’s full radius lands about 0.4 s later because it waits for the light; the first ground lands as soon as in dh.
What the look costs
Section titled “What the look costs”Measured on world-older with --gpuTimings (Debug host, RTX 4080 SUPER, 1600x900), from the map’s spawn, 275 regions resident, two runs each:
| Before this lane (cut-out leaves, faces between two leaf blocks hidden, nothing glowing) | opaqueLeaves on | opaqueLeaves off | |
|---|---|---|---|
| Voxel triangles | 1.91 M | 1.89 M | 2.26 M (+18%) |
| Scene items / draws | 691 / 799 | 659 / 767 | 660 / 768 |
| GPU ms (shadow ms) | 2.23–2.25 (0.51–0.80) | 2.02–2.18 (0.57–0.65) | 3.23–3.28 (1.36–1.40) |
| Still frame, wall ms | 9.3–9.6 | 9.05 | 10.6 |
| Flying at 20 m/s, p50 / p95 ms | 12.5–12.8 / 17.5–18.1 | 11.9–12.1 / 16.8–17.1 | 13.2–13.3 / 20.8–22.6 |
Glowing costs nothing measurable: a glowing face sits in its own surface’s draw, so opaque leaves draw fewer items than before (a region’s leaves join its opaque draw). Fancy leaves cost about a millisecond more GPU time a frame from the spawn, most of it in their shadows, which cast through every layer of a canopy.
From the birch forest camera, whose view reaches client.voxel.captureRadius, every mode fills client.voxel.meshBudgetMb: opaque leaves fit 679 regions in it (4.62 M triangles), fancy leaves only 411 for the same triangles, so with fancy leaves a forest’s view ends sooner.
Fluids
Section titled “Fluids”A block whose dh.block has a fluid is drawn from its level, as Minecraft draws water and lava, instead of from its model. Its level is its state’s level: 0 is a source, 1 to 7 flow farther from one, and 8 and up fall. A source and a falling column stand 8/9 of a block; a flow at level n stands (8 − n)/9.
- Surface. Each corner of the surface is the weighted mean of the four columns that share it: a column holding the same fluid counts its own height, ten times over once it is 0.8 or more, so a lake’s edge stays level; an open column counts 0, and a solid one drops out. A column under the same fluid is full, and so is a corner beside one, so a waterfall meets the pool under it without a step.
- Textures. A level surface wears the fluid’s still texture. One with a flow wears the flowing texture’s middle, turned so its stream runs downhill: the flow points away from each neighbor of the same fluid that stands higher, toward each that stands lower, and over an edge it would fall from. The sides wear the flowing texture’s upper quarter, as tall as the fluid. Both animate, and water takes its biome’s water tint.
- Faces. A face shows where the fluid meets something that does not hide it. None is drawn between two cells of the same fluid, against a side a block covers, or against the cell’s own block where it covers that side (the water in a waterlogged top slab has no surface under the slab). Glass, ice and other blended blocks give their own face up to a source or a falling column beside them, so the boundary is one surface, the water’s, and water seen through an aquarium’s glass is still water. Two fluids keep their own faces, and water and lava never average their heights together. The surface and the sides are drawn from both sides, so the surface shows from under water.
- Waterlogged blocks. A block whose state sets the fluid’s
loggedproperty (waterlogged=true) draws its model and a source of water in its cell, as does a block the fluid names a holder (kelp, seagrass, a bubble column, which draws only its water). The platform’s fluids are read before the palette, so a waterlogged stair holds water even in a structure with no water block. - Lava has no tint and gives off full light, so it glows, still and flowing alike.
A fluid’s corners read the cells diagonal to it, so a cell’s fluid reads all 26 cells around it, across chunk borders. Fluids are passable, as the block table says. Without a platform pallet, water is a translucent cube of the table’s color, and nothing swims in it.
Swimming
Section titled “Swimming”A fluid whose block names a medium is water to everything that asks the world about water, through the same seam a map’s water bodies answer: a pawn swims in it, a crate floats in it, a shot splashes on it, and a camera under its surface sees its fog. Map water and voxel water behave alike because they are one query (WorldWater), and the server and the client’s prediction each ask it of their own copy of the same volumes, so prediction agrees.
- The lookup is the grid itself. No trigger shapes are made. A point is in a fluid when its cell holds one (a fluid block, or a block standing in a source of it, as a waterlogged stair does), or when the cell under it does, so a point just over a surface still reads the surface beneath it and is dry. The surface over the point is the one the mesher draws: the corner heights above, read across the cell, on top of the highest cell of the same fluid in its column. So a level-4 flow is (8 − 4)/9 of a block deep to a swimmer as it is to the eye, and a shallow flow can be waded. The floor is the lowest cell of the run. A cave under a lake is dry, since the query is by cell and not by column.
- In the volume’s own space. The point is carried into each drawn grid’s cells by the volume’s placed pose, so a turned or scaled volume answers in its own space and hands back world heights.
- What it is. The medium material’s
waterblock resolves as any water material does (presets are pointers): its density decides whether a swimmer sinks or floats, as it does for a map body, and itsviscositythickens the swim: the world’s swim drag is multiplied by it, and the stroke, the sprint, the vertical stroke and the buoyant drift are divided by it. Its clarity and haze are the fog an eye inside it sees, clipped to the volume’s box topped at the surface, so the sky through the surface is not fogged by the air over it. - What it reads. The query reads the cells the collision streamer opened for the volume (
VoxelFluidField), not its bodies: a fluid is water as soon as its volume is placed, wherever the player is. A sample costs about 2 µs; a pawn takes one a tick, a floating crate one per buoyancy sampler. - No illumination column. A map body lights what stands under it through its water; a voxel fluid does not, since its box is the whole volume.
Lava is swum the same way, through its own medium: Minecraft’s is thick, slow and fogged orange. It does no damage.
Platform support
Section titled “Platform support”.dh-vox.bin data | .vox as MagicaVoxel | .vox as Teardown | .schem | .litematic | .nbt | .schematic | Java world | |
|---|---|---|---|---|---|---|---|---|
Compiler (dh build) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 🟡 Common blocks mapped, the rest kept as numbers | 🟡 1.13 on; 1.13 to 1.17 without biomes |
| Engine, loading and rendering | 🟡 Built-in colors | ✅ | 🟡 Painted colors, glass translucent | ✅ Models and textures from the workspace’s Minecraft pallet, built-in colors without it | ✅ | ✅ | 🟡 Legacy ids unresolved | ✅ Streamed by distance |
| Engine, collision | ✅ | ✅ | ✅ | ✅ Block shapes from the workspace’s Minecraft pallet, cubes without it | ✅ | ✅ | 🟡 Legacy ids solid | 🟡 Streamed around players |
| Engine, swimming | ❌ No fluids | ❌ No fluids | ❌ No fluids | ✅ Water and lava, through the Minecraft pallet’s media | ✅ | ✅ | 🟡 Legacy ids are not fluids | ✅ |
Engine, light field and hybrid | 🟡 Minecraft containers by default | 🟡 dh by default; hybrid on request, lit by sky only | 🟡 dh by default; hybrid on request, lit by sky only | ✅ hybrid by default; minecraft is shaded as hybrid | ✅ | ✅ | 🟡 Legacy ids neither emit nor cut light | ✅ Streamed around the camera |
| Level editor, placement | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Studio | 🟡 Listed with its icon, no preview | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 |
| Schedule I, rendering | ❔ Streamed by distance through the engine’s streamer, not yet checked in a game | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ |
| BONELAB, Garry’s Mod and the other game platforms | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ |
Code-drawn Minecraft blocks (chests, shulker boxes, banners, skulls and heads, the decorated pot, the conduit) draw from the importer’s recipes; the deferred few draw placeholder cubes, and water and lava are drawn as fluids. In Studio, the payload is listed as one asset with its definition.