diff --git a/crates/bevy_infinite_grid/src/render/grid.wgsl b/crates/bevy_infinite_grid/src/render/grid.wgsl index 9f65ca1b..1650794a 100644 --- a/crates/bevy_infinite_grid/src/render/grid.wgsl +++ b/crates/bevy_infinite_grid/src/render/grid.wgsl @@ -18,11 +18,13 @@ struct InfiniteGridSettings { }; struct View { - projection: mat4x4, - inverse_projection: mat4x4, - view: mat4x4, - inverse_view: mat4x4, + clip_from_view: mat4x4, + view_from_clip: mat4x4, + world_from_view: mat4x4, + view_from_world: mat4x4, world_position: vec3, + world_right: vec3, + world_forward: vec3, }; @group(0) @binding(0) var view: View; @@ -35,7 +37,7 @@ struct Vertex { }; fn unproject_point(p: vec3) -> vec3 { - let unprojected = view.view * view.inverse_projection * vec4(p, 1.0); + let unprojected = view.world_from_view * view.view_from_clip * vec4(p, 1.0); return unprojected.xyz / unprojected.w; } @@ -48,19 +50,19 @@ struct VertexOutput { @vertex fn vertex(vertex: Vertex) -> VertexOutput { // 0 1 2 1 2 3 - var grid_plane = array, 4>( - vec3(-1., -1., 1.), - vec3(-1., 1., 1.), - vec3(1., -1., 1.), - vec3(1., 1., 1.) + var grid_plane = array( + vec3(-1., -1., 1.), + vec3(-1., 1., 1.), + vec3(1., -1., 1.), + vec3(1., 1., 1.), ); let p = grid_plane[vertex.index].xyz; var out: VertexOutput; - out.clip_position = vec4(p, 1.); + out.clip_position = vec4(p, 1.); out.near_point = unproject_point(p); - out.far_point = unproject_point(vec3(p.xy, 0.001)); // unprojecting on the far plane + out.far_point = unproject_point(vec3(p.xy, 0.001)); // unprojecting on the far plane return out; } @@ -69,6 +71,17 @@ struct FragmentOutput { @builtin(frag_depth) depth: f32, }; +fn raycast_plane(plane_origin: vec3, plane_normal: vec3, ray_origin: vec3, ray_direction: vec3) -> vec3 { + let denominator = dot(ray_direction, plane_normal); + let point_to_point = plane_origin - ray_origin; + let t = dot(plane_normal, point_to_point) / denominator; + return ray_direction * t + ray_origin; +} + +fn log10(a: f32) -> f32 { + return log(a) / log(10.); +} + @fragment fn fragment(in: VertexOutput) -> FragmentOutput { let ray_origin = in.near_point; @@ -76,57 +89,53 @@ fn fragment(in: VertexOutput) -> FragmentOutput { let plane_normal = grid_position.normal; let plane_origin = grid_position.origin; - let denominator = dot(ray_direction, plane_normal); - let point_to_point = plane_origin - ray_origin; - let t = dot(plane_normal, point_to_point) / denominator; - let frag_pos_3d = ray_direction * t + ray_origin; + let frag_pos_3d = raycast_plane(plane_origin, plane_normal, ray_origin, ray_direction); let planar_offset = frag_pos_3d - plane_origin; let rotation_matrix = grid_position.planar_rotation_matrix; let plane_coords = (grid_position.planar_rotation_matrix * planar_offset).xz; + // TODO Handle ray misses/NaNs - let view_space_pos = view.inverse_view * vec4(frag_pos_3d, 1.); - let clip_space_pos = view.projection * view_space_pos; - let clip_depth = clip_space_pos.z / clip_space_pos.w; - let real_depth = -view_space_pos.z; - - var out: FragmentOutput; - - out.depth = clip_depth; - - // Perspective scaling - - let camera_distance_from_plane = abs(dot(view.world_position - plane_origin, plane_normal)); - - // The base 10 log of the camera distance - let log10_distance = log(max(grid_settings.scale, camera_distance_from_plane)) / log(10.); + // 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 when changing FOV or + // when using an orthographic projection. + // + // Instead, we want a solution that is related to the size of objects on screen. - // The scaling to be used when the camera projection has perspective - let perspective_scaling = pow(10., floor(log10_distance)); + // Cast a ray from the camera to the plane and get the point where the ray hits the plane. + let point_a = raycast_plane(plane_origin, plane_normal, view.world_position, view.world_forward); + // Then we offset that hit one world-space unit in the direction of the camera's right. + let point_b = point_a + view.world_right; - // Orthographic scaling + // Convert the points to view space + let view_space_point_a = view.view_from_world * vec4(point_a, 1.); + let view_space_point_b = view.view_from_world * vec4(point_b, 1.); + // Take the flat distance between the points in view space + let view_space_distance = distance(view_space_point_a.xy, view_space_point_b.xy); - // The height of the view in world units - let view_area_height = 2. / view.projection[1][1]; + // 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 in the view. If we are very far from the plane, the two points will be very close + // in the view, if we are very close to the plane, the two objects will be very far apart + // in the view. This will work for any camera projection regardless of the camera's + // translational distance. + let log10_scale = log10(max(grid_settings.scale, 1. / view_space_distance)); - // Who knows what it means? - let cool_magic_number = 300.; - let size = view_area_height / cool_magic_number; + // Floor the scaling to the nearest power of 10. + let scaling = pow(10., floor(log10_scale)); - // The base 10 log of the viewport size - let log10_size = log(max(1., size)) / log(10.); - - // The scaling to be used when the camera projection is orthographic - let orthographic_scaling = pow(10., floor(log10_size)) ; + let view_space_pos = view.view_from_world * vec4(frag_pos_3d, 1.); + let clip_space_pos = view.clip_from_view * view_space_pos; + let clip_depth = clip_space_pos.z / clip_space_pos.w; + let real_depth = -view_space_pos.z; + var out: FragmentOutput; - // Equal to 1 when the camera projection is orthographic. Otherwise 0 - let is_orthographic = view.projection[3].w; + out.depth = clip_depth; - // Choose different scaling methods for perspective and orthographic projections - let scaling = mix(perspective_scaling, orthographic_scaling, is_orthographic); + let camera_distance_from_plane = abs(dot(view.world_position - plane_origin, plane_normal)); let scale = grid_settings.scale * scaling; let coord = plane_coords / scale; // use the scale variable to set the distance between the lines @@ -138,11 +147,12 @@ fn fragment(in: VertexOutput) -> FragmentOutput { let minimumx = min(derivative.x, 1.) * scale; let derivative2 = fwidth(coord * 0.1); - let grid2 = abs(fract((coord * 0.1) - 0.5) - 0.5) / derivative2; - let mg_line = min(grid2.x, grid2.y); + let grid2 = abs(fract(coord * 0.1 - 0.5) - 0.5) / derivative2; + let is_minor_line = step(1., min(grid2.x, grid2.y)); + let minor_alpha_multiplier = 1. - fract(log10_scale); let grid_alpha = 1.0 - min(lne, 1.0); - let base_grid_color = mix(grid_settings.major_line_col, grid_settings.minor_line_col, step(1., mg_line)); + let base_grid_color = mix(grid_settings.major_line_col, grid_settings.minor_line_col * vec4(1., 1., 1., minor_alpha_multiplier), is_minor_line); var grid_color = vec4(base_grid_color.rgb, base_grid_color.a * grid_alpha); let main_axes_half_width = 0.8; @@ -156,7 +166,7 @@ fn fragment(in: VertexOutput) -> FragmentOutput { let dot_fadeout = abs(dot(grid_position.normal, normalize(view.world_position - frag_pos_3d))); let alpha_fadeout = mix(dist_fadeout, 1., dot_fadeout) * min(grid_settings.dot_fadeout_const * dot_fadeout, 1.); - grid_color.a = grid_color.a * alpha_fadeout; + grid_color.a *= alpha_fadeout; out.color = grid_color; return out; diff --git a/crates/bevy_infinite_grid/src/render/mod.rs b/crates/bevy_infinite_grid/src/render/mod.rs index 3898a2ac..0008eb2b 100755 --- a/crates/bevy_infinite_grid/src/render/mod.rs +++ b/crates/bevy_infinite_grid/src/render/mod.rs @@ -41,6 +41,7 @@ const GRID_SHADER_HANDLE: Handle = Handle::weak_from_u128(15204473893972 pub fn render_app_builder(app: &mut App) { load_internal_asset!(app, GRID_SHADER_HANDLE, "grid.wgsl", Shader::from_wgsl); + // app.add_systems(Last, update_grid); let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; @@ -140,11 +141,13 @@ struct InfiniteGridBindGroup { #[derive(Clone, ShaderType)] pub struct GridViewUniform { - projection: Mat4, - inverse_projection: Mat4, - view: Mat4, - inverse_view: Mat4, + clip_from_view: Mat4, + view_from_clip: Mat4, + world_from_view: Mat4, + view_from_world: Mat4, world_position: Vec3, + world_right: Vec3, + world_forward: Vec3, } #[derive(Resource, Default)] @@ -243,16 +246,18 @@ fn prepare_grid_view_uniforms( ) { view_uniforms.uniforms.clear(); for (entity, camera) in views.iter() { - let projection = camera.clip_from_view; - let view = camera.world_from_view.compute_matrix(); - let inverse_view = view.inverse(); + let clip_from_view = camera.clip_from_view; + let world_from_view = camera.world_from_view.compute_matrix(); + let view_from_world = world_from_view.inverse(); commands.entity(entity).insert(GridViewUniformOffset { offset: view_uniforms.uniforms.push(&GridViewUniform { - projection, - view, - inverse_view, - inverse_projection: projection.inverse(), + clip_from_view, + world_from_view, + view_from_world, + view_from_clip: clip_from_view.inverse(), world_position: camera.world_from_view.translation(), + world_right: camera.world_from_view.right().as_vec3(), + world_forward: camera.world_from_view.forward().as_vec3(), }), }); }