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    vector_float2 origin;
 38    vector_float2 size;
 39    vector_uchar4 background_color;
 40    float border_top;
 41    float border_right;
 42    float border_bottom;
 43    float border_left;
 44    vector_uchar4 border_color;
 45    float corner_radius;
 46};
 47
 48vertex QuadFragmentInput quad_vertex(
 49    uint unit_vertex_id [[vertex_id]],
 50    uint quad_id [[instance_id]],
 51    constant float2 *unit_vertices [[buffer(GPUIQuadInputIndexVertices)]],
 52    constant GPUIQuad *quads [[buffer(GPUIQuadInputIndexQuads)]],
 53    constant GPUIUniforms *uniforms [[buffer(GPUIQuadInputIndexUniforms)]]
 54) {
 55    float2 unit_vertex = unit_vertices[unit_vertex_id];
 56    GPUIQuad quad = quads[quad_id];
 57    float2 position = unit_vertex * quad.size + quad.origin;
 58    float4 device_position = to_device_position(position, uniforms->viewport_size);
 59
 60    return QuadFragmentInput {
 61        device_position,
 62        quad.origin,
 63        quad.size,
 64        quad.background_color,
 65        quad.border_top,
 66        quad.border_right,
 67        quad.border_bottom,
 68        quad.border_left,
 69        quad.border_color,
 70        quad.corner_radius,
 71    };
 72}
 73
 74fragment float4 quad_fragment(
 75    QuadFragmentInput input [[stage_in]]
 76) {
 77    float2 half_size = input.size / 2.;
 78    float2 center = input.origin + half_size;
 79    float2 center_to_point = input.position.xy - center;
 80    float2 edge_to_point = abs(center_to_point) - half_size;
 81    float2 rounded_edge_to_point = abs(center_to_point) - half_size + input.corner_radius;
 82    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;
 83
 84    float vertical_border = center_to_point.x <= 0. ? input.border_left : input.border_right;
 85    float horizontal_border = center_to_point.y <= 0. ? input.border_top : input.border_bottom;
 86    float2 inset_size = half_size - input.corner_radius - float2(vertical_border, horizontal_border);
 87    float2 point_to_inset_corner = abs(center_to_point) - inset_size;
 88    float border_width;
 89    if (point_to_inset_corner.x < 0. && point_to_inset_corner.y < 0.) {
 90        border_width = 0.;
 91    } else if (point_to_inset_corner.y > point_to_inset_corner.x) {
 92        border_width = horizontal_border;
 93    } else {
 94        border_width = vertical_border;
 95    }
 96
 97    float4 color;
 98    if (border_width == 0.) {
 99        color = coloru_to_colorf(input.background_color);
100    } else {
101        float inset_distance = distance + border_width;
102        color = mix(
103            coloru_to_colorf(input.border_color),
104            coloru_to_colorf(input.background_color),
105            saturate(0.5 - inset_distance)
106        );
107    }
108
109    float4 coverage = float4(1., 1., 1., saturate(0.5 - distance));
110    return coverage * color;
111}
112
113struct ShadowFragmentInput {
114    float4 position [[position]];
115    vector_float2 origin;
116    vector_float2 size;
117    float corner_radius;
118    float sigma;
119    vector_uchar4 color;
120};
121
122vertex ShadowFragmentInput shadow_vertex(
123    uint unit_vertex_id [[vertex_id]],
124    uint shadow_id [[instance_id]],
125    constant float2 *unit_vertices [[buffer(GPUIShadowInputIndexVertices)]],
126    constant GPUIShadow *shadows [[buffer(GPUIShadowInputIndexShadows)]],
127    constant GPUIUniforms *uniforms [[buffer(GPUIShadowInputIndexUniforms)]]
128) {
129    float2 unit_vertex = unit_vertices[unit_vertex_id];
130    GPUIShadow shadow = shadows[shadow_id];
131
132    float margin = 3. * shadow.sigma;
133    float2 position = unit_vertex * (shadow.size + 2. * margin) + shadow.origin - margin;
134    float4 device_position = to_device_position(position, uniforms->viewport_size);
135
136    return ShadowFragmentInput {
137        device_position,
138        shadow.origin,
139        shadow.size,
140        shadow.corner_radius,
141        shadow.sigma,
142        shadow.color,
143    };
144}
145
146fragment float4 shadow_fragment(
147    ShadowFragmentInput input [[stage_in]]
148) {
149    float sigma = input.sigma;
150    float corner_radius = input.corner_radius;
151    float2 half_size = input.size / 2.;
152    float2 center = input.origin + half_size;
153    float2 point = input.position.xy - center;
154
155    // The signal is only non-zero in a limited range, so don't waste samples
156    float low = point.y - half_size.y;
157    float high = point.y + half_size.y;
158    float start = clamp(-3. * sigma, low, high);
159    float end = clamp(3. * sigma, low, high);
160
161    // Accumulate samples (we can get away with surprisingly few samples)
162    float step = (end - start) / 4.;
163    float y = start + step * 0.5;
164    float alpha = 0.;
165    for (int i = 0; i < 4; i++) {
166        alpha += blur_along_x(point.x, point.y - y, sigma, corner_radius, half_size) * gaussian(y, sigma) * step;
167        y += step;
168    }
169
170    return float4(1., 1., 1., alpha) * coloru_to_colorf(input.color);
171}
172
173struct SpriteFragmentInput {
174    float4 position [[position]];
175    float2 atlas_position;
176    float4 color [[flat]];
177    uchar compute_winding [[flat]];
178};
179
180vertex SpriteFragmentInput sprite_vertex(
181    uint unit_vertex_id [[vertex_id]],
182    uint sprite_id [[instance_id]],
183    constant float2 *unit_vertices [[buffer(GPUISpriteVertexInputIndexVertices)]],
184    constant GPUISprite *sprites [[buffer(GPUISpriteVertexInputIndexSprites)]],
185    constant float2 *viewport_size [[buffer(GPUISpriteVertexInputIndexViewportSize)]],
186    constant float2 *atlas_size [[buffer(GPUISpriteVertexInputIndexAtlasSize)]]
187) {
188    float2 unit_vertex = unit_vertices[unit_vertex_id];
189    GPUISprite sprite = sprites[sprite_id];
190    float2 position = unit_vertex * sprite.size + sprite.origin;
191    float4 device_position = to_device_position(position, *viewport_size);
192    float2 atlas_position = (unit_vertex * sprite.size + sprite.atlas_origin) / *atlas_size;
193
194    return SpriteFragmentInput {
195        device_position,
196        atlas_position,
197        coloru_to_colorf(sprite.color),
198        sprite.compute_winding
199    };
200}
201
202#define MAX_WINDINGS 32.
203
204fragment float4 sprite_fragment(
205    SpriteFragmentInput input [[stage_in]],
206    texture2d<float> atlas [[ texture(GPUISpriteFragmentInputIndexAtlas) ]]
207) {
208    constexpr sampler atlas_sampler(mag_filter::linear, min_filter::linear);
209    float4 color = input.color;
210    float4 sample = atlas.sample(atlas_sampler, input.atlas_position);
211    float mask;
212    if (input.compute_winding) {
213        mask = 1. - abs(1. - fmod(sample.r * MAX_WINDINGS, 2.));
214    } else {
215        mask = sample.a;
216    }
217    color.a *= mask;
218    return color;
219}
220
221struct PathWindingFragmentInput {
222    float4 position [[position]];
223    float2 st_position;
224};
225
226vertex PathWindingFragmentInput path_winding_vertex(
227    uint vertex_id [[vertex_id]],
228    constant GPUIPathVertex *vertices [[buffer(GPUIPathWindingVertexInputIndexVertices)]],
229    constant float2 *atlas_size [[buffer(GPUIPathWindingVertexInputIndexAtlasSize)]]
230) {
231    GPUIPathVertex v = vertices[vertex_id];
232    float4 device_position = to_device_position(v.xy_position, *atlas_size);
233    return PathWindingFragmentInput {
234        device_position,
235        v.st_position,
236    };
237}
238
239fragment float4 path_winding_fragment(
240    PathWindingFragmentInput input [[stage_in]]
241) {
242    float2 dx = dfdx(input.st_position);
243    float2 dy = dfdy(input.st_position);
244    float2 gradient = float2(
245        (2. * input.st_position.x) * dx.x - dx.y,
246        (2. * input.st_position.x) * dy.x - dy.y
247    );
248    float f = (input.st_position.x * input.st_position.x) - input.st_position.y;
249    float distance = f / length(gradient);
250    float alpha = saturate(0.5 - distance) / MAX_WINDINGS;
251    return float4(alpha, 0., 0., 1.);
252}