VelekHacks
24 days agoHonored Guest
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);
}