QmlMaterial 0.1.0
Loading...
Searching...
No Matches
skia_shadow.h
1
2#include "qml_material/scenegraph/geometry.h"
3
4// https://github.com/google/skia/blob/canvaskit/0.38.2/src/gpu/ganesh/ops/ShadowRRectOp.cpp
5
6namespace qml_material::sk
7{
8
9using scalar = float;
10
11template<typename T>
12static constexpr const T& SkTPin(const T& x, const T& lo, const T& hi) {
13 return std::max(lo, std::min(x, hi));
14}
15
16constexpr auto sk_double_round(double x) { return std::floor((x) + 0.5); }
17constexpr auto sk_float_round(double x) { return (float)std::floor((x) + 0.5); }
18
19template<typename T, std::enable_if_t<std::is_floating_point_v<T>, bool> = true>
20static inline constexpr bool SkIsNaN(T x) {
21 return x != x;
22}
23
24#if defined(_MSC_VER) && ! defined(__clang__)
25# define SK_CHECK_NAN(resultVal) \
26 if (SkIsNaN(x)) { \
27 return resultVal; \
28 }
29#else
30# define SK_CHECK_NAN(resultVal)
31#endif
32
33inline constexpr int SK_MaxS32FitsInFloat = 2147483520;
34inline constexpr int SK_MinS32FitsInFloat = -SK_MaxS32FitsInFloat;
35static constexpr int sk_float_saturate2int(float x) {
36 SK_CHECK_NAN(SK_MaxS32FitsInFloat)
37 x = x < SK_MaxS32FitsInFloat ? x : SK_MaxS32FitsInFloat;
38 x = x > SK_MinS32FitsInFloat ? x : SK_MinS32FitsInFloat;
39 return (int)x;
40}
41constexpr auto SkScalarRoundToInt(double x) { return sk_float_saturate2int(sk_float_round(x)); }
42
43static constexpr auto kAmbientHeightFactor = 1.0f / 128.0f;
44static constexpr auto kAmbientGeomFactor = 64.0f;
45// Assuming that we have a light height of 600 for the spot shadow,
46// the spot values will reach their maximum at a height of approximately 292.3077.
47// We'll round up to 300 to keep it simple.
48static constexpr auto kMaxAmbientRadius = 300 * kAmbientHeightFactor * kAmbientGeomFactor;
49
50static inline float divide_and_pin(float numer, float denom, float min, float max) {
51 float result = SkTPin(numer / denom, min, max);
52 // ensure that SkTPin handled non-finites correctly
53 Q_ASSERT(result >= min && result <= max);
54 return result;
55}
56constexpr auto SK_Scalar1 { 1.0f };
57constexpr auto SK_ScalarNearlyZero { (SK_Scalar1 / (1 << 12)) };
58
59inline scalar AmbientBlurRadius(scalar height) {
60 return std::min(height * kAmbientHeightFactor * kAmbientGeomFactor, kMaxAmbientRadius);
61}
62inline scalar AmbientRecipAlpha(scalar height) {
63 return 1.0f + std::max(height * kAmbientHeightFactor, 0.0f);
64}
65
66inline scalar SpotBlurRadius(scalar occluderZ, scalar lightZ, scalar lightRadius) {
67 return lightRadius * divide_and_pin(occluderZ, lightZ - occluderZ, 0.0f, 0.95f);
68}
69
70inline void GetSpotParams(scalar occluderZ, scalar lightX, scalar lightY, scalar lightZ,
71 scalar lightRadius, scalar* blurRadius, scalar* scale,
72 QVector2D* translate) {
73 scalar zRatio = divide_and_pin(occluderZ, lightZ - occluderZ, 0.0f, 0.95f);
74 *blurRadius = lightRadius * zRatio;
75 *scale = divide_and_pin(lightZ, lightZ - occluderZ, 1.0f, 1.95f);
76 *translate = QVector2D(-zRatio * lightX, -zRatio * lightY);
77}
78
79inline void GetDirectionalParams(scalar occluderZ, scalar lightX, scalar lightY, scalar lightZ,
80 scalar lightRadius, scalar* blurRadius, scalar* scale,
81 QVector2D* translate) {
82 *blurRadius = lightRadius * occluderZ;
83 *scale = 1;
84 // Max z-ratio is "max expected elevation"/"min allowable z"
85 constexpr scalar kMaxZRatio = 64 / SK_ScalarNearlyZero;
86 scalar zRatio = divide_and_pin(occluderZ, lightZ, 0.0f, kMaxZRatio);
87 *translate = QVector2D(-zRatio * lightX, -zRatio * lightY);
88}
89
90class ShadowCircularRRectOp {
91public:
92 using scalar = float;
93 enum RRectType
94 {
95 kFill_RRectType,
96 kStroke_RRectType,
97 kOverstroke_RRectType,
98 };
99 // An insetWidth > 1/2 rect width or height indicates a simple fill.
100 ShadowCircularRRectOp(QRgb color, const QRectF& devRect, float devRadius, bool isCircle,
101 float blurRadius, float insetWidth)
102 : fIndexPtr(nullptr) {
103 QRectF bounds = devRect;
104 Q_ASSERT(insetWidth > 0);
105 scalar innerRadius = 0.0f;
106 scalar outerRadius = devRadius;
107 scalar umbraInset;
108
109 RRectType type = kFill_RRectType;
110 if (isCircle) {
111 umbraInset = 0;
112 } else {
113 umbraInset = std::max(outerRadius, blurRadius);
114 }
115
116 // If stroke is greater than width or height, this is still a fill,
117 // otherwise we compute stroke params.
118 if (isCircle) {
119 innerRadius = devRadius - insetWidth;
120 type = innerRadius > 0 ? kStroke_RRectType : kFill_RRectType;
121 } else {
122 if (insetWidth <= 0.5f * std::min(devRect.width(), devRect.height())) {
123 // We don't worry about a real inner radius, we just need to know if we
124 // need to create overstroke vertices.
125 innerRadius = std::max(insetWidth - umbraInset, 0.0f);
126 type = innerRadius > 0 ? kOverstroke_RRectType : kStroke_RRectType;
127 }
128 }
129
130 // this->setBounds(bounds, HasAABloat::kNo, IsHairline::kNo);
131
132 fGeoData = (Geometry {
133 color, outerRadius, umbraInset, innerRadius, blurRadius, bounds, type, isCircle });
134 if (isCircle) {
135 fVertCount = circle_type_to_vert_count(kStroke_RRectType == type);
136 fIndexCount = circle_type_to_index_count(kStroke_RRectType == type);
137 fIndexPtr = circle_type_to_indices(kStroke_RRectType == type);
138 } else {
139 fVertCount = rrect_type_to_vert_count(type);
140 fIndexCount = rrect_type_to_index_count(type);
141 fIndexPtr = rrect_type_to_indices(type);
142 }
143 }
144 struct Geometry {
145 QRgb color;
146 scalar outer_radius;
147 scalar umbra_inset;
148 scalar inner_radius;
149 scalar blur_radius;
150 QRectF dev_bounds;
151 RRectType type;
152 bool is_circle;
153 };
154
155 // In the case of a normal fill, we draw geometry for the circle as an octagon.
156 static constexpr uint16_t gFillCircleIndices[] = {
157 // enter the octagon
158 // clang-format off
159 0, 1, 8, 1, 2, 8,
160 2, 3, 8, 3, 4, 8,
161 4, 5, 8, 5, 6, 8,
162 6, 7, 8, 7, 0, 8,
163 // clang-format on
164 };
165
166 // For stroked circles, we use two nested octagons.
167 static constexpr uint16_t gStrokeCircleIndices[] = {
168 // enter the octagon
169 // clang-format off
170 0, 1, 9, 0, 9, 8,
171 1, 2, 10, 1, 10, 9,
172 2, 3, 11, 2, 11, 10,
173 3, 4, 12, 3, 12, 11,
174 4, 5, 13, 4, 13, 12,
175 5, 6, 14, 5, 14, 13,
176 6, 7, 15, 6, 15, 14,
177 7, 0, 8, 7, 8, 15,
178 // clang-format on
179 };
180
181 static constexpr uint16_t gRRectIndices[] = {
182 // clang-format off
183 // overstroke quads
184 // we place this at the beginning so that we can skip these indices when rendering as filled
185 0, 6, 25, 0, 25, 24,
186 6, 18, 27, 6, 27, 25,
187 18, 12, 26, 18, 26, 27,
188 12, 0, 24, 12, 24, 26,
189
190 // corners
191 0, 1, 2, 0, 2, 3, 0, 3, 4, 0, 4, 5,
192 6, 11, 10, 6, 10, 9, 6, 9, 8, 6, 8, 7,
193 12, 17, 16, 12, 16, 15, 12, 15, 14, 12, 14, 13,
194 18, 19, 20, 18, 20, 21, 18, 21, 22, 18, 22, 23,
195
196 // edges
197 0, 5, 11, 0, 11, 6,
198 6, 7, 19, 6, 19, 18,
199 18, 23, 17, 18, 17, 12,
200 12, 13, 1, 12, 1, 0,
201
202 // fill quad
203 // we place this at the end so that we can skip these indices when rendering as stroked
204 0, 6, 18, 0, 18, 12,
205 // clang-format on
206 };
207
208 static constexpr float SK_FloatSqrt2 = 1.41421356f;
209
210 // overstroke count
211 static constexpr int kIndicesPerOverstrokeRRect = std::size(gRRectIndices) - 6;
212 // simple stroke count skips overstroke indices
213 static constexpr int kIndicesPerStrokeRRect = kIndicesPerOverstrokeRRect - 6 * 4;
214 // fill count adds final quad to stroke count
215 static constexpr int kIndicesPerFillRRect = kIndicesPerStrokeRRect + 6;
216 static constexpr int kVertsPerStrokeRRect = 24;
217 static constexpr int kVertsPerOverstrokeRRect = 28;
218 static constexpr int kVertsPerFillRRect = 24;
219
220 static constexpr int kIndicesPerFillCircle = std::size(gFillCircleIndices);
221 static constexpr int kIndicesPerStrokeCircle = std::size(gStrokeCircleIndices);
222 static constexpr int kVertsPerStrokeCircle = 16;
223 static constexpr int kVertsPerFillCircle = 9;
224
225 static int circle_type_to_vert_count(bool stroked) {
226 return stroked ? kVertsPerStrokeCircle : kVertsPerFillCircle;
227 }
228
229 static int circle_type_to_index_count(bool stroked) {
230 return stroked ? kIndicesPerStrokeCircle : kIndicesPerFillCircle;
231 }
232
233 static const uint16_t* circle_type_to_indices(bool stroked) {
234 return stroked ? gStrokeCircleIndices : gFillCircleIndices;
235 }
236
237 static int rrect_type_to_vert_count(RRectType type) {
238 switch (type) {
239 case kFill_RRectType: return kVertsPerFillRRect;
240 case kStroke_RRectType: return kVertsPerStrokeRRect;
241 case kOverstroke_RRectType: return kVertsPerOverstrokeRRect;
242 }
243 Q_UNREACHABLE();
244 }
245
246 static int rrect_type_to_index_count(RRectType type) {
247 switch (type) {
248 case kFill_RRectType: return kIndicesPerFillRRect;
249 case kStroke_RRectType: return kIndicesPerStrokeRRect;
250 case kOverstroke_RRectType: return kIndicesPerOverstrokeRRect;
251 }
252 Q_UNREACHABLE();
253 }
254
255 static const uint16_t* rrect_type_to_indices(RRectType type) {
256 switch (type) {
257 case kFill_RRectType:
258 case kStroke_RRectType: return gRRectIndices + 6 * 4;
259 case kOverstroke_RRectType: return gRRectIndices;
260 }
261 Q_UNREACHABLE();
262 }
263
264public:
265 void fillInCircleVerts(bool isStroked, sg::ShadowVertex** vp) const {
266 const auto& args = this->fGeoData;
267 QRgb color = args.color;
268 scalar outerRadius = args.outer_radius;
269 scalar innerRadius = args.inner_radius;
270 scalar blurRadius = args.blur_radius;
271 scalar distanceCorrection = outerRadius / blurRadius;
272
273 const QRectF& bounds = args.dev_bounds;
274
275 // The inner radius in the vertex data must be specified in normalized space.
276 innerRadius = innerRadius / outerRadius;
277
278 auto center = QVector2D(bounds.center().x(), bounds.center().y());
279 scalar halfWidth = 0.5f * bounds.width();
280 scalar octOffset = 0.41421356237f; // sqrt(2) - 1
281
282 auto v = vp[0];
283
284 v->set_point(center + QVector2D(-octOffset * halfWidth, -halfWidth));
285 v->set_color(color);
286 v->set_offset({ -octOffset, -1 });
287 v->distance_correction = distanceCorrection;
288 v++;
289
290 // Second vertex
291 v->set_point(center + QVector2D(octOffset * halfWidth, -halfWidth));
292 v->set_color(color);
293 v->set_offset({ octOffset, -1 });
294 v->distance_correction = distanceCorrection;
295 v++;
296
297 // Third vertex
298 v->set_point(center + QVector2D(halfWidth, -octOffset * halfWidth));
299 v->set_color(color);
300 v->set_offset({ 1, -octOffset });
301 v->distance_correction = distanceCorrection;
302 v++;
303
304 // Fourth vertex
305 v->set_point(center + QVector2D(halfWidth, octOffset * halfWidth));
306 v->set_color(color);
307 v->set_offset({ 1, octOffset });
308 v->distance_correction = distanceCorrection;
309 v++;
310
311 // Fifth vertex
312 v->set_point(center + QVector2D(octOffset * halfWidth, halfWidth));
313 v->set_color(color);
314 v->set_offset({ octOffset, 1 });
315 v->distance_correction = distanceCorrection;
316 v++;
317
318 // Sixth vertex
319 v->set_point(center + QVector2D(-octOffset * halfWidth, halfWidth));
320 v->set_color(color);
321 v->set_offset({ -octOffset, 1 });
322 v->distance_correction = distanceCorrection;
323 v++;
324
325 // Seventh vertex
326 v->set_point(center + QVector2D(-halfWidth, octOffset * halfWidth));
327 v->set_color(color);
328 v->set_offset({ -1, octOffset });
329 v->distance_correction = distanceCorrection;
330 v++;
331
332 // Eighth vertex
333 v->set_point(center + QVector2D(-halfWidth, -octOffset * halfWidth));
334 v->set_color(color);
335 v->set_offset({ -1, -octOffset });
336 v->distance_correction = distanceCorrection;
337 v++;
338
339 if (isStroked) {
340 // compute the inner ring
341 // cosine and sine of pi/8
342 scalar c = 0.923579533f;
343 scalar s = 0.382683432f;
344 scalar r = args.inner_radius;
345 v->set_point(center + QVector2D(-s * r, -c * r));
346 v->set_color(color);
347 v->set_offset({ -s * innerRadius, -c * innerRadius });
348 v->distance_correction = distanceCorrection;
349 v++;
350
351 // Second vertex
352 v->set_point(center + QVector2D(s * r, -c * r));
353 v->set_color(color);
354 v->set_offset({ s * innerRadius, -c * innerRadius });
355 v->distance_correction = distanceCorrection;
356 v++;
357
358 // Third vertex
359 v->set_point(center + QVector2D(c * r, -s * r));
360 v->set_color(color);
361 v->set_offset({ c * innerRadius, -s * innerRadius });
362 v->distance_correction = distanceCorrection;
363 v++;
364
365 // Fourth vertex
366 v->set_point(center + QVector2D(c * r, s * r));
367 v->set_color(color);
368 v->set_offset({ c * innerRadius, s * innerRadius });
369 v->distance_correction = distanceCorrection;
370 v++;
371
372 // Fifth vertex
373 v->set_point(center + QVector2D(s * r, c * r));
374 v->set_color(color);
375 v->set_offset({ s * innerRadius, c * innerRadius });
376 v->distance_correction = distanceCorrection;
377 v++;
378
379 // Sixth vertex
380 v->set_point(center + QVector2D(-s * r, c * r));
381 v->set_color(color);
382 v->set_offset({ -s * innerRadius, c * innerRadius });
383 v->distance_correction = distanceCorrection;
384 v++;
385
386 // Seventh vertex
387 v->set_point(center + QVector2D(-c * r, s * r));
388 v->set_color(color);
389 v->set_offset({ -c * innerRadius, s * innerRadius });
390 v->distance_correction = distanceCorrection;
391 v++;
392
393 // Eighth vertex
394 v->set_point(center + QVector2D(-c * r, -s * r));
395 v->set_color(color);
396 v->set_offset({ -c * innerRadius, -s * innerRadius });
397 v->distance_correction = distanceCorrection;
398 v++;
399 } else {
400 // filled
401 v->set_point(center);
402 v->set_color(color);
403 v->set_offset({ 0, 0 });
404 v->distance_correction = distanceCorrection;
405 v++;
406 }
407
408 vp[0] = v;
409 }
410 void fillInRRectVerts(sg::ShadowVertex** vp) const {
411 const Geometry& args = this->fGeoData;
412 QRgb color = args.color;
413 scalar outer_radius = args.outer_radius;
414
415 const QRectF& bounds = args.dev_bounds;
416
417 scalar umbra_inset = args.umbra_inset;
418 scalar min_dim = 0.5f * std::min(bounds.width(), bounds.height());
419 if (umbra_inset > min_dim) {
420 umbra_inset = min_dim;
421 }
422
423 scalar xInner[4] = { (float)bounds.left() + umbra_inset,
424 (float)bounds.right() - umbra_inset,
425 (float)bounds.left() + umbra_inset,
426 (float)bounds.right() - umbra_inset };
427 scalar xMid[4] = { (float)bounds.left() + outer_radius,
428 (float)bounds.right() - outer_radius,
429 (float)bounds.left() + outer_radius,
430 (float)bounds.right() - outer_radius };
431 scalar xOuter[4] = {
432 (float)bounds.left(), (float)bounds.right(), (float)bounds.left(), (float)bounds.right()
433 };
434 scalar yInner[4] = { (float)bounds.top() + umbra_inset,
435 (float)bounds.top() + umbra_inset,
436 (float)bounds.bottom() - umbra_inset,
437 (float)bounds.bottom() - umbra_inset };
438 scalar yMid[4] = { (float)bounds.top() + outer_radius,
439 (float)bounds.top() + outer_radius,
440 (float)bounds.bottom() - outer_radius,
441 (float)bounds.bottom() - outer_radius };
442 scalar yOuter[4] = {
443 (float)bounds.top(), (float)bounds.top(), (float)bounds.bottom(), (float)bounds.bottom()
444 };
445
446 scalar blurRadius = args.blur_radius;
447
448 // In the case where we have to inset more for the umbra, our two triangles in the
449 // corner get skewed to a diamond rather than a square. To correct for that,
450 // we also skew the vectors we send to the shader that help define the circle.
451 // By doing so, we end up with a quarter circle in the corner rather than the
452 // elliptical curve.
453
454 // This is a bit magical, but it gives us the correct results at extrema:
455 // a) umbraInset == outerRadius produces an orthogonal vector
456 // b) outerRadius == 0 produces a diagonal vector
457 // And visually the corner looks correct.
458 QVector2D outerVec = QVector2D(outer_radius - umbra_inset, -outer_radius - umbra_inset);
459 outerVec.normalize();
460 // We want the circle edge to fall fractionally along the diagonal at
461 // (sqrt(2)*(umbraInset - outerRadius) + outerRadius)/sqrt(2)*umbraInset
462 //
463 // Setting the components of the diagonal offset to the following value will give us that.
464 scalar diag_val =
465 umbra_inset / (SK_FloatSqrt2 * (outer_radius - umbra_inset) - outer_radius);
466 QVector2D diag_vec = QVector2D(diag_val, diag_val);
467 scalar distance_correction = umbra_inset / blurRadius;
468
469 auto v = vp[0];
470 // build corner by corner
471 for (int i = 0; i < 4; ++i) {
472 // inner point
473 v->set_point(xInner[i], yInner[i]);
474 v->set_color(color);
475 v->set_offset({ 0, 0 });
476 v->distance_correction = distance_correction;
477 v++;
478
479 // outer points
480 v->set_point(xOuter[i], yInner[i]);
481 v->set_color(color);
482 v->set_offset({ 0, -1 });
483 v->distance_correction = distance_correction;
484 v++;
485
486 v->set_point(xOuter[i], yMid[i]);
487 v->set_color(color);
488 v->set_offset(outerVec);
489 v->distance_correction = distance_correction;
490 v++;
491
492 v->set_point(xOuter[i], yOuter[i]);
493 v->set_color(color);
494 v->set_offset(diag_vec);
495 v->distance_correction = distance_correction;
496 v++;
497
498 v->set_point(xMid[i], yOuter[i]);
499 v->set_color(color);
500 v->set_offset(outerVec);
501 v->distance_correction = distance_correction;
502 v++;
503
504 v->set_point(xInner[i], yOuter[i]);
505 v->set_color(color);
506 v->set_offset({ 0, -1 });
507 v->distance_correction = distance_correction;
508 v++;
509 }
510
511 // Add the additional vertices for overstroked rrects.
512 // Effectively this is an additional stroked rrect, with its
513 // parameters equal to those in the center of the 9-patch. This will
514 // give constant values across this inner ring.
515 if (kOverstroke_RRectType == args.type) {
516 Q_ASSERT(args.inner_radius > 0.0f);
517
518 scalar inset = umbra_inset + args.inner_radius;
519
520 // TL
521 v->set_point(bounds.left() + inset, bounds.top() + inset);
522 v->set_color(color);
523 v->set_offset({ 0, 0 });
524 v->distance_correction = distance_correction;
525 v++;
526
527 // TR
528 v->set_point(bounds.right() - inset, bounds.top() + inset);
529 v->set_color(color);
530 v->set_offset({ 0, 0 });
531 v->distance_correction = distance_correction;
532 v++;
533
534 // BL
535 v->set_point(bounds.left() + inset, bounds.bottom() - inset);
536 v->set_color(color);
537 v->set_offset({ 0, 0 });
538 v->distance_correction = distance_correction;
539 v++;
540
541 // BR
542 v->set_point(bounds.right() - inset, bounds.bottom() - inset);
543 v->set_color(color);
544 v->set_offset({ 0, 0 });
545 v->distance_correction = distance_correction;
546 v++;
547 }
548
549 vp[0] = v;
550 }
551
552 Geometry fGeoData;
553 int fVertCount;
554 int fIndexCount;
555 const uint16_t* fIndexPtr;
556};
557
558} // namespace qml_material::sk