[OpenGL] Texture was not loading, now is skewing

I'm currently using sdl_ttf to generate some text. I know it works as I've use it in another project and it renders it correctly. It just seems OpenGL is not loading the texture data. I print out the texture data and see the 1s and 0s. But after trying to load it in, and get it back there is no data there (assumption from glGetTexImage, this maybe incorrect).

Sort of following LearnOpenGL - Text Rendering and source code Code Viewer. Source code: src/7.in_practice/2.text_rendering/text_rendering.cpp just with sdl_ttf instead of freetype_gl.

mod mygl;

use gl::types::*;
use glam::{Mat4, Vec3, Vec4};
use std::ffi::CString;
use std::os::raw::c_void;
use std::time::Duration;
use sdl2::event::Event;
use sdl2::keyboard::Keycode;
use sdl2::pixels::{Color, PixelFormat, PixelFormatEnum};
use sdl2::rect::Rect;
use sdl2::ttf::FontStyle;

const VERTEX_SHADER: &str = r#"#version 330
layout (location = 0) in vec3 pos;
layout (location = 1) in vec2 tex_coord;

out vec2 vertex_tex_coord;

uniform mat4 projection;

void main() {
  gl_Position = projection * vec4(pos.x, pos.y, pos.z, 1.0);
  vertex_tex_coord = tex_coord;
}"#;

const FRAGMENT_SHADER: &str = r#"#version 330
out vec4 color;

in vec2 vertex_tex_coord;

uniform sampler2D tex_image;

void main(void) {
    vec4 sampled = vec4(1.0, 1.0, 1.0, texture(tex_image, vertex_tex_coord).r);
    color = vec4(1.0, 1.0, 1.0, 1.0) * sampled;
    //color = vec4(1.0, 0.0, 0.0, 1.0);
}"#;

fn main() {
    let sdl_context = sdl2::init().unwrap();
    let video_subsystem = sdl_context.video().unwrap();

    let window = video_subsystem.window("Hello World", 640, 480)
        .position_centered()
        .opengl()
        .build()
        .unwrap();

    let gl_attr = video_subsystem.gl_attr();

    gl_attr.set_context_profile(sdl2::video::GLProfile::Core);
    gl_attr.set_context_version(3, 3);

    let gl_context = window.gl_create_context().unwrap();
    let gl = gl::load_with(|s| video_subsystem.gl_get_proc_address(s) as *const std::os::raw::c_void);

    unsafe {
        gl::Enable(gl::BLEND);
        gl::BlendFunc(gl::SRC_ALPHA, gl::ONE_MINUS_SRC_ALPHA);
    }

    let (width, height) = window.size();

    // texture
    let mut hello_world_texture: GLuint = 0;
    let (hwt_width, hwt_height) = unsafe {
        gl::PixelStorei(gl::UNPACK_ALIGNMENT, 1); 

        let ttf_context = sdl2::ttf::init().unwrap();
        let mut font = ttf_context.load_font("assets/fonts/VT323/VT323-Regular.ttf", 24).unwrap();

        font.set_style(FontStyle::NORMAL);
        font.set_outline_width(0);
        font.set_hinting(sdl2::ttf::Hinting::Mono);
        font.set_kerning(false);

        let hello_world_surface = font.render("Hello World")
            .solid(Color::WHITE)
            .unwrap();
        
        let (hw_width, hw_height) = hello_world_surface.size();

        gl::GenTextures(1, &mut hello_world_texture);
        gl::BindTexture(gl::TEXTURE_2D, hello_world_texture);

        gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_S, gl::CLAMP_TO_EDGE as GLint);
        gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_T, gl::CLAMP_TO_EDGE as GLint);
        gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, gl::LINEAR as GLint);
        gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, gl::LINEAR as GLint);

        hello_world_surface.with_lock(|data| {
            println!("DATA BEFORE {:?}", data);
            gl::TexImage2D(
                gl::TEXTURE_2D, 
                0, 
                gl::RED as GLint,
                hw_width as GLint,
                hw_height as GLint, 
                0,
                gl::RED,
                gl::UNSIGNED_BYTE,
                data.as_ptr().cast()
            );

            let mut v = Vec::<u8>::with_capacity((hw_width * hw_height) as usize);
            gl::GetTexImage(gl::TEXTURE_2D, 0, gl::RED, gl::UNSIGNED_BYTE, v.as_mut_ptr().cast());
            println!("DATA AFTER {:?}", v);
        });

        (hw_width, hw_height)
    };


    // orthographic projection
    let trans_matrix = Mat4::from_cols_array(&[
        1.0/320.0,        0.0, 0.0, 0.0,
              0.0, -1.0/240.0, 0.0, 0.0,
              0.0,        0.0, 0.0, 0.0,
             -1.0,        1.0, 0.0, 1.0,
    ]);

    let (vertices, vertices_stride) = {
        let center = Vec3::new((width as f32) * 0.5, (height as f32) * 0.5, 0.0);
        let (w, h) = {
            (hwt_width as f32 * 0.5, hwt_height as f32 * 0.5)
        };

        let top_left = center - Vec3::new(-w, -h, 0.0);
        let top_right = center - Vec3::new(w, -h, 0.0);
        let bot_left = center - Vec3::new(-w, h, 0.0);
        let bot_right = center - Vec3::new(w, h, 0.0);

        // let top_left = center + Vec3::new(-square_apothem, square_apothem, 0.0);
        // let top_right = center + Vec3::new(square_apothem, square_apothem, 0.0);
        // let bot_left = center + Vec3::new(-square_apothem, -square_apothem, 0.0);
        // let bot_right = center + Vec3::new(square_apothem, -square_apothem, 0.0);

        let mut v = Vec::<f32>::with_capacity((3+2) * 4);
        v.extend(top_left.to_array());  v.extend([0.0, 1.0]);
        v.extend(top_right.to_array()); v.extend([1.0, 1.0]);
        v.extend(bot_left.to_array());  v.extend([1.0, 0.0]);
        v.extend(bot_right.to_array()); v.extend([0.0, 0.0]);

        (v, 5)
    };

    let indices = [
        0, 1, 3,
        0, 2, 3,
    ];

    // set viewport size
    unsafe {
        gl::Viewport(0, 0, width as i32, height as i32);
    }

    // load program
    let program: mygl::Program;
    let uniform_projection: GLint;
    let uniform_tex_image:GLint;
    {    
        // vertex shader
        let vertex_shader: mygl::Shader = mygl::Shader::new(mygl::ShaderType::Vertex).unwrap();
        vertex_shader.set_source_with_string(VERTEX_SHADER);
        vertex_shader.try_complie().expect("vertex_shader compile");
        
        // fragment shader
        let fragment_shader: mygl::Shader = mygl::Shader::new(mygl::ShaderType::Fragment).unwrap();
        fragment_shader.set_source_with_string(FRAGMENT_SHADER);
        fragment_shader.try_complie().expect("fragment_shader compile");

        program = mygl::Program::new();
        program.attach_shader(vertex_shader);
        program.attach_shader(fragment_shader);
        program.try_link().expect("program link");

        uniform_projection = program.get_uniform_location("projection").unwrap();
        // uniform_tex_image = program.get_uniform_location("tex_image").unwrap();
    }

    // create buffers
    let mut vao: GLuint = 0;
    let mut vbo: GLuint = 0;
    let mut ebo: GLuint = 0;

    unsafe {
        gl::GenBuffers(1, &mut vbo);
        gl::GenBuffers(1, &mut ebo);
        gl::GenVertexArrays(1, &mut vao);

        gl::BindVertexArray(vao);

        gl::BindBuffer(gl::ARRAY_BUFFER, vbo);
        gl::BufferData(
            gl::ARRAY_BUFFER,
            (vertices.len() * std::mem::size_of::<GLfloat>()) as GLsizeiptr,
            vertices.as_ptr().cast(),
            gl::STATIC_DRAW,
        );

        gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, ebo);
        gl::BufferData(
            gl::ELEMENT_ARRAY_BUFFER,
            (indices.len() * std::mem::size_of::<GLint>()) as GLsizeiptr,
            indices.as_ptr().cast(),
            gl::STATIC_DRAW
        );

        gl::VertexAttribPointer(
            0, 
            3, 
            gl::FLOAT, 
            gl::FALSE,
            (vertices_stride * std::mem::size_of::<GLfloat>()) as GLint, 
            std::ptr::null()
        );
        gl::EnableVertexAttribArray(0);

        gl::VertexAttribPointer(
            1,
            2,
            gl::FLOAT,
            gl::FALSE,
            (vertices_stride * std::mem::size_of::<GLfloat>()) as GLint,
            (3 * std::mem::size_of::<GLfloat>()) as *const c_void
        );
        gl::EnableVertexAttribArray(1);

        gl::BindBuffer(gl::ARRAY_BUFFER, 0);
        gl::BindVertexArray(0);
        
    }

    unsafe {
        gl::ClearColor(0.2, 0.3, 0.3, 1.0);
        gl::Clear(gl::COLOR_BUFFER_BIT);

        program.use_program();

        // gl::Uniform1i(uniform_tex_image, gl::TEXTURE0 as GLint);
        gl::UniformMatrix4fv(uniform_projection, 1, gl::FALSE, trans_matrix.to_cols_array().as_ptr() );

        gl::ActiveTexture(gl::TEXTURE0);
        gl::BindTexture(gl::TEXTURE_2D, hello_world_texture);

        gl::BindVertexArray(vao);
        gl::DrawElements(gl::TRIANGLES, indices.len() as GLint, gl::UNSIGNED_INT, std::ptr::null());
        // gl::DrawArrays(gl::TRIANGLES, 0, vertices.len() as GLint);

        window.gl_swap_window();
    }


    // event loop
    let mut event_pump = sdl_context.event_pump().unwrap();
    'main_loop: loop {
        for event in event_pump.poll_iter() {
            match event {
                Event::Quit {..} |
                Event::KeyDown { keycode: Some(Keycode::Escape), .. } => {
                    break 'main_loop
                },
                _ => {}
            }
        }

        // render frame

        std::thread::sleep(Duration::new(0, 1_000_000_000u32 / 60));
    }
}

Hopefully you can safely ignore Shader, and Program. They are just wrappers for gl stuff. If you think it would help to have direct gl calls, let me know.

Update: Decided to do a gl::GetError() after gl::TexImage2D and it comes back with a 1280 GL_INVALID_ENUM. I think that just confuses me more.
Turns out it was a result of the gl::GetError() was nothing to do with the gl::TexImage2D. I had to remove gl::Enable(gl::TEXTURE_2D) as that was causing GL_INVALID_ENUM. Still no texture showing or being loaded.

Side Note: gl::Enable(gl::TEXTURE_2D) it’s only meaningful for the fixed-function pipeline, and OpenGL 3+ core profile doesn’t support the fixed-function pipeline.

1 Like

Yep, gl has about three times as many things you need to enable than it really should!

It doesn't help that gles and webgl are subtly different, so it's easy to find tutorials that give you misleading information.

More modern APIs like WGPU are crazy in different ways, but at least they're often far more consistent and clearer about why it's not working (with validation layers)

I have correct my last sentence, but that was not the problem. gl::GetError() was telling me that gl::Enable(gl::TEXTURE) was the invalid enum.

Hmm, I can use glTexImage then glGetTexImage with a small set of bytes and get the result back out (modulo row alignment to 4 bytes when getting).

My first guess is that the hello_world_surface. with_lock() stomps the active gl texture, which would make some sense, but it still works for me even if I bind the texture to 0 (presumably there is a default one?)

Perhaps an environment issue?

Well I can successfully get the image back out even with the same with_lock call.

The only issue I can see with your code is you're using the texture surface width instead of pitch, so the data will get skewed and cut off, but you should still get something out.

width instead of pitch?

Maybe it is something environmental, but I have no idea what could be causing it.

Tried using hardcoded data and it's not loading either. Seriously what the hell is going on.

let other_data = [
    0x00, 0xFF, 0x00,
    0x00, 0xFF, 0x00,
    0x00, 0xFF, 0x00u8,
];

println!("DATA BEFORE {:?}", other_data);

gl::TexImage2D(
    gl::TEXTURE_2D, 
    0, 
    gl::RGBA as GLint,
    3 as GLint,
    3 as GLint, 
    0,
    gl::RGBA,
    gl::UNSIGNED_BYTE,
    other_data.as_ptr().cast()
);

let mut v = Vec::<u8>::with_capacity((hw_width * hw_height) as usize);
gl::GetTexImage(gl::TEXTURE_2D, 0, gl::RGBA, gl::UNSIGNED_BYTE, v.as_mut_ptr().cast());

println!("ERROR {}", gl::GetError());

println!("DATA AFTER {:?}", v);

Wondering if this might be a Macbook M2 problem.

Arg, sorry: I just noticed with that small example that you're using Vec::with_capacity() which creates a vector with enough capacity, but 0 length. Use something like vec![0u8; length] instead. (Remember Vec is basically { data: *mut T, len: usize, capacity: usize })

Pitch is the number of bytes between lines in image data, so the whole image data size is height * pitch, not (necessarily) width.

You can either make the texture width match the pitch or copy each line with TexSubImage, but either way it mostly just means the image will be skewed if you get it wrong.

There's also matters of pixel format etc. but that's getting deep into the font library API I'm not familiar with

Good news, it's working... sort of.

This is using pitch() and not width().

The surface's pixel format is Index8. I get a bunch of 0s and 1s in the &[u8] array. I map it, times it by 0xFF for sanity.

let ttf_context = sdl2::ttf::init().unwrap();
let mut font = ttf_context.load_font("assets/fonts/VT323/VT323-Regular.ttf", 24).unwrap();

font.set_style(FontStyle::NORMAL);
font.set_outline_width(0);
font.set_hinting(sdl2::ttf::Hinting::Mono);
font.set_kerning(false);

let hello_world_surface = font.render("Hello World")
    .solid(Color::WHITE)
    .unwrap();

let hw_height = hello_world_surface.height();
let hw_width = hello_world_surface.pitch() + 5;
println!("pitch: {} width: {}", hello_world_surface.pitch(), hello_world_surface.width());


gl::GenTextures(1, &mut hello_world_texture);
gl::BindTexture(gl::TEXTURE_2D, hello_world_texture);

gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_S, gl::CLAMP_TO_EDGE as GLint);
gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_T, gl::CLAMP_TO_EDGE as GLint);
gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, gl::LINEAR as GLint);
gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, gl::LINEAR as GLint);

let data: Vec<u8> = 
    Vec::<u8>::from(hello_world_surface.without_lock().unwrap())
    .iter().map(|i| i * 0xFF).collect();

gl::PixelStorei(gl::UNPACK_ALIGNMENT, 1); 

gl::TexImage2D(
    gl::TEXTURE_2D, 
    0, 
    gl::RED as GLint,
    hw_width as GLint,
    hw_height as GLint, 
    0,
    gl::RED,
    gl::UNSIGNED_BYTE,
    data.as_ptr().cast()
);

gl::BindTexture(gl::TEXTURE_2D, 0);

Due to embedded media constraints

I tried adjusting pitch() by 5 and

If I can get the pixels to align up or remove the padding somehow, I can call this done.

I don't understand why the image data is skewed.

I created a new Vec<u8> taking out the padding and the data appears correct

but the resulting image is still the same as before.

Trying someting else, I get the same skewing with an image

let image = sdl2::rwops::RWops::from_file("assets/images/awesomeface.png", "r")
    .expect("file did not load")
    .load_png().expect("image did not load");

let hw_height = image.height();
let hw_width = image.width();
let hw_pitch = image.pitch();
println!("pitch: {} width: {}", hw_pitch, hw_width);
let raw = image.pixel_format().raw();
println!("BytesPerPixel {}", (*raw).BytesPerPixel);

// gl::PixelStorei(gl::UNPACK_ROW_LENGTH, (hw_width) as GLint);
// gl::PixelStorei(gl::UNPACK_ALIGNMENT, 1); 

gl::GenTextures(1, &mut hello_world_texture);
gl::BindTexture(gl::TEXTURE_2D, hello_world_texture);

gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_S, gl::CLAMP_TO_EDGE as GLint);
gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_T, gl::CLAMP_TO_EDGE as GLint);
gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, gl::LINEAR as GLint);
gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, gl::LINEAR as GLint);

let data = image.without_lock().unwrap();
println!("LEN {}", data.len());

gl::TexImage2D(
    gl::TEXTURE_2D, 
    0, 
    gl::RGBA as GLint,
    hw_width as GLint,
    hw_height as GLint, 
    0,
    gl::RGBA,
    gl::UNSIGNED_BYTE,
    data.as_ptr().cast()
);

println!("ERROR {}", gl::GetError());

gl::BindTexture(gl::TEXTURE_2D, 0);

Another facepalm moment.

Thanks to using the image, made me wonder if I got the tex coords wrong. Corrected them, and now it works.

let mut v = Vec::<f32>::with_capacity((3+2) * 4);
v.extend(top_left.to_array());  v.extend([1.0, 1.0]); // corrected tex cord
v.extend(top_right.to_array()); v.extend([0.0, 1.0]); // corrected tex coord
v.extend(bot_left.to_array());  v.extend([1.0, 0.0]);
v.extend(bot_right.to_array()); v.extend([0.0, 0.0]);

Thank you for joining me on my journey.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.