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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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",