Skip to main content

renderling/ui_slab/
shader.rs

1//! GPU shader entry points for the 2D/UI rendering pipeline.
2//!
3//! These shaders are compiled via rust-gpu and used by the `renderling-ui`
4//! crate's `UiRenderer`.
5
6use crabslab::{Id, Slab, SlabItem};
7use glam::{Vec2, Vec4, Vec4Swizzles};
8use spirv_std::{image::Image2dArray, spirv, Sampler};
9
10use super::{GradientType, UiDrawCallDescriptor, UiElementType, UiVertex, UiViewport};
11use crate::atlas::shader::AtlasTextureDescriptor;
12
13/// SDF for a rounded rectangle.
14///
15/// `p` is the point relative to the rectangle center.
16/// `half_ext` is the half-extents of the rectangle.
17/// `radii` are the corner radii: (top-left, top-right, bottom-right,
18/// bottom-left).
19fn sdf_rounded_rect(p: Vec2, half_ext: Vec2, radii: Vec4) -> f32 {
20    // Select the appropriate corner radius based on quadrant.
21    let r = if p.x > 0.0 {
22        if p.y > 0.0 {
23            // bottom-right (in screen coords, +Y is down)
24            radii.z
25        } else {
26            // top-right
27            radii.y
28        }
29    } else if p.y > 0.0 {
30        // bottom-left
31        radii.w
32    } else {
33        // top-left
34        radii.x
35    };
36    let q = p.abs() - half_ext + Vec2::splat(r);
37    let outside = q.max(Vec2::ZERO).length();
38    let inside = q.x.max(q.y).min(0.0);
39    outside + inside - r
40}
41
42/// SDF for a circle.
43fn sdf_circle(p: Vec2, radius: f32) -> f32 {
44    p.length() - radius
45}
46
47/// SDF for an ellipse (approximation using the Iq formula).
48fn sdf_ellipse(p: Vec2, radii: Vec2) -> f32 {
49    // Simplified ellipse SDF (not exact but good for UI).
50    let p_norm = p / radii;
51    let d = p_norm.length() - 1.0;
52    d * radii.x.min(radii.y)
53}
54
55/// Evaluate a gradient at the given local UV coordinate.
56fn eval_gradient(
57    gradient_type: u32,
58    start: Vec2,
59    end: Vec2,
60    radius: f32,
61    color_start: Vec4,
62    color_end: Vec4,
63    local_uv: Vec2,
64) -> Vec4 {
65    let gt = GradientType::from_u32(gradient_type);
66    match gt {
67        GradientType::None => color_start,
68        GradientType::Linear => {
69            let dir = end - start;
70            let len_sq = dir.dot(dir);
71            let t = if len_sq > 0.0 {
72                let t = (local_uv - start).dot(dir) / len_sq;
73                t.clamp(0.0, 1.0)
74            } else {
75                0.0
76            };
77            color_start + (color_end - color_start) * t
78        }
79        GradientType::Radial => {
80            let d = (local_uv - start).length();
81            let t = if radius > 0.0 {
82                (d / radius).clamp(0.0, 1.0)
83            } else {
84                0.0
85            };
86            color_start + (color_end - color_start) * t
87        }
88    }
89}
90
91/// 2D UI vertex shader.
92///
93/// For SDF-based elements (Rectangle, Circle, Ellipse), this generates
94/// 6 vertices (2 triangles) per instance from the element's position and
95/// size, reading from the slab. The vertex index (0..5) selects which
96/// corner of the quad.
97///
98/// For Path elements, the vertex data is read directly from the slab
99/// (pre-tessellated vertices).
100#[spirv(vertex)]
101pub fn ui_vertex(
102    #[spirv(vertex_index)] vertex_index: u32,
103    #[spirv(instance_index)] draw_call_id: Id<UiDrawCallDescriptor>,
104    #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32],
105    out_uv: &mut Vec2,
106    out_color: &mut Vec4,
107    #[spirv(flat)] out_draw_call_id: &mut u32,
108    #[spirv(position)] out_clip_pos: &mut Vec4,
109) {
110    let viewport: UiViewport = slab.read_unchecked(Id::new(0));
111    let draw_call: UiDrawCallDescriptor = slab.read_unchecked(draw_call_id);
112
113    *out_draw_call_id = draw_call_id.inner();
114
115    match draw_call.element_type {
116        UiElementType::Path => {
117            // For path elements, the draw_call stores an offset into the
118            // slab where UiVertex data lives. We read the vertex directly.
119            // The atlas_descriptor_id field stores the vertex slab offset.
120            let vertex_offset = draw_call.atlas_descriptor_id.inner();
121            let vertex_id =
122                Id::<UiVertex>::new(vertex_offset + vertex_index * UiVertex::SLAB_SIZE as u32);
123            let vertex: UiVertex = slab.read_unchecked(vertex_id);
124            *out_uv = vertex.uv;
125            *out_color = vertex.color;
126
127            let pos4 = viewport.projection
128                * Vec4::new(vertex.position.x, vertex.position.y, draw_call.z, 1.0);
129            *out_clip_pos = pos4;
130        }
131        _ => {
132            // SDF-based element: generate quad vertices.
133            // Quad corners in CCW order for two triangles:
134            //   0: top-left, 1: bottom-left, 2: bottom-right,
135            //   3: bottom-right, 4: top-right, 5: top-left
136            let vi = vertex_index % 6;
137            let (corner_x, corner_y) = match vi {
138                0 => (0.0f32, 0.0f32), // top-left
139                1 => (0.0, 1.0),       // bottom-left
140                2 => (1.0, 1.0),       // bottom-right
141                3 => (1.0, 1.0),       // bottom-right
142                4 => (1.0, 0.0),       // top-right
143                _ => (0.0, 0.0),       // top-left
144            };
145
146            let local_uv = Vec2::new(corner_x, corner_y);
147            *out_uv = local_uv;
148            *out_color = draw_call.fill_color;
149
150            let screen_pos = draw_call.position
151                + Vec2::new(corner_x * draw_call.size.x, corner_y * draw_call.size.y);
152
153            let pos4 =
154                viewport.projection * Vec4::new(screen_pos.x, screen_pos.y, draw_call.z, 1.0);
155            *out_clip_pos = pos4;
156        }
157    }
158}
159
160/// 2D UI fragment shader.
161///
162/// Evaluates SDF shapes, gradients, textures, and text glyphs depending
163/// on the element type.
164#[spirv(fragment)]
165pub fn ui_fragment(
166    #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32],
167    #[spirv(descriptor_set = 0, binding = 1)] atlas: &Image2dArray,
168    #[spirv(descriptor_set = 0, binding = 2)] atlas_sampler: &Sampler,
169    in_uv: Vec2,
170    in_color: Vec4,
171    #[spirv(flat)] in_draw_call_id: u32,
172    frag_color: &mut Vec4,
173) {
174    let draw_call_id = Id::<UiDrawCallDescriptor>::new(in_draw_call_id);
175    let draw_call: UiDrawCallDescriptor = slab.read_unchecked(draw_call_id);
176    #[allow(unused_assignments)]
177    let mut color = Vec4::ZERO;
178
179    match draw_call.element_type {
180        UiElementType::Path => {
181            // Pre-tessellated path: start with vertex color.
182            color = in_color;
183            // If an atlas texture is set, sample it and multiply.
184            if !draw_call.atlas_texture_id.is_none() {
185                let atlas_tex: AtlasTextureDescriptor =
186                    slab.read_unchecked(draw_call.atlas_texture_id);
187                let viewport: UiViewport = slab.read_unchecked(Id::new(0));
188                let atlas_uv = atlas_tex.uv(in_uv, viewport.atlas_size);
189                let sample: Vec4 = atlas.sample_by_lod(*atlas_sampler, atlas_uv, 0.0);
190                color *= sample;
191            }
192        }
193        UiElementType::TextGlyph => {
194            // Text glyph: sample the glyph texture and multiply by color.
195            let atlas_tex: AtlasTextureDescriptor = slab.read_unchecked(draw_call.atlas_texture_id);
196            let viewport: UiViewport = slab.read_unchecked(Id::new(0));
197            let atlas_uv = atlas_tex.uv(in_uv, viewport.atlas_size);
198            let sample: Vec4 = atlas.sample_by_lod(*atlas_sampler, atlas_uv, 0.0);
199            color = draw_call.fill_color;
200            color.w *= sample.w;
201        }
202        UiElementType::Image => {
203            // Textured quad: sample the atlas texture.
204            let atlas_tex: AtlasTextureDescriptor = slab.read_unchecked(draw_call.atlas_texture_id);
205            let viewport: UiViewport = slab.read_unchecked(Id::new(0));
206            let atlas_uv = atlas_tex.uv(in_uv, viewport.atlas_size);
207            color = atlas.sample_by_lod(*atlas_sampler, atlas_uv, 0.0);
208            // Modulate with fill color (tint).
209            color *= draw_call.fill_color;
210        }
211        _ => {
212            // SDF-based element (Rectangle, Circle, Ellipse).
213            let half_size = draw_call.size * 0.5;
214            // Convert UV (0..1) to element-local coords centered on element
215            // center.
216            let local_pos = (in_uv - Vec2::splat(0.5)) * draw_call.size;
217
218            let distance = match draw_call.element_type {
219                UiElementType::Rectangle => {
220                    sdf_rounded_rect(local_pos, half_size, draw_call.corner_radii)
221                }
222                UiElementType::Circle => {
223                    let radius = half_size.x.min(half_size.y);
224                    sdf_circle(local_pos, radius)
225                }
226                UiElementType::Ellipse => sdf_ellipse(local_pos, half_size),
227                _ => 0.0,
228            };
229
230            // Evaluate fill color (possibly gradient).
231            let fill = eval_gradient(
232                draw_call.gradient.gradient_type,
233                draw_call.gradient.start,
234                draw_call.gradient.end,
235                draw_call.gradient.radius,
236                draw_call.gradient.color_start,
237                draw_call.gradient.color_end,
238                in_uv,
239            );
240            // If gradient is None, use the solid fill color.
241            let fill = if draw_call.gradient.gradient_type == 0 {
242                draw_call.fill_color
243            } else {
244                fill
245            };
246
247            // Anti-aliased edge using smoothstep.
248            let aa_width = 1.0; // 1 pixel of anti-aliasing
249            let fill_alpha = 1.0 - crate::math::smoothstep(-aa_width, aa_width, distance);
250
251            if draw_call.border_width > 0.0 {
252                // Border: the border region is between the outer edge and
253                // (outer edge - border_width).
254                let inner_distance = distance + draw_call.border_width;
255                let border_alpha =
256                    1.0 - crate::math::smoothstep(-aa_width, aa_width, inner_distance);
257                // Coverage weights.
258                let border_weight = fill_alpha - border_alpha;
259                let fill_weight = border_alpha;
260                let total = fill_alpha;
261                // Straight-alpha RGB: weighted blend of border and fill.
262                if total > 0.0 {
263                    let rgb = (draw_call.border_color.xyz() * border_weight
264                        + fill.xyz() * fill_weight)
265                        / total;
266                    let a =
267                        (draw_call.border_color.w * border_weight + fill.w * fill_weight) / total;
268                    color = rgb.extend(a * total);
269                } else {
270                    color = Vec4::ZERO;
271                }
272            } else {
273                color = fill;
274                color.w *= fill_alpha;
275            }
276        }
277    }
278
279    // Apply element opacity.
280    color.w *= draw_call.opacity;
281
282    // Premultiply RGB by final alpha for premultiplied-alpha blending.
283    color = (color.xyz() * color.w).extend(color.w);
284
285    *frag_color = color;
286}