Windows 10, UWP and Composition–Light and Shade

One of the functional areas that the composition APIs offer are the abilities to add both lighting and shadowing effects to UI and I was interested in trying them out after some of my efforts with general effects like blur.

It’s worth saying that the official samples have a DropShadow sample and some lighting samples within them but I wanted to experiment for myself and especially around how I might create this kind of effect that the UI team shared on one of their newsletters – this picture;

image

had me thinking “how do you do that?” and it got my interest.

Shadows and Fog

I’ll admit that there’s another version of this blog post where I fumbled around a bit trying to understand what the Shadow property was for on a SpriteVisual and trying to make use of it in a way that really didn’t work out for me and caused me to go looking for the manual.

Fortunately, at the time that I was writing the post, I did a search and I found this brand new article on MSDN;

Using the Visual Layer with XAML

which has a recipe for drop shadows and that helped a lot and caused me to more or less start over and rework the post entirely as I had spent a little time barking up the wrong tree.

I also realised that I should have watched this video from //Build as well around effects, lighting and shadows before trying to make it up on my own Smile 

image

Armed with some of what I saw in these materials, my first experiment with shadows was to make a blank UI.

  <Grid
    x:Name="grid"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

  </Grid>

and to write a tiny bit of code which set up a SpriteVisual which did not paint itself at all but which did have a Shadow set for it as below;

      var compositor = ElementCompositionPreview.GetElementVisual(this.grid).Compositor;
      var spriteVisual = compositor.CreateSpriteVisual();
      spriteVisual.Size = new Vector2(100, 100);
      var dropShadow = compositor.CreateDropShadow();
      dropShadow.Offset = new Vector3(10, 10, 0);
      dropShadow.Color = Colors.Orange;
      spriteVisual.Shadow = dropShadow;
      ElementCompositionPreview.SetElementChildVisual(this.grid, spriteVisual);

and the interesting thing to me was that this Visual does indeed paint a shadow even though it doesn’t paint any content.

My earlier attempts had assumed something quite different and that’s why I’d fumbled around quite a lot before getting to what now seems like a simple realisation – a SpriteVisual can paint both content and a shadow and, by default, the shadow is rectangular;

Capture

The other key part of what I learned in that recipe is that the DropShadow has a Mask property that you can use to shape the shadow.

I’m unsure how to get hold of such a mask for arbitrary content but, from the recipes post, a new method has been added to Image, TextBlock and ShapeGetAlphaMask() which makes this very easy for those specific element types.

So, if my challenge was to add a shadow to a TextBlock then my UI can become;

  <Grid
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Grid HorizontalAlignment="Center" VerticalAlignment="Center">
      <Grid
        x:Name="grid" />
      <TextBlock
        x:Name="txtBlock"
        Text="Drop Shadow"
        FontSize="48"
        HorizontalAlignment="Center"
        VerticalAlignment="Center" />
    </Grid>
  </Grid>

and my code can become;

      var compositor = ElementCompositionPreview.GetElementVisual(this.grid).Compositor;
      var spriteVisual = compositor.CreateSpriteVisual();
      spriteVisual.Size = this.grid.RenderSize.ToVector2();

      var dropShadow = compositor.CreateDropShadow();
      dropShadow.Mask = this.txtBlock.GetAlphaMask();
      dropShadow.Offset = new Vector3(10, 10, 0);
      spriteVisual.Shadow = dropShadow;

      ElementCompositionPreview.SetElementChildVisual(this.grid, spriteVisual);

and that works out well;

Capture

but I think the heavy lifting here is really being done by TextBlock.GetAlphaMask() and I wondered how I might do this for arbitrary content in the absence of a helper method?

For instance, TextBox doesn’t have a GetAlphaMask() property and neither does Slider and so on.

It’s worth noting that in the official samples, there’s a use of a circular image to mask a shadow in the “Shadow Playground” such that you can have either a rectangular shadow;

Capture

or a circular one;

Capture

and that makes it fairly clear that if you know the shape of the mask up-front then you can represent it by an image and use that to mask the shadow.

But, let’s say that I do have arbitrary content in XAML, how do I apply a shadow to it? Something like a Slider?

I searched around a little and found a similar question here;

Are shadows only for images?

and that was useful to me for 2 reasons.

One was that my local samples clone had got out of step with the remote (and I hadn’t noticed) and so I’d been staring at out of date samples thinking that my git pull had been updating them when it hadn’t been.

The other was that it seemed to answer my question with the supplied CompositionShadow user control until I took a look into it and realised that it’s hard-wired to assume that the content of the control is one of the three that have had a GetAlphaMask() method added to them – i.e. either a TextBlock, Image or Shape.

This then led me back to the same question that I started with which would be how I could apply a shadow to an arbitrary piece of UI which isn’t;

  • Image, TextBlock, Shape
  • Rectangular
  • Defined by a pre-defined shape that can be masked by an image of that shape

Now, I must admit that maybe this question isn’t particularly important given that this list probably covers 95%+ of the useful cases anyway but it still left me curious and I’m not sure how I’d do it at the time of writing other than to possibly try and render the XAML element as an image and then use that as a mask of some sort.

I gave it a quick try even though it’s likely to be prohibitively expensive to do it and I use the CompositionImageBrush class that I wrote in this post and with this UI;


  <Grid
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Grid
      HorizontalAlignment="Center"
      VerticalAlignment="Center">
      <Grid
        x:Name="grid"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch" />
      <Slider
        x:Name="slider"
        Width="284" />
    </Grid>
  </Grid>

and this chunk of code-behind;

    async void OnLoaded(object sender, RoutedEventArgs e)
    {
      var gridVisual = ElementCompositionPreview.GetElementVisual(this.grid);

      var spriteVisual = gridVisual.Compositor.CreateSpriteVisual();
      spriteVisual.Size = this.slider.RenderSize.ToVector2();

      var bitmap = new RenderTargetBitmap();

      await bitmap.RenderAsync(
        this.slider,
        (int)this.slider.ActualWidth,
        (int)this.slider.ActualHeight);

      var pixels = await bitmap.GetPixelsAsync();

      using (var softwareBitmap = SoftwareBitmap.CreateCopyFromBuffer(
        pixels,
        BitmapPixelFormat.Bgra8,
        bitmap.PixelWidth,
        bitmap.PixelHeight,
        BitmapAlphaMode.Premultiplied))
      {
        var brush = CompositionImageBrush.FromBGRASoftwareBitmap(
          gridVisual.Compositor,
          softwareBitmap,
          new Size(bitmap.PixelWidth, bitmap.PixelHeight));

        var dropShadow = gridVisual.Compositor.CreateDropShadow();
        dropShadow.Mask = brush.Brush;
        dropShadow.Offset = new Vector3(50, 50, 0);
        spriteVisual.Shadow = dropShadow;
      }
      ElementCompositionPreview.SetElementChildVisual(this.grid, spriteVisual);
    }

then it “kind of worked”;

Capture

although, clearly, that work would need re-doing every time the value changed and the slider resized and that kind of thing so it’s probably not a very “practical” solution Smile

No doubt there’s a better way and if you know of one, let me know in the comments below and I’ll update the post but it feels to me like the addition of the 3 GetAlphaMask() methods to Image, TextBlock and Shape signals the idea that there’s not perhaps a simple, one-method-fits-all approach that the UI team could easily add (otherwise, they’d almost certainly have done so Smile).

Lights

In terms of experimenting with lights, I spun up a UI with an image in it that looked like this;

 <Grid
    Background="Black"
    x:Name="grid">
    <Image
      Stretch="UniformToFill"
      Source="ms-appx:///Assets/cat.jpg" />
  </Grid>

and some code that tried to create a PointLight and point it at that UI;

    void OnLoaded(object sender, RoutedEventArgs e)
    {
      var gridVisual = ElementCompositionPreview.GetElementVisual(this.grid);
      var compositor = gridVisual.Compositor;

      var pointLight = compositor.CreatePointLight();
      pointLight.CoordinateSpace = gridVisual;
      pointLight.Color = Colors.White;

      pointLight.Offset = new Vector3(
        (float)this.grid.ActualWidth / 2,
        (float)this.grid.ActualHeight / 2,
        100);

      pointLight.Targets.Add(gridVisual);
    }

and it didn’t seem to have much of an effect so I thought that I’d create the visual to display the image myself using the ImageLoader class that’s part of the Windows UI Labs samples. So, I took the Image element out of the UI altogether and wrote this code instead;

    void OnLoaded(object sender, RoutedEventArgs e)
    {
      var gridVisual = ElementCompositionPreview.GetElementVisual(this.grid);
      var compositor = gridVisual.Compositor;

      var imageLoader = ImageLoaderFactory.CreateImageLoader(compositor);

      var image = imageLoader.LoadImageFromUri(new Uri("ms-appx:///Assets/cat.jpg"));
      var surfaceBrush = compositor.CreateSurfaceBrush(image);

      var spriteVisual = compositor.CreateSpriteVisual();
      spriteVisual.Brush = surfaceBrush;
      spriteVisual.Size = this.grid.RenderSize.ToVector2();

      var pointLight = compositor.CreatePointLight();
      pointLight.CoordinateSpace = gridVisual;
      pointLight.Color = Colors.White;

      pointLight.Offset = new Vector3(
        (float)this.grid.ActualWidth / 2,
        (float)this.grid.ActualHeight / 2,
        100);

      pointLight.Targets.Add(gridVisual);

      ElementCompositionPreview.SetElementChildVisual(this.grid, spriteVisual);
    }

and that gave me more like the desired effect;

Capture

so I’m not sure what went wrong in my trying to apply this to an existing XAML element. Once the light is created, it’s not too difficult to animate it around a little and give it that sort of ‘roaming spotlight’ effect. I added in a little bit of code;

      var centreX = (float)this.grid.ActualWidth / 2.0f;
      var centreY = (float)this.grid.ActualHeight / 2.0f;
      var deltaX = centreX * 0.9f;
      var deltaY = centreY * 0.9f;
      var upperZ = 250;
      var lowerZ = 100;

      var animation = compositor.CreateVector3KeyFrameAnimation();
      animation.Duration = TimeSpan.FromSeconds(5);
      animation.InsertKeyFrame(0.0f, new Vector3(centreX - deltaX, centreY, upperZ));
      animation.InsertKeyFrame(0.25f, new Vector3(centreX, centreY - deltaY, lowerZ));
      animation.InsertKeyFrame(0.5f, new Vector3(centreX + deltaX, centreY, upperZ));
      animation.InsertKeyFrame(0.75f, new Vector3(centreX, centreY + deltaY, lowerZ));
      animation.InsertKeyFrame(1.0f, new Vector3(centreX - deltaX, centreY, upperZ));
      animation.IterationBehavior = AnimationIterationBehavior.Forever;

      pointLight.StartAnimation("Offset", animation);

and that creates a reasonable animated effect with very little code;

and it’s pretty easy to get that spun up and working without writing tonnes of code around it. There’s 3 other types of light to experiment with here, AmbientLight, DistantLight and SpotLight and I’d intend to try replacing my PointLight with the latter two to see how that works out.

But that’s for another post…

3 thoughts on “Windows 10, UWP and Composition–Light and Shade

Comments are closed.