Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[flow] Switch to directional shadows #27124

Merged
merged 28 commits into from
Jul 17, 2021
Merged

[flow] Switch to directional shadows #27124

merged 28 commits into from
Jul 17, 2021

Conversation

untp
Copy link
Contributor

@untp untp commented Jul 1, 2021

Shadows incorrectly offset, shifts incrementally. This issue fixed by Skia with Issue 10781: Add orthographic shadow support. We need to add SkShadowFlags::kDirectionalLight_ShadowFlag to DrawShadow.

Related Issues

fixes flutter/flutter#28463
fixes flutter/flutter#51237

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the Flutter Style Guide and the C++, Objective-C, Java style guides.
  • I listed at least one issue that this PR fixes in the description above.
  • I added new tests to check the change I am making or feature I am adding, or Hixie said the PR is test-exempt. See testing the engine for instructions on writing and running engine tests.
  • I updated/added relevant documentation (doc comments with ///).
  • I signed the CLA.
  • All existing and new tests are passing.

@google-cla google-cla bot added the cla: yes label Jul 1, 2021
const SkScalar kLightHeight = 600;
const SkScalar kLightRadius = 800;
const SkScalar kLightXOffset = 0;
const SkScalar kLightYOffset = -450;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code used to use an offset of -600 from the top of the bounds, but the value is now hard-coded as a -450 offset directionally. Doesn't that produce a different effect?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or is this change intentional due to undesired results of the previous fixed offset?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know. I just copied these values from canvaskit/util.dart. We should ask this to @yjbanov.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The values we use on the web side don't have much science to them. We just eyeballed the result and checked with the material team. In the 6 months since the change we've received zero complaints about how the shadows look.

flow/layers/physical_shape_layer.cc Outdated Show resolved Hide resolved
flow/layers/physical_shape_layer_unittests.cc Show resolved Hide resolved
@flar flar requested a review from yjbanov July 2, 2021 19:21
@untp
Copy link
Contributor Author

untp commented Jul 3, 2021

I investigated GetLocalBounds for compatibility with old implementation.
Getting the same results of the old implementation is impossible.
So we need to choose values that gives a result closest to the old implementation.
Here are the simplified equations from GetLocalBounds:

ambientBlur = 0.5 * zPlane.z / ctm.getMinScale()
if kDirectionalLight_ShadowFlag in flags:
  spotBlur = lightRadius * zPlane.z / ctm.getMinScale()
  spotScale = 1
  spotOffset.x = - lightPos.normalize().x * zPlane.z / ctm.getMinScale()
  spotOffset.y = - lightPos.normalize().y * zPlane.z / ctm.getMinScale()
else:
  spotBlur = lightRadius * (zPlane.z / (lightPos.z - zPlane.z)) / ctm.getMinScale()
  spotScale = lightPos.z / (lightPos.z - zPlane.z)
  spotOffset.x = - (zPlane.z / (lightPos.z - zPlane.z)) *
                       ((lightPos.x - ctm.getTranslateX()) / ctm.getMinScale())
  spotOffset.y = - (zPlane.z / (lightPos.z - zPlane.z)) *
                       ((lightPos.y - ctm.getTranslateY()) / ctm.getMinScale())

If these equations applied to the old implementation:
(assume ctm.getMinScale() equal to dpr)

ambientBlur = 0.5 * elevation
spotBlur = kLightRadius * elevation / (kLightHeight - elevation)
spotScale = kLightHeight / (kLightHeight - elevation)
spotOffset.x = - elevation / (kLightHeight - elevation) *
    (((bounds.left() + bounds.right()) / 2 - ctm.getTranslateX()) / ctm.getMinScale())
spotOffset.y = - elevation / (kLightHeight - elevation) *
    ((bounds.top() - kLightHeight - ctm.getTranslateY()) / ctm.getMinScale())

These 5 variables needs to be equal between old and new implementation but it is impossible.
spotScale can not be equal because 1 != kLightHeight / (kLightHeight - elevation).
If we assume kLightHeight - elevation equals to kLightHeight, variables become:
(Also assuming ctm.getTranslateX() == (bounds.left() + bounds.right()) / 2
and assuming ctm.getTranslateY() == bounds.top())

ambientBlur = 0.5 * elevation
spotBlur = kLightRadius / kLightHeight * elevation
spotScale = 1
spotOffset.x = 0
spotOffset.y = elevation / ctm.getMinScale()

Let's find out new implementation's values:

0.5 * zPlane.z / ctm.getMinScale() = 0.5 * elevation
lightRadius * zPlane.z / ctm.getMinScale() = kLightRadius / kLightHeight * elevation
- zPlane.z * lightPos.normalize().x / ctm.getMinScale() = 0
- zPlane.z * lightPos.normalize().y / ctm.getMinScale() = elevation / ctm.getMinScale()

After simplifying:

zPlane.z = elevation * ctm.getMinScale()
lightRadius = kLightRadius / kLightHeight
lightPos.normalize().x = 0
lightPos.normalize().y = - 1 / ctm.getMinScale()

ctm.getMinScale() mostly greater than 1 but can be smaller than 1 on low resolution devices.
And normalize() can't return smaller than -1; so, we need to assume ctm.getMinScale() is 1 only for lightPos.normalize().y.
Therefore we set lightPos as (0, -1, 0).
So function call should be like this:
(Using dpr as ctm.getMinScale(), because sometimes ctm.getMinScale() != dpr)

SkShadowUtils::GetLocalBounds(
    ctm,
    path,
    SkPoint3::Make(0, 0, elevation * dpr),
    SkPoint3::Make(0, -1, 0),
    kLightRadius / kLightHeight,
    SkShadowFlags::kDirectionalLight_ShadowFlag,
    &shadow_bounds);

These are results of different rects calculated with old and new implementation:
(elevation = 1, dpr = 1, ctm = SkMatrix())

--------------------------------
rect: LTRB: -1, 0, 1, 1
old_impl: LTRB: -3.33723, -1.5, 3.33723, 4.3389
new_impl: LTRB: -3.33333, -1.5, 3.33333, 4.33333
--------------------------------
rect: LTRB: -10, 0, 10, 10
old_impl: LTRB: -12.3523, -1.5, 12.3523, 13.3539
new_impl: LTRB: -12.3333, -1.5, 12.3333, 13.3333
--------------------------------
rect: LTRB: -100, 0, 100, 100
old_impl: LTRB: -102.502, -1.5, 102.502, 103.504
new_impl: LTRB: -102.333, -1.5, 102.333, 103.333
--------------------------------
rect: LTRB: -1, 0, 1, 2
old_impl: LTRB: -3.33723, -1.5, 3.33723, 5.34057
new_impl: LTRB: -3.33333, -1.5, 3.33333, 5.33333
--------------------------------
rect: LTRB: -10, 0, 10, 20
old_impl: LTRB: -12.3523, -1.5, 12.3523, 23.3706
new_impl: LTRB: -12.3333, -1.5, 12.3333, 23.3333
--------------------------------
rect: LTRB: -100, 0, 100, 200
old_impl: LTRB: -102.502, -1.5, 102.502, 203.671
new_impl: LTRB: -102.333, -1.5, 102.333, 203.333
--------------------------------
rect: LTRB: 0, 0, 1, 1
old_impl: LTRB: -2.33639, -1.5, 3.33639, 4.3389
new_impl: LTRB: -2.33333, -1.5, 3.33333, 4.33333
--------------------------------
rect: LTRB: 0, 0, 10, 10
old_impl: LTRB: -2.34391, -1.5, 12.3439, 13.3539
new_impl: LTRB: -2.33333, -1.5, 12.3333, 13.3333
--------------------------------
rect: LTRB: 0, 0, 100, 100
old_impl: LTRB: -2.41903, -1.5, 102.419, 103.504
new_impl: LTRB: -2.33333, -1.5, 102.333, 103.333
--------------------------------
rect: LTRB: 1, 1, 2, 2
old_impl: LTRB: -1.33639, -0.5, 4.33639, 5.3389
new_impl: LTRB: -1.33333, -0.5, 4.33333, 5.33333
--------------------------------
rect: LTRB: 10, 10, 20, 20
old_impl: LTRB: 7.65609, 8.5, 22.3439, 23.3539
new_impl: LTRB: 7.66667, 8.5, 22.3333, 23.3333
--------------------------------
rect: LTRB: 100, 100, 200, 200
old_impl: LTRB: 97.581, 98.5, 202.419, 203.504
new_impl: LTRB: 97.6667, 98.5, 202.333, 203.333
--------------------------------

@flar
Copy link
Contributor

flar commented Jul 3, 2021

I investigated GetLocalBounds for compatibility with old implementation.

I think that may be a mistake in tactics. This assumes that the old implementation was correct. In particular, when I was using the PhysicalShapeLayer to compute bounds for my DisplayList tests I ran into problems with it not enclosing all of the pixels that were rendered (the DL tests do OOB testing for rendering vs DL.bounds()). I ended up ignoring it and using GetLocalBounds in the shadow utils instead for DL bounds computations.

The goal here will be to make sure that the bounds computed encompass the rendering, not that they are compatible with the prior implementation of something. It would be nice if they were no looser than the old implementation as well, for performance.

With respect to rendering, we need to decide what the goal is here. Some of the comments linked above talked about not matching the Material spec, and I think we do need to ensure our Material widgets match the material spec.

We also need to consider that we have Cupertino widgets that may also use these shadows and need to be compatible with the Cupertino spec.

If they are dragging us in 2 different directions with how we interpret various values like elevation and light direction, then maybe we need to add some extra parameters to our shadow parameters, but hopefully they have similar enough shadow specs that we can meet them with the existing parameters.

@untp
Copy link
Contributor Author

untp commented Jul 3, 2021

I think, people want to see same shadows after this fix. Because this PR edits code that 4 years old. Material specification is not strict, it doesn't provide values, it just say combine shadow from key and ambient lights.

I don't know what is the goal. I just fixed the above issues. Now shadows doesn't depend on ctm.

Maybe we need to invite someone from material design team.

Also here are some screenshots

Old implementation

old

New implementation

new

@jvanverth
Copy link
Contributor

jvanverth commented Jul 14, 2021

So the reason the resulting direction is unintuitive is that this case has exposed a bug in the directional lighting code. Rather than

spotOffset.x = - lightPos.normalize().x * zPlane.z / ctm.getMinScale()
spotOffset.y = - lightPos.normalize().y * zPlane.z / ctm.getMinScale()

it should be

spotOffset.x = - lightPos.x / lightPos.z * zPlane.z / ctm.getMinScale()
spotOffset.y = - lightPos.y / lightPos.z * zPlane.z / ctm.getMinScale()

(This also makes me think that the blur could be

spotBlur = lightRadius / lightPos.z * zPlane.z / ctm.getMinScale()

though either way it's sort of a hack. A true directional light wouldn't normally affect blur, since it's infinitely far away.)

In any case, with those changes lightPos should end up being (0, -sqrt(2)/2, sqrt(2)/2, which makes more sense as a light direction.

@jvanverth
Copy link
Contributor

Oh, and once I land this fix it probably means that the Web values would have to change, depending on how sharp the lighting angle is.

@flar
Copy link
Contributor

flar commented Jul 14, 2021

In any case, with those changes lightPos should end up being (0, -sqrt(2)/2, sqrt(2)/2, which makes more sense as a light direction.

Wouldn't (0, -1, 1) work as well since the values are "Z-normalized"?

@jvanverth
Copy link
Contributor

Yeah, any vector pointing in that direction would work, I just normalized it out of habit.

@yjbanov
Copy link
Contributor

yjbanov commented Jul 14, 2021

Oh, and once I land this fix it probably means that the Web values would have to change, depending on how sharp the lighting angle is.

We can address the web side in a separate PR. We roll Skia with CanvasKit releases, so this won't take effect immediately. Thanks for the heads up though.

@jvanverth
Copy link
Contributor

We can address the web side in a separate PR. We roll Skia with CanvasKit releases, so this won't take effect immediately. Thanks for the heads up though.

Sounds good, I'll land the fix tomorrow morning.

@flar
Copy link
Contributor

flar commented Jul 14, 2021

When the Skia fix lands and this PR will have to be rebased to catch it, I think that may also fix the odd error in the Linux Android Scenarios as well.

@flar
Copy link
Contributor

flar commented Jul 16, 2021

Just a status update. I'm on the CC thread for the Skia fix to the directional shadow math and it is actively being worked on. The delays are minor and related to making sure it passes all tests and golden image comparisons.

@jvanverth
Copy link
Contributor

The fix has landed: https://skia-review.googlesource.com/c/skia/+/428978

@flar
Copy link
Contributor

flar commented Jul 16, 2021

Try rebasing to >= c120a50 to check with the fix in place.

@untp
Copy link
Contributor Author

untp commented Jul 16, 2021

I already rebased with previous commits. My branch's DEPS' skia_revision is 94fda947.

Copy link
Contributor

@flar flar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@flar flar added the waiting for tree to go green This PR is approved and tested, but waiting for the tree to be green to land. label Jul 16, 2021
@fluttergithubbot fluttergithubbot merged commit 334fa80 into flutter:master Jul 17, 2021
blueboxd pushed a commit to blueboxd/skia that referenced this pull request Jul 17, 2021
This is a reland of 6789b82

Original change's description:
> Fix directional shadows.
>
> The xy offset calculation for drawShadow was not quite correct. Rather
> than normalizing the light vector and using the xy values of that as the
> base offset value, we should scale the light vector by 1/z.
>
> See flutter/engine#27124 (comment)
> for more detail.
>
> Bug: skia:10781
> Change-Id: Ib69a313cb96a532f8d89644e3d69f666a184e897
> Reviewed-on: https://skia-review.googlesource.com/c/skia/+/428880
> Reviewed-by: Brian Salomon <[email protected]>
> Commit-Queue: Jim Van Verth <[email protected]>

Bug: skia:10781
Change-Id: Ib58d374aa03d0144512e5ded6ccd572c74783607
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/428978
Reviewed-by: Brian Salomon <[email protected]>
Commit-Queue: Jim Van Verth <[email protected]>
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Jul 17, 2021
@untp untp deleted the untp-patch-1 branch July 17, 2021 09:44
moffatman pushed a commit to moffatman/engine that referenced this pull request Aug 5, 2021
naudzghebre pushed a commit to naudzghebre/engine that referenced this pull request Sep 2, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cla: yes waiting for tree to go green This PR is approved and tested, but waiting for the tree to be green to land.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Shadows depend on internal canvas state Shadow gets weird when card is too long
5 participants