Skip to content

Commit 2e3a883

Browse files
committed
ref: textures
1 parent 2aa6543 commit 2e3a883

File tree

6 files changed

+100
-52
lines changed

6 files changed

+100
-52
lines changed

common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,8 @@ import com.lambda.graphics.pipeline.UIPipeline
2525
import com.lambda.graphics.renderer.gui.font.core.GlyphInfo
2626
import com.lambda.graphics.renderer.gui.font.core.LambdaAtlas.get
2727
import com.lambda.graphics.renderer.gui.font.core.LambdaAtlas.height
28-
import com.lambda.graphics.renderer.gui.font.core.LambdaAtlas.slot
29-
import com.lambda.graphics.renderer.gui.font.sdf.DistanceFieldTexture
3028
import com.lambda.graphics.shader.Shader
31-
import com.lambda.graphics.texture.TextureOwner.texture
29+
import com.lambda.graphics.texture.TextureOwner.bind
3230
import com.lambda.module.modules.client.LambdaMoji
3331
import com.lambda.module.modules.client.RenderSettings
3432
import com.lambda.util.math.Vec2d
@@ -44,8 +42,6 @@ object FontRenderer {
4442
private val chars = RenderSettings.textFont
4543
private val emojis = RenderSettings.emojiFont
4644

47-
private val charsSDF = DistanceFieldTexture(chars.texture)
48-
4945
private val shader = Shader("font/font")
5046
private val pipeline = VertexPipeline(VertexMode.TRIANGLES, VertexAttrib.Group.FONT)
5147

@@ -241,12 +237,11 @@ object FontRenderer {
241237
fun render() {
242238
shader.use()
243239
shader["u_FontTexture"] = 0
244-
shader["u_EmojiTexture"] = emojis.slot
240+
shader["u_EmojiTexture"] = 1
245241
shader["u_SDFMin"] = 0.3
246242
shader["u_SDFMax"] = 1.0
247243

248-
charsSDF.frame.bind()
249-
//emojis.bind()
244+
bind(chars, emojis)
250245

251246
pipeline.immediateDraw()
252247
}

common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaAtlas.kt

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,16 @@ package com.lambda.graphics.renderer.gui.font.core
1919

2020
import com.google.common.math.IntMath
2121
import com.lambda.core.Loadable
22-
import com.lambda.graphics.texture.TextureOwner.texture
23-
import com.lambda.graphics.texture.TextureOwner.upload
22+
import com.lambda.graphics.texture.TextureOwner.uploadField
2423
import com.lambda.http.Method
2524
import com.lambda.http.request
2625
import com.lambda.threading.runGameScheduled
2726
import com.lambda.util.math.Vec2d
2827
import com.lambda.util.stream
29-
import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap
3028
import it.unimi.dsi.fastutil.objects.Object2DoubleArrayMap
31-
import it.unimi.dsi.fastutil.objects.Object2IntArrayMap
3229
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
3330
import java.awt.*
3431
import java.awt.image.BufferedImage
35-
import java.util.function.ToIntFunction
3632
import java.util.zip.ZipFile
3733
import javax.imageio.ImageIO
3834
import kotlin.math.ceil
@@ -66,10 +62,8 @@ import kotlin.time.Duration.Companion.days
6662
* ```
6763
*/
6864
object LambdaAtlas : Loadable {
69-
private val fontMap = Object2ObjectOpenHashMap<Any, Int2ObjectArrayMap<GlyphInfo>>()
70-
private val emojiMap = Object2ObjectOpenHashMap<Any, Object2ObjectOpenHashMap<String, GlyphInfo>>()
71-
private val slotReservation =
72-
Object2IntArrayMap<Any>() // Will cause undefined behavior if someone is trying to allocate more than 32 slots, unlikely
65+
private val fontMap = mutableMapOf<Any, Map<Char, GlyphInfo>>()
66+
private val emojiMap = mutableMapOf<Any, Map<String, GlyphInfo>>()
7367

7468
private val bufferPool =
7569
mutableMapOf<Any, BufferedImage>() // This array is nuked once the data is dispatched to OpenGL
@@ -78,16 +72,9 @@ object LambdaAtlas : Loadable {
7872
private val metricCache = mutableMapOf<Font, FontMetrics>()
7973
private val heightCache = Object2DoubleArrayMap<Font>()
8074

81-
operator fun LambdaFont.get(char: Char): GlyphInfo? = fontMap.getValue(this)[char.code]
75+
operator fun LambdaFont.get(char: Char): GlyphInfo? = fontMap.getValue(this)[char]
8276
operator fun LambdaEmoji.get(string: String): GlyphInfo? = emojiMap.getValue(this)[string]
8377

84-
// Allow binding any valid font definition enums
85-
fun <T : Enum<T>> T.bind() =
86-
this@bind.texture.bind(slot = slotReservation.computeIfAbsent(this@bind, ToIntFunction { slotReservation.size }))
87-
88-
val <T : Enum<T>> T.slot: Int
89-
get() = slotReservation.getInt(this@slot)
90-
9178
val LambdaFont.height: Double
9279
get() = heightCache.getDouble(fontCache[this@height])
9380

@@ -154,10 +141,10 @@ object LambdaAtlas : Loadable {
154141
x += emoji.width + 2
155142
}
156143

157-
emojiMap[this] = constructed
144+
emojiMap[this@buildBuffer] = constructed
158145
}
159146

160-
bufferPool[this] = image
147+
bufferPool[this@buildBuffer] = image
161148
}
162149

163150
fun LambdaFont.buildBuffer(
@@ -179,7 +166,7 @@ object LambdaAtlas : Loadable {
179166
var y = CHAR_SPACE
180167
var rowHeight = 0
181168

182-
val constructed = Int2ObjectArrayMap<GlyphInfo>()
169+
val constructed = mutableMapOf<Char, GlyphInfo>()
183170
(Char.MIN_VALUE..<characters.toChar()).forEach { char ->
184171
val charImage = getCharImage(font, char) ?: return@forEach
185172

@@ -201,14 +188,14 @@ object LambdaAtlas : Loadable {
201188
val uv1 = Vec2d(x, y) * oneTexelSize
202189
val uv2 = Vec2d(x, y).plus(size) * oneTexelSize
203190

204-
constructed[char.code] = GlyphInfo(size, uv1, uv2)
191+
constructed[char] = GlyphInfo(size, uv1, uv2)
205192
heightCache[font] = max(heightCache.getDouble(font), size.y) // No compare set unfortunately
206193

207194
x += charWidth
208195
}
209196

210-
fontMap[this] = constructed
211-
bufferPool[this] = image
197+
fontMap[this@buildBuffer] = constructed
198+
bufferPool[this@buildBuffer] = image
212199
}
213200

214201
// TODO: Change this when we've refactored the loadables
@@ -219,7 +206,7 @@ object LambdaAtlas : Loadable {
219206
val str = "Loaded ${bufferPool.size} fonts" // avoid race condition
220207

221208
runGameScheduled {
222-
bufferPool.forEach { (owner, image) -> owner.upload(image) }
209+
bufferPool.forEach { (owner, image) -> owner.uploadField(image) }
223210
bufferPool.clear()
224211
}
225212

@@ -255,4 +242,4 @@ object LambdaAtlas : Loadable {
255242

256243
return charImage
257244
}
258-
}
245+
}

common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/sdf/DistanceFieldTexture.kt

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,20 @@ import com.lambda.graphics.buffer.frame.FrameBuffer
2222
import com.lambda.graphics.shader.Shader
2323
import com.lambda.graphics.texture.Texture
2424
import com.lambda.util.math.Vec2d
25+
import java.awt.image.BufferedImage
2526

2627
/**
2728
* A class that represents a distance field texture, which is created by rendering a given texture
2829
* onto a framebuffer with specific shader operations.
2930
*
3031
* The texture is used to create a signed distance field (SDF) for rendering operations.
3132
*
32-
* @param texture The texture to be used for creating the distance field.
33+
* @param image Image data to upload
3334
*/
34-
class DistanceFieldTexture(texture: Texture) {
35-
val frame = CachedFrame(texture.width, texture.height).write {
35+
class DistanceFieldTexture(image: BufferedImage) : Texture(image) {
36+
private val frame = CachedFrame(width, height).write {
3637
FrameBuffer.pipeline.use {
37-
val (pos1, pos2) = Vec2d.ZERO to Vec2d(texture.width, texture.height)
38+
val (pos1, pos2) = Vec2d.ZERO to Vec2d(width, height)
3839

3940
grow(4)
4041
putQuad(
@@ -46,13 +47,17 @@ class DistanceFieldTexture(texture: Texture) {
4647

4748
shader.use()
4849
shader["u_TexelSize"] = Vec2d.ONE / pos2
49-
texture.bind()
50+
super.bind(0)
5051

5152
immediateDraw()
5253
}
5354
}
5455

56+
override fun bind(slot: Int) {
57+
frame.bind(slot)
58+
}
59+
5560
companion object {
5661
private val shader = Shader("signed_distance_field", "renderer/pos_tex")
5762
}
58-
}
63+
}

common/src/main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ class AnimatedTexture(path: LambdaResource) : Texture(image = null, forceConsist
5858

5959
gif.clear()
6060

61-
currentFrame = (currentFrame+1) % frames
61+
currentFrame = (currentFrame + 1) % frames
6262
lastUpload = System.currentTimeMillis()
6363
}
6464
}

common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,8 @@ open class Texture(
109109

110110
init {
111111
image?.let {
112-
bind()
112+
// Don't use bind() because if a child class overrides the function we're screwed
113+
bindTexture(id)
113114
upload(it)
114115
}
115116
}

common/src/main/kotlin/com/lambda/graphics/texture/TextureOwner.kt

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,36 +17,96 @@
1717

1818
package com.lambda.graphics.texture
1919

20+
import com.lambda.graphics.renderer.gui.font.sdf.DistanceFieldTexture
2021
import com.lambda.util.readImage
21-
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
2222
import java.awt.image.BufferedImage
2323

24+
/**
25+
* The [TextureOwner] object is responsible for managing textures owned by various objects in the render pipeline
26+
*/
2427
object TextureOwner {
25-
private val textureMap = Object2ObjectOpenHashMap<Any, Texture>()
28+
private val textureMap = HashMap<Any, MutableList<Texture>>()
2629

2730
/**
28-
* Returns the texture owned by a specific object
31+
* Retrieves the first texture owned by the object
2932
*/
3033
val Any.texture: Texture
31-
get() = textureMap.getValue(this@texture)
34+
get() = textureMap.getValue(this@texture)[0]
35+
36+
/**
37+
* Retrieves a specific texture owned by the object by its index
38+
*
39+
* @param index The index of the texture to retrieve
40+
* @return The texture [T] at the given index
41+
*/
42+
@Suppress("unchecked_cast")
43+
fun <T : Texture> Any.texture(index: Int) =
44+
textureMap.getValue(this@texture)[index] as T
3245

3346
/**
34-
* Generate mipmap texture from data and associate it with its owner
47+
* Binds a list of textures to texture slots, ensuring no more than 32 textures
48+
* are bound at once (to fit within the typical GPU limitations)
49+
*
50+
* @param textures The list of objects that own textures to be bound.
51+
* @throws IllegalArgumentException If more than 32 textures are provided.
52+
*/
53+
fun bind(vararg textures: Any) {
54+
check(textures.size < 33) { "Texture slot overflow, expected to use less than 33 slots, got ${textures.size} slots" }
55+
56+
textures.forEachIndexed { index, texture -> texture.texture.bind(index) }
57+
}
58+
59+
/**
60+
* Binds a list of textures to texture slots, ensuring no more than 32 textures
61+
* are bound at once (to fit within the typical GPU limitations)
62+
*
63+
* @param textures The list of textures to be bound
64+
* @throws IllegalArgumentException If more than 32 textures are provided
65+
*/
66+
fun bind(vararg textures: Texture) {
67+
check(textures.size < 33) { "Texture slot overflow, expected to use less than 33 slots, got ${textures.size} slots" }
68+
69+
textures.forEachIndexed { index, texture -> texture.bind(index) }
70+
}
71+
72+
/**
73+
* Uploads a texture from image data and associates it with the object,
74+
* optionally generating mipmaps for the texture
75+
*
76+
* @param data The image data as a [BufferedImage] to create the texture
77+
* @param mipmaps The number of mipmaps to generate for the texture (default is 1)
78+
* @return The created texture object
3579
*/
3680
fun Any.upload(data: BufferedImage, mipmaps: Int = 1) =
37-
Texture(data, levels = mipmaps).also { textureMap[this@upload] = it }
81+
Texture(data, levels = mipmaps).also { textureMap.computeIfAbsent(this@upload) { mutableListOf() }.add(it) }
3882

3983
/**
40-
* Generate mipmap texture from data and associate it with its owner
84+
* Uploads a texture from an image file path and associates it with the object,
85+
* optionally generating mipmaps for the texture
4186
*
42-
* @param path Lambda resource path containing the image data
87+
* @param path The resource path to the image file
88+
* @param mipmaps The number of mipmaps to generate for the texture (default is 1)
89+
* @return The created texture object
4390
*/
4491
fun Any.upload(path: String, mipmaps: Int = 1) =
45-
Texture(path.readImage(), levels = mipmaps).also { textureMap[this@upload] = it }
92+
Texture(path.readImage(), levels = mipmaps).also { textureMap.computeIfAbsent(this@upload) { mutableListOf() }.add(it) }
4693

4794
/**
48-
* Loads a gif and associate it with its owner
95+
* Uploads a distance field texture from image data and associates it with the object
96+
* Distance field textures are commonly used for rendering fonts.
97+
*
98+
* @param data The image data as a [BufferedImage] to create the distance field texture
99+
* @return The created distance field texture object
100+
*/
101+
fun Any.uploadField(data: BufferedImage) =
102+
DistanceFieldTexture(data).also { textureMap.computeIfAbsent(this@uploadField) { mutableListOf() }.add(it) }
103+
104+
/**
105+
* Uploads a GIF and associates it with the object as an animated texture
106+
*
107+
* @param path The resource path to the GIF file
108+
* @return The created animated texture object
49109
*/
50110
fun Any.uploadGif(path: String) =
51-
AnimatedTexture(path).also { textureMap[this@uploadGif] = it }
111+
AnimatedTexture(path).also { textureMap.computeIfAbsent(this@uploadGif) { mutableListOf() }.add(it) }
52112
}

0 commit comments

Comments
 (0)