shaders.metal

  1#include <metal_stdlib>
  2#include "shaders.h"
  3
  4using namespace metal;
  5
  6float4 coloru_to_colorf(uchar4 coloru) {
  7    return float4(coloru) / float4(0xff, 0xff, 0xff, 0xff);
  8}
  9
 10float4 to_device_position(float2 pixel_position, float2 viewport_size) {
 11    return float4(pixel_position / viewport_size * float2(2., -2.) + float2(-1., 1.), 0., 1.);
 12}
 13
 14// A standard gaussian function, used for weighting samples
 15float gaussian(float x, float sigma) {
 16    return exp(-(x * x) / (2. * sigma * sigma)) / (sqrt(2. * M_PI_F) * sigma);
 17}
 18
 19// This approximates the error function, needed for the gaussian integral
 20float2 erf(float2 x) {
 21    float2 s = sign(x);
 22    float2 a = abs(x);
 23    x = 1. + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a;
 24    x *= x;
 25    return s - s / (x * x);
 26}
 27
 28float blur_along_x(float x, float y, float sigma, float corner, float2 halfSize) {
 29    float delta = min(halfSize.y - corner - abs(y), 0.);
 30    float curved = halfSize.x - corner + sqrt(max(0., corner * corner - delta * delta));
 31    float2 integral = 0.5 + 0.5 * erf((x + float2(-curved, curved)) * (sqrt(0.5) / sigma));
 32    return integral.y - integral.x;
 33}
 34
 35struct QuadFragmentInput {
 36    float4 position [[position]];
 37    float2 atlas_position; // only used in the image shader
 38    float2 origin;
 39    float2 size;
 40    float4 background_color;
 41    float border_top;
 42    float border_right;
 43    float border_bottom;
 44    float border_left;
 45    float4 border_color;
 46    float corner_radius;
 47    uchar grayscale; // only used in image shader
 48};
 49
 50float4 quad_sdf(QuadFragmentInput input) {
 51    float2 half_size = input.size / 2.;
 52    float2 center = input.origin + half_size;
 53    float2 center_to_point = input.position.xy - center;
 54    float2 rounded_edge_to_point = abs(center_to_point) - half_size + input.corner_radius;
 55    float distance = length(max(0., rounded_edge_to_point)) + min(0., max(rounded_edge_to_point.x, rounded_edge_to_point.y)) - input.corner_radius;
 56
 57    float vertical_border = center_to_point.x <= 0. ? input.border_left : input.border_right;
 58    float horizontal_border = center_to_point.y <= 0. ? input.border_top : input.border_bottom;
 59    float2 inset_size = half_size - input.corner_radius - float2(vertical_border, horizontal_border);
 60    float2 point_to_inset_corner = abs(center_to_point) - inset_size;
 61    float border_width;
 62    if (point_to_inset_corner.x < 0. && point_to_inset_corner.y < 0.) {
 63        border_width = 0.;
 64    } else if (point_to_inset_corner.y > point_to_inset_corner.x) {
 65        border_width = horizontal_border;
 66    } else {
 67        border_width = vertical_border;
 68    }
 69
 70    float4 color;
 71    if (border_width == 0.) {
 72        color = input.background_color;
 73    } else {
 74        float inset_distance = distance + border_width;
 75
 76        // Decrease border's opacity as we move inside the background.
 77        input.border_color.a *= 1. - saturate(0.5 - inset_distance);
 78
 79        // Alpha-blend the border and the background.
 80        float output_alpha = input.border_color.a + input.background_color.a * (1. - input.border_color.a);
 81        float3 premultiplied_border_rgb = input.border_color.rgb * input.border_color.a;
 82        float3 premultiplied_background_rgb = input.background_color.rgb * input.background_color.a;
 83        float3 premultiplied_output_rgb = premultiplied_border_rgb + premultiplied_background_rgb * (1. - input.border_color.a);
 84        color = float4(premultiplied_output_rgb / output_alpha, output_alpha);
 85    }
 86
 87    return color * float4(1., 1., 1., saturate(0.5 - distance));
 88}
 89
 90vertex QuadFragmentInput quad_vertex(
 91    uint unit_vertex_id [[vertex_id]],
 92    uint quad_id [[instance_id]],
 93    constant float2 *unit_vertices [[buffer(GPUIQuadInputIndexVertices)]],
 94    constant GPUIQuad *quads [[buffer(GPUIQuadInputIndexQuads)]],
 95    constant GPUIUniforms *uniforms [[buffer(GPUIQuadInputIndexUniforms)]]
 96) {
 97    float2 unit_vertex = unit_vertices[unit_vertex_id];
 98    GPUIQuad quad = quads[quad_id];
 99    float2 position = unit_vertex * quad.size + quad.origin;
100    float4 device_position = to_device_position(position, uniforms->viewport_size);
101
102    return QuadFragmentInput {
103        device_position,
104        float2(0., 0.),
105        quad.origin,
106        quad.size,
107        coloru_to_colorf(quad.background_color),
108        quad.border_top,
109        quad.border_right,
110        quad.border_bottom,
111        quad.border_left,
112        coloru_to_colorf(quad.border_color),
113        quad.corner_radius,
114        0,
115    };
116}
117
118fragment float4 quad_fragment(
119    QuadFragmentInput input [[stage_in]]
120) {
121    return quad_sdf(input);
122}
123
124struct ShadowFragmentInput {
125    float4 position [[position]];
126    vector_float2 origin;
127    vector_float2 size;
128    float corner_radius;
129    float sigma;
130    vector_uchar4 color;
131};
132
133vertex ShadowFragmentInput shadow_vertex(
134    uint unit_vertex_id [[vertex_id]],
135    uint shadow_id [[instance_id]],
136    constant float2 *unit_vertices [[buffer(GPUIShadowInputIndexVertices)]],
137    constant GPUIShadow *shadows [[buffer(GPUIShadowInputIndexShadows)]],
138    constant GPUIUniforms *uniforms [[buffer(GPUIShadowInputIndexUniforms)]]
139) {
140    float2 unit_vertex = unit_vertices[unit_vertex_id];
141    GPUIShadow shadow = shadows[shadow_id];
142
143    float margin = 3. * shadow.sigma;
144    float2 position = unit_vertex * (shadow.size + 2. * margin) + shadow.origin - margin;
145    float4 device_position = to_device_position(position, uniforms->viewport_size);
146
147    return ShadowFragmentInput {
148        device_position,
149        shadow.origin,
150        shadow.size,
151        shadow.corner_radius,
152        shadow.sigma,
153        shadow.color,
154    };
155}
156
157fragment float4 shadow_fragment(
158    ShadowFragmentInput input [[stage_in]]
159) {
160    float sigma = input.sigma;
161    float corner_radius = input.corner_radius;
162    float2 half_size = input.size / 2.;
163    float2 center = input.origin + half_size;
164    float2 point = input.position.xy - center;
165
166    // The signal is only non-zero in a limited range, so don't waste samples
167    float low = point.y - half_size.y;
168    float high = point.y + half_size.y;
169    float start = clamp(-3. * sigma, low, high);
170    float end = clamp(3. * sigma, low, high);
171
172    // Accumulate samples (we can get away with surprisingly few samples)
173    float step = (end - start) / 4.;
174    float y = start + step * 0.5;
175    float alpha = 0.;
176    for (int i = 0; i < 4; i++) {
177        alpha += blur_along_x(point.x, point.y - y, sigma, corner_radius, half_size) * gaussian(y, sigma) * step;
178        y += step;
179    }
180
181    return float4(1., 1., 1., alpha) * coloru_to_colorf(input.color);
182}
183
184struct SpriteFragmentInput {
185    float4 position [[position]];
186    float2 atlas_position;
187    float4 color [[flat]];
188    uchar compute_winding [[flat]];
189};
190
191vertex SpriteFragmentInput sprite_vertex(
192    uint unit_vertex_id [[vertex_id]],
193    uint sprite_id [[instance_id]],
194    constant float2 *unit_vertices [[buffer(GPUISpriteVertexInputIndexVertices)]],
195    constant GPUISprite *sprites [[buffer(GPUISpriteVertexInputIndexSprites)]],
196    constant float2 *viewport_size [[buffer(GPUISpriteVertexInputIndexViewportSize)]],
197    constant float2 *atlas_size [[buffer(GPUISpriteVertexInputIndexAtlasSize)]]
198) {
199    float2 unit_vertex = unit_vertices[unit_vertex_id];
200    GPUISprite sprite = sprites[sprite_id];
201    float2 position = unit_vertex * sprite.target_size + sprite.origin;
202    float4 device_position = to_device_position(position, *viewport_size);
203    float2 atlas_position = (unit_vertex * sprite.source_size + sprite.atlas_origin) / *atlas_size;
204
205    return SpriteFragmentInput {
206        device_position,
207        atlas_position,
208        coloru_to_colorf(sprite.color),
209        sprite.compute_winding
210    };
211}
212
213fragment float4 sprite_fragment(
214    SpriteFragmentInput input [[stage_in]],
215    texture2d<float> atlas [[ texture(GPUISpriteFragmentInputIndexAtlas) ]]
216) {
217    constexpr sampler atlas_sampler(mag_filter::linear, min_filter::linear);
218    float4 color = input.color;
219    float4 sample = atlas.sample(atlas_sampler, input.atlas_position);
220    float mask;
221    if (input.compute_winding) {
222        mask = 1. - abs(1. - fmod(sample.r, 2.));
223    } else {
224        mask = sample.a;
225    }
226    color.a *= mask;
227    return color;
228}
229
230vertex QuadFragmentInput image_vertex(
231    uint unit_vertex_id [[vertex_id]],
232    uint image_id [[instance_id]],
233    constant float2 *unit_vertices [[buffer(GPUIImageVertexInputIndexVertices)]],
234    constant GPUIImage *images [[buffer(GPUIImageVertexInputIndexImages)]],
235    constant float2 *viewport_size [[buffer(GPUIImageVertexInputIndexViewportSize)]],
236    constant float2 *atlas_size [[buffer(GPUIImageVertexInputIndexAtlasSize)]]
237) {
238    float2 unit_vertex = unit_vertices[unit_vertex_id];
239    GPUIImage image = images[image_id];
240    float2 position = unit_vertex * image.target_size + image.origin;
241    float4 device_position = to_device_position(position, *viewport_size);
242    float2 atlas_position = (unit_vertex * image.source_size + image.atlas_origin) / *atlas_size;
243
244    return QuadFragmentInput {
245        device_position,
246        atlas_position,
247        image.origin,
248        image.target_size,
249        float4(0.),
250        image.border_top,
251        image.border_right,
252        image.border_bottom,
253        image.border_left,
254        coloru_to_colorf(image.border_color),
255        image.corner_radius,
256        image.grayscale,
257    };
258}
259
260fragment float4 image_fragment(
261    QuadFragmentInput input [[stage_in]],
262    texture2d<float> atlas [[ texture(GPUIImageFragmentInputIndexAtlas) ]]
263) {
264    constexpr sampler atlas_sampler(mag_filter::linear, min_filter::linear);
265    input.background_color = atlas.sample(atlas_sampler, input.atlas_position);
266    if (input.grayscale) {
267        float grayscale =
268            0.2126 * input.background_color.r +
269            0.7152 * input.background_color.g + 
270            0.0722 * input.background_color.b;
271        input.background_color = float4(grayscale, grayscale, grayscale, input.background_color.a);
272    }
273    return quad_sdf(input);
274}
275
276vertex QuadFragmentInput surface_vertex(
277    uint unit_vertex_id [[vertex_id]],
278    uint image_id [[instance_id]],
279    constant float2 *unit_vertices [[buffer(GPUISurfaceVertexInputIndexVertices)]],
280    constant GPUISurface *images [[buffer(GPUISurfaceVertexInputIndexSurfaces)]],
281    constant float2 *viewport_size [[buffer(GPUISurfaceVertexInputIndexViewportSize)]],
282    constant float2 *atlas_size [[buffer(GPUISurfaceVertexInputIndexAtlasSize)]]
283) {
284    float2 unit_vertex = unit_vertices[unit_vertex_id];
285    GPUISurface image = images[image_id];
286    float2 position = unit_vertex * image.target_size + image.origin;
287    float4 device_position = to_device_position(position, *viewport_size);
288    float2 atlas_position = (unit_vertex * image.source_size) / *atlas_size;
289
290    return QuadFragmentInput {
291        device_position,
292        atlas_position,
293        image.origin,
294        image.target_size,
295        float4(0.),
296        0.,
297        0.,
298        0.,
299        0.,
300        float4(0.),
301        0.,
302        0,
303    };
304}
305
306fragment float4 surface_fragment(
307    QuadFragmentInput input [[stage_in]],
308    texture2d<float> y_atlas [[ texture(GPUISurfaceFragmentInputIndexYAtlas) ]],
309    texture2d<float> cb_cr_atlas [[ texture(GPUISurfaceFragmentInputIndexCbCrAtlas) ]]
310) {
311    constexpr sampler atlas_sampler(mag_filter::linear, min_filter::linear);
312    const float4x4 ycbcrToRGBTransform = float4x4(
313        float4(+1.0000f, +1.0000f, +1.0000f, +0.0000f),
314        float4(+0.0000f, -0.3441f, +1.7720f, +0.0000f),
315        float4(+1.4020f, -0.7141f, +0.0000f, +0.0000f),
316        float4(-0.7010f, +0.5291f, -0.8860f, +1.0000f)
317    );
318    float4 ycbcr = float4(y_atlas.sample(atlas_sampler, input.atlas_position).r,
319                          cb_cr_atlas.sample(atlas_sampler, input.atlas_position).rg, 1.0);
320
321    input.background_color = ycbcrToRGBTransform * ycbcr;
322    return quad_sdf(input);
323}
324
325struct PathAtlasVertexOutput {
326    float4 position [[position]];
327    float2 st_position;
328    float clip_rect_distance [[clip_distance]] [4];
329};
330
331struct PathAtlasFragmentInput {
332    float4 position [[position]];
333    float2 st_position;
334};
335
336vertex PathAtlasVertexOutput path_atlas_vertex(
337    uint vertex_id [[vertex_id]],
338    constant GPUIPathVertex *vertices [[buffer(GPUIPathAtlasVertexInputIndexVertices)]],
339    constant float2 *atlas_size [[buffer(GPUIPathAtlasVertexInputIndexAtlasSize)]]
340) {
341    GPUIPathVertex v = vertices[vertex_id];
342    float4 device_position = to_device_position(v.xy_position, *atlas_size);
343    return PathAtlasVertexOutput {
344        device_position,
345        v.st_position,
346        {
347            v.xy_position.x - v.clip_rect_origin.x,
348            v.clip_rect_origin.x + v.clip_rect_size.x - v.xy_position.x,
349            v.xy_position.y - v.clip_rect_origin.y,
350            v.clip_rect_origin.y + v.clip_rect_size.y - v.xy_position.y
351        }
352    };
353}
354
355fragment float4 path_atlas_fragment(
356    PathAtlasFragmentInput input [[stage_in]]
357) {
358    float2 dx = dfdx(input.st_position);
359    float2 dy = dfdy(input.st_position);
360    float2 gradient = float2(
361        (2. * input.st_position.x) * dx.x - dx.y,
362        (2. * input.st_position.x) * dy.x - dy.y
363    );
364    float f = (input.st_position.x * input.st_position.x) - input.st_position.y;
365    float distance = f / length(gradient);
366    float alpha = saturate(0.5 - distance);
367    return float4(alpha, 0., 0., 1.);
368}
369
370struct UnderlineFragmentInput {
371    float4 position [[position]];
372    float2 origin;
373    float2 size;
374    float thickness;
375    float4 color;
376    bool squiggly;
377};
378
379vertex UnderlineFragmentInput underline_vertex(
380    uint unit_vertex_id [[vertex_id]],
381    uint underline_id [[instance_id]],
382    constant float2 *unit_vertices [[buffer(GPUIUnderlineInputIndexVertices)]],
383    constant GPUIUnderline *underlines [[buffer(GPUIUnderlineInputIndexUnderlines)]],
384    constant GPUIUniforms *uniforms [[buffer(GPUIUnderlineInputIndexUniforms)]]
385) {
386    float2 unit_vertex = unit_vertices[unit_vertex_id];
387    GPUIUnderline underline = underlines[underline_id];
388    float2 position = unit_vertex * underline.size + underline.origin;
389    float4 device_position = to_device_position(position, uniforms->viewport_size);
390
391    return UnderlineFragmentInput {
392        device_position,
393        underline.origin,
394        underline.size,
395        underline.thickness,
396        coloru_to_colorf(underline.color),
397        underline.squiggly != 0,
398    };
399}
400
401fragment float4 underline_fragment(
402    UnderlineFragmentInput input [[stage_in]]
403) {
404    if (input.squiggly) {
405        float half_thickness = input.thickness * 0.5;
406        float2 st = ((input.position.xy - input.origin) / input.size.y) - float2(0., 0.5);
407        float frequency = (M_PI_F * (3. * input.thickness)) / 8.;
408        float amplitude = 1. / (2. * input.thickness);
409        float sine = sin(st.x * frequency) * amplitude;
410        float dSine = cos(st.x * frequency) * amplitude * frequency;
411        float distance = (st.y - sine) / sqrt(1. + dSine * dSine);
412        float distance_in_pixels = distance * input.size.y;
413        float distance_from_top_border = distance_in_pixels - half_thickness;
414        float distance_from_bottom_border = distance_in_pixels + half_thickness;
415        float alpha = saturate(0.5 - max(-distance_from_bottom_border, distance_from_top_border));
416        return input.color * float4(1., 1., 1., alpha);
417    } else {
418        return input.color;
419    }
420}