// An infinite ground grid, drawn as one triangle over the whole frame.
//
// Nothing is tessellated: every pixel casts its own ray through the inverse
// view-projection, meets the ground plane, and asks how close it landed to a
// line. That is what keeps the lines a pixel wide however far away they are,
// and what lets the grid run to the horizon without a mesh big enough to get
// there.
struct Grid {
view_proj: mat4x4<f32>,
inv_view_proj: mat4x4<f32>,
// xyz: where the eye is. w: the height of the plane.
eye: vec4<f32>,
// x: spacing between lines, y: how many of those to a heavy line,
// z: where the fade starts, w: where nothing is left.
params: vec4<f32>,
line: vec4<f32>,
major: vec4<f32>,
x_axis: vec4<f32>,
z_axis: vec4<f32>,
};
@group(0) @binding(0)
var<uniform> grid: Grid;
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) ndc: vec2<f32>,
};
// One triangle large enough to cover the frame, from the vertex index alone.
// A quad would need a buffer and would seam down its diagonal.
@vertex
fn vs_main(@builtin(vertex_index) index: u32) -> VertexOutput {
let ndc = vec2<f32>(
f32(i32(index) % 2) * 4.0 - 1.0,
f32(i32(index) / 2) * 4.0 - 1.0,
);
var out: VertexOutput;
out.ndc = ndc;
out.clip_position = vec4<f32>(ndc, 0.0, 1.0);
return out;
}
// Where a point on the near plane and the same point on the far plane sit in
// the world, for the pixel at `ndc`.
fn unproject(ndc: vec2<f32>, depth: f32) -> vec3<f32> {
let world = grid.inv_view_proj * vec4<f32>(ndc, depth, 1.0);
return world.xyz / world.w;
}
// How lit a line is at `p`, given how much of the plane this pixel covers.
//
// `coverage` is the width of a pixel measured in cells: divide the distance
// to the nearest line by it and the answer is in pixels, so a line comes out
// the same weight at any distance or angle. Blender's grid does the same.
fn line_alpha(p: vec2<f32>, coverage: vec2<f32>) -> f32 {
let distance = abs(fract(p - 0.5) - 0.5) / max(coverage, vec2<f32>(1e-8));
return 1.0 - min(min(distance.x, distance.y), 1.0);
}
struct FragmentOutput {
@location(0) color: vec4<f32>,
// Written so the grid sits in the same depth as everything else drawn
// this frame: a model in front of it hides it, and one behind does not.
@builtin(frag_depth) depth: f32,
};
@fragment
fn fs_main(in: VertexOutput) -> FragmentOutput {
let near = unproject(in.ndc, 0.0);
let far = unproject(in.ndc, 1.0);
let ray = far - near;
// Where this pixel's ray crosses the plane, as a fraction of near..far.
// Outside that range it either misses (looking up, or away) or lands
// beyond the far plane, and either way there is nothing to draw.
let height = grid.eye.w;
let hit = (height - near.y) / ray.y;
if (hit < 0.0 || hit > 1.0) {
discard;
}
let world = near + ray * hit;
let spacing = grid.params.x;
let cell = world.xz / spacing;
// Screen-space derivatives of the plane coordinate: how much of the grid
// one pixel spans. This is the whole anti-aliasing scheme.
let coverage = fwidth(cell);
let minor = line_alpha(cell, coverage);
let heavy = grid.params.y;
let major = line_alpha(cell / heavy, coverage / heavy);
// Minor lines are dropped once they crowd together tighter than a pixel,
// which is where a grid otherwise turns into moire. The heavy lines are
// still spaced out at that point and carry on.
let crowding = max(coverage.x, coverage.y);
let minor_visible = minor * (1.0 - smoothstep(0.4, 1.0, crowding));
var color = grid.line;
var alpha = minor_visible * grid.line.a;
if (major * grid.major.a > alpha) {
color = grid.major;
alpha = major * grid.major.a;
}
// The two lines through the origin name the axes, the way they do in a
// modelling program: red across, blue into the screen.
let axis = abs(world.xz) / max(coverage * spacing, vec2<f32>(1e-8));
let on_z = 1.0 - min(axis.x, 1.0);
let on_x = 1.0 - min(axis.y, 1.0);
if (on_z * grid.z_axis.a > alpha) {
color = grid.z_axis;
alpha = on_z * grid.z_axis.a;
}
if (on_x * grid.x_axis.a > alpha) {
color = grid.x_axis;
alpha = on_x * grid.x_axis.a;
}
if (alpha <= 0.001) {
discard;
}
// Fade out with distance so the grid ends in nothing rather than in a
// hard rim at the far plane.
let distance = length(world - grid.eye.xyz);
let fade = 1.0 - smoothstep(grid.params.z, grid.params.w, distance);
let clip = grid.view_proj * vec4<f32>(world, 1.0);
var out: FragmentOutput;
out.color = vec4<f32>(color.rgb, alpha * fade);
out.depth = clip.z / clip.w;
return out;
}