1 | /* |
2 | * Copyright 2017 Google Inc. |
3 | * |
4 | * Use of this source code is governed by a BSD-style license that can be |
5 | * found in the LICENSE file. |
6 | */ |
7 | |
8 | #include "include/core/SkMatrix.h" |
9 | #include "include/core/SkPath.h" |
10 | #include "include/core/SkRect.h" |
11 | #include "src/core/SkDrawShadowInfo.h" |
12 | #include "src/utils/SkPolyUtils.h" |
13 | |
14 | namespace SkDrawShadowMetrics { |
15 | |
16 | static SkScalar compute_z(SkScalar x, SkScalar y, const SkPoint3& params) { |
17 | return x*params.fX + y*params.fY + params.fZ; |
18 | } |
19 | |
20 | bool GetSpotShadowTransform(const SkPoint3& lightPos, SkScalar lightRadius, |
21 | const SkMatrix& ctm, const SkPoint3& zPlaneParams, |
22 | const SkRect& pathBounds, SkMatrix* shadowTransform, SkScalar* radius) { |
23 | auto heightFunc = [zPlaneParams] (SkScalar x, SkScalar y) { |
24 | return zPlaneParams.fX*x + zPlaneParams.fY*y + zPlaneParams.fZ; |
25 | }; |
26 | SkScalar occluderHeight = heightFunc(pathBounds.centerX(), pathBounds.centerY()); |
27 | |
28 | if (!ctm.hasPerspective()) { |
29 | SkScalar scale; |
30 | SkVector translate; |
31 | SkDrawShadowMetrics::GetSpotParams(occluderHeight, lightPos.fX, lightPos.fY, lightPos.fZ, |
32 | lightRadius, radius, &scale, &translate); |
33 | shadowTransform->setScaleTranslate(scale, scale, translate.fX, translate.fY); |
34 | shadowTransform->preConcat(ctm); |
35 | } else { |
36 | if (SkScalarNearlyZero(pathBounds.width()) || SkScalarNearlyZero(pathBounds.height())) { |
37 | return false; |
38 | } |
39 | |
40 | // get rotated quad in 3D |
41 | SkPoint pts[4]; |
42 | ctm.mapRectToQuad(pts, pathBounds); |
43 | // No shadows for bowties or other degenerate cases |
44 | if (!SkIsConvexPolygon(pts, 4)) { |
45 | return false; |
46 | } |
47 | SkPoint3 pts3D[4]; |
48 | SkScalar z = heightFunc(pathBounds.fLeft, pathBounds.fTop); |
49 | pts3D[0].set(pts[0].fX, pts[0].fY, z); |
50 | z = heightFunc(pathBounds.fRight, pathBounds.fTop); |
51 | pts3D[1].set(pts[1].fX, pts[1].fY, z); |
52 | z = heightFunc(pathBounds.fRight, pathBounds.fBottom); |
53 | pts3D[2].set(pts[2].fX, pts[2].fY, z); |
54 | z = heightFunc(pathBounds.fLeft, pathBounds.fBottom); |
55 | pts3D[3].set(pts[3].fX, pts[3].fY, z); |
56 | |
57 | // project from light through corners to z=0 plane |
58 | for (int i = 0; i < 4; ++i) { |
59 | SkScalar dz = lightPos.fZ - pts3D[i].fZ; |
60 | // light shouldn't be below or at a corner's z-location |
61 | if (dz <= SK_ScalarNearlyZero) { |
62 | return false; |
63 | } |
64 | SkScalar zRatio = pts3D[i].fZ / dz; |
65 | pts3D[i].fX -= (lightPos.fX - pts3D[i].fX)*zRatio; |
66 | pts3D[i].fY -= (lightPos.fY - pts3D[i].fY)*zRatio; |
67 | pts3D[i].fZ = SK_Scalar1; |
68 | } |
69 | |
70 | // Generate matrix that projects from [-1,1]x[-1,1] square to projected quad |
71 | SkPoint3 h0, h1, h2; |
72 | // Compute homogenous crossing point between top and bottom edges (gives new x-axis). |
73 | h0 = (pts3D[1].cross(pts3D[0])).cross(pts3D[2].cross(pts3D[3])); |
74 | // Compute homogenous crossing point between left and right edges (gives new y-axis). |
75 | h1 = (pts3D[0].cross(pts3D[3])).cross(pts3D[1].cross(pts3D[2])); |
76 | // Compute homogenous crossing point between diagonals (gives new origin). |
77 | h2 = (pts3D[0].cross(pts3D[2])).cross(pts3D[1].cross(pts3D[3])); |
78 | // If h2 is a vector (z=0 in 2D homogeneous space), that means that at least |
79 | // two of the quad corners are coincident and we don't have a realistic projection |
80 | if (SkScalarNearlyZero(h2.fZ)) { |
81 | return false; |
82 | } |
83 | // In some cases the crossing points are in the wrong direction |
84 | // to map (-1,-1) to pts3D[0], so we need to correct for that. |
85 | // Want h0 to be to the right of the left edge. |
86 | SkVector3 v = pts3D[3] - pts3D[0]; |
87 | SkVector3 w = h0 - pts3D[0]; |
88 | SkScalar perpDot = v.fX*w.fY - v.fY*w.fX; |
89 | if (perpDot > 0) { |
90 | h0 = -h0; |
91 | } |
92 | // Want h1 to be above the bottom edge. |
93 | v = pts3D[1] - pts3D[0]; |
94 | perpDot = v.fX*w.fY - v.fY*w.fX; |
95 | if (perpDot < 0) { |
96 | h1 = -h1; |
97 | } |
98 | shadowTransform->setAll(h0.fX / h2.fZ, h1.fX / h2.fZ, h2.fX / h2.fZ, |
99 | h0.fY / h2.fZ, h1.fY / h2.fZ, h2.fY / h2.fZ, |
100 | h0.fZ / h2.fZ, h1.fZ / h2.fZ, 1); |
101 | // generate matrix that transforms from bounds to [-1,1]x[-1,1] square |
102 | SkMatrix toHomogeneous; |
103 | SkScalar xScale = 2/(pathBounds.fRight - pathBounds.fLeft); |
104 | SkScalar yScale = 2/(pathBounds.fBottom - pathBounds.fTop); |
105 | toHomogeneous.setAll(xScale, 0, -xScale*pathBounds.fLeft - 1, |
106 | 0, yScale, -yScale*pathBounds.fTop - 1, |
107 | 0, 0, 1); |
108 | shadowTransform->preConcat(toHomogeneous); |
109 | |
110 | *radius = SkDrawShadowMetrics::SpotBlurRadius(occluderHeight, lightPos.fZ, lightRadius); |
111 | } |
112 | |
113 | return true; |
114 | } |
115 | |
116 | void GetLocalBounds(const SkPath& path, const SkDrawShadowRec& rec, const SkMatrix& ctm, |
117 | SkRect* bounds) { |
118 | SkRect ambientBounds = path.getBounds(); |
119 | SkScalar occluderZ; |
120 | if (SkScalarNearlyZero(rec.fZPlaneParams.fX) && SkScalarNearlyZero(rec.fZPlaneParams.fY)) { |
121 | occluderZ = rec.fZPlaneParams.fZ; |
122 | } else { |
123 | occluderZ = compute_z(ambientBounds.fLeft, ambientBounds.fTop, rec.fZPlaneParams); |
124 | occluderZ = std::max(occluderZ, compute_z(ambientBounds.fRight, ambientBounds.fTop, |
125 | rec.fZPlaneParams)); |
126 | occluderZ = std::max(occluderZ, compute_z(ambientBounds.fLeft, ambientBounds.fBottom, |
127 | rec.fZPlaneParams)); |
128 | occluderZ = std::max(occluderZ, compute_z(ambientBounds.fRight, ambientBounds.fBottom, |
129 | rec.fZPlaneParams)); |
130 | } |
131 | SkScalar ambientBlur; |
132 | SkScalar spotBlur; |
133 | SkScalar spotScale; |
134 | SkPoint spotOffset; |
135 | if (ctm.hasPerspective()) { |
136 | // transform ambient and spot bounds into device space |
137 | ctm.mapRect(&ambientBounds); |
138 | |
139 | // get ambient blur (in device space) |
140 | ambientBlur = SkDrawShadowMetrics::AmbientBlurRadius(occluderZ); |
141 | |
142 | // get spot params (in device space) |
143 | SkPoint devLightPos = SkPoint::Make(rec.fLightPos.fX, rec.fLightPos.fY); |
144 | ctm.mapPoints(&devLightPos, 1); |
145 | SkDrawShadowMetrics::GetSpotParams(occluderZ, devLightPos.fX, devLightPos.fY, |
146 | rec.fLightPos.fZ, rec.fLightRadius, |
147 | &spotBlur, &spotScale, &spotOffset); |
148 | } else { |
149 | SkScalar devToSrcScale = SkScalarInvert(ctm.getMinScale()); |
150 | |
151 | // get ambient blur (in local space) |
152 | SkScalar devSpaceAmbientBlur = SkDrawShadowMetrics::AmbientBlurRadius(occluderZ); |
153 | ambientBlur = devSpaceAmbientBlur*devToSrcScale; |
154 | |
155 | // get spot params (in local space) |
156 | SkDrawShadowMetrics::GetSpotParams(occluderZ, rec.fLightPos.fX, rec.fLightPos.fY, |
157 | rec.fLightPos.fZ, rec.fLightRadius, |
158 | &spotBlur, &spotScale, &spotOffset); |
159 | |
160 | // convert spot blur to local space |
161 | spotBlur *= devToSrcScale; |
162 | } |
163 | |
164 | // in both cases, adjust ambient and spot bounds |
165 | SkRect spotBounds = ambientBounds; |
166 | ambientBounds.outset(ambientBlur, ambientBlur); |
167 | spotBounds.fLeft *= spotScale; |
168 | spotBounds.fTop *= spotScale; |
169 | spotBounds.fRight *= spotScale; |
170 | spotBounds.fBottom *= spotScale; |
171 | spotBounds.offset(spotOffset.fX, spotOffset.fY); |
172 | spotBounds.outset(spotBlur, spotBlur); |
173 | |
174 | // merge bounds |
175 | *bounds = ambientBounds; |
176 | bounds->join(spotBounds); |
177 | // outset a bit to account for floating point error |
178 | bounds->outset(1, 1); |
179 | |
180 | // if perspective, transform back to src space |
181 | if (ctm.hasPerspective()) { |
182 | // TODO: create tighter mapping from dev rect back to src rect |
183 | SkMatrix inverse; |
184 | if (ctm.invert(&inverse)) { |
185 | inverse.mapRect(bounds); |
186 | } |
187 | } |
188 | } |
189 | |
190 | |
191 | } |
192 | |
193 | |