Skip to content

Advanced Entity Rendering

This guide covers advanced entity rendering techniques beyond basic setup. If you’re new to entity rendering, start with Your First Renderer.

LOD reduces rendering cost by using simpler models at distances:

// Example: LOD implementation in entity renderer
public class AdvancedEntityRenderer extends EntityRenderer<AdvancedEntity> {
@Override
public void render(AdvancedEntity entity, float entityYaw, float partialTick,
PoseStack poseStack, MultiBufferSource bufferSource,
int packedLight) {
double distance = entity.distanceToSqr(Minecraft.getInstance().player);
int lodLevel = calculateLODLevel(distance);
switch (lodLevel) {
case 0: // High detail - close
renderHighDetail(entity, partialTick, poseStack, bufferSource, packedLight);
break;
case 1: // Medium detail - moderate distance
renderMediumDetail(entity, partialTick, poseStack, bufferSource, packedLight);
break;
case 2: // Low detail - far away
renderLowDetail(entity, partialTick, poseStack, bufferSource, packedLight);
break;
case 3: // Billboard - very far
renderBillboard(entity, partialTick, poseStack, bufferSource, packedLight);
break;
}
}
private int calculateLODLevel(double distanceSquared) {
if (distanceSquared < 64.0) return 0; // High detail within 8 blocks
if (distanceSquared < 256.0) return 1; // Medium detail within 16 blocks
if (distanceSquared < 1024.0) return 2; // Low detail within 32 blocks
return 3; // Billboard beyond 32 blocks
}
private void renderBillboard(AdvancedEntity entity, float partialTick,
PoseStack poseStack, MultiBufferSource bufferSource,
int packedLight) {
poseStack.pushPose();
// Face the camera
Vec3 cameraPos = Minecraft.getInstance().gameRenderer.getMainCamera().getPosition();
Vec3 entityPos = entity.getEyePosition(partialTick);
Vec3 lookDir = cameraPos.subtract(entityPos).normalize();
float yaw = (float) Mth.atan2(lookDir.x, lookDir.z) * (180.0f / (float)Math.PI);
poseStack.mulPose(Axis.YP.rotationDegrees(yaw));
// Simple quad for billboard
renderBillboardQuad(poseStack, bufferSource, packedLight, entity.getColor());
poseStack.popPose();
}
private void renderBillboardQuad(PoseStack poseStack, MultiBufferSource bufferSource,
int packedLight, Color color) {
Matrix4f matrix = poseStack.last().pose();
VertexConsumer consumer = bufferSource.getBuffer(RenderType.entityCutout(getTextureLocation(entity)));
float size = 0.5f;
// Billboard quad vertices
addBillboardVertex(matrix, consumer, -size, -size, color, 0, 0, packedLight);
addBillboardVertex(matrix, consumer, size, -size, color, 1, 0, packedLight);
addBillboardVertex(matrix, consumer, size, size, color, 1, 1, packedLight);
addBillboardVertex(matrix, consumer, -size, size, color, 0, 1, packedLight);
}
}

Render entities in multiple passes for complex effects:

// Example: Multi-pass entity rendering
public class MultiPassEntityRenderer extends EntityRenderer<MultiPassEntity> {
@Override
public void render(MultiPassEntity entity, float entityYaw, float partialTick,
PoseStack poseStack, MultiBufferSource bufferSource,
int packedLight) {
// First pass: Shadow
renderShadow(entity, partialTick, poseStack, bufferSource);
// Second pass: Main model
renderMainModel(entity, entityYaw, partialTick, poseStack, bufferSource, packedLight);
// Third pass: Glow effect
renderGlowEffect(entity, partialTick, poseStack, bufferSource);
// Fourth pass: Particles
renderAttachedParticles(entity, partialTick, poseStack, bufferSource);
}
private void renderShadow(MultiPassEntity entity, float partialTick,
PoseStack poseStack, MultiBufferSource bufferSource) {
poseStack.pushPose();
// Project shadow onto ground
Vec3 pos = entity.position();
float groundY = entity.level.getBlockFloorHeight(new BlockPos(pos.x, pos.y, pos.z));
float shadowHeight = groundY + 0.1f;
poseStack.translate(pos.x, shadowHeight, pos.z);
poseStack.scale(entity.getBbWidth(), 0.1f, entity.getBbWidth());
poseStack.mulPose(Axis.XP.rotationDegrees(90.0f));
Matrix4f matrix = poseStack.last().pose();
VertexConsumer consumer = bufferSource.getBuffer(RenderType.entityShadow(getShadowTexture()));
// Simple shadow quad
float shadowSize = 1.0f;
float alpha = calculateShadowAlpha(entity);
addShadowVertex(matrix, consumer, -shadowSize, -shadowSize, alpha);
addShadowVertex(matrix, consumer, shadowSize, -shadowSize, alpha);
addShadowVertex(matrix, consumer, shadowSize, shadowSize, alpha);
addShadowVertex(matrix, consumer, -shadowSize, shadowSize, alpha);
poseStack.popPose();
}
private float calculateShadowAlpha(MultiPassEntity entity) {
// Shadows fade with height
Vec3 pos = entity.position();
float groundY = entity.level.getBlockFloorHeight(new BlockPos(pos.x, pos.y, pos.z));
float heightAboveGround = pos.y - groundY;
return Math.max(0.1f, Math.min(0.8f, 1.0f - heightAboveGround * 0.1f));
}
}

Integrate custom shaders for unique effects:

// Example: Entity renderer with custom shader
public class ShaderEntityRenderer extends EntityRenderer<ShaderEntity> {
private static final ShaderInstance CUSTOM_SHADER;
private final ModelPart model;
static {
try {
CUSTOM_SHADER = new ShaderInstance(
Minecraft.getInstance().getResourceManager(),
"custom_entity",
DefaultVertexFormat.POSITION_COLOR_TEX_LIGHTMAP
);
} catch (IOException e) {
throw new RuntimeException("Failed to load custom shader", e);
}
}
@Override
public void render(ShaderEntity entity, float entityYaw, float partialTick,
PoseStack poseStack, MultiBufferSource bufferSource,
int packedLight) {
// Setup custom shader
setupCustomShader(entity, partialTick);
// Render with custom shader
VertexConsumer consumer = bufferSource.getBuffer(getCustomRenderType());
renderModelWithShader(entity, poseStack, consumer, packedLight);
// Clear shader state
CUSTOM_SHADER.clear();
}
private void setupCustomShader(ShaderEntity entity, float partialTick) {
float time = (entity.tickCount + partialTick) / 20.0f;
float waveIntensity = Mth.sin(time * 2.0f) * 0.5f + 0.5f;
// Set shader uniforms
CUSTOM_SHADER.safeGetUniform("Time").set(time);
CUSTOM_SHADER.safeGetUniform("WaveIntensity").set(waveIntensity);
CUSTOM_SHADER.safeGetUniform("EntityColor").set(
entity.getColor().getRed() / 255.0f,
entity.getColor().getGreen() / 255.0f,
entity.getColor().getBlue() / 255.0f
);
CUSTOM_SHADER.safeGetUniform("GlowStrength").set(entity.getGlowStrength());
// Apply shader
RenderSystem.setShader(() -> CUSTOM_SHADER);
}
private RenderType getCustomRenderType() {
return RenderType.create(
"custom_entity_shader",
DefaultVertexFormat.POSITION_COLOR_TEX_LIGHTMAP,
VertexFormat.Mode.QUADS,
256,
RenderType.CompositeState.builder()
.setShaderState(RenderStateShards.RENDERTYPE_ENTITY_TRANSLUCENT_SHADER)
.setTextureState(RenderStateShards.BLOCK_SHEET_MIPPED)
.setTransparencyState(RenderStateShards.TRANSLUCENT_TRANSPARENCY)
.setCullState(RenderStateShards.NO_CULL)
.setLightmapState(RenderStateShards.LIGHTMAP)
.setOverlayState(RenderStateShards.OVERLAY)
.createCompositeState(false)
);
}
}

Attach particles to entities:

// Example: Entity with integrated particle effects
public class ParticleEntityRenderer extends EntityRenderer<ParticleEntity> {
private final ParticleEngine particleEngine;
public ParticleEntityRenderer(EntityRendererProvider.Context context) {
super(context);
this.particleEngine = Minecraft.getInstance().particleEngine;
}
@Override
public void render(ParticleEntity entity, float entityYaw, float partialTick,
PoseStack poseStack, MultiBufferSource bufferSource,
int packedLight) {
// Render main entity
renderMainEntity(entity, entityYaw, partialTick, poseStack, bufferSource, packedLight);
// Create particle effects
updateParticleEffects(entity, partialTick);
}
private void updateParticleEffects(ParticleEntity entity, float partialTick) {
Vec3 pos = entity.getEyePosition(partialTick);
// Ambient particles
if (entity.tickCount % 5 == 0) {
for (int i = 0; i < 3; i++) {
double offsetX = (entity.getRandom().nextDouble() - 0.5) * 0.2;
double offsetY = entity.getRandom().nextDouble() * 0.1;
double offsetZ = (entity.getRandom().nextDouble() - 0.5) * 0.2;
particleEngine.add(
new AmbientParticle(
particleEngine,
pos.x + offsetX,
pos.y + offsetY,
pos.z + offsetZ,
entity.getParticleColor()
)
);
}
}
// Movement particles when entity moves
if (entity.getDeltaMovement().lengthSqr() > 0.01) {
if (entity.tickCount % 2 == 0) {
Vec3 movement = entity.getDeltaMovement();
particleEngine.add(
new MotionParticle(
particleEngine,
pos.x - movement.x * 0.5,
pos.y,
pos.z - movement.z * 0.5,
-movement.x * 0.1,
0.1,
-movement.z * 0.1,
entity.getTrailColor()
)
);
}
}
}
}

Complex animation system with state machines:

// Example: Advanced entity animation
public class AnimationEntityRenderer extends EntityRenderer<AnimationEntity> {
private final Map<String, Animation> animations = new HashMap<>();
@Override
public void render(AnimationEntity entity, float entityYaw, float partialTick,
PoseStack poseStack, MultiBufferSource bufferSource,
int packedLight) {
// Update animations
updateAnimations(entity, partialTick);
// Apply animations to model
applyAnimationsToModel(entity, partialTick, poseStack);
// Render animated model
renderAnimatedModel(entity, poseStack, bufferSource, packedLight);
}
private void updateAnimations(AnimationEntity entity, float partialTick) {
// Update animation states
entity.updateAnimationStates(partialTick);
// Process animation transitions
processAnimationTransitions(entity);
// Calculate animation transforms
calculateAnimationTransforms(entity);
}
private void applyAnimationsToModel(AnimationEntity entity, float partialTick,
PoseStack poseStack) {
AnimationState currentState = entity.getCurrentAnimationState();
// Apply root transform
poseStack.translate(
entity.getRootTransform().getTranslationX(partialTick),
entity.getRootTransform().getTranslationY(partialTick),
entity.getRootTransform().getTranslationZ(partialTick)
);
// Apply rotations
poseStack.mulPose(Axis.XP.rotationDegrees(
entity.getRootTransform().getRotationX(partialTick)));
poseStack.mulPose(Axis.YP.rotationDegrees(
entity.getRootTransform().getRotationY(partialTick)));
poseStack.mulPose(Axis.ZP.rotationDegrees(
entity.getRootTransform().getRotationZ(partialTick)));
// Apply scale
float scale = entity.getRootTransform().getScale(partialTick);
poseStack.scale(scale, scale, scale);
// Apply bone transforms to model parts
applyBoneTransforms(entity, partialTick);
}
}

Render multiple entities with same model efficiently:

// Example: Instanced rendering for similar entities
public class InstancedEntityRenderer extends EntityRenderer<InstancedEntity> {
private final Map<RenderType, InstancedBatch> batches = new HashMap<>();
@Override
public void render(InstancedEntity entity, float entityYaw, float partialTick,
PoseStack poseStack, MultiBufferSource bufferSource,
int packedLight) {
RenderType renderType = getRenderType(entity);
InstancedBatch batch = batches.computeIfAbsent(renderType, InstancedBatch::new);
// Add instance to batch instead of immediate rendering
Matrix4f transformMatrix = createTransformMatrix(entity, entityYaw, partialTick);
batch.addInstance(transformMatrix, entity.getColor(), packedLight);
}
@Override
public void onRenderComplete() {
// Render all batches at end of frame
for (InstancedBatch batch : batches.values()) {
batch.render();
batch.clear();
}
}
private static class InstancedBatch {
private final List<InstanceData> instances = new ArrayList<>();
private final RenderType renderType;
public InstancedBatch(RenderType renderType) {
this.renderType = renderType;
}
public void addInstance(Matrix4f transform, Color color, int packedLight) {
instances.add(new InstanceData(transform, color, packedLight));
}
public void render() {
if (instances.isEmpty()) return;
VertexConsumer consumer = Minecraft.getInstance().renderBuffers()
.bufferSource().getBuffer(renderType);
// Render all instances in single draw call
for (InstanceData instance : instances) {
renderInstance(consumer, instance);
}
}
public void clear() {
instances.clear();
}
}
}

Intelligent culling to reduce unnecessary rendering:

// Example: Advanced culling system
public class CullingEntityRenderer extends EntityRenderer<CullingEntity> {
private final Frustum frustum = new Frustum(Matrix4f::new, Matrix4f::new);
@Override
public void render(CullingEntity entity, float entityYaw, float partialTick,
PoseStack poseStack, MultiBufferSource bufferSource,
int packedLight) {
// Early-out checks
if (!shouldRender(entity)) {
return;
}
// Apply LOD
if (!shouldRenderAtDetail(entity)) {
return;
}
// Render entity
renderEntity(entity, entityYaw, partialTick, poseStack, bufferSource, packedLight);
}
private boolean shouldRender(CullingEntity entity) {
// Distance culling
double distanceSquared = entity.distanceToSqr(Minecraft.getInstance().player);
if (distanceSquared > entity.getMaxRenderDistanceSquared()) {
return false;
}
// Frustum culling
frustum.setPosition(Minecraft.getInstance().gameRenderer.getMainCamera().getPosition());
if (!frustum.isVisible(entity.getBoundingBox())) {
return false;
}
// Occlusion culling (simplified)
if (isOccluded(entity)) {
return false;
}
return true;
}
private boolean isOccluded(CullingEntity entity) {
// Simple ray check to camera
Vec3 from = entity.getEyePosition();
Vec3 to = Minecraft.getInstance().gameRenderer.getMainCamera().getPosition();
// Check if line of sight is blocked by solid blocks
return entity.level.clip(new ClipContext(from, to, Block.OUTLINE, CollisionContext.empty())).getType() != HitResult.Type.MISS;
}
}

Problem: Flickering between overlapping geometry Solution: Use proper depth offsets and polygon offset

// Example: Fixing z-fighting
public void renderWithDepthFix(PoseStack poseStack, MultiBufferSource bufferSource) {
RenderSystem.enablePolygonOffset();
RenderSystem.polygonOffset(-1.0f, -10.0f);
// Render overlapping geometry
renderGeometry(poseStack, bufferSource);
RenderSystem.polygonOffset(0.0f, 0.0f);
RenderSystem.disablePolygonOffset();
}

Problem: Animations not smooth or desynchronized Solution: Use proper partial tick handling

// Example: Smooth animation timing
public void updateAnimationTime(Entity entity, float partialTick) {
// Use combined time for smooth animations
float animationTime = entity.tickCount + partialTick;
// Calculate animation state with proper interpolation
float animationProgress = (animationTime % animationDuration) / animationDuration;
// Apply smooth interpolation
float interpolatedValue = Mth.lerp(partialTick,
previousAnimationValue,
currentAnimationValue);
// Use interpolated value for rendering
applyAnimation(interpolatedValue);
}

Problem: Slow rendering with many entities Solution: Profile and optimize bottlenecks

// Example: Performance profiling
public class ProfilingEntityRenderer extends EntityRenderer<ProfilingEntity> {
private static long totalRenderTime = 0;
private static int renderCount = 0;
@Override
public void render(ProfilingEntity entity, float entityYaw, float partialTick,
PoseStack poseStack, MultiBufferSource bufferSource,
int packedLight) {
long startTime = System.nanoTime();
// Actual rendering
renderEntity(entity, entityYaw, partialTick, poseStack, bufferSource, packedLight);
long endTime = System.nanoTime();
totalRenderTime += (endTime - startTime);
renderCount++;
// Log performance every 1000 renders
if (renderCount % 1000 == 0) {
float averageTime = totalRenderTime / (float) renderCount / 1_000_000.0f;
System.out.println(String.format("Average render time: %.3f ms", averageTime));
}
}
}
  1. Implement LOD for entities visible at different distances
  2. Use batching for multiple similar entities
  3. Apply proper culling to avoid unnecessary rendering
  4. Optimize animations with smooth interpolation
  5. Profile performance to identify bottlenecks
  6. Use appropriate shaders for visual effects
  7. Implement instancing for large numbers of similar entities