Skip to content

Commit 1efc938

Browse files
committed
initial working standard and sdf text world rendering
1 parent 0aec9f4 commit 1efc938

File tree

9 files changed

+1879
-4
lines changed

9 files changed

+1879
-4
lines changed

src/main/kotlin/com/lambda/graphics/mc/LambdaRenderPipelines.kt

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,86 @@ object LambdaRenderPipelines : Loadable {
116116
)
117117
.build()
118118
)
119+
120+
/**
121+
* Pipeline for textured text rendering with alpha blending.
122+
* Uses position_tex_color shader with Sampler0 for font atlas texture.
123+
*/
124+
val TEXT_QUADS: RenderPipeline =
125+
RenderPipelines.register(
126+
RenderPipeline.builder(LAMBDA_ESP_SNIPPET)
127+
.withLocation(Identifier.of("lambda", "pipeline/text_quads"))
128+
.withVertexShader(Identifier.ofVanilla("core/position_tex_color"))
129+
.withFragmentShader(Identifier.ofVanilla("core/position_tex_color"))
130+
.withSampler("Sampler0")
131+
.withBlend(BlendFunction.TRANSLUCENT)
132+
.withDepthWrite(false)
133+
.withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST)
134+
.withCull(false)
135+
.withVertexFormat(
136+
VertexFormats.POSITION_TEXTURE_COLOR,
137+
VertexFormat.DrawMode.QUADS
138+
)
139+
.build()
140+
)
141+
142+
/** Pipeline for text that renders through walls. */
143+
val TEXT_QUADS_THROUGH: RenderPipeline =
144+
RenderPipelines.register(
145+
RenderPipeline.builder(LAMBDA_ESP_SNIPPET)
146+
.withLocation(Identifier.of("lambda", "pipeline/text_quads_through"))
147+
.withVertexShader(Identifier.ofVanilla("core/position_tex_color"))
148+
.withFragmentShader(Identifier.ofVanilla("core/position_tex_color"))
149+
.withSampler("Sampler0")
150+
.withBlend(BlendFunction.TRANSLUCENT)
151+
.withDepthWrite(false)
152+
.withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
153+
.withCull(false)
154+
.withVertexFormat(
155+
VertexFormats.POSITION_TEXTURE_COLOR,
156+
VertexFormat.DrawMode.QUADS
157+
)
158+
.build()
159+
)
160+
161+
/**
162+
* Pipeline for SDF text rendering with proper smoothstep anti-aliasing.
163+
* Uses lambda:core/sdf_text shaders with SDF-specific uniforms for effects.
164+
*/
165+
val SDF_TEXT: RenderPipeline =
166+
RenderPipelines.register(
167+
RenderPipeline.builder(LAMBDA_ESP_SNIPPET)
168+
.withLocation(Identifier.of("lambda", "pipeline/sdf_text"))
169+
.withVertexShader(Identifier.of("lambda", "core/sdf_text"))
170+
.withFragmentShader(Identifier.of("lambda", "core/sdf_text"))
171+
.withSampler("Sampler0")
172+
.withBlend(BlendFunction.TRANSLUCENT)
173+
.withDepthWrite(false)
174+
.withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST)
175+
.withCull(false)
176+
.withVertexFormat(
177+
VertexFormats.POSITION_TEXTURE_COLOR,
178+
VertexFormat.DrawMode.QUADS
179+
)
180+
.build()
181+
)
182+
183+
/** SDF text pipeline that renders through walls. */
184+
val SDF_TEXT_THROUGH: RenderPipeline =
185+
RenderPipelines.register(
186+
RenderPipeline.builder(LAMBDA_ESP_SNIPPET)
187+
.withLocation(Identifier.of("lambda", "pipeline/sdf_text_through"))
188+
.withVertexShader(Identifier.of("lambda", "core/sdf_text"))
189+
.withFragmentShader(Identifier.of("lambda", "core/sdf_text"))
190+
.withSampler("Sampler0")
191+
.withBlend(BlendFunction.TRANSLUCENT)
192+
.withDepthWrite(false)
193+
.withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
194+
.withCull(false)
195+
.withVertexFormat(
196+
VertexFormats.POSITION_TEXTURE_COLOR,
197+
VertexFormat.DrawMode.QUADS
198+
)
199+
.build()
200+
)
119201
}
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
/*
2+
* Copyright 2026 Lambda
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
package com.lambda.graphics.text
19+
20+
import com.lambda.util.stream
21+
import com.mojang.blaze3d.systems.RenderSystem
22+
import com.mojang.blaze3d.textures.FilterMode
23+
import com.mojang.blaze3d.textures.GpuTexture
24+
import com.mojang.blaze3d.textures.GpuTextureView
25+
import com.mojang.blaze3d.textures.TextureFormat
26+
import net.minecraft.client.gl.GpuSampler
27+
import net.minecraft.client.texture.NativeImage
28+
import org.lwjgl.stb.STBTTFontinfo
29+
import org.lwjgl.stb.STBTruetype.stbtt_FindGlyphIndex
30+
import org.lwjgl.stb.STBTruetype.stbtt_GetFontVMetrics
31+
import org.lwjgl.stb.STBTruetype.stbtt_GetGlyphBitmapBox
32+
import org.lwjgl.stb.STBTruetype.stbtt_GetGlyphHMetrics
33+
import org.lwjgl.stb.STBTruetype.stbtt_InitFont
34+
import org.lwjgl.stb.STBTruetype.stbtt_MakeGlyphBitmap
35+
import org.lwjgl.stb.STBTruetype.stbtt_ScaleForPixelHeight
36+
import org.lwjgl.system.MemoryStack
37+
import org.lwjgl.system.MemoryUtil
38+
import java.nio.ByteBuffer
39+
40+
/**
41+
* Font atlas that uses MC 1.21's GPU texture APIs for proper rendering.
42+
*
43+
* Uses STB TrueType for glyph rasterization and MC's GpuTexture/GpuTextureView/GpuSampler
44+
* for texture management, enabling correct texture binding via RenderPass.bindTexture().
45+
*
46+
* @param fontPath Resource path to TTF/OTF file
47+
* @param fontSize Font size in pixels
48+
* @param atlasWidth Atlas texture width (must be power of 2)
49+
* @param atlasHeight Atlas texture height (must be power of 2)
50+
*/
51+
class FontAtlas(
52+
fontPath: String,
53+
val fontSize: Float = 64f,
54+
val atlasWidth: Int = 2048,
55+
val atlasHeight: Int = 2048
56+
) : AutoCloseable {
57+
58+
data class Glyph(
59+
val codepoint: Int,
60+
val x0: Int, val y0: Int,
61+
val x1: Int, val y1: Int,
62+
val xOffset: Float, val yOffset: Float,
63+
val xAdvance: Float,
64+
val u0: Float, val v0: Float,
65+
val u1: Float, val v1: Float
66+
)
67+
68+
private val fontBuffer: ByteBuffer
69+
private val fontInfo: STBTTFontinfo
70+
private val glyphs = mutableMapOf<Int, Glyph>()
71+
72+
// MC 1.21 GPU texture objects
73+
private var glTexture: GpuTexture? = null
74+
private var glTextureView: GpuTextureView? = null
75+
private var gpuSampler: GpuSampler? = null
76+
77+
// Temporary storage for atlas during construction
78+
private var atlasData: ByteArray? = null
79+
80+
val lineHeight: Float
81+
val ascent: Float
82+
val descent: Float
83+
84+
/** Get the texture view for binding in render pass */
85+
val textureView: GpuTextureView?
86+
get() = glTextureView
87+
88+
/** Get the sampler for binding in render pass */
89+
val sampler: GpuSampler?
90+
get() = gpuSampler
91+
92+
/** Check if texture is uploaded and ready */
93+
val isUploaded: Boolean
94+
get() = glTexture != null
95+
96+
init {
97+
// Load font file
98+
val fontBytes = fontPath.stream.readAllBytes()
99+
fontBuffer = MemoryUtil.memAlloc(fontBytes.size).put(fontBytes).flip()
100+
101+
fontInfo = STBTTFontinfo.create()
102+
if (!stbtt_InitFont(fontInfo, fontBuffer)) {
103+
MemoryUtil.memFree(fontBuffer)
104+
throw RuntimeException("Failed to initialize font: $fontPath")
105+
}
106+
107+
// Calculate scale and metrics
108+
val scale = stbtt_ScaleForPixelHeight(fontInfo, fontSize)
109+
110+
MemoryStack.stackPush().use { stack ->
111+
val ascentBuf = stack.mallocInt(1)
112+
val descentBuf = stack.mallocInt(1)
113+
val lineGapBuf = stack.mallocInt(1)
114+
stbtt_GetFontVMetrics(fontInfo, ascentBuf, descentBuf, lineGapBuf)
115+
116+
ascent = ascentBuf[0] * scale
117+
descent = descentBuf[0] * scale
118+
lineHeight = (ascentBuf[0] - descentBuf[0] + lineGapBuf[0]) * scale
119+
}
120+
121+
// Build atlas data
122+
atlasData = ByteArray(atlasWidth * atlasHeight * 4) // RGBA
123+
buildAtlas(scale)
124+
}
125+
126+
private fun buildAtlas(scale: Float) {
127+
val data = atlasData ?: return
128+
var penX = 1
129+
var penY = 1
130+
var rowHeight = 0
131+
132+
// Rasterize printable ASCII + extended Latin
133+
val codepoints = (32..126) + (160..255)
134+
135+
MemoryStack.stackPush().use { stack ->
136+
val x0 = stack.mallocInt(1)
137+
val y0 = stack.mallocInt(1)
138+
val x1 = stack.mallocInt(1)
139+
val y1 = stack.mallocInt(1)
140+
val advanceWidth = stack.mallocInt(1)
141+
val leftSideBearing = stack.mallocInt(1)
142+
143+
for (cp in codepoints) {
144+
val glyphIndex = stbtt_FindGlyphIndex(fontInfo, cp)
145+
if (glyphIndex == 0 && cp != 32) continue
146+
147+
stbtt_GetGlyphHMetrics(fontInfo, glyphIndex, advanceWidth, leftSideBearing)
148+
stbtt_GetGlyphBitmapBox(fontInfo, glyphIndex, scale, scale, x0, y0, x1, y1)
149+
150+
val glyphW = x1[0] - x0[0]
151+
val glyphH = y1[0] - y0[0]
152+
153+
// Check if we need to wrap to next row
154+
if (penX + glyphW + 1 >= atlasWidth) {
155+
penX = 1
156+
penY += rowHeight + 1
157+
rowHeight = 0
158+
}
159+
160+
// Check atlas overflow
161+
if (penY + glyphH + 1 >= atlasHeight) break
162+
163+
// Rasterize glyph
164+
if (glyphW > 0 && glyphH > 0) {
165+
val tempBuffer = MemoryUtil.memAlloc(glyphW * glyphH)
166+
try {
167+
stbtt_MakeGlyphBitmap(
168+
fontInfo, tempBuffer,
169+
glyphW, glyphH, glyphW, scale, scale, glyphIndex
170+
)
171+
// Copy to atlas as RGBA (white with grayscale as alpha)
172+
for (row in 0 until glyphH) {
173+
for (col in 0 until glyphW) {
174+
val srcIndex = row * glyphW + col
175+
val alpha = tempBuffer.get(srcIndex).toInt() and 0xFF
176+
val dstIndex = ((penY + row) * atlasWidth + penX + col) * 4
177+
data[dstIndex + 0] = 0xFF.toByte() // R
178+
data[dstIndex + 1] = 0xFF.toByte() // G
179+
data[dstIndex + 2] = 0xFF.toByte() // B
180+
data[dstIndex + 3] = alpha.toByte() // A
181+
}
182+
}
183+
} finally {
184+
MemoryUtil.memFree(tempBuffer)
185+
}
186+
}
187+
188+
// Store glyph info
189+
glyphs[cp] = Glyph(
190+
codepoint = cp,
191+
x0 = penX, y0 = penY,
192+
x1 = penX + glyphW, y1 = penY + glyphH,
193+
xOffset = x0[0].toFloat(),
194+
yOffset = y0[0].toFloat(),
195+
xAdvance = advanceWidth[0] * scale,
196+
u0 = penX.toFloat() / atlasWidth,
197+
v0 = penY.toFloat() / atlasHeight,
198+
u1 = (penX + glyphW).toFloat() / atlasWidth,
199+
v1 = (penY + glyphH).toFloat() / atlasHeight
200+
)
201+
202+
penX += glyphW + 1
203+
rowHeight = maxOf(rowHeight, glyphH)
204+
}
205+
}
206+
}
207+
208+
/**
209+
* Upload atlas to GPU using MC 1.21 APIs.
210+
* Must be called on the render thread.
211+
*/
212+
fun upload() {
213+
if (glTexture != null) return // Already uploaded
214+
val data = atlasData ?: return
215+
216+
RenderSystem.assertOnRenderThread()
217+
218+
val gpuDevice = RenderSystem.getDevice()
219+
220+
// Create GPU texture (usage flags: 5 = COPY_DST | TEXTURE_BINDING)
221+
glTexture = gpuDevice.createTexture(
222+
"Lambda FontAtlas",
223+
5, // COPY_DST (1) | TEXTURE_BINDING (4)
224+
TextureFormat.RGBA8,
225+
atlasWidth, atlasHeight,
226+
1, // layers
227+
1 // mip levels
228+
)
229+
230+
// Create texture view
231+
glTextureView = gpuDevice.createTextureView(glTexture)
232+
233+
// Get sampler with linear filtering
234+
gpuSampler = RenderSystem.getSamplerCache().get(FilterMode.LINEAR)
235+
236+
// Create NativeImage and copy data
237+
val nativeImage = NativeImage(atlasWidth, atlasHeight, false)
238+
for (y in 0 until atlasHeight) {
239+
for (x in 0 until atlasWidth) {
240+
val srcIndex = (y * atlasWidth + x) * 4
241+
val r = data[srcIndex + 0].toInt() and 0xFF
242+
val g = data[srcIndex + 1].toInt() and 0xFF
243+
val b = data[srcIndex + 2].toInt() and 0xFF
244+
val a = data[srcIndex + 3].toInt() and 0xFF
245+
// NativeImage uses ABGR format
246+
val abgr = (a shl 24) or (b shl 16) or (g shl 8) or r
247+
nativeImage.setColor(x, y, abgr)
248+
}
249+
}
250+
251+
// Upload to GPU
252+
RenderSystem.getDevice().createCommandEncoder().writeToTexture(glTexture, nativeImage)
253+
nativeImage.close()
254+
255+
// Free atlas data after upload
256+
atlasData = null
257+
}
258+
259+
fun getGlyph(codepoint: Int): Glyph? = glyphs[codepoint]
260+
261+
/** Calculate the width of a string in pixels. */
262+
fun getStringWidth(text: String): Float {
263+
var width = 0f
264+
for (char in text) {
265+
val glyph = glyphs[char.code] ?: glyphs[' '.code] ?: continue
266+
width += glyph.xAdvance
267+
}
268+
return width
269+
}
270+
271+
override fun close() {
272+
glTextureView?.close()
273+
glTextureView = null
274+
glTexture?.close()
275+
glTexture = null
276+
gpuSampler = null // Sampler is managed by cache, don't close
277+
atlasData = null
278+
MemoryUtil.memFree(fontBuffer)
279+
}
280+
}

0 commit comments

Comments
 (0)