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-003is 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
});
WorldCanvas | Meaning |
|---|---|
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-003and 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));
#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 asProjMat * ModelViewMat * vec4(Position, 1.0), like the game'score/position_color.vsh. - Game uniform blocks:
dynamictransforms.glsl:ModelViewMat,ColorModulatorprojection.glsl:ProjMatglobals.glsl:GameTime,ScreenSize, and others
- Builder settings:
blend:OPAQUE,TRANSLUCENT(default),ADDITIVEcull(): does not draw back facesnoDepthTest(): draws in front of blocksdepthWrite(): 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-006once 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));
#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 toSampler0,Sampler1, and so on (up to 4). Use a texture file (my_mod:textures/…png) or theid()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 UV0added to its vertices. Pass it withvertex(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 theModUniformsblock (up to 16, no duplicate names). The types areFLOAT,VEC2,VEC3, andVEC4, 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-007once 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 getPOLY-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 givesPOLY-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_VIEWis 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>.jsonand the shaders from the mod's resources as the game's post-processing chain. - If it cannot be loaded, it returns
falseand the game logs an error. - When the camera target or perspective changes, the game changes the effect (for example, spectating a creeper).
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.