// Composite quad for a captured UI layer (see `layer/render.rs`).
//
// The capture texture holds PREMULTIPLIED color: straight-alpha blending onto
// a transparent-black target accumulates `rgb * a` in the color channels. The
// group alpha therefore multiplies rgb AND a, and the pipeline blends with
// (One, OneMinusSrcAlpha) — premultiplied "over".
//
// Group 2 carries the per-quad composite params (`transform3d` support): a
// screen-space model matrix and a screen-space clip rect. Untransformed quads
// ride the same path with an identity matrix and an open clip sentinel (their
// ancestor clip was already clamped CPU-side by `clip_quad`); transformed
// quads keep full geometry and clip here instead — an axis-aligned rect can't
// clamp a rotated quad's vertices.
#import bevy_render::view::View
@group(0) @binding(0) var<uniform> view: View;
@group(1) @binding(0) var atlas_texture: texture_2d<f32>;
@group(1) @binding(1) var atlas_sampler: sampler;
// Mirrored byte-for-byte by `render/transform3d.rs::CompositeUniforms` (128
// bytes, guarded by `composite_uniforms_match_the_documented_wgsl_layout`).
// Offsets: model @0, clip_min @64, clip_max @72, edge_feather @80,
// image_size @88, radius @96, box_center @112, box_size @120. Pad names are
// digit-free (naga's namer appends `_` to digit-suffixed identifiers).
struct CompositeParams {
model: mat4x4<f32>,
clip_min: vec2<f32>,
clip_max: vec2<f32>,
edge_feather: f32,
pad_a: f32,
// The sampled texture's IMAGE size in texels: the layer's pixels occupy
// its top-left `image_size` — a bucket-allocated texture is larger (see
// `layer/render/store.rs`), and `uv` 0..1 spans the image, not the
// texture. Equal to the texture size for exactly-sized layers.
image_size: vec2<f32>,
// Rounded-corner mask: per-corner radii [TL, TR, BR, BL] (physical px,
// the node's layout-resolved values) over the UNCLIPPED border box.
// All-zero disables the mask (the edge_feather pattern); today only
// backdrop quads set it — frost clipped to the rounded panel.
radius: vec4<f32>,
box_center: vec2<f32>,
box_size: vec2<f32>,
}
@group(2) @binding(0) var<uniform> params: CompositeParams;
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) uv: vec2<f32>,
@location(1) alpha: f32,
// Homogeneous screen-space position (pre-divide) for the fragment clip
// test: dividing the perspective-correct-interpolated pair per fragment
// recovers the true screen position in `clip_min/max`'s space, independent
// of the render target (screen or a nested layer's capture texture).
@location(2) screen_pos: vec2<f32>,
@location(3) screen_w: f32,
}
// Signed distance from `point` (measured from the box CENTER) to a rounded
// box of `size` with per-quadrant `corner_radii` [TL, TR, BR, BL]; negative
// inside. Ported verbatim from bevy_ui_render's `ui.wgsl::sd_rounded_box`
// (including the .xy/.wz quadrant-select swap) so the frost's corner math is
// bit-identical to the one bevy_ui paints the node's own background with.
fn sd_rounded_box(point: vec2<f32>, size: vec2<f32>, corner_radii: vec4<f32>) -> f32 {
// If 0.0 < y then select bottom left (w) and bottom right corner radius (z).
// Else select top left (x) and top right corner radius (y).
let rs = select(corner_radii.xy, corner_radii.wz, 0.0 < point.y);
// w and z are swapped above so that both pairs are in left-to-right order, otherwise this second
// select statement would return the incorrect value for the bottom pair.
let radius = select(rs.x, rs.y, 0.0 < point.x);
// Vector from the corner closest to the point, to the point.
let corner_to_point = abs(point) - 0.5 * size;
// Vector from the center of the radius circle to the point.
let q = corner_to_point + radius;
// Length from center of the radius circle to the point, zeros a component if the point is not
// within the quadrant of the radius circle that is part of the curved corner.
let l = length(max(q, vec2(0.0)));
let m = min(max(q.x, q.y), 0.0);
return l + m - radius;
}
@vertex
fn vertex(
@location(0) position: vec2<f32>,
@location(1) uv: vec2<f32>,
@location(2) alpha: f32,
) -> VertexOutput {
var out: VertexOutput;
// Positions are physical screen px in the UI plane (z = 0); the model
// matrix is the layer's 3D transform in that same space (identity when
// untransformed). `w` is kept REAL through the projection — that is what
// buys perspective-correct UV interpolation — but `z` is flattened
// post-transform: the phase's view (the stock UI view, or an outer
// layer's capture view) projects with a near plane at the UI plane, and a
// rotated quad's depth excursions would otherwise be depth-clipped.
// Flattening is exactly the CSS projective flatten — the homography lives
// entirely in xy/w.
let world = params.model * vec4(position, 0.0, 1.0);
out.position = view.clip_from_world * vec4(world.xy, 0.0, world.w);
out.uv = uv;
out.alpha = alpha;
out.screen_pos = world.xy;
out.screen_w = world.w;
return out;
}
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
// Analytic edge AA for transformed quads: their diagonal silhouettes
// rasterize without MSAA, so coverage feathers over ~edge_feather px
// centered on the TRUE rect edge (uv 0/1 — the quad geometry is inflated
// by the same width, providing the outside half; see
// `clip_quad` with a negative inset). Derivatives convert uv distance to screen
// px per axis BEFORE the min — correct under anisotropic compression —
// and must be computed before the clip discard (uniform control flow).
// `edge_feather == 0` hard-disables the term: untransformed quads are
// CPU-clamped (uv_min/max inside [0,1] at clipped edges, where this
// distance would feather wrongly) and must stay pixel-identical.
let dist = min(in.uv, vec2(1.0) - in.uv);
let dist_px = dist / max(fwidth(in.uv), vec2(1e-6));
let edge_px = min(dist_px.x, dist_px.y);
let feathered = clamp(0.5 + edge_px / max(params.edge_feather, 1e-6), 0.0, 1.0);
var coverage = select(1.0, feathered, params.edge_feather > 0.0);
// True screen position (perspective divide) — shared by the rounded mask
// and the ancestor-clip test below.
let screen = in.screen_pos / in.screen_w;
// Rounded-corner mask (backdrop quads): clip coverage to the node's
// rounded border box, antialiased with bevy_ui's own convention —
// `saturate(0.5 - sd)` on the raw physical-px distance (ui.wgsl's
// `antialias`; deliberately no fwidth) — so the frost edge coincides
// with the node's painted rounded background. All-zero radii disable the
// term exactly (content quads, square backdrops stay pixel-identical).
let sd = sd_rounded_box(screen - params.box_center, params.box_size, params.radius);
let rounded = saturate(0.5 - sd);
coverage *= select(1.0, rounded, any(params.radius > vec4(0.0)));
// Ancestor clip of a transformed quad (screen-space rect vs. the true
// screen position) — a HARD cut, like every other overflow edge.
// Open-sentinel bounds make this a no-op for CPU-clamped/unclipped quads.
if any(screen < params.clip_min) || any(screen > params.clip_max) {
discard;
}
// Image UV -> texture UV. A bucketed texture holds the image in its
// top-left `image_size` texels with transparent padding beyond, so the
// lookup is rescaled and clamped to the image (bilinear footprint kept
// inside it — the same result clamp-to-edge gives on an exactly-sized
// texture; the edge-AA ring of a transformed quad deliberately samples
// past uv 0/1 and relies on that). Exactly-sized textures pass `in.uv`
// through untouched — bit-identical to the plain lookup.
let texture_size = vec2<f32>(textureDimensions(atlas_texture));
let image_px = clamp(in.uv * params.image_size, vec2<f32>(0.5), params.image_size - 0.5);
let uv = select(in.uv, image_px / texture_size, any(params.image_size != texture_size));
// Premultiplied output: coverage multiplies rgb AND a, like the group alpha.
return textureSample(atlas_texture, atlas_sampler, uv) * in.alpha * coverage;
}