The LWJGL3 library contains bindings to STB TrueType and other libraries made by Sean Barrett.
In order to understand and use that library I wanted to convert the code in the oversampling demo from using...
stbtt_PackFontRange(pc, ttf, 0, 16, 32, chardata);
...to instead using...
STBTTPackRange.Buffer packRanges = STBTTPackRange.malloc(1);
packRanges.put(STBTTPackRange.malloc().set(16, 32, null, 96, chardata));
...
stbtt_PackFontRanges(pc, ttf, 0, packRanges);
...which yields an empty texture.
From the understanding I could gather by reading the documentations and looking at the examples in the stb repo as well as in the lwjgl repo what I am doing should be working, yet it does not.
I modified the below class from the original ~1 line at a time to the point where I switched from using the stbtt_PackFontRange(...)
method to using the stbtt_PackFontRanges(...)
method.
The full load_fonts()
method for context:
private void load_fonts() {
font_tex = glGenTextures();
chardata = STBTTPackedchar.malloc(96);
chardataIndices = new HashMap<>();
for(int i = 0 ; i < chardata.remaining() ; i++) {
chardataIndices.put(i + 32, i);
}
STBTTPackRange.Buffer packRanges = STBTTPackRange.malloc(1);
packRanges.put(STBTTPackRange.malloc().set(16, 32, null, 96, chardata));
try (STBTTPackContext pc = STBTTPackContext.malloc()) {
ByteBuffer ttf = ioResourceToByteBuffer("demo/monof55.ttf", 512 * 1024);
ByteBuffer bitmap = BufferUtils.createByteBuffer(BITMAP_W * BITMAP_H);
stbtt_PackBegin(pc, bitmap, BITMAP_W, BITMAP_H, 0, 1, NULL);
stbtt_PackSetOversampling(pc, 1, 1);
//below method works
stbtt_PackFontRange(pc, ttf, 0, 16, 32, chardata);
//below method works not
//stbtt_PackFontRanges(pc, ttf, 0, packRanges);
stbtt_PackEnd(pc);
glBindTexture(GL_TEXTURE_2D, font_tex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, BITMAP_W, BITMAP_H, 0, GL_ALPHA, GL_UNSIGNED_BYTE, bitmap);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
The complete modified TruetypeOversample
class, can be easiest executed by cloning the lwjgl3 repo and replacing the class with the same name:
/*
* Copyright LWJGL. All rights reserved.
* License terms: https://www.lwjgl.org/license
*/
package org.lwjgl.demo.stb;
import static org.lwjgl.demo.glfw.GLFWUtil.glfwInvoke;
import static org.lwjgl.demo.util.IOUtil.ioResourceToByteBuffer;
import static org.lwjgl.glfw.Callbacks.glfwFreeCallbacks;
import static org.lwjgl.glfw.GLFW.GLFW_FALSE;
import static org.lwjgl.glfw.GLFW.GLFW_KEY_B;
import static org.lwjgl.glfw.GLFW.GLFW_KEY_ESCAPE;
import static org.lwjgl.glfw.GLFW.GLFW_KEY_V;
import static org.lwjgl.glfw.GLFW.GLFW_RELEASE;
import static org.lwjgl.glfw.GLFW.GLFW_RESIZABLE;
import static org.lwjgl.glfw.GLFW.GLFW_TRUE;
import static org.lwjgl.glfw.GLFW.GLFW_VISIBLE;
import static org.lwjgl.glfw.GLFW.glfwCreateWindow;
import static org.lwjgl.glfw.GLFW.glfwDefaultWindowHints;
import static org.lwjgl.glfw.GLFW.glfwGetPrimaryMonitor;
import static org.lwjgl.glfw.GLFW.glfwGetVideoMode;
import static org.lwjgl.glfw.GLFW.glfwInit;
import static org.lwjgl.glfw.GLFW.glfwMakeContextCurrent;
import static org.lwjgl.glfw.GLFW.glfwPollEvents;
import static org.lwjgl.glfw.GLFW.glfwSetErrorCallback;
import static org.lwjgl.glfw.GLFW.glfwSetFramebufferSizeCallback;
import static org.lwjgl.glfw.GLFW.glfwSetKeyCallback;
import static org.lwjgl.glfw.GLFW.glfwSetWindowPos;
import static org.lwjgl.glfw.GLFW.glfwSetWindowShouldClose;
import static org.lwjgl.glfw.GLFW.glfwSetWindowSizeCallback;
import static org.lwjgl.glfw.GLFW.glfwShowWindow;
import static org.lwjgl.glfw.GLFW.glfwSwapBuffers;
import static org.lwjgl.glfw.GLFW.glfwSwapInterval;
import static org.lwjgl.glfw.GLFW.glfwTerminate;
import static org.lwjgl.glfw.GLFW.glfwWindowHint;
import static org.lwjgl.glfw.GLFW.glfwWindowShouldClose;
import static org.lwjgl.opengl.GL11.GL_ALPHA;
import static org.lwjgl.opengl.GL11.GL_BLEND;
import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT;
import static org.lwjgl.opengl.GL11.GL_CULL_FACE;
import static org.lwjgl.opengl.GL11.GL_DEPTH_TEST;
import static org.lwjgl.opengl.GL11.GL_LIGHTING;
import static org.lwjgl.opengl.GL11.GL_LINEAR;
import static org.lwjgl.opengl.GL11.GL_MODELVIEW;
import static org.lwjgl.opengl.GL11.GL_ONE_MINUS_SRC_ALPHA;
import static org.lwjgl.opengl.GL11.GL_PROJECTION;
import static org.lwjgl.opengl.GL11.GL_QUADS;
import static org.lwjgl.opengl.GL11.GL_SRC_ALPHA;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_2D;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_MAG_FILTER;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_MIN_FILTER;
import static org.lwjgl.opengl.GL11.GL_UNSIGNED_BYTE;
import static org.lwjgl.opengl.GL11.glBegin;
import static org.lwjgl.opengl.GL11.glBindTexture;
import static org.lwjgl.opengl.GL11.glBlendFunc;
import static org.lwjgl.opengl.GL11.glClear;
import static org.lwjgl.opengl.GL11.glClearColor;
import static org.lwjgl.opengl.GL11.glColor3f;
import static org.lwjgl.opengl.GL11.glDisable;
import static org.lwjgl.opengl.GL11.glEnable;
import static org.lwjgl.opengl.GL11.glEnd;
import static org.lwjgl.opengl.GL11.glGenTextures;
import static org.lwjgl.opengl.GL11.glLoadIdentity;
import static org.lwjgl.opengl.GL11.glMatrixMode;
import static org.lwjgl.opengl.GL11.glOrtho;
import static org.lwjgl.opengl.GL11.glTexCoord2f;
import static org.lwjgl.opengl.GL11.glTexImage2D;
import static org.lwjgl.opengl.GL11.glTexParameteri;
import static org.lwjgl.opengl.GL11.glTranslatef;
import static org.lwjgl.opengl.GL11.glVertex2f;
import static org.lwjgl.opengl.GL11.glViewport;
import static org.lwjgl.stb.STBTruetype.stbtt_GetPackedQuad;
import static org.lwjgl.stb.STBTruetype.stbtt_PackBegin;
import static org.lwjgl.stb.STBTruetype.stbtt_PackEnd;
import static org.lwjgl.stb.STBTruetype.stbtt_PackFontRange;
import static org.lwjgl.stb.STBTruetype.stbtt_PackSetOversampling;
import static org.lwjgl.system.MemoryUtil.NULL;
import static org.lwjgl.system.MemoryUtil.memAllocFloat;
import static org.lwjgl.system.MemoryUtil.memFree;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.util.HashMap;
import java.util.Map;
import org.lwjgl.BufferUtils;
import org.lwjgl.glfw.GLFWErrorCallback;
import org.lwjgl.glfw.GLFWVidMode;
import org.lwjgl.opengl.GL;
import org.lwjgl.opengl.GLUtil;
import org.lwjgl.stb.STBTTAlignedQuad;
import org.lwjgl.stb.STBTTPackContext;
import org.lwjgl.stb.STBTTPackRange;
import org.lwjgl.stb.STBTTPackedchar;
import org.lwjgl.system.Callback;
/**
* STB Truetype oversampling demo.
*
* <p>This is a Java port of <a href="https://github.com/nothings/stb/blob/master/tests/oversample/main.c">https://github
* .com/nothings/stb/blob/master/tests/oversample/main.c</a>.</p>
*/
public final class TruetypeOversample {
private static final int BITMAP_W = 1024;
private static final int BITMAP_H = 1024;
// ----
private final STBTTAlignedQuad q = STBTTAlignedQuad.malloc();
private final FloatBuffer xb = memAllocFloat(1);
private final FloatBuffer yb = memAllocFloat(1);
private long window;
private Callback debugProc;
// ----
private int ww = 1600;
private int wh = 768;
private int fbw = ww;
private int fbh = wh;
private int font_tex;
private STBTTPackedchar.Buffer chardata;
//Map<char, index_in_chardata_buffer>
private Map<Integer, Integer> chardataIndices;
private boolean black_on_white;
private boolean show_tex;
private TruetypeOversample() {
}
public static void main(String[] args) {
new TruetypeOversample().run("STB Truetype Oversample Demo");
}
private void load_fonts() {
font_tex = glGenTextures();
chardata = STBTTPackedchar.malloc(96);
chardataIndices = new HashMap<>();
for(int i = 0 ; i < chardata.remaining() ; i++) {
chardataIndices.put(i + 32, i);
}
STBTTPackRange.Buffer packRanges = STBTTPackRange.malloc(1);
packRanges.put(STBTTPackRange.malloc().set(
16,
32,
null,
96,
chardata));
try (STBTTPackContext pc = STBTTPackContext.malloc()) {
ByteBuffer ttf = ioResourceToByteBuffer("demo/monof55.ttf", 512 * 1024);
ByteBuffer bitmap = BufferUtils.createByteBuffer(BITMAP_W * BITMAP_H);
stbtt_PackBegin(pc, bitmap, BITMAP_W, BITMAP_H, 0, 1, NULL);
stbtt_PackSetOversampling(pc, 1, 1);
//below method works
stbtt_PackFontRange(pc, ttf, 0, 16, 32, chardata);
//below method works not
// stbtt_PackFontRanges(pc, ttf, 0, packRanges);
stbtt_PackEnd(pc);
glBindTexture(GL_TEXTURE_2D, font_tex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, BITMAP_W, BITMAP_H, 0, GL_ALPHA, GL_UNSIGNED_BYTE, bitmap);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void draw_init() {
glDisable(GL_CULL_FACE);
glDisable(GL_TEXTURE_2D);
glDisable(GL_LIGHTING);
glDisable(GL_DEPTH_TEST);
glViewport(0, 0, fbw, fbh);
if (black_on_white) {
glClearColor(1.0f, 1.0f, 1.0f, 0.0f);
} else {
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
}
glClear(GL_COLOR_BUFFER_BIT);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(0.0, ww, wh, 0.0, -1.0, 1.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}
private static void drawBoxTC(float x0, float y0, float x1, float y1, float s0, float t0, float s1, float t1) {
glTexCoord2f(s0, t0);
glVertex2f(x0, y0);
glTexCoord2f(s1, t0);
glVertex2f(x1, y0);
glTexCoord2f(s1, t1);
glVertex2f(x1, y1);
glTexCoord2f(s0, t1);
glVertex2f(x0, y1);
}
private void print(float x, float y, int font, String text) {
xb.put(0, x);
yb.put(0, y);
chardata.position(0);
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, font_tex);
glBegin(GL_QUADS);
for (int i = 0; i < text.length(); i++) {
stbtt_GetPackedQuad(
chardata,
BITMAP_W, BITMAP_H,
chardataIndices.get((int)text.charAt(i)),
xb, yb,
q,
true);
drawBoxTC(
q.x0(), q.y0(), q.x1(), q.y1(),
q.s0(), q.t0(), q.s1(), q.t1()
);
}
glEnd();
}
private void draw_world() {
float x = 20;
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
if (black_on_white) {
glColor3f(0.0f, 0.0f, 0.0f);
} else {
glColor3f(1.0f, 1.0f, 1.0f);
}
print(80, 30, 0, "Controls:");
if (black_on_white) {
print(100, 210, 0, "B: toggle to white-on-black");
} else {
print(100, 210, 0, "B: toggle to black-on-white");
}
print(100, 235, 0, "V: view font texture");
print(80, 300, 0, "Current fontsize: 16 pixels");
if (show_tex) {
glBegin(GL_QUADS);
drawBoxTC(200, 400, 200 + BITMAP_W, 300 + BITMAP_H, 0, 0, 1, 1);
glEnd();
} else {
glMatrixMode(GL_MODELVIEW);
glTranslatef(200, 350, 0);
print(x, 100, 0, "This is a test");
print(x, 130, 0, "Now is the time for all good men to come to the aid of their country.");
print(x, 160, 0, "The quick brown fox jumps over the lazy dog.");
}
}
private void draw() {
draw_init();
draw_world();
glfwSwapBuffers(window);
}
private void loopmode(float dt) {
if (dt > 0.25f) {
dt = 0.25f;
}
if (dt < 0.01f) {
dt = 0.01f;
}
draw();
}
private void windowSizeChanged(long window, int width, int height) {
this.ww = width;
this.wh = height;
}
private void framebufferSizeChanged(long window, int width, int height) {
this.fbw = width;
this.fbh = height;
}
private void createWindow(String title) {
GLFWErrorCallback.createPrint().set();
if (!glfwInit()) {
throw new IllegalStateException("Unable to initialize GLFW");
}
glfwDefaultWindowHints();
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE);
this.window = glfwCreateWindow(ww, wh, title, NULL, NULL);
if (window == NULL) {
throw new RuntimeException("Failed to create the GLFW window");
}
glfwSetWindowSizeCallback(window, this::windowSizeChanged);
glfwSetFramebufferSizeCallback(window, this::framebufferSizeChanged);
glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
if (action == GLFW_RELEASE) {
return;
}
switch (key) {
case GLFW_KEY_ESCAPE:
glfwSetWindowShouldClose(window, true);
break;
case GLFW_KEY_V:
show_tex = !show_tex;
break;
case GLFW_KEY_B:
black_on_white = !black_on_white;
break;
}
});
// Center window
GLFWVidMode vidmode = glfwGetVideoMode(glfwGetPrimaryMonitor());
glfwSetWindowPos(
window,
(vidmode.width() - ww) / 2,
(vidmode.height() - wh) / 2
);
// Create context
glfwMakeContextCurrent(window);
GL.createCapabilities();
debugProc = GLUtil.setupDebugMessageCallback();
glfwSwapInterval(1);
glfwShowWindow(window);
glfwInvoke(window, this::windowSizeChanged, this::framebufferSizeChanged);
}
private void run(String title) {
try {
createWindow(title);
load_fonts();
long time = System.nanoTime();
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
long t = System.nanoTime();
float dt = (float)((t - time) / 1000000000.0);
time = t;
loopmode(dt);
}
} finally {
try {
destroy();
} catch (Exception e) {
e.printStackTrace();
}
}
}
private void destroy() {
chardata.free();
if (debugProc != null) {
debugProc.free();
}
glfwFreeCallbacks(window);
glfwTerminate();
glfwSetErrorCallback(null).free();
memFree(yb);
memFree(xb);
q.free();
}
}
Flip the buffer...
packRanges.flip()
Even though the underlying library is written in C, this wrapper is written in Java and uses Java Buffers to pass data to the underlying lib. Thus after writing to the buffer you need to flip()
it to prepare it for reading operations.
It doesn't matter that LWJGL does not modify the buffer.position()
when reading, it will still use the position as a start-point.