From 3e266b03e3753f4c85e1f028c769e431a4e6d6b3 Mon Sep 17 00:00:00 2001 From: beanbag44 <107891830+beanbag44@users.noreply.github.com> Date: Mon, 12 Jan 2026 11:51:58 +0000 Subject: [PATCH 01/26] depth test / block outline start / immediate region esp where you update vertices each frame --- .../com/lambda/graphics/esp/RegionESP.kt | 16 +- .../lambda/graphics/mc/ImmediateRegionESP.kt | 71 +++++++++ .../graphics/mc/InterpolatedRegionESP.kt | 139 ------------------ .../com/lambda/graphics/mc/RegionRenderer.kt | 12 +- .../lambda/graphics/mc/RegionShapeBuilder.kt | 7 +- .../managers/breaking/BreakManager.kt | 4 +- .../module/modules/combat/AutoDisconnect.kt | 4 +- .../lambda/module/modules/combat/AutoTotem.kt | 4 +- .../module/modules/render/BlockOutline.kt | 93 ++++++++++++ .../lambda/module/modules/render/EntityESP.kt | 83 +++-------- .../lambda/module/modules/render/Particles.kt | 7 +- .../kotlin/com/lambda/util/DebugInfoHud.kt | 4 +- .../kotlin/com/lambda/util/extension/Mixin.kt | 6 +- 13 files changed, 219 insertions(+), 231 deletions(-) create mode 100644 src/main/kotlin/com/lambda/graphics/mc/ImmediateRegionESP.kt delete mode 100644 src/main/kotlin/com/lambda/graphics/mc/InterpolatedRegionESP.kt create mode 100644 src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt diff --git a/src/main/kotlin/com/lambda/graphics/esp/RegionESP.kt b/src/main/kotlin/com/lambda/graphics/esp/RegionESP.kt index b32e03755..fe0f3e63f 100644 --- a/src/main/kotlin/com/lambda/graphics/esp/RegionESP.kt +++ b/src/main/kotlin/com/lambda/graphics/esp/RegionESP.kt @@ -21,18 +21,18 @@ 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.lambda.util.extension.tickDeltaF 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 +import java.util.concurrent.ConcurrentHashMap +import kotlin.math.floor /** * Base class for region-based ESP systems. Provides unified rendering logic and region management. */ -abstract class RegionESP(val name: String, val depthTest: Boolean) { +abstract class RegionESP(val name: String, var depthTest: Boolean) { protected val renderers = ConcurrentHashMap() /** Get or create a ShapeScope for a specific world position. */ @@ -53,9 +53,9 @@ abstract class RegionESP(val name: String, val depthTest: Boolean) { /** * Render all active regions. - * @param tickDelta Progress within current tick (used for interpolation) + * @param tickDeltaF Progress within current tick (used for interpolation) */ - open fun render(tickDelta: Float = mc.tickDelta) { + fun render() { val camera = mc.gameRenderer?.camera ?: return val cameraPos = camera.pos @@ -78,7 +78,7 @@ abstract class RegionESP(val name: String, val depthTest: Boolean) { } // Render Faces - RegionRenderer.createRenderPass("$name Faces")?.use { pass -> + RegionRenderer.createRenderPass("$name Faces", depthTest)?.use { pass -> val pipeline = if (depthTest) LambdaRenderPipelines.ESP_QUADS else LambdaRenderPipelines.ESP_QUADS_THROUGH @@ -91,7 +91,7 @@ abstract class RegionESP(val name: String, val depthTest: Boolean) { } // Render Edges - RegionRenderer.createRenderPass("$name Edges")?.use { pass -> + RegionRenderer.createRenderPass("$name Edges", depthTest)?.use { pass -> val pipeline = if (depthTest) LambdaRenderPipelines.ESP_LINES else LambdaRenderPipelines.ESP_LINES_THROUGH diff --git a/src/main/kotlin/com/lambda/graphics/mc/ImmediateRegionESP.kt b/src/main/kotlin/com/lambda/graphics/mc/ImmediateRegionESP.kt new file mode 100644 index 000000000..52bee6874 --- /dev/null +++ b/src/main/kotlin/com/lambda/graphics/mc/ImmediateRegionESP.kt @@ -0,0 +1,71 @@ +/* + * 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.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. + * + * This system rebuilds and uploads vertices every frame. Callers are responsible for providing + * interpolated positions (e.g., using entity.prevX/x with tickDelta). The tick() method swaps + * builders to allow smooth transitions between frames. + */ +class ImmediateRegionESP(name: String, depthTest: Boolean = false) : RegionESP(name, depthTest) { + // Current frame builders (being populated this tick) + private val currBuilders = ConcurrentHashMap() + + 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)) + } + scope.apply(block) + } + + override fun clear() { + currBuilders.clear() + } + + fun tick() { + currBuilders.clear() + } + + override fun upload() { + val activeKeys = currBuilders.keys.toSet() + + currBuilders.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/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/RegionRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/RegionRenderer.kt index 8d7f36f4a..f3742912b 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/RegionRenderer.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/RegionRenderer.kt @@ -126,14 +126,24 @@ class RegionRenderer(val region: RenderRegion) { companion object { /** Helper to create a render pass targeting the main framebuffer. */ fun createRenderPass(label: String): RenderPass? { + return createRenderPass(label, useDepth = true) + } + + /** + * Helper to create a render pass targeting the main framebuffer. + * @param label Debug label for the render pass + * @param useDepth Whether to attach the depth buffer for depth testing + */ + fun createRenderPass(label: String, useDepth: Boolean): RenderPass? { val framebuffer = mc.framebuffer ?: return null + val depthView = if (useDepth) framebuffer.depthAttachmentView else null return RenderSystem.getDevice() .createCommandEncoder() .createRenderPass( { label }, framebuffer.colorAttachmentView, OptionalInt.empty(), - framebuffer.depthAttachmentView, + depthView, OptionalDouble.empty() ) } diff --git a/src/main/kotlin/com/lambda/graphics/mc/RegionShapeBuilder.kt b/src/main/kotlin/com/lambda/graphics/mc/RegionShapeBuilder.kt index 8b33498be..9b2b844e4 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/RegionShapeBuilder.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/RegionShapeBuilder.kt @@ -24,7 +24,7 @@ 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 com.lambda.util.extension.tickDelta import net.minecraft.block.BlockState import net.minecraft.block.entity.BlockEntity import net.minecraft.entity.Entity @@ -35,7 +35,6 @@ 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 @@ -138,7 +137,7 @@ class RegionShapeBuilder(val region: RenderRegion) { val pair = box.pair ?: return val prev = pair.first val curr = pair.second - val tickDelta = mc.partialTicks + val tickDelta = mc.tickDelta val interpolated = Box( lerp(tickDelta, prev.minX, curr.minX), lerp(tickDelta, prev.minY, curr.minY), @@ -231,7 +230,7 @@ class RegionShapeBuilder(val region: RenderRegion) { val pair = box.pair ?: return val prev = pair.first val curr = pair.second - val tickDelta = mc.partialTicks + val tickDelta = mc.tickDelta val interpolated = Box( lerp(tickDelta, prev.minX, curr.minX), lerp(tickDelta, prev.minY, curr.minY), 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 252bad051..4d2555038 100644 --- a/src/main/kotlin/com/lambda/interaction/managers/breaking/BreakManager.kt +++ b/src/main/kotlin/com/lambda/interaction/managers/breaking/BreakManager.kt @@ -77,7 +77,7 @@ 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.extension.tickDelta import com.lambda.util.item.ItemUtils.block import com.lambda.util.math.lerp import com.lambda.util.player.gamemode @@ -248,7 +248,7 @@ object BreakManager : Manager( val currentProgress = currentDelta / adjustedThreshold val nextTicksProgress = (currentDelta + breakDelta) / adjustedThreshold - val interpolatedProgress = lerp(mc.partialTicks, currentProgress, nextTicksProgress) + val interpolatedProgress = lerp(mc.tickDelta, currentProgress, nextTicksProgress) val fillColor = if (config.dynamicFillColor) lerp( interpolatedProgress, 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 da86558f2..c512748c2 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 @@ -219,7 +219,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/render/BlockOutline.kt b/src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt new file mode 100644 index 000000000..d678ea284 --- /dev/null +++ b/src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt @@ -0,0 +1,93 @@ +/* + * 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.event.events.RenderEvent +import com.lambda.event.events.TickEvent +import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.graphics.mc.ImmediateRegionESP +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.block.BlockState +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, 60)) { fill } + private val outline by setting("Outline", true) + private val outlineColor by setting("Outline Color", Color.WHITE) { outline } + private val lineWidth by setting("Line Width", 1.0f, 0.5f..10.0f, 0.1f) { outline } + private val interpolate by setting("Interpolate", true) + private val throughWalls by setting("ESP", true) + .onValueChange { _, to -> renderer.depthTest = !to } + + val renderer = ImmediateRegionESP("BlockOutline") + + var previous: Pair, BlockState>? = null + + init { + listen { + renderer.tick() + + val hitResult = mc.crosshairTarget?.blockResult ?: return@listen + val pos = hitResult.blockPos + val blockState = blockState(pos) + val boxes = blockState + .getOutlineShape(world, pos) + .boundingBoxes + .mapIndexed { index, box -> + val offset = box.offset(pos) + val interpolated = previous?.let { previous -> + if (!interpolate || previous.second !== blockState) null + else lerp(mc.tickDelta, previous.first[index], offset) + } ?: offset + interpolated.expand(0.001) + } + + renderer.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) { + boxes.forEach { box -> + if (fill) filled(box, fillColor) + if (outline) outline(box, outlineColor, thickness = lineWidth) + } + } + + renderer.upload() + renderer.render() + } + + listen { + val hitResult = mc.crosshairTarget?.blockResult ?: return@listen + val state = blockState(hitResult.blockPos) + previous = Pair( + state + .getOutlineShape(world, hitResult.blockPos).boundingBoxes + .map { it.offset(hitResult.blockPos) }, + state + ) + } + } +} \ 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..93da41070 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/EntityESP.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/EntityESP.kt @@ -17,19 +17,15 @@ 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.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.ImmediateRegionESP 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.extension.tickDeltaF import com.lambda.util.math.setAlpha import com.lambda.util.world.entitySearch import imgui.ImGui @@ -54,7 +50,7 @@ object EntityESP : Module( description = "Highlight entities with smooth interpolated rendering", tag = ModuleTag.RENDER ) { - private val esp = InterpolatedRegionESP("EntityESP") + private val esp = ImmediateRegionESP("EntityESP") private data class LabelData( val screenX: Float, @@ -110,8 +106,10 @@ object EntityESP : Module( private val otherColor by setting("Other Color", Color(200, 200, 200), "Color for other entities").group(Group.Colors) init { - listen { + listen { + esp.depthTest = !throughWalls esp.tick() + val tickDelta = mc.tickDeltaF entitySearch(range) { shouldRender(it) }.forEach { entity -> val color = getEntityColor(entity) @@ -129,69 +127,26 @@ object EntityESP : Module( ) } } - } - } - - 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 color = getEntityColor(entity) + val entityPos = getInterpolatedPos(entity, tickDelta) 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 - ) - ) + tracer(startPos, endPos, entity.id) { + color(color.setAlpha(outlineAlpha)) + width(tracerWidth) + if (dashedTracers) dashed(dashLength, gapLength) } } } - - tracerEsp.upload() - tracerEsp.render() - tracerEsp.close() } + + esp.upload() + esp.render() + + // Clear pending labels from previous frame + pendingLabels.clear() } // Draw ImGUI labels using pre-computed screen coordinates @@ -310,7 +265,7 @@ object EntityESP : Module( 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)) + camera.cameraPos.add(Vec3d(camera.horizontalPlane).multiply(0.1)) } } } diff --git a/src/main/kotlin/com/lambda/module/modules/render/Particles.kt b/src/main/kotlin/com/lambda/module/modules/render/Particles.kt index a50eb92a9..fdf06c0cc 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/Particles.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/Particles.kt @@ -38,7 +38,7 @@ 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.extension.tickDelta import com.lambda.util.math.DOWN import com.lambda.util.math.MathUtils.random import com.lambda.util.math.UP @@ -53,7 +53,6 @@ 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 @@ -180,7 +179,7 @@ object Particles : Module( } fun build(builder: VertexBuilder) = builder.apply { - val smoothAge = age + mc.partialTicks + val smoothAge = age + mc.tickDelta val colorTicks = smoothAge * 0.1 / ClickGuiLayout.colorSpeed val alpha = when { @@ -196,7 +195,7 @@ object Particles : Module( 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 position = lerp(mc.tickDelta, prevPos, position) val size = if (lay) environmentSize else sizeSetting * lerp(alpha, 0.5, 1.0) withVertexTransform(buildWorldProjection(position, size, projRotation)) { 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/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) From ad8846928dedb8c5233fc1b7ab6bafc10d0b311b Mon Sep 17 00:00:00 2001 From: beanbag44 <107891830+beanbag44@users.noreply.github.com> Date: Mon, 12 Jan 2026 23:55:15 +0000 Subject: [PATCH 02/26] fix static and dynamic render events --- .../mixin/render/GameRendererMixin.java | 7 ++++++ .../com/lambda/event/events/RenderEvent.kt | 10 +++++--- .../kotlin/com/lambda/graphics/RenderMain.kt | 25 ++++++++++--------- .../managers/breaking/BreakManager.kt | 9 +++---- .../lambda/module/modules/movement/Blink.kt | 3 +-- .../module/modules/network/PacketDelay.kt | 2 +- .../module/modules/render/BlockOutline.kt | 4 +-- 7 files changed, 33 insertions(+), 27 deletions(-) diff --git a/src/main/java/com/lambda/mixin/render/GameRendererMixin.java b/src/main/java/com/lambda/mixin/render/GameRendererMixin.java index 98cf293e7..5f3db5280 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 { @@ -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/kotlin/com/lambda/event/events/RenderEvent.kt b/src/main/kotlin/com/lambda/event/events/RenderEvent.kt index 36e2dbe37..24d8fa73f 100644 --- a/src/main/kotlin/com/lambda/event/events/RenderEvent.kt +++ b/src/main/kotlin/com/lambda/event/events/RenderEvent.kt @@ -23,16 +23,18 @@ 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.ImmediateRegionESP import com.lambda.graphics.mc.TransientRegionESP fun Any.onStaticRender(block: SafeContext.(TransientRegionESP) -> Unit) = - listen { block(RenderMain.StaticESP) } + listen { block(RenderMain.staticESP) } -fun Any.onDynamicRender(block: SafeContext.(TransientRegionESP) -> Unit) = - listen { block(RenderMain.DynamicESP) } +fun Any.onDynamicRender(block: SafeContext.(ImmediateRegionESP) -> Unit) = + listen { block(RenderMain.dynamicESP) } sealed class RenderEvent { - object Upload : Event + object UploadStatic : Event + object UploadDynamic: Event object Render : Event class UpdateTarget : ICancellable by Cancellable() diff --git a/src/main/kotlin/com/lambda/graphics/RenderMain.kt b/src/main/kotlin/com/lambda/graphics/RenderMain.kt index 1353fec6e..92c2237ac 100644 --- a/src/main/kotlin/com/lambda/graphics/RenderMain.kt +++ b/src/main/kotlin/com/lambda/graphics/RenderMain.kt @@ -24,6 +24,7 @@ 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.ImmediateRegionESP import com.lambda.graphics.mc.TransientRegionESP import net.minecraft.util.math.Vec3d import org.joml.Matrix4f @@ -32,10 +33,10 @@ import org.joml.Vector4f object RenderMain { @JvmStatic - val StaticESP = TransientRegionESP("Static") + val staticESP = TransientRegionESP("Static") @JvmStatic - val DynamicESP = TransientRegionESP("Dynamic") + val dynamicESP = ImmediateRegionESP("Dynamic") val projectionMatrix = Matrix4f() val modelViewMatrix @@ -90,22 +91,22 @@ object RenderMain { 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) + staticESP.render() RenderEvent.Render.post() + dynamicESP.render() } init { listen { - StaticESP.clear() - DynamicESP.clear() - - RenderEvent.Upload.post() - - StaticESP.upload() - DynamicESP.upload() + staticESP.clear() + RenderEvent.UploadStatic.post() + staticESP.upload() + } + listen { + dynamicESP.clear() + RenderEvent.UploadDynamic.post() + dynamicESP.upload() } } } 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 4d2555038..c0b301b7c 100644 --- a/src/main/kotlin/com/lambda/interaction/managers/breaking/BreakManager.kt +++ b/src/main/kotlin/com/lambda/interaction/managers/breaking/BreakManager.kt @@ -26,7 +26,6 @@ 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.interaction.construction.blueprint.Blueprint.Companion.toStructure import com.lambda.interaction.construction.simulation.BuildSimulator.simulate import com.lambda.interaction.construction.simulation.context.BreakContext @@ -269,11 +268,9 @@ object BreakManager : Manager( it.offset(pos) }.forEach boxes@{ 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) + if (config.fill) filled(interpolatedBox, fillColor) + if (config.outline) outline(interpolatedBox, outlineColor) } } } 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..304f706d1 100644 --- a/src/main/kotlin/com/lambda/module/modules/movement/Blink.kt +++ b/src/main/kotlin/com/lambda/module/modules/movement/Blink.kt @@ -22,7 +22,6 @@ 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.gui.components.ClickGuiLayout import com.lambda.module.Module @@ -59,7 +58,7 @@ object Blink : Module( private var lastBox = Box(BlockPos.ORIGIN) init { - listen { + listen { val time = System.currentTimeMillis() if (isActive && time - lastUpdate < delay) return@listen 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..6095b6f48 100644 --- a/src/main/kotlin/com/lambda/module/modules/network/PacketDelay.kt +++ b/src/main/kotlin/com/lambda/module/modules/network/PacketDelay.kt @@ -53,7 +53,7 @@ object PacketDelay : Module( private var inboundLastUpdate = 0L init { - listen { + listen { if (mode != Mode.Static) return@listen flushPools(System.currentTimeMillis()) diff --git a/src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt b/src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt index d678ea284..fee5d34d3 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt @@ -37,9 +37,9 @@ object BlockOutline : Module( tag = ModuleTag.RENDER ) { private val fill by setting("Fill", true) - private val fillColor by setting("Fill Color", Color(255, 255, 255, 60)) { fill } + 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.WHITE) { outline } + private val outlineColor by setting("Outline Color", Color(255, 255, 255, 120)) { outline } private val lineWidth by setting("Line Width", 1.0f, 0.5f..10.0f, 0.1f) { outline } private val interpolate by setting("Interpolate", true) private val throughWalls by setting("ESP", true) From 0aec9f4d097c92a32378fc027d88e7b37e9c90b4 Mon Sep 17 00:00:00 2001 From: beanbag44 <107891830+beanbag44@users.noreply.github.com> Date: Tue, 13 Jan 2026 09:28:44 +0000 Subject: [PATCH 03/26] remove shape caching --- .../com/lambda/graphics/esp/ShapeScope.kt | 203 +----------------- .../lambda/module/modules/render/EntityESP.kt | 4 +- 2 files changed, 6 insertions(+), 201 deletions(-) diff --git a/src/main/kotlin/com/lambda/graphics/esp/ShapeScope.kt b/src/main/kotlin/com/lambda/graphics/esp/ShapeScope.kt index 14ab277f5..36a7a8a84 100644 --- a/src/main/kotlin/com/lambda/graphics/esp/ShapeScope.kt +++ b/src/main/kotlin/com/lambda/graphics/esp/ShapeScope.kt @@ -18,156 +18,78 @@ 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) { +class ShapeScope(val region: RenderRegion) { 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) { + fun box(box: Box, 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) { + fun line(start: Vec3d, end: Vec3d, color: Color, width: Float = 1.0f) { 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 = {}) { + fun line(from: Vec3d, to: Vec3d, 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( @@ -180,9 +102,6 @@ class ShapeScope(val region: RenderRegion, val collectShapes: Boolean = false) { 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( @@ -194,9 +113,6 @@ class ShapeScope(val region: RenderRegion, val collectShapes: Boolean = false) { 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( @@ -208,9 +124,6 @@ class ShapeScope(val region: RenderRegion, val collectShapes: Boolean = false) { 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( @@ -222,13 +135,6 @@ class ShapeScope(val region: RenderRegion, val collectShapes: Boolean = false) { 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( @@ -240,19 +146,6 @@ class ShapeScope(val region: RenderRegion, val collectShapes: Boolean = false) { 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( @@ -264,19 +157,6 @@ class ShapeScope(val region: RenderRegion, val collectShapes: Boolean = false) { 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 - ) - ) - } } } @@ -339,78 +219,3 @@ class LineScope(val from: Vec3d, val to: Vec3d, val parent: ShapeScope) { } } } - -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/module/modules/render/EntityESP.kt b/src/main/kotlin/com/lambda/module/modules/render/EntityESP.kt index 93da41070..5a200c2f0 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/EntityESP.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/EntityESP.kt @@ -117,7 +117,7 @@ object EntityESP : Module( esp.shapes(entity.x, entity.y, entity.z) { if (drawBoxes) { - box(box, entity.id) { + box(box) { if (drawFilled) filled(color.setAlpha(filledAlpha)) if (drawOutline) @@ -133,7 +133,7 @@ object EntityESP : Module( val entityPos = getInterpolatedPos(entity, tickDelta) val startPos = getTracerStartPos(tickDelta) val endPos = entityPos.add(0.0, entity.height / 2.0, 0.0) - tracer(startPos, endPos, entity.id) { + line(startPos, endPos) { color(color.setAlpha(outlineAlpha)) width(tracerWidth) if (dashedTracers) dashed(dashLength, gapLength) From 1efc938f4d092e020d8c7ce1e22dad706b76f844 Mon Sep 17 00:00:00 2001 From: beanbag44 <107891830+beanbag44@users.noreply.github.com> Date: Thu, 15 Jan 2026 15:18:29 +0000 Subject: [PATCH 04/26] initial working standard and sdf text world rendering --- .../graphics/mc/LambdaRenderPipelines.kt | 82 +++ .../com/lambda/graphics/text/FontAtlas.kt | 280 ++++++++ .../com/lambda/graphics/text/SDFFontAtlas.kt | 622 ++++++++++++++++++ .../lambda/graphics/text/SDFTextRenderer.kt | 460 +++++++++++++ .../com/lambda/graphics/text/TextRenderer.kt | 308 +++++++++ src/main/kotlin/com/lambda/gui/DearImGui.kt | 1 - .../lambda/module/modules/render/EntityESP.kt | 56 +- .../assets/lambda/shaders/core/sdf_text.fsh | 50 ++ .../assets/lambda/shaders/core/sdf_text.vsh | 24 + 9 files changed, 1879 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/com/lambda/graphics/text/FontAtlas.kt create mode 100644 src/main/kotlin/com/lambda/graphics/text/SDFFontAtlas.kt create mode 100644 src/main/kotlin/com/lambda/graphics/text/SDFTextRenderer.kt create mode 100644 src/main/kotlin/com/lambda/graphics/text/TextRenderer.kt create mode 100644 src/main/resources/assets/lambda/shaders/core/sdf_text.fsh create mode 100644 src/main/resources/assets/lambda/shaders/core/sdf_text.vsh diff --git a/src/main/kotlin/com/lambda/graphics/mc/LambdaRenderPipelines.kt b/src/main/kotlin/com/lambda/graphics/mc/LambdaRenderPipelines.kt index 350a40a77..c073d71d2 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/LambdaRenderPipelines.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/LambdaRenderPipelines.kt @@ -116,4 +116,86 @@ 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 SDF-specific uniforms for effects. + */ + val SDF_TEXT: RenderPipeline = + RenderPipelines.register( + RenderPipeline.builder(LAMBDA_ESP_SNIPPET) + .withLocation(Identifier.of("lambda", "pipeline/sdf_text")) + .withVertexShader(Identifier.of("lambda", "core/sdf_text")) + .withFragmentShader(Identifier.of("lambda", "core/sdf_text")) + .withSampler("Sampler0") + .withBlend(BlendFunction.TRANSLUCENT) + .withDepthWrite(false) + .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST) + .withCull(false) + .withVertexFormat( + VertexFormats.POSITION_TEXTURE_COLOR, + 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/sdf_text")) + .withFragmentShader(Identifier.of("lambda", "core/sdf_text")) + .withSampler("Sampler0") + .withBlend(BlendFunction.TRANSLUCENT) + .withDepthWrite(false) + .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) + .withCull(false) + .withVertexFormat( + VertexFormats.POSITION_TEXTURE_COLOR, + VertexFormat.DrawMode.QUADS + ) + .build() + ) } diff --git a/src/main/kotlin/com/lambda/graphics/text/FontAtlas.kt b/src/main/kotlin/com/lambda/graphics/text/FontAtlas.kt new file mode 100644 index 000000000..dec3f69c7 --- /dev/null +++ b/src/main/kotlin/com/lambda/graphics/text/FontAtlas.kt @@ -0,0 +1,280 @@ +/* + * 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.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.STBTruetype.stbtt_FindGlyphIndex +import org.lwjgl.stb.STBTruetype.stbtt_GetFontVMetrics +import org.lwjgl.stb.STBTruetype.stbtt_GetGlyphBitmapBox +import org.lwjgl.stb.STBTruetype.stbtt_GetGlyphHMetrics +import org.lwjgl.stb.STBTruetype.stbtt_InitFont +import org.lwjgl.stb.STBTruetype.stbtt_MakeGlyphBitmap +import org.lwjgl.stb.STBTruetype.stbtt_ScaleForPixelHeight +import org.lwjgl.system.MemoryStack +import org.lwjgl.system.MemoryUtil +import java.nio.ByteBuffer + +/** + * Font atlas that uses MC 1.21's GPU texture APIs for proper rendering. + * + * Uses STB TrueType for glyph rasterization and MC's GpuTexture/GpuTextureView/GpuSampler + * for texture management, enabling correct texture binding via RenderPass.bindTexture(). + * + * @param fontPath Resource path to TTF/OTF file + * @param fontSize Font size in pixels + * @param atlasWidth Atlas texture width (must be power of 2) + * @param atlasHeight Atlas texture height (must be power of 2) + */ +class FontAtlas( + fontPath: String, + val fontSize: Float = 64f, + val atlasWidth: Int = 2048, + val atlasHeight: Int = 2048 +) : AutoCloseable { + + data class Glyph( + val codepoint: Int, + val x0: Int, val y0: Int, + val x1: Int, val y1: Int, + val xOffset: Float, val yOffset: Float, + val xAdvance: Float, + val u0: Float, val v0: Float, + val u1: Float, val v1: Float + ) + + private val fontBuffer: ByteBuffer + private val fontInfo: STBTTFontinfo + 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 + + // Temporary storage for atlas during construction + private var atlasData: ByteArray? = null + + val lineHeight: Float + val ascent: Float + val descent: Float + + /** 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") + } + + // Calculate scale and metrics + val scale = stbtt_ScaleForPixelHeight(fontInfo, fontSize) + + 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 + } + + // Build atlas data + atlasData = ByteArray(atlasWidth * atlasHeight * 4) // RGBA + buildAtlas(scale) + } + + private fun buildAtlas(scale: Float) { + val data = atlasData ?: return + var penX = 1 + var penY = 1 + var rowHeight = 0 + + // Rasterize printable ASCII + extended Latin + val codepoints = (32..126) + (160..255) + + 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] + + // Check if we need to wrap to next row + if (penX + glyphW + 1 >= atlasWidth) { + penX = 1 + penY += rowHeight + 1 + rowHeight = 0 + } + + // Check atlas overflow + if (penY + glyphH + 1 >= atlasHeight) break + + // Rasterize glyph + if (glyphW > 0 && glyphH > 0) { + val tempBuffer = MemoryUtil.memAlloc(glyphW * glyphH) + try { + stbtt_MakeGlyphBitmap( + fontInfo, tempBuffer, + glyphW, glyphH, glyphW, scale, scale, glyphIndex + ) + // Copy to atlas as RGBA (white with grayscale as alpha) + for (row in 0 until glyphH) { + for (col in 0 until glyphW) { + val srcIndex = row * glyphW + col + val alpha = tempBuffer.get(srcIndex).toInt() and 0xFF + val dstIndex = ((penY + row) * atlasWidth + penX + col) * 4 + data[dstIndex + 0] = 0xFF.toByte() // R + data[dstIndex + 1] = 0xFF.toByte() // G + data[dstIndex + 2] = 0xFF.toByte() // B + data[dstIndex + 3] = alpha.toByte() // A + } + } + } finally { + MemoryUtil.memFree(tempBuffer) + } + } + + // Store glyph info + glyphs[cp] = Glyph( + codepoint = cp, + x0 = penX, y0 = penY, + x1 = penX + glyphW, y1 = penY + glyphH, + xOffset = x0[0].toFloat(), + yOffset = y0[0].toFloat(), + xAdvance = advanceWidth[0] * scale, + u0 = penX.toFloat() / atlasWidth, + v0 = penY.toFloat() / atlasHeight, + u1 = (penX + glyphW).toFloat() / atlasWidth, + v1 = (penY + glyphH).toFloat() / atlasHeight + ) + + penX += glyphW + 1 + rowHeight = maxOf(rowHeight, glyphH) + } + } + } + + /** + * Upload atlas to GPU using MC 1.21 APIs. + * Must be called on the render thread. + */ + fun upload() { + if (glTexture != null) return // Already uploaded + val data = atlasData ?: return + + RenderSystem.assertOnRenderThread() + + val gpuDevice = RenderSystem.getDevice() + + // Create GPU texture (usage flags: 5 = COPY_DST | TEXTURE_BINDING) + glTexture = gpuDevice.createTexture( + "Lambda FontAtlas", + 5, // COPY_DST (1) | TEXTURE_BINDING (4) + TextureFormat.RGBA8, + atlasWidth, atlasHeight, + 1, // layers + 1 // mip levels + ) + + // Create texture view + glTextureView = gpuDevice.createTextureView(glTexture) + + // Get sampler with linear filtering + gpuSampler = RenderSystem.getSamplerCache().get(FilterMode.LINEAR) + + // Create NativeImage and copy data + val nativeImage = NativeImage(atlasWidth, atlasHeight, false) + for (y in 0 until atlasHeight) { + for (x in 0 until atlasWidth) { + val srcIndex = (y * atlasWidth + x) * 4 + val r = data[srcIndex + 0].toInt() and 0xFF + val g = data[srcIndex + 1].toInt() and 0xFF + val b = data[srcIndex + 2].toInt() and 0xFF + val a = data[srcIndex + 3].toInt() and 0xFF + // NativeImage uses ABGR format + val abgr = (a shl 24) or (b shl 16) or (g shl 8) or r + nativeImage.setColor(x, y, abgr) + } + } + + // Upload to GPU + RenderSystem.getDevice().createCommandEncoder().writeToTexture(glTexture, nativeImage) + nativeImage.close() + + // Free atlas data after upload + atlasData = null + } + + fun getGlyph(codepoint: Int): Glyph? = glyphs[codepoint] + + /** Calculate the width of a string in pixels. */ + fun getStringWidth(text: String): Float { + var width = 0f + for (char in text) { + val glyph = glyphs[char.code] ?: glyphs[' '.code] ?: continue + width += glyph.xAdvance + } + return width + } + + override fun close() { + glTextureView?.close() + glTextureView = null + glTexture?.close() + glTexture = null + gpuSampler = null // Sampler is managed by cache, don't close + atlasData = null + MemoryUtil.memFree(fontBuffer) + } +} 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..97a850def --- /dev/null +++ b/src/main/kotlin/com/lambda/graphics/text/SDFFontAtlas.kt @@ -0,0 +1,622 @@ +/* + * 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.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_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.stb.STBTruetype.STBTT_vcurve +import org.lwjgl.stb.STBTruetype.STBTT_vline +import org.lwjgl.stb.STBTruetype.STBTT_vmove +import org.lwjgl.system.MemoryStack +import org.lwjgl.system.MemoryUtil +import java.nio.ByteBuffer +import kotlin.math.abs +import kotlin.math.min +import kotlin.math.sqrt + +/** + * 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 = 128f, + 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 + ) + + 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() + } + + private fun buildSDFAtlas() { + val data = atlasData ?: return + var penX = sdfSpread + var penY = sdfSpread + var rowHeight = 0 + + val codepoints = (32..126) + (160..255) + + 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 + } + + if (glyphW > 0 && glyphH > 0) { + generateGlyphSDF(glyphIndex, data, penX, penY, paddedW, paddedH, glyphW, glyphH) + } + + glyphs[cp] = 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 + ) + + penX += paddedW + sdfSpread + rowHeight = maxOf(rowHeight, paddedH) + } + } + } + + /** + * 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] + + 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 + } + + 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/text/SDFTextRenderer.kt b/src/main/kotlin/com/lambda/graphics/text/SDFTextRenderer.kt new file mode 100644 index 000000000..f36b8b71a --- /dev/null +++ b/src/main/kotlin/com/lambda/graphics/text/SDFTextRenderer.kt @@ -0,0 +1,460 @@ +/* + * 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.graphics.RenderMain +import com.lambda.graphics.mc.LambdaRenderPipelines +import com.lambda.graphics.mc.RegionRenderer +import com.mojang.blaze3d.buffers.GpuBuffer +import com.mojang.blaze3d.systems.RenderSystem +import com.mojang.blaze3d.vertex.VertexFormat +import net.minecraft.client.render.BufferBuilder +import net.minecraft.client.render.VertexFormats +import net.minecraft.client.util.BufferAllocator +import net.minecraft.util.math.Vec3d +import org.joml.Matrix4f +import org.joml.Vector3f +import org.joml.Vector4f +import java.awt.Color +import java.util.concurrent.ConcurrentHashMap + +/** + * High-quality SDF-based text renderer with anti-aliasing and effects. + * + * Features: + * - **Scalable**: Crisp text at any size without pixelation + * - **Anti-aliased**: Smooth edges via SDF sampling + * - **Outline**: Configurable outline color and width + * - **Glow**: Soft outer glow effect + * - **Shadow**: Drop shadow support + * + * Usage: + * ```kotlin + * // Load a font (once during init) + * val font = SDFTextRenderer.loadFont("fonts/FiraSans-Regular.ttf") + * + * // Render with effects + * SDFTextRenderer.drawWorld( + * font = font, + * text = "Player Name", + * pos = entity.eyePos.add(0.0, 0.5, 0.0), + * fontSize = 0.5f, + * style = TextStyle( + * color = Color.WHITE, + * outline = TextOutline(Color.BLACK, 0.1f), + * glow = TextGlow(Color.CYAN, 0.2f) + * ) + * ) + * ``` + */ +object SDFTextRenderer { + private val fonts = ConcurrentHashMap() + private var defaultFont: SDFFontAtlas? = null + + /** Outline effect configuration */ + data class TextOutline( + val color: Color = Color.BLACK, + val width: Float = 0.1f // 0.0 - 0.5 in SDF units + ) + + /** Glow effect configuration */ + data class TextGlow( + val color: Color = Color(0, 200, 255, 180), + val radius: Float = 0.15f // Glow spread in SDF units + ) + + /** Text style configuration */ + data class TextStyle( + val color: Color = Color.WHITE, + val outline: TextOutline? = null, + val glow: TextGlow? = null, + val shadow: Boolean = true, + val shadowColor: Color = Color(0, 0, 0, 180), + val shadowOffset: Float = 0.05f + ) + + /** + * Load a font from resources. + * + * @param path Resource path to TTF/OTF file (e.g., "fonts/FiraSans-Regular.ttf") + * @param size Font size in pixels + * @return The loaded FontAtlas, or null if loading failed + */ + fun loadFont(path: String, size: Float = 256f): SDFFontAtlas? { + val key = "$path@$size" + return fonts.getOrPut(key) { + try { + // Don't call upload() here - it requires render thread + // upload() is called lazily in drawTextQuads when textureId == 0 + SDFFontAtlas(path, size) + } catch (e: Exception) { + System.err.println("[TextRenderer] Failed to load font: $path") + System.err.println("[TextRenderer] Full path attempted: /assets/lambda/$path") + e.printStackTrace() + return null + } + } + } + + /** + * Get or create the default font. + * Size should match SDFFontAtlas defaults (128) to prevent atlas overflow. + */ + fun getDefaultFont(size: Float = 128f): SDFFontAtlas { + defaultFont?.let { return it } + + // Try to load without catching, so the actual exception is visible + 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 + } + + /** + * Draw text at a world position (billboard style). + * + * @param font SDF font atlas to use + * @param text Text to render + * @param pos World position + * @param fontSize Size in world units + * @param style Text styling (color, outline, glow, shadow) + * @param centered Center text horizontally + * @param seeThrough Render through walls + */ + fun drawWorld( + font: SDFFontAtlas? = null, + text: String, + pos: Vec3d, + fontSize: Float = 0.5f, + style: TextStyle = TextStyle(), + centered: Boolean = true, + seeThrough: Boolean = false + ) { + val atlas = font ?: getDefaultFont() + val camera = mc.gameRenderer?.camera ?: return + val cameraPos = camera.pos + + // Camera-relative position + val relX = (pos.x - cameraPos.x).toFloat() + val relY = (pos.y - cameraPos.y).toFloat() + val relZ = (pos.z - cameraPos.z).toFloat() + + // Build billboard model matrix + val modelMatrix = Matrix4f() + .translate(relX, relY, relZ) + .rotate(camera.rotation) + .scale(fontSize, -fontSize, fontSize) + + val textWidth = if (centered) atlas.getStringWidth(text, 1f) else 0f + val startX = -textWidth / 2f + + // Draw shadow first (offset, alpha < 50 signals shadow layer) + if (style.shadow) { + val shadowColor = Color(style.shadowColor.red, style.shadowColor.green, style.shadowColor.blue, 25) + renderTextLayer( + atlas, text, startX + style.shadowOffset, style.shadowOffset, + shadowColor, modelMatrix, seeThrough + ) + } + + // Draw glow layer (alpha 50-99 signals glow layer) + if (style.glow != null) { + val glowColor = Color(style.glow.color.red, style.glow.color.green, style.glow.color.blue, 75) + renderTextLayer( + atlas, text, startX, 0f, + glowColor, modelMatrix, seeThrough + ) + } + + // Draw outline layer (alpha 100-199 signals outline layer) + if (style.outline != null) { + val outlineColor = Color(style.outline.color.red, style.outline.color.green, style.outline.color.blue, 150) + renderTextLayer( + atlas, text, startX, 0f, + outlineColor, modelMatrix, seeThrough + ) + } + + // Draw main text (alpha >= 200 signals main text layer) + val mainColor = Color(style.color.red, style.color.green, style.color.blue, 255) + renderTextLayer( + atlas, text, startX, 0f, + mainColor, modelMatrix, seeThrough + ) + } + + /** + * Draw text on screen at pixel coordinates. + */ + fun drawScreen( + font: SDFFontAtlas? = null, + text: String, + x: Float, + y: Float, + fontSize: Float = 16f, + style: TextStyle = TextStyle() + ) { + val atlas = font ?: getDefaultFont() + val scale = fontSize / atlas.baseSize + + // Create orthographic model matrix + val modelMatrix = Matrix4f() + .translate(x, y, 0f) + .scale(scale, scale, 1f) + + // Use screen-space rendering + if (style.shadow) { + renderTextLayerScreen( + atlas, text, style.shadowOffset * fontSize, style.shadowOffset * fontSize, + style.shadowColor, modelMatrix + ) + } + + if (style.outline != null) { + renderTextLayerScreen( + atlas, text, 0f, 0f, + style.outline.color, modelMatrix + ) + } + + if (style.glow != null) { + renderTextLayerScreen( + atlas, text, 0f, 0f, + style.glow.color, modelMatrix + ) + } + + renderTextLayerScreen( + atlas, text, 0f, 0f, + style.color, modelMatrix + ) + } + + /** + * Draw text at a world position projected to screen. + */ + fun drawWorldToScreen( + font: SDFFontAtlas? = null, + text: String, + worldPos: Vec3d, + fontSize: Float = 16f, + style: TextStyle = TextStyle(), + offsetY: Float = 0f + ) { + val screenPos = RenderMain.worldToScreen(worldPos) ?: return + drawScreen(font, text, screenPos.x, screenPos.y + offsetY, fontSize, style) + } + + private fun renderTextLayer( + atlas: SDFFontAtlas, + text: String, + startX: Float, + startY: Float, + color: Color, + modelMatrix: Matrix4f, + seeThrough: Boolean + ) { + if (!atlas.isUploaded) atlas.upload() + val textureView = atlas.textureView ?: return + val sampler = atlas.sampler ?: return + if (text.isEmpty()) return + + // Build vertices for all glyphs + val vertices = buildTextVertices(atlas, text, startX, startY, color) + if (vertices.isEmpty()) return + + // Upload to GPU buffer + val gpuBuffer = uploadTextVertices(vertices) ?: return + + // Use SDF_TEXT pipeline for proper smoothstep anti-aliasing + val pipeline = if (seeThrough) LambdaRenderPipelines.SDF_TEXT_THROUGH + else LambdaRenderPipelines.SDF_TEXT + + // Calculate model-view uniform (projection is handled by bindDefaultUniforms) + val modelView = Matrix4f(RenderMain.modelViewMatrix).mul(modelMatrix) + val dynamicTransform = RenderSystem.getDynamicUniforms() + .write(modelView, Vector4f(1f, 1f, 1f, 1f), Vector3f(0f, 0f, 0f), Matrix4f()) + + RegionRenderer.createRenderPass("SDF Text", useDepth = !seeThrough)?.use { pass -> + pass.setPipeline(pipeline) + RenderSystem.bindDefaultUniforms(pass) + pass.setUniform("DynamicTransforms", dynamicTransform) + + // Bind texture using MC 1.21's proper API + pass.bindTexture("Sampler0", textureView, sampler) + + // Draw + pass.setVertexBuffer(0, gpuBuffer) + val indexBuffer = RenderSystem.getSequentialBuffer(VertexFormat.DrawMode.QUADS) + // For QUADS mode, each quad (4 vertices) needs 6 indices (2 triangles) + val quadCount = vertices.size / 4 + val indexCount = quadCount * 6 + pass.setIndexBuffer(indexBuffer.getIndexBuffer(indexCount), indexBuffer.indexType) + pass.drawIndexed(0, 0, indexCount, 1) + } + + gpuBuffer.close() + } + + private fun renderTextLayerScreen( + atlas: SDFFontAtlas, + text: String, + offsetX: Float, + offsetY: Float, + color: Color, + modelMatrix: Matrix4f + ) { + if (!atlas.isUploaded) atlas.upload() + val textureView = atlas.textureView ?: return + val sampler = atlas.sampler ?: return + if (text.isEmpty()) return + + val vertices = buildTextVertices(atlas, text, offsetX, offsetY, color) + if (vertices.isEmpty()) return + + val gpuBuffer = uploadTextVertices(vertices) ?: return + + val window = mc.window + val ortho = Matrix4f().ortho( + 0f, window.scaledWidth.toFloat(), + window.scaledHeight.toFloat(), 0f, + -1000f, 1000f + ) + + // Calculate MVP and dynamic uniforms BEFORE opening render pass + val mvp = Matrix4f(ortho).mul(modelMatrix) + val dynamicTransform = RenderSystem.getDynamicUniforms() + .write(mvp, Vector4f(1f, 1f, 1f, 1f), Vector3f(0f, 0f, 0f), Matrix4f()) + + RegionRenderer.createRenderPass("SDF Text Screen", useDepth = false)?.use { pass -> + pass.setPipeline(LambdaRenderPipelines.SDF_TEXT_THROUGH) + // Note: not calling bindDefaultUniforms - we provide complete MVP in DynamicTransforms + pass.setUniform("DynamicTransforms", dynamicTransform) + + // Bind texture using MC 1.21's proper API + pass.bindTexture("Sampler0", textureView, sampler) + + pass.setVertexBuffer(0, gpuBuffer) + val indexBuffer = RenderSystem.getSequentialBuffer(VertexFormat.DrawMode.QUADS) + // For QUADS mode, each quad (4 vertices) needs 6 indices (2 triangles) + val quadCount = vertices.size / 4 + val indexCount = quadCount * 6 + pass.setIndexBuffer(indexBuffer.getIndexBuffer(indexCount), indexBuffer.indexType) + pass.drawIndexed(0, 0, indexCount, 1) + } + + gpuBuffer.close() + } + + private data class TextVertex( + 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 + ) + + private fun buildTextVertices( + atlas: SDFFontAtlas, + text: String, + startX: Float, + startY: Float, + color: Color + ): List { + val vertices = mutableListOf() + var penX = startX + var charCount = 0 + + for (char in text) { + val glyph = atlas.getGlyph(char.code) + if (glyph == null) continue + charCount++ + + val x0 = penX + glyph.bearingX + val y0 = startY - glyph.bearingY + val x1 = x0 + glyph.width / atlas.baseSize + val y1 = y0 + glyph.height / atlas.baseSize + + // Quad vertices (counter-clockwise for MC) + // Bottom-left + vertices.add(TextVertex(x0, y1, 0f, glyph.u0, glyph.v1, color.red, color.green, color.blue, color.alpha)) + // Bottom-right + vertices.add(TextVertex(x1, y1, 0f, glyph.u1, glyph.v1, color.red, color.green, color.blue, color.alpha)) + // Top-right + vertices.add(TextVertex(x1, y0, 0f, glyph.u1, glyph.v0, color.red, color.green, color.blue, color.alpha)) + // Top-left + vertices.add(TextVertex(x0, y0, 0f, glyph.u0, glyph.v0, color.red, color.green, color.blue, color.alpha)) + + penX += glyph.advance + } + + return vertices + } + + private fun uploadTextVertices(vertices: List): GpuBuffer? { + if (vertices.isEmpty()) return null + + var result: GpuBuffer? = null + BufferAllocator(vertices.size * 24).use { allocator -> + val builder = BufferBuilder( + allocator, + VertexFormat.DrawMode.QUADS, + VertexFormats.POSITION_TEXTURE_COLOR + ) + + for (v in vertices) { + builder.vertex(v.x, v.y, v.z) + .texture(v.u, v.v) + .color(v.r, v.g, v.b, v.a) + } + + builder.endNullable()?.let { built -> + result = RenderSystem.getDevice().createBuffer( + { "SDF Text Buffer" }, + GpuBuffer.USAGE_VERTEX, + built.buffer + ) + built.close() + } + } + + return result + } + + /** Calculate text width in world units. */ + fun getWidth(font: SDFFontAtlas? = null, text: String, fontSize: Float = 1f): Float { + val atlas = font ?: getDefaultFont() + return atlas.getStringWidth(text, fontSize) + } + + /** Get line height for a font at given size. */ + fun getLineHeight(font: SDFFontAtlas? = null, fontSize: Float = 1f): Float { + val atlas = font ?: getDefaultFont() + return atlas.lineHeight * fontSize / atlas.baseSize + } + + /** Clean up all loaded fonts. */ + fun cleanup() { + fonts.values.forEach { it.close() } + fonts.clear() + defaultFont = null + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/graphics/text/TextRenderer.kt b/src/main/kotlin/com/lambda/graphics/text/TextRenderer.kt new file mode 100644 index 000000000..d6682b21b --- /dev/null +++ b/src/main/kotlin/com/lambda/graphics/text/TextRenderer.kt @@ -0,0 +1,308 @@ +/* + * 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.graphics.mc.LambdaRenderPipelines +import com.lambda.graphics.mc.RegionRenderer +import com.mojang.blaze3d.buffers.GpuBuffer +import com.mojang.blaze3d.systems.RenderSystem +import com.mojang.blaze3d.vertex.VertexFormat +import net.minecraft.client.render.BufferBuilder +import net.minecraft.client.render.VertexFormats +import net.minecraft.client.util.BufferAllocator +import net.minecraft.util.math.Vec3d +import org.joml.Matrix4f +import org.joml.Vector3f +import org.joml.Vector4f +import java.awt.Color + +/** + * Text renderer using MC 1.21's proper GPU texture APIs. + * + * Uses FontAtlas for glyph data and binds textures correctly via + * RenderPass.bindTexture() for compatibility with MC's new rendering pipeline. + */ +class TextRenderer( + fontPath: String, + fontSize: Float = 256f, + atlasSize: Int = 512 +) : AutoCloseable { + + private val atlas = FontAtlas(fontPath, fontSize, atlasSize, atlasSize) + + /** Font line height in pixels */ + val lineHeight: Float get() = atlas.lineHeight + + /** Font ascent in pixels */ + val ascent: Float get() = atlas.ascent + + /** Font descent in pixels (negative value) */ + val descent: Float get() = atlas.descent + + /** + * Draw text in world space, facing the camera (billboard style). + * + * @param pos World position for the text + * @param text Text string to render + * @param color Text color + * @param scale World-space scale (0.025f is similar to MC name tags) + * @param centered Center text horizontally at position + * @param seeThrough Render through walls + */ + fun drawWorld( + pos: Vec3d, + text: String, + color: Color = Color.WHITE, + scale: Float = 0.025f, + centered: Boolean = true, + seeThrough: Boolean = false + ) { + val camera = mc.gameRenderer?.camera ?: return + if (!atlas.isUploaded) atlas.upload() + val textureView = atlas.textureView ?: return + val sampler = atlas.sampler ?: return + + val cameraPos = camera.pos + + // Build transformation matrix: translate, billboard, scale + val modelView = Matrix4f(com.lambda.graphics.RenderMain.modelViewMatrix) + modelView.translate( + (pos.x - cameraPos.x).toFloat(), + (pos.y - cameraPos.y).toFloat(), + (pos.z - cameraPos.z).toFloat() + ) + // Billboard - rotate to face camera + modelView.rotate(camera.rotation) + // Scale with negative Y to flip text vertically (MC convention) + modelView.scale(scale, -scale, scale) + + // Calculate text offset for centering + val textWidth = atlas.getStringWidth(text) + val xOffset = if (centered) -textWidth / 2f else 0f + + // Build and upload vertices + val (buffer, vertexCount) = buildAndUploadVertices(text, xOffset, 0f, color) ?: return + + try { + // Use TEXT_QUADS pipeline + val pipeline = if (seeThrough) LambdaRenderPipelines.TEXT_QUADS_THROUGH + else LambdaRenderPipelines.TEXT_QUADS + + // Create dynamic transform + val dynamicTransform = RenderSystem.getDynamicUniforms() + .write( + modelView, + Vector4f(1f, 1f, 1f, 1f), + Vector3f(0f, 0f, 0f), + Matrix4f() + ) + + // Create render pass and draw + RegionRenderer.createRenderPass("TextRenderer World", !seeThrough)?.use { pass -> + pass.setPipeline(pipeline) + RenderSystem.bindDefaultUniforms(pass) + + // Bind our texture using MC 1.21's proper API + pass.bindTexture("Sampler0", textureView, sampler) + + // Set transform + pass.setUniform("DynamicTransforms", dynamicTransform) + + // Set vertex buffer and draw + pass.setVertexBuffer(0, buffer) + val shapeIndexBuffer = RenderSystem.getSequentialBuffer(VertexFormat.DrawMode.QUADS) + val indexBuffer = shapeIndexBuffer.getIndexBuffer(vertexCount) + pass.setIndexBuffer(indexBuffer, shapeIndexBuffer.indexType) + pass.drawIndexed(0, 0, vertexCount, 1) + } + } finally { + buffer.close() + } + } + + /** + * Draw text in screen space (2D overlay). + * + * @param x Screen X position + * @param y Screen Y position + * @param text Text string to render + * @param color Text color + * @param scale Scale factor (1.0 = native font size) + */ + fun drawScreen( + x: Float, + y: Float, + text: String, + color: Color = Color.WHITE, + scale: Float = 1f + ) { + if (!atlas.isUploaded) atlas.upload() + val textureView = atlas.textureView ?: return + val sampler = atlas.sampler ?: return + + // Build transformation for screen space with orthographic projection + val window = mc.window + val ortho = Matrix4f().ortho( + 0f, window.scaledWidth.toFloat(), + window.scaledHeight.toFloat(), 0f, + -1000f, 1000f + ) + + val modelView = Matrix4f() + modelView.translate(x, y, 0f) + modelView.scale(scale, scale, 1f) + + val mvp = Matrix4f(ortho).mul(modelView) + + // Build and upload vertices + val (buffer, vertexCount) = buildAndUploadVertices(text, 0f, 0f, color) ?: return + + try { + val pipeline = LambdaRenderPipelines.TEXT_QUADS_THROUGH // No depth test for screen + + // Create dynamic transform + val dynamicTransform = RenderSystem.getDynamicUniforms() + .write( + mvp, + Vector4f(1f, 1f, 1f, 1f), + Vector3f(0f, 0f, 0f), + Matrix4f() + ) + + RegionRenderer.createRenderPass("TextRenderer Screen", false)?.use { pass -> + pass.setPipeline(pipeline) + // Note: not calling bindDefaultUniforms - we provide complete MVP in DynamicTransforms + pass.bindTexture("Sampler0", textureView, sampler) + pass.setUniform("DynamicTransforms", dynamicTransform) + + pass.setVertexBuffer(0, buffer) + val shapeIndexBuffer = RenderSystem.getSequentialBuffer(VertexFormat.DrawMode.QUADS) + val indexBuffer = shapeIndexBuffer.getIndexBuffer(vertexCount) + pass.setIndexBuffer(indexBuffer, shapeIndexBuffer.indexType) + pass.drawIndexed(0, 0, vertexCount, 1) + } + } finally { + buffer.close() + } + } + + /** + * Get the width of a text string in pixels at scale 1.0. + */ + fun getStringWidth(text: String): Float = atlas.getStringWidth(text) + + /** + * Build and upload vertices to GPU buffer. + * Returns the buffer and vertex count, or null if no vertices. + */ + private fun buildAndUploadVertices( + text: String, + startX: Float, + startY: Float, + color: Color + ): Pair? { + val penY = startY + atlas.ascent + var penX = startX + + // Count quads for allocation + var quadCount = 0 + for (char in text) { + if (atlas.getGlyph(char.code) != null || atlas.getGlyph(' '.code) != null) { + quadCount++ + } + } + if (quadCount == 0) return null + + val vertexCount = quadCount * 4 + val vertexSize = VertexFormats.POSITION_TEXTURE_COLOR.vertexSize + + var result: Pair? = null + BufferAllocator(vertexCount * vertexSize).use { allocator -> + val builder = BufferBuilder( + allocator, + VertexFormat.DrawMode.QUADS, + VertexFormats.POSITION_TEXTURE_COLOR + ) + + val r = color.red + val g = color.green + val b = color.blue + val a = color.alpha + + for (char in text) { + val glyph = atlas.getGlyph(char.code) ?: atlas.getGlyph(' '.code) ?: continue + + val x0 = penX + glyph.xOffset + val y0 = penY + glyph.yOffset + val x1 = x0 + (glyph.x1 - glyph.x0) + val y1 = y0 + (glyph.y1 - glyph.y0) + + // Bottom-left + builder.vertex(x0, y1, 0f).texture(glyph.u0, glyph.v1).color(r, g, b, a) + // Bottom-right + builder.vertex(x1, y1, 0f).texture(glyph.u1, glyph.v1).color(r, g, b, a) + // Top-right + builder.vertex(x1, y0, 0f).texture(glyph.u1, glyph.v0).color(r, g, b, a) + // Top-left + builder.vertex(x0, y0, 0f).texture(glyph.u0, glyph.v0).color(r, g, b, a) + + penX += glyph.xAdvance + } + + builder.endNullable()?.let { built -> + val gpuDevice = RenderSystem.getDevice() + val buffer = gpuDevice.createBuffer( + { "Lambda TextRenderer" }, + GpuBuffer.USAGE_VERTEX, + built.buffer + ) + result = buffer to built.drawParameters.indexCount() + built.close() + } + } + + return result + } + + override fun close() { + atlas.close() + } + + companion object { + private val loadedFonts = mutableMapOf() + + /** + * Load or get a cached font renderer. + */ + fun loadFont(fontPath: String, fontSize: Float = 16f): TextRenderer { + val key = "$fontPath:$fontSize" + return loadedFonts.getOrPut(key) { + TextRenderer(fontPath, fontSize) + } + } + + /** + * Close and clear all cached fonts. + */ + fun closeAll() { + loadedFonts.values.forEach { it.close() } + loadedFonts.clear() + } + } +} 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/module/modules/render/EntityESP.kt b/src/main/kotlin/com/lambda/module/modules/render/EntityESP.kt index 5a200c2f0..346284570 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/EntityESP.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/EntityESP.kt @@ -27,7 +27,6 @@ import com.lambda.module.tag.ModuleTag import com.lambda.util.NamedEnum import com.lambda.util.extension.tickDeltaF import com.lambda.util.math.setAlpha -import com.lambda.util.world.entitySearch import imgui.ImGui import net.minecraft.entity.Entity import net.minecraft.entity.ItemEntity @@ -51,6 +50,9 @@ object EntityESP : Module( tag = ModuleTag.RENDER ) { private val esp = ImmediateRegionESP("EntityESP") + + // Text renderer for testing +// private val testTextRenderer by lazy { TextRenderer("fonts/FiraSans-Regular.ttf", 96f) } private data class LabelData( val screenX: Float, @@ -62,7 +64,6 @@ object EntityESP : Module( 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) @@ -111,7 +112,56 @@ object EntityESP : Module( esp.tick() val tickDelta = mc.tickDeltaF - entitySearch(range) { shouldRender(it) }.forEach { entity -> + // Test SDF text rendering with glow and outline +// val eyePos = player.eyePos.add(player.rotationVector.multiply(2.0)) // 2 blocks in front +// SDFTextRenderer.drawWorld( +// text = "SDFTextRenderer World", +// pos = eyePos, +// fontSize = 0.5f, +// style = SDFTextRenderer.TextStyle( +// color = Color.WHITE, +// outline = SDFTextRenderer.TextOutline(Color.BLACK, 0.15f), +// glow = SDFTextRenderer.TextGlow(Color(0, 200, 255, 180), 0.2f), +// shadow = true +// ), +// centered = true, +// seeThrough = true +// ) +// +// SDFTextRenderer.drawScreen( +// text = "SDFTextRenderer Screen", +// x = 20f, +// y = 20f, +// fontSize = 24f, +// style = SDFTextRenderer.TextStyle( +// color = Color.WHITE, +// outline = SDFTextRenderer.TextOutline(Color.BLACK, 0.15f), +// glow = SDFTextRenderer.TextGlow(Color(0, 200, 255, 180), 0.2f), +// shadow = true +// ) +// ) +// +// // Test regular TextRenderer - World space (slightly below SDF text) +// val textWorldPos = player.eyePos.add(player.rotationVector.multiply(2.0)).add(0.0, -0.5, 0.0) +// testTextRenderer.drawWorld( +// pos = textWorldPos, +// text = "TextRenderer World", +// color = Color.YELLOW, +// scale = 0.025f, +// centered = true, +// seeThrough = true +// ) +// +// // Test regular TextRenderer - Screen space +// testTextRenderer.drawScreen( +// x = 20f, +// y = 100f, +// text = "TextRenderer Screen", +// color = Color.GREEN, +// scale = 1f +// ) + + world.entities.forEach { entity -> val color = getEntityColor(entity) val box = entity.boundingBox diff --git a/src/main/resources/assets/lambda/shaders/core/sdf_text.fsh b/src/main/resources/assets/lambda/shaders/core/sdf_text.fsh new file mode 100644 index 000000000..5550045d4 --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/sdf_text.fsh @@ -0,0 +1,50 @@ +#version 330 +#moj_import +#moj_import + +uniform sampler2D Sampler0; + +in vec2 texCoord0; +in vec4 vertexColor; +in float sphericalVertexDistance; +in float cylindricalVertexDistance; + +out vec4 fragColor; + +void main() { + // Sample the SDF texture - use ALPHA channel + vec4 texSample = texture(Sampler0, texCoord0); + float sdfValue = texSample.a; // SDF in alpha channel + + // IMPORTANT: Adjust smoothing based on distance field range + // For a typical SDF with 0.5 at the edge: + float smoothing = fwidth(sdfValue) * 0.5; // Reduced from 0.7 + + int layerType = int(vertexColor.a * 255.0 + 0.5); // +0.5 for proper rounding + + float alpha; + + if (layerType >= 200) { + // Main text + alpha = smoothstep(0.5 - smoothing, 0.5 + smoothing, sdfValue); + } else if (layerType >= 100) { + // Outline - use wider threshold + alpha = smoothstep(0.4 - smoothing, 0.45 + smoothing * 2.0, sdfValue); + } else if (layerType >= 50) { + // Glow - softer, wider + alpha = smoothstep(0.3, 0.45, sdfValue) * 0.6; + } else { + // Shadow + alpha = smoothstep(0.25, 0.4, sdfValue) * 0.5; + } + + // Apply vertex color and discard + vec4 result = vec4(vertexColor.rgb, alpha); + + if (result.a <= 0.001) discard; + + 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/sdf_text.vsh b/src/main/resources/assets/lambda/shaders/core/sdf_text.vsh new file mode 100644 index 000000000..4e365d599 --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/sdf_text.vsh @@ -0,0 +1,24 @@ +#version 330 + +#moj_import +#moj_import +#moj_import + +in vec3 Position; +in vec2 UV0; +in vec4 Color; + +out vec2 texCoord0; +out vec4 vertexColor; +out float sphericalVertexDistance; +out float cylindricalVertexDistance; + +void main() { + gl_Position = ProjMat * ModelViewMat * vec4(Position, 1.0); + + texCoord0 = UV0; + vertexColor = Color; + + sphericalVertexDistance = fog_spherical_distance(Position); + cylindricalVertexDistance = fog_cylindrical_distance(Position); +} \ No newline at end of file From 05e3c612a5749c259e0e6f04e13b10dbc6f1360d Mon Sep 17 00:00:00 2001 From: beanbag44 <107891830+beanbag44@users.noreply.github.com> Date: Thu, 15 Jan 2026 22:11:48 +0000 Subject: [PATCH 05/26] remove per render region origin --- .../com/lambda/graphics/esp/RegionESP.kt | 126 ------- .../com/lambda/graphics/esp/ShapeScope.kt | 9 +- .../lambda/graphics/mc/ChunkedRegionESP.kt | 137 ++++++-- .../lambda/graphics/mc/ImmediateRegionESP.kt | 110 +++++-- .../graphics/mc/LambdaRenderPipelines.kt | 3 + .../com/lambda/graphics/mc/RegionRenderer.kt | 8 +- .../lambda/graphics/mc/RegionShapeBuilder.kt | 28 +- .../com/lambda/graphics/mc/RenderRegion.kt | 60 ---- .../lambda/graphics/mc/TransientRegionESP.kt | 106 ++++-- .../lambda/graphics/mc/WorldTextRenderer.kt | 307 ------------------ .../com/lambda/graphics/text/FontHandler.kt | 127 ++++++++ .../com/lambda/graphics/text/SDFFontAtlas.kt | 10 +- .../lambda/graphics/text/SDFTextRenderer.kt | 120 +++++-- .../com/lambda/graphics/text/TextRenderer.kt | 13 +- .../construction/simulation/Simulation.kt | 5 +- .../simulation/context/BreakContext.kt | 2 +- .../simulation/context/InteractContext.kt | 2 +- .../simulation/result/results/BreakResult.kt | 8 +- .../result/results/GenericResult.kt | 6 +- .../result/results/InteractResult.kt | 4 +- .../simulation/result/results/PreSimResult.kt | 11 +- .../managers/breaking/BreakManager.kt | 2 +- .../lambda/module/modules/debug/BlockTest.kt | 2 +- .../lambda/module/modules/debug/RenderTest.kt | 4 +- .../module/modules/movement/BackTrack.kt | 3 +- .../lambda/module/modules/movement/Blink.kt | 3 +- .../lambda/module/modules/player/AirPlace.kt | 2 +- .../module/modules/player/PacketMine.kt | 4 +- .../module/modules/player/WorldEater.kt | 2 +- .../module/modules/render/BlockOutline.kt | 2 +- .../lambda/module/modules/render/EntityESP.kt | 128 ++++---- .../module/modules/render/StorageESP.kt | 10 +- .../assets/lambda/shaders/core/sdf_text.fsh | 49 ++- 33 files changed, 653 insertions(+), 760 deletions(-) delete mode 100644 src/main/kotlin/com/lambda/graphics/esp/RegionESP.kt delete mode 100644 src/main/kotlin/com/lambda/graphics/mc/RenderRegion.kt delete mode 100644 src/main/kotlin/com/lambda/graphics/mc/WorldTextRenderer.kt create mode 100644 src/main/kotlin/com/lambda/graphics/text/FontHandler.kt 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 fe0f3e63f..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.tickDeltaF -import com.mojang.blaze3d.systems.RenderSystem -import org.joml.Matrix4f -import org.joml.Vector3f -import org.joml.Vector4f -import java.util.concurrent.ConcurrentHashMap -import kotlin.math.floor - -/** - * Base class for region-based ESP systems. Provides unified rendering logic and region management. - */ -abstract class RegionESP(val name: String, var 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 tickDeltaF Progress within current tick (used for interpolation) - */ - fun render() { - 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", depthTest)?.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", depthTest)?.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 index 36a7a8a84..7ae67ba01 100644 --- a/src/main/kotlin/com/lambda/graphics/esp/ShapeScope.kt +++ b/src/main/kotlin/com/lambda/graphics/esp/ShapeScope.kt @@ -18,7 +18,6 @@ package com.lambda.graphics.esp import com.lambda.graphics.mc.RegionShapeBuilder -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 @@ -28,9 +27,13 @@ import net.minecraft.util.math.Vec3d import net.minecraft.util.shape.VoxelShape import java.awt.Color +/** + * Scope for building ESP shapes with camera-relative coordinates. + * @param cameraPos The camera position for computing relative coordinates + */ @EspDsl -class ShapeScope(val region: RenderRegion) { - internal val builder = RegionShapeBuilder(region) +class ShapeScope(cameraPos: Vec3d) { + internal val builder = RegionShapeBuilder(cameraPos) /** Start building a box. */ fun box(box: Box, block: BoxScope.() -> Unit) { diff --git a/src/main/kotlin/com/lambda/graphics/mc/ChunkedRegionESP.kt b/src/main/kotlin/com/lambda/graphics/mc/ChunkedRegionESP.kt index d367694a5..4ab75050e 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/ChunkedRegionESP.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/ChunkedRegionESP.kt @@ -17,29 +17,35 @@ package com.lambda.graphics.mc +import com.lambda.Lambda.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 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 /** - * Region-based chunked ESP system using MC 1.21.11's new render pipeline. + * Chunked ESP system using chunk-origin relative coordinates. * * This system: - * - Uses region-relative coordinates for precision-safe rendering - * - Maintains per-chunk geometry for efficient updates + * - 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 @@ -49,18 +55,23 @@ import java.util.concurrent.ConcurrentLinkedDeque class ChunkedRegionESP( owner: Module, name: String, - depthTest: Boolean = false, + private val depthTest: Boolean = false, private val update: ShapeScope.(World, FastVector) -> Unit -) : RegionESP(name, depthTest) { - private val chunkMap = ConcurrentHashMap() +) { + 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 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>() - private val rebuildQueue = ConcurrentLinkedDeque() + + private fun getChunkKey(chunkX: Int, chunkZ: Int): Long { + return (chunkX.toLong() and 0xFFFFFFFFL) or ((chunkZ.toLong() and 0xFFFFFFFFL) shl 32) + } /** Mark all tracked chunks for rebuild. */ fun rebuild() { @@ -76,37 +87,94 @@ class ChunkedRegionESP( runSafe { val chunksArray = world.chunkManager.chunks.chunks (0 until chunksArray.length()).forEach { i -> - chunksArray.get(i)?.regionChunk?.markDirty() + chunksArray.get(i)?.chunkData?.markDirty() } } } - override fun clear() { + fun clear() { chunkMap.values.forEach { it.close() } chunkMap.clear() rebuildQueue.clear() uploadQueue.clear() } + fun close() { + clear() + } + + /** + * Render all chunks with camera-relative translation. + */ + fun render() { + val cameraPos = mc.gameRenderer?.camera?.pos ?: return + + val activeChunks = chunkMap.values.filter { it.renderer.hasData() } + if (activeChunks.isEmpty()) return + + val modelViewMatrix = com.lambda.graphics.RenderMain.modelViewMatrix + + // Pre-compute all transforms BEFORE starting render passes + val chunkTransforms = 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).translate(offsetX, offsetY, offsetZ) + val dynamicTransform = RenderSystem.getDynamicUniforms() + .write(modelView, Vector4f(1f, 1f, 1f, 1f), Vector3f(0f, 0f, 0f), Matrix4f()) + + chunkData to dynamicTransform + } + + // Render Faces + RegionRenderer.createRenderPass("ChunkedESP Faces", depthTest)?.use { pass -> + val pipeline = + if (depthTest) LambdaRenderPipelines.ESP_QUADS + else LambdaRenderPipelines.ESP_QUADS_THROUGH + pass.setPipeline(pipeline) + RenderSystem.bindDefaultUniforms(pass) + + chunkTransforms.forEach { (chunkData, transform) -> + pass.setUniform("DynamicTransforms", transform) + chunkData.renderer.renderFaces(pass) + } + } + + // Render Edges + RegionRenderer.createRenderPass("ChunkedESP Edges", depthTest)?.use { pass -> + val pipeline = + if (depthTest) LambdaRenderPipelines.ESP_LINES + else LambdaRenderPipelines.ESP_LINES_THROUGH + pass.setPipeline(pipeline) + RenderSystem.bindDefaultUniforms(pass) + + chunkTransforms.forEach { (chunkData, transform) -> + pass.setUniform("DynamicTransforms", transform) + chunkData.renderer.renderEdges(pass) + } + } + } + init { owner.listen { event -> val pos = event.pos - world.getWorldChunk(pos)?.regionChunk?.markDirty() + world.getWorldChunk(pos)?.chunkData?.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() + 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.regionChunk.markDirty() } + owner.listen { event -> event.chunk.chunkData.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() + chunkMap.remove(it.chunk.chunkKey)?.close() } owner.listenConcurrently { @@ -123,10 +191,15 @@ class ChunkedRegionESP( 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) + /** 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() private var isDirty = false @@ -137,9 +210,16 @@ class ChunkedRegionESP( } } + /** + * Rebuild geometry relative to chunk origin. + * Coordinates are stored as (worldPos - chunkOrigin).toFloat() + */ fun rebuild() { if (!isDirty) return - val scope = ShapeScope(region) + + // Use chunk origin as the "camera" position for relative coords + val chunkOriginVec = Vec3d(originX, originY, originZ) + val scope = ShapeScope(chunkOriginVec) for (x in chunk.pos.startX..chunk.pos.endX) { for (z in chunk.pos.startZ..chunk.pos.endZ) { @@ -150,14 +230,13 @@ class ChunkedRegionESP( } uploadQueue.add { - val renderer = renderers.getOrPut(key) { RegionRenderer(region) } renderer.upload(scope.builder.collector) isDirty = false } } fun close() { - renderers.remove(key)?.close() + renderer.close() } } } diff --git a/src/main/kotlin/com/lambda/graphics/mc/ImmediateRegionESP.kt b/src/main/kotlin/com/lambda/graphics/mc/ImmediateRegionESP.kt index 52bee6874..cd46ec3d8 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/ImmediateRegionESP.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/ImmediateRegionESP.kt @@ -17,55 +17,99 @@ package com.lambda.graphics.mc -import com.lambda.graphics.esp.RegionESP +import com.lambda.Lambda.mc import com.lambda.graphics.esp.ShapeScope -import java.util.concurrent.ConcurrentHashMap -import kotlin.math.floor +import com.mojang.blaze3d.systems.RenderSystem +import net.minecraft.util.math.Vec3d +import org.joml.Matrix4f +import org.joml.Vector3f +import org.joml.Vector4f /** - * Interpolated region-based ESP system for smooth entity rendering. + * Interpolated ESP system for smooth entity rendering. * - * This system rebuilds and uploads vertices every frame. Callers are responsible for providing - * interpolated positions (e.g., using entity.prevX/x with tickDelta). The tick() method swaps - * builders to allow smooth transitions between frames. + * 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 ImmediateRegionESP(name: String, depthTest: Boolean = false) : RegionESP(name, depthTest) { - // Current frame builders (being populated this tick) - private val currBuilders = ConcurrentHashMap() +class ImmediateRegionESP(val name: String, var depthTest: Boolean = false) { + private val renderer = RegionRenderer() - 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)) - } - scope.apply(block) + // Current frame builder (being populated this frame) + private var currScope: ShapeScope? = null + + /** + * Get the current camera position for building camera-relative shapes. + * Returns null if camera is not available. + */ + private fun getCameraPos(): Vec3d? = mc.gameRenderer?.camera?.pos + + /** Get or create a ShapeScope for drawing with camera-relative coordinates. */ + fun shapes(block: ShapeScope.() -> Unit) { + val s = currScope ?: ShapeScope(getCameraPos() ?: return).also { currScope = it } + s.apply(block) } - override fun clear() { - currBuilders.clear() + /** Clear all geometry data. */ + fun clear() { + currScope = null } + /** Called each tick to reset for next frame. */ fun tick() { - currBuilders.clear() + currScope = null + } + + /** Upload collected geometry to GPU. Must be called on main thread. */ + fun upload() { + currScope?.let { s -> + renderer.upload(s.builder.collector) + } ?: renderer.clearData() } - override fun upload() { - val activeKeys = currBuilders.keys.toSet() + /** Close and release all GPU resources. */ + fun close() { + renderer.close() + clear() + } + + /** + * Render all geometry. Since coordinates are already camera-relative, + * we just use the base modelView matrix without additional translation. + */ + fun render() { + if (!renderer.hasData()) return + + val modelViewMatrix = com.lambda.graphics.RenderMain.modelViewMatrix + + val dynamicTransform = RenderSystem.getDynamicUniforms() + .write( + modelViewMatrix, + Vector4f(1f, 1f, 1f, 1f), + Vector3f(0f, 0f, 0f), + Matrix4f() + ) - currBuilders.forEach { (key, scope) -> - val renderer = renderers.getOrPut(key) { RegionRenderer(scope.region) } - renderer.upload(scope.builder.collector) + // Render Faces + RegionRenderer.createRenderPass("$name Faces", depthTest)?.use { pass -> + val pipeline = + if (depthTest) LambdaRenderPipelines.ESP_QUADS + else LambdaRenderPipelines.ESP_QUADS_THROUGH + pass.setPipeline(pipeline) + RenderSystem.bindDefaultUniforms(pass) + pass.setUniform("DynamicTransforms", dynamicTransform) + renderer.renderFaces(pass) } - renderers.forEach { (key, renderer) -> - if (key !in activeKeys) { - renderer.clearData() - } + // Render Edges + RegionRenderer.createRenderPass("$name Edges", depthTest)?.use { pass -> + val pipeline = + if (depthTest) LambdaRenderPipelines.ESP_LINES + else LambdaRenderPipelines.ESP_LINES_THROUGH + pass.setPipeline(pipeline) + RenderSystem.bindDefaultUniforms(pass) + pass.setUniform("DynamicTransforms", dynamicTransform) + renderer.renderEdges(pass) } } } diff --git a/src/main/kotlin/com/lambda/graphics/mc/LambdaRenderPipelines.kt b/src/main/kotlin/com/lambda/graphics/mc/LambdaRenderPipelines.kt index c073d71d2..1238e590a 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 @@ -169,6 +170,7 @@ object LambdaRenderPipelines : Loadable { .withVertexShader(Identifier.of("lambda", "core/sdf_text")) .withFragmentShader(Identifier.of("lambda", "core/sdf_text")) .withSampler("Sampler0") + .withUniform("SDFParams", UniformType.UNIFORM_BUFFER) .withBlend(BlendFunction.TRANSLUCENT) .withDepthWrite(false) .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST) @@ -188,6 +190,7 @@ object LambdaRenderPipelines : Loadable { .withVertexShader(Identifier.of("lambda", "core/sdf_text")) .withFragmentShader(Identifier.of("lambda", "core/sdf_text")) .withSampler("Sampler0") + .withUniform("SDFParams", UniformType.UNIFORM_BUFFER) .withBlend(BlendFunction.TRANSLUCENT) .withDepthWrite(false) .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) diff --git a/src/main/kotlin/com/lambda/graphics/mc/RegionRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/RegionRenderer.kt index f3742912b..108d32cc1 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/RegionRenderer.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/RegionRenderer.kt @@ -25,14 +25,12 @@ 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) { +class RegionRenderer { // Dedicated GPU buffers for faces and edges private var faceVertexBuffer: GpuBuffer? = null diff --git a/src/main/kotlin/com/lambda/graphics/mc/RegionShapeBuilder.kt b/src/main/kotlin/com/lambda/graphics/mc/RegionShapeBuilder.kt index 9b2b844e4..4396348ef 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/RegionShapeBuilder.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/RegionShapeBuilder.kt @@ -37,20 +37,26 @@ import java.awt.Color import kotlin.math.min /** - * Shape builder for region-based rendering. All coordinates are automatically converted to - * region-relative positions. + * Shape builder for camera-relative rendering. All coordinates are computed + * relative to the camera position in double precision, then converted to float. + * This prevents floating-point jitter at large world coordinates. * - * 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) + * @param cameraPos The camera's world position for computing relative coordinates */ -class RegionShapeBuilder(val region: RenderRegion) { +class RegionShapeBuilder(private val cameraPos: Vec3d) { val collector = RegionVertexCollector() val lineWidth: Float get() = StyleEditor.outlineWidth.toFloat() + /** Convert world coordinates to camera-relative. Computed in double precision. */ + private fun toRelative(x: Double, y: Double, z: Double) = + Triple( + (x - cameraPos.x).toFloat(), + (y - cameraPos.y).toFloat(), + (z - cameraPos.z).toFloat() + ) + fun box( entity: BlockEntity, filled: Color, @@ -67,14 +73,6 @@ class RegionShapeBuilder(val region: RenderRegion) { 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, 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 index bc1cd812c..fbaa7af95 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/TransientRegionESP.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/TransientRegionESP.kt @@ -17,50 +17,98 @@ package com.lambda.graphics.mc -import com.lambda.graphics.esp.RegionESP +import com.lambda.Lambda.mc import com.lambda.graphics.esp.ShapeScope -import java.util.concurrent.ConcurrentHashMap -import kotlin.math.floor +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. Uses region-based rendering for precision. + * 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 TransientRegionESP(name: String, depthTest: Boolean = false) : RegionESP(name, depthTest) { - private val builders = ConcurrentHashMap() +class TransientRegionESP(val name: String, var depthTest: Boolean = false) { + private val renderer = RegionRenderer() + private var scope: ShapeScope? = null + + // Camera position captured at tick time (when shapes are built) + private var tickCameraPos: Vec3d? = null - /** 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) + /** Get the current shape scope for drawing. Geometry stored relative to tick camera. */ + fun shapes(block: ShapeScope.() -> Unit) { + val cameraPos = mc.gameRenderer?.camera?.pos ?: return + if (scope == null) { + tickCameraPos = cameraPos + scope = ShapeScope(cameraPos) + } + scope?.apply(block) } /** Clear all current builders. Call this at the end of every tick. */ - override fun clear() { - builders.clear() + fun clear() { + scope = null + tickCameraPos = null } /** Upload collected geometry to GPU. Must be called on main thread. */ - override fun upload() { - val activeKeys = builders.keys().asSequence().toSet() + fun upload() { + scope?.let { s -> + renderer.upload(s.builder.collector) + } ?: renderer.clearData() + } + + /** Close and release all GPU resources. */ + fun close() { + renderer.close() + clear() + } + + /** + * Render with smooth camera interpolation. + * Computes delta between tick-camera and current-camera in double precision. + */ + fun render() { + val currentCameraPos = mc.gameRenderer?.camera?.pos ?: return + val tickCamera = tickCameraPos ?: return + if (!renderer.hasData()) return + + val modelViewMatrix = com.lambda.graphics.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).translate(deltaX, deltaY, deltaZ) + val dynamicTransform = RenderSystem.getDynamicUniforms() + .write(modelView, Vector4f(1f, 1f, 1f, 1f), Vector3f(0f, 0f, 0f), Matrix4f()) - builders.forEach { (key, scope) -> - val renderer = renderers.getOrPut(key) { RegionRenderer(scope.region) } - renderer.upload(scope.builder.collector) + // Render Faces + RegionRenderer.createRenderPass("$name Faces", depthTest)?.use { pass -> + val pipeline = + if (depthTest) LambdaRenderPipelines.ESP_QUADS + else LambdaRenderPipelines.ESP_QUADS_THROUGH + pass.setPipeline(pipeline) + RenderSystem.bindDefaultUniforms(pass) + pass.setUniform("DynamicTransforms", dynamicTransform) + renderer.renderFaces(pass) } - renderers.forEach { (key, renderer) -> - if (key !in activeKeys) { - renderer.clearData() - } + // Render Edges + RegionRenderer.createRenderPass("$name Edges", depthTest)?.use { pass -> + val pipeline = + if (depthTest) LambdaRenderPipelines.ESP_LINES + else LambdaRenderPipelines.ESP_LINES_THROUGH + pass.setPipeline(pipeline) + RenderSystem.bindDefaultUniforms(pass) + pass.setUniform("DynamicTransforms", dynamicTransform) + renderer.renderEdges(pass) } } } 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/text/FontHandler.kt b/src/main/kotlin/com/lambda/graphics/text/FontHandler.kt new file mode 100644 index 000000000..284c56b7f --- /dev/null +++ b/src/main/kotlin/com/lambda/graphics/text/FontHandler.kt @@ -0,0 +1,127 @@ +/* + * 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 sdfFonts = ConcurrentHashMap() + private val fonts = ConcurrentHashMap() + private var defaultSDFFont: SDFFontAtlas? = null + private var defaultFont: FontAtlas? = null + + /** + * Load an SDF font from resources. + * + * @param path Resource path to TTF/OTF file (e.g., "fonts/FiraSans-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 loadSDFFont(path: String, size: Float = 128f): SDFFontAtlas? { + val key = "$path@$size" + return sdfFonts.getOrPut(key) { + try { + SDFFontAtlas(path, size) + } catch (e: Exception) { + println("[FontHandler] Failed to load font: $path - ${e.message}") + return null + } + } + } + + fun loadFont(path: String, size: Float = 128f): FontAtlas? { + val key = "$path@$size" + return fonts.getOrPut(key) { + try { + FontAtlas(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 getDefaultSDFFont(size: Float = 128f): SDFFontAtlas { + defaultSDFFont?.let { return it } + + val key = "fonts/FiraSans-Regular.ttf@$size" + val font = sdfFonts[key] ?: run { + val newFont = SDFFontAtlas("fonts/FiraSans-Regular.ttf", size) + sdfFonts[key] = newFont + newFont + } + defaultSDFFont = font + return font + } + + fun getDefaultFont(size: Float = 128f): FontAtlas { + defaultFont?.let { return it } + + val key = "fonts/FiraSans-Regular.ttf@$size" + val font = fonts[key] ?: run { + val newFont = FontAtlas("fonts/FiraSans-Regular.ttf", size) + fonts[key] = newFont + newFont + } + defaultFont = font + return font + } + + /** + * Check if a font is already loaded. + */ + fun isSDFFontLoaded(path: String, size: Float = 128f) = sdfFonts.containsKey("$path@$size") + + fun isFontLoaded(path: String, size: Float = 128f) = fonts.containsKey("path@$size") + + /** + * Get all loaded font paths. + */ + fun getLoadedSDFFonts(): Set = sdfFonts.keys.toSet() + + 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() { + sdfFonts.values.forEach { it.close() } + fonts.values.forEach { it.close() } + sdfFonts.clear() + fonts.clear() + defaultSDFFont = null + defaultFont = null + } +} diff --git a/src/main/kotlin/com/lambda/graphics/text/SDFFontAtlas.kt b/src/main/kotlin/com/lambda/graphics/text/SDFFontAtlas.kt index 97a850def..a705c3a6a 100644 --- a/src/main/kotlin/com/lambda/graphics/text/SDFFontAtlas.kt +++ b/src/main/kotlin/com/lambda/graphics/text/SDFFontAtlas.kt @@ -27,6 +27,9 @@ 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 @@ -36,14 +39,9 @@ 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.stb.STBTruetype.STBTT_vcurve -import org.lwjgl.stb.STBTruetype.STBTT_vline -import org.lwjgl.stb.STBTruetype.STBTT_vmove import org.lwjgl.system.MemoryStack import org.lwjgl.system.MemoryUtil import java.nio.ByteBuffer -import kotlin.math.abs -import kotlin.math.min import kotlin.math.sqrt /** @@ -61,7 +59,7 @@ import kotlin.math.sqrt */ class SDFFontAtlas( fontPath: String, - val baseSize: Float = 128f, + val baseSize: Float = 256f, val sdfSpread: Int = 16, val atlasSize: Int = 4096 ) : AutoCloseable { diff --git a/src/main/kotlin/com/lambda/graphics/text/SDFTextRenderer.kt b/src/main/kotlin/com/lambda/graphics/text/SDFTextRenderer.kt index f36b8b71a..2c82c80e5 100644 --- a/src/main/kotlin/com/lambda/graphics/text/SDFTextRenderer.kt +++ b/src/main/kotlin/com/lambda/graphics/text/SDFTextRenderer.kt @@ -70,23 +70,34 @@ object SDFTextRenderer { /** Outline effect configuration */ data class TextOutline( val color: Color = Color.BLACK, - val width: Float = 0.1f // 0.0 - 0.5 in SDF units + val width: Float = 0.1f // 0.0 - 0.3 in SDF units (distance from edge) ) /** Glow effect configuration */ data class TextGlow( val color: Color = Color(0, 200, 255, 180), - val radius: Float = 0.15f // Glow spread in SDF units + val radius: Float = 0.2f // Glow spread in SDF units ) + /** Shadow effect configuration */ + data class TextShadow( + val color: Color = Color(0, 0, 0, 180), + val offset: Float = 0.05f, // Distance in text units + val angle: Float = 135f, // Angle in degrees: 0=right, 90=down, 180=left, 270=up (default: bottom-right) + val softness: Float = 0.15f // Shadow blur in SDF units (for documentation, not currently used) + ) { + /** 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() + } + /** Text style configuration */ data class TextStyle( val color: Color = Color.WHITE, val outline: TextOutline? = null, val glow: TextGlow? = null, - val shadow: Boolean = true, - val shadowColor: Color = Color(0, 0, 0, 180), - val shadowOffset: Float = 0.05f + val shadow: TextShadow? = TextShadow() // Default shadow enabled ) /** @@ -96,7 +107,7 @@ object SDFTextRenderer { * @param size Font size in pixels * @return The loaded FontAtlas, or null if loading failed */ - fun loadFont(path: String, size: Float = 256f): SDFFontAtlas? { + fun loadFont(path: String, size: Float = 128f): SDFFontAtlas? { val key = "$path@$size" return fonts.getOrPut(key) { try { @@ -120,9 +131,9 @@ object SDFTextRenderer { defaultFont?.let { return it } // Try to load without catching, so the actual exception is visible - val key = "fonts/MinecraftDefault-Regular.ttf@$size" + val key = "fonts/FiraSans-Regular.ttf@$size" val font = fonts[key] ?: run { - val newFont = SDFFontAtlas("fonts/MinecraftDefault-Regular.ttf", size) + val newFont = SDFFontAtlas("fonts/FiraSans-Regular.ttf", size) fonts[key] = newFont newFont } @@ -169,11 +180,11 @@ object SDFTextRenderer { val startX = -textWidth / 2f // Draw shadow first (offset, alpha < 50 signals shadow layer) - if (style.shadow) { - val shadowColor = Color(style.shadowColor.red, style.shadowColor.green, style.shadowColor.blue, 25) + if (style.shadow != null) { + val shadowColor = Color(style.shadow.color.red, style.shadow.color.green, style.shadow.color.blue, 25) renderTextLayer( - atlas, text, startX + style.shadowOffset, style.shadowOffset, - shadowColor, modelMatrix, seeThrough + atlas, text, startX + style.shadow.offsetX, style.shadow.offsetY, + shadowColor, modelMatrix, seeThrough, style ) } @@ -182,7 +193,7 @@ object SDFTextRenderer { val glowColor = Color(style.glow.color.red, style.glow.color.green, style.glow.color.blue, 75) renderTextLayer( atlas, text, startX, 0f, - glowColor, modelMatrix, seeThrough + glowColor, modelMatrix, seeThrough, style ) } @@ -191,7 +202,7 @@ object SDFTextRenderer { val outlineColor = Color(style.outline.color.red, style.outline.color.green, style.outline.color.blue, 150) renderTextLayer( atlas, text, startX, 0f, - outlineColor, modelMatrix, seeThrough + outlineColor, modelMatrix, seeThrough, style ) } @@ -199,7 +210,7 @@ object SDFTextRenderer { val mainColor = Color(style.color.red, style.color.green, style.color.blue, 255) renderTextLayer( atlas, text, startX, 0f, - mainColor, modelMatrix, seeThrough + mainColor, modelMatrix, seeThrough, style ) } @@ -211,42 +222,43 @@ object SDFTextRenderer { text: String, x: Float, y: Float, - fontSize: Float = 16f, + fontSize: Float = 24f, style: TextStyle = TextStyle() ) { val atlas = font ?: getDefaultFont() val scale = fontSize / atlas.baseSize // Create orthographic model matrix + // Note: vertices are built with Y-up convention, so we negate Y scale for screen (Y-down) val modelMatrix = Matrix4f() .translate(x, y, 0f) - .scale(scale, scale, 1f) + .scale(scale, -scale, 1f) // Negative Y to flip for screen coordinates // Use screen-space rendering - if (style.shadow) { + if (style.shadow != null) { renderTextLayerScreen( - atlas, text, style.shadowOffset * fontSize, style.shadowOffset * fontSize, - style.shadowColor, modelMatrix + atlas, text, style.shadow.offsetX * fontSize, style.shadow.offsetY * fontSize, + style.shadow.color, modelMatrix, style ) } - if (style.outline != null) { + if (style.glow != null) { renderTextLayerScreen( atlas, text, 0f, 0f, - style.outline.color, modelMatrix + style.glow.color, modelMatrix, style ) } - if (style.glow != null) { + if (style.outline != null) { renderTextLayerScreen( atlas, text, 0f, 0f, - style.glow.color, modelMatrix + style.outline.color, modelMatrix, style ) } renderTextLayerScreen( atlas, text, 0f, 0f, - style.color, modelMatrix + style.color, modelMatrix, style ) } @@ -272,7 +284,8 @@ object SDFTextRenderer { startY: Float, color: Color, modelMatrix: Matrix4f, - seeThrough: Boolean + seeThrough: Boolean, + style: TextStyle ) { if (!atlas.isUploaded) atlas.upload() val textureView = atlas.textureView ?: return @@ -286,6 +299,12 @@ object SDFTextRenderer { // Upload to GPU buffer val gpuBuffer = uploadTextVertices(vertices) ?: return + // Create SDF params uniform buffer + val sdfParams = createSDFParamsBuffer(style) ?: run { + gpuBuffer.close() + return + } + // Use SDF_TEXT pipeline for proper smoothstep anti-aliasing val pipeline = if (seeThrough) LambdaRenderPipelines.SDF_TEXT_THROUGH else LambdaRenderPipelines.SDF_TEXT @@ -299,6 +318,7 @@ object SDFTextRenderer { pass.setPipeline(pipeline) RenderSystem.bindDefaultUniforms(pass) pass.setUniform("DynamicTransforms", dynamicTransform) + pass.setUniform("SDFParams", sdfParams) // Bind texture using MC 1.21's proper API pass.bindTexture("Sampler0", textureView, sampler) @@ -314,6 +334,7 @@ object SDFTextRenderer { } gpuBuffer.close() + sdfParams.close() } private fun renderTextLayerScreen( @@ -322,7 +343,8 @@ object SDFTextRenderer { offsetX: Float, offsetY: Float, color: Color, - modelMatrix: Matrix4f + modelMatrix: Matrix4f, + style: TextStyle ) { if (!atlas.isUploaded) atlas.upload() val textureView = atlas.textureView ?: return @@ -334,22 +356,31 @@ object SDFTextRenderer { val gpuBuffer = uploadTextVertices(vertices) ?: return + // Create SDF params uniform buffer + val sdfParams = createSDFParamsBuffer(style) ?: run { + gpuBuffer.close() + return + } + val window = mc.window + // Ortho projection: left=0, right=scaledWidth, top=0, bottom=scaledHeight (Y-down for screen) val ortho = Matrix4f().ortho( 0f, window.scaledWidth.toFloat(), window.scaledHeight.toFloat(), 0f, -1000f, 1000f ) - // Calculate MVP and dynamic uniforms BEFORE opening render pass + // Apply model matrix to ortho to get final MVP + // The model matrix has the screen position and scaling val mvp = Matrix4f(ortho).mul(modelMatrix) val dynamicTransform = RenderSystem.getDynamicUniforms() .write(mvp, Vector4f(1f, 1f, 1f, 1f), Vector3f(0f, 0f, 0f), Matrix4f()) RegionRenderer.createRenderPass("SDF Text Screen", useDepth = false)?.use { pass -> pass.setPipeline(LambdaRenderPipelines.SDF_TEXT_THROUGH) - // Note: not calling bindDefaultUniforms - we provide complete MVP in DynamicTransforms + RenderSystem.bindDefaultUniforms(pass) pass.setUniform("DynamicTransforms", dynamicTransform) + pass.setUniform("SDFParams", sdfParams) // Bind texture using MC 1.21's proper API pass.bindTexture("Sampler0", textureView, sampler) @@ -364,6 +395,7 @@ object SDFTextRenderer { } gpuBuffer.close() + sdfParams.close() } private data class TextVertex( @@ -451,6 +483,36 @@ object SDFTextRenderer { return atlas.lineHeight * fontSize / atlas.baseSize } + /** + * Create a GpuBuffer containing the SDF effect parameters for the shader. + * Layout matches std140 uniform block SDFParams in sdf_text.fsh: + * float SDFThreshold, OutlineWidth, GlowRadius, ShadowSoftness (4 floats = 16 bytes) + */ + private fun createSDFParamsBuffer(style: TextStyle): GpuBuffer? { + val device = RenderSystem.getDevice() + + // std140 layout: 4 floats (16 bytes total) + val bufferSize = 16 + + // Use LWJGL MemoryUtil for direct ByteBuffer allocation + val buffer = org.lwjgl.system.MemoryUtil.memAlloc(bufferSize) + return try { + // Write the 4 floats + buffer.putFloat(0.5f) // SDFThreshold - main text edge + buffer.putFloat(style.outline?.width ?: 0.1f) // OutlineWidth + buffer.putFloat(style.glow?.radius ?: 0.2f) // GlowRadius + buffer.putFloat(style.shadow?.softness ?: 0.15f) // ShadowSoftness + + buffer.flip() + + device.createBuffer({ "SDFParams" }, GpuBuffer.USAGE_UNIFORM, buffer) + } catch (e: Exception) { + null + } finally { + org.lwjgl.system.MemoryUtil.memFree(buffer) + } + } + /** Clean up all loaded fonts. */ fun cleanup() { fonts.values.forEach { it.close() } diff --git a/src/main/kotlin/com/lambda/graphics/text/TextRenderer.kt b/src/main/kotlin/com/lambda/graphics/text/TextRenderer.kt index d6682b21b..2a085cec1 100644 --- a/src/main/kotlin/com/lambda/graphics/text/TextRenderer.kt +++ b/src/main/kotlin/com/lambda/graphics/text/TextRenderer.kt @@ -40,7 +40,7 @@ import java.awt.Color */ class TextRenderer( fontPath: String, - fontSize: Float = 256f, + fontSize: Float = 128f, atlasSize: Int = 512 ) : AutoCloseable { @@ -139,23 +139,26 @@ class TextRenderer( /** * Draw text in screen space (2D overlay). * - * @param x Screen X position - * @param y Screen Y position + * @param x Screen X position in pixels + * @param y Screen Y position in pixels * @param text Text string to render * @param color Text color - * @param scale Scale factor (1.0 = native font size) + * @param fontSize Target text height in pixels (default 16) */ fun drawScreen( x: Float, y: Float, text: String, color: Color = Color.WHITE, - scale: Float = 1f + fontSize: Float = 24f ) { if (!atlas.isUploaded) atlas.upload() val textureView = atlas.textureView ?: return val sampler = atlas.sampler ?: return + // Convert fontSize to scale factor based on atlas font size + val scale = fontSize / atlas.fontSize + // Build transformation for screen space with orthographic projection val window = mc.window val ortho = Matrix4f().ortho( 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..7d5ceeb90 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,11 @@ 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.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 @@ -65,7 +64,7 @@ data class Simulation( 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()) { + esp.shapes { box(Vec3d.ofBottomCenter(pos).playerBox(), 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..6dce001a9 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 @@ -60,7 +60,7 @@ 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()) { + esp.shapes { box(blockPos, 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..d0999f0de 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 @@ -47,7 +47,7 @@ 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) { + esp.shapes { val box = with(hitResult.pos) { Box( x - 0.05, y - 0.05, z - 0.05, 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..31437a099 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 @@ -73,7 +73,7 @@ sealed class BreakResult : BuildResult() { private val color = Color(46, 0, 0, 30) override fun render(esp: TransientRegionESP) { - esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) { + esp.shapes { box(pos, color, color, side.mask) } } @@ -123,7 +123,7 @@ sealed class BreakResult : BuildResult() { private val color = Color(114, 27, 255, 100) override fun render(esp: TransientRegionESP) { - esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) { + esp.shapes { box(pos, color, color) } } @@ -141,7 +141,7 @@ sealed class BreakResult : BuildResult() { private val color = Color(50, 12, 112, 100) override fun render(esp: TransientRegionESP) { - esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) { + esp.shapes { val center = pos.toCenterPos() val box = Box( center.x - 0.1, center.y - 0.1, center.z - 0.1, @@ -165,7 +165,7 @@ 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()) { + esp.shapes { box(pos, color, 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..6d84b479b 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 @@ -54,7 +54,7 @@ sealed class GenericResult : BuildResult() { private val color = Color(46, 0, 0, 80) override fun render(esp: TransientRegionESP) { - esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) { + esp.shapes { val box = with(pos) { Box( x - 0.05, y - 0.05, z - 0.05, @@ -103,7 +103,7 @@ sealed class GenericResult : BuildResult() { } override fun render(esp: TransientRegionESP) { - esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) { + esp.shapes { val center = pos.toCenterPos() val box = Box( center.x - 0.1, center.y - 0.1, center.z - 0.1, @@ -136,7 +136,7 @@ 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()) { + esp.shapes { val center = pos.toCenterPos() val box = Box( center.x - 0.1, center.y - 0.1, center.z - 0.1, 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..eb20cff89 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 @@ -83,7 +83,7 @@ sealed class InteractResult : BuildResult() { private val color = Color(252, 3, 3, 100) override fun render(esp: TransientRegionESP) { - esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) { + esp.shapes { val box = with(simulated.hitPos) { Box( x - 0.05, y - 0.05, z - 0.05, @@ -122,7 +122,7 @@ sealed class InteractResult : BuildResult() { private val color = Color(252, 3, 3, 100) override fun render(esp: TransientRegionESP) { - esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) { + esp.shapes { val box = with(hitPos) { Box( x - 0.05, y - 0.05, z - 0.05, 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..c09e9ead0 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,7 +18,6 @@ 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.interaction.construction.simulation.result.BuildResult import com.lambda.interaction.construction.simulation.result.ComparableResult @@ -57,7 +56,7 @@ 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()) { + esp.shapes { box(pos, color, color) } } @@ -81,7 +80,7 @@ sealed class PreSimResult : BuildResult() { private val color = Color(255, 0, 0, 100) override fun render(esp: TransientRegionESP) { - esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) { + esp.shapes { box(pos, color, color) } } @@ -101,7 +100,7 @@ sealed class PreSimResult : BuildResult() { private val color = Color(255, 0, 0, 100) override fun render(esp: TransientRegionESP) { - esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) { + esp.shapes { box(pos, color, color) } } @@ -119,7 +118,7 @@ sealed class PreSimResult : BuildResult() { private val color = Color(3, 148, 252, 100) override fun render(esp: TransientRegionESP) { - esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) { + esp.shapes { box(pos, color, color) } } @@ -139,7 +138,7 @@ sealed class PreSimResult : BuildResult() { private val color = Color(11, 11, 11, 100) override fun render(esp: TransientRegionESP) { - esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) { + esp.shapes { box(pos, color, color) } } 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 c0b301b7c..abdba1303 100644 --- a/src/main/kotlin/com/lambda/interaction/managers/breaking/BreakManager.kt +++ b/src/main/kotlin/com/lambda/interaction/managers/breaking/BreakManager.kt @@ -263,7 +263,7 @@ object BreakManager : Manager( else config.staticOutlineColor val pos = info.context.blockPos - esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) { + esp.shapes { info.context.cachedState.getOutlineShape(world, pos).boundingBoxes.map { it.offset(pos) }.forEach boxes@{ box -> 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..9de1fe39c 100644 --- a/src/main/kotlin/com/lambda/module/modules/debug/BlockTest.kt +++ b/src/main/kotlin/com/lambda/module/modules/debug/BlockTest.kt @@ -51,7 +51,7 @@ object BlockTest : Module( blockSearch(range, step = step) { _, state -> state.isOf(Blocks.DIAMOND_BLOCK) }.forEach { (pos, state) -> - esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) { + esp.shapes { state.getOutlineShape(world, pos).boundingBoxes.forEach { box -> box(box.offset(pos), 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..939c8dafb 100644 --- a/src/main/kotlin/com/lambda/module/modules/debug/RenderTest.kt +++ b/src/main/kotlin/com/lambda/module/modules/debug/RenderTest.kt @@ -48,14 +48,14 @@ object RenderTest : Module( onDynamicRender { esp -> entitySearch(8.0) .forEach { entity -> - esp.shapes(entity.x, entity.y, entity.z) { + esp.shapes { box(entity.dynamicBox, filledColor, outlineColor, DirectionMask.ALL, DirectionMask.OutlineMode.And) } } } onStaticRender { esp -> - esp.shapes(player.x, player.y, player.z) { + esp.shapes { box(Box.of(player.pos, 0.3, 0.3, 0.3), filledColor, outlineColor) } } 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..4841d22d9 100644 --- a/src/main/kotlin/com/lambda/module/modules/movement/BackTrack.kt +++ b/src/main/kotlin/com/lambda/module/modules/movement/BackTrack.kt @@ -23,7 +23,6 @@ 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.gui.components.ClickGuiLayout import com.lambda.module.Module @@ -119,7 +118,7 @@ object BackTrack : Module( val p = target.hurtTime / 10.0 val c = lerp(p, c1, c2) - esp.shapes(target.pos.x, target.pos.y, target.pos.z) { + esp.shapes { box(box, c.multAlpha(0.3), c.multAlpha(0.8)) } } 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 304f706d1..0a82fe3f1 100644 --- a/src/main/kotlin/com/lambda/module/modules/movement/Blink.kt +++ b/src/main/kotlin/com/lambda/module/modules/movement/Blink.kt @@ -69,8 +69,7 @@ object Blink : Module( onDynamicRender { esp -> val color = ClickGuiLayout.primaryColor - val pos = player.pos - esp.shapes(pos.x, pos.y, pos.z) { + esp.shapes { box(box.update(lastBox), color.setAlpha(0.3), color) } } 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..6a4c28537 100644 --- a/src/main/kotlin/com/lambda/module/modules/player/AirPlace.kt +++ b/src/main/kotlin/com/lambda/module/modules/player/AirPlace.kt @@ -110,7 +110,7 @@ object AirPlace : Module( placementPos?.let { pos -> val boxes = placementState?.getOutlineShape(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()) { + esp.shapes { boxes.forEach { box -> outline(box.offset(pos), outlineColor) } 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 88bc758aa..e641d04d6 100644 --- a/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt +++ b/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt @@ -176,7 +176,7 @@ object PacketMine : Module( onStaticRender { esp -> if (renderRebreak) { rebreakPos?.let { pos -> - esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) { + esp.shapes { outline(pos, rebreakColor) } } @@ -191,7 +191,7 @@ object PacketMine : Module( 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()) { + esp.shapes { boxes.forEach { box -> box(box, 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..9b821c649 100644 --- a/src/main/kotlin/com/lambda/module/modules/player/WorldEater.kt +++ b/src/main/kotlin/com/lambda/module/modules/player/WorldEater.kt @@ -66,7 +66,7 @@ object WorldEater : Module( } onStaticRender { esp -> - esp.shapes(pos1.x.toDouble(), pos1.y.toDouble(), pos1.z.toDouble()) { + esp.shapes { outline(Box.enclosing(pos1, pos2), Color.BLUE) } } diff --git a/src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt b/src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt index fee5d34d3..bddfa2d31 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt @@ -68,7 +68,7 @@ object BlockOutline : Module( interpolated.expand(0.001) } - renderer.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) { + renderer.shapes { boxes.forEach { box -> if (fill) filled(box, fillColor) if (outline) outline(box, outlineColor, thickness = lineWidth) 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 346284570..e479f0ef1 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/EntityESP.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/EntityESP.kt @@ -22,11 +22,12 @@ import com.lambda.event.events.GuiEvent import com.lambda.event.events.RenderEvent import com.lambda.event.listener.SafeListener.Companion.listen import com.lambda.graphics.mc.ImmediateRegionESP +import com.lambda.graphics.text.SDFTextRenderer +import com.lambda.graphics.text.TextRenderer import com.lambda.module.Module import com.lambda.module.tag.ModuleTag import com.lambda.util.NamedEnum import com.lambda.util.extension.tickDeltaF -import com.lambda.util.math.setAlpha import imgui.ImGui import net.minecraft.entity.Entity import net.minecraft.entity.ItemEntity @@ -50,9 +51,9 @@ object EntityESP : Module( tag = ModuleTag.RENDER ) { private val esp = ImmediateRegionESP("EntityESP") - + // Text renderer for testing -// private val testTextRenderer by lazy { TextRenderer("fonts/FiraSans-Regular.ttf", 96f) } + private val testTextRenderer by lazy { TextRenderer("fonts/FiraSans-Regular.ttf", 96f) } private data class LabelData( val screenX: Float, @@ -64,6 +65,11 @@ object EntityESP : Module( private val pendingLabels = mutableListOf() + private val outlineWidth by setting("Outline Width", 0.15f, 0f..1f, 0.01f) + private val glowWidth by setting("Glow Width", 0.25f, 0f..1f, 0.01f) + private val shadowDistance by setting("Shadow Distance", 0.2f, 0f..1f, 0.01f) + private val shadowAngle by setting("Shadow Angle", 135f, 0f..360f, 1f) + 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) @@ -82,7 +88,7 @@ object EntityESP : Module( 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 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) @@ -113,34 +119,34 @@ object EntityESP : Module( val tickDelta = mc.tickDeltaF // Test SDF text rendering with glow and outline -// val eyePos = player.eyePos.add(player.rotationVector.multiply(2.0)) // 2 blocks in front + val eyePos = player.eyePos.add(player.rotationVector.multiply(2.0)) // 2 blocks in front // SDFTextRenderer.drawWorld( // text = "SDFTextRenderer World", // pos = eyePos, // fontSize = 0.5f, // style = SDFTextRenderer.TextStyle( // color = Color.WHITE, -// outline = SDFTextRenderer.TextOutline(Color.BLACK, 0.15f), -// glow = SDFTextRenderer.TextGlow(Color(0, 200, 255, 180), 0.2f), -// shadow = true +// outline = SDFTextRenderer.TextOutline(Color.BLACK, outlineWidth), +// glow = SDFTextRenderer.TextGlow(Color(0, 200, 255, 180), glowWidth), +// shadow = SDFTextRenderer.TextShadow(Color.YELLOW, offset = shadowDistance, angle = shadowAngle) // ), // centered = true, // seeThrough = true // ) -// -// SDFTextRenderer.drawScreen( -// text = "SDFTextRenderer Screen", -// x = 20f, -// y = 20f, -// fontSize = 24f, -// style = SDFTextRenderer.TextStyle( -// color = Color.WHITE, -// outline = SDFTextRenderer.TextOutline(Color.BLACK, 0.15f), -// glow = SDFTextRenderer.TextGlow(Color(0, 200, 255, 180), 0.2f), -// shadow = true -// ) -// ) -// + + SDFTextRenderer.drawScreen( + text = "SDFTextRenderer Screen", + x = 20f, + y = 20f, + fontSize = 24f, + style = SDFTextRenderer.TextStyle( + color = Color.WHITE, + outline = SDFTextRenderer.TextOutline(Color.BLACK, 0.15f), + glow = SDFTextRenderer.TextGlow(Color(0, 200, 255, 180), 0.2f), + shadow = SDFTextRenderer.TextShadow(Color.YELLOW, 0.2f) + ) + ) + // // Test regular TextRenderer - World space (slightly below SDF text) // val textWorldPos = player.eyePos.add(player.rotationVector.multiply(2.0)).add(0.0, -0.5, 0.0) // testTextRenderer.drawWorld( @@ -150,47 +156,47 @@ object EntityESP : Module( // scale = 0.025f, // centered = true, // seeThrough = true -// ) -// -// // Test regular TextRenderer - Screen space -// testTextRenderer.drawScreen( -// x = 20f, -// y = 100f, -// text = "TextRenderer Screen", -// color = Color.GREEN, -// scale = 1f // ) - world.entities.forEach { entity -> - val color = getEntityColor(entity) - val box = entity.boundingBox - - esp.shapes(entity.x, entity.y, entity.z) { - if (drawBoxes) { - box(box) { - if (drawFilled) - filled(color.setAlpha(filledAlpha)) - if (drawOutline) - outline( - color.setAlpha(outlineAlpha), - thickness = outlineWidth - ) - } - } - - if (tracers) { - val color = getEntityColor(entity) - val entityPos = getInterpolatedPos(entity, tickDelta) - val startPos = getTracerStartPos(tickDelta) - val endPos = entityPos.add(0.0, entity.height / 2.0, 0.0) - line(startPos, endPos) { - color(color.setAlpha(outlineAlpha)) - width(tracerWidth) - if (dashedTracers) dashed(dashLength, gapLength) - } - } - } - } + // Test regular TextRenderer - Screen space + testTextRenderer.drawScreen( + x = 20f, + y = 100f, + text = "TextRenderer Screen", + color = Color.GREEN, + fontSize = 24f + ) + +// 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) { +// if (drawFilled) +// filled(color.setAlpha(filledAlpha)) +// if (drawOutline) +// outline( +// color.setAlpha(outlineAlpha), +// thickness = outlineWidth +// ) +// } +// } +// +// if (tracers) { +// val color = getEntityColor(entity) +// val entityPos = getInterpolatedPos(entity, tickDelta) +// val startPos = getTracerStartPos(tickDelta) +// val endPos = entityPos.add(0.0, entity.height / 2.0, 0.0) +// line(startPos, endPos) { +// color(color.setAlpha(outlineAlpha)) +// width(tracerWidth) +// if (dashedTracers) dashed(dashLength, gapLength) +// } +// } +// } +// } esp.upload() esp.render() diff --git a/src/main/kotlin/com/lambda/module/modules/render/StorageESP.kt b/src/main/kotlin/com/lambda/module/modules/render/StorageESP.kt index e20dbe40d..75a91baf8 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/StorageESP.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/StorageESP.kt @@ -18,18 +18,18 @@ package com.lambda.module.modules.render import com.lambda.context.SafeContext +import com.lambda.event.events.onStaticRender 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 com.lambda.util.world.blockEntitySearch +import com.lambda.util.world.entitySearch import net.minecraft.block.entity.BarrelBlockEntity import net.minecraft.block.entity.BlastFurnaceBlockEntity import net.minecraft.block.entity.BlockEntity @@ -115,7 +115,7 @@ object StorageESP : Module( blockEntitySearch(distance) .filter { it::class in entities } .forEach { be -> - esp.shapes(be.pos.x.toDouble(), be.pos.y.toDouble(), be.pos.z.toDouble()) { + esp.shapes { build(be, excludedSides(be)) } } @@ -129,7 +129,7 @@ object StorageESP : Module( it::class in entities } (mineCarts + itemFrames).forEach { entity -> - esp.shapes(entity.getX(), entity.getY(), entity.getZ()) { + esp.shapes { build(entity, DirectionMask.ALL) } } diff --git a/src/main/resources/assets/lambda/shaders/core/sdf_text.fsh b/src/main/resources/assets/lambda/shaders/core/sdf_text.fsh index 5550045d4..3af9f372a 100644 --- a/src/main/resources/assets/lambda/shaders/core/sdf_text.fsh +++ b/src/main/resources/assets/lambda/shaders/core/sdf_text.fsh @@ -4,6 +4,14 @@ uniform sampler2D Sampler0; +// SDF effect parameters - passed via uniform buffer +layout(std140) uniform SDFParams { + float SDFThreshold; // Main text edge threshold (default 0.5) + float OutlineWidth; // Outline width in SDF units (0 = no outline) + float GlowRadius; // Glow radius in SDF units (0 = no glow) + float ShadowSoftness; // Shadow softness (0 = no shadow) +}; + in vec2 texCoord0; in vec4 vertexColor; in float sphericalVertexDistance; @@ -14,35 +22,48 @@ out vec4 fragColor; void main() { // Sample the SDF texture - use ALPHA channel vec4 texSample = texture(Sampler0, texCoord0); - float sdfValue = texSample.a; // SDF in alpha channel + float sdfValue = texSample.a; - // IMPORTANT: Adjust smoothing based on distance field range - // For a typical SDF with 0.5 at the edge: - float smoothing = fwidth(sdfValue) * 0.5; // Reduced from 0.7 + // Screen-space anti-aliasing + float smoothing = fwidth(sdfValue) * 0.5; - int layerType = int(vertexColor.a * 255.0 + 0.5); // +0.5 for proper rounding + // Decode layer type from vertex alpha + int layerType = int(vertexColor.a * 255.0 + 0.5); float alpha; if (layerType >= 200) { - // Main text - alpha = smoothstep(0.5 - smoothing, 0.5 + smoothing, sdfValue); + // Main text layer - sharp edge at threshold + alpha = smoothstep(SDFThreshold - smoothing, SDFThreshold + smoothing, sdfValue); } else if (layerType >= 100) { - // Outline - use wider threshold - alpha = smoothstep(0.4 - smoothing, 0.45 + smoothing * 2.0, sdfValue); + // 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 - softer, wider - alpha = smoothstep(0.3, 0.45, sdfValue) * 0.6; + // Glow layer - always starts from text edge (SDFThreshold) and extends outward + float glowStart = SDFThreshold - GlowRadius; + float glowEnd = SDFThreshold; + alpha = smoothstep(glowStart, glowEnd, sdfValue) * 0.6; + // Mask out the main text area (anything inside the text edge) + float textMask = smoothstep(SDFThreshold - smoothing, SDFThreshold + smoothing, sdfValue); + alpha = alpha * (1.0 - textMask); } else { - // Shadow - alpha = smoothstep(0.25, 0.4, sdfValue) * 0.5; + // 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 and discard + // 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, From 9edbe8f19f019f7fb6e1c1391b4443fe52487a1b Mon Sep 17 00:00:00 2001 From: beanbag44 <107891830+beanbag44@users.noreply.github.com> Date: Sat, 17 Jan 2026 00:52:19 +0000 Subject: [PATCH 06/26] move font rendering into RenderBuilder and fix dashed lines. --- .../com/lambda/config/groups/BreakSettings.kt | 2 +- .../kotlin/com/lambda/graphics/esp/EspDsl.kt | 32 - .../com/lambda/graphics/esp/ShapeScope.kt | 224 ------ .../com/lambda/graphics/mc/BoxBuilder.kt | 326 ++++++++ .../lambda/graphics/mc/ChunkedRegionESP.kt | 65 +- .../lambda/graphics/mc/ImmediateRegionESP.kt | 73 +- .../graphics/mc/LambdaRenderPipelines.kt | 8 +- .../lambda/graphics/mc/LambdaVertexFormats.kt | 130 +++ .../com/lambda/graphics/mc/LineDashStyle.kt | 116 +++ .../com/lambda/graphics/mc/RegionRenderer.kt | 35 +- .../lambda/graphics/mc/RegionShapeBuilder.kt | 755 ------------------ .../graphics/mc/RegionVertexCollector.kt | 199 ++++- .../com/lambda/graphics/mc/RenderBuilder.kt | 642 +++++++++++++++ .../lambda/graphics/mc/TransientRegionESP.kt | 70 +- .../graphics/renderer/esp/DynamicAABB.kt | 8 + .../com/lambda/graphics/text/FontAtlas.kt | 280 ------- .../com/lambda/graphics/text/FontHandler.kt | 52 +- .../lambda/graphics/text/SDFTextRenderer.kt | 522 ------------ .../com/lambda/graphics/text/TextRenderer.kt | 311 -------- .../construction/simulation/Simulation.kt | 4 +- .../simulation/context/BreakContext.kt | 4 +- .../simulation/context/InteractContext.kt | 4 +- .../simulation/result/results/BreakResult.kt | 17 +- .../result/results/GenericResult.kt | 12 +- .../result/results/InteractResult.kt | 8 +- .../simulation/result/results/PreSimResult.kt | 21 +- .../managers/breaking/BreakConfig.kt | 2 +- .../managers/breaking/BreakManager.kt | 5 +- .../lambda/module/modules/debug/BlockTest.kt | 4 +- .../lambda/module/modules/debug/RenderTest.kt | 10 +- .../module/modules/movement/BackTrack.kt | 6 +- .../lambda/module/modules/movement/Blink.kt | 5 +- .../lambda/module/modules/player/AirPlace.kt | 5 +- .../module/modules/player/PacketMine.kt | 10 +- .../module/modules/player/WorldEater.kt | 5 +- .../lambda/module/modules/render/BlockESP.kt | 18 +- .../module/modules/render/BlockOutline.kt | 9 +- .../lambda/module/modules/render/EntityESP.kt | 102 +-- .../module/modules/render/StorageESP.kt | 53 +- .../lambda/shaders/core/advanced_lines.fsh | 110 ++- .../lambda/shaders/core/advanced_lines.vsh | 109 +-- .../assets/lambda/shaders/core/sdf_text.vsh | 43 +- .../assets/lambda/shaders/fragment/font.glsl | 24 - .../lambda/shaders/fragment/pos_color.glsl | 9 - .../lambda/shaders/fragment/pos_tex.glsl | 11 - .../shaders/fragment/pos_tex_color.glsl | 13 - .../assets/lambda/shaders/post/sdf.glsl | 34 - .../assets/lambda/shaders/shared/hsb.glsl | 30 - .../assets/lambda/shaders/shared/rect.glsl | 45 -- .../assets/lambda/shaders/shared/sdf.glsl | 3 - .../assets/lambda/shaders/shared/shade.glsl | 22 - .../lambda/shaders/vertex/box_dynamic.glsl | 17 - .../lambda/shaders/vertex/box_static.glsl | 15 - .../assets/lambda/shaders/vertex/font.glsl | 19 - .../lambda/shaders/vertex/tracer_dynamic.glsl | 20 - .../lambda/shaders/vertex/tracer_static.glsl | 17 - src/main/resources/lambda.accesswidener | 6 + 57 files changed, 1992 insertions(+), 2709 deletions(-) delete mode 100644 src/main/kotlin/com/lambda/graphics/esp/EspDsl.kt delete mode 100644 src/main/kotlin/com/lambda/graphics/esp/ShapeScope.kt create mode 100644 src/main/kotlin/com/lambda/graphics/mc/BoxBuilder.kt create mode 100644 src/main/kotlin/com/lambda/graphics/mc/LambdaVertexFormats.kt create mode 100644 src/main/kotlin/com/lambda/graphics/mc/LineDashStyle.kt delete mode 100644 src/main/kotlin/com/lambda/graphics/mc/RegionShapeBuilder.kt create mode 100644 src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt delete mode 100644 src/main/kotlin/com/lambda/graphics/text/FontAtlas.kt delete mode 100644 src/main/kotlin/com/lambda/graphics/text/SDFTextRenderer.kt delete mode 100644 src/main/kotlin/com/lambda/graphics/text/TextRenderer.kt delete mode 100644 src/main/resources/assets/lambda/shaders/fragment/font.glsl delete mode 100644 src/main/resources/assets/lambda/shaders/fragment/pos_color.glsl delete mode 100644 src/main/resources/assets/lambda/shaders/fragment/pos_tex.glsl delete mode 100644 src/main/resources/assets/lambda/shaders/fragment/pos_tex_color.glsl delete mode 100644 src/main/resources/assets/lambda/shaders/post/sdf.glsl delete mode 100644 src/main/resources/assets/lambda/shaders/shared/hsb.glsl delete mode 100644 src/main/resources/assets/lambda/shaders/shared/rect.glsl delete mode 100644 src/main/resources/assets/lambda/shaders/shared/sdf.glsl delete mode 100644 src/main/resources/assets/lambda/shaders/shared/shade.glsl delete mode 100644 src/main/resources/assets/lambda/shaders/vertex/box_dynamic.glsl delete mode 100644 src/main/resources/assets/lambda/shaders/vertex/box_static.glsl delete mode 100644 src/main/resources/assets/lambda/shaders/vertex/font.glsl delete mode 100644 src/main/resources/assets/lambda/shaders/vertex/tracer_dynamic.glsl delete mode 100644 src/main/resources/assets/lambda/shaders/vertex/tracer_static.glsl diff --git a/src/main/kotlin/com/lambda/config/groups/BreakSettings.kt b/src/main/kotlin/com/lambda/config/groups/BreakSettings.kt index 9ef2b2d41..f0c0df6d6 100644 --- a/src/main/kotlin/com/lambda/config/groups/BreakSettings.kt +++ b/src/main/kotlin/com/lambda/config/groups/BreakSettings.kt @@ -109,7 +109,7 @@ open class BreakSettings( // 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 outlineWidth by c.setting("Outline Width", 2f, 0f..10f, 0.1f, "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() diff --git a/src/main/kotlin/com/lambda/graphics/esp/EspDsl.kt b/src/main/kotlin/com/lambda/graphics/esp/EspDsl.kt deleted file mode 100644 index 0c0a19a3f..000000000 --- a/src/main/kotlin/com/lambda/graphics/esp/EspDsl.kt +++ /dev/null @@ -1,32 +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.ChunkedRegionESP -import com.lambda.module.Module - -@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) -} 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 7ae67ba01..000000000 --- a/src/main/kotlin/com/lambda/graphics/esp/ShapeScope.kt +++ /dev/null @@ -1,224 +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.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.Vec3d -import net.minecraft.util.shape.VoxelShape -import java.awt.Color - -/** - * Scope for building ESP shapes with camera-relative coordinates. - * @param cameraPos The camera position for computing relative coordinates - */ -@EspDsl -class ShapeScope(cameraPos: Vec3d) { - internal val builder = RegionShapeBuilder(cameraPos) - - /** Start building a box. */ - fun box(box: Box, block: BoxScope.() -> Unit) { - val scope = BoxScope(box, this) - scope.apply(block) - } - - /** Draw a line between two points. */ - fun line(start: Vec3d, end: Vec3d, color: Color, width: Float = 1.0f) { - builder.line(start, end, color, width) - } - - /** Draw a tracer. */ - fun line(from: Vec3d, to: Vec3d, block: LineScope.() -> Unit = {}) { - val scope = LineScope(from, to, this) - scope.apply(block) - scope.draw() - } - - /** Draw a simple filled box. */ - fun filled(box: Box, color: Color, sides: Int = DirectionMask.ALL) { - builder.filled(box, color, 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) - } - - fun filled(box: DynamicAABB, color: Color, sides: Int = DirectionMask.ALL) { - builder.filled(box, color, sides) - } - - fun outline(box: DynamicAABB, color: Color, sides: Int = DirectionMask.ALL, thickness: Float = builder.lineWidth) { - builder.outline(box, color, sides, thickness = thickness) - } - - fun filled(pos: BlockPos, color: Color, sides: Int = DirectionMask.ALL) { - builder.filled(pos, color, sides) - } - - fun outline(pos: BlockPos, color: Color, sides: Int = DirectionMask.ALL, thickness: Float = builder.lineWidth) { - builder.outline(pos, color, sides, thickness = thickness) - } - - fun filled(pos: BlockPos, state: BlockState, color: Color, sides: Int = DirectionMask.ALL) { - builder.filled(pos, state, color, 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) - } - - fun filled(shape: VoxelShape, color: Color, sides: Int = DirectionMask.ALL) { - builder.filled(shape, color, sides) - } - - fun outline(shape: VoxelShape, color: Color, sides: Int = DirectionMask.ALL, thickness: Float = builder.lineWidth) { - builder.outline(shape, 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) - } - - 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) - } - - 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) - } - - 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) - } - - 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) - } - - 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) - } -} - -@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) - } - } -} 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..9442f91bb --- /dev/null +++ b/src/main/kotlin/com/lambda/graphics/mc/BoxBuilder.kt @@ -0,0 +1,326 @@ +/* + * 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.renderer.esp.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 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 index 4ab75050e..726f6141a 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/ChunkedRegionESP.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/ChunkedRegionESP.kt @@ -23,7 +23,7 @@ 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.ShapeScope +import com.lambda.graphics.text.FontHandler import com.lambda.module.Module import com.lambda.module.modules.client.StyleEditor import com.lambda.threading.runSafe @@ -56,7 +56,7 @@ class ChunkedRegionESP( owner: Module, name: String, private val depthTest: Boolean = false, - private val update: ShapeScope.(World, FastVector) -> Unit + private val update: RenderBuilder.(World, FastVector) -> Unit ) { private val chunkMap = ConcurrentHashMap() @@ -155,6 +155,53 @@ class ChunkedRegionESP( chunkData.renderer.renderEdges(pass) } } + + // Render Text (for any chunks that have text data) + val chunksWithText = chunkTransforms.filter { (chunkData, _) -> chunkData.renderer.hasTextData() } + if (chunksWithText.isNotEmpty()) { + // Use default font atlas for chunked text + val atlas = FontHandler.getDefaultFont() + if (!atlas.isUploaded) atlas.upload() + val textureView = atlas.textureView + val sampler = atlas.sampler + if (textureView != null && sampler != null) { + val sdfParams = createSDFParamsBuffer() + if (sdfParams != null) { + RegionRenderer.createRenderPass("ChunkedESP Text", depthTest)?.use { pass -> + val pipeline = + if (depthTest) LambdaRenderPipelines.SDF_TEXT + else LambdaRenderPipelines.SDF_TEXT_THROUGH + pass.setPipeline(pipeline) + RenderSystem.bindDefaultUniforms(pass) + pass.setUniform("SDFParams", sdfParams) + pass.bindTexture("Sampler0", textureView, sampler) + + chunksWithText.forEach { (chunkData, transform) -> + pass.setUniform("DynamicTransforms", transform) + chunkData.renderer.renderText(pass) + } + } + sdfParams.close() + } + } + } + } + + private fun createSDFParamsBuffer(): com.mojang.blaze3d.buffers.GpuBuffer? { + val device = RenderSystem.getDevice() + val buffer = org.lwjgl.system.MemoryUtil.memAlloc(16) + return try { + buffer.putFloat(0.5f) + buffer.putFloat(0.1f) + buffer.putFloat(0.2f) + buffer.putFloat(0.15f) + buffer.flip() + device.createBuffer({ "SDFParams" }, com.mojang.blaze3d.buffers.GpuBuffer.USAGE_UNIFORM, buffer) + } catch (e: Exception) { + null + } finally { + org.lwjgl.system.MemoryUtil.memFree(buffer) + } } init { @@ -219,7 +266,7 @@ class ChunkedRegionESP( // Use chunk origin as the "camera" position for relative coords val chunkOriginVec = Vec3d(originX, originY, originZ) - val scope = ShapeScope(chunkOriginVec) + val scope = RenderBuilder(chunkOriginVec) for (x in chunk.pos.startX..chunk.pos.endX) { for (z in chunk.pos.startZ..chunk.pos.endZ) { @@ -230,7 +277,7 @@ class ChunkedRegionESP( } uploadQueue.add { - renderer.upload(scope.builder.collector) + renderer.upload(scope.collector) isDirty = false } } @@ -239,4 +286,14 @@ class ChunkedRegionESP( renderer.close() } } + + companion object { + fun Module.chunkedEsp( + name: String, + depthTest: Boolean = false, + update: RenderBuilder.(World, FastVector) -> Unit + ): ChunkedRegionESP { + return ChunkedRegionESP(this, name, depthTest, update) + } + } } diff --git a/src/main/kotlin/com/lambda/graphics/mc/ImmediateRegionESP.kt b/src/main/kotlin/com/lambda/graphics/mc/ImmediateRegionESP.kt index cd46ec3d8..288e0c222 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/ImmediateRegionESP.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/ImmediateRegionESP.kt @@ -18,12 +18,12 @@ package com.lambda.graphics.mc import com.lambda.Lambda.mc -import com.lambda.graphics.esp.ShapeScope import com.mojang.blaze3d.systems.RenderSystem import net.minecraft.util.math.Vec3d import org.joml.Matrix4f import org.joml.Vector3f import org.joml.Vector4f +import org.lwjgl.system.MemoryUtil /** * Interpolated ESP system for smooth entity rendering. @@ -36,7 +36,7 @@ class ImmediateRegionESP(val name: String, var depthTest: Boolean = false) { private val renderer = RegionRenderer() // Current frame builder (being populated this frame) - private var currScope: ShapeScope? = null + private var renderBuilder: RenderBuilder? = null /** * Get the current camera position for building camera-relative shapes. @@ -45,28 +45,36 @@ class ImmediateRegionESP(val name: String, var depthTest: Boolean = false) { private fun getCameraPos(): Vec3d? = mc.gameRenderer?.camera?.pos /** Get or create a ShapeScope for drawing with camera-relative coordinates. */ - fun shapes(block: ShapeScope.() -> Unit) { - val s = currScope ?: ShapeScope(getCameraPos() ?: return).also { currScope = it } + fun shapes(block: RenderBuilder.() -> Unit) { + val s = renderBuilder ?: RenderBuilder(getCameraPos() ?: return).also { renderBuilder = it } s.apply(block) } /** Clear all geometry data. */ fun clear() { - currScope = null + renderBuilder = null } /** Called each tick to reset for next frame. */ fun tick() { - currScope = null + renderBuilder = null } /** Upload collected geometry to GPU. Must be called on main thread. */ fun upload() { - currScope?.let { s -> - renderer.upload(s.builder.collector) - } ?: renderer.clearData() + renderBuilder?.let { s -> + renderer.upload(s.collector) + // Track font atlas for text rendering + currentFontAtlas = s.fontAtlas + } ?: run { + renderer.clearData() + currentFontAtlas = null + } } + // Font atlas used for current text rendering + private var currentFontAtlas: com.lambda.graphics.text.SDFFontAtlas? = null + /** Close and release all GPU resources. */ fun close() { renderer.close() @@ -111,5 +119,52 @@ class ImmediateRegionESP(val name: String, var depthTest: Boolean = false) { pass.setUniform("DynamicTransforms", dynamicTransform) renderer.renderEdges(pass) } + + // Render Text + if (renderer.hasTextData()) { + val atlas = currentFontAtlas + if (atlas != null) { + if (!atlas.isUploaded) atlas.upload() + val textureView = atlas.textureView + val sampler = atlas.sampler + if (textureView != null && sampler != null) { + val sdfParams = createSDFParamsBuffer() + if (sdfParams != null) { + RegionRenderer.createRenderPass("$name Text", depthTest)?.use { pass -> + val pipeline = + if (depthTest) LambdaRenderPipelines.SDF_TEXT + else LambdaRenderPipelines.SDF_TEXT_THROUGH + pass.setPipeline(pipeline) + RenderSystem.bindDefaultUniforms(pass) + pass.setUniform("DynamicTransforms", dynamicTransform) + pass.setUniform("SDFParams", sdfParams) + pass.bindTexture("Sampler0", textureView, sampler) + renderer.renderText(pass) + } + sdfParams.close() + } + } + } + } + } + + /** + * Create SDF params uniform buffer with default values. + */ + private fun createSDFParamsBuffer(): com.mojang.blaze3d.buffers.GpuBuffer? { + val device = RenderSystem.getDevice() + val buffer = MemoryUtil.memAlloc(16) + return try { + buffer.putFloat(0.5f) // SDFThreshold + buffer.putFloat(0.1f) // OutlineWidth + buffer.putFloat(0.2f) // GlowRadius + buffer.putFloat(0.15f) // ShadowSoftness + buffer.flip() + device.createBuffer({ "SDFParams" }, com.mojang.blaze3d.buffers.GpuBuffer.USAGE_UNIFORM, buffer) + } catch (_: Exception) { + null + } finally { + MemoryUtil.memFree(buffer) + } } } diff --git a/src/main/kotlin/com/lambda/graphics/mc/LambdaRenderPipelines.kt b/src/main/kotlin/com/lambda/graphics/mc/LambdaRenderPipelines.kt index 1238e590a..48907b2e5 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/LambdaRenderPipelines.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/LambdaRenderPipelines.kt @@ -55,7 +55,7 @@ object LambdaRenderPipelines : Loadable { .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() @@ -73,7 +73,7 @@ object LambdaRenderPipelines : Loadable { .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() @@ -176,7 +176,7 @@ object LambdaRenderPipelines : Loadable { .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST) .withCull(false) .withVertexFormat( - VertexFormats.POSITION_TEXTURE_COLOR, + LambdaVertexFormats.POSITION_TEXTURE_COLOR_ANCHOR, VertexFormat.DrawMode.QUADS ) .build() @@ -196,7 +196,7 @@ object LambdaRenderPipelines : Loadable { .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) .withCull(false) .withVertexFormat( - VertexFormats.POSITION_TEXTURE_COLOR, + LambdaVertexFormats.POSITION_TEXTURE_COLOR_ANCHOR, 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..df3c616fe --- /dev/null +++ b/src/main/kotlin/com/lambda/graphics/mc/LambdaVertexFormats.kt @@ -0,0 +1,130 @@ +/* + * 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() +} 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..320b70e26 --- /dev/null +++ b/src/main/kotlin/com/lambda/graphics/mc/LineDashStyle.kt @@ -0,0 +1,116 @@ +/* + * 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 + ) + } +} diff --git a/src/main/kotlin/com/lambda/graphics/mc/RegionRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/RegionRenderer.kt index 108d32cc1..a5b46eba4 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/RegionRenderer.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/RegionRenderer.kt @@ -32,13 +32,15 @@ import java.util.* */ class RegionRenderer { - // Dedicated GPU buffers for faces and edges + // Dedicated GPU buffers for faces, edges, and text private var faceVertexBuffer: GpuBuffer? = null private var edgeVertexBuffer: GpuBuffer? = null + private var textVertexBuffer: GpuBuffer? = null // Index counts for draw calls private var faceIndexCount = 0 private var edgeIndexCount = 0 + private var textIndexCount = 0 // State tracking private var hasData = false @@ -55,6 +57,7 @@ class RegionRenderer { // Cleanup old buffers faceVertexBuffer?.close() edgeVertexBuffer?.close() + textVertexBuffer?.close() // Assign new buffers and counts faceVertexBuffer = result.faces?.buffer @@ -63,7 +66,10 @@ class RegionRenderer { edgeVertexBuffer = result.edges?.buffer edgeIndexCount = result.edges?.indexCount ?: 0 - hasData = faceVertexBuffer != null || edgeVertexBuffer != null + textVertexBuffer = result.text?.buffer + textIndexCount = result.text?.indexCount ?: 0 + + hasData = faceVertexBuffer != null || edgeVertexBuffer != null || textVertexBuffer != null } /** @@ -102,14 +108,39 @@ class RegionRenderer { 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 + /** Clear all geometry data and release GPU resources. */ fun clearData() { faceVertexBuffer?.close() edgeVertexBuffer?.close() + textVertexBuffer?.close() faceVertexBuffer = null edgeVertexBuffer = null + textVertexBuffer = null faceIndexCount = 0 edgeIndexCount = 0 + textIndexCount = 0 hasData = false } 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 4396348ef..000000000 --- a/src/main/kotlin/com/lambda/graphics/mc/RegionShapeBuilder.kt +++ /dev/null @@ -1,755 +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.tickDelta -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 - -/** - * Shape builder for camera-relative rendering. All coordinates are computed - * relative to the camera position in double precision, then converted to float. - * This prevents floating-point jitter at large world coordinates. - * - * @param cameraPos The camera's world position for computing relative coordinates - */ -class RegionShapeBuilder(private val cameraPos: Vec3d) { - val collector = RegionVertexCollector() - - val lineWidth: Float - get() = StyleEditor.outlineWidth.toFloat() - - /** Convert world coordinates to camera-relative. Computed in double precision. */ - private fun toRelative(x: Double, y: Double, z: Double) = - Triple( - (x - cameraPos.x).toFloat(), - (y - cameraPos.y).toFloat(), - (z - cameraPos.z).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) - - /** 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.tickDelta - 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.tickDelta - 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..091a1e291 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/RegionVertexCollector.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/RegionVertexCollector.kt @@ -23,6 +23,7 @@ import com.mojang.blaze3d.vertex.VertexFormat 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 +36,42 @@ import java.util.concurrent.ConcurrentLinkedDeque class RegionVertexCollector { val faceVertices = ConcurrentLinkedDeque() val edgeVertices = ConcurrentLinkedDeque() + val textVertices = 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 format for GPU-based billboarding. + * + * @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 + */ + 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 + ) + + /** Edge vertex data (position + color + normal + line width + dash style). */ data class EdgeVertex( val x: Float, val y: Float, @@ -59,7 +83,12 @@ 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 ) /** Add a face vertex. */ @@ -67,7 +96,7 @@ class RegionVertexCollector { 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 +112,78 @@ 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 + )) + } + /** * 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() + return UploadResult(faces, edges, text) } private fun uploadFaces(): BufferResult { @@ -133,19 +225,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 +277,57 @@ 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: 12 + 8 + 4 + 12 + 8 = 44 bytes per vertex + BufferAllocator(vertices.size * 48).use { allocator -> + val builder = BufferBuilder( + allocator, + VertexFormat.DrawMode.QUADS, + LambdaVertexFormats.POSITION_TEXTURE_COLOR_ANCHOR + ) + + 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) + } + } + + 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) + } + 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) } 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..0ccd53c60 --- /dev/null +++ b/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt @@ -0,0 +1,642 @@ +/* + * 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.context.SafeContext +import com.lambda.graphics.renderer.esp.DirectionMask +import com.lambda.graphics.renderer.esp.DirectionMask.hasDirection +import com.lambda.graphics.text.FontHandler +import com.lambda.graphics.text.SDFFontAtlas +import com.lambda.util.BlockUtils.blockState +import net.minecraft.block.BlockState +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Box +import net.minecraft.util.math.Vec3d +import org.joml.Matrix4f +import org.joml.Vector3f +import org.joml.Vector4f +import java.awt.Color + +@DslMarker +annotation class RenderDsl + +@RenderDsl +class RenderBuilder(private val cameraPos: Vec3d) { + val collector = RegionVertexCollector() + + /** Track font atlas for this builder (for rendering) */ + var fontAtlas: SDFFontAtlas? = null + private set + + fun box( + box: Box, + lineWidth: Float, + 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, + 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, + builder: (BoxBuilder.() -> Unit)? = null + ) = box(Box(pos), lineWidth, builder) + + context(safeContext: SafeContext) + fun boxes( + pos: BlockPos, + lineWidth: Float, + 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: TextStyle = TextStyle(), + 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 + val textWidth = if (centered) atlas.getStringWidth(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) + } + + // 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) + } + + // 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) + } + + // 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) + } + + /** + * Build text quad vertices for a layer with specified color and alpha. + * + * @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? + ) { + 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.addTextVertex(x0, y1, glyph.u0, glyph.v1, r, g, b, a, anchorX, anchorY, anchorZ, scale, true) + collector.addTextVertex(x1, y1, glyph.u1, glyph.v1, r, g, b, a, anchorX, anchorY, anchorZ, scale, true) + collector.addTextVertex(x1, y0, glyph.u1, glyph.v0, r, g, b, a, anchorX, anchorY, anchorZ, scale, true) + collector.addTextVertex(x0, y0, glyph.u0, glyph.v0, r, g, b, a, anchorX, anchorY, anchorZ, scale, true) + } 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.addTextVertex(p0.x, p0.y, glyph.u0, glyph.v1, r, g, b, a, anchorX, anchorY, anchorZ, scale, false) + collector.addTextVertex(p1.x, p1.y, glyph.u1, glyph.v1, r, g, b, a, anchorX, anchorY, anchorZ, scale, false) + collector.addTextVertex(p2.x, p2.y, glyph.u1, glyph.v0, r, g, b, a, anchorX, anchorY, anchorZ, scale, false) + collector.addTextVertex(p3.x, p3.y, glyph.u0, glyph.v0, r, g, b, a, anchorX, anchorY, anchorZ, scale, false) + } + + 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) + } + + /** Outline effect configuration */ + data class TextOutline( + val color: Color = Color.BLACK, + val width: Float = 0.1f // 0.0 - 0.3 in SDF units (distance from edge) + ) + + /** Glow effect configuration */ + data class TextGlow( + val color: Color = Color(0, 200, 255, 180), + val radius: Float = 0.2f // Glow spread in SDF units + ) + + /** Shadow effect configuration */ + data class TextShadow( + val color: Color = Color(0, 0, 0, 180), + val offset: Float = 0.05f, // Distance in text units + val angle: Float = 135f, // Angle in degrees: 0=right, 90=down, 180=left, 270=up (default: bottom-right) + val softness: Float = 0.15f // Shadow blur in SDF units (for documentation, not currently used) + ) { + /** 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() + } + + /** Text style configuration */ + data class TextStyle( + val color: Color = Color.WHITE, + val outline: TextOutline? = null, + val glow: TextGlow? = null, + val shadow: TextShadow? = TextShadow() // Default shadow enabled + ) +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/graphics/mc/TransientRegionESP.kt b/src/main/kotlin/com/lambda/graphics/mc/TransientRegionESP.kt index fbaa7af95..cf8cc1117 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/TransientRegionESP.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/TransientRegionESP.kt @@ -18,7 +18,6 @@ package com.lambda.graphics.mc import com.lambda.Lambda.mc -import com.lambda.graphics.esp.ShapeScope import com.mojang.blaze3d.systems.RenderSystem import net.minecraft.util.math.Vec3d import org.joml.Matrix4f @@ -34,34 +33,41 @@ import org.joml.Vector4f */ class TransientRegionESP(val name: String, var depthTest: Boolean = false) { private val renderer = RegionRenderer() - private var scope: ShapeScope? = null + private var renderBuilder: RenderBuilder? = null // Camera position captured at tick time (when shapes are built) private var tickCameraPos: Vec3d? = null /** Get the current shape scope for drawing. Geometry stored relative to tick camera. */ - fun shapes(block: ShapeScope.() -> Unit) { + fun shapes(block: RenderBuilder.() -> Unit) { val cameraPos = mc.gameRenderer?.camera?.pos ?: return - if (scope == null) { + if (renderBuilder == null) { tickCameraPos = cameraPos - scope = ShapeScope(cameraPos) + renderBuilder = RenderBuilder(cameraPos) } - scope?.apply(block) + renderBuilder?.apply(block) } /** Clear all current builders. Call this at the end of every tick. */ fun clear() { - scope = null + renderBuilder = null tickCameraPos = null } /** Upload collected geometry to GPU. Must be called on main thread. */ fun upload() { - scope?.let { s -> - renderer.upload(s.builder.collector) - } ?: renderer.clearData() + renderBuilder?.let { s -> + renderer.upload(s.collector) + currentFontAtlas = s.fontAtlas + } ?: run { + renderer.clearData() + currentFontAtlas = null + } } + // Font atlas used for current text rendering + private var currentFontAtlas: com.lambda.graphics.text.SDFFontAtlas? = null + /** Close and release all GPU resources. */ fun close() { renderer.close() @@ -110,5 +116,49 @@ class TransientRegionESP(val name: String, var depthTest: Boolean = false) { pass.setUniform("DynamicTransforms", dynamicTransform) renderer.renderEdges(pass) } + + // Render Text + if (renderer.hasTextData()) { + val atlas = currentFontAtlas + if (atlas != null) { + if (!atlas.isUploaded) atlas.upload() + val textureView = atlas.textureView + val sampler = atlas.sampler + if (textureView != null && sampler != null) { + val sdfParams = createSDFParamsBuffer() + if (sdfParams != null) { + RegionRenderer.createRenderPass("$name Text", depthTest)?.use { pass -> + val pipeline = + if (depthTest) LambdaRenderPipelines.SDF_TEXT + else LambdaRenderPipelines.SDF_TEXT_THROUGH + pass.setPipeline(pipeline) + RenderSystem.bindDefaultUniforms(pass) + pass.setUniform("DynamicTransforms", dynamicTransform) + pass.setUniform("SDFParams", sdfParams) + pass.bindTexture("Sampler0", textureView, sampler) + renderer.renderText(pass) + } + sdfParams.close() + } + } + } + } + } + + private fun createSDFParamsBuffer(): com.mojang.blaze3d.buffers.GpuBuffer? { + val device = RenderSystem.getDevice() + val buffer = org.lwjgl.system.MemoryUtil.memAlloc(16) + return try { + buffer.putFloat(0.5f) + buffer.putFloat(0.1f) + buffer.putFloat(0.2f) + buffer.putFloat(0.15f) + buffer.flip() + device.createBuffer({ "SDFParams" }, com.mojang.blaze3d.buffers.GpuBuffer.USAGE_UNIFORM, buffer) + } catch (e: Exception) { + null + } finally { + org.lwjgl.system.MemoryUtil.memFree(buffer) + } } } diff --git a/src/main/kotlin/com/lambda/graphics/renderer/esp/DynamicAABB.kt b/src/main/kotlin/com/lambda/graphics/renderer/esp/DynamicAABB.kt index 159b32268..2abf3f3be 100644 --- a/src/main/kotlin/com/lambda/graphics/renderer/esp/DynamicAABB.kt +++ b/src/main/kotlin/com/lambda/graphics/renderer/esp/DynamicAABB.kt @@ -18,6 +18,7 @@ package com.lambda.graphics.renderer.esp import com.lambda.util.extension.prevPos +import com.lambda.util.math.lerp import com.lambda.util.math.minus import net.minecraft.entity.Entity import net.minecraft.util.math.Box @@ -35,6 +36,13 @@ 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 diff --git a/src/main/kotlin/com/lambda/graphics/text/FontAtlas.kt b/src/main/kotlin/com/lambda/graphics/text/FontAtlas.kt deleted file mode 100644 index dec3f69c7..000000000 --- a/src/main/kotlin/com/lambda/graphics/text/FontAtlas.kt +++ /dev/null @@ -1,280 +0,0 @@ -/* - * 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.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.STBTruetype.stbtt_FindGlyphIndex -import org.lwjgl.stb.STBTruetype.stbtt_GetFontVMetrics -import org.lwjgl.stb.STBTruetype.stbtt_GetGlyphBitmapBox -import org.lwjgl.stb.STBTruetype.stbtt_GetGlyphHMetrics -import org.lwjgl.stb.STBTruetype.stbtt_InitFont -import org.lwjgl.stb.STBTruetype.stbtt_MakeGlyphBitmap -import org.lwjgl.stb.STBTruetype.stbtt_ScaleForPixelHeight -import org.lwjgl.system.MemoryStack -import org.lwjgl.system.MemoryUtil -import java.nio.ByteBuffer - -/** - * Font atlas that uses MC 1.21's GPU texture APIs for proper rendering. - * - * Uses STB TrueType for glyph rasterization and MC's GpuTexture/GpuTextureView/GpuSampler - * for texture management, enabling correct texture binding via RenderPass.bindTexture(). - * - * @param fontPath Resource path to TTF/OTF file - * @param fontSize Font size in pixels - * @param atlasWidth Atlas texture width (must be power of 2) - * @param atlasHeight Atlas texture height (must be power of 2) - */ -class FontAtlas( - fontPath: String, - val fontSize: Float = 64f, - val atlasWidth: Int = 2048, - val atlasHeight: Int = 2048 -) : AutoCloseable { - - data class Glyph( - val codepoint: Int, - val x0: Int, val y0: Int, - val x1: Int, val y1: Int, - val xOffset: Float, val yOffset: Float, - val xAdvance: Float, - val u0: Float, val v0: Float, - val u1: Float, val v1: Float - ) - - private val fontBuffer: ByteBuffer - private val fontInfo: STBTTFontinfo - 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 - - // Temporary storage for atlas during construction - private var atlasData: ByteArray? = null - - val lineHeight: Float - val ascent: Float - val descent: Float - - /** 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") - } - - // Calculate scale and metrics - val scale = stbtt_ScaleForPixelHeight(fontInfo, fontSize) - - 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 - } - - // Build atlas data - atlasData = ByteArray(atlasWidth * atlasHeight * 4) // RGBA - buildAtlas(scale) - } - - private fun buildAtlas(scale: Float) { - val data = atlasData ?: return - var penX = 1 - var penY = 1 - var rowHeight = 0 - - // Rasterize printable ASCII + extended Latin - val codepoints = (32..126) + (160..255) - - 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] - - // Check if we need to wrap to next row - if (penX + glyphW + 1 >= atlasWidth) { - penX = 1 - penY += rowHeight + 1 - rowHeight = 0 - } - - // Check atlas overflow - if (penY + glyphH + 1 >= atlasHeight) break - - // Rasterize glyph - if (glyphW > 0 && glyphH > 0) { - val tempBuffer = MemoryUtil.memAlloc(glyphW * glyphH) - try { - stbtt_MakeGlyphBitmap( - fontInfo, tempBuffer, - glyphW, glyphH, glyphW, scale, scale, glyphIndex - ) - // Copy to atlas as RGBA (white with grayscale as alpha) - for (row in 0 until glyphH) { - for (col in 0 until glyphW) { - val srcIndex = row * glyphW + col - val alpha = tempBuffer.get(srcIndex).toInt() and 0xFF - val dstIndex = ((penY + row) * atlasWidth + penX + col) * 4 - data[dstIndex + 0] = 0xFF.toByte() // R - data[dstIndex + 1] = 0xFF.toByte() // G - data[dstIndex + 2] = 0xFF.toByte() // B - data[dstIndex + 3] = alpha.toByte() // A - } - } - } finally { - MemoryUtil.memFree(tempBuffer) - } - } - - // Store glyph info - glyphs[cp] = Glyph( - codepoint = cp, - x0 = penX, y0 = penY, - x1 = penX + glyphW, y1 = penY + glyphH, - xOffset = x0[0].toFloat(), - yOffset = y0[0].toFloat(), - xAdvance = advanceWidth[0] * scale, - u0 = penX.toFloat() / atlasWidth, - v0 = penY.toFloat() / atlasHeight, - u1 = (penX + glyphW).toFloat() / atlasWidth, - v1 = (penY + glyphH).toFloat() / atlasHeight - ) - - penX += glyphW + 1 - rowHeight = maxOf(rowHeight, glyphH) - } - } - } - - /** - * Upload atlas to GPU using MC 1.21 APIs. - * Must be called on the render thread. - */ - fun upload() { - if (glTexture != null) return // Already uploaded - val data = atlasData ?: return - - RenderSystem.assertOnRenderThread() - - val gpuDevice = RenderSystem.getDevice() - - // Create GPU texture (usage flags: 5 = COPY_DST | TEXTURE_BINDING) - glTexture = gpuDevice.createTexture( - "Lambda FontAtlas", - 5, // COPY_DST (1) | TEXTURE_BINDING (4) - TextureFormat.RGBA8, - atlasWidth, atlasHeight, - 1, // layers - 1 // mip levels - ) - - // Create texture view - glTextureView = gpuDevice.createTextureView(glTexture) - - // Get sampler with linear filtering - gpuSampler = RenderSystem.getSamplerCache().get(FilterMode.LINEAR) - - // Create NativeImage and copy data - val nativeImage = NativeImage(atlasWidth, atlasHeight, false) - for (y in 0 until atlasHeight) { - for (x in 0 until atlasWidth) { - val srcIndex = (y * atlasWidth + x) * 4 - val r = data[srcIndex + 0].toInt() and 0xFF - val g = data[srcIndex + 1].toInt() and 0xFF - val b = data[srcIndex + 2].toInt() and 0xFF - val a = data[srcIndex + 3].toInt() and 0xFF - // NativeImage uses ABGR format - val abgr = (a shl 24) or (b shl 16) or (g shl 8) or r - nativeImage.setColor(x, y, abgr) - } - } - - // Upload to GPU - RenderSystem.getDevice().createCommandEncoder().writeToTexture(glTexture, nativeImage) - nativeImage.close() - - // Free atlas data after upload - atlasData = null - } - - fun getGlyph(codepoint: Int): Glyph? = glyphs[codepoint] - - /** Calculate the width of a string in pixels. */ - fun getStringWidth(text: String): Float { - var width = 0f - for (char in text) { - val glyph = glyphs[char.code] ?: glyphs[' '.code] ?: continue - width += glyph.xAdvance - } - return width - } - - override fun close() { - glTextureView?.close() - glTextureView = null - glTexture?.close() - glTexture = null - gpuSampler = null // Sampler is managed by cache, don't close - atlasData = null - MemoryUtil.memFree(fontBuffer) - } -} diff --git a/src/main/kotlin/com/lambda/graphics/text/FontHandler.kt b/src/main/kotlin/com/lambda/graphics/text/FontHandler.kt index 284c56b7f..477d703df 100644 --- a/src/main/kotlin/com/lambda/graphics/text/FontHandler.kt +++ b/src/main/kotlin/com/lambda/graphics/text/FontHandler.kt @@ -32,10 +32,8 @@ import java.util.concurrent.ConcurrentHashMap * ``` */ object FontHandler { - private val sdfFonts = ConcurrentHashMap() - private val fonts = ConcurrentHashMap() - private var defaultSDFFont: SDFFontAtlas? = null - private var defaultFont: FontAtlas? = null + private val fonts = ConcurrentHashMap() + private var defaultFont: SDFFontAtlas? = null /** * Load an SDF font from resources. @@ -44,23 +42,11 @@ object FontHandler { * @param size Base font size for SDF generation (larger = higher quality, default 128) * @return The loaded SDFFontAtlas, or null if loading failed */ - fun loadSDFFont(path: String, size: Float = 128f): SDFFontAtlas? { - val key = "$path@$size" - return sdfFonts.getOrPut(key) { - try { - SDFFontAtlas(path, size) - } catch (e: Exception) { - println("[FontHandler] Failed to load font: $path - ${e.message}") - return null - } - } - } - - fun loadFont(path: String, size: Float = 128f): FontAtlas? { + fun loadFont(path: String, size: Float = 128f): SDFFontAtlas? { val key = "$path@$size" return fonts.getOrPut(key) { try { - FontAtlas(path, size) + SDFFontAtlas(path, size) } catch (e: Exception) { println("[FontHandler] Failed to load font: $path - ${e.message}") return null @@ -72,25 +58,12 @@ object FontHandler { * Get or create the default font. * Uses MinecraftDefault-Regular.ttf at 128px base size. */ - fun getDefaultSDFFont(size: Float = 128f): SDFFontAtlas { - defaultSDFFont?.let { return it } - - val key = "fonts/FiraSans-Regular.ttf@$size" - val font = sdfFonts[key] ?: run { - val newFont = SDFFontAtlas("fonts/FiraSans-Regular.ttf", size) - sdfFonts[key] = newFont - newFont - } - defaultSDFFont = font - return font - } - - fun getDefaultFont(size: Float = 128f): FontAtlas { + fun getDefaultFont(size: Float = 128f): SDFFontAtlas { defaultFont?.let { return it } val key = "fonts/FiraSans-Regular.ttf@$size" val font = fonts[key] ?: run { - val newFont = FontAtlas("fonts/FiraSans-Regular.ttf", size) + val newFont = SDFFontAtlas("fonts/FiraSans-Regular.ttf", size) fonts[key] = newFont newFont } @@ -98,18 +71,8 @@ object FontHandler { return font } - /** - * Check if a font is already loaded. - */ - fun isSDFFontLoaded(path: String, size: Float = 128f) = sdfFonts.containsKey("$path@$size") - fun isFontLoaded(path: String, size: Float = 128f) = fonts.containsKey("path@$size") - /** - * Get all loaded font paths. - */ - fun getLoadedSDFFonts(): Set = sdfFonts.keys.toSet() - fun getLoadedFonts(): Set = fonts.keys.toSet() /** @@ -117,11 +80,8 @@ object FontHandler { * Call this when shutting down or when fonts are no longer needed. */ fun cleanup() { - sdfFonts.values.forEach { it.close() } fonts.values.forEach { it.close() } - sdfFonts.clear() fonts.clear() - defaultSDFFont = null defaultFont = null } } diff --git a/src/main/kotlin/com/lambda/graphics/text/SDFTextRenderer.kt b/src/main/kotlin/com/lambda/graphics/text/SDFTextRenderer.kt deleted file mode 100644 index 2c82c80e5..000000000 --- a/src/main/kotlin/com/lambda/graphics/text/SDFTextRenderer.kt +++ /dev/null @@ -1,522 +0,0 @@ -/* - * 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.graphics.RenderMain -import com.lambda.graphics.mc.LambdaRenderPipelines -import com.lambda.graphics.mc.RegionRenderer -import com.mojang.blaze3d.buffers.GpuBuffer -import com.mojang.blaze3d.systems.RenderSystem -import com.mojang.blaze3d.vertex.VertexFormat -import net.minecraft.client.render.BufferBuilder -import net.minecraft.client.render.VertexFormats -import net.minecraft.client.util.BufferAllocator -import net.minecraft.util.math.Vec3d -import org.joml.Matrix4f -import org.joml.Vector3f -import org.joml.Vector4f -import java.awt.Color -import java.util.concurrent.ConcurrentHashMap - -/** - * High-quality SDF-based text renderer with anti-aliasing and effects. - * - * Features: - * - **Scalable**: Crisp text at any size without pixelation - * - **Anti-aliased**: Smooth edges via SDF sampling - * - **Outline**: Configurable outline color and width - * - **Glow**: Soft outer glow effect - * - **Shadow**: Drop shadow support - * - * Usage: - * ```kotlin - * // Load a font (once during init) - * val font = SDFTextRenderer.loadFont("fonts/FiraSans-Regular.ttf") - * - * // Render with effects - * SDFTextRenderer.drawWorld( - * font = font, - * text = "Player Name", - * pos = entity.eyePos.add(0.0, 0.5, 0.0), - * fontSize = 0.5f, - * style = TextStyle( - * color = Color.WHITE, - * outline = TextOutline(Color.BLACK, 0.1f), - * glow = TextGlow(Color.CYAN, 0.2f) - * ) - * ) - * ``` - */ -object SDFTextRenderer { - private val fonts = ConcurrentHashMap() - private var defaultFont: SDFFontAtlas? = null - - /** Outline effect configuration */ - data class TextOutline( - val color: Color = Color.BLACK, - val width: Float = 0.1f // 0.0 - 0.3 in SDF units (distance from edge) - ) - - /** Glow effect configuration */ - data class TextGlow( - val color: Color = Color(0, 200, 255, 180), - val radius: Float = 0.2f // Glow spread in SDF units - ) - - /** Shadow effect configuration */ - data class TextShadow( - val color: Color = Color(0, 0, 0, 180), - val offset: Float = 0.05f, // Distance in text units - val angle: Float = 135f, // Angle in degrees: 0=right, 90=down, 180=left, 270=up (default: bottom-right) - val softness: Float = 0.15f // Shadow blur in SDF units (for documentation, not currently used) - ) { - /** 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() - } - - /** Text style configuration */ - data class TextStyle( - val color: Color = Color.WHITE, - val outline: TextOutline? = null, - val glow: TextGlow? = null, - val shadow: TextShadow? = TextShadow() // Default shadow enabled - ) - - /** - * Load a font from resources. - * - * @param path Resource path to TTF/OTF file (e.g., "fonts/FiraSans-Regular.ttf") - * @param size Font size in pixels - * @return The loaded FontAtlas, or null if loading failed - */ - fun loadFont(path: String, size: Float = 128f): SDFFontAtlas? { - val key = "$path@$size" - return fonts.getOrPut(key) { - try { - // Don't call upload() here - it requires render thread - // upload() is called lazily in drawTextQuads when textureId == 0 - SDFFontAtlas(path, size) - } catch (e: Exception) { - System.err.println("[TextRenderer] Failed to load font: $path") - System.err.println("[TextRenderer] Full path attempted: /assets/lambda/$path") - e.printStackTrace() - return null - } - } - } - - /** - * Get or create the default font. - * Size should match SDFFontAtlas defaults (128) to prevent atlas overflow. - */ - fun getDefaultFont(size: Float = 128f): SDFFontAtlas { - defaultFont?.let { return it } - - // Try to load without catching, so the actual exception is visible - val key = "fonts/FiraSans-Regular.ttf@$size" - val font = fonts[key] ?: run { - val newFont = SDFFontAtlas("fonts/FiraSans-Regular.ttf", size) - fonts[key] = newFont - newFont - } - defaultFont = font - return font - } - - /** - * Draw text at a world position (billboard style). - * - * @param font SDF font atlas to use - * @param text Text to render - * @param pos World position - * @param fontSize Size in world units - * @param style Text styling (color, outline, glow, shadow) - * @param centered Center text horizontally - * @param seeThrough Render through walls - */ - fun drawWorld( - font: SDFFontAtlas? = null, - text: String, - pos: Vec3d, - fontSize: Float = 0.5f, - style: TextStyle = TextStyle(), - centered: Boolean = true, - seeThrough: Boolean = false - ) { - val atlas = font ?: getDefaultFont() - val camera = mc.gameRenderer?.camera ?: return - val cameraPos = camera.pos - - // Camera-relative position - val relX = (pos.x - cameraPos.x).toFloat() - val relY = (pos.y - cameraPos.y).toFloat() - val relZ = (pos.z - cameraPos.z).toFloat() - - // Build billboard model matrix - val modelMatrix = Matrix4f() - .translate(relX, relY, relZ) - .rotate(camera.rotation) - .scale(fontSize, -fontSize, fontSize) - - val textWidth = if (centered) atlas.getStringWidth(text, 1f) else 0f - val startX = -textWidth / 2f - - // Draw shadow first (offset, alpha < 50 signals shadow layer) - if (style.shadow != null) { - val shadowColor = Color(style.shadow.color.red, style.shadow.color.green, style.shadow.color.blue, 25) - renderTextLayer( - atlas, text, startX + style.shadow.offsetX, style.shadow.offsetY, - shadowColor, modelMatrix, seeThrough, style - ) - } - - // Draw glow layer (alpha 50-99 signals glow layer) - if (style.glow != null) { - val glowColor = Color(style.glow.color.red, style.glow.color.green, style.glow.color.blue, 75) - renderTextLayer( - atlas, text, startX, 0f, - glowColor, modelMatrix, seeThrough, style - ) - } - - // Draw outline layer (alpha 100-199 signals outline layer) - if (style.outline != null) { - val outlineColor = Color(style.outline.color.red, style.outline.color.green, style.outline.color.blue, 150) - renderTextLayer( - atlas, text, startX, 0f, - outlineColor, modelMatrix, seeThrough, style - ) - } - - // Draw main text (alpha >= 200 signals main text layer) - val mainColor = Color(style.color.red, style.color.green, style.color.blue, 255) - renderTextLayer( - atlas, text, startX, 0f, - mainColor, modelMatrix, seeThrough, style - ) - } - - /** - * Draw text on screen at pixel coordinates. - */ - fun drawScreen( - font: SDFFontAtlas? = null, - text: String, - x: Float, - y: Float, - fontSize: Float = 24f, - style: TextStyle = TextStyle() - ) { - val atlas = font ?: getDefaultFont() - val scale = fontSize / atlas.baseSize - - // Create orthographic model matrix - // Note: vertices are built with Y-up convention, so we negate Y scale for screen (Y-down) - val modelMatrix = Matrix4f() - .translate(x, y, 0f) - .scale(scale, -scale, 1f) // Negative Y to flip for screen coordinates - - // Use screen-space rendering - if (style.shadow != null) { - renderTextLayerScreen( - atlas, text, style.shadow.offsetX * fontSize, style.shadow.offsetY * fontSize, - style.shadow.color, modelMatrix, style - ) - } - - if (style.glow != null) { - renderTextLayerScreen( - atlas, text, 0f, 0f, - style.glow.color, modelMatrix, style - ) - } - - if (style.outline != null) { - renderTextLayerScreen( - atlas, text, 0f, 0f, - style.outline.color, modelMatrix, style - ) - } - - renderTextLayerScreen( - atlas, text, 0f, 0f, - style.color, modelMatrix, style - ) - } - - /** - * Draw text at a world position projected to screen. - */ - fun drawWorldToScreen( - font: SDFFontAtlas? = null, - text: String, - worldPos: Vec3d, - fontSize: Float = 16f, - style: TextStyle = TextStyle(), - offsetY: Float = 0f - ) { - val screenPos = RenderMain.worldToScreen(worldPos) ?: return - drawScreen(font, text, screenPos.x, screenPos.y + offsetY, fontSize, style) - } - - private fun renderTextLayer( - atlas: SDFFontAtlas, - text: String, - startX: Float, - startY: Float, - color: Color, - modelMatrix: Matrix4f, - seeThrough: Boolean, - style: TextStyle - ) { - if (!atlas.isUploaded) atlas.upload() - val textureView = atlas.textureView ?: return - val sampler = atlas.sampler ?: return - if (text.isEmpty()) return - - // Build vertices for all glyphs - val vertices = buildTextVertices(atlas, text, startX, startY, color) - if (vertices.isEmpty()) return - - // Upload to GPU buffer - val gpuBuffer = uploadTextVertices(vertices) ?: return - - // Create SDF params uniform buffer - val sdfParams = createSDFParamsBuffer(style) ?: run { - gpuBuffer.close() - return - } - - // Use SDF_TEXT pipeline for proper smoothstep anti-aliasing - val pipeline = if (seeThrough) LambdaRenderPipelines.SDF_TEXT_THROUGH - else LambdaRenderPipelines.SDF_TEXT - - // Calculate model-view uniform (projection is handled by bindDefaultUniforms) - val modelView = Matrix4f(RenderMain.modelViewMatrix).mul(modelMatrix) - val dynamicTransform = RenderSystem.getDynamicUniforms() - .write(modelView, Vector4f(1f, 1f, 1f, 1f), Vector3f(0f, 0f, 0f), Matrix4f()) - - RegionRenderer.createRenderPass("SDF Text", useDepth = !seeThrough)?.use { pass -> - pass.setPipeline(pipeline) - RenderSystem.bindDefaultUniforms(pass) - pass.setUniform("DynamicTransforms", dynamicTransform) - pass.setUniform("SDFParams", sdfParams) - - // Bind texture using MC 1.21's proper API - pass.bindTexture("Sampler0", textureView, sampler) - - // Draw - pass.setVertexBuffer(0, gpuBuffer) - val indexBuffer = RenderSystem.getSequentialBuffer(VertexFormat.DrawMode.QUADS) - // For QUADS mode, each quad (4 vertices) needs 6 indices (2 triangles) - val quadCount = vertices.size / 4 - val indexCount = quadCount * 6 - pass.setIndexBuffer(indexBuffer.getIndexBuffer(indexCount), indexBuffer.indexType) - pass.drawIndexed(0, 0, indexCount, 1) - } - - gpuBuffer.close() - sdfParams.close() - } - - private fun renderTextLayerScreen( - atlas: SDFFontAtlas, - text: String, - offsetX: Float, - offsetY: Float, - color: Color, - modelMatrix: Matrix4f, - style: TextStyle - ) { - if (!atlas.isUploaded) atlas.upload() - val textureView = atlas.textureView ?: return - val sampler = atlas.sampler ?: return - if (text.isEmpty()) return - - val vertices = buildTextVertices(atlas, text, offsetX, offsetY, color) - if (vertices.isEmpty()) return - - val gpuBuffer = uploadTextVertices(vertices) ?: return - - // Create SDF params uniform buffer - val sdfParams = createSDFParamsBuffer(style) ?: run { - gpuBuffer.close() - return - } - - val window = mc.window - // Ortho projection: left=0, right=scaledWidth, top=0, bottom=scaledHeight (Y-down for screen) - val ortho = Matrix4f().ortho( - 0f, window.scaledWidth.toFloat(), - window.scaledHeight.toFloat(), 0f, - -1000f, 1000f - ) - - // Apply model matrix to ortho to get final MVP - // The model matrix has the screen position and scaling - val mvp = Matrix4f(ortho).mul(modelMatrix) - val dynamicTransform = RenderSystem.getDynamicUniforms() - .write(mvp, Vector4f(1f, 1f, 1f, 1f), Vector3f(0f, 0f, 0f), Matrix4f()) - - RegionRenderer.createRenderPass("SDF Text Screen", useDepth = false)?.use { pass -> - pass.setPipeline(LambdaRenderPipelines.SDF_TEXT_THROUGH) - RenderSystem.bindDefaultUniforms(pass) - pass.setUniform("DynamicTransforms", dynamicTransform) - pass.setUniform("SDFParams", sdfParams) - - // Bind texture using MC 1.21's proper API - pass.bindTexture("Sampler0", textureView, sampler) - - pass.setVertexBuffer(0, gpuBuffer) - val indexBuffer = RenderSystem.getSequentialBuffer(VertexFormat.DrawMode.QUADS) - // For QUADS mode, each quad (4 vertices) needs 6 indices (2 triangles) - val quadCount = vertices.size / 4 - val indexCount = quadCount * 6 - pass.setIndexBuffer(indexBuffer.getIndexBuffer(indexCount), indexBuffer.indexType) - pass.drawIndexed(0, 0, indexCount, 1) - } - - gpuBuffer.close() - sdfParams.close() - } - - private data class TextVertex( - 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 - ) - - private fun buildTextVertices( - atlas: SDFFontAtlas, - text: String, - startX: Float, - startY: Float, - color: Color - ): List { - val vertices = mutableListOf() - var penX = startX - var charCount = 0 - - for (char in text) { - val glyph = atlas.getGlyph(char.code) - if (glyph == null) continue - charCount++ - - val x0 = penX + glyph.bearingX - val y0 = startY - glyph.bearingY - val x1 = x0 + glyph.width / atlas.baseSize - val y1 = y0 + glyph.height / atlas.baseSize - - // Quad vertices (counter-clockwise for MC) - // Bottom-left - vertices.add(TextVertex(x0, y1, 0f, glyph.u0, glyph.v1, color.red, color.green, color.blue, color.alpha)) - // Bottom-right - vertices.add(TextVertex(x1, y1, 0f, glyph.u1, glyph.v1, color.red, color.green, color.blue, color.alpha)) - // Top-right - vertices.add(TextVertex(x1, y0, 0f, glyph.u1, glyph.v0, color.red, color.green, color.blue, color.alpha)) - // Top-left - vertices.add(TextVertex(x0, y0, 0f, glyph.u0, glyph.v0, color.red, color.green, color.blue, color.alpha)) - - penX += glyph.advance - } - - return vertices - } - - private fun uploadTextVertices(vertices: List): GpuBuffer? { - if (vertices.isEmpty()) return null - - var result: GpuBuffer? = null - BufferAllocator(vertices.size * 24).use { allocator -> - val builder = BufferBuilder( - allocator, - VertexFormat.DrawMode.QUADS, - VertexFormats.POSITION_TEXTURE_COLOR - ) - - for (v in vertices) { - builder.vertex(v.x, v.y, v.z) - .texture(v.u, v.v) - .color(v.r, v.g, v.b, v.a) - } - - builder.endNullable()?.let { built -> - result = RenderSystem.getDevice().createBuffer( - { "SDF Text Buffer" }, - GpuBuffer.USAGE_VERTEX, - built.buffer - ) - built.close() - } - } - - return result - } - - /** Calculate text width in world units. */ - fun getWidth(font: SDFFontAtlas? = null, text: String, fontSize: Float = 1f): Float { - val atlas = font ?: getDefaultFont() - return atlas.getStringWidth(text, fontSize) - } - - /** Get line height for a font at given size. */ - fun getLineHeight(font: SDFFontAtlas? = null, fontSize: Float = 1f): Float { - val atlas = font ?: getDefaultFont() - return atlas.lineHeight * fontSize / atlas.baseSize - } - - /** - * Create a GpuBuffer containing the SDF effect parameters for the shader. - * Layout matches std140 uniform block SDFParams in sdf_text.fsh: - * float SDFThreshold, OutlineWidth, GlowRadius, ShadowSoftness (4 floats = 16 bytes) - */ - private fun createSDFParamsBuffer(style: TextStyle): GpuBuffer? { - val device = RenderSystem.getDevice() - - // std140 layout: 4 floats (16 bytes total) - val bufferSize = 16 - - // Use LWJGL MemoryUtil for direct ByteBuffer allocation - val buffer = org.lwjgl.system.MemoryUtil.memAlloc(bufferSize) - return try { - // Write the 4 floats - buffer.putFloat(0.5f) // SDFThreshold - main text edge - buffer.putFloat(style.outline?.width ?: 0.1f) // OutlineWidth - buffer.putFloat(style.glow?.radius ?: 0.2f) // GlowRadius - buffer.putFloat(style.shadow?.softness ?: 0.15f) // ShadowSoftness - - buffer.flip() - - device.createBuffer({ "SDFParams" }, GpuBuffer.USAGE_UNIFORM, buffer) - } catch (e: Exception) { - null - } finally { - org.lwjgl.system.MemoryUtil.memFree(buffer) - } - } - - /** Clean up all loaded fonts. */ - fun cleanup() { - fonts.values.forEach { it.close() } - fonts.clear() - defaultFont = null - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/graphics/text/TextRenderer.kt b/src/main/kotlin/com/lambda/graphics/text/TextRenderer.kt deleted file mode 100644 index 2a085cec1..000000000 --- a/src/main/kotlin/com/lambda/graphics/text/TextRenderer.kt +++ /dev/null @@ -1,311 +0,0 @@ -/* - * 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.graphics.mc.LambdaRenderPipelines -import com.lambda.graphics.mc.RegionRenderer -import com.mojang.blaze3d.buffers.GpuBuffer -import com.mojang.blaze3d.systems.RenderSystem -import com.mojang.blaze3d.vertex.VertexFormat -import net.minecraft.client.render.BufferBuilder -import net.minecraft.client.render.VertexFormats -import net.minecraft.client.util.BufferAllocator -import net.minecraft.util.math.Vec3d -import org.joml.Matrix4f -import org.joml.Vector3f -import org.joml.Vector4f -import java.awt.Color - -/** - * Text renderer using MC 1.21's proper GPU texture APIs. - * - * Uses FontAtlas for glyph data and binds textures correctly via - * RenderPass.bindTexture() for compatibility with MC's new rendering pipeline. - */ -class TextRenderer( - fontPath: String, - fontSize: Float = 128f, - atlasSize: Int = 512 -) : AutoCloseable { - - private val atlas = FontAtlas(fontPath, fontSize, atlasSize, atlasSize) - - /** Font line height in pixels */ - val lineHeight: Float get() = atlas.lineHeight - - /** Font ascent in pixels */ - val ascent: Float get() = atlas.ascent - - /** Font descent in pixels (negative value) */ - val descent: Float get() = atlas.descent - - /** - * Draw text in world space, facing the camera (billboard style). - * - * @param pos World position for the text - * @param text Text string to render - * @param color Text color - * @param scale World-space scale (0.025f is similar to MC name tags) - * @param centered Center text horizontally at position - * @param seeThrough Render through walls - */ - fun drawWorld( - pos: Vec3d, - text: String, - color: Color = Color.WHITE, - scale: Float = 0.025f, - centered: Boolean = true, - seeThrough: Boolean = false - ) { - val camera = mc.gameRenderer?.camera ?: return - if (!atlas.isUploaded) atlas.upload() - val textureView = atlas.textureView ?: return - val sampler = atlas.sampler ?: return - - val cameraPos = camera.pos - - // Build transformation matrix: translate, billboard, scale - val modelView = Matrix4f(com.lambda.graphics.RenderMain.modelViewMatrix) - modelView.translate( - (pos.x - cameraPos.x).toFloat(), - (pos.y - cameraPos.y).toFloat(), - (pos.z - cameraPos.z).toFloat() - ) - // Billboard - rotate to face camera - modelView.rotate(camera.rotation) - // Scale with negative Y to flip text vertically (MC convention) - modelView.scale(scale, -scale, scale) - - // Calculate text offset for centering - val textWidth = atlas.getStringWidth(text) - val xOffset = if (centered) -textWidth / 2f else 0f - - // Build and upload vertices - val (buffer, vertexCount) = buildAndUploadVertices(text, xOffset, 0f, color) ?: return - - try { - // Use TEXT_QUADS pipeline - val pipeline = if (seeThrough) LambdaRenderPipelines.TEXT_QUADS_THROUGH - else LambdaRenderPipelines.TEXT_QUADS - - // Create dynamic transform - val dynamicTransform = RenderSystem.getDynamicUniforms() - .write( - modelView, - Vector4f(1f, 1f, 1f, 1f), - Vector3f(0f, 0f, 0f), - Matrix4f() - ) - - // Create render pass and draw - RegionRenderer.createRenderPass("TextRenderer World", !seeThrough)?.use { pass -> - pass.setPipeline(pipeline) - RenderSystem.bindDefaultUniforms(pass) - - // Bind our texture using MC 1.21's proper API - pass.bindTexture("Sampler0", textureView, sampler) - - // Set transform - pass.setUniform("DynamicTransforms", dynamicTransform) - - // Set vertex buffer and draw - pass.setVertexBuffer(0, buffer) - val shapeIndexBuffer = RenderSystem.getSequentialBuffer(VertexFormat.DrawMode.QUADS) - val indexBuffer = shapeIndexBuffer.getIndexBuffer(vertexCount) - pass.setIndexBuffer(indexBuffer, shapeIndexBuffer.indexType) - pass.drawIndexed(0, 0, vertexCount, 1) - } - } finally { - buffer.close() - } - } - - /** - * Draw text in screen space (2D overlay). - * - * @param x Screen X position in pixels - * @param y Screen Y position in pixels - * @param text Text string to render - * @param color Text color - * @param fontSize Target text height in pixels (default 16) - */ - fun drawScreen( - x: Float, - y: Float, - text: String, - color: Color = Color.WHITE, - fontSize: Float = 24f - ) { - if (!atlas.isUploaded) atlas.upload() - val textureView = atlas.textureView ?: return - val sampler = atlas.sampler ?: return - - // Convert fontSize to scale factor based on atlas font size - val scale = fontSize / atlas.fontSize - - // Build transformation for screen space with orthographic projection - val window = mc.window - val ortho = Matrix4f().ortho( - 0f, window.scaledWidth.toFloat(), - window.scaledHeight.toFloat(), 0f, - -1000f, 1000f - ) - - val modelView = Matrix4f() - modelView.translate(x, y, 0f) - modelView.scale(scale, scale, 1f) - - val mvp = Matrix4f(ortho).mul(modelView) - - // Build and upload vertices - val (buffer, vertexCount) = buildAndUploadVertices(text, 0f, 0f, color) ?: return - - try { - val pipeline = LambdaRenderPipelines.TEXT_QUADS_THROUGH // No depth test for screen - - // Create dynamic transform - val dynamicTransform = RenderSystem.getDynamicUniforms() - .write( - mvp, - Vector4f(1f, 1f, 1f, 1f), - Vector3f(0f, 0f, 0f), - Matrix4f() - ) - - RegionRenderer.createRenderPass("TextRenderer Screen", false)?.use { pass -> - pass.setPipeline(pipeline) - // Note: not calling bindDefaultUniforms - we provide complete MVP in DynamicTransforms - pass.bindTexture("Sampler0", textureView, sampler) - pass.setUniform("DynamicTransforms", dynamicTransform) - - pass.setVertexBuffer(0, buffer) - val shapeIndexBuffer = RenderSystem.getSequentialBuffer(VertexFormat.DrawMode.QUADS) - val indexBuffer = shapeIndexBuffer.getIndexBuffer(vertexCount) - pass.setIndexBuffer(indexBuffer, shapeIndexBuffer.indexType) - pass.drawIndexed(0, 0, vertexCount, 1) - } - } finally { - buffer.close() - } - } - - /** - * Get the width of a text string in pixels at scale 1.0. - */ - fun getStringWidth(text: String): Float = atlas.getStringWidth(text) - - /** - * Build and upload vertices to GPU buffer. - * Returns the buffer and vertex count, or null if no vertices. - */ - private fun buildAndUploadVertices( - text: String, - startX: Float, - startY: Float, - color: Color - ): Pair? { - val penY = startY + atlas.ascent - var penX = startX - - // Count quads for allocation - var quadCount = 0 - for (char in text) { - if (atlas.getGlyph(char.code) != null || atlas.getGlyph(' '.code) != null) { - quadCount++ - } - } - if (quadCount == 0) return null - - val vertexCount = quadCount * 4 - val vertexSize = VertexFormats.POSITION_TEXTURE_COLOR.vertexSize - - var result: Pair? = null - BufferAllocator(vertexCount * vertexSize).use { allocator -> - val builder = BufferBuilder( - allocator, - VertexFormat.DrawMode.QUADS, - VertexFormats.POSITION_TEXTURE_COLOR - ) - - val r = color.red - val g = color.green - val b = color.blue - val a = color.alpha - - for (char in text) { - val glyph = atlas.getGlyph(char.code) ?: atlas.getGlyph(' '.code) ?: continue - - val x0 = penX + glyph.xOffset - val y0 = penY + glyph.yOffset - val x1 = x0 + (glyph.x1 - glyph.x0) - val y1 = y0 + (glyph.y1 - glyph.y0) - - // Bottom-left - builder.vertex(x0, y1, 0f).texture(glyph.u0, glyph.v1).color(r, g, b, a) - // Bottom-right - builder.vertex(x1, y1, 0f).texture(glyph.u1, glyph.v1).color(r, g, b, a) - // Top-right - builder.vertex(x1, y0, 0f).texture(glyph.u1, glyph.v0).color(r, g, b, a) - // Top-left - builder.vertex(x0, y0, 0f).texture(glyph.u0, glyph.v0).color(r, g, b, a) - - penX += glyph.xAdvance - } - - builder.endNullable()?.let { built -> - val gpuDevice = RenderSystem.getDevice() - val buffer = gpuDevice.createBuffer( - { "Lambda TextRenderer" }, - GpuBuffer.USAGE_VERTEX, - built.buffer - ) - result = buffer to built.drawParameters.indexCount() - built.close() - } - } - - return result - } - - override fun close() { - atlas.close() - } - - companion object { - private val loadedFonts = mutableMapOf() - - /** - * Load or get a cached font renderer. - */ - fun loadFont(fontPath: String, fontSize: Float = 16f): TextRenderer { - val key = "$fontPath:$fontSize" - return loadedFonts.getOrPut(key) { - TextRenderer(fontPath, fontSize) - } - } - - /** - * Close and clear all cached fonts. - */ - fun closeAll() { - loadedFonts.values.forEach { it.close() } - loadedFonts.clear() - } - } -} 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 7d5ceeb90..c4b8f9dca 100644 --- a/src/main/kotlin/com/lambda/interaction/construction/simulation/Simulation.kt +++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/Simulation.kt @@ -65,7 +65,9 @@ data class Simulation( class PossiblePos(val pos: BlockPos, val interactions: Int) : Drawable { override fun render(esp: TransientRegionESP) { esp.shapes { - box(Vec3d.ofBottomCenter(pos).playerBox(), Color(0, 255, 0, 50), Color(0, 255, 0, 50)) + box(Vec3d.ofBottomCenter(pos).playerBox(), 1.5f) { + 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 6dce001a9..2f7300435 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 @@ -61,7 +61,9 @@ data class BreakContext( override fun render(esp: TransientRegionESP) { esp.shapes { - box(blockPos, baseColor, sideColor) + box(blockPos, 1.5f) { + 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 d0999f0de..807e136e1 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 @@ -54,7 +54,9 @@ data class InteractContext( x + 0.05, y + 0.05, z + 0.05, ).offset(hitResult.side.doubleVector.multiply(0.05)) } - box(box, baseColor, sideColor) + box(box, 1.5f) { + colors(baseColor, sideColor) + } } } 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 31437a099..87b3c4212 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 @@ -74,7 +74,10 @@ sealed class BreakResult : BuildResult() { override fun render(esp: TransientRegionESP) { esp.shapes { - box(pos, color, color, side.mask) + box(pos, 1.5f) { + allColors(color) + hideSides(side.mask.inv()) + } } } @@ -124,7 +127,9 @@ sealed class BreakResult : BuildResult() { override fun render(esp: TransientRegionESP) { esp.shapes { - box(pos, color, color) + box(pos, 1.5f) { + allColors(color) + } } } } @@ -147,7 +152,9 @@ sealed class BreakResult : BuildResult() { 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) + box(box, 1.5f) { + allColors(color) + } } } } @@ -166,7 +173,9 @@ sealed class BreakResult : BuildResult() { override fun render(esp: TransientRegionESP) { esp.shapes { - box(pos, color, color) + box(pos, 1.5f) { + 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 6d84b479b..05ff445ff 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 @@ -61,7 +61,9 @@ sealed class GenericResult : BuildResult() { x + 0.05, y + 0.05, z + 0.05, ).offset(pos) } - box(box, color, color) + box(box, 1.5f) { + allColors(color) + } } } @@ -109,7 +111,9 @@ sealed class GenericResult : BuildResult() { 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) + box(box, 1.5f) { + allColors(color) + } } } } @@ -142,7 +146,9 @@ sealed class GenericResult : BuildResult() { 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) + box(box, 1.5f) { + 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 eb20cff89..c81e58f43 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 @@ -90,7 +90,9 @@ sealed class InteractResult : BuildResult() { x + 0.05, y + 0.05, z + 0.05, ).offset(simulated.side.doubleVector.multiply(0.05)) } - box(box, color, color) + box(box, 1.5f) { + allColors(color) + } } } } @@ -129,7 +131,9 @@ sealed class InteractResult : BuildResult() { x + 0.05, y + 0.05, z + 0.05, ).offset(side.doubleVector.multiply(0.05)) } - box(box, color, color) + box(box, 1.5f) { + 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 c09e9ead0..32ac6a8fd 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 @@ -24,6 +24,7 @@ import com.lambda.interaction.construction.simulation.result.ComparableResult import com.lambda.interaction.construction.simulation.result.Drawable import com.lambda.interaction.construction.simulation.result.Navigable import com.lambda.interaction.construction.simulation.result.Rank +import com.lambda.util.ChatUtils.colors import net.minecraft.block.BlockState import net.minecraft.util.math.BlockPos import java.awt.Color @@ -57,7 +58,9 @@ sealed class PreSimResult : BuildResult() { override fun render(esp: TransientRegionESP) { esp.shapes { - box(pos, color, color) + box(pos, 1.5f) { + allColors(color) + } } } @@ -81,7 +84,9 @@ sealed class PreSimResult : BuildResult() { override fun render(esp: TransientRegionESP) { esp.shapes { - box(pos, color, color) + box(pos, 1.5f) { + allColors(color) + } } } } @@ -101,7 +106,9 @@ sealed class PreSimResult : BuildResult() { override fun render(esp: TransientRegionESP) { esp.shapes { - box(pos, color, color) + box(pos, 1.5f) { + allColors(color) + } } } } @@ -119,7 +126,9 @@ sealed class PreSimResult : BuildResult() { override fun render(esp: TransientRegionESP) { esp.shapes { - box(pos, color, color) + box(pos, 1.5f) { + allColors(color) + } } } } @@ -139,7 +148,9 @@ sealed class PreSimResult : BuildResult() { override fun render(esp: TransientRegionESP) { esp.shapes { - box(pos, color, color) + box(pos, 1.5f) { + 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 f6d7cc492..488b15121 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 abdba1303..fdae6562d 100644 --- a/src/main/kotlin/com/lambda/interaction/managers/breaking/BreakManager.kt +++ b/src/main/kotlin/com/lambda/interaction/managers/breaking/BreakManager.kt @@ -269,8 +269,9 @@ object BreakManager : Manager( }.forEach boxes@{ box -> val animationMode = info.breakConfig.animation val interpolatedBox = interpolateBox(box, interpolatedProgress, animationMode) - if (config.fill) filled(interpolatedBox, fillColor) - if (config.outline) outline(interpolatedBox, outlineColor) + box(interpolatedBox, info.breakConfig.outlineWidth) { + colors(fillColor, outlineColor) + } } } } 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 9de1fe39c..9ef3f30be 100644 --- a/src/main/kotlin/com/lambda/module/modules/debug/BlockTest.kt +++ b/src/main/kotlin/com/lambda/module/modules/debug/BlockTest.kt @@ -53,7 +53,9 @@ object BlockTest : Module( }.forEach { (pos, state) -> esp.shapes { 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 939c8dafb..ad940ee74 100644 --- a/src/main/kotlin/com/lambda/module/modules/debug/RenderTest.kt +++ b/src/main/kotlin/com/lambda/module/modules/debug/RenderTest.kt @@ -19,10 +19,10 @@ 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.module.Module import com.lambda.module.tag.ModuleTag +import com.lambda.util.extension.tickDelta import com.lambda.util.math.setAlpha import com.lambda.util.world.entitySearch import net.minecraft.entity.LivingEntity @@ -49,14 +49,18 @@ object RenderTest : Module( entitySearch(8.0) .forEach { entity -> esp.shapes { - box(entity.dynamicBox, filledColor, outlineColor, DirectionMask.ALL, DirectionMask.OutlineMode.And) + box(entity.dynamicBox.box(mc.tickDelta) ?: return@shapes, 1.5f) { + colors(filledColor, outlineColor) + } } } } onStaticRender { esp -> esp.shapes { - box(Box.of(player.pos, 0.3, 0.3, 0.3), filledColor, outlineColor) + box(Box.of(player.pos, 0.3, 0.3, 0.3), 1.5f) { + colors(filledColor, outlineColor) + } } } } 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 4841d22d9..f5399d7f3 100644 --- a/src/main/kotlin/com/lambda/module/modules/movement/BackTrack.kt +++ b/src/main/kotlin/com/lambda/module/modules/movement/BackTrack.kt @@ -32,6 +32,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 @@ -119,7 +120,10 @@ object BackTrack : Module( val c = lerp(p, c1, c2) esp.shapes { - box(box, c.multAlpha(0.3), c.multAlpha(0.8)) + box(box.box(mc.tickDelta) ?: return@shapes, 0f) { + hideOutline() + gradientY(c.multAlpha(0.3), c.multAlpha(0.8)) + } } } 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 0a82fe3f1..72570e3f0 100644 --- a/src/main/kotlin/com/lambda/module/modules/movement/Blink.kt +++ b/src/main/kotlin/com/lambda/module/modules/movement/Blink.kt @@ -30,6 +30,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 @@ -70,7 +71,9 @@ object Blink : Module( onDynamicRender { esp -> val color = ClickGuiLayout.primaryColor esp.shapes { - box(box.update(lastBox), color.setAlpha(0.3), color) + box(box.update(lastBox).box(mc.tickDelta) ?: return@shapes, 1.5f) { + colors(color.setAlpha(0.3), color) + } } } 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 6a4c28537..e315ab966 100644 --- a/src/main/kotlin/com/lambda/module/modules/player/AirPlace.kt +++ b/src/main/kotlin/com/lambda/module/modules/player/AirPlace.kt @@ -63,6 +63,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 @@ -112,7 +113,9 @@ object AirPlace : Module( ?: listOf(Box(0.0, 0.0, 0.0, 1.0, 1.0, 1.0)) esp.shapes { boxes.forEach { box -> - outline(box.offset(pos), outlineColor) + 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 e641d04d6..bc107a421 100644 --- a/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt +++ b/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt @@ -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() @@ -177,7 +178,10 @@ object PacketMine : Module( if (renderRebreak) { rebreakPos?.let { pos -> esp.shapes { - outline(pos, rebreakColor) + box(pos, outlineWidth) { + hideFill() + outlineColor(rebreakColor) + } } } } @@ -193,7 +197,9 @@ object PacketMine : Module( esp.shapes { 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 9b821c649..eb6982f5e 100644 --- a/src/main/kotlin/com/lambda/module/modules/player/WorldEater.kt +++ b/src/main/kotlin/com/lambda/module/modules/player/WorldEater.kt @@ -67,7 +67,10 @@ object WorldEater : Module( onStaticRender { esp -> esp.shapes { - outline(Box.enclosing(pos1, pos2), Color.BLUE) + 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 index 84a031bdc..100fb4b38 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/BlockESP.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/BlockESP.kt @@ -21,7 +21,7 @@ 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.mc.ChunkedRegionESP.Companion.chunkedEsp import com.lambda.graphics.renderer.esp.DirectionMask import com.lambda.graphics.renderer.esp.DirectionMask.buildSideMesh import com.lambda.module.Module @@ -54,7 +54,7 @@ object BlockESP : Module( 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 outlineWidth by setting("Outline Width", 0.01f, 0.001f..1.0f, 0.001f) { searchBlocks && drawOutlines }.onValueChange(::rebuildMesh) private val outlineMode by setting("Outline Mode", DirectionMask.OutlineMode.And, "Outline mode") { searchBlocks }.onValueChange(::rebuildMesh) @@ -85,11 +85,15 @@ object BlockESP : Module( 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) + box(worldBox, outlineWidth) { + val hiddenSides = sides.inv() + hideSides(hiddenSides) + if (drawFaces) fillColor(if (useBlockColor) finalColor else faceColor) else hideFill() + if (!drawOutlines) hideOutline() + else { + outlineColor(if (useBlockColor) extractedColor else outlineColor) + outlineMode(this@BlockESP.outlineMode) + } } } } diff --git a/src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt b/src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt index bddfa2d31..9da892c8a 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt @@ -40,7 +40,7 @@ object BlockOutline : Module( 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 lineWidth by setting("Line Width", 1.0f, 0.5f..10.0f, 0.1f) { outline } + private val lineWidth by setting("Line Width", 0.01f, 0.001f..1.0f, 0.001f) { outline } private val interpolate by setting("Interpolate", true) private val throughWalls by setting("ESP", true) .onValueChange { _, to -> renderer.depthTest = !to } @@ -70,8 +70,11 @@ object BlockOutline : Module( renderer.shapes { boxes.forEach { box -> - if (fill) filled(box, fillColor) - if (outline) outline(box, outlineColor, thickness = lineWidth) + box(box, lineWidth) { + colors(fillColor, outlineColor) + if (!fill) hideFill() + if (!outline) hideOutline() + } } } 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 e479f0ef1..ed79024e2 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/EntityESP.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/EntityESP.kt @@ -22,8 +22,6 @@ import com.lambda.event.events.GuiEvent import com.lambda.event.events.RenderEvent import com.lambda.event.listener.SafeListener.Companion.listen import com.lambda.graphics.mc.ImmediateRegionESP -import com.lambda.graphics.text.SDFTextRenderer -import com.lambda.graphics.text.TextRenderer import com.lambda.module.Module import com.lambda.module.tag.ModuleTag import com.lambda.util.NamedEnum @@ -52,9 +50,6 @@ object EntityESP : Module( ) { private val esp = ImmediateRegionESP("EntityESP") - // Text renderer for testing - private val testTextRenderer by lazy { TextRenderer("fonts/FiraSans-Regular.ttf", 96f) } - private data class LabelData( val screenX: Float, val screenY: Float, @@ -65,10 +60,12 @@ object EntityESP : Module( private val pendingLabels = mutableListOf() + private val lineLength by setting("Line Length", 5f, 0f..15f, 0.1f) private val outlineWidth by setting("Outline Width", 0.15f, 0f..1f, 0.01f) private val glowWidth by setting("Glow Width", 0.25f, 0f..1f, 0.01f) private val shadowDistance by setting("Shadow Distance", 0.2f, 0f..1f, 0.01f) private val shadowAngle by setting("Shadow Angle", 135f, 0f..360f, 1f) + private val animationSpeed by setting("Animation Speed", 1f, 0.1f..5f, 0.1f) 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) @@ -118,6 +115,28 @@ object EntityESP : Module( esp.tick() val tickDelta = mc.tickDeltaF +// esp.shapes { +// val startPos = lerp(mc.tickDelta, player.prevPos, player.pos) +// lineGradient( +// startPos, +// Color.BLUE, +// startPos.offset(Direction.EAST, lineLength.toDouble()), +// Color.RED, +// 0.1f, +// marchingAnts(speed = animationSpeed) +// ) +// worldText( +// "Test FONT FONT FONT BLEHHHHH!", +// startPos.offset(Direction.EAST, +// lineLength.toDouble()), +// style = RenderBuilder.TextStyle( +// outline = RenderBuilder.TextOutline(), +// glow = RenderBuilder.TextGlow(), +// shadow = RenderBuilder.TextShadow() +// ) +// ) +// } + // Test SDF text rendering with glow and outline val eyePos = player.eyePos.add(player.rotationVector.multiply(2.0)) // 2 blocks in front // SDFTextRenderer.drawWorld( @@ -134,70 +153,19 @@ object EntityESP : Module( // seeThrough = true // ) - SDFTextRenderer.drawScreen( - text = "SDFTextRenderer Screen", - x = 20f, - y = 20f, - fontSize = 24f, - style = SDFTextRenderer.TextStyle( - color = Color.WHITE, - outline = SDFTextRenderer.TextOutline(Color.BLACK, 0.15f), - glow = SDFTextRenderer.TextGlow(Color(0, 200, 255, 180), 0.2f), - shadow = SDFTextRenderer.TextShadow(Color.YELLOW, 0.2f) - ) - ) - -// // Test regular TextRenderer - World space (slightly below SDF text) -// val textWorldPos = player.eyePos.add(player.rotationVector.multiply(2.0)).add(0.0, -0.5, 0.0) -// testTextRenderer.drawWorld( -// pos = textWorldPos, -// text = "TextRenderer World", -// color = Color.YELLOW, -// scale = 0.025f, -// centered = true, -// seeThrough = true +// SDFTextRenderer.drawScreen( +// text = "SDFTextRenderer Screen", +// x = 20f, +// y = 20f, +// fontSize = 24f, +// style = SDFTextRenderer.TextStyle( +// color = Color.WHITE, +// outline = SDFTextRenderer.TextOutline(Color.BLACK, 0.15f), +// glow = SDFTextRenderer.TextGlow(Color(0, 200, 255, 180), 0.2f), +// shadow = SDFTextRenderer.TextShadow(Color.YELLOW, 0.2f) +// ) // ) - // Test regular TextRenderer - Screen space - testTextRenderer.drawScreen( - x = 20f, - y = 100f, - text = "TextRenderer Screen", - color = Color.GREEN, - fontSize = 24f - ) - -// 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) { -// if (drawFilled) -// filled(color.setAlpha(filledAlpha)) -// if (drawOutline) -// outline( -// color.setAlpha(outlineAlpha), -// thickness = outlineWidth -// ) -// } -// } -// -// if (tracers) { -// val color = getEntityColor(entity) -// val entityPos = getInterpolatedPos(entity, tickDelta) -// val startPos = getTracerStartPos(tickDelta) -// val endPos = entityPos.add(0.0, entity.height / 2.0, 0.0) -// line(startPos, endPos) { -// color(color.setAlpha(outlineAlpha)) -// width(tracerWidth) -// if (dashedTracers) dashed(dashLength, gapLength) -// } -// } -// } -// } - esp.upload() esp.render() diff --git a/src/main/kotlin/com/lambda/module/modules/render/StorageESP.kt b/src/main/kotlin/com/lambda/module/modules/render/StorageESP.kt index 75a91baf8..46ba85f9b 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/StorageESP.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/StorageESP.kt @@ -18,18 +18,18 @@ package com.lambda.module.modules.render import com.lambda.context.SafeContext -import com.lambda.event.events.onStaticRender -import com.lambda.graphics.esp.ShapeScope +import com.lambda.event.events.onDynamicRender +import com.lambda.graphics.mc.RenderBuilder import com.lambda.graphics.renderer.esp.DirectionMask import com.lambda.graphics.renderer.esp.DirectionMask.buildSideMesh +import com.lambda.graphics.renderer.esp.DynamicAABB.Companion.dynamicBox import com.lambda.module.Module import com.lambda.module.tag.ModuleTag import com.lambda.threading.runSafe import com.lambda.util.NamedEnum import com.lambda.util.extension.blockColor +import com.lambda.util.extension.tickDelta import com.lambda.util.math.setAlpha -import com.lambda.util.world.blockEntitySearch -import com.lambda.util.world.entitySearch import net.minecraft.block.entity.BarrelBlockEntity import net.minecraft.block.entity.BlastFurnaceBlockEntity import net.minecraft.block.entity.BlockEntity @@ -53,7 +53,6 @@ object StorageESP : Module( 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) @@ -111,23 +110,27 @@ object StorageESP : Module( ) init { - onStaticRender { esp -> - blockEntitySearch(distance) + onDynamicRender { esp -> + world.blockEntities .filter { it::class in entities } - .forEach { be -> + .forEach { entity -> esp.shapes { - build(be, excludedSides(be)) + build(entity, excludedSides(entity)) } } val mineCarts = - entitySearch(distance).filter { - it::class in entities - } + world.entities + .filterIsInstance() + .filter { + it::class in entities + } val itemFrames = - entitySearch(distance).filter { - it::class in entities - } + world.entities + .filterIsInstance() + .filter { + it::class in entities + } (mineCarts + itemFrames).forEach { entity -> esp.shapes { build(entity, DirectionMask.ALL) @@ -152,16 +155,24 @@ object StorageESP : Module( } else DirectionMask.ALL } - private fun ShapeScope.build(block: BlockEntity, sides: Int) = runSafe { + private fun RenderBuilder.build(blockEntity: 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) + if (useBlockColor) blockColor(blockEntity.cachedState, blockEntity.pos) + else blockEntity.color ?: return@runSafe + boxes(blockEntity.pos, blockEntity.cachedState, outlineWidth) { + colors(color.setAlpha(facesAlpha), color.setAlpha(edgesAlpha)) + outlineMode(mode) + hideSides(sides.inv()) + } } - private fun ShapeScope.build(entity: Entity, sides: Int) = runSafe { + private fun RenderBuilder.build(entity: Entity, sides: Int) = runSafe { val color = entity.color ?: return@runSafe - box(entity, color.setAlpha(facesAlpha), color.setAlpha(edgesAlpha), sides, mode, thickness = outlineWidth) + box(entity.dynamicBox.box(mc.tickDelta) ?: return@runSafe, outlineWidth) { + colors(color.setAlpha(facesAlpha), color.setAlpha(edgesAlpha)) + outlineMode(mode) + hideSides(sides.inv()) + } } private val BlockEntity?.color diff --git a/src/main/resources/assets/lambda/shaders/core/advanced_lines.fsh b/src/main/resources/assets/lambda/shaders/core/advanced_lines.fsh index 7727eca9d..19616438b 100644 --- a/src/main/resources/assets/lambda/shaders/core/advanced_lines.fsh +++ b/src/main/resources/assets/lambda/shaders/core/advanced_lines.fsh @@ -1,50 +1,94 @@ #version 330 #moj_import +#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; +// 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() { - // 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 - + // Reconstruct line geometry from flat varyings + vec3 lineDir = normalize(v_Normal); + float halfLength = v_SegmentLength / 2.0; + + // Compute line start and end from center (which IS consistent) + vec3 lineStart = v_LineCenter - lineDir * halfLength; + vec3 lineEnd = v_LineCenter + lineDir * halfLength; + + float radius = v_LineWidth / 2.0; + + // ===== CAPSULE SDF ===== + // Project fragment position onto line to find closest point + vec3 toFragment = v_ExpandedPos - lineStart; + float projLength = dot(toFragment, lineDir); + + // Clamp to segment bounds [0, segmentLength] for capsule behavior + float clampedProj = clamp(projLength, 0.0, v_SegmentLength); + + // Closest point on line segment + vec3 closestPoint = lineStart + lineDir * clampedProj; + + // 3D distance from fragment to closest point on line + float dist3D = length(v_ExpandedPos - closestPoint); + + // SDF: distance to capsule surface (positive = outside, negative = inside) + float sdf = dist3D - radius; + + // Anti-aliasing using screen-space derivatives + float aaWidth = fwidth(sdf); + float alpha = 1.0 - smoothstep(-aaWidth, aaWidth, sdf); + + // Skip fragments outside the line if (alpha <= 0.0) { discard; } - - vec4 color = vertexColor * ColorModulator; + + // ===== 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 the CLAMPED position along the line for dash calculation + // This ensures dashes are in world-space units + float dashPos = clampedProj + animatedOffset * cycleLength; + float posInCycle = mod(dashPos, cycleLength); + + // In gap = discard + if (posInCycle > dashLength) { + discard; + } + } + + // Apply color + vec4 color = v_Color * ColorModulator; color.a *= alpha; - fragColor = apply_fog(color, sphericalVertexDistance, cylindricalVertexDistance, FogEnvironmentalStart, FogEnvironmentalEnd, FogRenderDistanceStart, FogRenderDistanceEnd, FogColor); + // Apply fog + 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 index 46e84da2a..2bc885aac 100644 --- a/src/main/resources/assets/lambda/shaders/core/advanced_lines.vsh +++ b/src/main/resources/assets/lambda/shaders/core/advanced_lines.vsh @@ -5,69 +5,80 @@ #moj_import #moj_import +// Vertex inputs in vec3 Position; in vec4 Color; -in vec3 Normal; -in float LineWidth; +in vec3 Normal; // Direction vector to other endpoint (length = segment length) +in float LineWidth; // Line width in WORLD UNITS +in vec4 Dash; // Dash parameters -out vec4 vertexColor; -noperspective out float v_LineDist; -noperspective out float v_LineWidth; -noperspective out vec2 v_DistPixels; -noperspective out float v_LineLength; +// Outputs to fragment shader - ALL are debugging-friendly +out vec4 v_Color; +out vec3 v_WorldPos; // Original unexpanded position +out vec3 v_ExpandedPos; // Expanded world position +flat out vec3 v_Normal; // Raw Normal input (same for all vertices) +flat out vec3 v_LineCenter; // Line center (computed consistently for all vertices) +flat out float v_LineWidth; // Line width +flat out float v_SegmentLength; // Computed segment length +flat out float v_IsStart; // 1.0 if start vertex, 0.0 if end +flat out vec4 v_Dash; out float sphericalVertexDistance; out float cylindricalVertexDistance; -const float VIEW_SHRINK = 1.0 - (1.0 / 256.0); - void main() { + // Determine which corner of the quad this vertex is 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; + float side = (vertexIndex == 0 || vertexIndex == 3) ? -1.0 : 1.0; - // 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; + // Normal is the same for all vertices - use it directly + float segmentLength = length(Normal); + vec3 lineDir = Normal / segmentLength; - vec2 delta = screenEnd - screenStart; - float lenPixels = length(delta); + // Line center (computed consistently for all vertices) + vec3 lineCenter = isStart ? (Position + Normal * 0.5) : (Position - Normal * 0.5); - // 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; + // Reconstruct endpoints from center + vec3 lineStart = lineCenter - lineDir * (segmentLength * 0.5); + vec3 lineEnd = lineCenter + lineDir * (segmentLength * 0.5); + vec3 thisPoint = isStart ? lineStart : lineEnd; - // 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; + // Billboard direction + vec3 toCamera = normalize(-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); + + // Expand for AA + float halfWidth = LineWidth / 2.0; + float aaPadding = LineWidth * 0.3; + float halfWidthPadded = halfWidth + aaPadding; + + // Expand vertex + vec3 perpOffset = perpDir * side * halfWidthPadded; + float longitudinal = isStart ? -1.0 : 1.0; + vec3 longOffset = lineDir * longitudinal * halfWidthPadded; + + vec3 expandedPos = thisPoint + perpOffset + longOffset; + + // Transform to clip space + gl_Position = ProjMat * ModelViewMat * vec4(expandedPos, 1.0); - // Pass coordinates for SDF - v_LineDist = side; - v_DistPixels = vec2(isStart ? -halfWidthExtended : lenPixels + halfWidthExtended, side * halfWidthExtended); - v_LineWidth = actualWidth; - v_LineLength = lenPixels; + // Pass ALL debug data + v_Color = Color; + v_WorldPos = thisPoint; // Position BEFORE expansion + v_ExpandedPos = expandedPos; // Position AFTER expansion + v_Normal = Normal; // Raw Normal (flat - same for all) + v_LineCenter = lineCenter; // Line center (flat - same for all) + v_LineWidth = LineWidth; + 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/sdf_text.vsh b/src/main/resources/assets/lambda/shaders/core/sdf_text.vsh index 4e365d599..9ca9efc0f 100644 --- a/src/main/resources/assets/lambda/shaders/core/sdf_text.vsh +++ b/src/main/resources/assets/lambda/shaders/core/sdf_text.vsh @@ -4,9 +4,14 @@ #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; out vec2 texCoord0; out vec4 vertexColor; @@ -14,11 +19,43 @@ out float sphericalVertexDistance; out float cylindricalVertexDistance; void main() { - gl_Position = ProjMat * ModelViewMat * vec4(Position, 1.0); + 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); texCoord0 = UV0; vertexColor = Color; - sphericalVertexDistance = fog_spherical_distance(Position); - cylindricalVertexDistance = fog_cylindrical_distance(Position); + 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/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/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..5af0a8126 100644 --- a/src/main/resources/lambda.accesswidener +++ b/src/main/resources/lambda.accesswidener @@ -72,6 +72,12 @@ transitive-accessible class net/minecraft/client/gui/screen/SplashOverlay$LogoTe 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; From f4980a93a1fdfdebd1466a21953fcf49d57c5f79 Mon Sep 17 00:00:00 2001 From: beanbag44 <107891830+beanbag44@users.noreply.github.com> Date: Sat, 17 Jan 2026 12:38:28 +0000 Subject: [PATCH 07/26] cleanup --- .../com/lambda/event/events/RenderEvent.kt | 8 +- .../kotlin/com/lambda/graphics/RenderMain.kt | 8 +- .../com/lambda/graphics/mc/BoxBuilder.kt | 2 +- .../com/lambda/graphics/mc/RegionRenderer.kt | 1 - .../com/lambda/graphics/mc/RenderBuilder.kt | 4 +- .../ChunkedRenderer.kt} | 32 +-- .../ImmediateRenderer.kt} | 24 +- .../TickedRenderer.kt} | 31 ++- .../{renderer/esp => util}/DirectionMask.kt | 2 +- .../{renderer/esp => util}/DynamicAABB.kt | 2 +- .../construction/simulation/Simulation.kt | 4 +- .../simulation/context/BreakContext.kt | 4 +- .../simulation/context/InteractContext.kt | 4 +- .../simulation/result/Drawable.kt | 4 +- .../simulation/result/results/BreakResult.kt | 14 +- .../result/results/GenericResult.kt | 8 +- .../result/results/InteractResult.kt | 8 +- .../simulation/result/results/PreSimResult.kt | 13 +- .../lambda/module/modules/debug/RenderTest.kt | 2 +- .../module/modules/movement/BackTrack.kt | 2 +- .../lambda/module/modules/movement/Blink.kt | 2 +- .../lambda/module/modules/render/BlockESP.kt | 6 +- .../module/modules/render/BlockOutline.kt | 20 +- .../lambda/module/modules/render/EntityESP.kt | 4 +- .../lambda/module/modules/render/Particles.kt | 219 ------------------ .../module/modules/render/StorageESP.kt | 6 +- .../lambda/shaders/fragment/particles.glsl | 12 - .../lambda/shaders/vertex/particles.glsl | 19 -- 28 files changed, 117 insertions(+), 348 deletions(-) rename src/main/kotlin/com/lambda/graphics/mc/{ChunkedRegionESP.kt => renderer/ChunkedRenderer.kt} (90%) rename src/main/kotlin/com/lambda/graphics/mc/{ImmediateRegionESP.kt => renderer/ImmediateRenderer.kt} (84%) rename src/main/kotlin/com/lambda/graphics/mc/{TransientRegionESP.kt => renderer/TickedRenderer.kt} (82%) rename src/main/kotlin/com/lambda/graphics/{renderer/esp => util}/DirectionMask.kt (98%) rename src/main/kotlin/com/lambda/graphics/{renderer/esp => util}/DynamicAABB.kt (97%) delete mode 100644 src/main/kotlin/com/lambda/module/modules/render/Particles.kt delete mode 100644 src/main/resources/assets/lambda/shaders/fragment/particles.glsl delete mode 100644 src/main/resources/assets/lambda/shaders/vertex/particles.glsl diff --git a/src/main/kotlin/com/lambda/event/events/RenderEvent.kt b/src/main/kotlin/com/lambda/event/events/RenderEvent.kt index 24d8fa73f..d409c5253 100644 --- a/src/main/kotlin/com/lambda/event/events/RenderEvent.kt +++ b/src/main/kotlin/com/lambda/event/events/RenderEvent.kt @@ -23,13 +23,13 @@ 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.ImmediateRegionESP -import com.lambda.graphics.mc.TransientRegionESP +import com.lambda.graphics.mc.renderer.ImmediateRenderer +import com.lambda.graphics.mc.renderer.TickedRenderer -fun Any.onStaticRender(block: SafeContext.(TransientRegionESP) -> Unit) = +fun Any.onStaticRender(block: SafeContext.(TickedRenderer) -> Unit) = listen { block(RenderMain.staticESP) } -fun Any.onDynamicRender(block: SafeContext.(ImmediateRegionESP) -> Unit) = +fun Any.onDynamicRender(block: SafeContext.(ImmediateRenderer) -> Unit) = listen { block(RenderMain.dynamicESP) } sealed class RenderEvent { diff --git a/src/main/kotlin/com/lambda/graphics/RenderMain.kt b/src/main/kotlin/com/lambda/graphics/RenderMain.kt index 92c2237ac..73bc17e23 100644 --- a/src/main/kotlin/com/lambda/graphics/RenderMain.kt +++ b/src/main/kotlin/com/lambda/graphics/RenderMain.kt @@ -24,8 +24,8 @@ 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.ImmediateRegionESP -import com.lambda.graphics.mc.TransientRegionESP +import com.lambda.graphics.mc.renderer.ImmediateRenderer +import com.lambda.graphics.mc.renderer.TickedRenderer import net.minecraft.util.math.Vec3d import org.joml.Matrix4f import org.joml.Vector2f @@ -33,10 +33,10 @@ import org.joml.Vector4f object RenderMain { @JvmStatic - val staticESP = TransientRegionESP("Static") + val staticESP = TickedRenderer("Static") @JvmStatic - val dynamicESP = ImmediateRegionESP("Dynamic") + val dynamicESP = ImmediateRenderer("Dynamic") val projectionMatrix = Matrix4f() val modelViewMatrix diff --git a/src/main/kotlin/com/lambda/graphics/mc/BoxBuilder.kt b/src/main/kotlin/com/lambda/graphics/mc/BoxBuilder.kt index 9442f91bb..7c7202f35 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/BoxBuilder.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/BoxBuilder.kt @@ -17,7 +17,7 @@ package com.lambda.graphics.mc -import com.lambda.graphics.renderer.esp.DirectionMask +import com.lambda.graphics.util.DirectionMask import net.minecraft.util.math.Direction import java.awt.Color diff --git a/src/main/kotlin/com/lambda/graphics/mc/RegionRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/RegionRenderer.kt index a5b46eba4..371955fc4 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/RegionRenderer.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/RegionRenderer.kt @@ -31,7 +31,6 @@ import java.util.* * methods to render them within a RenderPass. */ class RegionRenderer { - // Dedicated GPU buffers for faces, edges, and text private var faceVertexBuffer: GpuBuffer? = null private var edgeVertexBuffer: GpuBuffer? = null diff --git a/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt b/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt index 0ccd53c60..5a2c4a448 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt @@ -18,10 +18,10 @@ package com.lambda.graphics.mc import com.lambda.context.SafeContext -import com.lambda.graphics.renderer.esp.DirectionMask -import com.lambda.graphics.renderer.esp.DirectionMask.hasDirection import com.lambda.graphics.text.FontHandler import com.lambda.graphics.text.SDFFontAtlas +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.util.math.BlockPos diff --git a/src/main/kotlin/com/lambda/graphics/mc/ChunkedRegionESP.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt similarity index 90% rename from src/main/kotlin/com/lambda/graphics/mc/ChunkedRegionESP.kt rename to src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt index 726f6141a..b71c00e0c 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/ChunkedRegionESP.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.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,7 +15,7 @@ * along with this program. If not, see . */ -package com.lambda.graphics.mc +package com.lambda.graphics.mc.renderer import com.lambda.Lambda.mc import com.lambda.event.events.RenderEvent @@ -23,12 +23,17 @@ 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.LambdaRenderPipelines +import com.lambda.graphics.mc.RegionRenderer +import com.lambda.graphics.mc.RenderBuilder import com.lambda.graphics.text.FontHandler 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 com.mojang.blaze3d.buffers.GpuBuffer import com.mojang.blaze3d.systems.RenderSystem import net.minecraft.util.math.Vec3d import net.minecraft.world.World @@ -36,6 +41,7 @@ import net.minecraft.world.chunk.WorldChunk import org.joml.Matrix4f import org.joml.Vector3f import org.joml.Vector4f +import org.lwjgl.system.MemoryUtil import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentLinkedDeque @@ -52,7 +58,7 @@ import java.util.concurrent.ConcurrentLinkedDeque * @param depthTest Whether to use depth testing * @param update The update function called for each block position */ -class ChunkedRegionESP( +class ChunkedRenderer( owner: Module, name: String, private val depthTest: Boolean = false, @@ -112,7 +118,7 @@ class ChunkedRegionESP( val activeChunks = chunkMap.values.filter { it.renderer.hasData() } if (activeChunks.isEmpty()) return - val modelViewMatrix = com.lambda.graphics.RenderMain.modelViewMatrix + val modelViewMatrix = RenderMain.modelViewMatrix // Pre-compute all transforms BEFORE starting render passes val chunkTransforms = activeChunks.map { chunkData -> @@ -129,7 +135,7 @@ class ChunkedRegionESP( } // Render Faces - RegionRenderer.createRenderPass("ChunkedESP Faces", depthTest)?.use { pass -> + RegionRenderer.Companion.createRenderPass("ChunkedESP Faces", depthTest)?.use { pass -> val pipeline = if (depthTest) LambdaRenderPipelines.ESP_QUADS else LambdaRenderPipelines.ESP_QUADS_THROUGH @@ -143,7 +149,7 @@ class ChunkedRegionESP( } // Render Edges - RegionRenderer.createRenderPass("ChunkedESP Edges", depthTest)?.use { pass -> + RegionRenderer.Companion.createRenderPass("ChunkedESP Edges", depthTest)?.use { pass -> val pipeline = if (depthTest) LambdaRenderPipelines.ESP_LINES else LambdaRenderPipelines.ESP_LINES_THROUGH @@ -167,7 +173,7 @@ class ChunkedRegionESP( if (textureView != null && sampler != null) { val sdfParams = createSDFParamsBuffer() if (sdfParams != null) { - RegionRenderer.createRenderPass("ChunkedESP Text", depthTest)?.use { pass -> + RegionRenderer.Companion.createRenderPass("ChunkedESP Text", depthTest)?.use { pass -> val pipeline = if (depthTest) LambdaRenderPipelines.SDF_TEXT else LambdaRenderPipelines.SDF_TEXT_THROUGH @@ -187,20 +193,20 @@ class ChunkedRegionESP( } } - private fun createSDFParamsBuffer(): com.mojang.blaze3d.buffers.GpuBuffer? { + private fun createSDFParamsBuffer(): GpuBuffer? { val device = RenderSystem.getDevice() - val buffer = org.lwjgl.system.MemoryUtil.memAlloc(16) + val buffer = MemoryUtil.memAlloc(16) return try { buffer.putFloat(0.5f) buffer.putFloat(0.1f) buffer.putFloat(0.2f) buffer.putFloat(0.15f) buffer.flip() - device.createBuffer({ "SDFParams" }, com.mojang.blaze3d.buffers.GpuBuffer.USAGE_UNIFORM, buffer) + device.createBuffer({ "SDFParams" }, GpuBuffer.USAGE_UNIFORM, buffer) } catch (e: Exception) { null } finally { - org.lwjgl.system.MemoryUtil.memFree(buffer) + MemoryUtil.memFree(buffer) } } @@ -292,8 +298,8 @@ class ChunkedRegionESP( name: String, depthTest: Boolean = false, update: RenderBuilder.(World, FastVector) -> Unit - ): ChunkedRegionESP { - return ChunkedRegionESP(this, name, depthTest, update) + ): ChunkedRenderer { + return ChunkedRenderer(this, name, depthTest, update) } } } diff --git a/src/main/kotlin/com/lambda/graphics/mc/ImmediateRegionESP.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt similarity index 84% rename from src/main/kotlin/com/lambda/graphics/mc/ImmediateRegionESP.kt rename to src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt index 288e0c222..5dc4fce3c 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/ImmediateRegionESP.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt @@ -15,9 +15,15 @@ * along with this program. If not, see . */ -package com.lambda.graphics.mc +package com.lambda.graphics.mc.renderer import com.lambda.Lambda.mc +import com.lambda.graphics.RenderMain +import com.lambda.graphics.mc.LambdaRenderPipelines +import com.lambda.graphics.mc.RegionRenderer +import com.lambda.graphics.mc.RenderBuilder +import com.lambda.graphics.text.SDFFontAtlas +import com.mojang.blaze3d.buffers.GpuBuffer import com.mojang.blaze3d.systems.RenderSystem import net.minecraft.util.math.Vec3d import org.joml.Matrix4f @@ -32,7 +38,7 @@ import org.lwjgl.system.MemoryUtil * 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 ImmediateRegionESP(val name: String, var depthTest: Boolean = false) { +class ImmediateRenderer(val name: String, var depthTest: Boolean = false) { private val renderer = RegionRenderer() // Current frame builder (being populated this frame) @@ -73,7 +79,7 @@ class ImmediateRegionESP(val name: String, var depthTest: Boolean = false) { } // Font atlas used for current text rendering - private var currentFontAtlas: com.lambda.graphics.text.SDFFontAtlas? = null + private var currentFontAtlas: SDFFontAtlas? = null /** Close and release all GPU resources. */ fun close() { @@ -88,7 +94,7 @@ class ImmediateRegionESP(val name: String, var depthTest: Boolean = false) { fun render() { if (!renderer.hasData()) return - val modelViewMatrix = com.lambda.graphics.RenderMain.modelViewMatrix + val modelViewMatrix = RenderMain.modelViewMatrix val dynamicTransform = RenderSystem.getDynamicUniforms() .write( @@ -99,7 +105,7 @@ class ImmediateRegionESP(val name: String, var depthTest: Boolean = false) { ) // Render Faces - RegionRenderer.createRenderPass("$name Faces", depthTest)?.use { pass -> + RegionRenderer.Companion.createRenderPass("$name Faces", depthTest)?.use { pass -> val pipeline = if (depthTest) LambdaRenderPipelines.ESP_QUADS else LambdaRenderPipelines.ESP_QUADS_THROUGH @@ -110,7 +116,7 @@ class ImmediateRegionESP(val name: String, var depthTest: Boolean = false) { } // Render Edges - RegionRenderer.createRenderPass("$name Edges", depthTest)?.use { pass -> + RegionRenderer.Companion.createRenderPass("$name Edges", depthTest)?.use { pass -> val pipeline = if (depthTest) LambdaRenderPipelines.ESP_LINES else LambdaRenderPipelines.ESP_LINES_THROUGH @@ -130,7 +136,7 @@ class ImmediateRegionESP(val name: String, var depthTest: Boolean = false) { if (textureView != null && sampler != null) { val sdfParams = createSDFParamsBuffer() if (sdfParams != null) { - RegionRenderer.createRenderPass("$name Text", depthTest)?.use { pass -> + RegionRenderer.Companion.createRenderPass("$name Text", depthTest)?.use { pass -> val pipeline = if (depthTest) LambdaRenderPipelines.SDF_TEXT else LambdaRenderPipelines.SDF_TEXT_THROUGH @@ -151,7 +157,7 @@ class ImmediateRegionESP(val name: String, var depthTest: Boolean = false) { /** * Create SDF params uniform buffer with default values. */ - private fun createSDFParamsBuffer(): com.mojang.blaze3d.buffers.GpuBuffer? { + private fun createSDFParamsBuffer(): GpuBuffer? { val device = RenderSystem.getDevice() val buffer = MemoryUtil.memAlloc(16) return try { @@ -160,7 +166,7 @@ class ImmediateRegionESP(val name: String, var depthTest: Boolean = false) { buffer.putFloat(0.2f) // GlowRadius buffer.putFloat(0.15f) // ShadowSoftness buffer.flip() - device.createBuffer({ "SDFParams" }, com.mojang.blaze3d.buffers.GpuBuffer.USAGE_UNIFORM, buffer) + device.createBuffer({ "SDFParams" }, GpuBuffer.USAGE_UNIFORM, buffer) } catch (_: Exception) { null } finally { diff --git a/src/main/kotlin/com/lambda/graphics/mc/TransientRegionESP.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.kt similarity index 82% rename from src/main/kotlin/com/lambda/graphics/mc/TransientRegionESP.kt rename to src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.kt index cf8cc1117..808246104 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/TransientRegionESP.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.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,14 +15,21 @@ * along with this program. If not, see . */ -package com.lambda.graphics.mc +package com.lambda.graphics.mc.renderer import com.lambda.Lambda.mc +import com.lambda.graphics.RenderMain +import com.lambda.graphics.mc.LambdaRenderPipelines +import com.lambda.graphics.mc.RegionRenderer +import com.lambda.graphics.mc.RenderBuilder +import com.lambda.graphics.text.SDFFontAtlas +import com.mojang.blaze3d.buffers.GpuBuffer import com.mojang.blaze3d.systems.RenderSystem import net.minecraft.util.math.Vec3d import org.joml.Matrix4f import org.joml.Vector3f import org.joml.Vector4f +import org.lwjgl.system.MemoryUtil /** * Modern replacement for the legacy Treed system. Handles geometry that is cleared and rebuilt @@ -31,7 +38,7 @@ import org.joml.Vector4f * 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 TransientRegionESP(val name: String, var depthTest: Boolean = false) { +class TickedRenderer(val name: String, var depthTest: Boolean = false) { private val renderer = RegionRenderer() private var renderBuilder: RenderBuilder? = null @@ -66,7 +73,7 @@ class TransientRegionESP(val name: String, var depthTest: Boolean = false) { } // Font atlas used for current text rendering - private var currentFontAtlas: com.lambda.graphics.text.SDFFontAtlas? = null + private var currentFontAtlas: SDFFontAtlas? = null /** Close and release all GPU resources. */ fun close() { @@ -83,7 +90,7 @@ class TransientRegionESP(val name: String, var depthTest: Boolean = false) { val tickCamera = tickCameraPos ?: return if (!renderer.hasData()) return - val modelViewMatrix = com.lambda.graphics.RenderMain.modelViewMatrix + 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) @@ -96,7 +103,7 @@ class TransientRegionESP(val name: String, var depthTest: Boolean = false) { .write(modelView, Vector4f(1f, 1f, 1f, 1f), Vector3f(0f, 0f, 0f), Matrix4f()) // Render Faces - RegionRenderer.createRenderPass("$name Faces", depthTest)?.use { pass -> + RegionRenderer.Companion.createRenderPass("$name Faces", depthTest)?.use { pass -> val pipeline = if (depthTest) LambdaRenderPipelines.ESP_QUADS else LambdaRenderPipelines.ESP_QUADS_THROUGH @@ -107,7 +114,7 @@ class TransientRegionESP(val name: String, var depthTest: Boolean = false) { } // Render Edges - RegionRenderer.createRenderPass("$name Edges", depthTest)?.use { pass -> + RegionRenderer.Companion.createRenderPass("$name Edges", depthTest)?.use { pass -> val pipeline = if (depthTest) LambdaRenderPipelines.ESP_LINES else LambdaRenderPipelines.ESP_LINES_THROUGH @@ -127,7 +134,7 @@ class TransientRegionESP(val name: String, var depthTest: Boolean = false) { if (textureView != null && sampler != null) { val sdfParams = createSDFParamsBuffer() if (sdfParams != null) { - RegionRenderer.createRenderPass("$name Text", depthTest)?.use { pass -> + RegionRenderer.Companion.createRenderPass("$name Text", depthTest)?.use { pass -> val pipeline = if (depthTest) LambdaRenderPipelines.SDF_TEXT else LambdaRenderPipelines.SDF_TEXT_THROUGH @@ -145,20 +152,20 @@ class TransientRegionESP(val name: String, var depthTest: Boolean = false) { } } - private fun createSDFParamsBuffer(): com.mojang.blaze3d.buffers.GpuBuffer? { + private fun createSDFParamsBuffer(): GpuBuffer? { val device = RenderSystem.getDevice() - val buffer = org.lwjgl.system.MemoryUtil.memAlloc(16) + val buffer = MemoryUtil.memAlloc(16) return try { buffer.putFloat(0.5f) buffer.putFloat(0.1f) buffer.putFloat(0.2f) buffer.putFloat(0.15f) buffer.flip() - device.createBuffer({ "SDFParams" }, com.mojang.blaze3d.buffers.GpuBuffer.USAGE_UNIFORM, buffer) + device.createBuffer({ "SDFParams" }, GpuBuffer.USAGE_UNIFORM, buffer) } catch (e: Exception) { null } finally { - org.lwjgl.system.MemoryUtil.memFree(buffer) + MemoryUtil.memFree(buffer) } } } 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 97% rename from src/main/kotlin/com/lambda/graphics/renderer/esp/DynamicAABB.kt rename to src/main/kotlin/com/lambda/graphics/util/DynamicAABB.kt index 2abf3f3be..c8f01e6b0 100644 --- a/src/main/kotlin/com/lambda/graphics/renderer/esp/DynamicAABB.kt +++ b/src/main/kotlin/com/lambda/graphics/util/DynamicAABB.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.extension.prevPos import com.lambda.util.math.lerp 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 c4b8f9dca..620dba0d0 100644 --- a/src/main/kotlin/com/lambda/interaction/construction/simulation/Simulation.kt +++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/Simulation.kt @@ -19,7 +19,7 @@ package com.lambda.interaction.construction.simulation import com.lambda.context.Automated import com.lambda.context.SafeContext -import com.lambda.graphics.mc.TransientRegionESP +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 @@ -63,7 +63,7 @@ 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) { + override fun render(esp: TickedRenderer) { esp.shapes { box(Vec3d.ofBottomCenter(pos).playerBox(), 1.5f) { 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 2f7300435..3635daf5e 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,7 @@ package com.lambda.interaction.construction.simulation.context import com.lambda.context.Automated -import com.lambda.graphics.mc.TransientRegionESP +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,7 +59,7 @@ data class BreakContext( override val sorter get() = breakConfig.sorter - override fun render(esp: TransientRegionESP) { + override fun render(esp: TickedRenderer) { esp.shapes { box(blockPos, 1.5f) { 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 807e136e1..178406dc2 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,7 @@ package com.lambda.interaction.construction.simulation.context import com.lambda.context.Automated -import com.lambda.graphics.mc.TransientRegionESP +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,7 +46,7 @@ data class InteractContext( override val sorter get() = interactConfig.sorter - override fun render(esp: TransientRegionESP) { + override fun render(esp: TickedRenderer) { esp.shapes { val box = with(hitResult.pos) { Box( 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..bd12ff89b 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,11 @@ package com.lambda.interaction.construction.simulation.result -import com.lambda.graphics.mc.TransientRegionESP +import com.lambda.graphics.mc.renderer.TickedRenderer /** * Represents a [BuildResult] that can be rendered in-game. */ interface Drawable { - fun render(esp: TransientRegionESP) + fun render(esp: TickedRenderer) } 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 87b3c4212..f86701a8a 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,8 @@ 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.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,7 +55,7 @@ sealed class BreakResult : BuildResult() { ) : Contextual, Drawable, BreakResult() { override val rank = Rank.BreakSuccess - override fun render(esp: TransientRegionESP) { + override fun render(esp: TickedRenderer) { context.render(esp) } } @@ -72,7 +72,7 @@ sealed class BreakResult : BuildResult() { override val rank = Rank.BreakNotExposed private val color = Color(46, 0, 0, 30) - override fun render(esp: TransientRegionESP) { + override fun render(esp: TickedRenderer) { esp.shapes { box(pos, 1.5f) { allColors(color) @@ -125,7 +125,7 @@ sealed class BreakResult : BuildResult() { override val rank = Rank.BreakSubmerge private val color = Color(114, 27, 255, 100) - override fun render(esp: TransientRegionESP) { + override fun render(esp: TickedRenderer) { esp.shapes { box(pos, 1.5f) { allColors(color) @@ -145,7 +145,7 @@ sealed class BreakResult : BuildResult() { override val rank = Rank.BreakIsBlockedByFluid private val color = Color(50, 12, 112, 100) - override fun render(esp: TransientRegionESP) { + override fun render(esp: TickedRenderer) { esp.shapes { val center = pos.toCenterPos() val box = Box( @@ -171,7 +171,7 @@ sealed class BreakResult : BuildResult() { override val goal = GoalInverted(GoalBlock(pos)) - override fun render(esp: TransientRegionESP) { + override fun render(esp: TickedRenderer) { esp.shapes { box(pos, 1.5f) { 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 05ff445ff..a7e914fdc 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,7 @@ 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.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,7 +53,7 @@ sealed class GenericResult : BuildResult() { override val rank = Rank.NotVisible private val color = Color(46, 0, 0, 80) - override fun render(esp: TransientRegionESP) { + override fun render(esp: TickedRenderer) { esp.shapes { val box = with(pos) { Box( @@ -104,7 +104,7 @@ sealed class GenericResult : BuildResult() { neededSelection.transferByTask(HotbarContainer)?.execute(task) } - override fun render(esp: TransientRegionESP) { + override fun render(esp: TickedRenderer) { esp.shapes { val center = pos.toCenterPos() val box = Box( @@ -139,7 +139,7 @@ sealed class GenericResult : BuildResult() { override val goal = GoalNear(pos, 3) - override fun render(esp: TransientRegionESP) { + override fun render(esp: TickedRenderer) { esp.shapes { val center = pos.toCenterPos() val box = Box( 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 c81e58f43..3e0e3ddb2 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,7 @@ 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.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,7 +57,7 @@ sealed class InteractResult : BuildResult() { ) : Contextual, Drawable, InteractResult() { override val rank = Rank.PlaceSuccess - override fun render(esp: TransientRegionESP) { + override fun render(esp: TickedRenderer) { context.render(esp) } } @@ -82,7 +82,7 @@ sealed class InteractResult : BuildResult() { override val rank = Rank.PlaceNoIntegrity private val color = Color(252, 3, 3, 100) - override fun render(esp: TransientRegionESP) { + override fun render(esp: TickedRenderer) { esp.shapes { val box = with(simulated.hitPos) { Box( @@ -123,7 +123,7 @@ sealed class InteractResult : BuildResult() { override val rank = Rank.PlaceBlockedByEntity private val color = Color(252, 3, 3, 100) - override fun render(esp: TransientRegionESP) { + override fun render(esp: TickedRenderer) { esp.shapes { val box = with(hitPos) { Box( 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 32ac6a8fd..4a4d357fb 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,13 +18,12 @@ package com.lambda.interaction.construction.simulation.result.results import baritone.api.pathing.goals.GoalBlock -import com.lambda.graphics.mc.TransientRegionESP +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 import com.lambda.interaction.construction.simulation.result.Navigable import com.lambda.interaction.construction.simulation.result.Rank -import com.lambda.util.ChatUtils.colors import net.minecraft.block.BlockState import net.minecraft.util.math.BlockPos import java.awt.Color @@ -56,7 +55,7 @@ sealed class PreSimResult : BuildResult() { override val goal = GoalBlock(pos) - override fun render(esp: TransientRegionESP) { + override fun render(esp: TickedRenderer) { esp.shapes { box(pos, 1.5f) { allColors(color) @@ -82,7 +81,7 @@ sealed class PreSimResult : BuildResult() { override val rank = Rank.BreakRestricted private val color = Color(255, 0, 0, 100) - override fun render(esp: TransientRegionESP) { + override fun render(esp: TickedRenderer) { esp.shapes { box(pos, 1.5f) { allColors(color) @@ -104,7 +103,7 @@ sealed class PreSimResult : BuildResult() { override val rank get() = Rank.BreakNoPermission private val color = Color(255, 0, 0, 100) - override fun render(esp: TransientRegionESP) { + override fun render(esp: TickedRenderer) { esp.shapes { box(pos, 1.5f) { allColors(color) @@ -124,7 +123,7 @@ sealed class PreSimResult : BuildResult() { override val rank = Rank.OutOfWorld private val color = Color(3, 148, 252, 100) - override fun render(esp: TransientRegionESP) { + override fun render(esp: TickedRenderer) { esp.shapes { box(pos, 1.5f) { allColors(color) @@ -146,7 +145,7 @@ sealed class PreSimResult : BuildResult() { override val rank = Rank.Unbreakable private val color = Color(11, 11, 11, 100) - override fun render(esp: TransientRegionESP) { + override fun render(esp: TickedRenderer) { esp.shapes { box(pos, 1.5f) { allColors(color) 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 ad940ee74..3b99f7bb2 100644 --- a/src/main/kotlin/com/lambda/module/modules/debug/RenderTest.kt +++ b/src/main/kotlin/com/lambda/module/modules/debug/RenderTest.kt @@ -19,7 +19,7 @@ package com.lambda.module.modules.debug import com.lambda.event.events.onDynamicRender import com.lambda.event.events.onStaticRender -import com.lambda.graphics.renderer.esp.DynamicAABB.Companion.dynamicBox +import com.lambda.graphics.util.DynamicAABB.Companion.dynamicBox import com.lambda.module.Module import com.lambda.module.tag.ModuleTag import com.lambda.util.extension.tickDelta 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 f5399d7f3..50e381efa 100644 --- a/src/main/kotlin/com/lambda/module/modules/movement/BackTrack.kt +++ b/src/main/kotlin/com/lambda/module/modules/movement/BackTrack.kt @@ -23,7 +23,7 @@ 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.renderer.esp.DynamicAABB +import com.lambda.graphics.util.DynamicAABB import com.lambda.gui.components.ClickGuiLayout import com.lambda.module.Module import com.lambda.module.modules.combat.KillAura 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 72570e3f0..e2fa86a73 100644 --- a/src/main/kotlin/com/lambda/module/modules/movement/Blink.kt +++ b/src/main/kotlin/com/lambda/module/modules/movement/Blink.kt @@ -22,7 +22,7 @@ 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.renderer.esp.DynamicAABB +import com.lambda.graphics.util.DynamicAABB import com.lambda.gui.components.ClickGuiLayout import com.lambda.module.Module import com.lambda.module.modules.combat.KillAura diff --git a/src/main/kotlin/com/lambda/module/modules/render/BlockESP.kt b/src/main/kotlin/com/lambda/module/modules/render/BlockESP.kt index 100fb4b38..164b46c9a 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/BlockESP.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/BlockESP.kt @@ -21,9 +21,9 @@ 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.mc.ChunkedRegionESP.Companion.chunkedEsp -import com.lambda.graphics.renderer.esp.DirectionMask -import com.lambda.graphics.renderer.esp.DirectionMask.buildSideMesh +import com.lambda.graphics.mc.renderer.ChunkedRenderer.Companion.chunkedEsp +import com.lambda.graphics.util.DirectionMask +import com.lambda.graphics.util.DirectionMask.buildSideMesh import com.lambda.module.Module import com.lambda.module.tag.ModuleTag import com.lambda.threading.runSafe diff --git a/src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt b/src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt index 9da892c8a..d452c8890 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt @@ -20,7 +20,7 @@ package com.lambda.module.modules.render import com.lambda.event.events.RenderEvent import com.lambda.event.events.TickEvent import com.lambda.event.listener.SafeListener.Companion.listen -import com.lambda.graphics.mc.ImmediateRegionESP +import com.lambda.graphics.mc.renderer.ImmediateRenderer import com.lambda.module.Module import com.lambda.module.tag.ModuleTag import com.lambda.util.BlockUtils.blockState @@ -45,7 +45,7 @@ object BlockOutline : Module( private val throughWalls by setting("ESP", true) .onValueChange { _, to -> renderer.depthTest = !to } - val renderer = ImmediateRegionESP("BlockOutline") + val renderer = ImmediateRenderer("BlockOutline") var previous: Pair, BlockState>? = null @@ -59,13 +59,15 @@ object BlockOutline : Module( val boxes = blockState .getOutlineShape(world, pos) .boundingBoxes - .mapIndexed { index, box -> - val offset = box.offset(pos) - val interpolated = previous?.let { previous -> - if (!interpolate || previous.second !== blockState) null - else lerp(mc.tickDelta, previous.first[index], offset) - } ?: offset - interpolated.expand(0.001) + .let { boxes -> + boxes.mapIndexed { index, box -> + val offset = box.offset(pos) + val interpolated = previous?.let { previous -> + if (!interpolate || previous.first.size < boxes.size) null + else lerp(mc.tickDelta, previous.first[index], offset) + } ?: offset + interpolated.expand(0.001) + } } renderer.shapes { 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 ed79024e2..7325aa2ce 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/EntityESP.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/EntityESP.kt @@ -21,7 +21,7 @@ import com.lambda.context.SafeContext import com.lambda.event.events.GuiEvent import com.lambda.event.events.RenderEvent import com.lambda.event.listener.SafeListener.Companion.listen -import com.lambda.graphics.mc.ImmediateRegionESP +import com.lambda.graphics.mc.renderer.ImmediateRenderer import com.lambda.module.Module import com.lambda.module.tag.ModuleTag import com.lambda.util.NamedEnum @@ -48,7 +48,7 @@ object EntityESP : Module( description = "Highlight entities with smooth interpolated rendering", tag = ModuleTag.RENDER ) { - private val esp = ImmediateRegionESP("EntityESP") + private val esp = ImmediateRenderer("EntityESP") private data class LabelData( val screenX: Float, diff --git a/src/main/kotlin/com/lambda/module/modules/render/Particles.kt b/src/main/kotlin/com/lambda/module/modules/render/Particles.kt deleted file mode 100644 index fdf06c0cc..000000000 --- a/src/main/kotlin/com/lambda/module/modules/render/Particles.kt +++ /dev/null @@ -1,219 +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.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.tickDelta -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 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.tickDelta - 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.tickDelta, 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/StorageESP.kt b/src/main/kotlin/com/lambda/module/modules/render/StorageESP.kt index 46ba85f9b..f0260d38b 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/StorageESP.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/StorageESP.kt @@ -20,9 +20,9 @@ package com.lambda.module.modules.render import com.lambda.context.SafeContext import com.lambda.event.events.onDynamicRender import com.lambda.graphics.mc.RenderBuilder -import com.lambda.graphics.renderer.esp.DirectionMask -import com.lambda.graphics.renderer.esp.DirectionMask.buildSideMesh -import com.lambda.graphics.renderer.esp.DynamicAABB.Companion.dynamicBox +import com.lambda.graphics.util.DirectionMask +import com.lambda.graphics.util.DirectionMask.buildSideMesh +import com.lambda.graphics.util.DynamicAABB.Companion.dynamicBox import com.lambda.module.Module import com.lambda.module.tag.ModuleTag import com.lambda.threading.runSafe 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/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 From 1c16e887c84a168083a6a2f3ceced7deeb215ad7 Mon Sep 17 00:00:00 2001 From: beanbag44 <107891830+beanbag44@users.noreply.github.com> Date: Sun, 18 Jan 2026 18:19:42 +0000 Subject: [PATCH 08/26] screen rendering --- .../com/lambda/graphics/mc/ImGuiWorldText.kt | 172 ------ .../graphics/mc/LambdaRenderPipelines.kt | 49 ++ .../lambda/graphics/mc/LambdaVertexFormats.kt | 33 + .../com/lambda/graphics/mc/LineDashStyle.kt | 58 ++ .../com/lambda/graphics/mc/RegionRenderer.kt | 111 +++- .../graphics/mc/RegionVertexCollector.kt | 219 +++++++ .../com/lambda/graphics/mc/RenderBuilder.kt | 573 +++++++++--------- .../graphics/mc/renderer/ChunkedRenderer.kt | 111 ++-- .../graphics/mc/renderer/ImmediateRenderer.kt | 82 ++- .../graphics/mc/renderer/RendererUtils.kt | 134 ++++ .../graphics/mc/renderer/TickedRenderer.kt | 89 ++- .../modules/debug/RendererTestModule.kt | 320 ++++++++++ .../lambda/module/modules/render/EntityESP.kt | 8 +- .../lambda/shaders/core/screen_lines.fsh | 78 +++ .../lambda/shaders/core/screen_lines.vsh | 67 ++ .../lambda/shaders/core/screen_sdf_text.fsh | 67 ++ .../lambda/shaders/core/screen_sdf_text.vsh | 21 + 17 files changed, 1605 insertions(+), 587 deletions(-) delete mode 100644 src/main/kotlin/com/lambda/graphics/mc/ImGuiWorldText.kt create mode 100644 src/main/kotlin/com/lambda/graphics/mc/renderer/RendererUtils.kt create mode 100644 src/main/kotlin/com/lambda/module/modules/debug/RendererTestModule.kt create mode 100644 src/main/resources/assets/lambda/shaders/core/screen_lines.fsh create mode 100644 src/main/resources/assets/lambda/shaders/core/screen_lines.vsh create mode 100644 src/main/resources/assets/lambda/shaders/core/screen_sdf_text.fsh create mode 100644 src/main/resources/assets/lambda/shaders/core/screen_sdf_text.vsh 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/LambdaRenderPipelines.kt b/src/main/kotlin/com/lambda/graphics/mc/LambdaRenderPipelines.kt index 48907b2e5..a0d01fa09 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/LambdaRenderPipelines.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/LambdaRenderPipelines.kt @@ -201,4 +201,53 @@ object LambdaRenderPipelines : Loadable { ) .build() ) + + // ============================================================================ + // Screen-Space Pipelines + // ============================================================================ + + /** + * Pipeline for screen-space lines. + * Uses a custom vertex format with 2D direction for perpendicular offset calculation. + */ + 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(false) + .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) + .withCull(false) + .withVertexFormat( + LambdaVertexFormats.SCREEN_LINE_FORMAT, + VertexFormat.DrawMode.QUADS + ) + .build() + ) + + /** + * Pipeline for screen-space SDF text rendering. + * Uses custom SDF shader with SDFParams for proper anti-aliased text with effects. + */ + 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") + .withUniform("SDFParams", UniformType.UNIFORM_BUFFER) + .withBlend(BlendFunction.TRANSLUCENT) + .withDepthWrite(false) + .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) + .withCull(false) + .withVertexFormat( + VertexFormats.POSITION_TEXTURE_COLOR, + 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 index df3c616fe..ed3e8f376 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/LambdaVertexFormats.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/LambdaVertexFormats.kt @@ -127,4 +127,37 @@ object LambdaVertexFormats { .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) + ) + + /** + * Screen-space line format with dash support. + * Layout: Position (vec3), Color (vec4), Direction2D (vec2), LineWidth (float), Dash (vec4) + * + * Total size: 12 + 4 + 8 + 4 + 16 = 44 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) + */ + 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) + .build() } + diff --git a/src/main/kotlin/com/lambda/graphics/mc/LineDashStyle.kt b/src/main/kotlin/com/lambda/graphics/mc/LineDashStyle.kt index 320b70e26..e1fad6768 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/LineDashStyle.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/LineDashStyle.kt @@ -112,5 +112,63 @@ data class 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 371955fc4..6f8d8bc72 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/RegionRenderer.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/RegionRenderer.kt @@ -31,18 +31,29 @@ import java.util.* * methods to render them within a RenderPass. */ class RegionRenderer { - // Dedicated GPU buffers for faces, edges, and text + // 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 - // Index counts for draw calls + // 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 + + // 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 @@ -52,13 +63,19 @@ class RegionRenderer { */ 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() - // Assign new buffers and counts + // Cleanup old screen-space buffers + screenFaceVertexBuffer?.close() + screenEdgeVertexBuffer?.close() + screenTextVertexBuffer?.close() + + // Assign new world-space buffers and counts faceVertexBuffer = result.faces?.buffer faceIndexCount = result.faces?.indexCount ?: 0 @@ -68,7 +85,18 @@ class RegionRenderer { 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 + hasData = faceVertexBuffer != null || edgeVertexBuffer != null || textVertexBuffer != null + hasScreenData = screenFaceVertexBuffer != null || screenEdgeVertexBuffer != null || screenTextVertexBuffer != null } /** @@ -129,8 +157,71 @@ class RegionRenderer { /** 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 + + /** 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() @@ -141,6 +232,18 @@ class RegionRenderer { 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 + hasScreenData = false } /** Check if this renderer has any data to render. */ diff --git a/src/main/kotlin/com/lambda/graphics/mc/RegionVertexCollector.kt b/src/main/kotlin/com/lambda/graphics/mc/RegionVertexCollector.kt index 091a1e291..9489fad21 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/RegionVertexCollector.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/RegionVertexCollector.kt @@ -38,6 +38,11 @@ class RegionVertexCollector { 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, @@ -91,6 +96,36 @@ class RegionVertexCollector { val animationSpeed: Float = 0f // 0 = no animation ) + // ============================================================================ + // Screen-Space Vertex Types + // ============================================================================ + + /** Screen-space face vertex data (2D position + color). */ + data class ScreenFaceVertex( + val x: Float, val y: Float, + val r: Int, val g: Int, val b: Int, val a: Int + ) + + /** Screen-space edge vertex data (2D position + color + direction + width + dash). */ + 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 + ) + + /** Screen-space text vertex data (2D position + UV + color). */ + 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 + ) + /** 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)) @@ -174,6 +209,51 @@ class RegionVertexCollector { )) } + // ============================================================================ + // Screen-Space Vertex Add Methods + // ============================================================================ + + /** Add a screen-space face vertex. */ + fun addScreenFaceVertex(x: Float, y: Float, color: Color) { + screenFaceVertices.add(ScreenFaceVertex(x, y, color.red, color.green, color.blue, color.alpha)) + } + + /** Add a screen-space edge vertex (solid line). */ + fun addScreenEdgeVertex(x: Float, y: Float, color: Color, dx: Float, dy: Float, lineWidth: Float) { + screenEdgeVertices.add(ScreenEdgeVertex(x, y, color.red, color.green, color.blue, color.alpha, dx, dy, lineWidth)) + } + + /** Add a screen-space edge vertex with dash style. */ + fun addScreenEdgeVertex( + x: Float, y: Float, + color: Color, + dx: Float, dy: Float, + lineWidth: Float, + dashStyle: LineDashStyle? + ) { + if (dashStyle == null) { + addScreenEdgeVertex(x, y, color, dx, dy, lineWidth) + } 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 + ) + ) + } + } + + /** Add a screen-space text vertex. */ + fun addScreenTextVertex(x: Float, y: Float, u: Float, v: Float, r: Int, g: Int, b: Int, a: Int) { + screenTextVertices.add(ScreenTextVertex(x, y, u, v, r, g, b, a)) + } + /** * Upload collected data to GPU buffers. Must be called on the main/render thread. * @@ -328,6 +408,145 @@ class RegionVertexCollector { 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 + BufferAllocator(vertices.size * 12).use { allocator -> + val builder = BufferBuilder( + allocator, + VertexFormat.DrawMode.QUADS, + VertexFormats.POSITION_COLOR + ) + + // For screen-space: use x, y, with z = 0 + vertices.forEach { v -> builder.vertex(v.x, v.y, 0f).color(v.r, v.g, v.b, v.a) } + + 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) = 44 bytes, round up + BufferAllocator(vertices.size * 48).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) + } + } + + 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 + // Position (8, 2D) + Texture (8) + Color (4) = 20 bytes, but using POSITION (12) for simplicity + BufferAllocator(vertices.size * 24).use { allocator -> + val builder = BufferBuilder( + allocator, + VertexFormat.DrawMode.QUADS, + VertexFormats.POSITION_TEXTURE_COLOR + ) + + // 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) + } + + 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, and text buffers + */ + fun uploadScreen(): ScreenUploadResult { + val faces = uploadScreenFaces() + val edges = uploadScreenEdges() + val text = uploadScreenText() + return ScreenUploadResult(faces, edges, text) + } + data class BufferResult(val buffer: GpuBuffer?, val indexCount: Int) data class UploadResult(val faces: BufferResult?, val edges: BufferResult?, val text: BufferResult? = null) + data class ScreenUploadResult(val faces: BufferResult?, val edges: BufferResult?, val text: BufferResult? = null) } diff --git a/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt b/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt index 5a2c4a448..ed52f5ec8 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt @@ -17,11 +17,11 @@ 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.util.DirectionMask -import com.lambda.graphics.util.DirectionMask.hasDirection import com.lambda.util.BlockUtils.blockState import net.minecraft.block.BlockState import net.minecraft.util.math.BlockPos @@ -81,19 +81,6 @@ class RenderBuilder(private val cameraPos: Vec3d) { 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, @@ -134,138 +121,6 @@ class RenderBuilder(private val cameraPos: Vec3d) { 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. @@ -343,6 +198,279 @@ class RenderBuilder(private val cameraPos: Vec3d) { anchorX, anchorY, anchorZ, size, rotationMatrix) } + // ============================================================================ + // Screen-Space Rendering Methods (Normalized Coordinates) + // ============================================================================ + // All coordinates use normalized 0-1 range: + // - (0, 0) = top-left corner + // - (1, 1) = bottom-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. + * By default uses the average of width and height for uniform scaling. + * Use toPixelSizeX/Y for non-uniform scaling. + */ + private fun toPixelSize(normalizedSize: Float): Float = + normalizedSize * (screenWidth + screenHeight) / 2f + + /** + * 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 + ) { + collector.addScreenFaceVertex(toPixelX(x1), toPixelY(y1), c1) + collector.addScreenFaceVertex(toPixelX(x2), toPixelY(y2), c2) + collector.addScreenFaceVertex(toPixelX(x3), toPixelY(y3), c3) + collector.addScreenFaceVertex(toPixelX(x4), toPixelY(y4), c4) + } + + /** + * 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 Top edge (0-1, where 0 = top, 1 = bottom) + * @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 Top edge (0-1) + * @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 + ) + } + + // 4 vertices for screen-space line quad + collector.addScreenEdgeVertex(px1, py1, startColor, dx, dy, pixelWidth, pixelDashStyle) + collector.addScreenEdgeVertex(px1, py1, startColor, dx, dy, pixelWidth, pixelDashStyle) + collector.addScreenEdgeVertex(px2, py2, endColor, dx, dy, pixelWidth, pixelDashStyle) + collector.addScreenEdgeVertex(px2, py2, endColor, dx, dy, pixelWidth, pixelDashStyle) + } + + /** + * 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) + + /** + * 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 = top, 1 = bottom) + * @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: TextStyle = TextStyle(), + centered: Boolean = false + ) { + val atlas = font ?: FontHandler.getDefaultFont() + fontAtlas = atlas + + // Convert to pixel coordinates + val pixelX = toPixelX(x) + val pixelY = toPixelY(y) + val pixelSize = toPixelSize(size) + + // Calculate text width for centering + val textWidth = if (centered) atlas.getStringWidth(text, pixelSize) else 0f + val startX = -textWidth / 2f + + // Render layers in order: shadow -> glow -> outline -> main text + // Alpha encodes layer type for shader + + // Shadow layer + if (style.shadow != null) { + val shadowColor = style.shadow.color + val offsetX = style.shadow.offsetX * pixelSize + val offsetY = style.shadow.offsetY * pixelSize + buildScreenTextQuads(atlas, text, startX + offsetX, offsetY, + shadowColor.red, shadowColor.green, shadowColor.blue, 25, + pixelX, pixelY, pixelSize) + } + + // Glow layer + if (style.glow != null) { + val glowColor = style.glow.color + buildScreenTextQuads(atlas, text, startX, 0f, + glowColor.red, glowColor.green, glowColor.blue, 75, + pixelX, pixelY, pixelSize) + } + + // Outline layer + if (style.outline != null) { + val outlineColor = style.outline.color + buildScreenTextQuads(atlas, text, startX, 0f, + outlineColor.red, outlineColor.green, outlineColor.blue, 150, + pixelX, pixelY, pixelSize) + } + + // Main text layer + val mainColor = style.color + buildScreenTextQuads(atlas, text, startX, 0f, + mainColor.red, mainColor.green, mainColor.blue, 255, + pixelX, pixelY, pixelSize) + } + + /** + * Build screen-space text quad vertices for a layer. + * Internal method - uses pixel coordinates. + */ + 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 + ) { + // 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 + val localX0 = penX + glyph.bearingX + val localY0 = -glyph.bearingY // Y flipped for screen (down = positive) + + // width/height are in pixels, need normalization + val localX1 = localX0 + glyph.width / atlas.baseSize + val localY1 = localY0 + glyph.height / atlas.baseSize + + // 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 + collector.addScreenTextVertex(x0, y1, glyph.u0, glyph.v1, r, g, b, a) + collector.addScreenTextVertex(x1, y1, glyph.u1, glyph.v1, r, g, b, a) + collector.addScreenTextVertex(x1, y0, glyph.u1, glyph.v0, r, g, b, a) + collector.addScreenTextVertex(x0, y0, glyph.u0, glyph.v0, r, g, b, a) + + // advance is already normalized, just add it + penX += glyph.advance + } + } + /** * Build text quad vertices for a layer with specified color and alpha. * @@ -405,162 +533,9 @@ class RenderBuilder(private val cameraPos: Vec3d) { } 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 */ diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt index b71c00e0c..5f56acad0 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt @@ -24,7 +24,6 @@ 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.LambdaRenderPipelines import com.lambda.graphics.mc.RegionRenderer import com.lambda.graphics.mc.RenderBuilder import com.lambda.graphics.text.FontHandler @@ -33,7 +32,6 @@ 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.GpuBuffer import com.mojang.blaze3d.systems.RenderSystem import net.minecraft.util.math.Vec3d import net.minecraft.world.World @@ -41,7 +39,6 @@ import net.minecraft.world.chunk.WorldChunk import org.joml.Matrix4f import org.joml.Vector3f import org.joml.Vector4f -import org.lwjgl.system.MemoryUtil import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentLinkedDeque @@ -61,7 +58,7 @@ import java.util.concurrent.ConcurrentLinkedDeque class ChunkedRenderer( owner: Module, name: String, - private val depthTest: Boolean = false, + var depthTest: Boolean = false, private val update: RenderBuilder.(World, FastVector) -> Unit ) { private val chunkMap = ConcurrentHashMap() @@ -136,10 +133,7 @@ class ChunkedRenderer( // Render Faces RegionRenderer.Companion.createRenderPass("ChunkedESP Faces", depthTest)?.use { pass -> - val pipeline = - if (depthTest) LambdaRenderPipelines.ESP_QUADS - else LambdaRenderPipelines.ESP_QUADS_THROUGH - pass.setPipeline(pipeline) + pass.setPipeline(RendererUtils.getFacesPipeline(depthTest)) RenderSystem.bindDefaultUniforms(pass) chunkTransforms.forEach { (chunkData, transform) -> @@ -150,10 +144,7 @@ class ChunkedRenderer( // Render Edges RegionRenderer.Companion.createRenderPass("ChunkedESP Edges", depthTest)?.use { pass -> - val pipeline = - if (depthTest) LambdaRenderPipelines.ESP_LINES - else LambdaRenderPipelines.ESP_LINES_THROUGH - pass.setPipeline(pipeline) + pass.setPipeline(RendererUtils.getEdgesPipeline(depthTest)) RenderSystem.bindDefaultUniforms(pass) chunkTransforms.forEach { (chunkData, transform) -> @@ -171,13 +162,10 @@ class ChunkedRenderer( val textureView = atlas.textureView val sampler = atlas.sampler if (textureView != null && sampler != null) { - val sdfParams = createSDFParamsBuffer() + val sdfParams = RendererUtils.createSDFParamsBuffer() if (sdfParams != null) { RegionRenderer.Companion.createRenderPass("ChunkedESP Text", depthTest)?.use { pass -> - val pipeline = - if (depthTest) LambdaRenderPipelines.SDF_TEXT - else LambdaRenderPipelines.SDF_TEXT_THROUGH - pass.setPipeline(pipeline) + pass.setPipeline(RendererUtils.getTextPipeline(depthTest)) RenderSystem.bindDefaultUniforms(pass) pass.setUniform("SDFParams", sdfParams) pass.bindTexture("Sampler0", textureView, sampler) @@ -193,20 +181,71 @@ class ChunkedRenderer( } } - private fun createSDFParamsBuffer(): GpuBuffer? { - val device = RenderSystem.getDevice() - val buffer = MemoryUtil.memAlloc(16) - return try { - buffer.putFloat(0.5f) - buffer.putFloat(0.1f) - buffer.putFloat(0.2f) - buffer.putFloat(0.15f) - buffer.flip() - device.createBuffer({ "SDFParams" }, GpuBuffer.USAGE_UNIFORM, buffer) - } catch (e: Exception) { - null - } finally { - MemoryUtil.memFree(buffer) + /** + * Render screen-space geometry for all chunks. + * Uses orthographic projection for 2D rendering. + */ + fun renderScreen() { + val activeChunks = chunkMap.values.filter { it.renderer.hasScreenData() } + if (activeChunks.isEmpty()) return + + RendererUtils.withScreenContext { + val dynamicTransform = RendererUtils.createScreenDynamicTransform() + + // Render Screen Faces + RegionRenderer.Companion.createRenderPass("ChunkedESP Screen Faces", false)?.use { pass -> + pass.setPipeline(RendererUtils.screenFacesPipeline) + RenderSystem.bindDefaultUniforms(pass) + pass.setUniform("DynamicTransforms", dynamicTransform) + activeChunks.forEach { chunkData -> + chunkData.renderer.renderScreenFaces(pass) + } + } + + // Render Screen Edges + RegionRenderer.Companion.createRenderPass("ChunkedESP Screen Edges", false)?.use { pass -> + pass.setPipeline(RendererUtils.screenEdgesPipeline) + RenderSystem.bindDefaultUniforms(pass) + pass.setUniform("DynamicTransforms", dynamicTransform) + activeChunks.forEach { chunkData -> + chunkData.renderer.renderScreenEdges(pass) + } + } + + // Render Screen Text + val chunksWithText = activeChunks.filter { it.renderer.hasScreenTextData() } + if (chunksWithText.isNotEmpty()) { + val atlas = FontHandler.getDefaultFont() + if (!atlas.isUploaded) atlas.upload() + val textureView = atlas.textureView + val sampler = atlas.sampler + if (textureView != null && sampler != null) { + val sdfParams = RendererUtils.createSDFParamsBuffer() + if (sdfParams != null) { + RegionRenderer.Companion.createRenderPass("ChunkedESP Screen Text", false)?.use { pass -> + pass.setPipeline(RendererUtils.screenTextPipeline) + RenderSystem.bindDefaultUniforms(pass) + pass.setUniform("DynamicTransforms", dynamicTransform) + pass.setUniform("SDFParams", sdfParams) + pass.bindTexture("Sampler0", textureView, sampler) + chunksWithText.forEach { chunkData -> + chunkData.renderer.renderScreenText(pass) + } + } + sdfParams.close() + } + } + } + } + } + + companion object { + fun Module.chunkedEsp( + name: String, + depthTest: Boolean = false, + update: RenderBuilder.(World, FastVector) -> Unit + ): ChunkedRenderer { + return ChunkedRenderer(this, name, depthTest, update) } } @@ -292,14 +331,4 @@ class ChunkedRenderer( renderer.close() } } - - companion object { - fun Module.chunkedEsp( - name: String, - depthTest: Boolean = false, - update: RenderBuilder.(World, FastVector) -> Unit - ): ChunkedRenderer { - return 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 index 5dc4fce3c..087e644a6 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt @@ -19,17 +19,14 @@ package com.lambda.graphics.mc.renderer import com.lambda.Lambda.mc import com.lambda.graphics.RenderMain -import com.lambda.graphics.mc.LambdaRenderPipelines import com.lambda.graphics.mc.RegionRenderer import com.lambda.graphics.mc.RenderBuilder import com.lambda.graphics.text.SDFFontAtlas -import com.mojang.blaze3d.buffers.GpuBuffer import com.mojang.blaze3d.systems.RenderSystem import net.minecraft.util.math.Vec3d import org.joml.Matrix4f import org.joml.Vector3f import org.joml.Vector4f -import org.lwjgl.system.MemoryUtil /** * Interpolated ESP system for smooth entity rendering. @@ -106,10 +103,7 @@ class ImmediateRenderer(val name: String, var depthTest: Boolean = false) { // Render Faces RegionRenderer.Companion.createRenderPass("$name Faces", depthTest)?.use { pass -> - val pipeline = - if (depthTest) LambdaRenderPipelines.ESP_QUADS - else LambdaRenderPipelines.ESP_QUADS_THROUGH - pass.setPipeline(pipeline) + pass.setPipeline(RendererUtils.getFacesPipeline(depthTest)) RenderSystem.bindDefaultUniforms(pass) pass.setUniform("DynamicTransforms", dynamicTransform) renderer.renderFaces(pass) @@ -117,10 +111,7 @@ class ImmediateRenderer(val name: String, var depthTest: Boolean = false) { // Render Edges RegionRenderer.Companion.createRenderPass("$name Edges", depthTest)?.use { pass -> - val pipeline = - if (depthTest) LambdaRenderPipelines.ESP_LINES - else LambdaRenderPipelines.ESP_LINES_THROUGH - pass.setPipeline(pipeline) + pass.setPipeline(RendererUtils.getEdgesPipeline(depthTest)) RenderSystem.bindDefaultUniforms(pass) pass.setUniform("DynamicTransforms", dynamicTransform) renderer.renderEdges(pass) @@ -134,13 +125,10 @@ class ImmediateRenderer(val name: String, var depthTest: Boolean = false) { val textureView = atlas.textureView val sampler = atlas.sampler if (textureView != null && sampler != null) { - val sdfParams = createSDFParamsBuffer() + val sdfParams = RendererUtils.createSDFParamsBuffer() if (sdfParams != null) { RegionRenderer.Companion.createRenderPass("$name Text", depthTest)?.use { pass -> - val pipeline = - if (depthTest) LambdaRenderPipelines.SDF_TEXT - else LambdaRenderPipelines.SDF_TEXT_THROUGH - pass.setPipeline(pipeline) + pass.setPipeline(RendererUtils.getTextPipeline(depthTest)) RenderSystem.bindDefaultUniforms(pass) pass.setUniform("DynamicTransforms", dynamicTransform) pass.setUniform("SDFParams", sdfParams) @@ -155,22 +143,54 @@ class ImmediateRenderer(val name: String, var depthTest: Boolean = false) { } /** - * Create SDF params uniform buffer with default values. + * Render screen-space geometry. Uses orthographic projection for 2D rendering. + * This should be called after world-space render() for proper layering. */ - private fun createSDFParamsBuffer(): GpuBuffer? { - val device = RenderSystem.getDevice() - val buffer = MemoryUtil.memAlloc(16) - return try { - buffer.putFloat(0.5f) // SDFThreshold - buffer.putFloat(0.1f) // OutlineWidth - buffer.putFloat(0.2f) // GlowRadius - buffer.putFloat(0.15f) // ShadowSoftness - buffer.flip() - device.createBuffer({ "SDFParams" }, GpuBuffer.USAGE_UNIFORM, buffer) - } catch (_: Exception) { - null - } finally { - MemoryUtil.memFree(buffer) + fun renderScreen() { + if (!renderer.hasScreenData()) return + + RendererUtils.withScreenContext { + val dynamicTransform = RendererUtils.createScreenDynamicTransform() + + // Render Screen Faces (no depth test for 2D) + RegionRenderer.createRenderPass("$name Screen Faces", false)?.use { pass -> + pass.setPipeline(RendererUtils.screenFacesPipeline) + RenderSystem.bindDefaultUniforms(pass) + pass.setUniform("DynamicTransforms", dynamicTransform) + renderer.renderScreenFaces(pass) + } + + // Render Screen Edges + RegionRenderer.createRenderPass("$name Screen Edges", false)?.use { pass -> + pass.setPipeline(RendererUtils.screenEdgesPipeline) + RenderSystem.bindDefaultUniforms(pass) + pass.setUniform("DynamicTransforms", dynamicTransform) + renderer.renderScreenEdges(pass) + } + + // Render Screen Text + if (renderer.hasScreenTextData()) { + val atlas = currentFontAtlas + if (atlas != null) { + if (!atlas.isUploaded) atlas.upload() + val textureView = atlas.textureView + val sampler = atlas.sampler + if (textureView != null && sampler != null) { + val sdfParams = RendererUtils.createSDFParamsBuffer() + if (sdfParams != null) { + RegionRenderer.createRenderPass("$name Screen Text", false)?.use { pass -> + pass.setPipeline(RendererUtils.screenTextPipeline) + RenderSystem.bindDefaultUniforms(pass) + pass.setUniform("DynamicTransforms", dynamicTransform) + pass.setUniform("SDFParams", sdfParams) + pass.bindTexture("Sampler0", textureView, sampler) + renderer.renderScreenText(pass) + } + sdfParams.close() + } + } + } + } } } } 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..ac8f6cefb --- /dev/null +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/RendererUtils.kt @@ -0,0 +1,134 @@ +/* + * 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.graphics.mc.LambdaRenderPipelines +import com.mojang.blaze3d.buffers.GpuBuffer +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.render.ProjectionMatrix2 +import org.joml.Matrix4f +import org.joml.Vector3f +import org.joml.Vector4f +import org.lwjgl.system.MemoryUtil + +/** + * 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 + private val screenProjectionMatrix = ProjectionMatrix2("lambda_screen", -1000f, 1000f, true) + + /** + * Create SDF params uniform buffer with default values. + * Used for SDF text rendering. + */ + fun createSDFParamsBuffer(): GpuBuffer? { + val device = RenderSystem.getDevice() + val buffer = MemoryUtil.memAlloc(16) + return try { + buffer.putFloat(0.5f) // SDFThreshold + buffer.putFloat(0.1f) // OutlineWidth + buffer.putFloat(0.2f) // GlowRadius + buffer.putFloat(0.15f) // ShadowSoftness + buffer.flip() + device.createBuffer({ "SDFParams" }, GpuBuffer.USAGE_UNIFORM, buffer) + } catch (_: Exception) { + null + } finally { + MemoryUtil.memFree(buffer) + } + } + + /** + * 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 + ) + } + + /** + * 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 based on depth test setting. */ + fun getFacesPipeline(depthTest: Boolean): RenderPipeline = + if (depthTest) LambdaRenderPipelines.ESP_QUADS + else LambdaRenderPipelines.ESP_QUADS_THROUGH + + /** Get the edge/line pipeline based on depth test setting. */ + fun getEdgesPipeline(depthTest: Boolean): RenderPipeline = + if (depthTest) LambdaRenderPipelines.ESP_LINES + else LambdaRenderPipelines.ESP_LINES_THROUGH + + /** Get the SDF text pipeline based on depth test setting. */ + fun getTextPipeline(depthTest: Boolean): RenderPipeline = + if (depthTest) LambdaRenderPipelines.SDF_TEXT + else LambdaRenderPipelines.SDF_TEXT_THROUGH + + /** Screen-space faces pipeline (always no depth test). */ + val screenFacesPipeline: RenderPipeline get() = LambdaRenderPipelines.ESP_QUADS_THROUGH + + /** Screen-space edges pipeline. */ + val screenEdgesPipeline: RenderPipeline get() = LambdaRenderPipelines.SCREEN_LINES + + /** Screen-space text pipeline. */ + val screenTextPipeline: RenderPipeline get() = LambdaRenderPipelines.SCREEN_TEXT +} diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.kt index 808246104..51a41f070 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.kt @@ -19,17 +19,14 @@ package com.lambda.graphics.mc.renderer import com.lambda.Lambda.mc import com.lambda.graphics.RenderMain -import com.lambda.graphics.mc.LambdaRenderPipelines import com.lambda.graphics.mc.RegionRenderer import com.lambda.graphics.mc.RenderBuilder import com.lambda.graphics.text.SDFFontAtlas -import com.mojang.blaze3d.buffers.GpuBuffer import com.mojang.blaze3d.systems.RenderSystem import net.minecraft.util.math.Vec3d import org.joml.Matrix4f import org.joml.Vector3f import org.joml.Vector4f -import org.lwjgl.system.MemoryUtil /** * Modern replacement for the legacy Treed system. Handles geometry that is cleared and rebuilt @@ -103,22 +100,16 @@ class TickedRenderer(val name: String, var depthTest: Boolean = false) { .write(modelView, Vector4f(1f, 1f, 1f, 1f), Vector3f(0f, 0f, 0f), Matrix4f()) // Render Faces - RegionRenderer.Companion.createRenderPass("$name Faces", depthTest)?.use { pass -> - val pipeline = - if (depthTest) LambdaRenderPipelines.ESP_QUADS - else LambdaRenderPipelines.ESP_QUADS_THROUGH - pass.setPipeline(pipeline) + RegionRenderer.createRenderPass("$name Faces", depthTest)?.use { pass -> + pass.setPipeline(RendererUtils.getFacesPipeline(depthTest)) RenderSystem.bindDefaultUniforms(pass) pass.setUniform("DynamicTransforms", dynamicTransform) renderer.renderFaces(pass) } // Render Edges - RegionRenderer.Companion.createRenderPass("$name Edges", depthTest)?.use { pass -> - val pipeline = - if (depthTest) LambdaRenderPipelines.ESP_LINES - else LambdaRenderPipelines.ESP_LINES_THROUGH - pass.setPipeline(pipeline) + RegionRenderer.createRenderPass("$name Edges", depthTest)?.use { pass -> + pass.setPipeline(RendererUtils.getEdgesPipeline(depthTest)) RenderSystem.bindDefaultUniforms(pass) pass.setUniform("DynamicTransforms", dynamicTransform) renderer.renderEdges(pass) @@ -132,13 +123,10 @@ class TickedRenderer(val name: String, var depthTest: Boolean = false) { val textureView = atlas.textureView val sampler = atlas.sampler if (textureView != null && sampler != null) { - val sdfParams = createSDFParamsBuffer() + val sdfParams = RendererUtils.createSDFParamsBuffer() if (sdfParams != null) { - RegionRenderer.Companion.createRenderPass("$name Text", depthTest)?.use { pass -> - val pipeline = - if (depthTest) LambdaRenderPipelines.SDF_TEXT - else LambdaRenderPipelines.SDF_TEXT_THROUGH - pass.setPipeline(pipeline) + RegionRenderer.createRenderPass("$name Text", depthTest)?.use { pass -> + pass.setPipeline(RendererUtils.getTextPipeline(depthTest)) RenderSystem.bindDefaultUniforms(pass) pass.setUniform("DynamicTransforms", dynamicTransform) pass.setUniform("SDFParams", sdfParams) @@ -152,20 +140,55 @@ class TickedRenderer(val name: String, var depthTest: Boolean = false) { } } - private fun createSDFParamsBuffer(): GpuBuffer? { - val device = RenderSystem.getDevice() - val buffer = MemoryUtil.memAlloc(16) - return try { - buffer.putFloat(0.5f) - buffer.putFloat(0.1f) - buffer.putFloat(0.2f) - buffer.putFloat(0.15f) - buffer.flip() - device.createBuffer({ "SDFParams" }, GpuBuffer.USAGE_UNIFORM, buffer) - } catch (e: Exception) { - null - } finally { - MemoryUtil.memFree(buffer) + /** + * Render screen-space geometry. Uses orthographic projection for 2D rendering. + * This should be called after world-space render() for proper layering. + */ + fun renderScreen() { + if (!renderer.hasScreenData()) return + + RendererUtils.withScreenContext { + val dynamicTransform = RendererUtils.createScreenDynamicTransform() + + // Render Screen Faces + RegionRenderer.createRenderPass("$name Screen Faces", false)?.use { pass -> + pass.setPipeline(RendererUtils.screenFacesPipeline) + RenderSystem.bindDefaultUniforms(pass) + pass.setUniform("DynamicTransforms", dynamicTransform) + renderer.renderScreenFaces(pass) + } + + // Render Screen Edges + RegionRenderer.createRenderPass("$name Screen Edges", false)?.use { pass -> + pass.setPipeline(RendererUtils.screenEdgesPipeline) + RenderSystem.bindDefaultUniforms(pass) + pass.setUniform("DynamicTransforms", dynamicTransform) + renderer.renderScreenEdges(pass) + } + + // Render Screen Text + if (renderer.hasScreenTextData()) { + val atlas = currentFontAtlas + if (atlas != null) { + if (!atlas.isUploaded) atlas.upload() + val textureView = atlas.textureView + val sampler = atlas.sampler + if (textureView != null && sampler != null) { + val sdfParams = RendererUtils.createSDFParamsBuffer() + if (sdfParams != null) { + RegionRenderer.Companion.createRenderPass("$name Screen Text", false)?.use { pass -> + pass.setPipeline(RendererUtils.screenTextPipeline) + RenderSystem.bindDefaultUniforms(pass) + pass.setUniform("DynamicTransforms", dynamicTransform) + pass.setUniform("SDFParams", sdfParams) + pass.bindTexture("Sampler0", textureView, sampler) + renderer.renderScreenText(pass) + } + sdfParams.close() + } + } + } + } } } } 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..f18c6fcbe --- /dev/null +++ b/src/main/kotlin/com/lambda/module/modules/debug/RendererTestModule.kt @@ -0,0 +1,320 @@ +/* + * 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.event.events.RenderEvent +import com.lambda.event.events.TickEvent +import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.graphics.mc.LineDashStyle.Companion.marchingAnts +import com.lambda.graphics.mc.LineDashStyle.Companion.screenMarchingAnts +import com.lambda.graphics.mc.RenderBuilder.TextGlow +import com.lambda.graphics.mc.RenderBuilder.TextOutline +import com.lambda.graphics.mc.RenderBuilder.TextShadow +import com.lambda.graphics.mc.RenderBuilder.TextStyle +import com.lambda.graphics.mc.renderer.ChunkedRenderer.Companion.chunkedEsp +import com.lambda.graphics.mc.renderer.ImmediateRenderer +import com.lambda.graphics.mc.renderer.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.util.math.ChunkPos +import net.minecraft.util.math.Direction +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, +) { + private val throughWalls by setting("Through Walls", false) + + var changedAlready = false + + private val esp = chunkedEsp("ChunkedRendererTest", depthTest = false) { world, pos -> + runSafe { + if (player.chunkPos != ChunkPos(pos.toBlockPos())) return@chunkedEsp + true + } ?: return@chunkedEsp + if (changedAlready) return@chunkedEsp + val startPos = runSafe { lerp(mc.tickDelta, player.prevPos, player.pos) } ?: return@chunkedEsp + 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 = TextStyle( + outline = TextOutline(), + glow = TextGlow(), + shadow = TextShadow() + ) + ) + + // 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 = TextStyle( + color = Color.WHITE, + outline = TextOutline(), + shadow = TextShadow() + ) + ) + + // 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 = TextStyle( + color = Color.YELLOW, + glow = TextGlow(Color(255, 200, 0, 150)), + shadow = TextShadow() + ), + centered = true + ) + changedAlready = true + } + + init { + listen { + esp.depthTest = !throughWalls + esp.render() + esp.renderScreen() + } + + listen { changedAlready = false } + + onDisable { esp.close() } + } +} + +/** + * 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) + private val renderer = TickedRenderer("TickedRendererTest") + + init { + listen { + renderer.render() + renderer.renderScreen() + } + + listen { + renderer.depthTest = !throughWalls + renderer.clear() + + renderer.shapes { + 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 = TextStyle( + outline = TextOutline(), + glow = TextGlow(), + shadow = TextShadow() + ) + ) + + // 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 = TextStyle( + color = Color.WHITE, + outline = TextOutline(), + shadow = TextShadow() + ) + ) + + // 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 = TextStyle( + color = Color.YELLOW, + glow = TextGlow(Color(255, 200, 0, 150)), + shadow = TextShadow() + ), + centered = true + ) + } + + renderer.upload() + } + + onDisable { renderer.close() } + } +} + +/** + * 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) + private val renderer = ImmediateRenderer("ImmediateRendererTest") + + init { + listen { + renderer.depthTest = !throughWalls + renderer.tick() + + renderer.shapes { + 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 = TextStyle( + outline = TextOutline(), + glow = TextGlow(), + shadow = TextShadow() + ) + ) + + // 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 = TextStyle( + color = Color.WHITE, + outline = TextOutline(), + shadow = TextShadow() + ) + ) + + // 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 = TextStyle( + color = Color.YELLOW, + glow = TextGlow(Color(255, 200, 0, 150)), + shadow = TextShadow() + ), + centered = true + ) + } + + renderer.upload() + renderer.render() + renderer.renderScreen() + } + + onDisable { renderer.close() } + } +} 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 7325aa2ce..aba7f05ce 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/EntityESP.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/EntityESP.kt @@ -60,13 +60,6 @@ object EntityESP : Module( private val pendingLabels = mutableListOf() - private val lineLength by setting("Line Length", 5f, 0f..15f, 0.1f) - private val outlineWidth by setting("Outline Width", 0.15f, 0f..1f, 0.01f) - private val glowWidth by setting("Glow Width", 0.25f, 0f..1f, 0.01f) - private val shadowDistance by setting("Shadow Distance", 0.2f, 0f..1f, 0.01f) - private val shadowAngle by setting("Shadow Angle", 135f, 0f..360f, 1f) - private val animationSpeed by setting("Animation Speed", 1f, 0.1f..5f, 0.1f) - 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) @@ -168,6 +161,7 @@ object EntityESP : Module( esp.upload() esp.render() + esp.renderScreen() // Clear pending labels from previous frame pendingLabels.clear() 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..2705b0f48 --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/screen_lines.fsh @@ -0,0 +1,78 @@ +#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) + +out vec4 fragColor; + +void main() { + // ===== CAPSULE SDF ===== + vec2 lineDir = normalize(v_LineEnd - v_LineStart); + float radius = v_LineWidth / 2.0; + + // Project fragment position onto line to find closest point + vec2 toFragment = v_ExpandedPos - v_LineStart; + float projLength = dot(toFragment, lineDir); + + // Clamp to segment bounds [0, segmentLength] for capsule behavior + float clampedProj = clamp(projLength, 0.0, v_SegmentLength); + + // Closest point on line segment + vec2 closestPoint = v_LineStart + lineDir * clampedProj; + + // 2D distance from fragment to closest point on line + float dist2D = length(v_ExpandedPos - closestPoint); + + // SDF: distance to capsule surface (positive = outside, negative = inside) + float sdf = dist2D - radius; + + // Anti-aliasing using screen-space derivatives (same as world-space) + float aaWidth = fwidth(sdf); + float alpha = 1.0 - smoothstep(-aaWidth, aaWidth, sdf); + + // Skip fragments outside the line + if (alpha <= 0.0) { + 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 the CLAMPED position along the line for dash calculation + float dashPos = clampedProj + animatedOffset * cycleLength; + float posInCycle = mod(dashPos, cycleLength); + + // In gap = discard + if (posInCycle > dashLength) { + discard; + } + } + + // Apply color + vec4 color = v_Color * ColorModulator; + color.a *= alpha; + + fragColor = color; +} 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..8213acbd1 --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/screen_lines.vsh @@ -0,0 +1,67 @@ +#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) + +// 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) + +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) + float halfWidth = LineWidth / 2.0; + float aaPadding = LineWidth * 0.25 + 1.0; // Scale-aware padding + 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; +} 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..5a4195f62 --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/screen_sdf_text.fsh @@ -0,0 +1,67 @@ +#version 330 + +#moj_import + +uniform sampler2D Sampler0; + +// SDF effect parameters - matches world-space sdf_text +layout(std140) uniform SDFParams { + float SDFThreshold; // Main text edge threshold (default 0.5) + float OutlineWidth; // Outline width in SDF units (0 = no outline) + float GlowRadius; // Glow radius in SDF units (0 = no glow) + float ShadowSoftness; // Shadow softness (0 = no shadow) +}; + +// Inputs from vertex shader +in vec2 texCoord0; +in vec4 vertexColor; + +out vec4 fragColor; + +void main() { + // 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 text edge and extends outward + float glowStart = SDFThreshold - GlowRadius; + float glowEnd = SDFThreshold; + alpha = smoothstep(glowStart, glowEnd, sdfValue) * 0.6; + // Mask out the main text area + float textMask = smoothstep(SDFThreshold - smoothing, SDFThreshold + smoothing, sdfValue); + alpha = alpha * (1.0 - textMask); + } 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; +} 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..19cb558e9 --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/screen_sdf_text.vsh @@ -0,0 +1,21 @@ +#version 330 + +#moj_import +#moj_import + +// Vertex inputs (POSITION_TEXTURE_COLOR format) +in vec3 Position; +in vec2 UV0; +in vec4 Color; + +// Outputs to fragment shader +out vec2 texCoord0; +out vec4 vertexColor; + +void main() { + // Screen-space position - already in screen coordinates + gl_Position = ProjMat * ModelViewMat * vec4(Position, 1.0); + + texCoord0 = UV0; + vertexColor = Color; +} From 811efe02f69dcaa76da8ba00d781566e40959622 Mon Sep 17 00:00:00 2001 From: beanbag44 <107891830+beanbag44@users.noreply.github.com> Date: Sun, 18 Jan 2026 18:43:17 +0000 Subject: [PATCH 09/26] render dash animation through end caps and fix antialiasing on dash ends --- .../lambda/shaders/core/advanced_lines.fsh | 25 ++++++++++++++++--- .../lambda/shaders/core/screen_lines.fsh | 24 +++++++++++++++--- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/main/resources/assets/lambda/shaders/core/advanced_lines.fsh b/src/main/resources/assets/lambda/shaders/core/advanced_lines.fsh index 19616438b..7659d14b9 100644 --- a/src/main/resources/assets/lambda/shaders/core/advanced_lines.fsh +++ b/src/main/resources/assets/lambda/shaders/core/advanced_lines.fsh @@ -72,15 +72,32 @@ void main() { animatedOffset += GameTime * animationSpeed * 1200.0; } - // Use the CLAMPED position along the line for dash calculation - // This ensures dashes are in world-space units - float dashPos = clampedProj + animatedOffset * cycleLength; + // Use UNCLAMPED projLength so dashes continue through endcaps + float dashPos = projLength + animatedOffset * cycleLength; float posInCycle = mod(dashPos, cycleLength); - // In gap = discard + // 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 diff --git a/src/main/resources/assets/lambda/shaders/core/screen_lines.fsh b/src/main/resources/assets/lambda/shaders/core/screen_lines.fsh index 2705b0f48..aba53911c 100644 --- a/src/main/resources/assets/lambda/shaders/core/screen_lines.fsh +++ b/src/main/resources/assets/lambda/shaders/core/screen_lines.fsh @@ -60,14 +60,32 @@ void main() { animatedOffset += GameTime * animationSpeed * 1200.0; } - // Use the CLAMPED position along the line for dash calculation - float dashPos = clampedProj + animatedOffset * cycleLength; + // Use UNCLAMPED projLength so dashes continue through endcaps + float dashPos = projLength + animatedOffset * cycleLength; float posInCycle = mod(dashPos, cycleLength); - // In gap = discard + // 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 From 8d8a49ee562350beb04075142a76e548a40b6131 Mon Sep 17 00:00:00 2001 From: beanbag44 <107891830+beanbag44@users.noreply.github.com> Date: Sun, 18 Jan 2026 22:28:04 +0000 Subject: [PATCH 10/26] fix glow rendering under outline and store a map of text style to text to allow for completely unique text rendering in the same renderer --- .../com/lambda/graphics/mc/RegionRenderer.kt | 19 +++ .../graphics/mc/RegionVertexCollector.kt | 91 ++++++++++++ .../com/lambda/graphics/mc/RenderBuilder.kt | 63 ++++++--- .../graphics/mc/renderer/ChunkedRenderer.kt | 131 +++++++++++++----- .../graphics/mc/renderer/ImmediateRenderer.kt | 105 ++++++++++---- .../graphics/mc/renderer/RendererUtils.kt | 20 ++- .../graphics/mc/renderer/TickedRenderer.kt | 104 ++++++++++---- .../lambda/shaders/core/screen_sdf_text.fsh | 14 +- .../assets/lambda/shaders/core/sdf_text.fsh | 14 +- 9 files changed, 430 insertions(+), 131 deletions(-) diff --git a/src/main/kotlin/com/lambda/graphics/mc/RegionRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/RegionRenderer.kt index 6f8d8bc72..6cc03b68f 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/RegionRenderer.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/RegionRenderer.kt @@ -278,5 +278,24 @@ class RegionRenderer { 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/RegionVertexCollector.kt b/src/main/kotlin/com/lambda/graphics/mc/RegionVertexCollector.kt index 9489fad21..0c307a87a 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/RegionVertexCollector.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/RegionVertexCollector.kt @@ -549,4 +549,95 @@ class RegionVertexCollector { data class BufferResult(val buffer: GpuBuffer?, val indexCount: Int) data class UploadResult(val faces: BufferResult?, val edges: BufferResult?, val text: BufferResult? = null) data class ScreenUploadResult(val faces: BufferResult?, val edges: BufferResult?, val text: BufferResult? = null) + + companion object { + /** + * Upload a list of text vertices to a GPU buffer. + * Used for style-grouped text rendering. + */ + fun uploadTextVertices(vertices: List): BufferResult { + if (vertices.isEmpty()) return BufferResult(null, 0) + + var result: BufferResult? = null + // POSITION_TEXTURE_COLOR_ANCHOR: 12 + 8 + 4 + 12 + 8 = 44 bytes per vertex + BufferAllocator(vertices.size * 48).use { allocator -> + val builder = BufferBuilder( + allocator, + VertexFormat.DrawMode.QUADS, + LambdaVertexFormats.POSITION_TEXTURE_COLOR_ANCHOR + ) + + 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) + } + } + + builder.endNullable()?.let { built -> + val gpuDevice = RenderSystem.getDevice() + val buffer = gpuDevice.createBuffer( + { "Lambda Styled Text Buffer" }, + GpuBuffer.USAGE_VERTEX, + built.buffer + ) + result = BufferResult(buffer, built.drawParameters.indexCount()) + built.close() + } + } + return result ?: BufferResult(null, 0) + } + + /** + * Upload a list of screen text vertices to a GPU buffer. + * Used for style-grouped screen text rendering. + */ + fun uploadScreenTextVertices(vertices: List): BufferResult { + if (vertices.isEmpty()) return BufferResult(null, 0) + + var result: BufferResult? = null + // Position (8, 2D) + Texture (8) + Color (4) = 20 bytes, but using POSITION (12) for simplicity + BufferAllocator(vertices.size * 24).use { allocator -> + val builder = BufferBuilder( + allocator, + VertexFormat.DrawMode.QUADS, + VertexFormats.POSITION_TEXTURE_COLOR + ) + + // 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) + } + + builder.endNullable()?.let { built -> + val gpuDevice = RenderSystem.getDevice() + val buffer = gpuDevice.createBuffer( + { "Lambda Styled Screen Text Buffer" }, + GpuBuffer.USAGE_VERTEX, + built.buffer + ) + result = BufferResult(buffer, built.drawParameters.indexCount()) + built.close() + } + } + return result ?: BufferResult(null, 0) + } + } } diff --git a/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt b/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt index ed52f5ec8..e7068a86e 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt @@ -43,6 +43,17 @@ class RenderBuilder(private val cameraPos: Vec3d) { var fontAtlas: SDFFontAtlas? = null private set + /** + * Map of TextStyle to lists of text vertices for that style. + * Each text piece is grouped by its style to allow rendering with unique SDF params. + */ + val textStyleGroups = mutableMapOf>() + + /** + * Map of TextStyle to lists of screen text vertices for that style. + */ + val screenTextStyleGroups = mutableMapOf>() + fun box( box: Box, lineWidth: Float, @@ -172,7 +183,7 @@ class RenderBuilder(private val cameraPos: Vec3d) { val offsetY = style.shadow.offsetY buildTextQuads(atlas, text, startX + offsetX, offsetY, shadowColor.red, shadowColor.green, shadowColor.blue, 25, - anchorX, anchorY, anchorZ, size, rotationMatrix) + anchorX, anchorY, anchorZ, size, rotationMatrix, style) } // Glow layer (alpha 50-99 signals glow) @@ -180,7 +191,7 @@ class RenderBuilder(private val cameraPos: Vec3d) { val glowColor = style.glow.color buildTextQuads(atlas, text, startX, 0f, glowColor.red, glowColor.green, glowColor.blue, 75, - anchorX, anchorY, anchorZ, size, rotationMatrix) + anchorX, anchorY, anchorZ, size, rotationMatrix, style) } // Outline layer (alpha 100-199 signals outline) @@ -188,14 +199,14 @@ class RenderBuilder(private val cameraPos: Vec3d) { val outlineColor = style.outline.color buildTextQuads(atlas, text, startX, 0f, outlineColor.red, outlineColor.green, outlineColor.blue, 150, - anchorX, anchorY, anchorZ, size, rotationMatrix) + 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) + anchorX, anchorY, anchorZ, size, rotationMatrix, style) } // ============================================================================ @@ -400,7 +411,7 @@ class RenderBuilder(private val cameraPos: Vec3d) { val offsetY = style.shadow.offsetY * pixelSize buildScreenTextQuads(atlas, text, startX + offsetX, offsetY, shadowColor.red, shadowColor.green, shadowColor.blue, 25, - pixelX, pixelY, pixelSize) + pixelX, pixelY, pixelSize, style) } // Glow layer @@ -408,7 +419,7 @@ class RenderBuilder(private val cameraPos: Vec3d) { val glowColor = style.glow.color buildScreenTextQuads(atlas, text, startX, 0f, glowColor.red, glowColor.green, glowColor.blue, 75, - pixelX, pixelY, pixelSize) + pixelX, pixelY, pixelSize, style) } // Outline layer @@ -416,14 +427,14 @@ class RenderBuilder(private val cameraPos: Vec3d) { val outlineColor = style.outline.color buildScreenTextQuads(atlas, text, startX, 0f, outlineColor.red, outlineColor.green, outlineColor.blue, 150, - pixelX, pixelY, pixelSize) + pixelX, pixelY, pixelSize, style) } // Main text layer val mainColor = style.color buildScreenTextQuads(atlas, text, startX, 0f, mainColor.red, mainColor.green, mainColor.blue, 255, - pixelX, pixelY, pixelSize) + pixelX, pixelY, pixelSize, style) } /** @@ -437,8 +448,12 @@ class RenderBuilder(private val cameraPos: Vec3d) { 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 + pixelSize: Float, // Final text size in pixels + style: TextStyle ) { + // Get or create the vertex list for this style + val vertices = screenTextStyleGroups.getOrPut(style) { mutableListOf() } + // 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 @@ -461,10 +476,10 @@ class RenderBuilder(private val cameraPos: Vec3d) { val y1 = anchorY + startY + localY1 * pixelSize // Screen-space text uses simple 2D quads - collector.addScreenTextVertex(x0, y1, glyph.u0, glyph.v1, r, g, b, a) - collector.addScreenTextVertex(x1, y1, glyph.u1, glyph.v1, r, g, b, a) - collector.addScreenTextVertex(x1, y0, glyph.u1, glyph.v0, r, g, b, a) - collector.addScreenTextVertex(x0, y0, glyph.u0, glyph.v0, r, g, b, a) + vertices.add(RegionVertexCollector.ScreenTextVertex(x0, y1, glyph.u0, glyph.v1, r, g, b, a)) + vertices.add(RegionVertexCollector.ScreenTextVertex(x1, y1, glyph.u1, glyph.v1, r, g, b, a)) + vertices.add(RegionVertexCollector.ScreenTextVertex(x1, y0, glyph.u1, glyph.v0, r, g, b, a)) + vertices.add(RegionVertexCollector.ScreenTextVertex(x0, y0, glyph.u0, glyph.v0, r, g, b, a)) // advance is already normalized, just add it penX += glyph.advance @@ -496,8 +511,12 @@ class RenderBuilder(private val cameraPos: Vec3d) { r: Int, g: Int, b: Int, a: Int, anchorX: Float, anchorY: Float, anchorZ: Float, scale: Float, - rotationMatrix: Matrix4f? + rotationMatrix: Matrix4f?, + style: TextStyle ) { + // Get or create the vertex list for this style + val vertices = textStyleGroups.getOrPut(style) { mutableListOf() } + var penX = startX for (char in text) { val glyph = atlas.getGlyph(char.code) ?: continue @@ -510,10 +529,10 @@ class RenderBuilder(private val cameraPos: Vec3d) { if (rotationMatrix == null) { // Billboard mode: pass local offsets directly, shader handles billboard // Bottom-left, Bottom-right, Top-right, Top-left - collector.addTextVertex(x0, y1, glyph.u0, glyph.v1, r, g, b, a, anchorX, anchorY, anchorZ, scale, true) - collector.addTextVertex(x1, y1, glyph.u1, glyph.v1, r, g, b, a, anchorX, anchorY, anchorZ, scale, true) - collector.addTextVertex(x1, y0, glyph.u1, glyph.v0, r, g, b, a, anchorX, anchorY, anchorZ, scale, true) - collector.addTextVertex(x0, y0, glyph.u0, glyph.v0, r, g, b, a, anchorX, anchorY, anchorZ, scale, true) + vertices.add(RegionVertexCollector.TextVertex(x0, y1, glyph.u0, glyph.v1, r, g, b, a, anchorX, anchorY, anchorZ, scale, 0f)) + vertices.add(RegionVertexCollector.TextVertex(x1, y1, glyph.u1, glyph.v1, r, g, b, a, anchorX, anchorY, anchorZ, scale, 0f)) + vertices.add(RegionVertexCollector.TextVertex(x1, y0, glyph.u1, glyph.v0, r, g, b, a, anchorX, anchorY, anchorZ, scale, 0f)) + vertices.add(RegionVertexCollector.TextVertex(x0, y0, glyph.u0, glyph.v0, r, g, b, a, anchorX, anchorY, anchorZ, scale, 0f)) } else { // Fixed rotation mode: pre-transform offsets with rotation matrix // Scale is applied in shader, so we just apply rotation here @@ -522,10 +541,10 @@ class RenderBuilder(private val cameraPos: Vec3d) { val p2 = transformPoint(rotationMatrix, x1, -y0, 0f) val p3 = transformPoint(rotationMatrix, x0, -y0, 0f) - collector.addTextVertex(p0.x, p0.y, glyph.u0, glyph.v1, r, g, b, a, anchorX, anchorY, anchorZ, scale, false) - collector.addTextVertex(p1.x, p1.y, glyph.u1, glyph.v1, r, g, b, a, anchorX, anchorY, anchorZ, scale, false) - collector.addTextVertex(p2.x, p2.y, glyph.u1, glyph.v0, r, g, b, a, anchorX, anchorY, anchorZ, scale, false) - collector.addTextVertex(p3.x, p3.y, glyph.u0, glyph.v0, r, g, b, a, anchorX, anchorY, anchorZ, scale, false) + vertices.add(RegionVertexCollector.TextVertex(p0.x, p0.y, glyph.u0, glyph.v1, r, g, b, a, anchorX, anchorY, anchorZ, scale, 1f)) + vertices.add(RegionVertexCollector.TextVertex(p1.x, p1.y, glyph.u1, glyph.v1, r, g, b, a, anchorX, anchorY, anchorZ, scale, 1f)) + vertices.add(RegionVertexCollector.TextVertex(p2.x, p2.y, glyph.u1, glyph.v0, r, g, b, a, anchorX, anchorY, anchorZ, scale, 1f)) + vertices.add(RegionVertexCollector.TextVertex(p3.x, p3.y, glyph.u0, glyph.v0, r, g, b, a, anchorX, anchorY, anchorZ, scale, 1f)) } penX += glyph.advance diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt index 5f56acad0..bb8074d94 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt @@ -25,6 +25,7 @@ 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.RegionVertexCollector import com.lambda.graphics.mc.RenderBuilder import com.lambda.graphics.text.FontHandler import com.lambda.module.Module @@ -32,6 +33,7 @@ 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.GpuBuffer import com.mojang.blaze3d.systems.RenderSystem import net.minecraft.util.math.Vec3d import net.minecraft.world.World @@ -112,7 +114,7 @@ class ChunkedRenderer( fun render() { val cameraPos = mc.gameRenderer?.camera?.pos ?: return - val activeChunks = chunkMap.values.filter { it.renderer.hasData() } + val activeChunks = chunkMap.values.filter { it.renderer.hasData() || it.styledTextBuffers.isNotEmpty() } if (activeChunks.isEmpty()) return val modelViewMatrix = RenderMain.modelViewMatrix @@ -153,29 +155,44 @@ class ChunkedRenderer( } } - // Render Text (for any chunks that have text data) - val chunksWithText = chunkTransforms.filter { (chunkData, _) -> chunkData.renderer.hasTextData() } - if (chunksWithText.isNotEmpty()) { - // Use default font atlas for chunked text + // Render Styled Text - each style gets its own SDF params + val chunksWithStyledText = chunkTransforms.filter { (chunkData, _) -> chunkData.styledTextBuffers.isNotEmpty() } + if (chunksWithStyledText.isNotEmpty()) { val atlas = FontHandler.getDefaultFont() if (!atlas.isUploaded) atlas.upload() val textureView = atlas.textureView val sampler = atlas.sampler if (textureView != null && sampler != null) { - val sdfParams = RendererUtils.createSDFParamsBuffer() - if (sdfParams != null) { - RegionRenderer.Companion.createRenderPass("ChunkedESP Text", depthTest)?.use { pass -> - pass.setPipeline(RendererUtils.getTextPipeline(depthTest)) - RenderSystem.bindDefaultUniforms(pass) - pass.setUniform("SDFParams", sdfParams) - pass.bindTexture("Sampler0", textureView, sampler) - - chunksWithText.forEach { (chunkData, transform) -> - pass.setUniform("DynamicTransforms", transform) - chunkData.renderer.renderText(pass) + // Collect all unique styles across all chunks + val allStyles = chunksWithStyledText.flatMap { (chunkData, _) -> + chunkData.styledTextBuffers.keys + }.toSet() + + // Render each style + allStyles.forEach { style -> + val outlineWidth = style.outline?.width ?: 0f + val glowRadius = style.glow?.radius ?: 0.2f + val shadowSoftness = style.shadow?.softness ?: 0.15f + + val sdfParams = RendererUtils.createSDFParamsBuffer(outlineWidth, glowRadius, shadowSoftness) + if (sdfParams != null) { + RegionRenderer.Companion.createRenderPass("ChunkedESP Text", depthTest)?.use { pass -> + pass.setPipeline(RendererUtils.getTextPipeline(depthTest)) + RenderSystem.bindDefaultUniforms(pass) + pass.setUniform("SDFParams", sdfParams) + pass.bindTexture("Sampler0", textureView, sampler) + + chunksWithStyledText.forEach { (chunkData, transform) -> + val bufferInfo = chunkData.styledTextBuffers[style] + if (bufferInfo != null) { + val (buffer, indexCount) = bufferInfo + pass.setUniform("DynamicTransforms", transform) + RegionRenderer.renderQuadBuffer(pass, buffer, indexCount) + } + } } + sdfParams.close() } - sdfParams.close() } } } @@ -186,7 +203,7 @@ class ChunkedRenderer( * Uses orthographic projection for 2D rendering. */ fun renderScreen() { - val activeChunks = chunkMap.values.filter { it.renderer.hasScreenData() } + val activeChunks = chunkMap.values.filter { it.renderer.hasScreenData() || it.styledScreenTextBuffers.isNotEmpty() } if (activeChunks.isEmpty()) return RendererUtils.withScreenContext { @@ -212,27 +229,42 @@ class ChunkedRenderer( } } - // Render Screen Text - val chunksWithText = activeChunks.filter { it.renderer.hasScreenTextData() } - if (chunksWithText.isNotEmpty()) { + // Render Styled Screen Text + val chunksWithStyledText = activeChunks.filter { it.styledScreenTextBuffers.isNotEmpty() } + if (chunksWithStyledText.isNotEmpty()) { val atlas = FontHandler.getDefaultFont() if (!atlas.isUploaded) atlas.upload() val textureView = atlas.textureView val sampler = atlas.sampler if (textureView != null && sampler != null) { - val sdfParams = RendererUtils.createSDFParamsBuffer() - if (sdfParams != null) { - RegionRenderer.Companion.createRenderPass("ChunkedESP Screen Text", false)?.use { pass -> - pass.setPipeline(RendererUtils.screenTextPipeline) - RenderSystem.bindDefaultUniforms(pass) - pass.setUniform("DynamicTransforms", dynamicTransform) - pass.setUniform("SDFParams", sdfParams) - pass.bindTexture("Sampler0", textureView, sampler) - chunksWithText.forEach { chunkData -> - chunkData.renderer.renderScreenText(pass) + // Collect all unique styles across all chunks + val allStyles = chunksWithStyledText.flatMap { it.styledScreenTextBuffers.keys }.toSet() + + // Render each style + allStyles.forEach { style -> + val outlineWidth = style.outline?.width ?: 0f + val glowRadius = style.glow?.radius ?: 0.2f + val shadowSoftness = style.shadow?.softness ?: 0.15f + + val sdfParams = RendererUtils.createSDFParamsBuffer(outlineWidth, glowRadius, shadowSoftness) + if (sdfParams != null) { + RegionRenderer.Companion.createRenderPass("ChunkedESP Screen Text", false)?.use { pass -> + pass.setPipeline(RendererUtils.screenTextPipeline) + RenderSystem.bindDefaultUniforms(pass) + pass.setUniform("DynamicTransforms", dynamicTransform) + pass.setUniform("SDFParams", sdfParams) + pass.bindTexture("Sampler0", textureView, sampler) + + chunksWithStyledText.forEach { chunkData -> + val bufferInfo = chunkData.styledScreenTextBuffers[style] + if (bufferInfo != null) { + val (buffer, indexCount) = bufferInfo + RegionRenderer.renderQuadBuffer(pass, buffer, indexCount) + } + } } + sdfParams.close() } - sdfParams.close() } } } @@ -292,6 +324,10 @@ class ChunkedRenderer( // This chunk's own renderer val renderer = RegionRenderer() + + // Styled text buffers: maps TextStyle to (buffer, indexCount) + val styledTextBuffers = mutableMapOf>() + val styledScreenTextBuffers = mutableMapOf>() private var isDirty = false @@ -321,14 +357,45 @@ class ChunkedRenderer( } } + // Capture the styled groups for upload on main thread + val textGroups = scope.textStyleGroups.toMap() + val screenTextGroups = scope.screenTextStyleGroups.toMap() + uploadQueue.add { renderer.upload(scope.collector) + + // Clean up previous styled buffers + styledTextBuffers.values.forEach { (buffer, _) -> buffer.close() } + styledTextBuffers.clear() + styledScreenTextBuffers.values.forEach { (buffer, _) -> buffer.close() } + styledScreenTextBuffers.clear() + + // Upload styled text groups + textGroups.forEach { (style, vertices) -> + val result = RegionVertexCollector.uploadTextVertices(vertices) + if (result.buffer != null) { + styledTextBuffers[style] = result.buffer to result.indexCount + } + } + + // Upload styled screen text groups + screenTextGroups.forEach { (style, vertices) -> + val result = RegionVertexCollector.uploadScreenTextVertices(vertices) + if (result.buffer != null) { + styledScreenTextBuffers[style] = result.buffer to result.indexCount + } + } + isDirty = false } } fun close() { renderer.close() + styledTextBuffers.values.forEach { (buffer, _) -> buffer.close() } + styledTextBuffers.clear() + styledScreenTextBuffers.values.forEach { (buffer, _) -> buffer.close() } + styledScreenTextBuffers.clear() } } } diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt index 087e644a6..1cd236ae7 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt @@ -20,8 +20,10 @@ package com.lambda.graphics.mc.renderer import com.lambda.Lambda.mc import com.lambda.graphics.RenderMain import com.lambda.graphics.mc.RegionRenderer +import com.lambda.graphics.mc.RegionVertexCollector import com.lambda.graphics.mc.RenderBuilder import com.lambda.graphics.text.SDFFontAtlas +import com.mojang.blaze3d.buffers.GpuBuffer import com.mojang.blaze3d.systems.RenderSystem import net.minecraft.util.math.Vec3d import org.joml.Matrix4f @@ -63,24 +65,53 @@ class ImmediateRenderer(val name: String, var depthTest: Boolean = false) { renderBuilder = null } + // Font atlas used for current text rendering + private var currentFontAtlas: SDFFontAtlas? = null + + // Styled text buffers: maps TextStyle to (buffer, indexCount) + private val styledTextBuffers = mutableMapOf>() + private val styledScreenTextBuffers = mutableMapOf>() + /** Upload collected geometry to GPU. Must be called on main thread. */ fun upload() { + // Clean up previous styled buffers + styledTextBuffers.values.forEach { (buffer, _) -> buffer.close() } + styledTextBuffers.clear() + styledScreenTextBuffers.values.forEach { (buffer, _) -> buffer.close() } + styledScreenTextBuffers.clear() + renderBuilder?.let { s -> renderer.upload(s.collector) - // Track font atlas for text rendering currentFontAtlas = s.fontAtlas + + // Upload styled text groups + s.textStyleGroups.forEach { (style, vertices) -> + val result = RegionVertexCollector.uploadTextVertices(vertices) + if (result.buffer != null) { + styledTextBuffers[style] = result.buffer to result.indexCount + } + } + + // Upload styled screen text groups + s.screenTextStyleGroups.forEach { (style, vertices) -> + val result = RegionVertexCollector.uploadScreenTextVertices(vertices) + if (result.buffer != null) { + styledScreenTextBuffers[style] = result.buffer to result.indexCount + } + } } ?: run { renderer.clearData() currentFontAtlas = null } } - // Font atlas used for current text rendering - private var currentFontAtlas: SDFFontAtlas? = null - /** Close and release all GPU resources. */ fun close() { renderer.close() + styledTextBuffers.values.forEach { (buffer, _) -> buffer.close() } + styledTextBuffers.clear() + styledScreenTextBuffers.values.forEach { (buffer, _) -> buffer.close() } + styledScreenTextBuffers.clear() clear() } @@ -89,7 +120,7 @@ class ImmediateRenderer(val name: String, var depthTest: Boolean = false) { * we just use the base modelView matrix without additional translation. */ fun render() { - if (!renderer.hasData()) return + if (!renderer.hasData() && styledTextBuffers.isEmpty()) return val modelViewMatrix = RenderMain.modelViewMatrix @@ -117,25 +148,32 @@ class ImmediateRenderer(val name: String, var depthTest: Boolean = false) { renderer.renderEdges(pass) } - // Render Text - if (renderer.hasTextData()) { + // Render Styled Text - each style gets its own SDF params + if (styledTextBuffers.isNotEmpty()) { val atlas = currentFontAtlas if (atlas != null) { if (!atlas.isUploaded) atlas.upload() val textureView = atlas.textureView val sampler = atlas.sampler if (textureView != null && sampler != null) { - val sdfParams = RendererUtils.createSDFParamsBuffer() - if (sdfParams != null) { - RegionRenderer.Companion.createRenderPass("$name Text", depthTest)?.use { pass -> - pass.setPipeline(RendererUtils.getTextPipeline(depthTest)) - RenderSystem.bindDefaultUniforms(pass) - pass.setUniform("DynamicTransforms", dynamicTransform) - pass.setUniform("SDFParams", sdfParams) - pass.bindTexture("Sampler0", textureView, sampler) - renderer.renderText(pass) + styledTextBuffers.forEach { (style, bufferInfo) -> + val (buffer, indexCount) = bufferInfo + val outlineWidth = style.outline?.width ?: 0f + val glowRadius = style.glow?.radius ?: 0.2f + val shadowSoftness = style.shadow?.softness ?: 0.15f + + val sdfParams = RendererUtils.createSDFParamsBuffer(outlineWidth, glowRadius, shadowSoftness) + if (sdfParams != null) { + RegionRenderer.Companion.createRenderPass("$name Text", depthTest)?.use { pass -> + pass.setPipeline(RendererUtils.getTextPipeline(depthTest)) + RenderSystem.bindDefaultUniforms(pass) + pass.setUniform("DynamicTransforms", dynamicTransform) + pass.setUniform("SDFParams", sdfParams) + pass.bindTexture("Sampler0", textureView, sampler) + RegionRenderer.renderQuadBuffer(pass, buffer, indexCount) + } + sdfParams.close() } - sdfParams.close() } } } @@ -147,7 +185,7 @@ class ImmediateRenderer(val name: String, var depthTest: Boolean = false) { * This should be called after world-space render() for proper layering. */ fun renderScreen() { - if (!renderer.hasScreenData()) return + if (!renderer.hasScreenData() && styledScreenTextBuffers.isEmpty()) return RendererUtils.withScreenContext { val dynamicTransform = RendererUtils.createScreenDynamicTransform() @@ -168,25 +206,32 @@ class ImmediateRenderer(val name: String, var depthTest: Boolean = false) { renderer.renderScreenEdges(pass) } - // Render Screen Text - if (renderer.hasScreenTextData()) { + // Render Styled Screen Text - each style gets its own SDF params + if (styledScreenTextBuffers.isNotEmpty()) { val atlas = currentFontAtlas if (atlas != null) { if (!atlas.isUploaded) atlas.upload() val textureView = atlas.textureView val sampler = atlas.sampler if (textureView != null && sampler != null) { - val sdfParams = RendererUtils.createSDFParamsBuffer() - if (sdfParams != null) { - RegionRenderer.createRenderPass("$name Screen Text", false)?.use { pass -> - pass.setPipeline(RendererUtils.screenTextPipeline) - RenderSystem.bindDefaultUniforms(pass) - pass.setUniform("DynamicTransforms", dynamicTransform) - pass.setUniform("SDFParams", sdfParams) - pass.bindTexture("Sampler0", textureView, sampler) - renderer.renderScreenText(pass) + styledScreenTextBuffers.forEach { (style, bufferInfo) -> + val (buffer, indexCount) = bufferInfo + val outlineWidth = style.outline?.width ?: 0f + val glowRadius = style.glow?.radius ?: 0.2f + val shadowSoftness = style.shadow?.softness ?: 0.15f + + val sdfParams = RendererUtils.createSDFParamsBuffer(outlineWidth, glowRadius, shadowSoftness) + if (sdfParams != null) { + RegionRenderer.createRenderPass("$name Screen Text", false)?.use { pass -> + pass.setPipeline(RendererUtils.screenTextPipeline) + RenderSystem.bindDefaultUniforms(pass) + pass.setUniform("DynamicTransforms", dynamicTransform) + pass.setUniform("SDFParams", sdfParams) + pass.bindTexture("Sampler0", textureView, sampler) + RegionRenderer.renderQuadBuffer(pass, buffer, indexCount) + } + sdfParams.close() } - sdfParams.close() } } } diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/RendererUtils.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/RendererUtils.kt index ac8f6cefb..4bea87a57 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/renderer/RendererUtils.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/RendererUtils.kt @@ -39,17 +39,25 @@ object RendererUtils { private val screenProjectionMatrix = ProjectionMatrix2("lambda_screen", -1000f, 1000f, true) /** - * Create SDF params uniform buffer with default values. + * Create SDF params uniform buffer with specified or default values. * Used for SDF text rendering. + * + * @param outlineWidth Width of text outline in SDF units (0 = no outline) + * @param glowRadius Radius of glow effect in SDF units (0 = no glow) + * @param shadowSoftness Softness of shadow effect (0 = no shadow) */ - fun createSDFParamsBuffer(): GpuBuffer? { + fun createSDFParamsBuffer( + outlineWidth: Float = 0f, + glowRadius: Float = 0.2f, + shadowSoftness: Float = 0.15f + ): GpuBuffer? { val device = RenderSystem.getDevice() val buffer = MemoryUtil.memAlloc(16) return try { - buffer.putFloat(0.5f) // SDFThreshold - buffer.putFloat(0.1f) // OutlineWidth - buffer.putFloat(0.2f) // GlowRadius - buffer.putFloat(0.15f) // ShadowSoftness + buffer.putFloat(0.5f) // SDFThreshold + buffer.putFloat(outlineWidth) // OutlineWidth + buffer.putFloat(glowRadius) // GlowRadius + buffer.putFloat(shadowSoftness) // ShadowSoftness buffer.flip() device.createBuffer({ "SDFParams" }, GpuBuffer.USAGE_UNIFORM, buffer) } catch (_: Exception) { diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.kt index 51a41f070..0d7dd41ee 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.kt @@ -20,8 +20,10 @@ package com.lambda.graphics.mc.renderer import com.lambda.Lambda.mc import com.lambda.graphics.RenderMain import com.lambda.graphics.mc.RegionRenderer +import com.lambda.graphics.mc.RegionVertexCollector import com.lambda.graphics.mc.RenderBuilder import com.lambda.graphics.text.SDFFontAtlas +import com.mojang.blaze3d.buffers.GpuBuffer import com.mojang.blaze3d.systems.RenderSystem import net.minecraft.util.math.Vec3d import org.joml.Matrix4f @@ -58,23 +60,53 @@ class TickedRenderer(val name: String, var depthTest: Boolean = false) { tickCameraPos = null } + // Font atlas used for current text rendering + private var currentFontAtlas: SDFFontAtlas? = null + + // Styled text buffers: maps TextStyle to (buffer, indexCount) + private val styledTextBuffers = mutableMapOf>() + private val styledScreenTextBuffers = mutableMapOf>() + /** Upload collected geometry to GPU. Must be called on main thread. */ fun upload() { + // Clean up previous styled buffers + styledTextBuffers.values.forEach { (buffer, _) -> buffer.close() } + styledTextBuffers.clear() + styledScreenTextBuffers.values.forEach { (buffer, _) -> buffer.close() } + styledScreenTextBuffers.clear() + renderBuilder?.let { s -> renderer.upload(s.collector) currentFontAtlas = s.fontAtlas + + // Upload styled text groups + s.textStyleGroups.forEach { (style, vertices) -> + val result = RegionVertexCollector.uploadTextVertices(vertices) + if (result.buffer != null) { + styledTextBuffers[style] = result.buffer to result.indexCount + } + } + + // Upload styled screen text groups + s.screenTextStyleGroups.forEach { (style, vertices) -> + val result = RegionVertexCollector.uploadScreenTextVertices(vertices) + if (result.buffer != null) { + styledScreenTextBuffers[style] = result.buffer to result.indexCount + } + } } ?: run { renderer.clearData() currentFontAtlas = null } } - // Font atlas used for current text rendering - private var currentFontAtlas: SDFFontAtlas? = null - /** Close and release all GPU resources. */ fun close() { renderer.close() + styledTextBuffers.values.forEach { (buffer, _) -> buffer.close() } + styledTextBuffers.clear() + styledScreenTextBuffers.values.forEach { (buffer, _) -> buffer.close() } + styledScreenTextBuffers.clear() clear() } @@ -85,7 +117,7 @@ class TickedRenderer(val name: String, var depthTest: Boolean = false) { fun render() { val currentCameraPos = mc.gameRenderer?.camera?.pos ?: return val tickCamera = tickCameraPos ?: return - if (!renderer.hasData()) return + if (!renderer.hasData() && styledTextBuffers.isEmpty()) return val modelViewMatrix = RenderMain.modelViewMatrix @@ -115,25 +147,32 @@ class TickedRenderer(val name: String, var depthTest: Boolean = false) { renderer.renderEdges(pass) } - // Render Text - if (renderer.hasTextData()) { + // Render Styled Text - each style gets its own SDF params + if (styledTextBuffers.isNotEmpty()) { val atlas = currentFontAtlas if (atlas != null) { if (!atlas.isUploaded) atlas.upload() val textureView = atlas.textureView val sampler = atlas.sampler if (textureView != null && sampler != null) { - val sdfParams = RendererUtils.createSDFParamsBuffer() - if (sdfParams != null) { - RegionRenderer.createRenderPass("$name Text", depthTest)?.use { pass -> - pass.setPipeline(RendererUtils.getTextPipeline(depthTest)) - RenderSystem.bindDefaultUniforms(pass) - pass.setUniform("DynamicTransforms", dynamicTransform) - pass.setUniform("SDFParams", sdfParams) - pass.bindTexture("Sampler0", textureView, sampler) - renderer.renderText(pass) + styledTextBuffers.forEach { (style, bufferInfo) -> + val (buffer, indexCount) = bufferInfo + val outlineWidth = style.outline?.width ?: 0f + val glowRadius = style.glow?.radius ?: 0.2f + val shadowSoftness = style.shadow?.softness ?: 0.15f + + val sdfParams = RendererUtils.createSDFParamsBuffer(outlineWidth, glowRadius, shadowSoftness) + if (sdfParams != null) { + RegionRenderer.createRenderPass("$name Text", depthTest)?.use { pass -> + pass.setPipeline(RendererUtils.getTextPipeline(depthTest)) + RenderSystem.bindDefaultUniforms(pass) + pass.setUniform("DynamicTransforms", dynamicTransform) + pass.setUniform("SDFParams", sdfParams) + pass.bindTexture("Sampler0", textureView, sampler) + RegionRenderer.renderQuadBuffer(pass, buffer, indexCount) + } + sdfParams.close() } - sdfParams.close() } } } @@ -145,7 +184,7 @@ class TickedRenderer(val name: String, var depthTest: Boolean = false) { * This should be called after world-space render() for proper layering. */ fun renderScreen() { - if (!renderer.hasScreenData()) return + if (!renderer.hasScreenData() && styledScreenTextBuffers.isEmpty()) return RendererUtils.withScreenContext { val dynamicTransform = RendererUtils.createScreenDynamicTransform() @@ -166,25 +205,32 @@ class TickedRenderer(val name: String, var depthTest: Boolean = false) { renderer.renderScreenEdges(pass) } - // Render Screen Text - if (renderer.hasScreenTextData()) { + // Render Styled Screen Text - each style gets its own SDF params + if (styledScreenTextBuffers.isNotEmpty()) { val atlas = currentFontAtlas if (atlas != null) { if (!atlas.isUploaded) atlas.upload() val textureView = atlas.textureView val sampler = atlas.sampler if (textureView != null && sampler != null) { - val sdfParams = RendererUtils.createSDFParamsBuffer() - if (sdfParams != null) { - RegionRenderer.Companion.createRenderPass("$name Screen Text", false)?.use { pass -> - pass.setPipeline(RendererUtils.screenTextPipeline) - RenderSystem.bindDefaultUniforms(pass) - pass.setUniform("DynamicTransforms", dynamicTransform) - pass.setUniform("SDFParams", sdfParams) - pass.bindTexture("Sampler0", textureView, sampler) - renderer.renderScreenText(pass) + styledScreenTextBuffers.forEach { (style, bufferInfo) -> + val (buffer, indexCount) = bufferInfo + val outlineWidth = style.outline?.width ?: 0f + val glowRadius = style.glow?.radius ?: 0.2f + val shadowSoftness = style.shadow?.softness ?: 0.15f + + val sdfParams = RendererUtils.createSDFParamsBuffer(outlineWidth, glowRadius, shadowSoftness) + if (sdfParams != null) { + RegionRenderer.Companion.createRenderPass("$name Screen Text", false)?.use { pass -> + pass.setPipeline(RendererUtils.screenTextPipeline) + RenderSystem.bindDefaultUniforms(pass) + pass.setUniform("DynamicTransforms", dynamicTransform) + pass.setUniform("SDFParams", sdfParams) + pass.bindTexture("Sampler0", textureView, sampler) + RegionRenderer.renderQuadBuffer(pass, buffer, indexCount) + } + sdfParams.close() } - sdfParams.close() } } } 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 index 5a4195f62..71e63b17c 100644 --- a/src/main/resources/assets/lambda/shaders/core/screen_sdf_text.fsh +++ b/src/main/resources/assets/lambda/shaders/core/screen_sdf_text.fsh @@ -42,13 +42,15 @@ void main() { float textMask = smoothstep(SDFThreshold - smoothing, SDFThreshold + smoothing, sdfValue); alpha = alpha * (1.0 - textMask); } else if (layerType >= 50) { - // Glow layer - starts from text edge and extends outward - float glowStart = SDFThreshold - GlowRadius; - float glowEnd = SDFThreshold; + // 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 main text area - float textMask = smoothstep(SDFThreshold - smoothing, SDFThreshold + smoothing, sdfValue); - alpha = alpha * (1.0 - textMask); + // 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; diff --git a/src/main/resources/assets/lambda/shaders/core/sdf_text.fsh b/src/main/resources/assets/lambda/shaders/core/sdf_text.fsh index 3af9f372a..5796d4b93 100644 --- a/src/main/resources/assets/lambda/shaders/core/sdf_text.fsh +++ b/src/main/resources/assets/lambda/shaders/core/sdf_text.fsh @@ -43,13 +43,15 @@ void main() { float textMask = smoothstep(SDFThreshold - smoothing, SDFThreshold + smoothing, sdfValue); alpha = alpha * (1.0 - textMask); } else if (layerType >= 50) { - // Glow layer - always starts from text edge (SDFThreshold) and extends outward - float glowStart = SDFThreshold - GlowRadius; - float glowEnd = SDFThreshold; + // 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 main text area (anything inside the text edge) - float textMask = smoothstep(SDFThreshold - smoothing, SDFThreshold + smoothing, sdfValue); - alpha = alpha * (1.0 - textMask); + // 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; From 8f82a346676c16281789a6ac753c39ad79ef177f Mon Sep 17 00:00:00 2001 From: beanbag44 <107891830+beanbag44@users.noreply.github.com> Date: Mon, 19 Jan 2026 13:04:11 +0000 Subject: [PATCH 11/26] improved line anti-aliasing --- .../kotlin/com/lambda/graphics/RenderMain.kt | 51 +++-- .../com/lambda/graphics/mc/RenderBuilder.kt | 185 ++++++++++++++++-- .../graphics/mc/renderer/ChunkedRenderer.kt | 6 +- .../graphics/mc/renderer/ImmediateRenderer.kt | 6 +- .../graphics/mc/renderer/TickedRenderer.kt | 6 +- .../modules/debug/RendererTestModule.kt | 68 +++---- .../lambda/module/modules/render/Tracers.kt | 58 ++++++ .../lambda/shaders/core/advanced_lines.fsh | 35 +++- .../lambda/shaders/core/screen_lines.fsh | 38 +++- .../lambda/shaders/core/screen_lines.vsh | 4 +- 10 files changed, 354 insertions(+), 103 deletions(-) create mode 100644 src/main/kotlin/com/lambda/module/modules/render/Tracers.kt diff --git a/src/main/kotlin/com/lambda/graphics/RenderMain.kt b/src/main/kotlin/com/lambda/graphics/RenderMain.kt index 73bc17e23..bde1442a4 100644 --- a/src/main/kotlin/com/lambda/graphics/RenderMain.kt +++ b/src/main/kotlin/com/lambda/graphics/RenderMain.kt @@ -45,13 +45,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 @@ -64,27 +67,35 @@ object RenderMain { val vec = Vector4f(relX, relY, relZ, 1f) projModel.transform(vec) - // Behind camera check - if (vec.w <= 0) return null + val w = if (kotlin.math.abs(vec.w) < 0.001f) 0.001f else kotlin.math.abs(vec.w) - // Perspective divide to get NDC - val ndcX = vec.x / vec.w - val ndcY = vec.y / vec.w - val ndcZ = vec.z / vec.w + // Perspective divide to get NDC (-1 to 1) + val ndcX = vec.x / w + val ndcY = vec.y / w - // Off-screen check (NDC is -1 to 1) - if (ndcZ < -1 || ndcZ > 1) return null + // NDC to normalized 0-1 coordinates (Y is flipped: 0 = top, 1 = bottom) + val normalizedX = (ndcX + 1f) * 0.5f + val normalizedY = (1f - ndcY) * 0.5f - // 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 - - 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) { diff --git a/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt b/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt index e7068a86e..d4c59ce7f 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt @@ -44,15 +44,15 @@ class RenderBuilder(private val cameraPos: Vec3d) { private set /** - * Map of TextStyle to lists of text vertices for that style. + * Map of SDFStyle to lists of text vertices for that style. * Each text piece is grouped by its style to allow rendering with unique SDF params. */ - val textStyleGroups = mutableMapOf>() + val textStyleGroups = mutableMapOf>() /** - * Map of TextStyle to lists of screen text vertices for that style. + * Map of SDFStyle to lists of screen text vertices for that style. */ - val screenTextStyleGroups = mutableMapOf>() + val screenTextStyleGroups = mutableMapOf>() fun box( box: Box, @@ -92,6 +92,19 @@ class RenderBuilder(private val cameraPos: Vec3d) { 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, @@ -132,6 +145,138 @@ class RenderBuilder(private val cameraPos: Vec3d) { 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. @@ -149,7 +294,7 @@ class RenderBuilder(private val cameraPos: Vec3d) { pos: Vec3d, size: Float = 0.5f, font: SDFFontAtlas? = null, - style: TextStyle = TextStyle(), + style: SDFStyle = SDFStyle(), centered: Boolean = true, rotation: Vec3d? = null ) { @@ -386,7 +531,7 @@ class RenderBuilder(private val cameraPos: Vec3d) { y: Float, size: Float = 0.02f, font: SDFFontAtlas? = null, - style: TextStyle = TextStyle(), + style: SDFStyle = SDFStyle(), centered: Boolean = false ) { val atlas = font ?: FontHandler.getDefaultFont() @@ -449,7 +594,7 @@ class RenderBuilder(private val cameraPos: Vec3d) { r: Int, g: Int, b: Int, a: Int, anchorX: Float, anchorY: Float, pixelSize: Float, // Final text size in pixels - style: TextStyle + style: SDFStyle ) { // Get or create the vertex list for this style val vertices = screenTextStyleGroups.getOrPut(style) { mutableListOf() } @@ -512,7 +657,7 @@ class RenderBuilder(private val cameraPos: Vec3d) { anchorX: Float, anchorY: Float, anchorZ: Float, scale: Float, rotationMatrix: Matrix4f?, - style: TextStyle + style: SDFStyle ) { // Get or create the vertex list for this style val vertices = textStyleGroups.getOrPut(style) { mutableListOf() } @@ -601,24 +746,24 @@ class RenderBuilder(private val cameraPos: Vec3d) { collector.addFaceVertex(rx, ry, rz, color) } - /** Outline effect configuration */ - data class TextOutline( + /** 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) ) - /** Glow effect configuration */ - data class TextGlow( + /** 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 ) - /** Shadow effect configuration */ - data class TextShadow( + /** SDF shadow effect configuration */ + data class SDFShadow( val color: Color = Color(0, 0, 0, 180), val offset: Float = 0.05f, // Distance in text units val angle: Float = 135f, // Angle in degrees: 0=right, 90=down, 180=left, 270=up (default: bottom-right) - val softness: Float = 0.15f // Shadow blur in SDF units (for documentation, not currently used) + val softness: Float = 0.15f // 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() @@ -626,11 +771,11 @@ class RenderBuilder(private val cameraPos: Vec3d) { val offsetY: Float get() = offset * kotlin.math.sin(Math.toRadians(angle.toDouble())).toFloat() } - /** Text style configuration */ - data class TextStyle( + /** SDF style configuration for text and other SDF-rendered elements */ + data class SDFStyle( val color: Color = Color.WHITE, - val outline: TextOutline? = null, - val glow: TextGlow? = null, - val shadow: TextShadow? = TextShadow() // Default shadow enabled + 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/renderer/ChunkedRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt index bb8074d94..ee6ea80d0 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt @@ -325,9 +325,9 @@ class ChunkedRenderer( // This chunk's own renderer val renderer = RegionRenderer() - // Styled text buffers: maps TextStyle to (buffer, indexCount) - val styledTextBuffers = mutableMapOf>() - val styledScreenTextBuffers = mutableMapOf>() + // Styled text buffers: maps SDFStyle to (buffer, indexCount) + val styledTextBuffers = mutableMapOf>() + val styledScreenTextBuffers = mutableMapOf>() private var isDirty = false diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt index 1cd236ae7..168f2a925 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt @@ -68,9 +68,9 @@ class ImmediateRenderer(val name: String, var depthTest: Boolean = false) { // Font atlas used for current text rendering private var currentFontAtlas: SDFFontAtlas? = null - // Styled text buffers: maps TextStyle to (buffer, indexCount) - private val styledTextBuffers = mutableMapOf>() - private val styledScreenTextBuffers = mutableMapOf>() + // Styled text buffers: maps SDFStyle to (buffer, indexCount) + private val styledTextBuffers = mutableMapOf>() + private val styledScreenTextBuffers = mutableMapOf>() /** Upload collected geometry to GPU. Must be called on main thread. */ fun upload() { diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.kt index 0d7dd41ee..118e3a072 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.kt @@ -63,9 +63,9 @@ class TickedRenderer(val name: String, var depthTest: Boolean = false) { // Font atlas used for current text rendering private var currentFontAtlas: SDFFontAtlas? = null - // Styled text buffers: maps TextStyle to (buffer, indexCount) - private val styledTextBuffers = mutableMapOf>() - private val styledScreenTextBuffers = mutableMapOf>() + // Styled text buffers: maps SDFStyle to (buffer, indexCount) + private val styledTextBuffers = mutableMapOf>() + private val styledScreenTextBuffers = mutableMapOf>() /** Upload collected geometry to GPU. Must be called on main thread. */ fun upload() { diff --git a/src/main/kotlin/com/lambda/module/modules/debug/RendererTestModule.kt b/src/main/kotlin/com/lambda/module/modules/debug/RendererTestModule.kt index f18c6fcbe..6afa40d97 100644 --- a/src/main/kotlin/com/lambda/module/modules/debug/RendererTestModule.kt +++ b/src/main/kotlin/com/lambda/module/modules/debug/RendererTestModule.kt @@ -22,10 +22,10 @@ import com.lambda.event.events.TickEvent import com.lambda.event.listener.SafeListener.Companion.listen import com.lambda.graphics.mc.LineDashStyle.Companion.marchingAnts import com.lambda.graphics.mc.LineDashStyle.Companion.screenMarchingAnts -import com.lambda.graphics.mc.RenderBuilder.TextGlow -import com.lambda.graphics.mc.RenderBuilder.TextOutline -import com.lambda.graphics.mc.RenderBuilder.TextShadow -import com.lambda.graphics.mc.RenderBuilder.TextStyle +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.chunkedEsp import com.lambda.graphics.mc.renderer.ImmediateRenderer import com.lambda.graphics.mc.renderer.TickedRenderer @@ -71,10 +71,10 @@ object ChunkedRendererTest : Module( worldText( "Test sdf font!", startPos.offset(Direction.EAST, 5.0), - style = TextStyle( - outline = TextOutline(), - glow = TextGlow(), - shadow = TextShadow() + style = SDFStyle( + outline = SDFOutline(), + glow = SDFGlow(), + shadow = SDFShadow() ) ) @@ -100,10 +100,10 @@ object ChunkedRendererTest : Module( 0.02f, 0.30f, size = 0.025f, // 2.5% of screen - style = TextStyle( + style = SDFStyle( color = Color.WHITE, - outline = TextOutline(), - shadow = TextShadow() + outline = SDFOutline(), + shadow = SDFShadow() ) ) @@ -113,10 +113,10 @@ object ChunkedRendererTest : Module( 0.5f, // 50% from left = center 0.05f, // 5% from top size = 0.03f, // 3% of screen - style = TextStyle( + style = SDFStyle( color = Color.YELLOW, - glow = TextGlow(Color(255, 200, 0, 150)), - shadow = TextShadow() + glow = SDFGlow(Color(255, 200, 0, 150)), + shadow = SDFShadow() ), centered = true ) @@ -171,10 +171,10 @@ object TickedRendererTest : Module( worldText( "Test sdf font!", startPos.offset(Direction.EAST, 5.0), - style = TextStyle( - outline = TextOutline(), - glow = TextGlow(), - shadow = TextShadow() + style = SDFStyle( + outline = SDFOutline(), + glow = SDFGlow(), + shadow = SDFShadow() ) ) @@ -200,10 +200,10 @@ object TickedRendererTest : Module( 0.02f, 0.30f, size = 0.025f, // 2.5% of screen - style = TextStyle( + style = SDFStyle( color = Color.WHITE, - outline = TextOutline(), - shadow = TextShadow() + outline = SDFOutline(), + shadow = SDFShadow() ) ) @@ -213,10 +213,10 @@ object TickedRendererTest : Module( 0.5f, // 50% from left = center 0.05f, // 5% from top size = 0.03f, // 3% of screen - style = TextStyle( + style = SDFStyle( color = Color.YELLOW, - glow = TextGlow(Color(255, 200, 0, 150)), - shadow = TextShadow() + glow = SDFGlow(Color(255, 200, 0, 150)), + shadow = SDFShadow() ), centered = true ) @@ -259,10 +259,10 @@ object ImmediateRendererTest : Module( worldText( "Test sdf font!", startPos.offset(Direction.EAST, 5.0), - style = TextStyle( - outline = TextOutline(), - glow = TextGlow(), - shadow = TextShadow() + style = SDFStyle( + outline = SDFOutline(), + glow = SDFGlow(), + shadow = SDFShadow() ) ) @@ -288,10 +288,10 @@ object ImmediateRendererTest : Module( 0.02f, 0.30f, size = 0.025f, // 2.5% of screen - style = TextStyle( + style = SDFStyle( color = Color.WHITE, - outline = TextOutline(), - shadow = TextShadow() + outline = SDFOutline(), + shadow = SDFShadow() ) ) @@ -301,10 +301,10 @@ object ImmediateRendererTest : Module( 0.5f, // 50% from left = center 0.05f, // 5% from top size = 0.03f, // 3% of screen - style = TextStyle( + style = SDFStyle( color = Color.YELLOW, - glow = TextGlow(Color(255, 200, 0, 150)), - shadow = TextShadow() + glow = SDFGlow(Color(255, 200, 0, 150)), + shadow = SDFShadow() ), centered = true ) 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..8ecec0bb7 --- /dev/null +++ b/src/main/kotlin/com/lambda/module/modules/render/Tracers.kt @@ -0,0 +1,58 @@ +/* + * 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.event.events.RenderEvent +import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.graphics.RenderMain +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 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 val color by setting("Color", Color.RED) + private val friendColor by setting("Friend Color", Color.BLUE) + private val width by setting("Width", 0.0004f, 0.0001f..0.01f, 0.0001f) + + val renderer = ImmediateRenderer("Tracers") + + init { + listen { + renderer.tick() + renderer.shapes { + world.entities.forEach { entity -> + val (toX, toY) = RenderMain.worldToScreenNormalized(lerp(mc.tickDelta, entity.prevPos, entity.pos)) ?: return@forEach + screenLine(0.5f, 0.5f, toX, toY, color, width) + } + } + renderer.upload() + renderer.render() + renderer.renderScreen() + } + } +} \ No newline at end of file diff --git a/src/main/resources/assets/lambda/shaders/core/advanced_lines.fsh b/src/main/resources/assets/lambda/shaders/core/advanced_lines.fsh index 7659d14b9..b88072616 100644 --- a/src/main/resources/assets/lambda/shaders/core/advanced_lines.fsh +++ b/src/main/resources/assets/lambda/shaders/core/advanced_lines.fsh @@ -35,24 +35,41 @@ void main() { vec3 toFragment = v_ExpandedPos - lineStart; float projLength = dot(toFragment, lineDir); + // Perpendicular distance - stable for AA calculation along the line body + vec3 perpVec = toFragment - lineDir * projLength; + float perpDist = length(perpVec); + // Clamp to segment bounds [0, segmentLength] for capsule behavior float clampedProj = clamp(projLength, 0.0, v_SegmentLength); - // Closest point on line segment - vec3 closestPoint = lineStart + lineDir * clampedProj; - - // 3D distance from fragment to closest point on line - float dist3D = length(v_ExpandedPos - closestPoint); + // For end caps, we need the actual distance to the endpoint + float dist3D; + if (projLength < 0.0) { + // Before start - distance to start point + dist3D = length(v_ExpandedPos - lineStart); + } else if (projLength > v_SegmentLength) { + // After end - distance to end point + dist3D = length(v_ExpandedPos - lineEnd); + } else { + // Along the line - use perpendicular distance + dist3D = perpDist; + } // SDF: distance to capsule surface (positive = outside, negative = inside) float sdf = dist3D - radius; - // Anti-aliasing using screen-space derivatives - float aaWidth = fwidth(sdf); - float alpha = 1.0 - smoothstep(-aaWidth, aaWidth, sdf); + // Calculate AA width from screen-space derivatives of expanded position + float aaWidth = length(vec2(fwidth(v_ExpandedPos.x), fwidth(v_ExpandedPos.y))); + + // Adaptive AA: thin lines get softer edges, thick lines get crisp edges + // Below 2px width, scale up AA for smooth thin lines; above 2px, use tight 0.5px AA + float thinness = clamp(1.0 - v_LineWidth / (2.0 * aaWidth), 0.0, 1.0); + float adaptiveAA = mix(aaWidth * 0.5, aaWidth * 1.5, thinness); + + float alpha = 1.0 - smoothstep(-adaptiveAA, adaptiveAA, sdf); // Skip fragments outside the line - if (alpha <= 0.0) { + if (alpha < 0.004) { discard; } diff --git a/src/main/resources/assets/lambda/shaders/core/screen_lines.fsh b/src/main/resources/assets/lambda/shaders/core/screen_lines.fsh index aba53911c..f41310310 100644 --- a/src/main/resources/assets/lambda/shaders/core/screen_lines.fsh +++ b/src/main/resources/assets/lambda/shaders/core/screen_lines.fsh @@ -17,30 +17,50 @@ out vec4 fragColor; void main() { // ===== CAPSULE SDF ===== vec2 lineDir = normalize(v_LineEnd - v_LineStart); - float radius = v_LineWidth / 2.0; + vec2 perpDir = vec2(-lineDir.y, lineDir.x); // Perpendicular to line // Project fragment position onto line to find closest point vec2 toFragment = v_ExpandedPos - v_LineStart; float projLength = dot(toFragment, lineDir); + // Perpendicular distance (signed) - this is stable for AA calculation + float perpDist = abs(dot(toFragment, perpDir)); + // Clamp to segment bounds [0, segmentLength] for capsule behavior float clampedProj = clamp(projLength, 0.0, v_SegmentLength); - // Closest point on line segment - vec2 closestPoint = v_LineStart + lineDir * clampedProj; + // For end caps, we need the actual distance to the endpoint + float dist2D; + if (projLength < 0.0) { + // Before start - distance to start point + dist2D = length(v_ExpandedPos - v_LineStart); + } else if (projLength > v_SegmentLength) { + // After end - distance to end point + dist2D = length(v_ExpandedPos - v_LineEnd); + } else { + // Along the line - use perpendicular distance + dist2D = perpDist; + } + + // Calculate AA width from screen-space derivatives of fragment position + // This is always stable regardless of line orientation + float aaWidth = length(vec2(fwidth(v_ExpandedPos.x), fwidth(v_ExpandedPos.y))); - // 2D distance from fragment to closest point on line - float dist2D = length(v_ExpandedPos - closestPoint); + // Use requested line width - no minimum enforcement for thin lines + float radius = v_LineWidth * 0.5; // SDF: distance to capsule surface (positive = outside, negative = inside) float sdf = dist2D - radius; - // Anti-aliasing using screen-space derivatives (same as world-space) - float aaWidth = fwidth(sdf); - float alpha = 1.0 - smoothstep(-aaWidth, aaWidth, sdf); + // Adaptive AA: thin lines get softer edges, thick lines get crisp edges + // Below 2px width, scale up AA for smooth thin lines; above 2px, use tight 0.5px AA + float thinness = clamp(1.0 - v_LineWidth / (2.0 * aaWidth), 0.0, 1.0); + float adaptiveAA = mix(aaWidth * 0.5, aaWidth * 1.5, thinness); + + float alpha = 1.0 - smoothstep(-adaptiveAA, adaptiveAA, sdf); // Skip fragments outside the line - if (alpha <= 0.0) { + if (alpha < 0.004) { discard; } diff --git a/src/main/resources/assets/lambda/shaders/core/screen_lines.vsh b/src/main/resources/assets/lambda/shaders/core/screen_lines.vsh index 8213acbd1..b1ac4d248 100644 --- a/src/main/resources/assets/lambda/shaders/core/screen_lines.vsh +++ b/src/main/resources/assets/lambda/shaders/core/screen_lines.vsh @@ -41,9 +41,9 @@ void main() { // Perpendicular direction for line thickness vec2 perpDir = vec2(-lineDir.y, lineDir.x); - // Expand for AA (capsule shape) + // Expand for AA (capsule shape) - ensure minimum expansion for thin lines float halfWidth = LineWidth / 2.0; - float aaPadding = LineWidth * 0.25 + 1.0; // Scale-aware padding + float aaPadding = max(LineWidth * 0.5, 2.0); // At least 2 pixels for AA gradient float halfWidthPadded = halfWidth + aaPadding; // Expand vertex From 0e9eff8094def8b9eaaded6880f633c887eaf38b Mon Sep 17 00:00:00 2001 From: beanbag44 <107891830+beanbag44@users.noreply.github.com> Date: Mon, 19 Jan 2026 13:22:18 +0000 Subject: [PATCH 12/26] fix zoom not affecting tracers --- src/main/java/com/lambda/mixin/render/GameRendererMixin.java | 4 ++-- src/main/kotlin/com/lambda/module/modules/render/Zoom.kt | 5 ----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/lambda/mixin/render/GameRendererMixin.java b/src/main/java/com/lambda/mixin/render/GameRendererMixin.java index 5f3db5280..87791ef08 100644 --- a/src/main/java/com/lambda/mixin/render/GameRendererMixin.java +++ b/src/main/java/com/lambda/mixin/render/GameRendererMixin.java @@ -55,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.render3D(positionMatrix, basicProjectionMatrix); } @ModifyExpressionValue(method = "renderWorld", at = @At(value = "INVOKE", target = "Ljava/lang/Math;max(FF)F", ordinal = 0)) @@ -71,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(); } 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 e8b9d82db..8c48e72d1 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/Zoom.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/Zoom.kt @@ -18,7 +18,6 @@ package com.lambda.module.modules.render import com.lambda.event.events.ButtonEvent -import com.lambda.event.events.RenderEvent import com.lambda.event.listener.SafeListener.Companion.listen import com.lambda.module.Module import com.lambda.module.tag.ModuleTag @@ -61,10 +60,6 @@ object Zoom : Module( event.cancel() } - listen(alwaysListen = true) { - updateCurrentZoom() - } - onEnable { updateZoomTime() } From f0bbefdf6e1afc9267d0b7043fb86994c2482d14 Mon Sep 17 00:00:00 2001 From: beanbag44 <107891830+beanbag44@users.noreply.github.com> Date: Tue, 20 Jan 2026 11:41:46 +0000 Subject: [PATCH 13/26] have tracers go off-screen when a target is behind the camera --- .../kotlin/com/lambda/graphics/RenderMain.kt | 20 ++++- .../graphics/mc/renderer/ImmediateRenderer.kt | 6 +- .../module/modules/render/BlockOutline.kt | 18 ++--- .../lambda/module/modules/render/NoRender.kt | 62 +++------------- .../lambda/module/modules/render/Tracers.kt | 32 +++++++- .../kotlin/com/lambda/util/EntityUtils.kt | 73 +++++++++++++++++++ .../lambda/util/reflections/Reflections.kt | 1 - 7 files changed, 140 insertions(+), 72 deletions(-) diff --git a/src/main/kotlin/com/lambda/graphics/RenderMain.kt b/src/main/kotlin/com/lambda/graphics/RenderMain.kt index bde1442a4..610ca56f0 100644 --- a/src/main/kotlin/com/lambda/graphics/RenderMain.kt +++ b/src/main/kotlin/com/lambda/graphics/RenderMain.kt @@ -67,11 +67,27 @@ object RenderMain { val vec = Vector4f(relX, relY, relZ, 1f) projModel.transform(vec) + val isBehind = vec.w < 0 val w = if (kotlin.math.abs(vec.w) < 0.001f) 0.001f else kotlin.math.abs(vec.w) // Perspective divide to get NDC (-1 to 1) - val ndcX = vec.x / w - val ndcY = vec.y / w + 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) + ndcY = 3f + } + } // NDC to normalized 0-1 coordinates (Y is flipped: 0 = top, 1 = bottom) val normalizedX = (ndcX + 1f) * 0.5f diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt index 168f2a925..d9bdd3b83 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt @@ -133,7 +133,7 @@ class ImmediateRenderer(val name: String, var depthTest: Boolean = false) { ) // Render Faces - RegionRenderer.Companion.createRenderPass("$name Faces", depthTest)?.use { pass -> + RegionRenderer.createRenderPass("$name Faces", depthTest)?.use { pass -> pass.setPipeline(RendererUtils.getFacesPipeline(depthTest)) RenderSystem.bindDefaultUniforms(pass) pass.setUniform("DynamicTransforms", dynamicTransform) @@ -141,7 +141,7 @@ class ImmediateRenderer(val name: String, var depthTest: Boolean = false) { } // Render Edges - RegionRenderer.Companion.createRenderPass("$name Edges", depthTest)?.use { pass -> + RegionRenderer.createRenderPass("$name Edges", depthTest)?.use { pass -> pass.setPipeline(RendererUtils.getEdgesPipeline(depthTest)) RenderSystem.bindDefaultUniforms(pass) pass.setUniform("DynamicTransforms", dynamicTransform) @@ -164,7 +164,7 @@ class ImmediateRenderer(val name: String, var depthTest: Boolean = false) { val sdfParams = RendererUtils.createSDFParamsBuffer(outlineWidth, glowRadius, shadowSoftness) if (sdfParams != null) { - RegionRenderer.Companion.createRenderPass("$name Text", depthTest)?.use { pass -> + RegionRenderer.createRenderPass("$name Text", depthTest)?.use { pass -> pass.setPipeline(RendererUtils.getTextPipeline(depthTest)) RenderSystem.bindDefaultUniforms(pass) pass.setUniform("DynamicTransforms", dynamicTransform) diff --git a/src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt b/src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt index d452c8890..2effbb983 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt @@ -27,7 +27,6 @@ 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.block.BlockState import net.minecraft.util.math.Box import java.awt.Color @@ -47,7 +46,7 @@ object BlockOutline : Module( val renderer = ImmediateRenderer("BlockOutline") - var previous: Pair, BlockState>? = null + var previous: List? = null init { listen { @@ -63,10 +62,10 @@ object BlockOutline : Module( boxes.mapIndexed { index, box -> val offset = box.offset(pos) val interpolated = previous?.let { previous -> - if (!interpolate || previous.first.size < boxes.size) null - else lerp(mc.tickDelta, previous.first[index], offset) + if (!interpolate || previous.size < boxes.size) null + else lerp(mc.tickDelta, previous[index], offset) } ?: offset - interpolated.expand(0.001) + interpolated.expand(0.0001) } } @@ -87,12 +86,9 @@ object BlockOutline : Module( listen { val hitResult = mc.crosshairTarget?.blockResult ?: return@listen val state = blockState(hitResult.blockPos) - previous = Pair( - state - .getOutlineShape(world, hitResult.blockPos).boundingBoxes - .map { it.offset(hitResult.blockPos) }, - state - ) + 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/NoRender.kt b/src/main/kotlin/com/lambda/module/modules/render/NoRender.kt index e0f576482..27a27dab8 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("(? + if (entity === player) return@forEach + val entityGroup = entity.entityGroup + if (entityGroup !in entities) return@forEach + val color = if (entity is OtherClientPlayerEntity && entity.isFriend) friendColor + 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 (toX, toY) = RenderMain.worldToScreenNormalized(lerp(mc.tickDelta, entity.prevPos, entity.pos)) ?: return@forEach - screenLine(0.5f, 0.5f, toX, toY, color, width) + screenLine(0.5f, 0.5f, toX, toY, color, width * 0.0001f) } } renderer.upload() diff --git a/src/main/kotlin/com/lambda/util/EntityUtils.kt b/src/main/kotlin/com/lambda/util/EntityUtils.kt index ebecce7da..0d20fc390 100644 --- a/src/main/kotlin/com/lambda/util/EntityUtils.kt +++ b/src/main/kotlin/com/lambda/util/EntityUtils.kt @@ -17,11 +17,47 @@ 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 net.minecraft.block.entity.BlockEntity import net.minecraft.entity.Entity import net.minecraft.util.math.BlockPos object EntityUtils { + val entities = 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: EntityGroup + get() { + 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 +74,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("(?.className: String get() = java.name .substringAfter("${java.packageName}.") .replace('$', '.') - /** * This function returns a instance of subtype [T]. * From 900a5e2a05ac489a216d2a41349970ffcf0d4766 Mon Sep 17 00:00:00 2001 From: beanbag44 <107891830+beanbag44@users.noreply.github.com> Date: Tue, 20 Jan 2026 13:44:20 +0000 Subject: [PATCH 14/26] tracer improvements --- .../lambda/module/modules/render/Tracers.kt | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/com/lambda/module/modules/render/Tracers.kt b/src/main/kotlin/com/lambda/module/modules/render/Tracers.kt index 8522795a1..c5c85f423 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/Tracers.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/Tracers.kt @@ -28,6 +28,7 @@ import com.lambda.util.EntityUtils.EntityGroup import com.lambda.util.EntityUtils.entityGroup 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.component1 @@ -42,13 +43,16 @@ object Tracers : Module( private val friendColor by setting("Friend Color", Color.BLUE) private val width by setting("Width", 1, 1..50, 1) private val entities by setting("Entities", setOf(EntityGroup.Player, EntityGroup.Mob, EntityGroup.Boss), EntityGroup.entries) - private val playerColor by setting("Players", Color.RED) { EntityGroup.Player in entities } - private val mobColor by setting("Mobs", Color(255, 40, 40, 255)) { EntityGroup.Mob in entities } + private val playerDistanceGradient by setting("Player Distance Gradient", true) { EntityGroup.Player in entities } + private val playerDistanceColorFar by setting("Player Far Color", Color.GREEN) { EntityGroup.Player in entities && playerDistanceGradient } + private val playerDistanceColorClose by setting("Player Close Color", Color.RED) { EntityGroup.Player in entities && playerDistanceGradient } + private val playerColor by setting("Players", Color.RED) { EntityGroup.Player in entities && !playerDistanceGradient } + private val mobColor by setting("Mobs", Color(255, 80, 0, 255)) { EntityGroup.Mob in entities } private val passiveColor by setting("Passives", Color.BLUE) { EntityGroup.Passive in entities } private val projectileColor by setting("Projectiles", Color.LIGHT_GRAY) { EntityGroup.Projectile in entities } private val vehicleColor by setting("Vehicles", Color.WHITE) { EntityGroup.Vehicle in entities } private val decorationColor by setting("Decorations", Color.PINK) { EntityGroup.Decoration in entities } - private val bossColor by setting("Bosses", Color.RED) { EntityGroup.Boss in entities } + private val bossColor by setting("Bosses", Color(255, 0, 255, 255)) { EntityGroup.Boss in entities } private val miscColor by setting("Miscellaneous", Color.magenta) { EntityGroup.Misc in entities } val renderer = ImmediateRenderer("Tracers") @@ -58,11 +62,17 @@ object Tracers : Module( renderer.tick() renderer.shapes { 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 && entity.isFriend) friendColor - else when (entityGroup) { + 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 From a6e6a6fcf7e9b65648475c7f391c2e7cf7faceb9 Mon Sep 17 00:00:00 2001 From: beanbag44 <107891830+beanbag44@users.noreply.github.com> Date: Tue, 20 Jan 2026 18:32:45 +0000 Subject: [PATCH 15/26] item screen rendering --- .../lambda/mixin/render/InGameHudMixin.java | 39 +++++++--- .../com/lambda/event/events/HudRenderEvent.kt | 28 +++++++ .../com/lambda/graphics/mc/RenderBuilder.kt | 31 ++++++++ .../graphics/mc/renderer/ImmediateRenderer.kt | 11 ++- .../graphics/mc/renderer/RendererUtils.kt | 77 +++++++++++++++++++ .../graphics/mc/renderer/TickedRenderer.kt | 11 ++- .../modules/debug/RendererTestModule.kt | 10 +++ 7 files changed, 195 insertions(+), 12 deletions(-) create mode 100644 src/main/kotlin/com/lambda/event/events/HudRenderEvent.kt diff --git a/src/main/java/com/lambda/mixin/render/InGameHudMixin.java b/src/main/java/com/lambda/mixin/render/InGameHudMixin.java index d6ca4f2ca..d5b0aaecf 100644 --- a/src/main/java/com/lambda/mixin/render/InGameHudMixin.java +++ b/src/main/java/com/lambda/mixin/render/InGameHudMixin.java @@ -17,7 +17,8 @@ package com.lambda.mixin.render; -import com.lambda.gui.DearImGui; +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; @@ -38,32 +39,38 @@ public class InGameHudMixin { @Inject(method = "renderNauseaOverlay", at = @At("HEAD"), cancellable = true) private void injectRenderNauseaOverlay(DrawContext context, float nauseaStrength, CallbackInfo ci) { - if (NoRender.INSTANCE.isEnabled() && NoRender.getNoNausea()) ci.cancel(); + if (NoRender.INSTANCE.isEnabled() && NoRender.getNoNausea()) + ci.cancel(); } @Inject(method = "renderPortalOverlay", at = @At("HEAD"), cancellable = true) private void injectRenderPortalOverlay(DrawContext context, float nauseaStrength, CallbackInfo ci) { - if (NoRender.INSTANCE.isEnabled() && NoRender.getNoPortalOverlay()) ci.cancel(); + if (NoRender.INSTANCE.isEnabled() && NoRender.getNoPortalOverlay()) + ci.cancel(); } @Inject(method = "renderVignetteOverlay", at = @At("HEAD"), cancellable = true) private void injectRenderVignetteOverlay(DrawContext context, Entity entity, CallbackInfo ci) { - if (NoRender.INSTANCE.isEnabled() && NoRender.getNoVignette()) ci.cancel(); + if (NoRender.INSTANCE.isEnabled() && NoRender.getNoVignette()) + ci.cancel(); } @Inject(method = "renderStatusEffectOverlay", at = @At("HEAD"), cancellable = true) private void injectRenderStatusEffectOverlay(DrawContext context, RenderTickCounter tickCounter, CallbackInfo ci) { - if (NoRender.INSTANCE.isEnabled() && NoRender.getNoStatusEffects()) ci.cancel(); + if (NoRender.INSTANCE.isEnabled() && NoRender.getNoStatusEffects()) + ci.cancel(); } @Inject(method = "renderSpyglassOverlay", at = @At("HEAD"), cancellable = true) private void injectRenderSpyglassOverlay(DrawContext context, float scale, CallbackInfo ci) { - if (NoRender.INSTANCE.isEnabled() && NoRender.getNoSpyglassOverlay()) ci.cancel(); + if (NoRender.INSTANCE.isEnabled() && NoRender.getNoSpyglassOverlay()) + ci.cancel(); } @ModifyArgs(method = "renderMiscOverlays", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/hud/InGameHud;renderOverlay(Lnet/minecraft/client/gui/DrawContext;Lnet/minecraft/util/Identifier;F)V")) private void modifyRenderOverlayArgs(Args args) { - if (!((Identifier) args.get(1)).getPath().contains("pumpkin")) return; + if (!((Identifier) args.get(1)).getPath().contains("pumpkin")) + return; if (NoRender.INSTANCE.isEnabled() && NoRender.getNoPumpkinOverlay()) { args.set(2, 0f); } @@ -75,12 +82,24 @@ private int modifyIsFirstPerson(int original) { } @Inject(method = "renderScoreboardSidebar(Lnet/minecraft/client/gui/DrawContext;Lnet/minecraft/scoreboard/ScoreboardObjective;)V", at = @At("HEAD"), cancellable = true) - private void injectRenderScoreboardSidebar(DrawContext drawContext, ScoreboardObjective objective, CallbackInfo ci) { - if (NoRender.INSTANCE.isEnabled() && NoRender.getNoScoreBoard()) ci.cancel(); + private void injectRenderScoreboardSidebar(DrawContext drawContext, ScoreboardObjective objective, + CallbackInfo ci) { + if (NoRender.INSTANCE.isEnabled() && NoRender.getNoScoreBoard()) + ci.cancel(); } @Inject(method = "renderCrosshair", at = @At("HEAD"), cancellable = true) private void injectRenderCrosshair(DrawContext context, RenderTickCounter tickCounter, CallbackInfo ci) { - if (NoRender.INSTANCE.isEnabled() && NoRender.getNoCrosshair()) ci.cancel(); + 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/kotlin/com/lambda/event/events/HudRenderEvent.kt b/src/main/kotlin/com/lambda/event/events/HudRenderEvent.kt new file mode 100644 index 000000000..bede02152 --- /dev/null +++ b/src/main/kotlin/com/lambda/event/events/HudRenderEvent.kt @@ -0,0 +1,28 @@ +/* + * 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 +import net.minecraft.client.gui.DrawContext + +/** + * 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/graphics/mc/RenderBuilder.kt b/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt index d4c59ce7f..d736f8c5c 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt @@ -24,6 +24,7 @@ import com.lambda.graphics.text.SDFFontAtlas import com.lambda.graphics.util.DirectionMask import com.lambda.util.BlockUtils.blockState import net.minecraft.block.BlockState +import net.minecraft.item.ItemStack import net.minecraft.util.math.BlockPos import net.minecraft.util.math.Box import net.minecraft.util.math.Vec3d @@ -54,6 +55,12 @@ class RenderBuilder(private val cameraPos: Vec3d) { */ val screenTextStyleGroups = mutableMapOf>() + /** + * Deferred ItemStack renders to be drawn via Minecraft's DrawContext. + * These are rendered after Lambda's geometry for proper layering. + */ + val deferredItems = mutableListOf() + fun box( box: Box, lineWidth: Float, @@ -513,6 +520,22 @@ class RenderBuilder(private val cameraPos: Vec3d) { dashStyle: LineDashStyle? = null ) = screenLineGradient(x1, y1, color, x2, y2, color, width, dashStyle) + /** + * Queue an ItemStack to be rendered on screen. + * Items are rendered after Lambda's geometry using Minecraft's DrawContext. + * This provides full-fidelity item rendering with 3D models, enchantment glint, + * durability bars, and stack counts. + * + * @param stack The ItemStack to render + * @param x X position (0-1, where 0 = left, 1 = right) + * @param y Y position (0-1, where 0 = top, 1 = bottom) + * @param size Normalized size using avg of screen dimensions (e.g., 0.03 = ~3%, default ~1.5%) + */ + fun screenItem(stack: ItemStack, x: Float, y: Float, size: Float = 0.015f) { + if (stack.isEmpty) return + deferredItems.add(ScreenItemRender(stack, x, y, size)) + } + /** * Draw text on screen at a specific position. * Position uses normalized 0-1 range, size is normalized. @@ -778,4 +801,12 @@ class RenderBuilder(private val cameraPos: Vec3d) { val glow: SDFGlow? = null, val shadow: SDFShadow? = SDFShadow() // Default shadow enabled ) + + /** Data class for deferred screen-space item rendering. */ + data class ScreenItemRender( + val stack: ItemStack, + val x: Float, // Normalized 0-1 + val y: Float, // Normalized 0-1 + val size: Float // Normalized size (e.g., 0.05 = 5% of screen height) + ) } \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt index d9bdd3b83..e03939cd7 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt @@ -185,7 +185,9 @@ class ImmediateRenderer(val name: String, var depthTest: Boolean = false) { * This should be called after world-space render() for proper layering. */ fun renderScreen() { - if (!renderer.hasScreenData() && styledScreenTextBuffers.isEmpty()) return + val hasDeferredItems = renderBuilder?.deferredItems?.isNotEmpty() == true + + if (!renderer.hasScreenData() && styledScreenTextBuffers.isEmpty() && !hasDeferredItems) return RendererUtils.withScreenContext { val dynamicTransform = RendererUtils.createScreenDynamicTransform() @@ -237,5 +239,12 @@ class ImmediateRenderer(val name: String, var depthTest: Boolean = false) { } } } + + // Render deferred items last (uses Minecraft's DrawContext pipeline) + renderBuilder?.deferredItems?.let { items -> + if (items.isNotEmpty()) { + RendererUtils.renderDeferredItems(items) + } + } } } diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/RendererUtils.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/RendererUtils.kt index 4bea87a57..244decead 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/renderer/RendererUtils.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/RendererUtils.kt @@ -18,6 +18,8 @@ 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.mc.LambdaRenderPipelines import com.mojang.blaze3d.buffers.GpuBuffer import com.mojang.blaze3d.buffers.GpuBufferSlice @@ -139,4 +141,79 @@ object RendererUtils { /** Screen-space text pipeline. */ val screenTextPipeline: RenderPipeline get() = LambdaRenderPipelines.SCREEN_TEXT + + // ============================================================================ + // Deferred Item Rendering + // ============================================================================ + + /** + * Global queue of items to render during the next HUD render pass. + * Items are added via renderDeferredItems() and rendered when HudRenderEvent fires. + */ + private val pendingItems = mutableListOf() + + /** + * Queue deferred ItemStack renders to be drawn during the HUD render pass. + * Items are rendered when Minecraft's HUD rendering occurs via HudRenderEvent. + * + * @param items List of ScreenItemRender to draw + */ + fun renderDeferredItems(items: List) { + if (items.isEmpty()) return + pendingItems.addAll(items) + } + + /** + * Render pending items using the provided DrawContext. + * Called by HudRenderEvent listener when Minecraft's HUD is being rendered. + * + * @param context The DrawContext from Minecraft's HUD rendering + */ + fun renderPendingItems(context: net.minecraft.client.gui.DrawContext) { + if (pendingItems.isEmpty()) return + + val window = mc.window ?: return + val textRenderer = mc.textRenderer ?: return + + val scaledWidth = window.scaledWidth + val scaledHeight = window.scaledHeight + + // Standard Minecraft item size is 16x16 pixels + val standardItemSize = 16f + + pendingItems.forEach { item -> + val pixelX = (item.x * scaledWidth).toInt() + val pixelY = (item.y * scaledHeight).toInt() + + // Calculate scale based on normalized size using average of dimensions (matches toPixelSize) + // Size of 0.05 means ~5% of screen, so pixelSize = size * (width + height) / 2 + val targetPixelSize = item.size * (scaledWidth + scaledHeight) / 2f + val scale = targetPixelSize / standardItemSize + + if (scale != 1f) { + // For scaled items, we need to translate and scale the matrix + // Matrix3x2fStack uses JOML methods directly + context.matrices.pushMatrix() + context.matrices.translate(pixelX.toFloat(), pixelY.toFloat()) + context.matrices.scale(scale, scale) + context.drawItem(item.stack, 0, 0) + context.drawStackOverlay(textRenderer, item.stack, 0, 0) + context.matrices.popMatrix() + } else { + context.drawItem(item.stack, pixelX, pixelY) + context.drawStackOverlay(textRenderer, item.stack, pixelX, pixelY) + } + } + + // Clear the queue after rendering + pendingItems.clear() + } + + // Initialize HudRenderEvent listener + init { + listenUnsafe { event -> + renderPendingItems(event.context) + } + } } + diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.kt index 118e3a072..a2c219cd9 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.kt @@ -184,7 +184,9 @@ class TickedRenderer(val name: String, var depthTest: Boolean = false) { * This should be called after world-space render() for proper layering. */ fun renderScreen() { - if (!renderer.hasScreenData() && styledScreenTextBuffers.isEmpty()) return + val hasDeferredItems = renderBuilder?.deferredItems?.isNotEmpty() == true + + if (!renderer.hasScreenData() && styledScreenTextBuffers.isEmpty() && !hasDeferredItems) return RendererUtils.withScreenContext { val dynamicTransform = RendererUtils.createScreenDynamicTransform() @@ -236,5 +238,12 @@ class TickedRenderer(val name: String, var depthTest: Boolean = false) { } } } + + // Render deferred items last (uses Minecraft's DrawContext pipeline) + renderBuilder?.deferredItems?.let { items -> + if (items.isNotEmpty()) { + RendererUtils.renderDeferredItems(items) + } + } } } diff --git a/src/main/kotlin/com/lambda/module/modules/debug/RendererTestModule.kt b/src/main/kotlin/com/lambda/module/modules/debug/RendererTestModule.kt index 6afa40d97..490c567b7 100644 --- a/src/main/kotlin/com/lambda/module/modules/debug/RendererTestModule.kt +++ b/src/main/kotlin/com/lambda/module/modules/debug/RendererTestModule.kt @@ -36,6 +36,7 @@ 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.Items import net.minecraft.util.math.ChunkPos import net.minecraft.util.math.Direction import java.awt.Color @@ -308,6 +309,15 @@ object ImmediateRendererTest : Module( ), centered = true ) + + // ========== Item Rendering Tests ========== + // Test screen items at various positions and sizes + // Size is normalized (e.g., 0.03 = 3% of screen height) + screenItem(Items.DIAMOND_SWORD.defaultStack, 0.02f, 0.40f) // Default size ~1.5% + screenItem(Items.NETHERITE_CHESTPLATE.defaultStack, 0.06f, 0.40f) + screenItem(Items.ENCHANTED_GOLDEN_APPLE.defaultStack, 0.10f, 0.40f) + // Test larger item (5% of screen height) + screenItem(Items.DIAMOND.defaultStack, 0.14f, 0.40f, size = 0.05f) } renderer.upload() From 69038ea94a724347349844397c28bf9217bc06dc Mon Sep 17 00:00:00 2001 From: beanbag44 <107891830+beanbag44@users.noreply.github.com> Date: Tue, 20 Jan 2026 20:26:47 +0000 Subject: [PATCH 16/26] vertex attribute based text sdf info --- .../graphics/mc/LambdaRenderPipelines.kt | 13 +- .../lambda/graphics/mc/LambdaVertexFormats.kt | 59 +++++++ .../graphics/mc/RegionVertexCollector.kt | 160 +++++++----------- .../com/lambda/graphics/mc/RenderBuilder.kt | 74 +++++--- .../graphics/mc/renderer/ChunkedRenderer.kt | 140 ++++----------- .../graphics/mc/renderer/ImmediateRenderer.kt | 118 ++++--------- .../graphics/mc/renderer/RendererUtils.kt | 31 +--- .../graphics/mc/renderer/TickedRenderer.kt | 119 ++++--------- .../lambda/shaders/core/screen_sdf_text.fsh | 16 +- .../lambda/shaders/core/screen_sdf_text.vsh | 6 +- .../assets/lambda/shaders/core/sdf_text.fsh | 16 +- .../assets/lambda/shaders/core/sdf_text.vsh | 5 + 12 files changed, 292 insertions(+), 465 deletions(-) diff --git a/src/main/kotlin/com/lambda/graphics/mc/LambdaRenderPipelines.kt b/src/main/kotlin/com/lambda/graphics/mc/LambdaRenderPipelines.kt index a0d01fa09..8dcd92dd5 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/LambdaRenderPipelines.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/LambdaRenderPipelines.kt @@ -161,7 +161,7 @@ object LambdaRenderPipelines : Loadable { /** * Pipeline for SDF text rendering with proper smoothstep anti-aliasing. - * Uses lambda:core/sdf_text shaders with SDF-specific uniforms for effects. + * Uses lambda:core/sdf_text shaders with per-vertex style parameters. */ val SDF_TEXT: RenderPipeline = RenderPipelines.register( @@ -170,13 +170,12 @@ object LambdaRenderPipelines : Loadable { .withVertexShader(Identifier.of("lambda", "core/sdf_text")) .withFragmentShader(Identifier.of("lambda", "core/sdf_text")) .withSampler("Sampler0") - .withUniform("SDFParams", UniformType.UNIFORM_BUFFER) .withBlend(BlendFunction.TRANSLUCENT) .withDepthWrite(false) .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST) .withCull(false) .withVertexFormat( - LambdaVertexFormats.POSITION_TEXTURE_COLOR_ANCHOR, + LambdaVertexFormats.POSITION_TEXTURE_COLOR_ANCHOR_SDF, VertexFormat.DrawMode.QUADS ) .build() @@ -190,13 +189,12 @@ object LambdaRenderPipelines : Loadable { .withVertexShader(Identifier.of("lambda", "core/sdf_text")) .withFragmentShader(Identifier.of("lambda", "core/sdf_text")) .withSampler("Sampler0") - .withUniform("SDFParams", UniformType.UNIFORM_BUFFER) .withBlend(BlendFunction.TRANSLUCENT) .withDepthWrite(false) .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) .withCull(false) .withVertexFormat( - LambdaVertexFormats.POSITION_TEXTURE_COLOR_ANCHOR, + LambdaVertexFormats.POSITION_TEXTURE_COLOR_ANCHOR_SDF, VertexFormat.DrawMode.QUADS ) .build() @@ -229,7 +227,7 @@ object LambdaRenderPipelines : Loadable { /** * Pipeline for screen-space SDF text rendering. - * Uses custom SDF shader with SDFParams for proper anti-aliased text with effects. + * Uses custom SDF shader with per-vertex style parameters for anti-aliased text with effects. */ val SCREEN_TEXT: RenderPipeline = RenderPipelines.register( @@ -238,13 +236,12 @@ object LambdaRenderPipelines : Loadable { .withVertexShader(Identifier.of("lambda", "core/screen_sdf_text")) .withFragmentShader(Identifier.of("lambda", "core/screen_sdf_text")) .withSampler("Sampler0") - .withUniform("SDFParams", UniformType.UNIFORM_BUFFER) .withBlend(BlendFunction.TRANSLUCENT) .withDepthWrite(false) .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) .withCull(false) .withVertexFormat( - VertexFormats.POSITION_TEXTURE_COLOR, + LambdaVertexFormats.SCREEN_TEXT_SDF_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 index ed3e8f376..d95b27578 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/LambdaVertexFormats.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/LambdaVertexFormats.kt @@ -159,5 +159,64 @@ object LambdaVertexFormats { .add("LineWidth", LINE_WIDTH_FLOAT) .add("Dash", DASH_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. + * Layout: Position (vec3), UV0 (vec2), Color (vec4), SDFStyle (vec4) + * + * Total size: 12 + 8 + 4 + 16 = 40 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) + */ + 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) + .build() } diff --git a/src/main/kotlin/com/lambda/graphics/mc/RegionVertexCollector.kt b/src/main/kotlin/com/lambda/graphics/mc/RegionVertexCollector.kt index 0c307a87a..6138d7332 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/RegionVertexCollector.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/RegionVertexCollector.kt @@ -51,7 +51,7 @@ class RegionVertexCollector { /** * Text vertex data for SDF billboard text rendering. - * Uses POSITION_TEXTURE_COLOR_ANCHOR format for GPU-based billboarding. + * 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) @@ -66,6 +66,10 @@ class RegionVertexCollector { * @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, @@ -73,7 +77,12 @@ class RegionVertexCollector { 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 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). */ @@ -119,11 +128,32 @@ class RegionVertexCollector { val animationSpeed: Float = 0f ) - /** Screen-space text vertex data (2D position + UV + color). */ + /** + * Screen-space text vertex data with SDF style params. + * Uses SCREEN_TEXT_SDF_FORMAT (position + UV + color + style). + * + * @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) + */ 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 + 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 ) /** Add a face vertex. */ @@ -364,12 +394,12 @@ class RegionVertexCollector { textVertices.clear() var result: BufferResult? = null - // POSITION_TEXTURE_COLOR_ANCHOR: 12 + 8 + 4 + 12 + 8 = 44 bytes per vertex - BufferAllocator(vertices.size * 48).use { allocator -> + // 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 + LambdaVertexFormats.POSITION_TEXTURE_COLOR_ANCHOR_SDF ) vertices.forEach { v -> @@ -392,6 +422,15 @@ class RegionVertexCollector { 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 -> @@ -505,12 +544,12 @@ class RegionVertexCollector { screenTextVertices.clear() var result: BufferResult? = null - // Position (8, 2D) + Texture (8) + Color (4) = 20 bytes, but using POSITION (12) for simplicity - BufferAllocator(vertices.size * 24).use { allocator -> + // SCREEN_TEXT_SDF_FORMAT: 12 + 8 + 4 + 16 = 40 bytes per vertex + BufferAllocator(vertices.size * 48).use { allocator -> val builder = BufferBuilder( allocator, VertexFormat.DrawMode.QUADS, - VertexFormats.POSITION_TEXTURE_COLOR + LambdaVertexFormats.SCREEN_TEXT_SDF_FORMAT ) // Screen text: position is already final screen coordinates @@ -518,6 +557,15 @@ class RegionVertexCollector { 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) + } } builder.endNullable()?.let { built -> @@ -549,95 +597,5 @@ class RegionVertexCollector { data class BufferResult(val buffer: GpuBuffer?, val indexCount: Int) data class UploadResult(val faces: BufferResult?, val edges: BufferResult?, val text: BufferResult? = null) data class ScreenUploadResult(val faces: BufferResult?, val edges: BufferResult?, val text: BufferResult? = null) - - companion object { - /** - * Upload a list of text vertices to a GPU buffer. - * Used for style-grouped text rendering. - */ - fun uploadTextVertices(vertices: List): BufferResult { - if (vertices.isEmpty()) return BufferResult(null, 0) - - var result: BufferResult? = null - // POSITION_TEXTURE_COLOR_ANCHOR: 12 + 8 + 4 + 12 + 8 = 44 bytes per vertex - BufferAllocator(vertices.size * 48).use { allocator -> - val builder = BufferBuilder( - allocator, - VertexFormat.DrawMode.QUADS, - LambdaVertexFormats.POSITION_TEXTURE_COLOR_ANCHOR - ) - - 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) - } - } - - builder.endNullable()?.let { built -> - val gpuDevice = RenderSystem.getDevice() - val buffer = gpuDevice.createBuffer( - { "Lambda Styled Text Buffer" }, - GpuBuffer.USAGE_VERTEX, - built.buffer - ) - result = BufferResult(buffer, built.drawParameters.indexCount()) - built.close() - } - } - return result ?: BufferResult(null, 0) - } - - /** - * Upload a list of screen text vertices to a GPU buffer. - * Used for style-grouped screen text rendering. - */ - fun uploadScreenTextVertices(vertices: List): BufferResult { - if (vertices.isEmpty()) return BufferResult(null, 0) - - var result: BufferResult? = null - // Position (8, 2D) + Texture (8) + Color (4) = 20 bytes, but using POSITION (12) for simplicity - BufferAllocator(vertices.size * 24).use { allocator -> - val builder = BufferBuilder( - allocator, - VertexFormat.DrawMode.QUADS, - VertexFormats.POSITION_TEXTURE_COLOR - ) - - // 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) - } - - builder.endNullable()?.let { built -> - val gpuDevice = RenderSystem.getDevice() - val buffer = gpuDevice.createBuffer( - { "Lambda Styled Screen Text Buffer" }, - GpuBuffer.USAGE_VERTEX, - built.buffer - ) - result = BufferResult(buffer, built.drawParameters.indexCount()) - built.close() - } - } - return result ?: BufferResult(null, 0) - } - } } + diff --git a/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt b/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt index d736f8c5c..7b097cfc9 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt @@ -44,16 +44,7 @@ class RenderBuilder(private val cameraPos: Vec3d) { var fontAtlas: SDFFontAtlas? = null private set - /** - * Map of SDFStyle to lists of text vertices for that style. - * Each text piece is grouped by its style to allow rendering with unique SDF params. - */ - val textStyleGroups = mutableMapOf>() - - /** - * Map of SDFStyle to lists of screen text vertices for that style. - */ - val screenTextStyleGroups = mutableMapOf>() + // Style grouping maps removed - style is now embedded in each text vertex /** * Deferred ItemStack renders to be drawn via Minecraft's DrawContext. @@ -607,7 +598,7 @@ class RenderBuilder(private val cameraPos: Vec3d) { /** * Build screen-space text quad vertices for a layer. - * Internal method - uses pixel coordinates. + * Internal method - uses pixel coordinates. Adds vertices directly to collector. */ private fun buildScreenTextQuads( atlas: SDFFontAtlas, @@ -619,8 +610,11 @@ class RenderBuilder(private val cameraPos: Vec3d) { pixelSize: Float, // Final text size in pixels style: SDFStyle ) { - // Get or create the vertex list for this style - val vertices = screenTextStyleGroups.getOrPut(style) { mutableListOf() } + // 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 @@ -643,11 +637,15 @@ class RenderBuilder(private val cameraPos: Vec3d) { val x1 = anchorX + startX + localX1 * pixelSize val y1 = anchorY + startY + localY1 * pixelSize - // Screen-space text uses simple 2D quads - vertices.add(RegionVertexCollector.ScreenTextVertex(x0, y1, glyph.u0, glyph.v1, r, g, b, a)) - vertices.add(RegionVertexCollector.ScreenTextVertex(x1, y1, glyph.u1, glyph.v1, r, g, b, a)) - vertices.add(RegionVertexCollector.ScreenTextVertex(x1, y0, glyph.u1, glyph.v0, r, g, b, a)) - vertices.add(RegionVertexCollector.ScreenTextVertex(x0, y0, glyph.u0, glyph.v0, r, g, b, a)) + // Screen-space text uses simple 2D quads - add directly to collector with style params + collector.screenTextVertices.add(RegionVertexCollector.ScreenTextVertex( + x0, y1, glyph.u0, glyph.v1, r, g, b, a, outlineWidth, glowRadius, shadowSoftness, threshold)) + collector.screenTextVertices.add(RegionVertexCollector.ScreenTextVertex( + x1, y1, glyph.u1, glyph.v1, r, g, b, a, outlineWidth, glowRadius, shadowSoftness, threshold)) + collector.screenTextVertices.add(RegionVertexCollector.ScreenTextVertex( + x1, y0, glyph.u1, glyph.v0, r, g, b, a, outlineWidth, glowRadius, shadowSoftness, threshold)) + collector.screenTextVertices.add(RegionVertexCollector.ScreenTextVertex( + x0, y0, glyph.u0, glyph.v0, r, g, b, a, outlineWidth, glowRadius, shadowSoftness, threshold)) // advance is already normalized, just add it penX += glyph.advance @@ -656,6 +654,7 @@ class RenderBuilder(private val cameraPos: Vec3d) { /** * 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 @@ -682,8 +681,11 @@ class RenderBuilder(private val cameraPos: Vec3d) { rotationMatrix: Matrix4f?, style: SDFStyle ) { - // Get or create the vertex list for this style - val vertices = textStyleGroups.getOrPut(style) { mutableListOf() } + // 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) { @@ -697,10 +699,18 @@ class RenderBuilder(private val cameraPos: Vec3d) { if (rotationMatrix == null) { // Billboard mode: pass local offsets directly, shader handles billboard // Bottom-left, Bottom-right, Top-right, Top-left - vertices.add(RegionVertexCollector.TextVertex(x0, y1, glyph.u0, glyph.v1, r, g, b, a, anchorX, anchorY, anchorZ, scale, 0f)) - vertices.add(RegionVertexCollector.TextVertex(x1, y1, glyph.u1, glyph.v1, r, g, b, a, anchorX, anchorY, anchorZ, scale, 0f)) - vertices.add(RegionVertexCollector.TextVertex(x1, y0, glyph.u1, glyph.v0, r, g, b, a, anchorX, anchorY, anchorZ, scale, 0f)) - vertices.add(RegionVertexCollector.TextVertex(x0, y0, glyph.u0, glyph.v0, r, g, b, a, anchorX, anchorY, anchorZ, scale, 0f)) + 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 @@ -709,10 +719,18 @@ class RenderBuilder(private val cameraPos: Vec3d) { val p2 = transformPoint(rotationMatrix, x1, -y0, 0f) val p3 = transformPoint(rotationMatrix, x0, -y0, 0f) - vertices.add(RegionVertexCollector.TextVertex(p0.x, p0.y, glyph.u0, glyph.v1, r, g, b, a, anchorX, anchorY, anchorZ, scale, 1f)) - vertices.add(RegionVertexCollector.TextVertex(p1.x, p1.y, glyph.u1, glyph.v1, r, g, b, a, anchorX, anchorY, anchorZ, scale, 1f)) - vertices.add(RegionVertexCollector.TextVertex(p2.x, p2.y, glyph.u1, glyph.v0, r, g, b, a, anchorX, anchorY, anchorZ, scale, 1f)) - vertices.add(RegionVertexCollector.TextVertex(p3.x, p3.y, glyph.u0, glyph.v0, r, g, b, a, anchorX, anchorY, anchorZ, scale, 1f)) + 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 diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt index ee6ea80d0..1048a8252 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt @@ -25,7 +25,6 @@ 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.RegionVertexCollector import com.lambda.graphics.mc.RenderBuilder import com.lambda.graphics.text.FontHandler import com.lambda.module.Module @@ -33,7 +32,6 @@ 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.GpuBuffer import com.mojang.blaze3d.systems.RenderSystem import net.minecraft.util.math.Vec3d import net.minecraft.world.World @@ -44,6 +42,7 @@ import org.joml.Vector4f import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentLinkedDeque + /** * Chunked ESP system using chunk-origin relative coordinates. * @@ -114,7 +113,7 @@ class ChunkedRenderer( fun render() { val cameraPos = mc.gameRenderer?.camera?.pos ?: return - val activeChunks = chunkMap.values.filter { it.renderer.hasData() || it.styledTextBuffers.isNotEmpty() } + val activeChunks = chunkMap.values.filter { it.renderer.hasData() } if (activeChunks.isEmpty()) return val modelViewMatrix = RenderMain.modelViewMatrix @@ -134,7 +133,7 @@ class ChunkedRenderer( } // Render Faces - RegionRenderer.Companion.createRenderPass("ChunkedESP Faces", depthTest)?.use { pass -> + RegionRenderer.createRenderPass("ChunkedESP Faces", depthTest)?.use { pass -> pass.setPipeline(RendererUtils.getFacesPipeline(depthTest)) RenderSystem.bindDefaultUniforms(pass) @@ -145,7 +144,7 @@ class ChunkedRenderer( } // Render Edges - RegionRenderer.Companion.createRenderPass("ChunkedESP Edges", depthTest)?.use { pass -> + RegionRenderer.createRenderPass("ChunkedESP Edges", depthTest)?.use { pass -> pass.setPipeline(RendererUtils.getEdgesPipeline(depthTest)) RenderSystem.bindDefaultUniforms(pass) @@ -155,62 +154,42 @@ class ChunkedRenderer( } } - // Render Styled Text - each style gets its own SDF params - val chunksWithStyledText = chunkTransforms.filter { (chunkData, _) -> chunkData.styledTextBuffers.isNotEmpty() } - if (chunksWithStyledText.isNotEmpty()) { + // Render Text - style params are now embedded in vertex attributes + val chunksWithText = chunkTransforms.filter { (chunkData, _) -> chunkData.renderer.hasTextData() } + if (chunksWithText.isNotEmpty()) { val atlas = FontHandler.getDefaultFont() if (!atlas.isUploaded) atlas.upload() val textureView = atlas.textureView val sampler = atlas.sampler if (textureView != null && sampler != null) { - // Collect all unique styles across all chunks - val allStyles = chunksWithStyledText.flatMap { (chunkData, _) -> - chunkData.styledTextBuffers.keys - }.toSet() - - // Render each style - allStyles.forEach { style -> - val outlineWidth = style.outline?.width ?: 0f - val glowRadius = style.glow?.radius ?: 0.2f - val shadowSoftness = style.shadow?.softness ?: 0.15f - - val sdfParams = RendererUtils.createSDFParamsBuffer(outlineWidth, glowRadius, shadowSoftness) - if (sdfParams != null) { - RegionRenderer.Companion.createRenderPass("ChunkedESP Text", depthTest)?.use { pass -> - pass.setPipeline(RendererUtils.getTextPipeline(depthTest)) - RenderSystem.bindDefaultUniforms(pass) - pass.setUniform("SDFParams", sdfParams) - pass.bindTexture("Sampler0", textureView, sampler) - - chunksWithStyledText.forEach { (chunkData, transform) -> - val bufferInfo = chunkData.styledTextBuffers[style] - if (bufferInfo != null) { - val (buffer, indexCount) = bufferInfo - pass.setUniform("DynamicTransforms", transform) - RegionRenderer.renderQuadBuffer(pass, buffer, indexCount) - } - } - } - sdfParams.close() + RegionRenderer.createRenderPass("ChunkedESP Text", depthTest)?.use { pass -> + pass.setPipeline(RendererUtils.getTextPipeline(depthTest)) + RenderSystem.bindDefaultUniforms(pass) + pass.bindTexture("Sampler0", textureView, sampler) + + chunksWithText.forEach { (chunkData, transform) -> + pass.setUniform("DynamicTransforms", transform) + chunkData.renderer.renderText(pass) } } } } } + /** * Render screen-space geometry for all chunks. * Uses orthographic projection for 2D rendering. */ fun renderScreen() { - val activeChunks = chunkMap.values.filter { it.renderer.hasScreenData() || it.styledScreenTextBuffers.isNotEmpty() } + val activeChunks = chunkMap.values.filter { it.renderer.hasScreenData() } if (activeChunks.isEmpty()) return RendererUtils.withScreenContext { val dynamicTransform = RendererUtils.createScreenDynamicTransform() // Render Screen Faces - RegionRenderer.Companion.createRenderPass("ChunkedESP Screen Faces", false)?.use { pass -> + RegionRenderer.createRenderPass("ChunkedESP Screen Faces", false)?.use { pass -> pass.setPipeline(RendererUtils.screenFacesPipeline) RenderSystem.bindDefaultUniforms(pass) pass.setUniform("DynamicTransforms", dynamicTransform) @@ -220,7 +199,7 @@ class ChunkedRenderer( } // Render Screen Edges - RegionRenderer.Companion.createRenderPass("ChunkedESP Screen Edges", false)?.use { pass -> + RegionRenderer.createRenderPass("ChunkedESP Screen Edges", false)?.use { pass -> pass.setPipeline(RendererUtils.screenEdgesPipeline) RenderSystem.bindDefaultUniforms(pass) pass.setUniform("DynamicTransforms", dynamicTransform) @@ -229,41 +208,22 @@ class ChunkedRenderer( } } - // Render Styled Screen Text - val chunksWithStyledText = activeChunks.filter { it.styledScreenTextBuffers.isNotEmpty() } - if (chunksWithStyledText.isNotEmpty()) { + // Render Screen Text - style params are now embedded in vertex attributes + val chunksWithText = activeChunks.filter { it.renderer.hasScreenTextData() } + if (chunksWithText.isNotEmpty()) { val atlas = FontHandler.getDefaultFont() if (!atlas.isUploaded) atlas.upload() val textureView = atlas.textureView val sampler = atlas.sampler if (textureView != null && sampler != null) { - // Collect all unique styles across all chunks - val allStyles = chunksWithStyledText.flatMap { it.styledScreenTextBuffers.keys }.toSet() - - // Render each style - allStyles.forEach { style -> - val outlineWidth = style.outline?.width ?: 0f - val glowRadius = style.glow?.radius ?: 0.2f - val shadowSoftness = style.shadow?.softness ?: 0.15f - - val sdfParams = RendererUtils.createSDFParamsBuffer(outlineWidth, glowRadius, shadowSoftness) - if (sdfParams != null) { - RegionRenderer.Companion.createRenderPass("ChunkedESP Screen Text", false)?.use { pass -> - pass.setPipeline(RendererUtils.screenTextPipeline) - RenderSystem.bindDefaultUniforms(pass) - pass.setUniform("DynamicTransforms", dynamicTransform) - pass.setUniform("SDFParams", sdfParams) - pass.bindTexture("Sampler0", textureView, sampler) - - chunksWithStyledText.forEach { chunkData -> - val bufferInfo = chunkData.styledScreenTextBuffers[style] - if (bufferInfo != null) { - val (buffer, indexCount) = bufferInfo - RegionRenderer.renderQuadBuffer(pass, buffer, indexCount) - } - } - } - sdfParams.close() + RegionRenderer.createRenderPass("ChunkedESP Screen Text", false)?.use { pass -> + pass.setPipeline(RendererUtils.screenTextPipeline) + RenderSystem.bindDefaultUniforms(pass) + pass.setUniform("DynamicTransforms", dynamicTransform) + pass.bindTexture("Sampler0", textureView, sampler) + + chunksWithText.forEach { chunkData -> + chunkData.renderer.renderScreenText(pass) } } } @@ -271,6 +231,7 @@ class ChunkedRenderer( } } + companion object { fun Module.chunkedEsp( name: String, @@ -325,10 +286,6 @@ class ChunkedRenderer( // This chunk's own renderer val renderer = RegionRenderer() - // Styled text buffers: maps SDFStyle to (buffer, indexCount) - val styledTextBuffers = mutableMapOf>() - val styledScreenTextBuffers = mutableMapOf>() - private var isDirty = false fun markDirty() { @@ -357,45 +314,18 @@ class ChunkedRenderer( } } - // Capture the styled groups for upload on main thread - val textGroups = scope.textStyleGroups.toMap() - val screenTextGroups = scope.screenTextStyleGroups.toMap() + // Capture collector for upload on main thread + val collector = scope.collector uploadQueue.add { - renderer.upload(scope.collector) - - // Clean up previous styled buffers - styledTextBuffers.values.forEach { (buffer, _) -> buffer.close() } - styledTextBuffers.clear() - styledScreenTextBuffers.values.forEach { (buffer, _) -> buffer.close() } - styledScreenTextBuffers.clear() - - // Upload styled text groups - textGroups.forEach { (style, vertices) -> - val result = RegionVertexCollector.uploadTextVertices(vertices) - if (result.buffer != null) { - styledTextBuffers[style] = result.buffer to result.indexCount - } - } - - // Upload styled screen text groups - screenTextGroups.forEach { (style, vertices) -> - val result = RegionVertexCollector.uploadScreenTextVertices(vertices) - if (result.buffer != null) { - styledScreenTextBuffers[style] = result.buffer to result.indexCount - } - } - + renderer.upload(collector) isDirty = false } } fun close() { renderer.close() - styledTextBuffers.values.forEach { (buffer, _) -> buffer.close() } - styledTextBuffers.clear() - styledScreenTextBuffers.values.forEach { (buffer, _) -> buffer.close() } - styledScreenTextBuffers.clear() } } } + diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt index e03939cd7..5c40e790d 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt @@ -20,16 +20,15 @@ package com.lambda.graphics.mc.renderer import com.lambda.Lambda.mc import com.lambda.graphics.RenderMain import com.lambda.graphics.mc.RegionRenderer -import com.lambda.graphics.mc.RegionVertexCollector import com.lambda.graphics.mc.RenderBuilder import com.lambda.graphics.text.SDFFontAtlas -import com.mojang.blaze3d.buffers.GpuBuffer import com.mojang.blaze3d.systems.RenderSystem import net.minecraft.util.math.Vec3d import org.joml.Matrix4f import org.joml.Vector3f import org.joml.Vector4f + /** * Interpolated ESP system for smooth entity rendering. * @@ -67,38 +66,12 @@ class ImmediateRenderer(val name: String, var depthTest: Boolean = false) { // Font atlas used for current text rendering private var currentFontAtlas: SDFFontAtlas? = null - - // Styled text buffers: maps SDFStyle to (buffer, indexCount) - private val styledTextBuffers = mutableMapOf>() - private val styledScreenTextBuffers = mutableMapOf>() /** Upload collected geometry to GPU. Must be called on main thread. */ fun upload() { - // Clean up previous styled buffers - styledTextBuffers.values.forEach { (buffer, _) -> buffer.close() } - styledTextBuffers.clear() - styledScreenTextBuffers.values.forEach { (buffer, _) -> buffer.close() } - styledScreenTextBuffers.clear() - renderBuilder?.let { s -> renderer.upload(s.collector) currentFontAtlas = s.fontAtlas - - // Upload styled text groups - s.textStyleGroups.forEach { (style, vertices) -> - val result = RegionVertexCollector.uploadTextVertices(vertices) - if (result.buffer != null) { - styledTextBuffers[style] = result.buffer to result.indexCount - } - } - - // Upload styled screen text groups - s.screenTextStyleGroups.forEach { (style, vertices) -> - val result = RegionVertexCollector.uploadScreenTextVertices(vertices) - if (result.buffer != null) { - styledScreenTextBuffers[style] = result.buffer to result.indexCount - } - } } ?: run { renderer.clearData() currentFontAtlas = null @@ -108,10 +81,6 @@ class ImmediateRenderer(val name: String, var depthTest: Boolean = false) { /** Close and release all GPU resources. */ fun close() { renderer.close() - styledTextBuffers.values.forEach { (buffer, _) -> buffer.close() } - styledTextBuffers.clear() - styledScreenTextBuffers.values.forEach { (buffer, _) -> buffer.close() } - styledScreenTextBuffers.clear() clear() } @@ -120,7 +89,7 @@ class ImmediateRenderer(val name: String, var depthTest: Boolean = false) { * we just use the base modelView matrix without additional translation. */ fun render() { - if (!renderer.hasData() && styledTextBuffers.isEmpty()) return + if (!renderer.hasData()) return val modelViewMatrix = RenderMain.modelViewMatrix @@ -148,33 +117,19 @@ class ImmediateRenderer(val name: String, var depthTest: Boolean = false) { renderer.renderEdges(pass) } - // Render Styled Text - each style gets its own SDF params - if (styledTextBuffers.isNotEmpty()) { - val atlas = currentFontAtlas - if (atlas != null) { - if (!atlas.isUploaded) atlas.upload() - val textureView = atlas.textureView - val sampler = atlas.sampler - if (textureView != null && sampler != null) { - styledTextBuffers.forEach { (style, bufferInfo) -> - val (buffer, indexCount) = bufferInfo - val outlineWidth = style.outline?.width ?: 0f - val glowRadius = style.glow?.radius ?: 0.2f - val shadowSoftness = style.shadow?.softness ?: 0.15f - - val sdfParams = RendererUtils.createSDFParamsBuffer(outlineWidth, glowRadius, shadowSoftness) - if (sdfParams != null) { - RegionRenderer.createRenderPass("$name Text", depthTest)?.use { pass -> - pass.setPipeline(RendererUtils.getTextPipeline(depthTest)) - RenderSystem.bindDefaultUniforms(pass) - pass.setUniform("DynamicTransforms", dynamicTransform) - pass.setUniform("SDFParams", sdfParams) - pass.bindTexture("Sampler0", textureView, sampler) - RegionRenderer.renderQuadBuffer(pass, buffer, indexCount) - } - sdfParams.close() - } - } + // Render Text - style params are now embedded in vertex attributes + val atlas = currentFontAtlas + if (atlas != null && renderer.hasTextData()) { + if (!atlas.isUploaded) atlas.upload() + val textureView = atlas.textureView + val sampler = atlas.sampler + if (textureView != null && sampler != null) { + RegionRenderer.createRenderPass("$name Text", depthTest)?.use { pass -> + pass.setPipeline(RendererUtils.getTextPipeline(depthTest)) + RenderSystem.bindDefaultUniforms(pass) + pass.setUniform("DynamicTransforms", dynamicTransform) + pass.bindTexture("Sampler0", textureView, sampler) + renderer.renderText(pass) } } } @@ -187,7 +142,7 @@ class ImmediateRenderer(val name: String, var depthTest: Boolean = false) { fun renderScreen() { val hasDeferredItems = renderBuilder?.deferredItems?.isNotEmpty() == true - if (!renderer.hasScreenData() && styledScreenTextBuffers.isEmpty() && !hasDeferredItems) return + if (!renderer.hasScreenData() && !hasDeferredItems) return RendererUtils.withScreenContext { val dynamicTransform = RendererUtils.createScreenDynamicTransform() @@ -208,33 +163,19 @@ class ImmediateRenderer(val name: String, var depthTest: Boolean = false) { renderer.renderScreenEdges(pass) } - // Render Styled Screen Text - each style gets its own SDF params - if (styledScreenTextBuffers.isNotEmpty()) { - val atlas = currentFontAtlas - if (atlas != null) { - if (!atlas.isUploaded) atlas.upload() - val textureView = atlas.textureView - val sampler = atlas.sampler - if (textureView != null && sampler != null) { - styledScreenTextBuffers.forEach { (style, bufferInfo) -> - val (buffer, indexCount) = bufferInfo - val outlineWidth = style.outline?.width ?: 0f - val glowRadius = style.glow?.radius ?: 0.2f - val shadowSoftness = style.shadow?.softness ?: 0.15f - - val sdfParams = RendererUtils.createSDFParamsBuffer(outlineWidth, glowRadius, shadowSoftness) - if (sdfParams != null) { - RegionRenderer.createRenderPass("$name Screen Text", false)?.use { pass -> - pass.setPipeline(RendererUtils.screenTextPipeline) - RenderSystem.bindDefaultUniforms(pass) - pass.setUniform("DynamicTransforms", dynamicTransform) - pass.setUniform("SDFParams", sdfParams) - pass.bindTexture("Sampler0", textureView, sampler) - RegionRenderer.renderQuadBuffer(pass, buffer, indexCount) - } - sdfParams.close() - } - } + // Render Screen Text - style params are now embedded in vertex attributes + val atlas = currentFontAtlas + if (atlas != null && renderer.hasScreenTextData()) { + if (!atlas.isUploaded) atlas.upload() + val textureView = atlas.textureView + val sampler = atlas.sampler + if (textureView != null && sampler != null) { + RegionRenderer.createRenderPass("$name Screen Text", false)?.use { pass -> + pass.setPipeline(RendererUtils.screenTextPipeline) + RenderSystem.bindDefaultUniforms(pass) + pass.setUniform("DynamicTransforms", dynamicTransform) + pass.bindTexture("Sampler0", textureView, sampler) + renderer.renderScreenText(pass) } } } @@ -248,3 +189,4 @@ class ImmediateRenderer(val name: String, var depthTest: Boolean = false) { } } } + diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/RendererUtils.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/RendererUtils.kt index 244decead..ced868380 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/renderer/RendererUtils.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/RendererUtils.kt @@ -21,7 +21,6 @@ import com.lambda.Lambda.mc import com.lambda.event.events.HudRenderEvent import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafe import com.lambda.graphics.mc.LambdaRenderPipelines -import com.mojang.blaze3d.buffers.GpuBuffer import com.mojang.blaze3d.buffers.GpuBufferSlice import com.mojang.blaze3d.pipeline.RenderPipeline import com.mojang.blaze3d.systems.ProjectionType @@ -30,7 +29,6 @@ import net.minecraft.client.render.ProjectionMatrix2 import org.joml.Matrix4f import org.joml.Vector3f import org.joml.Vector4f -import org.lwjgl.system.MemoryUtil /** * Shared utilities for ESP renderers. @@ -40,34 +38,7 @@ object RendererUtils { // Shared projection matrix for screen-space rendering private val screenProjectionMatrix = ProjectionMatrix2("lambda_screen", -1000f, 1000f, true) - /** - * Create SDF params uniform buffer with specified or default values. - * Used for SDF text rendering. - * - * @param outlineWidth Width of text outline in SDF units (0 = no outline) - * @param glowRadius Radius of glow effect in SDF units (0 = no glow) - * @param shadowSoftness Softness of shadow effect (0 = no shadow) - */ - fun createSDFParamsBuffer( - outlineWidth: Float = 0f, - glowRadius: Float = 0.2f, - shadowSoftness: Float = 0.15f - ): GpuBuffer? { - val device = RenderSystem.getDevice() - val buffer = MemoryUtil.memAlloc(16) - return try { - buffer.putFloat(0.5f) // SDFThreshold - buffer.putFloat(outlineWidth) // OutlineWidth - buffer.putFloat(glowRadius) // GlowRadius - buffer.putFloat(shadowSoftness) // ShadowSoftness - buffer.flip() - device.createBuffer({ "SDFParams" }, GpuBuffer.USAGE_UNIFORM, buffer) - } catch (_: Exception) { - null - } finally { - MemoryUtil.memFree(buffer) - } - } + /** * Create a dynamic transform uniform with identity matrices for screen-space rendering. diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.kt index a2c219cd9..b212b2c28 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.kt @@ -20,16 +20,15 @@ package com.lambda.graphics.mc.renderer import com.lambda.Lambda.mc import com.lambda.graphics.RenderMain import com.lambda.graphics.mc.RegionRenderer -import com.lambda.graphics.mc.RegionVertexCollector import com.lambda.graphics.mc.RenderBuilder import com.lambda.graphics.text.SDFFontAtlas -import com.mojang.blaze3d.buffers.GpuBuffer 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. @@ -62,38 +61,12 @@ class TickedRenderer(val name: String, var depthTest: Boolean = false) { // Font atlas used for current text rendering private var currentFontAtlas: SDFFontAtlas? = null - - // Styled text buffers: maps SDFStyle to (buffer, indexCount) - private val styledTextBuffers = mutableMapOf>() - private val styledScreenTextBuffers = mutableMapOf>() /** Upload collected geometry to GPU. Must be called on main thread. */ fun upload() { - // Clean up previous styled buffers - styledTextBuffers.values.forEach { (buffer, _) -> buffer.close() } - styledTextBuffers.clear() - styledScreenTextBuffers.values.forEach { (buffer, _) -> buffer.close() } - styledScreenTextBuffers.clear() - renderBuilder?.let { s -> renderer.upload(s.collector) currentFontAtlas = s.fontAtlas - - // Upload styled text groups - s.textStyleGroups.forEach { (style, vertices) -> - val result = RegionVertexCollector.uploadTextVertices(vertices) - if (result.buffer != null) { - styledTextBuffers[style] = result.buffer to result.indexCount - } - } - - // Upload styled screen text groups - s.screenTextStyleGroups.forEach { (style, vertices) -> - val result = RegionVertexCollector.uploadScreenTextVertices(vertices) - if (result.buffer != null) { - styledScreenTextBuffers[style] = result.buffer to result.indexCount - } - } } ?: run { renderer.clearData() currentFontAtlas = null @@ -103,13 +76,10 @@ class TickedRenderer(val name: String, var depthTest: Boolean = false) { /** Close and release all GPU resources. */ fun close() { renderer.close() - styledTextBuffers.values.forEach { (buffer, _) -> buffer.close() } - styledTextBuffers.clear() - styledScreenTextBuffers.values.forEach { (buffer, _) -> buffer.close() } - styledScreenTextBuffers.clear() clear() } + /** * Render with smooth camera interpolation. * Computes delta between tick-camera and current-camera in double precision. @@ -117,7 +87,7 @@ class TickedRenderer(val name: String, var depthTest: Boolean = false) { fun render() { val currentCameraPos = mc.gameRenderer?.camera?.pos ?: return val tickCamera = tickCameraPos ?: return - if (!renderer.hasData() && styledTextBuffers.isEmpty()) return + if (!renderer.hasData()) return val modelViewMatrix = RenderMain.modelViewMatrix @@ -147,33 +117,19 @@ class TickedRenderer(val name: String, var depthTest: Boolean = false) { renderer.renderEdges(pass) } - // Render Styled Text - each style gets its own SDF params - if (styledTextBuffers.isNotEmpty()) { - val atlas = currentFontAtlas - if (atlas != null) { - if (!atlas.isUploaded) atlas.upload() - val textureView = atlas.textureView - val sampler = atlas.sampler - if (textureView != null && sampler != null) { - styledTextBuffers.forEach { (style, bufferInfo) -> - val (buffer, indexCount) = bufferInfo - val outlineWidth = style.outline?.width ?: 0f - val glowRadius = style.glow?.radius ?: 0.2f - val shadowSoftness = style.shadow?.softness ?: 0.15f - - val sdfParams = RendererUtils.createSDFParamsBuffer(outlineWidth, glowRadius, shadowSoftness) - if (sdfParams != null) { - RegionRenderer.createRenderPass("$name Text", depthTest)?.use { pass -> - pass.setPipeline(RendererUtils.getTextPipeline(depthTest)) - RenderSystem.bindDefaultUniforms(pass) - pass.setUniform("DynamicTransforms", dynamicTransform) - pass.setUniform("SDFParams", sdfParams) - pass.bindTexture("Sampler0", textureView, sampler) - RegionRenderer.renderQuadBuffer(pass, buffer, indexCount) - } - sdfParams.close() - } - } + // Render Text - style params are now embedded in vertex attributes + val atlas = currentFontAtlas + if (atlas != null && renderer.hasTextData()) { + if (!atlas.isUploaded) atlas.upload() + val textureView = atlas.textureView + val sampler = atlas.sampler + if (textureView != null && sampler != null) { + RegionRenderer.createRenderPass("$name Text", depthTest)?.use { pass -> + pass.setPipeline(RendererUtils.getTextPipeline(depthTest)) + RenderSystem.bindDefaultUniforms(pass) + pass.setUniform("DynamicTransforms", dynamicTransform) + pass.bindTexture("Sampler0", textureView, sampler) + renderer.renderText(pass) } } } @@ -186,7 +142,7 @@ class TickedRenderer(val name: String, var depthTest: Boolean = false) { fun renderScreen() { val hasDeferredItems = renderBuilder?.deferredItems?.isNotEmpty() == true - if (!renderer.hasScreenData() && styledScreenTextBuffers.isEmpty() && !hasDeferredItems) return + if (!renderer.hasScreenData() && !hasDeferredItems) return RendererUtils.withScreenContext { val dynamicTransform = RendererUtils.createScreenDynamicTransform() @@ -207,33 +163,19 @@ class TickedRenderer(val name: String, var depthTest: Boolean = false) { renderer.renderScreenEdges(pass) } - // Render Styled Screen Text - each style gets its own SDF params - if (styledScreenTextBuffers.isNotEmpty()) { - val atlas = currentFontAtlas - if (atlas != null) { - if (!atlas.isUploaded) atlas.upload() - val textureView = atlas.textureView - val sampler = atlas.sampler - if (textureView != null && sampler != null) { - styledScreenTextBuffers.forEach { (style, bufferInfo) -> - val (buffer, indexCount) = bufferInfo - val outlineWidth = style.outline?.width ?: 0f - val glowRadius = style.glow?.radius ?: 0.2f - val shadowSoftness = style.shadow?.softness ?: 0.15f - - val sdfParams = RendererUtils.createSDFParamsBuffer(outlineWidth, glowRadius, shadowSoftness) - if (sdfParams != null) { - RegionRenderer.Companion.createRenderPass("$name Screen Text", false)?.use { pass -> - pass.setPipeline(RendererUtils.screenTextPipeline) - RenderSystem.bindDefaultUniforms(pass) - pass.setUniform("DynamicTransforms", dynamicTransform) - pass.setUniform("SDFParams", sdfParams) - pass.bindTexture("Sampler0", textureView, sampler) - RegionRenderer.renderQuadBuffer(pass, buffer, indexCount) - } - sdfParams.close() - } - } + // Render Screen Text - style params are now embedded in vertex attributes + val atlas = currentFontAtlas + if (atlas != null && renderer.hasScreenTextData()) { + if (!atlas.isUploaded) atlas.upload() + val textureView = atlas.textureView + val sampler = atlas.sampler + if (textureView != null && sampler != null) { + RegionRenderer.createRenderPass("$name Screen Text", false)?.use { pass -> + pass.setPipeline(RendererUtils.screenTextPipeline) + RenderSystem.bindDefaultUniforms(pass) + pass.setUniform("DynamicTransforms", dynamicTransform) + pass.bindTexture("Sampler0", textureView, sampler) + renderer.renderScreenText(pass) } } } @@ -247,3 +189,4 @@ class TickedRenderer(val name: String, var depthTest: Boolean = false) { } } } + 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 index 71e63b17c..18c870c5c 100644 --- a/src/main/resources/assets/lambda/shaders/core/screen_sdf_text.fsh +++ b/src/main/resources/assets/lambda/shaders/core/screen_sdf_text.fsh @@ -4,21 +4,21 @@ uniform sampler2D Sampler0; -// SDF effect parameters - matches world-space sdf_text -layout(std140) uniform SDFParams { - float SDFThreshold; // Main text edge threshold (default 0.5) - float OutlineWidth; // Outline width in SDF units (0 = no outline) - float GlowRadius; // Glow radius in SDF units (0 = no glow) - float ShadowSoftness; // Shadow softness (0 = no shadow) -}; - // Inputs from vertex shader in vec2 texCoord0; in vec4 vertexColor; +// 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; 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 index 19cb558e9..0a52bd95e 100644 --- a/src/main/resources/assets/lambda/shaders/core/screen_sdf_text.vsh +++ b/src/main/resources/assets/lambda/shaders/core/screen_sdf_text.vsh @@ -3,14 +3,17 @@ #moj_import #moj_import -// Vertex inputs (POSITION_TEXTURE_COLOR format) +// Vertex inputs (SCREEN_TEXT_SDF_FORMAT) in vec3 Position; in vec2 UV0; in vec4 Color; +// SDFStyle: vec4(outlineWidth, glowRadius, shadowSoftness, threshold) +in vec4 SDFStyle; // Outputs to fragment shader out vec2 texCoord0; out vec4 vertexColor; +out vec4 sdfStyleParams; void main() { // Screen-space position - already in screen coordinates @@ -18,4 +21,5 @@ void main() { texCoord0 = UV0; vertexColor = Color; + sdfStyleParams = SDFStyle; } diff --git a/src/main/resources/assets/lambda/shaders/core/sdf_text.fsh b/src/main/resources/assets/lambda/shaders/core/sdf_text.fsh index 5796d4b93..756998738 100644 --- a/src/main/resources/assets/lambda/shaders/core/sdf_text.fsh +++ b/src/main/resources/assets/lambda/shaders/core/sdf_text.fsh @@ -4,22 +4,22 @@ uniform sampler2D Sampler0; -// SDF effect parameters - passed via uniform buffer -layout(std140) uniform SDFParams { - float SDFThreshold; // Main text edge threshold (default 0.5) - float OutlineWidth; // Outline width in SDF units (0 = no outline) - float GlowRadius; // Glow radius in SDF units (0 = no glow) - float ShadowSoftness; // Shadow softness (0 = no shadow) -}; - 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; diff --git a/src/main/resources/assets/lambda/shaders/core/sdf_text.vsh b/src/main/resources/assets/lambda/shaders/core/sdf_text.vsh index 9ca9efc0f..0e209b0ae 100644 --- a/src/main/resources/assets/lambda/shaders/core/sdf_text.vsh +++ b/src/main/resources/assets/lambda/shaders/core/sdf_text.vsh @@ -12,11 +12,15 @@ in vec4 Color; 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; @@ -55,6 +59,7 @@ void main() { texCoord0 = UV0; vertexColor = Color; + sdfStyleParams = SDFStyle; sphericalVertexDistance = fog_spherical_distance(worldPos); cylindricalVertexDistance = fog_cylindrical_distance(worldPos); From 0bf9d34897fef2e40b9c3ee8ad48266cc232e5bc Mon Sep 17 00:00:00 2001 From: beanbag44 <107891830+beanbag44@users.noreply.github.com> Date: Wed, 21 Jan 2026 02:11:03 +0000 Subject: [PATCH 17/26] custom depth buffer layer indexing for the performance of batched rendering and the layering of staggered. Also abstracts a lot of rendering logic to avoid duplicate code --- .../mixin/render/GameRendererMixin.java | 1 + .../com/lambda/config/groups/Targeting.kt | 78 +----- .../lambda/config/groups/TargetingConfig.kt | 11 +- .../lambda/event/events/ScreenRenderEvent.kt | 32 +++ .../kotlin/com/lambda/graphics/RenderMain.kt | 24 +- .../graphics/mc/LambdaRenderPipelines.kt | 33 ++- .../lambda/graphics/mc/LambdaVertexFormats.kt | 44 +++- .../graphics/mc/RegionVertexCollector.kt | 81 ++++-- .../com/lambda/graphics/mc/RenderBuilder.kt | 240 ++++++++++++++++-- .../graphics/mc/renderer/AbstractRenderer.kt | 165 ++++++++++++ .../graphics/mc/renderer/ChunkedRenderer.kt | 131 ++-------- .../graphics/mc/renderer/ImmediateRenderer.kt | 117 ++------- .../graphics/mc/renderer/RendererUtils.kt | 12 +- .../graphics/mc/renderer/TickedRenderer.kt | 119 ++------- .../modules/debug/RendererTestModule.kt | 10 + .../lambda/module/modules/render/EntityESP.kt | 7 +- .../lambda/module/modules/render/Nametags.kt | 29 +++ .../lambda/module/modules/render/Tracers.kt | 6 + .../lambda/shaders/core/screen_faces.fsh | 24 ++ .../lambda/shaders/core/screen_faces.vsh | 22 ++ .../lambda/shaders/core/screen_lines.fsh | 4 + .../lambda/shaders/core/screen_lines.vsh | 3 + .../lambda/shaders/core/screen_sdf_text.fsh | 4 + .../lambda/shaders/core/screen_sdf_text.vsh | 3 + 24 files changed, 753 insertions(+), 447 deletions(-) create mode 100644 src/main/kotlin/com/lambda/event/events/ScreenRenderEvent.kt create mode 100644 src/main/kotlin/com/lambda/graphics/mc/renderer/AbstractRenderer.kt create mode 100644 src/main/kotlin/com/lambda/module/modules/render/Nametags.kt create mode 100644 src/main/resources/assets/lambda/shaders/core/screen_faces.fsh create mode 100644 src/main/resources/assets/lambda/shaders/core/screen_faces.vsh diff --git a/src/main/java/com/lambda/mixin/render/GameRendererMixin.java b/src/main/java/com/lambda/mixin/render/GameRendererMixin.java index 87791ef08..4c2e7d3af 100644 --- a/src/main/java/com/lambda/mixin/render/GameRendererMixin.java +++ b/src/main/java/com/lambda/mixin/render/GameRendererMixin.java @@ -76,6 +76,7 @@ private float modifyGetFov(float original) { @Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/render/GuiRenderer;render(Lcom/mojang/blaze3d/buffers/GpuBufferSlice;)V", shift = At.Shift.AFTER)) private void onGuiRenderComplete(RenderTickCounter tickCounter, boolean tick, CallbackInfo ci) { + RenderMain.renderScreen(); DearImGui.INSTANCE.render(); } diff --git a/src/main/kotlin/com/lambda/config/groups/Targeting.kt b/src/main/kotlin/com/lambda/config/groups/Targeting.kt index a50753e8b..7b45a6e2d 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.* /** @@ -60,52 +59,7 @@ abstract class Targeting( * 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) + override val targets by c.setting("Targets", setOf(EntityGroup.Player, EntityGroup.Mob, EntityGroup.Boss), EntityGroup.entries) /** * Validates whether a given entity is targetable by the player based on current settings. @@ -114,18 +68,8 @@ abstract class 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 { - !players && entity is OtherClientPlayerEntity -> false - players && entity is OtherClientPlayerEntity && entity.isFriend -> 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 - } + open fun validate(player: ClientPlayerEntity, entity: LivingEntity) = + entity.entityGroup in targets && (entity !is OtherClientPlayerEntity || !entity.isFriend) /** * Subclass for targeting entities specifically for combat purposes. @@ -160,6 +104,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 +123,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/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 610ca56f0..8e610efc6 100644 --- a/src/main/kotlin/com/lambda/graphics/RenderMain.kt +++ b/src/main/kotlin/com/lambda/graphics/RenderMain.kt @@ -20,6 +20,7 @@ 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.ScreenRenderEvent import com.lambda.event.events.TickEvent import com.lambda.event.listener.SafeListener.Companion.listen import com.lambda.graphics.gl.Matrices @@ -85,13 +86,15 @@ object RenderMain { ndcY = (ndcY / len) * 3f } else { // If almost directly behind, push down (arbitrary direction) - ndcY = 3f + // With Y-up, negative Y means down + ndcY = -3f } } - // NDC to normalized 0-1 coordinates (Y is flipped: 0 = top, 1 = bottom) + // NDC to normalized 0-1 coordinates + // Y-up convention: 0 = bottom, 1 = top (matches screen rendering) val normalizedX = (ndcX + 1f) * 0.5f - val normalizedY = (1f - ndcY) * 0.5f + val normalizedY = (ndcY + 1f) * 0.5f // No flip for Y-up return Vector2f(normalizedX, normalizedY) } @@ -124,6 +127,21 @@ object RenderMain { dynamicESP.render() } + /** + * Render all screen-space elements. + * Called after Minecraft's guiRenderer.render() to ensure Lambda's + * screen elements appear above all of Minecraft's GUI. + */ + @JvmStatic + fun renderScreen() { + // Render screen-space elements from the main renderers + staticESP.renderScreen() + dynamicESP.renderScreen() + + // Post event for modules with custom renderers + ScreenRenderEvent.post() + } + init { listen { staticESP.clear() diff --git a/src/main/kotlin/com/lambda/graphics/mc/LambdaRenderPipelines.kt b/src/main/kotlin/com/lambda/graphics/mc/LambdaRenderPipelines.kt index 8dcd92dd5..0ccc14b06 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/LambdaRenderPipelines.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/LambdaRenderPipelines.kt @@ -201,12 +201,34 @@ object LambdaRenderPipelines : Loadable { ) // ============================================================================ - // Screen-Space Pipelines + // 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( @@ -215,8 +237,8 @@ object LambdaRenderPipelines : Loadable { .withVertexShader(Identifier.of("lambda", "core/screen_lines")) .withFragmentShader(Identifier.of("lambda", "core/screen_lines")) .withBlend(BlendFunction.TRANSLUCENT) - .withDepthWrite(false) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) + .withDepthWrite(true) // Enable depth write for layer ordering + .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST) // Enable depth test .withCull(false) .withVertexFormat( LambdaVertexFormats.SCREEN_LINE_FORMAT, @@ -228,6 +250,7 @@ object LambdaRenderPipelines : Loadable { /** * 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( @@ -237,8 +260,8 @@ object LambdaRenderPipelines : Loadable { .withFragmentShader(Identifier.of("lambda", "core/screen_sdf_text")) .withSampler("Sampler0") .withBlend(BlendFunction.TRANSLUCENT) - .withDepthWrite(false) - .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) + .withDepthWrite(true) // Enable depth write for layer ordering + .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST) // Enable depth test .withCull(false) .withVertexFormat( LambdaVertexFormats.SCREEN_TEXT_SDF_FORMAT, diff --git a/src/main/kotlin/com/lambda/graphics/mc/LambdaVertexFormats.kt b/src/main/kotlin/com/lambda/graphics/mc/LambdaVertexFormats.kt index d95b27578..02eabfe63 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/LambdaVertexFormats.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/LambdaVertexFormats.kt @@ -141,16 +141,45 @@ object LambdaVertexFormats { ) /** - * Screen-space line format with dash support. - * Layout: Position (vec3), Color (vec4), Direction2D (vec2), LineWidth (float), Dash (vec4) + * 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 = 44 bytes + * 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) @@ -158,6 +187,7 @@ object LambdaVertexFormats { .add("Direction", DIRECTION_2D_ELEMENT) .add("LineWidth", LINE_WIDTH_FLOAT) .add("Dash", DASH_ELEMENT) + .add("Layer", LAYER_ELEMENT) .build() // ============================================================================ @@ -202,21 +232,23 @@ object LambdaVertexFormats { .build() /** - * Screen-space text format with SDF style parameters. - * Layout: Position (vec3), UV0 (vec2), Color (vec4), SDFStyle (vec4) + * 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 = 40 bytes + * 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() } diff --git a/src/main/kotlin/com/lambda/graphics/mc/RegionVertexCollector.kt b/src/main/kotlin/com/lambda/graphics/mc/RegionVertexCollector.kt index 6138d7332..788f642c4 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/RegionVertexCollector.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/RegionVertexCollector.kt @@ -109,13 +109,14 @@ class RegionVertexCollector { // Screen-Space Vertex Types // ============================================================================ - /** Screen-space face vertex data (2D position + color). */ + /** 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 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). */ + /** 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, @@ -125,12 +126,13 @@ class RegionVertexCollector { val dashLength: Float = 0f, val gapLength: Float = 0f, val dashOffset: Float = 0f, - val animationSpeed: 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. - * Uses SCREEN_TEXT_SDF_FORMAT (position + UV + color + style). + * 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 @@ -144,6 +146,7 @@ class RegionVertexCollector { * @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, @@ -153,7 +156,8 @@ class RegionVertexCollector { val outlineWidth: Float = 0f, val glowRadius: Float = 0f, val shadowSoftness: Float = 0f, - val threshold: Float = 0.5f + val threshold: Float = 0.5f, + val layer: Float = 0f // Depth for layering (higher = on top) ) /** Add a face vertex. */ @@ -243,26 +247,27 @@ class RegionVertexCollector { // Screen-Space Vertex Add Methods // ============================================================================ - /** Add a screen-space face vertex. */ - fun addScreenFaceVertex(x: Float, y: Float, color: Color) { - screenFaceVertices.add(ScreenFaceVertex(x, y, color.red, color.green, color.blue, color.alpha)) + /** 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). */ - fun addScreenEdgeVertex(x: Float, y: Float, color: Color, dx: Float, dy: Float, lineWidth: Float) { - screenEdgeVertices.add(ScreenEdgeVertex(x, y, color.red, color.green, color.blue, color.alpha, dx, dy, lineWidth)) + /** 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. */ + /** 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? + dashStyle: LineDashStyle?, + layer: Float ) { if (dashStyle == null) { - addScreenEdgeVertex(x, y, color, dx, dy, lineWidth) + addScreenEdgeVertex(x, y, color, dx, dy, lineWidth, layer) } else { screenEdgeVertices.add( ScreenEdgeVertex( @@ -273,15 +278,16 @@ class RegionVertexCollector { dashStyle.dashLength, dashStyle.gapLength, dashStyle.offset, - if (dashStyle.animated) dashStyle.animationSpeed else 0f + if (dashStyle.animated) dashStyle.animationSpeed else 0f, + layer ) ) } } - /** Add a screen-space text vertex. */ - fun addScreenTextVertex(x: Float, y: Float, u: Float, v: Float, r: Int, g: Int, b: Int, a: Int) { - screenTextVertices.add(ScreenTextVertex(x, y, u, v, r, g, b, a)) + /** 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)) } /** @@ -458,15 +464,24 @@ class RegionVertexCollector { screenFaceVertices.clear() var result: BufferResult? = null - BufferAllocator(vertices.size * 12).use { allocator -> + // SCREEN_FACE_FORMAT: 12 + 4 + 4 = 20 bytes per vertex + BufferAllocator(vertices.size * 24).use { allocator -> val builder = BufferBuilder( allocator, VertexFormat.DrawMode.QUADS, - VertexFormats.POSITION_COLOR + LambdaVertexFormats.SCREEN_FACE_FORMAT ) - // For screen-space: use x, y, with z = 0 - vertices.forEach { v -> builder.vertex(v.x, v.y, 0f).color(v.r, v.g, v.b, v.a) } + // 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() @@ -489,8 +504,8 @@ class RegionVertexCollector { screenEdgeVertices.clear() var result: BufferResult? = null - // Position (12) + Color (4) + Direction (8) + Width (4) + Dash (16) = 44 bytes, round up - BufferAllocator(vertices.size * 48).use { allocator -> + // 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, @@ -521,6 +536,12 @@ class RegionVertexCollector { 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 -> @@ -544,7 +565,7 @@ class RegionVertexCollector { screenTextVertices.clear() var result: BufferResult? = null - // SCREEN_TEXT_SDF_FORMAT: 12 + 8 + 4 + 16 = 40 bytes per vertex + // SCREEN_TEXT_SDF_FORMAT: 12 + 8 + 4 + 16 + 4 = 44 bytes per vertex BufferAllocator(vertices.size * 48).use { allocator -> val builder = BufferBuilder( allocator, @@ -566,6 +587,12 @@ class RegionVertexCollector { 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 -> diff --git a/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt b/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt index 7b097cfc9..bb1386631 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt @@ -22,6 +22,7 @@ import com.lambda.context.SafeContext import com.lambda.graphics.text.FontHandler import com.lambda.graphics.text.SDFFontAtlas 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.item.ItemStack @@ -46,6 +47,25 @@ class RenderBuilder(private val cameraPos: Vec3d) { // Style grouping maps removed - style is now embedded in each text vertex + // ============================================================================ + // Screen-Space Layer Tracking + // ============================================================================ + // Layer depth for screen-space ordering. Higher values render on top. + // Range: 0.0 to ~1.0, incrementing with each screen draw call. + + /** Current layer depth for screen-space ordering */ + private var currentLayer = 0f + + /** Increment per screen draw call (~10,000 possible layers) */ + private val layerIncrement = 0.0001f + + /** Get and increment the current layer depth */ + private fun nextLayer(): Float { + val layer = currentLayer + currentLayer += layerIncrement + return layer + } + /** * Deferred ItemStack renders to be drawn via Minecraft's DrawContext. * These are rendered after Lambda's geometry for proper layering. @@ -356,8 +376,8 @@ class RenderBuilder(private val cameraPos: Vec3d) { // Screen-Space Rendering Methods (Normalized Coordinates) // ============================================================================ // All coordinates use normalized 0-1 range: - // - (0, 0) = top-left corner - // - (1, 1) = bottom-right corner + // - (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). */ @@ -397,10 +417,11 @@ class RenderBuilder(private val cameraPos: Vec3d) { x3: Float, y3: Float, c3: Color, x4: Float, y4: Float, c4: Color ) { - collector.addScreenFaceVertex(toPixelX(x1), toPixelY(y1), c1) - collector.addScreenFaceVertex(toPixelX(x2), toPixelY(y2), c2) - collector.addScreenFaceVertex(toPixelX(x3), toPixelY(y3), c3) - collector.addScreenFaceVertex(toPixelX(x4), toPixelY(y4), c4) + 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) } /** @@ -420,7 +441,7 @@ class RenderBuilder(private val cameraPos: Vec3d) { * All values use normalized 0-1 range. * * @param x Left edge (0-1, where 0 = left, 1 = right) - * @param y Top edge (0-1, where 0 = top, 1 = bottom) + * @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 @@ -436,7 +457,7 @@ class RenderBuilder(private val cameraPos: Vec3d) { * All values use normalized 0-1 range. * * @param x Left edge (0-1) - * @param y Top 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 @@ -492,11 +513,14 @@ class RenderBuilder(private val cameraPos: Vec3d) { ) } + // Get layer for draw order + val layer = nextLayer() + // 4 vertices for screen-space line quad - collector.addScreenEdgeVertex(px1, py1, startColor, dx, dy, pixelWidth, pixelDashStyle) - collector.addScreenEdgeVertex(px1, py1, startColor, dx, dy, pixelWidth, pixelDashStyle) - collector.addScreenEdgeVertex(px2, py2, endColor, dx, dy, pixelWidth, pixelDashStyle) - collector.addScreenEdgeVertex(px2, py2, endColor, dx, dy, pixelWidth, pixelDashStyle) + 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) } /** @@ -561,39 +585,45 @@ class RenderBuilder(private val cameraPos: Vec3d) { 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 - val offsetY = style.shadow.offsetY * 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) + 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) + 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) + 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) + pixelX, pixelY, pixelSize, style, mainLayer) } /** @@ -608,7 +638,8 @@ class RenderBuilder(private val cameraPos: Vec3d) { r: Int, g: Int, b: Int, a: Int, anchorX: Float, anchorY: Float, pixelSize: Float, // Final text size in pixels - style: SDFStyle + style: SDFStyle, + layer: Float // Layer depth for draw order ) { // Extract SDF style params from SDFStyle object val outlineWidth = style.outline?.width ?: 0f @@ -624,12 +655,15 @@ class RenderBuilder(private val cameraPos: Vec3d) { 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 localY0 = -glyph.bearingY // Y flipped for screen (down = positive) + val localY1 = glyph.bearingY // Top of glyph (Y-up) // width/height are in pixels, need normalization val localX1 = localX0 + glyph.width / atlas.baseSize - val localY1 = localY0 + glyph.height / 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 @@ -638,14 +672,15 @@ class RenderBuilder(private val cameraPos: Vec3d) { 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, y1, glyph.u0, glyph.v1, r, g, b, a, outlineWidth, glowRadius, shadowSoftness, threshold)) + x0, y0, glyph.u0, glyph.v1, r, g, b, a, outlineWidth, glowRadius, shadowSoftness, threshold, layer)) collector.screenTextVertices.add(RegionVertexCollector.ScreenTextVertex( - x1, y1, glyph.u1, glyph.v1, r, g, b, a, outlineWidth, glowRadius, shadowSoftness, threshold)) + x1, y0, glyph.u1, glyph.v1, r, g, b, a, outlineWidth, glowRadius, shadowSoftness, threshold, layer)) collector.screenTextVertices.add(RegionVertexCollector.ScreenTextVertex( - x1, y0, glyph.u1, glyph.v0, r, g, b, a, outlineWidth, glowRadius, shadowSoftness, threshold)) + x1, y1, glyph.u1, glyph.v0, r, g, b, a, outlineWidth, glowRadius, shadowSoftness, threshold, layer)) collector.screenTextVertices.add(RegionVertexCollector.ScreenTextVertex( - x0, y0, glyph.u0, glyph.v0, r, g, b, a, outlineWidth, glowRadius, shadowSoftness, threshold)) + 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 @@ -738,9 +773,162 @@ class RenderBuilder(private val cameraPos: Vec3d) { } 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 */ @@ -803,7 +991,9 @@ class RenderBuilder(private val cameraPos: Vec3d) { data class SDFShadow( val color: Color = Color(0, 0, 0, 180), val offset: Float = 0.05f, // Distance in text units - val angle: Float = 135f, // Angle in degrees: 0=right, 90=down, 180=left, 270=up (default: bottom-right) + // 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 = 0.15f // Shadow blur in SDF units ) { /** X offset computed from angle and distance */ 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..4cdc551a7 --- /dev/null +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/AbstractRenderer.kt @@ -0,0 +1,165 @@ +/* + * 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.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 + +/** + * 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: Boolean = false) { + + /** + * 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? + + /** Deferred items for screen rendering (may be null) */ + protected abstract val deferredItems: List? + + /** + * Render world-space geometry (faces, edges, text). + * Iterates over all renderer/transform pairs from getRendererTransforms(). + */ + fun render() { + val chunks = getRendererTransforms() + if (chunks.isEmpty()) return + + // Render Faces + RegionRenderer.createRenderPass("$name Faces", depthTest)?.use { pass -> + pass.setPipeline(RendererUtils.getFacesPipeline(depthTest)) + RenderSystem.bindDefaultUniforms(pass) + chunks.forEach { (renderer, transform) -> + pass.setUniform("DynamicTransforms", transform) + renderer.renderFaces(pass) + } + } + + // Render Edges + RegionRenderer.createRenderPass("$name Edges", depthTest)?.use { pass -> + pass.setPipeline(RendererUtils.getEdgesPipeline(depthTest)) + 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", depthTest)?.use { pass -> + pass.setPipeline(RendererUtils.getTextPipeline(depthTest)) + RenderSystem.bindDefaultUniforms(pass) + pass.bindTexture("Sampler0", textureView, sampler) + textChunks.forEach { (renderer, transform) -> + pass.setUniform("DynamicTransforms", transform) + renderer.renderText(pass) + } + } + } + } + } + + /** + * Render screen-space geometry. Uses orthographic projection for 2D rendering. + * This should be called after world-space render() for proper layering. + */ + fun renderScreen() { + val renderers = getScreenRenderers() + val hasDeferredItems = deferredItems?.isNotEmpty() == true + + if (renderers.isEmpty() && !hasDeferredItems) return + + RendererUtils.withScreenContext { + val dynamicTransform = RendererUtils.createScreenDynamicTransform() + + // Render Screen Faces (no depth test for 2D) + RegionRenderer.createRenderPass("$name Screen Faces", false)?.use { pass -> + pass.setPipeline(RendererUtils.screenFacesPipeline) + RenderSystem.bindDefaultUniforms(pass) + pass.setUniform("DynamicTransforms", dynamicTransform) + renderers.forEach { it.renderScreenFaces(pass) } + } + + // Render Screen Edges + RegionRenderer.createRenderPass("$name Screen Edges", false)?.use { pass -> + pass.setPipeline(RendererUtils.screenEdgesPipeline) + 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) { + RegionRenderer.createRenderPass("$name Screen Text", false)?.use { pass -> + pass.setPipeline(RendererUtils.screenTextPipeline) + RenderSystem.bindDefaultUniforms(pass) + pass.setUniform("DynamicTransforms", dynamicTransform) + pass.bindTexture("Sampler0", textureView, sampler) + textRenderers.forEach { it.renderScreenText(pass) } + } + } + } + } + + // Render deferred items last (uses Minecraft's DrawContext pipeline) + deferredItems?.let { items -> + if (items.isNotEmpty()) { + RendererUtils.renderDeferredItems(items) + } + } + } +} diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt index 1048a8252..810f1088f 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt @@ -27,11 +27,13 @@ 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.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 com.mojang.blaze3d.buffers.GpuBufferSlice import com.mojang.blaze3d.systems.RenderSystem import net.minecraft.util.math.Vec3d import net.minecraft.world.World @@ -59,9 +61,10 @@ import java.util.concurrent.ConcurrentLinkedDeque class ChunkedRenderer( owner: Module, name: String, - var depthTest: Boolean = false, + depthTest: Boolean = false, private val update: RenderBuilder.(World, FastVector) -> Unit -) { +) : AbstractRenderer(name, depthTest) { + private val chunkMap = ConcurrentHashMap() private val WorldChunk.chunkKey: Long @@ -73,6 +76,14 @@ class ChunkedRenderer( private val rebuildQueue = ConcurrentLinkedDeque() private val uploadQueue = ConcurrentLinkedDeque<() -> Unit>() + // Font atlas from the default font handler + override val currentFontAtlas: SDFFontAtlas + get() = FontHandler.getDefaultFont() + + // ChunkedRenderer doesn't support deferred items (per-chunk geometry only) + override val deferredItems: List? + get() = null + private fun getChunkKey(chunkX: Int, chunkZ: Int): Long { return (chunkX.toLong() and 0xFFFFFFFFL) or ((chunkZ.toLong() and 0xFFFFFFFFL) shl 32) } @@ -108,18 +119,18 @@ class ChunkedRenderer( } /** - * Render all chunks with camera-relative translation. + * Get renderer/transform pairs for all active chunks. + * Each chunk has its own renderer and per-chunk transform (chunk-origin to camera). */ - fun render() { - val cameraPos = mc.gameRenderer?.camera?.pos ?: return + override fun getRendererTransforms(): List> { + val cameraPos = mc.gameRenderer?.camera?.pos ?: return emptyList() val activeChunks = chunkMap.values.filter { it.renderer.hasData() } - if (activeChunks.isEmpty()) return + if (activeChunks.isEmpty()) return emptyList() val modelViewMatrix = RenderMain.modelViewMatrix - // Pre-compute all transforms BEFORE starting render passes - val chunkTransforms = activeChunks.map { chunkData -> + 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() @@ -129,109 +140,20 @@ class ChunkedRenderer( val dynamicTransform = RenderSystem.getDynamicUniforms() .write(modelView, Vector4f(1f, 1f, 1f, 1f), Vector3f(0f, 0f, 0f), Matrix4f()) - chunkData to dynamicTransform - } - - // Render Faces - RegionRenderer.createRenderPass("ChunkedESP Faces", depthTest)?.use { pass -> - pass.setPipeline(RendererUtils.getFacesPipeline(depthTest)) - RenderSystem.bindDefaultUniforms(pass) - - chunkTransforms.forEach { (chunkData, transform) -> - pass.setUniform("DynamicTransforms", transform) - chunkData.renderer.renderFaces(pass) - } - } - - // Render Edges - RegionRenderer.createRenderPass("ChunkedESP Edges", depthTest)?.use { pass -> - pass.setPipeline(RendererUtils.getEdgesPipeline(depthTest)) - RenderSystem.bindDefaultUniforms(pass) - - chunkTransforms.forEach { (chunkData, transform) -> - pass.setUniform("DynamicTransforms", transform) - chunkData.renderer.renderEdges(pass) - } - } - - // Render Text - style params are now embedded in vertex attributes - val chunksWithText = chunkTransforms.filter { (chunkData, _) -> chunkData.renderer.hasTextData() } - if (chunksWithText.isNotEmpty()) { - val atlas = FontHandler.getDefaultFont() - if (!atlas.isUploaded) atlas.upload() - val textureView = atlas.textureView - val sampler = atlas.sampler - if (textureView != null && sampler != null) { - RegionRenderer.createRenderPass("ChunkedESP Text", depthTest)?.use { pass -> - pass.setPipeline(RendererUtils.getTextPipeline(depthTest)) - RenderSystem.bindDefaultUniforms(pass) - pass.bindTexture("Sampler0", textureView, sampler) - - chunksWithText.forEach { (chunkData, transform) -> - pass.setUniform("DynamicTransforms", transform) - chunkData.renderer.renderText(pass) - } - } - } + chunkData.renderer to dynamicTransform } } - /** - * Render screen-space geometry for all chunks. - * Uses orthographic projection for 2D rendering. + * Get renderers for screen-space rendering. + * Returns all chunk renderers that have screen data. */ - fun renderScreen() { - val activeChunks = chunkMap.values.filter { it.renderer.hasScreenData() } - if (activeChunks.isEmpty()) return - - RendererUtils.withScreenContext { - val dynamicTransform = RendererUtils.createScreenDynamicTransform() - - // Render Screen Faces - RegionRenderer.createRenderPass("ChunkedESP Screen Faces", false)?.use { pass -> - pass.setPipeline(RendererUtils.screenFacesPipeline) - RenderSystem.bindDefaultUniforms(pass) - pass.setUniform("DynamicTransforms", dynamicTransform) - activeChunks.forEach { chunkData -> - chunkData.renderer.renderScreenFaces(pass) - } - } - - // Render Screen Edges - RegionRenderer.createRenderPass("ChunkedESP Screen Edges", false)?.use { pass -> - pass.setPipeline(RendererUtils.screenEdgesPipeline) - RenderSystem.bindDefaultUniforms(pass) - pass.setUniform("DynamicTransforms", dynamicTransform) - activeChunks.forEach { chunkData -> - chunkData.renderer.renderScreenEdges(pass) - } - } - - // Render Screen Text - style params are now embedded in vertex attributes - val chunksWithText = activeChunks.filter { it.renderer.hasScreenTextData() } - if (chunksWithText.isNotEmpty()) { - val atlas = FontHandler.getDefaultFont() - if (!atlas.isUploaded) atlas.upload() - val textureView = atlas.textureView - val sampler = atlas.sampler - if (textureView != null && sampler != null) { - RegionRenderer.createRenderPass("ChunkedESP Screen Text", false)?.use { pass -> - pass.setPipeline(RendererUtils.screenTextPipeline) - RenderSystem.bindDefaultUniforms(pass) - pass.setUniform("DynamicTransforms", dynamicTransform) - pass.bindTexture("Sampler0", textureView, sampler) - - chunksWithText.forEach { chunkData -> - chunkData.renderer.renderScreenText(pass) - } - } - } - } - } + override fun getScreenRenderers(): List { + return chunkMap.values + .filter { it.renderer.hasScreenData() } + .map { it.renderer } } - companion object { fun Module.chunkedEsp( name: String, @@ -328,4 +250,3 @@ class ChunkedRenderer( } } } - diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt index 5c40e790d..e8ff72832 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt @@ -22,6 +22,7 @@ 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 @@ -36,12 +37,19 @@ import org.joml.Vector4f * 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(val name: String, var depthTest: Boolean = false) { +class ImmediateRenderer(name: String, depthTest: Boolean = false) : AbstractRenderer(name, depthTest) { private val renderer = RegionRenderer() // Current frame builder (being populated this frame) private var renderBuilder: RenderBuilder? = null + // Font atlas used for current text rendering + private var _currentFontAtlas: SDFFontAtlas? = null + override val currentFontAtlas: SDFFontAtlas? get() = _currentFontAtlas + + override val deferredItems: List? + get() = renderBuilder?.deferredItems + /** * Get the current camera position for building camera-relative shapes. * Returns null if camera is not available. @@ -64,17 +72,14 @@ class ImmediateRenderer(val name: String, var depthTest: Boolean = false) { renderBuilder = null } - // Font atlas used for current text rendering - private var currentFontAtlas: SDFFontAtlas? = null - /** Upload collected geometry to GPU. Must be called on main thread. */ fun upload() { renderBuilder?.let { s -> renderer.upload(s.collector) - currentFontAtlas = s.fontAtlas + _currentFontAtlas = s.fontAtlas } ?: run { renderer.clearData() - currentFontAtlas = null + _currentFontAtlas = null } } @@ -85,14 +90,13 @@ class ImmediateRenderer(val name: String, var depthTest: Boolean = false) { } /** - * Render all geometry. Since coordinates are already camera-relative, - * we just use the base modelView matrix without additional translation. + * Get renderer/transform pairs for world-space rendering. + * Returns single renderer with identity-based transform (camera-relative coords). */ - fun render() { - if (!renderer.hasData()) return - + override fun getRendererTransforms(): List> { + if (!renderer.hasData()) return emptyList() + val modelViewMatrix = RenderMain.modelViewMatrix - val dynamicTransform = RenderSystem.getDynamicUniforms() .write( modelViewMatrix, @@ -100,93 +104,14 @@ class ImmediateRenderer(val name: String, var depthTest: Boolean = false) { Vector3f(0f, 0f, 0f), Matrix4f() ) - - // Render Faces - RegionRenderer.createRenderPass("$name Faces", depthTest)?.use { pass -> - pass.setPipeline(RendererUtils.getFacesPipeline(depthTest)) - RenderSystem.bindDefaultUniforms(pass) - pass.setUniform("DynamicTransforms", dynamicTransform) - renderer.renderFaces(pass) - } - - // Render Edges - RegionRenderer.createRenderPass("$name Edges", depthTest)?.use { pass -> - pass.setPipeline(RendererUtils.getEdgesPipeline(depthTest)) - RenderSystem.bindDefaultUniforms(pass) - pass.setUniform("DynamicTransforms", dynamicTransform) - renderer.renderEdges(pass) - } - - // Render Text - style params are now embedded in vertex attributes - val atlas = currentFontAtlas - if (atlas != null && renderer.hasTextData()) { - if (!atlas.isUploaded) atlas.upload() - val textureView = atlas.textureView - val sampler = atlas.sampler - if (textureView != null && sampler != null) { - RegionRenderer.createRenderPass("$name Text", depthTest)?.use { pass -> - pass.setPipeline(RendererUtils.getTextPipeline(depthTest)) - RenderSystem.bindDefaultUniforms(pass) - pass.setUniform("DynamicTransforms", dynamicTransform) - pass.bindTexture("Sampler0", textureView, sampler) - renderer.renderText(pass) - } - } - } + + return listOf(renderer to dynamicTransform) } /** - * Render screen-space geometry. Uses orthographic projection for 2D rendering. - * This should be called after world-space render() for proper layering. + * Get renderers for screen-space rendering. */ - fun renderScreen() { - val hasDeferredItems = renderBuilder?.deferredItems?.isNotEmpty() == true - - if (!renderer.hasScreenData() && !hasDeferredItems) return - - RendererUtils.withScreenContext { - val dynamicTransform = RendererUtils.createScreenDynamicTransform() - - // Render Screen Faces (no depth test for 2D) - RegionRenderer.createRenderPass("$name Screen Faces", false)?.use { pass -> - pass.setPipeline(RendererUtils.screenFacesPipeline) - RenderSystem.bindDefaultUniforms(pass) - pass.setUniform("DynamicTransforms", dynamicTransform) - renderer.renderScreenFaces(pass) - } - - // Render Screen Edges - RegionRenderer.createRenderPass("$name Screen Edges", false)?.use { pass -> - pass.setPipeline(RendererUtils.screenEdgesPipeline) - RenderSystem.bindDefaultUniforms(pass) - pass.setUniform("DynamicTransforms", dynamicTransform) - renderer.renderScreenEdges(pass) - } - - // Render Screen Text - style params are now embedded in vertex attributes - val atlas = currentFontAtlas - if (atlas != null && renderer.hasScreenTextData()) { - if (!atlas.isUploaded) atlas.upload() - val textureView = atlas.textureView - val sampler = atlas.sampler - if (textureView != null && sampler != null) { - RegionRenderer.createRenderPass("$name Screen Text", false)?.use { pass -> - pass.setPipeline(RendererUtils.screenTextPipeline) - RenderSystem.bindDefaultUniforms(pass) - pass.setUniform("DynamicTransforms", dynamicTransform) - pass.bindTexture("Sampler0", textureView, sampler) - renderer.renderScreenText(pass) - } - } - } - } - - // Render deferred items last (uses Minecraft's DrawContext pipeline) - renderBuilder?.deferredItems?.let { items -> - if (items.isNotEmpty()) { - RendererUtils.renderDeferredItems(items) - } - } + override fun getScreenRenderers(): List { + return if (renderer.hasScreenData()) listOf(renderer) else emptyList() } } - diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/RendererUtils.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/RendererUtils.kt index ced868380..0aa059f3c 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/renderer/RendererUtils.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/RendererUtils.kt @@ -21,10 +21,13 @@ import com.lambda.Lambda.mc import com.lambda.event.events.HudRenderEvent import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafe import com.lambda.graphics.mc.LambdaRenderPipelines +import com.lambda.graphics.mc.RegionRenderer +import com.lambda.graphics.text.SDFFontAtlas 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 @@ -36,7 +39,8 @@ import org.joml.Vector4f */ object RendererUtils { // Shared projection matrix for screen-space rendering - private val screenProjectionMatrix = ProjectionMatrix2("lambda_screen", -1000f, 1000f, true) + // invertY=false means Y=0 at bottom, Y=height at top (OpenGL/math convention) + private val screenProjectionMatrix = ProjectionMatrix2("lambda_screen", -1000f, 1000f, false) @@ -104,8 +108,8 @@ object RendererUtils { if (depthTest) LambdaRenderPipelines.SDF_TEXT else LambdaRenderPipelines.SDF_TEXT_THROUGH - /** Screen-space faces pipeline (always no depth test). */ - val screenFacesPipeline: RenderPipeline get() = LambdaRenderPipelines.ESP_QUADS_THROUGH + /** Screen-space faces pipeline (with layer-based depth for draw order). */ + val screenFacesPipeline: RenderPipeline get() = LambdaRenderPipelines.SCREEN_FACES /** Screen-space edges pipeline. */ val screenEdgesPipeline: RenderPipeline get() = LambdaRenderPipelines.SCREEN_LINES @@ -140,7 +144,7 @@ object RendererUtils { * * @param context The DrawContext from Minecraft's HUD rendering */ - fun renderPendingItems(context: net.minecraft.client.gui.DrawContext) { + fun renderPendingItems(context: DrawContext) { if (pendingItems.isEmpty()) return val window = mc.window ?: return diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.kt index b212b2c28..23641e59a 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.kt @@ -22,6 +22,7 @@ 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 @@ -36,13 +37,20 @@ import org.joml.Vector4f * 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(val name: String, var depthTest: Boolean = false) { +class TickedRenderer(name: String, depthTest: Boolean = false) : AbstractRenderer(name, depthTest) { private val renderer = RegionRenderer() private var renderBuilder: RenderBuilder? = null // 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 + + override val deferredItems: List? + get() = renderBuilder?.deferredItems + /** Get the current shape scope for drawing. Geometry stored relative to tick camera. */ fun shapes(block: RenderBuilder.() -> Unit) { val cameraPos = mc.gameRenderer?.camera?.pos ?: return @@ -59,17 +67,14 @@ class TickedRenderer(val name: String, var depthTest: Boolean = false) { tickCameraPos = null } - // Font atlas used for current text rendering - private var currentFontAtlas: SDFFontAtlas? = null - /** Upload collected geometry to GPU. Must be called on main thread. */ fun upload() { renderBuilder?.let { s -> renderer.upload(s.collector) - currentFontAtlas = s.fontAtlas + _currentFontAtlas = s.fontAtlas } ?: run { renderer.clearData() - currentFontAtlas = null + _currentFontAtlas = null } } @@ -79,15 +84,14 @@ class TickedRenderer(val name: String, var depthTest: Boolean = false) { clear() } - /** - * Render with smooth camera interpolation. - * Computes delta between tick-camera and current-camera in double precision. + * Get renderer/transform pairs for world-space rendering. + * Computes delta between tick-camera and current-camera for smooth interpolation. */ - fun render() { - val currentCameraPos = mc.gameRenderer?.camera?.pos ?: return - val tickCamera = tickCameraPos ?: return - if (!renderer.hasData()) return + 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 @@ -100,93 +104,14 @@ class TickedRenderer(val name: String, var depthTest: Boolean = false) { val modelView = Matrix4f(modelViewMatrix).translate(deltaX, deltaY, deltaZ) val dynamicTransform = RenderSystem.getDynamicUniforms() .write(modelView, Vector4f(1f, 1f, 1f, 1f), Vector3f(0f, 0f, 0f), Matrix4f()) - - // Render Faces - RegionRenderer.createRenderPass("$name Faces", depthTest)?.use { pass -> - pass.setPipeline(RendererUtils.getFacesPipeline(depthTest)) - RenderSystem.bindDefaultUniforms(pass) - pass.setUniform("DynamicTransforms", dynamicTransform) - renderer.renderFaces(pass) - } - - // Render Edges - RegionRenderer.createRenderPass("$name Edges", depthTest)?.use { pass -> - pass.setPipeline(RendererUtils.getEdgesPipeline(depthTest)) - RenderSystem.bindDefaultUniforms(pass) - pass.setUniform("DynamicTransforms", dynamicTransform) - renderer.renderEdges(pass) - } - - // Render Text - style params are now embedded in vertex attributes - val atlas = currentFontAtlas - if (atlas != null && renderer.hasTextData()) { - if (!atlas.isUploaded) atlas.upload() - val textureView = atlas.textureView - val sampler = atlas.sampler - if (textureView != null && sampler != null) { - RegionRenderer.createRenderPass("$name Text", depthTest)?.use { pass -> - pass.setPipeline(RendererUtils.getTextPipeline(depthTest)) - RenderSystem.bindDefaultUniforms(pass) - pass.setUniform("DynamicTransforms", dynamicTransform) - pass.bindTexture("Sampler0", textureView, sampler) - renderer.renderText(pass) - } - } - } + + return listOf(renderer to dynamicTransform) } /** - * Render screen-space geometry. Uses orthographic projection for 2D rendering. - * This should be called after world-space render() for proper layering. + * Get renderers for screen-space rendering. */ - fun renderScreen() { - val hasDeferredItems = renderBuilder?.deferredItems?.isNotEmpty() == true - - if (!renderer.hasScreenData() && !hasDeferredItems) return - - RendererUtils.withScreenContext { - val dynamicTransform = RendererUtils.createScreenDynamicTransform() - - // Render Screen Faces - RegionRenderer.createRenderPass("$name Screen Faces", false)?.use { pass -> - pass.setPipeline(RendererUtils.screenFacesPipeline) - RenderSystem.bindDefaultUniforms(pass) - pass.setUniform("DynamicTransforms", dynamicTransform) - renderer.renderScreenFaces(pass) - } - - // Render Screen Edges - RegionRenderer.createRenderPass("$name Screen Edges", false)?.use { pass -> - pass.setPipeline(RendererUtils.screenEdgesPipeline) - RenderSystem.bindDefaultUniforms(pass) - pass.setUniform("DynamicTransforms", dynamicTransform) - renderer.renderScreenEdges(pass) - } - - // Render Screen Text - style params are now embedded in vertex attributes - val atlas = currentFontAtlas - if (atlas != null && renderer.hasScreenTextData()) { - if (!atlas.isUploaded) atlas.upload() - val textureView = atlas.textureView - val sampler = atlas.sampler - if (textureView != null && sampler != null) { - RegionRenderer.createRenderPass("$name Screen Text", false)?.use { pass -> - pass.setPipeline(RendererUtils.screenTextPipeline) - RenderSystem.bindDefaultUniforms(pass) - pass.setUniform("DynamicTransforms", dynamicTransform) - pass.bindTexture("Sampler0", textureView, sampler) - renderer.renderScreenText(pass) - } - } - } - } - - // Render deferred items last (uses Minecraft's DrawContext pipeline) - renderBuilder?.deferredItems?.let { items -> - if (items.isNotEmpty()) { - RendererUtils.renderDeferredItems(items) - } - } + override fun getScreenRenderers(): List { + return if (renderer.hasScreenData()) listOf(renderer) else emptyList() } } - diff --git a/src/main/kotlin/com/lambda/module/modules/debug/RendererTestModule.kt b/src/main/kotlin/com/lambda/module/modules/debug/RendererTestModule.kt index 490c567b7..9130fb9a4 100644 --- a/src/main/kotlin/com/lambda/module/modules/debug/RendererTestModule.kt +++ b/src/main/kotlin/com/lambda/module/modules/debug/RendererTestModule.kt @@ -18,6 +18,7 @@ package com.lambda.module.modules.debug import com.lambda.event.events.RenderEvent +import com.lambda.event.events.ScreenRenderEvent import com.lambda.event.events.TickEvent import com.lambda.event.listener.SafeListener.Companion.listen import com.lambda.graphics.mc.LineDashStyle.Companion.marchingAnts @@ -128,6 +129,9 @@ object ChunkedRendererTest : Module( listen { esp.depthTest = !throughWalls esp.render() + } + + listen { esp.renderScreen() } @@ -152,6 +156,9 @@ object TickedRendererTest : Module( init { listen { renderer.render() + } + + listen { renderer.renderScreen() } @@ -322,6 +329,9 @@ object ImmediateRendererTest : Module( renderer.upload() renderer.render() + } + + listen { renderer.renderScreen() } 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 aba7f05ce..d9687fa5c 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/EntityESP.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/EntityESP.kt @@ -20,6 +20,7 @@ package com.lambda.module.modules.render import com.lambda.context.SafeContext import com.lambda.event.events.GuiEvent import com.lambda.event.events.RenderEvent +import com.lambda.event.events.ScreenRenderEvent import com.lambda.event.listener.SafeListener.Companion.listen import com.lambda.graphics.mc.renderer.ImmediateRenderer import com.lambda.module.Module @@ -161,11 +162,15 @@ object EntityESP : Module( esp.upload() esp.render() - esp.renderScreen() + // Screen rendering handled by ScreenRenderEvent listener below // Clear pending labels from previous frame pendingLabels.clear() } + + listen { + esp.renderScreen() + } // Draw ImGUI labels using pre-computed screen coordinates listen { 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..7d1312bb6 --- /dev/null +++ b/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt @@ -0,0 +1,29 @@ +/* + * 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.module.Module +import com.lambda.module.tag.ModuleTag + +object Nametags : Module( + name = "Nametags", + description = "Displays information about entities above them", + tag = ModuleTag.RENDER +) { +// private val +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/module/modules/render/Tracers.kt b/src/main/kotlin/com/lambda/module/modules/render/Tracers.kt index c5c85f423..9a4f2dfa3 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/Tracers.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/Tracers.kt @@ -18,6 +18,7 @@ package com.lambda.module.modules.render 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 @@ -62,6 +63,7 @@ object Tracers : Module( renderer.tick() renderer.shapes { 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) { @@ -88,6 +90,10 @@ object Tracers : Module( } renderer.upload() renderer.render() + // Screen rendering handled by ScreenRenderEvent listener below + } + + listen { renderer.renderScreen() } } 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..e6dae2896 --- /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; + + // Use layer as fragment depth for draw order + gl_FragDepth = v_Layer; +} 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_lines.fsh b/src/main/resources/assets/lambda/shaders/core/screen_lines.fsh index f41310310..c27181979 100644 --- a/src/main/resources/assets/lambda/shaders/core/screen_lines.fsh +++ b/src/main/resources/assets/lambda/shaders/core/screen_lines.fsh @@ -11,6 +11,7 @@ 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; @@ -113,4 +114,7 @@ void main() { color.a *= alpha; fragColor = color; + + // Use layer as fragment depth for draw order + gl_FragDepth = v_Layer; } diff --git a/src/main/resources/assets/lambda/shaders/core/screen_lines.vsh b/src/main/resources/assets/lambda/shaders/core/screen_lines.vsh index b1ac4d248..e268a7f96 100644 --- a/src/main/resources/assets/lambda/shaders/core/screen_lines.vsh +++ b/src/main/resources/assets/lambda/shaders/core/screen_lines.vsh @@ -10,6 +10,7 @@ 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; @@ -19,6 +20,7 @@ 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 @@ -64,4 +66,5 @@ void main() { 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 index 18c870c5c..302dba730 100644 --- a/src/main/resources/assets/lambda/shaders/core/screen_sdf_text.fsh +++ b/src/main/resources/assets/lambda/shaders/core/screen_sdf_text.fsh @@ -9,6 +9,7 @@ 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; @@ -66,4 +67,7 @@ void main() { // Apply color modulator (no fog for screen-space) fragColor = result * ColorModulator; + + // Use layer as fragment depth for draw order + gl_FragDepth = v_Layer; } 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 index 0a52bd95e..06c67b55b 100644 --- a/src/main/resources/assets/lambda/shaders/core/screen_sdf_text.vsh +++ b/src/main/resources/assets/lambda/shaders/core/screen_sdf_text.vsh @@ -9,11 +9,13 @@ 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 @@ -22,4 +24,5 @@ void main() { texCoord0 = UV0; vertexColor = Color; sdfStyleParams = SDFStyle; + v_Layer = Layer; } From 30af1c268d9695179493af44a8d441b98c139302 Mon Sep 17 00:00:00 2001 From: beanbag44 <107891830+beanbag44@users.noreply.github.com> Date: Wed, 21 Jan 2026 13:25:52 +0000 Subject: [PATCH 18/26] world line rendering improvements --- .../module/modules/render/BlockOutline.kt | 4 +-- .../lambda/shaders/core/advanced_lines.fsh | 29 ++++++++++++++----- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt b/src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt index 2effbb983..cbfd87f4b 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt @@ -39,7 +39,7 @@ object BlockOutline : Module( 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 lineWidth by setting("Line Width", 0.01f, 0.001f..1.0f, 0.001f) { outline } + private val lineWidth by setting("Line Width", 5, 1..50, 1) { outline } private val interpolate by setting("Interpolate", true) private val throughWalls by setting("ESP", true) .onValueChange { _, to -> renderer.depthTest = !to } @@ -71,7 +71,7 @@ object BlockOutline : Module( renderer.shapes { boxes.forEach { box -> - box(box, lineWidth) { + box(box, lineWidth * 0.001f) { colors(fillColor, outlineColor) if (!fill) hideFill() if (!outline) hideOutline() diff --git a/src/main/resources/assets/lambda/shaders/core/advanced_lines.fsh b/src/main/resources/assets/lambda/shaders/core/advanced_lines.fsh index b88072616..a81e4ac31 100644 --- a/src/main/resources/assets/lambda/shaders/core/advanced_lines.fsh +++ b/src/main/resources/assets/lambda/shaders/core/advanced_lines.fsh @@ -58,15 +58,30 @@ void main() { // SDF: distance to capsule surface (positive = outside, negative = inside) float sdf = dist3D - radius; - // Calculate AA width from screen-space derivatives of expanded position - float aaWidth = length(vec2(fwidth(v_ExpandedPos.x), fwidth(v_ExpandedPos.y))); + // Use fwidth(sdf) for AA - this measures how fast the SDF changes per pixel, + // which is stable regardless of viewing angle. When looking down the line, + // the SDF change per pixel remains consistent because we care about the + // perpendicular distance to the capsule surface, not world-space position. + float sdfGrad = fwidth(sdf); - // Adaptive AA: thin lines get softer edges, thick lines get crisp edges - // Below 2px width, scale up AA for smooth thin lines; above 2px, use tight 0.5px AA - float thinness = clamp(1.0 - v_LineWidth / (2.0 * aaWidth), 0.0, 1.0); - float adaptiveAA = mix(aaWidth * 0.5, aaWidth * 1.5, thinness); + // Calculate screen-space line width in pixels (diameter) + float screenLineWidth = (radius * 2.0) / max(sdfGrad, 0.0001); - float alpha = 1.0 - smoothstep(-adaptiveAA, adaptiveAA, sdf); + // For sub-pixel lines: fade alpha based on line width + // This allows lines to naturally disappear at distance + float coverageFactor = clamp(screenLineWidth, 0.0, 1.0); + + // Adaptive AA using consistent sdfGrad units: + // - Thick lines (>4px): crisp edges with 0.5px AA on each side + // - Thin lines (<2px): soft edges with 1.5px AA on each side + // All in screen-space for consistency + float thinness = clamp(1.0 - (screenLineWidth - 2.0) / 2.0, 0.0, 1.0); + float aaWidth = mix(sdfGrad * 0.5, sdfGrad * 1.5, thinness); + + float alpha = 1.0 - smoothstep(-aaWidth, aaWidth, sdf); + + // Apply coverage factor to fade sub-pixel lines + alpha *= coverageFactor; // Skip fragments outside the line if (alpha < 0.004) { From fbb6c5257f20d31bbcd8e58f5e30dade2573c530 Mon Sep 17 00:00:00 2001 From: beanbag44 <107891830+beanbag44@users.noreply.github.com> Date: Wed, 21 Jan 2026 15:03:59 +0000 Subject: [PATCH 19/26] more tracer improvements --- .../mixin/render/GameRendererMixin.java | 12 ++++++- .../kotlin/com/lambda/graphics/RenderMain.kt | 6 ++-- .../lambda/module/modules/render/Nametags.kt | 1 - .../lambda/module/modules/render/Tracers.kt | 32 ++++++++++++++++--- 4 files changed, 43 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/lambda/mixin/render/GameRendererMixin.java b/src/main/java/com/lambda/mixin/render/GameRendererMixin.java index 4c2e7d3af..340a5f114 100644 --- a/src/main/java/com/lambda/mixin/render/GameRendererMixin.java +++ b/src/main/java/com/lambda/mixin/render/GameRendererMixin.java @@ -74,9 +74,19 @@ private float modifyGetFov(float original) { return original / Zoom.getLerpedZoom(); } + /** + * Inject screen rendering after InGameHud.render() but before overlays/screens. + * This makes Lambda's screen renders appear: + * - Above: hotbar, held items, health bars + * - Below: inventory GUI, chat, escape menu + */ + @Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/hud/InGameHud;render(Lnet/minecraft/client/gui/DrawContext;Lnet/minecraft/client/render/RenderTickCounter;)V", shift = At.Shift.AFTER)) + private void onHudRenderComplete(RenderTickCounter tickCounter, boolean tick, CallbackInfo ci) { + RenderMain.renderScreen(); + } + @Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/render/GuiRenderer;render(Lcom/mojang/blaze3d/buffers/GpuBufferSlice;)V", shift = At.Shift.AFTER)) private void onGuiRenderComplete(RenderTickCounter tickCounter, boolean tick, CallbackInfo ci) { - RenderMain.renderScreen(); DearImGui.INSTANCE.render(); } diff --git a/src/main/kotlin/com/lambda/graphics/RenderMain.kt b/src/main/kotlin/com/lambda/graphics/RenderMain.kt index 8e610efc6..67d0763cd 100644 --- a/src/main/kotlin/com/lambda/graphics/RenderMain.kt +++ b/src/main/kotlin/com/lambda/graphics/RenderMain.kt @@ -129,8 +129,10 @@ object RenderMain { /** * Render all screen-space elements. - * Called after Minecraft's guiRenderer.render() to ensure Lambda's - * screen elements appear above all of Minecraft's GUI. + * Called after Minecraft's InGameHud.render() but before overlays/screens. + * Lambda screen elements appear: + * - Above: hotbar, held items, health bars + * - Below: inventory GUI, chat, escape menu */ @JvmStatic fun renderScreen() { diff --git a/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt b/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt index 7d1312bb6..47fa00250 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt @@ -25,5 +25,4 @@ object Nametags : Module( description = "Displays information about entities above them", tag = ModuleTag.RENDER ) { -// private val } \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/module/modules/render/Tracers.kt b/src/main/kotlin/com/lambda/module/modules/render/Tracers.kt index 9a4f2dfa3..0cff7a589 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/Tracers.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/Tracers.kt @@ -21,7 +21,7 @@ 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 +import com.lambda.graphics.RenderMain.worldToScreenNormalized import com.lambda.graphics.mc.renderer.ImmediateRenderer import com.lambda.module.Module import com.lambda.module.tag.ModuleTag @@ -32,6 +32,7 @@ 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 @@ -41,9 +42,11 @@ object Tracers : Module( description = "Draws lines to entities within the world", tag = ModuleTag.RENDER ) { - private val friendColor by setting("Friend Color", Color.BLUE) private val width by setting("Width", 1, 1..50, 1) + private val target by setting("Target", TracerMode.Feet) + private val stem by setting("Stem", true) private val entities by setting("Entities", setOf(EntityGroup.Player, EntityGroup.Mob, EntityGroup.Boss), EntityGroup.entries) + private val friendColor by setting("Friend Color", Color.BLUE) private val playerDistanceGradient by setting("Player Distance Gradient", true) { EntityGroup.Player in entities } private val playerDistanceColorFar by setting("Player Far Color", Color.GREEN) { EntityGroup.Player in entities && playerDistanceGradient } private val playerDistanceColorClose by setting("Player Close Color", Color.RED) { EntityGroup.Player in entities && playerDistanceGradient } @@ -84,17 +87,38 @@ object Tracers : Module( EntityGroup.Boss -> bossColor else -> miscColor } - val (toX, toY) = RenderMain.worldToScreenNormalized(lerp(mc.tickDelta, entity.prevPos, entity.pos)) ?: return@forEach + 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, width * 0.0001f) + 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, width * 0.0001f) + } } } renderer.upload() renderer.render() - // Screen rendering handled by ScreenRenderEvent listener below } listen { renderer.renderScreen() } } + + private enum class TracerMode { + Feet, + Middle, + Eyes + } } \ No newline at end of file From ea9aefcd944d0020a60a1b316440448c8f2a3ed2 Mon Sep 17 00:00:00 2001 From: beanbag44 <107891830+beanbag44@users.noreply.github.com> Date: Thu, 22 Jan 2026 12:47:49 +0000 Subject: [PATCH 20/26] nametags --- .../com/lambda/mixin/entity/EntityMixin.java | 2 + .../render/LivingEntityRendererMixin.java | 9 ++ .../com/lambda/graphics/mc/RenderBuilder.kt | 29 ++-- .../graphics/mc/renderer/RendererUtils.kt | 32 ++--- .../com/lambda/graphics/text/FontHandler.kt | 6 +- .../com/lambda/graphics/text/SDFFontAtlas.kt | 46 ++++++- .../com/lambda/graphics/util/DynamicAABB.kt | 6 + .../lambda/module/modules/render/Nametags.kt | 127 ++++++++++++++++++ .../lambda/module/modules/render/Tracers.kt | 2 +- .../com/lambda/util/extension/Entity.kt | 3 + 10 files changed, 232 insertions(+), 30 deletions(-) diff --git a/src/main/java/com/lambda/mixin/entity/EntityMixin.java b/src/main/java/com/lambda/mixin/entity/EntityMixin.java index 2cfeb7747..4f67764e6 100644 --- a/src/main/java/com/lambda/mixin/entity/EntityMixin.java +++ b/src/main/java/com/lambda/mixin/entity/EntityMixin.java @@ -151,11 +151,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 != Lambda.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/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/graphics/mc/RenderBuilder.kt b/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt index bb1386631..90bf87ff6 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt @@ -324,8 +324,8 @@ class RenderBuilder(private val cameraPos: Vec3d) { val anchorY = (pos.y - cameraPos.y).toFloat() val anchorZ = (pos.z - cameraPos.z).toFloat() - // Calculate text width for centering - val textWidth = if (centered) atlas.getStringWidth(text, 1f) else 0f + // 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 @@ -396,11 +396,11 @@ class RenderBuilder(private val cameraPos: Vec3d) { /** * Convert normalized size to pixel size. - * By default uses the average of width and height for uniform scaling. - * Use toPixelSizeX/Y for non-uniform scaling. + * 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 * (screenWidth + screenHeight) / 2f + normalizedSize * screenHeight /** * Draw a filled quad on screen with gradient colors. @@ -557,7 +557,7 @@ class RenderBuilder(private val cameraPos: Vec3d) { * * @param text Text to render * @param x X position (0-1, where 0 = left, 1 = right) - * @param y Y position (0-1, where 0 = top, 1 = bottom) + * @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 @@ -578,10 +578,19 @@ class RenderBuilder(private val cameraPos: Vec3d) { // Convert to pixel coordinates val pixelX = toPixelX(x) val pixelY = toPixelY(y) - val pixelSize = toPixelSize(size) - - // Calculate text width for centering - val textWidth = if (centered) atlas.getStringWidth(text, pixelSize) else 0f + + // 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 diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/RendererUtils.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/RendererUtils.kt index 0aa059f3c..51312e80c 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/renderer/RendererUtils.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/RendererUtils.kt @@ -157,27 +157,29 @@ object RendererUtils { val standardItemSize = 16f pendingItems.forEach { item -> - val pixelX = (item.x * scaledWidth).toInt() - val pixelY = (item.y * scaledHeight).toInt() + // Use floating point for smooth sub-pixel positioning (prevents jitter from integer truncation) + val pixelX = item.x * scaledWidth - // Calculate scale based on normalized size using average of dimensions (matches toPixelSize) - // Size of 0.05 means ~5% of screen, so pixelSize = size * (width + height) / 2 - val targetPixelSize = item.size * (scaledWidth + scaledHeight) / 2f + // Calculate scale based on normalized size using height-only (matches toPixelSize) + // Size of 0.05 means 5% of screen height, so pixelSize = size * height + val targetPixelSize = item.size * scaledHeight val scale = targetPixelSize / standardItemSize + // Flip Y: our normalized coords use Y=0 at bottom, but DrawContext uses Y=0 at top + // Also offset by item height so items grow UPWARD from the specified position + // (DrawContext draws from top-left extending down, we want bottom-left extending up) + val itemHeight = standardItemSize * scale + val pixelY = (1f - item.y) * scaledHeight - itemHeight + + // Always use matrix translation for smooth sub-pixel positioning + context.matrices.pushMatrix() + context.matrices.translate(pixelX, pixelY) if (scale != 1f) { - // For scaled items, we need to translate and scale the matrix - // Matrix3x2fStack uses JOML methods directly - context.matrices.pushMatrix() - context.matrices.translate(pixelX.toFloat(), pixelY.toFloat()) context.matrices.scale(scale, scale) - context.drawItem(item.stack, 0, 0) - context.drawStackOverlay(textRenderer, item.stack, 0, 0) - context.matrices.popMatrix() - } else { - context.drawItem(item.stack, pixelX, pixelY) - context.drawStackOverlay(textRenderer, item.stack, pixelX, pixelY) } + context.drawItem(item.stack, 0, 0) + context.drawStackOverlay(textRenderer, item.stack, 0, 0) + context.matrices.popMatrix() } // Clear the queue after rendering diff --git a/src/main/kotlin/com/lambda/graphics/text/FontHandler.kt b/src/main/kotlin/com/lambda/graphics/text/FontHandler.kt index 477d703df..8c6561a16 100644 --- a/src/main/kotlin/com/lambda/graphics/text/FontHandler.kt +++ b/src/main/kotlin/com/lambda/graphics/text/FontHandler.kt @@ -38,7 +38,7 @@ object FontHandler { /** * Load an SDF font from resources. * - * @param path Resource path to TTF/OTF file (e.g., "fonts/FiraSans-Regular.ttf") + * @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 */ @@ -61,9 +61,9 @@ object FontHandler { fun getDefaultFont(size: Float = 128f): SDFFontAtlas { defaultFont?.let { return it } - val key = "fonts/FiraSans-Regular.ttf@$size" + val key = "fonts/MinecraftDefault-Regular.ttf@$size" val font = fonts[key] ?: run { - val newFont = SDFFontAtlas("fonts/FiraSans-Regular.ttf", size) + val newFont = SDFFontAtlas("fonts/MinecraftDefault-Regular.ttf", size) fonts[key] = newFont newFont } diff --git a/src/main/kotlin/com/lambda/graphics/text/SDFFontAtlas.kt b/src/main/kotlin/com/lambda/graphics/text/SDFFontAtlas.kt index a705c3a6a..b612ddf5d 100644 --- a/src/main/kotlin/com/lambda/graphics/text/SDFFontAtlas.kt +++ b/src/main/kotlin/com/lambda/graphics/text/SDFFontAtlas.kt @@ -17,6 +17,7 @@ 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 @@ -599,7 +600,7 @@ class SDFFontAtlas( fun getGlyph(codepoint: Int): Glyph? = glyphs[codepoint] - fun getStringWidth(text: String, fontSize: Float): Float { + private fun getStringWidth(text: String, fontSize: Float): Float { var width = 0f for (char in text) { val glyph = glyphs[char.code] ?: glyphs[' '.code] ?: continue @@ -608,6 +609,49 @@ class SDFFontAtlas( 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) + } + override fun close() { glTextureView?.close() glTextureView = null diff --git a/src/main/kotlin/com/lambda/graphics/util/DynamicAABB.kt b/src/main/kotlin/com/lambda/graphics/util/DynamicAABB.kt index c8f01e6b0..f7bd7a3a4 100644 --- a/src/main/kotlin/com/lambda/graphics/util/DynamicAABB.kt +++ b/src/main/kotlin/com/lambda/graphics/util/DynamicAABB.kt @@ -17,7 +17,9 @@ 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 @@ -49,6 +51,10 @@ class DynamicAABB { } 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/module/modules/render/Nametags.kt b/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt index 47fa00250..c2d72f209 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt @@ -17,12 +17,139 @@ package com.lambda.module.modules.render +import com.lambda.Lambda.mc +import com.lambda.event.events.RenderEvent +import com.lambda.event.events.ScreenRenderEvent +import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.graphics.RenderMain.worldToScreenNormalized +import com.lambda.graphics.mc.RenderBuilder +import com.lambda.graphics.mc.renderer.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.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.entity.Entity +import net.minecraft.entity.EquipmentSlot +import net.minecraft.entity.LivingEntity +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 val textScale by setting("Text Scale", 1.2f, 0.4f..5f, 0.01f) + private val itemScale by setting("Item Scale", 1.9f, 0.4f..5f, 0.01f) + private val yOffset by setting("Y Offset", 0.2, 0.0..1.0, 0.01) + private val spacing by setting("Spacing", 0, 0..10, 1) + private val color by setting("Color", Color.WHITE) + private val friendColor by setting("Friend Color", Color.BLUE) + private val self by setting("Self", false) + private val health by setting("Health", false) + private val gear by setting("Gear", true) + private val mainItem by setting("Main Item", true) { gear } + private val offhandItem by setting("Offhand Item", true) { gear } + private val enchantments by setting("Enchantments", false) { gear } + private val entities by setting("Entities", setOf(EntityUtils.EntityGroup.Player), EntityUtils.EntityGroup.entries) + + val renderer = ImmediateRenderer("Nametags") + + var heightWidthRatio = 0f + var trueTextScale = 0f + var trueItemScaleX = 0f + var trueItemScaleY = 0f + var trueSpacingX = 0f + var trueSpacingY = 0f + + init { + listen { + renderer.tick() + heightWidthRatio = mc.window.height / mc.window.width.toFloat() + trueTextScale = textScale * 0.01f + trueItemScaleY = itemScale * 0.01f + trueItemScaleX = trueItemScaleY * heightWidthRatio + trueSpacingY = spacing * 0.0005f + trueSpacingX = trueSpacingY * heightWidthRatio + + renderer.shapes { + world.entities + .sortedByDescending { it distSq mc.gameRenderer.camera.pos } + .forEach { entity -> + if (!shouldRenderNametag(entity)) return@forEach + val nameText = entity.displayName?.string ?: return@forEach + val box = entity.interpolatedBox + val boxCenter = box.center + val (anchorX, anchorY) = + worldToScreenNormalized(Vec3d(boxCenter.x, box.maxY + yOffset, boxCenter.z)) + ?: return@forEach + + if (entity is LivingEntity) { + val healthCount = if (health) entity.fullHealth else -1.0 + val nameWidth = getDefaultFont().getStringWidthNormalized(nameText, trueTextScale) + val healthText = if (health) " ${healthCount.roundToStep(0.01)}" else "" + val healthWidth = getDefaultFont().getStringWidthNormalized(healthText, trueTextScale) + var combinedWidth = nameWidth + healthWidth + if (healthCount >= 0) combinedWidth += trueSpacingX + val nameX = anchorX - (combinedWidth / 2) + screenText(nameText, nameX, anchorY, trueTextScale) + if (healthCount >= 0) { + val healthColor = lerp(entity.fullHealth / entity.maxFullHealth, Color.RED, Color.GREEN) + val healthStyle = RenderBuilder.SDFStyle(healthColor) + screenText(healthText, nameX + nameWidth + trueSpacingX, anchorY, trueTextScale, style = healthStyle) + } + if (gear) { + if (EquipmentSlot.entries.none { it.index in 1..4 && !entity.getEquippedStack(it).isEmpty }) { + if (mainItem && !entity.mainHandStack.isEmpty) + screenItem(entity.mainHandStack, nameX - trueItemScaleX - trueSpacingX, anchorY, trueItemScaleY) + if (offhandItem && !entity.offHandStack.isEmpty) + screenItem(entity.offHandStack, anchorX + (combinedWidth / 2) + trueSpacingX, anchorY, trueItemScaleY) + } else drawArmorAndItems(entity, anchorX, anchorY + trueTextScale + trueSpacingY) + } + } else screenText(nameText, anchorX, anchorY + (trueTextScale / 2f), trueTextScale, centered = true) + } + } + + renderer.upload() + renderer.render() + } + + listen { + renderer.renderScreen() + } + } + + 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) screenItem(entity.mainHandStack, iteratorX, y, trueItemScaleY) + 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) screenItem(headStack, iteratorX, y, trueItemScaleY) + iteratorX += stepAmount + if (!chestStack.isEmpty) screenItem(chestStack, iteratorX, y, trueItemScaleY) + iteratorX += stepAmount + if (!legsStack.isEmpty) screenItem(legsStack, iteratorX, y, trueItemScaleY) + iteratorX += stepAmount + if (!feetStack.isEmpty) screenItem(feetStack, iteratorX, y, trueItemScaleY) + iteratorX += stepAmount + if (offhandItem && !entity.offHandStack.isEmpty) screenItem(entity.offHandStack, iteratorX, y, trueItemScaleY) + } + + @JvmStatic + fun shouldRenderNametag(entity: Entity) = + entity.entityGroup in entities && (self || entity !== mc.player) && (entity !is LivingEntity || entity.isAlive) } \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/module/modules/render/Tracers.kt b/src/main/kotlin/com/lambda/module/modules/render/Tracers.kt index 0cff7a589..8cfc67c7f 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/Tracers.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/Tracers.kt @@ -46,7 +46,7 @@ object Tracers : Module( private val target by setting("Target", TracerMode.Feet) private val stem by setting("Stem", true) private val entities by setting("Entities", setOf(EntityGroup.Player, EntityGroup.Mob, EntityGroup.Boss), EntityGroup.entries) - private val friendColor by setting("Friend Color", Color.BLUE) + private val friendColor by setting("Friend Color", Color(80, 80, 255, 255)) private val playerDistanceGradient by setting("Player Distance Gradient", true) { EntityGroup.Player in entities } private val playerDistanceColorFar by setting("Player Far Color", Color.GREEN) { EntityGroup.Player in entities && playerDistanceGradient } private val playerDistanceColorClose by setting("Player Close Color", Color.RED) { EntityGroup.Player in entities && playerDistanceGradient } 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) { From 8708ad3db3ae0d4e443f2e808ee85c72e4235c7d Mon Sep 17 00:00:00 2001 From: beanbag44 <107891830+beanbag44@users.noreply.github.com> Date: Fri, 23 Jan 2026 15:56:03 +0000 Subject: [PATCH 21/26] nametags improvements --- .../com/lambda/graphics/text/SDFFontAtlas.kt | 28 +++++++ .../lambda/module/modules/render/Nametags.kt | 74 +++++++++++++++---- 2 files changed, 87 insertions(+), 15 deletions(-) diff --git a/src/main/kotlin/com/lambda/graphics/text/SDFFontAtlas.kt b/src/main/kotlin/com/lambda/graphics/text/SDFFontAtlas.kt index b612ddf5d..dbb7464ae 100644 --- a/src/main/kotlin/com/lambda/graphics/text/SDFFontAtlas.kt +++ b/src/main/kotlin/com/lambda/graphics/text/SDFFontAtlas.kt @@ -652,6 +652,34 @@ class SDFFontAtlas( 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 diff --git a/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt b/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt index c2d72f209..c3909088a 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt @@ -30,6 +30,7 @@ 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 @@ -38,17 +39,22 @@ import com.lambda.util.math.lerp 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 { + + } + private val textScale by setting("Text Scale", 1.2f, 0.4f..5f, 0.01f) private val itemScale by setting("Item Scale", 1.9f, 0.4f..5f, 0.01f) private val yOffset by setting("Y Offset", 0.2, 0.0..1.0, 0.01) @@ -57,10 +63,15 @@ object Nametags : Module( private val friendColor by setting("Friend Color", Color.BLUE) private val self by setting("Self", false) private val health by setting("Health", false) + private val ping by setting("Ping", true) private val gear by setting("Gear", true) private val mainItem by setting("Main Item", true) { gear } + private val itemName by setting("Item Name", true) + private val itemNameScale by setting("Item Name Scale", 0.7f, 0.1f..1.0f, 0.01f) private val offhandItem by setting("Offhand Item", true) { gear } - private val enchantments by setting("Enchantments", false) { gear } + private val durability by setting("Durability", true) { gear } + //ToDo: Implement +// private val enchantments by setting("Enchantments", false) { gear } private val entities by setting("Entities", setOf(EntityUtils.EntityGroup.Player), EntityUtils.EntityGroup.entries) val renderer = ImmediateRenderer("Nametags") @@ -95,25 +106,45 @@ object Nametags : Module( ?: return@forEach if (entity is LivingEntity) { - val healthCount = if (health) entity.fullHealth else -1.0 + if (itemName && !entity.mainHandStack.isEmpty) { + val itemNameText = entity.mainHandStack.name.string + val itemNameScale = trueTextScale * itemNameScale + screenText(itemNameText, anchorX, anchorY - (itemNameScale * 1.1f) - trueSpacingY, itemNameScale, centered = true) + } + val nameWidth = getDefaultFont().getStringWidthNormalized(nameText, trueTextScale) + + val healthCount = if (health) entity.fullHealth else -1.0 val healthText = if (health) " ${healthCount.roundToStep(0.01)}" else "" - val healthWidth = getDefaultFont().getStringWidthNormalized(healthText, trueTextScale) - var combinedWidth = nameWidth + healthWidth - if (healthCount >= 0) combinedWidth += trueSpacingX + val healthWidth = + getDefaultFont().getStringWidthNormalized(healthText, trueTextScale) + .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, trueTextScale) + .let { if (pingCount > 0 ) it + trueSpacingX else it } + + var combinedWidth = nameWidth + healthWidth + pingWidth val nameX = anchorX - (combinedWidth / 2) screenText(nameText, nameX, anchorY, trueTextScale) if (healthCount >= 0) { - val healthColor = lerp(entity.fullHealth / entity.maxFullHealth, Color.RED, Color.GREEN) + val healthColor = lerp(entity.fullHealth / entity.maxFullHealth, Color.RED, Color.GREEN).brighter() val healthStyle = RenderBuilder.SDFStyle(healthColor) screenText(healthText, nameX + nameWidth + trueSpacingX, anchorY, trueTextScale, style = healthStyle) } + if (pingCount >= 0) { + val pingColor = lerp(pingCount / 500.0, Color.GREEN, Color.RED).brighter() + val pingStyle = RenderBuilder.SDFStyle(pingColor) + screenText(pingText, nameX + nameWidth + healthWidth + trueSpacingX, anchorY, trueTextScale, style = pingStyle) + } if (gear) { if (EquipmentSlot.entries.none { it.index in 1..4 && !entity.getEquippedStack(it).isEmpty }) { if (mainItem && !entity.mainHandStack.isEmpty) - screenItem(entity.mainHandStack, nameX - trueItemScaleX - trueSpacingX, anchorY, trueItemScaleY) + renderItem(entity.mainHandStack, nameX - trueItemScaleX - trueSpacingX - (trueItemScaleX * 0.1f), anchorY) if (offhandItem && !entity.offHandStack.isEmpty) - screenItem(entity.offHandStack, anchorX + (combinedWidth / 2) + trueSpacingX, anchorY, trueItemScaleY) + renderItem(entity.offHandStack, anchorX + (combinedWidth / 2) + trueSpacingX, anchorY) } else drawArmorAndItems(entity, anchorX, anchorY + trueTextScale + trueSpacingY) } } else screenText(nameText, anchorX, anchorY + (trueTextScale / 2f), trueTextScale, centered = true) @@ -132,21 +163,34 @@ object Nametags : Module( 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) screenItem(entity.mainHandStack, iteratorX, y, trueItemScaleY) + if (mainItem && !entity.mainHandStack.isEmpty) renderItem(entity.mainHandStack, iteratorX - (trueItemScaleX * 0.1f), 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) screenItem(headStack, iteratorX, y, trueItemScaleY) + if (!headStack.isEmpty) renderItem(headStack, iteratorX, y) iteratorX += stepAmount - if (!chestStack.isEmpty) screenItem(chestStack, iteratorX, y, trueItemScaleY) + if (!chestStack.isEmpty) renderItem(chestStack, iteratorX, y) iteratorX += stepAmount - if (!legsStack.isEmpty) screenItem(legsStack, iteratorX, y, trueItemScaleY) + if (!legsStack.isEmpty) renderItem(legsStack, iteratorX, y) iteratorX += stepAmount - if (!feetStack.isEmpty) screenItem(feetStack, iteratorX, y, trueItemScaleY) + if (!feetStack.isEmpty) renderItem(feetStack, iteratorX, y) iteratorX += stepAmount - if (offhandItem && !entity.offHandStack.isEmpty) screenItem(entity.offHandStack, iteratorX, y, trueItemScaleY) + if (offhandItem && !entity.offHandStack.isEmpty) renderItem(entity.offHandStack, iteratorX, y) + } + + private fun RenderBuilder.renderItem(stack: ItemStack, x: Float, y: Float) { + screenItem(stack, x, y, trueItemScaleY) + var iteratorY = y + iteratorY += trueItemScaleY + if (durability && stack.isDamageable) { + val dura = (1 - (stack.damage / stack.maxDamage.toDouble())).roundToStep(0.01) * 100 + val duraText = "$dura%" + val textSize = getDefaultFont().getSizeForWidthNormalized(duraText, trueItemScaleX) * 0.9f + val textStyle = RenderBuilder.SDFStyle(lerp(dura / 100, Color.RED, Color.GREEN).brighter()) + screenText(duraText, x, iteratorY, textSize, style = textStyle) + } } @JvmStatic From d3653b071bfe7fadcd4a93b79704cb58adc737ca Mon Sep 17 00:00:00 2001 From: beanbag44 <107891830+beanbag44@users.noreply.github.com> Date: Fri, 23 Jan 2026 20:23:28 +0000 Subject: [PATCH 22/26] fix world lines not rotating to face the camera --- .../lambda/module/modules/render/Nametags.kt | 90 ++++++++++--------- .../lambda/shaders/core/advanced_lines.vsh | 5 +- 2 files changed, 50 insertions(+), 45 deletions(-) diff --git a/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt b/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt index c3909088a..8eb9a8e13 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt @@ -105,49 +105,53 @@ object Nametags : Module( worldToScreenNormalized(Vec3d(boxCenter.x, box.maxY + yOffset, boxCenter.z)) ?: return@forEach - if (entity is LivingEntity) { - if (itemName && !entity.mainHandStack.isEmpty) { - val itemNameText = entity.mainHandStack.name.string - val itemNameScale = trueTextScale * itemNameScale - screenText(itemNameText, anchorX, anchorY - (itemNameScale * 1.1f) - trueSpacingY, itemNameScale, centered = true) - } - - val nameWidth = getDefaultFont().getStringWidthNormalized(nameText, trueTextScale) - - val healthCount = if (health) entity.fullHealth else -1.0 - val healthText = if (health) " ${healthCount.roundToStep(0.01)}" else "" - val healthWidth = - getDefaultFont().getStringWidthNormalized(healthText, trueTextScale) - .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, trueTextScale) - .let { if (pingCount > 0 ) it + trueSpacingX else it } - - var combinedWidth = nameWidth + healthWidth + pingWidth - val nameX = anchorX - (combinedWidth / 2) - screenText(nameText, nameX, anchorY, trueTextScale) - if (healthCount >= 0) { - val healthColor = lerp(entity.fullHealth / entity.maxFullHealth, Color.RED, Color.GREEN).brighter() - val healthStyle = RenderBuilder.SDFStyle(healthColor) - screenText(healthText, nameX + nameWidth + trueSpacingX, anchorY, trueTextScale, style = healthStyle) - } - if (pingCount >= 0) { - val pingColor = lerp(pingCount / 500.0, Color.GREEN, Color.RED).brighter() - val pingStyle = RenderBuilder.SDFStyle(pingColor) - screenText(pingText, nameX + nameWidth + healthWidth + trueSpacingX, anchorY, trueTextScale, style = pingStyle) - } - if (gear) { - 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 + trueTextScale + trueSpacingY) - } - } else screenText(nameText, anchorX, anchorY + (trueTextScale / 2f), trueTextScale, centered = true) + if (entity !is LivingEntity) { + screenText(nameText, anchorX, anchorY + (trueTextScale / 2f), trueTextScale, centered = true) + return@forEach + } + + if (itemName && !entity.mainHandStack.isEmpty) { + val itemNameText = entity.mainHandStack.name.string + val itemNameScale = trueTextScale * itemNameScale + screenText(itemNameText, anchorX, anchorY - (itemNameScale * 1.1f) - trueSpacingY, itemNameScale, centered = true) + } + + val nameWidth = getDefaultFont().getStringWidthNormalized(nameText, trueTextScale) + + val healthCount = if (health) entity.fullHealth else -1.0 + val healthText = if (health) " ${healthCount.roundToStep(0.01)}" else "" + val healthWidth = + getDefaultFont().getStringWidthNormalized(healthText, trueTextScale) + .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, trueTextScale) + .let { if (pingCount > 0 ) it + trueSpacingX else it } + + var combinedWidth = nameWidth + healthWidth + pingWidth + val nameX = anchorX - (combinedWidth / 2) + screenText(nameText, nameX, anchorY, trueTextScale) + if (healthCount >= 0) { + val healthColor = lerp(entity.fullHealth / entity.maxFullHealth, Color.RED, Color.GREEN).brighter() + val healthStyle = RenderBuilder.SDFStyle(healthColor) + screenText(healthText, nameX + nameWidth + trueSpacingX, anchorY, trueTextScale, style = healthStyle) + } + if (pingCount >= 0) { + val pingColor = lerp(pingCount / 500.0, Color.GREEN, Color.RED).brighter() + val pingStyle = RenderBuilder.SDFStyle(pingColor) + screenText(pingText, nameX + nameWidth + healthWidth + trueSpacingX, anchorY, trueTextScale, style = pingStyle) + } + + 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 + trueTextScale + trueSpacingY) } } diff --git a/src/main/resources/assets/lambda/shaders/core/advanced_lines.vsh b/src/main/resources/assets/lambda/shaders/core/advanced_lines.vsh index 2bc885aac..dea393834 100644 --- a/src/main/resources/assets/lambda/shaders/core/advanced_lines.vsh +++ b/src/main/resources/assets/lambda/shaders/core/advanced_lines.vsh @@ -43,8 +43,9 @@ void main() { vec3 lineEnd = lineCenter + lineDir * (segmentLength * 0.5); vec3 thisPoint = isStart ? lineStart : lineEnd; - // Billboard direction - vec3 toCamera = normalize(-lineCenter); + // Billboard direction: extract camera forward from ModelViewMat + // ModelViewMat is the view matrix, its third row gives the camera's forward direction in world space + vec3 toCamera = vec3(ModelViewMat[0][2], ModelViewMat[1][2], ModelViewMat[2][2]); vec3 perpDir = cross(lineDir, toCamera); if (length(perpDir) < 0.001) { perpDir = cross(lineDir, vec3(0.0, 1.0, 0.0)); From 7ee3e585fb990b623ef1a098d7a162655e8143ba Mon Sep 17 00:00:00 2001 From: beanbag44 <107891830+beanbag44@users.noreply.github.com> Date: Tue, 27 Jan 2026 23:55:15 +0000 Subject: [PATCH 23/26] merge bug --- src/main/kotlin/com/lambda/config/groups/BreakSettings.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/lambda/config/groups/BreakSettings.kt b/src/main/kotlin/com/lambda/config/groups/BreakSettings.kt index dd6fadb2f..57888bd9d 100644 --- a/src/main/kotlin/com/lambda/config/groups/BreakSettings.kt +++ b/src/main/kotlin/com/lambda/config/groups/BreakSettings.kt @@ -109,7 +109,8 @@ open class BreakSettings( // 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", 2f, 0f..10f, 0.1f, "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 outlineWidth by c.setting("Outline Width", 2f, 0f..10f, 0.1f, "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() From f762d5f26b8170d7428a34842e9702da88ab35a5 Mon Sep 17 00:00:00 2001 From: beanbag44 <107891830+beanbag44@users.noreply.github.com> Date: Wed, 28 Jan 2026 00:24:29 +0000 Subject: [PATCH 24/26] threaded font atlas generation --- .../com/lambda/mixin/entity/EntityMixin.java | 3 +- .../com/lambda/graphics/text/SDFFontAtlas.kt | 64 +++++++++++++++++-- .../lambda/module/modules/render/Nametags.kt | 1 + 3 files changed, 62 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/lambda/mixin/entity/EntityMixin.java b/src/main/java/com/lambda/mixin/entity/EntityMixin.java index 0237570b7..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,7 +152,7 @@ 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 != Lambda.getMc().player) return true; + if ((Object) this != getMc().player) return true; return RotationManager.getLockYaw() == null; } diff --git a/src/main/kotlin/com/lambda/graphics/text/SDFFontAtlas.kt b/src/main/kotlin/com/lambda/graphics/text/SDFFontAtlas.kt index dbb7464ae..786184b61 100644 --- a/src/main/kotlin/com/lambda/graphics/text/SDFFontAtlas.kt +++ b/src/main/kotlin/com/lambda/graphics/text/SDFFontAtlas.kt @@ -44,6 +44,9 @@ 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. @@ -76,6 +79,22 @@ class SDFFontAtlas( 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 @@ -131,6 +150,12 @@ class SDFFontAtlas( 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 @@ -138,7 +163,9 @@ class SDFFontAtlas( 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) @@ -170,11 +197,7 @@ class SDFFontAtlas( break } - if (glyphW > 0 && glyphH > 0) { - generateGlyphSDF(glyphIndex, data, penX, penY, paddedW, paddedH, glyphW, glyphH) - } - - glyphs[cp] = Glyph( + val glyph = Glyph( codepoint = cp, width = paddedW, height = paddedH, @@ -187,10 +210,41 @@ class SDFFontAtlas( 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 + ) + } + } + } } /** diff --git a/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt b/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt index 8eb9a8e13..9c2867a89 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt @@ -46,6 +46,7 @@ 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", From f05599cd92de8fb838b9fda1f759423e597d1581 Mon Sep 17 00:00:00 2001 From: beanbag44 <107891830+beanbag44@users.noreply.github.com> Date: Sat, 31 Jan 2026 00:40:11 +0000 Subject: [PATCH 25/26] image rendering --- .../graphics/mc/LambdaRenderPipelines.kt | 78 +++- .../lambda/graphics/mc/LambdaVertexFormats.kt | 58 +++ .../com/lambda/graphics/mc/RegionRenderer.kt | 117 ++++- .../graphics/mc/RegionVertexCollector.kt | 235 ++++++++++- .../com/lambda/graphics/mc/RenderBuilder.kt | 249 +++++++++++ .../graphics/mc/renderer/AbstractRenderer.kt | 58 ++- .../graphics/mc/renderer/ChunkedRenderer.kt | 6 +- .../graphics/mc/renderer/ImmediateRenderer.kt | 4 +- .../graphics/mc/renderer/RendererUtils.kt | 202 ++++++++- .../graphics/mc/renderer/TickedRenderer.kt | 3 +- .../graphics/texture/LambdaImageAtlas.kt | 399 ++++++++++++++++++ .../modules/debug/RendererTestModule.kt | 256 ++++++++--- .../lambda/module/modules/render/Nametags.kt | 5 +- .../lambda/shaders/core/advanced_lines.vsh | 15 +- .../lambda/shaders/core/screen_image.fsh | 57 +++ .../lambda/shaders/core/screen_image.vsh | 28 ++ .../assets/lambda/shaders/core/sdf_text.vsh | 18 + .../lambda/shaders/core/world_image.fsh | 53 +++ .../lambda/shaders/core/world_image.vsh | 50 +++ 19 files changed, 1782 insertions(+), 109 deletions(-) create mode 100644 src/main/kotlin/com/lambda/graphics/texture/LambdaImageAtlas.kt create mode 100644 src/main/resources/assets/lambda/shaders/core/screen_image.fsh create mode 100644 src/main/resources/assets/lambda/shaders/core/screen_image.vsh create mode 100644 src/main/resources/assets/lambda/shaders/core/world_image.fsh create mode 100644 src/main/resources/assets/lambda/shaders/core/world_image.vsh diff --git a/src/main/kotlin/com/lambda/graphics/mc/LambdaRenderPipelines.kt b/src/main/kotlin/com/lambda/graphics/mc/LambdaRenderPipelines.kt index 0ccc14b06..d72764f21 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/LambdaRenderPipelines.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/LambdaRenderPipelines.kt @@ -51,7 +51,7 @@ object LambdaRenderPipelines : Loadable { .withVertexShader(Identifier.of("lambda", "core/advanced_lines")) .withFragmentShader(Identifier.of("lambda", "core/advanced_lines")) .withBlend(BlendFunction.TRANSLUCENT) - .withDepthWrite(false) + .withDepthWrite(false) // No depth write for proper transparency blending .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST) .withCull(false) .withVertexFormat( @@ -90,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( @@ -171,7 +171,7 @@ object LambdaRenderPipelines : Loadable { .withFragmentShader(Identifier.of("lambda", "core/sdf_text")) .withSampler("Sampler0") .withBlend(BlendFunction.TRANSLUCENT) - .withDepthWrite(false) + .withDepthWrite(false) // No depth write for proper transparency blending .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST) .withCull(false) .withVertexFormat( @@ -269,5 +269,77 @@ object LambdaRenderPipelines : Loadable { ) .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() + ) } diff --git a/src/main/kotlin/com/lambda/graphics/mc/LambdaVertexFormats.kt b/src/main/kotlin/com/lambda/graphics/mc/LambdaVertexFormats.kt index 02eabfe63..78a6b9d49 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/LambdaVertexFormats.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/LambdaVertexFormats.kt @@ -250,5 +250,63 @@ object LambdaVertexFormats { .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 (as vec3 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, + 3 // count (overlayU, overlayV, hasOverlay) + ) + + /** + * 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 + 12 + 4 = 40 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: vec3(overlayU, overlayV, hasOverlay) for glint effect (3 floats = 12 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 + 12 = 56 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: vec3(overlayU, overlayV, hasOverlay) (3 floats = 12 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() } diff --git a/src/main/kotlin/com/lambda/graphics/mc/RegionRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/RegionRenderer.kt index 6cc03b68f..a980f3b19 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 @@ -41,6 +42,10 @@ class RegionRenderer { 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() + // Index counts for world-space draw calls private var faceIndexCount = 0 private var edgeIndexCount = 0 @@ -95,8 +100,16 @@ class RegionRenderer { screenTextVertexBuffer = screenResult.text?.buffer screenTextIndexCount = screenResult.text?.indexCount ?: 0 - hasData = faceVertexBuffer != null || edgeVertexBuffer != null || textVertexBuffer != null - hasScreenData = screenFaceVertexBuffer != null || screenEdgeVertexBuffer != null || screenTextVertexBuffer != null + // Clean up old image batches + screenImageBatches.forEach { it.buffer.close() } + worldImageBatches.forEach { it.buffer.close() } + + // Store new image batches + screenImageBatches = screenResult.images + worldImageBatches = collector.uploadWorldImageBatches() + + hasData = faceVertexBuffer != null || edgeVertexBuffer != null || textVertexBuffer != null || worldImageBatches.isNotEmpty() + hasScreenData = screenFaceVertexBuffer != null || screenEdgeVertexBuffer != null || screenTextVertexBuffer != null || screenImageBatches.isNotEmpty() } /** @@ -216,6 +229,58 @@ class RegionRenderer { /** 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) + } + } + + /** Check if this renderer has screen-space image data. */ + fun hasScreenImageData(): Boolean = screenImageBatches.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() + /** Check if this renderer has any screen-space data to render. */ fun hasScreenData(): Boolean = hasScreenData @@ -243,6 +308,13 @@ class RegionRenderer { screenFaceIndexCount = 0 screenEdgeIndexCount = 0 screenTextIndexCount = 0 + + // Clear image batches + screenImageBatches.forEach { it.buffer.close() } + worldImageBatches.forEach { it.buffer.close() } + screenImageBatches = emptyList() + worldImageBatches = emptyList() + hasScreenData = false } @@ -255,19 +327,29 @@ class RegionRenderer { } 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, useDepth = true) + return createRenderPass(label, useMcDepth = true) } /** - * Helper to create a render pass targeting the main framebuffer. + * Helper to create a render pass for world-space rendering. * @param label Debug label for the render pass - * @param useDepth Whether to attach the depth buffer for depth testing + * @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, useDepth: Boolean): RenderPass? { + fun createRenderPass(label: String, useMcDepth: Boolean): RenderPass? { val framebuffer = mc.framebuffer ?: return null - val depthView = if (useDepth) framebuffer.depthAttachmentView else 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( @@ -279,6 +361,25 @@ class RegionRenderer { ) } + /** + * 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() + ) + } + /** * Render a custom vertex buffer using quads mode. * Used for styled text rendering where each style has its own buffer. diff --git a/src/main/kotlin/com/lambda/graphics/mc/RegionVertexCollector.kt b/src/main/kotlin/com/lambda/graphics/mc/RegionVertexCollector.kt index 788f642c4..083f86e01 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/RegionVertexCollector.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/RegionVertexCollector.kt @@ -19,6 +19,7 @@ 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 net.minecraft.client.render.BufferBuilder import net.minecraft.client.render.VertexFormats @@ -160,6 +161,104 @@ class RegionVertexCollector { 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 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 + ) + + /** + * 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>() + + /** + * 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 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)) @@ -612,17 +711,145 @@ class RegionVertexCollector { /** * Upload screen-space data to GPU buffers. * - * @return ScreenUploadResult containing screen-space face, edge, and text buffers + * @return ScreenUploadResult containing screen-space face, edge, text, and image buffers */ fun uploadScreen(): ScreenUploadResult { val faces = uploadScreenFaces() val edges = uploadScreenEdges() val text = uploadScreenText() - return ScreenUploadResult(faces, edges, text) + val images = uploadScreenImageBatches() + return ScreenUploadResult(faces, edges, text, images) + } + + /** + * 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) + 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) + } + + // 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) + 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) + } + } + + 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 } data class BufferResult(val buffer: GpuBuffer?, val indexCount: Int) - data class UploadResult(val faces: BufferResult?, val edges: BufferResult?, val text: BufferResult? = null) - data class ScreenUploadResult(val faces: BufferResult?, val edges: BufferResult?, val text: BufferResult? = null) + data class UploadResult(val faces: BufferResult?, val edges: BufferResult?, val text: BufferResult? = null, val images: List = emptyList()) + data class ScreenUploadResult(val faces: BufferResult?, val edges: BufferResult?, val text: BufferResult? = null, val images: List = emptyList()) } diff --git a/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt b/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt index 90bf87ff6..63132ad07 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt @@ -21,11 +21,13 @@ 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.item.ItemStack +import net.minecraft.util.Identifier import net.minecraft.util.math.BlockPos import net.minecraft.util.math.Box import net.minecraft.util.math.Vec3d @@ -551,6 +553,253 @@ class RenderBuilder(private val cameraPos: Vec3d) { deferredItems.add(ScreenItemRender(stack, x, y, size)) } + // ============================================================================ + // 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, layer + ), + RegionVertexCollector.ScreenImageVertex( + x1, y0, u1, v1, tint.red, tint.green, tint.blue, tint.alpha, + glintTime, aspectRatio, overlayFlag, layer + ), + RegionVertexCollector.ScreenImageVertex( + x1, y1, u1, v0, tint.red, tint.green, tint.blue, tint.alpha, + glintTime, aspectRatio, overlayFlag, layer + ), + RegionVertexCollector.ScreenImageVertex( + x0, y1, u0, v0, tint.red, tint.green, tint.blue, tint.alpha, + glintTime, aspectRatio, overlayFlag, layer + ) + ) + collector.addScreenImageVertices(image.textureView, vertices, pixelPerfect) + } + + /** + * 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 + + // 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 + val glintAspectRatio = if (hasOverlay) ratio else 1f + + // 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, + glintTime, glintAspectRatio, overlayFlag + ), + RegionVertexCollector.WorldImageVertex( + x1, y0, u1, v1, tint.red, tint.green, tint.blue, tint.alpha, + anchorX, anchorY, anchorZ, size, billboardFlag, + glintTime, glintAspectRatio, overlayFlag + ), + RegionVertexCollector.WorldImageVertex( + x1, y1, u1, v0, tint.red, tint.green, tint.blue, tint.alpha, + anchorX, anchorY, anchorZ, size, billboardFlag, + glintTime, glintAspectRatio, overlayFlag + ), + RegionVertexCollector.WorldImageVertex( + x0, y1, u0, v0, tint.red, tint.green, tint.blue, tint.alpha, + anchorX, anchorY, anchorZ, size, billboardFlag, + glintTime, glintAspectRatio, overlayFlag + ) + ) + } 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, + glintTime, glintAspectRatio, overlayFlag + ), + RegionVertexCollector.WorldImageVertex( + p1.x, p1.y, u1, v1, tint.red, tint.green, tint.blue, tint.alpha, + anchorX, anchorY, anchorZ, size, billboardFlag, + glintTime, glintAspectRatio, overlayFlag + ), + RegionVertexCollector.WorldImageVertex( + p2.x, p2.y, u1, v0, tint.red, tint.green, tint.blue, tint.alpha, + anchorX, anchorY, anchorZ, size, billboardFlag, + glintTime, glintAspectRatio, overlayFlag + ), + RegionVertexCollector.WorldImageVertex( + p3.x, p3.y, u0, v0, tint.red, tint.green, tint.blue, tint.alpha, + anchorX, anchorY, anchorZ, size, billboardFlag, + glintTime, glintAspectRatio, overlayFlag + ) + ) + } + 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. diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/AbstractRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/AbstractRenderer.kt index 4cdc551a7..4918601f3 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/renderer/AbstractRenderer.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/AbstractRenderer.kt @@ -66,6 +66,12 @@ abstract class AbstractRenderer(val name: String, var depthTest: Boolean = false val chunks = getRendererTransforms() if (chunks.isEmpty()) return + // When using xray mode (depthTest=false), clear our custom depth buffer + // This gives us correct self-ordering while showing through MC's world + if (!depthTest) { + RendererUtils.clearXrayDepthBuffer() + } + // Render Faces RegionRenderer.createRenderPass("$name Faces", depthTest)?.use { pass -> pass.setPipeline(RendererUtils.getFacesPipeline(depthTest)) @@ -105,6 +111,28 @@ abstract class AbstractRenderer(val name: String, var depthTest: Boolean = false } } } + + // 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", depthTest)?.use { pass -> + pass.setPipeline(RendererUtils.getWorldImagePipeline(depthTest)) + 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) + } + } + } } /** @@ -120,8 +148,8 @@ abstract class AbstractRenderer(val name: String, var depthTest: Boolean = false RendererUtils.withScreenContext { val dynamicTransform = RendererUtils.createScreenDynamicTransform() - // Render Screen Faces (no depth test for 2D) - RegionRenderer.createRenderPass("$name Screen Faces", false)?.use { pass -> + // Render Screen Faces (no depth test - painter's algorithm) + RegionRenderer.createScreenRenderPass("$name Screen Faces")?.use { pass -> pass.setPipeline(RendererUtils.screenFacesPipeline) RenderSystem.bindDefaultUniforms(pass) pass.setUniform("DynamicTransforms", dynamicTransform) @@ -129,7 +157,7 @@ abstract class AbstractRenderer(val name: String, var depthTest: Boolean = false } // Render Screen Edges - RegionRenderer.createRenderPass("$name Screen Edges", false)?.use { pass -> + RegionRenderer.createScreenRenderPass("$name Screen Edges")?.use { pass -> pass.setPipeline(RendererUtils.screenEdgesPipeline) RenderSystem.bindDefaultUniforms(pass) pass.setUniform("DynamicTransforms", dynamicTransform) @@ -144,7 +172,7 @@ abstract class AbstractRenderer(val name: String, var depthTest: Boolean = false val textureView = atlas.textureView val sampler = atlas.sampler if (textureView != null && sampler != null) { - RegionRenderer.createRenderPass("$name Screen Text", false)?.use { pass -> + RegionRenderer.createScreenRenderPass("$name Screen Text")?.use { pass -> pass.setPipeline(RendererUtils.screenTextPipeline) RenderSystem.bindDefaultUniforms(pass) pass.setUniform("DynamicTransforms", dynamicTransform) @@ -153,6 +181,28 @@ abstract class AbstractRenderer(val name: String, var depthTest: Boolean = false } } } + + // 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() + + RegionRenderer.createScreenRenderPass("$name Screen Images")?.use { pass -> + pass.setPipeline(RendererUtils.getScreenImagePipeline()) + 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) } + } + } } // Render deferred items last (uses Minecraft's DrawContext pipeline) diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt index 810f1088f..894466893 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt @@ -121,6 +121,7 @@ class ChunkedRenderer( /** * 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() @@ -129,6 +130,9 @@ class ChunkedRenderer( 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 @@ -138,7 +142,7 @@ class ChunkedRenderer( val modelView = Matrix4f(modelViewMatrix).translate(offsetX, offsetY, offsetZ) val dynamicTransform = RenderSystem.getDynamicUniforms() - .write(modelView, Vector4f(1f, 1f, 1f, 1f), Vector3f(0f, 0f, 0f), Matrix4f()) + .write(modelView, Vector4f(1f, 1f, 1f, 1f), Vector3f(0f, 0f, 0f), glintMatrix) chunkData.renderer to dynamicTransform } diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt index e8ff72832..85071224e 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt @@ -91,7 +91,7 @@ class ImmediateRenderer(name: String, depthTest: Boolean = false) : AbstractRend /** * Get renderer/transform pairs for world-space rendering. - * Returns single renderer with identity-based transform (camera-relative coords). + * Returns single renderer with camera-relative transform and fresh glint TextureMat. */ override fun getRendererTransforms(): List> { if (!renderer.hasData()) return emptyList() @@ -102,7 +102,7 @@ class ImmediateRenderer(name: String, depthTest: Boolean = false) : AbstractRend modelViewMatrix, Vector4f(1f, 1f, 1f, 1f), Vector3f(0f, 0f, 0f), - Matrix4f() + RendererUtils.createGlintTransform(0.25f) // Fresh glint matrix for world images ) return listOf(renderer to dynamicTransform) diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/RendererUtils.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/RendererUtils.kt index 51312e80c..6a51f7824 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/renderer/RendererUtils.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/RendererUtils.kt @@ -23,6 +23,7 @@ import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafe 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 @@ -58,6 +59,63 @@ object RendererUtils { ) } + /** + * Create a dynamic transform with glint texture matrix for animated enchantment effect. + * Used for screen image rendering with overlay support. + * Note: We use a smaller scale (0.25) compared to vanilla (8.0) because our UVs are + * normalized 0-1, not model-based coordinates. + */ + fun createScreenDynamicTransformWithGlint(): GpuBufferSlice { + val identityMatrix = Matrix4f() + return RenderSystem.getDynamicUniforms() + .write( + identityMatrix, + Vector4f(1f, 1f, 1f, 1f), + Vector3f(0f, 0f, 0f), + createGlintTransform(0.25f) // Smaller scale for screen-space (larger pattern) + ) + } + + /** + * 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 // Default to 0.5 if options not available + val time = (net.minecraft.util.Util.getMeasuringTimeMs() * glintSpeed * 8.0).toLong() + + // Calculate scroll offsets (0-1 range) + val scrollX = (time % 110000L) / 110000.0f // X cycle: 110 seconds + val scrollY = (time % 30000L) / 30000.0f // Y cycle: 30 seconds + + // Build matrix: translation(-f, g, 0) -> rotateZ(π/18) -> scale + val matrix = Matrix4f() + matrix.translation(-scrollX, scrollY, 0f) + matrix.rotateZ((Math.PI / 18.0).toFloat()) // 10 degrees + matrix.scale(scale) + + return matrix + } + + /** + * Create a dynamic transform with glint texture matrix for world-space image rendering. + * Uses the current model-view matrix from RenderSystem for proper camera alignment. + * Note: We use a smaller scale (0.25) compared to vanilla because our UVs are + * normalized 0-1, not model-based coordinates. + */ + fun createWorldDynamicTransformWithGlint(): GpuBufferSlice { + return RenderSystem.getDynamicUniforms() + .write( + RenderSystem.getModelViewMatrix(), + Vector4f(1f, 1f, 1f, 1f), + Vector3f(0f, 0f, 0f), + createGlintTransform(0.25f) // Smaller scale for normalized UVs (larger pattern) + ) + } + /** * Execute a block with screen-space rendering context. * Sets up orthographic projection and identity model-view, then restores state after. @@ -93,20 +151,24 @@ object RendererUtils { // Pipeline Helpers // ============================================================================ - /** Get the face/quad pipeline based on depth test setting. */ - fun getFacesPipeline(depthTest: Boolean): RenderPipeline = - if (depthTest) LambdaRenderPipelines.ESP_QUADS - else LambdaRenderPipelines.ESP_QUADS_THROUGH + /** + * 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 based on depth test setting. */ - fun getEdgesPipeline(depthTest: Boolean): RenderPipeline = - if (depthTest) LambdaRenderPipelines.ESP_LINES - else LambdaRenderPipelines.ESP_LINES_THROUGH + /** + * 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 based on depth test setting. */ - fun getTextPipeline(depthTest: Boolean): RenderPipeline = - if (depthTest) LambdaRenderPipelines.SDF_TEXT - else LambdaRenderPipelines.SDF_TEXT_THROUGH + /** + * Get the SDF text pipeline. + * Always uses depth testing for proper self-ordering. + */ + fun getTextPipeline(depthTest: Boolean): RenderPipeline = LambdaRenderPipelines.SDF_TEXT /** Screen-space faces pipeline (with layer-based depth for draw order). */ val screenFacesPipeline: RenderPipeline get() = LambdaRenderPipelines.SCREEN_FACES @@ -117,6 +179,122 @@ object RendererUtils { /** Screen-space text pipeline. */ val screenTextPipeline: RenderPipeline get() = LambdaRenderPipelines.SCREEN_TEXT + /** Get the screen-space image pipeline. */ + fun getScreenImagePipeline(): 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 + + // 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 + glintSampler = RenderSystem.getSamplerCache().get(com.mojang.blaze3d.textures.FilterMode.LINEAR) + glintTextureLoaded = true + } + } + + /** + * 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) + } + + // ============================================================================ + // 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 + } + // ============================================================================ // Deferred Item Rendering // ============================================================================ diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.kt index 23641e59a..5c9b5f0eb 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.kt @@ -87,6 +87,7 @@ class TickedRenderer(name: String, depthTest: Boolean = false) : AbstractRendere /** * 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() @@ -103,7 +104,7 @@ class TickedRenderer(name: String, depthTest: Boolean = false) : AbstractRendere val modelView = Matrix4f(modelViewMatrix).translate(deltaX, deltaY, deltaZ) val dynamicTransform = RenderSystem.getDynamicUniforms() - .write(modelView, Vector4f(1f, 1f, 1f, 1f), Vector3f(0f, 0f, 0f), Matrix4f()) + .write(modelView, Vector4f(1f, 1f, 1f, 1f), Vector3f(0f, 0f, 0f), RendererUtils.createGlintTransform(0.25f)) return listOf(renderer to dynamicTransform) } 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..3cc941588 --- /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(SpriteAtlasTexture.BLOCK_ATLAS_TEXTURE) + 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/module/modules/debug/RendererTestModule.kt b/src/main/kotlin/com/lambda/module/modules/debug/RendererTestModule.kt index 9130fb9a4..f89bb68fe 100644 --- a/src/main/kotlin/com/lambda/module/modules/debug/RendererTestModule.kt +++ b/src/main/kotlin/com/lambda/module/modules/debug/RendererTestModule.kt @@ -38,6 +38,7 @@ import com.lambda.util.extension.tickDelta import com.lambda.util.math.lerp import com.lambda.util.world.toBlockPos import net.minecraft.item.Items +import net.minecraft.util.Identifier import net.minecraft.util.math.ChunkPos import net.minecraft.util.math.Direction import java.awt.Color @@ -51,86 +52,122 @@ object ChunkedRendererTest : Module( description = "Test module for ChunkedRenderer - cached chunk-based rendering", tag = ModuleTag.DEBUG, ) { - private val throughWalls by setting("Through Walls", false) - var changedAlready = false private val esp = chunkedEsp("ChunkedRendererTest", depthTest = false) { world, pos -> runSafe { if (player.chunkPos != ChunkPos(pos.toBlockPos())) return@chunkedEsp - true - } ?: return@chunkedEsp - if (changedAlready) return@chunkedEsp - val startPos = runSafe { lerp(mc.tickDelta, player.prevPos, player.pos) } ?: return@chunkedEsp - 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() + if (changedAlready) return@chunkedEsp + 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 ) - ) - - // 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() + + // ========== Item Rendering Tests ========== + // Test screen items at various positions and sizes + // Size is normalized (e.g., 0.03 = 3% of screen height) + screenItem(Items.DIAMOND_SWORD.defaultStack, 0.02f, 0.40f) // Default size ~1.5% + screenItem(Items.NETHERITE_CHESTPLATE.defaultStack, 0.06f, 0.40f) + screenItem(Items.ENCHANTED_GOLDEN_APPLE.defaultStack, 0.10f, 0.40f) + // Test larger item (5% of screen height) + screenItem(Items.DIAMOND.defaultStack, 0.14f, 0.40f, size = 0.05f) + + // ========== Image Rendering Tests ========== + // Test screen image using simple Identifier-based API + // screenImage( + // texture = Identifier.ofVanilla("textures/gui/sprites/hud/heart/hardcore_full.png"), + // x = 0.02f, y = 0.48f, + // width = 0.12f, height = 0.12f, + // tint = Color.WHITE + // ) + + // 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 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 - ) - changedAlready = true + changedAlready = true + } } init { - listen { - esp.depthTest = !throughWalls - esp.render() - } - listen { esp.renderScreen() } @@ -228,6 +265,50 @@ object TickedRendererTest : Module( ), centered = true ) + + // ========== Item Rendering Tests ========== + // Test screen items at various positions and sizes + // Size is normalized (e.g., 0.03 = 3% of screen height) + screenItem(Items.DIAMOND_SWORD.defaultStack, 0.02f, 0.40f) // Default size ~1.5% + screenItem(Items.NETHERITE_CHESTPLATE.defaultStack, 0.06f, 0.40f) + screenItem(Items.ENCHANTED_GOLDEN_APPLE.defaultStack, 0.10f, 0.40f) + // Test larger item (5% of screen height) + screenItem(Items.DIAMOND.defaultStack, 0.14f, 0.40f, size = 0.05f) + + // ========== Image Rendering Tests ========== + // Test screen image using simple Identifier-based API + // screenImage( + // texture = Identifier.ofVanilla("textures/gui/sprites/hud/heart/hardcore_full.png"), + // x = 0.02f, y = 0.48f, + // width = 0.12f, height = 0.12f, + // tint = Color.WHITE + // ) + + // 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 + ) } renderer.upload() @@ -325,6 +406,41 @@ object ImmediateRendererTest : Module( screenItem(Items.ENCHANTED_GOLDEN_APPLE.defaultStack, 0.10f, 0.40f) // Test larger item (5% of screen height) screenItem(Items.DIAMOND.defaultStack, 0.14f, 0.40f, size = 0.05f) + + // ========== Image Rendering Tests ========== + // Test screen image using simple Identifier-based API +// screenImage( +// texture = Identifier.ofVanilla("textures/gui/sprites/hud/heart/hardcore_full.png"), +// x = 0.02f, y = 0.48f, +// width = 0.12f, height = 0.12f, +// tint = Color.WHITE +// ) + + // 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 + ) } renderer.upload() diff --git a/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt b/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt index 9c2867a89..39ef81f26 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt @@ -25,9 +25,12 @@ import com.lambda.graphics.RenderMain.worldToScreenNormalized import com.lambda.graphics.mc.RenderBuilder import com.lambda.graphics.mc.renderer.ImmediateRenderer import com.lambda.graphics.text.FontHandler.getDefaultFont +import com.lambda.graphics.texture.LambdaImageAtlas +import com.lambda.graphics.texture.TextureOwner.texture import com.lambda.graphics.util.DynamicAABB.Companion.interpolatedBox import com.lambda.module.Module import com.lambda.module.tag.ModuleTag +import com.lambda.util.EnchantmentUtils.hasEnchantments import com.lambda.util.EntityUtils import com.lambda.util.EntityUtils.entityGroup import com.lambda.util.NamedEnum @@ -186,7 +189,7 @@ object Nametags : Module( } private fun RenderBuilder.renderItem(stack: ItemStack, x: Float, y: Float) { - screenItem(stack, x, y, trueItemScaleY) + screenImage(LambdaImageAtlas.getItemSprite(stack) ?: return, x, y, trueItemScaleX, trueItemScaleY, hasOverlay = stack.hasEnchantments, pixelPerfect = true) var iteratorY = y iteratorY += trueItemScaleY if (durability && stack.isDamageable) { diff --git a/src/main/resources/assets/lambda/shaders/core/advanced_lines.vsh b/src/main/resources/assets/lambda/shaders/core/advanced_lines.vsh index dea393834..27e780941 100644 --- a/src/main/resources/assets/lambda/shaders/core/advanced_lines.vsh +++ b/src/main/resources/assets/lambda/shaders/core/advanced_lines.vsh @@ -43,9 +43,18 @@ void main() { vec3 lineEnd = lineCenter + lineDir * (segmentLength * 0.5); vec3 thisPoint = isStart ? lineStart : lineEnd; - // Billboard direction: extract camera forward from ModelViewMat - // ModelViewMat is the view matrix, its third row gives the camera's forward direction in world space - vec3 toCamera = vec3(ModelViewMat[0][2], ModelViewMat[1][2], ModelViewMat[2][2]); + // Billboard direction: compute vector from line center to camera POSITION + // Extract camera position from ModelViewMat: + // For a view matrix, camera pos = inverse(rotation) * -translation + // The rotation part is the upper-left 3x3, translation is column 3 + mat3 rotationInv = transpose(mat3(ModelViewMat)); // inverse of rotation = transpose + vec3 translation = vec3(ModelViewMat[3]); + vec3 cameraPos = rotationInv * (-translation); + + // Now compute vector from line center to camera position + vec3 toCamera = normalize(cameraPos - lineCenter); + + // Compute perpendicular direction using cross product vec3 perpDir = cross(lineDir, toCamera); if (length(perpDir) < 0.001) { perpDir = cross(lineDir, vec3(0.0, 1.0, 0.0)); 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..c43990417 --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/screen_image.fsh @@ -0,0 +1,57 @@ +#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 vec3 v_OverlayUV; // (overlayU, overlayV, hasOverlay) +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 additive blending + color.rgb += glint.rgb * glint.a * 0.4; + } + + fragColor = color; + + // Use layer as fragment depth for draw order + gl_FragDepth = v_Layer; +} 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..b287b93ce --- /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 vec3 OverlayUV; // vec3(overlayU, overlayV, hasOverlay) +in float Layer; // Layer depth for draw order + +// Outputs to fragment shader +out vec2 v_TexCoord; +out vec4 v_Color; +out vec3 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/sdf_text.vsh b/src/main/resources/assets/lambda/shaders/core/sdf_text.vsh index 0e209b0ae..8aa50c8b0 100644 --- a/src/main/resources/assets/lambda/shaders/core/sdf_text.vsh +++ b/src/main/resources/assets/lambda/shaders/core/sdf_text.vsh @@ -56,6 +56,24 @@ void main() { } 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; 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..5853b6ffe --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/world_image.fsh @@ -0,0 +1,53 @@ +#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 vec3 v_OverlayUV; // (time, unused, hasOverlay) + +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! + // All renderers (Immediate, Ticked, Chunked) now include fresh glint matrix + vec4 transformedUV = TextureMat * vec4(baseUV, 0.0, 1.0); + + // Sample glint texture using transformed coordinates + vec4 glint = texture(Sampler1, fract(transformedUV.xy)); + + // Apply with additive blending + color.rgb += glint.rgb * glint.a * 0.4; + } + + 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..4782f069c --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/world_image.vsh @@ -0,0 +1,50 @@ +#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 vec3 OverlayUV; // (overlayU, overlayV, hasOverlay) + +// Outputs to fragment shader +out vec2 v_TexCoord; +out vec4 v_Color; +out vec3 v_OverlayUV; + +void main() { + float scale = BillboardData.x; + float billboardFlag = BillboardData.y; + + // Get rotation matrix for billboarding (camera-facing) + // In perspective mode, we can extract the right/up vectors from the inverse model-view + // For billboard, we want quads to always face the camera + + vec3 worldPos; + + if (billboardFlag < 0.5) { + // Billboard mode: face the camera + // Extract camera right and up from inverse model-view matrix + // For a perspective view, these are the transpose of the first two rows + vec3 camRight = vec3(ModelViewMat[0][0], ModelViewMat[1][0], ModelViewMat[2][0]); + vec3 camUp = vec3(ModelViewMat[0][1], ModelViewMat[1][1], ModelViewMat[2][1]); + + // Apply local offset (scaled) in camera space, then add anchor + worldPos = Anchor + (Position.x * scale * camRight) + (Position.y * scale * camUp); + } else { + // Fixed rotation mode: use pre-transformed Position + worldPos = Anchor + Position * scale; + } + + // Transform to clip space + gl_Position = ProjMat * ModelViewMat * vec4(worldPos, 1.0); + + // Pass data to fragment shader + v_TexCoord = UV0; + v_Color = Color; + v_OverlayUV = OverlayUV; +} From 954f7ef9ac623696baf27daa6aaaf6fef1e0e69c Mon Sep 17 00:00:00 2001 From: beanbag44 <107891830+beanbag44@users.noreply.github.com> Date: Sun, 1 Feb 2026 02:41:07 +0000 Subject: [PATCH 26/26] text and line setting groups --- .../lambda/mixin/items/BarrierBlockMixin.java | 8 +- .../mixin/render/BlockRenderManagerMixin.java | 8 +- src/main/kotlin/com/lambda/config/Setting.kt | 7 +- .../kotlin/com/lambda/config/SettingGroup.kt | 1 + .../com/lambda/config/groups/BreakSettings.kt | 95 ++++++++-------- .../com/lambda/config/groups/BuildSettings.kt | 31 +++--- .../com/lambda/config/groups/EatSettings.kt | 27 ++--- .../lambda/config/groups/FormatterSettings.kt | 13 +-- .../lambda/config/groups/HotbarSettings.kt | 15 +-- .../lambda/config/groups/InteractSettings.kt | 25 ++--- .../lambda/config/groups/InventorySettings.kt | 23 ++-- .../com/lambda/config/groups/LineConfig.kt | 39 +++++++ .../lambda/config/groups/RotationSettings.kt | 13 +-- .../config/groups/ScreenLineSettings.kt | 52 +++++++++ .../config/groups/ScreenTextSettings.kt | 54 ++++++++++ .../com/lambda/config/groups/Targeting.kt | 10 +- .../com/lambda/config/groups/TextConfig.kt | 51 +++++++++ .../lambda/config/groups/WorldLineSettings.kt | 56 ++++++++++ .../lambda/config/groups/WorldTextSettings.kt | 54 ++++++++++ .../com/lambda/graphics/mc/BoxBuilder.kt | 5 + .../com/lambda/graphics/mc/RenderBuilder.kt | 4 +- .../managers/rotating/RotationConfig.kt | 2 +- .../lambda/module/modules/chat/AntiSpam.kt | 5 +- .../modules/debug/SettingsTestModule.kt | 101 ++++++++++++++++++ .../lambda/module/modules/render/Nametags.kt | 74 +++++++------ .../modules/render/{BlockESP.kt => Search.kt} | 42 ++++---- .../lambda/module/modules/render/Tracers.kt | 57 ++++++---- src/main/kotlin/com/lambda/util/Formatting.kt | 1 + .../lambda/shaders/core/advanced_lines.fsh | 4 +- .../lambda/shaders/core/screen_lines.fsh | 4 +- 30 files changed, 674 insertions(+), 207 deletions(-) create mode 100644 src/main/kotlin/com/lambda/config/groups/LineConfig.kt create mode 100644 src/main/kotlin/com/lambda/config/groups/ScreenLineSettings.kt create mode 100644 src/main/kotlin/com/lambda/config/groups/ScreenTextSettings.kt create mode 100644 src/main/kotlin/com/lambda/config/groups/TextConfig.kt create mode 100644 src/main/kotlin/com/lambda/config/groups/WorldLineSettings.kt create mode 100644 src/main/kotlin/com/lambda/config/groups/WorldTextSettings.kt create mode 100644 src/main/kotlin/com/lambda/module/modules/debug/SettingsTestModule.kt rename src/main/kotlin/com/lambda/module/modules/render/{BlockESP.kt => Search.kt} (74%) diff --git a/src/main/java/com/lambda/mixin/items/BarrierBlockMixin.java b/src/main/java/com/lambda/mixin/items/BarrierBlockMixin.java index b4775b72f..7556e5b9c 100644 --- a/src/main/java/com/lambda/mixin/items/BarrierBlockMixin.java +++ b/src/main/java/com/lambda/mixin/items/BarrierBlockMixin.java @@ -17,7 +17,7 @@ package com.lambda.mixin.items; -import com.lambda.module.modules.render.BlockESP; +import com.lambda.module.modules.render.Search; import com.llamalad7.mixinextras.injector.ModifyReturnValue; import net.minecraft.block.BarrierBlock; import net.minecraft.block.BlockRenderType; @@ -29,12 +29,12 @@ @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 + * Modifies barrier block render type to {@link BlockRenderType#MODEL} when {@link Search} is enabled and {@link Search#getBarrier()} is true */ @ModifyReturnValue(method = "getRenderType", at = @At("RETURN")) private BlockRenderType modifyGetRenderType(BlockRenderType original, BlockState state) { - if (BlockESP.INSTANCE.isEnabled() - && BlockESP.getBarrier() + if (Search.INSTANCE.isEnabled() + && Search.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 index 49ff48b7f..3a6dfa41c 100644 --- a/src/main/java/com/lambda/mixin/render/BlockRenderManagerMixin.java +++ b/src/main/java/com/lambda/mixin/render/BlockRenderManagerMixin.java @@ -17,7 +17,7 @@ package com.lambda.mixin.render; -import com.lambda.module.modules.render.BlockESP; +import com.lambda.module.modules.render.Search; import net.minecraft.block.BlockState; import net.minecraft.block.Blocks; import net.minecraft.client.render.block.BlockRenderManager; @@ -31,9 +31,9 @@ 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() + if (Search.INSTANCE.isEnabled() + && Search.getBarrier() && state.getBlock() == Blocks.BARRIER - ) cir.setReturnValue(BlockESP.getModel()); + ) cir.setReturnValue(Search.getModel()); } } diff --git a/src/main/kotlin/com/lambda/config/Setting.kt b/src/main/kotlin/com/lambda/config/Setting.kt index 75f9ed688..402c6b7e8 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, @@ -160,6 +158,7 @@ class Setting, R>( 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 57888bd9d..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", 2f, 0f..10f, 0.1f, "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..23719be86 --- /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 + +open 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", 5, 1..50, 1, "The width of the line", visibility = visibility).group(*baseGroup).index() + override val width get() = widthSetting * 0.0001f + + 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", false, "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..b38667e54 --- /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", 12, 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 04e1e1cbf..839e23410 100644 --- a/src/main/kotlin/com/lambda/config/groups/Targeting.kt +++ b/src/main/kotlin/com/lambda/config/groups/Targeting.kt @@ -53,13 +53,14 @@ 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) - override val targets by c.setting("Targets", setOf(EntityGroup.Player, EntityGroup.Mob, EntityGroup.Boss), EntityGroup.entries) + 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. @@ -82,17 +83,18 @@ abstract class Targeting( baseGroup: NamedEnum, defaultRange: Double = 5.0, maxRange: Double = 16.0, + override val visibility: () -> Boolean = { true }, ) : Targeting(c, baseGroup, defaultRange, maxRange) { /** * 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. 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..d47398553 --- /dev/null +++ b/src/main/kotlin/com/lambda/config/groups/WorldLineSettings.kt @@ -0,0 +1,56 @@ +/* + * 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 line configuration. + * Provides individual settings for line colors, width, and dash patterns. + */ +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 widthSetting by c.setting("${prefix}Line Width", 5, 1..50, 1, "The width of the line", visibility = visibility).group(*baseGroup).index() + override val width get() = widthSetting * 0.001f + + 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", false, "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/mc/BoxBuilder.kt b/src/main/kotlin/com/lambda/graphics/mc/BoxBuilder.kt index 7c7202f35..69d204ccb 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/BoxBuilder.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/BoxBuilder.kt @@ -221,6 +221,11 @@ class BoxBuilder(val lineWidth: Float) { outlineTopSouthEast = south } + @RenderDsl + fun lineDashStyle(lineDashStyle: LineDashStyle) { + dashStyle = lineDashStyle + } + @RenderDsl fun showSides(vararg directions: Direction) { showFillSides(*directions) diff --git a/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt b/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt index 63132ad07..36b98ab61 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt @@ -1252,7 +1252,7 @@ class RenderBuilder(private val cameraPos: Vec3d) { // 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 = 0.15f // Shadow blur in SDF units + 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() @@ -1262,7 +1262,7 @@ class RenderBuilder(private val cameraPos: Vec3d) { /** SDF style configuration for text and other SDF-rendered elements */ data class SDFStyle( - val color: Color = Color.WHITE, + var color: Color = Color.WHITE, val outline: SDFOutline? = null, val glow: SDFGlow? = null, val shadow: SDFShadow? = SDFShadow() // Default shadow enabled 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/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/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/render/Nametags.kt b/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt index 39ef81f26..69587fdfa 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt @@ -18,9 +18,12 @@ 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.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.RenderBuilder import com.lambda.graphics.mc.renderer.ImmediateRenderer @@ -39,6 +42,7 @@ 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 @@ -56,32 +60,41 @@ object Nametags : Module( 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 textScale by setting("Text Scale", 1.2f, 0.4f..5f, 0.01f) - private val itemScale by setting("Item Scale", 1.9f, 0.4f..5f, 0.01f) - private val yOffset by setting("Y Offset", 0.2, 0.0..1.0, 0.01) - private val spacing by setting("Spacing", 0, 0..10, 1) - private val color by setting("Color", Color.WHITE) - private val friendColor by setting("Friend Color", Color.BLUE) - private val self by setting("Self", false) - private val health by setting("Health", false) - private val ping by setting("Ping", true) - private val gear by setting("Gear", true) - private val mainItem by setting("Main Item", true) { gear } - private val itemName by setting("Item Name", true) - private val itemNameScale by setting("Item Name Scale", 0.7f, 0.1f..1.0f, 0.01f) - private val offhandItem by setting("Offhand Item", true) { gear } - private val durability by setting("Durability", true) { gear } + private val entities by setting("Entities", setOf(EntityUtils.EntityGroup.Player), EntityUtils.EntityGroup.entries).group(Group.General) + private val itemScale by setting("Item Scale", 1.9f, 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).group(Group.General) + private val durability by setting("Durability", true) { gear }.group(Group.General) //ToDo: Implement // private val enchantments by setting("Enchantments", false) { gear } - private val entities by setting("Entities", setOf(EntityUtils.EntityGroup.Player), EntityUtils.EntityGroup.entries) + + 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) val renderer = ImmediateRenderer("Nametags") var heightWidthRatio = 0f - var trueTextScale = 0f var trueItemScaleX = 0f var trueItemScaleY = 0f var trueSpacingX = 0f @@ -91,7 +104,6 @@ object Nametags : Module( listen { renderer.tick() heightWidthRatio = mc.window.height / mc.window.width.toFloat() - trueTextScale = textScale * 0.01f trueItemScaleY = itemScale * 0.01f trueItemScaleX = trueItemScaleY * heightWidthRatio trueSpacingY = spacing * 0.0005f @@ -101,6 +113,9 @@ object Nametags : Module( 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 @@ -110,42 +125,40 @@ object Nametags : Module( ?: return@forEach if (entity !is LivingEntity) { - screenText(nameText, anchorX, anchorY + (trueTextScale / 2f), trueTextScale, centered = true) + 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 = trueTextScale * itemNameScale + val itemNameScale = textSize * itemNameScale screenText(itemNameText, anchorX, anchorY - (itemNameScale * 1.1f) - trueSpacingY, itemNameScale, centered = true) } - val nameWidth = getDefaultFont().getStringWidthNormalized(nameText, trueTextScale) + 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, trueTextScale) + 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, trueTextScale) + 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, trueTextScale) + screenText(nameText, nameX, anchorY, textSize, style = textStyle) if (healthCount >= 0) { val healthColor = lerp(entity.fullHealth / entity.maxFullHealth, Color.RED, Color.GREEN).brighter() - val healthStyle = RenderBuilder.SDFStyle(healthColor) - screenText(healthText, nameX + nameWidth + trueSpacingX, anchorY, trueTextScale, style = healthStyle) + 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() - val pingStyle = RenderBuilder.SDFStyle(pingColor) - screenText(pingText, nameX + nameWidth + healthWidth + trueSpacingX, anchorY, trueTextScale, style = pingStyle) + screenText(pingText, nameX + nameWidth + healthWidth + trueSpacingX, anchorY, textSize, style = textStyle.apply { color = pingColor }) } if (!gear) return@forEach @@ -155,7 +168,7 @@ object Nametags : Module( 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 + trueTextScale + trueSpacingY) + } else drawArmorAndItems(entity, anchorX, anchorY + textSize + trueSpacingY) } } @@ -196,8 +209,7 @@ object Nametags : Module( val dura = (1 - (stack.damage / stack.maxDamage.toDouble())).roundToStep(0.01) * 100 val duraText = "$dura%" val textSize = getDefaultFont().getSizeForWidthNormalized(duraText, trueItemScaleX) * 0.9f - val textStyle = RenderBuilder.SDFStyle(lerp(dura / 100, Color.RED, Color.GREEN).brighter()) - screenText(duraText, x, iteratorY, textSize, style = textStyle) + screenText(duraText, x, iteratorY, textSize, style = RenderBuilder.SDFStyle(color = lerp(dura / 100, Color.RED, Color.GREEN).brighter())) } } diff --git a/src/main/kotlin/com/lambda/module/modules/render/BlockESP.kt b/src/main/kotlin/com/lambda/module/modules/render/Search.kt similarity index 74% rename from src/main/kotlin/com/lambda/module/modules/render/BlockESP.kt rename to src/main/kotlin/com/lambda/module/modules/render/Search.kt index 164b46c9a..c1f3c97d8 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/BlockESP.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/Search.kt @@ -18,6 +18,8 @@ package com.lambda.module.modules.render import com.lambda.Lambda.mc +import com.lambda.config.applyEdits +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 @@ -27,6 +29,7 @@ import com.lambda.graphics.util.DirectionMask.buildSideMesh import com.lambda.module.Module import com.lambda.module.tag.ModuleTag import com.lambda.threading.runSafe +import com.lambda.util.NamedEnum import com.lambda.util.extension.blockColor import com.lambda.util.extension.getBlockState import com.lambda.util.world.toBlockPos @@ -35,28 +38,30 @@ 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", +object Search : Module( + name = "Search", + description = "Highlight blocks within the rendered world", 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 } + private val blocks by setting("Blocks", setOf(Blocks.BEDROCK), description = "Render blocks") .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 var drawFaces: Boolean by setting("Draw Faces", true, "Draw faces of blocks").onValueChange(::rebuildMesh).onValueChange { _, to -> if (!to) drawOutlines = true } + private var drawOutlines: Boolean by setting("Draw Outlines", true, "Draw outlines of blocks").onValueChange(::rebuildMesh).onValueChange { _, to -> if (!to) drawFaces = true } + private val mesh by setting("Mesh", true, "Connect similar adjacent blocks").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 useBlockColor by setting("Use Block Color", false, "Use the color of the block instead").onValueChange(::rebuildMesh) + private val blockColorAlpha by setting("Block Color Alpha", 0.3, 0.1..1.0, 0.05) { 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", 0.01f, 0.001f..1.0f, 0.001f) { searchBlocks && drawOutlines }.onValueChange(::rebuildMesh) - - private val outlineMode by setting("Outline Mode", DirectionMask.OutlineMode.And, "Outline mode") { searchBlocks }.onValueChange(::rebuildMesh) + private val faceColor by setting("Face Color", Color(100, 150, 255, 51), "Color of the surfaces") { drawFaces && !useBlockColor }.onValueChange(::rebuildMesh) + private val outlineMode by setting("Outline Mode", DirectionMask.OutlineMode.And, "Outline mode").onValueChange(::rebuildMesh) + private val lineColor by setting("Line Color", Color(100, 150, 255, 128)) { !useBlockColor }.onValueChange(::rebuildMesh) + private val lineConfig = WorldLineSettings("", this).apply { + applyEdits { + hide(::startColor, ::endColor) + } + } @JvmStatic val barrier by setting("Solid Barrier Block", true, "Render barrier blocks") @@ -85,14 +90,15 @@ object BlockESP : Module( val pos = position.toBlockPos() val shape = state.getOutlineShape(world, pos) val worldBox = if (shape.isEmpty) Box(pos) else shape.boundingBox.offset(pos) - box(worldBox, outlineWidth) { + box(worldBox, lineConfig.width) { val hiddenSides = sides.inv() hideSides(hiddenSides) if (drawFaces) fillColor(if (useBlockColor) finalColor else faceColor) else hideFill() if (!drawOutlines) hideOutline() else { - outlineColor(if (useBlockColor) extractedColor else outlineColor) - outlineMode(this@BlockESP.outlineMode) + outlineColor(if (useBlockColor) extractedColor else lineColor) + lineConfig.getDashStyle()?.let { lineDashStyle(it) } + outlineMode(this@Search.outlineMode) } } } diff --git a/src/main/kotlin/com/lambda/module/modules/render/Tracers.kt b/src/main/kotlin/com/lambda/module/modules/render/Tracers.kt index 8cfc67c7f..ca312d32e 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/Tracers.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/Tracers.kt @@ -17,6 +17,8 @@ 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 @@ -27,6 +29,7 @@ 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 @@ -42,22 +45,39 @@ object Tracers : Module( description = "Draws lines to entities within the world", tag = ModuleTag.RENDER ) { - private val width by setting("Width", 1, 1..50, 1) - private val target by setting("Target", TracerMode.Feet) - private val stem by setting("Stem", true) - private val entities by setting("Entities", setOf(EntityGroup.Player, EntityGroup.Mob, EntityGroup.Boss), EntityGroup.entries) - private val friendColor by setting("Friend Color", Color(80, 80, 255, 255)) - private val playerDistanceGradient by setting("Player Distance Gradient", true) { EntityGroup.Player in entities } - private val playerDistanceColorFar by setting("Player Far Color", Color.GREEN) { EntityGroup.Player in entities && playerDistanceGradient } - private val playerDistanceColorClose by setting("Player Close Color", Color.RED) { EntityGroup.Player in entities && playerDistanceGradient } - private val playerColor by setting("Players", Color.RED) { EntityGroup.Player in entities && !playerDistanceGradient } - private val mobColor by setting("Mobs", Color(255, 80, 0, 255)) { EntityGroup.Mob in entities } - private val passiveColor by setting("Passives", Color.BLUE) { EntityGroup.Passive in entities } - private val projectileColor by setting("Projectiles", Color.LIGHT_GRAY) { EntityGroup.Projectile in entities } - private val vehicleColor by setting("Vehicles", Color.WHITE) { EntityGroup.Vehicle in entities } - private val decorationColor by setting("Decorations", Color.PINK) { EntityGroup.Decoration in entities } - private val bossColor by setting("Bosses", Color(255, 0, 255, 255)) { EntityGroup.Boss in entities } - private val miscColor by setting("Miscellaneous", Color.magenta) { EntityGroup.Misc in entities } + 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) } + } val renderer = ImmediateRenderer("Tracers") @@ -87,6 +107,7 @@ object Tracers : Module( 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) { @@ -95,7 +116,7 @@ object Tracers : Module( TracerMode.Eyes -> lerpedEyePos } val (toX, toY) = worldToScreenNormalized(targetPos) ?: return@forEach - screenLine(0.5f, 0.5f, toX, toY, color, width * 0.0001f) + screenLine(0.5f, 0.5f, toX, toY, color, lineConfig.width, lineConfig.getDashStyle()) if (stem) { val (lowerX, lowerY) = if (target == TracerMode.Feet) Vector2f(toX, toY) @@ -103,7 +124,7 @@ object Tracers : Module( val (upperX, upperY) = if (target == TracerMode.Eyes) Vector2f(toX, toY) else worldToScreenNormalized(lerpedEyePos) ?: return@forEach - screenLine(lowerX, lowerY, upperX, upperY, color, width * 0.0001f) + screenLine(lowerX, lowerY, upperX, upperY, color, lineConfig.width, lineConfig.getDashStyle()) } } } diff --git a/src/main/kotlin/com/lambda/util/Formatting.kt b/src/main/kotlin/com/lambda/util/Formatting.kt index bd37d3e3e..d95de117d 100644 --- a/src/main/kotlin/com/lambda/util/Formatting.kt +++ b/src/main/kotlin/com/lambda/util/Formatting.kt @@ -125,6 +125,7 @@ object Formatting { object Default : FormatterConfig { override val settings = mutableListOf>() + override val visibility: () -> Boolean = { true } override val locale: Locale = Locale.US override val separator: String = "," override val prefix: String = "(" diff --git a/src/main/resources/assets/lambda/shaders/core/advanced_lines.fsh b/src/main/resources/assets/lambda/shaders/core/advanced_lines.fsh index a81e4ac31..becd5d355 100644 --- a/src/main/resources/assets/lambda/shaders/core/advanced_lines.fsh +++ b/src/main/resources/assets/lambda/shaders/core/advanced_lines.fsh @@ -100,8 +100,8 @@ void main() { // Calculate animated offset float animatedOffset = dashOffset; - if (animationSpeed > 0.0) { - animatedOffset += GameTime * animationSpeed * 1200.0; + if (animationSpeed != 0.0) { + animatedOffset -= GameTime * animationSpeed * 1200.0; } // Use UNCLAMPED projLength so dashes continue through endcaps diff --git a/src/main/resources/assets/lambda/shaders/core/screen_lines.fsh b/src/main/resources/assets/lambda/shaders/core/screen_lines.fsh index c27181979..cbb1461a3 100644 --- a/src/main/resources/assets/lambda/shaders/core/screen_lines.fsh +++ b/src/main/resources/assets/lambda/shaders/core/screen_lines.fsh @@ -77,8 +77,8 @@ void main() { // Calculate animated offset float animatedOffset = dashOffset; - if (animationSpeed > 0.0) { - animatedOffset += GameTime * animationSpeed * 1200.0; + if (animationSpeed != 0.0) { + animatedOffset -= GameTime * animationSpeed * 1200.0; } // Use UNCLAMPED projLength so dashes continue through endcaps