asny / three-d

2D/3D renderer - makes it simple to draw stuff across platforms (including web)
MIT License
1.28k stars 107 forks source link

Run three-d in GTK-GL #292

Open breynard0 opened 1 year ago

breynard0 commented 1 year ago

In GTK, there is a GLArea widget (https://docs.gtk.org/gtk4/class.GLArea.html). Is it possible to integrate three-d in there? I see it's possible within egui, but I don't know if that crosses over. Thanks for anything!

asny commented 1 year ago

It should definitely be possible, it's exactly the same thing as eframe (egui) or the default winit window setup. All you need is an OpenGL context to use three-d and GLArea provides that in the form of GLContext. So now comes the tricky part, to construct a three-d context you'll need a glow context which is just a wrapper around different types of OpenGL like contexts. This glow context can be created from a loader function which provides the address to the OpenGL functionality. This function is provided by winit or eframe windows, however, it's not provided by GTK it seems. I found this discussion which is very much related and suggests to use the epoxy crate (see this example). I also found this which is exactly the functionality we need, but I'm not sure that is in Rust. Anyway, I don't know what is the right solution, but I suggest you open an issue in the GTK crate and ask how to create a glow context from GLArea.

Also, this is a duplicate of #27

asny commented 1 year ago

@Siliwolf Did you make it work? 🙂

breynard0 commented 1 year ago

No, I didn't mean to mark as completed. I just came to the issues page for an unrelated reason, saw my duplicate was still up, and decided to close it.

UnsuccessfulLaminator commented 1 year ago

Hello! I've gotten the triangle example to work in a GLArea, more or less, after looking at the things you linked. I can't make it work using RenderTarget, but that's probably my bad usage of three-d. In main for epoxy setup I have:

epoxy::load_with(|s| unsafe {
    DynamicLibrary::open(None).unwrap().symbol(s).unwrap_or(ptr::null_mut())
});

This is a paraphrase of what's in the glium example. I assume that open(None), which doesn't load any library itself, works because GTK has already loaded epoxy. After that, I've subclassed GLArea and overridden render (though it would equally work without subclassing, by connecting to the "render" signal):

fn render(&self, _: &gdk::GLContext) -> bool {
    let ctx = unsafe {
        context::Context::from_loader_function(|s| epoxy::get_proc_addr(s))
    };
    let ctx = Arc::new(ctx);
    let ctx = core::Context::from_gl_context(ctx).unwrap();

    let alloc = self.obj().allocation();
    let (width, height) = (alloc.width() as u32, alloc.height() as u32);

    let mut camera = Camera::new_perspective(
        Viewport::new_at_origo(width, height),
        vec3(0.0, 0.0, 2.0),
        vec3(0.0, 0.0, 0.0),
        vec3(0.0, 1.0, 0.0),
        degrees(45.0),
        0.1,
        10.0,
    );
    let positions = vec![
        vec3(0.5, -0.5, 0.0),
        vec3(-0.5, -0.5, 0.0),
        vec3(0.0, 0.5, 0.0)
    ];
    let colors = vec![
        Color::RED,
        Color::GREEN,
        Color::BLUE
    ];
    let cpu_mesh = CpuMesh {
        positions: Positions::F32(positions),
        colors: Some(colors),
        ..Default::default()
    };
    let mut model = Gm::new(Mesh::new(&ctx, &cpu_mesh), ColorMaterial::default());

    unsafe {
        ctx.clear_color(0.8, 0.8, 0.8, 1.);
        ctx.clear(context::COLOR_BUFFER_BIT);
    }

    model.render(&camera, &[]);

    // RenderTarget::screen(&ctx, width, height)
    //     .clear(ClearState::color_and_depth(0.8, 0.8, 0.8, 1., 1.))
    //     .render(&camera, &model, &[]);

    true
}

This isn't meant to be good (I don't need to be re-making the model every time), it's just meant to work, which it does. I would like to avoid using the 2 unsafe lines, but the commented-out RenderTarget doesn't render anything. Is there something more I should have to make that work?

asny commented 1 year ago

Super nice work 💪 that would be great to have as an example 🥳

About the render target, GTK probably uses a custom render target instead of the default one, and since it's created in GTK, three-d doesn't know about it. If you can somehow get the FrameBuffer ID from GTK, you can use this function to create the render target instead of the screen function.

UnsuccessfulLaminator commented 1 year ago

Thanks! Sadly GDK's GLContext docs say "A GdkGLContext is not tied to any particular normal framebuffer". Probably there is some way but the GDK & GTK docs don't make it obvious. Besides manually calling glGetIntegerv to get the ID, which seems like a worse solution than just using the core context's unsafe functions 😆

If it doesn't bother you that this is slightly hacky, I can turn it into a clean(er) example and make a pull for it.

asny commented 1 year ago

Hmm. Yeah, it seems like GTK doesn't expose that information 😬 The problem is not that it uses the core unsafe functions for clearing the render target, the problem is that it's not possible to use a render target, which for example means deferred rendering will not work. If we used a render target, then there's no way to know which frame buffer to switch back to when we want to draw to the screen. We could maybe use the attach_buffers method but it requires that we call it at the right time, which isn't obvious how to do. I actually think getting the frame buffer index manually using glGetIntegerv is the better solution 🙂

If it doesn't bother you that this is slightly hacky, I can turn it into a clean(er) example and make a pull for it.

That would be great 👍 As I see it, the only thing that needs to be fixed is the render target issue ☝️ and maybe figuring out how to create models outside of the render loop.

asny commented 1 year ago

I'm opening this issue in favour of #27

UnsuccessfulLaminator commented 1 year ago

I have something working with a RenderTarget and with the model initialised outside the render loop. I've used glGetIntegerv once before rendering starts to get the draw fb, which seems to work fine. It relies on GTK not randomly deciding to use a different fb, and although I've never seen that happen on my setup, I don't know enough about GTK/GDK/GSK rendering to say it can't 😢 https://github.com/UnsuccessfulLaminator/triangle-gtk4 Runs on my linux machine, but fails to load epoxy functions on my windows one. I'm not sure why yet. Something to do with it deciding not to be able to find dll funcs from the main process I think... but odd given that gtk4 itself runs happily 🤷

asny commented 1 year ago

Sorry for the late reply.

I've used glGetIntegerv once before rendering starts to get the draw fb, which seems to work fine.

Great, then I think that's a good solution.

It relies on GTK not randomly deciding to use a different fb, and although I've never seen that happen on my setup, I don't know enough about GTK/GDK/GSK rendering to say it can't 😢

True, so maybe it's better to call glGetIntegerv each frame to make sure it's the correct frame buffer? I don't think it matters for performance 🤞

https://github.com/UnsuccessfulLaminator/triangle-gtk4 Runs on my linux machine, but fails to load epoxy functions on my windows one. I'm not sure why yet. Something to do with it deciding not to be able to find dll funcs from the main process I think... but odd given that gtk4 itself runs happily 🤷

I'm sorry, I can't help with that, but I think it's ok to add the example even though it's confirmed to work on all platforms. Having an example is much better than not having anything 🙂