Skip to main content
Version: Minecraft 26.2

Rendering and camera

Mob box models are in Mob models. Particle types are in Game rules, sounds, and particles.

Block entity rendering​

client.blockEntityRenderer(GRINDER, (entity, canvas) -> { // block coordinates (0,0,0) to (1,1,1)
canvas.item(ResourceId.minecraft("cobblestone"), 0.5, 1.3, 0.5, 0.5f, canvas.animationTime() * 4);
canvas.text("Grinding 40%", 0.5, 1.75, 0.5, 0.02f, 0xFFFFFFFF); // faces the camera
});
  • Register one for each of your mod's blocks that has a block entity.
  • It is called every frame on the render thread, with the client-side block entity. If you need data, turn on syncToClients() on the block entity.
  • BlockEntityCanvas: item(item, x, y, z, scale, yawDegrees) (like an item frame), text(text, x, y, z, scale, argb) (text facing the camera), block(…) (block model), defaultState(block), quads(pipeline, vertices), animationTime()
  • If it throws an exception, that rendering is turned off and POLY-CLIENT-003 is logged.

You can also draw block models and shader quads.

canvas.block(GLOW_BLOCK, 0.5, 2.2, 0.5, 0.35f, canvas.animationTime() * 3 % 360); // centered, size, rotation
canvas.quads(glow, quads -> quads.vertex(0, 1.01, 0, 0x60FFD54F).vertex(0, 1.01, 1, 0x60FFD54F)
.vertex(1, 1.01, 1, 0x60FFD54F).vertex(1, 1.01, 0, 0x60FFD54F)); // glowing plate on top of the block

block(block, x, y, z, scale, yaw) draws the model of the block's default state as if it were a block in the world. For coordinates, the lowest corner of the block is 0, 0, 0.

To draw the model of another state, use BlockStateView. Get a state with defaultState(block) or entity.world().stateAt(pos), and create a changed copy with with(property, value). with does not change the world.

BlockStateView lamp = canvas.defaultState(LAMP).with(LIT, true).with(COLOR, "blue");
canvas.block(lamp, 0.5, 2.7, 0.5, 0.25f, 0); // a lit blue lamp
canvas.block(entity.world().stateAt(entity.pos()), 0.5, 1.5, 0.5, 0.5f, 0); // the current state as it is

If you pass with a property the block does not have, or a value that is not allowed, it throws IllegalArgumentException.

Drawing in the world​

client.worldPainter draws lines, boxes, points, text, and shader quads in world coordinates every frame. Use it for area markers, paths, debug views, and glow effects.

client.worldPainter("compass", canvas -> {
double x = canvas.cameraX(), y = canvas.cameraY() - 1.5, z = canvas.cameraZ();
canvas.line(x, y, z, x, y, z - 2, 0xFFFF5555, 3); // red line to the north, 3 pixels
canvas.text("N", x, y + 0.3, z - 2.2, 0xFFFF5555, 1); // text that faces the camera
canvas.box(x - 0.1, y - 0.1, z - 0.1, x + 0.1, y + 0.1, z + 0.1,
0xFFFFFFFF, 2, 0x40FFFFFF); // outline and translucent faces
canvas.onTop().blockOutline(BlockPos.of(0, 64, 0), 0xFF55FF55, 2); // not hidden by blocks
});
WorldCanvasMeaning
line(x1, y1, z1, x2, y2, z2, argb, width)Line, width in pixels
box(min…, max…, strokeArgb, width, fillArgb)Box outline and faces. If 0, that part is not drawn
blockOutline(pos, argb, width)Outline of one block
point(x, y, z, argb, size)Point
text(text, x, y, z, argb, scale)Text that faces the camera
quads(pipeline, vertices)Quads drawn with a mod shader pipeline
onTop()A canvas that draws lines, boxes, points, and text in front of blocks
cameraX/Y/Z(), animationTime()Camera position, game time plus partial tick
  • It is called every frame on the render thread. Keep it light.
  • Lines, boxes, points, and text are drawn with the game's gizmo drawing. Shader quads follow the depth settings of their pipeline.
  • If drawing throws an exception, Poly Mod Loader logs POLY-CLIENT-003 and turns that drawing off.

Custom shader pipelines​

You draw quads with core shaders (GLSL) that your mod writes. Put the files at assets/<namespace>/shaders/<path>.vsh and .fsh.

ShaderPipeline glow = client.shaderPipeline("glow",
ShaderPipelineDefinition.builder(ResourceId.of("my_mod", "core/glow")) // shaders/core/glow.vsh, .fsh
.blend(ShaderPipelineDefinition.Blend.ADDITIVE)
.build());

canvas.quads(glow, quads -> quads
.vertex(x - 0.6, y, z - 0.6, 0x80FFD54F).vertex(x - 0.6, y, z + 0.6, 0x80FFD54F)
.vertex(x + 0.6, y, z + 0.6, 0x80FFD54F).vertex(x + 0.6, y, z - 0.6, 0x80FFD54F));
assets/my_mod/shaders/core/glow.fsh
#version 330
#moj_import <minecraft:dynamictransforms.glsl>
#moj_import <minecraft:globals.glsl>

in vec4 vertexColor;
out vec4 fragColor;

void main() {
float pulse = 0.6 + 0.4 * sin(GameTime * 2400.0); // blinks about every 3 seconds
fragColor = vec4(vertexColor.rgb * pulse, vertexColor.a) * ColorModulator;
}
  • Vertex shader input: in vec3 Position, in vec4 Color. Do the transform as ProjMat * ModelViewMat * vec4(Position, 1.0), like the game's core/position_color.vsh.
  • Game uniform blocks:
    • dynamictransforms.glsl: ModelViewMat, ColorModulator
    • projection.glsl: ProjMat
    • globals.glsl: GameTime, ScreenSize, and others
  • Builder settings:
    • blend: OPAQUE, TRANSLUCENT (default), ADDITIVE
    • cull(): does not draw back faces
    • noDepthTest(): draws in front of blocks
    • depthWrite(): hides what is drawn behind it
  • Created on first use: A pipeline is created on the render thread the first time it is used. If the shader does not compile, Poly Mod Loader logs POLY-CLIENT-006 once and skips the quads of that pipeline. When resources reload, the shaders are read again.

Textures and mod uniforms​

You can attach textures to a pipeline and pass values that your mod decides to the shader.

AtomicLong frames = new AtomicLong();
ShaderPipeline pulse = client.shaderPipeline("pulse",
ShaderPipelineDefinition.builder(ResourceId.of("my_mod", "core/pulse"))
.texture(ResourceId.of("my_mod", "textures/item/spark.png")) // Sampler0
.uniform("Strength", ShaderUniform.Type.FLOAT,
() -> new float[] {0.75f + 0.25f * (float) Math.sin(frames.incrementAndGet() / 20.0)})
.uniform("Tint", ShaderUniform.Type.VEC4, () -> new float[] {1f, 0.85f, 0.4f, 1f})
.build());

canvas.quads(pulse, quads -> quads // u, v for each vertex
.vertex(x - 0.3, y, z - 1.3, 0, 0, 0xFFFFFFFF).vertex(x - 0.3, y, z - 0.7, 0, 1, 0xFFFFFFFF)
.vertex(x + 0.3, y, z - 0.7, 1, 1, 0xFFFFFFFF).vertex(x + 0.3, y, z - 1.3, 1, 0, 0xFFFFFFFF));
assets/my_mod/shaders/core/pulse.fsh
#version 330
#moj_import <minecraft:dynamictransforms.glsl>

layout(std140) uniform ModUniforms { // in the order you called uniform(...)
float Strength;
vec4 Tint;
};
uniform sampler2D Sampler0;

in vec2 texCoord0;
in vec4 vertexColor;
out vec4 fragColor;

void main() {
vec4 color = texture(Sampler0, texCoord0) * vertexColor * Tint;
if (color.a == 0.0) discard;
fragColor = vec4(color.rgb * Strength, color.a) * ColorModulator;
}
  • Textures: In the order you call texture(id), they are bound to Sampler0, Sampler1, and so on (up to 4). Use a texture file (my_mod:textures/…png) or the id() of a dynamic texture. If the file is missing, the game's missing-texture pattern appears.
  • Texture coordinates: A pipeline with textures gets in vec2 UV0 added to its vertices. Pass it with vertex(x, y, z, u, v, argb). (0, 0) is the top left. vertex(x, y, z, argb) without coordinates uses (0, 0).
  • Mod uniforms: uniform(name, type, value) becomes a field of the ModUniforms block (up to 16, no duplicate names). The types are FLOAT, VEC2, VEC3, and VEC4, and the field order in the GLSL block must match the declaration order.
  • When values are requested: A value supplier is called once every frame, on the render thread, before the world is drawn. Keep it light. The first frame is 0.
  • Invalid values: If a supplier throws an exception or returns the wrong count, Poly Mod Loader logs POLY-CLIENT-007 once and keeps using the last valid value.
  • Invalid declarations: If a name is not a GLSL identifier (it also must not start with gl_), the count goes over the limit, or names collide, you get POLY-CLIENT-002.

Dynamic textures​

DynamicTexture map = client.dynamicTexture("map", 64, 64); // my_mod:dynamic/map
map.setPixel(x, y, argb);
map.upload();
canvas.drawTexture(map.id(), 4, 4, 64, 64); // in the HUD
  • Poly Mod Loader keeps the pixels and sends them to the game texture on the client thread when you call upload(). The texture is created the first time you upload.
  • fill(argb) paints the whole texture.
  • Calling upload() from a thread other than the client thread gives POLY-CLIENT-005.

Field of view and perspective​

mod.events().register(ClientEvents.FIELD_OF_VIEW, fov -> zooming ? fov * 0.3f : fov);
view.setCameraPerspective(CameraPerspective.THIRD_PERSON_BACK); // FIRST_PERSON, THIRD_PERSON_FRONT
  • FIELD_OF_VIEW is called every frame, after the game computes the field of view (sprinting, underwater). Keep it light.
  • Switching perspective is the same as F5.

Post-processing shaders​

boolean loaded = view.setPostEffect(ResourceId.of("my_mod", "sepia"));
// assets/my_mod/post_effect/sepia.json + assets/my_mod/shaders/post/*.fsh
view.clearPostEffect();
  • It applies post_effect/<path>.json and the shaders from the mod's resources as the game's post-processing chain.
  • If it cannot be loaded, it returns false and the game logs an error.
  • When the camera target or perspective changes, the game changes the effect (for example, spectating a creeper).
Human check

A person must check whether the drawn result looks right. The automated check (clientWorldReloadTest) only verifies the execution paths of pipeline creation, uniform upload and binding, and drawing block models by state.

Buy me a coffee