@group(0) @binding(0) var<uniform> camera_raw: CameraRaw;
// Raw camera uniform structure (matches GPU buffer layout with padding)
struct CameraRaw {
view: mat4x4<f32>,
proj: mat4x4<f32>,
view_proj: mat4x4<f32>,
inv_view_proj: mat4x4<f32>,
inv_proj: mat4x4<f32>,
inv_view: mat4x4<f32>,
position: vec4<f32>, // .xyz = position, .w = unused
frame_count_and_padding: vec4<u32>, // .x = frame_count, .yzw = padding
frustum_rays: array<vec4<f32>, 4>,
_padding_end: array<vec4<f32>, 2>, // Total: 512 bytes
};
// Friendly camera structure (no padding, easier to work with)
struct Camera {
view: mat4x4<f32>,
proj: mat4x4<f32>,
view_proj: mat4x4<f32>,
inv_view_proj: mat4x4<f32>,
inv_proj: mat4x4<f32>,
inv_view: mat4x4<f32>,
position: vec3<f32>,
frame_count: u32,
frustum_rays: array<vec4<f32>, 4>,
};
// Convert from raw uniform to friendly structure
fn camera_from_raw(raw: CameraRaw) -> Camera {
var camera: Camera;
camera.view = raw.view;
camera.proj = raw.proj;
camera.view_proj = raw.view_proj;
camera.inv_view_proj = raw.inv_view_proj;
camera.inv_proj = raw.inv_proj;
camera.inv_view = raw.inv_view;
camera.position = raw.position.xyz;
camera.frame_count = raw.frame_count_and_padding.x;
camera.frustum_rays = raw.frustum_rays;
return camera;
}
@vertex
fn vert_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
var out: VertexOutput;
// Generate oversized triangle vertices using bit manipulation
// Goal: vertex 0→(-1,-1), vertex 1→(3,-1), vertex 2→(-1,3)
// X coordinate generation:
// vertex_index: 0 → 0<<1 = 0 → 0&2 = 0 → 0*2-1 = -1 ✓
// vertex_index: 1 → 1<<1 = 2 → 2&2 = 2 → 2*2-1 = 3 ✓
// vertex_index: 2 → 2<<1 = 4 → 4&2 = 0 → 0*2-1 = -1 ✓
let x = f32((vertex_index << 1u) & 2u) * 2.0 - 1.0;
// Y coordinate generation:
// vertex_index: 0 → 0&2 = 0 → 0*2-1 = -1 ✓
// vertex_index: 1 → 1&2 = 0 → 0*2-1 = -1 ✓
// vertex_index: 2 → 2&2 = 2 → 2*2-1 = 3 ✓
let y = f32(vertex_index & 2u) * 2.0 - 1.0;
out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
out.ndc = vec2<f32>(x, y);
return out;
}
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) ndc: vec2<f32>,
}
struct FragmentInput {
@location(0) ndc: vec2<f32>,
}
struct FragmentOutput {
@location(0) color: vec4<f32>,
@builtin(frag_depth) depth: f32,
}
// ===== GRID CONFIGURATION =====
// Blender-like color scheme
const GRID_COLOR_BACKGROUND: vec3<f32> = vec3<f32>(0.18, 0.18, 0.18); // Light gray background/tiles
const GRID_COLOR_MINOR: vec3<f32> = vec3<f32>(0.28, 0.28, 0.28); // Medium gray minor lines
const GRID_COLOR_MAJOR: vec3<f32> = vec3<f32>(0.38, 0.38, 0.38); // Lighter gray major lines
const GRID_COLOR_X_AXIS: vec3<f32> = vec3<f32>(0.95, 0.3, 0.3); // Red X axis
const GRID_COLOR_Z_AXIS: vec3<f32> = vec3<f32>(0.3, 0.5, 0.95); // Blue Z axis
const GRID_ALPHA_BACKGROUND: f32 = 0.7;
const GRID_ALPHA_MINOR: f32 = 0.9;
const GRID_ALPHA_MAJOR: f32 = 1.0;
const GRID_ALPHA_AXIS: f32 = 1.0;
@fragment
fn frag_main(in: FragmentInput) -> FragmentOutput {
let ndc = in.ndc;
// Convert raw camera uniform to friendly structure
let camera = camera_from_raw(camera_raw);
// ===== PERSPECTIVE & ORTHOGRAPHIC =====
// Unproject NDC to world space
let clip_near = vec4<f32>(ndc.x, ndc.y, 0.0, 1.0);
var world_near_h = camera.inv_view_proj * clip_near;
let world_near = world_near_h.xyz / world_near_h.w;
// Detect camera type
let is_ortho = camera.proj[3][3] > 0.9;
var ray_origin: vec3<f32>;
var ray_dir: vec3<f32>;
var world_pos: vec3<f32>;
var t: f32;
if (is_ortho) {
// Orthographic: unproject far plane for direction
let clip_far = vec4<f32>(ndc.x, ndc.y, 1.0, 1.0);
var world_far_h = camera.inv_view_proj * clip_far;
let world_far = world_far_h.xyz / world_far_h.w;
ray_origin = world_near;
// DON'T normalize - preserves world-space derivative consistency
ray_dir = world_far - world_near;
// Intersect with y=0 plane
t = (0.0 - ray_origin.y) / ray_dir.y;
world_pos = ray_origin + ray_dir * t;
} else {
// Perspective: use pre-computed frustum rays
let uv = (ndc + 1.0) * 0.5;
// Bilinearly interpolate frustum rays
let ray_bottom = mix(camera.frustum_rays[0].xyz, camera.frustum_rays[1].xyz, uv.x);
let ray_top = mix(camera.frustum_rays[2].xyz, camera.frustum_rays[3].xyz, uv.x);
let view_ray = mix(ray_bottom, ray_top, uv.y);
// Transform view-space ray to world space (rotation only)
let world_ray = mat3x3<f32>(
camera.inv_view[0].xyz,
camera.inv_view[1].xyz,
camera.inv_view[2].xyz
) * view_ray;
ray_origin = camera.position;
ray_dir = world_ray; // Already normalized from CPU
// Intersect with y=0 plane
t = (0.0 - camera.position.y) / ray_dir.y;
world_pos = camera.position + ray_dir * t;
}
// Calculate derivatives BEFORE any branching
let coord = world_pos.xz;
let derivative = fwidth(coord);
// Check for invalid intersections
let is_parallel = abs(ray_dir.y) < 0.001;
// For perspective, reject rays pointing away from the ground plane (t < 0)
// For orthographic, only reject if parallel (no horizon line - grid fills screen like Blender)
let is_behind = !is_ortho && t < 0.0;
if (is_parallel || is_behind) {
discard;
// // Discard pixels that don't hit the ground
// var output: FragmentOutput;
// output.color = vec4<f32>(0.0, 0.0, 0.0, 0.0);
// output.depth = 1.0; // Far plane
// return output;
}
// Minor grid (every 1 unit)
let grid = abs(fract(coord - 0.5) - 0.5) / derivative;
let line_minor = min(grid.x, grid.y);
let minor_alpha = (1.0 - min(line_minor, 1.0)) * GRID_ALPHA_MINOR;
// Major grid (every 10 units)
let grid_major = abs(fract(coord / 10.0 - 0.5) - 0.5) / (derivative / 10.0);
let line_major = min(grid_major.x, grid_major.y);
let major_alpha = (1.0 - min(line_major, 1.0)) * GRID_ALPHA_MAJOR;
// Axes
let x_axis_dist = abs(world_pos.z) / derivative.y;
let z_axis_dist = abs(world_pos.x) / derivative.x;
let x_axis_alpha = (1.0 - min(x_axis_dist, 1.0)) * GRID_ALPHA_AXIS;
let z_axis_alpha = (1.0 - min(z_axis_dist, 1.0)) * GRID_ALPHA_AXIS;
// Priority-based color selection with background
var final_color = GRID_COLOR_BACKGROUND;
var final_alpha = GRID_ALPHA_BACKGROUND;
// Layer lines on top of background (higher priority = drawn on top)
if (minor_alpha > 0.01) {
final_color = GRID_COLOR_MINOR;
final_alpha = minor_alpha;
}
if (major_alpha > 0.01) {
final_color = GRID_COLOR_MAJOR;
final_alpha = major_alpha;
}
if (z_axis_alpha > 0.01) {
final_color = GRID_COLOR_Z_AXIS;
final_alpha = z_axis_alpha;
}
if (x_axis_alpha > 0.01) {
final_color = GRID_COLOR_X_AXIS;
final_alpha = x_axis_alpha;
}
// Calculate depth by transforming world position back through view and projection
let view_pos_depth = camera.view * vec4<f32>(world_pos, 1.0);
let clip_pos_depth = camera.proj * view_pos_depth;
let ndc_depth = clip_pos_depth.z / clip_pos_depth.w;
// Clamp depth to valid WebGPU range [0, 1]
let depth = clamp(ndc_depth, 0.0, 1.0);
var output_final: FragmentOutput;
output_final.color = vec4<f32>(final_color, final_alpha);
output_final.depth = depth;
return output_final;
}