bevyengine / bevy

A refreshingly simple data-driven game engine built in Rust
https://bevyengine.org
Apache License 2.0
35.15k stars 3.46k forks source link

Billboard 3d Sphere Gizmo #14559

Open DasLixou opened 1 month ago

DasLixou commented 1 month ago

What problem does this solve or what need does it fill?

The current gizmos for circles/spheres are pretty hard to correctly see/interpret.

What solution would you like?

Add a billboard 3d sphere that is kind of like a 2d circle gizmo that looks at the camera but that it really outlines the sphere and respects fov distortion, scale, etc.

What alternative(s) have you considered?

Use the current 3d gizmo.

Additional context

Not sure how hard this would be to implement and what the best strategy would be. Maybe a real sphere with a custom shader that just renders at the "edges"?

Bcompartment commented 1 month ago

Billboard Gizmo by definition is 2d so if we take depth into account and spawn multiple shapes, how we should sort and render them? No matter how many n-dimensional shape is, it still projected to 2d screen or 3d hologram, but illusion breaks when this objects interact and we can see depth. What if camera is inside sphere ?
What if we have two spheres with same position, but different radius ? IMO good way to represent 3d sphere is some kind of grid like globus. Screenshot_149

DasLixou commented 1 month ago

Interesting idea. What I've thought about was something like this: image so just outline them. the outline would also be approximately as far away in depth perspective as the sphere itself so a triangle facing towards the camera at the point of the sphere should be enough. Question is how to render the outline. As seen in the distorted fov thing, most of them are weird ellipses. No clue how to go from position and projection matrix to them.

Bcompartment commented 1 month ago

Rendering of outlines have multiple solutions, but IMO its out of scope of this thread. If camera is close to sphere surface 2d billboard will be hidden by horizon.

DasLixou commented 1 month ago

Well.. it's more or less what I need, so not really that out of scope. I have a circle collider without a mesh and want to see where my mouse must be in order to interact with it, to share my usecase.

cactusdualcore commented 1 month ago

@DasLixou

I am a bit confused. You have a circle collider in a 3D world that collides with your mouse? The mouse position is inherently 2D (might be viewport or ndc though) and so are circles. Either way, I think that a billboard might be "overkill" (there are subtleties involved) here.

I suggest this

/// We'll need a custom Gizmo group for our 2D overlays 
#[derive(Default, Reflect, GizmoConfigGroup)]
pub struct ScreenGizmoGroup;

fn spawn_screen_overlay_camera(mut commands: Commands, mut config_store: ResMut<GizmoConfigStore>) {
    // This spawns a 2D Camera which by default renders to the primary window (there's a
    // 'RenderTarget' Component in here somewhere). By using a 'Camera::order' higher than
    // that of your 3D Camera this renders on top
    commands.spawn((
        Camera2dBundle {
            camera: Camera {
                // Make sure this is at least > 0 to render on top of 3D world.
                // If you already use Camera Ordering, you probably already
                // know how to adapt this for your project anyway. 
                order: 1,
                ..Default::default()
            },
            ..Default::default()
        },
        // We don't want this to render 3D world objects, because that would be... janky at best. So we don't.
        RenderLayers::layer(1),
    ));

    let config = config_store.config_mut::<ScreenGizmoGroup>().0;

    // This makes our Gizmo group render to the 2D camera we just spawned.
    config.render_layers = RenderLayers::layer(1);
}

fn draw_screen_overlays(mut gizmos: Gizmos<ScreenGizmoGroup>) {
    // Normally this doesn't work in 3D because the implementation is... odd.
    // Using the 'ScreenGizmoGroup' we defined earlier makes this render to our
    // 2D Camera which makes it work.
    // Using 'Gizmos<ScreenGizmoGroup>' you can actually render all of the 2D gizmos.
    gizmos.circle_2d(Vec2::ZERO, 25., bevy::color::palettes::basic::GREEN);
}

fn main() -> AppExit {
  App::new()
    .add_systems(Startup, spawn_screen_overlay_camera)
    .add_systems(Update, draw_screen_overlays)
    // --snip--
    .run()
}

I intentionally omitted imports and probably everything that makes the code worthwhile (or even compile). I hope this demonstrates the technique enough for you to work with it.

cactusdualcore commented 1 month ago

I think the actual problem here is not the lack of billboard gizmo primitives, but the way the 2D gizmos are implemented.

As an example, Gizmos::circle_2d is very roughly implemented like this

fn circle_2d(/* ... */) {
    let positions = points_on_circle();
    self.gizmos.linestrip_2d(positions, /* ... */);
}

fn linestrip_2d(/* ... */) {
    // Behold! It's actually the 3D machinery!
    self.linestrip(positions.into_iter().map(|vec2| vec2.extend(0.)), color);
}

This means that 2D Gizmos are actually rendered in a 3D world, as 3D Gizmos in the XY plane. This almost certainly violates the principle of least surprise and I can't find this behavior in documentation!

It's even worse for the `arc_*` methods The documentation for `Gizmos::arc_3d` states the following > A standard arc is defined as > - an arc with a center at Vec3::ZERO > - starting at Vec3::X > - embedded in the XZ plane > - rotates counterclockwise So, you call `arc_2d` in 3D context. It doesn't show. You decide to check the documentation, but it doesn't say what's going on there. So you check the docs on `arc_3d` out of desperation and somehow it says a standard arc will be in the XZ plane! The documentation isn't factually wrong here at all, but with 2D Gizmos it might end up being very confusing and misleading. It certainly was for me when I first wanted to this.
cactusdualcore commented 1 month ago

Oh, and if you have the coordinates of your mouse and still need world coordinates, or the opposite, or anything: There's conversion methods on the Camera type from bevy_render (it's also included in the bevy::prelude).

DasLixou commented 1 month ago

I know, I think you misunderstood my problem. Let me just.. quickly... yep, there it is: image The problem is that with my mouse cursor being over the x, it is still over the 3d sphere (see the circle around the y axis underneath), but it isn't clearly visible where the exact border would be.

DasLixou commented 1 month ago

The outline would be something like this image And that is what I want to visualize.

cactusdualcore commented 1 month ago
fn draw_sphere_outline(
  mut gizmos: Gizmos,
  camera: Query<&Transform, With<RelevantCamera>>
) {
  let towards_camera = camera.single().translation - position_of_your_sphere;
  gizmos.circle(
    position_of_your_sphere,
    Dir3::new(towards_camera).unwrap(),
    // Making it a bit larger makes it easier to see
    radius_of_your_sphere * 1.05,
    RED,
  );
}

It's not perfect but should be good enough