Forum Discussion

VelekHacks's avatar
VelekHacks
Honored Guest
24 days ago

Text upside down in OpenGL on Quest

This one has Gemini stumped.  It sent me to learnopengl.com where I got the initial version of the RenderText() function.

I can't get the right transform to draw the text right side up.

#include <android_native_app_glue.h>
#include <EGL/egl.h>
#include <GLES3/gl3.h>
#include <android/log.h>
#include <android/asset_manager.h>

#include <map>
#include <string>
#include <vector>

#include <ft2build.h>
#include FT_FREETYPE_H

#define LOG_TAG "NativeText"
#define ALOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

// --- Text Rendering Data Structures ---
struct Character {
GLuint textureID;
int width;
int height;
int bearingX;
int bearingY;
GLuint advance;
};

std::map<GLchar, Character> Characters;
GLuint textVAO, textVBO;
GLuint textShaderProgram;

// --- Graphics State ---
struct GraphicsContext {
EGLDisplay display = EGL_NO_DISPLAY;
EGLSurface surface = EGL_NO_SURFACE;
EGLContext context = EGL_NO_CONTEXT;
int width = 0;
int height = 0;
};

// --- Shaders for Text Rendering ---
const char* textVertexShaderSource = R"glsl(
#version 300 es
layout (location = 0) in vec4 vertex; // <vec2 pos, vec2 tex>
out vec2 TexCoords;
uniform mat4 projection;
void main() {
gl_Position = projection * vec4(vertex.xy, 0.0, 1.0);
TexCoords = vertex.zw;
}
)glsl";

const char* textFragmentShaderSource = R"glsl(
#version 300 es
precision mediump float;
in vec2 TexCoords;
out vec4 color;
uniform sampler2D text;
uniform vec3 textColor;
void main() {
// Sample the font texture (which is just the red channel for intensity)
vec4 sampled = vec4(1.0, 1.0, 1.0, texture(text, TexCoords).r);
color = vec4(textColor, 1.0) * sampled;
}
)glsl";

// --- Shader Compilation and Error Checking ---
GLuint createShaderProgram(const char* vs_source, const char* fs_source) {
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vs_source, NULL);
glCompileShader(vertexShader);
GLint success;
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success) {
GLchar infoLog[512];
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
ALOGE("Vertex shader compilation failed: %s", infoLog);
}

GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fs_source, NULL);
glCompileShader(fragmentShader);
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if (!success) {
GLchar infoLog[512];
glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
ALOGE("Fragment shader compilation failed: %s", infoLog);
}

GLuint program = glCreateProgram();
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
glLinkProgram(program);
glGetProgramiv(program, GL_LINK_STATUS, &success);
if (!success) {
GLchar infoLog[512];
glGetProgramInfoLog(program, 512, NULL, infoLog);
ALOGE("Shader program linking failed: %s", infoLog);
}

glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
return program;
}

// --- Text Rendering Functions ---
void init_text(AAssetManager* assetManager, int screenWidth, int screenHeight) {
ALOGI("Initializing FreeType and loading font.");
textShaderProgram = createShaderProgram(textVertexShaderSource, textFragmentShaderSource);

float projection[16] = {
2.0f / screenWidth, 0.0f, 0.0f, 0.0f,
0.0f, -2.0f / screenHeight, 0.0f, 0.0f,
0.0f, 0.0f, -1.0f, 0.0f,
-1.0f, 1.0f, 0.0f, 1.0f,
};
glUseProgram(textShaderProgram);
glUniformMatrix4fv(glGetUniformLocation(textShaderProgram, "projection"), 1, GL_FALSE, projection);
glUniform1i(glGetUniformLocation(textShaderProgram, "text"), 0);

FT_Library ft;
if (FT_Init_FreeType(&ft)) ALOGE("Could not init FreeType Library");

const char* fontPath = "fonts/BitterPro-Bold.ttf";
AAsset* fontAsset = AAssetManager_open(assetManager, fontPath, AASSET_MODE_BUFFER);
if (fontAsset == nullptr) {
ALOGE("Failed to open font: %s", fontPath);
return;
}

const void* fontBuffer = AAsset_getBuffer(fontAsset);
off_t fontSize = AAsset_getLength(fontAsset);

FT_Face face;
if (FT_New_Memory_Face(ft, (const FT_Byte*)fontBuffer, fontSize, 0, &face)) {
ALOGE("Failed to load font");
AAsset_close(fontAsset);
return;
}

FT_Set_Pixel_Sizes(face, 0, 48);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

for (GLubyte c = 0; c < 128; c++) {
if (FT_Load_Char(face, c, FT_LOAD_RENDER)) {
ALOGE("Failed to load Glyph for char %c", c);
continue;
}
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, face->glyph->bitmap.width, face->glyph->bitmap.rows, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, face->glyph->bitmap.buffer);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

Character character = { texture, (int)face->glyph->bitmap.width, (int)face->glyph->bitmap.rows, face->glyph->bitmap_left, face->glyph->bitmap_top, (GLuint)face->glyph->advance.x };
Characters.insert(std::pair<GLchar, Character>(c, character));
}
AAsset_close(fontAsset);
FT_Done_Face(face);
FT_Done_FreeType(ft);

glGenVertexArrays(1, &textVAO);
glGenBuffers(1, &textVBO);
glBindVertexArray(textVAO);
glBindBuffer(GL_ARRAY_BUFFER, textVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * 6 * 4, NULL, GL_DYNAMIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), 0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
ALOGI("Font loaded and textures created.");
}

void RenderText(std::string text, GLfloat x, GLfloat y, GLfloat scale, float color[3]) {
glUseProgram(textShaderProgram);
glUniform3f(glGetUniformLocation(textShaderProgram, "textColor"), color[0], color[1], color[2]);
glActiveTexture(GL_TEXTURE0);
glBindVertexArray(textVAO);

for (auto c = text.begin(); c != text.end(); c++) {
Character ch = Characters[*c];

GLfloat xpos = x + ch.bearingX * scale;
GLfloat ypos = y - (ch.height - ch.bearingY) * scale;
GLfloat w = ch.width * scale;
GLfloat h = ch.height * scale;

// Correctly flipped texture coordinates
GLfloat vertices[6][4] = {
{ xpos, ypos + h, 0.0, 1.0 },
{ xpos, ypos, 0.0, 0.0 },
{ xpos + w, ypos, 1.0, 0.0 },

{ xpos, ypos + h, 0.0, 1.0 },
{ xpos + w, ypos, 1.0, 0.0 },
{ xpos + w, ypos + h, 1.0, 1.0 }
};
glBindTexture(GL_TEXTURE_2D, ch.textureID);
glBindBuffer(GL_ARRAY_BUFFER, textVBO);
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glDrawArrays(GL_TRIANGLES, 0, 6);
x += (ch.advance >> 6) * scale;
}
glBindVertexArray(0);
glBindTexture(GL_TEXTURE_2D, 0);
}

// --- EGL/GLES and App Lifecycle ---
void init_graphics(struct android_app* app, GraphicsContext& graphics) {
graphics.display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
eglInitialize(graphics.display, nullptr, nullptr);
const EGLint attribs[] = { EGL_SURFACE_TYPE, EGL_WINDOW_BIT, EGL_BLUE_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_RED_SIZE, 8, EGL_NONE };
EGLConfig config;
EGLint numConfigs;
eglChooseConfig(graphics.display, attribs, &config, 1, &numConfigs);
graphics.surface = eglCreateWindowSurface(graphics.display, config, app->window, nullptr);
const EGLint context_attribs[] = { EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE };
graphics.context = eglCreateContext(graphics.display, config, EGL_NO_CONTEXT, context_attribs);

if (eglMakeCurrent(graphics.display, graphics.surface, graphics.surface, graphics.context) == EGL_FALSE) {
ALOGE("eglMakeCurrent failed.");
return;
}

eglQuerySurface(graphics.display, graphics.surface, EGL_WIDTH, &graphics.width);
eglQuerySurface(graphics.display, graphics.surface, EGL_HEIGHT, &graphics.height);
glViewport(0, 0, graphics.width, graphics.height);

glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

init_text(app->activity->assetManager, graphics.width, graphics.height);
}

void destroy_graphics(GraphicsContext& graphics) {
if (graphics.display != EGL_NO_DISPLAY) {
eglMakeCurrent(graphics.display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
if (graphics.context != EGL_NO_CONTEXT) eglDestroyContext(graphics.display, graphics.context);
if (graphics.surface != EGL_NO_SURFACE) eglDestroySurface(graphics.display, graphics.surface);
eglTerminate(graphics.display);
}
graphics.display = EGL_NO_DISPLAY;
graphics.surface = EGL_NO_SURFACE;
graphics.context = EGL_NO_CONTEXT;
}

static void handle_cmd(struct android_app* app, int32_t cmd) {
auto* graphics = (GraphicsContext*)app->userData;
switch (cmd) {
case APP_CMD_INIT_WINDOW:
if (app->window != nullptr) {
init_graphics(app, *graphics);
}
break;
case APP_CMD_TERM_WINDOW:
destroy_graphics(*graphics);
break;
}
}

void android_main(struct android_app* app) {
GraphicsContext graphics = {};
app->userData = &graphics;
app->onAppCmd = handle_cmd;

while (app->destroyRequested == 0) {
int events;
struct android_poll_source* source;
if (ALooper_pollOnce(-1, nullptr, &events, (void**)&source) >= 0) {
if (source != nullptr) source->process(app, source);
}

if (graphics.display == EGL_NO_DISPLAY) {
continue;
}

glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);

float color[3] = {1.0f, 1.0f, 1.0f};
RenderText("Hello, World!", 25.0f, 50.0f, 1.0f, color);

eglSwapBuffers(graphics.display, graphics.surface);
}

destroy_graphics(graphics);
}

3 Replies

  • Gemini is right, this is not a Quest-Related problem and if it is flipped, the solution might be, because you set it to be that direction. So the general solution would be to learn how to setup the direction correctly.

    Your text is upside-down because the texture’s V (Y) coordinate is flipped the wrong way when you draw the quad.

    You upload the FreeType bitmap directly with:

    glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE,
                 face->glyph->bitmap.width,
                 face->glyph->bitmap.rows,
                 0, GL_LUMINANCE, GL_UNSIGNED_BYTE,
                 face->glyph->bitmap.buffer);

    FreeType’s bitmap buffer is laid out top-to-bottom, but OpenGL reads the rows from the start of the buffer as v = 0 and from the end as v = 1. Because of that, if you pass the buffer as-is, you need to be careful which vertex gets v = 0 and which gets v = 1.

    You currently have:

    GLfloat vertices[6][4] = {
        { xpos,     ypos + h, 0.0, 1.0 }, 
        { xpos,     ypos,     0.0, 0.0 },
        { xpos + w, ypos,     1.0, 0.0 },

        { xpos,     ypos + h, 0.0, 1.0 },
        { xpos + w, ypos,     1.0, 0.0 },
        { xpos + w, ypos + h, 1.0, 1.0 } 
    };

    With your projection matrix, ypos is the top of the glyph and ypos + h is the bottom.
    Right now:

    The top of the quad uses v = 0.0

    The bottom of the quad uses v = 1.0

    Given how the FreeType bitmap is stored, this combination ends up mirroring the glyph vertically.

    Swap the V coordinates (0.0 and 1.0) so the quad uses the “standard” mapping:

    GLfloat vertices[6][4] = {
        // bottom-left
        { xpos,     ypos + h, 0.0, 0.0 },
        // top-left
        { xpos,     ypos,     0.0, 1.0 },
        // top-right
        { xpos + w, ypos,     1.0, 1.0 },

        // bottom-left
        { xpos,     ypos + h, 0.0, 0.0 },
        // top-right
        { xpos + w, ypos,     1.0, 1.0 },
        // bottom-right
        { xpos + w, ypos + h, 1.0, 0.0 }
    };



    • VelekHacks's avatar
      VelekHacks
      Honored Guest

      Thanks for your reply.  I just tried substituting your version of the vertices matrix, but the text is still upside down.

      • florian.buchholz.1988's avatar
        florian.buchholz.1988
        Expert Protege

        In that case, I assume there are some other settings different that you did not post and it's hard to figure out remotely. In any case, OpenGL is not really a Quest-Topic, so I suggest you try to find help in some other forum as well.

        In any case, why not try a much simpler example and, if that works, iterate step by step to the point where you basically have the same code as you have now. 

        And if you don't know what steps to take, then yes, I think it is time to actually do a tutorial in OpenGL and learn the code without relying too much on AI-models. 

→ Find helpful resources to begin your development journey in Getting Started

→ Get the latest information about HorizonOS development in News & Announcements.

→ Access Start program mentor videos and share knowledge, tutorials, and videos in Community Resources.

→ Get support or provide help in Questions & Discussions.

→ Show off your work in What I’m Building to get feedback and find playtesters.

→ Looking for documentation?  Developer Docs

→ Looking for account support?  Support Center

→ Looking for the previous forum?  Forum Archive

→ Looking to join the Start program? Apply here.

 

Recent Discussions