Drawing resolved models
Register standalone models during standalone_models, then resolve their ModelRef
through RenderAssets::model after resources are published. World render hooks receive
WorldDrawContext: submit transient copies with draw_model_instances, or retain
copies in a group. GUI scenes, mobs and kept/spinning renderers use their existing
CPU routes; groups are a world-drawing facility, not another renderer.
Terrain materials in a block entity
draw.model(model, tint) draws a resolved model as an entity model. A block entity, mob or
player submit hook that wants the terrain's own materials calls
draw.terrain_model(model, options) instead. That covers a machine's belts, shafts and
casings, cut out and shaded as blocks are:
let belt = TerrainUvShift::new("mymod:block/belt", "mymod:block/belt_scroll", [1.0, 0.5], [0.0, scroll]);
let options = TerrainModelOptions::default()
.with_material(TerrainMaterial::Cutout)
.with_uv_shift(belt);
draw.terrain_model(self.belt, options)?;
- Faces sample the animated block sprites, cull back faces and write colour and depth like opaque terrain. Light is the draw's own, raised by each quad's emission.
material:Modelkeeps each quad'srender_type(solid,cutout,cutout_mipped), and a translucent quad refuses the draw.Solid,Cutout(alpha under 0.1 discarded, no mipmaps) andCutoutMipped(under 0.5, mipmapped) choose one material for all quads.shade:Modelfollows each quad'sshadeflag,Offturns directional shading off, andEntityshades every quad by the level's entity light directions. Full-bright light never turns shading off.uv_shiftdraws faces of exactly one sprite with another, their UVs scaled then moved.uv_shrink_ratio(0.0..=1.0, default0.0) pulls UVs towards each face's centre first, for a model baked with an olderFaceBakery's atlas shrink. The engine never guesses it.tint(0xRRGGBB) multiplies only quads with a tint index.- A stale handle is
TerrainModelError::Model. If any quad can't be drawn, the result isTerrainModelError::Refusedand nothing is drawn. That covers a translucent quad underModel, a missing sprite, a pose that flattens the model, a ratio out of range, and a shift whose numbers aren't finite, whose sprites are unknown or whose source sprite no face uses.
Persistent groups
host.create_group(model, capacity)reserves up to 16,384 copies of a solid model. Initial drawn length is zero. Translucent models are refused.group.write(host, first, copies)atomically overwrites a range and extends drawn length if needed. Unwritten slots start as white, full-bright identity copies at world origin. Invalid coordinates or light, or a range beyond capacity, change nothing.group.set_len(host, len)changes drawn length without changing reserved capacity. Shrinking to zero draws nothing; growing restores retained copies, including hidden slots written earlier.group.set_group_transform(host, transform)sets an identity-by-default row-major 3×4 affine pose. For each local vertex the composition is exactlyworld = origin + group_transform * (copy_transform * local). The world-space f64 origin is not rotated or scaled. Non-finite pose values change nothing.draw.draw_instance_group(group)draws the live length once per view. Repeated submissions in that view are ignored. Culling uses the composed pose.group.destroy(host)retires the handle and releases capacity and CPU caches. Copies already accepted for a view remain drawn; subsequent calls returnStale.
Each mod may reserve 65,536 copies across all its groups; a zero-capacity group reserves one slot. Destroying a group releases that reservation immediately. A view accepts 16,384 visible copies shared across transient batches, groups and listeners, and at most 16,384 group submissions. Culled copies do not consume the visible-copy budget. Refusal is atomic: no prefix of a refused submission draws.
Groups cache converted records on the CPU. Unchanged groups avoid pose, normal and tint conversion, but still cull and stage visible copies each frame. Every visible GPU-routed copy uploads each frame; groups do not retain a private GPU buffer. Frequent whole-group pose changes rebuild every live record and can cost more than uncached transient copies. Shader-pack selection, missing prepared GPU records and overlays use the same CPU baking path as transient batches, including tint and light. There is no simulation-tick work.
Only the creating mod can access a group. A foreign handle returns GroupError::Stale,
as does a destroyed group, a group after resource reload or world close, or a group
whose mod was disabled. Re-enabling starts without groups: recreate them from the
newly published model handles. Slot reuse never revives an old handle. Native and
wasm use the same ownership, budgets, lifetime and rendering implementation.
Still fluids in a block entity
A block entity, mob or player submit hook draws a tank's contents with
draw.fluid_box(&stack, bounds, options), where bounds is
[x0, y0, z0, x1, y1, z1] in blocks under the draw's current pose:
let options = FluidBoxOptions::new(false, 4.0 / 1024.0);
draw.fluid_box(&tank.stack, [0.125, 0.0625, 0.125, 0.875, level, 0.875], options)?;
- The stack is
minecraft:waterorminecraft:lavawith no components. Any other fluid, an unresolved one or a component patch isFluidBoxError::UnsupportedFluid. - Each face tiles the fluid's animated still sprite once per block, cut at whole-block lines.
Each tile keeps its place in the sprite. The box is not inset.
uv_shrink_ratiois the sprite'sTextureAtlasSprite.uvShrinkRatio(0.0..=1.0), andrender_bottomadds the bottom face. - The colour is the stack's tint (plain water
0xFF3F76E4, lava white), never the biome's water colour.with_argbreplaces it, alpha included. Faces have no directional shading. Sky light is the draw's own, and block light rises to the fluid's (lava 15). - The faces use a material of their own: sorted far to near, alpha blended, culled from behind, and depth-writing. They draw with the block entities, before the level's translucent terrain, under shader packs too.
- A stack of 0 draws nothing and returns
Okwithout checkingbounds. Non-finite or reversed bounds, a box of more than 65,536 tiled quads and a ratio out of range areFluidBoxError::InvalidBox. A refusal draws nothing.
A player's or humanoid's parts
mob_layer gets a humanoid's parts as vanilla posed them this frame (parts,
HumanoidModel.setupAnim), read-only, to attach geometry to an arm or the head without
animating it again. Only HumanoidMobRenderer's families have them (players, zombies and their
kin, skeletons, piglins); every other mob gets None, a cow included, though its model has a
head and a body. PosedParts is a fixed-size snapshot: get(part) or iter(). It is taken
from the vanilla body's own draw, so a part player_submit hid (below) has visible false.
player_submit can hide single parts of the vanilla player model instead of replacing all of
it: hidden.hide(HumanoidPart::Head), as a mod drawing its own outer skin layer does.
- Hiding happens after Skin Customization, for this frame only.
- A hidden part takes its children with it: the hat goes with the head, a sleeve with its arm.
- Only the body loses the part. The armour, held items and other layers keep every part, and the cape, a layer of its own, stays.