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