// GLPixelFont2 provides meshes to render basic ASCII characters in OpenGL. // The font is fixed-width 5x7, and contains [0,9], uppercase [A,Z] and - . : = ? // Sample: https://giphy.com/gifs/text-opengl-pixelfont-BZiUFIQN3gGlnS7fie // Ulf Benjaminsson, 2019-03-15, ulfbenjaminsson.com // The first GLPixelFont stored "pixels" as char '1' / '0'. A char is 2 bytes in Java, so: // 46 (characters) * 7 (height) * 5 (width) * 16 (bits) = 25760 bit, or 3220 bytes // See: https://pastebin.com/MA7ZuB6Q // Version 2 stores "pixels" as bits in a byte array, so: // 46 (characters) * 7 (height) * 8 (width) * 1 (bits) = 2576 bit, or 322 byte // Ergo: 10x less memory. public class GLPixelFont2 { private static final int HEIGHT = 7; //characters are 7 units tall private static final int WIDTH = 8; //characters are 8 units wide private static final int CHAR_COUNT = 46; //the font definition contains 46 entries private static final int OFFSET = 45; //it start at ASCII code 45 "-", and ends at 90 "Z". private static final float SCALE = 0.75f; //scale vertex coordinates. private final Mesh[] _glyphs = new Mesh[CHAR_COUNT]; //a vertex buffer for each glyph, for rendering with OpenGL. public GLPixelFont2() { for (int c = 0; c < CHAR_COUNT; c++) { _glyphs[c] = null; } } //invalid characters will be null - make sure your renderer skips those! public Mesh[] getString(final String s){ final int count = s.length(); final Mesh[] result = new Mesh[count]; for(int i = 0; i < count; i++){ result[i] = getChar(s.charAt(i)); } return result; } public Mesh getChar(char c){ c = Character.toUpperCase(c); if(c < OFFSET || c >= OFFSET+CHAR_COUNT){ return null; } final int i = c - OFFSET; if(_glyphs[i] == null){ _glyphs[i] = createMeshForGlyph(c); } return _glyphs[i]; } private Mesh createMeshForGlyph(final char c){ assert(c >= OFFSET && c < OFFSET+CHAR_COUNT); final int charIndex = c-OFFSET; final float z = 0; final float[] vertices = new float[HEIGHT*WIDTH*Mesh.COORDS_PER_VERTEX]; int i = 0; for (int y = 0; y < HEIGHT; y++) { for (int x = 0; x < WIDTH; x++) { final int posByte = HEIGHT * charIndex + (x/WIDTH) + y; //thank Bog for Wolfram Alpha! final int posBit = x; final int bit = (BITMAP[posByte] >> (7-posBit) & 0x01); if((BITMAP[posByte] >> (7-posBit) & 0x01) == 0){continue;} vertices[i++] = x*SCALE; vertices[i++] = -y*SCALE; //OpenGL has inverted y-axis. vertices[i++] = z; } } final float[] clean = Arrays.copyOfRange(vertices, 0, i); return new Mesh(clean, GLES20.GL_POINTS); } private static final byte[] BITMAP = { /*[ind asc sym]*/ /*[0 45 '-']*/ 0x0, 0x0, 0x0, 0x1f, 0x0, 0x0, 0x0, /*[1 46 '.']*/ 0x0, 0x0, 0x0, 0x0, 0x0, 0xc, 0xc, /*[2 47 '/']*/ 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, /*[3 48 '0']*/ 0xe, 0x11, 0x13, 0x15, 0x19, 0x11, 0xe, /*[4 49 '1']*/ 0x4, 0xc, 0x4, 0x4, 0x4, 0x4, 0xe, /*[5 50 '2']*/ 0xe, 0x11, 0x1, 0x2, 0x4, 0x8, 0x1f, /*[6 51 '3']*/ 0xe, 0x11, 0x1, 0x6, 0x1, 0x11, 0xe, /*[7 52 '4']*/ 0x2, 0x6, 0xa, 0x12, 0x1f, 0x2, 0x7, /*[8 53 '5']*/ 0x1f, 0x10, 0x1e, 0x1, 0x1, 0x11, 0xe, /*[9 54 '6']*/ 0xe, 0x11, 0x10, 0x1e, 0x11, 0x11, 0xe, /*[10 55 '7']*/ 0x1f, 0x11, 0x2, 0x2, 0x4, 0x4, 0x4, /*[11 56 '8']*/ 0xe, 0x11, 0x11, 0xe, 0x11, 0x11, 0xe, /*[12 57 '9']*/ 0xe, 0x11, 0x11, 0xf, 0x1, 0x1, 0xe, /*[13 58 ':']*/ 0x0, 0xc, 0xc, 0x0, 0xc, 0xc, 0x0, /*[14 59 ';']*/ 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, /*[15 60 '<']*/ 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, /*[16 61 '=']*/ 0x0, 0x0, 0x1f, 0x0, 0x1f, 0x0, 0x0, /*[17 62 '>']*/ 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, /*[18 63 '?']*/ 0xe, 0x11, 0x11, 0x2, 0x4, 0x0, 0x4, /*[19 64 '@']*/ 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, /*[20 65 'A']*/ 0xe, 0x11, 0x11, 0x1f, 0x11, 0x11, 0x11, /*[21 66 'B']*/ 0x1e, 0x11, 0x11, 0x1e, 0x11, 0x11, 0x1e, /*[22 67 'C']*/ 0xe, 0x11, 0x10, 0x10, 0x10, 0x11, 0xe, /*[23 68 'D']*/ 0x1e, 0x11, 0x11, 0x11, 0x11, 0x11, 0x1e, /*[24 69 'E']*/ 0x1f, 0x10, 0x10, 0x1e, 0x10, 0x10, 0x1f, /*[25 70 'F']*/ 0x1f, 0x10, 0x10, 0x1e, 0x10, 0x10, 0x10, /*[26 71 'G']*/ 0xe, 0x11, 0x10, 0x17, 0x11, 0x11, 0xe, /*[27 72 'H']*/ 0x11, 0x11, 0x11, 0x1f, 0x11, 0x11, 0x11, /*[28 73 'I']*/ 0xe, 0x4, 0x4, 0x4, 0x4, 0x4, 0xe, /*[29 74 'J']*/ 0x1, 0x1, 0x1, 0x1, 0x11, 0x11, 0xe, /*[30 75 'K']*/ 0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11, /*[31 76 'L']*/ 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1f, /*[32 77 'M']*/ 0x11, 0x1b, 0x15, 0x15, 0x11, 0x11, 0x11, /*[33 78 'N']*/ 0x11, 0x11, 0x19, 0x15, 0x13, 0x11, 0x11, /*[34 79 'O']*/ 0xe, 0x11, 0x11, 0x11, 0x11, 0x11, 0xe, /*[35 80 'P']*/ 0x1e, 0x11, 0x11, 0x1e, 0x10, 0x10, 0x10, /*[36 81 'Q']*/ 0xe, 0x11, 0x11, 0x11, 0x15, 0x12, 0xd, /*[37 82 'R']*/ 0x1e, 0x11, 0x11, 0x1e, 0x14, 0x12, 0x11, /*[38 83 'S']*/ 0xf, 0x10, 0x10, 0xe, 0x1, 0x1, 0x1e, /*[39 84 'T']*/ 0x1f, 0x4, 0x4, 0x4, 0x4, 0x4, 0x4, /*[40 85 'U']*/ 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0xe, /*[41 86 'V']*/ 0x11, 0x11, 0x11, 0x11, 0x11, 0xa, 0x4, /*[42 87 'W']*/ 0x11, 0x11, 0x11, 0x15, 0x15, 0x15, 0xa, /*[43 88 'X']*/ 0x11, 0x11, 0xa, 0x4, 0xa, 0x11, 0x11, /*[44 89 'Y']*/ 0x11, 0x11, 0x11, 0xa, 0x4, 0x4, 0x4, /*[45 90 'Z']*/ 0x1f, 0x1, 0x2, 0x4, 0x8, 0x10, 0x1f }; } //Mesh.java, to save your struggles. :) public class Mesh { private static final String TAG = "Mesh"; public static final int SIZE_OF_FLOAT = Float.SIZE/Byte.SIZE; //32bit/8bit = 4 bytes public static final int COORDS_PER_VERTEX = 3; //X, Y, Z public static final int VERTEX_STRIDE = COORDS_PER_VERTEX * SIZE_OF_FLOAT; // number of bytes per vertex public FloatBuffer _vertexBuffer = null; public int _vertexCount = 0; public int _drawMode = GLES20.GL_TRIANGLES; public Mesh(final float[] geometry){ init(geometry, GLES20.GL_TRIANGLES); } public Mesh(final float[] geometry, final int drawMode){ init(geometry, drawMode); } private void init(final float[] geometry, final int drawMode){ setVertices(geometry); setDrawmode(drawMode); } public void setDrawmode(int drawMode){ assert(drawMode == GLES20.GL_TRIANGLES || drawMode == GLES20.GL_LINES || drawMode == GLES20.GL_POINTS); _drawMode = drawMode; } public void setVertices(final float[] geometry){ // create a floating point buffer from a ByteBuffer _vertexBuffer = ByteBuffer.allocateDirect(geometry.length * SIZE_OF_FLOAT) .order(ByteOrder.nativeOrder()) // use the device hardware's native byte order .asFloatBuffer(); _vertexBuffer.put(geometry); //add the coordinates to the FloatBuffer _vertexBuffer.position(0); // set the buffer to read the first coordinate _vertexCount = geometry.length / COORDS_PER_VERTEX; } }