asny / three-d

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

Gost faces #268

Closed Strosel closed 2 years ago

Strosel commented 2 years ago

This bug occurred when using three-d to render to an egui canvas according to the example in the egui repo here but as far as I can see the issue originates in three-d.

The bug in question is that three-d seems to render missing faces, but only if "seen through" another missing face as seen in this image

image

I can't seem to figure out why this happens so any insight would be valuable.

Full Code

#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release

use eframe::egui;

fn main() {
    let options = eframe::NativeOptions {
        initial_window_size: Some(egui::vec2(550.0, 610.0)),
        multisampling: 8,
        renderer: eframe::Renderer::Glow,
        ..Default::default()
    };
    eframe::run_native(
        "Custom 3D painting in eframe!",
        options,
        Box::new(|cc| Box::new(MyApp::new(cc))),
    );
}

struct MyApp {
    angle: f32,
}

impl MyApp {
    fn new(_cc: &eframe::CreationContext<'_>) -> Self {
        Self { angle: 0.2 }
    }
}

impl eframe::App for MyApp {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        egui::CentralPanel::default().show(ctx, |ui| {
            egui::widgets::global_dark_light_mode_buttons(ui);

            ui.horizontal(|ui| {
                ui.spacing_mut().item_spacing.x = 0.0;
                ui.label("The triangle is being painted using ");
                ui.hyperlink_to("three-d", "https://github.com/asny/three-d");
                ui.label(".");
            });

            egui::ScrollArea::both().show(ui, |ui| {
                egui::Frame::canvas(ui.style()).show(ui, |ui| {
                    self.custom_painting(ui);
                });
                ui.label("Drag to rotate!");
            });
        });
    }
}

impl MyApp {
    fn custom_painting(&mut self, ui: &mut egui::Ui) {
        let (rect, response) =
            ui.allocate_exact_size(egui::Vec2::splat(512.0), egui::Sense::drag());

        self.angle += response.drag_delta().x * 0.01;

        // Clone locals so we can move them into the paint callback:
        let angle = self.angle;

        let callback = egui::PaintCallback {
            rect,
            callback: std::sync::Arc::new(egui_glow::CallbackFn::new(move |info, painter| {
                with_three_d_context(painter.gl(), |three_d| {
                    paint_with_three_d(three_d, &info, angle);
                });
            })),
        };
        ui.painter().add(callback);
    }
}

/// We get a [`glow::Context`] from `eframe`, but we want a [`three_d::Context`].
///
/// Sadly we can't just create a [`three_d::Context`] in [`MyApp::new`] and pass it
/// to the [`egui::PaintCallback`] because [`three_d::Context`] isn't `Send+Sync`, which
/// [`egui::PaintCallback`] is.
fn with_three_d_context<R>(
    gl: &std::sync::Arc<glow::Context>,
    f: impl FnOnce(&three_d::Context) -> R,
) -> R {
    use std::cell::RefCell;
    thread_local! {
        pub static THREE_D: RefCell<Option<three_d::Context>> = RefCell::new(None);
    }

    // If you are using the depth buffer you need to do this:
    #[allow(unsafe_code)]
    unsafe {
        use glow::HasContext as _;
        gl.enable(glow::DEPTH_TEST);
        if !cfg!(target_arch = "wasm32") {
            gl.disable(glow::FRAMEBUFFER_SRGB);
        }
        gl.clear(glow::DEPTH_BUFFER_BIT);
    }

    THREE_D.with(|three_d| {
        let mut three_d = three_d.borrow_mut();
        let three_d =
            three_d.get_or_insert_with(|| three_d::Context::from_gl_context(gl.clone()).unwrap());
        f(three_d)
    })
}

fn paint_with_three_d(three_d: &three_d::Context, info: &egui::PaintCallbackInfo, angle: f32) {
    // Based on https://github.com/asny/three-d/blob/master/examples/triangle/src/main.rs
    use three_d::*;

    // Set where to paint
    let viewport = info.viewport_in_pixels();
    let viewport = Viewport {
        x: viewport.left_px.round() as _,
        y: viewport.from_bottom_px.round() as _,
        width: viewport.width_px.round() as _,
        height: viewport.height_px.round() as _,
    };

    // Respect the egui clip region (e.g. if we are inside an `egui::ScrollArea`).
    let clip_rect = info.clip_rect_in_pixels();
    three_d.set_scissor(ScissorBox {
        x: clip_rect.left_px.round() as _,
        y: clip_rect.from_bottom_px.round() as _,
        width: clip_rect.width_px.round() as _,
        height: clip_rect.height_px.round() as _,
    });

    let mut model = Gm::new(
        Mesh::new(three_d, &CpuMesh::cube()).unwrap(),
        PhysicalMaterial::new_opaque(
            three_d,
            &CpuMaterial {
                ..Default::default()
            },
        )
        .unwrap(),
    );

    let ambient = AmbientLight::new(three_d, 0.6, Color::WHITE).unwrap();
    let point_light =
        DirectionalLight::new(three_d, 10.0, Color::WHITE, &vec3(0.0, 0.0, -1.0)).unwrap();
    let camera = Camera::new_perspective(
        three_d,
        viewport,
        vec3(0.0, 0.0, 2.0),
        vec3(0.0, 0.0, 0.0),
        vec3(0.0, 1.0, 0.0),
        degrees(90.0),
        0.01,
        1000.0,
    )
    .unwrap();

    // Set the current transformation of the triangle
    model.set_transformation(Mat4::from_angle_y(radians(angle)));

    // Render the triangle with the color material which uses the per vertex colors defined at construction
    model.render(&camera, &[&ambient, &point_light]).unwrap();
}
asny commented 2 years ago

It's difficult to say exactly what goes wrong but I can give you a few suggestions to try out:

Strosel commented 2 years ago

I've tried both your suggestions as well as setting the cull mode manually on the context, but nothing solved the issue. However, changing the position and perspective of the camera did yield similar behaviour on other faces as seen here:

image

Since this does not seem to happen with three_d::window I'm starting to think this is more egui specific although at the moment I can't figure out how...

asny commented 2 years ago

Thanks for testing 👍 I'm on vacation, so I don't really have the option to test it myself. Will look more into it in a week or two from now when I'm back. However, I have an idea that there is no depth buffer or that the depth buffer is not configured correctly. That means that the sides of the cubes will overlap each other based on the order that they are rendered, not based on the distance from the camera. So I think this is very much related to https://github.com/emilk/egui/issues/1744 . Which platform and OS are you using?

Strosel commented 2 years ago

No worries 😄 I'm actually on vacation myself so no pressure. I'll look into the issue you linked to see if it provides any insight.

I'm running macOs 12.3.1 (Monterey) on an m1 pro

asny commented 2 years ago

Back from vacation, so I had some time to look into it. It actually makes a lot of sense. In the original example, only a triangle is rendered, which means that you don't need a depth buffer (nothing is overlapping something else and egui does not need it either). Therefore, the window is initialised with

let options = eframe::NativeOptions {
        initial_window_size: Some(egui::vec2(550.0, 610.0)),
        multisampling: 8,
        renderer: eframe::Renderer::Glow,
        ..Default::default()
    };

But when we have a more complex scene, we need the depth buffer and have to specify it when constructing the window like this

let options = eframe::NativeOptions {
        initial_window_size: Some(egui::vec2(550.0, 610.0)),
        multisampling: 8,
        renderer: eframe::Renderer::Glow,
        depth_buffer: 32,
        ..Default::default()
    };

I think it should be the default for a 3D example to be initialised with a depth buffer, will try to make the change in egui 💪

Strosel commented 2 years ago

Wow, I feel really silly now. I should have noticed that when debugging and reading the docs. Thank you for your help!

asny commented 2 years ago

Don't feel silly, I suggested that there was no depth buffer as the reason and, when I actually looked at the code, it still took me a while to figure out that there actually was no depth buffer 😆 No problem 👍