Advanced Entity Rendering
Advanced Entity Rendering
Section titled “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.
Advanced Rendering Patterns
Section titled “Advanced Rendering Patterns”Level of Detail (LOD)
Section titled “Level of Detail (LOD)”LOD reduces rendering cost by using simpler models at distances:
// Example: LOD implementation in entity rendererpublic 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); }}Multi-Pass Rendering
Section titled “Multi-Pass Rendering”Render entities in multiple passes for complex effects:
// Example: Multi-pass entity renderingpublic 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)); }}Custom Shader Integration
Section titled “Custom Shader Integration”Integrate custom shaders for unique effects:
// Example: Entity renderer with custom shaderpublic 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) ); }}Advanced Effects
Section titled “Advanced Effects”Particle Integration
Section titled “Particle Integration”Attach particles to entities:
// Example: Entity with integrated particle effectspublic 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() ) ); } } }}Animation System
Section titled “Animation System”Complex animation system with state machines:
// Example: Advanced entity animationpublic 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); }}Performance Optimization
Section titled “Performance Optimization”Instanced Rendering
Section titled “Instanced Rendering”Render multiple entities with same model efficiently:
// Example: Instanced rendering for similar entitiespublic 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(); } }}Culling and Optimization
Section titled “Culling and Optimization”Intelligent culling to reduce unnecessary rendering:
// Example: Advanced culling systempublic 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; }}Common Issues and Solutions
Section titled “Common Issues and Solutions”Z-Fighting
Section titled “Z-Fighting”Problem: Flickering between overlapping geometry Solution: Use proper depth offsets and polygon offset
// Example: Fixing z-fightingpublic 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();}Animation Synchronization
Section titled “Animation Synchronization”Problem: Animations not smooth or desynchronized Solution: Use proper partial tick handling
// Example: Smooth animation timingpublic 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);}Performance Profiling
Section titled “Performance Profiling”Problem: Slow rendering with many entities Solution: Profile and optimize bottlenecks
// Example: Performance profilingpublic 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)); } }}Best Practices
Section titled “Best Practices”- Implement LOD for entities visible at different distances
- Use batching for multiple similar entities
- Apply proper culling to avoid unnecessary rendering
- Optimize animations with smooth interpolation
- Profile performance to identify bottlenecks
- Use appropriate shaders for visual effects
- Implement instancing for large numbers of similar entities
Next Steps
Section titled “Next Steps”- Custom Render Types - Advanced render type configurations
- Shader Pipeline - Working with custom shaders
- Performance Optimization - Deep performance dive
- Complete Examples - Full working examples with advanced features