/*
* Copyright 2017 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "SkDrawShadowInfo.h"
#include "SkMatrix.h"
#include "SkPath.h"
#include "SkPolyUtils.h"
#include "SkRect.h"
namespace SkDrawShadowMetrics {
static SkScalar compute_z(SkScalar x, SkScalar y, const SkPoint3& params) {
return x*params.fX + y*params.fY + params.fZ;
}
bool GetSpotShadowTransform(const SkPoint3& lightPos, SkScalar lightRadius,
const SkMatrix& ctm, const SkPoint3& zPlaneParams,
const SkRect& pathBounds, SkMatrix* shadowTransform, SkScalar* radius) {
auto heightFunc = [zPlaneParams] (SkScalar x, SkScalar y) {
return zPlaneParams.fX*x + zPlaneParams.fY*y + zPlaneParams.fZ;
};
SkScalar occluderHeight = heightFunc(pathBounds.centerX(), pathBounds.centerY());
if (!ctm.hasPerspective()) {
SkScalar scale;
SkVector translate;
SkDrawShadowMetrics::GetSpotParams(occluderHeight, lightPos.fX, lightPos.fY, lightPos.fZ,
lightRadius, radius, &scale, &translate);
shadowTransform->setScaleTranslate(scale, scale, translate.fX, translate.fY);
shadowTransform->preConcat(ctm);
} else {
if (SkScalarNearlyZero(pathBounds.width()) || SkScalarNearlyZero(pathBounds.height())) {
return false;
}
// get rotated quad in 3D
SkPoint pts[4];
ctm.mapRectToQuad(pts, pathBounds);
// No shadows for bowties or other degenerate cases
if (!SkIsConvexPolygon(pts, 4)) {
return false;
}
SkPoint3 pts3D[4];
SkScalar z = heightFunc(pathBounds.fLeft, pathBounds.fTop);
pts3D[0].set(pts[0].fX, pts[0].fY, z);
z = heightFunc(pathBounds.fRight, pathBounds.fTop);
pts3D[1].set(pts[1].fX, pts[1].fY, z);
z = heightFunc(pathBounds.fRight, pathBounds.fBottom);
pts3D[2].set(pts[2].fX, pts[2].fY, z);
z = heightFunc(pathBounds.fLeft, pathBounds.fBottom);
pts3D[3].set(pts[3].fX, pts[3].fY, z);
// project from light through corners to z=0 plane
for (int i = 0; i < 4; ++i) {
SkScalar dz = lightPos.fZ - pts3D[i].fZ;
// light shouldn't be below or at a corner's z-location
if (dz <= SK_ScalarNearlyZero) {
return false;
}
SkScalar zRatio = pts3D[i].fZ / dz;
pts3D[i].fX -= (lightPos.fX - pts3D[i].fX)*zRatio;
pts3D[i].fY -= (lightPos.fY - pts3D[i].fY)*zRatio;
pts3D[i].fZ = SK_Scalar1;
}
// Generate matrix that projects from [-1,1]x[-1,1] square to projected quad
SkPoint3 h0, h1, h2;
// Compute homogenous crossing point between top and bottom edges (gives new x-axis).
h0 = (pts3D[1].cross(pts3D[0])).cross(pts3D[2].cross(pts3D[3]));
// Compute homogenous crossing point between left and right edges (gives new y-axis).
h1 = (pts3D[0].cross(pts3D[3])).cross(pts3D[1].cross(pts3D[2]));
// Compute homogenous crossing point between diagonals (gives new origin).
h2 = (pts3D[0].cross(pts3D[2])).cross(pts3D[1].cross(pts3D[3]));
// If h2 is a vector (z=0 in 2D homogeneous space), that means that at least
// two of the quad corners are coincident and we don't have a realistic projection
if (SkScalarNearlyZero(h2.fZ)) {
return false;
}
// In some cases the crossing points are in the wrong direction
// to map (-1,-1) to pts3D[0], so we need to correct for that.
// Want h0 to be to the right of the left edge.
SkVector3 v = pts3D[3] - pts3D[0];
SkVector3 w = h0 - pts3D[0];
SkScalar perpDot = v.fX*w.fY - v.fY*w.fX;
if (perpDot > 0) {
h0 = -h0;
}
// Want h1 to be above the bottom edge.
v = pts3D[1] - pts3D[0];
perpDot = v.fX*w.fY - v.fY*w.fX;
if (perpDot < 0) {
h1 = -h1;
}
shadowTransform->setAll(h0.fX / h2.fZ, h1.fX / h2.fZ, h2.fX / h2.fZ,
h0.fY / h2.fZ, h1.fY / h2.fZ, h2.fY / h2.fZ,
h0.fZ / h2.fZ, h1.fZ / h2.fZ, 1);
// generate matrix that transforms from bounds to [-1,1]x[-1,1] square
SkMatrix toHomogeneous;
SkScalar xScale = 2/(pathBounds.fRight - pathBounds.fLeft);
SkScalar yScale = 2/(pathBounds.fBottom - pathBounds.fTop);
toHomogeneous.setAll(xScale, 0, -xScale*pathBounds.fLeft - 1,
0, yScale, -yScale*pathBounds.fTop - 1,
0, 0, 1);
shadowTransform->preConcat(toHomogeneous);
*radius = SkDrawShadowMetrics::SpotBlurRadius(occluderHeight, lightPos.fZ, lightRadius);
}
return true;
}
void GetLocalBounds(const SkPath& path, const SkDrawShadowRec& rec, const SkMatrix& ctm,
SkRect* bounds) {
SkRect ambientBounds = path.getBounds();
SkScalar occluderZ;
if (SkScalarNearlyZero(rec.fZPlaneParams.fX) && SkScalarNearlyZero(rec.fZPlaneParams.fY)) {
occluderZ = rec.fZPlaneParams.fZ;
} else {
occluderZ = compute_z(ambientBounds.fLeft, ambientBounds.fTop, rec.fZPlaneParams);
occluderZ = SkTMax(occluderZ, compute_z(ambientBounds.fRight, ambientBounds.fTop,
rec.fZPlaneParams));
occluderZ = SkTMax(occluderZ, compute_z(ambientBounds.fLeft, ambientBounds.fBottom,
rec.fZPlaneParams));
occluderZ = SkTMax(occluderZ, compute_z(ambientBounds.fRight, ambientBounds.fBottom,
rec.fZPlaneParams));
}
SkScalar ambientBlur;
SkScalar spotBlur;
SkScalar spotScale;
SkPoint spotOffset;
if (ctm.hasPerspective()) {
// transform ambient and spot bounds into device space
ctm.mapRect(&ambientBounds);
// get ambient blur (in device space)
ambientBlur = SkDrawShadowMetrics::AmbientBlurRadius(occluderZ);
// get spot params (in device space)
SkPoint devLightPos = SkPoint::Make(rec.fLightPos.fX, rec.fLightPos.fY);
ctm.mapPoints(&devLightPos, 1);
SkDrawShadowMetrics::GetSpotParams(occluderZ, devLightPos.fX, devLightPos.fY,
rec.fLightPos.fZ, rec.fLightRadius,
&spotBlur, &spotScale, &spotOffset);
} else {
SkScalar devToSrcScale = SkScalarInvert(ctm.getMinScale());
// get ambient blur (in local space)
SkScalar devSpaceAmbientBlur = SkDrawShadowMetrics::AmbientBlurRadius(occluderZ);
ambientBlur = devSpaceAmbientBlur*devToSrcScale;
// get spot params (in local space)
SkDrawShadowMetrics::GetSpotParams(occluderZ, rec.fLightPos.fX, rec.fLightPos.fY,
rec.fLightPos.fZ, rec.fLightRadius,
&spotBlur, &spotScale, &spotOffset);
// convert spot blur to local space
spotBlur *= devToSrcScale;
}
// in both cases, adjust ambient and spot bounds
SkRect spotBounds = ambientBounds;
ambientBounds.outset(ambientBlur, ambientBlur);
spotBounds.fLeft *= spotScale;
spotBounds.fTop *= spotScale;
spotBounds.fRight *= spotScale;
spotBounds.fBottom *= spotScale;
spotBounds.offset(spotOffset.fX, spotOffset.fY);
spotBounds.outset(spotBlur, spotBlur);
// merge bounds
*bounds = ambientBounds;
bounds->join(spotBounds);
// outset a bit to account for floating point error
bounds->outset(1, 1);
// if perspective, transform back to src space
if (ctm.hasPerspective()) {
// TODO: create tighter mapping from dev rect back to src rect
SkMatrix inverse;
if (ctm.invert(&inverse)) {
inverse.mapRect(bounds);
}
}
}
}