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/platform/mac/rendering/metal/shaders/shaders.metal
1#include <metal_stdlib>
2 
3using namespace metal;
4 
5#include "shader_types.h"
6 
7constant float EPSILON = 0.00001;
8 
9// Vertex shader outputs and fragment shader inputs
10struct RectFragmentData
11{
12 float4 position [[position]];
13 float2 pixel_position [[pixel_position]];
14 float2 rect_origin;
15 float2 rect_size;
16 float2 rect_center;
17 float2 rect_corner;
18 float border_top;
19 float border_right;
20 float border_bottom;
21 float border_left;
22 float corner_radius_top_left;
23 float corner_radius_top_right;
24 float corner_radius_bottom_left;
25 float corner_radius_bottom_right;
26 float2 background_start;
27 float2 background_end;
28 float4 background_start_color;
29 float4 background_end_color;
30 float2 border_start;
31 float2 border_end;
32 float4 border_start_color;
33 float4 border_end_color;
34 float2 texture_coordinate;
35 bool is_icon;
36 float4 icon_color;
37 float2 drop_shadow_offsets;
38 float4 drop_shadow_color;
39 float drop_shadow_sigma;
40 float drop_shadow_padding_factor;
41 float dash_length;
42 float2 gap_lengths;
43};
44 
45struct GlyphFragmentData
46{
47 float4 position [[position]];
48 float2 rect_center;
49 float2 rect_corner;
50 float2 texture_coordinate;
51 float fade_alpha;
52 float4 color;
53 bool is_emoji;
54};
55 
56 
57float distance_from_rect(vector_float2 pixel_pos, vector_float2 rect_center, vector_float2 rect_corner, float corner_radius) {
58 vector_float2 p = pixel_pos - rect_center;
59 vector_float2 q = abs(p) - rect_corner + corner_radius;
60 return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - corner_radius;
61}
62 
63float4 derive_color(float2 pixel_pos, float2 start, float2 end, float4 start_color, float4 end_color) {
64 float2 adjusted_end = end - start;
65 float h = dot(pixel_pos - start, adjusted_end) / dot(adjusted_end, adjusted_end);
66 return mix(start_color, end_color, h);
67}
68 
69vertex RectFragmentData
70rect_vertex_shader(
71 uint vertex_id [[vertex_id]],
72 uint instance_id [[instance_id]],
73 constant float2 *vertices [[buffer(0)]],
74 constant PerRectUniforms *glyph_uniforms [[buffer(1)]],
75 constant Uniforms *uniforms [[buffer(2)]])
76{
77 const constant PerRectUniforms *rect = &glyph_uniforms[instance_id];
78 
79 float2 pixel_pos = vertices[vertex_id] * rect->size + rect->origin;
80 float2 device_pos = pixel_pos / uniforms->viewport_size * float2(2.0, -2.0) + float2(-1.0, 1.0);
81 
82 RectFragmentData out;
83 out.position = float4(device_pos, 0.0, 1.0);
84 out.pixel_position = pixel_pos;
85 out.rect_origin = rect->origin;
86 out.rect_size = rect->size;
87 out.rect_corner = rect->size / 2.0;
88 out.rect_center = rect->origin + out.rect_corner;
89 out.border_top = rect->border_top;
90 out.border_right = rect->border_right;
91 out.border_bottom = rect->border_bottom;
92 out.border_left = rect->border_left;
93 out.corner_radius_top_left = rect->corner_radius_top_left;
94 out.corner_radius_top_right = rect->corner_radius_top_right;
95 out.corner_radius_bottom_left = rect->corner_radius_bottom_left;
96 out.corner_radius_bottom_right = rect->corner_radius_bottom_right;
97 out.background_start = rect->background_start * rect->size + rect->origin;
98 out.background_end = rect->background_end * rect->size + rect->origin;
99 out.background_start_color = rect->background_start_color;
100 out.background_end_color = rect->background_end_color;
101 out.border_start = rect->border_start * rect->size + rect->origin;
102 out.border_end = rect->border_end * rect->size + rect->origin;
103 out.border_start_color = rect->border_start_color;
104 out.border_end_color = rect->border_end_color;
105 out.texture_coordinate = vertices[vertex_id];
106 out.is_icon = rect->is_icon;
107 out.icon_color = rect->icon_color;
108 out.drop_shadow_offsets = rect->drop_shadow_offsets;
109 out.drop_shadow_color = rect->drop_shadow_color;
110 out.drop_shadow_sigma = rect->drop_shadow_sigma;
111 out.drop_shadow_padding_factor = rect->drop_shadow_padding_factor;
112 out.dash_length = rect->dash_length;
113 out.gap_lengths = rect->gap_lengths;
114 return out;
115}
116 
117// Drop shadow code *heavily* inspired by this post:
118// http://madebyevan.com/shaders/fast-rounded-rectangle-shadows/
119 
120// A standard gaussian function, used for weighting samples
121float gaussian(float x, float sigma) {
122 const float pi = 3.141592653589793;
123 return exp(-(x * x) / (2.0 * sigma * sigma)) / (sqrt(2.0 * pi) * sigma);
124}
125 
126// This approximates the error function, needed for the gaussian integral
127float2 erf(float2 x) {
128 float2 s = sign(x), a = abs(x);
129 x = 1.0 + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a;
130 x *= x;
131 return s - s / (x * x);
132}
133 
134// Return the blurred mask along the x dimension
135float roundedBoxShadowX(float x, float y, float sigma, float corner, float2 halfSize) {
136 float delta = min(halfSize.y - corner - abs(y), 0.0);
137 float curved = halfSize.x - corner + sqrt(max(0.0, corner * corner - delta * delta));
138 float2 integral = 0.5 + 0.5 * erf((x + float2(-curved, curved)) * (sqrt(0.5) / sigma));
139 return integral.y - integral.x;
140}
141 
142// Return the mask for the shadow of a box from lower to upper
143float roundedBoxShadow(float2 lower, float2 upper, float2 point, float sigma, float corner) {
144 // Center everything to make the math easier
145 float2 center = (lower + upper) * 0.5;
146 float2 halfSize = (upper - lower) * 0.5;
147 point -= center;
148 
149 // The signal is only non-zero in a limited range, so don't waste samples
150 float low = point.y - halfSize.y;
151 float high = point.y + halfSize.y;
152 float start = clamp(-3.0 * sigma, low, high);
153 float end = clamp(3.0 * sigma, low, high);
154 
155 // Accumulate samples (we can get away with surprisingly few samples)
156 float step = (end - start) / 4.0;
157 float y = start + step * 0.5;
158 float value = 0.0;
159 for (int i = 0; i < 4; i++) {
160 value += roundedBoxShadowX(point.x, point.y - y, sigma, corner, halfSize) * gaussian(y, sigma) * step;
161 y += step;
162 }
163 
164 return value;
165}
166 
167fragment float4 rect_fragment_shader(
168 RectFragmentData in [[stage_in]],
169 constant Uniforms *uniforms [[buffer(0)]])
170{
171 float outer_distance;
172 float inner_distance;
173 // There are actually two different radii at play here - the inner
174 // (background) and outer (shape) radii. The inner radius is equal to the
175 // outer radius minus the border width, in order for the two curves to
176 // maintain a constant distance from each other.
177 float outer_corner_radius;
178 float inner_corner_radius;
179 
180 // Length along the perimeter of (rounded) rectangle, starting from top left.
181 float length_along = 0.;
182 float2 pos_from_origin = in.position.xy - in.rect_origin;
183 
184 float2 border_inner_corner = in.rect_corner;
185 if (in.position.y >= in.rect_center.y) {
186 // Bottom half
187 border_inner_corner.y -= in.border_bottom;
188 if (in.position.x >= in.rect_center.x) {
189 // Bottom right quadrant
190 border_inner_corner.x -= in.border_right;
191 outer_corner_radius = in.corner_radius_bottom_right;
192 inner_corner_radius = max(0.0, outer_corner_radius - in.border_bottom);
193 } else {
194 // Bottom left quadrant
195 border_inner_corner.x -= in.border_left;
196 outer_corner_radius = in.corner_radius_bottom_left;
197 inner_corner_radius = max(0.0, outer_corner_radius - in.border_bottom);
198 }
199 } else {
200 // Top half
201 border_inner_corner.y -= in.border_top;
202 if (in.position.x >= in.rect_center.x) {
203 // Top right quadrant
204 border_inner_corner.x -= in.border_right;
205 outer_corner_radius = in.corner_radius_top_right;
206 inner_corner_radius = max(0.0, outer_corner_radius - in.border_top);
207 } else {
208 // Top left quadrant
209 border_inner_corner.x -= in.border_left;
210 outer_corner_radius = in.corner_radius_top_left;
211 inner_corner_radius = max(0.0, outer_corner_radius - in.border_top);
212 }
213 }
214 
215 float2 rect_bottom_right = in.rect_origin + in.rect_size;
216 
217 outer_distance = distance_from_rect(in.position.xy, in.rect_center, in.rect_corner, outer_corner_radius);
218 inner_distance = distance_from_rect(in.position.xy, in.rect_center, border_inner_corner, inner_corner_radius);
219 
220 float4 color;
221 if (in.drop_shadow_sigma > 0) {
222 color = in.drop_shadow_color;
223 // When we are rendering a drop shadow we need to pass in the positions
224 // of the original rect, so we figure them out from the padding.
225 // Note we subtract twice the padding, because the padding is specified
226 // in terms of padding on a single side.
227 float2 shadowed_rect_origin = in.rect_origin + in.drop_shadow_padding_factor;
228 float2 shadowed_rect_size = in.rect_size - 2 * in.drop_shadow_padding_factor;
229 color.a *= roundedBoxShadow(
230 shadowed_rect_origin,
231 shadowed_rect_origin + shadowed_rect_size,
232 in.pixel_position,
233 in.drop_shadow_sigma,
234 outer_corner_radius);
235 } else {
236 // Solid fill case (not a drop shadow)
237 float4 background_color = derive_color(in.position.xy, in.background_start, in.background_end, in.background_start_color, in.background_end_color);
238 float4 border_color = derive_color(in.position.xy, in.border_start, in.border_end, in.border_start_color, in.border_end_color);
239 
240 // Adjust the opacity of the border color based on where the pixel lies
241 // between the background and the border.
242 border_color.a *= saturate(inner_distance + 0.5);
243 
244 // Force the alpha value to 0 (fully transparent) if the pixel is
245 // outside the border.
246 //
247 // When we are outside the border, outer_distance is a larger positive
248 // value than inner_distance. When we are inside the border itself,
249 // outer_distance is negative and inner_distance is positive. When we
250 // are inside the inner border edge, outer_distance is more negative
251 // than inner_distance.
252 border_color.a *= inner_distance > outer_distance;
253 
254 // Masks for pixels outside of inner rectangle or on border
255 bool is_horizontal_border = (in.position.y <= in.rect_origin.y + in.border_top) || (in.position.y >= rect_bottom_right.y - in.border_bottom);
256 bool is_vertical_border = (in.position.x <= in.rect_origin.x + in.border_left) || (in.position.x >= rect_bottom_right.x - in.border_right);
257 
258 // Get length along the dash and gap segment and determine if pixel is in dash or gap
259 float length_on_dash_and_gap_segment_x = fmod(pos_from_origin.x, in.dash_length + in.gap_lengths.x);
260 float length_on_dash_and_gap_segment_y = fmod(pos_from_origin.y, in.dash_length + in.gap_lengths.y);
261 bool is_horizontal_dash = is_horizontal_border && (length_on_dash_and_gap_segment_x < in.dash_length);
262 bool is_vertical_dash = is_vertical_border && (length_on_dash_and_gap_segment_y < in.dash_length);
263 
264 // Mask out any gaps in the border
265 border_color.a *= in.dash_length <= 0 || (is_horizontal_dash || is_vertical_dash);
266 
267 // Perform proper alpha blending on the two colors, avoiding a
268 // divide-by-zero if both colors are fully transparent.
269 //
270 // See formula for "over" compositing here: https://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending
271 float alpha = border_color.a + background_color.a * (1.0 - border_color.a);
272 color.rgb = (border_color.rgb * border_color.a + background_color.rgb * background_color.a * (1.0 - border_color.a)) / (alpha + EPSILON);
273 color.a = alpha;
274 }
275 
276 // If there's a corner radius we need to do some anti aliasing to smooth out the rounded corner effect.
277 if (outer_corner_radius > 0) {
278 color.a *= 1.0 - saturate(outer_distance + 0.5);
279 }
280 
281 return color;
282}
283 
284fragment float4 image_fragment_shader(
285 RectFragmentData in [[stage_in]],
286 texture2d<half> color_texture [[ texture(0) ]])
287{
288 constexpr sampler texture_sampler (mag_filter::linear,
289 min_filter::linear);
290 
291 // Sample the texture to obtain a color
292 const half4 color_sample = color_texture.sample(texture_sampler, in.texture_coordinate);
293 
294 float4 color;
295 // If the image is an icon, use the provided icon_color instead of sampling from texture
296 if (in.is_icon) {
297 vector_float4 in_color = in.icon_color;
298 in_color.a *= color_sample.r;
299 color = float4(in_color);
300 } else {
301 color = float4(color_sample);
302 color.a *= in.icon_color.a;
303 }
304 
305 float outer_corner_radius;
306 
307 if (in.position.y >= in.rect_center.y) {
308 // Bottom half
309 if (in.position.x >= in.rect_center.x) {
310 // Bottom right quadrant
311 outer_corner_radius = in.corner_radius_bottom_right;
312 } else {
313 // Bottom left quadrant
314 outer_corner_radius = in.corner_radius_bottom_left;
315 }
316 } else {
317 // Top half
318 if (in.position.x >= in.rect_center.x) {
319 // Top right quadrant
320 outer_corner_radius = in.corner_radius_top_right;
321 } else {
322 // Top left quadrant
323 outer_corner_radius = in.corner_radius_top_left;
324 }
325 }
326 
327 float outer_distance = distance_from_rect(in.position.xy, in.rect_center, in.rect_corner, outer_corner_radius);
328 
329 // If there's a corner radius we need to do some anti aliasing to smooth out the rounded corner effect.
330 if (outer_corner_radius > 0) {
331 color.a *= 1.0 - saturate(outer_distance + 0.5);
332 }
333 return color;
334}
335 
336vertex GlyphFragmentData
337glyph_vertex_shader(
338 uint vertex_id [[vertex_id]],
339 uint instance_id [[instance_id]],
340 constant vector_float2 *vertices [[buffer(0)]],
341 const device PerGlyphUniforms *glyph_uniforms [[buffer(1)]],
342 constant Uniforms *uniforms [[buffer(2)]])
343{
344 const device PerGlyphUniforms *glyph = &glyph_uniforms[instance_id];
345 
346 float2 pixel_pos = vertices[vertex_id] * glyph->size + glyph->origin;
347 // Use floor here to vertically align the glyph to the pixel grid.
348 // If it's not aligned to the grid, the fragment shader will do its
349 // own interpolation, which makes it so we don't use the anti-aliasing
350 // from core text, which is what we want. We don't force the glyph to a
351 // horizontal pixel position because we rasterize the glyph at multiple
352 // subpixel positions, and so the very slight linear interpolation here
353 // won't produce a fuzzy glyph, just a correctly-positioned one.
354 pixel_pos = float2(pixel_pos.x, floor(pixel_pos.y));
355 
356 // Evaluating the glyphs fade effect. Note that the fade may go in two different directions:
357 // - Right to left (default) - where the opaque side is on the right, and transparent on the left
358 // (in this case, the start_fade < end_fade; start is where the fade is transparent)
359 // - Left to right - where the opaque side is on the left, and it fades towards the right side.
360 // In this case, start_fade > end_fade, and the opaque side is on the left (end_fade).
361 // To clarify: fade_start is ALWAYS where the fade is transparent, and fade_end is ALWAYS where
362 // the opaque part is, this is reflected in how we compute width, dist, and alpha.
363 float fade_width = fabs(glyph->fade_end - glyph->fade_start);
364 float fade_dist = pixel_pos.x - fmin(glyph->fade_start, glyph->fade_end);
365 
366 float fade_alpha;
367 if (glyph->fade_end < glyph->fade_start) { // left-to-right case
368 fade_alpha = fade_dist / fade_width;
369 } else { // right-to-left case
370 fade_alpha = 1 - fade_dist / fade_width;
371 }
372 
373 vector_float2 device_pos = pixel_pos / uniforms->viewport_size * vector_float2(2.0, -2.0) + vector_float2(-1.0, 1.0);
374 
375 vector_float2 texture_coordinate = vector_float2(glyph->uv_left, glyph->uv_top) + vertices[vertex_id] * vector_float2(glyph->uv_width, glyph->uv_height);
376 
377 GlyphFragmentData out;
378 out.position = vector_float4(device_pos, 0.0, 1.0);
379 out.rect_corner = glyph->size / 2.0;
380 out.rect_center = glyph->origin + out.rect_corner;
381 out.texture_coordinate = texture_coordinate;
382 out.fade_alpha = fade_alpha;
383 out.color = glyph->color;
384 out.is_emoji = glyph->is_emoji;
385 return out;
386}
387 
388fragment float4 glyph_fragment_shader(
389 GlyphFragmentData in [[stage_in]],
390 texture2d<half> color_texture [[ texture(0) ]]
391) {
392 // Sample the texture to obtain a color.
393 constexpr sampler texture_sampler (mag_filter::linear, min_filter::linear);
394 const float4 color_sample = float4(color_texture.sample(texture_sampler, in.texture_coordinate));
395 // Use the input color for non-emoji, and the sampled color for emoji.
396 float4 color = mix(in.color, color_sample, float(in.is_emoji));
397 // Multiply alpha by the sampled color's red channel for non-emoji.
398 color.a *= max(color_sample.r, float(in.is_emoji));
399 // Apply the fade.
400 color.a *= saturate(in.fade_alpha);
401 return color;
402}
403