@@ -26,76 +26,129 @@ public class Font {
26
26
private final Int2ObjectOpenHashMap <CharData > charMap = new Int2ObjectOpenHashMap <>();
27
27
private static final int size = 2048 ;
28
28
29
+ private final STBTTFontinfo fontInfo ;
30
+ private final ByteBuffer fontBuffer ;
31
+ private final ByteBuffer bitmap ;
32
+ private int currentX = 0 ;
33
+ private int currentY = 0 ;
34
+ private int rowHeight = 0 ;
35
+ private static final int PADDING = 2 ;
36
+
37
+ // track dirty regions for partial texture updates
38
+ private int dirtyMinX = Integer .MAX_VALUE ;
39
+ private int dirtyMinY = Integer .MAX_VALUE ;
40
+ private int dirtyMaxX = 0 ;
41
+ private int dirtyMaxY = 0 ;
42
+ private boolean needsTextureUpdate = false ;
43
+
29
44
public Font (ByteBuffer buffer , int height ) {
30
45
this .height = height ;
46
+ this .fontBuffer = buffer ;
31
47
32
- // Initialize font
33
- STBTTFontinfo fontInfo = STBTTFontinfo .create ();
48
+ // init
49
+ fontInfo = STBTTFontinfo .create ();
34
50
STBTruetype .stbtt_InitFont (fontInfo , buffer );
35
51
36
- // Allocate buffers
37
- ByteBuffer bitmap = BufferUtils .createByteBuffer (size * size );
38
- STBTTPackedchar .Buffer [] cdata = {
39
- STBTTPackedchar .create (95 ), // Basic Latin
40
- STBTTPackedchar .create (96 ), // Latin 1 Supplement
41
- STBTTPackedchar .create (128 ), // Latin Extended-A
42
- STBTTPackedchar .create (144 ), // Greek and Coptic
43
- STBTTPackedchar .create (256 ), // Cyrillic
44
- STBTTPackedchar .create (1 ) // infinity symbol
45
- };
46
-
47
- // create and initialise packing context
48
- STBTTPackContext packContext = STBTTPackContext .create ();
49
- STBTruetype .stbtt_PackBegin (packContext , bitmap , size , size , 0 ,1 );
50
-
51
- // create the pack range, populate with the specific packing ranges
52
- STBTTPackRange .Buffer packRange = STBTTPackRange .create (cdata .length );
53
- packRange .put (STBTTPackRange .create ().set (height , 32 , null , 95 , cdata [0 ], (byte ) 2 , (byte ) 2 ));
54
- packRange .put (STBTTPackRange .create ().set (height , 160 , null , 96 , cdata [1 ], (byte ) 2 , (byte ) 2 ));
55
- packRange .put (STBTTPackRange .create ().set (height , 256 , null , 128 , cdata [2 ], (byte ) 2 , (byte ) 2 ));
56
- packRange .put (STBTTPackRange .create ().set (height , 880 , null , 144 , cdata [3 ], (byte ) 2 , (byte ) 2 ));
57
- packRange .put (STBTTPackRange .create ().set (height , 1024 , null , 256 , cdata [4 ], (byte ) 2 , (byte ) 2 ));
58
- packRange .put (STBTTPackRange .create ().set (height , 8734 , null , 1 , cdata [5 ], (byte ) 2 , (byte ) 2 )); // lol
59
- packRange .flip ();
60
-
61
- // write and finish
62
- STBTruetype .stbtt_PackFontRanges (packContext , buffer , 0 , packRange );
63
- STBTruetype .stbtt_PackEnd (packContext );
64
-
65
- // Create texture object and get font scale
52
+ bitmap = BufferUtils .createByteBuffer (size * size );
53
+
66
54
texture = new Texture (size , size , TextureFormat .RED8 , FilterMode .LINEAR , FilterMode .LINEAR );
67
- texture .upload (bitmap );
68
55
scale = STBTruetype .stbtt_ScaleForPixelHeight (fontInfo , height );
69
56
70
- // Get font vertical ascent
57
+ // get font vertical ascent
71
58
try (MemoryStack stack = MemoryStack .stackPush ()) {
72
59
IntBuffer ascent = stack .mallocInt (1 );
73
60
STBTruetype .stbtt_GetFontVMetrics (fontInfo , ascent , null , null );
74
61
this .ascent = ascent .get (0 );
75
62
}
76
63
77
- for (int i = 0 ; i < cdata .length ; i ++) {
78
- STBTTPackedchar .Buffer cbuf = cdata [i ];
79
- int offset = packRange .get (i ).first_unicode_codepoint_in_range ();
80
-
81
- for (int j = 0 ; j < cbuf .capacity (); j ++) {
82
- STBTTPackedchar packedChar = cbuf .get (j );
83
-
84
- float ipw = 1f / size ; // pixel width and height
85
- float iph = 1f / size ;
86
-
87
- charMap .put (j + offset , new CharData (
88
- packedChar .xoff (),
89
- packedChar .yoff (),
90
- packedChar .xoff2 (),
91
- packedChar .yoff2 (),
92
- packedChar .x0 () * ipw ,
93
- packedChar .y0 () * iph ,
94
- packedChar .x1 () * ipw ,
95
- packedChar .y1 () * iph ,
96
- packedChar .xadvance ()
97
- ));
64
+ // preload only basic ascii
65
+ preloadCharacterRange (32 , 126 );
66
+ preloadCharacterRange (160 , 255 );
67
+
68
+ texture .upload (bitmap );
69
+ }
70
+
71
+ private void preloadCharacterRange (int start , int end ) {
72
+ for (int cp = start ; cp <= end ; cp ++) {
73
+ loadCharacter (cp );
74
+ }
75
+ }
76
+
77
+ private void loadCharacter (int codepoint ) {
78
+ if (charMap .containsKey (codepoint )) return ;
79
+
80
+ try (MemoryStack stack = MemoryStack .stackPush ()) {
81
+ IntBuffer width = stack .mallocInt (1 );
82
+ IntBuffer height = stack .mallocInt (1 );
83
+ IntBuffer xoff = stack .mallocInt (1 );
84
+ IntBuffer yoff = stack .mallocInt (1 );
85
+
86
+ ByteBuffer charBitmap = STBTruetype .stbtt_GetCodepointBitmap (
87
+ fontInfo , scale , scale , codepoint , width , height , xoff , yoff
88
+ );
89
+
90
+ if (charBitmap == null ) {
91
+ if (codepoint != 32 && charMap .containsKey (32 )) {
92
+ charMap .put (codepoint , charMap .get (32 ));
93
+ }
94
+ return ;
95
+ }
96
+
97
+ int w = width .get (0 );
98
+ int h = height .get (0 );
99
+
100
+ if (currentX + w + PADDING > size ) {
101
+ currentX = 0 ;
102
+ currentY += rowHeight + PADDING ;
103
+ rowHeight = 0 ;
104
+ }
105
+
106
+ // if we're out of texture space
107
+ if (currentY + h + PADDING > size ) {
108
+ STBTruetype .stbtt_FreeBitmap (charBitmap , 0L );
109
+ return ;
98
110
}
111
+
112
+ // cp char bitmap to main bitmap
113
+ for (int y = 0 ; y < h ; y ++) {
114
+ for (int x = 0 ; x < w ; x ++) {
115
+ int srcIdx = y * w + x ;
116
+ int dstIdx = (currentY + y ) * size + (currentX + x );
117
+ bitmap .put (dstIdx , charBitmap .get (srcIdx ));
118
+ }
119
+ }
120
+
121
+ // Update dirty region bounds
122
+ dirtyMinX = Math .min (dirtyMinX , currentX );
123
+ dirtyMinY = Math .min (dirtyMinY , currentY );
124
+ dirtyMaxX = Math .max (dirtyMaxX , currentX + w );
125
+ dirtyMaxY = Math .max (dirtyMaxY , currentY + h );
126
+
127
+ IntBuffer advanceWidth = stack .mallocInt (1 );
128
+ STBTruetype .stbtt_GetCodepointHMetrics (fontInfo , codepoint , advanceWidth , null );
129
+
130
+ float ipw = 1f / size ;
131
+ float iph = 1f / size ;
132
+
133
+ float u0 = currentX * ipw ;
134
+ float v0 = currentY * iph ;
135
+ float u1 = (currentX + w ) * ipw ;
136
+ float v1 = (currentY + h ) * iph ;
137
+
138
+ // save char data
139
+ charMap .put (codepoint , new CharData (
140
+ xoff .get (0 ),
141
+ yoff .get (0 ),
142
+ xoff .get (0 ) + w ,
143
+ yoff .get (0 ) + h ,
144
+ u0 , v0 , u1 , v1 ,
145
+ advanceWidth .get (0 ) * scale
146
+ ));
147
+
148
+ currentX += w + PADDING ;
149
+ rowHeight = Math .max (rowHeight , h );
150
+
151
+ STBTruetype .stbtt_FreeBitmap (charBitmap , 0L );
99
152
}
100
153
}
101
154
@@ -104,8 +157,17 @@ public double getWidth(String string, int length) {
104
157
105
158
for (int i = 0 ; i < length ; i ++) {
106
159
int cp = string .charAt (i );
160
+
161
+ // load not loaded char
162
+ if (!charMap .containsKey (cp )) {
163
+ loadCharacter (cp );
164
+ }
165
+
107
166
CharData c = charMap .get (cp );
108
- if (c == null ) c = charMap .get (32 );
167
+ if (c == null ) {
168
+ c = charMap .get (32 ); // fallback
169
+ if (c == null ) continue ;
170
+ }
109
171
110
172
width += c .xAdvance ;
111
173
}
@@ -123,10 +185,23 @@ public double render(MeshBuilder mesh, String string, double x, double y, Color
123
185
int length = string .length ();
124
186
mesh .ensureCapacity (length * 4 , length * 6 );
125
187
188
+ // load all missing chars
189
+ for (int i = 0 ; i < length ; i ++) {
190
+ int cp = string .charAt (i );
191
+ if (!charMap .containsKey (cp )) {
192
+ loadCharacter (cp );
193
+ needsTextureUpdate = true ;
194
+ }
195
+ }
196
+
197
+ // render chars
126
198
for (int i = 0 ; i < length ; i ++) {
127
199
int cp = string .charAt (i );
128
200
CharData c = charMap .get (cp );
129
- if (c == null ) c = charMap .get (32 );
201
+ if (c == null ) {
202
+ c = charMap .get (32 );
203
+ if (c == null ) continue ;
204
+ }
130
205
131
206
mesh .quad (
132
207
mesh .vec2 (x + c .x0 * scale , y + c .y0 * scale ).vec2 (c .u0 , c .v0 ).color (color ).next (),
@@ -141,5 +216,25 @@ public double render(MeshBuilder mesh, String string, double x, double y, Color
141
216
return x ;
142
217
}
143
218
219
+ public void updateTextureIfNeeded () {
220
+ if (needsTextureUpdate ) {
221
+ if (dirtyMinX < dirtyMaxX && dirtyMinY < dirtyMaxY ) {
222
+ int width = dirtyMaxX - dirtyMinX ;
223
+ int height = dirtyMaxY - dirtyMinY ;
224
+ texture .uploadRegion (bitmap , dirtyMinX , dirtyMinY , width , height );
225
+ } else {
226
+ texture .upload (bitmap );
227
+ }
228
+
229
+ needsTextureUpdate = false ;
230
+
231
+ // reset dirty region
232
+ dirtyMinX = Integer .MAX_VALUE ;
233
+ dirtyMinY = Integer .MAX_VALUE ;
234
+ dirtyMaxX = 0 ;
235
+ dirtyMaxY = 0 ;
236
+ }
237
+ }
238
+
144
239
private record CharData (float x0 , float y0 , float x1 , float y1 , float u0 , float v0 , float u1 , float v1 , float xAdvance ) {}
145
240
}
0 commit comments