diff --git a/src/main/java/com/lambda/mixin/entity/EntityMixin.java b/src/main/java/com/lambda/mixin/entity/EntityMixin.java
index bb116af94..f53bb4818 100644
--- a/src/main/java/com/lambda/mixin/entity/EntityMixin.java
+++ b/src/main/java/com/lambda/mixin/entity/EntityMixin.java
@@ -17,6 +17,7 @@
package com.lambda.mixin.entity;
+import com.lambda.Lambda;
import com.lambda.event.EventFlow;
import com.lambda.event.events.EntityEvent;
import com.lambda.event.events.PlayerEvent;
@@ -151,11 +152,13 @@ private boolean modifyGetFlagGlowing(boolean original) {
@WrapWithCondition(method = "changeLookDirection", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;setYaw(F)V"))
private boolean wrapSetYaw(Entity instance, float yaw) {
+ if ((Object) this != getMc().player) return true;
return RotationManager.getLockYaw() == null;
}
@WrapWithCondition(method = "changeLookDirection", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;setPitch(F)V"))
private boolean wrapSetPitch(Entity instance, float yaw) {
+ if ((Object) this != Lambda.getMc().player) return true;
return RotationManager.getLockPitch() == null;
}
diff --git a/src/main/java/com/lambda/mixin/items/BarrierBlockMixin.java b/src/main/java/com/lambda/mixin/items/BarrierBlockMixin.java
deleted file mode 100644
index b4775b72f..000000000
--- a/src/main/java/com/lambda/mixin/items/BarrierBlockMixin.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright 2025 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.mixin.items;
-
-import com.lambda.module.modules.render.BlockESP;
-import com.llamalad7.mixinextras.injector.ModifyReturnValue;
-import net.minecraft.block.BarrierBlock;
-import net.minecraft.block.BlockRenderType;
-import net.minecraft.block.BlockState;
-import net.minecraft.block.Blocks;
-import org.spongepowered.asm.mixin.Mixin;
-import org.spongepowered.asm.mixin.injection.At;
-
-@Mixin(BarrierBlock.class)
-public class BarrierBlockMixin {
- /**
- * Modifies barrier block render type to {@link BlockRenderType#MODEL} when {@link BlockESP} is enabled and {@link BlockESP#getBarrier()} is true
- */
- @ModifyReturnValue(method = "getRenderType", at = @At("RETURN"))
- private BlockRenderType modifyGetRenderType(BlockRenderType original, BlockState state) {
- if (BlockESP.INSTANCE.isEnabled()
- && BlockESP.getBarrier()
- && state.getBlock() == Blocks.BARRIER
- ) return BlockRenderType.MODEL;
- return original;
- }
-}
diff --git a/src/main/java/com/lambda/mixin/render/BlockRenderManagerMixin.java b/src/main/java/com/lambda/mixin/render/BlockRenderManagerMixin.java
deleted file mode 100644
index 49ff48b7f..000000000
--- a/src/main/java/com/lambda/mixin/render/BlockRenderManagerMixin.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright 2025 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.mixin.render;
-
-import com.lambda.module.modules.render.BlockESP;
-import net.minecraft.block.BlockState;
-import net.minecraft.block.Blocks;
-import net.minecraft.client.render.block.BlockRenderManager;
-import net.minecraft.client.render.model.BlockStateModel;
-import org.spongepowered.asm.mixin.Mixin;
-import org.spongepowered.asm.mixin.injection.At;
-import org.spongepowered.asm.mixin.injection.Inject;
-import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
-
-@Mixin(BlockRenderManager.class)
-public abstract class BlockRenderManagerMixin {
- @Inject(method = "getModel", at = @At("HEAD"), cancellable = true)
- private void getModel(BlockState state, CallbackInfoReturnable cir) {
- if (BlockESP.INSTANCE.isEnabled()
- && BlockESP.getBarrier()
- && state.getBlock() == Blocks.BARRIER
- ) cir.setReturnValue(BlockESP.getModel());
- }
-}
diff --git a/src/main/java/com/lambda/mixin/render/GameRendererMixin.java b/src/main/java/com/lambda/mixin/render/GameRendererMixin.java
index 98cf293e7..1b91bc5fd 100644
--- a/src/main/java/com/lambda/mixin/render/GameRendererMixin.java
+++ b/src/main/java/com/lambda/mixin/render/GameRendererMixin.java
@@ -21,6 +21,7 @@
import com.lambda.event.events.RenderEvent;
import com.lambda.graphics.RenderMain;
import com.lambda.gui.DearImGui;
+import com.lambda.module.modules.render.BlockOutline;
import com.lambda.module.modules.render.NoRender;
import com.lambda.module.modules.render.Zoom;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
@@ -40,6 +41,7 @@
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(GameRenderer.class)
public class GameRendererMixin {
@@ -53,8 +55,7 @@ private void updateTargetedEntityInvoke(float tickDelta, CallbackInfo info) {
@WrapOperation(method = "renderWorld", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/WorldRenderer;render(Lnet/minecraft/client/util/ObjectAllocator;Lnet/minecraft/client/render/RenderTickCounter;ZLnet/minecraft/client/render/Camera;Lorg/joml/Matrix4f;Lorg/joml/Matrix4f;Lorg/joml/Matrix4f;Lcom/mojang/blaze3d/buffers/GpuBufferSlice;Lorg/joml/Vector4f;Z)V"))
void onRenderWorld(WorldRenderer instance, ObjectAllocator allocator, RenderTickCounter tickCounter, boolean renderBlockOutline, Camera camera, Matrix4f positionMatrix, Matrix4f basicProjectionMatrix, Matrix4f projectionMatrix, GpuBufferSlice fogBuffer, Vector4f fogColor, boolean renderSky, Operation original) {
original.call(instance, allocator, tickCounter, renderBlockOutline, camera, positionMatrix, basicProjectionMatrix, projectionMatrix, fogBuffer, fogColor, renderSky);
-
- RenderMain.render3D(positionMatrix, projectionMatrix);
+ RenderMain.render(positionMatrix, basicProjectionMatrix);
}
@ModifyExpressionValue(method = "renderWorld", at = @At(value = "INVOKE", target = "Ljava/lang/Math;max(FF)F", ordinal = 0))
@@ -69,6 +70,7 @@ private void injectShowFloatingItem(ItemStack floatingItem, CallbackInfo ci) {
@ModifyReturnValue(method = "getFov", at = @At("RETURN"))
private float modifyGetFov(float original) {
+ Zoom.updateCurrentZoom();
return original / Zoom.getLerpedZoom();
}
@@ -76,4 +78,9 @@ private float modifyGetFov(float original) {
private void onGuiRenderComplete(RenderTickCounter tickCounter, boolean tick, CallbackInfo ci) {
DearImGui.INSTANCE.render();
}
+
+ @Inject(method = "shouldRenderBlockOutline()Z", at = @At("HEAD"), cancellable = true)
+ private void injectShouldRenderBlockOutline(CallbackInfoReturnable cir) {
+ if (BlockOutline.INSTANCE.isEnabled()) cir.setReturnValue(false);
+ }
}
diff --git a/src/main/java/com/lambda/mixin/render/InGameHudMixin.java b/src/main/java/com/lambda/mixin/render/InGameHudMixin.java
index 3fc01c204..bdf2a3ba4 100644
--- a/src/main/java/com/lambda/mixin/render/InGameHudMixin.java
+++ b/src/main/java/com/lambda/mixin/render/InGameHudMixin.java
@@ -17,6 +17,8 @@
package com.lambda.mixin.render;
+import com.lambda.event.EventFlow;
+import com.lambda.event.events.HudRenderEvent;
import com.lambda.module.modules.render.NoRender;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import net.minecraft.client.gui.DrawContext;
@@ -82,4 +84,13 @@ private void injectRenderScoreboardSidebar(DrawContext drawContext, ScoreboardOb
private void injectRenderCrosshair(DrawContext context, RenderTickCounter tickCounter, CallbackInfo ci) {
if (NoRender.INSTANCE.isEnabled() && NoRender.getNoCrosshair()) ci.cancel();
}
+
+ /**
+ * Fire HudRenderEvent at the end of HUD rendering to allow Lambda modules
+ * to render items and other GUI elements using the valid DrawContext.
+ */
+ @Inject(method = "render", at = @At("RETURN"))
+ private void onRenderEnd(DrawContext context, RenderTickCounter tickCounter, CallbackInfo ci) {
+ EventFlow.post(new HudRenderEvent(context));
+ }
}
diff --git a/src/main/java/com/lambda/mixin/render/LivingEntityRendererMixin.java b/src/main/java/com/lambda/mixin/render/LivingEntityRendererMixin.java
index 5ce465d21..6f82aa2e2 100644
--- a/src/main/java/com/lambda/mixin/render/LivingEntityRendererMixin.java
+++ b/src/main/java/com/lambda/mixin/render/LivingEntityRendererMixin.java
@@ -19,12 +19,15 @@
import com.lambda.Lambda;
import com.lambda.interaction.managers.rotating.RotationManager;
+import com.lambda.module.modules.render.Nametags;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import net.minecraft.client.render.entity.LivingEntityRenderer;
import net.minecraft.entity.LivingEntity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import static com.lambda.util.math.LinearKt.lerp;
@@ -56,4 +59,10 @@ private float wrapGetLerpedPitch(LivingEntity livingEntity, float v, Operation cir) {
+ if (Nametags.INSTANCE.isEnabled() && Nametags.shouldRenderNametag(livingEntity))
+ cir.setReturnValue(false);
+ }
}
diff --git a/src/main/kotlin/com/lambda/command/commands/PrefixCommand.kt b/src/main/kotlin/com/lambda/command/commands/PrefixCommand.kt
index 57444d190..68e9c83aa 100644
--- a/src/main/kotlin/com/lambda/command/commands/PrefixCommand.kt
+++ b/src/main/kotlin/com/lambda/command/commands/PrefixCommand.kt
@@ -29,6 +29,7 @@ import com.lambda.command.CommandRegistry
import com.lambda.command.LambdaCommand
import com.lambda.config.Configuration
import com.lambda.config.Setting
+import com.lambda.config.SettingCore
import com.lambda.util.Communication.info
import com.lambda.util.extension.CommandBuilder
import com.lambda.util.text.buildText
@@ -52,7 +53,8 @@ object PrefixCommand : LambdaCommand(
}
val prefixChar = prefix.first()
val configurable = Configuration.configurableByName("command") ?: return@executeWithResult failure("No command configurable found.")
- val setting = configurable.settings.find { it.name == "prefix" } as? Setting<*, Char>
+ @Suppress("UNCHECKED_CAST")
+ val setting = configurable.settings.find { it.name == "prefix" } as? Setting, Char>
?: return@executeWithResult failure("Prefix setting is not a Char or can not be found.")
setting.trySetValue(prefixChar)
return@executeWithResult success()
diff --git a/src/main/kotlin/com/lambda/config/AutomationConfig.kt b/src/main/kotlin/com/lambda/config/AutomationConfig.kt
index c32f0677f..1acfefddc 100644
--- a/src/main/kotlin/com/lambda/config/AutomationConfig.kt
+++ b/src/main/kotlin/com/lambda/config/AutomationConfig.kt
@@ -26,7 +26,7 @@ import com.lambda.config.groups.InteractSettings
import com.lambda.config.groups.InventorySettings
import com.lambda.config.groups.RotationSettings
import com.lambda.context.Automated
-import com.lambda.event.events.onStaticRender
+import com.lambda.graphics.mc.renderer.TickedRenderer.Companion.tickedRenderer
import com.lambda.interaction.construction.simulation.result.Drawable
import com.lambda.module.Module
import com.lambda.util.NamedEnum
@@ -85,10 +85,9 @@ open class AutomationConfig(
var drawables = listOf()
init {
- onStaticRender { esp ->
- if (renders)
- drawables.forEach { it.render(esp) }
- }
+ tickedRenderer("Ticked Automation Config Renderer") {
+ if (renders) drawables.forEach { with(it) { render() } }
+ }
}
}
}
diff --git a/src/main/kotlin/com/lambda/config/ConfigEditor.kt b/src/main/kotlin/com/lambda/config/ConfigEditor.kt
index 2e74a71b1..25e930e43 100644
--- a/src/main/kotlin/com/lambda/config/ConfigEditor.kt
+++ b/src/main/kotlin/com/lambda/config/ConfigEditor.kt
@@ -102,6 +102,13 @@ open class SettingGroupEditor(open val c: T) {
@SettingEditorDsl
fun groups(groups: MutableList>) =
settings.forEach { it.groups = groups }
+
+ @SettingEditorDsl
+ fun visibility(visibility: (() -> Boolean) -> () -> Boolean) {
+ settings.forEach {
+ it.visibility = visibility(it.visibility)
+ }
+ }
}
class TypedEditBuilder(
diff --git a/src/main/kotlin/com/lambda/config/MutableAutomationConfig.kt b/src/main/kotlin/com/lambda/config/MutableAutomationConfig.kt
index 06f8d7e80..eaa194555 100644
--- a/src/main/kotlin/com/lambda/config/MutableAutomationConfig.kt
+++ b/src/main/kotlin/com/lambda/config/MutableAutomationConfig.kt
@@ -60,7 +60,7 @@ class MutableAutomationConfigImpl : MutableAutomationConfig {
if (setting.core.type != newSetting.core.type)
throw IllegalStateException("Settings with the same name do not have the same type.")
@Suppress("UNCHECKED_CAST")
- (setting as Setting, Any>).core = newSetting.core as SettingCore
+ (setting as Setting, Any>).core = newSetting.core as SettingCore
}
}
}
diff --git a/src/main/kotlin/com/lambda/config/Setting.kt b/src/main/kotlin/com/lambda/config/Setting.kt
index 75f9ed688..eef00628e 100644
--- a/src/main/kotlin/com/lambda/config/Setting.kt
+++ b/src/main/kotlin/com/lambda/config/Setting.kt
@@ -90,9 +90,7 @@ import kotlin.reflect.KProperty
* ```
*
* @property defaultValue The default value of the setting.
- * @property description A description of the setting.
* @property type The type reflection of the setting.
- * @property visibility A function that determines whether the setting is visible.
*/
abstract class SettingCore(
var defaultValue: T,
@@ -155,11 +153,12 @@ class Setting, R>(
override val description: String,
var core: T,
val configurable: Configurable,
- val visibility: () -> Boolean,
+ var visibility: () -> Boolean,
) : Nameable, Describable {
val originalCore = core
var disabled = { false }
var groups: MutableList> = mutableListOf()
+ var buttonMenu: NamedEnum? = null
var value by this
@@ -227,6 +226,10 @@ class Setting, R>(
path?.let { groups.add(listOf(it)) }
}
+ fun buttonMenu(menu: NamedEnum) = apply {
+ buttonMenu = menu
+ }
+
fun trySetValue(newValue: R) {
if (newValue == value) {
ConfigCommand.info(notChangedMessage())
diff --git a/src/main/kotlin/com/lambda/config/SettingGroup.kt b/src/main/kotlin/com/lambda/config/SettingGroup.kt
index 6ee65aae3..6f7249714 100644
--- a/src/main/kotlin/com/lambda/config/SettingGroup.kt
+++ b/src/main/kotlin/com/lambda/config/SettingGroup.kt
@@ -19,6 +19,7 @@ package com.lambda.config
interface ISettingGroup {
val settings: MutableList>
+ val visibility: () -> Boolean
}
abstract class SettingGroup(c: Configurable) : ISettingGroup {
diff --git a/src/main/kotlin/com/lambda/config/groups/BreakSettings.kt b/src/main/kotlin/com/lambda/config/groups/BreakSettings.kt
index f4986db31..1b37e9471 100644
--- a/src/main/kotlin/com/lambda/config/groups/BreakSettings.kt
+++ b/src/main/kotlin/com/lambda/config/groups/BreakSettings.kt
@@ -32,7 +32,8 @@ import java.awt.Color
open class BreakSettings(
c: Configurable,
- baseGroup: NamedEnum
+ baseGroup: NamedEnum,
+ override val visibility: () -> Boolean = { true },
) : SettingGroup(c), BreakConfig {
private enum class Group(override val displayName: String) : NamedEnum {
General("General"),
@@ -40,78 +41,78 @@ open class BreakSettings(
}
// General
- override val breakMode by c.setting("Break Mode", BreakMode.Packet).group(baseGroup, Group.General).index()
- override val sorter by c.setting("Break Sorter", ActionConfig.SortMode.Tool, "The order in which breaks are performed").group(baseGroup, Group.General).index()
- override val rebreak by c.setting("Rebreak", true, "Re-breaks blocks after they've been broken once").group(baseGroup, Group.General).index()
+ override val breakMode by c.setting("Break Mode", BreakMode.Packet, visibility = visibility).group(baseGroup, Group.General).index()
+ override val sorter by c.setting("Break Sorter", ActionConfig.SortMode.Tool, "The order in which breaks are performed", visibility = visibility).group(baseGroup, Group.General).index()
+ override val rebreak by c.setting("Rebreak", true, "Re-breaks blocks after they've been broken once", visibility = visibility).group(baseGroup, Group.General).index()
// Double break
- override val doubleBreak by c.setting("Double Break", true, "Allows breaking two blocks at once").group(baseGroup, Group.General).index()
- override val unsafeCancels by c.setting("Unsafe Cancels", true, "Allows cancelling block breaking even if the server might continue breaking sever side, potentially causing unexpected state changes") { doubleBreak }.group(baseGroup, Group.General).index()
+ override val doubleBreak by c.setting("Double Break", true, "Allows breaking two blocks at once", visibility = visibility).group(baseGroup, Group.General).index()
+ override val unsafeCancels by c.setting("Unsafe Cancels", true, "Allows cancelling block breaking even if the server might continue breaking sever side, potentially causing unexpected state changes") { visibility() && doubleBreak }.group(baseGroup, Group.General).index()
// Fixes / Delays
- override val breakThreshold by c.setting("Break Threshold", 0.70f, 0.1f..1.0f, 0.01f, "The break amount at which the block is considered broken").group(baseGroup, Group.General).index()
- override val fudgeFactor by c.setting("Fudge Factor", 1, 0..5, 1, "The number of ticks to add to the break time, usually to account for server lag").group(baseGroup, Group.General).index()
- override val serverSwapTicks by c.setting("Server Swap", 0, 0..5, 1, "The number of ticks to give the server time to recognize the player attributes on the swapped item", " tick(s)").group(baseGroup, Group.General).index()
+ override val breakThreshold by c.setting("Break Threshold", 0.70f, 0.1f..1.0f, 0.01f, "The break amount at which the block is considered broken", visibility = visibility).group(baseGroup, Group.General).index()
+ override val fudgeFactor by c.setting("Fudge Factor", 1, 0..5, 1, "The number of ticks to add to the break time, usually to account for server lag", visibility = visibility).group(baseGroup, Group.General).index()
+ override val serverSwapTicks by c.setting("Server Swap", 0, 0..5, 1, "The number of ticks to give the server time to recognize the player attributes on the swapped item", " tick(s)", visibility = visibility).group(baseGroup, Group.General).index()
// override val desyncFix by c.setting("Desync Fix", false, "Predicts if the players breaking will be slowed next tick as block break packets are processed using the players next position") { vis() && page == Page.General }
- override val breakDelay by c.setting("Break Delay", 0, 0..6, 1, "The delay between breaking blocks", " tick(s)").group(baseGroup, Group.General).index()
+ override val breakDelay by c.setting("Break Delay", 0, 0..6, 1, "The delay between breaking blocks", " tick(s)", visibility = visibility).group(baseGroup, Group.General).index()
// Timing
- override val tickStageMask by c.setting("Break Stage Mask", setOf(TickEvent.Input.Post), ALL_STAGES.toSet(), "The sub-tick timing at which break actions can be performed", displayClassName = true).group(baseGroup, Group.General).index()
+ override val tickStageMask by c.setting("Break Stage Mask", setOf(TickEvent.Input.Post), ALL_STAGES.toSet(), "The sub-tick timing at which break actions can be performed", displayClassName = true, visibility = visibility).group(baseGroup, Group.General).index()
// Swap
- override val swapMode by c.setting("Break Swap Mode", BreakConfig.SwapMode.End, "Decides when to swap to the best suited tool when breaking a block").group(baseGroup, Group.General).index()
+ override val swapMode by c.setting("Break Swap Mode", BreakConfig.SwapMode.End, "Decides when to swap to the best suited tool when breaking a block", visibility = visibility).group(baseGroup, Group.General).index()
// Swing
- override val swing by c.setting("Swing Mode", SwingMode.Constant, "The times at which to swing the players hand").group(baseGroup, Group.General).index()
- override val swingType by c.setting("Break Swing Type", BuildConfig.SwingType.Vanilla, "The style of swing") { swing != SwingMode.None }.group(baseGroup, Group.General).index()
+ override val swing by c.setting("Swing Mode", SwingMode.Constant, "The times at which to swing the players hand", visibility = visibility).group(baseGroup, Group.General).index()
+ override val swingType by c.setting("Break Swing Type", BuildConfig.SwingType.Vanilla, "The style of swing") { visibility() && swing != SwingMode.None }.group(baseGroup, Group.General).index()
// Rotate
- override val rotate by c.setting("Rotate For Break", false, "Rotate towards block while breaking").group(baseGroup, Group.General).index()
+ override val rotate by c.setting("Rotate For Break", false, "Rotate towards block while breaking", visibility = visibility).group(baseGroup, Group.General).index()
// Pending / Post
- override val breakConfirmation by c.setting("Break Confirmation", BreakConfirmationMode.BreakThenAwait, "The style of confirmation used when breaking").group(baseGroup, Group.General).index()
- override val breaksPerTick by c.setting("Breaks Per Tick", 5, 1..30, 1, "Maximum instant block breaks per tick").group(baseGroup, Group.General).index()
+ override val breakConfirmation by c.setting("Break Confirmation", BreakConfirmationMode.BreakThenAwait, "The style of confirmation used when breaking", visibility = visibility).group(baseGroup, Group.General).index()
+ override val breaksPerTick by c.setting("Breaks Per Tick", 5, 1..30, 1, "Maximum instant block breaks per tick", visibility = visibility).group(baseGroup, Group.General).index()
// Block
- override val ignoredBlocks by c.setting("Ignored Blocks", emptySet(), description = "Blocks that wont be broken").group(baseGroup, Group.General).index()
- override val avoidLiquids by c.setting("Avoid Liquids", true, "Avoids breaking blocks that would cause liquid to spill").group(baseGroup, Group.General).index()
- override val avoidSupporting by c.setting("Avoid Supporting", true, "Avoids breaking the block supporting the player").group(baseGroup, Group.General).index()
+ override val ignoredBlocks by c.setting("Ignored Blocks", emptySet(), description = "Blocks that wont be broken", visibility = visibility).group(baseGroup, Group.General).index()
+ override val avoidLiquids by c.setting("Avoid Liquids", true, "Avoids breaking blocks that would cause liquid to spill", visibility = visibility).group(baseGroup, Group.General).index()
+ override val avoidSupporting by c.setting("Avoid Supporting", true, "Avoids breaking the block supporting the player", visibility = visibility).group(baseGroup, Group.General).index()
// Tool
- override val efficientOnly by c.setting("Efficient Tools Only", true, "Only use tools suitable for the given block (will get the item drop)") { swapMode.isEnabled() }.group(baseGroup, Group.General).index()
- override val suitableToolsOnly by c.setting("Suitable Tools Only", true, "Only use tools suitable for the given block (will get the item drop)") { swapMode.isEnabled() }.group(baseGroup, Group.General).index()
- override val forceSilkTouch by c.setting("Force Silk Touch", false, "Force silk touch when breaking blocks") { swapMode.isEnabled() }.group(baseGroup, Group.General).index()
- override val forceFortunePickaxe by c.setting("Force Fortune Pickaxe", false, "Force fortune pickaxe when breaking blocks") { swapMode.isEnabled() }.group(baseGroup, Group.General).index()
- override val minFortuneLevel by c.setting("Min Fortune Level", 1, 1..3, 1, "The minimum fortune level to use") { swapMode.isEnabled() && forceFortunePickaxe }.group(baseGroup, Group.General).index()
- override val useWoodenTools by c.setting("Use Wooden Tools", true, "Use wooden tools when breaking blocks") { swapMode.isEnabled() }.group(baseGroup, Group.General).index()
- override val useStoneTools by c.setting("Use Stone Tools", true, "Use stone tools when breaking blocks") { swapMode.isEnabled() }.group(baseGroup, Group.General).index()
- override val useIronTools by c.setting("Use Iron Tools", true, "Use iron tools when breaking blocks") { swapMode.isEnabled() }.group(baseGroup, Group.General).index()
- override val useDiamondTools by c.setting("Use Diamond Tools", true, "Use diamond tools when breaking blocks") { swapMode.isEnabled() }.group(baseGroup, Group.General).index()
- override val useGoldTools by c.setting("Use Gold Tools", true, "Use gold tools when breaking blocks") { swapMode.isEnabled() }.group(baseGroup, Group.General).index()
- override val useNetheriteTools by c.setting("Use Netherite Tools", true, "Use netherite tools when breaking blocks") { swapMode.isEnabled() }.group(baseGroup, Group.General).index()
+ override val efficientOnly by c.setting("Efficient Tools Only", true, "Only use tools suitable for the given block (will get the item drop)") { visibility() && swapMode.isEnabled() }.group(baseGroup, Group.General).index()
+ override val suitableToolsOnly by c.setting("Suitable Tools Only", true, "Only use tools suitable for the given block (will get the item drop)") { visibility() && swapMode.isEnabled() }.group(baseGroup, Group.General).index()
+ override val forceSilkTouch by c.setting("Force Silk Touch", false, "Force silk touch when breaking blocks") { visibility() && swapMode.isEnabled() }.group(baseGroup, Group.General).index()
+ override val forceFortunePickaxe by c.setting("Force Fortune Pickaxe", false, "Force fortune pickaxe when breaking blocks") { visibility() && swapMode.isEnabled() }.group(baseGroup, Group.General).index()
+ override val minFortuneLevel by c.setting("Min Fortune Level", 1, 1..3, 1, "The minimum fortune level to use") { visibility() && swapMode.isEnabled() && forceFortunePickaxe }.group(baseGroup, Group.General).index()
+ override val useWoodenTools by c.setting("Use Wooden Tools", true, "Use wooden tools when breaking blocks") { visibility() && swapMode.isEnabled() }.group(baseGroup, Group.General).index()
+ override val useStoneTools by c.setting("Use Stone Tools", true, "Use stone tools when breaking blocks") { visibility() && swapMode.isEnabled() }.group(baseGroup, Group.General).index()
+ override val useIronTools by c.setting("Use Iron Tools", true, "Use iron tools when breaking blocks") { visibility() && swapMode.isEnabled() }.group(baseGroup, Group.General).index()
+ override val useDiamondTools by c.setting("Use Diamond Tools", true, "Use diamond tools when breaking blocks") { visibility() && swapMode.isEnabled() }.group(baseGroup, Group.General).index()
+ override val useGoldTools by c.setting("Use Gold Tools", true, "Use gold tools when breaking blocks") { visibility() && swapMode.isEnabled() }.group(baseGroup, Group.General).index()
+ override val useNetheriteTools by c.setting("Use Netherite Tools", true, "Use netherite tools when breaking blocks") { visibility() && swapMode.isEnabled() }.group(baseGroup, Group.General).index()
// Cosmetics
- override val sounds by c.setting("Break Sounds", true, "Plays the breaking sounds").group(baseGroup, Group.Cosmetic).index()
- override val particles by c.setting("Particles", true, "Renders the breaking particles").group(baseGroup, Group.Cosmetic).index()
- override val breakingTexture by c.setting("Breaking Overlay", true, "Overlays the breaking texture at its different stages").group(baseGroup, Group.Cosmetic).index()
+ override val sounds by c.setting("Break Sounds", true, "Plays the breaking sounds", visibility = visibility).group(baseGroup, Group.Cosmetic).index()
+ override val particles by c.setting("Particles", true, "Renders the breaking particles", visibility = visibility).group(baseGroup, Group.Cosmetic).index()
+ override val breakingTexture by c.setting("Breaking Overlay", true, "Overlays the breaking texture at its different stages", visibility = visibility).group(baseGroup, Group.Cosmetic).index()
// Modes
- override val renders by c.setting("Renders", true, "Enables the render settings for breaking progress").group(baseGroup, Group.Cosmetic).index()
- override val animation by c.setting("Animation", AnimationMode.Out, "The style of animation used for the box") { renders }.group(baseGroup, Group.Cosmetic).index()
+ override val renders by c.setting("Renders", true, "Enables the render settings for breaking progress", visibility = visibility).group(baseGroup, Group.Cosmetic).index()
+ override val animation by c.setting("Animation", AnimationMode.Out, "The style of animation used for the box") { visibility() && renders }.group(baseGroup, Group.Cosmetic).index()
// Fill
- override val fill by c.setting("Fill", true, "Renders the sides of the box to display break progress") { renders }.group(baseGroup, Group.Cosmetic).index()
- override val dynamicFillColor by c.setting("Dynamic Colour", true, "Enables fill color interpolation from start to finish for fill when breaking a block") { renders && fill }.group(baseGroup, Group.Cosmetic).index()
- override val staticFillColor by c.setting("Fill Color", Color(255, 0, 0, 60).brighter(), "The color of the fill") { renders && !dynamicFillColor && fill }.group(baseGroup, Group.Cosmetic).index()
- override val startFillColor by c.setting("Start Fill Color", Color(255, 0, 0, 60).brighter(), "The color of the fill at the start of breaking") { renders && dynamicFillColor && fill }.group(baseGroup, Group.Cosmetic).index()
- override val endFillColor by c.setting("End Fill Color", Color(0, 255, 0, 60).brighter(), "The color of the fill at the end of breaking") { renders && dynamicFillColor && fill }.group(baseGroup, Group.Cosmetic).index()
+ override val fill by c.setting("Fill", true, "Renders the sides of the box to display break progress") { visibility() && renders }.group(baseGroup, Group.Cosmetic).index()
+ override val dynamicFillColor by c.setting("Dynamic Colour", true, "Enables fill color interpolation from start to finish for fill when breaking a block") { visibility() && renders && fill }.group(baseGroup, Group.Cosmetic).index()
+ override val staticFillColor by c.setting("Fill Color", Color(255, 0, 0, 60).brighter(), "The color of the fill") { visibility() && renders && !dynamicFillColor && fill }.group(baseGroup, Group.Cosmetic).index()
+ override val startFillColor by c.setting("Start Fill Color", Color(255, 0, 0, 60).brighter(), "The color of the fill at the start of breaking") { visibility() && renders && dynamicFillColor && fill }.group(baseGroup, Group.Cosmetic).index()
+ override val endFillColor by c.setting("End Fill Color", Color(0, 255, 0, 60).brighter(), "The color of the fill at the end of breaking") { visibility() && renders && dynamicFillColor && fill }.group(baseGroup, Group.Cosmetic).index()
// Outline
- override val outline by c.setting("Outline", true, "Renders the lines of the box to display break progress") { renders }.group(baseGroup, Group.Cosmetic).index()
- override val outlineWidth by c.setting("Outline Width", 2, 0..5, 1, "The width of the outline") { renders && outline }.group(baseGroup, Group.Cosmetic).index()
- override val dynamicOutlineColor by c.setting("Dynamic Outline Color", true, "Enables color interpolation from start to finish for the outline when breaking a block") { renders && outline }.group(baseGroup, Group.Cosmetic).index()
- override val staticOutlineColor by c.setting("Outline Color", Color.RED.brighter(), "The Color of the outline at the start of breaking") { renders && !dynamicOutlineColor && outline }.group(baseGroup, Group.Cosmetic).index()
- override val startOutlineColor by c.setting("Start Outline Color", Color.RED.brighter(), "The color of the outline at the start of breaking") { renders && dynamicOutlineColor && outline }.group(baseGroup, Group.Cosmetic).index()
- override val endOutlineColor by c.setting("End Outline Color", Color.GREEN.brighter(), "The color of the outline at the end of breaking") { renders && dynamicOutlineColor && outline }.group(baseGroup, Group.Cosmetic).index()
+ override val outline by c.setting("Outline", true, "Renders the lines of the box to display break progress") { visibility() && renders }.group(baseGroup, Group.Cosmetic).index()
+ override val outlineWidth by c.setting("Outline Width", 2f, 0f..10f, 0.1f, "The width of the outline") { visibility() && renders && outline }.group(baseGroup, Group.Cosmetic).index()
+ override val dynamicOutlineColor by c.setting("Dynamic Outline Color", true, "Enables color interpolation from start to finish for the outline when breaking a block") { visibility() && renders && outline }.group(baseGroup, Group.Cosmetic).index()
+ override val staticOutlineColor by c.setting("Outline Color", Color.RED.brighter(), "The Color of the outline at the start of breaking") { visibility() && renders && !dynamicOutlineColor && outline }.group(baseGroup, Group.Cosmetic).index()
+ override val startOutlineColor by c.setting("Start Outline Color", Color.RED.brighter(), "The color of the outline at the start of breaking") { visibility() && renders && dynamicOutlineColor && outline }.group(baseGroup, Group.Cosmetic).index()
+ override val endOutlineColor by c.setting("End Outline Color", Color.GREEN.brighter(), "The color of the outline at the end of breaking") { visibility() && renders && dynamicOutlineColor && outline }.group(baseGroup, Group.Cosmetic).index()
}
diff --git a/src/main/kotlin/com/lambda/config/groups/BuildSettings.kt b/src/main/kotlin/com/lambda/config/groups/BuildSettings.kt
index e1fa36913..41d8bb19c 100644
--- a/src/main/kotlin/com/lambda/config/groups/BuildSettings.kt
+++ b/src/main/kotlin/com/lambda/config/groups/BuildSettings.kt
@@ -26,6 +26,7 @@ import kotlin.math.max
class BuildSettings(
c: Configurable,
baseGroup: NamedEnum,
+ override val visibility: () -> Boolean = { true },
) : SettingGroup(c), BuildConfig {
enum class Group(override val displayName: String) : NamedEnum {
General("General"),
@@ -33,23 +34,23 @@ class BuildSettings(
Scan("Scan")
}
- override val breakBlocks by c.setting("Break", true, "Break blocks").group(baseGroup, Group.General).index()
- override val interactBlocks by c.setting("Place / Interact", true, "Interact blocks").group(baseGroup, Group.General).index()
+ override val breakBlocks by c.setting("Break", true, "Break blocks", visibility = visibility).group(baseGroup, Group.General).index()
+ override val interactBlocks by c.setting("Place / Interact", true, "Interact blocks", visibility = visibility).group(baseGroup, Group.General).index()
- override val pathing by c.setting("Pathing", false, "Path to blocks").group(baseGroup, Group.General).index()
- override val stayInRange by c.setting("Stay In Range", false, "Stay in range of blocks").group(baseGroup, Group.General).index()
- override val collectDrops by c.setting("Collect All Drops", false, "Collect all drops when breaking blocks").group(baseGroup, Group.General).index()
- override val spleefEntities by c.setting("Spleef Entities", false, "Breaks blocks beneath entities blocking placements to get them out of the way").group(baseGroup, Group.General).index()
- override val maxPendingActions by c.setting("Max Pending Actions", 15, 1..30, 1, "The maximum count of pending interactions to allow before pausing future interactions").group(baseGroup, Group.General).index()
- override val actionTimeout by c.setting("Action Timeout", 10, 1..30, 1, "Timeout for block breaks in ticks", unit = " ticks").group(baseGroup, Group.General).index()
- override val maxBuildDependencies by c.setting("Max Sim Dependencies", 3, 0..10, 1, "Maximum dependency build results").group(baseGroup, Group.General).index()
+ override val pathing by c.setting("Pathing", false, "Path to blocks", visibility = visibility).group(baseGroup, Group.General).index()
+ override val stayInRange by c.setting("Stay In Range", false, "Stay in range of blocks", visibility = visibility).group(baseGroup, Group.General).index()
+ override val collectDrops by c.setting("Collect All Drops", false, "Collect all drops when breaking blocks", visibility = visibility).group(baseGroup, Group.General).index()
+ override val spleefEntities by c.setting("Spleef Entities", false, "Breaks blocks beneath entities blocking placements to get them out of the way", visibility = visibility).group(baseGroup, Group.General).index()
+ override val maxPendingActions by c.setting("Max Pending Actions", 15, 1..30, 1, "The maximum count of pending interactions to allow before pausing future interactions", visibility = visibility).group(baseGroup, Group.General).index()
+ override val actionTimeout by c.setting("Action Timeout", 10, 1..30, 1, "Timeout for block breaks in ticks", unit = " ticks", visibility = visibility).group(baseGroup, Group.General).index()
+ override val maxBuildDependencies by c.setting("Max Sim Dependencies", 3, 0..10, 1, "Maximum dependency build results", visibility = visibility).group(baseGroup, Group.General).index()
- override var blockReach by c.setting("Interact Reach", 4.5, 1.0..7.0, 0.01, "Maximum block interaction distance").group(baseGroup, Group.Reach).index()
- override var entityReach by c.setting("Attack Reach", 3.0, 1.0..7.0, 0.01, "Maximum entity interaction distance").group(baseGroup, Group.Reach).index()
+ override var blockReach by c.setting("Interact Reach", 4.5, 1.0..7.0, 0.01, "Maximum block interaction distance", visibility = visibility).group(baseGroup, Group.Reach).index()
+ override var entityReach by c.setting("Attack Reach", 3.0, 1.0..7.0, 0.01, "Maximum entity interaction distance", visibility = visibility).group(baseGroup, Group.Reach).index()
override val scanReach: Double get() = max(entityReach, blockReach)
- override val checkSideVisibility by c.setting("Visibility Check", true, "Whether to check if an AABB side is visible").group(baseGroup, Group.Scan).index()
- override val strictRayCast by c.setting("Strict Raycast", false, "Whether to include the environment to the ray cast context").group(baseGroup, Group.Scan).index()
- override val resolution by c.setting("Resolution", 5, 1..20, 1, "The amount of grid divisions per surface of the hit box", "") { strictRayCast }.group(baseGroup, Group.Scan).index()
- override val pointSelection by c.setting("Point Selection", PointSelection.Optimum, "The strategy to select the best hit point").group(baseGroup, Group.Scan).index()
+ override val checkSideVisibility by c.setting("Visibility Check", true, "Whether to check if an AABB side is visible", visibility = visibility).group(baseGroup, Group.Scan).index()
+ override val strictRayCast by c.setting("Strict Raycast", false, "Whether to include the environment to the ray cast context", visibility = visibility).group(baseGroup, Group.Scan).index()
+ override val resolution by c.setting("Resolution", 5, 1..20, 1, "The amount of grid divisions per surface of the hit box", "") { visibility() && strictRayCast }.group(baseGroup, Group.Scan).index()
+ override val pointSelection by c.setting("Point Selection", PointSelection.Optimum, "The strategy to select the best hit point", visibility = visibility).group(baseGroup, Group.Scan).index()
}
diff --git a/src/main/kotlin/com/lambda/config/groups/EatSettings.kt b/src/main/kotlin/com/lambda/config/groups/EatSettings.kt
index 28a7aab51..62f4230ce 100644
--- a/src/main/kotlin/com/lambda/config/groups/EatSettings.kt
+++ b/src/main/kotlin/com/lambda/config/groups/EatSettings.kt
@@ -24,23 +24,24 @@ import net.minecraft.item.Items
class EatSettings(
c: Configurable,
- baseGroup: NamedEnum
+ baseGroup: NamedEnum,
+ override val visibility: () -> Boolean = { true },
) : SettingGroup(c), EatConfig {
val nutritiousFoodDefaults = listOf(Items.APPLE, Items.BAKED_POTATO, Items.BEEF, Items.BEETROOT, Items.BEETROOT_SOUP, Items.BREAD, Items.CARROT, Items.CHICKEN, Items.CHORUS_FRUIT, Items.COD, Items.COOKED_BEEF, Items.COOKED_CHICKEN, Items.COOKED_COD, Items.COOKED_MUTTON, Items.COOKED_PORKCHOP, Items.COOKED_RABBIT, Items.COOKED_SALMON, Items.COOKIE, Items.DRIED_KELP, Items.ENCHANTED_GOLDEN_APPLE, Items.GOLDEN_APPLE, Items.GOLDEN_CARROT, Items.HONEY_BOTTLE, Items.MELON_SLICE, Items.MUSHROOM_STEW, Items.MUTTON, Items.POISONOUS_POTATO, Items.PORKCHOP, Items.POTATO, Items.PUFFERFISH, Items.PUMPKIN_PIE, Items.RABBIT, Items.RABBIT_STEW, Items.ROTTEN_FLESH, Items.SALMON, Items.SPIDER_EYE, Items.SUSPICIOUS_STEW, Items.SWEET_BERRIES, Items.GLOW_BERRIES, Items.TROPICAL_FISH)
val resistanceFoodDefaults = listOf(Items.ENCHANTED_GOLDEN_APPLE)
val regenerationFoodDefaults = listOf(Items.ENCHANTED_GOLDEN_APPLE, Items.GOLDEN_APPLE)
val negativeFoodDefaults = listOf(Items.CHICKEN, Items.POISONOUS_POTATO, Items.PUFFERFISH, Items.ROTTEN_FLESH, Items.SPIDER_EYE)
- override val eatOnHunger by c.setting("Eat On Hunger", true, "Whether to eat when hungry").group(baseGroup).index()
- override val minFoodLevel by c.setting("Minimum Food Level", 6, 0..20, 1, "The minimum food level to eat food", " food level") { eatOnHunger }.group(baseGroup).index()
- override val saturated by c.setting("Saturated", EatConfig.Saturation.EatSmart, "When to stop eating") { eatOnHunger }.group(baseGroup).index()
- override val nutritiousFood by c.setting("Nutritious Food", nutritiousFoodDefaults, nutritiousFoodDefaults, "Items that are be considered nutritious") { eatOnHunger }.group(baseGroup).index()
- override val selectionPriority by c.setting("Selection Priority", EatConfig.SelectionPriority.MostNutritious, "The priority for selecting food items") { eatOnHunger }.group(baseGroup).index()
- override val eatOnFire by c.setting("Eat On Fire", true, "Whether to eat when on fire").group(baseGroup).index()
- override val resistanceFood by c.setting("Resistance Food", resistanceFoodDefaults, resistanceFoodDefaults, "Items that give Fire Resistance") { eatOnFire }.group(baseGroup).index()
- override val eatOnDamage by c.setting("Eat On Damage", true, "Whether to eat when damaged").group(baseGroup).index()
- override val minDamage by c.setting("Minimum Damage", 10, 0..20, 1, "The minimum damage threshold to trigger eating") { eatOnDamage }.group(baseGroup).index()
- override val regenerationFood by c.setting("Regeneration Food", regenerationFoodDefaults, regenerationFoodDefaults, "Items that give Regeneration") { eatOnDamage }.group(baseGroup).index()
- override val ignoreBadFood by c.setting("Ignore Bad Food", true, "Whether to eat when the food is bad").group(baseGroup).index()
- override val badFood by c.setting("Bad Food", negativeFoodDefaults, negativeFoodDefaults, "Items that are considered bad food") { ignoreBadFood }.group(baseGroup).index()
+ override val eatOnHunger by c.setting("Eat On Hunger", true, "Whether to eat when hungry", visibility = visibility).group(baseGroup).index()
+ override val minFoodLevel by c.setting("Minimum Food Level", 6, 0..20, 1, "The minimum food level to eat food", " food level") { visibility() && eatOnHunger }.group(baseGroup).index()
+ override val saturated by c.setting("Saturated", EatConfig.Saturation.EatSmart, "When to stop eating") { visibility() && eatOnHunger }.group(baseGroup).index()
+ override val nutritiousFood by c.setting("Nutritious Food", nutritiousFoodDefaults, nutritiousFoodDefaults, "Items that are be considered nutritious") { visibility() && eatOnHunger }.group(baseGroup).index()
+ override val selectionPriority by c.setting("Selection Priority", EatConfig.SelectionPriority.MostNutritious, "The priority for selecting food items") { visibility() && eatOnHunger }.group(baseGroup).index()
+ override val eatOnFire by c.setting("Eat On Fire", true, "Whether to eat when on fire", visibility = visibility).group(baseGroup).index()
+ override val resistanceFood by c.setting("Resistance Food", resistanceFoodDefaults, resistanceFoodDefaults, "Items that give Fire Resistance") { visibility() && eatOnFire }.group(baseGroup).index()
+ override val eatOnDamage by c.setting("Eat On Damage", true, "Whether to eat when damaged", visibility = visibility).group(baseGroup).index()
+ override val minDamage by c.setting("Minimum Damage", 10, 0..20, 1, "The minimum damage threshold to trigger eating") { visibility() && eatOnDamage }.group(baseGroup).index()
+ override val regenerationFood by c.setting("Regeneration Food", regenerationFoodDefaults, regenerationFoodDefaults, "Items that give Regeneration") { visibility() && eatOnDamage }.group(baseGroup).index()
+ override val ignoreBadFood by c.setting("Ignore Bad Food", true, "Whether to eat when the food is bad", visibility = visibility).group(baseGroup).index()
+ override val badFood by c.setting("Bad Food", negativeFoodDefaults, negativeFoodDefaults, "Items that are considered bad food") { visibility() && ignoreBadFood }.group(baseGroup).index()
}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/config/groups/FormatterSettings.kt b/src/main/kotlin/com/lambda/config/groups/FormatterSettings.kt
index 1d53a3d72..7ce90e2e0 100644
--- a/src/main/kotlin/com/lambda/config/groups/FormatterSettings.kt
+++ b/src/main/kotlin/com/lambda/config/groups/FormatterSettings.kt
@@ -24,21 +24,22 @@ import com.lambda.util.NamedEnum
class FormatterSettings(
c: Configurable,
vararg baseGroup: NamedEnum,
+ override val visibility: () -> Boolean = { true },
) : FormatterConfig, SettingGroup(c) {
- val localeEnum by c.setting("Locale", FormatterConfig.Locales.US, "The regional formatting used for numbers").group(*baseGroup).index()
+ val localeEnum by c.setting("Locale", FormatterConfig.Locales.US, "The regional formatting used for numbers", visibility = visibility).group(*baseGroup).index()
override val locale get() = localeEnum.locale
- val sep by c.setting("Separator", FormatterConfig.TupleSeparator.Comma, "Separator for string serialization of tuple data structures").group(*baseGroup).index()
- val customSep by c.setting("Custom Separator", "") { sep == FormatterConfig.TupleSeparator.Custom }.group(*baseGroup).index()
+ val sep by c.setting("Separator", FormatterConfig.TupleSeparator.Comma, "Separator for string serialization of tuple data structures", visibility = visibility).group(*baseGroup).index()
+ val customSep by c.setting("Custom Separator", "") { visibility() && sep == FormatterConfig.TupleSeparator.Custom }.group(*baseGroup).index()
override val separator get() = if (sep == FormatterConfig.TupleSeparator.Custom) customSep else sep.separator
- val group by c.setting("Tuple Prefix", FormatterConfig.TupleGrouping.Parentheses).group(*baseGroup).index()
+ val group by c.setting("Tuple Prefix", FormatterConfig.TupleGrouping.Parentheses, visibility = visibility).group(*baseGroup).index()
override val prefix get() = group.prefix
override val postfix get() = group.postfix
- val floatingPrecision by c.setting("Floating Precision", 3, 0..6, 1, "Precision for floating point numbers").group(*baseGroup).index()
+ val floatingPrecision by c.setting("Floating Precision", 3, 0..6, 1, "Precision for floating point numbers", visibility = visibility).group(*baseGroup).index()
override val precision get() = floatingPrecision
- val timeFormat by c.setting("Time Format", FormatterConfig.Time.IsoDateTime).group(*baseGroup).index()
+ val timeFormat by c.setting("Time Format", FormatterConfig.Time.IsoDateTime, visibility = visibility).group(*baseGroup).index()
override val format get() = timeFormat.format
}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/config/groups/HotbarSettings.kt b/src/main/kotlin/com/lambda/config/groups/HotbarSettings.kt
index 34baab4e8..3601e5dad 100644
--- a/src/main/kotlin/com/lambda/config/groups/HotbarSettings.kt
+++ b/src/main/kotlin/com/lambda/config/groups/HotbarSettings.kt
@@ -26,12 +26,13 @@ import com.lambda.util.NamedEnum
class HotbarSettings(
c: Configurable,
- baseGroup: NamedEnum
+ baseGroup: NamedEnum,
+ override val visibility: () -> Boolean = { true },
) : SettingGroup(c), HotbarConfig {
- override val swapMode by c.setting("Swap Mode", HotbarConfig.SwapMode.Temporary).group(baseGroup).index()
- override val keepTicks by c.setting("Keep Ticks", 1, 0..20, 1, "The number of ticks to keep the current hotbar selection active", " ticks") { swapMode == HotbarConfig.SwapMode.Temporary }.group(baseGroup).index()
- override val swapDelay by c.setting("Swap Delay", 0, 0..3, 1, "The number of ticks delay before allowing another hotbar selection swap", " ticks").group(baseGroup).index()
- override val swapsPerTick by c.setting("Swaps Per Tick", 3, 1..10, 1, "The number of hotbar selection swaps that can take place each tick") { swapDelay <= 0 }.group(baseGroup).index()
- override val swapPause by c.setting("Swap Pause", 0, 0..20, 1, "The delay in ticks to pause actions after switching to the slot", " ticks").group(baseGroup).index()
- override val tickStageMask by c.setting("Hotbar Stage Mask", setOf(TickEvent.Input.Post), ALL_STAGES.toSet(), "The sub-tick timing at which hotbar actions are performed", displayClassName = true).group(baseGroup).index()
+ override val swapMode by c.setting("Swap Mode", HotbarConfig.SwapMode.Temporary, visibility = visibility).group(baseGroup).index()
+ override val keepTicks by c.setting("Keep Ticks", 1, 0..20, 1, "The number of ticks to keep the current hotbar selection active", " ticks") { visibility() && swapMode == HotbarConfig.SwapMode.Temporary }.group(baseGroup).index()
+ override val swapDelay by c.setting("Swap Delay", 0, 0..3, 1, "The number of ticks delay before allowing another hotbar selection swap", " ticks", visibility = visibility).group(baseGroup).index()
+ override val swapsPerTick by c.setting("Swaps Per Tick", 3, 1..10, 1, "The number of hotbar selection swaps that can take place each tick") { visibility() && swapDelay <= 0 }.group(baseGroup).index()
+ override val swapPause by c.setting("Swap Pause", 0, 0..20, 1, "The delay in ticks to pause actions after switching to the slot", " ticks", visibility = visibility).group(baseGroup).index()
+ override val tickStageMask by c.setting("Hotbar Stage Mask", setOf(TickEvent.Input.Post), ALL_STAGES.toSet(), "The sub-tick timing at which hotbar actions are performed", displayClassName = true, visibility = visibility).group(baseGroup).index()
}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/config/groups/InteractSettings.kt b/src/main/kotlin/com/lambda/config/groups/InteractSettings.kt
index 90fc0b8be..36d27a17f 100644
--- a/src/main/kotlin/com/lambda/config/groups/InteractSettings.kt
+++ b/src/main/kotlin/com/lambda/config/groups/InteractSettings.kt
@@ -28,17 +28,18 @@ import com.lambda.util.NamedEnum
class InteractSettings(
c: Configurable,
- baseGroup: NamedEnum
+ baseGroup: NamedEnum,
+ override val visibility: () -> Boolean = { true },
) : SettingGroup(c), InteractConfig {
- override val rotate by c.setting("Rotate For Interact", true, "Rotate towards block while placing").group(baseGroup).index()
- override val airPlace by c.setting("Air Place", AirPlaceMode.None, "Allows for placing blocks without adjacent faces").group(baseGroup).index()
- override val axisRotateSetting by c.setting("Axis Rotate", true, "Overrides the Rotate For Place setting and rotates the player on each axis to air place rotational blocks") { airPlace.isEnabled }.group(baseGroup).index()
- override val sorter by c.setting("Interaction Sorter", ActionConfig.SortMode.Tool, "The order in which placements are performed").group(baseGroup).index()
- override val tickStageMask by c.setting("Interaction Stage Mask", setOf(TickEvent.Input.Post), ALL_STAGES.toSet(), "The sub-tick timing at which place actions are performed", displayClassName = true).group(baseGroup).index()
- override val interactConfirmationMode by c.setting("Interact Confirmation", InteractConfirmationMode.PlaceThenAwait, "Wait for block placement confirmation").group(baseGroup).index()
- override val interactDelay by c.setting("Interact Delay", 0, 0..3, 1, "Tick delay between interacting with another block").group(baseGroup).index()
- override val interactionsPerTick by c.setting("Interactions Per Tick", 1, 1..30, 1, "Maximum instant block places per tick").group(baseGroup).index()
- override val swing by c.setting("Swing On Interact", true, "Swings the players hand when placing").group(baseGroup).index()
- override val swingType by c.setting("Interact Swing Type", BuildConfig.SwingType.Vanilla, "The style of swing") { swing }.group(baseGroup).index()
- override val sounds by c.setting("Place Sounds", true, "Plays the placing sounds").group(baseGroup).index()
+ override val rotate by c.setting("Rotate For Interact", true, "Rotate towards block while placing", visibility = visibility).group(baseGroup).index()
+ override val airPlace by c.setting("Air Place", AirPlaceMode.None, "Allows for placing blocks without adjacent faces", visibility = visibility).group(baseGroup).index()
+ override val axisRotateSetting by c.setting("Axis Rotate", true, "Overrides the Rotate For Place setting and rotates the player on each axis to air place rotational blocks") { visibility() && airPlace.isEnabled }.group(baseGroup).index()
+ override val sorter by c.setting("Interaction Sorter", ActionConfig.SortMode.Tool, "The order in which placements are performed", visibility = visibility).group(baseGroup).index()
+ override val tickStageMask by c.setting("Interaction Stage Mask", setOf(TickEvent.Input.Post), ALL_STAGES.toSet(), "The sub-tick timing at which place actions are performed", displayClassName = true, visibility = visibility).group(baseGroup).index()
+ override val interactConfirmationMode by c.setting("Interact Confirmation", InteractConfirmationMode.PlaceThenAwait, "Wait for block placement confirmation", visibility = visibility).group(baseGroup).index()
+ override val interactDelay by c.setting("Interact Delay", 0, 0..3, 1, "Tick delay between interacting with another block", visibility = visibility).group(baseGroup).index()
+ override val interactionsPerTick by c.setting("Interactions Per Tick", 1, 1..30, 1, "Maximum instant block places per tick", visibility = visibility).group(baseGroup).index()
+ override val swing by c.setting("Swing On Interact", true, "Swings the players hand when placing", visibility = visibility).group(baseGroup).index()
+ override val swingType by c.setting("Interact Swing Type", BuildConfig.SwingType.Vanilla, "The style of swing") { visibility() && swing }.group(baseGroup).index()
+ override val sounds by c.setting("Place Sounds", true, "Plays the placing sounds", visibility = visibility).group(baseGroup).index()
}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/config/groups/InventorySettings.kt b/src/main/kotlin/com/lambda/config/groups/InventorySettings.kt
index 2dc25e38a..9fd601406 100644
--- a/src/main/kotlin/com/lambda/config/groups/InventorySettings.kt
+++ b/src/main/kotlin/com/lambda/config/groups/InventorySettings.kt
@@ -26,7 +26,8 @@ import com.lambda.util.item.ItemUtils
class InventorySettings(
c: Configurable,
- baseGroup: NamedEnum
+ baseGroup: NamedEnum,
+ override val visibility: () -> Boolean = { true },
) : SettingGroup(c), InventoryConfig {
enum class Group(override val displayName: String) : NamedEnum {
General("General"),
@@ -34,15 +35,15 @@ class InventorySettings(
Access("Access")
}
- override val actionsPerSecond by c.setting("Actions Per Second", 100, 0..100, 1, "How many inventory actions can be performed per tick").group(baseGroup, Group.General).index()
- override val tickStageMask by c.setting("Inventory Stage Mask", ALL_STAGES.toSet(), description = "The sub-tick timing at which inventory actions are performed", displayClassName = true).group(baseGroup, Group.General).index()
- override val disposables by c.setting("Disposables", ItemUtils.defaultDisposables, description = "Items that will be ignored when checking for a free slot").group(baseGroup, Group.Container).index()
- override val swapWithDisposables by c.setting("Swap With Disposables", true, "Swap items with disposable ones").group(baseGroup, Group.Container).index()
- override val providerPriority by c.setting("Provider Priority", InventoryConfig.Priority.WithMinItems, "What container to prefer when retrieving the item from").group(baseGroup, Group.Container).index()
- override val storePriority by c.setting("Store Priority", InventoryConfig.Priority.WithMinItems, "What container to prefer when storing the item to").group(baseGroup, Group.Container).index()
+ override val actionsPerSecond by c.setting("Actions Per Second", 100, 0..100, 1, "How many inventory actions can be performed per tick", visibility = visibility).group(baseGroup, Group.General).index()
+ override val tickStageMask by c.setting("Inventory Stage Mask", ALL_STAGES.toSet(), description = "The sub-tick timing at which inventory actions are performed", displayClassName = true, visibility = visibility).group(baseGroup, Group.General).index()
+ override val disposables by c.setting("Disposables", ItemUtils.defaultDisposables, description = "Items that will be ignored when checking for a free slot", visibility = visibility).group(baseGroup, Group.Container).index()
+ override val swapWithDisposables by c.setting("Swap With Disposables", true, "Swap items with disposable ones", visibility = visibility).group(baseGroup, Group.Container).index()
+ override val providerPriority by c.setting("Provider Priority", InventoryConfig.Priority.WithMinItems, "What container to prefer when retrieving the item from", visibility = visibility).group(baseGroup, Group.Container).index()
+ override val storePriority by c.setting("Store Priority", InventoryConfig.Priority.WithMinItems, "What container to prefer when storing the item to", visibility = visibility).group(baseGroup, Group.Container).index()
- override val accessShulkerBoxes by c.setting("Access Shulker Boxes", false, "Allow access to the player's shulker boxes").group(baseGroup, Group.Access).index()
- override val accessChests by c.setting("Access Chests", false, "Allow access to the player's normal chests").group(baseGroup, Group.Access).index()
- override val accessEnderChest by c.setting("Access Ender Chest", false, "Allow access to the player's ender chest").group(baseGroup, Group.Access).index()
- override val accessStashes by c.setting("Access Stashes", false, "Allow access to the player's stashes").group(baseGroup, Group.Access).index()
+ override val accessShulkerBoxes by c.setting("Access Shulker Boxes", false, "Allow access to the player's shulker boxes", visibility = visibility).group(baseGroup, Group.Access).index()
+ override val accessChests by c.setting("Access Chests", false, "Allow access to the player's normal chests", visibility = visibility).group(baseGroup, Group.Access).index()
+ override val accessEnderChest by c.setting("Access Ender Chest", false, "Allow access to the player's ender chest", visibility = visibility).group(baseGroup, Group.Access).index()
+ override val accessStashes by c.setting("Access Stashes", false, "Allow access to the player's stashes", visibility = visibility).group(baseGroup, Group.Access).index()
}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/config/groups/LineConfig.kt b/src/main/kotlin/com/lambda/config/groups/LineConfig.kt
new file mode 100644
index 000000000..edd9d010a
--- /dev/null
+++ b/src/main/kotlin/com/lambda/config/groups/LineConfig.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.config.groups
+
+import com.lambda.graphics.mc.LineDashStyle
+import java.awt.Color
+
+interface LineConfig {
+ val startColor: Color
+ val endColor: Color
+ val width: Float
+ val dashEnabled: Boolean
+ val dashLength: Float
+ val gapLength: Float
+ val dashOffset: Float
+ val animated: Boolean
+ val animationSpeed: Float
+
+ /**
+ * Get the dash style for rendering, or null if dashing is disabled.
+ */
+ fun getDashStyle(): LineDashStyle? =
+ if (dashEnabled) LineDashStyle(dashLength, gapLength, dashOffset, animated, animationSpeed) else null
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/config/groups/RotationSettings.kt b/src/main/kotlin/com/lambda/config/groups/RotationSettings.kt
index 8c6fb1746..37b6a8783 100644
--- a/src/main/kotlin/com/lambda/config/groups/RotationSettings.kt
+++ b/src/main/kotlin/com/lambda/config/groups/RotationSettings.kt
@@ -34,31 +34,32 @@ import kotlin.random.Random
class RotationSettings(
c: Configurable,
baseGroup: NamedEnum,
+ override val visibility: () -> Boolean = { true },
) : SettingGroup(c), RotationConfig {
- override var rotationMode by c.setting("Mode", RotationMode.Sync, "How the player is being rotated on interaction").group(baseGroup).index()
+ override var rotationMode by c.setting("Mode", RotationMode.Sync, "How the player is being rotated on interaction", visibility = visibility).group(baseGroup).index()
/** How many ticks to keep the rotation before resetting */
- override val keepTicks by c.setting("Keep Rotation", 1, 1..10, 1, "Ticks to keep rotation", " ticks").group(baseGroup).index()
+ override val keepTicks by c.setting("Keep Rotation", 1, 1..10, 1, "Ticks to keep rotation", " ticks", visibility = visibility).group(baseGroup).index()
/** How many ticks to wait before resetting the rotation */
- override val decayTicks by c.setting("Reset Rotation", 1, 1..10, 1, "Ticks before rotation is reset", " ticks").group(baseGroup).index()
+ override val decayTicks by c.setting("Reset Rotation", 1, 1..10, 1, "Ticks before rotation is reset", " ticks", visibility = visibility).group(baseGroup).index()
override val tickStageMask = ALL_STAGES.subList(0, ALL_STAGES.indexOf(TickEvent.Player.Post)).toSet()
/** Whether the rotation is instant */
- var instant by c.setting("Instant Rotation", true, "Instantly rotate").group(baseGroup).index()
+ var instant by c.setting("Instant Rotation", true, "Instantly rotate", visibility = visibility).group(baseGroup).index()
/**
* The mean (average/base) value used to calculate rotation speed.
* This value represents the center of the distribution.
*/
- var mean by c.setting("Mean", 40.0, 1.0..120.0, 0.1, "Average rotation speed", unit = "°") { !instant }.group(baseGroup).index()
+ var mean by c.setting("Mean", 40.0, 1.0..120.0, 0.1, "Average rotation speed", unit = "°") { visibility() && !instant }.group(baseGroup).index()
/**
* The standard deviation for the Gaussian distribution used to calculate rotation speed.
* This value represents the spread of rotation speed.
*/
- var spread by c.setting("Spread", 10.0, 0.0..60.0, 0.1, "Spread of rotation speeds", unit = "°") { !instant }.group(baseGroup).index()
+ var spread by c.setting("Spread", 10.0, 0.0..60.0, 0.1, "Spread of rotation speeds", unit = "°") { visibility() && !instant }.group(baseGroup).index()
/**
* We must always provide turn speed to the interpolator because the player's yaw might exceed the -180 to 180 range.
diff --git a/src/main/kotlin/com/lambda/config/groups/ScreenLineSettings.kt b/src/main/kotlin/com/lambda/config/groups/ScreenLineSettings.kt
new file mode 100644
index 000000000..a5b60e732
--- /dev/null
+++ b/src/main/kotlin/com/lambda/config/groups/ScreenLineSettings.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.config.groups
+
+import com.lambda.config.Configurable
+import com.lambda.config.SettingGroup
+import com.lambda.util.NamedEnum
+import java.awt.Color
+
+class ScreenLineSettings(
+ prefix: String,
+ c: Configurable,
+ vararg baseGroup: NamedEnum,
+ override val visibility: () -> Boolean = { true },
+) : SettingGroup(c), LineConfig {
+ private enum class Group(override val displayName: String) : NamedEnum {
+ Color("Color"),
+ Dash("Dash")
+ }
+
+ val widthSetting by c.setting("${prefix}Line Width", 10, 1..100, 1, "The width of the line", visibility = visibility).group(*baseGroup).index()
+ override val width get() = widthSetting * 0.00005f
+
+ override val startColor by c.setting("${prefix}Start Color", Color.WHITE, "The color at the start of the line", visibility = visibility).group(*baseGroup, Group.Color).index()
+ override val endColor by c.setting("${prefix}End Color", Color.WHITE, "The color at the end of the line", visibility = visibility).group(*baseGroup, Group.Color).index()
+
+ override val dashEnabled by c.setting("${prefix}Dashed", false, "Enable dashed line pattern", visibility = visibility).group(*baseGroup, Group.Dash).index()
+ val dashLengthSetting by c.setting("${prefix}Dash Length", 30, 1..50, 1, "Length of each dash") { visibility() && dashEnabled }.group(*baseGroup, Group.Dash).index()
+ override val dashLength get() = dashLengthSetting * 0.001f
+ val gapLengthSetting by c.setting("${prefix}Gap Length", 15, 1..50, 1, "Length of gaps between dashes") { visibility() && dashEnabled }.group(*baseGroup, Group.Dash).index()
+ override val gapLength get() = gapLengthSetting * 0.001f
+ override val animated by c.setting("${prefix}Animated", true, "Animate the dash pattern") { visibility() && dashEnabled }.group(*baseGroup, Group.Dash).index()
+ val dashOffsetSetting by c.setting("${prefix}Dash Offset", 0, 0..100, 1, "Offset of the dash pattern") { visibility() && dashEnabled && !animated }.group(*baseGroup, Group.Dash).index()
+ override val dashOffset get() = dashOffsetSetting * 0.01f
+ val animationSpeedSetting by c.setting("${prefix}Animation Speed", 30, -100..100, 1, "Speed of dash animation (negative = reverse)") { visibility() && dashEnabled && animated }.group(*baseGroup, Group.Dash).index()
+ override val animationSpeed get() = animationSpeedSetting * 0.1f
+}
diff --git a/src/main/kotlin/com/lambda/config/groups/ScreenTextSettings.kt b/src/main/kotlin/com/lambda/config/groups/ScreenTextSettings.kt
new file mode 100644
index 000000000..01b3a8f1c
--- /dev/null
+++ b/src/main/kotlin/com/lambda/config/groups/ScreenTextSettings.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.config.groups
+
+import com.lambda.config.Configurable
+import com.lambda.config.SettingGroup
+import com.lambda.util.NamedEnum
+import java.awt.Color
+
+class ScreenTextSettings(
+ prefix: String,
+ c: Configurable,
+ vararg baseGroup: NamedEnum,
+ override val visibility: () -> Boolean = { true },
+) : SettingGroup(c), TextConfig {
+ private enum class Group(override val displayName: String) : NamedEnum {
+ Outline("Outline"),
+ Glow("Glow"),
+ Shadow("Shadow")
+ }
+
+ override val textColor by c.setting("${prefix}Text Color", Color.WHITE, "The main text color", visibility = visibility).group(*baseGroup).index()
+ val sizeSetting by c.setting("${prefix}Text Size", 18, 1..50, 1, visibility = visibility).group(*baseGroup).index()
+ override val size get() = sizeSetting * 0.001f
+
+ override val outlineEnabled by c.setting("${prefix}Outline", false, "Enable text outline", visibility = visibility).group(*baseGroup, Group.Outline).index()
+ override val outlineColor by c.setting("${prefix}Outline Color", Color.BLACK, "Color of the outline") { visibility() && outlineEnabled }.group(*baseGroup, Group.Outline).index()
+ override val outlineWidth by c.setting("${prefix}Outline Width", 0.1f, 0f..0.4f, 0.005f, "Width of the outline") { visibility() && outlineEnabled }.group(*baseGroup, Group.Outline).index()
+
+ override val glowEnabled by c.setting("${prefix}Glow", false, "Enable text glow effect", visibility = visibility).group(*baseGroup, Group.Glow).index()
+ override val glowColor by c.setting("${prefix}Glow Color", Color.WHITE, "Color of the glow") { visibility() && glowEnabled }.group(*baseGroup, Group.Glow).index()
+ override val glowRadius by c.setting("${prefix}Glow Radius", 0.2f, 0f..0.5f, 0.01f, "Radius of the glow effect") { visibility() && glowEnabled }.group(*baseGroup, Group.Glow).index()
+
+ override val shadowEnabled by c.setting("${prefix}Shadow", true, "Enable text shadow", visibility = visibility).group(*baseGroup, Group.Shadow).index()
+ override val shadowColor by c.setting("${prefix}Shadow Color", Color(0, 0, 0, 180), "Color of the shadow") { visibility() && shadowEnabled }.group(*baseGroup, Group.Shadow).index()
+ override val shadowOffset by c.setting("${prefix}Shadow Offset", 0.05f, 0f..0.5f, 0.005f, "Distance of shadow from text") { visibility() && shadowEnabled }.group(*baseGroup, Group.Shadow).index()
+ override val shadowAngle by c.setting("${prefix}Shadow Angle", 135f, 0f..360f, 1f, "Angle of the shadow") { visibility() && shadowEnabled }.group(*baseGroup, Group.Shadow).index()
+ override val shadowSoftness by c.setting("${prefix}Shadow Softness", 0f, 0f..0.5f, 0.01f, "Softness of shadow edges") { visibility() && shadowEnabled }.group(*baseGroup, Group.Shadow).index()
+}
diff --git a/src/main/kotlin/com/lambda/config/groups/Targeting.kt b/src/main/kotlin/com/lambda/config/groups/Targeting.kt
index a82766c1c..5c3e94253 100644
--- a/src/main/kotlin/com/lambda/config/groups/Targeting.kt
+++ b/src/main/kotlin/com/lambda/config/groups/Targeting.kt
@@ -25,6 +25,8 @@ import com.lambda.interaction.managers.rotating.Rotation.Companion.dist
import com.lambda.interaction.managers.rotating.Rotation.Companion.rotation
import com.lambda.interaction.managers.rotating.Rotation.Companion.rotationTo
import com.lambda.threading.runSafe
+import com.lambda.util.EntityUtils.EntityGroup
+import com.lambda.util.EntityUtils.entityGroup
import com.lambda.util.NamedEnum
import com.lambda.util.extension.fullHealth
import com.lambda.util.math.distSq
@@ -32,9 +34,6 @@ import com.lambda.util.world.fastEntitySearch
import net.minecraft.client.network.ClientPlayerEntity
import net.minecraft.client.network.OtherClientPlayerEntity
import net.minecraft.entity.LivingEntity
-import net.minecraft.entity.decoration.ArmorStandEntity
-import net.minecraft.entity.mob.HostileEntity
-import net.minecraft.entity.passive.PassiveEntity
import java.util.*
/**
@@ -54,78 +53,24 @@ abstract class Targeting(
baseGroup: NamedEnum,
private val defaultRange: Double,
private val maxRange: Double,
+ override val visibility: () -> Boolean = { true },
) : SettingGroup(c), TargetingConfig {
- /**
- * The range within which entities can be targeted. This value is configurable and constrained
- * between 1.0 and [maxRange].
- */
- override val targetingRange by c.setting("Targeting Range", defaultRange, 1.0..maxRange, 0.05).group(baseGroup)
-
- /**
- * Whether players are included in the targeting scope.
- */
- override val players by c.setting("Players", true).group(baseGroup)
-
- /**
- * Whether friends are included in the targeting scope.
- * Requires [players] to be true.
- */
- override val friends by c.setting("Friends", false) { players }.group(baseGroup)
-
- /**
- * Whether mobs are included in the targeting scope.
- */
- private val mobs by c.setting("Mobs", true).group(baseGroup)
-
- /**
- * Whether hostile mobs are included in the targeting scope
- */
- private val hostilesSetting by c.setting("Hostiles", true) { mobs }.group(baseGroup)
-
- /**
- * Whether passive animals are included in the targeting scope
- */
- private val animalsSetting by c.setting("Animals", true) { mobs }.group(baseGroup)
-
- /**
- * Indicates whether hostile entities are included in the targeting scope.
- */
- override val hostiles get() = mobs && hostilesSetting
-
- /**
- * Indicates whether passive animals are included in the targeting scope.
- */
- override val animals get() = mobs && animalsSetting
-
- /**
- * Whether invisible entities are included in the targeting scope.
- */
- override val invisible by c.setting("Invisible", true).group(baseGroup)
-
- /**
- * Whether dead entities are included in the targeting scope.
- */
- override val dead by c.setting("Dead", false).group(baseGroup)
-
- /**
- * Validates whether a given entity is targetable by the player based on current settings.
- *
- * @param player The [ClientPlayerEntity] performing the targeting.
- * @param entity The [LivingEntity] being evaluated.
- * @return `true` if the entity is valid for targeting, `false` otherwise.
- */
- open fun validate(player: ClientPlayerEntity, entity: LivingEntity) = when {
- !friends && entity is OtherClientPlayerEntity && entity.isFriend -> false
- !players && entity is OtherClientPlayerEntity -> false
- !animals && entity is PassiveEntity -> false
- !hostiles && entity is HostileEntity -> false
- entity is ArmorStandEntity -> false
-
- !invisible && entity.isInvisibleTo(player) -> false
- !dead && entity.isDead -> false
-
- else -> true
- }
+ /**
+ * The range within which entities can be targeted. This value is configurable and constrained
+ * between 1.0 and [maxRange].
+ */
+ override val targetingRange by c.setting("Targeting Range", defaultRange, 1.0..maxRange, 0.05, visibility = visibility).group(baseGroup)
+ override val targets by c.setting("Targets", setOf(EntityGroup.Player, EntityGroup.Mob, EntityGroup.Boss), EntityGroup.entries, visibility = visibility)
+
+ /**
+ * Validates whether a given entity is targetable by the player based on current settings.
+ *
+ * @param player The [ClientPlayerEntity] performing the targeting.
+ * @param entity The [LivingEntity] being evaluated.
+ * @return `true` if the entity is valid for targeting, `false` otherwise.
+ */
+ open fun validate(player: ClientPlayerEntity, entity: LivingEntity) =
+ entity.entityGroup in targets && (entity !is OtherClientPlayerEntity || !entity.isFriend)
/**
* Subclass for targeting entities specifically for combat purposes.
@@ -138,17 +83,17 @@ abstract class Targeting(
baseGroup: NamedEnum,
defaultRange: Double = 5.0,
maxRange: Double = 16.0,
- ) : Targeting(c, baseGroup, defaultRange, maxRange) {
-
+ visibility: () -> Boolean = { true },
+ ) : Targeting(c, baseGroup, defaultRange, maxRange, visibility) {
/**
* The field of view limit for targeting entities. Configurable between 5 and 180 degrees.
*/
- val fov by c.setting("FOV Limit", 180, 5..180, 1) { priority == Priority.Fov }.group(baseGroup)
+ val fov by c.setting("FOV Limit", 180, 5..180, 1) { visibility() && priority == Priority.Fov }.group(baseGroup)
/**
* The priority used to determine which entity is targeted. Configurable with default set to [Priority.Distance].
*/
- val priority by c.setting("Priority", Priority.Distance).group(baseGroup)
+ val priority by c.setting("Priority", Priority.Distance, visibility = visibility).group(baseGroup)
/**
* Validates whether a given entity is targetable for combat based on the field of view limit and other settings.
@@ -160,6 +105,7 @@ abstract class Targeting(
override fun validate(player: ClientPlayerEntity, entity: LivingEntity): Boolean {
if (fov < 180 && player.rotation dist player.eyePos.rotationTo(entity.pos) > fov) return false
if (entity.uuid in illegalTargets) return false
+ if (entity.isDead) return false
return super.validate(player, entity)
}
@@ -178,18 +124,11 @@ abstract class Targeting(
private val illegalTargets = setOf(
UUID(5706954458220675710, -6736729783554821869),
- UUID(-2945922493004570036, -7599209072395336449)
+ UUID(-6076316721184881576, -7147993044363569449),
+ UUID(-2932596226593701300, -7553629058088633089)
)
}
- /**
- * Subclass for targeting entities for ESP (Extrasensory Perception) purposes.
- */
- class ESP(
- c: Configurable,
- baseGroup: NamedEnum,
- ) : Targeting(c, baseGroup, 128.0, 1024.0)
-
/**
* Enum representing the different priority factors used for determining the best target.
*
diff --git a/src/main/kotlin/com/lambda/config/groups/TargetingConfig.kt b/src/main/kotlin/com/lambda/config/groups/TargetingConfig.kt
index a54df2231..117d18269 100644
--- a/src/main/kotlin/com/lambda/config/groups/TargetingConfig.kt
+++ b/src/main/kotlin/com/lambda/config/groups/TargetingConfig.kt
@@ -17,14 +17,9 @@
package com.lambda.config.groups
+import com.lambda.util.EntityUtils
+
interface TargetingConfig {
val targetingRange: Double
-
- val players: Boolean
- val friends: Boolean
- val hostiles: Boolean
- val animals: Boolean
-
- val invisible: Boolean
- val dead: Boolean
+ val targets: Collection
}
diff --git a/src/main/kotlin/com/lambda/config/groups/TextConfig.kt b/src/main/kotlin/com/lambda/config/groups/TextConfig.kt
new file mode 100644
index 000000000..20282275d
--- /dev/null
+++ b/src/main/kotlin/com/lambda/config/groups/TextConfig.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.config.groups
+
+import com.lambda.graphics.mc.RenderBuilder
+import java.awt.Color
+
+interface TextConfig {
+ val size: Float
+ val textColor: Color
+
+ val outlineEnabled: Boolean
+ val outlineColor: Color
+ val outlineWidth: Float
+
+ val glowEnabled: Boolean
+ val glowColor: Color
+ val glowRadius: Float
+
+ val shadowEnabled: Boolean
+ val shadowColor: Color
+ val shadowOffset: Float
+ val shadowAngle: Float
+ val shadowSoftness: Float
+
+ /**
+ * Get the SDF style for text rendering.
+ */
+ fun getSDFStyle(): RenderBuilder.SDFStyle {
+ val outline = if (outlineEnabled) RenderBuilder.SDFOutline(outlineColor, outlineWidth) else null
+ val glow = if (glowEnabled) RenderBuilder.SDFGlow(glowColor, glowRadius) else null
+ val shadow = if (shadowEnabled) RenderBuilder.SDFShadow(shadowColor, shadowOffset, shadowAngle, shadowSoftness) else null
+
+ return RenderBuilder.SDFStyle(textColor, outline, glow, shadow)
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/config/groups/WorldLineSettings.kt b/src/main/kotlin/com/lambda/config/groups/WorldLineSettings.kt
new file mode 100644
index 000000000..8ac4962db
--- /dev/null
+++ b/src/main/kotlin/com/lambda/config/groups/WorldLineSettings.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.config.groups
+
+import com.lambda.config.Configurable
+import com.lambda.config.SettingGroup
+import com.lambda.util.NamedEnum
+import java.awt.Color
+
+/**
+ * SettingGroup for world-space line configuration.
+ * Provides settings for line colors, width, dash patterns, and distance scaling.
+ */
+class WorldLineSettings(
+ prefix: String,
+ c: Configurable,
+ vararg baseGroup: NamedEnum,
+ override val visibility: () -> Boolean = { true },
+) : SettingGroup(c), LineConfig {
+ private enum class Group(override val displayName: String) : NamedEnum {
+ Color("Color"),
+ Dash("Dash")
+ }
+
+ val distanceScaling by c.setting("${prefix}Distance Scaling", true, "Line width stays constant on screen regardless of distance", visibility = visibility).group(*baseGroup).index()
+ val worldWidthSetting by c.setting("${prefix}Line Width", 5, 1..50, 1, "Line width in world units (blocks)") { visibility() && !distanceScaling }.group(*baseGroup).index()
+ val screenWidthSetting by c.setting("${prefix}Screen Width", 10, 1..100, 1, "Line width in screen-space (stays constant size)") { visibility() && distanceScaling }.group(*baseGroup).index()
+
+ override val width: Float get() = if (distanceScaling) -screenWidthSetting * 0.00005f // Negative = screen-space mode
+ else worldWidthSetting * 0.001f // Positive = world units
+
+ override val startColor by c.setting("${prefix}Start Color", Color.WHITE, "The color at the start of the line", visibility = visibility).group(*baseGroup, Group.Color).index()
+ override val endColor by c.setting("${prefix}End Color", Color.WHITE, "The color at the end of the line", visibility = visibility).group(*baseGroup, Group.Color).index()
+
+ override val dashEnabled by c.setting("${prefix}Dashed", false, "Enable dashed line pattern", visibility = visibility).group(*baseGroup, Group.Dash).index()
+ val dashLengthSetting by c.setting("${prefix}Dash Length", 50, 1..200, 1, "Length of each dash") { visibility() && dashEnabled }.group(*baseGroup, Group.Dash).index()
+ override val dashLength get() = dashLengthSetting * 0.01f
+ val gapLengthSetting by c.setting("${prefix}Gap Length", 25, 1..200, 1, "Length of gaps between dashes") { visibility() && dashEnabled }.group(*baseGroup, Group.Dash).index()
+ override val gapLength get() = gapLengthSetting * 0.01f
+ override val animated by c.setting("${prefix}Animated", true, "Animate the dash pattern") { visibility() && dashEnabled }.group(*baseGroup, Group.Dash).index()
+ val dashOffsetSetting by c.setting("${prefix}Dash Offset", 0, 0..100, 1, "Offset of the dash pattern") { visibility() && dashEnabled && !animated }.group(*baseGroup, Group.Dash).index()
+ override val dashOffset get() = dashOffsetSetting * 0.01f
+ val animationSpeedSetting by c.setting("${prefix}Animation Speed", 30, -100..100, 1, "Speed of dash animation (negative = reverse)") { visibility() && dashEnabled && animated }.group(*baseGroup, Group.Dash).index()
+ override val animationSpeed get() = animationSpeedSetting * 0.1f
+}
diff --git a/src/main/kotlin/com/lambda/config/groups/WorldTextSettings.kt b/src/main/kotlin/com/lambda/config/groups/WorldTextSettings.kt
new file mode 100644
index 000000000..7b7993205
--- /dev/null
+++ b/src/main/kotlin/com/lambda/config/groups/WorldTextSettings.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.config.groups
+
+import com.lambda.config.Configurable
+import com.lambda.config.SettingGroup
+import com.lambda.util.NamedEnum
+import java.awt.Color
+
+class WorldTextSettings(
+ prefix: String,
+ c: Configurable,
+ vararg baseGroup: NamedEnum,
+ override val visibility: () -> Boolean = { true },
+) : SettingGroup(c), TextConfig {
+ private enum class Group(override val displayName: String) : NamedEnum {
+ Outline("Outline"),
+ Glow("Glow"),
+ Shadow("Shadow")
+ }
+
+ override val textColor by c.setting("${prefix}Text Color", Color.WHITE, "The main text color", visibility = visibility).group(*baseGroup).index()
+ val sizeSetting by c.setting("${prefix}Text Size", 5, 1..50, 1, visibility = visibility).group(*baseGroup).index()
+ override val size get() = sizeSetting * 0.1f
+
+ override val outlineEnabled by c.setting("${prefix}Outline", false, "Enable text outline", visibility = visibility).group(*baseGroup, Group.Outline).index()
+ override val outlineColor by c.setting("${prefix}Outline Color", Color.BLACK, "Color of the outline") { visibility() && outlineEnabled }.group(*baseGroup, Group.Outline).index()
+ override val outlineWidth by c.setting("${prefix}Outline Width", 0.1f, 0f..0.4f, 0.005f, "Width of the outline") { visibility() && outlineEnabled }.group(*baseGroup, Group.Outline).index()
+
+ override val glowEnabled by c.setting("${prefix}Glow", false, "Enable text glow effect", visibility = visibility).group(*baseGroup, Group.Glow).index()
+ override val glowColor by c.setting("${prefix}Glow Color", Color.WHITE, "Color of the glow") { visibility() && glowEnabled }.group(*baseGroup, Group.Glow).index()
+ override val glowRadius by c.setting("${prefix}Glow Radius", 0.2f, 0f..0.5f, 0.01f, "Radius of the glow effect") { visibility() && glowEnabled }.group(*baseGroup, Group.Glow).index()
+
+ override val shadowEnabled by c.setting("${prefix}Shadow", true, "Enable text shadow", visibility = visibility).group(*baseGroup, Group.Shadow).index()
+ override val shadowColor by c.setting("${prefix}Shadow Color", Color(0, 0, 0, 180), "Color of the shadow") { visibility() && shadowEnabled }.group(*baseGroup, Group.Shadow).index()
+ override val shadowOffset by c.setting("${prefix}Shadow Offset", 0.05f, 0f..0.5f, 0.005f, "Distance of shadow from text") { visibility() && shadowEnabled }.group(*baseGroup, Group.Shadow).index()
+ override val shadowAngle by c.setting("${prefix}Shadow Angle", 135f, 0f..360f, 1f, "Angle of the shadow") { visibility() && shadowEnabled }.group(*baseGroup, Group.Shadow).index()
+ override val shadowSoftness by c.setting("${prefix}Shadow Softness", 0f, 0f..0.5f, 0.01f, "Softness of shadow edges") { visibility() && shadowEnabled }.group(*baseGroup, Group.Shadow).index()
+}
diff --git a/src/main/kotlin/com/lambda/graphics/esp/EspDsl.kt b/src/main/kotlin/com/lambda/event/events/HudRenderEvent.kt
similarity index 61%
rename from src/main/kotlin/com/lambda/graphics/esp/EspDsl.kt
rename to src/main/kotlin/com/lambda/event/events/HudRenderEvent.kt
index 0c0a19a3f..bede02152 100644
--- a/src/main/kotlin/com/lambda/graphics/esp/EspDsl.kt
+++ b/src/main/kotlin/com/lambda/event/events/HudRenderEvent.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2025 Lambda
+ * Copyright 2026 Lambda
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,18 +15,14 @@
* along with this program. If not, see .
*/
-package com.lambda.graphics.esp
+package com.lambda.event.events
-import com.lambda.graphics.mc.ChunkedRegionESP
-import com.lambda.module.Module
+import com.lambda.event.Event
+import net.minecraft.client.gui.DrawContext
-@DslMarker
-annotation class EspDsl
-
-fun Module.chunkedEsp(
- name: String,
- depthTest: Boolean = false,
- update: ShapeScope.(net.minecraft.world.World, com.lambda.util.world.FastVector) -> Unit
-): ChunkedRegionESP {
- return ChunkedRegionESP(this, name, depthTest, update)
-}
+/**
+ * Event fired during HUD rendering with access to Minecraft's DrawContext.
+ * Use this for rendering items, textures, and other GUI elements that need
+ * to integrate with Minecraft's deferred GUI rendering system.
+ */
+class HudRenderEvent(val context: DrawContext) : Event
diff --git a/src/main/kotlin/com/lambda/event/events/RenderEvent.kt b/src/main/kotlin/com/lambda/event/events/RenderEvent.kt
index 36e2dbe37..572d434b1 100644
--- a/src/main/kotlin/com/lambda/event/events/RenderEvent.kt
+++ b/src/main/kotlin/com/lambda/event/events/RenderEvent.kt
@@ -17,23 +17,13 @@
package com.lambda.event.events
-import com.lambda.context.SafeContext
import com.lambda.event.Event
import com.lambda.event.callback.Cancellable
import com.lambda.event.callback.ICancellable
-import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.graphics.RenderMain
-import com.lambda.graphics.mc.TransientRegionESP
-
-fun Any.onStaticRender(block: SafeContext.(TransientRegionESP) -> Unit) =
- listen { block(RenderMain.StaticESP) }
-
-fun Any.onDynamicRender(block: SafeContext.(TransientRegionESP) -> Unit) =
- listen { block(RenderMain.DynamicESP) }
sealed class RenderEvent {
- object Upload : Event
- object Render : Event
+ object RenderWorld : Event
+ object RenderScreen : Event
class UpdateTarget : ICancellable by Cancellable()
}
diff --git a/src/main/kotlin/com/lambda/event/events/ScreenRenderEvent.kt b/src/main/kotlin/com/lambda/event/events/ScreenRenderEvent.kt
new file mode 100644
index 000000000..58bcd984e
--- /dev/null
+++ b/src/main/kotlin/com/lambda/event/events/ScreenRenderEvent.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.event.events
+
+import com.lambda.event.Event
+
+/**
+ * Event fired after Minecraft's GUI has been fully rendered.
+ *
+ * This fires after guiRenderer.render() in GameRenderer, ensuring that
+ * any screen-space rendering done in response to this event will appear
+ * above all of Minecraft's native GUI elements (hotbar, held items, etc.).
+ *
+ * Use this event for screen-space rendering that needs to appear on top of
+ * Minecraft's HUD. For world-space (3D) rendering, use RenderEvent.Render.
+ */
+object ScreenRenderEvent : Event
diff --git a/src/main/kotlin/com/lambda/graphics/RenderMain.kt b/src/main/kotlin/com/lambda/graphics/RenderMain.kt
index 1353fec6e..381149694 100644
--- a/src/main/kotlin/com/lambda/graphics/RenderMain.kt
+++ b/src/main/kotlin/com/lambda/graphics/RenderMain.kt
@@ -20,23 +20,16 @@ package com.lambda.graphics
import com.lambda.Lambda.mc
import com.lambda.event.EventFlow.post
import com.lambda.event.events.RenderEvent
-import com.lambda.event.events.TickEvent
-import com.lambda.event.listener.SafeListener.Companion.listen
import com.lambda.graphics.gl.Matrices
import com.lambda.graphics.gl.Matrices.resetMatrices
-import com.lambda.graphics.mc.TransientRegionESP
+import com.lambda.graphics.mc.renderer.RendererUtils
import net.minecraft.util.math.Vec3d
import org.joml.Matrix4f
import org.joml.Vector2f
import org.joml.Vector4f
+import kotlin.math.abs
object RenderMain {
- @JvmStatic
- val StaticESP = TransientRegionESP("Static")
-
- @JvmStatic
- val DynamicESP = TransientRegionESP("Dynamic")
-
val projectionMatrix = Matrix4f()
val modelViewMatrix
get() = Matrices.peek()
@@ -44,13 +37,16 @@ object RenderMain {
get() = Matrix4f(projectionMatrix).mul(modelViewMatrix)
/**
- * Project a world position to screen coordinates. Returns null if the position is behind the
- * camera or off-screen.
+ * Project a world position to normalized screen coordinates (0-1 range).
+ * This is the format used by RenderBuilder's screen methods (screenText, screenRect, etc).
+ *
+ * Always returns coordinates, even if off-screen or behind camera.
+ * For behind-camera positions, the direction is preserved (useful for tracers).
*
* @param worldPos The world position to project
- * @return Screen coordinates (x, y) in pixels, or null if not visible
+ * @return Normalized screen coordinates (x, y)
*/
- fun worldToScreen(worldPos: Vec3d): Vector2f? {
+ fun worldToScreenNormalized(worldPos: Vec3d): Vector2f? {
val camera = mc.gameRenderer?.camera ?: return null
val cameraPos = camera.pos
@@ -63,49 +59,64 @@ object RenderMain {
val vec = Vector4f(relX, relY, relZ, 1f)
projModel.transform(vec)
- // Behind camera check
- if (vec.w <= 0) return null
-
- // Perspective divide to get NDC
- val ndcX = vec.x / vec.w
- val ndcY = vec.y / vec.w
- val ndcZ = vec.z / vec.w
-
- // Off-screen check (NDC is -1 to 1)
- if (ndcZ < -1 || ndcZ > 1) return null
+ val isBehind = vec.w < 0
+ val w = if (abs(vec.w) < 0.001f) 0.001f else abs(vec.w)
+
+ // Perspective divide to get NDC (-1 to 1)
+ var ndcX = vec.x / w
+ var ndcY = vec.y / w
+
+ // When behind camera, extend the direction past the screen edge
+ // so tracers go off-screen rather than landing on-screen
+ if (isBehind) {
+ // Normalize the direction and extend to a fixed off-screen distance
+ val len = kotlin.math.sqrt(ndcX * ndcX + ndcY * ndcY)
+ if (len > 0.0001f) {
+ // Extend to 3.0 in NDC space (well past the -1 to 1 range)
+ ndcX = (ndcX / len) * 3f
+ ndcY = (ndcY / len) * 3f
+ } else {
+ // If almost directly behind, push down (arbitrary direction)
+ // With Y-up, negative Y means down
+ ndcY = -3f
+ }
+ }
- // NDC to screen coordinates (Y is flipped in screen space)
- val window = mc.window
- val screenX = (ndcX + 1f) * 0.5f * window.framebufferWidth
- val screenY = (1f - ndcY) * 0.5f * window.framebufferHeight
+ // NDC to normalized 0-1 coordinates
+ // Y-up convention: 0 = bottom, 1 = top (matches screen rendering)
+ val normalizedX = (ndcX + 1f) * 0.5f
+ val normalizedY = (ndcY + 1f) * 0.5f // No flip for Y-up
- return Vector2f(screenX, screenY)
+ return Vector2f(normalizedX, normalizedY)
}
- /** Check if a world position is visible on screen. */
- fun isOnScreen(worldPos: Vec3d): Boolean = worldToScreen(worldPos) != null
+ /** Check if a world position is visible on screen (within 0-1 bounds and in front of camera). */
+ fun isOnScreen(worldPos: Vec3d): Boolean {
+ val camera = mc.gameRenderer?.camera ?: return false
+ val cameraPos = camera.pos
+
+ // Check if in front of camera first
+ val relX = (worldPos.x - cameraPos.x).toFloat()
+ val relY = (worldPos.y - cameraPos.y).toFloat()
+ val relZ = (worldPos.z - cameraPos.z).toFloat()
+ val vec = Vector4f(relX, relY, relZ, 1f)
+ projModel.transform(vec)
+ if (vec.w <= 0) return false
+
+ val pos = worldToScreenNormalized(worldPos) ?: return false
+ return pos.x in 0f..1f && pos.y in 0f..1f
+ }
@JvmStatic
- fun render3D(positionMatrix: Matrix4f, projMatrix: Matrix4f) {
+ fun render(positionMatrix: Matrix4f, projMatrix: Matrix4f) {
resetMatrices(positionMatrix)
projectionMatrix.set(projMatrix)
-
- // Render transient ESPs using the new pipeline
- StaticESP.render() // Uses internal depthTest flag (true)
- DynamicESP.render() // Uses internal depthTest flag (false)
-
- RenderEvent.Render.post()
- }
-
- init {
- listen {
- StaticESP.clear()
- DynamicESP.clear()
-
- RenderEvent.Upload.post()
-
- StaticESP.upload()
- DynamicESP.upload()
- }
+
+ // Clear xray depth buffer once per frame before any renderer runs.
+ // All world-space renderers share this depth state for proper inter-renderer occlusion.
+ RendererUtils.clearXrayDepthBuffer()
+
+ RenderEvent.RenderWorld.post()
+ RenderEvent.RenderScreen.post()
}
}
diff --git a/src/main/kotlin/com/lambda/graphics/esp/RegionESP.kt b/src/main/kotlin/com/lambda/graphics/esp/RegionESP.kt
deleted file mode 100644
index b32e03755..000000000
--- a/src/main/kotlin/com/lambda/graphics/esp/RegionESP.kt
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Copyright 2025 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.graphics.esp
-
-import com.lambda.Lambda.mc
-import com.lambda.graphics.mc.LambdaRenderPipelines
-import com.lambda.graphics.mc.RegionRenderer
-import com.lambda.graphics.mc.RenderRegion
-import com.lambda.util.extension.tickDelta
-import com.mojang.blaze3d.systems.RenderSystem
-import java.util.concurrent.ConcurrentHashMap
-import kotlin.math.floor
-import org.joml.Matrix4f
-import org.joml.Vector3f
-import org.joml.Vector4f
-
-/**
- * Base class for region-based ESP systems. Provides unified rendering logic and region management.
- */
-abstract class RegionESP(val name: String, val depthTest: Boolean) {
- protected val renderers = ConcurrentHashMap()
-
- /** Get or create a ShapeScope for a specific world position. */
- open fun shapes(x: Double, y: Double, z: Double, block: ShapeScope.() -> Unit) {}
-
- /** Upload collected geometry to GPU. Must be called on main thread. */
- open fun upload() {}
-
- /** Clear all geometry data. */
- abstract fun clear()
-
- /** Close and release all GPU resources. */
- open fun close() {
- renderers.values.forEach { it.close() }
- renderers.clear()
- clear()
- }
-
- /**
- * Render all active regions.
- * @param tickDelta Progress within current tick (used for interpolation)
- */
- open fun render(tickDelta: Float = mc.tickDelta) {
- val camera = mc.gameRenderer?.camera ?: return
- val cameraPos = camera.pos
-
- val activeRenderers = renderers.values.filter { it.hasData() }
- if (activeRenderers.isEmpty()) return
-
- val modelViewMatrix = com.lambda.graphics.RenderMain.modelViewMatrix
- val transforms = activeRenderers.map { renderer ->
- val offset = renderer.region.computeCameraRelativeOffset(cameraPos)
- val modelView = Matrix4f(modelViewMatrix).translate(offset)
-
- val dynamicTransform = RenderSystem.getDynamicUniforms()
- .write(
- modelView,
- Vector4f(1f, 1f, 1f, 1f),
- Vector3f(0f, 0f, 0f),
- Matrix4f()
- )
- renderer to dynamicTransform
- }
-
- // Render Faces
- RegionRenderer.createRenderPass("$name Faces")?.use { pass ->
- val pipeline =
- if (depthTest) LambdaRenderPipelines.ESP_QUADS
- else LambdaRenderPipelines.ESP_QUADS_THROUGH
- pass.setPipeline(pipeline)
- RenderSystem.bindDefaultUniforms(pass)
- transforms.forEach { (renderer, transform) ->
- pass.setUniform("DynamicTransforms", transform)
- renderer.renderFaces(pass)
- }
- }
-
- // Render Edges
- RegionRenderer.createRenderPass("$name Edges")?.use { pass ->
- val pipeline =
- if (depthTest) LambdaRenderPipelines.ESP_LINES
- else LambdaRenderPipelines.ESP_LINES_THROUGH
- pass.setPipeline(pipeline)
- RenderSystem.bindDefaultUniforms(pass)
- transforms.forEach { (renderer, transform) ->
- pass.setUniform("DynamicTransforms", transform)
- renderer.renderEdges(pass)
- }
- }
- }
-
- /**
- * Compute a unique key for a region based on its coordinates. Prevents collisions between
- * regions at different Y levels.
- */
- protected fun getRegionKey(x: Double, y: Double, z: Double): Long {
- val rx = (RenderRegion.REGION_SIZE * floor(x / RenderRegion.REGION_SIZE)).toInt()
- val ry = (RenderRegion.REGION_SIZE * floor(y / RenderRegion.REGION_SIZE)).toInt()
- val rz = (RenderRegion.REGION_SIZE * floor(z / RenderRegion.REGION_SIZE)).toInt()
-
- return getRegionKey(rx, ry, rz)
- }
-
- protected fun getRegionKey(rx: Int, ry: Int, rz: Int): Long {
- // 20 bits for X, 20 bits for Z, 24 bits for Y (total 64)
- // This supports +- 500k blocks in X/Z and full Y range
- return (rx.toLong() and 0xFFFFF) or
- ((rz.toLong() and 0xFFFFF) shl 20) or
- ((ry.toLong() and 0xFFFFFF) shl 40)
- }
-}
diff --git a/src/main/kotlin/com/lambda/graphics/esp/ShapeScope.kt b/src/main/kotlin/com/lambda/graphics/esp/ShapeScope.kt
deleted file mode 100644
index 14ab277f5..000000000
--- a/src/main/kotlin/com/lambda/graphics/esp/ShapeScope.kt
+++ /dev/null
@@ -1,416 +0,0 @@
-/*
- * Copyright 2025 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.graphics.esp
-
-import com.lambda.graphics.mc.RegionShapeBuilder
-import com.lambda.graphics.mc.RegionVertexCollector
-import com.lambda.graphics.mc.RenderRegion
-import com.lambda.graphics.renderer.esp.DirectionMask
-import com.lambda.graphics.renderer.esp.DynamicAABB
-import net.minecraft.block.BlockState
-import net.minecraft.util.math.BlockPos
-import net.minecraft.util.math.Box
-import net.minecraft.util.math.MathHelper
-import net.minecraft.util.math.Vec3d
-import net.minecraft.util.shape.VoxelShape
-import java.awt.Color
-
-@EspDsl
-class ShapeScope(val region: RenderRegion, val collectShapes: Boolean = false) {
- internal val builder = RegionShapeBuilder(region)
- internal val shapes = if (collectShapes) mutableListOf() else null
-
- /** Start building a box. */
- fun box(box: Box, id: Any? = null, block: BoxScope.() -> Unit) {
- val scope = BoxScope(box, this)
- scope.apply(block)
- if (collectShapes) {
- shapes?.add(
- EspShape.BoxShape(
- id?.hashCode() ?: box.hashCode(),
- box,
- scope.filledColor,
- scope.outlineColor,
- scope.sides,
- scope.outlineMode,
- scope.thickness
- )
- )
- }
- }
-
- /** Draw a line between two points. */
- fun line(start: Vec3d, end: Vec3d, color: Color, width: Float = 1.0f, id: Any? = null) {
- builder.line(start, end, color, width)
- if (collectShapes) {
- shapes?.add(
- EspShape.LineShape(
- id?.hashCode() ?: (start.hashCode() xor end.hashCode()),
- start,
- end,
- color,
- width
- )
- )
- }
- }
-
- /** Draw a tracer. */
- fun tracer(from: Vec3d, to: Vec3d, id: Any? = null, block: LineScope.() -> Unit = {}) {
- val scope = LineScope(from, to, this)
- scope.apply(block)
- scope.draw()
- if (collectShapes) {
- shapes?.add(
- EspShape.LineShape(
- id?.hashCode() ?: (from.hashCode() xor to.hashCode()),
- from,
- to,
- scope.lineColor,
- scope.lineWidth,
- scope.lineDashLength,
- scope.lineGapLength
- )
- )
- }
- }
-
- /** Draw a simple filled box. */
- fun filled(box: Box, color: Color, sides: Int = DirectionMask.ALL) {
- builder.filled(box, color, sides)
- if (collectShapes) {
- shapes?.add(EspShape.BoxShape(box.hashCode(), box, color, null, sides))
- }
- }
-
- /** Draw a simple outlined box. */
- fun outline(box: Box, color: Color, sides: Int = DirectionMask.ALL, thickness: Float = builder.lineWidth) {
- builder.outline(box, color, sides, thickness = thickness)
- if (collectShapes) {
- shapes?.add(EspShape.BoxShape(box.hashCode(), box, null, color, sides, thickness = thickness))
- }
- }
-
- fun filled(box: DynamicAABB, color: Color, sides: Int = DirectionMask.ALL) {
- builder.filled(box, color, sides)
- if (collectShapes) {
- box.pair?.second?.let {
- shapes?.add(EspShape.BoxShape(it.hashCode(), it, color, null, sides))
- }
- }
- }
-
- fun outline(box: DynamicAABB, color: Color, sides: Int = DirectionMask.ALL, thickness: Float = builder.lineWidth) {
- builder.outline(box, color, sides, thickness = thickness)
- if (collectShapes) {
- box.pair?.second?.let {
- shapes?.add(EspShape.BoxShape(it.hashCode(), it, null, color, sides, thickness = thickness))
- }
- }
- }
-
- fun filled(pos: BlockPos, color: Color, sides: Int = DirectionMask.ALL) {
- builder.filled(pos, color, sides)
- if (collectShapes) {
- shapes?.add(EspShape.BoxShape(pos.hashCode(), Box(pos), color, null, sides))
- }
- }
-
- fun outline(pos: BlockPos, color: Color, sides: Int = DirectionMask.ALL, thickness: Float = builder.lineWidth) {
- builder.outline(pos, color, sides, thickness = thickness)
- if (collectShapes) {
- shapes?.add(EspShape.BoxShape(pos.hashCode(), Box(pos), null, color, sides, thickness = thickness))
- }
- }
-
- fun filled(pos: BlockPos, state: BlockState, color: Color, sides: Int = DirectionMask.ALL) {
- builder.filled(pos, state, color, sides)
- if (collectShapes) {
- shapes?.add(EspShape.BoxShape(pos.hashCode(), Box(pos), color, null, sides))
- }
- }
-
- fun outline(pos: BlockPos, state: BlockState, color: Color, sides: Int = DirectionMask.ALL, thickness: Float = builder.lineWidth) {
- builder.outline(pos, state, color, sides, thickness = thickness)
- if (collectShapes) {
- shapes?.add(EspShape.BoxShape(pos.hashCode(), Box(pos), null, color, sides, thickness = thickness))
- }
- }
-
- fun filled(shape: VoxelShape, color: Color, sides: Int = DirectionMask.ALL) {
- builder.filled(shape, color, sides)
- if (collectShapes) {
- shape.boundingBoxes.forEach {
- shapes?.add(EspShape.BoxShape(it.hashCode(), it, color, null, sides))
- }
- }
- }
-
- fun outline(shape: VoxelShape, color: Color, sides: Int = DirectionMask.ALL, thickness: Float = builder.lineWidth) {
- builder.outline(shape, color, sides, thickness = thickness)
- if (collectShapes) {
- shape.boundingBoxes.forEach {
- shapes?.add(EspShape.BoxShape(it.hashCode(), it, null, color, sides, thickness = thickness))
- }
- }
- }
-
- fun box(
- pos: BlockPos,
- state: BlockState,
- filled: Color,
- outline: Color,
- sides: Int = DirectionMask.ALL,
- mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And,
- thickness: Float = builder.lineWidth
- ) {
- builder.box(pos, state, filled, outline, sides, mode, thickness = thickness)
- if (collectShapes) {
- shapes?.add(EspShape.BoxShape(pos.hashCode(), Box(pos), filled, outline, sides, mode, thickness = thickness))
- }
- }
-
- fun box(
- pos: BlockPos,
- filled: Color,
- outline: Color,
- sides: Int = DirectionMask.ALL,
- mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And,
- thickness: Float = builder.lineWidth
- ) {
- builder.box(pos, filled, outline, sides, mode, thickness = thickness)
- if (collectShapes) {
- shapes?.add(EspShape.BoxShape(pos.hashCode(), Box(pos), filled, outline, sides, mode, thickness = thickness))
- }
- }
-
- fun box(
- box: Box,
- filledColor: Color,
- outlineColor: Color,
- sides: Int = DirectionMask.ALL,
- mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And,
- thickness: Float = builder.lineWidth
- ) {
- builder.box(box, filledColor, outlineColor, sides, mode, thickness = thickness)
- if (collectShapes) {
- shapes?.add(EspShape.BoxShape(box.hashCode(), box, filledColor, outlineColor, sides, mode, thickness = thickness))
- }
- }
-
- fun box(
- box: DynamicAABB,
- filledColor: Color,
- outlineColor: Color,
- sides: Int = DirectionMask.ALL,
- mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And,
- thickness: Float = builder.lineWidth
- ) {
- builder.box(box, filledColor, outlineColor, sides, mode, thickness = thickness)
- if (collectShapes) {
- box.pair?.second?.let {
- shapes?.add(
- EspShape.BoxShape(it.hashCode(), it, filledColor, outlineColor, sides, mode, thickness = thickness)
- )
- }
- }
- }
-
- fun box(
- entity: net.minecraft.block.entity.BlockEntity,
- filled: Color,
- outline: Color,
- sides: Int = DirectionMask.ALL,
- mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And,
- thickness: Float = builder.lineWidth
- ) {
- builder.box(entity, filled, outline, sides, mode, thickness = thickness)
- if (collectShapes) {
- shapes?.add(
- EspShape.BoxShape(
- entity.pos.hashCode(),
- Box(entity.pos),
- filled,
- outline,
- sides,
- mode,
- thickness = thickness
- )
- )
- }
- }
-
- fun box(
- entity: net.minecraft.entity.Entity,
- filled: Color,
- outline: Color,
- sides: Int = DirectionMask.ALL,
- mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And,
- thickness: Float = builder.lineWidth
- ) {
- builder.box(entity, filled, outline, sides, mode, thickness = thickness)
- if (collectShapes) {
- shapes?.add(
- EspShape.BoxShape(
- entity.hashCode(),
- entity.boundingBox,
- filled,
- outline,
- sides,
- mode,
- thickness = thickness
- )
- )
- }
- }
-}
-
-@EspDsl
-class BoxScope(val box: Box, val parent: ShapeScope) {
- internal var filledColor: Color? = null
- internal var outlineColor: Color? = null
- internal var sides: Int = DirectionMask.ALL
- internal var outlineMode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And
- internal var thickness: Float = parent.builder.lineWidth
-
- fun filled(color: Color, sides: Int = DirectionMask.ALL) {
- this.filledColor = color
- this.sides = sides
- parent.builder.filled(box, color, sides)
- }
-
- fun outline(
- color: Color,
- sides: Int = DirectionMask.ALL,
- mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And,
- thickness: Float = parent.builder.lineWidth
- ) {
- this.outlineColor = color
- this.sides = sides
- this.outlineMode = mode
- this.thickness = thickness
- parent.builder.outline(box, color, sides, mode, thickness = thickness)
- }
-}
-
-@EspDsl
-class LineScope(val from: Vec3d, val to: Vec3d, val parent: ShapeScope) {
- internal var lineColor: Color = Color.WHITE
- internal var lineWidth: Float = 1.0f
- internal var lineDashLength: Double? = null
- internal var lineGapLength: Double? = null
-
- fun color(color: Color) {
- this.lineColor = color
- }
-
- fun width(width: Float) {
- this.lineWidth = width
- }
-
- fun dashed(dashLength: Double = 0.5, gapLength: Double = 0.25) {
- this.lineDashLength = dashLength
- this.lineGapLength = gapLength
- }
-
- internal fun draw() {
- val dLen = lineDashLength
- val gLen = lineGapLength
-
- if (dLen != null && gLen != null) {
- parent.builder.dashedLine(from, to, lineColor, dLen, gLen, lineWidth)
- } else {
- parent.builder.line(from, to, lineColor, lineWidth)
- }
- }
-}
-
-sealed class EspShape(val id: Int) {
- abstract fun renderInterpolated(
- prev: EspShape,
- tickDelta: Float,
- collector: RegionVertexCollector,
- region: RenderRegion
- )
-
- class BoxShape(
- id: Int,
- val box: Box,
- val filledColor: Color?,
- val outlineColor: Color?,
- val sides: Int = DirectionMask.ALL,
- val outlineMode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And,
- val thickness: Float = 1.0f
- ) : EspShape(id) {
- override fun renderInterpolated(
- prev: EspShape,
- tickDelta: Float,
- collector: RegionVertexCollector,
- region: RenderRegion
- ) {
- val interpBox =
- if (prev is BoxShape) {
- Box(
- MathHelper.lerp(tickDelta.toDouble(), prev.box.minX, box.minX),
- MathHelper.lerp(tickDelta.toDouble(), prev.box.minY, box.minY),
- MathHelper.lerp(tickDelta.toDouble(), prev.box.minZ, box.minZ),
- MathHelper.lerp(tickDelta.toDouble(), prev.box.maxX, box.maxX),
- MathHelper.lerp(tickDelta.toDouble(), prev.box.maxY, box.maxY),
- MathHelper.lerp(tickDelta.toDouble(), prev.box.maxZ, box.maxZ)
- )
- } else box
-
- val shapeBuilder = RegionShapeBuilder(region)
- filledColor?.let { shapeBuilder.filled(interpBox, it, sides) }
- outlineColor?.let { shapeBuilder.outline(interpBox, it, sides, outlineMode, thickness = thickness) }
-
- collector.faceVertices.addAll(shapeBuilder.collector.faceVertices)
- collector.edgeVertices.addAll(shapeBuilder.collector.edgeVertices)
- }
- }
-
- class LineShape(
- id: Int,
- val from: Vec3d,
- val to: Vec3d,
- val color: Color,
- val width: Float,
- val dashLength: Double? = null,
- val gapLength: Double? = null
- ) : EspShape(id) {
- override fun renderInterpolated(
- prev: EspShape,
- tickDelta: Float,
- collector: RegionVertexCollector,
- region: RenderRegion
- ) {
- val iFrom = if (prev is LineShape) prev.from.lerp(from, tickDelta.toDouble()) else from
- val iTo = if (prev is LineShape) prev.to.lerp(to, tickDelta.toDouble()) else to
-
- val shapeBuilder = RegionShapeBuilder(region)
- if (dashLength != null && gapLength != null) {
- shapeBuilder.dashedLine(iFrom, iTo, color, dashLength, gapLength, width)
- } else {
- shapeBuilder.line(iFrom, iTo, color, width)
- }
-
- collector.faceVertices.addAll(shapeBuilder.collector.faceVertices)
- collector.edgeVertices.addAll(shapeBuilder.collector.edgeVertices)
- }
- }
-}
diff --git a/src/main/kotlin/com/lambda/graphics/mc/BoxBuilder.kt b/src/main/kotlin/com/lambda/graphics/mc/BoxBuilder.kt
new file mode 100644
index 000000000..69d204ccb
--- /dev/null
+++ b/src/main/kotlin/com/lambda/graphics/mc/BoxBuilder.kt
@@ -0,0 +1,331 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.graphics.mc
+
+import com.lambda.graphics.util.DirectionMask
+import net.minecraft.util.math.Direction
+import java.awt.Color
+
+/**
+ * DSL builder for creating boxes with fine-grained control over:
+ * - Which sides to show for outlines vs faces (independently)
+ * - Individual vertex colors for all 8 corners
+ *
+ * Vertex naming convention (looking at box from outside):
+ * - Bottom corners: bottomNorthWest, bottomNorthEast, bottomSouthWest, bottomSouthEast
+ * - Top corners: topNorthWest, topNorthEast, topSouthWest, topSouthEast
+ *
+ * Usage:
+ * ```
+ * builder.box(myBox) {
+ * outlineSides = DirectionMask.UP or DirectionMask.DOWN
+ * faceSides = DirectionMask.ALL
+ * thickness = 2f
+ *
+ * // Set all vertices to one color
+ * allColors(Color.RED)
+ *
+ * // Or set gradient colors
+ * bottomColor = Color.RED
+ * topColor = Color.BLUE
+ *
+ * // Or set individual vertex colors
+ * topNorthWest = Color.RED
+ * topNorthEast = Color.GREEN
+ * // etc.
+ * }
+ * ```
+ */
+class BoxBuilder(val lineWidth: Float) {
+ // Side masks - independent control for outlines and faces
+ var outlineSides: Int = DirectionMask.ALL
+ var fillSides: Int = DirectionMask.ALL
+
+ // Outline mode for edge visibility
+ var outlineMode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And
+
+ // Dash style for outline edges (null = solid lines)
+ var dashStyle: LineDashStyle? = null
+
+ // Bottom layer fill colors
+ var fillBottomNorthWest: Color = Color.WHITE
+ var fillBottomNorthEast: Color = Color.WHITE
+ var fillBottomSouthWest: Color = Color.WHITE
+ var fillBottomSouthEast: Color = Color.WHITE
+
+ // Top layer fill colors
+ var fillTopNorthWest: Color = Color.WHITE
+ var fillTopNorthEast: Color = Color.WHITE
+ var fillTopSouthWest: Color = Color.WHITE
+ var fillTopSouthEast: Color = Color.WHITE
+
+ // Bottom layer outline colors
+ var outlineBottomNorthWest: Color = Color.WHITE
+ var outlineBottomNorthEast: Color = Color.WHITE
+ var outlineBottomSouthWest: Color = Color.WHITE
+ var outlineBottomSouthEast: Color = Color.WHITE
+
+ // Top layer outline colors
+ var outlineTopNorthWest: Color = Color.WHITE
+ var outlineTopNorthEast: Color = Color.WHITE
+ var outlineTopSouthWest: Color = Color.WHITE
+ var outlineTopSouthEast: Color = Color.WHITE
+
+ /** Set both outline and fill colors at once. */
+ @RenderDsl
+ fun allColors(color: Color) {
+ outlineColor(color)
+ fillColor(color)
+ }
+
+ /** Set outline and fill to different colors. */
+ @RenderDsl
+ fun colors(fill: Color, outline: Color) {
+ fillColor(fill)
+ outlineColor(outline)
+ }
+
+ /** Set all fill (face) colors to a single color. */
+ @RenderDsl
+ fun fillColor(color: Color) {
+ fillBottomNorthWest = color
+ fillBottomNorthEast = color
+ fillBottomSouthWest = color
+ fillBottomSouthEast = color
+ fillTopNorthWest = color
+ fillTopNorthEast = color
+ fillTopSouthWest = color
+ fillTopSouthEast = color
+ }
+
+ /** Set all outline (edge) colors to a single color. */
+ @RenderDsl
+ fun outlineColor(color: Color) {
+ outlineBottomNorthWest = color
+ outlineBottomNorthEast = color
+ outlineBottomSouthWest = color
+ outlineBottomSouthEast = color
+ outlineTopNorthWest = color
+ outlineTopNorthEast = color
+ outlineTopSouthWest = color
+ outlineTopSouthEast = color
+ }
+
+ /** Set all bottom vertices to one color and all top vertices to another (both outline and fill). */
+ @RenderDsl
+ fun gradientY(bottom: Color, top: Color) {
+ fillGradientY(bottom, top)
+ outlineGradientY(bottom, top)
+ }
+
+ /** Set fill gradient along Y axis (bottom to top). */
+ @RenderDsl
+ fun fillGradientY(bottom: Color, top: Color) {
+ fillBottomNorthWest = bottom
+ fillBottomNorthEast = bottom
+ fillBottomSouthWest = bottom
+ fillBottomSouthEast = bottom
+ fillTopNorthWest = top
+ fillTopNorthEast = top
+ fillTopSouthWest = top
+ fillTopSouthEast = top
+ }
+
+ /** Set outline gradient along Y axis (bottom to top). */
+ @RenderDsl
+ fun outlineGradientY(bottom: Color, top: Color) {
+ outlineBottomNorthWest = bottom
+ outlineBottomNorthEast = bottom
+ outlineBottomSouthWest = bottom
+ outlineBottomSouthEast = bottom
+ outlineTopNorthWest = top
+ outlineTopNorthEast = top
+ outlineTopSouthWest = top
+ outlineTopSouthEast = top
+ }
+
+ @RenderDsl
+ fun gradientX(west: Color, east: Color) {
+ fillGradientX(west, east)
+ outlineGradientX(west, east)
+ }
+
+ /** Set gradient along X axis (west to east) for both outline and fill. */
+ @RenderDsl
+ fun fillGradientX(west: Color, east: Color) {
+ fillBottomNorthWest = west
+ fillBottomSouthWest = west
+ fillTopNorthWest = west
+ fillTopSouthWest = west
+ fillBottomNorthEast = east
+ fillBottomSouthEast = east
+ fillTopNorthEast = east
+ fillTopSouthEast = east
+ }
+
+ @RenderDsl
+ fun outlineGradientX(west: Color, east: Color) {
+ outlineBottomNorthWest = west
+ outlineBottomSouthWest = west
+ outlineTopNorthWest = west
+ outlineTopSouthWest = west
+ outlineBottomNorthEast = east
+ outlineBottomSouthEast = east
+ outlineTopNorthEast = east
+ outlineTopSouthEast = east
+ }
+
+ /** Set gradient along Z axis (north to south) for both outline and fill. */
+ @RenderDsl
+ fun gradientZ(north: Color, south: Color) {
+ fillGradientZ(north, south)
+ outlineGradientZ(north, south)
+ }
+
+ @RenderDsl
+ fun fillGradientZ(north: Color, south: Color) {
+ fillBottomNorthWest = north
+ fillBottomNorthEast = north
+ fillTopNorthWest = north
+ fillTopNorthEast = north
+ fillBottomSouthWest = south
+ fillBottomSouthEast = south
+ fillTopSouthWest = south
+ fillTopSouthEast = south
+ }
+
+ @RenderDsl
+ fun outlineGradientZ(north: Color, south: Color) {
+ outlineBottomNorthWest = north
+ outlineBottomNorthEast = north
+ outlineTopNorthWest = north
+ outlineTopNorthEast = north
+ outlineBottomSouthWest = south
+ outlineBottomSouthEast = south
+ outlineTopSouthWest = south
+ outlineTopSouthEast = south
+ }
+
+ @RenderDsl
+ fun lineDashStyle(lineDashStyle: LineDashStyle) {
+ dashStyle = lineDashStyle
+ }
+
+ @RenderDsl
+ fun showSides(vararg directions: Direction) {
+ showFillSides(*directions)
+ showOutlineSides(*directions)
+ }
+
+ @RenderDsl
+ fun showSides(mask: Int) {
+ showFillSides(mask)
+ showOutlineSides(mask)
+ }
+
+ @RenderDsl
+ fun hideSides(vararg directions: Direction) {
+ hideFillSides(*directions)
+ hideOutlineSides(*directions)
+ }
+
+ @RenderDsl
+ fun hideSides(mask: Int) {
+ hideFillSides(mask)
+ hideOutlineSides(mask)
+ }
+
+ /** Hide all outline edges. */
+ @RenderDsl
+ fun hideOutline() {
+ outlineSides = DirectionMask.NONE
+ }
+
+ /** Hide all faces. */
+ @RenderDsl
+ fun hideFill() {
+ fillSides = DirectionMask.NONE
+ }
+
+ /** Show only outline (no faces). */
+ @RenderDsl
+ fun outlineOnly() {
+ outlineSides = DirectionMask.ALL
+ fillSides = DirectionMask.NONE
+ }
+
+ /** Show only faces (no outline). */
+ @RenderDsl
+ fun fillOnly() {
+ outlineSides = DirectionMask.NONE
+ fillSides = DirectionMask.ALL
+ }
+
+ /** Show the specified fill (face) sides, adding to current mask. */
+ @RenderDsl
+ fun showFillSides(vararg directions: Direction) {
+ directions.forEach { fillSides = fillSides or DirectionMask.run { it.mask } }
+ }
+
+ /** Show the specified fill (face) sides by mask, adding to current mask. */
+ @RenderDsl
+ fun showFillSides(mask: Int) {
+ fillSides = fillSides or mask
+ }
+
+ /** Hide the specified fill (face) sides, removing from current mask. */
+ @RenderDsl
+ fun hideFillSides(vararg directions: Direction) {
+ directions.forEach { fillSides = fillSides and DirectionMask.run { it.mask }.inv() }
+ }
+
+ /** Hide the specified fill (face) sides by mask, removing from current mask. */
+ @RenderDsl
+ fun hideFillSides(mask: Int) {
+ fillSides = fillSides and mask.inv()
+ }
+
+ /** Show the specified outline sides, adding to current mask. */
+ @RenderDsl
+ fun showOutlineSides(vararg directions: Direction) {
+ directions.forEach { outlineSides = outlineSides or DirectionMask.run { it.mask } }
+ }
+
+ /** Show the specified outline sides by mask, adding to current mask. */
+ @RenderDsl
+ fun showOutlineSides(mask: Int) {
+ outlineSides = outlineSides or mask
+ }
+
+ /** Hide the specified outline sides, removing from current mask. */
+ @RenderDsl
+ fun hideOutlineSides(vararg directions: Direction) {
+ directions.forEach { outlineSides = outlineSides and DirectionMask.run { it.mask }.inv() }
+ }
+
+ /** Hide the specified outline sides by mask, removing from current mask. */
+ @RenderDsl
+ fun hideOutlineSides(mask: Int) {
+ outlineSides = outlineSides and mask.inv()
+ }
+
+ @RenderDsl
+ fun outlineMode(outlineMode: DirectionMask.OutlineMode) {
+ this.outlineMode = outlineMode
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/graphics/mc/ChunkedRegionESP.kt b/src/main/kotlin/com/lambda/graphics/mc/ChunkedRegionESP.kt
deleted file mode 100644
index d367694a5..000000000
--- a/src/main/kotlin/com/lambda/graphics/mc/ChunkedRegionESP.kt
+++ /dev/null
@@ -1,163 +0,0 @@
-/*
- * Copyright 2025 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.graphics.mc
-
-import com.lambda.event.events.RenderEvent
-import com.lambda.event.events.TickEvent
-import com.lambda.event.events.WorldEvent
-import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.event.listener.SafeListener.Companion.listenConcurrently
-import com.lambda.graphics.esp.RegionESP
-import com.lambda.graphics.esp.ShapeScope
-import com.lambda.module.Module
-import com.lambda.module.modules.client.StyleEditor
-import com.lambda.threading.runSafe
-import com.lambda.util.world.FastVector
-import com.lambda.util.world.fastVectorOf
-import net.minecraft.world.World
-import net.minecraft.world.chunk.WorldChunk
-import java.util.concurrent.ConcurrentHashMap
-import java.util.concurrent.ConcurrentLinkedDeque
-
-/**
- * Region-based chunked ESP system using MC 1.21.11's new render pipeline.
- *
- * This system:
- * - Uses region-relative coordinates for precision-safe rendering
- * - Maintains per-chunk geometry for efficient updates
- *
- * @param owner The module that owns this ESP system
- * @param name The name of the ESP system
- * @param depthTest Whether to use depth testing
- * @param update The update function called for each block position
- */
-class ChunkedRegionESP(
- owner: Module,
- name: String,
- depthTest: Boolean = false,
- private val update: ShapeScope.(World, FastVector) -> Unit
-) : RegionESP(name, depthTest) {
- private val chunkMap = ConcurrentHashMap()
-
- private val WorldChunk.regionChunk
- get() = chunkMap.getOrPut(getRegionKey(pos.x shl 4, bottomY, pos.z shl 4)) {
- RegionChunk(this)
- }
-
- private val uploadQueue = ConcurrentLinkedDeque<() -> Unit>()
- private val rebuildQueue = ConcurrentLinkedDeque()
-
- /** Mark all tracked chunks for rebuild. */
- fun rebuild() {
- rebuildQueue.clear()
- rebuildQueue.addAll(chunkMap.values)
- }
-
- /**
- * Load all currently loaded world chunks and mark them for rebuild. Call this when the module
- * is enabled to populate initial chunks.
- */
- fun rebuildAll() {
- runSafe {
- val chunksArray = world.chunkManager.chunks.chunks
- (0 until chunksArray.length()).forEach { i ->
- chunksArray.get(i)?.regionChunk?.markDirty()
- }
- }
- }
-
- override fun clear() {
- chunkMap.values.forEach { it.close() }
- chunkMap.clear()
- rebuildQueue.clear()
- uploadQueue.clear()
- }
-
- init {
- owner.listen { event ->
- val pos = event.pos
- world.getWorldChunk(pos)?.regionChunk?.markDirty()
-
- val xInChunk = pos.x and 15
- val zInChunk = pos.z and 15
-
- if (xInChunk == 0) world.getWorldChunk(pos.west())?.regionChunk?.markDirty()
- if (xInChunk == 15) world.getWorldChunk(pos.east())?.regionChunk?.markDirty()
- if (zInChunk == 0) world.getWorldChunk(pos.north())?.regionChunk?.markDirty()
- if (zInChunk == 15) world.getWorldChunk(pos.south())?.regionChunk?.markDirty()
- }
-
- owner.listen { event -> event.chunk.regionChunk.markDirty() }
-
- owner.listen {
- val pos = getRegionKey(it.chunk.pos.x shl 4, it.chunk.bottomY, it.chunk.pos.z shl 4)
- chunkMap.remove(pos)?.close()
- }
-
- owner.listenConcurrently {
- val queueSize = rebuildQueue.size
- val polls = minOf(StyleEditor.rebuildsPerTick, queueSize)
- repeat(polls) { rebuildQueue.poll()?.rebuild() }
- }
-
- owner.listen {
- val polls = minOf(StyleEditor.uploadsPerTick, uploadQueue.size)
- repeat(polls) { uploadQueue.poll()?.invoke() }
- }
-
- owner.listen { render() }
- }
-
- /** Per-chunk rendering data. */
- private inner class RegionChunk(val chunk: WorldChunk) {
- val region = RenderRegion.forChunk(chunk.pos.x, chunk.pos.z, chunk.bottomY)
- private val key = getRegionKey(chunk.pos.x shl 4, chunk.bottomY, chunk.pos.z shl 4)
-
- private var isDirty = false
-
- fun markDirty() {
- isDirty = true
- if (!rebuildQueue.contains(this)) {
- rebuildQueue.add(this)
- }
- }
-
- fun rebuild() {
- if (!isDirty) return
- val scope = ShapeScope(region)
-
- for (x in chunk.pos.startX..chunk.pos.endX) {
- for (z in chunk.pos.startZ..chunk.pos.endZ) {
- for (y in chunk.bottomY..chunk.height) {
- update(scope, chunk.world, fastVectorOf(x, y, z))
- }
- }
- }
-
- uploadQueue.add {
- val renderer = renderers.getOrPut(key) { RegionRenderer(region) }
- renderer.upload(scope.builder.collector)
- isDirty = false
- }
- }
-
- fun close() {
- renderers.remove(key)?.close()
- }
- }
-}
diff --git a/src/main/kotlin/com/lambda/graphics/mc/ImGuiWorldText.kt b/src/main/kotlin/com/lambda/graphics/mc/ImGuiWorldText.kt
deleted file mode 100644
index 735f17de1..000000000
--- a/src/main/kotlin/com/lambda/graphics/mc/ImGuiWorldText.kt
+++ /dev/null
@@ -1,172 +0,0 @@
-/*
- * Copyright 2025 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.graphics.mc
-
-import com.lambda.graphics.RenderMain
-import imgui.ImGui
-import imgui.ImVec2
-import net.minecraft.util.math.Vec3d
-import java.awt.Color
-
-/**
- * ImGUI-based world text renderer.
- * Projects world coordinates to screen space and draws text using ImGUI.
- *
- * Usage:
- * ```kotlin
- * // In a GuiEvent.NewFrame listener
- * ImGuiWorldText.drawText(entity.pos, "Label", Color.WHITE)
- * ```
- */
-object ImGuiWorldText {
-
- /**
- * Draw text at a world position using ImGUI.
- *
- * @param worldPos World position for the text
- * @param text The text to render
- * @param color Text color
- * @param centered Whether to center the text horizontally
- * @param offsetY Vertical offset in screen pixels (negative = up)
- */
- fun drawText(
- worldPos: Vec3d,
- text: String,
- color: Color = Color.WHITE,
- centered: Boolean = true,
- offsetY: Float = 0f
- ) {
- val screen = RenderMain.worldToScreen(worldPos) ?: return
-
- val drawList = ImGui.getBackgroundDrawList()
- val colorInt = colorToImGui(color)
-
- val x = if (centered) {
- val textSize = ImVec2()
- ImGui.calcTextSize(textSize, text)
- screen.x - textSize.x / 2f
- } else {
- screen.x
- }
-
- drawList.addText(x, screen.y + offsetY, colorInt, text)
- }
-
- /**
- * Draw text with a shadow/outline effect.
- */
- fun drawTextWithShadow(
- worldPos: Vec3d,
- text: String,
- color: Color = Color.WHITE,
- shadowColor: Color = Color.BLACK,
- centered: Boolean = true,
- offsetY: Float = 0f
- ) {
- val screen = RenderMain.worldToScreen(worldPos) ?: return
-
- val drawList = ImGui.getBackgroundDrawList()
- val textSize = ImVec2()
- ImGui.calcTextSize(textSize, text)
-
- val x = if (centered) screen.x - textSize.x / 2f else screen.x
- val y = screen.y + offsetY
-
- // Draw shadow (offset by 1 pixel)
- val shadowInt = colorToImGui(shadowColor)
- drawList.addText(x + 1f, y + 1f, shadowInt, text)
-
- // Draw main text
- val colorInt = colorToImGui(color)
- drawList.addText(x, y, colorInt, text)
- }
-
- /**
- * Draw multiple lines of text stacked vertically.
- */
- fun drawMultilineText(
- worldPos: Vec3d,
- lines: List,
- color: Color = Color.WHITE,
- centered: Boolean = true,
- lineSpacing: Float = 12f,
- offsetY: Float = 0f
- ) {
- val screen = RenderMain.worldToScreen(worldPos) ?: return
-
- val drawList = ImGui.getBackgroundDrawList()
- val colorInt = colorToImGui(color)
-
- lines.forEachIndexed { index, line ->
- val textSize = ImVec2()
- ImGui.calcTextSize(textSize, line)
-
- val x = if (centered) screen.x - textSize.x / 2f else screen.x
- val y = screen.y + offsetY + (index * lineSpacing)
-
- drawList.addText(x, y, colorInt, line)
- }
- }
-
- /**
- * Draw text with a background box.
- */
- fun drawTextWithBackground(
- worldPos: Vec3d,
- text: String,
- textColor: Color = Color.WHITE,
- backgroundColor: Color = Color(0, 0, 0, 128),
- centered: Boolean = true,
- padding: Float = 4f,
- offsetY: Float = 0f
- ) {
- val screen = RenderMain.worldToScreen(worldPos) ?: return
-
- val drawList = ImGui.getBackgroundDrawList()
- val textSize = ImVec2()
- ImGui.calcTextSize(textSize, text)
-
- val x = if (centered) screen.x - textSize.x / 2f else screen.x
- val y = screen.y + offsetY
-
- // Draw background
- val bgInt = colorToImGui(backgroundColor)
- drawList.addRectFilled(
- x - padding,
- y - padding,
- x + textSize.x + padding,
- y + textSize.y + padding,
- bgInt,
- 2f // corner rounding
- )
-
- // Draw text
- val colorInt = colorToImGui(textColor)
- drawList.addText(x, y, colorInt, text)
- }
-
- /**
- * Convert java.awt.Color to ImGui color format (ABGR)
- */
- private fun colorToImGui(color: Color): Int {
- return (color.alpha shl 24) or
- (color.blue shl 16) or
- (color.green shl 8) or
- color.red
- }
-}
diff --git a/src/main/kotlin/com/lambda/graphics/mc/InterpolatedRegionESP.kt b/src/main/kotlin/com/lambda/graphics/mc/InterpolatedRegionESP.kt
deleted file mode 100644
index 8b2b0b4b7..000000000
--- a/src/main/kotlin/com/lambda/graphics/mc/InterpolatedRegionESP.kt
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- * Copyright 2025 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.graphics.mc
-
-import com.lambda.graphics.esp.RegionESP
-import com.lambda.graphics.esp.ShapeScope
-import java.util.concurrent.ConcurrentHashMap
-import kotlin.math.floor
-
-/**
- * Interpolated region-based ESP system for smooth entity rendering.
- *
- * Unlike TransientRegionESP which rebuilds every tick, this system stores both previous and current
- * frame data and interpolates between them during rendering for smooth movement at any framerate.
- */
-class InterpolatedRegionESP(name: String, depthTest: Boolean = false) : RegionESP(name, depthTest) {
- // Current frame builders (being populated this tick)
- private val currBuilders = ConcurrentHashMap()
-
- // Previous frame data (uploaded last tick)
- private val prevBuilders = ConcurrentHashMap()
-
- // Interpolated collectors for rendering (computed each frame)
- private val interpolatedCollectors =
- ConcurrentHashMap()
-
- // Track if we need to re-interpolate
- private var lastTickDelta = -1f
- private var needsInterpolation = true
-
- override fun shapes(x: Double, y: Double, z: Double, block: ShapeScope.() -> Unit) {
- val key = getRegionKey(x, y, z)
- val scope =
- currBuilders.getOrPut(key) {
- val size = RenderRegion.REGION_SIZE
- val rx = (size * floor(x / size)).toInt()
- val ry = (size * floor(y / size)).toInt()
- val rz = (size * floor(z / size)).toInt()
- ShapeScope(RenderRegion(rx, ry, rz), collectShapes = true)
- }
- scope.apply(block)
- }
-
- override fun clear() {
- prevBuilders.clear()
- currBuilders.clear()
- interpolatedCollectors.clear()
- }
-
- fun tick() {
- prevBuilders.clear()
- prevBuilders.putAll(currBuilders)
- currBuilders.clear()
- needsInterpolation = true
- }
-
- override fun upload() {
- needsInterpolation = true
- }
-
- override fun render(tickDelta: Float) {
- if (needsInterpolation || lastTickDelta != tickDelta) {
- interpolate(tickDelta)
- uploadInterpolated()
- lastTickDelta = tickDelta
- needsInterpolation = false
- }
- super.render(tickDelta)
- }
-
- private fun interpolate(tickDelta: Float) {
- interpolatedCollectors.clear()
- (prevBuilders.keys + currBuilders.keys).toSet().forEach { key ->
- val prevScope = prevBuilders[key]
- val currScope = currBuilders[key]
- val collector = RegionVertexCollector()
- val region = currScope?.region ?: prevScope?.region ?: return@forEach
-
- val prevShapes = prevScope?.shapes?.associateBy { it.id } ?: emptyMap()
- val currShapes = currScope?.shapes?.associateBy { it.id } ?: emptyMap()
-
- val allIds = (prevShapes.keys + currShapes.keys).toSet()
-
- for (id in allIds) {
- val prev = prevShapes[id]
- val curr = currShapes[id]
-
- when {
- prev != null && curr != null -> {
- curr.renderInterpolated(prev, tickDelta, collector, region)
- }
- curr != null -> {
- // New shape - just render
- curr.renderInterpolated(curr, 1.0f, collector, region)
- }
- prev != null -> {
- // Disappeared - render at previous position
- prev.renderInterpolated(prev, 1.0f, collector, region)
- }
- }
- }
-
- if (collector.faceVertices.isNotEmpty() || collector.edgeVertices.isNotEmpty()) {
- interpolatedCollectors[key] = collector
- }
- }
- }
-
- private fun uploadInterpolated() {
- val activeKeys = interpolatedCollectors.keys.toSet()
- interpolatedCollectors.forEach { (key, collector) ->
- val region = currBuilders[key]?.region ?: prevBuilders[key]?.region ?: return@forEach
-
- val renderer = renderers.getOrPut(key) { RegionRenderer(region) }
- renderer.upload(collector)
- }
-
- renderers.forEach { (key, renderer) ->
- if (key !in activeKeys) {
- renderer.clearData()
- }
- }
- }
-}
diff --git a/src/main/kotlin/com/lambda/graphics/mc/ItemLighting.kt b/src/main/kotlin/com/lambda/graphics/mc/ItemLighting.kt
new file mode 100644
index 000000000..6cd00c498
--- /dev/null
+++ b/src/main/kotlin/com/lambda/graphics/mc/ItemLighting.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.graphics.mc
+
+import org.joml.Vector3f
+
+/**
+ * Configuration for GUI item lighting.
+ * Controls the two-light shading model used for inventory-style rendering.
+ *
+ * @param light0 Primary light direction (normalized)
+ * @param light1 Fill light direction (normalized)
+ * @param ambient Base ambient lighting level (0.0-1.0)
+ * @param strength0 Primary light contribution multiplier
+ * @param strength1 Fill light contribution multiplier
+ * @param respectsUseLight When true, respects the layer's useLight flag to skip shading on flat items.
+ * When false, always applies the specified lighting regardless of item type.
+ */
+data class ItemLighting(
+ val light0: Vector3f,
+ val light1: Vector3f,
+ val ambient: Float = 0.6f,
+ val strength0: Float = 0.2f,
+ val strength1: Float = 0.2f,
+ val respectsUseLight: Boolean = false
+) {
+ companion object {
+ /**
+ * Vanilla inventory lighting for GUI items.
+ * Respects the per-layer useLight flag to match vanilla behavior:
+ * - 3D blocks (useLight=true): Apply diffuse shading
+ * - Flat items (useLight=false): Skip shading, full brightness
+ *
+ * Raw light values from DiffuseLighting.java:
+ * - DEFAULT_DIFFUSION_LIGHT_0 = (0.2, 1.0, -0.7)
+ * - DEFAULT_DIFFUSION_LIGHT_1 = (-0.2, 1.0, 0.7)
+ */
+ val VANILLA: ItemLighting by lazy {
+ val light0 = Vector3f(0.2f, 1.0f, -0.7f).normalize()
+ val light1 = Vector3f(-0.2f, 1.0f, 0.7f).normalize()
+ ItemLighting(light0, light1, respectsUseLight = true)
+ }
+
+ /**
+ * No shading - produces a flat, evenly lit look.
+ * Ignores the layer's useLight flag.
+ */
+ val NONE = ItemLighting(
+ light0 = Vector3f(0f, 1f, 0f),
+ light1 = Vector3f(0f, 1f, 0f),
+ ambient = 1.0f,
+ strength0 = 0f,
+ strength1 = 0f,
+ respectsUseLight = false
+ )
+
+ /**
+ * Creates lighting with a single directional light.
+ * Ignores the layer's useLight flag - always applies the specified lighting.
+ * @param direction The light direction (will be normalized)
+ * @param strength How strong the directional component is (0.0-1.0)
+ */
+ fun directional(direction: Vector3f, strength: Float = 0.4f): ItemLighting {
+ val normalized = Vector3f(direction).normalize()
+ return ItemLighting(
+ light0 = normalized,
+ light1 = Vector3f(-normalized.x, normalized.y, -normalized.z).normalize(),
+ ambient = 1.0f - strength,
+ strength0 = strength * 0.7f,
+ strength1 = strength * 0.3f,
+ respectsUseLight = false
+ )
+ }
+ }
+}
diff --git a/src/main/kotlin/com/lambda/graphics/mc/ItemOverlay.kt b/src/main/kotlin/com/lambda/graphics/mc/ItemOverlay.kt
new file mode 100644
index 000000000..005b0efdc
--- /dev/null
+++ b/src/main/kotlin/com/lambda/graphics/mc/ItemOverlay.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.graphics.mc
+
+import net.minecraft.util.Identifier
+
+/**
+ * Configuration for GUI item overlay effects (e.g., enchantment glint).
+ *
+ * @param texture The overlay texture identifier
+ * @param scale UV scale multiplier (higher = smaller pattern)
+ * @param speed Animation speed multiplier
+ * @param angle Rotation angle in degrees
+ * @param alpha Overlay opacity (0.0-1.0)
+ */
+data class ItemOverlay(
+ val texture: Identifier,
+ val scale: Float = 1.0f,
+ val speed: Float = 1.0f,
+ val angle: Float = 0f,
+ val alpha: Float = 0.5f
+) {
+ companion object {
+ /**
+ * Vanilla enchantment glint effect.
+ * Matches the animated purple shimmer on enchanted items.
+ */
+ val ENCHANT_GLINT = ItemOverlay(
+ texture = Identifier.of("minecraft", "textures/misc/enchanted_glint_item.png"),
+ scale = 8.0f,
+ speed = 1.0f,
+ angle = 10f,
+ alpha = 0.5f
+ )
+
+ /**
+ * Entity glint (used on enchanted armor).
+ */
+ val ENTITY_GLINT = ItemOverlay(
+ texture = Identifier.of("minecraft", "textures/misc/enchanted_glint_entity.png"),
+ scale = 8.0f,
+ speed = 1.0f,
+ angle = 10f,
+ alpha = 0.5f
+ )
+
+ /**
+ * Placeholder to explicitly disable glint.
+ */
+ val DISABLED = ItemOverlay(
+ texture = Identifier.of("minecraft", "textures/misc/unknown.png"),
+ alpha = 0f
+ )
+ }
+}
diff --git a/src/main/kotlin/com/lambda/graphics/mc/LambdaRenderPipelines.kt b/src/main/kotlin/com/lambda/graphics/mc/LambdaRenderPipelines.kt
index 350a40a77..c2d57686c 100644
--- a/src/main/kotlin/com/lambda/graphics/mc/LambdaRenderPipelines.kt
+++ b/src/main/kotlin/com/lambda/graphics/mc/LambdaRenderPipelines.kt
@@ -23,6 +23,7 @@ import com.mojang.blaze3d.pipeline.RenderPipeline
import com.mojang.blaze3d.platform.DepthTestFunction
import com.mojang.blaze3d.vertex.VertexFormat
import net.minecraft.client.gl.RenderPipelines
+import net.minecraft.client.gl.UniformType
import net.minecraft.client.render.VertexFormats
import net.minecraft.util.Identifier
@@ -47,14 +48,14 @@ object LambdaRenderPipelines : Loadable {
RenderPipelines.register(
RenderPipeline.builder(LAMBDA_ESP_SNIPPET, RenderPipelines.GLOBALS_SNIPPET)
.withLocation(Identifier.of("lambda", "pipeline/esp_lines"))
- .withVertexShader(Identifier.of("lambda", "core/advanced_lines"))
- .withFragmentShader(Identifier.of("lambda", "core/advanced_lines"))
+ .withVertexShader(Identifier.of("lambda", "core/world_lines"))
+ .withFragmentShader(Identifier.of("lambda", "core/world_lines"))
.withBlend(BlendFunction.TRANSLUCENT)
- .withDepthWrite(false)
+ .withDepthWrite(false) // No depth write for proper transparency blending
.withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST)
.withCull(false)
.withVertexFormat(
- VertexFormats.POSITION_COLOR_NORMAL_LINE_WIDTH,
+ LambdaVertexFormats.POSITION_COLOR_NORMAL_LINE_WIDTH_DASH,
VertexFormat.DrawMode.QUADS
)
.build()
@@ -65,14 +66,14 @@ object LambdaRenderPipelines : Loadable {
RenderPipelines.register(
RenderPipeline.builder(LAMBDA_ESP_SNIPPET, RenderPipelines.GLOBALS_SNIPPET)
.withLocation(Identifier.of("lambda", "pipeline/esp_lines_through"))
- .withVertexShader(Identifier.of("lambda", "core/advanced_lines"))
- .withFragmentShader(Identifier.of("lambda", "core/advanced_lines"))
+ .withVertexShader(Identifier.of("lambda", "core/world_lines"))
+ .withFragmentShader(Identifier.of("lambda", "core/world_lines"))
.withBlend(BlendFunction.TRANSLUCENT)
.withDepthWrite(false)
.withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
.withCull(false)
.withVertexFormat(
- VertexFormats.POSITION_COLOR_NORMAL_LINE_WIDTH,
+ LambdaVertexFormats.POSITION_COLOR_NORMAL_LINE_WIDTH_DASH,
VertexFormat.DrawMode.QUADS
)
.build()
@@ -89,7 +90,7 @@ object LambdaRenderPipelines : Loadable {
.withVertexShader(Identifier.ofVanilla("core/position_color"))
.withFragmentShader(Identifier.ofVanilla("core/position_color"))
.withBlend(BlendFunction.TRANSLUCENT)
- .withDepthWrite(false)
+ .withDepthWrite(false) // No depth write for proper transparency blending
.withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST)
.withCull(false)
.withVertexFormat(
@@ -116,4 +117,307 @@ object LambdaRenderPipelines : Loadable {
)
.build()
)
+
+ /**
+ * Pipeline for textured text rendering with alpha blending.
+ * Uses position_tex_color shader with Sampler0 for font atlas texture.
+ */
+ val TEXT_QUADS: RenderPipeline =
+ RenderPipelines.register(
+ RenderPipeline.builder(LAMBDA_ESP_SNIPPET)
+ .withLocation(Identifier.of("lambda", "pipeline/text_quads"))
+ .withVertexShader(Identifier.ofVanilla("core/position_tex_color"))
+ .withFragmentShader(Identifier.ofVanilla("core/position_tex_color"))
+ .withSampler("Sampler0")
+ .withBlend(BlendFunction.TRANSLUCENT)
+ .withDepthWrite(false)
+ .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST)
+ .withCull(false)
+ .withVertexFormat(
+ VertexFormats.POSITION_TEXTURE_COLOR,
+ VertexFormat.DrawMode.QUADS
+ )
+ .build()
+ )
+
+ /** Pipeline for text that renders through walls. */
+ val TEXT_QUADS_THROUGH: RenderPipeline =
+ RenderPipelines.register(
+ RenderPipeline.builder(LAMBDA_ESP_SNIPPET)
+ .withLocation(Identifier.of("lambda", "pipeline/text_quads_through"))
+ .withVertexShader(Identifier.ofVanilla("core/position_tex_color"))
+ .withFragmentShader(Identifier.ofVanilla("core/position_tex_color"))
+ .withSampler("Sampler0")
+ .withBlend(BlendFunction.TRANSLUCENT)
+ .withDepthWrite(false)
+ .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
+ .withCull(false)
+ .withVertexFormat(
+ VertexFormats.POSITION_TEXTURE_COLOR,
+ VertexFormat.DrawMode.QUADS
+ )
+ .build()
+ )
+
+ /**
+ * Pipeline for SDF text rendering with proper smoothstep anti-aliasing.
+ * Uses lambda:core/sdf_text shaders with per-vertex style parameters.
+ */
+ val SDF_TEXT: RenderPipeline =
+ RenderPipelines.register(
+ RenderPipeline.builder(LAMBDA_ESP_SNIPPET)
+ .withLocation(Identifier.of("lambda", "pipeline/sdf_text"))
+ .withVertexShader(Identifier.of("lambda", "core/world_sdf_text"))
+ .withFragmentShader(Identifier.of("lambda", "core/world_sdf_text"))
+ .withSampler("Sampler0")
+ .withBlend(BlendFunction.TRANSLUCENT)
+ .withDepthWrite(false) // No depth write for proper transparency blending
+ .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST)
+ .withCull(false)
+ .withVertexFormat(
+ LambdaVertexFormats.POSITION_TEXTURE_COLOR_ANCHOR_SDF,
+ VertexFormat.DrawMode.QUADS
+ )
+ .build()
+ )
+
+ /** SDF text pipeline that renders through walls. */
+ val SDF_TEXT_THROUGH: RenderPipeline =
+ RenderPipelines.register(
+ RenderPipeline.builder(LAMBDA_ESP_SNIPPET)
+ .withLocation(Identifier.of("lambda", "pipeline/sdf_text_through"))
+ .withVertexShader(Identifier.of("lambda", "core/world_sdf_text"))
+ .withFragmentShader(Identifier.of("lambda", "core/world_sdf_text"))
+ .withSampler("Sampler0")
+ .withBlend(BlendFunction.TRANSLUCENT)
+ .withDepthWrite(false)
+ .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
+ .withCull(false)
+ .withVertexFormat(
+ LambdaVertexFormats.POSITION_TEXTURE_COLOR_ANCHOR_SDF,
+ VertexFormat.DrawMode.QUADS
+ )
+ .build()
+ )
+
+ // ============================================================================
+ // Screen-Space Pipelines (with layer-based depth for draw order)
+ // ============================================================================
+
+ /**
+ * Pipeline for screen-space faces/quads.
+ * Uses custom shader with layer support for draw order preservation.
+ */
+ val SCREEN_FACES: RenderPipeline =
+ RenderPipelines.register(
+ RenderPipeline.builder(LAMBDA_ESP_SNIPPET)
+ .withLocation(Identifier.of("lambda", "pipeline/screen_faces"))
+ .withVertexShader(Identifier.of("lambda", "core/screen_faces"))
+ .withFragmentShader(Identifier.of("lambda", "core/screen_faces"))
+ .withBlend(BlendFunction.TRANSLUCENT)
+ .withDepthWrite(true) // Enable depth write for layer ordering
+ .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST) // Enable depth test
+ .withCull(false)
+ .withVertexFormat(
+ LambdaVertexFormats.SCREEN_FACE_FORMAT,
+ VertexFormat.DrawMode.QUADS
+ )
+ .build()
+ )
+
+ /**
+ * Pipeline for screen-space lines.
+ * Uses a custom vertex format with 2D direction for perpendicular offset calculation.
+ * Includes layer support for draw order preservation.
+ */
+ val SCREEN_LINES: RenderPipeline =
+ RenderPipelines.register(
+ RenderPipeline.builder(LAMBDA_ESP_SNIPPET, RenderPipelines.GLOBALS_SNIPPET)
+ .withLocation(Identifier.of("lambda", "pipeline/screen_lines"))
+ .withVertexShader(Identifier.of("lambda", "core/screen_lines"))
+ .withFragmentShader(Identifier.of("lambda", "core/screen_lines"))
+ .withBlend(BlendFunction.TRANSLUCENT)
+ .withDepthWrite(true) // Enable depth write for layer ordering
+ .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST) // Depth test for layer ordering
+ .withCull(false)
+ .withVertexFormat(
+ LambdaVertexFormats.SCREEN_LINE_FORMAT,
+ VertexFormat.DrawMode.QUADS
+ )
+ .build()
+ )
+
+ /**
+ * Pipeline for screen-space SDF text rendering.
+ * Uses custom SDF shader with per-vertex style parameters for anti-aliased text with effects.
+ * Includes layer support for draw order preservation.
+ */
+ val SCREEN_TEXT: RenderPipeline =
+ RenderPipelines.register(
+ RenderPipeline.builder(LAMBDA_ESP_SNIPPET)
+ .withLocation(Identifier.of("lambda", "pipeline/screen_text"))
+ .withVertexShader(Identifier.of("lambda", "core/screen_sdf_text"))
+ .withFragmentShader(Identifier.of("lambda", "core/screen_sdf_text"))
+ .withSampler("Sampler0")
+ .withBlend(BlendFunction.TRANSLUCENT)
+ .withDepthWrite(true) // Enable depth write for layer ordering
+ .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST) // Enable depth test
+ .withCull(false)
+ .withVertexFormat(
+ LambdaVertexFormats.SCREEN_TEXT_SDF_FORMAT,
+ VertexFormat.DrawMode.QUADS
+ )
+ .build()
+ )
+
+ // ============================================================================
+ // Image Rendering Pipelines (with glint overlay support)
+ // ============================================================================
+
+ /**
+ * Pipeline for screen-space image rendering with overlay support.
+ * Uses two samplers: Sampler0 for main texture, Sampler1 for overlay (glint).
+ */
+ val SCREEN_IMAGE: RenderPipeline =
+ RenderPipelines.register(
+ RenderPipeline.builder(LAMBDA_ESP_SNIPPET, RenderPipelines.GLOBALS_SNIPPET)
+ .withLocation(Identifier.of("lambda", "pipeline/screen_image"))
+ .withVertexShader(Identifier.of("lambda", "core/screen_image"))
+ .withFragmentShader(Identifier.of("lambda", "core/screen_image"))
+ .withSampler("Sampler0")
+ .withSampler("Sampler1")
+ .withBlend(BlendFunction.TRANSLUCENT)
+ .withDepthWrite(true) // Enable depth write for layer ordering
+ .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST)
+ .withCull(false)
+ .withVertexFormat(
+ LambdaVertexFormats.SCREEN_IMAGE_FORMAT,
+ VertexFormat.DrawMode.QUADS
+ )
+ .build()
+ )
+
+ /**
+ * Pipeline for world-space billboard image rendering with overlay support.
+ * Uses anchor-based positioning with optional billboarding.
+ */
+ val WORLD_IMAGE: RenderPipeline =
+ RenderPipelines.register(
+ RenderPipeline.builder(LAMBDA_ESP_SNIPPET, RenderPipelines.GLOBALS_SNIPPET)
+ .withLocation(Identifier.of("lambda", "pipeline/world_image"))
+ .withVertexShader(Identifier.of("lambda", "core/world_image"))
+ .withFragmentShader(Identifier.of("lambda", "core/world_image"))
+ .withSampler("Sampler0")
+ .withSampler("Sampler1")
+ .withBlend(BlendFunction.TRANSLUCENT)
+ .withDepthWrite(false) // No depth write for proper transparency blending
+ .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST)
+ .withCull(false)
+ .withVertexFormat(
+ LambdaVertexFormats.WORLD_IMAGE_FORMAT,
+ VertexFormat.DrawMode.QUADS
+ )
+ .build()
+ )
+
+ /**
+ * Pipeline for world-space billboard image rendering that renders through walls.
+ */
+ val WORLD_IMAGE_THROUGH: RenderPipeline =
+ RenderPipelines.register(
+ RenderPipeline.builder(LAMBDA_ESP_SNIPPET, RenderPipelines.GLOBALS_SNIPPET)
+ .withLocation(Identifier.of("lambda", "pipeline/world_image_through"))
+ .withVertexShader(Identifier.of("lambda", "core/world_image"))
+ .withFragmentShader(Identifier.of("lambda", "core/world_image"))
+ .withSampler("Sampler0")
+ .withSampler("Sampler1")
+ .withBlend(BlendFunction.TRANSLUCENT)
+ .withDepthWrite(false)
+ .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
+ .withCull(false)
+ .withVertexFormat(
+ LambdaVertexFormats.WORLD_IMAGE_FORMAT,
+ VertexFormat.DrawMode.QUADS
+ )
+ .build()
+ )
+
+ /**
+ * Pipeline for world-space 3D model rendering.
+ * Supports Position, Color, UV0 (Atlas), OverlayUV (Overlay), UV2 (Lightmap), Normal.
+ */
+ val WORLD_MODEL: RenderPipeline =
+ RenderPipelines.register(
+ RenderPipeline.builder(LAMBDA_ESP_SNIPPET)
+ .withLocation(Identifier.of("lambda", "pipeline/world_model"))
+ .withVertexShader(Identifier.of("lambda", "core/world_model"))
+ .withFragmentShader(Identifier.of("lambda", "core/world_model"))
+ .withSampler("Sampler0") // Atlas
+ .withSampler("Sampler1") // Overlay
+ .withSampler("Sampler2") // Lightmap
+ .withSampler("Sampler3") // Glint
+ .withUniform("GlintTransforms", UniformType.UNIFORM_BUFFER)
+ .withBlend(BlendFunction.TRANSLUCENT)
+ .withDepthWrite(true)
+ .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST)
+ .withCull(false)
+ .withVertexFormat(
+ LambdaVertexFormats.WORLD_MODEL_FORMAT,
+ VertexFormat.DrawMode.QUADS
+ )
+ .build()
+ )
+
+ /**
+ * Pipeline for screen-space 3D model rendering.
+ * Same as WORLD_MODEL but without lightmap sampler (Sampler2).
+ * Includes culling to prevent backfaces from clipping with front faces.
+ */
+ val SCREEN_MODEL: RenderPipeline =
+ RenderPipelines.register(
+ RenderPipeline.builder(LAMBDA_ESP_SNIPPET)
+ .withLocation(Identifier.of("lambda", "pipeline/screen_model"))
+ .withVertexShader(Identifier.of("lambda", "core/world_model"))
+ .withFragmentShader(Identifier.of("lambda", "core/world_model"))
+ .withSampler("Sampler0") // Atlas
+ .withSampler("Sampler1") // Overlay
+ .withSampler("Sampler2") // Lightmap (White/Neutral in screen space)
+ .withSampler("Sampler3") // Glint
+ .withUniform("GlintTransforms", UniformType.UNIFORM_BUFFER)
+ .withBlend(BlendFunction.TRANSLUCENT)
+ .withDepthWrite(true)
+ .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST)
+ .withCull(false)
+ .withVertexFormat(
+ LambdaVertexFormats.WORLD_MODEL_FORMAT,
+ VertexFormat.DrawMode.QUADS
+ )
+ .build()
+ )
+
+ /**
+ * Pipeline for world-space 3D model rendering that renders through walls.
+ */
+ val WORLD_MODEL_THROUGH: RenderPipeline =
+ RenderPipelines.register(
+ RenderPipeline.builder(LAMBDA_ESP_SNIPPET)
+ .withLocation(Identifier.of("lambda", "pipeline/world_model_through"))
+ .withVertexShader(Identifier.of("lambda", "core/world_model"))
+ .withFragmentShader(Identifier.of("lambda", "core/world_model"))
+ .withSampler("Sampler0")
+ .withSampler("Sampler1")
+ .withSampler("Sampler2")
+ .withSampler("Sampler3") // Glint
+ .withUniform("GlintTransforms", UniformType.UNIFORM_BUFFER)
+ .withBlend(BlendFunction.TRANSLUCENT)
+ .withDepthWrite(false)
+ .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
+ .withCull(false)
+ .withVertexFormat(
+ LambdaVertexFormats.WORLD_MODEL_FORMAT,
+ VertexFormat.DrawMode.QUADS
+ )
+ .build()
+ )
}
+
diff --git a/src/main/kotlin/com/lambda/graphics/mc/LambdaVertexFormats.kt b/src/main/kotlin/com/lambda/graphics/mc/LambdaVertexFormats.kt
new file mode 100644
index 000000000..6b076d8a4
--- /dev/null
+++ b/src/main/kotlin/com/lambda/graphics/mc/LambdaVertexFormats.kt
@@ -0,0 +1,375 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.graphics.mc
+
+import com.mojang.blaze3d.vertex.VertexFormat
+import com.mojang.blaze3d.vertex.VertexFormatElement
+
+/**
+ * Custom vertex formats for Lambda's advanced rendering features.
+ * Extends Minecraft's standard formats with additional attributes.
+ */
+object LambdaVertexFormats {
+ /**
+ * Custom vertex format element for Normal as 3 floats.
+ * MC's NORMAL uses signed bytes which is unsuitable for world-space direction vectors.
+ */
+ val NORMAL_FLOAT: VertexFormatElement = VertexFormatElement.register(
+ 30, // ID
+ 0, // index
+ VertexFormatElement.Type.FLOAT,
+ VertexFormatElement.Usage.NORMAL,
+ 3 // count (x, y, z direction)
+ )
+
+ /**
+ * Custom vertex format element for LineWidth as float.
+ * Ensures we get a proper float value in the shader.
+ */
+ val LINE_WIDTH_FLOAT: VertexFormatElement = VertexFormatElement.register(
+ 29, // ID
+ 0, // index
+ VertexFormatElement.Type.FLOAT,
+ VertexFormatElement.Usage.GENERIC,
+ 1 // count (single float)
+ )
+
+ /**
+ * Custom vertex format element for dash parameters.
+ * Contains: dashLength, gapLength, dashOffset, animationSpeed (as vec4 of floats)
+ *
+ * Uses ID 31 (high value to avoid conflicts with Minecraft/mods).
+ * Uses index 0 and GENERIC usage since this is a custom attribute.
+ */
+ val DASH_ELEMENT: VertexFormatElement = VertexFormatElement.register(
+ 31, // ID (use high value to avoid conflicts with MC/mods)
+ 0, // index
+ VertexFormatElement.Type.FLOAT,
+ VertexFormatElement.Usage.GENERIC,
+ 4 // count (dashLength, gapLength, dashOffset, animationSpeed)
+ )
+
+ /**
+ * Anchor position element for billboard text.
+ * Contains the world-space position (camera-relative) that the text is anchored to.
+ */
+ val ANCHOR_ELEMENT: VertexFormatElement = VertexFormatElement.register(
+ 20, // ID (unique, in valid range [0, 32))
+ 0, // index
+ VertexFormatElement.Type.FLOAT,
+ VertexFormatElement.Usage.GENERIC,
+ 3 // count (x, y, z anchor position)
+ )
+
+ /**
+ * Billboard data element for text rendering.
+ * Contains: scale, billboard flag (0 = billboard towards camera, non-zero = use rotation)
+ */
+ val BILLBOARD_DATA_ELEMENT: VertexFormatElement = VertexFormatElement.register(
+ 21, // ID (unique, in valid range [0, 32))
+ 0, // index
+ VertexFormatElement.Type.FLOAT,
+ VertexFormatElement.Usage.GENERIC,
+ 2 // count (scale, billboardFlag)
+ )
+
+ /**
+ * Extended line format with dash support.
+ * Layout: Position (vec3), Color (vec4), Normal (vec3 FLOAT), LineWidth (float), Dash (vec4)
+ *
+ * Total size: 12 + 4 + 12 + 4 + 16 = 48 bytes
+ *
+ * - Position: World-space vertex position (3 floats = 12 bytes)
+ * - Color: RGBA color (4 bytes)
+ * - Normal: Segment direction vector as FLOATS (3 floats = 12 bytes)
+ * - LineWidth: Per-vertex line width in world units (1 float = 4 bytes)
+ * - Dash: vec4(dashLength, gapLength, dashOffset, animationSpeed) (4 floats = 16 bytes)
+ */
+ val POSITION_COLOR_NORMAL_LINE_WIDTH_DASH: VertexFormat = VertexFormat.builder()
+ .add("Position", VertexFormatElement.POSITION)
+ .add("Color", VertexFormatElement.COLOR)
+ .add("Normal", NORMAL_FLOAT)
+ .add("LineWidth", LINE_WIDTH_FLOAT)
+ .add("Dash", DASH_ELEMENT)
+ .build()
+
+ /**
+ * Billboard text format with anchor position for GPU-based billboard rotation.
+ * Layout: Position (vec3), UV0 (vec2), Color (vec4), Anchor (vec3), BillboardData (vec2)
+ *
+ * Total size: 12 + 8 + 4 + 12 + 8 = 44 bytes
+ *
+ * - Position: Local glyph offset (x, y) with z unused (3 floats = 12 bytes)
+ * - UV0: Texture coordinates (2 floats = 8 bytes)
+ * - Color: RGBA color with alpha encoding layer type (4 bytes)
+ * - Anchor: Camera-relative world position of text anchor (3 floats = 12 bytes)
+ * - BillboardData: vec2(scale, billboardFlag) where billboardFlag 0 = auto-billboard (2 floats = 8 bytes)
+ */
+ val POSITION_TEXTURE_COLOR_ANCHOR: VertexFormat = VertexFormat.builder()
+ .add("Position", VertexFormatElement.POSITION)
+ .add("UV0", VertexFormatElement.UV0)
+ .add("Color", VertexFormatElement.COLOR)
+ .add("Anchor", ANCHOR_ELEMENT)
+ .add("BillboardData", BILLBOARD_DATA_ELEMENT)
+ .build()
+
+ /**
+ * 2D direction element for screen-space lines.
+ * Contains the line direction vector (dx, dy) used to compute perpendicular offset.
+ */
+ val DIRECTION_2D_ELEMENT: VertexFormatElement = VertexFormatElement.register(
+ 22, // ID (unique, in valid range [0, 32))
+ 0, // index
+ VertexFormatElement.Type.FLOAT,
+ VertexFormatElement.Usage.GENERIC,
+ 2 // count (dx, dy)
+ )
+
+ /**
+ * Layer depth element for screen-space ordering.
+ * Contains a single float representing the draw order (higher = on top).
+ */
+ val LAYER_ELEMENT: VertexFormatElement = VertexFormatElement.register(
+ 24, // ID (unique, in valid range [0, 32))
+ 0, // index
+ VertexFormatElement.Type.FLOAT,
+ VertexFormatElement.Usage.GENERIC,
+ 1 // count (single float: layer depth)
+ )
+
+ /**
+ * Screen-space face format with layer support for draw order preservation.
+ * Layout: Position (vec3), Color (vec4), Layer (float)
+ *
+ * Total size: 12 + 4 + 4 = 20 bytes
+ *
+ * - Position: Screen-space position (x, y, z=0) (3 floats = 12 bytes)
+ * - Color: RGBA color (4 bytes)
+ * - Layer: Depth for layering (1 float = 4 bytes)
+ */
+ val SCREEN_FACE_FORMAT: VertexFormat = VertexFormat.builder()
+ .add("Position", VertexFormatElement.POSITION)
+ .add("Color", VertexFormatElement.COLOR)
+ .add("Layer", LAYER_ELEMENT)
+ .build()
+
+ /**
+ * Screen-space line format with dash support and layer for draw order.
+ * Layout: Position (vec3), Color (vec4), Direction2D (vec2), LineWidth (float), Dash (vec4), Layer (float)
+ *
+ * Total size: 12 + 4 + 8 + 4 + 16 + 4 = 48 bytes
+ *
+ * - Position: Screen-space position (x, y, z where z = 0) (3 floats = 12 bytes)
+ * - Color: RGBA color (4 bytes)
+ * - Direction2D: Line direction for perpendicular offset (2 floats = 8 bytes)
+ * - LineWidth: Line width in pixels (1 float = 4 bytes)
+ * - Dash: vec4(dashLength, gapLength, dashOffset, animationSpeed) (4 floats = 16 bytes)
+ * - Layer: Depth for layering (1 float = 4 bytes)
+ */
+ val SCREEN_LINE_FORMAT: VertexFormat = VertexFormat.builder()
+ .add("Position", VertexFormatElement.POSITION)
+ .add("Color", VertexFormatElement.COLOR)
+ .add("Direction", DIRECTION_2D_ELEMENT)
+ .add("LineWidth", LINE_WIDTH_FLOAT)
+ .add("Dash", DASH_ELEMENT)
+ .add("Layer", LAYER_ELEMENT)
+ .build()
+
+ // ============================================================================
+ // SDF Text Style Vertex Attributes (replaces SDFParams uniform buffer)
+ // ============================================================================
+
+ /**
+ * SDF style parameters as vertex attributes.
+ * Contains: OutlineWidth, GlowRadius, ShadowSoftness, SDFThreshold (as vec4 of floats)
+ *
+ * This replaces the SDFParams uniform buffer, enabling per-vertex style control
+ * and eliminating the need for style-based batching.
+ */
+ val SDF_STYLE_ELEMENT: VertexFormatElement = VertexFormatElement.register(
+ 23, // ID (unique, in valid range [0, 32))
+ 0, // index
+ VertexFormatElement.Type.FLOAT,
+ VertexFormatElement.Usage.GENERIC,
+ 4 // count (outlineWidth, glowRadius, shadowSoftness, sdfThreshold)
+ )
+
+ /**
+ * Billboard text format with anchor position AND SDF style parameters.
+ * Layout: Position (vec3), UV0 (vec2), Color (vec4), Anchor (vec3), BillboardData (vec2), SDFStyle (vec4)
+ *
+ * Total size: 12 + 8 + 4 + 12 + 8 + 16 = 60 bytes
+ *
+ * - Position: Local glyph offset (x, y) with z unused (3 floats = 12 bytes)
+ * - UV0: Texture coordinates (2 floats = 8 bytes)
+ * - Color: RGBA color with alpha encoding layer type (4 bytes)
+ * - Anchor: Camera-relative world position of text anchor (3 floats = 12 bytes)
+ * - BillboardData: vec2(scale, billboardFlag) (2 floats = 8 bytes)
+ * - SDFStyle: vec4(outlineWidth, glowRadius, shadowSoftness, threshold) (4 floats = 16 bytes)
+ */
+ val POSITION_TEXTURE_COLOR_ANCHOR_SDF: VertexFormat = VertexFormat.builder()
+ .add("Position", VertexFormatElement.POSITION)
+ .add("UV0", VertexFormatElement.UV0)
+ .add("Color", VertexFormatElement.COLOR)
+ .add("Anchor", ANCHOR_ELEMENT)
+ .add("BillboardData", BILLBOARD_DATA_ELEMENT)
+ .add("SDFStyle", SDF_STYLE_ELEMENT)
+ .build()
+
+ /**
+ * Screen-space text format with SDF style parameters and layer for draw order.
+ * Layout: Position (vec3), UV0 (vec2), Color (vec4), SDFStyle (vec4), Layer (float)
+ *
+ * Total size: 12 + 8 + 4 + 16 + 4 = 44 bytes
+ *
+ * - Position: Screen-space position (x, y, z=0) (3 floats = 12 bytes)
+ * - UV0: Texture coordinates (2 floats = 8 bytes)
+ * - Color: RGBA color with alpha encoding layer type (4 bytes)
+ * - SDFStyle: vec4(outlineWidth, glowRadius, shadowSoftness, threshold) (4 floats = 16 bytes)
+ * - Layer: Depth for layering (1 float = 4 bytes)
+ */
+ val SCREEN_TEXT_SDF_FORMAT: VertexFormat = VertexFormat.builder()
+ .add("Position", VertexFormatElement.POSITION)
+ .add("UV0", VertexFormatElement.UV0)
+ .add("Color", VertexFormatElement.COLOR)
+ .add("SDFStyle", SDF_STYLE_ELEMENT)
+ .add("Layer", LAYER_ELEMENT)
+ .build()
+
+ // ============================================================================
+ // Image Rendering Vertex Formats
+ // ============================================================================
+
+ /**
+ * Overlay UV element for image rendering with overlay textures (e.g., enchantment glint).
+ * Contains: overlayU, overlayV, hasOverlay, diffuseAmount (as vec4 of floats)
+ */
+ val OVERLAY_UV_ELEMENT: VertexFormatElement = VertexFormatElement.register(
+ 25, // ID (unique, in valid range [0, 32))
+ 0, // index
+ VertexFormatElement.Type.FLOAT,
+ VertexFormatElement.Usage.GENERIC,
+ 4 // count (overlayU, overlayV, hasOverlay, diffuseAmount)
+ )
+
+ /**
+ * Screen-space image format with overlay support and layer for draw order.
+ * Layout: Position (vec3), UV0 (vec2), Color (vec4), OverlayUV (vec3), Layer (float)
+ *
+ * Total size: 12 + 8 + 4 + 16 + 4 = 44 bytes
+ *
+ * - Position: Screen-space position (x, y, z=0) (3 floats = 12 bytes)
+ * - UV0: Main texture coordinates (2 floats = 8 bytes)
+ * - Color: RGBA tint color (4 bytes)
+ * - OverlayUV: vec4(overlayU, overlayV, hasOverlay, diffuseAmount) (4 floats = 16 bytes)
+ * - Layer: Depth for layering (1 float = 4 bytes)
+ */
+ val SCREEN_IMAGE_FORMAT: VertexFormat = VertexFormat.builder()
+ .add("Position", VertexFormatElement.POSITION)
+ .add("UV0", VertexFormatElement.UV0)
+ .add("Color", VertexFormatElement.COLOR)
+ .add("OverlayUV", OVERLAY_UV_ELEMENT)
+ .add("Layer", LAYER_ELEMENT)
+ .build()
+
+ /**
+ * World-space image format with anchor for billboarding and overlay support.
+ * Layout: Position (vec3), UV0 (vec2), Color (vec4), Anchor (vec3), BillboardData (vec2), OverlayUV (vec3)
+ *
+ * Total size: 12 + 8 + 4 + 12 + 8 + 16 = 60 bytes
+ *
+ * - Position: Local offset (x, y) with z unused (3 floats = 12 bytes)
+ * - UV0: Main texture coordinates (2 floats = 8 bytes)
+ * - Color: RGBA tint color (4 bytes)
+ * - Anchor: Camera-relative world position (3 floats = 12 bytes)
+ * - BillboardData: vec2(scale, billboardFlag) (2 floats = 8 bytes)
+ * - OverlayUV: vec4(overlayU, overlayV, hasOverlay, diffuseAmount) (4 floats = 16 bytes)
+ */
+ val WORLD_IMAGE_FORMAT: VertexFormat = VertexFormat.builder()
+ .add("Position", VertexFormatElement.POSITION)
+ .add("UV0", VertexFormatElement.UV0)
+ .add("Color", VertexFormatElement.COLOR)
+ .add("Anchor", ANCHOR_ELEMENT)
+ .add("BillboardData", BILLBOARD_DATA_ELEMENT)
+ .add("OverlayUV", OVERLAY_UV_ELEMENT)
+ .build()
+ /**
+ * Edge data element for analytic geometry anti-aliasing.
+ * Contains face-relative coordinates (0.0 to 1.0) for edge distance calculation.
+ */
+ val EDGE_DATA_ELEMENT: VertexFormatElement = VertexFormatElement.register(
+ 26, // ID (unique, in valid range [0, 32))
+ 0, // index
+ VertexFormatElement.Type.FLOAT,
+ VertexFormatElement.Usage.GENERIC,
+ 2 // count (faceX, faceY)
+ )
+
+ /**
+ * Custom light direction element for per-item shading (primary light).
+ * Contains the world-space light direction vector.
+ */
+ val LIGHT_DIR_ELEMENT: VertexFormatElement = VertexFormatElement.register(
+ 27, // ID (unique, in valid range [0, 32))
+ 0, // index
+ VertexFormatElement.Type.FLOAT,
+ VertexFormatElement.Usage.GENERIC,
+ 3 // count (x, y, z light direction)
+ )
+
+ /**
+ * Secondary light direction element for vanilla's two-light shading.
+ * Contains the fill light direction vector.
+ */
+ val LIGHT1_DIR_ELEMENT: VertexFormatElement = VertexFormatElement.register(
+ 28, // ID (unique, in valid range [0, 32))
+ 0, // index
+ VertexFormatElement.Type.FLOAT,
+ VertexFormatElement.Usage.GENERIC,
+ 3 // count (x, y, z light direction)
+ )
+
+ /**
+ * World-space model format with overlay, lightmap, normals, edge data, and custom lighting.
+ * Layout: Position (vec3), Color (vec4), UV0 (vec2), OverlayUV (vec4), Light (vec2), LightDir (vec3), Light1Dir (vec3), Normal (vec3), EdgeData (vec2)
+ *
+ * Total size: 12 + 4 + 8 + 16 + 4 + 12 + 12 + 12 + 8 = 88 bytes
+ *
+ * - Position: World-space position (3 floats = 12 bytes)
+ * - Color: RGBA tint color (4 bytes)
+ * - UV0: Main texture coordinates (2 floats = 8 bytes)
+ * - OverlayUV: vec4(overlayU, overlayV, hasOverlay, diffuseAmount) (4 floats = 16 bytes)
+ * - Light: Lightmap coordinates (2 shorts = 4 bytes)
+ * - LightDir: Primary light direction (3 floats = 12 bytes)
+ * - Light1Dir: Fill light direction (3 floats = 12 bytes)
+ * - Normal: Normal vector as floats (3 floats = 12 bytes)
+ * - EdgeData: Face-relative coordinates for AA (2 floats = 8 bytes)
+ */
+ val WORLD_MODEL_FORMAT: VertexFormat = VertexFormat.builder()
+ .add("Position", VertexFormatElement.POSITION)
+ .add("Color", VertexFormatElement.COLOR)
+ .add("UV0", VertexFormatElement.UV0)
+ .add("OverlayUV", OVERLAY_UV_ELEMENT)
+ .add("Light", VertexFormatElement.UV2)
+ .add("LightDir", LIGHT_DIR_ELEMENT)
+ .add("Light1Dir", LIGHT1_DIR_ELEMENT)
+ .add("Normal", NORMAL_FLOAT)
+ .add("EdgeData", EDGE_DATA_ELEMENT)
+ .build()
+}
+
diff --git a/src/main/kotlin/com/lambda/graphics/mc/LineDashStyle.kt b/src/main/kotlin/com/lambda/graphics/mc/LineDashStyle.kt
new file mode 100644
index 000000000..e1fad6768
--- /dev/null
+++ b/src/main/kotlin/com/lambda/graphics/mc/LineDashStyle.kt
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.graphics.mc
+
+/**
+ * Configuration for dashed line rendering in world-space.
+ *
+ * All measurements are in WORLD UNITS (blocks). For example:
+ * - dashLength = 0.5 means each dash is half a block long
+ * - gapLength = 0.25 means gaps are a quarter block
+ *
+ * When applied to lines, creates a repeating dash pattern where visible segments
+ * alternate with gaps. The pattern repeats with a period of (dashLength + gapLength).
+ *
+ * Animation is now handled by the shader using Minecraft's GameTime, so the
+ * animated/animationSpeed properties control whether animation is enabled.
+ *
+ * @property dashLength Length of each visible dash segment in world units (blocks)
+ * @property gapLength Length of each invisible gap segment in world units (blocks)
+ * @property offset Phase offset to shift the pattern along the line (0.0 to 1.0, normalized)
+ * @property animated If true, the dash pattern animates (marching ants effect)
+ * @property animationSpeed Speed multiplier for animation (higher = faster marching)
+ *
+ * Usage:
+ * ```
+ * // Simple dashed line (0.5 block dash, 0.25 block gap)
+ * val dashed = LineDashStyle(dashLength = 0.5f, gapLength = 0.25f)
+ *
+ * // Dotted line (equal dash and gap, 0.15 blocks each)
+ * val dotted = LineDashStyle.dotted()
+ *
+ * // Animated marching ants for selection highlight
+ * val marching = LineDashStyle.marchingAnts()
+ * ```
+ */
+data class LineDashStyle(
+ val dashLength: Float = 0.5f,
+ val gapLength: Float = 0.25f,
+ val offset: Float = 0f,
+ val animated: Boolean = false,
+ val animationSpeed: Float = 1f
+) {
+ init {
+ require(dashLength > 0f) { "dashLength must be positive" }
+ require(gapLength >= 0f) { "gapLength must be non-negative" }
+ require(offset in 0f..1f) { "offset must be between 0.0 and 1.0" }
+ }
+
+ /** Total length of one dash+gap cycle in world units. */
+ val cycleLength: Float get() = dashLength + gapLength
+
+ /** Ratio of the dash portion (0.0 to 1.0) within each cycle. */
+ val dashRatio: Float get() = dashLength / cycleLength
+
+ companion object {
+ /** No dashing - solid line. */
+ val SOLID: LineDashStyle? = null
+
+ /**
+ * Create a dotted pattern with equal dash and gap lengths.
+ * Default: 0.15 blocks each (small dots)
+ */
+ fun dotted(size: Float = 0.15f) = LineDashStyle(
+ dashLength = size,
+ gapLength = size
+ )
+
+ /**
+ * Create an animated "marching ants" selection pattern.
+ * Default: 0.4 block dash, 0.2 block gap, animated
+ */
+ fun marchingAnts(
+ dashLength: Float = 0.4f,
+ gapLength: Float = 0.2f,
+ speed: Float = 1f
+ ) = LineDashStyle(
+ dashLength = dashLength,
+ gapLength = gapLength,
+ animated = true,
+ animationSpeed = speed
+ )
+
+ /**
+ * Create a long-dash pattern (3:1 dash to gap ratio).
+ * Default: 0.75 block dash, 0.25 block gap
+ */
+ fun longDash(dashLength: Float = 0.75f) = LineDashStyle(
+ dashLength = dashLength,
+ gapLength = dashLength / 3f
+ )
+
+ /**
+ * Create a short-dash pattern (1:1 ratio, larger than dotted).
+ * Default: 0.3 block dash and gap
+ */
+ fun shortDash(size: Float = 0.3f) = LineDashStyle(
+ dashLength = size,
+ gapLength = size
+ )
+
+ // ============================================================================
+ // Screen-Space Convenience Methods (Normalized 0-1 Coordinates)
+ // ============================================================================
+ // These use screen-normalized units where 1.0 = full screen dimension.
+ // Typical values: 0.01 = 1% of screen, 0.02 = 2% of screen, etc.
+
+ /**
+ * Create a dotted pattern for screen-space rendering.
+ * Default: 0.01 (1% of screen) for each dot and gap
+ */
+ fun screenDotted(size: Float = 0.01f) = LineDashStyle(
+ dashLength = size,
+ gapLength = size
+ )
+
+ /**
+ * Create an animated "marching ants" selection pattern for screen-space.
+ * Great for selection boxes and interactive UI elements.
+ * Default: 0.02 dash, 0.01 gap (2% dash, 1% gap)
+ */
+ fun screenMarchingAnts(
+ dashLength: Float = 0.02f,
+ gapLength: Float = 0.01f,
+ speed: Float = 1f
+ ) = LineDashStyle(
+ dashLength = dashLength,
+ gapLength = gapLength,
+ animated = true,
+ animationSpeed = speed
+ )
+
+ /**
+ * Create a dashed pattern for screen-space rendering.
+ * Default: 0.03 dash, 0.015 gap (3% dash, 1.5% gap)
+ */
+ fun screenDashed(dashLength: Float = 0.03f, gapLength: Float = 0.015f) = LineDashStyle(
+ dashLength = dashLength,
+ gapLength = gapLength
+ )
+
+ /**
+ * Create a short-dash pattern for screen-space rendering.
+ * Default: 0.015 (1.5% of screen) for each dash and gap
+ */
+ fun screenShortDash(size: Float = 0.015f) = LineDashStyle(
+ dashLength = size,
+ gapLength = size
+ )
+
+ /**
+ * Create a long-dash pattern for screen-space rendering.
+ * Default: 0.04 dash, 0.013 gap (4% dash, ~1.3% gap - 3:1 ratio)
+ */
+ fun screenLongDash(dashLength: Float = 0.04f) = LineDashStyle(
+ dashLength = dashLength,
+ gapLength = dashLength / 3f
+ )
+ }
+}
diff --git a/src/main/kotlin/com/lambda/graphics/mc/RegionRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/RegionRenderer.kt
index 8d7f36f4a..dab46a533 100644
--- a/src/main/kotlin/com/lambda/graphics/mc/RegionRenderer.kt
+++ b/src/main/kotlin/com/lambda/graphics/mc/RegionRenderer.kt
@@ -18,6 +18,7 @@
package com.lambda.graphics.mc
import com.lambda.Lambda.mc
+import com.lambda.graphics.mc.renderer.RendererUtils
import com.mojang.blaze3d.buffers.GpuBuffer
import com.mojang.blaze3d.systems.RenderPass
import com.mojang.blaze3d.systems.RenderSystem
@@ -25,25 +26,41 @@ import com.mojang.blaze3d.vertex.VertexFormat
import java.util.*
/**
- * Region-based renderer for ESP rendering using MC 1.21.11's new render pipeline.
+ * Renderer for ESP rendering using MC 1.21.11's new render pipeline.
*
- * This renderer manages the lifecycle of dedicated GPU buffers for a specific region and provides
+ * This renderer manages the lifecycle of dedicated GPU buffers and provides
* methods to render them within a RenderPass.
- *
- * @param region The render region this renderer is associated with
*/
-class RegionRenderer(val region: RenderRegion) {
-
- // Dedicated GPU buffers for faces and edges
+class RegionRenderer {
+ // Dedicated GPU buffers for world-space faces, edges, and text
private var faceVertexBuffer: GpuBuffer? = null
private var edgeVertexBuffer: GpuBuffer? = null
+ private var textVertexBuffer: GpuBuffer? = null
+
+ // Dedicated GPU buffers for screen-space faces, edges, and text
+ private var screenFaceVertexBuffer: GpuBuffer? = null
+ private var screenEdgeVertexBuffer: GpuBuffer? = null
+ private var screenTextVertexBuffer: GpuBuffer? = null
+
+ // Image batches (texture -> buffer) for screen and world space
+ private var screenImageBatches: List = emptyList()
+ private var worldImageBatches: List = emptyList()
+ private var modelBatches: List = emptyList()
+ private var screenModelBatches: List = emptyList()
- // Index counts for draw calls
+ // Index counts for world-space draw calls
private var faceIndexCount = 0
private var edgeIndexCount = 0
+ private var textIndexCount = 0
+
+ // Index counts for screen-space draw calls
+ private var screenFaceIndexCount = 0
+ private var screenEdgeIndexCount = 0
+ private var screenTextIndexCount = 0
// State tracking
private var hasData = false
+ private var hasScreenData = false
/**
* Upload collected vertices from an external collector. This must be called on the main/render
@@ -53,19 +70,52 @@ class RegionRenderer(val region: RenderRegion) {
*/
fun upload(collector: RegionVertexCollector) {
val result = collector.upload()
+ val screenResult = collector.uploadScreen()
- // Cleanup old buffers
+ // Cleanup old world-space buffers
faceVertexBuffer?.close()
edgeVertexBuffer?.close()
+ textVertexBuffer?.close()
+
+ // Cleanup old screen-space buffers
+ screenFaceVertexBuffer?.close()
+ screenEdgeVertexBuffer?.close()
+ screenTextVertexBuffer?.close()
- // Assign new buffers and counts
+ // Assign new world-space buffers and counts
faceVertexBuffer = result.faces?.buffer
faceIndexCount = result.faces?.indexCount ?: 0
edgeVertexBuffer = result.edges?.buffer
edgeIndexCount = result.edges?.indexCount ?: 0
- hasData = faceVertexBuffer != null || edgeVertexBuffer != null
+ textVertexBuffer = result.text?.buffer
+ textIndexCount = result.text?.indexCount ?: 0
+
+ // Assign new screen-space buffers and counts
+ screenFaceVertexBuffer = screenResult.faces?.buffer
+ screenFaceIndexCount = screenResult.faces?.indexCount ?: 0
+
+ screenEdgeVertexBuffer = screenResult.edges?.buffer
+ screenEdgeIndexCount = screenResult.edges?.indexCount ?: 0
+
+ screenTextVertexBuffer = screenResult.text?.buffer
+ screenTextIndexCount = screenResult.text?.indexCount ?: 0
+
+ // Clean up old image batches
+ screenImageBatches.forEach { it.buffer.close() }
+ worldImageBatches.forEach { it.buffer.close() }
+ modelBatches.forEach { it.buffer.close() }
+ screenModelBatches.forEach { it.buffer.close() }
+
+ // Store new batches
+ screenImageBatches = screenResult.images
+ worldImageBatches = result.images
+ modelBatches = result.models
+ screenModelBatches = screenResult.models
+
+ hasData = faceVertexBuffer != null || edgeVertexBuffer != null || textVertexBuffer != null || worldImageBatches.isNotEmpty() || modelBatches.isNotEmpty()
+ hasScreenData = screenFaceVertexBuffer != null || screenEdgeVertexBuffer != null || screenTextVertexBuffer != null || screenImageBatches.isNotEmpty() || screenModelBatches.isNotEmpty()
}
/**
@@ -104,38 +154,345 @@ class RegionRenderer(val region: RenderRegion) {
renderPass.drawIndexed(0, 0, edgeIndexCount, 1)
}
+ /**
+ * Render text using the given render pass.
+ * Note: Caller must bind the font texture and SDF params uniform before calling.
+ *
+ * @param renderPass The active RenderPass to record commands into
+ */
+ fun renderText(renderPass: RenderPass) {
+ val vb = textVertexBuffer ?: return
+ if (textIndexCount == 0) return
+
+ renderPass.setVertexBuffer(0, vb)
+ // Use vanilla's sequential index buffer for quads
+ val shapeIndexBuffer = RenderSystem.getSequentialBuffer(VertexFormat.DrawMode.QUADS)
+ val indexBuffer = shapeIndexBuffer.getIndexBuffer(textIndexCount)
+
+ renderPass.setIndexBuffer(indexBuffer, shapeIndexBuffer.indexType)
+ renderPass.drawIndexed(0, 0, textIndexCount, 1)
+ }
+
+ /** Check if this renderer has text data. */
+ fun hasTextData(): Boolean = textVertexBuffer != null && textIndexCount > 0
+
+ // ============================================================================
+ // Screen-Space Render Methods
+ // ============================================================================
+
+ /**
+ * Render screen-space faces using the given render pass.
+ *
+ * @param renderPass The active RenderPass to record commands into
+ */
+ fun renderScreenFaces(renderPass: RenderPass) {
+ val vb = screenFaceVertexBuffer ?: return
+ if (screenFaceIndexCount == 0) return
+
+ renderPass.setVertexBuffer(0, vb)
+ val shapeIndexBuffer = RenderSystem.getSequentialBuffer(VertexFormat.DrawMode.QUADS)
+ val indexBuffer = shapeIndexBuffer.getIndexBuffer(screenFaceIndexCount)
+
+ renderPass.setIndexBuffer(indexBuffer, shapeIndexBuffer.indexType)
+ renderPass.drawIndexed(0, 0, screenFaceIndexCount, 1)
+ }
+
+ /**
+ * Render screen-space edges using the given render pass.
+ *
+ * @param renderPass The active RenderPass to record commands into
+ */
+ fun renderScreenEdges(renderPass: RenderPass) {
+ val vb = screenEdgeVertexBuffer ?: return
+ if (screenEdgeIndexCount == 0) return
+
+ renderPass.setVertexBuffer(0, vb)
+ val shapeIndexBuffer = RenderSystem.getSequentialBuffer(VertexFormat.DrawMode.QUADS)
+ val indexBuffer = shapeIndexBuffer.getIndexBuffer(screenEdgeIndexCount)
+
+ renderPass.setIndexBuffer(indexBuffer, shapeIndexBuffer.indexType)
+ renderPass.drawIndexed(0, 0, screenEdgeIndexCount, 1)
+ }
+
+ /**
+ * Render screen-space text using the given render pass.
+ * Note: Caller must bind the font texture before calling.
+ *
+ * @param renderPass The active RenderPass to record commands into
+ */
+ fun renderScreenText(renderPass: RenderPass) {
+ val vb = screenTextVertexBuffer ?: return
+ if (screenTextIndexCount == 0) return
+
+ renderPass.setVertexBuffer(0, vb)
+ val shapeIndexBuffer = RenderSystem.getSequentialBuffer(VertexFormat.DrawMode.QUADS)
+ val indexBuffer = shapeIndexBuffer.getIndexBuffer(screenTextIndexCount)
+
+ renderPass.setIndexBuffer(indexBuffer, shapeIndexBuffer.indexType)
+ renderPass.drawIndexed(0, 0, screenTextIndexCount, 1)
+ }
+
+ /** Check if this renderer has screen-space text data. */
+ fun hasScreenTextData(): Boolean = screenTextVertexBuffer != null && screenTextIndexCount > 0
+
+ /**
+ * Render screen-space images using the given render pass.
+ * Each texture batch is rendered separately with its texture bound.
+ *
+ * @param renderPass The active RenderPass to record commands into
+ */
+ fun renderScreenImages(renderPass: RenderPass) {
+ if (screenImageBatches.isEmpty()) return
+
+ val shapeIndexBuffer = RenderSystem.getSequentialBuffer(VertexFormat.DrawMode.QUADS)
+ val linearSampler = RenderSystem.getSamplerCache().get(com.mojang.blaze3d.textures.FilterMode.LINEAR)
+ val nearestSampler = RenderSystem.getSamplerCache().get(com.mojang.blaze3d.textures.FilterMode.NEAREST)
+
+ for (batch in screenImageBatches) {
+ val sampler = if (batch.useNearestFilter) nearestSampler else linearSampler
+ renderPass.bindTexture("Sampler0", batch.textureView, sampler)
+
+ renderPass.setVertexBuffer(0, batch.buffer)
+ val indexBuffer = shapeIndexBuffer.getIndexBuffer(batch.indexCount)
+ renderPass.setIndexBuffer(indexBuffer, shapeIndexBuffer.indexType)
+
+ renderPass.drawIndexed(0, 0, batch.indexCount, 1)
+ }
+ }
+
+ /**
+ * Render screen-space models using the given render pass.
+ */
+ fun renderScreenModels(renderPass: RenderPass) {
+ if (screenModelBatches.isEmpty()) return
+
+ val shapeIndexBuffer = RenderSystem.getSequentialBuffer(VertexFormat.DrawMode.QUADS)
+ val linearSampler = RenderSystem.getSamplerCache().get(com.mojang.blaze3d.textures.FilterMode.LINEAR)
+ val nearestSampler = RenderSystem.getSamplerCache().get(com.mojang.blaze3d.textures.FilterMode.NEAREST)
+
+ // Get glint texture for enchantment shimmer
+ val glintTexture = mc.textureManager.getTexture(net.minecraft.client.render.item.ItemRenderer.ITEM_ENCHANTMENT_GLINT)?.glTextureView
+
+ for (batch in screenModelBatches) {
+ val sampler = if (batch.useNearestFilter) nearestSampler else linearSampler
+ renderPass.bindTexture("Sampler0", batch.textureView, sampler)
+
+ // Bind glint texture to Sampler3
+ if (glintTexture != null) {
+ renderPass.bindTexture("Sampler3", glintTexture, linearSampler)
+ }
+
+ renderPass.setVertexBuffer(0, batch.buffer)
+ val indexBuffer = shapeIndexBuffer.getIndexBuffer(batch.indexCount)
+ renderPass.setIndexBuffer(indexBuffer, shapeIndexBuffer.indexType)
+
+ renderPass.drawIndexed(0, 0, batch.indexCount, 1)
+ }
+ }
+
+ /** Check if this renderer has screen-space image data. */
+ fun hasScreenImageData(): Boolean = screenImageBatches.isNotEmpty()
+
+ /** Check if this renderer has screen-space model data. */
+ fun hasScreenModelData(): Boolean = screenModelBatches.isNotEmpty()
+
+ /**
+ * Render world-space images using the given render pass.
+ * Each texture batch is rendered separately with its texture bound.
+ *
+ * @param renderPass The active RenderPass to record commands into
+ */
+ fun renderWorldImages(renderPass: RenderPass) {
+ if (worldImageBatches.isEmpty()) return
+
+ val shapeIndexBuffer = RenderSystem.getSequentialBuffer(VertexFormat.DrawMode.QUADS)
+ val linearSampler = RenderSystem.getSamplerCache().get(com.mojang.blaze3d.textures.FilterMode.LINEAR)
+ val nearestSampler = RenderSystem.getSamplerCache().get(com.mojang.blaze3d.textures.FilterMode.NEAREST)
+
+ for (batch in worldImageBatches) {
+ val sampler = if (batch.useNearestFilter) nearestSampler else linearSampler
+ renderPass.bindTexture("Sampler0", batch.textureView, sampler)
+ renderPass.setVertexBuffer(0, batch.buffer)
+ val indexBuffer = shapeIndexBuffer.getIndexBuffer(batch.indexCount)
+ renderPass.setIndexBuffer(indexBuffer, shapeIndexBuffer.indexType)
+ renderPass.drawIndexed(0, 0, batch.indexCount, 1)
+ }
+ }
+
+ /** Check if this renderer has world-space image data. */
+ fun hasWorldImageData(): Boolean = worldImageBatches.isNotEmpty()
+
+ /**
+ * Render 3D models using the given render pass.
+ * Each texture batch is rendered separately with its texture bound.
+ *
+ * @param renderPass The active RenderPass to record commands into
+ */
+ fun renderModels(renderPass: RenderPass) {
+ if (modelBatches.isEmpty()) return
+
+ val shapeIndexBuffer = RenderSystem.getSequentialBuffer(VertexFormat.DrawMode.QUADS)
+ val linearSampler = RenderSystem.getSamplerCache().get(com.mojang.blaze3d.textures.FilterMode.LINEAR)
+ val nearestSampler = RenderSystem.getSamplerCache().get(com.mojang.blaze3d.textures.FilterMode.NEAREST)
+
+ // Get glint texture for enchantment shimmer
+ val glintTexture = mc.textureManager.getTexture(net.minecraft.client.render.item.ItemRenderer.ITEM_ENCHANTMENT_GLINT)?.glTextureView
+
+ for (batch in modelBatches) {
+ val sampler = if (batch.useNearestFilter) nearestSampler else linearSampler
+ renderPass.bindTexture("Sampler0", batch.textureView, sampler)
+
+ // Bind glint texture to Sampler3
+ if (glintTexture != null) {
+ renderPass.bindTexture("Sampler3", glintTexture, linearSampler)
+ }
+
+ renderPass.setVertexBuffer(0, batch.buffer)
+ val indexBuffer = shapeIndexBuffer.getIndexBuffer(batch.indexCount)
+ renderPass.setIndexBuffer(indexBuffer, shapeIndexBuffer.indexType)
+ renderPass.drawIndexed(0, 0, batch.indexCount, 1)
+ }
+ }
+
+ /** Check if this renderer has model data. */
+ fun hasModelData(): Boolean = modelBatches.isNotEmpty()
+
+ /** Check if this renderer has any screen-space data to render. */
+ fun hasScreenData(): Boolean = hasScreenData
+
/** Clear all geometry data and release GPU resources. */
fun clearData() {
+ // Clear world-space buffers
faceVertexBuffer?.close()
edgeVertexBuffer?.close()
+ textVertexBuffer?.close()
faceVertexBuffer = null
edgeVertexBuffer = null
+ textVertexBuffer = null
faceIndexCount = 0
edgeIndexCount = 0
+ textIndexCount = 0
hasData = false
+
+ // Clear screen-space buffers
+ screenFaceVertexBuffer?.close()
+ screenEdgeVertexBuffer?.close()
+ screenTextVertexBuffer?.close()
+ screenFaceVertexBuffer = null
+ screenEdgeVertexBuffer = null
+ screenTextVertexBuffer = null
+ screenFaceIndexCount = 0
+ screenEdgeIndexCount = 0
+ screenTextIndexCount = 0
+
+ // Clear image batches
+ screenImageBatches.forEach { it.buffer.close() }
+ worldImageBatches.forEach { it.buffer.close() }
+ modelBatches.forEach { it.buffer.close() }
+ screenImageBatches = emptyList()
+ worldImageBatches = emptyList()
+ modelBatches = emptyList()
+
+ hasScreenData = false
}
/** Check if this renderer has any data to render. */
fun hasData(): Boolean = hasData
- /** Clean up all resources. */
- fun close() {
- clearData()
- }
-
companion object {
- /** Helper to create a render pass targeting the main framebuffer. */
+ /** Helper to create a render pass targeting the main framebuffer with MC's depth. */
fun createRenderPass(label: String): RenderPass? {
+ return createRenderPass(label, useMcDepth = true)
+ }
+
+ /**
+ * Helper to create a render pass for world-space rendering.
+ * @param label Debug label for the render pass
+ * @param useMcDepth If true, use MC's depth buffer (normal depth testing against world).
+ * If false, use Lambda's custom xray depth buffer (self-ordering, ignores MC world).
+ */
+ fun createRenderPass(label: String, useMcDepth: Boolean): RenderPass? {
val framebuffer = mc.framebuffer ?: return null
+
+ // Choose depth buffer:
+ // - true = MC's depth (normal depth testing against world)
+ // - false = Lambda's xray depth (self-ordering, ignores MC world)
+ val depthView = if (useMcDepth) {
+ framebuffer.depthAttachmentView
+ } else {
+ RendererUtils.getXrayDepthView()
+ }
+
return RenderSystem.getDevice()
.createCommandEncoder()
.createRenderPass(
{ label },
framebuffer.colorAttachmentView,
OptionalInt.empty(),
- framebuffer.depthAttachmentView,
+ depthView,
OptionalDouble.empty()
)
}
+
+ /**
+ * Helper to create a render pass for screen-space rendering (no depth buffer).
+ * Uses painter's algorithm: last drawn is on top.
+ * @param label Debug label for the render pass
+ */
+ fun createScreenRenderPass(label: String): RenderPass? {
+ val framebuffer = mc.framebuffer ?: return null
+
+ return RenderSystem.getDevice()
+ .createCommandEncoder()
+ .createRenderPass(
+ { label },
+ framebuffer.colorAttachmentView,
+ OptionalInt.empty(),
+ null, // No depth buffer - painter's algorithm
+ OptionalDouble.empty()
+ )
+ }
+
+ /**
+ * Helper to create a render pass for screen-space rendering WITH depth testing.
+ * Enables unified layering across all screen element types (models, faces, text, etc.).
+ * Uses a SEPARATE screen depth buffer to ensure complete isolation from world-space.
+ *
+ * @param label Debug label for the render pass
+ * @param clearDepth If true, clear the depth buffer to 1.0 (call this on the first screen pass)
+ */
+ fun createScreenRenderPassWithDepth(label: String, clearDepth: Boolean = false): RenderPass? {
+ val framebuffer = mc.framebuffer ?: return null
+ val depthView = RendererUtils.getScreenDepthView() // Use separate screen depth buffer
+
+ return RenderSystem.getDevice()
+ .createCommandEncoder()
+ .createRenderPass(
+ { label },
+ framebuffer.colorAttachmentView,
+ OptionalInt.empty(),
+ depthView,
+ if (clearDepth) OptionalDouble.of(1.0) else OptionalDouble.empty()
+ )
+ }
+
+ /**
+ * Render a custom vertex buffer using quads mode.
+ * Used for styled text rendering where each style has its own buffer.
+ *
+ * @param renderPass The active RenderPass to record commands into
+ * @param buffer The vertex buffer to render
+ * @param indexCount The number of indices (vertices) to render
+ */
+ fun renderQuadBuffer(renderPass: RenderPass, buffer: GpuBuffer, indexCount: Int) {
+ if (indexCount == 0) return
+
+ renderPass.setVertexBuffer(0, buffer)
+ val shapeIndexBuffer = RenderSystem.getSequentialBuffer(VertexFormat.DrawMode.QUADS)
+ val indexBuffer = shapeIndexBuffer.getIndexBuffer(indexCount)
+
+ renderPass.setIndexBuffer(indexBuffer, shapeIndexBuffer.indexType)
+ renderPass.drawIndexed(0, 0, indexCount, 1)
+ }
}
}
diff --git a/src/main/kotlin/com/lambda/graphics/mc/RegionShapeBuilder.kt b/src/main/kotlin/com/lambda/graphics/mc/RegionShapeBuilder.kt
deleted file mode 100644
index 8b33498be..000000000
--- a/src/main/kotlin/com/lambda/graphics/mc/RegionShapeBuilder.kt
+++ /dev/null
@@ -1,758 +0,0 @@
-/*
- * Copyright 2025 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.graphics.mc
-
-import com.lambda.Lambda.mc
-import com.lambda.graphics.renderer.esp.DirectionMask
-import com.lambda.graphics.renderer.esp.DirectionMask.hasDirection
-import com.lambda.graphics.renderer.esp.DynamicAABB
-import com.lambda.module.modules.client.StyleEditor
-import com.lambda.threading.runSafe
-import com.lambda.util.BlockUtils.blockState
-import com.lambda.util.extension.partialTicks
-import net.minecraft.block.BlockState
-import net.minecraft.block.entity.BlockEntity
-import net.minecraft.entity.Entity
-import net.minecraft.util.math.BlockPos
-import net.minecraft.util.math.Box
-import net.minecraft.util.math.MathHelper.lerp
-import net.minecraft.util.math.Vec3d
-import net.minecraft.util.shape.VoxelShape
-import java.awt.Color
-import kotlin.math.min
-import kotlin.math.sqrt
-
-/**
- * Shape builder for region-based rendering. All coordinates are automatically converted to
- * region-relative positions.
- *
- * This class provides drawing primitives for region-based rendering and collects vertex data in thread-safe collections
- * for later upload to MC's BufferBuilder.
- *
- * @param region The render region (provides origin for coordinate conversion)
- */
-class RegionShapeBuilder(val region: RenderRegion) {
- val collector = RegionVertexCollector()
-
- val lineWidth: Float
- get() = StyleEditor.outlineWidth.toFloat()
-
- fun box(
- entity: BlockEntity,
- filled: Color,
- outline: Color,
- sides: Int = DirectionMask.ALL,
- mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And
- ) = box(entity.pos, entity.cachedState, filled, outline, sides, mode)
-
- fun box(
- entity: Entity,
- filled: Color,
- outline: Color,
- sides: Int = DirectionMask.ALL,
- mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And
- ) = box(entity.boundingBox, filled, outline, sides, mode)
-
- /** Convert world coordinates to region-relative. */
- private fun toRelative(x: Double, y: Double, z: Double) =
- Triple(
- (x - region.originX).toFloat(),
- (y - region.originY).toFloat(),
- (z - region.originZ).toFloat()
- )
-
- /** Add a colored quad face (filled rectangle). */
- fun filled(
- box: Box,
- bottomColor: Color,
- topColor: Color = bottomColor,
- sides: Int = DirectionMask.ALL
- ) {
- val (x1, y1, z1) = toRelative(box.minX, box.minY, box.minZ)
- val (x2, y2, z2) = toRelative(box.maxX, box.maxY, box.maxZ)
-
- // Bottom-left-back, bottom-left-front, etc.
- if (sides.hasDirection(DirectionMask.EAST)) {
- // East face (+X)
- faceVertex(x2, y1, z1, bottomColor)
- faceVertex(x2, y2, z1, topColor)
- faceVertex(x2, y2, z2, topColor)
- faceVertex(x2, y1, z2, bottomColor)
- }
- if (sides.hasDirection(DirectionMask.WEST)) {
- // West face (-X)
- faceVertex(x1, y1, z1, bottomColor)
- faceVertex(x1, y1, z2, bottomColor)
- faceVertex(x1, y2, z2, topColor)
- faceVertex(x1, y2, z1, topColor)
- }
- if (sides.hasDirection(DirectionMask.UP)) {
- // Top face (+Y)
- faceVertex(x1, y2, z1, topColor)
- faceVertex(x1, y2, z2, topColor)
- faceVertex(x2, y2, z2, topColor)
- faceVertex(x2, y2, z1, topColor)
- }
- if (sides.hasDirection(DirectionMask.DOWN)) {
- // Bottom face (-Y)
- faceVertex(x1, y1, z1, bottomColor)
- faceVertex(x2, y1, z1, bottomColor)
- faceVertex(x2, y1, z2, bottomColor)
- faceVertex(x1, y1, z2, bottomColor)
- }
- if (sides.hasDirection(DirectionMask.SOUTH)) {
- // South face (+Z)
- faceVertex(x1, y1, z2, bottomColor)
- faceVertex(x2, y1, z2, bottomColor)
- faceVertex(x2, y2, z2, topColor)
- faceVertex(x1, y2, z2, topColor)
- }
- if (sides.hasDirection(DirectionMask.NORTH)) {
- // North face (-Z)
- faceVertex(x1, y1, z1, bottomColor)
- faceVertex(x1, y2, z1, topColor)
- faceVertex(x2, y2, z1, topColor)
- faceVertex(x2, y1, z1, bottomColor)
- }
- }
-
- fun filled(box: Box, color: Color, sides: Int = DirectionMask.ALL) =
- filled(box, color, color, sides)
-
- fun filled(box: DynamicAABB, color: Color, sides: Int = DirectionMask.ALL) {
- val pair = box.pair ?: return
- val prev = pair.first
- val curr = pair.second
- val tickDelta = mc.partialTicks
- val interpolated = Box(
- lerp(tickDelta, prev.minX, curr.minX),
- lerp(tickDelta, prev.minY, curr.minY),
- lerp(tickDelta, prev.minZ, curr.minZ),
- lerp(tickDelta, prev.maxX, curr.maxX),
- lerp(tickDelta, prev.maxY, curr.maxY),
- lerp(tickDelta, prev.maxZ, curr.maxZ)
- )
- filled(interpolated, color, sides)
- }
-
- fun filled(
- pos: BlockPos,
- state: BlockState,
- color: Color,
- sides: Int = DirectionMask.ALL
- ) = runSafe {
- val shape = state.getOutlineShape(world, pos)
- if (shape.isEmpty) {
- filled(Box(pos), color, sides)
- } else {
- filled(shape.offset(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()), color, sides)
- }
- }
-
- fun filled(pos: BlockPos, color: Color, sides: Int = DirectionMask.ALL) = runSafe {
- filled(pos, blockState(pos), color, sides)
- }
-
- fun filled(pos: BlockPos, entity: BlockEntity, color: Color, sides: Int = DirectionMask.ALL) =
- filled(pos, entity.cachedState, color, sides)
-
- fun filled(shape: VoxelShape, color: Color, sides: Int = DirectionMask.ALL) {
- shape.boundingBoxes.forEach { filled(it, color, color, sides) }
- }
-
- /** Add outline (lines) for a box. */
- fun outline(
- box: Box,
- bottomColor: Color,
- topColor: Color = bottomColor,
- sides: Int = DirectionMask.ALL,
- mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And,
- thickness: Float = lineWidth
- ) {
- val (x1, y1, z1) = toRelative(box.minX, box.minY, box.minZ)
- val (x2, y2, z2) = toRelative(box.maxX, box.maxY, box.maxZ)
-
- val hasEast = sides.hasDirection(DirectionMask.EAST)
- val hasWest = sides.hasDirection(DirectionMask.WEST)
- val hasUp = sides.hasDirection(DirectionMask.UP)
- val hasDown = sides.hasDirection(DirectionMask.DOWN)
- val hasSouth = sides.hasDirection(DirectionMask.SOUTH)
- val hasNorth = sides.hasDirection(DirectionMask.NORTH)
-
- // Top edges
- if (mode.check(hasUp, hasNorth)) line(x1, y2, z1, x2, y2, z1, topColor, topColor, thickness)
- if (mode.check(hasUp, hasSouth)) line(x1, y2, z2, x2, y2, z2, topColor, topColor, thickness)
- if (mode.check(hasUp, hasWest)) line(x1, y2, z1, x1, y2, z2, topColor, topColor, thickness)
- if (mode.check(hasUp, hasEast)) line(x2, y2, z2, x2, y2, z1, topColor, topColor, thickness)
-
- // Bottom edges
- if (mode.check(hasDown, hasNorth)) line(x1, y1, z1, x2, y1, z1, bottomColor, bottomColor, thickness)
- if (mode.check(hasDown, hasSouth)) line(x1, y1, z2, x2, y1, z2, bottomColor, bottomColor, thickness)
- if (mode.check(hasDown, hasWest)) line(x1, y1, z1, x1, y1, z2, bottomColor, bottomColor, thickness)
- if (mode.check(hasDown, hasEast)) line(x2, y1, z1, x2, y1, z2, bottomColor, bottomColor, thickness)
-
- // Vertical edges
- if (mode.check(hasWest, hasNorth)) line(x1, y2, z1, x1, y1, z1, topColor, bottomColor, thickness)
- if (mode.check(hasNorth, hasEast)) line(x2, y2, z1, x2, y1, z1, topColor, bottomColor, thickness)
- if (mode.check(hasEast, hasSouth)) line(x2, y2, z2, x2, y1, z2, topColor, bottomColor, thickness)
- if (mode.check(hasSouth, hasWest)) line(x1, y2, z2, x1, y1, z2, topColor, bottomColor, thickness)
- }
-
- fun outline(
- box: Box,
- color: Color,
- sides: Int = DirectionMask.ALL,
- mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And,
- thickness: Float = lineWidth
- ) = outline(box, color, color, sides, mode, thickness)
-
- fun outline(
- box: DynamicAABB,
- color: Color,
- sides: Int = DirectionMask.ALL,
- mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And,
- thickness: Float = lineWidth
- ) {
- val pair = box.pair ?: return
- val prev = pair.first
- val curr = pair.second
- val tickDelta = mc.partialTicks
- val interpolated = Box(
- lerp(tickDelta, prev.minX, curr.minX),
- lerp(tickDelta, prev.minY, curr.minY),
- lerp(tickDelta, prev.minZ, curr.minZ),
- lerp(tickDelta, prev.maxX, curr.maxX),
- lerp(tickDelta, prev.maxY, curr.maxY),
- lerp(tickDelta, prev.maxZ, curr.maxZ)
- )
- outline(interpolated, color, sides, mode, thickness)
- }
-
- fun outline(
- pos: BlockPos,
- state: BlockState,
- color: Color,
- sides: Int = DirectionMask.ALL,
- mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And,
- thickness: Float = lineWidth
- ) = runSafe {
- val shape = state.getOutlineShape(world, pos)
- if (shape.isEmpty) {
- outline(Box(pos), color, sides, mode, thickness)
- } else {
- outline(shape.offset(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()), color, sides, mode, thickness)
- }
- }
-
- fun outline(
- pos: BlockPos,
- color: Color,
- sides: Int = DirectionMask.ALL,
- mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And,
- thickness: Float = lineWidth
- ) = runSafe { outline(pos, blockState(pos), color, sides, mode, thickness) }
-
- fun outline(
- pos: BlockPos,
- entity: BlockEntity,
- color: Color,
- sides: Int = DirectionMask.ALL,
- mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And,
- thickness: Float = lineWidth
- ) = runSafe { outline(pos, entity.cachedState, color, sides, mode, thickness) }
-
- fun outline(
- shape: VoxelShape,
- color: Color,
- sides: Int = DirectionMask.ALL,
- mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And,
- thickness: Float = lineWidth
- ) {
- shape.boundingBoxes.forEach { outline(it, color, sides, mode, thickness) }
- }
-
- /** Add both filled and outline for a box. */
- fun box(
- pos: BlockPos,
- state: BlockState,
- filledColor: Color,
- outlineColor: Color,
- sides: Int = DirectionMask.ALL,
- mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And,
- thickness: Float = lineWidth
- ) = runSafe {
- filled(pos, state, filledColor, sides)
- outline(pos, state, outlineColor, sides, mode, thickness)
- }
-
- fun box(
- pos: BlockPos,
- filledColor: Color,
- outlineColor: Color,
- sides: Int = DirectionMask.ALL,
- mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And,
- thickness: Float = lineWidth
- ) = runSafe {
- filled(pos, filledColor, sides)
- outline(pos, outlineColor, sides, mode, thickness)
- }
-
- fun box(
- box: Box,
- filledColor: Color,
- outlineColor: Color,
- sides: Int = DirectionMask.ALL,
- mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And,
- thickness: Float = lineWidth
- ) {
- filled(box, filledColor, sides)
- outline(box, outlineColor, sides, mode, thickness)
- }
-
- fun box(
- box: DynamicAABB,
- filledColor: Color,
- outlineColor: Color,
- sides: Int = DirectionMask.ALL,
- mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And,
- thickness: Float = lineWidth
- ) {
- filled(box, filledColor, sides)
- outline(box, outlineColor, sides, mode, thickness)
- }
-
- fun box(
- entity: BlockEntity,
- filled: Color,
- outlineColor: Color,
- sides: Int = DirectionMask.ALL,
- mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And,
- thickness: Float = lineWidth
- ) = runSafe {
- filled(entity.pos, entity, filled, sides)
- outline(entity.pos, entity, outlineColor, sides, mode, thickness)
- }
-
- fun box(
- entity: Entity,
- filled: Color,
- outlineColor: Color,
- sides: Int = DirectionMask.ALL,
- mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And,
- thickness: Float = lineWidth
- ) = runSafe {
- filled(entity.boundingBox, filled, sides)
- outline(entity.boundingBox, outlineColor, sides, mode, thickness)
- }
-
- private fun faceVertex(x: Float, y: Float, z: Float, color: Color) {
- collector.addFaceVertex(x, y, z, color)
- }
-
- private fun line(
- x1: Float,
- y1: Float,
- z1: Float,
- x2: Float,
- y2: Float,
- z2: Float,
- color: Color,
- width: Float = lineWidth
- ) {
- line(x1, y1, z1, x2, y2, z2, color, color, width)
- }
-
- private fun line(
- x1: Float,
- y1: Float,
- z1: Float,
- x2: Float,
- y2: Float,
- z2: Float,
- color1: Color,
- color2: Color,
- width: Float = lineWidth
- ) {
- // Calculate segment vector (dx, dy, dz)
- val dx = x2 - x1
- val dy = y2 - y1
- val dz = z2 - z1
-
- // Quad-based lines need 4 vertices per segment
- // We pass the full vector as 'Normal' so the shader knows where the other end is
- collector.addEdgeVertex(x1, y1, z1, color1, dx, dy, dz, width)
- collector.addEdgeVertex(x1, y1, z1, color1, dx, dy, dz, width)
- collector.addEdgeVertex(x2, y2, z2, color2, dx, dy, dz, width)
- collector.addEdgeVertex(x2, y2, z2, color2, dx, dy, dz, width)
- }
-
- /**
- * Draw a dashed line between two world positions.
- *
- * @param start Start position in world coordinates
- * @param end End position in world coordinates
- * @param color Line color
- * @param dashLength Length of each dash in blocks
- * @param gapLength Length of each gap in blocks
- * @param width Line width (uses default if null)
- */
- fun dashedLine(
- start: Vec3d,
- end: Vec3d,
- color: Color,
- dashLength: Double = 0.5,
- gapLength: Double = 0.25,
- width: Float = lineWidth
- ) {
- val direction = end.subtract(start)
- val totalLength = direction.length()
- if (totalLength < 0.001) return
-
- val normalizedDir = direction.normalize()
- var pos = 0.0
- var isDash = true
-
- while (pos < totalLength) {
- val segmentLength = if (isDash) dashLength else gapLength
- val segmentEnd = min(pos + segmentLength, totalLength)
-
- if (isDash) {
- val segStart = start.add(normalizedDir.multiply(pos))
- val segEnd = start.add(normalizedDir.multiply(segmentEnd))
-
- val (x1, y1, z1) = toRelative(segStart.x, segStart.y, segStart.z)
- val (x2, y2, z2) = toRelative(segEnd.x, segEnd.y, segEnd.z)
-
- lineWithWidth(x1, y1, z1, x2, y2, z2, color, width)
- }
-
- pos = segmentEnd
- isDash = !isDash
- }
- }
-
- /** Draw a dashed outline for a box. */
- fun dashedOutline(
- box: Box,
- color: Color,
- dashLength: Double = 0.5,
- gapLength: Double = 0.25,
- sides: Int = DirectionMask.ALL,
- mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And
- ) {
- val hasEast = sides.hasDirection(DirectionMask.EAST)
- val hasWest = sides.hasDirection(DirectionMask.WEST)
- val hasUp = sides.hasDirection(DirectionMask.UP)
- val hasDown = sides.hasDirection(DirectionMask.DOWN)
- val hasSouth = sides.hasDirection(DirectionMask.SOUTH)
- val hasNorth = sides.hasDirection(DirectionMask.NORTH)
-
- // Top edges
- if (mode.check(hasUp, hasNorth))
- dashedLine(
- Vec3d(box.minX, box.maxY, box.minZ),
- Vec3d(box.maxX, box.maxY, box.minZ),
- color,
- dashLength,
- gapLength
- )
- if (mode.check(hasUp, hasSouth))
- dashedLine(
- Vec3d(box.minX, box.maxY, box.maxZ),
- Vec3d(box.maxX, box.maxY, box.maxZ),
- color,
- dashLength,
- gapLength
- )
- if (mode.check(hasUp, hasWest))
- dashedLine(
- Vec3d(box.minX, box.maxY, box.minZ),
- Vec3d(box.minX, box.maxY, box.maxZ),
- color,
- dashLength,
- gapLength
- )
- if (mode.check(hasUp, hasEast))
- dashedLine(
- Vec3d(box.maxX, box.maxY, box.maxZ),
- Vec3d(box.maxX, box.maxY, box.minZ),
- color,
- dashLength,
- gapLength
- )
-
- // Bottom edges
- if (mode.check(hasDown, hasNorth))
- dashedLine(
- Vec3d(box.minX, box.minY, box.minZ),
- Vec3d(box.maxX, box.minY, box.minZ),
- color,
- dashLength,
- gapLength
- )
- if (mode.check(hasDown, hasSouth))
- dashedLine(
- Vec3d(box.minX, box.minY, box.maxZ),
- Vec3d(box.maxX, box.minY, box.maxZ),
- color,
- dashLength,
- gapLength
- )
- if (mode.check(hasDown, hasWest))
- dashedLine(
- Vec3d(box.minX, box.minY, box.minZ),
- Vec3d(box.minX, box.minY, box.maxZ),
- color,
- dashLength,
- gapLength
- )
- if (mode.check(hasDown, hasEast))
- dashedLine(
- Vec3d(box.maxX, box.minY, box.minZ),
- Vec3d(box.maxX, box.minY, box.maxZ),
- color,
- dashLength,
- gapLength
- )
-
- // Vertical edges
- if (mode.check(hasWest, hasNorth))
- dashedLine(
- Vec3d(box.minX, box.maxY, box.minZ),
- Vec3d(box.minX, box.minY, box.minZ),
- color,
- dashLength,
- gapLength
- )
- if (mode.check(hasNorth, hasEast))
- dashedLine(
- Vec3d(box.maxX, box.maxY, box.minZ),
- Vec3d(box.maxX, box.minY, box.minZ),
- color,
- dashLength,
- gapLength
- )
- if (mode.check(hasEast, hasSouth))
- dashedLine(
- Vec3d(box.maxX, box.maxY, box.maxZ),
- Vec3d(box.maxX, box.minY, box.maxZ),
- color,
- dashLength,
- gapLength
- )
- if (mode.check(hasSouth, hasWest))
- dashedLine(
- Vec3d(box.minX, box.maxY, box.maxZ),
- Vec3d(box.minX, box.minY, box.maxZ),
- color,
- dashLength,
- gapLength
- )
- }
-
- /** Draw a line between two world positions. */
- fun line(start: Vec3d, end: Vec3d, color: Color, width: Float = lineWidth) {
- val (x1, y1, z1) = toRelative(start.x, start.y, start.z)
- val (x2, y2, z2) = toRelative(end.x, end.y, end.z)
- lineWithWidth(x1, y1, z1, x2, y2, z2, color, width)
- }
-
- /** Draw a polyline through a list of points. */
- fun polyline(points: List, color: Color, width: Float = lineWidth) {
- if (points.size < 2) return
- for (i in 0 until points.size - 1) {
- line(points[i], points[i + 1], color, width)
- }
- }
-
- /** Draw a dashed polyline through a list of points. */
- fun dashedPolyline(
- points: List,
- color: Color,
- dashLength: Double = 0.5,
- gapLength: Double = 0.25,
- width: Float = lineWidth
- ) {
- if (points.size < 2) return
- for (i in 0 until points.size - 1) {
- dashedLine(points[i], points[i + 1], color, dashLength, gapLength, width)
- }
- }
-
- /**
- * Draw a quadratic Bezier curve.
- *
- * @param p0 Start point
- * @param p1 Control point
- * @param p2 End point
- * @param color Line color
- * @param segments Number of line segments (higher = smoother)
- */
- fun quadraticBezier(
- p0: Vec3d,
- p1: Vec3d,
- p2: Vec3d,
- color: Color,
- segments: Int = 16,
- width: Float = lineWidth
- ) {
- val points = CurveUtils.quadraticBezierPoints(p0, p1, p2, segments)
- polyline(points, color, width)
- }
-
- /**
- * Draw a cubic Bezier curve.
- *
- * @param p0 Start point
- * @param p1 First control point
- * @param p2 Second control point
- * @param p3 End point
- * @param color Line color
- * @param segments Number of line segments (higher = smoother)
- */
- fun cubicBezier(
- p0: Vec3d,
- p1: Vec3d,
- p2: Vec3d,
- p3: Vec3d,
- color: Color,
- segments: Int = 32,
- width: Float = lineWidth
- ) {
- val points = CurveUtils.cubicBezierPoints(p0, p1, p2, p3, segments)
- polyline(points, color, width)
- }
-
- /**
- * Draw a Catmull-Rom spline that passes through all control points.
- *
- * @param controlPoints List of points the spline should pass through (minimum 4)
- * @param color Line color
- * @param segmentsPerSection Segments between each pair of control points
- */
- fun catmullRomSpline(
- controlPoints: List,
- color: Color,
- segmentsPerSection: Int = 16,
- width: Float = lineWidth
- ) {
- val points = CurveUtils.catmullRomSplinePoints(controlPoints, segmentsPerSection)
- polyline(points, color, width)
- }
-
- /**
- * Draw a smooth path through waypoints using Catmull-Rom splines. Handles endpoints
- * naturally by mirroring.
- *
- * @param waypoints List of points to pass through (minimum 2)
- * @param color Line color
- * @param segmentsPerSection Smoothness (higher = smoother)
- */
- fun smoothPath(
- waypoints: List,
- color: Color,
- segmentsPerSection: Int = 16,
- width: Float = lineWidth
- ) {
- val points = CurveUtils.smoothPath(waypoints, segmentsPerSection)
- polyline(points, color, width)
- }
-
- /** Draw a dashed Bezier curve. */
- fun dashedCubicBezier(
- p0: Vec3d,
- p1: Vec3d,
- p2: Vec3d,
- p3: Vec3d,
- color: Color,
- segments: Int = 32,
- dashLength: Double = 0.5,
- gapLength: Double = 0.25,
- width: Float = lineWidth
- ) {
- val points = CurveUtils.cubicBezierPoints(p0, p1, p2, p3, segments)
- dashedPolyline(points, color, dashLength, gapLength, width)
- }
-
- /** Draw a dashed smooth path. */
- fun dashedSmoothPath(
- waypoints: List,
- color: Color,
- segmentsPerSection: Int = 16,
- dashLength: Double = 0.5,
- gapLength: Double = 0.25,
- width: Float = lineWidth
- ) {
- val points = CurveUtils.smoothPath(waypoints, segmentsPerSection)
- dashedPolyline(points, color, dashLength, gapLength, width)
- }
-
- /**
- * Draw a circle in a plane.
- *
- * @param center Center of the circle
- * @param radius Radius of the circle
- * @param normal Normal vector of the plane (determines orientation)
- * @param color Line color
- * @param segments Number of segments
- */
- fun circle(
- center: Vec3d,
- radius: Double,
- normal: Vec3d = Vec3d(0.0, 1.0, 0.0),
- color: Color,
- segments: Int = 32,
- width: Float = lineWidth
- ) {
- // Create basis vectors perpendicular to normal
- val up =
- if (kotlin.math.abs(normal.y) < 0.99) Vec3d(0.0, 1.0, 0.0)
- else Vec3d(1.0, 0.0, 0.0)
- val u = normal.crossProduct(up).normalize()
- val v = u.crossProduct(normal).normalize()
-
- val points =
- (0..segments).map { i ->
- val angle = 2.0 * Math.PI * i / segments
- val x = kotlin.math.cos(angle) * radius
- val y = kotlin.math.sin(angle) * radius
- center.add(u.multiply(x)).add(v.multiply(y))
- }
-
- polyline(points, color, width)
- }
-
- private fun lineWithWidth(
- x1: Float,
- y1: Float,
- z1: Float,
- x2: Float,
- y2: Float,
- z2: Float,
- color: Color,
- width: Float
- ) {
- val dx = x2 - x1
- val dy = y2 - y1
- val dz = z2 - z1
- collector.addEdgeVertex(x1, y1, z1, color, dx, dy, dz, width)
- collector.addEdgeVertex(x1, y1, z1, color, dx, dy, dz, width)
- collector.addEdgeVertex(x2, y2, z2, color, dx, dy, dz, width)
- collector.addEdgeVertex(x2, y2, z2, color, dx, dy, dz, width)
- }
-}
diff --git a/src/main/kotlin/com/lambda/graphics/mc/RegionVertexCollector.kt b/src/main/kotlin/com/lambda/graphics/mc/RegionVertexCollector.kt
index c82347817..249622998 100644
--- a/src/main/kotlin/com/lambda/graphics/mc/RegionVertexCollector.kt
+++ b/src/main/kotlin/com/lambda/graphics/mc/RegionVertexCollector.kt
@@ -19,10 +19,13 @@ package com.lambda.graphics.mc
import com.mojang.blaze3d.buffers.GpuBuffer
import com.mojang.blaze3d.systems.RenderSystem
+import com.mojang.blaze3d.textures.GpuTextureView
import com.mojang.blaze3d.vertex.VertexFormat
+import com.mojang.blaze3d.vertex.VertexFormatElement
import net.minecraft.client.render.BufferBuilder
import net.minecraft.client.render.VertexFormats
import net.minecraft.client.util.BufferAllocator
+import org.lwjgl.system.MemoryUtil
import java.awt.Color
import java.util.concurrent.ConcurrentLinkedDeque
@@ -35,19 +38,56 @@ import java.util.concurrent.ConcurrentLinkedDeque
class RegionVertexCollector {
val faceVertices = ConcurrentLinkedDeque()
val edgeVertices = ConcurrentLinkedDeque()
+ val textVertices = ConcurrentLinkedDeque()
+
+ // Screen-space vertex collections
+ val screenFaceVertices = ConcurrentLinkedDeque()
+ val screenEdgeVertices = ConcurrentLinkedDeque()
+ val screenTextVertices = ConcurrentLinkedDeque()
/** Face vertex data (position + color). */
data class FaceVertex(
- val x: Float,
- val y: Float,
- val z: Float,
- val r: Int,
- val g: Int,
- val b: Int,
- val a: Int
+ val x: Float, val y: Float, val z: Float,
+ val r: Int, val g: Int, val b: Int, val a: Int
)
- /** Edge vertex data (position + color + normal + line width). */
+ /**
+ * Text vertex data for SDF billboard text rendering.
+ * Uses POSITION_TEXTURE_COLOR_ANCHOR_SDF format for GPU-based billboarding with embedded style.
+ *
+ * @param localX Local glyph offset X (before billboard transform)
+ * @param localY Local glyph offset Y (before billboard transform)
+ * @param u Texture U coordinate
+ * @param v Texture V coordinate
+ * @param r Red color component
+ * @param g Green color component
+ * @param b Blue color component
+ * @param a Alpha component (encodes layer type)
+ * @param anchorX Camera-relative anchor position X
+ * @param anchorY Camera-relative anchor position Y
+ * @param anchorZ Camera-relative anchor position Z
+ * @param scale Text scale
+ * @param billboardFlag 0 = billboard towards camera, non-zero = fixed rotation already applied
+ * @param outlineWidth SDF outline width (0 = no outline)
+ * @param glowRadius SDF glow radius (0 = no glow)
+ * @param shadowSoftness SDF shadow softness (0 = no shadow)
+ * @param threshold SDF edge threshold (default 0.5)
+ */
+ data class TextVertex(
+ val localX: Float, val localY: Float,
+ val u: Float, val v: Float,
+ val r: Int, val g: Int, val b: Int, val a: Int,
+ val anchorX: Float, val anchorY: Float, val anchorZ: Float,
+ val scale: Float,
+ val billboardFlag: Float,
+ // SDF style params (replaces SDFParams uniform)
+ val outlineWidth: Float = 0f,
+ val glowRadius: Float = 0f,
+ val shadowSoftness: Float = 0f,
+ val threshold: Float = 0.5f
+ )
+
+ /** Edge vertex data (position + color + normal + line width + dash style). */
data class EdgeVertex(
val x: Float,
val y: Float,
@@ -59,15 +99,239 @@ class RegionVertexCollector {
val nx: Float,
val ny: Float,
val nz: Float,
- val lineWidth: Float
+ val lineWidth: Float,
+ // Dash style parameters (0 = solid line)
+ val dashLength: Float = 0f,
+ val gapLength: Float = 0f,
+ val dashOffset: Float = 0f,
+ val animationSpeed: Float = 0f // 0 = no animation
+ )
+
+ // ============================================================================
+ // Screen-Space Vertex Types
+ // ============================================================================
+
+ /** Screen-space face vertex data (2D position + color + layer). */
+ data class ScreenFaceVertex(
+ val x: Float, val y: Float,
+ val r: Int, val g: Int, val b: Int, val a: Int,
+ val layer: Float // Depth for layering (higher = on top)
+ )
+
+ /** Screen-space edge vertex data (2D position + color + direction + width + dash + layer). */
+ data class ScreenEdgeVertex(
+ val x: Float, val y: Float,
+ val r: Int, val g: Int, val b: Int, val a: Int,
+ val dx: Float, val dy: Float,
+ val lineWidth: Float,
+ // Dash style parameters (0 = solid line)
+ val dashLength: Float = 0f,
+ val gapLength: Float = 0f,
+ val dashOffset: Float = 0f,
+ val animationSpeed: Float = 0f,
+ val layer: Float = 0f // Depth for layering (higher = on top)
+ )
+
+ /**
+ * Screen-space text vertex data with SDF style params and layer.
+ * Uses SCREEN_TEXT_SDF_FORMAT (position + UV + color + style + layer).
+ *
+ * @param x Screen-space X position
+ * @param y Screen-space Y position
+ * @param u Texture U coordinate
+ * @param v Texture V coordinate
+ * @param r Red color component
+ * @param g Green color component
+ * @param b Blue color component
+ * @param a Alpha component (encodes layer type)
+ * @param outlineWidth SDF outline width (0 = no outline)
+ * @param glowRadius SDF glow radius (0 = no glow)
+ * @param shadowSoftness SDF shadow softness (0 = no shadow)
+ * @param threshold SDF edge threshold (default 0.5)
+ * @param layer Depth for layering (higher = on top)
+ */
+ data class ScreenTextVertex(
+ val x: Float, val y: Float,
+ val u: Float, val v: Float,
+ val r: Int, val g: Int, val b: Int, val a: Int,
+ // SDF style params (replaces SDFParams uniform)
+ val outlineWidth: Float = 0f,
+ val glowRadius: Float = 0f,
+ val shadowSoftness: Float = 0f,
+ val threshold: Float = 0.5f,
+ val layer: Float = 0f // Depth for layering (higher = on top)
+ )
+
+ // ============================================================================
+ // Image Vertex Types
+ // ============================================================================
+
+ /**
+ * Screen-space image vertex data with overlay support.
+ * Uses SCREEN_IMAGE_FORMAT (position + UV + color + overlayUV + layer).
+ *
+ * @param x Screen-space X position
+ * @param y Screen-space Y position
+ * @param u Main texture U coordinate
+ * @param v Main texture V coordinate
+ * @param r Red tint component
+ * @param g Green tint component
+ * @param b Blue tint component
+ * @param a Alpha component
+ * @param overlayU Overlay texture U coordinate
+ * @param overlayV Overlay texture V coordinate
+ * @param hasOverlay 1.0 if overlay should be rendered, 0.0 otherwise
+ * @param layer Depth for layering (higher = on top)
+ */
+ data class ScreenImageVertex(
+ val x: Float, val y: Float,
+ val u: Float, val v: Float,
+ val r: Int, val g: Int, val b: Int, val a: Int,
+ val overlayU: Float, val overlayV: Float,
+ val hasOverlay: Float,
+ val diffuseAmount: Float,
+ val layer: Float
+ )
+
+ /**
+ * World-space image vertex data with billboard support and overlay.
+ * Uses WORLD_IMAGE_FORMAT (position + UV + color + anchor + billboard + overlayUV).
+ *
+ * @param localX Local offset X (before billboard transform)
+ * @param localY Local offset Y (before billboard transform)
+ * @param u Main texture U coordinate
+ * @param v Main texture V coordinate
+ * @param r Red tint component
+ * @param g Green tint component
+ * @param b Blue tint component
+ * @param a Alpha component
+ * @param anchorX Camera-relative anchor X
+ * @param anchorY Camera-relative anchor Y
+ * @param anchorZ Camera-relative anchor Z
+ * @param scale Image scale
+ * @param billboardFlag 0 = billboard towards camera, non-zero = fixed rotation
+ * @param overlayU Overlay texture U coordinate
+ * @param overlayV Overlay texture V coordinate
+ * @param hasOverlay 1.0 if overlay should be rendered, 0.0 otherwise
+ */
+ data class WorldImageVertex(
+ val localX: Float, val localY: Float,
+ val u: Float, val v: Float,
+ val r: Int, val g: Int, val b: Int, val a: Int,
+ val anchorX: Float, val anchorY: Float, val anchorZ: Float,
+ val scale: Float,
+ val billboardFlag: Float,
+ val overlayU: Float, val overlayV: Float,
+ val hasOverlay: Float,
+ val diffuseAmount: Float
+ )
+
+ /**
+ * Model vertex data for generic 3D models.
+ * Uses WORLD_MODEL_FORMAT (position + color + UV0 + overlay + light + lightDir + light1Dir + normal + edgeData).
+ *
+ * @param x World X
+ * @param y World Y
+ * @param z World Z
+ * @param u Texture U
+ * @param v Texture V
+ * @param r Red
+ * @param g Green
+ * @param b Blue
+ * @param a Alpha
+ * @param overlayU Overlay U
+ * @param overlayV Overlay V
+ * @param hasOverlay 1.0 if overlay active
+ * @param diffuseAmount Amount of diffuse shading (0 = lightmap only, 1 = full diffuse)
+ * @param light Packed lightmap coordinates
+ * @param lx Primary light direction X (pre-transformed for ITEMS_FLAT)
+ * @param ly Primary light direction Y
+ * @param lz Primary light direction Z
+ * @param l1x Secondary/fill light direction X
+ * @param l1y Secondary/fill light direction Y
+ * @param l1z Secondary/fill light direction Z
+ * @param nx Normal X
+ * @param ny Normal Y
+ * @param nz Normal Z
+ * @param edgeX Edge coordinate for AA
+ * @param edgeY Edge coordinate for AA
+ */
+ data class ModelVertex(
+ val x: Float, val y: Float, val z: Float,
+ val u: Float, val v: Float,
+ val r: Int, val g: Int, val b: Int, val a: Int,
+ val overlayU: Float, val overlayV: Float, val hasOverlay: Float,
+ val diffuseAmount: Float,
+ val light: Int,
+ val lx: Float, val ly: Float, val lz: Float,
+ val l1x: Float, val l1y: Float, val l1z: Float,
+ val nx: Float, val ny: Float, val nz: Float,
+ val edgeX: Float, val edgeY: Float
)
+ /**
+ * Key for image batches - combines texture and filter mode.
+ * Batches with the same texture but different filter modes are separate.
+ */
+ data class ImageBatchKey(
+ val textureView: com.mojang.blaze3d.textures.GpuTextureView,
+ val useNearestFilter: Boolean
+ )
+
+ // Image vertex collections - keyed by texture + filter mode for batching
+ // Each unique key gets its own list of vertices, rendered as separate draw calls
+ private val screenImageBatches = java.util.concurrent.ConcurrentHashMap>()
+ private val worldImageBatches = java.util.concurrent.ConcurrentHashMap>()
+ private val modelBatches = java.util.concurrent.ConcurrentHashMap>()
+ private val screenModelBatches = java.util.concurrent.ConcurrentHashMap>()
+
+ /**
+ * Add screen image vertices for a specific texture.
+ * @param texture The GPU texture view
+ * @param vertices The vertices to add
+ * @param useNearestFilter If true, use NEAREST filtering for pixel-perfect rendering
+ */
+ fun addScreenImageVertices(texture: GpuTextureView, vertices: List, useNearestFilter: Boolean = false) {
+ val key = ImageBatchKey(texture, useNearestFilter)
+ screenImageBatches.getOrPut(key) { ConcurrentLinkedDeque() }.addAll(vertices)
+ }
+
+ /**
+ * Add world image vertices for a specific texture.
+ * @param texture The GPU texture view
+ * @param vertices The vertices to add
+ * @param useNearestFilter If true, use NEAREST filtering for pixel-perfect rendering
+ */
+ fun addWorldImageVertices(texture: GpuTextureView, vertices: List, useNearestFilter: Boolean = false) {
+ val key = ImageBatchKey(texture, useNearestFilter)
+ worldImageBatches.getOrPut(key) { ConcurrentLinkedDeque() }.addAll(vertices)
+ }
+
+ /**
+ * Add model vertices for a specific texture.
+ * @param texture The GPU texture view
+ * @param vertices The vertices to add
+ * @param useNearestFilter If true, use NEAREST filtering
+ */
+ fun addModelVertices(texture: GpuTextureView, vertices: List, useNearestFilter: Boolean = false) {
+ val key = ImageBatchKey(texture, useNearestFilter)
+ modelBatches.getOrPut(key) { ConcurrentLinkedDeque() }.addAll(vertices)
+ }
+
+ /**
+ * Add screen model vertices for a specific texture.
+ */
+ fun addScreenModelVertices(texture: GpuTextureView, vertices: List, useNearestFilter: Boolean = false) {
+ val key = ImageBatchKey(texture, useNearestFilter)
+ screenModelBatches.getOrPut(key) { ConcurrentLinkedDeque() }.addAll(vertices)
+ }
+
/** Add a face vertex. */
fun addFaceVertex(x: Float, y: Float, z: Float, color: Color) {
faceVertices.add(FaceVertex(x, y, z, color.red, color.green, color.blue, color.alpha))
}
- /** Add an edge vertex. */
+ /** Add an edge vertex (solid line). */
fun addEdgeVertex(
x: Float,
y: Float,
@@ -83,15 +347,127 @@ class RegionVertexCollector {
)
}
+ /** Add an edge vertex with dash style. */
+ fun addEdgeVertex(
+ x: Float,
+ y: Float,
+ z: Float,
+ color: Color,
+ nx: Float,
+ ny: Float,
+ nz: Float,
+ lineWidth: Float,
+ dashStyle: LineDashStyle?
+ ) {
+ if (dashStyle == null) {
+ addEdgeVertex(x, y, z, color, nx, ny, nz, lineWidth)
+ } else {
+ edgeVertices.add(
+ EdgeVertex(
+ x, y, z,
+ color.red, color.green, color.blue, color.alpha,
+ nx, ny, nz,
+ lineWidth,
+ dashStyle.dashLength,
+ dashStyle.gapLength,
+ dashStyle.offset,
+ if (dashStyle.animated) dashStyle.animationSpeed else 0f
+ )
+ )
+ }
+ }
+
+ /**
+ * Add a billboard text vertex.
+ *
+ * @param localX Local glyph offset X (before billboard transform)
+ * @param localY Local glyph offset Y (before billboard transform)
+ * @param u Texture U coordinate
+ * @param v Texture V coordinate
+ * @param r Red color component
+ * @param g Green color component
+ * @param b Blue color component
+ * @param a Alpha component (encodes layer type)
+ * @param anchorX Camera-relative anchor X
+ * @param anchorY Camera-relative anchor Y
+ * @param anchorZ Camera-relative anchor Z
+ * @param scale Text scale
+ * @param billboard True = auto-billboard towards camera, False = fixed rotation (offset already transformed)
+ */
+ fun addTextVertex(
+ localX: Float, localY: Float,
+ u: Float, v: Float,
+ r: Int, g: Int, b: Int, a: Int,
+ anchorX: Float, anchorY: Float, anchorZ: Float,
+ scale: Float,
+ billboard: Boolean
+ ) {
+ textVertices.add(TextVertex(
+ localX, localY, u, v, r, g, b, a,
+ anchorX, anchorY, anchorZ, scale,
+ if (billboard) 0f else 1f
+ ))
+ }
+
+ // ============================================================================
+ // Screen-Space Vertex Add Methods
+ // ============================================================================
+
+ /** Add a screen-space face vertex with layer for draw order. */
+ fun addScreenFaceVertex(x: Float, y: Float, color: Color, layer: Float) {
+ screenFaceVertices.add(ScreenFaceVertex(x, y, color.red, color.green, color.blue, color.alpha, layer))
+ }
+
+ /** Add a screen-space edge vertex (solid line) with layer for draw order. */
+ fun addScreenEdgeVertex(x: Float, y: Float, color: Color, dx: Float, dy: Float, lineWidth: Float, layer: Float) {
+ screenEdgeVertices.add(ScreenEdgeVertex(x, y, color.red, color.green, color.blue, color.alpha, dx, dy, lineWidth, layer = layer))
+ }
+
+ /** Add a screen-space edge vertex with dash style and layer for draw order. */
+ fun addScreenEdgeVertex(
+ x: Float, y: Float,
+ color: Color,
+ dx: Float, dy: Float,
+ lineWidth: Float,
+ dashStyle: LineDashStyle?,
+ layer: Float
+ ) {
+ if (dashStyle == null) {
+ addScreenEdgeVertex(x, y, color, dx, dy, lineWidth, layer)
+ } else {
+ screenEdgeVertices.add(
+ ScreenEdgeVertex(
+ x, y,
+ color.red, color.green, color.blue, color.alpha,
+ dx, dy,
+ lineWidth,
+ dashStyle.dashLength,
+ dashStyle.gapLength,
+ dashStyle.offset,
+ if (dashStyle.animated) dashStyle.animationSpeed else 0f,
+ layer
+ )
+ )
+ }
+ }
+
+ /** Add a screen-space text vertex with layer for draw order. */
+ fun addScreenTextVertex(x: Float, y: Float, u: Float, v: Float, r: Int, g: Int, b: Int, a: Int, layer: Float) {
+ screenTextVertices.add(ScreenTextVertex(x, y, u, v, r, g, b, a, layer = layer))
+ }
+
/**
* Upload collected data to GPU buffers. Must be called on the main/render thread.
*
- * @return Pair of (faceBuffer, edgeBuffer) and their index counts, or null if no data
+ * @return UploadResult containing face, edge, and text buffers with index counts
*/
fun upload(): UploadResult {
val faces = uploadFaces()
val edges = uploadEdges()
- return UploadResult(faces, edges)
+ val text = uploadText()
+ val models = uploadModelBatches()
+ val images = uploadWorldImageBatches()
+ return UploadResult(faces, edges, text, models, images)
}
private fun uploadFaces(): BufferResult {
@@ -133,19 +509,41 @@ class RegionVertexCollector {
edgeVertices.clear()
var result: BufferResult? = null
- BufferAllocator(vertices.size * 32).use { allocator ->
+ // Increased buffer size to accommodate the new dash vec3 (3 floats = 12 bytes extra)
+ BufferAllocator(vertices.size * 48).use { allocator ->
val builder =
BufferBuilder(
allocator,
VertexFormat.DrawMode.QUADS,
- VertexFormats.POSITION_COLOR_NORMAL_LINE_WIDTH
+ LambdaVertexFormats.POSITION_COLOR_NORMAL_LINE_WIDTH_DASH
)
vertices.forEach { v ->
builder.vertex(v.x, v.y, v.z)
.color(v.r, v.g, v.b, v.a)
- .normal(v.nx, v.ny, v.nz)
- .lineWidth(v.lineWidth)
+
+ // Write Normal as 3 floats (NOT using .normal() which writes bytes)
+ val normalPointer = builder.beginElement(LambdaVertexFormats.NORMAL_FLOAT)
+ if (normalPointer != -1L) {
+ MemoryUtil.memPutFloat(normalPointer, v.nx)
+ MemoryUtil.memPutFloat(normalPointer + 4L, v.ny)
+ MemoryUtil.memPutFloat(normalPointer + 8L, v.nz)
+ }
+
+ // Write LineWidth as float
+ val widthPointer = builder.beginElement(LambdaVertexFormats.LINE_WIDTH_FLOAT)
+ if (widthPointer != -1L) {
+ MemoryUtil.memPutFloat(widthPointer, v.lineWidth)
+ }
+
+ // Write dash data using access-widened beginElement (vec4)
+ val dashPointer = builder.beginElement(LambdaVertexFormats.DASH_ELEMENT)
+ if (dashPointer != -1L) {
+ MemoryUtil.memPutFloat(dashPointer, v.dashLength)
+ MemoryUtil.memPutFloat(dashPointer + 4L, v.gapLength)
+ MemoryUtil.memPutFloat(dashPointer + 8L, v.dashOffset)
+ MemoryUtil.memPutFloat(dashPointer + 12L, v.animationSpeed)
+ }
}
builder.endNullable()?.let { built ->
@@ -163,6 +561,519 @@ class RegionVertexCollector {
return result ?: BufferResult(null, 0)
}
+ private fun uploadText(): BufferResult {
+ if (textVertices.isEmpty()) return BufferResult(null, 0)
+
+ val vertices = textVertices.toList()
+ textVertices.clear()
+
+ var result: BufferResult? = null
+ // POSITION_TEXTURE_COLOR_ANCHOR_SDF: 12 + 8 + 4 + 12 + 8 + 16 = 60 bytes per vertex
+ BufferAllocator(vertices.size * 64).use { allocator ->
+ val builder = BufferBuilder(
+ allocator,
+ VertexFormat.DrawMode.QUADS,
+ LambdaVertexFormats.POSITION_TEXTURE_COLOR_ANCHOR_SDF
+ )
+
+ vertices.forEach { v ->
+ // Position stores local glyph offset (z unused, set to 0)
+ builder.vertex(v.localX, v.localY, 0f)
+ .texture(v.u, v.v)
+ .color(v.r, v.g, v.b, v.a)
+
+ // Write Anchor position (camera-relative world pos)
+ val anchorPointer = builder.beginElement(LambdaVertexFormats.ANCHOR_ELEMENT)
+ if (anchorPointer != -1L) {
+ MemoryUtil.memPutFloat(anchorPointer, v.anchorX)
+ MemoryUtil.memPutFloat(anchorPointer + 4L, v.anchorY)
+ MemoryUtil.memPutFloat(anchorPointer + 8L, v.anchorZ)
+ }
+
+ // Write Billboard data (scale, billboardFlag)
+ val billboardPointer = builder.beginElement(LambdaVertexFormats.BILLBOARD_DATA_ELEMENT)
+ if (billboardPointer != -1L) {
+ MemoryUtil.memPutFloat(billboardPointer, v.scale)
+ MemoryUtil.memPutFloat(billboardPointer + 4L, v.billboardFlag)
+ }
+
+ // Write SDF style params (outlineWidth, glowRadius, shadowSoftness, threshold)
+ val sdfPointer = builder.beginElement(LambdaVertexFormats.SDF_STYLE_ELEMENT)
+ if (sdfPointer != -1L) {
+ MemoryUtil.memPutFloat(sdfPointer, v.outlineWidth)
+ MemoryUtil.memPutFloat(sdfPointer + 4L, v.glowRadius)
+ MemoryUtil.memPutFloat(sdfPointer + 8L, v.shadowSoftness)
+ MemoryUtil.memPutFloat(sdfPointer + 12L, v.threshold)
+ }
+ }
+
+ builder.endNullable()?.let { built ->
+ val gpuDevice = RenderSystem.getDevice()
+ val buffer = gpuDevice.createBuffer(
+ { "Lambda ESP Text Buffer" },
+ GpuBuffer.USAGE_VERTEX,
+ built.buffer
+ )
+ result = BufferResult(buffer, built.drawParameters.indexCount())
+ built.close()
+ }
+ }
+ return result ?: BufferResult(null, 0)
+ }
+
+ // ============================================================================
+ // Screen-Space Upload Methods
+ // ============================================================================
+
+ private fun uploadScreenFaces(): BufferResult {
+ if (screenFaceVertices.isEmpty()) return BufferResult(null, 0)
+
+ val vertices = screenFaceVertices.toList()
+ screenFaceVertices.clear()
+
+ var result: BufferResult? = null
+ // SCREEN_FACE_FORMAT: 12 + 4 + 4 = 20 bytes per vertex
+ BufferAllocator(vertices.size * 24).use { allocator ->
+ val builder = BufferBuilder(
+ allocator,
+ VertexFormat.DrawMode.QUADS,
+ LambdaVertexFormats.SCREEN_FACE_FORMAT
+ )
+
+ // For screen-space: use x, y, with z = 0, plus layer
+ vertices.forEach { v ->
+ builder.vertex(v.x, v.y, 0f).color(v.r, v.g, v.b, v.a)
+
+ // Write layer for draw order
+ val layerPointer = builder.beginElement(LambdaVertexFormats.LAYER_ELEMENT)
+ if (layerPointer != -1L) {
+ MemoryUtil.memPutFloat(layerPointer, v.layer)
+ }
+ }
+
+ builder.endNullable()?.let { built ->
+ val gpuDevice = RenderSystem.getDevice()
+ val buffer = gpuDevice.createBuffer(
+ { "Lambda Screen Face Buffer" },
+ GpuBuffer.USAGE_VERTEX,
+ built.buffer
+ )
+ result = BufferResult(buffer, built.drawParameters.indexCount())
+ built.close()
+ }
+ }
+ return result ?: BufferResult(null, 0)
+ }
+
+ private fun uploadScreenEdges(): BufferResult {
+ if (screenEdgeVertices.isEmpty()) return BufferResult(null, 0)
+
+ val vertices = screenEdgeVertices.toList()
+ screenEdgeVertices.clear()
+
+ var result: BufferResult? = null
+ // Position (12) + Color (4) + Direction (8) + Width (4) + Dash (16) + Layer (4) = 48 bytes
+ BufferAllocator(vertices.size * 52).use { allocator ->
+ val builder = BufferBuilder(
+ allocator,
+ VertexFormat.DrawMode.QUADS,
+ LambdaVertexFormats.SCREEN_LINE_FORMAT
+ )
+
+ vertices.forEach { v ->
+ builder.vertex(v.x, v.y, 0f).color(v.r, v.g, v.b, v.a)
+
+ // Write direction (for calculating perpendicular offset in shader)
+ val dirPointer = builder.beginElement(LambdaVertexFormats.DIRECTION_2D_ELEMENT)
+ if (dirPointer != -1L) {
+ MemoryUtil.memPutFloat(dirPointer, v.dx)
+ MemoryUtil.memPutFloat(dirPointer + 4L, v.dy)
+ }
+
+ // Write line width
+ val widthPointer = builder.beginElement(LambdaVertexFormats.LINE_WIDTH_FLOAT)
+ if (widthPointer != -1L) {
+ MemoryUtil.memPutFloat(widthPointer, v.lineWidth)
+ }
+
+ // Write dash data
+ val dashPointer = builder.beginElement(LambdaVertexFormats.DASH_ELEMENT)
+ if (dashPointer != -1L) {
+ MemoryUtil.memPutFloat(dashPointer, v.dashLength)
+ MemoryUtil.memPutFloat(dashPointer + 4L, v.gapLength)
+ MemoryUtil.memPutFloat(dashPointer + 8L, v.dashOffset)
+ MemoryUtil.memPutFloat(dashPointer + 12L, v.animationSpeed)
+ }
+
+ // Write layer for draw order
+ val layerPointer = builder.beginElement(LambdaVertexFormats.LAYER_ELEMENT)
+ if (layerPointer != -1L) {
+ MemoryUtil.memPutFloat(layerPointer, v.layer)
+ }
+ }
+
+ builder.endNullable()?.let { built ->
+ val gpuDevice = RenderSystem.getDevice()
+ val buffer = gpuDevice.createBuffer(
+ { "Lambda Screen Edge Buffer" },
+ GpuBuffer.USAGE_VERTEX,
+ built.buffer
+ )
+ result = BufferResult(buffer, built.drawParameters.indexCount())
+ built.close()
+ }
+ }
+ return result ?: BufferResult(null, 0)
+ }
+
+ private fun uploadScreenText(): BufferResult {
+ if (screenTextVertices.isEmpty()) return BufferResult(null, 0)
+
+ val vertices = screenTextVertices.toList()
+ screenTextVertices.clear()
+
+ var result: BufferResult? = null
+ // SCREEN_TEXT_SDF_FORMAT: 12 + 8 + 4 + 16 + 4 = 44 bytes per vertex
+ BufferAllocator(vertices.size * 48).use { allocator ->
+ val builder = BufferBuilder(
+ allocator,
+ VertexFormat.DrawMode.QUADS,
+ LambdaVertexFormats.SCREEN_TEXT_SDF_FORMAT
+ )
+
+ // Screen text: position is already final screen coordinates
+ vertices.forEach { v ->
+ builder.vertex(v.x, v.y, 0f)
+ .texture(v.u, v.v)
+ .color(v.r, v.g, v.b, v.a)
+
+ // Write SDF style params (outlineWidth, glowRadius, shadowSoftness, threshold)
+ val sdfPointer = builder.beginElement(LambdaVertexFormats.SDF_STYLE_ELEMENT)
+ if (sdfPointer != -1L) {
+ MemoryUtil.memPutFloat(sdfPointer, v.outlineWidth)
+ MemoryUtil.memPutFloat(sdfPointer + 4L, v.glowRadius)
+ MemoryUtil.memPutFloat(sdfPointer + 8L, v.shadowSoftness)
+ MemoryUtil.memPutFloat(sdfPointer + 12L, v.threshold)
+ }
+
+ // Write layer for draw order
+ val layerPointer = builder.beginElement(LambdaVertexFormats.LAYER_ELEMENT)
+ if (layerPointer != -1L) {
+ MemoryUtil.memPutFloat(layerPointer, v.layer)
+ }
+ }
+
+ builder.endNullable()?.let { built ->
+ val gpuDevice = RenderSystem.getDevice()
+ val buffer = gpuDevice.createBuffer(
+ { "Lambda Screen Text Buffer" },
+ GpuBuffer.USAGE_VERTEX,
+ built.buffer
+ )
+ result = BufferResult(buffer, built.drawParameters.indexCount())
+ built.close()
+ }
+ }
+ return result ?: BufferResult(null, 0)
+ }
+
+ /**
+ * Upload screen-space data to GPU buffers.
+ *
+ * @return ScreenUploadResult containing screen-space face, edge, text, and image buffers
+ */
+ fun uploadScreen(): ScreenUploadResult {
+ val faces = uploadScreenFaces()
+ val edges = uploadScreenEdges()
+ val text = uploadScreenText()
+ val images = uploadScreenImageBatches()
+ val models = uploadScreenModelBatches()
+ return ScreenUploadResult(faces, edges, text, images, models)
+ }
+
+ /**
+ * Result for a single texture batch - buffer, index count, and filter mode.
+ */
+ data class TextureBatchResult(
+ val textureView: com.mojang.blaze3d.textures.GpuTextureView,
+ val buffer: GpuBuffer,
+ val indexCount: Int,
+ val useNearestFilter: Boolean = false
+ )
+
+ private fun uploadScreenImageBatches(): List {
+ if (screenImageBatches.isEmpty()) return emptyList()
+
+ val results = mutableListOf()
+
+ screenImageBatches.forEach { (batchKey, vertexDeque) ->
+ val vertices = vertexDeque.toList()
+ vertexDeque.clear()
+ if (vertices.isEmpty()) return@forEach
+
+ // SCREEN_IMAGE_FORMAT: 12 + 8 + 4 + 12 + 4 = 40 bytes per vertex
+ BufferAllocator(vertices.size * 44).use { allocator ->
+ val builder = BufferBuilder(
+ allocator,
+ VertexFormat.DrawMode.QUADS,
+ LambdaVertexFormats.SCREEN_IMAGE_FORMAT
+ )
+
+ vertices.forEach { v ->
+ builder.vertex(v.x, v.y, 0f)
+ .texture(v.u, v.v)
+ .color(v.r, v.g, v.b, v.a)
+
+ // Write overlay UV data (overlayU, overlayV, hasOverlay, diffuseAmount)
+ val overlayPointer = builder.beginElement(LambdaVertexFormats.OVERLAY_UV_ELEMENT)
+ if (overlayPointer != -1L) {
+ MemoryUtil.memPutFloat(overlayPointer, v.overlayU)
+ MemoryUtil.memPutFloat(overlayPointer + 4L, v.overlayV)
+ MemoryUtil.memPutFloat(overlayPointer + 8L, v.hasOverlay)
+ MemoryUtil.memPutFloat(overlayPointer + 12L, v.diffuseAmount)
+ }
+
+ // Write layer for draw order
+ val layerPointer = builder.beginElement(LambdaVertexFormats.LAYER_ELEMENT)
+ if (layerPointer != -1L) {
+ MemoryUtil.memPutFloat(layerPointer, v.layer)
+ }
+ }
+
+ builder.endNullable()?.let { built ->
+ val gpuDevice = RenderSystem.getDevice()
+ val buffer = gpuDevice.createBuffer(
+ { "Lambda Screen Image Buffer" },
+ GpuBuffer.USAGE_VERTEX,
+ built.buffer
+ )
+ results.add(TextureBatchResult(batchKey.textureView, buffer, built.drawParameters.indexCount(), batchKey.useNearestFilter))
+ built.close()
+ }
+ }
+ }
+ screenImageBatches.clear()
+ return results
+ }
+
+ fun uploadWorldImageBatches(): List {
+ if (worldImageBatches.isEmpty()) return emptyList()
+
+ val results = mutableListOf()
+
+ worldImageBatches.forEach { (batchKey, vertexDeque) ->
+ val vertices = vertexDeque.toList()
+ vertexDeque.clear()
+ if (vertices.isEmpty()) return@forEach
+
+ // WORLD_IMAGE_FORMAT: 12 + 8 + 4 + 12 + 8 + 12 = 56 bytes per vertex
+ BufferAllocator(vertices.size * 60).use { allocator ->
+ val builder = BufferBuilder(
+ allocator,
+ VertexFormat.DrawMode.QUADS,
+ LambdaVertexFormats.WORLD_IMAGE_FORMAT
+ )
+
+ vertices.forEach { v ->
+ builder.vertex(v.localX, v.localY, 0f)
+ .texture(v.u, v.v)
+ .color(v.r, v.g, v.b, v.a)
+
+ // Write Anchor position (camera-relative world pos)
+ val anchorPointer = builder.beginElement(LambdaVertexFormats.ANCHOR_ELEMENT)
+ if (anchorPointer != -1L) {
+ MemoryUtil.memPutFloat(anchorPointer, v.anchorX)
+ MemoryUtil.memPutFloat(anchorPointer + 4L, v.anchorY)
+ MemoryUtil.memPutFloat(anchorPointer + 8L, v.anchorZ)
+ }
+
+ // Write Billboard data (scale, billboardFlag)
+ val billboardPointer = builder.beginElement(LambdaVertexFormats.BILLBOARD_DATA_ELEMENT)
+ if (billboardPointer != -1L) {
+ MemoryUtil.memPutFloat(billboardPointer, v.scale)
+ MemoryUtil.memPutFloat(billboardPointer + 4L, v.billboardFlag)
+ }
+
+ // Write overlay UV data (overlayU, overlayV, hasOverlay, diffuseAmount)
+ val overlayPointer = builder.beginElement(LambdaVertexFormats.OVERLAY_UV_ELEMENT)
+ if (overlayPointer != -1L) {
+ MemoryUtil.memPutFloat(overlayPointer, v.overlayU)
+ MemoryUtil.memPutFloat(overlayPointer + 4L, v.overlayV)
+ MemoryUtil.memPutFloat(overlayPointer + 8L, v.hasOverlay)
+ MemoryUtil.memPutFloat(overlayPointer + 12L, v.diffuseAmount)
+ }
+ }
+
+ builder.endNullable()?.let { built ->
+ val gpuDevice = RenderSystem.getDevice()
+ val buffer = gpuDevice.createBuffer(
+ { "Lambda World Image Buffer" },
+ GpuBuffer.USAGE_VERTEX,
+ built.buffer
+ )
+ results.add(TextureBatchResult(batchKey.textureView, buffer, built.drawParameters.indexCount(), batchKey.useNearestFilter))
+ built.close()
+ }
+ }
+ }
+ worldImageBatches.clear()
+ return results
+ }
+
+ fun uploadModelBatches(): List {
+ if (modelBatches.isEmpty()) return emptyList()
+
+ val results = mutableListOf()
+
+ modelBatches.forEach { (batchKey, vertexDeque) ->
+ val vertices = vertexDeque.toList()
+ vertexDeque.clear()
+ if (vertices.isEmpty()) return@forEach
+
+ // WORLD_MODEL_FORMAT: 12 + 4 + 8 + 16 + 4 + 12 + 12 + 12 + 8 = 88 bytes per vertex
+ BufferAllocator(vertices.size * 88).use { allocator ->
+ val builder = BufferBuilder(
+ allocator,
+ VertexFormat.DrawMode.QUADS,
+ LambdaVertexFormats.WORLD_MODEL_FORMAT
+ )
+
+ vertices.forEach { v ->
+ builder.vertex(v.x, v.y, v.z)
+ .color(v.r, v.g, v.b, v.a)
+ .texture(v.u, v.v)
+
+ // Write Overlay UV
+ val overlayPointer = builder.beginElement(LambdaVertexFormats.OVERLAY_UV_ELEMENT)
+ if (overlayPointer != -1L) {
+ MemoryUtil.memPutFloat(overlayPointer, v.overlayU)
+ MemoryUtil.memPutFloat(overlayPointer + 4L, v.overlayV)
+ MemoryUtil.memPutFloat(overlayPointer + 8L, v.hasOverlay)
+ MemoryUtil.memPutFloat(overlayPointer + 12L, v.diffuseAmount)
+ }
+
+ // Write Light (UV2) - 2 shorts
+ val lightPointer = builder.beginElement(VertexFormatElement.UV2)
+ if (lightPointer != -1L) {
+ MemoryUtil.memPutShort(lightPointer, (v.light and 0xFFFF).toShort())
+ MemoryUtil.memPutShort(lightPointer + 2L, ((v.light shr 16) and 0xFFFF).toShort())
+ }
+
+ // Write LightDir (primary light)
+ val lightDirPointer = builder.beginElement(LambdaVertexFormats.LIGHT_DIR_ELEMENT)
+ if (lightDirPointer != -1L) {
+ MemoryUtil.memPutFloat(lightDirPointer, v.lx)
+ MemoryUtil.memPutFloat(lightDirPointer + 4L, v.ly)
+ MemoryUtil.memPutFloat(lightDirPointer + 8L, v.lz)
+ }
+
+ // Write Light1Dir (fill light)
+ val light1DirPointer = builder.beginElement(LambdaVertexFormats.LIGHT1_DIR_ELEMENT)
+ if (light1DirPointer != -1L) {
+ MemoryUtil.memPutFloat(light1DirPointer, v.l1x)
+ MemoryUtil.memPutFloat(light1DirPointer + 4L, v.l1y)
+ MemoryUtil.memPutFloat(light1DirPointer + 8L, v.l1z)
+ }
+
+ // Write Normal (Float)
+ val normalPointer = builder.beginElement(LambdaVertexFormats.NORMAL_FLOAT)
+ if (normalPointer != -1L) {
+ MemoryUtil.memPutFloat(normalPointer, v.nx)
+ MemoryUtil.memPutFloat(normalPointer + 4L, v.ny)
+ MemoryUtil.memPutFloat(normalPointer + 8L, v.nz)
+ }
+
+ // Write EdgeData (vec2)
+ val edgePointer = builder.beginElement(LambdaVertexFormats.EDGE_DATA_ELEMENT)
+ if (edgePointer != -1L) {
+ MemoryUtil.memPutFloat(edgePointer, v.edgeX)
+ MemoryUtil.memPutFloat(edgePointer + 4L, v.edgeY)
+ }
+ }
+
+ builder.endNullable()?.let { built ->
+ val gpuDevice = RenderSystem.getDevice()
+ val buffer = gpuDevice.createBuffer(
+ { "Lambda Model Buffer" },
+ GpuBuffer.USAGE_VERTEX,
+ built.buffer
+ )
+ results.add(TextureBatchResult(batchKey.textureView, buffer, built.drawParameters.indexCount(), batchKey.useNearestFilter))
+ built.close()
+ }
+ }
+ }
+ modelBatches.clear()
+ return results
+ }
+
+ private fun uploadScreenModelBatches(): List {
+ if (screenModelBatches.isEmpty()) return emptyList()
+ val results = mutableListOf()
+ screenModelBatches.forEach { (batchKey, vertexDeque) ->
+ val vertices = vertexDeque.toList()
+ vertexDeque.clear()
+ if (vertices.isEmpty()) return@forEach
+ // 88 bytes per vertex (added Light1Dir)
+ BufferAllocator(vertices.size * 88).use { allocator ->
+ val builder = BufferBuilder(allocator, VertexFormat.DrawMode.QUADS, LambdaVertexFormats.WORLD_MODEL_FORMAT)
+ vertices.forEach { v ->
+ builder.vertex(v.x, v.y, v.z).color(v.r, v.g, v.b, v.a).texture(v.u, v.v)
+ builder.beginElement(LambdaVertexFormats.OVERLAY_UV_ELEMENT).let { p ->
+ if (p != -1L) {
+ MemoryUtil.memPutFloat(p, v.overlayU)
+ MemoryUtil.memPutFloat(p + 4, v.overlayV)
+ MemoryUtil.memPutFloat(p + 8, v.hasOverlay)
+ MemoryUtil.memPutFloat(p + 12, v.diffuseAmount)
+ }
+ }
+ builder.beginElement(VertexFormatElement.UV2).let { p ->
+ if (p != -1L) {
+ MemoryUtil.memPutShort(p, (v.light and 0xFFFF).toShort())
+ MemoryUtil.memPutShort(p + 2, ((v.light shr 16) and 0xFFFF).toShort())
+ }
+ }
+ builder.beginElement(LambdaVertexFormats.LIGHT_DIR_ELEMENT).let { p ->
+ if (p != -1L) {
+ MemoryUtil.memPutFloat(p, v.lx)
+ MemoryUtil.memPutFloat(p + 4, v.ly)
+ MemoryUtil.memPutFloat(p + 8, v.lz)
+ }
+ }
+ builder.beginElement(LambdaVertexFormats.LIGHT1_DIR_ELEMENT).let { p ->
+ if (p != -1L) {
+ MemoryUtil.memPutFloat(p, v.l1x)
+ MemoryUtil.memPutFloat(p + 4, v.l1y)
+ MemoryUtil.memPutFloat(p + 8, v.l1z)
+ }
+ }
+ builder.beginElement(LambdaVertexFormats.NORMAL_FLOAT).let { p ->
+ if (p != -1L) {
+ MemoryUtil.memPutFloat(p, v.nx)
+ MemoryUtil.memPutFloat(p + 4, v.ny)
+ MemoryUtil.memPutFloat(p + 8, v.nz)
+ }
+ }
+ builder.beginElement(LambdaVertexFormats.EDGE_DATA_ELEMENT).let { p ->
+ if (p != -1L) {
+ MemoryUtil.memPutFloat(p, v.edgeX)
+ MemoryUtil.memPutFloat(p + 4, v.edgeY)
+ }
+ }
+ }
+ builder.endNullable()?.let { built ->
+ val buffer = RenderSystem.getDevice().createBuffer({ "Lambda Screen Model Buffer" }, GpuBuffer.USAGE_VERTEX, built.buffer)
+ results.add(TextureBatchResult(batchKey.textureView, buffer, built.drawParameters.indexCount(), batchKey.useNearestFilter))
+ built.close()
+ }
+ }
+ }
+ screenModelBatches.clear()
+ return results
+ }
+
data class BufferResult(val buffer: GpuBuffer?, val indexCount: Int)
- data class UploadResult(val faces: BufferResult?, val edges: BufferResult?)
+ data class UploadResult(val faces: BufferResult?, val edges: BufferResult?, val text: BufferResult? = null, val models: List = emptyList(), val images: List = emptyList())
+ data class ScreenUploadResult(val faces: BufferResult?, val edges: BufferResult?, val text: BufferResult? = null, val images: List = emptyList(), val models: List = emptyList())
}
+
diff --git a/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt b/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt
new file mode 100644
index 000000000..3bfd2ec22
--- /dev/null
+++ b/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt
@@ -0,0 +1,1841 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.graphics.mc
+
+import com.lambda.Lambda.mc
+import com.lambda.context.SafeContext
+import com.lambda.graphics.text.FontHandler
+import com.lambda.graphics.text.SDFFontAtlas
+import com.lambda.graphics.texture.LambdaImageAtlas
+import com.lambda.graphics.util.DirectionMask
+import com.lambda.graphics.util.DirectionMask.hasDirection
+import com.lambda.util.BlockUtils.blockState
+import net.minecraft.block.BlockState
+import net.minecraft.client.font.TextRenderer
+import net.minecraft.client.render.OverlayTexture
+import net.minecraft.client.render.item.ItemRenderState
+import net.minecraft.client.render.command.OrderedRenderCommandQueue
+import net.minecraft.client.render.VertexConsumer
+import net.minecraft.client.render.RenderLayer
+import net.minecraft.client.render.command.ModelCommandRenderer
+import net.minecraft.client.render.entity.state.EntityRenderState
+import net.minecraft.client.render.model.BakedQuad
+import net.minecraft.client.render.model.BlockModelPart
+import net.minecraft.client.render.model.BlockStateModel
+import net.minecraft.client.render.state.CameraRenderState
+import net.minecraft.client.texture.Sprite
+import net.minecraft.client.util.math.MatrixStack
+import net.minecraft.util.Identifier
+import net.minecraft.util.math.BlockPos
+import net.minecraft.util.math.Box
+import net.minecraft.util.math.Vec3d
+import net.minecraft.item.ItemDisplayContext
+import net.minecraft.item.ItemStack
+import net.minecraft.text.OrderedText
+import net.minecraft.text.Text
+import net.minecraft.util.math.random.Random
+import org.joml.Matrix4f
+import org.joml.Quaternionf
+import org.joml.Vector3f
+import org.joml.Vector4f
+import java.awt.Color
+
+@DslMarker
+annotation class RenderDsl
+
+@Suppress("unused")
+@RenderDsl
+class RenderBuilder(private val cameraPos: Vec3d) {
+ val collector = RegionVertexCollector()
+
+ /** Track font atlas for this builder (for rendering) */
+ var fontAtlas: SDFFontAtlas? = null
+ private set
+
+ // Style grouping maps removed - style is now embedded in each text vertex
+
+ // ============================================================================
+ // Screen-Space Layer Tracking
+ // ============================================================================
+ // Layer depth for screen-space ordering.
+ // With orthographic projection (near=-1000, far=1000) and LEQUAL depth test:
+ // - Higher Z = lower depth = closer to viewer = renders ON TOP
+ // - Start at -800 and increment, so later calls have higher Z (on top)
+
+ /** Current layer depth for screen-space ordering.
+ * Range: -1000 (far) to 1000 (near) in our orthographic projection.
+ */
+ private var currentLayer = -800f
+
+ /** Distance between screen layers. Each call moves slightly closer to viewer. */
+ private val layerIncrement = 1f
+
+ // Vanilla's raw light directions
+ private val DEFAULT_LIGHT_DIR = Vector3f(0.2f, 1.0f, -0.7f).normalize()
+ private val DEFAULT_LIGHT1_DIR = Vector3f(-0.2f, 1.0f, 0.7f).normalize()
+
+ private fun eulerToQuaternion(rot: Vec3d): Quaternionf {
+ return Quaternionf().rotationYXZ(
+ Math.toRadians(rot.y).toFloat(),
+ Math.toRadians(rot.x).toFloat(),
+ Math.toRadians(rot.z).toFloat()
+ )
+ }
+
+ /** Get next layer depth for screen-space ordering. Later calls render on top. */
+ private fun nextLayer(): Float {
+ val layer = currentLayer
+ currentLayer += layerIncrement
+ return layer
+ }
+
+ fun box(
+ box: Box,
+ lineWidth: Float = 0.005f,
+ builder: (BoxBuilder.() -> Unit)? = null
+ ) {
+ val boxBuilder = BoxBuilder(lineWidth).apply { builder?.invoke(this) }
+ if (boxBuilder.fillSides != DirectionMask.NONE) boxBuilder.boxFaces(box)
+ if (boxBuilder.outlineSides != DirectionMask.NONE) boxBuilder.boxOutline(box)
+ }
+
+ context(safeContext: SafeContext)
+ fun boxes(
+ pos: BlockPos,
+ state: BlockState,
+ lineWidth: Float = 0.005f,
+ builder: (BoxBuilder.() -> Unit)? = null
+ ) = with(safeContext) {
+ val boxes = state.getOutlineShape(world, pos).boundingBoxes.map { it.offset(pos) }
+ val boxBuilder = BoxBuilder(lineWidth).apply { builder?.invoke(this) }
+ boxes.forEach { box ->
+ if (boxBuilder.fillSides != DirectionMask.NONE) boxBuilder.boxFaces(box)
+ if (boxBuilder.outlineSides != DirectionMask.NONE) boxBuilder.boxOutline(box)
+ }
+ }
+
+ fun box(
+ pos: BlockPos,
+ lineWidth: Float = 0.005f,
+ builder: (BoxBuilder.() -> Unit)? = null
+ ) = box(Box(pos), lineWidth, builder)
+
+ context(safeContext: SafeContext)
+ fun boxes(
+ pos: BlockPos,
+ lineWidth: Float = 0.005f,
+ builder: (BoxBuilder.() -> Unit)? = null
+ ) = boxes(pos, safeContext.blockState(pos), lineWidth, builder)
+
+ fun filledQuadGradient(
+ corner1: Vec3d,
+ corner2: Vec3d,
+ corner3: Vec3d,
+ corner4: Vec3d,
+ color: Color
+ ) {
+ faceVertex(corner1.x, corner1.y, corner1.z, color)
+ faceVertex(corner2.x, corner2.y, corner2.z, color)
+ faceVertex(corner3.x, corner3.y, corner3.z, color)
+ faceVertex(corner4.x, corner4.y, corner4.z, color)
+ }
+
+ fun filledQuadGradient(
+ x1: Double, y1: Double, z1: Double, c1: Color,
+ x2: Double, y2: Double, z2: Double, c2: Color,
+ x3: Double, y3: Double, z3: Double, c3: Color,
+ x4: Double, y4: Double, z4: Double, c4: Color
+ ) {
+ faceVertex(x1, y1, z1, c1)
+ faceVertex(x2, y2, z2, c2)
+ faceVertex(x3, y3, z3, c3)
+ faceVertex(x4, y4, z4, c4)
+ }
+
+ fun lineGradient(
+ startPos: Vec3d, startColor: Color,
+ endPos: Vec3d, endColor: Color,
+ width: Float,
+ dashStyle: LineDashStyle? = null
+ ) = lineGradient(
+ startPos.x, startPos.y, startPos.z, startColor,
+ endPos.x, endPos.y, endPos.z, endColor,
+ width,
+ dashStyle
+ )
+
+ fun lineGradient(
+ x1: Double, y1: Double, z1: Double, c1: Color,
+ x2: Double, y2: Double, z2: Double, c2: Color,
+ width: Float,
+ dashStyle: LineDashStyle? = null
+ ) = line(x1, y1, z1, x2, y2, z2, c1, c2, width, dashStyle)
+
+ /** Draw a line between two world positions. */
+ fun line(
+ start: Vec3d,
+ end: Vec3d,
+ color: Color,
+ width: Float,
+ dashStyle: LineDashStyle? = null
+ ) = line(start.x, start.y, start.z, end.x, end.y, end.z, color, color, width, dashStyle)
+
+ /** Draw a polyline through a list of points. */
+ fun polyline(
+ points: List,
+ color: Color,
+ width: Float,
+ dashStyle: LineDashStyle? = null
+ ) {
+ if (points.size < 2) return
+ for (i in 0 until points.size - 1) {
+ line(points[i], points[i + 1], color, width, dashStyle)
+ }
+ }
+
+ /**
+ * Draw a quadratic Bezier curve.
+ *
+ * @param p0 Start point
+ * @param p1 Control point
+ * @param p2 End point
+ * @param color Line color
+ * @param segments Number of line segments (higher = smoother)
+ */
+ fun quadraticBezierLine(
+ p0: Vec3d,
+ p1: Vec3d,
+ p2: Vec3d,
+ color: Color,
+ segments: Int = 16,
+ width: Float,
+ dashStyle: LineDashStyle? = null
+ ) {
+ val points = CurveUtils.quadraticBezierPoints(p0, p1, p2, segments)
+ polyline(points, color, width, dashStyle)
+ }
+
+ /**
+ * Draw a cubic Bezier curve.
+ *
+ * @param p0 Start point
+ * @param p1 First control point
+ * @param p2 Second control point
+ * @param p3 End point
+ * @param color Line color
+ * @param segments Number of line segments (higher = smoother)
+ */
+ fun cubicBezierLine(
+ p0: Vec3d,
+ p1: Vec3d,
+ p2: Vec3d,
+ p3: Vec3d,
+ color: Color,
+ segments: Int = 32,
+ width: Float,
+ dashStyle: LineDashStyle? = null
+ ) {
+ val points = CurveUtils.cubicBezierPoints(p0, p1, p2, p3, segments)
+ polyline(points, color, width, dashStyle)
+ }
+
+ /**
+ * Draw a Catmull-Rom spline that passes through all control points.
+ *
+ * @param controlPoints List of points the spline should pass through (minimum 4)
+ * @param color Line color
+ * @param segmentsPerSection Segments between each pair of control points
+ */
+ fun catmullRomSplineLine(
+ controlPoints: List,
+ color: Color,
+ segmentsPerSection: Int = 16,
+ width: Float,
+ dashStyle: LineDashStyle? = null
+ ) {
+ val points = CurveUtils.catmullRomSplinePoints(controlPoints, segmentsPerSection)
+ polyline(points, color, width, dashStyle)
+ }
+
+ /**
+ * Draw a smooth path through waypoints using Catmull-Rom splines. Handles endpoints
+ * naturally by mirroring.
+ *
+ * @param waypoints List of points to pass through (minimum 2)
+ * @param color Line color
+ * @param segmentsPerSection Smoothness (higher = smoother)
+ */
+ fun smoothLine(
+ waypoints: List,
+ color: Color,
+ segmentsPerSection: Int = 16,
+ width: Float,
+ dashStyle: LineDashStyle? = null
+ ) {
+ val points = CurveUtils.smoothPath(waypoints, segmentsPerSection)
+ polyline(points, color, width, dashStyle)
+ }
+
+ /**
+ * Draw a circle in a plane.
+ *
+ * @param center Center of the circle
+ * @param radius Radius of the circle
+ * @param normal Normal vector of the plane (determines orientation)
+ * @param color Line color
+ * @param segments Number of segments
+ */
+ fun circleLine(
+ center: Vec3d,
+ radius: Double,
+ normal: Vec3d = Vec3d(0.0, 1.0, 0.0),
+ color: Color,
+ segments: Int = 32,
+ width: Float,
+ dashStyle: LineDashStyle? = null
+ ) {
+ // Create basis vectors perpendicular to normal
+ val up =
+ if (kotlin.math.abs(normal.y) < 0.99) Vec3d(0.0, 1.0, 0.0)
+ else Vec3d(1.0, 0.0, 0.0)
+ val u = normal.crossProduct(up).normalize()
+ val v = u.crossProduct(normal).normalize()
+
+ val points =
+ (0..segments).map { i ->
+ val angle = 2.0 * Math.PI * i / segments
+ val x = kotlin.math.cos(angle) * radius
+ val y = kotlin.math.sin(angle) * radius
+ center.add(u.multiply(x)).add(v.multiply(y))
+ }
+
+ polyline(points, color, width, dashStyle)
+ }
+
+ /**
+ * Draw billboard text at a world position.
+ * The text will face the camera by default, or use a custom rotation.
+ *
+ * @param text Text to render
+ * @param pos World position for the text
+ * @param size Size in world units
+ * @param font Font atlas to use (null = default font)
+ * @param style Text style with color and effects (shadow, glow, outline)
+ * @param centered Center text horizontally
+ * @param rotation Custom rotation as Euler angles in degrees (x=pitch, y=yaw, z=roll), null = billboard towards camera
+ */
+ fun worldText(
+ text: String,
+ pos: Vec3d,
+ size: Float = 0.5f,
+ font: SDFFontAtlas? = null,
+ style: SDFStyle = SDFStyle(),
+ centered: Boolean = true,
+ rotation: Vec3d? = null
+ ) {
+ val atlas = font ?: FontHandler.getDefaultFont()
+ fontAtlas = atlas
+
+ // Camera-relative anchor position
+ val anchorX = (pos.x - cameraPos.x).toFloat()
+ val anchorY = (pos.y - cameraPos.y).toFloat()
+ val anchorZ = (pos.z - cameraPos.z).toFloat()
+
+ // Calculate text width for centering (using normalized width which works directly with glyph advances)
+ val textWidth = if (centered) atlas.getStringWidthNormalized(text, 1f) else 0f
+ val startX = -textWidth / 2f
+
+ // For fixed rotation, we need to build a rotation matrix to pre-transform offsets
+ val rotationMatrix: Matrix4f? = if (rotation != null) {
+ Matrix4f()
+ .rotateY(Math.toRadians(rotation.y).toFloat())
+ .rotateX(Math.toRadians(rotation.x).toFloat())
+ .rotateZ(Math.toRadians(rotation.z).toFloat())
+ } else null
+
+ // Render layers in order: shadow -> glow -> outline -> main text
+ // Alpha encodes layer type for shader: <50 = shadow, 50-99 = glow, 100-199 = outline, >=200 = main
+
+ // Shadow layer (alpha < 50 signals shadow)
+ if (style.shadow != null) {
+ val shadowColor = style.shadow.color
+ val offsetX = style.shadow.offsetX
+ val offsetY = style.shadow.offsetY
+ buildTextQuads(atlas, text, startX + offsetX, offsetY,
+ shadowColor.red, shadowColor.green, shadowColor.blue, 25,
+ anchorX, anchorY, anchorZ, size, rotationMatrix, style)
+ }
+
+ // Glow layer (alpha 50-99 signals glow)
+ if (style.glow != null) {
+ val glowColor = style.glow.color
+ buildTextQuads(atlas, text, startX, 0f,
+ glowColor.red, glowColor.green, glowColor.blue, 75,
+ anchorX, anchorY, anchorZ, size, rotationMatrix, style)
+ }
+
+ // Outline layer (alpha 100-199 signals outline)
+ if (style.outline != null) {
+ val outlineColor = style.outline.color
+ buildTextQuads(atlas, text, startX, 0f,
+ outlineColor.red, outlineColor.green, outlineColor.blue, 150,
+ anchorX, anchorY, anchorZ, size, rotationMatrix, style)
+ }
+
+ // Main text layer (alpha >= 200 signals main text)
+ val mainColor = style.color
+ buildTextQuads(atlas, text, startX, 0f,
+ mainColor.red, mainColor.green, mainColor.blue, 255,
+ anchorX, anchorY, anchorZ, size, rotationMatrix, style)
+ }
+
+ // ============================================================================
+ // Screen-Space Rendering Methods (Normalized Coordinates)
+ // ============================================================================
+ // All coordinates use normalized 0-1 range:
+ // - (0, 0) = bottom-left corner
+ // - (1, 1) = top-right corner
+ // - Sizes are also normalized (e.g., 0.1 = 10% of screen dimension)
+
+ /** Get screen width in pixels (uses MC's scaled width). */
+ private val screenWidth: Float
+ get() = mc.window?.scaledWidth?.toFloat() ?: 1920f
+
+ /** Get screen height in pixels (uses MC's scaled height). */
+ private val screenHeight: Float
+ get() = mc.window?.scaledHeight?.toFloat() ?: 1080f
+
+ /** Convert normalized X coordinate (0-1) to pixel coordinate. */
+ private fun toPixelX(normalizedX: Float): Float = normalizedX * screenWidth
+
+ /** Convert normalized Y coordinate (0-1) to pixel coordinate. */
+ private fun toPixelY(normalizedY: Float): Float = normalizedY * screenHeight
+
+ /**
+ * Convert normalized size to pixel size.
+ * Uses height-only scaling to maintain consistent visual size regardless of aspect ratio.
+ * This matches how world-space elements behave when projected to screen.
+ */
+ private fun toPixelSize(normalizedSize: Float): Float =
+ normalizedSize * screenHeight
+
+ /**
+ * Draw a filled quad on screen with gradient colors.
+ * All coordinates use normalized 0-1 range.
+ *
+ * @param x1, y1 First corner position (0-1) and color
+ * @param x2, y2 Second corner position (0-1) and color
+ * @param x3, y3 Third corner position (0-1) and color
+ * @param x4, y4 Fourth corner position (0-1) and color
+ */
+ fun screenQuadGradient(
+ x1: Float, y1: Float, c1: Color,
+ x2: Float, y2: Float, c2: Color,
+ x3: Float, y3: Float, c3: Color,
+ x4: Float, y4: Float, c4: Color
+ ) {
+ val layer = nextLayer()
+ collector.addScreenFaceVertex(toPixelX(x1), toPixelY(y1), c1, layer)
+ collector.addScreenFaceVertex(toPixelX(x2), toPixelY(y2), c2, layer)
+ collector.addScreenFaceVertex(toPixelX(x3), toPixelY(y3), c3, layer)
+ collector.addScreenFaceVertex(toPixelX(x4), toPixelY(y4), c4, layer)
+ }
+
+ /**
+ * Draw a filled quad on screen with a single color.
+ * All coordinates use normalized 0-1 range.
+ */
+ fun screenQuad(
+ x1: Float, y1: Float,
+ x2: Float, y2: Float,
+ x3: Float, y3: Float,
+ x4: Float, y4: Float,
+ color: Color
+ ) = screenQuadGradient(x1, y1, color, x2, y2, color, x3, y3, color, x4, y4, color)
+
+ /**
+ * Draw a filled rectangle on screen.
+ * All values use normalized 0-1 range.
+ *
+ * @param x Left edge (0-1, where 0 = left, 1 = right)
+ * @param y Bottom edge (0-1, where 0 = bottom, 1 = top)
+ * @param width Rectangle width (0-1, where 1 = full screen width)
+ * @param height Rectangle height (0-1, where 1 = full screen height)
+ * @param color Fill color
+ */
+ fun screenRect(x: Float, y: Float, width: Float, height: Float, color: Color) {
+ val x2 = x + width
+ val y2 = y + height
+ screenQuad(x, y, x2, y, x2, y2, x, y2, color)
+ }
+
+ /**
+ * Draw a filled rectangle on screen with gradient colors.
+ * All values use normalized 0-1 range.
+ *
+ * @param x Left edge (0-1)
+ * @param y Bottom edge (0-1, where 0 = bottom, 1 = top)
+ * @param width Rectangle width (0-1)
+ * @param height Rectangle height (0-1)
+ * @param topLeft Color at top-left corner
+ * @param topRight Color at top-right corner
+ * @param bottomRight Color at bottom-right corner
+ * @param bottomLeft Color at bottom-left corner
+ */
+ fun screenRectGradient(
+ x: Float, y: Float, width: Float, height: Float,
+ topLeft: Color, topRight: Color, bottomRight: Color, bottomLeft: Color
+ ) {
+ val x2 = x + width
+ val y2 = y + height
+ screenQuadGradient(x, y, topLeft, x2, y, topRight, x2, y2, bottomRight, x, y2, bottomLeft)
+ }
+
+ /**
+ * Draw a line on screen with gradient colors.
+ * All coordinates use normalized 0-1 range.
+ *
+ * @param x1, y1 Start position (0-1)
+ * @param x2, y2 End position (0-1)
+ * @param startColor Color at start
+ * @param endColor Color at end
+ * @param width Line width (normalized, e.g., 0.005 = 0.5% of screen)
+ * @param dashStyle Optional dash style for dashed lines
+ */
+ fun screenLineGradient(
+ x1: Float, y1: Float, startColor: Color,
+ x2: Float, y2: Float, endColor: Color,
+ width: Float,
+ dashStyle: LineDashStyle? = null
+ ) {
+ // Convert to pixels
+ val px1 = toPixelX(x1)
+ val py1 = toPixelY(y1)
+ val px2 = toPixelX(x2)
+ val py2 = toPixelY(y2)
+ val pixelWidth = toPixelSize(width)
+
+ // Calculate line direction in pixel space
+ val dx = px2 - px1
+ val dy = py2 - py1
+
+ // Convert dash style lengths to pixels if present
+ val pixelDashStyle = dashStyle?.let {
+ LineDashStyle(
+ dashLength = toPixelSize(it.dashLength),
+ gapLength = toPixelSize(it.gapLength),
+ offset = it.offset,
+ animated = it.animated,
+ animationSpeed = it.animationSpeed
+ )
+ }
+
+ // Get layer for draw order
+ val layer = nextLayer()
+
+ // 4 vertices for screen-space line quad
+ collector.addScreenEdgeVertex(px1, py1, startColor, dx, dy, pixelWidth, pixelDashStyle, layer)
+ collector.addScreenEdgeVertex(px1, py1, startColor, dx, dy, pixelWidth, pixelDashStyle, layer)
+ collector.addScreenEdgeVertex(px2, py2, endColor, dx, dy, pixelWidth, pixelDashStyle, layer)
+ collector.addScreenEdgeVertex(px2, py2, endColor, dx, dy, pixelWidth, pixelDashStyle, layer)
+ }
+
+ /**
+ * Draw a line on screen with a single color.
+ * All coordinates use normalized 0-1 range.
+ */
+ fun screenLine(
+ x1: Float, y1: Float,
+ x2: Float, y2: Float,
+ color: Color,
+ width: Float,
+ dashStyle: LineDashStyle? = null
+ ) = screenLineGradient(x1, y1, color, x2, y2, color, width, dashStyle)
+
+ // ============================================================================
+ // Image Rendering Methods
+ // ============================================================================
+
+ /**
+ * Draw an image on screen at a specific position.
+ * Uses Lambda's custom image rendering pipeline for direct GPU rendering.
+ *
+ * @param image The ImageEntry from LambdaImageAtlas
+ * @param x X position (0-1, normalized screen coordinates)
+ * @param y Y position (0-1, normalized screen coordinates)
+ * @param width Width (0-1, normalized)
+ * @param height Height (0-1, normalized)
+ * @param tint Tint color (default white = no tint)
+ * @param hasOverlay Whether to render an overlay (e.g., enchantment glint)
+ * @param pixelPerfect If true, use NEAREST filtering for crisp pixel art (default: false)
+ */
+ fun screenImage(
+ image: LambdaImageAtlas.ImageEntry,
+ x: Float, y: Float,
+ width: Float, height: Float,
+ tint: Color = Color.WHITE,
+ hasOverlay: Boolean = false,
+ pixelPerfect: Boolean = false
+ ) {
+ val layer = nextLayer()
+ val x0 = toPixelX(x)
+ val y0 = toPixelY(y)
+ val x1 = toPixelX(x + width)
+ val y1 = toPixelY(y + height)
+
+ val overlayFlag = if (hasOverlay) 1f else 0f
+ val u0 = image.u0
+ val v0 = image.v0
+ val u1 = image.u1
+ val v1 = image.v1
+
+ // Calculate animation time for glint effect
+ // Use Util.getMeasuringTimeMs() for consistent timing matching Minecraft's system
+ val glintTime = if (hasOverlay) {
+ (net.minecraft.util.Util.getMeasuringTimeMs() / 1000.0f) % 1000f // Seconds, 0-1000 loop
+ } else 0f
+
+ // Calculate aspect ratio for square glint tiling
+ // overlayV carries width/height ratio so shader can correct UVs
+ val aspectRatio = if (hasOverlay && height != 0f) width / height else 1f
+
+ // Build quad: bottom-left, bottom-right, top-right, top-left (CCW for Y-up)
+ // overlayU = animation time, overlayV = aspect ratio
+ val vertices = listOf(
+ RegionVertexCollector.ScreenImageVertex(
+ x0, y0, u0, v1, tint.red, tint.green, tint.blue, tint.alpha,
+ glintTime, aspectRatio, overlayFlag, 0f, layer
+ ),
+ RegionVertexCollector.ScreenImageVertex(
+ x1, y0, u1, v1, tint.red, tint.green, tint.blue, tint.alpha,
+ glintTime, aspectRatio, overlayFlag, 0f, layer
+ ),
+ RegionVertexCollector.ScreenImageVertex(
+ x1, y1, u1, v0, tint.red, tint.green, tint.blue, tint.alpha,
+ glintTime, aspectRatio, overlayFlag, 0f, layer
+ ),
+ RegionVertexCollector.ScreenImageVertex(
+ x0, y1, u0, v0, tint.red, tint.green, tint.blue, tint.alpha,
+ glintTime, aspectRatio, overlayFlag, 0f, layer
+ )
+ )
+ collector.addScreenImageVertices(image.textureView, vertices, pixelPerfect)
+ }
+
+ // ============================================================================
+ // Model Rendering Methods (World Space)
+ // ============================================================================
+
+ /**
+ * Render a 3D model in world space.
+ *
+ * @param model The BlockModelPart to render (replaces BakedModel in 1.21.11+)
+ * @param pos World position to render at. If centered is true, this is the center of the model. If false, it's the 0,0,0 corner.
+ * @param scale XML scale
+ * @param rotation Rotation quaternion (around the center if centered=true, else around 0,0,0)
+ * @param color Tint color
+ * @param light Packed light
+ * @param overlay Overlay UV
+ * @param centered If true, shifts model vertices by -0.5 to rotate/scale around the center, then renders at pos.
+ * @param pixelPerfect If true, use NEAREST filtering. If false (default), use LINEAR.
+ * @param smartAA If true, uses shader-based analytic anti-aliasing (Pixel Art AA). Requires pixelPerfect=false (automatically handled).
+ */
+ fun model(
+ model: BlockModelPart,
+ pos: Vec3d,
+ scale: Vec3d = Vec3d(1.0, 1.0, 1.0),
+ rotation: Quaternionf? = null,
+ color: Color = Color.WHITE,
+ light: Int = 0xF000F0,
+ overlay: Int = OverlayTexture.DEFAULT_UV,
+ centered: Boolean = false,
+ pixelPerfect: Boolean = false,
+ smartAA: Boolean = false,
+ shadingAmount: Float = 0.0f
+ ) {
+ val sprite = model.particleSprite() ?: return
+ val atlas = sprite.atlasId
+ // We need to resolve the texture view for the sprite's atlas
+ val textureView = mc.textureManager.getTexture(atlas)?.glTextureView ?: return
+
+ val vertices = ArrayList()
+
+ val scaleVec = Vector3f(scale.x.toFloat(), scale.y.toFloat(), scale.z.toFloat())
+ val posVec = Vector3f(
+ (pos.x - cameraPos.x).toFloat(),
+ (pos.y - cameraPos.y).toFloat(),
+ (pos.z - cameraPos.z).toFloat()
+ )
+
+ val vertexPos = Vector3f()
+ val normalVec = Vector3f()
+
+ val tr = color.red
+ val tg = color.green
+ val tb = color.blue
+ val ta = color.alpha
+
+ val olU = (overlay and 0xFFFF).toFloat()
+ val olV = ((overlay shr 16) and 0xFFFF).toFloat()
+
+ // Encode Overlay + AA flags into hasOverlay
+ // 0 = None, 1 = Overlay, 2 = AA, 3 = Overlay + AA
+ var overlayFlag = if (overlay != OverlayTexture.DEFAULT_UV) 1.0f else 0.0f
+ if (smartAA) {
+ overlayFlag += 2.0f
+ }
+
+ val random = Random.create()
+ val quads = mutableListOf()
+
+ // Collect quads for all directions and null direction
+ for (direction in net.minecraft.util.math.Direction.entries) {
+ random.setSeed(42L)
+ quads.addAll(model.getQuads(direction))
+ }
+ random.setSeed(42L)
+ quads.addAll(model.getQuads(null))
+
+ for (quad in quads) {
+ // Use face normal for all vertices since BakedQuad doesn't have per-vertex normals easily accessible
+ val face = quad.face
+ val nx = face.offsetX.toFloat()
+ val ny = face.offsetY.toFloat()
+ val nz = face.offsetZ.toFloat()
+
+ // Iterate 4 vertices
+ for (i in 0 until 4) {
+ val posVecSrc = quad.getPosition(i)
+
+ // Transform Position
+ vertexPos.set(posVecSrc.x(), posVecSrc.y(), posVecSrc.z())
+
+ if (centered) {
+ // Shift to center (assuming 0..1 model block)
+ vertexPos.sub(0.5f, 0.5f, 0.5f)
+ }
+
+ vertexPos.mul(scaleVec)
+ rotation?.transform(vertexPos)
+ vertexPos.add(posVec)
+
+ // Transform Normal
+ normalVec.set(nx, ny, nz)
+ rotation?.transform(normalVec)
+
+ // Extract UV - Vector2f.toLong packs X (U) in high 32 bits, Y (V) in low 32 bits
+ val packedUV = quad.getTexcoords(i)
+ val u = Float.fromBits((packedUV ushr 32).toInt())
+ val v = Float.fromBits((packedUV and 0xFFFFFFFFL).toInt())
+
+ // Edge Data: Map vertex index to a quad corner (0,0 to 1,1)
+ // Standard quad winding: 0:0,0 | 1:0,1 | 2:1,1 | 3:1,0 (approx)
+ // Reverted to simple bounds without dilation
+ val edgeX = if (i == 1 || i == 2) 1.0f else 0.0f
+ val edgeY = if (i == 2 || i == 3) 1.0f else 0.0f
+
+ vertices.add(RegionVertexCollector.ModelVertex(
+ vertexPos.x, vertexPos.y, vertexPos.z,
+ u, v,
+ tr, tg, tb, ta,
+ olU, olV, overlayFlag, shadingAmount,
+ light,
+ DEFAULT_LIGHT_DIR.x, DEFAULT_LIGHT_DIR.y, DEFAULT_LIGHT_DIR.z,
+ DEFAULT_LIGHT1_DIR.x, DEFAULT_LIGHT1_DIR.y, DEFAULT_LIGHT1_DIR.z,
+ normalVec.x, normalVec.y, normalVec.z,
+ edgeX, edgeY
+ ))
+ }
+ }
+
+ if (vertices.isNotEmpty()) {
+ // Use Nearest filter only if requested AND Smart AA is NOT used (Smart AA handles sharpness in shader via Linear)
+ val useNearest = pixelPerfect && !smartAA
+ collector.addModelVertices(textureView, vertices, useNearest)
+ }
+ } // Default filter?
+
+ fun worldGuiItem(
+ stack: ItemStack,
+ pos: Vec3d,
+ scale: Float = 0.5f,
+ rotation: Vec3d? = null,
+ centered: Boolean = true,
+ flat: Boolean = true,
+ lighting: ItemLighting = ItemLighting.VANILLA,
+ overlay: ItemOverlay? = null
+ ) {
+ if (stack.isEmpty) return
+
+ val renderState = ItemRenderState()
+ mc.itemModelManager.updateForNonLivingEntity(renderState, stack, ItemDisplayContext.GUI, mc.player ?: return)
+
+ val rot = rotation?.let { eulerToQuaternion(it) }
+ renderItemState(renderState, pos, scale, rot, centered, isScreen = false, flat = flat, lighting = lighting, overlay = overlay)
+ }
+
+ fun screenGuiItem(
+ stack: ItemStack,
+ x: Float, y: Float,
+ size: Float = 0.05f,
+ rotation: Vec3d? = null,
+ centered: Boolean = true,
+ lighting: ItemLighting = ItemLighting.VANILLA,
+ overlay: ItemOverlay? = null
+ ) {
+ if (stack.isEmpty) return
+
+ val renderState = ItemRenderState()
+ mc.itemModelManager.updateForNonLivingEntity(renderState, stack, ItemDisplayContext.GUI, mc.player ?: return)
+
+ // Convert screen pos to camera-relative "pseudo-world" for the generic renderer
+ val pixelX = toPixelX(x)
+ val pixelY = toPixelY(y)
+ val pixelSize = toPixelSize(size)
+
+ val rot = rotation?.let { eulerToQuaternion(it) }
+ // Screen items are always flat (viewed straight-on)
+ renderItemState(renderState, Vec3d(pixelX.toDouble(), pixelY.toDouble(), nextLayer().toDouble()), pixelSize, rot, centered, isScreen = true, flat = true, lighting = lighting, overlay = overlay)
+ }
+
+ private fun renderItemState(
+ state: ItemRenderState,
+ pos: Vec3d,
+ scale: Float,
+ rotation: Quaternionf?,
+ centered: Boolean,
+ isScreen: Boolean,
+ flat: Boolean = false,
+ lighting: ItemLighting = ItemLighting.VANILLA,
+ overlay: ItemOverlay? = null
+ ) {
+ val posVec = if (isScreen) {
+ Vector3f(pos.x.toFloat(), pos.y.toFloat(), pos.z.toFloat())
+ } else {
+ Vector3f((pos.x - cameraPos.x).toFloat(), (pos.y - cameraPos.y).toFloat(), (pos.z - cameraPos.z).toFloat())
+ }
+
+ val lightDirs = Pair(Vector3f(lighting.light0), Vector3f(lighting.light1))
+
+ val queue = CapturingQueue(
+ posVec, Vector3f(scale), rotation, centered, flat, lighting, lightDirs, state.isSideLit
+ ) { vertices, textureView ->
+ if (isScreen) {
+ collector.addScreenModelVertices(textureView, vertices, true)
+ } else {
+ collector.addModelVertices(textureView, vertices, true)
+ }
+ }
+
+ val matrixStack = MatrixStack()
+
+ for (i in 0 until state.layerCount) {
+ val layer = state.layers[i]
+
+ // Refined Glint Logic:
+ // - If overlay is ItemOverlay.DISABLED, force glint OFF.
+ // - If overlay is non-null (any other overlay), force glint ON.
+ // - If overlay is null, use the item's own enchantment state.
+ queue.currentGlint = when (overlay) {
+ ItemOverlay.DISABLED -> false
+ null -> layer.glint != ItemRenderState.Glint.NONE
+ else -> true
+ }
+
+ matrixStack.push()
+ layer.transform.apply(state.displayContext.isLeftHand, matrixStack.peek())
+
+ val specialModel = layer.specialModelType
+
+ if (specialModel != null) {
+ specialModel.render(layer.data, state.displayContext, matrixStack, queue, 15728880, 0, queue.currentGlint, 0)
+ } else {
+ val renderLayer = layer.renderLayer
+ if (renderLayer != null) {
+ // We pass STANDARD if currentGlint is true, otherwise NONE to ensure our capture can correctly toggle it.
+ val captureGlint = if (queue.currentGlint) ItemRenderState.Glint.STANDARD else ItemRenderState.Glint.NONE
+ queue.submitItem(matrixStack, state.displayContext, 15728880, 0, 0, layer.tints, layer.quads, renderLayer, captureGlint)
+ }
+ }
+
+ matrixStack.pop()
+ }
+ }
+
+ private class CapturingConsumer(
+ private val vertices: MutableList,
+ private val posTransform: (Vector3f) -> Unit,
+ private val normalTransform: (Vector3f) -> Unit,
+ private val flat: Boolean,
+ private val rotation: Quaternionf?,
+ private val glint: Boolean,
+ private val lightDirs: Pair,
+ private val shadingAmount: Float,
+ private val baseLight: Int,
+ private val baseOverlay: Int
+ ) : VertexConsumer {
+ private var x = 0f; private var y = 0f; private var z = 0f
+ private var color = -1
+ private var u = 0f; private var v = 0f
+ private var currentOverlay = 0
+ private var currentLight = 0
+ private var nx = 0f; private var ny = 0f; private var nz = 0f
+ private val quadBuffer = ArrayList(4)
+
+ override fun vertex(float1: Float, float2: Float, float3: Float): VertexConsumer {
+ this.x = float1; this.y = float2; this.z = float3
+ return this
+ }
+
+ override fun color(argb: Int): VertexConsumer {
+ this.color = argb
+ return this
+ }
+
+ override fun color(r: Int, g: Int, b: Int, a: Int): VertexConsumer {
+ this.color = (a shl 24) or (r shl 16) or (g shl 8) or b
+ return this
+ }
+
+ override fun texture(float1: Float, float2: Float): VertexConsumer {
+ this.u = float1; this.v = float2
+ return this
+ }
+
+ override fun overlay(int1: Int, int2: Int): VertexConsumer {
+ this.currentOverlay = (int2 shl 16) or int1
+ return this
+ }
+
+ override fun light(int1: Int, int2: Int): VertexConsumer {
+ this.currentLight = (int2 shl 16) or int1
+ return this
+ }
+
+ override fun lineWidth(width: Float): VertexConsumer = this
+
+ override fun normal(float1: Float, float2: Float, float3: Float): VertexConsumer {
+ this.nx = float1; this.ny = float2; this.nz = float3
+ commitVertex()
+ return this
+ }
+
+ override fun vertex(
+ x: Float, y: Float, z: Float,
+ color: Int, u: Float, v: Float,
+ overlay: Int, light: Int,
+ normalX: Float, normalY: Float, normalZ: Float
+ ) {
+ this.x = x; this.y = y; this.z = z
+ this.color = color
+ this.u = u; this.v = v
+ this.currentOverlay = overlay
+ this.currentLight = light
+ this.nx = normalX; this.ny = normalY; this.nz = normalZ
+ commitVertex()
+ }
+
+ private fun commitVertex() {
+ // 1. Coordinates are already transformed by model parts into "Item Space"
+ val p = Vector3f(this.x, this.y, this.z)
+
+ // 2. Apply Flattening in Item Space (before Lambda world/screen transforms)
+ if (flat) p.z = 0f
+
+ // 3. Apply Lambda positioning (ESP world pos or GUI position)
+ posTransform(p)
+
+ // 4. Normal handling
+ val n = Vector3f(this.nx, this.ny, this.nz)
+ normalTransform(n)
+
+ val vIdx = quadBuffer.size
+ val qu = if (vIdx == 1 || vIdx == 2) 1f else 0f
+ val qv = if (vIdx == 2 || vIdx == 3) 1f else 0f
+
+ val ov = if (currentOverlay != 0) currentOverlay else baseOverlay
+ val lgt = if (currentLight != 0) currentLight else baseLight
+
+ quadBuffer.add(RegionVertexCollector.ModelVertex(
+ p.x, p.y, p.z,
+ u, v,
+ (color shr 16) and 0xFF, (color shr 8) and 0xFF, color and 0xFF, (color ushr 24) and 0xFF,
+ (ov and 0xFFFF).toFloat(), (ov ushr 16).toFloat(),
+ if (glint) 4.0f else 0.0f,
+ shadingAmount,
+ lgt,
+ lightDirs.first.x, lightDirs.first.y, lightDirs.first.z,
+ lightDirs.second.x, lightDirs.second.y, lightDirs.second.z,
+ n.x, n.y, n.z,
+ qu, qv
+ ))
+
+ if (quadBuffer.size == 4) {
+ if (flat) {
+ // Cull backfaces: The normal is already in Item Space.
+ // Apply Lambda rotation to see if it faces the screen.
+ val testN = Vector3f(this.nx, this.ny, this.nz)
+ rotation?.transform(testN)
+ if (testN.z < 0f) {
+ quadBuffer.clear()
+ return
+ }
+ }
+ vertices.addAll(quadBuffer)
+ quadBuffer.clear()
+ }
+ }
+ }
+
+ private inner class CapturingQueue(
+ private val pos: Vector3f,
+ private val scale: Vector3f,
+ private val rotation: Quaternionf?,
+ private val centered: Boolean,
+ private val flat: Boolean,
+ private val lighting: ItemLighting,
+ private val lightDirs: Pair,
+ private val isSideLit: Boolean,
+ private val onSubmission: (List, com.mojang.blaze3d.textures.GpuTextureView) -> Unit
+ ) : OrderedRenderCommandQueue {
+ var currentGlint: Boolean = false
+
+ private fun posTransform(v: Vector3f) {
+ if (!centered) v.add(0.5f, 0.5f, 0.5f)
+ if (flat) v.z = 0f
+ v.mul(scale)
+ rotation?.transform(v)
+ v.add(pos)
+ }
+
+ private fun normalTransform(v: Vector3f) {
+ rotation?.transform(v)
+ v.normalize()
+ }
+
+ override fun submitItem(
+ matrices: MatrixStack,
+ displayContext: ItemDisplayContext,
+ light: Int,
+ overlay: Int,
+ outlineColors: Int,
+ tintLayers: IntArray,
+ quads: List,
+ renderLayer: RenderLayer,
+ glintType: ItemRenderState.Glint
+ ) {
+ val sprite = quads.firstOrNull()?.sprite ?: return
+ val textureView = mc.textureManager.getTexture(sprite.atlasId)?.glTextureView ?: return
+
+ val vertices = ArrayList()
+ val shadingAmount = if (lighting.respectsUseLight && !isSideLit) 0.0f else 1.0f
+
+ // Transform lights by GUI layer matrix to match normal transformation
+ val l0 = Vector3f(lightDirs.first).mulDirection(matrices.peek().positionMatrix)
+ val l1 = Vector3f(lightDirs.second).mulDirection(matrices.peek().positionMatrix)
+ l0.x = -l0.x; l1.x = -l1.x; l0.normalize(); l1.normalize()
+
+ for (quad in quads) {
+ val nx = quad.face.offsetX.toFloat()
+ val ny = quad.face.offsetY.toFloat()
+ val nz = quad.face.offsetZ.toFloat()
+
+ if (flat) {
+ val testN = Vector3f(nx, ny, nz).mulDirection(matrices.peek().positionMatrix)
+ rotation?.transform(testN)
+ if (testN.z < 0f) continue
+ }
+
+ for (vIdx in 0 until 4) {
+ val posSrc = quad.getPosition(vIdx)
+ val vp = matrices.peek().positionMatrix.transformPosition(posSrc.x(), posSrc.y(), posSrc.z(), Vector3f())
+ posTransform(vp)
+
+ val vn = matrices.peek().transformNormal(nx, ny, nz, Vector3f())
+ normalTransform(vn)
+
+ val packedUV = quad.getTexcoords(vIdx)
+ val u = java.lang.Float.intBitsToFloat((packedUV ushr 32).toInt())
+ val v = java.lang.Float.intBitsToFloat((packedUV and 0xFFFFFFFFL).toInt())
+
+ val tint = if (quad.hasTint() && quad.tintIndex() < tintLayers.size) tintLayers[quad.tintIndex()] else -1
+ val r = if (tint != -1) (tint shr 16 and 0xFF) else 255
+ val g = if (tint != -1) (tint shr 8 and 0xFF) else 255
+ val b = if (tint != -1) (tint and 0xFF) else 255
+
+ vertices.add(RegionVertexCollector.ModelVertex(
+ vp.x, vp.y, vp.z,
+ u, v,
+ r, g, b, 255,
+ (overlay and 0xFFFF).toFloat(), (overlay ushr 16).toFloat(),
+ if (glintType != ItemRenderState.Glint.NONE) 4.0f else 0.0f,
+ shadingAmount,
+ light,
+ l0.x, l0.y, l0.z,
+ l1.x, l1.y, l1.z,
+ vn.x, vn.y, vn.z,
+ if (vIdx == 1 || vIdx == 2) 1f else 0f,
+ if (vIdx == 2 || vIdx == 3) 1f else 0f
+ ))
+ }
+ }
+ onSubmission(vertices, textureView)
+ }
+
+ override fun submitModel(
+ model: net.minecraft.client.model.Model,
+ state: S,
+ matrices: MatrixStack,
+ renderLayer: RenderLayer,
+ light: Int,
+ overlay: Int,
+ tintedColor: Int,
+ sprite: Sprite?,
+ outlineColor: Int,
+ crumblingOverlay: ModelCommandRenderer.CrumblingOverlayCommand?
+ ) {
+ val textureView = if (sprite != null) {
+ mc.textureManager.getTexture(sprite.atlasId)?.glTextureView
+ } else {
+ mc.textureManager.getTexture(Identifier.ofVanilla("textures/atlas/blocks.png"))?.glTextureView
+ } ?: return
+
+ val vertices = ArrayList()
+ val shadingAmount = if (lighting.respectsUseLight && !isSideLit) 0.0f else 1.0f
+ val consumer = CapturingConsumer(
+ vertices, ::posTransform, ::normalTransform,
+ flat, rotation, currentGlint, lightDirs, shadingAmount, light, overlay
+ )
+ val wrappedConsumer = sprite?.getTextureSpecificVertexConsumer(consumer) ?: consumer
+ model.setAngles(state)
+ model.render(matrices, wrappedConsumer, light, overlay, tintedColor)
+ onSubmission(vertices, textureView)
+ }
+
+ override fun submitModelPart(
+ part: net.minecraft.client.model.ModelPart,
+ matrices: MatrixStack,
+ renderLayer: RenderLayer,
+ light: Int,
+ overlay: Int,
+ sprite: Sprite?,
+ sheeted: Boolean,
+ hasGlint: Boolean,
+ tintedColor: Int,
+ crumblingOverlay: ModelCommandRenderer.CrumblingOverlayCommand?,
+ i: Int
+ ) {
+ val textureView = if (sprite != null) {
+ mc.textureManager.getTexture(sprite.atlasId)?.glTextureView
+ } else {
+ mc.textureManager.getTexture(Identifier.ofVanilla("textures/atlas/blocks.png"))?.glTextureView
+ } ?: return
+
+ val vertices = ArrayList()
+ val shadingAmount = if (lighting.respectsUseLight && !isSideLit) 0.0f else 1.0f
+ val consumer = CapturingConsumer(
+ vertices, ::posTransform, ::normalTransform,
+ flat, rotation, hasGlint, lightDirs, shadingAmount, light, overlay
+ )
+ val wrappedConsumer = sprite?.getTextureSpecificVertexConsumer(consumer) ?: consumer
+ part.render(matrices, wrappedConsumer, light, overlay, tintedColor)
+ onSubmission(vertices, textureView)
+ }
+
+ override fun getBatchingQueue(order: Int): net.minecraft.client.render.command.RenderCommandQueue = this
+ override fun submitShadowPieces(matrices: MatrixStack, radius: Float, pieces: List) {}
+ override fun submitLabel(matrices: MatrixStack, pos: Vec3d?, y: Int, label: Text, ns: Boolean, l: Int, dist: Double, cam: CameraRenderState) {}
+ override fun submitText(matrices: MatrixStack, x: Float, y: Float, text: OrderedText, ds: Boolean, lt: TextRenderer.TextLayerType, l: Int, c: Int, bc: Int, oc: Int) {}
+ override fun submitFire(matrices: MatrixStack, state: EntityRenderState, rot: Quaternionf) {}
+ override fun submitLeash(matrices: MatrixStack, data: EntityRenderState.LeashData) {}
+ override fun submitBlock(matrices: MatrixStack, state: BlockState, light: Int, overlay: Int, outlineColor: Int) {
+ val model = mc.blockRenderManager.getModel(state)
+ val textureView = mc.textureManager.getTexture(Identifier.ofVanilla("textures/atlas/blocks.png"))?.glTextureView ?: return
+
+ val vertices = ArrayList()
+ val shadingAmount = if (lighting.respectsUseLight && !isSideLit) 0.0f else 1.0f
+ val consumer = CapturingConsumer(
+ vertices, ::posTransform, ::normalTransform,
+ flat, rotation, currentGlint, lightDirs, shadingAmount, light, overlay
+ )
+
+ val random = Random.create()
+ val parts = model.getParts(random)
+ for (part in parts) {
+ for (direction in net.minecraft.util.math.Direction.entries) {
+ for (quad in part.getQuads(direction)) {
+ consumer.quad(matrices.peek(), quad, 1f, 1f, 1f, 1f, light, overlay)
+ }
+ }
+ for (quad in part.getQuads(null)) {
+ consumer.quad(matrices.peek(), quad, 1f, 1f, 1f, 1f, light, overlay)
+ }
+ }
+
+ onSubmission(vertices, textureView)
+ }
+
+ override fun submitMovingBlock(matrices: MatrixStack, state: net.minecraft.client.render.block.MovingBlockRenderState) {
+ }
+ override fun submitBlockStateModel(matrices: MatrixStack, layer: RenderLayer, model: BlockStateModel, r: Float, g: Float, b: Float, l: Int, o: Int, oc: Int) {}
+ override fun submitCustom(matrices: MatrixStack, layer: RenderLayer, renderer: OrderedRenderCommandQueue.Custom) {}
+ override fun submitCustom(renderer: OrderedRenderCommandQueue.LayeredCustom) {}
+ }
+
+ /**
+ * Draw a billboard image at a world position.
+ * The image will face the camera by default, or use a custom rotation.
+ *
+ * @param image The ImageEntry from LambdaImageAtlas
+ * @param pos World position for the image
+ * @param size Size in world units
+ * @param tint Tint color (default white = no tint)
+ * @param hasOverlay Whether to render an overlay (e.g., enchantment glint)
+ * @param aspectRatio Width/height ratio (auto-calculated from image if not specified)
+ * @param rotation Custom rotation as Euler angles in degrees (x=pitch, y=yaw, z=roll), null = billboard towards camera
+ * @param pixelPerfect If true, use NEAREST filtering for crisp pixel art (default: false)
+ */
+ fun worldImage(
+ image: LambdaImageAtlas.ImageEntry,
+ pos: Vec3d,
+ size: Float = 0.5f,
+ tint: Color = Color.WHITE,
+ hasOverlay: Boolean = false,
+ aspectRatio: Float? = null,
+ rotation: Vec3d? = null,
+ pixelPerfect: Boolean = false
+ ) {
+ val ratio = aspectRatio ?: image.aspectRatio
+ val u0 = image.u0
+ val v0 = image.v0
+ val u1 = image.u1
+ val v1 = image.v1
+
+ // Camera-relative anchor position
+ val anchorX = (pos.x - cameraPos.x).toFloat()
+ val anchorY = (pos.y - cameraPos.y).toFloat()
+ val anchorZ = (pos.z - cameraPos.z).toFloat()
+
+ // Calculate quad corners (centered on anchor)
+ val halfWidth = size * ratio / 2f
+ val halfHeight = size / 2f
+
+ val overlayFlag = if (hasOverlay) 1f else 0f
+ val billboardFlag = if (rotation == null) 0f else 1f
+
+ // Quad relative glint UVs (0-1 range)
+ val gx0 = 0f
+ val gx1 = 1f
+ val gy0 = 0f // 0 at y0 (bottom)
+ val gy1 = 1f // 1 at y1 (top)
+
+ // Quad offsets (local space, scaled in shader)
+ val x0 = -halfWidth / size
+ val x1 = halfWidth / size
+ val y0 = -halfHeight / size
+ val y1 = halfHeight / size
+
+ val vertices = if (rotation == null) {
+ // Billboard mode: pass local offsets directly, shader handles billboard
+ listOf(
+ RegionVertexCollector.WorldImageVertex(
+ x0, y0, u0, v1, tint.red, tint.green, tint.blue, tint.alpha,
+ anchorX, anchorY, anchorZ, size, billboardFlag,
+ gx0, gy0, overlayFlag, 0f
+ ),
+ RegionVertexCollector.WorldImageVertex(
+ x1, y0, u1, v1, tint.red, tint.green, tint.blue, tint.alpha,
+ anchorX, anchorY, anchorZ, size, billboardFlag,
+ gx1, gy0, overlayFlag, 0f
+ ),
+ RegionVertexCollector.WorldImageVertex(
+ x1, y1, u1, v0, tint.red, tint.green, tint.blue, tint.alpha,
+ anchorX, anchorY, anchorZ, size, billboardFlag,
+ gx1, gy1, overlayFlag, 0f
+ ),
+ RegionVertexCollector.WorldImageVertex(
+ x0, y1, u0, v0, tint.red, tint.green, tint.blue, tint.alpha,
+ anchorX, anchorY, anchorZ, size, billboardFlag,
+ gx0, gy1, overlayFlag, 0f
+ )
+ )
+ } else {
+ // Fixed rotation mode: pre-transform offsets with rotation matrix
+ val rotationMatrix = Matrix4f()
+ .rotateY(Math.toRadians(rotation.y).toFloat())
+ .rotateX(Math.toRadians(rotation.x).toFloat())
+ .rotateZ(Math.toRadians(rotation.z).toFloat())
+
+ val p0 = transformPoint(rotationMatrix, x0, y0, 0f)
+ val p1 = transformPoint(rotationMatrix, x1, y0, 0f)
+ val p2 = transformPoint(rotationMatrix, x1, y1, 0f)
+ val p3 = transformPoint(rotationMatrix, x0, y1, 0f)
+
+ listOf(
+ RegionVertexCollector.WorldImageVertex(
+ p0.x, p0.y, u0, v1, tint.red, tint.green, tint.blue, tint.alpha,
+ anchorX, anchorY, anchorZ, size, billboardFlag,
+ gx0, gy0, overlayFlag, 0f
+ ),
+ RegionVertexCollector.WorldImageVertex(
+ p1.x, p1.y, u1, v1, tint.red, tint.green, tint.blue, tint.alpha,
+ anchorX, anchorY, anchorZ, size, billboardFlag,
+ gx1, gy0, overlayFlag, 0f
+ ),
+ RegionVertexCollector.WorldImageVertex(
+ p2.x, p2.y, u1, v0, tint.red, tint.green, tint.blue, tint.alpha,
+ anchorX, anchorY, anchorZ, size, billboardFlag,
+ gx1, gy1, overlayFlag, 0f
+ ),
+ RegionVertexCollector.WorldImageVertex(
+ p3.x, p3.y, u0, v0, tint.red, tint.green, tint.blue, tint.alpha,
+ anchorX, anchorY, anchorZ, size, billboardFlag,
+ gx0, gy1, overlayFlag, 0f
+ )
+ )
+ }
+ collector.addWorldImageVertices(image.textureView, vertices, pixelPerfect)
+ }
+
+ // ============================================================================
+ // Simplified Image API (Identifier-based)
+ // ============================================================================
+
+ /**
+ * Draw a Minecraft texture on screen at a specific position.
+ * The texture is loaded automatically - no UV coordinates needed.
+ *
+ * @param texture Identifier of the texture (e.g., Identifier.ofVanilla("textures/item/diamond.png"))
+ * @param x X position (0-1, normalized screen coordinates)
+ * @param y Y position (0-1, normalized screen coordinates)
+ * @param width Width (0-1, normalized)
+ * @param height Height (0-1, normalized)
+ * @param tint Tint color (default white = no tint)
+ * @param hasOverlay Whether to render an overlay (e.g., enchantment glint)
+ * @param pixelPerfect If true, use NEAREST filtering for crisp pixel art (default: true for MC textures)
+ */
+ fun screenImage(
+ texture: Identifier,
+ x: Float, y: Float,
+ width: Float, height: Float,
+ tint: Color = Color.WHITE,
+ hasOverlay: Boolean = false,
+ pixelPerfect: Boolean = true
+ ) {
+ // Load the texture via LambdaImageAtlas
+ val imageEntry = LambdaImageAtlas.loadMCTexture(texture) ?: return
+ screenImage(imageEntry, x, y, width, height, tint, hasOverlay, pixelPerfect)
+ }
+
+ /**
+ * Draw a Minecraft texture as a billboard at a world position.
+ * The texture is loaded automatically - no UV coordinates needed.
+ *
+ * @param texture Identifier of the texture
+ * @param pos World position for the image
+ * @param size Size in world units
+ * @param tint Tint color (default white = no tint)
+ * @param hasOverlay Whether to render an overlay (e.g., enchantment glint)
+ * @param aspectRatio Width/height ratio (for non-square images)
+ * @param rotation Custom rotation, null = billboard towards camera
+ * @param pixelPerfect If true, use NEAREST filtering for crisp pixel art (default: true for MC textures)
+ */
+ fun worldImage(
+ texture: Identifier,
+ pos: Vec3d,
+ size: Float = 0.5f,
+ tint: Color = Color.WHITE,
+ hasOverlay: Boolean = false,
+ aspectRatio: Float? = null,
+ rotation: Vec3d? = null,
+ pixelPerfect: Boolean = true
+ ) {
+ // Load the texture via LambdaImageAtlas
+ val imageEntry = LambdaImageAtlas.loadMCTexture(texture) ?: return
+ val ratio = aspectRatio ?: imageEntry.aspectRatio
+ worldImage(imageEntry, pos, size, tint, hasOverlay, ratio, rotation, pixelPerfect)
+ }
+
+ /**
+ * Draw text on screen at a specific position.
+ * Position uses normalized 0-1 range, size is normalized.
+ *
+ * @param text Text to render
+ * @param x X position (0-1, where 0 = left, 1 = right)
+ * @param y Y position (0-1, where 0 = bottom, 1 = top)
+ * @param size Text size (normalized, e.g., 0.02 = 2% of screen height)
+ * @param font Font atlas to use (null = default font)
+ * @param style Text style with color and effects
+ * @param centered Center text horizontally at the given position
+ */
+ fun screenText(
+ text: String,
+ x: Float,
+ y: Float,
+ size: Float = 0.02f,
+ font: SDFFontAtlas? = null,
+ style: SDFStyle = SDFStyle(),
+ centered: Boolean = false
+ ) {
+ val atlas = font ?: FontHandler.getDefaultFont()
+ fontAtlas = atlas
+
+ // Convert to pixel coordinates
+ val pixelX = toPixelX(x)
+ val pixelY = toPixelY(y)
+
+ // Convert normalized size to target pixel height
+ val targetPixelHeight = toPixelSize(size)
+
+ // Adjust font size so that text ASCENT (height of capital letters) matches the target pixel height
+ // getAscent(fontSize) = ascent / baseSize * fontSize
+ // We want: ascent / baseSize * adjustedFontSize = targetPixelHeight
+ // So: adjustedFontSize = targetPixelHeight * baseSize / ascent
+ val pixelSize = targetPixelHeight * atlas.baseSize / atlas.ascent
+
+ // Calculate text width for centering (normalized width converted to pixels)
+ val normalizedTextWidth = if (centered) atlas.getStringWidthNormalized(text, size) else 0f
+ val textWidth = normalizedTextWidth * screenWidth
+ val startX = -textWidth / 2f
+
+ // Render layers in order: shadow -> glow -> outline -> main text
+ // Each layer gets its own draw depth so they render in correct order
+ // Alpha encodes layer type for shader
+
+ // Shadow layer
+ if (style.shadow != null) {
+ val shadowColor = style.shadow.color
+ val offsetX = style.shadow.offsetX * pixelSize
+ // Negate offsetY for Y-up coordinate system (shadow should appear below text)
+ val offsetY = -style.shadow.offsetY * pixelSize
+ val layer = nextLayer()
+ buildScreenTextQuads(atlas, text, startX + offsetX, offsetY,
+ shadowColor.red, shadowColor.green, shadowColor.blue, 25,
+ pixelX, pixelY, pixelSize, style, layer)
+ }
+
+ // Glow layer
+ if (style.glow != null) {
+ val glowColor = style.glow.color
+ val layer = nextLayer()
+ buildScreenTextQuads(atlas, text, startX, 0f,
+ glowColor.red, glowColor.green, glowColor.blue, 75,
+ pixelX, pixelY, pixelSize, style, layer)
+ }
+
+ // Outline layer
+ if (style.outline != null) {
+ val outlineColor = style.outline.color
+ val layer = nextLayer()
+ buildScreenTextQuads(atlas, text, startX, 0f,
+ outlineColor.red, outlineColor.green, outlineColor.blue, 150,
+ pixelX, pixelY, pixelSize, style, layer)
+ }
+
+ // Main text layer
+ val mainColor = style.color
+ val mainLayer = nextLayer()
+ buildScreenTextQuads(atlas, text, startX, 0f,
+ mainColor.red, mainColor.green, mainColor.blue, 255,
+ pixelX, pixelY, pixelSize, style, mainLayer)
+ }
+
+ /**
+ * Build screen-space text quad vertices for a layer.
+ * Internal method - uses pixel coordinates. Adds vertices directly to collector.
+ */
+ private fun buildScreenTextQuads(
+ atlas: SDFFontAtlas,
+ text: String,
+ startX: Float, // Offset in SCALED pixels (for centering)
+ startY: Float, // Offset in SCALED pixels
+ r: Int, g: Int, b: Int, a: Int,
+ anchorX: Float, anchorY: Float,
+ pixelSize: Float, // Final text size in pixels
+ style: SDFStyle,
+ layer: Float // Layer depth for draw order
+ ) {
+ // Extract SDF style params from SDFStyle object
+ val outlineWidth = style.outline?.width ?: 0f
+ val glowRadius = style.glow?.radius ?: 0f
+ val shadowSoftness = style.shadow?.softness ?: 0f
+ val threshold = 0.5f // Default SDF threshold
+
+ // Glyph metrics (advance, bearingX, bearingY) are ALREADY normalized by baseSize in SDFFontAtlas
+ // Glyph width/height are in PIXELS and need to be normalized
+ var penX = 0f // Pen position in normalized units
+
+ for (char in text) {
+ val glyph = atlas.getGlyph(char.code) ?: continue
+
+ // bearingX/Y are already normalized, just multiply by pixelSize
+ // bearingY is the distance from baseline to glyph top, so with Y-up:
+ // - glyph top is at baseline + bearingY
+ // - glyph bottom is at baseline + bearingY - height
+ val localX0 = penX + glyph.bearingX
+ val localY1 = glyph.bearingY // Top of glyph (Y-up)
+
+ // width/height are in pixels, need normalization
+ val localX1 = localX0 + glyph.width / atlas.baseSize
+ val localY0 = localY1 - glyph.height / atlas.baseSize // Bottom of glyph
+
+ // Scale to final pixels and add anchor + offsets
+ val x0 = anchorX + startX + localX0 * pixelSize
+ val y0 = anchorY + startY + localY0 * pixelSize
+ val x1 = anchorX + startX + localX1 * pixelSize
+ val y1 = anchorY + startY + localY1 * pixelSize
+
+ // Screen-space text uses simple 2D quads - add directly to collector with style params
+ // Quad winding: bottom-left, bottom-right, top-right, top-left (CCW for Y-up)
+ collector.screenTextVertices.add(RegionVertexCollector.ScreenTextVertex(
+ x0, y0, glyph.u0, glyph.v1, r, g, b, a, outlineWidth, glowRadius, shadowSoftness, threshold, layer))
+ collector.screenTextVertices.add(RegionVertexCollector.ScreenTextVertex(
+ x1, y0, glyph.u1, glyph.v1, r, g, b, a, outlineWidth, glowRadius, shadowSoftness, threshold, layer))
+ collector.screenTextVertices.add(RegionVertexCollector.ScreenTextVertex(
+ x1, y1, glyph.u1, glyph.v0, r, g, b, a, outlineWidth, glowRadius, shadowSoftness, threshold, layer))
+ collector.screenTextVertices.add(RegionVertexCollector.ScreenTextVertex(
+ x0, y1, glyph.u0, glyph.v0, r, g, b, a, outlineWidth, glowRadius, shadowSoftness, threshold, layer))
+
+ // advance is already normalized, just add it
+ penX += glyph.advance
+ }
+ }
+
+ /**
+ * Build text quad vertices for a layer with specified color and alpha.
+ * Adds vertices directly to collector with embedded SDF style params.
+ *
+ * @param atlas Font atlas
+ * @param text Text string
+ * @param startX Starting X offset for text
+ * @param startY Starting Y offset for text
+ * @param r Red color component
+ * @param g Green color component
+ * @param b Blue color component
+ * @param a Alpha component (encodes layer type)
+ * @param anchorX Camera-relative anchor X position
+ * @param anchorY Camera-relative anchor Y position
+ * @param anchorZ Camera-relative anchor Z position
+ * @param scale Text scale
+ * @param rotationMatrix Optional rotation matrix for fixed rotation mode
+ */
+ private fun buildTextQuads(
+ atlas: SDFFontAtlas,
+ text: String,
+ startX: Float,
+ startY: Float,
+ r: Int, g: Int, b: Int, a: Int,
+ anchorX: Float, anchorY: Float, anchorZ: Float,
+ scale: Float,
+ rotationMatrix: Matrix4f?,
+ style: SDFStyle
+ ) {
+ // Extract SDF style params from SDFStyle object
+ val outlineWidth = style.outline?.width ?: 0f
+ val glowRadius = style.glow?.radius ?: 0f
+ val shadowSoftness = style.shadow?.softness ?: 0f
+ val threshold = 0.5f // Default SDF threshold
+
+ var penX = startX
+ for (char in text) {
+ val glyph = atlas.getGlyph(char.code) ?: continue
+
+ val x0 = penX + glyph.bearingX
+ val y0 = startY - glyph.bearingY
+ val x1 = x0 + glyph.width / atlas.baseSize
+ val y1 = y0 + glyph.height / atlas.baseSize
+
+ if (rotationMatrix == null) {
+ // Billboard mode: pass local offsets directly, shader handles billboard
+ // Bottom-left, Bottom-right, Top-right, Top-left
+ collector.textVertices.add(RegionVertexCollector.TextVertex(
+ x0, y1, glyph.u0, glyph.v1, r, g, b, a, anchorX, anchorY, anchorZ, scale, 0f,
+ outlineWidth, glowRadius, shadowSoftness, threshold))
+ collector.textVertices.add(RegionVertexCollector.TextVertex(
+ x1, y1, glyph.u1, glyph.v1, r, g, b, a, anchorX, anchorY, anchorZ, scale, 0f,
+ outlineWidth, glowRadius, shadowSoftness, threshold))
+ collector.textVertices.add(RegionVertexCollector.TextVertex(
+ x1, y0, glyph.u1, glyph.v0, r, g, b, a, anchorX, anchorY, anchorZ, scale, 0f,
+ outlineWidth, glowRadius, shadowSoftness, threshold))
+ collector.textVertices.add(RegionVertexCollector.TextVertex(
+ x0, y0, glyph.u0, glyph.v0, r, g, b, a, anchorX, anchorY, anchorZ, scale, 0f,
+ outlineWidth, glowRadius, shadowSoftness, threshold))
+ } else {
+ // Fixed rotation mode: pre-transform offsets with rotation matrix
+ // Scale is applied in shader, so we just apply rotation here
+ val p0 = transformPoint(rotationMatrix, x0, -y1, 0f) // Negate Y for flip
+ val p1 = transformPoint(rotationMatrix, x1, -y1, 0f)
+ val p2 = transformPoint(rotationMatrix, x1, -y0, 0f)
+ val p3 = transformPoint(rotationMatrix, x0, -y0, 0f)
+
+ collector.textVertices.add(RegionVertexCollector.TextVertex(
+ p0.x, p0.y, glyph.u0, glyph.v1, r, g, b, a, anchorX, anchorY, anchorZ, scale, 1f,
+ outlineWidth, glowRadius, shadowSoftness, threshold))
+ collector.textVertices.add(RegionVertexCollector.TextVertex(
+ p1.x, p1.y, glyph.u1, glyph.v1, r, g, b, a, anchorX, anchorY, anchorZ, scale, 1f,
+ outlineWidth, glowRadius, shadowSoftness, threshold))
+ collector.textVertices.add(RegionVertexCollector.TextVertex(
+ p2.x, p2.y, glyph.u1, glyph.v0, r, g, b, a, anchorX, anchorY, anchorZ, scale, 1f,
+ outlineWidth, glowRadius, shadowSoftness, threshold))
+ collector.textVertices.add(RegionVertexCollector.TextVertex(
+ p3.x, p3.y, glyph.u0, glyph.v0, r, g, b, a, anchorX, anchorY, anchorZ, scale, 1f,
+ outlineWidth, glowRadius, shadowSoftness, threshold))
+ }
+
+ penX += glyph.advance
+ }
+ }
+
+ private fun BoxBuilder.boxFaces(box: Box) {
+ // We need to call the internal methods, so we'll use filled() with interpolated colors
+ // For per-vertex colors on faces, we need direct access to the collector
+
+ if (fillSides.hasDirection(DirectionMask.EAST)) {
+ // East face (+X): uses NE and SE corners
+ filledQuadGradient(
+ box.maxX, box.minY, box.minZ, fillBottomNorthEast,
+ box.maxX, box.maxY, box.minZ, fillTopNorthEast,
+ box.maxX, box.maxY, box.maxZ, fillTopSouthEast,
+ box.maxX, box.minY, box.maxZ, fillBottomSouthEast
+ )
+ }
+ if (fillSides.hasDirection(DirectionMask.WEST)) {
+ // West face (-X): uses NW and SW corners
+ filledQuadGradient(
+ box.minX, box.minY, box.minZ, fillBottomNorthWest,
+ box.minX, box.minY, box.maxZ, fillBottomSouthWest,
+ box.minX, box.maxY, box.maxZ, fillTopSouthWest,
+ box.minX, box.maxY, box.minZ, fillTopNorthWest
+ )
+ }
+ if (fillSides.hasDirection(DirectionMask.UP)) {
+ // Top face (+Y): uses all top corners
+ filledQuadGradient(
+ box.minX, box.maxY, box.minZ, fillTopNorthWest,
+ box.minX, box.maxY, box.maxZ, fillTopSouthWest,
+ box.maxX, box.maxY, box.maxZ, fillTopSouthEast,
+ box.maxX, box.maxY, box.minZ, fillTopNorthEast
+ )
+ }
+ if (fillSides.hasDirection(DirectionMask.DOWN)) {
+ // Bottom face (-Y): uses all bottom corners
+ filledQuadGradient(
+ box.minX, box.minY, box.minZ, fillBottomNorthWest,
+ box.maxX, box.minY, box.minZ, fillBottomNorthEast,
+ box.maxX, box.minY, box.maxZ, fillBottomSouthEast,
+ box.minX, box.minY, box.maxZ, fillBottomSouthWest
+ )
+ }
+ if (fillSides.hasDirection(DirectionMask.SOUTH)) {
+ // South face (+Z): uses SW and SE corners
+ filledQuadGradient(
+ box.minX, box.minY, box.maxZ, fillBottomSouthWest,
+ box.maxX, box.minY, box.maxZ, fillBottomSouthEast,
+ box.maxX, box.maxY, box.maxZ, fillTopSouthEast,
+ box.minX, box.maxY, box.maxZ, fillTopSouthWest
+ )
+ }
+ if (fillSides.hasDirection(DirectionMask.NORTH)) {
+ // North face (-Z): uses NW and NE corners
+ filledQuadGradient(
+ box.minX, box.minY, box.minZ, fillBottomNorthWest,
+ box.minX, box.maxY, box.minZ, fillTopNorthWest,
+ box.maxX, box.maxY, box.minZ, fillTopNorthEast,
+ box.maxX, box.minY, box.minZ, fillBottomNorthEast
+ )
+ }
+ }
+
+ private fun BoxBuilder.boxOutline(box: Box) {
+ val hasEast = outlineSides.hasDirection(DirectionMask.EAST)
+ val hasWest = outlineSides.hasDirection(DirectionMask.WEST)
+ val hasUp = outlineSides.hasDirection(DirectionMask.UP)
+ val hasDown = outlineSides.hasDirection(DirectionMask.DOWN)
+ val hasSouth = outlineSides.hasDirection(DirectionMask.SOUTH)
+ val hasNorth = outlineSides.hasDirection(DirectionMask.NORTH)
+
+ // Top edges (all use top vertex colors)
+ if (outlineMode.check(hasUp, hasNorth)) {
+ lineGradient(
+ box.minX, box.maxY, box.minZ, outlineTopNorthWest,
+ box.maxX, box.maxY, box.minZ, outlineTopNorthEast,
+ lineWidth, dashStyle
+ )
+ }
+ if (outlineMode.check(hasUp, hasSouth)) {
+ lineGradient(
+ box.minX, box.maxY, box.maxZ, outlineTopSouthWest,
+ box.maxX, box.maxY, box.maxZ, outlineTopSouthEast,
+ lineWidth, dashStyle
+ )
+ }
+ if (outlineMode.check(hasUp, hasWest)) {
+ lineGradient(
+ box.minX, box.maxY, box.minZ, outlineTopNorthWest,
+ box.minX, box.maxY, box.maxZ, outlineTopSouthWest,
+ lineWidth, dashStyle
+ )
+ }
+ if (outlineMode.check(hasUp, hasEast)) {
+ lineGradient(
+ box.maxX, box.maxY, box.maxZ, outlineTopSouthEast,
+ box.maxX, box.maxY, box.minZ, outlineTopNorthEast,
+ lineWidth, dashStyle
+ )
+ }
+
+ // Bottom edges (all use bottom vertex colors)
+ if (outlineMode.check(hasDown, hasNorth)) {
+ lineGradient(
+ box.minX, box.minY, box.minZ, outlineBottomNorthWest,
+ box.maxX, box.minY, box.minZ, outlineBottomNorthEast,
+ lineWidth, dashStyle
+ )
+ }
+ if (outlineMode.check(hasDown, hasSouth)) {
+ lineGradient(
+ box.minX, box.minY, box.maxZ, outlineBottomSouthWest,
+ box.maxX, box.minY, box.maxZ, outlineBottomSouthEast,
+ lineWidth, dashStyle
+ )
+ }
+ if (outlineMode.check(hasDown, hasWest)) {
+ lineGradient(
+ box.minX, box.minY, box.minZ, outlineBottomNorthWest,
+ box.minX, box.minY, box.maxZ, outlineBottomSouthWest,
+ lineWidth, dashStyle
+ )
+ }
+ if (outlineMode.check(hasDown, hasEast)) {
+ lineGradient(
+ box.maxX, box.minY, box.minZ, outlineBottomNorthEast,
+ box.maxX, box.minY, box.maxZ, outlineBottomSouthEast,
+ lineWidth, dashStyle
+ )
+ }
+
+ // Vertical edges (gradient from top to bottom)
+ if (outlineMode.check(hasWest, hasNorth)) {
+ lineGradient(
+ box.minX, box.maxY, box.minZ, outlineTopNorthWest,
+ box.minX, box.minY, box.minZ, outlineBottomNorthWest,
+ lineWidth, dashStyle
+ )
+ }
+ if (outlineMode.check(hasNorth, hasEast)) {
+ lineGradient(
+ box.maxX, box.maxY, box.minZ, outlineTopNorthEast,
+ box.maxX, box.minY, box.minZ, outlineBottomNorthEast,
+ lineWidth, dashStyle
+ )
+ }
+ if (outlineMode.check(hasEast, hasSouth)) {
+ lineGradient(
+ box.maxX, box.maxY, box.maxZ, outlineTopSouthEast,
+ box.maxX, box.minY, box.maxZ, outlineBottomSouthEast,
+ lineWidth, dashStyle
+ )
+ }
+ if (outlineMode.check(hasSouth, hasWest)) {
+ lineGradient(
+ box.minX, box.maxY, box.maxZ, outlineTopSouthWest,
+ box.minX, box.minY, box.maxZ, outlineBottomSouthWest,
+ lineWidth, dashStyle
+ )
+ }
+ }
+
+ /** Draw a line with world coordinates - handles relative conversion internally */
+ private fun line(
+ x1: Double, y1: Double, z1: Double,
+ x2: Double, y2: Double, z2: Double,
+ color1: Color,
+ color2: Color,
+ width: Float,
+ dashStyle: LineDashStyle? = null
+ ) {
+ // Convert to camera-relative coordinates
+ val rx1 = (x1 - cameraPos.x).toFloat()
+ val ry1 = (y1 - cameraPos.y).toFloat()
+ val rz1 = (z1 - cameraPos.z).toFloat()
+ val rx2 = (x2 - cameraPos.x).toFloat()
+ val ry2 = (y2 - cameraPos.y).toFloat()
+ val rz2 = (z2 - cameraPos.z).toFloat()
+
+ // Calculate segment vector
+ val dx = rx2 - rx1
+ val dy = ry2 - ry1
+ val dz = rz2 - rz1
+
+ // Quad-based lines need 4 vertices per segment
+ collector.addEdgeVertex(rx1, ry1, rz1, color1, dx, dy, dz, width, dashStyle)
+ collector.addEdgeVertex(rx1, ry1, rz1, color1, dx, dy, dz, width, dashStyle)
+ collector.addEdgeVertex(rx2, ry2, rz2, color2, dx, dy, dz, width, dashStyle)
+ collector.addEdgeVertex(rx2, ry2, rz2, color2, dx, dy, dz, width, dashStyle)
+ }
+
+ /** Helper to transform a point by a matrix */
+ private fun transformPoint(matrix: Matrix4f, x: Float, y: Float, z: Float): Vector3f {
+ val result = Vector4f(x, y, z, 1f)
+ matrix.transform(result)
+ return Vector3f(result.x, result.y, result.z)
+ }
+
+ /** Add a face vertex with world coordinates - handles relative conversion internally */
+ private fun faceVertex(x: Double, y: Double, z: Double, color: Color) {
+ val rx = (x - cameraPos.x).toFloat()
+ val ry = (y - cameraPos.y).toFloat()
+ val rz = (z - cameraPos.z).toFloat()
+ collector.addFaceVertex(rx, ry, rz, color)
+ }
+
+ /** SDF outline effect configuration */
+ data class SDFOutline(
+ val color: Color = Color.BLACK,
+ val width: Float = 0.1f // 0.0 - 0.3 in SDF units (distance from edge)
+ )
+
+ /** SDF glow effect configuration */
+ data class SDFGlow(
+ val color: Color = Color(0, 200, 255, 180),
+ val radius: Float = 0.2f // Glow spread in SDF units
+ )
+
+ /** SDF shadow effect configuration */
+ data class SDFShadow(
+ val color: Color = Color(0, 0, 0, 180),
+ val offset: Float = 0.05f, // Distance in text units
+ // Angle in degrees: 0=right, 90=up, 180=left, 270=down (for screen text with Y-up)
+ // For world text, angle is applied in local text space before billboarding
+ val angle: Float = 135f, // Default: bottom-right (45° below horizontal)
+ val softness: Float = 0f // Shadow blur in SDF units
+ ) {
+ /** X offset computed from angle and distance */
+ val offsetX: Float get() = offset * kotlin.math.cos(Math.toRadians(angle.toDouble())).toFloat()
+ /** Y offset computed from angle and distance */
+ val offsetY: Float get() = offset * kotlin.math.sin(Math.toRadians(angle.toDouble())).toFloat()
+ }
+
+ /** SDF style configuration for text and other SDF-rendered elements */
+ data class SDFStyle(
+ var color: Color = Color.WHITE,
+ val outline: SDFOutline? = null,
+ val glow: SDFGlow? = null,
+ val shadow: SDFShadow? = SDFShadow() // Default shadow enabled
+ )
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/graphics/mc/RenderRegion.kt b/src/main/kotlin/com/lambda/graphics/mc/RenderRegion.kt
deleted file mode 100644
index 6687aa44e..000000000
--- a/src/main/kotlin/com/lambda/graphics/mc/RenderRegion.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright 2025 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.graphics.mc
-
-import net.minecraft.util.math.Vec3d
-import org.joml.Vector3f
-
-/**
- * A render region represents a chunk-sized area in the world where vertices are stored relative to
- * the region's origin. This solves floating-point precision issues at large world coordinates.
- *
- * @param originX The X coordinate of the region's origin (typically chunk corner)
- * @param originY The Y coordinate of the region's origin
- * @param originZ The Z coordinate of the region's origin
- */
-class RenderRegion(val originX: Int, val originY: Int, val originZ: Int) {
- /**
- * Compute the camera-relative offset for this region. This is done in double precision to
- * maintain accuracy at large coordinates.
- *
- * @param cameraPos The camera's world position (double precision)
- * @return The offset from camera to region origin (small float, high precision)
- */
- fun computeCameraRelativeOffset(cameraPos: Vec3d): Vector3f {
- val offsetX = originX.toDouble() - cameraPos.x
- val offsetY = originY.toDouble() - cameraPos.y
- val offsetZ = originZ.toDouble() - cameraPos.z
- return Vector3f(offsetX.toFloat(), offsetY.toFloat(), offsetZ.toFloat())
- }
-
- companion object {
- /** Standard size of a render region (matches Minecraft chunk size). */
- const val REGION_SIZE = 16
-
- /**
- * Create a region for a chunk position.
- *
- * @param chunkX Chunk X coordinate
- * @param chunkZ Chunk Z coordinate
- * @param bottomY World bottom Y coordinate (typically -64)
- */
- fun forChunk(chunkX: Int, chunkZ: Int, bottomY: Int) =
- RenderRegion(chunkX * 16, bottomY, chunkZ * 16)
- }
-}
diff --git a/src/main/kotlin/com/lambda/graphics/mc/TransientRegionESP.kt b/src/main/kotlin/com/lambda/graphics/mc/TransientRegionESP.kt
deleted file mode 100644
index bc1cd812c..000000000
--- a/src/main/kotlin/com/lambda/graphics/mc/TransientRegionESP.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright 2025 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.graphics.mc
-
-import com.lambda.graphics.esp.RegionESP
-import com.lambda.graphics.esp.ShapeScope
-import java.util.concurrent.ConcurrentHashMap
-import kotlin.math.floor
-
-/**
- * Modern replacement for the legacy Treed system. Handles geometry that is cleared and rebuilt
- * every tick. Uses region-based rendering for precision.
- */
-class TransientRegionESP(name: String, depthTest: Boolean = false) : RegionESP(name, depthTest) {
- private val builders = ConcurrentHashMap()
-
- /** Get or create a builder for a specific region. */
- override fun shapes(x: Double, y: Double, z: Double, block: ShapeScope.() -> Unit) {
- val key = getRegionKey(x, y, z)
- val scope =
- builders.getOrPut(key) {
- val size = RenderRegion.REGION_SIZE
- val rx = (size * floor(x / size)).toInt()
- val ry = (size * floor(y / size)).toInt()
- val rz = (size * floor(z / size)).toInt()
- ShapeScope(RenderRegion(rx, ry, rz))
- }
- scope.apply(block)
- }
-
- /** Clear all current builders. Call this at the end of every tick. */
- override fun clear() {
- builders.clear()
- }
-
- /** Upload collected geometry to GPU. Must be called on main thread. */
- override fun upload() {
- val activeKeys = builders.keys().asSequence().toSet()
-
- builders.forEach { (key, scope) ->
- val renderer = renderers.getOrPut(key) { RegionRenderer(scope.region) }
- renderer.upload(scope.builder.collector)
- }
-
- renderers.forEach { (key, renderer) ->
- if (key !in activeKeys) {
- renderer.clearData()
- }
- }
- }
-}
diff --git a/src/main/kotlin/com/lambda/graphics/mc/WorldTextRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/WorldTextRenderer.kt
deleted file mode 100644
index 9aa34034d..000000000
--- a/src/main/kotlin/com/lambda/graphics/mc/WorldTextRenderer.kt
+++ /dev/null
@@ -1,307 +0,0 @@
-/*
- * Copyright 2025 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.graphics.mc
-
-import com.lambda.Lambda.mc
-import net.minecraft.client.font.TextRenderer
-import net.minecraft.client.render.LightmapTextureManager
-import net.minecraft.client.util.math.MatrixStack
-import net.minecraft.text.Text
-import net.minecraft.util.math.Vec3d
-import java.awt.Color
-
-/**
- * Utility for rendering text in 3D world space.
- *
- * Uses Minecraft's TextRenderer to draw text that faces the camera (billboard style) at any world
- * position. Handles Unicode, formatting codes, and integrates with MC's rendering system.
- *
- * Usage:
- * ```kotlin
- * // In your render event
- * WorldTextRenderer.drawText(
- * pos = entity.pos.add(0.0, entity.height + 0.5, 0.0),
- * text = entity.name,
- * color = Color.WHITE,
- * scale = 0.025f
- * )
- * ```
- */
-object WorldTextRenderer {
-
- /** Default scale for world text (MC uses 0.025f for name tags) */
- const val DEFAULT_SCALE = 0.025f
-
- /** Maximum light level for full brightness */
- private const val FULL_BRIGHT = LightmapTextureManager.MAX_LIGHT_COORDINATE
-
- /**
- * Draw text at a world position, facing the camera.
- *
- * @param pos World position for the text
- * @param text The text to render
- * @param color Text color (ARGB)
- * @param scale Text scale (0.025f is default name tag size)
- * @param shadow Whether to draw drop shadow
- * @param seeThrough Whether text should be visible through blocks
- * @param centered Whether to center the text horizontally
- * @param backgroundColor Background color (0 for no background)
- * @param light Light level (uses full bright by default)
- */
- fun drawText(
- pos: Vec3d,
- text: Text,
- color: Color = Color.WHITE,
- scale: Float = DEFAULT_SCALE,
- shadow: Boolean = true,
- seeThrough: Boolean = false,
- centered: Boolean = true,
- backgroundColor: Int = 0,
- light: Int = FULL_BRIGHT
- ) {
- val client = mc
- val camera = client.gameRenderer?.camera ?: return
- val textRenderer = client.textRenderer ?: return
- val immediate = client.bufferBuilders?.entityVertexConsumers ?: return
-
- val cameraPos = camera.pos
-
- val matrices = MatrixStack()
- matrices.push()
-
- // Translate to world position relative to camera
- matrices.translate(pos.x - cameraPos.x, pos.y - cameraPos.y, pos.z - cameraPos.z)
-
- // Billboard - face camera using camera rotation directly (same as MC's LabelCommandRenderer)
- matrices.multiply(camera.rotation)
-
- // Scale with negative Y to flip text vertically (matches MC's 0.025, -0.025, 0.025)
- matrices.scale(scale, -scale, scale)
-
- // Calculate text position
- val textWidth = textRenderer.getWidth(text)
- val x = if (centered) -textWidth / 2f else 0f
-
- val layerType =
- if (seeThrough) TextRenderer.TextLayerType.SEE_THROUGH
- else TextRenderer.TextLayerType.NORMAL
-
- // Draw text
- textRenderer.draw(
- text,
- x,
- 0f,
- color.rgb,
- shadow,
- matrices.peek().positionMatrix,
- immediate,
- layerType,
- backgroundColor,
- light
- )
-
- matrices.pop()
-
- // Flush immediately for world rendering
- immediate.draw()
- }
-
- /**
- * Draw text at a world position with an outline effect.
- *
- * @param pos World position for the text
- * @param text The text to render
- * @param color Text color
- * @param outlineColor Outline color
- * @param scale Text scale
- * @param centered Whether to center the text horizontally
- * @param light Light level
- */
- fun drawTextWithOutline(
- pos: Vec3d,
- text: Text,
- color: Color = Color.WHITE,
- outlineColor: Color = Color.BLACK,
- scale: Float = DEFAULT_SCALE,
- centered: Boolean = true,
- light: Int = FULL_BRIGHT
- ) {
- val client = mc
- val camera = client.gameRenderer?.camera ?: return
- val textRenderer = client.textRenderer ?: return
- val immediate = client.bufferBuilders?.entityVertexConsumers ?: return
-
- val cameraPos = camera.pos
-
- val matrices = MatrixStack()
- matrices.push()
-
- matrices.translate(pos.x - cameraPos.x, pos.y - cameraPos.y, pos.z - cameraPos.z)
-
- // Billboard - face camera using camera rotation directly (same as MC's LabelCommandRenderer)
- matrices.multiply(camera.rotation)
- matrices.scale(scale, -scale, scale)
-
- val textWidth = textRenderer.getWidth(text)
- val x = if (centered) -textWidth / 2f else 0f
-
- textRenderer.drawWithOutline(
- text.asOrderedText(),
- x,
- 0f,
- color.rgb,
- outlineColor.rgb,
- matrices.peek().positionMatrix,
- immediate,
- light
- )
-
- matrices.pop()
- immediate.draw()
- }
-
- /** Draw a simple string at a world position. */
- fun drawString(
- pos: Vec3d,
- text: String,
- color: Color = Color.WHITE,
- scale: Float = DEFAULT_SCALE,
- shadow: Boolean = true,
- seeThrough: Boolean = false,
- centered: Boolean = true
- ) {
- drawText(pos, Text.literal(text), color, scale, shadow, seeThrough, centered)
- }
-
- /**
- * Draw multiple lines of text stacked vertically.
- *
- * @param pos World position for the top line
- * @param lines List of text lines to render
- * @param color Text color
- * @param scale Text scale
- * @param lineSpacing Spacing between lines in scaled units (default 10)
- */
- fun drawMultilineText(
- pos: Vec3d,
- lines: List,
- color: Color = Color.WHITE,
- scale: Float = DEFAULT_SCALE,
- lineSpacing: Float = 10f,
- shadow: Boolean = true,
- seeThrough: Boolean = false,
- centered: Boolean = true
- ) {
- val client = mc
- val camera = client.gameRenderer?.camera ?: return
- val textRenderer = client.textRenderer ?: return
- val immediate = client.bufferBuilders?.entityVertexConsumers ?: return
-
- val cameraPos = camera.pos
-
- val matrices = MatrixStack()
- matrices.push()
-
- matrices.translate(pos.x - cameraPos.x, pos.y - cameraPos.y, pos.z - cameraPos.z)
-
- // Billboard - face camera using camera rotation directly (same as MC's LabelCommandRenderer)
- matrices.multiply(camera.rotation)
- matrices.scale(scale, -scale, scale)
-
- val layerType =
- if (seeThrough) TextRenderer.TextLayerType.SEE_THROUGH
- else TextRenderer.TextLayerType.NORMAL
-
- lines.forEachIndexed { index, text ->
- val textWidth = textRenderer.getWidth(text)
- val x = if (centered) -textWidth / 2f else 0f
- val y = index * lineSpacing
-
- textRenderer.draw(
- text,
- x,
- y,
- color.rgb,
- shadow,
- matrices.peek().positionMatrix,
- immediate,
- layerType,
- 0,
- FULL_BRIGHT
- )
- }
-
- matrices.pop()
- immediate.draw()
- }
-
- /**
- * Draw text with a background box.
- *
- * @param pos World position
- * @param text Text to render
- * @param textColor Text color
- * @param backgroundColor Background color (with alpha)
- * @param scale Text scale
- * @param padding Padding around text in pixels
- */
- fun drawTextWithBackground(
- pos: Vec3d,
- text: Text,
- textColor: Color = Color.WHITE,
- backgroundColor: Color = Color(0, 0, 0, 128),
- scale: Float = DEFAULT_SCALE,
- padding: Int = 2,
- shadow: Boolean = false,
- seeThrough: Boolean = false,
- centered: Boolean = true
- ) {
- val client = mc
- client.textRenderer ?: return
-
- // Calculate background color as ARGB int
- val bgColorInt =
- (backgroundColor.alpha shl 24) or
- (backgroundColor.red shl 16) or
- (backgroundColor.green shl 8) or
- backgroundColor.blue
-
- drawText(
- pos = pos,
- text = text,
- color = textColor,
- scale = scale,
- shadow = shadow,
- seeThrough = seeThrough,
- centered = centered,
- backgroundColor = bgColorInt
- )
- }
-
- /** Calculate the width of text in world units at a given scale. */
- fun getTextWidth(text: Text, scale: Float = DEFAULT_SCALE): Float {
- val textRenderer = mc.textRenderer ?: return 0f
- return textRenderer.getWidth(text) * scale
- }
-
- /** Calculate the height of text in world units at a given scale. */
- fun getTextHeight(scale: Float = DEFAULT_SCALE): Float {
- val textRenderer = mc.textRenderer ?: return 0f
- return textRenderer.fontHeight * scale
- }
-}
diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/AbstractRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/AbstractRenderer.kt
new file mode 100644
index 000000000..cb4c9d7f3
--- /dev/null
+++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/AbstractRenderer.kt
@@ -0,0 +1,272 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.graphics.mc.renderer
+
+import com.lambda.context.SafeContext
+import com.lambda.graphics.mc.RegionRenderer
+import com.lambda.graphics.mc.RenderBuilder
+import com.lambda.graphics.text.SDFFontAtlas
+import com.mojang.blaze3d.buffers.GpuBufferSlice
+import com.mojang.blaze3d.systems.RenderPass
+import com.mojang.blaze3d.systems.RenderSystem
+import kotlin.collections.isNotEmpty
+
+/**
+ * Abstract base class for ESP renderers.
+ *
+ * Provides shared world-space and screen-space rendering logic while allowing
+ * subclasses to define their own lifecycle (upload frequency, geometry building, etc.)
+ *
+ * Subclasses implement [getRendererTransforms] to return their renderer/transform pairs:
+ * - ImmediateRenderer/TickedRenderer: returns a single pair (one renderer, one transform)
+ * - ChunkedRenderer: returns multiple pairs (one per active chunk with per-chunk transforms)
+ *
+ * @param name Debug name for render passes
+ * @param depthTest Whether to use depth testing (true = through walls disabled)
+ */
+abstract class AbstractRenderer(val name: String, var depthTest: SafeContext.() -> Boolean) {
+ /**
+ * Get all renderer/transform pairs to render.
+ * Each pair contains a RegionRenderer and its associated dynamic transform.
+ *
+ * @return List of (RegionRenderer, GpuBufferSlice) pairs, or empty list if nothing to render
+ */
+ protected abstract fun getRendererTransforms(): List>
+
+ /**
+ * Get all screen-space renderers.
+ * Returns renderers that have screen-space data to render.
+ */
+ protected abstract fun getScreenRenderers(): List
+
+ /** Current font atlas for text rendering (may be null if no text) */
+ protected abstract val currentFontAtlas: SDFFontAtlas?
+
+ /**
+ * Render world-space geometry (faces, edges, text).
+ * Iterates over all renderer/transform pairs from getRendererTransforms().
+ *
+ * All world-space renderers share the same xray depth buffer (cleared once per frame in RenderMain).
+ * - depthTest = true: Uses MC's depth buffer (respects world geometry)
+ * - depthTest = false: Uses Lambda's xray depth buffer (ignores world, self-ordering only)
+ */
+ fun SafeContext.render() {
+ val chunks = getRendererTransforms()
+ if (chunks.isEmpty()) return
+
+ val depth = depthTest()
+
+ // Render Faces
+ RegionRenderer.createRenderPass("$name Faces", depth)?.use { pass ->
+ pass.setPipeline(RendererUtils.getFacesPipeline(depth))
+ RenderSystem.bindDefaultUniforms(pass)
+ chunks.forEach { (renderer, transform) ->
+ pass.setUniform("DynamicTransforms", transform)
+ renderer.renderFaces(pass)
+ }
+ }
+
+ // Render Edges
+ RegionRenderer.createRenderPass("$name Edges", depth)?.use { pass ->
+ pass.setPipeline(RendererUtils.getEdgesPipeline(depth))
+ RenderSystem.bindDefaultUniforms(pass)
+ chunks.forEach { (renderer, transform) ->
+ pass.setUniform("DynamicTransforms", transform)
+ renderer.renderEdges(pass)
+ }
+ }
+
+ // Render Text - style params are now embedded in vertex attributes
+ val textChunks = chunks.filter { (renderer, _) -> renderer.hasTextData() }
+ val atlas = currentFontAtlas
+ if (atlas != null && textChunks.isNotEmpty()) {
+ if (!atlas.isUploaded) atlas.upload()
+ val textureView = atlas.textureView
+ val sampler = atlas.sampler
+ if (textureView != null && sampler != null) {
+ RegionRenderer.createRenderPass("$name Text", depth)?.use { pass ->
+ pass.setPipeline(RendererUtils.getTextPipeline(depth))
+ RenderSystem.bindDefaultUniforms(pass)
+ pass.bindTexture("Sampler0", textureView, sampler)
+ textChunks.forEach { (renderer, transform) ->
+ pass.setUniform("DynamicTransforms", transform)
+ renderer.renderText(pass)
+ }
+ }
+ }
+ }
+
+ // Render World Images
+ val imageChunks = chunks.filter { (renderer, _) -> renderer.hasWorldImageData() }
+ if (imageChunks.isNotEmpty()) {
+ // Pre-load glint texture before creating render pass
+ RendererUtils.ensureGlintTextureLoaded()
+
+ RegionRenderer.createRenderPass("$name World Images", depth)?.use { pass ->
+ pass.setPipeline(RendererUtils.getWorldImagePipeline(depth))
+ RenderSystem.bindDefaultUniforms(pass)
+
+ // Bind enchantment glint texture for overlay support
+ RendererUtils.bindGlintTexture(pass, "Sampler1")
+
+ // Use per-chunk transforms for correct positioning
+ // Glint animation is calculated in shader using GameTime with corrected speed
+ imageChunks.forEach { (renderer, transform) ->
+ pass.setUniform("DynamicTransforms", transform)
+ renderer.renderWorldImages(pass)
+ }
+ }
+ }
+
+ // Render World Models
+ val modelChunks = chunks.filter { (renderer, _) -> renderer.hasModelData() }
+ if (modelChunks.isNotEmpty()) {
+ RendererUtils.ensureGlintTextureLoaded()
+
+ // Create dedicated glint uniform slice (scale 8.0 for vanilla atlas parity)
+ val glintUniform = RendererUtils.createGlintUniform(8.0f)
+
+ RegionRenderer.createRenderPass("$name World Models", depth)?.use { pass ->
+ pass.setPipeline(RendererUtils.getModelPipeline(depth))
+ RenderSystem.bindDefaultUniforms(pass)
+
+ // Bind overlay, lightmap, and glint textures
+ RendererUtils.bindOverlayTexture(pass, "Sampler1")
+ RendererUtils.bindLightmapTexture(pass, "Sampler2")
+ RendererUtils.bindGlintTexture(pass, "Sampler3")
+
+ // Set the global glint animation matrix for this pass
+ pass.setUniform("GlintTransforms", glintUniform)
+
+ // Use original chunk transforms for correct geometry position
+ modelChunks.forEach { (renderer, transform) ->
+ pass.setUniform("DynamicTransforms", transform)
+ renderer.renderModels(pass)
+ }
+ }
+ }
+ }
+
+ /**
+ * Render screen-space geometry. Uses orthographic projection for 2D rendering.
+ * Uses a SEPARATE screen depth buffer for complete isolation from world-space.
+ * This should be called after world-space render() for proper layering.
+ *
+ * Each renderer clears the screen depth buffer at the start to ensure
+ * complete isolation from other renderers.
+ */
+ fun renderScreen() {
+ val renderers = getScreenRenderers()
+
+ RendererUtils.withScreenContext {
+ val dynamicTransform = RendererUtils.createScreenDynamicTransform()
+
+ // Clear the SCREEN depth buffer (separate from world xray depth)
+ // This ensures complete isolation between renderers
+ RendererUtils.clearScreenDepthBuffer()
+
+ // Helper to get the right render pass (depth already cleared at start)
+ fun getScreenPass(label: String): RenderPass? {
+ return RegionRenderer.createScreenRenderPassWithDepth(label, clearDepth = false)
+ }
+
+ // Render Screen Models
+ val modelRenderers = renderers.filter { it.hasScreenModelData() }
+ if (modelRenderers.isNotEmpty()) {
+ RendererUtils.ensureGlintTextureLoaded()
+
+ // Create dedicated glint uniform slice (scale 8.0 for vanilla GUI parity)
+ val glintUniform = RendererUtils.createGlintUniform(8.0f)
+
+ getScreenPass("$name Screen Models")?.use { pass ->
+ pass.setPipeline(RendererUtils.getModelPipeline(depthTest = true))
+ RenderSystem.bindDefaultUniforms(pass)
+
+ // Use global screen dynamic transform for geometry position
+ pass.setUniform("DynamicTransforms", dynamicTransform)
+ // Use dedicated glint uniform for animation
+ pass.setUniform("GlintTransforms", glintUniform)
+
+ // Bind overlay and lightmap textures for item effects
+ RendererUtils.bindOverlayTexture(pass, "Sampler1")
+ RendererUtils.bindLightmapTexture(pass, "Sampler2")
+ // Bind glint texture for enchantment shimmer
+ RendererUtils.bindGlintTexture(pass, "Sampler3")
+
+ modelRenderers.forEach { it.renderScreenModels(pass) }
+ }
+ }
+
+ // Render Screen Faces
+ getScreenPass("$name Screen Faces")?.use { pass ->
+ pass.setPipeline(RendererUtils.getScreenFacesPipeline(depthTest = true))
+ RenderSystem.bindDefaultUniforms(pass)
+ pass.setUniform("DynamicTransforms", dynamicTransform)
+ renderers.forEach { it.renderScreenFaces(pass) }
+ }
+
+ // Render Screen Edges
+ getScreenPass("$name Screen Edges")?.use { pass ->
+ pass.setPipeline(RendererUtils.getScreenEdgesPipeline(depthTest = true))
+ RenderSystem.bindDefaultUniforms(pass)
+ pass.setUniform("DynamicTransforms", dynamicTransform)
+ renderers.forEach { it.renderScreenEdges(pass) }
+ }
+
+ // Render Screen Text - style params are now embedded in vertex attributes
+ val textRenderers = renderers.filter { it.hasScreenTextData() }
+ val atlas = currentFontAtlas
+ if (atlas != null && textRenderers.isNotEmpty()) {
+ if (!atlas.isUploaded) atlas.upload()
+ val textureView = atlas.textureView
+ val sampler = atlas.sampler
+ if (textureView != null && sampler != null) {
+ getScreenPass("$name Screen Text")?.use { pass ->
+ pass.setPipeline(RendererUtils.getScreenTextPipeline(depthTest = true))
+ RenderSystem.bindDefaultUniforms(pass)
+ pass.setUniform("DynamicTransforms", dynamicTransform)
+ pass.bindTexture("Sampler0", textureView, sampler)
+ textRenderers.forEach { it.renderScreenText(pass) }
+ }
+ }
+ }
+
+ // Render Screen Images - needs separate transform with glint matrix for animation
+ val imageRenderers = renderers.filter { it.hasScreenImageData() }
+ if (imageRenderers.isNotEmpty()) {
+ // Pre-load glint texture BEFORE creating render pass to avoid command conflicts
+ RendererUtils.ensureGlintTextureLoaded()
+
+ // Create a fresh dynamic transform with glint matrix calculated NOW (not at build time)
+ val glintTransform = RendererUtils.createScreenDynamicTransformWithGlint()
+
+ getScreenPass("$name Screen Images")?.use { pass ->
+ pass.setPipeline(RendererUtils.getScreenImagePipeline(depthTest = true))
+ RenderSystem.bindDefaultUniforms(pass)
+ pass.setUniform("DynamicTransforms", glintTransform)
+
+ // Bind enchantment glint texture for overlay support
+ RendererUtils.bindGlintTexture(pass, "Sampler1")
+
+ // Each renderer handles its own texture batches
+ imageRenderers.forEach { it.renderScreenImages(pass) }
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt
new file mode 100644
index 000000000..3b1a063e7
--- /dev/null
+++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt
@@ -0,0 +1,226 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.graphics.mc.renderer
+
+import com.lambda.Lambda.mc
+import com.lambda.context.SafeContext
+import com.lambda.event.events.RenderEvent
+import com.lambda.event.events.TickEvent
+import com.lambda.event.events.WorldEvent
+import com.lambda.event.listener.SafeListener.Companion.listen
+import com.lambda.event.listener.SafeListener.Companion.listenConcurrently
+import com.lambda.graphics.RenderMain
+import com.lambda.graphics.mc.RegionRenderer
+import com.lambda.graphics.mc.RenderBuilder
+import com.lambda.graphics.text.FontHandler
+import com.lambda.graphics.text.SDFFontAtlas
+import com.lambda.module.modules.client.StyleEditor
+import com.lambda.threading.runSafe
+import com.lambda.util.world.FastVector
+import com.lambda.util.world.fastVectorOf
+import com.mojang.blaze3d.buffers.GpuBufferSlice
+import com.mojang.blaze3d.systems.RenderSystem
+import net.minecraft.util.math.Vec3d
+import net.minecraft.world.World
+import net.minecraft.world.chunk.WorldChunk
+import org.joml.Matrix4f
+import org.joml.Vector3f
+import org.joml.Vector4f
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.ConcurrentLinkedDeque
+
+
+/**
+ * Chunked ESP system using chunk-origin relative coordinates.
+ *
+ * This system:
+ * - Stores geometry relative to chunk origin (stable, small floats)
+ * - Only rebuilds when chunks are modified
+ * - At render time, translates from chunk origin to camera-relative position
+ *
+ * @param owner The module that owns this ESP system
+ * @param name The name of the ESP system
+ * @param depthTest Whether to use depth testing
+ * @param update The update function called for each block position
+ */
+class ChunkedRenderer(
+ owner: Any,
+ name: String,
+ depthTest: SafeContext.() -> Boolean,
+ private val update: RenderBuilder.(World, FastVector) -> Unit
+) : AbstractRenderer(name, depthTest) {
+ private val chunkMap = ConcurrentHashMap()
+
+ private val WorldChunk.chunkKey: Long
+ get() = getChunkKey(pos.x, pos.z)
+
+ private val WorldChunk.chunkData
+ get() = chunkMap.getOrPut(chunkKey) { ChunkData(this) }
+
+ private val rebuildQueue = ConcurrentLinkedDeque()
+ private val uploadQueue = ConcurrentLinkedDeque<() -> Unit>()
+
+ // Font atlas from the default font handler
+ override val currentFontAtlas: SDFFontAtlas
+ get() = FontHandler.getDefaultFont()
+
+ init {
+ owner.listen { event ->
+ val pos = event.pos
+ world.getWorldChunk(pos)?.chunkData?.markDirty()
+
+ val xInChunk = pos.x and 15
+ val zInChunk = pos.z and 15
+
+ if (xInChunk == 0) world.getWorldChunk(pos.west())?.chunkData?.markDirty()
+ if (xInChunk == 15) world.getWorldChunk(pos.east())?.chunkData?.markDirty()
+ if (zInChunk == 0) world.getWorldChunk(pos.north())?.chunkData?.markDirty()
+ if (zInChunk == 15) world.getWorldChunk(pos.south())?.chunkData?.markDirty()
+ }
+
+ owner.listen { event -> event.chunk.chunkData.markDirty() }
+ owner.listen { chunkMap.remove(it.chunk.chunkKey)?.clearData() }
+
+ owner.listenConcurrently {
+ val queueSize = rebuildQueue.size
+ val polls = minOf(StyleEditor.rebuildsPerTick, queueSize)
+ repeat(polls) { rebuildQueue.poll()?.rebuild() }
+ }
+
+ owner.listen {
+ val polls = minOf(StyleEditor.uploadsPerTick, uploadQueue.size)
+ repeat(polls) { uploadQueue.poll()?.invoke() }
+ }
+
+ owner.listen { render() }
+ owner.listen { renderScreen() }
+ }
+
+ private fun getChunkKey(chunkX: Int, chunkZ: Int) =
+ (chunkX.toLong() and 0xFFFFFFFFL) or ((chunkZ.toLong() and 0xFFFFFFFFL) shl 32)
+
+ /** Mark all tracked chunks for rebuild. */
+ fun rebuild() {
+ rebuildQueue.clear()
+ rebuildQueue.addAll(chunkMap.values)
+ }
+
+ /**
+ * Load all currently loaded world chunks and mark them for rebuild. Call this when the module
+ * is enabled to populate initial chunks.
+ */
+ fun rebuildAll() {
+ runSafe {
+ val chunksArray = world.chunkManager.chunks.chunks
+ (0 until chunksArray.length()).forEach { i ->
+ chunksArray.get(i)?.chunkData?.markDirty()
+ }
+ }
+ }
+
+ fun clear() {
+ chunkMap.values.forEach { it.clearData() }
+ chunkMap.clear()
+ rebuildQueue.clear()
+ uploadQueue.clear()
+ }
+
+ /**
+ * Get renderer/transform pairs for all active chunks.
+ * Each chunk has its own renderer and per-chunk transform (chunk-origin to camera).
+ * Includes fresh glint TextureMat for world image animation.
+ */
+ override fun getRendererTransforms(): List> {
+ val cameraPos = mc.gameRenderer?.camera?.pos ?: return emptyList()
+
+ val activeChunks = chunkMap.values.filter { it.renderer.hasData() }
+ if (activeChunks.isEmpty()) return emptyList()
+
+ val modelViewMatrix = RenderMain.modelViewMatrix
+
+ // Pre-compute the glint matrix once for all chunks (same animation for all)
+ val glintMatrix = RendererUtils.createGlintTransform(0.25f)
+
+ return activeChunks.map { chunkData ->
+ // Compute chunk-to-camera offset in double precision
+ val offsetX = (chunkData.originX - cameraPos.x).toFloat()
+ val offsetY = (chunkData.originY - cameraPos.y).toFloat()
+ val offsetZ = (chunkData.originZ - cameraPos.z).toFloat()
+
+ val modelView = Matrix4f(modelViewMatrix).m30(0f).m31(0f).m32(0f).translate(offsetX, offsetY, offsetZ)
+ val dynamicTransform = RenderSystem.getDynamicUniforms()
+ .write(modelView, Vector4f(1f, 1f, 1f, 1f), Vector3f(0f, 0f, 0f), glintMatrix)
+
+ chunkData.renderer to dynamicTransform
+ }
+ }
+
+ /**
+ * Get renderers for screen-space rendering.
+ * Returns all chunk renderers that have screen data.
+ */
+ override fun getScreenRenderers() =
+ chunkMap.values
+ .filter { it.renderer.hasScreenData() }
+ .map { it.renderer }
+
+ /** Per-chunk data with its own renderer and origin. */
+ private inner class ChunkData(val chunk: WorldChunk) {
+ // Chunk origin in world coordinates
+ val originX: Double = (chunk.pos.x shl 4).toDouble()
+ val originY: Double = chunk.bottomY.toDouble()
+ val originZ: Double = (chunk.pos.z shl 4).toDouble()
+
+ // This chunk's own renderer
+ val renderer = RegionRenderer()
+
+ fun markDirty() {
+ if (!rebuildQueue.contains(this)) rebuildQueue.add(this)
+ }
+
+ /**
+ * Rebuild geometry relative to chunk origin.
+ * Coordinates are stored as (worldPos - chunkOrigin).toFloat()
+ */
+ fun rebuild() {
+ // Use chunk origin as the "camera" position for relative coords
+ val chunkOriginVec = Vec3d(originX, originY, originZ)
+ val scope = RenderBuilder(chunkOriginVec)
+
+ for (x in chunk.pos.startX..chunk.pos.endX) {
+ for (z in chunk.pos.startZ..chunk.pos.endZ) {
+ for (y in chunk.bottomY..chunk.height) {
+ update(scope, chunk.world, fastVectorOf(x, y, z))
+ }
+ }
+ }
+
+ uploadQueue.add { renderer.upload(scope.collector) }
+ }
+
+ fun clearData() = renderer.clearData()
+ }
+
+ companion object {
+ fun Any.chunkedRenderer(
+ name: String,
+ depthTest: SafeContext.() -> Boolean = { false },
+ update: RenderBuilder.(World, FastVector) -> Unit
+ ) = ChunkedRenderer(this, name, depthTest, update)
+ }
+}
diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt
new file mode 100644
index 000000000..1b02f7fb8
--- /dev/null
+++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.graphics.mc.renderer
+
+import com.lambda.context.SafeContext
+import com.lambda.event.events.RenderEvent
+import com.lambda.event.listener.SafeListener.Companion.listen
+import com.lambda.graphics.RenderMain
+import com.lambda.graphics.mc.RegionRenderer
+import com.lambda.graphics.mc.RenderBuilder
+import com.lambda.graphics.text.SDFFontAtlas
+import com.mojang.blaze3d.buffers.GpuBufferSlice
+import com.mojang.blaze3d.systems.RenderSystem
+import org.joml.Matrix4f
+import org.joml.Vector3f
+import org.joml.Vector4f
+
+/**
+ * Interpolated ESP system for smooth entity rendering.
+ *
+ * This system rebuilds and uploads vertices every frame with camera-relative coordinates.
+ * Callers are responsible for providing interpolated positions (e.g., using entity.prevX/x
+ * with tickDelta). The tick() method clears builders to allow smooth transitions between frames.
+ */
+class ImmediateRenderer(
+ owner: Any,
+ name: String,
+ depthTest: SafeContext.() -> Boolean,
+ update: RenderBuilder.(SafeContext) -> Unit
+) : AbstractRenderer(name, depthTest) {
+ private val renderer = RegionRenderer()
+
+ // Font atlas used for current text rendering
+ private var _currentFontAtlas: SDFFontAtlas? = null
+ override val currentFontAtlas: SDFFontAtlas? get() = _currentFontAtlas
+
+ init {
+ owner.listen {
+ renderer.clearData()
+ val renderBuilder = RenderBuilder(mc.gameRenderer.camera.pos).also { it.update(SafeContext.create() ?: return@listen) }
+ upload(renderBuilder)
+ render()
+ }
+ owner.listen { renderScreen() }
+ }
+
+ /** Upload collected geometry to GPU. Must be called on main thread. */
+ fun upload(renderBuilder: RenderBuilder) {
+ renderer.upload(renderBuilder.collector)
+ _currentFontAtlas = renderBuilder.fontAtlas
+ }
+
+ /**
+ * Get renderer/transform pairs for world-space rendering.
+ * Returns single renderer with camera-relative transform and fresh glint TextureMat.
+ */
+ override fun getRendererTransforms(): List> {
+ if (!renderer.hasData()) return emptyList()
+
+ val modelViewMatrix = RenderMain.modelViewMatrix
+ val modelView = Matrix4f(modelViewMatrix).m30(0f).m31(0f).m32(0f)
+ val dynamicTransform = RenderSystem.getDynamicUniforms()
+ .write(
+ modelView,
+ Vector4f(1f, 1f, 1f, 1f),
+ Vector3f(0f, 0f, 0f),
+ RendererUtils.createGlintTransform(0.125f) // Calibrated glint scale for world images
+ )
+
+ return listOf(renderer to dynamicTransform)
+ }
+
+ /**
+ * Get renderers for screen-space rendering.
+ */
+ override fun getScreenRenderers() = if (renderer.hasScreenData()) listOf(renderer) else emptyList()
+
+ companion object {
+ fun Any.immediateRenderer(
+ name: String,
+ depthTest: SafeContext.() -> Boolean = { false },
+ update: RenderBuilder.(SafeContext) -> Unit
+ ) = ImmediateRenderer(this, name, depthTest, update)
+ }
+}
diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/RendererUtils.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/RendererUtils.kt
new file mode 100644
index 000000000..2f66c049a
--- /dev/null
+++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/RendererUtils.kt
@@ -0,0 +1,470 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.graphics.mc.renderer
+
+import com.lambda.Lambda.mc
+import com.lambda.event.events.HudRenderEvent
+import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafe
+import com.lambda.graphics.RenderMain
+import com.lambda.graphics.mc.LambdaRenderPipelines
+import com.lambda.graphics.mc.RegionRenderer
+import com.lambda.graphics.text.SDFFontAtlas
+import com.lambda.graphics.texture.LambdaImageAtlas
+import com.mojang.blaze3d.buffers.GpuBufferSlice
+import com.mojang.blaze3d.pipeline.RenderPipeline
+import com.mojang.blaze3d.systems.ProjectionType
+import com.mojang.blaze3d.systems.RenderSystem
+import net.minecraft.client.gui.DrawContext
+import net.minecraft.client.render.ProjectionMatrix2
+import org.joml.Matrix4f
+import org.joml.Vector3f
+import org.joml.Vector4f
+
+/**
+ * Shared utilities for ESP renderers.
+ * Contains common rendering setup code used by ImmediateRenderer, TickedRenderer, and ChunkedRenderer.
+ */
+object RendererUtils {
+ // Shared projection matrix for screen-space rendering
+ // invertY=false means Y=0 at bottom, Y=height at top (OpenGL/math convention)
+ private val screenProjectionMatrix = ProjectionMatrix2("lambda_screen", -1000f, 1000f, false)
+
+ // Vanilla inventory lighting vectors (Studio Lighting)
+ // These are from net.minecraft.client.render.DiffuseLighting and flipped to be "towards light"
+ val INVENTORY_LIGHT_0 = Vector3f(-0.2f, 1.0f, -1.0f).normalize()
+ val INVENTORY_LIGHT_1 = Vector3f(0.2f, 1.0f, 0.0f).normalize()
+
+
+
+ /**
+ * Create a dynamic transform uniform with identity matrices for screen-space rendering.
+ */
+ fun createScreenDynamicTransform(): GpuBufferSlice {
+ val identityMatrix = Matrix4f()
+ return RenderSystem.getDynamicUniforms()
+ .write(
+ identityMatrix,
+ Vector4f(1f, 1f, 1f, 1f),
+ Vector3f(0f, 0f, 0f),
+ identityMatrix
+ )
+ }
+
+ /**
+ * Create a dynamic transform with glint texture matrix for screen-space IMAGE rendering.
+ * Note: We use a smaller scale (0.125) compared to vanilla (8.0) because images use
+ * normalized 0-1 UVs. 0.125 results in the same ~1/8 visual density.
+ */
+ fun createScreenDynamicTransformWithGlint(): GpuBufferSlice {
+ val identityMatrix = Matrix4f()
+ return RenderSystem.getDynamicUniforms()
+ .write(
+ identityMatrix,
+ Vector4f(1f, 1f, 1f, 1f),
+ Vector3f(0f, 0f, 0f),
+ createGlintTransform(0.125f) // Normalized scale for [0, 1] UV parity
+ )
+ }
+
+ /**
+ * Create a dynamic transform with glint texture matrix for screen-space MODEL rendering.
+ * Uses vanilla's 8.0 scale since model UVs are texture-based, not normalized.
+ */
+ fun createScreenModelDynamicTransformWithGlint(): GpuBufferSlice {
+ val identityMatrix = Matrix4f()
+ return RenderSystem.getDynamicUniforms()
+ .write(
+ identityMatrix,
+ Vector4f(1f, 1f, 1f, 1f),
+ Vector3f(0f, 0f, 0f),
+ createGlintTransform(8.0f) // Vanilla glint scale parity
+ )
+ }
+
+ /**
+ * Create a glint transformation matrix exactly like Minecraft's TextureTransform.getGlintTransformation().
+ * This is called every frame to get smooth animation.
+ *
+ * @param scale The UV scale factor (8.0 for items, 0.5 for entities, 0.16 for armor)
+ */
+ fun createGlintTransform(scale: Float): Matrix4f {
+ // Exactly replicate Minecraft's TextureTransform.getGlintTransformation()
+ val glintSpeed = mc.options?.glintSpeed?.value ?: 0.5
+ val time = (net.minecraft.util.Util.getMeasuringTimeMs() * glintSpeed * 8.0).toLong()
+
+ // Calculate scroll offsets (0-1 range)
+ val scrollX = (time % 110000L) / 110000.0f // 110s
+ val scrollY = (time % 30000L) / 30000.0f // 30s
+
+ // Build matrix: Translation -> Rotation -> Scale (T * R * S)
+ // This order ensures the translation/scrolling is NOT multiplied by the scale
+ val matrix = Matrix4f()
+ matrix.translation(-scrollX, scrollY, 0f)
+ matrix.rotateZ((Math.PI / 18.0).toFloat()) // 10 degrees (Vanilla)
+ matrix.scale(scale)
+
+ return matrix
+ }
+
+ /**
+ * Create a dynamic uniform slice containing two glint transformation matrices.
+ * Pack Mat2 into the ModelView slot and Mat1 into the GlintMat slot of the GlintTransforms block.
+ */
+ fun createGlintUniform(scale: Float): GpuBufferSlice {
+ val glintSpeed = mc.options?.glintSpeed?.value ?: 0.5
+ val time = (net.minecraft.util.Util.getMeasuringTimeMs() * glintSpeed * 8.0).toLong()
+
+ // Up-Left (Vanilla parity in Y-up space)
+ val scroll1X = (time % 110000L) / 110000.0f
+ val scroll1Y = (time % 30000L) / 30000.0f
+ val mat1 = Matrix4f().translation(-scroll1X, scroll1Y, 0f)
+ .rotateZ((Math.PI / 18.0).toFloat()) // Exactly 10 deg (Vanilla)
+ .scale(scale)
+
+ return RenderSystem.getDynamicUniforms()
+ .write(
+ Matrix4f(), // ModelView slot (Identity)
+ Vector4f(1f, 1f, 1f, 1f), // Color
+ Vector3f(0f, 0f, 0f), // Offset
+ mat1 // GlintMat slot
+ )
+ }
+
+ /**
+ * Create a dynamic transform with glint texture matrix for world-space MODEL rendering.
+ * Uses RenderMain.modelViewMatrix with translation zeroed for correct camera-relative positioning.
+ * Uses vanilla's 8.0 scale since model UVs are texture-based, not normalized.
+ */
+ fun createWorldDynamicTransformWithGlint(): GpuBufferSlice {
+ // Use modelViewMatrix with translation zeroed out (same pattern as ImmediateRenderer)
+ val modelViewMatrix = RenderMain.modelViewMatrix
+ val modelView = Matrix4f(modelViewMatrix).m30(0f).m31(0f).m32(0f)
+
+ return RenderSystem.getDynamicUniforms()
+ .write(
+ modelView,
+ Vector4f(1f, 1f, 1f, 1f),
+ Vector3f(0f, 0f, 0f),
+ createGlintTransform(8.0f) // Vanilla glint scale parity
+ )
+ }
+
+ /**
+ * Create a dynamic transform for a specific chunk position with glint texture matrix.
+ * Used for world-space model rendering where items need enchantment glint animation.
+ * Uses vanilla's 8.0 scale since model UVs are texture-based, not normalized.
+ *
+ * @param chunkOffset The chunk's position offset (usually cameraRelative)
+ */
+ fun createChunkTransformWithGlint(chunkOffset: Vector3f): GpuBufferSlice {
+ return RenderSystem.getDynamicUniforms()
+ .write(
+ RenderSystem.getModelViewMatrix(),
+ Vector4f(1f, 1f, 1f, 1f),
+ chunkOffset,
+ createGlintTransform(8.0f) // Vanilla glint scale parity
+ )
+ }
+
+ /**
+ * Execute a block with screen-space rendering context.
+ * Sets up orthographic projection and identity model-view, then restores state after.
+ *
+ * @param block The rendering code to execute in screen-space context
+ */
+ fun withScreenContext(block: () -> Unit) {
+ val window = mc.window ?: return
+ val width = window.scaledWidth.toFloat()
+ val height = window.scaledHeight.toFloat()
+
+ // Backup current projection matrix
+ RenderSystem.backupProjectionMatrix()
+
+ // Use orthographic projection matrix
+ screenProjectionMatrix.set(width, height).let { slice ->
+ RenderSystem.setProjectionMatrix(slice, ProjectionType.ORTHOGRAPHIC)
+ }
+
+ // Identity model-view for screen-space
+ RenderSystem.getModelViewStack().pushMatrix().identity()
+
+ try {
+ block()
+ } finally {
+ // Restore matrices
+ RenderSystem.getModelViewStack().popMatrix()
+ RenderSystem.restoreProjectionMatrix()
+ }
+ }
+
+ // ============================================================================
+ // Pipeline Helpers
+ // ============================================================================
+
+ /**
+ * Get the face/quad pipeline.
+ * Always uses depth testing. Xray effect is achieved by using Lambda's
+ * custom depth buffer (which doesn't contain MC world geometry).
+ */
+ fun getFacesPipeline(depthTest: Boolean): RenderPipeline = LambdaRenderPipelines.ESP_QUADS
+
+ /**
+ * Get the edge/line pipeline.
+ * Always uses depth testing for proper self-ordering.
+ */
+ fun getEdgesPipeline(depthTest: Boolean): RenderPipeline = LambdaRenderPipelines.ESP_LINES
+
+ /**
+ * Get the SDF text pipeline.
+ * Always uses depth testing for proper self-ordering.
+ */
+ fun getTextPipeline(depthTest: Boolean): RenderPipeline = LambdaRenderPipelines.SDF_TEXT
+
+ /** Get the screen-space faces pipeline. */
+ fun getScreenFacesPipeline(depthTest: Boolean = true): RenderPipeline = LambdaRenderPipelines.SCREEN_FACES
+
+ /** Screen-space faces pipeline (legacy, use getScreenFacesPipeline instead). */
+ val screenFacesPipeline: RenderPipeline get() = LambdaRenderPipelines.SCREEN_FACES
+
+ /** Get the screen-space edges pipeline. */
+ fun getScreenEdgesPipeline(depthTest: Boolean = true): RenderPipeline = LambdaRenderPipelines.SCREEN_LINES
+
+ /** Screen-space edges pipeline (legacy, use getScreenEdgesPipeline instead). */
+ val screenEdgesPipeline: RenderPipeline get() = LambdaRenderPipelines.SCREEN_LINES
+
+ /** Get the screen-space text pipeline. */
+ fun getScreenTextPipeline(depthTest: Boolean = true): RenderPipeline = LambdaRenderPipelines.SCREEN_TEXT
+
+ /** Screen-space text pipeline (legacy, use getScreenTextPipeline instead). */
+ val screenTextPipeline: RenderPipeline get() = LambdaRenderPipelines.SCREEN_TEXT
+
+ /** Get the screen-space image pipeline. */
+ fun getScreenImagePipeline(depthTest: Boolean = true): RenderPipeline = LambdaRenderPipelines.SCREEN_IMAGE
+
+ /**
+ * Get the world-space image pipeline.
+ * Always uses depth testing for proper self-ordering.
+ */
+ fun getWorldImagePipeline(depthTest: Boolean): RenderPipeline = LambdaRenderPipelines.WORLD_IMAGE
+
+ /**
+ * Get the world-space model pipeline.
+ * Always uses depth testing for proper self-ordering.
+ */
+ fun getModelPipeline(depthTest: Boolean): RenderPipeline =
+ if (depthTest) LambdaRenderPipelines.WORLD_MODEL else LambdaRenderPipelines.SCREEN_MODEL
+
+ // Cached glint texture view and sampler
+ private var glintTextureView: com.mojang.blaze3d.textures.GpuTextureView? = null
+ private var glintSampler: net.minecraft.client.gl.GpuSampler? = null
+ private var glintTextureLoaded = false
+
+ /**
+ * Pre-load the glint texture and process any pending texture loads.
+ * Must be called BEFORE creating a render pass.
+ * This avoids the "close the existing render pass" error.
+ */
+ fun ensureGlintTextureLoaded() {
+ // Process any pending texture loads from background threads
+ LambdaImageAtlas.processPendingLoads()
+
+ if (!glintTextureLoaded) {
+ val textureManager = mc.textureManager
+ val glintId = net.minecraft.util.Identifier.ofVanilla("textures/misc/enchanted_glint_item.png")
+ val texture = textureManager.getTexture(glintId)
+ glintTextureView = texture?.glTextureView
+ glintTextureLoaded = true
+ }
+
+ // Always ensure sampler is using REPEAT mode
+ glintSampler = RenderSystem.getSamplerCache().getRepeated(com.mojang.blaze3d.textures.FilterMode.LINEAR)
+ }
+
+ /**
+ * Bind the Minecraft enchanted item glint texture to a sampler slot.
+ * Must call ensureGlintTextureLoaded() BEFORE starting the render pass.
+ *
+ * @param pass The render pass to bind the texture to
+ * @param samplerName The sampler name to bind to (e.g., "Sampler1")
+ */
+ fun bindGlintTexture(pass: com.mojang.blaze3d.systems.RenderPass, samplerName: String) {
+ val view = glintTextureView ?: return
+ val sampler = glintSampler ?: return
+ pass.bindTexture(samplerName, view, sampler)
+ }
+
+ /**
+ * Bind the generic Overlay texture (white/hurt flash) to a sampler slot.
+ */
+ fun bindOverlayTexture(pass: com.mojang.blaze3d.systems.RenderPass, samplerName: String) {
+ val overlay = mc.gameRenderer.overlayTexture ?: return
+ val view = overlay.textureView ?: return
+ // Overlay texture usually uses LINEAR filtering
+ val sampler = RenderSystem.getSamplerCache().get(com.mojang.blaze3d.textures.FilterMode.LINEAR)
+ pass.bindTexture(samplerName, view, sampler)
+ }
+
+ /**
+ * Bind the Lightmap texture to a sampler slot.
+ */
+ fun bindLightmapTexture(pass: com.mojang.blaze3d.systems.RenderPass, samplerName: String) {
+ val lightmap = mc.gameRenderer.lightmapTextureManager ?: return
+ val view = lightmap.glTextureView ?: return
+ // Lightmap uses LINEAR filtering
+ val sampler = RenderSystem.getSamplerCache().get(com.mojang.blaze3d.textures.FilterMode.LINEAR)
+ pass.bindTexture(samplerName, view, sampler)
+ }
+
+ // ============================================================================
+ // Custom Depth Buffer for Xray Rendering
+ // ============================================================================
+
+ // Custom depth buffer for Lambda's xray rendering.
+ // This allows proper depth ordering among our own renders while ignoring MC's world.
+ private var xrayDepthTexture: com.mojang.blaze3d.textures.GpuTexture? = null
+ private var xrayDepthView: com.mojang.blaze3d.textures.GpuTextureView? = null
+ private var xrayDepthWidth = 0
+ private var xrayDepthHeight = 0
+
+ /**
+ * Get the xray depth buffer view, creating/resizing if necessary.
+ * This depth buffer is separate from MC's main depth buffer, allowing
+ * our renders to show through MC's world while still having correct
+ * depth ordering among themselves.
+ *
+ * @return The depth buffer view, or null if framebuffer is not available
+ */
+ fun getXrayDepthView(): com.mojang.blaze3d.textures.GpuTextureView? {
+ val framebuffer = mc.framebuffer ?: return null
+ val width = framebuffer.textureWidth
+ val height = framebuffer.textureHeight
+
+ // Recreate if size changed or doesn't exist
+ if (xrayDepthTexture == null || xrayDepthWidth != width || xrayDepthHeight != height) {
+ // Clean up old resources
+ xrayDepthView?.close()
+ xrayDepthTexture?.close()
+
+ // Create new depth texture matching framebuffer size
+ val gpuDevice = RenderSystem.getDevice()
+ xrayDepthTexture = gpuDevice.createTexture(
+ { "Lambda Xray Depth Buffer" },
+ 15, // Usage flags (same as MC's depth buffers)
+ com.mojang.blaze3d.textures.TextureFormat.DEPTH32,
+ width,
+ height,
+ 1, // Layers
+ 1 // Mip levels
+ )
+ xrayDepthView = gpuDevice.createTextureView(xrayDepthTexture)
+ xrayDepthWidth = width
+ xrayDepthHeight = height
+ }
+
+ return xrayDepthView
+ }
+
+ /**
+ * Clear the xray depth buffer to prepare for a new render sequence.
+ * Should be called once at the start of each frame's xray rendering.
+ */
+ fun clearXrayDepthBuffer() {
+ val depthView = getXrayDepthView() ?: return
+ val framebuffer = mc.framebuffer ?: return
+
+ // Create a render pass that just clears the depth buffer
+ // We pass OptionalDouble.of(1.0) to clear depth to far plane (1.0)
+ RenderSystem.getDevice()
+ .createCommandEncoder()
+ .createRenderPass(
+ { "Lambda Clear Xray Depth" },
+ framebuffer.colorAttachmentView,
+ java.util.OptionalInt.empty(), // Don't clear color
+ depthView,
+ java.util.OptionalDouble.of(1.0) // Clear depth to 1.0 (far)
+ )?.close() // Immediately close to execute the clear
+ }
+
+ // ============================================================================
+ // Separate Screen-Space Depth Buffer
+ // ============================================================================
+
+ // A completely separate depth buffer for screen-space rendering.
+ // This ensures screen elements don't depth-fight with world-space elements.
+ private var screenDepthTexture: com.mojang.blaze3d.textures.GpuTexture? = null
+ private var screenDepthView: com.mojang.blaze3d.textures.GpuTextureView? = null
+ private var screenDepthWidth = 0
+ private var screenDepthHeight = 0
+
+ /**
+ * Get the screen-space depth buffer view, creating/resizing if necessary.
+ * This depth buffer is separate from both MC's depth and the xray depth buffer,
+ * ensuring screen-space elements are completely isolated.
+ *
+ * @return The screen depth buffer view, or null if framebuffer is not available
+ */
+ fun getScreenDepthView(): com.mojang.blaze3d.textures.GpuTextureView? {
+ val framebuffer = mc.framebuffer ?: return null
+ val width = framebuffer.textureWidth
+ val height = framebuffer.textureHeight
+
+ // Recreate if size changed or doesn't exist
+ if (screenDepthTexture == null || screenDepthWidth != width || screenDepthHeight != height) {
+ // Clean up old resources
+ screenDepthView?.close()
+ screenDepthTexture?.close()
+
+ // Create new depth texture matching framebuffer size
+ val gpuDevice = RenderSystem.getDevice()
+ screenDepthTexture = gpuDevice.createTexture(
+ { "Lambda Screen Depth Buffer" },
+ 15, // Usage flags (same as MC's depth buffers)
+ com.mojang.blaze3d.textures.TextureFormat.DEPTH32,
+ width,
+ height,
+ 1, // Layers
+ 1 // Mip levels
+ )
+ screenDepthView = gpuDevice.createTextureView(screenDepthTexture)
+ screenDepthWidth = width
+ screenDepthHeight = height
+ }
+
+ return screenDepthView
+ }
+
+ /**
+ * Clear the screen depth buffer to prepare for a new screen render sequence.
+ * Should be called at the start of each renderer's screen rendering.
+ */
+ fun clearScreenDepthBuffer() {
+ val depthView = getScreenDepthView() ?: return
+ val framebuffer = mc.framebuffer ?: return
+
+ RenderSystem.getDevice()
+ .createCommandEncoder()
+ .createRenderPass(
+ { "Lambda Clear Screen Depth" },
+ framebuffer.colorAttachmentView,
+ java.util.OptionalInt.empty(),
+ depthView,
+ java.util.OptionalDouble.of(1.0)
+ )?.close()
+ }
+}
+
diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.kt
new file mode 100644
index 000000000..058ad2b17
--- /dev/null
+++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.graphics.mc.renderer
+
+import com.lambda.Lambda.mc
+import com.lambda.context.SafeContext
+import com.lambda.event.events.RenderEvent
+import com.lambda.event.events.TickEvent
+import com.lambda.event.listener.SafeListener.Companion.listen
+import com.lambda.graphics.RenderMain
+import com.lambda.graphics.mc.RegionRenderer
+import com.lambda.graphics.mc.RenderBuilder
+import com.lambda.graphics.text.SDFFontAtlas
+import com.mojang.blaze3d.buffers.GpuBufferSlice
+import com.mojang.blaze3d.systems.RenderSystem
+import net.minecraft.util.math.Vec3d
+import org.joml.Matrix4f
+import org.joml.Vector3f
+import org.joml.Vector4f
+
+
+/**
+ * Modern replacement for the legacy Treed system. Handles geometry that is cleared and rebuilt
+ * every tick.
+ *
+ * Geometry is stored relative to the camera position at tick time. At render time, we compute
+ * the delta between tick-camera and current-camera to ensure smooth motion without jitter.
+ */
+class TickedRenderer(
+ owner: Any,
+ name: String,
+ depthTest: SafeContext.() -> Boolean,
+ update: RenderBuilder.(SafeContext) -> Unit
+) : AbstractRenderer(name, depthTest) {
+ private val renderer = RegionRenderer()
+
+ // Camera position captured at tick time (when shapes are built)
+ private var tickCameraPos: Vec3d? = null
+
+ // Font atlas used for current text rendering
+ private var _currentFontAtlas: SDFFontAtlas? = null
+ override val currentFontAtlas: SDFFontAtlas? get() = _currentFontAtlas
+
+ init {
+ owner.listen {
+ clear()
+ tickCameraPos = mc.gameRenderer.camera.pos
+ val renderBuilder = RenderBuilder(tickCameraPos ?: return@listen).also {
+ it.update(SafeContext.create() ?: return@listen)
+ }
+ upload(renderBuilder)
+ }
+
+ owner.listen { render() }
+ owner.listen { renderScreen() }
+ }
+
+ /** Clear all current builders. Call this at the end of every tick. */
+ fun clear() {
+ renderer.clearData()
+ tickCameraPos = null
+ }
+
+ /** Upload collected geometry to GPU. Must be called on main thread. */
+ fun upload(renderBuilder: RenderBuilder) {
+ renderer.upload(renderBuilder.collector)
+ _currentFontAtlas = renderBuilder.fontAtlas
+ }
+
+ /**
+ * Get renderer/transform pairs for world-space rendering.
+ * Computes delta between tick-camera and current-camera for smooth interpolation.
+ * Includes fresh glint TextureMat for world image animation.
+ */
+ override fun getRendererTransforms(): List> {
+ val currentCameraPos = mc.gameRenderer?.camera?.pos ?: return emptyList()
+ val tickCamera = tickCameraPos ?: return emptyList()
+ if (!renderer.hasData()) return emptyList()
+
+ val modelViewMatrix = RenderMain.modelViewMatrix
+
+ // Compute the camera movement since tick time in double precision
+ // Geometry is stored relative to tickCamera, so we translate by (tickCamera - currentCamera)
+ val deltaX = (tickCamera.x - currentCameraPos.x).toFloat()
+ val deltaY = (tickCamera.y - currentCameraPos.y).toFloat()
+ val deltaZ = (tickCamera.z - currentCameraPos.z).toFloat()
+
+ val modelView = Matrix4f(modelViewMatrix).m30(0f).m31(0f).m32(0f).translate(deltaX, deltaY, deltaZ)
+ val dynamicTransform = RenderSystem.getDynamicUniforms()
+ .write(modelView, Vector4f(1f, 1f, 1f, 1f), Vector3f(0f, 0f, 0f), RendererUtils.createGlintTransform(0.25f))
+
+ return listOf(renderer to dynamicTransform)
+ }
+
+ /**
+ * Get renderers for screen-space rendering.
+ */
+ override fun getScreenRenderers() = if (renderer.hasScreenData()) listOf(renderer) else emptyList()
+
+ companion object {
+ fun Any.tickedRenderer(
+ name: String,
+ depthTest: SafeContext.() -> Boolean = { false },
+ update: RenderBuilder.(SafeContext) -> Unit
+ ) = TickedRenderer(this, name, depthTest, update)
+ }
+}
diff --git a/src/main/kotlin/com/lambda/graphics/text/FontHandler.kt b/src/main/kotlin/com/lambda/graphics/text/FontHandler.kt
new file mode 100644
index 000000000..8c6561a16
--- /dev/null
+++ b/src/main/kotlin/com/lambda/graphics/text/FontHandler.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.graphics.text
+
+import java.util.concurrent.ConcurrentHashMap
+
+/**
+ * Central handler for font loading and caching.
+ *
+ * Manages SDF font atlases with automatic caching by path and size.
+ * Use this instead of creating SDFFontAtlas instances directly.
+ *
+ * Usage:
+ * ```kotlin
+ * val font = FontHandler.loadFont("fonts/MyFont.ttf", 128f)
+ * val defaultFont = FontHandler.getDefaultFont()
+ * ```
+ */
+object FontHandler {
+ private val fonts = ConcurrentHashMap()
+ private var defaultFont: SDFFontAtlas? = null
+
+ /**
+ * Load an SDF font from resources.
+ *
+ * @param path Resource path to TTF/OTF file (e.g., "fonts/MinecraftDefault-Regular.ttf")
+ * @param size Base font size for SDF generation (larger = higher quality, default 128)
+ * @return The loaded SDFFontAtlas, or null if loading failed
+ */
+ fun loadFont(path: String, size: Float = 128f): SDFFontAtlas? {
+ val key = "$path@$size"
+ return fonts.getOrPut(key) {
+ try {
+ SDFFontAtlas(path, size)
+ } catch (e: Exception) {
+ println("[FontHandler] Failed to load font: $path - ${e.message}")
+ return null
+ }
+ }
+ }
+
+ /**
+ * Get or create the default font.
+ * Uses MinecraftDefault-Regular.ttf at 128px base size.
+ */
+ fun getDefaultFont(size: Float = 128f): SDFFontAtlas {
+ defaultFont?.let { return it }
+
+ val key = "fonts/MinecraftDefault-Regular.ttf@$size"
+ val font = fonts[key] ?: run {
+ val newFont = SDFFontAtlas("fonts/MinecraftDefault-Regular.ttf", size)
+ fonts[key] = newFont
+ newFont
+ }
+ defaultFont = font
+ return font
+ }
+
+ fun isFontLoaded(path: String, size: Float = 128f) = fonts.containsKey("path@$size")
+
+ fun getLoadedFonts(): Set = fonts.keys.toSet()
+
+ /**
+ * Clean up all loaded fonts and release GPU resources.
+ * Call this when shutting down or when fonts are no longer needed.
+ */
+ fun cleanup() {
+ fonts.values.forEach { it.close() }
+ fonts.clear()
+ defaultFont = null
+ }
+}
diff --git a/src/main/kotlin/com/lambda/graphics/text/SDFFontAtlas.kt b/src/main/kotlin/com/lambda/graphics/text/SDFFontAtlas.kt
new file mode 100644
index 000000000..786184b61
--- /dev/null
+++ b/src/main/kotlin/com/lambda/graphics/text/SDFFontAtlas.kt
@@ -0,0 +1,746 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.graphics.text
+
+import com.lambda.Lambda.mc
+import com.lambda.util.stream
+import com.mojang.blaze3d.systems.RenderSystem
+import com.mojang.blaze3d.textures.FilterMode
+import com.mojang.blaze3d.textures.GpuTexture
+import com.mojang.blaze3d.textures.GpuTextureView
+import com.mojang.blaze3d.textures.TextureFormat
+import net.minecraft.client.gl.GpuSampler
+import net.minecraft.client.texture.NativeImage
+import org.lwjgl.stb.STBTTFontinfo
+import org.lwjgl.stb.STBTTVertex
+import org.lwjgl.stb.STBTruetype.STBTT_vcurve
+import org.lwjgl.stb.STBTruetype.STBTT_vline
+import org.lwjgl.stb.STBTruetype.STBTT_vmove
+import org.lwjgl.stb.STBTruetype.stbtt_FindGlyphIndex
+import org.lwjgl.stb.STBTruetype.stbtt_FreeShape
+import org.lwjgl.stb.STBTruetype.stbtt_GetFontVMetrics
+import org.lwjgl.stb.STBTruetype.stbtt_GetGlyphBitmapBox
+import org.lwjgl.stb.STBTruetype.stbtt_GetGlyphBox
+import org.lwjgl.stb.STBTruetype.stbtt_GetGlyphHMetrics
+import org.lwjgl.stb.STBTruetype.stbtt_GetGlyphShape
+import org.lwjgl.stb.STBTruetype.stbtt_InitFont
+import org.lwjgl.stb.STBTruetype.stbtt_ScaleForPixelHeight
+import org.lwjgl.system.MemoryStack
+import org.lwjgl.system.MemoryUtil
+import java.nio.ByteBuffer
+import kotlin.math.sqrt
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+
+/**
+ * Signed Distance Field font atlas for high-quality scalable text rendering.
+ *
+ * SDF fonts store the distance to the nearest edge instead of raw coverage,
+ * enabling crisp text at any scale with effects like outlines and glows.
+ *
+ * Uses MC 1.21's GpuTexture APIs for proper texture binding via RenderPass.bindTexture().
+ *
+ * @param fontPath Resource path to TTF/OTF file
+ * @param baseSize Base font size for SDF generation (larger = more detail, 48-64 recommended)
+ * @param sdfSpread SDF spread in pixels (how far the distance field extends)
+ * @param atlasSize Atlas texture dimensions (must be power of 2)
+ */
+class SDFFontAtlas(
+ fontPath: String,
+ val baseSize: Float = 256f,
+ val sdfSpread: Int = 16,
+ val atlasSize: Int = 4096
+) : AutoCloseable {
+
+ data class Glyph(
+ val codepoint: Int,
+ val width: Int,
+ val height: Int,
+ val bearingX: Float,
+ val bearingY: Float,
+ val advance: Float,
+ val u0: Float, val v0: Float,
+ val u1: Float, val v1: Float
+ )
+
+ /**
+ * Work unit for parallel glyph SDF generation.
+ * Contains all data needed to generate the SDF independently.
+ */
+ private data class GlyphJob(
+ val codepoint: Int,
+ val glyphIndex: Int,
+ val atlasX: Int,
+ val atlasY: Int,
+ val paddedW: Int,
+ val paddedH: Int,
+ val glyphW: Int,
+ val glyphH: Int,
+ val glyph: Glyph
+ )
+
+ private val fontBuffer: ByteBuffer
+ private val fontInfo: STBTTFontinfo
+ private var atlasData: ByteArray? = null
+ private val glyphs = mutableMapOf()
+
+ // MC 1.21 GPU texture objects
+ private var glTexture: GpuTexture? = null
+ private var glTextureView: GpuTextureView? = null
+ private var gpuSampler: GpuSampler? = null
+
+ val lineHeight: Float
+ val ascent: Float
+ val descent: Float
+ val scale: Float
+
+ /** The pixel range used for SDF, needed by shader for proper AA */
+ val sdfPixelRange: Float get() = (sdfSpread * 2).toFloat()
+
+ /** Get the texture view for binding in render pass */
+ val textureView: GpuTextureView? get() = glTextureView
+
+ /** Get the sampler for binding in render pass */
+ val sampler: GpuSampler? get() = gpuSampler
+
+ /** Check if texture is uploaded and ready */
+ val isUploaded: Boolean get() = glTexture != null
+
+ init {
+ // Load font file
+ val fontBytes = fontPath.stream.readAllBytes()
+ fontBuffer = MemoryUtil.memAlloc(fontBytes.size).put(fontBytes).flip()
+
+ fontInfo = STBTTFontinfo.create()
+ if (!stbtt_InitFont(fontInfo, fontBuffer)) {
+ MemoryUtil.memFree(fontBuffer)
+ throw RuntimeException("Failed to initialize font: $fontPath")
+ }
+
+ scale = stbtt_ScaleForPixelHeight(fontInfo, baseSize)
+
+ MemoryStack.stackPush().use { stack ->
+ val ascentBuf = stack.mallocInt(1)
+ val descentBuf = stack.mallocInt(1)
+ val lineGapBuf = stack.mallocInt(1)
+ stbtt_GetFontVMetrics(fontInfo, ascentBuf, descentBuf, lineGapBuf)
+
+ ascent = ascentBuf[0] * scale
+ descent = descentBuf[0] * scale
+ lineHeight = (ascentBuf[0] - descentBuf[0] + lineGapBuf[0]) * scale
+ }
+
+ atlasData = ByteArray(atlasSize * atlasSize)
+ buildSDFAtlas()
+ }
+
+ /**
+ * Build the SDF atlas using parallel glyph generation.
+ *
+ * Phase 1: Sequential layout - calculate glyph positions in the atlas
+ * Phase 2: Parallel generation - generate SDF for each glyph concurrently
+ */
+ private fun buildSDFAtlas() {
+ val data = atlasData ?: return
+ var penX = sdfSpread
+ var penY = sdfSpread
+ var rowHeight = 0
+
+ val codepoints = (32..126) + (160..255)
+ val jobs = mutableListOf()
+
+ // Phase 1: Calculate all glyph positions (sequential, fast)
+ MemoryStack.stackPush().use { stack ->
+ val x0 = stack.mallocInt(1)
+ val y0 = stack.mallocInt(1)
+ val x1 = stack.mallocInt(1)
+ val y1 = stack.mallocInt(1)
+ val advanceWidth = stack.mallocInt(1)
+ val leftSideBearing = stack.mallocInt(1)
+
+ for (cp in codepoints) {
+ val glyphIndex = stbtt_FindGlyphIndex(fontInfo, cp)
+ if (glyphIndex == 0 && cp != 32) continue
+
+ stbtt_GetGlyphHMetrics(fontInfo, glyphIndex, advanceWidth, leftSideBearing)
+ stbtt_GetGlyphBitmapBox(fontInfo, glyphIndex, scale, scale, x0, y0, x1, y1)
+
+ val glyphW = x1[0] - x0[0]
+ val glyphH = y1[0] - y0[0]
+ val paddedW = glyphW + sdfSpread * 2
+ val paddedH = glyphH + sdfSpread * 2
+
+ if (penX + paddedW >= atlasSize) {
+ penX = sdfSpread
+ penY += rowHeight + sdfSpread
+ rowHeight = 0
+ }
+
+ if (penY + paddedH >= atlasSize) {
+ System.err.println("SDF Atlas overflow at codepoint $cp")
+ break
+ }
+
+ val glyph = Glyph(
+ codepoint = cp,
+ width = paddedW,
+ height = paddedH,
+ bearingX = (x0[0] - sdfSpread) / baseSize,
+ bearingY = (-y0[0] + sdfSpread) / baseSize,
+ advance = advanceWidth[0] * scale / baseSize,
+ u0 = penX.toFloat() / atlasSize,
+ v0 = penY.toFloat() / atlasSize,
+ u1 = (penX + paddedW).toFloat() / atlasSize,
+ v1 = (penY + paddedH).toFloat() / atlasSize
+ )
+
+ glyphs[cp] = glyph
+
+ // Only create job if glyph has visible content
+ if (glyphW > 0 && glyphH > 0) {
+ jobs.add(GlyphJob(
+ codepoint = cp,
+ glyphIndex = glyphIndex,
+ atlasX = penX,
+ atlasY = penY,
+ paddedW = paddedW,
+ paddedH = paddedH,
+ glyphW = glyphW,
+ glyphH = glyphH,
+ glyph = glyph
+ ))
+ }
+
+ penX += paddedW + sdfSpread
+ rowHeight = maxOf(rowHeight, paddedH)
+ }
+ }
+
+ // Phase 2: Generate SDF for each glyph in parallel
+ runBlocking(Dispatchers.Default) {
+ for (job in jobs) {
+ launch {
+ generateGlyphSDF(
+ job.glyphIndex, data,
+ job.atlasX, job.atlasY,
+ job.paddedW, job.paddedH,
+ job.glyphW, job.glyphH
+ )
+ }
+ }
+ }
+ }
+
+ /**
+ * Generate vector-based SDF for a glyph.
+ * Computes distances directly from bezier curves for smooth edges.
+ */
+ private fun generateGlyphSDF(
+ glyphIndex: Int,
+ atlasData: ByteArray,
+ atlasX: Int, atlasY: Int,
+ paddedW: Int, paddedH: Int,
+ glyphW: Int, glyphH: Int
+ ) {
+ MemoryStack.stackPush().use { stack ->
+ // Get glyph bounding box in FONT UNITS
+ val boxX0 = stack.mallocInt(1)
+ val boxY0 = stack.mallocInt(1)
+ val boxX1 = stack.mallocInt(1)
+ val boxY1 = stack.mallocInt(1)
+ stbtt_GetGlyphBox(fontInfo, glyphIndex, boxX0, boxY0, boxX1, boxY1)
+
+ val fontX0 = boxX0[0].toFloat()
+ val fontY0 = boxY0[0].toFloat()
+ val fontX1 = boxX1[0].toFloat()
+ val fontY1 = boxY1[0].toFloat()
+ val fontWidth = fontX1 - fontX0
+ val fontHeight = fontY1 - fontY0
+
+ // Get glyph shape (bezier curves in font units)
+ val verticesPtr = stack.mallocPointer(1)
+ val numVertices = stbtt_GetGlyphShape(fontInfo, glyphIndex, verticesPtr)
+
+ if (numVertices <= 0 || fontWidth <= 0 || fontHeight <= 0) {
+ // Empty glyph (space, etc) - fill with "outside" value
+ for (py in 0 until paddedH) {
+ for (px in 0 until paddedW) {
+ val index = (atlasY + py) * atlasSize + atlasX + px
+ if (index >= 0 && index < atlasSize * atlasSize) {
+ atlasData[index] = 0
+ }
+ }
+ }
+ return
+ }
+
+ val vertices = STBTTVertex.create(verticesPtr[0], numVertices)
+
+ try {
+ // Extract curve segments from vertices (in font units)
+ val segments = mutableListOf()
+ var lastX = 0f
+ var lastY = 0f
+
+ for (i in 0 until numVertices) {
+ val v = vertices[i]
+ val type = v.type().toInt()
+ val x = v.x().toFloat()
+ val y = v.y().toFloat()
+
+ when (type) {
+ STBTT_vmove.toInt() -> {
+ lastX = x
+ lastY = y
+ }
+ STBTT_vline.toInt() -> {
+ segments.add(LineSegment(lastX, lastY, x, y))
+ lastX = x
+ lastY = y
+ }
+ STBTT_vcurve.toInt() -> {
+ val cx = v.cx().toFloat()
+ val cy = v.cy().toFloat()
+ segments.add(QuadraticBezier(lastX, lastY, cx, cy, x, y))
+ lastX = x
+ lastY = y
+ }
+ }
+ }
+
+ // Font units per pixel in the output
+ // The glyph area (without padding) maps to the font bounding box
+ val fontUnitsPerPixelX = fontWidth / glyphW
+ val fontUnitsPerPixelY = fontHeight / glyphH
+
+ // Compute SDF for each pixel in output
+ for (py in 0 until paddedH) {
+ for (px in 0 until paddedW) {
+ // Map output pixel to font units
+ // px, py are in padded coordinate space
+ // The glyph occupies pixels [sdfSpread, sdfSpread+glyphW) x [sdfSpread, sdfSpread+glyphH)
+ val gx = px - sdfSpread // Glyph-local X (0 to glyphW maps to fontX0 to fontX1)
+ val gy = py - sdfSpread // Glyph-local Y
+
+ // Convert to font units
+ // X: direct mapping
+ val fontX = fontX0 + gx * fontUnitsPerPixelX
+ // Y: font coords have Y up, screen coords have Y down
+ // gy=0 should map to fontY1 (top), gy=glyphH should map to fontY0 (bottom)
+ val fontY = fontY1 - gy * fontUnitsPerPixelY
+
+ // Find minimum distance to any curve segment (in font units)
+ var minDist = Float.MAX_VALUE
+ for (seg in segments) {
+ val d = seg.distance(fontX, fontY)
+ if (d < minDist) {
+ minDist = d
+ }
+ }
+
+ // Determine if inside or outside using winding number
+ val inside = computeWindingNumber(fontX, fontY, segments) != 0
+ val signedDist = if (inside) minDist else -minDist
+
+ // Convert distance from font units to pixels
+ val avgFontUnitsPerPixel = (fontUnitsPerPixelX + fontUnitsPerPixelY) / 2f
+ val pixelDist = signedDist / avgFontUnitsPerPixel
+
+ // Normalize: map [-sdfSpread, +sdfSpread] pixels to [0, 1]
+ val normalizedDist = (pixelDist / sdfSpread + 1f) * 0.5f
+ val value = (normalizedDist.coerceIn(0f, 1f) * 255).toInt().toByte()
+
+ val index = (atlasY + py) * atlasSize + atlasX + px
+ if (index >= 0 && index < atlasSize * atlasSize) {
+ atlasData[index] = value
+ }
+ }
+ }
+ } finally {
+ stbtt_FreeShape(fontInfo, vertices)
+ }
+ }
+ }
+
+ /** Curve segment interface */
+ private sealed interface CurveSegment {
+ fun distance(px: Float, py: Float): Float
+ }
+
+ /** Line segment */
+ private data class LineSegment(
+ val x0: Float, val y0: Float,
+ val x1: Float, val y1: Float
+ ) : CurveSegment {
+ override fun distance(px: Float, py: Float): Float {
+ val dx = x1 - x0
+ val dy = y1 - y0
+ val lenSq = dx * dx + dy * dy
+ if (lenSq < 1e-10f) return sqrt((px - x0) * (px - x0) + (py - y0) * (py - y0))
+
+ val t = ((px - x0) * dx + (py - y0) * dy) / lenSq
+ val tc = t.coerceIn(0f, 1f)
+ val nearX = x0 + tc * dx
+ val nearY = y0 + tc * dy
+ return sqrt((px - nearX) * (px - nearX) + (py - nearY) * (py - nearY))
+ }
+ }
+
+ /** Quadratic bezier curve */
+ private data class QuadraticBezier(
+ val x0: Float, val y0: Float,
+ val cx: Float, val cy: Float,
+ val x1: Float, val y1: Float
+ ) : CurveSegment {
+ override fun distance(px: Float, py: Float): Float {
+ // Use iterative refinement for accurate bezier distance
+ // First pass: coarse sampling to find approximate t
+ var bestT = 0f
+ var minDist = Float.MAX_VALUE
+
+ // Coarse pass: 32 samples
+ for (i in 0..32) {
+ val t = i / 32f
+ val d = distAtT(px, py, t)
+ if (d < minDist) {
+ minDist = d
+ bestT = t
+ }
+ }
+
+ // Refinement: search around bestT with smaller steps
+ val step = 1f / 64f
+ var tLo = (bestT - step * 2).coerceIn(0f, 1f)
+ var tHi = (bestT + step * 2).coerceIn(0f, 1f)
+
+ for (i in 0..16) {
+ val t = tLo + (tHi - tLo) * i / 16f
+ val d = distAtT(px, py, t)
+ if (d < minDist) {
+ minDist = d
+ bestT = t
+ }
+ }
+
+ return minDist
+ }
+
+ private fun distAtT(px: Float, py: Float, t: Float): Float {
+ val u = 1f - t
+ val bx = u * u * x0 + 2 * u * t * cx + t * t * x1
+ val by = u * u * y0 + 2 * u * t * cy + t * t * y1
+ return sqrt((px - bx) * (px - bx) + (py - by) * (py - by))
+ }
+
+ /** Get subdivided points for winding calculation */
+ fun getSubdividedPoints(numSegments: Int = 8): List> {
+ val points = mutableListOf>()
+ for (i in 0..numSegments) {
+ val t = i.toFloat() / numSegments
+ val u = 1f - t
+ val bx = u * u * x0 + 2 * u * t * cx + t * t * x1
+ val by = u * u * y0 + 2 * u * t * cy + t * t * y1
+ points.add(Pair(bx, by))
+ }
+ return points
+ }
+ }
+
+ /** Compute winding number to determine if point is inside the glyph */
+ private fun computeWindingNumber(px: Float, py: Float, segments: List): Int {
+ var winding = 0
+ for (seg in segments) {
+ when (seg) {
+ is LineSegment -> {
+ winding += windingForLine(px, py, seg.x0, seg.y0, seg.x1, seg.y1)
+ }
+ is QuadraticBezier -> {
+ // Subdivide bezier into line segments for accurate winding
+ val points = seg.getSubdividedPoints(8)
+ for (i in 0 until points.size - 1) {
+ val (ax, ay) = points[i]
+ val (bx, by) = points[i + 1]
+ winding += windingForLine(px, py, ax, ay, bx, by)
+ }
+ }
+ }
+ }
+ return winding
+ }
+
+ /** Compute winding contribution for a single line segment */
+ private fun windingForLine(px: Float, py: Float, x0: Float, y0: Float, x1: Float, y1: Float): Int {
+ if (y0 <= py) {
+ if (y1 > py) {
+ val cross = (x1 - x0) * (py - y0) - (px - x0) * (y1 - y0)
+ if (cross > 0) return 1
+ }
+ } else {
+ if (y1 <= py) {
+ val cross = (x1 - x0) * (py - y0) - (px - x0) * (y1 - y0)
+ if (cross < 0) return -1
+ }
+ }
+ return 0
+ }
+
+ /**
+ * Compute signed distance field using Euclidean Distance Transform (EDT).
+ * Uses the Felzenszwalb-Huttenlocher algorithm for O(n) linear time.
+ *
+ * @param coverage Grayscale values 0-1 where > 0.5 is "inside"
+ * @param width Image width
+ * @param height Image height
+ * @return Signed distance field (positive = inside, negative = outside)
+ */
+ private fun computeEDT(coverage: FloatArray, width: Int, height: Int): FloatArray {
+ val INF = 1e10f
+
+ // Create binary inside/outside arrays based on coverage threshold
+ val inside = FloatArray(width * height) { i ->
+ if (coverage[i] > 0.5f) 0f else INF
+ }
+ val outside = FloatArray(width * height) { i ->
+ if (coverage[i] <= 0.5f) 0f else INF
+ }
+
+ // Compute EDT for both inside and outside
+ edtTransform(inside, width, height)
+ edtTransform(outside, width, height)
+
+ // Combine into signed distance field
+ // distOutside - distInside: positive inside glyph, negative outside
+ val sdf = FloatArray(width * height)
+ for (i in 0 until width * height) {
+ val distInside = sqrt(inside[i])
+ val distOutside = sqrt(outside[i])
+ sdf[i] = distOutside - distInside
+ }
+
+ return sdf
+ }
+
+ /**
+ * 2D Euclidean Distance Transform using Felzenszwalb-Huttenlocher algorithm.
+ * Transforms the input array in-place to contain squared distances.
+ */
+ private fun edtTransform(data: FloatArray, width: Int, height: Int) {
+ val INF = 1e10f
+ val maxDim = maxOf(width, height)
+
+ // Temporary arrays for 1D transform
+ val f = FloatArray(maxDim)
+ val d = FloatArray(maxDim)
+ val v = IntArray(maxDim)
+ val z = FloatArray(maxDim + 1)
+
+ // Transform columns
+ for (x in 0 until width) {
+ for (y in 0 until height) {
+ f[y] = data[y * width + x]
+ }
+ edt1d(f, d, v, z, height)
+ for (y in 0 until height) {
+ data[y * width + x] = d[y]
+ }
+ }
+
+ // Transform rows
+ for (y in 0 until height) {
+ for (x in 0 until width) {
+ f[x] = data[y * width + x]
+ }
+ edt1d(f, d, v, z, width)
+ for (x in 0 until width) {
+ data[y * width + x] = d[x]
+ }
+ }
+ }
+
+ /**
+ * 1D squared Euclidean distance transform.
+ * f = input function, d = output distances
+ */
+ private fun edt1d(f: FloatArray, d: FloatArray, v: IntArray, z: FloatArray, n: Int) {
+ val INF = 1e10f
+ var k = 0
+ v[0] = 0
+ z[0] = -INF
+ z[1] = INF
+
+ for (q in 1 until n) {
+ var s = ((f[q] + q * q) - (f[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k])
+ while (s <= z[k]) {
+ k--
+ s = ((f[q] + q * q) - (f[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k])
+ }
+ k++
+ v[k] = q
+ z[k] = s
+ z[k + 1] = INF
+ }
+
+ k = 0
+ for (q in 0 until n) {
+ while (z[k + 1] < q) {
+ k++
+ }
+ val dist = q - v[k]
+ d[q] = dist * dist + f[v[k]]
+ }
+ }
+
+ /**
+ * Upload atlas to GPU using MC 1.21 APIs.
+ * Must be called on render thread.
+ */
+ fun upload() {
+ if (glTexture != null) return
+ val data = atlasData ?: return
+
+ RenderSystem.assertOnRenderThread()
+
+ val gpuDevice = RenderSystem.getDevice()
+
+ // Create RGBA8 texture - the shader samples red channel for SDF value
+ glTexture = gpuDevice.createTexture(
+ "Lambda SDF FontAtlas",
+ 5, // COPY_DST (1) | TEXTURE_BINDING (4)
+ TextureFormat.RGBA8,
+ atlasSize, atlasSize,
+ 1, 1
+ )
+
+ glTextureView = gpuDevice.createTextureView(glTexture)
+
+ // Use LINEAR filtering for smooth SDF interpolation
+ gpuSampler = RenderSystem.getSamplerCache().get(FilterMode.LINEAR)
+
+ // Create NativeImage with SDF value in alpha channel for transparency blending
+ // The position_tex_color shader multiplies texture.rgba by vertex color
+ // So we need SDF in alpha, with white RGB for the text color from vertex
+ val nativeImage = NativeImage(atlasSize, atlasSize, false)
+ for (y in 0 until atlasSize) {
+ for (x in 0 until atlasSize) {
+ val sdfValue = data[y * atlasSize + x].toInt() and 0xFF
+ // ABGR format: alpha=sdfValue, blue=255, green=255, red=255
+ // SDF in alpha allows proper transparency blending
+ val abgr = (sdfValue shl 24) or (255 shl 16) or (255 shl 8) or 255
+ nativeImage.setColor(x, y, abgr)
+ }
+ }
+
+ RenderSystem.getDevice().createCommandEncoder().writeToTexture(glTexture, nativeImage)
+ nativeImage.close()
+
+ atlasData = null
+ }
+
+ fun getGlyph(codepoint: Int): Glyph? = glyphs[codepoint]
+
+ private fun getStringWidth(text: String, fontSize: Float): Float {
+ var width = 0f
+ for (char in text) {
+ val glyph = glyphs[char.code] ?: glyphs[' '.code] ?: continue
+ width += glyph.advance * fontSize
+ }
+ return width
+ }
+
+ /** Get screen width in pixels (uses MC's scaled width). */
+ private val screenWidth: Float
+ get() = mc.window.scaledWidth.toFloat()
+
+ /** Get screen height in pixels (uses MC's scaled height). */
+ private val screenHeight: Float
+ get() = mc.window.scaledHeight.toFloat()
+
+ /**
+ * Get the width of text using normalized size (0-1 range, matching screenText).
+ * @param text The text string to measure
+ * @param normalizedSize Text size in normalized units (e.g., 0.02 = 2% of screen)
+ * @return Width in normalized units (0-1 range relative to screen width)
+ */
+ fun getStringWidthNormalized(text: String, normalizedSize: Float): Float {
+ // Apply the same baseSize/ascent correction that screenText uses
+ // so dimensions match what actually gets rendered
+ val targetPixelHeight = normalizedSize * screenHeight
+ val pixelSize = targetPixelHeight * baseSize / ascent
+ val pixelWidth = getStringWidth(text, pixelSize)
+ return pixelWidth / screenWidth
+ }
+
+ /**
+ * Get the descent using normalized size (0-1 range, matching screenText).
+ * @param normalizedSize Text size in normalized units
+ * @return Descent in normalized units (0-1 range relative to screen height)
+ */
+ fun getDescentNormalized(normalizedSize: Float): Float {
+ // descent / ascent = proportion of ascent that is descent
+ return normalizedSize * descent / ascent
+ }
+
+ /**
+ * Get both width and height of text using normalized size (0-1 range, matching screenText).
+ * @param text The text string to measure
+ * @param normalizedSize Text size in normalized units
+ * @return Pair of (width, height) in normalized units
+ */
+ fun getStringDimensionsNormalized(text: String, normalizedSize: Float): Pair {
+ return Pair(getStringWidthNormalized(text, normalizedSize), normalizedSize)
+ }
+
+ /**
+ * Get the normalized size needed to make text fit a target width.
+ * This is the inverse of getStringWidthNormalized.
+ * @param text The text string to measure
+ * @param targetWidthNormalized The desired width in normalized units (0-1 range relative to screen width)
+ * @return The normalized size that would produce the target width
+ */
+ fun getSizeForWidthNormalized(text: String, targetWidthNormalized: Float): Float {
+ // Calculate the raw advance width of the text (sum of glyph advances)
+ var rawAdvance = 0f
+ for (char in text) {
+ val glyph = glyphs[char.code] ?: glyphs[' '.code] ?: continue
+ rawAdvance += glyph.advance
+ }
+ if (rawAdvance <= 0f) return 0f
+
+ // Width formula from getStringWidthNormalized:
+ // targetPixelHeight = normalizedSize * screenHeight
+ // pixelSize = targetPixelHeight * baseSize / ascent
+ // pixelWidth = rawAdvance * pixelSize (since getStringWidth multiplies advance by fontSize)
+ // normalizedWidth = pixelWidth / screenWidth
+ //
+ // Solving for normalizedSize:
+ // normalizedWidth = (rawAdvance * normalizedSize * screenHeight * baseSize / ascent) / screenWidth
+ // normalizedSize = (normalizedWidth * screenWidth * ascent) / (rawAdvance * screenHeight * baseSize)
+ return (targetWidthNormalized * screenWidth * ascent) / (rawAdvance * screenHeight * baseSize)
+ }
+
+ override fun close() {
+ glTextureView?.close()
+ glTextureView = null
+ glTexture?.close()
+ glTexture = null
+ gpuSampler = null
+ atlasData = null
+ MemoryUtil.memFree(fontBuffer)
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/graphics/texture/LambdaImageAtlas.kt b/src/main/kotlin/com/lambda/graphics/texture/LambdaImageAtlas.kt
new file mode 100644
index 000000000..4f8f4d325
--- /dev/null
+++ b/src/main/kotlin/com/lambda/graphics/texture/LambdaImageAtlas.kt
@@ -0,0 +1,399 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.graphics.texture
+
+import com.lambda.Lambda.mc
+import com.mojang.blaze3d.systems.RenderSystem
+import com.mojang.blaze3d.textures.GpuTextureView
+import net.minecraft.client.texture.AbstractTexture
+import net.minecraft.client.texture.MissingSprite
+import net.minecraft.client.texture.Sprite
+import net.minecraft.client.texture.SpriteAtlasTexture
+import net.minecraft.item.ItemStack
+import net.minecraft.util.Identifier
+import java.awt.image.BufferedImage
+import java.nio.ByteBuffer
+import java.util.concurrent.ConcurrentHashMap
+
+/**
+ * Central service for loading and caching textures as GPU resources for Lambda's
+ * custom image rendering system.
+ *
+ * Provides access to:
+ * - Custom Lambda textures from resources
+ * - Minecraft textures (e.g., enchantment glint)
+ * - Item sprites from MC's atlas
+ *
+ * All textures are returned as [ImageEntry] which contains GPU texture views,
+ * UV coordinates, and dimensions for rendering.
+ */
+object LambdaImageAtlas {
+
+ /**
+ * Represents a renderable image entry with GPU resources and UV coordinates.
+ *
+ * @property textureView The GPU texture view for binding
+ * @property u0 Left UV coordinate (0-1)
+ * @property v0 Top UV coordinate (0-1)
+ * @property u1 Right UV coordinate (0-1)
+ * @property v1 Bottom UV coordinate (0-1)
+ * @property width Width in pixels (for aspect ratio calculations)
+ * @property height Height in pixels
+ */
+ data class ImageEntry(
+ val textureView: GpuTextureView,
+ val u0: Float,
+ val v0: Float,
+ val u1: Float,
+ val v1: Float,
+ val width: Int,
+ val height: Int
+ ) {
+ /** Aspect ratio (width / height) for maintaining proportions. */
+ val aspectRatio: Float get() = if (height != 0) width.toFloat() / height.toFloat() else 1f
+
+ /** Check if this is a full texture (UV spans 0-1). */
+ val isFullTexture: Boolean get() = u0 == 0f && v0 == 0f && u1 == 1f && v1 == 1f
+ }
+
+ // Cache for MC textures loaded by identifier
+ private val mcTextureCache = ConcurrentHashMap()
+
+ // Cached glint texture entry
+ private var glintEntry: ImageEntry? = null
+
+ // Cached missing texture entry
+ private var missingEntry: ImageEntry? = null
+
+ /**
+ * Load a Minecraft texture by its identifier.
+ *
+ * @param id The texture identifier (e.g., Identifier.ofVanilla("textures/misc/enchanted_glint_item.png"))
+ * @return ImageEntry for the texture, or null if not found
+ */
+ // Queue for textures requested from background threads
+ private val pendingLoadQueue = java.util.concurrent.ConcurrentLinkedQueue()
+
+ /**
+ * Load a Minecraft texture by its identifier.
+ *
+ * Thread-safe: Can be called from any thread.
+ * - If called from render thread: loads and caches immediately
+ * - If called from background thread: returns cached value if available,
+ * otherwise queues for loading and returns null
+ *
+ * @param id The texture identifier (e.g., Identifier.ofVanilla("textures/misc/enchanted_glint_item.png"))
+ * @return ImageEntry for the texture, or null if not found/not yet loaded
+ */
+ fun loadMCTexture(id: Identifier): ImageEntry? {
+ // Check cache first (thread-safe)
+ mcTextureCache[id]?.let { return it }
+
+ // If not on render thread, queue for loading and return null
+ if (!RenderSystem.isOnRenderThread()) {
+ pendingLoadQueue.add(id)
+ return null
+ }
+
+ // Process any pending loads while we're on the render thread
+ processPendingLoads()
+
+ return mcTextureCache.getOrPut(id) {
+ val textureManager = mc.textureManager
+ val texture: AbstractTexture = textureManager.getTexture(id) ?: return@getOrPut null
+
+ val gpuTextureView = texture.glTextureView ?: return@getOrPut null
+
+ // For full textures, we use default dimensions since GpuTexture dimensions are private
+ // The actual dimensions can be retrieved from the texture view's underlying texture
+ ImageEntry(
+ textureView = gpuTextureView,
+ u0 = 0f, v0 = 0f,
+ u1 = 1f, v1 = 1f,
+ width = 256, // Default size for standalone textures
+ height = 256
+ )
+ }
+ }
+
+ /**
+ * Process any pending texture load requests.
+ * Must be called from the render thread.
+ */
+ fun processPendingLoads() {
+ if (!RenderSystem.isOnRenderThread()) return
+
+ var id = pendingLoadQueue.poll()
+ while (id != null) {
+ // Try to load the texture (will add to cache if successful)
+ val textureManager = mc.textureManager
+ val texture: AbstractTexture? = textureManager.getTexture(id)
+
+ if (texture != null) {
+ val gpuTextureView = texture.glTextureView
+ if (gpuTextureView != null) {
+ mcTextureCache.putIfAbsent(id, ImageEntry(
+ textureView = gpuTextureView,
+ u0 = 0f, v0 = 0f,
+ u1 = 1f, v1 = 1f,
+ width = 256,
+ height = 256
+ ))
+ }
+ }
+ id = pendingLoadQueue.poll()
+ }
+ }
+
+ /**
+ * Get the enchantment glint texture for overlay rendering.
+ */
+ fun getGlintTexture(): ImageEntry? {
+ if (glintEntry != null) return glintEntry
+
+ val id = Identifier.ofVanilla("textures/misc/enchanted_glint_item.png")
+ glintEntry = loadMCTexture(id)
+ return glintEntry
+ }
+
+ /**
+ * Get the missing/error texture.
+ */
+ fun getMissingTexture(): ImageEntry? {
+ if (missingEntry != null) return missingEntry
+
+ val textureManager = mc.textureManager
+ val atlas = textureManager.getTexture(Identifier.ofVanilla("textures/atlas/blocks.png"))
+ as? SpriteAtlasTexture ?: return null
+ val sprite = atlas.getSprite(MissingSprite.getMissingSpriteId())
+
+ missingEntry = createEntryFromSprite(sprite, atlas)
+ return missingEntry
+ }
+
+ /**
+ * Get the sprite for an ItemStack from Minecraft's item atlas.
+ *
+ * This extracts the 2D item texture with proper UV coordinates
+ * for rendering flat item representations.
+ *
+ * @param stack The ItemStack to get the sprite for
+ * @return ImageEntry with sprite UV coordinates, or missing texture if not found
+ */
+ fun getItemSprite(stack: ItemStack): ImageEntry? {
+ if (stack.isEmpty) return getMissingTexture()
+
+ RenderSystem.assertOnRenderThread()
+
+ // Get the item's registry ID and use it to load the texture
+ val itemId = net.minecraft.registry.Registries.ITEM.getId(stack.item)
+ return loadItemTexture(itemId) ?: getMissingTexture()
+ }
+
+ /**
+ * Load an item texture by trying common texture paths.
+ *
+ * This is useful for simple items where you know the item ID.
+ * For block items, it will try both item and block texture paths.
+ *
+ * @param itemId The item's registry ID (e.g., Identifier.ofVanilla("diamond"))
+ * @return ImageEntry for the texture, or null if not found
+ */
+ fun loadItemTexture(itemId: Identifier): ImageEntry? {
+ RenderSystem.assertOnRenderThread()
+
+ // Try item texture first (most common)
+ val itemTextureId = Identifier.of(itemId.namespace, "textures/item/${itemId.path}.png")
+ val itemEntry = loadMCTexture(itemTextureId)
+ if (itemEntry != null) return itemEntry
+
+ // Try block texture for block items
+ val blockTextureId = Identifier.of(itemId.namespace, "textures/block/${itemId.path}.png")
+ return loadMCTexture(blockTextureId)
+ }
+
+ /**
+ * Check if an ItemStack has enchantment glint.
+ */
+ fun hasGlint(stack: ItemStack): Boolean {
+ return stack.hasGlint()
+ }
+
+ /**
+ * Create an ImageEntry from a Minecraft Sprite.
+ */
+ private fun createEntryFromSprite(sprite: Sprite, atlas: SpriteAtlasTexture): ImageEntry? {
+ val gpuTextureView = atlas.glTextureView ?: return null
+
+ return ImageEntry(
+ textureView = gpuTextureView,
+ u0 = sprite.minU,
+ v0 = sprite.minV,
+ u1 = sprite.maxU,
+ v1 = sprite.maxV,
+ width = sprite.contents.width,
+ height = sprite.contents.height
+ )
+ }
+
+ // ============================================================================
+ // Custom Texture Upload
+ // ============================================================================
+
+ // Cache for uploaded custom textures
+ private val uploadedTextureCache = ConcurrentHashMap()
+ private var nextTextureId = 0
+
+ /**
+ * Represents an uploaded custom texture with its GPU resources.
+ */
+ data class UploadedTexture(
+ val id: String,
+ val texture: net.minecraft.client.texture.NativeImageBackedTexture,
+ val entry: ImageEntry
+ )
+
+ /**
+ * Upload a BufferedImage as a texture for rendering.
+ * The texture is cached and can be reused.
+ *
+ * @param image The BufferedImage to upload
+ * @param name Optional name for caching (if same name is used, returns cached version)
+ * @return ImageEntry for the uploaded texture
+ */
+ fun upload(image: BufferedImage, name: String? = null): ImageEntry? {
+ RenderSystem.assertOnRenderThread()
+
+ val cacheKey = name ?: "uploaded_${nextTextureId++}"
+
+ // Check cache first
+ uploadedTextureCache[cacheKey]?.let { return it.entry }
+
+ // Convert BufferedImage to NativeImage
+ val nativeImage = net.minecraft.client.texture.NativeImage(image.width, image.height, true)
+ for (y in 0 until image.height) {
+ for (x in 0 until image.width) {
+ val argb = image.getRGB(x, y)
+ // Convert ARGB to ABGR (NativeImage uses ABGR)
+ val a = (argb shr 24) and 0xFF
+ val r = (argb shr 16) and 0xFF
+ val g = (argb shr 8) and 0xFF
+ val b = argb and 0xFF
+ val abgr = (a shl 24) or (b shl 16) or (g shl 8) or r
+ nativeImage.setColor(x, y, abgr)
+ }
+ }
+
+ // Create texture and upload
+ val nativeTexture = net.minecraft.client.texture.NativeImageBackedTexture({ cacheKey }, nativeImage)
+ val textureId = Identifier.of("lambda", "uploaded/$cacheKey")
+
+ // Get texture view via texture manager after registration
+ mc.textureManager.registerTexture(textureId, nativeTexture)
+ val gpuTextureView = nativeTexture.glTextureView ?: return null
+
+ val entry = ImageEntry(
+ textureView = gpuTextureView,
+ u0 = 0f, v0 = 0f,
+ u1 = 1f, v1 = 1f,
+ width = image.width,
+ height = image.height
+ )
+
+ uploadedTextureCache[cacheKey] = UploadedTexture(cacheKey, nativeTexture, entry)
+ return entry
+ }
+
+ /**
+ * Upload a ByteBuffer as a texture for rendering.
+ * The buffer should contain RGBA pixel data.
+ *
+ * @param buffer The ByteBuffer containing RGBA pixel data
+ * @param width Width of the image in pixels
+ * @param height Height of the image in pixels
+ * @param name Optional name for caching
+ * @return ImageEntry for the uploaded texture
+ */
+ fun upload(buffer: ByteBuffer, width: Int, height: Int, name: String? = null): ImageEntry? {
+ RenderSystem.assertOnRenderThread()
+
+ val cacheKey = name ?: "uploaded_${nextTextureId++}"
+
+ // Check cache first
+ uploadedTextureCache[cacheKey]?.let { return it.entry }
+
+ // Create NativeImage from ByteBuffer
+ val nativeImage = net.minecraft.client.texture.NativeImage(width, height, true)
+ buffer.rewind()
+ for (y in 0 until height) {
+ for (x in 0 until width) {
+ val r = buffer.get().toInt() and 0xFF
+ val g = buffer.get().toInt() and 0xFF
+ val b = buffer.get().toInt() and 0xFF
+ val a = buffer.get().toInt() and 0xFF
+ // NativeImage uses ABGR
+ val abgr = (a shl 24) or (b shl 16) or (g shl 8) or r
+ nativeImage.setColor(x, y, abgr)
+ }
+ }
+
+ // Create texture and upload
+ val nativeTexture = net.minecraft.client.texture.NativeImageBackedTexture({ cacheKey }, nativeImage)
+ val textureId = Identifier.of("lambda", "uploaded/$cacheKey")
+ mc.textureManager.registerTexture(textureId, nativeTexture)
+ val gpuTextureView = nativeTexture.glTextureView ?: return null
+
+ val entry = ImageEntry(
+ textureView = gpuTextureView,
+ u0 = 0f, v0 = 0f,
+ u1 = 1f, v1 = 1f,
+ width = width,
+ height = height
+ )
+
+ uploadedTextureCache[cacheKey] = UploadedTexture(cacheKey, nativeTexture, entry)
+ return entry
+ }
+
+ /**
+ * Remove an uploaded texture from cache and release GPU resources.
+ *
+ * @param name The name used when uploading the texture
+ */
+ fun removeUploaded(name: String) {
+ uploadedTextureCache.remove(name)?.let { uploaded ->
+ mc.textureManager.destroyTexture(Identifier.of("lambda", "uploaded/$name"))
+ }
+ }
+
+ /**
+ * Clear all cached textures. Should be called on resource reload.
+ */
+ fun clearCache() {
+ mcTextureCache.clear()
+ glintEntry = null
+ missingEntry = null
+
+ // Clean up uploaded textures
+ uploadedTextureCache.values.forEach { uploaded ->
+ mc.textureManager.destroyTexture(Identifier.of("lambda", "uploaded/${uploaded.id}"))
+ }
+ uploadedTextureCache.clear()
+ }
+}
+
diff --git a/src/main/kotlin/com/lambda/graphics/renderer/esp/DirectionMask.kt b/src/main/kotlin/com/lambda/graphics/util/DirectionMask.kt
similarity index 98%
rename from src/main/kotlin/com/lambda/graphics/renderer/esp/DirectionMask.kt
rename to src/main/kotlin/com/lambda/graphics/util/DirectionMask.kt
index 1b510e3b1..d6bbd37a1 100644
--- a/src/main/kotlin/com/lambda/graphics/renderer/esp/DirectionMask.kt
+++ b/src/main/kotlin/com/lambda/graphics/util/DirectionMask.kt
@@ -15,7 +15,7 @@
* along with this program. If not, see .
*/
-package com.lambda.graphics.renderer.esp
+package com.lambda.graphics.util
import com.lambda.util.world.FastVector
import com.lambda.util.world.offset
diff --git a/src/main/kotlin/com/lambda/graphics/renderer/esp/DynamicAABB.kt b/src/main/kotlin/com/lambda/graphics/util/DynamicAABB.kt
similarity index 74%
rename from src/main/kotlin/com/lambda/graphics/renderer/esp/DynamicAABB.kt
rename to src/main/kotlin/com/lambda/graphics/util/DynamicAABB.kt
index 159b32268..f7bd7a3a4 100644
--- a/src/main/kotlin/com/lambda/graphics/renderer/esp/DynamicAABB.kt
+++ b/src/main/kotlin/com/lambda/graphics/util/DynamicAABB.kt
@@ -15,9 +15,12 @@
* along with this program. If not, see .
*/
-package com.lambda.graphics.renderer.esp
+package com.lambda.graphics.util
+import com.lambda.Lambda.mc
import com.lambda.util.extension.prevPos
+import com.lambda.util.extension.tickDelta
+import com.lambda.util.math.lerp
import com.lambda.util.math.minus
import net.minecraft.entity.Entity
import net.minecraft.util.math.Box
@@ -35,12 +38,23 @@ class DynamicAABB {
return this
}
+ fun box(tickDelta: Double): Box? =
+ prev?.let { prev ->
+ curr?.let { curr ->
+ lerp(tickDelta, prev, curr)
+ }
+ }
+
fun reset() {
prev = null
curr = null
}
companion object {
+ val Entity.interpolatedBox
+ get() = boundingBox.let { box ->
+ lerp(mc.tickDelta, box.offset(prevPos - pos), box)
+ }
val Entity.dynamicBox
get() = DynamicAABB().apply {
update(boundingBox.offset(prevPos - pos))
diff --git a/src/main/kotlin/com/lambda/gui/DearImGui.kt b/src/main/kotlin/com/lambda/gui/DearImGui.kt
index 770468d52..7baae282d 100644
--- a/src/main/kotlin/com/lambda/gui/DearImGui.kt
+++ b/src/main/kotlin/com/lambda/gui/DearImGui.kt
@@ -36,7 +36,6 @@ import imgui.glfw.ImGuiImplGlfw
import net.minecraft.client.gl.GlBackend
import net.minecraft.client.texture.GlTexture
import org.lwjgl.opengl.GL30.GL_FRAMEBUFFER
-import org.lwjgl.opengl.GL32C
import kotlin.math.abs
object DearImGui : Loadable {
diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/Simulation.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/Simulation.kt
index 7785c578e..fdf2fd14e 100644
--- a/src/main/kotlin/com/lambda/interaction/construction/simulation/Simulation.kt
+++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/Simulation.kt
@@ -19,12 +19,12 @@ package com.lambda.interaction.construction.simulation
import com.lambda.context.Automated
import com.lambda.context.SafeContext
-import com.lambda.graphics.esp.ShapeScope
-import com.lambda.graphics.mc.TransientRegionESP
+import com.lambda.graphics.mc.RenderBuilder
+import com.lambda.graphics.mc.renderer.TickedRenderer
import com.lambda.interaction.construction.blueprint.Blueprint
+import com.lambda.interaction.construction.simulation.BuildSimulator.simulate
import com.lambda.interaction.construction.simulation.result.BuildResult
import com.lambda.interaction.construction.simulation.result.Drawable
-import com.lambda.interaction.construction.simulation.BuildSimulator.simulate
import com.lambda.threading.runSafeAutomated
import com.lambda.util.BlockUtils.blockState
import com.lambda.util.world.FastVector
@@ -64,9 +64,9 @@ data class Simulation(
.map { PossiblePos(it.key.toBlockPos(), it.value.count { it.rank.ordinal < 4 }) }
class PossiblePos(val pos: BlockPos, val interactions: Int) : Drawable {
- override fun render(esp: TransientRegionESP) {
- esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) {
- box(Vec3d.ofBottomCenter(pos).playerBox(), Color(0, 255, 0, 50), Color(0, 255, 0, 50))
+ override fun RenderBuilder.render() {
+ box(Vec3d.ofBottomCenter(pos).playerBox()) {
+ colors(Color(0, 255, 0, 50), Color(0, 255, 0, 50))
}
}
}
diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/context/BreakContext.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/context/BreakContext.kt
index 17ecce3ef..6d58ea018 100644
--- a/src/main/kotlin/com/lambda/interaction/construction/simulation/context/BreakContext.kt
+++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/context/BreakContext.kt
@@ -18,7 +18,8 @@
package com.lambda.interaction.construction.simulation.context
import com.lambda.context.Automated
-import com.lambda.graphics.mc.TransientRegionESP
+import com.lambda.graphics.mc.RenderBuilder
+import com.lambda.graphics.mc.renderer.TickedRenderer
import com.lambda.interaction.managers.rotating.RotationRequest
import com.lambda.interaction.material.StackSelection
import com.lambda.threading.runSafe
@@ -59,9 +60,9 @@ data class BreakContext(
override val sorter get() = breakConfig.sorter
- override fun render(esp: TransientRegionESP) {
- esp.shapes(blockPos.x.toDouble(), blockPos.y.toDouble(), blockPos.z.toDouble()) {
- box(blockPos, baseColor, sideColor)
+ override fun RenderBuilder.render() {
+ box(blockPos) {
+ colors(baseColor, sideColor)
}
}
}
diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/context/InteractContext.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/context/InteractContext.kt
index cd78d3f52..8ee811ad0 100644
--- a/src/main/kotlin/com/lambda/interaction/construction/simulation/context/InteractContext.kt
+++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/context/InteractContext.kt
@@ -18,7 +18,8 @@
package com.lambda.interaction.construction.simulation.context
import com.lambda.context.Automated
-import com.lambda.graphics.mc.TransientRegionESP
+import com.lambda.graphics.mc.RenderBuilder
+import com.lambda.graphics.mc.renderer.TickedRenderer
import com.lambda.interaction.construction.simulation.processing.PreProcessingInfo
import com.lambda.interaction.managers.hotbar.HotbarRequest
import com.lambda.interaction.managers.interacting.InteractRequest
@@ -46,15 +47,15 @@ data class InteractContext(
override val sorter get() = interactConfig.sorter
- override fun render(esp: TransientRegionESP) {
- esp.shapes(hitResult.pos.x, hitResult.pos.y, hitResult.pos.z) {
- val box = with(hitResult.pos) {
- Box(
- x - 0.05, y - 0.05, z - 0.05,
- x + 0.05, y + 0.05, z + 0.05,
- ).offset(hitResult.side.doubleVector.multiply(0.05))
- }
- box(box, baseColor, sideColor)
+ override fun RenderBuilder.render() {
+ val box = with(hitResult.pos) {
+ Box(
+ x - 0.05, y - 0.05, z - 0.05,
+ x + 0.05, y + 0.05, z + 0.05,
+ ).offset(hitResult.side.doubleVector.multiply(0.05))
+ }
+ box(box) {
+ colors(baseColor, sideColor)
}
}
diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/result/Drawable.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/result/Drawable.kt
index ac339712a..b140940a6 100644
--- a/src/main/kotlin/com/lambda/interaction/construction/simulation/result/Drawable.kt
+++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/result/Drawable.kt
@@ -17,11 +17,12 @@
package com.lambda.interaction.construction.simulation.result
-import com.lambda.graphics.mc.TransientRegionESP
+import com.lambda.graphics.mc.RenderBuilder
+import com.lambda.graphics.mc.renderer.TickedRenderer
/**
* Represents a [BuildResult] that can be rendered in-game.
*/
interface Drawable {
- fun render(esp: TransientRegionESP)
+ fun RenderBuilder.render()
}
diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/BreakResult.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/BreakResult.kt
index ed85fd9e8..472088bb2 100644
--- a/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/BreakResult.kt
+++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/BreakResult.kt
@@ -20,8 +20,9 @@ package com.lambda.interaction.construction.simulation.result.results
import baritone.api.pathing.goals.GoalBlock
import baritone.api.pathing.goals.GoalInverted
import com.lambda.context.AutomatedSafeContext
-import com.lambda.graphics.mc.TransientRegionESP
-import com.lambda.graphics.renderer.esp.DirectionMask.mask
+import com.lambda.graphics.mc.RenderBuilder
+import com.lambda.graphics.mc.renderer.TickedRenderer
+import com.lambda.graphics.util.DirectionMask.mask
import com.lambda.interaction.construction.simulation.context.BreakContext
import com.lambda.interaction.construction.simulation.result.BuildResult
import com.lambda.interaction.construction.simulation.result.ComparableResult
@@ -55,8 +56,8 @@ sealed class BreakResult : BuildResult() {
) : Contextual, Drawable, BreakResult() {
override val rank = Rank.BreakSuccess
- override fun render(esp: TransientRegionESP) {
- context.render(esp)
+ override fun RenderBuilder.render() {
+ with(context) { render() }
}
}
@@ -72,9 +73,10 @@ sealed class BreakResult : BuildResult() {
override val rank = Rank.BreakNotExposed
private val color = Color(46, 0, 0, 30)
- override fun render(esp: TransientRegionESP) {
- esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) {
- box(pos, color, color, side.mask)
+ override fun RenderBuilder.render() {
+ box(pos) {
+ allColors(color)
+ hideSides(side.mask.inv())
}
}
@@ -122,9 +124,9 @@ sealed class BreakResult : BuildResult() {
override val rank = Rank.BreakSubmerge
private val color = Color(114, 27, 255, 100)
- override fun render(esp: TransientRegionESP) {
- esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) {
- box(pos, color, color)
+ override fun RenderBuilder.render() {
+ box(pos) {
+ allColors(color)
}
}
}
@@ -140,14 +142,14 @@ sealed class BreakResult : BuildResult() {
override val rank = Rank.BreakIsBlockedByFluid
private val color = Color(50, 12, 112, 100)
- override fun render(esp: TransientRegionESP) {
- esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) {
- val center = pos.toCenterPos()
- val box = Box(
- center.x - 0.1, center.y - 0.1, center.z - 0.1,
- center.x + 0.1, center.y + 0.1, center.z + 0.1
- )
- box(box, color, color)
+ override fun RenderBuilder.render() {
+ val center = pos.toCenterPos()
+ val box = Box(
+ center.x - 0.1, center.y - 0.1, center.z - 0.1,
+ center.x + 0.1, center.y + 0.1, center.z + 0.1
+ )
+ box(box) {
+ allColors(color)
}
}
}
@@ -164,9 +166,9 @@ sealed class BreakResult : BuildResult() {
override val goal = GoalInverted(GoalBlock(pos))
- override fun render(esp: TransientRegionESP) {
- esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) {
- box(pos, color, color)
+ override fun RenderBuilder.render() {
+ box(pos) {
+ allColors(color)
}
}
}
diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/GenericResult.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/GenericResult.kt
index 0694c3c6d..3c5fd125f 100644
--- a/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/GenericResult.kt
+++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/GenericResult.kt
@@ -19,7 +19,8 @@ package com.lambda.interaction.construction.simulation.result.results
import baritone.api.pathing.goals.GoalNear
import com.lambda.context.AutomatedSafeContext
-import com.lambda.graphics.mc.TransientRegionESP
+import com.lambda.graphics.mc.RenderBuilder
+import com.lambda.graphics.mc.renderer.TickedRenderer
import com.lambda.interaction.construction.simulation.result.BuildResult
import com.lambda.interaction.construction.simulation.result.ComparableResult
import com.lambda.interaction.construction.simulation.result.Drawable
@@ -53,15 +54,15 @@ sealed class GenericResult : BuildResult() {
override val rank = Rank.NotVisible
private val color = Color(46, 0, 0, 80)
- override fun render(esp: TransientRegionESP) {
- esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) {
- val box = with(pos) {
- Box(
- x - 0.05, y - 0.05, z - 0.05,
- x + 0.05, y + 0.05, z + 0.05,
- ).offset(pos)
- }
- box(box, color, color)
+ override fun RenderBuilder.render() {
+ val box = with(pos) {
+ Box(
+ x - 0.05, y - 0.05, z - 0.05,
+ x + 0.05, y + 0.05, z + 0.05,
+ ).offset(pos)
+ }
+ box(box) {
+ allColors(color)
}
}
@@ -102,14 +103,14 @@ sealed class GenericResult : BuildResult() {
neededSelection.transferByTask(HotbarContainer)?.execute(task)
}
- override fun render(esp: TransientRegionESP) {
- esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) {
- val center = pos.toCenterPos()
- val box = Box(
- center.x - 0.1, center.y - 0.1, center.z - 0.1,
- center.x + 0.1, center.y + 0.1, center.z + 0.1
- )
- box(box, color, color)
+ override fun RenderBuilder.render() {
+ val center = pos.toCenterPos()
+ val box = Box(
+ center.x - 0.1, center.y - 0.1, center.z - 0.1,
+ center.x + 0.1, center.y + 0.1, center.z + 0.1
+ )
+ box(box) {
+ allColors(color)
}
}
}
@@ -135,14 +136,14 @@ sealed class GenericResult : BuildResult() {
override val goal = GoalNear(pos, 3)
- override fun render(esp: TransientRegionESP) {
- esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) {
- val center = pos.toCenterPos()
- val box = Box(
- center.x - 0.1, center.y - 0.1, center.z - 0.1,
- center.x + 0.1, center.y + 0.1, center.z + 0.1
- )
- box(box, color, color)
+ override fun RenderBuilder.render() {
+ val center = pos.toCenterPos()
+ val box = Box(
+ center.x - 0.1, center.y - 0.1, center.z - 0.1,
+ center.x + 0.1, center.y + 0.1, center.z + 0.1
+ )
+ box(box) {
+ allColors(color)
}
}
diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/InteractResult.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/InteractResult.kt
index b4537bcec..b72feb920 100644
--- a/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/InteractResult.kt
+++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/InteractResult.kt
@@ -19,7 +19,8 @@ package com.lambda.interaction.construction.simulation.result.results
import baritone.api.pathing.goals.GoalBlock
import baritone.api.pathing.goals.GoalInverted
-import com.lambda.graphics.mc.TransientRegionESP
+import com.lambda.graphics.mc.RenderBuilder
+import com.lambda.graphics.mc.renderer.TickedRenderer
import com.lambda.interaction.construction.simulation.context.InteractContext
import com.lambda.interaction.construction.simulation.result.BuildResult
import com.lambda.interaction.construction.simulation.result.Contextual
@@ -57,8 +58,8 @@ sealed class InteractResult : BuildResult() {
) : Contextual, Drawable, InteractResult() {
override val rank = Rank.PlaceSuccess
- override fun render(esp: TransientRegionESP) {
- context.render(esp)
+ override fun RenderBuilder.render() {
+ with(context) { render() }
}
}
@@ -82,15 +83,15 @@ sealed class InteractResult : BuildResult() {
override val rank = Rank.PlaceNoIntegrity
private val color = Color(252, 3, 3, 100)
- override fun render(esp: TransientRegionESP) {
- esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) {
- val box = with(simulated.hitPos) {
- Box(
- x - 0.05, y - 0.05, z - 0.05,
- x + 0.05, y + 0.05, z + 0.05,
- ).offset(simulated.side.doubleVector.multiply(0.05))
- }
- box(box, color, color)
+ override fun RenderBuilder.render() {
+ val box = with(simulated.hitPos) {
+ Box(
+ x - 0.05, y - 0.05, z - 0.05,
+ x + 0.05, y + 0.05, z + 0.05,
+ ).offset(simulated.side.doubleVector.multiply(0.05))
+ }
+ box(box) {
+ allColors(color)
}
}
}
@@ -121,15 +122,15 @@ sealed class InteractResult : BuildResult() {
override val rank = Rank.PlaceBlockedByEntity
private val color = Color(252, 3, 3, 100)
- override fun render(esp: TransientRegionESP) {
- esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) {
- val box = with(hitPos) {
- Box(
- x - 0.05, y - 0.05, z - 0.05,
- x + 0.05, y + 0.05, z + 0.05,
- ).offset(side.doubleVector.multiply(0.05))
- }
- box(box, color, color)
+ override fun RenderBuilder.render() {
+ val box = with(hitPos) {
+ Box(
+ x - 0.05, y - 0.05, z - 0.05,
+ x + 0.05, y + 0.05, z + 0.05,
+ ).offset(side.doubleVector.multiply(0.05))
+ }
+ box(box) {
+ allColors(color)
}
}
}
diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/PreSimResult.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/PreSimResult.kt
index baa2a2513..db73c3773 100644
--- a/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/PreSimResult.kt
+++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/PreSimResult.kt
@@ -18,8 +18,8 @@
package com.lambda.interaction.construction.simulation.result.results
import baritone.api.pathing.goals.GoalBlock
-import com.lambda.graphics.esp.ShapeScope
-import com.lambda.graphics.mc.TransientRegionESP
+import com.lambda.graphics.mc.RenderBuilder
+import com.lambda.graphics.mc.renderer.TickedRenderer
import com.lambda.interaction.construction.simulation.result.BuildResult
import com.lambda.interaction.construction.simulation.result.ComparableResult
import com.lambda.interaction.construction.simulation.result.Drawable
@@ -56,9 +56,9 @@ sealed class PreSimResult : BuildResult() {
override val goal = GoalBlock(pos)
- override fun render(esp: TransientRegionESP) {
- esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) {
- box(pos, color, color)
+ override fun RenderBuilder.render() {
+ box(pos) {
+ allColors(color)
}
}
@@ -80,9 +80,9 @@ sealed class PreSimResult : BuildResult() {
override val rank = Rank.BreakRestricted
private val color = Color(255, 0, 0, 100)
- override fun render(esp: TransientRegionESP) {
- esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) {
- box(pos, color, color)
+ override fun RenderBuilder.render() {
+ box(pos) {
+ allColors(color)
}
}
}
@@ -100,9 +100,9 @@ sealed class PreSimResult : BuildResult() {
override val rank get() = Rank.BreakNoPermission
private val color = Color(255, 0, 0, 100)
- override fun render(esp: TransientRegionESP) {
- esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) {
- box(pos, color, color)
+ override fun RenderBuilder.render() {
+ box(pos) {
+ allColors(color)
}
}
}
@@ -118,9 +118,9 @@ sealed class PreSimResult : BuildResult() {
override val rank = Rank.OutOfWorld
private val color = Color(3, 148, 252, 100)
- override fun render(esp: TransientRegionESP) {
- esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) {
- box(pos, color, color)
+ override fun RenderBuilder.render() {
+ box(pos) {
+ allColors(color)
}
}
}
@@ -138,9 +138,9 @@ sealed class PreSimResult : BuildResult() {
override val rank = Rank.Unbreakable
private val color = Color(11, 11, 11, 100)
- override fun render(esp: TransientRegionESP) {
- esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) {
- box(pos, color, color)
+ override fun RenderBuilder.render() {
+ box(pos) {
+ allColors(color)
}
}
}
diff --git a/src/main/kotlin/com/lambda/interaction/managers/breaking/BreakConfig.kt b/src/main/kotlin/com/lambda/interaction/managers/breaking/BreakConfig.kt
index 2cba3128b..ce6f35f3c 100644
--- a/src/main/kotlin/com/lambda/interaction/managers/breaking/BreakConfig.kt
+++ b/src/main/kotlin/com/lambda/interaction/managers/breaking/BreakConfig.kt
@@ -73,7 +73,7 @@ interface BreakConfig : ActionConfig, ISettingGroup {
val renders: Boolean
val fill: Boolean
val outline: Boolean
- val outlineWidth: Int
+ val outlineWidth: Float
val animation: AnimationMode
val dynamicFillColor: Boolean
diff --git a/src/main/kotlin/com/lambda/interaction/managers/breaking/BreakManager.kt b/src/main/kotlin/com/lambda/interaction/managers/breaking/BreakManager.kt
index b985c82e9..a1c44be64 100644
--- a/src/main/kotlin/com/lambda/interaction/managers/breaking/BreakManager.kt
+++ b/src/main/kotlin/com/lambda/interaction/managers/breaking/BreakManager.kt
@@ -23,10 +23,9 @@ import com.lambda.event.events.ConnectionEvent
import com.lambda.event.events.EntityEvent
import com.lambda.event.events.TickEvent
import com.lambda.event.events.WorldEvent
-import com.lambda.event.events.onDynamicRender
import com.lambda.event.listener.SafeListener.Companion.listen
import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafe
-import com.lambda.graphics.renderer.esp.DynamicAABB
+import com.lambda.graphics.mc.renderer.ImmediateRenderer.Companion.immediateRenderer
import com.lambda.interaction.construction.blueprint.Blueprint.Companion.toStructure
import com.lambda.interaction.construction.simulation.BuildSimulator.simulate
import com.lambda.interaction.construction.simulation.context.BreakContext
@@ -77,7 +76,8 @@ import com.lambda.util.BlockUtils.calcItemBlockBreakingDelta
import com.lambda.util.BlockUtils.isEmpty
import com.lambda.util.BlockUtils.isNotBroken
import com.lambda.util.BlockUtils.isNotEmpty
-import com.lambda.util.extension.partialTicks
+import com.lambda.util.ChatUtils.colors
+import com.lambda.util.extension.tickDelta
import com.lambda.util.item.ItemUtils.block
import com.lambda.util.math.lerp
import com.lambda.util.player.gamemode
@@ -218,71 +218,70 @@ object BreakManager : Manager(
?.internalOnItemDrop(it.entity)
}
- onDynamicRender { esp ->
- val activeStack = breakInfos
- .filterNotNull()
- .firstOrNull()?.swapStack ?: return@onDynamicRender
+ listenUnsafe(priority = Int.MIN_VALUE) {
+ primaryBreak = null
+ secondaryBreak = null
+ breakCooldown = 0
+ }
- breakInfos
- .filterNotNull()
- .forEach { info ->
- if (!info.breaking) return@forEach
-
- val config = info.breakConfig
- if (!config.renders) return@onDynamicRender
- val swapMode = info.breakConfig.swapMode
- val breakDelta = info.request.runSafeAutomated {
- info.context.cachedState.calcBreakDelta(
- info.context.blockPos,
- if (info.type != RedundantSecondary &&
- swapMode.isEnabled() &&
- swapMode != BreakConfig.SwapMode.Start
+ immediateRenderer("BreakManager Immediate Renderer") { safeContext ->
+ with(safeContext) {
+ val activeStack = breakInfos
+ .filterNotNull()
+ .firstOrNull()?.swapStack ?: return@immediateRenderer
+
+ breakInfos
+ .filterNotNull()
+ .forEach { info ->
+ if (!info.breaking) return@forEach
+
+ val config = info.breakConfig
+ if (!config.renders) return@immediateRenderer
+ val swapMode = info.breakConfig.swapMode
+ val breakDelta = info.request.runSafeAutomated {
+ info.context.cachedState.calcBreakDelta(
+ info.context.blockPos,
+ if (info.type != RedundantSecondary &&
+ swapMode.isEnabled() &&
+ swapMode != BreakConfig.SwapMode.Start
) activeStack
- else null
- ).toDouble()
- }
- val currentDelta = info.breakingTicks * breakDelta
-
- val threshold = if (info.type == Primary) info.breakConfig.breakThreshold else 1f
- val adjustedThreshold = threshold + (breakDelta * config.fudgeFactor)
-
- val currentProgress = currentDelta / adjustedThreshold
- val nextTicksProgress = (currentDelta + breakDelta) / adjustedThreshold
- val interpolatedProgress = lerp(mc.partialTicks, currentProgress, nextTicksProgress)
-
- val fillColor = if (config.dynamicFillColor) lerp(
- interpolatedProgress,
- config.startFillColor,
- config.endFillColor
- )
- else config.staticFillColor
- val outlineColor = if (config.dynamicOutlineColor) lerp(
- interpolatedProgress,
- config.startOutlineColor,
- config.endOutlineColor
- )
- else config.staticOutlineColor
-
- val pos = info.context.blockPos
- esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) {
+ else null
+ ).toDouble()
+ }
+ val currentDelta = info.breakingTicks * breakDelta
+
+ val threshold = if (info.type == Primary) info.breakConfig.breakThreshold else 1f
+ val adjustedThreshold = threshold + (breakDelta * config.fudgeFactor)
+
+ val currentProgress = currentDelta / adjustedThreshold
+ val nextTicksProgress = (currentDelta + breakDelta) / adjustedThreshold
+ val interpolatedProgress = lerp(mc.tickDelta, currentProgress, nextTicksProgress)
+
+ val fillColor = if (config.dynamicFillColor) lerp(
+ interpolatedProgress,
+ config.startFillColor,
+ config.endFillColor
+ )
+ else config.staticFillColor
+ val outlineColor = if (config.dynamicOutlineColor) lerp(
+ interpolatedProgress,
+ config.startOutlineColor,
+ config.endOutlineColor
+ )
+ else config.staticOutlineColor
+
+ val pos = info.context.blockPos
info.context.cachedState.getOutlineShape(world, pos).boundingBoxes.map {
it.offset(pos)
- }.forEach boxes@{ box ->
+ }.forEach { box ->
val animationMode = info.breakConfig.animation
- val currentProgressBox = interpolateBox(box, currentProgress, animationMode)
- val nextProgressBox = interpolateBox(box, nextTicksProgress, animationMode)
- val dynamicAABB = DynamicAABB().update(currentProgressBox).update(nextProgressBox)
- if (config.fill) filled(dynamicAABB, fillColor)
- if (config.outline) outline(dynamicAABB, outlineColor)
+ val interpolatedBox = interpolateBox(box, interpolatedProgress, animationMode)
+ box(interpolatedBox, info.breakConfig.outlineWidth) {
+ colors(fillColor, outlineColor)
+ }
}
}
- }
- }
-
- listenUnsafe(priority = Int.MIN_VALUE) {
- primaryBreak = null
- secondaryBreak = null
- breakCooldown = 0
+ }
}
return "Loaded Break Manager"
diff --git a/src/main/kotlin/com/lambda/interaction/managers/rotating/RotationConfig.kt b/src/main/kotlin/com/lambda/interaction/managers/rotating/RotationConfig.kt
index d17e62cef..9c9a64412 100644
--- a/src/main/kotlin/com/lambda/interaction/managers/rotating/RotationConfig.kt
+++ b/src/main/kotlin/com/lambda/interaction/managers/rotating/RotationConfig.kt
@@ -46,7 +46,7 @@ interface RotationConfig : ISettingGroup {
val tickStageMask: Set
- open class Instant(mode: RotationMode) : RotationConfig {
+ open class Instant(mode: RotationMode, override val visibility: () -> Boolean = { true }) : RotationConfig {
override val settings = mutableListOf>()
override val rotationMode = mode
override val keepTicks = 1
diff --git a/src/main/kotlin/com/lambda/module/hud/FPS.kt b/src/main/kotlin/com/lambda/module/hud/FPS.kt
index e2ec81ae1..dbdf15590 100644
--- a/src/main/kotlin/com/lambda/module/hud/FPS.kt
+++ b/src/main/kotlin/com/lambda/module/hud/FPS.kt
@@ -39,7 +39,7 @@ object FPS : HudModule(
var fps = 0
init {
- listen {
+ listen {
var currentFps = 0
if (average) {
frames.add(Unit)
diff --git a/src/main/kotlin/com/lambda/module/modules/chat/AntiSpam.kt b/src/main/kotlin/com/lambda/module/modules/chat/AntiSpam.kt
index d01d101eb..52b220f83 100644
--- a/src/main/kotlin/com/lambda/module/modules/chat/AntiSpam.kt
+++ b/src/main/kotlin/com/lambda/module/modules/chat/AntiSpam.kt
@@ -137,8 +137,9 @@ object AntiSpam : Module(
name: String,
c: Configurable,
baseGroup: NamedEnum,
+ override val visibility: () -> Boolean = { true },
) : ReplaceConfig, SettingGroup(c) {
- override val action by setting("$name Action Strategy", ReplaceConfig.ActionStrategy.Replace).group(baseGroup)
- override val replace by setting("$name Replace Strategy", ReplaceConfig.ReplaceStrategy.CensorAll) { action == ReplaceConfig.ActionStrategy.Replace }.group(baseGroup)
+ override val action by setting("$name Action Strategy", ReplaceConfig.ActionStrategy.Replace, visibility = visibility).group(baseGroup)
+ override val replace by setting("$name Replace Strategy", ReplaceConfig.ReplaceStrategy.CensorAll) { visibility() && action == ReplaceConfig.ActionStrategy.Replace }.group(baseGroup)
}
}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/module/modules/combat/AutoDisconnect.kt b/src/main/kotlin/com/lambda/module/modules/combat/AutoDisconnect.kt
index 3ada7d918..cbb60ac9c 100644
--- a/src/main/kotlin/com/lambda/module/modules/combat/AutoDisconnect.kt
+++ b/src/main/kotlin/com/lambda/module/modules/combat/AutoDisconnect.kt
@@ -31,7 +31,7 @@ import com.lambda.util.Formatting.format
import com.lambda.util.combat.CombatUtils.hasDeadlyCrystal
import com.lambda.util.combat.DamageUtils.isFallDeadly
import com.lambda.util.extension.fullHealth
-import com.lambda.util.extension.tickDelta
+import com.lambda.util.extension.tickDeltaF
import com.lambda.util.player.SlotUtils.allStacks
import com.lambda.util.text.buildText
import com.lambda.util.text.color
@@ -230,7 +230,7 @@ object AutoDisconnect : Module(
}),
Creeper({ creeper }, {
fastEntitySearch(15.0).find {
- it.getLerpedFuseTime(mc.tickDelta) > 0.0
+ it.getLerpedFuseTime(mc.tickDeltaF) > 0.0
&& it.pos.distanceTo(player.pos) <= 5.0
}?.let { creeper ->
buildText {
diff --git a/src/main/kotlin/com/lambda/module/modules/combat/AutoTotem.kt b/src/main/kotlin/com/lambda/module/modules/combat/AutoTotem.kt
index 322486cd7..a3234fb4f 100644
--- a/src/main/kotlin/com/lambda/module/modules/combat/AutoTotem.kt
+++ b/src/main/kotlin/com/lambda/module/modules/combat/AutoTotem.kt
@@ -31,7 +31,7 @@ import com.lambda.util.NamedEnum
import com.lambda.util.combat.CombatUtils.hasDeadlyCrystal
import com.lambda.util.combat.DamageUtils.isFallDeadly
import com.lambda.util.extension.fullHealth
-import com.lambda.util.extension.tickDelta
+import com.lambda.util.extension.tickDeltaF
import com.lambda.util.world.fastEntitySearch
import net.minecraft.entity.mob.CreeperEntity
import net.minecraft.entity.player.PlayerEntity
@@ -85,7 +85,7 @@ object AutoTotem : Module(
enum class Reason(val check: SafeContext.() -> Boolean) {
Health({ player.fullHealth < minimumHealth }),
Creeper({ creeper && fastEntitySearch(15.0).any {
- it.getLerpedFuseTime(mc.tickDelta) > 0.0
+ it.getLerpedFuseTime(mc.tickDeltaF) > 0.0
&& it.pos.distanceTo(player.pos) <= 5.0
} }),
Player({ players && fastEntitySearch(minPlayerDistance.toDouble()).any { otherPlayer ->
diff --git a/src/main/kotlin/com/lambda/module/modules/debug/BlockTest.kt b/src/main/kotlin/com/lambda/module/modules/debug/BlockTest.kt
index 499db8337..309b19ba2 100644
--- a/src/main/kotlin/com/lambda/module/modules/debug/BlockTest.kt
+++ b/src/main/kotlin/com/lambda/module/modules/debug/BlockTest.kt
@@ -17,9 +17,10 @@
package com.lambda.module.modules.debug
-import com.lambda.event.events.onStaticRender
+import com.lambda.graphics.mc.renderer.TickedRenderer.Companion.tickedRenderer
import com.lambda.module.Module
import com.lambda.module.tag.ModuleTag
+import com.lambda.util.ChatUtils.colors
import com.lambda.util.world.blockSearch
import net.minecraft.block.Blocks
import net.minecraft.util.math.Vec3i
@@ -47,13 +48,15 @@ object BlockTest : Module(
private val outlineColor = Color(100, 150, 255, 51)
init {
- onStaticRender { esp ->
- blockSearch(range, step = step) { _, state ->
- state.isOf(Blocks.DIAMOND_BLOCK)
- }.forEach { (pos, state) ->
- esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) {
+ tickedRenderer("BlockTest Ticked Renderer") { safeContext ->
+ with(safeContext) {
+ blockSearch(range, step = step) { _, state ->
+ state.isOf(Blocks.DIAMOND_BLOCK)
+ }.forEach { (pos, state) ->
state.getOutlineShape(world, pos).boundingBoxes.forEach { box ->
- box(box.offset(pos), filledColor, outlineColor)
+ box(box.offset(pos), 1.5f) {
+ colors(filledColor, outlineColor)
+ }
}
}
}
diff --git a/src/main/kotlin/com/lambda/module/modules/debug/RenderTest.kt b/src/main/kotlin/com/lambda/module/modules/debug/RenderTest.kt
index 7520f308a..ff70da49f 100644
--- a/src/main/kotlin/com/lambda/module/modules/debug/RenderTest.kt
+++ b/src/main/kotlin/com/lambda/module/modules/debug/RenderTest.kt
@@ -17,12 +17,13 @@
package com.lambda.module.modules.debug
-import com.lambda.event.events.onDynamicRender
-import com.lambda.event.events.onStaticRender
-import com.lambda.graphics.renderer.esp.DirectionMask
-import com.lambda.graphics.renderer.esp.DynamicAABB.Companion.dynamicBox
+import com.lambda.graphics.mc.renderer.ImmediateRenderer.Companion.immediateRenderer
+import com.lambda.graphics.mc.renderer.TickedRenderer.Companion.tickedRenderer
+import com.lambda.graphics.util.DynamicAABB.Companion.dynamicBox
import com.lambda.module.Module
import com.lambda.module.tag.ModuleTag
+import com.lambda.util.ChatUtils.colors
+import com.lambda.util.extension.tickDelta
import com.lambda.util.math.setAlpha
import com.lambda.util.world.entitySearch
import net.minecraft.entity.LivingEntity
@@ -45,18 +46,20 @@ object RenderTest : Module(
private val filledColor = outlineColor.setAlpha(0.2)
init {
- onDynamicRender { esp ->
- entitySearch(8.0)
- .forEach { entity ->
- esp.shapes(entity.x, entity.y, entity.z) {
- box(entity.dynamicBox, filledColor, outlineColor, DirectionMask.ALL, DirectionMask.OutlineMode.And)
+ immediateRenderer("RenderTest Immediate Renderer") { safeContext ->
+ with(safeContext) {
+ entitySearch(8.0)
+ .forEach { entity ->
+ box(entity.dynamicBox.box(mc.tickDelta) ?: return@forEach, 1.5f) {
+ colors(filledColor, outlineColor)
+ }
}
- }
+ }
}
- onStaticRender { esp ->
- esp.shapes(player.x, player.y, player.z) {
- box(Box.of(player.pos, 0.3, 0.3, 0.3), filledColor, outlineColor)
+ tickedRenderer("RenderTest Ticked Renderer") { safeContext ->
+ box(Box.of(safeContext.player.pos, 0.3, 0.3, 0.3), 1.5f) {
+ colors(filledColor, outlineColor)
}
}
}
diff --git a/src/main/kotlin/com/lambda/module/modules/debug/RendererTestModule.kt b/src/main/kotlin/com/lambda/module/modules/debug/RendererTestModule.kt
new file mode 100644
index 000000000..9669aaf63
--- /dev/null
+++ b/src/main/kotlin/com/lambda/module/modules/debug/RendererTestModule.kt
@@ -0,0 +1,466 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.module.modules.debug
+
+import com.lambda.graphics.mc.ItemLighting
+import com.lambda.graphics.mc.ItemOverlay
+import com.lambda.graphics.mc.LineDashStyle.Companion.marchingAnts
+import com.lambda.graphics.mc.LineDashStyle.Companion.screenMarchingAnts
+import com.lambda.graphics.mc.RenderBuilder.SDFGlow
+import com.lambda.graphics.mc.RenderBuilder.SDFOutline
+import com.lambda.graphics.mc.RenderBuilder.SDFShadow
+import com.lambda.graphics.mc.RenderBuilder.SDFStyle
+import com.lambda.graphics.mc.renderer.ChunkedRenderer.Companion.chunkedRenderer
+import com.lambda.graphics.mc.renderer.ImmediateRenderer.Companion.immediateRenderer
+import com.lambda.graphics.mc.renderer.TickedRenderer.Companion.tickedRenderer
+import com.lambda.module.Module
+import com.lambda.module.tag.ModuleTag
+import com.lambda.threading.runSafe
+import com.lambda.util.extension.prevPos
+import com.lambda.util.extension.tickDelta
+import com.lambda.util.math.lerp
+import com.lambda.util.world.toBlockPos
+import net.minecraft.item.ItemStack
+import net.minecraft.item.Items
+import net.minecraft.util.Identifier
+import net.minecraft.util.math.ChunkPos
+import net.minecraft.util.math.Direction
+import net.minecraft.util.math.Vec3d
+import net.minecraft.util.math.random.Random
+import org.joml.Quaternionf
+import java.awt.Color
+
+/**
+ * Test module for ChunkedRenderer - renders blocks around the player using chunk-based caching.
+ * Geometry is cached per-chunk and only rebuilt when chunks change.
+ */
+object ChunkedRendererTest : Module(
+ name = "ChunkedRendererTest",
+ description = "Test module for ChunkedRenderer - cached chunk-based rendering",
+ tag = ModuleTag.DEBUG,
+) {
+ init {
+ chunkedRenderer("ChunkedRendererTest", depthTest = { false }) { world, pos ->
+ runSafe {
+ if (player.chunkPos != ChunkPos(pos.toBlockPos())) return@chunkedRenderer
+
+ val startPos = lerp(mc.tickDelta, player.prevPos, player.pos)
+ lineGradient(
+ startPos,
+ Color.BLUE,
+ startPos.offset(Direction.EAST, 5.0),
+ Color.RED,
+ 0.1f,
+ marchingAnts(1f)
+ )
+ worldText(
+ "Test sdf font!",
+ startPos.offset(Direction.EAST, 5.0),
+ style = SDFStyle(
+ outline = SDFOutline(),
+ glow = SDFGlow(),
+ shadow = SDFShadow()
+ )
+ )
+
+ // Screen-space test renders (normalized 0-1 coordinates)
+ // Test screen rect with gradient
+ screenRectGradient(
+ 0.02f, 0.1f, 0.15f, 0.05f, // x, y, width, height (0-1)
+ Color.RED, Color.BLUE, Color.GREEN, Color.YELLOW
+ )
+
+ // Test screen rect with solid color
+ screenRect(0.02f, 0.17f, 0.1f, 0.03f, Color(50, 50, 200, 180))
+
+ // Test screen line
+ screenLine(0.02f, 0.22f, 0.17f, 0.25f, Color.CYAN, 0.003f, dashStyle = screenMarchingAnts())
+
+ // Test screen line with gradient
+ screenLineGradient(0.02f, 0.27f, Color.MAGENTA, 0.17f, 0.27f, Color.ORANGE, 0.004f)
+
+ // Test screen text
+ screenText(
+ "Screen Space Text!",
+ 0.02f,
+ 0.30f,
+ size = 0.025f, // 2.5% of screen
+ style = SDFStyle(
+ color = Color.WHITE,
+ outline = SDFOutline(),
+ shadow = SDFShadow()
+ )
+ )
+
+ // Test centered screen text
+ screenText(
+ "Centered Screen Text",
+ 0.5f, // 50% from left = center
+ 0.05f, // 5% from top
+ size = 0.03f, // 3% of screen
+ style = SDFStyle(
+ color = Color.YELLOW,
+ glow = SDFGlow(Color(255, 200, 0, 150)),
+ shadow = SDFShadow()
+ ),
+ centered = true
+ )
+
+ // Test screen image with tint
+ screenImage(
+ texture = Identifier.ofVanilla("textures/item/netherite_sword.png"),
+ x = 0.08f, y = 0.48f,
+ width = 0.1f, height = 0.1f * mc.window.width / mc.window.height.toFloat(),
+ hasOverlay = true // With glint
+ )
+
+ // Test world image - billboard facing the camera
+ val worldImagePos = startPos.offset(Direction.NORTH, 3.0).add(0.0, 1.5, 0.0)
+ worldImage(
+ texture = Identifier.ofVanilla("textures/item/diamond.png"),
+ pos = worldImagePos,
+ size = 0.8f,
+ tint = Color.WHITE
+ )
+
+ // Test world image with glint
+ worldImage(
+ texture = Identifier.ofVanilla("textures/item/netherite_sword.png"),
+ pos = worldImagePos.offset(Direction.EAST, 2.0),
+ size = 0.8f,
+ tint = Color.WHITE,
+ hasOverlay = true
+ )
+
+ // Test 3D Model (Chunked - Static)
+ // Render a Stone Block
+ val stoneState = net.minecraft.block.Blocks.STONE.defaultState
+ val stoneModel = mc.bakedModelManager.blockModels.getModel(stoneState)
+ val stoneRandom = Random.create()
+ val stoneParts = stoneModel.getParts(stoneRandom)
+
+ stoneParts.forEach { part ->
+ model(
+ part,
+ pos = startPos.offset(Direction.WEST, 2.0).add(0.0, 1.0, 0.0),
+ scale = Vec3d(1.0, 1.0, 1.0)
+ )
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Test module for TickedRenderer - rebuilds geometry every tick.
+ * Uses tick-camera relative coordinates with render-time delta interpolation.
+ */
+object TickedRendererTest : Module(
+ name = "TickedRendererTest",
+ description = "Test module for TickedRenderer - tick-based rendering with interpolation",
+ tag = ModuleTag.DEBUG,
+) {
+ private val throughWalls by setting("Through Walls", true)
+
+ init {
+ tickedRenderer("TickedRendererTest", depthTest = { !throughWalls }) { safeContext ->
+ with(safeContext) {
+ val startPos = lerp(mc.tickDelta, player.prevPos, player.pos)
+ lineGradient(
+ startPos,
+ Color.BLUE,
+ startPos.offset(Direction.EAST, 5.0),
+ Color.RED,
+ 0.1f,
+ marchingAnts(1f)
+ )
+ worldText(
+ "Test sdf font!",
+ startPos.offset(Direction.EAST, 5.0),
+ style = SDFStyle(
+ outline = SDFOutline(),
+ glow = SDFGlow(),
+ shadow = SDFShadow()
+ )
+ )
+
+ // Screen-space test renders (normalized 0-1 coordinates)
+ // Test screen rect with gradient
+ screenRectGradient(
+ 0.02f, 0.1f, 0.15f, 0.05f, // x, y, width, height (0-1)
+ Color.RED, Color.BLUE, Color.GREEN, Color.YELLOW
+ )
+
+ // Test screen rect with solid color
+ screenRect(0.02f, 0.17f, 0.1f, 0.03f, Color(50, 50, 200, 180))
+
+ // Test screen line
+ screenLine(0.02f, 0.22f, 0.17f, 0.25f, Color.CYAN, 0.003f, dashStyle = screenMarchingAnts())
+
+ // Test screen line with gradient
+ screenLineGradient(0.02f, 0.27f, Color.MAGENTA, 0.17f, 0.27f, Color.ORANGE, 0.004f)
+
+ // Test screen text
+ screenText(
+ "Screen Space Text!",
+ 0.02f,
+ 0.30f,
+ size = 0.025f, // 2.5% of screen
+ style = SDFStyle(
+ color = Color.WHITE,
+ outline = SDFOutline(),
+ shadow = SDFShadow()
+ )
+ )
+
+ // Test centered screen text
+ screenText(
+ "Centered Screen Text",
+ 0.5f, // 50% from left = center
+ 0.05f, // 5% from top
+ size = 0.03f, // 3% of screen
+ style = SDFStyle(
+ color = Color.YELLOW,
+ glow = SDFGlow(Color(255, 200, 0, 150)),
+ shadow = SDFShadow()
+ ),
+ centered = true
+ )
+
+ // Test screen image with tint
+ screenImage(
+ texture = Identifier.ofVanilla("textures/item/netherite_sword.png"),
+ x = 0.08f, y = 0.48f,
+ width = 0.1f, height = 0.1f * mc.window.width / mc.window.height.toFloat(),
+ hasOverlay = true // With glint
+ )
+
+ // Test world image - billboard facing the camera
+ val worldImagePos = startPos.offset(Direction.NORTH, 3.0).add(0.0, 1.5, 0.0)
+ worldImage(
+ texture = Identifier.ofVanilla("textures/item/diamond.png"),
+ pos = worldImagePos,
+ size = 0.8f,
+ tint = Color.WHITE
+ )
+
+ // Test world image with glint
+ worldImage(
+ texture = Identifier.ofVanilla("textures/item/netherite_sword.png"),
+ pos = worldImagePos.offset(Direction.EAST, 2.0),
+ size = 0.8f,
+ tint = Color.WHITE,
+ hasOverlay = true
+ )
+
+ // Test 3D Model (Ticked - Interpolated)
+ // Render a Gold Block
+ val goldState = net.minecraft.block.Blocks.GOLD_BLOCK.defaultState
+ val goldModel = mc.bakedModelManager.blockModels.getModel(goldState)
+ val goldRandom = net.minecraft.util.math.random.Random.create()
+ val goldParts = goldModel.getParts(goldRandom)
+
+ goldParts.forEach { part ->
+ model(
+ part,
+ pos = startPos.offset(Direction.WEST, 3.0).add(0.0, 1.0, 0.0),
+ scale = Vec3d(1.0, 1.0, 1.0)
+ )
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Test module for ImmediateRenderer - rebuilds geometry every frame.
+ * Uses render-camera relative coordinates for smooth interpolated rendering.
+ */
+object ImmediateRendererTest : Module(
+ name = "ImmediateRendererTest",
+ description = "Test module for ImmediateRenderer - frame-based interpolated rendering",
+ tag = ModuleTag.DEBUG,
+) {
+ private val throughWalls by setting("Through Walls", true)
+
+ init {
+ immediateRenderer("ImmediateRendererTest", depthTest = { !throughWalls }) { safeContext ->
+ with(safeContext) {
+ val startPos = lerp(mc.tickDelta, player.prevPos, player.pos)
+ lineGradient(
+ startPos,
+ Color.BLUE,
+ startPos.offset(Direction.EAST, 5.0),
+ Color.RED,
+ 0.1f,
+ marchingAnts(1f)
+ )
+ worldText(
+ "Test sdf font!",
+ startPos.offset(Direction.EAST, 5.0),
+ style = SDFStyle(
+ outline = SDFOutline(),
+ glow = SDFGlow(),
+ shadow = SDFShadow()
+ )
+ )
+
+ // Screen-space test renders (normalized 0-1 coordinates)
+ // Test screen rect with gradient
+ screenRectGradient(
+ 0.02f, 0.1f, 0.15f, 0.05f, // x, y, width, height (0-1)
+ Color.RED, Color.BLUE, Color.GREEN, Color.YELLOW
+ )
+
+ // Test screen rect with solid color
+ screenRect(0.02f, 0.17f, 0.1f, 0.03f, Color(50, 50, 200, 180))
+
+ // Test screen line
+ screenLine(0.02f, 0.22f, 0.17f, 0.25f, Color.CYAN, 0.003f, dashStyle = screenMarchingAnts())
+
+ // Test screen line with gradient
+ screenLineGradient(0.02f, 0.27f, Color.MAGENTA, 0.17f, 0.27f, Color.ORANGE, 0.004f)
+
+ // Test screen text
+ screenText(
+ "Screen Space Text!",
+ 0.02f,
+ 0.30f,
+ size = 0.025f, // 2.5% of screen
+ style = SDFStyle(
+ color = Color.WHITE,
+ outline = SDFOutline(),
+ shadow = SDFShadow()
+ )
+ )
+
+ // Test centered screen text
+ screenText(
+ "Centered Screen Text",
+ 0.5f, // 50% from left = center
+ 0.05f, // 5% from top
+ size = 0.03f, // 3% of screen
+ style = SDFStyle(
+ color = Color.YELLOW,
+ glow = SDFGlow(Color(255, 200, 0, 150)),
+ shadow = SDFShadow()
+ ),
+ centered = true
+ )
+
+ // Test screen image with tint
+ screenImage(
+ texture = Identifier.ofVanilla("textures/item/netherite_sword.png"),
+ x = 0.08f, y = 0.48f,
+ width = 0.1f, height = 0.1f * mc.window.width / mc.window.height.toFloat(),
+ hasOverlay = true // With glint
+ )
+
+ // Test world image - billboard facing the camera
+ val worldImagePos = startPos.offset(Direction.NORTH, 3.0).add(0.0, 1.5, 0.0)
+ worldImage(
+ texture = Identifier.ofVanilla("textures/item/diamond.png"),
+ pos = worldImagePos,
+ size = 0.8f,
+ tint = Color.WHITE
+ )
+
+ // Test world image with glint
+ worldImage(
+ texture = Identifier.ofVanilla("textures/item/netherite_sword.png"),
+ pos = worldImagePos.offset(Direction.EAST, 2.0),
+ size = 0.8f,
+ tint = Color.WHITE,
+ hasOverlay = true
+ )
+
+ // Test 3D Model (Immediate - Rotating)
+ // Render a Diamond Block
+ val diamondState = net.minecraft.block.Blocks.DIAMOND_BLOCK.defaultState
+ val diamondModel = mc.bakedModelManager.blockModels.getModel(diamondState)
+ val diamondParts = diamondModel.getParts(Random.create())
+
+ val time = System.currentTimeMillis() % 2000L / 2000f
+ val rotation = Quaternionf().rotateY(time * Math.PI.toFloat() * 2f)
+
+ diamondParts.forEach { part ->
+ model(
+ part,
+ pos = startPos.offset(Direction.WEST, 4.0).add(0.0, 1.0, 0.0),
+ scale = Vec3d(0.7, 0.7, 0.7),
+ rotation = rotation,
+ centered = true,
+ pixelPerfect = true,
+ smartAA = true
+ )
+ }
+
+ // ========== High-Fidelity GUI Item Rendering Tests ==========
+ // 1. World-space GUI Item (Netherite Sword)
+ worldGuiItem(
+ stack = ItemStack(Items.NETHERITE_SWORD),
+ pos = startPos.offset(Direction.NORTH, 2.0).add(0.0, 1.5, 0.0),
+ scale = 0.5f,
+ )
+
+ // 2. World-space GUI Block (Grass Block)
+ worldGuiItem(
+ stack = ItemStack(Items.GRASS_BLOCK),
+ pos = startPos.offset(Direction.NORTH, 2.0).offset(Direction.WEST, 1.0).add(0.0, 1.5, 0.0),
+ scale = 0.5f,
+ overlay = ItemOverlay.ENCHANT_GLINT,
+ )
+
+ // 3. Screen-space GUI Item
+ screenGuiItem(
+ stack = ItemStack(Items.DIAMOND_PICKAXE),
+ x = 0.5f, y = 0.4f,
+ size = 0.08f,
+ )
+
+ // ========== New: Flat & Shaded GUI Items ==========
+ val lightTime = (System.currentTimeMillis() % 4000L / 4000f) * 360f
+
+ // 4. Flat World Block (Grass Block)
+ worldGuiItem(
+ stack = ItemStack(Items.GRASS_BLOCK),
+ pos = startPos.offset(Direction.NORTH, 3.0),
+ scale = 0.5f,
+ flat = true,
+ overlay = ItemOverlay.ENCHANT_GLINT
+ )
+
+ // 5. Custom Shading World Item (Golden Apple)
+ worldGuiItem(
+ stack = ItemStack(Items.GOLDEN_APPLE),
+ pos = startPos.offset(Direction.NORTH, 3.0).offset(Direction.EAST, 1.0),
+ scale = 0.5f,
+ )
+
+ // 6. Flat Screen Item (Enchanted Book)
+ screenGuiItem(
+ stack = ItemStack(Items.GRASS_BLOCK),
+ x = 0.6f, y = 0.4f,
+ size = 0.08f,
+ rotation = Vec3d(0.0, 0.0, lightTime.toDouble()), // Spinning on screen
+ lighting = ItemLighting.NONE
+ )
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/com/lambda/module/modules/debug/SettingsTestModule.kt b/src/main/kotlin/com/lambda/module/modules/debug/SettingsTestModule.kt
new file mode 100644
index 000000000..4d75eaf83
--- /dev/null
+++ b/src/main/kotlin/com/lambda/module/modules/debug/SettingsTestModule.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.module.modules.debug
+
+import com.lambda.config.groups.ScreenLineSettings
+import com.lambda.config.groups.WorldLineSettings
+import com.lambda.config.groups.ScreenTextSettings
+import com.lambda.config.groups.WorldTextSettings
+import com.lambda.event.events.RenderEvent
+import com.lambda.event.listener.SafeListener.Companion.listen
+import com.lambda.graphics.mc.renderer.ImmediateRenderer
+import com.lambda.module.Module
+import com.lambda.module.tag.ModuleTag
+import com.lambda.util.extension.prevPos
+import com.lambda.util.extension.tickDelta
+import com.lambda.util.math.lerp
+import net.minecraft.util.math.Direction
+import java.awt.Color
+
+object SettingsTestModule : Module(
+ name = "SettingsTestModule",
+ description = "Test module for Line and Text Config Settings",
+ tag = ModuleTag.DEBUG
+) {
+ private enum class Page(override val displayName: String) : com.lambda.util.NamedEnum {
+ WorldLine("World Line"),
+ ScreenLine("Screen Line"),
+ WorldText("World Text"),
+ ScreenText("Screen Text")
+ }
+
+ private val worldLineConfig = WorldLineSettings(
+ "World Line ",
+ this,
+ Page.WorldLine,
+ )
+
+ private val screenLineConfig = ScreenLineSettings(
+ "Screen Line ",
+ this,
+ Page.ScreenLine,
+ )
+
+ private val worldTextConfig = WorldTextSettings(
+ "World Text ",
+ this,
+ Page.WorldText
+ )
+
+ private val textConfig = ScreenTextSettings(
+ "Screen Text ",
+ this,
+ Page.ScreenText
+ )
+
+// private val renderer = ImmediateRenderer("SettingsTestRenderer")
+
+// init {
+// listen {
+// renderer.tick()
+// renderer.shapes {
+// val startPos = lerp(mc.tickDelta, player.prevPos, player.pos).offset(Direction.NORTH, 3.0)
+//
+// // Render line using config
+// lineGradient(
+// startPos,
+// lineConfig.startColor,
+// startPos.offset(Direction.EAST, 3.0),
+// lineConfig.endColor,
+// lineConfig.lineWidth,
+// lineConfig.getDashStyle()
+// )
+//
+// // Render text using config
+// worldText(
+// "Configured Text",
+// startPos.add(0.0, 1.0, 0.0),
+// style = textConfig.getSDFStyle()
+// )
+// }
+// renderer.render()
+// }
+//
+// onDisable { renderer.close() }
+// }
+}
diff --git a/src/main/kotlin/com/lambda/module/modules/movement/BackTrack.kt b/src/main/kotlin/com/lambda/module/modules/movement/BackTrack.kt
index a6b255d66..ffa456841 100644
--- a/src/main/kotlin/com/lambda/module/modules/movement/BackTrack.kt
+++ b/src/main/kotlin/com/lambda/module/modules/movement/BackTrack.kt
@@ -17,14 +17,14 @@
package com.lambda.module.modules.movement
+import com.lambda.Lambda.mc
import com.lambda.context.SafeContext
import com.lambda.event.events.ConnectionEvent
import com.lambda.event.events.PacketEvent
import com.lambda.event.events.TickEvent
-import com.lambda.event.events.onDynamicRender
import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.graphics.esp.ShapeScope
-import com.lambda.graphics.renderer.esp.DynamicAABB
+import com.lambda.graphics.mc.renderer.ImmediateRenderer.Companion.immediateRenderer
+import com.lambda.graphics.util.DynamicAABB
import com.lambda.gui.components.ClickGuiLayout
import com.lambda.module.Module
import com.lambda.module.modules.combat.KillAura
@@ -33,6 +33,7 @@ import com.lambda.util.ClientPacket
import com.lambda.util.PacketUtils.handlePacketSilently
import com.lambda.util.PacketUtils.sendPacketSilently
import com.lambda.util.ServerPacket
+import com.lambda.util.extension.tickDelta
import com.lambda.util.math.dist
import com.lambda.util.math.lerp
import com.lambda.util.math.minus
@@ -111,19 +112,6 @@ object BackTrack : Module(
poolPackets()
}
- onDynamicRender { esp ->
- val target = target ?: return@onDynamicRender
-
- val c1 = ClickGuiLayout.primaryColor
- val c2 = Color.RED
- val p = target.hurtTime / 10.0
- val c = lerp(p, c1, c2)
-
- esp.shapes(target.pos.x, target.pos.y, target.pos.z) {
- box(box, c.multAlpha(0.3), c.multAlpha(0.8))
- }
- }
-
listen { event ->
if (!outbound || target == null) return@listen
sendPool.add(event.packet to currentTime)
@@ -179,6 +167,20 @@ object BackTrack : Module(
onDisable {
poolPackets(true)
}
+
+ immediateRenderer("BackTrack Immediate Renderer") {
+ val target = target ?: return@immediateRenderer
+
+ val c1 = ClickGuiLayout.primaryColor
+ val c2 = Color.RED
+ val p = target.hurtTime / 10.0
+ val c = lerp(p, c1, c2)
+
+ box(box.box(mc.tickDelta) ?: return@immediateRenderer, 0f) {
+ hideOutline()
+ gradientY(c.multAlpha(0.3), c.multAlpha(0.8))
+ }
+ }
}
private fun SafeContext.poolPackets(all: Boolean = false) {
diff --git a/src/main/kotlin/com/lambda/module/modules/movement/Blink.kt b/src/main/kotlin/com/lambda/module/modules/movement/Blink.kt
index f94ddb8fd..280aa7e05 100644
--- a/src/main/kotlin/com/lambda/module/modules/movement/Blink.kt
+++ b/src/main/kotlin/com/lambda/module/modules/movement/Blink.kt
@@ -17,13 +17,13 @@
package com.lambda.module.modules.movement
+import com.lambda.Lambda.mc
import com.lambda.context.SafeContext
import com.lambda.event.events.PacketEvent
-import com.lambda.event.events.RenderEvent
-import com.lambda.event.events.onDynamicRender
import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.graphics.esp.ShapeScope
-import com.lambda.graphics.renderer.esp.DynamicAABB
+import com.lambda.graphics.mc.renderer.ImmediateRenderer.Companion.immediateRenderer
+import com.lambda.graphics.mc.renderer.TickedRenderer.Companion.tickedRenderer
+import com.lambda.graphics.util.DynamicAABB
import com.lambda.gui.components.ClickGuiLayout
import com.lambda.module.Module
import com.lambda.module.modules.combat.KillAura
@@ -31,6 +31,7 @@ import com.lambda.module.tag.ModuleTag
import com.lambda.util.PacketUtils.handlePacketSilently
import com.lambda.util.PacketUtils.sendPacketSilently
import com.lambda.util.ServerPacket
+import com.lambda.util.extension.tickDelta
import com.lambda.util.math.minus
import com.lambda.util.math.setAlpha
import net.minecraft.network.packet.c2s.play.PlayerMoveC2SPacket
@@ -59,20 +60,19 @@ object Blink : Module(
private var lastBox = Box(BlockPos.ORIGIN)
init {
- listen {
+ tickedRenderer("Blink Ticked Renderer") { safeContext ->
val time = System.currentTimeMillis()
- if (isActive && time - lastUpdate < delay) return@listen
+ if (isActive && time - lastUpdate < delay) return@tickedRenderer
lastUpdate = time
- poolPackets()
+ with(safeContext) { poolPackets() }
}
- onDynamicRender { esp ->
+ immediateRenderer("Blink Immediate Renderer") {
val color = ClickGuiLayout.primaryColor
- val pos = player.pos
- esp.shapes(pos.x, pos.y, pos.z) {
- box(box.update(lastBox), color.setAlpha(0.3), color)
+ box(box.update(lastBox).box(mc.tickDelta) ?: return@immediateRenderer, 1.5f) {
+ colors(color.setAlpha(0.3), color)
}
}
diff --git a/src/main/kotlin/com/lambda/module/modules/network/PacketDelay.kt b/src/main/kotlin/com/lambda/module/modules/network/PacketDelay.kt
index 20787710a..e0ceed0b9 100644
--- a/src/main/kotlin/com/lambda/module/modules/network/PacketDelay.kt
+++ b/src/main/kotlin/com/lambda/module/modules/network/PacketDelay.kt
@@ -19,8 +19,8 @@ package com.lambda.module.modules.network
import com.lambda.context.SafeContext
import com.lambda.event.events.PacketEvent
-import com.lambda.event.events.RenderEvent
import com.lambda.event.listener.SafeListener.Companion.listen
+import com.lambda.graphics.mc.renderer.TickedRenderer.Companion.tickedRenderer
import com.lambda.module.Module
import com.lambda.module.tag.ModuleTag
import com.lambda.threading.runConcurrent
@@ -53,10 +53,10 @@ object PacketDelay : Module(
private var inboundLastUpdate = 0L
init {
- listen {
- if (mode != Mode.Static) return@listen
+ tickedRenderer("PacketDelay Ticked Renderer") { safeContext ->
+ if (mode != Mode.Static) return@tickedRenderer
- flushPools(System.currentTimeMillis())
+ with(safeContext) { flushPools(System.currentTimeMillis()) }
}
listen(Int.MIN_VALUE) { event ->
diff --git a/src/main/kotlin/com/lambda/module/modules/player/AirPlace.kt b/src/main/kotlin/com/lambda/module/modules/player/AirPlace.kt
index c5b3b0b1b..b7c139997 100644
--- a/src/main/kotlin/com/lambda/module/modules/player/AirPlace.kt
+++ b/src/main/kotlin/com/lambda/module/modules/player/AirPlace.kt
@@ -24,8 +24,8 @@ import com.lambda.context.SafeContext
import com.lambda.event.events.ButtonEvent
import com.lambda.event.events.PlayerEvent
import com.lambda.event.events.TickEvent
-import com.lambda.event.events.onStaticRender
import com.lambda.event.listener.SafeListener.Companion.listen
+import com.lambda.graphics.mc.renderer.TickedRenderer.Companion.tickedRenderer
import com.lambda.interaction.construction.simulation.BuildSimulator.simulate
import com.lambda.interaction.construction.simulation.context.BuildContext
import com.lambda.interaction.construction.verify.TargetState
@@ -48,6 +48,7 @@ import net.minecraft.world.RaycastContext
import org.lwjgl.glfw.GLFW
import java.awt.Color
import java.util.concurrent.ConcurrentLinkedQueue
+import kotlin.invoke
object AirPlace : Module(
name = "AirPlace",
@@ -63,6 +64,7 @@ object AirPlace : Module(
private val scrollBind by setting("Scroll Bind", Bind(KeyCode.Unbound.code, GLFW.GLFW_MOD_CONTROL), "Allows you to hold the ctrl key and scroll to adjust distance").group(Group.General)
private val outlineColor by setting("Outline Color", Color.WHITE).group(Group.Render)
+ private val outlineWidth by setting("Outline Width", 1.5f, 0.5f..10f, 0.1f)
private var placementPos: BlockPos? = null
private var placementState: BlockState? = null
@@ -106,13 +108,13 @@ object AirPlace : Module(
listen { if (airPlace()) it.cancel() }
listen { if (airPlace()) it.cancel() }
- onStaticRender { esp ->
+ tickedRenderer("Air Place Ticked Renderer") { safeContext ->
placementPos?.let { pos ->
- val boxes = placementState?.getOutlineShape(world, pos)?.boundingBoxes
+ val boxes = placementState?.getOutlineShape(safeContext.world, pos)?.boundingBoxes
?: listOf(Box(0.0, 0.0, 0.0, 1.0, 1.0, 1.0))
- esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) {
- boxes.forEach { box ->
- outline(box.offset(pos), outlineColor)
+ boxes.forEach { box ->
+ box(box, outlineWidth) {
+ hideFill()
}
}
}
diff --git a/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt b/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt
index bc16a1d11..037d6a29b 100644
--- a/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt
+++ b/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt
@@ -22,8 +22,8 @@ import com.lambda.config.applyEdits
import com.lambda.context.SafeContext
import com.lambda.event.events.PlayerEvent
import com.lambda.event.events.TickEvent
-import com.lambda.event.events.onStaticRender
import com.lambda.event.listener.SafeListener.Companion.listen
+import com.lambda.graphics.mc.renderer.TickedRenderer.Companion.tickedRenderer
import com.lambda.interaction.construction.simulation.BuildSimulator.simulate
import com.lambda.interaction.construction.simulation.context.BreakContext
import com.lambda.interaction.construction.simulation.context.BuildContext
@@ -71,6 +71,7 @@ object PacketMine : Module(
private val staticColor by setting("Color", Color(255, 0, 0, 60)) { renderQueue && !dynamicColor }.group(Group.Renders)
private val startColor by setting("Start Color", Color(255, 255, 0, 60), "The color of the start (closest to breaking) of the queue") { renderQueue && dynamicColor }.group(Group.Renders)
private val endColor by setting("End Color", Color(255, 0, 0, 60), "The color of the end (farthest from breaking) of the queue") { renderQueue && dynamicColor }.group(Group.Renders)
+ private val outlineWidth by setting("Outline Width", 1.5f, 0.5f..10f, 0.1f)
private val pendingActions = ConcurrentLinkedQueue()
@@ -173,27 +174,30 @@ object PacketMine : Module(
}
}
- onStaticRender { esp ->
+ tickedRenderer("PacketMine Ticked Renderer") { safeContext ->
if (renderRebreak) {
rebreakPos?.let { pos ->
- esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) {
- outline(pos, rebreakColor)
+ box(pos, outlineWidth) {
+ hideFill()
+ outlineColor(rebreakColor)
}
}
}
- if (!renderQueue) return@onStaticRender
- queueSorted.forEachIndexed { index, positions ->
- positions.forEach { pos ->
- val color = if (dynamicColor) lerp(index / queuePositions.size.toDouble(), startColor, endColor)
- else staticColor
- val boxes = when (renderMode) {
- RenderMode.State -> blockState(pos).getOutlineShape(world, pos).boundingBoxes
- RenderMode.Box -> listOf(Box(0.0, 0.0, 0.0, 1.0, 1.0, 1.0))
- }.map { lerp(renderSize.toDouble(), Box(it.center, it.center), it).offset(pos) }
+ if (!renderQueue) return@tickedRenderer
+ with(safeContext) {
+ queueSorted.forEachIndexed { index, positions ->
+ positions.forEach { pos ->
+ val color = if (dynamicColor) lerp(index / queuePositions.size.toDouble(), startColor, endColor)
+ else staticColor
+ val boxes = when (renderMode) {
+ RenderMode.State -> blockState(pos).getOutlineShape(world, pos).boundingBoxes
+ RenderMode.Box -> listOf(Box(0.0, 0.0, 0.0, 1.0, 1.0, 1.0))
+ }.map { lerp(renderSize.toDouble(), Box(it.center, it.center), it).offset(pos) }
- esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) {
boxes.forEach { box ->
- box(box, color, color.setAlpha(1.0))
+ box(box, outlineWidth) {
+ colors(color, color.setAlpha(1.0))
+ }
}
}
}
diff --git a/src/main/kotlin/com/lambda/module/modules/player/WorldEater.kt b/src/main/kotlin/com/lambda/module/modules/player/WorldEater.kt
index c97f8242a..02341d866 100644
--- a/src/main/kotlin/com/lambda/module/modules/player/WorldEater.kt
+++ b/src/main/kotlin/com/lambda/module/modules/player/WorldEater.kt
@@ -17,7 +17,7 @@
package com.lambda.module.modules.player
-import com.lambda.event.events.onStaticRender
+import com.lambda.graphics.mc.renderer.TickedRenderer.Companion.tickedRenderer
import com.lambda.interaction.BaritoneManager
import com.lambda.interaction.construction.blueprint.Blueprint.Companion.toStructure
import com.lambda.interaction.construction.blueprint.StaticBlueprint.Companion.toBlueprint
@@ -65,9 +65,10 @@ object WorldEater : Module(
BaritoneManager.cancel()
}
- onStaticRender { esp ->
- esp.shapes(pos1.x.toDouble(), pos1.y.toDouble(), pos1.z.toDouble()) {
- outline(Box.enclosing(pos1, pos2), Color.BLUE)
+ tickedRenderer("WorldEater Ticked Renderer") {
+ box(Box.enclosing(pos1, pos2), 1.5f) {
+ hideFill()
+ outlineColor(Color.BLUE)
}
}
}
diff --git a/src/main/kotlin/com/lambda/module/modules/render/BlockESP.kt b/src/main/kotlin/com/lambda/module/modules/render/BlockESP.kt
deleted file mode 100644
index 84a031bdc..000000000
--- a/src/main/kotlin/com/lambda/module/modules/render/BlockESP.kt
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright 2025 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.module.modules.render
-
-import com.lambda.Lambda.mc
-import com.lambda.config.settings.collections.CollectionSetting.Companion.onDeselect
-import com.lambda.config.settings.collections.CollectionSetting.Companion.onSelect
-import com.lambda.context.SafeContext
-import com.lambda.graphics.esp.chunkedEsp
-import com.lambda.graphics.renderer.esp.DirectionMask
-import com.lambda.graphics.renderer.esp.DirectionMask.buildSideMesh
-import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
-import com.lambda.threading.runSafe
-import com.lambda.util.extension.blockColor
-import com.lambda.util.extension.getBlockState
-import com.lambda.util.world.toBlockPos
-import net.minecraft.block.Blocks
-import net.minecraft.client.render.model.BlockStateModel
-import net.minecraft.util.math.Box
-import java.awt.Color
-
-object BlockESP : Module(
- name = "BlockESP",
- description = "Render block ESP",
- tag = ModuleTag.RENDER,
-) {
- private val searchBlocks by setting("Search Blocks", true, "Search for blocks around the player")
- private val blocks by setting("Blocks", setOf(Blocks.BEDROCK), description = "Render blocks") { searchBlocks }
- .onSelect { rebuildMesh(this, null, null) }
- .onDeselect { rebuildMesh(this, null, null) }
-
- private var drawFaces: Boolean by setting("Draw Faces", true, "Draw faces of blocks") { searchBlocks }.onValueChange(::rebuildMesh).onValueChange { _, to -> if (!to) drawOutlines = true }
- private var drawOutlines: Boolean by setting("Draw Outlines", true, "Draw outlines of blocks") { searchBlocks }.onValueChange(::rebuildMesh).onValueChange { _, to -> if (!to) drawFaces = true }
- private val mesh by setting("Mesh", true, "Connect similar adjacent blocks") { searchBlocks }.onValueChange(::rebuildMesh)
-
- private val useBlockColor by setting("Use Block Color", false, "Use the color of the block instead") { searchBlocks }.onValueChange(::rebuildMesh)
- private val blockColorAlpha by setting("Block Color Alpha", 0.3, 0.1..1.0, 0.05) { searchBlocks && useBlockColor }.onValueChange { _, _ -> ::rebuildMesh }
-
- private val faceColor by setting("Face Color", Color(100, 150, 255, 51), "Color of the surfaces") { searchBlocks && drawFaces && !useBlockColor }.onValueChange(::rebuildMesh)
- private val outlineColor by setting("Outline Color", Color(100, 150, 255, 128), "Color of the outlines") { searchBlocks && drawOutlines && !useBlockColor }.onValueChange(::rebuildMesh)
- private val outlineWidth by setting("Outline Width", 1.0f, 0.5f..5.0f, 0.5f) { searchBlocks && drawOutlines }.onValueChange(::rebuildMesh)
-
- private val outlineMode by setting("Outline Mode", DirectionMask.OutlineMode.And, "Outline mode") { searchBlocks }.onValueChange(::rebuildMesh)
-
- @JvmStatic
- val barrier by setting("Solid Barrier Block", true, "Render barrier blocks")
-
- // ToDo: I wanted to render this as a transparent / translucent block with a red tint.
- // Like the red stained glass block without the texture sprite.
- // Creating a custom baked model for this would be needed but seems really hard to do.
- // mc.blockRenderManager.getModel(Blocks.RED_STAINED_GLASS.defaultState)
- @JvmStatic
- val model: BlockStateModel get() = mc.bakedModelManager.missingModel
-
- private val esp = chunkedEsp("BlockESP") { world, position ->
- val state = world.getBlockState(position)
- if (state.block !in blocks) return@chunkedEsp
-
- val sides = if (mesh) {
- buildSideMesh(position) {
- world.getBlockState(it).block in blocks
- }
- } else DirectionMask.ALL
-
- runSafe {
- // TODO: Add custom color option when map options are implemented
- val extractedColor = blockColor(state, position.toBlockPos())
- val finalColor = Color(extractedColor.red, extractedColor.green, extractedColor.blue, (blockColorAlpha * 255).toInt())
- val pos = position.toBlockPos()
- val shape = state.getOutlineShape(world, pos)
- val worldBox = if (shape.isEmpty) Box(pos) else shape.boundingBox.offset(pos)
- box(worldBox) {
- if (drawFaces)
- filled(if (useBlockColor) finalColor else faceColor, sides)
- if (drawOutlines)
- outline(if (useBlockColor) extractedColor else BlockESP.outlineColor, sides, BlockESP.outlineMode, thickness = outlineWidth)
- }
- }
- }
-
- init {
- onEnable { esp.rebuildAll() }
- onDisable { esp.close() }
- }
-
- private fun rebuildMesh(ctx: SafeContext, from: Any?, to: Any?): Unit = esp.rebuild()
-}
diff --git a/src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt b/src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt
new file mode 100644
index 000000000..9b3413f7c
--- /dev/null
+++ b/src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.module.modules.render
+
+import com.lambda.config.applyEdits
+import com.lambda.config.groups.WorldLineSettings
+import com.lambda.event.events.TickEvent
+import com.lambda.event.listener.SafeListener.Companion.listen
+import com.lambda.graphics.mc.renderer.ImmediateRenderer.Companion.immediateRenderer
+import com.lambda.module.Module
+import com.lambda.module.tag.ModuleTag
+import com.lambda.util.BlockUtils.blockState
+import com.lambda.util.extension.tickDelta
+import com.lambda.util.math.lerp
+import com.lambda.util.world.raycast.RayCastUtils.blockResult
+import net.minecraft.util.math.Box
+import java.awt.Color
+
+object BlockOutline : Module(
+ name = "BlockOutline",
+ description = "Overrides the default block outline rendering",
+ tag = ModuleTag.RENDER
+) {
+ private val fill by setting("Fill", true)
+ private val fillColor by setting("Fill Color", Color(255, 255, 255, 20)) { fill }
+ private val outline by setting("Outline", true)
+ private val outlineColor by setting("Outline Color", Color(255, 255, 255, 120)) { outline }
+ private val lineConfig = WorldLineSettings("Outline ", this) { outline }.apply {
+ applyEdits {
+ hide(::startColor, ::endColor)
+ }
+ }
+ private val interpolate by setting("Interpolate", true)
+ private val esp by setting("ESP", true)
+
+ var previous: List? = null
+
+ init {
+ immediateRenderer("BlockOutline Immediate Renderer", depthTest = { !esp }) { safeContext ->
+ with(safeContext) {
+ val hitResult = mc.crosshairTarget?.blockResult ?: return@immediateRenderer
+ val pos = hitResult.blockPos
+ val blockState = blockState(pos)
+ val boxes = blockState
+ .getOutlineShape(world, pos)
+ .boundingBoxes
+ .let { boxes ->
+ boxes.mapIndexed { index, box ->
+ val offset = box.offset(pos)
+ val interpolated = previous?.let { previous ->
+ if (!interpolate || previous.size < boxes.size) null
+ else lerp(mc.tickDelta, previous[index], offset)
+ } ?: offset
+ interpolated.expand(0.0001)
+ }
+ }
+
+ boxes.forEach { box ->
+ box(box, lineConfig.width) {
+ colors(fillColor, outlineColor)
+ if (!fill) hideFill()
+ if (!outline) hideOutline()
+ }
+ }
+ }
+ }
+
+ listen {
+ val hitResult = mc.crosshairTarget?.blockResult ?: return@listen
+ val state = blockState(hitResult.blockPos)
+ previous = state
+ .getOutlineShape(world, hitResult.blockPos).boundingBoxes
+ .map { it.offset(hitResult.blockPos) }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/module/modules/render/EntityESP.kt b/src/main/kotlin/com/lambda/module/modules/render/EntityESP.kt
index 506a98a2f..a09f5d698 100644
--- a/src/main/kotlin/com/lambda/module/modules/render/EntityESP.kt
+++ b/src/main/kotlin/com/lambda/module/modules/render/EntityESP.kt
@@ -17,21 +17,16 @@
package com.lambda.module.modules.render
-import com.lambda.Lambda.mc
import com.lambda.context.SafeContext
import com.lambda.event.events.GuiEvent
import com.lambda.event.events.RenderEvent
-import com.lambda.event.events.TickEvent
+import com.lambda.event.events.ScreenRenderEvent
import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.graphics.RenderMain
-import com.lambda.graphics.mc.InterpolatedRegionESP
-import com.lambda.graphics.mc.TransientRegionESP
+import com.lambda.graphics.mc.renderer.ImmediateRenderer
import com.lambda.module.Module
import com.lambda.module.tag.ModuleTag
import com.lambda.util.NamedEnum
-import com.lambda.util.extension.tickDelta
-import com.lambda.util.math.setAlpha
-import com.lambda.util.world.entitySearch
+import com.lambda.util.extension.tickDeltaF
import imgui.ImGui
import net.minecraft.entity.Entity
import net.minecraft.entity.ItemEntity
@@ -54,209 +49,34 @@ object EntityESP : Module(
description = "Highlight entities with smooth interpolated rendering",
tag = ModuleTag.RENDER
) {
- private val esp = InterpolatedRegionESP("EntityESP")
-
- private data class LabelData(
- val screenX: Float,
- val screenY: Float,
- val text: String,
- val color: Color,
- val scale: Float
- )
-
- private val pendingLabels = mutableListOf()
-
- private val range by setting("Range", 64.0, 8.0..256.0, 1.0, "Maximum render distance").group(Group.General)
- private val throughWalls by setting("Through Walls", true, "Render through blocks").group(Group.General)
- private val self by setting("Self", false, "Render own player in third person").group(Group.General)
-
- private val players by setting("Players", true, "Highlight players").group(Group.Entities)
- private val hostiles by setting("Hostiles", true, "Highlight hostile mobs").group(Group.Entities)
- private val passives by setting("Passives", false, "Highlight passive mobs (animals)").group(Group.Entities)
- private val neutrals by setting("Neutrals", false, "Highlight neutral mobs").group(Group.Entities)
- private val items by setting("Items", false, "Highlight dropped items").group(Group.Entities)
- private val projectiles by setting("Projectiles", false, "Highlight projectiles").group(Group.Entities)
- private val vehicles by setting("Vehicles", false, "Highlight boats and minecarts").group(Group.Entities)
- private val crystals by setting("Crystals", true, "Highlight end crystals").group(Group.Entities)
- private val armorStands by setting("Armor Stands", false, "Highlight armor stands").group(Group.Entities)
-
- private val drawBoxes by setting("Boxes", true, "Draw entity boxes").group(Group.Render)
- private val drawFilled by setting("Filled", true, "Fill entity boxes") { drawBoxes }.group(Group.Render)
- private val drawOutline by setting("Outline", true, "Draw box outlines") { drawBoxes }.group(Group.Render)
- private val filledAlpha by setting("Filled Alpha", 0.2, 0.0..1.0, 0.05) { drawBoxes && drawFilled }.group(Group.Render)
- private val outlineAlpha by setting("Outline Alpha", 0.8, 0.0..1.0, 0.05) { drawBoxes && drawOutline }.group(Group.Render)
- private val outlineWidth by setting("Outline Width", 1.0f, 0.5f..5.0f, 0.5f) { drawBoxes && drawOutline }.group(Group.Render)
-
- private val tracers by setting("Tracers", true, "Draw lines to entities").group(Group.Tracers)
- private val tracerOrigin by setting("Tracer Origin", TracerOrigin.Eyes, "Where tracers start from") { tracers }.group(Group.Tracers)
- private val tracerWidth by setting("Tracer Width", 1.5f, 0.5f..5f, 0.5f) { tracers }.group(Group.Tracers)
- private val dashedTracers by setting("Dashed Tracers", false, "Use dashed lines for tracers") { tracers }.group(Group.Tracers)
- private val dashLength by setting("Dash Length", 1.0, 0.25..2.0, 0.25) { tracers && dashedTracers }.group(Group.Tracers)
- private val gapLength by setting("Gap Length", 0.5, 0.1..1.0, 0.1) { tracers && dashedTracers }.group(Group.Tracers)
-
- private val nameTags by setting("Name Tags", false, "Show entity name tags").group(Group.NameTags)
- private val nameTagDistance by setting("Show Distance", true, "Show distance in name tags") { nameTags }.group(Group.NameTags)
- private val nameTagHealth by setting("Show Health", true, "Show health in name tags") { nameTags }.group(Group.NameTags)
- private val nameTagBackground by setting("Name Background", true, "Draw background behind name tags") { nameTags }.group(Group.NameTags)
-
- private val playerColor by setting("Player Color", Color(255, 50, 50), "Color for players").group(Group.Colors)
- private val hostileColor by setting("Hostile Color", Color(255, 100, 0), "Color for hostile mobs").group(Group.Colors)
- private val passiveColor by setting("Passive Color", Color(50, 255, 50), "Color for passive mobs").group(Group.Colors)
- private val neutralColor by setting("Neutral Color", Color(255, 255, 50), "Color for neutral mobs").group(Group.Colors)
- private val itemColor by setting("Item Color", Color(100, 100, 255), "Color for items").group(Group.Colors)
- private val projectileColor by setting("Projectile Color", Color(200, 200, 200), "Color for projectiles").group(Group.Colors)
- private val vehicleColor by setting("Vehicle Color", Color(150, 100, 50), "Color for vehicles").group(Group.Colors)
- private val crystalColor by setting("Crystal Color", Color(255, 0, 255), "Color for end crystals").group(Group.Colors)
- private val otherColor by setting("Other Color", Color(200, 200, 200), "Color for other entities").group(Group.Colors)
-
- init {
- listen {
- esp.tick()
-
- entitySearch(range) { shouldRender(it) }.forEach { entity ->
- val color = getEntityColor(entity)
- val box = entity.boundingBox
-
- esp.shapes(entity.x, entity.y, entity.z) {
- if (drawBoxes) {
- box(box, entity.id) {
- if (drawFilled)
- filled(color.setAlpha(filledAlpha))
- if (drawOutline)
- outline(
- color.setAlpha(outlineAlpha),
- thickness = outlineWidth
- )
- }
- }
- }
- }
-
- esp.upload()
- }
-
- listen {
- val tickDelta = mc.tickDelta
- esp.render(tickDelta)
-
- // Clear pending labels from previous frame
- pendingLabels.clear()
-
- if (tracers || nameTags) {
- val tracerEsp = TransientRegionESP(
- "EntityESP-Tracers",
- depthTest = !throughWalls
- )
- entitySearch(range) { shouldRender(it) }.forEach { entity ->
- val color = getEntityColor(entity)
- val entityPos = getInterpolatedPos(entity, tickDelta)
-
- if (tracers) {
- val startPos = getTracerStartPos(tickDelta)
- val endPos = entityPos.add(0.0, entity.height / 2.0, 0.0)
-
- tracerEsp.shapes(entity.x, entity.y, entity.z) {
- tracer(startPos, endPos, entity.id) {
- color(color.setAlpha(outlineAlpha))
- width(tracerWidth)
- if (dashedTracers) dashed(dashLength, gapLength)
- }
- }
- }
-
- if (nameTags) {
- val namePos = entityPos.add(0.0, entity.height + 0.3, 0.0)
- // Project to screen coords NOW while matrices are
- // valid
- val screen = RenderMain.worldToScreen(namePos)
- if (screen != null) {
- val nameText = buildNameTag(entity)
- // Calculate distance-based scale (closer =
- // larger)
- val distance = player.pos.distanceTo(namePos).toFloat()
- val scale = (1.0f / (distance * 0.1f + 1f)).coerceIn(0.5f, 2.0f)
- pendingLabels.add(
- LabelData(
- screen.x,
- screen.y,
- nameText,
- color,
- scale
- )
- )
- }
- }
- }
-
- tracerEsp.upload()
- tracerEsp.render()
- tracerEsp.close()
- }
- }
-
- // Draw ImGUI labels using pre-computed screen coordinates
- listen {
- val drawList = ImGui.getBackgroundDrawList()
- val font = ImGui.getFont()
-
- pendingLabels.forEach { label ->
- val fontSize = (font.fontSize * label.scale).toInt()
- val textSize = imgui.ImVec2()
- ImGui.calcTextSize(textSize, label.text)
-
- // Scale text size based on our custom scale
- val tw = textSize.x * label.scale
- val th = textSize.y * label.scale
-
- // Center text horizontally
- val x = label.screenX - tw / 2f
- val y = label.screenY
-
- // Color conversion (ABGR for ImGui)
- val textColor =
- (label.color.alpha shl 24) or
- (label.color.blue shl 16) or
- (label.color.green shl 8) or
- label.color.red
- val shadowColor = (200 shl 24) or 0 // Black with alpha
-
- if (nameTagBackground) {
- val bgColor = (160 shl 24) or 0 // Black with 160 alpha
- val padX = 4f * label.scale
- val padY = 2f * label.scale
- drawList.addRectFilled(
- x - padX,
- y - padY,
- x + tw + padX,
- y + th + padY,
- bgColor,
- 3f * label.scale
- )
- } else {
- // Shadow
- drawList.addText(
- font,
- fontSize,
- imgui.ImVec2(
- x + 1f * label.scale,
- y + 1f * label.scale
- ),
- shadowColor,
- label.text
- )
- }
- drawList.addText(
- font,
- fontSize,
- imgui.ImVec2(x, y),
- textColor,
- label.text
- )
- }
- }
-
- onDisable { esp.close() }
- }
+ private val throughWalls by setting("Through Walls", true, "Render through blocks")
+ private val self by setting("Self", false, "Render own player in third person")
+
+ private val players by setting("Players", true, "Highlight players")
+ private val hostiles by setting("Hostiles", true, "Highlight hostile mobs")
+ private val passives by setting("Passives", false, "Highlight passive mobs (animals)")
+ private val neutrals by setting("Neutrals", false, "Highlight neutral mobs")
+ private val items by setting("Items", false, "Highlight dropped items")
+ private val projectiles by setting("Projectiles", false, "Highlight projectiles")
+ private val vehicles by setting("Vehicles", false, "Highlight boats and minecarts")
+ private val crystals by setting("Crystals", true, "Highlight end crystals")
+ private val armorStands by setting("Armor Stands", false, "Highlight armor stands")
+
+ private val drawBoxes by setting("Boxes", true, "Draw entity boxes")
+ private val drawFilled by setting("Filled", true, "Fill entity boxes") { drawBoxes }
+ private val drawOutline by setting("Outline", true, "Draw box outlines") { drawBoxes }
+ private val filledAlpha by setting("Filled Alpha", 0.2, 0.0..1.0, 0.05) { drawBoxes && drawFilled }
+ private val outlineAlpha by setting("Outline Alpha", 0.8, 0.0..1.0, 0.05) { drawBoxes && drawOutline }
+
+ private val playerColor by setting("Player Color", Color(255, 50, 50), "Color for players")
+ private val hostileColor by setting("Hostile Color", Color(255, 100, 0), "Color for hostile mobs")
+ private val passiveColor by setting("Passive Color", Color(50, 255, 50), "Color for passive mobs")
+ private val neutralColor by setting("Neutral Color", Color(255, 255, 50), "Color for neutral mobs")
+ private val itemColor by setting("Item Color", Color(100, 100, 255), "Color for items")
+ private val projectileColor by setting("Projectile Color", Color(200, 200, 200), "Color for projectiles")
+ private val vehicleColor by setting("Vehicle Color", Color(150, 100, 50), "Color for vehicles")
+ private val crystalColor by setting("Crystal Color", Color(255, 0, 255), "Color for end crystals")
+ private val otherColor by setting("Other Color", Color(200, 200, 200), "Color for other entities")
private fun SafeContext.shouldRender(entity: Entity): Boolean {
if (entity == player && !self) return false
@@ -292,56 +112,4 @@ object EntityESP : Module(
else -> otherColor
}
}
-
- private fun getInterpolatedPos(entity: Entity, tickDelta: Float): Vec3d {
- val x = entity.lastRenderX + (entity.x - entity.lastRenderX) * tickDelta
- val y = entity.lastRenderY + (entity.y - entity.lastRenderY) * tickDelta
- val z = entity.lastRenderZ + (entity.z - entity.lastRenderZ) * tickDelta
- return Vec3d(x, y, z)
- }
-
- private fun SafeContext.getTracerStartPos(tickDelta: Float): Vec3d {
- val playerPos = getInterpolatedPos(player, tickDelta)
-
- return when (tracerOrigin) {
- TracerOrigin.Feet -> playerPos
- TracerOrigin.Center -> playerPos.add(0.0, player.height / 2.0, 0.0)
- TracerOrigin.Eyes ->
- playerPos.add(0.0, player.standingEyeHeight.toDouble(), 0.0)
- TracerOrigin.Crosshair -> {
- val camera = mc.gameRenderer?.camera ?: return playerPos
- camera.pos.add(Vec3d(camera.horizontalPlane).multiply(0.1))
- }
- }
- }
-
- private fun SafeContext.buildNameTag(entity: Entity): String {
- val builder = StringBuilder()
- val name = entity.displayName?.string ?: entity.type.name.string
- builder.append(name)
- if (nameTagHealth && entity is LivingEntity) {
- builder.append(" [${entity.health.toInt()}/${entity.maxHealth.toInt()}]")
- }
- if (nameTagDistance) {
- val dist = player.distanceTo(entity).toInt()
- builder.append(" ${dist}m")
- }
- return builder.toString()
- }
-
- enum class TracerOrigin(override val displayName: String) : NamedEnum {
- Feet("Feet"),
- Center("Center"),
- Eyes("Eyes"),
- Crosshair("Crosshair")
- }
-
- private enum class Group(override val displayName: String) : NamedEnum {
- General("General"),
- Entities("Entities"),
- Render("Render"),
- Tracers("Tracers"),
- NameTags("Name Tags"),
- Colors("Colors")
- }
}
diff --git a/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt b/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt
new file mode 100644
index 000000000..08b7952ed
--- /dev/null
+++ b/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt
@@ -0,0 +1,227 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.module.modules.render
+
+import com.lambda.Lambda.mc
+import com.lambda.config.applyEdits
+import com.lambda.config.groups.ScreenTextSettings
+import com.lambda.friend.FriendManager.isFriend
+import com.lambda.graphics.RenderMain.worldToScreenNormalized
+import com.lambda.graphics.mc.RenderBuilder
+import com.lambda.graphics.mc.renderer.ImmediateRenderer.Companion.immediateRenderer
+import com.lambda.graphics.text.FontHandler.getDefaultFont
+import com.lambda.graphics.util.DynamicAABB.Companion.interpolatedBox
+import com.lambda.module.Module
+import com.lambda.module.tag.ModuleTag
+import com.lambda.util.EntityUtils
+import com.lambda.util.EntityUtils.entityGroup
+import com.lambda.util.NamedEnum
+import com.lambda.util.extension.fullHealth
+import com.lambda.util.extension.maxFullHealth
+import com.lambda.util.math.MathUtils.roundToStep
+import com.lambda.util.math.distSq
+import com.lambda.util.math.lerp
+import net.minecraft.client.network.OtherClientPlayerEntity
+import net.minecraft.entity.Entity
+import net.minecraft.entity.EquipmentSlot
+import net.minecraft.entity.LivingEntity
+import net.minecraft.entity.player.PlayerEntity
+import net.minecraft.item.ItemStack
+import net.minecraft.util.math.Vec3d
+import org.joml.component1
+import org.joml.component2
+import java.awt.Color
+
+//ToDo: implement all settings
+object Nametags : Module(
+ name = "Nametags",
+ description = "Displays information about entities above them",
+ tag = ModuleTag.RENDER
+) {
+ private enum class Group(override val displayName: String) : NamedEnum {
+ General("General"),
+ Text("Text")
+ }
+
+ private enum class TextGroup(override val displayName: String): NamedEnum {
+ Friend("Friend"),
+ Other("Other")
+ }
+
+ private val entities by setting("Entities", setOf(EntityUtils.EntityGroup.Player), EntityUtils.EntityGroup.entries).group(Group.General)
+ private val itemScale by setting("Item Scale", 3f, 0.4f..5f, 0.01f).group(Group.General)
+ private val yOffset by setting("Y Offset", 0.2, 0.0..1.0, 0.01).group(Group.General)
+ private val spacing by setting("Spacing", 0, 0..10, 1).group(Group.General)
+ private val self by setting("Self", false).group(Group.General)
+ private val health by setting("Health", false).group(Group.General)
+ private val ping by setting("Ping", true).group(Group.General)
+ private val gear by setting("Gear", true).group(Group.General)
+ private val mainItem by setting("Main Item", true) { gear }.group(Group.General)
+ private val offhandItem by setting("Offhand Item", true) { gear }.group(Group.General)
+ private val itemName by setting("Item Name", true).group(Group.General)
+ private val itemNameScale by setting("Item Name Scale", 0.7f, 0.1f..1.0f, 0.01f) { itemName }.group(Group.General)
+ private val itemCount by setting("Item Count", true).group(Group.General)
+ private val durabilityMode by setting("Durability Mode", DurabilityMode.Text) { gear }.group(Group.General)
+ //ToDo: Implement
+// private val enchantments by setting("Enchantments", false) { gear }
+
+ private val friendTextConfig = ScreenTextSettings("Friend ", this, Group.Text, TextGroup.Friend).apply {
+ applyEdits {
+ ::textColor.edit { defaultValue(Color(0, 255, 255, 255)) }
+ }
+ }
+ private val otherTextConfig = ScreenTextSettings("Other ", this, Group.Text, TextGroup.Other)
+
+ var heightWidthRatio = 0f
+ var trueItemScaleX = 0f
+ var trueItemScaleY = 0f
+ var trueSpacingX = 0f
+ var trueSpacingY = 0f
+
+ init {
+ immediateRenderer("Nametags Immediate Renderer") { safeContext ->
+ with(safeContext) {
+ heightWidthRatio = mc.window.height / mc.window.width.toFloat()
+ trueItemScaleY = itemScale * 0.01f
+ trueItemScaleX = trueItemScaleY * heightWidthRatio
+ trueSpacingY = spacing * 0.0005f
+ trueSpacingX = trueSpacingY * heightWidthRatio
+
+ world.entities
+ .sortedByDescending { it distSq mc.gameRenderer.camera.pos }
+ .forEach { entity ->
+ val textConfig = if (entity is OtherClientPlayerEntity && entity.isFriend) friendTextConfig else otherTextConfig
+ val textStyle = textConfig.getSDFStyle()
+ val textSize = textConfig.size
+ if (!shouldRenderNametag(entity)) return@forEach
+ val nameText = entity.displayName?.string ?: return@forEach
+ val box = entity.interpolatedBox
+ val boxCenter = box.center
+ var (anchorX, anchorY) =
+ worldToScreenNormalized(Vec3d(boxCenter.x, box.maxY + yOffset, boxCenter.z))
+ ?: return@forEach
+
+ if (entity !is LivingEntity) {
+ screenText(nameText, anchorX, anchorY + (textSize / 2f), textSize, style = textStyle, centered = true)
+ return@forEach
+ }
+
+ if (itemName && !entity.mainHandStack.isEmpty) {
+ val itemNameText = entity.mainHandStack.name.string
+ val itemNameScale = textSize * itemNameScale
+ screenText(itemNameText, anchorX, anchorY, itemNameScale, centered = true)
+ anchorY += (itemNameScale * 1.1f) + trueSpacingY
+ }
+
+ val nameWidth = getDefaultFont().getStringWidthNormalized(nameText, textSize)
+
+ val healthCount = if (health) entity.fullHealth else -1.0
+ val healthText = if (health) " ${healthCount.roundToStep(0.01)}" else ""
+ val healthWidth =
+ getDefaultFont().getStringWidthNormalized(healthText, textSize)
+ .let { if (healthCount > 0) it + trueSpacingX else it }
+
+ val pingCount = if (ping && entity is PlayerEntity) connection.getPlayerListEntry(entity.uuid)?.latency ?: -1 else -1
+ val pingText = if (pingCount >= 0) " [$pingCount]" else ""
+ val pingWidth =
+ getDefaultFont().getStringWidthNormalized(pingText, textSize)
+ .let { if (pingCount >= 0) it + trueSpacingX else it }
+
+ var combinedWidth = nameWidth + healthWidth + pingWidth
+ val nameX = anchorX - (combinedWidth / 2)
+ screenText(nameText, nameX, anchorY, textSize, style = textStyle)
+ if (healthCount >= 0) {
+ val healthColor = lerp(entity.fullHealth / entity.maxFullHealth, Color.RED, Color.GREEN).brighter()
+ screenText(healthText, nameX + nameWidth + trueSpacingX, anchorY, textSize, style = textStyle.apply { color = healthColor })
+ }
+ if (pingCount >= 0) {
+ val pingColor = lerp(pingCount / 500.0, Color.GREEN, Color.RED).brighter()
+ screenText(pingText, nameX + nameWidth + healthWidth + trueSpacingX, anchorY, textSize, style = textStyle.apply { color = pingColor })
+ }
+
+ if (!gear) return@forEach
+
+ if (EquipmentSlot.entries.none { it.index in 1..4 && !entity.getEquippedStack(it).isEmpty }) {
+ if (mainItem && !entity.mainHandStack.isEmpty)
+ renderItem(entity.mainHandStack, nameX - trueItemScaleX - trueSpacingX - (trueItemScaleX * 0.1f), anchorY)
+ if (offhandItem && !entity.offHandStack.isEmpty)
+ renderItem(entity.offHandStack, anchorX + (combinedWidth / 2) + trueSpacingX, anchorY)
+ } else drawArmorAndItems(entity, anchorX, anchorY + textSize + trueSpacingY)
+ }
+ }
+ }
+ }
+
+ private fun RenderBuilder.drawArmorAndItems(entity: LivingEntity, x: Float, y: Float) {
+ val stepAmount = trueItemScaleX + trueSpacingX
+ var iteratorX = x - (stepAmount * 3) + (trueSpacingX / 2)
+ if (mainItem && !entity.mainHandStack.isEmpty) renderItem(entity.mainHandStack, iteratorX, y)
+ iteratorX += stepAmount
+ val headStack = entity.getEquippedStack(EquipmentSlot.HEAD)
+ val chestStack = entity.getEquippedStack(EquipmentSlot.CHEST)
+ val legsStack = entity.getEquippedStack(EquipmentSlot.LEGS)
+ val feetStack = entity.getEquippedStack(EquipmentSlot.FEET)
+ if (!headStack.isEmpty) renderItem(headStack, iteratorX, y)
+ iteratorX += stepAmount
+ if (!chestStack.isEmpty) renderItem(chestStack, iteratorX, y)
+ iteratorX += stepAmount
+ if (!legsStack.isEmpty) renderItem(legsStack, iteratorX, y)
+ iteratorX += stepAmount
+ if (!feetStack.isEmpty) renderItem(feetStack, iteratorX, y)
+ iteratorX += stepAmount
+ if (offhandItem && !entity.offHandStack.isEmpty) renderItem(entity.offHandStack, iteratorX, y)
+ }
+
+ private fun RenderBuilder.renderItem(stack: ItemStack, x: Float, y: Float) {
+ screenGuiItem(stack, x, y, trueItemScaleY, centered = false)
+ var iteratorY = y
+ iteratorY += trueItemScaleY
+ if (durabilityMode != DurabilityMode.None && stack.isDamageable) {
+ val dura = (1 - (stack.damage / stack.maxDamage.toDouble()))
+ if (durabilityMode.bar) {
+ val yOffset = trueItemScaleY / 16
+ val xOffset = trueItemScaleX / 16
+ val maxWidth = xOffset * 14
+ screenRect(x + xOffset, y + yOffset, maxWidth, yOffset * 2, Color.BLACK)
+ screenRect(x + xOffset, y + (yOffset * 2), maxWidth * dura.toFloat(), yOffset, lerp(dura, Color.RED, Color.GREEN).brighter())
+ }
+ if (durabilityMode.text) {
+ val duraText = "${(dura * 100).toInt()}%"
+ val textSize = getDefaultFont().getSizeForWidthNormalized(duraText, trueItemScaleX) * 0.9f
+ screenText(duraText, x + (trueItemScaleX / 2), iteratorY, textSize.coerceAtMost(trueItemScaleY * 0.33f), centered = true, style = RenderBuilder.SDFStyle(color = lerp(dura, Color.RED, Color.GREEN).brighter()))
+ }
+ }
+ if (itemCount && stack.isStackable && stack.count > 1) {
+ val countText = "${stack.count}"
+ val textSize = trueItemScaleY / 2
+ val textWidth = getDefaultFont().getStringWidthNormalized(countText, textSize)
+ screenText(countText, x - (textWidth - trueItemScaleX), y, textSize)
+ }
+ }
+
+ @JvmStatic
+ fun shouldRenderNametag(entity: Entity) =
+ entity.entityGroup in entities && (self || entity !== mc.player) && (entity !is LivingEntity || entity.isAlive)
+
+ private enum class DurabilityMode(val text: Boolean, val bar: Boolean) {
+ None(false, false),
+ Text(true, false),
+ Bar(false, true),
+ Both(true, true)
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/module/modules/render/NoRender.kt b/src/main/kotlin/com/lambda/module/modules/render/NoRender.kt
index 09b287385..5998ed581 100644
--- a/src/main/kotlin/com/lambda/module/modules/render/NoRender.kt
+++ b/src/main/kotlin/com/lambda/module/modules/render/NoRender.kt
@@ -19,10 +19,18 @@ package com.lambda.module.modules.render
import com.lambda.module.Module
import com.lambda.module.tag.ModuleTag
-import com.lambda.util.DynamicReflectionSerializer.remappedName
+import com.lambda.util.EntityUtils.blockEntityMap
+import com.lambda.util.EntityUtils.bossEntityMap
+import com.lambda.util.EntityUtils.createNameMap
+import com.lambda.util.EntityUtils.decorationEntityMap
+import com.lambda.util.EntityUtils.miscEntityMap
+import com.lambda.util.EntityUtils.mobEntityMap
+import com.lambda.util.EntityUtils.passiveEntityMap
+import com.lambda.util.EntityUtils.playerEntityMap
+import com.lambda.util.EntityUtils.projectileEntityMap
+import com.lambda.util.EntityUtils.vehicleEntityMap
import com.lambda.util.NamedEnum
import com.lambda.util.reflections.scanResult
-import io.github.classgraph.ClassInfo
import net.minecraft.block.entity.BlockEntity
import net.minecraft.client.particle.Particle
import net.minecraft.entity.Entity
@@ -34,20 +42,7 @@ object NoRender : Module(
description = "Disables rendering of certain things",
tag = ModuleTag.RENDER,
) {
- private val entities = scanResult
- .getSubclasses(Entity::class.java)
- .filter { !it.isAbstract && it.name.startsWith("net.minecraft") }
-
private val particleMap = createParticleNameMap()
- private val blockEntityMap = createBlockEntityNameMap()
- private val playerEntityMap = createEntityNameMap("net.minecraft.client.network.")
- private val bossEntityMap = createEntityNameMap("net.minecraft.entity.boss.")
- private val decorationEntityMap = createEntityNameMap("net.minecraft.entity.decoration.")
- private val mobEntityMap = createEntityNameMap("net.minecraft.entity.mob.")
- private val passiveEntityMap = createEntityNameMap("net.minecraft.entity.passive.")
- private val projectileEntityMap = createEntityNameMap("net.minecraft.entity.projectile.")
- private val vehicleEntityMap = createEntityNameMap("net.minecraft.entity.vehicle.")
- private val miscEntityMap = createEntityNameMap("net.minecraft.entity.", strictDir = true)
private enum class Group(override val displayName: String) : NamedEnum {
Hud("Hud"),
@@ -112,37 +107,6 @@ object NoRender : Module(
.filter { !it.isAbstract }
.createNameMap("net.minecraft.client.particle.", "Particle")
- private fun createEntityNameMap(directory: String, strictDir: Boolean = false) =
- entities.createNameMap(directory, "Entity", strictDir)
-
- private fun createBlockEntityNameMap() =
- scanResult
- .getSubclasses(BlockEntity::class.java)
- .filter { !it.isAbstract }.createNameMap("net.minecraft.block.entity", "BlockEntity")
-
- private fun Collection.createNameMap(
- directory: String,
- removePattern: String = "",
- strictDirectory: Boolean = false
- ) = map {
- val remappedName = it.name.remappedName
- val displayName = remappedName
- .substring(remappedName.indexOfLast { it == '.' } + 1)
- .replace(removePattern, "")
- .fancyFormat()
- MappingInfo(it.simpleName, remappedName, displayName)
- }
- .sortedBy { it.displayName.lowercase() }
- .filter { info ->
- if (strictDirectory)
- info.remapped.startsWith(directory) && !info.remapped.substring(directory.length).contains(".")
- else info.remapped.startsWith(directory)
- }
- .associate { it.raw to it.displayName }
-
- private fun String.fancyFormat() =
- replace("$", " - ").replace("(?.
- */
-
-package com.lambda.module.modules.render
-
-import com.lambda.Lambda.mc
-import com.lambda.context.SafeContext
-import com.lambda.event.events.MovementEvent
-import com.lambda.event.events.PlayerEvent
-import com.lambda.event.events.RenderEvent
-import com.lambda.event.events.TickEvent
-import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.graphics.buffer.vertex.attributes.VertexAttrib
-import com.lambda.graphics.buffer.vertex.attributes.VertexMode
-import com.lambda.graphics.gl.GlStateUtils.withBlendFunc
-import com.lambda.graphics.gl.GlStateUtils.withDepth
-import com.lambda.graphics.gl.Matrices
-import com.lambda.graphics.gl.Matrices.buildWorldProjection
-import com.lambda.graphics.gl.Matrices.withVertexTransform
-import com.lambda.graphics.pipeline.VertexBuilder
-import com.lambda.graphics.pipeline.VertexPipeline
-import com.lambda.graphics.shader.Shader
-import com.lambda.gui.components.ClickGuiLayout
-import com.lambda.interaction.managers.rotating.Rotation
-import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
-import com.lambda.util.extension.partialTicks
-import com.lambda.util.math.DOWN
-import com.lambda.util.math.MathUtils.random
-import com.lambda.util.math.UP
-import com.lambda.util.math.lerp
-import com.lambda.util.math.multAlpha
-import com.lambda.util.math.plus
-import com.lambda.util.math.times
-import com.lambda.util.math.transform
-import com.lambda.util.player.MovementUtils.moveDelta
-import com.lambda.util.world.raycast.InteractionMask
-import com.mojang.blaze3d.opengl.GlConst.GL_ONE
-import com.mojang.blaze3d.opengl.GlConst.GL_SRC_ALPHA
-import net.minecraft.entity.Entity
-import net.minecraft.util.math.Vec3d
-import org.joml.Matrix4f
-import kotlin.math.sin
-
-// FixMe: Do not call render stuff in the initialization block
-object Particles : Module(
- name = "Particles",
- description = "Spawns fancy particles",
- tag = ModuleTag.RENDER,
-) {
- // ToDo: resort, cleanup settings
- private val duration by setting("Duration", 5.0, 1.0..500.0, 1.0)
- private val fadeDuration by setting("Fade Ticks", 5.0, 1.0..30.0, 1.0)
- private val spawnAmount by setting("Spawn Amount", 20, 3..500, 1)
- private val sizeSetting by setting("Size", 2.0, 0.1..50.0, 0.1)
- private val alphaSetting by setting("Alpha", 1.5, 0.01..2.0, 0.01)
- private val speedH by setting("Speed H", 1.0, 0.0..10.0, 0.1)
- private val speedV by setting("Speed V", 1.0, 0.0..10.0, 0.1)
- private val inertia by setting("Inertia", 0.0, 0.0..1.0, 0.01)
- private val gravity by setting("Gravity", 0.2, 0.0..1.0, 0.01)
- private val onMove by setting("On Move", false)
-
- private val environment by setting("Environment", true)
- private val environmentSpawnAmount by setting("E Spawn Amount", 10, 3..100, 1) { environment }
- private val environmentSize by setting("E Size", 2.0, 0.1..50.0, 0.1) { environment }
- private val environmentRange by setting("E Spread", 5.0, 1.0..20.0, 0.1) { environment }
- private val environmentSpeedH by setting("E Speed H", 0.0, 0.0..10.0, 0.1) { environment }
- private val environmentSpeedV by setting("E Speed V", 0.1, 0.0..10.0, 0.1) { environment }
-
- private var particles = mutableListOf()
- private val pipeline = VertexPipeline(VertexMode.Triangles, VertexAttrib.Group.PARTICLE)
- private val shader = Shader("shaders/vertex/particles.glsl", "shaders/fragment/particles.glsl")
-
- init {
- listen {
- if (environment) spawnForEnvironment()
- particles.removeIf(Particle::update)
- }
-
- listen {
- // Todo: interpolated tickbased upload?
- val builder = pipeline.build()
- particles.forEach { it.build(builder) }
-
- withBlendFunc(GL_SRC_ALPHA, GL_ONE) {
- shader.use()
- pipeline.upload(builder)
- withDepth(false, pipeline::render)
- pipeline.clear()
- }
- }
-
- listen { event ->
- spawnForEntity(event.entity)
- }
-
- listen {
- if (!onMove || player.moveDelta < 0.05) return@listen
- spawnForEntity(player)
- }
- }
-
- private fun spawnForEntity(entity: Entity) {
- repeat(spawnAmount) {
- val i = (it + 1) / spawnAmount.toDouble()
-
- val pos = entity.pos
- val height = entity.boundingBox.lengthY
- val spawnHeight = height * transform(i, 0.0, 1.0, 0.2, 0.8)
- val particlePos = pos.add(0.0, spawnHeight, 0.0)
- val particleMotion = Rotation(
- random(-180.0, 180.0),
- random(-90.0, 90.0)
- ).vector * Vec3d(speedH, speedV, speedH) * 0.1
-
- particles += Particle(particlePos, particleMotion, false)
- }
- }
-
- private fun SafeContext.spawnForEnvironment() {
- if (mc.paused) return
- repeat(environmentSpawnAmount) {
- var particlePos = player.pos + Rotation(random(-180.0, 180.0), 0.0).vector * random(0.0, environmentRange)
-
- Rotation.DOWN.rayCast(6.0, particlePos + UP * 2.0, true, InteractionMask.Block)?.pos?.let {
- particlePos = it + UP * 0.03
- } ?: return@repeat
-
- val particleMotion = Rotation(
- random(-180.0, 180.0),
- random(-90.0, 90.0)
- ).vector * Vec3d(environmentSpeedH, environmentSpeedV, environmentSpeedH) * 0.1
-
- particles += Particle(particlePos, particleMotion, true)
- }
- }
-
- private class Particle(
- initialPosition: Vec3d,
- initialMotion: Vec3d,
- val lay: Boolean,
- ) {
- private val fadeTicks = fadeDuration
-
- private var age = 0
- private val maxAge = (duration + random(0.0, 20.0)).toInt()
-
- private var prevPos = initialPosition
- private var position = initialPosition
- private var motion = initialMotion
-
- private val projRotation = if (lay) Matrices.ProjRotationMode.Up else Matrices.ProjRotationMode.ToCamera
-
- fun update(): Boolean {
- if (mc.paused) return false
- age++
-
- prevPos = position
-
- if (!lay) motion += DOWN * gravity * 0.01
- motion *= 0.9 + inertia * 0.1
-
- position += motion
-
- return age > maxAge + fadeTicks * 2 + 5
- }
-
- fun build(builder: VertexBuilder) = builder.apply {
- val smoothAge = age + mc.partialTicks
- val colorTicks = smoothAge * 0.1 / ClickGuiLayout.colorSpeed
-
- val alpha = when {
- smoothAge < fadeTicks -> smoothAge / fadeTicks
- smoothAge in fadeTicks..fadeTicks + maxAge -> 1.0
- else -> {
- val min = fadeTicks + maxAge
- val max = fadeTicks * 2 + maxAge
- transform(smoothAge, min, max, 1.0, 0.0)
- }
- }
-
- val (c1, c2) = ClickGuiLayout.primaryColor to ClickGuiLayout.secondaryColor
- val color = lerp(sin(colorTicks) * 0.5 + 0.5, c1, c2).multAlpha(alpha * alphaSetting)
-
- val position = lerp(mc.partialTicks, prevPos, position)
- val size = if (lay) environmentSize else sizeSetting * lerp(alpha, 0.5, 1.0)
-
- withVertexTransform(buildWorldProjection(position, size, projRotation)) {
- buildQuad(
- vertex {
- vec3m(-1.0, -1.0, 0.0).vec2(0.0, 0.0).color(color)
- },
- vertex {
- vec3m(-1.0, 1.0, 0.0).vec2(0.0, 1.0).color(color)
- },
- vertex {
- vec3m(1.0, 1.0, 0.0).vec2(1.0, 1.0).color(color)
- },
- vertex {
- vec3m(1.0, -1.0, 0.0).vec2(1.0, 0.0).color(color)
- }
- )
- }
- }
- }
-}
diff --git a/src/main/kotlin/com/lambda/module/modules/render/Search.kt b/src/main/kotlin/com/lambda/module/modules/render/Search.kt
new file mode 100644
index 000000000..f621cb3ad
--- /dev/null
+++ b/src/main/kotlin/com/lambda/module/modules/render/Search.kt
@@ -0,0 +1,219 @@
+/*
+ * Copyright 2025 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.module.modules.render
+
+import com.lambda.config.applyEdits
+import com.lambda.config.groups.ScreenLineSettings
+import com.lambda.config.groups.WorldLineSettings
+import com.lambda.config.settings.collections.CollectionSetting.Companion.onDeselect
+import com.lambda.config.settings.collections.CollectionSetting.Companion.onSelect
+import com.lambda.context.SafeContext
+import com.lambda.event.events.WorldEvent
+import com.lambda.event.listener.SafeListener.Companion.listen
+import com.lambda.graphics.RenderMain
+import com.lambda.graphics.mc.RenderBuilder
+import com.lambda.graphics.mc.renderer.ChunkedRenderer.Companion.chunkedRenderer
+import com.lambda.graphics.mc.renderer.ImmediateRenderer.Companion.immediateRenderer
+import com.lambda.graphics.util.DirectionMask
+import com.lambda.graphics.util.DirectionMask.buildSideMesh
+import com.lambda.graphics.util.DynamicAABB.Companion.interpolatedBox
+import com.lambda.module.Module
+import com.lambda.module.tag.ModuleTag
+import com.lambda.threading.runSafe
+import com.lambda.util.EntityUtils.decorationEntityMap
+import com.lambda.util.EntityUtils.entityGroup
+import com.lambda.util.extension.blockColor
+import com.lambda.util.extension.entityColor
+import com.lambda.util.extension.getBlockState
+import com.lambda.util.math.setAlpha
+import com.lambda.util.world.toBlockPos
+import io.ktor.util.collections.ConcurrentMap
+import net.fabricmc.fabric.mixin.block.BlockStateMixin
+import net.minecraft.block.BlockState
+import net.minecraft.block.Blocks
+import net.minecraft.entity.Entity
+import net.minecraft.util.math.BlockPos
+import net.minecraft.util.math.Box
+import net.minecraft.util.math.Vec3d
+import java.awt.Color
+
+object Search : Module(
+ name = "Search",
+ description = "Highlight blocks within the rendered world",
+ tag = ModuleTag.RENDER,
+) {
+ private val blocks by setting("Blocks", setOf(Blocks.CHEST, Blocks.ENDER_CHEST, Blocks.NETHER_PORTAL, Blocks.END_PORTAL, Blocks.END_PORTAL_FRAME, Blocks.END_GATEWAY), description = "Render blocks")
+ .onSelect { rebuildMesh(this) }.onDeselect { rebuildMesh(this) }
+ private val entities by setting("Entities", decorationEntityMap.values)
+ .onSelect { rebuildMesh(this) }.onDeselect { rebuildMesh(this) }
+
+ private var fill: Boolean by setting("Fill", true, "Fill the faces of blocks").onValueChange(::rebuildMesh).onValueChange { _, to -> if (!to) outline = true }
+ private var outline: Boolean by setting("Outline", true, "Draw the outlines of blocks").onValueChange(::rebuildMesh).onValueChange { _, to -> if (!to) fill = true }
+ private val tracers by setting("Tracers", true, "Draw a line from your cursor to the highlighted position")
+ private val mesh by setting("Mesh", true, "Connect similar adjacent blocks").onValueChange(::rebuildMesh)
+
+ private val useNaturalColor by setting("Use Natural Color", false, "Use the color of the block instead").onValueChange(::rebuildMesh)
+ private val naturalColorAlpha by setting("Natural Color Alpha", 0.3, 0.1..1.0, 0.05) { useNaturalColor }.onValueChange(::rebuildMesh)
+ private val naturalTracerAlpha by setting("Natural Tracer Alpha", 1.0, 0.1..1.0, 0.05) { useNaturalColor }.onValueChange(::rebuildMesh)
+ private val minimumNaturalBrightness by setting("Min Brightness", 150, 0..255, 1) { useNaturalColor }.onValueChange(::rebuildMesh)
+
+ private val blockFillColor by setting("Block Fill Color", Color(100, 150, 255, 51), "Color of the surfaces") { fill && !useNaturalColor }.onValueChange(::rebuildMesh)
+ private val blockLineColor by setting("Block Line Color", Color(100, 150, 255, 128)) { outline && !useNaturalColor }.onValueChange(::rebuildMesh)
+ private val entityFillColor by setting("Entity Fill Color", Color(100, 150, 255, 51)) { fill && !useNaturalColor }.onValueChange(::rebuildMesh)
+ private val entityOutlineColor by setting("Entity Outline Color", Color(100, 150, 255, 128)) { outline && !useNaturalColor }.onValueChange(::rebuildMesh)
+
+ private val blockOutlineMode by setting("Block Outline Mode", DirectionMask.OutlineMode.And, "Outline mode") { outline }.onValueChange(::rebuildMesh)
+ private val outlineConfig = WorldLineSettings("Outline ", this) { outline }.apply {
+ applyEdits {
+ hide(::startColor, ::endColor)
+ settings.forEach { it.onValueChange(::rebuildMesh) }
+ }
+ }
+ private val tracerConfig = ScreenLineSettings("Tracer ", this).apply {
+ applyEdits {
+ editTyped(::startColor, ::endColor) {
+ visibility { { !useNaturalColor } }
+ }
+ }
+ }
+
+ private val tracerBlockPositions = ConcurrentMap>>()
+
+ val chunkedRenderer = chunkedRenderer("Chunked Search") { world, position ->
+ runSafe {
+ val pos = position.toBlockPos()
+ val state = world.getBlockState(pos)
+ if (state.block !in blocks) {
+ tracerBlockPositions.remove(pos)
+ return@chunkedRenderer
+ }
+
+ val sides = if (mesh) {
+ buildSideMesh(position) {
+ world.getBlockState(it).block in blocks
+ }
+ } else DirectionMask.ALL
+
+ val lineColor = getBlockColor(state, position.toBlockPos())
+ val fillColor = Color(lineColor.red, lineColor.green, lineColor.blue, (naturalColorAlpha * 255).toInt())
+ val shape = state.getOutlineShape(world, pos)
+ val boxes =
+ if (shape.isEmpty) listOf(Box(pos))
+ else shape.boundingBoxes.map { it.offset(pos) }
+ if (tracers) {
+ val center =
+ shape
+ .boundingBoxes
+ .reduce(Box::union)
+ .offset(pos)
+ .center
+ tracerBlockPositions[pos] = Pair(center, getTracerColors(lineColor))
+ }
+ box(
+ boxes,
+ sides.inv(),
+ if (useNaturalColor) fillColor else blockFillColor,
+ if (useNaturalColor) lineColor else blockLineColor
+ )
+ }
+ }
+
+ init {
+ immediateRenderer("Immediate Search") { safeContext ->
+ safeContext.world.entities.forEach { entity ->
+ if (entity.entityGroup.nameToDisplayNameMap[entity::class.simpleName] in entities) {
+ val entityColor = getEntityColor(entity)
+ box(
+ listOf(entity.interpolatedBox),
+ DirectionMask.NONE,
+ if (useNaturalColor) entityColor.setAlpha(naturalColorAlpha) else entityFillColor,
+ if (useNaturalColor) entityColor else entityOutlineColor
+ )
+ if (tracers) tracer(Pair(entity.interpolatedBox.center, getTracerColors(entityColor)))
+ }
+ }
+ if (tracers) tracerBlockPositions.values.forEach { tracer(it) }
+ }
+
+ listen { event ->
+ if (tracers) tracerBlockPositions.keys.removeIf { it in event.chunk.pos }
+ }
+ }
+
+ private fun RenderBuilder.tracer(pair: Pair>) {
+ val endPoint = RenderMain.worldToScreenNormalized(pair.first) ?: return
+ val startColor = if (useNaturalColor) pair.second.first else tracerConfig.startColor
+ val endColor = if (useNaturalColor) pair.second.second else tracerConfig.endColor
+ screenLineGradient(
+ 0.5f, 0.5f,
+ startColor,
+ endPoint.x, endPoint.y,
+ endColor,
+ tracerConfig.width,
+ tracerConfig.getDashStyle()
+ )
+ }
+
+ private fun RenderBuilder.box(boxes: List, ignoreSides: Int, fillColor: Color, lineColor: Color) {
+ boxes.forEach { box ->
+ box(box, outlineConfig.width) {
+ hideSides(ignoreSides)
+ if (fill) fillColor(fillColor) else hideFill()
+ if (!outline) hideOutline()
+ else {
+ outlineColor(lineColor)
+ outlineConfig.getDashStyle()?.let { lineDashStyle(it) }
+ outlineMode(blockOutlineMode)
+ }
+ }
+ }
+ }
+
+ private fun getTracerColors(naturalColor: Color): Pair =
+ if (useNaturalColor) {
+ val adjustedNaturalColor = naturalColor.setAlpha(naturalTracerAlpha)
+ Pair(adjustedNaturalColor, adjustedNaturalColor)
+ } else Pair(tracerConfig.startColor, tracerConfig.endColor)
+
+ private fun getEntityColor(entity: Entity) =
+ entityColor(entity).ensureMinBrightness(minimumNaturalBrightness)
+ private fun SafeContext.getBlockColor(state: BlockState, pos: BlockPos) =
+ blockColor(state, pos).ensureMinBrightness(minimumNaturalBrightness)
+
+ /**
+ * Ensures a color meets the minimum brightness threshold.
+ * If the color is too dark, scales up the RGB values proportionally.
+ */
+ private fun Color.ensureMinBrightness(minBrightness: Int): Color {
+ if (minBrightness <= 0) return this
+
+ val brightness = maxOf(red, green, blue)
+ if (brightness >= minBrightness) return this
+ if (brightness == 0) return Color(minBrightness, minBrightness, minBrightness, alpha)
+
+ val scale = minBrightness.toFloat() / brightness
+ return Color(
+ (red * scale).toInt().coerceIn(0, 255),
+ (green * scale).toInt().coerceIn(0, 255),
+ (blue * scale).toInt().coerceIn(0, 255),
+ alpha
+ )
+ }
+
+ private fun rebuildMesh(ctx: SafeContext, from: Any? = null, to: Any? = null): Unit = chunkedRenderer.rebuild()
+}
diff --git a/src/main/kotlin/com/lambda/module/modules/render/StorageESP.kt b/src/main/kotlin/com/lambda/module/modules/render/StorageESP.kt
deleted file mode 100644
index e20dbe40d..000000000
--- a/src/main/kotlin/com/lambda/module/modules/render/StorageESP.kt
+++ /dev/null
@@ -1,197 +0,0 @@
-/*
- * Copyright 2025 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.module.modules.render
-
-import com.lambda.context.SafeContext
-import com.lambda.graphics.esp.ShapeScope
-import com.lambda.graphics.renderer.esp.DirectionMask
-import com.lambda.graphics.renderer.esp.DirectionMask.buildSideMesh
-import com.lambda.event.events.onStaticRender
-import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
-import com.lambda.util.world.blockEntitySearch
-import com.lambda.util.world.entitySearch
-import com.lambda.threading.runSafe
-import com.lambda.util.NamedEnum
-import com.lambda.util.extension.blockColor
-import com.lambda.util.math.setAlpha
-import net.minecraft.block.entity.BarrelBlockEntity
-import net.minecraft.block.entity.BlastFurnaceBlockEntity
-import net.minecraft.block.entity.BlockEntity
-import net.minecraft.block.entity.BrewingStandBlockEntity
-import net.minecraft.block.entity.ChestBlockEntity
-import net.minecraft.block.entity.DispenserBlockEntity
-import net.minecraft.block.entity.EnderChestBlockEntity
-import net.minecraft.block.entity.FurnaceBlockEntity
-import net.minecraft.block.entity.HopperBlockEntity
-import net.minecraft.block.entity.ShulkerBoxBlockEntity
-import net.minecraft.block.entity.SmokerBlockEntity
-import net.minecraft.block.entity.TrappedChestBlockEntity
-import net.minecraft.entity.Entity
-import net.minecraft.entity.decoration.ItemFrameEntity
-import net.minecraft.entity.vehicle.AbstractMinecartEntity
-import net.minecraft.entity.vehicle.MinecartEntity
-import java.awt.Color
-
-object StorageESP : Module(
- name = "StorageESP",
- description = "Render storage blocks/entities",
- tag = ModuleTag.RENDER,
-) {
- private val distance by setting("Distance", 64.0, 10.0..256.0, 1.0, "Maximum distance for rendering").group(Group.General)
- private var drawFaces: Boolean by setting("Draw Faces", true, "Draw faces of blocks").group(Group.Render)
- private var drawEdges: Boolean by setting("Draw Edges", true, "Draw edges of blocks").group(Group.Render)
- private val mode by setting("Outline Mode", DirectionMask.OutlineMode.And, "Outline mode").group(Group.Render)
- private val mesh by setting("Mesh", true, "Connect similar adjacent blocks").group(Group.Render)
- private val useBlockColor by setting("Use Block Color", true, "Use the color of the block instead").group(Group.Color)
- private val facesAlpha by setting("Faces Alpha", 0.3, 0.1..1.0, 0.05).group(Group.Color)
- private val edgesAlpha by setting("Edges Alpha", 0.3, 0.1..1.0, 0.05).group(Group.Color)
- private val outlineWidth by setting("Outline Width", 1.0f, 0.5f..5.0f, 0.5f) { drawEdges }.group(Group.Render)
-
- // TODO:
- // val blockColors by setting("Block Colors", mapOf()) { page == Page.Color
- // &&
- // !useBlockColor }
- // val renders by setting("Render Blocks", mapOf()) { page == Page.General
- // }
- //
- // TODO: Create enum of MapColors
-
- // I used this to extract the colors as rgb format
- // > function extract(color) {
- // ... console.log((color >> 16) & 0xFF)
- // ... console.log((color >> 8) & 0xFF)
- // ... console.log(color & 0xFF)
- // ... }
-
- private val barrelColor by setting("Barrel Color", Color(143, 119, 72)) { !useBlockColor }.group(Group.Color)
- private val blastFurnaceColor by setting("Blast Furnace Color", Color(153, 153, 153)) { !useBlockColor }.group(Group.Color)
- private val brewingStandColor by setting("Brewing Stand Color", Color(167, 167, 167)) { !useBlockColor }.group(Group.Color)
- private val trappedChestColor by setting("Trapped Chest Color", Color(216, 127, 51)) { !useBlockColor }.group(Group.Color)
- private val chestColor by setting("Chest Color", Color(216, 127, 51)) { !useBlockColor }.group(Group.Color)
- private val dispenserColor by setting("Dispenser Color", Color(153, 153, 153)) { !useBlockColor }.group(Group.Color)
- private val enderChestColor by setting("Ender Chest Color", Color(127, 63, 178)) { !useBlockColor }.group(Group.Color)
- private val furnaceColor by setting("Furnace Color", Color(153, 153, 153)) { !useBlockColor }.group(Group.Color)
- private val hopperColor by setting("Hopper Color", Color(76, 76, 76)) { !useBlockColor }.group(Group.Color)
- private val smokerColor by setting("Smoker Color", Color(112, 112, 112)) { !useBlockColor }.group(Group.Color)
- private val shulkerColor by setting("Shulker Color", Color(178, 76, 216)) { !useBlockColor }.group(Group.Color)
- private val itemFrameColor by setting("Item Frame Color", Color(216, 127, 51)) { !useBlockColor }.group(Group.Color)
- private val cartColor by setting("Minecart Color", Color(102, 127, 51)) { !useBlockColor }.group(Group.Color)
-
- private val entities = setOf(
- BarrelBlockEntity::class,
- BlastFurnaceBlockEntity::class,
- BrewingStandBlockEntity::class,
- TrappedChestBlockEntity::class,
- ChestBlockEntity::class,
- DispenserBlockEntity::class,
- EnderChestBlockEntity::class,
- FurnaceBlockEntity::class,
- HopperBlockEntity::class,
- SmokerBlockEntity::class,
- ShulkerBoxBlockEntity::class,
- AbstractMinecartEntity::class,
- ItemFrameEntity::class,
- MinecartEntity::class,
- )
-
- init {
- onStaticRender { esp ->
- blockEntitySearch(distance)
- .filter { it::class in entities }
- .forEach { be ->
- esp.shapes(be.pos.x.toDouble(), be.pos.y.toDouble(), be.pos.z.toDouble()) {
- build(be, excludedSides(be))
- }
- }
-
- val mineCarts =
- entitySearch(distance).filter {
- it::class in entities
- }
- val itemFrames =
- entitySearch(distance).filter {
- it::class in entities
- }
- (mineCarts + itemFrames).forEach { entity ->
- esp.shapes(entity.getX(), entity.getY(), entity.getZ()) {
- build(entity, DirectionMask.ALL)
- }
- }
- }
- }
-
- private fun SafeContext.excludedSides(blockEntity: BlockEntity): Int {
- val isFullCube = blockEntity.cachedState.isFullCube(world, blockEntity.pos)
- return if (mesh && isFullCube) {
- buildSideMesh(blockEntity.pos) { neighbor ->
- val other =
- world.getBlockEntity(neighbor) ?: return@buildSideMesh false
- val otherFullCube = other.cachedState.isFullCube(world, other.pos)
- val sameType =
- blockEntity.cachedState.block == other.cachedState.block
- val searchedFor = other::class in entities
-
- searchedFor && otherFullCube && sameType
- }
- } else DirectionMask.ALL
- }
-
- private fun ShapeScope.build(block: BlockEntity, sides: Int) = runSafe {
- val color =
- if (useBlockColor) blockColor(block.cachedState, block.pos)
- else block.color ?: return@runSafe
- box(block, color.setAlpha(facesAlpha), color.setAlpha(edgesAlpha), sides, mode, thickness = outlineWidth)
- }
-
- private fun ShapeScope.build(entity: Entity, sides: Int) = runSafe {
- val color = entity.color ?: return@runSafe
- box(entity, color.setAlpha(facesAlpha), color.setAlpha(edgesAlpha), sides, mode, thickness = outlineWidth)
- }
-
- private val BlockEntity?.color
- get() =
- when (this) {
- is BarrelBlockEntity -> barrelColor
- is BlastFurnaceBlockEntity -> blastFurnaceColor
- is BrewingStandBlockEntity -> brewingStandColor
- is TrappedChestBlockEntity -> trappedChestColor
- is ChestBlockEntity -> chestColor
- is DispenserBlockEntity -> dispenserColor
- is EnderChestBlockEntity -> enderChestColor
- is FurnaceBlockEntity -> furnaceColor
- is HopperBlockEntity -> hopperColor
- is SmokerBlockEntity -> smokerColor
- is ShulkerBoxBlockEntity -> shulkerColor
- else -> null
- }
-
- private val Entity?.color
- get() =
- when (this) {
- is AbstractMinecartEntity -> cartColor
- is ItemFrameEntity -> itemFrameColor
- else -> null
- }
-
- private enum class Group(override val displayName: String) : NamedEnum {
- General("General"),
- Render("Render"),
- Color("Color")
- }
-}
diff --git a/src/main/kotlin/com/lambda/module/modules/render/Tracers.kt b/src/main/kotlin/com/lambda/module/modules/render/Tracers.kt
new file mode 100644
index 000000000..39cf1df67
--- /dev/null
+++ b/src/main/kotlin/com/lambda/module/modules/render/Tracers.kt
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.module.modules.render
+
+import com.lambda.config.applyEdits
+import com.lambda.config.groups.ScreenLineSettings
+import com.lambda.event.events.RenderEvent
+import com.lambda.event.events.ScreenRenderEvent
+import com.lambda.event.listener.SafeListener.Companion.listen
+import com.lambda.friend.FriendManager.isFriend
+import com.lambda.graphics.RenderMain.worldToScreenNormalized
+import com.lambda.graphics.mc.renderer.ImmediateRenderer
+import com.lambda.graphics.mc.renderer.ImmediateRenderer.Companion.immediateRenderer
+import com.lambda.module.Module
+import com.lambda.module.tag.ModuleTag
+import com.lambda.util.EntityUtils.EntityGroup
+import com.lambda.util.EntityUtils.entityGroup
+import com.lambda.util.NamedEnum
+import com.lambda.util.extension.prevPos
+import com.lambda.util.extension.tickDelta
+import com.lambda.util.math.dist
+import com.lambda.util.math.lerp
+import net.minecraft.client.network.OtherClientPlayerEntity
+import org.joml.Vector2f
+import org.joml.component1
+import org.joml.component2
+import java.awt.Color
+
+object Tracers : Module(
+ name = "Tracers",
+ description = "Draws lines to entities within the world",
+ tag = ModuleTag.RENDER
+) {
+ private enum class Group(override val displayName: String) : NamedEnum {
+ General("General"),
+ Color("Color"),
+ LineStyle("Line Style")
+ }
+
+ private enum class LineGroup(override val displayName: String) : NamedEnum {
+ Friend("Friend"),
+ Other("Other")
+ }
+
+ private val target by setting("Target", TracerMode.Feet).group(Group.General)
+ private val stem by setting("Stem", true).group(Group.General)
+ private val entities by setting("Entities", setOf(EntityGroup.Player, EntityGroup.Mob, EntityGroup.Boss), EntityGroup.entries).group(Group.General)
+ private val friendColor by setting("Friend Color", Color(0, 255, 255, 255)).group(Group.Color)
+ private val playerDistanceGradient by setting("Player Distance Gradient", true) { EntityGroup.Player in entities }.group(Group.Color)
+ private val playerDistanceColorFar by setting("Player Far Color", Color.GREEN) { EntityGroup.Player in entities && playerDistanceGradient }.group(Group.Color)
+ private val playerDistanceColorClose by setting("Player Close Color", Color.RED) { EntityGroup.Player in entities && playerDistanceGradient }.group(Group.Color)
+ private val playerColor by setting("Players", Color.RED) { EntityGroup.Player in entities && !playerDistanceGradient }.group(Group.Color)
+ private val mobColor by setting("Mobs", Color(255, 80, 0, 255)) { EntityGroup.Mob in entities }.group(Group.Color)
+ private val passiveColor by setting("Passives", Color.BLUE) { EntityGroup.Passive in entities }.group(Group.Color)
+ private val projectileColor by setting("Projectiles", Color.LIGHT_GRAY) { EntityGroup.Projectile in entities }.group(Group.Color)
+ private val vehicleColor by setting("Vehicles", Color.WHITE) { EntityGroup.Vehicle in entities }.group(Group.Color)
+ private val decorationColor by setting("Decorations", Color.PINK) { EntityGroup.Decoration in entities }.group(Group.Color)
+ private val bossColor by setting("Bosses", Color(255, 0, 255, 255)) { EntityGroup.Boss in entities }.group(Group.Color)
+ private val miscColor by setting("Miscellaneous", Color.magenta) { EntityGroup.Misc in entities }.group(Group.Color)
+
+ private val friendLineConfig = ScreenLineSettings("Friend ", this, Group.LineStyle, LineGroup.Friend).apply {
+ applyEdits { hide(::startColor, ::endColor) }
+ }
+ private val otherLineConfig = ScreenLineSettings("Other ", this, Group.LineStyle, LineGroup.Other).apply {
+ applyEdits { hide(::startColor, ::endColor) }
+ }
+
+ init {
+ immediateRenderer("Tracers Immediate Renderer") { safeContext ->
+ with(safeContext) {
+ world.entities.forEach { entity ->
+ if (entity === player) return@forEach
+ val entityGroup = entity.entityGroup
+ if (entityGroup !in entities) return@forEach
+ val color = if (entity is OtherClientPlayerEntity) {
+ if (entity.isFriend) friendColor
+ else {
+ if (playerDistanceGradient) {
+ val distance = player dist entity
+ lerp(distance / 60.0, playerDistanceColorClose, playerDistanceColorFar)
+ } else playerColor
+ }
+ } else when (entityGroup) {
+ EntityGroup.Player -> playerColor
+ EntityGroup.Mob -> mobColor
+ EntityGroup.Passive -> passiveColor
+ EntityGroup.Projectile -> projectileColor
+ EntityGroup.Vehicle -> vehicleColor
+ EntityGroup.Decoration -> decorationColor
+ EntityGroup.Boss -> bossColor
+ else -> miscColor
+ }
+ val lineConfig = if (entity is OtherClientPlayerEntity && entity.isFriend) friendLineConfig else otherLineConfig
+ val lerpedPos = lerp(mc.tickDelta, entity.prevPos, entity.pos)
+ val lerpedEyePos = lerpedPos.add(0.0, entity.standingEyeHeight.toDouble(), 0.0)
+ val targetPos = when(target) {
+ TracerMode.Feet -> lerpedPos
+ TracerMode.Middle -> lerpedPos.add(0.0, entity.standingEyeHeight / 2.0, 0.0)
+ TracerMode.Eyes -> lerpedEyePos
+ }
+ val (toX, toY) = worldToScreenNormalized(targetPos) ?: return@forEach
+ screenLine(0.5f, 0.5f, toX, toY, color, lineConfig.width, lineConfig.getDashStyle())
+ if (stem) {
+ val (lowerX, lowerY) =
+ if (target == TracerMode.Feet) Vector2f(toX, toY)
+ else worldToScreenNormalized(lerpedPos) ?: return@forEach
+ val (upperX, upperY) =
+ if (target == TracerMode.Eyes) Vector2f(toX, toY)
+ else worldToScreenNormalized(lerpedEyePos) ?: return@forEach
+ screenLine(lowerX, lowerY, upperX, upperY, color, lineConfig.width, lineConfig.getDashStyle())
+ }
+ }
+ }
+ }
+ }
+
+ private enum class TracerMode {
+ Feet,
+ Middle,
+ Eyes
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/module/modules/render/Zoom.kt b/src/main/kotlin/com/lambda/module/modules/render/Zoom.kt
index 26b7a44e7..963685d61 100644
--- a/src/main/kotlin/com/lambda/module/modules/render/Zoom.kt
+++ b/src/main/kotlin/com/lambda/module/modules/render/Zoom.kt
@@ -61,10 +61,6 @@ object Zoom : Module(
event.cancel()
}
- listen(alwaysListen = true) {
- updateCurrentZoom()
- }
-
onEnable {
updateZoomTime()
}
diff --git a/src/main/kotlin/com/lambda/util/DebugInfoHud.kt b/src/main/kotlin/com/lambda/util/DebugInfoHud.kt
index 198eb0a1d..cee004368 100644
--- a/src/main/kotlin/com/lambda/util/DebugInfoHud.kt
+++ b/src/main/kotlin/com/lambda/util/DebugInfoHud.kt
@@ -23,7 +23,7 @@ import com.lambda.command.CommandRegistry
import com.lambda.event.EventFlow
import com.lambda.module.ModuleRegistry
import com.lambda.util.Formatting.format
-import com.lambda.util.extension.tickDelta
+import com.lambda.util.extension.tickDeltaF
import net.minecraft.util.Formatting
import net.minecraft.util.hit.BlockHitResult
import net.minecraft.util.hit.EntityHitResult
@@ -55,7 +55,7 @@ object DebugInfoHud {
null -> add("Crosshair Target: None")
}
- add("Eye Pos: ${mc.cameraEntity?.getCameraPosVec(mc.tickDelta)?.format()}")
+ add("Eye Pos: ${mc.cameraEntity?.getCameraPosVec(mc.tickDeltaF)?.format()}")
return
}
diff --git a/src/main/kotlin/com/lambda/util/EntityUtils.kt b/src/main/kotlin/com/lambda/util/EntityUtils.kt
index ebecce7da..9dacb7753 100644
--- a/src/main/kotlin/com/lambda/util/EntityUtils.kt
+++ b/src/main/kotlin/com/lambda/util/EntityUtils.kt
@@ -17,11 +17,51 @@
package com.lambda.util
+import com.lambda.util.DynamicReflectionSerializer.remappedName
import com.lambda.util.math.MathUtils.floorToInt
+import com.lambda.util.reflections.scanResult
+import io.github.classgraph.ClassInfo
+import io.github.classgraph.ClassInfoList
+import net.minecraft.block.entity.BlockEntity
import net.minecraft.entity.Entity
import net.minecraft.util.math.BlockPos
+import kotlin.jvm.java
object EntityUtils {
+ val entities: Collection = scanResult
+ .getSubclasses(Entity::class.java)
+ .filter { !it.isAbstract && it.name.startsWith("net.minecraft") }
+
+ val blockEntityMap = createBlockEntityNameMap()
+ val playerEntityMap = createEntityNameMap("net.minecraft.client.network.")
+ val bossEntityMap = createEntityNameMap("net.minecraft.entity.boss.")
+ val decorationEntityMap = createEntityNameMap("net.minecraft.entity.decoration.")
+ val mobEntityMap = createEntityNameMap("net.minecraft.entity.mob.")
+ val passiveEntityMap = createEntityNameMap("net.minecraft.entity.passive.")
+ val projectileEntityMap = createEntityNameMap("net.minecraft.entity.projectile.")
+ val vehicleEntityMap = createEntityNameMap("net.minecraft.entity.vehicle.")
+ val miscEntityMap = createEntityNameMap("net.minecraft.entity.", strictDir = true)
+
+ enum class EntityGroup(val nameToDisplayNameMap: Map) {
+ Player(createEntityNameMap("net.minecraft.client.network.")),
+ Mob(createEntityNameMap("net.minecraft.entity.mob.")),
+ Passive(createEntityNameMap("net.minecraft.entity.passive.")),
+ Projectile(createEntityNameMap("net.minecraft.entity.projectile.")),
+ Vehicle(createEntityNameMap("net.minecraft.entity.vehicle.")),
+ Decoration(createEntityNameMap("net.minecraft.entity.decoration.")),
+ Boss(createEntityNameMap("net.minecraft.entity.boss.")),
+ Misc(createEntityNameMap("net.minecraft.entity.", strictDir = true)),
+ Block(createBlockEntityNameMap())
+ }
+
+ val Entity.entityGroup get() = entityGroup()
+ val BlockEntity.entityGroup: EntityGroup get() = entityGroup()
+
+ private fun Any.entityGroup(): EntityGroup {
+ val simpleName = javaClass.simpleName
+ return EntityGroup.entries.first { simpleName in it.nameToDisplayNameMap }
+ }
+
fun Entity.getPositionsWithinHitboxXZ(minY: Int, maxY: Int): Set {
val hitbox = boundingBox
val minX = hitbox.minX.floorToInt()
@@ -38,4 +78,41 @@ object EntityUtils {
}
return positions
}
+
+ private fun createEntityNameMap(directory: String, strictDir: Boolean = false) =
+ entities.createNameMap(directory, "Entity", strictDir)
+
+ private fun createBlockEntityNameMap() =
+ scanResult
+ .getSubclasses(BlockEntity::class.java)
+ .filter { !it.isAbstract }
+ .createNameMap("net.minecraft.block.entity", "BlockEntity")
+
+ fun Collection.createNameMap(
+ directory: String,
+ removePattern: String = "",
+ strictDirectory: Boolean = false
+ ) = map {
+ val remappedName = it.name.remappedName
+ val displayName = remappedName
+ .substring(remappedName.indexOfLast { it == '.' } + 1)
+ .replace(removePattern, "")
+ .fancyFormat()
+ MappingInfo(it.simpleName, remappedName, displayName)
+ }.sortedBy { it.displayName.lowercase() }
+ .filter { info ->
+ if (strictDirectory)
+ info.remapped.startsWith(directory) && !info.remapped.substring(directory.length).contains(".")
+ else info.remapped.startsWith(directory)
+ }
+ .associate { it.raw to it.displayName }
+
+ private fun String.fancyFormat() =
+ replace("$", " - ").replace("(?>()
+ override val visibility: () -> Boolean = { true }
override val locale: Locale = Locale.US
override val separator: String = ","
override val prefix: String = "("
diff --git a/src/main/kotlin/com/lambda/util/extension/Entity.kt b/src/main/kotlin/com/lambda/util/extension/Entity.kt
index c7bc6e583..8d790cddd 100644
--- a/src/main/kotlin/com/lambda/util/extension/Entity.kt
+++ b/src/main/kotlin/com/lambda/util/extension/Entity.kt
@@ -31,6 +31,9 @@ val Entity.rotation
val LivingEntity.fullHealth: Double
get() = health + absorptionAmount.toDouble()
+val LivingEntity.maxFullHealth: Double
+ get() = maxHealth + maxAbsorption.toDouble()
+
var LivingEntity.isElytraFlying
get() = isGliding
set(value) {
diff --git a/src/main/kotlin/com/lambda/util/extension/Mixin.kt b/src/main/kotlin/com/lambda/util/extension/Mixin.kt
index 30c210929..7b539430e 100644
--- a/src/main/kotlin/com/lambda/util/extension/Mixin.kt
+++ b/src/main/kotlin/com/lambda/util/extension/Mixin.kt
@@ -19,8 +19,8 @@ package com.lambda.util.extension
import net.minecraft.client.MinecraftClient
-val MinecraftClient.partialTicks
- get() = tickDelta.toDouble()
-
val MinecraftClient.tickDelta
+ get() = tickDeltaF.toDouble()
+
+val MinecraftClient.tickDeltaF
get() = renderTickCounter.getTickProgress(true)
diff --git a/src/main/kotlin/com/lambda/util/extension/World.kt b/src/main/kotlin/com/lambda/util/extension/World.kt
index a1b2f5124..64bba101f 100644
--- a/src/main/kotlin/com/lambda/util/extension/World.kt
+++ b/src/main/kotlin/com/lambda/util/extension/World.kt
@@ -17,20 +17,36 @@
package com.lambda.util.extension
+import com.lambda.Lambda.mc
import com.lambda.context.SafeContext
import com.lambda.util.world.FastVector
import com.lambda.util.world.toBlockPos
import com.lambda.util.world.x
import com.lambda.util.world.y
import com.lambda.util.world.z
+import net.minecraft.block.Block
import net.minecraft.block.BlockState
import net.minecraft.block.Blocks
+import net.minecraft.client.render.entity.EntityRenderer
+import net.minecraft.client.render.model.BlockStateManagers
+import net.minecraft.client.texture.NativeImage
+import net.minecraft.client.texture.Sprite
+import net.minecraft.entity.Entity
+import net.minecraft.entity.EntityType
+import net.minecraft.entity.decoration.ItemFrameEntity
+import net.minecraft.entity.decoration.painting.PaintingEntity
import net.minecraft.fluid.FluidState
import net.minecraft.fluid.Fluids
+import net.minecraft.util.Atlases
+import net.minecraft.util.Identifier
import net.minecraft.util.math.BlockPos
+import net.minecraft.util.math.ColorHelper
import net.minecraft.util.shape.VoxelShape
import net.minecraft.world.World
import java.awt.Color
+import java.util.concurrent.ConcurrentHashMap
+
+private val blockColorCache = ConcurrentHashMap()
val SafeContext.worldName: String
get() = when {
@@ -53,16 +69,306 @@ fun SafeContext.collisionShape(state: BlockState, pos: BlockPos): VoxelShape =
fun SafeContext.outlineShape(state: BlockState, pos: BlockPos) =
state.getOutlineShape(world, pos).offset(pos)
+/**
+ * Gets the average color of a block from its texture.
+ * Results are cached by block type for performance.
+ * Falls back to map color if texture access fails.
+ */
fun SafeContext.blockColor(state: BlockState, pos: BlockPos): Color {
- return when (state.block) {
- Blocks.ENDER_CHEST -> Color(0xFF00FF)
- Blocks.NETHER_PORTAL -> Color(0xaa00aa)
- Blocks.END_PORTAL -> Color(0xFF00FF)
- else ->
- Color(state.getMapColor(world, pos).color, false)
+ return blockColorCache.getOrPut(state.block) {
+ calculateAverageTextureColor(state) ?: Color(state.getMapColor(world, pos).color, false)
+ }
+}
+
+private val entityColorCache = ConcurrentHashMap, Color>()
+
+/**
+ * Gets the average color of an entity from its texture.
+ * Results are cached by entity type for performance.
+ * Falls back to gray if texture access fails.
+ */
+fun entityColor(entity: Entity): Color {
+ return entityColorCache.getOrPut(entity.type) {
+ calculateEntityTextureColor(entity) ?: Color(128, 128, 128)
}
}
+/**
+ * Calculates the average color from an entity's texture.
+ * Uses the entity renderer to get the texture identifier, then samples the texture.
+ * Supports both living and non-living entities using reflection to find texture methods/fields.
+ */
+private fun calculateEntityTextureColor(entity: Entity): Color? {
+ return try {
+ // Special handling for sprite-based entities
+ if (entity is ItemFrameEntity) return calculateItemFrameColor(entity)
+ if (entity is PaintingEntity) return calculatePaintingColor(entity)
+
+ val renderer = mc.entityRenderDispatcher.getRenderer(entity) ?: return null
+ val textureId = getTextureFromRenderer(entity, renderer) ?: return null
+ calculateAverageColorFromTexture(textureId)
+ } catch (_: Exception) { null }
+}
+
+/**
+ * Attempts to get the texture identifier from an entity renderer using reflection.
+ * Tries the following approaches in order:
+ * 1. Look for a getTexture(RenderState) method and call it
+ * 2. Look for Identifier-typed fields (like 'texture' or 'TEXTURE')
+ */
+private fun getTextureFromRenderer(entity: Entity, renderer: EntityRenderer<*, *>): Identifier? {
+ val rendererClass = renderer.javaClass
+
+ // Approach 1: Try to find and call a getTexture method
+ val textureFromMethod = tryGetTextureFromMethod(entity, renderer, rendererClass)
+ if (textureFromMethod != null) return textureFromMethod
+
+ // Approach 2: Try to find an Identifier field
+ val textureFromField = tryGetTextureFromField(renderer, rendererClass)
+ if (textureFromField != null) return textureFromField
+
+ return null
+}
+
+/**
+ * Tries to get texture by calling a getTexture(RenderState) method via reflection.
+ */
+private fun tryGetTextureFromMethod(entity: Entity, renderer: EntityRenderer<*, *>, rendererClass: Class<*>): Identifier? {
+ return try {
+ // Find getTexture method that takes a single parameter
+ val getTextureMethod = rendererClass.methods.find { method ->
+ method.name == "getTexture" &&
+ method.parameterCount == 1 &&
+ Identifier::class.java.isAssignableFrom(method.returnType)
+ } ?: return null
+
+ getTextureMethod.isAccessible = true
+
+ // Create and update render state
+ val createStateMethod = rendererClass.getMethod("createRenderState")
+ val state = createStateMethod.invoke(renderer)
+
+ // Try to update the state with entity data
+ try {
+ val updateStateMethod = rendererClass.methods.find {
+ it.name == "updateRenderState" && it.parameterCount == 3
+ }
+ updateStateMethod?.invoke(renderer, entity, state, 0f)
+ } catch (_: Exception) { /* State update is optional */ }
+
+ getTextureMethod.invoke(renderer, state) as? Identifier
+ } catch (_: Exception) { null }
+}
+
+/**
+ * Tries to get texture from an Identifier-typed field via reflection.
+ * Searches for common field names like 'texture', 'TEXTURE', etc.
+ */
+private fun tryGetTextureFromField(renderer: EntityRenderer<*, *>, rendererClass: Class<*>): Identifier? {
+ return try {
+ // Look through all fields in the class hierarchy for Identifier types
+ var currentClass: Class<*>? = rendererClass
+ while (currentClass != null && currentClass != EntityRenderer::class.java) {
+ for (field in currentClass.declaredFields) {
+ if (Identifier::class.java.isAssignableFrom(field.type)) {
+ field.isAccessible = true
+ val value = field.get(renderer)
+ if (value is Identifier) return value
+
+ // Also try static fields
+ if (java.lang.reflect.Modifier.isStatic(field.modifiers)) {
+ val staticValue = field.get(null)
+ if (staticValue is Identifier) return staticValue
+ }
+ }
+ }
+ currentClass = currentClass.superclass
+ }
+ null
+ } catch (_: Exception) { null }
+}
+
+/**
+ * Calculates the average color from an item frame entity using the block model sprite.
+ */
+private fun calculateItemFrameColor(entity: ItemFrameEntity): Color? {
+ return try {
+ val isGlow = entity.type == EntityType.GLOW_ITEM_FRAME
+ val hasMap = entity.getMapId(entity.heldItemStack) != null
+ val blockState = BlockStateManagers.getStateForItemFrame(isGlow, hasMap)
+ val sprite = mc.blockRenderManager.models.getModelParticleSprite(blockState)
+ calculateAverageColorFromSprite(sprite)
+ } catch (_: Exception) { null }
+}
+
+/**
+ * Calculates the average color from a painting entity using the painting variant's sprite.
+ */
+private fun calculatePaintingColor(entity: PaintingEntity): Color? {
+ return try {
+ val variant = entity.variant.value()
+ val paintingAtlas = mc.atlasManager.getAtlasTexture(Atlases.PAINTINGS)
+ val sprite = paintingAtlas.getSprite(variant.assetId())
+ calculateAverageColorFromSprite(sprite)
+ } catch (_: Exception) { null }
+}
+
+/**
+ * Calculates the average color from a sprite.
+ */
+private fun calculateAverageColorFromSprite(sprite: Sprite): Color? {
+ return try {
+ val contents = sprite.contents
+ val image = contents.image
+
+ val width = contents.width
+ val height = contents.height
+
+ var totalR = 0L
+ var totalG = 0L
+ var totalB = 0L
+ var totalWeight = 0L
+
+ for (y in 0 until height) {
+ for (x in 0 until width) {
+ val argb = image.getColorArgb(x, y)
+ val alpha = ColorHelper.getAlpha(argb)
+
+ if (alpha == 0) continue
+
+ val r = ColorHelper.getRed(argb)
+ val g = ColorHelper.getGreen(argb)
+ val b = ColorHelper.getBlue(argb)
+
+ totalR += r.toLong() * alpha
+ totalG += g.toLong() * alpha
+ totalB += b.toLong() * alpha
+ totalWeight += alpha.toLong()
+ }
+ }
+
+ if (totalWeight == 0L) return null
+
+ val avgR = (totalR / totalWeight).toInt().coerceIn(0, 255)
+ val avgG = (totalG / totalWeight).toInt().coerceIn(0, 255)
+ val avgB = (totalB / totalWeight).toInt().coerceIn(0, 255)
+
+ Color(avgR, avgG, avgB)
+ } catch (_: Exception) { null }
+}
+
+/**
+ * Calculates the average color from a texture identifier.
+ */
+private fun calculateAverageColorFromTexture(textureId: Identifier): Color? {
+ return try {
+ // Try to load the texture from the resource manager
+ val resource = mc.resourceManager.getResource(textureId).orElse(null) ?: run {
+ // If that fails, try constructing a texture path with .png extension
+ val pngId = Identifier.of(textureId.namespace, textureId.path + ".png")
+ mc.resourceManager.getResource(pngId).orElse(null)
+ } ?: return null
+
+ resource.inputStream.use { inputStream ->
+ val image = NativeImage.read(inputStream)
+
+ val width = image.width
+ val height = image.height
+
+ var totalR = 0L
+ var totalG = 0L
+ var totalB = 0L
+ var totalWeight = 0L
+
+ for (y in 0 until height) {
+ for (x in 0 until width) {
+ val abgr = image.getColorArgb(x, y)
+ val alpha = ColorHelper.getAlpha(abgr)
+
+ if (alpha == 0) continue
+
+ val r = ColorHelper.getRed(abgr)
+ val g = ColorHelper.getGreen(abgr)
+ val b = ColorHelper.getBlue(abgr)
+
+ totalR += r.toLong() * alpha
+ totalG += g.toLong() * alpha
+ totalB += b.toLong() * alpha
+ totalWeight += alpha.toLong()
+ }
+ }
+
+ image.close()
+
+ if (totalWeight == 0L) return null
+
+ val avgR = (totalR / totalWeight).toInt().coerceIn(0, 255)
+ val avgG = (totalG / totalWeight).toInt().coerceIn(0, 255)
+ val avgB = (totalB / totalWeight).toInt().coerceIn(0, 255)
+
+ Color(avgR, avgG, avgB)
+ }
+ } catch (_: Exception) { null }
+}
+
+/**
+ * Calculates the average color from a block's particle sprite texture.
+ * Uses alpha-weighted averaging to ignore transparent pixels.
+ * Applies block tints (for grass, leaves, water, etc.) using BlockColors.
+ */
+private fun calculateAverageTextureColor(state: BlockState): Color? {
+ return try {
+ val sprite = mc.blockRenderManager.models.getModelParticleSprite(state)
+ val contents = sprite.contents
+ val image = contents.image
+
+ val width = contents.width
+ val height = contents.height
+
+ var totalR = 0L
+ var totalG = 0L
+ var totalB = 0L
+ var totalWeight = 0L
+
+ for (y in 0 until height) {
+ for (x in 0 until width) {
+ val argb = image.getColorArgb(x, y)
+ val alpha = ColorHelper.getAlpha(argb)
+
+ if (alpha == 0) continue
+
+ val r = ColorHelper.getRed(argb)
+ val g = ColorHelper.getGreen(argb)
+ val b = ColorHelper.getBlue(argb)
+
+ totalR += r.toLong() * alpha
+ totalG += g.toLong() * alpha
+ totalB += b.toLong() * alpha
+ totalWeight += alpha.toLong()
+ }
+ }
+
+ if (totalWeight == 0L) return null
+
+ var avgR = (totalR / totalWeight).toInt().coerceIn(0, 255)
+ var avgG = (totalG / totalWeight).toInt().coerceIn(0, 255)
+ var avgB = (totalB / totalWeight).toInt().coerceIn(0, 255)
+
+ val tint = mc.blockColors.getColor(state, null, null, 0)
+ if (tint != -1) {
+ val tintR = ColorHelper.getRed(tint)
+ val tintG = ColorHelper.getGreen(tint)
+ val tintB = ColorHelper.getBlue(tint)
+
+ avgR = (avgR * tintR / 255).coerceIn(0, 255)
+ avgG = (avgG * tintG / 255).coerceIn(0, 255)
+ avgB = (avgB * tintB / 255).coerceIn(0, 255)
+ }
+
+ Color(avgR, avgG, avgB)
+ } catch (_: Exception) { null }
+}
+
fun World.getBlockState(x: Int, y: Int, z: Int): BlockState {
if (isOutOfHeightLimit(y)) return Blocks.VOID_AIR.defaultState
diff --git a/src/main/kotlin/com/lambda/util/math/Color.kt b/src/main/kotlin/com/lambda/util/math/Color.kt
index 3c65162d6..5c821be1a 100644
--- a/src/main/kotlin/com/lambda/util/math/Color.kt
+++ b/src/main/kotlin/com/lambda/util/math/Color.kt
@@ -20,6 +20,9 @@ package com.lambda.util.math
import net.minecraft.util.math.Vec3d
import java.awt.Color
+fun Color.setAlpha(value: Int) =
+ Color(red, green, blue, value.coerceIn(0, 255))
+
fun Color.setAlpha(value: Double) =
Color(red, green, blue, (value * 255.0).coerceIn(0.0, 255.0).toInt())
diff --git a/src/main/kotlin/com/lambda/util/reflections/Reflections.kt b/src/main/kotlin/com/lambda/util/reflections/Reflections.kt
index c7bcfbeb9..36570a564 100644
--- a/src/main/kotlin/com/lambda/util/reflections/Reflections.kt
+++ b/src/main/kotlin/com/lambda/util/reflections/Reflections.kt
@@ -37,7 +37,6 @@ val KClass<*>.className: String get() = java.name
.substringAfter("${java.packageName}.")
.replace('$', '.')
-
/**
* This function returns a instance of subtype [T].
*
diff --git a/src/main/resources/assets/lambda/shaders/core/advanced_lines.fsh b/src/main/resources/assets/lambda/shaders/core/advanced_lines.fsh
deleted file mode 100644
index 7727eca9d..000000000
--- a/src/main/resources/assets/lambda/shaders/core/advanced_lines.fsh
+++ /dev/null
@@ -1,50 +0,0 @@
-#version 330
-
-#moj_import
-#moj_import
-
-in vec4 vertexColor;
-noperspective in float v_LineDist;
-noperspective in float v_LineWidth;
-noperspective in vec2 v_DistPixels;
-noperspective in float v_LineLength;
-in float sphericalVertexDistance;
-in float cylindricalVertexDistance;
-
-out vec4 fragColor;
-
-void main() {
- // Closest point on the center line segment [0, L]
- float closestX = clamp(v_DistPixels.x, 0.0, v_LineLength);
- vec2 closestPoint = vec2(closestX, 0.0);
-
- // Pixel distance from the closest point (Round Capsule SDF)
- float dist = length(v_DistPixels - closestPoint);
-
- // SDF value: distance from the capsule edge
- float sdf = dist - (v_LineWidth / 2.0);
-
- // Ultra-sharp edges (AA transition of 0.3 pixels total)
- float alpha;
- if (v_LineWidth >= 1.0) {
- alpha = smoothstep(0.15, -0.15, sdf);
- } else {
- // Super thin lines: reduce opacity instead of shrinking width
- float transverseAlpha = (1.0 - smoothstep(0.0, 1.0, abs(v_DistPixels.y))) * v_LineWidth;
- alpha = transverseAlpha;
- }
-
- // Aggressive fade for tiny segments far away to prevent blobbing
- // If a segment is less than 0.8px on screen, fade it out to nothing
- float lengthFade = clamp(v_LineLength / 0.8, 0.0, 1.0);
- alpha *= lengthFade * lengthFade; // Quadratic falloff for tiny segments
-
- if (alpha <= 0.0) {
- discard;
- }
-
- vec4 color = vertexColor * ColorModulator;
- color.a *= alpha;
-
- fragColor = apply_fog(color, sphericalVertexDistance, cylindricalVertexDistance, FogEnvironmentalStart, FogEnvironmentalEnd, FogRenderDistanceStart, FogRenderDistanceEnd, FogColor);
-}
diff --git a/src/main/resources/assets/lambda/shaders/core/advanced_lines.vsh b/src/main/resources/assets/lambda/shaders/core/advanced_lines.vsh
deleted file mode 100644
index 46e84da2a..000000000
--- a/src/main/resources/assets/lambda/shaders/core/advanced_lines.vsh
+++ /dev/null
@@ -1,74 +0,0 @@
-#version 330
-
-#moj_import
-#moj_import
-#moj_import
-#moj_import
-
-in vec3 Position;
-in vec4 Color;
-in vec3 Normal;
-in float LineWidth;
-
-out vec4 vertexColor;
-noperspective out float v_LineDist;
-noperspective out float v_LineWidth;
-noperspective out vec2 v_DistPixels;
-noperspective out float v_LineLength;
-out float sphericalVertexDistance;
-out float cylindricalVertexDistance;
-
-const float VIEW_SHRINK = 1.0 - (1.0 / 256.0);
-
-void main() {
- int vertexIndex = gl_VertexID % 4;
- bool isStart = (vertexIndex < 2);
-
- float actualWidth = max(LineWidth, 0.1);
- float padding = 0.5; // AA padding
- float halfWidthExtended = actualWidth / 2.0 + padding;
-
- // Transform start and end
- vec4 posStart = ProjMat * ModelViewMat * vec4(isStart ? Position : Position - Normal, 1.0);
- vec4 posEnd = ProjMat * ModelViewMat * vec4(isStart ? Position + Normal : Position, 1.0);
-
- vec3 ndcStart = posStart.xyz / posStart.w;
- vec3 ndcEnd = posEnd.xyz / posEnd.w;
-
- // Screen space coordinates
- vec2 screenStart = (ndcStart.xy * 0.5 + 0.5) * ScreenSize;
- vec2 screenEnd = (ndcEnd.xy * 0.5 + 0.5) * ScreenSize;
-
- vec2 delta = screenEnd - screenStart;
- float lenPixels = length(delta);
-
- // Stable direction
- vec2 lineDir = (lenPixels > 0.001) ? delta / lenPixels : vec2(1.0, 0.0);
- vec2 lineNormal = vec2(-lineDir.y, lineDir.x);
-
- // Quad vertex layout
- float side = (vertexIndex == 0 || vertexIndex == 3) ? -1.0 : 1.0;
- float longitudinalSide = isStart ? -1.0 : 1.0;
-
- // Expansion in pixels: full radius + padding to contain capsule end
- vec2 offsetPixels = lineNormal * side * halfWidthExtended + lineDir * longitudinalSide * halfWidthExtended;
-
- // Current point NDC
- vec3 ndcThis = isStart ? ndcStart : ndcEnd;
- float wThis = isStart ? posStart.w : posEnd.w;
-
- // Convert pixel offset back to NDC
- vec2 offsetNDC = (offsetPixels / ScreenSize) * 2.0;
- gl_Position = vec4((ndcThis + vec3(offsetNDC, 0.0)) * wThis, wThis);
-
- vertexColor = Color;
-
- // Pass coordinates for SDF
- v_LineDist = side;
- v_DistPixels = vec2(isStart ? -halfWidthExtended : lenPixels + halfWidthExtended, side * halfWidthExtended);
- v_LineWidth = actualWidth;
- v_LineLength = lenPixels;
-
- sphericalVertexDistance = fog_spherical_distance(Position);
- cylindricalVertexDistance = fog_cylindrical_distance(Position);
-}
diff --git a/src/main/resources/assets/lambda/shaders/core/screen_faces.fsh b/src/main/resources/assets/lambda/shaders/core/screen_faces.fsh
new file mode 100644
index 000000000..b6bdde8e1
--- /dev/null
+++ b/src/main/resources/assets/lambda/shaders/core/screen_faces.fsh
@@ -0,0 +1,24 @@
+#version 330
+
+#moj_import
+
+// Inputs from vertex shader
+in vec4 v_Color;
+in float v_Layer;
+
+out vec4 fragColor;
+
+void main() {
+ // Apply color modulator
+ vec4 color = v_Color * ColorModulator;
+
+ // Discard nearly transparent fragments
+ if (color.a < 0.004) {
+ discard;
+ }
+
+ fragColor = color;
+
+ // Map layer to depth: higher layer = smaller depth = renders on top (LEQUAL)
+ gl_FragDepth = (1000.0 - v_Layer) / 2000.0;
+}
diff --git a/src/main/resources/assets/lambda/shaders/core/screen_faces.vsh b/src/main/resources/assets/lambda/shaders/core/screen_faces.vsh
new file mode 100644
index 000000000..8e0ef30a9
--- /dev/null
+++ b/src/main/resources/assets/lambda/shaders/core/screen_faces.vsh
@@ -0,0 +1,22 @@
+#version 330
+
+#moj_import
+#moj_import
+
+// Vertex inputs - matches SCREEN_FACE_FORMAT
+in vec3 Position; // Screen-space position (x, y, 0)
+in vec4 Color;
+in float Layer; // Layer depth for draw order
+
+// Outputs to fragment shader
+out vec4 v_Color;
+out float v_Layer;
+
+void main() {
+ // Transform to clip space
+ gl_Position = ProjMat * ModelViewMat * vec4(Position, 1.0);
+
+ // Pass data to fragment shader
+ v_Color = Color;
+ v_Layer = Layer;
+}
diff --git a/src/main/resources/assets/lambda/shaders/core/screen_image.fsh b/src/main/resources/assets/lambda/shaders/core/screen_image.fsh
new file mode 100644
index 000000000..f2c8f6645
--- /dev/null
+++ b/src/main/resources/assets/lambda/shaders/core/screen_image.fsh
@@ -0,0 +1,58 @@
+#version 330
+
+#moj_import
+#moj_import
+
+// Sampler for main texture
+uniform sampler2D Sampler0;
+// Sampler for overlay texture (e.g., enchantment glint)
+uniform sampler2D Sampler1;
+
+// Inputs from vertex shader
+in vec2 v_TexCoord;
+in vec4 v_Color;
+in vec4 v_OverlayUV; // (overlayU, overlayV, hasOverlay, diffuseAmount)
+in float v_Layer;
+
+out vec4 fragColor;
+
+void main() {
+ // Sample main texture
+ vec4 texColor = texture(Sampler0, v_TexCoord);
+
+ // Apply tint color
+ vec4 color = texColor * v_Color * ColorModulator;
+
+ // Discard nearly transparent fragments
+ if (color.a < 0.004) {
+ discard;
+ }
+
+ // Apply overlay (enchantment glint) if present
+ // v_OverlayUV.y = aspect ratio (width/height) for square tiling
+ // v_OverlayUV.z = hasOverlay flag (1.0 = enabled)
+ if (v_OverlayUV.z > 0.5) {
+ float aspectRatio = v_OverlayUV.y;
+
+ // Use texture coordinates corrected for aspect ratio
+ // This ensures square tiling regardless of image dimensions
+ vec2 baseUV = vec2(v_TexCoord.x * aspectRatio, v_TexCoord.y);
+
+ // Apply TextureMat from DynamicTransforms - this contains the glint transform
+ // calculated at render time using Util.getMeasuringTimeMs(), exactly like vanilla!
+ // TextureMat = translation(-scrollX, scrollY) * rotateZ(π/18) * scale
+ vec4 transformedUV = TextureMat * vec4(baseUV, 0.0, 1.0);
+
+ // Sample glint texture using transformed coordinates
+ vec4 glint = texture(Sampler1, fract(transformedUV.xy));
+
+ // Apply with squared additive blending (matching 1.21 model parity)
+ vec3 layer = glint.rgb * glint.a * 0.75; // GLINT_ALPHA = 0.75
+ color.rgb += (layer * layer);
+ }
+
+ fragColor = color;
+
+ // Map layer to depth: higher layer = smaller depth = renders on top (LEQUAL)
+ gl_FragDepth = (1000.0 - v_Layer) / 2000.0;
+}
diff --git a/src/main/resources/assets/lambda/shaders/core/screen_image.vsh b/src/main/resources/assets/lambda/shaders/core/screen_image.vsh
new file mode 100644
index 000000000..d4ee9619f
--- /dev/null
+++ b/src/main/resources/assets/lambda/shaders/core/screen_image.vsh
@@ -0,0 +1,28 @@
+#version 330
+
+#moj_import
+#moj_import
+
+// Vertex inputs - matches SCREEN_IMAGE_FORMAT
+in vec3 Position; // Screen-space position (x, y, 0)
+in vec2 UV0; // Main texture UV coordinates
+in vec4 Color; // Tint color
+in vec4 OverlayUV; // vec4(overlayU, overlayV, hasOverlay, diffuseAmount)
+in float Layer; // Layer depth for draw order
+
+// Outputs to fragment shader
+out vec2 v_TexCoord;
+out vec4 v_Color;
+out vec4 v_OverlayUV;
+out float v_Layer;
+
+void main() {
+ // Transform to clip space
+ gl_Position = ProjMat * ModelViewMat * vec4(Position, 1.0);
+
+ // Pass data to fragment shader
+ v_TexCoord = UV0;
+ v_Color = Color;
+ v_OverlayUV = OverlayUV;
+ v_Layer = Layer;
+}
diff --git a/src/main/resources/assets/lambda/shaders/core/screen_lines.fsh b/src/main/resources/assets/lambda/shaders/core/screen_lines.fsh
new file mode 100644
index 000000000..eef02b698
--- /dev/null
+++ b/src/main/resources/assets/lambda/shaders/core/screen_lines.fsh
@@ -0,0 +1,121 @@
+#version 330
+
+#moj_import
+#moj_import
+
+// Inputs from vertex shader
+in vec4 v_Color;
+in vec2 v_ExpandedPos; // Fragment position (expanded for AA)
+flat in vec2 v_LineStart; // Line start point
+flat in vec2 v_LineEnd; // Line end point
+flat in float v_LineWidth; // Line width
+flat in float v_SegmentLength; // Segment length
+flat in vec4 v_Dash; // Dash params (x=dashLen, y=gapLen, z=offset, w=speed)
+in float v_Layer; // Layer depth for draw order
+
+out vec4 fragColor;
+
+void main() {
+ // ===== CAPSULE SDF =====
+ vec2 lineDir = normalize(v_LineEnd - v_LineStart);
+ vec2 perpDir = vec2(-lineDir.y, lineDir.x);
+
+ vec2 toFragment = v_ExpandedPos - v_LineStart;
+ float projLength = dot(toFragment, lineDir);
+ float perpDist = abs(dot(toFragment, perpDir));
+
+ // Calculate stable pixel size from screen-space position derivatives
+ // This is more reliable than fwidth(dist2D) which can be unstable at edges
+ vec2 dPos_dx = dFdx(v_ExpandedPos);
+ vec2 dPos_dy = dFdy(v_ExpandedPos);
+ // Average pixel size in screen units
+ float pixelSize = (length(dPos_dx) + length(dPos_dy)) * 0.5;
+
+ // For end caps, compute actual distance to endpoints
+ float dist2D;
+ if (projLength < 0.0) {
+ dist2D = length(v_ExpandedPos - v_LineStart);
+ } else if (projLength > v_SegmentLength) {
+ dist2D = length(v_ExpandedPos - v_LineEnd);
+ } else {
+ dist2D = perpDist;
+ }
+
+ // Calculate screen line width in pixels
+ float screenLineWidth = v_LineWidth / max(pixelSize, 0.0001);
+
+ // Minimum 1-pixel rendering width - thinner lines scale alpha instead of getting gaps
+ float minWidth = pixelSize; // 1 pixel
+ float effectiveRadius = max(v_LineWidth * 0.5, minWidth * 0.5);
+
+ // Alpha scaling: lines < 1px get proportionally reduced opacity
+ float alphaScale = min(screenLineWidth, 1.0);
+
+ // SDF: distance to capsule surface
+ float sdf = dist2D - effectiveRadius;
+
+ // AA: 1 pixel transition for crisp edges
+ float aaWidth = pixelSize;
+ float alpha = 1.0 - smoothstep(-aaWidth, aaWidth, sdf);
+
+ // Apply alpha scaling for sub-pixel lines
+ alpha *= alphaScale;
+
+ if (alpha < 0.004) {
+ discard;
+ }
+
+ // ===== DASH PATTERN =====
+ float dashLength = v_Dash.x;
+ float gapLength = v_Dash.y;
+ float dashOffset = v_Dash.z;
+ float animationSpeed = v_Dash.w;
+
+ // Only apply dash if dashLength > 0 (0 = solid line)
+ if (dashLength > 0.0) {
+ float cycleLength = dashLength + gapLength;
+
+ // Calculate animated offset
+ float animatedOffset = dashOffset;
+ if (animationSpeed != 0.0) {
+ animatedOffset -= GameTime * animationSpeed * 1200.0;
+ }
+
+ // Use UNCLAMPED projLength so dashes continue through endcaps
+ float dashPos = projLength + animatedOffset * cycleLength;
+ float posInCycle = mod(dashPos, cycleLength);
+
+ // SDF for dash edges with anti-aliasing
+ float dashSdf;
+ if (posInCycle > dashLength) {
+ // In gap region - positive SDF
+ float distToGapEnd = cycleLength - posInCycle;
+ dashSdf = min(posInCycle - dashLength, distToGapEnd);
+ } else {
+ // In dash region - negative SDF (distance to nearest gap)
+ float distToDashEnd = dashLength - posInCycle;
+ float distFromDashStart = posInCycle;
+ dashSdf = -min(distToDashEnd, distFromDashStart);
+ }
+
+ // Apply anti-aliasing at dash edges (use fwidth of SDF for consistent AA with capsule)
+ float dashAaWidth = fwidth(dashSdf);
+ float dashAlpha = 1.0 - smoothstep(-dashAaWidth, dashAaWidth, dashSdf);
+
+ if (dashAlpha <= 0.0) {
+ discard;
+ }
+
+ alpha *= dashAlpha;
+ }
+
+ // Apply color
+ vec4 color = v_Color * ColorModulator;
+ color.a *= alpha;
+
+ fragColor = color;
+
+ // Map layer to depth: higher layer = smaller depth = renders on top (LEQUAL)
+ // Layer starts at -800 and increments, so later elements have higher values
+ gl_FragDepth = (1000.0 - v_Layer) / 2000.0;
+}
diff --git a/src/main/resources/assets/lambda/shaders/core/screen_lines.vsh b/src/main/resources/assets/lambda/shaders/core/screen_lines.vsh
new file mode 100644
index 000000000..e268a7f96
--- /dev/null
+++ b/src/main/resources/assets/lambda/shaders/core/screen_lines.vsh
@@ -0,0 +1,70 @@
+#version 330
+
+#moj_import
+#moj_import
+#moj_import
+
+// Vertex inputs - matches SCREEN_LINE_FORMAT
+in vec3 Position; // Screen-space position (x, y, 0)
+in vec4 Color;
+in vec2 Direction; // Line direction vector to OTHER endpoint (length = segment length)
+in float LineWidth; // Line width in pixels
+in vec4 Dash; // Dash parameters (dashLength, gapLength, offset, animSpeed)
+in float Layer; // Layer depth for draw order
+
+// Outputs to fragment shader
+out vec4 v_Color;
+out vec2 v_ExpandedPos; // Expanded screen position
+flat out vec2 v_LineStart; // Line start point
+flat out vec2 v_LineEnd; // Line end point
+flat out float v_LineWidth; // Line width
+flat out float v_SegmentLength; // Segment length
+flat out vec4 v_Dash; // Dash parameters (future: passed from vertex)
+out float v_Layer; // Layer depth for draw order
+
+void main() {
+ // Determine which corner of the quad this vertex is
+ int vertexIndex = gl_VertexID % 4;
+ bool isStart = (vertexIndex < 2);
+ float side = (vertexIndex == 0 || vertexIndex == 3) ? -1.0 : 1.0;
+
+ // Calculate segment properties
+ float segmentLength = length(Direction);
+ vec2 lineDir = Direction / max(segmentLength, 0.001);
+
+ // Line center (reconstruct for each vertex consistently)
+ vec2 lineCenter = isStart ? (Position.xy + Direction * 0.5) : (Position.xy - Direction * 0.5);
+
+ // Reconstruct endpoints from center
+ vec2 lineStart = lineCenter - lineDir * (segmentLength * 0.5);
+ vec2 lineEnd = lineCenter + lineDir * (segmentLength * 0.5);
+ vec2 thisPoint = isStart ? lineStart : lineEnd;
+
+ // Perpendicular direction for line thickness
+ vec2 perpDir = vec2(-lineDir.y, lineDir.x);
+
+ // Expand for AA (capsule shape) - ensure minimum expansion for thin lines
+ float halfWidth = LineWidth / 2.0;
+ float aaPadding = max(LineWidth * 0.5, 2.0); // At least 2 pixels for AA gradient
+ float halfWidthPadded = halfWidth + aaPadding;
+
+ // Expand vertex
+ vec2 perpOffset = perpDir * side * halfWidthPadded;
+ float longitudinal = isStart ? -1.0 : 1.0;
+ vec2 longOffset = lineDir * longitudinal * halfWidthPadded;
+
+ vec2 expandedPos = thisPoint + perpOffset + longOffset;
+
+ // Transform to clip space
+ gl_Position = ProjMat * ModelViewMat * vec4(expandedPos, 0.0, 1.0);
+
+ // Pass data to fragment shader
+ v_Color = Color;
+ v_ExpandedPos = expandedPos;
+ v_LineStart = lineStart;
+ v_LineEnd = lineEnd;
+ v_LineWidth = LineWidth;
+ v_SegmentLength = segmentLength;
+ v_Dash = Dash;
+ v_Layer = Layer;
+}
diff --git a/src/main/resources/assets/lambda/shaders/core/screen_sdf_text.fsh b/src/main/resources/assets/lambda/shaders/core/screen_sdf_text.fsh
new file mode 100644
index 000000000..15ab758ef
--- /dev/null
+++ b/src/main/resources/assets/lambda/shaders/core/screen_sdf_text.fsh
@@ -0,0 +1,73 @@
+#version 330
+
+#moj_import
+
+uniform sampler2D Sampler0;
+
+// Inputs from vertex shader
+in vec2 texCoord0;
+in vec4 vertexColor;
+// SDF style params from vertex shader: (outlineWidth, glowRadius, shadowSoftness, threshold)
+in vec4 sdfStyleParams;
+in float v_Layer; // Layer depth for draw order
+
+out vec4 fragColor;
+
+void main() {
+ // Extract SDF parameters from vertex attributes
+ float OutlineWidth = sdfStyleParams.x;
+ float GlowRadius = sdfStyleParams.y;
+ float ShadowSoftness = sdfStyleParams.z;
+ float SDFThreshold = sdfStyleParams.w;
+
+ // Sample the SDF texture - use ALPHA channel
+ vec4 texSample = texture(Sampler0, texCoord0);
+ float sdfValue = texSample.a;
+
+ // Screen-space anti-aliasing
+ float smoothing = fwidth(sdfValue) * 0.5;
+
+ // Decode layer type from vertex alpha
+ int layerType = int(vertexColor.a * 255.0 + 0.5);
+
+ float alpha;
+
+ if (layerType >= 200) {
+ // Main text layer - sharp edge at threshold
+ alpha = smoothstep(SDFThreshold - smoothing, SDFThreshold + smoothing, sdfValue);
+ } else if (layerType >= 100) {
+ // Outline layer - uses OutlineWidth
+ float outlineEdge = SDFThreshold - OutlineWidth;
+ alpha = smoothstep(outlineEdge - smoothing, outlineEdge + smoothing, sdfValue);
+ // Mask out the main text area
+ float textMask = smoothstep(SDFThreshold - smoothing, SDFThreshold + smoothing, sdfValue);
+ alpha = alpha * (1.0 - textMask);
+ } else if (layerType >= 50) {
+ // Glow layer - starts from outline edge (if outline enabled) or text edge
+ // Only expand past outline if OutlineWidth is actually set
+ float glowEdge = (OutlineWidth > 0.001) ? (SDFThreshold - OutlineWidth) : SDFThreshold;
+ float glowStart = glowEdge - GlowRadius;
+ float glowEnd = glowEdge;
+ alpha = smoothstep(glowStart, glowEnd, sdfValue) * 0.6;
+ // Mask out the text and outline area
+ float outlineMask = smoothstep(glowEdge - smoothing, glowEdge + smoothing, sdfValue);
+ alpha = alpha * (1.0 - outlineMask);
+ } else {
+ // Shadow layer - uses ShadowSoftness
+ float shadowStart = SDFThreshold - ShadowSoftness - 0.15;
+ float shadowEnd = SDFThreshold - 0.1;
+ alpha = smoothstep(shadowStart, shadowEnd, sdfValue) * 0.5;
+ }
+
+ // Apply vertex color (RGB from vertex, alpha computed above)
+ vec4 result = vec4(vertexColor.rgb, alpha);
+
+ // Discard nearly transparent fragments
+ if (result.a <= 0.001) discard;
+
+ // Apply color modulator (no fog for screen-space)
+ fragColor = result * ColorModulator;
+
+ // Map layer to depth: higher layer = smaller depth = renders on top (LEQUAL)
+ gl_FragDepth = (1000.0 - v_Layer) / 2000.0;
+}
diff --git a/src/main/resources/assets/lambda/shaders/core/screen_sdf_text.vsh b/src/main/resources/assets/lambda/shaders/core/screen_sdf_text.vsh
new file mode 100644
index 000000000..06c67b55b
--- /dev/null
+++ b/src/main/resources/assets/lambda/shaders/core/screen_sdf_text.vsh
@@ -0,0 +1,28 @@
+#version 330
+
+#moj_import
+#moj_import
+
+// Vertex inputs (SCREEN_TEXT_SDF_FORMAT)
+in vec3 Position;
+in vec2 UV0;
+in vec4 Color;
+// SDFStyle: vec4(outlineWidth, glowRadius, shadowSoftness, threshold)
+in vec4 SDFStyle;
+in float Layer; // Layer depth for draw order
+
+// Outputs to fragment shader
+out vec2 texCoord0;
+out vec4 vertexColor;
+out vec4 sdfStyleParams;
+out float v_Layer; // Layer depth for draw order
+
+void main() {
+ // Screen-space position - already in screen coordinates
+ gl_Position = ProjMat * ModelViewMat * vec4(Position, 1.0);
+
+ texCoord0 = UV0;
+ vertexColor = Color;
+ sdfStyleParams = SDFStyle;
+ v_Layer = Layer;
+}
diff --git a/src/main/resources/assets/lambda/shaders/core/world_image.fsh b/src/main/resources/assets/lambda/shaders/core/world_image.fsh
new file mode 100644
index 000000000..575885929
--- /dev/null
+++ b/src/main/resources/assets/lambda/shaders/core/world_image.fsh
@@ -0,0 +1,46 @@
+#version 330
+
+#moj_import
+#moj_import
+
+// Sampler for main texture
+uniform sampler2D Sampler0;
+// Sampler for overlay texture (e.g., enchantment glint)
+uniform sampler2D Sampler1;
+
+// Inputs from vertex shader
+in vec2 v_TexCoord;
+in vec4 v_Color;
+in vec4 v_OverlayUV; // (time, unused, hasOverlay, diffuseAmount)
+
+out vec4 fragColor;
+
+void main() {
+ // Sample main texture
+ vec4 texColor = texture(Sampler0, v_TexCoord);
+
+ // Apply tint color
+ vec4 color = texColor * v_Color * ColorModulator;
+
+ // Discard nearly transparent fragments
+ if (color.a < 0.004) {
+ discard;
+ }
+
+ // Apply overlay (enchantment glint) if present
+ // v_OverlayUV.y = aspect ratio (width/height) for square tiling
+ // v_OverlayUV.z = hasOverlay flag (1.0 = enabled)
+ if (v_OverlayUV.z > 0.5) {
+ // Use v_TexCoord (Atlas UVs) for parity with model glint logic
+ vec4 transformedUV = TextureMat * vec4(v_TexCoord, 0.0, 1.0);
+
+ // Sample glint texture using transformed coordinates
+ vec4 glint = texture(Sampler1, fract(transformedUV.xy));
+
+ // Apply with squared additive blending (matching 1.21 model parity)
+ vec3 layer = glint.rgb * glint.a * 0.75; // GLINT_ALPHA = 0.75
+ color.rgb += (layer * layer);
+ }
+
+ fragColor = color;
+}
diff --git a/src/main/resources/assets/lambda/shaders/core/world_image.vsh b/src/main/resources/assets/lambda/shaders/core/world_image.vsh
new file mode 100644
index 000000000..9ab6ad168
--- /dev/null
+++ b/src/main/resources/assets/lambda/shaders/core/world_image.vsh
@@ -0,0 +1,41 @@
+#version 330
+
+#moj_import
+#moj_import
+
+// Vertex inputs - matches WORLD_IMAGE_FORMAT
+in vec3 Position; // Local offset (x, y, z)
+in vec2 UV0; // Main texture UV coordinates
+in vec4 Color; // Tint color
+in vec3 Anchor; // Camera-relative world anchor position
+in vec2 BillboardData;// (scale, billboardFlag)
+in vec4 OverlayUV; // (overlayU, overlayV, hasOverlay, diffuseAmount)
+
+// Outputs to fragment shader
+out vec2 v_TexCoord;
+out vec4 v_Color;
+out vec4 v_OverlayUV;
+
+void main() {
+ float scale = BillboardData.x;
+ float billboardFlag = BillboardData.y;
+
+ vec4 mvPos;
+ if (billboardFlag < 0.5) {
+ // Billboard mode: face the camera perfectly
+ // 1. Transform anchor to camera space
+ mvPos = ModelViewMat * vec4(Anchor, 1.0);
+ // 2. Apply local offset in camera-aligned XY plane
+ mvPos.xy += Position.xy * scale;
+ } else {
+ // Fixed rotation mode: everything is pre-calculated/transformed
+ // worldPos = Anchor + Position * scale
+ mvPos = ModelViewMat * vec4(Anchor + Position * scale, 1.0);
+ }
+
+ gl_Position = ProjMat * mvPos;
+
+ v_TexCoord = UV0;
+ v_Color = Color;
+ v_OverlayUV = OverlayUV;
+}
diff --git a/src/main/resources/assets/lambda/shaders/core/world_lines.fsh b/src/main/resources/assets/lambda/shaders/core/world_lines.fsh
new file mode 100644
index 000000000..93751bc41
--- /dev/null
+++ b/src/main/resources/assets/lambda/shaders/core/world_lines.fsh
@@ -0,0 +1,131 @@
+#version 330
+
+#moj_import
+#moj_import
+#moj_import
+
+// Inputs from vertex shader
+in vec4 v_Color;
+in vec3 v_WorldPos; // Position before expansion (interpolated along line)
+in vec3 v_ExpandedPos; // Position after expansion (interpolated - fragment position)
+flat in vec3 v_Normal; // Raw Normal input (line direction * length)
+flat in vec3 v_LineCenter; // Line center (same for all vertices)
+flat in float v_LineWidth; // Line width
+flat in float v_SegmentLength; // Segment length
+flat in float v_IsStart; // 1.0 if from start vertex
+flat in vec4 v_Dash; // x = dashLength, y = gapLength, z = dashOffset, w = animationSpeed
+in float sphericalVertexDistance;
+in float cylindricalVertexDistance;
+
+out vec4 fragColor;
+
+void main() {
+ // Reconstruct line geometry from flat varyings
+ vec3 lineDir = normalize(v_Normal);
+ float halfLength = v_SegmentLength / 2.0;
+
+ // Compute line start and end from center
+ vec3 lineStart = v_LineCenter - lineDir * halfLength;
+ vec3 lineEnd = v_LineCenter + lineDir * halfLength;
+
+ // ===== CAPSULE SDF =====
+ vec3 toFragment = v_ExpandedPos - lineStart;
+ float projLength = dot(toFragment, lineDir);
+
+ // Perpendicular distance
+ vec3 perpVec = toFragment - lineDir * projLength;
+ float perpDist = length(perpVec);
+
+ // Calculate stable pixel size from screen-space position derivatives
+ // This is more reliable than fwidth(sdf) which can be unstable at edges
+ vec3 dPos_dx = dFdx(v_ExpandedPos);
+ vec3 dPos_dy = dFdy(v_ExpandedPos);
+ float pixelSize = (length(dPos_dx) + length(dPos_dy)) * 0.5;
+
+ // For end caps, compute actual distance to endpoints
+ float dist3D;
+ if (projLength < 0.0) {
+ dist3D = length(v_ExpandedPos - lineStart);
+ } else if (projLength > v_SegmentLength) {
+ dist3D = length(v_ExpandedPos - lineEnd);
+ } else {
+ dist3D = perpDist;
+ }
+
+ // Calculate screen line width in pixels
+ float screenLineWidth = v_LineWidth / max(pixelSize, 0.0001);
+
+ // Minimum 1-pixel rendering width - thinner lines scale alpha instead of getting gaps
+ float minWidth = pixelSize; // 1 pixel
+ float effectiveRadius = max(v_LineWidth * 0.5, minWidth * 0.5);
+
+ // Alpha scaling: lines < 1px get proportionally reduced opacity
+ float alphaScale = min(screenLineWidth, 1.0);
+
+ // SDF: distance to capsule surface
+ float sdf = dist3D - effectiveRadius;
+
+ // AA: 1 pixel transition for crisp edges
+ float aaWidth = pixelSize;
+ float alpha = 1.0 - smoothstep(-aaWidth, aaWidth, sdf);
+
+ // Apply alpha scaling for sub-pixel lines
+ alpha *= alphaScale;
+
+ if (alpha < 0.004) {
+ discard;
+ }
+
+ // ===== DASH PATTERN =====
+ float dashLength = v_Dash.x;
+ float gapLength = v_Dash.y;
+ float dashOffset = v_Dash.z;
+ float animationSpeed = v_Dash.w;
+
+ // Only apply dash if dashLength > 0 (0 = solid line)
+ if (dashLength > 0.0) {
+ float cycleLength = dashLength + gapLength;
+
+ // Calculate animated offset
+ float animatedOffset = dashOffset;
+ if (animationSpeed != 0.0) {
+ animatedOffset -= GameTime * animationSpeed * 1200.0;
+ }
+
+ // Use UNCLAMPED projLength so dashes continue through endcaps
+ float dashPos = projLength + animatedOffset * cycleLength;
+ float posInCycle = mod(dashPos, cycleLength);
+
+ // SDF for dash edges with anti-aliasing
+ float dashSdf;
+ if (posInCycle > dashLength) {
+ // In gap region - positive SDF
+ float distToGapEnd = cycleLength - posInCycle;
+ dashSdf = min(posInCycle - dashLength, distToGapEnd);
+ } else {
+ // In dash region - negative SDF (distance to nearest gap)
+ float distToDashEnd = dashLength - posInCycle;
+ float distFromDashStart = posInCycle;
+ dashSdf = -min(distToDashEnd, distFromDashStart);
+ }
+
+ // Apply anti-aliasing at dash edges (use fwidth of SDF for consistent AA with capsule)
+ float dashAaWidth = fwidth(dashSdf);
+ float dashAlpha = 1.0 - smoothstep(-dashAaWidth, dashAaWidth, dashSdf);
+
+ if (dashAlpha <= 0.0) {
+ discard;
+ }
+
+ alpha *= dashAlpha;
+ }
+
+ // Apply color
+ vec4 color = v_Color * ColorModulator;
+ color.a *= alpha;
+
+ // Apply fog
+ fragColor = apply_fog(color, sphericalVertexDistance, cylindricalVertexDistance,
+ FogEnvironmentalStart, FogEnvironmentalEnd,
+ FogRenderDistanceStart, FogRenderDistanceEnd, FogColor);
+}
diff --git a/src/main/resources/assets/lambda/shaders/core/world_lines.vsh b/src/main/resources/assets/lambda/shaders/core/world_lines.vsh
new file mode 100644
index 000000000..d142b2c59
--- /dev/null
+++ b/src/main/resources/assets/lambda/shaders/core/world_lines.vsh
@@ -0,0 +1,101 @@
+#version 330
+
+#moj_import
+#moj_import
+#moj_import
+#moj_import
+
+// Vertex inputs
+in vec3 Position;
+in vec4 Color;
+in vec3 Normal; // Direction vector to other endpoint (length = segment length)
+in float LineWidth; // Line width: positive = world units, negative = screen-space fraction
+in vec4 Dash; // Dash parameters
+
+// Outputs to fragment shader
+out vec4 v_Color;
+out vec3 v_WorldPos;
+out vec3 v_ExpandedPos;
+flat out vec3 v_Normal;
+flat out vec3 v_LineCenter;
+flat out float v_LineWidth; // Always positive (actual width in world units)
+flat out float v_SegmentLength;
+flat out float v_IsStart;
+flat out vec4 v_Dash;
+out float sphericalVertexDistance;
+out float cylindricalVertexDistance;
+
+void main() {
+ int vertexIndex = gl_VertexID % 4;
+ bool isStart = (vertexIndex < 2);
+ float side = (vertexIndex == 0 || vertexIndex == 3) ? -1.0 : 1.0;
+
+ float segmentLength = length(Normal);
+ vec3 lineDir = Normal / segmentLength;
+
+ vec3 lineCenter = isStart ? (Position + Normal * 0.5) : (Position - Normal * 0.5);
+
+ vec3 lineStart = lineCenter - lineDir * (segmentLength * 0.5);
+ vec3 lineEnd = lineCenter + lineDir * (segmentLength * 0.5);
+ vec3 thisPoint = isStart ? lineStart : lineEnd;
+
+ // Extract camera position from ModelViewMat
+ mat3 rotationInv = transpose(mat3(ModelViewMat));
+ vec3 translation = vec3(ModelViewMat[3]);
+ vec3 cameraPos = rotationInv * (-translation);
+
+ vec3 toCamera = normalize(cameraPos - lineCenter);
+
+ vec3 perpDir = cross(lineDir, toCamera);
+ if (length(perpDir) < 0.001) {
+ perpDir = cross(lineDir, vec3(0.0, 1.0, 0.0));
+ if (length(perpDir) < 0.001) {
+ perpDir = cross(lineDir, vec3(1.0, 0.0, 0.0));
+ }
+ }
+ perpDir = normalize(perpDir);
+
+ // Calculate actual line width in world units
+ float actualLineWidth;
+ if (LineWidth < 0.0) {
+ // Distance-scaled mode: negative value = screen-space fraction
+ // Convert screen fraction to world units at this distance
+ float screenFraction = -LineWidth;
+ float distToCamera = length(cameraPos - lineCenter);
+
+ // Extract tan(fov/2) from projection matrix: ProjMat[1][1] = 1/tan(fov/2)
+ float tanHalfFov = 1.0 / ProjMat[1][1];
+
+ // At distance d, visible height = 2 * d * tan(fov/2)
+ // world width = screenFraction * visible height
+ actualLineWidth = screenFraction * 2.0 * distToCamera * tanHalfFov;
+ } else {
+ actualLineWidth = LineWidth;
+ }
+
+ // Expand for AA
+ float halfWidth = actualLineWidth / 2.0;
+ float aaPadding = actualLineWidth * 0.3;
+ float halfWidthPadded = halfWidth + aaPadding;
+
+ vec3 perpOffset = perpDir * side * halfWidthPadded;
+ float longitudinal = isStart ? -1.0 : 1.0;
+ vec3 longOffset = lineDir * longitudinal * halfWidthPadded;
+
+ vec3 expandedPos = thisPoint + perpOffset + longOffset;
+
+ gl_Position = ProjMat * ModelViewMat * vec4(expandedPos, 1.0);
+
+ v_Color = Color;
+ v_WorldPos = thisPoint;
+ v_ExpandedPos = expandedPos;
+ v_Normal = Normal;
+ v_LineCenter = lineCenter;
+ v_LineWidth = actualLineWidth; // Pass the computed world-unit width
+ v_SegmentLength = segmentLength;
+ v_IsStart = isStart ? 1.0 : 0.0;
+ v_Dash = Dash;
+
+ sphericalVertexDistance = fog_spherical_distance(Position);
+ cylindricalVertexDistance = fog_cylindrical_distance(Position);
+}
diff --git a/src/main/resources/assets/lambda/shaders/core/world_model.fsh b/src/main/resources/assets/lambda/shaders/core/world_model.fsh
new file mode 100644
index 000000000..5e192d610
--- /dev/null
+++ b/src/main/resources/assets/lambda/shaders/core/world_model.fsh
@@ -0,0 +1,126 @@
+#version 330
+
+// Sampler0: Main Atlas
+// Sampler1: Overlay Texture (White/Hurt flash)
+// Sampler2: Lightmap
+// Sampler3: Glint Texture (enchantment shimmer)
+#moj_import
+
+uniform sampler2D Sampler0;
+uniform sampler2D Sampler1;
+uniform sampler2D Sampler2;
+uniform sampler2D Sampler3;
+
+layout(std140) uniform GlintTransforms {
+ mat4 GlintMat2; // Mat2 packed into the ModelView slot
+ vec4 UnusedColor;
+ vec3 UnusedOffset;
+ mat4 GlintMat; // Mat1 original
+};
+
+in vec2 v_TexCoord;
+in vec4 v_Color;
+in vec4 v_OverlayUV; // (u, v, mode, diffuseAmount)
+in vec2 v_LightCoord;
+in vec2 v_EdgeData;
+in vec3 v_Normal;
+in vec3 v_LightDir; // Primary light (pre-transformed)
+in vec3 v_Light1Dir; // Fill light (pre-transformed)
+
+out vec4 fragColor;
+
+// DEBUG MODE:
+// 0 = Normal rendering
+// 1 = Show normals as RGB (red=+X, green=+Y, blue=+Z)
+// 2 = Show light0 direction as RGB
+// 3 = Show dot product with light0 as grayscale
+// 4 = Show which faces are lit (d0 > 0.5 = red, d1 > 0.5 = green)
+// 5 = Show glint scroll values as colors (R=scrollX, G=scrollY, B=mode/10)
+// 6 = Show raw glint texture with base UVs (no animation)
+// 7 = Show computed glint UVs as color (R=U fract, G=V fract, B=0.5)
+#define DEBUG_MODE 0
+
+// Glint constants matching vanilla (from TextureTransform.getGlintTransformation)
+const float GLINT_ALPHA = 0.75; // Default from options.getGlintStrength()
+
+void main() {
+ float mode = floor(v_OverlayUV.z + 0.5);
+ // Mode bits: 1=overlay, 2=AA, 4=glint
+ bool useOverlay = (mod(mode, 2.0) >= 1.0);
+ bool useAA = (mod(floor(mode / 2.0), 2.0) >= 1.0);
+ bool useGlint = (mode >= 4.0);
+
+ // 1. Determine Texture Coordinates (with optional Sharp AA)
+ vec2 texCoord = v_TexCoord;
+ if (useAA) {
+ vec2 texSize = vec2(textureSize(Sampler0, 0));
+ vec2 pixels = texCoord * texSize;
+ vec2 t = fract(pixels - 0.5);
+ vec2 d = fwidth(pixels);
+ vec2 sharp_t = clamp((t - 0.5) / d + 0.5, 0.0, 1.0);
+ texCoord = (floor(pixels - 0.5) + sharp_t + 0.5) / texSize;
+ }
+
+ // 2. Sample Base Texture from atlas
+ vec4 texColor = texture(Sampler0, texCoord);
+ if (texColor.a < 0.1) discard;
+
+ // 3. Apply Multiplier (v_Color) and Global Modulator
+ vec4 baseColor = texColor * v_Color * ColorModulator;
+
+ // 4. Calculate lighting
+ vec3 litColor = baseColor.rgb;
+
+ // Handle Shading:
+ // v_OverlayUV.w is 'shadingAmount': 1.0 = shaded, 0.0 = unshaded.
+ // NOTE: For non-3D items, shadingAmount is 0.0. We want factor 1.0 (unshaded), not 0.0 (black).
+ if (v_OverlayUV.w >= 0.0) {
+ vec3 n = normalize(v_Normal);
+ vec3 light0 = normalize(v_LightDir);
+ vec3 light1 = normalize(v_Light1Dir);
+
+ float d0 = max(0.0, dot(light0, n));
+ float d1 = max(0.0, dot(light1, n));
+
+ // Vanilla's diffuse lighting formula: diffuse = max(0, n.l) * 0.6 + 0.4
+ float diffuse = min(1.0, (d0 + d1) * 0.6 + 0.4);
+
+ // Final light factor: mix between 1.0 (unshaded) and 'diffuse' (shaded) based on shadingAmount.
+ float lightFactor = mix(1.0, diffuse, v_OverlayUV.w);
+ litColor *= lightFactor;
+ } else {
+ // For world objects without directional shading, use lightmap
+ litColor *= texture(Sampler2, v_LightCoord).rgb;
+ }
+
+ // 5. Apply Hurt Flash / Overlay
+ if (useOverlay) {
+ vec4 overlayColor = texture(Sampler1, v_OverlayUV.xy);
+ litColor = mix(litColor, overlayColor.rgb, overlayColor.a);
+ }
+
+ // 6. Apply Enchantment Glint (Absolute 1.21 Parity)
+ if (useGlint) {
+ // Use v_TexCoord (Atlas UVs) for parity with model glint logic
+ vec2 transformedUV = (GlintMat * vec4(v_TexCoord, 0.0, 1.0)).xy;
+ vec3 glint = texture(Sampler3, fract(transformedUV)).rgb;
+
+ // Intensity = (sample * alpha)^2
+ vec3 layer = glint * GLINT_ALPHA;
+ litColor += (layer * layer);
+
+ // Debug visualizations
+ #if DEBUG_MODE == 5
+ fragColor = vec4(v_EdgeData.x, v_EdgeData.y, 0.0, 1.0); return;
+ #elif DEBUG_MODE == 6
+ fragColor = texture(Sampler3, v_EdgeData); return;
+ #elif DEBUG_MODE == 7
+ fragColor = vec4(fract(guv.x), fract(guv.y), 0.5, 1.0); return;
+ #elif DEBUG_MODE == 8
+ // Show diffuse shading amount
+ fragColor = vec4(vec3(v_OverlayUV.w), 1.0); return;
+ #endif
+ }
+
+ fragColor = vec4(litColor, baseColor.a);
+}
diff --git a/src/main/resources/assets/lambda/shaders/core/world_model.vsh b/src/main/resources/assets/lambda/shaders/core/world_model.vsh
new file mode 100644
index 000000000..b969f8902
--- /dev/null
+++ b/src/main/resources/assets/lambda/shaders/core/world_model.vsh
@@ -0,0 +1,40 @@
+#version 330
+
+#moj_import
+#moj_import
+#moj_import
+
+// Attributes matching WORLD_MODEL_FORMAT
+in vec3 Position;
+in vec4 Color;
+in vec2 UV0;
+in vec4 OverlayUV; // (u, v, mode, diffuseAmount)
+in ivec2 UV2; // Lightmap coords
+in vec3 LightDir; // Primary light direction (pre-transformed for ITEMS_FLAT)
+in vec3 Light1Dir; // Fill light direction (pre-transformed)
+in vec3 Normal;
+in vec2 EdgeData;
+
+out vec2 v_TexCoord;
+out vec4 v_Color;
+out vec4 v_OverlayUV;
+out vec2 v_LightCoord;
+out vec2 v_EdgeData;
+out vec3 v_Normal;
+out vec3 v_LightDir; // Primary light
+out vec3 v_Light1Dir; // Fill light
+
+void main() {
+ gl_Position = ProjMat * ModelViewMat * vec4(Position, 1.0);
+
+ v_Color = Color;
+ v_TexCoord = UV0;
+ v_OverlayUV = OverlayUV;
+ v_LightCoord = (vec2(UV2) + 0.5) / 256.0; // Normalize light coords
+ v_EdgeData = EdgeData;
+
+ // Both normals and light directions are already transformed on CPU
+ v_Normal = Normal;
+ v_LightDir = LightDir;
+ v_Light1Dir = Light1Dir;
+}
diff --git a/src/main/resources/assets/lambda/shaders/core/world_sdf_text.fsh b/src/main/resources/assets/lambda/shaders/core/world_sdf_text.fsh
new file mode 100644
index 000000000..756998738
--- /dev/null
+++ b/src/main/resources/assets/lambda/shaders/core/world_sdf_text.fsh
@@ -0,0 +1,73 @@
+#version 330
+#moj_import
+#moj_import
+
+uniform sampler2D Sampler0;
+
+in vec2 texCoord0;
+in vec4 vertexColor;
+in float sphericalVertexDistance;
+in float cylindricalVertexDistance;
+// SDF style params from vertex shader: (outlineWidth, glowRadius, shadowSoftness, threshold)
+in vec4 sdfStyleParams;
+
+out vec4 fragColor;
+
+void main() {
+ // Extract SDF parameters from vertex attributes
+ float OutlineWidth = sdfStyleParams.x;
+ float GlowRadius = sdfStyleParams.y;
+ float ShadowSoftness = sdfStyleParams.z;
+ float SDFThreshold = sdfStyleParams.w;
+
+ // Sample the SDF texture - use ALPHA channel
+ vec4 texSample = texture(Sampler0, texCoord0);
+ float sdfValue = texSample.a;
+
+ // Screen-space anti-aliasing
+ float smoothing = fwidth(sdfValue) * 0.5;
+
+ // Decode layer type from vertex alpha
+ int layerType = int(vertexColor.a * 255.0 + 0.5);
+
+ float alpha;
+
+ if (layerType >= 200) {
+ // Main text layer - sharp edge at threshold
+ alpha = smoothstep(SDFThreshold - smoothing, SDFThreshold + smoothing, sdfValue);
+ } else if (layerType >= 100) {
+ // Outline layer - uses OutlineWidth
+ float outlineEdge = SDFThreshold - OutlineWidth;
+ alpha = smoothstep(outlineEdge - smoothing, outlineEdge + smoothing, sdfValue);
+ // Mask out the main text area
+ float textMask = smoothstep(SDFThreshold - smoothing, SDFThreshold + smoothing, sdfValue);
+ alpha = alpha * (1.0 - textMask);
+ } else if (layerType >= 50) {
+ // Glow layer - starts from outline edge (if outline enabled) or text edge
+ // Only expand past outline if OutlineWidth is actually set
+ float glowEdge = (OutlineWidth > 0.001) ? (SDFThreshold - OutlineWidth) : SDFThreshold;
+ float glowStart = glowEdge - GlowRadius;
+ float glowEnd = glowEdge;
+ alpha = smoothstep(glowStart, glowEnd, sdfValue) * 0.6;
+ // Mask out the text and outline area
+ float outlineMask = smoothstep(glowEdge - smoothing, glowEdge + smoothing, sdfValue);
+ alpha = alpha * (1.0 - outlineMask);
+ } else {
+ // Shadow layer - uses ShadowSoftness
+ float shadowStart = SDFThreshold - ShadowSoftness - 0.15;
+ float shadowEnd = SDFThreshold - 0.1;
+ alpha = smoothstep(shadowStart, shadowEnd, sdfValue) * 0.5;
+ }
+
+ // Apply vertex color (RGB from vertex, alpha computed above)
+ vec4 result = vec4(vertexColor.rgb, alpha);
+
+ // Discard nearly transparent fragments
+ if (result.a <= 0.001) discard;
+
+ // Apply color modulator and fog
+ result *= ColorModulator;
+ fragColor = apply_fog(result, sphericalVertexDistance, cylindricalVertexDistance,
+ FogEnvironmentalStart, FogEnvironmentalEnd,
+ FogRenderDistanceStart, FogRenderDistanceEnd, FogColor);
+}
\ No newline at end of file
diff --git a/src/main/resources/assets/lambda/shaders/core/world_sdf_text.vsh b/src/main/resources/assets/lambda/shaders/core/world_sdf_text.vsh
new file mode 100644
index 000000000..8aa50c8b0
--- /dev/null
+++ b/src/main/resources/assets/lambda/shaders/core/world_sdf_text.vsh
@@ -0,0 +1,84 @@
+#version 330
+
+#moj_import
+#moj_import
+#moj_import
+
+// Position contains local glyph offset (x, y) with z unused
+in vec3 Position;
+in vec2 UV0;
+in vec4 Color;
+// Anchor is the camera-relative world position of the text
+in vec3 Anchor;
+// BillboardData.x = scale, BillboardData.y = billboardFlag (0 = auto-billboard)
+in vec2 BillboardData;
+// SDFStyle: vec4(outlineWidth, glowRadius, shadowSoftness, threshold)
+in vec4 SDFStyle;
+
+out vec2 texCoord0;
+out vec4 vertexColor;
+out float sphericalVertexDistance;
+out float cylindricalVertexDistance;
+// Pass SDF style to fragment shader
+out vec4 sdfStyleParams;
+
+void main() {
+ float scale = BillboardData.x;
+ float billboardFlag = BillboardData.y;
+
+ vec3 worldPos;
+
+ if (billboardFlag == 0.0) {
+ // Billboard mode: compute right/up vectors from ModelViewMat
+ // ModelViewMat transforms from world to view space
+ // To billboard, we need right and up vectors in world space
+ // For a view matrix, the inverse transpose gives us camera orientation
+ // The first column of ModelViewMat is the right vector (in view space)
+ // The second column is the up vector
+ // Since ModelViewMat = View, and we want to face the camera:
+ // right = normalize(ModelViewMat[0].xyz)
+ // up = normalize(ModelViewMat[1].xyz)
+
+ vec3 right = vec3(ModelViewMat[0][0], ModelViewMat[1][0], ModelViewMat[2][0]);
+ vec3 up = vec3(ModelViewMat[0][1], ModelViewMat[1][1], ModelViewMat[2][1]);
+
+ // Apply scale (negative Y to flip for correct text orientation)
+ float scaledX = Position.x * scale;
+ float scaledY = Position.y * -scale; // Negate Y to flip
+
+ // Compute world position from anchor + billboard offset
+ worldPos = Anchor + right * scaledX + up * scaledY;
+ } else {
+ // Fixed rotation mode: position is already transformed, just add anchor
+ // In this case, Position.xy contains the pre-transformed local offset (scaled)
+ // We still need to apply the anchor offset
+ worldPos = Anchor + Position * scale;
+ }
+
+ gl_Position = ProjMat * ModelViewMat * vec4(worldPos, 1.0);
+
+ // Apply per-layer depth bias to prevent z-fighting between text effect layers
+ // Layer type is encoded in Color.a: 200+ = text, 100+ = outline, 50+ = glow, <50 = shadow
+ // Each layer needs a unique depth offset so they don't fight
+ // Order from back to front: shadow < glow < outline < text
+ int layerType = int(Color.a * 255.0 + 0.5);
+ float layerOffset;
+ if (layerType >= 200) {
+ layerOffset = 0.0004; // Text - closest to camera
+ } else if (layerType >= 100) {
+ layerOffset = 0.0003; // Outline
+ } else if (layerType >= 50) {
+ layerOffset = 0.0002; // Glow
+ } else {
+ layerOffset = 0.0001; // Shadow - furthest back
+ }
+
+ gl_Position.z -= layerOffset * gl_Position.w;
+
+ texCoord0 = UV0;
+ vertexColor = Color;
+ sdfStyleParams = SDFStyle;
+
+ sphericalVertexDistance = fog_spherical_distance(worldPos);
+ cylindricalVertexDistance = fog_cylindrical_distance(worldPos);
+}
\ No newline at end of file
diff --git a/src/main/resources/assets/lambda/shaders/fragment/font.glsl b/src/main/resources/assets/lambda/shaders/fragment/font.glsl
deleted file mode 100644
index afb98a7ff..000000000
--- a/src/main/resources/assets/lambda/shaders/fragment/font.glsl
+++ /dev/null
@@ -1,24 +0,0 @@
-#version 420
-
-in vec2 v_TexCoord;
-in vec4 v_Color;
-
-out vec4 color;
-
-float sdf(float channel, float min, float max) {
- return 1.0 - smoothstep(min, max, 1.0 - channel);
-}
-
-void main()
-{
- bool isEmoji = v_TexCoord.x < 0.0;
-
- if (isEmoji) {
- vec4 c = texture(u_EmojiTexture, -v_TexCoord);
- color = vec4(c.rgb, sdf(c.a, u_SDFMin, u_SDFMax)) * v_Color;
- return;
- }
-
- float sdf = sdf(texture(u_FontTexture, v_TexCoord).r, u_SDFMin, u_SDFMax);
- color = vec4(1.0, 1.0, 1.0, sdf) * v_Color;
-}
\ No newline at end of file
diff --git a/src/main/resources/assets/lambda/shaders/fragment/particles.glsl b/src/main/resources/assets/lambda/shaders/fragment/particles.glsl
deleted file mode 100644
index 967ef6a91..000000000
--- a/src/main/resources/assets/lambda/shaders/fragment/particles.glsl
+++ /dev/null
@@ -1,12 +0,0 @@
-#version 330 core
-
-in vec2 v_TexCoord;
-in vec4 v_Color;
-
-out vec4 color;
-
-void main()
-{
- float a = 1.0 - length(v_TexCoord - 0.5) * 2.0;
- color = v_Color * vec4(1.0, 1.0, 1.0, a);
-}
\ No newline at end of file
diff --git a/src/main/resources/assets/lambda/shaders/fragment/pos_color.glsl b/src/main/resources/assets/lambda/shaders/fragment/pos_color.glsl
deleted file mode 100644
index 2eb6b4242..000000000
--- a/src/main/resources/assets/lambda/shaders/fragment/pos_color.glsl
+++ /dev/null
@@ -1,9 +0,0 @@
-#version 420
-
-in vec4 v_Color;
-out vec4 color;
-
-void main()
-{
- color = v_Color;
-}
\ No newline at end of file
diff --git a/src/main/resources/assets/lambda/shaders/fragment/pos_tex.glsl b/src/main/resources/assets/lambda/shaders/fragment/pos_tex.glsl
deleted file mode 100644
index efac6ba5c..000000000
--- a/src/main/resources/assets/lambda/shaders/fragment/pos_tex.glsl
+++ /dev/null
@@ -1,11 +0,0 @@
-#version 330 core
-
-in vec2 v_TexCoord;
-out vec4 color;
-
-uniform sampler2D u_Texture;
-
-void main()
-{
- color = texture(u_Texture, v_TexCoord);
-}
\ No newline at end of file
diff --git a/src/main/resources/assets/lambda/shaders/fragment/pos_tex_color.glsl b/src/main/resources/assets/lambda/shaders/fragment/pos_tex_color.glsl
deleted file mode 100644
index 09f3f6b96..000000000
--- a/src/main/resources/assets/lambda/shaders/fragment/pos_tex_color.glsl
+++ /dev/null
@@ -1,13 +0,0 @@
-#version 330 core
-
-in vec2 v_TexCoord;
-in vec4 v_Color;
-
-out vec4 color;
-
-uniform sampler2D u_Texture;
-
-void main()
-{
- color = texture(u_Texture, v_TexCoord) * v_Color
-}
\ No newline at end of file
diff --git a/src/main/resources/assets/lambda/shaders/post/sdf.glsl b/src/main/resources/assets/lambda/shaders/post/sdf.glsl
deleted file mode 100644
index fa3566da6..000000000
--- a/src/main/resources/assets/lambda/shaders/post/sdf.glsl
+++ /dev/null
@@ -1,34 +0,0 @@
-attributes {
- vec4 pos;
- vec2 uv;
-};
-
-uniforms {
- sampler2D u_Texture; # fragment
- vec2 u_TexelSize; # fragment
-};
-
-export {
- vec2 v_TexCoord; # uv
-};
-
-#define SPREAD 4
-
-void fragment() {
- vec4 colors = vec4(0.0);
- vec4 blurWeight = vec4(0.0);
-
- for (int x = -SPREAD; x <= SPREAD; ++x) {
- for (int y = -SPREAD; y <= SPREAD; ++y) {
- vec2 offset = vec2(x, y) * u_TexelSize;
-
- vec4 color = texture(u_Texture, v_TexCoord + offset);
- vec4 weight = exp(-color * color);
-
- colors += color * weight;
- blurWeight += weight;
- }
- }
-
- color = colors / blurWeight;
-}#
\ No newline at end of file
diff --git a/src/main/resources/assets/lambda/shaders/shared/hsb.glsl b/src/main/resources/assets/lambda/shaders/shared/hsb.glsl
deleted file mode 100644
index 25b9eb99d..000000000
--- a/src/main/resources/assets/lambda/shaders/shared/hsb.glsl
+++ /dev/null
@@ -1,30 +0,0 @@
-vec3 hsb2rgb(vec3 hsb) {
- float C = hsb.z * hsb.y;
- float X = C * (1.0 - abs(mod(hsb.x / 60.0, 2.0) - 1.0));
- float m = hsb.z - C;
-
- vec3 rgb;
-
- if (0.0 <= hsb.x && hsb.x < 60.0) {
- rgb = vec3(C, X, 0.0);
- } else if (60.0 <= hsb.x && hsb.x < 120.0) {
- rgb = vec3(X, C, 0.0);
- } else if (120.0 <= hsb.x && hsb.x < 180.0) {
- rgb = vec3(0.0, C, X);
- } else if (180.0 <= hsb.x && hsb.x < 240.0) {
- rgb = vec3(0.0, X, C);
- } else if (240.0 <= hsb.x && hsb.x < 300.0) {
- rgb = vec3(X, 0.0, C);
- } else {
- rgb = vec3(C, 0.0, X);
- }
-
- return (rgb + vec3(m));
-}#
-
-float hue(vec2 uv) {
- vec2 centered = uv * 2.0 - 1.0;
- float hue = degrees(atan(centered.y, centered.x)) + 180.0;
-
- return hue;
-}#
\ No newline at end of file
diff --git a/src/main/resources/assets/lambda/shaders/shared/rect.glsl b/src/main/resources/assets/lambda/shaders/shared/rect.glsl
deleted file mode 100644
index 4fe1b2220..000000000
--- a/src/main/resources/assets/lambda/shaders/shared/rect.glsl
+++ /dev/null
@@ -1,45 +0,0 @@
-attributes {
- vec4 pos;
- vec2 uv;
- vec4 color;
-};
-
-uniforms {
- vec2 u_Size; # fragment
-
- float u_RoundLeftTop; # fragment
- float u_RoundLeftBottom; # fragment
- float u_RoundRightBottom; # fragment
- float u_RoundRightTop; # fragment
-};
-
-export {
- vec2 v_TexCoord; # uv
- vec4 v_Color; # color
-};
-
-#include "shade"
-
-#define NOISE_GRANULARITY 0.004
-#define SMOOTHING 0.3
-
-#define noise getNoise()
-
-vec4 getNoise() {
- // https://shader-tutorial.dev/advanced/color-banding-dithering/
- float random = fract(sin(dot(v_TexCoord, vec2(12.9898, 78.233))) * 43758.5453);
- float ofs = mix(-NOISE_GRANULARITY, NOISE_GRANULARITY, random);
- return vec4(ofs, ofs, ofs, 0.0);
-}#
-
-float signedDistance(in vec4 r) {
- r.xy = (v_TexCoord.x > 0.5) ? r.xy : r.zw;
- r.x = (v_TexCoord.y > 0.5) ? r.x : r.y;
-
- vec2 q = u_Size * (abs(v_TexCoord - 0.5) - 0.5) + r.x;
- return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r.x;
-}#
-
-float signedDistance() {
- return signedDistance(vec4(u_RoundRightBottom, u_RoundRightTop, u_RoundLeftBottom, u_RoundLeftTop));
-}#
diff --git a/src/main/resources/assets/lambda/shaders/shared/sdf.glsl b/src/main/resources/assets/lambda/shaders/shared/sdf.glsl
deleted file mode 100644
index 165043f1c..000000000
--- a/src/main/resources/assets/lambda/shaders/shared/sdf.glsl
+++ /dev/null
@@ -1,3 +0,0 @@
-float sdf(float channel, float min, float max) {
- return 1.0 - smoothstep(min, max, 1.0 - channel);
-}#
\ No newline at end of file
diff --git a/src/main/resources/assets/lambda/shaders/shared/shade.glsl b/src/main/resources/assets/lambda/shaders/shared/shade.glsl
deleted file mode 100644
index 9371edc22..000000000
--- a/src/main/resources/assets/lambda/shaders/shared/shade.glsl
+++ /dev/null
@@ -1,22 +0,0 @@
-uniforms {
- float u_Shade; # fragment
- float u_ShadeTime; # fragment
- vec4 u_ShadeColor1; # fragment
- vec4 u_ShadeColor2; # fragment
- vec2 u_ShadeSize; # fragment
-};
-
-export {
- vec2 v_Position; # gl_Position.xy * 0.5 + 0.5
-};
-
-#define shade getShadeColor()
-
-vec4 getShadeColor() {
- if (u_Shade != 1.0) return vec4(1.0);
-
- vec2 pos = v_Position * u_ShadeSize;
- float p = sin(pos.x - pos.y - u_ShadeTime) * 0.5 + 0.5;
-
- return mix(u_ShadeColor1, u_ShadeColor2, p);
-}#
\ No newline at end of file
diff --git a/src/main/resources/assets/lambda/shaders/vertex/box_dynamic.glsl b/src/main/resources/assets/lambda/shaders/vertex/box_dynamic.glsl
deleted file mode 100644
index aad9c7385..000000000
--- a/src/main/resources/assets/lambda/shaders/vertex/box_dynamic.glsl
+++ /dev/null
@@ -1,17 +0,0 @@
-#version 330 core
-
-layout (location = 0) in vec3 pos1;
-layout (location = 1) in vec3 pos2;
-layout (location = 2) in vec4 color;
-
-out vec4 v_Color;
-
-uniform mat4 u_ProjModel;
-uniform mat4 u_View;
-uniform float u_TickDelta;
-
-void main()
-{
- gl_Position = u_ProjModel * u_View * vec4(mix(pos1, pos2, u_TickDelta), 1.0);
- v_Color = color;
-}
\ No newline at end of file
diff --git a/src/main/resources/assets/lambda/shaders/vertex/box_static.glsl b/src/main/resources/assets/lambda/shaders/vertex/box_static.glsl
deleted file mode 100644
index ba73727da..000000000
--- a/src/main/resources/assets/lambda/shaders/vertex/box_static.glsl
+++ /dev/null
@@ -1,15 +0,0 @@
-#version 330 core
-
-layout (location = 0) in vec3 pos;
-layout (location = 1) in vec4 color;
-
-out vec4 v_Color;
-
-uniform mat4 u_ProjModel;
-uniform mat4 u_View;
-
-void main()
-{
- gl_Position = u_ProjModel * u_View * vec4(pos, 1.0);
- v_Color = color;
-}
\ No newline at end of file
diff --git a/src/main/resources/assets/lambda/shaders/vertex/font.glsl b/src/main/resources/assets/lambda/shaders/vertex/font.glsl
deleted file mode 100644
index e8a0f3b92..000000000
--- a/src/main/resources/assets/lambda/shaders/vertex/font.glsl
+++ /dev/null
@@ -1,19 +0,0 @@
-#version 330 core
-
-layout (location = 0) in vec4 pos;
-layout (location = 1) in vec2 uv;
-layout (location = 2) in vec4 color; // Does this fuck the padding ?
-
-out vec2 v_TexCoord;
-out vec4 v_Color;
-
-uniform sampler2D u_FontTexture;
-uniform sampler2D u_EmojiTexture;
-uniform float u_SDFMin;
-uniform float u_SDFMin;
-
-void main()
-{
- v_TexCoord = uv;
- v_Color = color;
-}
\ No newline at end of file
diff --git a/src/main/resources/assets/lambda/shaders/vertex/particles.glsl b/src/main/resources/assets/lambda/shaders/vertex/particles.glsl
deleted file mode 100644
index 08e0e0a43..000000000
--- a/src/main/resources/assets/lambda/shaders/vertex/particles.glsl
+++ /dev/null
@@ -1,19 +0,0 @@
-#version 330 core
-
-layout (location = 0) in vec3 pos;
-layout (location = 1) in vec2 uv;
-layout (location = 2) in vec4 color;
-
-out vec2 v_TexCoord;
-out vec4 v_Color;
-
-uniform mat4 u_ProjModel;
-uniform mat4 u_View;
-
-void main()
-{
- gl_Position = u_ProjModel * u_View * vec4(pos, 1.0);
-
- v_TexCoord = uv;
- v_Color = color;
-}
\ No newline at end of file
diff --git a/src/main/resources/assets/lambda/shaders/vertex/tracer_dynamic.glsl b/src/main/resources/assets/lambda/shaders/vertex/tracer_dynamic.glsl
deleted file mode 100644
index 2d7f171e5..000000000
--- a/src/main/resources/assets/lambda/shaders/vertex/tracer_dynamic.glsl
+++ /dev/null
@@ -1,20 +0,0 @@
-#version 330 core
-
-layout (location = 0) in vec3 pos1;
-layout (location = 1) in vec2 pos1;
-layout (location = 2) in vec4 color;
-
-out vec4 v_Color;
-
-uniform mat4 u_ProjModel;
-uniform mat4 u_View;
-uniform float u_TickDelta;
-
-void main()
-{
- if (l_VertexID % 2 != 0)
- return;
-
- vec3 VERTEX_POSITION = mix(pos1, pos2, u_TickDelta);
- gl_Position = u_ProjModel * u_View * vec4(VERTEX_POSITION, 1.0);
-}
\ No newline at end of file
diff --git a/src/main/resources/assets/lambda/shaders/vertex/tracer_static.glsl b/src/main/resources/assets/lambda/shaders/vertex/tracer_static.glsl
deleted file mode 100644
index 1d111b6ca..000000000
--- a/src/main/resources/assets/lambda/shaders/vertex/tracer_static.glsl
+++ /dev/null
@@ -1,17 +0,0 @@
-#version 330 core
-
-layout (location = 0) in vec3 pos;
-layout (location = 1) in vec4 color;
-
-out vec4 v_Color;
-
-uniform mat4 u_ProjModel;
-uniform mat4 u_View;
-
-void main()
-{
- if (gl_VertexID % 2 != 0)
- return;
-
- gl_Position = u_ProjModel * u_View * vec4(pos, 1.0);
-}
\ No newline at end of file
diff --git a/src/main/resources/lambda.accesswidener b/src/main/resources/lambda.accesswidener
index 484d80718..21abfb4d7 100644
--- a/src/main/resources/lambda.accesswidener
+++ b/src/main/resources/lambda.accesswidener
@@ -68,10 +68,17 @@ transitive-accessible field net/minecraft/client/render/Camera pos Lnet/minecraf
# Renderer
transitive-accessible field net/minecraft/client/texture/NativeImage pointer J
+transitive-accessible field net/minecraft/client/texture/SpriteContents image Lnet/minecraft/client/texture/NativeImage;
transitive-accessible class net/minecraft/client/gui/screen/SplashOverlay$LogoTexture
transitive-accessible field com/mojang/blaze3d/systems/RenderSystem$ShapeIndexBuffer indexBuffer Lcom/mojang/blaze3d/buffers/GpuBuffer;
transitive-accessible field net/minecraft/client/gl/GlGpuBuffer id I
+# BufferBuilder - Custom vertex element support
+transitive-accessible method net/minecraft/client/render/BufferBuilder beginElement (Lcom/mojang/blaze3d/vertex/VertexFormatElement;)J
+transitive-accessible field net/minecraft/client/render/BufferBuilder offsetsByElementId [I
+transitive-accessible field net/minecraft/client/render/BufferBuilder vertexPointer J
+transitive-accessible field net/minecraft/client/render/BufferBuilder currentMask I
+
# Text
transitive-accessible field net/minecraft/text/Style color Lnet/minecraft/text/TextColor;
transitive-accessible field net/minecraft/text/Style bold Ljava/lang/Boolean;
@@ -123,3 +130,22 @@ transitive-accessible method net/minecraft/item/BlockItem getPlacementState (Lne
transitive-accessible method net/minecraft/block/AbstractBlock getPickStack (Lnet/minecraft/world/WorldView;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/BlockState;Z)Lnet/minecraft/item/ItemStack;
transitive-accessible field net/minecraft/client/gui/screen/ingame/HandledScreen focusedSlot Lnet/minecraft/screen/slot/Slot;
transitive-accessible field net/minecraft/registry/SimpleRegistry frozen Z
+
+# Render Item
+transitive-accessible field net/minecraft/client/render/item/ItemRenderState layers [Lnet/minecraft/client/render/item/ItemRenderState$LayerRenderState;
+transitive-accessible field net/minecraft/client/render/item/ItemRenderState layerCount I
+transitive-accessible field net/minecraft/client/render/item/ItemRenderState displayContext Lnet/minecraft/item/ItemDisplayContext;
+transitive-accessible field net/minecraft/client/render/item/ItemRenderState$LayerRenderState quads Ljava/util/List;
+transitive-accessible field net/minecraft/client/render/item/ItemRenderState$LayerRenderState useLight Z
+transitive-accessible field net/minecraft/client/render/item/ItemRenderState$LayerRenderState particle Lnet/minecraft/client/texture/Sprite;
+transitive-accessible field net/minecraft/client/render/item/ItemRenderState$LayerRenderState transform Lnet/minecraft/client/render/model/json/Transformation;
+transitive-accessible field net/minecraft/client/render/item/ItemRenderState$LayerRenderState tints [I
+transitive-accessible field net/minecraft/client/render/item/ItemRenderState$LayerRenderState glint Lnet/minecraft/client/render/item/ItemRenderState$Glint;
+transitive-accessible field net/minecraft/client/render/item/ItemRenderState$LayerRenderState specialModelType Lnet/minecraft/client/render/item/model/special/SpecialModelRenderer;
+transitive-accessible field net/minecraft/client/render/item/ItemRenderState$LayerRenderState renderLayer Lnet/minecraft/client/render/RenderLayer;
+transitive-accessible field net/minecraft/client/render/item/ItemRenderState$LayerRenderState data Ljava/lang/Object;
+
+# Entity Renderer
+transitive-accessible method net/minecraft/client/render/entity/ProjectileEntityRenderer getTexture (Lnet/minecraft/client/render/entity/state/ProjectileEntityRenderState;)Lnet/minecraft/util/Identifier;
+transitive-accessible field net/minecraft/client/render/entity/BoatEntityRenderer texture Lnet/minecraft/util/Identifier;
+transitive-accessible field net/minecraft/client/render/entity/EndCrystalEntityRenderer TEXTURE Lnet/minecraft/util/Identifier;
diff --git a/src/main/resources/lambda.mixins.json b/src/main/resources/lambda.mixins.json
index 0326b21f6..96fefd9a1 100644
--- a/src/main/resources/lambda.mixins.json
+++ b/src/main/resources/lambda.mixins.json
@@ -1,98 +1,97 @@
{
- "required": true,
- "minVersion": "0.8",
- "package": "com.lambda.mixin",
- "compatibilityLevel": "JAVA_21",
- "client": [
- "CrashReportMixin",
- "MinecraftClientMixin",
- "baritone.BaritonePlayerContextMixin",
- "baritone.LookBehaviourMixin",
- "client.sound.SoundSystemMixin",
- "entity.ClientPlayerEntityMixin",
- "entity.ClientPlayInteractionManagerMixin",
- "entity.EntityMixin",
- "entity.FireworkRocketEntityMixin",
- "entity.HandledScreensMixin",
- "entity.LivingEntityMixin",
- "entity.PlayerEntityMixin",
- "entity.PlayerInventoryMixin",
- "input.KeyBindingMixin",
- "input.KeyboardMixin",
- "input.MouseMixin",
- "items.BarrierBlockMixin",
- "items.BlockItemMixin",
- "items.FilledMapItemMixin",
- "network.ClientConnectionMixin",
- "network.ClientLoginNetworkMixin",
- "network.ClientPlayNetworkHandlerMixin",
- "network.HandshakeC2SPacketMixin",
- "network.LoginHelloC2SPacketMixin",
- "network.LoginKeyC2SPacketMixin",
- "render.AbstractTerrainRenderContextMixin",
- "render.ArmorFeatureRendererMixin",
- "render.BlockMixin",
- "render.BlockModelRendererMixin",
- "render.BlockRenderManagerMixin",
- "render.BossBarHudMixin",
- "render.CameraMixin",
- "render.CapeFeatureRendererMixin",
- "render.ChatHudMixin",
- "render.ChatInputSuggestorMixin",
- "render.ChatScreenMixin",
- "render.ChunkOcclusionDataBuilderMixin",
- "render.DrawContextMixin",
- "render.ElytraFeatureRendererMixin",
- "render.EntityRendererMixin",
- "render.FluidRendererMixin",
- "render.FogRendererMixin",
- "render.GameRendererMixin",
- "render.GlStateManagerMixin",
- "render.HandledScreenMixin",
- "render.HeadFeatureRendererMixin",
- "render.HeldItemRendererMixin",
- "render.InGameHudMixin",
- "render.InGameOverlayRendererMixin",
- "render.LightmapTextureManagerMixin",
- "render.LivingEntityRendererMixin",
- "render.PlayerListHudMixin",
- "render.RenderLayersMixin",
- "render.ScreenHandlerMixin",
- "render.ScreenMixin",
- "render.SodiumBlockOcclusionCacheMixin",
- "render.SodiumBlockRendererMixin",
- "render.SodiumFluidRendererImplMixin",
- "render.SodiumLightDataAccessMixin",
- "render.SodiumWorldRendererMixin",
- "render.SplashOverlayMixin",
- "render.SplashOverlayMixin$LogoTextureMixin",
- "render.StatusEffectFogModifierMixin",
- "render.TooltipComponentMixin",
- "render.WeatherRenderingMixin",
- "render.WorldBorderRenderingMixin",
- "render.WorldRendererMixin",
- "render.blockentity.AbstractSignBlockEntityRendererMixin",
- "render.blockentity.BeaconBlockEntityRendererMixin",
- "render.blockentity.BlockEntityRenderDispatcherMixin",
- "render.blockentity.EnchantingTableBlockEntityRendererMixin",
- "render.blockentity.MobSpawnerBlockEntityRendererMixin",
- "render.particle.BillboardParticleMixin",
- "render.particle.ElderGuardianParticleRendererMixin",
- "render.particle.ItemPickupParticleRendererMixin",
- "world.AbstractBlockMixin",
- "world.BlockCollisionSpliteratorMixin",
- "world.ClientChunkManagerMixin",
- "world.ClientWorldMixin",
- "world.DirectionMixin",
- "world.StructureTemplateMixin",
- "world.WorldMixin"
- ],
- "injectors": {
- "defaultRequire": 0
- },
- "overwrites": {
- "conformVisibility": true
- },
- "mixinextras": {
- "minVersion": "0.5.0"}
-}
+ "required": true,
+ "minVersion": "0.8",
+ "package": "com.lambda.mixin",
+ "compatibilityLevel": "JAVA_21",
+ "client": [
+ "CrashReportMixin",
+ "MinecraftClientMixin",
+ "baritone.BaritonePlayerContextMixin",
+ "baritone.LookBehaviourMixin",
+ "client.sound.SoundSystemMixin",
+ "entity.ClientPlayerEntityMixin",
+ "entity.ClientPlayInteractionManagerMixin",
+ "entity.EntityMixin",
+ "entity.FireworkRocketEntityMixin",
+ "entity.HandledScreensMixin",
+ "entity.LivingEntityMixin",
+ "entity.PlayerEntityMixin",
+ "entity.PlayerInventoryMixin",
+ "input.KeyBindingMixin",
+ "input.KeyboardMixin",
+ "input.MouseMixin",
+ "items.BlockItemMixin",
+ "items.FilledMapItemMixin",
+ "network.ClientConnectionMixin",
+ "network.ClientLoginNetworkMixin",
+ "network.ClientPlayNetworkHandlerMixin",
+ "network.HandshakeC2SPacketMixin",
+ "network.LoginHelloC2SPacketMixin",
+ "network.LoginKeyC2SPacketMixin",
+ "render.AbstractTerrainRenderContextMixin",
+ "render.ArmorFeatureRendererMixin",
+ "render.BlockMixin",
+ "render.BlockModelRendererMixin",
+ "render.BossBarHudMixin",
+ "render.CameraMixin",
+ "render.CapeFeatureRendererMixin",
+ "render.ChatHudMixin",
+ "render.ChatInputSuggestorMixin",
+ "render.ChatScreenMixin",
+ "render.ChunkOcclusionDataBuilderMixin",
+ "render.DrawContextMixin",
+ "render.ElytraFeatureRendererMixin",
+ "render.EntityRendererMixin",
+ "render.FluidRendererMixin",
+ "render.FogRendererMixin",
+ "render.GameRendererMixin",
+ "render.GlStateManagerMixin",
+ "render.HandledScreenMixin",
+ "render.HeadFeatureRendererMixin",
+ "render.HeldItemRendererMixin",
+ "render.InGameHudMixin",
+ "render.InGameOverlayRendererMixin",
+ "render.LightmapTextureManagerMixin",
+ "render.LivingEntityRendererMixin",
+ "render.PlayerListHudMixin",
+ "render.RenderLayersMixin",
+ "render.ScreenHandlerMixin",
+ "render.ScreenMixin",
+ "render.SodiumBlockOcclusionCacheMixin",
+ "render.SodiumBlockRendererMixin",
+ "render.SodiumFluidRendererImplMixin",
+ "render.SodiumLightDataAccessMixin",
+ "render.SodiumWorldRendererMixin",
+ "render.SplashOverlayMixin",
+ "render.SplashOverlayMixin$LogoTextureMixin",
+ "render.StatusEffectFogModifierMixin",
+ "render.TooltipComponentMixin",
+ "render.WeatherRenderingMixin",
+ "render.WorldBorderRenderingMixin",
+ "render.WorldRendererMixin",
+ "render.blockentity.AbstractSignBlockEntityRendererMixin",
+ "render.blockentity.BeaconBlockEntityRendererMixin",
+ "render.blockentity.BlockEntityRenderDispatcherMixin",
+ "render.blockentity.EnchantingTableBlockEntityRendererMixin",
+ "render.blockentity.MobSpawnerBlockEntityRendererMixin",
+ "render.particle.BillboardParticleMixin",
+ "render.particle.ElderGuardianParticleRendererMixin",
+ "render.particle.ItemPickupParticleRendererMixin",
+ "world.AbstractBlockMixin",
+ "world.BlockCollisionSpliteratorMixin",
+ "world.ClientChunkManagerMixin",
+ "world.ClientWorldMixin",
+ "world.DirectionMixin",
+ "world.StructureTemplateMixin",
+ "world.WorldMixin"
+ ],
+ "injectors": {
+ "defaultRequire": 0
+ },
+ "overwrites": {
+ "conformVisibility": true
+ },
+ "mixinextras": {
+ "minVersion": "0.5.0"
+ }
+}
\ No newline at end of file