Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-renderer/src/rendering/wgpu/shaders/rect_shader.wgsl
1struct Uniforms {
2 viewport_size: vec2<f32>,
3 // Padding necessary to ensure that the uniforms is 16 bytes. Some wgpu-supported devices (such as webgl) require
4 // buffer bindings to be a multiple of 16 bytes.
5 padding: vec2<f32>
6}
7 
8const EPSILON: f32 = 0.0000001;
9const PI: f32 = 3.141592653589793;
10 
11@group(0) @binding(0) var<uniform> uniforms: Uniforms;
12 
13struct RectVertexShaderInput {
14 // The position of the vertex in normalized device coordinates.
15 @location(0) vertex_position: vec2<f32>,
16 // Bounds of the item in screen coordinates. Origin is contained in `xy`, size is contained in `zw`.
17 @location(1) bounds: vec4<f32>,
18 @location(2) background_start: vec2<f32>,
19 @location(3) background_start_color: vec4<f32>,
20 @location(4) background_end: vec2<f32>,
21 @location(5) background_end_color: vec4<f32>,
22 // Width of the border in the order top, left, right, bottom.
23 @location(6) border_width: vec4<f32>,
24 @location(7) border_start: vec2<f32>,
25 @location(8) border_start_color: vec4<f32>,
26 @location(9) border_end: vec2<f32>,
27 @location(10) border_end_color: vec4<f32>,
28 // Corner radius in the order top_left, top_right, bottom_left, bottom_right.
29 @location(11) corner_radius: vec4<f32>,
30 // The sigma and padding factor values packed into a single vec2. We pack them together in order
31 // to reduce the total number of attributes, which maxes out at 16. See here:
32 // https://docs.rs/wgpu/latest/wgpu/struct.Limits.html#structfield.max_vertex_attributes
33 @location(12) drop_shadow_data: vec2<f32>,
34 // The length of the dash and the gaps for the x and y dimensions, packed into a single vec3.
35 @location(13) dashed_border_data: vec3<f32>,
36};
37 
38struct RectVertexShaderOutput {
39 @builtin(position) position: vec4<f32>,
40 @location(0) background_start: vec2<f32>,
41 @location(1) background_start_color: vec4<f32>,
42 @location(2) background_end: vec2<f32>,
43 @location(3) background_end_color: vec4<f32>,
44 @location(4) border_width: vec4<f32>,
45 @location(5) border_start: vec2<f32>,
46 @location(6) border_start_color: vec4<f32>,
47 @location(7) border_end: vec2<f32>,
48 @location(8) border_end_color: vec4<f32>,
49 @location(9) rect_corner: vec2<f32>,
50 @location(10) rect_center: vec2<f32>,
51 @location(11) corner_radius: vec4<f32>,
52 @location(12) drop_shadow_data: vec2<f32>,
53 @location(13) dashed_border_data: vec3<f32>,
54};
55 
56@vertex
57fn vs_main(
58 in: RectVertexShaderInput,
59) -> RectVertexShaderOutput {
60 var out: RectVertexShaderOutput;
61 var origin: vec2<f32> = in.bounds.xy;
62 var size: vec2<f32> = in.bounds.zw;
63 var pixel_pos: vec2<f32> = in.vertex_position * size + origin;
64 // Convert the position of the item from screen coordinates into normalized device coordinates
65 var ndc_position: vec2<f32> = pixel_pos / uniforms.viewport_size * vec2(2.0, -2.0) + vec2(-1.0, 1.0);
66 
67 out.position = vec4<f32>(ndc_position, 0.0, 1.0);
68 out.background_start = in.background_start * size + origin;
69 out.background_start_color = in.background_start_color;
70 out.background_end = in.background_end * size + origin;
71 out.background_end_color = in.background_end_color;
72 out.border_start = in.border_start * size + origin;
73 out.border_start_color = in.border_start_color;
74 out.border_end = in.border_end * size + origin;
75 out.border_end_color = in.border_end_color;
76 out.border_width = in.border_width;
77 out.corner_radius = in.corner_radius;
78 out.rect_corner = size / 2.;
79 out.rect_center = origin + out.rect_corner;
80 out.drop_shadow_data = in.drop_shadow_data;
81 out.dashed_border_data = in.dashed_border_data;
82 
83 return out;
84}
85 
86@fragment
87fn rect_fs_main(in: RectVertexShaderOutput) -> @location(0) vec4<f32> {
88 var background_color: vec4<f32> = derive_color(
89 in.position.xy,
90 in.background_start,
91 in.background_end,
92 in.background_start_color,
93 in.background_end_color
94 );
95 var border_color: vec4<f32> = derive_color(
96 in.position.xy,
97 in.border_start,
98 in.border_end,
99 in.border_start_color,
100 in.border_end_color
101 );
102 
103 // There are actually two different radii at play here - the inner
104 // (background) and outer (shape) radii. The inner radius is equal to the
105 // outer radius minus the border width, in order for the two curves to
106 // maintain a constant distance from each other.
107 var inner_corner_radius: f32;
108 var outer_corner_radius: f32;
109 
110 var border_inner_corner: vec2<f32> = in.rect_corner;
111 if in.position.y >= in.rect_center.y {
112 // Bottom half
113 border_inner_corner.y -= in.border_width.z;
114 if in.position.x >= in.rect_center.x {
115 // Bottom right quadrant
116 border_inner_corner.x -= in.border_width.y;
117 outer_corner_radius = in.corner_radius.w;
118 inner_corner_radius = max(0.0, outer_corner_radius - in.border_width.z);
119 } else {
120 // Bottom left quadrant
121 border_inner_corner.x -= in.border_width.w;
122 outer_corner_radius = in.corner_radius.z;
123 inner_corner_radius = max(0.0, outer_corner_radius - in.border_width.z);
124 }
125 } else {
126 // Top half
127 border_inner_corner.y -= in.border_width.x;
128 if in.position.x >= in.rect_center.x {
129 // Top right quadrant
130 border_inner_corner.x -= in.border_width.y;
131 outer_corner_radius = in.corner_radius.y;
132 inner_corner_radius = max(0.0, outer_corner_radius - in.border_width.x);
133 } else {
134 // Top left quadrant
135 border_inner_corner.x -= in.border_width.w;
136 outer_corner_radius = in.corner_radius.x;
137 inner_corner_radius = max(0.0, outer_corner_radius - in.border_width.x);
138 }
139 }
140 
141 var rect_origin: vec2<f32> = in.rect_center - in.rect_corner;
142 var outer_distance: f32 = distance_from_rect(in.position.xy, in.rect_center, in.rect_corner, outer_corner_radius);
143 var inner_distance: f32 = distance_from_rect(in.position.xy, in.rect_center, border_inner_corner, inner_corner_radius);
144 
145 var drop_shadow_sigma = in.drop_shadow_data.x;
146 var drop_shadow_padding_factor = in.drop_shadow_data.y;
147 if drop_shadow_sigma > 0.0 {
148 var rect_size: vec2<f32> = in.rect_corner * 2.0;
149 // When we are rendering a drop shadow we need to pass in the positions
150 // of the original rect, so we figure them out from the padding.
151 // Note we subtract twice the padding, because the padding is specified
152 // in terms of padding on a single side.
153 var shadowed_rect_origin: vec2<f32> = rect_origin + drop_shadow_padding_factor;
154 var shadowed_rect_size: vec2<f32> = rect_size - 2.0 * drop_shadow_padding_factor;
155 background_color.a *= rounded_box_shadow(
156 shadowed_rect_origin,
157 shadowed_rect_origin + shadowed_rect_size,
158 in.position.xy,
159 drop_shadow_sigma,
160 outer_corner_radius
161 );
162 } else {
163 // Adjust the opacity of the border color based on where the pixel lies
164 // between the background and the border_width.
165 border_color.a *= saturate(inner_distance + 0.5);
166 
167 // Force the alpha value to 0 (fully transparent) if the pixel is
168 // outside the border_width.
169 //
170 // When we are outside the border, outer_distance is a larger positive
171 // value than inner_distance. When we are inside the border itself,
172 // outer_distance is negative and inner_distance is positive. When we
173 // are inside the inner border edge, outer_distance is more negative
174 // than inner_distance.
175 border_color.a *= f32(inner_distance > outer_distance);
176 
177 var rect_bottom_right = in.rect_center + in.rect_corner;
178 var pos_from_origin = in.position.xy - rect_origin;
179 
180 // Masks for pixels outside of inner rectangle or on border
181 var is_horizontal_border = (in.position.y <= rect_origin.y + in.border_width.x) || (in.position.y >= rect_bottom_right.y - in.border_width.z);
182 var is_vertical_border = (in.position.x <= rect_origin.x + in.border_width.w) || (in.position.x >= rect_bottom_right.x - in.border_width.y);
183 
184 var dash_length = in.dashed_border_data.x;
185 var gap_lengths = in.dashed_border_data.yz;
186 
187 // Get length along the dash and gap segment and determine if pixel is in dash or gap
188 var length_on_dash_and_gap_segment_x = pos_from_origin.x % (dash_length + gap_lengths.x);
189 var length_on_dash_and_gap_segment_y = pos_from_origin.y % (dash_length + gap_lengths.y);
190 var is_horizontal_dash = is_horizontal_border && (length_on_dash_and_gap_segment_x < dash_length);
191 var is_vertical_dash = is_vertical_border && (length_on_dash_and_gap_segment_y < dash_length);
192 
193 // Mask out any gaps in the border
194 border_color.a *= f32(dash_length <= 0.0 || is_horizontal_dash || is_vertical_dash);
195 
196 // Perform proper alpha blending on the two colors, avoiding a
197 // divide-by-zero if both colors are fully transparent.
198 //
199 // See formula for "over" compositing here: https://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending
200 var alpha: f32 = border_color.a + background_color.a * (1.0 - border_color.a);
201 var new_background_color: vec3<f32> = (border_color.rgb * border_color.a + background_color.rgb * background_color.a * (1.0 - border_color.a)) / (alpha + EPSILON);
202 background_color = vec4(new_background_color, alpha);
203 }
204 
205 // If there's a corner radius we need to do some anti aliasing to smooth out the rounded corner effect.
206 if outer_corner_radius > 0. {
207 background_color.a *= 1.0 - saturate(outer_distance + 0.5);
208 }
209 
210 return background_color;
211}
212 
213fn derive_color(
214 position: vec2<f32>,
215 start: vec2<f32>,
216 end: vec2<f32>,
217 start_color: vec4<f32>,
218 end_color: vec4<f32>
219) -> vec4<f32> {
220 var adjusted_end: vec2<f32> = end - start;
221 var h: f32 = dot(position - start, adjusted_end) / dot(adjusted_end, adjusted_end);
222 return mix(start_color, end_color, h);
223}
224 
225// Based on the fragement position and the center of the quad, select one of the 4 radi.
226// Order matches CSS border radius attribute:
227// radi.x = top-left, radi.y = top-right, radi.z = bottom-right, radi.w = bottom-left
228fn select_border_radius(radi: vec4<f32>, position: vec2<f32>, center: vec2<f32>) -> f32 {
229 var rx = radi.x;
230 var ry = radi.y;
231 rx = select(radi.x, radi.y, position.x > center.x);
232 ry = select(radi.w, radi.z, position.x > center.x);
233 rx = select(rx, ry, position.y > center.y);
234 return rx;
235}
236 
237fn distance_from_rect(pixel_pos: vec2<f32>, rect_center: vec2<f32>, rect_corner: vec2<f32>, corner_radius: f32) -> f32 {
238 var p: vec2<f32> = pixel_pos - rect_center;
239 var q: vec2<f32> = abs(p) - rect_corner + corner_radius;
240 return length(max(q, vec2(0.0))) + min(max(q.x, q.y), 0.0) - corner_radius;
241}
242 
243// Drop shadow code *heavily* inspired by this post:
244// http://madebyevan.com/shaders/fast-rounded-rectangle-shadows/
245 
246// Return the mask for the shadow of a box from lower to upper
247fn rounded_box_shadow(lower: vec2<f32>, upper: vec2<f32>, in_point: vec2<f32>, sigma: f32, corner: f32) -> f32 {
248 // Center everything to make the math easier
249 var center: vec2<f32> = (lower + upper) * 0.5;
250 var half_size: vec2<f32> = (upper - lower) * 0.5;
251 var point = in_point - center;
252 
253 // The signal is only non-zero in a limited range, so don't waste samples
254 var low: f32 = point.y - half_size.y;
255 var high: f32 = point.y + half_size.y;
256 var start: f32 = clamp(-3.0 * sigma, low, high);
257 var end: f32 = clamp(3.0 * sigma, low, high);
258 
259 // Accumulate samples (we can get away with surprisingly few samples)
260 var step: f32 = (end - start) / 4.0;
261 var y: f32 = start + step * 0.5;
262 var value: f32 = 0.0;
263 for (var i = 0; i < 4; i++) {
264 value += rounded_box_shadow_x(point.x, point.y - y, sigma, corner, half_size) * gaussian(y, sigma) * step;
265 y += step;
266 }
267 
268 return value;
269}
270 
271// Return the blurred mask along the x dimension
272fn rounded_box_shadow_x(x: f32, y: f32, sigma: f32, corner: f32, half_size: vec2<f32>) -> f32 {
273 var delta: f32 = min(half_size.y - corner - abs(y), 0.0);
274 var curved: f32 = half_size.x - corner + sqrt(max(0.0, corner * corner - delta * delta));
275 var integral: vec2<f32> = 0.5 + 0.5 * erf((x + vec2(-curved, curved)) * (sqrt(0.5) / sigma));
276 return integral.y - integral.x;
277}
278 
279// This approximates the error function, needed for the gaussian integral
280fn erf(x: vec2<f32>) -> vec2<f32> {
281 var s = sign(x);
282 var a = abs(x);
283 var denom = 1.0 + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a;
284 denom *= denom;
285 return s - s / (denom * denom);
286}
287 
288// A standard gaussian function, used for weighting samples
289fn gaussian(x: f32, sigma: f32) -> f32 {
290 return exp(-(x * x) / (2.0 * sigma * sigma)) / (sqrt(2.0 * PI) * sigma);
291}
292