bevy_editor_prototypes icon indicating copy to clipboard operation
bevy_editor_prototypes copied to clipboard

Improved grid zoom

Open tim-blackbird opened this issue 1 year ago • 1 comments

Work in progress

Original implementation provided by our friends at Foresight (@aevyrie)

/// Update the grid to match the [`GridSettings`] and the current camera angle.
pub fn update_grid(
    // TODO use fse specific marker
    camera_query: Query<
        (&GlobalTransform, Ref<EditorCam>, &EditorCam, &Camera),
        Without<InfiniteGrid>,
    >,
    mut grid_query: Query<(&GlobalTransform, &mut InfiniteGridSettings), With<InfiniteGrid>>,
    grid_colors: Res<GridSettings>,
) {
    for (camera_transform, camera_change_tracker, editor_cam, cam) in &camera_query {
        if !camera_change_tracker.is_changed() && !grid_colors.is_changed() {
            continue;
        }

        let Ok((grid_transform, mut grid_params)) = grid_query.get_single_mut() else {
            continue;
        };

        let z_distance = (camera_transform.translation().z - grid_transform.translation().z)
            .abs()
            .max(editor_cam.last_anchor_depth as f32);

        // To scale the grid, we need to know how far the camera is from the grid plane. The naive
        // solution is to simply use the distance, however this breaks down during dolly zooms or
        // when using an orthographic projection.
        //
        // Instead, we want a solution that is related to the size of objects on screen. If an
        // object on screen is the same size during a dolly zoom switch from perspective to ortho,
        // we would expect that the grid scale should also not change.
        //
        // First, we raycast against the plane:
        let world_to_screen = cam.get_world_to_screen(camera_transform);
        let ray = Ray3d {
            origin: camera_transform.translation(),
            direction: Direction3d::new_unchecked(camera_transform.forward()),
        };
        let hit = ray
            .intersect_plane(
                grid_transform.translation(),
                Plane3d::new(grid_transform.up()),
            )
            .unwrap_or_default();
        let hit_world = ray.origin + ray.direction.normalize() * hit;
        // Then we offset that hit one world-space unit in the direction of the camera's right.
        let hit_world_offset = hit_world + camera_transform.right();
        // Now we project these two positions into screen space, and determine the distance between
        // them when projected on the screen:
        let hit_screen = world_to_screen(hit_world).unwrap_or_default();
        let hit_screen_offset = world_to_screen(hit_world_offset).unwrap_or_default();
        let size = (hit_screen_offset - hit_screen).length();
        // Finally, we use the relationship that the scale of an object is inversely proportional to
        // the distance from the camera. We can now do the reverse - compute a distance based on the
        // size on the screen. If we are very far from the plane, the two points will be very close
        // on the screen, if we are very close to the plane, the two objects will be very far apart
        // on the screen. This will work for any camera projection regardless of the camera's
        // translational distance.
        let screen_distance_unchecked = (1_000.0 / size as f64).abs() as f32;
        let screen_distance =
            if !screen_distance_unchecked.is_finite() || screen_distance_unchecked == 0.0 {
                z_distance
            } else {
                // The distance blows up when the camera is very close, this looks much nicer
                screen_distance_unchecked.min(z_distance)
            };
        // We need to add `1` to screen_distance because the logarithm is negative when x < 1;
        let log_scale = (screen_distance + 1.0).log10();

        if grid_params.x_axis_color.a() != 0. {
            let GridSettings {
                lightness,
                alpha,
                fadeout_multiplier,
                edge_on_fadeout_strength,
            } = grid_colors.to_owned();

            // lerp minor grid line alpha based on scale
            let minor_alpha = (1.0 - log_scale.fract()) * alpha;

            grid_params.minor_line_color =
                Color::rgba(lightness, lightness, lightness, minor_alpha);
            grid_params.major_line_color = Color::rgba(lightness, lightness, lightness, alpha);
            grid_params.fadeout_distance = fadeout_multiplier * z_distance;
            grid_params.x_axis_color = Color::rgba(1.0, 0.0, 0.0, 1.0);
            grid_params.z_axis_color = Color::rgba(0.0, 1.0, 0.0, 1.0);
            grid_params.dot_fadeout_strength = edge_on_fadeout_strength;
            grid_params.scale = 10f32.powi(1i32.saturating_sub(log_scale.floor() as i32));
        }
    }
}

I'm doing something wrong calculating the view_space_distance. It should smoothly become less as the camera moves further away, instead it seems to be changing randomly :(

tim-blackbird avatar Nov 02 '24 14:11 tim-blackbird

@tim-blackbird Appreciate this is an older PR, just wanted to check if you were still looking at this or should we try and pick up?

jbuehler23 avatar Jun 05 '25 05:06 jbuehler23